Skip to main content

Types

์ด ๊ฐ€์ด๋“œ๋Š” Typescript์™€ ๊ฐ™์€ ์ •์  ํƒ€์ž… ์–ธ์–ด์˜ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•๊ณผ FSD ๊ตฌ์กฐ ๋‚ด์—์„œ ํƒ€์ž…์ด ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

info

์ด ๊ฐ€์ด๋“œ์—์„œ ๋‹ค๋ฃจ์ง€ ์•Š๋Š” ์งˆ๋ฌธ์ด ์žˆ์œผ์‹ ๊ฐ€์š”? ์˜ค๋ฅธ์ชฝ ํŒŒ๋ž€์ƒ‰ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ํ”ผ๋“œ๋ฐฑ์„ ๋‚จ๊ฒจ์ฃผ์„ธ์š”. ์—ฌ๋Ÿฌ๋ถ„์˜ ์˜๊ฒฌ์„ ๋ฐ˜์˜ํ•ด ๊ฐ€์ด๋“œ๋ฅผ ํ™•์žฅํ•ด ๋‚˜๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค!

์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…โ€‹

์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…์€ ์ž์ฒด๋กœ ํฐ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง€์ง€๋Š” ์•Š์ง€๋งŒ, ๋‹ค๋ฅธ ํƒ€์ž…๊ณผ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์€ ํƒ€์ž…์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฐฐ์—ด์˜ ๊ฐ’์„ ๋‚˜ํƒ€๋‚ด๋Š” ArrayValues ํƒ€์ž…์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

type ArrayValues<T extends readonly unknown[]> = T[number];

Source: https://github.com/sindresorhus/type-fest/blob/main/source/array-values.d.ts

ํ”„๋กœ์ ํŠธ์—์„œ ์ด๋Ÿฌํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…์„ ํ™œ์šฉํ•˜๋ ค๋ฉด, type-fest ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•˜๊ฑฐ๋‚˜, ์ง์ ‘ shared/lib์— ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…์„ ๋ชจ์•„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ํƒ€์ž…๊ณผ ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์†ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž…์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๋ฅผ shared/lib/utility-types๋กœ ์ˆ˜์ •ํ•˜๊ณ  ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…๋“ค์— ๋Œ€ํ•œ ์„ค๋ช…์„ ํฌํ•จํ•œ README ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…์„ ๋„ˆ๋ฌด ๋งŽ์ด ์žฌ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•ด์„œ ๊ผญ ๋ชจ๋“  ๊ณณ์—์„œ ์‚ฌ์šฉํ•  ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…์„ ๊ณต์œ  ํด๋”์— ๋„ฃ๊ธฐ๋ณด๋‹ค๋Š”, ์ƒํ™ฉ์— ๋”ฐ๋ผ ํ•„์š”ํ•œ ํŒŒ์ผ ๊ฐ€๊นŒ์—์— ๋‘๋Š” ๊ฒƒ์ด ๋” ์ข‹์„ ๋–„๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๐Ÿ“‚ pages
    • ๐Ÿ“‚ home
      • ๐Ÿ“‚ api
        • ๐Ÿ“„ ArrayValues.ts (์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…)
        • ๐Ÿ“„ getMemoryUsageMetrics.ts (์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ)
warning

shared/types ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ๊ฐ ์Šฌ๋ผ์ด์Šค์— types๋ผ๋Š” ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์€ ๋งˆ์Œ์ด ๋“ค ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ทธ๋ ‡๊ฒŒ ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
types๋ผ๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๋Š” components๋‚˜ hooks์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋‚ด์šฉ์ด ๋ฌด์—‡์ธ์ง€๋ฅผ ์„ค๋ช…ํ•  ๋ฟ, ์ฝ”๋“œ์˜ ๋ชฉ์ ์„ ๋ช…ํ™•ํžˆ ์„ค๋ช…ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์Šฌ๋ผ์ด์Šค๋Š” ํ•ด๋‹น ์ฝ”๋“œ์˜ ๋ชฉ์ ์„ ์ •ํ™•ํžˆ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋น„์ฆˆ๋‹ˆ์Šค ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ƒํ˜ธ ์ฐธ์กฐ ๊ด€๊ณ„โ€‹

