Client/ReactJS & React-Native

React에서 formData로 텍스트와 이미지 전송하기

Juzdalua 2022. 1. 26. 23:50

백엔드에서만 파일전송을 구현한다면 multer와 enc-type만 지정하면 됐었다.

프론트를 React로, 백엔드를 NodeJS로 포트를 나눠 구현하다보니 파일 전송조차도 쉽지는 않았다.

 

백엔드 개발자가 프론트엔드를 공부하다보니, 리액트가 아니라 그냥 자바스크립트를 활용하는 경우가 많았다...

너무 부족하지만 프론트에 시간을 쏟는게 아까워 나중을 기약하고 넘어갔다.

 

어쨌든 오늘은 프론트에서 서버로 파일을 어떻게 넘기는지, 내가 구현했던 방법을 작성하겠다.

 

 

return (
    <div>
        <form id="toDoForm" className="toDoForm" onSubmit={onSubmit} encType="multipart/form-data" >            
            <textarea className="toDo" onChange={onChange} value={toDo} type="text" placeholder="메모를 입력하세요." />                
            <div className="toDo__img-container">
                <label htmlFor="img">사진 추가하기</label>
                <input className="toDo__img-item" id="img" type="file" accept="image/*" onChange={onImageHandler}/>
                <div className="toDo__img-preview">
                    {imgPreview === '' ? null : 
                        <img className="toDo__img-preview" src={imgPreview} />
                    }
                </div>
            </div>
            <button className="toDoBtn">추가하기</button>
        </form>
    <div>
);

먼저 폼을 구현했다.

 

const [toDo, setToDo] = useState("");
const [image, setImage] = useState()
const [imgPreview, setImgPreview] = useState("");    

const dispatch = useDispatch();
const navigator = useNavigate();

그리고 state들을 선언해준다. 텍스트와 전송을 위한 이미지, 그리고 이미지 프리뷰까지.

 

const onImageHandler = (event) => {        
    setImage(()=>event.target.files[0]);

    //set preview image
    objectUrl = URL.createObjectURL(event.target.files[0])
    setImgPreview(objectUrl);
};

이미지를 선택하면 state에 파일을 담아준다.

잠깐 샛길로 빠지자면, URL을 만들어 이미지 프리뷰에 넣어주면 이미지를 확인할 수 있다.

파일은 항상 올리면 배열에 담긴다. 그러므로 0번째 배열이 첫번째 우리의 파일.

어떻게 담겨있는지, 어떤 정보들을 담고있는지 궁금하다면 console.log를 찍어보자.

 

const onChange = (event) => {
    setToDo(event.target.value);        
};

const onSubmit = async (event) => {        
    event.preventDefault();
    if(toDo === "")
        return;

    setToDo("");   
    setImgPreview("");     
    document.querySelector(".toDo").focus();        


    if(getItem('user')){              
        let body;

        body = new FormData();
        body.append("toDo", toDo);
        body.append("user",  JSON.stringify(getItem('user')));
        if (image) 
            body.append("images", image);

        // body = {
        //     toDo: toDo,
        //     images: image ? JSON.stringify(image) : null,                
        //     user: JSON.stringify(getItem('user'))
        // };              

        const response = await dispatch(writeMemo(body));
        if(response.status === 200){
            URL.revokeObjectURL(image);
            setImage("");
            // navigator("/");
            window.location.href = '/'
        };

    };              

};
useEffect(() => {   

}, [image]);

다시 돌아와서, 메모를 작성하면 state에 담아준다.

그리고 추가하기 버튼을 누르면 state에 담긴 form 정보들을 formData에 담아준다.

그냥 form 형식으로 전송한다면 파일은 전송되지 않는다.

 

formData에 객체를 담으면 string으로 변경되어 저장된다.

백엔드에서 string타입을 json 객체타입으로 바꿔 사용하기 위해 나는 JSON.stringify를 사용했다.

이 부분에서 엄청 난처했지만, stackoverflow 형님들이 도와줬다.

 

//authorized user
export const ssoInstance = axios.create({
    baseURL: process.env.REACT_APP_API_URL,
    headers: {
        Authorization: `${getItem('token')}`,
        "Content-Type": "application/json",
    },
});

나는 악시오스를 따로 axios.js에서 관리한다.

메모 작성은 로그인 된 유저만 가능해야하기에 토큰을 함께 헤더에 담아줬다.

Content-Type은 항상 json으로 파일을 전송하기 때문에 application/json으로 설정했다.

이를 받기 위한 백엔드에서 설정이 필요한데, 이는 다음편에서 다뤄보겠다.

 

//create memo
export const writeMemo = (data) => {
    return async(dispatch) => {
        try {            
            const response = await ssoInstance.post('api/memo/create', data);
            dispatch({
                type: actions.CREATE_MEMO,
                payload: response
            });            
            return response;
        } catch (error) {
            return error.response;
        };
    };
};

actionCreator.js에서는 서버와 통신하기 위한 설정을 해준다.

try catch문에서 error.response를 활용하면 에러를 쉽게 확인할 수 있다.

 

const actions = {
    CREATE_MEMO: 'CREATE_MEMO',
}

export default actions;

리덕스의 액션을 지정해주고

 

import actions from "../users/actions";

const memoReducer = (state ={}, action) => { 
    switch (action.type) {
        case actions.CREATE_MEMO:
            return {
                ...state,
                payload: action.payload
            };
        default:
        return state;
    }
};

export default memoReducer;

리듀서를 작성해준다. 참고로 루트리듀서를 생성하고 리덕스 썽크를 활용해야 비동기로 데이터를 보낼 수 있으니 이 부분은 구글링을 참고해보시길!

 

이렇게 하면 백엔드 서버의 req.body에서 데이터를 받을 수 있다.

그럼 multer를 활용한 백엔드 데이터를 다루는 방법은 다음편에 소개하겠다.