메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

타입스크립트 초보를 위한 3가지 필수 레시피 - 타입스크립트 설치, 애너테이션, any와 unknown의 차이

한빛미디어

|

2024-07-08

|

by 슈테판 바움가르트너

2,567

의심의 여지가 없이, 타입스크립트가 대세입니다! 

 

하지만 그 인기에도 불구하고 여전히 많은 개발자에게 어려움을 줍니다. 타입 검사기와 싸우고 있다거나, any를 몇 개 던져 문제를 해결한다는 말을 자주 들을 수 있죠.

 

어떤 사람들은 당연히 동작해야 하는 코드임에도 컴파일러를 만족시키는 목적으로 코드를 구현해야 하며 이 때문에 속도가 느려진다고 느낍니다. 하지만 타입스크립트의 유일한 목적은 자바스크립트 개발자의 생산성과 효율성을 높이는 것입니다. 

 

이 도구가 궁극적으로 그 목표를 달성하지 못하는 것일까요? 아니면 개발자로서 우리는 이 도구의 설계 목적과는 다른 것을 기대하는 것일까요?

 

그 해답은 중간 어딘가에 있으며, 바로 이 지점에서 『실무로 통하는 타입스크립트』가 등장합니다.이 책에서는 복잡한 프로젝트 설정부터 고급 타이핑 기법까지 모든 것을 다루는 105가지 레시피를 찾을 수 있습니다. 

 

형식 시스템의 복잡성과 내부 작동 방식은 물론, 자바스크립트의 근간을 해치지 않으려면 고려해야 하는 절충점과 예외에 관해서도 배울 수 있죠. 또한 더 우수하고 강력한 타입스크립트 코드를 작성하기 위한 방법론, 디자인 패턴, 개발 기법도 배우게 될 것입니다.

 

오늘은 그 중에서도 이제 막 타입스크립트를 시작한 초보 개발자에게 유용할 레시피 3개를 공개합니다.

 

 

1. 타입스크립트 설치하기

 

➡️노드Node의 주요 패키지 등록 장소인 NPM에서 타입스크립트를 설치합니다.

 

타입스크립트로 구현된 코드는 자바스크립트로 컴파일한 다음 자바스크립트 런타임인 Node.js를 주요 실행 환경으로 운영합니다. Node.js 앱을 구현하는 상황이 아니더라도 자바스크립트 애플리케이션의 도구는 노드에서 실행됩니다. 따라서 노드 공식 웹사이트에서 Node.js를 내려받아 명령행 도구에 익숙해지도록 노력합시다.


새 프로젝트를 만들려면 프로젝트 폴더를 새로운 package.json으로 초기화해야 합니다. 이 파일은 노드와 노드의 패키지 관리자 NPM이 프로젝트의 콘텐츠를 이해하는 데 필요한 모든 정보를 포함하고 있죠. 다음처럼 NPM 명령행 도구로 프로젝트 폴더에 기본적인 package.json 파일을 만듭니다.

 

$ npm init -y

 

NPM은 노드의 패키지 관리자입니다. NPM은 CLI, 레지스트리, 의존성을 설치하는 데 필요한 다양한 도구를 포함합니다. package.json을 설치했다면 NPM으로 타입스크립트를 설치합니다. 예제에서는 타입스크립트를 개발development 의존성으로 추가하는데, 이는 프로젝트를 NPM의 라이브러리로 배포할 때 타입스크립트를 포함하지 않는다는 의미입니다.

 

$ npm install -D typescript

 

타입스크립트를 전역으로 설치하면 매번 타입스크립트 컴파일러를 설치할 필요가 없습니다. 하지만 프로젝트별로 타입스크립트를 설치하기를 권장합니다. 프로젝트를 얼마나 자주 갱신하느냐에 따라 프로젝트 코드가 사용하는 타입스크립트 버전이 달라질 수 있기 때문입니다. 타입스크립트를 전역으로 설치하거나 업데이트한다면 기존 프로젝트의 코드가 동작하지 않을 수 있습니다.

 

타입스크립트를 설치했고, 새 타입스크립트 프로젝트도 초기화했습니다. NPX는 프로젝트의 상대 경로에 설치된 명령행 유틸리티를 실행하게 해 주는 도구입니다. 다음처럼NPX를 사용합니다.

 

$ npx tsc --init

 

프로젝트의 지역 버전 타입스크립트 컴파일러를 실행하면서 새 tsconfig.json을 만들도록 init 플래그를 전달합니다. tsconfig.json은 타입스크립트 프로젝트의 핵심 설정 파일입니다. 타입스크립트가 코드를 어떻게 해석하고, 어떻게 의존성에 형식을 제공하며, 어떤 기능을 켜고 끌지를 이 파일로 설정합니다. 다음은 타입스크립트가 기본으로 설정하는 옵션입니다.

 

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

 

이 파일을 자세히 살펴볼까요?


target은 es2016으로, 프로젝트의 타입스크립트 파일을 ECMAScript 2016 문법(ECMAScript 배포 연도를 ECMAScript 버전으로 사용함)으로 컴파일합니다. 현재 사용하는
브라우저나 작업 환경에 따라 더 최신 버전이나 es5 같은 예전 버전을 target으로 설정할 수 있습니다. 

 

module은 commonjs입니다. 이 옵션을 이용하면 ECMAScript 모듈 문법을 사용할 수 있죠. 하지만 이 문법을 출력으로 전달하는 방식이 아니라 타입스크립트가 모듈 문법을 CommonJS 형식으로 컴파일합니다. 

 

다음과 같은 코드가 있다고 가정해 봅시다.

 

import { name } from "./my-module";

console.log(name);
//...

 

이 코드를 컴파일하면 다음처럼 바뀝니다.

 

const my_module_1 = require("./my-module");
console.log(my_module_1.name);

 

CommonJS는 Node.js의 모듈 시스템인데 노드가 인기를 끌면서 동시에 유명해졌습니다.  esModuleInterop은 ECMAScript 모듈 이외의 임포트된 모듈들의 표준을 일치시킵니다. forceConsistentCasingInFileNames는 대소문자를 구별하는 파일 시스템 지원하는 옵션입니다. skipLibCheck는 오류가 없는 형식 정의 파일(뒤에서 자세히 설명함)들을 설치했다고 가정합니다. 따라서 컴파일러가 형식 정의 파일을 다시 검사하지 않으므로 컴파일러 동작 속도를 조금 높일 수 있죠.


가장 흥미로운 기능은 엄격strict 모드 입니다. 이 옵션을 true로 설정하면 타입스크립트가 일부 영역에서 조금 다르게 동작합니다. 타입스크립트 팀은 이를 이용해 자신들의 형식 시스템이 어떤 모습이어야 할지를 정의합니다.


형식 시스템의 뷰가 바뀌면서 타입스크립트가 이전 코드와 호환되지 않는 새 기능을 추가하는 상황이라면 엄격 모드로 추가됩니다. 즉, 타입스크립트를 업데이트하면서 엄격 모드를 적용한다면 어느 순간 코드가 동작하지 않을 수 있습니다. 이러한 변화를 적용할 시간이 필요함을 감안해 타입스크립트는 엄격 모드 안에서 기능별로 엄격 모드를 켜거나 끌 수 있도록 허용하고 있습니다.


기본 설정 외에 다음 두 가지 설정을 추가하기를 권장합니다.

 

{
  "compilerOptions": {
    //...
    "rootDir": "./src",
    "outDir": "./dist"
  }
}

 

이 설정은 타입스크립트가 src 폴더에서 소스 파일을 수집하고 컴파일된 파일을 dist 폴더에 저장하도록 지시합니다. 이 설정을 이용하면 구현 코드와 빌드된 파일을 다른 장소에 분리 저장할 수 있습니다. src 폴더는 직접 만들어야 하지만 dist 폴더는 컴파일 시 자동으로 만들어집니다. 

 

이제 설정이 끝났습니다. 프로젝트 설정을 마쳤으므로 src 폴더에 다음 내용을 포함하는 index.ts 파일을 만들어봅시다.

 

 

2. 애너테이션 효과적으로 사용하기

 

➡️귀찮고 지루한 형식 애너테이션 추가하기, 형식 확인이 필요할 때만 형식을 지정하세요.

 

형식 애너테이션으로 어떤 형식을 기대하는지 명시할 수 있다. StringBuilder stringBuilder = new StringBuilder()처럼 장황한 문법을 사용하는 다른 프로그래밍 언어에서는 확실하게 StringBuilder 형식을 처리한다는 사실을 명확히 밝힙니다. 이와 반대로 타입스크립트는 여러분이 사용하려는 형식이 무엇인지 추론하는 기능을 추가 제공합니다.

 

// 형식 추론
let aNumber = 2;
// aNumber: number

// 형식 애너테이션
let anotherNumber: number = 3;
// anotherNumber: number

 

타입스크립트와 자바스크립트 간의 큰 문법적 차이 하나가 바로 형식 애너테이션 지원 여부입니다.

 

타입스크립트를 처음 배울 때는 명시적으로 기대하는 모든 형식을 지정할 수 있습니다. 어떤 사람들은 이를 당연하게 여기기도 합니다. 하지만 따로 형식을 지정하지 않고 타입스크립트가 형식을 추론하도록 하는 방법도 있습니다. 형식 애너테이션으로 계약 사항을 어떻게 확인해야 하는지 표현합니다. 변수 선언에 형식 애너테이션을 추가하면 컴파일러는 변수에 값을 할당할 때 형식에 맞는지 확인합니다.

 

type Person = {
  name: string;
  age: number;
};

const me: Person = createPerson();

 

createPerson이 Person과 호환되지 않는 값을 반환하면 타입스크립트는 오류를 일으킵니다. 올바른 형식을 사용하는지 확인하고 싶다면 이 방법을 사용합니다.

 

이 시점부터 me는 Person 형식이며 타입스크립트는 me를 Person처럼 취급합니다. me에 다른 프로퍼티(예: profession)가 있어도 타입스크립트는 이 프로퍼티에 접근을 불허합니다. 이를 Person에 정의하지 않았기 때문입니다.


함수 시그니처의 반환값에 형식 애너테이션을 추가하면 컴파일러는 반환값이 형식에 맞는지 확인합니다.

 

function createPerson(): Person {
  return { name: "Stefan", age: 39 };
}

 

Person과 일치하지 않는 값을 반환하면 타입스크립트가 오류를 일으킵니다. 반환하는 형식이 정확하게 정해져 있을 때 이 방법을 활용하죠. 특히 다양한 소스를 이용해 큰 객체를 만드는 함수에서 이를 유용하게 사용합니다. 함수의 시그니처 매개변수에 형식 애너테이션을 추가하면 컴파일러가 함수를 호출할 때 인수의 형식을 검사합니다.

 

function printPerson(person: Person) {
  console.log(person.name, person.age);
}

printPerson(me);

 

저는 이것이 반드시 형식 애너테이션을 사용해야 하는 가장 중요한 부분이라 생각합니다. 그 밖의 형식은 추론할 수 있습니다.

 

type Person = {
  name: string;
  age: number;
};

// 추론됨!
// 반환 형식은 { name: string, age: number }

function createPerson() {
  return { name: “Stefan”, age: 39 };
}
  
// 추론됨!
// me: { name: string, age: number}
const me = createPerson();

// 애너테이션 사용! 형식이 호환되는지 검사해야 함
function printPerson(person: Person) {
  console.log(person.name, person.age);
}

// 모두 동작함
printPerson(me);

 

타입스크립트는 구조적 형식 시스템structural type system을 적용하므로 애너테이션을 기대하는 상황에서 추론된 객체 형식을 사용할 수 있습니다. 구조적 형식 시스템에서 컴파일러는 형식의 멤버(프로퍼티)만 따지며 실제 이름은 고려하지 않죠. 더불어 모든 멤버에 대응하는 값의 형식이 서로 일치하면 이를 형식이 호환되는 것으로 간주합니다. ‘형식의 모양shape이나 구조structure가 일치해야 한다’라고 표현하기도 합니다.

 

type Person = {
  name: string;
  age: number;
};

type User = {
  name: string;
  age: number;
  id: number;
};

function printPerson(person: Person) {
  console.log(person.name, person.age);
}

const user: User = {
  name: “Stefan”,
  age: 40,
  id: 815,
};

printPerson(user); // 동작함!

 

User는 Person보다 프로퍼티가 많지만 Person의 모든 프로퍼티를 User가 포함하므로 이 둘에는 같은 형식이 존재합니다. 따라서 User와 Person을 명시적으로 연결하지 않았지만 printPerson에 Person 대신 User 객체를 전달할 수 있습니다. 리터럴을 전달할 때는 추가 프로퍼티가 허용되지 않으므로 컴파일 오류가 발생합니다.

 

printPerson({
 
  name: “Stefan”,
  age: 40,
  id: 1000,
  // ^- 객체 리터럴은 알려진 속성만 지정할 수 있으며
  // ‘Person’ 형식에 ‘id’이(가) 없습니다.ts(2353)
});

 

이는 의도하지 않은 프로퍼티가 형식에 포함되어 예상하지 못한 상황이 일어나는 일을 방지합니다. 구조적 형식 시스템에서는 형식이 추론된 캐리어carrier 변수라는 재미있는 패턴을 만들 수 있으며 해당 변수를 아무 연관성이 없는 소프트웨어의 다른 부분에서 재사용할 수 있습니다.

 

type Person = {
  name: string;
  age: number;
};

type Studying = {
  semester: number;
};

type Student = {
  id: string;
  age: number;
  semester: number;
};

function createPerson() {
  return { name: “Stefan”, age: 39, semester: 25, id: “XPA” };
}

function printPerson(person: Person) {
  console.log(person.name, person.age);
}

function studyForAnotherSemester(student: Studying) {
  student.semester++;
}

function isLongTimeStudent(student: Student) {
  return student.age - student.semester / 2 > 30 && student.semester > 20;
}

const me = createPerson();

// 모두 동작함!
printPerson(me);
studyForAnotherSemester(me);
isLongTimeStudent(me);

 

Student, Person, Studying은 일부 프로퍼티를 공유하지만 서로 직접적인 연관성은 없습니다. createPerson은 이 세 가지 형식과 호환되는 형식을 반환하죠. 애너테이션을 너무 많이 사용하면 더 많은 형식을 만들어야 하고 필요 이상으로 형식을 검사해야 하는 불필요한 오버헤드가 발생할 수 있습니다. 따라서 형식 검사가 필요한 곳(특히 함수 매개변수)에 애너테이션을 추가하면 좋습니다.

 

 

3. any와 unknown 형식 중 무엇을 사용해야 할까요?

 

➡️형식 기능을 끄고 싶은 상황에서는 any를, 주의가 필요할 땐 unknown을 사용합니다.

 

any와 unknown 모두 최상위 형식이므로 모든 값은 any 나 unknow과 호환됩니다.

 

const name: any = "Stefan";
const person: any = { name: "Stefan", age: 40 };
const notAvailable: any = undefined;

 

any는 모든 값과 호환되므로 모든 프로퍼티에 마음대로 접근할 수 있습니다.

 

const name: any = “Stefan”;
// 타입스크립트에서는 괜찮지만 자바스크립트에서는 충돌 발생
console.log(name.profession.experience[0].level);

 

any는 never를 제외한 다른 모든 하위 형식과 호환됩니다. 

즉 any에 새로운 형식을 할당하는 방식으로 가능한 값의 범위를 좁힐 수 있습니다.

 

const me: any = “Stefan”;
// 좋음!
const name: string = me;
// 나쁨. 하지만 형식 시스템상으로는 문제가 없음.
const age: number = me;

 

다만 any는 허용성이 너무 좋아 형식 검사를 무력화하므로 잠재적으로 오류와 문제를 일으킬 수 있습니다. 대다수의 사람은 any를 코드에 사용하지 않아야 한다는 사실에 동의하지만, 다음과 같이 any를 유용하게 활용할 수 있는 상황도 있습니다.

 

• 마이그레이션

자바스크립트에서 타입스크립트로 프로젝트를 마이그레이션하는 상황이라면 자료 구조와 객체의 동작과 관련해 많은 암묵적인 정보를 이미 포함할 것입니다. 이런 상황에서는 마이그레이션 과정에서 발생하는 모든 문제를 한 번에 해결하기가 어렵습니다. 이럴 때 any를 이용해 안전하게 점진적으로 코드를 마이그레이션할 수 있습니다.

 

• 형식이 없는 서드 파티 의존성

하지만 여전히 타입스크립트(또는 이와 유사한 기능)를 사용하지 못하게 방해하는 자바스크립트 의존성이 존재할 수 있습니다. 심지어 최악의 상황에는 최신 형식 정보를 아예 구할 수 없을 수도 있죠. Definitely Typed는 유용한 저장소이지만 자발적으로 유지 보수됩니다. 자바스크립트에 존재하는 기능의 형식을 제공하지만 공식적인 방법은 아닙니다. 따라서 Definitely Typed에는 (리액트 같은 유명한 형식 정의에도) 오류가 있을 수 있으며, 최신 자료가 없을 수도 있습니다!


이런 상황에 any가 도움이 됩니다. 라이브러리가 어떻게 동작하는지 알고, 문서가 잘 작성되었고, 자주 사용하지 않는 상황이라면 정확한 형식을 결정하는 데 구애받지 말고 any를 활용하는 것도 좋은 선택입니다.

 

• 자바스크립트 프로토타이핑

타입스크립트는 자바스크립트와 동작 방식이 다르므로 몇 가지 작업을 해주어야만 다양한 문제를 피할 수 있습니다. 때로는 자바스크립트에서는 문제없이 동작하는 코드라도 타입스크립트에서는 오류가 발생할 수 있습니다.

 

type Person = {
  name: string;
  age: number;
};

function printPerson(person: Person) {
  for (let key in person) {
    console.log(`${key}: ${person[key]}`);
    // ‘string’ 형식은 ‘Person’의 ----^
    // 인덱스 형식으로 사용할 수 없으므로
    // key는 암묵적으로 ‘any’ 형식임.
    // ‘Person’ 형식에는 ‘string’ 형식의 매개변수가
    // 존재하지 않음.(7053)
  }
}

 

여기서 any는 형식 검사를 잠시 중단하고 해결해야 할 일에 집중하는 데 도움을 줍니다. 모든 형식을 any로 변환하거나 any를 다시 아무 형식으로 변환할 수 있으므로 any를 사용한 블록은 안전하지 않은 코드를 포함한다는 사실을 명시적으로 보여주고 있습니다.

 

function printPerson(person: any) {
  for (let key in person) {
    console.log(`${key}: ${person[key]}`);
  }
}

 

이 부분의 코드가 동작한다는 사실을 확인했으면 타입스크립트의 제한과 형식 어서션을 이용해 올바로 형식을 추가할 수 있습니다.

 

function printPerson(person: Person) {
  for (let key in person) {
    console.log(`${key}: ${person[key as keyof Person]}`);
  }
}

 

any를 사용할 때는 tsconfig.json에 noImplicitAny 플래그를 활성화해야 합니다(strict 모드에서는 기본값으로 활성화됨). 추론이나 애너테이션으로 형식을 지정할 수 없는 상황에서는 명시적으로 any로 형식을 지정해야 하죠. 이렇게 하면 나중에 잠재적 오류를 쉽게 찾을 수 있습니다.


any 대신 unknown을 사용할 수 있습니다. any와 unknown으로 같은 값을 가리킬 수 있지만 실제 사용 방법은 서로 다릅니다. any로는 모든 것을 할 수 있지만, unknown으로는 아무것도 할 수 없으며 단지 값을 여기서 저기로 전달할 수 있을 뿐이죠. 함수를 호출하거나 형식을 구체화하려면 먼저 형식을 검사해야 합니다.

 

const me: unknown = “Stefan”;
const name: string = me;
// ^- ‘unknown’ 형식은 ‘string’ 형식에 할당할 수 없습니다.ts(2322)
const age: number = me;
// ^- ‘unknown’ 형식은 ‘number’ 형식에 할당할 수 없습니다.ts(2322)

 

형식 검사와 제어 흐름을 이용해 unknown을 특정 형식으로 구체화할 수 있습니다.

 

function doSomething(value: unknown) {
  if (typeof value === “string”) {
    // value: string
    console.log(“It’s a string”, value.toUpperCase());
    } else if (typeof value === “number”) {
    // value: number
    console.log(“it’s a number”, value * 2);
  }
}

 

다양한 형식을 사용하는 상황에서는 unknown을 적절하게 활용해 문제를 일으키지 않고 코드에서 값을 전달할 수 있습니다. 이는 any와 비슷한 unknown의 허용성 덕분입니다.

 


위 콘텐츠는 『실무로 통하는 타입스크립트』에서 내용을 발췌하여 작성하였습니다.

 

댓글 입력
자료실

최근 본 책0