Skip to content

Latest commit

ย 

History

History
1406 lines (997 loc) ยท 108 KB

deal-with-async-process-by-redux-saga.md

File metadata and controls

1406 lines (997 loc) ยท 108 KB

kuy๋‹˜์ด ์“ฐ์‹  redux-sagaใง้žๅŒๆœŸๅ‡ฆ็†ใจๆˆฆใ†๋ผ๋Š” ๊ธ€์˜ ๋ฒˆ์—ญ์ž…๋‹ˆ๋‹ค. ๋ณธ ๋ฒˆ์—ญ ๊ธ€์€ ์› ์ž‘์ž์˜ ํ—ˆ๊ฐ€ํ•˜์— ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, ์ผ๋ณธ์–ด์˜ ํŠน์„ฑ์ƒ ํ•œ๊ตญ์–ด์—์„œ๋Š” ์ž˜ ์•ˆ์“ฐ๋Š” ํ•œ์ž์–ด๊ฐ€ ๋งŽ๊ณ  ์ง์—ญํ•  ๊ฒฝ์šฐ, ๊ฐ€๋” ๋‰˜์•™์Šค๊ฐ€ ์• ๋งคํ•ด์ง€๊ฑฐ๋‚˜ ํ‘œํ˜„์ด ์žฅํ™ฉํ•ด์ง€๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๊ธฐ์—, ์ด๋Ÿฌํ•œ ๋ถ€๋ถ„์€ ์˜๋„๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š๋Š” ์„ ์—์„œ ์˜์—ญ์„ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฒˆ์—ญ์ž : Junyoung Choi (Rokt33r)

redux-saga๋กœ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ์™€ ๋ถ„ํˆฌํ•˜๋‹ค.

์‹œ์ž‘ํ•˜๋Š” ๋ง

Redux๋Š” ๋‹จ์ผ Store, ๋ถˆ๋ณ€์ ์ธ State, Side effect๊ฐ€ ์—†๋Š” Reducer์˜ 3๊ฐ€์ง€ ์›์น™์„ ๋‚ด์„ธ์šด Flux ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ์™€๋Š” ๋‹ฌ๋ฆฌ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์€ ์ตœ์†Œํ•œ์œผ๋กœ, Fullstack์ด๋ผ๊ณ ๋Š” ๋งํ•˜๊ธฐ ํž˜๋“ค ๋งŒํผ ์–‡์Šต๋‹ˆ๋‹ค. ๊ทธ ๋•Œ๋ฌธ์— ๋ชจ๋“  ๋ฉด์— ์žˆ์–ด์„œ ์ผ๋ฐ˜์ ์ด๋ผ๊ณ  ํ• ๋งŒํ•œ ์‚ฌ์šฉ๋ฒ•์ด ์ •๋ฆฌ๋˜์ง€ ์•Š์•„์„œ, ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•ด์•ผํ• ์ง€ ๋ฐฉํ™ฉํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์ ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ ํ•„๋‘๋กœ ๋งํ•  ์ˆ˜ ์žˆ๋Š”๊ฒŒ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ์ž…๋‹ˆ๋‹ค. ์ปค๋ฎค๋‹ˆํ‹ฐ๋Š” ์ง€๊ธˆ๋„ ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์„ ๋ชจ์ƒ‰ํ•˜๊ณ  ์žˆ๊ณ , ์ฃผ๋กœ ์“ฐ์—ฌ์ง€๋Š”๊ฑด redux-thunk์™€ redux-promise์ •๋„ ์ผ๊ฒ๋‹ˆ๋‹ค. Redux๋กœ ํ•œ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด react-side-effect๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฑด Twitter์˜ ๋ชจ๋ฐ”์ผ ๋ฒ„์ ผ์— ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋–ค ๊ฑธ ์จ๋„ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋˜์ง€๋งŒ, ๊ทธ๊ฒƒ๋“ค์€ ์–ด๋””๊นŒ์ง€๋‚˜ ๋„๊ตฌ๋กœ์จ, ์„ค๊ณ„์˜ ์ง€์นจ๊นŒ์ง€๋Š” ์•Œ๋ ค์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฌธ์ œ๋Š” ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผ ์–ด๋””์— ์“ธ ๊ฒƒ ์ธ๊ฐ€, ์–ด๋–ป๊ฒŒ ์“ธ ๊ฒƒ ์ธ๊ฐ€, ๊ทธ๋ฆฌ๊ณ  ์–ด๋””์„œ ๋ถˆ๋Ÿฌ์™€์•ผ ํ•˜๋Š”๊ฐ€ ์ž…๋‹ˆ๋‹ค. Redux๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์žˆ์œผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์ด ๊ณ ๋ฏผํ•˜๊ณ  ์žˆ์ง€๋Š” ์•Š์œผ์‹ ๊ฐ€์š”?

  • ํŠน์ • Action์„ ๊ธฐ๋‹ค๋ฆฌ๋‹ค ๋‹ค๋ฅธ Action์„ dispatchํ•œ๋‹ค.
  • ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ณ , ๋‹ค๋ฅธ ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค.
  • ์ดˆ๊ธฐํ™”์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด๋“ค์ด๊ณ  ์‹ถ๋‹ค.
  • ๋นˆ๋ฒˆํžˆ ๋ฐœ์ƒํ•˜๋Š” Action์„ ๋ชจ์•„์„œ dispatchํ•˜๊ณ  ์‹ถ๋‹ค.
  • ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ์ž˜ ์–ด์šธ๋ฆฌ๊ฒŒ ํ•˜๊ณ  ์‹ถ๋‹ค.

React + Redux์˜ ์•„๋ฆ„๋‹ค์šด ์„ธ๊ณ„์—์„œ ์›€์ธ ๋Ÿฌ๋“œ๋Š” ๊ทธ ์ฝ”๋“œ๋“ค์„ ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•  ๊ฒƒ์ธ๊ฐ€. ์–ด๋–ป๊ฒŒ ์‹ธ์›Œ์•ผ ํ•  ๊ฒƒ์ธ๊ฐ€. ์ด ๊ธ€์—๋Š” ํ•œ๊ฐ€์ง€์˜ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์œผ๋กœ์จ redux-saga๋ฅผ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค. redux-saga์˜ ๊ฐœ์š”์™€ ๊ธฐ๋ณธ์ ์ธ ๊ฐœ๋…์—๋Œ€ํ•ด ๊ฐ€๋ณ๊ฒŒ ์„ค๋ช…ํ•˜๊ณ , ์ต์ˆ™ํ•œ redux-thunk๋กœ ๋งŒ๋“ค์–ด์ง„ ์ฝ”๋“œ์™€ ๋น„๊ตํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์กฐ๊ธˆ๋งŒ ์ดˆ๋ณด์ ์ธ ์…‹์—…๋ฐฉ๋ฒ•๊ณผ ์‹ค์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ๋ถ€๋ถ„์„ ์„ค๋ช…ํ•˜๊ณ , ํ›„๋ฐ˜์€ ์‹ค์ „์ ์ธ redux-saga์˜ ์‚ฌ์šฉ๋ฒ•์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ ๋กœ ๊ณต์‹ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ ํ•œ๊ตญ์–ด๋กœ๋œ README๊ฐ€ ์ œ๊ณต๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋‹จ ์จ๋ณด๊ณ ์‹ถ๋‹ค! ๋ผ๋Š” ๋ถ„๋“ค์€ ๋จผ์ € ์ด์ชฝ ๋งํฌ๋„ ํ›‘์–ด๋ด์ฃผ์„ธ์š”.

redux-saga๋ž€

redux-saga๋Š” Redux๋กœ Sideeffect๋ฅผ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•œ Middleware์ž…๋‹ˆ๋‹ค. ... ์•„๋งˆ ์ด๋Œ€๋กœ๋ผ๋ฉด ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ต๊ฒ ์ง€์š”. ์ด ๊ตฌ๋ฌธ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์งง์€ ์„ค๋ช…๋ฌธ์ด์ง€๋งŒ, ์‹ค์€ ๊ทธ๋‹ค์ง€ ๋ณธ์งˆ์„ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ์ด์œ ๋กœ ์ œ ๋‚˜๋ฆ„์˜ ์ดํ•ด์™€ ๋‹จ์–ด๋กœ ์„ค๋ช…ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

redux-saga ๋ž€ (๋‹ค์‹œ ์„ค๋ช…)

redux-saga๋Š” "Task"๋ผ๋Š” ๊ฐœ๋…์„ Redux๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ์œ„ํ•œ ์ง€์› ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋งํ•˜๋Š” Task๋ž€ ์ผ์˜ ์ ˆ์ฐจ์™€ ๊ฐ™์€ ๋…๋ฆฝ์ ์ธ ์‹คํ–‰ ๋‹จ์œ„๋กœ์จ, ๊ฐ๊ฐ ํ‰ํ–‰์ ์œผ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. redux-saga๋Š” ์ด Task์˜ ์‹คํ–‰ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋”๋ถˆ์–ด ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผ Task๋กœ์จ ๊ธฐ์ˆ ํ•˜๊ธฐ ์œ„ํ•œ ์ค€๋น„๋ฌผ์ธ "Effect"์™€ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Effect๋ž€ Task๋ฅผ ๊ธฐ์ˆ ํ•˜๊ธฐ ์œ„ํ•œ ์ปค๋งจ๋“œ(๋ช…๋ น, Primitive)์™€ ๊ฐ™์€ ๊ฒƒ์œผ๋กœ, ์˜ˆ๋ฅผ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒƒ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • select: State๋กœ๋ถ€ํ„ฐ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊บผ๋‚ธ๋‹ค.
  • put: Action์„ dispatchํ•œ๋‹ค.
  • take: Action์„ ๊ธฐ๋‹ค๋ฆฐ๋‹ค. ์ด๋ฒคํŠธ์˜ ๋ฐœ์ƒ์„ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
  • call: Promise์˜ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
  • fork: ๋‹ค๋ฅธ Task๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค.
  • join: ๋‹ค๋ฅธ Task์˜ ์ข…๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
  • ...

์ด๋Ÿฌํ•œ ์ฒ˜๋ฆฌ์ค‘์—๋Š” Task์•ˆ์— ์ง์ ‘ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋„ ์žˆ์ง€๋งŒ, redux-saga์—๊ฒŒ ๋ถ€ํƒํ•˜์—ฌ ๊ฐ„์ ‘์ ์œผ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ์จ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผ co์ฒ˜๋Ÿผ ๋™๊ธฐ์ ์œผ๋กœ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ณ , ๋ณต์ˆ˜์˜ Task๋ฅผ ๋™์‹œํ‰ํ–‰์ ์œผ๋กœ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ทธ๋ฆผ์€ redux-saga์—์„œ ์‹คํ–‰๋˜๋Š” Task์˜ ์ด๋ฏธ์ง€์ž…๋‹ˆ๋‹ค.

redux-saga.png

๋ฌด์—‡์ด ์ข‹์•„์ง€๋‚˜?

Flux๋‚˜ Redux๋งŒ์œผ๋กœ๋„ ๋‚œํ•ดํ–ˆ๋˜๊ฒƒ์ด ๋”์šฑ ์ƒˆ๋กœ์šด ๊ฐœ๋…์„ ๊ฐ€์ ธ์™€์„œ ํ˜ผ๋ž€์Šค๋Ÿฝ๊ฒŒ ํ•˜๊ณ ์žˆ๋„ค์š”. ๊ทธ๋ž˜๋„ Redux-saga๋ฅผ ์“ธ ๊ฐ€์น˜๋Š” ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

  • Mock ์ฝ”๋“œ๋ฅผ ๋งŽ์ด ์“ฐ์ง€ ์•Š์•„๋„ ๋œ๋‹ค.
  • ์ž‘์€ ์ฝ”๋“œ๋กœ ๋” ๋ถ„ํ• ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์žฌ์ด์šฉ์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.

์ด๊ฒƒ์€ ๋‹จ์ˆœํ•œ "์žฌ์ด์šฉ๊ฐ€๋Šฅ"์ด๋ผ๋Š” ๋‹จ์–ด ์ด์ƒ์˜ ์˜๋ฏธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฌด์Šจ ๋ง์ด๋ƒ๋ฉด ์žฌ์ด์šฉ๊ฐ€๋Šฅํ•œ Container Component๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š”๋ฐ ์žˆ์–ด ํ•„์ˆ˜์ ์ธ ์š”์†Œ์ž…๋‹ˆ๋‹ค. Middleware๋ผ๋Š” ์ •๋ง ์ดํ•ดํ•˜๊ธฐ ํž˜๋“ค์ง€๋งŒ ์‹ ๊ฒฝ์“ธ ์ˆ˜ ๋ฐ–์— ์—†๋Š” ๊ฒƒ์ด ์‚ฐ๋”๋ฏธ์ฒ˜๋Ÿผ ์žˆ๊ณ , ๋”์šฑ์ด ์žฌ์ด์šฉ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ์จ ๋„์ž…ํ•  ๋•Œ๋„, ์–ด๋””์— Middleware๋ฅผ ๋„ฃ์–ด์ค„ ๊ฒƒ์ธ๊ฐ€๋ฅผ ์ƒ๊ฐํ•˜์ง€์•Š์œผ๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด, saga๋ผ๋ฉด ์›์น™์ ์œผ๋กœ ์„œ๋กœ ๋…๋ฆฝ์ ์„ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ž์‹ ์˜ ์„ธ๊ณ„์—์„œ๋งŒ ์ฝ”๋“œ๋ฅผ ์“ฐ๋Š”๊ฒŒ ๊ฐ€๋Šฅํ•˜์—ฌ, ๋‹ค๋ฅธ saga์— ์˜ํ–ฅ์„ ๋ผ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ถ”์ƒ์ ์ธ ์„ค๋ช…์€ ๊ทธ๋‹ค์ง€ ์ดํ•ด๋„๊ฐ€ ๋†’์•„์ง€์ง€ ์•Š๊ธฐ์—, redux-thnk๋กœ ์“ด ์ฝ”๋“œ์™€ ๋น„๊ตํ•˜๋ฉฐ redux-saga๋กœ ์ธํ•ด ์–ด๋–ป๊ฒŒ ๋ฐ”๋€Œ๋Š”์ง€๋ฅผ ํ•œ๋ฒˆ ๋ด…์‹œ๋‹ค.

redux-thunk โ†’ redux-saga

์ƒ˜ํ”Œ๋กœ์จ FetchAPI๋ฅผ ์‚ฌ์šฉํ•œ ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด๋ด…์‹œ๋‹ค.

๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด๋“ค์ด๋Š”๊ฑด ๊ฐ„๋‹จํ•˜์ง€๋งŒ, Redux๋กœ ์ œ๋Œ€๋กœ ์ƒ๊ฐํ•ด์•ผํ• ๊ฒŒ ์ ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ ๋“ค์ž…๋‹ˆ๋‹ค.

  • ์–ด๋””์„œ ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ์ ์„ ๊ฒƒ์ธ๊ฐ€
  • ์–ด๋””๋กœ๋ถ€ํ„ฐ ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ๊ฒƒ์ธ๊ฐ€
  • ํ†ต์‹ ์ฒ˜๋ฆฌ์˜ ์ƒํƒœ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ฐ€์ง€๊ฒŒ ํ•  ๊ฒƒ์ธ๊ฐ€.

3๋ฒˆ์งธ ์š”์†Œ๋Š” redux-thunk๋‚˜ redux-saga ๋‘˜๋‹ค ๊ณตํ†ต์ ์ธ ๋ถ€๋ถ„์ด๋ฏ€๋กœ ๋จผ์ € ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋˜๊ธฐ๊นŒ์ง€ "์ฝ์–ด๋“ค์ด๋Š” ์ค‘..."์ด๋ผ๋Š” ๋ฉ”์„ธ์ง€๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”, ํ†ต์‹ ์ƒํƒœ๋ฅผ Store๋กœ ๊ฐ€์ง€๊ฒŒ ํ•œ ๋‹ค์Œ์—, ํ†ต์‹ ์˜ ๊ฐœ์‹œ/์„ฑ๊ณต/์‹คํŒจ์˜ 3 ํƒ€์ด๋ฐ์— Action์„ dispatchํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๋ฐ”๊ฟ€ ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ ์šฉํŒจํ„ด์ธ Redux์˜ ์ƒ˜ํ”Œ ์ฝ”๋“œ๊ฐ€ ์•„๋งˆ ์›๋ณธ์œผ๋กœ, ์ด๊ฒƒ๋งŒ ๋ฝ‘์•„๋‚ธ redux-api-middleware๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์ง€๋งŒ, ์ด๋ฒˆ์—” Middleware๋‚˜ redux-api-middleware๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์“ฐ์—ฌ์žˆ์Šต๋‹ˆ๋‹ค. ํ†ต์‹ ์ƒํƒœ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ํ†ต์‹ ์ด ์ •์ƒ์ ์œผ๋กœ ์ข…๋ฃŒ๋˜์—ˆ๋Š”๊ฐ€, ์—๋Ÿฌ๋กœ ์ธํ•ด ์ข…๋ฃŒ๋˜์—ˆ๋Š”๊ฐ€๊นŒ์ง€ ๋งž์ถ”์–ด ๋„ฃ์–ด๋‘๋ฉด ์—๋Ÿฌ๋ฉ”์„ธ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋Š”๋ฐ ์“ธ ์ˆ˜ ์žˆ์–ด์„œ ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ƒ˜ํ”Œ ์ฝ”๋“œ๋Š” 3๊ฐ€์ง€์˜ Action ํƒ€์ž… REQUEST_USER, SUCCESS_USER, FAILURE_USER์˜ ๋ฌธ์ž์—ด ์ƒ์ˆ˜์™€ Action ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ์œ„ํ•œ 3๊ฐ€์ง€์˜ Action Creator requestUser, successUser, failureUser๋Š” actions.js์— ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ redux-thunk ์ฝ”๋“œ๋ฅผ ๋ด…์‹œ๋‹ค.

redux-thunk

api.js

export function user(id) {
  return fetch(`http://localhost:3000/users/${id}`)
    .then(res => res.json())
    .then(payload => ({ payload }))
    .catch(error => ({ error }));
}

actions.js

export function fetchUser(id) {
  return dispatch => {
    dispatch(requestUser(id));
    API.user(id).then(res => {
      const { payload, error } = res;
      if (payload && !error) {
        dispatch(successUser(payload));
      } else {
        dispatch(failureUser(error));
      }
    });
  };
}

๋จผ์ € ์ฒ˜๋ฆฌ ์ „์ฒด์˜ ํ๋ฆ„์„ ํ™•์ธํ•ด๋ด…์‹œ๋‹ค.

  1. (๋ˆ„๊ตฐ๊ฐ€๊ฐ€ fetchUser ํ•จ์ˆ˜์˜ ๋ฆฌํ„ด ๊ฐ’์„ dispatchํ•œ๋‹ค)
  2. redux-thunk์˜ Middleware๊ฐ€ dispatch๋œ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  3. ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐœ์‹œํ•˜๊ธฐ ์ „์— REQUEST_USER Action์„ dispatchํ•œ๋‹ค.
  4. API.user ํ•จ์ˆ˜๋ฅผ ๋ถˆ๋Ÿฌ๋‚ด์„œ ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐœ์‹œํ•œ๋‹ค.
  5. ์™„๋ฃŒ๋˜๋ฉด SUCCESS_USER ํ˜น์€ FAILURE_USER Action์„ dispatchํ•œ๋‹ค.

api.js์˜ userํ•จ์ˆ˜๋Š” ์œ ์ €์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜์ด๋‹ค. Fetch API๋Š” Promise๋ฅผ ๋Œ๋ ค์ฃผ๊ธฐ์— ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌํ•ธ๋“ค๋ง์˜ ๋ฐฉ๋ฒ•์€ ์›ํ•˜๋Š” ๋Œ€๋กœ ํ•˜์…”๋„ ๋˜์ง€๋งŒ, ์—ฌ๊ธฐ์„  try/catch๋ฅผ ์“ฐ์ง€ ์•Š๊ณ , ๋ฆฌํ„ด ๊ฐ’์œผ๋กœ ํŒ์ •ํ•˜๋Š” ์Šคํƒ€์ผ์„ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

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ํ•˜๋Š” ๋ฐ๋‹ค๊ฐ€, ์•ž๋’ค๋กœ ์ด๋Ÿฐ์ €๋Ÿฐ ๋กœ์ง์ด ๋“ค์–ด๊ฐ€๋Š”๊ฑด ์œ„ํ—˜ํ•œ ๋ƒ„์ƒˆ๊ฐ€ ๋‚ฉ๋‹ˆ๋‹ค. ๋„์ž…๋„ ์‚ฌ์šฉ๋ฒ•๋„ ๊ฐ„๋‹จํ•œ ๋ฐ˜๋ฉด, ์‚ฌํƒœํŒŒ์•…๋„ ์•ˆ๋œ์ฑ„๋กœ ํŽธํ•˜๋‹ˆ๊นŒ ๋ง‰์“ฐ๋‹ค๊ฐ„ ๋‚˜์ค‘์— ์ง€์˜ฅ์„ ๋งž์ดํ• ์ง€๋„ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ์†Œ๊ทน์ ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๋ฌธ์ œ๊ฐ€ ์—†๊ฒ ์ง€๋งŒ, ๋ณต์žกํ•œ ํ†ต์‹ ์ฒ˜๋ฆฌ๋Š” ์ •๋ง๋กœ ์“ฐ๊ณ  ์‹ถ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์“ฐ๊ณ  ์‹ถ๋‹ค๋Š” ์ƒ๊ฐ๋„ ์•ˆ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ redux-saga๋กœ ๋ฐ”๊ฟ”์“ฐ๋ฉด ์–ด๋–ป๊ฒŒ ๋˜๋Š”์ง€ ๋ด…์‹œ๋‹ค.

redux-saga

sagas.js

function* handleRequestUser() {
  while (true) {
    const action = yield take(REQUEST_USER);
    const { payload, error } = yield call(API.user, action.payload);
    if (payload && !error) {
      yield put(successUser(payload));
    } else {
      yield put(failureUser(error));
    }
  }
}

export default function* rootSaga() {
  yield fork(handleRequestUser);
}

๋ฐ€๋„๊ฐ€ ๋†’์€ ์ฝ”๋“œ์ด๋ฏ€๋กœ ๊ธฐํ•ฉ์„ ๋„ฃ๊ณ  ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋จผ์ € ์ „์ฒด์ ์ธ ํ๋ฆ„๋ถ€ํ„ฐ..

  1. redux-saga์˜ Middleware๊ฐ€ rootSaga Task๋ฅผ ์‹œ์ž‘์‹œํ‚จ๋‹ค.
  2. fork Effect๋กœ ์ธํ•ด handleRequestUser Task๊ฐ€ ์‹œ์ž‘๋œ๋‹ค.
  3. take Effect๋กœ REQUEST_USER Action์ด dispatch๋˜๊ธธ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
  4. (๋ˆ„๊ตฐ๊ฐ€๊ฐ€ REQUEST_USER Action์„ dispatchํ•œ๋‹ค.)
  5. call Effect๋กœ API.user ํ•จ์ˆ˜๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ํ†ต์‹ ์ฒ˜๋ฆฌ์˜ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
  6. (ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋œ๋‹ค.)
  7. put Effect๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SUCCESS_USER ํ˜น์€ FAILURE_USER Action์„ dispatchํ•œ๋‹ค.
  8. while ๋ฃจํ”„์— ๋”ฐ๋ผ 3๋ฒˆ์œผ๋กœ ๋Œ์•„๊ฐ„๋‹ค.

redux-thunk๋กœ ์ธํ•œ ์ฝ”๋“œ์™€์˜ ๋น„๊ต๋ฅผ ์œ„ํ•ด ์ผ๋ จ์˜ ํ๋ฆ„์„ ์จ๋ณด์•˜์ง€๋งŒ, ์‹ค์€ ์ด ์ฒ˜๋ฆฌ๋กœ๋Š” ๋™์‹œ๋ณ‘ํ–‰ํ•˜์—ฌ ์›€์ง์ด๋Š” 2๊ฐœ์˜ ํ๋ฆ„์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์ด Task์ž…๋‹ˆ๋‹ค. sagas.js๋กœ ์ •์˜๋œ 2๊ฐ€์ง€ ํ•จ์ˆ˜ ๋ชจ๋‘ redux-saga์˜ Task์ž…๋‹ˆ๋‹ค. ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ด…์‹œ๋‹ค.

rootSaga Task๋Š” Redux์˜ Store๊ฐ€ ์ž‘์„ฑ ๋œ ํ›„, redux-saga์˜ Middleware๊ฐ€ ๊ธฐ๋™ ๋  ๋•Œ 1๋ฒˆ๋งŒ ๋ถˆ๋Ÿฌ์™€์ง‘๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  fork Effect์„ ์‚ฌ์šฉํ•˜์—ฌ redux-saga์—๊ฒŒ ๋‹ค๋ฅธ Task๋ฅผ ๊ธฐ๋™ํ•  ๊ฒƒ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์•ž์„œ ์„ค๋ช…ํ•œ๋“ฏ์ด, Task๋‚ด์—๋Š” ์‹ค์ œ ์ฒ˜๋ฆฌ๋ฅผ ํ–‰ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, forkํ•จ์ˆ˜๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑ๋œ ๊ฒƒ์€ ๋‹จ์ˆœํ•œ ์˜ค๋ธŒ์ ํŠธ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ Flux ์•„ํ‚คํ…์ณ์˜ Action ์˜ค๋ธŒ์ ํŠธ์™€ ๊ฐ€๊นŒ์šด ๋Š๋‚Œ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ ๋‹ค์Œ๊ณผ๊ฐ™์ด ์˜ค๋ธŒ์ ํŠธ์˜ ๋‚ด์šฉ์„ ๋ณด๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

console.log(fork(handleRequestUser));

์ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋Š๋‚Œ์˜ ์˜ค๋ธŒ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

{
  Symbol<IO>: true,
  FORK: {
    context: ...,
    fn: <func>,
    args: [...]
  }
}

์ผ๋‹จ, Effect ์˜ค๋ธŒ์ ํŠธ๋Š” ์ƒ์„ฑ๋ ๋ฟ ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€์•Š์œผ๋ฏ€๋กœ, redux-saga๋กœ ๋„˜๊ฒจ์ ธ์„œ ์‹คํ–‰๋  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์‹คํ–‰์„ ์œ„ํ•ด์„œ๋Š” Generator ํ•จ์ˆ˜์˜ yield๋ฅผ ์‚ฌ์šฉํ•ด ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ฝ”๋“œ ๊ฐ’์„ ๋„˜๊ฒจ์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. "๋ถˆ๋Ÿฌ์˜ค๋Š” ์ชฝ์˜ ์ฝ”๋“œ"๋Š” ๋ˆ„๊ตฌ์˜ ๊ฒƒ์ธ๊ฐ€? ๊ทธ๊ฒƒ์€ redux-saga๊ฐ€ ์ œ๊ณตํ•˜๋Š” Task์˜ ์‹คํ–‰ํ™˜๊ฒฝ์ธ Middleware์ž…๋‹ˆ๋‹ค. Effect ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋ฐ›์•„๋“ค์ธ redux-saga์˜ Middleware๋Š” ๋„˜๊ฒจ์ง„ ํ•จ์ˆ˜๋ฅผ ์ƒˆ๋กœ์šด Task๋กœ์จ ๊ธฐ๋™์‹œํ‚ต๋‹ˆ๋‹ค. ๊ทธ ์ดํ›„ redux-saga๋Š” 2๊ฐœ์˜ Task๊ฐ€ ๋™์‹œ์— ์›€์ง์ด๊ณ ์žˆ๋Š” ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ƒˆ๋กญ๊ฒŒ ๊ธฐ๋™๋œ handleRequestUser Task์˜ ์„ค๋ช…์œผ๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ์ „์— rootSaga Task์˜ "๊ทธ ์ดํ›„"๋ฅผ ๋”ฐ๋ผ๊ฐ‘๋‹ˆ๋‹ค.

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๋ฅผ ๊ธฐ๋™ํ•ด์ฃผ์„ธ์š”.