์•ฑ์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ํƒ€์ž… ์ค‘ ํ•˜๋‚˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์—”ํ‹ฐํ‹ฐ, ์ฆ‰ ์•ฑ์—์„œ ๋‹ค๋ฃจ๋Š” ๊ฐ์ฒด๋“ค ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์Œ์•… ์ŠคํŠธ๋ฆฌ๋ฐ ์•ฑ์—์„œ๋Š” Song, Album ๋“ฑ์ด ๋น„์ฆˆ๋‹ˆ์Šค ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

์˜ˆ๋ฅผ ๋“ค์–ด, ๋ชจ๋“  ์š”์ฒญ์„ Shared์— ๋ณด๊ด€ํ•˜๋Š” ๊ฒฝ์šฐ ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

shared/api/songs.ts
import type { Artist } from "./artists";

interface Song {
id: number;
title: string;
artists: Array<Artist>;
}

export function listSongs() {
return fetch('/api/songs').then((res) => res.json() as Promise<Array<Song>>);
}

Song ํƒ€์ž…์€ ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ์ธ Artist๋ฅผ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค. ์ด์™€ ๊ฐ™์ด ์š”์ฒญ ๊ด€๋ จ ์ฝ”๋“œ๋“ค์„ Shared์— ๊ด€๋ฆฌํ•˜๋ฉด, ํƒ€์ž…๋“ค์˜ ์„œ๋กœ ์–ฝํ˜€ ์žˆ์„ ๋–„ ๊ด€๋ฆฌ๊ฐ€ ์šฉ์ดํ•ด์ง‘๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ด ํ•จ์ˆ˜๋ฅผ entities/song/api์— ๋ณด๊ด€ํ–ˆ๋‹ค๋ฉด, entities/artist์—์„œ ๊ฐ„๋‹จํžˆ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ์–ด๋ ค์› ์„ ๊ฒƒ ์ž…๋‹ˆ๋‹ค. FSD ๊ตฌ์กฐ์—์„œ๋Š” ๋ ˆ์ด์–ด๋ณ„ import ๊ทœ์น™์„ ํ†ตํ•ด ์Šฌ๋ผ์ด์Šค ๊ฐ„์˜ ๊ต์ฐจ import๋ฅผ ์ œํ•œํ•˜๊ณ  ์žˆ๊ธฐ ๋–„๋ฌธ์ž…๋‹ˆ๋‹ค:

