패스 웹 회고

들어가는글

본 글은 패스 웹 프로젝트를 회고하면서 고민했던 기술적인 부분을 정리한 글입니다. 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월 릴리즈까지 (중간에 다른 프로젝트로 한 개월 중단되기도 했었구요.) 거의 혼자 진행하였던 프로젝트라 새로운 기술들과 패턴을 따라 잡지 못한 부분도 많이 있을 겁니다. 이런 문제를 이렇게도 풀 수 있구나 하는 참고로 봐주시면 좋을 것 같습니다. 긴글 읽어주셔서 감사합니다.

Redux

Redux의 소개 글을 보면 왜 Redux를 써야 하는지 어떤 생각으로 설계를 하였는지를 설명하면서 코드 외에도 어떻게 짜라는 방법을 설명해주고 있다. Redux 글과 강의를 보다 내 나름대로의 정리가 필요해서 글로 정리하였다. 이 글은 Redux에 대한 소개를 다루지 않으며 Redux todo 프로그램을 기준으로 왜 이렇게 짜여지는 어떤 부분에 장점이 있는지를 다룬다.  todo  소스코드는 egghead.io에 소개된 redux 강의를 기준으로 하였으며(소개 글의 코드와 다르다) redux기본은 redux 사이트를 보는것을 추천 한글로 된 소개글은 태곤님의 소개글을 추천한다.

기능

Screen Shot 2015-12-18 at 10.57.17 PM일단, 예제 프로그램으로 소개되고 있는 todo 프로그램의 기능은 다음과 같다.

  1. 상단의 input을 통해서 todo를 추가 할 수 있다.
  2. 추가된 todo는 중간의 todo 목록으로 보여지며 todo를 클릭하면 완료 여부를 표시할 수 있다.
  3. 하단의 필터는 입력한 todo 목록을 상태별로 나눠서 볼 수 있게 해준다.

위 기능들이 동작을 할 때 현재 프로그램이 어떤지를 저장한게 상태(State)라고 한다. todo 프로그램에서는 해야  할 일을 정의한 todo와 이들의 목록인 todos 그리고 현재 화면에서 어떤 타입의 todo를 보여주고 있는지 여부인 visibilityFilter가 todo 프로그램의 상태로 저장되는 값이다.

State

redux_model

프로그램의 모든 상태들이 이 상태 트리 하나에 저장된다. MVC의 모델이 지녀야 할 값 외에도 프로그램의 현재 화면 상태, View의 상태도 이 트리에 모두 저장한다. (Single source of truth는 redux의 삼원칙중 처음에 나온다.) 상태 트리를 하나에 모두 저장하게 되면 현재 프로그램이 어떤 상태인지, 어떻게 변했는지를 알기가 쉬우며 디버깅도 쉬워진다. 프로그램 구현시 어려운 부분인 do/undo도 상태의 전/후를 저장하면 쉽게 처리할 수 있다.

Redux_동기화케이스

복수 개의 모델을 가지는 경우 단위별 개발이 빠르고 쉽게 만들 수도 있지만, 모델 상호간에 의존된 값을 사용하게 되는 경우 동기화를 해줘야 하는 이슈가 발생할 수도 있고 동기화를 하게 되는 경우는 각 모델간의 생성 순서나 동기화 순서가 이슈가 될 수 있다. Flux를 사용할 때 복수 개의 class로 만들어진 store를 사용할 때, 사용자의 요청에 따라서 응답하는 store들이 다른경우 서버 렌더링하는 경우에 응답에 따른 store 생성순서를 처리/관리하는 빙용이 생기게 되는데 한 개의 트리에 모든 상태를 저장하게 되면 이러한 이슈들을 해결할 수 있게 된다.

상태 트리를 만들 때는 트리의 구성은 다음의 기준을 제안하고 있다.

  • 어떠한 중첩된 구조 없이 가능한한 정규화된 상태를 유지한다.
    (샘플 코드에서는 nomalizr를 사용하여 중첩 데이타를 펼쳐서 사용)
  • 모든 엔터티들이ID를 키로 가지게 하고 엔터티 또는 리스트에서 특정 엔터티를 참조하고 싶을 때는 ID를 사용한다.
  • 더 자세한 것은 normalizr의 기준을 참고한다.

