LogoSEO Jing
  • All Posts
  • SEO Jing
  • okayJing
  • KD Team
  • CLAB Coreteam
  • Study

Contact Me

© 2026 SEOJing. All rights reserved.

이펙티브 타입스크립트 2판 Day 14: JS에서 TS로 옮길 때 먼저 좁혀야 할 경계

2026년 7월 8일·8분 읽기
범위: Effective TypeScript 2판 Item 79–83
오늘의 질문: “JavaScript 프로젝트를 TypeScript로 바꾸는 일은 파일 확장자를 .ts로 바꾸는 일일까?”

이 글의 원천 앵커

마지막 범위는 마이그레이션입니다. TypeScript를 처음부터 쓰는 프로젝트보다, 이미 돌아가는 JavaScript를 점진적으로 옮기는 프로젝트가 더 어렵습니다. 기존 런타임 의미를 깨지 않으면서 타입 경계를 좁혀야 하기 때문입니다.

이번 글에서 잃으면 안 되는 포인트는 이겁니다.

낡은 JavaScript 문법을 먼저 현대화하면 타입 전환 비용이 줄어든다. @ts-check와 JSDoc은 JS 파일 안에서도 타입 피드백을 준다. allowJs는 혼합 상태를 허용하지만, 영구 방치하면 경계가 흐려진다. 모듈별 마이그레이션은 입출력 계약부터 좁혀야 한다. noImplicitAny는 마지막에 켜는 엄격 옵션이 아니라, 새 구멍을 막는 품질 게이트가 될 수 있다.

JS에서 TS로 옮기는 게이트

먼저 보는 코드

js
function getUserName(user) {
  return user.profile.name.trim();
}

작은 함수처럼 보이지만 이 코드에는 많은 가정이 숨어 있습니다.

user는 객체다. profile이 있다. name은 문자열이다. trim을 호출해도 된다. 실패하면 throw해도 된다.

파일을 user.ts로 바꾸면 타입 에러가 생깁니다. 여기서 급하게 any를 붙이면 마이그레이션은 빨라 보이지만, 실제로는 기존 불확실성을 타입 시스템 안으로 들여오는 겁니다.

ts
function getUserName(user: any) {
  return user.profile.name.trim();
}

이건 TypeScript로 옮긴 코드가 아니라, JavaScript의 불확실성을 any로 포장한 코드에 가깝습니다.

1단계: JS 현대화로 런타임 의미를 선명하게 만들기

마이그레이션 전에 오래된 JS 습관을 줄이면 타입 전환이 쉬워집니다.

js
var user = getUser();
if (user && user.profile && user.profile.name) {
  console.log(user.profile.name);
}

현대적인 JS로 먼저 바꾸면 의도가 더 잘 보입니다.

js
const user = getUser();
const name = user?.profile?.name;

if (typeof name === "string") {
  console.log(name);
}

이 변화는 타입스크립트만을 위한 게 아닙니다. var를 줄이고, 모듈 경계를 ESM으로 정리하고, optional chaining/nullish coalescing을 쓰면 런타임 의미가 선명해집니다. Day 13의 Module 글과도 이어집니다. CJS/ESM이 뒤섞인 상태에서 TS 전환을 시작하면 타입보다 로더/번들러 문제가 먼저 터질 수 있습니다.

리뷰 질문은 이렇습니다.

TS 전환 전에 JS 자체의 의미가 충분히 선명한가? 오래된 문법 때문에 타입이 불필요하게 넓어지는가? this, prototype, module side effect 같은 런타임 동작을 이해하지 못한 채 타입만 붙이고 있지 않은가?

2단계: @ts-check와 JSDoc으로 먼저 신호를 보기

모든 파일을 한 번에 .ts로 바꾸지 않아도 타입 피드백을 받을 수 있습니다.

js
// @ts-check

/**
 * @param {{ id: string, name: string }} user
 * @returns {string}
 */
export function getDisplayName(user) {
  return user.name.trim();
}

JSDoc은 “TS를 못 쓰는 임시방편”이 아닙니다. JS 파일을 유지해야 하는 상황에서 public contract를 드러내는 도구가 됩니다. 특히 라이브러리 코드, 빌드 설정, 레거시 script, 점진 전환 중인 파일에서 유용합니다.

다만 JSDoc도 만능은 아닙니다.

타입이 복잡해지면 TS 파일보다 읽기 어렵다. 런타임 검증을 대신하지 않는다. 문서와 실제 코드가 어긋날 수 있다. 팀이 JSDoc 타입 문법을 읽을 수 있어야 한다.

그래서 JSDoc은 최종 목적지라기보다, “어디가 불확실한지 먼저 드러내는 스캐너”처럼 쓰는 편이 좋습니다.

