여행준비물

짐쌀때마다 고민되는 준비물 목록, 저의 목록은 다음과 같습니다.

  • 여권
    • 방문 비자 필요 시 – 사진 * 2
  • 비행기 티켓
    • 숙소 바우쳐 (입국시 작성하는 문서에숙소 주소 입력을 받기 때문에 쉽게 꺼낼 수 있는 곳에 두세요)
    • 폰에 사진이나 문서로 들고가도 대부분 되지만 국가에 따라서는 출력된 문서만 받는 곳이 있기 때문에 준비는 해두는 것을 추천
  • 외화 + 비상금 달러
  • 우산
  • 휴지
  • 셔츠
  • 보조가방
  • 안경닦이
  • 충전케이블
    • 충전용 배터리
    • 충전 어댑터
    • 국가별 어댑터
  • 파우치, 비닐 (입었던 옷들이나 액체류를 한 번 감싸면 좋아요)
  • 면도기
  • 치약/칫솔
  • 샴푸/바디워시
    • 수건: 게스트하우스나 호스텔인 경우 수건을 돈내고 빌려주기도 하나 없는 곳도 있습니다.
  • 바지
  • 티셔츠
  • 동남아 가더라도 긴팔 챙기기(에어아시아 같이 내부 기온을 낮게 운영하는 항공사나 동남아의 커피숍을 생각하면 추워요)
  • 손수건
  • 부채
  • 소화제, 설사약
  • 모기약
  • 노트
  • 볼펜 / 샤프
  • 카메라

패스 웹 회고

들어가는글

본 글은 패스 웹 프로젝트를 회고하면서 고민했던 기술적인 부분을 정리한 글입니다. react, redux에 대해 어느정도 알고 있다고 가정하고 작성되었음을 참고해주시기 바랍니다.

Mimar_Sinan,_architecte_de_Soliman_le_Magnifique

프로젝트: 미마르 시난(Mimar Sinan)

패스 웹 프로젝트는 웹으로 패스 서비스를 이용할 수 있게 제공함으로 윈도우 폰이나 타이젠 폰 등의 지원하지 않는 폰에 대한 영역을 넓히자는 목적으로 프로젝트를 시작하게 되었습니다.(만… 목표와 현실이 달라졌답니다.) 프로젝트 명은 미마르 시난(Mimar Sinan은 Sinan the Architect의 현대 터키어)이라는 오스만 제국의 건축가의 이름을 사용했으며 패스의 주 서비스 지역인 이슬람 국가 인도네시아를 고려하여 선택하였습니다.

들어가는 글

react + redux를 쓰는 경우 딱 하나의 정답이 없다는 얘기들을 많이 하는데요. 여기에서 소개하는 내용들이 프로젝트 진행하면서 여러 고민 끝에 결론을 내려 적용한 내용이긴 합니다. 그러나, 정답이 아닌 이상 틀린 부분도 있을 수도 있고 더 나은 해결책이 있을거로 생각됩니다. 아무쪼록 보시면 얘는 이렇게 생각했구나로 봐주시면 감사하겠습니다. 혹시 더 나은 의견 있으시면 덧글로 부탁드릴께요. 감사합니다.

기반기술

선택한 기술 기반의 기준은 신기술 적용을 통하여 기술 향상을 얻으면서 신기술 중에도 어느정도 안정화된 기술을 사용하여 기술 적용시 실패하지 않도록 고려를 하였습니다. (만.. 취향이 리액트를 좋와하는지라..)

SkillSet

Gulp를 사용하는 이유는 ES6코드를 변환하면서 노드 서버와 관련 있는 코드가 변경된 경우 노드 서버를 재시작(nodemon)하기 위해서 Gulp를 적용하였고 마크업의 css를 마크업 git 저장소에서 가져와서 로컬에 반영하거나 빌드 결과물들이나 이미지 들을 CDN으로 올리고 버전 처리하는 부분도 모두 Gulp를 통하여 진행하고 있습니다.

혼자 개발하고 있기 때문에 기존 코드가 정상적으로 동작하는지를 검증하기 위해서 mocha를 기본적으로 사용하고 가상돔은 jsdom으로 리액트 코드는 enzyme으로 사용하고 있으며 문법 검사는 eslint를 통하여 확인합니다.

* react에는 코드 변경시 자동으로 이를 반영하는 Hot loader가 제공되고 있는데요. 노드 서버의 코드를 재시작하는 경우에는 페이지를 새로 고치는 것 외에는 방법을 찾을 수 없어서 react 개발 툴들의 대부분은 포기하고 작업하였습니다.

