✍️ 기록/React

[React highlight.js] 사용, 라인 번호, 복사 기능

김물사 2025. 5. 12. 09:58

📌highlight.js 사용

- highlight.js는 코드 구문 강조를 쉽게 구현할 수 있도록 도와주는 JavaScript 라이브러리

 

📍설치

// 🔽 저는 기본 highlight.js 진행
npm install highlight.js

// ✅ 아래와 같은 방법으로 위와 다르게 설치 후 편하게 사용할 수 있습니다.
// 1
npm install react-highlight
// 2 👍
npm install react-syntax-highlighter
// import 
import hljs from 'highlight.js';
// css 테마
import 'highlight.js/styles/default.css';

 

📍테마 변경하기

테마 리스트

테마 Demo 미리보기

 

✅ import 'highlight.js/styles/theme-name.css';

theme-name -> 원하는 테마 이름으로 변경 시 사용할 수 있습니다.

 

📍 사용 방법

import { useEffect, useRef } from "react";
import hljs from "highlight.js";
import 'highlight.js/styles/atom-one-dark.css';

interface HljsPropsType {
  language?: string;
  code?: string;
}

export const Hljs = ({
  language='typescript',
  code='Test'
}:HljsPropsType) => {
  const codeRef = useRef(null);

  useEffect(() => {
    if (codeRef.current) {
      hljs.highlightElement(codeRef.current);
    }
  }, [code, language]);
  return (
    <pre>
      <code ref={codeRef} className={`language-${language}`}>
        {code}
      </code>
    </pre>
  )
}


// EX)
<Hljs 
    code={`
      function Test() {
        console.log('Test')
      }
    `}
  />

✅ 완료!

console을 확인해보니 dataset.highlighted 에러 메시지 안내

Element previously highlighted. To highlight again, first unset `dataset.highlighted`. 

상태 업데이트가 있을 때 useEffect 실행으로 hljs.highlightElement(codeRef.current); 이미 하이라이팅 된 요소에

다시 하이라이팅을 시도하는 문제 발생.

if (codeRef.current) {
  // ✅ highlighted 데이터 속성 초기화
  codeRef.current.dataset.highlighted = '';

  hljs.highlightElement(codeRef.current);
}

 

❗ One of your code blocks includes unescaped HTML. This is a potentially serious security risk.

렌더링하는 코드 문자열 내 HTML 태그가 escape 되지 않아서 발생하는 문제

- <, >, HTML 문자 등 포함된 코드 문자열이 그대로 들어가면 실제 태그로 해석하려 시도 및 렌더링하면 문제 발생

- 렌더링 전 HTML 이스케이프 처리

 

✅ DOMPurify.sanitize() 사용하여 안전하게 이스케이프 처리

🔗 DOMPurify 안전하게 사용하기

// HTML 특수문자 이스케이프
const escapeHtml = (str: string) => {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
};

useEffect(() => {
  if (codeRef.current) {
    // HTML 태그 이스케이프 - 코드 그대로 보이게
    const escaped = escapeHtml(code);
    // XSS 방지 DOMPurify.sanitize
    const sanitizedCode = DOMPurify.sanitize(escaped);
    codeRef.current.innerHTML = sanitizedCode;
    codeRef.current.dataset.highlighted = ''; // highlight.js 초기화
    hljs.highlightElement(codeRef.current);
  }
}, [code, language]);

✅ 더 이상 콘솔에 경고나 에러가 발생하지 않네요!  - DOMPurify.sanitize로 해결 완료!

✅ <, />, 태그를 코드로 그대로 보여주기 위해 escapeHtml <- 유틸 함수로 따로 분리를 하여 재활용하기

<, /> 외 ' " ` 등 입력되는 코드에 따라 더 추가할 수 있어요

 

react-syntax-highlighter 방식으로 진행하게 될 경우 위와 같은 처리 없이 편하게 사용할 수 있는 것 같았어요.

 

📍 필요 언어 수동 등록

import hljs from "highlight.js"; // 기존 전체
🔽 
import hljs from 'highlight.js/lib/core'; // core 일부만 가져오기 위해 변경

// javascript, typescript 등록
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';

hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);

 

📍 Line Number 추가 

interface HljsPropsType {
  language?: string;
  code?: string;
  isLineNumber?: boolean;
}
export const Hljs = ({
  language = 'javascript',
  code = 'Test',
  isLineNumber = true
}: HljsPropsType) => {
  const codeRef = useRef<HTMLElement>(null!);

  // line number
  const lineNumbers = useMemo(() => (
    code.split('\n').map((_, index) => index + 1)
  ),[code]);

  useEffect(() => {
    if (codeRef.current) {
      const sanitizedCode = DOMPurify.sanitize(code);
      codeRef.current.textContent = sanitizedCode;
      codeRef.current.dataset.highlighted = ''; // 데이터 속성 초기화
      hljs.highlightElement(codeRef.current);
    }
  }, [code, language]);

  return (
    <div className="hljs-wrap">
      { isLineNumber && (
        <div className="hljs-numbers">
          {lineNumbers.map(num => (
            <span key={num} className="number">{num}</span>
          ))}
        </div>
      )}
      <pre className="hljs-pre">
        <code ref={codeRef} className={`language-${language}`}>
          {code}
        </code>
      </pre>
    </div>
  );
};

 

✅ lineNumbers \n 기준으로 줄을 나눠 배열을 원하는 구조로 만들어주면 끝!

// CSS
.hljs-wrap {
  display: flex;
  overflow: hidden;
}
.hljs-numbers {
  display: flex;
  flex-direction: column;
  padding:10px;
  user-select: none;
  background-color: #282c34;
  color: #7e7c92;
  border-right: 1px solid #7e7c92;
}
.hljs-pre {
  flex: 1;
  margin:0;
}
.hljs-pre .hljs{ 
  padding:10px;
}

✅ 코드와 line에 번호가 추가 확인 완료!

- line과 code 간격이 맞지 않는 경우 line-height 등 css 수정 필요!

 

📍 복사 기능 

const [copied, setCopied] = useState(false);

// copied click
const handleCopyClick = async () => {
  try {
    await navigator.clipboard.writeText(code);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  } catch (err) {
    console.error('복사 실패:', err);
  }
};

<button 
  type="button" 
  className="copy-button"
  onClick={handleCopyClick}>
  <span>{copied ? '복사 완료' : '복사'}</span>
</button>

 

✅ 복사까지 완료!!

* 저는 복사 버튼 옆에 언어도 같이 보여줘서 어떤 코드인지 한눈에 확인할 수 있도록 했어요 😀

 

highlight.js, 라인 번호, 복사 기능까지 간단히 구현 완료!!

사용자 스타일에 맞게 자유롭게 추가로 해보세요!

 

✍️ 기록

 

감사합니다. 😁

반응형