Never give up

React - code editor 3(feat. Prism.js) 본문

WEB

React - code editor 3(feat. Prism.js)

대기만성 개발자 2023. 12. 15. 16:47
반응형

마지막으로 코드에디터가 그냥 textarea에 밋밋한 text를 그대로 사용할 수는 없으니

 

higlighting을 어떻게 처리하는지 확인해볼텐데

 

일단 옵션이 여러가지가 있는거 같았습니다

 

필자가 사용한 Prism.js

https://www.npmjs.com/package/prismjs

 

그리고 Highlight.js

https://www.npmjs.com/package/highlight.js

 

기타 등등...

 

뭐 사용방법은 크게 다르지 않으나 필자는 Prism.js를 사용했는데

 

이유는 조금더 작은 번들 크기, 더 많은 다운로드수, 기타 등등인데

 

어떤것을 사용하셔도 무방합니다

 

이번 예제는 조금 tricky한 작업이 있다보니 자료찾는데 고생을 조금했습니다

 

먼저 참고한 자료를 공유해드리자면

 

https://css-tricks.com/creating-an-editable-textarea-that-supports-syntax-highlighted-code/

 

Creating an Editable Textarea That Supports Syntax-Highlighted Code | CSS-Tricks

When I was working on a project that needed an editor component for source code, I really wanted a way to have that editor highlight the syntax that is typed.

css-tricks.com

 

해당 예제에서 textarea 태그에 어떻게 적용했는지 하나하나 보면서 적용을 해나갔습니다

 

간단하게 말씀드리자면

 

1. 껍데기(?)를 넣는데

2. textarea, pre 태그를 같은 위치에 위치시킨다

3. position, font size, color등을 잘(?) 설정한다

4. 투명 textarea를 pre위에 덮는다

 

인데 코드를 보면 확실하게 이해가 될것입니다

 

src/components/Code/index.tsx

type CodeProps = {
  text: string;
  setText: React.Dispatch<React.SetStateAction<string>>;
};

const Code = ({ text, setText }: CodeProps) => {
  const { textareaRef, preRef, getCode, onKeyDown, onScroll } = useCodeAera(
    text,
    setText
  );

  return (
    <div className={styles.div_code}>
      <div>index.jsx</div>
      <textarea
        ref={textareaRef}
        onKeyDown={onKeyDown}
        value={text}
        spellCheck={false}
        autoComplete="false"
        onScroll={onScroll}
        onChange={(e) => setText(e.target.value)}
      />
      <pre ref={preRef}>
        <code className="language-jsx" dangerouslySetInnerHTML={getCode()} />
      </pre>
    </div>
  );
};

export default Code;

먼저 props로 text와 setText를 같이 받는건

 

앞에 포스트2개에서 공통으로 사용하려고 만들어 놓은 컴포넌트여서 그렇습니다

 

컴포넌트 내부 로직은 useCodeArea에 들어있으니 나중에 보도록 하고

 

textarea 태그와 pre 태그에 어떤식으로 스타일을 적용하는지 보도록 하겠습니다

 

src/components/Code/code.module.css

.div_code {
  width: 60%;
  height: 100%;
  position: relative;
}

.div_code > div {
  padding: 2px 0 2px 8px;
  background-color: grey;
  color: white;
}

.div_code > textarea,
.div_code > pre {
  position: absolute;
  height: calc(100% - 30px);
  width: 100%;
  margin: 0;
  padding: 10px;
  border: unset;
  border-radius: unset;
}

.div_code > textarea,
.div_code > pre,
.div_code > pre * {
  font-size: 15px;
  line-height: 1.5;
  font-family: monospace;
  tab-size: 2;
  white-space: pre;
}

.div_code > textarea {
  color: transparent;
  background-color: transparent;
  z-index: 1;
  color: transparent;
  caret-color: white;
  display: block;
  resize: none;
}

.div_code > pre {
  z-index: 0;
}

div_code로 되어있는 div 태그 아래에 동일한 위치로 셋팅하기위해

 

position relative, absolute를 적극활용하고...

 

폰트 크기, 라인 넓이, 폰트 종류, 탭 사이즈 등등 동일하게 설정을 해주고

 

textarea를 완전히 투명하게 만들고 z index를 활용해서 pre태그 위에 넣습니다

 

처음에 작업할 때, 동일하게 했는데 왜 차이가 생기나 봤더니

 

폰트 종류가 달라서 발생했던것을 캐치를 못해서 시간낭비 잔뜩했습니다 하하...

(여전히 잘못된 삽질중인 필자...)

 

