๐ŸŽ“ [ํ† ์Šค Frontend Fundamentals ๋ชจ์˜๊ณ ์‚ฌ 1ํšŒ ํ›„๊ธฐ] ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„

Goldยท2025๋…„ 11์›” 27์ผ

Toss Frontend Fundamentals

๋ชฉ๋ก ๋ณด๊ธฐ
1/2

ํ† ์Šค๊ฐ€ ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ์€ ๋ฌด์—‡์ผ๊นŒ?
ํ† ์Šค๊ฐ€ ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ์ด ์™œ ์ค‘์š”ํ• ๊นŒ?

์ง์ ‘ ๊ณผ์ œ๋ฅผ ํ’€๊ณ  ํ•ด์„ค ๋ผ์ด๋ธŒ๋ฅผ ๋ณด๋ฉด์„œ ํ™•์žฅ์„ฑ์„ ์œ„ํ•ด ํ† ์Šค๋Š” ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ•˜๋Š”์ง€ ์‚ดํŽด๋ณผ ์ˆ˜ ์žˆ๋Š” ๊ธฐํšŒ๋ฅผ ๊ฐ€์กŒ์Šต๋‹ˆ๋‹ค!

์•ˆ๋…•ํ•˜์„ธ์š”! ํ† ์Šค ๋ชจ์˜๊ณ ์‚ฌ 1ํšŒ์— ์ฐธ์—ฌํ•˜์—ฌ ํšŒ๊ณ  ๋ฐ ํ›„๊ธฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

์ฒ˜์Œ์— ์ฐธ์—ฌ ์‹ ์ฒญํ–ˆ์„ ๋•Œ ๊ต‰์žฅํžˆ ๊ธฐ์˜๊ธฐ๋„ ํ–ˆ์ง€๋งŒ ํ•œํŽธ ์˜์•„ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ญ ์™œ ๊ณ ๋ง™๊ฒŒ ์ด๋Ÿฐ ๊ฑธ ์—ด์–ด์ฃผ์ง€? ๋ฐ”์˜์‹คํ…๋ฐ..? ์—ญ์‹œ ํ† ์Šค๋Š” ์™ธ๋ถ€์™€ ์†Œํ†ต์„ ์ค‘์š”ํ•˜๊ฒŒ ์—ฌ๊ธฐ๋Š” ๊ตฌ๋‚˜! FE ๊ฐœ๋ฐœ ์ƒํƒœ๊ณ„์˜ ๋ฐœ์ „์„ ๋„๋ชจํ•˜๋Š”๊ตฌ๋‚˜! (๊ทธ์น˜๋งŒ ๋ชจ์˜๊ณ ์‚ฌ ..?)

์ด ์ˆจ๊ธธ ์ˆ˜ ์—†๋Š” ์˜๋ฌธ๋“ค์„ ์• ์จ ๋’ค๋กœํ•œ ์ฑ„ ์„ค๋ ˆ๋Š” ๋งˆ์Œ์œผ๋กœ ์ฃผ๋ง๋™์•ˆ ์—ด์‹ฌํžˆ ๊ณ ๋ฏผํ•˜์—ฌ ๊ณผ์ œ๋ฅผ ์ œ์ถœํ•˜๊ณ  ํ™”์š”์ผ์— ํ•ด์„ค ๋ผ์ด๋ธŒ๋ฅผ ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๋ผ์ด๋ธŒ ์‹œ์ž‘ํ•˜์ž๋งˆ์ž ์ด ์˜๋ฌธ์„ ํ•ด๊ฒฐํ•ด ์ฃผ์…จ๋Š”๋ฐ ํ† ์Šค ๊ณผ์ œ์— ๋Œ€ํ•œ ๋ฃจ๋จธ ๋•Œ๋ฌธ์— ์‚ฌ์„ค(?) ๋ง๊ณ  ์ถœ์ œ ํ‰๊ฐ€์œ„์›์˜ ์ถœ์ œ ์˜๋„๋ฅผ ์ง์ ‘ ๋ฐํžˆ๊ฒ ๋‹ค๋Š” ์˜๋„์˜€๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค! ๐Ÿ‘


๐Ÿ“ ๊ณผ์ œ: ๊ผญ ์‹ ๊ฒฝ์จ์ฃผ์„ธ์š” โ˜๏ธ


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

์ธ์ƒ ๊นŠ์€ ๋ถ€๋ถ„ ์ค‘ ํ•˜๋‚˜์˜€์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์š”๊ตฌ์‚ฌํ•ญ ์ „ ์•„์ฃผ ํฐ ๊ธ€์”จ๋กœ ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„์™€ ์ถ”์ƒํ™” ๊ด€์ ์— ์ง‘์ค‘ํ•ด์„œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ผ๋Š” ์š”๊ตฌ์‚ฌํ•ญ์„ ๋ณด๊ณ  ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๋ถ€ํ„ฐ ๊ณ ๋ฏผ์ด ๋“ค๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ“ ๋‚˜์˜ ์ ‘๊ทผ ๐Ÿค“


๊ณผ์ œ๋ฅผ ์‹œ์ž‘ํ•˜๊ณ  1~2์‹œ๊ฐ„ ๋™์•ˆ ์ œ๊ฐ€ ์ด์šฉํ•ด ๋ณธ ๋ชจ๋“  AI๋ฅผ ์ด ์ถœ๋™ํ•ด์„œ ํ† ๋ก ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ญ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋Š” ์–ด๋–ป๊ฒŒ ์žก์„๊นŒ? ํ™•์žฅ์„ฑ์žˆ๋Š” ๊ตฌ์กฐ๋ผ๋ฉด ์—ญ์‹œ FSD ์ผ๊นŒ?
๐Ÿ’ญ ์ƒํ™ฉ๊ณผ ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ์— ๋งž๊ฒŒ ์„ค๊ณ„ํ•ด์•ผ ํ•˜๋Š”๋ฐ ์š”๊ตฌ์‚ฌํ•ญ ์ž์ฒด๊ฐ€ ํ™•์žฅ์„ฑ์žˆ๊ฒŒ ์„ค๊ณ„ํ•˜๋ผ๊ณ  ํ•˜๋„ค?
๐Ÿ’ญ ์–ด๋–ป๊ฒŒ ํ•˜์ง€? ํ† ์Šค๋Š” ๋ญ˜ ์›ํ•˜๋Š” ๊ฑฐ์ง€? ์–ด๋–ป๊ฒŒ ์ ‘๊ทผํ•˜๋Š” ๊ฒŒ ๋งž๋Š” ๊ฑฐ์ง€?