3단계: allowJs는 혼합 상태를 위한 장치다

allowJs를 켜면 TS 프로젝트 안에서 JS 파일을 함께 다룰 수 있습니다.

json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true
  }
}

이 설정은 큰 프로젝트를 한 번에 옮기지 않아도 되게 합니다. 하지만 혼합 상태는 관리하지 않으면 오래 갑니다. .js, .jsx, .ts, .tsx가 섞인 채 규칙이 없으면 팀은 어떤 파일에 어느 정도 타입 엄격도를 기대해야 하는지 알기 어렵습니다.

마이그레이션 PR에서 봐야 할 건 파일 확장자 수가 아닙니다.

어떤 폴더부터 옮기는가? 새 파일은 TS로만 받는가? JS 파일에 @ts-check를 붙이는 기준이 있는가? allowJs를 언제 줄일지 계획이 있는가? 타입 에러를 skipLibCheck, any, // @ts-ignore로 덮고 있지 않은가?

allowJs는 다리입니다. 다리를 건너는 계획이 없으면 프로젝트 구조가 영구히 애매해집니다.

4단계: 모듈별로 입출력 계약부터 좁히기

마이그레이션은 아래에서 위로, 또는 밖에서 안으로만 정해지는 게 아닙니다. 중요한 것은 경계입니다.

js
// price.js
export function formatPrice(value, currency) {
  if (currency === "KRW") return `${value.toLocaleString()}원`;
  return `$${value.toFixed(2)}`;
}

이 파일을 옮길 때 함수 내부부터 세밀하게 타입을 붙이기보다, 먼저 입출력을 고정합니다.

ts
type Currency = "KRW" | "USD";

export function formatPrice(value: number, currency: Currency): string {
  if (currency === "KRW") return `${value.toLocaleString()}원`;
  return `$${value.toFixed(2)}`;
}

그다음 호출 지점에서 깨지는 곳을 봅니다. 어떤 호출자는 currency를 그냥 string으로 들고 있을 수 있습니다. 어떤 호출자는 value가 API에서 온 문자열일 수 있습니다. 이때 타입 에러는 귀찮은 방해물이 아니라, 경계가 어디서 흐려졌는지 알려주는 지도입니다.

AI가 만든 migration PR에서 특히 자주 보이는 나쁜 패턴은 이것입니다.

ts
export function formatPrice(value: any, currency: any): any {
  // 기존 코드 그대로
}

겉으로는 TS 파일이 됐지만 타입 경계는 하나도 좁아지지 않았습니다. 이런 PR은 “변환량”은 많아도 신뢰도는 거의 올라가지 않습니다.

noImplicitAny는 새 구멍을 막는 게이트다

noImplicitAny를 켜면 타입스크립트가 추론하지 못한 암묵적 any를 에러로 봅니다.

ts
function handleChange(event) {
  console.log(event.target.value);
}

이 함수의 event는 암묵적 any가 됩니다. 작은 코드에서는 편하지만, 프로젝트가 커지면 any가 타입 시스템의 검문을 우회하는 통로가 됩니다.

ts
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  console.log(event.target.value);
}

noImplicitAny를 처음부터 전역으로 켜면 오래된 프로젝트에서는 너무 많은 에러가 날 수 있습니다. 그렇다고 영원히 끄면 새 코드도 계속 구멍을 만듭니다. 현실적인 전략은 단계적 게이트입니다.

새로 작성하는 TS 파일에서는 noImplicitAny를 지킨다. 마이그레이션한 폴더부터 엄격도를 올린다. 임시 any에는 TODO와 제거 기준을 단다. unknown으로 받을 수 있는 외부 값은 any 대신 unknown을 쓴다. @ts-ignore는 사유 없이 늘리지 않는다.

핵심은 “strict를 켰다/안 켰다”가 아니라, 타입 시스템을 우회하는 새 구멍을 더 이상 만들지 않는 것입니다.

AI migration PR을 리뷰하는 기준

AI는 파일 확장자 변경, annotation 추가, import 정리 같은 반복 작업을 잘합니다. 하지만 그럴듯한 migration PR일수록 리뷰 기준이 필요합니다.

PR 변화확인할 질문
.js → .ts 대량 변경런타임 의미가 바뀌지 않았는가?
any 추가정말 필요한 임시 구멍인가, 귀찮은 에러를 덮었는가?
as SomeType 추가외부 값을 검증 없이 믿게 만들지 않는가?
JSDoc 추가실제 호출 방식과 문서가 일치하는가?
allowJs 설정혼합 상태를 줄이는 다음 단계가 있는가?
noImplicitAny 대응새 코드의 암묵적 any를 막고 있는가?

