React.js 실서비스 적용기

Coupang

Bali 팀

김태희

자기소개

  • 이름은 부끄럽게도... 김태희
  • JSP, Spring으로 웹 개발에 입문
  • 요즘은 node.js를 비롯한 javascript 를 파는 중...
  • coupang에서 bali 라는 팀에서
    여행 관련 서비스를 개발하고 있습니다.

 

react.js를 실 서비스에 적용하면서 느꼈던 내용들을 공유하고자 합니다.

react.js?

  • facebook이 만든 view engine
  • MVC 패턴에서 V만 담당
  • JSX
  • Component Base

대표적인 사용처

...and many more...

 첫 도입 시도

  • API 규격이 바뀜
  • 기존 Server Side 기반의 화면을 Client Side 기반으로 변경(레이턴시 이슈)
  • Component 단위로 만들어서 재사용가능하도록 만드는 것이 목적

목표

개발은 끝냈지만..

  • 환경적인 문제
  • 개발시의 문제점
    • 별도의 gulp watch를 실행해야하고
    • gulp를 통한 rebuild가 너무 오래 걸렸음
      (대략 8초)
  • deploy process 문제
  • 그외 자잘한 문제들

그러나 제일 큰 문제는

눈물을 머금고 DROP

그러다 찾아온 두번째 기회

신규 프로젝트

Async Client Side Rendering

  • 서비스 특성 상 외부 연동을 통한 API 호출이 많음
  • 데이터 조회 속도 때문에 ajax를 통한
    client side rendering이 필요해짐

반복되는 화면 요소

Home

호텔 목록

호텔 상세

호텔을 못 찾겠다!

팀 개발자들의 요구사항

  • 상식적이고 모던하게 개발하자
  • 요새 좋은 도구들 많던데 우리도 좀 써보자
  • IE 7 드랍좀....

PO 님의 요구사항

  • 뭘 쓰던 좋으니까 빨리 만들어라

Front End Tools

  • jsx transpiling 목적
  • ecma 2015의 적극적인 사용
    • class
    • arrow function
    • block scope variable
    • string template
  • ecma 2015의 import 키워드를 통해 js, css bundling + minify
  • 의존성 추적이 되서 js 패키지 리팩토링을 겁없이 할 수 있었음

webpack-dev-server

  • local에서 개발할 때만 사용
  • gulp watch를 대체
  • hot loader 지원
    • 파일 변경 감지해서 자동으로 현재 화면에 반영
    • 새로고침 주도 개발 일부 탈피

and dummy server

  • 별도의 tomcat 서버 없이 webpack-dev-server만 띄워서 개발할 수 있게 만드려는 게 목적
  • node.js의 express를 통해 url end point 및 api proxy, api mocking
  • selenium driver 기반의 e2e test tool
  • build / deploy에 대한 제약이 아닌 최소한의 보호장치로 사용
  • jenkins를 이용해 30분에 한번씩 주문 직전까지 태우는 테스트를 돌림

테스트가 깨지면....

React Developer Tools

  • Chrome, Firefox 지원
  • React Component 기준으로 DOM Tree를 볼 수 있음
  • 특정 Component의 현재 state 및 props를 볼 수 있고, state를 직접 수정할 수도 있다.
  • 데이터 흐름 추적에 큰 도움

esdoc

jenkins build를 통해 생성

  • jsx 지원
  • 일관된 코딩 스타일 유지를 위함
  • 일부 에러 사전 방지 효과

Architecture

Rendering Flow

프로젝트 분리

  • 기존엔 spring mvc project 아래에 위치해있던 client resource들을 모두 별도의 프로젝트로 분리
  • 사내 nexus에 npm publish 해서 다른 프로젝트에서도 npm install을 통해 쓸 수 있는 구조
  • client와 server가 완전히 분리됨으로써 java외 서버로도 서비스 가능해짐

개발 시에는...

  • webpack-dev-server를 통해 bundle.js, bundle.css를 제공 받음

배포 시에는...

  • 빌드에 gradle을 사용 중
  • 서버 환경은 spring + tomcat
  • webpack build를 통해 spring project의 WEB-INF 안에 bundle file들을 생성함
  • 빌드 시 gradle task를 통해 npm build 명령어를 수행

Server Side

  • Spring MVC로 url end point 처리
  • 초기 rendering에 필요한 데이터를 model에 적재해서 사용