AI๋“ค๊ณผ ํ† ๋ก  ๋์— ์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ „๋žต์„ ์ทจํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์šฐ์„ 
  2. ์„ฃ๋ถ€๋ฅธ ์ถ”์ƒํ™”์™€ ์ตœ์ ํ™” ์ง€์–‘
  3. ๊ฐ™์€ ๊ด€์‹ฌ์‚ฌ๋Š” ๊ฐ€๊น๊ฒŒ ์œ„์น˜
  4. ํ•˜์ง€๋งŒ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋Š” ํ•„์ˆ˜์  ์ง„ํ–‰
  5. ๊ตฌํ˜„ ์ดํ›„ ๋ฆฌํŒฉํ† ๋ง ํ•˜๋ฉด์„œ ์œ ์ง€๋ณด์ˆ˜์™€ ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•œ ์ถ”์ƒํ™” ์ง„ํ–‰

์ดˆ๊ธฐ์—๋Š” YAGNI(You Aren't Gonna Need It) ์›์น™์— ๋”ฐ๋ผ ๊ณผ๋„ํ•œ ์„ค๊ณ„๋ฅผ ์ง€์–‘ํ•˜๊ณ  ๊ตฌํ˜„์— ์ง‘์ค‘ํ•œ ๋’ค, ์žฅ๊ธฐ์ ์ธ ํ™•์žฅ์„ฑ์„ ํ™•๋ณดํ•˜๊ธฐ ์œ„ํ•ด ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

... ์ €๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ํ™•์žฅ์„ฑ์ด๋ผ๋Š” ์˜๋ฏธ๋ฅผ ์˜ค์ธํ•ด ํ•จ์ •์— ๋น ์กŒ์„ ์ง€๋„ ๋ชจ๋ฆ…๋‹ˆ๋‹ค.

ํ•ด์„ค ๋ผ์ด๋ธŒ๋ฅผ ๋ณด๊ณ ๋‚˜๋‹ˆ ๊ตฌ์กฐ๋‚˜ ๋ฐฉ๋ฒ•๋ก  ์ž์ฒด๋Š” ์ค‘์š”ํ•˜์ง€ ์•Š๋‹ค๋Š” ๊ฒƒ์„ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค.
์ œ์ผ ์ค‘์š”ํ•œ ๊ฒƒ์€ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์—ˆ์Šต๋‹ˆ๋‹ค.

์ผ๋‹จ ์ €์˜ ์ ‘๊ทผ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ๋จผ์ € ์„ค๋ช…ํ•ด๋ณด์ง€๋งŒ ๋ณ„๋กœ ์ค‘์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


1. ๊ตฌํ˜„ ๋จผ์ € ๐Ÿ™† . ์„ฃ๋ถ€๋ฅธ ์ถ”์ƒํ™”๋Š” ์ง€์–‘ ๐Ÿ™…

1) ์ฒ˜์Œ์—๋Š” ํŽ˜์ด์ง€ ์ง‘์ค‘ํ˜• (Colocation) ์•„ํ‚คํ…์ฒ˜ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

src/
โ”œโ”€โ”€ utils/
โ”‚   โ””โ”€โ”€ format.ts                        # ์ „์—ญ: ์ˆซ์ž ํฌ๋งทํŒ…
โ””โ”€โ”€ pages/
    โ””โ”€โ”€ SavingsCalculatorPage/           # ์ ๊ธˆ ๊ณ„์‚ฐ ์ „์šฉ ํŽ˜์ด์ง€
        โ”œโ”€โ”€ index.tsx                    # export
        โ”œโ”€โ”€ SavingsCalculatorPage.tsx    # ๋ฉ”์ธ ํŽ˜์ด์ง€
        โ”œโ”€โ”€ types.ts                     # ํƒ€์ž… ์ •์˜
        โ”œโ”€โ”€ hooks/
        โ”‚   โ”œโ”€โ”€ useSavingsFormState.ts    # ์ž…๋ ฅ ์ƒํƒœ
        โ”‚   โ”œโ”€โ”€ useSavingsProductData.ts  # ๋ฐ์ดํ„ฐ ์กฐํšŒ
        โ”‚   โ”œโ”€โ”€ useProductSelection.ts    # ์ƒํ’ˆ ์„ ํƒ
        โ”‚   โ”œโ”€โ”€ useSavingsResult.ts       # ๊ณ„์‚ฐ ๊ฒฐ๊ณผ
        โ”‚   โ””โ”€โ”€ useRecommendedProducts.ts # ์ถ”์ฒœ ์ƒํ’ˆ
        โ””โ”€โ”€ components/
            โ”œโ”€โ”€ SavingsForm.tsx
            โ”œโ”€โ”€ ProductList.tsx
            โ”œโ”€โ”€ CalculationResult.tsx
            โ””โ”€โ”€ RecommendedProducts.tsx

2) ์ปดํฌ๋„ŒํŠธ๋Š” ์ปจํ…์ธ ์™€ ์—ญํ•  ๊ธฐ์ค€์œผ๋กœ ๊ตฌ๋ถ„ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค.

return (
  <>
    <PageHeader title="์ ๊ธˆ ๊ณ„์‚ฐ๊ธฐ" />

    {/* ๊ณ„์‚ฐ ์ž…๋ ฅ */}
    <section>
      <Spacing size={16} />
      <SavingForm value={inputs} onChange={handleInputChange} />
      <Spacing size={16} />
    </section>

    <Divider borderHeight={16} spacingHeight={8} />

    {/* ์‚ฌ์šฉ์ž ์„ ํƒ ํƒญ */}
    <Tab onChange={value => setActiveTab(value as 'products' | 'results')}>
      <Tab.Item value="products" selected={activeTab === 'products'}>
        ์ ๊ธˆ ์ƒํ’ˆ
      </Tab.Item>
      <Tab.Item value="results" selected={activeTab === 'results'}>
        ๊ณ„์‚ฐ ๊ฒฐ๊ณผ
      </Tab.Item>
    </Tab>

    {activeTab === 'products' && (
      <ProductList products={products} selectedProductId={selectedProductId} onSelect={handleProductSelect} />
    )}

    {activeTab === 'results' && (
      <>
        {/* ๊ณ„์‚ฐ ๊ฒฐ๊ณผ */}
        <CalculationResult />

        <Divider borderHeight={16} spacingHeight={8} />

        {/* ์ถ”์ฒœ ์ƒํ’ˆ */}
        <RecommendedProducts />
      </>
    )}

    <Spacing size={40} />
  </>
);

