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

Next
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 코드블럭에 복사 버튼을 추가하는 방법을 알아봤다. 이후에는 디자인을 개선하고, 마우스 호버 시에만 버튼을 표시하게 만드는 등의 개선 작업을 거쳐 블로그에 적용해 줬다.

Reference

접기/펼치기

How to add a copy code button to your blog posts
Copy to Clipboard Button In MDX with Next.js and Rehype Pretty Code