객체와 인터페이스 - "덕 타이핑"의 정식화
공식문서 기반의 타입스크립트 입문기
들어가며
제가 예전에 만들었었던 개인 블로그 프로젝트에서는 Markdown 파일에서 추출한 meta 객체 안에 author와 tags가 들어 있는데, 어떤 파일에서는 meta 자체가 아예 없을 때도 있었습니다. 간단하게 meta.author.name을 참조해 둔 코드가 있었고, 자바스크립트에서는 그걸 if (meta && meta.author && meta.author.name) { ... }처럼 감싼 뒤에야 겨우 넘어갔습니다. 로컬에서는 그 코드가 대체로 문제가 없었지만, 실제 배포 후 Cannot read properties of undefined (reading 'name')가 찍히며 포스트 하나 제대로 표시되지 못한 적이 있습니다. 그럴 때마다 "데이터가 없어서 그렇다"는 식으로 같은 null 체크를 반복했지만, 정작 중요한 질문은 놓쳐 버렸습니다.
"이 객체는 어떤 모양을 가져야 할까요?" TypeScript는 이런 질문부터 제기합니다. TypeScript는 자바스크립트의 덕 타이핑 관점을 인터페이스나 타입 선언과 구조적 타이핑이라는 계약으로 정식화하고, 컴파일러가 그 계약을 검증하도록 합니다. obj && obj.data && obj.data.users처럼 방어 코드를 계속 붙이기보다, "이 구조라면 안전하다"는 조건을 타입으로 적어두고 컴파일 타임에 빨간 줄을 받으며 실수를 사전에 막는 방식입니다.
TypeScript의 제안
meta가 언제나 존재하지 않기 때문에 TypeScript에서는 타입 선언으로 문제를 정식으로 풀어줍니다. 블로그 포스트의 메타데이터를 안전하게 다루는 방법을 제안합니다.
인터페이스 선언: 옵셔널 속성으로 안전한 구조 정의
interface Author {
name: string;
profileUrl?: string;
}
interface Meta {
author?: Author;
tags?: string[];
}
interface PostPage {
title: string;
meta?: Meta;
}
function renderTitle(page: PostPage) {
return page.meta?.author ? page.meta.author.name : "작가 미정";
}옵셔널 프로퍼티(?)를 사용해 meta나 author가 없을 수 있음을 명시하고, 옵셔널 체이닝(?.)으로 안전하게 접근합니다. TypeScript는 구조적 타이핑으로 객체의 형태를 검사하며, PostPage 인터페이스와 호환되는지 확인합니다. 이름이 아닌 속성 구조를 비교하므로 유연하면서도 타입 안전합니다.
참고: API 응답 JSON을
as PostPage로 강제로 단언하기보다,meta?.author?.name처럼 옵셔널 체이닝을 섞거나 응답을 정리한 뒤 타입에 맞게 만드는 편이 구조적 타이핑과 더 조화롭습니다.
심층 분석
1) 덕 타이핑의 실제 모습
자바스크립트의 덕 타이핑은 "오리처럼 걷고 오리처럼 울면 오리다"라는 개념으로, 객체의 타입 이름이 아닌 실제 속성과 구조를 중요시합니다.
// 덕 타이핑 예시
const user = { name: "김개발", age: 30 };
const admin = { name: "관리자", role: "admin" };
function greet(person) {
console.log(`안녕하세요, ${person.name}님!`);
}
greet(user); // "안녕하세요, 김개발님!"
greet(admin); // "안녕하세요, 관리자님!"user와 admin은 서로 다른 객체지만 name 속성이 있으므로 greet 함수에서 문제없이 작동합니다. 자바스크립트는 클래스나 타입 이름이 아닌 실제 속성 구조를 확인합니다. 이 유연함은 편리하지만 런타임에서 person.fullName 같은 속성 불일치로 오류가 발생할 수 있습니다.
2) 구조적 타이핑으로 안전하게
TypeScript는 이름이 아닌 객체의 속성 구조를 비교하는 구조적 타이핑을 사용합니다.
// 구조적 타이핑 예시
const blogPost = {
title: "TypeScript 소개",
meta: {
author: { name: "김개발" },
tags: ["typescript", "javascript"],
},
};
// PostPage 인터페이스와 구조가 맞으므로 호환됨
function displayPost(post: PostPage) {
console.log(`${post.title} - ${post.meta?.author?.name ?? "익명"}`);
}
displayPost(blogPost); // 정상 작동blogPost는 PostPage 인터페이스를 직접 구현하지 않았지만, 필요한 속성 구조를 가지고 있으므로 호환됩니다. 옵셔널 체이닝과 조건문을 통해 안전하게 속성에 접근할 수 있습니다.
3) interface와 type의 선택
interface는 확장과 선언 병합이 용이하고, type은 복합 타입 조합에 유리합니다.
// interface와 type의 활용
interface BasePost {
title: string;
}
interface BlogPost extends BasePost {
meta?: Meta;
}
// type을 활용한 유니온 타입
type ApiResponse<T> = {
data: T;
status: "success" | "error";
};
type PostResponse = ApiResponse<PostPage>;interface는 extends로 확장하기 좋고, type은 제네릭이나 유니온 타입 조합에 유연합니다. 구조적 타이핑에서는 둘 다 객체 형태를 검사하므로 상황에 맞게 선택할 수 있습니다.
4) 옵셔널 체이닝과 타입 좁히기
옵셔널 체이닝과 조건문을 활용하면 TypeScript의 타입 추론이 더 정교해집니다.
// 옵셔널 체이닝과 타입 좁히기
function processPostPage(page: PostPage) {
// 옵셔널 체이닝으로 안전하게 접근
const authorName = page.meta?.author?.name ?? "작가 미정";
// 조건문 내에서 타입 좁히기
if (page.meta?.author) {
// 이 블록에서는 author가 존재하는 것으로 추론됨
console.log(`글쓴이: ${page.meta.author.name}`);
console.log(`프로필: ${page.meta.author.profileUrl ?? "없음"}`);
}
return authorName;
}옵셔널 체이닝(?.)으로 안전하게 속성에 접근하고, 조건문으로 타입을 좁히면 TypeScript는 해당 블록에서 속성이 존재한다고 추론합니다. 이를 통해 런타임 안전성과 컴파일 타임 검증을 모두 확보할 수 있습니다.
실전 패턴 (In React)
React 컴포넌트: 옵셔널 props 안전하게 다루기
React 컴포넌트에서 옵셔널 props를 안전하게 다루는 방법을 보여줍니다.
type PostPreviewProps = {
title: string;
meta?: Meta;
};
function PostPreview({ title, meta }: PostPreviewProps) {
return (
<article>
<h1>{title}</h1>
<p>{meta?.author?.name ?? "작가 미정"}</p>
<small>{meta?.tags?.length ? meta.tags.join(", ") : "태그 없음"}</small>
</article>
);
}옵셔널 체이닝(?.)과 널리시(nullish) 병합(??)을 활용해 meta 객체가 없을 때를 안전하게 처리합니다. meta?.tags?.length는 meta와 tags가 모두 존재할 때만 length를 확인하며, JSX에서 타입 안전성을 유지하면서 자연스러운 폴백 UI를 구현할 수 있습니다.
참고:
meta가 비동기적으로 들어온다면useState<Meta | null>(null)처럼null을 포함한 제네릭을 쓰거나meta ?? defaultMeta패턴을 섞어 구조적 타이핑으로 “항상 필요한 Shape”을 보장해 보세요.
함정
interface Meta { author: Author; }처럼 옵셔널을 빼버리면 Markdown 파싱에서meta가 빠진 순간 컴파일이 실패합니다. 파일 처리 단계에서meta: parsedMeta.meta ?? undefined처럼 명시적으로 보정하는 한 줄이 큰 차이를 만듭니다.as Meta를 남발하면 구조적 타이핑의 보호막을 스스로 걷어내는 셈입니다. Markdown 파싱 결과를 구조화하는 헬퍼를 만들어 타입을 구성하는 편이 안전합니다.meta.author.name을 곧바로 쓰는 렌더링 코드는null분기를 놓친 것입니다. 조건문을 넣는 대신const authorName = meta?.author?.name;처럼 한 번 변수로 빼면, TypeScript도 "이 시점에서는 name이 없다"를 기억합니다.
예상 질문
Q1. Markdown 파일에 meta가 늘 있는데 왜 옵셔널을 써야 하나요?
항상 들어온다는 보장이 없다면 파싱 과정에서 예외가 발생하는 순간 TypeScript가 미리 경고해 주는 편이 낫습니다. meta가 빠져 있더라도 컴파일 타임에 걸러지니 런타임에서 undefined를 접근하는 일이 줄어듭니다.
Q2. interface와 type 중 어떤 걸 더 써야 하나요?
interface는 선언 병합과 확장에 강해 객체 구조를 문서처럼 표현할 때 더 명시적입니다. 반면 type은 Union, Intersection, Mapped Type 등과 조합돼 더 유연하므로 PostData & { meta?: Meta }처럼 복합적인 조합이 필요하면 type이 편합니다. 결국 구조적 타이핑에서는 이름보다 형태이므로 두 가지를 혼용해도 괜찮습니다.
Q3. meta?.author?.name이 없으면 어떻게 디버깅하죠?
TypeScript는 “이 값이 없을 수 있다”는 사실을 명시적으로 알려주므로 meta?.author?.name ?? "기본 이름"처럼 기본값을 채우거나, if (!meta?.author) { throw new Error("meta.author가 없습니다"); }처럼 빠르게 실패하게 만들어 진짜 문제가 생겼을 때 로그를 확인할 수 있게 합니다.
요약
JS에서는 obj && obj.prop && obj.prop.sub처럼 값의 존재를 계속 확인했지만 TypeScript는 구조(Shape)를 정의하면 meta?.author?.name 같은 체인이 안전하다고 판단합니다. 인터페이스/타입 선언, 옵셔널 프로퍼티, 옵셔널 체이닝은 문법이 아니라 “덕 타이핑을 정식 API”로 만드는 도구입니다. React에서 이 타입을 그대로 props에 심으면, 어떤 값이 없을 때 컴파일러가 미리 “여기 위험하다”고 알려 런타임 전에 불편함을 해소할 수 있습니다.
참조
- TypeScript 공식문서