-
Notifications
You must be signed in to change notification settings - Fork 2
코드 하이라이팅
부스트 캠프 그룹 프로젝트로 코드를 작성하고 코드 리뷰를 할 수 있는 플랫폼을 개발하고 있다.
가독성이 중요한 플랫폼인데 하이라이팅이 안 된 코드는 가독성이 좋지 않다.
따라서 코드 하이라이팅을 개발해야 했는데 실제로 모든 언어의 하이라이팅을 분기 처리해서 개발하기에는 일정이 빠듯한 관계로 highlight.js라는 라이브러리를 이용하였다.
이 글은 그 과정에서 겪었던 어려움과 해결 과정, 해결한 코드를 포함한다.
현재 개발 중인 서비스는 프로그래밍 언어를 가리지 않고 많은 사용자들이 참가할 수 있게 하기 위해 다양한 종류의 프로그래밍 언어를 지원해야 한다. 필자는 다양한 언어의 의미 있는 토큰 (function
, class
, this
등등)을 알고 있지 못해서 공부해서 언어별로 만들기에는 시간이 굉장히 오래 걸릴 것이고 확실히 그 토큰들의 특성을 이해하고 개발하지 못해 사용자들이 불편함을 느낄 수 있다.
<textarea
value={code}
onChange={changeCode}
className="code-editor__textarea"
autoComplete="false"
spellCheck="false"
/>
textarea나 input은 하나의 html 태그고 이 내부에 있는 각각의 의미 있는 토큰(ex: function
, #include
, console
, class
)을 각각 다르게 스타일링할 수 없다.
div 등의 글쓰기 기능이 없는 태그들도 contentEditable 속성을 이용하면 textarea나 input 태그처럼 글자는 입력할 수 있지만 하나의 태그 내부에서 모든 토큰들을 다르게 스타일링하는 것은 불가능하다.
highlight.js에서 제공해주는 API인 highlight 메서드를 텍스트에 적용하면 html 태그로 바꾸어준다.
hljs.highlight("function(){\n console.log('hi');\n}", { language: "javascript" }).value;
위 코드의 반환 값은 이런 모양의 HTML이다.
<span class="hljs-keyword">function</span>(<span class="hljs-params"></span>){
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">'hi'</span>);
}
즉 이런 HTML을 사용자가 코드를 입력할 때 만들어 사용자에게 보여주면 된다.
레이아웃을 살펴보자.
return (
<div className="code-editor">
<textarea
value={code}
onChange={changeCode}
className="code-editor__textarea"
autoComplete="false"
spellCheck="false"
/>
<pre className="code-editor__present">
<code
dangerouslySetInnerHTML={createMarkUpCode(highlightedHTML)}
></code>
</pre>
</div>
);
리턴을 살펴보면 div로 감싸진 textarea와 pre가 있다.
필자는 같은 크기의 textarea와 pre를 div내에 만들고 내부에 있는 code에 highlight.js가 만들어준 html을 innerHTML
하여 textarea의 각각의 코드들에 스타일을 입힐 수 없는 문제를 해결했다.
해당 레이아웃의 style은 다음과 같다.
.code-editor{
position: relative;
width: 100%;
height: 90%;
border-radius: 0.25rem;
&__textarea, &__present {
color: $weview-white;
font-size: 1.25rem;
position: absolute;
margin: 0;
width: calc(100% - 1em);
height: calc(100% - 1em);
padding: 0.5em;
}
&__textarea {
caret-color: $alert;
color: transparent;
background-color: transparent;
z-index: 1;
border: none;
resize: none;
}
&__present{
background-color: $codeblock-color;
color: #c9d1d9;
z-index: 0;
border-radius: 0.25rem;
text-overflow: ellipsis;
}
}
.code-editor
에 position: relative;
를 주고 자식 태그인 textarea와 pre태그에 position: absolute
를 적용하여 레이아웃을 맞추고 width
와 height
는 아래 패딩에 맞추어 설정한다.
이후 textarea에만 z-index
를 높여 클릭 시 textarea가 클릭되도록 적용하고 color
와 background-color
속성을 transparent
로 적용하여 textarea 태그에 있는 배경과 글씨는 보이지 않도록 한다.
필자의 경우에는 github-dark 테마를 적용했는데 이 스타일은 highlight.js Github에서 가져올 수 있다.
위 스타일을 복사한다음에 스타일이 적용되는 .css 혹은 .scss 혹은 styled-component등에 붙여넣기 하면 적용시킬 수 있다.
highlight가 만들어주는 태그의 class 이름 별로 styling이 돼있기 때문이다.
github-dark 테마 외에 다른 테마를 적용하고 싶다면 highlight.js 깃헙에 src/styles 폴더 내부에서 선택할 수 있다.
이제 전체 코드를 살펴보며 react component 내부에서 어떤 식의 흐름으로 동작하는지 살펴보자.
import React, { ChangeEvent, useCallback, useEffect, useState } from "react";
import useWritingStore from "@/store/useWritingStore";
import hljs from "highlight.js";
const CodeEditor = (): JSX.Element => {
const [highlightedHTML, setHighlightedCode] = useState("");
const { code, setCode, language } = useWritingStore((state) => ({
code: state.code,
setCode: state.setCode,
language: state.language,
}));
useEffect(() => {
setHighlightedCode(
hljs.highlight(code, { language }).value.replace(/" "/g, " ")
);
}, [code]);
const changeCode = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setCode(e.target.value);
}, []);
const createMarkUpCode = useCallback(
(code: string): { __html: string } => ({
__html: code,
}),
[code]
);
return (
<div className="code-editor">
<textarea
value={code}
onChange={changeCode}
className="code-editor__textarea"
autoComplete="false"
spellCheck="false"
/>
<pre className="code-editor__present">
<code
dangerouslySetInnerHTML={createMarkUpCode(highlightedHTML)}
></code>
</pre>
</div>
);
};
export default CodeEditor;
아래 코드에서 code
, language
는 전역 상태여서 전역 store에서 가져오고 setCode
라는 action을 가져온다.
const { code, setCode, language } = useWritingStore((state) => ({
code: state.code,
setCode: state.setCode,
language: state.language,
}));
전역 상태 관리가 필요 없을 때는 useState
를 사용하면 된다.
const [code, setCode] = useState("");
이후 highlight.js를 import해오기 위해 highlight.js를 install 해주었다.
npm i highlight.js
이후 useEffect
를 사용하여 code
가 변하는 동시에 하이라이팅을 시켜준다.
useEffect(() => {
setHighlightedCode(
hljs.highlight(code, { language }).value.replace(/" "/g, " ")
);
}, [code]);
textarea의 입력이 바뀌는 이벤트가 일어나면 setCode
로 code
를 변화시켜 하이라이팅 이벤트가 동작하도록 한다.
const changeCode = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setCode(e.target.value);
}, []);
react는 dangerouslySetInnerHTML메서드로 innerHTML을 사용할 때에는 xss 공격에 쉽게 노출될 수 있다는 걸 상기할 수 있도록 한다.
지금의 경우에는 highlight.js에서 <
를 <
>
를 >
로 변환해주기 때문에 <script>
의 삽입은 불가하다.
위험한 것은 서버로 가는 요청인데 필자의 서비스에서는 서버로 가는 요청은 따로 한번 걸러준다.
혹시 이 글을 보며 이 로직을 사용하실 거라면 서버로 요청이 가기 전에 한 번의 검증 로직을 추가해주세요!
const createMarkUpCode = useCallback(
(code: string): { __html: string } => ({
__html: code,
}),
[code]
);
그리고 아까 확인한 return문은 그대로이다.
return (
<div className="code-editor">
<textarea
value={code}
onChange={changeCode}
className="code-editor__textarea"
autoComplete="false"
spellCheck="false"
/>
<pre className="code-editor__present">
<code
dangerouslySetInnerHTML={createMarkUpCode(highlightedHTML)}
></code>
</pre>
</div>
);
프로젝트를 진행하며 재밌는 문제를 마주해서 글을 업로드해본다.
서비스 이름은 WeView이고 코드 리뷰의 어려움, 막막함, 내 코드 읽어줄 사람이 없음 등의 문제를 재밌게 풀어보려고 한다.
위 코드도 서비스에 첨부돼있으니 해당 링크에서 확인해도 좋을 것 같다.
현재 에디터에 재밌는 기능들이 추가될 것 같다.
또 재밌는 문제를 발견하면 해결하는 글을 작성해 보려 한다.