[React highlight.js] 사용, 라인 번호, 복사 기능
📌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';
📍테마 변경하기
✅ 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() 사용하여 안전하게 이스케이프 처리
// HTML 특수문자 이스케이프
const escapeHtml = (str: string) => {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
};
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, 라인 번호, 복사 기능까지 간단히 구현 완료!!
사용자 스타일에 맞게 자유롭게 추가로 해보세요!
✍️ 기록
감사합니다. 😁