handleRequestUser Task๋ฅผ ๊ธฐ๋™์‹œํ‚ค๋ฉด ๊ธˆ๋ฐฉ REQUEST_USER Action์„ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ์œ„ํ•ด take Effect๊ฐ€ ๋ถˆ๋Ÿฌ์™€์ง‘๋‹ˆ๋‹ค. ์ด "๊ธฐ๋‹ค๋ฆผ"์ด๋ผ๋Š” ํ–‰๋™์ด ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ์“ด๋‹ค ๋ผ๋Š” ํŠน์ง•์ ์ธ Task์˜ ํ‘œํ˜„์œผ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค. redux-saga์˜ Task๋ฅผ Generatorํ•จ์ˆ˜๋กœ ์“ฐ๋Š” ์ด์œ ๋Š” yield ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ์˜ ํ๋ฆ„์„ ์ผ์‹œ์ •์ง€ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ฒด๊ณ„ ๋•๋ถ„์— ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ์˜ Javascript๋กœ ๋ณต์ˆ˜์˜ Task๋ฅผ ๋งŒ๋“ค์–ด, ๊ฐ๊ฐ ํŠน์ •ํ•œ Action์„ ๊ธฐ๋‹ค๋ฆฌ๊ฑฐ๋‚˜, ํ†ต์‹ ์ฒ˜๋ฆฌ์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ ค๋„ ์ฒ˜๋ฆฌ๊ฐ€ ๋ฐ€๋ฆฌ์ง€ ์•Š๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

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๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์œผ๋กœ ํ†ต์‹ ์ฒ˜๋ฆฌ ์ž์ฒด๋Š” ์™„๋ฃŒ๋˜์—ˆ์ง€๋งŒ, ํ•œ๊ฐ€์ง€ ๋” Task๋ฅผ ์ •์˜ํ•  ๋•Œ ์ž์ฃผ ์“ฐ๋Š” ์šฉ์–ด์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ด๋‘๊ฒ ์Šต๋‹ˆ๋‹ค. ์ตœ์ดˆ๋กœ ์ฝ”๋“œ๋ฅผ ๋ดค์„๋•Œ "์–ด?" ๋ผ๊ณ  ๋Š๋ผ์…จ์„ ๊ฑฐ๋ผ ์ƒ๊ฐํ•˜์ง€๋งŒ, handleRequestUser Task๋Š” ์ „์ฒด๊ฐ€ while๋ฌธ์œผ๋กœ๋œ ๋ฌดํ•œ ๋ฃจํ”„๋กœ ๊ฐ์‹ธ์—ฌ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ฒฐ๊ณผ put Effect๋กœ Effect๋กœ Action์„ dispatchํ•œ ํ›„, ๋ฃจํ”„์˜ ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„๊ฐ€์„œ ๋‹ค์‹œ ํ•œ๋ฒˆ take Effect๋กœ REQUEST_USER Action์„ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, Action์„ ๊ธฐ๋‹ค๋ ค ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ๋ฟ์ธ Task๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ๊ฐ€ ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๊ทน๋‹จ์ ์œผ๋กœ ํ•ด์•ผ ํ•  ์ผ์„ ์ œํ•œํ•ด๋‘๋Š” ๊ฒƒ์œผ๋กœ ์ฝ”๋“œ๋Š” ๋งค์šฐ ๋‹จ์ˆœํ•ด์ง‘๋‹ˆ๋‹ค. ๋‹น์—ฐํžˆ ๋ฒ„๊ทธ๋„ ์ค„๊ฒ ์ฃ . ๊ฒŒ๋‹ค๊ฐ€ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ์— ํ•ญ์ƒ ๋”ฐ๋ผ์˜ค๋Š” ์ฝœ๋ฐฑ ์ง€์˜ฅ, ๊นŠ์€ ๊ตฌ์กฐ, ๋œฌ๊ธˆ์—†์ด ๋‚˜ํƒ€๋‚˜๋Š” Promise๊ฐ€ ์‚ฌ๋ผ์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์–ด๋–ป๊ฒŒ ๋ฐ”๋€Œ์—ˆ๋‚˜?

redux-thunk์™€ redux-saga์˜ ๊ฐ๊ฐ์˜ ์ฝ”๋“œ์— ๋Œ€ํ•ด์„œ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์‚ดํŽด ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ž ๊น ๋‹ค๋ฅธ ๊ด€์ ์œผ๋กœ๋ถ€ํ„ฐ ์ƒ๊ฐํ•ด๋ณด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ์ด ์„น์…˜์˜ ๋จธ๋ฆฟ๊ธ€์— ๋งํ•œ "์–ด๋””์— ์“ธ ๊ฒƒ ์ธ๊ฐ€", "์–ด๋””์—์„œ ๋ถˆ๋Ÿฌ์˜ฌ ๊ฒƒ์ธ๊ฐ€"์— ๋Œ€ํ•œ ์–˜๊ธฐ์ž…๋‹ˆ๋‹ค.

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๋กœ ํ‘œ์‹œ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ, ์•ž์„œ ํ†ต์‹ ์ฒ˜๋ฆฌ๋Š” ๋งค์šฐ ๋‹จ์ˆœํ•œ ๊ฒƒ์ด์—ˆ์œผ๋ฏ€๋กœ, ์ฒ˜์Œ๋ณด๋Š” ๋ฐฉ์‹์œผ๋กœ ์“ฐ๊ธฐ๊ฐ€ ๊ฐ•์ œ๋œ๊ฒŒ ๋ฒ„๊ฑฐ์›Œ ๊ทธ๋‹ค์ง€ ๊ฐ์‚ฌํ•˜๋‹ค๋ผ๋Š” ์ธ์ƒ์ด ์˜…์—ˆ์„ ์ง€๋„ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ์ด์œ ๋กœ, ํ˜„์‹ค์˜ ํ”„๋กœ์ ํŠธ์—์„œ๋„ ํ•„์š”ํ•  ๋ฒ•ํ•œ ๊ธฐ๋Šฅ์ถ”๊ฐ€๋ฅผ ํ•ด๋ด…์‹œ๋‹ค. ๋ณต์žกํ• ์ˆ˜๋ก ์ง„๊ฐ€๋ฅผ ๋ฐœํœ˜ํ•˜๋Š”๊ฒŒ redux-saga์ž…๋‹ˆ๋‹ค.

์ฒ˜๋ฆฌ๋ฅผ ๋ณต์žกํ•˜๊ฒŒ ํ•ด๋ณด์ž

๊ทธ๋‹ค์ง€ ์–ด๋ ค์šด ์ฒ˜๋ฆฌ๋ผ๋ฉด ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ค์›Œ์ง€๊ธฐ ๋•Œ๋ฌธ์—, ์ด์ „ Redux์˜ middleware๋ฅผ ์ ๊ทน์ ์œผ๋กœ ์จ๋ณด๊ธฐ๋ผ๋Š” ํฌ์ŠคํŒ…์— ์‘์šฉ ์˜ˆ๋กœ ๋งŒ๋“  API ์š”์ฒญ์„ ์ฒด์ธ์‹œํ‚ค๊ธฐ๋ฅผ redux-thunk์™€ redux-saga๋กœ ๊ฐ๊ฐ ์จ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ํฌ์ŠคํŒ…์˜ ์˜ˆ์ œ์€ ์–ด๋–ค ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚œ ์ดํ›„, ๊ทธ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ๋‹ค์‹œ ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐœ์‹œํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋ฒˆ ์˜ˆ์ œ์—์„œ๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์ทจ๋“ํ•œ ์ดํ›„, ์œ ์ €์ •๋ณด์— ํฌํ•จ๋œ ์ง€์—ญ๋ช…์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ™์€ ์ง€์—ญ์— ์‚ด๊ณ  ์žˆ๋Š” ๋‹ค๋ฅธ ์œ ์ €๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์ œ์•ˆํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ด…๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด api.js์— ์ถ”๊ฐ€๋œ searchByLocation ํ•จ์ˆ˜๋Š” redux-thunk์™€ redux-saga๋กœ ๋งŒ๋“  ์˜ˆ์ œ ๋ชจ๋‘ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Action Type์ด๋‚˜ Action Creator๋“ฑ์€ ์ ๋‹นํžˆ ์ •์˜ํ•ด๋’€๋‹ค๊ณ  ์ƒ๊ฐํ•ด์ฃผ์„ธ์š”.

์—ญ์ž์ฃผ: ์˜ˆ์ œ์˜ ์›๋ณธ์ด ๋˜๋Š” ๋งํฌ๋Š” ์ผ๋ณธ์–ด์ด์ง€๋งŒ ์ด๋ฏธ ํ•„์š”ํ•œ ๋‚ด์šฉ๋“ค์€ ๋ณธ ํฌ์ŠคํŒ…์— ๋‹ค ์žˆ๊ธฐ์— ๋ชจ๋ฅด์…”๋„ ํฌ๊ฒŒ ๋ฌธ์ œ ์—†์Šต๋‹ˆ๋‹ค.

redux-thunk

api.js

export function searchByLocation(name) {
  return fetch(`http://localhost:3000/users/${id}/following`)
    .then(res => res.json())
    .then(payload => { payload })
    .catch(error => { error });
}

actions.js

export function fetchUser(id) {
  return dispatch => {
    // ์œ ์ € ์ •๋ณด๋ฅผ ์ฝ์–ด๋“ค์ธ๋‹ค
    dispatch(requestUser(id));
    API.user(id).then(res => {
      const { payload, error } = res;
      if (payload && !error) {
        dispatch(successUser(payload));

        // ์ฒด์ธ: ์ง€์—ญ๋ช…์œผ๋กœ ์œ ์ €๋ฅผ ๊ฒ€์ƒ‰
        dispatch(requestSearchByLocation(id));
        API.searchByLocation(id).then(res => {
          const { payload, error } = res;
          if (payload && !error) {
            dispatch(successSearchByLocation(payload));
          } else {
            dispatch(failureSearchByLocation(error));
          }
        });
      } else {
        dispatch(failureUser(error));
      }
    });
  };
}

...ํ . ๋ญ˜ ํ•˜๊ณ ํ”ˆ์ง€๋Š” ์••๋‹ˆ๋‹ค. ์•„๋งˆ ๋ณดํ†ต ์ด๋ ‡๊ฒŒ ๋˜๊ฒ ์ฃ . ํ•˜์ง€๋งŒ ...ํ•œ(๋ญ”๊ฐ€ ์ฐ์ฐํ•œ) ๋Š๋‚Œ์ด ๋“œ๋„ค์š”. ๊ทธ๋ฆฌ๊ณ  ์—ฌ๊ธฐ์— ๋˜ ๋‹ค๋ฅธ ์ฒด์ธ์„ ๋Š˜๋ฆฌ๊ฑฐ๋‚˜ ์ฒด์ธ์‹œํ‚ค๋Š” ์œ„์น˜๋ฅผ ๋ฐ”๊พธ๊ฒŒ ๋œ๋‹ค๋ฉด ๊ณค๋ž€ํ•ด ์งˆ๋“ฏ ํ•˜๋„ค์š”. ๋ฌด์—‡๋ณด๋‹ค๋„ ๊ธฐ๋ถ„ ๋‚˜์œ๊ฑด fetchUser๋ผ๋Š” Action Creator๋ฅผ ๋ถˆ๋Ÿฌ๋‚ด์„œ ์™œ ์œ ์ € ๊ฒ€์ƒ‰๊นŒ์ง€ ์‹คํ–‰ํ•˜๊ณ  ์žˆ๋Š๋ƒ ๋ผ๋Š” ์ ์ž…๋‹ˆ๋‹ค. Middleware๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌ๋ฅผ ๋ถ„๋ฆฌํ•˜๋ฉด ์ด ๋ฌธ์ œ์ ์ด ์กฐ๊ธˆ์€ ํ•ด์†Œ๋  ๋“ฏ ํ•˜์ง€๋งŒ, ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋…๋ฆฝ์ ์ธ DSL๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ๊ณ„์† ๋Š˜์–ด๋‚˜๊ฒŒ ๋˜๋Š” ๊ฒƒ ์—ญ์‹œ ๊ดด๋กœ์šธ ๋“ฏ ํ•ฉ๋‹ˆ๋‹ค.

์—ญ์‹œ, ์ด ํฌ์ŠคํŒ…์€ redux-saga๋ฅผ ํŽธ์• ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ redux-thunk๋ฅผ ์ฒ ์ €ํ•˜๊ฒŒ ๊นŒ๋‚ด๋ฆด ์˜๋„๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ์ €๋„ ์•„์ง ์‚ฌ์šฉํ•˜๋Š” ๋ถ€๋ถ„์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด์ฉ” ์ˆ˜ ์—†์ด redux-thunk๋ฅผ ๊ณ„์† ์‚ฌ์šฉํ•  ์ˆ˜ ๋ฐ–์— ์—†์œผ์‹  ๋ถ„๋„ ์žˆ์„๊ฑฐ๋ผ ์ƒ๊ฐ๋˜๋ฏ€๋กœ, ํ˜น์‹œ "redux-thunk๋ผ๋„ ์ด๋ ‡๊ฒŒ ์“ฐ๋ฉด ๊ดด๋กœ์›€์„ ์ค„์ผ์ˆ˜ ์žˆ์–ด!" ๊ฐ™์€ ์˜๊ฒฌ์ด ์žˆ์œผ์‹œ๋ฉด ์ฝ”๋ฉ˜ํŠธ๋กœ ์•Œ๋ ค์ฃผ์„ธ์š”.

๊ทธ๋Ÿผ redux-saga๋กœ ์จ๋ด…์‹œ๋‹ค.

redux-saga

sagas.js

// ์ถ”๊ฐ€
function* handleRequestSearchByLocation() {
  while (true) {
    const action = yield take(SUCCESS_USER);
    const { payload, error } = yield call(API.searchByLocation, action.payload.location);
    if (payload && !error) {
      yield put(successSearchByLocation(payload));
    } else {
      yield put(failureSearchByLocation(error));
    }
  }
}

// ๋ณ€๊ฒฝ์—†์Œ!
function* handleRequestUser() {
  while (true) {
    const action = yield take(REQUEST_USER);
    const { payload, error } = yield call(API.user, action.payload);
    if (payload && !error) {
      yield put(successUser(payload));
    } else {
      yield put(failureUser(error));
    }
  }
}

export default function* rootSaga() {
  yield fork(handleRequestUser);
  yield fork(handleRequestSearchByLocation); // ์ถ”๊ฐ€
}

