들어가며 — 까만 창의 정체
매일 쓰는 iTerm, Windows Terminal, VS Code terminal. 거기에 `ls`를 치면 파일 목록이 나오고, `Ctrl+C`로 실행 중인 프로세스를 멈추고, `vim`을 실행하면 전체 화면을 점유한 에디터가 뜬다. 자연스러워 보이지만, 이 모든 게 **1970년대 VT100 텔레타이프 단말기의 추상화**를 현대 OS가 여전히 유지한 결과다.
터미널 에뮬레이터가 진짜 뭘 할까?
- GUI 창 하나를 열어서 문자를 렌더링한다 (폰트, 커서, 스크롤백)
- 사용자의 키보드 입력을 받아 **가상 터미널(PTY)**의 "master" 쪽에 쓴다
- "slave" 쪽에서 프로세스(bash, vim 등)가 stdin/stdout으로 읽고 쓴다
- 프로세스가 쓴 바이트 중 **ANSI 이스케이프 시퀀스**를 해석해 색/커서 이동/화면 클리어 수행
그리고 bash 같은 셸은:
- PTY에서 한 줄 입력을 읽고 파싱
- 파이프/리다이렉션 처리
- fork + exec로 자식 프로세스 실행
- `Ctrl+C`, `Ctrl+Z`를 받아 **프로세스 그룹**에 signal 전달
- 프롬프트(`$`, `%`)를 그리고 history 관리
이 모든 게 합쳐져서 "터미널"이 된다. 이 글에서는 각 층을 하나씩 벗겨보자. 글을 끝까지 읽으면 `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
(터미널이 읽고 씀) (프로세스가 읽고 씀)
- **Master** (`/dev/ptmx`, `/dev/pts/0`의 반대편): 터미널 에뮬레이터가 보유. 여기로 쓰면 slave 쪽에서 stdin으로 읽힘.
- **Slave** (`/dev/pts/0`, `/dev/pts/1`, ...): shell이 stdin/stdout/stderr로 사용.
**비유**: 과거에는 컴퓨터와 터미널이 **시리얼 케이블**로 연결됐다. 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` line-buffered)
- 컬러 출력 활성화
- 프로그레스 바 표시
- 비밀번호 입력 시 echo 끄기
단순히 stdout을 파일로 리디렉션하면 `isatty`가 false를 반환하고 이런 기능이 꺼진다. 반대로 `tmux`/`screen`이 이런 프로그램을 감쌀 수 있는 건, PTY를 중간에 끼우기 때문.
3. termios — 터미널의 설정
3.1 무엇을 제어하는가
`termios` 구조체는 PTY의 **입력/출력/라인 처리** 규칙을 정의한다. 주요 설정:
- **Echo on/off**: 타이핑한 문자를 다시 출력할지
- **Canonical mode on/off**: 줄 단위로 모을지, 한 글자씩 바로 보낼지
- **Signal generation**: `Ctrl+C` → SIGINT 자동 변환 여부
- **Flow control**: `Ctrl+S`/`Ctrl+Q`로 멈추고 재개
3.2 기본 모드 — Canonical (Cooked)
- 사용자가 입력한 문자들을 버퍼에 모음
- `Enter`를 치면 한 줄 전체를 프로세스에 전달
- `Backspace`로 수정 가능 (커널이 처리)
- Echo on → 입력이 화면에 표시
- `Ctrl+C`/`Ctrl+Z` → 시그널 발생
bash 프롬프트에서 명령을 입력하는 평범한 상태.
3.3 Raw 모드
- 문자 단위로 즉시 전달
- Echo off
- 시그널 생성 꺼짐 (프로그램이 직접 `Ctrl+C` 바이트 0x03을 받음)
`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 예시
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 흔한 용도
- **비밀번호 입력**: `stty -echo`
- **터미널 크기 쿼리**: `stty size` → `24 80`
- **cron 스크립트에서 에러**: "stty: standard input: Inappropriate ioctl for device" → PTY 없는 환경에서 터미널 설정 시도
4. ANSI Escape Code — 제어 문자의 세계
4.1 기본 구조
모든 이스케이프 시퀀스는 **`ESC` (0x1B, `\x1b`, `\033`)**로 시작.
- **CSI (Control Sequence Introducer)**: `ESC [` (흔히 `\x1b[`)
- **OSC (Operating System Command)**: `ESC ]`
- **DCS (Device Control String)**: `ESC P`
4.2 색상
$ echo -e "\x1b[31mRed Text\x1b[0m normal"
- `\x1b[31m`: 전경색 빨강
- `\x1b[0m`: 리셋
기본 8색:
| code | 색 |
| ---- | -------- |
| 30 | Black |
| 31 | Red |
| 32 | Green |
| 33 | Yellow |
| 34 | Blue |
| 35 | Magenta |
| 36 | Cyan |
| 37 | White |
배경색은 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 커서 이동
- `\x1b[H` 또는 `\x1b[1;1H`: 홈 (1,1)
- `\x1b[<n>;<m>H`: (n, m)으로 이동
- `\x1b[A` (up), `[B` (down), `[C` (right), `[D` (left): n칸 이동 (숫자 생략 시 1)
- `\x1b[s` / `\x1b[u`: 커서 위치 저장/복원
4.4 화면 제어
- `\x1b[2J`: 전체 화면 지우기
- `\x1b[K`: 현재 줄의 커서 이후 지우기
- `\x1b[?1049h`: Alternate screen buffer 진입 (vim/less가 사용 — 종료 시 원래 화면 복구)
- `\x1b[?1049l`: Alternate screen 종료
4.5 스타일
- `\x1b[1m`: Bold
- `\x1b[3m`: Italic
- `\x1b[4m`: Underline
- `\x1b[5m`: Blink (요즘은 대부분 무시 — "접근성 문제")
- `\x1b[7m`: Reverse video
- `\x1b[9m`: Strikethrough
- `\x1b[0m`: 전부 리셋
4.6 OSC — 터미널 "대화"
- `\x1b]0;My Title\x07`: 창 제목 설정 (BEL = `\x07`로 종료)
- `\x1b]8;;https://example.com\x1b\\Link Text\x1b]8;;\x1b\\`: Hyperlink (2017~)
- `\x1b]52;c;<base64>\x07`: 클립보드에 복사 (tmux, iTerm2 지원)
4.7 Bracketed paste mode
\x1b[?2004h ← 활성화
(붙여넣기 시)
\x1b[200~ ...내용... \x1b[201~
\x1b[?2004l ← 비활성화
셸이 "이건 붙여넣은 내용이야"라고 인지해 **자동 실행되지 않게** 한다. Zsh/Bash 최근 버전에서 기본 활성화.
5. Signal과 Job Control
5.1 시그널 개요
`kill -l`로 전체 목록 확인. 터미널 관련 주요:
| Signal | 번호 | 기본 동작 | 트리거 |
| --------- | ---- | ---------- | ------------------------------------------- |
| SIGINT | 2 | 종료 | `Ctrl+C` |
| SIGQUIT | 3 | core dump | `Ctrl+\` |
| SIGTSTP | 20 | 정지 | `Ctrl+Z` |
| SIGTTIN | 21 | 정지 | background에서 stdin 읽음 |
| SIGTTOU | 22 | 정지 | background에서 stdout 씀 (tostop 옵션 시) |
| SIGHUP | 1 | 종료 | 터미널 연결 끊김 (SSH 단절, 창 닫음) |
| SIGWINCH | 28 | 무시 | 터미널 크기 변경 |
5.2 프로세스 그룹과 세션
Session (login 단위)
├── Session leader (보통 shell)
└── Process groups
├── Foreground group (현재 PTY를 제어)
│ └── e.g., "grep foo | less" 파이프라인
└── Background groups
├── "long-running &"로 실행된 그룹
└── ...
- **`setsid`**: 새 세션 만듦 (daemon 생성)
- **`setpgid`**: 프로세스를 특정 그룹으로 옮김
- **`tcsetpgrp(fd, pgid)`**: 특정 그룹을 foreground로 지정
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 | less` 중 `Ctrl+C`로 grep만 종료하면 less가 EOF 받고 끝나야 정상. 그룹 단위 시그널이 이걸 가능케 한다.
5.4 Job Control
bash/zsh에서:
- `foo &`: background 실행
- `Ctrl+Z`: 현재 foreground 작업 정지 → SIGTSTP
- `jobs`: 작업 목록
- `fg %1`: 1번 작업 foreground로
- `bg %1`: 정지된 1번을 background에서 계속
- `disown %1`: shell의 job table에서 제거 (SIGHUP 안 받음)
- `nohup cmd &`: SIGHUP 무시하고 실행
5.5 SIGHUP와 nohup / tmux
SSH 세션이 끊기면 shell이 SIGHUP를 받고, 셸이 자식들에게 HUP 전파 → 모두 종료. 이걸 피하려면:
- `nohup`: HUP 무시
- `disown`: job table에서 빼서 전파 대상 제외
- `tmux` / `screen`: 별도 세션으로 분리 (shell이 죽어도 tmux 서버가 살아있음)
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
- **Builtin**: shell이 직접 구현 — `cd`, `export`, `echo`, `read`, `source`, `alias`
- cd가 external이면 자식 프로세스의 chdir이라 부모에 반영 안 됨 → 반드시 builtin
- **External**: 별도 바이너리 실행 — `ls`, `grep`, `cat`, `node`
`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
- 0: 성공
- 1~255: 실패. 관례상 128+N은 시그널 N으로 종료 (SIGINT=2 → exit 130)
- `$?`: 최근 명령의 exit
- `set -e`: 실패 시 즉시 스크립트 종료 (pipeline의 마지막만 체크하는 건 함정)
- `set -o pipefail`: pipeline 중간 실패도 감지
6.7 zsh vs bash
- **Zsh**: 2020년 macOS 기본. 더 풍부한 completion, globbing(`**/` recursive, `(.)` modifier), theme (oh-my-zsh, starship)
- **Bash**: Linux 기본. 호환성 최고, POSIX 스크립트에 적합
- **Fish**: non-POSIX, UX 우선 (자동 제안, 타입별 컬러). 스크립트 이식성 나쁨
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 모드 기본)
- `Ctrl+A` / `Ctrl+E`: 줄 처음/끝
- `Ctrl+K`: 커서 이후 잘라내기 (kill)
- `Ctrl+U`: 커서 앞 잘라내기
- `Ctrl+Y`: yank (붙여넣기)
- `Ctrl+R`: history 역검색
- `Ctrl+W`: 단어 단위 삭제
- `Alt+.`: 이전 명령의 마지막 인자 (매우 유용!)
- `Ctrl+L`: 화면 clear
7.2 Vi mode
$ set -o vi # bash
$ bindkey -v # zsh
`Esc`로 normal mode, `i`로 insert. 명령줄에서 vim 매크로급 편집 가능.
7.3 History
- 기본: `~/.bash_history` 또는 `~/.zsh_history`
- 세션 종료 시 저장 (bash) vs 즉시 append (zsh, `setopt inc_append_history`)
- `history -c`: 메모리에서만 삭제
- `HISTIGNORE="ls:pwd"`: 저장 안 할 패턴
- `HISTCONTROL=ignoreboth`: 중복/공백 시작 무시
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/포트 포워딩
- `-X`: X11 forwarding (GUI 앱)
- `-L`: 로컬 포트를 원격으로
- `-R`: 원격 포트를 로컬로
- `-D`: 동적 SOCKS proxy
9. tmux / screen — Terminal Multiplexer
9.1 왜 필요한가
- 한 SSH 세션에서 여러 "가상 창"
- SSH 끊겨도 서버 쪽 세션 유지 (attach/detach)
- 창 분할(split), 창 전환(window)
- 복사/붙여넣기 버퍼 공유
9.2 구조
tmux server (daemon, 호스트당 1개)
└── sessions
├── session "main"
│ ├── window 0 (several panes)
│ ├── window 1
│ └── ...
└── session "work"
└── ...
클라이언트는 attach로 session에 붙고, detach로 떨어짐.
서버는 클라이언트 없어도 계속 실행.
9.3 tmux가 PTY를 쓰는 방식
- 각 pane이 별도 PTY
- tmux는 **bytepile forwarder + terminal emulator 일부 기능**을 구현
- pane 안 프로그램이 보는 건 tmux가 만든 PTY → 실제 터미널 크기와 무관하게 관리 가능
9.4 단축키 (기본 prefix `Ctrl+B`)
- `prefix c`: 새 window
- `prefix %`: 좌우 split
- `prefix "`: 상하 split
- `prefix d`: detach
- `tmux ls`: session 목록
- `tmux attach -t main`: 재접속
10. 터미널의 복사/붙여넣기
10.1 왜 `Ctrl+C`가 복사가 아닌가
`Ctrl+C`는 오래전부터 SIGINT 의미로 고정. 터미널 복사는 관례적으로:
- Linux xterm: 자동 primary selection (드래그)
- macOS Terminal/iTerm: `Cmd+C`
- Windows Terminal: `Ctrl+Shift+C`
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에서 클립보드
일반적으로 서버가 클라이언트 클립보드에 접근 불가. 해결:
- OSC 52 지원 (iTerm2, Windows Terminal, WezTerm 일부 버전)
- X11 forwarding + `xclip`
- mosh + clipboard script
11. 현대 터미널 에뮬레이터 비교
11.1 주요 선수
- **iTerm2 (macOS)**: 가장 풍부한 기능, tabs, splits, triggers, shell integration
- **Alacritty**: Rust, GPU 가속, 설정 최소화, 매우 빠름
- **WezTerm**: Rust, 최고 수준 기능 + GPU + scripting
- **Kitty**: Python 설정, GPU, graphics 프로토콜 (이미지 인라인)
- **Windows Terminal**: MS 공식, WSL 친화
- **Warp**: Rust + AI 기능. 2025년 급부상
- **Ghostty**: 2024년 Mitchell Hashimoto 발표, zig 기반
11.2 성능 차이
단순 문자 렌더링은 차이 없어 보이지만:
- `cat huge-log-file`처럼 초당 수십만 줄 출력 시 → Alacritty/Kitty/WezTerm이 Terminal.app보다 10배+ 빠름
- `tmux` 내 세션 전환 → GPU 가속 있는 쪽이 부드러움
- RAM 사용: Electron 기반 터미널(Hyper 등)이 가장 많이 먹음
11.3 Graphics 프로토콜
- **Kitty image protocol**: 인라인 이미지 표시
- **Sixel**: 오래된 pixel 프로토콜, iTerm2/WezTerm/Kitty 지원 확대
- **iTerm2 inline images**: iTerm2 고유
`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 테마 도구
- **base16**: 16색 팔레트 표준, 200+ 테마
- **Dracula**, **Solarized**, **Nord**, **Gruvbox**: 인기 팔레트
- **starship / powerlevel10k**: 프롬프트 테마
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 `[[`
- `[`: POSIX, 실제로 별도 바이너리 `/usr/bin/[`
- `[[`: bash/zsh 확장, `&&`/`||` 사용, 단어 분리 안 함
[[ -f "$file" && -r "$file" ]] # bash OK
[ -f "$file" -a -r "$file" ] # POSIX
13.4 `$*` vs `$@`
- `"$@"`: 각 인자 separately (거의 항상 원하는 것)
- `"$*"`: 한 문자열 결합
- `$@` (쿼트 없음): 위험. 공백 있는 인자 쪼개짐
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
- 2005 첫 릴리스, 2023 Rust 재구현
- non-POSIX (스크립트 이식성 낮음)
- Out-of-the-box 자동 제안, 히스토리 기반 completion
- 신규 사용자 UX 최고
14.2 nushell
- 2019, Rust
- 구조화된 데이터가 first-class (JSON/CSV/YAML을 테이블로)
- SQL-like 파이프라인: `ls | where size > 1MB | sort-by modified`
- Unix philosophy에 대한 재해석 — "all text"가 아니라 "structured records"
14.3 xonsh
- Python + shell. `ls | [x.upper() for x in _]` 같은 혼합
- 데이터 분석가 친화
14.4 oil shell (osh/ysh)
- POSIX 호환이면서 안전한 syntax 옵션
- 한 번에 옮기기 쉬운 "next-gen bash"
**현실**: 2025년 생산성 터미널은 **starship 프롬프트 + fish 또는 zsh(oh-my-zsh/p10k)**가 주류. nushell은 특수 워크로드에 실험적으로 사용.
15. Terminal의 미래
15.1 AI 통합
- **Warp**: AI command suggestion, 자연어 → shell command
- **Fig/Amazon Q**: 자동완성 확장
- **GitHub Copilot CLI**: `gh copilot explain`, `suggest`
15.2 GPU/rich rendering
- Kitty graphics, Sixel 확산
- Ghostty 같은 zig 구현으로 성능 극한 추구
- 터미널에서 차트/이미지 인라인 표시 늘어남
15.3 Shell 세션의 공유
- **tmate**: tmux fork, 세션 URL로 공유
- **Zellij**: Rust multiplexer + 멀티유저
- **upterm**: `upterm session create`로 페어 프로그래밍
15.4 Web-based terminals
- **xterm.js**: VS Code, JupyterLab, Grafana 등에 내장
- SSH 없이도 브라우저에서 컨테이너 터미널 접속
16. 실전 팁
디버깅
- `ps -ef` / `ps aux`: 프로세스 목록
- `ps -ejH` 또는 `pstree`: 트리 구조 (프로세스 그룹/세션 구분)
- `ps -o pid,ppid,pgid,sid,cmd`: PID/PPID/PGID/SID 보기
- `lsof -p <pid>`: 프로세스의 열린 파일/fd
- `strace -p <pid>`: 시스템 콜 추적
- `tty`: 현재 터미널 경로 (`/dev/pts/0` 등)
생산성
- `alt+.`로 이전 명령 마지막 인자 가져오기
- `Ctrl+R` history 역검색 (fzf 연동하면 더 강력)
- `!!`: 직전 명령
- `!$`: 직전 명령 마지막 인자
- `^foo^bar`: 직전 명령의 foo를 bar로 바꿔 재실행
- `sudo !!`: 직전 명령 sudo로 재실행
생존
- `tmux new -s main` → 항상 tmux에서 작업
- `ls --color=auto | cat`은 파이프에서 색 안 나옴 (정상)
- `script typescript.log cmd`: 터미널 전체 세션 녹화
마무리 — 50년 된 추상화의 힘
VT100(1978) 프로토콜이 2025년에도 살아있다는 건 놀라운 일이다. 그 이유는 단순하다: **바이트 스트림 + 이스케이프 시퀀스**라는 단순한 인터페이스가 "텍스트 I/O의 보편 언어"가 됐기 때문. 그 위에 tmux, ssh, Docker exec, VS Code terminal, 그리고 Warp 같은 AI 터미널까지 모두 똑같은 계층을 재활용한다.
개발자가 터미널을 "제대로" 이해하면:
- **SSH 끊기면 작업 날아가는 문제**를 tmux로 해결
- **Ctrl+C가 안 먹는 프로그램**은 raw mode라는 걸 앎
- **`stty: Inappropriate ioctl`** 에러는 PTY 없는 환경 → cron/CI에서 `isatty` 체크
- **vim이 종료 후 화면이 지워지는 이유**는 alternate screen buffer
- **color가 파이프에서 사라지는 이유**는 `isatty(1)` 체크 때문
이 글의 교훈 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년의 소프트웨어 진화가 누적된 살아있는 박물관**이다. 그 박물관의 구조를 이해하는 순간, 매일 하는 작업이 훨씬 덜 신비롭고 훨씬 더 흥미로워진다.
현재 단락 (1/377)
매일 쓰는 iTerm, Windows Terminal, VS Code terminal. 거기에 `ls`를 치면 파일 목록이 나오고, `Ctrl+C`로 실행 중인 프로세스를 멈추고,...