๐ฆพ
NextJS๋ก ๋ธ๋ก๊ทธ ๋ง๋ค๋ฉฐ ๊ฒช์ ์ข์ถฉ์ฐ๋ ํํ๊ธฐ
blog
2025-04-03
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๋ฅผ ์ฌ์ฉํ๋ค. ์๋๋ ๋ด๊ฐ ํ๋ ์ฝ์ง์ ๋ํ ๋ด์ฉ์ด๋ค.
์ ๋จน์๋ ๋ถ๋ถ ์ค ํ๋๋, ๋งํฌ๋ค์ด์ ์คํ์ผ์ ์ ์ฉํ๋ ๊ฒ์ด์๋ค. ํ.. ์ ๋ง ์ด๊ฑฐ ๋๋ฌธ์ ๊ณ ์์ ํ๋ค. tailwind์ preflight ์คํ์ผ ์ด๊ธฐํ ๋๋ฌธ์ ๋ชจ๋ ํ๊ทธ๋ค์ ์คํ์ผ์ด ์ด๊ธฐํ๊ฐ ์ด๋ฃจ์ด์ ธ์ markdownํ์ฑ์ด ์ฑ๊ณต์ ์ผ๋ก <pre><code>
. . ์ ๊ฐ์ด ์ด๋ฃจ์ด์ก์ด๋ ์คํ์ผ์ ๊ทธ๋๋ก์๋ค.
๊ทธ๋์ ์ด๋ป๊ฒ ํด๊ฒฐ์ ์๋ํ๋ . . . ๋งจ ์ฒ์์๋ ์ ์ญ css ํ์ผ์ base ๋ ์ด์ด๋ฅผ ํ์ฅํด์ ์ผ๋ค. ๊ทธ ์ด์ ๋ Preflight๋ฅผ ์ ์งํ๋ฉด์ ํ์ํ ์ปค์คํ ์คํ์ผ์ ๋ง๋ถ์ด๋ ๊ฒ์ด ์ผ๊ด์ฑ๊ณผ ์์ธก ๊ฐ๋ฅํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฅํ๋ ๋ฐฉ๋ฒ์ด๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ๋ฌ๋ค๊ฐ, @next/mdx๋ก ๋งํฌ๋ค์ด ํ์ฑํ๋ ๋ฐฉ์์ ๋ฐ๊พธ๊ณ ๋์๋, mdx์ ์ญ Provider์ ๊ฐ ํ๊ทธ์ ๋ง์ถ ์ปค์คํ ์ปดํฌ๋ํธ๋ฅผ ์ ์ํ์ฌ ๋งคํํด์ฃผ์๋ค.
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 }1415 .main h1 {16 font-size: 2.5rem; /* ์ฝ 40px */17 font-weight: 800;18 line-height: 1.2;19 margin: 1rem 0;20 }2122 .main h2 {23 font-size: 2rem;24 font-weight: 700;25 margin: 1rem 0;26 }2728 .main h3 {29 font-size: 1.75rem;30 font-weight: 600;31 margin: 1rem 0;32 }33}
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";67const 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. . .1718export 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.tsx2export function useMDXComponents(components: MDXComponents): MDXComponents {3 return {4 h1: H1,5 . . . .6 code: InlineCode,7 pre: CodeBlock,8 ...components,9 };10}1112// CodeBlock.tsx13const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {14 if (!code) return null;1516 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 ํ์ ์ด๋ค.
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 ๋ ธ๋ - ์ฝ๋ ๋ฐ์ดํฐ ์ป๊ธฐ ์ํจ34}: 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);34const { children: codeString } = children.props as CodeElementProps;5const code = typeof codeString === "string" ? codeString : "";67return (8 <Highlight theme={themes.dracula} code={code} language={language}>9 {({ style, tokens, getLineProps, getTokenProps }) => {10 const slicedTokens = tokens.slice(0, -1);1112 return (13 <pre14 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
๊ฒฝ๋ก๋ก ๊ฐ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋ด๊ฐ ์์ฑํ ๊ธ๋ค์ด ๋ฆฌ์คํธ ํํ๋ก ๋ณด์ฌ์ผ ํ๋ค.
๋ฐฐํฌ๋ฅผ ํ๊ณ ๋ค์ด๊ฐ ๋ณด๋ ์๋ฌ๊ฐ ๋๋ฅผ ๋ฐ๊ฒจ์คฌ๋ค.(์ธ์ ๋ด๋ ์ ํ ๋ฐ๊ฐ์ง ์์ ์น๊ตฌ๋ค.)
ํด๋น ๊ฒฝ๋ก์ ์กด์ฌํ๋ ํ์ผ์ด ์๋ค๋ ๊ฒ์ด์๋ค. . .
์์ง? ๋ก์ปฌ์์ ์ ๋๋๋ฐ, ๋น๋ํ์ฌ ๋ฐฐํฌ๋ฅผ ํ๋ ํด๋น ๋ฌธ์ ๊ฐ ์๊ธฐ๋ ๊ฑธ๊น?
์๋ฟ์ธ! ํ์ฌ mdx ํ์ผ๋ค์ ์ฐพ๊ธฐ ์ํด /(contents)์๋์ ํด๋๋ค์ page.mdx๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๊ฐ์ ธ์ค๊ณ ์์๋ค.
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 ์๋ฒ๋ฅผ ๋๋ ธ๋๋ฐ,,, ์ค๋ฅ๊ฐ ๋ฐ์ํ์ฌ ๋์๊ฐ์ง ์์๋ค.
1import remarkGfm from "remark-gfm";2import createMDX from "@next/mdx";34/** @type {import('next').NextConfig} */5const nextConfig = {6 // Allow .mdx extensions for files7 pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],8 // Optionally, add any other Next.js config below9};1011const withMDX = createMDX({12 // Add markdown plugins here, as desired13 options: {14 remarkPlugins: [remarkGfm],15 },16});1718// Combine MDX and Next.js config19export default withMDX(nextConfig);
์ด๊ฒ ๋ฌด์จ ์๋ฌ์ผ๊น..
1> next dev --turbo23 โฒ Next.js 15.1.7 (Turbopack)4 - Local: http://localhost:30005 - Network: http://192.168.35.53:30006 - Experiments (use with caution):7 ยท turbo89 โ 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 ํค์๋๊ฐ ํฌํจ๋์ด ์์ผ๋ฉด ํด๋น ๋ฌธ์ ๊ฐ ์๊ธฐ๋ ๋ฏ ํ๋ค.
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์ ์ฌ์ฉํ๋๋ก ํ๋ ํค์๋๋ค. ๊ทธ๋ฌ๋ ์์ง ๋ถ์์ ํ ๋ถ๋ถ์ด ์๋ ๊ฒ ๊ฐ๋ค.