TypeScript 5장 / 6장(43 ~ 47)

이종서·2023년 1월 5일
0

TypeScript

목록 보기
8/9

Item 43. 몽키 패치보다는 안전한 타입을 사용하기

자바스크립트의 가장 유명한 특징 중 하나는, 객체와 클래스에 임의의 속성을 추가할 수 있을 만큼 유연하다는 것입니다.

객체에 속성을 추가할 수 있는 기능은 종종 웹 페이지에서 window나 document에 값을 할당하여 전역 변수를 만드는 데 사용합니다.

document.monkey = 'Tamarin';
	// ~~~~~~~~~ 'Document' 유형에 'monkey' 속성이 없습니다.

이 오류를 해결하는 가장 간단한 방법은 any 단언문을 사용하는 것입니다.

(document as any).monkey = 'Tamarin';

❗️ 그러나 any를 사용함으로써 타입 안전성을 상실하고, 언어 서비스를 사용할 수 없게 되는 단점이 있습니다. (ex. 오타 또는 잘못된 타입)

최선의 해결책은 document 또는 DOM으로부터 데이터를 분리하는 것입니다.
분리를 할 수 없는 경우 두가지 차선책이 존재합니다.

1. interface의 특수 기능 중 하나인 보강을 사용하는 방법.

interface Document {
  /* 몽키 패치의 속(genus) 또는 종(species) */
  monkey: string;
}

document.monkey = 'Tamarin'; // 정상

그리고 모듈의 관점에서 제대로 동작하게 하려면 global 선언을 추가해야 합니다.

export{};
declare global {
  interface Document {
    /* 몽키 패치의 속(genus) 또는 종(species) */
    monkey: string;
  }
}
document.monkey = 'Tamarin'; // 정상

⭐️ 보강을 사용한 방법이 any 보다 나은점

  • 타입이 더 안전합니다. 타입 체커는 오타나 잘못된 타입의 할당을 오류로 표시합니다.
  • 속성에 주석을 붙일 수 있습니다.
  • 속성에 자동완성을 사용할 수 있습니다.
  • 몽키 패치가 어떤 부분에 적용되었는지 정확한 기록이 남습니다.

❗️주의점
모듈 영역과 관련이 있습니다. 보강은 전역적으로 적용되기 때문에, 코드의 다른 부분이나 라이브러리로부터 분리할 수 없습니다. 그리고 애플리케이션이 실행되는 동안 속성을 할당하면 실행 시점에서 보강을 적용할 방법이 없습니다.

2. 더 구체적인 타입 단언문을 사용하는 방법.

interface MonkeyDocument extends Document {
  /* 몽키 패치의 속(genus) 또는 종(species) */
  monkey: string;
}
(document as MonkeyDocument).moneky = 'Macaque';

MonkeyDocument는 Document를 확장하기 때문에 타입 단언문은 정상이며 할당문의 타입은 안전합니다.
또한, Document 타입을 건드리지 않고 별도로 확장하는 새로운 타입을 도입했기 때문에 모듈 영역 문제도 해결할 수 있습니다.

요약

📌 전역 변수나 DOM에 데이터를 저장하지 말고, 데이터를 분리하여 사용해야 합니다.
📌 내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나(보강이나 사용자 정의 인터페이스로 단언)를 사용해야 합니다.

Item 44. 타입 커버리지를 추적하여 타입 안전성 유지하기

noImplicitAny를 설정하고 모든 암시적 any 대신 명시적 타입 구문을 추가해도 any 타입과 관련된 문제들로부터 안전하다고 할 수 없습니다.

any 타입이 여전히 프로그램 내에 존재할 수 있는 두 가지 경우

1. 명시적 any 타입

any 타입의 범위를 좁히고 구체적으로 만들어도 여전히 any 타입입니다. 특히 any[]와 {[key: string]: any} 같은 타입은 인덱스를 생성하면 단순 any가 되고 코드 전반에 영향을 미칩니다.

2. 서드파티 타입 선언

이 경우는 @types 선언 파일로부터 any 타입이 전판되기 때문에 특별히 조심해야 합니다. noImplicitAny를 설정하고 절대 any를 사용하지 않았다 하더라도 여전히 any 타입은 코드 전반에 영향을 미칩니다.

⭐️ any의 개수를 추적하는 방법.

any 타입을 체크 할 수 있고 백분률 점수에 따라 소개한 조언들을 얼마나 잘 따랐는지 확인할 수 있습니다.

$ npx type-coverage
9985 / 10117 98.69%

타입 커버리지 정보를 수집해 보는 것도 유용할 수 있습니다.
type-coverage를 실행 할 때 --detail 플래그를 붙이면, any 타입이 있는 곳을 모두 출력해 줍니다.

