메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
Sonamu ν”„λ‘œμ νŠΈλŠ” TypeScript둜 μž‘μ„±λ˜λ©°, API와 Web 각각 독립적인 tsconfig.json을 κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€. 각 ν™˜κ²½μ— μ΅œμ ν™”λœ 섀정을 μ‚¬μš©ν•©λ‹ˆλ‹€.

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

API와 Web은 각각 독립적인 TypeScript ν”„λ‘œμ νŠΈμž…λ‹ˆλ‹€. 섀정이 μ„œλ‘œ λΆ„λ¦¬λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

API μ„œλ²„ μ„€μ •

κΈ°λ³Έ tsconfig.json

api/tsconfig.json
{
  "compilerOptions": {
    /* Basic Options */
    "target": "esnext",
    "module": "esnext",
    "outDir": "dist",
    "sourceMap": true,
    "lib": ["esnext", "dom"],

    /* Strict Type-Checking Options */
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,

    /* Additional Checks */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "useUnknownInCatchVariables": true,

    /* Module Resolution Options */
    "moduleResolution": "bundler",
    "esModuleInterop": true,

    /* Experimental Options */
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,

    /* Advanced Options */
    "forceConsistentCasingInFileNames": true,
    "noErrorTruncation": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["src/**/*.ts", "src/**/*.json"],
  "exclude": [
    "node_modules",
    "dist/*",
    "public",
    // "src/**/*.test.ts",  // ν…ŒμŠ€νŠΈ νŒŒμΌλ„ νƒ€μž… μ²΄ν¬ν•˜λ―€λ‘œ 주석 처리
    "**/__mocks__/**",
    "vite.config.ts"
  ]
}

μ£Όμš” μ˜΅μ…˜ μ„€λͺ…

{
  "target": "esnext",           // μ΅œμ‹  ECMAScript 문법 μ‚¬μš©
  "module": "esnext",            // ES λͺ¨λ“ˆ 좜λ ₯
  "moduleResolution": "bundler"  // Vite/esbuild와 ν˜Έν™˜
}
moduleResolution: β€œbundler”
  • Node.js의 node λͺ¨λ“œ λŒ€μ‹  λ²ˆλ“€λŸ¬ λͺ¨λ“œ μ‚¬μš©
  • Vite와 esbuildκ°€ λͺ¨λ“ˆμ„ ν•΄μ„ν•˜λŠ” 방식과 일치
  • package.json의 exports ν•„λ“œ 지원
moduleResolution: "node"λ₯Ό μ‚¬μš©ν•˜λ©΄ Vite와 μΆ©λŒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

ν™•μž₯ μ„€μ • νŒŒμΌλ“€

Sonamu APIλŠ” μ½”λ“œ 생성을 μœ„ν•΄ μΆ”κ°€ tsconfig νŒŒμΌμ„ μ‚¬μš©ν•©λ‹ˆλ‹€.
api/tsconfig.schemas.json
{
  "extends": "./tsconfig.json",
  "include": ["src/sonamu.generated.ts"],
  "exclude": ["src/**/*.types.ts"]
}
μš©λ„:
  • sonamu.generated.ts 파일 생성
  • Entity μŠ€ν‚€λ§ˆ νƒ€μž… 생성
  • DB μŠ€ν‚€λ§ˆ β†’ TypeScript νƒ€μž… λ³€ν™˜
μ‚¬μš© μ‹œμ :
# Sonamuκ°€ μžλ™μœΌλ‘œ μ‚¬μš©
pnpm sonamu entity:load
api/tsconfig.types.json
{
  "extends": "./tsconfig.json",
  "include": ["src/**/*.types.ts"],
  "references": [{ "path": "./tsconfig.schemas.json" }]
}
μš©λ„:
  • API 응닡 νƒ€μž… 생성
  • Request/Response νƒ€μž… μ •μ˜
  • ν”„λ‘ νŠΈμ—”λ“œμ™€ νƒ€μž… 곡유
μ‚¬μš© μ‹œμ :
# Web으둜 νƒ€μž… 동기화
pnpm sonamu sync:types

Web (React) μ„€μ •

web/tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src", "src/routeTree.gen.ts"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

APIμ™€μ˜ 차이점

μ˜΅μ…˜APIWeb이유
moduleResolutionbundlerBundlerViteκ°€ λ²ˆλ“€λŸ¬ 방식 μ‚¬μš©
jsx-react-jsxReact 17+ JSX Transform
noEmitfalsetrueViteκ°€ λΉŒλ“œ λ‹΄λ‹Ή
isolatedModules-trueVite의 esbuild μš”κ΅¬μ‚¬ν•­
paths-@/*μ ˆλŒ€ 경둜 import
APIλŠ” moduleResolution: "bundler" (μ†Œλ¬Έμž), Web은 moduleResolution: "Bundler" (λŒ€λ¬Έμž)λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. λ‘˜ λ‹€ Viteκ°€ λ²ˆλ“€λŸ¬ λ°©μ‹μœΌλ‘œ λͺ¨λ“ˆμ„ ν•΄μ„ν•©λ‹ˆλ‹€.

Path Mapping

{
  "paths": {
    "@/*": ["./src/*"]
  }
}
μ ˆλŒ€ 경둜둜 importν•  수 μžˆμŠ΅λ‹ˆλ‹€:
// ❌ μƒλŒ€ 경둜
import { Button } from "../../../components/Button";

// βœ… μ ˆλŒ€ 경둜
import { Button } from "@/components/Button";
μžμ„Έν•œ λ‚΄μš©μ€ Path Mapping을 μ°Έκ³ ν•˜μ„Έμš”.

νƒ€μž… 체크 μ‹€ν–‰

cd api

# νƒ€μž… 체크만
pnpm tsc --noEmit

# Watch λͺ¨λ“œ
pnpm tsc --noEmit --watch
개발 μ€‘μ—λŠ” --watch λͺ¨λ“œλ‘œ μ‹€μ‹œκ°„ νƒ€μž… 체크λ₯Ό ꢌμž₯ν•©λ‹ˆλ‹€.

일반적인 문제 ν•΄κ²°

증상:
Cannot find module 'sonamu' or its corresponding type declarations.
원인:
  • node_modulesκ°€ μ„€μΉ˜λ˜μ§€ μ•ŠμŒ
  • 잘λͺ»λœ import 경둜
ν•΄κ²°:
# μ˜μ‘΄μ„± μž¬μ„€μΉ˜
pnpm install

# νƒ€μž… μ •μ˜ 확인
ls node_modules/sonamu/dist/*.d.ts
증상:
Experimental support for decorators is a feature that is subject to change
원인:
  • experimentalDecoratorsκ°€ λΉ„ν™œμ„±ν™”λ¨
ν•΄κ²°:
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
증상:
Cannot use JSX unless the '--jsx' flag is provided.
원인:
  • jsx μ˜΅μ…˜μ΄ μ„€μ •λ˜μ§€ μ•ŠμŒ
ν•΄κ²°:
{
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}
React 17+ ν•„μˆ˜ μ„€μ •
증상:
Cannot find module '@/components/Button'
원인:
  • paths μ„€μ • λˆ„λ½
  • baseUrl μ„€μ • ν•„μš”
ν•΄κ²°:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
Vite 섀정도 ν•„μš”:
vite.config.ts
import path from "path";

export default {
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
};

μ»€μŠ€ν„°λ§ˆμ΄μ§•

μΆ”κ°€ 라이브러리 νƒ€μž…

{
  "compilerOptions": {
    "lib": ["esnext", "dom", "dom.iterable", "webworker"]
  }
}

더 μ—„κ²©ν•œ 체크

{
  "compilerOptions": {
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true
  }
}
이 μ˜΅μ…˜λ“€μ€ κΈ°μ‘΄ μ½”λ“œμ™€ ν˜Έν™˜μ„± 문제λ₯Ό μΌμœΌν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

include/exclude νŒ¨ν„΄

{
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.json"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts",
    "**/*.spec.ts",
    "**/__mocks__/**"
  ]
}

λ‹€μŒ 단계