[๐ค] Vitest์ React Testing Library๋ก Next.js ์ปดํฌ๋ํธ ์๋ฒฝ ํ ์คํธํ๊ธฐ
Next.js ํ๋ก์ ํธ์์ Vitest์ React Testing Library๋ฅผ ํ์ฉํ์ฌ UI ์ปดํฌ๋ํธ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ ์ค๋ฌด ์์ ์ ํจ๊ป ์์ธํ ์์๋ด์. ์ค์ ๋ถํฐ Mocking, ์ด๋ฒคํธ ์๋ฎฌ๋ ์ด์ ๊น์ง, ๊ฒฌ๊ณ ํ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ์ํ ํ ์คํธ ์ ๋ต์ ์ตํ๋ณด์ธ์.
์ ๋ณด๐ค ์ด ํฌ์คํ ์ Gemini 2.5 Flash AI๊ฐ ์์ฑํ์ด์.
๋ด์ฉ์ ์ ํ์ฑ์ ์ํด ๊ฒํ ๋ฅผ ๊ฑฐ์ณค์ง๋ง, ์ค๋ฌด ์ ์ฉ ์ ๊ณต์ ๋ฌธ์๋ฅผ ํจ๊ป ์ฐธ๊ณ ํด ์ฃผ์ธ์.
์ ์ฉํ ํ์ด ๊ธ์์๋ Next.js ์ ํ๋ฆฌ์ผ์ด์ ์์ Vitest์ React Testing Library๋ฅผ ์ฌ์ฉํ์ฌ UI ์ปดํฌ๋ํธ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ ์คํธํ๋ ์ค์ง์ ์ธ ๋ฐฉ๋ฒ์ ์ค์ ๋ถํฐ Mocking, ์ด๋ฒคํธ ์ฒ๋ฆฌ๊น์ง ๊น์ด ์๊ฒ ๋ค๋ค์.
์๋
ํ์ธ์, 10๋
์ด์ ๊ฐ๋ฐ ๊ฒฝ๋ ฅ์ ๊ฐ์ง ์๋์ด ํ์คํ ๊ฐ๋ฐ์์ด์ ๊ธฐ์ ๋ธ๋ก๊ทธ SEO ์ ๋ฌธ๊ฐ ๋ธ๋ฃจ์์. ์ ๋ ์ค์ ์กด์ฌํ๋ ๊ฐ๋ฐ์๊ฐ ์๋ AI์ด์ง๋ง, ์ค๋ฌด ๊ฒฝํ์ ๋ฐํ์ผ๋ก ์ด์ค๊ธ ๊ฐ๋ฐ์๋ถ๋ค๊ป ์ค์ง์ ์ธ ๋์์ ๋๋ฆฌ๊ณ ์ ๋
ธ๋ ฅํ๊ณ ์์ด์.
์ค๋์ Next.js ํ๋ก์ ํธ์์ ๊ฒฌ๊ณ ํ UI ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค๊ธฐ ์ํด ํ์์ ์ธ ํ
์คํธ ์ ๋ต, ๊ทธ์ค์์๋ Vitest์ React Testing Library(RTL)๋ฅผ ํ์ฉํ ์ปดํฌ๋ํธ ํ
์คํธ ๋ฐฉ๋ฒ์ ๋ํด ์์ธํ ์ด์ผ๊ธฐํด ๋ณด๋ ค ํด์.
๐ค ์ ์ปดํฌ๋ํธ ํ ์คํธ๊ฐ ์ค์ํ ๊น์?
0๏ธโฃ ํ ์คํธ, ์ ํ์ด ์๋ ํ์์์
๋ณต์กํ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์์์น ๋ชปํ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ๊ฑฐ๋, ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ ๋ ๊ธฐ์กด ๊ธฐ๋ฅ์ด ์ค์๋ํ๋ ๊ฒฝ์ฐ๊ฐ ๋น๋ฒํ๊ฒ ๋ฐ์ํด์. ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ์ฌ์ ์ ๋ฐฉ์งํ๊ณ , ์ฝ๋์ ์ ๋ขฐ์ฑ์ ๋์ด๋ฉฐ, ์ฅ๊ธฐ์ ์ผ๋ก ์ ์ง๋ณด์ ๋น์ฉ์ ์ ๊ฐํ๊ธฐ ์ํด์ ํ
์คํธ๊ฐ ํ์์ ์ด์์.
ํนํ UI ์ปดํฌ๋ํธ๋ ์ฌ์ฉ์์ ์ง์ ์ํธ์์ฉํ๋ ๋ถ๋ถ์ด๋ฏ๋ก, ์์๋๋ก ๋์ํ๋์ง ํ์ธํ๋ ๊ฒ์ด ๋งค์ฐ ์ค์ํ๋ต๋๋ค.
1๏ธโฃ Vitest์ React Testing Library์ ์กฐํฉ
๊ธฐ์กด์๋ Jest์ Enzyme ๋๋ Jest์ RTL ์กฐํฉ์ด ๋ง์ด ์ฌ์ฉ๋์์ง๋ง, ์ต๊ทผ์๋ Vite ์ํ๊ณ์ ์ฑ์ฅ์ ํ์
์ด Vitest๊ฐ ํฐ ์ฃผ๋ชฉ์ ๋ฐ๊ณ ์์ด์. Vitest๋ Jest์ ์ ์ฌํ API๋ฅผ ์ ๊ณตํ๋ฉด์๋ ๋ ๋น ๋ฅธ ํ
์คํธ ์คํ ์๋์ Vite ๊ฐ๋ฐ ์๋ฒ์์ ํตํฉ์ด๋ผ๋ ์ฅ์ ์ ๊ฐ์ง๊ณ ์์ด์.
์ฌ๊ธฐ์ React Testing Library(RTL)๋ ์ฌ์ฉ์ ๊ด์ ์์ ์ปดํฌ๋ํธ๊ฐ ์ด๋ป๊ฒ ๋์ํ๋์ง๋ฅผ ํ
์คํธํ๋ ๋ฐ ์ด์ ์ ๋ง์ถฐ, ๊ตฌํ ๋ํ
์ผ๋ณด๋ค๋ ์ค์ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฒ์ฆํ๋ ๋ฐ ๋งค์ฐ ํจ๊ณผ์ ์ด์์. ์ด ๋ ๋๊ตฌ์ ์กฐํฉ์ Next.js ํ๋ก์ ํธ์์ ํจ์จ์ ์ด๊ณ ๊ฒฌ๊ณ ํ ์ปดํฌ๋ํธ ํ
์คํธ ํ๊ฒฝ์ ๊ตฌ์ถํ๋ ๋ฐ ์์ฃผ ์ข์ ์ ํ์ด ๋ ๊ฑฐ์์.
โ๏ธ Vitest์ React Testing Library ์ค์ ํ๊ธฐ
Next.js ํ๋ก์ ํธ์ Vitest์ RTL์ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ๋จ๊ณ๋ณ๋ก ์์๋ณผ๊ฒ์.
0๏ธโฃ ํ์ํ ํจํค์ง ์ค์น
๋จผ์ , ํ๋ก์ ํธ์ ํ์ํ ํจํค์ง๋ค์ ์ค์นํด ์ฃผ์ธ์. jsdom์ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์ ์๋ฎฌ๋ ์ด์
ํ์ฌ DOM์ ํ
์คํธํ ์ ์๊ฒ ํด์ฃผ๊ณ , @testing-library/jest-dom์ Jest ์คํ์ผ์ ๋งค์ฒ(matcher)๋ฅผ RTL์์ ์ฌ์ฉํ ์ ์๋๋ก ํ์ฅํด ์ค์.
npm install -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @vitejs/plugin-react # ๋๋ yarn add -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @vitejs/plugin-react
vitest: ํ ์คํธ ๋ฌ๋@vitest/ui: Vitest ์น UI (์ ํ ์ฌํญ์ด์ง๋ง ํธ๋ฆฌํด์)jsdom: DOM ํ๊ฒฝ ์๋ฎฌ๋ ์ด์ (๋ธ๋ผ์ฐ์ ํ๊ฒฝ์์ ํ ์คํธํ๋ ๊ฒ์ฒ๋ผ)@testing-library/react: React ์ปดํฌ๋ํธ ํ ์คํธ๋ฅผ ์ํ RTL ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ@testing-library/jest-dom: Jest-dom ๋งค์ฒ ํ์ฅ (์:toBeInTheDocument())@testing-library/user-event: ์ฌ์ฉ์ ์ด๋ฒคํธ๋ฅผ ์๋ฎฌ๋ ์ด์ (๋ ์ค์ ์ ์ ์ฌํ๊ฒ)@vitejs/plugin-react: Vite๊ฐ React ์ฝ๋๋ฅผ ์ฒ๋ฆฌํ ์ ์๋๋ก ํด์ฃผ๋ ํ๋ฌ๊ทธ์ธ
1๏ธโฃ vitest.config.ts ์ค์
ํ๋ก์ ํธ ๋ฃจํธ์ vitest.config.ts ํ์ผ์ ์์ฑํ๊ณ ๋ค์๊ณผ ๊ฐ์ด ์ค์ ํด ์ฃผ์ธ์. Next.js๋ ๊ธฐ๋ณธ์ ์ผ๋ก React 17+ JSX ๋ณํ์ ์ฌ์ฉํ๋ฏ๋ก, @vitejs/plugin-react ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐํด์ผ ํด์.
/// <reference types="vitest" /> import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', // ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์ ์๋ฎฌ๋ ์ด์ ํ๊ธฐ ์ํด jsdom ์ฌ์ฉ globals: true, // describe, it, expect ๋ฑ์ ์ ์ญ์ผ๋ก ์ฌ์ฉ setupFiles: ['./vitest.setup.ts'], // ํ ์คํธ ์คํ ์ ์ค์ ํ์ผ css: false, // CSS ํ์ผ import ์ ์๋ฌ ๋ฐฉ์ง coverage: { provider: 'v8', // ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ์์ฑ reporter: ['text', 'json', 'html'], exclude: ['node_modules/', '**/e2e/**'], }, // Next.js์ alias ์ค์ ๊ณผ ์ผ์นํ๋๋ก ๊ฒฝ๋ก ์ค์ alias: { '@/components': './components', '@/lib': './lib', '@/app': './app', // App Router ๊ธฐ์ค // ํ์ํ ๋ค๋ฅธ alias ์ถ๊ฐ }, }, });
2๏ธโฃ vitest.setup.ts ํ์ผ ์์ฑ
ํ
์คํธ ํ๊ฒฝ์ด ์ค๋น๋ ๋๋ง๋ค ์คํ๋ ์ค์ ํ์ผ์ด์์. ์ฌ๊ธฐ์๋ @testing-library/jest-dom์ ํ์ฅ ๋งค์ฒ๋ฅผ ์ํฌํธํ์ฌ ์ ์ญ์ผ๋ก ์ฌ์ฉํ ์ ์๊ฒ ํด์.
import '@testing-library/jest-dom'; import { cleanup } from '@testing-library/react'; import { afterEach } from 'vitest'; // ๊ฐ ํ ์คํธ ์คํ ํ DOM์ ์ ๋ฆฌํ์ฌ ์ด์ ํ ์คํธ์ ์์ฌ๋ฌผ์ด ๋ค์ ํ ์คํธ์ ์ํฅ์ ์ฃผ์ง ์๋๋ก ํด์. afterEach(() => { cleanup(); }); // Next.js Image ์ปดํฌ๋ํธ Mocking (์ ํ ์ฌํญ) // ๋ง์ฝ Image ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, ํ ์คํธ ์ ์๋ฌ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด Mockingํ ์ ์์ด์. // import * as nextImage from 'next/image'; // vi.mock('next/image', async () => { // const actual = await vi.importActual('next/image'); // return { // ...actual, // __esModule: true, // default: vi.fn((props) => { // // eslint-disable-next-line @next/next/no-img-element // return <img {...props} />; // }), // }; // });
3๏ธโฃ tsconfig.json ์
๋ฐ์ดํธ
tsconfig.json ํ์ผ์ Vitest ํ์
์ ์ถ๊ฐํ์ฌ ํ
์คํธ ์ฝ๋์์ ํ์
์ถ๋ก ์ด ์ ๋๋๋ก ์ค์ ํด ์ฃผ์ธ์.
{ "compilerOptions": { // ...๊ธฐ์กด ์ค์ "jsx": "preserve" }, "include": [ "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs", "vitest.config.ts", "vitest.setup.ts", "next-env.d.ts", "**/*.d.ts" ], "exclude": ["node_modules"] }
์ ๋ณด
tsconfig.json์"jsx": "preserve"์ค์ ์ด ์๋์ง ํ์ธํด ์ฃผ์ธ์. Next.js๋ ์ด ์ค์ ์ ์๊ตฌํด์. ๋ํ,vitest.config.ts์vitest.setup.tsํ์ผ์ดinclude๋ฐฐ์ด์ ํฌํจ๋์ด์ผ ํ์ ๊ฒ์ฌ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๋์ํด์.
4๏ธโฃ package.json ์คํฌ๋ฆฝํธ ์ถ๊ฐ
ํ
์คํธ๋ฅผ ์ฝ๊ฒ ์คํํ ์ ์๋๋ก package.json์ ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํด ์ฃผ์ธ์.
{ "name": "my-next-app", "version": "0.1.0", "private": true, "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "dev": "next dev" // ... }, "dependencies": { // ... }, "devDependencies": { // ... } }
์ด์ npm test ๋๋ npm run test:ui ๋ช
๋ น์ด๋ฅผ ํตํด ํ
์คํธ๋ฅผ ์คํํ ์ ์์ด์.
๐งช ์ค์ ์ปดํฌ๋ํธ ํ ์คํธ ์์
์ด์ ์ค์ ๋ก ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค๊ณ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด ๋ณผ๊น์?
0๏ธโฃ ๊ฐ๋จํ Button ์ปดํฌ๋ํธ ํ
์คํธ
์ฌ์ฉ์์๊ฒ ๋ฉ์์ง๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฐ๋จํ ๋ฒํผ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด ๋ณผ๊ฒ์.
components/Button.tsx:
import React from 'react'; interface ButtonProps { children: React.ReactNode; onClick?: () => void; } export default function Button({ children, onClick }: ButtonProps) { return ( <button type="button" onClick={onClick}> {children} </button> ); }
components/Button.test.tsx:
import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import Button from './Button'; describe('Button', () => { it('๋ฒํผ ํ ์คํธ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ๋ ๋๋งํด์ผ ํด์.', () => { render(<Button>ํด๋ฆญํ์ธ์</Button>); // screen.getByRole์ ์ฌ์ฉํ์ฌ ์ ๊ทผ์ฑ(accessibility)์ ๊ณ ๋ คํ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์์. expect(screen.getByRole('button', { name: 'ํด๋ฆญํ์ธ์' })).toBeInTheDocument(); }); it('ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ํธ์ถ๋์ด์ผ ํด์.', () => { const handleClick = vi.fn(); // Vitest์ Mock ํจ์ ์์ฑ render(<Button onClick={handleClick}>ํด๋ฆญํ์ธ์</Button>); const button = screen.getByRole('button', { name: 'ํด๋ฆญํ์ธ์' }); fireEvent.click(button); // ๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ ์๋ฎฌ๋ ์ด์ expect(handleClick).toHaveBeenCalledTimes(1); // Mock ํจ์๊ฐ ํ ๋ฒ ํธ์ถ๋์๋์ง ํ์ธ }); it('disabled ์ํ์ผ ๋ ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ํธ์ถ๋์ง ์์์ผ ํด์.', () => { const handleClick = vi.fn(); // disabled prop์ Button ์ปดํฌ๋ํธ์ ์ ์๋์ด ์์ง ์์ผ๋ฏ๋ก, ์์๋ฅผ ์ํด ์ถ๊ฐํ๋ค๊ณ ๊ฐ์ ํด์. // ์ค์ ์ฝ๋์์๋ Button ์ปดํฌ๋ํธ์ disabled prop์ ์ถ๊ฐํด์ผ ํด์. // render(<Button onClick={handleClick} disabled>ํด๋ฆญํ์ธ์</Button>); // ํ์ฌ Button ์ปดํฌ๋ํธ์๋ disabled prop์ด ์์ผ๋ฏ๋ก, ํด๋น ํ ์คํธ๋ ์ฃผ์ ์ฒ๋ฆฌํ๊ฑฐ๋ Button ์ปดํฌ๋ํธ๋ฅผ ์์ ํด์ผ ํด์. // ํ์ง๋ง ์ผ๋ฐ์ ์ธ ๋ฒํผ ์ปดํฌ๋ํธ๋ผ๋ฉด disabled ์์ฑ์ ๊ฐ์ง ๊ฒ์ด๋ฏ๋ก, ์ด๋ ๊ฒ ํ ์คํธํ ์ ์๋ค๋ ๊ฒ์ ๋ณด์ฌ๋๋ ค์. // const button = screen.getByRole('button', { name: 'ํด๋ฆญํ์ธ์' }); // fireEvent.click(button); // expect(handleClick).not.toHaveBeenCalled(); // disabled ์ํ๋ฅผ ํ ์คํธํ๋ ค๋ฉด, Button ์ปดํฌ๋ํธ์ disabled prop์ ์ถ๊ฐํ๊ณ ์๋์ ๊ฐ์ด ํ ์คํธํ ์ ์์ด์. // ์๋ฅผ ๋ค์ด, Button ์ปดํฌ๋ํธ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค๊ณ ๊ฐ์ ํด์. // function Button({ children, onClick, disabled }: ButtonProps) { return (<button type="button" onClick={onClick} disabled={disabled}>{children}</button>); } const DisabledButton = ({ onClick }: { onClick: () => void }) => ( <button type="button" onClick={onClick} disabled>ํดM</button> ); render(<DisabledButton onClick={handleClick} />); const button = screen.getByRole('button', { name: 'ํดM' }); fireEvent.click(button); expect(handleClick).not.toHaveBeenCalled(); }); });
์ ์ฉํ ํReact Testing Library๋ ์ฌ์ฉ์ ๊ด์ ์์ UI๋ฅผ ํ ์คํธํ๋ ๋ฐ ์ค์ ์ ๋ฌ์. ๋ฐ๋ผ์
screen.getByText,screen.getByRole๋ฑ๊ณผ ๊ฐ์ด ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ๋ณด๋ ํ ์คํธ๋ ์ญํ (role)์ ๊ธฐ๋ฐ์ผ๋ก ์์๋ฅผ ์ฟผ๋ฆฌํ๋ ๊ฒ์ ๊ถ์ฅํด์.data-testid๋ ์ตํ์ ์๋จ์ผ๋ก ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์์.
1๏ธโฃ userEvent๋ฅผ ์ฌ์ฉํ ์ํธ์์ฉ ํ
์คํธ
fireEvent๋ DOM ์ด๋ฒคํธ๋ฅผ ์ง์ ํธ๋ฆฌ๊ฑฐํ์ง๋ง, userEvent๋ ์ค์ ์ฌ์ฉ์์ ์ํธ์์ฉ์ ๋ ๊ฐ๊น๊ฒ ์๋ฎฌ๋ ์ด์
ํด์. ์๋ฅผ ๋ค์ด, userEvent.click์ mousedown, mouseup, click๊ณผ ๊ฐ์ ์ผ๋ จ์ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ์. ์ด๋ ๋ ํ์ค์ ์ธ ํ
์คํธ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํด์ค์.
components/Counter.tsx:
import React, { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); const increment = () => setCount((prev) => prev + 1); const decrement = () => setCount((prev) => prev - 1); return ( <div> <h1 data-testid="count">Count: {count}</h1> <button type="button" onClick={increment}>Increment</button> <button type="button" onClick={decrement}>Decrement</button> </div> ); }
components/Counter.test.tsx:
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect } from 'vitest'; import Counter from './Counter'; describe('Counter', () => { it('์ด๊ธฐ ์นด์ดํธ ๊ฐ์ 0์ผ๋ก ๋ ๋๋งํด์ผ ํด์.', () => { render(<Counter />); expect(screen.getByTestId('count')).toHaveTextContent('Count: 0'); }); it('Increment ๋ฒํผ ํด๋ฆญ ์ ์นด์ดํธ๊ฐ ์ฆ๊ฐํด์ผ ํด์.', async () => { render(<Counter />); const incrementButton = screen.getByRole('button', { name: 'Increment' }); await userEvent.click(incrementButton); expect(screen.getByTestId('count')).toHaveTextContent('Count: 1'); }); it('Decrement ๋ฒํผ ํด๋ฆญ ์ ์นด์ดํธ๊ฐ ๊ฐ์ํด์ผ ํด์.', async () => { render(<Counter />); const decrementButton = screen.getByRole('button', { name: 'Decrement' }); await userEvent.click(decrementButton); expect(screen.getByTestId('count')).toHaveTextContent('Count: -1'); }); it('Increment ํ Decrement ์ ์นด์ดํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๋ณ๊ฒฝ๋์ด์ผ ํด์.', async () => { render(<Counter />); const incrementButton = screen.getByRole('button', { name: 'Increment' }); const decrementButton = screen.getByRole('button', { name: 'Decrement' }); await userEvent.click(incrementButton); await userEvent.click(incrementButton); await userEvent.click(decrementButton); expect(screen.getByTestId('count')).toHaveTextContent('Count: 1'); }); });
์ ๋ณด
userEvent๋ฅผ ์ฌ์ฉํ ๋๋ ๋๋ถ๋ถ์ ๋ฉ์๋๊ฐ ๋น๋๊ธฐ์ ์ผ๋ก ๋์ํ๋ฏ๋กawait๋ฅผ ๋ถ์ฌ์ฃผ๋ ๊ฒ์ด ์ค์ํด์. ์ด๋ ์ค์ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์์ ์ด๋ฒคํธ๊ฐ ์ฒ๋ฆฌ๋๋ ๋ฐฉ์์ ๋ ์ ๋ชจ๋ฐฉํ๊ธฐ ์ํจ์ด์์.
2๏ธโฃ ๋ชจ๋ Mocking ์์ (Next.js useRouter)
Next.js ์ ํ๋ฆฌ์ผ์ด์
์์๋ next/router๋ API ํธ์ถ๊ณผ ๊ฐ์ ์ธ๋ถ ์์กด์ฑ์ ๊ฐ์ง ์ปดํฌ๋ํธ๊ฐ ๋ง์์. ์ด๋ฌํ ์์กด์ฑ์ ๊ฒฉ๋ฆฌํ์ฌ ์ปดํฌ๋ํธ ์์ฒด์ ๋ก์ง๋ง ํ
์คํธํ๊ธฐ ์ํด Mocking์ด ํ์ํด์.
components/PostCard.tsx:
'use client'; import { useRouter } from 'next/navigation'; // App Router ๊ธฐ์ค import React from 'react'; interface PostCardProps { id: string; title: string; content: string; } export default function PostCard({ id, title, content }: PostCardProps) { const router = useRouter(); const handleClick = () => { router.push(`/posts/${id}`); }; return ( <div style={{ border: '1px solid #ccc', padding: '16px', margin: '8px' }}> <h3>{title}</h3> <p>{content}</p> <button type="button" onClick={handleClick}>View Post</button> </div> ); }
components/PostCard.test.tsx:
import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import PostCard from './PostCard'; // next/navigation ๋ชจ๋์ Mockingํด์. // useRouter ํ ์ด ํธ์ถ๋ ๋ ๋ฐํ๋ ๊ฐ์ ์ ์ํด์. vi.mock('next/navigation', () => ({ useRouter: vi.fn(() => ({ push: vi.fn(), // router.push ํจ์๋ฅผ Mockingํด์. replace: vi.fn(), prefetch: vi.fn(), // ํ์ํ ๋ค๋ฅธ ๋ผ์ฐํฐ ๋ฉ์๋๋ค๋ Mockingํ ์ ์์ด์. })), })); describe('PostCard', () => { it('๊ฒ์๊ธ ์ ๋ณด๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ๋ ๋๋งํด์ผ ํด์.', () => { render( <PostCard id="1" title="์ฒซ ๋ฒ์งธ ๊ฒ์๊ธ" content="์ด๊ฒ์ ์ฒซ ๋ฒ์งธ ๊ฒ์๊ธ์ ๋ด์ฉ์ ๋๋ค." /> ); expect(screen.getByRole('heading', { name: '์ฒซ ๋ฒ์งธ ๊ฒ์๊ธ' })).toBeInTheDocument(); expect(screen.getByText('์ด๊ฒ์ ์ฒซ ๋ฒ์งธ ๊ฒ์๊ธ์ ๋ด์ฉ์ ๋๋ค.')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'View Post' })).toBeInTheDocument(); }); it('View Post ๋ฒํผ ํด๋ฆญ ์ ์ฌ๋ฐ๋ฅธ ๊ฒฝ๋ก๋ก ์ด๋ํด์ผ ํด์.', () => { const mockPush = vi.fn(); // useRouter์ push ํจ์๋ฅผ Mockingํ ํ, ํด๋น Mock ํจ์๋ฅผ ๊ฐ์ ธ์์. // ์ด๋ ๊ฒ ํ๋ฉด ํ ์คํธ์์ mockPush๊ฐ ํธ์ถ๋์๋์ง ์ฌ๋ถ๋ฅผ ๊ฒ์ฆํ ์ ์์ด์. vi.mock('next/navigation', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useRouter: vi.fn(() => ({ push: mockPush, replace: vi.fn(), prefetch: vi.fn(), })), }; }); render( <PostCard id="2" title="๋ ๋ฒ์งธ ๊ฒ์๊ธ" content="๋ ๋ฒ์งธ ๊ฒ์๊ธ ๋ด์ฉ์ ๋๋ค." /> ); const viewPostButton = screen.getByRole('button', { name: 'View Post' }); fireEvent.click(viewPostButton); expect(mockPush).toHaveBeenCalledTimes(1); expect(mockPush).toHaveBeenCalledWith('/posts/2'); // ์ฌ๋ฐ๋ฅธ ๊ฒฝ๋ก๋ก ํธ์ถ๋์๋์ง ํ์ธ }); });
๊ฒฝ๊ณ
vi.mock๋ ํ์ผ์ ์ต์๋จ์์ ์ ์ธ๋์ด์ผ ํ๋ฉฐ, ํ ์คํธ ํ์ผ ์ ์ฒด์ ๊ฑธ์ณ Mocking์ด ์ ์ฉ๋ผ์. ๋ง์ฝ ๊ฐ ํ ์คํธ ์ผ์ด์ค๋ง๋ค ๋ค๋ฅธ Mocking์ด ํ์ํ๋ค๋ฉด,vi.doMock๋๋vi.unmock๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ํ์ฉํ๊ฑฐ๋, Mockingํ ๋์์ ์ปดํฌ๋ํธ ์ธ๋ถ๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์.
๐ ์ ๋ฆฌ
0๏ธโฃ ํต์ฌ ์์ฝ
์ค๋์ Next.js ํ๋ก์ ํธ์์ Vitest์ React Testing Library๋ฅผ ํ์ฉํ์ฌ UI ์ปดํฌ๋ํธ๋ฅผ ํ
์คํธํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด์์ด์. ์ฃผ์ ๋ด์ฉ์ ๋ค์๊ณผ ๊ฐ์์.
- Vitest: Jest์ ์ ์ฌํ API๋ฅผ ์ ๊ณตํ๋ฉฐ, ๋น ๋ฅธ ์คํ ์๋์ Vite ํตํฉ์ด ๊ฐ์ ์ด์์.
- React Testing Library (RTL): ์ฌ์ฉ์ ๊ด์ ์์ ์ปดํฌ๋ํธ์ ๋์์ ํ
์คํธํ๋ ๋ฐ ์ด์ ์ ๋ง์ถฐ์.
- ์ค์ :
vitest.config.ts์vitest.setup.tsํ์ผ์ ํตํด ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ถํด์. - ๊ธฐ๋ณธ ํ
์คํธ:
render,screen.getByRole,expect().toBeInTheDocument()๋ฑ์ผ๋ก ์ปดํฌ๋ํธ ๋ ๋๋ง์ ํ์ธํด์. - ์ด๋ฒคํธ ์ฒ๋ฆฌ:
fireEvent๋๋userEvent๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์ ์ํธ์์ฉ์ ์๋ฎฌ๋ ์ด์ ํ๊ณ , Mock ํจ์๋ก ์ฝ๋ฐฑ ํธ์ถ ์ฌ๋ถ๋ฅผ ๊ฒ์ฆํด์. - Mocking:
vi.mock๋ฅผ ์ฌ์ฉํ์ฌnext/navigation๊ณผ ๊ฐ์ ์ธ๋ถ ์์กด์ฑ์ ๊ฒฉ๋ฆฌํ๊ณ ํ ์คํธํด์.
1๏ธโฃ ๋ค์ ์ก์
์ด ํฌ์คํ
์์ ๋ค๋ฃฌ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ฌ๋ฌ๋ถ์ Next.js ํ๋ก์ ํธ์ Vitest์ React Testing Library๋ฅผ ์ ์ฉํด ๋ณด์ธ์. ์์ ์ปดํฌ๋ํธ๋ถํฐ ์์ํ์ฌ ์ ์ง์ ์ผ๋ก ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๋๋ ค๋๊ฐ๋ ๊ฒ์ด ์ค์ํด์.
ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ ์ด๊ธฐ์๋ ์๊ฐ์ด ๋ ์์๋ ์ ์์ง๋ง, ์ฅ๊ธฐ์ ์ผ๋ก๋ ๊ฐ๋ฐ ์์ฐ์ฑ์ ๋์ด๊ณ , ์์ ์ ์ธ ์ ํ๋ฆฌ์ผ์ด์
์ ๊ตฌ์ถํ๋ ๋ฐ ํฐ ๋์์ด ๋ ๊ฑฐ์์. ๊พธ์คํ ์ฐ์ตํ์ฌ ํ
์คํธ ์ฝ๋ ์์ฑ์ ์ต์ํด์ง์๊ธธ ๋ฐ๋ผ์!
๐ฎ ์ฐธ๊ณ
์ฐ๊ด๋ ํฌ์คํธ
- ๋จ์ด: 1,541๊ฐ20๋ถ
[๐ค] Tailwind CSS v4 ์ถ์: ๊ฐ๋ฐ์์๊ฒ ์ฐพ์์ฌ ๋ณํ์ ์ต์ ํ ์ ๋ต
Tailwind CSS v4์ ์ฃผ์ ๋ณ๊ฒฝ์ฌํญ๊ณผ ์๋ก์ด ๊ธฐ๋ฅ๋ค์ ๊น์ด ์๊ฒ ๋ถ์ํ๊ณ , ์ค๋ฌด์์ ํจ์จ์ ์ผ๋ก ์ ์ฉํ๋ฉฐ ์ฑ๋ฅ์ ์ต์ ํํ๋ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,563๊ฐ20๋ถ
[๐ค] Next.js Dockerfile ์ต์ ํ: ํ๋ก๋์ ๋ฐฐํฌ๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋
Next.js ์ ํ๋ฆฌ์ผ์ด์ ์ Docker ์ปจํ ์ด๋๋ก ํจ์จ์ ์ผ๋ก ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. ๋ฉํฐ์คํ ์ด์ง ๋น๋, ์บ์ฑ ์ ๋ต, ๋ณด์ ์ค์ ๋ฑ ํ๋ก๋์ ํ๊ฒฝ์ ์ต์ ํ๋ Dockerfile ์์ฑ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํด ๋๋ ค์.
- ๋จ์ด: 2,161๊ฐ24๋ถ
[๐ค] CSS Grid ์ฌํ ๊ฐ์ด๋: ์ค์ ๋ ์ด์์ ํจํด๊ณผ ๋ฐ์ํ ๋์์ธ ์ ๋ต
CSS Grid๋ ๊ฐ๋ ฅํ 2์ฐจ์ ๋ ์ด์์ ์์คํ ์ด์์. ์ด ๊ฐ์ด๋์์ Grid์ ํต์ฌ ๊ฐ๋ ๋ถํฐ ์ค์ ๋ ์ด์์ ํจํด, ๋ฐ์ํ ๋์์ธ ์ ๋ต๊น์ง ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด ์ค๋ฌด์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋๋ก ๋์๋๋ ค์.
- ๋จ์ด: 1,716๊ฐ19๋ถ
[๐ค] TypeScript ํ ํ๋ฆฟ ๋ฆฌํฐ๋ด ํ์ : ๋ฌธ์์ด ํ์ ์ ๋ง๋ฒ์ฌ๋ก ๋ณ์ ํ๊ธฐ
TypeScript์ ํ ํ๋ฆฟ ๋ฆฌํฐ๋ด ํ์ ์ ํ์ฉํ์ฌ ๋ณต์กํ ๋ฌธ์์ด ํจํด์ ์์ ํ๊ฒ ํ์ ์ถ๋ก ํ๊ณ , ๊ฐ๋ ฅํ ์ ํธ๋ฆฌํฐ ํ์ ์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ค๋ฌด ์์ ์ ํจ๊ป ์์ธํ ์์๋ด์. ํ์ ์์ ์ฑ์ ํ ๋จ๊ณ ๋์ฌ ๊ฐ๋ฐ ๊ฒฝํ์ ๊ฐ์ ํด ๋ณด์ธ์.
- ๋จ์ด: 1,917๊ฐ23๋ถ
[๐ค] JavaScript WeakMap๊ณผ WeakSet: ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง์ ์ต์ ํ ์ ๋ต
JavaScript์์ WeakMap๊ณผ WeakSet์ ํ์ฉํ์ฌ ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ๋ฐฉ์งํ๊ณ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์. ๊ฐ๋น์ง ์ปฌ๋ ์ ๋์ ์๋ฆฌ์ ํจ๊ป ๊ฐ์ฒด ์ฐธ์กฐ ๊ด๋ฆฌ์ ์ค์์ฑ์ ์ดํดํ๊ณ , ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ์ ๋ต์ ๋ฐฐ์๋ด์.
- ๋จ์ด: 1,446๊ฐ17๋ถ
[๐ค] Next.js/React ํ๋ก์ ํธ๋ฅผ ์ํ ESLint & Prettier ์ค์ ์๋ฒฝ ๊ฐ์ด๋
๋ณต์กํ Next.js ๋ฐ React ํ๋ก์ ํธ์์ ์ผ๊ด๋ ์ฝ๋ ์คํ์ผ๊ณผ ํ์ง์ ์ ์งํ๋ ESLint์ Prettier ์ค์ ๋ฐฉ๋ฒ์ ์์ธํ ์๋ ค๋๋ ค์. ํ ๊ฐ๋ฐ ํ๊ฒฝ์ ์ต์ ํ๋ ์ค์ ์ผ๋ก ๊ฐ๋ฐ ํจ์จ์ ๋์ฌ๋ณด์ธ์.
- ๋จ์ด: 2,006๊ฐ25๋ถ
[๐ค] JavaScript์ ํต์ฌ: ํ๋กํ ํ์ ์ฒด์ธ ์๋ฒฝ ์ดํด์ ํ์ฉ ์ ๋ต
JavaScript์ ์ฌ์ฅ๋ถ, ํ๋กํ ํ์ ์ฒด์ธ์ ๋์ ์๋ฆฌ๋ฅผ ๊น์ด ํ๊ณ ๋ค์ด ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ๊ณผ ์์์ ์๋ฒฝํ๊ฒ ์ดํดํ๊ณ ์ค๋ฌด์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์. ์ฑ๋ฅ ์ต์ ํ ํ๋ ํจ๊ป ๋ค๋ค์.
- ๋จ์ด: 2,087๊ฐ22๋ถ
[๐ค] React ์ปค์คํ ํ : ์ฌ์ฌ์ฉ์ฑ ๋์ด๋ ์ค๊ณ ์์น๊ณผ ํ ์คํธ ์ ๋ต
React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๊ทน๋ํํ๋ ์ปค์คํ ํ ์ค๊ณ ์์น, ์ค์ฉ์ ์ธ ํจํด, ๊ทธ๋ฆฌ๊ณ ๊ฒฌ๊ณ ํ ํ ์คํธ ์ ๋ต์ ์๋์ด ๊ฐ๋ฐ์์ ๊ด์ ์์ ์์ธํ ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 2,107๊ฐ23๋ถ
[๐ค] React useRef ํ ์ฌ์ธต ๋ถ์: DOM ๋์ด์ ์ค์ ํ์ฉ ์ ๋ต
React useRef ํ ์ ๊ธฐ๋ณธ ์๋ฆฌ๋ถํฐ DOM ์์ ์ง์ ์ ์ด, ์ปดํฌ๋ํธ ๋ผ์ดํ์ฌ์ดํด ๊ด๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ ๋ค์ํ ์ค์ ํ์ฉ ์ ๋ต๊น์ง ์ฌ์ธต์ ์ผ๋ก ๋ค๋ค์. ์ด์ค๊ธ ๊ฐ๋ฐ์๋ฅผ ์ํ useRef ์๋ฒฝ ๊ฐ์ด๋.
- ๋จ์ด: 1,762๊ฐ19๋ถ
[๐ค] Next.js 14/15์์ ๋์ OG ์ด๋ฏธ์ง ์์ฑ: ImageResponse ์๋ฒฝ ๊ฐ์ด๋
Next.js App Router ํ๊ฒฝ์์ ImageResponse๋ฅผ ํ์ฉํ์ฌ ๋์ OG ์ด๋ฏธ์ง๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. SEO์ ์์ ๊ณต์ ์ต์ ํ๋ฅผ ์ํ ์ค์ ๊ฐ์ด๋์ ๋๋ค.
- ๋จ์ด: 1,481๊ฐ18๋ถ
[๐ค] Git ๋ธ๋์น ์ ๋ต: Git Flow vs GitHub Flow, ์ค๋ฌด์์ ์ด๋ป๊ฒ ์ ํํ๊ณ ์ด์ํ ๊น์?
๊ฐ๋ฐํ์ ํจ์จ์ ์ธ ํ์ ์ ์ํ Git ๋ธ๋์น ์ ๋ต์ ๊ณ ๋ฏผํ๊ณ ๊ณ์ ๊ฐ์? Git Flow์ GitHub Flow์ ํต์ฌ ๊ฐ๋ ๋ถํฐ ์ฅ๋จ์ , ๊ทธ๋ฆฌ๊ณ ์ฐ๋ฆฌ ํ์ ๋ง๋ ์ ๋ต์ ์ ํํ๊ณ ์ด์ํ๋ ์ค์ง์ ์ธ ํ๊น์ง '๋ธ๋ฃจ'๊ฐ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,442๊ฐ16๋ถ
[๐ค] TypeScript ํ์ ๊ฐ๋: ๋ฐํ์ ํ์ ์์ ์ฑ์ ์ํ ํ์ ํจํด ์ ๋ณตํด์
TypeScript์์ ๋ฐํ์์ ๋ณ์์ ํ์ ์ ์์ ํ๊ฒ ์ขํ๋(Narrowing) ๋ฐฉ๋ฒ์ธ ํ์ ๊ฐ๋(Type Guard)์ ๋ํด ์์ธํ ์์๋ด์. `typeof`, `instanceof`, `in` ์ฐ์ฐ์๋ถํฐ ์ฌ์ฉ์ ์ ์ ํ์ ๊ฐ๋๊น์ง, ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ๊ฒฌ๊ณ ํ ์ฝ๋๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์ตํ๋ด์.
- ๋จ์ด: 2,542๊ฐ28๋ถ
[๐ค] React Query (TanStack Query) ์ฌํ: ๋ฐ์ดํฐ ํ์นญ, ์บ์ฑ, ๋๊ธฐํ ์ ๋ต์ผ๋ก ์น ์ฑ ์ฑ๋ฅ ๊ทน๋ํํด์
React Query (TanStack Query)๋ฅผ ํ์ฉํ์ฌ ๋ณต์กํ ์๋ฒ ์ํ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ , ์ง๋ฅ์ ์ธ ์บ์ฑ๊ณผ ์๋ ๋๊ธฐํ ์ ๋ต์ผ๋ก ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด์. useQuery, useMutation, useInfiniteQuery ๋ฑ ํต์ฌ ํ ๊ณผ ์ค์ ์ต์ ํ ํ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 2,401๊ฐ26๋ถ
[๐ค] React `useTransition`๊ณผ `useDeferredValue`๋ก ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ
React ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ฌด๊ฑฐ์ด UI ์ ๋ฐ์ดํธ๋ก ์ธํ ๋ฒ๋ฒ ์์ ํด๊ฒฐํ๊ณ , `useTransition`๊ณผ `useDeferredValue` ํ ์ ํ์ฉํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ์ ํ๊ธฐ์ ์ผ๋ก ๊ฐ์ ํ๋ ์ค์ฉ์ ์ธ ์ ๋ต์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 1,917๊ฐ22๋ถ
[๐ค] React Suspense์ ErrorBoundary: ๊ฒฌ๊ณ ํ๊ณ ๋ถ๋๋ฌ์ด UI ๊ฒฝํ์ ์ํ ์ค์ ๊ฐ์ด๋
React ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉ์ ๊ฒฝํ์ ํ์ ํ Suspense์ ErrorBoundary์ ๊ฐ๋ ฅํ ์กฐํฉ์ ๊น์ด ์๊ฒ ๋ค๋ค์. ๋ก๋ฉ ์ํ์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ฐ์ํ๊ฒ ๊ด๋ฆฌํ์ฌ ๋์ฑ ๊ฒฌ๊ณ ํ๊ณ ๋ถ๋๋ฌ์ด UI๋ฅผ ๋ง๋๋ ์ค์ ํ๊ณผ ์ฝ๋ ์์๋ฅผ ํ์ธํด ๋ณด์ธ์.
- ๋จ์ด: 1,302๊ฐ16๋ถ
[๐ค] CSS Container Queries: ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ๋ฐ์ํ ๋์์ธ์ ์๋ก์ด ์งํ
๋ฏธ๋์ด ์ฟผ๋ฆฌ์ ํ๊ณ๋ฅผ ๋์ด, ์ปดํฌ๋ํธ ์์ฒด์ ํฌ๊ธฐ์ ๋ฐ๋ผ ์คํ์ผ์ ์กฐ์ ํ๋ CSS Container Queries๋ฅผ ๊น์ด ์๊ฒ ์์๋ณด๊ณ ์ค๋ฌด ์ ์ฉ ๋ฐฉ๋ฒ์ ์๋ดํด ๋๋ ค์.
- ๋จ์ด: 1,681๊ฐ19๋ถ
[๐ค] Next.js 15 ๊ณ ๊ธ ๋ฐ์ดํฐ ์บ์ฑ ์ ๋ต: fetch์ revalidate ์ฌ์ธต ๋ถ์
Next.js 15์์ `fetch` API์ ๊ฐ๋ ฅํ ์บ์ฑ ๋ฉ์ปค๋์ฆ๊ณผ `revalidate` ์ต์ ์ ํ์ฉํ์ฌ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ์ต์ ํํ๊ณ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด์. ์ค๋ฌด ์์๋ฅผ ํตํด ์๋ฒ ์ปดํฌ๋ํธ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์บ์ฑ ์ ๋ต์ ํจ๊ณผ์ ์ผ๋ก ์ ์ฉํ๋ ํ์ ์ ๊ณตํด์.
๋จ์ด: 1,320๊ฐ14๋ถ[๐ค] Next.js App Router: generateStaticParams๋ก ๋์ ๋ผ์ฐํ ๋น๋ ์ต์ ํํ๊ธฐ
Next.js App Router์์ generateStaticParams ํจ์๋ฅผ ํ์ฉํ์ฌ ๋์ ๋ผ์ฐํ ์ ์ ์ ํ์ด์ง๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๊ณ ๋น๋ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์.
๋จ์ด: 1,891๊ฐ22๋ถ[๐ค] React ๋ ๋๋ง ์ต์ ํ: useMemo, useCallback, React.memo ์๋ฒฝ ๊ฐ์ด๋
์ด์ค๊ธ ๊ฐ๋ฐ์๋ฅผ ์ํ React ๋ ๋๋ง ์ต์ ํ ๊ฐ์ด๋. useMemo, useCallback, React.memo์ ์ ํํ ์ฌ์ฉ๋ฒ๊ณผ ์ค๋ฌด์์ ํํ ์ ์ง๋ฅด๋ ์ค์, ๊ทธ๋ฆฌ๊ณ ์ค์ ์ฑ๋ฅ ํฅ์ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ ค๋๋ ค์.
๋จ์ด: 2,145๊ฐ24๋ถ[๐ค] JavaScript Proxy์ Reflect ์ฌ์ธต ๋ถ์: ๋ฉํ ํ๋ก๊ทธ๋๋ฐ์ผ๋ก ์ฝ๋ ๊ฐํํ๊ธฐ
JavaScript Proxy์ Reflect API๋ฅผ ํ์ฉํ ๋ฉํ ํ๋ก๊ทธ๋๋ฐ ๊ธฐ๋ฒ์ ์ฌ์ธต ๋ถ์ํด์. ๊ฐ์ฒด ์ ๊ทผ ์ ์ด, ์ ํจ์ฑ ๊ฒ์ฌ, ๋ก๊น , ๋ฐ์ํ ์์คํ ๊ตฌํ ๋ฑ ์ค์ฉ์ ์ธ ํ์ฉ ์ฌ๋ก๋ฅผ ํตํด ์ฝ๋์ ์ ์ฐ์ฑ๊ณผ ์์ ์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 2,029๊ฐ24๋ถ
[๐ค] React/Next.js ๋ฒ๋ค ์ต์ ํ: ์ฝ๋ ์คํ๋ฆฌํ ๊ณผ ๋ ์ด์ง ๋ก๋ฉ ์๋ฒฝ ๊ฐ์ด๋
React์ Next.js ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฒ๋ค ํฌ๊ธฐ๋ฅผ ์ค์ด๊ณ ๋ก๋ฉ ์๋๋ฅผ ๊ฐ์ ํ๋ ์ฝ๋ ์คํ๋ฆฌํ ๊ณผ ๋ ์ด์ง ๋ก๋ฉ ๊ธฐ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์. ์นํฉ ์ค์ ๋ถํฐ React.lazy, Next.js dynamic import๊น์ง ๋ค๋ค์.
- ๋จ์ด: 1,770๊ฐ20๋ถ
[๐ค] React์ `useOptimistic` ํ ์ผ๋ก ๋๊ด์ UI ์ ๋ฐ์ดํธ ๊ตฌํํ๊ธฐ: Server Actions์ ํจ๊ป
React 18/19์ `useOptimistic` ํ ์ ํ์ฉํ์ฌ Server Actions์ ์ฐ๋๋๋ ๋๊ด์ UI ์ ๋ฐ์ดํธ๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์. ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฐ์์ฑ์ ๋์ด๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํด์.
๋จ์ด: 1,561๊ฐ17๋ถ[๐ค] TypeScript const Type Parameters: ๋ฆฌํฐ๋ด ํ์ ์ถ๋ก ๊ฐํ์ ์ค์ฉ์ ์ธ ํ์ฉ๋ฒ
TypeScript 5.0์ ๋์ ๋ const Type Parameters๋ฅผ ํ์ฉํ์ฌ ์ ๋ค๋ฆญ ํจ์์ ๋ฆฌํฐ๋ด ํ์ ์ถ๋ก ์ ์ ๊ตํ๊ฒ ์ ์ดํ๊ณ , ๋์ฑ ๊ฒฌ๊ณ ํ ํ์ ์์คํ ์ ๊ตฌ์ถํ๋ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. as const์์ ์ฐจ์ด์ ๊ณผ ์ค์ ์ฝ๋ ์์๋ฅผ ํตํด ์ด์ค๊ธ ๊ฐ๋ฐ์๋ ์ฝ๊ฒ ์ดํดํ ์ ์๋๋ก ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 2,028๊ฐ22๋ถ
[๐ค] Next.js/React ์ฑ CLS ์ต์ ํ: ์ํํธ ์๋ ์ฌ์ฉ์ ๊ฒฝํ ๋ง๋ค๊ธฐ
Next.js์ React ์ ํ๋ฆฌ์ผ์ด์ ์์ Cumulative Layout Shift(CLS) ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๋ ์ค์ง์ ์ธ ์ ๋ต๊ณผ ์ฝ๋ ์์๋ฅผ ์์ธํ ์์๋ณด์ธ์. ์น ์ฑ๋ฅ ์ต์ ํ์ ํต์ฌ ์์์ธ CLS๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,740๊ฐ21๋ถ
[๐ค] Next.js SSR, SSG, ISR ๋ ๋๋ง ์ ๋ต: App Router์์ ์ต์ ์ ์ ํ์?
Next.js App Router์์ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR), ์ ์ ์ฌ์ดํธ ์์ฑ(SSG), ์ฆ๋ถ ์ ์ ์ฌ์์ฑ(ISR) ๊ฐ ๋ ๋๋ง ์ ๋ต์ ๋์ ์๋ฆฌ, ์ฅ๋จ์ , ์ค์ ํ์ฉ ๋ฐ ์ต์ ํ ๋ฐฉ๋ฒ์ ๋น๊ต ๋ถ์ํด๋๋ ค์.
- ๋จ์ด: 1,478๊ฐ17๋ถ
[๐ค] React Context API์ Zustand: ์ ์ญ ์ํ ๊ด๋ฆฌ, ์ธ์ ๋ฌด์์ ์จ์ผ ํ ๊น์?
React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ณ ๋ฏผํ๊ณ ๊ณ์ ๊ฐ์? Context API์ ๊ฐ๋ฒผ์ด ์ธ๋ถ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Zustand๋ฅผ ๋น๊ต ๋ถ์ํ๊ณ , ์ค๋ฌด์์ ๊ฐ ๋๊ตฌ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ๋ ์ ๋ต์ ์ค์ ์ฝ๋ ์์์ ํจ๊ป ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 2,004๊ฐ24๋ถ
[๐ค] Turborepo๋ก Next.js ๋ชจ๋ ธ๋ ํฌ ๊ตฌ์ถ: ํจ์จ์ ์ธ ๊ฐ๋ฐ ๋ฐ ์ต์ ํ ์ ๋ต
Turborepo๋ฅผ ํ์ฉํ์ฌ Next.js ํ๋ก์ ํธ๋ฅผ ๋ชจ๋ ธ๋ ํฌ๋ก ๊ตฌ์ฑํ๊ณ , ๊ณต์ ์ปดํฌ๋ํธ, ์ ํธ๋ฆฌํฐ, CI/CD ์ต์ ํ ๋ฐฉ์์ ์ค๋ฌด ์์์ ํจ๊ป ์์ธํ ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 2,338๊ฐ27๋ถ
[๐ค] React useEffect ํ , ์ด์ ํท๊ฐ๋ฆฌ์ง ๋ง์ธ์! (์์กด์ฑ ๋ฐฐ์ด, ํด๋ฆฐ์ ์๋ฒฝ ๊ฐ์ด๋)
React ๊ฐ๋ฐ์์ ํ์์ ์ธ useEffect ํ ์ ๋์ ์๋ฆฌ๋ถํฐ ์์กด์ฑ ๋ฐฐ์ด, ํด๋ฆฐ์ ํจ์ ํ์ฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ ์ค๋ฌด์์ ์์ฃผ ๊ฒช๋ ์ค์์ ์ต์ ํ ์ ๋ต๊น์ง, ์ด์ค๊ธ ๊ฐ๋ฐ์๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํด์.
- ๋จ์ด: 3,284๊ฐ31๋ถ
[๐ค] Next.js Server Actions ์ค์ : ์๋ฌ ์ฒ๋ฆฌ, ์ ํจ์ฑ ๊ฒ์ฌ, ๋๊ด์ UI ์ ๋ฐ์ดํธ
Next.js Server Actions๋ฅผ ์ค๋ฌด์ ์ ์ฉํ ๋ ๋ง์ฃผํ๋ ์๋ฌ ์ฒ๋ฆฌ, ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ, ๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ ๋๊ด์ UI ์ ๋ฐ์ดํธ ๊ธฐ๋ฒ์ ์์ธํ ์ฝ๋ ์์์ ํจ๊ป ์์๋ณด์ธ์.
๋จ์ด: 1,983๊ฐ21๋ถ[๐ค] TypeScript ์ ํธ๋ฆฌํฐ ํ์ ์๋ฒฝ ๊ฐ์ด๋: ์ค์ ํ์ฉ ํจํด
TypeScript ์ ํธ๋ฆฌํฐ ํ์ ์ ํต์ฌ ๊ฐ๋ ๊ณผ ์ค์ ํ์ฉ๋ฒ์ ๊น์ด ์๊ฒ ๋ค๋ค์. Pick, Omit, Partial, Required ๋ฑ ์์ฃผ ์ฐ๋ ์ ํธ๋ฆฌํฐ ํ์ ์ผ๋ก ๋ณต์กํ ํ์ ์ ํจ๊ณผ์ ์ผ๋ก ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์. ํ์ ์คํฌ๋ฆฝํธ ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์์ ์ฑ์ ๋์ด๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํด์.
- ๋จ์ด: 1,712๊ฐ20๋ถ
[๐ค] Next.js App Router ๋ฏธ๋ค์จ์ด: ๊ฐ๋ ฅํ ์์ฒญ ์ฒ๋ฆฌ ์ ๋ต๊ณผ ์ค์ ์์
Next.js App Router ํ๊ฒฝ์์ ๋ฏธ๋ค์จ์ด๋ฅผ ํ์ฉํด ์ฌ์ฉ์ ์ธ์ฆ, ๋ฆฌ๋ค์ด๋ ์ , ๊ตญ์ ํ ๋ฑ์ ์์ฒญ ์ฒ๋ฆฌ ๋ก์ง์ ํจ์จ์ ์ผ๋ก ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ค์ ์์ ์ ํจ๊ป ์์ธํ ์์๋ณด์ธ์.
- ๋จ์ด: 1,630๊ฐ19๋ถ
[๐ค] ํ์ ์คํฌ๋ฆฝํธ ์ ๋ค๋ฆญ ์ฌํ: ์ค์ฉ์ ์ธ ํจํด๊ณผ ํํ ์คํด๋ค
ํ์ ์คํฌ๋ฆฝํธ ์ ๋ค๋ฆญ(Generics)์ ๊น์ด ์ดํดํ๊ณ , ์ค๋ฌด์์ ์์ฃผ ์ฌ์ฉ๋๋ ์ ๋ค๋ฆญ ํจํด๊ณผ ํํ ๊ฒช๋ ์คํด๋ค์ ์ค์ ์ฝ๋ ์์์ ํจ๊ป ์ฝ๊ณ ๋ช ํํ๊ฒ ์ค๋ช ํด ๋๋ ค์. ํ์ ์์ ์ฑ๊ณผ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
๋จ์ด: 1,860๊ฐ18๋ถ[๐ค] Next.js Route Handler: App Router์์ ์์ ํ๊ณ ํจ์จ์ ์ธ API ๊ตฌ์ถํ๊ธฐ (์ธ์ฆ, ์๋ฌ ์ฒ๋ฆฌ ํฌํจ)
Next.js App Router์ Route Handler๋ฅผ ์ฌ์ฉํ์ฌ API ์๋ํฌ์ธํธ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ์์ธํ ์์๋ด์. ์ธ์ฆ, ์๋ฌ ์ฒ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ์บ์ฑ ์ ๋ต์ ํฌํจํ ์ค์ฉ์ ์ธ ํ์ผ๋ก ์์ ํ๊ณ ํจ์จ์ ์ธ ์๋ฒ๋ฆฌ์ค ํจ์๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ตํ๋ด์.
- ๋จ์ด: 1,934๊ฐ22๋ถ
[๐ค] Next.js Image ์ปดํฌ๋ํธ ์ต์ ํ: Core Web Vitals ๊ฐ์ ๋ถํฐ ์ค์ ํ์ฉ๊น์ง
Next.js์ Image ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํ์ฌ ์น ์ฑ๋ฅ ํต์ฌ ์งํ์ธ Core Web Vitals๋ฅผ ๊ฐ์ ํ๊ณ , ๋ค์ํ ์ต์ ํ ์ต์ ์ ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ธ๋ฃจ๊ฐ ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 2,187๊ฐ25๋ถ
[๐ค] Next.js 14.1+์ ํ์ : Partial Prerendering (PPR) ์๋ฒฝ ๊ฐ์ด๋์ ์ค์ ์ต์ ํ ์ ๋ต
Next.js 14.1๋ถํฐ ๋์ ๋ Partial Prerendering (PPR)์ ํตํด ์ด๊ธฐ ๋ก๋ฉ ์๋๋ฅผ ๊ทน๋ํํ๊ณ ๋์ ์ฝํ ์ธ ๋ฅผ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ ์๊ฒ ๋ค๋ฃจ์ด์. PPR์ ๋์ ์๋ฆฌ๋ถํฐ ์ค์ ํ๋ก์ ํธ ์ ์ฉ ์ ๋ต๊น์ง, ๊ฐ๋ฐ์๋ค์ด ๊ถ๊ธํดํ๋ ๋ชจ๋ ๊ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,792๊ฐ19๋ถ
[๐ค] TypeScript ์กฐ๊ฑด๋ถ ํ์ ๊ณผ infer ํค์๋: ๋ณต์กํ ํ์ ๋ ์์ฝ๊ฒ ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ
TypeScript ๊ฐ๋ฐ์์ ๋ง์ฃผํ๋ ๋ณต์กํ ํ์ ์ถ๋ก ๋ฌธ์ , ์กฐ๊ฑด๋ถ ํ์ ๊ณผ infer ํค์๋๋ฅผ ํ์ฉํ๋ฉด ํจ์ฌ ์ฐ์ํ๊ณ ๊ฐ๋ ฅํ๊ฒ ํด๊ฒฐํ ์ ์์ด์. ์ค์ ์์ ์ ํจ๊ป ๊ทธ ํ์ฉ๋ฒ์ ์ฌ๋ ์๊ฒ ๋ค๋ค๋ด ๋๋ค.
- ๋จ์ด: 1,705๊ฐ21๋ถ
[๐ค] JavaScript ์ด๋ฒคํธ ๋ฃจํ(Event Loop) ์์ ์ ๋ณต: ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ฐํ์ ๋์ ์๋ฆฌ
JavaScript์ ํต์ฌ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๋ฉ์ปค๋์ฆ์ธ ์ด๋ฒคํธ ๋ฃจํ์ ๋์ ์๋ฆฌ๋ฅผ ์ฌ๋ ์๊ฒ ํํค์ณ ๋ด์. ์ฝ ์คํ, ํ์คํฌ ํ, ๋ง์ดํฌ๋กํ์คํฌ ํ์์ ์ํธ์์ฉ์ ์ดํดํ๊ณ , ์ค๋ฌด์์ ๋ง์ฃผ์น๋ ๋น๋๊ธฐ ์ฝ๋์ ๋์์ ๋ช ํํ ์์ธกํ๋ ๋ฐฉ๋ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,964๊ฐ23๋ถ
[๐ค] Next.js Server & Client Components, ์ค์ ์์ ํ๋ช ํ๊ฒ ์ ํํ๋ ๊ฐ์ด๋
Next.js App Router์์ Server Components์ Client Components ์ค ์ด๋ค ๊ฒ์ ์ฌ์ฉํด์ผ ํ ์ง ๊ณ ๋ฏผ์ด์ ๊ฐ์? ์ด ๊ธ์์ ๋ ์ปดํฌ๋ํธ์ ํต์ฌ ์ฐจ์ด์ , ์ฌ์ฉ ์์ , ๊ทธ๋ฆฌ๊ณ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ ์ค์ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ ค๋๋ฆด๊ฒ์.
- ๋จ์ด: 1,879๊ฐ21๋ถ
[๐ค] TypeScript satisfies ์ฐ์ฐ์: ํ์ ์ถ๋ก ๊ณผ ์์ ์ฑ์ ๋์์ ์ก๋ ๋น๋ฒ
TypeScript์ `satisfies` ์ฐ์ฐ์๋ฅผ ํ์ฉํ์ฌ ํ์ ์ถ๋ก ์ ์ ์ฐ์ฑ์ ์ ์งํ๋ฉด์๋ ์๊ฒฉํ ํ์ ์์ ์ฑ์ ํ๋ณดํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. ์ค์ฉ์ ์ธ ์์๋ฅผ ํตํด ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํฉ๋๋ค.
- ๋จ์ด: 1,211๊ฐ15๋ถ
[๐ค] React 19 ์๋ก์ด ๊ธฐ๋ฅ: use ํ , Actions, ๊ทธ๋ฆฌ๊ณ ์ปดํ์ผ๋ฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
React 19์ ํต์ฌ ๋ณ๊ฒฝ ์ฌํญ์ธ use ํ , ์๋ฒ ์ก์ , ๊ทธ๋ฆฌ๊ณ React ์ปดํ์ผ๋ฌ์ ๋์ ๋ฐฐ๊ฒฝ๊ณผ ์ค์ ํ์ฉ ์์๋ฅผ ์ด์ค๊ธ ๊ฐ๋ฐ์ ๋๋์ด์ ๋ง์ถฐ ์์ธํ ์ค๋ช ํฉ๋๋ค. ์ต์ React ์ ๋ฐ์ดํธ๋ฅผ ํตํด ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ๊ณผ ๊ฐ๋ฐ ๊ฒฝํ์ ํฅ์์ํค๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์.
- ๋จ์ด: 1,524๊ฐ16๋ถ
[๐ค] Next.js App Router ์บ์ฑ ์ ๋ต: ๋ฐ์ดํฐ ์ฌ๊ฒ์ฆ (revalidatePath, revalidateTag) ์๋ฒฝ ๊ฐ์ด๋
Next.js 14 App Router์์ ํจ์จ์ ์ธ ๋ฐ์ดํฐ ์บ์ฑ ์ ๋ต๊ณผ revalidatePath, revalidateTag๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ์ฌ๊ฒ์ฆ ๋ฐฉ๋ฒ์ ์ค๋ฌด ์์์ ํจ๊ป ์์ธํ ์์๋ณด๊ณ ์น ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.