타입스크립트 2편: 구조적 타이핑, 이름보다 모양이 중요한 이유
kwangmook.dev·2026. 05. 09.·2 min read
"우아한 타입스크립트 with 리액트"를 읽으며 정리한 학습 노트.
구조적 타이핑, 구조적 서브타이핑, 명목적/DUCK 타이핑 비교
타입 이름보다 모양(shape)이 더 중요하다
![]()
1. 왜 서로 다른 타입 이름인데 대입이 될까?
type User = { name: string };
type Author = { name: string };
let u: User = { name: "kim" };
let a: Author = u; // 에러 없음
User와 Author는 이름이 다른데도 대입이 된다.
이유인즉슨 타입스크립트는 기본적으로 구조적 타이핑을 사용한다.
즉, "이름이 같은가"가 아니라 "모양이 같은가"를 본다.
2. 구조적 타이핑: 이름 말고 구조를 본다
구조적 타이핑을 한 줄로 말하면:
필요한 프로퍼티를 갖고 있으면 같은 타입으로 본다.
type Point2D = { x: number; y: number };
type Coord = { x: number; y: number };
const p: Point2D = { x: 1, y: 2 };
const c: Coord = p; // 가능
이 관점의 장점은
- 라이브러리/모듈이 달라도 구조가 맞으면 유연하게 연결됨
- 함수 파라미터 설계가 단순해짐
3. 구조적 서브타이핑: "요구사항 포함"이면 통과
구조적 서브타이핑은 좀 더 쉽게 말해서
- 어떤 타입이 요구하는 최소 조건을 만족하면 대입 가능
- 더 많은 정보를 가진 타입을 더 적은 정보를 요구하는 곳에 넣는 건 가능
type Animal = { name: string };
type Dog = { name: string; bark: () => void };
const dog: Dog = { name: "coco", bark: () => {} };
const animal: Animal = dog; // 가능
// const dog2: Dog = animal; // 불가 (bark가 없을 수 있음)
요약
Dog -> Animal: 가능Animal -> Dog: 불가
4. 초과 프로퍼티 체크
안되는 예시 (X)
type Animal = { name: string };
const a1: Animal = { name: "navi", age: 3 };
// 에러: 객체 리터럴에 초과 프로퍼티(age)
그런데 이렇게 하면 (O)
const temp = { name: "navi", age: 3 };
const a2: Animal = temp; // 가능
왜 차이가 날까?
- 객체 리터럴 (중괄호로 직접 객체를 만드는 문법) 을 바로 넣을 때는 오타/실수를 잡기 위해 더 엄격하게 검사
- 변수에 한번 담긴 값은 구조 호환 관점으로 검사 -
타입 이름이 아니라 속성 구조가 맞는지로 판단한다는 뜻
5. 명목적 vs 구조적 vs DUCK 타이핑
| 구분 | 판단 기준 | 대표 느낌 |
|---|---|---|
| 명목적 타이핑 | 타입 이름이 같은가 | Java, C# 전통 |
| 구조적 타이핑 | 프로퍼티 모양이 같은가 | TypeScript 핵심 |
| DUCK 타이핑 | 런타임에 필요한 동작이 되는가 | JavaScript 스타일 |
![]()
DUCK 타이핑 - "오리처럼 걷고 오리처럼 울면 오리"
즉, 런타임에서 실제 동작이 되면 통과시키는 방식.
타입스크립트는 컴파일타임에서 구조를 검사하고, 자바스크립트는 런타임에서 실제 동작으로 확인하는 느낌
6. 실무에서 체감되는 장점과 주의점
- props, API 응답, 함수 인자 타입을 돌려 쓰기 편하다
- 타입 이름보다 데이터 모양 중심으로 설계하게 돼서 코드가 덜 경직된다
- 필요한 필드만 맞으면 연결되니 리팩터링할 때 부담이 줄어든다
예를 들어 함수가 name만 필요하면 전체 User를 강요하지 않아도 된다.
type User = {
id: number;
name: string;
email: string;
};
function printName(data: { name: string }) {
console.log(data.name);
}
const user: User = { id: 1, name: "moomoocow", email: "moomoo@x.com" };
printName(user); // 가능: 필요한 모양(name) 포함
유의사항
- 타입을 너무 넓게 열어두면("일단 다 받자") 나중에 버그 찾기가 어려워진다
any가 섞이기 시작하면 구조 검사로 얻던 안정성이 없어짐- 객체 리터럴을 바로 넘길 때와 변수로 넘길 때 체크 강도가 다르다는 점을 잊을수 있다
any가 위험한 이유
function sendEmail(user: { email: string }) {
console.log(user.email.toLowerCase());
}
const fromApi: any = { emali: "typo@test.com" }; // email 오타
sendEmail(fromApi); // 컴파일 통과, 런타임에서 터질 수 있음
초과 프로퍼티 체크 예시
type Animal = { name: string };
const a1: Animal = { name: "navi", age: 3 }; // 에러
const temp = { name: "navi", age: 3 };
const a2: Animal = temp; // 가능
결론
- 타입은 "최소한으로 필요한 모양"만 받게 만든다
- 함수 인자는 큰 타입 전체보다 필요한 필드만 뽑아서 받는다
- 외부 입력은
unknown으로 받고, 좁힌 뒤에 사용한다
외부 입력을 unknown 으로 받는 예시
function handle(data: unknown) {
if (typeof data === "object" && data !== null && "name" in data) {
console.log((data as { name: string }).name);
}
}
이전/다음 포스트
댓글 남기기
댓글 0개
첫 댓글을 남겨보세요.