[TypeScript] 타입 기반 Form/Response 매퍼 설계

Chan의 기술 블로그·2025년 11월 12일

TypeScript

목록 보기
9/10

이 글은 Chat GPT로 TypeScript를 공부하며 정리한 글이다.

1. 문제 의식 — 깊은 API 응답과 Form 모델 간의 불일치

실무에서 API 응답 타입은 다음과 같이 깊고 중첩된 구조를 갖기 쉽다.

interface ApiResponse {
  status: string;
  data: {
    user: {
      id: number;
      profile: {
        name: string;
        email: string;
      };
    };
    posts: {
      id: number;
      title: string;
    }[];
  };
}

하지만 실제 <form>에서는 이런 깊은 구조를 그대로 다루기 어렵다.

  • 입력 값 접근: "user.profile.name"
  • 업데이트: setValue("user.profile.email", ...)
  • 서버 전송 시 다시 원래 구조로 감싸기

이 과정을 매번 수동으로 작성하면 반복적이며, 타입 안정성도 낮다.

목표:
하나의 API 타입 → 폼 구조 / 검증 / 서버 전송 모델까지 자동으로 생성

2. DeepExtract — API 응답에서 “핵심 데이터”만 추출

API 응답은 보통 data 안에 핵심 값이 있다.
이를 자동으로 추출하는 타입이 필요하다.

type DeepExtract<T> =
  T extends Promise<infer U> ? DeepExtract<U> :
  T extends { data: infer D } ? DeepExtract<D> :
  T;

적용:

type Data = DeepExtract<ApiResponse>;

결과:

/*
{
  user: { id: number; profile: { name: string; email: string } };
  posts: { id: number; title: string }[];
}
*/

3. Flatten — 중첩 객체를 “문자열 경로 기반 타입”으로 평탄화

폼 필드 이름은 "user.profile.name"처럼 문자열 경로가 적합하다.

안전한 Flatten 구현

type IsPlainObj<T> = T extends object
  ? T extends any[]
    ? false
    : T extends Function
      ? false
      : true
  : false;

✔ 배열/함수/Date 같은 타입이 flatten되는 것을 방지

개선된 Flatten

type Flatten<T, P extends string = ''> = {
  [K in keyof T & string]:
    IsPlainObj<T[K]> extends true
      ? Flatten<T[K], `${P}${K}.`>
      : { [key in `${P}${K}`]: T[K] };
}[keyof T & string];

적용:

type FormFields = Flatten<DeepExtract<ApiResponse>>;

결과:

/*
{
  'user.id': number;
  'user.profile.name': string;
  'user.profile.email': string;
  'posts': { id: number; title: string }[];
}
*/

4. UnionToIntersection — Flatten 결과를 하나의 객체로 병합

Flatten은 조각 객체들의 유니온을 생성한다.
예: { 'user.id': number } | { 'user.profile.name': string } | ...

이를 하나의 객체로 합쳐야 한다.

type UnionToIntersection<U> =
  (U extends any ? (x: U) => void : never) extends
  (x: infer I) => void ? I : never;

5. Flat<T> — 평탄화 객체 완성

type Flat<T> = UnionToIntersection<Flatten<T>>;

6. ExpandType — 평탄화된 키를 다시 중첩 구조로 복원

type ExpandType<T> = UnionToIntersection<{
  [K in keyof T & string]:
    K extends `${infer Head}.${infer Tail}`
      ? { [P in Head]: ExpandType<{ [Q in Tail]: T[K] }> }
      : { [P in K]: T[K] }
}[keyof T & string]>;

적용 예:

type Expanded = ExpandType<{
  'user.profile.name': string;
  'user.profile.email': string;
}>;

결과 (병합 포함):

/*
{
  user: {
    profile: {
      name: string;
      email: string;
    };
  };
}
*/

7. FormMapper — API ↔ Form 타입 자동 연결

type FormMapper<T> = {
  form: Flat<DeepExtract<T>>;
  response: DeepExtract<T>;
  expand: ExpandType<Flat<DeepExtract<T>>>;
};

8. React 예시 — 타입 기반 자동 완성 Form

type UserForm = FormMapper<ApiResponse>["form"];

function UserProfileForm() {
  const [form, setForm] = useState<Partial<UserForm>>({});

  return (
    <>
      <input
        name="user.profile.name"
        value={form["user.profile.name"] ?? ""}
        onChange={e =>
          setForm(prev => ({
            ...prev,
            "user.profile.name": e.target.value
          }))
        }
      />

      <input
        name="user.profile.email"
        value={form["user.profile.email"] ?? ""}
        onChange={e =>
          setForm(prev => ({
            ...prev,
            "user.profile.email": e.target.value
          }))
        }
      />
    </>
  );
}
  • name 경로 자동 완성
  • 잘못된 필드 입력시 컴파일러가 오류로 잡아줌

9. Form → Response 역매핑도 자동

type ApiData = ExpandType<Flat<DeepExtract<ApiResponse>>>;

10. 실무 활용 시나리오

시나리오설명
Form 라이브러리RHF/Formik의 name 자동 생성
DTO 자동 생성서버 응답 → Form 모델 자동 변환
역매핑Form 데이터 → 서버 전송 구조 자동 감싸기
Validation필드 단위 타입 기반 검증 자동화

✔ 최종 한 문장 요약

"API 타입 하나로 form 구조·검증·역매핑까지 자동화한다."

profile
퍼블리셔에서 프론트앤드로 전향하기

0개의 댓글