ํ ์ค๊ฐ ์ค์ํ๊ฒ ์๊ฐํ๋ ๊ฒ์ ๋ฌด์์ผ๊น?
ํ ์ค๊ฐ ์ค์ํ๊ฒ ์๊ฐํ๋ ๊ฒ์ด ์ ์ค์ํ ๊น?
์ง์ ๊ณผ์ ๋ฅผ ํ๊ณ ํด์ค ๋ผ์ด๋ธ๋ฅผ ๋ณด๋ฉด์ ํ์ฅ์ฑ์ ์ํด ํ ์ค๋ ์ด๋ป๊ฒ ์ค๊ณํ๋์ง ์ดํด๋ณผ ์ ์๋ ๊ธฐํ๋ฅผ ๊ฐ์ก์ต๋๋ค!

์๋ ํ์ธ์! ํ ์ค ๋ชจ์๊ณ ์ฌ 1ํ์ ์ฐธ์ฌํ์ฌ ํ๊ณ ๋ฐ ํ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
์ฒ์์ ์ฐธ์ฌ ์ ์ฒญํ์ ๋ ๊ต์ฅํ ๊ธฐ์๊ธฐ๋ ํ์ง๋ง ํํธ ์์ํ์ต๋๋ค.
๐ญ ์ ๊ณ ๋ง๊ฒ ์ด๋ฐ ๊ฑธ ์ด์ด์ฃผ์ง? ๋ฐ์์คํ ๋ฐ..? ์ญ์ ํ ์ค๋ ์ธ๋ถ์ ์ํต์ ์ค์ํ๊ฒ ์ฌ๊ธฐ๋ ๊ตฌ๋! FE ๊ฐ๋ฐ ์ํ๊ณ์ ๋ฐ์ ์ ๋๋ชจํ๋๊ตฌ๋! (๊ทธ์น๋ง ๋ชจ์๊ณ ์ฌ ..?)
์ด ์จ๊ธธ ์ ์๋ ์๋ฌธ๋ค์ ์ ์จ ๋ค๋กํ ์ฑ ์ค๋ ๋ ๋ง์์ผ๋ก ์ฃผ๋ง๋์ ์ด์ฌํ ๊ณ ๋ฏผํ์ฌ ๊ณผ์ ๋ฅผ ์ ์ถํ๊ณ ํ์์ผ์ ํด์ค ๋ผ์ด๋ธ๋ฅผ ๋ค์์ต๋๋ค.
๋ผ์ด๋ธ ์์ํ์๋ง์ ์ด ์๋ฌธ์ ํด๊ฒฐํด ์ฃผ์ จ๋๋ฐ ํ ์ค ๊ณผ์ ์ ๋ํ ๋ฃจ๋จธ ๋๋ฌธ์ ์ฌ์ค(?) ๋ง๊ณ ์ถ์ ํ๊ฐ์์์ ์ถ์ ์๋๋ฅผ ์ง์ ๋ฐํ๊ฒ ๋ค๋ ์๋์๋ค๊ณ ํฉ๋๋ค! ๐
- ์๋น์ค์ ์ ์ง๋ณด์๋ ์ฅ๊ธฐ์ ์ธ ํ์ฅ์ฑ์ ๊ณ ๋ คํ ์ค๊ณ, ์ถ์ํ ๊ด์ ์ ์ง์คํด์ ๊ธฐ๋ฅ์ ๊ตฌํํด์ฃผ์ธ์.
- ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ณ ์ถ์ ์ง์ ์ด ์๋ค๋ฉด ๊ตฌํ์ ํฌํจ์ํค๊ฑฐ๋ README ๋ฑ์ ๋ฌธ์๋ก ๋จ๊ฒจ์ฃผ์ธ์.
- ์๋ฌธ์ ์ด ์๋ค๋ฉด ์ค์ค๋ก ํฉ๋ฆฌ์ ์ธ ๊ฐ์ค์ ์ธ์ฐ๊ณ ์งํํด์ฃผ์ธ์.
์ธ์ ๊น์ ๋ถ๋ถ ์ค ํ๋์์ต๋๋ค.
๊ตฌํ ์๊ตฌ์ฌํญ ์ ์์ฃผ ํฐ ๊ธ์จ๋ก ํ์ฅ์ฑ์ ๊ณ ๋ คํ ์ค๊ณ์ ์ถ์ํ ๊ด์ ์ ์ง์คํด์ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ผ๋ ์๊ตฌ์ฌํญ์ ๋ณด๊ณ ํ๋ก์ ํธ ๊ตฌ์กฐ ๋ถํฐ ๊ณ ๋ฏผ์ด ๋ค๊ธฐ ์์ํ์ต๋๋ค.
๊ณผ์ ๋ฅผ ์์ํ๊ณ 1~2์๊ฐ ๋์ ์ ๊ฐ ์ด์ฉํด ๋ณธ ๋ชจ๋ AI๋ฅผ ์ด ์ถ๋ํด์ ํ ๋ก ์ ํ์ต๋๋ค.
๐ญ ํ๋ก์ ํธ ๊ตฌ์กฐ๋ ์ด๋ป๊ฒ ์ก์๊น? ํ์ฅ์ฑ์๋ ๊ตฌ์กฐ๋ผ๋ฉด ์ญ์ FSD ์ผ๊น?
๐ญ ์ํฉ๊ณผ ํ๋ก์ ํธ ๊ท๋ชจ์ ๋ง๊ฒ ์ค๊ณํด์ผ ํ๋๋ฐ ์๊ตฌ์ฌํญ ์์ฒด๊ฐ ํ์ฅ์ฑ์๊ฒ ์ค๊ณํ๋ผ๊ณ ํ๋ค?
๐ญ ์ด๋ป๊ฒ ํ์ง? ํ ์ค๋ ๋ญ ์ํ๋ ๊ฑฐ์ง? ์ด๋ป๊ฒ ์ ๊ทผํ๋ ๊ฒ ๋ง๋ ๊ฑฐ์ง?
AI๋ค๊ณผ ํ ๋ก ๋์ ์ ๋ ๋ค์๊ณผ ๊ฐ์ ์ ๋ต์ ์ทจํ์ต๋๋ค.
์ด๊ธฐ์๋ YAGNI(You Aren't Gonna Need It) ์์น์ ๋ฐ๋ผ ๊ณผ๋ํ ์ค๊ณ๋ฅผ ์ง์ํ๊ณ ๊ตฌํ์ ์ง์คํ ๋ค, ์ฅ๊ธฐ์ ์ธ ํ์ฅ์ฑ์ ํ๋ณดํ๊ธฐ ์ํด ๋ฆฌํฉํ ๋ง์ ์งํํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
... ์ ๋ ์ฒ์๋ถํฐ ํ์ฅ์ฑ์ด๋ผ๋ ์๋ฏธ๋ฅผ ์ค์ธํด ํจ์ ์ ๋น ์ก์ ์ง๋ ๋ชจ๋ฆ ๋๋ค.
ํด์ค ๋ผ์ด๋ธ๋ฅผ ๋ณด๊ณ ๋๋ ๊ตฌ์กฐ๋ ๋ฐฉ๋ฒ๋ก ์์ฒด๋ ์ค์ํ์ง ์๋ค๋ ๊ฒ์ ๊นจ๋ฌ์์ต๋๋ค.
์ ์ผ ์ค์ํ ๊ฒ์ ์์ธก ๊ฐ๋ฅํ๊ณ ์ฝ๊ธฐ ์ฌ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ด ์์ต๋๋ค.
์ผ๋จ ์ ์ ์ ๊ทผ ๋ฐฉ๋ฒ์ ๋ํด์ ๋จผ์ ์ค๋ช ํด๋ณด์ง๋ง ๋ณ๋ก ์ค์ํ์ง ์์ต๋๋ค.
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); // ๐ค ...
๐ง ํ์ฅ์ฑ์ ๊ณ ๋ คํ๊ธฐ ์ํด ๋ฆฌํฉํ ๋ง์ ํ๊ฒ ๋ค๊ณ ์๊ฐํ์๋ง์ ๋ณด์ธ ๊ฑด ํญ์ด์์ต๋๋ค. ํญ ์ํ ๊ด๋ฆฌ์ ๋๋ฉ์ธ๊ณผ๋ ๊ด๊ณ๊ฐ ์์ผ๋ ๋ถ๋ฆฌํด์ผ ๋ ๊ฑฐ ๊ฐ์๋ฐ?
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
์ ๋ ๊ตฌํ์ ์ง์คํ ๋ค์ ๋ฆฌํฉํ ๋ง์ ํ๋ ๊ฒ์ผ๋ก ๋จ๊ณ๋ฅผ ์ชผ๊ฐ์ ์งํํ๋ ค ํ์ต๋๋ค.
๋ฌผ๋ก ๋ฌผ๊ณผ ๊ธฐ๋ฆ์ฒ๋ผ ๋ฆฌํฉํ ๋ง์ ๋ฑ ๋ถ๋ฆฌํด์ ํ์ง๋ ์์ง๋ง ๊ตณ์ด ๋จ๊ณ๋ฅผ ๋๋ก ์ชผ๊ฐ์ ์งํํ๊ฒ ๋ค๋ ๋ฐฉํฅ ์ค์ ์ ์ข์ ์ ๋ต์ด ์๋์๋ ๊ฒ ๊ฐ์ต๋๋ค.
์์ ๋ง์๋๋ ธ๋ค์ํผ ํ ์ค์์ ์๊ธฐํ ์ ์ง๋ณด์๋ฅผ ์ํ ํ์ฅ์ฑ ์๋ ์ค๊ณ๋ ์์ธก ๊ฐ๋ฅํ๊ณ ์ฝ๊ธฐ ์ฌ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ด ์ฃผ๋ ํฌ์ธํธ์์ต๋๋ค.
๋๋จธ์ง ๊ตฌ์กฐ๋ ๋ฐฉ๋ฒ๋ก ์ ์ํฉ์ ๋ง๊ฒ ์ ํํ๋ ๊ฒ์ด ์๊ณ , ๋ชจ์๊ณ ์ฌ์์ ์๊ตฌํ ํ์ฅ์ฑ์๋ ์ค๊ณ๊ฐ ๊ณผ์ ์ ํ ํ์ด์ง๋ฅผ ๊ตฌํํ์ง๋ง ์ธ์ ๊ฐ ๋๊ท๋ชจ๋ก ํ์ฅ๋ ๊ฒ์ ๋๋นํด์ ์ค๊ณํ๋ ๋ป์ด ์๋์์ต๋๋ค.
๊ธ๋ก ์ฐ๊ณ ๋๋ ๋น์ฐํ ์๊ธฐ ๊ฐ์ง๋ง ๋ง์ ์๊ตฌ์ฌํญ์ผ๋ก ์ฃผ์ด์ก์ ๋๋ ๊ต์ฅํ ๋ง์ ๊ณ ๋ฏผ์ด ๋ค๊ฒ ํ์์ต๋๋ค.
์ถ์ ์๋๋ฅผ ์ ํ์
ํ์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ด๊ฒ ์ฃ .
โ ๏ธ ์๋ ๋ด์ฉ์ ์ฝ๋๋ ์ ๋ต์ด ์๋๋ฉฐ ํด์ค ๋ผ์ด๋ธ ๋ด์ฉ์ ๋ค์ ๋ค์ ์ ์ฝ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ฑํด ๋ณธ ๊ฒ ๋ฟ์ ๋๋ค.
๊ฐ์ฅ ๋จผ์ ์ด์์ ์ธ ์ธํฐํ์ด์ค๋ฅผ ์ค๊ณํ์!
์์ฌ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ๊ณผ ๊ฐ์์ต๋๋ค.
ํ ํ์ด์ง๋ฅผ ๊ธฐ์ค์ผ๋ก ์ ๊ฐ ์๊ฐํ๋ ์ด์์ ์ธ ์ธํฐํ์ด์ค๋ฅผ ์ค๊ณํด ๋ณด๊ฒ ์ต๋๋ค.
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>
);
}
โ ๏ธ ์ด ๊ธ์ ์์ฑํ ์ฝ๋๋ ํ ์ค ๋ชจ์๊ณ ์ฌ ํด์ค ๋ผ์ด๋ธ์์ ์ ์ํ ์ ๋ต์ด ์๋๋ฉฐ, ๋ค์๋ ๋ด์ฉ์ ๋ณต๊ธฐํ๋ฉด์ ์ ๊ธฐ์ค์์ ์์ฑํด ๋ณธ ์ฝ๋์ ๋๋ค.
์ด์์ ์ด๋ผ๊ณ ์๊ฐํ ์ธํฐํ์ด์ค ์ค๊ณ๋ฅผ ํ๋์ฉ ์ ๊ณตํด์ค ์ปดํฌ๋ํธ๋ก ์ ์ฉํด ๋ด ๋๋ค.
<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 ์ปดํฌ๋ํธ๊ฐ ๋์ค๋ฉด ์๋๋ค๊ณ ํ์
จ๋ ๊ฑฐ ๊ฐ์์. (๋น์ทํ๊ฒ ๋ง์ํ์ ๊ฑฐ ๊ฐ์๋ฐ ์ ํํ ํํ์ ํ๋ฆด ์ ์์ต๋๋ค. ๐
)
์ง๊ธ ๋ชจ์๊ณ ์ฌ์ ๊ฒฝ์ฐ๋ ๋๋ผ๋ ์ปค๋
๋ช ๋, ๋ช ํธ ๋๋ ๋๊ตฌ ์ฌ๋ฌผํจ ์์น ์ ๋์ ๋ ๋ฒจ์ธ ๊ฑฐ ๊ฐ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ข ๋ ๋ง์ ๋ด์ฉ์ ์ ๋ฆฌํ๊ณ ์ถ์ง๋ง ๋ฆฌํฉํ ๋ง ํ ์ ์๋ ๊ธฐ๊ฐ์ด ์๊ฒจ์ ๊ทธ ์ดํ์ ์ด ํฌ์คํธ์ ์ถ๊ฐ๋ก ์ ๋ฐ์ดํธ ํ๊ฒ ์ต๋๋ค.
์ด ๋ด์ฉ๋ ์ธ์ ๊น์์ต๋๋ค. ๋ผ์ด๋ธ ๋ง์ง๋ง ์ฏค "์ฌ์ฌ์ฉํ ๊ฑฐ ๊ฐ์ผ๋ ๋ฐ๋ก ๋บ๊น์?" ๋ผ๋ ๋ฅ์ ๋ง์ ์ธ๊ธ ์ํ ์ด์ ์ ๋ํด ์ค๋ช ํ์ จ๋๋ฐ์.
์ฑ ์ ๋จ์๋ก ์ถ์ํ๋ฅผ ํ๋ฉด ์ฌ์ฌ์ฉ์ ๋ฐ๋ผ์จ๋ค. ์ด ํ ๋ฌธ์ฅ์ผ๋ก ์ ๋ฆฌํ์ต๋๋ค.
์ ๋ ๊ตฌํ ๋น์ ํ๋ก์ ํธ ๊ตฌ์กฐ์ ๋ํด ๊ณ ๋ฏผํ๋๋ผ ๋ง์ ์๊ฐ์ ์๋นํ์ต๋๋ค.
ํ ํ์ด์ง๋ง ๊ตฌํํ๋ ๋ถ๋์์ ์ผ๋ง๋ ์ฌ์ฌ์ฉ์ ๊ณ ๋ คํด์ ์ค๊ณ๋ฅผ ํด์ผํ ์ง ์ ์์์ด๋ ๊ธฐ์กด ๊ฒฝํ์ ์์กดํด์ผ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ํ์ง๋ง ์ ๊ฒฝํ์ด ๊ธฐ์ค์ด ๋ ์ ์๋ ์ง๋ ํ์ ์ด ์์๊ธฐ ๋๋ฌธ์ ์ฒ์์๋ ํด๋น ํ์ด์ง ํ์์์ ์ต๋ํ ๊ฐ๊น์ด ๊ตฌ์กฐ๋ฅผ ๊ฐ์ ธ ๊ฐ์๊ณ ํ์ฅ์ฑ ์ํ ์ค๊ณ๋ฅผ ์ธ์ ๊ฐ ๋๊ท๋ชจ๋ฅผ ๋๋นํ ์ค๊ณ๋ก ์ค์ธํ์ฌ FSD์ ๊ฐ๊น์ด ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝํ๊ฒ ๋์ต๋๋ค. ์ค์ ๋ก ํด๋น ํ์ด์ง๋ง๊ณค ์ฌ์ฌ์ฉํ์ง ์๋ ์์๋ค์ shared ์์น๋ก ์ค์ ํ๋ฉด์ ๋ง์ด์ฃ .
๋ผ์ด๋ธ๋ฅผ ํตํด ๋ค์๋ ๋ชจ๋ ๋ด์ฉ์ ์ ๋ฆฌํ๊ณ ์ถ์์ง๋ง ์์ธํ ๋ด์ฉ์ ๋ฆฌํฉํ ๋ง์ ์งํํ๋ฉด์ ์ ๋ฆฌํ ์ ์์ ๊ฑฐ ๊ฐ์ต๋๋ค.
ํ ์ค ๊ธฐ์ ๊ณผ์ ์ ํด์ค์ ๋ค์ ์ ์๋ ์์ฃผ ํน๋ณํ ๊ฒฝํ์ ํ๊ฒ ๋ผ์ ์ข์ ์๊ฐ์ด์๊ณ ๋๋ฌด๋ ์ ์ตํ์ต๋๋ค. ํ์ฅ์ฑ, ์ถ์ํ ์ฌ์ค ์ฝ์ง ์์๊ธฐ์ ์ค์ ๋ก ๊ตฌํ ์ ํ ์ค๋ ์ด๋ค ๊ด์ ์ผ๋ก, ์ด๋ป๊ฒ ์ถ์ํ ํ๋์ง ์์ธํ ์ ์ ์์ด ๋ง์ ๋์์ด ๋ ์๊ฐ์ด์์ต๋๋ค. ๊ฐ์ฌํฉ๋๋ค!