์Šฌ๋ผ์ด์Šค ์•ˆ์— ์žˆ๋Š” ๋ชจ๋“ˆ์€ ๊ณ„์ธต์ ์œผ๋กœ ๋” ๋‚ฎ์€ ๋ ˆ์ด์–ด์— ์œ„์น˜ํ•œ ์Šฌ๋ผ์ด์Šค๋งŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  1. ํƒ€์ž… ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”
    ํƒ€์ž…์ด ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ์™€ ์—ฐ๊ฒฐ๋  ๋•Œ, ํƒ€์ž… ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Song ํƒ€์ž…์— ArtistType์ด๋ผ๋Š” ์ œ์•ฝ ์กฐ๊ฑด์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    entities/song/model/song.ts
    interface Song<ArtistType extends { id: string }> {
    id: number;
    title: string;
    artists: Array<ArtistType>;
    }

    ์ด ๋ฐฉ๋ฒ•์€ ์ผ๋ถ€ ํƒ€์ž…์— ๋” ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Cart = { items: Array<Product> }์ฒ˜๋Ÿผ ๊ฐ„๋‹จํ•œ ํƒ€์ž…์€ ๋‹ค์–‘ํ•œ ์ œํ’ˆ ํƒ€์ž…์„ ์ง€์›ํ•˜๊ธฐ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ Country์™€ City์ฒ˜๋Ÿผ ๋” ๋ฐ€์ ‘ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋œ ํƒ€์ž…์€ ๋ถ„๋ฆฌํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

  2. Cross-import (๊ณต๊ฐœ API๋ฅผ ์‚ฌ์šฉํ•ด ๊ด€๋ฆฌํ•˜๊ธฐ)
    FSD์—์„œ ์—”ํ‹ฐํ‹ฐ ๊ฐ„ cross-imports๋ฅผ ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ณต๊ฐœ API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, song, artist, playlist๋ผ๋Š” ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์žˆ๊ณ , ํ›„์ž์˜ ๋‘ ์—”ํ‹ฐํ‹ฐ๊ฐ€ song์„ ์ฐธ์กฐํ•ด์•ผ ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ, song ์—”ํ‹ฐํ‹ฐ ๋‚ด์— artist์™€ playlist์šฉ ๊ณต๊ฐœ API๋ฅผ ๋”ฐ๋กœ @x ํ‘œ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • ๐Ÿ“‚ entities
      • ๐Ÿ“‚ song
        • ๐Ÿ“‚ @x
          • ๐Ÿ“„ artist.ts (artist entities๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ public API)
          • ๐Ÿ“„ playlist.ts (playlist.ts (playlist entities๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ public API))
        • ๐Ÿ“„ index.ts (์ผ๋ฐ˜์ ์ธ public API)

    ํŒŒ์ผ ๐Ÿ“„ entities/song/@x/artist.ts์˜ ๋‚ด์šฉ์€ ๐Ÿ“„ entities/song/index.ts์™€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค:

    entities/song/@x/artist.ts
    export type { Song } from "../model/song.ts";

    ๋”ฐ๋ผ์„œ ๐Ÿ“„ entities/artist/model/artist.ts ํŒŒ์ผ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด Song์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    entities/artist/model/artist.ts
    import type { Song } from "entities/song/@x/artist";

    export interface Artist {
    name: string;
    songs: Array<Song>;
    }

    ์ด๋ ‡๊ฒŒ ์—”ํ‹ฐํ‹ฐ ๊ฐ„ ๋ช…์‹œ์ ์œผ๋กœ ์—ฐ๊ฒฐ์„ ํ•ด๋‘๋ฉด ์˜์กด ๊ด€๊ณ„๋ฅผ ํŒŒ์•…ํ•˜๊ณ  ๋„๋ฉ”์ธ ๋ถ„๋ฆฌ ์ˆ˜์ค€์„ ์œ ์ง€ํ•˜๊ธฐ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ์ฒด์™€ mappersโ€‹

๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ์ฒด(Data Transfer Object, DTO)๋Š” ๋ฐฑ์—”๋“œ์—์„œ ์˜ค๋Š” ๋ฐ์ดํ„ฐ์˜ ๊ตฌ์กฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์šฉ์–ด์ž…๋‹ˆ๋‹ค. ๋–„๋กœ๋Š” DTO๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํŽธ๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ฒฝ์šฐ์— ๋”ฐ๋ผ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” ๋ถˆํŽธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ๋งคํผ๋ฅผ ์‚ฌ์šฉํ•ด DTO๋ฅผ ๋” ํŽธ๋ฆฌํ•œ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

DTO์˜ ์œ„์น˜โ€‹

๋ฐฑ์—”๋“œ ํƒ€์ž…์ด ๋ณ„๋„์˜ ํŒจํ‚ค์ง€์— ์žˆ๋Š” ๊ฒฝ์šฐ(์˜ˆ: ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ์—์„œ ์ฝ”๋“œ๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ฒฝ์šฐ) DTO๋ฅผ ํ•ด๋‹น ํŒจํ‚ค์ง€์—์„œ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋ฐฑ์—”๋“œ์™€ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐ„ ์ฝ”๋“œ ๊ณต์œ ๊ฐ€ ์—†๋‹ค๋ฉด, ํ”„๋ก ํŠธ์—”๋“œ ์ฝ”๋“œ๋ฒ ์ด์Šค ์–ด๋”˜๊ฐ€์— DTO๋ฅผ ๋ณด๊ด€ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๋ฅผ ์•„๋ž˜์—์„œ ๋‹ค๋ฃจ์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

shared/api์— ์š”์ฒญ ํ•จ์ˆ˜๊ฐ€ ์žˆ๋‹ค๋ฉด, DTO ์—ญ์‹œ ํ•ด๋‹น ํ•จ์ˆ˜ ๋ฐ”๋กœ ์˜†์— ๋‘๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค:

shared/api/songs.ts
import type { ArtistDTO } from "./artists";

interface SongDTO {
id: number;
title: string;
artist_ids: Array<ArtistDTO["id"]>;
}

export function listSongs() {
return fetch('/api/songs').then((res) => res.json() as Promise<Array<SongDTO>>);
}

