흔히 만들게 되는 마이페이지 수정기능이다. 수정페이지에서 프로필 사진을 클릭하면 기기의 파일 선택하기 창이 열리고, 여기서 선택한 이미지를 미리보기로 보여준 다음, 정보를 서버로 보내서 프로필 사진을 수정할 수 있도록 구현하고 싶다.
이미지 업로드 기능을 이전에 한 번도 해본 적이 없어서 여러 가지 구글링과 chatGPT 선생님의 도움을 받아 한번 해보게 되었다.
이미지 뿐만 아니라 닉네임 역시 기존 입력값을 불러와서 표시하고, 수정된 값으로 서버에 변경요청을 할 것이다.
{/* 파일이 선택되었는지 상태를 관리하기 위해 필요한 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);
}
};
isEditing=true일 때, 즉 수정중일 때, "지금이라면 이미지를 클릭하면 아바타를 바꿀 수 있어요!"를 사용자에게 좀 더 명시적으로 보여주고 싶었다. 그래서 수정 중일 때에만 이미지에 마우스 오버 시 transform 효과가 실행되고, 마우스 커서가 pointer로 바뀌게 하고 싶다!
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을 넣어버리는 순간 원하는대로 작동이 되지 않는다.
챗지피티도 모른다....