폴더의 구조

folder_structure빨간색 마크는 빌드 결과물이 위치하는 곳으로 build에는 서버 빌드 결과물이 static/js에는 클라이언트 빌드 결과물이 배치됩니다. 처음에는 build 폴더에 모든 결과물을 두었으나 CDN에 static 폴더 통째(js 빌드 결과물 포함)로 올리고 버전 관리를 원할하게 하는 것과 css/images의 경우는 일반 파일들 처럼 git으로 관리가 필요했기 때문에 두 개를 분리하는 방향으로 잡았습니다.

초반에는 client/server를 최상위에 두어서 분리를 하였으나 서버 렌더링을 작업 하다보니 서버 렌더링 코드가 클라이언트 코드를 참조하기 위해서는 항상 최상위 경로로 접근 하는 이슈가 발생하면서 이 둘을 src 폴더 아래로 함께 넣게 되었습니다.

config은 빌드 버전에 따라서 필요한 설정들을 넣어주기 위한 설정 폴더, githook은 커밋/푸시 전에 코드나 저장소를 체크하는 코드가 들어있습니다 support의 경우 테스트를 위한 jsdom 초기화 코드나 공통으로 사용되게 되는 mock 데이타나 샘플 응답을 담고 있으며 webpack은 gulp로 웹팩을 실행할 때 필요한 폴더 경로나 유틸 코드들이 들어있습니다.

JavaScript 파일관리

js파일들은 최상단 폴더에 index.js 파일을 두고 폴더아래의 파일들 중 필요한 파일들만 골라서 index 파일에서 import후 다시 export하는 형태로 제공을 함으로서 해당 폴더 아래 영역에서만 사용하는 컴포넌트와 외부 노출 컴포넌트를 구분시켰습니다. 불필요한 부분의 노출을 감추고 import 자동완성시 파일을 모두 알지 않아도 찾기가 쉬워지는 장점이 있습니다.

index_js

그리고, babel에서 import 구문을 변환하는 과정에서 의존 트리에 따라서 코드를 변환해주게 되는데요. 너무 많은 의존 관계는 의도치 않게 빌드시에 코드가 빠지는 오류도 방지해줄 수 있습니다. 혹시 빌드 결과물에서 import로 코드를 포함시켰음에도 빠진 경우는 의존 관계일 확률이 높습니다.

노드 서버 구조

server_structure

노드 서버는 API 서버에 API를 호출하고 응답을 받아서 전달을 해주는 기본 역할에 서버 렌더링이 더해진 구성을 가지고 있습니다. 기본적인 API응답은 호출 url에 /a/를 앞에 추가하여 여기로 들어오는 응답은 API 라우터를 거쳐서 전달되게 됩니다.

서버 렌더링의 경우는 크게 두 가지 요소로 나눠지는데요. 하나는 처음 해당 페이지 url로 들어가는 경우이고 처음 페이지를 로딩한 후, 페이지를 라우팅하다가 새로 고침하는 경우입니다. 로그인이 필요한 페이지의 경우 처음 접근하였을 때 비로그인 상태면 로그인 페이지로 라우팅 시켜주고 이미 로그인 되어 있다면 페이지를 재구성할 수 있도록 필요 API들을 함께 다시 호출해줘야 합니다.

예를 들면, 패스 웹에서는 친구 정보를 로그인시에 한 번만 호출하여 저장을 해두는데요. 이미 로그인된 상태에서 페이지를 새로고침 하는 경우에는 페이지 진입시 필요한 API 호출과 로그인 시에만 호출하는 API들을 함께 불러야지만 화면이 제대로 보여지게 됩니다.

응답 결과들은 normalization 처리후 클라이언트로 전달되게 되는데요. (normalization 라이브러리를 사용하지 않고 직접 처리하게 된 이유는 패스 API 응답이 부분 normalization처리가 되어 있어서 라이브러리 사용시 이슈가 있었습니다.) 처음에는 순수하게 normalization 처리만 진행하였던게 웹 클라이언트 포멧에 맞게 응답을 변환하는 역할을 맡게 되었습니다. (API 응답의 대부분이 앱 먼저로 작업이 되다보니 힘이 없는 웹 서비스의 경우 앱의 응답에 맞추게 되더군요.)

클라이언트의 계층 구조

Client-hierarchy