3) ํ›…์€ ๊ด€์‹ฌ์‚ฌ ๋ณ„ ๋ถ„๋ฆฌํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ํŽ˜์ด์ง€์—์„œ ๋ช…์‹œ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ์—๋Š” ํ›…์€ ๋‹ด๋‹นํ•˜๋Š” ์ƒํƒœ ๊ด€๋ฆฌ ์—ญํ•  ๊ธฐ์ค€์œผ๋กœ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ , ๊ทธ ํ›…๋“ค์„ ํ•œ ๊ณณ์—์„œ ๋ชจ์•„๋‘๋Š” ํ›…์„ ๋งŒ๋“ค์–ด ํŽ˜์ด์ง€์—์„œ ํ•œ๋ฒˆ์— ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•˜๋‹ค๊ฐ€... ์ƒํƒœ์™€ ๊ทธ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฉ”์„œ๋“œ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ํ•œ๋ฒˆ์— ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ต๊ณ  โ˜ ๏ธ ์–ด๋–ค ๊ฐ’์ด ๋„คํŠธ์›Œํฌ ์š”์ฒญ์˜ ํŽ˜์ด๋กœ๋“œ๋กœ ์ „๋‹ฌ๋˜๋Š”์ง€ ํŒŒ์•…์ด ์–ด๋ ค์›Œ์„œ โ˜ ๏ธ ์ค‘์•™ ์ง‘์ค‘ํ˜• ํ›…์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

const { inputs, handleInputChange } = useSavingsFormState();
const { products } = useSavingsProductData(inputs); // ๐Ÿค” ...
const { selectedProductId, selectedProduct, handleProductSelect } = useProductSelection(products);
const { savingResult } = useSavingsResult(inputs, selectedProduct);
const { recommendedProducts } = useRecommendedProducts(products); // ๐Ÿค” ...

2. ๊ตฌํ˜„ ๋. ์ข‹์•„ ์ด์ œ ํ™•์žฅ์„ฑ ์ฑ™๊ฒจ! โ˜ ๏ธ

๐Ÿง ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•˜๊ธฐ ์œ„ํ•ด ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์ž๋งˆ์ž ๋ณด์ธ ๊ฑด ํƒญ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ํƒญ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ๋„๋ฉ”์ธ๊ณผ๋Š” ๊ด€๊ณ„๊ฐ€ ์—†์œผ๋‹ˆ ๋ถ„๋ฆฌํ•ด์•ผ ๋ ๊ฑฐ ๊ฐ™์€๋ฐ?

const [activeTab, setActiveTab] = useState<'products' | 'results'>('products');
{/* ์‚ฌ์šฉ์ž ์„ ํƒ ํƒญ */}
<Tab onChange={value => setActiveTab(value as 'products' | 'results')}>
  <Tab.Item value="products" selected={activeTab === 'products'}>
    ์ ๊ธˆ ์ƒํ’ˆ
  </Tab.Item>
  <Tab.Item value="results" selected={activeTab === 'results'}>
    ๊ณ„์‚ฐ ๊ฒฐ๊ณผ
  </Tab.Item>
</Tab>

{activeTab === 'products' && (
  <ProductList products={products} selectedProductId={selectedProductId} onSelect={handleProductSelect} />
)}

{activeTab === 'results' && (
  <>
    {/* ๊ณ„์‚ฐ ๊ฒฐ๊ณผ */}
    <CalculationResult result={savingResult} />

    <Divider borderHeight={16} spacingHeight={8} />

    {/* ์ถ”์ฒœ ์ƒํ’ˆ */}
    <RecommendedProducts
      products={recommendedProducts}
      selectedProductId={selectedProductId}
      onSelect={handleProductSelect}
      />
    </>
)}

โžก๏ธ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋„๋ก ํƒญ ๊ฐœ์ˆ˜๋งŒ ํ™•์žฅํ•ด์„œ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์ž! Tabs.Panel props๋กœ ํƒญ ์ด๋ฆ„๊ณผ ์‹๋ณ„์ž๋ฅผ ๋ณด๋‚ด๊ณ  ๋ณด์—ฌ์ค„ ์ฃผ ๋‚ด์šฉ์€ children์œผ๋กœ ์ „๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค.

1๊ฐœ๋“  2๊ฐœ๋“  4๊ฐœ๋“  ๊ฐ๋‹น ๊ฐ€๋Šฅ! ์ด๋Ÿฐ๊ฒŒ ํ™•์žฅ์„ฑ ์ด๊ฒ ์ง€? ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ถœ์ œ ์˜๋„๊ฐ€ ์•„๋‹ˆ์˜€์Šต๋‹ˆ๋‹ค.

import { Tabs } from 'components/common';
<Tabs defaultValue="products">
  <Tabs.Panel label="์ ๊ธˆ ์ƒํ’ˆ" value="products">
    {/* ์ƒํ’ˆ ๋ชฉ๋ก */}
    <ProductList products={products} selectedProductId={selectedProductId} onSelect={handleProductSelect} />
  </Tabs.Panel>

  <Tabs.Panel label="๊ณ„์‚ฐ ๊ฒฐ๊ณผ" value="results">
    {/* ๊ณ„์‚ฐ ๊ฒฐ๊ณผ */}
    <CalculationResult result={savingResult} />
    <Divider borderHeight={16} spacingHeight={8} />
    {/* ์ถ”์ฒœ ์ƒํ’ˆ */}
    <RecommendedProducts
      products={recommendedProducts}
      selectedProductId={selectedProductId}
      onSelect={handleProductSelect}
      />
  </Tabs.Panel>
</Tabs>

๐Ÿง api๋Š” ๊ณตํ†ต์œผ๋กœ ๊ด€๋ฆฌํ•ด์•ผ๊ฒ ์ง€? DTO ํƒ€์ž…์ด๋ž‘ ์‹ค์ œ ๋„๋ฉ”์ธ ํƒ€์ž…์ด๋ž‘ ๊ณ„์ธต ๋ถ„๋ฆฌํ•ด์•ผ๊ฒ ์ง€? ๊ทธ๋ฆฌ๊ณ  ์ด๋Ÿฌ์ฟต ์ €๋Ÿฌ์ฟต ํ•ด์•ผ๊ฒ ์ง€? ๊ทธ๋ฆฌ๊ณ ... ์•„.. ํ™•์žฅ์„ฑ์ด๋ผ๋ฉด ํ•ด๋‹น ๋„๋ฉ”์ธ ๋ง๊ณ  ๋‹ค๋ฅธ ๋„๋ฉ”์ธ๋„ ์žˆ์„ ํ…๋ฐ ๊ฒฐ๊ตญ feature ๋ณ„๋กœ ๋‚˜๋ˆ ์•ผ๊ฒ ์ง€?

