Next.js MDX 코드블럭에 복사 버튼을 추가해보자

Next.js로 MDX 블로그를 만들었는데 코드블럭에 복사 버튼을 추가하고 싶었다.
Gatsby에는 해당 기능을 쉽게 구현하도록 도와주는 remark-plugin
이 있었지만,
Next.js에서는 이와 비슷한 기능을 수행하는 플러그인을 찾을 수 없어서 MDX의 기본 구성 요소를 교체하는 방식으로 직접 구현해 봤다.
개발 과정
사용한 라이브러리 버전
Next.js
- 14.2.1
contentlayer
- 0.3.4
개발 환경 설정
프로젝트 환경 설정은 해당 게시글과 동일하다.
Next.js로 MDX 블로그를 만들어보자
이 글에서는 contentlayer
라이브러리를 사용하여 구현하지만,
next-mdx-remote
라이브러리를 사용해도 거의 비슷하게 구현할 수 있다.
Pre.tsx 컴포넌트 생성
먼저, 사용자 정의 MDX 구성 요소로 사용할 Pre.tsx
컴포넌트를 만들어준다.
이 컴포넌트는 코드블럭을 감싸는 역할을 하며, 복사 버튼이 코드블럭 안의 우측 상단에 위치하게 스타일을 적용했다.
// Pre.tsx
interface PreProps {
children?: React.ReactNode;
props?: React.HTMLAttributes<HTMLPreElement>;
}
export const Pre = ({ children, ...props }: PreProps) => {
return (
<div className="relative">
<div className="absolute flex items-center space-x-2 top-0 right-0 m-[11px]">
<button>복사하기</button>
</div>
<pre {...props}>{children}</pre>
</div>
);
};
page.tsx에서 컴포넌트 적용
이제 생성한 Pre.tsx 컴포넌트를 실제 페이지에 적용해야 한다.
page.tsx
파일에서 MDXComponents
를 사용하여 pre
태그를 위에서 만든 Pre
컴포넌트로 교체한다.
// page.tsx
import { allPosts } from "@/.contentlayer/generated";
import { notFound } from "next/navigation";
import { useMDXComponent } from "next-contentlayer/hooks";
import type { MDXComponents } from "mdx/types";
import { Pre } from "./Pre";
export default function Page({ params }: { params: { slug: string } }) {
const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
if (!post) notFound();
const mdxComponents: MDXComponents = {
pre: (props) => <Pre {...props} />,
};
const MDXContent = useMDXComponent(post.body.code);
return (
<article>
<h2>{post.title}</h2>
<time>{post.createdAt}</time>
<MDXContent components={mdxComponents} />
</article>
);
}
복사 기능 구현
다시 Pre.tsx
컴포넌트로 돌아와서 코드블럭의 코드를 복사하는 기능을 구현했다.
useRef
훅을 사용하여 <pre>
요소에 접근하고, 브라우저에서 제공하는 navigator.clipboard.writeText
API를 이용하여
클립보드에 코드블럭의 텍스트를 복사한다.
// Pre.tsx
"use client";
import { useRef } from "react";
interface PreProps {
children?: React.ReactNode;
props?: React.HTMLAttributes<HTMLPreElement>;
}
export const Pre = ({ children, ...props }: PreProps) => {
const preRef = useRef<HTMLPreElement>(null);
const handleCopyText = async () => {
const text = preRef.current?.innerText;
await navigator.clipboard.writeText(text ?? "");
}
return (
<div className="relative">
<div className="absolute flex items-center space-x-2 top-0 right-0 m-[11px]">
<button onClick={handleCopyText}>복사하기</button>
</div>
<pre {...props} ref={preRef}>
{children}
</pre>
</div>
);
};
사용자 피드백 추가
이제 복사 기능은 완성되었지만, 사용자가 복사 버튼을 클릭했을 때 실제로 복사가 되었는지 직관적으로 알 수 없는 문제점이 존재한다. 사용자 경험을 개선하기 위해 복사 성공 여부를 시각적으로 표시하는 기능을 추가했다.
useState
훅을 사용하여 복사 상태를 관리하고,
복사 버튼을 클릭하면 잠시 동안 복사가 완료되었다는 텍스트를 표시하도록 구현했다.
// Pre.tsx
"use client";
import { useRef, useState } from "react";
interface PreProps {
children?: React.ReactNode;
props?: React.HTMLAttributes<HTMLPreElement>;
}
export const Pre = ({ children, ...props }: PreProps) => {
const preRef = useRef<HTMLPreElement>(null);
const [isCopied, setIsCopied] = useState(false);
const handleCopyText = async () => {
const text = preRef.current?.innerText;
await navigator.clipboard.writeText(text ?? "");
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 3000);
};
return (
<div className="relative">
<div className="absolute flex items-center space-x-2 top-0 right-0 m-[11px]">
<button onClick={handleCopyText}>
{isCopied ? "복사완료" : "복사하기"}
</button>
</div>
<pre {...props} ref={preRef}>
{children}
</pre>
</div>
);
};
마치며...
이것으로 Next.js MDX 코드블럭에 복사 버튼을 추가하는 방법을 알아봤다. 이후에는 디자인을 개선하고, 마우스 호버 시에만 버튼을 표시하게 만드는 등의 개선 작업을 거쳐 블로그에 적용해 줬다.