[React] 이미지 파일 업로드 기능 구현

Yeojin Choi·2022년 4월 5일
45

벨로그의 설정 페이지를 보면, 위 이미지와 같이 썸네일 이미지, 이미지 업로드 버튼 / 이미지 제거 버튼이 있다.

이처럼 클라이언트 앱에서 이미지 업로드 버튼을 클릭해 이미지를 선택하고, 서버를 통해 클라우드 스토리지(오브젝트 스토리지)에 이미지 파일을 업로드해보자.

input[type=file]

<input type="file"> 요소는 사용자가 장치 저장소에서 하나 이상의 파일을 선택할 수 있도록 해준다. 선택된 파일은 폼 제출을 사용해 서버에 업로드 하거나, javascript code 또는 File API 를 사용해 조작할 수 있게 된다.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file

<input type="file"> 로 버튼을 만들고, accept='image/*' 속성을 줘서 이미지 파일 유형을 가진 파일만 업로드 할 수 있도록 해준다.

그리고 input 을 숨기고, 이미지 업로드 버튼에 연결시켜 주기 위해 2가지 방법을 시도해보았다.

1) <label><input> 요소에 linking 하기

  • <label> 을 클릭해서 <input> 요소 자체에 초점을 맞추거나 활성화를 시킬 수 있다.
  • <label><input> 요소와 연관시키려면, <input>id 속성을 넣고, <label>id 와 같은 값for 속성을 넣어야한다.
  • React 의 경우 htmlFor 속성으로 넣어줘야한다.
  • <label> 안에 <input> 을 중첩시킬 경우 연관이 암시적이므로 for 및 id 속성이 필요없다.
    https://developer.mozilla.org/ko/docs/Web/HTML/Element/label

나의 경우에는 이미지 업로드 버튼이 button tag 로 이루어진 컴포넌트 였기에, <label> 로 이미지 업로드 버튼을 감쌌더니 linking 이 되지 않았다. <label><button> 과 중첩되어 있을 경우 <input> 과 linking 되지 않는 것 같다.

두번째 방법으로 <label><input> 을 감싸고 버튼과 겹쳐지도록 css 로 위치조절을 해줬는데, <label> 이 버튼 위에 있으면 hover effect 가 발생하지 않았고, <label> 이 버튼 밑에 있으면 장치 관리자 창이 생기지 않았다.

2) useRef 사용하기
<input> 요소를 display: none 으로 없애고, 이미지 업로드 onClick 이벤트 핸들러에서 useRef 로 참조하고있는 <input> 요소에 onClick 이벤트를 호출해 장치 관리자 창을 띄웠다.

그리고 파일을 선택하면 <input> 요소의 onChange 이벤트가 호출되는데, onChange 이벤트 핸들러에서 e.target.files 를 통해 선택한 파일 객체에 대한 정보를 알 수 있게 된다.

// SettingUserThumbnail.tsx
const SettingUserThumbnail = () => {
  const inputRef = useRef<HTMLInputElement | null>(null);
  
  const onUploadImage = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) {
      return;
    }
    console.log(e.target.files[0].name);
  }, []);
  
  
  const onUploadImageButtonClick = useCallback(() => {
    if (!inputRef.current) {
      return;
    }
    inputRef.current.click();
  }, []);
  
  return (
      <input type="file" accept="image/*" ref={inputRef} onChange={onUploadImage} />
      <Button label="이미지 업로드" onClick={onUploadImageButtonClick} />
  );
}

FormData

이미지 파일을 폼 제출을 사용해 서버에 업로드 하기 위해 FormData 객체를 사용한다. key 값과 value 쌍의 형태로 데이터 전송을 도와준다.
https://developer.mozilla.org/ko/docs/Web/API/FormData
https://developer.mozilla.org/ko/docs/Web/API/FormData/FormData

// SettingUserThumbnail.tsx
const SettingUserThumbnail = () => {

  const onUploadImage = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) {
      return;
    }

    const formData = new FormData();
    formData.append('image', e.target.files[0]);

    axios({
      baseURL: API_HOST,
      url: '/images/:username/thumbnail',
      method: 'POST',
      data: formData,
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    })
      .then(response => {
        console.log(response.data);
      })
      .catch(error => {
        console.error(error);
      });
  }, []);
  
  
  const onUploadImageButtonClick = useCallback(() => {
    if (!inputRef.current) {
      return;
    }
    inputRef.current.click();
  }, []);
  
  return (
      <input type="file" accept="image/*" ref={inputRef} onChange={onUploadImage} />
      <Button label="이미지 업로드" onClick={onUploadImageButtonClick} />
  );
}

MulterError: Unexpected field

single('name') rhk input name 이 일치하지 않을 때 발생한다.

클라이언트 측 코드

const SettingUserThumbnail = () => {
  // 기존 코드 중략
  return (
    <SettingUserThumbnailLayout>
      <input
        type="file"
        accept="image/*"
        name="thumbnail"
        ref={inputRef}
        onChange={onUploadImage}
      />
      <Button label="이미지 업로드" onClick={onUploadImageButtonClick} />
      <Button label="이미지 제거" onClick={onDeleteImage} />
    </SettingUserThumbnailLayout>
  );
};

서버 측 코드

// 사용자 썸네일 이미지 업로드
router.post('/images/:username/thumbnail', upload.single('thumbnail'), (req, response) => {
  // 코드 중략
  );
});
profile
프론트가 좋아요

1개의 댓글

comment-user-thumbnail
2022년 10월 23일

깃헙 공유 가능한가요??.. 🙏🙏

답글 달기