const submitForm = async (value: FieldValues) => {
const formData = new FormData();
Object.entries(value).forEach((item) => {
if (item[0] === "file") formData.append(item[0], item[1][0]);
formData.append(item[0], item[1]);
});
// formdata가 body에 들어갔으므로 브라우저가 Content-Type을 multipart/form-data로 바꿔서 요청보낸다.
const result = await axios.post("/api/binary/handler", value);
}
코드를 작성 후에 api router로 보냈다.
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const method = req.method;
const url = req.url;
const body = req.body;
console.log(body);
res.status(200).json({ body, method, url });
실행 결과를 api router의 terminal창에서 확인해보면,
FormData값이 찍히기는 찍히지만, file의 encoding이 깨진다.
// body값
------WebKitFormBoundaryr6H187HqeF3oLOWI\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n
12345ㅁㄴㅇ // text값
\r\n
------WebKitFormBoundaryr6H187HqeF3oLOWI\r\nContent-Disposition: form-data; name=\"file[]\
//...생략
=�]P��ڌ�'{ED6#^�*G���Un
RM5�OsEH��Y���!'J����9#�Y�SS�]���w�`��d�H�P22��s�E��K�Y�sᅦ͵5<z�
����t�d��vhª
���jdcӊ�mBӴSM`=�7��XX�%c-���_�����{�I�:)�*��Ȼ�BY��Ev�D���.���ӥ�'����|��
��[��1��#S�g�7��u�5V��,[U�X���k��8��
l���#������M���Y����?EX5����l"d��`8G�(I)�*Z��V#��Bj�lO�)����#��/}[mW+nzK��qQKY��8�
4�1�Eڴ�#�g��r���i��9�iɦee��jSz��zA��L'��߬
�*Q���;/8���M����5�������U��:�<7<3f�R�A��Q�$��k
l��?�62%���缿�@���j� � � ������Jr$@$@$@$@$@$@$@$@$@$@$@$@|�'���v��(��l�:5k�H�
�q�;4���w��n��bs��!�(B�VE����Y��^&8NXx)�%��
"�v�
�%�5Z"�z �f�Ng5�&)�vv��i�P]����v�;�&;a�q��e���P�z��Z1�w����k^@/ڳ�2�^V ���ַlA8
����E�f��ƌ�>-�f]�V3F�wm�ت�ϴ!�� �^�R[w��)r��<��}�8
DI��
인코딩이 화려하게 깨지기에,
단순히 body 값을 우리쪽 서버측에 보내는 방식으로 선택했었는데, 에러가 발생해서 문제에 직면했다.
특히나, 왜 그런지를 튜터, stackoverflow등을 찾아봐도 별 다른 소득이 없었다.
네트워크 탭의 preview
를 확인했을 때도 위와 같은 인코딩 깨짐 이슈가 있기에, 무언가 통신하는 네트워크 경로쪽에서 전송과정에서 손상된것 같다.
그렇다면 어떻게 해결해야 하나...? 🙄
서버측(Node.js)으로 보낸 form 데이터를 쉽게 처리하기 위한 nodejs 기반 middleware인 formidable
라이브러리를 사용하여 처리하는 방식이 있다.
formidable을 사용하려면 데이터의 값을 실제 우리(사람)가 아닌, 컴퓨터가 인지 할 수 있는 rawbody (데이터)를 client단에서 받아와야한다.
formidable을 사용하면 얻는 이점은
위의 2가지 이점이 있기에 사용해서 데이터를 처리한 후, 다시 서버로 보내는 방법이다.
여기서 주의 해야 할 점은 api router에서 bodyParser=false
를 해야 raw한 데이터 값을 받아올 수 있다.
물론 bodyParser이전에는 request
의 on
method를 사용해서 chunk 단위로 값을 받고, 그 값들을 다시 Buffer객체 형태로 만들어서 데이터를 처리한다.
// if bodyParser = true
function api(req,res){
req.body // 우리가 알고 있는 body 값
}
// if bodyParser =non or false
function api(req,res){
req.on('data',(chunk)=>{
//... 데이터가 스트리밍 방식으로 전송되며, chunk는 이 데이터의 일부분을 나타낸다.
// chunk의 타입은 buffer이며 buffer는 이진데이터를 Node.js에서 다루기 위한 객체이다.
})
req.on('end',()=>{...})
}
다시 돌아와서
pnpm i formidable // 설치
formidable라이브러리를 설치하고 ( multer library도 있다. ) , 로직을 작성해주면 된다.
function formidablePromise(
req: NextApiRequest,
opts?: Parameters<typeof formidable>[0],
): Promise<{ fields: formidable.Fields; files: formidable.Files }> {
return new Promise((res, rej) => {
const form = formidable(opts);
form.parse(req, (err, fields, files) => {
if (err) {
return rej(err);
}
return res({ fields, files });
});
});
}
formidable은 비동기 함수이므로 Promise
를 return하는 함수를 만들었다.
formidablePromise
함수에서 req
를 인자로 받는데, 이 req
를 formidable
미들웨어가 parse하면서 formData를 추출하면서, file
과 fields
를 구별하여 return 하는 함수이다.
export const config = {
api: {
bodyParser: false,
},
};
이때 Next.js
에서는 기본적으로 api router에서 bodyParser
의 값이 true로 받아오기 때문에, false
로 변환 해줘야 formidable
로 rawBody를 파싱하면서 데이터로 변환 해준다.
// client
const submitForm = async (value: FieldValues) => {
const _file = value.file[0];
const result = await axios.postForm("/api/binary/handler", {
...value,
file: _file,
});
console.log(result);
};
return (
<form onSubmit={method.handleSubmit(submitForm)}>
<input type="text" {...method.register("text")} />
<input type="file" {...method.register("file")} />
<button type="submit">제출</button>
</form>
//...생략
// api router
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
const { fields, files } = await formidablePromise(req, {
keepExtensions: true,
});
//...생략
이렇게 terminal에서 값을 확인해보면
fields
와 files
의 값이 보인다.
------WebKitFormBoundaryr6H187HqeF3oLOWI\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n
12345ㅁㄴㅇ // text값
\r\n
------WebKitFormBoundaryr6H187HqeF3oLOWI\r\nContent-Disposition: form-data; name=\"file\
//...생략
=�]P��ڌ�'{ED6#^�*G���Un // file값
RM5�OsEH��Y���!'J����9#�Y�SS�]���w�`��d�H�P22��s�E��K�Y�sᅦ͵5<z�
����t�d��vhª
���jdcӊ�mBӴSM`=�7��XX�%c-���_�����{�I�:)�*��Ȼ�BY��Ev�D���.���ӥ�'����|��
��[��1��#S�g�7��u�5V��,[U�X���k��8��
l���#������M���Y����?EX5����l"d��`8G�(I)�*Z��V#��Bj�lO�)����#��/}[mW+nzK��qQKY��8�
4�1�Eڴ�#�g��r���i��9�iɦee��jSz��zA��L'��߬
�*Q���;/8���M����5�������U��:�<7
이걸 처리했어야 했는데... 다행이였다.
다시 돌아와서
node.js에서 formData를 처리하기 위해 라이브러리를 설치하자
pnpm i form-data
설치 후에 다음과 같이 코드를 작성한다.
import FormData from "form-data";
//... 생략
const formData = new FormData();
if (files && files.file) { // 업로드된 파일의 field 이름이 `file`이름임
const _file = files.file[0] as formidable.File;
const readFile = fs.createReadStream(_file.filepath);
formData.append("file", readFile);
}
for (const key in fields) {
formData.append(key, fields[key]![0]);
}
await axios.postForm(
"http://localhost:4000/users",
JSON.stringify(formData),
);
//...생략
이렇게 formdata
의 프로퍼티로 종속시켜 서버와 통신하면 된다.
여기서 fs.createReadStream
이 있는데, 왜 _file
이 아닌, fs
메소드를 통해 변경된 값을 formdata에 보내지는지 잠깐 설명하자면
_file
객체 : 파일의 메타데이터와 경로를 포함할 뿐 실질적인 데이터가 아니다.- 파일 스트리밍 : 파일을 전송 할 때는 파일의 내용을 스트리밍 방식으로 읽어야 하기에 파일 시스템의 메소드를 통해 객체 값을 읽어들이는 실질적인 데이터이다.
쉽게 내가 이해한 대로 말하자면,
Javascript에서 브라우저 환경에서 생성된File
객체를 직접 Node.js 서버로 보내면 이 객체는 빈 값이고,
실질적인 데이터는 없는 그런 것으로 이해 했다.
파일시스템이란
: 컴퓨터의 저장 장치(ex, 디스크, 등)에서 파일과 디렉토리를 관리하고 조직하는 구조를 의미.
파일이란
: 데이터나 정보를 저장하는 단위,
저장된 데이터의 유형은 문자열데이터, 객체 데이터, 바이너리 데이터(이미지, 비디오)
fs란
: Node.js에서 파일시스템과 상호작용할 수 있는 다양한 기능을 제공한다. 파일을 주고 받을 수 있는 fs.createReadStream
파일 스트림을 생성 할수 있다.
import { NextApiRequest, NextApiResponse } from "next";
import formidable from "formidable";
import fs from "fs";
import FormData from "form-data";
import axios from "axios";
function formidablePromise(
req: NextApiRequest,
opts?: Parameters<typeof formidable>[0],
): Promise<{ fields: formidable.Fields; files: formidable.Files }> {
return new Promise((res, rej) => {
const form = formidable(opts);
form.parse(req, (err, fields, files) => {
if (err) {
return rej(err);
}
return res({ fields, files });
});
});
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
const { fields, files } = await formidablePromise(req, {
//기본적으로 Formidable은 임의의 이름을 생성하고 파일 확장자를 제거한 채 파일을 저장하지만, keepExtensions: true로 설정하면 원래 확장자가 유지
keepExtensions: true,
});
const formData = new FormData();
if (files && files.file) {
const _file = files.file[0] as formidable.File;
const readFile = fs.createReadStream(_file.filepath);
formData.append("file", readFile);
}
for (const key in fields) {
formData.append(key, fields[key]![0]);
}
await axios.postForm(
"http://localhost:4000/users",
JSON.stringify(formData),
);
res.send(200);
} catch (err) {
res.send(err);
}
}
export const config = {
api: {
bodyParser: false, // rawBody로 인코딩 깨지지 않게 하기 위해,
},
};
전반적인 node.js의 라이브러리들과 데이터 통신을 해봤다.
해보면서, 협업을 위해서 서버쪽 지식도 필요하다는 것을 느낀다.