Next.js로 Markdown 편집기 만들기

조경민·2024년 5월 3일
0
post-thumbnail

Next.js

Next.js는 2016년에 발표되어 현재 React.js 기반의 풀스택 웹 개발을 이끄는 프레임워크로 주목받고 있습니다. React의 공식 홈페이지에서는 React로 신규 프로젝트를 구축하는 경우, Next.js를 추천하고 있기도 하죠. Vercel에서는 Next.js의 주요 기능을 다음과 같이 소개하고 있습니다.

Built-in Optimizations

다양한 형태의 형태의 데이터에 대하여 최적화를 지원하고 있습니다. 이미지, 동영상과 같은 미디어 에셋 뿐만 아니라 캐싱, 렌더링, 폰트 등 하나의 서비스를 구성하는 여러 요소를 Next.js에서 지원하는 라이브러리를 통해 손쉽게 설정할 수 있습니다.

Dynamic HTML Streaming

동적인 HTML 스트리밍을 지원합니다. API에서 데이터 fetch가 완료되지 않은 시점에서는 세팅해놓은 loading 메세지 혹은 화면이 클라이언트에 표시되었다가 fetch가 완료되면 전체 HTML의 일부분에 대하여 동적으로 렌더링 된 부분만 바꿔치기하여 화면을 갱신합니다. 이러한 기능은 React의 Suspense라는 컴포넌트를 활용한 것이며, 자식 요소에서 사용할 데이터의 fetch가 완료될 때 까지 특정 메세지나 화면을 클라이언트에 표시해줍니다.

React Server Components

Next.js는 기본적으로 서버 컴포넌트 렌더링을 지원합니다. 정적 렌더링, 동적 렌더링, 스트리밍 등의 세 가지 렌더링 전략을 활용할 수 있으며, 클라이언트 단의 불필요한 JavaScript 호출을 줄이고 민감 정보 등이 노출되는 것을 방지할 수 있다는 점에서 여러 이점이 있습니다.

Data Fetching

일반적으로 서버 컴포넌트를 활용한 데이터 fetch를 권장하고 있습니다. 이는 서버와 클라이언트 사이의 불필요한 호출 주고받기를 줄이고 민감 정보를 보다 더 안전하게 관리할 수 있기 때문이죠. UI/UX 향상을 위해 캐싱과 프리로딩 등의 방식을 혼합 적용하여 병렬 데이터 처리를 효과적으로 수행할 수 있도록 구성할 수 있습니다.

실습

이번 실습에서는 Next.js를 활용한 Markdown 편집기를 배포해보겠습니다. React에서 Markdown을 렌더링 할 수 있는 react-markdown 패키지가 사용되었으며, GitHub 스타일의 Markdown을 적용할 수 있는 등 옵션을 적용할 수 있습니다.

버전 정보

  • Node.js v18
  • Next.js v14.2.2
  • React.js v18

준비 사항

GitHub 저장소

실습은 아래 저장소의 Next.js 어플리케이션을 통해 진행됩니다. 저장소를 clone 하거나 fork 해주세요.

따라하기

프로젝트 구조

