[Typescript] 우아한 타입스크립트 (with. 리액트) -12장 타입스크립트 프로젝트 관리

rondido·2024년 8월 27일
0

Typescript

목록 보기
14/14

앰비언트 타입 활용하기

앰비언트 타입 선언

타입스크립트 타입 선언은 .ts 또는 .tsx 확장자를 가진 파일에서 할 수 있지만 .d.ts확장자를 가진 파일에서도 선언 가능

앰비언트 타입 선언

.d.ts 확장자를 가진 파일에서는 타입 선언만 할 수 있으며 값을 표현할 수는 없다. 값을 포함하는 일반적인 선언과 구별하기 위해 .d.ts 확장자를 가진 파일에서 하는 타입 선언을 앰비언트 타입

❗앰비언트는 사전적으로 ‘주변의’란 의미를 가짐

앰버언트는 타입 선언으로 값을 정의할 수는 없지만 declare라는 키워드를 사용하여 어딘가에 자바스크립트값이 존재한다는 사실을 선언할 수 있다.

❗declare는 타입스크립트 컴파일러에 어떤 것의 존재 여부를 명시해주는 역할을 한다. 단순히 존재 여부만 알려주기 때문에 컴파일 대상이 아니다.

대표적인 앰비언트 타입 선언 활용 사례

타입스크립트는 기본적으로 .ts와 .js 파일만 이해하는데 이런 상황에서 declare 키워드를 사용하여 아래와 같이 특정 형식을 모듈로 선언하면 타입스크립트 컴파일러에 미리 정보를 제공하여 에러를 방지

declare module "*.png" {
  const src: string;
  export default src;
}

타입스크립트가 알지 못하는 부분을 ‘이러한 것이 존재해’라고 알려줌.

자바스크립트로 작성된 라이브러리

자바스크립트로 작성된 npm 라이브러리가 있다고 가정

자바스크립트로만 구현되어 있기 때문에 타입스크립트에서 이 라이브러리를 사용할 수 있지만 모두 any로 추론 하지만 tsconfig.json파일에서 any를 사용하지 못하게 설정했다면 프로젝트가 빌드되지 않음.

자바스크립트 내부 함수와 변수의 타입을 앰비언트 타입으로 선언하면 타입스크립트는 자동으로 .d.ts 확장자를 가진 파일을 검색하여 타입 검사를 진행하게 되므로 문제없이 컴파일된다.

즉, 앰비언트 타입 선언은 타입스크립트에게 ‘자바스크립트 코드 안에는 이러한 정보들이 있어’라고 알려주는 도구

타입스크립트로 작성된 라이브러리

타입스크립트로 작성된 라이브러리일지라도 자바스크립트 파일과 .d.ts 파일로 배포되는 것이 일반적

타입스크립트 파일을 직접 배포하여 라이브러리 사용자가 타입스크립트를 컴파일할 때 라이브러리 코드도 함께 컴파일할 수 있다. 그러나 자바스크립트 파일과 .d.ts파일로 배포하면 라이브러리 코드를 따로 컴파일하지 않아도 되기 때문에 컴파일 시간이 크게 줄어든다. .d.ts 파일이 있기 때문에 사용자는 .d.ts 파일에 정의된 타입 정보를 활용하여 라이브러리를 사용할 수 있다. 또한 tsconfig.json 파일의 declarantion을 true로 설정하면 타입스크립트 컴파일러는 .d.ts 파일을 자동으로 생성

자바스크립트 어딘가에 전역 변수가 정의되어 있음을 타입스크립트에 알릴 때

타입스크립트로 직접 구현하지 않았지만 실제 자바스크립트 어딘가에 전역 변수가 정의되어 있는 상황을 타입스크립트에 알릴 때 앰비언트 타입 선언을 사용

declare module global {
  interface Window {
    deviceId: string | undefined;
    appVersion: string;
  }
}

앰비언트 타입 선언 시 주의점

타입스크립트로 만드는 라이브러리에는 불필요

tsconfing.json의 declaration을 true로 설정하면 타입스크립트 컴파일러가 .d.ts 파일을 자동으로 생성해주기 때문에 수동으로 .d.ts 파일을 작성할 필요가 없다.

