[해결 방법] [Python] 콘솔에서 X Y 좌표에 맞춰 문자 출력하기: ANSI 이스케이프 코드 활용 가이드

안녕하세요! 파이썬 개발자 여러분, 콘솔 환경에서 특정 X, Y 좌표에 문자를 정확히 출력하여 시각적인 형태를 만들고 싶었지만 어려움을 겪으셨나요? 표준 print() 함수만으로는 원하는 위치에 문자를 그리는 것이 쉽지 않습니다. 이번 글에서는 Stack Overflow에서 제기된 문제 상황을 분석하고, 파이썬에서 ANSI 이스케이프 코드를 활용하여 콘솔에 정확한 좌표에 문자를 그리는 방법을 자세히 알아보겠습니다. 이 방법을 통해 여러분의 콘솔 애플리케이션에 새로운 시각적 경험을 더할 수 있을 것입니다.

1. 문제 발생 상황

한 개발자 분이 구글 문서에서 테이블 형태의 데이터를 스크래핑하여 X, Y 좌표와 해당 위치에 출력할 문자를 딕셔너리 형태로 저장했습니다. 목표는 이 딕셔너리 데이터를 기반으로 콘솔에 특정 문자(예: ‘F’ 형태)를 그려내는 것이었습니다. 하지만 일반적인 print() 함수만으로는 각 문자를 원하는 좌표에 정확히 위치시키는 데 어려움을 겪고 있었습니다.

원본 코드 예시

import requests
from bs4 import BeautifulSoup

URL = "https://docs.google.com/document/d/e/2PACX-1vTMOmshQe8YvaRXi6gEPKKlsC6UpFJSMAk4mQjLm_u1gmHdVVTaeh7nBNFBRlui0sTZ-snGwZM4DBCT/pub"

data = requests.get(URL).text

# parse data with external library BeautifulSoup
soup = BeautifulSoup(data, "lxml")

# find the table and assign it to a variable (assumed to be first table in form)
table = soup.find_all("table")[0]

# extract rows
rows = table.find_all("tr")

# remove first row because its just labels for the data
rows.pop(0)

data_formatted = list()

# extract data from each row
for i in rows:
    table_data = i.find_all("td")
    for j in table_data:
        data_formatted.append(j.text)


data_formatted_dict = dict()

# format each x y coordinate and char into their own key value pair
for i in range(0, len(data_formatted), 3):
    data_formatted_dict[int(data_formatted[i]), int(data_formatted[i + 2])] = (
        data_formatted[i + 1]
    )

print(data_formatted_dict)

문제 딕셔너리 출력 결과

위 코드를 실행하면 다음과 같은 딕셔너리 형태로 데이터가 잘 준비됩니다.

{(0, 0): '█', (0, 1): '█', (0, 2): '█', (1, 1): '▀', (1, 2): '▀', (2, 1): '▀', (2, 2): '▀', (3, 2): '▀'}

하지만 이 딕셔너리 내용을 바탕으로 아래와 같은 ‘F’ 모양을 콘솔에 출력하는 것이 목표였습니다.

이미지: 콘솔에 출력될 대문자 F 형태

2. 명확한 발생 원인

파이썬의 기본 print() 함수는 문자열을 출력한 후 자동으로 줄바꿈(newline)을 수행하고, 커서를 다음 줄의 시작 위치로 이동시킵니다. 따라서 여러 문자를 연속해서 출력할 경우, 각각의 문자는 이전 문자의 바로 다음 위치나 다음 줄에 출력될 뿐, 임의의 X, Y 좌표에 정확히 위치시키는 것이 불가능합니다.

콘솔 환경에서 커서의 위치를 제어하고 특정 좌표에 문자를 출력하려면 ANSI 이스케이프 코드(ANSI Escape Codes)를 사용해야 합니다. ANSI 이스케이프 코드는 터미널(콘솔)에게 특정 명령을 내리는 특수 문자열 시퀀스이며, 커서 이동, 텍스트 색상 변경, 화면 지우기 등 다양한 터미널 제어 기능을 제공합니다.

3. 해결 방법 및 코드 예시

