Bellog

๐Ÿฆพ

NextJS๋กœ ๋ธ”๋กœ๊ทธ ๋งŒ๋“ค๋ฉฐ ๊ฒช์€ ์ขŒ์ถฉ์šฐ๋Œ ํƒํ—˜๊ธฐ

blog

2025-04-03

์‚ฌ์šฉํ•œ ๊ธฐ์ˆ 

NextJS + TailwindCSS

๋งˆํฌ๋‹ค์šด ํŒŒ์‹ฑ: @next/mdx

์ฒ˜์Œ์—๋Š” ๋งˆํฌ๋‹ค์šด ํŒŒ์‹ฑ์„ remark, remark-html๋กœ ํ–ˆ์—ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‹ค๊ฐ€ @next/mdx ๊ฐ€ nextJS์—์„œ ์ง€์›ํ•˜๊ธฐ๋„ ํ•˜๊ณ  ๋ฌด์—‡๋ณด๋‹ค ์ „์—ญ MDXProvider๋ฅผ ์ œ๊ณตํ•˜์—ฌ์„œ, ์ „์—ญ์ ์ธ ์Šคํƒ€์ผ๋ง ํ•˜๊ธฐ๊ฐ€ ์šฉ์ดํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.

remark, remark-html์˜ ๊ฒฝ์šฐ์—๋Š” tailwind์˜ preflight ์Šคํƒ€์ผ ๋•Œ๋ฌธ์—, ๊ธฐ๋ณธ h1, h2, code์™€ ๊ฐ™์€ ํƒœ๊ทธ๋“ค์˜ ์Šคํƒ€์ผ์ด ์ดˆ๊ธฐํ™”๊ฐ€ ๋œ๋‹ค. ๊ทธ๋ ‡๊ธฐ์— @base ๋ ˆ์ด์•„์›ƒ์— ์˜ค๋ฒ„๋ผ์ด๋“œ๋ฅผ ํ•˜์—ฌ์„œ ์Šคํƒ€์ผ์„ ์žฌ์ •์˜ ํ•ด์ค˜์•ผ ํ•œ๋‹ค. ์‚ฌ์‹ค @next/mdx๋„ ์Šคํƒ€์ผ์„ MDXProvider์—์„œ ์žฌ์ •์˜ ํ•ด์ค˜์•ผ ํ•˜์ง€๋งŒ. . .(์กฐ์‚ผ๋ชจ์‚ฌ) ๊ทธ๋ž˜๋„ ์ด ๋ฐฉ์‹์ด ์ข€ ๋” ๊น”๋”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  @next/mdx๋ฅผ ์ด์šฉํ•จ์œผ๋กœ์„œ NextJS์— ์ตœ์ ํ™” ๋œ ๊ธฐ๋Šฅ๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ๊ธฐ์— @next/mdx๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ์•„๋ž˜๋Š” ๋‚ด๊ฐ€ ํ–ˆ๋˜ ์‚ฝ์งˆ์— ๋Œ€ํ•œ ๋‚ด์šฉ์ด๋‹ค.

TailwindCSS Preflight

์•  ๋จน์—ˆ๋˜ ๋ถ€๋ถ„ ์ค‘ ํ•˜๋‚˜๋Š”, ๋งˆํฌ๋‹ค์šด์— ์Šคํƒ€์ผ์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. ํ•˜.. ์ •๋ง ์ด๊ฑฐ ๋•Œ๋ฌธ์— ๊ณ ์ƒ์„ ํ–ˆ๋‹ค. tailwind์˜ preflight ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” ๋•Œ๋ฌธ์— ๋ชจ๋“  ํƒœ๊ทธ๋“ค์˜ ์Šคํƒ€์ผ์ด ์ดˆ๊ธฐํ™”๊ฐ€ ์ด๋ฃจ์–ด์ ธ์„œ markdownํŒŒ์‹ฑ์ด ์„ฑ๊ณต์ ์œผ๋กœ <pre><code> . . ์™€ ๊ฐ™์ด ์ด๋ฃจ์–ด์กŒ์–ด๋„ ์Šคํƒ€์ผ์€ ๊ทธ๋Œ€๋กœ์˜€๋‹ค.