์•ž์—์„œ ์–ธ๊ธ‰ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ, ์š”์ฒญ๊ณผ DTO๋ฅผ shared์— ๋‘๋ฉด ๋‹ค๋ฅธ DTO๋ฅผ ์ฐธ์กฐํ•˜๊ธฐ๊ฐ€ ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.

Mappers์˜ ์œ„์น˜โ€‹

Mappers๋Š” DTO๋ฅผ ๋ฐ›์•„ ๋ณ€ํ™˜ํ•˜๋Š” ์—ญํ• ์„ ํ•˜๋ฏ€๋กœ, DTO ์ •์˜์™€ ๊ฐ€๊นŒ์šด ์œ„์น˜์— ๋‘๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์š”์ฒญ๊ณผ DTO๊ฐ€ shared/api์— ์ •์˜๋˜์–ด ์žˆ๋‹ค๋ฉด, mappers๋„ ๊ทธ๊ณณ์— ์œ„์น˜ํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

shared/api/songs.ts
import type { ArtistDTO } from "./artists";

interface SongDTO {
id: number;
title: string;
disc_no: number;
artist_ids: Array<ArtistDTO["id"]>;
}

interface Song {
id: string;
title: string;
/** ๋…ธ๋ž˜์˜ ์ „์ฒด ์ œ๋ชฉ, ๋””์Šคํฌ ๋ฒˆํ˜ธ๊นŒ์ง€ ํฌํ•จ๋œ ์ œ๋ชฉ์ž…๋‹ˆ๋‹ค. */
fullTitle: string;
artistIds: Array<string>;
}

function adaptSongDTO(dto: SongDTO): Song {
return {
id: String(dto.id),
title: dto.title,
fullTitle: `${dto.disc_no} / ${dto.title}`,
artistIds: dto.artist_ids.map(String),
};
}

export function listSongs() {
return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO));
}

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

entities/song/api/dto.ts
import type { ArtistDTO } from "entities/artist/@x/song";

export interface SongDTO {
id: number;
title: string;
disc_no: number;
artist_ids: Array<ArtistDTO["id"]>;
}
entities/song/api/mapper.ts
import type { SongDTO } from "./dto";

export interface Song {
id: string;
title: string;
/** ๋…ธ๋ž˜์˜ ์ „์ฒด ์ œ๋ชฉ, ๋””์Šคํฌ ๋ฒˆํ˜ธ๊นŒ์ง€ ํฌํ•จ๋œ ์ œ๋ชฉ์ž…๋‹ˆ๋‹ค. */
fullTitle: string;
artistIds: Array<string>;
}

export function adaptSongDTO(dto: SongDTO): Song {
return {
id: String(dto.id),
title: dto.title,
fullTitle: `${dto.disc_no} / ${dto.title}`,
artistIds: dto.artist_ids.map(String),
};
}
entities/song/api/listSongs.ts
import { adaptSongDTO } from "./mapper";

export function listSongs() {
return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO));
}
entities/song/model/songs.ts
import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";

import { listSongs } from "../api/listSongs";

export const fetchSongs = createAsyncThunk('songs/fetchSongs', listSongs);

const songAdapter = createEntityAdapter();
const songsSlice = createSlice({
name: "songs",
initialState: songAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchSongs.fulfilled, (state, action) => {
songAdapter.upsertMany(state, action.payload);
})
},
});

์ค‘์ฒฉ๋œ DTO ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•โ€‹

๋ฐฑ์—”๋“œ ์‘๋‹ต์— ์—ฌ๋Ÿฌ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ ๋ฌธ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ณก ์ •๋ณด์— ์ €์ž์˜ ID๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ €์ž ๊ฐ์ฒด ์ „์ฒด๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ๋Š” ์—”ํ‹ฐํ‹ฐ ๊ฐ„์˜ ์ƒํ˜ธ ์ฐธ์กฐ๋ฅผ ํ”ผํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ์ง€์šฐ๊ฑฐ๋‚˜ ๋ฐฑ์—”๋“œ ํŒ€๊ณผ ํ˜‘์˜ํ•˜์ง€ ์•Š๋Š” ํ•œ, ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ์—๋Š” ์Šฌ๋ผ์ด์Šค ๊ฐ„ ๊ฐ„์ ‘์ ์ธ ์—ฐ๊ฒฐ ๋Œ€์‹  ๋ช…์‹œ์ ์ธ ๊ต์ฐจ ์ฐธ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด @x ํ‘œ๊ธฐ๋ฒ•์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋‹ค์Œ์€ Redux Toolkit์„ ์‚ฌ์šฉํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค:

