인턴

[ Troubleshooting🛠️ ] Zod 스키마 검증과 데이터 처리

choijming21 2025. 3. 28. 12:11

1.  문제 발생❓

React 폼 컴포넌트를 여러 곳에서 재사용하면서 발생한 문제입니다:

< 배경 >

  • 같은 입력 폼을 여러 페이지에서 재사용해야 함
  • 각 페이지별로 자료 유형(materialType)만 다름
  • 자료 유형은 4가지로 고정: case-studies, company-intro, 2025-dxreport, digital-skill-standard

 

< 구현 방식 >

  • 폼 컴포넌트에 material prop을 전달해 구분
  • 폼 제출 시 스프레드 연산자를 사용해 formData에 materialType 추가:
body: JSON.stringify({ ...formData, materialType: material })
  • 문제점: 폼 데이터를 서버에 전송할 때 materialType 필드가 페이로드에는 있지만, 검증 후의 데이터에서는 사라지는 문제가 발생

 

< 초기 설정 코드 >

 

클라이언트 측 코드 (RequestMaterialForm.tsx):

function RequestMaterialForm({ material }: { material: string }) {
  const form = useForm<RequestMaterialFormValues>({
    resolver: zodResolver(requestMaterialFormSchema),
    defaultValues: DEFAULT_VALUES,
  });

  const onSubmit: SubmitHandler<RequestMaterialFormValues> = async (formData) => {
    // ...
    const request = new Request("/api/request-materials", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ ...formData, materialType: material }),
    });
    // ...
  };

  // 나머지 폼 렌더링 코드...
}

 

스키마 정의(schema.ts):

export const requestMaterialFormSchema = z.object({
  name: inquiryFormSchema.shape.name,
  company: inquiryFormSchema.shape.company,
  department: inquiryFormSchema.shape.department,
  phoneNumber: inquiryFormSchema.shape.phoneNumber,
  email: inquiryFormSchema.shape.email,
  acquisitionChannel: inquiryFormSchema.shape.acquisitionChannel,
  // materialType은 정의되지 않음
});

 

API 엔드포인트(route.ts):

export async function POST(request: Request) {
  const payload = await request.json();

  console.log("페이로드 =>", payload);

  try {
    const parsedResult = requestMaterialFormSchema.safeParse(payload);

    if (!parsedResult.success) {
      return NextResponse.json({ error: "invalid request" }, { status: 400 });
    }

    const { data } = parsedResult;

    console.log("데이터 =>", data);
    // ...
  } catch (error) {
    // ...
  }
}

 

콘솔 로그 결과:

페이로드 => {
  name: 'dd',
  company: 'dd',
  department: 'dd',
  phoneNumber: '10020393939',
  email: 'choijming211@gmail.com',
  acquisitionChannel: 'web-search',
  materialType: '2025-dxreport'
}
데이터 => {
  name: 'dd',
  company: 'dd',
  department: 'dd',
  phoneNumber: '10020393939',
  email: 'choijming211@gmail.com',
  acquisitionChannel: 'web-search'
}

 

 

 

 

 

 

 

2.  원인 추론 🔎

  • Zod로 폼 유효성 검증을 하고 있음
  • Zod 스키마에서 TypeScript 타입이 자동 생성됨
  • materialType은 스키마에 정의되지 않아 타입 오류 발생
  • API 엔드포인트에서 검증 후 materialType 필드가 누락됨

이는 Zod 스키마 검증 과정에서 스키마에 정의되지 않은 필드가 제거되기 때문입니다. 결국 "폼 검증 로직과 API 데이터 구조 간의 불일치" 문제가 발생했습니다. 

 

 

 

 

 

 

3.  해결 과정 📋

< 시도했던 해결책 >

방법: 스키마 materialType 필드 추가

export const requestMaterialFormSchema = z.object({
  // ...기존 필드들...
  materialType: z.string(),
});

 

이 방법을 시도했지만, 폼 제출 과정에서 콘솔에 아무것도 찍히지 않는 결과가 나왔습니다. 이는 폼 입력 시점에 materialType이 사용자 입력으로 들어오지 않았기 때문입니다.

 

 

