팀프로젝트: Wingle(1.75) API 연결 - 회원가입 데이터 전송 API 구현 및 트러블슈팅

윤뿔소·2023년 5월 9일
0

팀프로젝트: Wingle

목록 보기
12/16

전편에 이어서 API 연결과 리액트쿼리를 구현해 요청을 기어코 보내보겠다!

API 명세서

제일 처음에 보여줬던 회원가입 API 명세서를 봐보자.

Request Body
🚨 multipart/form-data로 주셔야 합니다!
항목
idCardImage string, 학생증 이미지
email string, 이메일
password string, 비밀번호
name string, 이름
isNicknameChecked boolean, 닉네임 중복 검사를 통과했는지
nickname string, 닉네임
gender boolean, 성별, 여자가 true
nation string, 국적
termsOfUse boolean, 이용약관 동의 여부
termsOfPersonalInformation boolean, 개인정보 수집 및 이용동의 여부
termsOfPromotion boolean, 이벤트 프로모션 알림 동의 여부
예시

{
	"idCardImage": "image",
	"email": "wingle@gmail.com",
	"password": "1234!",
	"name": "김윙글",
	"isNicknameChecked" : true,
	"nickname": "윙그리",
	"gender": true,
	"nation": "kr",
	"termsOfUse": true,
	"termsOfPersonalInformation": true,
    "termsOfPromotion": false
}

저 데이터에 맞게 지금까지 컴포넌트들을 만들어왔고, 모델로서 가입 데이터를 관리한 것이다.

가장 중요한 포인트가 multipart/form-data로 줘야한단 말이다. 즉, 줘야되는 데이터의 형태도 form-data로 줘야한다.

그런 점을 유의하며 기능 구현을 해보자.

기능 구현

보내는 기능을 만드는데 중요한 재료들을 만들어가보자.

  1. API : /api/signUpApi.ts - postSignUp 만들기
  2. multipart/form-data로 데이터 만들기

미리 쓰기 전에 생각으로 트러블 슈팅이 은근 많다.. 작성해보자잇!

API : /api/signUpApi.ts

여기는 되게 쉽다. 만들어둔 instance에 넣어주자.

export const postSignUp = async (signUpData: SignUpFormData) => {
  const response = await instance.post("/auth/signup", (Form화된 signUpData), {
    headers: { "Content-Type": "multipart/form-data" },
  });

  return response.data;
};

짠! 중요하게 볼 것은 headers에 Content Type이 multipart/form-data이고, instance.post에서 두번째 인자로 Form화된 signUpData를 받고 있다. 이제 Form화된 signUpData를 만들어보자.

우당탕탕 Form화된 signUpData 만들기 대작전

formData에 모두 담겨야하니 반복문을 통해 signUpData의 Key, Value를 formData의 Key와 Value로 담아야 한다.

어떻게 해야할까?

  1. formData 새로 만들기! : new FormData()로 만들기!
  2. 반복을 고차함수를 사용할거기 때문에 entries(data)를 사용해 2차원 배열 만들기
  3. forEach를 사용해 2차원 배열을 돌며 각 요소인 배열 [key, value]를 추출하기
  4. formDataappend를 사용해 넣기

이정도로 하면 된다! 그런 줄만 알고 신나게 따라했다.

// src/types/auth/signupFormDataType.ts
export interface SignUpData {
  idCardImage: string;
  email: string;
  password: string;
  name: string;
  isNicknameChecked: boolean;
  nickname: string;
  gender: boolean;
  nation: string;
  termsOfUse: boolean;
  termsOfPersonalInformation: boolean;
  termsOfPromotion: boolean;
}

// src/api/auth/signUpApi.ts
const withFormData = (data: SignUpData) => {
  // multipart/form-data 형식으로 데이터를 보내기 위해 FormData 객체를 생성
  const formData = new FormData();
  Object.entries(data).forEach(([key, value]) => {
      formData.append(key, value);
  });
  return formData;
};

export const postSignUp = async (signUpData: SignUpData) => {
  const response = await instance.post("/auth/signup", withFormData(signUpData), {
    headers: { "Content-Type": "multipart/form-data" },
  });

  return response.data;
};

그리고 보내봤더니 문제가 생겼다.

🚨 이후 글은 트러블 슈팅 위주 글입니다. 보셔도 되지만 저의 피땀눈물이 담겨있습니다!!@ 🚨

CORS 문제 발생

이 걸 보고 든 생각은

웬 CORS냐!! 다른 이메일이나 닉네임 검증 API를 보낼 땐 아무 문제 없었고, CORS 허락 헤더에 port도 3000~3010, localhost, ip 등등 다 열어주라고 백엔드와 협의 마쳤는데 ㅠㅠ

