메인 콘텐츠로 건너뛰기
Sonamu가 API 변경을 컴파일 타임에 감지하여 런타임 에러를 사전에 방지하는 방법을 알아봅니다.

컴파일 타임 에러 개요

즉시 감지

API 변경 시컴파일 타임 에러

런타임 안전

배포 전 발견프로덕션 안정성

IDE 통합

빨간 밑줄즉각적인 피드백

리팩터링 안전

대규모 변경영향 범위 파악

컴파일 타임 에러란?

문제: 런타임에만 발견되는 에러

전통적인 개발에서는 API 변경이 런타임에서만 발견됩니다.
// ❌ 백엔드에서 필드명 변경
// username → displayName

// 프론트엔드 (변경 사항 모름)
function UserProfile({ userId }: { userId: number }) {
  const user = await fetchUser(userId);
  
  return (
    <div>
      {/* 런타임 에러! username 필드가 없음 */}
      <h1>{user.username}</h1>
    </div>
  );
}
런타임 에러의 문제점:
  1. 프로덕션에서 발생: 사용자가 직접 경험
  2. 발견 지연: QA나 사용자 리포트로만 발견
  3. 디버깅 어려움: 어디서 문제가 발생했는지 추적 필요
  4. 신뢰도 하락: 서비스 안정성 저하

해결: 컴파일 타임 에러

Sonamu는 API 변경을 컴파일 타임에 즉시 감지합니다.
// ✅ 백엔드에서 필드명 변경
@api({ httpMethod: "GET" })
async getUser(): Promise<{
  user: {
    id: number;
    displayName: string; // username → displayName
    email: string;
  };
}> {
  // 구현...
}

// pnpm generate 실행 → Service 재생성

// 프론트엔드 (컴파일 타임 에러 발생!)
function UserProfile({ userId }: { userId: number }) {
  const { data } = UserService.useUser("A", userId);
  
  return (
    <div>
      {/* ❌ 컴파일 에러! Property 'username' does not exist */}
      <h1>{data.user.username}</h1>
      
      {/* ✅ 수정 후 정상 */}
      <h1>{data.user.displayName}</h1>
    </div>
  );
}
컴파일 타임 에러의 장점:
  1. 즉시 발견: 코드 작성 중 IDE에서 즉시 표시
  2. 배포 전 수정: 프로덕션 배포 전에 모든 문제 해결
  3. 영향 범위 파악: 변경이 영향을 주는 모든 곳 확인
  4. 자동 리팩터링: IDE의 Rename 기능 활용

에러 감지 시나리오

1. 필드명 변경

// 백엔드: username → displayName
@api({ httpMethod: "GET" })
async getUser(): Promise<{
  user: { id: number; displayName: string; }
}> {
  // 구현...
}

// pnpm generate 후
// 프론트엔드: 모든 user.username 사용 지점에서 에러
const { data } = await UserService.getUser("A", 123);
console.log(data.user.username); // ❌ 컴파일 에러
IDE 에러 메시지:
Property 'username' does not exist on type '{ id: number; displayName: string; }'.
Did you mean 'displayName'?

2. 필드 타입 변경

// 백엔드: age를 number에서 string으로 변경
@api({ httpMethod: "GET" })
async getUser(): Promise<{
  user: { id: number; age: string; } // number → string
}> {
  // 구현...
}

// 프론트엔드
const { data } = await UserService.getUser("A", 123);
const nextYear = data.user.age + 1; 
// ❌ 컴파일 에러: string + number
IDE 에러 메시지:
Operator '+' cannot be applied to types 'string' and 'number'.

3. 필수/옵셔널 변경

// 백엔드: bio를 필수에서 옵셔널로 변경
@api({ httpMethod: "GET" })
async getUser(): Promise<{
  user: {
    id: number;
    bio?: string; // string → string | undefined
  };
}> {
  // 구현...
}

// 프론트엔드
const { data } = await UserService.getUser("C", 123);
const bioLength = data.user.bio.length;
// ❌ 컴파일 에러: bio가 undefined일 수 있음
IDE 에러 메시지:
Object is possibly 'undefined'.
수정:
// ✅ 옵셔널 체이닝 사용
const bioLength = data.user.bio?.length;

// ✅ 또는 널 체크
if (data.user.bio) {
  const bioLength = data.user.bio.length;
}

4. 필드 삭제

// 백엔드: profile 필드 제거
@api({ httpMethod: "GET" })
async getUser(): Promise<{
  user: {
    id: number;
    username: string;
    // profile 필드 삭제됨
  };
}> {
  // 구현...
}

// 프론트엔드
const { data } = await UserService.getUser("C", 123);
console.log(data.user.profile.avatar);
// ❌ 컴파일 에러: Property 'profile' does not exist

5. 파라미터 변경

// 백엔드: 파라미터 타입 변경
@api({ httpMethod: "GET" })
async searchUsers(query: {
  keyword: string;
  limit: number; // string → number
}): Promise<{ users: User[] }> {
  // 구현...
}

// 프론트엔드
await UserService.searchUsers({
  keyword: "john",
  limit: "10", // ❌ 컴파일 에러: string은 number에 할당 불가
});
IDE 에러 메시지:
Type 'string' is not assignable to type 'number'.

6. 반환 타입 변경

// 백엔드: 단일 객체에서 배열로 변경
@api({ httpMethod: "GET" })
async getPosts(): Promise<{
  posts: Post[]; // Post → Post[]
}> {
  // 구현...
}

// 프론트엔드
const { data } = await PostService.getPosts();
console.log(data.posts.title);
// ❌ 컴파일 에러: Property 'title' does not exist on type 'Post[]'
수정:
// ✅ 배열 처리
data.posts.forEach((post) => {
  console.log(post.title);
});

