TypeScript 또는 눈물

발행: (2026년 2월 4일 오전 05:00 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

또 보기: Backend Quality Gates

Backend linters catch async footguns. Type checkers prevent runtime explosions. Now it’s the frontend’s turn.

문제

JavaScript는 조용히 실패합니다. 한 문장으로 정리하면 바로 이게 문제입니다.

  • 잘못된 인자를 가지고 함수를 호출하면 → 실행됩니다.
  • 존재하지 않는 속성에 접근하면 → 실행됩니다.
  • null을 처리하는 것을 잊으면 → 실행됩니다.

모두 실행됩니다. 아무것도 동작하지 않습니다. 사용자는 불평합니다. 왜 그런지 전혀 알 수 없습니다.

“괜찮아요. 모든 것이 괜찮아요.”
농담입니다. 이건 혼돈입니다.

백엔드와 마찬가지로, 이 모든 것이 아직 테스트조차 되지 않았습니다. 실제 테스트를 실행하기 전에 코드가 명백히 깨지지는 않았는지만 확인하고 있습니다. 기준은 여전히 바닥에 있습니다. 최소한 그 위를 한 걸음 넘어서 봅시다.

왜 JavaScript에 타입이 필요한가

JavaScript는 타입이 없습니다. 이것은 설계상의 결정이었으며, 잘못된 선택이었습니다.

function processUser(user) {
  return user.name.toUpperCase();
}

user는 무엇일까요? 객체? 문자열? 프로미스? JavaScript는 알 수 없습니다. JavaScript는 신경 쓰지 않으며, 모든 것이 가능하다고 믿습니다.

JavaScript는 낙관주의자입니다. 여러분은 그렇지 않아야 합니다.

TypeScript (strict mode) 도입

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

이제 컴파일러가 여러분에게 경고합니다:

// TypeScript는 이것을 거부합니다
function processUser(user) {  // Error: implicit 'any'
  return user.name.toUpperCase();
}

// TypeScript는 이것을 허용합니다
function processUser(user: User): string {
  return user.name.toUpperCase();
}

짜증나요? 네.
사용자보다 버그를 먼저 잡아주나요? 역시 그렇습니다. 이것이 트레이드‑오프입니다.

저는 strict: true noUncheckedIndexedAccess를 함께 사용합니다. 후자는 배열 접근이 undefined를 반환할 수 있다고 가정하기 때문에 특히 짜증나지만, 실제로 그럴 수 있으니 처리해 주어야 합니다.

ESLint를 사용한 린팅

TypeScript는 타입 오류를 잡아냅니다. ESLint는 그 외 모든 것을 잡아냅니다:

  • 사용되지 않은 변수
  • 일관성 없는 포맷팅
  • 위험한 패턴
  • 프로덕션에서 잊혀진 console.log

저는 Airbnb 스타일 가이드를 사용합니다. 의견이 강하고, 엄격하며, 검증된 가이드입니다. 수천 명의 엔지니어가 이 규칙에 대해 논쟁했으니 제가 직접 고민할 필요가 없습니다.

// eslint.config.js
import { configs, extensions, plugins } from 'eslint-config-airbnb-extended';

export default [
  plugins.stylistic,
  plugins.importX,
  plugins.react,
  plugins.reactA11y,
  plugins.reactHooks,
  plugins.typescriptEslint,

  ...extensions.base.typescript,
  ...extensions.react.typescript,

  ...configs.react.recommended,
  ...configs.react.typescript,
];

한 번의 import. 수백 개의 규칙. TypeScript 지원, React 훅, 접근성, import 정렬—모두 사전 설정되어 있습니다.

no-explicit-any가 가장 중요한 규칙입니다. 이 규칙은 탈출구를 닫아줍니다: any 타입을 마음대로 사용하고 타입 안전성을 가장할 수 없습니다. 올바르게 타입을 지정 하거나 빌드가 실패하도록 해야 합니다.

이 규칙이 제한적이라고 생각하는 사람도 있습니다. 그런 사람들은 새벽 2시 에 프로덕션 이슈를 디버깅합니다. 저는 새벽 2시 에 넷플릭스를 봅니다. 선택은 각자 다릅니다.

Storybook으로 컴포넌트 상태 문서화

재미있는 게임이 있습니다: 컴포넌트를 리팩터링하고, 앱을 실행하고, 클릭해보고, 배포합니다. 3개월 뒤에 아무도 자주 방문하지 않는 페이지에서 로딩 상태를 깨뜨린 것을 발견합니다.

컴포넌트에는 다양한 상태가 있습니다:

  • 정상 흐름
  • 오류 상태
  • 로딩 상태
  • 빈 상태
  • 엣지 케이스

모든 상태를 매번 수동으로 테스트할 수 없습니다. 여러분도, 저도, 누구도 할 수 없습니다.

Storybook은 모든 상태를 문서화합니다:

// Button.stories.tsx
export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Click me',
  },
};