๋ณด์‹œ๋‹ค์‹ถ์ด handleRequestUser Task๋Š” ๋ณ€๊ฒฝ์ ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€๋œ handleRequestSearchByLocation Task๋Š” handleRequestUser Task์™€ ๊ฑฐ์˜ ๋™์ผํ•œ ์ฒ˜๋ฆฌ์ž…๋‹ˆ๋‹ค. rootSaga Task๋Š” handleRequestSearchByLocation Task๋ฅผ ๊ธฐ๋™ํ•˜๊ธฐ์œ„ํ•ด fork ๋ฅผ ํ•˜๋‚˜ ๋” ์ถ”๊ฐ€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์กฐ๊ธˆ ๊ธธ์–ด์ง€์ง€๋งŒ, ์ฒ˜๋ฆฌ์˜ ํ๋ฆ„์„ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. redux-saga์˜ Middleware๊ฐ€ rootSaga Task๋ฅผ ๊ธฐ๋™์‹œํ‚จ๋‹ค.
  2. fork Effect์— ๋”ฐ๋ผ handleRequestUser ์™€ handleRequestSearchByLocation Task๊ฐ€ ๊ธฐ๋™๋œ๋‹ค.
  3. ๊ฐ๊ฐ์˜ Task์— ๋Œ€ํ•ด take Effect๋กœ๋ถ€ํ„ฐ REQUEST_USER ์™€ SUCCESS_USER Action์ด dispatch๋˜๋Š” ๊ฒƒ์„ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
  4. (๋ˆ„๊ตฐ๊ฐ€๊ฐ€ REQUEST_USER Action์„ dispatchํ•œ๋‹ค.)
  5. call Effect์œผ๋กœ API.user ํ•จ์ˆ˜๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ, ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚˜๊ธธ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
  6. (ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋œ๋‹ค.)
  7. put Effect๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SUCCESS_USER Action์„ dispatchํ•œ๋‹ค.
  8. handleRequestSearchByLocation ํƒœ์Šคํฌ๊ฐ€ ๋‹ค์‹œ ์‹œ์ž‘๋˜์–ด, call Effect๋กœ API.searchByLocation ํ•จ์ˆ˜๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ, ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚˜๊ธธ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
  9. (ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋œ๋‹ค.)
  10. put Effect๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SUCCESS_SEARCH_BY_LOCATION Action์„ dispatchํ•œ๋‹ค.
  11. ๊ฐ๊ฐ์˜ Task์—์„œ while ๋ฃจํ”„๊ฐ€ ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„๊ฐ€ take ๋กœ Action์˜ dispatch๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.

๊ฐ๊ฐ์˜ ํƒœ์Šคํฌ๋ฅผ ์ฃผ์˜ํ•ด์„œ ๋ณด๋ฉด ๋‹จ์ˆœํ•œ ๊ฒƒ ๋ฐ–์— ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์›Œ์ง€์ง€ ์•Š์•˜๋‚˜์š”? ๊ฒŒ๋‹ค๊ฐ€ ์ด ์ฝ”๋“œ๋ฅผ ํ™•์žฅํ•˜์—ฌ ์ฒด์ธ์„ ๋Š˜๋ฆฌ๊ฑฐ๋‚˜, ์ฒด์ธ์„ ์ˆœ์„œ๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋ฌด์—‡์„ ํ•˜๋”๋ผ๋„ Task๋Š” ํ•˜๋‚˜๋งŒ ์ง‘์ค‘ํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ๋‹ค๋ฅธ ๋ฌด์—‡์„ ํ•ด๋„ ๊ทธ๋‹ค์ง€ ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์„ฑ์งˆ์„ ์ ๊ทน์ ์œผ๋กœ ์ด์šฉํ•˜์—ฌ Task๊ฐ€ ๋„ˆ๋ฌด ๋น„๋Œ€ํ•ด์ง€๊ธฐ ์ „์— ๊ณ„์†ํ•ด์„œ ์ž˜๋ผ ๋‚˜๋ˆ„๋Š” ๊ฒƒ์œผ๋กœ ์ฝ”๋“œ์˜ ๊ฑด์ „์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ๋ฅผ ์จ๋ณด์ž

redux-saga๋ฅผ ์ ๊ทน์ ์œผ๋กœ ์“ฐ๊ณ ์‹ถ์€ ์ด์œ ๋กœ์„œ ํ…Œ์ŠคํŠธ์˜ ํŽธ๋ฆฌํ•จ์„ ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์•„์ง ๋‹ค๋ฅธ์‚ฌ๋žŒ์—๊ฒŒ ๋ฒˆ์—ญํ• ๋งŒํผ ๋…ธํ•˜์šฐ์˜ ์ถ•์ ์ด ์•ˆ๋˜์—ˆ์ง€๋งŒ, ๋ถ„์œ„๊ธฐ๋ฅผ ํŒŒ์•…ํ•˜๋Š” ์ •๋„๋กœ ์จ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ๋Œ€์ƒ์€ ์ตœ์ดˆ์˜ ํ†ต์‹ ์ฒ˜๋ฆฌ์˜ ์ฝ”๋“œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ๋ณต์žกํ•˜๊ฒŒ ๋˜๊ธฐ ์ „์˜ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค. ๋จผ์ € ๋‹จ์ˆœํ•ด๋ณด์ด๋Š” rootSaga Task์˜ ํ…Œ์ŠคํŠธ๋ถ€ํ„ฐ ์จ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋Š” mocha + power-assert ์ž…๋‹ˆ๋‹ค.

sagas.js

export default function* rootSaga() {
  yield fork(handleRequestUser);
}

์—ฌ๊ธฐ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

test.js

describe('rootSaga', () => {
  it('launches handleRequestUser task', () => {
    const saga = rootSaga();

    ret = saga.next();
    assert.deepEqual(ret.value, fork(handleRequestUser));

    ret = saga.next();
    assert(ret.done);
  });
});

Task๋ฅผ ํฌํฌํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ํ•œ๋‹ค. ๋ผ๊ณ  ํ•˜๋ฉด ์–ด๋ ต๊ฒŒ ๋“ค๋ฆฌ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ Task๋ผ๋Š” ๊ฑด ๋‹จ์ˆœํ•œ Generator ํ•จ์ˆ˜๋กœ, Task๊ฐ€ ๋Œ๋ ค์ฃผ๋Š”๊ฑด ๋ชจ๋‘ ๋‹จ์ˆœํ•œ ์˜ค๋ธŒ์ ํŠธ๋ผ๋Š”๊ฑธ ๋– ์˜ฌ๋ฆฝ์‹œ๋‹ค. ๊ณ ๋กœ redux-saga์— ๋‘์–ด์ง„ Task์˜ ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์ˆœํžˆ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋น„๊ตํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋Œ€๋ถ€๋ถ„ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ์ด rootSaga Task๊ฐ€ ํฌํฌํ•˜๊ณ  ์žˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด fork Effect๋กœ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋น„๊ตํ•˜๋Š” ๊ฒƒ์œผ๋กœ OK์ž…๋‹ˆ๋‹ค. ์ด expected๋กœ ์ง€์ •๋œ ์˜ค๋ธŒ์ ํŠธ๋„ Task์˜ ์ž‘์„ฑ์— ์“ฐ์—ฌ์ง„ Effect Creator๋กœ ์ƒ์„ฑํ•˜์—ฌ ๋ฌธ์ œ์—†๋Š”๊ฒƒ์ด ์žฌ๋ฐ‹๋Š” ํฌ์ธํŠธ์ž…๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•  ๊ฒƒ์€ ์ด Task๊ฐ€ ๋ฌด์—‡์„ ํ•˜๊ณ  ์žˆ๋Š” ๊ฐ€๋กœ์จ, ๊ทธ์— ์•ž์„œ ๋ฌด์—‡์„ ํ•˜๋Š”๊ฐ€๋Š” ์•Œ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ๋งŒ์ด๋ฉด ํ…Œ์ŠคํŠธํ•œ ๋Š๋‚Œ์ด ์•ˆ๋‚˜๋ฏ€๋กœ ์ข€ ๋” ๋ณต์žกํ•œ handleRequestUser Task๋„ ํ…Œ์ŠคํŠธ๋กœ ์จ๋ด…๋‹ˆ๋‹ค.

sagas.js

function* handleRequestUser() {
  while (true) {
    const action = yield take(REQUEST_USER);
    const { payload, error } = yield call(API.user, action.payload);
    if (payload && !error) {
      yield put(successUser(payload));
    } else {
      yield put(failureUser(error));
    }
  }
}

ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ์‹คํŒจํ–ˆ๋Š”์ง€๋กœ ๋ถ„๊ธฐ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ ํ…Œ์ŠคํŠธ๋„ ๊ฐ๊ฐ์˜ ์ผ€์ด์Šค๋ฅผ ์ ์Šต๋‹ˆ๋‹ค.

test.js

describe('handleRequestUser', () => {
  let saga;
  beforeEach(() => {
    saga = handleRequestUser();
  });

  it('receives fetch request and succeeds to get data', () => {
    let ret = saga.next();
    assert.deepEqual(ret.value, take(REQUEST_USER)); // (A')

    ret = saga.next({ payload: 123 }); // (A)
    assert.deepEqual(ret.value, call(API.user, 123)); // (B')

    ret = saga.next({ payload: 'GOOD' }); // (B)
    assert.deepEqual(ret.value, put(successUser('GOOD')));

    ret = saga.next();
    assert.deepEqual(ret.value, take(REQUEST_USER));
  });

  it('receives fetch request and fails to get data', () => {
    let ret = saga.next();
    assert.deepEqual(ret.value, take(REQUEST_USER));

    ret = saga.next({ payload: 456 });
    assert.deepEqual(ret.value, call(API.user, 456));

    ret = saga.next({ error: 'WRONG' });
    assert.deepEqual(ret.value, put(failureUser('WRONG')));

    ret = saga.next();
    assert.deepEqual(ret.value, take(REQUEST_USER));
  });
});

์ด๊ฒƒ์€ Generatorํ•จ์ˆ˜์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ๋˜๋ฏ€๋กœ ์ต์ˆ™ํ•˜์ง€ ์•Š์œผ๋ฉด ์–ด๋ ต๊ฒ ๋„ค์š”. ์ ‘๊ทผํ•˜๋Š” ๋ฐฉ๋ฒ•์€ next()๋ฅผ ๋ถ€๋ฅผ๋•Œ ์ฒ˜์Œ์˜ yield๊นŒ์ง€ ์‹คํ–‰๋˜๋Š” ๊ทธ๋•Œ์˜ ์šฐ๋ณ€์˜ ๊ฐ’์„ ๋žฉํ•‘ํ•œ ๊ฒƒ์ด ๋ฆฌํ„ด๊ฐ’์œผ๋กœ ๋‚˜์˜ต๋‹ˆ๋‹ค. ์šฐ๋ณ€ ๊ฐ’ ์ž์ฒด๋Š” value ํ”„๋กœํผํ‹ฐ์— ๊ฒฉ๋‚ฉ๋˜์–ด์žˆ์œผ๋ฏ€๋กœ ๊ฑฐ๊ธฐ์„œ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

์ง€๊ธˆ, Task๋Š” ์ •์ง€๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์žฌ๊ฐœํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋”์šฑ์ด next()๋ฅผ ๋ถˆ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค. ์ด next()์˜ ์ธ์ˆ˜๋กœ์จ ๋„˜๊ฒจ์ค€ ๊ฒƒ์€, Task๊ฐ€ ์žฌ๊ฐœ๋  ๋•Œ์— yield๋กœ๋ถ€ํ„ฐ ๋‚˜์˜จ ๋ฆฌํ„ด ๊ฐ’์ด ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰ ์ฝ”๋“œ์ค‘์˜ (A)๋กœ ๋„˜๊ฒจ์ง€๋Š” ๊ฒƒ์ด (A')๋กœ๋ถ€ํ„ฐ ๋‚˜์˜ฌ๊ฑฐ๋ผ ๊ธฐ๋Œ€๋˜๋Š” ๋ฆฌํ„ด ๊ฐ’์ด๋ผ๋Š” ๊ฒƒ์ด์ฃ . ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ (B)๋กœ ๋„˜๊ฒจ์ง„ ํ†ต์‹ ๊ฒฐ๊ณผ์˜ ์˜ค๋ธŒ์ ํŠธ๊ฐ€ (B')์˜ call Effect๋กœ ๋ถˆ๋Ÿฌ์ง„ ๊ฒฐ๊ณผ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

๋งˆ์ง€๋ง‰์œผ๋กœ, ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚˜๋ฉด, ๋‹ค์‹œ ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ƒํƒœ๊ฐ€ ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. Task๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ์ป๊ธฐ์—, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ๋™๊ธฐ์ ์œผ๋กœ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์กฐ๊ธˆ ์„ค๋ช…์ด ๋น ๋ฅธ ๋Š๋‚Œ๋„ ์žˆ์ง€๋งŒ, ์™œ redux-saga์˜ ์‹คํ–‰๋ชจ๋ธ๋กœ ์ œ๊ณต๋œ ์ปค๋งจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Task๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ธ๊ฐ€๊ฐ€ ์ดํ•ด๋˜์—ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“ ๊ฒƒ์ด ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋˜์–ด, ๋ณต์žกํ•œ ๋ชฉ์„ ๋งŒ๋“ค ํ•„์š”์„ฑ๋„ ์ตœ์†Œํ•œ์œผ๋กœ ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์…‹์—…

Task์˜ ์„ค๋ช…์œผ๋กœ ์ด๊ฒƒ ์ €๊ฒƒ ๋„˜์–ด๊ฐ€๋ฒ„๋ ธ๊ธฐ์—, ์กฐ๊ธˆ๋” redux-saga์˜ ์„ค์ •์— ๋Œ€ํ•ด ์ฃผ์˜์ ๋„ ์•Œ๋ ค๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ์•ž์„œ ๋งํ•˜์ง€๋งŒ, ๊ธฐ๋ณธ์ ์ธ๊ฑด ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฝ๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹์Šต๋‹ˆ๋‹ค. ์ดํ›„ ๋Œ€ํญ ๋ฐ”๋€” ๊ฐ€๋Šฅ์„ฑ์€ ์ ์ง€๋งŒ, ๊ทธ๋Ÿด ๋•Œ ๊ธฐ๋Œˆ ์ˆ˜ ์žˆ๋Š”๊ฑด ์—ญ์‹œ ๊ณต์‹์ด๋‹ˆ๊นŒ์š”.

