타입스크립트 구조적 타이핑 면접: 호환성·잉여 속성·함수 분산
Quick Overview
새 객체 리터럴·기존 변수·함수 콜백의 TypeScript 호환성 판정을 작은 컴파일 행렬로 설명하는 한국어 인터뷰 가이드.
TypeScript의 구조적 타이핑은 속성 모양을 기준으로 타입 호환성을 판단하지만, 같은 모양의 값도 어디서 만들어지고 어떤 문맥에 전달되는지에 따라 진단 결과가 달라질 수 있습니다. 면접에서는 “더 많은 속성이 있어도 대입될 수 있다”는 규칙만 말하지 말고, 기존 변수 대입과 새 객체 리터럴의 초과 속성 검사, 함수 인수의 검사, 콜백 매개변수의 분산을 각각 재현해 보세요. 아래 표와 작은 컴파일 예제로 판정의 근거를 추적하고, Implement a Publish-Subscribe Event Bus in JavaScript or TypeScript에서 타입 계약을 실제 UI와 API 경계에 적용해 보세요. 이 글의 공식 사실은 TypeScript 핸드북과 릴리스 문서에 한정합니다. 회사별 출제 방식에 관한 후보자 보고는 수집하지 않았으며, 예제와 인터뷰 설명 방식은 교육 목적의 추론입니다.

