react formData와 spring boot dto (리스트, 이미지파일, 문자 보내기)

햐얀하늘·2023년 10월 6일
1

글의 목적

프론트에서 백엔드로 데이터를 보내고자 할 때 우리는 보통 FormData를 활용해 백엔드에 데이터를 보낸다. 근데 FormData에는 File, text 두가지 형태만 들어갈 수 있다. 이때 리스트형태의 객체를 보내게 되면서 문제가 발생 하였다.

FormData에는 리스트 형태를 넣을 수 없어 json형태로 각각의 리스트 값을 키와 값으로 넣어서 보냈다. 그래서 이것을 spring boot에서는 우리가 보내는 방식에 맞게 저장해야한다.

React - FormData 보내기 (그룹만들기)

1. FormData로 보낼 type설정하기

interface CrewData {
  crewName: string;
  crewMembers: number[];
  crewImg?: string | File | null;
}

2. 데이터 저장할 useState 설정

const [friendList, setFriendList] = useState<
    { memberNickname: string; memberImg: string; memberSeq: number }[]
  >([]);
  const [crew, setCrew] = useState<number[]>([]);
  const [crewName, setCrewName] = useState("");
  const [crewImg, setCrewImg] = useState<File | null>(null);

3. crewImg, crewName, crewMembers 저장할 tsx작성

  1. input에 그룹명 작성
  2. crewImg 파일로 이미지 미리보기 - 이미지 파일은 URL.createObjectURL(crewImg)을 이용해 url로 만들어준다.
  3. StyledButton(커스텀 버튼) 클릭 시 이미지 파일 업로드 가능
  4. input창에는 이미지 파일을 업로드 할 수 있게 하지만 hidden 처리를 해서 버튼 클릭 시에만 동작함
  5. friendList는 axios로 받아온 친구목록이다 친구를 클릭하면 toggleFriend를 통해 crew에 친구들의 seq(id)가 들어가게 된다.
<>
<form onSubmit={handleMakeCrew}>
          <input 
            style={inputStyle}
            value={crewName}
            placeholder="그룹명을 적어주세요"
            onChange={handleCrewName}
          />
          <div>
            {crewImg && (
              <img
                src={URL.createObjectURL(crewImg)}
                alt="Crew"
                width="100"
                height="100"
                style={{ marginTop: "20px" }}
              />
            )}
            <StyledButton
              type="button"
              width="25vw"
              fontSize="10px"
              onClick={handleCrewImgUpload}
              background="#FFF6EC"
              color="gray"
            >
              사진 업로드
            </StyledButton>

            <input
              accept="image/*"
              type="file"
              hidden
              id="crewUploadInput"
              onChange={(e) => handleFileChange(e, setCrewImg)}
            />
            <StyledButton fontSize="15px" type="submit">
              생성하기
            </StyledButton>
          </div>
        </form>

        {friendList.map((friend, i) => {
          return (
            <CrewAdd
              name={friend.memberNickname}
              imgUrl={friend.memberImg}
              key={friend.memberSeq}
              onClick={() => toggleFriend(friend.memberSeq)}
              selected={crew.includes(friend.memberSeq)}
            />
          );
        })}
</>
import React from "react";
import classes from "./CrewAdd.module.css";
import axios from "axios";
import ToggleButton from "@mui/material/ToggleButton";

interface FriendProps {
  name: string;
  key: number;
  imgUrl: string | null;
  onClick: () => void;
  selected: boolean;
}

export const CrewAdd: React.FC<FriendProps> = ({
  name,
  key,
  imgUrl,
  onClick,
  selected,
}) => {
  return (
    <>
      <div className={classes.friendBox} onClick={onClick}>
        <div className={classes.profileBox}>
          {imgUrl ? (
            <img
              src={imgUrl}
              alt="sds"
              key={key}
              className={classes.profileImg}
            />
          ) : (
            <img
              src="/images/profileImg.png"
              alt="sds"
              key={key}
              className={classes.profileImg}
            />
          )}
        </div>

        <h3>{name}</h3>

        <div
          style={{
            backgroundColor: selected ? "orange" : "white",
            borderRadius: "50%",
            border: "1px solid black",
            width: "5vw",
            height: "5vw",
            marginLeft: "30vw",
          }}
        ></div>
      </div>
    </>
  );
};

4. crewName에 입력값의 변화에 반응해 저장할 함수 만들기

const handleCrewName = (e: ChangeEvent<HTMLInputElement>) => {
    setCrewName(e.target.value);
  };

