이번에는 백엔드 부분까지 작성하여 터미널 다운 웹 터미널을 만들어 볼 예정입니다. 진정한 웹 터미널을 만들기 위해서는 앞서 다룬 프론트엔드의 xterm.js
뿐만 아니라 bash
나 cmd
와 같은 쉘이 필요합니다. xterm.js
과 쉘을 연결시키기 위해 node-pty
모듈을 활용할 것입니다. node-pty
는 Microsoft에서 관리하고 있는 node 모듈로, pty 프로세스를 만들어주는 모듈입니다. 웹 터미널 백엔드 부분은 프론트엔드 부분보다 간결하게 작성할 수 있습니다.
// server/src/Pty.ts
import os from 'os';
import { spawn, IPty } from 'node-pty';
class Pty {
ptyProcess: IPty;
shell: string;
constructor() {
this.shell = os.platform() === 'win32' ? 'cmd.exe' : 'bash';
this.init();
}
init() {
this.ptyProcess = spawn(this.shell, [], {
name: 'xterm-color',
cwd: process.env.HOME,
env: process.env as { [key: string]: string },
});
}
}
export default Pty;
node-pty
는 리눅스의 forkpty(3)
(그 중 opentty()
, fork(2)
부분) 명령어를 NodeJS에서 쓸 수 있도록 만든 모듈입니다. 위 예제 코드의 init()
함수를 잠시 살펴보면 node-pty
의 spawn
함수로 this.shell
명령어를 실행(fork) 합니다. 이 같은 부분은 node-pty
모듈의 unixTerminal.ts
코드를 참고하면 찾을 수 있습니다. node-pty
모듈 코드를 읽어보는 것도 흥미로우니, 관심있으신 분은 한 번 찾아보는 걸 추천드립니다.
export class UnixTerminal extends Terminal {
...
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) {
...
// fork
const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), onexit);
...
}
...
}
저희는 쉘이 필요하므로 OS가 윈도우일 경우는 cmd
를 그 외는 bash
를 넣어줍니다. xterm.js
를 위한 쉘을 만들었지만 어떻게 이 둘 사이의 통신을 하나요? 실시간으로 양방향 통신하기 위해 socket 통신을 사용합니다.
Socket 통신을 하기 위해서 프론트엔드, 백엔드 두 쪽 모두 통신할 수 있는 길(이벤트)을 만들어야합니다. 길을 만들기 위해 통신 과정을 생각 해봅시다.
웹 터미널 socket 통신 과정
2, 3번 과정에서 xterm.js
에서 문자 데이터를 보내고(emit) 백엔드에서 데이터를 받고(on) 4, 5번에서 백엔드에서 데이터를 보내고(emit) xterm.js
에서 받는(on) 설정이 필요합니다. 이를 코드로 작성하면 다음과 같습니다.
// 프론트엔드에서의 socket 이벤트 설정
// frontend/src/config/SocketConfig.ts 코드 일부분
init(term: xterm) {
this.socket.on('output', (message) => {
term.write(message);
});
}
execute(command: string) {
this.socket.emit('input', command);
}
// 백엔드에서의 socket 이벤트 설정
// server/src/Socket.ts 코드 일부분
io.on('connection', (socket) => {
console.log('Socket Connected: ', socket.id);
this.pty = new Pty(socket);
socket.on('disconnect', () => {
console.log('Socket Disconnected: ', socket.id);
});
socket.on('input', (input: string) => {
this.pty.write(input);
});
});
// server/src/Pty.ts 코드 일부분
send(data: string) {
this.socket.emit('output', data);
}
둘 사이 데이터를 주고 받는 socket 이벤트를 만들었지만, 그 전에 socket 연결 초기화하는 부분도 작성해봅시다. Socket 연결하는 코드는 프론트엔드 쪽에 작성하고 컴포넌트 props로 넘깁니다. 웹 터미널 라이브러리 코드도 이에 따라 수정합니다.