โžก๏ธ ์ตœ์ข… ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์ฒ˜์Œ๊ณผ ๋งŽ์ด ๋‹ฌ๋ผ์กŒ์Šต๋‹ˆ๋‹ค.

src/
โ”œโ”€โ”€ apis/                                # API ๋ ˆ์ด์–ด
โ”‚   โ”œโ”€โ”€ savingsApi.ts                    # API ํ˜ธ์ถœ
โ”‚   โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ types/                               # ํƒ€์ž… ๋ ˆ์ด์–ด (๋Œ€์นญ ๊ตฌ์กฐ)
โ”‚   โ”œโ”€โ”€ apis/
โ”‚   โ”‚   โ”œโ”€โ”€ savingsApiTypes.ts           # API ์‘๋‹ต ํƒ€์ž… (DTO)
โ”‚   โ”‚   โ””โ”€โ”€ index.ts
โ”‚   โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ utils/                               # ์ „์—ญ ์œ ํ‹ธ๋ฆฌํ‹ฐ
โ”‚   โ””โ”€โ”€ format.ts                        
โ”œโ”€โ”€ components/                          # ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ
โ”‚   โ””โ”€โ”€ common/
โ”‚       โ”œโ”€โ”€ Tabs.tsx                     # Compound Component Pattern
โ”‚       โ”œโ”€โ”€ PageHeader.tsx
โ”‚       โ”œโ”€โ”€ Divider.tsx
โ”‚       โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ pages/                                # ๋ผ์šฐํŒ… ์ง„์ž…์  (Top Layer)
โ”‚   โ””โ”€โ”€ SavingsCalculatorPage.ts          # features/savings๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ๋ Œ๋”๋ง (Thin Layer)
โ”‚
โ””โ”€โ”€ features/                             # (Middle Layer)
    โ””โ”€โ”€ savings/                          # ๋„๋ฉ”์ธ
        โ”œโ”€โ”€ types.ts                      # ๋„๋ฉ”์ธ ๋ชจ๋ธ ํƒ€์ž…
        โ”œโ”€โ”€ constants.ts                  
        โ”œโ”€โ”€ pages/                        # ์‹ค์ œ ํŽ˜์ด์ง€ ๊ด€๋ฆฌ
        โ”‚   โ””โ”€โ”€ SavingsCalculatorPage.tsx # ๋ผ์šฐํŒ… ์ง„์ž…์  (Hook ์กฐ๋ฆฝ)
        โ”œโ”€โ”€ hooks/                        # [Internal] Hook Composition Pattern
        โ”‚   โ”œโ”€โ”€ useSavingsFormState.ts    
        โ”‚   โ”œโ”€โ”€ useSavingsProductData.ts  
        โ”‚   โ”œโ”€โ”€ useProductSelection.ts    
        โ”‚   โ”œโ”€โ”€ useSavingsResult.ts       
        โ”‚   โ”œโ”€โ”€ useRecommendedProducts.ts 
        โ”‚   โ””โ”€โ”€ index.ts
        โ””โ”€โ”€ components/                   # [Internal] UI Components
            โ”œโ”€โ”€ SavingsForm.tsx
            โ”œโ”€โ”€ ProductList.tsx           # Inversion of Control
            โ”œโ”€โ”€ FilteredProducts.tsx
            โ”œโ”€โ”€ CalculationResult.tsx
            โ””โ”€โ”€ RecommendedProducts.tsx

๐Ÿ“ ๋ชจ์˜๊ณ ์‚ฌ ํ•ด์„ค ๋ผ์ด๋ธŒ ์ดํ›„


0. ๊ตฌํ˜„ ๋”ฐ๋กœ, ๋ฆฌํŒฉํ† ๋ง ๋”ฐ๋กœ ? ๐Ÿค”

์ €๋Š” ๊ตฌํ˜„์— ์ง‘์ค‘ํ•œ ๋’ค์— ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋‹จ๊ณ„๋ฅผ ์ชผ๊ฐœ์„œ ์ง„ํ–‰ํ•˜๋ ค ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก  ๋ฌผ๊ณผ ๊ธฐ๋ฆ„์ฒ˜๋Ÿผ ๋ฆฌํŒฉํ† ๋ง์„ ๋”ฑ ๋ถ„๋ฆฌํ•ด์„œ ํ•˜์ง€๋Š” ์•Š์ง€๋งŒ ๊ตณ์ด ๋‹จ๊ณ„๋ฅผ ๋‘˜๋กœ ์ชผ๊ฐœ์„œ ์ง„ํ–‰ํ•˜๊ฒ ๋‹ค๋Š” ๋ฐฉํ–ฅ ์„ค์ •์€ ์ข‹์€ ์ „๋žต์ด ์•„๋‹ˆ์˜€๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์•ž์„œ ๋ง์”€๋“œ๋ ธ๋‹ค์‹œํ”ผ ํ† ์Šค์—์„œ ์–˜๊ธฐํ•œ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์œ„ํ•œ ํ™•์žฅ์„ฑ ์žˆ๋Š” ์„ค๊ณ„๋Š” ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ฃผ๋œ ํฌ์ธํŠธ์˜€์Šต๋‹ˆ๋‹ค.

๋‚˜๋จธ์ง€ ๊ตฌ์กฐ๋‚˜ ๋ฐฉ๋ฒ•๋ก ์€ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์˜€๊ณ , ๋ชจ์˜๊ณ ์‚ฌ์—์„œ ์š”๊ตฌํ•œ ํ™•์žฅ์„ฑ์žˆ๋Š” ์„ค๊ณ„๊ฐ€ ๊ณผ์ œ์˜ ํ•œ ํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ•˜์ง€๋งŒ ์–ธ์  ๊ฐ€ ๋Œ€๊ทœ๋ชจ๋กœ ํ™•์žฅ๋  ๊ฒƒ์„ ๋Œ€๋น„ํ•ด์„œ ์„ค๊ณ„ํ•˜๋ž€ ๋œป์ด ์•„๋‹ˆ์˜€์Šต๋‹ˆ๋‹ค.