먼저 질문을 네 가지 판정으로 나누기 (Decision matrix)
한 줄의 “호환되나요?”에는 서로 다른 질문이 섞일 수 있습니다. 타입의 구조가 맞는지, 객체 리터럴에 낯선 속성이 있는지, 함수 타입의 입력과 출력이 대입 가능한지, 현재 컴파일러 설정에서 어떤 진단이 나오는지 분리해야 합니다.
공식 사실: TypeScript는 기본적으로 구조적 호환성을 사용합니다. 대상 타입이 요구하는 멤버를 원본 값이 제공하면, 원본에 추가 멤버가 있어도 일반적인 변수 대입은 허용될 수 있습니다. 하지만 객체 리터럴을 특정 타입의 값으로 바로 작성할 때는 오타 가능성을 찾기 위한 초과 속성 검사가 추가됩니다. 이 검사는 “객체는 정확히 같은 속성만 가질 수 있다”는 일반 규칙과 같지 않습니다.
| 코드의 형태 | 우선 확인할 규칙 | 흔한 잘못된 결론 |
|---|---|---|
| 이미 만들어 둔 변수를 대입 | 대상이 요구하는 멤버가 원본에 있는가 | 추가 속성이 있으면 모두 오류다 |
| 새 객체 리터럴을 타입 위치에 전달 | 구조 호환성에 더해 초과 속성 검사도 적용되는가 | 구조적으로 호환되면 리터럴 검사도 항상 통과한다 |
| 콜백을 인수로 전달 | 함수 매개변수 타입과 선언 문법, strictFunctionTypes | 함수 매개변수는 항상 공변 또는 항상 반변이다 |
satisfies로 설정 객체 확인 | 타입을 강제로 바꾸지 않고 조건을 검증하는가 | satisfies가 런타임 검증을 해 준다 |
이 표는 인터뷰 답변의 출발점입니다. 각 사례에서 원본 타입, 대상 타입, 값이 만들어진 위치, 컴파일러 옵션을 먼저 말한 다음 결과를 예측하세요. 컴파일 결과만 맞히는 것보다 어느 검사 단계가 작동했는지 구분하는 편이 수정 가능한 설명입니다.
기존 변수의 추가 속성은 왜 허용될까? (Structural assignment)
작은 계약 타입 Pet이 name만 요구한다고 가정해 봅시다. 구조적 타이핑은 값의 정체성보다 사용할 멤버를 확인합니다.
interface Pet {
name: string;
}
const dog = { name: "Mandu", owner: "Jina" };
const pet: Pet = dog; // 허용: dog는 name을 제공한다.
pet을 사용하는 코드는 name만 읽을 수 있습니다. owner가 더 있다는 사실은 이 사용 계약을 깨지 않습니다. 덕분에 큰 객체의 일부를 작은 인터페이스가 요구하는 곳에 전달할 수 있고, 기존 API가 필요 이상의 정확한 객체 모양을 강제하지 않습니다.
그렇다고 모든 extra property가 안전하다는 뜻은 아닙니다. 대상 타입의 필수 필드가 없거나 타입이 맞지 않으면 대입은 실패합니다. 또한 개발자가 속성 이름을 잘못 썼다면 변수에 담긴 순간 오타를 알려줄 기회가 줄어듭니다. 따라서 “초과 속성이 항상 무시된다”가 아니라 “일반적인 구조 호환성은 대상이 요구하는 멤버를 중심으로 검사한다”고 답해야 합니다.
const missingName = { owner: "Jina" };
// const invalid: Pet = missingName; // 오류: 필수 name이 없음
const wrongNameType = { name: 42, owner: "Jina" };
// const alsoInvalid: Pet = wrongNameType; // 오류: name이 string이 아님
타입 경계가 공개 API라면 추가 속성을 무조건 없애려 하기보다, 어떤 속성이 계약의 일부인지 먼저 정하세요. 나중에 서버 응답의 필드가 추가될 수 있는 DTO, 서드파티 데이터, 내부 도메인 객체는 요구 멤버만 소비하는 구조적 타입과 잘 맞을 수 있습니다. 반대로 설정 파일에서 colro 같은 오타를 즉시 잡고 싶다면 새 리터럴 검사가 유용합니다.
새 객체 리터럴의 초과 속성 검사는 별도 단계다 (Fresh literals)
다음 두 표현은 모양이 비슷하지만 타입 검사 문맥이 다릅니다.
interface SquareConfig {
color?: string;
width?: number;
}
const fromVariable = { colour: "red", width: 100 };
const a: SquareConfig = fromVariable; // 구조적으로 호환될 수 있음
// const b: SquareConfig = { colour: "red", width: 100 };
// 오류: 새 리터럴에서 알려지지 않은 colour를 발견
첫 번째는 변수의 추론된 구조를 대상으로 호환성을 확인합니다. 두 번째는 타입이 붙은 위치에 새 리터럴이 직접 나타나므로, TypeScript는 오타일 수 있는 알 수 없는 프로퍼티를 별도로 경고합니다. 이것은 “fresh object literal”이라는 면접 표현으로 자주 설명되지만, 새 객체가 영구히 고정된 별도 타입이라는 뜻은 아닙니다. 어디에 직접 작성했는지가 중요한 판정 조건입니다.
함수 호출에서도 같은 차이를 확인할 수 있습니다.
function createSquare(config: SquareConfig) {
return {
color: config.color ?? "red",
area: (config.width ?? 0) ** 2,
};
}
createSquare(fromVariable); // 일반적인 구조 호환성
// createSquare({ colour: "red", width: 100 }); // 초과 속성 오류
이 오류를 피하려고 임시 변수에 담는 것은 API를 더 정확하게 만드는 해결책이 아닙니다. 오타가 의심되어 검사되는 위치를 우회할 뿐입니다. 정말 의도된 확장 속성이 있다면 인덱스 시그니처, 제네릭 제약, 별도 확장 객체처럼 허용 범위를 계약으로 표현하세요. 무작정 [key: string]: unknown을 넣으면 colro도 유효해져 검사가 약해질 수 있습니다.
satisfies는 값을 변환하지 않고 계약을 확인한다 (Satisfies)
설정 객체를 변수에 저장하면 초과 속성 검사의 위치가 바뀔 수 있습니다. TypeScript 4.9에서 도입된 satisfies 연산자는 값을 지정 타입으로 단언하는 대신 호환성을 검사하며, 타입 주석보다 구체적인 추론 정보를 유지할 수 있는 방법입니다. 다만 문맥적 타이핑이 작동하므로 satisfies를 붙이지 않았을 때와 추론 결과가 언제나 같다고 단정하면 안 됩니다. 예를 들어 { enabled: true } satisfies { enabled: boolean }은 enabled를 리터럴 true로 유지할 수 있어, 뒤에서 false를 대입하면 오류가 납니다. 이 역시 정적 추론이며 런타임 값 검증이 아닙니다.
const theme = {
color: "forest",
width: 320,
} satisfies SquareConfig;
theme.width.toFixed(); // width는 number로 추론됨
다음 오타는 선언 가까이에서 발견됩니다.
const badTheme = {
colour: "forest",
width: 320,
} satisfies SquareConfig; // 오류: colour는 알려진 속성이 아님
공식 사실: satisfies는 런타임에서 객체를 검사하거나 값을 다른 형태로 변환하지 않습니다. 정적 타입 검사를 수행하는 문법입니다. 또한 as SquareConfig와 목적이 다릅니다. 단언은 작성자가 컴파일러에게 호환성을 믿으라고 말하는 반면, satisfies는 표현식에 대해 계약을 검사하고 더 구체적인 추론 정보를 보존하려고 합니다.
면접에서의 적용: “설정 맵의 키와 값이 계약을 만족하는지 검사하면서 각 항목의 구체적인 타입도 유지하고 싶습니다. 그래서 단언보다 satisfies를 검토하겠습니다. 하지만 JSON이나 HTTP 응답은 런타임 값이므로 별도의 검증이 필요합니다.” 이렇게 정적 보장과 실제 입력 검증을 한 문장 안에서 구별하면 타입 시스템의 범위를 과장하지 않습니다.

