-
๊ฒ์ฆ๋ UI ํจํด์ ์ด์ฉํด ๋ฆฌ์กํธ ์ดํ๋ฆฌ์ผ์ด์ ๋ง๋ค๊ธฐ์นดํ ๊ณ ๋ฆฌ ์์ 2024. 2. 7. 17:01
https://martinfowler.com/articles/modularizing-react-apps.html#ApartFromTheUserInterface
์ ๊ธ์ ์ฝ๊ณ ๋ฒ์ญ ๋ฐ ํ์ตํ ๋ด์ฉ์ด๋ค.
์ด ๊ธ์ ์ ๋ชฉ์์ ‘๋ฆฌ์กํธ ์ดํ๋ฆฌ์ผ์ด์ ’์ด๋ ํํ์ ์ฌ์ฉํ์ง๋ง, ์ฌ์ค ๋ฆฌ์กํธ ์ดํ๋ฆฌ์ผ์ด์ ์ด๋ผ๋ ๊ฒ์ ์๋ค. ์๋ฐ์คํฌ๋ฆฝํธ ํน์ ํ์ ์คํฌ๋ฆฝํธ๋ก ์์ฑํ๊ณ ๋ฆฌ์กํธ๋ฅผ ๋ทฐ๋ฅผ ๊ทธ๋ฆฌ๊ธฐ ์ํด ์ฌ์ฉํ ํ๋ก ํธ์๋ ์ดํ๋ฆฌ์ผ์ด์ ์ด ์์ ๋ฟ์ด๋ค.
์ข ์ข , ์ฌ๋๋ค์ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๋ ํ ์ค์ ๋ค์ํ ๊ฒ๋ค์ ์ฑ์ฌ๋ฃ์ด ์ดํ๋ฆฌ์ผ์ด์ ์ด ๋์๊ฐ๋๋ก ํ๋ค. ์ด๋ ๊ฒ ๋ ์ ๋ฆฌ๋ ๊ตฌ์กฐ๋ค์ ์์ ์ดํ๋ฆฌ์ผ์ด์ ์ด๋ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ง์ง ์์ ๋์๋ ๊ด์ฐฎ์ ์ ์๋ค. ๊ทธ๋ฌ๋, ์์ฆ๊ฐ์ด ๋ ๋ง์ ๋น์ฆ๋์ค ๋ก์ง๋ค์ด ํ๋ก ํธ์๋๋ก ๋์ด์จ ์์ ์์๋, ๋ชจ๋ ๊ฒ์ ์ปดํฌ๋ํธ์ ๋ด๋ ์ด๋ฐ ๊ตฌ์กฐ๋ ๋ง์ ๋ฌธ์ ๋ฅผ ์ผ์ผํจ๋ค.
๋ฆฌ์กํธ๋ ๋ทฐ๋ฅผ ๋ง๋ค๊ธฐ ์ํ ๋จ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
๋ฆฌ์กํธ์ ํต์ฌ์ ์ ์ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ๋ ์ฌ์ค์ ์ข ์ข ๊น๋จน๋ ๊ฒ ๊ฐ๋ค.
์ด๋ฌํ ๋งฅ๋ฝ์์, ๋ฆฌ์กํธ๋ ์น ๊ฐ๋ฐ์ ํน์ ํ ๋ฉด์ ์ง์คํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ๋ ์ ์ด ๋๋๋ฌ์ง๋ค. ์ฆ, UI ์ปดํฌ๋ํธ๋ฅผ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ฉฐ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ ๋ฐ์ ์ธ ๊ตฌ์กฐ๋ ๋์์ธ ์ธก๋ฉด์ ์์ด์ UI ์ปดํฌ๋ํธ๋ฅผ ์ฝ๊ฒ ๊ทธ๋ฆด ์ ์๊ฒ ํด ์ฃผ๊ธฐ ๋๋ฌธ์ ๋ฆฌ์กํธ๋ ์ฌ์ค์ ์์ฒญ๋ ์์ ๋ฅผ ์ ๊ณตํ๊ณ ์๋ ๊ฒ์ด๋ค.์ด๋ ๊ฒ ๋งํ๋ฉด ๊ต์ฅํ ๋จ์ํ๊ณ ์ฝ๊ฒ ๋ค๋ฆฐ๋ค. ๊ทธ๋ฌ๋ ๋๋ ๋ง์ ์ฌ๋๋ค์ด ๋ฐ์ดํฐ ํ์นญ๊ณผ ๋ก์ง๋ค์ ๊ทธ ๋ฐ์ดํฐ๊ฐ ์ฌ์ฉ๋๋ ๊ณณ์ ๋ฐ๋ก ๊ด๋ จ ์ฝ๋๋ฅผ ์์ฑํ ๊ฒฝ์ฐ๋ฅผ ๋ง์ด ๋ณด์๋ค. ์๋ฅผ ๋ค์ด, ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ์์์ ๋ฐ์ดํฐ๋ฅผ ํ์นญํ ๋, ์๋์ ๊ฐ์ด ๋ ๋๋ง ์ง์ ์ useEffect๋ฅผ ์ฌ์ฉํด ๋ฐ์ดํฐ๋ฅผ ํ์นญํด์ค๋ ๊ฒฝ์ฐ๋ฅผ ๋ง์ด ๋ณด์๋ค.
useEffect(() => { fetch("https://address.service/api") .then((res) => res.json()) .then((data) => { const addresses = data.map((item)=>({ street: item.streetName, address: item.streetAddress, postcode: item.postCode, })) }) }, []) // the actual rendering...
์ด๋ฐ ์์ผ๋ก ์์ฑํ๋ ๊ฒ์ด ํ๋ก ํธ์๋ ์ํ๊ณ์์ ์์ง ์ ๋ฐ์ ์ผ๋ก ํต์ฉ๋๋ ์คํ ๋ค๋๊ฐ ์์ด์ ๊ทธ๋ฐ๊ฑธ ์๋ ์๊ณ , ๊ทธ๋ฅ ์ ์ข์ ํ๋ก๊ทธ๋๋ฐ ์ต๊ด์ผ ์๋ ์๋ค. ๊ทธ๋ฌ๋ ํ๋ก ํธ์๋ ์ดํ๋ฆฌ์ผ์ด์ ๋ค๋ ๋ค๋ฅธ ์ผ๋ฐ์ ์ธ ์ํํธ์จ์ด ์ดํ๋ฆฌ์ผ์ด์ ๊ณผ ๊ฐ์ ๋ฐฉํฅ์ผ๋ก ์๊ฐํด์ผ ํ๋ค. ํ๋ก ํธ์๋ ์ํ๊ณ์์, ์ฝ๋์ ๊ตฌ์กฐ๋ฅผ ์ ๋ฆฌํ๊ธฐ ์ํด '๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ' ๊ฐ๋ ์ ์ฌ์ฉํด์ผ ํ๋ฉฐ, ๋ค๋ฅธ ์ ์ฉํ ๋์์ธ ํจํด๋ค๋ ์ ์ฉ๋์ด์ผ ํ๋ค.
Welcome to the real world React application
๋๋ถ๋ถ์ ๊ฐ๋ฐ์๋ค์ด UI๋ฅผ ํ๋์ ๋ ๋ฆฝ๋ ํจ์๋ก ๋ง๋ค์ด data๋ฅผ DOM์ ๋ฌถ๋ ๋ฐฉ์์ผ๋ก ๊ฐ๋จํ๊ฒ ๋ง๋ค์ ์๋ค๋ ๋ฐฉ์์ ๊ฐํํ์๋ค. ์ด๋ค ์ ๋๊น์ง๋ ์ด ๋ง์ด ๋ง๋ค.
๊ทธ๋ฌ๋ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ๋ฐฑ์๋์์ ๋คํธ์ํฌ ํธ์ถ์ ํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฅด๊ฑฐ๋, ํ์ด์ง ๋ค๋น๊ฒ์ด์ ์ ํด์ผํ ๋ ๊ณค๋ํจ์ ๊ฒช๊ฒ ๋๋๋ฐ, ์ฌ์ด๋ ์ดํํธ๊ฐ ์ปดํฌ๋ํธ๋ฅผ
๋ ์์
ํ๊ฒ ๋ง๋ค๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ๋ฆฌ๊ณ ์ด๋ฐ ์ํ๊ฐ๋ค์ ๋ํด์ ์๊ฐ์ ํ๋ ์๊ฐ, ๋ง์ ๊ฒ๋ค์ด ๋ณต์กํด์ง๊ณ UI์ ์ด๋์ด ๋ฉด์ด ๋ฑ์ฅํ๊ฒ ๋๋ค.Apart from the user interface
๋ฆฌ์กํธ๋ ์ ์ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋งํผ, ์์ํ ๊ณ์ฐ์ด๋ ๋น์ฆ๋์ค ๋ก์ง์ ์ด๋์ ๋๋๋์ ํฐ ๊ด์ฌ์ด ์๋ค. ๊ทธ๋ฌ๋ ๊ทธ ๋ทฐ๋จ์ ๋์ด์, ํ๋ก ํธ์๋ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ค๋ฅธ ์์ญ๋ค๋ ๋ง์ด ์๋ค. ์ดํ๋ฆฌ์ผ์ด์ ์ด ๋์ํ๋๋ก ํ๋ ค๋ฉด, ๋ผ์ฐํฐ, ๋ก์ปฌ์คํ ๋ฆฌ์ง, ๋ค๋ฅธ ๋ ๋ฒจ์ ์บ์ฑ๋ค, ๋คํธ์ํฌ ์์ฒญ, ์จ๋ํํฐํตํฉ, ์จ๋ํํฐ๋ก๊ทธ์ธ, ๋ณด์, ๋ก๋ฐ ํผํฌ๋จผ์ค ํ๋ ๋ฑ ์ ๊ฒฝ์จ์ผ ํ ๊ฒ ๋ง์ด ์๋ค.
์ด๋ ๋ฏ, ๋ง์ ๊ฒ๋ค์ ์ฒ๋ฆฌํด์ผํ๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ๊ฒ์ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๋ ํ ์ค ์์ ๋ผ์ ๋ฃ๋ ๊ฒ์ ์ข์ ์๊ฐ์ด ์๋๋ค. ์ฌ๋ฌ๊ฐ์ง ์ปจ์ ์ ํ ๊ณณ์ ๋๋ ๊ฒ์ ๋๋ถ๋ถ ๋ ๋ง์ ํผ๋์ ๊ฐ์ ธ๋ค์ฃผ๊ธฐ ๋๋ฌธ์ด๋ค.
๋ง์ฝ ์ฐ๋ฆฌ๊ฐ ๊ตฌ์กฐํ๋ ํด๋๋ ํ์ผ๋ก ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํ ์ ์๋ค๋ฉด, ์ดํ๋ฆฌ์ผ์ด์ ์ ์ดํดํ๊ธฐ ์ํ ์ ์ ์ ๋ถํ๋ฅผ ํจ์ฌ ๋ ๋ง์ด ์ค์ผ ์ ์์ ๊ฒ์ด๋ค. ๊ทธ๋ฆฌ๊ณ ํ ๋ฒ์ ํ ๊ฐ์ง์๋ง ์ง์คํ ์ ์๊ฒ ๋ ๊ฒ์ด๋ค. ๋คํํ๋ ์ ์๋ํ๊ธฐ๋ก ์ฆ๋ช ๋ ํ๋ก๊ทธ๋๋ฐ ํจํด๋ค์ด ์กด์ฌํ๋ค.
๋ฆฌ์กํธ ์ดํ๋ฆฌ์ผ์ด์ ์ ์งํ
์๊ฑฐ๋ ๋จ์ํ ํ๋ก์ ํธ์์๋ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ์์ ๋ชจ๋ ๋ก์ง์ด ๋ค์ด์๋ ํํ๋ฅผ ๋ณด์์ ๊ฒ์ด๋ค. ์ ์ฒด์ ์ผ๋ก ์ปดํฌ๋ํธ๋ค๋ ๋ช ๊ฐ ์์์ ๊ฒ์ด๋ค. ํ์ด์ง๋ฅผ
๋์
์ผ๋ก ๋ง๋ค๊ธฐ ์ํ ๋ช๋ช์ ๋ณ์๋ ์คํ ์ดํธ๋ฅผ ์ ์ธํ๊ณ ๋ ์ ๋ฐ์ ์ผ๋ก HTML ์ฒ๋ผ ๋ณด์์ ๊ฒ์ด๋ค. ๋ฐ์ดํฐ ํ์นญ ๋ก์ง์ด ์ปดํฌ๋ํธ์ useEffect์์ ๋ค์ด์๋ ๊ฒ์ ๋ง์ด ๋ณด์์ ๊ฒ์ด๋ค.๊ทธ๋ฌ๋ ์ดํ๋ฆฌ์ผ์ด์ ์ด ์ปค์ง์๋ก ๋ ๋ง์ ๊ฒ๋ค์ด ์ฝ๋์ ๋ํด์ง๊ธฐ ์์ํ๋ค. ์ด๋ฅผ ์ ์ ๋ฆฌํ์ง ์์ผ๋ฉด ์ ์ ์ฝ๋๋ ์ ์ง๋ณด์๊ฐ ๋ถ๊ฐ๋ฅํ ์ํ๋ก ๋ณํด๋ฒ๋ฆฌ๊ณ , ์์ ๊ธฐ๋ฅ ํ๋๋ฅผ ๋ง๋๋ ๋ฐ์๋ ์๊ฐ์ด ๋ง์ด ๋ค๊ณ ์ฝ๊ธฐ์๋ ์ด๋ ค์ด ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ๋๋ค๋ ๋ป์ด๋ค.
๊ทธ๋์ ์ง์๊ฐ๋ฅํ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ์ ์๋ ๋ช ๊ฐ์ง ๋จ๊ณ๋ค์ ๋ฆฌ์คํธํ ํ์ฌ ๋งํ๊ณ ์ ํ๋ค. ์ ๋ฐ์ ์ผ๋ก ๋ ธ๋ ฅ์ด ๋ ํ์ํ๋, ์ดํ๋ฆฌ์ผ์ด์ ๋ด์ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์๋ ๊ฒ์ด ๋งค์ฐ ๋์์ด ๋ ๊ฒ์ด๋ค. ์ด๋ป๊ฒ ํ๋ฉด ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง ํ๋ก ํธ์๋ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ์ ์๋์ง ํ์ํ ๋จ๊ณ๋ค์ ๊ฐ๋จํ๊ฒ ํ๋ฒ ์ดํด๋ณด๋๋ก ํ์.
Single Component Application
<Component> Network requests State management Domain loginc Render </Component>
์ด๋ฐ ํํ๋ก ํ ์ปดํฌ๋ํธ๋ก ๋ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ ๋ค๋ฉด, ๊ณง ํ ์ปดํฌ๋ํธ ์์์ ์ฝ์ด์ผ ํ๋ ๊ฒ์ด ์์ฒญ ๋ง์์ง๋ ๊ฒ์ ์ ์ ์์ ๊ฒ์ด๋ค.
Multiple Component Application
<Component> Network requests State management Domain logic </Component> <Component> State Render </Component> <Component> Render </Component>
์ด๋ฐ ๋ฐฉ์์ผ๋ก ํ๋ฒ์ ํ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ ์๋ ์๊ฒ ์ผ๋, UI ์ ๊ด๋ จ์๋ ์ฝ๋๋ค์ ์ปดํฌ๋ํธ ์์ ๋ฃ์ด ๋๋๊ฒ ๋ญ๊ฐ ๋ง์ง ์๋ค๊ณ ๋๋ ๊ฒ์ด๋ค. ๋ํ, ์ด๋ค ์ปดํฌ๋ํธ๋ค์ ๋๋ฌด ๋ง์ ๋ด๋ถ ๋ณ์๋ฅผ ๊ฐ๊ฒ ๋๋ค.
State management with hooks
๊ทธ๋์ ์ด๋ฐ UI ์ ๊ด๋ จ์๋ ๋ก์ง๋ค์ ๋ณ๋์ ๊ณต๊ฐ์ ๋๋๊ฒ ์ข๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค ๊ฒ์ด๋ค. ๋คํํ๋ ๋ฆฌ์กํธ์์ ์ฌ์ฉ์๋ ์ง์ ํ ์ ๋ง๋ค ์ ์๋ค.
<Component> useHook State -----> Network request Render Domain logic </Component> | \ | \ <Component>. <Component> Render Render </Component> </Component>
์ ์ด์ ๋ชจ๋ ๊ฑธ ๋ค ๋๋ ค๋ฃ์ ์ฑ๊ธ์ปดํฌ๋ํธ์์ ๋ง์ ๊ฒ๋ค์ ์ถ์ถํด ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ช๋ช ๊ฐ์ ๋ ๋๋ง์ ์ํ ์์ํ ์ปดํฌ๋ํธ๋ค๊ณผ ์คํ ์ดํธ๋ฅผ ๊ฐ์ง๊ณ ์๋ ์ฌ์ฌ์ฉ๊ฐ๋ฅํ ํ ๋ค์ ๋ง๋ค์ด๋๋ค. ๊ทธ๋ฌ๋ ํ ์ค์ ๋ํ ๋ฌธ์ ์ ์, ์ฌ์ด๋ ์ดํํธ์ ์ํ๊ด๋ฆฌ์๋ ๋ณ๊ฐ๋ก, ์ด๋ค ๋ก์ง๋ค์ ์ํ์ ์๊ด์๋ ์์ํ ๊ณ์ฐ์ฒ๋ผ ๋ณด์ธ๋ค๋ ๊ฒ์ด๋ค.
Business models emerged
๋ฐ๋ผ์ ์ด๋ฌํ ์ํ์ ๊ด๋ จ์๋ ๋ก์ง๋ค์ ๋ฐ๋ก ์ ๋ฆฌํ๋ฉด ์ข๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค๊ฒ ๋๋ค. ์๋ฅผ ๋ค์ด, ์ด๋ฐ ๋ก์ง๋ค์ ๋ผ์ด๋์ผ๋ฉด ๋ก์ง์ด ๋ค์ํ ๋ทฐ์ ๊ฒฐํฉํ ์ ์๊ฒ ๋ ๊ฒ์ด๋ค.
๋ฐ๋ผ์ presentational/container component ํจํด๊ณผ ๊ฐ์ ๋ค๋ฅธ ๊ณณ์ ์ฌ์ฉ๋ ๋์์ธ ํจํด๋ค์ ๊ฐ์ ธ๋ค๊ฐ ์ฌ์ฉํ ์ ์๊ฒ ๋๋ค.
์ฐธ๊ณ .
https://patterns-dev-kr.github.io/design-patterns/container-presentational-pattern/์์ฝํ์๋ฉด, presentational ์ปดํฌ๋ํธ๋ ๋ฐ์ดํฐ๋ฅผ prop์ผ๋ก ๋ฐ์ ๋ ๋๋ง๋ง ๋ด๋นํ๋ ์ปดํฌ๋ํธ์ด๊ณ , container ์ปดํฌ๋ํธ๋ ์ฌ์ฉํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ก์ง์ ๊ฐ์ง๊ณ ์๋ ์ปดํฌ๋ํธ๋ฅผ ์๋ฏธํ๋ค. ๊ทธ๋ฌ๋ ํ ์ค์ ๋ฑ์ฅ์ผ๋ก ์ด์ container ์ปดํฌ๋ํธ๋ ๊ตณ์ด ์ฌ์ฉํ ํ์๊ฐ ์์ด์ง๊ณ , presentational ์ปดํฌ๋ํธ์์ ํ ์ค๋ฅผ ์ด์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ ๋ฐ๋ก ๋ ๋๋ง ํ ์ ์๊ฒ ๋์๋ค.
Layered frontend application
์ดํ๋ฆฌ์ผ์ด์ ์ด ์ ์ ๋ ๋ณต์กํด์ง๋ฉด ์ด๋ค ํจํด์ด ํ์ํด์ง ๊ฒ์ด๋ค. ๊ทธ๋ฆฌ๊ณ ๋ค์ํ ๋ ์ด์ด๋ก ๋ถ๋ฆฌํ์ฌ ํ๋ฒ์ ํ๊ฐ์ง๋ง ์ง์คํ ์ ์๋๋ก ๋ง๋ค๊ณ ์์ด์ง ๊ฒ์ด๋ค. ์ด ๋ ์ฌ์ฉํ ์ ์๋ ๊ฒ์ด
Presentation Domain Data Layering
์ด๋ค.https://martinfowler.com/bliki/PresentationDomainDataLayering.html
presentation(UI), domain logic(business logic layer that contains validations and calculations) , data access(sorts out how to manage persistent data in a database or remote services)
์ด์ ์ด๋ป๊ฒ ํ๋ฉด ์ด๋ฌํ ๊ตฌ์กฐ๋ฅผ ํ๋ก ํธ์๋ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ ์ฉํ ์ ์๋์ง ์์๋ณด๋๋ก ํ์.
Introduction of Payment feature
์จ๋ผ์ธ ์ผํ๋ชฐ์ ์์ ๋ก ๋คํ ๋ฐ, ๊ณ ๊ฐ์ด ์ฃผ๋ฌธ์์ ๋ฌผํ์ ์ถ๊ฐํ๊ณ , ๊ฒฐ์ ๋ฐฉ์์ค ํ๋๋ฅผ ์ ํํด ๊ฒฐ์ ํ๋ ๊ฒฝ์ฐ๋ฅผ ์๋ก ๋ค์ด๋ณด๋ ค๊ณ ํ๋ค.
์ด ๊ฒฐ์ ๋ฐฉ์ ์ต์ ๋ค์ ์๋ฒ์ฌ์ด๋์์ ๋ฐ์ ๊ณ ๊ฐ์ ๋๋ผ์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ๋ณด์ฌ์ฃผ๋ ค๊ณ ํ๋ค. ์ ์ด๋ฅผ ํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ์์ฑํ๋ค๊ณ ํด๋ณด์.
import { useEffect, useState } from 'react'; export const Payment = ({amount}: {amount: number}) => { const [paymentMethods, setPaymentMethods] = useState<LocalPaymentMethod[]>([]) useEffect(() => { const fetchPaymentMethods = async() => { const url = "https://online-ordering.com/api/payment-methods"; const response = await fetch(url); const methods: RemotePaymentMethod[] = await response.json(); if(methods.length> 0){ const extended: LocalPaymentMethod[] = methods.map(method => ({ provider: method.name, label : `Pay with ${method.name}` })); extended.push({provider: "cash", label : "Pay in cash"}); }else{ setPaymentMethods([]) } }; fetchPaymentMethods(); }, []) return( <div> <h3>Payment</h3> <div> {paymentMethods.map(method => ( <label key={method.provider}> <input type="radio" name="payment" value={method.provider} defaultChecked={method.provider === 'cash'} /> <span>{method.label}</span> </label> ))} </div> <button>{amount}</button> </div> )}
์, ์ด๋ฐ ๋ฐฉ์์ผ๋ก ๋ ์ ํ์ ์ธ ์ปดํฌ๋ํธ๋ฅผ ์๋ง ๋ง์ด ๋ดค์ ๊ฒ์ด๋ค. ๋์์ง๋ ์์ง๋ง, ์์์ ์ธ๊ธํ๋ฏ์ด ์ด ์์ ๋๋ฌด ๋ง์ ๊ด์ฌ์ฌ๊ฐ ๋ค์ด์์ด์ ์ฝ๊ธฐ๊ฐ ์กฐ๊ธ ์ด๋ ต๋ค.
the problem with the initial implementation
์ฒซ ๋ฒ์งธ๋ก ๋ค๋ฃฐ ๋ฌธ์ ๋, ์ปดํฌ๋ํธ๊ฐ ์ ๋ง ๋ฐ์๋ค๋ ๊ฒ์ด๋ค.
๋ง์ฝ ์ด ์ปดํฌ๋ํธ๋ฅผ ๋ณ๊ฒฝํ๋ ค๋ฉด
๋คํธ์ํฌ ์์ฒญ์ ์ด๋ป๊ฒ ์์ํด์ผํ๋์ง
,์ด๋ป๊ฒ ํ๋ฉด ๋ฐ์ดํธ๋ฅผ ์ปดํฌ๋ํธ๊ฐ ์ดํดํ ์ ์๋ ํํ๋ก ๊ฐ๊ณตํ ์ ์๋์ง
,๊ทธ๋ฆฌ๊ณ ๋ ๋๋ง์ ์ด๋ค ๋ฐฉ์์ผ๋ก ์ผ์ด๋๋์ง
๋ฅผ ๋ชจ๋ ๋ค ์ดํดํด์ผ ํ๋ค.์ฒซ ๋ฒ์งธ๋ก ํ ์ ์๋ ๊ฒ์ view์ non-view๋ฅผ ๋ถ๋ฆฌํ๋ ๊ฒ์ด๋ค. ๊ทธ ์ด์ ๋ ์ ๋ฐ์ ์ผ๋ก ๋ทฐ ๋ก์ง์ non-๋ทฐ ๋ก์ง๋ณด๋ค ํจ์ฌ ๋ ์์ฃผ ๋ณ๊ฒฝ๋๊ธฐ ๋๋ฌธ์ด๋ค.
The split of view and non-view code
๋ฆฌ์กํธ์์๋ ํ ์ค๋ฅผ ์ฌ์ฉํ์ฌ state๋ฅผ ๊ด๋ฆฌํ๊ฒ ํ๊ณ , ์ปดํฌ๋ํธ ์์ฒด๋ฅผ ์กฐ๊ธ ๋ statelessํ๊ฒ ๋ง๋ค ์ ์๋ค.
ํจ์๋ฅผ ์ถ์ถ
ํ์ฌ ํ ์ค๋ฅผ ๋ง๋ค์ด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.import { useEffect, useState } from 'react'; const usePaymentMethods = () => { const [paymentMethods, setPaymentMethods] = useState<LocalPaymentMethod[]>([]) useEffect(() => { const fetchPaymentMethods = async() => { const url = "https://online-ordering.com/api/payment-methods"; const response = await fetch(url); const methods: RemotePaymentMethod[] = await response.json(); if(methods.length> 0){ const extended: LocalPaymentMethod[] = methods.map(method => ({ provider: method.name, label : `Pay with ${method.name}` })); extended.push({provider: "cash", label : "Pay in cash"}); }else{ setPaymentMethods([]) } }; fetchPaymentMethods(); }, []) return paymentMethods; } export default usePaymentMethods;
import usePaymentMethods from './usePaymentMethods'; export const Payment = ({amount}: {amount: number}) => { const {paymentMethods} = usePaymentMethods(); return( <div> <h3>Payment</h3> <div> {paymentMethods.map(method => ( <label key={method.provider}> <input type="radio" name="payment" value={method.provider} defaultChecked={method.provider === 'cash'} /> <span>{method.label}</span> </label> ))} </div> <button>{amount}</button> </div> )}
์ ์ด๋ ๊ฒ view์ non-view๋ก ๋๋์ด๋ณด์๋ค. ๊ทธ๋ฐ๋ฐ paymentMethods๊ฐ ๋ฐ์ดํฐ ๋ฐฐ์ด์ ์ํํ๋ฉฐ ํ๋ ์ผ์ ์ดํด๋ณด๋ฉด, ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ๊ฐ ๋ ํ์ํ๋ค๋ ๊ฒ์ ์๊ฒ๋ ๊ฒ์ด๋ค.
Split the view by extracting sub component
๊ทธ๋ฆฌ๊ณ ๋ง์ฝ ์ฐ๋ฆฌ๊ฐ ์์ํจ์์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ์ ์๋ค๋ฉด, ์ฐ๋ฆฌ๊ฐ ๋ ์ฝ๊ฒ ์ฝ๋๋ฅผ ์ดํดํ ์ ์๊ณ ์ฌ์ฌ์ฉํ๊ธฐ๊ฐ ์ฌ์์ง ๊ฒ์ด๋ค. ๊ฒฐ๊ตญ, ์ปดํฌ๋ํธ๊ฐ ์์ผ๋ฉด ์์์๋ก ์ฌ์ฌ์ฉ๊ฐ๋ฅ์ฑ์ด ๋ ๋์์ง๋ค.
์, ๊ทธ๋ผ ๋ค์ํ๋ฒ ๋ ํจ์๋ฅผ ์ถ์ถํด๋ณด๋๋ก ํ์.
const PaymentMethods = ({ paymentMethods, }: LocalPaymentMethod[]) => ( <> {paymentMethods.map(method => ( <label key={method.provider}> <input type="radio" name="payment" value={method.provider} defaultChecked={method.provider === 'cash'} /> <span>{method.label}</span> </label> ))} </> )
export const Payment = ({amount}: {amount: number}) => { const {paymentMethods} = usePaymentMethods(); return( <div> <h3>Payment</h3> <PaymentMethods paymentMethods={paymentMethods} /> <button>{amount}</button> </div> )};
PaymentMethodsํจ์ ์ปดํฌ๋ํธ๋ ์ด์ ์ด๋คํ ์ํ๋ ๊ฐ์ง๊ณ ์์ง ์์ ์์ํ ์ปดํฌ๋ํธ๊ฐ ๋์๋ค.
Data modeling to encapsulate logic
์ด๋ ๊ฒ ๋ทฐ์ ๋ทฐ๊ฐ ์๋ ๊ฒ์ผ๋ก ๋๋์ด ๋ณด์๋ค. ๊ทธ๋ฌ๋ ์์ธํ ๋ณด๋ฉด ๋ ๊ฐ์ ์ ์ฌ์ง๊ฐ ์๋ ๊ฒ์ ์ ์ ์๋ค. ์๋ฅผ ๋ค์ด PaymentMethods ์ปดํฌ๋ํธ๋ฅผ ๋ณด๋ฉด default๋ก ์ฒดํฌ๋์ด ์์ด์ผํ ์ง ์ฌ๋ถ์ ๋ํ ๋ก์ง์ด ์๋ ๊ฒ์ ์ ์ ์๋ค. ์ด๋ ๋ฐ์ดํฐ ๋์๊ฐ ๋ฐ์ํ ์ ์๋ ๊ฐ๋ฅ์ฑ์ ์ง๋๊ณ ์๋ ๊ฒ์ด๋ค.
๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ๋ณํํ๋ ๊ณผ์ ์์๋ ๋์๊ฐ ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ ๊ฐ์ง๊ณ ์๋ค.
์ด๋ฐ ์ํฉ์ ์ฐ๋ฆฌ๋ data์ ์ด์ ๋ฐ๋ฅธ ํ๋๋ณํ๋ฅผ ํ ๊ณณ์ ์ง์คํด ๋ class PaymentMethod๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ ์ ์๋ค.
class PaymentMethod{ private remotePaymentMethod:remotePaymentMethod; constructor(remotePaymentMethod:remotePaymentMethod) { this.remotePaymentMethod = remotePaymentMethod; } get provider(){ return this.remotePaymentMethod.name; } get label(){ if(this.provider === 'cash'){ return `Pay in ${this.provider}` } return `Pay with ${this.provider}` } get isDefaultMethod(){ return this.provider === 'cash'; } }
์ด ํด๋์ค๋ฅผ ํ์ฉํ์ฌ default cash payment method๋ฅผ ๋ง๋ค ์ ์๋ค.
const payInCash = new PaymentMethod({name:"cash"})
๋ํ, ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ ๊ฐ๊ณตํ๋ ๊ณผ์ ์์ ์ด ํด๋์ค๋ก ๋ง๋ PaymentMethod ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ ์ด๋ฅผ
convertPaymentMethods
๋ผ๋ ํจ์๋ก ๋ง๋ค ์๋ ์๋ค.๊ธฐ์กด์ usePaymentMethods๋ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝ๋ ์ ์๊ฒ ๋ค.
const payInCash = new PaymentMethod({name:'cash'}) const convertPaymentMethods = (methods) => { if(methods.length === 0 ){ return []; } const extended = methods.map( method => new PaymentMethod(method) ); extended.push(payInCash); return extended; } ///// import { useEffect, useState } from 'react'; const usePaymentMethods = () => { const [paymentMethods, setPaymentMethods] = useState<LocalPaymentMethod[]>([]) useEffect(() => { const fetchPaymentMethods = async() => { const url = "https://online-ordering.com/api/payment-methods"; const response = await fetch(url); const methods: RemotePaymentMethod[] = await response.json(); setPaymentMethods(convertPaymentMethods(methods)); }; fetchPaymentMethods(); }, []) return paymentMethods; } export default usePaymentMethods;
๊ทธ๋ฆฌ๊ณ ๋ํ PaymentMethods๋ฅผ ๋ ๋๋ง ํ๋ ๊ณผ์ ์์
method.provider === 'cash
๋ฅผ ์ฌ์ฉํ๋ ๋์ getter๋ฅผ ์ง์ ๋ถ๋ฅผ ์๋ ์๊ฒ ๋ค.... const PaymentMethods = ({ ...}) => ( <> {paymentMethods.map(method => ( <label key={method.provider}> <input type="radio" name="payment" value={method.provider} defaultChecked={method.isDefaultMethod} /> // ์ด๋ ๊ฒ! <span>{method.label}</span> </label> ))} </> ) ...
์ ์ด์ ์ฐ๋ฆฌ๋ Payment ์ปดํฌ๋ํธ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ ๋จ์๋ก ์ชผ๊ฐฐ๋ค.
์๋ก์ด ๊ตฌ์กฐ์ ์ฅ์
- ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ UI์ ๊ด๋ จ์ด ์๋ ์ธํฌ๋ฉ์ด์ ์ ์ ์ธํ ๊ฒ๋ค์ ๋ด์๋ค. ๋ฐ๋ผ์ ๋ก์ง ๋ณ๊ฒฝ์ด ๋์์ ๋์ ์ด ๋ก์ง์ด ๋ทฐ ์์ ์๋ ๊ฒ๋ณด๋ค ํจ์ฌ ์ฝ๊ฒ ๋ณ๊ฒฝํ ์ ์๋ค.
- ์๋ก์ด PaymentMethods ์ปดํฌ๋ํธ๋ ์์ ํจ์๋ก์ ๋๋ฉ์ธ์์ ๋ฐ์ ๋ฐ์ดํฐ์๋ง ์์กดํ๊ณ ์๊ธฐ ๋๋ฌธ์ ํ ์คํธํ๊ธฐ๋ ์ฝ๊ณ , ์ฌ์ฌ์ฉํ๊ธฐ๋ ์ฝ๋ค.
- ์ฝ๋๋ค์ด ๊ฐ๊ฐ ๋ช ํํ ์๊ธฐ ๊ธฐ๋ฅ์ ํ๊ณ ์๋ค. ์๋ก์ด ์๊ตฌ์ฌํญ์ด ์ค๋๋ผ๋ ์ ๋นํ ์์น์ ๊ฐ์ ์ฝ๋๋ฅผ ๋ค ์ฝ์ง ์๋๋ผ๋ ์ฝ๊ฒ ๋ณ๊ฒฝํ ์ ์๋ค.
์ ๊ทธ๋ฌ๋ฉด, ์๋ก์ด ๋ณ๊ฒฝ์ฌํญ์ด ์๊ฒผ์ ๋ ์ด๋ป๊ฒ ์ฝ๊ฒ ๋์ํ ์ ์๋์ง ํ๋ฒ ์ดํด๋ณด์.
New Requirement: donate to charity
์ด์ ์ฌ๊ธฐ์ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํด์ผ ํ๋ค๊ณ ํด๋ณด์.
์ฌ์ฉ์์ ์ด๊ธ์ก์ ๋น๋กํ์ฌ ์ผ์ ๊ธ์ก์ ๊ธฐ๋ถํ๋๋ก ํ๋ ๋ถ๋ถ์ด ์๊ธฐ๋ฉฐ, ๋ฒํผ์๋ '์ต์ข ๊ธ์ก(์ด๊ธ์ก - ๊ธฐ๋ถ๊ธ์ก)'์ด ํ์๋๋๋ก ํด์ผํ๋ค๊ณ ํด๋ณด์.
ํ์ฌ ํด๋๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ด ๋์ด์๋ค.
src โโโ App.tsx โโโ components โ โโโ Payment.tsx โ โโโ PaymentMethods.tsx โโโ hooks โ โโโ usePaymentMethods.ts โโโ models โ โโโ PaymentMethod.ts โโโ types.ts
Internal state: agree to donation
Payment์ ๋ํด ๋ณ๊ฒฝ์ ํ๊ธฐ ์ํด์, ์ฆ ์ ์ ์ ์ฒดํฌ๋ฐ์ค ์ฒดํฌ์ฌ๋ถ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํด ์ฐ๋ฆฌ๋ ์๋ก์ด ๋ด๋ถ state๋ฅผ ํ๋ ๋ง๋ค์ด์ผ ํ๋ค.
export const Payment = ({amount}: {amount: number}) => { const [agreeToDonate, setAgreeToDonate] = useState<boolean>(false); const {total, tip} = useMemo(() => ( { total : agreeToDonate? Math.floor(amount+1) : amount, tip : parseFloat((Math.floor(amount+1)-amount).toPrecision(10)) } ), [amount, agreeToDonate]) const {paymentMethods} = usePaymentMethods(); return( <div> <h3>Payment</h3> <PaymentMethods paymentMethods={paymentMethods} /> <div> <label> <input type='checkbox' onChange={handleChange} checked= {agreeToDonate}/> <p> {agreeToDonate? "Thanks for your donation" : `I would like to donate ${tip} to charity.`} </p> </label> </div> <button>{total}</button> </div> )};
์ด๋ ๊ฒ ๋ณํ๋ฅผ ๋ฐ์ํจ์ผ๋ก์จ ์ฐ๋ฆฌ๋ ๋๋ค์ ์ปดํฌ๋ํธ๋ฅผ ๋๋ฝํ๊ณ ๋ง์ ๊ฒ๋ค์ ํ๊ฒ ๋์๋ค. ์ ๊ทธ๋ฌ๋ฉด ๋ ์ด๊ฑธ ์ด๋ป๊ฒ ๋ทฐ์ non-๋ทฐ ์ฝ๋๋ก ๋ฐ๊ฟ ์ ์๋์ง ์ดํด๋ณผ ํ์ด๋ฐ์ด ๋ ๊ฒ์ด๋ค.
Extract a hook to the rescue
๊ทธ๋ฌ๋ฉด ๋ค์ ํ๋ฒ ๋ค์๊ณผ ๊ฐ์ด hooks๋ก state๊ด๋ จ ๋ก์ง์ ๋ถ๋ฆฌํ ์ ์๋ค.
export const useRoundUp = (amount:number) => { const [agreeToDonate, setAgreeToDonate] = useState<boolean>(false); const {total, tip} = useMemo(() => ( { total : agreeToDonate? Math.floor(amount+1) : amount, tip : parseFloat((Math.floor(amount+1)-amount).toPrecision(10)) } ), [amount, agreeToDonate]) const updateAgreeToDonate = () => { setAgreeToDonate(prev => !prev) } return{ total, tip, agreeToDonate, updateAgreeToDonate, } }
๊ทธ๋ฆฌ๊ณ Payment๋ ๋ค์๊ณผ ๊ฐ์ด ๋ค์ ๊น๋ํด์ง ๊ฒ์ด๋ค.
export const Payment = ({amount}: {amount: number}) => { const {paymentMethods} = usePaymentMethods(); const {total, tip, agreeToDonate, updateAgreeToDonate} = useRoundUp(amount); const formatCheckboxLabel = (agreeToDonate:boolean, tip:number) => { return agreeToDonate? "Thanks for your donation" : `I would like to donate ${tip} to charity.` } return( <div> <h3>Payment</h3> <PaymentMethods paymentMethods={paymentMethods} /> <div> <label> <input type='checkbox' onChange={handleChange} checked={agreeToDonate}/> <p>{formatCheckboxLabel(agreeToDonate, tip)}</p> </label> </div> <button>{total}</button> </div> )};
๊ทธ๋ฆฌ๊ณ ๋์ ๋ค์ ํ๋ฒ ์ฒดํฌ๋ฐ์ค UI๋ ๋ถ๋ฆฌํ ์ ์๋ค.
export const DonationCheckbox = ({onChange, checked, content}) => { return( <div> <label> <input type='checkbox' onChange={onChange} checked={checked}/> <p>{content}</p> </label> </div> )}
๊ทธ๋ฆฌ๊ณ Payment๋ ๋ค์ ํ๋ฒ ๋ค์๊ณผ ๊ฐ์ด ๊น๋ํด์ง๋ค.
export const Payment = ({amount}: {amount: number}) => { const {paymentMethods} = usePaymentMethods(); const {total, tip, agreeToDonate, updateAgreeToDonate} = useRoundUp(amount); const formatCheckboxLabel = (agreeToDonate:boolean, tip:number) => { return agreeToDonate? "Thanks for your donation" : `I would like to donate ${tip} to charity.` } return( <div> <h3>Payment</h3> <PaymentMethods paymentMethods={paymentMethods} /> <DonationCheckbox onChange={updateAgreeToDonate} checked={agreeToDonate} content={formatCheckboxLabel(agreeToDonate, tip)}/> <button>{total}</button> </div> )};
๊ทธ๋ฌ๋ฉด ์ด ์์ ์์ ์ฐ๋ฆฌ์ ์ฝ๋ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์์ง๋ค. ๊ฐ๊ฐ์ ์ปดํฌ๋ํธ๊ฐ ํ๋์ ์ญํ ์ ์ ํด์ฃผ๊ณ ์๋ ํํ์ด๋ค.
More changes about round-up logic
๋ง์ฝ ๊ตญ๊ฐ๊ฐ ์ถ๊ฐ๋๋ฉด ๊ธฐ๋ถ๊ธ์ก์ ๊ณ์ฐํ๋ ๋ก์ง๋ ์ ๋ถ ๋ค ๋ฌ๋ผ์ ธ์ผ ํ ๊ฒ์ด๋ค. ์๋ฅผ ๋ค์ด ์ผ๋ณธ ์์ฅ์์ 0.1์์ ๋๋ฌด ์๋ค. ์ด๋ป๊ฒ ํ๋ฉด ๊ณ ์น๊ธฐ ์ฌ์ธ๊น?
countryCode
๋ง ์ถ๊ฐ๋กPayment
์ปดํฌ๋ํธ์ ๋ด๋ ค์ฃผ๋ฉด ๋๋ค.<Payment amount={3312} countryCode="JP"
๊ทธ๋ฆฌ๊ณ ๊ธฐ๋ถ๊ธ์ก์ ๊ณ์ฐํ๋ ๋ชจ๋ ๋ก์ง์ด useRoundUp์ ์๊ธฐ ๋๋ฌธ์, countryCode๋ฅผ ํด๋น ํ ์ ๋๊ฒจ์ค ์ ์๊ฒ ๋ค.
export const Payment = ({amount, countryCode}: {amount: number}) => { const {paymentMethods} = usePaymentMethods(); const {total, tip, agreeToDonate, updateAgreeToDonate} = useRoundUp(amount, countryCode); const formatCheckboxLabel = (agreeToDonate:boolean, tip:number) => { const currencySign = countryCode === "JP" ? "Y" : "$" return agreeToDonate? "Thanks for your donation" : `I would like to donate ${currencySign}${tip} to charity.`} ...
export const useRoundUp = (amount:number, countryCode:string) => { const [agreeToDonate, setAgreeToDonate] = useState<boolean>(false); const {total, tip} = useMemo(() => ( { total : agreeToDonate? countryCode === "JP" ? Math.floor(amount / 100 + 1) * 100 : Math.floor(amount+1) : amount, tip : parseFloat((Math.floor(amount+1)-amount).toPrecision(10)) } ), [amount, agreeToDonate, countryCode]) ... }
The shotgun surgery problem
์ด๋ฐ ์์ผ๋ก ๋ฒ๊ทธ๋ฅผ ์์ ํ๊ฑฐ๋ ํ๋์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ๋ ์ฌ๋ฌ ๊ฐ์ ๋ชจ๋์ ๋ณ๊ฒฝ์์ผ์ค์ผ ํ๋ ๋ฌธ์ ๋ฅผ "shotgun surgery"๋ผ๊ณ ๋ถ๋ฅธ๋ค.
์๋ฅผ ๋ค์ด, ๋ง์ฝ ์ด ์ํ์์ ๋ด๋งํฌ๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค๋ฉด ์๋์ ๊ฐ์ ์ฝ๋๋ฅผ ์ฌ๊ธฐ์ ๊ธฐ ๋ถ์ฌ์ผ ํ ๊ฒ์ด๋ค.
const currencySignMap = { JP: "¥", DK: "Kr.", AU: "$", }; const getCurrencySign = (countryCode: CountryCode) => currencySignMap[countryCode];
๊ทธ๋ผ ์ด ๋ฌธ์ ๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํด์ผ ํ ๊น Extract Class์ Replace Conditional with Polymorphism (์กฐ๊ฑด๋ฌธ์ ๋คํ์ฑ์ผ๋ก ๋ฐ๊พธ๊ธฐ)๋ฅผ ํตํด ํด๊ฒฐํ ์ ์์ ๊ฒ์ด๋ค.
export interface PaymentStrategy{ getRoundUpAmount(amount:number) : number; getTip(amount:number): number; } export class PaymentStrategyAU implements PaymentStrategy{ get currencySign(): string{ return '$'; } getRoundUpAmount(amount: number): number { return Math.floor(amount+1); } getTip(amount: number): number { return parseFloat((this.getRoundUpAmount(amount) - amount).toPrecision(10)) } }
์ด๋ ๊ฒ ๋ง๋ ํด๋์ค๋ฅผ ๊ฐ์ง๊ณ ๋๋ผ๋ง๋ค ์๋ธํด๋์ค๋ฅผ ๋ง๋ค ์๋ ์๊ฒ ์ง๋ง ์๋ฐ์คํฌ๋ฆฝํธ์์๋ ํจ์๊ฐ ์ผ๊ธ์๋ฏผ์ด๋๊น, ๊ทธ๋ฅ round-up ์๊ณ ๋ฆฌ์ฆ์ ๋๊ฒจ ์ฌ์ฉํ ์ ๋ ์๊ฒ ๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์๋ธํด๋์ค ๊ฐ์๋ ์ค์ด๋ค ๊ฒ์ด๋ค.
export class CountryPayment{ private readonly _currencySign : string; private readonly algorithm: RoundUpStrategy; public constructor(currencySign:string, roundUpAlgorithm:RoundUpStrategy) { this._currencySign=currencySign; this.algorithm = roundUpAlgorithm; } get currencySign(): string{ return this._currencySign; } getRoundUpAmount(amount:number): number{ return this.algorithm(amount); } getTip(amount:number):number{ return calculateTipFor(this.getRoundUpAmount.bind(this))(amount) } }
๊ทธ๋ฆฌ๊ณ ๋๋ฉด ์ด์ useRoundUp ํ ์ ๋ค์๊ณผ ๊ฐ์ด strategy๋ฅผ ์ด์ฉํด์ ๋ณ๊ฒฝํ ์ ์๋ค.
export const useRoundUp = (amount:number, strategy: PaymentStrategy) => { const [agreeToDonate, setAgreeToDonate] = useState<boolean>(false); const {total, tip} = useMemo(() => ( { total : agreeToDonate? strategy.getRoundUpAmount(amount) : amount, tip : strategy.getTip(amount), } ), [amount, agreeToDonate, strategy]) const updateAgreeToDonate = () => { setAgreeToDonate(prev => !prev) } return{ total, tip, agreeToDonate, updateAgreeToDonate, } }
๊ทธ๋ฆฌ๊ณ ๋ง๋ ํด๋์ค๋ฅผ ์ด์ฉํ์ฌ Payment์์๋ถํฐ countryCode ๋์ ์ด ํด๋์ค๋ฅผ ํ์ฉํ์ฌ strategy๋ฅผ ๋ด๋ ค์ฃผ๋๋ก ํ๋ค.
๊ทธ๋ผ ์ด์ countryCode์ ๋ฐ๋ผ์ ๋ฐ๊พธ์ง ์์๋ ์์ฝ๊ฒ ๋๋ผ๋ณ ์ ๋ณด๋ฅผ ์ถ๊ฐํ ์ ์๊ฒ ๋์๋ค.
export const Payment = ({amount, strategy = new PaymentStrategyAU("$", roundUpToNearestInteger)}: {amount: number, strategy?: PaymentStrategy}) => { const {paymentMethods} = usePaymentMethods(); const {total, tip, agreeToDonate, updateAgreeToDonate} = useRoundUp(amount, strategy); const formatCheckboxLabel = (agreeToDonate:boolean, tip:number, strategy: CountryPayment) => { return agreeToDonate? "Thanks for your donation" : `I would like to donate ${strategy.currencySign}${tip} to charity.` } return( <div> <h3>Payment</h3> <PaymentMethods paymentMethods={paymentMethods} /> <DonationCheckbox onChange={updateAgreeToDonate} checked={agreeToDonate} content={formatCheckboxLabel(agreeToDonate, tip, strategy)}/> <button>{total}</button> </div> )};
Push the design a bit further: extract a network client
'๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ'๋ผ๋ ๋ง์ธ๋์ ์ ๊ฐ์ง๊ณ ์๋ค๋ฉด, ์ด์ ๋ค์์ผ๋ก๋
usePaymentMethods
ํ ์ ์ ๋ฆฌํ๊ณ ์ถ์ด์ง ๊ฒ์ด๋ค. ์ง๊ธ ์ํ๋ก๋ ํฐ ๋ฌธ์ ๋ ์์ง๋ง, ์๋ฌ ํธ๋ค๋ง๊ณผ ์ฌ์๋ ๋ฑ ๋คํธ์ํฌ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ์ฝ๋ ๋ฑ์ ์ถ๊ฐํ๊ฒ ๋๋ฉด ์ฝ๋๊ฐ ์์ฒญ ๊ธธ์ด์ง ์ ์๋ค.๋ฐ๋ผ์ ๋คํธ์ํฌ ํ์นญํ๋ ํจ์๋ ๋ณ๋๋ก ๋นผ์ ์ฌ๋ฌ ๊ตฐ๋ฐ์์ ์ฌํ์ฉ ํ ์ ์๊ฒ ํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ๋ ๊ฐ๊ฐ ๋ค๋ฅด๊ฒ ํ๋๋ก ํ ์ ์๋ค. ๊ฒฐ๋ก ์ ์ผ๋ก ํด๋น ์ปดํฌ๋ํธ๋ ์๋์ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๊ฒ์ด๋ค.
const fetchPaymentMethods = async() => { const url = "https://online-ordering.com/api/payment-methods"; const response = await fetch(url); const methods: RemotePaymentMethod[] = await response.json(); }; const usePaymentMethods = () => { const [paymentMethods, setPaymentMethods] = useState<LocalPaymentMethod[]>([]) useEffect(() => { fetchPaymentMethods().then(methods => setPaymentMethods(convertPaymentMethods(methods)); }, []) return paymentMethods; } export default usePaymentMethods;