[3장] 고급 타입 - 북 스터디

코딩 마을 방범대 북 스터디 DAY 2

·

8 min read

[3장] 고급 타입 - 북 스터디
🤓 스터디원 🤓
강지윤 @eeeyooon
이예솔 @lulla-by
이성령 @sryung1225
이에스더 @Stilllee
채하은 @chaehaeun



코딩 마을 방범대 : 3장. 고급 타입

질문과 답변

1. 불필요한 제네릭 사용

제네릭을 굳이 사용하지 않아도 되는 코드를 적절하게 수정해보세요.

타입스크립트에서 제네릭이 필요하지 않을 때에도 무분별하게 사용할 경우 코드의 길이만 늘어나고 가독성을 해칠 수 있습니다.

아래의 코드에서 불필요한 부분을 찾아내 코드를 개선해보세요.

type Identity<T> = T;
type Status = "ACTIVE" | "INACTIVE" | "PENDING";

interface User {
  getStatus: () => Identity<Status>;
}

const user: User = {
  getStatus: () => "ACTIVE"
};
lulla-by
제네릭을 사용한 type Identity가 문맥상 필수 요소로 보이지 않아 다음과 같이 수정했습니다.

type Status = "ACTIVE" | "INACTIVE" | "PENDING";

interface User {
  getStatus: () => Status;
}

const user: User = {
  getStatus: () => "ACTIVE"
};
eeeyooon
Identity<T>T를 그대로 반환하기만 하고 있어 제네릭이 불필요합니다. Identity<T>를 제거하고, getStatus가 직접 Status를 반환하도록 수정할 수 있습니다.

type Status = "ACTIVE" | "INACTIVE" | "PENDING";

interface User {
  getStatus: () => Status;
}

const user: User = {
  getStatus: () => "ACTIVE"
};
sryung1225
선언된 Identity 제네릭은 타입을 그대로 반환하는 역할을 하고 있습니다.
Identity가 사용되고 있는 getStatus은 단순히 Status 타입의 값을 반환하는 것이기 때문에 굳이 Identity를 사용하는 의미가 없어 제거 가능합니다.
아래와 같이 코드를 개선시킬 수 있습니다.

type Status = "ACTIVE" | "INACTIVE" | "PENDING";

interface User {
  getStatus: () => Status;
}

const user: User = {
  getStatus: () => "ACTIVE"
};

출제자 : Stilllee

이 코드에서 Identity라는 타입은 단순히 제네릭 매개변수 T를 그대로 반환하고 있습니다.

User인터페이스의 getStatus메서드에서 Identity<Status>를 반환타입으로 사용할 필요없이 직접 Status의 타입을 사용하는것이 더 간결하고 명확합니다.

따라서 다음과 같이 코드를 수정할 수 있습니다.

type Status = "ACTIVE" | "INACTIVE" | "PENDING";

interface User {
  getStatus: () => Status;
}

const user: User = {
  getStatus: () => "ACTIVE"
};

이렇게 수정하면 코드가 더 이해하기 쉽고 간결해지며, Status 타입의 목적과 사용이 명확해지므로 협업 시에도 실수를 줄일 수 있습니다.




2. enum의 특징

enum의 특징인 직접 할당되지 않은 멤버의 값을 추론하는 방식이 안전하지 이유는 무엇인가요?

enum 타입은 타입스크립트에서 지원하는 특수한 타입입니다. 각 멤버에 명시적으로 값을 할당할 수도 있고, 아래와 같이 값을 스스로 추론하도록 할 수도 있습니다.

enum HttpStatusType {
  Forbidden = 403,
  NotFound,
  BadGateway = 502,
}
console.log(HttpStatusType[404]); // ? "NotFound" // 역방향 접근

하지만 이런 타입스크립트가 자동으로 추론한 enum은 안전하지 않다는 이유로 책에서 권장하지 않고 있습니다. 안전하지 않다고 평가한 근거는 무엇인가요? 그리고 대신에 어떤 방식으로 선언된 enum을 권장하는 지 설명하시오.

Stilllee
안전하지 않은 이유
1. 값의 중복과 예상치 못한 결과
타입스크립트에서 enum을 사용할 때, 명시적으로 값을 할당하지 않으면 이전 멤버의 값을 기반으로 다음 멤버의 값을 자동으로 할당하게 됩니다.

enum HttpStatusType {
  Forbidden = 403,
  NotFound, // 자동으로 404가 할당됨
  BadGateway = 502,
}
이는 명시적으로 선언되지 않았기 때문에 코드를 읽는 사람이나 컴파일러가 예측하기 어렵습니다.

2. 명시적 재할당으로 인한 중복 값
enum 내에서 값이 명시적으로 재할당되면, 다른 멤버가 같은 값을 갖게 되어 예상치 못한 중복 값이 발생할 수 있습니다.

