첨부한 이미지 서버로 보내기 & 아직도 해결 안 된 수수께끼

voyager 999·2024년 2월 23일

React

목록 보기
22/27

이미지 업로드 기능 구현하기

흔히 만들게 되는 마이페이지 수정기능이다. 수정페이지에서 프로필 사진을 클릭하면 기기의 파일 선택하기 창이 열리고, 여기서 선택한 이미지를 미리보기로 보여준 다음, 정보를 서버로 보내서 프로필 사진을 수정할 수 있도록 구현하고 싶다.

이미지 업로드 기능을 이전에 한 번도 해본 적이 없어서 여러 가지 구글링과 chatGPT 선생님의 도움을 받아 한번 해보게 되었다.

이미지 뿐만 아니라 닉네임 역시 기존 입력값을 불러와서 표시하고, 수정된 값으로 서버에 변경요청을 할 것이다.

  • 파일 업로드 UI 기본 형태
{/* 파일이 선택되었는지 상태를 관리하기 위해 필요한 state */}
const [selectedFile, setSelectedFile] = useState(null);

<div>
      {/* 파일을 선택하는 input 요소 */}
      <input type="file" onChange={handleFileChange} />

      {/* 선택된 파일의 이름을 보여주는 부분 */}
      {selectedFile && <p>선택된 파일: {selectedFile.name}</p>}

      {/* 파일 업로드 버튼 */}
      <button onClick={handleFileUpload}>파일 업로드</button>
</div>

1. 파일을 선택하는 input
type="file"로 설정해주면, 파일을 선택할 수 있다. 이렇게 실행하면 아마도 "찾아보기" 텍스트가 있는 기본 모양으로 출력될 것이다. 나의 경우에는 별도의 버튼이 아니라 이미지를 클릭하면 파일을 선택해야 하기 때문에, "찾아보기" 버튼이 보이지 않도록 style={{ display: 'none' }} 스타일을 주었다.

이렇게 "찾아보기"버튼은 안 보이고, 이미지를 클릭하여 파일을 선택하므로, 뭔가를 클릭하면 클릭 이벤트가 발생했다는 것을 추적할 수 있어야 한다. 리액트 프로젝트에서는 이 클릭 이벤트를 위해 useRef를 사용한다.

또한, 나의 UI에서는 isEditing 상태에 상관없이 이미지는 항상 출력되고 있으므로 isEditing=true일때만 이미지의 클릭 이벤트를 추적하도록 조건문을 사용했다.

import { useRef } from 'react';

const [isEditing, setIsEditing] = useState(false);
const fileInputRef = useRef(null); //초깃값 null


  {/* 이미지를 클릭하면 실행되는 함수 */}
  //이미지를 클릭하면 fileInputRef라는 이름을 가진 요소를 클릭해주세요!
  const handleClickAvatar = () => {
    if (isEditing) {
      fileInputRef.current.click();
    }
  };
  
   <MyAvatar onClick={handleClickAvatar}> //이미지에 함수 걸어줌
      <AvatarImage src={avatar} alt="사용자 아바타" />
      {/* 파일 입력(input)요소 */}
      <input
         type="file"
         id="fileInput"
         ref={fileInputRef} // 바로 요 input을 클릭하면 된다고 표시
         style={{ display: 'none' }}
         onChange={handleFileChange}
      />
      //input이 fileInputRef를 참조
   </MyAvatar>


2. 선택된 이미지를 미리 보여주기
파일 선택하기 창에서 선택된 파일이 있는지 없는지를 파악하기 위해 새로운 state가 하나 더 필요하다. 파일이 선택되지 않으면 초깃값 설정에 의해 기존의 이미지가 보인다.

{/* 서버에서 리덕스로 가져와 둔 데이터들 */}
const { userId, nickname: initialNickname, avatar: initialAvatar, accessToken } = useSelector((state) => state.auth);