entities/song/model/songs.ts
import {
createSlice,
createEntityAdapter,
createAsyncThunk,
createSelector,
} from '@reduxjs/toolkit'
import { normalize, schema } from 'normalizr'

import { getSong } from "../api/getSong";

// Normalizr์˜ entities ์Šคํ‚ค๋งˆ ์ •์˜
export const artistEntity = new schema.Entity('artists')
export const songEntity = new schema.Entity('songs', {
artists: [artistEntity],
})

const songAdapter = createEntityAdapter()

export const fetchSong = createAsyncThunk(
'songs/fetchSong',
async (id: string) => {
const data = await getSong(id)
// ๋ฐ์ดํ„ฐ๋ฅผ ์ •๊ทœํ™”ํ•˜์—ฌ ๋ฆฌ๋“€์„œ๊ฐ€ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ payload๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค:
// `action.payload = { songs: {}, artists: {} }`
const normalized = normalize(data, songEntity)
return normalized.entities
}
)

export const slice = createSlice({
name: 'songs',
initialState: songAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchSong.fulfilled, (state, action) => {
songAdapter.upsertMany(state, action.payload.songs)
})
},
})

const reducer = slice.reducer
export default reducer
entities/song/@x/artist.ts
export { fetchSong } from "../model/songs";
entities/artist/model/artists.ts
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'

import { fetchSong } from 'entities/song/@x/artist'

const artistAdapter = createEntityAdapter()

export const slice = createSlice({
name: 'users',
initialState: artistAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchSong.fulfilled, (state, action) => {
// ๊ฐ™์€ fetch ๊ฒฐ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ์—ฌ๊ธฐ์„œ artists๋ฅผ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค.
artistAdapter.upsertMany(state, action.payload.artists)
})
},
})

const reducer = slice.reducer
export default reducer

์ด ๋ฐฉ๋ฒ•์€ ์Šฌ๋ผ์ด์Šค ๋ถ„๋ฆฌ์˜ ์ด์ ์„ ๋‹ค์†Œ ์ œํ•œํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์šฐ๋ฆฌ๊ฐ€ ์ œ์–ดํ•  ์ˆ˜ ์—†๋Š” ๋‘ ์—”ํ‹ฐํ‹ฐ ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ด๋Ÿฌํ•œ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋ฆฌํŒฉํ† ๋ง๋˜์–ด์•ผ ํ•œ๋‹ค๋ฉด, ํ•จ๊ป˜ ๋ฆฌํŒฉํ† ๋งํ•ด์•ผ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ „์—ญ ํƒ€์ž…๊ณผ Reduxโ€‹

์ „์—ญ ํƒ€์ž…์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „๋ฐ˜์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํƒ€์ž…์„ ์˜๋ฏธํ•˜๋ฉฐ, ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŠน์„ฑ์ด ์—†๋Š” ์ œ๋„ˆ๋ฆญ ํƒ€์ž…
  2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์— ์•Œ๊ณ  ์žˆ์–ด์•ผ ํ•˜๋Š” ํƒ€์ž…

์ฒซ ๋ฒˆ์งธ ๊ฒฝ์šฐ์—๋Š” ๊ด€๋ จ ํƒ€์ž…์„ Shared ํด๋” ์•ˆ์— ์ ์ ˆํ•œ ์„ธ๊ทธ๋จผํŠธ๋กœ ๋ฐฐ์น˜ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ถ„์„ ์ „์—ญ ๋ณ€์ˆ˜๋ฅผ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์žˆ๋‹ค๋ฉด shared/analytics์— ๋‘๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

warning