enum HttpStatusType {
  Forbidden = 403,
  NotFound, // 자동으로 404가 할당됨
  BadGateway = 404, // BadGateway에 404라는 값을 명시적으로 재할당
}
예를들어 위 코드의 경우 NotFoundBadGateway 두 멤버 모두 같은 값인 404를 갖게 됩니다.

이는 enum의 기본 목적인 각 멤버의 고유성을 해치며, 런타임에 예상치 못한 결과를 초래할 수 있습니다.

3. 역방향 접근의 문제
enum은 역방향으로도 접근할 수 있습니다. 즉, 숫자를 사용하여 해당 숫자에 할당된 enum 멤버의 이름을 얻을 수 있습니다.

이는 할당된 값의 범위를 넘어서는 접근을 시도할 때도 막지 않으므로, 예상치 못한 동작을 유발할 수 있습니다.

// 정상적인 역방향 접근
console.log(HttpStatusType[404]); // "NotFound"

// 범위를 넘어서는 역방향 접근
console.log(HttpStatusType[405]); // undefined 또는 예상치 못한 결과

대신 권장되는 방식
1. const enum 사용
const enum은 역방향으로의 접근을 허용하지 않으며, 컴파일 시 enum을 인라인 상수로 변환하여 성능상의 이점을 제공합니다.
이는 자바스크립트에서 객체에 접근하는 것과 유사한 동작을 보장하며, 불필요한 런타임 오버헤드를 줄여줍니다.

2. 명시적 값 할당
모든 enum 멤버에 대해 명시적으로 값을 할당하면 자동 값 추론으로 인한 혼란을 방지할 수 있으며, 코드의 가독성과 예측 가능성이 높아집니다.
lulla-by
자동으로 추론한 enum의 경우 의도하지 않은 결과가 나올 수 있습니다. 자동 추론을 사용하여 멤버를 변경하는 등의 경우 다른 enum 멤버들에게도 영향이 미쳐 의도하지 않은 값의 변경이 생길 수 있습니다.

const enum으로 열거형을 선언하여 역방향 접근을 허용하지 않는 방법을 권장하며 숫자 상수 방식은 선언하지 않은 멤버로 접근 및 할당이 가능하기에 이를 막아주는 문자열 상수 방식으로 선언하는 것을 권장합니다.
eeeyooon
자동으로 추론한 열거형은 안전하지 않은 결과를 낳을 수도 있습니다. 만약 할당된 값을 넘어서는 범위로 역방향으로 접근하더라도 타입스크립트는 막지 않습니다. 또한 선언되지 않은 key값의 접근을 허용하기도 합니다.

이러한 enum의 문제점 때문에 책에서는 const enum으로 열거형을 선언하는 방법을 권장합니다. 다만 const enum 으로 열거형을 선언하더라도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 이를 방지하지 못합니다. 반면 문자열 상수 방식으로 선언한 열거형은 미리 선언하지 않은 멤버로 접근을 방지하기 때문에 숫자 상수 방식보다 더 안전하며 의도하지 않은 값의 할당이나 접근을 방지하는 데 도움이 되는 문자열 상수 방식을 권장합니다.

출제자 : sryung1225

☑ 예시 코드와 같이 enum은 직접 할당한 값 또는 추론 된 값을 이용해 멤버를 알아내는 역방향 접근이 가능합니다. 문제는 할당한 값을 넘어서는 범위로도 역방향 접근이 가능하다는 것입니다.

console.log(HttpStatusType[404]); // ? "NotFound"
console.log(HttpStatusType[476]); // ? undefined

위에서 자동으로 추론된 NotFound = 404BadGateway = 502 사이 임의의 값으로 역방향 접근을 하게 될 경우 undefined를 출력해 냅니다.

여기에서 핵심은 실행 과정에서 막히는 부분이 없다는 사실입니다. 에러가 발생하지 않는 이 결과를 안전하지 않은 결과라고 평가합니다.

때문에 더 안전한 방식상수 enum을 사용하는 것입니다. const enum 을 이용해 enum을 선언하는 경우 역방향 접근을 아예 허용하지 않게 됩니다.

const enum HttpStatusType {
  Forbidden = 403,
  NotFound,
  BadGateway = 502,
}
console.log(HttpStatusType[404]); // 🚨 A const enum member can only be accessed using a string literal.

image




3. any 타입과 제네릭 타입

any 타입과 제네릭 타입을 사용하여 배열을 생성했을 때의 차이점을 설명해주세요.

107쪽에서

앞서 제네릭이 일반화된 데이터 타입을 말한다고 했는데, 이 표현만 보면 any의 쓰임과 혼동할 수도 있을 것이다. 하지만 둘은 명확히 다르다. 둘의 차이는 배열을 떠올리면 쉽게 알 수 있다.

라고 서술하고 있는데, 둘의 차이점이 무엇이 있는 지 설명해주세요.

Stilllee
any 타입의 배열에는 모든 타입의 요소가 들어갈 수 있습니다. 타입 검사를 하지 않고 모든 타입이 허용되는 타입으로 취급되기 때문에 요소들의 타입이 전부 같지 않을 수 있습니다.

