LogoSEO Jing
  • All Posts
  • SEO Jing
  • okayJing
  • KD Team
  • CLAB Coreteam
  • Study

Contact Me

© 2026 SEOJing. All rights reserved.

이펙티브 타입스크립트 2판 Day 11: 객체 타입은 열려 있고, 계약은 닫아야 한다

2026년 7월 6일·7분 읽기
범위: Effective TypeScript 2판 Item 60–66
오늘의 질문: “객체 타입이 맞으면 객체 순회와 API prop 조합도 안전하다고 봐도 될까?”

먼저 보는 코드

ts
type User = {
  id: string;
  name: string;
  role: "admin" | "member";
};

function renderUser(user: User) {
  return Object.keys(user).map((key) => user[key].toString());
}

User의 키는 id | name | role처럼 보입니다. 그런데 Object.keys(user)의 타입은 보통 string[]입니다. 타입스크립트가 답답해서 그런 게 아니라, 자바스크립트 객체가 런타임에서 더 열려 있기 때문입니다. User 타입을 만족하는 값이 실제로는 추가 프로퍼티를 더 가질 수도 있습니다.

객체 타입 도구 지도

틀리기 쉬운 직감: keyof와 Object.keys는 같은 세계다

keyof User는 타입 공간의 키 집합입니다. Object.keys(user)는 런타임 값에서 열거 가능한 문자열 키를 읽습니다. 비슷해 보이지만 같은 계약이 아닙니다.

ts
const user = {
  id: "u1",
  name: "Jin",
  role: "admin",
  debug: true,
} satisfies User;

Object.keys(user); // ["id", "name", "role", "debug"]

이런 값은 User로 사용할 수 있지만 런타임 키에는 debug가 포함됩니다. 그래서 객체 순회 코드는 타입 단언으로 덮기 전에 질문해야 합니다.

이 순회는 “정해진 키 목록”을 돌고 싶은가, 아니면 실제 객체의 런타임 키를 전부 보고 싶은가?

정해진 키 목록이 필요하다면 명시적으로 닫는 편이 낫습니다.

ts
const userFields = ["id", "name", "role"] as const;
type UserField = (typeof userFields)[number];

function renderUser(user: User) {
  return userFields.map((field) => String(user[field]));
}

Record는 닫힌 키 공간을 표현할 때 강하다

Record는 “이 키 집합에 대해 값이 모두 있다”는 계약을 만들 때 유용합니다.

ts
type Status = "idle" | "loading" | "success" | "error";

const labels: Record<Status, string> = {
  idle: "대기",
  loading: "불러오는 중",
  success: "완료",
  error: "실패",
};

나중에 Status에 empty를 추가하면 labels가 빠진 키를 알려 줍니다. 이때 Record는 좋은 선택입니다.

반대로 키가 사용자가 만든 임의 문자열이고 계속 추가/삭제된다면 Record<string, V>보다 Map이나 명시적 자료구조가 더 솔직할 수 있습니다. 코드 리뷰에서는 키 공간이 닫혔는지부터 봅니다.

상황더 잘 맞는 형태리뷰 질문
union 키 전체에 라벨 매핑Record새 union 값이 추가되면 빠진 매핑을 잡는가?
동적 cache, id 기반 저장소Map 또는 별도 store추가/삭제/순회 순서가 중요한가?
일부 키만 있을 수 있음Partial>없는 키를 어떻게 처리하는가?
외부 JSON 객체runtime validation + 타입실제 입력 검증이 있는가?

tuple과 rest는 위치까지 계약으로 만든다

배열 타입은 보통 “같은 종류의 값 여러 개”를 뜻합니다. tuple은 “몇 번째 자리에 무엇이 있는지”까지 계약으로 만듭니다.

ts
type RouteMatch = [path: string, params: Record<string, string>];

function useRoute(): RouteMatch {
  return ["/posts/:id", { id: "42" }];
}

이런 경우 tuple label은 문서 역할을 합니다. 하지만 tuple이 길어지면 읽기 어렵습니다. 세 칸 이상이고 각 값의 의미가 독립적이라면 객체가 나을 수 있습니다.

rest parameter도 비슷합니다.

ts
type LogArgs = [
  level: "info" | "warn" | "error",
  message: string,
  ...meta: unknown[],
];

function log(...args: LogArgs) {
  const [level, message, ...meta] = args;
}

리뷰 질문은 “위치가 이 API의 본질인가?”입니다. 위치가 본질이면 tuple이 좋고, 이름이 더 중요하면 객체가 좋습니다.

XOR 타입은 prop 조합을 막는 데 쓴다

프론트엔드 컴포넌트에서는 둘 중 하나만 허용해야 하는 prop이 자주 나옵니다.

ts
type LinkButtonProps =
  | { href: string; onClick?: never }
  | { href?: never; onClick: () => void };

이 타입은 링크 버튼과 클릭 버튼을 동시에 받는 실수를 막습니다.

ts
// 가능
const link: LinkButtonProps = { href: "/settings" };