좋은 migration은 diff가 큰 PR이 아닙니다. 경계를 하나씩 좁히고, 깨지는 호출 지점을 드러내고, 런타임 동작을 그대로 유지하는 PR입니다.

시리즈를 닫으며

Effective TypeScript 2판을 프론트엔드 코드 리뷰 관점으로 읽으면 결론은 하나로 모입니다.

타입은 런타임을 바꾸지 않는다. 타입은 값의 가능한 집합을 줄인다. 좋은 모델은 불가능한 상태를 표현하지 않는다. 외부 입력은 경계에서 좁혀야 한다. public API 타입은 사용자 경험이다. 마이그레이션은 코드의 신뢰 경계를 조금씩 좁히는 일이다.

JS Quizbook과도 이어집니다. JavaScript 런타임 모델을 모르면 TypeScript migration도 겉만 바뀝니다. this, module, closure, async timing, object reference를 이해해야 타입이 실제 코드 의미와 맞게 붙습니다.

Day 14 타입 리뷰 퀴즈

Quiz1 / 4
Q.JavaScript 파일을 TypeScript로 옮길 때 `any`를 대량으로 붙이는 방식의 가장 큰 문제는 무엇일까요?

Post Q&A

오케이징에게 물어보기

이펙티브 타입스크립트 2판 Day 14: JS에서 TS로 옮길 때 먼저 좁혀야 할 경계 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!

0/500

포스트 목록

/study/effective-typescript
파일 14개, 폴더 0개
이펙티브 타입스크립트 2판 Day 1: 타입스크립트를 믿기 전에 알아야 할 경계이펙티브 타입스크립트 2판 Day 2: 타입을 값의 집합으로 보기이펙티브 타입스크립트 2판 Day 3: 타입 반복을 줄이는 리뷰 감각이펙티브 타입스크립트 2판 Day 4: 추론을 살리는 값 생성 패턴이펙티브 타입스크립트 2판 Day 5: 좁혀진 타입은 언제 다시 넓어지는가이펙티브 타입스크립트 2판 Day 6: 유효한 상태만 표현하는 타입 설계이펙티브 타입스크립트 2판 Day 7: 문자열보다 도메인 의미를 좁히기이펙티브 타입스크립트 2판 Day 8: any를 밖에 세우고 unknown으로 검증하기이펙티브 타입스크립트 2판 Day 9: 제네릭은 관계를 보존할 때만 강하다이펙티브 타입스크립트 2판 Day 10: 타입도 테스트하고 빠지는 분기는 막는다이펙티브 타입스크립트 2판 Day 11: 객체 타입은 열려 있고, 계약은 닫아야 한다이펙티브 타입스크립트 2판 Day 12: 타입은 공개 API의 사용법이다이펙티브 타입스크립트 2판 Day 13: 타입은 런타임과 테스트를 대신하지 않는다이펙티브 타입스크립트 2판 Day 14: JS에서 TS로 옮길 때 먼저 좁혀야 할 경계

같은 섹션의 대표 이미지

14 posts · latest first
런타임 값 검증, DOM 환경, 테스트, 컴파일러 성능으로 이어지는 타입스크립트 경계 다이어그램
Study26. 07. 09.

이펙티브 타입스크립트 2판 Day 13: 타입은 런타임과.

Item 74–78 범위를 바탕으로 런타임 타입 재구성, DOM과 환경 모델, 단위 테스트 관계, 컴파일러 성능을 코드 리뷰 관점에서 정리합니다.

26. 07. 09.SEOJing
JavaScript 현대화에서 @ts-check, allowJs, 모듈별 전환, noImplicitAny로 이어지는 마이그레이션 단계 다이어그램
Study26. 07. 09.

이펙티브 타입스크립트 2판 Day 14: JS에서 TS로 옮길.

Item 79–83 범위를 바탕으로 JS 현대화, @ts-check와 JSDoc, allowJs, 모듈별 마이그레이션, noImplicitAny를 코드 리뷰 관점에서 정리합니다.

26. 07. 09.SEOJing
내부 구현 타입에서 문서화된 public API 타입으로 넘어가는 경계 다이어그램
Study26. 07. 08.

이펙티브 타입스크립트 2판 Day 12: 타입은 공개 API의.

Item 67–73 범위를 바탕으로 public API 타입, TSDoc, this callback, module augmentation, TypeScript 기능 선택, source map을 코드 리뷰 관점에서 정리합니다.

26. 07. 08.SEOJing
Object iteration, Record, tuple/rest, XOR, brand, @types 버전 관리를 연결한 타입 리뷰 다이어그램
Study26. 07. 07.

이펙티브 타입스크립트 2판 Day 11: 객체 타입은 열려.

Item 60–66 범위를 바탕으로 Object iteration, Record, tuple/rest, XOR, brand, @types 버전 경계를 코드 리뷰에서 다루는 방법을 정리합니다.

