Open Source

Markdown으로 변환 시 Link의 Tag가 잘못 배치되는 이유 분석하기

2026년 1월 18일

Lexical의 Markdown 변환 과정에서 Link 노드의 위치에 따라 Bold/Italic 태그가 잘못 적용되는 문제를 분석하고 해결한 과정을 공유합니다.

문제 상황

Link가 텍스트의 처음이나 마지막에 존재할 때, Markdown 변환 시 태그가 잘못 생성되거나 누락되는 현상이 발생했습니다.

1. Link가 처음에 존재하는 경우

  1. 텍스트 "link text" 입력
  2. "link"에 Link 삽입 및 전체 텍스트에 Bold 또는 Italic 적용
  3. Markdown 변환 결과: [**link](https://) text**
  4. Markdown을 다시 텍스트로 변환 시: **link text**

Markdown 변환 과정에서 "link" 텍스트에 Bold나 Italic 태그가 Link 구문 내부에 포함되는 문제가 발생합니다.

2. Link가 마지막에 존재하는 경우

  1. 텍스트 "text link" 입력
  2. "link"에 Link 삽입 및 전체 텍스트에 Bold 또는 Italic 적용
  3. Markdown 변환 결과: **text [link](https://)
  4. Markdown을 다시 텍스트로 변환 시: **text link

Markdown 변환 과정에서 마지막에 위치한 Link 뒤에 닫는 Bold나 Italic 태그가 누락되는 문제가 발생합니다.


원인 분석

디버깅

문제 1번 (Link가 처음에 있는 경우)

구조가 다음과 같을 때:

1. ParagraphNode
	- child: {
		1. LinkNode
			- url: "https://"
			- child: {
				1. TextNode
					- text: "link"
					- format: bold, italic
			}
		2. TextNode
			- text: "text"
			- format: bold, italic
		}
	- format: bold, italic
  1. exportChildren(ParagraphNode) 실행
  2. LinkNode 처리:
    • LinkNode.export() -> 내부 TextNode("link")에 대해 exportTextFormat 실행
    • exportTextFormat**link 문자열 생성 (포맷 적용)
    • Link 구문 생성: [**link](https://)
  3. TextNode("text") 처리:
    • exportTextFormat 실행 -> text** 문자열 생성
  4. 결과: [**link](https://) text**

Link의 텍스트에 포맷을 적용하지만, 부모의 형제 텍스트 노드에도 같은 포맷이 존재하여 태그를 닫지 않고 넘어갑니다. Link의 텍스트에서 포맷을 열고 조부모의 형제 노드에서 포맷을 닫게 되면서 구문 안으로 태그가 들어가게 됩니다.

문제 2번 (Link가 마지막에 있는 경우)

구조가 다음과 같을 때:

1. ParagraphNode
	- child: {
		1. TextNode
			- text: "text"
			- format: bold, italic
		2. LinkNode
			- url: "https://"
			- child: {
				1. TextNode
					- text: "link"
					- format: bold, italic
			}
		}
  1. exportChildren(ParagraphNode) 실행
  2. TextNode("text") 처리:
    • exportTextFormat 실행 -> **text 문자열 생성 (태그 엶)
  3. LinkNode 처리:
    • LinkNode.export() 내부에서 [link](https://) 문자열 생성
  4. 결과: **text [link](https://) (닫는 태그 누락)

Link의 export 과정에서 자식 텍스트를 처리할 때, 이전에 열린 태그가 unclosableTags 목록에 추가되어 전달됩니다. 이로 인해 Link 내부 텍스트에서는 해당 태그를 닫을 수 없게 되어 포맷 적용이 무시됩니다. 또한, 포맷을 적용하는 exportTextFormat 함수는 Text 노드에만 포맷을 적용하기에, Link 노드에 포맷을 처리하지 않아 닫는 태그가 처리되지 않게 됩니다.


해결 방법

여러 해결 방안 중, 코드의 복잡성을 줄이고 직관적인 해결을 위해 getTextSibling 함수를 수정했습니다.

기존 코드는 Markdown 결과물의 간결성(최적화)을 위해 인라인 요소(Link 등)의 텍스트 형제 노드까지 복잡하게 탐색하여 컨텍스트를 판단하려 했으나, 이로 인해 오히려 포맷 적용 범위 판단에 오류가 있었습니다. 결과물의 태그가 다소 중복되더라도 정확한 변환을 보장하기 위해 형제 노드가 텍스트 노드인 경우만 단순하게 확인하도록 변경했습니다.

변경된 코드

// (수정 전) 복잡한 형제 노드 탐색 로직
function getTextSibling(node: TextNode, backward: boolean): TextNode | null {
  let sibling = backward ? node.getPreviousSibling() : node.getNextSibling();

  if (!sibling) {
    const parent = node.getParentOrThrow();
    if (parent.isInline()) {
      sibling = backward
        ? parent.getPreviousSibling()
        : parent.getNextSibling();
    }
  }
  // ... (중략: 깊은 탐색 로직) ...
  return null;
}
// (수정 후) 단순화된 로직
function getTextSibling(node: TextNode, backward: boolean): TextNode | null {
  const sibling = backward ? node.getPreviousSibling() : node.getNextSibling();

  if ($isTextNode(sibling)) {
    return sibling;
  }

  return null;
}

이렇게 변경함으로써 exportTextFormat이 현재 텍스트 노드의 포맷팅을 결정할 때, 인접한 텍스트 노드와의 관계만 명확하게 판단하게 되어 Link 내외부의 태그가 꼬이는 문제를 해결할 수 있었습니다.


느낀점

이전 문제를 해결할 때는 함수 하나만 분석해서 해결해도 되었지만, 이번에는 여러 함수가 얽혀있어 전체적인 흐름을 파악하고 디버깅하는 데 어려움이 있었습니다.

기존 코드에서 Link가 앞뒤에 있을 때 각각 발생하는 원인이 달라 서로 다른 방법으로 해결해야 한다고 생각했지만, 결국 공통된 방법으로 여러 문제를 해결할 수 있었습니다. 특히 기존의 최적화 로직을 제거하고 구조를 단순하게 만들어야 했기에, 분석 결과를 바탕으로 리뷰어를 설득하는 과정이 중요했습니다.

영어로 논리적인 근거를 작성해야 했어서 성공적으로 통과할 수 있을까 걱정도 되었지만, 다행히 리뷰어를 잘 설득하여 별다른 코멘트 없이 머지될 수 있었습니다.