< 최종 해결책 >

API 엔드포인트에서 페이로드를 분해한 후 검증된 데이터 materialType을 다시 추가하는 방식으로 해결했습니다.

export async function POST(request: Request) {
  const payload = await request.json();
  const { materialType, ...formFields } = payload; // materialType 분리

  console.log("페이로드 =>", payload);

  try {
    // formFields만 검증 (materialType 제외)
    const parsedResult = requestMaterialFormSchema.safeParse(formFields);

    if (!parsedResult.success) {
      return NextResponse.json({ error: "invalid request" }, { status: 400 });
    }

    // 검증된 데이터에 materialType 다시 추가
    const data = {
      ...parsedResult.data,
      materialType
    };

    console.log("데이터 =>", data);
    // ...
  } catch (error) {
    // ...
  }
}

 

이 해결책의 결과:

페이로드 => {
  name: 'dd',
  company: 'dd',
  department: 'dd',
  phoneNumber: '10020393939',
  email: 'choijming211@gmail.com',
  acquisitionChannel: 'web-search',
  materialType: '2025-dxreport'
}
데이터 => {
  name: 'dd',
  company: 'dd',
  department: 'dd',
  phoneNumber: '10020393939',
  email: 'choijming211@gmail.com',
  acquisitionChannel: 'web-search',
  materialType: '2025-dxreport'
}

 

< 추가 문제 >

API 엔드포인트에서 데이터 처리 문제를 해결한 후, 슬랙 메세지 전송 함수에서 새로운 타입 오류가 발생했습니다.

export function sendRequestMaterialSlackMessage(
  requestMaterialFormValues: RequestMaterialFormValues
) {
  console.log("마지막 관문 =>", requestMaterialFormValues);

  let message = `
  新しい資料請求がありました。(자료청구)

  資料名:: ${requestMaterialFormValues.materialType} // 오류: 'materialType' 속성이 'RequestMaterialFormValues' 타입에 존재하지 않습니다.
  ...
  `;

  return sendSlackMessage(message);
}

 

이 문제는 RequestMaterialFormValues 타입에 materialType 필드가 정의되어 있지 않기 때문에 발생했습니다.

 

 

< 해결책: 교차 타입(intersection Type) 사용 >

이 방법은 기존 타입에 새로운 속성을 추가하는 교차 타입을 즉시 정의합니다.

export function sendRequestMaterialSlackMessage(
  requestMaterialFormValues: RequestMaterialFormValues & { materialType: string }
) {
  console.log("마지막 관문 =>", requestMaterialFormValues);

  let message = `
  新しい資料請求がありました。(자료청구)

  資料名:: ${requestMaterialFormValues.materialType}
  氏名: ${requestMaterialFormValues.name}
  ...
  `;

  return sendSlackMessage(message);
}

 

 

< 해결책 분석 >

  1. 문제의 원인 
    • Zod는 스키마에 정의된 필드만 유지하고 나머지는 제거합니다. materialType은 스키마에 정의되지 않았기 때문에 검증 과정에서 제거되었습니다,
  2. 왜 스키마에 추가하는 방법이 적합하지 않았는가?
    • 사용자가 직접 입력하는 값이 아니기 때문에 폼 검증에 포함시키면 문제가 발생
    • 폼 입력과 API 요청 사이에 추가되는 값이라 폼 검증 로직과 분리하는 것이 적절
  3. 해결책의 장점:
    • 스키마는 사용자 입력에 집중
    • materialType은 API 엔드포인트에서 안전하게 처리
    • 코드 구조가 명확해지고 책임분리가 잘 됨

 

 

 

 

4.  결과 ❤‍🔥

 

이로써 슬랙으로 메세지가 잘 전달하게 되었습니다!

콘솔메세지 => 
  新しい資料請求がありました。(자료청구)

  資料名:: 2025-dxreport
  氏名: 최지민
  会社名: 내배캠
  部署名: 프론트엔드
  電話番号: 01012345678
  メールアドレス: email@gmail.com
  スパルタを知ったきっかけ: YouTube