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

lunaxislu·2024년 8월 3일

공모전

목록 보기
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#�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 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이전에는 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도 있다. ) , 로직을 작성해주면 된다.

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를 인자로 받는데, 이 req를 formidable 미들웨어가 parse하면서 formData를 추출하면서, file 과 fields를 구별하여 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에서 값을 확인해보면

fields 와 files 의 값이 보인다.


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#�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

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


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개의 댓글