Next에서 정적 미디어파일 사용하기

2023. 3. 19. 15:05React/Next.js

mp3나 mp4같은 파일을 정적으로 넣어두고 사용할 때, output 경로를 잘 설정해줘야한다.

이런 설정은 기본적으로 next.config.js에서 해줘야하는데 일반 React와는 다른 점이 있어서 공유해보려고 한다.

module과 webpack 관련 설정 내용이므로 custom.d.ts와 next.config.js를 수정하면서 대응할 수 있다.

 

1. <audio /> 태그 사용법


먼저, DOM에서 음성파일을 사용하려면 audio 태그를 사용하면 된다.

const MusicPlayer = () => {
  const audioRef = useRef<HTMLAudioElement>(null);
  return (
    <audio muted controls playsInline loop ref={audioRef}>
      <source src={''} type={'audio/mp3'} />
    </audio>
  );
}

export default MusicPlayer;

 

audio의 src 안에 정적 경로를 넣어주면 되는데, 상태로 관리하지 않고 ref를 사용한 직접 DOM 조작을 사용하여 관리한다.

controls 속성을 사용하면 아래처럼 내장된 audio UI를 사용할 수 있다.

 

 

그리고 정적 경로를 담은 배열의 커서를 바꾸면서 src를 조작하면 간단하게 플레이리스트를 구현하는게 가능하다!

이 때 사용자 경험을 위해서 곡 제목 등을 바뀌는 걸 <button /> 태그 등으로 보여주면 다채롭게 audio태그를 사용할 수 있다.

 

interface Props {
  playlist: { src: string, name: string }[];
}

const MusicPlayder = ({ playlist }: Props) => {
  const audioRef = useRef<HTMLAudioElement>(null);
  const [cursor, setCursor] = useState(0);
  
  // const handleMusic = () => {} 다음곡, 이전곡 등 조작
  // const toggleMusic = () => {} 일시정지, 재생 등 조작
  
  useEffect(() => {
    // 0번째 playlist의 정적경로 삽입. 
    audioRef.current?.src = playlist[cursor].src;
  }, []);
  
  return (
    <>
      <button>{playlist[cursor].name}</button>
      <audio muted playsInline loop ref={audioRef}>
        <source src={''} type={'audio/mp3'} />
      </audio>
    </>
  )
}

 

2. file-loader를 사용한 정적 경로 가져오기


사용은 어렵지 않지만, React나 Next에 기본적으로 mp3 같은 파일을 대응하지 않다보니 file-loader나 url-loader를 사용해서 import 했을 때 경로를 받아보도록 설정해야한다. Next에서는 next.config.js에서 경로를 설정해줄 수 있다.

 

// next.config.js

module.export = {
  // ... 기타 설정들
  
  webpack(config) {
    config.module.rules.push({
      test: /\.(mp3|ogg|wav|flac)$/i,
      use: {
        loader: 'file-loader',
        options: {
          publicPath: '/_next/static/sounds',
          outputPath: 'static/sounds',
          name: '[name].[ext]',
          esModule: false,
        },
      }
    });
    // ...다른 웹팩 설정들
    return config;
  }
}

 

따로 publicPath와 outputPath 설정을 하지 않으면 _next 폴더의 루트 경로에 파일이 위치하게 되는데, 그 위치를 next 서버가 접근할 수 없기 때문에 404 에러가 날 수 있다. 미연에 방지하기 위해 static 폴더에 음성 파일을 위치시키자.

 

 

+ custom.d.ts


경로는 Module의 default 경로에 저장되어 있다. 타입스크립트를 사용하면 린트에서 문법 오류가 나는 경우가 있는데 해당 확장자에 대한 모듈을 선언해주면 간단하게 해결된다. tsconfig.json 파일에 .d.ts 파일이 포함되어있는 지 확인하는 것도 잊지말자!

 

// custom.d.ts

declare module '*.mp3' {
  const src: string;
  export defualt src;
}
// tsconfig.json

{
  "compilerOptions": {
    // ...기타 설정들
    "include": [
      "src",
      "public",
      "custom.d.ts", // 이거 추가되었는지 확인해주자.
      "next-env.d.ts",
      "**/*.ts",
      "**/*.tsx",
      "next.config.js"
    ],
  }
}

 

 

3. (선택) 음악 파일 lazyloading 하기


라이브러리 설치 없이 lazyloading을 할 수 있다. 다만 파일 이름을 1.mp3, 2.mp3 등으로 사전에 정렬해놓을 필요는 있다.

import를 컴포넌트 최상단이 아니라 hooks 내부에서 사용해주면 되는데 따로 custom hooks로 분리해주면 재사용성이 늘어난다. 테스트 코드 짜기에도 용이하다.

 

약간의 약속이 필요한데, 1.mp3에서부터 5.mp3까지 있다면, 해당 훅을 선언할 때, 아래처럼 선언해야한다.

const [musics] = useMp3Loader(5);

 

폴더 내부의 파일 수를 가져오는 다른 라이브러리를 설치하면 훨씬 유연하게 대응할 수 있으나, 이 기능 하나를 위해 설치해야할까? 라는 의문이 들어서 그냥 파일명을 분리하는 걸로 만족했다.

 

또, import함수는 비동기로 동작하기 때문에 성능을 위해서 가져와야할 파일 수가 많으면 많을 수록 async/await를 지양하고, Promise를 채용하자.

 

import { useCallback, useEffect, useState } from 'react';

export const useMp3Loader = (end: number): [resources: string[]] => {
  const [musicResources, setMusicResources] = useState<string[]>([]);

  const syncImporters = useCallback(() => {
    const promises: Promise<typeof import('*.mp3')>[] = [];
    for (let i = 1; i <= end; i++) {
      promises.push(import(`@/assets/music/${i}.mp3`));
    }

    Promise.all(promises).then((values) => {
      setMusicResources(values.map(v => v.default));
    });
  }, []);

  useEffect(() => {
    syncImporters();
  }, []);

  return [musicResources];
};

참고

https://github.com/vercel/next.js/discussions/12810

 

Unable to import mp3 in Next Js · vercel/next.js · Discussion #12810

I am developing a website for that I need to import mp3 into the project but it's not letting me import it. I've always used Webpack's default config, not familiar with its inner workin...

github.com