전역으로 타입을 정의하여 사용할 때 주의해야 할 점

서로 다른 라이브러리에서 동일한 이름의 앰비언트 타입 선언을 한다면 충돌이 발생하여 어떤 타입 선언이 적용될지 알기 어려우며, 의도한 대로 동작하지 않을 수 있다. 또한 앰비언트 타입 선언은 명시적인 임포트나 익스포트가 없기 때문에 코드의 의존성 관계가 명확하지 않아 나중에 변경할 때 어려움을 겪을 수 있음.

앰비언트 타입 선언을 잘못 사용했을 때의 문제점

앰비언트 변수 선언은 어느 곳에나 영향을 줄 수 있기 때문에 일반 타입 선언과 섞이게 되면 앰비언트 선언이 어떤 파일에 포함되어 있는지 파악하기 어렵다.

작은 컴포넌트에 앰비언트 변수 선언이 포함되어 있다면 모든 파일의 타입에 영향을 주기 때문에 어떤 파일에서 앰비언트 타입이 선언되었는지 찾기 어려워진다.

.d.ts 확장자 파일 내에서 앰비언트 타입 선언을 하는 것은 일종의 개발자 간의 약속.

타입 선언 위치가 명확해야 가독성이 높아지고 유지보수도 편하게 할 수 있다.

앰비언트 타입 활용하기

타입스크립트 컴파일러에 타입 정보를 알려주는 declare 키워드를 더 효과적으로 활용할 수 있다.

타입을 정의하여 임포트 없이 전역으로 공유

.d.ts 파일에서의 앰비언트 타입 선언은 전역 변수와 같은 역할을 한다. 따라서 앰비언트 타입을 선언하면 모든 코드 내에서 임포트 하지 않고 사용할 수 있다. 가령 유용한 유틸리티 타입을 작성했다고 가정. 이렇게 앰비언트 타입으로 유틸리티 타입을 선언하면 모든 코드에서 임포트하지 않아도 해당 타입을 사용할 수 있다.

type Optional<T extends object, K extends keyof T = keyof T> = Omit<T, K> &
  Partial<Pick<T, K>>;

type Props = { name: string; age: number; visible: boolean };
type OptionalProps = Optional<Props>;

declare type 활용하기

보편적으로 많이 사용하는 커스텀 유틸리티 타입을 declare type으로 선언하여 전역에서 사용할 수 있다. 아래 예시처럼 Nullable 타입을 선언해서 어디에서든 쉽게 사용

declare type Nullable<T> = T | null;
const name: Nullable<string> = "woowa";

declare module 활용하기

theme의 인터페이스 타입을 확장하여 theme 타입이 자동으로 완성되도록 하는 기능이 추가

css-in-js 라이브러리는 기존의 폰트 크기, 색상 등을 객체로 관리한다. 이렇게 정의된 theme에서 스타일 값을 가져와 기존 인터페이스 타입과 통합하여 theme 타입이 자동으로 완성되는 기능을 지원

const fontSizes = {
  xl: "30px",
  //...
};

const colors = {
  gray_100: "#22222",
  gray_200: "#444444",
};

const depths = {
  origin: 0,
  foreground: 10,
  dialog: 100,
};

const theme = {
  fontSizes,
  colors,
  depths,
};

declare module "styled-components" {
  type Theme = typeof theme;
  export interface DefaultThme extends Theme {}
}

이외에도 로컬 이미지나 SVG같이 외부로 노출되어 있지 않은 파일을 모듈로 인식하여 사용할 수있게끔 만들 수 있음.

declare module "*.gif" {
  const src: string;
  export default src;
}

declare namespace 활용하기

Node.js 환경에서 .env 파일을 사용할 떄, declare namespace를 활용하여 process.env로 설정값을 손쉽게 불러오고 환경변수의 자동 완성 기능을 쓸 수 있다

declare namespace NodeJS {
  interface ProcessEnv {
    readonly API_URL: string;
    readonly API_INTERNAL_URL: string;
  }
}

아래와 같은 함수 예시를 통해 타입을 보강한 경우와 그렇지 않은 경우를 비교하여 개념을 이해할 수 있다.

function log(str: string) {
  console.log(str);
}

//.env
API_URL = "localhost:8080";
log(process.env.API_URL as string);

