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);
    • 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로 설정한 채 무기한으로 마이그레이션으로 미루는 것은 지양해야 한다.
    • 따라서 점진적인 타입스크립트 마이그레이션을 진행하기로 했다면, 프로젝트 참여자와 함께 우선순위를 대략적이라도 설정해두는 게 좋다.
  • 마이그레이션 진행하기
    1. 타입스크립트 개발 환경을 설정하고, 빌드 파이프라인에 타입스크립트 컴파일러를 통합한다.
      • 타입을 점진적으로 추가하는 과정에서 오류가 발생하지 않도록 tsconfig.json 파일에서 allowJS를 true, noImplicitAny를 false로 설정해야 한다.
    2. 작성된 자바스크립트 파일을 타입스크립트 파일로 변환한다.
      • 이 단계에서는 필요한 타입과 인터페이스를 하나씩 정의하며 함수 시그니처를 추가해나간다.
    3. 기존 자바스크립트 파일을 모두 타입스크립트로 변환하는 작업이 완료되었다면 tsconfig.json 파일에서 allowJS를 false로 변경하고, noImplicityAny를 true로 설정하여 타입이 명시되지 않은 부분이 없는지 점검한다.

모노레포

여러 프로젝트를 관리하는 상황에서 공통적인 요소를 통합해 효율적으로 관리할 수 있다.

  • 분산된 구조의 문제점

    • 만약 프로젝트에 필요한 기능이 다른 프로젝트에 존재한다면 단순한 복사-붙여넣기로 빠르게 구현할 수 있다.
    • 이는 개발 시간을 아낄 수 있지만 프로젝트 관리 측면에서 어려움이 생기기도 한다.
      • 뒤늦게 새로운 버그가 발견되거나 기능 확장을 위해 해당 기능을 수정해야 할 때
      • 특정 라이브러리에 문제가 생기거나 더 이상 사용되지 않는 경우에도
    • 이는 개발자가 변경이 필요한 지점을 모두 인지하고 있어야 하기 때문에 개발자 경험(DX)이 저하될 수 있다.
    • 장기적으로 프로젝트 관리가 어려워지면서 업무 효율이 갈수록 악화되는 현상이 발생하게 된다.

    ⇒ 분산된 구조는 생산성을 떨어뜨리기 때문에, 이러한 상황을 벗어나기 위해서는 반복되는 코드를 함수화하여 통합하듯이 프로젝트를 관리할 수 있도록 통합해야 한다.

  • 통합할 수 있는 요소 찾기

    • 프로젝트에서 공통으로 통합할 수 있는 요소를 찾아야한다.
  • 공통 모듈화로 관리하기

    • 소스코드를 수정한 다음에 모듈화를 통해 통합할 수 있다.
    • 이 과정에서 npm과 같은 패키지 관리자를 활용하여 공통 모듈을 생성하고 관리한다면 각 프로젝트에서 간편하게 모듈과 의존성을 맺고 사용할 수 있게 된다.
    • 새로운 프로젝트를 시작하더라도 모듈을 통해 코드를 재사용할 수 있으며, 특정 기능의 변경이 필요할 때는 해당 모듈의 소스코드만 수정하면 되기 때문에 유지보수도 쉬워진다.
    • 공통 모듈화의 아쉬운 점
      • 공통 모듈에 변경이 발생한다면 해당 모듈을 사용하는 프로젝트에서도 추가 작업이 필요할 수 있다.
      • 또한 공통 모듈의 개수가 늘어나면 관리해야 할 레포지토리도 그만큼 늘어난다.
      • 새로운 공통 모듈이 필요하다면 개발자는 새로운 레포지토리를 생성하고 개발 환경을 설정하며 패키지 관리자를 사용하여 모듈을 게시해야 한다.
      • 새로운 프로젝트를 시작할 때도 빌드를 위한 CI/CD 파이프라인, Lint, 테스트 등도 별도로 설정해야 한다.
  • 모노레포의 탄생

    • 모노레포(Monorepo) : 버전 관리 시스템에서 여러 프로젝트를 하나의 레포지토리로 통합하여 관리하는 소프트웨어 개발 전략

      다른 소프트웨어 개발 전략


      • 모놀리식(Monolithic) : 다양한 기능을 가진 프로젝트를 하나의 레포지토리로 관리하는 구조

        ⇒ 코드 간의 직접적인 의존이 발생 ⇒ 일부 로직만 변경될 때도 전체 프로젝트에 영향

        ⇒ 설계적인 측면과 빌드 및 배포 등에서 비효율적

      • 폴리레포(Polyrepo) : 거대한 프로젝트를 작은 프로젝트의 집합으로 나누어 관리하는 구조

  • 모노레포의 장점

    • Lint, CI/CD 등 개발 환경 설정도 통합하여 관리하기 때문에 불필요한 코드 중복을 줄여준다.

    • 별도의 패키지 관리를 통해 모듈을 게시하지 않아도 된다.

      ← 공통 모듈도 동일한 프로젝트 내에서 관리되므로

      기능 변화를 쉽게 추적하고 의존성을 관리할 수 있게 된다.

  • 모노레포의 단점

    • 시간이 지나면서 레포지토리가 거대해질 수 있다.

    • 하나의 레포지토리에 여러 팀의 이해관계가 얽혀있다면 소유권과 권한 관리가 복잡해질 수 있다.

      ⇒ 각 프로젝트나 모듈의 소유권을 명확히 정의하고 규칙을 설정하는 과정이 별도로 필요하다.


모노레포에 대한 우형 구성원들의 생각

필요성

  • 패키지들끼리 연관성이 높아서 한 곳에서 효율적으로 관리할 필요성을 느낌
  • 디자인시스템

장점

  • 공통적인 코드를 관리한다는 측면에서 모노레포 유용
  • 모노레포를 사용하면 패키지 간의 연관 관계가 있을 때 버저닝이 확실하게 보장된다는 장점이 있음
  • 공통적인 부분을 새로운 패키지로 분리해야할 때도 빠르게 작업할 수 있어서 좋음

단점

  • 모노레포를 여러 부서에서 관리하다보니 깃 로그를 읽고 변경 내역을 빠르게 파악하는 게 어려움. 또한 모노레포로 합쳐져 있지만 배포 주기가 각각 다름
  • 하지만 모노레포로 개발하게 되면 모든 패키지를 보게 되어 퍼포먼스가 떨어지는 것 같기도 함