๊ธ€๋กœ ์“ฐ๊ณ ๋‚˜๋‹ˆ ๋‹น์—ฐํ•œ ์–˜๊ธฐ ๊ฐ™์ง€๋งŒ ๋ง‰์ƒ ์š”๊ตฌ์‚ฌํ•ญ์œผ๋กœ ์ฃผ์–ด์กŒ์„ ๋•Œ๋Š” ๊ต‰์žฅํžˆ ๋งŽ์€ ๊ณ ๋ฏผ์ด ๋“ค๊ฒŒ ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค.
์ถœ์ œ ์˜๋„๋ฅผ ์ž˜ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๊ฒ ์ฃ .

โš ๏ธ ์•„๋ž˜ ๋‚ด์šฉ์˜ ์ฝ”๋“œ๋Š” ์ •๋‹ต์ด ์•„๋‹ˆ๋ฉฐ ํ•ด์„ค ๋ผ์ด๋ธŒ ๋‚ด์šฉ์„ ๋“ค์€ ๋’ค์— ์ œ ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘์„ฑํ•ด ๋ณธ ๊ฒƒ ๋ฟ์ž…๋‹ˆ๋‹ค.


1. ์š”๊ตฌ์‚ฌํ•ญ ๋ฌธ์„œ๋ฅผ ๋ณด๊ณ  '์ด์ƒ์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค' ์„ค๊ณ„ ๋จผ์ €

๊ฐ€์žฅ ๋จผ์ € ์ด์ƒ์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ค๊ณ„ํ•˜์ž!
์˜์‚ฌ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค.

ํ•œ ํŽ˜์ด์ง€๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ œ๊ฐ€ ์ƒ๊ฐํ•˜๋Š” ์ด์ƒ์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ค๊ณ„ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

export default function SavingsCalculatorPage() {

  return (
    <main>
      <h1>์ ๊ธˆ ๊ณ„์‚ฐ๊ธฐ</h1>

      <section>
        <h2 className="sr-only">์ ๊ธˆ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ์„ค์ •</h2>
        <SavingsForm inputs={inputs} onChange={handleFormStates} />
      </section>
      
      <Divider />
      
      <Tabs>
        <Tab.Panel>
          <section>
            <h2>์ ๊ธˆ ์ƒํ’ˆ</h2>
            <SavingsProductList {...props} />
          </section>
        </Tab.Panel>
        
        <Tab.Panel>
          <section>
            <h2>๊ณ„์‚ฐ ๊ฒฐ๊ณผ</h2>
            <CalculationResult selected={selected} inputs={inputs} />
          </section>
          
          <Divider />
          
          <section>
            <h2>์ถ”์ฒœ ์ƒํ’ˆ ๋ชฉ๋ก</h2>
            <RecommendProductList {...props} />
          </section>
        </Tab.Panel>
      </Tabs>        
    </main>
  );
}

์ œ๊ฐ€ ๋ผ์ด๋ธŒ ํ•ด์„ค์„ ๋“ฃ๊ธฐ ์ „์ด๋ผ๋ฉด ํŽ˜์ด์ง€์˜ ๊ฐœ์š”์™€ ์ „์ฒด์ ์ธ UI ๊ตฌ์กฐ๊ฐ€ ํ•œ๋ฒˆ์— ํŒŒ์•…์ด ๋˜๋Š” ์ด ๊ฐ„๊ฒฐํ•œ ์ฝ”๋“œ๊ฐ€ ๊ฝค ๋งˆ์Œ์— ๋“ค์–ด ํ–ˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ SavingsForm ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์–ด๋–ค ์ž…๋ ฅ ๊ฐ’๋“ค์„ ํ•„์š”๋กœ ํ•˜๋Š”์ง€ ์ „ํ˜€ ์˜ˆ์ธกํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ฐ ์„น์…˜๋งˆ๋‹ค ์—„์ฒญ ๋งŽ์€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ ๊ฐ€๋…์„ฑ์„ ํ•ด์น ๋งŒํผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งŽ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ปดํฌ๋„ŒํŠธ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์ด ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ตณ์ด ์ˆจ๊ธฐ์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

<form onSubmit={e => e.preventDefault()}>
  <Input
    label="๋ชฉํ‘œ ๊ธˆ์•ก" 
    placeholder="๋ชฉํ‘œ ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" 
    value={value.๋ชฉํ‘œ๊ธˆ์•ก} 
    onChange={handleFormStates} 
    />
  <Input 
    label="์›” ๋‚ฉ์ž…์•ก" 
    placeholder="ํฌ๋ง ์›” ๋‚ฉ์ž…์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" 
    value={value.์›”๋‚ฉ์ž…์•ก} 
    onChange={handleFormStates} 
    />
  <Select label="์ €์ถ• ๊ธฐ๊ฐ„" value={12} onChange={handleFormStates}>
    <Option>6</Option>
    <Option>12</Option>
    <Option>24</Option>
  </Select>
</form>

์ฃผ์–ด์ง„ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์— ์ดˆ๊ธฐ ์ฝ”๋“œ๋Š” ๋ชฉํ‘œ ๊ธˆ์•ก๊ณผ ์›” ๋‚ฉ์ž…์•ก์„ ์ž…๋ ฅํ•˜๋Š” TextInput ์ปดํฌ๋„ŒํŠธ 2๊ฐœ์™€ ๊ฐœ์›” ์ˆ˜ ์˜ต์…˜์„ ์„ ํƒํ•˜๋Š” SelectBottomSheet 1๊ฐœ๋งŒ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

// ์ˆ˜์ • ์ „ ์ฝ”๋“œ๋“ค

<NavigationBar title="์ ๊ธˆ ๊ณ„์‚ฐ๊ธฐ" />

<Spacing size={16} />

<TextField label="๋ชฉํ‘œ ๊ธˆ์•ก" placeholder="๋ชฉํ‘œ ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" suffix="์›" />
<Spacing size={16} />
<TextField label="์›” ๋‚ฉ์ž…์•ก" placeholder="ํฌ๋ง ์›” ๋‚ฉ์ž…์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" suffix="์›" />
<Spacing size={16} />
<SelectBottomSheet label="์ €์ถ• ๊ธฐ๊ฐ„" title="์ €์ถ• ๊ธฐ๊ฐ„์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”" value={12} onChange={() => {}}>
  <SelectBottomSheet.Option value={6}>6๊ฐœ์›”</SelectBottomSheet.Option>
  <SelectBottomSheet.Option value={12}>12๊ฐœ์›”</SelectBottomSheet.Option>
  <SelectBottomSheet.Option value={24}>24๊ฐœ์›”</SelectBottomSheet.Option>
</SelectBottomSheet>

<Spacing size={24} />

// ... ์ƒ๋žต

