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분에 한번씩 PG 결제 직전까지 태우는 테스트를 돌림
테스트가 깨지면....
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부터 지원
- 힘냅시다 여러분...