napcespace를 활용하여 process.env 타입을 보강해주지 않는 경우

//.env

API_URL = "localhost:8080";

declare namespace NodeJS {
  interface ProcessEnv {
    readonly API_URL: string;
  }
}

log(process.env.API_URL);

napcespace를 활용하여 process.env 타입을 보강한 경우

declare global 활용하기

declare global 키워드는 전역 변수를 선언할 때 사용.

declare global {
  interface Window {
    newProprty: string;
  }
}

declare와 번들러의 시너지

declare global로 전역 변수를 선언하는 과정과 번들러를 통해 데이터를 주입하는 절차를 함께 활용하면 시너지를 낼 수 있다.

const color = {
  white: "#ffffff",
  black: "#0000000",
} as const;

type ColorSet = typeof color;
declare gloabl{
  const _color:ColorSet
}

앞과 같이 전역에 _color라는 변수가 존재함을 타입스크립트 컴파일러에 알리면 해당 객체를 활용할 수 있다.

const white = _color["white"];

하지만 아직 ColorSet 타입을 가지고 있는 _color 객체의 실제 데이터가 존재하지 않는다.

다시 말해 앞의 코드는 타입스크립트 에러를 발생시키지 않지만, 코드가 실행될 경우에는 실제 데이터가 없기 때문에 기대하는 동작과 다를 수 있다.

이러한 문제를 해결하기 위한 방법 중 하나가 번들 시점에 번들러를 통해서 해당 데이터를 주입

롤업 번들러 설정에서 inject 모듈을 사용하여 _color에 해당하는 데이터를 삽입

💡 inject 모듈 inject는 임포트문의 경로를 분석하여 데이터를 가져옴 import {color} form “./data”; ./data 경로에서 color를 가져오는 경우 [’./data’, ‘color’]로 지정하여 어떤 데이터 값을 가져올지 명시할 수 있다.

스크립트와 설정 파일 활용하기

스크립트 활용하기

실시간으로 타입을 검사하자

일반적으로 타입스크립트 프로젝트에서는 에디터 가능한 한 빠르게 타입 에러를 감지해준다.

그러나 컴퓨터 성능이 떨어지거나 프로젝트의 규모가 커지면 에디터가 타입 에러를 알려주는 속도느가 느려진다. 때로는 검사하려는 특정 파일을 열어야만 타입 에러가 나타나기도 하며, 에디터에서 분명히 에러가 없다고 확인하고 나서 커밋했는데 뒤늦게 깃훅 도구인 husky에 의해 타입 에러가 발생

yarn tsc -noEmit -incremental -w

이 스크립트는 프로젝트의 tsc(타입스크립트 컴파일러)를 실행

  • noEmit 옵션은 자바스크립트로 된 출력 파일을 생성하지 않도록 설정하는 것
  • incremental 옵션은 증분 컴파일을 활성화하여 컴파일 시간을 단축할 수 있게 해준다.
  • w는 파일 변경 사항을 모니터링 한다는 의미
💡 증분 컴파일 매번 모든 대상을 컴파일하는 것이 아니라 변경 사항이 있는 부분만을 컴파는 것을 말하며, 이를 활용하면 컴파일 시간을 줄일 수 있다.

타임 커버리지 확인하기

타입스크립트를 사용하면서 any 타입을 이곳저곳에서 남발하면 타입스크립트의 장점을 활용하지 못할 수 있다. 따라서 현재 프로젝트에서 얼마나 타입스크립트를 적절하게 쓰고 있는지 확인할 필요가 있다. 프로젝트의 모든 부분이 타입스크립트 통제하에 돌아가고 있는지를 정량적으로 판단하기 위해 다음과 같은 스크립트를 사용

npx type -coverage --detail

타입스크립트로 마이그레이션 중인 프로젝트나 레거시 코드가 많은 프로젝트를 다룰때 타입 커버지를 체크함으로써 더 나은 코드 퀄리티로 리팩토링하기 위한 기반을 마려하는 데 도움이 되는 정량적인 지표

설정 파일 활용하기

타입스크립트 컴파일 속도 높이기

tsconfig의 incremental 속성을 활용하여 타입스크립트의 컴파일 속도를 높일 수 있다.

