Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — 까만 창의 정체

매일 쓰는 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`로 실행 중인 프로세스를 멈추고,...

작성 글자: 0원문 글자: 14,585작성 단락: 0/377