• logo

      SeolMyeongTang

  • xterm.js과 node-pty로 웹 터미널을 만들어보겠습니다. 근데 이제 rollup을 곁들인 - 이론편

    2022년 10월 16일

    대부분의 프로그램들은 그래픽 형태로 보여주는 GUI(Graphics User Interface) 방식입니다. 하지만 일부 프로그램은 터미널의 검은 화면 바탕인 CLI(Command Line Interface) 방식으로 보여줍니다(ex. git, mysql cli, aws cli 등등). 저는 CLI 환경이 GUI 환경에 못지 않게 기능들을 제공하고 CLI 환경이 더 가볍게 사용할 수 있을 것 같은 믿음(?)으로 CLI 환경을 좋아합니다. 서버 작업을 하다보면 자연스럽게 터미널 환경을 많이 접해서 그런지 오히려 그래픽 환경보다 터미널 환경이 편할 때도 있습니다.

    기능이 많은 OpenSSL  프로그램은 CLI 환경이 더 편할지도 모릅니다
    기능이 많은 OpenSSL 프로그램은 CLI 환경이 더 편할지도 모릅니다 🥲 [출처]

    터미널의 시-꺼먼 환경을 좋아하는 저한테는, 웹 터미널은 반갑고 흥미로운 아이입니다. 예전에 인턴을 하면서 웹 터미널을 잠깐 구현할 기회가 있어서 그 때의 기억을 더듬어 socketIO, xtermjs, node-pty 모듈을 통해 웹 터미널을 구현 해보고 한술 더 떠서 rollup을 이용하여 라이브러리로 만들어보는 시간을 가지도록 하겠습니다. 웹 터미널을 구현하기 전에 terminal, tty, shell와 같은 헷갈리는 용어 정리와 리눅스 pty 작동 방식에 대해 알아보면 좋습니다.

    Terminal, tty, shell 이 세 단어는 모두 같은 의미를 뜻하는 용어 같은데 각각의 의미 차이가 궁금했습니다. 먼저 terminal(이하 터미널)부터 알아보도록 합시다. 초기의 터미널이라는 용어는 데이터를 입력하거나 출력하는데 쓰이는 하드웨어를 말했습니다. ‘종료하다', ‘종점에 닿는다'라는 의미를 가지는 terminal의 동사 형태인 terminate 단어에서 볼 수 있듯이 통신의 처음과 마지막에 위치하는 장치를 터미널이라고 합니다. 하드웨어 형태의 터미널도 있지만 기술의 발전으로 컴퓨터 커널의 도움으로 물리적인 터미널을 ssh, xterm, puTTY와 같은 소프트웨어로 모방한(emulate) 터미널도 있습니다. 이런 아이들을 terminal emulator라고 하며, terminal emulator를 통해 접속하면 가상 터미널(pty, pesudo tty)이 만들어집니다. 반면에 시리얼 포트와 같이 하드웨어에 직접적으로 연결된 터미널을 tty(Teletype)라 합니다[1]. 즉, 컴퓨터에서 터미널이라고 하면 넓은 의미로는 컴퓨터와 사용자 사이를 소통해주는 물건을(하드웨어든, 소프트웨어든) 말하고 좁은 의미로는 흔히 말하는 터미널 검은 창의 CLI 환경을 말합니다.

    비슷한 용어인 console(이하 콘솔)이라는 아이도 있는데 터미널과 콘솔을 동의어라 생각하셔도 될 것 같습니다. 콘솔이라고 하면 Xbox, Playstation과 같은 게임 전용 단말기를 떠올릴 수 있는데 소프트웨어적인 의미보다 하드웨어적인 의미가 내포있는 단어인 것 같습니다.

    Shell(이하 쉘)은 문자, 명령어를 처리하고 해석해주는 프로그램입니다. 터미널은 컴퓨터와 사용자 간의 창구이지만 터미널만 ****가지고 둘 사이를 소통을 할 수 없습니다. 쉘은 명령어 해석을 하거나 프로그램을 실행하여 그 결과값을 출력하는 역할을 합니다. 보통 사용자는 터미널과 쉘을 활용한 CLI 환경에서 컴퓨터와 소통합니다. 쉘의 예로 sh, bash, zsh 등이 있습니다.

    웹 터미널을 구현하는데 쓸 socketIO, xtermjs, node-pty 모듈들을 살펴봅시다. xtermjsGitHub readme는 다음과 같이 설명합니다.

    Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser.

    xtermjs는 ‘터미널’로 웹 브라우저에서 사용자가 키보드를 통해 입력을 하면 그저 문자를 출력을 하는 역할만 합니다. xtermjs만 가지고 bash와 같이 명령어 해석이나 프로그램을 실행시킬 수 없습니다. bash와 같이 프로그램을 실행할 수 있도록 만들려면 리눅스 서버에 pty(가상 터미널)를 만들고, 이를 통해 xtermjs과 쉘 사이를 연결하여 데이터를 주고 받아야합니다. node-pty는 pty를 생성하여 쉘과 통신할 수 있도록 합니다. xtermjsnode-pty 사이에는 socketIO 모듈로 소켓 통신을 하여 실시간으로 데이터를 주고 받습니다. 이를 정리하면 다음과 같이 나타낼 수 있습니다.

    웹 터미널 통신 구조
    웹 터미널 통신 구조

    리눅스 운영체제가 설치되어 있는 컴퓨터의 터미널(tty)을 통해서나 ssh로 리눅스 서버에 원격으로 접속하여 가상 터미널(pty)을 통해 명령어를 실행해 본 경험이 있을겁니다. 이 때, 검은 창 터미널에 문자 ‘A’를 입력하면 바로 화면에 ‘A’가 출력되는 것이 아니라 여러 단계를 거쳐 화면에 보이게됩니다.

    내부 터미널 동작 방식
    내부 터미널 동작 방식[2]

    사용자가 키보드를 통해 입력을 하면 키보드 디바이스 드라이버를 통해 키보드를 제어하고 문자를 처리(Line discipline)한 뒤[3] bash(User process)가 문자 캐릭터들을 해석하고 그 결과를 터미널(Terminal emulator)로 보내 VGA 그래픽의 도움으로 화면에 랜더링되는 과정을 거칩니다. Line discipline은 단순히 문자를 처리할 뿐만 아니라 backspace(^?, ^H), enter(^M), 인터럽트(^C)와 같은 문자를 처리할 수 있으며, enter를 눌렀을 때 입력 값을 LF 형식으로 변환합니다. Line discipline은 사용자가 잘못 입력한 문자를 지우거나 space로 빈 칸을 만드는 line editing의 기능을 합니다. 내부 터미널은 위처럼 동작하지만 xtermjsssh와 같은 외부 터미널은 다르게 동작합니다.

    외부 터미널 동작 방식
    외부 터미널 동작 방식[4]

    xterm는 리눅스 pty master side(이하 ptm)에 붙고 중간에 Line discipline을 두고 pty slave side(이하 pts)에 연결됩니다. pts는 bash(User process)와 같은 프로세스와 통신하게 됩니다. 복잡하게 생겼지만 하나의 예를 보도록 합시다[5].

    1. 사용자가 xterm에 문자 A를 누른다.
    2. 문자 A는 ptm에 전달된다.
    3. ptm은 받은 문자 A를 Line discipline한테 전달한다.
    4. Line discipline은 내부 line editor 버퍼에 저장하고 다시 ptm한테 돌려준다.
    5. ptm은 Line discipline로 부터 받은 문자를 xterm한테 보낸다.
    6. xterm은 ptm으로부터 받은 문자 A를 출력한다.

    이 상태에서 backspace(뒤로가기) 키를 눌러봅시다.

    1. 사용자가 xterm에 backspace를 누른다.
    2. backspace는 xterm에 의해 ^H 문자가 되어 ptm에 전달된다.
    3. ptm은 받은 ^H를 Line discipline한테 전달한다.
    4. Line discipline의 내부 버퍼에 있던 문자 A를 지우고 ptm으로 ^H을 다시 돌려준다.
    5. ptm은 Line discipline로 부터 받은 ^H를 xterm한테 보낸다.
    6. xterm은 A를 지운다.

    앞의 두 예시에서 볼 수 있듯이 사용자가 키보드를 통해 문자를 전달하면 xterm → ptm → Line discipline → ptm → xterm의 과정을 거치며, Line discipline 내부 버퍼를 업데이트 합니다. 그러면 어느 경우에 사용자의 입력이 pts와 User process까지 전달이 될까요? 사용자가 enter 키를 누르면 앞의 예시와 다르게 동작합니다. whoami 이라는 문자가 버퍼에 저장되어있다고 가정합시다.

    1. 사용자가 xterm에 enter를 누른다.
    2. enter는 xterm에 의해 ^M 문자가 되어 ptm에 전달된다.
    3. ptm은 받은 ^M를 Line discipline한테 전달한다.
    4. Line discipline는 ^M(CR)을 ^J(LF)로 변환하고 버퍼에 있는 문자들을 pts로 전달한다.
    5. pts는 whoami<LF>를 bash(User process)로 보낸다.
    6. bash는 whoami<LF> 해석하고 결과 값을 pts → Line discipline → ptm → xterm 순서로 전달한다.
    7. xterm에 결과 값이 나타나며, LF로 인해 커서가 다음 줄로 이동한다.

    터미널에 enter를 누르기 전까지는 문자들이 화면에 바로 표시 되었다가 enter를 누르면 그 때 내부적으로 처리하여 명령어 결과 값을 출력해주는지 알았는데 한 문자마다 위와 같은 방식을 거친다는 사실에 신기했습니다. 가만 생각해보면 ssh로 원격 접속을 하고 네트워크 환경이 좋지 않거나 원격 서버 사양이 좋지 않으면 한 글자를 쓸 때마다 버벅거리는 이유가 위와 같은 이유인 것 같습니다. 또한 가만 생각해보면 bashzsh가 아닌 sh 같은 쉘에 오른쪽 방향키를 누르면 커서가 오른쪽으로 이동하는 것이 아닌 ^[[C(^[[C는 escape인 ^[ + open bracket인 [, 그리고 C로 이루어져 있습니다) 같은 이상한 문자가 표시됩니다. 이는 오른쪽 방향키에 해당하는 ^[[C에 대한 정보가 Line discipline에 없기 때문이지 않을까(맵핑이 되어있지 않을까) 하는 추측을 하였습니다.

    웹 터미널을 만들기 앞서 딱딱한 이론을 다루어 보았습니다. pty에 대해 설명하다 리눅스가 키보드 입력을 어떻게 다루고 문자를 인식하는지에 대해 다루어 보았는데 다음 기회에 키보드 디바이스 드라이버에 대한 글을 정리하는 것도 재미있을 것 같습니다. 개인적으로 공부한 내용을 정리하다보니 오류가 있을 수 있습니다. 다음 글부터 본격적으로 코드와 함께 웹 터미널을 구현해보는 시간을 가져보도록 하겠습니다.


    [1] https://unix.stackexchange.com/questions/21280/difference-between-pts-and-tty

    [2], [4] http://www.linusakesson.net/programming/tty/

    [3] https://en.wikipedia.org/wiki/Line_discipline, https://www.kernel.org/doc/html/latest/driver-api/tty/tty_ldisc.html

    [5] https://unix.stackexchange.com/questions/117981/what-are-the-responsibilities-of-each-pseudo-terminal-pty-component-software


    참고 사이트들