Programming/Nextjs

[Nextjs] API 기능을 통한 파일 업로드 처리(base64) #2

minarae7 2023. 3. 22. 11:47
728x90
반응형

Nextjs

개요

얼마 전 포스팅에서 nextjs를 통해서 api 구조에서 파일을 업로드하는 내용을 다루었다. 이게 로컬에서 개발할 때는 잘 작동하고 별 문제 없이 백엔드로 파일이 전달이 되었는데, 배포하려고 서버에 올려두고 테스트를 진행하니 backend에서 파일을 전달받을 때 자꾸 파일이 바뀌어서 업로드 되었다.

미세하게 파일 용량이 바뀌어서 업로드되는데 이렇게 되면 파일이 손상되어서 이미지 파일이 브라우저에서 열리지 않는 문제가 발생하였다.

어떤 원인 때문일지 디버깅을 해보다가 파일이 로컬 브라우저에서 nextjs API로 전달될 때 이미 파일이 바뀌 상태에서 업로드되었다.

파일 흐름도

위의 그림과 같이 파일이 전달되는데 브라우저에서 nextjs server로 전달될 때 이미 파일이 바뀐다. 근데 로컬 nextjs를 구동할 때는 문제가 없다. 결론은 컴퓨터에서 설치된 보안 프로그램이 파일을 검사하면서 파일 자체에 뭔가 변경을 주는 것이 아닌가라는 의심이 들었다.

그럼 해결책은 파일을 업로드할 때 이 파일을 파일 자체로 업로드하지 않고 base64로 변경해서 String으로 업로드하여 보안프로그램이 파일을 검색하는 로직을 우회하는 것이다.

물론 이렇게 하면 일단 base64로 인코딩하면서 전달해야하는 용량이 커지고 그래서 트래픽이 증가한다는 단점이 있다. 하지만 일단 프로그램이 정상적으로 작동하도록 하는게 우선이다.

728x90

Client Side

그럼 이제 프로그램을 작성해보도록 하겠다. 우선 브라우저에서 작동될 스크립트를 만들어야 한다.

우선 아래와 같이 두 가지 state 변수를 사용하였다.

const [targetFiles, setTargetFiles] = useState({}); // 파일의 내용을 저장
const [isFilesReady, setIsFilesReady] = useState(true); // 파일을 다 읽었는지 확인

주석의 내용처럼 targetFiles는 실제 파일을 읽어서 base64로 저장하기 위해서 사용하고 isFilesReady는 파일을 다 읽어서 서버에 전달할 준비가 되었는지 확인하기 위해서 사용한다.

그래서 isFilesReady는 파일을 읽기 시작할 때 false로 변경하였다가 파일을 읽어서 변수에 담는 작업이 끝나면 다시 true로 변경하게 된다.

이제 Form에서 File Input을 다음과 같이 작성한다.