๋ฐ”๋กœ form ์š”์†Œ๋‚˜ onSubmit ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜๊ณ  ์ž…๋ ฅ ์‹œ์— ๋ฐ”๋กœ ์ž…๋ ฅ๊ฐ’์— ์˜ํ•ด ์ƒํ’ˆ ๋ชฉ๋ก์ด ํ•„ํ„ฐ๋ง ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๊ตณ์ด form ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๋Š” ์—†์–ด ๋ณด์ž…๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ์ €๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์„ ๋ฐ›์•„์•ผ ํ•œ๋‹ค๋ฉด form ์š”์†Œ๋กœ ๊ทธ๋ฃนํ™” ํ•˜๋Š” ๊ฒƒ์ด HTML5 ํ‘œ์ค€์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฐ€์žฅ ๋จผ์ € ์ด์ƒ์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•˜์…จ๊ณ  ์ €๋Š” ์ œ ๊ธฐ์ค€ ์ด์ƒ์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ์ค‘ ์ž…๋‹ˆ๋‹ค.

์ฃผ์–ด์ง„ SelectBottomSheet ์ปดํฌ๋„ŒํŠธ ๊ฒฝ์šฐ ์˜ต์…˜์„ ์„ ํƒํ•˜๋Š” ๋ฐ BottomSheet๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๊ทธ๋ ‡๊ฒŒ ์ค‘์š”ํ•˜์ง€ ์•Š๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๋ถ€ ๊ตฌ์„ฑ ์š”์†Œ๋Š” select ์š”์†Œ๋กœ ๋ฐ”๋€” ์ˆ˜๋„ ์žˆ๊ณ  UI ํ˜•ํƒœ๊ฐ€ ๋ชจ๋‹ฌ ๋กœ ๋ฐ”๋€” ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ HTML์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ผ๋ฐ˜์ ์ธ ์ƒํ™ฉ์—์„œ ๋ชจ๋‘๊ฐ€ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•˜์…จ๊ณ , ํŠนํžˆ ์ฐฝ์˜๋ ฅ์„ ๋ฐœํœ˜ํ•˜์ง€ ๋ง์ž! ์ด ํ•œ๋งˆ๋””๊ฐ€ ๋งˆ์Œ ์† ๊นŠ์ด ๋ฐ•ํ˜”์Šต๋‹ˆ๋‹ค.

๋ผ์ด๋ธŒ๋ฅผ ๋“ค์œผ๋ฉด์„œ UI ๋˜๋Š” ๋‚ด๋ถ€ ๊ตฌ์กฐ๊ฐ€ ๋ฐ”๋€”์ง€ ๋ชจ๋ฅด๋Š” ์ƒํ™ฉ์—์„œ ๋ณธ์งˆ์— ์ง‘์ค‘ํ•ด ์ˆ˜์ •์— ์œ ์—ฐํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“œ๋Š” ์ดˆ์„์ด๊ตฌ๋‚˜ ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ์ œ ๊ธฐ์ค€ ์ด์ƒ์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ค๊ณ„ํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

export default function SavingsCalculatorPage() {

  return (
    <main>
      <h1>์ ๊ธˆ ๊ณ„์‚ฐ๊ธฐ&</h1>
      
      <section>
        <h2 className="sr-only">์ ๊ธˆ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ์„ค์ •</h2>
        
        <form onSubmit={e => e.preventDefault()}>
          <Input 
            label="๋ชฉํ‘œ ๊ธˆ์•ก" 
            placeholder="๋ชฉํ‘œ ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" 
            value={value.๋ชฉํ‘œ๊ธˆ์•ก} 
            onChange={handleFormStates} 
          />
          <Input 
            label="์›” ๋‚ฉ์ž…์•ก" 
            placeholder="ํฌ๋ง ์›” ๋‚ฉ์ž…์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" 
            value={value.์›”๋‚ฉ์ž…์•ก} 
            onChange={handleFormStates} 
          />
          <Select label="์ €์ถ• ๊ธฐ๊ฐ„" value={12} onChange={handleFormStates}>
            <Option>6</Option>
            <Option>12</Option>
            <Option>24</Option>
          </Select>
        </form>
      </section>
      
      <Divider />
      
      <Tabs>
        <Tab.Panel>
          <section>
            <h2>์ ๊ธˆ ์ƒํ’ˆ</h2>
            <SavingsProductList {...props} />
          </section>
        </Tab.Panel>
        
        <Tab.Panel>
          <section>
            <h2>๊ณ„์‚ฐ ๊ฒฐ๊ณผ</h2>
            <CalculationResult selected={selected} inputs={inputs} />
          </section>
          
          <Divider />
          
          <section>
            <h2>์ถ”์ฒœ ์ƒํ’ˆ ๋ชฉ๋ก</h2>
            <RecommendProductList {...props} />
          </section>
        </Tab.Panel>
      </Tabs>        
    </main>
  );
}

โš ๏ธ ์ด ๊ธ€์— ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋Š” ํ† ์Šค ๋ชจ์˜๊ณ ์‚ฌ ํ•ด์„ค ๋ผ์ด๋ธŒ์—์„œ ์ œ์‹œํ•œ ์ •๋‹ต์ด ์•„๋‹ˆ๋ฉฐ, ๋“ค์—ˆ๋˜ ๋‚ด์šฉ์„ ๋ณต๊ธฐํ•˜๋ฉด์„œ ์ œ ๊ธฐ์ค€์—์„œ ์ž‘์„ฑํ•ด ๋ณธ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.


2. UI ํ˜•ํƒœ์™€ ์ฝ”๋“œ ํ˜•ํƒœ๊ฐ€ 1:1 ๋งคํ•‘๋˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฝ๋‹ค

์ด์ƒ์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ ์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„๋ฅผ ํ•˜๋‚˜์”ฉ ์ œ๊ณตํ•ด์ค€ ์ปดํฌ๋„ŒํŠธ๋กœ ์ ์šฉํ•ด ๋ด…๋‹ˆ๋‹ค.

<NavigationBar title="์ ๊ธˆ ๊ณ„์‚ฐ๊ธฐ" />
<Spacing size={16} />

<TextField 
  label="๋ชฉํ‘œ ๊ธˆ์•ก" 
  placeholder="๋ชฉํ‘œ ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" 
  suffix="์›"
/>

<Spacing size={16} />

<TextField 
  label="์›” ๋‚ฉ์ž…์•ก" 
  placeholder="ํฌ๋ง ์›” ๋‚ฉ์ž…์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" 
  suffix="์›" 
/>

<Spacing size={16} />

