본문으로 바로가기
검색
회원가입로그인

GCS 연동 시 Buffer와 Uint8Array 타입 호환 에러 수정

chore: node 버전 업그레이드 및 gcs 타입 수정 by SihyeonHong · Pull Request #70 · SihyeonHong/EasiestCV

위 PR 링크에 내 코드와 연관된 자세한 설명 있음.


상황

GCS(Google Cloud Storage)에 업로드한 이미지 파일을 다운로드받는 downloadFile() 함수에서 Buffer와 Uint8Array 간 타입 충돌.

수정 전 코드

export const downloadFile = async (filename: string) => {
  const file = bucket.file(filename);

  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];

    file
      .createReadStream()
      .on("data", (chunk) => chunks.push(chunk))
      .on("error", (err) => reject(err))
      .on("end", () => resolve(Buffer.concat(chunks)));
  });
};

배경

이 프로젝트는 Next.js API Route에서 GCS를 호출하는 구조이므로, 실제 실행 환경은 Node.js이지만, 타입 체크는 bundler 기준으로 진행되면서 타입 충돌이 발생.

Uint8Array 타입과 Buffer 타입의 차이

Buffer extends Uint8Array

Buffer는 Uint8Array를 상속받는 하위 클래스.

Uint8Array

  • 웹 표준 타입: 파일 스트림 방식으로 데이터를 받아올 때 각 청크를 Uint8Array로 처리.

  • Next.js 기본 moduleResolution: bundler 는 클라이언트 친화적으로 타입을 체크하기 위한 옵션. 브라우저/서버 통합 환경을 위해 웹 표준 타입 Uint8Array을 우선 적용하는 성질이 있다.

Buffer

  • Node.js 고유 타입.

  • 서버 전용 라이브러리(GCS SDK, Node.js Buffer 등)를 사용할 때는 moduleResolution: node가 더 적절. Node.js 전용 라이브러리로서 Node.js 고유 타입인 Buffer 를 기대.

문제점

  • 내가! 타입 지정을 잘못함!: downloadFile()의 반환 타입은 Promise<Buffer> 여야 하므로, chunksBuffer[] 여야 한다고 오인.

    • file.createReadStream()을 통해 Node.js 스트림이 주는 데이터는 Uint8Array 가 맞음.

  • @types/node버전 18까지는 내가 Uint8Array로 들어오는 데이터를 저렇게 강제로 Buffer 로 형변환해도 받아줬었지만, @types/node버전 20부터는 이 두 타입 간 호환성 관련 정의가 변경되면서 더 이상 이렇게 순순히 호환되지 않음.

    • Buffer.concat() 호출 시 Argument of type 'Buffer[]' is not assignable to parameter of type 'readonly Uint8Array<ArrayBufferLike>[]' 타입 에러 발생.

해결

수정한 코드

export const downloadFile = async (filename: string): Promise<Buffer> => {
  const file = bucket.file(filename);

  return new Promise((resolve, reject) => {
    const chunks: Uint8Array[] = [];

    file
      .createReadStream()
      .on("data", (chunk: Uint8Array) => chunks.push(chunk))
      .on("error", (err) => reject(err))
      .on("end", () => {
        // Uint8Array를 Buffer로 변환
        const totalLength = chunks.reduce(
          (sum, chunk) => sum + chunk.length,
          0,
        );
        const result = new Uint8Array(totalLength);
        let offset = 0;

        for (const chunk of chunks) {
          result.set(chunk, offset);
          offset += chunk.length;
        }

        resolve(Buffer.from(result));
      });
  });
};
  • .on("data", (chunk: Uint8Array) => chunks.push(chunk)) 파라미터 타입 명시.

  • 이에 따라 const chunks: Uint8Array[] = []; 타입 올바르게 지정.

  • 반환값 타입 맞추기 위해 .on("end"... 에서 Uint8ArrayBuffer로 직접 변환하는 코드 추가.

  • 반환값 헷갈리지 않으려고 downloadFile() 반환값 타입 명시.

여담

처음 이 문제 발생 당시에는 나의 타입 지정 오류를 제대로 이해하지 못해서 @types/node 버전을 20에서 18로 다운그레이드하는 것으로 해결했다. 당시 Next.js 공식 런타임과 Vercel 기본 런타임 둘 다 Node 18 기준이었으니, 운영 환경과 타입 정의를 맞추는 것이 합리적으로 보이기도 했다.

그러나 오늘 Vercel에서 Node.js 18 지원을 9월 1일부로 중단할 거니까 그 전에 빨리 업그레이드하라는 메일이 와서 부랴부랴 고쳤다... 그땐 이해 못 한 걸 지금이라도 이해해서 다행이다.