5. 이미지 파일을 업로드하고 저장할 함수 만들기

  • handleCrewImgUpload : input창에 img를 업로드 하는 함수 만듦
  • handleFileChange : 새로운 파일을 넣으면 가장 앞에 있는 파일을 선택함
const handleCrewImgUpload = () => {
    const input = document.getElementById(
      "crewUploadInput"
    ) as HTMLInputElement;
    input?.click();
  };

  const handleFileChange = (
    event: ChangeEvent<HTMLInputElement>,
    setter: React.Dispatch<React.SetStateAction<File | null>>
  ) => {
    const file = event.target.files?.[0];
    if (file) {
      setter(file);
    }
  };

6. FormData를 활용해 axios보내기

  1. formData에 데이터를 넣어보자!!
  • 일반 문자열을 formdata에 넣어보자
formData.append("key값",value값);
  • image파일을 formData에 넣어보자
formData.append("이마자 key값", 이미지파일);
  • 리스트를 formdata에 넣어보자
    [{"memberseq":"1"},{"memberSeq":"2"}]
    이러한 형식으로 데이터가 만들어진다.
const crewMembersJSON = JSON.stringify(
      crew.map((memberSeq) => ({ memberSeq }))
    );
    formData.append("crewMembers", crewMembersJSON);
  1. formData가 만들어졌다면 axios보내보자
    headers에는 "Content-Type": "multipart/form-data"와 jwt인증을 위한 Authorization : accessToken을 넣어준다.
const handleMakeCrew = (e: React.FormEvent) => {
    e.preventDefault(); // submit시 렌더링 막기
    let crewData: CrewData | undefined;

    const formData = new FormData(); //formData 생성

    formData.append("crewName", crewName); // crewName 추가
    
    if (crewImg) {
      formData.append("crewImg", crewImg); // crewImg 추가
    }
	
  	// 리스트를 JSON 형식으로 key,value값으로 저장하는 코드
    const crewMembersJSON = JSON.stringify(
      crew.map((memberSeq) => ({ memberSeq }))
    );
    formData.append("crewMembers", crewMembersJSON);

    // formData를 post로 보내기
    if (friendCount >= 1) {
      axios
        .post(`${process.env.REACT_APP_BASE_URL}/crew/regist`, formData, {
          headers: {
            "Content-Type": "multipart/form-data",
            Authorization: `Bearer ${localStorage.getItem("accesstoken")}`,
          },
        })
        .then((res) => {
          navigate("/crew");
        })
        .catch((err) => console.log(err));
    } else {
      toast.error("최소 1명이상의 친구를 골라주세요.", {
        position: "top-center",
        autoClose: 1000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
        theme: "colored",
      });
    }
  };

ps) formData는 console.log(formData) 하면 값이 나오지 않는다.

아래의 방식으로 formData.get("key값")을 해주면된다.

console.log(formData.get("crewMembers"));

추가로 react-hook-form을 쓰면 formData를 사용할 필요없이 바로 보낼 수 있다.

Spring Boot dto와 controller

dto 만들때 주의!!

Dto를 만들 때 리스트의 formData가 text,file 형태로만 보낼 수 있다는 것을 모르면 Dto에서 리스트의 타입을 List로 설정 할 수 있다.
List가 아니라 string으로 해줘야한다.

  1. CrewSignUpDto : 그룹 등록하기 위한 dto 만들기
  • Img 파일은 MultipartFile로 데이터 타입을 설정해주자!
  • List로 들어오는 파일은 String으로!
  • 일반 문자열은 String으로!
@Data
@NoArgsConstructor
public class CrewSignUpDto {
    private String crewName;
    private MultipartFile crewImg;
    private String crewMembers;

    @Builder
    public CrewSignUpDto(String crewName, MultipartFile crewImg, String crewMembers) {
        this.crewName = crewName;
        this.crewImg = crewImg;
        this.crewMembers = crewMembers;
    }
}
  1. CrewService에서 registerCrewForMember

전체 코드