다음으로 로직처리하는 부분을 보면

 

src/hooks/useCodeArea.ts

const useCodeAera = (
  text: string,
  setText: React.Dispatch<React.SetStateAction<string>>
) => {
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const preRef = useRef<HTMLPreElement>(null);

  useEffect(() => {
    Prism.highlightAll();
  }, [text]);

  /** - tab key and string quote feature */
  const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.code === "Tab") {
      handleTab(e);
    }

    if (e.code === "Quote") {
      handleQuote(e);
    }
  };

  /** - add tab and move cursor */
  const handleTab = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    const { start, end, hasStartEnd } = checkTarget(e);

    if (hasStartEnd) {
      const value = text.substring(0, start) + "\t" + text.substring(end);

      setText(value);

      resetCursur(start);
    }
  };

  /** - add quote string and move cursur */
  const handleQuote = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    const { start, end, hasStartEnd } = checkTarget(e);

    if (hasStartEnd) {
      e.preventDefault();
      const selected = text.substring(start, end);

      const startText = text.substring(0, start);
      const endText = text.substring(end);

      const shift = e.shiftKey;

      const quote = shift ? '"' : "'";

      const value =
        selected.startsWith(quote) && selected.endsWith(quote)
          ? startText + selected.replaceAll(quote, "") + endText
          : startText + quote + `${selected}` + quote + endText;

      setText(value);

      resetCursur(start);
    }
  };

  /** - target text start, end location */
  const checkTarget = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    e.preventDefault();
    const target = e.currentTarget;
    const start = target.selectionStart;
    const end = target.selectionEnd;

    const hasStartEnd = start !== null && end !== null;

    return { start, end, hasStartEnd };
  };

  /** - move to changed cursur location */
  const resetCursur = (start: number) => {
    const timeOut = setTimeout(() => {
      textareaRef.current!.setSelectionRange(start + 1, start + 1);

      clearTimeout(timeOut);
    }, 100);
  };

  /** - synchronize text editor and prism */
  const onScroll = (e: UIEvent<HTMLTextAreaElement>) => {
    const target = e.currentTarget;

    preRef.current!.scrollTop = target.scrollTop;
    preRef.current!.scrollLeft = target.scrollLeft;
  };

  /** - js string to jsx string */
  const getCode = () => {
    const __html = text
      .replace(new RegExp("&", "g"), "&amp;")
      .replace(new RegExp("<", "g"), "&lt;");

    return { __html };
  };

  return { textareaRef, preRef, getCode, onKeyDown, onScroll };
};

export default useCodeAera;

각각 함수를 하나씩 보자면

 

1. onKeyDown, handleTab, handleQuote은 탭이랑 문자열 처리할 때

 

키보드 이벤트 한번 줘보려고 테스트용도로 만들어봤습니다

 

텍스트를 어디서부터 어디까지 자르고 붙이고, 커서 위치 바꿔주고.. 등등 간단한 작업만 해봤는데

 

코드 에디터 만드시는분들 참 대단한거 같다는 느낌을 받았습니다..

 

IDE나 코드 에디터를 만들어주신 갓 개발자님들께 다시한번 감사드립니다

 

2. checkTarget, resetCursor은 커서 위치나 텍스트가 드래그 되었을 때

 

해당 위치를 찾아주고 변경 후 cursor 위치를 변경해 줄 때 사용되는데

 

위에 keyboard event 발생 시 커서 위치를 처리하기 위해 만들어봤습니다

 

3. onScroll은 textarea 태그와 pre 태그의 위치는 같지만 scroll은 별도로 동기를 시켜줘야돼서

 

textarea의 scroll event 발생시 pre의 scroll을 변경해주는 부분입니다

 

4. getCode는 특수문자 처리하는 경우가 있는데

 

일반적인 JS는 코드내에 직접적으로 태그를 사용하지 않아서

 

code higlighter에서 <, &과 같은 문자는 별도로 처리를 해줘야됩니다

 

마지막으로 결과물을 보면

 

< 작업 완료된 에디터 >

 

전체 소스 링크 : https://github.com/devmemory/react_code_editor

 

component로 구현 : https://devmemory.tistory.com/121

iframe으로 구현 : https://devmemory.tistory.com/122

반응형

'WEB' 카테고리의 다른 글

React - code editor 2  (0) 2023.12.15
React - code editor 1(feat. babel standalone)  (0) 2023.12.13
React - localization without library  (2) 2023.11.18
React - image resize example  (0) 2023.07.16
React, Node - SSE example  (0) 2023.03.04
Comments