diff --git a/June/article/Type-safe_module_mocking_in_Storybook.md b/June/article/Type-safe_module_mocking_in_Storybook.md new file mode 100644 index 0000000..d3aec20 --- /dev/null +++ b/June/article/Type-safe_module_mocking_in_Storybook.md @@ -0,0 +1,273 @@ +## πŸ”— [Type-safe module mocking in Storybook](https://storybook.js.org/blog/type-safe-module-mocking/?utm_source=newsletter.reactdigest.net&utm_medium=newsletter&utm_campaign=sneaky-react-memory-leaks) + +### πŸ—“οΈ λ²ˆμ—­ λ‚ μ§œ: 2024.06.03 + +### 🧚 λ²ˆμ—­ν•œ 크루: 러기(λ°•μ •μš°) + +--- + +## μŠ€ν† λ¦¬λΆμ—μ„œμ˜ Type-safe λͺ¨λ“ˆ λͺ¨ν‚Ή + +UIλ₯Ό λΆ„λ¦¬ν•˜μ—¬ κ°œλ°œν•˜κ³  ν…ŒμŠ€νŠΈν•˜λŠ” 데 μžˆμ–΄ 일관성은 맀우 μ€‘μš”ν•©λ‹ˆλ‹€. + +μ΄μƒμ μœΌλ‘œ, μŠ€ν† λ¦¬λΆμ˜ μŠ€ν† λ¦¬λŠ” λˆ„κ°€ μ–Έμ œ 보든, λ°±μ—”λ“œκ°€ μž‘λ™ν•˜κ³  μžˆλ“  μ•„λ‹ˆλ“  항상 λ™μΌν•œ UIλ₯Ό λ Œλ”λ§ν•΄μ•Ό ν•©λ‹ˆλ‹€. μŠ€ν† λ¦¬μ— 주어진 μž…λ ₯은 항상 λ™μΌν•œ 좜λ ₯ κ²°κ³Όλ₯Ό κ°€μ Έμ•Ό ν•©λ‹ˆλ‹€. + +μ΄λŠ” UI의 μž…λ ₯이 μ»΄ν¬λ„ŒνŠΈμ— μ „λ‹¬λ˜λŠ” props만 μžˆμ„ λ•ŒλŠ” λ‹¨μˆœν•©λ‹ˆλ‹€. μ»΄ν¬λ„ŒνŠΈκ°€ μ»¨ν…μŠ€νŠΈ μ œκ³΅μžλ‘œλΆ€ν„° 데이터λ₯Ό μ˜μ‘΄ν•˜λŠ” 경우, 이λ₯Ό λͺ¨ν‚Ήν•˜κΈ° μœ„ν•΄ μŠ€ν† λ¦¬λ₯Ό Decorator둜 κ°μ‹Έμ„œ λͺ¨ν‚Ήν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ„€νŠΈμ›Œν¬μ—μ„œ κ°€μ Έμ˜¨ μž…λ ₯이 μžˆλŠ” UI의 경우, λ„€νŠΈμ›Œν¬ μš”μ²­μ„ κ²°μ •λ‘ μ μœΌλ‘œ λͺ¨μ˜ν•˜λŠ” 맀우 인기 μžˆλŠ” Mock Service Worker μ• λ“œμ˜¨μ΄ μžˆμŠ΅λ‹ˆλ‹€. + +κ·Έλ ‡λ‹€λ©΄ μ»΄ν¬λ„ŒνŠΈκ°€ λΈŒλΌμš°μ € API와 같은 λ‹€λ₯Έ μΆœμ²˜μ— μ˜μ‘΄ν•˜λŠ” κ²½μš°λŠ” μ–΄λ–»κ²Œ λ κΉŒμš”? 예λ₯Ό λ“€μ–΄ μ‚¬μš©μžμ˜ ν…Œλ§ˆ μ„ ν˜Έλ„, 둜컬 μŠ€ν† λ¦¬μ§€μ˜ 데이터, λ˜λŠ” μΏ ν‚€λ₯Ό μ½λŠ” 경우, ν˜Ήμ€ μ»΄ν¬λ„ŒνŠΈκ°€ ν˜„μž¬ λ‚ μ§œλ‚˜ μ‹œκ°„μ— 따라 λ‹€λ₯΄κ²Œ λ™μž‘ν•˜λŠ” 경우, μ•„λ‹ˆλ©΄ μ»΄ν¬λ„ŒνŠΈκ°€ Next.js의 next/router와 같은 메타 ν”„λ ˆμž„μ›Œν¬ APIλ₯Ό μ‚¬μš©ν•˜λŠ” κ²½μš°λŠ” μ–΄λ–¨κΉŒμš”? + +μ΄λŸ¬ν•œ μœ ν˜•μ˜ μž…λ ₯을 λͺ¨ν‚Ήν•˜λŠ” 것은 μ—­μ‚¬μ μœΌλ‘œ μŠ€ν† λ¦¬λΆμ—μ„œ μ–΄λ €μ› μŠ΅λ‹ˆλ‹€. 그리고 λ°”λ‘œ 였늘 μš°λ¦¬κ°€ λͺ¨λ“ˆ λͺ¨ν‚Ήμ„ 톡해 ν•΄κ²°ν•˜κ³  μžˆλŠ” λ¬Έμ œμž…λ‹ˆλ‹€! 우리의 μ ‘κ·Ό 방식은 κ°„λ‹¨ν•˜κ³ , νƒ€μž… μ•ˆμ „ν•˜λ©°, ν‘œμ€€ κΈ°λ°˜μž…λ‹ˆλ‹€. 그것은 뢈투λͺ…ν•˜κ±°λ‚˜ 독점적인 λͺ¨λ“ˆ API보닀 λͺ…ν™•μ„±κ³Ό λ””λ²„κΉ…μ˜ λͺ…확성을 μ„ ν˜Έν•©λ‹ˆλ‹€. +_그리고 μš°λ¦¬λŠ” 쒋은 νšŒμ‚¬μ— μžˆμŠ΅λ‹ˆλ‹€: Epic Stack의 창쑰자 Kent C. DoddλŠ” μ ˆλŒ€μ μΈ μˆ˜μž…κ³Ό React μ„œλ²„ μ»΄ν¬λ„ŒνŠΈ μ•„ν‚€ν…νŠΈ Seb MarkbΓ₯geκ°€ μŠ€ν† λ¦¬λΆ λͺ¨ν‚Ήμ— 직접적인 μ˜κ°μ„ μ£Όμ—ˆλ‹€κ³  μΆ”μ²œν•©λ‹ˆλ‹€._ + + + +## λͺ¨λ“ˆ λͺ¨ν‚Ήμ΄ λ¬΄μ—‡μΈκ°€μš”? + +λͺ¨λ“ˆ λͺ¨ν‚Ήμ€ μ»΄ν¬λ„ŒνŠΈκ°€ μ§μ ‘μ μ΄κ±°λ‚˜ κ°„μ ‘μ μœΌλ‘œ κ°€μ Έμ˜€λŠ” λͺ¨λ“ˆμ„ μΌκ΄€λ˜κ³  독립적인 λŒ€μ²΄λ¬Όλ‘œ κ΅μ²΄ν•˜λŠ” κΈ°μˆ μž…λ‹ˆλ‹€. μœ λ‹› ν…ŒμŠ€νŠΈμ—μ„œλŠ” 이 기술이 μ½”λ“œλ₯Ό μž¬ν˜„ κ°€λŠ₯ν•œ μƒνƒœλ‘œ ν…ŒμŠ€νŠΈν•˜λŠ” 데 도움을 쀄 수 μžˆμŠ΅λ‹ˆλ‹€. μŠ€ν† λ¦¬λΆμ—μ„œλŠ” 이λ₯Ό μ‚¬μš©ν•˜μ—¬ 데이터λ₯Ό ν₯미둜운 λ°©μ‹μœΌλ‘œ κ²€μƒ‰ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈλ₯Ό λ Œλ”λ§ν•˜κ³  ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +예λ₯Ό λ“€μ–΄, μ‚¬μš©μžκ°€ ν‘œμ‹œν•  정보λ₯Ό 선택할 수 있고 κ·Έ 섀정을 λΈŒλΌμš°μ €μ˜ 둜컬 μŠ€ν† λ¦¬μ§€μ— μ €μž₯ν•˜λŠ” μ‚¬μš©μž μ„€μ • κ°€λŠ₯ν•œ λŒ€μ‹œλ³΄λ“œ μ»΄ν¬λ„ŒνŠΈλ₯Ό κ³ λ €ν•΄ λ³΄μ„Έμš”. + +![alt text](https://storybookblog.ghost.io/content/images/size/w1600/2024/05/Dashboard.png) + +이것은 μ‚¬μš©μžμ˜ 섀정을 둜컬 μŠ€ν† λ¦¬μ§€μ— 읽고 μ“°λŠ” μ„€μ • 데이터 μ ‘κ·Ό κ³„μΈ΅μœΌλ‘œ κ΅¬ν˜„λ˜λ©°, UIλ₯Ό λ‹΄λ‹Ήν•˜λŠ” λ””μŠ€ν”Œλ ˆμ΄ μ»΄ν¬λ„ŒνŠΈμΈ λŒ€μ‹œλ³΄λ“œ μ˜ˆμ‹œμž…λ‹ˆλ‹€: + +```tsx +// lib/settings.ts +export const getDashboardLayout = () => { + const layout = window.localStorage.getItem("dashboard.layout"); + return layout ? parseLayout(layout) : []; +}; +``` + +```tsx +// components/Dashboard.tsx +import { getDashboardLayout } from "../lib/settings.ts"; + +export const Dashboard = (props) => { + const layout = getDashboardLayout(); + // logic to display layout +}; +``` + +λŒ€μ‹œλ³΄λ“œ μ»΄ν¬λ„ŒνŠΈλ₯Ό ν…ŒμŠ€νŠΈν•˜κΈ° μœ„ν•΄, μš°λ¦¬λŠ” 핡심 μƒνƒœλ₯Ό λ‹€λ£¨λŠ” λ‹€μ–‘ν•œ λ ˆμ΄μ•„μ›ƒμ˜ 예제 μ„ΈνŠΈλ₯Ό λ§Œλ“€κ³ μž ν•©λ‹ˆλ‹€. κ°„λ‹¨ν•˜κ²Œ ν•˜κΈ° μœ„ν•΄μ„œ, μΌλ°˜μ„±μ„ μžƒμ§€ μ•ŠλŠ” μ„ μ—μ„œ, μš°λ¦¬λŠ” λ ˆμ΄μ•„μ›ƒμ„ μ½λŠ” λΆ€λΆ„μ—λ§Œ 집쀑할 κ²ƒμž…λ‹ˆλ‹€. + +이 κΈ€μ—μ„œλŠ” λͺ¨λ“ˆ λͺ¨ν‚Ήμ„ μ„€λͺ…ν•˜κ³ , μš°λ¦¬κ°€ μ–΄λ–»κ²Œ 그것을 λ‹¬μ„±ν•˜λŠ”μ§€, 그리고 우리의 접근법이 λ‹€λ₯Έ κ΅¬ν˜„κ³Ό λΉ„κ΅ν–ˆμ„ λ•Œ μ–΄λ–€ 이점이 μžˆλŠ”μ§€λ₯Ό μ„€λͺ…ν•˜λŠ” μ‹€ν–‰ 예제둜 이λ₯Ό μ‚¬μš©ν•  κ²ƒμž…λ‹ˆλ‹€. + +## κΈ°μ‘΄ μ ‘κ·Ό 방식: 독점 API + +Jest 및 Vitest와 같은 인기 μžˆλŠ” μœ λ‹› ν…ŒμŠ€νŠΈ 도ꡬ듀은 λͺ¨λ“ˆ λͺ¨ν‚Ήμ„ μœ„ν•œ μœ μ—°ν•œ λ©”μ»€λ‹ˆμ¦˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€. 이듀은 μΈμ ‘ν•œ mocks λ””λ ‰ν† λ¦¬μ—μ„œ λͺ¨ν‚Ή νŒŒμΌμ„ μžλ™μœΌλ‘œ μ°ΎλŠ” μ˜ˆμ‹œμž…λ‹ˆλ‹€: + +```tsx +// lib/__mocks__/settings.ts +export const getDashboardLayout = () => [ + /* dummy data here */ +]; +``` + +λ˜λŠ”, ν…ŒμŠ€νŠΈ 파일 λ‚΄μ—μ„œ λͺ¨ν‚Ήλ₯Ό μ„ μ–Έν•˜κΈ° μœ„ν•΄ λͺ…λ Ήν˜• APIλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€: + +```tsx +// components/Dashboard.test.ts +import { vi, fn } from 'vitest'; +import { getDashboardLayout } from '../lib/settings.ts'; + +vi.mock('../lib/settings.ts', () => ({ + getDashboardLayout: fn(() => ([ /* dummy data here */])), +}); +``` + +이 APIλŠ” 간단해 λ³΄μ΄μ§€λ§Œ, μ‹€μ œλ‘œλŠ” κ°€μ Έμ˜€κΈ°λ₯Ό λͺ¨ν‚ΉμœΌλ‘œ λŒ€μ²΄ν•˜κΈ° μœ„ν•΄ λ³΅μž‘ν•˜κ³  λ‹€μ†Œ λ§ˆλ²• 같은 파일 λ³€ν™˜μ„ μœ λ°œν•©λ‹ˆλ‹€. 결과적으둜 μ½”λ“œμ˜ μž‘μ€ 변경이 λͺ¨ν‚Ήμ„ ν˜Όλž€μŠ€λŸ½κ²Œ λ§Œλ“œλŠ” λ°©μ‹μœΌλ‘œ 깨뜨릴 수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, λ‹€μŒκ³Ό 같은 λ³€ν˜•μ€ μ‹€νŒ¨ν•©λ‹ˆλ‹€: + +```tsx +// components/Dashboard.test.ts +import { vi, fn } from 'vitest'; +import { getDashboardLayout } from '../lib/settings.ts'; + +const dummyLayout = [ /* dummy data here */]; +vi.mock('../lib/settings.ts', () => ({ + getDashboardLayout: fn(() => dummyLayout), // FAIL!!! +}); +``` + +ν•˜μ§€λ§Œ 우리의 λͺ©ν‘œλŠ” 이 λ›°μ–΄λ‚œ 도ꡬ듀을 λΉ„νŒν•˜λŠ” 것이 μ•„λ‹™λ‹ˆλ‹€. 였히렀, μš°λ¦¬λŠ” μƒˆλ‘­κ³  ν‘œμ€€ 기반의 μ ‘κ·Ό 방식을 μ‚¬μš©ν•˜μ—¬ 더 λ‚˜μ€ λͺ¨ν‚Ήμ„ μ–΄λ–»κ²Œ ν•  수 μžˆλŠ”μ§€ νƒκ΅¬ν•˜κ³ μž ν•©λ‹ˆλ‹€. + +## 우리의 μ ‘κ·Ό 방식: Subpath Imports + +μŠ€ν† λ¦¬λΆμ—μ„œμ˜ λͺ¨λ“ˆ λͺ¨ν‚Ήμ€ μ„œλΈŒνŒ¨μŠ€ μž„ν¬νŠΈ ν‘œμ€€μ„ ν™œμš©ν•˜λ©°, μ΄λŠ” `package.json의 imports` ν•„λ“œλ₯Ό 톡해 μ„€μ • κ°€λŠ₯ν•©λ‹ˆλ‹€ β€” λͺ¨λ“  JS ν”„λ‘œμ νŠΈμ˜ 심μž₯λΆ€μž…λ‹ˆλ‹€ β€” ν”„λ‘œμ νŠΈ 전체에 걸쳐 λͺ¨μ˜λ₯Ό κ°€μ Έμ˜€λŠ” νŒŒμ΄ν”„λΌμΈ 역할을 ν•©λ‹ˆλ‹€. + +이 μ ‘κ·Ό λ°©μ‹μ˜ ν•˜λ‚˜μ˜ μŠˆνΌνŒŒμ›ŒλŠ” `package.json exports`와 λ§ˆμ°¬κ°€μ§€λ‘œ, `package.json imports`도 μ‘°κ±΄λΆ€λ‘œ λ§Œλ“€ 수 μžˆλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. 즉, λŸ°νƒ€μž„ ν™˜κ²½μ— 따라 κ°€μ Έμ˜€λŠ” 경둜λ₯Ό λ‹€λ₯΄κ²Œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” μŠ€ν† λ¦¬λΆμ—μ„œλŠ” λͺ¨μ˜λœ λͺ¨λ“ˆμ„ κ°€μ Έμ˜€κ³ , λ‹€λ₯Έ κ³³μ—μ„œλŠ” μ‹€μ œ λͺ¨λ“ˆμ„ κ°€μ Έμ˜€λ„λ‘ package.json을 맞좀 μ„€μ •ν•  수 μžˆλ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€! + +μ„œλΈŒνŒ¨μŠ€ μž„ν¬νŠΈλŠ” μ²˜μŒμ— Node.jsμ—μ„œ λ„μž…λ˜μ—ˆμ§€λ§Œ, μ΄μ œλŠ” JS μƒνƒœκ³„ μ „λ°˜μ—μ„œ μ§€μ›λ˜κ³  있으며, TypeScript(버전 5.4λΆ€ν„°), Webpack, Vite, Jest, Vitest λ“±μ—μ„œλ„ μ§€μ›λ©λ‹ˆλ‹€. + +μœ„μ˜ 예제λ₯Ό κ³„μ†ν•΄μ„œ, λ‹€μŒμ€ `./lib/settings.ts`μ—μ„œ λͺ¨λ“ˆμ„ λͺ¨ν‚Ήν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€: + +```tsx +{ + "imports": { + "#lib/settings": { + "storybook": "./lib/settings.mock.ts", + "default": "./lib/settings.ts" + }, + "#*": [ // fallback for non-mocked absolute imports + "./*", + "./*.ts", + "./*.tsx" + ] + } +} +``` + +μ—¬κΈ°μ„œ μš°λ¦¬λŠ” λͺ¨λ“ˆ λ¦¬μ‘Έλ²„μ—κ²Œ λͺ¨λ“  `#lib/settings`μ—μ„œμ˜ κ°€μ Έμ˜€κΈ°κ°€ μŠ€ν† λ¦¬λΆμ—μ„œλŠ” `../lib/settings.mock.ts`둜, μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œλŠ” `../lib/settings.ts`둜 ν•΄μ„λ˜λ„λ‘ μ§€μ‹œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. + +μ΄λŠ” λ˜ν•œ Node.js 사양에 따라 #-기호둜 μ‹œμž‘ν•˜λŠ” μ ˆλŒ€ κ²½λ‘œμ—μ„œ κ°€μ Έμ˜€λ„λ‘ μ»΄ν¬λ„ŒνŠΈλ₯Ό μˆ˜μ •ν•΄μ•Ό ν•˜λ©°, μ΄λŠ” κ²½λ‘œλ‚˜ νŒ¨ν‚€μ§€ κ°€μ Έμ˜€κΈ°μ™€ κ΄€λ ¨λœ λͺ¨ν˜Έμ„±μ΄ 없도둝 보μž₯ν•©λ‹ˆλ‹€. + +```tsx +// Dashboard.test.ts + +- import { getDashboardLayout } from '../lib/settings'; ++ import { getDashboardLayout } from '#lib/settings'; +``` + +이것은 λ‹€μ†Œ λ²ˆκ±°λ‘œμ›Œ 보일 수 μžˆμ§€λ§Œ, λŸ°νƒ€μž„μ— 따라 λͺ¨λ“ˆμ΄ λ‹¬λΌμ§ˆ 수 μžˆλ‹€λŠ” 것을 νŒŒμΌμ„ μ½λŠ” κ°œλ°œμžλ“€μ—κ²Œ λͺ…ν™•ν•˜κ²Œ μ „λ‹¬ν•˜λŠ” μž₯점이 μžˆμŠ΅λ‹ˆλ‹€. μ‹€μ œλ‘œ, λͺ¨ν‚Ήμ„ μœ„ν•΄ ν›Œλ₯­ν•œ λͺ¨λ“  μ΄μœ λ“€λ‘œ 인해, 일반적으둜 μ ˆλŒ€ κ°€μ Έμ˜€κΈ°μ— λŒ€ν•΄ 이 ν‘œμ€€μ„ ꢌμž₯ν•©λ‹ˆλ‹€(μ•„λž˜ μ°Έμ‘°). + +## μŠ€ν† λ¦¬λ³„ λͺ¨ν‚Ή + +Subpath Importλ₯Ό μ‚¬μš©ν•˜μ—¬, μš°λ¦¬λŠ” `settings.ts` 파일 전체λ₯Ό ν‘œμ€€ 기반 μ ‘κ·Ό 방식을 μ‚¬μš©ν•˜λŠ” μƒˆ λͺ¨λ“ˆλ‘œ ꡐ체할 수 μžˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ 각 ν…ŒμŠ€νŠΈ(λ˜λŠ” 우리의 경우, μŠ€ν† λ¦¬λΆ μŠ€ν† λ¦¬)λ§ˆλ‹€ κ·Έ κ΅¬ν˜„μ„ λ‹¬λ¦¬ν•˜κ³  μ‹Άλ‹€λ©΄ `settings.mock.ts`λ₯Ό μ–΄λ–»κ²Œ ꡬ쑰화해야 ν• κΉŒμš”? + +λ‹€μŒμ€ μ–΄λ–€ λͺ¨λ“ˆμ΄λ“  λͺ¨ν‚Ήν•˜κΈ° μœ„ν•œ λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈ κ΅¬μ‘°μž…λ‹ˆλ‹€. μ½”λ“œλ₯Ό μ™„μ „νžˆ μ œμ–΄ν•  수 있기 λ•Œλ¬Έμ—, νŠΉλ³„ν•œ 상황에 맞게 μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€(예λ₯Ό λ“€μ–΄, λΈŒλΌμš°μ €μ—μ„œ μ‹€ν–‰λ˜μ§€ μ•Šλ„λ‘ λ…Έλ“œ μ½”λ“œλ₯Ό μ œκ±°ν•˜κ±°λ‚˜ κ·Έ λ°˜λŒ€μ˜ 경우). + +```tsx +// lib/settings.mock.ts +import { fn } from "@storybook/test"; +import * as actual from "./settings"; // πŸ‘ˆ Import the actual implementation + +// πŸ‘‡ Re-export the actual implementation. +// This catch-all ensures that the exports of the mock file always contains +// all the exports of the original. It is up to the user to override +// individual exports below as appropriate. +export * from "./settings"; + +// πŸ‘‡ Export a mock function whose default implementation is the actual implementation. +// With a useful mockName, it displays nicely in Storybook's Actions addon +// for debugging. +export const getDashboardLayout = fn(actual.getDashboardLayout).mockName("settings::getDashboardLayout"); +``` + +이 λͺ¨μ˜ νŒŒμΌμ€ 이제 #lib/settings이 κ°€μ Έμ™€μ§ˆ λ•Œλ§ˆλ‹€ μŠ€ν† λ¦¬λΆμ—μ„œ μ‚¬μš©λ©λ‹ˆλ‹€. μ‹€μ œ κ΅¬ν˜„μ„ κ°μ‹ΈλŠ” 것 μ™Έμ—λŠ” 아직 λ§Žμ€ 것을 ν•˜μ§€ μ•Šμ§€λ§Œ β€” 그것이 μ€‘μš”ν•œ λΆ€λΆ„μž…λ‹ˆλ‹€. + +이제 μŠ€ν† λ¦¬λΆ μŠ€ν† λ¦¬μ—μ„œ μ‚¬μš©ν•΄ λ΄…μ‹œλ‹€: + +```tsx +// components/Dashboard.stories.ts + +import type { Meta, StoryObj } from "@storybook/react"; +import { expect } from "@storybook/test"; + +// πŸ‘‡ You can use subpaths as an absolute import convention even +// for non-conditional paths +import { Dashboard } from "#components/Dashboard"; + +// πŸ‘‡ Import the mock file explicitly, as that will make +// TypeScript understand that these exports are the mock functions +import { getDashboardLayout } from "#lib/settings.mock"; + +const meta = { + component: Dashboard, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Empty: Story = { + beforeEach: () => { + // πŸ‘‡ Mock return an empty layout + getDashboardLayout.mockReturnValue([]); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + // πŸ‘‡ Expect the UI to prompt when the dashboard is empty + await expect(canvas).toHaveTextContent("Configure your dashboard"); + // πŸ‘‡ Assert directly on the mock function that it was called as expected + expect(getDashboardLayout).toHaveBeenCalled(); + }, +}; + +export const Row: Story = { + beforeEach: () => { + // πŸ‘‡ Mock return a different, story-specific layout + getDashboardLayout.mockReturnValue([ + /* hard-coded "row" layout data */ + ]); + }, +}; +``` + +μŠ€ν† λ¦¬λΆμ—μ„œ 'fn' λͺ¨μ˜ ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λŠ” 것은 λ‹€μŒμ„ μ˜λ―Έν•©λ‹ˆλ‹€: + +1. μŠ€ν† λ¦¬λΆμ˜ μƒˆλ‘œμš΄ beforeEach 훅을 μ‚¬μš©ν•˜μ—¬ 각 μŠ€ν† λ¦¬μ— λŒ€ν•œ λ™μž‘μ„ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +2. ν•¨μˆ˜κ°€ 호좜될 λ•Œλ§ˆλ‹€ Actions νŒ¨λ„μ΄ 이λ₯Ό κΈ°λ‘ν•©λ‹ˆλ‹€. +3. play ν•¨μˆ˜μ—μ„œ ν˜ΈμΆœμ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. + + + +### λͺ…μ‹œμ  + +λͺ¨ν‚Ή ν”„λ ˆμž„μ›Œν¬μ˜ 일뢀 λ§ˆλ²•κ³Ό 같은 κΈ°λŠ₯은 λͺ¨ν‚Ήμ΄ μ–΄λ–»κ²Œ 그리고 μ–Έμ œ μ μš©λ˜λŠ”μ§€ μ΄ν•΄ν•˜κΈ° μ–΄λ ΅κ²Œ λ§Œλ“­λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, vi.mock 호좜 λ‚΄μ—μ„œ μ™ΈλΆ€ μ •μ˜ λ³€μˆ˜λ₯Ό μ°Έμ‘°ν•˜λŠ” 것이 μœ νš¨ν•œ μžλ°”μŠ€ν¬λ¦½νŠΈμž„μ—λ„ λΆˆκ΅¬ν•˜κ³  λͺ¨ν‚Ή 였λ₯˜λ₯Ό μΌμœΌν‚€λŠ” 것을 λ³΄μ•˜μŠ΅λ‹ˆλ‹€. + +λ°˜λ©΄μ— λͺ¨λ“  λͺ¨ν‚Ήμ„ package.jsonμ—μ„œ λͺ…μ‹œμ μœΌλ‘œ μ •μ˜ν•¨μœΌλ‘œμ¨, 우리의 μ†”λ£¨μ…˜μ€ λ‹€μ–‘ν•œ ν™˜κ²½μ—μ„œ λͺ¨λ“ˆμ΄ μ–΄λ–»κ²Œ ν•΄κ²°λ˜λŠ”μ§€ μ΄ν•΄ν•˜λŠ” λͺ…ν™•ν•˜κ³  예츑 κ°€λŠ₯ν•œ 방법을 μ œκ³΅ν•©λ‹ˆλ‹€. μ΄λŸ¬ν•œ 투λͺ…성은 디버깅을 λ‹¨μˆœν™”ν•˜κ³  ν…ŒμŠ€νŠΈλ₯Ό 더 예츑 κ°€λŠ₯ν•˜κ²Œ λ§Œλ“­λ‹ˆλ‹€. + +### Type-Safe + +λͺ¨ν‚Ή ν”„λ ˆμž„μ›Œν¬λŠ” κ°œλ°œμžκ°€ μ΅μˆ™ν•΄μ Έμ•Ό ν•  κ΄€λ‘€, 문법 μŠ€νƒ€μΌ, νŠΉμ • APIλ₯Ό λ„μž…ν•©λ‹ˆλ‹€. λ˜ν•œ, μ΄λŸ¬ν•œ μ†”λ£¨μ…˜λ“€μ€ μ’…μ’… νƒ€μž… 검사λ₯Ό μ§€μ›ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. + +기쑴의 package.json을 μ‚¬μš©ν•¨μœΌλ‘œμ¨ 우리의 μ†”λ£¨μ…˜μ€ μ΅œμ†Œν•œμ˜ 섀정이 ν•„μš”ν•©λ‹ˆλ‹€. λ˜ν•œ, TypeScriptκ°€ 이제 package.json의 μ„œλΈŒνŒ¨μŠ€ μž„ν¬νŠΈλ₯Ό μžλ™μ™„μ„±μœΌλ‘œ 지원함에 따라(2024λ…„ 3μ›” TypeScript 5.4 버전뢀터) μžμ—°μŠ€λŸ½κ²Œ TypeScript와 ν†΅ν•©λ©λ‹ˆλ‹€. + +### ν‘œμ€€ 기반 + +κ°€μž₯ μ€‘μš”ν•œ 것은 μŠ€ν† λ¦¬λΆμ˜ μ ‘κ·Ό 방식이 100% ν‘œμ€€ 기반이기 λ•Œλ¬Έμ—, μ–΄λ–€ 도ꡬ μ²΄μΈμ΄λ‚˜ ν™˜κ²½μ—μ„œλ„ λͺ¨ν‚Ήμ„ μ‚¬μš©ν•  수 μžˆλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. + +μ΄λŠ” ν‘œμ€€μ„ 배우고 κ·Έ 지식을 μ–΄λ””μ—μ„œλ‚˜ μž¬μ‚¬μš©ν•  수 있게 ν•΄μ£Όλ―€λ‘œ μœ μš©ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, vi.mock μ‚¬μš©λ²•μ€ Jest의 λͺ¨ν‚Ήκ³Ό λΉ„μŠ·ν•˜μ§€λ§Œ λ™μΌν•˜μ§€λŠ” μ•ŠμŠ΅λ‹ˆλ‹€. + +λ˜ν•œ μ—¬λŸ¬ 도ꡬλ₯Ό ν•¨κ»˜ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μ‚¬μš©μžκ°€ μ»΄ν¬λ„ŒνŠΈμ— λŒ€ν•œ μŠ€ν† λ¦¬λ₯Ό μž‘μ„±ν•œ λ‹€μŒ, 우리의 Portable Stories κΈ°λŠ₯을 μ‚¬μš©ν•˜μ—¬ κ·Έ μŠ€ν† λ¦¬λ₯Ό λ‹€λ₯Έ ν…ŒμŠ€νŠΈ λ„κ΅¬μ—μ„œ μž¬μ‚¬μš©ν•˜λŠ” 것이 μΌλ°˜μ μž…λ‹ˆλ‹€. + +κ°€μž₯ μ€‘μš”ν•œ 것은 μŠ€ν† λ¦¬λΆμ˜ μ ‘κ·Ό 방식이 100% ν‘œμ€€ 기반이기 λ•Œλ¬Έμ—, μ–΄λ–€ 도ꡬ μ²΄μΈμ΄λ‚˜ ν™˜κ²½μ—μ„œλ„ λͺ¨ν‚Ήμ„ μ‚¬μš©ν•  수 μžˆλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. + +μ΄λŠ” ν‘œμ€€μ„ 배우고 κ·Έ 지식을 μ–΄λ””μ—μ„œλ‚˜ μž¬μ‚¬μš©ν•  수 있게 ν•΄μ£Όλ―€λ‘œ μœ μš©ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, vi.mock μ‚¬μš©λ²•μ€ Jest의 λͺ¨ν‚Ήκ³Ό λΉ„μŠ·ν•˜μ§€λ§Œ λ™μΌν•˜μ§€λŠ” μ•ŠμŠ΅λ‹ˆλ‹€. + +λ˜ν•œ μ—¬λŸ¬ 도ꡬλ₯Ό ν•¨κ»˜ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μ‚¬μš©μžκ°€ μ»΄ν¬λ„ŒνŠΈμ— λŒ€ν•œ μŠ€ν† λ¦¬λ₯Ό μž‘μ„±ν•œ λ‹€μŒ, 우리의 Portable Stories κΈ°λŠ₯을 μ‚¬μš©ν•˜μ—¬ κ·Έ μŠ€ν† λ¦¬λ₯Ό λ‹€λ₯Έ ν…ŒμŠ€νŠΈ λ„κ΅¬μ—μ„œ μž¬μ‚¬μš©ν•˜λŠ” 것이 μΌλ°˜μ μž…λ‹ˆλ‹€. + +λ˜ν•œ, μ—¬λŸ¬ ν™˜κ²½μ—μ„œ λͺ¨ν‚Ήμ„ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μŠ€ν† λ¦¬λΆμ˜ λͺ¨ν‚Ήμ€ Node ν‘œμ€€μ˜ 일뢀이기 λ•Œλ¬Έμ— Nodeμ—μ„œ "for free"둜 μž‘λ™ν•˜μ§€λ§Œ, κ·Έ ν‘œμ€€μ΄ Webpackκ³Ό Vite에 μ˜ν•΄ κ΅¬ν˜„λ˜μ—ˆκΈ° λ•Œλ¬Έμ— λΈŒλΌμš°μ €μ—μ„œλ„ 잘 μž‘λ™ν•œλ‹€κ³  κ°€μ •ν•  경우 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +λ§ˆμ§€λ§‰μœΌλ‘œ, μš°λ¦¬λŠ” ESM ν‘œμ€€μ— 맞좰져 있기 λ•Œλ¬Έμ—, 미래의 JS λ³€κ²½κ³Ό ν˜Έν™˜λ  수 μžˆμŠ΅λ‹ˆλ‹€. μš°λ¦¬λŠ” ν”Œλž«νΌμ— λ² νŒ…ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μš°λ¦¬λŠ” 이것이 λͺ¨λ“ˆ λͺ¨ν‚Ήμ˜ 미래이며 λͺ¨λ“  ν…ŒμŠ€νŠΈ 도ꡬ가 이λ₯Ό 채택해야 ν•œλ‹€κ³  λ―ΏμŠ΅λ‹ˆλ‹€. + +### 였늘 λ°”λ‘œ μ‹œλ„ν•΄λ³΄μ„Έμš” + +λͺ¨λ“ˆ λͺ¨ν‚Ήμ€ μŠ€ν† λ¦¬λΆ 8.1μ—μ„œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μƒˆ ν”„λ‘œμ νŠΈμ—μ„œ μ‹œλ„ν•΄λ³΄μ„Έμš”: + +```bash +npx storybook@latest init +``` + +λ˜λŠ” κΈ°μ‘΄ ν”„λ‘œμ νŠΈλ₯Ό μ—…κ·Έλ ˆμ΄λ“œν•˜μ„Έμš”: + +```bash +npx storybook@latest upgrade +``` + +λͺ¨λ“ˆ λͺ¨ν‚Ήμ— λŒ€ν•΄ μžμ„Ένžˆ μ•Œμ•„λ³΄λ €λ©΄ μŠ€ν† λ¦¬λΆ λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•΄ μ£Όμ„Έμš”. μ—¬κΈ°μ—λŠ” 더 λ§Žμ€ μ˜ˆμ œμ™€ 전체 APIκ°€ ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μš°λ¦¬λŠ” λͺ¨λ“ˆ λͺ¨ν‚Ή μ ‘κ·Ό 방식을 μ‚¬μš©ν•˜μ—¬ ν…ŒμŠ€νŠΈν•œ Next.js React μ„œλ²„ μ»΄ν¬λ„ŒνŠΈ(RSC) μ•±μ˜ 전체 데λͺ¨λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€. 이에 λŒ€ν•΄ 더 μžμ„Ένžˆ μ„€λͺ…ν•  μ˜ˆμ •μ΄λ©°, 곧 λΈ”λ‘œκ·Έ 포슀트λ₯Ό 톡해 λ¬Έμ„œν™”ν•  κ³„νšμž…λ‹ˆλ‹€. + +λ‹€μŒ 단계 +μŠ€ν† λ¦¬λΆμ˜ λͺ¨λ“ˆ λͺ¨ν‚Ήμ€ κΈ°λŠ₯ μ™„μ„± 단계이며 μ‚¬μš© μ€€λΉ„κ°€ λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μš°λ¦¬λŠ” λ‹€μŒκ³Ό 같은 κ°œμ„  사항을 κ³ λ €ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€: + +- 주어진 λͺ¨λ“ˆμ— λŒ€ν•΄ λͺ¨μ˜ λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈλ₯Ό μžλ™μœΌλ‘œ μƒμ„±ν•˜λŠ” CLI μœ ν‹Έλ¦¬ν‹° +- UIμ—μ„œ λͺ¨μ˜ 데이터λ₯Ό μ‹œκ°ν™”ν•˜κ³  νŽΈμ§‘ν•  수 μžˆλŠ” 지원 + +λͺ¨λ“ˆ λͺ¨ν‚Ή 외에도, μš°λ¦¬λŠ” μ—¬λŸ¬ ν…ŒμŠ€νŠΈ κ°œμ„  μž‘μ—…μ„ 진행 μ€‘μž…λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, λΈŒλΌμš°μ €μ—μ„œ React μ„œλ²„ μ»΄ν¬λ„ŒνŠΈλ₯Ό μœ λ‹› ν…ŒμŠ€νŠΈν•  수 μžˆλŠ” μƒˆλ‘œμš΄ 방법을 κ°œλ°œν–ˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ μŠ€ν† λ¦¬λΆμ˜ ν…ŒμŠ€νŠΈλ₯Ό Jest/Vitest의 Jasmineμ—μ„œ μ˜κ°μ„ 받은 ꡬ쑰에 훨씬 더 κ°€κΉκ²Œ κ°€μ Έμ˜€λŠ” μž‘μ—…μ„ μ§„ν–‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. diff --git a/June/article/article.md b/June/article/article.md deleted file mode 100644 index 91ed32b..0000000 --- a/June/article/article.md +++ /dev/null @@ -1 +0,0 @@ -article diff --git a/June/study/Type-safe_module_mocking_in_Storybook.md b/June/study/Type-safe_module_mocking_in_Storybook.md new file mode 100644 index 0000000..5930e18 --- /dev/null +++ b/June/study/Type-safe_module_mocking_in_Storybook.md @@ -0,0 +1,49 @@ +# λ²ˆμ—­ν•˜λ©΄μ„œ 곡뢀 ν•œ 것듀 + +## νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ satisfies ν‚€μ›Œλ“œμ™€ as ν‚€μ›Œλ“œ + +νƒ€μž…μŠ€ν¬λ¦½νŠΈμ—μ„œ as와 satisfies ν‚€μ›Œλ“œλŠ” νƒ€μž…μ„ λͺ…μ‹œν•˜κ±°λ‚˜ κ²€μ¦ν•˜λŠ” 데 μ‚¬μš©λ˜μ§€λ§Œ, κ·Έ μ‚¬μš©λ²•κ³Ό λͺ©μ μ— μžˆμ–΄μ„œ 차이가 μžˆμŠ΅λ‹ˆλ‹€. + +as ν‚€μ›Œλ“œ +as ν‚€μ›Œλ“œλŠ” νƒ€μž… 단언(Type Assertion)을 μ‚¬μš©ν•  λ•Œ 쓰이며, κ°œλ°œμžκ°€ νŠΉμ • κ°’μ˜ νƒ€μž…μ„ νƒ€μž…μŠ€ν¬λ¦½νŠΈλ³΄λ‹€ 더 잘 μ•Œκ³  μžˆλ‹€κ³  λͺ…μ‹œν•˜λŠ” μš©λ„λ‘œ μ‚¬μš©λ©λ‹ˆλ‹€. 이λ₯Ό 톡해 μ»΄νŒŒμΌλŸ¬μ—κ²Œ ν•΄λ‹Ή 값이 μ§€μ •λœ νƒ€μž…μ„ κ°–κ³  μžˆλ‹€κ³  μ•Œλ €μ£Όλ©°, μ‹€μ œ νƒ€μž… 체크λ₯Ό μš°νšŒν•©λ‹ˆλ‹€. + +μ˜ˆμ‹œ: + +```ts +let someValue: any = "this is a string"; +let strLength: number = (someValue as string).length; +``` + +μž₯점: + +- κ°•μ œ νƒ€μž… λ³€ν™˜μ„ 톡해 κ°œλ°œμžκ°€ μ˜λ„ν•˜λŠ” νƒ€μž…μ„ λͺ…ν™•νžˆ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +- any νƒ€μž…μ—μ„œ ꡬ체적인 νƒ€μž…μœΌλ‘œμ˜ μ „ν™˜μ„ κ°€λŠ₯ν•˜κ²Œ ν•˜μ—¬, νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ νƒ€μž… μ‹œμŠ€ν…œμ„ μœ μ—°ν•˜κ²Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +단점: + +- νƒ€μž… 단언은 μ‹€μ œ νƒ€μž…κ³Ό μΌμΉ˜ν•˜μ§€ μ•Šμ„ μœ„ν—˜μ„ λ‚΄ν¬ν•˜κ³  μžˆμ–΄, λŸ°νƒ€μž„ μ—λŸ¬λ₯Ό λ°œμƒμ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€. +- μ»΄νŒŒμΌλŸ¬κ°€ νƒ€μž…μ„ κ°•μ œλ‘œ μˆ˜μš©ν•˜κΈ° λ•Œλ¬Έμ—, νƒ€μž… μ•ˆμ „μ„±μ„ μ €ν•΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +### satisfies ν‚€μ›Œλ“œ + +νƒ€μž…μŠ€ν¬λ¦½νŠΈ 4.9μ—μ„œ λ„μž…λœ satisfies ν‚€μ›Œλ“œλŠ” νŠΉμ • 값이 주어진 νƒ€μž…μ„ λ§Œμ‘±ν•˜λŠ”μ§€ κ²€μ¦ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. satisfiesλŠ” 값을 λ‹€λ₯Έ νƒ€μž…μœΌλ‘œ λ³€ν™˜ν•˜μ§€ μ•ŠμœΌλ©΄μ„œ νƒ€μž… ν˜Έν™˜μ„±μ„ 확인할 수 있게 ν•΄μ€λ‹ˆλ‹€. + +μ˜ˆμ‹œ: + +```ts +let config = { + width: 800, + height: 600, + title: "My app", +} satisfies { width: number; height: number }; +``` + +μž₯점: + +- μ‹€μ œ νƒ€μž… λ³€ν™˜ 없이 νƒ€μž… ν˜Έν™˜μ„±μ„ 확인할 수 μžˆμ–΄ μ•ˆμ „ν•©λ‹ˆλ‹€. +- 개발 κ³Όμ •μ—μ„œ νƒ€μž… 였λ₯˜λ₯Ό 미리 λ°œκ²¬ν•˜κ³  μˆ˜μ •ν•  수 있게 λ„μ™€μ€λ‹ˆλ‹€. + +단점: + +- μƒˆλ‘œμš΄ ν‚€μ›Œλ“œμ΄κΈ° λ•Œλ¬Έμ— κΈ°μ‘΄ μ½”λ“œλ² μ΄μŠ€μ™€μ˜ ν˜Έν™˜μ„± λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. +- νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ λ³΅μž‘μ„±μ„ μ¦κ°€μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€. diff --git a/June/study/study.md b/June/study/study.md deleted file mode 100644 index 896aeb7..0000000 --- a/June/study/study.md +++ /dev/null @@ -1 +0,0 @@ -study