이 문제를 해결하는 핵심은 ANSI 이스케이프 코드를 사용하여 커서의 위치를 지정한 후 문자를 출력하는 것입니다. 특히 \033[{ROW};{COL}H (또는 \033[{ROW};{COL}f) 이스케이프 시퀀스는 커서를 지정된 행(ROW)과 열(COL)로 이동시키는 역할을 합니다. 여기서 주의할 점은 터미널 좌표계가 보통 1-based (1부터 시작)이며, 순서가 행(Y) 다음에 열(X)이라는 것입니다.

해결 코드 예시

기존 스크래핑 로직에 ANSI 이스케이프 코드를 활용한 출력 부분을 추가하여 문제를 해결할 수 있습니다.

import requests
from bs4 import BeautifulSoup
import sys # sys.stdout.write를 사용하기 위함

# --- 1. 데이터 스크래핑 및 딕셔너리 생성 (원본 코드와 동일) ---
URL = "https://docs.google.com/document/d/e/2PACX-1vTMOmshQe8YvaRXi6gEPKKlsC6UpFJSMAk4mQjLm_u1gmHdVVTaeh7nBNFBRlui0sTZ-snGwZM4DBCT/pub"

data = requests.get(URL).text
soup = BeautifulSoup(data, "lxml")
table = soup.find_all("table")[0]
rows = table.find_all("tr")
rows.pop(0)

data_formatted = []
for i in rows:
    table_data = i.find_all("td")
    for j in table_data:
        data_formatted.append(j.text)

data_formatted_dict = {}
for i in range(0, len(data_formatted), 3):
    # 딕셔너리 키가 (x, y) 형태이므로, y는 data_formatted[i+2], x는 data_formatted[i]
    data_formatted_dict[int(data_formatted[i]), int(data_formatted[i + 2])] = data_formatted[i + 1]

# --- 2. ANSI 이스케이프 코드를 이용한 콘솔 출력 ---

# 콘솔 화면을 지우는 ANSI 이스케이프 코드 (선택 사항)
# Windows에서는 colorama.init()이 필요할 수 있습니다.
sys.stdout.write("\033[2J") 
sys.stdout.flush()

# 각 문자를 해당 X, Y 좌표에 출력
for (x, y), char in data_formatted_dict.items():
    # ANSI 커서 이동: \033[{ROW};{COL}H
    # 콘솔 좌표는 1-based이므로, (x, y)에 1을 더해줍니다.
    # 또한, ANSI는 (ROW, COL) 순서이므로 (y+1, x+1)로 전달합니다.
    sys.stdout.write(f"\033[{y+1};{x+1}H{char}")
    sys.stdout.flush() # 버퍼를 비워 즉시 출력되도록 함

# 모든 문자를 출력한 후, 커서를 마지막 줄 다음으로 이동하여 프롬프트가 겹치지 않도록 합니다.
# 최대 y 값을 찾아 그 다음 줄로 이동하는 것이 일반적입니다.
max_y = max(key[1] for key in data_formatted_dict.keys()) if data_formatted_dict else 0
sys.stdout.write(f"\033[{max_y + 2};1H") # 모든 출력이 끝나고 두 칸 아래 첫 열로 이동
sys.stdout.flush()
```

코드 설명

  • sys.stdout.write(): print() 함수와 달리 자동으로 줄바꿈을 하지 않으며, 버퍼에 쓰기만 합니다.
  • sys.stdout.flush(): 버퍼에 있는 내용을 즉시 터미널로 출력하게 하여 지연 없이 문자가 나타나도록 합니다.
  • "\033[2J": 현재 터미널 화면을 완전히 지우는 ANSI 이스케이프 코드입니다. 깨끗한 상태에서 그림을 시작할 수 있습니다.
  • f"\033[{y+1};{x+1}H{char}": 이 부분이 핵심입니다.
    • \033[: ANSI 이스케이프 코드의 시작을 알립니다.
    • {y+1};{x+1}: 커서를 이동할 행(y)과 열(x) 좌표를 지정합니다. 콘솔은 보통 1부터 시작하므로 입력 좌표에 1을 더해줍니다.
    • H: 커서를 지정된 행과 열로 이동시키는 명령입니다.
    • {char}: 이동된 커서 위치에 출력할 문자입니다.
  • max_y + 2: 모든 출력이 완료된 후, 터미널 프롬프트가 그려진 문자와 겹치지 않도록 커서를 그림의 가장 아래 행보다 두 칸 아래로 이동시킵니다.

예상 출력

위 코드를 실행하면 터미널 화면이 지워지고, 각 X, Y 좌표에 문자들이 정확히 배치되어 목표했던 'F' 형태가 깔끔하게 출력되는 것을 확인할 수 있습니다.

4. 향후 예방을 위한 팁

  • 플랫폼 호환성 고려 (Colorama): 리눅스나 macOS 터미널은 대부분 ANSI 이스케이프 코드를 기본적으로 지원하지만, Windows 운영체제의 구형 CMD 터미널에서는 기본 지원이 약할 수 있습니다. 이런 경우 colorama 라이브러리를 사용하면 cross-platform 호환성을 확보할 수 있습니다. colorama.init()을 호출하면 Windows 터미널에서도 ANSI 코드를 해석할 수 있도록 변환해줍니다.
  • 복잡한 UI를 위한 라이브러리: 이처럼 단순한 문자 그리기를 넘어 복잡한 대화형 콘솔 UI를 구축하려면 curses (유닉스/리눅스)나 windows-curses (윈도우) 같은 전문 라이브러리를 고려하는 것이 좋습니다. 이들 라이브러리는 창 관리, 이벤트 처리 등 고급 기능을 제공합니다.
  • 좌표계 혼동 방지: 콘솔의 좌표계는 보통 (행, 열)이며 1-based입니다. 데이터 소스에서 (X, Y) 또는 0-based 좌표를 사용한다면, 출력 시 항상 (Y+1, X+1) 형태로 변환하는 것을 잊지 마세요.
  • 성능 최적화: 대량의 문자를 자주 업데이트해야 하는 경우, 매번 sys.stdout.write와 sys.stdout.flush를 호출하는 대신, 모든 문자를 하나의 문자열로 조합한 후 한 번에 출력하는 방식(예: 화면 버퍼를 문자열로 만들고 한 번에 업데이트)을 고려하여 I/O 오버헤드를 줄일 수 있습니다.
  • 사용자 경험(UX) 개선: 콘솔 애플리케이션의 UX를 고려하여, 화면을 지우고 깨끗한 상태에서 출력을 시작하거나, 작업 완료 후 커서를 적절한 위치로 이동시켜 사용자에게 다음 입력을 위한 명확한 프롬프트를 제공하는 것이 좋습니다.

이 가이드가 콘솔 그래픽 작업에 도전하는 파이썬 개발자 여러분에게 유용한 정보가 되었기를 바랍니다. 이제 여러분도 콘솔에서 자신만의 멋진 시각적 표현을 만들어보세요!

댓글 남기기