TypeScript 또는 눈물
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 ci–npm 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를 전달하기 전까지는 정상적으로 렌더링되는 컴포넌트가 있습니다.
이 모든 것을 수동으로 잡아낼 수 없습니다. 제 집중력도 그리 좋지 않으니, 다른 사람도 마찬가지입니다.
그래서 눈에 띄는 부분을 자동화합니다:
- 타입은 명시적이어야 합니다
- 코드는 일관된 패턴을 따라야 합니다
- 컴포넌트는 충돌 없이 렌더링되어야 합니다
머신이 제가 놓친 부분을 잡아주고, 파이프라인이 제가 후회할 상황을 차단합니다.
백엔드와 마찬가지로 한 번 규칙을 작성하면 영원히 적용됩니다.
다음 단계: 보안 – 곧 추가됩니다 – 의존성은 다른 사람들의 코드이며, 다른 사람들은 실수를 합니다.