{/* 그 중에서 avatar 주소를 초깃값으로 셋팅 */}
//마이페이지에 들어오면 초깃값에 따라 기존 이미지 출력
const [avatar, setAvatar] = useState(initialAvatar);

{/* 선택된 파일이 있는지 없는지 파악하기 위한 state */}
const [myFile, setMyFile] = useState(null);

파일 선택 input에 onChange 함수를 걸어준다. 이 때 e.target.files[0] 요 인덱스는 사용자가 선택한 파일들의 목록 중 0번째 인덱스라는 뜻이다. 즉 이미지 파일은 1개만 선택할 수 있다는 뜻이다. 만약 여러 개의 이미지 파일을 업로드 할 수 있는 상황이라면 이 부분에서 처리가 달라져야 할 것이다.

  const handleFileChange = (e) => {
    const file = e.target.files[0];
    
    //선택된 이미지가 있다면, 이미지의 주소를 만들어 setAvatar(출력용)
    //선택된 이미지가 있다면, 해당 이미지를 setFile(서버 전송용)
    if (file) {
      const fileUrl = URL.createObjectURL(file);
      setAvatar(fileUrl);
      setMyFile(file);
    }
  };

이제 여기까지 하면 새로 선택된 이미지의 주소가 setAvatar에 의해 avatar 상태가 업데이트 되므로, 리렌더링이 발생해 화면에서도 선택한 이미지를 볼 수 있게 된다.

3. 변경사항 서버로 보내기

  //서버와 통신하는 부분이니 async 함수로 작성한다.
  const EditCompleteHandler = async () => {
    try {
      //새로운 FormData 만들기
      const formData = new FormData();
      formData.append('avatar', myFile); //선택된 파일
      formData.append('nickname', nickname); //변경한 닉네임 문자열
      
      //사용자 정보 중 아바타와 닉네임만 변경하므로 patch를 사용한다.
      const response = await api.patch(`/profile`, formData, {
        headers: {
          //FormData에는 이미지 파일도 있고, 텍스트도 있다고 알림
          'Content-Type': 'multipart/form-data',
          Authorization: `Bearer ${accessToken}`
        }
      });
      
      // 정보가 수정되었음을 사용자에게 알림
      toast.success(`정보가 수정되었습니다.`);
      
      //수정 모드 종료
      setIsEditing(false);
      
    } catch (error) {
    
      //통신에 실패한 경우 사용자에게 알림
      toast.error(`정보 수정에 실패했습니다.`);
      console.log(error);
    }
  };

아직도 해결이 안 된 문제

  • My Needs

    isEditing=true일 때, 즉 수정중일 때, "지금이라면 이미지를 클릭하면 아바타를 바꿀 수 있어요!"를 사용자에게 좀 더 명시적으로 보여주고 싶었다. 그래서 수정 중일 때에만 이미지에 마우스 오버 시 transform 효과가 실행되고, 마우스 커서가 pointer로 바뀌게 하고 싶다!


  • My Code (doesn't work)
    챗지피티의 도움을 받아서 styled-components에 isEditing의 상태에 따른 조건부 스타일을 작성해보았다.
const MyAvatar = styled.div`
  height: 150px;
  margin: 20px;
  cursor: ${({ isEditing }) => (isEditing ? 'pointer' : 'default')};
`;

const AvatarImage = styled.img`
  width: 150px;
  height: 150px;
  border-radius: 50%;
  transition: ${({ isEditing }) => (isEditing ? 'transform 0.3s ease-in-out' : 'none')};
  ${({ isEditing }) =>
    isEditing &&
    `&:hover {
    transform: scale(1.1)
  }`}
`;

그런데 무슨 수를 써도 효과가 적용이 되지 않았다..... isEditing prop이랑 상관 없이 단순 스타일링 적용을 하면 당연히 잘 되는데, isEditing prop을 넣어버리는 순간 원하는대로 작동이 되지 않는다.

챗지피티도 모른다....

0개의 댓글