
안녕하세요! 오늘은 TypeScript에서 Partial 타입과 스프레드 연산자를 함께 사용할 때 발생할 수 있는 함정과 이를 해결하는 방법에 대해 알아보겠습니다.
우리는 종종 객체의 일부 속성만 업데이트하기 위해 Partial<T> 타입을 사용합니다. 이때 스프레드 연산자를 함께 사용하면 코드가 간결해지지만, 의도치 않은 타입 문제가 발생할 수 있습니다.
다음과 같은 간단한 예제를 살펴봅시다:
interface User {
id: number;
name: string;
email: string;
age: number;
}
function updateUser(userId: number, userData: Partial<User>) {
// 기존 사용자 데이터를 가져온다고 가정
const existingUser: User = {
id: userId,
name: "홍길동",
email: "hong@example.com",
age: 30
};
// 문제가 되는 코드
const updatedUser: User = {
...existingUser,
...userData,
updatedAt: new Date()
};
// 저장 로직...
console.log(updatedUser);
}
// 사용 예시
updateUser(1, { name: "김철수", age: undefined });
위 코드에서 updatedUser는 어떤 값을 가지게 될까요?
기대하는 결과는 아마도 이럴 것입니다:
{
id: 1,
name: "김철수",
email: "hong@example.com",
age: 30, // undefined가 무시되길 기대
updatedAt: [Date 객체]
}
하지만 실제 결과는:
{
id: 1,
name: "김철수",
email: "hong@example.com",
age: undefined, // undefined가 덮어씌워졌습니다!
updatedAt: [Date 객체]
}
이렇게 됩니다. age 속성이 undefined로 덮어씌워졌고, 이는 우리가 의도한 바가 아닙니다!
이 문제는 TypeScript의 타입 시스템과 JavaScript의 런타임 동작 차이에서 발생합니다.
TypeScript의 Partial 타입: Partial<User>는 User 인터페이스의 모든 속성을 선택적(optional)으로 만듭니다. 즉, { name?: string; email?: string; age?: number; }와 같은 타입이 됩니다.
JavaScript의 스프레드 연산자: 스프레드 연산자는 객체의 모든 속성을 펼쳐서 새 객체에 복사합니다. 이때 undefined 값도 함께 복사됩니다.
TypeScript는 타입 체크만 수행하고 실제 런타임 동작은 JavaScript의 규칙을 따릅니다. 따라서 Partial 타입으로 선언된 객체에 undefined 값이 있으면, 스프레드 연산자는 이를 그대로 복사합니다.
이 문제를 해결하기 위한 가장 좋은 방법은 undefined 값을 필터링하는 것입니다:
function updateUser(userId: number, userData: Partial<User>) {
const existingUser: User = {
id: userId,
name: "홍길동",
email: "hong@example.com",
age: 30
};
// undefined 값을 필터링하는, 보다 안전한 방법
const filteredUserData = Object.fromEntries(
Object.entries(userData).filter(([_, value]) => value !== undefined)
);
const updatedUser: User = {
...existingUser,
...filteredUserData,
updatedAt: new Date()
};
console.log(updatedUser);
}
이렇게 하면 undefined 값이 있는 속성은 새 객체에 복사되지 않으므로, 우리가 의도한대로 기존 값이 유지됩니다.
import _ from 'lodash';
const filteredUserData = _.pickBy(userData, value => value !== undefined);
function updateUser(userId: number, userData: Partial<User>) {
const existingUser = /* ... */;
// 각 필드를 명시적으로 검사
const updatedUser: User = {
...existingUser,
...(userData.name !== undefined ? { name: userData.name } : {}),
...(userData.email !== undefined ? { email: userData.email } : {}),
...(userData.age !== undefined ? { age: userData.age } : {}),
updatedAt: new Date()
};
}
이 방법은 필드가 많으면 번거롭지만, 타입 안전성을 높이고 코드 의도를 명확히 표현할 수 있습니다.
개발 요구사항에 따라 null과 undefined를 다르게 처리해야 할 수도 있습니다:
undefined: 필드를 업데이트하지 않음 (기존 값 유지)null: 필드 값을 명시적으로 null로 설정이 경우에는:
const filteredUserData = Object.fromEntries(
Object.entries(userData).filter(([_, value]) => value !== undefined)
);
위 코드는 undefined 값만 필터링하고 null 값은 유지합니다.
TypeScript의 Partial 타입과 스프레드 연산자를 함께 사용할 때는 undefined 값에 주의해야 합니다. 이러한 함정을 이해하고 적절한 해결 방법을 적용하면 코드의 신뢰성과 안정성을 높일 수 있습니다.
개인적으로는 Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined)) 패턴을 사용하는 것이 가장 명확하고 안전한 방법이라고 생각합니다. 이 패턴은 의도를 명확히 표현하고 타입 안전성을 보장하며, 어떤 경우에도 예상대로 동작합니다.
TypeScript의 타입 시스템의 특성을 이해하고 JavaScript의 런타임 동작도 함께 고려하는 것이 중요합니다. 이 두 가지를 모두 이해하면 보다 견고한 코드를 작성할 수 있습니다.
TypeScript 5.0부터는 satisfies 연산자를 활용하여 이런 문제를 보다 타입 안전하게 해결할 수 있습니다:
function updateUser(userId: number, userData: Partial<User>) {
const existingUser: User = /* ... */;
const filteredUserData = Object.fromEntries(
Object.entries(userData).filter(([_, value]) => value !== undefined)
) satisfies Partial<User>;
const updatedUser: User = {
...existingUser,
...filteredUserData,
updatedAt: new Date()
};
}
satisfies 연산자를 사용하면 Object.fromEntries의 결과가 Partial<User> 타입에 맞는지 컴파일 타임에 검사할 수 있어 더 안전합니다.
이처럼 TypeScript는 계속 발전하고 있으며, 새로운 기능을 활용하면 더 안전하고 표현력 있는 코드를 작성할 수 있습니다.
위 블로그는 Partial 타입 변수를 스프레드 연산자 사용할 때, 주의 해야할 예제 코드를 주고 claude.ai에 블로그 작성 요청을 통해 나온 글 입니다.