@Transactional
public void registCrewforMember(CrewSignUpDto crewSignUpDto, String memberEmail)
            throws IllegalArgumentException, EntityNotFoundException, JsonProcessingException {
        if(crewSignUpDto.getCrewMembers().isEmpty()) {
            throw new IllegalArgumentException("그룹은 최소 2명 이상이여야 합니다.");
        }
        
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(crewSignUpDto.getCrewMembers());

        List<Long> memberSeqs = new ArrayList<>(); // JsonNode를 순회하며 데이터 추출
        for (JsonNode node : jsonNode) {
            Long memberSeq = node.get("memberSeq").asLong();
            memberSeqs.add(memberSeq);
        }

        Member member = memberRepository.findByEmail(memberEmail)
                .orElseThrow(() -> new EntityNotFoundException("회원을 찾을 수 없습니다."));
        String S3_fileName="";
        if(crewSignUpDto.getCrewImg() != null){
            S3_fileName = "crewImg/" + foodService.getRandomFileName();
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(crewSignUpDto.getCrewImg().getContentType());
            metadata.setContentLength(crewSignUpDto.getCrewImg().getSize());
            try {
                amazonS3Client.putObject(bucket, S3_fileName, crewSignUpDto.getCrewImg().getInputStream(), metadata);
            }catch (Exception e) {
                e.printStackTrace();
                throw new IllegalStateException("이미지 저장 중 에러 발생");
            }
        }
        
        // Crew 생성
        Crew crew = Crew.builder()
                .name(crewSignUpDto.getCrewName())
                .img(S3_fileName)
                .status("투표전")
                .createdAt(LocalDateTime.now())
                .lastModifiedAt(LocalDateTime.now())
                .build();
        crew = crewRepository.save(crew);

        for (Long memberSeq:memberSeqs) {
            CrewMember crewMember = CrewMember.builder()
                    .crew(crew)
                    .member(
                            Member.builder()
                                    .memberSeq(memberSeq)
                                    .build()
                    )
                    .checkVote(1)
                    .status(0)
                    .build();
            crewMemberRepository.save(crewMember);
        }
        CrewMember crewMember = CrewMember.builder()
                .crew(crew)
                .member(member)
                .status(1)
                .checkVote(1)
                .build();
        crewMemberRepository.save(crewMember);
        log.info("crew 생성 완료");
    }

코드 해석

  1. Jackson 라이브러리의 ObjectMapper를 사용하여 JSON 문자열을 파싱하여 JsonNode 객체로 변환하는 코드이다.

crewSignUpDto에서 crewMembers를 가져와서 JsonNode로 객체로 변환

ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(crewSignUpDto.getCrewMembers());
  1. 앞에서 변환한 JsonNode를 순환하며 memberSeq값을 Long으로 변환해 빈 리스트로 만든 memberSeqs에 값을 집어넣음
List<Long> memberSeqs = new ArrayList<>();
// JsonNode를 순회하며 데이터 추출
for (JsonNode node : jsonNode) {
     Long memberSeq = node.get("memberSeq").asLong();
     memberSeqs.add(memberSeq);
}
  1. email을 찾은 다음에 값이 없으면 EntityNotFoundException을 던짐
Member member = memberRepository.findByEmail(memberEmail)
                .orElseThrow(() -> new EntityNotFoundException("회원을 찾을 수 없습니다."));
  1. S3에서 이미지를 업로드하는 코드
  • S3_fileName : Amazon S3에 업로드할 파일의 경로 및 이름을 저장하는 빈 문자열

  • part1 : Amazon S3에 업로드될 이미지 파일의 경로와 파일 이름을 생성 이 경로는 "crewImg/"에 foodService.getRandomFileName() 메소드를 사용하여 무작위 파일 이름을 생성한 후 더함

  • part2 : Amazon S3 객체 메타데이터를 생성합니다. 이 메타데이터에는 업로드한 이미지 파일의 유형(contentType)과 크기(contentLength)가 설정

  • part3 : 이미지 파일의 contentType과 size 설정

  • part4 : amazonS3Client.putObject() 메소드를 사용하여 이미지 파일을 Amazon S3 버킷에 업로드하고, 업로드 중에 에러가 발생하면 예외를 처리하고 에러 메시지를 출력

String S3_fileName="";
        if(crewSignUpDto.getCrewImg() != null){
        	// part1
            S3_fileName = "crewImg/" + foodService.getRandomFileName();
            // part2
            ObjectMetadata metadata = new ObjectMetadata();
           
            //part3
            metadata.setContentType(crewSignUpDto.getCrewImg().getContentType());            
            metadata.setContentLength(crewSignUpDto.getCrewImg().getSize());
            
			// part4
            try {
                amazonS3Client.putObject(bucket, S3_fileName, crewSignUpDto.getCrewImg().getInputStream(), metadata);
            }catch (Exception e) {
                e.printStackTrace();
                throw new IllegalStateException("이미지 저장 중 에러 발생");
            }
        }
  1. Crew를 생성하고 저장하는 코드
Crew crew = Crew.builder()
                .name(crewSignUpDto.getCrewName())
                .img(S3_fileName)
                .status("투표전")
                .createdAt(LocalDateTime.now())
                .lastModifiedAt(LocalDateTime.now())
                .build();
        crew = crewRepository.save(crew);
profile
나는 커서 개발자가 될거야!

0개의 댓글