<SelectBottomSheet 
  label="์ €์ถ• ๊ธฐ๊ฐ„" 
  title="์ €์ถ• ๊ธฐ๊ฐ„์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”" 
  value={12} 
  onChange={() => {}}
>
  <SelectBottomSheet.Option value={6}>6๊ฐœ์›”</SelectBottomSheet.Option>
  <SelectBottomSheet.Option value={12}>12๊ฐœ์›”</SelectBottomSheet.Option>
  <SelectBottomSheet.Option value={24}>24๊ฐœ์›”</SelectBottomSheet.Option>
</SelectBottomSheet>
<Spacing size={24} />

์œ„์˜ ์ฃผ์–ด์ง„ ์ฝ”๋“œ๋ฅผ ์ ์šฉํ•˜๋ฉด

<PageHeader title="์ ๊ธˆ ๊ณ„์‚ฐ๊ธฐ" />
      
<section>
  <Spacing size={16} />
  <h2 className="sr-only">์ ๊ธˆ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ์„ค์ •</h2>
  
   <form onSubmit={(e: React.FormEvent<HTMLFormElement>) => e.preventDefault()}>
    <TextField 
      label="๋ชฉํ‘œ ๊ธˆ์•ก" 
      placeholder="๋ชฉํ‘œ ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" 
      value={value.๋ชฉํ‘œ๊ธˆ์•ก}
      suffix="์›"
      onChange={handleFormStates} 
      />

    <Spacing size={16} />

    <Input 
      label="์›” ๋‚ฉ์ž…์•ก" 
      placeholder="ํฌ๋ง ์›” ๋‚ฉ์ž…์•ก์„ ์ž…๋ ฅํ•˜์„ธ์š”" 
      value={value.์›”๋‚ฉ์ž…์•ก}
      suffix="์›"
      onChange={handleFormStates} 
      />

    <Spacing size={16} />

    <SelectBottomSheet 
      label="์ €์ถ• ๊ธฐ๊ฐ„" 
      title="์ €์ถ• ๊ธฐ๊ฐ„์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”" 
      value={12} 
      onChange={handleFormStates}
    >
      <SelectBottomSheet.Option value={6}>6๊ฐœ์›”</SelectBottomSheet.Option>
      <SelectBottomSheet.Option value={12}>12๊ฐœ์›”</SelectBottomSheet.Option>
      <SelectBottomSheet.Option value={24}>24๊ฐœ์›”</SelectBottomSheet.Option>
    </SelectBottomSheet>
  </form>
  
  <Spacing size={24} />
</section>

์‹œ๋ฉ˜ํ‹ฑ ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ props๊ฐ€ ์ถ”๊ฐ€ํ•œ ๊ฒƒ ์™ธ์—” ๋”ฑํžˆ ์›๋ž˜ ์ฃผ์–ด์ง„ ์ฝ”๋“œ์™€ ๋ณ„ ์ฐจ์ด๊ฐ€ ์—†์–ด ๋ณด์ž…๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์ œ๊ฐ€ ์ฒ˜์Œ์— ์ƒ๊ฐํ–ˆ๋˜ ๋Œ€๋กœ ๋ณ„๋„์˜ Form ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ด์„œ ๋ถ„๋ฆฌํ•œ๋‹ค๋ฉด ๋” ๋‚˜์„๊นŒ์š”?

<PageHeader title="์ ๊ธˆ ๊ณ„์‚ฐ๊ธฐ" />
      
<section>
  <h2 className="sr-only">์ ๊ธˆ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ์„ค์ •</h2>
  
  <Spacing size={16} />
  
  <SavingsForm inputs={inputs} onChange={handleFormStates} />
  
  <Spacing size={24} />
</section>

๋ณด์ด๋Š” ํ™”๋ฉด์˜ ์ฝ”๋“œ ์ˆ˜๋Š” ์ค„์–ด๋“ค์–ด ๊น”๋”ํ•ด ๋ณด์ด์ง€๋งŒ ์‹ค์ œ ์ฝ”๋“œ ์ˆ˜๋Š” ๊ทธ๋Œ€๋กœ์ž…๋‹ˆ๋‹ค.

์ด๋Ÿฐ ์ƒํ™ฉ์„ "์ถ”์ƒํ™”๊ฐ€ ์•„๋‹Œ ์ถ”์ถœ" ์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ •ํ™•ํ•œ ์˜ˆ์‹œ๋กœ๋Š” ๋ชฌ์Šคํ„ฐ ํ›…(ํ•˜๋‚˜์˜ ํ›…์— ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋ชฐ๋นตํ•œ ํ›…)์„ ์‚ฌ์šฉํ•œ ์•ˆํ‹ฐ ํŒจํ„ด์˜ ์˜ˆ์‹œ์—์„œ ์–ธ๊ธ‰ํ•˜์…จ์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€(์ข‹์€ ์ฝ”๋“œ๋ž€ ๋ฌด์—‡์ผ๊นŒ ๐Ÿงš)์„ ์ฝ์–ด ๋ณด์‹œ๋ฉด ์ถ”์ถœ๊ณผ ์ถ”์ƒํ™” ์ฐจ์ด์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์•„๊นŒ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด ์–ด๋–ค ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์„ ๋ฐ›๋Š”์ง€ ๋ฐ”๋กœ ์˜ˆ์ธก์ด ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋…์„ฑ์„ ์˜คํžˆ๋ ค ํ•ด์น˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ’ญ ํ•˜์ง€๋งŒ ๋„ˆ๋ฌด ๋งŽ์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ๋Š”? ๋ณต์žกํ•˜๊ณ  ๋งŽ์€ UI ๋กœ์ง๊ณผ ์ปดํฌ๋„ŒํŠธ๋“ค ํ•œ ํŽ˜์ด์ง€์— ๋ชจ๋‘ ๋ณด์—ฌ์ฃผ๊ฒŒ ๋˜๋ฉด ๋” ์ฝ๊ธฐ ์–ด๋ ค์šด ์ฝ”๋“œ๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹๊นŒ?

์‹ค์ œ๋กœ ๋น„์Šทํ•œ ๋‚ด์šฉ์˜ ์งˆ๋ฌธ์„ ๋ˆ„๊ตฐ๊ฐ€ ํ•˜์…จ๋Š” ๋ฐ ๋‹ต๋ณ€์œผ๋กœ ์ถ•์ฒ™์— ๋งž์ถฐ์„œ ์„ค๊ณ„ํ•˜๋ผ๊ณ  ํ•˜์‹  ๊ฒŒ ๊ธฐ์–ต์— ๋‚จ์Šต๋‹ˆ๋‹ค.
์ถ•์ฒ™(็ธฎๅฐบ)์€ ์ง€๋„๋ฅผ ๋งŒ๋“ค ๋•Œ ์‹ค์ œ ๊ฑฐ๋ฆฌ๋ฅผ ์–ผ๋งˆ๋‚˜ ์ค„์—ฌ์„œ ํ‘œํ˜„ํ•˜๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋น„์œจ์ž…๋‹ˆ๋‹ค.