26. 07. 07.SEOJing
타입 계약, 타입 테스트, never exhaustiveness, 코드 생성 경계를 보여주는 다이어그램
Study26. 07. 06.

이펙티브 타입스크립트 2판 Day 10: 타입도 테스트하고.

Item 55–59 범위를 바탕으로 type test, 타입 표시, 꼬리 재귀 한계, 코드 생성, never exhaustiveness를 코드 리뷰에서 다루는 방법을 정리합니다.

26. 07. 06.SEOJing
외부 경계, 제네릭, 조건부 타입, 템플릿 리터럴 타입의 역할과 과한 타입 계산의 위험을 보여주는 다이어그램
Study26. 07. 05.

이펙티브 타입스크립트 2판 Day 9: 제네릭은 관계를 보존할.

Item 49–54 범위를 바탕으로 type coverage, 제네릭, 조건부 타입, 템플릿 리터럴 타입을 코드 리뷰에서 어떻게 판단할지 정리합니다.

26. 07. 05.SEOJing
외부 JSON을 unknown으로 받고 검증 뒤 도메인 타입으로 넘기는 경계와 any가 내부로 번지는 위험을 보여주는 다이어그램
Study26. 07. 04.

이펙티브 타입스크립트 2판 Day 8: any를 밖에 세우고.

Item 42–48 범위를 바탕으로 any, unknown, 타입 단언, monkey patching, soundness 함정을 외부 입력 경계와 코드 리뷰 관점에서 정리합니다.

26. 07. 04.SEOJing
넓은 문자열과 선택적 필드를 도메인 이름과 유효 상태 유니온으로 좁히는 다이어그램
Study26. 06. 29.

이펙티브 타입스크립트 2판 Day 7: 문자열보다 도메인.

Item 35–41 범위를 바탕으로 string 남용, optional 필드, 특수 값, 도메인 이름 설계를 코드 리뷰 관점에서 정리합니다.

26. 06. 29.SEOJing
API 응답을 유효한 상태 유니온으로 변환하는 흐름 다이어그램
Study26. 06. 28.

이펙티브 타입스크립트 2판 Day 6: 유효한 상태만 표현하는.

Item 28–34 범위를 바탕으로 추론 위치, 유효 상태 모델링, API 입출력, null 경계, union 설계를 프론트엔드 코드 리뷰 관점에서 확장 정리합니다.

26. 06. 28.SEOJing
타입스크립트 추론이 값에서 API 경계로 흐르는 과정을 보여주는 다이어그램
Study26. 06. 27.

이펙티브 타입스크립트 2판 Day 5: 좁혀진 타입은 언제.

Item 22–27 범위를 바탕으로 narrowing, alias, context inference, evolving type, async/type flow를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.

26. 06. 27.SEOJing
TypeScript에서 넓은 타입으로 먼저 담으면 key와 literal 정보가 사라지고 리뷰 체크가 약해지는 흐름 다이어그램
Study26. 06. 26.

이펙티브 타입스크립트 2판 Day 4: 추론을 살리는 값 생성.

Effective TypeScript 2판의 Item 16–21을 바탕으로 index signature, 타입 추론 기본, 변수/객체 생성 패턴을 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 26.SEOJing
반복된 타입 선언을 원본 타입에서 파생한 타입으로 바꿔 리뷰 체크를 강하게 만드는 흐름 다이어그램
Study26. 06. 25.

이펙티브 타입스크립트 2판 Day 3: 타입 반복을 줄이는 리뷰.

Effective TypeScript 2판의 Item 11–15를 바탕으로 excess property check, 함수식 타입, type vs interface, readonly, 타입 반복 제거를 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 25.SEOJing
TypeScript 타입을 값의 집합, type space, value space, assertion 경계로 정리한 다이어그램
Study26. 06. 24.

이펙티브 타입스크립트 2판 Day 2: 타입을 값의 집합으로.

Effective TypeScript 2판의 Item 6–10을 바탕으로 타입 시스템을 탐색하는 법, 타입을 값의 집합으로 보는 관점, type/value space, 타입 단언의 경계를 코드 리뷰 관점에서 정리합니다.

26. 06. 24.SEOJing
TypeScript를 JavaScript 위의 정적 타입 계층, 타입 제거, 구조적 타이핑, any의 구멍으로 정리한 다이어그램
Study26. 06. 23.

이펙티브 타입스크립트 2판 Day 1: 타입스크립트를 믿기.

Effective TypeScript 2판의 Item 1–5를 바탕으로 TypeScript와 JavaScript의 관계, tsconfig, 타입 제거, 구조적 타이핑, any의 위험을 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 23.SEOJing