ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ๊ฒ€์ฆ๋œ 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;

    ๋Œ“๊ธ€

Designed by Tistory.