[React] xTerm.js로 터미널 화면 구현

wldud·2024년 10월 7일

React

목록 보기
4/8
post-thumbnail

웹 IDE 프로젝트를 하면서 터미널화면을 구현했어야 했다. 간단하게 입력값을 넘겨주고 출력값을 벡엔드에서 받아올거라서 react-terminal로 구현했다. 정말 디자인도 마음에 들었는데..입력한 값을 뽑아낼 수가 없었다..여기저기 찾아봐서 해봤지만 계속 안되었다. 그래서 xTerm 라이브러리로 바꾸었다. react-terminal보다는 사용하기 어려웠지만 그래도 괜찮았다.

먼저 사용하기 위해서 아래코드로 xterm과 xterm-addon-fit을 설치해준다.
xter-addon-fit은 터미널의 크기가 안맞아서 컨테이너 크기에 터미널 크기를 자동으로 맞춰주는 애드온이다.

npm install xterm xterm-addon-fit

설치했으면 아래코드처럼 사용해주고 이 컴포넌트를 쓸 곳에 넣어주기만 하면 된다!

import React, { useEffect, useRef } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import Input from '../../../recoil/Input/atom';
import Output from '../../../recoil/Output/atom';

const XtermComponent: React.FC = () => {
  const terminalRef = useRef<HTMLDivElement>(null);
  const xtermRef = useRef<Terminal | null>(null);
  const fitAddonRef = useRef<FitAddon | null>(null);
  const setInput = useSetRecoilState(Input);
  const [output, setOutput] = useRecoilState(Output);

  let inputBuffer = ''; // 입력된 문자열을 저장할 버퍼

  useEffect(() => {
    if (terminalRef.current) {
      // 터미널과 애드온 인스턴스 생성
      const xterm = new Terminal();
      const fitAddon = new FitAddon();
      xterm.loadAddon(fitAddon);

      // 터미널을 DOM 요소에 연결
      xterm.open(terminalRef.current);
      fitAddon.fit();

      // 기본 메시지 출력
      xterm.writeln('Welcome to DPIDE!!');

      // 사용자 입력 처리
      xterm.onData(data => {
        if (data === '\r') {
          // 엔터 키 입력 처리
          xterm.write('\r\n'); // 줄바꿈만 수행
          if (inputBuffer.trim() === 'clear') {
            // 입력이 'clear'일 경우 터미널 초기화
            xterm.clear(); // 터미널 내용 초기화
            // eslint-disable-next-line react-hooks/exhaustive-deps
            inputBuffer = '';
          } else if (inputBuffer.trim()) {
            // 입력이 있을 때만 상태 업데이트
            setInput(prevInput => prevInput + inputBuffer + '\n'); // Recoil 상태로 inputBuffer 업데이트
            // eslint-disable-next-line react-hooks/exhaustive-deps
            inputBuffer = '';
          }
        } else if (data.charCodeAt(0) === 127) {
          // 백스페이스 처리
          if (inputBuffer.length > 0) {
            inputBuffer = inputBuffer.slice(0, -1);//문자열에서 마지막 부분 제외하고 반환
            xterm.write('\b \b'); // 터미널에서 백스페이스 표현
          }
        } else {
          inputBuffer += data; // 입력된 데이터를 버퍼에 추가
          xterm.write(data); // 터미널에 입력된 데이터 출력
        }
      });

      // 참조에 인스턴스 저장
      xtermRef.current = xterm;
      fitAddonRef.current = fitAddon;

      return () => {
        // 컴포넌트 언마운트 시 인스턴스 정리
        xterm.dispose();
      };
    }
  }, [setInput]);

  // output이 있으면 터미널에 출력하고 상태 초기화
  useEffect(() => {
    if (output && xtermRef.current) {
      const formattedOutput = output.replace(/\n/g, '\r\n');
      xtermRef.current.write(formattedOutput);
      setOutput(null); // 출력 후 상태 초기화
      setInput('');
    }
  }, [output, setInput, setOutput]);

  return <div ref={terminalRef} style={{ width: '100%', height: '100%' }} />;
};

export default XtermComponent;

clear 기능도 넣어주었고 백엔드에 입력값을 넘겨줄 때 엔터를 기준으로 입력값의 개수를 구분하기로 하였는데 xterm에서는 엔터를 따로 인식하지 못하였다. 그래서 setInput(prevInput => prevInput + inputBuffer + '\n'); 이렇게 해주었다.
xTerms는 내가 직접 하나씩 다 구현해줘야했다. 백스페이스 처리도 문자열에서 마지막 부분 제외하고 반환한 다음 터미널에서도 그렇게 보이기 위해서 xterm.write('\b \b')를 해주어야했다. \b로 커서를 한 칸 왼쪽으로 이동시키고 그다음 문자열을 ' '로 공백으로 출력된 문자를 덮어씌우고 다시 \b로 커서를 왼쪽으로 한 칸 이동시킨다.

그리고 실행시켰을 때 백엔드에서 값을 잘 받아왔는데...!!!! 출력할 때 그 다음줄로 넘어가면

hello
	sum=8

이런식으로 줄마다 앞에 공백이 생기면서 출력이 되었다.
const formattedOutput = output.replace(/\n/g, '\r\n');
그래서 위에 코드처럼 \n\r\n으로 바꿔서 커서를 맨 앞으로 이동시키고 커서가 한 칸 내려가도록 해놨다. 그래서 출력결과도 잘나왔다..!!!!

이번에 xterm에 대해서 많이 알게되었다. 다음에 또 터미널 화면 구현하는 일이 있으면 써야겠담

0개의 댓글