클라이언트 파트는 기본적은 react + redux를 사용한 구조인데요. redux에서 생성된 상태 트리를 App에 전달해주고 각 페이지에 해당하는 컨테이너 객체들이 이를 바탕으로 필요한 컴포넌트들을 생성하는 구조를 가지고 있습니다. 컨테이너에서 건내지는 액션 함수들이 컴포넌트에서 실행되면 액션이 리덕스로 전달되어 미들웨어를 거쳐 비동기 등의 동작을 실행하고 결과 액션이 리듀서들을 거치면서 새로운 상태 트리를 만들어 App으로 전달되는 형태입니다.

상태 트리

state

redux에서 생성된 상태 트리는 컨테이너와 컴포넌트의 필터를 통해 필요 데이타들만 갈무리하여 전달되게 됩니다. 위의 이미지에서는 현재 상태에서의 전체 글과 사용자들의 데이타가 들어있는 상태 트리에서 현재 페이지의 글(패스에서는 모멘트라고 부릅니다.)과 사용자 덧글 정보들만 컨테이너로 넘기고 이를 다시 컴포넌트들이 필요한 값들만 넘겨서 최소한의 상태만 들고 있도록 구성하였습니다.

redux를 적용하기로 결정하면서 가장 고민에 쌓였던 부분은 현재 화면에 대한 상태를 들고 갈 것인지 여부였는데요. 처음에는 정확한 판단이 서지 않았기 때문에모든 상태들을 store로 보낸 후 처리하는 것으로 시작을 하였지만, 이 경우에는 빈번한 리액트 업데이트가 발생하는 이슈가 있어서 외부 컴포넌트와 연동이 필요하거나 비동기 동작이 필요한 경우에만 view 상태에 저장하고 외부 영향이 없는, 즉 캡슐화 할 수 있는 상태라면 컴포넌트는 내부에서 처리하도록 최종 결정하였습니다.

Music-state

예를 들면 재생 중인 음원 정보는 redux의 상태 트리에 저장을 하고 (타임라인에 여러 음원이 있어도 재생이 하나만 되어야 한다는 스펙이 있씁니다.) 트랙 재생을 시작 한 후, 얼마나 재생했는지 여부 (재생 버튼 주위의 원형바)는 내부 컴포넌트에서만 상태를 저장합니다.

view 상태에 저장되는 값들로는 현재 페이지의 에러와 현재 열려있는 레이어나 토스트팝업 정보, 스크롤 정보(스크롤 정보들은 패스의 글을 현재 사용자가 읽었는지 여부나 페이지 이동 전/후 스크롤 위치 복원을 위해서 사용), 재생되고 있는 음원이나 동영상 정보 등을 저장하고 있습니다.

Ducks 모듈 패턴

패스 웹의 reducer들은 세 가지 형태로 구성되어 있습니다.

  • middleware: store의 리듀서를 타기 전/후를 처리할 수 있는 미들웨어에서는 api처리  timer, 이미지 라이브러리 로딩과 같은 비동기 동작과 i18, 경로 처리를 담당합니다.
  • modules: Ducks 모듈 패턴이 적용되는 코드로 액션 명령어, 액션 함수들, 리듀서 필요시에는 미들웨어도 함께 포함시켰습니다. 목적에 따른 코드 배치로 가독성이 증가되는 것과 외부에서 필요없는 액션 명령어들은 감출 수 있는 장점이 있습니다.
  • reducers: 순수한 reducer 함수들로 구성되어 있습니다.

로그아웃 페이지

사용자가 로그아웃 버튼을 누르면 로그아웃 api를 호출한 후, 응답을 받아서 데이타 초기화 및 로그아웃 페이지로 이동을 하게 됩니다. 처음 구현을 시작 하였을 때 고려했던 방법은 로그아웃 명령어를 사용하는 방법이었습니다.

LOGOUT이라는 명령어를 받은 reducer들이 현재 저장되어 있는 데이타들을 초기화 시키고 초기화된 데이타를 받아서 react에서 화면을 새로 그리는 방법이었는데요. 이 방법을 사용하는 경우, 컨테이너에서 로그인 여부를 다시 한 번 체크해줘야 하는 이슈가 있었습니다. 페이지 진입 시 비로그인시에 리다이랙션을 처리해주는 코드가 있는 상태에서 GNB의 로그아웃 버튼을 누르게 되는 경우 페이지를 새로 고치거나 컨테이너 상단에서 로그아웃을 체크하는 중복 코드가 들어가야 하는 이슈가 있었는데요.

