무색
기술블로그
에세이
연구
프로덕트
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

[email protected]

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관
INCHEON, KR
PayloadCMS Lexical 4개 레이어 — 등각 투영 구조
museck 만들기
2026. 2. 1.

PayloadCMS Lexical 에디터에 코드 블록 넣기: 4개 레이어 삽질기

PayloadCMSLexicalNext.jsTypeScriptCMS

블로그에 기술 글을 쓰려면 코드 블록이 필수다. 근데 PayloadCMS 3.x의 Lexical 에디터는 코드 블록을 기본으로 안 준다. 직접 만들어야 한다. Live Preview도 붙이고 싶었고.

결론부터 말하면 8개 커밋 중 6개가 fix:였다. 삽질의 본질은 PayloadCMS Lexical이 가진 4개 레이어 구조를 한번에 파악하기 어렵다는 데 있다.

Live Preview부터 붙여보자

PayloadCMS에는 관리자 패널에서 글을 편집하면서 프론트엔드 미리보기를 실시간으로 볼 수 있는 Live Preview 기능이 있다. payload.config.ts에 설정 몇 줄이면 되는 거라 간단해 보였는데 바로 벽에 부딪혔다.

@payloadcms/live-preview-react 패키지 버전이 Payload 코어 버전과 안 맞으면 런타임에서 터진다. 에러 메시지도 뭐가 문제인지 감을 잡기 힘든 류였다. 3.74.0으로 다운그레이드하니까 해결됐다. 프론트엔드 쪽에는 RefreshRouteOnSave 클라이언트 컴포넌트를 넣어서 저장할 때마다 라우트를 새로고침하게 했다.

Lexical 커스텀 Feature의 4개 레이어

코드 블록 하나 추가하는 건데 생각보다 손이 많이 간다. PayloadCMS의 Lexical 에디터에서 커스텀 기능을 넣으려면 4개 레이어를 전부 건드려야 한다.

1. 서버 Feature: 노드 등록

createServerFeature로 서버 측 Feature를 만든다. 여기서 CodeNode와 CodeHighlightNode를 등록하고 클라이언트 Feature 경로를 지정한다.

const CodeBlockFeature = createServerFeature({
  key: 'codeBlock',
  feature: {
    ClientFeature: './features/codeBlock/client#CodeBlockFeatureClient',
    nodes: [
      createNode({ node: CodeNode }),
      createNode({ node: CodeHighlightNode }),
    ],
  },
})

2. 클라이언트 Feature: 에디터 UI

서버에 노드를 등록했으니 끝인 줄 알았는데 아니다. 에디터에서 실제로 코드 블록을 삽입하려면 클라이언트 Feature가 별도로 필요하다. 슬래시 커맨드(/code)로 코드 블록을 넣을 수 있게 하고 registerCodeHighlighting으로 구문 강조까지 붙였다.

export const CodeBlockFeatureClient = createClientFeature({
  nodes: [CodeNode, CodeHighlightNode],
  plugins: [{ Component: CodeBlockPlugin, position: 'normal' }],
  slashMenu: {
    groups: [{
      items: [{
        key: 'codeBlock',
        keywords: ['code', 'codeblock', '코드'],
        label: () => 'Code Block',
        onSelect: ({ editor }) => {
          editor.update(() => {
            const selection = $getSelection()
            if (selection) $setBlocksType(selection, () => $createCodeNode())
          })
        },
      }],
    }],
  },
})

3. Import Map 갱신

이게 빠뜨리기 쉬운 함정이다. Lexical Feature를 추가하거나 수정하면 반드시 pnpm generate:importmap을 실행해야 한다. 안 하면 에디터가 새로 등록한 노드를 인식 못해서 아무런 에러 없이 그냥 안 된다. 이것 때문에 한참 헤맸다.

4. 시리얼라이저 확장

