๊ธฐํ์ ๊ณ์ ๋ฐ๋๋๋ฐ ๊ฐ๋ฐ ์ผ์ ์ ๊ณ ์ ๋์ด ์์๋ค
์๋น์ค ์ ์ฒด ๊ฐํธ์ ์งํํ๊ฒ ๋๋ฉด์ ์งง์ ๊ธฐ๊ฐ ์์ ๋งค์ฐ ๋ง์ ํ๋ฉด์ ๊ฐ๋ฐํด์ผ ํ๋ ์ํฉ์ด ์์์ต๋๋ค.
ํ์ด์ง, ์ฌ๋ผ์ด๋์์, ๋ชจ๋ฌ์ ํฌํจํด 30๊ฐ ์ด์์ ํ๋ฉด์ด ์๋กญ๊ฒ ์ถ๊ฐ๋์ด์ผ ํ๊ณ , ๋ฌธ์ ๋ ๋จ์ํ ํ๋ฉด ์๋ง ๋ง์๋ ๊ฒ์ด ์๋์์ต๋๋ค.
๊ธฐํ์ ์์ ํ ํ์ ๋์ง ์์ ์ํ์๊ณ , ๊ฐ๋ฐ์ ์งํํ๋ ๋์์๋ ๊ณ์ํด์ ์๊ตฌ์ฌํญ์ด ๋ณ๊ฒฝ๋๊ณ ์์์ต๋๋ค.
ํ์ง๋ง ๋ฆด๋ฆฌ์ฆ ์ผ์ ์ ๊ณ ์ ๋์ด ์์๊ณ , ๊ฒฐ๊ตญ ๊ฐ๋ฐ์๊ฐ ์ค์ ๊ตฌํ์ ์ฌ์ฉํ ์ ์๋ ์๊ฐ์ ๊ณ์ ์ค์ด๋๋ ๊ตฌ์กฐ์์ต๋๋ค.
์ด๋ฐ ์ํฉ์์ ์ ๋ ์์ฐ์ค๋ฝ๊ฒ ๋ค์ ์ง๋ฌธ์ ํ๊ฒ ๋์์ต๋๋ค.
“์ง๊ธ ๋ด๊ฐ ์ ๋ง ์ ํ ๊ฐ๋ฐ์ ์๊ฐ์ ์ฐ๊ณ ์๋ ๊ฑธ๊น? ์๋ฏธ ์๋ ๋ฐ๋ณต ์์ ์ ์๊ฐ์ ์๋๊ฑฐ ์๋๊น?”
๋ฐ๋ณต๋๋ API Boilerplate ๋ฌธ์
๊ธฐ์กด ๊ตฌ์กฐ์์๋ ์๋ก์ด API๊ฐ ์ถ๊ฐ๋ ๋๋ง๋ค ๋ฐ๋ณต์ ์ผ๋ก ์์ฑํด์ผ ํ๋ ์ฝ๋๊ฐ ๋ง์์ต๋๋ค.
์๋ฅผ ๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ์ ๋๋ค.
API ํต์ ๊ณ์ธต
import axios from '@/lib/axios';
export const fetchProducts = async (categoryId: string) => {
try {
const { data } = await axios.get(
`/api/categories/${categoryId}/products`
);
return data;
} catch (error) {
console.error('Failed to fetch products', error);
throw error;
}
};
React Query ๊ณ์ธต
import { useQuery } from '@tanstack/react-query';
import { fetchProducts } from './fetchProducts';
export const useProducts = (categoryId: string) => {
return useQuery({
queryKey: ['products', categoryId],
queryFn: () => fetchProducts(categoryId),
enabled: !!categoryId,
});
};
๋น์ ์์ฑํ๋ ๋๋ถ๋ถ์ API ์ฝ๋๋ค์ ๋ค์ ์์๋ง ๋ฌ๋์ต๋๋ค.
- endpoint
- request/response type
- queryKey
- mutation/query ์ฌ๋ถ
ํ์ง๋ง ์ ์ฒด ๊ตฌ์กฐ๋ ๊ฑฐ์ ๋์ผํ์ต๋๋ค.
๊ฒฐ๊ตญ ์ ๋ ๊ธฐ๋ฅ ๊ฐ๋ฐ๋ณด๋ค ๋ฐ๋ณต์ ์ธ boilerplate ์์ฐ์ ๋ง์ ์๊ฐ์ ์ฐ๊ณ ์๋ค๋ ๊ฒ์ ์ธ์ํ๊ฒ ๋์์ต๋๋ค.
ํ์์๋ ํฌ๊ฒ ์ฒด๊ฐ๋์ง ์์์ง๋ง, ์๋น์ค ์ ์ฒด ๊ฐํธ์ฒ๋ผ API ์ฐ๋์ด ํญ๋ฐ์ ์ผ๋ก ์ฆ๊ฐํ๋ ์์ ์์๋ ์ด ๋ฐ๋ณต ์์ ์ด ์ผ์ ์์ฒด๋ฅผ ์ํํ๋ ์์ค์ ๋น์ฉ์ด ๋์์ต๋๋ค.
์ฒ์์๋ AI๋ก ํด๊ฒฐํ๋ ค๊ณ ํ๋ค
์ฒ์์๋ Cursor, Claude ๊ฐ์ AI ๊ธฐ๋ฐ ์ฝ๋ ์์ฑ ๋ฐฉ์์ ๊ณ ๋ฏผํ์ต๋๋ค.
์๋ฅผ ๋ค์ด:
- ํ ๋ด๋ถ ๊ท์น ์ ์
- rules.md ์์ฑ
- API ์์ฑ ๊ท์น ๋ฌธ์ํ
- ํ๋กฌํํธ ๊ธฐ๋ฐ ์์ฑ
๊ฐ์ ๋ฐฉ์์ ๋๋ค.
ํ์ง๋ง ๊ณ ๋ฏผํ๋ค ๋ณด๋ ํ ๊ฐ์ง ์๊ฐ์ด ๋ค์์ต๋๋ค.
“์ด ๋ฌธ์ ๋ฅผ ์ ๋ง AI๋ก ํด๊ฒฐํด์ผ ํ๋ ๊ฑธ๊น?”
์ด๋ฏธ ์ ํ์๋ OpenAPI ๊ธฐ๋ฐ Swagger ๋ช
์ธ๊ฐ ์กด์ฌํ๊ณ ์์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด ์ฌ๋์ด ํ๋กฌํํธ๋ฅผ ํตํด ๋ฐ๋ณต ์์ฑํ๋๋ก ๋ง๋๋ ๊ฒ๋ณด๋ค, OpenAPI ์คํ ์์ฒด๋ฅผ ํ์ฉํ๋ ๊ฒ์ด ๋ ๊ตฌ์กฐ์ ์ด์ง ์์๊น ์๊ฐํ์ต๋๋ค. ๊ทธ๋ ๊ฒ ๋ฐ๊ฒฌํ ๋๊ตฌ๊ฐ ๋ฐ๋ก Orval์ด์์ต๋๋ค.
Orval์ ์ ํํ ์ด์
์ฐ๋ฆฌ ํ์ ๋ฐฑ์๋๊ฐ ์ด๋ฏธ OpenAPI ๊ธฐ๋ฐ์ผ๋ก API๋ฅผ ๊ฐ๋ฐํ๊ณ ์์์ต๋๋ค.
์ฆ, ์๋ก์ด ํ๋ก์ธ์ค๋ฅผ ๊ฐ์ํ๋ ๊ฒ์ด ์๋๋ผ ์ด๋ฏธ ์กด์ฌํ๋ ๋ช ์ธ๋ฅผ ํ์ฉํ ์ ์๋ ์ํฉ์ด์์ต๋๋ค.
Orval์ OpenAPI ์คํ์ ๊ธฐ๋ฐ์ผ๋ก ๋ค์์ ์๋ ์์ฑํ ์ ์์์ต๋๋ค.
- axios ๊ธฐ๋ฐ API ํจ์
- react-query hook
- TypeScript ํ์
- MSW mock ๋ฐ์ดํฐ
์ฆ, ๊ธฐ์กด์ ๋ฐ๋ณต์ ์ผ๋ก ์์ฑํ๋ boilerplate ๋๋ถ๋ถ์ ์๋ํํ ์ ์์์ต๋๋ค.
ํ์ง๋ง ์ ์ฒด ์ ์ฉ์ ํ์ง ์์๋ค
์ค์ํ๋ ๊ฑด “์ด๋ป๊ฒ ๋์ ํ ๊ฒ์ธ๊ฐ”์์ต๋๋ค.
์ด๋ฏธ ์๋น์ค์๋ ๊ธฐ์กด API ๊ตฌ์กฐ๊ฐ ๋งค์ฐ ๋ง์ด ์กด์ฌํ๊ณ ์์๊ณ , ์ ์ฒด๋ฅผ ํ ๋ฒ์ ๋ณ๊ฒฝํ๋ ๊ฒ์ ์ํฅ๋๊ฐ ๋๋ฌด ์ปธ์ต๋๋ค.
๊ทธ๋์ ๋ฐฑ์๋ ๊ฐ๋ฐ์์ ๋ ผ์ ํ ์ ๊ท๋ก ์ถ๊ฐ๋๋ API์ ๋ํด์๋ง ์ ๋ณ์ ์ผ๋ก ์ ์ฉ ํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
์ด ๊ฒฐ์ ์ ๊ฒฐ๊ณผ์ ์ผ๋ก ๋งค์ฐ ์ค์ํ์ต๋๋ค.
์๋ํ๋ฉด:
- ๊ธฐ์กด ์ฝ๋ ์ํฅ ์ต์ํ
- ์ ์ง์ ๋์ ๊ฐ๋ฅ
- ํ ์ ์ ๋น์ฉ ๊ฐ์
- ๋ฆฌ์คํฌ ๊ด๋ฆฌ ๊ฐ๋ฅ
์ด๋ผ๋ ์ฅ์ ์ด ์์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ค๋ฌด์์๋ “์ข์ ๊ธฐ์ ”๋ณด๋ค “์์ ํ๊ฒ ๋์ ๊ฐ๋ฅํ ๊ธฐ์ ”์ด ๋ ์ค์ํ๋ค๊ณ ์๊ฐํฉ๋๋ค.
์ค์ ์ ์ฉ ๋ฐฉ์
์ฐ๋ฆฌ๋ tags-split ๋ชจ๋๋ฅผ ํ์ฉํด OpenAPI tag ๊ธฐ์ค์ผ๋ก ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ์ต๋๋ค.
export default defineConfig({
ecommerceApi: {
input: {
target: 'http://localhost:8080/v3/api-docs',
},
output: {
mode: 'tags-split',
target: 'src/api/generated',
client: 'react-query',
mock: true,
},
},
});
config๋ฅผ ์ค์ ํ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํฉ๋๋ค.
yarn generate:api
์์ script ํ ๋ฒ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๊ฐ ์๋ ์์ฑ๋์์ต๋๋ค.
src/api/generated/
โโโ product/
โโโ category/
โโโ user/
โโโ order/
โโโ ...
์ค์ ๋ก๋ ๋ค์๊ณผ ๊ฐ์ ๊ฒ๋ค๊น์ง ๋ชจ๋ ์์ฑ๋์์ต๋๋ค.
- axios ํจ์
- react-query hook
- schema type
- msw mock
๊ฒฐ๊ณผ — API ์ฐ๋ ์๊ฐ์ด ๊ทน์ ์ผ๋ก ๊ฐ์ํ๋ค
๋์ ์ดํ ๊ฐ์ฅ ํฌ๊ฒ ์ฒด๊ฐํ ๊ฒ์ “์ด์ API ์ฐ๋ ์์ฒด์ ์๊ฐ์ ๊ฑฐ์ ์ฐ์ง ์๊ฒ ๋์๋ค”๋ ์ ์ด์์ต๋๋ค.
AS-IS
๊ธฐ์กด์๋ ์๋ก์ด API ํ๋๊ฐ ์ถ๊ฐ๋ ๋๋ง๋ค ๋ค์๊ณผ ๊ฐ์ ์ก์ ์ ๋ฐ๋ณตํ์ต๋๋ค.
- axios ํจ์ ์์ฑ
- ํ์ ์ ์
- react-query hook ์์ฑ
- queryKey ๊ด๋ฆฌ
- mutation/query ๋ถ๋ฆฌ
๋ฑ์ ์ง์ ๊ตฌํํด์ผ ํ์ต๋๋ค.
TO-BE
ํ์ง๋ง Orval ๋์ ์ดํ์๋ ๋ค์ ์ก์ ์ผ๋ก ๋๋ถ๋ถ์ด ํด๊ฒฐ๋์์ต๋๋ค.
- OpenAPI ์ ์
- generate ์คํ
์ค์ ๋ก ์๋น์ค ์ ์ฒด ๊ฐํธ ๊ธฐ๊ฐ ๋์ ๊ธฐ์กด ๋๋น API ์ฐ๋์ ์ฌ์ฉํ๋ ์๊ฐ์ 10%๋ ์ ๋๋ ์์ค์ผ๋ก ๊ฐ๋ฐ ์๊ฐ์ ์ค์ผ ์ ์์์ต๋๋ค.
๊ทธ ๋๋ถ์ ์๋์ ๊ฐ์ “์ ํ ์์ฒด์ ๋ฌธ์ ”์ ๋ ๋ง์ ์๊ฐ์ ์ฌ์ฉํ ์ ์์์ต๋๋ค.
- ์ด๋ฐํ ์ผ์ ๋์
- ๊ธฐํ ๋ณ๊ฒฝ ๋์
- UI/UX ๊ฐ์ ์ง์ค
๊ทธ๋ฐ๋ฐ ์๋ํ์๋ ๋ฌธ์ ๊ฐ ์์๋ค
ํ์ง๋ง ์ฌ์ฉํ๋ฉด์ ๋ ๋ค๋ฅธ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
์ฐ๋ฆฌ๋ tags-split ๋ชจ๋๋ฅผ ์ฌ์ฉํ๊ณ ์์๊ธฐ ๋๋ฌธ์ OpenAPI์ tag ์ด๋ฆ ๊ธฐ์ค์ผ๋ก generated ํ์ผ ๊ฒฝ๋ก๊ฐ ์์ฑ๋์์ต๋๋ค.
์๋ฅผ ๋ค๋ฉด ๋ค์ ๊ตฌ์กฐ์ ๋๋ค.
import { useProducts } from '@/api/generated/product/product';
๋ฌธ์ ๋ ๋ฐฑ์๋์์ tag ์ด๋ฆ์ ๋ณ๊ฒฝํ๋ฉด generated ๊ฒฝ๋ก ์์ฒด๊ฐ ๋ฐ๋์ด๋ฒ๋ฆฐ๋ค๋ ์ ์ด์์ต๋๋ค.
๊ฒฐ๊ตญ ํ๋ก์ ํธ ์ ๋ฐ์์ import ๊ฒฝ๋ก๊ฐ ๋๋์ผ๋ก ๊นจ์ง๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
์ฆ, ์๋ ์์ฑ ์ฝ๋์ ์๋น์ค ์ฝ๋๊ฐ ๋๋ฌด ๊ฐํ๊ฒ ๊ฒฐํฉ(coupling)๋์ด ์์๋ ๊ฒ์ ๋๋ค.
ํด๊ฒฐ — Generated Layer๋ฅผ ์ง์ ์ฐธ์กฐํ์ง ์๋๋ก ๋ง๋ค์๋ค
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๊ตฌ์กฐ๋ฅผ ๋ค์ ์ ๋ฆฌํ์ต๋๋ค.
ํต์ฌ ์์ด๋์ด๋ ์๋น์ค ์ฝ๋๊ฐ generated ๊ตฌ์กฐ๋ฅผ ์ง์ ์์ง ๋ชปํ๊ฒ ๋ง๋๋ ๊ฒ์ด์์ต๋๋ค.
์ ๋ ์ค๊ฐ layer๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
src/api/hooks/*
// product.ts
export * from '../generated/product/product';
์์ ์์ ์ฝ๋์ฒ๋ผ barrel ํ์ผ๋ง ๋์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์๋น์ค ์ฝ๋์์๋ ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉ ๊ฐ๋ฅํ๋๋ก ๋ณ๊ฒฝํ์์ต๋๋ค.
import { useProducts } from '@/api/hooks/product';
์ด ๊ตฌ์กฐ๊ฐ ์ค์ํ๋ ์ด์
์ดํ๋ถํฐ๋ ๋ฐฑ์๋์์ tag ์ด๋ฆ์ด ๋ณ๊ฒฝ๋๋๋ผ๋
- generated ๊ฒฝ๋ก๋ง ์์
- barrel export๋ง ์์
ํ๋ฉด ๋์๊ณ , ์ค์ ์๋น์ค ์ฝ๋ ์์ ์ ๊ฑฐ์ ๋ฐ์ํ์ง ์๊ฒ ๋์์ต๋๋ค.
๊ฒฐ๊ตญ ์ด ๊ตฌ์กฐ๋
- generated layer
- service layer
์ฌ์ด์ ์์ถฉ ์ง์ ์ ๋ง๋ ์ ์ด์์ต๋๋ค.
์๋ํ ์์ฒด๋ณด๋ค “์๋ํ ๊ฒฐ๊ณผ๋ฌผ์ ์ด๋ป๊ฒ ๊ฒฉ๋ฆฌํ ๊ฒ์ธ๊ฐ”๊ฐ ๋ ์ค์ํ๋ค๋ ๊ฒ์ ๊ฒฝํํ๊ฒ ๋ ์๊ฐ์ด์์ต๋๋ค.
๋ง๋ฌด๋ฆฌ
์ด๋ฒ ๊ฒฝํ์ ํตํด ๋๋ ์ ์ ๋จ์ํ “Orval์ด ํธํ๋ค”๊ฐ ์๋์์ต๋๋ค.
์ ๋ง ์ค์ํ๋ ๊ฒ์ ๋ค์๊ณผ ๊ฐ์ ์ด์ ์ด์์ต๋๋ค.
- ๋ฐ๋ณต ์์ ์ ๊ฑฐ
- ๊ฐ๋ฐ์ ์ง์ค๋ ํฅ์
- ์ผ์ ๋์ ๋ฅ๋ ฅ ํ๋ณด
- ๋ณ๊ฒฝ ๋์ ๋น์ฉ ๊ฐ์
ํนํ ์๋น์ค ์ ์ฒด ๊ฐํธ์ฒ๋ผ ์๊ตฌ์ฌํญ์ด ๊ณ์ ๋ฐ๋๊ณ , API๊ฐ ๊ธ์ฆํ๋ฉฐ ์ผ์ ์ ๊ณ ์ ๋ ์ํฉ์์๋ DX ๊ฐ์ ์ด ๋จ์ํ ํธ์์ฑ์ด ์๋๋ผ ์์ฐ์ฑ๊ณผ ์ผ์ ์์ ์ฑ์ ์ง์ ์ ์ผ๋ก ์ข์ฐํ๋ค๊ณ ๋๊ผ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ ํ๋ ๋๋ ์ ์ ์๋ํ๋ ์์ฑ ์์ฒด๋ณด๋ค coupling ๊ด๋ฆฌ๊ฐ ๋ ์ค์ํ๋ค๋ ์ ์ด์์ต๋๋ค.
๋ฐ๋ณต ์์
์ ์ค์ด๋ ๊ฒ๋ง์ผ๋ก ๋๋๋ ๊ฒ์ด ์๋๋ผ,
๊ทธ ์๋ํ ๊ฒฐ๊ณผ๋ฌผ์ด ์๋น์ค ๊ตฌ์กฐ๋ฅผ ์นจ๋ฒํ์ง ์๋๋ก ์ค๊ณํ๋ ๊ฒ๊น์ง ํฌํจ๋์ด์ผ ์ง์ง DX ๊ฐ์ ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค.
๋๊ธ