<Form.Control
    type="file"
    name="image"
    className="form-control-sm"
    accept="*.png, *.jpg, *.gif"
    onChange={(e) => {
    	setFile(e.target.files);
    }
/>

여기서는 react-bootstrap을 사용한다고 가정하고 코드를 작성하였다. 위의 코드는 이미지 파일만 업로드할 수 있도록 하였으며 내용이 변경되면 setFile 함수를 호출하도록 하였다.

이제 실제로 파일을 읽어서 변수에 담는 역할을 하는 코드를 작성하도록 하겠다.

const setFile = async (files) => {
  let list = {};
  setIsFilesReady(false);  // 파일을 읽기 시작할 때 false로 변경해 둠
  // 파일을 비동기로 읽기 시작한다.
  // parameter가 Object이므로 Object.entries로 array로 치환하여 map 함수를 사용
  const filePromises = Object.entries(files).map(item => {
    return new Promise((resolve, reject) => {
      const [index, file] = item;
      // 파일을 읽기 위해서 FileReader를 생성하고 파일을 읽기 시작
      const reader = new FileReader();
      reader.readAsDataURL(file);
      
      // 파일 읽기가 끝났을 때 처리할 함수
      reader.onload = (event) => {
        list[index] = event.target.result; // 읽은 데이터를 배열에 추가
        resolve();
      };
      
      // 파일 읽기에 실패했을 때 처리 함수
      reader.onerror = () => {
        console.log("Couldn't read the file");
        reject();
      };
    });
  });
  
  // 여기서 비동기로 읽고 있는 파일들이 모두 읽혀질 때까지 대기
  Promise.all(filePromises)
    .then(() => {
      // 파일을 정상적으로 읽었다면 읽은 결과를 targetFiles에 담기
      setTargetFiles(list);
      console.log("Ready to submit");
      // 파일 읽기가 모두 정상적으로 끝났다면 state를 다시 true로 변경
      setIsFilesReady(true);
    })
    .catch((error) => {
      // 예외가 발생했다면 여기서 처리
      console.log(error);
      console.log("Something wrong happened");
    });
};

파일의 내용이 좀 긴데 코드 사이사이에 주석이 작성하였으니 코드를 보는데는 문제가 없을 것이다.

이제 submit을 하는 곳에서 읽은 데이터를 추가하는 작업을 진행하여야 한다.

브라우저에서 동작할 script에서 할 일이 많다.

const onSubmit = (e) => {
  ...
  let formData = new FormData(); // 서버로 전달할 form을 생성
  // formData에 전달할 변수들 담기
  ...
  
  // 파일을 아직 읽고 있을 경우에 대한 처리
  if (isFilesReady === false) {
    alert("파일을 읽는 동안 잠시만 기다려주세요.");
    return;
  }
  
  // 파일에서 읽은 내용을 formData에 추가
  if (targetFiles[0] !== undefined) {
    formData.append('image', targetFiles[0]);
  }
  
  // 이하 내용 생략
  ...
};

여기서는 이미지 파일 하나면 전송할 것이기 때문에 위의 코드와 같이 작성하였으며 파일이 많을 경우는 위의 코드를 참조해서 개발할 수 있을 것이다.

이제 브라우저에서 할 일은 끝났다. api 쪽 코드를 작성해보자.

반응형

Nextjs API Side

api에서 처리하는 코드에서도 해야할 일이 많다. base64로 전달된 내용을 기반으로 다시 파일을 생성하고 이 파일을 다시 전달할 form에 추가해주어야 하고, 파일의 확장자는 뭔지도 알아내야 한다.

우선 파일에 대한 정보를 추출하는 함수를 작성한다.

function DataURIToBlob(dataURI) {
  const splitDataURI = dataURI.split(',');
  const byteString = splitDataURI[0].indexOf('base64') >= 0 
    ? atob(splitDataURI[1])
    : decodeURI(splitDataURI[1]);
  // 파일의 mime값을 추출한다.(확장자를 만들기 위해서)
  const mimeString = splitDataURI[0].split(':')[1].split(';')[0];
  
  // 파일을 다시 binary로 복원
  const ia = new Uint8Array(byteString.length);
  for (let i = 0; i < byteString.length; i++) {
    ia[i] = byteString.charCodeAt(i);
  }
  return {
    data: ia,
    type: mimeString,
  };
}

이제 위의 함수를 통해서 전달된 base64에서 파일의 내용과 mime를 찾을 수 있다. mime 값을 통해서 확장자를 만들어낼 것이다.

마지막으로 파일에 대한 처리를 진행한다.

export default async function (req, res) {
  try {
    ...
    const formData = new ForData();
    const info = DataURIToBlob(fields['image']);
    // 확장자 추출
    const ext = info.type.split('/')[1];
    const filename = moment().format('YYYYMMDDHHmmss') + `.${ext}`;
    fs.writeFileSync('image', fs.createReadStream(filename), {
      filename: filename,
      contextType: info.type,
    });
    
    // 이제 다른 필드를 formData에 추가
    for (const key in fields) {
      // 파일 업로드 관련 항목을 추가하지 않음
      if (['image', 'filename', 'image_path'].includes(key)) {
        continue;
      }
      form.append(key, fields[key]);
    }
    
    // post로 Backend에 전송
    const result = await client.post(url, req, formData);
    // 처리가 완료되었으니 임시로 생성한 파일은 삭제
    fs.unlinkSync(filename);
    ...
  } catch (error) {
    res.status(error.response 
      ? error.response.status : 500)
    ).send(error.response ? error.response.data : error.message);
  }
};

위의 코드에서는 DataURI에서 stream과 mime값을 생성해서 임시로 파일을 만들고 이걸 서버로 전송한 다음에 임시로 생성한 파일은 삭제하도록 하였다.

이렇게 하면 파일을 브라우저에서 nextjs server까지는 파일이 바이너리가 아닌 dataURI로 전송되고 nextjs 서버부터는 이렇게 생성한 정보로 임시로 파일을 만들어서 Backend 서버로 파일을 전달할 수 있다.

728x90
반응형

'Programming > Nextjs' 카테고리의 다른 글

[nextjs] 서버 설정 파일 읽기  (0) 2023.04.27
[Nextjs] API 기능을 통한 파일 업로드 처리  (0) 2023.02.09