1. 계기
저장되어 있는 이미지를 서버에서 보내주는 값에 따라 보여주어야 하는 UI를 개발하게 되었다.
이미지의 개수가 몇 개 안 되었을 때는, import를 사용하여 미리 이미지를 불러온 후, 값에 따라 로드하는 방식을 선택했지만, 이미지가 점차 많아져 중복된 로직을 하나의 함수로 사용하는 것처럼 동적으로 이미지를 불러오고 싶었다.
배포 환경에서도 동작하도록 어떻게 해결했는지 정리해 보려고 한다.

2. 개발 환경
- Vue.js 버전: 2.6.14
- Vue.js CLI 버전: 5.0.8
- TypeScript : 미사용
3. require 동적 이미지 로드 + @error로 처리 (실패)
처음에는 단순하게 아이디를 기준으로 이미지를 동적으로 받은 후, 이미지가 없을 때 @error 이벤트를 사용해 fallback 이미지를 설정하려고 했다.
(아래에서 추가 설명하겠지만, 애초에 Vue template 내에서 저렇게 require로 바로 불러오는 것 또한 권장되지 않는 방법이다.)
<img
:src="require(`@/assets/${id}.svg`)"
:alt="name"
@error="replaceImg"
/>
replaceImg(e) {
e.target.src = require(`@/assets/error.svg`);
}
하지만 이 방식은 작동하지 않았다...
require()는 Webpack이 정적 분석하는 대상인데, ${id} 같은 동적 경로는 컴파일 타임에 분석이 불가능하기 때문이었다.
즉, 렌더링 시점에서 이미지가 없어서 오류(@error)가 나는 게 아니라, 빌드 자체에서 에러가 났기 때문에 @error까지도 가지 못하고 이미지를 불러오려고 할 때, 컴파일 에러가 나게 된다.

4. require.context()
Webpack이 제공하는 API인 require.context()를 이용하면 특정 폴더 내의 파일들을 한 번에 불러올 수 있다.
이렇게 하면 동적으로 파일 이름을 구성해도 내부적으로 캐싱된 객체에서 접근하는 방식이라, 오류 없이 사용할 수 있다.
const requireWorkImages = require.context('@/assets/work', false, /\.svg$/);
export function getImage(fileName) {
try {
const filePath = `./${fileName}.svg`;
return requireWorkImages(filePath);
} catch (e) {
return requireWorkImages('./error.svg');
}
}
이렇게 하면 <img :src="getImage(id)" />처럼 사용할 수 있고, 파일이 없을 경우에도 error.svg로 fallback 처리된다.
단점은 Webpack에서 제공하는 것이기 때문에 Webpack을 사용하지 않는다면 쓸 수 없는 API다.
(Vite에서는 비슷하게 import.meta.glob()가 있다.)
5. /public 폴더 사용하기
이미지를 전부 public 폴더에 넣어서 사용하는 방법도 가능하다.
Webpack 빌드 프로세스를 아예 우회해서 브라우저가 직접 접근 가능한 /public 폴더에 이미지를 넣어서 사용할 수도 있다.
<img :src="`/images/${id}.svg`" />
이 방식은 Webpack의 정적 분석과 무관하게 경로만 맞으면 파일을 가져올 수 있기 때문에 단순하다.
하지만 현재 프로젝트 전체적으로 사용 중인 alias 경로(@/assets/...)와 분리되어 있고, IDE 자동완성이나 경로 확인이 불편하다는 점이 있어 require.context()를 선택하기로 하였다.
6. require.context()를 선택한 이유
위에서도 말했다시피, require.context()를 선택했다.
현재 프로젝트는 Vue CLI로 생성해서 Webpack 기반이라 require.context()를 사용할 수 있고, 팀에서 사용하는 이미지 경로가 이미 @/assets/... 에 구조화되어 있어, /public 방식보다 접근과 관리가 편하다는 판단이 들었기 때문이다.
구현도 require.context()로 이미지를 모아두고, id에 따라 키처럼 접근해서 없는 경우엔 try-catch로 대체 이미지를 지정해주면 끝이라 간단했다.
const requireTemplateImages = require.context(
'@/assets/template',
false,
/\.svg$/,
);
export function getTemplateImage(fileName) {
const filePath = `./${fileName}.svg`;
try {
return requireTemplateImages(filePath);
} catch (e) {
return requireTemplateImages('./notFound.svg');
}
}