$ npx type-coverage --detail
path/to/code.ts:1:10 getColumnInfo
path/to/module.ts:7:1 pt2

요약

📌 noImplicitAny가 설정되어 있어도, 명시적 any 또는 서드파티 타입 선언(@types)을 통해 any 타입은 코드 내에 여전히 존재할 수 있다는 점을 주의해야 합니다.
📌 작성한 프로그램의 타입이 얼마나 잘 선언되었는지 추적해야 합니다. 추적함으로써 any의 사용을 줄여 나갈 수 있고 타입 안전성을 꾸준히 높일 수 있습니다.

Item 45. devDependencies에 typescript와 @types 추가하기

npm(node package manager)은 자바스크스크립트 라이브러리 저장소(npm 레지스트리)와, 프로젝트가 의존하고 있는 라이브러리들의 버전을 지정하는 방법을 제공합니다.

⭐️ npm의 세 가지 종류의 의존성

  1. dependencies
    현재 프로젝트를 실행하는 데 필수적인 라이브러리들이 포함됩니다. 프로젝트의 런타임에 lodash가 사용된다면 dependencies에 포함되어야 합니다.프로젝트를 npm에 공개하여 다른 사용자가 해당 프로젝트를 설치한다면, dependencies에 들어 있는 라이브러리도 함께 설치될 것입니다. 이러한 현상을 전이(transitive) 의존성이라고 합니다.

  2. devDependencies
    현재 프로젝트를 개발하고 테스트하는 데 사용되지만, 런타임에는 필요 없는 라이브러리들이 포함됩니다. 프로젝트를 npm에 공개하여 다른 사용자가 해당 프로젝트를 설치한다면, devDependencies에 포함된 라이브러리들은 제외된다는 것이 dependencies와 다른 점입니다.

  3. peerDependencies
    런타임에 필요하긴 하지만, 의존성을 직접 관리하지 않는 라이브러리들이 포함됩니다. 단적인 예로 플러그인을 들 수 있습니다.

이 세가지 의존성 중에서는 dependencies와 devDependencies가 일반적으로 사용됩니다.
타입스크립트 개발자라면 라이브러리를 추가할 때 어떤 종류의 의존성을 사용해야 하는지 알고 있어야 합니다.
타입스크립트와 관련된 라이브러리는 일반적으로 devDependencies에 속합니다.

⭐️ 타입스크립트 프로젝트에서 고려해야 할 의존성 두 가지

  1. 타입스크립트 자체 의존성을 고려해야 합니다.

    타입스크립트를 시스템 레벨로 설치를 추천하지 않는 이유

    • 팀원들 모두가 항상 동일한 버전을 설치한다는 보장이 없습니다.
    • 프로젝트를 셋업할 때 별도의 단계가 추가됩니다.

따라서 타입스크립를 시스템 레벨로 설치하기보다는 devDependencies에 넣는 것이 좋습니다. devDependencies에 포함되어 있다면, npm install을 실행할 때 팀원들 모두 항상 정확한 버전의 타입스크립트를 설치할 수 있습니다.

커맨드 라인에서 npx를 사용해서 devDependencies를 통해 설치된 타입스크립트 컴파일러를 실행할 수 있습니다.

$ npx tsc
  1. 타입 의존성(@types)을 고려해야 합니다.

사용하려는 라이브러리에 타입 선언이 포함되어 있지 않더라도, DefinitelyTyped(타입스크립트 커뮤니티에서 유지보수하고 있는 자바스크립트 라이브러리의 타입을 정의한 모음)에서 타입 정보를 얻을 수 있습니다. DefinitelyTyped의 타입 정의들은 npm 레지스트리의 @types 스코프에 공개됩니다. 즉, @types/jquery에는 제이쿼리의 타입 정의가 있고, types/lodash에는 로대시의 타입 정의가 있습니다. @types 라이브러리는 타입 정보만 포함하고 있으며 구현체는 포함하지 않습니다.

원본 라이브러리 자체가 dependencies에 있더라도 @types 의존성은 devDependencies에 있어야 합니다.
예를 들어, 리액트 타입 선언과 리액트를 의존성에 추가하려면 다음처럼 실행합니다.

$ npm install react
$ npm install --save-dev @types/react

package.json 파일

{
	"devDependencies": {
    	"@types/react": "^16.8.19",
        "typescript": "^3.5.3"
    },
    "dependencies": {
    	"react": "^16.8.6"
    }
}

요약

📌 타입스크립트를 시스템 레벨로 설치하면 안 됩니다. 타입스크립트를 프로젝트의 devDependencies에 포함시키고 팀원 모두가 동일한 버전을 사용하도록 해야 합니다.
📌 @types 의존성은 dependencies가 아니라 devDependencies에 포함시켜야 합니다. 런타임에 @types가 필요한 경우라면 별도의 작업이 필요할 수 있습니다.