redux-saga์˜ ์ ์šฉ

์˜ˆ์ œ ์ฝ”๋“œ์˜ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋ณด๋ฉด ๊ธˆ์ƒˆ ์•Œ์•„์ฐจ๋ฆฌ์…จ์„์ง€ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ, redux-saga๋ฅผ ์“ธ ๋• 2๊ฐ€์ง€๋ฅผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜๋Š” Store์— Middleware๋ฅผ ์ง‘์–ด๋„ฃ๊ณ , ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” Task๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ดํ•˜๋Š” ์ „ํ˜•์ ์ธ ์…‹์—… ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. redux-logger๋Š” ํ•„์š”ํ•˜์ง€ ์•Š์œผ๋ฉด ์ง€์›Œ์ฃผ์„ธ์š”.

store.js

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import reducer from './reducers';
import rootSaga from './sagas';

export default function configureStore(initialState) {
  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
    reducer,
    initialState,
    applyMiddleware(
      sagaMiddleware, logger()
    )
  );
  sagaMiddleware.run(rootSaga);
  return store;
};

Store๋ฅผ ์ดˆ๊ธฐํ™”์‹œํ‚ค๋Š” ํƒ€์ด๋ฐ

์ด์ „์— ๊ฒช์—ˆ๋˜ ์ผ๋กœ, ์˜๋„ํ•˜์ง€ ์•Š์€ ํŽ˜์ด์ง€์—์„œ redux-saga๊ฐ€ ๊ธฐ๋™๋˜์–ด ํ†ต์‹ ์ฒ˜๋ฆฌ๊ฐ€ ์‹œ์ž‘๋œ ์ ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์›์ธ์€ store.js์— ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

store.js

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  reducer,
  applyMiddleware(
    sagaMiddleware, logger()
  )
);
sagaMiddleware.run(rootSaga);
export default store;

configureStore ํ•จ์ˆ˜๋ฅผ exportํ•˜๋Š” ๋Œ€์‹ ์— ์ž‘์„ฑํ•œ Store๋ฅผ exportํ•˜๊ณ  ์žˆ๋„ค์š”. ๊ทธ๋ฆฌ๊ณ  saga.js๋Š” ์ด๋Ÿฐ ์ƒํƒœ์˜€์Šต๋‹ˆ๋‹ค.

sagas.js

export default function* rootSaga() {
  yield fork(loadHogeHoge);
}

์ดˆ๊ธฐํ™”์‹œ ๋ฌด์–ธ๊ฐ€๋ฅผ ์ฝ์–ด๋“ค์ด๋Š” ํ˜•ํƒœ์˜ ํƒœ์Šคํฌ์ž…๋‹ˆ๋‹ค.

index.js

import store from './store.js';

// ...

const el = document.getElementById('container');
if (el) {
  ReactDOM.render(
    <Provider store={store}>
      <App />
    </Provider>,
  );
}

์ด๋ฏธ ํŒŒ์•…ํ•˜์…จ์„๊ฑฐ๋ผ ์ƒ๊ฐ๋˜์ง€๋งŒ, ์œ„์˜ ๊ตฌ์„ฑ์ด๋ฉด Provider ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ๋˜์—ˆ๋Š”์ง€ ๋ง์•˜๋Š”์ง€์— ์ƒ๊ด€์—†์ด Store๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์–ด Middleware๋„ ์ดˆ๊ธฐํ™”๋˜๊ณ  ๋ง™๋‹ˆ๋‹ค. ๊ทธ ๊ฒฐ๊ณผ, ๊ธฐ๋™์‹œ ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๋ณด๋‚ด๋Š” ํ˜•ํƒœ์˜ Task๋ผ๋ฉด ์—‰๋šฑํ•œ ํƒ€์ด๋ฐ์— ์‚ฌํƒœ๊ฐ€ ๋ฒŒ์–ด์ง€๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์กฐ์‹ฌํ•˜๋„๋ก ํ•ฉ์‹œ๋‹ค. (์ €๋„ ๋ฐ˜์„ฑ์„..)

Middleware์˜ ์‹คํ–‰ ํƒ€์ด๋ฐ

v0.10.0 ๋ถ€ํ„ฐ redux-saga์˜ ๊ธฐ๋™๋ฐฉ๋ฒ•์ด ๋ฐ”๋€Œ์—ˆ์Šต๋‹ˆ๋‹ค.

before.js

const store = createStore(
  reducer,
  applyMiddleware(createSagaMiddleware(rootSaga))
)

์ด๋ ‡๊ฒŒ ์“ฐ๋˜ ๊ฒƒ์ด

after.js

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  reducer,
  initialState,
  applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);

์ด๋ ‡๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ดˆ๊ธฐ์‹คํ–‰ Task๋ฅผ Middleware์˜ ์ƒ์„ฑ์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ, Store์˜ ์ดˆ๊ธฐํ™”๊ฐ€ ์™„๋ฃŒ๋œ ๋’ค์— run ๋ฉ”์†Œ๋“œ๋ฅผ ๋ถ€๋ฅด๋Š” ๊ฒƒ์œผ๋กœ ๊ธฐ๋™ํ•ฉ๋‹ˆ๋‹ค.

๋””๋ฒ„๊ทธ

Task๋Š” ํ•˜๋‚˜ํ•˜๋‚˜ ๋…๋ฆฝํ•˜๊ฒŒ ์‹คํ–‰๋˜๋ฏ€๋กœ, ํ•  ๊ฒƒ์„ ์ œํ•œํ•ด์„œ ๋‹จ์ˆœํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋ฉด ๋””๋ฒ„๊ทธ ํˆด์ด ํ•„์š”ํ•  ๋งŒํผ ๋ณต์žกํ•ด์ง€์ง„ ์•Š๊ฒ ์ง€๋งŒ, ์ผ๋‹จ, redux-saga์—๋Š” ๋ชจ๋‹ˆํ„ฐ๋ง ํˆด์„ ์ง‘์–ด ๋„ฃ์„ ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. effectTriggered, effectResolved, effectRejected, effectCancelled ์˜ 4๊ฐœ์˜ ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์ง€๋Š” ์˜ค๋ธŒ์ ํŠธ๋ฅผ createSagaMiddleware ํ•จ์ˆ˜์˜ ์˜ค๋ธŒ์ ํŠธ๋กœ์จ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.

store.js

import sagaMonitor from './saga-monitor';

