Skip to content

Comments

[Refactor] 회원가입 API를 multipart/form-data 방식으로 변경#8

Open
hyobin-yang wants to merge 1 commit intomainfrom
refactor/7
Open

[Refactor] 회원가입 API를 multipart/form-data 방식으로 변경#8
hyobin-yang wants to merge 1 commit intomainfrom
refactor/7

Conversation

@hyobin-yang
Copy link

@hyobin-yang hyobin-yang commented Jan 5, 2026

회원가입 API 변경사항

개요

회원가입 API가 application/json 방식에서 multipart/form-data 방식으로 변경되었습니다. JSON 데이터와 이미지 파일을 하나의 FormData에 담아서 전송해야 합니다.

변경 전후 비교

변경 전 (기존 방식)

// ❌ 기존 방식 - 더 이상 사용하지 않음
const response = await fetch('/auth/signup', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: '홍길동',
    email: 'test@example.com',
    password: 'password123',
    // ... 기타 필드
    profileImage: base64String // base64 문자열로 전송
  }),
});

변경 후 (새로운 방식)

// ✅ 새로운 방식
const formData = new FormData();

// JSON 데이터를 Blob으로 감싸서 추가
const jsonData = {
  name: '홍길동',
  email: 'test@example.com',
  password: 'password123',
  birthdate: '2002-04-18',
  nickname: '닉네임',
  bio: '자기소개',
};
const jsonBlob = new Blob([JSON.stringify(jsonData)], { type: 'application/json' });
formData.append('signupRequest', jsonBlob);

// 이미지 파일을 Blob으로 변환하여 추가
const blob = new Blob([byteArray], { type: 'image/jpeg' });
formData.append('image', blob, 'profile.jpg');

const response = await fetch('/auth/signup', {
  method: 'POST',
  // Content-Type 헤더를 명시하지 않음 (브라우저가 자동으로 multipart/form-data 설정)
  body: formData,
});

주요 변경사항

1. FormData 사용

기존의 JSON 객체 대신 FormData 객체를 사용합니다.

const formData = new FormData();

2. JSON 데이터를 Blob으로 감싸기

⚠️ 중요: JSON 객체를 그대로 FormData에 넣으면 안 됩니다!

// ❌ 잘못된 방법 - [object Object]로 변환되어 서버에서 에러 발생
formData.append('signupRequest', jsonData);

// ✅ 올바른 방법 - Blob으로 감싸고 type 명시
const jsonBlob = new Blob([JSON.stringify(jsonData)], { type: 'application/json' });
formData.append('signupRequest', jsonBlob);

이유:

  • 백엔드의 @RequestPart가 JSON 데이터를 인식하려면 type: "application/json"이 명시된 Blob이어야 합니다.
  • JSON 객체를 그대로 넣으면 [object Object] 문자열로 변환되어 서버에서 파싱할 수 없습니다.

3. 이미지 파일 처리

이미지는 base64 문자열이 아닌 **파일(Blob)**로 전송합니다.

// base64 data URL을 Blob으로 변환
const base64Data = profileImage.split(',')[1] || profileImage; // data:image/jpeg;base64, 부분 제거
const byteCharacters = atob(base64Data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
  byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'image/jpeg' });
formData.append('image', blob, 'profile.jpg');

4. Content-Type 헤더 제거

multipart/form-data를 사용할 때는 Content-Type 헤더를 명시하지 않습니다.

// ❌ 잘못된 방법
headers: {
  'Content-Type': 'multipart/form-data', // boundary가 없어서 에러 발생
}

// ✅ 올바른 방법 - 헤더를 명시하지 않으면 브라우저가 자동으로 설정
// 브라우저가 자동으로 "multipart/form-data; boundary=----WebKitFormBoundary..." 형태로 설정

브라우저가 자동으로 multipart/form-data와 boundary를 설정합니다.

5. 필드 이름

백엔드의 @RequestPart와 일치하도록 필드 이름을 정확히 지정해야 합니다.

// JSON 데이터 필드 이름
formData.append('signupRequest', jsonBlob); // ✅ 백엔드: @RequestPart("signupRequest")

// 이미지 파일 필드 이름
formData.append('image', blob, 'profile.jpg'); // ✅ 백엔드: @RequestPart(value = "image", required = false)

구현 코드 위치

파일: src/app/loginpage/register/profile/page.tsx