실습 코드의 Next.js에서는 App Router 방식을 활용하여, /app 디렉토리 하위에 있는 React 코드를 렌더링하게 됩니다. 주요 파일로는 layout.jspage.js가 있으며, 파일의 내용은 다음과 같습니다.

  • layout.js

    import { Inter } from "next/font/google";
    import "./globals.css";
    
    const inter = Inter({ subsets: ["latin"] });
    
    export const metadata = {
      title: "Markdown Editor",
      description: "Next.js Markdown Editor",
    };
    
    export default function RootLayout({ children }) {
      return (
        <html lang="ko">
          <body className={inter.className}>{children}</body>
        </html>
      );
    }

    /app 디렉토리에서 UI를 구성하는 최상위 레이아웃을 나타내는 파일입니다. 일반적인 HTML 파일의 <head> 태그에서 정의하는 메타데이터를 metadata 라는 이름의 변수에 대입하여 적용할 수 있습니다.
    RootLayout 함수는 전역적으로 적용되는 <html><body> 태그를 반환하며, 생략할 수 없는 필수요소입니다. 필요에 따라 /app 디렉토리 하위에 다른 디렉토리를 만들어 layout.js 파일을 만들어 페이지를 구분할 수 있습니다.

  • page.js

    'use client';
    
    import { useState, useRef } from 'react';
    import ReactMarkdown from 'react-markdown';
    import remarkGfm from "remark-gfm";
    import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
    import { materialLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
    
    export default function Home() {
      const [markdown, setMarkdown] = useState(`
      # Hello, Cloudtype!
    
      \`\`\`javascript
      function sum(a, b) {
        return a + b;
      }
      \`\`\`
      `);
    
      const contentRef = useRef(null);
    
      const handleChange = (event) => {
        setMarkdown(event.target.value);
      };
    
      return (
        <div className="flex h-screen font-sans">
          <div className="flex-1 p-6">
            <textarea
              value={markdown}
              onChange={handleChange}
              className="w-full h-full resize-none border-2 border-gray-300 rounded-lg p-4 text-lg leading-normal focus:outline-none focus:border-blue-500"
            />
          </div>
          <div className="flex-1 p-6 overflow-auto border-l border-gray-300">
            <div ref={contentRef} className='markdown'>
              <ReactMarkdown
                remarkPlugins={[remarkGfm]}
                components={{
                  code({ node, inline, className, children, ...props }) {
                    const match = /language-(\w+)/.exec(className || '');
                    return !inline && match ? (
                      <SyntaxHighlighter
                        style={materialLight}
                        language={match[1]}
                        PreTag="div"
                        children={String(children).replace(/\n$/, '')}
                        {...props}
                      />
                    ) : (
                      <code className={className} {...props}>
                        {children}
                      </code>
                    );
                  }
                }}
              >
                {markdown}
              </ReactMarkdown>
            </div>
          </div>
        </div>
      );
    }          

'use client' 는 Next.js에서 Client Components를 활용할 때에 파일 최상위(import 문의 윗부분)에 선언하는 디렉티브입니다. 클라이언트 단에서 일어나는 이벤트 리스너나 각종 상태값을 관리/조작하기 위해서는 Client Components를 사용해야 하죠. Next.js는 기본적으로 Server Components를 사용하기 때문에 Markdown 에디터에서 useState(), useRef(), onChange 등을 활용하기 위해서는 반드시 디렉티브를 설정해야 합니다.

Next.js 서비스 배포

  1. 클라우드타입에 로그인 후 우측 네비바의 ➕ 버튼을 눌러 새 프로젝트 창을 띄우고 프로젝트 이름과 표시 이름을 입력한 뒤 생성하기 버튼을 누릅니다.

  2. package.json 파일은 다음과 같습니다.

    {
      "name": "nextjs-markdown",
      "version": "0.1.0",
      "private": true,
      "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
      },
      "dependencies": {
        "file-saver": "^2.0.5",
        "next": "14.2.2",
        "react": "^18",
        "react-dom": "^18",
        "react-markdown": "^9.0.1",
        "react-syntax-highlighter": "^15.5.0",
        "remark-gfm": "^4.0.0"
      },
      "devDependencies": {
        "@tailwindcss/typography": "^0.5.12",
        "postcss": "^8",
        "tailwindcss": "^3.4.1"
      }
    }

    scripts 에 Next.js 프로젝트를 빌드 및 실행하기 위한 명령어가 정의되어 있습니다.

  1. 클라우드타입의 프로젝트 페이지에서 ➕ 버튼을 누르고 Next.js를 선택한 후, 미리 fork 해놓은 nextjs-markdown 를 선택합니다. 기타 설정은 아래를 참고하여 입력한 후 배포하기 버튼을 클릭합니다.

    • 버전: v18
    • Build command: npm run build 혹은 yarn build
  2. 배포가 완료되면 Next.js 어플리케이션 페이지의 연결 탭에서 https:// 로 시작되는 URL을 통해 서비스에 접속이 가능합니다.

  1. URL로 접속하면 다음과 같이 좌측의 작성부, 우측의 렌더링부로 나뉘어진 페이지를 확인할 수 있습니다.

References

profile
Live And Let Live!

0개의 댓글