export default function configureStore(initialState) {
  const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
  const store = createStore(...

๋ชจ๋‹ˆํ„ฐ์˜ ์ ์šฉ์€ ์ผ๋‹จ redux-saga์˜ examples/sagaMonitor๋ฅผ ์จ๋ด์ฃผ์„ธ์š”. ๊ทธ๋ฆฌ๊ณ , ์ด ๋ชจ๋‹ˆํ„ฐ๋Š” ๋””ํดํŠธ๋กœ ์•„๋ฌด๊ฒƒ๋„ ํ‘œ์‹œํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ฝ”๋“œ์ค‘์˜ VERBOSE๋ผ๋Š” ๋ณ€์ˆ˜๋ฅผ true๋กœ ๋ฐ”๊พธ์…”์•ผ ๋– ๋“ค๊ธฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ, redux-logger์™€ ๊ฐ™์ด ๋กœ๊ทธ๊ฐ€ ๊ณ„์†ํ•ด์„œ ํ˜๋Ÿฌ๊ฐ€๋Š” ๊ฑธ ๋ณด๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ, ํ•„์š”ํ•  ๋•Œ ๋ธŒ๋ผ์šฐ์ €ํˆด๋กœ๋ถ€ํ„ฐ window.$$LogSagas ํ•จ์ˆ˜๋ฅผ ๋ถˆ๋Ÿฌ์„œ Task tree๋ฅผ ์ง€์ผœ๋ณด๋Š”๊ฒŒ ์ฃผ ๋ชฉ์  ์ž…๋‹ˆ๋‹ค. ์‹คํ–‰ํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ๊ทธ๋‹ค์ง€ ๋ฉ‹์žˆ์–ด ๋ณด์ด์ง„ ์•Š์œผ๋ฏ€๋กœ D3.js๋กœ ์‹œ๊ฐํ™” ํˆด์„ ๋งŒ๋“ค์–ด ๋ณผ ์ƒ๊ฐ์ž…๋‹ˆ๋‹ค.

saga-monitor.png

์ด ๋‹ค์Œ์˜ API ์š”์ฒญ์‹œ์˜ ์Šค๋กœํ‹€๋ง์—์„œ ์†Œ๊ฐœํ•˜๋Š” ์˜ˆ์ œ์—๋Š” ๋ชจ๋‹ˆํ„ฐ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๋ฐ๋ชจ๋กœ ์‹œํ—˜ํ•ด ๋ณด์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‹ค์ „ redux-saga

redux-saga์—๋Š” ํ’๋ถ€ํ•œ ์˜ˆ์ œ๊ฐ€ ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ญ”๊ฐ€ ๊ณค๋ž€ํ•  ๋•Œ, ํžŒํŠธ๊ฐ€ ์—†๋‚˜ ์‹ถ์„ ๋•Œ ๋ณด๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค. ...ํ•˜์ง€๋งŒ, ์ด๊ฑธ๋กœ ๋งˆ๋ฌด๋ฆฌํ•˜๊ธฐ์—” ์•„์‰ฝ๊ธฐ์— ๋‹ค๋ฅธ ์ด์šฉ ์˜ˆ๋ฅผ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ๋ช‡์ผ์ „ ๋ฆด๋ฆฌ์ฆˆ๋œ 0.10.0์˜ ์‹ ๊ธฐ๋Šฅ์ธ eventChannel๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ๋Š” ๊ทธ๋‹ค์ง€ ์—†์œผ๋ฏ€๋กœ ์ฐธ๊ณ ๊ฐ€ ๋  ๋“ฏ ํ•ฉ๋‹ˆ๋‹ค.

์—ญ์ž์ฃผ: ์—ฌ๊ธฐ์„œ ์†Œ๊ฐœ๋˜๋Š” ์‹ค์ „ ์˜ˆ์ œ๋“ค์˜ ์†Œ์Šค ์ฝ”๋“œ์™€ ๋ฐ๋ชจ๋“ค์€ ๋ฐ‘์˜ ๋งํฌ์—์„œ ์ฐพ์œผ์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํฌ์ŠคํŒ…์— ์„ค๋ช…๋˜์ง€ ์•Š์€ ์˜ˆ์ œ๋„ ์žˆ์œผ๋‹ˆ ์ด์ชฝ๋„ ํ›‘์–ด ๋ด์ฃผ์‹œ๋ฉด ๋”์šฑ ์ข‹์„ ๋“ฏํ•ฉ๋‹ˆ๋‹ค.

์†Œ์Šค ์ฝ”๋“œ : https://github.com/kuy/redux-saga-examples ๋ฐ๋ชจ : http://kuy.github.io/redux-saga-examples/

์ž๋™ ์™„์„ฑ

ํ…์ŠคํŠธ ํ•„๋“œ์— ์ž๋™ ์™„์„ฑ ๊ธฐ๋Šฅ์„ ๋„ฃ์„ ๋•Œ, ๋‹จ์ˆœํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค๋ฉด dispatch๋œ Action์„ take๋กœ ๋ฐ›์•„๋“ค์—ฌ call๋กœ ์š”์ฒญ์„ ๋ฐœํ–‰ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ put์œผ๋กœ ํ•˜๋ฉด ์ข‹์•„๋ณด์ž…๋‹ˆ๋‹ค. ๋‹จ, ์ด๊ฒƒ์€ ์ผ๋ฐ˜์ ์ธ ํ†ต์‹ ์ฒ˜๋ฆฌ๋กœ์จ, ๊ทธ๋Œ€๋กœ ์ ์šฉํ•ด๋ฒ„๋ฆฌ๋ฉด ์ž…๋ ฅ ํ•  ๋•Œ๋งˆ๋‹ค ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๋ณด๋‚ด๊ธฐ์— ๊ทธ๋‹ค์ง€ ์ข‹์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ์—์„œ๋Š” ์ดˆ๊ธฐ์˜ ๋ชป์ƒ๊ธด ์ž๋™ ์™„์„ฑ ๊ธฐ๋Šฅ์„ ๋ฉ‹์ง€๊ฒŒ ๊ณ ์ณ๋‚˜๊ฐ€๋Š” ๊ณผ์ •์„ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

๋ฐ๋ชจ: Autocomplete ์˜ˆ์ œ ์ฝ”๋“œ: kuy/redux-saga-examples > autocomplete

">https://github.com/kuy/redux-saga-examples/tree/master/autocomplete)-->

์ดˆ๊ธฐ ๊ตฌํ˜„

sagas.js

function* handleRequestSuggests() {
  while (true) {
    const { payload } = yield take(REQUEST_SUGGEST);
    const { data, error } = yield call(API.suggest, payload);
    if (data && !error) {
      yield put(successSuggest({ data }));
    } else {
      yield put(failureSuggest({ error }));
    }
  }
}

export default function* rootSaga() {
  yield fork(handleRequestSuggests);
}

ํ†ต์‹ ์ฒ˜๋ฆฌ์˜ ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ์ด๋„ค์š”. ์ฐธ๊ณ ๋กœ ์‹ค์€ ์ด ์ฝ”๋“œ, ํฐ ๋ฌธ์ œ๋ฅผ ์ง€๋‹ˆ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฌด์—‡์ธ๊ฐ€ ํ•˜๋ฉด, ํ†ต์‹ ์ฒ˜๋ฆฌ์˜ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ dispatch๋˜๋Š” Action์„ ํ˜๋ ค๋ณด๋‚ด ๋ฒ„๋ฆฝ๋‹ˆ๋‹ค. ์˜ˆ์ œ์—์„œ๋Š” ํ†ต์‹ ์ฒ˜๋ฆฌ๋ถ€๋ถ„์„ ๋”๋ฏธ๋กœ์จ setTimeout์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋“ฏ์ด ๋งŒ๋“ค์—ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๋ถ€๋ถ„์˜ ์‹œ๊ฐ„์„ 3์ดˆ ์ •๋„๋กœ ๋ฐ”๊พธ๋ฉด ํ™•์‹คํ•˜๊ฒŒ ๋ณด์ผ๊ฒ๋‹ˆ๋‹ค.

ํ˜๋ฆผ๋ฐฉ์ง€ ๋Œ€์ฑ…

์ด๋Ÿฐ ์ด์œ ๋กœ ๋ฉ‹์ง€๊ฒŒ ๋งŒ๋“ค๊ธฐ ์•ž์„œ ๋ฒ„๊ทธ๋ฅผ ์žก์์‹œ๋‹ค. ๋ฌธ์ œ๋Š” call๋กœ API.suggest์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋ถˆ๋Ÿฌ์ง€๋Š”๊ฑธ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  take๋กœ ๋Œ์•„๊ฐ€๋ฉด ํ˜๋ฆฌ์ง€๋Š” ์•Š๊ฒŒ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด fork๋กœ ์ƒˆ๋กœ์šด Task๋ฅผ ๊ธฐ๋™์‹œํ‚ค๋Š” ๊ฒƒ์ด ์ข‹์•„๋ณด์ด๋„ค์š”.

sagas.js

function* runRequestSuggest(text) {
  const { data, error } = yield call(API.suggest, text);
  if (data && !error) {
    yield put(successSuggest({ data }));
  } else {
    yield put(failureSuggest({ error }));
  }
}

function* handleRequestSuggest() {
  while (true) {
    const { payload } = yield take(REQUEST_SUGGEST);
    yield fork(runRequestSuggest, payload);
  }
}

export default function* rootSaga() {
  yield fork(handleRequestSuggest);
}

์ด๋Ÿฐ ํ˜•ํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์œผ๋กœ handleRequestSuggest Task๋กœ ํ†ต์‹ ์ฒ˜๋ฆฌ๊นŒ์ง€ ํ•ธ๋“ค๋งํ•˜๊ณ  ์žˆ์ง€๋งŒ, call ์ดํ›„์˜ ๋ถ€๋ถ„์„ ๋”ฐ๋กœ Task๋กœ ๋‚˜๋ˆ„์—ˆ์Šต๋‹ˆ๋‹ค. ์•„๋ฌด๋ฆฌ ์ด๋ฒˆ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ์ผ์–ด๋‚˜์ง€ ์•Š๋Š” ๋‹ค๊ณ  ํ•ด๋„, Action์„ ๊ฐ์‹œํ•˜๋Š” Task์™€ ํ†ต์‹ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š” ํƒœ์Šคํฌ๋ฅผ ๋‚˜๋ˆ„๋Š” ๊ฒƒ์ด ์ข‹์•„๋ณด์ž…๋‹ˆ๋‹ค. ์ด๊ฑธ๋กœ ๋ง‰ํž˜์—†์ด ๋ฆฌํ€˜์ŠคํŠธ๋„ ๋‚ ๋ฆด ์ˆ˜ ์žˆ๋„ค์š”! ์ž˜ ๋๋„ค์š”!

๋‹ค๋ฅธ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

์ž, ๋ฒ„๊ทธ๋Š” ๊ณ ์ณค์ง€๋งŒ ๊ณต๋ถ€๋ฅผ ์œ„ํ•ด ์ž ๊น ์˜†๊ธธ๋กœ ์ƒˆ๊ฒ ์Šต๋‹ˆ๋‹ค. redux-saga๋กœ Task๋ฅผ ์“ฐ๊ณ  ์žˆ์œผ๋ฉด ์œ„์˜ ํŒจํ„ด์ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋‚˜์˜ค๊ธฐ ๋•Œ๋ฌธ์— takeEvery๊ฐ€ ์ค€๋น„๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์‹œ ์จ๋ด…์‹œ๋‹ค.

sagas.js

import { call, put, fork, takeEvery } from 'redux-saga/effects';

function* runRequestSuggest(action) {
  const { data, error } = yield call(API.suggest, action.payload);
  if (data && !error) {
    yield put(successSuggest({ data }));
  } else {
    yield put(failureSuggest({ error }));
  }
}

function* handleRequestSuggest() {
  yield takeEvery(REQUEST_SUGGEST, runRequestSuggest);
}

export default function* rootSaga() {
  yield fork(handleRequestSuggest);
}

takeEvery ๋Š” ์ง€์ •ํ•œ Action์˜ dispatch๋ฅผ ๊ธฐ๋‹ค๋ ค, ๊ทธ Action์„ ์ธ์ˆ˜๋กœ์จ Task๋ฅผ ๊ธฐ๋™ํ•ฉ๋‹ˆ๋‹ค. ์ด์ „์—” ํ—ฌํผ ํ•จ์ˆ˜๋กœ์จ ์ œ๊ณต๋˜์—ˆ์ง€๋งŒ, 0.14.0๋ถ€ํ„ฐ ์ •์‹์œผ๋กœ Effect๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹จ, ํ—ฌํผ์˜ takeEvery๋Š” ์—†์–ด์งˆ ์˜ˆ์ •์ด๋ฏ€๋กœ ๋ฐ”๊พธ์‹ค๊ฑธ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , Effect๋กœ์จ takeEvery์™€ ํ—ฌํผ์˜ takeEvery๋Š” ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ Effect์ธ takeEvery๋ฅผ yield*๋กœ ์‚ฌ์šฉํ•ด์„œ๋Š” ์•ˆ๋ฉ๋‹ˆ๋‹ค.

๋ฉ‹์ง„ ๊ตฌํ˜„

๋ฒ„๊ทธ๋„ ๊ณ ์ณค๊ณ , ์ด๊ฑธ๋กœ ๊ณ ์น  ์ค€๋น„๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์–ด๋– ํ•œ ๋™์ž‘์ด ์ข‹์€์ง€ ์ •๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์จ๋ด…์‹œ๋‹ค.

  1. 1๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.
  2. ๋ฐ”๋กœ ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๋‚ ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
  3. ๋ช‡ ๊ธ€์ž ๋” ์ž…๋ ฅํ•œ๋‹ค.
  4. ์•„์ง ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค.
  5. ์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅ์ด ์—†๋Š” ์ƒํƒœ๊ฐ€ ์ผ์ •์‹œ๊ฐ„ ์ง€์†๋˜๋ฉด ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๋ณด๋‚ธ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” ์ผ์ • ์‹œ๊ฐ„ ๊ธฐ๋‹ค๋ฆฌ๋ฉด ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๊ฐœ์‹œํ•˜๋Š” ์ง€์—ฐ์‹คํ–‰ Task๋ฅผ ์ •์˜ํ•˜์—ฌ, ์ž…๋ ฅ์ด ์žˆ์„ ๋•Œ๋งˆ๋‹ค ๊ทธ๊ฒƒ์„ ๊ธฐ๋™ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋‹จ, ์ž…๋ ฅ์ด ์žˆ์—ˆ์„๋•Œ์— ์ด๋ฏธ ์ง€์—ฐ Task๊ฐ€ ๊ธฐ๋™๋˜์–ด ์žˆ์„ ๋•Œ๋Š”, ๋จผ์ € ๊ทธ๊ฒƒ์„ ์ทจ์†Œํ•˜๊ณ ๋‚˜์„œ ์ƒˆ๋กœ์šด Task๋ฅผ ๊ธฐ๋™์‹œํ‚ฌ ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ง€์—ฐ์‹คํ–‰ Task๋Š” ์•„๋ฌด๋ฆฌ ๋งŽ์•„๋„ 1๊ฐœ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์ฝ”๋“œ๋ฅผ ๋ด…์‹œ๋‹ค.

sagas.js

import { delay } from 'redux-saga';
import { call, put, fork, take } from 'redux-saga/effects';

function* runRequestSuggest(text) {
  const { data, error } = yield call(API.suggest, text);
  if (data && !error) {
    yield put(successSuggest({ data }));
  } else {
    yield put(failureSuggest({ error }));
  }
}

function forkLater(task, ...args) {
  return fork(function* () {
    yield call(delay, 1000);
    yield fork(task, ...args);
  });
}

function* handleRequestSuggest() {
  let task;
  while (true) {
    const { payload } = yield take(REQUEST_SUGGEST);
    if (task && task.isRunning()) {
      task.cancel();
    }
    task = yield forkLater(runRequestSuggest, payload);
  }
}

export default function* rootSaga() {
  yield fork(handleRequestSuggest);
}

์ฃผ๋ชฉํ•  ํฌ์ธํŠธ๋Š” 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 ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฐ€์ง€๋Š” ์˜ค๋ธŒ์ ํŠธ๋กœ ๊ธฐ๋™๋œ Task์˜ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ์ทจ์†Œํ•˜๋Š” ๋“ฑ, ์ด๊ฒƒ์ €๊ฒƒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์›ํ•˜๋Š” ๋™์ž‘์€ ๋งŒ๋“ค์–ด ์กŒ์ง€๋งŒ, handleRequestSuggest Task์˜ "Action์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค"๋ผ๋Š” ์—ญํ• ์ด ์•Œ์•„๋ณด๊ธฐ ์–ด๋ ค์›Œ์กŒ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•œ ์›๋ž˜์˜ Task์ฒ˜๋Ÿผ, ํ•˜๊ณ  ์‹ถ์€ ์˜๋„๋ฅผ ์ „ํ•˜๋Š” ์ฝ”๋“œ๋ผ๋ฉด ์ข‹์•˜๊ฒ ์ง€์š”.

before.js

function* handleRequestSuggest() {
  while (true) {
    const { payload } = yield take(REQUEST_SUGGEST);
    yield fork(runRequestSuggest, payload);
  }
}

์ž๋™ ์™„์„ฑ์˜ ๊ธฐ๋Šฅ๋งŒ ์ƒ๊ฐํ•˜๋ฉด ๋ฉ‹์ง€๊ฒŒ ๋˜์—ˆ์œผ๋‹ˆ, ์ด์ œ ์ฝ”๋“œ๋„ ๋ฉ‹์ง€๊ฒŒ ๋งŒ๋“ค์–ด ๋ด…์‹œ๋‹ค.

๋”์šฑ ๋ฉ‹์ง„ ๊ตฌํ˜„

์–ด๋–ป๊ฒŒ ํ•˜๋ƒ๋ฉด, handleRequestSuggest Task์— ํฉ์–ด์ง„ ์ทจ์†Œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์„ ๋ถ„๋ฆฌ์‹œํ‚ต๋‹ˆ๋‹ค. ์ด๋Š” 1๊ฐœ์˜ Task๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์ผ์„ ์ค„์—ฌ ์—ญํ• ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์ ๊ทน์ ์œผ๋กœ ํ•ด๋ณด๊ณ  ์‹ถ์—ˆ๋˜ ๊ฐœ์„  ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค.

sagas.js

function* runRequestSuggest(text) {
  const { data, error } = yield call(API.suggest, text);
  if (data && !error) {
    yield put(successSuggest({ data }));
  } else {
    yield put(failureSuggest({ error }));
  }
}

function createLazily(msec = 1000) {
  let ongoing;
  return function* (task, ...args) {
    if (ongoing && ongoing.isRunning()) {
      ongoing.cancel();
    }
    ongoing = yield fork(function* () {
      yield call(delay, msec);
      yield fork(task, ...args);
    });
  }
}

function* handleRequestSuggest() {
  const lazily = createLazily();
  while (true) {
    const { payload } = yield take(REQUEST_SUGGEST);
    yield fork(lazily, runRequestSuggest, payload);
  }
}

export default function* rootSaga() {
  yield fork(handleRequestSuggest);
}

handleRequestSuggest Task๊ฐ€ ๋งค์šฐ ๊น”๋”ํ•ด์กŒ์Šต๋‹ˆ๋‹ค. fork(runRequestSuggest, payload)์˜€๋˜ ๋ถ€๋ถ„์ด fork(lazily, runRequestSuggest, payload)๋กœ ๋ฐ”๋€Œ์—ˆ์„ ๋ฟ์ด๊ธฐ์— ๋ณ€ํ™”๋Š” ๋งŽ์ง„ ์•Š์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ ์–ด๋„ ์˜์–ด์ฒ˜๋Ÿผ "fork lazily"๋กœ ์ฝ์–ด์ง€๊ธฐ์— ์˜๋„๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ๊ฐ€ ์‰ฌ์›Œ์กŒ์„ ๊ฒ๋‹ˆ๋‹ค.

๋งˆ๋ฒ•์ฒ˜๋Ÿผ ์ง€์—ฐ ์‹คํ–‰ํ•ด์ฃผ๋Š” lazily Task์ด์ง€๋งŒ ์ด๊ฒƒ์€ createLazily ํ•จ์ˆ˜๋กœ ์ƒ์„ฑํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์‹คํ–‰์ค‘์˜ Task๋ฅผ ์กด์†์‹œํ‚ค๊ธฐ ์œ„ํ•ด ํด๋กœ์ ธ๋กœ ๋งŒ๋“ค ํ•„์š”๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜๋Š” ์ผ์€ ์ด์ „์˜ ๊ตฌํ˜„๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฑธ๋กœ ๊ธฐ๋Šฅ๋„ ๊ตฌํ˜„๋„ ๋ฉ‹์ง€๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์—ฐ๊ตฌ๊ณผ์ œ

  • ์ง€์—ฐ์‹คํ–‰์ด ์‹œ์ž‘๋˜๊ธฐ๊นŒ์ง€ ์•„๋ฌด๊ฒƒ๋„ ํ‘œ์‹œํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ๋‹ค.
  • takeLatest ํ—ฌํผํ•จ์ˆ˜๋ฅผ ์จ์„œ ๋‹ค์‹œ ์“ด๋‹ค.

API ์š”์ฒญ์‹œ์˜ ์Šค๋กœํ‹€๋ง

๋ฐ๋ชจ: Throttle ์˜ˆ์ œ: kuy/redux-saga-examples > throttle

">https://github.com/kuy/redux-saga-examples/tree/master/throttle)-->

ํฌ์ŠคํŒ… ๋ฆฌ์ŠคํŠธ์™€ ๊ฐ™์ด ๋งŽ์€ ์ปจํ…์ธ ๋ฅผ ํ•œ๋ฒˆ์— ์ฝ์–ด์™€์„œ, ๋”์šฑ์ด ๊ฐ๊ฐ์˜ ์ปจํ…์ธ ๋งˆ๋‹ค ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ์š”์ฒญํ•˜๋ฉด, ์ปจํ…์ธ ์˜ ์ˆ˜๋งŒํผ ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ๋™์‹œ์— ๋ณด๋‚ด๊ฒŒ๋˜๋‹ˆ ์‹ฌ๊ฐํ•œ ์ผ์ด ์ผ์–ด๋‚˜๊ฒ ์ฃ . ์„œ๋ฒ„๋ถ€ํ•˜์™€ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค๊ณ  ํ•ด๋„, Dos๊ณต๊ฒฉ์œผ๋กœ ํŒ๋‹จ๋˜์–ด ๋ฆฌํ€˜์ŠคํŠธ๊ฐ€ ์ฐจ๋‹จ๋˜๋Š” ์ผ๋„ ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค. ๋˜ ํ†ต์‹ ์ฒ˜๋ฆฌ์˜ ๊ฒฝ์šฐ, ๋Œ€๋Ÿ‰๋ฐœ์ƒํ•˜๋Š” Action์— ๋”ฐ๋ฅธ Task์˜ ๋™์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ˆ˜ ์ด์ƒ์€ ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ณ  ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋งŒ๋“ค์–ด์„œ, ์•ž์„  Task๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ˆœ์„œ๋Œ€๋กœ ๋‹ค์Œ Task๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š” ํ(Queue)๊ฐ€ ํ•„์š”ํ• ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ์˜ˆ์ œ๋Š” ๊ธฐ๋™์ค‘์ธ Task์˜ ์ˆ˜๋ฅผ ์กฐ์ ˆํ•˜๋Š” ์Šค๋กœํ‹€๋ง์„ redux-saga๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

sagas.js

const newId = (() => {
  let n = 0;
  return () => n++;
})();

function something() {
  return new Promise(resolve => {
    const duration = 1000 + Math.floor(Math.random() * 1500);
    setTimeout(() => {
      resolve({ data: duration });
    }, duration);
  });
}

function* runSomething(text) {
  const { data, error } = yield call(something);
  if (data && !error) {
    yield put(successSomething({ data }));
  } else {
    yield put(failureSomething({ error }));
  }
}

function* withThrottle(job, ...args) {
  const id = newId();
  yield put(newJob({ id, status: 'pending', job, args }));
}

function* handleThrottle() {
  while (true) {
    yield take([NEW_JOB, RUN_JOB, SUCCESS_JOB, FAILURE_JOB, INCREMENT_LIMIT]);
    while (true) {
      const jobs = yield select(throttleSelector.pending);
      if (jobs.length === 0) {
        break; // No pending jobs
      }

      const limit = yield select(throttleSelector.limit);
      const num = yield select(throttleSelector.numOfRunning);
      if (limit <= num) {
        break; // No rooms to run job
      }

      const job = jobs[0];
      const task = yield fork(function* () {
        yield call(job.job, ...job.args);
        yield put(successJob({ id: job.id }));
      });
      yield put(runJob({ id: job.id, task }));
    }
  }
}

function* handleRequestSomething() {
  while (true) {
    yield take(REQUEST_SOMETHING);
    yield fork(withThrottle, runSomething);
  }
}

export default function* rootSaga() {
  yield fork(handleRequestSomething);
  yield fork(handleThrottle);
}

์ž๋™์™„์„ฑ ์˜ˆ์ œ๋Š” 1๊ฐœ์˜ Task๋งŒ ๋™์‹œ์— ์‹คํ–‰์‹œํ‚ค๊ณ , ์ƒˆ๋กœ์šด Task๊ฐ€ ์˜ค๋ฉด ์ฒ˜๋ฆฌ์ค‘์ธ Task๋ฅผ ์ทจ์†Œ์‹œํ‚ค๊ณ  ๋‚˜์„œ ๊ธฐ๋™์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ Task๊ฐ€ ๊ธฐ๋™์ค‘์ธ์ง€ ์•„๋‹Œ์ง€๋ฅผ ํŒ์ •ํ•˜๊ธฐ ์œ„ํ•ด ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ํ•„์š”๊ฐ€ ์žˆ์—ˆ๊ณ , ๊ทธ๊ฒƒ์„ ํด๋กœ์ ธ ๋‚ด์—์„œ ๋‹ค๋ฃจ๊ฒŒ ํ•˜๋Š” ์–ดํ”„๋กœ์น˜์˜€์Šต๋‹ˆ๋‹ค. ์Šค๋กœํ‹€๋ง์œผ๋กœ๋„ ์‹คํ–‰์ค‘์˜ Task๋ฅผ ํŒŒ์•…ํ•  ํ•„์š”๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฌด์–ธ๊ฐ€์˜ ์ƒํƒœ๋ฅผ ๊ธฐ๋‹ค๋ฆด ํ•„์š”๊ฐ€ ์žˆ๋Š” ์ ์ด ๊ณตํ†ต๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ฒˆ์—” ๋‹ค๋ฅธ ์–ดํ”„๋กœ์น˜๋กœ, ์ƒํƒœ๋ฅผ Task ๋‚ด๋ถ€์—์„œ ๊ฐ€์ง€์ง€ ์•Š๊ณ , ๋Œ€์‹  Store์—๋‹ค ๋„ฃ์–ด๋‘ก๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‹คํ–‰ ์ƒํƒœ๊ฐ€ ๋ทฐ์—์„œ ์‹ค์‹œ๊ฐ„ ํ‘œ์‹œ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์œ„๋Š” sagas.js์˜ ์ฝ”๋“œ๋งŒ ๋ณด์—ฌ์ฃผ์—ˆ์ง€๋งŒ, ์ด๋ฒˆ์—” ์ƒํƒœ๋ฅผ Store๊ฐ€ ๊ฐ€์ง€๊ฒŒ ํ•˜๋ฏ€๋กœ ์ „์ฒด๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด reducers.js๋„ ์ค‘์š”ํ•˜๋‹ˆ ํ•œ๋ฒˆ ํ›‘์–ด๋ด์ฃผ์„ธ์š”.

๏ผ’๊ฐœ์˜ Task

๊ตฌํ˜„์€ ํฌ๊ฒŒ ๋‚˜๋ˆ„๋ฉด 2๊ฐœ์˜ Task, handleRequestSomething์™€ handleThrottle๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค. ์ „์ž๋Š” REQUEST_SOMETHING Action์˜ dispatch๋ฅผ ๊ฐ์‹œํ•˜์—ฌ ์‹คํ–‰ํ•ด์•ผํ•  Task๋งŒ์„ ๋ณด๋‚ด์ค๋‹ˆ๋‹ค. ํ›„์ž๋Š” ์กฐ๊ธˆ ๋ณต์žกํ•ฉ๋‹ˆ๋‹ค. handleRequestSomething Task๋กœ ๋ถ€ํ„ฐ ์‹คํ–‰ ์š”์ฒญ๋œ Task๋ฅผ ์ผ๋‹จ ํ์— ๋„ฃ์–ด๋‘๊ณ , ๋™์‹œ ์‹คํ–‰ ์ˆ˜๋ฅผ ์กฐ์ •ํ•˜๋ฉด์„œ ์ฒ˜๋ฆฌํ•ด๊ฐ‘๋‹ˆ๋‹ค. ์Šค๋กœํ‹€๋ง์ด ์—†๋Š” ์‹คํ–‰ fork(runSomething)๊ณผ ์Šค๋กœํ‹€๋ง์ด ์žˆ๋Š” ์‹คํ–‰ fork(withThrottle, runSomething)์—์„  ์ฝ”๋“œ์˜ ์ฐจ์ด๊ฐ€ ์กฐ๊ธˆ๋งŒ ์žˆ๋„๋ก ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๏ผ’์ค‘ while ๋ฃจํ”„

handleThrottle Task๋ฅผ ๋ณด๋ฉด ์กฐ๊ธˆ ๋‚ฏ์„ค์€ 2์ค‘์˜ while ๋ฃจํ”„๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ๋ฒˆ์งธ ๋ฃจํ”„๋Š” ์ต์ˆ™ํ•œ ํŒจํ„ด์ด๋ฏ€๋กœ ๊ดœ์ฐฎ์„ ๊ฒ๋‹ˆ๋‹ค. 2๋ฒˆ์งธ ๋ฃจํ”„๋Š” ์‹คํ–‰๊ฐ€๋Šฅํ•œ job์˜ ์ˆ˜์— ์—ฌ์œ ๊ฐ€ ์žˆ๋Š”ํ•œ job์˜ ์‹คํ–‰์„ ์‹œ์ž‘ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ์šฐ์„ ํ•ด์„œ while ๋ฃจํ”„๋กœ ๋งŒ๋“ค์–ด์ ธ ์žˆ์ง€๋งŒ, ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ job์˜ ์ˆ˜์™€ ๋Œ€๊ธฐ ์ƒํƒœ์˜ job์„ ๋งŒ๋“ค์–ด ํ•œ๋ฒˆ์— ์‹คํ–‰์‹œ์ผœ๋„ ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค.

์—ฐ๊ตฌ๊ณผ์ œ

  • ๋‹ค์ค‘ ์‹คํ–‰ ํ
  • ์šฐ์„  ์ˆœ์œ„

์ธ์ฆ ์ ˆ์ฐจ๏ผˆ์„ธ์…˜ ์œ ์ง€๏ผ‰

redux-saga๋กœ ์ธ์ฆ์ฒ˜๋ฆฌ๋ฅผ ์–ด๋–ป๊ฒŒ ๋‹ค๋ฃจ๋Š”์ง€๋ฅผ ์ƒ๊ฐํ•ด๋ด…์‹œ๋‹ค.

๋งŒ๋“ค๊ณ  ์‹ถ์€๊ฑด, ์œ ์ €๊ฐ€ ๋กœ๊ทธ์ธํ•˜์—ฌ, ์ธ์ฆ๋ฐ›๊ณ , ์„ฑ๊ณตํ•˜๋ฉด ํ™”๋ฉด์ „์ด๋ฅผ ํ•˜๊ฑฐ๋‚˜, ์‹คํŒจํ•˜๋ฉด ๊ทธ ์ด์œ ๋ฅผ ํ‘œ์‹œํ•˜๊ณ , ๋กœ๊ทธ์•„์›ƒํ•˜๋ฉด ๋‹ค์‹œ ๋Œ€๊ธฐ์ƒํƒœ๋กœ ๋Œ์•„๊ฐ€๋Š”, ์ธ์ฆ ๋ผ์ดํ”„์‚ฌ์ดํด์˜ ์ „์ฒด์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์„œ๋ฒ„์‚ฌ์ด๋“œ์— ๊ตฌํ˜„ํ•˜๋ฉด, Cookie์™€ ๊ฐ™์€ ํ† ํฐ์„ ๊ฐ€์ง€๊ณ  ๋‚ ์•„์˜จ ๋ฆฌํ€˜์ŠคํŠธ๊ฐ€ ์–ด๋–ค ์œ ์ €๋กœ๋ถ€ํ„ฐ ์˜จ ๊ฑด์ง€๋ฅผ ์‹๋ณ„ํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰ ์ฒ˜๋ฆฌ ์ž์ฒด๋Š” ๋ฆฌํ€˜์ŠคํŠธ ๋‹จ์œ„๋กœ ๋˜์–ด ํ† ๋ง‰๋‚œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์„ redux-saga์˜(๋ญ ์˜ˆ์ œ๋Š” ํ˜ผ์ž์„œ ํ•˜๋ฏ€๋กœ ์‹๋ณ„ํ•˜๋Š”๊ฒŒ ๊ทธ๋‹ค์ง€ ์˜๋ฏธ์—†์ง€๋งŒ) Task๊ฐ€ ์ผ์‹œ ์ •์ง€์‹œํ‚ค๋Š”๊ฒŒ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ํŠน์ง•์„ ์‚ด๋ ค์„œ, ์ธ์ฆ ๋ผ์ดํ”„์‚ฌ์ดํด ์ „์ฒด๋ฅผ 1๊ฐœ์˜ Task๊ฐ€ ๊ด€๋ฆฌํ•˜๋„๋ก ๋งŒ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ํ•œ๋งˆ๋””๋กœ ์„ธ์…˜ ์œ ์ง€์— Task๋ฅผ ์“ฐ๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค.

์˜ˆ์ œ๋Š” ์–ด๋–ป๊ฒŒ ๋Œ์•„๊ฐ€๋Š”์ง€ ๋ถ„์œ„๊ธฐ ํŒŒ์•…์„ ์œ„ํ•œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. ์›๋ž˜ Gist์—๋‹ค ์“ด ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ•˜๋ฉด react-router-redux๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ€์‹œ๋ณด๋“œ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

sagas.js

import { push } from 'react-router-redux';

function* authSaga() {
  while (true) {
    // ๋กœ๊ทธ์ธ ํ•  ๋•Œ ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
    const { user, pass } = yield take(REQUEST_LOGIN);

    // ์ธ์ฆ์ฒ˜๋ฆฌ ์š”์ฒญ (์—ฌ๊ธฐ์„  try-catch๋ฅผ ์“ฐ์ง€์•Š๊ณ , ๋ฆฌํ„ด ๊ฐ’์— ์—๋Ÿฌ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜๋Š” ์Šคํƒ€์ผ๏ผ‰
    const { token, error } = yield call(authorize, user, pass);
    if (!token && error) {
      yield put({ type: FAILURE_LOGIN, payload: error });
      continue; // ์ธ์ฆ์— ์‹คํŒจํ•˜๋ฉด ์žฌ์‹œ๋„์™€ ํ•จ๊ป˜ ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.
    }

    // ๋กœ๊ทธ์ธ ์„ฑ๊ณต์˜ ์ฒ˜๋ฆฌ (ํ† ํฐ ์œ ์ง€ ๋“ฑ)
    yield put({ type: SUCCESS_LOGIN, payload: token });

    // ๋กœ๊ทธ์•„์›ƒ ํ•  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
    yield take(REQUEST_LOGOUT);

    // ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ (ํ† ํฐ ์ •๋ฆฌ๋“ฑ)
    yield call(SUCCESS_LOGOUT);
  }
}

function* pageSaga() {
  while (true) {
    // ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
    yield take(SUCCESS_LOGIN);

    // ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ํ•œ๋‹ค.
    yield put(push('/dashboard'));
  }
}

2๊ฐœ์˜ Task

ํ•ด์•ผ ํ•  ์ผ์€ ์ธ์ฆ์ฒ˜๋ฆฌ์˜ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ๊ณผ, ๋กœ๊ทธ์ธ ์„ฑ๊ณต์‹œ์— ํŽ˜์ด์ง€ ์ „์ด๋ฅผ ํ•˜๋Š” 2๊ฐœ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ๋“ค์€ ๋ฌผ๋ก  1๊ฐœ์˜ Task๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, redux-saga's way(๋ผ๋Š”๊ฒŒ ์žˆ์„์ง€๋Š” ๋ชจ๋ฅด์ง€๋งŒ)์— ๋”ฐ๋ผ ์ œ๋Œ€๋กœ ์—ญํ• ๋ณ„๋กœ Task๋ฅผ ๋‚˜๋ˆ„์–ด, ๊ฐ๊ฐ authSaga์™€ pageSaga๋กœ์จ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€์˜ 2๊ฐœ์˜ ์˜ˆ์ œ์—์„  ํ•„์š”์— ๋”ฐ๋ผ Task ๋‚ด๋ถ€์— ์™ธ๋ถ€์˜ ์ƒํƒœ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ์˜ˆ์ œ๋Š” ์ธ์ฆ์ฒ˜๋ฆฌ์˜ ๋ผ์ดํ”„์‚ฌ์ดํด์ด ์–ด๋””๊นŒ์ง€ ๋˜์–ด์žˆ๋‚˜๋ฅผ ์ƒํƒœ๋กœ ๊ฐ€์ง€๋Š” ๊ฒƒ์„ ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉํ•˜๋Š” ์˜ˆ์ž…๋‹ˆ๋‹ค. 1๊ฐœ์˜ ์ฒ˜๋ฆฌ๋Š” 1๊ฐœ์˜ ํƒœ์Šคํฌ๊ฐ€ ํ•ญ์ƒ ๋ถ™์–ด์žˆ๊ฒŒ ๋˜๋Š”๋ฐ redux-saga๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํƒœ์Šคํฌ ์‹คํ–‰ ํ™˜๊ฒฝ ๋•๋ถ„์ž…๋‹ˆ๋‹ค. ๋•๋ถ„์— ์ฝ”๋“œ๊ฐ€ ๋งค์šฐ ์ง๊ด€์ ์œผ๋กœ ๋ฉ๋‹ˆ๋‹ค.

์—ฐ๊ตฌ๊ณผ์ œ

  • ๋ณต์ˆ˜ ์„ธ์…˜์˜ ์œ ์ง€

Socket.IO

์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์กฐ๊ธˆ ๋‹ค๋ฅธ ์ข…๋ฅ˜์˜ ์˜ˆ๋ฅผ ์†Œ๊ฐœํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค. ๋จผ์ € Socket.IO์™€์˜ ์—ฐ๊ณ„์ž…๋‹ˆ๋‹ค.

์˜ˆ์ œ์ฝ”๋“œ: kuy/redux-saga-chat-examples

๋ฐ‘์€ ์˜ˆ์ œ์—์„œ ๊ฐ€์ ธ์˜จ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.

sagas.js

function subscribe(socket) {
  return eventChannel(emit => {
    socket.on('users.login', ({ username }) => {
      emit(addUser({ username }));
    });
    socket.on('users.logout', ({ username }) => {
      emit(removeUser({ username }));
    });
    socket.on('messages.new', ({ message }) => {
      emit(newMessage({ message }));
    });
    socket.on('disconnect', e => {
      // TODO: handle
    });
    return () => {};
  });
}

function* read(socket) {
  const channel = yield call(subscribe, socket);
  while (true) {
    const action = yield take(channel);
    yield put(action);
  }
}

function* write(socket) {
  while (true) {
    const { payload } = yield take(`${sendMessage}`);
    socket.emit('message', payload);
  }
}

function* handleIO(socket) {
  yield fork(read, socket);
  yield fork(write, socket);
}

function* flow() {
  while (true) {
    let { payload } = yield take(`${login}`);
    const socket = yield call(connect);
    socket.emit('login', { username: payload.username });

    const task = yield fork(handleIO, socket);

    let action = yield take(`${logout}`);
    yield cancel(task);
    socket.emit('logout');
  }
}

export default function* rootSaga() {
  yield fork(flow);
}

Socket.IO์œผ๋กœ๋ถ€ํ„ฐ ๋ฉ”์„ธ์ง€์˜ ์ˆ˜์‹ ์— eventChannel์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ›์€ Socket.IO์˜ ์ด๋ฒคํŠธ๋งˆ๋‹ค Redux์˜ Action์œผ๋กœ ๋งตํ•‘ํ•ด์ฃผ์–ด put์œผ๋กœ dispatchํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฑฐ๊ธฐ์— Task์˜ ์—ฐ์‡„์ ์ธ ์ทจ์†Œ ์—ญ์‹œ ์“ฐ์—ฌ์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ์˜ˆ์ œ๋Š” ๋Œ€์ถฉ ์ž‘๋™ํ•˜๋Š”์ง€๋งŒ ๋Œ๋ ค๋ณธ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. Read/Write์˜ ๋ถ€๋ถ„์ด๋‚˜ ๋ณต์ˆ˜ ์ฑ„๋„ ๋Œ€์‘, ํ•˜๋‚˜ํ•˜๋‚˜ ๋งตํ•‘์ด ๊ท€์ฐฎ์•„์„œ ์ด๋ฒคํŠธ์ด๋ฆ„์„ ๊ทธ๋Œ€๋กœ Action Types์œผ๋กœ ์“ฐ๊ฑฐ๋‚˜, Socket.IO์˜ ํ†ต์‹ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ฑฐ๋‚˜, ์–ด๋–ป๊ฒŒ ๋‹ค๋ฃฐ๊ฒƒ์ธ๊ฐ€์— ์•„์ง ๊ณ ๋ฏผ์ค‘์ž…๋‹ˆ๋‹ค. ์–ธ์  ๊ฐ€ ๊ทธ๊ฒƒ๋“ค์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋งŒ๋“ค์–ด๋ƒˆ์œผ๋ฉด ์ข‹๊ฒ ๋‹ค๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Firebase (Authentication + Realtime Database)

๋ฐ๋ชจ: Miniblog ์˜ˆ์ œ์ฝ”๋“œ kuy/redux-saga-examples > microblog

">https://github.com/kuy/redux-saga-examples/tree/master/microblog)-->

์ตœ์‹  ๋Œ€ํญ ์—…๋ฐ์ดํŠธ๊ฐ€ ์žˆ์—ˆ๋˜ Firebase๋ฅผ ์จ์„œ ์‹œํ—˜์‚ผ์•„ redux-saga์™€ ์—ฐ๊ณ„์‹œํ‚จ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค. ํ…Œ๋งˆ๋Š” ํŠธ์œ„ํ„ฐ์™€ ๊ฐ™์€ ๋ฏธ๋‹ˆ ๋ธ”๋กœ๊ทธ ์„œ๋น„์Šค์ด์ง€๋งŒ, ๊ตฌํ˜„์ด ๋œ๋˜์„œ ์ฑ„ํŒ… ์•ฑ์ฒ˜๋Ÿผ ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค... ๋ธŒ๋ผ์šฐ์ €์˜ ์‹œํฌ๋ฆฟ๋ชจ๋“œ์— ์—ฌ๋Ÿฌ๊ฐœ๋ฅผ ์—ด์–ด ๊ฐ๊ฐ์˜ ์ด๋ฆ„์œผ๋กœ ํˆฌ๊ณ ํ•˜๋ฉด ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ์˜ˆ์ œ๋Š” ์˜ˆ์ œ ์ด์ƒ์˜ ์˜๋ฏธ๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ redux-saga๋กœ Firebase๋‚˜ Socket.IO๋ฅผ ์“ธ ๊ฒƒ์ธ๊ฐ€์— ๋„ˆ๋ฌด ์ง‘์ฐฉํ•˜์ง€๋Š” ๋ง์•„์ฃผ์„ธ์š”. ์™œ๋ƒ๋ฉด redux-saga๋Š” ๊ธฐ๋Šฅ์ ์œผ๋ก  Middleware์˜ ์„œ๋ธŒ์…‹์ด๋ฏ€๋กœ, redux-saga๋กœ ํ•  ์ˆ˜ ์žˆ๋Š”๊ฑด Middleware๋กœ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ redux-saga๋กœ ๋งŒ๋“ค๋ฉด, ํ”„๋กœ์ ํŠธ์˜ ๋„์ž…ํ• ๋•Œ redux-saga๊ฐ€ ํ•„์ˆ˜๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. Middleware๋กœ ๊ฐ€๋Šฅํ•œ ๊ฒƒ์„ redux-saga๋กœ ๋งŒ๋“ค์–ด์„œ, ๋„์ž… ์žฅ๋ฒฝ์„ ๋†’์ด๊ฒŒ ๋˜๋ฉด ์˜๋ฏธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์€ ์ˆœ์ˆ˜ํ•˜๊ฒŒ Middleware๋‚˜ Store Enhancer ์ˆ˜์ค€์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š”๊ฒƒ์ด ์ข‹์ง€ ์•Š์„๊นŒ ์‹ถ์Šต๋‹ˆ๋‹ค.

PubNub

redux-saga-chat-example์ด๋ผ๋Š” redux-saga์™€ Socket.IO๋ฅผ ํ•ฉ์นœ ์ฑ„ํŒ… ์•ฑ์„ ๋งŒ๋“œ๋‹ˆ, ์™œ์ธ์ง€ PubNub์™€ ํ•จ๊ป˜ ์“ธ๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ•˜์ง€?๋ผ๋Š” ์งˆ๋ฌธ ์žˆ์–ด์„œ ์˜ˆ์ œ๋ฅผ ์จ๋ดค์Šต๋‹ˆ๋‹ค.

๋งŒ๋ณ‘ํ†ต์น˜์•ฝ์€ ์—†๋‹ค

redux-saga์˜ ์‚ฌ์šฉ๋ฒ•์„ ๋‹ค์–‘ํ•œ ๊ฐ๋„์—์„œ ๋ดค์Šต๋‹ˆ๋‹ค. ๋ญ๋“  ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์ง€๋งŒ, redux-saga์—๋„ ์ œ์•ฝ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  Middleware์˜ ์ฒ˜๋ฆฌ๋ฅผ ์ด์‹ํ•˜๋Š”๊ฑด ๋ถˆ๊ฐ€๋Šฅ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด Middleware์ฒ˜๋Ÿผ Action์„ ์†Ž์•„ ๋‚ด๋Š” ๊ฑด ๋ชปํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋ฒˆ ์ƒ˜ํ”Œ์€ Redux์˜ middleware๋ฅผ ์ ๊ทน์ ์œผ๋กœ ์จ๋ณด์ž์—์„œ ์†Œ๊ฐœํ•œ Action์„ Dispatchํ•˜๊ธฐ ์ „์— ๋ธŒ๋ผ์šฐ์ € ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ํ‘œ์‹œํ•˜์ž๋ฅผ ๊ทธ๋Œ€๋กœ ์ด์‹ํ•˜๋Š”๊ฑด ๋ถˆ๊ฐ€๋Šฅํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ™์€ ์ผ์„ ํ•˜๊ธฐ์—” ์šฐ์„  ๋‹ค๋ฅธ Action์„ dispatch์‹œ์ผœ, ํ™•์ธ ๋‹ค์ด๋Ÿด๋กœ๊ทธ์— Yes๊ฐ€ ๋‚˜์˜ค๋ฉด ์›๋ž˜์˜ Action์„ dispatchํ•˜๋Š” ์‹์˜ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌ๋ฉด ๋ณธ๋ง์ „๋„๊ฐ€ ๋˜๋ฏ€๋กœ ๊ทธ๋ƒฅ Middleware๋ฅผ ์“ฐ๋Š” ๊ฒŒ ์ข‹์€ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. ๋ง๋ถ™์—ฌ ์ด๋Ÿฌํ•œ ์ œ์•ฝ์€ Redux Middleware in Depth๋ผ๋Š” ํฌ์ŠคํŒ…์—๋„ ํ•ด์„คํ•œ Middleware๋ฅผ ์‹คํ–‰ํ•˜๋Š” ํƒ€์ด๋ฐ ๋•Œ๋ฌธ์— ์ผ์–ด๋‚˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. redux-saga์˜ ๊ฒฝ์šฐ, ํ•ญ์ƒ Reducer์˜ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚œ ๋‹ค์Œ์— Saga๊ฐ€ ์‹คํ–‰๋˜๋ฏ€๋กœ, ์ง€๊ธˆ ์ƒํƒœ๋กœ๋Š” ์–ด๋–ป๊ฒŒ ํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ˆ˜์š”๊ฐ€ ์žˆ์„์ง€๋Š” ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ redux-saga์— issue๋ฅผ ์„ธ์›Œ๋ณผ๊นŒ ์ƒ๊ฐํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

redux-saga๋ฅผ ์“ฐ๋Š” ๊ฒƒ์œผ๋กœ redux-thunk๋‚˜ Middleware๋ณด๋‹ค๋„ ๊ตฌ์กฐํ™”๋œ ์ฝ”๋“œ๋กœ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผ Task๋ผ๋Š” ์‹คํ–‰๋‹จ์œ„๋กœ ๊ธฐ์ˆ ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค. ๊ฑฐ๊ธฐ์— Mock์„ ์จ์•ผํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ค„์ด๊ณ , ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์€ ๋กœ์ง์— ์ง‘์ค‘ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, ์žฌ์ด์šฉ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐœ๋ฐœ์—์„œ๋„ ํ•„์š”ํ•œ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผ redux-saga์˜ Task๋กœ์„œ ์ œ๊ณตํ•˜๋ฉด, Middleware๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์— ์ผ์–ด๋‚˜๋Š” ์‹คํ–‰์ˆœ์„œ์˜ ๋ฌธ์ œ๋ฅผ ํ”ผํ•  ์ˆ˜ ์žˆ์–ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ชจ๋“  Middleware๋ฅผ redux-saga๋กœ ๋ฐ”๊พธ๋Š”๊ฑด ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.