export const Loading: Story = {
  args: {
    isLoading: true,
    children: 'Loading...',
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
    children: 'Nope',
  },
};

CI에서 build-storybook을 실행합니다. 어떤 컴포넌트라도 특정 상태에서 렌더링되지 않으면 빌드가 실패합니다. 뭔가를 깨뜨렸다면 배포 전에 고치세요.

보너스: AI는 이 스토리를 읽고, 존재하는 상태를 이해하며, 이를 처리하는 코드를 생성할 수 있습니다. 실제로 사용되는 문서화.

체크당 하나의 작업

무언가 실패하면 정확히 무엇이 문제인지 알 수 있습니다.

.frontend-quality:
  stage: quality
  image: node:lts-slim
  cache:
    key: npm-frontend-${CI_COMMIT_REF_SLUG}
    paths:
      - frontend/node_modules
      - ~/.npm
  before_script:
    - cd frontend
    - npm ci --prefer-offline
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

eslint:
  extends: .frontend-quality
  script:
    - npm run lint

typecheck:
  extends: .frontend-quality
  script:
    - npm run typecheck

storybook:build:
  extends: .frontend-quality
  script:
    - npm run build-storybook --quiet
  artifacts:
    paths:
      - frontend/storybook-static
    expire_in: 1 week
    when: always

세 개의 작업. 같은 단계. 병렬로 실행됩니다.

  • 숨겨진 작업(.frontend-quality)은 점으로 시작하므로 GitLab은 이를 템플릿으로 간주합니다(직접 실행되지 않음).
  • npm cinpm install이 아닙니다. ci는 더 빠르고 엄격하며 lockfile을 정확히 사용합니다. 예상치 못한 일이 없습니다.
  • --prefer-offline – 가능한 경우 캐시된 패키지를 사용합니다. 네트워크가 느릴 때 캐시는 빠릅니다.

병렬 실행

세 작업 모두 동시에 실행됩니다:

  • ESLint가 실패하면? 즉시 확인할 수 있습니다.
  • TypeScript가 실패하면? 두 가지 모두 확인됩니다. 함께 수정하세요.
  • Storybook 빌드가 실패하면? 이것도 확인됩니다.

Storybook 아티팩트는 빌드된 Storybook을 보관합니다. when: always는 빌드가 실패해도 저장하므로, 컴포넌트가 깨진 이유를 디버깅하는 데 유용합니다.

하나의 작업이 실패하면 파이프라인 뷰에서 정확히 어떤 작업이 실패했는지 확인할 수 있습니다. 로그를 스크롤할 필요도, 추측할 필요도 없습니다.

작업실패 의미
eslint스타일 문제 또는 위험한 패턴
typecheck타입 오류
storybook:build스토리에서 컴포넌트를 렌더링할 수 없음

이 중 어느 것도 코드가 의도대로 동작하는지를 검증하지 않습니다—그것이 테스트의 역할입니다. 이것은 코드가 명백히 깨지지 않았음을 검증할 뿐입니다.

기준은 낮지만, 이를 통과하지 못하는 프로젝트가 얼마나 많은지 놀라실 겁니다.

stages:
  - quality

.frontend-quality:
  stage: quality
  image: node:lts-slim
  cache:
    key: npm-frontend-${CI_COMMIT_REF_SLUG}
    paths:
      - frontend/node_modules
      - ~/.npm
  before_script:
    - cd frontend
    - npm ci --prefer-offline
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

eslint:
  extends: .frontend-quality
  script:
    - npm run lint

typecheck:
  extends: .frontend-quality
  script:
    - npm run typecheck

storybook:build:
  extends: .frontend-quality
  script:
    - npm run build-storybook --quiet
  artifacts:
    paths:
      - frontend/storybook-static
    expire_in: 1 week
    when: always

복사하고 붙여넣고, 적절히 수정하면 동작합니다.

프런트엔드 코드는 창의적인 방식으로 실패합니다: 조용히 실패하거나 런타임 오류가 발생하고, “내 컴퓨터에서는 동작한다”는 말이 사파리에서는 통하지 않으며, 예상치 못한 props를 전달하기 전까지는 정상적으로 렌더링되는 컴포넌트가 있습니다.

이 모든 것을 수동으로 잡아낼 수 없습니다. 제 집중력도 그리 좋지 않으니, 다른 사람도 마찬가지입니다.

그래서 눈에 띄는 부분을 자동화합니다:

  • 타입은 명시적이어야 합니다
  • 코드는 일관된 패턴을 따라야 합니다
  • 컴포넌트는 충돌 없이 렌더링되어야 합니다

머신이 제가 놓친 부분을 잡아주고, 파이프라인이 제가 후회할 상황을 차단합니다.

백엔드와 마찬가지로 한 번 규칙을 작성하면 영원히 적용됩니다.

다음 단계: 보안 – 곧 추가됩니다 – 의존성은 다른 사람들의 코드이며, 다른 사람들은 실수를 합니다.

Back to Blog

관련 글

더 보기 »