로그 아웃 페이지를 따로 두고 페이지 전환으로 처리를 하는 경우에는 로그아웃 여부를 체크하는 코드는 routes의 onEnter에서만 처리를 하게 됩니다. 예를 들어 아래의 알림 페이지에서 로그 아웃을 누르는 경우 로그아웃페이지로 이동하면서 상태 트리들을 모두 초기화 시킨 후, 페이지를 다시 돌아오면서 onEnter에서 로그인 여부를 체크하고 로그인하지 않은 경우 로그인 페이지로 이동시켜주게 됩니다.

Login

React 컴포넌트 코딩 룰

React 컴포넌트를 어떻게 짤지에 관해서는 프로젝트나 사용하는 문법(ES6, ES7) 작성자의 관점에 따라서 달라질 수 있다고 생각합니다. 여기에서 소개 드리는 룰은 여기에서는 이렇게 사용하였구나 정도로 참고로 생각하고 봐주셨으면 합니다.

ReactBasic

  1. 컴포넌트를 그리기 위한 데이타들은 모두 속성(property)를 통해서만 받습니다.
  2. 화면의 변경은 속성을 통해서 받은 함수(action)의 실행을 통해서 변경되며 다른 컴포넌트에 영향을 주지 않는 범위의 변경에 한해서는 내부 상태(state)로 처리를 할 수 있습니다.
  3. 컴포넌트가 생성될 때 정의되고 이 후 변경이 안되는 값이 있다면 생성자단에서 캐시를 합니다.
  4. DOM을 다루는 코드들은 componentDidMount와 componentWillUnmount에서 제어합니다.
  5. 컴포넌트들에 필요로 하는 속성코드들은  props()를 통해서 상태 트리에서 컴포넌트가 필요한 값들을 추출한 후, get()을 통해서 컴포넌트를 생성해서 넘겨줍니다.

1~4번은 react를 사용해보시거나 다른 항목들에서 설명하는 부분이라 생략을 하고 5번에 대해서 설명을 드리겠습니다. react 컴포넌트를 사용하다보면 컴포넌트의 복잡도가 증가할 수록 설정하는 값들의 증가와 컴포넌트에 필요한 값을 찾고 액션 함수 실행시 필요한 값들을 부분 바인딩하는 작업들을 수행하는 코드들이 너무 많아졌습니다.

keypropsget

그래서, 선택한 방법이 컴포넌트가 자기 자신에 필요한 속성 값들을 상태 트리로부터 직접 가지고오도록 하였습니다. connect()안에서 정적함수인 Component.props(state)를 호출하여 속성을 정의한 객체를 얻어와서 부모컨테이너의 속성에 설정해줍니다. 필요한 속성들을 그룹화 시킴으로서 부모 컨테이너에 정의해야 하는 속성들을 하나로 묶어주게 되었죠. 액션함수들의 경우 dispatch함수가 바인딩 되는 것을 기다려야 하기 때문에 컴포넌트 생성 시점인 get() 호출 시 인자로 받는 부모의 속성 값을 받는 형태로 작성하였습니다.

DOM 다루기

react에서 제공하는 기능만으로는 DOM의 모든 기능을 제공하지는 않기 때문에 직접 DOM을 다루는 코드가 들어갈 수 밖에 없는데요. 예를 들면 전체 화면에 딤드화면을 깔고 레이어를 띄우는 경우 body태그에 fixed style을 설정하는 클래스 명을 추가/삭제 한다거나 스크롤 위치를 지정하는 등의 작업이 있을 수 있습니다.