@RequestMapping({"/hotels/{productId}", "/overseas/hotels/{productId}"})
@CoupangWebLayoutEnable
public String hotelDetail(@PathVariable Long productId,
		Model model) {

    HotelDto hotelDto = overseasHotelService.findOverSeasHotel(productId);
    if (hotelDto == null) {
        return "pages/pc/hotel/overseas/hotelNotFound";
    }

    model.addAttribute("hotel", hotelDto);

    createMetaInfo(model, hotelDto);

    return "pages/pc/hotel/overseas/hotelDetail";
}

Server Side

  • server side handlebars로 초기 rendering
  • OG 태그를 위한 meta tag 처리
  • Spring model에 넣은 JSON을 rendering
{{#partial "head"}}
  {{#unless hotLoaderEnable}}
    {{assetStyle "bundles/pc.overseas.tdp.HotelDetailController.bundle.css"}}
  {{/unless}}
{{/partial}}
 
{{!-- 메뉴별 바디 영역 --}}
{{#partial "body"}}
  {{!-- 해당 영역 아래에 react를 통한 client side rendering이 이루어진다. --}}
  <div id="booking-contents" class="booking-main-wrapper"></div>
{{/partial}}
 
{{#partial "script-page"}}
  {{!-- 서버에서 spring의 model에 넣어준 값들을 application/json 형식으로 미리 렌더링하여 가져다 쓰는 구조 --}}
  <script id="hotel" type="application/json">{{json hotel}}</script>
  <script id="user" type="application/json">{{json user}}</script>
  {{#if hotLoaderEnable}}
    {{assetScript "devModeChecker.js"}}
    <script src="http://{{serverName}}:3001/bundles/pc.overseas.tdp.HotelDetailController.bundle.js"
            type="application/javascript"
            onerror="handleDevModeScriptLoadingFail()"></script>
  {{else}}
    {{assetScript "bundles/pc.overseas.tdp.HotelDetailController.bundle.js"}}
  {{/if}}
{{/partial}}

multi entry point

  • SPA 방식이 아닌, 각 화면별로 webpack entry point 를 분리
  • ~Controller라는 접미사를 붙여서 사용
  • 초기 데이터 로딩 및 주요 state 관리, 최초 렌더링을 담당
import React from 'react';
import ReactDOM from 'react-dom';

import JSONLoader from 'utils/JSONLoader';
  
export default class HotelDetailController extends React.Component {
  ... controller react component 구현
  
  state = {    
    hotel: JSONLoader.load('hotel') 
  };
  
  componentDidMount () {
    // 컴포넌트가 렌더링된 이후에 1회 실행
    // ajax를 통해 비동기로 데이터를 가져오는 코드도 보통 이곳에 들어옴
  }

  render () {
    // rendering code
    return (
      <div>hello bali!</div>
    );
  }

  // 이벤트 핸들러들
  handleXXX() = (e) => {
  };
}
  
// 실질적인 렌더링은 여기서 일어남
try {
    ReactDOM.render(<HomeController />, document.getElementById('booking-contents'));  
} catch (e) {
    console.error('[Home rendering error]', e);
}

Ajax Pattern

  • Root Component 에서 ajax 처리를 모두 담당하고, 실제 데이터가 필요한 하위 Component까진 props로 넘기는 구조
  • props 로 넘기는 노가다가 있지만, 나중에 데이터 흐름 파악하기가 쉽다.
  • http://andrewhfarmer.com/react-ajax-best-practices/

무엇이 좋아졌나?

Client와 Server의 완전한 분리

  • Server Side에 의존적이지 않은 Client 구조
  • 개발된 UI Component를 다른 페이지, 다른 프로젝트에 재사용하기가 매우 좋아짐

Component Tree

  • 실제 그리는 영역별로 Component가 쪼개져 있기 때문에 고쳐야 하는 부분이 명확해짐
  • 쪼개진 Component 별로 재사용이 수월

propTypes

  • propTypes 를 통한 props의 데이터 형 정의를 통해 잘못된 데이터 흐름을 미연에 방지
  • 소스코드 가독성도 좋아지는 효과

재사용 가능한 Component

<PCQuickSearch />

같은 컴포넌트에 CSS만

다르게 입힌 것

그외에 재사용된 Component

<StaticGoogleMap />

<HotelStars />

<PriceText />

<HotelDecriptionText />

<Breadcrumb />

..and many more...

프로젝트 완료 후,

팀원들에게 물어봤습니다.

좋아진 점

  • state와 props를 통해서만 Component 간의 데이터가 흐르므로 데이터 흐름 파악이 용이해서 좋았다.
  • propTypes를 통한 type checking 
  • 일관된 this context
  • state update시 알아서 화면이 바뀌므로 rendering에 신경 쓸 부분이 줄어듦
  • 자연스럽게 팀 내 코딩 컨벤션이 생김
  • 코드 퀄리티가 전체적으로 좋아짐
  • 팀 내 다른 개발자가 작성한 Component를 가져다 쓰기가 매우 쉬워서 좋았다.

아쉽거나 개선되어야 할 점

  • 익혀야 할 기술이 많고, 개념 전환이 어려웠다.
  • 개발할 때 서버를 두대 띄워야 한다.
  • 간단한 DOM 조작으로 해결될 문제도 복잡하게 처리해야 하는 경우가 생김
  • input 처리
  • 0.14 기준 react가 자동으로 만들어주는 span의 문제
    => 공용 css 등에 span 자체에 걸려있는 css가 있다면 자연스럽게 side effect 생김
  • build에 사용된 npm modules의 완성도가 떨어져 삽질을 했던 경우도.
    • 어느날 갑자기 빌드가 안 되는 경우도 있었다.
  • windows 7 문제

Tips

windows 7 개발 환경 문제

  • npm 2.x.x를 쓸 경우 node_modules 의존성의 긴 경로명이 문제를 일으키기 때문에 반드시 npm 3.x.x를 써야함.
  • 그럼에도 불구하고 일부 module은 windows 7에서 정상동작을 안 하는 경우가 있다.
  • 전체적으로 watch 성능이 떨어지는 느낌적인 느낌

Client Side Rendering의 숙명

  • 그것은 화면 깜빡임
  • server side rendering하는 곳에서 wire frame이라도 잡아두는 것이 좋다.
<div id="booking-contents">
  <div class="live-search-section clearFix">
    <div class="dummy-block" style="margin-bottom:5px;height:48px"></div>
    <div style="background-color:#eaf6ff;height:59px;margin-bottom:10px;"></div>
    <div class="filter">
      <div class="dummy-block" style="background-color:#fff6d2;width:240px;height:340px;"></div>
      <div class="dummy-block" style="width:240px;margin-top:10px;height:358px;"></div>
    </div>
    <div class="booking-hotel-list">
      <div class="live-list">
      <div class="dummy-block" style="background-color:#fff;margin-top:35px;height:30px;"></div>

      <div class="dummy-block" style="background-color:#fff;height:202px;margin-top:8px;">
        <div style="float:left;width:198px;height:200px;border-right:1px solid #ccc;"></div>
      </div>
      <div class="dummy-block" style="background-color:#fff;height:202px;margin-top:8px;">
        <div style="float:left;width:198px;height:200px;border-right:1px solid #ccc;"></div>
      </div>
      <div class="dummy-block" style="background-color:#fff;height:202px;margin-top:8px;">
        <div style="float:left;width:198px;height:200px;border-right:1px solid #ccc;"></div>
      </div>
      <div class="dummy-block" style="background-color:#fff;height:202px;margin-top:8px;">
        <div style="float:left;width:198px;height:200px;border-right:1px solid #ccc;"></div>
      </div>
      <div class="dummy-block" style="background-color:#fff;height:202px;margin-top:8px;">
        <div style="float:left;width:198px;height:200px;border-right:1px solid #ccc;"></div>
      </div>
      </div>
    </div>
  </div>
</div>

 Dummy Markup 처리

Ajax Control

  • 실시간 해외호텔 예약 서비스의 경우 ajax 요청이 빈번하게 일어남
  • chrome 기준 http 동시에 맺을 수 있는 갯수는 6개
  • 직접 Queue 처리하거나 bluebird.js로 처리함
    • Promise.map과 concurrency 옵션 이용

우리의 숙적 Legacy IE

  • react.js 0.14 버전은IE 8부터 지원
  • 차기 버전인 15 버전 부터는 IE 9부터 지원
  • 힘냅시다 여러분...

Q & A

많이 써주세요...

감사합니다!

React.js 실서비스 적용기

By 김태희(로토/Roto) [Travel Systems CX] ­