TypeScript의 Partial 타입과 스프레드 연산자 함정

오픈소스·2025년 4월 5일
post-thumbnail

안녕하세요! 오늘은 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의 런타임 동작 차이에서 발생합니다.

  1. TypeScript의 Partial 타입: Partial<User>User 인터페이스의 모든 속성을 선택적(optional)으로 만듭니다. 즉, { name?: string; email?: string; age?: number; }와 같은 타입이 됩니다.

  2. 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 값이 있는 속성은 새 객체에 복사되지 않으므로, 우리가 의도한대로 기존 값이 유지됩니다.

다른 방법

  1. lodash의 _.pickBy 사용:
import _ from 'lodash';

const filteredUserData = _.pickBy(userData, value => value !== undefined);
  1. 명시적으로 각 필드를 검사:
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 값은 어떻게 처리할까?

개발 요구사항에 따라 nullundefined를 다르게 처리해야 할 수도 있습니다:

  • 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 이상에서의 개선

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에 블로그 작성 요청을 통해 나온 글 입니다.

0개의 댓글