콜백 분산은 호출자가 넣을 값부터 따져 보기 (Callback variance)
함수 호환성은 반환값과 매개변수에서 방향이 다릅니다. 반환값은 생산자가 돌려주는 값입니다. 대상이 넓은 이벤트를 기대하고 원본이 더 구체적인 이벤트를 반환한다면, 반환값에 대상이 요구하는 필드가 모두 있을 수 있습니다. 반대로 대상이 좌표를 기대하는데 원본이 일반 이벤트만 반환하면 약속을 충족하지 못합니다.
매개변수는 호출자가 전달할 값입니다. 일반 이벤트를 모두 처리하는 핸들러는 클릭 이벤트를 받는 슬롯에서 사용할 수 있습니다. 하지만 클릭 전용 핸들러를 일반 이벤트 슬롯에 넣으면 호출자가 x, y가 없는 값을 전달할 수 있습니다.
type AppEvent = { id: string };
type ClickEvent = AppEvent & { x: number; y: number };
type Handler<T> = (event: T) => void;
declare const handleAny: Handler<AppEvent>;
declare const handleClick: Handler<ClickEvent>;
const clickSlot: Handler<ClickEvent> = handleAny; // 넓은 입력을 받으므로 가능
// const anySlot: Handler<AppEvent> = handleClick; // 위험한 방향
TypeScript 핸드북은 함수 매개변수 비교에서 양방향 호환을 허용하는 실용적 규칙과 그에 따른 비건전성 가능성을 설명합니다. strictFunctionTypes를 켜면 함수 타입의 매개변수 검사가 관련 위치에서 더 엄격하게 반공변적으로 동작합니다. 코드베이스에서 결과를 판단할 때는 “TypeScript는 원래 이렇다”로 끝내지 말고 컴파일러 버전, strict 설정, 함수가 속성 함수인지 메서드 문법인지 확인하세요.
메서드 문법과 함수 속성을 비교하기 (Methods vs properties)
다음 두 인터페이스는 겉보기에는 비슷하지만 strictFunctionTypes의 매개변수 검사에서 차이가 날 수 있습니다. TypeScript는 메서드와 생성자 매개변수 위치에 예외를 두므로, 선언 문법을 실제로 읽어야 합니다.
interface MethodSink<T> {
accept(event: T): void;
}
interface PropertySink<T> {
accept: (event: T) => void;
}
declare const clickMethod: MethodSink<ClickEvent>;
declare const clickProperty: PropertySink<ClickEvent>;
const methodSlot: MethodSink<AppEvent> = clickMethod;
// strictFunctionTypes 환경에서 함수 속성은 더 엄격히 검사됨:
// const propertySlot: PropertySink<AppEvent> = clickProperty;
중요한 한계: 위 주석을 모든 프로젝트에서 같은 컴파일 결과를 보장하는 규칙으로 외우면 안 됩니다. strictFunctionTypes가 활성화되어 있는지, 파라미터가 메서드인지 함수 프로퍼티인지, 라이브러리 선언에 어떤 타입 별칭이 있는지 확인한 뒤 실행해야 합니다. 면접에서는 코드를 컴파일러에 넣기 전에 예상 결과와 그 이유를 말하고, 실제 결과가 다르면 설정이나 문법을 확인하는 태도가 더 안전합니다.
콜백 API를 새로 설계한다면, 더 넓은 인수를 받을 수 있는 소비자를 좁은 슬롯에 전달하는 상황이 안전한지 호출부에서 검증하세요. 공용 라이브러리라면 메서드 문법의 호환성 선택이 기존 사용자를 위한 것인지, 함수 속성이 더 정확한 제약을 제공하는지 팀의 API 기준에 맞춰 결정할 수 있습니다. 한쪽을 모든 경우의 정답으로 선언하지 말고, 허용하려는 호출을 실제 예제로 보여 주세요.
컴파일러 결과를 재현하는 최소 테스트 행렬 (Compiler evidence)
면접 전에는 말로만 결과를 외우지 말고 작은 파일 하나를 만들어 네 축을 바꿔 보세요.
| 축 | 첫 실험 | 기대할 설명 |
|---|---|---|
| 원본 값 | 익명 리터럴과 이름 있는 변수 | 새 리터럴 위치에서 초과 속성 검사가 개입할 수 있다 |
| 검사 문맥 | 변수 대입과 함수 호출 | 구조 호환성은 비슷해도 직접 리터럴 검사는 추가 규칙을 적용한다 |
| 함수 선언 | 메서드와 함수 타입 프로퍼티 | 문법 형태가 함수 매개변수의 검사 규칙에 영향을 준다 |
| 컴파일 옵션 | strictFunctionTypes 켜기와 끄기 | 콜백 호환 결과와 진단이 달라질 수 있다 |
테스트 파일은 한 번에 하나의 원인을 바꾸세요. 여러 진단을 동시에 넣으면 어느 조건이 결과를 바꿨는지 알기 어렵습니다. 성공해야 하는 줄에는 타입 주석을 붙이고, 실패해야 하는 줄에는 @ts-expect-error를 사용해 테스트가 실제로 실패하는지 확인할 수 있습니다. 이 주석이 더 이상 오류를 기대하지 않는 코드 위에 남으면 TypeScript가 불필요한 지시문으로 진단하므로, 부정 테스트의 회귀 탐지에 쓸 수 있습니다.
예를 들어 Pet 인터페이스, dog 변수, SquareConfig 리터럴, 메서드 핸들러와 함수 프로퍼티를 한 파일에 놓고 다음 질문에 답하세요. “변수의 타입은 무엇인가?”, “이 줄에서 대상이 요구하는 필드는 무엇인가?”, “오류가 난다면 구조가 부족해서인가, 초과 속성 검사 때문인가?”, “콜백이면 호출자가 전달할 수 있는 가장 넓은 인수는 무엇인가?” 답한 뒤 실제 컴파일을 실행하고, 예상과 결과가 다르면 tsconfig와 컴파일러 버전을 기록합니다.
로컬 검산은 설치된 TypeScript 5.9.3에서 strict, noEmit을 사용했습니다. 본문 코드 블록 8개를 앞 절의 필요한 선언과 함께 검사했으며, badTheme 블록은 의도한 초과 속성 오류가 발생하는지 확인했습니다. 이어서 독립 부정 사례와 옵션 전환을 비교했습니다. 이 결과는 모든 컴파일러 버전에 대한 보장이 아닙니다.
| 독립 사례 | strictFunctionTypes 켬 | 끔 | 확인하는 경계 |
|---|---|---|---|
| 클릭 전용 함수를 일반 이벤트 슬롯에 대입 | 오류 | 허용 | 함수 매개변수 방향 |
| 함수 프로퍼티인 Sink의 같은 대입 | 오류 | 허용 | 프로퍼티 함수 검사 |
| 메서드 문법인 Sink의 같은 대입 | 허용 | 이 표에서는 별도 실행하지 않음 | 메서드 예외 |
새 리터럴의 잘못된 colour | 오류 | 이 표에서는 별도 실행하지 않음 | 초과 속성 검사 |
옵션을 끄고 통과했다고 호출이 안전해지는 것은 아닙니다. 일반 이벤트 {id:"x"}를 클릭 전용 함수에 넘기는 반례는 그대로 남습니다. 반대로 컴파일이 거절되었다는 관찰만으로 런타임 JSON 검증까지 끝났다고 설명할 수도 없습니다.
자주 나오는 꼬리질문에 답하는 순서 (Follow-up reasoning)
“TypeScript는 명목적 타입인가요?” 구조적 타이핑을 기본으로 설명하되, 클래스의 private 또는 protected 멤버 등 호환성에 추가 조건을 주는 경우가 있다는 예외를 덧붙이세요. 한 개념을 언어 전체의 모든 규칙으로 확대하지 않는 게 중요합니다.
“초과 속성 오류를 없애려면 어떻게 하나요?” 먼저 오타인지 확인합니다. 확장 속성이 정말 계약의 일부라면 필요한 키만 제한하는 인덱스 시그니처, 제네릭 보존, 별도 확장 필드 중 무엇이 소비자에게 가장 안전한지 선택합니다. 임시 변수나 단언으로 검사를 우회하는 것만으로는 런타임의 추가 데이터가 검증되지 않습니다.
“함수 매개변수는 왜 반공변이라고 하나요?” 이름을 먼저 말하지 말고 호출을 제시합니다. 일반 슬롯의 호출자는 일반 이벤트를 넣을 권리가 있으므로 클릭 전용 함수는 안전하지 않습니다. 그 뒤 strictFunctionTypes와 메서드 예외를 밝혀 용어를 조건과 함께 설명하세요.
“이 타입이면 API 데이터가 안전한가요?” 아닙니다. TypeScript 타입은 일반적으로 컴파일 시 계약입니다. 네트워크에서 온 JSON이 실제로 계약을 따르는지 확인하려면 런타임 파서나 검증 로직이 필요합니다. 타입 단언은 그 검증을 대신하지 않습니다.
이 질문들은 특정 회사의 기출이라는 뜻이 아닙니다. PracHub 연습 링크는 타입 계약과 UI/API 구현을 연결하는 연습 대상이며, 채용 결과를 예측하거나 동일한 문제가 출제된다고 보장하지 않습니다.
30분 실전 연습: 결과보다 근거를 말하기 (Mock rehearsal)
첫 5분에는 Pet과 SquareConfig를 정의하고 네 개의 판정 사례를 직접 만드세요. 다음 10분에는 기존 변수, 새 리터럴, 함수 인수, satisfies를 각각 시험하고 어떤 문맥이 검사를 일으켰는지 말합니다. 이어서 10분 동안 AppEvent와 ClickEvent 콜백을 메서드/함수 프로퍼티로 나누고, strictFunctionTypes를 전환하며 결과를 비교하세요. 마지막 5분에는 미니 API에서 요청 함수 타입과 JSON 검증을 분리해 설명합니다.
각 사례의 기록 형식은 짧게 통일하면 됩니다.
- 원본 값과 대상 타입을 적는다.
- 직접 작성한 새 객체 리터럴인지 기존 변수인지 표시한다.
- 대상이 요구하는 필드를 나열한다.
- 함수라면 호출자가 전달할 수 있는 입력 범위를 말한다.
- 컴파일 옵션과 선언 문법을 확인하고 예상 결과를 기록한다.
- 런타임 데이터라면 타입 검사 외에 필요한 검증을 적는다.
이 연습은 “초과 속성은 허용되나요?”라는 예/아니오 질문을 반복하는 대신, 각 진단이 어떤 위치에서 생기는지 구체적으로 말하게 합니다. 실제 면접에서도 바로 코드를 고치기 전에 하나의 허용 호출과 하나의 거부되어야 하는 호출을 만들면, 계약이 원하는 방향을 먼저 확인할 수 있습니다.
PracHub에서 연결해 연습할 질문 (Practice questions)
다음 질문은 타입 설계, 콜백 계약, 런타임 경계가 실제 프론트엔드 구현과 만나는 지점입니다. 특정 고용주의 출제 예측으로 해석하지 마세요.
| PracHub 질문 | 이 글과 연결되는 연습 |
|---|---|
| Implement a Publish-Subscribe Event Bus in JavaScript or TypeScript | 이벤트 이름과 payload 타입의 관계, 핸들러 계약 |
| Validate Nested Configuration Objects | 정적 구조 설명과 실제 입력 검증을 구분하기 |
| Implement debounced autocomplete component | 콜백·요청 결과·UI 상태의 타입 경계 |
| Debug a Frontend Component That Shows Wrong API Results | 서버 응답과 현재 요청의 관계 및 stale-state 방지 |
| Implement list, form, and API rendering with pitfalls | 폼·API의 상태 모델과 반복 렌더링 계약 |
먼저 기존 객체 변수를 Pet에 대입하는 경우와 새 리터럴을 직접 전달하는 경우를 하나씩 풀어 보세요. 각 답에는 예상 타입 오류, 소비 코드가 실제로 요구하는 필드, 확인할 컴파일 옵션을 함께 적습니다. 그다음 이벤트 버스에서 좁은 핸들러가 넓은 이벤트 슬롯에 들어갈 때 호출자가 어떤 값을 전달할 수 있는지 반례를 말해 보세요.
결론: 검사 결과는 위치와 계약으로 설명하기 (Contract summary)
구조적 타이핑은 요구된 멤버가 맞는지를 묻는 기본 규칙입니다. 초과 속성 검사는 새 리터럴에서 오타를 찾아주는 추가 진단이고, 함수 호환성은 호출자의 가능한 입력과 함수의 선언 문법, 컴파일 옵션을 함께 고려합니다. 따라서 “같은 모양이니 항상 통과” 또는 “추가 속성이면 모두 실패”라고 단정하지 마세요.
인터뷰 답변은 값의 출처 → 대상의 요구 멤버 → 함수 입력/출력 방향 → 옵션과 선언 문법 → 런타임 검증 순서로 짧게 정리하면 재현 가능해집니다. 더 넓은 프론트엔드 질문에서 이 구분을 응용해 본 뒤, 다른 타입 문제에서도 컴파일러 결과를 가정이 아니라 증거로 설명하세요.
Sources and Further Reading
- TypeScript Handbook: Type Compatibility — 구조적 호환성과 함수 매개변수 비교의 공식 설명.
- TypeScript Handbook: Object Types — 객체 타입과 초과 속성 검사를 포함한 객체 리터럴 문맥.
- TypeScript 2.6 Release Notes —
strictFunctionTypes및 메서드 매개변수 예외의 공식 설명. - TypeScript 4.9 Release Notes —
satisfies연산자의 도입과 추론 보존 목적. - TypeScript TSConfig: strictFunctionTypes — 현재 컴파일러 옵션의 범위와 효과.
근거 경계: TypeScript 규칙은 공식 문서에서 확인했습니다. 후보자 보고는 수집하지 않았습니다. 판정 표·미니 API·연습 절차는 기술 사실을 면접 준비에 적용한 교육용 예시이며, 특정 회사의 절차나 출제 빈도를 주장하지 않습니다.
Comments (0)