incremental 속성을 true로 설정하면 증분 컴파일이 활성화되어 매번 모든 대상을 컴파일 하는 것이 아니라 변경된 부분만 컴파일하게 된다. 이로써 매번 모든 대상을 컴파일하지 않아도 되므로 컴파일타임을 줄일수 있다. 이 설정은 tsconfig 파일에 추가하거나 스크립트에서 사용할 수 있다.

//tsconfing에 추가
{
	"compilerOptions":{
		..
		incremental:true
	}
}
  • 스크립트 활용
yarn tsc --noEmit --incremental --diagnostic

에디터 활용하기

에디터에서 타입스크립트 서버 재시작하기

VSCode나 WebStorm과 같은 자바스크립트 IDE에서 프로그래밍하다 보면 때로는 정의된 타입이 있는 객체인데도 임포트되지 않거나 자동 완성 기능이 동작하지 않는 경우를 종종 볼 수 있다.

이런 상황에서 타입스크립트 서버를 재실행하면 된다. vscode에서는 Restart ts server 기능을 지원하는데 커맨드 + 쉬프트 + p(윈도우: 컨트롤 + 쉬프트 + p)를 누르고 실행

타입스크립트 마이그레이션

타입스크립트 마이그레이션 필요성

타입스크립트를 새로운 기술 스택으로 도입하는 결정을 했더라도 반드시 기존 프로젝트를 타입스크립트로 마이그레션해야만 하는 것은 아니다. 우형에서는 타입스크립트로 프로젝트를 새로 구축 한세라가 기존 자바스크립트 프로젝트를 타입스크립트로 마이그레션한 사례보다 많다. 아마도 새로운 설계를 바탕으로 타입을 작성하는 게 더 효율적이기 때문.

빠르게 변화하는 비즈니스 환경에서는 기존 코드의 구조적인 한계가 드러남.

따라서 상황에 따라 비즈니스 요구 사항의 변화를 반영할 수 있는 새로운 설계를 기반으로 타입을 작성하는 게 효율적. 프로젝트의 규모와 특성 및 내외부 여건을 종합적으로 고려하여, 기존 프로젝트를 신규 프로젝트로 바꾸는 게 나을지 단순히 마이그레이션 하는게 나을지 신중하게 따져봐야 함.

점진적인 마이그레이션

하지만 단순히 allowJS를 true, noImplicitAny를 false로 설정한 채 무기한으로 마이그레이션을 미루는 것을 지양해야 한다. 데드라인이 없는 프로젝트는 언제 완료될지 모르는 과도기에 빠질 수 있다.따라서 점진적인 마이그레이션을 진행하기로 했다면, 프로젝트 참여자와 함께 우선순위를 정해두는 것이 좋다.

마이그레이션 진행하기

타입스크립트 마이그레션을 진행하기로 했다면 다음과 같은 단계를 거치게 됨.

  1. 타입스크립트 개발 환경을 설정하고, 빌드 파이프라인에 타입스크립트 컴파일을 통합. tsconfig.json 파일에서 allowJS를 true로 noImplicitAny를 false로 설정. allowJS는 자바스크립트 파일을 컴파일할 때 사용하는 옵션으로 기존 자바스크립트 함수를 타입스크립트에서 임포트하거나 반대로 타입스크립트 함수를 자바스크립트에서 임포트할 수 있게 해준다. 또한noImplicitAny는 암시적 any 타입이 있을 때 오류가 발생하게 하는 옵션 따라서 타입을 점진적을 추가하는 과정에서는 오류가 발생하지 않도록 noImplicitAny를 false로 설정
  2. 작성된 자바스크립트 파일을 타입스크립트 파일로 변환. 이 단계에서는 필요한 타입과 인터페이스를 하나씩 정의하며 함수 시그니처를 추가해나간다.
  3. 기존 자바스크립트 파일을 모두 타입스크립트로 변환하는 작업이 완료되었다면 tsconfig.json파일에서 allowJS를 false로 변경하고 noImplicitAny를 true로 설정하여 타입이 명시되지 않는 부분이 없는지 점검

모노레포

분산 구조의 문제점

image.png

