npm install react-markdown
// components/posts/post-detail/post-content.js
import classes from "./post-content.module.css";
import PostHeader from "./post-header";
import ReactMarkdown from "react-markdown";
const DUMMY_POST = {
title: "Getting Started With NextJS",
image: "getting-started-nextjs.png",
slug: "getting-started-with-nextjs",
content: "# This is a first post.",
date: "2024-04-15",
};
export default function PostContent() {
return (
<article className={classes.content}>
<PostHeader
title={DUMMY_POST.title}
image={`/images/posts/${DUMMY_POST.slug}/${DUMMY_POST.image}`}
/>
<ReactMarkdown>{DUMMY_POST.content}</ReactMarkdown>
</article>
);
}
---
title: "Getting Stared with NextJS"
data: "2023-04-17"
image: getting-started-nextjs.png
slug: "getting-started-with-nextjs"
excerpt: "NextJS is a the React framework for production."
isFeatured: true
---
# This is a title
🔗 [Google](www.google.com)
---
안에는 메타데이터를 작성하였다.npm install gray-matter
// lib/posts-util.js
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const postsDirectory = path.join(process.cwd(), "posts"); // 마크다운이 들어있는 전체 posts 디렉토리 경로 설정
function getPostData(fileName) {
const filePath = path.join(postsDirectory, fileName);
const fileContent = fs.readFileSync(filePath, "utf-8");
// gray-matter 이용해 데이터 가져오기
const { data, content } = matter(fileContent); // data: 메타데이터 포함, content: 실제 콘텐츠 포함
const postSlug = fileName.replace(/\.md$/, ""); // 파일 확장자 제거
const postData = { slug: postSlug, ...data, content };
return postData;
}
export function getAllPosts() {
// 마크다운 파일이 몇 개 있는지 확인.
const postFiles = fs.readdirSync(postsDirectory); // 동기적으로 모든 콘텐츠 가져옴. 한번에 전체 콘텐츠 읽어옴.
// 메타데이터 + 내부 데이터 가져오기
const allPosts = postFiles.map((postFile) => {
return getPostData(postFile);
});
const sortedPosts = allPosts.sort((postA, postB) =>
postA.date > postB.date ? -1 : 1
);
return sortedPosts;
}
export function getFeaturedPosts() {
const allPosts = getAllPosts();
const featuredPosts = allPosts.filter((post) => post.isFeatured);
return featuredPosts;
}
getFeaturedPosts
함수를 이용하여 FeaturedPosts 표현하기// pages/index.js
import Hero from "@/components/home-page/hero";
import FeaturedPosts from "@/components/home-page/featured-posts";
import { getFeaturedPosts } from "@/lib/posts-util";
export default function HomePage({ posts }) {
return (
<>
<Hero />
<FeaturedPosts posts={posts} />
</>
);
}
export function getStaticProps() {
const featuredPosts = getFeaturedPosts();
return {
props: {
posts: featuredPosts,
},
revalidate: 1000, //1000초 당 다시 확인하여 최신 데이터 반영.
};
}
getStaticProps
를 이용해 사전 렌더링을 하여 해당 데이터를 HomePage
에 전달getAllPosts
함수를 이용하여 AllPosts 표현하기import AllPosts from "@/components/posts/all-posts";
import { getAllPosts } from "@/lib/posts-util";
export default function AllPostsPage({ posts }) {
return <AllPosts posts={posts} />;
}
export function getStaticProps() {
const allPosts = getAllPosts();
return {
props: {
posts: allPosts,
},
revalidate: 600,
};
}
getPostData
의 수정이 필요하다.// lib/posts-util.js
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const postsDirectory = path.join(process.cwd(), "posts"); // 마크다운이 들어있는 전체 posts 디렉토리 경로 설정
// 리팩토링
export function getPostsFiles() {
return fs.readdirSync(postsDirectory);
}
// slug를 이용하여 데이터를 가져오도록..
export function getPostData(postIdentifier) {
const postSlug = postIdentifier.replace(/\.md$/, ""); // 파일 확장자 제거
const filePath = path.join(postsDirectory, `${postSlug}.md`);
const fileContent = fs.readFileSync(filePath, "utf-8");
// gray-matter 이용해 데이터 가져오기
const { data, content } = matter(fileContent); // data: 메타데이터 포함, content: 실제 콘텐츠 포함
const postData = { slug: postSlug, ...data, content };
return postData;
}
export function getAllPosts() {
// 마크다운 파일이 몇 개 있는지 확인.
const postFiles = getPostsFiles(); // 동기적으로 모든 콘텐츠 가져옴. 한번에 전체 콘텐츠 읽어옴.
// 메타데이터 + 내부 데이터 가져오기
const allPosts = postFiles.map((postFile) => {
return getPostData(postFile);
});
const sortedPosts = allPosts.sort((postA, postB) =>
postA.date > postB.date ? -1 : 1
);
return sortedPosts;
}
export function getFeaturedPosts() {
const allPosts = getAllPosts();
const featuredPosts = allPosts.filter((post) => post.isFeatured);
return featuredPosts;
}
// pages/posts/[slug].js
import PostContent from "@/components/posts/post-detail/post-content";
import { getPostData, getPostsFiles } from "@/lib/posts-util";
export default function PostDetailPage({ post }) {
return <PostContent post={post} />;
}
export function getStaticProps(context) {
const { params } = context;
const { slug } = params;
const postData = getPostData(slug);
return {
props: {
post: postData,
},
revalidate: 600, //전체 어플리케이션을 다시 구축하지 않고 마크다운 파일이 업데이트 되어 있는지 확인.
};
}
// 동적인 페이지이므로 getStaticProps만으로는 안됨. 미리 생성해야 하는 모든 경로를 갖게 한다.
export function getStaticPaths() {
const postFileNames = getPostsFiles();
const slugs = postFileNames.map((fileName) => fileName.replace(/.md$/, ""));
return {
paths: slugs.map((slug) => ({ params: { slug: slug } })),
fallback: false,
// fallback: "blocking", // 페이지에 방문할 때, 게시물이 뜰 때까지 기다림(인기있는 게시물만 미리 생성.). true를 통해서 fallback 페이지 설정 가능.
};
}
import classes from "./post-content.module.css";
import PostHeader from "./post-header";
import ReactMarkdown from "react-markdown";
export default function PostContent({ post }) {
return (
<article className={classes.content}>
<PostHeader
title={post.title}
image={`/images/posts/${post.slug}/${post.image}`}
/>
<ReactMarkdown>{post.content}</ReactMarkdown>
</article>
);
}
<Image>
컴포넌트를 통해 마크다운으로부터 이미지 렌더링하기---
title: "Getting Started with NextJS"
date: "2022-10-16"
image: getting-started-nextjs.png
excerpt: NextJS is a the React framework for production - it makes building fullstack React apps and sites a breeze and ships with built-in SSR.
isFeatured: true
---
NextJS is a **framework for ReactJS**.
Wait a second ... a "framework" for React? Isn't React itself already a framework for JavaScript?
Well ... first of all, React is a "library" for JavaScript. That seems to be important for some people.
Not for me, but still, there is a valid point: React already is a framework / library for JavaScript. So it's already an extra layer on top of JS.
## Why would we then need NextJS?
Because NextJS makes building React apps easier - especially React apps that should have server-side rendering (though it does way more than just take care of that).
In this article, we'll dive into the core concepts and features NextJS has to offer:
- File-based Routing
- Built-in Page Pre-rendering
- Rich Data Fetching Capabilities
- Image Optimization
- Much More
## File-based Routing

... More content ...
/* 예시 */
.content img {
max-width: 200px;
}
그러나 스타일보다 더 큰 문제는 이미지가 Next.js로 최적화되지 않았다는 것이다.

// components/posts/post-detail/post-content.js
import classes from "./post-content.module.css";
import PostHeader from "./post-header";
import ReactMarkdown from "react-markdown";
import Image from "next/image";
export default function PostContent({ post }) {
const customRenderers = {
// img(image) {
// //react-markdown은 이 메서드를 호출해서 마크다운 콘텐츠의 이미지를 찾아낸다.
// // 
// return (
// <Image
// src={`/images/posts/${post.slug}/${image.src}`}
// alt={image.alt}
// width={600}
// height={300}
// />
// );
// },
p(paragraph) {
// 단순 문자열 뿐만 아니라, ![]()으로 렌더링 된 이미지도 포함된다.
// 일반 문자열은 오버라이드 하지 않고 이미지와 관련된 것만 오버라이드 할 것
const { node } = paragraph;
if (node.children[0].tagName === "img") {
const image = node.children[0];
return (
<div className={classes.image}>
<Image
src={`/images/posts/${post.slug}/${image.properties.src}`}
alt={image.properties.alt}
width={600}
height={300}
/>
</div>
);
}
return <p>{paragraph.children}</p>;
},
};
return (
<article className={classes.content}>
<PostHeader
title={post.title}
image={`/images/posts/${post.slug}/${post.image}`}
/>
<ReactMarkdown components={customRenderers}>{post.content}</ReactMarkdown>
</article>
);
}
<ReactMarkdown renderers={} />
를 사용했으나 현재는 renderers
대신 components
를 이용하도록 변경되었고, 해당 props에서 image를 오버라이드 할 때 단순히 image
가 아닌 img
로 사용해야하고 paragraph
대신 p
로 사용해야 한다.🔗 참고 | react-markdown
🔗 react-markdown changelog
npm install react-syntax-highlighter
→ 코드를 쉽게 강조할 수 있는 패키지// components/posts/post-detail/post-content.js
import classes from "./post-content.module.css";
import PostHeader from "./post-header";
import ReactMarkdown from "react-markdown";
import Image from "next/image";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; // 테마
export default function PostContent({ post }) {
const customRenderers = {
// img(image) {
// //react-markdown은 이 메서드를 호출해서 마크다운 콘텐츠의 이미지를 찾아낸다.
// // 
// return (
// <Image
// src={`/images/posts/${post.slug}/${image.src}`}
// alt={image.alt}
// width={600}
// height={300}
// />
// );
// },
p(paragraph) {
// 단순 문자열 뿐만 아니라, ![]()으로 렌더링 된 이미지도 포함된다.
// 일반 문자열은 오버라이드 하지 않고 이미지와 관련된 것만 오버라이드 할 것
const { node } = paragraph;
if (node.children[0].tagName === "img") {
const image = node.children[0];
return (
<div className={classes.image}>
<Image
src={`/images/posts/${post.slug}/${image.properties.src}`}
alt={image.properties.alt}
width={600}
height={300}
/>
</div>
);
}
return <p>{paragraph.children}</p>;
},
code(code) {
const { className, children } = code;
const language = className.split("-")[1]; // className is something like language-js => We need the "js" part here
return (
<SyntaxHighlighter
language={language}
style={atomDark}
children={children}
/>
);
},
};
return (
<article className={classes.content}>
<PostHeader
title={post.title}
image={`/images/posts/${post.slug}/${post.image}`}
/>
<ReactMarkdown components={customRenderers}>{post.content}</ReactMarkdown>
</article>
);
}