Skip to content

prgrms-fdc-2nd-final-mosdaq/mosdaq-front

Repository files navigation

mosdaq - μ˜ν™” κ°œλ΄‰ μ£Όκ°€ 예츑 μ„œλΉ„μŠ€

배포 링크

http://mosdaq.site/

ν”„λ‘œμ νŠΈ κ°œμš”

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 컀밋 μ „ μ½”λ“œ ν’ˆμ§ˆ 검사λ₯Ό μžλ™ν™”ν•˜μ—¬, 버그와 μŠ€νƒ€μΌλ§ 문제λ₯Ό 사전에 λ°©μ§€ν–ˆμŠ΅λ‹ˆλ‹€.

μ„ΈλΆ€ κ΅¬ν˜„

μž¬μ‚¬μš© κ°€λŠ₯ν•œ HTTP μš”μ²­ API ꡬ좕

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λ₯Ό κ΅¬μΆ•ν•˜μ˜€μŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 μ½”λ“œ 쀑볡을 쀄이고 μœ μ§€λ³΄μˆ˜μ„±μ„ λ†’μ˜€μŠ΅λ‹ˆλ‹€.

μ½”λ“œ 일관성 확보

.prettierrc

{
  "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
}

eslint.config.js

  ...
  // 상세 κ·œμΉ™ μ„€μ •
  {
    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',
    },
  },
];

package.json

  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  }

μ½”λ“œ μŠ€νƒ€μΌμ˜ 일관성을 ν™•λ³΄ν•˜κΈ° μœ„ν•΄ Prettier와 ESLintλ₯Ό Huskyλ₯Ό μ‚¬μš©ν•˜μ—¬ μžλ™ν™”ν•˜μ˜€μŠ΅λ‹ˆλ‹€. μ΄λŠ” μ½”λ“œ ν’ˆμ§ˆμ„ μœ μ§€ν•˜κ³  νŒ€μ› κ°„μ˜ ν˜‘μ—…μ„ μ›ν™œν•˜κ²Œ ν•©λ‹ˆλ‹€.

μ΅œμ ν™”

1. μ½”λ“œ λΆ„ν•  및 지연 λ‘œλ”©

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 λ©”μ„œλ“œλ₯Ό μΆ”κ°€ν•˜μ—¬, νŠΉμ • νŽ˜μ΄μ§€λ‘œμ˜ μ „ν™˜ 전에 미리 μžμ›μ„ λ‘œλ“œν•  수 있게 ν–ˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€μ˜ 지연 μ‹œκ°„μ„ μ΅œμ†Œν™”ν•˜κ³ , νŽ˜μ΄μ§€ μ „ν™˜μ΄ μ›ν™œν•˜κ²Œ 이루어지도둝 ν–ˆμŠ΅λ‹ˆλ‹€.

2. 헀더 링크의 사전 λ‘œλ”©

ν—€λ”μ—μ„œ 마우슀λ₯Ό 링크에 μ˜¬λ Έμ„ λ•Œ ν•΄λ‹Ή νŽ˜μ΄μ§€μ˜ λ¦¬μ†ŒμŠ€λ₯Ό 사전 λ‘œλ”©ν•˜μ—¬, μ‚¬μš©μžκ°€ ν΄λ¦­ν–ˆμ„ λ•Œ λ°”λ‘œ νŽ˜μ΄μ§€κ°€ ν‘œμ‹œλ˜λ„λ‘ μ΅œμ ν™”ν–ˆμŠ΅λ‹ˆλ‹€.

// 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 이벀트λ₯Ό μ‚¬μš©ν•˜μ—¬ νŽ˜μ΄μ§€λ₯Ό 사전 λ‘œλ”©ν•©λ‹ˆλ‹€. μ‚¬μš©μžκ°€ 링크에 마우슀λ₯Ό μ˜¬λ¦¬λŠ” μˆœκ°„ ν•΄λ‹Ή νŽ˜μ΄μ§€μ˜ λ¦¬μ†ŒμŠ€λ₯Ό 미리 λ‘œλ“œν•¨μœΌλ‘œμ¨, 클릭 ν›„ νŽ˜μ΄μ§€ μ „ν™˜μ΄ μ¦‰κ°μ μœΌλ‘œ μ΄λ£¨μ–΄μ§‘λ‹ˆλ‹€. 이λ₯Ό 톡해 μ‚¬μš©μž κ²½ν—˜μ„ 크게 ν–₯μƒμ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

