0131 - 0206

0131

타입 추론 & 타입 단언

타입 추론

  • 타입스크립트가 코드를 해석해 나가는 동작
  • 변수 선언, 초기화, 인자의 기본값, 함수의 반환값 등을 설정할 때 타입을 따로 지정하지 않더라도 타입 추론이 일어난다.

인터페이스와 제네릭을 이용한 타입 추론

interface Dropdown<T> {
  value: T;
  title: string;
}

const shoppingItem: Dropdown<string> = {
  value: 'abc',
  ...
}
  • 제네릭으로 값을 넘겼을 때, 그 값을 추론해서 속성들의 타입을 보장해준다.

복잡한 구조에서의 타입 추론

interface Dropdown<T> {
  value: T;
  title: string;
}

interface DetailedDropdown<K> extends Dropdown<K> {
  description: string;
  tag: K;
}
  • 위와 같이 인터페이스를 확장할 때도 제네릭을 넘길 수 있는데, 이 경우 상위 인터페이스의 value 타입까지 추론할 수 있다.

Best Common Type

타입은 보통 몇 개의 표현식을 바탕으로 가장 근접한 타입을 추론하는데 이 가장 근접한 타입을 Best Common Type이라고 한다.

const arr = [0, "a", true];
  • arr의 타입을 추론하기 위해서는 배열의 각 아이템을 살펴야 한다.
  • 타입은 number, string, boolean으로 구분된다.
  • 이 때 Best Common Type 알고리즘으로 다른 타입들과 가장 잘 호환되는 타입을 선정한다.

문맥상의 타이핑

타입 스크립트에서 타입을 추론하는 또 하나의 방식은 문맥상으로 타입을 결정하는 것이다.
이 문맥상의 타이핑은 코드의 위치를 기준으로 일어난다.

window.onscroll = function (uiEvent) {
  console.log(uiEvent.button); // <- Error!
};

const handler = function (uiEvent) {
  console.log(uiEvent.button); //<- OK
};
  • window.onscroll에 할당되었기 때문에 타입스크립트 검사기는 window.onscroll 타입을 검사하고, 함수의 타입이 UI 이벤트와 연관이 있다고 추론하기 때문에 함수의 인자는 UIEvent로 간주된다.
  • 그러므로 uiEvent.button 속성이 없다고 추론하기에 에러가 발생한다.
  • 하지만 handler라는 임의의 변수에 할당하는 경우 타입을 추정하기 어렵기 때문에 아무 에러가 발생하지 않는다.

Typescript Language Server

자동 완성과 같은 인텔리센스가 일어나기 위해서는 타입스크립트 랭귀지 서버가 돌아야 한다.
vscode에는 타입스크립트를 지원하는 랭귀지 서버가 내부적으로 탑재되어 있다.
이를 통해 타입 추론이 발생한다.

타입 체킹에 있어서 타입스크립트의 지향점은 타입 체크는 값의 형태에 기반하여 이루어져야 한다는 점이다!!

타입 단언

타입스크립트가 타입을 원하는대로 추론하지 못하고, 개발자가 더 명확하게 타입을 지정해야 하는 경우 사용하는 것이 타입 단언이다.

let a;
a = "a";
let b = a as string;
  • 위와 같이 선언과 초기화를 분리하면 타입스크립트는 변수 a의 타입을 any로 추론하게 된다.
  • 하지만 개발자는 a에 문자열을 할당한 것을 명확하게 알고 있기에 as 키워드와 함께 a의 타입을 지정할 수 있다.

활용

타입 단언은 주로 DOM API를 조작할 때 사용된다.

const div = document.querySelector("div") as HTMLDivElement;
if (div) {
  div.innerText;
}
  • 예를 들어 위와 같이 html 문서에서 요소를 찾고 사용하는 경우 결과로 null이 나올 수도 있기 때문에 if 문으로 null 체크를 해줘야 한다.
  • 하지만 위와 같이 이 값이 HTMLDivElement 타입이라고 단언해주면 if 문 없이 작성이 가능하다.

타입 가드 & 타입 호환

타입 가드

interface Developer {
  name: string;
  skill: string;
}

interface Person {
  name: string;
  age: number;
}

function introduce(): Developer | Person {
  return { name: "Tony", age: 33, skill: "Iron Making" };
}

const tony = introduce();
  • 위와 같이 코드를 작성하면 유니온 타입 특성 상 skill과 age 속성에는 접근할 수 없다.
  • 이 때 타입 단언만을 사용해서 문제를 해결하려고 하면 가독성이 많이 떨어진다.
  • 타입 가드라는 기술을 사용해서 위의 문제를 해결할 수 있다.

사용자 정의 타입가드

function isDeveloper(target: Developer | Person): target is Developer {
  return (target as Developer).skill !== undefined;
}

if (isDeveloper(tony)) {
  tony.skill;
} else {
  tony.age;
}
  • 타입 가드는 주로 is로 시작하는 명으로 boolean 값을 반환하는 형식으로 작성한다.
  • 반환값으로는 is 키워드를 사용하여 인자가 지정한 인터페이스와 같은 지 반환한다.
  • isDeveloper의 결과를 만족하면 타입이 Developer로 지정되어 Developer가 포함한 속성을 사용 가능하다.
  • 자주 사용하는 패턴이니 알아두자!

타입 호환

타입 호환이란 코드에서 특정 타입이 다른 타입에 잘 맞는지를 의미한다.

interface Ironman {
  name: string;
}

class Avengers {
  name: string;
}

let i: Ironman;
i = new Avengers(); // OK, because of structural typing

위와 같이 코드를 작성할 경우 정상적으로 동작한다.
그 이유는 자바스크립트는 명시적으로 타입을 지정하는 것보다 코드의 구조 관점에서 타입을 지정하는 것이 더 잘 어울리기 때문이다.

구조적 타이핑

코드 구조 관점에서 타입이 서로 호환되는지의 여부를 판단하는 것

interface Developer {
  name: string;
  skill: string;
}
interface Person {
  name: string;
}

let developer: Developer;
let person: Person;

person = developer;
developer = person; // Error - 'skill' 속성이 'Person' 형식에 없지만 'Developer' 형식에서 필수입니다.
  • person에 developer을 할당했을 때는 person의 경우 name만 선언하면 되기에 문제가 없지만, 반대의 경우에는 developer의 경우 skill 속성을 꼭 선언해야 하기 때문에 문제가 발생한다.
let add = function (a: number) {
  // ...
};

let sum = function (a: number, b: number) {
  // ...
};

sum = add;
add = sum; // Error
  • add에 sum을 할당하는 경우에는 할당하는 함수가 할당받는 함수보다 범위가 더 크기 때문에 에러가 발생한다.
interface Empty<T> {}
let empty1: Empty<string>;
let empty2: Empty<number>;
empty1 = empty2;
empty2 = empty1;

interface NotEmpty<T> {
  data: T;
}
let notempty1: NotEmpty<string>;
let notempty2: NotEmpty<number>;
notempty1 = notempty2; // Error
notempty2 = notempty1; // Error
  • Empty 인터페이스의 경우 제네릭을 선언만 하고 사용하지 않았기 때문에 에러가 발생하지 않지만, NotEmpty 인터페이스의 경우 제네릭을 사용하여 타입이 일치하지 않아 문제가 발생한다.
  • 즉, 제네릭은 제네릭 타입 간 호환 여부를 판단할 때 타입 인자가 속성에 할당 되었는지를 기준으로 한다.

reference
타입스크립트 공식 문서
타입스크립트 핸드북
타입스크립트 입문 - 기초부터 실전까지




0202

유틸리티 타입

  • 이미 정의해 놓은 타입을 변환할 때 사용하기 좋은 타입 문법
  • 유틸리티 타입을 쓰지 않더라도 기존의 인터페이스, 제네릭 등의 기본 문법으로 충분히 타입을 변환할 수 있지만 유틸리티 타입을 쓰면 간결한 문법으로 타입을 정의할 수 있다.

Pick

Pick 타입은 특정 타입에서 몇 개의 속성을 선택하여 타입을 정의할 수 있다.

interface Product {
  id: number;
  name: string;
  price: number;
  brand: string;
  stock: number;
}

// 특정 상품의 상세 정보를 나타내기 위한 함수
type ShoppingItem = Pick<Product, "id" | "name" | "price">;
function displayProductDetail(shoppingItem: ShoppingItem) {}
  • 예를 들어, 위와 같이 정의되어 있는 인터페이스의 속성 중 일부만 사용하고 싶은 경우 새로운 인터페이스를 작성하는 것이 아니라 Pick 유틸리티 타입을 사용하여 코드의 양을 줄일 수 있다.
  • 제네릭의 첫 번째 값으로 속성을 꺼낼 인터페이스를 두 번째 값으로 꺼낼 속성을 유니온 타입으로 설정하여 사용한다.

Omit

Omit 타입은 특정 타입에서 지정된 속성만 제거한 타입을 정의해 준다.

const shirt: Omit<Product, "brand" | "stock"> = {
  id: 1,
  name: "T-shirt",
  price: 500,
};

Partial

Partial 타입은 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있다.

interface UpdateProduct {
  id?: number;
  name?: string;
  price?: number;
  brand?: string;
  stock?: number;
}

type UpdateProduct = Partial<Product>;

function updateProductItem(productItem: UpdateProduct) {}
  • 위에 정의한 인터페이스와 타입은 같은 의미를 가지고 있다.
  • 즉, Partial 유틸리티 타입을 사용하면 모든 속성을 자동으로 옵셔널 처리해 준다.
  • 예를 들어, 속성 중 변경할 속성만 전달해야 하는 경우 유용하게 사용할 수 있다.

유틸리티 타입 구현하기 - Partial

interface UserProfileUpdate {
  username?: string;
  email?: string;
  profilePhotoUrl?: string;
}

type UserProfileUpdate = {
  username?: UserProfile["username"];
  email?: UserProfile["email"];
  profilePhotoUrl?: UserProfile["profilePhotoUrl"];
};
  • 위의 인터페이스와 타입의 결과는 동일하다.
  • 특정 인터페이스를 기준으로 키로 접근하여 타입 값을 얻는 방식으로 변경하였다.
type UserProfileUpdate = {
  [p in keyof UserProfile]?: UserProfile[p];
};
  • in 연산자는 마치 for in 문과 같은 효과를 낸다. 우항에 위치한 값의 갯수만큼 반복하고, 각 요소가 좌항의 임의의 변수에 할당되어 사용한다.
  • keyof 연산자는 UserProfile에 정의되어 있는 키들의 목록을 반환하는 연산자이다.
  • 즉, 특정 인터페이스에 정의된 키들의 목록을 받아와 그 횟수만큼 반복하고, 값으로 Userprofile의 값에 접근하여 옵셔널 속성으로 선언하겠다는 의미이다.
type Partial<T> = {
  [P in keyof T]?: T[P];
};
  • 최종적으로 구현된 Partial 유틸리티 타입이다. vscode에서 실제 구현된 코드를 확인하면 같은 것을 확인할 수 있다.
  • 제네릭으로 타입을 전달받아 해당 인터페이스를 키, 값으로 하여 모든 속성을 옵셔널 속성으로 설정하는 역할을 한다.

맵드 타입

맵드 타입이란 기존에 정의되어 있는 타입을 새로운 타입으로 변환해 주는 문법을 의미한다.
마치 자바스크립트 map() API 함수를 타입에 적용한 것과 같은 효과를 가진다.

기본 문법

{ [ P in K ] : T }

type Heroes = "Hulk" | "Thor" | "Capt";

type HeroProfiles = { [K in Heroes]: number };
const heroInfo: HeroProfiles = {
  Hulk: 54,
  Thor: 1000,
  Capt: 33,
};
  • [K in Heroes] 부분은 자바스크립트의 for in 문법과 유사하게 동작한다.
  • Heroes 타입의 3개의 문자열을 각각 순회하여 number 타입을 값으로 가지는 객체의 키로 정의가 된다.

reference
타입스크립트 공식 문서
타입스크립트 핸드북
실전 프로젝트로 배우는 타입스크립트




0204

국가별 코로나 정보 애플리케이션 실습

지금까지 배운 내용들을 활용하여 나라별로 코로나 정보를 나타내는 애플리케이션 실습을 진행하고자 한다.
이번 실습을 통해 타입스크립트 세팅이나 여러 문제 상황들에서의 해결을 경험해보고, 다음으로는 이전에 내가 자바스크립트로 진행했던 프로젝트에 타입스크립트를 사용하여 변환하는 작업을 해볼 계획이다.

자바스크립트 코드에 타입스크립트를 적용할 때 주의해야 할 점

  1. 기능적인 변경은 절대 하지 않을 것
  2. 처음부터 타입을 엄격하게 적용하지 않을 것 (점진적으로 strict 레벨을 증가)
  3. 테스트 커버리지가 낮을 땐 함부로 타입스크립트를 적용하지 않을 것

타입스크립트 프로젝트 구성 및 기본 설정

  • NPM 초기화
  • 타입스크립트 라이브러리 설치
  • 타입스크립트 설정 파일 생성 및 기본 값 추가
  • 자바스크립트 파일을 타입스크립트 파일로 변환
  • tsc npm script 작성 후 명령어로 타입스크립트 변환하기

NPM 초기화 및 타입스크립트 라이브러리 설치

npm init -y

npm i -D typescript

타입스크립트 설정 파일 생성 및 기본 값 추가

tsconfig.json 파일 생성

{
  "compilerOptions": {
    "allowJs": true,
    "target": "es5",
    "outDir": "./built",
    "moduleResolution": "Node",
    "lib": ["ES2015", "DOM", "DOM.Iterable"]
  },
  "include": ["./src/**/*"]
}
  • target 속성은 컴파일 할 결과물의 버전 설정
  • outDir 속성은 빌드 파일 폴더 설정
  • moduleResolution 속성은 Promise를 인식시켜주기 위한 옵션
  • lib 속성은 Promise, DOM 타입을 인식시켜주기 위한 옵션
  • include 속성은 타입스크립트 컴파일 대상을 설정

tsc npm script 작성 후 명령어로 타입스크립트 변환하기

"scripts": {
  "build": "tsc"
},

타입스크립트 컴파일 동작 특징

  1. 컴파일 시 타입 에러가 나더라도 런타임 에러와는 직접적인 연관이 없을 수도 있다. 즉, 타입 에러와 런타임 에러는 독립적이다.
  2. 모두 확장자명이 ts인 파일이 아니더라도 컴파일을 수행하는데 문제가 없다. 즉, 모든 파일을 ts 파일로 변경할 필요는 없다. (점진적인 변환!)

모듈화를 위한 프로젝트 환경 구성

npm i -D typescript @babel/core @babel/preset-env @babel/preset-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint prettier eslint-config-prettier eslint-plugin-prettier

타입스크립트, 바벨, aslant, prettier와 필요한 플러그인을 개발 의존성으로 설치한다.

eslint 설정 파일

// .eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
  ],
  plugins: ["prettier", "@typescript-eslint"],
  rules: {
    "prettier/prettier": [
      "error",
      {
        singleQuote: true,
        semi: true,
        useTabs: false,
        tabWidth: 2,
        printWidth: 80,
        bracketSpacing: true,
        arrowParens: "avoid",
      },
    ],
  },
  parserOptions: {
    parser: "@typescript-eslint/parser",
  },
};

// .eslintignore
node_modules;

eslint 플러그인 관련 설정

명령어 실행 창에서 settings.json 입력

"editor.codeActionsOnSave": {
  "source.fixAll.eslint": true
},
"eslint.alwaysShowStatus": true,
"eslint.workingDirectories": [
  {
    "mode": "auto"
  }
],
"eslint.validate": ["javascript", "typescript"],

추가적으로 vscode 설정에서 format on save 체크 해제

tslint가 아닌 eslint를 사용하는 이유

eslint가 tslint 보다 좋은 퍼포먼스를 냈기 때문에 eslint를 지원하기로 결정

자바스크립트 프로젝트에 타입스크립트를 적용하는 순서

  1. 타입스크립트 환경 설정 및 ts 파일로 변환
  2. any 타입 선언
  3. any 타입을 더 적절한 타입으로 변경

엄격하지 않은 타입 환경에서 프로젝트 돌려보기

  • 프로젝트에 테스트 코드가 있다면 테스트 코드가 통과하는지 먼저 확인할 것
  • 프로젝트의 js 파일을 ts 파일로 변경할 것
  • 타입스크립트 컴파일 에러가 나는 것 위주로 먼저 에러가 나지 않게 수정
    • 여기서, 기능을 사소하게라도 변경하지 않도록 주의

명시적인 any 선언하기

  • 타입스크립트 설정 파일에 noImplicitAny: true를 추가
  • 가능한 타입을 적용할 수 있는 모든 곳에 타입을 적용
    • 라이브러리를 쓰는 경우 DefinitelyTyped에서 @types 관련 라이브러리를 찾아 설치
    • 타입을 정하기 어려운 곳이 있으면 명시적으로라도 any를 선언

strict 모드 설정하기

tsconfig.json에 strict 설정 추가

{
  "strict": true,
  "strictNullChecks": true,
  "strictFunctionTypes": true,
  "strictBindCallApply": true,
  "strictPropertyInitialization": true,
  "noImplicitThis": true,
  "alwaysStrict": true
}
  • strict: true 옵션은 밑에 있는 모든 옵션을 true로 하는 것과 같다.
  • any로 되어 있는 타입을 최대한 더 적절한 타입으로 변경해야 한다.
  • as와 같은 타입 단언 키워드를 최대한 사용하지 않도록 고민해서 변경한다.

reference
타입스크립트 공식 문서
타입스크립트 핸드북
실전 프로젝트로 배우는 타입스크립트




0205

실습을 진행하며 배운점

화살표 함수에 타입 추가하기

arr.forEach((value: any) => {
  // ...
});

화살표 함수의 경우 인자가 하나이면 괄호가 생략이 가능했으나, 타입을 지정하기 위해서는 괄호를 필수로 붙여줘야 한다.

DOM 함수 관련 오류 해결

const deathsTotal = document.querySelector(".deaths");

function setTotalDeathsByCountry(data: any) {
  deathsTotal.innerText = data[0].Cases;
}
  • document.querySelector로 가져온 값은 가장 기본 DOM 인터페이스인 Element로 타입이 추론된다.
  • 즉 dethsTotal은 Element 타입으로 추론되고, 이 인터페이스는 innerText를 가지고 않기 때문에 에러가 발생한다.
  • 이를 해결하기 위해서는 타입 단언을 사용하여 문제를 해결할 수 있다.
const deathsTotal = $(".deaths") as HTMLParagraphElement;

function setTotalDeathsByCountry(data: any) {
  deathsTotal.innerText = data[0].Cases;
}
  • html 문서에서 태그의 종류를 확인하고, 그에 해당하는 인터페이스 타입으로 단언하면 문제를 해결할 수 있다.
  • 이와 같이 타입 단언은 DOM의 타입을 결정하는 데 자주 사용된다.

외부 라이브러리 모듈화

외부 라이브러리들이 CDN으로 불러와지는 경우, 외부 라이브러리들을 인식할 수 있는 형태로 모듈화 해야 하고 타입스크립트에 대한 정의를 들어가야 한다.

외부 라이브러리는 크게 세 가지 유형으로 나뉜다.

  1. 설치하고 import 하면 타입 문제가 해결되는 경우
    • axios처럼 잘 만들어진 라이브러리의 경우 설치 시, index.d.ts 파일을 제공하여 타입 문제가 발생하지 않는다.
  2. 설치하고 추가로 타입 선언 라이브러리를 설치해야 문제가 해결되는 경우
    • 대부분의 라이브러리들은 추가로 타입 선언 라이브러리를 설치해야 타입스크립트에서 정상적으로 작동한다.
    • 타입 선언 라이브러리들은 @types/ 로 시작하는 명칭을 가지고 있다.
    • 타입 선언 라이브러리를 설치하면 타입스크립트가 node_modules의 @types 폴더에 존재하는 타입 정의를 확인한다.
  3. 타입 선언 라이브러리가 제공되지 않는 경우
    • tsconfig.json 파일에 "typeRoots": ["./node_modules/@types", "./types"]를 추가한다.
    • typeRoots 속성은 타입 정의를 확인할 폴더를 정의한다.
    • 루트에 types 폴더를 생성하고, 그 내부에 타입 정의를 할 라이브러리명의 폴더를 생성하고, 그 내부에 index.d.ts 파일을 생성하여 타입을 정의한다.

Definitely Typed

  • 자바스크립트 라이브러리를 타입스크립트에서 사용하려면 타입 정의가 필요한데, 미리 만들어두어 오픈소스화 한 것
  • @types/로 시작하는 라이브러리

d.ts 파일

  • 타입스크립트 선언 파일 d.ts는 타입 스크립트 코드의 타입 추론을 돕는 파일이다.
  • 예를 들어, 전역 변수로 선언한 변수를 특정 파일에서 import 구문 없이 사용하는 경우 해당 변수를 인식하지 못한다.
  • 그럴 때 변수를 선언해서 에러가 나지 않게 할 수 있다.
  • 외부 라이브러리를 타입 정의를 선언할 때 주로 사용한다.
declare const global = "global";

API 함수 타입 정의

api를 호출하는 함수와 그 함수를 사용하는 함수에서의 타입 정의를 진행하며 과정을 정리하고자 한다.

axis 라이브러리를 사용하여 api를 호출하였기에 axios의 규칙에 따라야만 했다.

function fetchCovidSummary(): Promise<AxiosResponse<CovidSummaryResponse>> {
  const url = "https://api.covid19api.com/summary";
  return axios.get(url);
}
  • axios 호출 결과를 반환하는 함수의 경우 AxiosResponse를 제네릭으로 한 Promise를 반환한다고 선언해야했고, AxiosResponse 또한 실제 결과를 분석한 타입을 제네릭으로 해야 했다.
export interface CovidSummaryResponse {
  Countries: Country[];
  Date: string;
  Global: Global;
  Message: string;
}
  • axios의 호출 결과를 실제 네트워크 패널을 통해 분석하고 응답 결과에 맞게 interface를 정의하였다.
  • 정의한 CovidSummaryResponse를 모듈화하고, 이를 import 해서 사용했다.
  • 만약 응답 결과가 배열 형태라면 interface에 배열은 담을 수 없으므로 type에 interface를 배열로 한 것을 타입으로 정의해야 했다.
function setTotalConfirmedNumber(data: CovidSummaryResponse) {
  confirmedTotal.innerText = data.Countries.reduce(
    (total: number, current: Country) => (total += current.TotalConfirmed),
    0
  ).toString();
}
  • api 호출 결과의 data를 빼내서 다른 함수를 호출하면 타입 추론을 통해 쉽게 파라미터의 타입을 정할 수 있었다.
  • 또한 미리 선언해둔 인터페이스의 속성을 자동 완성 기능으로 쉽게 접근하고, 콜백 함수의 인자 타입 역시 쉽게 추론해서 사용이 가능했다.

null 타입 오류 해결

function initEvents() {
  if (!rankList) return;
  rankList.addEventListener("click", handleListClick);
}

async function handleListClick(event: Event) {
  // ...
}
  • if (!rankList) return; 코드가 없다면 rankList가 null일 수도 있기 때문에 오류가 발생한다.
  • 가장 기본적인 방법은 위와 같이 타입 가드를 이용하여 null을 체크해주는 것이다.
  • 또한 이벤트리스너의 타입의 기본 스펙은 Event로 구현하는 것이므로 MouseEvent에서 Event로 바꾸었다.
  • 변수에 할당되는 코드에서 null 타입 오류가 발생한다면 삼항 연산자로 변경하여 코드의 가독성을 높이는 방법을 고려해 볼 수 있다.
deathsList!.appendChild(li);
  • if 문으로 리턴하는 방법 말고 ! 연산자를 사용하여 타입 단언을 할 수 있다.
  • eslint에서 non-null 경고 표시를 발생시킨다. 이는 null이 아니라고 타입 단언하는 것이 위험할 수 있음을 경고하는 것이다.
  • 타입 단언의 경우 개발자가 임의로 타입을 결정하는 것이기에 의도치 않은 오류가 발생할 수 있으므로 주의해서 사용해야 한다.
    • 예를 들어, 타입 단언으로 변수를 선언하면 인터페이스에 정의한 속성이 존재하지 않아도 초기화가 가능하다.
deathsList?.appendChild(li);
  • 위와 같이 옵셔널 체이닝 연산자를 사용하여 deathsList가 null이나 undefined가 아닌 경우에만 메서드를 실행시킬 수 있다.
  • 가독성도 좋고 타입 단언을 사용하지 않아도 되므로 사용 가능한 코드 라인에서는 잘 사용하면 유용할 것이라는 생각이 들었다.

DOM 유틸 함수

function $<T extends HTMLElement>(selector: string) {
  const element = document.querySelector(selector);
  return element as T;
}

const confirmedTotal = $<HTMLSpanElement>(".confirmed-total");
  • 기존에 document.querySelector()만 사용했던 $ 유틸 함수를 제네릭을 활용하여 타입을 결정하도록 구현했다.
  • 변수 선언 시마다 타입 단언을 사용하지 않아도 되는 코드로 바꾸어 가독성을 높였다.

reference
타입스크립트 공식 문서
타입스크립트 핸드북
실전 프로젝트로 배우는 타입스크립트




0206

FE 자바스크립트 프로젝트에 타입스크립트 적용하기 - feat. kanban

새로운 기술을 학습할 때, 가장 좋은 방법은 기존에 진행했던 프로젝트에 기술을 적용해보거나 기술로 새롭게 프로젝트에 적용해보는 것이라고 생각한다

최근에 타입스크립트에 관심이 생겨 학습을 진행했고, 이에 대한 결과물로 기존에 내가 바닐라 자바스크립트로 진행한 프로젝트에 타입스크립트를 적용해서 변환하는 과정을 글에 녹여내고자 한다 🙂



프로젝트 환경 구성

개인적으로 프로젝트를 진행하면서 어려운 일 중 하나는 환경설정을 세팅하는 것이라고 생각한다

처음에 설정 코드나 라이브러리가 어떤 역할을 하는지 제대로 파악하지 못하고 마구잡이식으로 사용하다 보면 설정이 얽혀서 문제가 발생했을 때 해결하기에 더욱 어려워질 수 있고, 수정이 두려워지기 때문이다

내가 한 환경 구성이 누군가에게 도움이 될 수 있다면 좋을 것 같다 😀



기존 프로젝트 디렉토리

JS 파일은 엔트리 파일이자 컨트롤러 역할을 하는 app.js, 상태를 관리하고 변경 메서드를 제공하는 store.js, 화면에 그리는 역할을 담당하는 view.js, 상수와 그 외 헬퍼 함수를 작성한 utils 폴더로 구분하였다

웹팩은 bundle, devserver 버전으로 나누어 설정 파일을 관리했다



필요한 의존성 설치

package.json

{
  "name": "kanban-board",
  "version": "1.0.0",
  "scripts": {
    "start": "npm run serve -- --open",
    "serve": "webpack serve --node-env development --config webpack/config.server.js",
    "bundle": "webpack --node-env development --config webpack/config.dev.js"
  },
  "devDependencies": {
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "@babel/preset-typescript": "^7.16.7",
    "@typescript-eslint/eslint-plugin": "^5.10.2",
    "@typescript-eslint/parser": "^5.10.2",
    "css-loader": "^6.5.1",
    "eslint": "^8.8.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "prettier": "^2.5.1",
    "style-loader": "^3.3.1",
    "ts-loader": "^9.2.6",
    "typescript": "^4.5.5",
    "webpack": "^5.65.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.7.1"
  }
}
  • 구형 브라우저에서 동작을 위한 babel과 문법 검사와 코드 통일 및 가독성을 위해 eslint와 prettier를 설치하였다
  • 웹팩의 모듈과 devserver를 사용하기 위해 웹팩을 설치하였고, JS 파일에서 CSS 파일을 읽어들이고 해석하기 위해 style-loader와 css-loader를 설치하였다
  • 설치 방법은 package.json 파일에 devDependencies의 내용을 추가하고 npm i 명령어로 설치하는 것이 가장 간단하다



eslint + prettier + typescript 설정

.eslintrc.js

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
  ],
  plugins: ["prettier", "@typescript-eslint"],
  rules: {
    "prettier/prettier": [
      "error",
      {
        singleQuote: true,
        semi: true,
        useTabs: false,
        tabWidth: 2,
        printWidth: 100,
        bracketSpacing: true,
        arrowParens: "avoid",
      },
    ],
  },
  parserOptions: {
    parser: "@typescript-eslint/parser",
  },
};
  • eslint 설정은 기본 eslint와 prettier 설정에 typescript도 eslint로 검사하기 위한 플러그인과 옵션들을 추가하였다



웹팩 설정

webpack/dev.js

/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path");
const __root = process.cwd();

const devConfig = {
  target: ["web"],
  mode: "development",
  devtool: "source-map",
  entry: "./src/js/app.ts",
  output: {
    path: path.resolve(__root, "dist"),
    filename: "js/[name].js",
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
      },
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
};

module.exports = devConfig;
  • entry 속성은 어떤 파일을 entry 진입 파일로 설정할 것인지 지정하는 속성이다. 기존에 js 확장자로 되어있던 app.js 파일명을 app.ts로 변경하고 설정 파일의 값도 수정하였다
  • rules 속성에 ts 확장자명을 가진 파일은 ts-loader로 컴파일 해주겠다는 옵션을 추가하였다
  • resolve 옵션은 import 시, ts 파일도 확장자명을 붙이지 않고 사용하기 위해 추가한 옵션이다



타입스크립트 설정 파일

tsconfig.json

{
  "compilerOptions": {
    "allowJs": true,
    "sourceMap": true,
    "target": "ES5",
    "module": "ES2015",
    "outDir": "./dist",
    "lib": ["ES2015", "DOM", "DOM.Iterable"],
    "noImplicitAny": true,
    "downlevelIteration": true
  }
}
  • allowJs는 JS 파일도 허용하겠다는 속성이다
  • sourceMap을 true로 설정해야 정확한 코드 위치를 매핑하여 console에서 확인할 수 있었다
  • target 속성은 컴파일 할 결과물의 버전을 설정한다
  • lib 속성은 DOM 타입을 인식시켜주기 위해 설정했다
  • noImplicitAny 옵션은 타입스크립트는 타입 추론이 안되는 경우 암묵적으로 any로 타입을 추론하기 때문에 이를 방지하고, 타입 추론이 되지 않는 경우 개발자가 명시적으로라도 any 타입을 지정해줘야 하는 옵션이다
  • downlevelIteration 옵션은 타겟이 ES3이나 ES5여도 스프레드 연산자 등의 문법을 작성하겠다는 옵션




점진적인 적용

타입스크립트를 적용하면서 세운 규칙은 2가지이다

  1. 기능적인 변경은 하지 않을 것
  2. 처음부터 타입을 엄격하게 적용하지 않고, 점진적으로 구체적인 타입을 적용할 것

tsconfig.json파일에 "noImplicitAny": true 옵션을 추가하고, js 확장자명을 가진 파일을 ts 파일로 바꾸니 수많은 에러가 검출되었다

우선 이를 타입스크립트 환경에서도 문제가 발생하지 않도록 하기 위해 any 타입을 활용하여 에러를 해결하였다

ts 파일로 바꾸고 난 후 발생한 수많은 에러들을 any 타입을 이용해서 해결하고, 정상적으로 코드가 동작하는 것을 확인하였다

다음 단계는 any 타입으로 정의한 타입들을 좀 더 구체적으로 변경하는 작업이었다


// types/Issue/index.ts
export interface DragIssue {
  $el: HTMLLIElement;
  index: number;
  boardType: string;
}

export interface Issue {
  id: string;
  regDate: string;
  title: string;
  writer: string;
}

export interface UpdateIssue {
  id: string;
  type: string;
  title: string;
  writer: string;
}

export interface Issues {
  [key: string]: Issue[];
}

export interface IssuesInfo {
  idNumber: number;
  issues: Issues;
}

export interface DragDropInfo {
  dragIndex: number;
  dragBoardType: string;
  dropIndex: number;
  dropBoardType: string;
}

export interface DropInfo {
  $drop: HTMLLIElement;
  $ul: HTMLUListElement;
  $el: HTMLLIElement;
  dragIndexOfTargetList: number;
  dropIndex: number;
}

위의 코드와 같이 객체의 경우 interface로 정의하여 매개변수로 받을 타입을 지정하고, 원시값을 넘겨받는 경우에는 string, number, 유니온 타입을 적절히 사용하여 전체 파일을 타입스크립트 파일로 변경하였다


그 다음 단계로는 tsconfig.json 파일에서 strict 옵션을 true로 변경하였다

{
  "compilerOptions": {
    ...
    "strict": true
  },
}

이렇게 옵션을 설정하면 기존보다 더 엄격하게 타입을 검사하고 예기치 못한 에러를 발생시키지 않도록 검사한다

대부분의 발생한 오류의 경우 DOM이나 타입이 null일 수도 있는 경우 발생하는 타입 에러였다

이를 해결하기 위해서 옵셔널 체이닝 연산자, if 문을 사용한 타입 가드, 타입 단언 등을 사용하여 문제를 해결했다




진행하며 느낀 점

점진적으로 타입스크립트의 타입을 구체화하며 기존에 바닐라 자바스크립트로 동작되던 프로젝트를 타입스크립트 환경에서도 동작하도록 변환하는 데 성공하였다

REST API 요청을 하지 않고, 작은 규모의 프로젝트임에도 불구하고 예상한 것보다 변환하는 데 시간이 소요되었다. 직접 타입스크립트를 공부하고 프로젝트를 진행하기 전까지는 단순히 타이핑을 도와주는 도구라고 생각하여 쉬울 것이라고 생각했었는데, 직접 사용해보니 잘 사용하기 위해서는 추가적인 학습과 시간이 필요할 것 같다는 생각이 들었다

앞으로는 두 가지 방향으로 타입스크립트 학습을 이어나가고자 한다

  1. 타입스크립트를 어떻게 더 잘 활용할 수 있을지 책과 공식 문서를 참고하고, 스터디와 실습을 통해 타입스크립트로 코드를 작성하는 데 익숙해지기
  2. 타입스크립트를 프레임워크와 함께 사용하는 것에 대해 학습하고, 기존에 프레임워크를 사용해 진행했던 프로젝트를 타입스크립트 방식으로 변환하기

태그:

카테고리:

업데이트: