데굴데굴

[Next.js] Next.js 13 vercel 배포 시 이미지 로딩 실패 문제 (ENOENT: no such file or directory) 본문

Programming

[Next.js] Next.js 13 vercel 배포 시 이미지 로딩 실패 문제 (ENOENT: no such file or directory)

aemaaeng 2023. 10. 6. 11:57

서론

Next.js 13 프로젝트를 vercel로 배포하고 나니 이미지 로딩에 실패하는 문제가 발생했다.

이 문제를 해결하느라 몇날 며칠을 골머리를 앓아 블로그에 기록을 남겨두려고 한다.
글은 총 두 편으로 올릴 예정인데, 이 글에 나온 방법은 성능 측면에서 좋은 방법이 아니기에 실제로 이 방식을 쓰기보다는 그냥 나의 삽질 여정으로 봐주면 좋을 것 같다.

문제 상황

프로젝트를 vercel로 배포한 후 이미지가 포함된 글에 들어가게 되면 오류가 뜨며 페이지가 로딩되지 않았다.

[Error: ENOENT: no such file or directory, open 'public/media/247845.jpeg'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'public/media/247845.jpeg'
}]

vercel에서 서버 로그를 확인하니 디렉토리나 파일이 존재하지 않는다는 ENOENT: no such file or directory 오류가 떠있었다.

문제의 원인

가설 1. .gitignorepublic/media 폴더가 포함되어 있기 때문이다.

public/media 폴더는 용량이 큰 사진과 영상, 음성 파일이 포함되어 있어 .gitignore에 포함시키고 원격 저장소에는 올리지 않았다.

스크린샷 2023-10-05 오후 4 41 01


처음엔 이게 문제인가 싶었지만 배포는 로컬에서 vercel-cli로 하고 있었고 deployment summary를 확인했을 때 static assets에 잘 들어가있었다. 또, 같은 폴더에 있는 영상과 음성 파일은 아무 문제없이 잘 불러오고 있었기 때문에 이것 때문은 아닌 것 같았다.

가설 2. fs.readFile()로 파일을 직접 읽어오는 코드가 있기 때문이다.

영상과 음성은 별도의 가공 없이 바로 렌더링하고 있었지만 이미지는 로딩 전 블러 이미지를 보여주기 위해 plaiceholder 라이브러리를 사용한 함수를 한 번 거친 후에 렌더링되고 있었다.

plaiceholder로 placeholder 이미지를 생성하려면 파일의 버퍼가 필요한데 그러려면 fs.readFile()로 파일을 읽어와야 했다. 이 과정에서 이미지 파일을 찾지 못해 ENOENT 에러가 발생하는 것으로 판단했다.

해결 방법

내가 떠올린 해결 방법은 두 가지였다.

  1. 절대 경로를 이용한다.
  2. 실행 환경에 따라 이미지를 다른 방식으로 불러온다.

보통의 경우라면 절대 경로를 적용하면 바로 해결된다. 

 

스크린샷 2023-10-05 오후 4 53 48

하지만 내 프로젝트에서는 절대 경로를 이용하면 위 캡쳐처럼 서버리스 함수 용량 초과가 발생해 배포 자체가 불가능했다.

따라서 2번의 방법을 선택했다. (하지만 처음에 언급한 것처럼 이 방법은 완전한 해결 방법이 아니었다.)

 

process.env.NODE_ENV를 이용해 개발 환경과 배포 환경을 구별하여 파일 접근 방식을 다르게 하는 byEnv() 함수를 작성했다.

  const byEnv = async (src: string) => {
    if (process.env.NODE_ENV === "development") {
      return await fs.readFile(path.join("./public", src));
    } else {
      return await fetch(`${process.env.NEXT_PUBLIC_API_DOMAIN}${src}`).then(
        async (res) => Buffer.from(await res.arrayBuffer())
      );
    }
  };
// utils/getBase64.ts
import { promises as fs } from "fs";
import path from "node:path";
import { getPlaiceholder } from "plaiceholder";

const getBase64 = async (src: string) => {
  const byEnv = async (src: string) => {
    if (process.env.NODE_ENV === "development") {
      return await fs.readFile(path.join("./public", src));
    } else {
      return await fetch(`${process.env.NEXT_PUBLIC_API_DOMAIN}${src}`).then(
        async (res) => Buffer.from(await res.arrayBuffer())
      );
    }
  };

  const buffer = await byEnv(src);

  const {
    metadata: { height, width },
    ...plaiceholder
  } = await getPlaiceholder(buffer, { size: 10 });

  return {
    ...plaiceholder,
    img: { src, height, width },
  };
};

export default getBase64;

이렇게 하면 배포 환경에서는 이미지 url을 이용해 파일을 불러오기 때문에 경로 문제가 더 이상 발생하지 않고 잘 불러와졌다.

산 넘어 산

serverless function err

문제가 해결된 줄 알고 신나게 배포를 해보았지만 fetch()로 이미지를 불러오기 때문인지 로딩이 너무나도 오래 걸렸다.

심지어 사진이 많이 포함된 페이지에서는 서버리스 함수가 실행 시간을 초과해 페이지가 아예 다운되어버리는 치명적인 상황이 발생했다.

 

결국 `fetch()`를 쓰지 않고 절대 경로를 이용해야 했는데 이를 위해서는 서버리스 함수의 용량 초과 문제를 해결해야 했다. 

글이 너무 길어질 것 같아 그 과정은 다음 글에 이어서 써보겠다. 

Comments