๊ฒฝ๊ณ : shared/types ํด๋”๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. "ํƒ€์ž…"์ด๋ผ๋Š” ๊ณตํ†ต๋œ ์†์„ฑ์œผ๋กœ ๊ด€๋ จ ์—†๋Š” ํ•ญ๋ชฉ๋“ค์„ ๊ทธ๋ฃนํ™”ํ•˜๋ฉด, ํ”„๋กœ์ ํŠธ์—์„œ ์ฝ”๋“œ๋ฅผ ๊ฒ€์ƒ‰ํ•  ๋•Œ ํšจ์œจ์„ฑ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‘ ๋ฒˆ์งธ ๊ฒฝ์šฐ๋Š” Redux๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ RTK๊ฐ€ ์—†๋Š” ํ”„๋กœ์ ํŠธ์—์„œ ์ž์ฃผ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ตœ์ข… ์Šคํ† ์–ด ํƒ€์ž…์€ ๋ชจ๋“  ๋ฆฌ๋“€์„œ๋ฅผ ์ถ”๊ฐ€ํ•œ ํ›„์—๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์ด ์Šคํ† ์–ด ํƒ€์ž…์€ ์•ฑ ์ „์ฒด์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์…€๋ ‰ํ„ฐ์— ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ผ๋ฐ˜์ ์ธ ์Šคํ† ์–ด ์ •์˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

app/store/index.ts
import { combineReducers, rootReducer } from "redux";

import { songReducer } from "entities/song";
import { artistReducer } from "entities/artist";

const rootReducer = combineReducers(songReducer, artistReducer);

const store = createStore(rootReducer);

type RootState = ReturnType<typeof rootReducer>;
type AppDispatch = typeof store.dispatch;

shared/store์—์„œ useAppDispatch์™€ useAppSelector์™€ ๊ฐ™์€ ํƒ€์ž…์ด ์ง€์ •๋œ Redux ํ›…์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์ง€๋งŒ, ๋ ˆ์ด์–ด์— ๋Œ€ํ•œ import ๊ทœ์น™ ๋–„๋ฌธ์— App ๋ ˆ์ด์–ด์—์„œ RootState์™€ AppDispatch๋ฅผ import ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์Šฌ๋ผ์ด์Šค์˜ ๋ชจ๋“ˆ์€ ๋” ๋‚ฎ์€ ๋ ˆ์ด์–ด์— ์œ„์น˜ํ•œ ๋‹ค๋ฅธ ์Šฌ๋ผ์ด์Šค๋งŒ import ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ฒฝ์šฐ ๊ถŒ์žฅ๋˜๋Š” ํ•ด๊ฒฐ์ฑ…์€ Shared์™€ App ๋ ˆ์ด์–ด ๊ฐ„์— ์•”๋ฌต์ ์ธ ์˜์กด์„ฑ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. RootState์™€ AppDispatch ๋‘ ํƒ€์ž…์€ ์œ ์ง€๋ณด์ˆ˜ ํ•„์š”์„ฑ์ด ์ ๊ณ  Redux๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์ต์ˆ™ํ•˜๋ฏ€๋กœ ํฐ ๋ฌธ์ œ ์—†์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

TypeScript์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํƒ€์ž…์„ ์ „์—ญ์œผ๋กœ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

app/store/index.ts
/* ์ด์ „ ์ฝ”๋“œ ๋ธ”๋ก๊ณผ ๋™์ผํ•œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹คโ€ฆ */

declare type RootState = ReturnType<typeof rootReducer>;
declare type AppDispatch = typeof store.dispatch;
shared/store/index.ts
import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux";

export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

์—ด๊ฑฐํ˜•โ€‹

