01. 기본 구조 설계
2024-06-26 오후 03시 11분
2024-06-26 오후 03시 11분
Next.js 14.2.3 버전을 기준으로 작성되었습니다.
개발환경 구축하기
우선 Next.js DOC를 참고해서 환경을 설정합시다.
Next.js에서도 typescript
, tailwindCSS
를 권장하므로 첫 개발환경 세팅은 간단합니다.
App-router는 반드시 선택해야 합니다.
npx create-next-app@latest
What is your project named? newBlog
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? no
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? no
기능의 구현과정 파악하기
우선, 마크다운 파일 디렉토리를 기반으로 블로그를 생성하게 됩니다.
따라서 구조 또한 처음부터 잘 설계해야합니다.
저는 디렉토리를 자동으로 파싱해서 디렉토리를 기준으로 카테고리를 나누게 설계하였습니다.
app/posts
ㄴPage.tsx
ㄴ[category]
Page.tsx
ㄴ[slug]
Page.tsx
실제로 MDX파일을 파싱하는 디렉토리의 구조입니다.
해당 디렉토리에 맞게 MDX파일의 경로또한 설정합니다.
app/posts
ㄴPage.tsx
ㄴ[category]
Page.tsx
ㄴ[slug]
Page.tsx
(root)/posts
ㄴJavascript
js_01.mdx
ㄴTypescript
ts_01.mdx
ㄴproject
project_01.mdx
게시물의 노출 내용
일단은 각 Page.tsx
에 무엇을 넣어야될지는 나중에 생각하고
게시물마다 전달받을 수 있는 내용을 설계해봅시다.
interface FrontMatterTypes {
title: string;
description: string;
image: string;
tags: string[];
date: Date;
[key: string]: any;
toc?: boolean;
comment: boolean;
hidden?: boolean;
}