제네릭은 배열 생성 시점에 원하는 타입으로 특정할 수 있어 배열 요소가 전부 동일한 타입이라고 보장할 수 있으며 타입 검사가 이루어지기 때문에 타입 오류를 사전에 방지할 수 있습니다.
lulla-by
any 타입은 모든 타입을 받기 때문에 any 타입으로 배열 요소들의 타입을 지정할 경우 모든 타입의 값들이 배열 요소로 들어갈 수 있습니다. 반면 제네릭은 배열생성시점에 타입 변수에 원하는 타입을 특정함으로써 배열의 구성요소들의 동일성을 보장할 수 있습니다.
sryung1225
any는 모든 타입을 허용하는 데에 반면, generic은 모든 타입을 허용한다는 의미가 아닌 사용 시점에 원하는 타입으로 선언을 진행합니다.

// any 타입의 배열 : 배열 요소들의 타입이 서로 다를 수 있음
const arrayAny: any[] = [1, "two", true];

// generic 타입의 배열 : 배열 요소들이 특정된 타입으로 선언되어 동일함
const arrayGeneric: Array = [1, "two", true];
// 🚨 Type 'string' is not assignable to type 'number'.
// 🚨 Type 'boolean' is not assignable to type 'number'.

출제자 : eeeyooon

any 타입의 배열은 배열의 각 요소가 어떤 타입이든 될 수 있습니다. 어떤 타입의 데이터도 저장될 수 있지만, 타입 검사를 하지않고 모든 타입이 허용되는 타입으로 취급되기 때문에 타입 안정성이 떨어지고 런타임에 타입 관련 오류가 발생할 가능성이 높아집니다.

let arrayAny: any[] = [1, "string", true, { key: "value" }];

제네릭 타입의 배열은 배열 생성 시점에 원하는 타입을 지정하며, 선언 시 지정된 타입의 요소만 포함할 수 있습니다. 그렇기 때문에 배열의 모든 요소가 동일한 타입임을 보장합니다. 이는 타입 안정성을 높이고, 오류 가능성을 줄입니다.

let arrayGeneric: Array<number> = [1, 2, 3, 4];
arrayGeneric = [1, "string", true];
// error: Type 'string' is not assignable to type 'number'
//        Type 'boolean' is not assignable to type 'number'


추가 논의

문자열 상수 방식

2번 문제의 출제자 답변에서 파생된 논제이다.

사실 책에서 내린 결론은 const enum으로는 충분하지 않고 문자열 상수 방식을 사용하는 것이 숫자 상수 방식보다 더 안전하다 이다.

그러나 const enum으로 열거형을 선언하더라도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 이를 방지하지 못한다. 반면 문자열 상수 방식으로 선언한 열거형은 미리 선언하지 않은 멤버로 접근을 방지한다. 따라서 문자열 상수 방식으로 열거형을 사용하는 것이 숫자 상수 방식보다 더 안전하며 의도하지 않은 값의 할당이나 접근을 방지하는 데 도움이 된다. -p97

하지만 이 구문의 근거를 찾지 못해 해당 문제의 답변에서 제외되었다.

p97 하단 예제 3.1.6-5는 숫자 상수 방식과 문자열 상수 방식를 비교하는 예제이다.

const enum NUMBER {
  ONE = 1,
  TWO = 2,
}
const myNumber: NUMBER = 100; // NUMBER enum에서 100을 관리하고 있지 않지만 이는 에러를 발생시키지 않는다

const enum STRING_NUMBER {
  ONE = "ONE",
  TWO = "TWO",
}
const myStringNumber: STRING_NUMBER = "THREE"; // Error

숫자 상수 방식(Number)은 에러를 반환하지 않고, 문자열 상수 방식(STRING_NUMBER)은 에러를 반환하기 때문에 더 안전함을 근거로 들었다.

하지만 실제로는 두 방식 모두 동일한 에러를 반환했다.

const enum NUMBER {
  ONE = 1,
  TWO = 2,
}
const myNumber: NUMBER = 100; // 🚨 Type '100' is not assignable to type 'NUMBER'.

const enum STRING_NUMBER {
  ONE = "ONE",
  TWO = "TWO",
}
const myStringNumber: STRING_NUMBER = "THREE"; // 🚨 Type '"THREE"' is not assignable to type 'STRING_NUMBER'.


TS playground 실행 결과나 직접 만든 예시도 상황은 동일하다.

const enum HttpStatusType {
  Forbidden = 403,
  BadGateway = 502,
}
const myStatus: HttpStatusType = 430; // 🚨 Type '430' is not assignable to type 'HttpStatusType'.

그 외 chatGPT와 검색을 통해서도 명확한 정답을 찾아내기 어려움이 있었기 때문에 해당 문제의 답에 문자열 상수 방식은 포함하지 않는 것으로 정리되었다.