์ผ๋ฐ˜์ ์œผ๋กœ ์—ด๊ฑฐํ˜•(enum)์€ ์‚ฌ์šฉ๋˜๋Š” ์œ„์น˜์™€ ์ตœ๋Œ€ํ•œ ๊ฐ€๊นŒ์šด ๊ณณ์— ์ •์˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์—ด๊ฑฐํ˜•์ด ํŠน์ • ๊ธฐ๋Šฅ๊ณผ ๊ด€๋ จ๋œ ๊ฐ’์„ ๋‚˜ํƒ€๋‚ธ๋‹ค๋ฉด, ํ•ด๋‹น ๊ธฐ๋Šฅ ๋‚ด์— ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์„ธ๊ทธ๋จผํŠธ ์„ ํƒ๋„ ์‚ฌ์šฉ ์œ„์น˜์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ™”๋ฉด์—์„œ ํ† ์ŠคํŠธ ์œ„์น˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ด๋ผ๋ฉด ui ์„ธ๊ทธ๋จผํŠธ์— ๋‘๋Š” ๊ฒƒ์ด ์ข‹๊ณ , ๋ฐฑ์—”๋“œ ์‘๋‹ต ์ƒํƒœ ๋“ฑ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค๋ฉด api ์„ธ๊ทธ๋จผํŠธ์— ๋‘๋Š” ๊ฒƒ์ด ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ ์ „๋ฐ˜์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์—ด๊ฑฐํ˜•๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ผ๋ฐ˜์ ์ธ ๋ฐฑ์—”๋“œ ์‘๋‹ต ์ƒํƒœ๋‚˜ ๋””์ž์ธ ์‹œ์Šคํ…œ ํ† ํฐ ๋“ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ Shared์— ๋‘๋˜, ์—ด๊ฑฐํ˜•์ด ๋‚˜ํƒ€๋‚ด๋Š” ๊ฒƒ์„ ๊ธฐ์ค€์œผ๋กœ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค (api๋Š” ์‘๋‹ต ์ƒํƒœ, ui๋Š” ๋””์ž์ธ ํ† ํฐ ๋“ฑ).

ํƒ€์ž… ๊ฒ€์ฆ ์Šคํ‚ค๋งˆ์™€ Zodโ€‹

๋ฐ์ดํ„ฐ๊ฐ€ ํŠน์ • ํ˜•ํƒœ๋‚˜ ์ œ์•ฝ ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋ ค๋ฉด ๊ฒ€์ฆ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. TypeScript์—์„œ๋Š” Zod์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ฆ ์Šคํ‚ค๋งˆ๋Š” ๊ฐ€๋Šฅํ•˜๋ฉด ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ์™€ ๊ฐ™์€ ์œ„์น˜์— ๋‘๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๊ฒ€์ฆ ์Šคํ‚ค๋งˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•˜๋ฉฐ, ํŒŒ์‹ฑ์— ์‹คํŒจํ•˜๋ฉด ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.(Data transfoer objects and mappers ํ† ๋ก ์„ ์ฐธ์กฐํ•˜์„ธ์š”.) ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ๊ฒ€์ฆ ์‚ฌ๋ก€ ์ค‘ ํ•˜๋‚˜๋Š” ๋ฐฑ์—”๋“œ์—์„œ ์˜ค๋Š” ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ์Šคํ‚ค๋งˆ์™€ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์š”์ฒญ์„ ์‹คํŒจ์‹œํ‚ค๊ธฐ๋ฅผ ์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋ณดํ†ต api ์„ธ๊ทธ๋จผํŠธ์— ์Šคํ‚ค๋งˆ๋ฅผ ๋‘๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž ์ž…๋ ฅ(์˜ˆ: ํผ)์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ๊ฒฝ์šฐ, ์ž…๋ ฅ๋œ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด ๋ฐ”๋กœ ๊ฒ€์ฆ์ด ์ด๋ฃจ์–ด์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ์Šคํ‚ค๋งˆ๋ฅผ ui ์„ธ๊ทธ๋จผํŠธ ๋‚ด ํผ ์ปดํฌ๋„ŒํŠธ ์˜†์— ๋‘๊ฑฐ๋‚˜, ui ์„ธ๊ทธ๋จผํŠธ๊ฐ€ ๋„ˆ๋ฌด ๋ณต์žกํ•˜๋‹ค๋ฉด model ์„ธ๊ทธ๋จผํŠธ์— ๋‘˜ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ปดํฌ๋„ŒํŠธ props์™€ context์˜ ํƒ€์ž… ์ •์˜โ€‹

๋ณดํ†ต props๋‚˜ context ์ธํ„ฐํŽ˜์ด์Šค๋Š” ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋‚˜ ์ปจํ…์ŠคํŠธ์™€ ๊ฐ™์€ ํŒŒ์ผ์— ๋‘๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ Vue๋‚˜ Svelte์ฒ˜๋Ÿผ ๋‹จ์ผ ํŒŒ์ผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ ๊ฐ„์— ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ณต์œ ํ•ด์•ผ ํ•œ๋‹ค๋ฉด, ui ์„ธ๊ทธ๋จผํŠธ ๋‚ด ๋™์ผ ํด๋”์— ๋ณ„๋„์˜ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, React์˜ JSX์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜ํ•ฉ๋‹ˆ๋‹ค:

