├── .gitignore ├── .travis.yml ├── code_of_conduct.md ├── contributing.md ├── package.json ├── readme.md └── translated ├── assets ├── deal-with-async-process-by-redux-saga │ ├── redux-saga-diagram.png │ └── saga-monitor.png └── the-right-way-to-test-react-components │ └── demo.gif ├── deal-with-async-process-by-redux-saga.md └── the-right-way-to-test-react-components.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # 기여자 행동 강령 규약 2 | 3 | ## 서약 4 | 5 | 개방적이고 친근한 환경 조성을 위해, 기여자와 유지자는 프로젝트와 커뮤니티에서 6 | 연령, 신체 크기, 장애, 민족성, 성 정체성과 표현, 경력, 국적, 외모, 인종, 종교 7 | 또는 성적 정체성과 지향에 관계없이 모두에게 차별없이 참여할 것을 서약합니다. 8 | 9 | ## 표준 10 | 11 | 긍정적인 환경을 조성하기 위해 기여자가 해야 할 행동은 다음과 같습니다: 12 | 13 | - 소외하지 않고 배려하는 언어 사용 14 | - 서로 다른 경험과 관점 존중 15 | - 열린 마음으로 건설적인 비판을 수용 16 | - 커뮤니티에 가장 최선이 무엇인지에 주력 17 | - 다른 커뮤니티 구성원들에 대한 공감 표현 18 | 19 | 하지말아야 할 행동은 다음과 같습니다: 20 | 21 | - 성적인 언어와 이미지 사용, 원치않는 성적 관심이나 접근 22 | - 소모적인 논쟁, 모욕적이거나 비하하는 댓글과 개인적 또는 정치적인 공격 23 | - 공개적이거나 개인적인 괴롭힘 24 | - 동의없는 집주소 또는 전자주소 등의 개인 정보의 공개 25 | - 부적절한 것으로 간주될 수 있는 다른 행위 26 | 27 | ## 책임 28 | 29 | 프로젝트 유지자는 허용되는 행동의 기준을 명확히 해야할 책임이 있습니다. 또한, 30 | 하지말아야 할 행동에 대해 적당하고 공정한 시정 조치를 취할 것 입니다. 31 | 32 | 프로젝트 유지자는 이 행동 강령을 따르지 않은 댓글, 커밋, 코드, 위키 편집, 33 | 이슈와 그 외 다른 기여를 삭제, 수정 또는 거부할 권리와 책임이 있습니다. 또한, 34 | 부적당하거나 험악하거나 공격적이거나 해롭다고 생각하는 다른 행동을 한 기여자를 35 | 일시적 또는 영구적으로 퇴장시킬 수 있습니다. 36 | 37 | ## 범위 38 | 39 | 이 행동 강령은 프로젝트 영역에 적용되며, 프로젝트 또는 커뮤니티를 대표할 경우 40 | 공개 영역에도 적용됩니다. 프로젝트 또는 커뮤니티 대표의 예로는 공식 프로젝트 41 | 이메일 주소, 공식 소셜 미디어 계정사용 또는 온/오프라인 이벤트에서 임명된 42 | 대표자의 활동이 있습니다. 프로젝트의 대표는 프로젝트 유지자에 의해 더 정의되고 43 | 명확히 될 것 입니다. 44 | 45 | ## 강제 46 | 47 | 모욕적인, 괴롭힘 또는 기타 하지말아야 할 행동을 발견하면 을 48 | 통해 프로젝트 팀에 보고 해 주세요. 모든 불만사항은 검토하고 조사한 뒤 상황에 49 | 따라 필요하고 적절하다고 생각되는 응답을 할 것 입니다. 프로젝트 팀은 사건의 50 | 보고자와 관련한 비밀을 유지할 의무가 있습니다. 구체적인 시행 정책의 자세한 51 | 사항은 별도로 게시할 수 있습니다. 52 | 53 | 행동 강령을 따르지 않거나 강제하지 않은 프로젝트 유지자는 프로젝트 리더의 다른 54 | 구성원의 결정에 따라 일시적 또는 영구적인 제재를 받을 수 있습니다. 55 | 56 | ## 참고 57 | 58 | 이 행동 강령은 [기여자 규약][homepage] 의 1.4 버전을 변형하였습니다. 그 내용은 59 | [http://contributor-covenant.org/version/1/4/ko][version] 에서 확인할 수 60 | 있습니다. 61 | 62 | [homepage]: http://contributor-covenant.org 63 | 64 | [version]: http://contributor-covenant.org/version/1/4/ko/ 65 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # 기여하기 2 | 3 | 이 프로젝트는 [기여자 규약](./code_of_conduct.md)를 준수합니다. 4 | 5 | ## Issue 제출 6 | 7 | React의 교재와 관련된 이슈라면 뭐든지 환영합니다. 힌트를 드리면 다음과 같은 것이 되겠네요. 8 | 9 | - React와 관련된 좋은 자료의 추가(자기가 쓴 것도 좋습니다.) 10 | - 오래되거나 잘못된 자료의 삭제 11 | - 번역 요청 12 | 13 | ## Pull Request 14 | 15 | 본 리포지터리는 글들의 포맷을 유지하기위해 린터가 내장되어있습니다. 글을 작성 혹은 편집하실 경우, `npm test`로 린트 테스트가 통과하는지를 확인해주세요. 또한, `npm run fix`를 사용하면 자동적으로 대부분의 린트 에러를 고쳐줍니다. 16 | 17 | 푸쉬된 커밋은 TravisCI를 통해 다시 한번 확인되므로, 빌드 실패시 에러메세지를 확인하셔서 수정 커밋을 추가해주세요. 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-react-in-korean", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:Rokt33r/learn-react-in-korean.git", 6 | "author": "Junyoung Choi ", 7 | "license": "CC-BY-NC-SA-4.0", 8 | "devDependencies": { 9 | "remark-cli": "^3.0.0", 10 | "remark-lint": "^6.0.0", 11 | "remark-preset-lint-recommended": "^2.0.0" 12 | }, 13 | "scripts": { 14 | "test": "remark . -qf", 15 | "fix": "remark . -qfo" 16 | }, 17 | "remarkConfig": { 18 | "plugins": ["remark-preset-lint-recommended"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 한국어로 배우는 리액트 2 | 3 | 한국어로된 리액트 관련 포스팅이나 웹사이트의 모음입니다. 현재 직접 링크를 모으고 있습니다. PR은 언제나 환영입니다. 4 | 5 | ## 입문 6 | 7 | 처음 리액트를 접하시는 분들께 추천할만한 글들입니다. 8 | 9 | - [\[번역\] react-howto](https://github.com/petehunt/react-howto/blob/master/README-ko.md) 10 | - [React 소개 및 맛보기](http://webframeworks.kr/tutorials/react/react-intro-and-give-it-a-try/) 11 | - [Velopert님의 텍스트 강좌](https://velopert.com/reactjs-tutorials) 12 | - [React & Express 를 이용한 웹 어플리케이션 개발하기](https://www.inflearn.com/course/react-강좌-velopert/) 13 | - [\[번역\] 리액트, 리덕스와 리액트-리덕스(React, Redux and react-redux)](https://www.vobour.com/book/view/6vas6uCQF8GXDJDHt) 14 | - [\[번역\] 리액트 도움닫기(The Road to learn React)](https://github.com/sujinleeme/the-road-to-learn-react-korean) 15 | 16 | ## 추천 17 | 18 | 리액트를 100% 활용하기 위해 꼭 읽어 볼 만한 내용들 입니다. 19 | 20 | ### React 21 | 22 | - [React 적용 가이드 - 네이버 메일 모바일 웹 적용기](http://d2.naver.com/helloworld/4966453) 23 | - [리안 개발 일기 #1: Front-End 개발(React)](https://medium.com/@RianCommunity/리안-개발-일기-2-front-end-개발-react-9f6ccb5b016d) 24 | - [React의 기본, 컴포넌트를 알아보자](https://medium.com/little-big-programming/react의-기본-컴포넌트를-알아보자-92c923011818#.uemkhn2ym) 25 | - [React 앱의 최적화 전략](http://webframeworks.kr/tutorials/react/react-optimization/) 26 | - [React 앱의 데이터 흐름](http://webframeworks.kr/tutorials/react/react-dataflow/) 27 | - [\[번역\] 상세한 리액트 Higher Order Components 설명(React Higher Order Components in depth)](https://www.vobour.com/book/view/XSSFQ5wBzsCLAbbo4) 28 | - [React 렌더링과 성능 알아보기](https://github.com/nhnent/fe.javascript/wiki/March-20---March-24,-2017-(2)) 29 | - [React 컴포넌트를 테스트하는 세 가지 방법](http://webframeworks.kr/tutorials/react/testing/) 30 | - [React Component를 테스트하기 위한 올바른 방법](./translated/the-right-way-to-test-react-components.md) 31 | - [\[번역\] Presentational and Container Components](https://medium.com/@seungha_kim_IT/presentational-and-container-components-%EB%B2%88%EC%97%AD-1b1fb2e36afb) 32 | ### Redux 33 | 34 | - [\[번역\] Redux 한글 문서](http://dobbit.github.io/redux/index.html) 35 | - [\[번역\] redux-saga로 비동기처리와 분투하다](./translated/deal-with-async-process-by-redux-saga.md) 36 | - [\[번역\] 리덕스 패턴과 안티 패턴 (Redux Patterns and Anti-Patterns)](https://www.vobour.com/book/view/TGJKKFN2TmyxaGDpN) 37 | - [리덕스(Redux) 애플리케이션 설계에 대한 생각](http://huns.me/development/1953) 38 | - [Redux 를 통한 React 어플리케이션 상태 관리](https://velopert.com/3365) 39 | 40 | ### Blogs 41 | 42 | 리액트와 관련된 좋은 리소스를 제공하는 블로그들입니다. 43 | 44 | - [Velopert.log](https://velopert.com/) 45 | - [ZeroCho Blog](https://www.zerocho.com/) 46 | - [오늘도 끄적끄적](https://perfectacle.github.io/) 47 | 48 | ## 문서들 49 | 50 | ### React 51 | 52 | - [Create React App에 Flow(정적 타입 체크 라이브러리) 사용 하기](https://medium.com/@bestseob93/starting-create-react-app-with-flow-a44a72d295cf) 2018/01 53 | - [\[번역\] Next.js 2.0을 이용하여 보다 나은 Universal JavaScript 앱 만들기](https://medium.com/@LetMeEatTheCake/next-js-2-0을-이용하여-보다-낳은-universal-javascript-앱을-만들기-70fb32714ad4) 2017/04 54 | - [\[Next.js 2.0\] 간단한 React 전용 서버사이드 프레임워크, 기초부터 본격적으로 파보기](https://velopert.com/3293) 2017/04 55 | - [\[번역\] 리액트하다가 막혔을 때 생각해볼 4가지 질문](https://velopert.com/3260) 2017/03 56 | - [\[번역\] 리액트에 대해서 그 누구도 제대로 설명하기 어려운 것 – 왜 Virtual DOM 인가?](https://velopert.com/3236) 2017/03 57 | - [ReactJS: Props와 State 비교](https://wonhada.com/?topic=reactjs-props와-state-비교) 2017/03 58 | - [(React Router) v4로 마이그레이션 해보자!](https://perfectacle.github.io/2017/03/25/react-router-v4/) 2017/03 59 | - [(React Hot Loader) v3로 마이그레이션 해보자!](https://perfectacle.github.io/2017/03/25/react-hot-loader-3/) 2017/03 60 | - [React Study v2 #00](https://blog.weirdx.io/post/38247) 2017/02 61 | - [\[번역\] Building Tesla's battery range calculator with React Part 1](https://gyver98.github.io/blog/development/react/2017/02/09/react-tesla-battery-range-calculator-part1-korean/) 2017/02 62 | - [\[번역\] Building Tesla's battery range calculator with React Part 2 (Redux version)](https://gyver98.github.io/blog/development/react/redux/2017/03/17/react-tesla-battery-range-calculator-part2-korean/) 2017/03 63 | - [리액트 인라인 스타일링과 스타일드 컴포넌트](https://medium.com/@jimkimau/리액트-인라인-스타일링과-스타일드-컴포넌트-f0514d32982a#.u8oi6gvge) 2017/02 64 | - [\[번역\] 함수형 setState가 리액트(React)의 미래이다](https://www.vobour.com/book/view/MPTQLpzxAHxzywcBc) 2017/02 65 | - [React 프로젝트의 디렉토리 구조](https://medium.com/@FourwingsY/react-프로젝트의-디렉토리-구조-bb183c0a426e#.q36tlor7g) 2017/02 66 | - [React 소개 및 구현방법 Demo](https://www.slideshare.net/zonekom/react-demo) 2017/01 67 | - [\[번역\] \`setState\` 메쏘드 파라미터로 객체 대신 함수 사용하기(Using a function in \`setState\` instead of an object)](https://www.vobour.com/book/view/kgFc5hdkZ5p7sm7tj) 2017/01 68 | - [\[번역\] 리액트 죽음의 다섯 손가락. 이 다섯 가지 개념을 마스터 한 다음 리액트를 마스터하라(React’s Five Fingers of Death. Master these five concepts, then master React)](https://www.vobour.com/book/view/fzfscDgHWQDeqr3B5) 2017/01 69 | - [Immutable한 양방향 데이터 바인딩](https://www.slideshare.net/xpressengine/xecon2016-a1-react-immutable) 2016/12 70 | - [RxJS로 React 컴포넌트 상태 관리하기](http://blog.sapzil.org/2016/12/15/react-with-rx/) 2016/12 71 | - [create-react-app에 관하여 -1](https://techstory.shma.so/create-react-app에-관하여-1-935a21297550) 2016/10 72 | - [react로 개발자 2명이 플랫폼 4개를 서비스하는 이야기](https://www.slideshare.net/deview/125react24) 2016/10 73 | - [\[번역\] 리액트 State 비쥬얼 가이드(A Visual Guide to State in React)](https://www.vobour.com/book/view/3wKFokAjFncKKCiQg) 2016/10 74 | - [\[번역\]\[React Router\] 시리즈](http://yubylab.tistory.com/entry/React-Router-Lesson01-settingup) 2016/10 75 | - [React, Google Maps, D3 를 이용한 Hexagonal Binning 기법](http://meshlabs.ghost.io/react-hexagonal-binning) 2016/9 76 | - [React, Redux and es6/7](https://www.slideshare.net/looklazy/react-redux-and-es67) 2016/09 77 | - [React.js가 IE 브라우저 지원 중단했다면서요?](https://medium.com/little-big-programming/react-js가-ie-브라우저-지원-중단했다면서요-a9734bc323cb#.ym56hg9ek) 2016/09 78 | - [리액트 딜레마](http://huns.me/development/2011) 2016/08 79 | - [React.js 실서비스 적용기](http://slides.com/roto/react-js-live-service#/) 2016/07 80 | - [애니메타의 React 서버 렌더링 아키텍쳐](http://blog.sapzil.org/2016/07/29/animeta-react-ssr/) 2016/07 81 | - [왜 React와 서버 사이드 렌더링인가?](https://subicura.com/2016/06/20/server-side-rendering-with-react.html) 2016/06 82 | - [안녕, 리액트(Hello, React)](http://blog.gaerae.com/2016/04/hello-react.html) 2016/04 83 | - [React 소스 코드 읽기 - ReactElement](http://blog.sapzil.org/2016/03/17/react-internals-elements/) 2016/03 84 | - [React 소스 코드 읽기 - 유틸리티들](http://blog.sapzil.org/2016/03/20/react-internals-utils/) 2016/03 85 | - [React 튜토리얼 1차시](https://www.slideshare.net/ssuser555dd7/react-1) 2016/03 86 | - [React 튜토리얼 2차시](https://www.slideshare.net/ssuser555dd7/react-2) 2016/03 87 | - [React로 개발하는 SPA 실무 이야기](https://www.slideshare.net/xpressengine/xecon2015-22-react-spa) 2016/02 88 | - [React 애플리케이션의 서버 렌더링](http://webframeworks.kr/tutorials/react/server-side-rendering/) 2016 89 | - [자바스크립트 프레임워크 소개 4 - React](http://meetup.toast.com/posts/100) 2016 90 | - [React와 불변객체](http://blog.coderifleman.com/2015/08/16/react-and-immutable/) 2015/08 91 | 92 | ## Redux 93 | 94 | - [React 적용 가이드 - React와 Redux](http://d2.naver.com/helloworld/1848131) 2017/02 95 | - [State management — Redux vs. MobX](https://engineering.huiseoul.com/state-management-redux-vs-mobx-a8853a7c80ea) 2016/09 96 | - [\[번역\] Redux Step by Step : 실제 앱을 위한 간단하고 탄탄한 워크 플로(Redux Step by Step: A Simple and Robust Workflow for Real Life Apps)](https://www.vobour.com/book/view/SiDR6QXtoCayx7afd) 2016/11 97 | - [\[번역\] 당신에게 Redux는 필요 없을지도 모릅니다.](https://medium.com/@Dev_Bono/당신에게-redux는-필요-없을지도-모릅니다-b88dcd175754) 2016/09 98 | - [\[번역\] 리덕스에 대한 이해](http://webframeworks.kr/tutorials/translate/understanding-redux/) 2016 99 | - [Flux와 Redux](http://webframeworks.kr/tutorials/react/flux/) 2016 100 | - [역시 Redux](https://www.slideshare.net/dalinaum/redux-55650128) 2015/11 101 | 102 | ## Misc 103 | 104 | ### React Native 105 | - [React Native Swift 구글 스트리트뷰 컴포넌트 만들기 #1](https://medium.com/@bestseob93/react-native-swift-구글-스트리트-뷰-컴포넌트-만들기-1-fe2fbf7b59b1) 2017/05 106 | - [React Native Swift 구글 스트리트뷰 컴포넌트 만들기 #2](https://medium.com/@bestseob93/react-native-swift-%EA%B5%AC%EA%B8%80-%EC%8A%A4%ED%8A%B8%EB%A6%AC%ED%8A%B8-%EB%B7%B0-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-c3725eaa6cf7) 2017/05 107 | - [리액트 네이티브 FlatList (React Native FlatList)](https://www.vobour.com/book/view/gYkhxyL2FsWJWXPPS) 2017/04 108 | - [React Native 안드로이드 성능 최적화](https://taegon.kim/archives/5950) 2017/03 109 | - [\[번역\] 인스타그램이 React Native로 앱을 만든 과정](https://taegon.kim/archives/5745) 2017/03 110 | - [리액트 네이티브 (React Native) 일년](https://medium.com/@joyeon/리액트-네이티브-react-native-일년-a0556f2755aa#.i1q40rr3i) 2016/12 111 | - [ReactNative 튜토리얼](https://g6ling.gitbooks.io/react-native-tutorial-korean/content/) 2016/07 112 | - [리액트 네이티브로 시작하는 앱 개발 #3](https://realm.io/kr/news/react-native3/) 2016/03 113 | - [리액트 네이티브로 시작하는 앱 개발 #2](https://realm.io/kr/news/react-native2/) 2016/03 114 | - [리액트 네이티브로 시작하는 앱 개발 #1](https://realm.io/kr/news/react-native/) 2015/11 115 | - [React Native를 사용한 초간단 커뮤니티 앱 제작](https://www.slideshare.net/taggon/react-native) 2015/05 116 | - [MobX with React Native — Intro](https://engineering.huiseoul.com/mobx-with-react-native-intro-605dc3a7fe94) 2016/04 117 | - [리액트 네이티브\[React Native\] 0.4x](https://wonhada.com/?docs=리액트-네이티브react-native-0-41/기본the-basics/시작하기) 118 | - [React Native Swift 구글 스트리트뷰 컴포넌트 만들기 #1](https://medium.com/@bestseob93/react-native-swift-구글-스트리트-뷰-컴포넌트-만들기-1-fe2fbf7b59b1) 2017/05 119 | - [React Native Swift 구글 스트리트뷰 컴포넌트 만들기 #2](https://medium.com/@bestseob93/react-native-swift-%EA%B5%AC%EA%B8%80-%EC%8A%A4%ED%8A%B8%EB%A6%AC%ED%8A%B8-%EB%B7%B0-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-c3725eaa6cf7) 2017/05 120 | 121 | ### Webpack 122 | 123 | - [(Webpack 2) 상대경로 헬파티에서 탈출하기](http://perfectacle.github.io/2017/04/20/webpack2-escape-relative-path-hell/) 2017/04 124 | - [(Webpack 2) 최적화하기](http://perfectacle.github.io/2017/04/18/webpack2-optimize/) 2017/04 125 | - [(Webpack 2) 코드를 분할해보자!](https://perfectacle.github.io/2017/03/13/webpack2-code-splitting/) 2017/03 126 | - [(Webpack 2) 트리 쉐이킹을 해보자!](https://perfectacle.github.io/2017/03/12/webpack2-tree-shaking/) 2017/03 127 | - [(Webpack 2) 트리 쉐이킹을 똑똑하게 해보자!](https://perfectacle.github.io/2017/04/12/webpack2-smart-tree-shaking/) 2017/04 128 | - [웹팩2(Webpack) 설정하기](https://www.zerocho.com/category/Javascript/post/58aa916d745ca90018e5301d) 2017/02 129 | - [웹팩2로 CSS와 기타 파일 번들링하기](https://www.zerocho.com/category/Javascript/post/58ac2d6f2e437800181c1657) 2017/02 130 | - [웹팩2로 청크 관리 및 코드 스플리팅 하기](https://www.zerocho.com/category/Javascript/post/58ad4c9d1136440018ba44e7) 2017/02 131 | 132 | ### 미분류 133 | - [ReactXP - 스카이프(Skype)에서 만든 리액트(React) 기반 멀티 플랫폼 지원 라이브러리 사용 해보기](https://www.vobour.com/book/view/zWoy697Q5c5EppwfK) 2017/04 134 | - [React VR](http://sungjk.github.io/2017/04/19/react-vr.html) 2017/04 135 | - [JavaScript in 2017: 옛날 사람 탈출하기](http://meshlabs.ghost.io/javascript-in-2017/) 2017/03 136 | - [rest에서 graph ql과 relay로 갈아타기](https://www.slideshare.net/deview/112rest-graph-ql-relay) 2016/10 137 | - [snippod-starter-demo-app–Full Stack Architecture : React & Flux + Django REST Framework](http://www.shalomeir.com/2016/07/snippod-starter-demo-app-full-stack-react-redux-django/) 2016/07 138 | - [개발자가 가져야 할 균형 감각에 대한 단상](http://huns.me/development/1775) 2016/02 139 | 140 | ## 기여 141 | 142 | ### 글 제보 143 | 144 | 다음과 같은 조건이면 바로 PR을 보내주셔도 됩니다. 그리고 작성 년월도 PR에 커멘트로 알려주세요. 145 | 146 | - 완전히 번역된 글 147 | - 2017년 이후 글들 148 | 149 | 이 조건에 해당하지 않는 경우 이슈를 열어주세요! 검토해보도록 하겠습니다. 150 | 151 | ### 번역 요청 152 | 153 | 번역이 필요한 문서들은 [새 이슈](https://github.com/Rokt33r/learn-react-in-korean/issues/new)를 만들어서 요청해주세요! 154 | 155 | ## 메인테이너 156 | 157 | [Junyoung Choi (Rokt33r)](https://github.com/rokt33r) 158 | 159 | ## Special Thanks 160 | 161 | [Eclatant](https://github.com/Eclatant) 162 | 163 | ## License 164 | 165 | 본 링크 리스트는 [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)하에 이용 가능합니다. 166 | -------------------------------------------------------------------------------- /translated/assets/deal-with-async-process-by-redux-saga/redux-saga-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactkr/learn-react-in-korean/dc1e796198af7e55518aeadb0bded7d6bfdffe7c/translated/assets/deal-with-async-process-by-redux-saga/redux-saga-diagram.png -------------------------------------------------------------------------------- /translated/assets/deal-with-async-process-by-redux-saga/saga-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactkr/learn-react-in-korean/dc1e796198af7e55518aeadb0bded7d6bfdffe7c/translated/assets/deal-with-async-process-by-redux-saga/saga-monitor.png -------------------------------------------------------------------------------- /translated/assets/the-right-way-to-test-react-components/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactkr/learn-react-in-korean/dc1e796198af7e55518aeadb0bded7d6bfdffe7c/translated/assets/the-right-way-to-test-react-components/demo.gif -------------------------------------------------------------------------------- /translated/deal-with-async-process-by-redux-saga.md: -------------------------------------------------------------------------------- 1 | > [kuy](https://github.com/kuy)님이 쓰신 [redux-sagaで非同期処理と戦う](http://qiita.com/kuy/items/716affc808ebb3e1e8ac)라는 글의 번역입니다. 본 번역 글은 [원 작자의 허가하](http://qiita.com/kuy/items/716affc808ebb3e1e8ac#comment-55361d9ba905b600cf68)에 작성되어 있습니다. 2 | > 3 | > 또한, 일본어의 특성상 한국어에서는 잘 안쓰는 한자어가 많고 직역할 경우, 가끔 뉘앙스가 애매해지거나 표현이 장황해지는 경우가 있기에, 이러한 부분은 **의도가 변하지 않는 선에서** 의역을 하고 있습니다. 4 | > 5 | > 번역자 : [Junyoung Choi (Rokt33r)](https://github.com/rokt33r) 6 | 7 | # redux-saga로 비동기처리와 분투하다. 8 | 9 | 10 | 11 | ## 시작하는 말 12 | 13 | 14 | 15 | Redux는 단일 Store, 불변적인 State, Side effect가 없는 Reducer의 3가지 원칙을 내세운 Flux 프레임워크입니다. 하지만 다른 프레임워크와는 달리 제공하는 것은 최소한으로, Fullstack이라고는 말하기 힘들 만큼 얇습니다. 그 때문에 모든 면에 있어서 일반적이라고 할만한 사용법이 정리되지 않아서, 어떻게 사용해야할지 방황하는 경우도 적지 않습니다. 그 필두로 말할 수 있는게 **비동기처리**입니다. 커뮤니티는 지금도 여러 방법을 모색하고 있고, 주로 쓰여지는건 [redux-thunk](https://github.com/gaearon/redux-thunk)와 [redux-promise](https://github.com/acdlite/redux-promise)정도 일겁니다. Redux로 한정하지 않는다면 [react-side-effect](https://github.com/gaearon/react-side-effect)도 있습니다. 이건 [Twitter의 모바일 버젼](https://mobile.twitter.com)에 사용되고 있습니다. 어떤 걸 써도 비동기처리가 가능하게 되지만, 그것들은 어디까지나 도구로써, 설계의 지침까지는 알려주지 않습니다. **문제는 비동기처리를 어디에 쓸 것 인가, 어떻게 쓸 것 인가, 그리고 어디서 불러와야 하는가 입니다.** Redux를 사용하고있으면 다음과 같은 상황이 고민하고 있지는 않으신가요? 16 | 17 | 18 | 19 | - 특정 Action을 기다리다 다른 Action을 dispatch한다. 20 | - 통신처리를 완료를 기다리고, 다른 통신처리를 시작한다. 21 | - 초기화시 데이터를 읽어들이고 싶다. 22 | - 빈번히 발생하는 Action을 모아서 dispatch하고 싶다. 23 | - 다른 프레임워크, 라이브러리와 잘 어울리게 하고 싶다. 24 | 25 | 30 | 31 | React + Redux의 아름다운 세계에서 움츠러드는 그 코드들을 어떻게 해야 할 것인가. 어떻게 싸워야 할 것인가. 이 글에는 한가지의 해결방법으로써 [redux-saga](https://github.com/yelouafi/redux-saga)를 소개합니다. redux-saga의 개요와 기본적인 개념에대해 가볍게 설명하고, 익숙한 redux-thunk로 만들어진 코드와 비교해보겠습니다. 조금만 초보적인 셋업방법과 실수하기 쉬운 부분을 설명하고, 후반은 실전적인 redux-saga의 사용법을 소개합니다. 32 | 33 | 34 | 35 | 참고로 공식 리포지토리에서 [한국어로된 README](https://github.com/redux-saga/redux-saga/blob/master/README_ko.md)가 제공되고 있습니다. 일단 써보고싶다! 라는 분들은 먼저 이쪽 링크도 훑어봐주세요. 36 | 37 | 38 | 39 | ## redux-saga란 40 | 41 | 42 | 43 | **redux-saga는 Redux로 Sideeffect를 다루기 위한 Middleware입니다.** ... 아마 이대로라면 이해하기 어렵겠지요. 이 구문은 라이브러리의 짧은 설명문이지만, 실은 그다지 본질을 표현하고 있지 못합니다. 그런 이유로 제 나름의 이해와 단어로 설명해보겠습니다. 44 | 45 | 46 | 47 | ## redux-saga 란 (다시 설명) 48 | 49 | 50 | 51 | **redux-saga는 "Task"라는 개념을 Redux로 가져오기위한 지원 라이브러리입니다.** 여기서 말하는 Task란 일의 절차와 같은 독립적인 실행 단위로써, 각각 평행적으로 작동합니다. redux-saga는 이 Task의 실행환경을 제공합니다. 더불어 비동기처리를 Task로써 기술하기 위한 준비물인 "**Effect**"와 비동기처리를 동기적으로 표현하는 방법을 제공하고 있습니다. Effect란 Task를 기술하기 위한 커맨드(명령, Primitive)와 같은 것으로, 예를들면 다음과 같은 것들이 있습니다. 52 | 53 | 54 | 55 | - `select`: State로부터 필요한 데이터를 꺼낸다. 56 | - `put`: Action을 dispatch한다. 57 | - `take`: Action을 기다린다. 이벤트의 발생을 기다린다. 58 | - `call`: Promise의 완료를 기다린다. 59 | - `fork`: 다른 Task를 시작한다. 60 | - `join`: 다른 Task의 종료를 기다린다. 61 | - ... 62 | 63 | 70 | 71 | 이러한 처리중에는 Task안에 직접 실행할 수 있는 것도 있지만, redux-saga에게 부탁하여 간접적으로 실행합니다. 이로써 **비동기처리를 [co](https://github.com/tj/co)처럼 동기적으로 쓸 수 있게 하고, 복수의 Task를 동시평행적으로 실행**하는 것이 가능합니다. 다음 그림은 redux-saga에서 실행되는 Task의 이미지입니다. 72 | 73 | 74 | 75 | ![redux-saga.png](./assets/deal-with-async-process-by-redux-saga/redux-saga-diagram.png) 76 | 77 | ### 무엇이 좋아지나? 78 | 79 | 80 | 81 | Flux나 Redux만으로도 난해했던것이 더욱 새로운 개념을 가져와서 혼란스럽게 하고있네요. 그래도 Redux-saga를 쓸 가치는 있다고 생각합니다. 82 | 83 | 84 | 85 | - Mock 코드를 많이 쓰지 않아도 된다. 86 | - 작은 코드로 더 분할할 수 있다. 87 | - 재이용이 가능해진다. 88 | 89 | 92 | 93 | 이것은 단순한 "재이용가능"이라는 단어 이상의 의미가 있습니다. 무슨 말이냐면 재이용가능한 Container Component를 개발하는데 있어 필수적인 요소입니다. Middleware라는 정말 이해하기 힘들지만 신경쓸 수 밖에 없는 것이 산더미처럼 있고, 더욱이 재이용가능한 컴포넌트로써 도입할 때도, 어디에 Middleware를 넣어줄 것인가를 생각하지않으면 안됩니다. 반면, saga라면 원칙적으로 서로 독립적을 동작하기 때문에 자신의 세계에서만 코드를 쓰는게 가능하여, 다른 saga에 영향을 끼치지 않습니다. 94 | 95 | 96 | 97 | 추상적인 설명은 그다지 이해도가 높아지지 않기에, redux-thnk로 쓴 코드와 비교하며 redux-saga로 인해 어떻게 바뀌는지를 한번 봅시다. 98 | 99 | 100 | 101 | ## redux-thunk → redux-saga 102 | 103 | 샘플로써 [FetchAPI](https://r.mozilla.org/en-US/docs/Web/API/Fetch_API)를 사용한 통신처리를 해봅시다. 104 | 105 | 106 | 107 | 데이터를 읽어들이는건 간단하지만, Redux로 제대로 생각해야할게 적지 않습니다. 예를 들면 다음과 같은 점들입니다. 108 | 109 | 110 | 111 | - 어디서 통신처리를 적을 것인가 112 | - 어디로부터 통신처리를 불러올 것인가 113 | - 통신처리의 상태를 어떻게 가지게 할 것인가. 114 | 115 | 118 | 119 | 3번째 요소는 redux-thunk나 redux-saga 둘다 공통적인 부분이므로 먼저 설명하겠습니다. 120 | 121 | 122 | 123 | 통신처리가 완료되기까지 "읽어들이는 중..."이라는 메세지를 표시하기 위해서는, 통신상태를 Store로 가지게 한 다음에, 통신의 개시/성공/실패의 3 타이밍에 Action을 dispatch하여 상태를 바꿀 필요가 있습니다. 이 적용패턴인 Redux의 [샘플 코드](https://github.com/reactjs/redux/blob/master/examples/real-world/src/actions/index.js#L3-L5)가 아마 원본으로, 이것만 뽑아낸 [redux-api-middleware](https://github.com/agraboso/redux-api-middleware)라는 라이브러리가 있지만, 이번엔 Middleware나 redux-api-middleware를 사용하지 않고 쓰여있습니다. 통신상태뿐만 아니라, 통신이 정상적으로 종료되었는가, 에러로 인해 종료되었는가까지 맞추어 넣어두면 에러메세지를 표시하는데 쓸 수 있어서 편리합니다. 124 | 125 | 126 | 127 | 샘플 코드는 3가지의 Action 타입 `REQUEST_USER`, `SUCCESS_USER`, `FAILURE_USER`의 문자열 상수와 Action 오브젝트를 생성하기위한 3가지의 Action Creator `requestUser`, `successUser`, `failureUser`는 `actions.js`에 정의되어 있습니다. 128 | 129 | 130 | 131 | 그럼 redux-thunk 코드를 봅시다. 132 | 133 | 134 | 135 | ### redux-thunk 136 | 137 | `api.js` 138 | 139 | ```js 140 | export function user(id) { 141 | return fetch(`http://localhost:3000/users/${id}`) 142 | .then(res => res.json()) 143 | .then(payload => ({ payload })) 144 | .catch(error => ({ error })); 145 | } 146 | ``` 147 | 148 | `actions.js` 149 | 150 | ```js 151 | export function fetchUser(id) { 152 | return dispatch => { 153 | dispatch(requestUser(id)); 154 | API.user(id).then(res => { 155 | const { payload, error } = res; 156 | if (payload && !error) { 157 | dispatch(successUser(payload)); 158 | } else { 159 | dispatch(failureUser(error)); 160 | } 161 | }); 162 | }; 163 | } 164 | ``` 165 | 166 | 먼저 처리 전체의 흐름을 확인해봅시다. 167 | 168 | 169 | 170 | 1. _(누군가가 `fetchUser` 함수의 리턴 값을 dispatch한다)_ 171 | 2. redux-thunk의 Middleware가 dispatch된 함수를 실행한다. 172 | 3. 통신처리를 개시하기 전에 `REQUEST_USER` Action을 dispatch한다. 173 | 4. `API.user` 함수를 불러내서 통신처리를 개시한다. 174 | 5. 완료되면 `SUCCESS_USER` 혹은 `FAILURE_USER` Action을 dispatch한다. 175 | 176 | 181 | 182 | `api.js`의 `user`함수는 유저정보를 가져오는 함수이다. Fetch API는 Promise를 돌려주기에 적절히 처리할 필요가 있습니다. 에러핸들링의 방법은 원하는 대로 하셔도 되지만, 여기선 `try/catch`를 쓰지 않고, 리턴 값으로 판정하는 스타일을 사용하겠습니다. 183 | 184 | 185 | 186 | `actions.js`의 `fetchUser`함수는 Action Creator이지만, redux-thunk로 부터 실행되기 위해선, Action 오브젝트가 아니라 함수를 돌려줍니다. redux-thunk는 dispatch뿐만 아니라 getState도 파라미터로 넘겨주지만, 지금은 필요하지 않으므로 생략합니다. 좀 전의 통신처리의 패턴에 따라 처음에는 `REQUEST_USER` Action을 dispatch하고, 완료하거나 실패하면 `SUCCESS_USER` 혹은 `FAILURE_USER` Action을 dispatch합니다. 이렇게 redux-thunk를 사용하면 비동기처리 코드를 Action Creator에다 적게 됩니다. 본래의 Action Creator는 Action 오브젝트를 생성하여 돌려줄 뿐이었기에,생성한 Action 오브젝트를 dispatch하는 데다가, 앞뒤로 이런저런 로직이 들어가는건 위험한 냄새가 납니다. 도입도 사용법도 간단한 반면, 사태파악도 안된채로 편하니까 막쓰다간 나중에 지옥을 맞이할지도 모릅니다. 소극적으로 사용한다면 문제가 없겠지만, 복잡한 통신처리는 정말로 쓰고 싶지 않습니다. 쓰고 싶다는 생각도 안됩니다. 187 | 188 | 189 | 190 | 그럼 redux-saga로 바꿔쓰면 어떻게 되는지 봅시다. 191 | 192 | 193 | 194 | ### redux-saga 195 | 196 | `sagas.js` 197 | 198 | ```js 199 | function* handleRequestUser() { 200 | while (true) { 201 | const action = yield take(REQUEST_USER); 202 | const { payload, error } = yield call(API.user, action.payload); 203 | if (payload && !error) { 204 | yield put(successUser(payload)); 205 | } else { 206 | yield put(failureUser(error)); 207 | } 208 | } 209 | } 210 | 211 | export default function* rootSaga() { 212 | yield fork(handleRequestUser); 213 | } 214 | ``` 215 | 216 | 밀도가 높은 코드이므로 기합을 넣고 살펴보겠습니다. 먼저 전체적인 흐름부터.. 217 | 218 | 219 | 220 | 1. redux-saga의 Middleware가 `rootSaga` Task를 시작시킨다. 221 | 2. `fork` Effect로 인해 `handleRequestUser` Task가 시작된다. 222 | 3. `take` Effect로 `REQUEST_USER` Action이 dispatch되길 기다린다. 223 | 4. _(누군가가 `REQUEST_USER` Action을 dispatch한다.)_ 224 | 5. `call` Effect로 `API.user` 함수를 불러와서 통신처리의 완료를 기다린다. 225 | 6. _(통신처리가 완료된다.)_ 226 | 7. `put` Effect를 사용하여 `SUCCESS_USER` 혹은 `FAILURE_USER` Action을 dispatch한다. 227 | 8. while 루프에 따라 3번으로 돌아간다. 228 | 229 | 237 | 238 | redux-thunk로 인한 코드와의 비교를 위해 일련의 흐름을 써보았지만, 실은 이 처리로는 동시병행하여 움직이는 2개의 흐름이 있습니다. 그것이 Task입니다. `sagas.js`로 정의된 2가지 함수 모두 redux-saga의 Task입니다. 하나씩 살펴봅시다. 239 | 240 | 241 | 242 | `rootSaga` Task는 Redux의 Store가 작성 된 후, redux-saga의 Middleware가 기동 될 때 1번만 불러와집니다. 그리고 `fork` Effect을 사용하여 redux-saga에게 다른 Task를 기동할 것을 요청합니다. 앞서 설명한듯이, Task내에는 실제 처리를 행하지 않으므로, `fork`함수로부터 생성된 것은 단순한 오브젝트입니다. 이것은 Flux 아키텍쳐의 Action 오브젝트와 가까운 느낌입니다. 그러므로 다음과같이 오브젝트의 내용을 보는 것도 가능합니다. 243 | 244 | 245 | 246 | ```js 247 | console.log(fork(handleRequestUser)); 248 | ``` 249 | 250 | 이를 실행하면, 다음과 같은 느낌의 오브젝트가 생성됩니다. 251 | 252 | 253 | 254 | ```js 255 | { 256 | Symbol: true, 257 | FORK: { 258 | context: ..., 259 | fn: , 260 | args: [...] 261 | } 262 | } 263 | ``` 264 | 265 | 일단, Effect 오브젝트는 생성될뿐 아무것도 하지않으므로, redux-saga로 넘겨져서 실행될 필요가 있습니다. 이 실행을 위해서는 Generator 함수의 `yield`를 사용해 불러오는 코드 값을 넘겨주고 있습니다. "불러오는 쪽의 코드"는 누구의 것인가? 그것은 redux-saga가 제공하는 Task의 실행환경인 Middleware입니다. Effect 오브젝트를 받아들인 redux-saga의 Middleware는 넘겨진 함수를 새로운 Task로써 기동시킵니다. 그 이후 redux-saga는 2개의 Task가 동시에 움직이고있는 상태가 됩니다. 새롭게 기동된 `handleRequestUser` Task의 설명으로 넘어가기 전에 `rootSaga` Task의 "그 이후"를 따라갑니다. 266 | 267 | 268 | 269 | `fork` Effect는 지정한 Task의 완료를 기다리지 않습니다. 그러므로 `yield`는 블록되지않고 곧 제어로 돌아옵니다. 하지만 `rootSaga` Task는 `handleRequestUser` Task의 기동 이외에 할 일이 없습니다. 그때문에 `rootSaga` Task내에는 `fork`를 사용하여 기동된 모든 Task가 종료 될 때까지 기다립니다. 이러한 움직임은 redux-saga v0.10.0부터 도입된 새로운 실행모델로 인한 것으로, 연쇄적인 Task의 취소를 구현하기위해 필요했습니다. 이것은 부모 Task, 자식 Task, 손자 Task 3개에서, 부모가 자식을 Fork하고, 자식이 손자를 Fork 할 때 부모 Task를 취소하면 제대로 손자Task까지 취소를 알려주는 편리한 기능입니다. 만약 자식Task의 완료를 의도적으로 기다리고 싶지 않다면 `spawn`을 사용하여 Task를 기동해주세요. 270 | 271 | 272 | 273 | `handleRequestUser` Task를 기동시키면 금방 `REQUEST_USER` Action을 기다리기 위해 `take` Effect가 불러와집니다. 이 "기다림"이라는 행동이 **비동기처리를 동기적으로 쓴다** 라는 특징적인 Task의 표현으로 이어집니다. redux-saga의 Task를 Generator함수로 쓰는 이유는 `yield` 에 따라 처리의 흐름을 일시정지하기 때문입니다. 이러한 체계 덕분에 싱글 스레드의 Javascript로 복수의 Task를 만들어, 각각 특정한 Action을 기다리거나, 통신처리의 결과를 기다려도 처리가 밀리지 않게 됩니다. 274 | 275 | 276 | 277 | `REQUEST_USER` Action이 dispatch되면 `take` Effect를 `yield` 하여 일시정지된 코드가 재개되고, dispatch된 Action 오브젝트를 돌려줍니다. 그리고 곧 API 불러냅니다. 여기서 `call` Effect를 사용합니다. 이것도 다른 Effect와 같이 그 장소에서 실행되지 않는건 공통적이지만, 지정된 함수가 Promise를 돌려줄 경우, 그 Promise 가 resolve되고 나서 제어를 돌려줍니다. `take` Effect와 닮은 움직임이네요. 통신처리가 완려하면 다시 한번 `handleRequestUser` Task로 제어를 돌려주고, 결과에 따라 Action을 dispatch합니다. Action의 dispatch에는 `put` Effect를 사용합니다. 278 | 279 | 280 | 281 | 이것으로 통신처리 자체는 완료되었지만, 한가지 더 Task를 정의할 때 자주 쓰는 용어에 대해 설명해두겠습니다. 최초로 코드를 봤을때 "어?" 라고 느끼셨을 거라 생각하지만, `handleRequestUser` Task는 전체가 while문으로된 무한 루프로 감싸여 있습니다. 그 결과 `put` Effect로 Effect로 Action을 dispatch한 후, 루프의 처음으로 돌아가서 다시 한번 `take` Effect로 `REQUEST_USER` Action을 기다리게 됩니다. 즉, Action을 기다려 통신처리를 할 뿐인 Task가 됩니다. 여기가 매우 중요합니다. 이렇게 극단적으로 해야 할 일을 제한해두는 것으로 코드는 매우 단순해집니다. 당연히 버그도 줄겠죠. 게다가 비동기처리에 항상 따라오는 콜백 지옥, 깊은 구조, 뜬금없이 나타나는 Promise가 사라지게 됩니다. 282 | 283 | 284 | 285 | ### 어떻게 바뀌었나? 286 | 287 | 288 | 289 | redux-thunk와 redux-saga의 각각의 코드에 대해서 세밀하게 살펴 보았습니다. 여기서 잠깐 다른 관점으로부터 생각해보고 싶습니다. 이 섹션의 머릿글에 말한 "어디에 쓸 것 인가", "어디에서 불러올 것인가"에 대한 얘기입니다. 290 | 291 | 292 | 293 | redux-thunk는 Action Creator가 함수를 넘겨주기에 필연적으로 Action Creator에 비동기처리의 코드나 관련된 로직이 (약간 무리하게) 들어갑니다. 반면, redux-saga는 비동기 처리를 기술하는 전용의 방식인 Task로 쓰여있습니다. 그 결과, Action Creator는 본래의 모습을 되찾아, Action 오브젝트를 생성하여 돌려주는 순수한 상태로 돌아갑니다. 개인적으로 이 변화는 작지 않다고 생각합니다. 그렇게, redux-thunk는 dispatch된 함수를 붙잡아 실행하는 성질상, Middleware 스택(양파같은 구조니까 셸?)의 가장 바깥쪽에 배치될 필요가 있습니다. 그렇지 않으면, 다른 Middleware함수에 붙잡혀 에러가 날지도 모르기 때문입니다. 이러한 사정으로인해 함수가 dispatch되었는지에 대해 redux-thunk이외의 누구도 모르는 사태에 빠지게 됩니다. 만약 redux-thunk의 직전에 redux-logger라든가 다른 Middleware가 오게 되면 이들이 받는건 함수가 됩니다. 내용이 어떻게 될건지 실행하기 전까진 알 수 없습니다. 정말 좋지않네요. redux-saga를 쓴다면 Action Creator가 표준적인 Action 오브젝트를 생성할 뿐이기에 redux-logger로 표시시킬 수 있습니다. 294 | 295 | 296 | 297 | 그럼, 앞서 통신처리는 매우 단순한 것이었으므로, 처음보는 방식으로 쓰기가 강제된게 버거워 그다지 감사하다라는 인상이 옅었을 지도 모릅니다. 그런 이유로, 현실의 프로젝트에서도 필요할 법한 기능추가를 해봅시다. 복잡할수록 진가를 발휘하는게 redux-saga입니다. 298 | 299 | 300 | 301 | ### 처리를 복잡하게 해보자 302 | 303 | 304 | 305 | 그다지 어려운 처리라면 이해하기 어려워지기 때문에, 이전 [Redux의 middleware를 적극적으로 써보기](http://qiita.com/kuy/items/57c6007f3b8a9b267a8e)라는 포스팅에 응용 예로 만든 [API 요청을 체인시키기](http://qiita.com/kuy/items/57c6007f3b8a9b267a8e#api%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%82%92%E3%83%81%E3%82%A7%E3%82%A4%E3%83%B3%E3%81%95%E3%81%9B%E3%82%8B)를 redux-thunk와 redux-saga로 각각 써보겠습니다. 포스팅의 예제은 어떤 통신처리가 끝난 이후, 그 결과에 따라 다시 통신처리를 개시하는 것입니다. 이번 예제에서는 유저 정보를 취득한 이후, 유저정보에 포함된 지역명을 사용하여 같은 지역에 살고 있는 다른 유저를 검색하여 제안하는 기능을 추가해봅니다. 새로운 `api.js`에 추가된 `searchByLocation` 함수는 redux-thunk와 redux-saga로 만든 예제 모두 사용합니다. Action Type이나 Action Creator등은 적당히 정의해뒀다고 생각해주세요. 306 | 307 | > 역자주: 예제의 원본이 되는 링크는 일본어이지만 이미 필요한 내용들은 본 포스팅에 다 있기에 모르셔도 크게 문제 없습니다. 308 | 309 | 310 | 311 | #### redux-thunk 312 | 313 | `api.js` 314 | 315 | ```js 316 | export function searchByLocation(name) { 317 | return fetch(`http://localhost:3000/users/${id}/following`) 318 | .then(res => res.json()) 319 | .then(payload => { payload }) 320 | .catch(error => { error }); 321 | } 322 | ``` 323 | 324 | `actions.js` 325 | 326 | ```js 327 | export function fetchUser(id) { 328 | return dispatch => { 329 | // 유저 정보를 읽어들인다 330 | dispatch(requestUser(id)); 331 | API.user(id).then(res => { 332 | const { payload, error } = res; 333 | if (payload && !error) { 334 | dispatch(successUser(payload)); 335 | 336 | // 체인: 지역명으로 유저를 검색 337 | dispatch(requestSearchByLocation(id)); 338 | API.searchByLocation(id).then(res => { 339 | const { payload, error } = res; 340 | if (payload && !error) { 341 | dispatch(successSearchByLocation(payload)); 342 | } else { 343 | dispatch(failureSearchByLocation(error)); 344 | } 345 | }); 346 | } else { 347 | dispatch(failureUser(error)); 348 | } 349 | }); 350 | }; 351 | } 352 | ``` 353 | 354 | 381 | 382 | ...흠. 뭘 하고픈지는 압니다. 아마 보통 이렇게 되겠죠. 하지만 `...`한(뭔가 찝찝한) 느낌이 드네요. 그리고 여기에 또 다른 체인을 늘리거나 체인시키는 위치를 바꾸게 된다면 곤란해 질듯 하네요. 무엇보다도 기분 나쁜건 `fetchUser`라는 Action Creator를 불러내서 왜 유저 검색까지 실행하고 있느냐 라는 점입니다. Middleware를 사용하여 처리를 분리하면 이 문제점이 조금은 해소될 듯 하지만, 어플리케이션 독립적인 DSL과 같은 코드가 계속 늘어나게 되는 것 역시 괴로울 듯 합니다. 383 | 384 | 385 | 386 | 역시, 이 포스팅은 redux-saga를 편애하고 있습니다. 하지만 redux-thunk를 철저하게 까내릴 의도는 없습니다. 실제 저도 아직 사용하는 부분이 있습니다. 어쩔 수 없이 redux-thunk를 계속 사용할 수 밖에 없으신 분도 있을거라 생각되므로, 혹시 "redux-thunk라도 이렇게 쓰면 괴로움을 줄일수 있어!" 같은 의견이 있으시면 코멘트로 알려주세요. 387 | 388 | 389 | 390 | 그럼 redux-saga로 써봅시다. 391 | 392 | 393 | 394 | #### redux-saga 395 | 396 | `sagas.js` 397 | 398 | ```js 399 | // 추가 400 | function* handleRequestSearchByLocation() { 401 | while (true) { 402 | const action = yield take(SUCCESS_USER); 403 | const { payload, error } = yield call(API.searchByLocation, action.payload.location); 404 | if (payload && !error) { 405 | yield put(successSearchByLocation(payload)); 406 | } else { 407 | yield put(failureSearchByLocation(error)); 408 | } 409 | } 410 | } 411 | 412 | // 변경없음! 413 | function* handleRequestUser() { 414 | while (true) { 415 | const action = yield take(REQUEST_USER); 416 | const { payload, error } = yield call(API.user, action.payload); 417 | if (payload && !error) { 418 | yield put(successUser(payload)); 419 | } else { 420 | yield put(failureUser(error)); 421 | } 422 | } 423 | } 424 | 425 | export default function* rootSaga() { 426 | yield fork(handleRequestUser); 427 | yield fork(handleRequestSearchByLocation); // 추가 428 | } 429 | ``` 430 | 431 | 463 | 464 | 보시다싶이 `handleRequestUser` Task는 변경점이 없습니다. 새롭게 추가된 `handleRequestSearchByLocation` Task는 `handleRequestUser` Task와 거의 동일한 처리입니다. `rootSaga` Task는 `handleRequestSearchByLocation` Task를 기동하기위해 `fork` 를 하나 더 추가하고 있습니다. 조금 길어지지만, 처리의 흐름을 다음과 같습니다. 465 | 466 | 467 | 468 | 1. redux-saga의 Middleware가 `rootSaga` Task를 기동시킨다. 469 | 2. `fork` Effect에 따라 `handleRequestUser` 와 `handleRequestSearchByLocation` Task가 기동된다. 470 | 3. 각각의 Task에 대해 `take` Effect로부터 `REQUEST_USER` 와 `SUCCESS_USER` Action이 dispatch되는 것을 기다린다. 471 | 4. _(누군가가 `REQUEST_USER` Action을 dispatch한다.)_ 472 | 5. `call` Effect으로 `API.user` 함수를 불러와서, 통신처리가 끝나길 기다린다. 473 | 6. _(통신처리가 완료된다.)_ 474 | 7. `put` Effect를 사용하여 `SUCCESS_USER` Action을 dispatch한다. 475 | 8. `handleRequestSearchByLocation` 태스크가 다시 시작되어, `call` Effect로 `API.searchByLocation` 함수를 불러와서, 통신처리가 끝나길 기다린다. 476 | 9. _(통신처리가 완료된다.)_ 477 | 10. `put` Effect를 사용하여 `SUCCESS_SEARCH_BY_LOCATION` Action을 dispatch한다. 478 | 11. 각각의 Task에서 while 루프가 처음으로 돌아가 `take` 로 Action의 dispatch를 기다린다. 479 | 480 | 491 | 492 | 각각의 태스크를 주의해서 보면 단순한 것 밖에 하지 않기 때문에 이해하기 쉬워지지 않았나요? 게다가 이 코드를 확장하여 체인을 늘리거나, 체인을 순서를 바꾸거나 무엇을 하더라도 Task는 하나만 집중하고 있으므로 다른 무엇을 해도 그다지 영향을 받지 않습니다. 이러한 성질을 적극적으로 이용하여 Task가 너무 비대해지기 전에 계속해서 잘라 나누는 것으로 코드의 건전성을 유지할 수 있습니다. 493 | 494 | 495 | 496 | ### 테스트를 써보자 497 | 498 | 499 | 500 | redux-saga를 적극적으로 쓰고싶은 이유로서 테스트의 편리함을 들었습니다. 아직 다른사람에게 번역할만큼 노하우의 축적이 안되었지만, 분위기를 파악하는 정도로 써보겠습니다. 테스트 대상은 최초의 통신처리의 코드로 합니다. 복잡하게 되기 전의 예제입니다. 먼저 단순해보이는 `rootSaga` Task의 테스트부터 써보겠습니다. 그리고, 테스트코드는 mocha + power-assert 입니다. 501 | 502 | 503 | 504 | `sagas.js` 505 | 506 | ```js 507 | export default function* rootSaga() { 508 | yield fork(handleRequestUser); 509 | } 510 | ``` 511 | 512 | 여기에 대한 테스트 코드는 다음과 같습니다. 513 | 514 | 515 | 516 | `test.js` 517 | 518 | ```js 519 | describe('rootSaga', () => { 520 | it('launches handleRequestUser task', () => { 521 | const saga = rootSaga(); 522 | 523 | ret = saga.next(); 524 | assert.deepEqual(ret.value, fork(handleRequestUser)); 525 | 526 | ret = saga.next(); 527 | assert(ret.done); 528 | }); 529 | }); 530 | ``` 531 | 532 | Task를 포크하고 있는지 테스트를 한다. 라고 하면 어렵게 들리지만, 여기서 Task라는 건 단순한 Generator 함수로, Task가 돌려주는건 모두 단순한 오브젝트라는걸 떠올립시다. 고로 redux-saga에 두어진 Task의 테스트는 단순히 오브젝트를 비교하는 것으로 대부분 충분합니다. 이 `rootSaga` Task가 포크하고 있는지를 확인하기 위해 `fork` Effect로 오브젝트를 생성하여 비교하는 것으로 OK입니다. 이 expected로 지정된 오브젝트도 Task의 작성에 쓰여진 Effect Creator로 생성하여 문제없는것이 재밋는 포인트입니다. **테스트해야 할 것은 이 Task가 무엇을 하고 있는 가로써, 그에 앞서 무엇을 하는가는 알 필요 없습니다.** 533 | 534 | 535 | 536 | 이것만이면 테스트한 느낌이 안나므로 좀 더 복잡한 `handleRequestUser` Task도 테스트로 써봅니다. 537 | 538 | 539 | 540 | `sagas.js` 541 | 542 | ```js 543 | function* handleRequestUser() { 544 | while (true) { 545 | const action = yield take(REQUEST_USER); 546 | const { payload, error } = yield call(API.user, action.payload); 547 | if (payload && !error) { 548 | yield put(successUser(payload)); 549 | } else { 550 | yield put(failureUser(error)); 551 | } 552 | } 553 | } 554 | ``` 555 | 556 | 통신처리가 성공했는지 실패했는지로 분기됩니다. 그러므로 테스트도 각각의 케이스를 적습니다. 557 | 558 | 559 | 560 | `test.js` 561 | 562 | ```js 563 | describe('handleRequestUser', () => { 564 | let saga; 565 | beforeEach(() => { 566 | saga = handleRequestUser(); 567 | }); 568 | 569 | it('receives fetch request and succeeds to get data', () => { 570 | let ret = saga.next(); 571 | assert.deepEqual(ret.value, take(REQUEST_USER)); // (A') 572 | 573 | ret = saga.next({ payload: 123 }); // (A) 574 | assert.deepEqual(ret.value, call(API.user, 123)); // (B') 575 | 576 | ret = saga.next({ payload: 'GOOD' }); // (B) 577 | assert.deepEqual(ret.value, put(successUser('GOOD'))); 578 | 579 | ret = saga.next(); 580 | assert.deepEqual(ret.value, take(REQUEST_USER)); 581 | }); 582 | 583 | it('receives fetch request and fails to get data', () => { 584 | let ret = saga.next(); 585 | assert.deepEqual(ret.value, take(REQUEST_USER)); 586 | 587 | ret = saga.next({ payload: 456 }); 588 | assert.deepEqual(ret.value, call(API.user, 456)); 589 | 590 | ret = saga.next({ error: 'WRONG' }); 591 | assert.deepEqual(ret.value, put(failureUser('WRONG'))); 592 | 593 | ret = saga.next(); 594 | assert.deepEqual(ret.value, take(REQUEST_USER)); 595 | }); 596 | }); 597 | ``` 598 | 599 | 이것은 Generator함수의 테스트가 되므로 익숙하지 않으면 어렵겠네요. 접근하는 방법은 `next()`를 부를때 처음의 `yield`까지 실행되는 그때의 우변의 값을 랩핑한 것이 리턴값으로 나옵니다. 우변 값 자체는 `value` 프로퍼티에 격납되어있으므로 거기서 확인합니다. 600 | 601 | 602 | 603 | 지금, Task는 정지되어있습니다. 이를 재개하기 위해서는 더욱이 `next()`를 불러냅니다. 이 `next()`의 인수로써 넘겨준 것은, Task가 재개될 때에 `yield`로부터 나온 리턴 값이 됩니다. 즉 코드중의 `(A)`로 넘겨지는 것이 `(A')`로부터 나올거라 기대되는 리턴 값이라는 것이죠. 같은 방식으로 `(B)`로 넘겨진 통신결과의 오브젝트가 `(B')`의 `call` Effect로 불러진 결과가 됩니다. 604 | 605 | 606 | 607 | 마지막으로, 통신처리가 끝나면, 다시 리퀘스트를 기다리는 상태가 되었는지 확인합니다. Task를 동기적으로 썻기에, 테스트 코드도 동기적으로 되었습니다. 608 | 609 | 610 | 611 | 조금 설명이 빠른 느낌도 있지만, 왜 redux-saga의 실행모델로 제공된 커맨드를 사용하여 Task를 만드는 것인가가 이해되었다고 생각합니다. 모든것이 예측 가능한 테스트가 간단하게 되어, 복잡한 목을 만들 필요성도 최소한으로 되기 때문입니다. 612 | 613 | 614 | 615 | ## 셋업 616 | 617 | 618 | 619 | Task의 설명으로 이것 저것 넘어가버렸기에, 조금더 redux-saga의 설정에 대해 주의점도 알려드리겠습니다. 앞서 말하지만, 기본적인건 공식 문서를 읽는 것이 가장 좋습니다. 이후 대폭 바뀔 가능성은 적지만, 그럴 때 기댈 수 있는건 역시 공식이니까요. 620 | 621 | 622 | 623 | ### redux-saga의 적용 624 | 625 | 626 | 627 | 예제 코드의 디렉토리를 보면 금새 알아차리셨을지 모르겠지만, redux-saga를 쓸 땐 2가지를 합니다. 하나는 Store에 Middleware를 집어넣고, 다른 하나는 Task를 정의합니다. 이하는 전형적인 셋업 코드입니다. `redux-logger`는 필요하지 않으면 지워주세요. 628 | 629 | 630 | 631 | `store.js` 632 | 633 | ```js 634 | import { createStore, applyMiddleware } from 'redux'; 635 | import createSagaMiddleware from 'redux-saga'; 636 | import logger from 'redux-logger'; 637 | import reducer from './reducers'; 638 | import rootSaga from './sagas'; 639 | 640 | export default function configureStore(initialState) { 641 | const sagaMiddleware = createSagaMiddleware(); 642 | const store = createStore( 643 | reducer, 644 | initialState, 645 | applyMiddleware( 646 | sagaMiddleware, logger() 647 | ) 648 | ); 649 | sagaMiddleware.run(rootSaga); 650 | return store; 651 | }; 652 | ``` 653 | 654 | ### Store를 초기화시키는 타이밍 655 | 656 | 657 | 658 | 이전에 겪었던 일로, 의도하지 않은 페이지에서 redux-saga가 기동되어 통신처리가 시작된 적이 있었습니다. 원인은 `store.js`에 있었습니다. 659 | 660 | 661 | 662 | `store.js` 663 | 664 | ```js 665 | const sagaMiddleware = createSagaMiddleware(); 666 | const store = createStore( 667 | reducer, 668 | applyMiddleware( 669 | sagaMiddleware, logger() 670 | ) 671 | ); 672 | sagaMiddleware.run(rootSaga); 673 | export default store; 674 | ``` 675 | 676 | `configureStore` 함수를 export하는 대신에 작성한 Store를 export하고 있네요. 그리고 `saga.js`는 이런 상태였습니다. 677 | 678 | 679 | 680 | `sagas.js` 681 | 682 | ```js 683 | export default function* rootSaga() { 684 | yield fork(loadHogeHoge); 685 | } 686 | ``` 687 | 688 | 초기화시 무언가를 읽어들이는 형태의 태스크입니다. 689 | 690 | 691 | 692 | `index.js` 693 | 694 | ```jsx 695 | import store from './store.js'; 696 | 697 | // ... 698 | 699 | const el = document.getElementById('container'); 700 | if (el) { 701 | ReactDOM.render( 702 | 703 | 704 | , 705 | ); 706 | } 707 | ``` 708 | 709 | 이미 파악하셨을거라 생각되지만, 위의 구성이면 Provider 컴포넌트가 마운트되었는지 말았는지에 상관없이 Store가 초기화되어 Middleware도 초기화되고 맙니다. 그 결과, 기동시 리퀘스트를 보내는 형태의 Task라면 엉뚱한 타이밍에 사태가 벌어지는 것입니다. 조심하도록 합시다. (저도 반성을..) 710 | 711 | 712 | 713 | ### Middleware의 실행 타이밍 714 | 715 | 716 | 717 | v0.10.0 부터 redux-saga의 기동방법이 바뀌었습니다. 718 | 719 | 720 | 721 | `before.js` 722 | 723 | ```js 724 | const store = createStore( 725 | reducer, 726 | applyMiddleware(createSagaMiddleware(rootSaga)) 727 | ) 728 | ``` 729 | 730 | 이렇게 쓰던 것이 731 | 732 | 733 | 734 | `after.js` 735 | 736 | ```js 737 | const sagaMiddleware = createSagaMiddleware(); 738 | const store = createStore( 739 | reducer, 740 | initialState, 741 | applyMiddleware(sagaMiddleware) 742 | ); 743 | sagaMiddleware.run(rootSaga); 744 | ``` 745 | 746 | 이렇게 되었습니다. 초기실행 Task를 Middleware의 생성시가 아니라, Store의 초기화가 완료된 뒤에 `run` 메소드를 부르는 것으로 기동합니다. 747 | 748 | 749 | 750 | ### 디버그 751 | 752 | 753 | 754 | Task는 하나하나 독립하게 실행되므로, 할 것을 제한해서 단순하게 유지하면 디버그 툴이 필요할 만큼 복잡해지진 않겠지만, 일단, redux-saga에는 모니터링 툴을 집어 넣을 수 있는 인터페이스가 준비되어 있습니다. `effectTriggered`, `effectResolved`, `effectRejected`, `effectCancelled` 의 4개의 프로퍼티를 가지는 오브젝트를 [createSagaMiddleware](http://yelouafi.github.io/redux-saga/docs/api/index.html#createsagamiddlewareoptions) 함수의 오브젝트로써 넘겨줍니다. 755 | 756 | 757 | 758 | `store.js` 759 | 760 | ```js 761 | import sagaMonitor from './saga-monitor'; 762 | 763 | export default function configureStore(initialState) { 764 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); 765 | const store = createStore(... 766 | ``` 767 | 768 | 모니터의 적용은 일단 [redux-saga의 examples/sagaMonitor](https://github.com/yelouafi/redux-saga/blob/master/examples%2FsagaMonitor%2Findex.js)를 써봐주세요. 그리고, 이 모니터는 디폴트로 아무것도 표시하지 않으므로, 코드중의 `VERBOSE`라는 변수를 `true`로 바꾸셔야 떠들기 시작합니다. 단, `redux-logger`와 같이 로그가 계속해서 흘러가는 걸 보는게 아니라, 필요할 때 브라우저툴로부터 `window.$$LogSagas` 함수를 불러서 Task tree를 지켜보는게 주 목적 입니다. 실행해보면 다음과 같이 나타납니다. 하지만, 그다지 멋있어 보이진 않으므로 [D3.js](https://d3js.org/)로 시각화 툴을 만들어 볼 생각입니다. 769 | 770 | 771 | 772 | ![saga-monitor.png](./assets/deal-with-async-process-by-redux-saga/saga-monitor.png "saga-monitor.png") 773 | 774 | 이 다음의 [API 요청시의 스로틀링](http://qiita.com/kuy/items/716affc808ebb3e1e8ac#api%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%81%AE%E3%82%B9%E3%83%AD%E3%83%83%E3%83%88%E3%83%AA%E3%83%B3%E3%82%B0)에서 소개하는 예제에는 모니터가 포함되어 있으므로 [데모](http://kuy.github.io/redux-saga-examples/throttle.html)로 시험해 보실 수 있습니다. 775 | 776 | 777 | 778 | ## 실전 redux-saga 779 | 780 | 781 | 782 | redux-saga에는 [풍부한 예제](https://github.com/yelouafi/redux-saga/tree/master/examples)가 준비되어 있습니다. 뭔가 곤란할 때, 힌트가 없나 싶을 때 보면 좋습니다. ...하지만, 이걸로 마무리하기엔 아쉽기에 다른 이용 예를 소개합니다. 특히 몇일전 릴리즈된 [0.10.0](https://github.com/yelouafi/redux-saga/releases/tag/v0.10.0)의 신기능인 [eventChannel](http://yelouafi.github.io/redux-saga/docs/api/index.html#eventchannelsubscribe-buffer-matcher)를 사용한 예제는 그다지 없으므로 참고가 될 듯 합니다. 783 | 784 | 785 | 786 | > 역자주: 여기서 소개되는 실전 예제들의 소스 코드와 데모들은 밑의 링크에서 찾으실 수 있습니다. 이 포스팅에 설명되지 않은 예제도 있으니 이쪽도 훑어 봐주시면 더욱 좋을 듯합니다. 787 | > 788 | > 소스 코드 : 789 | > 데모 : 790 | 791 | ### 자동 완성 792 | 793 | 794 | 795 | 텍스트 필드에 자동 완성 기능을 넣을 때, 단순하게 만든다면 dispatch된 Action을 `take`로 받아들여 `call`로 요청을 발행하여 결과를 `put`으로 하면 좋아보입니다. 단, 이것은 일반적인 통신처리로써, 그대로 적용해버리면 입력 할 때마다 리퀘스트를 보내기에 그다지 좋지 못합니다. 이 예제에서는 초기의 못생긴 자동 완성 기능을 멋지게 고쳐나가는 과정을 보여드리겠습니다. 796 | 797 | 798 | 799 | 데모: [Autocomplete](http://kuy.github.io/redux-saga-examples/autocomplete.html) 800 | 예제 코드: [kuy/redux-saga-examples > autocomplete](https://github.com/kuy/redux-saga-examples/tree/master/autocomplete) 801 | 802 | 804 | 805 | #### 초기 구현 806 | 807 | 808 | 809 | `sagas.js` 810 | 811 | ```js 812 | function* handleRequestSuggests() { 813 | while (true) { 814 | const { payload } = yield take(REQUEST_SUGGEST); 815 | const { data, error } = yield call(API.suggest, payload); 816 | if (data && !error) { 817 | yield put(successSuggest({ data })); 818 | } else { 819 | yield put(failureSuggest({ error })); 820 | } 821 | } 822 | } 823 | 824 | export default function* rootSaga() { 825 | yield fork(handleRequestSuggests); 826 | } 827 | ``` 828 | 829 | 통신처리의 코드 그대로이네요. 참고로 실은 이 코드, 큰 문제를 지니고 있습니다. 무엇인가 하면, 통신처리의 완료를 기다리는 동안 dispatch되는 Action을 흘려보내 버립니다. 예제에서는 통신처리부분을 더미로써 `setTimeout`을 사용하여 시간이 걸리듯이 만들었기 때문에, 이 부분의 시간을 3초 정도로 바꾸면 확실하게 보일겁니다. 830 | 831 | 832 | 833 | #### 흘림방지 대책 834 | 835 | 836 | 837 | 이런 이유로 멋지게 만들기 앞서 버그를 잡읍시다. 문제는 `call`로 `API.suggest`의 결과를 기다리는 곳입니다. 이것이 불러지는걸 기다리지 않고 `take`로 돌아가면 흘리지는 않게됩니다. 그렇다면 `fork`로 새로운 Task를 기동시키는 것이 좋아보이네요. 838 | 839 | 840 | 841 | `sagas.js` 842 | 843 | ```js 844 | function* runRequestSuggest(text) { 845 | const { data, error } = yield call(API.suggest, text); 846 | if (data && !error) { 847 | yield put(successSuggest({ data })); 848 | } else { 849 | yield put(failureSuggest({ error })); 850 | } 851 | } 852 | 853 | function* handleRequestSuggest() { 854 | while (true) { 855 | const { payload } = yield take(REQUEST_SUGGEST); 856 | yield fork(runRequestSuggest, payload); 857 | } 858 | } 859 | 860 | export default function* rootSaga() { 861 | yield fork(handleRequestSuggest); 862 | } 863 | ``` 864 | 865 | 이런 형태가 됩니다. 이것으로 `handleRequestSuggest` Task로 통신처리까지 핸들링하고 있지만, `call` 이후의 부분을 따로 Task로 나누었습니다. 아무리 이번과 같은 문제가 일어나지 않는 다고 해도, Action을 감시하는 Task와 통신처리를 하는 태스크를 나누는 것이 좋아보입니다. 이걸로 막힘없이 리퀘스트도 날릴 수 있네요! 잘 됐네요! 866 | 867 | 868 | 869 | #### 다른 해결방법 870 | 871 | 872 | 873 | 자, 버그는 고쳤지만 공부를 위해 잠깐 옆길로 새겠습니다. redux-saga로 Task를 쓰고 있으면 위의 패턴이 빈번하게 나오기 때문에 [`takeEvery`](http://yelouafi.github.io/redux-saga/docs/api/index.html#takeeverypattern-saga-args)가 준비되어있습니다. 이것을 사용하여 다시 써봅시다. 874 | 875 | 876 | 877 | `sagas.js` 878 | 879 | ```js 880 | import { call, put, fork, takeEvery } from 'redux-saga/effects'; 881 | 882 | function* runRequestSuggest(action) { 883 | const { data, error } = yield call(API.suggest, action.payload); 884 | if (data && !error) { 885 | yield put(successSuggest({ data })); 886 | } else { 887 | yield put(failureSuggest({ error })); 888 | } 889 | } 890 | 891 | function* handleRequestSuggest() { 892 | yield takeEvery(REQUEST_SUGGEST, runRequestSuggest); 893 | } 894 | 895 | export default function* rootSaga() { 896 | yield fork(handleRequestSuggest); 897 | } 898 | ``` 899 | 900 | `takeEvery` 는 지정한 Action의 dispatch를 기다려, 그 Action을 인수로써 Task를 기동합니다. 이전엔 헬퍼 함수로써 제공되었지만, [0.14.0](https://github.com/redux-saga/redux-saga/releases/tag/v0.14.0)부터 정식으로 Effect가 되었습니다. 단, 헬퍼의 `takeEvery`는 없어질 예정이므로 바꾸실걸 추천합니다. 그리고, Effect로써 `takeEvery`와 헬퍼의 `takeEvery`는 다릅니다. 따라서 Effect인 `takeEvery`를 `yield*`로 사용해서는 안됩니다. 901 | 902 | 903 | 904 | #### 멋진 구현 905 | 906 | 907 | 908 | 버그도 고쳤고, 이걸로 고칠 준비가 되었습니다. 어떠한 동작이 좋은지 정리하기 위해서 시나리오를 써봅시다. 909 | 910 | 911 | 912 | 1. 1글자를 입력한다. 913 | 2. 바로 리퀘스트를 날리지 않는다. 914 | 3. 몇 글자 더 입력한다. 915 | 4. 아직 리퀘스트를 보내지 않는다. 916 | 5. 아무것도 입력이 없는 상태가 일정시간 지속되면 리퀘스트를 보낸다. 917 | 918 | 923 | 924 | 기본적으로는 일정 시간 기다리면 리퀘스트를 개시하는 지연실행 Task를 정의하여, 입력이 있을 때마다 그것을 기동하게 됩니다. 단, 입력이 있었을때에 이미 지연 Task가 기동되어 있을 때는, 먼저 그것을 취소하고나서 새로운 Task를 기동시킬 필요가 있습니다. 따라서 지연실행 Task는 아무리 많아도 1개만 실행됩니다. 그럼 코드를 봅시다. 925 | 926 | 927 | 928 | `sagas.js` 929 | 930 | ```js 931 | import { delay } from 'redux-saga'; 932 | import { call, put, fork, take } from 'redux-saga/effects'; 933 | 934 | function* runRequestSuggest(text) { 935 | const { data, error } = yield call(API.suggest, text); 936 | if (data && !error) { 937 | yield put(successSuggest({ data })); 938 | } else { 939 | yield put(failureSuggest({ error })); 940 | } 941 | } 942 | 943 | function forkLater(task, ...args) { 944 | return fork(function* () { 945 | yield call(delay, 1000); 946 | yield fork(task, ...args); 947 | }); 948 | } 949 | 950 | function* handleRequestSuggest() { 951 | let task; 952 | while (true) { 953 | const { payload } = yield take(REQUEST_SUGGEST); 954 | if (task && task.isRunning()) { 955 | task.cancel(); 956 | } 957 | task = yield forkLater(runRequestSuggest, payload); 958 | } 959 | } 960 | 961 | export default function* rootSaga() { 962 | yield fork(handleRequestSuggest); 963 | } 964 | ``` 965 | 966 | 주목할 포인트는 2개입니다. 1번째 포인트는 넘겨진 Task를 지연처리하는 `forkLater`함수는 `fork` Effect를 돌려주는 함수입니다. `call` Effect로 `delay` 함수를 불러와 일정시간을 기다리고, `delay` 함수가 돌려주는 Promise가 resolve되면 제어가 돌아와서 Task를 `fork`합니다. 참고로 `delay`함수는 `redux-saga` 모듈로부터 읽어들입니다. 2번째 포인트는 `handleRequestSuggest` Task에 실행 의 지연실행 Task가 있는 경우, 그것을 취소하고나서 기동시키는 부분입니다. `fork` Effect를 `yield` 했을 때 리턴 값은 [Task 인터페이스](http://yelouafi.github.io/redux-saga/docs/api/index.html#task)를 가지는 오브젝트로 기동된 Task의 상태를 가져오거나 취소하는 등, 이것저것 할 수 있습니다. 967 | 968 | 969 | 970 | 이러한 방법으로 원하는 동작은 만들어 졌지만, `handleRequestSuggest` Task의 "Action을 기다리는 리퀘스트를 시작한다"라는 역할이 알아보기 어려워졌습니다. 가능한 원래의 Task처럼, 하고 싶은 의도를 전하는 코드라면 좋았겠지요. 971 | 972 | 973 | 974 | `before.js` 975 | 976 | ```js 977 | function* handleRequestSuggest() { 978 | while (true) { 979 | const { payload } = yield take(REQUEST_SUGGEST); 980 | yield fork(runRequestSuggest, payload); 981 | } 982 | } 983 | ``` 984 | 985 | 자동 완성의 기능만 생각하면 멋지게 되었으니, 이제 코드도 멋지게 만들어 봅시다. 986 | 987 | 988 | 989 | #### 더욱 멋진 구현 990 | 991 | 992 | 993 | 어떻게 하냐면, `handleRequestSuggest` Task에 흩어진 취소를 처리하는 부분을 분리시킵니다. 이는 1개의 Task로 처리하는 일을 줄여 역할을 명확하게 하기 위해 적극적으로 해보고 싶었던 개선 사항입니다. 994 | 995 | 996 | 997 | `sagas.js` 998 | 999 | ```js 1000 | function* runRequestSuggest(text) { 1001 | const { data, error } = yield call(API.suggest, text); 1002 | if (data && !error) { 1003 | yield put(successSuggest({ data })); 1004 | } else { 1005 | yield put(failureSuggest({ error })); 1006 | } 1007 | } 1008 | 1009 | function createLazily(msec = 1000) { 1010 | let ongoing; 1011 | return function* (task, ...args) { 1012 | if (ongoing && ongoing.isRunning()) { 1013 | ongoing.cancel(); 1014 | } 1015 | ongoing = yield fork(function* () { 1016 | yield call(delay, msec); 1017 | yield fork(task, ...args); 1018 | }); 1019 | } 1020 | } 1021 | 1022 | function* handleRequestSuggest() { 1023 | const lazily = createLazily(); 1024 | while (true) { 1025 | const { payload } = yield take(REQUEST_SUGGEST); 1026 | yield fork(lazily, runRequestSuggest, payload); 1027 | } 1028 | } 1029 | 1030 | export default function* rootSaga() { 1031 | yield fork(handleRequestSuggest); 1032 | } 1033 | ``` 1034 | 1035 | `handleRequestSuggest` Task가 매우 깔끔해졌습니다. `fork(runRequestSuggest, payload)`였던 부분이 `fork(lazily, runRequestSuggest, payload)`로 바뀌었을 뿐이기에 변화는 많진 않습니다. 하지만 적어도 영어처럼 "fork lazily"로 읽어지기에 의도를 전달하기가 쉬워졌을 겁니다. 1036 | 1037 | 1038 | 1039 | 마법처럼 지연 실행해주는 `lazily` Task이지만 이것은 `createLazily` 함수로 생성하고 있습니다. 실행중의 Task를 존속시키기 위해 클로져로 만들 필요가 있었습니다. 하는 일은 이전의 구현과 동일합니다. 1040 | 1041 | 1042 | 1043 | 이걸로 기능도 구현도 멋지게 되었습니다. 1044 | 1045 | 1046 | 1047 | #### 연구과제 1048 | 1049 | 1050 | 1051 | - 지연실행이 시작되기까지 아무것도 표시하지 않는 문제를 해결한다. 1052 | - `takeLatest` 헬퍼함수를 써서 다시 쓴다. 1053 | 1054 | 1056 | 1057 | ### API 요청시의 스로틀링 1058 | 1059 | 1060 | 1061 | 데모: [Throttle](http://kuy.github.io/redux-saga-examples/throttle.html) 1062 | 예제: [kuy/redux-saga-examples > throttle](https://github.com/kuy/redux-saga-examples/tree/master/throttle) 1063 | 1064 | 1066 | 1067 | 포스팅 리스트와 같이 많은 컨텐츠를 한번에 읽어와서, 더욱이 각각의 컨텐츠마다 리퀘스트를 요청하면, 컨텐츠의 수만큼 리퀘스트를 동시에 보내게되니 심각한 일이 일어나겠죠. 서버부하와 같은 문제가 없다고 해도, Dos공격으로 판단되어 리퀘스트가 차단되는 일도 있을 겁니다. 또 통신처리의 경우, 대량발생하는 Action에 따른 Task의 동시 실행 가능한 수 이상은 시작하지 않고 기다리게 만들어서, 앞선 Task가 완료되면 순서대로 다음 Task를 실행시키는 큐(Queue)가 필요할때가 있습니다. 이번 예제는 기동중인 Task의 수를 조절하는 스로틀링을 redux-saga로 구현합니다. 1068 | 1069 | 1070 | 1071 | `sagas.js` 1072 | 1073 | ```js 1074 | const newId = (() => { 1075 | let n = 0; 1076 | return () => n++; 1077 | })(); 1078 | 1079 | function something() { 1080 | return new Promise(resolve => { 1081 | const duration = 1000 + Math.floor(Math.random() * 1500); 1082 | setTimeout(() => { 1083 | resolve({ data: duration }); 1084 | }, duration); 1085 | }); 1086 | } 1087 | 1088 | function* runSomething(text) { 1089 | const { data, error } = yield call(something); 1090 | if (data && !error) { 1091 | yield put(successSomething({ data })); 1092 | } else { 1093 | yield put(failureSomething({ error })); 1094 | } 1095 | } 1096 | 1097 | function* withThrottle(job, ...args) { 1098 | const id = newId(); 1099 | yield put(newJob({ id, status: 'pending', job, args })); 1100 | } 1101 | 1102 | function* handleThrottle() { 1103 | while (true) { 1104 | yield take([NEW_JOB, RUN_JOB, SUCCESS_JOB, FAILURE_JOB, INCREMENT_LIMIT]); 1105 | while (true) { 1106 | const jobs = yield select(throttleSelector.pending); 1107 | if (jobs.length === 0) { 1108 | break; // No pending jobs 1109 | } 1110 | 1111 | const limit = yield select(throttleSelector.limit); 1112 | const num = yield select(throttleSelector.numOfRunning); 1113 | if (limit <= num) { 1114 | break; // No rooms to run job 1115 | } 1116 | 1117 | const job = jobs[0]; 1118 | const task = yield fork(function* () { 1119 | yield call(job.job, ...job.args); 1120 | yield put(successJob({ id: job.id })); 1121 | }); 1122 | yield put(runJob({ id: job.id, task })); 1123 | } 1124 | } 1125 | } 1126 | 1127 | function* handleRequestSomething() { 1128 | while (true) { 1129 | yield take(REQUEST_SOMETHING); 1130 | yield fork(withThrottle, runSomething); 1131 | } 1132 | } 1133 | 1134 | export default function* rootSaga() { 1135 | yield fork(handleRequestSomething); 1136 | yield fork(handleThrottle); 1137 | } 1138 | ``` 1139 | 1140 | 자동완성 예제는 1개의 Task만 동시에 실행시키고, 새로운 Task가 오면 처리중인 Task를 취소시키고 나서 기동을 하였습니다. 이미 Task가 기동중인지 아닌지를 판정하기 위해 상태를 가질 필요가 있었고, 그것을 클로져 내에서 다루게 하는 어프로치였습니다. 스로틀링으로도 실행중의 Task를 파악할 필요가 있기 때문에 무언가의 상태를 기다릴 필요가 있는 점이 공통됩니다. 하지만 이번엔 다른 어프로치로, 상태를 Task 내부에서 가지지 않고, 대신 Store에다 넣어둡니다. 이렇게 하면 실행 상태가 뷰에서 실시간 표시가 가능합니다. 1141 | 1142 | 1143 | 1144 | 위는 `sagas.js`의 코드만 보여주었지만, 이번엔 상태를 Store가 가지게 하므로 전체를 이해하기 위해 `reducers.js`도 중요하니 한번 훑어봐주세요. 1145 | 1146 | 1147 | 1148 | #### 2개의 Task 1149 | 1150 | 1151 | 1152 | 구현은 크게 나누면 2개의 Task, `handleRequestSomething`와 `handleThrottle`로 나뉩니다. 전자는 `REQUEST_SOMETHING` Action의 dispatch를 감시하여 실행해야할 Task만을 보내줍니다. 후자는 조금 복잡합니다. `handleRequestSomething` Task로 부터 실행 요청된 Task를 일단 큐에 넣어두고, 동시 실행 수를 조정하면서 처리해갑니다. 스로틀링이 없는 실행 `fork(runSomething)`과 스로틀링이 있는 실행 `fork(withThrottle, runSomething)`에선 코드의 차이가 조금만 있도록 만들었습니다. 1153 | 1154 | 1155 | 1156 | #### 2중 while 루프 1157 | 1158 | 1159 | 1160 | `handleThrottle` Task를 보면 조금 낯설은 2중의 while 루프가 있습니다. 첫번째 루프는 익숙한 패턴이므로 괜찮을 겁니다. 2번째 루프는 실행가능한 job의 수에 여유가 있는한 job의 실행을 시작 위한 것입니다. 코드의 가독성을 우선해서 while 루프로 만들어져 있지만, 실행 가능한 job의 수와 대기 상태의 job을 만들어 한번에 실행시켜도 괜찮습니다. 1161 | 1162 | 1163 | 1164 | #### 연구과제 1165 | 1166 | 1167 | 1168 | - 다중 실행 큐 1169 | - 우선 순위 1170 | 1171 | 1173 | 1174 | ### 인증 절차(세션 유지) 1175 | 1176 | 1177 | 1178 | redux-saga로 인증처리를 어떻게 다루는지를 생각해봅시다. 1179 | 1180 | 1181 | 1182 | 만들고 싶은건, 유저가 로그인하여, 인증받고, 성공하면 화면전이를 하거나, 실패하면 그 이유를 표시하고, 로그아웃하면 다시 대기상태로 돌아가는, 인증 라이프사이클의 전체입니다. 이러한 처리를 서버사이드에 구현하면, Cookie와 같은 토큰을 가지고 날아온 리퀘스트가 어떤 유저로부터 온 건지를 식별할 필요가 있습니다. 즉 처리 자체는 리퀘스트 단위로 되어 토막난 상태입니다. 그것을 redux-saga의(뭐 예제는 혼자서 하므로 식별하는게 그다지 의미없지만) Task가 일시 정지시키는게 가능하다는 특징을 살려서, 인증 라이프사이클 전체를 1개의 Task가 관리하도록 만들어보겠습니다. 한마디로 세션 유지에 Task를 쓰는 형식입니다. 1183 | 1184 | 1185 | 1186 | 예제는 어떻게 돌아가는지 분위기 파악을 위한 코드입니다. 원래 [Gist](https://gist.github.com/kuy/18aca23ac885d36eeb5f687a9ad7eff1)에다 쓴 것입니다. 로그인이 성공하면 `react-router-redux`를 사용하여 대시보드 페이지로 이동합니다. 1187 | 1188 | 1189 | 1190 | `sagas.js` 1191 | 1192 | ```js 1193 | import { push } from 'react-router-redux'; 1194 | 1195 | function* authSaga() { 1196 | while (true) { 1197 | // 로그인 할 때 까지 기다린다. 1198 | const { user, pass } = yield take(REQUEST_LOGIN); 1199 | 1200 | // 인증처리 요청 (여기선 try-catch를 쓰지않고, 리턴 값에 에러정보가 포함되는 스타일) 1201 | const { token, error } = yield call(authorize, user, pass); 1202 | if (!token && error) { 1203 | yield put({ type: FAILURE_LOGIN, payload: error }); 1204 | continue; // 인증에 실패하면 재시도와 함께 처음으로 돌아갑니다. 1205 | } 1206 | 1207 | // 로그인 성공의 처리 (토큰 유지 등) 1208 | yield put({ type: SUCCESS_LOGIN, payload: token }); 1209 | 1210 | // 로그아웃 할 때까지 기다린다. 1211 | yield take(REQUEST_LOGOUT); 1212 | 1213 | // 로그아웃 처리 (토큰 정리등) 1214 | yield call(SUCCESS_LOGOUT); 1215 | } 1216 | } 1217 | 1218 | function* pageSaga() { 1219 | while (true) { 1220 | // 로그인에 성공할 때까지 기다린다. 1221 | yield take(SUCCESS_LOGIN); 1222 | 1223 | // 대시보드로 이동한다. 1224 | yield put(push('/dashboard')); 1225 | } 1226 | } 1227 | ``` 1228 | 1229 | 1265 | 1266 | #### 2개의 Task 1267 | 1268 | 1269 | 1270 | 해야 할 일은 인증처리의 라이프사이클을 관리하는 것과, 로그인 성공시에 페이지 전이를 하는 2개입니다. 이것들은 물론 1개의 Task로 구현 가능하지만, redux-saga's way(라는게 있을지는 모르지만)에 따라 제대로 역할별로 Task를 나누어, 각각 `authSaga`와 `pageSaga`로써 정의합니다. 1271 | 1272 | 1273 | 1274 | 여기까지의 2개의 예제에선 필요에 따라 Task 내부에 외부의 상태를 가지고 있었습니다. 이번 예제는 인증처리의 라이프사이클이 어디까지 되어있나를 상태로 가지는 것을 적극적으로 활용하는 예입니다. 1개의 처리는 1개의 태스크가 항상 붙어있게 되는데 redux-saga가 제공하는 태스크 실행 환경 덕분입니다. 덕분에 코드가 매우 직관적으로 됩니다. 1275 | 1276 | 1277 | 1278 | #### 연구과제 1279 | 1280 | 1281 | 1282 | - 복수 세션의 유지 1283 | 1284 | 1285 | 1286 | ### Socket.IO 1287 | 1288 | 1289 | 1290 | 여기서부터 조금 다른 종류의 예를 소개하려 합니다. 먼저 [Socket.IO](http://socket.io/)와의 연계입니다. 1291 | 1292 | 1293 | 1294 | 예제코드: [kuy/redux-saga-chat-examples](https://github.com/kuy/redux-saga-chat-example) 1295 | 1296 | 1297 | 1298 | 밑은 예제에서 가져온 내용입니다. 1299 | 1300 | 1301 | 1302 | `sagas.js` 1303 | 1304 | ```js 1305 | function subscribe(socket) { 1306 | return eventChannel(emit => { 1307 | socket.on('users.login', ({ username }) => { 1308 | emit(addUser({ username })); 1309 | }); 1310 | socket.on('users.logout', ({ username }) => { 1311 | emit(removeUser({ username })); 1312 | }); 1313 | socket.on('messages.new', ({ message }) => { 1314 | emit(newMessage({ message })); 1315 | }); 1316 | socket.on('disconnect', e => { 1317 | // TODO: handle 1318 | }); 1319 | return () => {}; 1320 | }); 1321 | } 1322 | 1323 | function* read(socket) { 1324 | const channel = yield call(subscribe, socket); 1325 | while (true) { 1326 | const action = yield take(channel); 1327 | yield put(action); 1328 | } 1329 | } 1330 | 1331 | function* write(socket) { 1332 | while (true) { 1333 | const { payload } = yield take(`${sendMessage}`); 1334 | socket.emit('message', payload); 1335 | } 1336 | } 1337 | 1338 | function* handleIO(socket) { 1339 | yield fork(read, socket); 1340 | yield fork(write, socket); 1341 | } 1342 | 1343 | function* flow() { 1344 | while (true) { 1345 | let { payload } = yield take(`${login}`); 1346 | const socket = yield call(connect); 1347 | socket.emit('login', { username: payload.username }); 1348 | 1349 | const task = yield fork(handleIO, socket); 1350 | 1351 | let action = yield take(`${logout}`); 1352 | yield cancel(task); 1353 | socket.emit('logout'); 1354 | } 1355 | } 1356 | 1357 | export default function* rootSaga() { 1358 | yield fork(flow); 1359 | } 1360 | ``` 1361 | 1362 | Socket.IO으로부터 메세지의 수신에 [eventChannel](http://yelouafi.github.io/redux-saga/docs/api/index.html#eventchannelsubscribe-buffer-matcher)을 사용하고 있습니다. 받은 Socket.IO의 이벤트마다 Redux의 Action으로 맵핑해주어 `put`으로 dispatch하고 있습니다. 거기에 Task의 연쇄적인 취소 역시 쓰여지고 있습니다. 1363 | 1364 | 1365 | 1366 | 이 예제는 대충 작동하는지만 돌려본 상태입니다. Read/Write의 부분이나 복수 채널 대응, 하나하나 맵핑이 귀찮아서 이벤트이름을 그대로 Action Types으로 쓰거나, Socket.IO의 통신상태를 모니터링하거나, 어떻게 다룰것인가에 아직 고민중입니다. 언젠가 그것들을 라이브러리로 만들어냈으면 좋겠다라고 생각하고 있습니다. 1367 | 1368 | 1369 | 1370 | ### Firebase (Authentication + Realtime Database) 1371 | 1372 | 데모: [Miniblog](http://kuy.github.io/redux-saga-examples/microblog.html) 1373 | 예제코드 [kuy/redux-saga-examples > microblog](https://github.com/kuy/redux-saga-examples/tree/master/microblog) 1374 | 1375 | 1377 | 1378 | [최신 대폭 업데이트](http://jp.techcrunch.com/2016/05/20/20160518google-turns-firebase-into-its-unified-platform-for-mobile-developers/)가 있었던 [Firebase](firebase.google.com)를 써서 시험삼아 redux-saga와 연계시킨 예제입니다. 테마는 트위터와 같은 미니 블로그 서비스이지만, 구현이 덜되서 채팅 앱처럼 되어있습니다... 브라우저의 시크릿모드에 여러개를 열어 각각의 이름으로 투고하면 실시간으로 갱신됩니다. 1379 | 1380 | 1381 | 1382 | 이러한 예제는 예제 이상의 의미는 없습니다. 어떻게 redux-saga로 Firebase나 Socket.IO를 쓸 것인가에 너무 집착하지는 말아주세요. 왜냐면 redux-saga는 기능적으론 Middleware의 서브셋이므로, redux-saga로 할 수 있는건 Middleware로도 가능합니다. 게다가 redux-saga로 만들면, 프로젝트의 도입할때 redux-saga가 필수가 됩니다. Middleware로 가능한 것을 redux-saga로 만들어서, 도입 장벽을 높이게 되면 의미가 없습니다. 이러한 기능은 순수하게 Middleware나 Store Enhancer 수준으로 구현하는것이 좋지 않을까 싶습니다. 1383 | 1384 | 1385 | 1386 | ### PubNub 1387 | 1388 | [redux-saga-chat-example](https://github.com/kuy/redux-saga-chat-example)이라는 redux-saga와 Socket.IO를 합친 채팅 앱을 만드니, 왜인지 [PubNub와 함께 쓸려면 어떻게 해야하지?](https://github.com/kuy/redux-saga-chat-example/issues/2#issuecomment-217758099)라는 질문 있어서 예제를 써봤습니다. 1389 | 1390 | 1391 | 1392 | ## 만병통치약은 없다 1393 | 1394 | 1395 | 1396 | redux-saga의 사용법을 다양한 각도에서 봤습니다. 뭐든 할 수 있을 것 같지만, redux-saga에도 제약이 있습니다. 모든 Middleware의 처리를 이식하는건 불가능 합니다. 예를들어 Middleware처럼 Action을 솎아 내는 건 못합니다. 그래서 이번 샘플은 [Redux의 middleware를 적극적으로 써보자](http://qiita.com/kuy/items/57c6007f3b8a9b267a8e)에서 소개한 [Action을 Dispatch하기 전에 브라우저 확인 다이얼로그를 표시하자](http://qiita.com/kuy/items/57c6007f3b8a9b267a8e#action%E3%82%92dispatch%E3%81%99%E3%82%8B%E5%89%8D%E3%81%AB%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%AE%E7%A2%BA%E8%AA%8D%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B)를 그대로 이식하는건 불가능했습니다. 같은 일을 하기엔 우선 다른 Action을 dispatch시켜, 확인 다이럴로그에 Yes가 나오면 원래의 Action을 dispatch하는 식의 변경이 필요했습니다. 이러면 본말전도가 되므로 그냥 Middleware를 쓰는 게 좋은 경우입니다. 덧붙여 이러한 제약은 [Redux Middleware in Depth](http://qiita.com/kuy/items/c6784fe443f1d5c7bbdc)라는 포스팅에도 해설한 Middleware를 실행하는 타이밍 때문에 일어나는 것입니다. redux-saga의 경우, 항상 Reducer의 처리가 끝난 다음에 Saga가 실행되므로, 지금 상태로는 어떻게 할 수 없기 때문입니다. 수요가 있을지는 모르겠지만 redux-saga에 issue를 세워볼까 생각하고 있습니다. 1397 | 1398 | 1399 | 1400 | ## 결론 1401 | 1402 | 1403 | 1404 | redux-saga를 쓰는 것으로 redux-thunk나 Middleware보다도 구조화된 코드로 비동기처리를 Task라는 실행단위로 기술하는 것이 가능해집니다. 거기에 Mock을 써야하는 테스트를 줄이고, 테스트하고 싶은 로직에 집중 할 수 있습니다. 또한, 재이용가능한 컴포넌트의 개발에서도 필요한 비동기처리를 redux-saga의 Task로서 제공하면, Middleware를 사용하는 경우에 일어나는 실행순서의 문제를 피할 수 있어 안전합니다. 하지만 모든 Middleware를 redux-saga로 바꾸는건 불가능하므로 주의가 필요합니다. 1405 | 1406 | 1407 | -------------------------------------------------------------------------------- /translated/the-right-way-to-test-react-components.md: -------------------------------------------------------------------------------- 1 | > [Stephen Scott](https://twitter.com/suchipi)님이 쓰신 [The Right Way to Test React Components](https://medium.freecodecamp.com/the-right-way-to-test-react-components-548a4736ab22)라는 글의 번역입니다. 본 번역 글은 [원 작자의 허가하](https://medium.com/@suchipi/sure-please-just-do-the-same-things-indicate-that-it-is-translated-from-this-one-with-a-link-51eb96fbbb58)에 작성되어 있습니다. 2 | > 3 | > 번역문이지만 한국어로 읽히기 쉽게 하게 위해 원작자의 **의도가 변하지 않는 선에서** 적극적으로 의역을 하고 있습니다. 반대로 본문에서의 중요하게 다뤄지는 개념들(React의 Component, Prop, State와 같이)은 번역시 오해가 생길 수 있으므로 원문을 그대로 씁니다. 4 | > 5 | > 번역자 : [Junyoung Choi (Rokt33r)](https://github.com/rokt33r) 6 | 7 | # React Component를 테스트하기 위한 올바른 방법 8 | 9 | 10 | 11 | 지금 React Component를 테스트하기 위한 "올바른" 방법에 대해 많은 혼란이 있습니다. 당신은 모든 테스트를 수작업으로 써야할까요? 아니면 스냅샷만 활용해야 할까요? 어쩌면 둘 다? Prop들도 테스트 할 건가요? State는? 스타일이나 레이아웃은 어떻게 할 건가요? 12 | 13 | 14 | 15 | 제 생각에는 하나의 "올바른" 방법은 없는 것 같습니다만, 그래도 효과가 있는 몇가지 팁과 패턴들을 소개드리고 싶습니다. 16 | 17 | 18 | 19 | ## 배경: 테스트해 볼 어플리케이션 20 | 21 | 22 | 23 | 스마트폰의 잠김화면과 같이 동작하는 `LockScreen` Component를 테스트하고 싶다고 가정합시다. 이 앱은 다음과 같은 것을 합니다: 24 | 25 | 26 | 27 | - 현재 시간을 표시한다. 28 | - 유저가 설정한 메세지를 표시할 수 있어야 한다. 29 | - 유저가 설정한 배경 화면을 표시할 수 있어야 한다. 30 | - 밀어서 잠금 해제 위젯을 밑에 둬야한다. 31 | 32 | 36 | 37 | 아마 이런 모양이 될것입니다: 38 | 39 | 40 | 41 | ![Demo](./assets/the-right-way-to-test-react-components/demo.gif) 42 | 43 | [여기](https://suchipi.github.io/react-testing-example-lockscreen)에서 직접 써보실 수 있습니다. 그리고 코드는 [GitHub](https://github.com/suchipi/react-testing-example-lockscreen)에서 찾으실 수 있습니다. 44 | 45 | 46 | 47 | 최상위 레벨의 `App` Component는 다음과 같습니다: 48 | 49 | 50 | 51 | `App.jsx` 52 | 53 | ```js 54 | import React from "react"; 55 | import LockScreen from "./LockScreen"; 56 | 57 | export default class App extends React.Component { 58 | render() { 59 | return ( 60 | alert("열림!")} 64 | /> 65 | ); 66 | } 67 | } 68 | ``` 69 | 70 | 86 | 87 | 보다싶이, `LockScreen`은 세가지 Prop들을 받습니다: `wallpaperPath`, `userInfoMessage`, 그리고 `onUnlocked`. 88 | 89 | 90 | 91 | `LockScreen`의 코드는 다음과 같습니다: 92 | 93 | 94 | 95 | `LockScreen.jsx` 96 | 97 | ```js 98 | import React, { PropTypes } from "react"; 99 | import ClockDisplay from "./ClockDisplay"; 100 | import TopOverlay from "./TopOverlay"; 101 | import SlideToUnlock from "./SlideToUnlock"; 102 | 103 | export default class LockScreen extends React.Component { 104 | static propTypes = { 105 | wallpaperPath: PropTypes.string, 106 | userInfoMessage: PropTypes.string, 107 | onUnlocked: PropTypes.func, 108 | }; 109 | 110 | render() { 111 | const { 112 | wallpaperPath, 113 | userInfoMessage, 114 | onUnlocked, 115 | } = this.props; 116 | 117 | return ( 118 |
130 | 131 | {userInfoMessage ? ( 132 | 138 | {userInfoMessage} 139 | 140 | ) : null} 141 | 142 |
143 | ); 144 | } 145 | } 146 | ``` 147 | 148 | `LockScreen`는 다른 Component들을 불르고 있지만, 우리는 `LockScreen`만 테스트를 할 것이므로, 지금은 여기에만 집중합시다. 149 | 150 | 151 | 152 | ## Component Contracts 153 | 154 | 155 | 156 | `LockScreen`를 테스트 하려면, 우선 그것의 **Contract**가 뭐인지 알아야 합니다. 한 Component의 Contract를 이해하는 것은 React Component의 테스트에서 매우 중요한 부분입니다. Contract는 Component로부터 기대되는 동작과 사용시 어떤한 가정이 이치에 맞는 지를 정의합니다. 분명한 Contract가 없다면, 당신의 Component는 아마 쉽게 이해하기 어려울 것입니다. 테스트를 작성하는 것은 Component의 Contract를 제대로 정의하기 위한 아주 좋은 방법입니다. 157 | 158 | > 역자 주: Contract는 비즈니스에서의 계약에 대한 은유로써 본문에선 Component가 **어떻게 표시**되고 **무엇을 해야하는지** 등을 결정하는 **사양(Specification)** 과 같은 의미로 생각하시면 됩니다. 159 | > 160 | > 161 | 162 | 163 | 164 | 각각의 React Component는 Contract를 정의하는 기능을 적어도 하나는 가지고 있습니다. 165 | 166 | 167 | 168 | - **무엇을 렌더시키는가** (어쩌면 아무것도 안 할 수도 있겟죠) 169 | 170 | 171 | 172 | 그리고, 대부분의 Component Contract는 주로 다음 요소들에 영향을 받습니다: 173 | 174 | 175 | 176 | - **Component가 받을 props** 177 | - **Component가 가지는 state** 178 | - **유저와 상호작용이 일어날 때** Component가 하는 일 (클릭, 드래그, 키보드입력 등) 179 | 180 | 183 | 184 | Component Contract에 덜 영향을 끼치는 것들은 다음과 같습니다: 185 | 186 | 187 | 188 | - **Component가 표시되고있는 context** 189 | - **Component의 인스턴스 메소드**가 불러와졌을 때, 컴포넌트가 하는 것 (public ref interface) 190 | - Component 일부 lifecycle에서 일어나는 **Side effects** (componentDidMount, componentWillUnmount, 등) 191 | 192 | 195 | 196 | 여러분의 Component의 Contract를 알기 위해선 다음의 질문에 대해 스스로 답해보셔야 합니다: 197 | 198 | 199 | 200 | - Component는 무엇을 렌더링하는가? 201 | - Component는 다양한 상황에 따라 다른 것들을 렌더링 하는가? 202 | - Prop으로 넘겨준 함수를 Component는 무얼 위해 쓰는가? 그대로 사용하는가? 아니면 또 다른 Component로 넘겨줄 건가? 만약 그대로 사용한다면, 동시에 무엇을 하는가? 203 | - 유저가 Component와 상호작용을 하면 무슨 일이 일어나는가? 204 | 205 | 209 | 210 | ## LockScreen의 Contract를 알아보자 211 | 212 | 213 | 214 | `LockScreen`의 `render` 메소드를 살펴보고, 어디에서 렌더링이 바뀔 수 있는지 코멘트로 추가해보겠습니다. 3항 연산자, If문, Switch문이 단서가 될 것입니다. 이렇게 함으로써 좀 더 쉽게 Contract로 인한 변화를 파악 할 수 있습니다. 215 | 216 | 217 | 218 | `LockScreen-render.jsx` 219 | 220 | ```js 221 | render() { 222 | const { 223 | wallpaperPath, 224 | userInfoMessage, 225 | onUnlocked, 226 | } = this.props; 227 | 228 | return ( 229 |
243 | 244 | {/* 245 | 만약 userInfoMessage prop이 넘겨지면, TopOverlay를 통해 userInfoMessage를 표시시킨다. 246 | 넘겨지지 않는다면, 여기에 아무것도 렌더링 시키지 않는다.(null) 247 | */} 248 | {userInfoMessage ? ( 249 | 255 | {userInfoMessage} 256 | 257 | ) : null} 258 | 259 |
260 | ); 261 | } 262 | ``` 263 | 264 | 309 | 310 | 이제 `LockScreen`의 contract를 나타내는 3가지의 Contraint에 대해 알게 되었습니다: 311 | 312 | 313 | 314 | - 만약 `wallpaperPath` Prop이 넘겨지면, 가장 바깥의 `div`는 그 값이 무엇이든 url(...)로 감싸여져서 `background-image` CSS property를 inline 스타일로 가지게 됩니다. 315 | - 만약 `userInfoMessage` Prop이 넘겨지면, 몇가지 inline 스타일과 함께 `TopOverlay`의 자식으로 넘겨줍니다. 316 | - 만약 `userInfoMessage` Prop이 넘겨지지 않는다면, `TopOverlay`는 렌더링되지 않습니다. 317 | 318 | > 역자 주: Constraint는 Contract(사양)에 대한 각각의 항목, 바꿔말하면 세세한 규칙을 의미합니다. 의도적으로 Contract와 Constraint를 혼동하지 않도록 두 단어는 영어 그대로 썻습니다. 319 | 320 | 323 | 324 | 그리고 Contract에서 몇가지 Contraint은 항상 그대로라는 것을 알 수 있습니다: 325 | 326 | 327 | 328 | - `div`는 무엇이 들어오든 항상 렌더링됩니다. inline 스타일을 가집니다. 329 | - `ClockDisplay`는 항상 렌더링됩니다. 아무런 Prop도 받지 않습니다. 330 | - `SlideToUnlock`는 항상 렌더링됩니다. `onUnlocked`이 정의되든 말든 `SlideToUnlock`의 `onSlide` Prop으로 넘겨줍니다. 331 | 332 | 335 | 336 | Component의 `propTypes` 역시 Contract를 알기 위한 단서를 찾기 좋은 곳입니다. 여기서 좀 더 많은 Contraint들을 알게 되었습니다: 337 | 338 | 339 | 340 | - `wallpaperPath`는 문자열이며, 선택적으로 주어질 수 있습니다. 341 | - `userInfoMessage`는 문자열이며, 선택적으로 주어질 수 있습니다. 342 | - `onUnlocked`는 함수이며, 선택적으로 주어질 수 있습니다. 343 | 344 | 347 | 348 | 이런식으로 Component의 Contract를 찾을 수 있습니다. 아마 더 많은 Contraint이 있어야 할 것이고, Production 수준의 코드이면 (품질을 보증하기 위해) 가능한 한 더 많이 찾고 싶으실 겁니다. 이것은 예제이므로 일단 이것들만을 가지고 해봅시다. 여러분들은 언제든지 Contraint들을 더 발견하면 테스트를 늘릴 수 있습니다. 349 | 350 | 351 | 352 | ## 무엇이 테스트 할만한 가치가 있나? 353 | 354 | 355 | 356 | - `wallpaperPath`는 문자열이며, 선택적으로 주어질 수 있습니다. 357 | - `userInfoMessage`는 문자열이며, 선택적으로 주어질 수 있습니다. 358 | - `onUnlocked`는 함수이며, 선택적으로 주어질 수 있습니다. 359 | - `div`는 항상 렌더링되며, 모든 것을 담고 있습니다. inline 스타일을 가집니다. 360 | - `ClockDisplay`는 항상 렌더링됩니다. 아무런 Prop도 받지 않습니다. 361 | - `SlideToUnlock`는 항상 렌더링됩니다. `onUnlocked`이 정의되든 말든 `SlideToUnlock`의 `onSlide` Prop으로 넘겨줍니다. 362 | - 만약 `wallpaperPath` Prop이 넘겨지면, 가장 바깥의 `div`는 그 값이 무엇이든 `url(...)`로 감싸여져서 `background-image` CSS property를 inline 스타일로 가지게 됩니다. 363 | - 만약 `userInfoMessage` Prop이 넘겨지면, 몇가지 inline 스타일과 함께 `TopOverlay`의 자식으로 넘겨줍니다. 364 | - 만약 `userInfoMessage` Prop이 넘겨지지 않는다면, `TopOverlay`는 렌더링되지 않습니다. 365 | 366 | 375 | 376 | 일부 Contraint은 테스트할 가치가 있지만, 그렇지 않은 것도 있습니다. 저는 **테스트할 가치가 없는 것들**을 3가지 Rules of thumb을 통해 결정합니다: 377 | 378 | > 역자 주: Rules of Thumb는 경험상으로 얻어진 만든 규칙들을 의미합니다. 여기선 원작자의 경험상 쓸만한 규칙으로, 이하는 이 Rules of Thumb를 통해 테스트할 Constraint를 결정합니다. 의미상 Contract, Constraint 그리고 Rules of Thumb 모두 규칙으로 번역될 우려가 있으므로 이 역시 원문 그대로 씁니다. 379 | 380 | 381 | 382 | 1. 테스트 코드에 **실행 코드가 그대로 중복되어** 쓰여져 있는가? 이것은 테스트가 망가지기 쉽게 만듭니다. 383 | 2. 테스트 코드의 Assertion이 **이미 다른 라이브러리 코드가 보증하는 (그리고 책임지는) 부분**과 중복되는가? _(가장 중요)_ 384 | 3. Component 바깥에서의 시점에서, **이러한 세부항목들이 중요한가? 혹은 그저 내부적인 사정**인가? 이러한 내부적인 사정으로 인한 영향는 Component의 public API를 쓰는 것만으로 설명 될 수 있는가? 385 | 386 | 389 | 390 | 이것들은 그저 Rules of thumb이므로, 어렵다다고 테스트하기 싫다는 변명으로 쓰여지지 않도록 조심하세요. **종종, 테스트하기 어려워 보이는 것들이 테스트에서 가장 중요합니다.** 테스트가 행해지는 코드는 어플리케이션의 다른 나머지 코드까지도 여러 많은 가정들을 잡아 주기 때문에 안정적인 결과물을 만들 수 있습니다. 391 | 392 | 393 | 394 | 395 | 396 | 그럼 Constraint를 살펴보고 무엇을 테스트할지 말지를 Rules of Thumb를 통해 결정해봅시다. 먼저 처음 3개부터 시작합시다: 397 | 398 | 399 | 400 | - `wallpaperPath`는 문자열이며, 선택적으로 주어질 수 있습니다. 401 | - `userInfoMessage`는 문자열이며, 선택적으로 주어질 수 있습니다. 402 | - `onUnlocked`는 함수이며, 선택적으로 주어질 수 있습니다. 403 | 404 | 407 | 408 | 이 Constraint들은 React의 `PropTypes` 메커니즘이 신경쓰는 부분입니다. 그러므로, 이걸 테스트로 만드는건 Rule #2(이미 다른 라이브러리가 보증하고 있음)를 위배하게 됩니다. 고로, **저는 Prop의 타입들**은 테스트 하지 않습니다. 저는, 종종 테스트들은 문서를 겸할 수 있기에, 가끔 Rule #2를 위배하더라도 실행코드가 어떤 타입을 받을지 알기 어렵다면 테스트를 쓰기도 합니다. 하지만, `propTypes`는 충분히 좋고 읽기도 쉬우므로 하지 않습니다. 409 | 410 | 411 | 412 | 다음 Constraint를 봅시다: 413 | 414 | 415 | 416 | - `div`는 항상 렌더링되며, 모든 것을 담고 있습니다. inline 스타일을 가집니다. 417 | 418 | 419 | 420 | 이건 3가지의 Constraint들로 나누어 질 수 있습니다. 421 | 422 | 423 | 424 | - `div`는 항상 렌더링 됩니다. 425 | - `div`는 렌더링되는 다른 모든 것들을 담고 있습니다. 426 | - `div`는 inline 스타일을 가집니다. 427 | 428 | 431 | 432 | 처음 2개의 Constraint들은 Rules of Thumb를 위반하지 않으므로, 우리는 이것들을 **테스트할 것입니다**. 하지만, 3번째를 봅시다. 433 | 434 | 435 | 436 | 다른 constraint가 다루고 있는 `background-image`를 제외하고, `div`는 다음의 스타일들을 가집니다: 437 | 438 | 439 | 440 | ```js 441 | height: "100%", 442 | display: "flex", 443 | justifyContent: "space-between", 444 | flexDirection: "column", 445 | backgroundColor: "black", 446 | backgroundPosition: "center", 447 | backgroundSize: "cover", 448 | ``` 449 | 450 | 만약 이 스타일들이 있는지 테스트를 하려 한다면, 유효한 Assertion를 위해 각 스타일의 값을 _있는 그대로_ 테스트해야 할 것입니다. 고로 Assertion들은 다음과 같이 됩니다: 451 | 452 | 453 | 454 | - `div`는 `height` 스타일은 `100%`의 값을 가진다. 455 | - `div`는 `display` 스타일을 `flex`로 가진다. 456 | - …다른 스타일들도 똑같이 한다. 457 | 458 | 461 | 462 | 간결한 테스트를 위해 [`toMatchObject`](https://facebook.github.io/jest/docs/expect.html#tomatchobjectobject)같은 걸 쓰고 있더라도, 이건 실행 코드의 스타일과 중복되는데다 망가지기 쉽게 됩니다. 만약 다른 스타일을 추가한다해도, 똑같은 코드를 테스트에도 넣어야 할겁니다. 또한, Component의 행동이 바뀌지 않더라도, 스타일을 약간 수정하게되면 테스트에서의 스타일 역시 그대로 수정되어져야 합니다. 그러므로 이 Constraint는 Rule #1를 위반하고 있습니다.(실행 코드와 중복됨; 망가지기 쉬움) 이런 이유로, **저는 런타임에서 변화가 일어나지 않는 한 inline 스타일 테스트를 하지 않습니다**. 463 | 464 | 465 | 466 | 종종 "이것은 이것이 하는 걸 한다"나 "이것은 실행 코드에서 하는 걸 그대로 한다" 같은 코드를 쓰신다면, 이러한 테스트들은 불필요 하거나 너무 명백한 겁니다. 467 | 468 | 469 | 470 | 이제 그 다음 2개의 Constraint를 봅시다: 471 | 472 | 473 | 474 | - `ClockDisplay`는 항상 렌더링됩니다. 아무런 Prop도 받지 않습니다. 475 | - `SlideToUnlock`는 항상 렌더링됩니다. `onUnlocked`이 정의되든 말든 `SlideToUnlock`의 `onSlide` Prop으로 넘겨줍니다. 476 | 477 | 479 | 480 | 이것들은 다음과 같이 나뉠 수 있습니다: 481 | 482 | 483 | 484 | - `ClockDisplay`는 항상 렌더링 됩니다. 485 | - 렌더링된 `ClockDisplay`는 아무런 Prop도 받지 않습니다. 486 | - `SlideToUnlock`는 항상 렌더링 됩니다. 487 | - `onUnlocked` Prop이 정의되었으면, 렌더링된 `SlideToUnlock`은 `onSlide` Prop으로 `onUnlocked`를 받습니다. 488 | - `onUnlocked` Prop이 정의되지 않았으면, 렌더링된 `SlideToUnlock`은 `onSlide` Prop 역시 `undefined`를 받습니다. 489 | 490 | 495 | 496 | 이러한 Constraint들은 2가지 카테고리로 정리됩니다: "어떤 Component가 렌더링된다", 그리고 "렌더링된 Component는 이러한 Prop을 받는다". 이것들은 어떻게 여러분의 Component가 다른 Component들과 상호작용을 하는 지를 설명하기 때문에, **두가지 모두 테스트가 필요할 만큼 중요합니다**. 고로, 우리는 이러한 Constraint들을 모두 테스트 할 겁니다. 497 | 498 | 499 | 500 | 다음 Constraint를 봅시다: 501 | 502 | 503 | 504 | - 만약 `wallpaperPath` Prop이 넘겨지면, 가장 바깥의 `div`는 그 값이 무엇이든 url(...)로 감싸여져서 `background-image` CSS property를 inline 스타일로 가지게 됩니다. 505 | 506 | 507 | 508 | 어쩌면 여러분들은, inline 스타일이기 때문에, 테스트 할 필요가 없다고 생각할지도 모릅니다. 하지만, **`background-image`는 `wallpaperPath` Prop에 따라 바뀌므로 테스트 될 필요가 있습니다**. 만약 테스트하지 않는다면, Component의 Public interface의 일부인 `wallpaperPath` Prop의 영향에대한 테스트는 아무것도 없게 됩니다. Public interface는 항상 테스트 되어져야만 합니다. 509 | 510 | 511 | 512 | 마지막으로 남은 2개의 Constraint들을 봅시다: 513 | 514 | 515 | 516 | - 만약 `userInfoMessage` Prop이 넘겨지면, 몇가지 inline 스타일과 함께 `TopOverlay`의 자식으로 넘겨줍니다. 517 | - 만약 `userInfoMessage` Prop이 넘겨지지 않는다면, `TopOverlay`는 렌더링되지 않습니다. 518 | 519 | 521 | 522 | 이것들도 다음과 같이 나뉠 수 있습니다: 523 | 524 | 525 | 526 | - 만약 `userInfoMessage` Prop이 넘겨지면, `TopOverlay`를 렌더링합니다. 527 | - 만약 `userInfoMessage` Prop이 넘겨지면, 그것은 `TopOverlay`의 children으로 넘겨져야 합니다. 528 | - 만약 `userInfoMessage` Prop이 넘겨지면, 렌더링된 `TopOverlay`는 inline 스타일을 가집니다. 529 | - 만약 `userInfoMessage` Prop이 넘겨지지 않는다면, `TopOverlay`는 렌더링되지 않습니다. 530 | 531 | 535 | 536 | 첫번째과 4번째 Constraint는(`TopOverlay`은 렌더링 된다/되지 않는다) **무엇을 렌더링할지를 말하고 있으므로, 테스트합니다**. 537 | 538 | 539 | 540 | 두번째 Constraint는 `TopOverlay`가 `userInfoMessage`의 값을 Prop으로 받고 있는지를 확인합니다. **이 역시 렌더링된 Component가 받을 Prop에 대한 것이므로 테스트할 만큼 중요합니다. 고로, 이것도 테스트합니다**. 541 | 542 | 543 | 544 | 3번째 Constraint도 `TopOverlay`가 받는 특정 Prop을 확인하기 때문에, 어쩌면 테스트해야 한다고 생각하셨을 수도 있습니다. 하지만, 이 Prop은 그저 inline 스타일입니다. Prop이 넘겨졌는지 확인하는 것은 중요하지만, inline 스타일에 대한 테스트를 만드는건 망가지기 쉽고 실행코드와 중복됩니다. (Rule #1에 위배됩니다) 넘겨진 Prop들을 확인하는 건 중요하기 때문에, Rule #1만 생각하면 테스트를 해야할지 말지가 애매해질 수 있습니다; 다행스럽게도 제가 Rule #3를 말한 이유가 여기에 있습니다. 다시 상기하면: 545 | 546 | 547 | 548 | > Component 바깥에서의 시점에서, **이러한 세부항목들이 중요한가? 혹은 그저 내부적인 사정**인가? 이러한 내부적인 사정으로 인한 영향는 Component의 public API를 쓰는 것만으로 설명 될 수 있는가? 549 | 550 | 552 | 553 | 테스트를 쓸 때, 저는 가능한 **Component의 public API만을 테스트**합니다. (어플리케이션 API가 가지는 Side effects를 포함해서) **이 Component의 레이아웃은 public API로부터 아무런 영향을 받지 않습니다; 이것은 CSS엔진이 신경 쓸 일입니다.** 이로 인해 Rule #3까지 위반하게 됩니다. 결국 Rule #1와 Rule #3에 위배되므로, TopOverlay가 Prop을 받는지를 확인하는 것이 일반적으로 중요하지만, **테스트 하지 않습니다**. 554 | 555 | 556 | 557 | 마지막 Constraint는 테스트할지 말지 결정하기 어려웠습니다. 결국 여러분이 어떤 부분이 테스트할만큼 중요한지에 달려있습니다; 이러한 Rules of Thumb는 그저 가이드라인일 뿐입니다. 558 | 559 | 560 | 561 | 이제 우린 모든 Constraint들을 확인해보았고, 무엇을 테스트할건지 알게 되었습니다. 여기서 다시 한번 리스트를 보여드리겠습니다: 562 | 563 | 564 | 565 | - `div`는 항상 렌더링 됩니다. 566 | - `div`는 렌더링되는 다른 모든 것들을 담고 있습니다. 567 | - `ClockDisplay`는 항상 렌더링 됩니다. 568 | - 렌더링된 `ClockDisplay`는 아무런 Prop도 받지 않습니다. 569 | - `SlideToUnlock`는 항상 렌더링 됩니다. 570 | - `onUnlocked` Prop이 정의되었으면, 렌더링된 `SlideToUnlock`은 `onSlide` Prop으로 `onUnlocked`를 받습니다. 571 | - `onUnlocked` Prop이 정의되지 않았으면, 렌더링된 `SlideToUnlock`은 `onSlide` Prop 역시 `undefined`를 받습니다. 572 | - 만약 `wallpaperPath` Prop이 넘겨지면, 가장 바깥의 `div`는 그 값이 무엇이든 url(...)로 감싸여져서 `background-image` CSS property를 inline 스타일로 가지게 됩니다. 573 | - 만약 `userInfoMessage` Prop이 넘겨지면, `TopOverlay`를 렌더링합니다. 574 | - 만약 `userInfoMessage` Prop이 넘겨지면, 그것은 `TopOverlay`의 children으로 넘겨져야 합니다. 575 | - 만약 `userInfoMessage` Prop이 넘겨지지 않는다면, `TopOverlay`는 렌더링되지 않습니다. 576 | 577 | 588 | 589 | Constraint들을 세세하게 살펴봄으로써, 많은 수의 Constraint를 여러 작은 Constraint들로 나누었습니다. **이건 정말 좋아요!** 이제 테스트 코드를 쓰기가 더욱 쉬워질 겁니다. 590 | 591 | 592 | 593 | ## 테스트 보일러플레이트 준비하기 594 | 595 | 596 | 597 | Component테스트를 위한 준비를 해봅시다. 저는 [enzyme](http://airbnb.io/enzyme/)와 함께 [Jest](https://facebook.github.io/jest/)를 쓸겁니다. Jest는 [React와 매우 잘 맞고](https://facebook.github.io/jest/docs/tutorial-react.html) [create-react-app](https://github.com/facebookincubator/create-react-app)으로 만들어진 어플리케이션에도 포함된 테스트러너입니다. 그래서 어쩌면 여러분들은 이미 사용할 준비가 되어있을 수도 있습니다. Enzyme는 브라우저와 Node환경에서 동작하는 믿을 만한 React 테스트 라이브러리입니다. 598 | 599 | 600 | 601 | 비록 저는 Jest와 enzyme를 사용하지만, 여기서 쓰인 개념들은 어떤 테스트 환경에서도 적용시키실 수 있을 겁니다. 602 | 603 | 604 | 605 | `LockScreen.test.jsx` 606 | 607 | ```js 608 | import React from "react"; 609 | import { mount } from "enzyme"; 610 | import LockScreen from "./LockScreen"; 611 | 612 | describe("LockScreen", () => { 613 | let props; 614 | let mountedLockScreen; 615 | const lockScreen = () => { 616 | if (!mountedLockScreen) { 617 | mountedLockScreen = mount( 618 | 619 | ); 620 | } 621 | return mountedLockScreen; 622 | } 623 | 624 | beforeEach(() => { 625 | props = { 626 | wallpaperPath: undefined, 627 | userInfoMessage: undefined, 628 | onUnlocked: undefined, 629 | }; 630 | mountedLockScreen = undefined; 631 | }); 632 | 633 | // 모든 테스트들은 여기에 쓰여질 것입니다. 634 | }); 635 | ``` 636 | 637 | 666 | 667 | 조금 큰 보일러플레이트가 되었네요. 뭘 하고 있는지 설명해드릴게요: 668 | 669 | 670 | 671 | - `let`으로 `props`와 `mountedLockScreen`를 설정합니다. 이로써 `describe`함수 안이면 어디에서든 부를 수 있습니다. 672 | - **`lockScreen` 함수** 역시 `describe` 안 어디에서든 부를 수 있습니다. 이것은 `mountedLockScreen` 변수에 LockScreen에 Prop와 함꼐 Mount해주고, 이미 Mount된 것을 돌려줍니다. 이 함수는 enzyme ReactWrapper를 리턴합니다. 이건 매 테스트마다 사용할 겁니다. 673 | - `beforeEach`는 `props`와 `mountedLockScreen`을 매 테스트마다 초기화시킵니다. 하지 않을 경우, 한 테스트의 상태가 다른 테스트에 영향을 줄 수 있습니다. 여기에 `mountedLockScreen`를 `undefined`로 설정함으로써, 다음 테스트가 실행될 때, `lockScreen`을 부르면 매번 새로운 `LockScreen`이 `props`와 함께 Mount될겁니다. 674 | 675 | 678 | 679 | 이 보일러플레이트는 Component 하나를 테스트하기엔 많아보일지도 모릅니다만, Component를 Mount하기 전에 추가적으로 Props를 준비해 줄 수 있습니다. 이는 테스트가 Dry하도록 도와줍니다. 저는 이것을 모든 Component 테스트에 사용하고, 여러분들에게도 유용하길 빕니다; 유용성은 이제 테스트 케이스들을 써볼 수록 더욱 확실하게 알게 되실 겁니다. 680 | 681 | 682 | 683 | > 역자 주: Dry한 테스트는 다른 외부 요인들이 통제된 테스트를 의미합니다. 684 | > 685 | > Prop들이 초기화되지 않는다면 앞서한 테스트에 따라 결과가 바뀔 수 있습니다. 하지만 초기화를 시켜준다면 앞서 어떤 테스트가 행해지든 지금 테스트에는 아무런 영향이 없게 되므로 신뢰성 있는 테스트를 하는게 가능해집니다. 이를 Dry 테스트라고 합니다. 686 | 687 | ## 테스트를 써보자! 688 | 689 | 690 | 691 | Constraint목록을 보며 하나씩 테스트를 추가해 봅시다. 각각의 테스트는 보일러플레이트의 `// All tests will go here` 코멘트 뒤로 추가되는 형식으로 쓰여져나갈 것입니다. 692 | 693 | 694 | 695 | - `div`는 항상 렌더링 됩니다. 696 | 697 | 698 | 699 | `LockScreen.test.jsx` 700 | 701 | ```js 702 | it("always renders a div", () => { 703 | const divs = lockScreen().find("div"); 704 | expect(divs.length).toBeGreaterThan(0); 705 | }); 706 | ``` 707 | 708 | - `div`는 렌더링되는 다른 모든 것들을 담고 있습니다. 709 | 710 | 711 | 712 | ```js 713 | describe("the rendered div", () => { 714 | it("contains everything else that gets rendered", () => { 715 | const divs = lockScreen().find("div"); 716 | // .find를 사용하실 때, enzyme는 노드들을 차례로 나열해 줍니다. 717 | // 여기서 바깥쪽부터 읽어오기 때문에 .first()를 사용하면 718 | // 가장 바깥쪽의 div를 불러올 수 있습니다. 719 | const wrappingDiv = divs.first(); 720 | 721 | // Enzyme는 .children()를 쓰면 가장 바깥쪽의 Node를 생략해줍니다. 722 | // 조금 어려울지 모르겠지만, 이것으로 wrappingDiv가 다른 모든 723 | // Component를 가지고 있는지 확인 할 수 있습니다. 724 | expect(wrappingDiv.children()).toEqual(lockScreen().children()); 725 | }); 726 | }); 727 | ``` 728 | 729 | 746 | 747 | - `ClockDisplay`는 항상 렌더링 됩니다. 748 | 749 | 750 | 751 | ```js 752 | it("always renders a `ClockDisplay`", () => { 753 | expect(lockScreen().find(ClockDisplay).length).toBe(1); 754 | }); 755 | ``` 756 | 757 | - 렌더링된 `ClockDisplay`는 아무런 Prop도 받지 않습니다. 758 | 759 | 760 | 761 | ```js 762 | describe("rendered `ClockDisplay`", () => { 763 | it("does not receive any props", () => { 764 | const clockDisplay = lockScreen().find(ClockDisplay); 765 | expect(Object.keys(clockDisplay.props()).length).toBe(0); 766 | }); 767 | }); 768 | ``` 769 | 770 | - `SlideToUnlock`는 항상 렌더링 됩니다. 771 | 772 | 773 | 774 | ```js 775 | it("always renders a `SlideToUnlock`", () => { 776 | expect(lockScreen().find(SlideToUnlock).length).toBe(1); 777 | }); 778 | ``` 779 | 780 | 여기까지 모든 Constraint들은 항상 참이 되는 것들이므로 비교적 테스트를 만들기가 쉬웠습니다. 그렇지만, 나머지 Constraint들은 "만약 ... 한다면" 같은 조건이나 때와 함께합니다. 이러한 것들은 조건적으로 참이 되므로 `beforeEach`와 함께 2개의 `describe`를 준비할 겁니다. 781 | 782 | 783 | 784 | - `onUnlocked` Prop이 정의되었으면, 렌더링된 `SlideToUnlock`은 `onSlide` Prop으로 `onUnlocked`를 받습니다. 785 | - `onUnlocked` Prop이 정의되지 않았으면, 렌더링된 `SlideToUnlock`은 `onSlide` Prop 역시 `undefined`를 받습니다. 786 | 787 | 789 | 790 | ```js 791 | describe("when `onUnlocked` is defined", () => { 792 | beforeEach(() => { 793 | props.onUnlocked = jest.fn(); 794 | }); 795 | 796 | it("sets the rendered `SlideToUnlock`'s `onSlide` prop to the same value as `onUnlocked`'", () => { 797 | const slideToUnlock = lockScreen().find(SlideToUnlock); 798 | expect(slideToUnlock.props().onSlide).toBe(props.onUnlocked); 799 | }); 800 | }); 801 | 802 | describe("when `onUnlocked` is undefined", () => { 803 | beforeEach(() => { 804 | props.onUnlocked = undefined; 805 | }); 806 | 807 | it("sets the rendered `SlideToUnlock`'s `onSlide` prop to undefined'", () => { 808 | const slideToUnlock = lockScreen().find(SlideToUnlock); 809 | expect(slideToUnlock.props().onSlide).not.toBeDefined(); 810 | }); 811 | }); 812 | ``` 813 | 814 | 어떤 조건에서만 일어나는 동작을 묘사할 때, 그 조건에 대한 설명을 describe에 넣어줍니다. 그리고, beforeEach를 통해 그 조건을 설정해줍니다. 815 | 816 | 817 | 818 | > 역자 주: `describe`에는 테스트하려는 조건에 대한 얘기가, `it`은 그 조건에서 어떤 결과가 얻어질지에 대한 설명이 들어가있습니다. 테스트 코드는 영어 문법으로 자연스럽게 읽히기에 일부러 번역하지 않았지만, 한국어로 쓰면 다음과 같이 됩니다. 819 | > 820 | > ```js 821 | > describe("`onUnlocked` Prop이 정의되었으면", () => { 822 | > beforeEach(() => { 823 | > props.onUnlocked = jest.fn(); 824 | > }); 825 | > 826 | > it("렌더링된 `SlideToUnlock`은 `onSlide` Prop으로 `onUnlocked`를 받습니다.", () => { 827 | > const slideToUnlock = lockScreen().find(SlideToUnlock); 828 | > expect(slideToUnlock.props().onSlide).toBe(props.onUnlocked); 829 | > }); 830 | > }); 831 | > 832 | > describe("`onUnlocked` Prop이 정의되지 않았으면", () => { 833 | > beforeEach(() => { 834 | > props.onUnlocked = undefined; 835 | > }); 836 | > 837 | > it("렌더링된 `SlideToUnlock`은 `onSlide` Prop 역시 `undefined`를 받습니다.", () => { 838 | > const slideToUnlock = lockScreen().find(SlideToUnlock); 839 | > expect(slideToUnlock.props().onSlide).not.toBeDefined(); 840 | > }); 841 | > }); 842 | > ``` 843 | 844 | - 만약 `wallpaperPath` Prop이 넘겨지면, 가장 바깥의 `div`는 그 값이 무엇이든 `url(...)`로 감싸여져서 `background-image` CSS property를 inline 스타일로 가지게 됩니다. 845 | 846 | 847 | 848 | ```js 849 | describe("when `wallpaperPath` is passed", () => { 850 | beforeEach(() => { 851 | props.wallpaperPath = "some/image.png"; 852 | }); 853 | 854 | it("applies that wallpaper as a background-image on the wrapping div", () => { 855 | const wrappingDiv = lockScreen().find("div").first(); 856 | expect(wrappingDiv.props().style.backgroundImage).toBe(`url(${props.wallpaperPath})`); 857 | }); 858 | }); 859 | ``` 860 | 861 | - 만약 `userInfoMessage` Prop이 넘겨지면, `TopOverlay`를 렌더링합니다. 862 | - 만약 `userInfoMessage` Prop이 넘겨지면, 그것은 `TopOverlay`의 children으로 넘겨져야 합니다. 863 | 864 | 866 | 867 | ```js 868 | describe("when `userInfoMessage` is passed", () => { 869 | beforeEach(() => { 870 | props.userInfoMessage = "This is my favorite phone!"; 871 | }); 872 | 873 | it("renders a `TopOverlay`", () => { 874 | expect(lockScreen().find(TopOverlay).length).toBe(1); 875 | }); 876 | 877 | it("passes `userInfoMessage` to the rendered `TopOverlay` as `children`", () => { 878 | const topOverlay = lockScreen().find(TopOverlay); 879 | expect(topOverlay.props().children).toBe(props.userInfoMessage); 880 | }); 881 | }); 882 | ``` 883 | 884 | - 만약 `userInfoMessage` Prop이 넘겨지지 않는다면, `TopOverlay`는 렌더링되지 않습니다. 885 | 886 | 887 | 888 | ```js 889 | describe("when `userInfoMessage` is undefined", () => { 890 | beforeEach(() => { 891 | props.userInfoMessage = undefined; 892 | }); 893 | 894 | it("does not render a `TopOverlay`", () => { 895 | expect(lockScreen().find(TopOverlay).length).toBe(0); 896 | }); 897 | }); 898 | ``` 899 | 900 | 이걸로 모든 Constraint의 테스트가 완성되었습니다! 최종 테스트 파일은 [여기](https://gist.github.com/suchipi/8f8d7de60e8e4ae48153db0c36133e63)에서 확인하실 수 있습니다. 901 | 902 | 903 | 904 | ## "이건 내 일이 아닌데" 905 | 906 | 907 | 908 | 글 처음의 Gif을 보여드렸을 때, 여러분은 테스트들이 다음과 같이 완성될거라 예상하셨던 분들이 있으셨을 겁니다: 909 | 910 | 911 | 912 | - 유저가 밀어서 잠금해제를 드래그 오른쪽 끝까지 하면, unlock 콜백이 불러진다. 913 | - 드래그하는 도중에 핸들을 놓아버리면, 핸들은 원래 위치로 돌아가는 애니메이션을 보여준다. 914 | - 스크린 최상단의 시계는 언제나 현재 시간을 보여준다. 915 | 916 | 919 | 920 | 이러한 생각들은 자연스러운 겁니다. 어플리케이션의 전체적인 시점에서 보면 매우 당연한 것들입니다. 921 | 922 | 923 | 924 | 하지만 우리는 이에 대해 아무런 테스트도 쓰지 않았습니다. 왜냐고요? 이건 **`LockScreen`이 신경쓸 것**이 아니니까요. 925 | 926 | 927 | 928 | React Coponent가 재사용 가능한건, Unit 테스트들과 자연스럽게 어울릴 수 있기 때문입니다. 우리가 그리고 Unit 테스트를 할때, **해당 Unit이 신경써야 할 것만 테스트 하셔야 됩니다**. React Component 테스트를 할 땐 숲을 보는 것보다 각각의 나무를 신경쓰시는게 더 좋습니다. 929 | 930 | 931 | 932 | **대부분의 React Component들이 신경써야 할 것들**을 설명하는 컨닝 페이퍼를 드리겠습니다: 933 | 934 | 935 | 936 | - 받은 Prop으로 뭘 할건가? 937 | - 어떤 Component들을 렌더링해야하나? 렌더링된 Component들에겐 무엇을 넘겨주어야 하는가? 938 | - State를 가질 필요가 있나? 혹시 그렇다면, 새 Prop을 받았을 때 초기화 시켜야 하는가? 언제 State를 업데이트 해야하나? 939 | - 만약 해당 Component나 Child Component로 넘겨준 Callback이 유저와 상호작용으로 불러와진다면 Component는 무엇을 해야 하는가? 940 | - Mount되었을 때 무엇이 일어나는가? Unmount되었을 때는? 941 | 942 | 947 | 948 | 고로, 앞서 말한 기능들은 `SlideToUnlock`와 `ClockDisplay`가 신경써야할 것이므로 여기가 아니라 각각의 Component의 테스트로 만들어야 할겁니다. 949 | 950 | 951 | 952 | ## 결론 953 | 954 | 955 | 956 | 이런 방법들이 여러분이 React Component 테스트를 작성하는데 도움이 되길 빕니다. 요약하자면: 957 | 958 | 959 | 960 | - **우선 Component의 Contract를 파악해라** 961 | - 어떤 Constraint가 테스트할 가치가 있는지 정해라. 962 | - Prop 타입들은 테스트할 필요가 없다. 963 | - Inline 스타일들은 일반적으로 테스트할 필요가 없다. 964 | - 렌더링하려는 Component들과 그것들에게 넘겨주는 Prop들은 테스트해야 할만큼 중요하다. 965 | - 해당 Component가 신경쓸게 아닌 것은 테스트 하지 말라. 966 | 967 | 973 | 974 | 만약 다른 생각이 있으시거나, 이 포스트가 도움이 되셧다면, [Twitter](https://twitter.com/suchipi)로 얘기해주세요. React Component를 어떻게 테스트하는지 모두 함께 배울 수 있을거에요. 975 | 976 | 977 | 978 | _Stephen Scott님은 통합 스마트홈 자동화 시스템을 만드는 [Nexia](http://www.nexiahome.com/)의 개발자입니다. Nexia는 고용중입니다! Colorado주 Broomfield에 있는 사무실에있는 저희 개발자 팀에 함께하고 싶으시다면 [Twitter](https://twitter.com/suchipi)로 알려주세요._ 979 | 980 | 981 | 982 | * * * 983 | 984 | _이 글의 저작권은 보호받고 있지만, 이 포스팅의 모든 샘플 코드들은 MIT 라이센스하에 공개되어 있습니다. [GitHub](https://github.com/suchipi/react-testing-example-lockscreen)에서 찾으실 수 있습니다._ 985 | 986 | 987 | --------------------------------------------------------------------------------