12장. 타입스크립트 프로젝트 관리
앰비언트 타입 활용하기
-
앰비언트 타입 선언
- 앰비언트 타입 : 값을 포함하는 일반적인 선언과 구별하기 위해
.d.ts
확장자를 가진 파일에서 하는 타입 선언 - 앰비언트 타입 선언으로 값을 정의할 수는 없지만 declare라는 키워드를 사용하여 어딘가에 자바스크립트 값이 존재한다는 사실을 선언할 수 있다.
- 앰비언트 타입 : 값을 포함하는 일반적인 선언과 구별하기 위해
-
대표적인 앰비언트 타입 선언 활용 사례
-
타입스크립트는 기본적으로
.ts
와.js
파일만 이해하며 그 외의 다른 파일 형식은 인식하지 못한다. -
따라서 알지 못하는 파일 형식을 모듈로 가져오려 하면 에러가 발생한다.
-
이런 상황에서 타입 스크립트의 declare 키워드를 사용하여 아래와 같이 특정 형식을 모듈로 선언하면 타입스크립트 컴파일러에 미리 정보를 제공함으로써 에러를 수정할 수 있게 된다.
declare module "*.png" { const src: string; export default src; }
⇒ declare 키워드는 이미 존재하지만 타입스크립트가 알지 못하는 부분을 컴파일러에 “이런것이 존재해”라고 알려주는 역할을 한다.
-
-
앰비언트 타입 선언 시 주의점
- 타입스크립트로 만드는 라이브러리에는 불필요
- 전역으로 타입을 정의하여 사용할 때 주의해야 할 점
- 동일한 이름의 앰비언트 타입 선언을 한다면 충돌이 발생해 어떤 타입 선언이 적용될지 알기 어려움, 의도한 대로 동작하지 않을 수 있다.
- 코드의 의존성 관계가 명확하지 않아 나중에 변경할 때 어려움을 겪을 수 있다.
-
앰비언트 타입 선언을 잘못 사용했을 때의 문제점
.ts
파일 내의 앰비언트 변수 선언은 개발자에게 혼란을 야기할 수 있다.- 앰비언트 타입의 의존성 관계가 보이지 않기 때문에 변경에 의한 영향 범위를 파악하기 어렵다.
- 앰비언트 타입은 명시적인 임포트나 익스포트 없이 코드 전역에서 사용할 수 있기 때문이다.
- 특히 소스코드 규모가 크다면 추후 변경이 어려워질 수 있다.
.d.ts
확장자 파일 내에서 앰비언트 타입 선언을 하는 것은 일종의 개발자 간의 약속이다. 타입 선언 위치가 명확해야 가독성이 높아지고 유지보수도 편하게 할 수 있기 때문이다.
-
앰비언트 타입 활용하기
-
타입을 정의하여 임포트 없이 전역으로 공유
-
declare type 활용하기
- 보편적으로 많이 사용하는 커스텀 유틸리티 타입을 declare type으로 선언하여 전역에서 사용할 수 있다.
declare type Nullable<T> = T | null; const name: Nullable<string> = "woowa";
-
declare module 활용하기
-
declare namespace 활용하기
- Node.js 환경에서
.env
파일을 사용할 때, declare namespace를 활용하여process.env
로 설정값을 손쉽게 불러오고 환경변수의 자동 완성 기능을 쓸 수 잇다. process.env
를 통해 접근하는 변수 또한 타입을 지정할 수 있기 때문에 as 단언을 사용하지 않아도 된다.
function log(str: string) { console.log(str); } // 1. namespace를 활용하여 process.env 타입을 보강해주지 않은 경우 // .env API_URL = "localhost:8080"; log(process.env.API_URL as string); // 2. namespace를 활용하여 process.env 타입을 보강한 경우 // .env API_URL = "localhost:8080"; declare namespace NodeJS { interface ProcessEnv { readonly API_URL: string; } } log(process.env.API_URL);
- Node.js 환경에서
-
declare global 활용하기
- 전역 변수를 선언할 때 사용한다.
-
-
declare와 번들러의 시너지
- declare global로 전역 변수를 선언하는 과정과 번들러를 통해 데이터를 주입하는 절차를 함께 활용하면 시너지를 낼 수 있다.
스크립트와 설정 파일 활용하기
- 스크립트 활용하기
-
실시간으로 타입을 검사하자
$> yarn tsc -noEMit -incremental -w # noEmit : 자바스크립트로 된 출력 파일을 생성하지 않도록 설정 # incremental : 증분 컴파일(변경 사항이 있는 부분만 컴파일)을 활성화하여 컴파일 시간 단축 # w : 변경 사항을 모니터링
⇒ 파일이 변경될 때마다 tsc가 실행되어 어디에서 타입 에러가 발생했는지 실시간으로 추적할 수 있다.
-
타입 커버리지 확인하기
$> npx type-coverage -detail
⇒ 현재 프로젝트의 타입 커버리지와 any를 사용하고 있는 변수의 위치가 나타난다.
⇒ 타입스크립트로 마이그레이션 중인 프로젝트나 레거시 코드가 많은 프로젝트를 다룰 때 타입 커버리지를 체크함으로써 더 나은 코드 퀄리티로 리팩터링하기 위한 기반을 마련하는데 도움이 되는 정량적인 지표를 얻을 수 있다.
-
- 설정 파일 활용하기
- 타입스크립트 컴파일 속도 높이기
- incremental 속성을 true로 설정하면 증분 컴파일이 활성화되어 매번 모든 대상을 컴파일하는 것이 아니라 변경된 부분만 컴파일하게 된다
- 이 설정은 tsconfig 파일에 추가하거나
yarn tsc -noEmit -incremental -diagnostic
으로 스크립트에서 사용할 수 있다. - 이를 적용하면 특히 검사 시간의 차이가 확연하다.
- 타입스크립트 컴파일 속도 높이기
- 에디터 활용하기
- 에디터에서 타입스크립트 서버 재시작하기
- 때로는 정의된 타입이 있는 객체인데도 임포트되지 않거나 자동 완성 기능이 동작하지 않는 경우가 있다.
- 이런 상황에서는 타입스크립트 서버를 재실행하면 된다.
- VSCode에서는 Restart TS Server 기능을 지원하는데
command + shift + P
를 누르고 실행하면 된다.
- 에디터에서 타입스크립트 서버 재시작하기
타입스크립트 마이그레이션
- 타입스크립트 마이그레이션의 필요성
- 상황에 따라 비즈니스 요구 사항의 변화를 반영할 수 있는 새로운 설계를 기반으로 타입을 작성하는게 효율적일 수 있다.
- 프로젝트 규모와 특성 및 내외부 여건을 종합적으로 고려하여, 기존 프로젝트를 신규 프로젝트로 바꾸는 게 나을지 단순히 마이그레이션하는게 나을지를 신중하게 따져봐야 한다.
- 점진적인 마이그레이션
- 점진적인 마이그레이션은 진입 장벽이 낮아지고 프로젝트의 전반적인 동작을 안정적으로 유지할 수 있게 된다.
- 하지만 단순히 allowJS를 true, noImplicitAny를 false로 설정한 채 무기한으로 마이그레이션으로 미루는 것은 지양해야 한다.
- 따라서 점진적인 타입스크립트 마이그레이션을 진행하기로 했다면, 프로젝트 참여자와 함께 우선순위를 대략적이라도 설정해두는 게 좋다.
- 마이그레이션 진행하기
- 타입스크립트 개발 환경을 설정하고, 빌드 파이프라인에 타입스크립트 컴파일러를 통합한다.
- 타입을 점진적으로 추가하는 과정에서 오류가 발생하지 않도록
tsconfig.json
파일에서 allowJS를 true, noImplicitAny를 false로 설정해야 한다.
- 타입을 점진적으로 추가하는 과정에서 오류가 발생하지 않도록
- 작성된 자바스크립트 파일을 타입스크립트 파일로 변환한다.
- 이 단계에서는 필요한 타입과 인터페이스를 하나씩 정의하며 함수 시그니처를 추가해나간다.
- 기존 자바스크립트 파일을 모두 타입스크립트로 변환하는 작업이 완료되었다면
tsconfig.json
파일에서 allowJS를 false로 변경하고, noImplicityAny를 true로 설정하여 타입이 명시되지 않은 부분이 없는지 점검한다.
- 타입스크립트 개발 환경을 설정하고, 빌드 파이프라인에 타입스크립트 컴파일러를 통합한다.
모노레포
여러 프로젝트를 관리하는 상황에서 공통적인 요소를 통합해 효율적으로 관리할 수 있다.
-
분산된 구조의 문제점
- 만약 프로젝트에 필요한 기능이 다른 프로젝트에 존재한다면 단순한 복사-붙여넣기로 빠르게 구현할 수 있다.
- 이는 개발 시간을 아낄 수 있지만 프로젝트 관리 측면에서 어려움이 생기기도 한다.
- 뒤늦게 새로운 버그가 발견되거나 기능 확장을 위해 해당 기능을 수정해야 할 때
- 특정 라이브러리에 문제가 생기거나 더 이상 사용되지 않는 경우에도
- 이는 개발자가 변경이 필요한 지점을 모두 인지하고 있어야 하기 때문에 개발자 경험(DX)이 저하될 수 있다.
- 장기적으로 프로젝트 관리가 어려워지면서 업무 효율이 갈수록 악화되는 현상이 발생하게 된다.
⇒ 분산된 구조는 생산성을 떨어뜨리기 때문에, 이러한 상황을 벗어나기 위해서는 반복되는 코드를 함수화하여 통합하듯이 프로젝트를 관리할 수 있도록 통합해야 한다.
-
통합할 수 있는 요소 찾기
- 프로젝트에서 공통으로 통합할 수 있는 요소를 찾아야한다.
-
공통 모듈화로 관리하기
- 소스코드를 수정한 다음에 모듈화를 통해 통합할 수 있다.
- 이 과정에서 npm과 같은 패키지 관리자를 활용하여 공통 모듈을 생성하고 관리한다면 각 프로젝트에서 간편하게 모듈과 의존성을 맺고 사용할 수 있게 된다.
- 새로운 프로젝트를 시작하더라도 모듈을 통해 코드를 재사용할 수 있으며, 특정 기능의 변경이 필요할 때는 해당 모듈의 소스코드만 수정하면 되기 때문에 유지보수도 쉬워진다.
- 공통 모듈화의 아쉬운 점
- 공통 모듈에 변경이 발생한다면 해당 모듈을 사용하는 프로젝트에서도 추가 작업이 필요할 수 있다.
- 또한 공통 모듈의 개수가 늘어나면 관리해야 할 레포지토리도 그만큼 늘어난다.
- 새로운 공통 모듈이 필요하다면 개발자는 새로운 레포지토리를 생성하고 개발 환경을 설정하며 패키지 관리자를 사용하여 모듈을 게시해야 한다.
- 새로운 프로젝트를 시작할 때도 빌드를 위한 CI/CD 파이프라인, Lint, 테스트 등도 별도로 설정해야 한다.
-
모노레포의 탄생
-
모노레포(Monorepo) : 버전 관리 시스템에서 여러 프로젝트를 하나의 레포지토리로 통합하여 관리하는 소프트웨어 개발 전략
다른 소프트웨어 개발 전략
-
모놀리식(Monolithic) : 다양한 기능을 가진 프로젝트를 하나의 레포지토리로 관리하는 구조
⇒ 코드 간의 직접적인 의존이 발생 ⇒ 일부 로직만 변경될 때도 전체 프로젝트에 영향
⇒ 설계적인 측면과 빌드 및 배포 등에서 비효율적
-
폴리레포(Polyrepo) : 거대한 프로젝트를 작은 프로젝트의 집합으로 나누어 관리하는 구조
-
-
-
모노레포의 장점
-
Lint, CI/CD 등 개발 환경 설정도 통합하여 관리하기 때문에 불필요한 코드 중복을 줄여준다.
-
별도의 패키지 관리를 통해 모듈을 게시하지 않아도 된다.
← 공통 모듈도 동일한 프로젝트 내에서 관리되므로
⇒ 기능 변화를 쉽게 추적하고 의존성을 관리할 수 있게 된다.
-
-
모노레포의 단점
-
시간이 지나면서 레포지토리가 거대해질 수 있다.
-
하나의 레포지토리에 여러 팀의 이해관계가 얽혀있다면 소유권과 권한 관리가 복잡해질 수 있다.
⇒ 각 프로젝트나 모듈의 소유권을 명확히 정의하고 규칙을 설정하는 과정이 별도로 필요하다.
-
모노레포에 대한 우형 구성원들의 생각
필요성
- 패키지들끼리 연관성이 높아서 한 곳에서 효율적으로 관리할 필요성을 느낌
- 디자인시스템
장점
- 공통적인 코드를 관리한다는 측면에서 모노레포 유용
- 모노레포를 사용하면 패키지 간의 연관 관계가 있을 때 버저닝이 확실하게 보장된다는 장점이 있음
- 공통적인 부분을 새로운 패키지로 분리해야할 때도 빠르게 작업할 수 있어서 좋음
단점
- 모노레포를 여러 부서에서 관리하다보니 깃 로그를 읽고 변경 내역을 빠르게 파악하는 게 어려움. 또한 모노레포로 합쳐져 있지만 배포 주기가 각각 다름
- 하지만 모노레포로 개발하게 되면 모든 패키지를 보게 되어 퍼포먼스가 떨어지는 것 같기도 함