[ 공모전 ] Binary Data : Node.js - formidable, form-data, bodyParser

최문길·2024년 8월 3일
0

공모전

목록 보기
42/46
 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로 보냈다.

issue - 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#�YSS]���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��,[UX���k��8��
l���#������M���Y����?EX5����l"d��`8G�(I)*Z��V#��Bj�lO�)����#��/}[mW+nzK��qQKY��841Eڴ�#�g��r���i��9�iɦee��jSz��zA��L'��߬
�*Q���;/8�݋��M����5�������U��:<7<3f�RA��Q�$��k
l��?62%���缿�@���j�       �       �       ������Jr$@$@$@$@$@$@$@$@$@$@$@$@|�'���v��(��l�:5k�H�
�q�;4���w��n��bs��!(BVE����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 formidable

서버측(Node.js)으로 보낸 form 데이터를 쉽게 처리하기 위한 nodejs 기반 middleware인 formidable 라이브러리를 사용하여 처리하는 방식이 있다.

formidable을 사용하려면 데이터의 값을 실제 우리(사람)가 아닌, 컴퓨터가 인지 할 수 있는 rawbody (데이터)를 client단에서 받아와야한다.

formidable을 사용하면 얻는 이점은

  • raw한 데이터를 인코딩 깨짐 이슈없이 Buffer형태로 받아올 수 있다.
  • formData의 구분선을 알아서 처리해주고, field,files를 분리해서 얻을 수 있다.

위의 2가지 이점이 있기에 사용해서 데이터를 처리한 후, 다시 서버로 보내는 방법이다.

여기서 주의 해야 할 점은 api router에서 bodyParser=false를 해야 raw한 데이터 값을 받아올 수 있다.

번외 ) bodyParser와 req.on('',()=>)

아직 백엔드 지식은 없지만, bodyParser를 false로 변경해줘야 함은, 기본적으로 node.js는 업데이트를 거치면서 bodyParser 내장시켰는데, 이를 사용하면, request요청에 적재되어있는 body(본문)을 추출해서 req.body로 변형시킨다.
즉, 우리가 아는 req.body값인 것이다.

물론 bodyParser이전에는 requeston 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도 있다. ) , 로직을 작성해주면 된다.

formidable

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를 인자로 받는데, 이 reqformidable 미들웨어가 parse하면서 formData를 추출하면서, filefields를 구별하여 return 하는 함수이다.

bodyPrser - false

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에서 값을 확인해보면

fieldsfiles 의 값이 보인다.


formidable을 안해줬다면
------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#�YSS]���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��,[UX���k��8��
l���#������M���Y����?EX5����l"d��`8G�(I)*Z��V#��Bj�lO�)����#��/}[mW+nzK��qQKY��841Eڴ�#�g��r���i��9�iɦee��jSz��zA��L'��߬
�*Q���;/8�݋��M����5�������U��:<7

이걸 처리했어야 했는데... 다행이였다.


pnpm i form-data

다시 돌아와서

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 서버로 보내면 이 객체는 빈 값이고,
    실질적인 데이터는 없는 그런 것으로 이해 했다.

번외 - fs, 파일시스템

  • 파일시스템이란 : 컴퓨터의 저장 장치(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의 라이브러리들과 데이터 통신을 해봤다.
해보면서, 협업을 위해서 서버쪽 지식도 필요하다는 것을 느낀다.

참고

0개의 댓글

관련 채용 정보