함수: handleNext() (약 98번째 줄부터)

백엔드 요구사항

RequestPart 필드

  1. signupRequest (필수)

    • 타입: SignupRequestDto (JSON)
    • Content-Type: application/json
    • Blob으로 감싸서 전송
  2. image (선택)

    • 타입: MultipartFile
    • Content-Type: image/jpeg 또는 기타 이미지 타입
    • 파일이 없으면 전송하지 않아도 됨 (required = false)

SignupRequestDto 필드

{
  name: string;              // 필수, 공백 불가
  birthdate: string;          // 필수, "YYYY-MM-DD" 형식
  email: string;              // 필수, 이메일 형식
  password: string;           // 필수, 영문 대/소문자, 숫자, 특수문자 포함 10자 이상
  nickname: string;           // 필수, 2~8자, 한글/영문/숫자/일부 특수문자(_, .)만 허용
  bio: string | null;         // 선택, 최대 12자
}

주의사항

  1. JSON 데이터는 반드시 Blob으로 감싸야 합니다.

    • type: 'application/json' 명시 필수
    • 백엔드의 @RequestPart가 인식하지 못하면 에러 발생
  2. 필드 이름은 정확히 일치해야 합니다.

    • signupRequest: JSON 데이터 필드
    • image: 이미지 파일 필드
  3. Content-Type 헤더를 명시하지 않습니다.

    • 브라우저가 자동으로 multipart/form-data와 boundary를 설정합니다.
  4. 이미지는 선택사항입니다.

    • 이미지가 없으면 image 필드를 전송하지 않아도 됩니다.
    • 기본 이미지를 사용하려면 이미지를 전송하지 않으면 됩니다.

테스트 체크리스트

  • JSON 데이터가 올바르게 전송되는가?
  • 이미지 파일이 올바르게 전송되는가?
  • 이미지 없이 회원가입이 가능한가?
  • 필드 이름이 백엔드와 일치하는가?
  • Content-Type 헤더가 자동으로 설정되는가?
  • 에러 메시지가 올바르게 표시되는가?

참고 자료

  • 구현 파일: src/app/loginpage/register/profile/page.tsx
  • 백엔드 API: POST /auth/signup
  • 백엔드 요구사항: @RequestPart 사용, multipart/form-data 방식

@hyobin-yang hyobin-yang self-assigned this Jan 5, 2026
@gemini-code-assist
Copy link

Summary of Changes

Hello @hyobin-yang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 회원가입 API의 데이터 전송 방식을 "application/json"에서 "multipart/form-data"로 리팩토링합니다. 이를 통해 JSON 형식의 회원 정보와 이미지 파일을 하나의 요청으로 효율적으로 전송할 수 있게 되어, 사용자 프로필 이미지 처리 방식이 개선됩니다.

Highlights

  • FormData 사용: 회원가입 API 요청 방식을 "application/json"에서 "multipart/form-data"로 변경하기 위해 FormData 객체를 사용하도록 전환했습니다.
  • JSON 데이터 Blob 처리: 회원가입에 필요한 JSON 데이터를 Blob 객체로 감싸고 "application/json" 타입을 명시하여 FormData에 "signupRequest" 필드로 추가했습니다. 이는 백엔드에서 JSON 데이터를 올바르게 파싱할 수 있도록 합니다.
  • 이미지 파일 직접 전송: 프로필 이미지를 base64 문자열 대신 Blob 형태로 변환하여 FormData의 "image" 필드로 직접 전송하도록 변경했습니다.
  • Content-Type 헤더 자동 설정: multipart/form-data 요청 시 브라우저가 Content-Type 헤더와 boundary를 자동으로 설정하도록, 코드에서 명시적인 Content-Type 헤더 설정을 제거했습니다.
  • 백엔드 필드명 일치: 백엔드의 @RequestPart 어노테이션과 일치하도록 JSON 데이터는 "signupRequest"로, 이미지 파일은 "image"로 필드 이름을 정확히 지정했습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