리팩터링 지원

대규모 변경도 안전

API 변경이 영향을 주는 모든 위치를 즉시 파악할 수 있습니다.
// 시나리오: User 엔티티의 구조 대폭 변경

// 백엔드 (변경 전)
@api({ httpMethod: "GET" })
async getUser(): Promise<{
  user: {
    id: number;
    name: string;
    email: string;
  };
}> {
  // ...
}

// 백엔드 (변경 후)
@api({ httpMethod: "GET" })
async getUser(): Promise<{
  user: {
    id: number;
    profile: {
      displayName: string;
      contactEmail: string;
    };
  };
}> {
  // ...
}
pnpm generate 실행 후: IDE에서 모든 에러 위치가 표시됩니다:
src/components/UserProfile.tsx:15 - Property 'name' does not exist
src/components/UserCard.tsx:8 - Property 'name' does not exist
src/pages/users/[id].tsx:42 - Property 'email' does not exist
... (총 23개 위치)
수정 과정:
  1. IDE의 “Problems” 패널에서 모든 에러 확인
  2. 각 위치를 방문하여 새로운 구조에 맞게 수정
  3. 모든 에러가 해결되면 컴파일 성공
  4. 안전하게 배포

IDE의 자동 리팩터링 활용

TypeScript의 강력한 리팩터링 기능을 활용할 수 있습니다. 예시: 필드명 일괄 변경
  1. Service 파일에서 타입 확인
  2. 필드명에 커서 위치
  3. F2 (Rename Symbol) 또는 우클릭 → Rename
  4. 새 이름 입력
  5. 모든 사용처가 자동으로 변경됨
// ✅ username → displayName 일괄 변경
// IDE가 자동으로 프로젝트 전체에서 변경

// Before
data.user.username

// After (자동 변경됨)
data.user.displayName

실전 워크플로우

1. 백엔드 API 변경

// backend/models/user.model.ts
@api({ httpMethod: "GET" })
async getProfile(): Promise<{
  user: {
    id: number;
    displayName: string; // 변경: username → displayName
    email: string;
  };
}> {
  // 구현...
}

2. Service 재생성

pnpm generate
출력:
✓ Analyzing API methods...
✓ Generating services.generated.ts...
✓ Generating types...
✓ Done!

3. TypeScript 컴파일

pnpm tsc --noEmit
출력 (에러 발생):
src/components/UserProfile.tsx:15:23 - error TS2339: 
Property 'username' does not exist on type '{ id: number; displayName: string; email: string; }'.

15     <h1>{data.user.username}</h1>
                      ~~~~~~~~

src/components/UserCard.tsx:8:35 - error TS2339: 
Property 'username' does not exist on type '{ id: number; displayName: string; email: string; }'.

8   const name = user.username;
                      ~~~~~~~~

Found 2 errors.

4. 에러 수정

// src/components/UserProfile.tsx
- <h1>{data.user.username}</h1>
+ <h1>{data.user.displayName}</h1>

// src/components/UserCard.tsx
- const name = user.username;
+ const name = user.displayName;

5. 재검증

pnpm tsc --noEmit
출력 (성공):
✓ No errors found!

6. 안전하게 배포

모든 타입 에러가 해결되었으므로 안전하게 배포할 수 있습니다.

CI/CD 통합

TypeScript 체크를 CI에 추가

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  typecheck:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '22'
      
      - name: Install dependencies
        run: pnpm install
      
      - name: Generate Services
        run: pnpm generate
      
      - name: TypeScript Check
        run: pnpm tsc --noEmit
이점:
  • Pull Request에서 타입 에러 자동 감지
  • 타입 에러가 있으면 머지 차단
  • 팀 전체의 코드 품질 보장

베스트 프랙티스

1. 정기적인 Service 재생성

# 개발 시작 시
pnpm generate

# 백엔드 변경 후
pnpm generate

# Pull 받은 후
pnpm generate

2. Pre-commit Hook 설정

// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "pnpm tsc --noEmit"
    }
  }
}
타입 에러가 있으면 커밋 차단됩니다.

3. VSCode 설정

// .vscode/settings.json
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  }
}

4. 타입 에러 무시하지 않기

// ❌ 타입 에러 무시 (위험!)
// @ts-ignore
console.log(data.user.username);

// ✅ 올바른 수정
console.log(data.user.displayName);
@ts-ignore절대 사용하지 마세요. 타입 에러는 실제 문제를 나타냅니다.

에러 메시지 이해하기

자주 보는 에러 메시지

1. Property does not exist
Property 'username' does not exist on type 'User'.
→ 필드가 삭제되었거나 이름이 변경됨 2. Type is not assignable
Type 'string' is not assignable to type 'number'.
→ 타입이 변경됨 3. Object is possibly undefined
Object is possibly 'undefined'.
→ 필수에서 옵셔널로 변경됨 4. Cannot find name
Cannot find name 'UserService'.
→ Service가 재생성되지 않았거나 import 누락 5. Expected N arguments, but got M
Expected 2 arguments, but got 1.
→ API 파라미터가 추가/삭제됨

주의사항

컴파일 타임 에러 처리 시 주의사항:
  1. pnpm generate 필수: API 변경 후 반드시 실행
  2. @ts-ignore 금지: 타입 에러를 무시하지 말 것
  3. any 타입 금지: 타입 안전성이 깨짐
  4. 모든 에러 수정: 하나라도 남기면 런타임 에러 가능
  5. CI/CD 통합: 자동 타입 체크 설정

다음 단계