Types
์ด ๊ฐ์ด๋๋ Typescript์ ๊ฐ์ ์ ์ ํ์ ์ธ์ด์ ๋ฐ์ดํฐ ํ์ ์ ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ๊ณผ FSD ๊ตฌ์กฐ ๋ด์์ ํ์ ์ด ์ด๋ป๊ฒ ํ์ฉ๋๋์ง ์ค๋ช ํฉ๋๋ค.
์ด ๊ฐ์ด๋์์ ๋ค๋ฃจ์ง ์๋ ์ง๋ฌธ์ด ์์ผ์ ๊ฐ์? ์ค๋ฅธ์ชฝ ํ๋์ ๋ฒํผ์ ๋๋ฌ ํผ๋๋ฐฑ์ ๋จ๊ฒจ์ฃผ์ธ์. ์ฌ๋ฌ๋ถ์ ์๊ฒฌ์ ๋ฐ์ํด ๊ฐ์ด๋๋ฅผ ํ์ฅํด ๋๊ฐ๊ฒ ์ต๋๋ค!
์ ํธ๋ฆฌํฐ ํ์ โ
์ ํธ๋ฆฌํฐ ํ์ ์ ์์ฒด๋ก ํฐ ์๋ฏธ๋ฅผ ๊ฐ์ง์ง๋ ์์ง๋ง, ๋ค๋ฅธ ํ์ ๊ณผ ์์ฃผ ์ฌ์ฉ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ ํ์ ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ๋ฐฐ์ด์ ๊ฐ์ ๋ํ๋ด๋ ArrayValues ํ์ ์ ์ ์ํ ์ ์์ต๋๋ค.
ํ๋ก์ ํธ์์ ์ด๋ฌํ ์ ํธ๋ฆฌํฐ ํ์
์ ํ์ฉํ๋ ค๋ฉด, type-fest
๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํ๊ฑฐ๋, ์ง์ shared/lib
์ ์ ํธ๋ฆฌํฐ ํ์
์ ๋ชจ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ตฌ์ถํ ์ ์์ต๋๋ค. ์๋ก ์ถ๊ฐํ ํ์
๊ณผ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ํ์ง ์๋ ํ์
์ ๋ช
ํํ๊ฒ ๊ตฌ๋ถํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ์ด๋ฅผ shared/lib/utility-types
๋ก ์์ ํ๊ณ ์ ํธ๋ฆฌํฐ ํ์
๋ค์ ๋ํ ์ค๋ช
์ ํฌํจํ README ํ์ผ์ ์ถ๊ฐํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์
๋๋ค.
ํ์ง๋ง ์ ํธ๋ฆฌํฐ ํ์ ์ ๋๋ฌด ๋ง์ด ์ฌ์ฌ์ฉํ๋ ค๊ณ ํ์ง ์๋ ๊ฒ๋ ์ค์ํฉ๋๋ค. ์ฌ์ฌ์ฉํ ์ ์๋ค๊ณ ํด์ ๊ผญ ๋ชจ๋ ๊ณณ์์ ์ฌ์ฉํ ํ์๋ ์์ต๋๋ค. ๋ชจ๋ ์ ํธ๋ฆฌํฐ ํ์ ์ ๊ณต์ ํด๋์ ๋ฃ๊ธฐ๋ณด๋ค๋, ์ํฉ์ ๋ฐ๋ผ ํ์ํ ํ์ผ ๊ฐ๊น์์ ๋๋ ๊ฒ์ด ๋ ์ข์ ๋๋ ์์ต๋๋ค.
- ๐ pages
- ๐ home
- ๐ api
- ๐ ArrayValues.ts (์ ํธ๋ฆฌํฐ ํ์ )
- ๐ getMemoryUsageMetrics.ts (์ ํธ๋ฆฌํฐ ํ์ ์ ์ฌ์ฉํ๋ ์ฝ๋)
- ๐ api
- ๐ home
shared/types
ํด๋๋ฅผ ์์ฑํ๊ฑฐ๋ ๊ฐ ์ฌ๋ผ์ด์ค์ types
๋ผ๋ ์ธ๊ทธ๋จผํธ๋ฅผ ์ถ๊ฐํ๊ณ ์ถ์ ๋ง์์ด ๋ค ์ ์์ง๋ง, ๊ทธ๋ ๊ฒ ํ์ง ์๋ ๊ฒ์ด ์ข์ต๋๋ค.
types
๋ผ๋ ์นดํ
๊ณ ๋ฆฌ๋ components
๋ hooks
์ ๋ง์ฐฌ๊ฐ์ง๋ก ๋ด์ฉ์ด ๋ฌด์์ธ์ง๋ฅผ ์ค๋ช
ํ ๋ฟ, ์ฝ๋์ ๋ชฉ์ ์ ๋ช
ํํ ์ค๋ช
ํ์ง ์์ต๋๋ค. ์ฌ๋ผ์ด์ค๋ ํด๋น ์ฝ๋์ ๋ชฉ์ ์ ์ ํํ ์ค๋ช
ํ ์ ์์ด์ผ ํฉ๋๋ค.
๋น์ฆ๋์ค ์ํฐํฐ ๋ฐ ์ํธ ์ฐธ์กฐ ๊ด๊ณโ
์ฑ์์ ๊ฐ์ฅ ์ค์ํ ํ์ ์ค ํ๋๋ ๋น์ฆ๋์ค ์ํฐํฐ, ์ฆ ์ฑ์์ ๋ค๋ฃจ๋ ๊ฐ์ฒด๋ค ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ์์ ์คํธ๋ฆฌ๋ฐ ์ฑ์์๋ Song, Album ๋ฑ์ด ๋น์ฆ๋์ค ์ํฐํฐ๊ฐ ๋ ์ ์์ต๋๋ค.
๋น์ฆ๋์ค ์ํฐํฐ๋ ์ฃผ๋ก ๋ฐฑ์๋ ๋ฐํ์ด๊ธฐ ๋๋ฌธ์, ๋ฐฑ์๋ ์๋ต์ ํ์ ์ผ๋ก ์ ์ํ๋ ๊ฒ์ด ์ฒซ ๋ฒ์งธ ๋จ๊ณ์ ๋๋ค. ๊ฐ ์๋ํฌ์ธํธ์ ๋ํ ์์ฒญ ํจ์์ ๊ทธ ์๋ต์ ํ์ ์ผ๋ก ์ง์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค, ์ถ๊ฐ์ ์ธ ํ์ ์์ ์ฑ์ ์ํด Zod์ ๊ฐ์ ์คํค๋ง ๊ฒ์ฆ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด ์๋ต์ ๊ฒ์ฆํ ์๋ ์์ต๋๋ค.
์๋ฅผ ๋ค์ด, ๋ชจ๋ ์์ฒญ์ Shared์ ๋ณด๊ดํ๋ ๊ฒฝ์ฐ ์ด๋ ๊ฒ ์์ฑํ ์ ์์ต๋๋ค.
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๋ฅผ ์ ํํ๊ณ ์๊ธฐ ๋๋ฌธ์
๋๋ค:
์ฌ๋ผ์ด์ค ์์ ์๋ ๋ชจ๋์ ๊ณ์ธต์ ์ผ๋ก ๋ ๋ฎ์ ๋ ์ด์ด์ ์์นํ ์ฌ๋ผ์ด์ค๋ง ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
-
ํ์ ๋งค๊ฐ๋ณ์ํ
ํ์ ์ด ๋ค๋ฅธ ์ํฐํฐ์ ์ฐ๊ฒฐ๋ ๋, ํ์ ๋งค๊ฐ๋ณ์๋ฅผ ํตํด ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, Song ํ์ ์ ArtistType์ด๋ผ๋ ์ ์ฝ ์กฐ๊ฑด์ ์ค์ ํ ์ ์์ต๋๋ค.entities/song/model/song.tsinterface Song<ArtistType extends { id: string }> {
id: number;
title: string;
artists: Array<ArtistType>;
}์ด ๋ฐฉ๋ฒ์ ์ผ๋ถ ํ์ ์ ๋ ์ ํฉํฉ๋๋ค. ์๋ฅผ ๋ค์ด,
Cart = { items: Array<Product> }
์ฒ๋ผ ๊ฐ๋จํ ํ์ ์ ๋ค์ํ ์ ํ ํ์ ์ ์ง์ํ๊ธฐ ์ฝ๊ฒ ํ ์ ์์ต๋๋ค. ํ์ง๋งCountry
์City
์ฒ๋ผ ๋ ๋ฐ์ ํ๊ฒ ์ฐ๊ฒฐ๋ ํ์ ์ ๋ถ๋ฆฌํ๊ธฐ ์ด๋ ต์ต๋๋ค. -
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)
- ๐ @x
- ๐ song
ํ์ผ
๐ entities/song/@x/artist.ts
์ ๋ด์ฉ์๐ entities/song/index.ts
์ ์ ์ฌํฉ๋๋ค:entities/song/@x/artist.tsexport type { Song } from "../model/song.ts";
๋ฐ๋ผ์
๐ entities/artist/model/artist.ts
ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ดSong
์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค:entities/artist/model/artist.tsimport type { Song } from "entities/song/@x/artist";
export interface Artist {
name: string;
songs: Array<Song>;
}์ด๋ ๊ฒ ์ํฐํฐ ๊ฐ ๋ช ์์ ์ผ๋ก ์ฐ๊ฒฐ์ ํด๋๋ฉด ์์กด ๊ด๊ณ๋ฅผ ํ์ ํ๊ณ ๋๋ฉ์ธ ๋ถ๋ฆฌ ์์ค์ ์ ์งํ๊ธฐ ์ฌ์์ง๋๋ค.
- ๐ entities
๋ฐ์ดํฐ ์ ์ก ๊ฐ์ฒด์ mappersโ
๋ฐ์ดํฐ ์ ์ก ๊ฐ์ฒด(Data Transfer Object, DTO)๋ ๋ฐฑ์๋์์ ์ค๋ ๋ฐ์ดํฐ์ ๊ตฌ์กฐ๋ฅผ ๋ํ๋ด๋ ์ฉ์ด์ ๋๋ค. ๋๋ก๋ DTO๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ๋ ๊ฒ์ด ํธ๋ฆฌํ ์ ์์ง๋ง, ๊ฒฝ์ฐ์ ๋ฐ๋ผ ํ๋ก ํธ์๋์์๋ ๋ถํธํ ์ ์์ต๋๋ค. ์ด๋ ๋งคํผ๋ฅผ ์ฌ์ฉํด DTO๋ฅผ ๋ ํธ๋ฆฌํ ํํ๋ก ๋ณํํฉ๋๋ค.
DTO์ ์์นโ
๋ฐฑ์๋ ํ์ ์ด ๋ณ๋์ ํจํค์ง์ ์๋ ๊ฒฝ์ฐ(์: ํ๋ก ํธ์๋์ ๋ฐฑ์๋์์ ์ฝ๋๋ฅผ ๊ณต์ ํ๋ ๊ฒฝ์ฐ) DTO๋ฅผ ํด๋น ํจํค์ง์์ ๊ฐ์ ธ์ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค. ๋ฐฑ์๋์ ํ๋ก ํธ์๋ ๊ฐ ์ฝ๋ ๊ณต์ ๊ฐ ์๋ค๋ฉด, ํ๋ก ํธ์๋ ์ฝ๋๋ฒ ์ด์ค ์ด๋๊ฐ์ DTO๋ฅผ ๋ณด๊ดํด์ผ ํ๋๋ฐ, ์ด๋ฅผ ์๋์์ ๋ค๋ฃจ์ด ๋ณด๊ฒ ์ต๋๋ค.
shared/api
์ ์์ฒญ ํจ์๊ฐ ์๋ค๋ฉด, DTO ์ญ์ ํด๋น ํจ์ ๋ฐ๋ก ์์ ๋๋ ๊ฒ์ด ์ข์ต๋๋ค:
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๋ ๊ทธ๊ณณ์ ์์นํ๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค.
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 ์ญ์ ํด๋น ์ฌ๋ผ์ด์ค ๋ด์ ๋๋ ๊ฒ์ด ์ข์ต๋๋ค. ์ด๋ ์ฌ๋ผ์ด์ค ๊ฐ ๊ต์ฐจ ์ฐธ์กฐ๊ฐ ๋ฐ์ํ์ง ์๋๋ก ์ฃผ์ํด์ผ ํฉ๋๋ค.
import type { ArtistDTO } from "entities/artist/@x/song";
export interface SongDTO {
id: number;
title: string;
disc_no: number;
artist_ids: Array<ArtistDTO["id"]>;
}
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),
};
}
import { adaptSongDTO } from "./mapper";
export function listSongs() {
return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO));
}
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์ ์ฌ์ฉํ ์์์
๋๋ค:
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
export { fetchSong } from "../model/songs";
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โ
์ ์ญ ํ์
์ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ฐ์์ ์ฌ์ฉ๋๋ ํ์
์ ์๋ฏธํ๋ฉฐ, ํฌ๊ฒ ๋ ๊ฐ์ง๋ก ๋๋ ์ ์์ต๋๋ค:
- ์ ํ๋ฆฌ์ผ์ด์ ํน์ฑ์ด ์๋ ์ ๋๋ฆญ ํ์
- ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฒด์ ์๊ณ ์์ด์ผ ํ๋ ํ์
์ฒซ ๋ฒ์งธ ๊ฒฝ์ฐ์๋ ๊ด๋ จ ํ์
์ Shared ํด๋ ์์ ์ ์ ํ ์ธ๊ทธ๋จผํธ๋ก ๋ฐฐ์นํ๋ฉด ๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋ถ์ ์ ์ญ ๋ณ์๋ฅผ ์ํ ์ธํฐํ์ด์ค๊ฐ ์๋ค๋ฉด shared/analytics
์ ๋๋ ๊ฒ์ด ์ข์ต๋๋ค.
๊ฒฝ๊ณ : shared/types
ํด๋๋ฅผ ์์ฑํ์ง ์๋ ๊ฒ์ด ์ข์ต๋๋ค. "ํ์
"์ด๋ผ๋ ๊ณตํต๋ ์์ฑ์ผ๋ก ๊ด๋ จ ์๋ ํญ๋ชฉ๋ค์ ๊ทธ๋ฃนํํ๋ฉด, ํ๋ก์ ํธ์์ ์ฝ๋๋ฅผ ๊ฒ์ํ ๋ ํจ์จ์ฑ์ด ๋จ์ด์ง ์ ์์ต๋๋ค.
๋ ๋ฒ์งธ ๊ฒฝ์ฐ๋ Redux๋ฅผ ์ฌ์ฉํ์ง๋ง RTK๊ฐ ์๋ ํ๋ก์ ํธ์์ ์์ฃผ ๋ฐ์ํฉ๋๋ค. ์ต์ข ์คํ ์ด ํ์ ์ ๋ชจ๋ ๋ฆฌ๋์๋ฅผ ์ถ๊ฐํ ํ์๋ง ์ฌ์ฉ ๊ฐ๋ฅํ์ง๋ง, ์ด ์คํ ์ด ํ์ ์ ์ฑ ์ ์ฒด์์ ์ฌ์ฉํ๋ ์ ๋ ํฐ์ ํ์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ์ผ๋ฐ์ ์ธ ์คํ ์ด ์ ์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
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์์๋ ๋ค์๊ณผ ๊ฐ์ด ํ์ ์ ์ ์ญ์ผ๋ก ์ ์ธํ ์ ์์ต๋๋ค:
/* ์ด์ ์ฝ๋ ๋ธ๋ก๊ณผ ๋์ผํ ๋ด์ฉ์
๋๋คโฆ */
declare type RootState = ReturnType<typeof rootReducer>;
declare type AppDispatch = typeof store.dispatch;
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์์๋ ๋ค์๊ณผ ๊ฐ์ด ์ ์ํฉ๋๋ค:
interface RecentActionsProps {
actions: Array<{ id: string; text: string }>;
}
export function RecentActions({ actions }: RecentActionsProps) {
/* โฆ */
}
Vue์์ ์ธํฐํ์ด์ค๋ฅผ ๋ณ๋ ํ์ผ์ ์ ์ฅํ ์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
export interface RecentActionsProps {
actions: Array<{ id: string; text: string }>;
}
<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
ํ์ผ์ ์์ฑํ๊ณ ํ์ํ ํ์
์ ์ ์ธํฉ๋๋ค
// ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํ์
์ ์๊ฐ ์์ผ๋ฉฐ ์์ฑํ๋ ๊ฒ์ ์๋ตํ์ต๋๋ค.
declare module "use-react-screenshot";
ํ์ ์๋ ์์ฑโ
์ธ๋ถ ์์ค๋ก๋ถํฐ ํ์
์ ์์ฑํ๋ ์ผ์ ํํ ๋ฐ์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, OpenAPI ์คํค๋ง๋ก๋ถํฐ ๋ฐฑ์๋ ํ์
์ ์์ฑํ๋ ๊ฒฝ์ฐ๊ฐ ์์ต๋๋ค.
์ด๋ฌํ ํ์
์ ์ํ ์ ์ฉ ์์น๋ฅผ ์ฝ๋๋ฒ ์ด์ค์ ๋ง๋๋ ๊ฒ์ด ์ข์ต๋๋ค. ์๋ฅผ ๋ค์ด shared/api/openapi
์ ๊ฐ์ ์์น๊ฐ ์ ํฉํฉ๋๋ค. ์ด์์ ์ผ๋ก๋ ์ด๋ฌํ ํ์ผ์ด ๋ฌด์์ธ์ง, ์ด๋ป๊ฒ ์ฌ์์ฑํ๋์ง ๋ฑ์ ์ค๋ช
ํ๋ README ํ์ผ๋ ํฌํจํ๋ ๊ฒ์ด ์ข์ต๋๋ค.