에디터에서 입력은 됐는데 프론트엔드에서 렌더링이 안 된다? Lexical JSON을 React 컴포넌트로 변환하는 시리얼라이저(serialize-lexical.tsx)에 새 노드 타입을 추가해야 하기 때문이다. code 노드, code-highlight 노드, linebreak 노드를 각각 처리하는 case를 추가했다.

여기서 한 가지 주의할 점. 코드 블록 안의 텍스트는 일반 text 타입이 아니라 code-highlight 타입이다. 시리얼라이저에서 text만 처리하고 있으면 코드 블록 내용이 통째로 사라진다.

hh2 버그 이야기

시리얼라이저를 만들면서 가장 황당했던 버그가 있다. heading 렌더링에서 Lexical이 넘겨주는 node.tag 값이 이미 "h2" 형태인 줄 모르고 앞에 h를 한 번 더 붙였다.

// 버그: "hh2" HTML 태그가 생성됨
const Tag = `h${node.tag}`  // node.tag = "h2" → "hh2"

// 수정: node.tag를 그대로 사용
const Tag = node.tag as keyof React.JSX.IntrinsicElements

브라우저 개발자 도구에서 <hh2>라는 정체불명의 태그를 발견하고서야 원인을 파악했다. Lexical 공식 문서에는 heading 노드의 tag 필드가 어떤 형태인지 명확히 안 나와 있었다. 소스 코드를 직접 열어봐야 알 수 있는 부분이었다.

삽질에서 배운 것

PayloadCMS Lexical 에디터는 확실히 강력하다. Meta의 Lexical 프레임워크를 그대로 쓰기 때문에 확장성이 좋다. 하지만 커스텀 Feature 하나 넣으려면 알아야 할 게 많다.

  • 서버 Feature에서 노드를 등록하고 클라이언트 Feature 경로를 지정해야 한다
  • 클라이언트 Feature에서 에디터 UI(슬래시 커맨드, 플러그인)를 구현해야 한다
  • Feature를 바꿀 때마다 import map을 재생성해야 한다
  • 프론트엔드에서 보여주려면 시리얼라이저에 노드 타입을 추가해야 한다

하나만 빠뜨려도 어딘가에서 조용히 실패한다. 에러 메시지도 친절하지 않다.

이번 작업에서 가장 크게 느낀 건 프레임워크 확장 포인트가 문서에 잘 안 나와 있을 때 소스 코드를 직접 읽는 게 가장 빠르다는 거다. PayloadCMS Lexical 관련 코드는 node_modules/@payloadcms/richtext-lexical 안에 있고 구조가 비교적 깔끔해서 읽을 만하다.

Live Preview까지 붙이고 나니 관리자 패널에서 글 편집하면서 바로 결과를 확인할 수 있게 됐다. 삽질은 길었지만 블로그 운영 워크플로우가 확실히 나아졌다.

자주 묻는 질문

PayloadCMS Lexical 에디터에 코드 블록을 추가하려면?
서버 Feature에 CodeNode를 등록하고, 클라이언트 Feature에 슬래시 커맨드와 구문 강조 플러그인을 구현한 뒤, import map을 재생성하고, 시리얼라이저에 code/code-highlight 노드 타입을 추가해야 한다.
Lexical Feature 추가 후 import map을 재생성해야 하는 이유는?
pnpm generate:importmap을 실행하지 않으면 에디터가 새로 등록한 노드를 인식하지 못한다. 에러 없이 조용히 실패하므로 빠뜨리기 쉬운 함정이다.
PayloadCMS Live Preview 설정 시 주의할 점은?
@payloadcms/live-preview-react 패키지 버전이 Payload 코어 버전과 일치해야 한다. 버전 불일치 시 런타임 에러가 발생하며, 프론트엔드에 RefreshRouteOnSave 클라이언트 컴포넌트를 추가해야 저장 시 미리보기가 갱신된다.
museck 만들기(8/10)
Prev

MCP로 AI 에이전트가 CMS를 직접 조작하게 만든 이야기

Next

CI/CD 빌드가 느려서 3곳을 동시에 고쳤다