자바스크립트 프로젝트를 타입스크립트로 점진적 마이그레이션 하기
사내 프로젝트 중 하나는 자바스크립트로 작성된 레거시 프로젝트였고, 신규 기능 개발이나 유지보수 시마다 런타임 에러가 자주 발생하거나, 함수나 변수 정의 추적이 어려운 등의 문제가 있었습니다.
전반적인 생산성이 낮았고, 결국 타입스크립트로 마이그레이션을 진행하기로 했습니다. 다만 일정이 여유롭지 않아 전체 코드를 한 번에 마이그레이션 하긴 어려웠고, 신규 개발과 연관된 부분을 중심으로 점진적으로 변환하기로 했습니다.
1. 자바스크립트 프로젝트에 타입스크립트 세팅하기
타입스크립트 패키지를 설치 해줍니다. 기본적으로는 react 타입 패키지를 먼저 설치하고, 나중에 타입 패키지가 필요한 라이브러리가 있으면 그 때 마다 설치해서 사용했습니다.
yarn add -D typescript @types/react @types/react-dom
tsconfig.json
에서는 아래 옵션을 주로 설정했습니다.
allowJs
: true- .js 파일과 .ts 파일을 함께 사용할 수 있도록 허용
- 타입스크립트 파일을 자바스크립트 파일과 함께 사용하도록 허용했습니다
noImplicitAny
: false- 명시적 타입이 없을 경우 any로 추론.
- 마이그레이션 작업 초반에는
false
로 설정해두고, 코드베이스가 안정화 되면 옵션을 활성화 하는 걸 목표로 두었습니다
이 외에는 프로젝트가 create-react-app
으로 되어 있어 Adding TypeScript docs 를 참고했습니다.
2. 마이그레이션 전략 세우기
마이그레이션 작업을 하기 전 두 가지 접근 방식을 고려했습니다.
-
전체 코드를 .ts, .tsx로 변환하고, any, @ts-ignore를 사용해 일단 빌드를 통과시키는 방식
-
자주 사용하는 핵심 로직 또는 신규 개발과 맞닿은 영역만 .ts로 변환하고, 나머지는 .js로 유지하며 점진적으로 마이그레이션
결론적으로 2번을 선택했는데요,
1번 방식은 함수나 변수 정의를 추적하는 등의 이점이 있지만 any나 ts-ignore가 코드베이스에 퍼지는게 꺼려져서 선택해지 않았습니다.
따라서 .js는 기존 코드, .ts는 타입 적용 코드라는 경계를 유지하며 점진적으로 코드 베이스를 전환하기로 결정했습니다.
저희팀에서는 사용하지는 않았지만 만약 (1번 방식) 모든 코드를 일단 .ts로 변환하기로 결정했거나 코드 규모가 크고 자동화가 필요하다면 Airbnb에서 만든 ts-migrate 툴이 있습니다.
기계적으로 .js 파일을 .ts로 변환하고, 타입이 불분명한 부분에는 자동으로 any, @ts-ignore 등을 삽입해주는 도구입니다.
타입스크립트 파일로 마이그레이션 진행하기
어떤 코드부터 타입스크립트 코드로 바꿀지 우선순위를 판단하는 기준은 아래와 같이 정했습니다.
-
우선 순위가 높은 경우 🔼
- 유지보수할 코드와 연결되어 있는 코드
- 전역 상태를 관리하는 코드
- 서버에서 가져오는 API 응답 데이터를 다루는 로직
-
우선 순위가 낮은 경우 🔽
- 캡슐화가 잘 되어 있어 외부에서는 사용하는 메서드만 노출된 모듈
- 단순한 UI 렌더링만 하는 컴포넌트
2.1 데이터를 기반으로 타입을 생성해주는 도구 활용하기
마이그레이션을 하다보면 반복적으로 타입을 선언해줘야 하기 때문에, 타입 생성 도구를 적극적으로 활용했습니다.
transform - json-to-typescript 를 주로 사용했는데요, 이 도구는 JSON 데이터를 기반으로 TypeScript 인터페이스를 생성해줍니다.
{
"name": "John Doe",
"age": 30,
"address": {
"city": "Somewhere",
"state": "CA"
},
"phoneNumbers": [
{
"type": "home",
"number": "123-456-7890"
}
],
"children": [
{
"name": "Jane Doe",
"age": 10
}
]
}
예를 들어 위 json 데이터를 넣으면 아래 타입을 반환합니다.
export interface Root {
name: string;
age: number;
address: Address;
phoneNumbers: PhoneNumber[];
children: Children[];
}
export interface Address {
city: string;
state: string;
}
export interface PhoneNumber {
type: string;
number: string;
}
export interface Children {
name: string;
age: number;
}
물론 데이터를 기반으로 생성하는 코드기 때문에 100% 사용할 수는 없어 필요한 부분만 사용하면서 작업했습니다.
가령 Record<string, string>
을 의도한 객체의 경우, json 데이터를 넣으면 데이터를 기반으로 객체의 키로 타입이 추론됩니다. 이런 부분을 의도한 Record<string, string>
타입으로 수정해주는 등의 작업이 필요합니다.
2.2 .ts
파일로 바꾸지 않고, .d.ts 파일로 타입 선언만 추가하기
.js
파일을 .ts
파일로 바꾸면 많은 타입 에러와 부딫칩니다. 가능하면 모든 타입 에러에 대응해서 타입스크립트 코드를 늘리는게 제일 베스트겠지만, .js
로 작성되어 있는 파일이 너무 커서 대응하기가 힘들거나 캡슐화되어 외부에서는 맥락을 알 필요가 없는 로직인 경우에는 자바스크립트 파일을 유지하고, 타입만 추가로 선언해주었습니다.
이는 d.ts
파일을 사용해서 타입을 선언할 수 있습니다. 타입을 선언하고자 하는 자바스크립트 파일이 있는 위치에 파일을 만들어주면 됩니다.
예를 들어 hello.js 라는 자바스크립트 파일에 타입 선언을 추가 하고 싶다면, hello.d.ts 파일을 만들어줍니다.
// hello.js
export function greet(name) {
return `Hello, ${name}!`;
}
// hello.d.ts
export function greet(name: string): string;
이후 ./hello
를 import 해서 사용하면 작성한 타입이 적용됩니다.
2.3 openapi-generator 를 통해 백엔드와 통신하는 코드 자동 생성하기
만약 서버와 통신하는 코드마다 타입을 지정해주고 있다면, 차라리 타입을 지원하는 코드를 만들고 코드를 바꾸는 방법도 생각해볼만 합니다.
백엔드에서 openapi
스펙을 기반으로 제공되어 있다면 서버와 통신하는 부분을 openapi-generator 를 사용해 타입을 지원하는 rest api 호출 코드를 자동으로 만들 수 있습니다.
openapi-generator-cli generate -i <openapi 스펙 주소> -g typescript-axios -o ./models -c ./openapi.json --skip-validate-spec
기존에 사용하던 네트워크 요청 방식에 따라 typescript-axios 외에도 typescript-fetch 등 다른 생성 옵션을 선택할 수 있습니다.
이 방법은 타입만 지정하는 방법과는 다르게 기존 네트워크 호출 코드를 직접 수정해야 하므로 주의가 필요하지만, 하나씩 API 명세를 보고 수동으로 타입을 지정하는 것보다 실질적인 생산성을 높일 수 있습니다. 자동으로 생성된 타입 지원 코드를 기반으로 점진적으로 API 레이어를 교체하는 방식도 고려해볼 만합니다.
결론
반복되는 작업을 최대한 자동화 하기 위해서 Json 데이터를 타입 스크립트로 생성해주는 도구를 쓰거나, 일단 모든 파일을 .ts 코드로 변환하는 작업을 한다면 ts-migrate, openapi 스펙으로 typescript 코드를 만들어주는 openapi-generator 등 자동화 도구를 사용해보는걸 고려해보면 좋을 것 같습니다.
당장 .ts 파일로 전환할 필요가 없는 경우 (파일이 너무 크거나, 캡슐화가 잘 되어 있어 일부 메소드만 모듈 밖으로 드러나는 등) .js 파일을 유지하면서 타입 선언 파일만 만들어주는 방법을 사용했습니다.
코드베이스를 빠르게 마이그레이션해야 할 때는 전체 코드베이스를 변환 하기 보다는, 실제 사용 빈도가 높은 모듈부터 점진적으로 TypeScript를 도입하는 전략이 더 효과적이었습니다.