포트폴리오를 만들면서 지금까지 참여했던 프로젝트에 대한 내용을 소개하는 페이지를 만들었는데 각자 좀 구성이 달라서 페이지별로 파일을 만들었는데, 스터디를 하면서 여러 사람들이 재사용성을 고려해서, 본문에 대한 내용 파일과 디자인 틀만 만들어 놓고 불러오면 되지 않냐는 말을 들었습니다.
● dynamic import를 해야했던 과정
말로 설명하기 애매한데, 수정하기 이전에 저는 코드에 직접적으로 텍스트를 주입하면서 렌더하고 있었습니다.
import { ReactComponent as Github } from '../../../public/icon/github.svg';
import { ReactComponent as Web } from '../../../public/icon/link.svg';
import * as SC from './Result1.styled';
export function Component() {
return (
<SC.Container>
<div className="relative flex justify-center">
<SC.Title>SharePetment</SC.Title>
<SC.TitleShadow style={{ WebkitTextStroke: '1px black' }}>
SharePetment
</SC.TitleShadow>
</div>
<SC.IntroImg>
<SC.UrlWrap>
<a
href="https://github.com/SharePetment/SharePetment"
target="_blank">
<SC.UrlBtn>
<Github width={40} height={40} />
</SC.UrlBtn>
</a>
<a href="https://sharepetment.site/home" target="_blank">
<SC.UrlBtn>
<Web width={40} height={40} />
</SC.UrlBtn>
</a>
</SC.UrlWrap>
</SC.IntroImg>
<div>
<SC.SemiTitle>부트캠프 메인 프로젝트</SC.SemiTitle>
<SC.Paragraph>
실제로 동물들 사진들만 모여있는 SNS에 대한 니즈가 있다는 것을 파악하여
기존에 있는 SNS말고, 반려동물을 키우는 사람들 사이에서 사용할 수 있는
SNS를 기획하였습니다. 키우고 있는 동물을 사진을 보고 좋아요를 누를 수
있고 강아지, 고양이 제외하고도 다른 귀여운 동물들 포스팅하고 공유할 수
있습니다.
</SC.Paragraph>
</div>
<div>
<SC.SemiTitle>기술스택</SC.SemiTitle>
<div className="flex flex-wrap gap-1">
<SC.Stack>React</SC.Stack>
<SC.Stack>Vite</SC.Stack>
<SC.Stack>TypeScript</SC.Stack>
<SC.Stack>React-Query</SC.Stack>
<SC.Stack>React Hook Form</SC.Stack>
<SC.Stack>styled-components</SC.Stack>
<SC.Stack>Tailwind</SC.Stack>
<SC.Stack>ESLINT</SC.Stack>
<SC.Stack>Prettier</SC.Stack>
<SC.Stack>Husky</SC.Stack>
</div>
</div>
....
즉 스택이나 관련된 소개내용을 페이지 파일 내부에 직접적으로 넣어서 코드를 구성하였습니다. (페이지마다 구성이 달랐기때문에..) 따라서 구성을 완전히 통일하고, 아래와 같이 내부 텍스트를 객체로 빼 분리하였습니다.
import { ProjectType } from 'src/page-contents/projectType';
const Project1: ProjectType = {
mainTitle: 'SharePetment',
introImg: 'bg-[url(/portfolio/sharepetment/sharepetment2.webp)]',
github: ['https://github.com/SharePetment/SharePetment'],
deploy: 'https://sharepetment.site/home',
blog: 'https://ddaeunbb.tistory.com/305',
semiTitle: '부트캠프 메인 프로젝트',
semiContent:
'실제로 동물들 사진들만 모여있는 SNS에 대한 니즈가 있다는 것을 파악하여 기존에 있는 SNS말고, 반려동물을 키우는 사람들 사이에서 사용할 수 있는 SNS를 기획하였습니다. 키우고 있는 동물을 사진을 보고 좋아요를 누를 수 있고 강아지, 고양이 제외하고도 다른 귀여운 동물들 포스팅하고 공유할 수 있습니다.',
stacks: [
'React',
'Vite',
'TypeScript',
'React-Query',
'React Hook Form',
'styled-componets',
'Tailwind',
'ESLINT',
'Prettier',
'Husky',
],
...
export default Project1;
이제 페이지별로 나뉘어져있는 객체들을 페이지의 경로에 따라 import해서 사용해와야했는데요, 즉 동적 import가 필요했습니다.
● dynamic import를 하면서 마주했던 문제점
저는 단순하게 useParams로 가져와 import를 해오면 되지 않을까란 생각이 들었는데, 이는 쉽지 않은 방법이었습니다.
export function Component() {
const { id } = useParams();
if(id === 1) import(`src/.../project${id}`);
return (
...)
}
위와 같이 id로 import를 해오면 되지 않을까란 생각을 했는데, 바로 에러를 마주하게되었고, import를 함수 내부에서 사용해야지만 동적 import를 사용할 수 있었습니다. 그럼 언제 함수 실행을 트리거하냐도 문제였고, 저는 페이지 컴포넌트가 렌더링 되기 이전에 텍스트 객체파일을 import를 해오고 싶었습니다.. chat GPT에게 물어보니 흑흑 순 엉터리뿌니얌. 그래서 저는 이전에 메인 프로젝트 리팩토링을 하면서 알게된 react-router-dom의 loader를 사용하게 되었습니다.
● react-router-dom의 loader를 사용한 dynamic import해오기
loader v6.15.0
loader Each route can define a "loader" function to provide data to the route element before it renders. This feature only works if using a data router, see Picking a Router createBrowserRouter([ { element: , path: "teams", loader: async () => { return fak
reactrouter.com
react-router-dom 공식문서를 보니 페이지 loader는 실행할 함수에 매개변수로 params를 넘겨줄 수 있는 것을 볼 수 있었습니다.
// 공식문서
createBrowserRouter([
{
element: <Teams />,
path: "teams",
},
children: [
{
element: <Team />,
path: ":teamId",
// params를 넘겨주는 부분
loader: async ({ params }) => {
return fetch(`/api/teams/${params.teamId}.json`);
},
},
],
},
]);
즉, loader는 페이지가 보이지기 전에 일련의 어떠한 행위를 하기 위한 장치..? 라고 생각했습니다. 이 loader의 실행은 렌더링 되기 이전에 된다는 점입니다.
따라서 저는 페이지를 렌더링하기 이전에, 경로에 따라 import를 해오도록 하고, 이 import를 쓸 순 없을까..? 란 생각이 들엇습니다.
공식문서를 뒤져보니 'useLoaderData'라는 hook을 사용하여, 페이지에게 넘겨진 데이터를 사용할 수 있었습니다.
// 공식문서
import {
createBrowserRouter,
RouterProvider,
useLoaderData,
} from "react-router-dom";
function loader() {
return fetchFakeAlbums();
}
export function Albums() {
const albums = useLoaderData();
// ...
}
예를 들어, loader에서 어떠한 데이터를 fetch, axios를 사용해 불러오고, 리턴해주게 되면 Albums라는 컴포넌트에서 useLoaderData hook을 사용해 가져올 수 있는 것입니다. 저는 이 공식문서의 예제를 보자마자, 제가 원하는 것이라고 생각이 들었습니다.
따라서 저는 loader를 아래와 같이 만들어 사용하였습니다.
router.tsx
{
path: Path.Project,
lazy: () => import('src/page/Project/Project'),
loader: async ({ params }) => projectLoader(params),
},
projectLoader.ts
import { Params } from 'react-router-dom';
import { ProjectType } from 'src/page-contents/projectType';
export const projectLoader = async (
params: Params<string>,
): Promise<ProjectType | null> => {
const { id } = params;
try {
// 경로에 따라 import 해오기
const module = await import(`../page-contents/project${id}.ts`);
// 그리고 module의 default를 내보낸다.
return module.default;
} catch (error) {
console.error('Error loading project:', error);
return null;
}
};
이후, 컴포넌트에서 useLoaderData hook을 사용해, 객체를 Project로 가져오고 내부에서 사용할 수 있었습니다.
export function Component() {
const Project = useLoaderData() as ProjectType;
return (
<>
<Link to="/project">
<SC.BackBtn>
<Back
className="stroke-deeppurple fill-deeppurple"
width={40}
height={40}
/>
</SC.BackBtn>
</Link>
<motion.div initial="hidden" variants={fadePop} animate="visible">
<SC.Container>
<SC.Title style={{ WebkitTextStroke: '2px #463ED8' }}>
{Project.mainTitle}
</SC.Title>
<SC.IntroImg className={Project.introImg}>
<SC.UrlWrap>
// Project 객체 사용하는부분
{Project.github.map((url, urlKey) => (
<a href={url} target="_blank" key={urlKey}>
<SC.UrlBtn>
<Github width={40} height={40} />
</SC.UrlBtn>
</a>
))}
...
위의 구조에 맞춰서 페이지 구조 파일을 하나만 만들 수 있었고, 경로에 따라 dynamic import를 해와 내용을 채워넣을 수 있었습니다 :)
포트폴리오를 만들면서 지금까지 참여했던 프로젝트에 대한 내용을 소개하는 페이지를 만들었는데 각자 좀 구성이 달라서 페이지별로 파일을 만들었는데, 스터디를 하면서 여러 사람들이 재사용성을 고려해서, 본문에 대한 내용 파일과 디자인 틀만 만들어 놓고 불러오면 되지 않냐는 말을 들었습니다.
● dynamic import를 해야했던 과정
말로 설명하기 애매한데, 수정하기 이전에 저는 코드에 직접적으로 텍스트를 주입하면서 렌더하고 있었습니다.
import { ReactComponent as Github } from '../../../public/icon/github.svg';
import { ReactComponent as Web } from '../../../public/icon/link.svg';
import * as SC from './Result1.styled';
export function Component() {
return (
<SC.Container>
<div className="relative flex justify-center">
<SC.Title>SharePetment</SC.Title>
<SC.TitleShadow style={{ WebkitTextStroke: '1px black' }}>
SharePetment
</SC.TitleShadow>
</div>
<SC.IntroImg>
<SC.UrlWrap>
<a
href="https://github.com/SharePetment/SharePetment"
target="_blank">
<SC.UrlBtn>
<Github width={40} height={40} />
</SC.UrlBtn>
</a>
<a href="https://sharepetment.site/home" target="_blank">
<SC.UrlBtn>
<Web width={40} height={40} />
</SC.UrlBtn>
</a>
</SC.UrlWrap>
</SC.IntroImg>
<div>
<SC.SemiTitle>부트캠프 메인 프로젝트</SC.SemiTitle>
<SC.Paragraph>
실제로 동물들 사진들만 모여있는 SNS에 대한 니즈가 있다는 것을 파악하여
기존에 있는 SNS말고, 반려동물을 키우는 사람들 사이에서 사용할 수 있는
SNS를 기획하였습니다. 키우고 있는 동물을 사진을 보고 좋아요를 누를 수
있고 강아지, 고양이 제외하고도 다른 귀여운 동물들 포스팅하고 공유할 수
있습니다.
</SC.Paragraph>
</div>
<div>
<SC.SemiTitle>기술스택</SC.SemiTitle>
<div className="flex flex-wrap gap-1">
<SC.Stack>React</SC.Stack>
<SC.Stack>Vite</SC.Stack>
<SC.Stack>TypeScript</SC.Stack>
<SC.Stack>React-Query</SC.Stack>
<SC.Stack>React Hook Form</SC.Stack>
<SC.Stack>styled-components</SC.Stack>
<SC.Stack>Tailwind</SC.Stack>
<SC.Stack>ESLINT</SC.Stack>
<SC.Stack>Prettier</SC.Stack>
<SC.Stack>Husky</SC.Stack>
</div>
</div>
....
즉 스택이나 관련된 소개내용을 페이지 파일 내부에 직접적으로 넣어서 코드를 구성하였습니다. (페이지마다 구성이 달랐기때문에..) 따라서 구성을 완전히 통일하고, 아래와 같이 내부 텍스트를 객체로 빼 분리하였습니다.
import { ProjectType } from 'src/page-contents/projectType';
const Project1: ProjectType = {
mainTitle: 'SharePetment',
introImg: 'bg-[url(/portfolio/sharepetment/sharepetment2.webp)]',
github: ['https://github.com/SharePetment/SharePetment'],
deploy: 'https://sharepetment.site/home',
blog: 'https://ddaeunbb.tistory.com/305',
semiTitle: '부트캠프 메인 프로젝트',
semiContent:
'실제로 동물들 사진들만 모여있는 SNS에 대한 니즈가 있다는 것을 파악하여 기존에 있는 SNS말고, 반려동물을 키우는 사람들 사이에서 사용할 수 있는 SNS를 기획하였습니다. 키우고 있는 동물을 사진을 보고 좋아요를 누를 수 있고 강아지, 고양이 제외하고도 다른 귀여운 동물들 포스팅하고 공유할 수 있습니다.',
stacks: [
'React',
'Vite',
'TypeScript',
'React-Query',
'React Hook Form',
'styled-componets',
'Tailwind',
'ESLINT',
'Prettier',
'Husky',
],
...
export default Project1;
이제 페이지별로 나뉘어져있는 객체들을 페이지의 경로에 따라 import해서 사용해와야했는데요, 즉 동적 import가 필요했습니다.
● dynamic import를 하면서 마주했던 문제점
저는 단순하게 useParams로 가져와 import를 해오면 되지 않을까란 생각이 들었는데, 이는 쉽지 않은 방법이었습니다.
export function Component() {
const { id } = useParams();
if(id === 1) import(`src/.../project${id}`);
return (
...)
}
위와 같이 id로 import를 해오면 되지 않을까란 생각을 했는데, 바로 에러를 마주하게되었고, import를 함수 내부에서 사용해야지만 동적 import를 사용할 수 있었습니다. 그럼 언제 함수 실행을 트리거하냐도 문제였고, 저는 페이지 컴포넌트가 렌더링 되기 이전에 텍스트 객체파일을 import를 해오고 싶었습니다.. chat GPT에게 물어보니 흑흑 순 엉터리뿌니얌. 그래서 저는 이전에 메인 프로젝트 리팩토링을 하면서 알게된 react-router-dom의 loader를 사용하게 되었습니다.
● react-router-dom의 loader를 사용한 dynamic import해오기
loader v6.15.0
loader Each route can define a "loader" function to provide data to the route element before it renders. This feature only works if using a data router, see Picking a Router createBrowserRouter([ { element: , path: "teams", loader: async () => { return fak
reactrouter.com
react-router-dom 공식문서를 보니 페이지 loader는 실행할 함수에 매개변수로 params를 넘겨줄 수 있는 것을 볼 수 있었습니다.
// 공식문서
createBrowserRouter([
{
element: <Teams />,
path: "teams",
},
children: [
{
element: <Team />,
path: ":teamId",
// params를 넘겨주는 부분
loader: async ({ params }) => {
return fetch(`/api/teams/${params.teamId}.json`);
},
},
],
},
]);
즉, loader는 페이지가 보이지기 전에 일련의 어떠한 행위를 하기 위한 장치..? 라고 생각했습니다. 이 loader의 실행은 렌더링 되기 이전에 된다는 점입니다.
따라서 저는 페이지를 렌더링하기 이전에, 경로에 따라 import를 해오도록 하고, 이 import를 쓸 순 없을까..? 란 생각이 들엇습니다.
공식문서를 뒤져보니 'useLoaderData'라는 hook을 사용하여, 페이지에게 넘겨진 데이터를 사용할 수 있었습니다.
// 공식문서
import {
createBrowserRouter,
RouterProvider,
useLoaderData,
} from "react-router-dom";
function loader() {
return fetchFakeAlbums();
}
export function Albums() {
const albums = useLoaderData();
// ...
}
예를 들어, loader에서 어떠한 데이터를 fetch, axios를 사용해 불러오고, 리턴해주게 되면 Albums라는 컴포넌트에서 useLoaderData hook을 사용해 가져올 수 있는 것입니다. 저는 이 공식문서의 예제를 보자마자, 제가 원하는 것이라고 생각이 들었습니다.
따라서 저는 loader를 아래와 같이 만들어 사용하였습니다.
router.tsx
{
path: Path.Project,
lazy: () => import('src/page/Project/Project'),
loader: async ({ params }) => projectLoader(params),
},
projectLoader.ts
import { Params } from 'react-router-dom';
import { ProjectType } from 'src/page-contents/projectType';
export const projectLoader = async (
params: Params<string>,
): Promise<ProjectType | null> => {
const { id } = params;
try {
// 경로에 따라 import 해오기
const module = await import(`../page-contents/project${id}.ts`);
// 그리고 module의 default를 내보낸다.
return module.default;
} catch (error) {
console.error('Error loading project:', error);
return null;
}
};
이후, 컴포넌트에서 useLoaderData hook을 사용해, 객체를 Project로 가져오고 내부에서 사용할 수 있었습니다.
export function Component() {
const Project = useLoaderData() as ProjectType;
return (
<>
<Link to="/project">
<SC.BackBtn>
<Back
className="stroke-deeppurple fill-deeppurple"
width={40}
height={40}
/>
</SC.BackBtn>
</Link>
<motion.div initial="hidden" variants={fadePop} animate="visible">
<SC.Container>
<SC.Title style={{ WebkitTextStroke: '2px #463ED8' }}>
{Project.mainTitle}
</SC.Title>
<SC.IntroImg className={Project.introImg}>
<SC.UrlWrap>
// Project 객체 사용하는부분
{Project.github.map((url, urlKey) => (
<a href={url} target="_blank" key={urlKey}>
<SC.UrlBtn>
<Github width={40} height={40} />
</SC.UrlBtn>
</a>
))}
...
위의 구조에 맞춰서 페이지 구조 파일을 하나만 만들 수 있었고, 경로에 따라 dynamic import를 해와 내용을 채워넣을 수 있었습니다 :)