3. 이미지 μ»¨ν…Œμ΄λ„ˆλ₯Ό μ‚¬μš©ν•œ CLS ν•΄κ²°

μ•ˆμ’‹μ€ 예:

circular-no-height

쒋은 예:

Animation1s-min

μ„€λͺ…:

  • 이미지 데이터λ₯Ό λΉ„λ™κΈ°μ μœΌλ‘œ κ°€μ Έμ˜€λŠ” λ™μ•ˆ λ°œμƒν•  수 μžˆλŠ” λ ˆμ΄μ•„μ›ƒ λ³€κ²½ 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄, κ³ μ •λœ 크기λ₯Ό 가진 μ»¨ν…Œμ΄λ„ˆλ‘œ 이미지λ₯Ό κ°μŒŒμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 이미지가 λ‘œλ“œλ˜κΈ° 전후에 λ ˆμ΄μ•„μ›ƒμ΄ λ³€ν•˜λŠ” 것을 λ°©μ§€ν•˜μ—¬ Cumulative Layout Shift(CLS) 문제λ₯Ό ν•΄κ²°ν–ˆμŠ΅λ‹ˆλ‹€. 이 μ ‘κ·Ό 방식은 μ‚¬μš©μž κ²½ν—˜μ„ κ°œμ„ ν•˜λŠ” μ€‘μš”ν•œ μ΅œμ ν™” 기법 쀑 ν•˜λ‚˜λ‘œ, 특히 이미지 λ‘œλ“œ μ‹œκ°„μ— 따라 νŽ˜μ΄μ§€μ˜ μš”μ†Œλ“€μ΄ μœ„μΉ˜λ₯Ό 바꾸지 μ•Šλ„λ‘ ν•¨μœΌλ‘œμ¨ μ‹œκ°μ  μ•ˆμ •μ„±μ„ 보μž₯ν•©λ‹ˆλ‹€.

4. ν…μŠ€νŠΈ 파일 μ••μΆ•(Gzip)을 ν†΅ν•œ 전솑 μ΅œμ ν™”