๋น„์œ ๋กœ ํŽ˜์ด์ง€๊ฐ€ ์„ธ๊ณ„์ง€๋„๋ผ๋ฉด ํ•œ๊ตญ, ์ผ๋ณธ ๋“ฑ ๋‚˜๋ผ๋ฅผ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋Š” ๋ ˆ๋ฒจ์˜ ์ปดํฌ๋„ŒํŠธ๋“ค๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋Š”๋ฐ ๊ฐ‘์ž๊ธฐ ๊ฐ•๋‚จ๊ตฌ, ํ…Œํ—ค๋ž€๋กœ 142 ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‚˜์˜ค๋ฉด ์•ˆ๋œ๋‹ค๊ณ  ํ•˜์…จ๋˜ ๊ฑฐ ๊ฐ™์•„์š”. (๋น„์Šทํ•˜๊ฒŒ ๋ง์”€ํ•˜์‹  ๊ฑฐ ๊ฐ™์€๋ฐ ์ •ํ™•ํ•œ ํ‘œํ˜„์€ ํ‹€๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ˜…)

์ง€๊ธˆ ๋ชจ์˜๊ณ ์‚ฌ์˜ ๊ฒฝ์šฐ๋Š” ๋‚˜๋ผ๋Š” ์ปค๋…• ๋ช‡ ๋™, ๋ช‡ ํ˜ธ ๋˜๋Š” ๋ˆ„๊ตฌ ์‚ฌ๋ฌผํ•จ ์œ„์น˜ ์ •๋„์˜ ๋ ˆ๋ฒจ์ธ ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค.


๊ทธ๋ฆฌ๊ณ  ์ข€ ๋” ๋งŽ์€ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๊ณ  ์‹ถ์ง€๋งŒ ๋ฆฌํŒฉํ† ๋ง ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๊ฐ„์ด ์ƒ๊ฒจ์„œ ๊ทธ ์ดํ›„์— ์ด ํฌ์ŠคํŠธ์— ์ถ”๊ฐ€๋กœ ์—…๋ฐ์ดํŠธ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.


3. ์žฌ์‚ฌ์šฉ์€ ๋”ฐ๋ผ์˜ค๋Š” ๊ฒƒ. ์ฑ…์ž„ ๋‹จ์œ„๋กœ ์ถ”์ƒํ™”๋ฅผ ํ•  ๋ฟ ๐Ÿ™

์ด ๋‚ด์šฉ๋„ ์ธ์ƒ ๊นŠ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ ๋งˆ์ง€๋ง‰ ์ฏค "์žฌ์‚ฌ์šฉํ•  ๊ฑฐ ๊ฐ™์œผ๋‹ˆ ๋”ฐ๋กœ ๋บ„๊นŒ์š”?" ๋ผ๋Š” ๋ฅ˜์˜ ๋ง์„ ์–ธ๊ธ‰ ์•ˆํ•œ ์ด์œ ์— ๋Œ€ํ•ด ์„ค๋ช…ํ•˜์…จ๋Š”๋ฐ์š”.

์ฑ…์ž„ ๋‹จ์œ„๋กœ ์ถ”์ƒํ™”๋ฅผ ํ•˜๋ฉด ์žฌ์‚ฌ์šฉ์€ ๋”ฐ๋ผ์˜จ๋‹ค. ์ด ํ•œ ๋ฌธ์žฅ์œผ๋กœ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” ๊ตฌํ˜„ ๋‹น์‹œ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•˜๋А๋ผ ๋งŽ์€ ์‹œ๊ฐ„์„ ์†Œ๋น„ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•œ ํŽ˜์ด์ง€๋งŒ ๊ตฌํ˜„ํ•˜๋Š” ๋ถ„๋Ÿ‰์—์„œ ์–ผ๋งˆ๋‚˜ ์žฌ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•ด์„œ ์„ค๊ณ„๋ฅผ ํ•ด์•ผํ• ์ง€ ์ œ ์ƒ์ƒ์ด๋‚˜ ๊ธฐ์กด ๊ฒฝํ—˜์— ์˜์กดํ•ด์•ผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ œ ๊ฒฝํ—˜์ด ๊ธฐ์ค€์ด ๋  ์ˆ˜ ์žˆ๋Š” ์ง€๋„ ํ™•์‹ ์ด ์—†์—ˆ๊ธฐ ๋–„๋ฌธ์— ์ฒ˜์Œ์—๋Š” ํ•ด๋‹น ํŽ˜์ด์ง€ ํ•˜์œ„์—์„œ ์ตœ๋Œ€ํ•œ ๊ฐ€๊นŒ์šด ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ ธ ๊ฐ”์—ˆ๊ณ  ํ™•์žฅ์„ฑ ์œ„ํ•œ ์„ค๊ณ„๋ฅผ ์–ธ์  ๊ฐ€ ๋Œ€๊ทœ๋ชจ๋ฅผ ๋Œ€๋น„ํ•œ ์„ค๊ณ„๋กœ ์˜ค์ธํ•˜์—ฌ FSD์— ๊ฐ€๊นŒ์šด ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ•˜๊ฒŒ ๋์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ํ•ด๋‹น ํŽ˜์ด์ง€๋ง๊ณค ์žฌ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ž์›๋“ค์„ shared ์œ„์น˜๋กœ ์„ค์ •ํ•˜๋ฉด์„œ ๋ง์ด์ฃ .


๐Ÿ“ ํšŒ๊ณ 

๋ผ์ด๋ธŒ๋ฅผ ํ†ตํ•ด ๋“ค์—ˆ๋˜ ๋ชจ๋“  ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๊ณ  ์‹ถ์—ˆ์ง€๋งŒ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค.

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


๐Ÿ“ ๊ฐ™์ด ๋ณด๋ฉด ์ข‹์€ ์ž๋ฃŒ ๐Ÿงš


๐Ÿ“ (์ถ”๊ฐ€) ๋ฆฌํŒฉํ† ๋งํ•œ ๋‚ด์šฉ

0๊ฐœ์˜ ๋Œ“๊ธ€