๊ทธ๋ž˜์„œ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐ์„ ์‹œ๋„ํ–ˆ๋ƒ . . . ๋งจ ์ฒ˜์Œ์—๋Š” ์ „์—ญ css ํŒŒ์ผ์— base ๋ ˆ์ด์–ด๋ฅผ ํ™•์žฅํ•ด์„œ ์ผ๋‹ค. ๊ทธ ์ด์œ ๋Š” Preflight๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ํ•„์š”ํ•œ ์ปค์Šคํ…€ ์Šคํƒ€์ผ์„ ๋ง๋ถ™์ด๋Š” ๊ฒƒ์ด ์ผ๊ด€์„ฑ๊ณผ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ๋Ÿฌ๋‹ค๊ฐ€, @next/mdx๋กœ ๋งˆํฌ๋‹ค์šด ํŒŒ์‹ฑํ•˜๋Š” ๋ฐฉ์‹์„ ๋ฐ”๊พธ๊ณ  ๋‚˜์„œ๋Š”, mdx์ „์—ญ Provider์— ๊ฐ ํƒœ๊ทธ์— ๋งž์ถ˜ ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ •์˜ํ•˜์—ฌ ๋งคํ•‘ํ•ด์ฃผ์—ˆ๋‹ค.

BEFORE

global.css
1@layer base {
2 .main {
3 display: flex;
4 flex-direction: column;
5 overflow: hidden;
6 word-wrap: break-word;
7 font-size: 1.125rem; /* ๊ธฐ๋ณธ ๋ณธ๋ฌธ ํฌ๊ธฐ = 18px */
8 padding: 30px 0;
9 box-sizing: border-box;
10 line-height: 1.8;
11 color: hsl(var(--foreground));
12 max-width: 1000px;
13 }
14
15 .main h1 {
16 font-size: 2.5rem; /* ์•ฝ 40px */
17 font-weight: 800;
18 line-height: 1.2;
19 margin: 1rem 0;
20 }
21
22 .main h2 {
23 font-size: 2rem;
24 font-weight: 700;
25 margin: 1rem 0;
26 }
27
28 .main h3 {
29 font-size: 1.75rem;
30 font-weight: 600;
31 margin: 1rem 0;
32 }
33}

AFTER

mdx-components.tsx
1import type { MDXComponents } from "mdx/types";
2import React from "react";
3import { Pre } from "./components/Pre";
4import Header from "./components/Header";
5import Footer from "./components/Footer";
6
7const H1: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
8 <h1 className="text-4xl font-bold my-7" {...props} />
9);
10const H2: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
11 <h2 className="text-3xl font-semibold my-6" {...props} />
12);
13const H3: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
14 <h3 className="text-2xl font-semibold my-5" {...props} />
15);
16. . .
17
18export function useMDXComponents(components: MDXComponents): MDXComponents {
19 return {
20 h1: H1,
21 h2: H2,
22 h3: H3,
23 . . .
24 ...components,
25 };
26}
27

๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ์˜ ํ•ต์‹ฌ - ์ฝ”๋“œ๋ธ”๋Ÿญ

๋‹ค์Œ์œผ๋กœ ์•  ๋จน์—ˆ๋˜ ๋ถ€๋ถ„์€ ๋ฐ”๋กœ ์ฝ”๋“œ๋ธ”๋Ÿญ์ด๋‹ค. ์ฝ”๋“œ ๋ธ”๋Ÿญ์— ๊ฐ ์–ธ์–ด์— ๋งž์ถ˜ ๋ฌธ๋ฒ• ํ•˜์ด๋ผ์ดํŠธ ๊ธฐ๋Šฅ์ด ์žˆ์–ด์•ผ ํ•˜๋Š”๋ฐ, ๊ทธ๊ฒŒ ํ•˜๋‚˜๋„ ์—†๋‹ค ๋ณด๋‹ˆ ๋„ˆ๋ฌด ํ—ˆ์ ‘ํ•œ ๋Š๋‚Œ์ด ๋“ค์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ prism-react-renderer๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„์ž…ํ–ˆ๋‹ค.

1// mdx-components.tsx
2export function useMDXComponents(components: MDXComponents): MDXComponents {
3 return {
4 h1: H1,
5 . . . .
6 code: InlineCode,
7 pre: CodeBlock,
8 ...components,
9 };
10}
11
12// CodeBlock.tsx
13const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {
14 if (!code) return null;
15
16 return (
17 <Highlight theme={themes.shadesOfPurple} code={code} language={language}>
18 {({ style, tokens, getLineProps, getTokenProps }) => (
19 <pre style={style}>
20 {tokens.map((line, i) => (
21 <div key={i} {...getLineProps({ line })}>
22 <span>{i + 1}</span>
23 {line.map((token, key) => (
24 <span key={key} {...getTokenProps({ token })} />
25 ))}
26 </div>
27 ))}
28 </pre>
29 )}
30 </Highlight>
31 );
32};

์›๋ž˜๋Š” ์œ„์™€ ๊ฐ™์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ •์˜ํ•˜์—ฌ ์‚ฌ์šฉํ–ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์˜๋„ํ•œ ๋Œ€๋กœ ์ฝ”๋“œ๋ธ”๋Ÿญ์ด ๋ Œ๋”๋ง๋˜์ง€ ์•Š์•˜๋‹ค. ๋ฐ”๋กœ props(code, language)๊ฐ€ undefined๋กœ ๋„˜๊ฒจ์ง€๊ณ  ์žˆ์—ˆ๋‹ค.

ํ•ด๋‹น ๋ฌธ์„œ์—์„œ mdx์˜ provider์—์„œ๋Š” ์ฝ”๋“œ๋ธ”๋Ÿญ ์ •๋ณด๋ฅผ children์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ์•„๋ƒˆ๋‹ค. children์€ Reac.Node ํƒ€์ž…์ด๋‹ค.

children
1{
2 '$$typeof': Symbol(react.transitional.element),
3 type: [Function: InlineCode],
4 key: null,
5 props: {
6 className: 'language-javascript',
7 children: 'function greet(name) {\r\n' +
8 ' return `Hello, ${name}!`;\r\n' +
9 '}\r\n' +
10 '\r\n' +
11 'console.log(greet("World"));\n'
12 },
13 _owner: {
14 name: 'MDXContent',
15 env: 'Server',
16 key: null,
17 owner: null,
18 props: { params: [Promise], searchParams: [Promise] }
19 },
20 _store: {}
21}

์‹ค์ œ ์šฐ๋ฆฌ๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋Š” children.props.children์— ๋ฌธ์ž์—ด ํ˜•ํƒœ๋กœ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ค ํ™•์žฅ์ž๋กœ ์ผ๋Š”์ง€์— ๋Œ€ํ•œ ์ •๋ณด๋Š” className์œผ๋กœ ๋„˜๊ฒจ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