reference

Action

Ajax 응답을 받거나 사용자 인터랙션이 발생하였을 때 변경사항을 상태 트리에 알려주는 것은 액션(action)이다. 액션은 순수 객체로 만들어진다.  액션은 무엇이 변경되었는지를 표현하기 위한 type을 가진다. 액션은 최소한의 데이타로 표현되어야 하며 이 액션이 적용되었을 때 상태가 어떻게 바뀌었는지 유추할 수 있어야 한다.

todo 프로그램에서 액션은 다음의 3가지를 정의하고 있다.Redux_Actions

액션을 정의할  때는 몇 가지 컨벤션을 권장한다.

  • 액션의 타입은 문자열 상수 (const string)로 정의한다.
    • 심볼(Symbol)을 사용할 수도 있지만 문자열과 달리 기록하고 다시 실행하기 어려운 문제점을 가지고 있기 때문에 권장하지 않는다.
  • 상수를 사용하는 것을 권장한다.
    • 액션종류들을 한 곳에 모음으로써 어떠한 상수가 있는지 확인하기 쉽다.
    • 액션들의 이름을 짓는경우에 대한 일관성을 가질 수 있다.
    • PR에 올라오는 액션종류(추가,삭제,수정)에 따라 기능이 얼만큼 작업이 되었는지 같은 팀원이 인지하기 쉬워진다.
    • 상수를 가져다 사용하기 때문에 오타가 발생할 여지를 줄인다.
  • 액션을 생성할 때는 문자열 리터럴을 그대로 사용하기 보다는 액션을 생성하는 메소드를 사용하는 것을 권장한다.
    • 액션을 생성하는 곳이 여러 곳인 경우 함수만 변경 함으로서 한 번에 수정이 가능하다.

Screen Shot 2015-12-31 at 9.31.16 AM

Reducer

어떠한 액션에 대해서 상태를 어떻게 변경할지를 결정하는 것은 Reducer다. Reducer는 함수로 구성이 되며 이전 상태와 액션을 받아서 변경된 상태를 리턴한다. Reducer는 순수함수로 부수 효과가 없는 함수여야 한다. 즉, 자신이 처리할 액션인 경우 인자로 받은 상태에 변경 후의 새로운 상태를 만들어 리턴함으로서 참조 객체 변경으로 인한 오류를 막을 수 있게 된다. Reducer는 체인 형태로 동작해야 하기 때문에 자신이 처리하지 않는 액션에 대해서는 항상 인자로 받은 상태를 리턴해줘야 한다.

Redux_reducer

위의 그림을 예로 리듀서를 설명해보면 ADD_TODO 액션과 상태 트리를 받아서 새로운 상태 트리를 만들었지만 액션과 관련된 트리만 새롭게 변하고 필터와 관련된 좌측 부분은 그대로 유지된다. 상태 트리가 무한정 커질 수도 있지만 필요한 트리의 가지만 변경된다는 점과 변경안된 부분의 레퍼런스가 유지되어서 해당 값을 참조하는 곳에서 빠르게 동작할 수 있다는 장점이 있다.

Redux에서는 생성한 Reducer들을 함께 묶어주는 combinedReducers()를 제공해주는데 todo 프로그램에서는 할일 목록들을 관리해주는 todos와 필터 설정과 관련된 visibilityFilter를 사용하고 있으며 각각의 함수들은 자신의 관심사에만 집중한 형태로 구성된다. todos 함수는 할일 전체 목록에 대해서만 처리하며 할일 자체에 대해서는  todo 함수를 통해 다시 할당하게 된다.

Redux_separate_concern.png

아래는 todos와 todo를 처리하는 리듀서로 변경된 상태는 항상 새로운 객체를 생성해서 리턴하고 변경되지 않은 경우에는 기존 상태를 리턴하는 것을 확인할 수 있다.

Redux_reducer

Store

 

