-
[React] Dynamic Import 를 조금 더 동적으로 활용할 순 없을까?Today's/DevelopStory 2024. 9. 4. 11:04
디자이너 및 기획자의 화면기획, 디자인 시안 등을 보면서 작업하다 보면 흔히 생기는 고충 중에 하나가 바로 Asset관리이다.
특히 아이콘들이 심한데, 오래된 프로젝트일수록 아이콘이나 이미지가 중복적으로 프로젝트에 포함되곤 한다.
혹은 유사한 아이콘이지만 색상만 다르거나, 아주 미세하게 모양이 다른 경우도 많다.
Figma나 Zeplin에서 이런 아이콘들을 생각없이 export하여 프로젝트 폴더에 집어넣다 보면 아래와 같이 웃지 못할 상황이 발생하곤 한다.이러한 상황을 벗어나기 위해 이번에 진행하는 프로젝트에서는 아이콘 라이브러리를 설치하여 사용해보기로 결정했다.
아이콘 라이브러리는 디자이너가 직접 디자인한 아이콘은 아니지만 간단하고 통용적으로 사용되는 아이콘들이 많이 있고,
해당 아이콘의 색상이나 사이즈 등을 자유롭게 조정할 수 있다는 게 장점이 있다.
라이브러리는 "react-icons"를 사용하기로 결정했다.
왠만한 Icon들을 모두 종합하여 관리해주는 라이브러리라 디자이너의 선택지도 충분히 넓다고 판단했기 때문이다.https://react-icons.github.io/react-icons/
이 라이브러리는 다른 아이콘 라이브러리를 종합하여 사용하는 라이브러리기에,
모듈의 경로에 각 라이브러리의 약자가 들어간다.
ex: FontAwsome 아이콘의 경우 "react-icons/fa" 와 같은 식이다.개발자의 입장에선 디자인 시안에서 폰트의 파일명만 보고 어느 라이브러리의 아이콘인지 알 수 있으며
해당 아이콘을 Import 함으로써 간단하게 사용할 수도 있지만,
좀 더 동적으로 사용해야 될 경우도 있어 Dynamic Import를 사용하여 공통적으로 사용할 수 있는
DynamicIcon 이라는 컴포넌트를 만들어 보았다.import React, { useEffect, useState } from "react"; import { IconType } from "react-icons"; type Props = { iconName: string; // 예: "RiHome3Fill" category?: string; // 예: "ri" size?: number }; const DynamicIcon: React.FC<Props> = ({ iconName, category = "ri", size }) => { const [IconComponent, setIconComponent] = useState<IconType | null>(null); useEffect(() => { if (!category || !iconName) return; const loadIcon = async () => { try { // 모듈 전체를 임포트하고, iconName으로 접근 const iconModule = await import("react-icons/${category}"); // IconType 객체에서 아이콘을 안전하게 추출하기 위해 any를 사용합니다. const iconMap: { [key: string]: IconType } = iconModule as any; const LoadedIcon = iconMap[iconName]; if (LoadedIcon) { setIconComponent(() => LoadedIcon); } else { console.error(`Icon ${iconName} not found in ${category}`); } } catch (error) { console.error( `Could not load icon: ${iconName} from category: ${category}`, error ); } }; loadIcon(); }, [category, iconName]); if (!IconComponent) return null; return <IconComponent size={size} />; }; export default DynamicIcon;
하지만 이렇게 사용한 경우 모듈을 정상적으로 불러오지 못하여 아이콘이 출력되지 않는 문제가 발생했다.
이유가 뭘까? 우선 Props로 전달받는 category가 랜더되는 시점과 맞지 않아 발생하는 문제라 보기엔 useEffect 블럭에서 이미 category 가 없을 경우 return시키고 있었고, console.log()로 확인해도 마찬가지로 정상출력 됨을 확인할 수 있었다.결국 코드를 약간 변경하여 테스트를 진행해 보았다.
const loadIcon = async () => { try { // 모듈 전체를 임포트하고, iconName으로 접근 const iconModule = await import("react-icons/fa"); // IconType 객체에서 아이콘을 안전하게 추출하기 위해 any를 사용합니다. const iconMap: { [key: string]: IconType } = iconModule as any; const LoadedIcon = iconMap[iconName]; if (LoadedIcon) { setIconComponent(() => LoadedIcon); } else { console.error(`Icon ${iconName} not found in ${category}`); } } catch (error) { console.error( `Could not load icon: ${iconName} from category: ${category}`, error ); } };
위와 같이 import 해오는 경로에 변수값을 넣지 않고 원시 문자열만을 사용할 경우 정상적으로 동작함을 확인했다.
도대체 무슨 차이가 있어 이런 상황이 발생하는 것일까?const modulePath = "react-icons/fa" const iconModule = await import(modulePath)
이처럼 static한 값을 할당한 변수를 사용해도 마찬가지로 동작하지 않는 걸 확인하면서 '왜 그럴까?' 라는 의문이 거듭되었고,
검색을 통해 알아보기로 했다.
원인을 알 수 있었던 글을 첨부한다.문제는 JavaScript에서 모듈을 import할 때엔 모듈 번들러(Webpack, Vite 등)가 사용되는데, 이 번들러들은 코드를 정적으로 분석하고 필요한 모듈을 "미리" 찾아내 번들링하는 작업을 수행한다.
따라서 코드가 실행되기 전에 미리 모듈을 포함시켜야 하는데, 모듈 경로가 변수에 할당되는 시점이 번들러가 모듈을 불러오는 시점보다 나중이기 때문에 발생하는 문제였다.
그렇다면 해결 방법이 없을까?
어차피 모듈 경로는 완벽히 동적일 수 없다. 무슨 말이냐면 모듈은 코드가 동작하면서 그때그때 만들어 지는 것이 아니라 이미 만들어진 것들을 사용하는 것들이기 때문이다. 따라서 변수에 들어갈 경로도 분명 정해져 있음이다.
결국 switch 문을 통해 모듈에서 사용되는 모든 경로를 미리 작성하여 import 하는 방식을 고려할 수 있다.const loadIcon = async () => { try { // 모듈 전체를 임포트하고, iconName으로 접근 let iconModule; switch (category) { case "ai": iconModule = await import("react-icons/ai"); break; case "bs": iconModule = await import("react-icons/bs"); break; case "bi": iconModule = await import("react-icons/bi"); break; case "ci": ... 중략 ... default: throw new Error("Unknown category"); } ... 후략 ... }
혹은 동적 import를 위한 별도의 설정을 사용하는 방법이 있다.
번들러 설정에서 import()에 동적으로 로드될 수 있는 모든 모듈을 미리 포함시키도록 설정을 추가 하는 방식이다.const context = require.context('react-icons', true, /\.js$/); const iconModule = context(`./${category}`);
위의 예시 처럼 Webpack에서 require.context()를 사용하여 특정 디렉터리의 모든 모듈을 로드하도록 할 수도 있다.
다만 위 방법은 사용되지 않는 모듈까지 번들에 모두 포함됨으로 번들의 크기가 커질 우려가 있을 것 같다.
따라서 나의 경우엔 전자인 switch문을 사용하는 방식을 사용했다.
이번 경우, 디자이너가 다양한 아이콘을 사용하긴 하겠지만 react-icons에서 제공하는 모든 아이콘 카테고리를 사용하진 않을 것이고
주로 사용되는 아이콘 카테고리와 몇몇 카테고리만 switch문에 포함시켜도 되겠다는 생각이 들어서이다.이처럼 코드의 실행 순서 혹은 번들러의 동작 방식 등을 고려해야 되는 상황이 일반적으로 흔히 발생하진 않겠지만
새로운 경험이기에 기록하려 한다.'Today's > DevelopStory' 카테고리의 다른 글
[React] React-Query와 상태관리 (feat.우아한 테크 세미나) - 1 (0) 2023.06.30 [ReactNative] Build 환경 분리하기 (feat. Codepush) (0) 2023.03.28 [ReactNative] 숫자 3자리마다 콤마(",") 삽입하기 (0) 2023.03.24 [ReactNative] Appcenter Codepush 연동하기 (0) 2023.03.24 [Compose] LaunchedEffect와 DisposableEffect 그리고 React의 useEffect와 유사점 (1) 2023.02.23