pages/home/ui/RecentActions.tsx
interface RecentActionsProps {
actions: Array<{ id: string; text: string }>;
}

export function RecentActions({ actions }: RecentActionsProps) {
/* โ€ฆ */
}

Vue์—์„œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ณ„๋„ ํŒŒ์ผ์— ์ €์žฅํ•œ ์˜ˆ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

pages/home/ui/RecentActionsProps.ts
export interface RecentActionsProps {
actions: Array<{ id: string; text: string }>;
}
pages/home/ui/RecentActions.vue
<script setup lang="ts">
import type { RecentActionsProps } from "./RecentActionsProps";

const props = defineProps<RecentActionsProps>();
</script>

Ambient ์„ ์–ธ ํŒŒ์ผ(*.d.ts)โ€‹

Vite๋‚˜ ts-reset ๊ฐ™์€ ์ผ๋ถ€ ํŒจํ‚ค์ง€๋Š” ์•ฑ ์ „๋ฐ˜์—์„œ ์ž‘๋™ํ•˜๊ธฐ ์œ„ํ•ด Ambient ์„ ์–ธ ํŒŒ์ผ์„ ํ•„์š”๋กœ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํŒŒ์ผ๋“ค์€ ๋ณดํ†ต ํฌ๊ฑฐ๋‚˜ ๋ณต์žกํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— src/ ํด๋”์— ๋‘์–ด๋„ ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค. ๋” ์ •๋ฆฌ๋œ ๊ตฌ์กฐ๋ฅผ ์œ„ํ•ด app/ambient/ ํด๋”์— ๋‘๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

ํƒ€์ดํ•‘์ด ์—†๋Š” ํŒจํ‚ค์ง€์ธ ๊ฒฝ์šฐ, ํ•ด๋‹น ํŒจํ‚ค์ง€๋ฅผ ๋ฏธํƒ€์ž…์œผ๋กœ ์„ ์–ธํ•˜๊ฑฐ๋‚˜ ์ง์ ‘ ํƒ€์ดํ•‘์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํƒ€์ดํ•‘์„ ์œ„ํ•œ ์ข‹์€ ์œ„์น˜๋Š” shared/lib ํด๋” ๋‚ด์˜ shared/lib/untyped-packages ํด๋”์ž…๋‹ˆ๋‹ค. ์ด ํด๋”์— %LIBRARY_NAME%.d.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ํ•„์š”ํ•œ ํƒ€์ž…์„ ์„ ์–ธํ•ฉ๋‹ˆ๋‹ค

shared/lib/untyped-packages/use-react-screenshot.d.ts
// ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ํƒ€์ž… ์ •์˜๊ฐ€ ์—†์œผ๋ฉฐ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ์ƒ๋žตํ–ˆ์Šต๋‹ˆ๋‹ค.
declare module "use-react-screenshot";

ํƒ€์ž… ์ž๋™ ์ƒ์„ฑโ€‹

์™ธ๋ถ€ ์†Œ์Šค๋กœ๋ถ€ํ„ฐ ํƒ€์ž…์„ ์ƒ์„ฑํ•˜๋Š” ์ผ์€ ํ”ํžˆ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, OpenAPI ์Šคํ‚ค๋งˆ๋กœ๋ถ€ํ„ฐ ๋ฐฑ์—”๋“œ ํƒ€์ž…์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
์ด๋Ÿฌํ•œ ํƒ€์ž…์„ ์œ„ํ•œ ์ „์šฉ ์œ„์น˜๋ฅผ ์ฝ”๋“œ๋ฒ ์ด์Šค์— ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด shared/api/openapi์™€ ๊ฐ™์€ ์œ„์น˜๊ฐ€ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์ด์ƒ์ ์œผ๋กœ๋Š” ์ด๋Ÿฌํ•œ ํŒŒ์ผ์ด ๋ฌด์—‡์ธ์ง€, ์–ด๋–ป๊ฒŒ ์žฌ์ƒ์„ฑํ•˜๋Š”์ง€ ๋“ฑ์„ ์„ค๋ช…ํ•˜๋Š” README ํŒŒ์ผ๋„ ํฌํ•จํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.