개발자는 각각의 레포지토리에서 해당 프로젝트를 위한 jest,바벨,eslint, 타입스크립트 등의 설정 파일을 별도 구성하고 빌드 파이프라인, 공통적인 컴포넌트 그리고 해당 프로젝트에 필요한 소스코드를 독립적으로 관리 만약 프로젝트에 필요한 기능이 다른 프로젝트에 존재한다면 해당 기능을 복사하여 붙여넣기 함으로써 빠르게 구현

이런 과정을 통해 개발 시간을 아낄 수 잇지만 프로젝트 관리 측면에서 어려움이 생기기도 한다.

여러 프로젝트에 동일한 코드를 복사하여 붙여 넣은 후에 뒤늦게 새로운 버그가 발견되거나 기능 확장을 위해 해당 기능을 수정해야 할 때 프로젝트의 개수만큼 반복적인 수정 작업을 해야 한다. 특정 라이브러리에 문제가 생기거나 더 이상 사용되지 않는 경우에도 마찬가지로 모든 프로젝트에서 일일이 대응해야 한다

통합할 수 있는 요소 찾기

모든 요소를 통합할 수 있다면 이상적이겠지만, 완전히 동일한 프로젝트가 아니라면 프로젝트 마다 다른 요소가 존재하기 마련 따라서 먼저 프로젝트내에서 공통으로 통합할 수 있는 요소를 찾아야함.

image.png

공통 모듈화로 관리하기

소스코드를 수정한 다음에 모듈화를 통해 통합할 수 있다.이 과정에서 npm과 같은 패키지 관리자를 활용하여 공동 모듈을 생성하고 관리한다면 각 프로젝트에서 간편하게 모듈과 의존성을 맺고 사용할 수 있게 된다. 새로운 프로젝트를 시작하더라도 모듈을 통해 코드를 재사용할 수 있으며, 특정 기능의 변경이 필요할 떄는 해당 모듈의 소스코드만 수정하면 되기 때문에 유지보수도 쉬워짐

새로운 공동 모듈이 필요한다면 개발자는 새로운 레포지토리를 생성하고 개발 환경을 설정하며 패키지 관리자를 사용하여 모듈을 게시해야 함. 새로운 프로젝트를 시작할 때도 빌드를 위한 CI/CD 파이프라인, Lint, 테스트 등도 별도로 설정 해야 함.

모노레포의 탄생

모노레포

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

모놀리식 구조는 코드 간의 직접적인 의존이 발생하여 일부 로직만 변경될 때도 전체 프로젝트에 영향을 줄 수 있다. 이에 따라 설계적인 측면과 빌드 및 배포 등에서 효율적이지 못함.

당연하게도 효율적인 구조에 대한 수요가 생겼고 거대한 프로젝트를 작은 프로젝트의 집합으로 나누어 관리하는 폴리레포 방식과 하나의 레포지토리로 모든 것을 관리하는 모노레포 방식이 등장하게 되었다. 최근에는 작은 프로젝트에서도 번들러,테스트,Lint, CI/CD 스크립트나 도커 등 다양한 설정을 적용

모노레포로 관리했을 때의 장점

모노레포는 여러 프로젝트를 하나의 레포지토리로 통합하여 관리하며, 프로젝트마다 개별적인 레포지토리를 만드는 방식과는 달리 Lint,Ci/CD 등 개발 환경 설정도 통합적으로 관리하기 때문에 불필요한 코드 중복을 줄여준다.

개별적으로 프로젝트를 형성하는 폴리레포와는 다르게 공통 모듈로 동일한 프로젝트 내에서 관리되므로 별도의 패키지 관리자를 통해 모듈을 게시하지 않아도 된다. 이에 따라 기능 변화를 쉽게 추적하고 의존성을 관리할 수 있게 됨.

모노레포로 관리했을 때의 단점

모노레포로 프로젝트를 관리할 때 시간이 지나면서 레포지토리가 거대해질 수 있다. 그리고 하나의 레포지토리에 여러 팀의 이해관계가 얽혀있다면 소유권과 권한 관리가 복잡해질 수 있다. 따라서 각 프로젝트나 모듈의 소유권을 명확히 정의하고 규칙을 설정해야 하는 과정이 별도로 필요

profile
풋살을 좋아하는 프론트엔드 개발자

0개의 댓글