mdํ์ผ์ ์ด๋ฏธ์ง ๊ฒฝ๋ก๋ฅผ ๋ณ๊ฒฝํด์ ์ด๋ฏธ์ง๋ฅผ ์ ๋๋ก ๋ถ๋ฌ์ฌ ์ ์๋๋ก ํ์!
ํ์ฌ md ํ์ผ์ ์ต์์ ๊ฒฝ๋ก์ data/posts/[ํฌ์คํธ ์ด๋ฆ]/index.md์ ๋ง๋ค๊ณ ์๋ค. ์ด๋ mdํ์ผ ์์์ ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํ๊ณ ์ถ์ ๋, ์ด๋ฏธ์ง ํ์ผ์ mdํ์ผ๊ณผ ๊ฐ์ ๊ณ์ธต์ ๋ฃ๊ณ ์ฌ์ฉํ๊ณ ์๋ค.
- data
- post
- first-post
- index.md
- first.jpg
์ด๋ ๊ฒ ๋ง๋ค๊ณ index.md
์์ ์๋ ๊ฒฝ๋ก๋ก ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์จ๋ค.
first-post์ index.md
์์ ./first.jpg
๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ฏธ์ง๊ฐ ์ ๋ถ๋ฌ์์ง๋ค.
๊ทธ๋ฐ๋ฐ ๋ฌธ์ ๋ Next.js์์ ๋น๋ ํ์์ ์ ์ ํ์ผ์ ์ ๊ทผํ ๋, public ํด๋ ์์ ํ์ผ๋ง ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ด๋ค. ๋ฐ๋ผ์ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ๋ฒ ์ค ์ ํํด์ผ ํ๋ค.
๋ ๊ฐ์ ๊ฒฝ์ฐ์๋ 1๋ฒ์ ํํ ๊น ํ์ง๋ง, ํ์ผ์ ์ธ ๋ ๊ธ ๋ฐ๋ก ์ด๋ฏธ์ง ๋ฐ๋ก ์ ๋ ฅํด์ผ ํ๊ณ , ๊ฐ ์ด๋ฏธ์ง๊ฐ ์ด๋์ ์๋์ง ๋์ค์ ํท๊ฐ๋ฆด ๊ฒ ๊ฐ์์ 3๋ฒ์ ํํ๊ฒ ๋์๋ค.
๊ทธ๋ฆฌ๊ณ ์ด ๋ถ์ ๋ธ๋ก๊ทธ ๊ธ์ ์ฐธ๊ณ ํด์ ์ด ์์ ์ ํ ์ ์์๋ค.
์ด๋ฏธ์ง ๊ฒฝ๋ก๋ฅผ ๋ฐ๊ฟ์ฃผ๊ธฐ ์ํ ํ๋ก์ธ์ค๋ ๋ค์๊ณผ ๊ฐ๋ค.
์ด๋ฅผ ์ํด ํด๋น ์์ ์ ํ๋ script๋ฅผ ์ง๊ณ , ์๋ฒ ์คํ ์ ๊ณผ ๋น๋ ์คํ ์ ๋๋ ค์ฃผ๋ ์์ ์ ํด์ผ ํ๋ค.
๋๋ ํ ๋ฆฌ๋ฅผ ๋น์ฐ๊ธฐ ์ํด fs-extra๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ค. fs์ ๊ธฐ๋ฅ์ ๋ ํ์ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ๊ณ ์๊ฐํ๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
/bin/pre-build.mjs
import fsExtra from 'fs-extra';
import {copyFile, mkdir, readdir} from 'fs/promises';
// ๋ณ๊ฒฝํ ํ์ฅ์ ๋ฆฌ์คํธ
const imageExtensionList= ['.png', '.jpg', '.jpeg', '.gif', '.svg'];
const IMAGE_DIR='./public/posts';
const POST_DIR='./data/posts';
async function copyImages(sourceDir, targetDir, imageList) {
imageList.forEach((image) =>
{
const sourcePath = `${sourceDir}/${image}`;
const targetPath = `${targetDir}/${image}`;
copyFile(sourcePath, targetPath);
}
)
}
async function copyPostImages() {
const postNameList = await readdir(POST_DIR);
for (const postName of postNameList) {
const fileList = await readdir(`${POST_DIR}/${postName}`);
const imageFiles = fileList.filter((file) =>
imageExtensionList.some((extension) => file.endsWith(extension)))
await mkdir(`${IMAGE_DIR}/${postName}`, { recursive: true })
await copyImages(`${POST_DIR}/${postName}`, `${IMAGE_DIR}/${postName}`, imageFiles);
}
}
// 1. public ์์ posts ๋๋ ํ ๋ฆฌ ๋น์ฐ๊ธฐ
await fsExtra.emptyDir(IMAGE_DIR);
// 2. data/posts/${ํฌ์คํธ ์ด๋ฆ} => public/posts/${ํฌ์คํธ ์ด๋ฆ}์ผ๋ก ์ด๋ฏธ์ง ๋ณต์ฌํ๊ธฐ
copyPostImages();
script์์ pre prefix๋ฅผ ๋ถ์ด๋ฉด ๊ฐ ๋ช ๋ น์ด ์คํ ์ pre๊ฐ ๋ถ์ ์คํฌ๋ฆฝํธ๊ฐ ์๋์ ์ผ๋ก ์คํ๋๊ฒ ๋๋ค.
"scripts": {
...
"copy-image": "node ./bin/pre-build.mjs",
"predev": "npm run copy-image",
"prebuild": "npm run copy-image"
}
์ด๋ ๊ฒ ๋๋ฉด public ์์ post์ ์ด๋ฏธ์ง๋ค์ด ๊ณ์ธต ๊ตฌ์กฐ๋๋ก ๋ค์ด๊ฐ๋ค๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ, mdํ์ผ์ ์๋๊ฒฝ๋ก๋ก ์ ์ด์คฌ๋๋ฐ ์ด๋ ์ด๋ฏธ์ง์ ์๋๊ฒฝ๋ก๊ฐ ๊ทธ๋๋ก markup ์ ๋ด๊ธฐ๊ธฐ ๋๋ฌธ์ ์ค์ ์ด๋ฏธ์ง ํ์ผ์ ์ฝ์ด์ฌ ์ ์๋ ํ์์ด ๋ฐ๊ฒฌ๋๋ ๊ฒ์ด๋ค. ๊ทธ๋ ๋ค๊ณ md ํ์ผ์ ํผ๋ธ๋ฆญ ํด๋ ์์ ๊ฒฝ๋ก๋ฅผ ์จ์ฃผ๋ฉด ๊ฑฐ์ pre-build script ๋ฅผ ์ง ๊ฒ ์๋ฏธ๊ฑฐ ์์ด์ง๊ธด ํ๋ค.
๊ทธ๋ ๋ค๋ฉด!~! ๋งํฌ์ ํ๋ฌ๊ทธ์ธ์ผ๋ก ์ด๋ฏธ์ง๋ค์ ๊ฒฝ๋ก๋ฅผ ๊ต์ฒดํด์ค๋ค.
์๋ฅผ ๋ค์ด ./image1.jpg
=> /posts/first-post/image1.jpg
๋ก ๋ฐ๊ฟ์ฃผ๋ ๊ฒ์ด๋ค.
ํ๋ฌ๊ทธ์ธ ๋ฆฌ์คํธ๋ค mdast ๋ฌธ๋ฒ์ ์ํด ๊ตฌ๋ฌธ ํธ๋ฆฌ๋ก ์ฒ๋ฆฌ๋๋ค.
remark์์๋ mdํ์ผ์ JSON object ํ์์ผ๋ก ์ฐ์ธ AST๋ก ๋ฐ๊ฟ์ฃผ๊ธฐ ๋๋ฌธ์ ์ค๊ฐ ๊ณผ์ ์์ ํ๋ฌ๊ทธ์ธ์ด ์ด ํธ๋ฆฌ๋ฅผ ๊ฐ์ง๊ณ ์ค๊ฐ ์์ ์ ํ ์ ์๋ค.
์์ฒด ํ๋ฌ๊ทธ์ธ์ ๋ง๋ค์ด์ฃผ๊ธฐ ์ํด ์คํฌ๋ฆฝํธ๋ฅผ ํ๋ ์์ฑํ๋ค. ์ฌ์ค ํ๋ฌ๊ทธ์ธ์ด๋ผ๋ ๊ฑฐ์ฐฝํ ์ด๋ฆ์ด์ง๋ง, ์คํํ ํจ์๋ฅผ ์ ์ ํ return ํด์ฃผ๋ฉด ๋๋ค. (๋ง๋ก๋ ์ฝ๋ค. ์ฌ๊ธฐ์ ์ฝ์ง์ ์์ฒญ ํ๋ค)
import {visit} from 'unist-util-visit';
const IMAGE_PUBLIC_DIR = '/posts';
export default function changeImageSrc({path}) {
return function(tree, file) {
console.log(tree, file);
console.log(path);
visit(tree, (node) => {
if(node.children) {
const image = node.children.find(child => child.type === 'image');
if (image) {
const fileName = image.url.replace('./', '');
image.url = `${IMAGE_PUBLIC_DIR}/${path}/${fileName}`;
}
}
});
};
}
ํ๋ฌ๊ทธ์ธ์ ๋ง๋ค์๋ค๋ฉด, ์ด์ ์ ์ฉํด์ฃผ๋ฉด ๋๋ค. react-remark์์ ๋ด๋ถ์ ์ผ๋ก unified
๋ฅผ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ ๊ฑฐ๊ธฐ ๊ปด์ฃผ๋ฉด ๋๋ค.
import ReactMarkdown from 'react-markdown';
import {FC} from 'react';
import transformImgSrc from '../../plugins/change-md-image-path.mjs';
type Props = {
content: string;
currentPath: string;
};
const MarkdownViewer: FC<Props> = ({content, currentPath}) => {
return (
<ReactMarkdown remarkPlugins={[[transformImgSrc, {path: currentPath}]]}>
{content}
</ReactMarkdown>
);
};
export default MarkdownViewer;
์ด๋ ๊ฒ ๋ง๋ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ฉด ๋์ด๋ค.
๋์ค์ ํ์ผ ์์ ์ Promise.all์ด๋ allSettled๋ก ๋ณ๊ฒฝํด์ ๋น๋ ์๋๋ฅผ ๋ ๋์ผ ์ ์์ ๋ฏ ํ๋.
https://www.codeconcisely.com/posts/nextjs-relative-image-paths-in-markdown/