Screen Shot 2015-12-31 at 9.32.39 AM

스토어의 기본 골격은 위와 같다. 관찰자들을 등록하고 액션을 받았을 때 이를 전파시켜주는게 기본 골격이다. 즉, subscribe()를 통해 상태 트리가 변경된 경우 이를 청취할 함수를 등록하고 dispatch()를 통해 발생한 액션을 받으며 현재 상태를 얻어갈 수 있도록 getState()를 제공한다.

todo 프로그램에서는 사용자가 클릭과 같은 동작을 실행했을 때 액션을 생성하고 이를 store.dispatch(action)에 전달한 다음 store에 저장되어 있는 reducer들을 실행하면서 변경사항을 생성한 다음 새로운 상태 트리를 render()에 전달하게 된다. render()가 호출될 때 상태 트리의 속성들은 react의 속성(props)로 전달되어 리액트 컴포넌트가 새로 그려질 때 사용되게 된다.

 

Redux_flow

화면이나 네트워크에서 액션이 발생하여 스토어로 전달되고 스토어에서 생성된 새로운 상태가 다시 화면으로 전달되어 화면이 갱신되게 된다. 흐름을 보게 되면 단방향으로 구성되며 여기에는 동기 흐름만이 존재하며 비동기 처리는 middleware를 통해 이 흐름 밖에서 처리하게 된다.

 

Container component

액션으로 변경된 상태트리를 받는 리액트 컴포넌트를 Container component라 부른다. Todo  프로그램에서는 App(강의 코드에서는 TodoApp)이 여기에 해당된다. Container component의 아래에는 Presentational component들로 구성되며 르는 redux를 알지 못하면서 화면 그리는데 집중하는 요소들이 있다.

 

Redux_component

TodoList에서는 상태트리에서 todos 목록만 인자로 받아서 화면에 그리고 todo 항목을 클릭했을 때 이벤트를 배포하는 함수를 인자로 받아서 이벤트 핸들러에 설정하는 형태로 층을 분리해서 구성하고 있다.

Middleware

Middleware는 액션이 발생되어서 전파되는 시점부터 reducer에 도착하는 지점 사이에 서드파티를 추가할 수 있는 기능을 제공한다.(the moment it reaches the reducer로 사용되었는데 이 의미가 리듀서의 시작점이 아니라 리듀서 종료 지점으로 봐야할 것 같다. Implement middleware도 참조)  액션이 발생하여 상태 트리가 변경되었을 때 전/후의 흐름을 체크하거나 충돌 리포터를 사용하여 실행 시 예외를 체크 보고 할 수 있는 기능을 체인 형태로 제공할 수 있다. (Redux 의 Middleware 항목을 보면 Middleware를 어떻게 만들었는지, 개념에 대해서 잘 소개 되어 있다.)

Redux_concept

Middleware 개요 부분의 코드만 추려보면 다음과 같다. (실제 코드와는 다름에 주의!) store와 dispatch를 인자로 받아서 middleware의 코드를 적용한 dispatch를 리턴해서 액션이 발생하면 적용된 middleware들을 모두 실행하게 된다. Redux에서는 Middleware를 제외하고는 동기화 흐름만 지원하기 때문에 비동기 동작들은 Middleware로 구현되며 Ajax 호출과 같은 경우는 Thunk를 사용해 구현된다.

Redux_middleware_code

 Thunk

Redux에서 비동기 동작은 Redux Thunk Middleware를 통해서 실행된다. 인자로 받는 액션이 함수인 경우 이를 실행시켜 주는 간단한 함수이지만 비동기 동작을 통한 액션 발생을 함수 안으로 캡슐화 시켜주는 기능을 하게 된다.

thunkMiddleware

 

 

참고 자료

※ redux 문서를 보다보면 지속적으로 용어나 설명이 추가/변경이 되고 있어서 가장 최신 내용은 영문의 공식 문서를 보시기를 추천합니다. “왜?”라는 질문과 함께 ‘이렇게 풀었다’라는 설명을 보다보면 개발문서의 정석으로 느껴지네요.