1export default function CodeBlock({
2 children, // React ๋…ธ๋“œ - ์ฝ”๋“œ ๋ฐ์ดํ„ฐ ์–ป๊ธฐ ์œ„ํ•จ
3
4}: CodeBlockProps): JSX.Element {
5. . .
6const className = children?.props.className; // ์ฝ”๋“œ๊ฐ€ ์–ด๋–ค ํฌ๋งท์œผ๋กœ ์“ฐ์˜€๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•จ('language-<ํฌ๋งท>' ํ˜•ํƒœ)

์ด์ œ ํ•ด๋‹น ์–ธ์–ด์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ prism-react-renderer์—์„œ ์ œ๊ณตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ธ Highlight์— ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค.

code: children.props.childer, language: children.props.className ์ด๋ ‡๊ฒŒ ์ถ”์ถœํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ํ•ด๋‹น ์†์„ฑ์— ๋Œ€ํ•ด ๋ช…์‹œํ•ด์ค˜์•ผ ํ•œ๋‹ค. ๊ทธ๋Ÿฌ์ง€ ์•Š์œผ๋ฉด unknown' ํ˜•์‹์— 'className' ์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌ๊ฐ€ ์ƒ๊ธด๋‹ค.

1interface CodeElementProps {
2 children?: string;
3 className?: string;
4}

์œ„ ๋‚ด์šฉ์„ ๋ฐ˜์˜ํ•œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

1const { className: codeClassName } = children.props as CodeElementProps;
2const { title, highlight, language } = parseMeta(codeClassName);
3
4const { children: codeString } = children.props as CodeElementProps;
5const code = typeof codeString === "string" ? codeString : "";
6
7return (
8 <Highlight theme={themes.dracula} code={code} language={language}>
9 {({ style, tokens, getLineProps, getTokenProps }) => {
10 const slicedTokens = tokens.slice(0, -1);
11
12 return (
13 <pre
14 style={{
15 ...style,
16 borderRadius: "10px",
17 padding: "10px 0",
18 margin: "20px 0",
19 }}
20 >
21 {slicedTokens.map((line, i) => (
22 <div key={i} {...getLineProps({ line })}>
23 <span style={{ padding: "0 15px 0 10px", color: "#6a6192" }}>
24 {i + 1}
25 </span>
26 {line.map((token, key) => (
27 <span key={key} {...getTokenProps({ token })} />
28 ))}
29 </div>
30 ))}
31 </pre>
32 );
33 }}
34 </Highlight>
35 );

์ด๋ฒˆ์—” posts๊ฐ€ ์•ˆ๋ณด์ž„

/posts ๊ฒฝ๋กœ๋กœ ๊ฐ€๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ๊ธ€๋“ค์ด ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ๋ณด์—ฌ์•ผ ํ•œ๋‹ค.

์›๋ž˜๋ผ๋ฉด ๋ณด์—ฌ์•ผํ•  ๊ด‘๊ฒฝ

๋ฐฐํฌ๋ฅผ ํ•˜๊ณ  ๋“ค์–ด๊ฐ€ ๋ณด๋‹ˆ ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฅผ ๋ฐ˜๊ฒจ์คฌ๋‹ค.(์–ธ์ œ๋ด๋„ ์ „ํ˜€ ๋ฐ˜๊ฐ‘์ง€ ์•Š์€ ์นœ๊ตฌ๋‹ค.)

๋‚ด๊ฐ€ ๋ณธ ๊ด‘๊ฒฝ ์„œ๋ฒ„ ๋กœ๊ทธ

ํ•ด๋‹น ๊ฒฝ๋กœ์— ์กด์žฌํ•˜๋Š” ํŒŒ์ผ์ด ์—†๋‹ค๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. . .

์™œ์ง€? ๋กœ์ปฌ์—์„  ์ž˜ ๋๋Š”๋ฐ, ๋นŒ๋“œํ•˜์—ฌ ๋ฐฐํฌ๋ฅผ ํ•˜๋‹ˆ ํ•ด๋‹น ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š” ๊ฑธ๊นŒ?

์•„๋ฟ”์‹ธ! ํ˜„์žฌ mdx ํŒŒ์ผ๋“ค์„ ์ฐพ๊ธฐ ์œ„ํ•ด /(contents)์•„๋ž˜์˜ ํด๋”๋“ค์˜ page.mdx๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐ€์ ธ์˜ค๊ณ  ์žˆ์—ˆ๋‹ค.

getPosts
1const posts = await Promise.all(
2 slugs.map(async (slug) => {
3 const data = await import(`../app/(contents)/${slug}/page.mdx`);
4 const metadata = data.meta;
5 return { slug, ...metadata } as PostData;
6 })
7);

๊ทธ๋Ÿฌ๋‚˜ ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ๋ถ€๋ถ„์€ ๋ฐ”๋กœ ์ƒ๋Œ€๊ฒฝ๋กœ๋ฅผ ์ด์šฉํ•˜๊ณ  ์žˆ๋‹ค๋Š” ์ ์ด๋‹ค.

๋ฐฐํฌ๋ฅผ ํ•˜๊ฒŒ ๋˜๋ฉด ํ˜„์žฌ ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ(process.cwd())์™€์˜ ์ƒ๋Œ€ ๊ด€๊ณ„๊ฐ€ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์–ด ์˜ฌ๋ฐ”๋ฅธ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ž˜์„œ process.cwd()๋ฅผ ํ†ตํ•ด ์ ˆ๋Œ€๊ฒฝ๋กœ๋กœ ๋ช…์‹œ๋ฅผ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

1// MDX ํŒŒ์ผ๋“ค์ด ์œ„์น˜ํ•œ ๋””๋ ‰ํ† ๋ฆฌ
2const postsDir = path.join(process.cwd(), "src", "app", "(contents)");

์‚ฌ์†Œํ•œ ์˜ค๋ฅ˜

remark ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ ์šฉํ•˜๊ณ ,pnpm run dev ๋ฅผ ํ†ตํ•ด dev ์„œ๋ฒ„๋ฅผ ๋Œ๋ ธ๋Š”๋ฐ,,, ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋Œ์•„๊ฐ€์งˆ ์•Š์•˜๋‹ค.

next.config.ts
1import remarkGfm from "remark-gfm";
2import createMDX from "@next/mdx";
3
4/** @type {import('next').NextConfig} */
5const nextConfig = {
6 // Allow .mdx extensions for files
7 pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
8 // Optionally, add any other Next.js config below
9};
10
11const withMDX = createMDX({
12 // Add markdown plugins here, as desired
13 options: {
14 remarkPlugins: [remarkGfm],
15 },
16});
17
18// Combine MDX and Next.js config
19export default withMDX(nextConfig);

์ด๊ฒŒ ๋ฌด์Šจ ์—๋Ÿฌ์ผ๊นŒ..

1> next dev --turbo
2
3 โ–ฒ Next.js 15.1.7 (Turbopack)
4 - Local: http://localhost:3000
5 - Network: http://192.168.35.53:3000
6 - Experiments (use with caution):
7 ยท turbo
8
9 โœ“ Starting...
10[Error: loader /Users/castle_bell/Documents/Github/bellog/node_modules/.pnpm/@next+mdx@15.2.4_@mdx-js+loader@3.1.0_acorn@8.14.1__@mdx-js+react@3.1.0_@types+react@19.0.10_react@19.0.0_/node_modules/@next/mdx/mdx-js-loader.js for match "*.mdx" does not have serializable options. Ensure that options passed are plain JavaScript objects and values.]

์ด์Šˆ๋ฅผ ์ฐพ์•„๋ณด๋‹ˆ dev ์Šคํฌ๋ฆฝํŠธ์—์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ๋’ค์— โ€”turbo ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š” ๋“ฏ ํ—€๋‹ค.

package.json
1 "scripts": {
2 "dev": "next dev --turbo",
3 "build": "next build",
4 "start": "next start",
5 "lint": "next lint",
6 "prepare": "husky"
7 },

๊ทธ๋ž˜์„œ โ€” turbo๋ฅผ ๋นผ์คฌ๋”๋‹ˆ ์ž˜ ๋Œ์•„๊ฐ”๋‹คใ…Žใ…Ž turbo๋Š” NextJS์˜ ๊ธฐ๋ณธ ๋ฒˆ๋“ค๋Ÿฌ์ธ Webpack ๋Œ€์‹ , ๋” ์ง„๋ณด๋œ turbopack์„ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๋Š” ํ‚ค์›Œ๋“œ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์•„์ง ๋ถˆ์•ˆ์ •ํ•œ ๋ถ€๋ถ„์ด ์žˆ๋Š” ๊ฒƒ ๊ฐ™๋‹ค.