회원가입을 위해 필요한 6개 유저 정보(email, password, username, accountname, intro, image) 중 image를 전달하는 과정을 정리했다.
(나머지 정보들은 input에서 바로 받아오는 값이기 때문에 그렇게 어렵지 않다)
moonhee.jpg ▶ 123123.jpg ▶ API/123123.jpg ▶ 문자열(AAASDEEEAFSDQ08AZCBCNVZRNZRRREE)
프로필 컴포넌트의 return은 아래와 같다. Wrapper로 label를 감싸고 그 안에 img태그가 있다. 이 img 태그는 프로필 이미지 우측 하단의 작은 동그라미 아이콘이다.
그리고 Wrapper 밖에는 input 태그가 있다. 이 input 태그는 file 타입이다. 파일을 하나 혹은 여러 개 선택할 수 있게 해주는 기능이 있다. 이 input 태그에 id와 Wrapper 안에 있는 label의 htmlFor을 profileImg로 동일하게 줌으로써 두 요소가 연결되게 해놨다. 이렇게 한 이유는 기본적으로 제공되는 input 디자인 말고 다른 이미지를 쓰고 싶기 때문이다. input을 숨기고 두 요소를 연결해줌으로써 label 안의 아이콘을 누르면 input의 기능이 동작하게 된다.
return (
...
<Wrapper ref={previewImage}>
<Label htmlFor="profileImg">
<Img src={upload_icon} alt="프로필 이미지 업로드"/>
</Label>
</Wrapper>
<Input
type="file"
accept="image/*"
id="profileImg"
onChange={handleImageChange}
/>
...
언뜻 생각했을 때는 이미지를 선택해서 브라우저에 보이게 하는 거니까 서버에 뭔가를 전송해야한다고 생각하지 못했다. 먼저 서버에 전송해야 하는 이유는 최종적으로 회원가입 요청을 보낼 때 이 이미지를 문자열로 전송해야 하기 때문이다.
예를 들면, 이런 형태로 최종적으로 회원가입 폼에 담아 보내야 한다.
API/1111111111.png
최종적으로 이렇게 나와야 한다는 걸 알았으니, 이제 '1111111111.png'와 같이 파일 이름을 숫자로 바꿔주기 위해 이미지를 서버에 전송해보자.
const [image, setImage] = useState('기본값으로 들어갈 이미지 URL');
const previewImage = useRef();
async function onLoadImage (formData, loadImage) {
try {
const config = {
headers: {
"Content-Type": "multipart/form-data",
},
};
const response = await axios.post(
`${API_URL}/image/uploadfile`,
formData,
config
);
// 응답에 파일이름이 있으면, image 값을 API + filename으로 지정하고
// files정보(loadImage)를 가지고, 이미지를 보이게 해주는 함수를 실행한다
if (response?.data?.filename) {
setImage(`${API_URL}/` + response?.data?.filename);
preview(loadImage);
} else {
alert('.jpg, .gif, .png, .jpeg, .bmp, .tif, .heic 파일만 업로드 가능합니다.');
}
} catch (error) {
console.error(error);
alert('잘못된 접근입니다.');
}
};
위 코드의 image는 최종적으로 회원가입 폼에 담겨서 전달할 image의 값이다. 그리고 이미지를 감싸고 있는 Wrapper에 접근하기 위해 useRef를 사용한다. Wrapper의 background-image를 조정해서 선택한 이미지가 보이게 만들거기 때문이다.
onLoadImage는 formData와 loadImage라는 인자를 갖는다. formData는 폼 데이터 객체를 의미하고 loadImage는 files 정보이다(2.에 관련내용 나옴).
POST 요청을 보내기 위해 본문에 담길 FormData
와 헤더에 담길 config
가 필요하다. formData는 2.에서 알아보고 config부터 살펴보면, Body에 들어갈 데이터인 formData의 타입을 명시하기 위해 사용한다. form은 컨트롤 요소로 구성된다.
- name: form의 이름. 서버로 보낼 때 이 이름으로 데이터가 전송된다.
- action: form이 전송되는 서버 URL 또는 HTML 링크
- method: 전송 방법 설정(기본값: GET)
- autocomplete: 자동완성 여부
- entype: 폼 데이터가 서버로 제출될 때 해당 데이터가 인코딩 되는 방법
마지막 entype의 값 중에 하나가 multipart/form-data
이다. 이 값은 모든 문자를 인코딩하지 않음을 명시하는데, 이 방식은 form 요소가 파일이나 이미지를 서버로 전송할 때 주로 사용한다.
전송 후 if 문으로 response에 filename이 있으면 image값을 API/filename
으로 변경해주고, 브라우저에 보이게 만들어주는 preview라는 함수에 loadImage(input의 files 정보)를 담아 실행시켜준다.
function handleImageChange (event) {
const loadImage = event.target.files;
const formData = new FormData();
formData.append('image', loadImage[0]);
onLoadImage(formData, loadImage);
}
최종적인 이미지 URL을 그리기 전에 필요한 인자인 formData
와 loadImage
를 정의하는 구간이다. 순서상으로는 1. 보다 앞이지만 거꾸로 올라가는게 더 도움이 될 것 같아서 이 순서로 구성했다.
loadImage
라는 변수에 files 정보들을 담아준다. files는 file 타입의 input이 가지고 있는 file에 대한 정보가 담겨있다(로컬에 저장되어 있는 '파일이름.확장자' 등). 그리고 새로운 formData 객체를 생성한다. 이 객체에 image라는 이름의 값으로 loadImage[0], 즉 0번째 파일을 지정해준다.
formData(폼데이터객체)와 loadImage(files 정보)를 가지고 이미지를 서버에 전송하는 함수인 onLoadImage를 실행한다(1. 내용).
function preview(loadImage) {
const reader = new FileReader();
reader.onload = () => (
previewImage.current.style.backgroundImage = `url(${reader.result})`
);
reader.readAsDataURL(loadImage[0]);
};
loadImage(input의 files 정보들)를 가지고 미리보기 기능을 만든다.
FileReader
라는 생성자 함수를 하나 만든다. FileReader는 파일의 내용을 읽고 사용자의 컴퓨터에 저장하는 생성자 함수이다.
reader.onload
는 읽기 동작이 성공적으로 완료될 때마다 실행된다. input의 files 정보들을 성공적으로 읽었다면 Wrapper에 접근해서 background-image를 변경시킬 수 있는 코드다.
돔을 조작하고 나면 reader.readAsDataURL
메서드를 사용하여 result 속성에 문자열 데이터(엄청나게 김)가 담기게 한다.
const reqData = {
user: {
username: username,
email: auth.email,
password: auth.password,
accountname: accountname,
intro: intro,
image: image
}
};
이렇게 image 한 글자에 문자열 데이터를 넣기 위해 복잡한 과정을 거쳤다. 선택한 파일을 폼 데이터에 담아 제출하기 위해서는 문자열(엄청나게 길다)로 담아야 하고 문자열로 담으려면 API 뒤에 숫자.확장자 형식으로 만들어 주고 이렇게 만드려면 input의 file 정보를 가져와야 한다.
이미지가 한개라서 다행이지, 여러개는 또 어떻게 할까 모르겠다...