DOM을 다룰 때 주의 할 점은 크게 세 가지 정도 있었는데요.

  1. componentDidMount()에서 DOM에 접근을 하고 이벤트 등록이나 클래스 명을 추가하며
  2. componentWillUnmount()에서 DOM 과 이벤트 등록 해지나 클래스 명을 삭제하는 초기화 작업을 수행합니다. 아래의 코드는 윈도우 크기 변경을 감지하기 위한 컴포넌트인데요. DOM에 접근할 수 있는 마운트된 시점에서 최초 크기 측정 및 이벤트 등록을 시행하고 언마운트되는 시점에서 이벤트를 해지합니다.
    window_size
  3. DOM의 레퍼런스를 얻어올 때는 해당하는 react 엘리먼트의 ref 를 이용해서 react 컴포넌트 로딩시에 엘리먼트를 얻어와서 컴포넌트에 저장해두고 사용합니다. (https://facebook.github.io/react/docs/refs-and-the-dom.html)
    ref={(c) => {this.form = c;}}

테스트

테스트는 mocha라이브러리에 expect를 사용하여 확인을 하고 있습니다. react 코드는 airbnb에서 만든 enzyme(http://airbnb.io/enzyme/index.html)을 사용해서 작성하고 있습니다. enzyme에서 제공하는 리액트 컴포넌트 렌더링 함수는 render, shallow, mount가 있는데요. 각각의 함수가 정적인 화면을 그려서 리액트가 제대로 그려졌는지 확인은 render를 컴포넌트의 렌더링과 액션까지 확인할 수 있는 얇게 그려주는 shallow, 자신을 포함한 자식 컴포넌트의 모든 기능이나 컴포넌트의 전체 기능을 다 확인하기 위해서는 mount를 사용할 수 있습니다. mount를 사용하는 경우 테스트 전체 시간이 오래 걸릴 수 있기 때문에 테스트 하고 싶은 범위나 전체 실행시간을 고려해서 사용하셔야 합니다.

test_sample
Dialog를 생성해서 속성으로 주어진  title과 content가 반영되었는지를 확인하고 버튼을 누른 경우 동작도 확인합니다.

리액트 코드에서 DOM을 호출하는 코드가 있는 경우 커맨드라인 상에서 테스트를 그냥 실행하게 되는 경우 에러가 발생하게 되는데요. 이런 경우는 jsdom을 사용하여 필요한 DOM요소들 window 등을 미리 생성해두면 에러없이 동작시킬 수 있습니다. 다만, 버전이 업데이트 되면서 진짜 DOM처럼 제약요소가 많아져서 버전 업데이트시 테스트 코드가 깨지는 경우도 나오더군요. (저의 경우에는 스크롤 위치를 jsdom에서 변경시켜주면서 스크롤에 따른 동작을 테스트하는 코드를 짰었는데요. 버전 업데이트 하면서 스크롤관련 속성을 설정(set)할 수 없게 바뀌면서 에러가 났었습니다.)

빌드

모든 운영체제를 고려하는 오픈소스의 빌드 시스템과는 달리 회사 전체의 환경이 맥으로 통일이 되어 있어서 선택한 방법은 gulp와 node의 execSyn()를 사용하여 command로 실행이었습니다. gulp로 webpack이나 nodemon을 실행했기 때문에 경로나 실행 순서 등을 코드로 작성할 수 있었던 장점이 있었지만 마찬가지로 코드이기 때문에 코드 관리가 필요했었습니다. (지금 다시 보니 정리가 더 필요하네요…)

웹 프론트에서 빌드에서 주로 사용하는 기능중의 하나가 이미지, css, js 정적 파일들을 CDN에 올리고 html 파일안에 갱신된 리소스들로 갱신해주는 기능일건데요. 패스 웹의 빌드에서는 이렇게 풀었습니다.

  1. 임시 폴더를 생성한 후 js,css,이미지 리소스를 묶음으로 받으 후,
  2. 필요 없는 파일들을 삭제 한 후,
  3. git의 hash를 리턴해주는 git-rev-sync를 사용하여 이번 배포(커밋)를 기준으로 이미지를 갱신할 수 있는 힌트(CDN에서는 폴더 명으로 사용)를 생성하고
  4. CDN에 커맨드 명령어를 execSync를 사용하여 업로드 후
    1. CDN이 오픈 스택을 사용하여 swift 명령어로 업로드 할 수 있었고
    2. 실행 순서를 명확하게 학 위해서 동기로 커맨드를 실행할 수 있는 execSync를 사용하였습니다.
  5. html과 css에 선언되어 있는 경로들을 3에서 생성한 힌트로 교체한 후 결과물을 생성
    ( gulp 스트림에서 문자열 교체를 하기 위해서 gulp-replace를 사용)

이 외에도 재미있게 쓴 모듈 중 config라는 모듈이 있는데요. config을 사용하게 되면 NODE_ENV의 단계에 따라서 설정 정보를 다르게 얻어올 수 있는 모듈인데요. config을 es6로 작성된 gulp로 읽거나 쓰려고 하면 json 포맷만 처리할 수 있기 때문에 yml 포맷으로 된 경우는 주의가 필요합니다.

마무리

작년 6월 중순(디자인 완료 시점)에 시작하여서 올해 2월 릴리즈까지 (중간에 다른 프로젝트로 한 개월 중단되기도 했었구요.) 거의 혼자 진행하였던 프로젝트라 새로운 기술들과 패턴을 따라 잡지 못한 부분도 많이 있을 겁니다. 이런 문제를 이렇게도 풀 수 있구나 하는 참고로 봐주시면 좋을 것 같습니다. 긴글 읽어주셔서 감사합니다.