๐ŸŒฟ

mdํŒŒ์ผ์˜ image path ์„ค์ •ํ•˜๊ธฐ

mdํŒŒ์ผ์˜ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ๋ณ€๊ฒฝํ•ด์„œ ์ด๋ฏธ์ง€๋ฅผ ์ œ๋Œ€๋กœ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์ž!

2023.11.10

ํ˜„์žฌ 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 ํด๋” ์•ˆ์˜ ํŒŒ์ผ๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋”ฐ๋ผ์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ๋ฒ• ์ค‘ ์„ ํƒํ•ด์•ผ ํ–ˆ๋‹ค.

  • ์™ธ๋ถ€ ์ €์žฅ์†Œ์— ์—…๋กœ๋“œํ•˜๊ณ  URL ๊ฐ€์ ธ์˜ค๊ธฐ.
  • ํผ๋ธ”๋ฆญ ํด๋”์— ์ด๋ฏธ์ง€ ๋„ฃ์€ ๋’ค mdํŒŒ์ผ์—์„œ ํ•ด๋‹น ๊ฒฝ๋กœ๋กœ ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ.
  • ๋งˆํฌ์—… ๋ Œ๋”๋ง ์‹œ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ๋ฐ”๊ฟ”์ฃผ๊ธฐ

๋‚˜ ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” 1๋ฒˆ์„ ํƒํ• ๊นŒ ํ–ˆ์ง€๋งŒ, ํŒŒ์ผ์„ ์“ธ ๋•Œ ๊ธ€ ๋”ฐ๋กœ ์ด๋ฏธ์ง€ ๋”ฐ๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•˜๊ณ , ๊ฐ ์ด๋ฏธ์ง€๊ฐ€ ์–ด๋””์— ์žˆ๋Š”์ง€ ๋‚˜์ค‘์— ํ—ท๊ฐˆ๋ฆด ๊ฒƒ ๊ฐ™์•„์„œ 3๋ฒˆ์„ ํƒํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ด ๋ถ„์˜ ๋ธ”๋กœ๊ทธ ๊ธ€์„ ์ฐธ๊ณ ํ•ด์„œ ์ด ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ๋ฐ”๊ฟ”์ฃผ๊ธฐ ์œ„ํ•œ ํ”„๋กœ์„ธ์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ๊ธฐ์กด์— /public์— ์žˆ๋˜ ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ ์ด๋ฏธ์ง€๋“ค์„ ๋ชจ๋‘ ์‚ญ์ œํ•œ๋‹ค. (์ดˆ๊ธฐํ™”)
  2. /posts ํด๋”์˜ ๋ชจ๋“  ๊ธ€ ์ด๋ฏธ์ง€๋ฅผ /public์œผ๋กœ ์˜ฎ๊ธด๋‹ค.

์ด๋ฅผ ์œ„ํ•ด ํ•ด๋‹น ์ž‘์—…์„ ํ•˜๋Š” 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/

@ 2025. nyoung all rights reserved