TypeScript 면접 질문
TypeScript 인터뷰 질문은 정적 타이핑, 타입 추론 및 고급 타입 시스템 기능에 대한 이해도를 테스트합니다. 프론트엔드, 백엔드 및 풀스택 역할에서 확장 가능하고 유지보수 가능한 코드를 작성할 수 있는지 확인하기 위해 자주 묻습니다. 개념 설명과 실습 코딩 문제가 혼합되어 나옵니다.
TypeScript 면접에서 다루는 내용
타입과 인터페이스
타입 별칭과 인터페이스의 차이점과 각각을 언제 사용할지 이해합니다. 객체 타입, 함수 시그니처, 선언 병합을 다룹니다.
제네릭
제네릭 매개변수를 사용하여 재사용 가능하고 타입 안전한 컴포넌트와 함수를 만드는 방법을 알아야 합니다. 제약 조건과 기본 타입을 포함합니다.
타입 추론과 내로잉
TypeScript가 타입을 추론하고 조건문, 판별 유니온, 타입 가드를 사용하여 타입을 좁히는 방법을 보여줍니다.
유틸리티 및 조건부 타입
내장 유틸리티 타입(예: Partial, Pick)을 활용하고 사용자 정의 조건부 타입을 만들어 타입을 동적으로 변환합니다.
샘플 TypeScript 면접 질문
- TypeScript에서 `type`과 `interface`의 차이점을 설명하세요. 각각 언제 사용하나요?좋은 답변이 다루는 것
- type은 유니온, 교차, 매핑 등 다양한 타입 조합이 가능하지만 interface는 객체 형태만 가능
- interface는 선언 병합(declaration merging)이 지원되어 확장에 유리
- type은 computed property나 conditional types 사용 가능
- interface는 객체 지향적 설계에, type은 복잡한 타입 조합에 적합
샘플 답변 보기
TypeScript에서 `type`과 `interface`는 객체 타입을 정의하는 주요 방법입니다. `interface`는 선언 병합(declaration merging)이 가능하여 동일한 이름으로 여러 번 선언하면 자동으로 병합되므로, 라이브러리 확장이나 전역 타입 보강에 유리합니다. 반면 `type`은 유니온 타입, 인터섹션 타입, 매핑된 타입, 조건부 타입 등 더 다양한 타입 조합이 가능하고 computed property 이름을 사용할 수 있습니다. 일반적으로 라이브러리나 공개 API의 객체 타입은 `interface`로 정의하는 것이 관례이며, 내부 구현이나 복잡한 타입 로직은 `type`을 사용합니다. 또한 상속이 필요한 경우 `interface extends`가 더 직관적입니다. 단, 두 방식 모두 객체 타입을 정의할 수 있지만 `type`은 선언 병합이 안 되므로 확장성을 고려해야 합니다.
- 모든 프로퍼티와 중첩 프로퍼티를 읽기 전용으로 만드는 제네릭 `DeepReadonly<T>` 타입을 구현하세요.좋은 답변이 다루는 것
- 객체의 모든 프로퍼티(중첩 포함)를 readonly로 변환
- 재귀적(recursive) 매핑 사용
- 배열, Map, Set 등도 처리해야 함
샘플 답변 보기
`DeepReadonly<T>`는 객체 타입 T의 모든 프로퍼티와 중첩된 객체의 프로퍼티를 재귀적으로 읽기 전용으로 만드는 제네릭 타입입니다. 기본 아이디어는 매핑된 타입(mapped type)을 사용하여 각 프로퍼티를 `readonly`로 만들고, 값이 객체인 경우 `DeepReadonly`를 다시 적용하는 것입니다. 단, 원시 타입이나 함수는 그대로 유지하고 배열이나 튜플도 재귀적으로 처리해야 합니다. 이 구현은 간단하지만, 순환 참조(circular reference)가 있는 타입에서는 무한 재귀에 빠질 수 있으므로 주의해야 합니다. 실제 사용 시에는 유틸리티 타입의 일부로 활용됩니다.
참고 코드typescript type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? T[P] extends Function ? T[P] : DeepReadonly<T[P]> : T[P]; }; // 사용 예 interface User { name: string; address: { city: string; zip: number; }; } type ReadonlyUser = DeepReadonly<User>; // { readonly name: string; readonly address: { readonly city: string; readonly zip: number; } } // 복잡도: O(N) (N은 프로퍼티 수, 재귀 깊이) - 타입 서술어를 기반으로 필터링하는 제네릭 함수 `filterArray<T>(arr: T[], predicate: (item: T) => item is T): T[]`를 작성하세요.좋은 답변이 다루는 것
- 타입 서술어(predicate)로 타입 가드 역할
- 반환 타입이 `item is T`여야 함
- 제네릭을 사용하여 다양한 타입 지원
샘플 답변 보기
`filterArray` 함수는 배열과 타입 서술어(predicate)를 받아, 조건에 맞는 요소들만 필터링하여 반환합니다. 타입 서술어는 `(item: T) => item is T` 형태로, 반환 타입이 `boolean`이 아니라 `item is T`여야 TypeScript가 타입을 좁힐 수 있습니다. 이 함수는 표준 `Array.prototype.filter`와 유사하지만, 타입 안전성을 보장하기 위해 제네릭을 사용합니다. 단, 실제로는 `filter` 메서드 자체에 타입 가드를 전달할 수 있으므로 꼭 필요한 경우에만 구현합니다.
참고 코드typescript function filterArray<T>( arr: T[], predicate: (item: T) => item is T ): T[] { return arr.filter(predicate); } // 사용 예 const items: (string | null)[] = ['a', null, 'b', null]; const strings = filterArray(items, (item): item is string => item !== null); // strings: string[] // 시간 복잡도: O(N), 공간 복잡도: O(N) - `unknown` 타입이란 무엇이며 `any`와 어떻게 다른가요? 각각을 언제 사용할지 예를 들어 설명하세요.좋은 답변이 다루는 것
- unknown은 타입 안전성을 보장하기 위한 top type
- any는 모든 타입을 허용하지만 타입 검사 비활성화
- unknown은 사용 전에 타입 좁히기 필요
- any는 레거시 코드 이전이나 임시 대처에 사용
샘플 답변 보기
`unknown`과 `any`는 모든 값을 할당받을 수 있는 점에서 유사하지만, `unknown`은 타입 안전성을 유지하는 반면 `any`는 타입 검사를 완전히 비활성화합니다. `unknown` 타입의 변수는 다른 타입에 할당할 수 없으며, 사용하기 전에 typeof나 instanceof 등으로 타입을 좁혀야 합니다. 반면 `any`는 어떤 타입에도 할당 가능하고 메서드 호출도 허용하므로, 타입 오류를 감춥니다. 따라서 외부 데이터(예: API 응답)를 처리할 때는 `unknown`을 사용하여 타입 검사를 강제하는 것이 좋고, `any`는 마이그레이션 중 긴급한 경우나 타입 정보가 전혀 없을 때 제한적으로 사용합니다. 예를 들어, `JSON.parse`의 반환 타입은 `any`이지만 실제로는 `unknown`으로 캐스팅하는 것이 더 안전합니다.
- 사용자 객체의 Promise를 반환하는 비동기 함수를 어떻게 타입 지정하나요? resolve와 reject 상태를 처리하는 방법을 보여주세요.좋은 답변이 다루는 것
- Promise<T>의 T는 resolve되는 값의 타입
- reject는 보통 Error 타입으로 처리
- async 함수는 항상 Promise<ReturnType> 반환
샘플 답변 보기
비동기 함수의 반환 타입은 `Promise<T>`로 지정하며, T는 resolve되는 값의 타입입니다. 예를 들어, 사용자 객체를 반환하는 함수는 `Promise<User>`로 타입을 지정합니다. reject 상태는 주로 `Error` 또는 `unknown` 타입으로 처리하며, TypeScript에서는 reject 타입을 강제하지 않으므로, 필요하다면 `Promise<T>` 대신 유틸리티 타입을 만들어 명시할 수 있습니다. 실제로는 async 함수 내에서 try/catch로 오류를 잡거나, 호출부에서 `.catch()`로 처리합니다. 또한, async 함수는 항상 `Promise`를 반환하므로 반환 타입을 생략해도 TypeScript가 추론합니다.
참고 코드typescript interface User { id: number; name: string; } // resolve 타입만 명시 async function fetchUser(id: number): Promise<User> { const response = await fetch(`/users/${id}`); if (!response.ok) { throw new Error('Failed to fetch'); } return response.json(); } // 사용 예 fetchUser(1) .then(user => console.log(user.name)) .catch(error => console.error(error)); // reject를 명시적으로 처리하려면 유니온 타입 사용 (권장되지 않음) // type PromiseWithReject<T, E = Error> = Promise<T>; // 또는 neverthrow 같은 라이브러리 활용 - 이벤트 이름과 페이로드 타입을 강제하는 타입 안전한 이벤트 이미터 클래스를 설계하세요.좋은 답변이 다루는 것
- 제네릭을 사용하여 이벤트 이름과 핸들러 타입 연결
- on, emit, off 메서드 구현
- Map 자료구조로 핸들러 저장
샘플 답변 보기
타입 안전한 이벤트 이미터는 객체 타입 `Events`를 제네릭으로 받아, 각 이벤트 이름에 해당하는 핸들러의 타입을 연결합니다. `on` 메서드는 이벤트 이름과 핸들러를 등록하고, `emit`은 페이로드와 함께 핸들러를 호출합니다. 타입 검사를 통해 존재하지 않는 이벤트나 잘못된 페이로드를 컴파일 타임에 걸러냅니다. 구현 시 핸들러 배열을 저장하기 위해 `Map`을 사용하고, 제네릭에서 `keyof Events`를 활용합니다. 단, 오버로딩이 복잡할 수 있으므로 단순화된 설계가 중요합니다.
참고 코드typescript type EventHandler<T> = (payload: T) => void; class EventEmitter<Events extends Record<string, unknown>> { private handlers = new Map<keyof Events, EventHandler<Events[keyof Events]>[]>(); on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void { const existing = this.handlers.get(event) || []; existing.push(handler as EventHandler<Events[keyof Events]>); this.handlers.set(event, existing); } emit<K extends keyof Events>(event: K, payload: Events[K]): void { const handlers = this.handlers.get(event); if (handlers) { handlers.forEach(handler => handler(payload as Events[keyof Events])); } } off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void { const existing = this.handlers.get(event); if (existing) { this.handlers.set( event, existing.filter(h => h !== handler) ); } } } // 사용 예 interface AppEvents { userLogin: { userId: number }; userLogout: { userId: number }; } const emitter = new EventEmitter<AppEvents>(); emitter.on('userLogin', (payload) => { console.log(payload.userId); }); emitter.emit('userLogin', { userId: 1 }); // emitter.emit('invalid', {}); // 오류: 'invalid'는 이벤트에 없음 - `keyof`와 `typeof` 연산자가 어떻게 작동하는지 설명하세요. 동적 타입을 만드는 예제를 제시하세요.좋은 답변이 다루는 것
- keyof는 객체의 키를 유니온 타입으로 반환
- typeof는 값의 타입을 추출
- 둘을 조합하여 동적 타입 생성 가능
샘플 답변 보기
`keyof T`는 객체 타입 T의 모든 키를 문자열 리터럴 유니온 타입으로 반환합니다. 예를 들어, `interface Person { name: string; age: number }`에서 `keyof Person`은 `'name' | 'age'`입니다. `typeof`는 변수의 타입을 추출하여 타입 컨텍스트에서 사용할 수 있습니다. `typeof obj`는 obj의 런타임 타입을 반환합니다. 이 둘을 조합하면 동적 타입을 만들 수 있습니다. 예를 들어, 객체를 받아 그 키 중 하나를 반환하는 함수의 타입을 정의할 때 `function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]`와 같이 사용합니다. 또한 `typeof`를 이용해 상수 객체의 타입을 추출하는 패턴도 자주 사용됩니다.
참고 코드typescript const colors = { red: '#ff0000', green: '#00ff00', blue: '#0000ff' } as const; // typeof colors -> { readonly red: "#ff0000"; readonly green: "#00ff00"; readonly blue: "#0000ff"; } type ColorKeys = keyof typeof colors; // 'red' | 'green' | 'blue' type ColorValues = typeof colors[ColorKeys]; // '#ff0000' | '#00ff00' | '#0000ff' function getColor(key: ColorKeys): ColorValues { return colors[key]; } - 매핑된 타입을 사용하여 객체 타입 `T`의 모든 불리언 프로퍼티를 문자열 리터럴 'true' | 'false'로 변환하는 `FlagProperties<T>` 타입을 만드세요.좋은 답변이 다루는 것
- 매핑된 타입으로 객체의 프로퍼티 순회
- 조건부 타입으로 boolean 타입 체크
- 변환 결과는 'true' | 'false' 리터럴 유니온
샘플 답변 보기
`FlagProperties<T>` 타입은 객체 타입 T의 모든 프로퍼티 중 값이 `boolean`인 것만 찾아, 해당 프로퍼티의 타입을 `'true' | 'false'` 리터럴 유니온으로 변경합니다. 매핑된 타입에서 `[P in keyof T]`로 각 프로퍼티를 순회하고, `T[P] extends boolean` 조건부 타입으로 boolean인지 검사합니다. boolean이면 `'true' | 'false'`로, 아니면 `T[P]`를 그대로 반환합니다. 이때 `boolean` 자체는 `true | false`의 유니온이므로 `extends boolean`으로 충분히 체크됩니다. 단, `boolean` 리터럴(`true` 또는 `false`)은 그냥 `boolean`으로 취급되므로 주의가 필요합니다.
참고 코드typescript type FlagProperties<T> = { [P in keyof T]: T[P] extends boolean ? 'true' | 'false' : T[P]; }; // 사용 예 interface Config { debug: boolean; verbose: boolean; port: number; host: string; } type FlagConfig = FlagProperties<Config>; // { // debug: 'true' | 'false'; // verbose: 'true' | 'false'; // port: number; // host: string; // } // 시간 복잡도: O(N) (N은 프로퍼티 수)
준비 방법
- 유틸리티 타입(예: Partial, Pick, ReturnType)을 처음부터 구현해보며 연습하세요.
- 구조적 타이핑(덕 타이핑)과 그것이 타입 호환성에 미치는 영향을 이해하세요.
- 판별 유니온과 브랜디드 타입과 같은 고급 패턴을 마스터하세요.
- TypeScript 플레이그라운드와 공식 챌린지(예: type challenges)를 사용하여 실력을 연마하세요.
- 실제 TypeScript 코드베이스(예: React 또는 Node.js 라이브러리)를 검토하여 패턴을 실제로 확인하세요.
자주 묻는 질문
React 직무를 위해 TypeScript를 알아야 하나요?
네, 대부분의 최신 React 코드베이스는 TypeScript를 사용하여 초기에 오류를 잡고 개발자 경험을 향상시킵니다.
인터뷰를 위해 TypeScript를 어떻게 연습할 수 있나요?
TypeScript 플레이그라운드 사용, GitHub의 type-challenges 해결, 작은 프로젝트 구현 등을 해보세요.
TypeScript 인터뷰에서 흔한 실수는 무엇인가요?
`any`를 과도하게 사용하거나, strict 모드를 무시하거나, 조건부 타입의 작동 방식을 이해하지 못하는 것입니다.
TypeScript는 배우기 어렵나요?
기초는 쉽지만 조건부 타입과 템플릿 리터럴 타입 같은 고급 기능은 연습이 필요합니다.
타입 정의를 외워야 하나요?
암기보다 개념 이해에 집중하세요. 제네릭이나 유틸리티 타입을 언제 사용할지 아는 것이 더 중요합니다.
즉각적인 AI 피드백으로 TypeScript 질문 연습하기
이력서를 업로드하고 맞춤형 모의 면접을 받아 무엇을 개선해야 할지 정확히 확인하세요 — 무료로 시작하세요.