Mosdaqμ μν κ°λ΄ μ νμ κ΄λ ¨ μ£Όκ° λ³νλ₯Ό μμΈ‘ν΄λ³΄λ μΉ μ ν리μΌμ΄μ μ λλ€.
μ΄ νλ‘μ νΈλ μ΅μ μΉ κΈ°μ μ μ¬μ©νμ¬ μ±λ₯ μ΅μ ν, μ¬μ©μ± ν₯μμ μ€μ μ λμ΄ κ°λ°λμμ΅λλ€.
- μν λͺ©λ‘: μ΅μ κ°λ΄ μνμ ν¬ν λ§κ°λ μν λͺ©λ‘μ μ‘°νν μ μμ΅λλ€.
- μ£Όκ° μμΈ‘ ν΄μ¦: μ¬μ©μκ° μνμ κ°λ΄ ν μ£Όκ° λ³λμ μμΈ‘νλ ν΄μ¦λ₯Ό μ 곡νλ©°, μμΈ‘ κ²°κ³Όλ₯Ό κΈ°λ°μΌλ‘ μ¬μ©μμ μ±κ³Όλ₯Ό κΈ°λ‘ν©λλ€.
- ν¬ν κ²°κ³Ό νμΈ: μ¬μ©μκ° μμΈ‘ν μ£Όκ° λ³νμ μ€μ λ°μ΄ν°λ₯Ό λΉκ΅νμ¬ κ²°κ³Όλ₯Ό νμΈν μ μμ΅λλ€.
- μ μ νλ‘ν: μ¬μ©μλ μμ μ νλ‘νμ κ΄λ¦¬νκ³ , μμΈ‘ κΈ°λ‘μ νμΈν μ μμ΅λλ€.
- React: λͺ¨λ μΉ μ ν리μΌμ΄μ κ°λ°μ μν΄ Reactλ₯Ό μ¬μ©νμ΅λλ€. μ»΄ν¬λνΈ κΈ°λ° κ΅¬μ‘°λ‘ μ¬μ¬μ©μ±κ³Ό μ μ§λ³΄μμ±μ λμμ΅λλ€.
- TypeScript: μ μ νμ μ ν΅ν΄ μ½λμ μμ μ±μ λμ΄κ³ , κ°λ° μ€ μ€λ₯λ₯Ό μ¬μ μ λ°©μ§νμ΅λλ€.
- Styled-components: CSS-in-JSλ₯Ό μ¬μ©νμ¬ μ»΄ν¬λνΈ μ€νμΌλ§μ κ΄λ¦¬νμ΅λλ€. λμ μ€νμΌλ§κ³Ό ν λ§ κ΄λ¦¬μ μ μ©νμ΅λλ€.
- React Query: μλ² μν κ΄λ¦¬μ λ°μ΄ν° νμΉμ μν΄ React Queryλ₯Ό μ¬μ©νμ¬ ν¨μ¨μ μΈ λΉλκΈ° μ²λ¦¬μ μΊμ±μ ꡬννμ΅λλ€.
- React Router: νμ΄μ§ κ° λΌμ°ν μ κ΄λ¦¬νμ¬ ν΄λΌμ΄μΈνΈ μ¬μ΄λ λΌμ°ν μ ꡬννμ΅λλ€.
- Zustand: κ°λ³κ³ μ¬μ©νκΈ° μ¬μ΄ μν κ΄λ¦¬ λΌμ΄λΈλ¬λ¦¬μΈ Zustandλ₯Ό μ¬μ©νμ¬, React μ ν리μΌμ΄μ μ μ μ μν κ΄λ¦¬λ₯Ό ꡬννμ΅λλ€. Reduxλ³΄λ€ κ°λ¨ν APIλ‘ κ°λ° μμ°μ±μ λμμ΅λλ€.
- Axios: HTTP ν΄λΌμ΄μΈνΈλ‘, API μμ²μ λ³΄λ€ κ°νΈνκ² κ΄λ¦¬νκ³ μ²λ¦¬ν©λλ€.
- Vite: λΉ λ₯Έ κ°λ° νκ²½ μ€μ μ μν΄ Viteλ₯Ό μ¬μ©νμΌλ©°, λΉλ μλλ₯Ό μ΅μ ννμ΅λλ€.
- ESLint & Prettier: μ½λ μΌκ΄μ±κ³Ό νμ§μ μ μ§νκΈ° μν΄ ESLintμ Prettierλ₯Ό μ¬μ©νμ¬ μ½λ μ€νμΌμ ν΅μΌνμ΅λλ€.
- Husky & Lint-staged: Git μ»€λ° μ μ½λ νμ§ κ²μ¬λ₯Ό μλννμ¬, λ²κ·Έμ μ€νμΌλ§ λ¬Έμ λ₯Ό μ¬μ μ λ°©μ§νμ΅λλ€.
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
withCredentials: true,
});
export default axiosInstance;
곡ν΅μ μΌλ‘ μ¬μ©λλ μ΅μ λ€μ μ€μμμ κ΄λ¦¬νκΈ° μν΄ Axios μΈμ€ν΄μ€λ₯Ό μμ±νμ¬ μ¬μ¬μ© κ°λ₯ν HTTP μμ² APIλ₯Ό ꡬμΆνμμ΅λλ€. μ΄λ₯Ό ν΅ν΄ μ½λ μ€λ³΅μ μ€μ΄κ³ μ μ§λ³΄μμ±μ λμμ΅λλ€.
{
"singleQuote": true,
"semi": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf",
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": ["^react", "^[./]", "^@?\\w"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}
...
// μμΈ κ·μΉ μ€μ
{
plugins: {
'react-hooks': pluginReactHooks,
},
rules: {
// TypeScript κ΄λ ¨ κ·μΉ
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
// React κ΄λ ¨ κ·μΉ
'react/react-in-jsx-scope': 'off',
'react/no-unescaped-entities': 'off',
'react/display-name': 'off',
// React Hooks κ΄λ ¨ κ·μΉ
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// μΌλ° κ·μΉ
'no-unexpected-multiline': 'warn',
},
},
];
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
μ½λ μ€νμΌμ μΌκ΄μ±μ ν보νκΈ° μν΄ Prettierμ ESLintλ₯Ό Huskyλ₯Ό μ¬μ©νμ¬ μλννμμ΅λλ€. μ΄λ μ½λ νμ§μ μ μ§νκ³ νμ κ°μ νμ μ μννκ² ν©λλ€.
SPA νΉμ±μ μ΄κΈ° λ λλ§μ΄ μ§μ°λ μ μμΌλ―λ‘, μ½λ λΆν κ³Ό μ¬μ λ‘λ©μ ν΅ν΄ μ¬μ©μ κ²½ν(UX)μ κ°μ νμμ΅λλ€. μ£Όμ μ»΄ν¬λνΈλ₯Ό 미리 λ‘λνμ¬ νμ΄μ§ μ ν μ λ‘λ© μκ°μ λ¨μΆνμμ΅λλ€.
Reactμ lazy
μ Suspense
λ₯Ό νμ©νμ¬ μ΄κΈ° λ‘λ μκ°μ μ€μ΄κ³ , νμν μμ μ μ»΄ν¬λνΈλ₯Ό λ‘λνλλ‘ κ΅¬μ±νμ΅λλ€. νΉν, νμ΄μ§ μ ν μ μ¬μ©μ κ²½νμ ν₯μμν€κΈ° μν΄ μ¬μ λ‘λ©μ ꡬννμ΅λλ€.
// src/App.tsx
const HomePage = Object.assign(
lazy(() => import('./pages/Home')),
{
preload: () => import('./pages/Home'),
},
);
const MovieListPage = Object.assign(
lazy(() => import('./pages/MovieList')),
{
preload: () => import('./pages/MovieList'),
},
);
// μ€λ΅...
const router = createBrowserRouter([
{
path: '/',
element: (
<RootLayout
preloadMovieListPage={MovieListPage.preload}
preloadQuizPage={QuizPage.preload}
preloadMyPage={MyPage.preload}
preloadLoginPage={LoginPage.preload}
preloadHomePage={HomePage.preload}
/>
),
children: [
{
path: '/',
element: <HomePage />,
},
// μ€λ΅...
],
},
]);
μ€λͺ :
- μ΄ μ½λμμλ Reactμ
lazy
λ₯Ό μ¬μ©ν΄ νμ΄μ§ μ»΄ν¬λνΈλ₯Ό μ§μ° λ‘λ©νκ³ μμ΅λλ€.Object.assign
μ νμ©ν΄lazy
λ‘ λ‘λλ μ»΄ν¬λνΈμpreload
λ©μλλ₯Ό μΆκ°νμ¬, νΉμ νμ΄μ§λ‘μ μ ν μ μ 미리 μμμ λ‘λν μ μκ² νμ΅λλ€. μ΄λ₯Ό ν΅ν΄ μ¬μ©μ μΈν°νμ΄μ€μ μ§μ° μκ°μ μ΅μννκ³ , νμ΄μ§ μ νμ΄ μννκ² μ΄λ£¨μ΄μ§λλ‘ νμ΅λλ€.
ν€λμμ λ§μ°μ€λ₯Ό λ§ν¬μ μ¬λ Έμ λ ν΄λΉ νμ΄μ§μ 리μμ€λ₯Ό μ¬μ λ‘λ©νμ¬, μ¬μ©μκ° ν΄λ¦νμ λ λ°λ‘ νμ΄μ§κ° νμλλλ‘ μ΅μ ννμ΅λλ€.
// src/components/layout/Header/index.tsx
interface IHeaderProps {
preloadQuizPage: () => void;
preloadMyPage: () => void;
preloadLoginPage: () => void;
preloadHomePage: () => void;
preloadMovieListPage: () => void;
}
export default function Header({
preloadQuizPage,
preloadMyPage,
preloadLoginPage,
preloadHomePage,
preloadMovieListPage,
}: IHeaderProps) {
// μ€λ΅...
return (
<StyledHeaderContainer>
<StyledHeaderContent>
<StyledLeftSection>
<Link
onMouseEnter={preloadHomePage}
onClick={() => handleNaviagte('/')}
>
<StyledMainLogo src={mainLogo} alt="Main Logo" />
</Link>
<StyledNav>
<Button size="small">
<Txt typography={matchMovieList ? 'Pretendard24bold' : 'p'}>
<Link
onMouseEnter={preloadMovieListPage}
onClick={() => handleNaviagte('/movie-list')}
>
μν λͺ©λ‘
</Link>
</Txt>
</Button>
<Button size="small">
<Txt typography={matchQuiz ? 'Pretendard24bold' : 'p'}>
<Link
onClick={() => handleNaviagte('/quiz')}
onMouseEnter={preloadQuizPage}
>
μν ν΄μ¦
</Link>
</Txt>
</Button>
</StyledNav>
</StyledLeftSection>
{/* μ€λ΅... */}
</StyledHeaderContent>
</StyledHeaderContainer>
);
}
μ€λͺ :
- ν€λ λ§ν¬μμ
onMouseEnter
μ΄λ²€νΈλ₯Ό μ¬μ©νμ¬ νμ΄μ§λ₯Ό μ¬μ λ‘λ©ν©λλ€. μ¬μ©μκ° λ§ν¬μ λ§μ°μ€λ₯Ό μ¬λ¦¬λ μκ° ν΄λΉ νμ΄μ§μ 리μμ€λ₯Ό 미리 λ‘λν¨μΌλ‘μ¨, ν΄λ¦ ν νμ΄μ§ μ νμ΄ μ¦κ°μ μΌλ‘ μ΄λ£¨μ΄μ§λλ€. μ΄λ₯Ό ν΅ν΄ μ¬μ©μ κ²½νμ ν¬κ² ν₯μμν¬ μ μμ΅λλ€.
μμ’μ μ:
μ’μ μ:
μ€λͺ :
- μ΄λ―Έμ§ λ°μ΄ν°λ₯Ό λΉλκΈ°μ μΌλ‘ κ°μ Έμ€λ λμ λ°μν μ μλ λ μ΄μμ λ³κ²½ λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄, κ³ μ λ ν¬κΈ°λ₯Ό κ°μ§ 컨ν μ΄λλ‘ μ΄λ―Έμ§λ₯Ό κ°μμ΅λλ€. μ΄λ₯Ό ν΅ν΄ μ΄λ―Έμ§κ° λ‘λλκΈ° μ νμ λ μ΄μμμ΄ λ³νλ κ²μ λ°©μ§νμ¬ Cumulative Layout Shift(CLS) λ¬Έμ λ₯Ό ν΄κ²°νμ΅λλ€. μ΄ μ κ·Ό λ°©μμ μ¬μ©μ κ²½νμ κ°μ νλ μ€μν μ΅μ ν κΈ°λ² μ€ νλλ‘, νΉν μ΄λ―Έμ§ λ‘λ μκ°μ λ°λΌ νμ΄μ§μ μμλ€μ΄ μμΉλ₯Ό λ°κΎΈμ§ μλλ‘ ν¨μΌλ‘μ¨ μκ°μ μμ μ±μ 보μ₯ν©λλ€.
νλ‘μ νΈμ μ±λ₯μ μ΅μ ννκΈ° μν΄ Viteμ vite-plugin-compression
νλ¬κ·ΈμΈμ μ¬μ©νμ¬ ν
μ€νΈ νμΌ(μ: JavaScript, CSS λ±)μ GzipμΌλ‘ μμΆνμ΅λλ€. μ΄λ₯Ό ν΅ν΄ μλ²μμ ν΄λΌμ΄μΈνΈλ‘ μ μ‘λλ νμΌ ν¬κΈ°λ₯Ό μ€μ¬ λ‘λ© μκ°μ λ¨μΆνκ³ , λ€νΈμν¬ λΉμ©μ μ κ°νμ΅λλ€.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import compression from 'vite-plugin-compression';
// Vite μ€μ νμΌ (vite.config.js)
export default defineConfig({
plugins: [
react(),
compression({
algorithm: 'gzip', // Gzip μκ³ λ¦¬μ¦μ μ¬μ©νμ¬ ν
μ€νΈ νμΌμ μμΆ
ext: '.gz', // μμΆλ νμΌμ νμ₯μλ₯Ό .gzλ‘ μ€μ
}),
],
resolve: {
alias: [{ find: '@', replacement: '/src' }],
},
});
μ€λͺ :
- Viteμ
vite-plugin-compression
νλ¬κ·ΈμΈμ νμ©νμ¬ JavaScript, CSS λ±μ ν μ€νΈ νμΌμ GzipμΌλ‘ μμΆνμ΅λλ€. μ΄λ‘ μΈν΄ ν΄λΌμ΄μΈνΈλ‘ μ μ‘λλ νμΌμ ν¬κΈ°λ₯Ό μ€μ¬ νμ΄μ§ λ‘λ© μλλ₯Ό κ°μ ν μ μμμ΅λλ€. μμΆλ νμΌμ.gz
νμ₯μλ₯Ό κ°μ§λ©°, μλ²μμ μ΄λ₯Ό μ¬λ°λ₯΄κ² μ 곡νλλ‘ μ€μ ν¨μΌλ‘μ¨ μ΅μ μ μ±λ₯μ λ¬μ±νμ΅λλ€.
-
무ν μ€ν¬λ‘€μ ν΅ν μ¬μ©μ κ²½ν ν₯μ React Queryμ useInfiniteQueryλ₯Ό νμ©νμ¬ λ¬΄ν μ€ν¬λ‘€ κΈ°λ₯μ ꡬννμ΅λλ€. μ΄λ₯Ό ν΅ν΄ μ¬μ©μλ νμ΄μ§ λμ λλ¬ν λλ§λ€ μλμΌλ‘ μΆκ° μ½ν μΈ λ₯Ό λ‘λν μ μμ΄, λλμ λ°μ΄ν°λ₯Ό ν¨μ¨μ μΌλ‘ νμν μ μμμ΅λλ€. μ΄ λ°©μμ μ¬μ©μ μΈν°νμ΄μ€λ₯Ό κ°μννκ³ , νμ΄μ§ μ ν μμ΄ μ½ν μΈ λ₯Ό μ°μμ μΌλ‘ μ 곡νλ μ¬μ©μ κ²½νμ μ 곡ν©λλ€.
-
μ΄λ―Έμ§ μ§μ° λ‘λ© (Lazy Loading) νκ·Έμ loading='lazy' μμ±μ μΆκ°νμ¬, νλ©΄μ 보μ΄μ§ μλ μ΄λ―Έμ§λ€μ μ€ν¬λ‘€ν λκΉμ§ λ‘λλμ§ μλλ‘ μ΅μ ννμ΅λλ€. μ΄λ₯Ό ν΅ν΄ μ΄κΈ° λ‘λ© μκ°μ μ€μ΄κ³ , λΆνμν λ€νΈμν¬ νΈλν½μ κ°μμμΌ°μ΅λλ€. μ΄ κΈ°λ²μ νΉν μ΄λ―Έμ§κ° λ§μ νμ΄μ§μμ μ±λ₯μ ν¬κ² ν₯μμν΅λλ€.
-
WOFF ν°νΈ μ¬μ©μ ν΅ν μ±λ₯ μ΅μ ν μΉ ν°νΈλ‘ WOFF(μΉ μ€ν ν°νΈ νμ)λ₯Ό μ¬μ©νμ¬ ν μ€νΈ λ λλ§μ μ΅μ ννμ΅λλ€. WOFF ν¬λ§·μ μμΆλ₯ μ΄ λκ³ , λλΆλΆμ λΈλΌμ°μ μμ μ§μλλ―λ‘, ν°νΈ νμΌ ν¬κΈ°λ₯Ό μ€μ¬ μ±λ₯μ κ°μ ν μ μμ΅λλ€. μ΄λ‘ μΈν΄ ν μ€νΈκ° λΉ λ₯΄κ² λ λλ§λκ³ , νμ΄μ§ λ‘λ© μκ°μ΄ λ¨μΆλμμ΅λλ€.
src/
β
βββ components/ # μ¬μ¬μ© κ°λ₯ν UI μ»΄ν¬λνΈλ€
β βββ common/ # 곡ν΅μ μΌλ‘ μ¬μ©λλ μ»΄ν¬λνΈλ€
β βββ layout/ # λ μ΄μμ κ΄λ ¨ μ»΄ν¬λνΈλ€
β
βββ hooks/ # 컀μ€ν
ν
λ€
β
βββ pages/ # κ° νμ΄μ§ μ»΄ν¬λνΈλ€
β
βββ store/ # μ μ μν κ΄λ¦¬ (Zustand)
β
βββ constants/ # μμ κ°λ€ (μ: μμ, URL λ±)
β
βββ models/ # TypeScript μΈν°νμ΄μ€μ νμ
μ μ
β
βββ utils/ # μ νΈλ¦¬ν° ν¨μλ€
μ΄ READMEκ° νλ‘μ νΈμ κΈ°μ μ μλμ μ λλ¬λ΄κ³ μλμ§, λλ μμ ν λΆλΆμ΄ μλ€λ©΄ νΌλλ°±μ μ£Όμλ©΄ λ°μνκ² μ΅λλ€. μΆκ°μ μΈ μ 보λ κ°μ‘°νκ³ μΆμ λΆλΆμ΄ μλ€λ©΄ μλ €μ£ΌμΈμ!