포스트

curses와 ncurses — 터미널 제어가 화면·입력 추상화로 올라온 순간

ANSI/VT 제어 시퀀스와 terminfo를 직접 다루던 단계에서 curses/ncurses가 화면 상태, refresh, 입력, Window 같은 공통 추상화를 제공한 의미를 정리한다.

curses와 ncurses — 터미널 제어가 화면·입력 추상화로 올라온 순간

앞 단계까지는 애플리케이션이 터미널 제어에 꽤 가까이 붙어 있었다.

1
2
3
4
5
애플리케이션
  ↓
ANSI/VT 제어 시퀀스
  ↓
터미널 에뮬레이터

또 필요하면 TERM과 terminfo를 이용해 현재 터미널 인터페이스에 맞는 제어 시퀀스를 선택할 수 있었다.

하지만 개발자 입장에서는 여전히 저수준이다.

화면에 무엇을 보여줄지 결정하고 싶은데, 왜 매번 커서 이동·화면 지우기·입력 시퀀스를 직접 다뤄야 할까?

이 반복되는 처리를 공통 라이브러리로 끌어올린 대표적인 고전 TUI 추상화가 curses다.

한마디로 말하면:

curses는 애플리케이션이 ANSI/VT 제어 시퀀스와 원시 입력 바이트를 직접 다루지 않아도 되게 해주는 고전적인 TUI API/추상화 계열이다.

curses를 모든 TUI의 시초라고 할 수는 없다. 초기 vi처럼 자체 화면 처리 계층을 가진 프로그램도 있었다. 중요한 점은 curses가 여러 프로그램이 각자 구현하던 터미널 UI 처리 패턴을 공통 API로 제공했다는 것이다.

전체 구조

1
2
3
4
5
6
7
8
9
10
애플리케이션
  ↓
curses / ncurses
  ├─ 화면 상태 관리
  ├─ 입력 해석
  └─ Window 같은 편의 추상화
  ↓
terminfo / termios / 제어 시퀀스
  ↓
터미널 에뮬레이터

curses는 terminfo, termios, ANSI/VT를 없애는 것이 아니다. 그 위에서 애플리케이션이 더 높은 수준으로 생각할 수 있게 해준다.

예를 들어 애플리케이션은 다음처럼 사용할 수 있다.

1
2
3
4
5
initscr();
mvaddstr(5, 10, "hello");
refresh();
getch();
endwin();

대략적인 의미는 다음과 같다.

1
2
3
4
5
initscr()  → 화면 처리 초기화
mvaddstr() → 원하는 위치에 문자열 기록
refresh()  → 화면 상태를 실제 출력에 반영
getch()    → 입력 받기
endwin()   → 터미널 상태 복구

1. 출력 추상화 — 제어 시퀀스 대신 화면 상태를 다룬다

직접 ANSI/VT를 사용하면 개발자는 이런 식으로 생각해야 한다.

1
2
3
커서를 5행 10열로 옮긴다
→ 문자열을 출력한다
→ 필요한 속성을 복구한다

curses를 사용하면 관심사가 바뀐다.

1
화면 5행 10열에 "hello"가 보이게 한다

즉 애플리케이션은 어떤 제어 시퀀스를 보낼지보다 어떤 화면 상태를 만들지를 다룬다.

1
2
3
4
5
직접 제어
= 명령 중심

curses
= 화면 상태 중심

이 변화가 curses의 가장 중요한 의미다.

2. refresh — 화면 상태를 실제 출력으로 반영한다

mvaddstr()를 호출했다고 해서 반드시 그 순간 모든 바이트를 바로 출력해야 하는 것은 아니다.

curses 계열 구현은 내부에 애플리케이션이 원하는 화면 상태를 관리하고, refresh() 시점에 현재 표시 상태와 비교해 필요한 변경을 실제 출력으로 반영한다.

1
2
3
4
5
6
7
8
9
애플리케이션이 원하는 화면
        ↓
curses 계열 구현의 내부 화면 모델
        ↓ 비교
구현이 추적하는 현재 표시 상태
        ↓
필요한 제어 시퀀스 출력
        ↓
터미널 에뮬레이터

즉 refresh()는 단순히 “출력해라”보다:

내부 화면 상태를 실제 표시 상태와 맞춰라

에 가깝다.

화면 한 글자만 바뀌었다면 전체 화면을 지우고 다시 그리는 대신, 필요한 위치로 커서를 이동해 바뀐 문자만 출력할 수 있다.

