기본 콘텐츠로 건너뛰기

REPL Resize Fix

thumbnail

Python REPL에서 창 크기 바꾸면 화면이 깨질 때: CPython의 ‘리사이즈 이벤트’ 처리 방식이 바뀐 이유

meta_description: CPython PR #146459(gh-146458)는 Windows에서 콘솔 창 리사이즈 시 REPL의 높이/너비 추적이 틀어지는 문제를 수정했다. 핵심은 SIGWINCH 핸들러에서 즉시 크기를 갱신하던 방식을 바꾸고, refresh 단계에서 getheightwidth()로 화면 크기를 다시 계산하도록 만든 것이다. 이 글은 왜 이벤트 핸들러에서 상태를 미리 바꾸면 문제가 생기는지, pyrepl 기반 REPL에서 “resize” 이벤트가 어떻게 흘러가는지, 그리고 터미널 UI/CLI 도구를 만드는 개발자가 적용할 수 있는 실전 체크리스트를 정리한다. meta_keywords: python,repl,pyrepl,console,windows,resize,sigwinch,event queue,terminal ui,tty,refresh,rendering,bugfix,regression,test,mock,height,width meta_robots: index,follow

REPL이나 터미널 기반 CLI를 만들다 보면, 은근히 사람을 미치게 하는 버그가 있다.

  • 창 크기 바꾸면 화면이 깨짐
  • 커서 위치가 튐
  • 줄바꿈이 꼬여서 입력이 보이지 않음

특히 Windows 콘솔은 환경이 다양해서(기본 콘솔/Windows Terminal 등),

  • “내 컴퓨터에서는 되는데”

가 자주 나온다.

최근 CPython에 들어간 PR #146459(gh-146458)는 딱 그 지점을 고쳤다.

  • 콘솔 리사이즈에서 REPL의 height/width 추적이 틀어지는 문제

이번 글은 단순 ‘버그 픽스’ 소개가 아니라,

  • 왜 이런 버그가 생기는지
  • 터미널 UI를 만드는 입장에서 어떤 패턴이 안전한지

를 실무적으로 정리한다.


1) PR #146459가 바꾼 핵심: “리사이즈 이벤트”와 “크기 갱신”을 분리

diff를 보면 핵심 변경은 2줄로 요약된다.

1) 리사이즈 신호/이벤트가 왔을 때는 - 이벤트 큐에 resize 이벤트만 넣고 - height/width를 그 자리에서 바꾸지 않는다

2) 실제 화면을 다시 그릴 때(refresh) - getheightwidth()로 현재 콘솔 크기를 다시 읽고 - 그 값으로 렌더링을 계산한다

즉,

  • 이벤트 핸들러(비동기)에선 상태를 “예약”만 하고
  • 렌더링 단계(동기)에선 상태를 “확정”한다

이 구조가 된 것이다.


2) 왜 이벤트 핸들러에서 height/width를 미리 바꾸면 위험할까

터미널 UI는 보통 이런 3단계를 가진다.

  • 입력/이벤트 수집
  • 상태 업데이트
  • 렌더링

문제는 리사이즈가 이 중간 어디서든 끼어들 수 있다는 점이다.

리사이즈 핸들러에서 height/width를 즉시 바꿔버리면,

  • 아직 이전 프레임(screen)이 남아있는 상태에서
  • 새 크기 기준으로 커서/오프셋을 계산하게 되고
  • 결과적으로 화면이 “부분적으로” 꼬일 수 있다

이게 “가끔만 터지는” 이유다.

  • 타이밍 이슈

그래서 실무에서는 보통 이런 규칙이 안전하다.

  • 비동기 이벤트에서는 ‘플래그/이벤트’만 쌓고
  • 실제 계산은 렌더링 루프에서 한 번에

PR #146459는 CPython REPL에도 그 원칙을 적용한 셈이다.


3) pyrepl의 resize 이벤트 흐름을 운영자 시선으로 보면

pyrepl 쪽 구조를 아주 단순화하면:

  • 콘솔에서 이벤트를 읽어서 event_queue에 넣는다
  • reader가 event를 처리하면서 screen을 계산한다
  • console.refresh가 실제 출력/커서 이동을 한다

여기서 리사이즈는 “입력 이벤트” 중 하나다.

이번 변경으로,

  • 리사이즈 이벤트가 들어오면
  • refresh가 호출되는 순간에
  • 콘솔 크기를 다시 읽는다

즉, 프레임 경계에서만 크기가 갱신된다.

이건 터미널 UI에서 흔히 쓰는 패턴이다.

  • 프레임 시작 시: 입력 스냅샷
  • 프레임 렌더 시: 크기 스냅샷

4) 터미널 UI/CLI 개발자가 바로 적용할 수 있는 체크리스트 7개

이 PR을 읽고, 내 코드에 바로 적용할 수 있는 체크리스트를 정리한다.

1) 리사이즈 핸들러에서 “렌더링 상태”를 직접 건드리지 않기 2) 리사이즈는 이벤트 큐에 넣고, 루프에서 처리하기 3) refresh 직전에 현재 크기를 다시 읽기(가능하면) 4) 화면 계산(calc_screen)과 출력(refresh)을 분리하기 5) 커서 위치(cxy)처럼 깨지기 쉬운 값은 “calc_screen이 만든다”를 원칙으로 6) 테스트에서 height/width는 하드코딩보다 getheightwidth()를 mock으로 주입하기 7) Windows/Unix 둘 다에서 리사이즈 케이스를 최소 2개(넓어짐/좁아짐) 고정 테스트로

PR diff에서도 테스트가 getheightwidth를 side_effect로 바꿔, 리사이즈 상태를 더 명확히 시뮬레이션하도록 정리된 게 보인다.


5) 실전 디버깅: ‘화면 깨짐’ 리포트가 왔을 때 바로 물어볼 6가지

터미널 UI 이슈는 재현이 어려워서, 질문을 잘해야 시간이 줄어든다.

내가 바로 물어보는 건 아래 6개다.

1) 어떤 OS/터미널인가? - Windows Terminal / 기본 콘솔 / VSCode 내장 터미널 / WSL 등

2) 폰트/배율/HiDPI 설정이 있나? - 배율이 들어가면 리사이즈 이벤트가 더 자주(혹은 이상하게) 들어오기도 한다.

3) 창을 “드래그로 늘리는 중”에도 깨지나, “늘린 뒤”에만 깨지나? - 전자는 프레임 경계/이벤트 폭주 이슈일 가능성이 크다.

4) 멀티라인 입력에서만 깨지나? - pyrepl 테스트도 멀티라인 함수 입력 케이스를 따로 둔다.

5) 깨진 뒤에 ‘Enter’를 한 번 치면 복구되나? - 복구되면 refresh 타이밍이 힌트다.

6) 화면이 깨질 때 커서 좌표가 어긋나는가, 내용이 잘리는가? - 좌표 문제면 cxy/offset 계열 - 잘림 문제면 height/width 스냅샷 문제

이 질문 6개만으로도 “어디를 봐야 하는지”가 크게 좁혀진다.

그리고 재현이 되기 시작하면, 그 다음은 ‘최소 재현 스크립트’를 만드는 게 가장 빠르다.

예:

  • 멀티라인 입력을 일부러 만들기
  • 리사이즈를 넓게/좁게 각각 한 번씩 만들기
  • 마지막에 화면 스냅샷(출력)만 남기기

터미널 UI 버그는 복잡한 서비스 코드 전체에서 찾는 것보다, 입력 시퀀스를 고정해두는 게 효율이 훨씬 좋다.


6) 실전 적용: 내 CLI/REPL에 ‘resize 안정성’을 넣는 가장 작은 변경

이 PR에서 가져올 수 있는 가장 작은 변화는 이거다.

  • 렌더링 직전에 “현재 화면 크기”를 다시 읽는다

예를 들어 구조가 이런 식이라면:

# pseudo-code
while True:
    ev = get_event()
    handle_event(ev)
    screen, cursor = calc_screen()
    refresh(screen, cursor)

여기서 refresh 직전에만 크기를 갱신해도, 리사이즈로 인한 화면 깨짐이 크게 줄어든다.

# pseudo-code
while True:
    ev = get_event()
    handle_event(ev)

    height, width = getheightwidth()
    screen, cursor = calc_screen(height, width)
    refresh(screen, cursor)

물론 실제 코드는 더 복잡하지만, 요지는 동일하다.

  • 계산에 쓰는 크기 스냅샷을 프레임 경계로 옮겨라

이 원칙만 잡아도, “가끔 깨짐” 같은 타이밍 버그의 대부분이 사라진다.


6) 결론: 리사이즈 버그는 “계산 시점”을 고치면 대부분 해결된다

창 크기 바뀔 때 REPL이 깨지는 버그는,

  • 출력 코드가 나쁘다

기보다는,

  • 상태를 언제 갱신하느냐

에서 많이 터진다.

PR #146459는 그걸 “이벤트에서는 예약, refresh에서 확정”으로 바꿨다.

터미널 UI를 만드는 입장에선,

  • 리사이즈를 ‘특수 케이스’로 때우지 말고
  • 프레임 경계로 흡수하라

는 교훈을 준다.


Sources

  • CPython PR #146459 — gh-146458: Fix repl height and width tracking on resize
    • https://github.com/python/cpython/pull/146459

Keywords

python,repl,pyrepl,console,windows,resize,sigwinch,event queue,terminal ui,tty,refresh,rendering,bugfix,regression,test,mock,height,width


이미지 크레딧/라이선스

  • Looking at computer and phone (Unsplash).jpg — Tim Gouw / CC0
    • https://commons.wikimedia.org/wiki/File:Looking_at_computer_and_phone_(Unsplash).jpg
    • http://creativecommons.org/publicdomain/zero/1.0/deed.en
  • Laptop coding programs (Unsplash).jpg — Christopher Gower / CC0
    • https://commons.wikimedia.org/wiki/File:Laptop_coding_programs_(Unsplash).jpg
    • http://creativecommons.org/publicdomain/zero/1.0/deed.en

댓글

이 블로그의 인기 게시물

Django에서 트랜잭션 관리하기

Django에서 트랜잭션 관리하기 안녕하세요! 오늘은 Django에서 데이터베이스 트랜잭션을 효과적으로 관리하는 방법에 대해 알아보겠습니다. 1. 트랜잭션의 중요성 트랜잭션은 데이터베이스의 일관성과 무결성을 보장하는 중요한 개념입니다. Django에서는 여러 가지 방법으로 트랜잭션을 관리할 수 있습니다. 1.1 기본 개념 원자성(Atomicity) : 트랜잭션은 모두 실행되거나 모두 실행되지 않아야 합니다. 일관성(Consistency) : 트랜잭션 전후로 데이터베이스의 일관성이 유지되어야 합니다. 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않아야 합니다. 지속성(Durability) : 완료된 트랜잭션의 결과는 영구적으로 저장되어야 합니다. 2. Django의 트랜잭션 관리 2.1 기본 설정 # settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mydatabase', 'USER': 'myuser', 'PASSWORD': 'mypassword', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, # 모든 뷰를 트랜잭션으로 래핑 } } 2.2 데코레이터 사용 from django.db import transaction @transaction.atomic def create_order(user, items): order = Order.objects.create(user=...

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드 안녕하세요! 오늘은 AWS S3와 CloudFront를 사용하여 정적 파일을 효율적으로 서빙하는 방법에 대해 알아보겠습니다. 왜 S3와 CloudFront를 사용할까요? 높은 가용성 : AWS의 글로벌 인프라를 활용 빠른 전송 속도 : CloudFront의 CDN 기능으로 전 세계 사용자에게 빠른 전송 비용 효율성 : 사용한 만큼만 지불 보안 : AWS의 보안 기능 활용 확장성 : 트래픽 증가에 자동 대응 1. S3 버킷 설정 1.1 버킷 생성 및 설정 import boto3 def create_s3_bucket(): s3 = boto3.client('s3') # 버킷 생성 bucket_name = 'your-static-files-bucket' s3.create_bucket( Bucket=bucket_name, CreateBucketConfiguration={ 'LocationConstraint': 'ap-northeast-2' } ) # 버킷 정책 설정 bucket_policy = { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObje...

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁 안녕하세요! 오늘은 AWS RDS를 사용하는 Django 애플리케이션의 성능을 최적화하는 방법에 대해 알아보겠습니다. 1. RDS 인스턴스 최적화 1.1 인스턴스 타입 선택 # RDS 인스턴스 크기 조정 import boto3 def resize_rds_instance(): rds = boto3.client('rds') response = rds.modify_db_instance( DBInstanceIdentifier='your-db', DBInstanceClass='db.t3.large', # 워크로드에 맞는 인스턴스 타입 선택 ApplyImmediately=True ) return response['DBInstance'] 1.2 파라미터 그룹 설정 def create_parameter_group(): rds = boto3.client('rds') # PostgreSQL 파라미터 그룹 생성 response = rds.create_db_parameter_group( DBParameterGroupName='django-optimized', DBParameterGroupFamily='postgres13', Description='Optimized parameters for Django applications' ) # 성능 관련 파라미터 설정 parameters = [ { 'ParameterName': 'shared_buffers', 'ParameterValue': '2GB...