ν”„λ‘œμ νŠΈμ˜ μ„±λŠ₯을 μ΅œμ ν™”ν•˜κΈ° μœ„ν•΄ 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 ν™•μž₯자λ₯Ό 가지며, μ„œλ²„μ—μ„œ 이λ₯Ό μ˜¬λ°”λ₯΄κ²Œ μ œκ³΅ν•˜λ„λ‘ μ„€μ •ν•¨μœΌλ‘œμ¨ 졜적의 μ„±λŠ₯을 λ‹¬μ„±ν–ˆμŠ΅λ‹ˆλ‹€.
  1. λ¬΄ν•œ μŠ€ν¬λ‘€μ„ ν†΅ν•œ μ‚¬μš©μž κ²½ν—˜ ν–₯상 React Query의 useInfiniteQueryλ₯Ό ν™œμš©ν•˜μ—¬ λ¬΄ν•œ 슀크둀 κΈ°λŠ₯을 κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 μ‚¬μš©μžλŠ” νŽ˜μ΄μ§€ 끝에 도달할 λ•Œλ§ˆλ‹€ μžλ™μœΌλ‘œ μΆ”κ°€ μ½˜ν…μΈ λ₯Ό λ‘œλ“œν•  수 μžˆμ–΄, λŒ€λŸ‰μ˜ 데이터λ₯Ό 효율적으둜 ν‘œμ‹œν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이 방식은 μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ°„μ†Œν™”ν•˜κ³ , νŽ˜μ΄μ§€ μ „ν™˜ 없이 μ½˜ν…μΈ λ₯Ό μ—°μ†μ μœΌλ‘œ μ œκ³΅ν•˜λŠ” μ‚¬μš©μž κ²½ν—˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€.

  2. 이미지 지연 λ‘œλ”© (Lazy Loading) νƒœκ·Έμ— loading='lazy' 속성을 μΆ”κ°€ν•˜μ—¬, 화면에 보이지 μ•ŠλŠ” 이미지듀은 μŠ€ν¬λ‘€ν•  λ•ŒκΉŒμ§€ λ‘œλ“œλ˜μ§€ μ•Šλ„λ‘ μ΅œμ ν™”ν–ˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 초기 λ‘œλ”© μ‹œκ°„μ„ 쀄이고, λΆˆν•„μš”ν•œ λ„€νŠΈμ›Œν¬ νŠΈλž˜ν”½μ„ κ°μ†Œμ‹œμΌ°μŠ΅λ‹ˆλ‹€. 이 기법은 특히 이미지가 λ§Žμ€ νŽ˜μ΄μ§€μ—μ„œ μ„±λŠ₯을 크게 ν–₯μƒμ‹œν‚΅λ‹ˆλ‹€.

  3. WOFF 폰트 μ‚¬μš©μ„ ν†΅ν•œ μ„±λŠ₯ μ΅œμ ν™” μ›Ή 폰트둜 WOFF(μ›Ή μ˜€ν”ˆ 폰트 ν˜•μ‹)λ₯Ό μ‚¬μš©ν•˜μ—¬ ν…μŠ€νŠΈ λ Œλ”λ§μ„ μ΅œμ ν™”ν–ˆμŠ΅λ‹ˆλ‹€. WOFF 포맷은 μ••μΆ•λ₯ μ΄ λ†’κ³ , λŒ€λΆ€λΆ„μ˜ λΈŒλΌμš°μ €μ—μ„œ μ§€μ›λ˜λ―€λ‘œ, 폰트 파일 크기λ₯Ό 쀄여 μ„±λŠ₯을 κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이둜 인해 ν…μŠ€νŠΈκ°€ λΉ λ₯΄κ²Œ λ Œλ”λ§λ˜κ³ , νŽ˜μ΄μ§€ λ‘œλ”© μ‹œκ°„μ΄ λ‹¨μΆ•λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

ν”„λ‘œμ νŠΈ ꡬ쑰

src/
β”‚
β”œβ”€β”€ components/       # μž¬μ‚¬μš© κ°€λŠ₯ν•œ UI μ»΄ν¬λ„ŒνŠΈλ“€
β”‚   β”œβ”€β”€ common/       # κ³΅ν†΅μ μœΌλ‘œ μ‚¬μš©λ˜λŠ” μ»΄ν¬λ„ŒνŠΈλ“€
β”‚   └── layout/       # λ ˆμ΄μ•„μ›ƒ κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈλ“€
β”‚
β”œβ”€β”€ hooks/            # μ»€μŠ€ν…€ ν›…λ“€
β”‚
β”œβ”€β”€ pages/            # 각 νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈλ“€
β”‚
β”œβ”€β”€ store/            # μ „μ—­ μƒνƒœ 관리 (Zustand)
β”‚
β”œβ”€β”€ constants/        # μƒμˆ˜ κ°’λ“€ (예: 색상, URL λ“±)
β”‚
β”œβ”€β”€ models/           # TypeScript μΈν„°νŽ˜μ΄μŠ€μ™€ νƒ€μž… μ •μ˜
β”‚
└── utils/            # μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜λ“€

ν”Όλ“œλ°± μš”μ²­

이 READMEκ°€ ν”„λ‘œμ νŠΈμ˜ 기술적 μ—­λŸ‰μ„ 잘 λ“œλŸ¬λ‚΄κ³  μžˆλŠ”μ§€, λ˜λŠ” μˆ˜μ •ν•  뢀뢄이 μžˆλ‹€λ©΄ ν”Όλ“œλ°±μ„ μ£Όμ‹œλ©΄ λ°˜μ˜ν•˜κ² μŠ΅λ‹ˆλ‹€. 좔가적인 μ •λ³΄λ‚˜ κ°•μ‘°ν•˜κ³  싢은 뢀뢄이 μžˆλ‹€λ©΄ μ•Œλ €μ£Όμ„Έμš”!

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages