Search
Duplicate
🔉

7장. 코드를 작성하고 실행하기

item 53. 타입스크립트 기능보다는 ECMAScript 기능 사용하기

타입스크립트가 태동하던 시기의 자바스크립트는 결함이 많고 개선할 부분이 많은 언어였으며 클래스, 데코레이터, 모듈 시스템같은 기능을 지원하기 위해 프레임워크나 트랜스파일러로 보완하는 것이 일반적이었다.
따라서 타입스크립트 초기 버전도 이런 기능들을 지원하였고 점차 자바스크립트가 개선되어 이런 기능들을 지원하자. 기존 타입스크립트 기능들과 호환성 문제를 일으켰다.
대부분의 타입스크립트 진영은 이를 해결하기 위해 자바스크립트의 신규 기능을 그대로 채택하고 타입스크립트 초기 버전과 호환성을 포기하는 전략을 선택했다.
이 전략이 세워지기 전, 이미 사용되고 있던 몇 기능이 존재하는데, 이 기능들은 타입 공간과 값 공간의 경계를 혼란스럽게 만들기 때문에 사용하지 않는 것이 좋다.
열거형
enum Flavor { VANILLA = 0, CHOCOLATE = 1, STRAWBERRY = 2, } let flavor = Flavor.CHOCOLATE; // Type is Flavor Flavor // Autocomplete shows: VANILLA, CHOCOLATE, STRAWBERRY Flavor[0] // Value is "VANILLA"
TypeScript
복사
단순히 값을 나열하는 것보다 실수가 적고 명확하기 때문에 일반적으로는 열거형을 사용하는 것이 좋다.
그러나 타입스크립트에는 문제가 존재하는데, 타입스크립트의 열거형은 다음 목록처럼 상황에 따라 다르게 동작한다.
숫자 열거형에 0, 1, 2 외의 다른 숫자가 할당되면 매우 위험하다.
상수 열거형은 보통의 열거형과 달리 런타임에 완전히 제거된다.
preserverConstEnums 플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다.
문자열 열거형은 런타임의 타입 안전성과 투명성을 제공한다. 그러나 타입스크립트의 다른 타입과 달리 명목적 타이핑을 사용한다.
enum Flavor { VANILLA = 'vanilla', CHOCOLATE = 'chocolate', STRAWBERRY = 'strawberry', } let flavor = Flavor.CHOCOLATE; // Type is Flavor flavor = 'strawberry'; // ~~~~~~ Type '"strawberry"' is not assignable to type 'Flavor'
TypeScript
복사
이처럼 자바스크립트와 타입스크립트의 동작이 일관되지 않기 때문에 문자열 열거형 대신 리터럴 타입의 유니온을 사용하는 것이 좋다.
type Flavor = 'vanilla' | 'chocolate' | 'strawberry'; let flavor: Flavor = 'chocolate'; // OK flavor = 'mint chip'; // ~~~~~~ Type '"mint chip"' is not assignable to type 'Flavor'
TypeScript
복사
매개변수 속성
일반적으로 클래스를 초기화할 때 속성을 할당하기 위해 생성자의 매개변수를 사용한다.
class Person { name: string; constructor(name: string) { this.name = name; } }
TypeScript
복사
타입스크립트는 이에 대해 다음처럼 더 간결한 문법을 제공한다.
class Person { constructor(public name: string) {} }
TypeScript
복사
위 예제의 public name을 매개변수 속성이라고 부르며 멤버 변수로 name을 선언한 이전 에제와 동일하게 동작한다. 이런 매개변수 속성은 다음과 같은 문제점을 가진다.
매개변수 속성이 런타임에는 실제로 사용되지만 타입 스크립트 관점에서는 사용되지 않는 것처럼 보인다.
매개변수 속성과 일반 속성을 섞어서 기재하면 클래스의 설계가 혼란스러워진다.
찬반이 존재하는 주제이긴 하나 가급적이면 일반 속성과 둘 중 하나를 선택해 일관성있게 사용하는 것이 좋다.
네임스페이스와 트리플 슬래스 임포트
각 환경마다 자신만의 방식으로 모듈 시스템을 마련했는데, Node.js는 requiremodule.exports를 사용한 반면 AMD는 define 함수와 콜백을 사용했다.
타입스크립트 역시 자체적으로 모듈 시스템을 구축했고 module 키워드와 트리플 슬래시 임포트를 사용했다.
ECMAScript 2015가 공식적으로 모듈 시스템을 도입하고서는 충돌을 피하기 위해 module과 같은 기능을 하는 namespace 키워드를 추가했다.
namespace foo { function bar() {} } /// <reference path="other.ts"/> foo.bar();
TypeScript
복사
트리플 슬래시 임포트와 module 키워드는 호환성을 위해 남아있을 뿐이니 가급적이면 ECMAScript 2015 스타일의 모듈을 사용하는 것이 좋다.
데코레이터
데코레이터는 클래스, 메소드, 속성에 어노테이션을 붙이거나 기능을 추가하는 데 사용할 수 있다.
예를 들어 클래스의 메소드가 호출될 때마다 로그를 남기려면 다음과 같이 logged 어노테이션을 정의하여 사용할 수 있다.
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } @logged greet() { return "Hello, " + this.greeting; } } function logged(target: any, name: string) { const fn = target[name]; target[name] = function() { console.log(`Calling ${name}`); return fn.apply(this, arguments); }; return target; } console.log(new Greeter('Dave').greet()); // Logs: // Calling greet // Hello, Dave
TypeScript
복사
데코레이터는 앵귤러 프레임워크를 지원하기 위해 추가되었으며 tsconfig.jsonexperimentalDecorators 속성을 설정하고 사용해야 한다.
현재까지도 표준화가 완료되지 않았으므로 사용 중인 데코레이터가 비표준으로 바귀거나 호환성이 깨질 가능성이 있다.
요약
일반적으로 타입스크립트 코드에서 모든 타입 정보를 제거하면 자바스크립트가 되지만 열거형, 매개변수 속성, 모듈 시스템, 데코레이터는 타입 정보를 제거한다고 자바스크립트가 되진 않는다.
타입스크립트의 역할을 명확하게 하려면 열거형, 매개변수 속성, 모듈 시스템, 데코레이터는 사용하지 않는 것이 좋다.

item 54. 객체를 순회하는 노하우

다음 코드는 정상적으로 실행되지만 편집기에서는 오류가 발생하는데, 원인이 무엇일까?
const obj = { one: 'uno', two: 'dos', three: 'tres', }; for (const k in obj) { const v = obj[k]; // ~~~~~~ Element implicitly has an 'any' type // because type ... has no index signature }
TypeScript
복사
상수 k의 타입이 string으로 추론되는 반면, obj의 키는 one, two, three 세 개의 키만 존재하므로 kobj 객체의 키 타입이 서로 다르게 추론되어 발생하는 오류다.
해당 오류는 다음과 같이 k의 타입을 구체적으로 유니온 타입을 이용해 명시해주면 해결된다.
let k: keyof typeof obj; // Type is "one" | "two" | "three" for (k in obj) { const v = obj[k]; // OK }
TypeScript
복사
그렇다면 k 타입이 “one” | “two” | “three”가 아닌 string으로 추론된 이유는 무엇일까?
interface ABC { a: string; b: string; c: number; } function foo(abc: ABC) { for (const k in abc) { // const k: string const v = abc[k]; // ~~~~~~ Element implicitly has an 'any' type // because type 'ABC' has no index signature } }
TypeScript
복사
앞선 예제와 같은 오류이므로 let k: keyof ABC 같은 선언을 통해 오류를 제거할 수 있다.
foo 함수는 ABC 타입에 할당 가능한 어떠한 값이든 매개변수로 허용하기 때문에 a, b, c 속성 외에 d를 추가로 가지는 다음 x 객체로 호출이 가능하다.
const x = {a: 'a', b: 'b', c: 2, d: new Date()}; foo(x); // OK
TypeScript
복사
즉, ABC 타입에 할당 가능한 객체는 ABC의 속성 외에 다른 속성이 존재할 수 있기 때문에 타입스크립트는 ABC 타입의 키를 string 타입으로 선택하게 된다.
keyof 키워드를 사용한 해결법은 또 다른 문제점을 내포하고 있다.
function foo(abc: ABC) { let k: keyof ABC; for (k in abc) { // let k: "a" | "b" | "c" const v = abc[k]; // Type is string | number } }
TypeScript
복사
k“a” | “b” | “c” 타입으로 한정되어 문제가 된 것처럼 vstring | number 타입으로 한정되어 범위가 너무 좁아 문제가 된다.
d: new Date()와 같은 이전 예제처럼 d 속성은 어떠한 타입도 될 수 있기 때문에 vstring | number로 추론된 것은 런타임의 동작을 예상하기 어렵게 만든다.
타입 문제를 신경쓰지 않고 단지 객체의 키와 값을 순회하고 싶다면 Object.entries를 사용하면 된다.
function foo(abc: ABC) { for (const [k, v] of Object.entries(abc)) { k // Type is string v // Type is any } }
TypeScript
복사
객체를 순회하며 키와 값을 얻으려면 (let k: keyof T) 같은 keyof 선언이나 Object.entries를 사용하면 된다.
keyof 선언은 상수이거나 추가적인 키없이 정확한 타입을 원하는 경우에 적합하다.
Object.entries는 더욱 일반적으로 사용되지만 키와 값의 타입을 다루기 까다롭다.
요약
객체를 순회할 때, 키가 어떤 타입인지 정확히 파악하고 있다면 let k: keyof Tfor-in 루프를 사용하자. 함수의 매개변수로 쓰이는 객체에는 추가적인 속성이 있을 수 있음을 명심하자.
객체를 순회하며 키와 값을 얻는 가장 일반적인 방법은 Object.entries를 사용하는 것이다.

item 55. DOM 계층 구조 이해하기

브라우저 관련 내용이라 생략하였습니다.

item 56. 정보를 감추는 목적으로 private 사용하지 않기

자바스크립트는 클래스에 비공개 속성을 만들 수 없다. 다만 비공개 속성임을 나타내기 위해 언더스코어를 접두사로 붙이던 것이 관례로 인정될 뿐이었다.
단순히 비공개라고 표시한 것뿐이지 일반적인 속성과 동일하게 클래스 외부에 공개되어 있다는 점을 주의해야 한다.
타입스크립트에는 public, protected, private 접근 제어자를 사용해서 공개 규칙을 강제할 수 있는 것으로 오해할 수 있다.
class Diary { private secret = 'cheated on my English test'; } const diary = new Diary(); diary.secret // ~~~~~~ Property 'secret' is private and only // accessible within class 'Diary'
TypeScript
복사
public, protected, private같은 접근 제어자는 타입스크립트 키워드이기 때문에 컴파일 이후에는 제거된다.
위 예제 코드가 컴파일되면 다음 예제의 자바스크립트 코드로 변환된다.
class Diary { constructor() { this.secret = 'cheated on my English test'; } } const diary = new Diary(); diary.secret;
TypeScript
복사
private 키워드는 사라졌고 secret은 일반적인 속성이므로 접근할 수 있다. 심지어 단언문을 사용하면 타입스크립트 상태에서도 private 속성에 접근할 수 있다.
정보를 감추기 위해서 private을 사용해서는 안 된다.
자바스크립트에서 정보를 숨기기 위해 가장 효과적인 방법은 클로저를 사용하는 것이다. 다음 코드처럼 생성자에서 클로저를 만들어낼 수 있다.
declare function hash(text: string): number; class PasswordChecker { checkPassword: (password: string) => boolean; constructor(passwordHash: number) { this.checkPassword = (password: string) => { return hash(password) === passwordHash; } } } const checker = new PasswordChecker(hash('s3cret')); checker.checkPassword('s3cret'); // Returns true
TypeScript
복사
다른 선택지로 현재 표준화가 진행 중인 비공개 필드 기능을 사용할 수 있다.
접두사로 #를 붙여서 타입 체크와 런타임 모두에서 비공개로 만드는 역할을 한다.
class PasswordChecker { #passwordHash: number; constructor(passwordHash: number) { this.#passwordHash = passwordHash; } checkPassword(password: string) { return hash(password) === this.#passwordHash; } } const checker = new PasswordChecker(hash('s3cret')); checker.checkPassword('secret'); // Returns false checker.checkPassword('s3cret'); // Returns true
TypeScript
복사
요약
public, protected, private 접근 제어자는 타입 시스템에서만 강제될 뿐으로 런타임에는 소용이 없으며 단언문을 통해 우회할 수 있다. 접근 제어자로 캡슐화를 달성하려해선 안 된다.
확실히 데이터를 감추고 싶다면 클로저나 # 접두사를 사용하자.

item 57. 소스맵을 사용하여 타입스크립트 디버깅하기

엄밀히 말해 타입스크립트를 실행한다는 것은 타입스크립트 컴파일러가 생성한 자바스크립트 코드를 실행한다는 것이다.
이렇게 변환된 자바스크립트 코드는 복잡해 디버깅하기 매우 어렵기 때문에 이를 해결하고 브라우저 제조사들은 서로 협력하여 소스맵이라는 해결책을 내놓았다.
소스맵은 변환된 코드의 위치와 심벌들을 원본 코드의 원래 위치와 심벌들로 매핑한다.
NodeJS 프로그램의 디버깅에도 소스맵을 사용할 수 있는데, 보통 편집기가 자동 인식하거나 NodeJS 프로세스를 브라우저 디버거와 연결한다.
타입 체커가 코드를 실행하기 전 많은 오류를 잡을 수 있지만 디버거를 대체할 순 없다. 소스맵을 사용하여 제대로 된 타입스크립트 디버깅 환경을 구축하라.
요약
원본 코드가 아닌 변환된 자바스크립트 코드를 디버깅하지 말라. 소스맵을 사용해서 런타임에 타입스크립트 코드를 디버깅하자.
소스맵이 최종적으로 변환된 코드에 완전히 잘 매핑되어 있는지 확인하라.
소스맵에 원본 코드가 그대로 포함되도록 설정되어 있을 수 있으니 배포가 필요한 경우, 공개되지 않도록 설정을 확인하라.