LabHub

블로그

터미널과 셸 내부 완전 해부 — PTY, ANSI 이스케이프, termios, 시그널, Job Control 끝장 가이드 (2025)

들어가며 — 까만 창의 정체

매일 쓰는 iTerm, Windows Terminal, VS Code terminal. 거기에 ls를 치면 파일 목록이 나오고, Ctrl+C로 실행 중인 프로세스를 멈추고, vim을 실행하면 전체 화면을 점유한 에디터가 뜬다. 자연스러워 보이지만, 이 모든 게 1970년대 VT100 텔레타이프 단말기의 추상화를 현대 OS가 여전히 유지한 결과다.

터미널 에뮬레이터가 진짜 뭘 할까?

그리고 bash 같은 셸은:

이 모든 게 합쳐져서 "터미널"이 된다. 이 글에서는 각 층을 하나씩 벗겨보자. 글을 끝까지 읽으면 tmux/screen이 왜 필요한지, ssh가 어떻게 원격 터미널을 흉내내는지, stty -echo가 비밀번호 입력에 쓰이는 이유, ANSI 컬러 코드가 단순 숫자 나열인 이유를 단순히 "그렇게 된다"가 아니라 "왜 그렇게 설계됐는지" 설명할 수 있게 된다.


1. 역사 — 텔레타이프에서 가상 터미널까지

1.1 1869년 — Teletype

Edward Calahan이 설계한 전신 기계. 키보드 + 프린터. 상대 기계로 전신 신호를 보내면 그쪽 프린터가 글자를 찍음.

1.2 1960년대 — Computer Terminal

컴퓨터와 사람을 연결하는 단말기. 초기엔 프린터 기반(Teletype Model 33, "TTY"). 한 번 프린트한 줄은 수정 불가. \r (carriage return)은 프린트 헤드를 줄 처음으로, \n (line feed)은 종이를 한 줄 올림. Windows가 \r\n을 쓰는 이유의 역사적 기원.

1.3 1970년대 — Video Display Terminal

CRT 화면 + 키보드. 대표 모델: DEC VT100 (1978). 80x24 칸, 이스케이프 시퀀스로 커서 제어. 오늘날 "terminal"의 사실상 표준이 됨.

1.4 1980년대 이후 — Terminal Emulator

개인용 컴퓨터 보급으로 실제 단말기 장치 대신 소프트웨어 에뮬레이터가 주류. xterm(X Window), DEC VT320 호환. 이후 등장한 iTerm2, Windows Terminal, Alacritty, WezTerm 등 모두 VT100의 후손.

중요한 관찰: 2025년에도 우리는 VT100 시절 설계된 프로토콜 위에 구축된 도구로 일한다. 그 프로토콜이 50년 동안 교체되지 않은 이유는, 단순하고 바이트 스트림 기반이어서 네트워크/파이프/파일 어디든 동작하기 때문이다.


2. PTY — 가상 터미널의 정체

2.1 실제 단말기와의 차이

초창기 Unix는 실제 터미널 장치 /dev/tty0, /dev/tty1...을 통해 사용자와 통신. 요즘은 터미널 에뮬레이터(GUI 프로그램)가 실제 하드웨어가 아니니, 커널은 가상의 터미널 한 쌍을 만들어준다. 이게 pseudo-terminal (PTY).

2.2 Master-Slave 구조

┌─────────────────────┐    ┌──────────┐    ┌─────────────────┐
│ Terminal Emulator   │◀──▶│ PTY pair │◀──▶│ Shell (bash,...) │
│ (iTerm, xterm, ...) │    │          │    │                  │
└─────────────────────┘    └──────────┘    └─────────────────┘
       master side                             slave side
    (터미널이 읽고 씀)                       (프로세스가 읽고 씀)

비유: 과거에는 컴퓨터와 터미널이 시리얼 케이블로 연결됐다. PTY는 그 케이블의 소프트웨어 에뮬레이션. 양 끝은 서로를 "파일"로 본다.

2.3 만드는 법

#include <pty.h>
int master_fd;
pid_t pid = forkpty(&master_fd, NULL, NULL, NULL);
if (pid == 0) {
    // child: slave 쪽이 stdin/stdout/stderr가 됨
    execlp("bash", "bash", NULL);
} else {
    // parent: master_fd에 쓰고 읽기
}

터미널 에뮬레이터, SSH 서버, tmux, Docker의 -t, VS Code의 integrated terminal 모두 이 forkpty로 PTY를 만든다.

2.4 왜 필요한가

많은 프로그램이 "stdin이 터미널인지" 체크한다(isatty(0)). 터미널이면:

단순히 stdout을 파일로 리디렉션하면 isatty가 false를 반환하고 이런 기능이 꺼진다. 반대로 tmux/screen이 이런 프로그램을 감쌀 수 있는 건, PTY를 중간에 끼우기 때문.


3. termios — 터미널의 설정

3.1 무엇을 제어하는가

termios 구조체는 PTY의 입력/출력/라인 처리 규칙을 정의한다. 주요 설정:

3.2 기본 모드 — Canonical (Cooked)

bash 프롬프트에서 명령을 입력하는 평범한 상태.

3.3 Raw 모드

vim, htop, less 같은 TUI 앱이 사용. 이들은 방향키, Ctrl+X 같은 조합을 직접 해석해야 하기 때문.

3.4 실전 명령

# 비밀번호 입력 시 echo off
$ stty -echo; read password; stty echo

# 현재 설정 보기
$ stty -a

# Canonical 끄고 한 글자씩 받기
$ stty raw -echo; read -n 1 key; stty sane

# 리셋
$ stty sane

3.5 Python 예시

import termios, tty, sys

fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
    tty.setraw(fd)
    ch = sys.stdin.read(1)  # 한 글자 즉시
finally:
    termios.tcsetattr(fd, termios.TCSADRAIN, old)

3.6 stty 흔한 용도


4. ANSI Escape Code — 제어 문자의 세계

4.1 기본 구조

모든 이스케이프 시퀀스는 ESC (0x1B, \x1b, \033)로 시작.

4.2 색상

$ echo -e "\x1b[31mRed Text\x1b[0m normal"

기본 8색:

code
30Black
31Red
32Green
33Yellow
34Blue
35Magenta
36Cyan
37White

배경색은 40~47. 90~97/100~107은 bright 변형.

256색:

\x1b[38;5;196m  ← 전경, 팔레트 196번 (빨강 계열)
\x1b[48;5;20m   ← 배경, 팔레트 20번

Truecolor (16.7M):

\x1b[38;2;255;128;0m  ← RGB (255, 128, 0) 오렌지

대부분의 현대 터미널이 truecolor 지원. $COLORTERM=truecolor 환경변수로 확인.

4.3 커서 이동

4.4 화면 제어

4.5 스타일

4.6 OSC — 터미널 "대화"

4.7 Bracketed paste mode

\x1b[?2004h   ← 활성화
(붙여넣기 시)
\x1b[200~ ...내용... \x1b[201~
\x1b[?2004l   ← 비활성화

셸이 "이건 붙여넣은 내용이야"라고 인지해 자동 실행되지 않게 한다. Zsh/Bash 최근 버전에서 기본 활성화.


5. Signal과 Job Control

5.1 시그널 개요

kill -l로 전체 목록 확인. 터미널 관련 주요:

Signal번호기본 동작트리거
SIGINT2종료Ctrl+C
SIGQUIT3core dumpCtrl+\
SIGTSTP20정지Ctrl+Z
SIGTTIN21정지background에서 stdin 읽음
SIGTTOU22정지background에서 stdout 씀 (tostop 옵션 시)
SIGHUP1종료터미널 연결 끊김 (SSH 단절, 창 닫음)
SIGWINCH28무시터미널 크기 변경

5.2 프로세스 그룹과 세션

Session (login 단위)
├── Session leader (보통 shell)
└── Process groups
    ├── Foreground group (현재 PTY를 제어)
    │   └── e.g., "grep foo | less" 파이프라인
    └── Background groups
        ├── "long-running &"로 실행된 그룹
        └── ...

5.3 Ctrl+C의 여정

  1. 사용자가 Ctrl+C 누름 → 터미널 에뮬레이터가 바이트 0x03 전송
  2. PTY master 쪽에서 받음 → Line discipline(N_TTY)이 canonical 모드 + isig 활성 확인
  3. Line discipline이 foreground 프로세스 그룹 전체에 SIGINT 발송
  4. 파이프라인 전체가 종료 (또는 handler 등록된 프로세스는 자체 처리)

왜 파이프라인 전체에? grep foo huge.log | lessCtrl+C로 grep만 종료하면 less가 EOF 받고 끝나야 정상. 그룹 단위 시그널이 이걸 가능케 한다.

5.4 Job Control

bash/zsh에서:

5.5 SIGHUP와 nohup / tmux

SSH 세션이 끊기면 shell이 SIGHUP를 받고, 셸이 자식들에게 HUP 전파 → 모두 종료. 이걸 피하려면:

5.6 SIGWINCH

창 크기 바꾸면 커널이 foreground 프로세스에 SIGWINCH 발송. vim/less/tmux는 이걸 handler로 받아 화면 다시 그림. 안 받으면 깨진 레이아웃.


6. Shell 내부 — bash/zsh가 하는 일

6.1 REPL 흐름

while True:
    1. 프롬프트 출력 (PS1)
    2. readline으로 한 줄 입력 받음
    3. parse: 토큰화, 파이프/리다이렉션/expansion
    4. 실행: builtin이면 직접, 아니면 fork+exec
    5. wait (foreground인 경우)

6.2 Builtin vs External

type cd, type ls로 확인. command prefix로 builtin 우회.

6.3 Expansion (순서 중요)

  1. Brace expansion: {a,b,c}a b c, {1..5}1 2 3 4 5
  2. Tilde: ~$HOME
  3. Parameter: $VAR, ${VAR:-default}
  4. Command substitution: $(cmd) 또는 `cmd`
  5. Arithmetic: $((1 + 2))
  6. Process substitution: <(cmd), >(cmd)
  7. Word splitting: IFS로 split
  8. Pathname expansion (globbing): *.txt → 파일 목록
  9. Quote removal

실행 전 이 순서대로 전개된다. echo "$VAR"에서 쿼트가 있으면 word splitting을 건너뛰는 등 세부 규칙이 복잡.

6.4 Pipe와 Redirection

$ a | b | c
  1. Shell이 3번 fork + exec, 각각 pipe() 생성
  2. a의 stdout → pipe 1 → b의 stdin
  3. b의 stdout → pipe 2 → c의 stdin
  4. 모두 같은 프로세스 그룹 (foreground)
  5. Shell은 c만 wait (마지막 exit code가 $?) — pipefail 옵션 있으면 중간도 체크

6.5 Glob 방식 — shell vs 프로그램

Unix 철학: ls *.txt는 shell이 *.txt를 파일 목록으로 펼쳐 ls file1.txt file2.txt로 exec. ls는 아무것도 몰라도 됨. Windows cmd/PowerShell이 개별 프로그램에 글로브 해석을 맡기는 것과 대조.

부작용: rm *를 빈 디렉터리에서 실행하면 shell이 "* 그대로" 넘김 (nullglob 옵션 없으면) → rm이 * 이름 파일을 못 찾아 에러. shopt -s nullglob로 다르게 설정 가능.

6.6 Exit code

6.7 zsh vs bash

6.8 프롬프트 파싱

PS1='\u@\h:\w\$ '
# \u: 사용자, \h: 호스트, \w: pwd, \$: #(root) or $

Zsh:

PROMPT='%n@%m:%~%# '

oh-my-zsh의 Git 상태 표시는 매 프롬프트마다 git status를 실행해서 체감 느릴 수 있음. starship / powerlevel10k는 async/cache로 개선.


7. Readline — 한 줄 편집의 기반

Bash(및 대부분 REPL)가 사용하는 GNU Readline 라이브러리.

7.1 주요 바인딩 (emacs 모드 기본)

7.2 Vi mode

$ set -o vi    # bash
$ bindkey -v   # zsh

Esc로 normal mode, i로 insert. 명령줄에서 vim 매크로급 편집 가능.

7.3 History

7.4 inputrc

~/.inputrc로 readline 커스터마이즈:

set completion-ignore-case on
set show-all-if-ambiguous on
"\e[A": history-search-backward  # 방향키 위 → prefix 매치 검색

8. SSH — 원격에서 터미널 에뮬레이션

8.1 세션 흐름

  1. TCP 연결 + SSH 프로토콜 핸드셰이크 (키 교환, 인증)
  2. 클라이언트가 "pty 요청" 채널 open (-t 옵션 또는 interactive)
  3. 서버가 PTY 한 쌍 생성
  4. 서버의 slave 쪽에 login shell 실행
  5. 클라이언트 ↔ 서버 사이 양방향 바이트 스트림 (암호화)

로컬 키보드 입력 → SSH 암호화 → 서버 PTY master → shell stdin. 출력은 역방향.

8.2 SIGHUP과 SSH 단절

SSH 연결 끊김 → 서버의 PTY master close → slave 쪽 프로세스들이 SIGHUP 받음 → 종료. tmux/screen을 쓰는 이유.

8.3 SSH agent forwarding

ssh -A user@host: 로컬 SSH agent를 서버에서 접근 가능하게. 편하지만 서버가 해킹되면 내 SSH 키가 모든 다른 서버에 접근 가능 → 보안 위험. ForwardAgent no 기본 권장, 필요한 호스트만 config로 허용.

8.4 X11/포트 포워딩


9. tmux / screen — Terminal Multiplexer

9.1 왜 필요한가

9.2 구조

tmux server (daemon, 호스트당 1개)
└── sessions
    ├── session "main"
    │   ├── window 0 (several panes)
    │   ├── window 1
    │   └── ...
    └── session "work"
        └── ...

클라이언트는 attach로 session에 붙고, detach로 떨어짐.
서버는 클라이언트 없어도 계속 실행.

9.3 tmux가 PTY를 쓰는 방식

9.4 단축키 (기본 prefix Ctrl+B)


10. 터미널의 복사/붙여넣기

10.1 왜 Ctrl+C가 복사가 아닌가

Ctrl+C는 오래전부터 SIGINT 의미로 고정. 터미널 복사는 관례적으로:

10.2 tmux copy mode

prefix [로 copy mode 진입 → vi/emacs 키로 선택 → Enter로 버퍼에 저장. prefix ]로 붙여넣기.

시스템 클립보드와 연동하려면 OSC 52 (clipboard paste)를 사용. tmux 3.2+에서 set -g set-clipboard on.

10.3 원격 SSH에서 클립보드

일반적으로 서버가 클라이언트 클립보드에 접근 불가. 해결:


11. 현대 터미널 에뮬레이터 비교

11.1 주요 선수

11.2 성능 차이

단순 문자 렌더링은 차이 없어 보이지만:

11.3 Graphics 프로토콜

icat, kitty +kitten icat, timg 같은 툴로 터미널에서 이미지 보기 가능.


12. 색과 테마

12.1 $TERM 환경변수

$ echo $TERM
xterm-256color

terminfo 데이터베이스(/usr/share/terminfo/)의 엔트리 이름. 각 터미널이 자기 capability를 선언. tput 명령으로 쿼리:

$ tput colors   # 256
$ tput cols     # 120
$ tput setaf 1; echo Red; tput sgr0

12.2 $COLORTERM

$ echo $COLORTERM
truecolor

truecolor 지원 여부. 프로그램(vim, tmux)이 이 값을 보고 24비트 컬러 활성화.

12.3 테마 도구

12.4 스크립트에서 색

# 직접 이스케이프
$ echo -e "\033[31mRed\033[0m"

# tput으로 이식성 있게
$ echo "$(tput setaf 1)Red$(tput sgr0)"

# terminfo 상수
$ RED=$(tput setaf 1); NC=$(tput sgr0)
$ echo "${RED}error${NC}"

파이프 감지:

if [ -t 1 ]; then
    echo -e "\033[31mRed\033[0m"
else
    echo "Red"  # 파일로 리디렉션 시 색 제거
fi

ls --color=auto가 정확히 이 로직.


13. Shell 스크립트의 숨은 함정

13.1 Quoting

# 거의 항상 쌍따옴표로 감싸라
foo="$bar"       # ✅
foo=$bar         # ❌ word splitting
foo='$bar'       # 리터럴 (expansion 안 함)

13.2 파일 이름에 공백

for f in $(ls); do echo $f; done
# "my file.txt"가 두 개 인자로 쪼개짐

# ✅
for f in *; do echo "$f"; done

# 또는
find . -type f -print0 | while IFS= read -r -d '' f; do ...; done

13.3 [ vs [[

[[ -f "$file" && -r "$file" ]]  # bash OK
[ -f "$file" -a -r "$file" ]    # POSIX

13.4 $* vs $@

13.5 errexit의 함정

set -e
cmd || fallback   # errexit 건너뜀 (설계대로)
cmd && echo ok    # cmd 실패 시 스크립트 종료 안 함 (&&의 좌측은 체크 안 함)

생각대로 안 돌 때가 많아서 set -e 대신 명시적 에러 체크를 선호하는 학파도 있다.

13.6 Subshell

(cd /tmp; some_cmd)   # 현재 디렉터리 안 바뀜
cd /tmp; some_cmd     # 현재 디렉터리 바뀜

|도 일부 경우 subshell 생성. 변수 할당이 "사라지는" 이유.


14. 현대 대안 — fish, nushell, xonsh

14.1 fish

14.2 nushell

14.3 xonsh

14.4 oil shell (osh/ysh)

현실: 2025년 생산성 터미널은 starship 프롬프트 + fish 또는 zsh(oh-my-zsh/p10k)가 주류. nushell은 특수 워크로드에 실험적으로 사용.


15. Terminal의 미래

15.1 AI 통합

15.2 GPU/rich rendering

15.3 Shell 세션의 공유

15.4 Web-based terminals


16. 실전 팁

디버깅

생산성

생존


마무리 — 50년 된 추상화의 힘

VT100(1978) 프로토콜이 2025년에도 살아있다는 건 놀라운 일이다. 그 이유는 단순하다: 바이트 스트림 + 이스케이프 시퀀스라는 단순한 인터페이스가 "텍스트 I/O의 보편 언어"가 됐기 때문. 그 위에 tmux, ssh, Docker exec, VS Code terminal, 그리고 Warp 같은 AI 터미널까지 모두 똑같은 계층을 재활용한다.

개발자가 터미널을 "제대로" 이해하면:

이 글의 교훈 5가지:

  1. 터미널은 에뮬레이터 + PTY + shell + readline의 협연이지 단일 프로그램이 아니다
  2. Raw vs Canonical 모드 차이가 bash 입력과 vim 입력의 본질적 차이
  3. 시그널은 프로세스 그룹 단위 — 그래서 파이프라인 전체가 Ctrl+C로 죽는다
  4. ANSI 이스케이프 시퀀스는 1978년 스펙이고 여전히 표준이다
  5. tmux는 "SSH 끊김 보험"이 아니라 멀티플렉서 — 기본 도구로 써라

다음 글에서는 Linux 커널 내부 — 시스템 콜의 여정, VFS, 프로세스 스케줄러, 메모리 관리, I/O 스택을 파고든다. 터미널에서 친 ls가 화면에 파일 목록으로 나오기까지, 키 입력 → PTY → shell → fork+exec → ls → open/read/write → VFS → filesystem driver → block device → 화면 경로 중 이 글은 PTY~shell까지만 다뤘다. 다음 글은 그 아래 레이어로 내려간다.

터미널은 단순한 "까만 창"이 아니라, 50년의 소프트웨어 진화가 누적된 살아있는 박물관이다. 그 박물관의 구조를 이해하는 순간, 매일 하는 작업이 훨씬 덜 신비롭고 훨씬 더 흥미로워진다.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다