- 게시물 제목
- 게시물 설명
- 게시물 썸네일 이미지
- 게시물 태그
- 게시물 작성일
toc
사용 여부
comment
사용 여부
hidden
여부
상황에 따라 숨겨진 게시물도 존재할거고 내용이 짧아 목차가 없어야 되거나
댓글이 무의미한 게시물도 있을겁니다.
그에 맞게 설계를 합니다.
이제, 필요한 모든 데이터를 가져올 수 있는 유틸함수를 만들어야겠죠?
여기서 생각해볼 수 있는 데이터를 일단 분류해보겠습니다.
전체 게시글 페이지
- 모든 파일 명
- 모든 디렉토리 명 (카테고리 분류)
- 모든 카테고리 별 게시물 개수 (카테고리 표기)
- 모든 파일의
frontMatter
정보
카테고리 게시글 페이지
- 카테고리에 따른 파일명
- 카테고리에 따른
frontMatter
정보
- 카테고리에 따른 게시물 개수
게시글
- 현재 주소에 따른 게시물 상세 내용 (
Content
)
저는 utils/parseData.ts
에 작성해보겠습니다.
next.js
에 기본적으로 존재하는 node.js
로 경로를 추출해보겠습니다.
전체 게시글 페이지
const BASE_DIR = 'posts'
//? 전체 Directory 로드
export const allDirectoryLoad = () => {
const allItems = fs.readdirSync(path.join(BASE_DIR));
const category = allItems.filter(
(item) =>
!item.includes('.mdx') &&
fs.lstatSync(path.join(BASE_DIR, item)).isDirectory()
);
return category;
};
//? 전체 File 로드
export const allFilesLoad = (category: string[]) => {
const files = category.flatMap((categoryItem: string) => {
const fileNames = fs.readdirSync(path.join(BASE_DIR, categoryItem));
return fileNames.map((file) => ({
category: categoryItem,
file,
}));
});
return files;
};
우선 BASE_DIR
, 여기서는 (root)/posts
폴더가 해당합니다.
[
{ category: 'Javascript_basic', file: 'js_01.mdx' },
{ category: 'Javascript_basic', file: 'js_02.mdx' },
]
위와 같은 형태의 데이터가 allFilesLoad
를 통해 반환 됩니다.
두개의 유틸함수로 카테고리와 파일명을 추출하였습니다.
카테고리 게시글 페이지
이제 위 데이터를 활용해서 카테고리 별 파일 개수와 모든파일의 frontMatter
정보를 가져와보겠습니다.
//? 전체 카테고리와 전체 파일을 인식 후 matter정보, 경로 불러옴
//! /posts/[category]
export const loadBlogDetails = async () => {
const category = allDirectoryLoad();
const categoryFiles = allFilesLoad(category);
const allFiles = [...categoryFiles];
const blogs = allFiles.map(({ category, file }) => {
const filePath = category
? path.join(BASE_DIR, category, file)
: path.join(BASE_DIR, file);
const fileContent = fs.readFileSync(filePath, 'utf-8');
const { content, data } = matter(fileContent);
const grayMatter = data as FrontMatterTypes;
return {
meta: {
title: grayMatter.title,
tags: grayMatter.tags,
src: grayMatter.image,
category: category,
date: dayjs.utc(grayMatter.date).format('YYYY년 MM월 DD일-HH:mm:ss'),
readingMinutes: Math.ceil(readingTime(content).minutes),
hidden: grayMatter?.hidden || false,
},
slug: category
? `${category}/${file.replace('.mdx', '')}`
: file.replace('.mdx', ''),
};
});
return blogs;
};
//? 전체 카테고리와 전체 파일의 총 게시물 갯수를 불러옴
//! matter 내 hidden 속성 제외
//! /posts, /posts/[category],
export const loadBlogCategoryCount = async () => {
const blogs = await loadBlogDetails();
const categoryCount: { [key: string]: number } = { All: 0 };
blogs.forEach(({ meta: { category, hidden } }) => {
if (!hidden) {
categoryCount.All++;
if (category) {
categoryCount[category] = (categoryCount[category] || 0) + 1;
}
}
});
return categoryCount;
};
allDirectoryLoad()
→ allFilesLoad()
를 통해 전체 게시물 정보를 파싱합니다.
파일에서 content
, data
를 파싱해서 모든 정보를 처리합니다.
이로서 전체 게시글에서 필요한 데이터는 전부 설계하였습니다.
이제 카테고리 게시글 페이지에서 필요한 정보를 추출해봅시다.
전체 게시물을 추출하는 과정은 완료했으므로, 전체 게시물중 해당하는 카테고리로 필터링만 하면 됩니다!
//? 전체 카테고리와 전체 파일중 카테고리에 맞는 항목만 추출
//! matter 내 hidden 속성 제외
//! /posts/[category]
export async function getPosts(category: string) {
const allBlogs = await loadBlogDetails();
return allBlogs.filter(
(blog) => blog.meta.category === category && !blog.meta.hidden
);
}
//? 게시물 내부 컨텐츠와 상세정보를 불러옴
//! /posts/[category]/[slug]
export async function getPost(category: string, slug: string) {
const filePath = path.join(BASE_DIR, category, `${slug}.mdx`);
const fileContent = fs.readFileSync(filePath, 'utf-8');
const { data, content } = matter(fileContent);
const frontMatter = data as FrontMatterTypes;
return {
frontMatter,
content,
};
}
category
와 slug
(현재 path)를 인수로 받아 보여줄 데이터와 카테고리에 해당하는 게시물만 가져올 수 있게 되었습니다.
이로서 모든 유틸함수의 설계는 완료되었습니다.
이제 해당함수들을 각 path에서 알맞게 호출하면 되는겁니다.
Path별 게시글 데이터 불러오기
- /posts 전체 게시물 목록
- /posts/[category] 카테고리 게시물 목록
- /posts/[category]/[slug] 내부 컨텐츠
세 가지의 Page.tsx
를 설계하면 문제없이 값이 나올 것입니다. 어떻게 할당하면 될지 알아봅시다.
/posts 전체 게시물 목록
import PageContainer from '@/components/PageContainer';
import { loadBlogCategoryCount, loadBlogDetails } from '@/utils/parseData';
export default async function Home() {
const blogs = await loadBlogDetails();
const category = await loadBlogCategoryCount();
return (
<PageContainer>
<div className='w-[90%] m-auto select-none'>
<Browse blogs={blogs} categories={category} />
</div>
</PageContainer>
);
}
Browse
컴포넌트에서 모든 blogs
의 정보를 가져와서 스타일을 알맞게 적용해서 렌더링해주면 됩니다.
/posts/[category] 카테고리 게시물 목록
app/posts/[category]/page.tsx
export async function generateStaticParams() {
const categories = fs.readdirSync(BASE_DIR);
return categories.map((category) => ({
category,
}));
}
export default async function Page({ params }: { params: { category: string } }) {
const { category } = params;
const blogCategory = await loadBlogCategoryCount();
const blogs = await getPosts(category);
return (
<PageContainer>
<div className='w-[90%] select-none m-auto'>
<Browse blogs={blogs} categories={blogCategory} />
</div>
</PageContainer>
);
}
app-router
에서 params
는 generateStaticParams
에서 반환된 params
가 전달됩니다.
현재 동적 생성된 경로, 즉 category가 나오게 됩니다.
해당 카테고리를 아까 만들었던 getPosts
함수에 전달하여
카테고리에 해당하는 데이터를 추출합니다.
/posts/[category]/[slug] 내부 컨텐츠
app/posts/[category]/[slug]/page.tsx
export async function generateStaticParams() {
const allItems = fs.readdirSync(BASE_DIR);
const categories = allItems.filter(
(item) =>
!item.includes('.mdx') &&
fs.lstatSync(path.join(BASE_DIR, item)).isDirectory()
);
const allPaths = categories.flatMap((category) => {
const files = fs.readdirSync(path.join(BASE_DIR, category));
return files.map((file) => ({
category,
slug: file.replace('.mdx', ''),
}));
});
return allPaths;
}
export default async function Page({
params,
}: {
params: { category: string; slug: string };
}) {
const { category, slug } = params;
const { frontMatter, content } = await getPost(category, slug);
const tocControl = frontMatter?.toc === undefined && true;
const footControl = frontMatter?.comment === undefined && true;
return (
<PageContainer>
{tocControl && <Mdx_Toc footControl={footControl} />}
<div className='m-auto w-[95%] md:w-[75%] 3xl:w-[50%]'>
<Mdx_Header frontMatter={{ ...frontMatter, category: category }} />
<Mdx_Body content={content} />
{footControl && <Mdx_Footer/>}
</div>
</PageContainer>
);
}
params로 동적 라우팅을 생성해야 하므로, 현재 경로에 맞게 .mdx
부분은 제거하고
content, category, tocControl, footControl 4가지를 추출합니다.
각 컨트롤은 이전에 타입을 선언해두었던 데이터로 toc
를 보여줄지, comment
를 보여줄지를 조건부 렌더링합니다.
이제 모든 데이터를 전달해주었지만, 아직 mdx연동을 진행하지 않았습니다.
해당하는 경로로 들어가도 에러가 발생하는건 당연한겁니다!
이제 우리는 모든 경로로의 동적 라우팅 생성은 끝냇습니다.
동적 라우팅내의 동적라우팅을 설계했기때문에 상당히 복잡해 보이지만, 생각보다 어려운 내용은 아닙니다. 천천히
따라해보신다면 문제없이 적용하실 수 있을 겁니다.
이제 remote-mdx
를 설치해서 화면이 렌더링 되는지 확인 해 보겠습니다.
댓글은 포스팅에 도움이됩니다. 적극적인 의견 감사드립니다.