이렇게 생각이 들었다. 그래서 백엔드와 긴밀한 협의와 같이 자바 코드를 봐주고 30분 간 대화했다.
그런 결과 서버와 통신 중 Content-Type도 CORS 허락이 없다면 반려한다는 사실을 알게 됐다. 왜 이걸 처음부터 몰랐느냐! 전 백엔드 팀원분이 다 해주셨어서 몰랐다.. 지금 팀원분과 나도 처음이라 이런 문제가 발생했다.

그래도 간단한 문제이기에 서로 껄껄대며 헤더에 추가하고 EC2에 올리는 걸로 합의 봤다.

그런데 문제가 또 생겼다.

자바스프링에서 이 데이터 못먹어요! 빼주세요!

Error: Failed to convert value of type 'java.lang.String' to required type 'org.springframework.web.multipart.MultipartFile'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'org.springframework.web.multipart.MultipartFile': no matching editors or conversion strategy found

이.. 이게 뭐야.. 나는 처음 보는 에러였다. 왜이러지 하고 검색을 해보니

Spring 어플리케이션에서 String 객체를 MultipartFile 객체로 변환하려고 할 때, Spring이 이를 수행하는 데 필요한 변환 전략을 찾을 수 없어서 발생하는 오류

라고 한다. 즉, 내가 FormData로 넣었던 String들 중 서버에서의 맞는 데이터로 변환하지 못해 생긴 에러다. 뭐지 싶어서 바로 백엔드와 의논하러 들어갔다.

@프론트엔드_윤뿔소
님 위 이슈는 idCardImage를 보내실 때 string 타입으로 보내셔서 발생한 문제입니다~
html에서는 input type을 file로 해서 보내주시면 된다고 하는데 MultipartFile 타입에만 맞게 보내주시면 좋을 것 같습니다!

바로 idCardImage에서 Base64가 아니라 File 자체로 집어 넣어야한다고 말이다. 분명 명세서에 String이었고, 이 부분에 대해서 말씀 드렸다.

결론은 명세서 File 데이터로 고치고, 나도 고쳐야한다.. 이럴 수가 ㅠ 하지만 이런 문제도 있기에 다음부턴 File을 주고 받을 때 그 데이터 형식을 확인 받는 식으로 해야겠다고 생각했다. 나중에 블로그 글을 보고 복습하자!