// 실패해야 함
const broken: LinkButtonProps = { href: "/settings", onClick: () => {} };

여기서는 타입 기술 자체보다 “잘못된 조합을 public API에서 막을 가치가 있는가?”를 봅니다. 모든 prop 조합을 복잡한 타입으로 막으려 하면 API가 어려워집니다. 사용자가 자주 틀리고 런타임 버그로 이어지는 조합부터 막는 편이 좋습니다.

brand는 값의 모양이 같아도 의미가 다를 때 쓴다

구조적 타입 시스템에서는 모양이 같으면 호환됩니다.

ts
type UserId = string;
type PostId = string;

function loadUser(id: UserId) {}

const postId: PostId = "p1";
loadUser(postId); // 통과

둘 다 string이라서 구분되지 않습니다. 의미가 섞이면 위험한 경계에서는 brand를 붙일 수 있습니다.

ts
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;

brand는 런타임 검증이 아닙니다. 값이 정말 UserId인지 확인하는 함수나 생성 경계가 있어야 합니다. 리뷰에서는 brand 자체보다 “이 branded 값은 어디서 만들어지고 검증되는가?”를 봐야 합니다.

@types 버전도 계약이다

라이브러리 타입은 코드와 따로 움직일 수 있습니다. 특히 런타임 패키지와 @types/* 패키지가 분리되어 있으면 버전 어긋남이 버그처럼 보일 수 있습니다.

json
{
  "dependencies": {
    "some-lib": "2.4.0"
  },
  "devDependencies": {
    "@types/some-lib": "1.9.0"
  }
}

이 상태에서 타입 에러가 난다면 내 코드가 틀린 게 아니라 타입 패키지가 오래됐을 수도 있습니다. 반대로 타입이 통과해도 런타임 라이브러리와 맞지 않을 수 있습니다.

코드 리뷰에서는 dependency 변경 PR에서 타입 패키지도 함께 봅니다.

런타임 패키지와 @types 버전이 맞는가? 라이브러리가 자체 타입을 제공하는데 불필요한 @types가 남아 있지 않은가? 타입 에러를 단언으로 덮기 전에 버전 불일치를 확인했는가? lockfile 변경이 실제 의존성 변화를 설명하는가?

프론트엔드 코드 리뷰 체크리스트

변화확인할 질문
Object.keys 순회런타임 키 전체를 도는가, 닫힌 키 목록을 도는가?
Record 추가키 공간이 union처럼 닫혀 있는가? 없는 키 처리는 명확한가?
tuple/rest 타입위치가 계약인가, 이름 있는 객체가 더 읽기 쉬운가?
XOR prop 타입실제로 자주 틀리는 조합을 막는가, 타입만 과하게 복잡한가?
brand 타입branded 값의 생성/검증 경계가 있는가?
@types 변경런타임 패키지 버전과 타입 패키지 버전이 맞는가?

이번 범위의 핵심은 타입 도구를 많이 쓰는 것이 아닙니다. 객체 타입은 자바스크립트 런타임의 열린 성질 위에 놓입니다. 그래서 좋은 타입 설계는 “무엇을 허용할지”뿐 아니라 “어떤 잘못된 조합을 어디서 막을지”를 코드 리뷰에서 말할 수 있어야 합니다.

Day 11 타입 리뷰 퀴즈

Quiz1 / 4
Q.Object.keys(user)의 타입이 보통 string[]인 이유로 가장 적절한 것은 무엇일까요?

Post Q&A

오케이징에게 물어보기

이펙티브 타입스크립트 2판 Day 11: 객체 타입은 열려 있고, 계약은 닫아야 한다 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!

0/500

포스트 목록

/study/effective-typescript
파일 11개, 폴더 0개
이펙티브 타입스크립트 2판 Day 1: 타입스크립트를 믿기 전에 알아야 할 경계이펙티브 타입스크립트 2판 Day 2: 타입을 값의 집합으로 보기이펙티브 타입스크립트 2판 Day 3: 타입 반복을 줄이는 리뷰 감각이펙티브 타입스크립트 2판 Day 4: 추론을 살리는 값 생성 패턴이펙티브 타입스크립트 2판 Day 5: 타입 추론을 방해하지 않는 API 설계이펙티브 타입스크립트 2판 Day 6: 유효한 상태만 표현하는 타입 설계이펙티브 타입스크립트 2판 Day 7: 문자열보다 도메인 의미를 좁히기이펙티브 타입스크립트 2판 Day 8: any를 밖에 세우고 unknown으로 검증하기이펙티브 타입스크립트 2판 Day 9: 제네릭은 관계를 보존할 때만 강하다이펙티브 타입스크립트 2판 Day 10: 타입도 테스트하고 빠지는 분기는 막는다이펙티브 타입스크립트 2판 Day 11: 객체 타입은 열려 있고, 계약은 닫아야 한다

같은 섹션의 대표 이미지

11 posts · latest first
Object iteration, Record, tuple/rest, XOR, brand, @types 버전 관리를 연결한 타입 리뷰 다이어그램
Study26. 07. 07.

이펙티브 타입스크립트 2판 Day 11: 객체 타입은 열려.

Item 60–66 범위를 바탕으로 Object iteration, Record, tuple/rest, XOR, brand, @types 버전 경계를 코드 리뷰에서 다루는 방법을 정리합니다.

26. 07. 07.SEOJing
타입 계약, 타입 테스트, never exhaustiveness, 코드 생성 경계를 보여주는 다이어그램
Study26. 07. 06.

이펙티브 타입스크립트 2판 Day 10: 타입도 테스트하고.

Item 55–59 범위를 바탕으로 type test, 타입 표시, 꼬리 재귀 한계, 코드 생성, never exhaustiveness를 코드 리뷰에서 다루는 방법을 정리합니다.

26. 07. 06.SEOJing
외부 경계, 제네릭, 조건부 타입, 템플릿 리터럴 타입의 역할과 과한 타입 계산의 위험을 보여주는 다이어그램
Study26. 07. 05.

이펙티브 타입스크립트 2판 Day 9: 제네릭은 관계를 보존할.

Item 49–54 범위를 바탕으로 type coverage, 제네릭, 조건부 타입, 템플릿 리터럴 타입을 코드 리뷰에서 어떻게 판단할지 정리합니다.

26. 07. 05.SEOJing
외부 JSON을 unknown으로 받고 검증 뒤 도메인 타입으로 넘기는 경계와 any가 내부로 번지는 위험을 보여주는 다이어그램
Study26. 07. 04.

이펙티브 타입스크립트 2판 Day 8: any를 밖에 세우고.

Item 42–48 범위를 바탕으로 any, unknown, 타입 단언, monkey patching, soundness 함정을 외부 입력 경계와 코드 리뷰 관점에서 정리합니다.

26. 07. 04.SEOJing
넓은 문자열과 선택적 필드를 도메인 이름과 유효 상태 유니온으로 좁히는 다이어그램
Study26. 06. 29.

이펙티브 타입스크립트 2판 Day 7: 문자열보다 도메인.

Item 35–41 범위를 바탕으로 string 남용, optional 필드, 특수 값, 도메인 이름 설계를 코드 리뷰 관점에서 정리합니다.

26. 06. 29.SEOJing
API 응답을 유효한 상태 유니온으로 변환하는 흐름 다이어그램
Study26. 06. 28.

이펙티브 타입스크립트 2판 Day 6: 유효한 상태만 표현하는.

Item 28–34 범위를 바탕으로 추론 위치, API 경계, null 처리, union 설계를 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 28.SEOJing
타입스크립트 추론이 값에서 API 경계로 흐르는 과정을 보여주는 다이어그램
Study26. 06. 27.

이펙티브 타입스크립트 2판 Day 5: 타입 추론을 방해하지 않는.

Effective TypeScript 2판 Item 19–22를 바탕으로 타입 추론, 타입 넓히기, 함수형 기법, 유니온 설계를 코드 리뷰 관점에서 정리합니다.

26. 06. 27.SEOJing
TypeScript에서 넓은 타입으로 먼저 담으면 key와 literal 정보가 사라지고 리뷰 체크가 약해지는 흐름 다이어그램
Study26. 06. 26.

이펙티브 타입스크립트 2판 Day 4: 추론을 살리는 값 생성.

Effective TypeScript 2판의 Item 16–21을 바탕으로 index signature, 타입 추론 기본, 변수/객체 생성 패턴을 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 26.SEOJing
반복된 타입 선언을 원본 타입에서 파생한 타입으로 바꿔 리뷰 체크를 강하게 만드는 흐름 다이어그램
Study26. 06. 25.

이펙티브 타입스크립트 2판 Day 3: 타입 반복을 줄이는 리뷰.

Effective TypeScript 2판의 Item 11–15를 바탕으로 excess property check, 함수식 타입, type vs interface, readonly, 타입 반복 제거를 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 25.SEOJing
TypeScript 타입을 값의 집합, type space, value space, assertion 경계로 정리한 다이어그램
Study26. 06. 24.

이펙티브 타입스크립트 2판 Day 2: 타입을 값의 집합으로.

Effective TypeScript 2판의 Item 6–10을 바탕으로 타입 시스템을 탐색하는 법, 타입을 값의 집합으로 보는 관점, type/value space, 타입 단언의 경계를 코드 리뷰 관점에서 정리합니다.

26. 06. 24.SEOJing
TypeScript를 JavaScript 위의 정적 타입 계층, 타입 제거, 구조적 타이핑, any의 구멍으로 정리한 다이어그램
Study26. 06. 23.

이펙티브 타입스크립트 2판 Day 1: 타입스크립트를 믿기.

Effective TypeScript 2판의 Item 1–5를 바탕으로 TypeScript와 JavaScript의 관계, tsconfig, 타입 제거, 구조적 타이핑, any의 위험을 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 23.SEOJing