React Markdown에서 특수문자로 인해 마크다운 형식이 제대로 적용되지 않는 문제
서버에서 메시지를 받을 때 특수문자를 이스케이프 처리하는 유틸리티 함수를 만들어 사용합니다:
// utils/markdownHelper.js
export const escapeSpecialChars = (text) => {
if (!text) return '';
// 마크다운 특수문자들을 이스케이프 처리
const specialChars = {
'*': '\\\\*',
'_': '\\\\_',
'`': '\\\\`',
'#': '\\\\#',
'+': '\\\\+',
'-': '\\\\-',
'.': '\\\\.',
'!': '\\\\!',
'[': '\\\\[',
']': '\\\\]',
'(': '\\\\(',
')': '\\\\)',
'{': '\\\\{',
'}': '\\\\}',
'|': '\\\\|',
'>': '\\\\>',
'<': '\\\\<'
};
// 코드 블록 내부는 이스케이프하지 않도록 처리
const codeBlockRegex = /```[\\s\\S]*?```|`[^`]+`/g;
const codeBlocks = [];
let processedText = text;
// 코드 블록을 임시로 치환
processedText = processedText.replace(codeBlockRegex, (match) => {
codeBlocks.push(match);
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
});
// 특수문자 이스케이프
Object.entries(specialChars).forEach(([char, escaped]) => {
const regex = new RegExp(`\\\\${char}`, 'g');
processedText = processedText.replace(regex, escaped);
});
// 코드 블록 복원
codeBlocks.forEach((block, index) => {
processedText = processedText.replace(`__CODE_BLOCK_${index}__`, block);
});
return processedText;
};
MessageItem.jsx 파일을 수정하여 react-markdown에 추가 설정을 적용합니다:
import React from 'react'
import styles from './MessageItem.module.css'
import NoaIcon from '../../assets/noamessageicon.svg'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import PlanCardGroup from './PlanCard'
const MessageItem = ({ message, formatTime }) => {
if (message.role === 'card') {
return <PlanCardGroup plans={message.content} />
}
const messageClasses = [
styles.message,
styles[message.role],
message.isStreaming ? styles.streaming : '',
message.isTemp ? styles.temp : '',
].join(' ')
// 마크다운 컴포넌트 커스터마이징
const markdownComponents = {
// 줄바꿈 처리
p: ({children}) => <p className="mb-2">{children}</p>,
// 코드 블록 스타일링
code: ({inline, children}) => {
if (inline) {
return <code className="bg-gray-100 px-1 py-0.5 rounded text-sm">{children}</code>
}
return (
<pre className="bg-gray-100 p-3 rounded overflow-x-auto">
<code>{children}</code>
</pre>
)
},
// 리스트 스타일링
ul: ({children}) => <ul className="list-disc list-inside mb-2">{children}</ul>,
ol: ({children}) => <ol className="list-decimal list-inside mb-2">{children}</ol>,
// 링크 스타일링
a: ({href, children}) => (
<a href={href} target="_blank" rel="noopener noreferrer"
className="text-blue-500 hover:underline">
{children}
</a>
),
}
return (
<div className={styles.messageWrapper} data-role={message.role}>
{message.role === 'assistant' && (
<div className={styles.avatarContainer}>
<img src={NoaIcon} alt="노아 아이콘" className={styles.avatarIcon} />
<span className={styles.noaText}>NOA</span>
</div>
)}
<div className={messageClasses}>
<div className={styles.messageContent}>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
components={markdownComponents}
skipHtml={false}
allowedElements={undefined} // 모든 HTML 요소 허용
>
{message.content}
</ReactMarkdown>
</div>
<div className={styles.messageTime}>
{formatTime(message.timestamp)}
{message.isTemp && <span className={styles.tempIndicator}>전송중</span>}
</div>
</div>
</div>
)
}
export default MessageItem
{
"dependencies": {
// 기존 의존성...
"remark-gfm": "^4.0.0",
"remark-breaks": "^4.0.0"
}
}
useChat.js에서 메시지를 받을 때 전처리를 추가합니다:
// useChat.js에 추가
const preprocessMessage = (content) => {
if (!content) return '';
// 특수문자 앞뒤에 공백이 없어서 마크다운이 적용되지 않는 경우 처리
let processed = content;
// 볼드(**)나 이탤릭(*) 처리
processed = processed.replace(/(\\S)\\*\\*(\\S)/g, '$1 **$2');
processed = processed.replace(/(\\S)\\*(\\S)/g, '$1 *$2');
// 코드 블록(```) 처리
processed = processed.replace(/(\\S)```/g, '$1\\n```');
processed = processed.replace(/```(\\S)/g, '```\\n$1');
// 인라인 코드(`) 처리
processed = processed.replace(/(\\S)`(\\S)/g, '$1 `$2');
return processed;
};
// handleStreamChunk 함수 수정
const handleStreamChunk = chunk => {
if (chunk && streamingMessageIdRef.current) {
setMessages(prev =>
prev.map(msg =>
msg.id === streamingMessageIdRef.current
? { ...msg, content: preprocessMessage(msg.content + chunk) }
: msg
)
)
}
}
/* 마크다운 콘텐츠 스타일링 */
.messageContent {
word-break: break-word;
white-space: pre-wrap;
}
.messageContent h1,
.messageContent h2,
.messageContent h3 {
margin-top: 1em;
margin-bottom: 0.5em;
font-weight: bold;
}
.messageContent h1 { font-size: 1.5em; }
.messageContent h2 { font-size: 1.3em; }
.messageContent h3 { font-size: 1.1em; }
.messageContent blockquote {
border-left: 4px solid #ddd;
padding-left: 1em;
margin: 0.5em 0;
color: #666;
}
.messageContent table {
border-collapse: collapse;
margin: 0.5em 0;
}
.messageContent th,
.messageContent td {
border: 1px solid #ddd;
padding: 0.5em;
}