Highlight.js로 코드 하이라이팅 + 넘버링 구현
학습 플랫폼에서 사용자의 몰입도를 높이는 가장 중요한 요소 중 하나는 콘텐츠의 '가독성'입니다. 특히 코딩을 배우는 입문자에게 실제 개발 환경과 유사한 경험을 제공하는 것은 학습 효율과 직접적으로 연결된다고 생각했습니다.
이번 글에서는 Coko 프로젝트에서 밋밋한 텍스트에 불과했던 코드 블록을, 어떻게 재사용 가능한 React 커스텀 훅으로 발전시키고, IDE와 같은 생생한 학습 경험을 제공하게 되었는지 그 과정을 공유하고자 합니다.
하이라이팅 구현

기술 스택 선정하기
이 문제를 해결하기 위해 직접 파싱 로직을 구현하는 것은 지나치게 많은 공수가 든다고 판단, 검증된 라이브러리를 활용하기로 했습니다. 여러 후보군 중 저희 팀이 선택한 라이브러리는 Highlight.js였습니다.
- 압도적인 점유율과 레퍼런스: 가장 널리 사용되는 라이브러리 중 하나로, 방대한 커뮤니티와 레퍼런스를 통해 안정성을 확보할 수 있었습니다.
구현 내용
import hljs from 'highlight.js'; import { DependencyList, useState, useLayoutEffect } from 'react'; /** * 주어진 코드 문자열을 하이라이트 처리된 HTML로 변환하는 React 커스텀 훅입니다. * * `highlight.js` 라이브러리를 사용하여 특정 언어의 코드 구문을 강조하고, * 이를 하이라이트 처리된 HTML 문자열로 반환합니다. * 의존성 배열(`deps`)을 통해 재렌더링 조건을 제어할 수 있습니다. * 코드 "문자열" 을 반환하기 때문에 html-react-parser과 xss에 취약한 점을 보완하기 위해 dompurify를 같이 사용하는것을 권장드립니다. * * @param {string} code - 하이라이트 처리할 코드 문자열. * @param {DependencyList} [deps] - 훅 실행을 제어할 의존성 배열. 기본값은 `undefined`이며, 이 경우 `code`와 `language`를 기본 의존성으로 사용합니다. * @param {string} [language='javascript'] - 코드 하이라이트에 사용할 언어. 기본값은 'javascript'입니다. * @returns {string} 하이라이트 처리된 HTML 문자열. * * @example * const code = ` * const greet = (name) => { * console.log(\`Hello, \${name}!\`); * }; * greet('World'); * `; * * const highlightedCode = useCodeHighlight(code, [code], 'javascript'); * * return ( * <pre> * <code> * {parse(dompurify.sanitize(addLineNumberCode), options)} * </code> * </pre> * ); */ const useCodeHighlight = ( code: string, deps?: DependencyList, language: string = 'javascript' ) => { const [highlightCode, setHighlightCode] = useState<string>(''); useLayoutEffect(() => { try { hljs.configure({ ignoreUnescapedHTML: true }); const highlightedCode = hljs.highlight(code, { language }).value; setHighlightCode(highlightedCode); } catch (error) { setHighlightCode(code); } }, deps ?? [code, language]); return highlightCode; }; export default useCodeHighlight;
설계 포인트
-
useLayoutEffect사용:useEffect와 달리,useLayoutEffect는 DOM이 페인팅되기 전에 동기적으로 실행됩니다. 하이라이팅 결과가 화면에 반영되는 과정에서 발생할 수 있는 미세한 깜빡임을 방지하기 위해 이 훅을 선택했습니다. -
유연한 의존성 관리:
deps를 인자로 받아, 훅을 사용하는 컴포넌트가 직접 리하이라이팅 시점을 제어할 수 있도록 설계했습니다. 이를 통해 페이지(state)가 변경될 때마다 필요한 부분만 효율적으로 다시 렌더링할 수 있습니다. -
확장성 고려:
language파라미터를 추가하여, 추후 자바스크립트 외 다른 언어의 코드도 지원해야 하는 기획 변경에 유연하게 대처할 수 있도록 구조를 잡았습니다. -
보안에 대한 경고: 주석에 명시했듯, 이 훅은
HTML문자열을 반환하므로XSS공격에 취약할 수 있습니다. 따라서DOMPurify로 새니타이즈(Sanitize)하고,html-react-parser와 같은 라이브러리로 렌더링하도록 구현했습니다.
사용자를 위한 Line Number 제안
기본적인 하이라이팅 기능 구현 후, 어떻게 하면 사용자가 코드를 더 쉽게 읽을 수 있을까? 라는 고민을 하게 되었습니다. 저는 팀에 코드 옆에 라인 넘버를 추가하는 아이디어를 제안했고, 팀원들의 긍정적인 반응을 얻어 즉시 개발에 착수했습니다.
데이터베이스에 저장된 코드 문자열이 \n (줄바꿈) 문자를 포함하고 있다는 점을 활용하여 간단한 유틸 함수를 작성했습니다.
/** * 코드 문자열의 각 줄에 줄 번호를 추가합니다. * @param {string} code - 줄 번호를 추가할 코드 문자열. * @returns {string} 각 줄에 줄 번호가 추가된 문자열. */ const addLineNumbersToCode = (code: string) => { return code .split('\n') .map((line, i) => `<span>${i + 1} |</span> ${line}`) .join('\n'); }; export default addLineNumbersToCode;
결과물!
두 가지 기능이 결합된 최종 결과물은 학습자의 가독성과 편의성을 크게 향상시켰습니다.