회원가입 API를 multipart/form-data로 전환하는 리팩토링 작업을 잘 수행해주셨습니다. PR 설명에 변경 사항을 상세히 기술해주셔서 리뷰하기 수월했습니다. 코드 변경 사항은 전반적으로 좋아 보이지만, 몇 가지 개선할 점과 잠재적인 버그를 발견하여 리뷰 코멘트를 남겼습니다. 특히, 이미지 데이터 처리 로직에서 발생할 수 있는 런타임 에러를 수정하고, API 엔드포인트를 환경 변수로 관리하여 코드의 유지보수성을 높이는 것을 제안합니다. 또한, 디버깅용으로 추가된 console.log 구문은 프로덕션 배포 전에 제거하는 것이 좋겠습니다. 자세한 내용은 각 파일의 코멘트를 확인해주세요.

Comment on lines +152 to +163
if (profileImage) {
// base64 data URL을 Blob으로 변환
const base64Data = profileImage.split(',')[1] || profileImage; // data:image/jpeg;base64, 부분 제거
const byteCharacters = atob(base64Data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'image/jpeg' });
formData.append('image', blob, 'profile.jpg');
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

현재 Base64-인코딩된 이미지 데이터를 Blob으로 변환하는 로직에 몇 가지 문제가 있습니다:

  1. 런타임 에러 발생 가능성: profileImage가 데이터 URL(data:image/...)이 아닌 일반 URL 문자열일 경우, atob() 함수에서 에러가 발생하여 애플리케이션이 중단될 수 있습니다. 현재 코드에서는 기본 이미지로 설정할 때 일반 URL이 할당될 수 있어 이 문제가 실제로 발생할 수 있습니다.
  2. 비효율성: 문자열을 순회하며 charCodeAt을 호출하는 방식은 큰 이미지의 경우 비효율적일 수 있습니다.
  3. 하드코딩된 MIME 타입: type: 'image/jpeg'로 MIME 타입을 하드코딩하고 있어, 사용자가 PNG 등 다른 형식의 이미지를 업로드할 경우 문제가 될 수 있습니다.

fetch API를 사용하면 이 로직을 더 안전하고 간결하게 개선할 수 있습니다. fetch는 데이터 URL을 직접 처리할 수 있으며, 올바른 MIME 타입을 가진 Blob 객체를 반환해줍니다.

      if (profileImage && profileImage.startsWith('data:image')) {
        // data: URL을 Blob으로 변환하는 더 효율적이고 안전한 방법입니다.
        const response = await fetch(profileImage);
        const blob = await response.blob();
        formData.append('image', blob, 'profile.jpg');
      }

Comment on lines 165 to 175
// 디버깅: 요청 데이터 확인
console.log('=== 회원가입 요청 데이터 ===');
console.log('name:', requestBody.name, 'length:', requestBody.name.length);
console.log('email:', requestBody.email);
console.log('password:', requestBody.password, 'length:', requestBody.password.length);
console.log('birthdate:', requestBody.birthdate);
console.log('nickname:', requestBody.nickname, 'length:', requestBody.nickname.length);
console.log('bio:', requestBody.bio, 'length:', requestBody.bio.length);
console.log('profileImage:', requestBody.profileImage);
console.log('JSON 데이터:', jsonData);
console.log('name:', jsonData.name, 'length:', jsonData.name.length);
console.log('email:', jsonData.email);
console.log('password:', jsonData.password, 'length:', jsonData.password.length);
console.log('birthdate:', jsonData.birthdate);
console.log('nickname:', jsonData.nickname, 'length:', jsonData.nickname.length);
console.log('bio:', jsonData.bio, 'length:', jsonData.bio.length);
console.log('profileImage:', profileImage ? '파일로 전송됨' : '없음');
console.log('========================');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

디버깅 목적으로 사용된 console.log 구문들이 남아있습니다. 프로덕션 코드에 포함되지 않도록 제거하는 것이 좋습니다. 민감한 정보가 노출될 수 있고, 브라우저 콘솔을 불필요하게 채울 수 있습니다.

console.log('========================');

// 백엔드 API로 회원가입 요청
const response = await fetch('https://classic-daramg.duckdns.org/auth/signup', {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

API 엔드포인트 URL이 하드코딩되어 있습니다. 환경 변수를 사용하여 관리하는 것이 좋습니다. 이렇게 하면 개발, 스테이징, 프로덕션 등 다른 환경에 맞게 쉽게 설정을 변경할 수 있습니다.

예시:

const API_URL = process.env.NEXT_PUBLIC_API_URL;
const response = await fetch(`${API_URL}/auth/signup`, {
  // ...
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 회원가입 API를 multipart/form-data 방식으로 변경

1 participant