Item 46. 타입 선언과 관련된 세 가지 버전 이해하기

타입스크립트를 사용하면 다음 세 가지 사항을 추가로 고려해야합니다.

  • 라이브러리의 버전
  • 타입 선언(@types)의 버전
  • 타입스크립트의 버전

세 가지 버전 중 하나라도 맞지 않으면, 의존성과 상관 없어 보이는 곳에서 엉뚱한 오류가 발생할 수 있습니다.

실제 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식은 다음 4 가지 문제점이 있습니다.

  1. 라이브러리를 업데이트했지만 실수로 타입 선언은 업데이하지 않은 경우 라이브러리 업데이트와 관련된 새로운 기능을 사용하려 할 때마다 타입 오류가 발생하게 됩니다.

  2. 라이브러리보다 타입 선언의 버전이 최신인 경우입니다. 이런 경우는 타입 정보 없이 라리브러리를 사용해 오다가 타입 선언을 설치하려고 할 때 뒤늦게 발생합니다.

  3. 프로젝트에서 사용하는 타입스크립트 버전보다 라이브러리에서 필요로 하는 타입스크립트 버전이 최신인 경우입니다.

  4. @types 의존성이 중복될 수도 있습니다.

일부 라이브러리, 특히 타입스크립트로 작성된 라이브러리들은 자체적으로 타입 선언을 포함(번들링)하게 됩니다. 자체적인 타입 선언은 보통 package.json의 "types" 필드에서 .d.ts 파일을 가리키도록 되어 있습니다. 라이브러리가 타입스크립트로 작성되고 컴파일러를 통해 타입선언이 생성된 경우라면 버전 불일치 문제를 해결하기는 합니다.

그러나 번들링 방식은 부수적인 4 가지 문제점을 가지고 있습니다.

  1. 번들된 타입 선언에 보강 기법으로 해결 할 수 없는 오류가 있는 경우, 또는 공개 시점에는 잘 동작했지만 타입스크립트 버전이 올라가면서 오류가 발생하는 경우에 문제가 됩니다.

  2. 프로젝트 내의 타입 선언이 다른 라이브러리의 타입 선언에 의존한다면 문제가 됩니다.

  3. 프로젝트의 과거 버전에 있는 타입 선언에 문제가 있는 경우에는 과거 버전으로 돌아가서 패치 업데이트를 해야 합니다.

  4. 타입 선언의 패치 업데이트를 자주 하기 어렵다는 문제가 있습니다.

요약

📌 @types 의존성과 관련된 세 가지 버전이 있습니다. 라이브러리 버전, @types 버전, 타입스크립트 버전입니다.
📌 라이브러리를 업데이트하는 경우, 해당 @types 역시 업데이트해야 합니다.
📌 타입 선언을 라이브러리에 포함하는 것과 DefinitelyTyped에 공개하는 것 사이의 장단점을 이해해야 합니다. 타입스크립트로 작성된 라이브러리라면 타입 선언을 자체적으로 포함하고, 자바스크립트로 작성된 라이브러리라면 타입 선언을 DefinitelyTyped에 공개하는 것이 좋습니다.

Item 47. 공개 API에 등장하는 모든 타입을 익스포트하기

타입스크립트를 사용하다 보면, 언제가는 서드파티의 모듈에 익스포트되지 않은 타입 정보가 필요한 경우가 생깁니다.
함수의 선언에 이미 타입 정보가 있다면 제대로 익스포트되고 있는 것이며, 타입 정보가 없다면 타입을 명시적으로 작성해야 합니다.

만약 어떤 타입을 숨기고 싶어서 익스포트하지 않았다고 가정.

interface SecretName {
  first: string;
  last: string;
}

interface SecretSanta {
  name: SecretName;
  gift: string;
}

export function getGift(name: SecretName, gift: string): SecretSanta {
  // ...
}

해당 라이브러리 사용자는 SecretName 또는 SecretSanta를 직접 임포트할 수 없고, getGift만 임포트 가능합니다. 그러나 타입들은 익스포트된 함수 시그니처에 등장하기 때문에 추출 가능합니다. 방법은 Parameters와 ReturnType 제너릭 타입을 사용하는 것입니다.

type MySanta = ReturnType<typeof getGift>; // SecretSanta
type MyName = Parameters<typeof getGift>[0]; // SecretName

요약

📌 공개 메서드에 등장한 어떤 형태의 타입이든 익스포트합시다. 어차피 라이브러리 사용자가 추출할 수 있으므로, 익스포트하기 쉽게 만드는 것이 좋습니다.

profile
프론트엔드 개발자

0개의 댓글