물건은 잘 받으셨나? (feat. 여러 파일 업로드하기)

홀랄쓰·2024년 10월 12일

JavaScript & TypeScript

목록 보기
2/2
post-thumbnail

# 배경

지난주부터 참여한 프로젝트에서 다수의 파일 업로드하는 기능을 개발하게 되었다. 개발 과정 중에서 팀원들과 함께 논의하고 고민한 경험을 기록해두려 한다.

# 요구사항

요구사항이 꽤나 복잡했는데, 이 글에서는 비동기 처리에 관련된 요구사항만 뽑았다.

  • ...
  • 다수의 파일을 서버에 전송
  • 파일 업로드를 실패한 파일명 따로 모으기
  • 실패한 파일 목록 다른 페이지에서 보여주기
  • ...

# 요구사항 분석

다수의 파일을 서버에 전송

연관이 없는 파일을 처리하기 때문에 순차 처리보다 병렬 처리가 더 적합하다. 순차 처리는 한번의 업로드가 끝나면 다음 파일의 업로드를 진행하는 방식으로 처리한다. 따라서 사용자 입장에서는 대기 시간이 매우 길어진다. 그에 반해 병렬로 업로드를 처리하면 한번에 여러 파일을 처리할 수 있어서 사용자 입장에서는 더 좋다. 하지만 브라우저 - 서버, 서버 - 스토리지 (AWS S3) 간에 많은 대역폭을 소모하고, 부하가 걸린다. 성능 개선을 위해서 사용했지만, 오히려 이러한 이유로 성능 저하를 유발할 수 있다.

파일 업로드를 실패한 파일명 따로 모으기

앞서 설명한 두 방식 중, 나는 병렬 처리 방식이 더 적합하다고 판단했다. 병렬 처리하면서 먼저 떠올린 방식은 Promise.all를 이용하는 방법이었다. 하지만 이 방식은 여러 요청 중 하나라도 실패한다면 전체 요청이 실패로 결론이 난다. 또한 어느 요청에서 실패했는지 클라이언트 입장에서는 알 방법이 없다. 따라서 Promise.allSettled 메소드를 이용하여 어느 요청에서 실패한지 파악하기로 했다.

실패한 파일 목록 다른 페이지에서 보여주기

먼저 파일 업로드에 실패하는 이유는 어떤 것이 있을지부터 고민해봤다.

1. 용량 제한이 넘는 파일을 업로드하는 경우
2. 서버 API에 요청을 보내는 과정에서 발생하는 문제
3. 스토리지에 업로드하는 과정에서 발생하는 문제

나는 이렇게 세 가지 문제를 떠올렸고, 여기서 분류를 나누면 아래와 같이 될 것이라고 생각했다.

1. 클라이언트 단에서의 문제 (1)
2. 서버 단의 문제 (2, 3)

파일을 UI를 통해서 업로드할 때 용량을 확인하여 유저에게 피드백을 주는 방식으로 구현했고, 2,3번은 서버가 업로드 API에 대한 응답에 특정 코드를 내려주면 그에 따라 유저에게 피드백하는 방식으로 처리했다. 해당 코드를 받으면 에러를 던져서 Promise.allSettled의 결과에 rejected 상태인 파일을 뽑아냈고, 이를 별도 페이지에서 유저에게 보여주었다.

# 개발 과정

1. ReactQuery 함께 사용하기

우리팀은 ReactQuery를 사용하고 있다. Promise.allSettled를 활용하기 위해서 mutateAsync를 사용했다. 처음에는 업로드 실패한 파일들을 전역 상태로 관리하는게 좋겠다고 생각했다. 하지만 MutationCachevariables를 참조하여 Request에 대한 정보를 알아낼 수 있는 방법이 있었고, 이를 사용하여 업로드 실패한 파일을 노출했다.

2. 파일을 전송하기 위한 타입

기존에는 파일 업로드를 base64로 encoding하여 서버로 전송했다. 이 프로젝트에서는 파일 크기가 MB 단위일 수 있다는 점, 그리고 그러한 파일들이 여러 개가 업로드 될 수 있다는 점들이 계속해서 신경쓰였다. base64로 파일 업로드하는 방식은 각 파일마다 encoding / decoding을 거쳐야 하고, 성능 부하가 걸리며 크기 또한 증가하기 때문이다. 따라서 서버 개발자와 논의 끝에 Multipart나 Binary 형태로 처리하도록 논의했다. (아직 논의중)

# 배운점

용어

이 프로젝트를 하면서 비동기 처리에 관한 많은 글을 읽어볼 수 있었다. 그중 가장 기억에 남는 것은 동시(Concurrency)과 병렬(Parallelism)의 차이이다.

동시: 1명의 계산원이 2줄의 고객들의 계산을 동시에 해주는 것
병렬: 2명의 계산원이 2줄의 고객들의 계산을 한번에 해주는 것

동시적이라는 것은 순차적으로 실행되지만, 그 실행된 작업이 끝날 때까지 기다릴 필요가 없다는 것이고,
병렬을 여러 작업을 분리된 환경에서 동시에 실행한다는 것이다.

그렇다면 Promise.allPromise.allSettled는 어떻게 동작할까?
바로 동시적으로 동작한다. 요청한 비동기 작업들을 이벤트 루프 큐에 한번에 추가하지만, 작업은 하나씩 순차적으로 시작한다.

time  1 2 3 4 5 6 7
p1    ---------
p2      ----------
p3        --------
p4           ---

# 결론

지금까지 내가 어떻게 비동기 처리를 했는지 얘기해보았다. 이 프로젝트를 잘 마무리 짓기 위해서 팀원, 담당자들과 많은 소통을 했고, 개인적으로 많은 자료를 찾아보았다. 이 과정에서 많은 것을 배울 수 있었고, 한층 성장한 것 같다. 머릿속에 정리된 것들을 기억하고자 이 글을 작성했다.

또한, 많은 블로그 포스트를 찾아보다가 이동욱(향로)님이 작성하신 Promise pool에 대한 게시글을 읽어볼 기회가 있었다. 현재 진행중인 프로젝트를 배포하고 운영하면서 도입을 고민해보려한다.

# 참고자료

profile
FE맛집이 되고 싶다.

0개의 댓글