바로 변경이 들어갔다.

  1. 리코일 아톰 SignUpFormData 수정 : string => FormData 타입 설정
    // src/types/auth/signupFormDataType.ts
    export interface SignUpFormData {
      idCardImage: FormData | null;
      email: string;
      ...
    }
    // src/api/auth/signUpApi.ts
    export const signUpFormDataAtom = atom<SignUpFormData>({
      key: "signUpFormDataAtom",
      default: {
        idCardImage: null,
        email: "",
        ...
  2. 파일 업로드 시 base64가 아닌 formData로 담기게 수정
    쉽다! 파일 데이터를 담기 위해 formData를 선언해 넣어주기만 하면 된다.
    const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
      const imageFile = event.target.files?.[0];
      if (imageFile) {
        const fileSizeInMB = imageFile.size / (1024 * 1024);
        if (fileSizeInMB > 20) {
          // 20MB 이하인 경우에만 처리
          setError(true);
          setSignUpFormData((prev: SignUpFormData) => ({
            ...prev,
            idCardImage: null, // null 로 초기화
          }));
          return;
        }
        setError(false);
        const formData = new FormData();
        formData.append("idCardImage", imageFile);
        // File 데이터를 formData 에 append
        setSignUpFormData((prev: SignUpFormData) => ({
          ...prev,
          idCardImage: formData,
        }));
        setUploadedFileName(imageFile.name);
      }
    };

왜 File 객체를 아톰에 직접 넣어주지 않고 formData로 담아준 후 아톰에 넣을까?

File 객체는 크기가 큰 바이너리 데이터를 나타내기 때문에, 리코일과 같은 상태 관리 라이브러리에 직접 담는 것은 권장되지 않는다.

왜냐하면 리코일과 같은 상태 관리 라이브러리는 일반적으로 변화가 일어날 때마다 전체 상태를 복사하고 업데이트해야 하는 구조를 가지고 있는데, 큰 크기의 바이너리 데이터를 포함하는 경우 상태 업데이트 시간과 메모리 사용량이 많아져 성능에 영향을 미치기 때문이다.

그래서 FormData를 사용하여 큰 용량의 데이터를 다루는 역할을 맡긴 것이다. 왜냐하면 FormData에 데이터를 추가할 때는 해당 데이터가 포함된 Blob 객체를 전달해야 하는데, 이 때 Blob 객체는 큰 용량의 데이터를 다룰 수 있도록 최적화돼있기 때문이다!

또, 왜 Blob객체는 최적화 돼있냐면... (중략) 참고 사이트

즉, 일반 수레로는 내용이 커 옮기기 힘드니 옮겨다닐 힘을 줄일 수 있게 FormData라는 모터를 달아줘 슈퍼 수레로 옮겨다니는 것이다.


이러면 됐겠지? 또 제출해보자!

FormData : 힝 속았지?!😋

또 똑같은 에러가 떴다. 왜이러지?! 이 문제를 찾느라 거의 1시간을 썼던 거 같다. 내가 결코 이 문제를 풀리라 다짐했다.

다양하게 써봤다. 뭐 불린데이터 문자열로 넣거나 log를 찍어보며 데이터를 확인했다. 아톰 하나하나 봤었는데 문제가 안보였다!!

그러다가 withFormData를 직접 만져보며 여러가지 테스트 해본 결과 idCardImage: FormDataFormData가 아니라 String으로 들어가는 것이다! 왜지?

바로 FormDatastring | Blop만을 받는 객체이기에 다른 타입들도 전!부! string으로 변환되는 로직을 갖고 있다.

그래서 아래와 같이 하고 로그를 찍어보면

const formData2 = new FormData();
formData2.append("idCardImage", formData);
console.log(formData2.getAll("idCardImage"));

문자열로 찍히는 걸 볼 수 있다. 이걸 내가 하고 있었던 거고 놓친 것이다.
이렇게 설명하니까 당연해보이고 이런 실수를 한다고? 하는데 FormData를 넣는 구조가 한 문단 안에 없었기도 했고 FormData 배경지식을 알고 있는 상태가 아니었기에 그랬었다.. 이 글을 보는 분들은 이런 실수때메 1시간이나 낭비하지 않았으면 좋겠다. 물론 나는 이제 까먹지 않을 거 같다..

쨌든 이제 알았으니 FormDataFile을 직접 넣어주자!

const withFormData = (data: SignUpFormData) => {
  // multipart/form-data 형식으로 데이터를 보내기 위해 FormData 객체를 생성
  // formData를 그대로 넣어버리면 string으로 받아 오류, 그래서 idCardImage를 조건으로 넣고 그 값을 그대로 넣어줘야함.
  const formData = new FormData();
  Object.entries(data).forEach(([key, value]) => {
    if (typeof value === "boolean") {
      formData.append(key, value ? "true" : "false");
    } else if (key === "idCardImage") {
      formData.append(key, value.get("idCardImage"));
    } else {
      formData.append(key, value);
    }
  });
  return formData;
};

export const postSignUp = async (signUpData: SignUpFormData) => {
  const response = await instance.post(
    "/auth/signup",
    withFormData(signUpData),
    {
      headers: { "Content-Type": "multipart/form-data" },
    }
  );
  return response.data;
};

조건을 보면 key === "idCardImage"일 때, value(아톰의 formData)에서 idCardImage를 get해와 api의 formDataappend해주는 모습이다.

추가로 불린데이터도 문자열로 반환 후 넣어주는 걸 명시적으로 해주면 더 안전하다고 해서 그 조건 로직도 추가했다.

결과

이제 잘 됐는지 확인해보자!

참고로 내 이메일은 이미 가입돼있기에 disabled이 풀리는 모습과 통신이 되는 모습에 집중했음 좋겠다!

에러를 보자면

{
  ...
  data: 
    data: null
    message: "이미 가입된 유저입니다."
    status: 400
  ...
}

이렇게 떴다. 즉, 갔다가 서버 통신 후 가입됐다는게 됐으니까 저렇게 온거다. 다른 이메일이나 다른 걸로 하면 잘된다! 유후! 대장정이 끝이 났다!!!!

profile
코뿔소처럼 저돌적으로

5개의 댓글

comment-user-thumbnail
2023년 5월 11일

좋네요 혹시 multipart/form-data 사용하시는게 파일도 같이 보내서 쓰시는건가용?

1개의 답글
comment-user-thumbnail
2023년 5월 14일

하 !! CORS 오랜만에 보네요 나쁜 넘 ㅡㅡ

답글 달기
comment-user-thumbnail
2023년 5월 14일

와우.. 엄청난 대장정이네요ㅋㅋㅋㅋ formData에 불린값 넣어줄 때 문자열로 넣으면 좋다는 것도 새롭게 배우고 갑니다!!

답글 달기
comment-user-thumbnail
2023년 5월 14일

오 저렇게 불리언값으로 하는거 저도 처음보네용 고생하셨어요!

답글 달기