7. 컴포넌트 분리와 import 지연 로딩
import로 구현하지 않은 이유 중 하나가 import 로직이 컴포넌트가 로딩되면 해당 이미지들이 쓰이지 않더라도 이미지 또한 import된다는 점이었다.
반대로 생각해보면, 이미지를 로드하는 컴포넌트를 분리하여 이미지들이 쓰일 때만 불러올 수도 있겠다는 생각이 들었다.!
예를 들어, 아래처럼 컴포넌트를 지연 로드하면 해당 컴포넌트가 로딩될 때까지 그 안의 import도 실행되지 않는다.
const LazyComponent = () => import('./LazyComponent.vue');
// LazyComponent.vue
import specialIcon from '@/assets/special.svg';
이 이미지는 LazyComponent가 처음 보여질 때까지는 실제로 로딩되지 않는다.
즉, import라고 해서 무조건 번들에 포함되는 것이 아니라, 동적 컴포넌트 분리를 통해 지연 로딩처럼 작동시킬 수 있다.!!
8. 이미지를 불러오는 비권장 방식들
맨처음 언급했듯, template 내부에서 require()를 바로 사용하는 것은 Webpack + Vue CLI 환경에선 동작하지만 권장되지 않는 방법이다.
<template>
<img :src="require('@/assets/icons/close.svg')" />
</template>
require()를 템플릿에서 바로 쓰면, Webpack이나 linter가 분석과 코드 추적이 복잡해지고, (우리 프로젝트에서는 TypeScript를 사용하진 않지만) 타입 추론이나 경로 자동완성 같은 기능을 사용하기 어렵다. 또한 재사용할 수 없고, 로직이 분산되어 있어 유지보수할 때도 불편할 수 있기 때문이다.
하지만 이미지를 많이 불러오지 않고, 간단하여 현재 프로젝트에서는 이 방식으로 많이 사용하고 있었다..

그리고 이렇게 불러온 이미지를 template에서 바로 쓰면 안 된다. (쓸 수도 없음)
<template>
<img :src="apple" />
</template>
<script>
import apple from '@/assets/apple.svg';
</script>
왜냐하면 template은 Vue 인스턴스의 context 안에 있는 변수만 접근할 수 있기 때문이다.
해결하려면 아래처럼 data, computed, methods에 등록한 후 사용해야 한다.
<template>
<img :src="appleImage" />
</template>
<script>
export default {
data() {
return {
appleImage: apple,
};
}
}
</script>
9. 결론
require.context()는 이미지를 동적으로 불러올 때, Webpack을 사용 중이라면 편리하고 유용한 도구라고 생각한다.
실제로 동작하는 방식, 프로젝트 구조, 성능을 모두 고려했을 때, 정답은 없고 상황에 맞는 선택이 정답이다라는 걸 다시 느꼈다.

'Study' 카테고리의 다른 글
[Vue.js] 라우터 미들웨어에서 location.replace() 사용하기 (feat. next()) (1) | 2025.05.28 |
---|---|
[React] SWR은 언제 사용할까? (feat. 리액트에서 반복적인 useEffect 대신 SWR 사용하기) (1) | 2025.05.20 |
[Web] Webpack HMR과 풀 페이지 리로드 (+IE9 해결 방법) (0) | 2025.03.14 |
[Web] 브라우저 렌더링, SSR과 CSR는 다를까? (0) | 2025.02.11 |
[Next.js] App Router vs Pages Router (feat. 달라진 폴더 구조) (1) | 2025.01.19 |