1
2
3
4
5
6
7
8
9
기존에 앱이 직접 하던 일
- 현재 화면 상태 기억
- 새 화면과 비교
- 변경 위치 계산
- 적절한 제어 시퀀스 출력

curses 사용
- 앱은 원하는 화면 상태를 변경
- refresh가 실제 반영을 담당

3. 입력 추상화 — 바이트 시퀀스를 논리적 키로 바꾼다

출력뿐 아니라 입력도 비슷하다.

방향키 같은 특수키는 터미널 에뮬레이터에서 여러 바이트의 시퀀스로 들어올 수 있다.

1
2
3
4
5
6
7
8
9
10
11
위쪽 방향키
  ↓
터미널 에뮬레이터
  ↓
ESC [ A 같은 입력 시퀀스
  ↓
curses 입력 처리
  ↓
KEY_UP
  ↓
애플리케이션

curses 없이 처리한다면 애플리케이션이 ESC, [, A 같은 바이트를 묶어 직접 “위쪽 방향키”라고 해석해야 한다.

curses를 사용하면 애플리케이션은 논리적인 키 값으로 받을 수 있다.

1
2
3
4
int ch = getch();
if (ch == KEY_UP) {
    ...
}

즉 역할을 비교하면:

1
2
3
4
5
termios
= 입력을 어떤 방식으로 앱 쪽에 전달할지 결정

curses
= 전달된 입력 바이트를 앱이 쓰기 좋은 키 의미로 해석

curses는 raw(), cbreak(), noecho() 같은 API를 통해 아래의 termios 설정도 조정할 수 있다.

1
2
3
4
5
curses API
  ↓
termios 설정
  ↓
PTY / TTY 터미널 드라이버

4. Window — 영역 관리 패턴을 공통 API로 제공한다

Window는 curses가 화면 분할 자체를 새로 가능하게 만든 기능은 아니다.

curses 이전에도 애플리케이션이 직접 다음 같은 구조를 만들 수 있었다.

1
2
3
영역 시작 위치
영역 크기
영역 내부 좌표계

그리고 직접 좌표를 계산하고 화면 상태를 관리하면 됐다.

curses의 WINDOW가 해준 일은 이런 반복되는 영역 관리 패턴을 라이브러리의 공통 추상화로 제공한 것이다.

1
2
3
4
5
Window 생성
= "화면의 이 직사각형 영역을 하나의 논리적 영역으로 쓰겠다"

이후
= 그 Window의 (0,0)을 기준으로 좌표 사용

예를 들어 애플리케이션이 임의로 다음처럼 영역을 나누고 싶다고 하자.

1
2
3
4
5
6
앱이 정한 예시 레이아웃
┌────────────────────────────┐
│ Window A                   │
├─────────┬──────────────────┤
│ Window B│ Window C         │
└─────────┴──────────────────┘

이 배치는 curses가 미리 정해둔 것이 아니다. Window의 위치, 크기, 개수, 용도는 애플리케이션이 결정한다.

Window의 장점은 한 번 영역을 정의하면 그 영역의 위치·크기·좌표계를 curses가 관리해준다는 데 있다.

1
2
3
4
5
6
기존
= 앱이 직접 영역 구조체, 좌표 변환, 갱신 로직 관리

curses Window
= 영역을 Window 객체로 만들고
  Window 기준 좌표로 그리기

즉 Window는 curses의 본질이라기보다 화면 영역 관리까지 공통화한 편의 추상화라고 보면 된다.

ncurses는 무엇인가

curses는 고전적인 터미널 UI API/추상화 계열이고, ncurses는 오늘날 Unix/Linux 계열에서 널리 사용되는 대표적인 구현이다.

1
2
3
4
5
curses
= API / 추상화 계열

ncurses
= 널리 쓰이는 구현

ncurses만 유일한 구현은 아니다. PDCurses나 여러 Unix 계열의 curses 구현처럼 다른 구현도 있다.

중요한 점은 현대 TUI 프레임워크들이 보통 curses API를 구현한 구현체가 아니라는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
curses 계열
애플리케이션
→ curses API
→ ncurses 같은 구현
→ terminfo / termios / 제어 시퀀스
→ 터미널 에뮬레이터

현대 TUI 프레임워크
애플리케이션
→ State / Event / Layout / Render 같은 자체 API
→ 자체 렌더러·백엔드
→ 터미널 제어

즉 Bubble Tea, Ratatui, Textual, OpenTUI 같은 현대 TUI는 대체로 curses를 구현한 것이 아니라, curses가 풀던 터미널 UI 문제를 더 높은 수준의 구조로 다시 추상화한 별도 계열이라고 보면 된다.

여기까지가 핵심

처음 읽을 때는 다음만 기억하면 충분하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
curses
= 앱이 직접 하던 반복적인 터미널 UI 처리를 공통 라이브러리 API로 올린 고전적 방식

ncurses
= curses API의 대표 구현

현대 TUI
= 보통 curses 구현체가 아니라,
  같은 문제를 더 높은 수준에서 다시 추상화한 별도 계열

출력
= ANSI/VT 제어 시퀀스 대신 화면 상태를 다룸

refresh
= 내부 화면 상태를 실제 표시 상태에 반영

입력
= 원시 입력 시퀀스를 KEY_UP 같은 논리적 키로 해석

Window
= 앱이 직접 만들던 영역 관리 패턴을 공통 API로 제공

여기부터는 심화 — 화면 갱신 최적화

가상 화면과 현재 화면

ncurses 내부 개념을 단순화하면 두 상태를 생각할 수 있다.

1
2
3
4
5
가상 화면
= 애플리케이션이 다음에 보여주고 싶은 상태

현재 화면 모델
= ncurses가 알고 있는 현재 표시 상태

refresh() 계열은 둘을 비교해 필요한 변경만 제어 시퀀스로 출력한다.

1
전체 화면 지우기 + 재출력

보다:

1
2
변경된 위치로 커서 이동
→ 바뀐 문자만 출력

하는 편이 효율적이다.

이 차이 기반 렌더링은 현대 TUI 렌더러에서도 반복되는 아이디어다.

여러 Window를 한 번에 갱신하기

ncurses는 wnoutrefresh()와 doupdate() 같은 API로 여러 Window의 변경을 내부 화면에 먼저 모은 뒤 실제 출력을 한 번에 수행할 수 있다.

1
2
3
4
5
6
7
8
9
Window A 변경
→ 내부 화면 반영

Window B 변경
→ 내부 화면 반영

마지막
→ 필요한 제어 시퀀스를 한 번에 출력
→ 터미널 에뮬레이터

처음에는 API 이름보다 여러 UI 변경을 내부 화면에 모으고 실제 출력은 나중에 최적화할 수 있다는 점만 보면 된다.

vi와 curses의 관계를 단순화하면 안 된다

초기 vi는 ncurses보다 오래됐고, termcap 같은 capability 계층을 활용하면서 자체 화면 처리를 했다.

1
2
3
초기 화면 편집기
  ├─ termcap 등을 이용한 자체 화면 처리
  └─ curses 같은 범용 화면 추상화의 발전

따라서 모든 오래된 TUI가 curses 위에 있었던 것은 아니다.

현대에도 fzf, Neovim, btop처럼 자체 렌더링 계층을 가진 프로그램이 있다.

curses가 제공한 추상화의 위치

지금까지를 연결하면:

1
2
3
4
5
6
7
8
9
10
11
12
ANSI / VT
"어떤 제어 시퀀스를 보낼까?"

        ↓

TERM / terminfo
"현재 터미널 인터페이스에서는 어떤 시퀀스를 써야 할까?"

        ↓

curses
"어떤 화면 상태와 입력을 다룰까?"

즉 관심사가 터미널 명령 → capability → UI 화면·입력 상태로 올라왔다.

그런데 curses도 애플리케이션 구조까지 책임지지는 않는다

curses가 화면과 입력을 추상화해도 다음 같은 고수준 구조까지 자동으로 만들어주는 것은 아니다.

1
2
3
4
5
애플리케이션 상태
이벤트 루프
컴포넌트 구조
레이아웃 정책
비동기 작업 통합

그래서 복잡한 TUI에서는 curses 위에 다시 자체 애플리케이션 구조를 만들거나, 더 높은 수준의 현대 TUI 프레임워크를 사용한다.

다음 단계 — TUI 엔진

다음에는 어떤 TUI 프레임워크를 쓰더라도 반복해서 등장하는 공통 구조를 본다.

1
2
3
4
5
6
7
8
9
10
11
입력
  ↓
이벤트 루프
  ↓
상태
  ↓
레이아웃
  ↓
렌더링
  ↓
터미널 에뮬레이터

이 구조를 먼저 알면 Bubble Tea, Ratatui, Textual, OpenTUI의 차이도 훨씬 쉽게 비교할 수 있다.

참고

  • ncurses manual — curses screen handling과 terminal I/O
  • terminfo(5) — curses가 사용하는 terminal capability database
  • ncurses refresh 계열 API — virtual/physical screen 갱신
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.