AI 개발 · 기본
바이브 코딩 따라하기 - 요청문 그대로 따라 만드는 가계부 앱
화면 만들기 - 보고서를 HTML 파일로
HTML 문자열 생성, html.escape 로 메모 이스케이프, 표와 간단한 막대(문자 또는 너비 비율), 임시 폴더에 파일 저장 후 내용 일부 확인
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 가계부 기록을 CSV 파일로 주고받았다. 이번에는 같은 기록을 웹 브라우저에서 읽기 좋은 보고서로 바꾼다. 날짜와 분류, 금액, 메모를 표로 놓고, 분류별 지출 크기를 문자 막대로 보여 준다. 인터넷에 올리거나 서버를 실행하지 않고 HTML 파일 하나를 만드는 작업이다.
HTML은 웹 문서의 구조를 나타내는 표기법이다. 제목은 제목으로, 기록 목록은 표로 표시하라고 브라우저에 알려 준다. 이번에는 Python이 HTML 문자열을 만들고 임시 폴더에 저장한다. 마지막에는 저장한 파일을 다시 읽어 제목, 메모, 합계를 직접 확인한다.
- 정해진 가계부 기록을 HTML 문자열로 바꾼다.
- 메모에 들어 있는 특수문자를 html.escape로 안전하게 표시한다.
- 기록 표와 분류별 문자 막대를 만든다.
- 임시 폴더에 파일을 저장하고 다시 읽어 일부 내용을 확인한다.
- AI가 바꾼 코드의 차이와 실행 결과를 함께 검토한다.
이번 장에서 만들 것
혼자 쓰는 가계부에 세 건의 지출이 있다고 가정한다. 식비 두 건과 교통 한 건이다. 터미널에서도 읽을 수 있지만, 보고서를 열었을 때 열 제목과 합계가 분명하게 보이면 내용을 훑기 편하다. 메모에 꺾쇠괄호가 있어도 원래 입력한 글자가 그대로 보여야 한다.
완성할 main.py는 앞 장의 파일을 요구하지 않는다. 필요한 기록을 프로그램 안에 준비하고, 2026년 10월 기록만 골라 보고서를 만든다. 실행 날짜를 읽거나 사용자에게 입력을 받지 않으므로 실행할 때마다 같은 결과가 나온다. CSV 처리 전체를 다시 만들지 않고 화면 생성에 필요한 기록 구조만 포함한다.
| 날짜 | 분류 | 금액 | 메모 |
|---|---|---|---|
| 2026-10-03 | 식비 | 8,000원 | 점심 & 커피 |
| 2026-10-04 | 교통 | 12,000원 | 교통카드 충전 |
| 2026-10-05 | 식비 | 4,000원 | <간식> "빵" |
보고서에는 기록 표와 분류별 합계 표가 들어간다. 식비와 교통은 각각 12,000원이며 전체 합계는 24,000원이다. 가장 큰 분류의 막대를 네모 문자 열 개로 표시한다. 이번 기록에서는 두 분류의 합계가 같으므로 막대 길이도 같다.
파일은 실행 중에만 존재하는 임시 폴더에 만든다. 임시 폴더는 작업을 끝낸 뒤 자동으로 정리할 수 있는 작업 공간이다. 이번 예제는 파일 저장과 읽기 확인까지 마친 뒤 바로 종료한다. 브라우저를 자동으로 열거나 결과 파일을 계속 남겨 두는 동작은 넣지 않는다.
따라하기
아래 요청문을 순서대로 AI에게 보낸다. 각 단계의 답변 요지는 방향을 확인하기 위한 예시이며, AI의 답은 매번 다를 수 있다. 받은 코드는 main.py에 반영하고 실행한다. 중간 답변이 달라졌다면 요청문의 조건과 실제 출력이 맞는지 먼저 비교한다. 마지막에는 이 글의 완성 코드로 전체 동작을 재현할 수 있다.
명령은 터미널에 입력하는 문장이고, Python 코드는 main.py에 저장하는 내용이다. 두 가지를 같은 파일에 넣지 않는다. 이번 작업은 Python 3.12 이상과 표준 라이브러리만 사용한다. 외부 패키지 설치나 네트워크 연결은 필요하지 않다.
단계 1. 입력과 출력 범위를 고정한다
① 보낼 요청문
혼자 쓰는 가계부의 HTML 보고서를 만들겠다. Python 3.12 이상 표준 라이브러리만 사용하고 main.py 하나로 작성하라. 입력이나 네트워크 호출 없이 python3 main.py로 실행하고 끝나게 하라. 기록은 날짜, 분류, 금액, 메모를 가진 딕셔너리 세 개의 목록으로 준비하라. 기록은 2026-10-03 식비 8000원 메모 '점심 & 커피', 2026-10-04 교통 12000원 메모 '교통카드 충전', 2026-10-05 식비 4000원 메모 '<간식> "빵"'이다. 대상 월은 2026-10이다. 우선 대상 월의 건수와 전체 합계만 출력하라.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 고정 기록 목록을 만들고 대상 월의 기록을 골라, 건수 3과 전체 합계 24,000원을 출력하는 코드를 제안할 것이다.
③ 직접 확인할 것
터미널에서 python3 main.py를 실행한다. 대상 월이 2026-10이고 기록이 3건이며 합계가 24,000원인지 확인한다. 실행 중 질문이 나오거나 현재 날짜에 따라 결과가 바뀌면 고정 입력 조건을 다시 전달한다.
딕셔너리는 이름과 값을 짝지어 담는 자료다. 이 예제에서는 date라는 이름으로 날짜를, amount라는 이름으로 금액을 찾는다. 금액을 따옴표로 감싸면 숫자 계산에 문제가 생길 수 있으므로 정수로 저장했는지도 살펴본다.
④ 체크포인트
세 기록의 금액을 직접 더한 값과 프로그램의 합계가 같으면 다음 단계로 간다. 화면을 만들기 전에 보고서에 들어갈 숫자를 먼저 고정한 상태다.
단계 2. 기록을 HTML 표로 바꾼다
① 보낼 요청문
지금 코드에 build_report(records, month) 함수를 추가하라. 함수는 대상 월의 기록으로 완전한 HTML 문서 문자열을 반환해야 한다. 문서 제목은 '2026-10 가계부 보고서'처럼 만들고 언어는 한국어, 문자 인코딩은 UTF-8로 지정하라. 날짜, 분류, 금액, 메모 네 열을 가진 표를 만들고 금액에는 천 단위 쉼표와 '원'을 붙여라. 아직 파일은 저장하지 말고 생성한 문자열에 제목과 기록 표가 있는지 확인하는 출력만 추가하라.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 기록마다 표의 한 행을 만들고, 행을 이어 붙여 문서 문자열을 반환하는 함수를 제안할 것이다. 문서에는 제목과 문자 인코딩 정보가 포함된다.
③ 직접 확인할 것
python3 main.py를 실행한다. 제목이 '2026-10 가계부 보고서'이고 기록 표에 세 행이 들어 있는지 확인한다. 생성 문자열을 출력하도록 요청했다면 <table>, <tr>, <td>가 짝을 이루는지 살펴본다.
표에서 한 행을 나타내는 표시는 tr이고, 그 안의 값 한 칸은 td다. 열 이름에는 th를 쓴다. 지금 모든 표기법을 외울 필요는 없다. 날짜가 금액 칸으로 밀리거나 마지막 메모가 표 밖으로 나가지 않는지 확인할 수 있으면 된다.
④ 체크포인트
기록 한 건이 표 한 행으로 이어지고 제목에 대상 월이 들어가면 다음 단계로 간다. 이 단계에서 메모의 꺾쇠괄호를 어떻게 다루는지도 다음 요청에서 명확히 정한다.
단계 3. 메모를 글자 그대로 표시한다
① 보낼 요청문
HTML에 넣는 메모를 html.escape로 처리하라. 원본 records는 변경하지 말고 HTML 문자열을 만드는 순간에 한 번 처리하라. 날짜, 분류, 제목도 HTML에 넣을 때 같은 방식으로 처리하라. '<간식> "빵"' 메모가 HTML 안에서 <간식> "빵"이 되는지 출력으로 확인하게 하라. 점심 메모의 &도 처리하라.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. html 모듈에서 escape를 가져오고 메모를 표에 끼워 넣기 전에 변환할 것이다. 꺾쇠괄호는 문서 구조가 아니라 표시할 글자를 뜻하는 표기로 바뀐다.
③ 직접 확인할 것
python3 main.py를 실행한다. HTML 원문에서 간식 메모가 <간식> "빵"으로 나오고 점심 메모가 점심 & 커피로 나오는지 확인한다. 원본 기록의 메모에는 원래 글자가 남아 있어야 한다.
이스케이프(escape)는 문서에서 특별한 뜻을 갖는 문자를 일반 글자로 표시할 수 있도록 바꾸는 작업이다. 브라우저는 HTML 원문의 <를 화면에서 <로 보여 준다. 따라서 파일 안의 글자와 브라우저에서 보이는 글자는 표현 방식이 다를 수 있다.
여기서 escape를 적용하는 대상은 표 안에 넣을 텍스트다. 이미 완성된 HTML 전체에 적용하면 표를 만드는 기호까지 글자로 바뀐다. 반대로 메모를 처리하지 않으면 메모 일부가 문서 구조로 해석될 수 있다. 기록에는 원본을 두고 표시할 때 변환하는 위치를 확인한다.
④ 체크포인트
특수문자가 있는 두 메모가 예상한 형태로 바뀌고 기록 원본은 유지되면 다음 단계로 간다.
단계 4. 분류별 합계와 문자 막대를 붙인다
① 보낼 요청문
보고서에 분류별 합계 표를 추가하라. 분류명은 정렬해서 출력하고 열은 분류, 합계, 상대 막대 세 개로 구성하라. 가장 큰 합계를 네모 문자 ■ 열 개로 표시하라. 막대 개수는 '분류 합계 × 10 ÷ 최대 합계'의 정수 몫으로 계산하라. 기록이 없거나 최대 합계가 0이면 막대는 빈 문자열로 만들고 0으로 나누지 않게 하라. 전체 합계 문단도 추가하라. 이번 기록의 금액은 0 이상의 정수라고 가정한다.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 분류 이름별로 금액을 누적하고 가장 큰 합계를 기준으로 막대 길이를 계산할 것이다. 식비와 교통이 각각 12,000원이므로 두 막대 모두 네모 열 개가 된다.
③ 직접 확인할 것
python3 main.py를 실행한다. 분류 표의 순서는 교통, 식비이며 각각 12,000원과 네모 열 개가 나오는지 확인한다. 전체 합계는 여전히 24,000원이어야 한다.
이 막대는 전체 지출에서 차지하는 비중을 뜻하지 않는다. 가장 큰 분류와 비교한 길이다. 두 분류가 모두 열 칸이어도 전체 지출의 전부를 각각 사용했다는 뜻은 아니다. 표 제목을 '상대 막대'로 쓰고 금액도 나란히 두어 해석을 돕는다.
④ 체크포인트
분류 합계를 더한 값이 전체 합계와 같고, 같은 금액의 막대 길이도 같으면 다음 단계로 간다.
단계 5. 임시 파일에 저장하고 다시 읽는다
① 보낼 요청문
tempfile.TemporaryDirectory와 pathlib.Path를 사용해 임시 폴더에 report.html을 저장하라. 쓰기와 읽기에 모두 encoding="utf-8"을 지정하라. 저장 후 파일을 다시 읽어 제목 줄, 간식 메모가 있는 행, 전체 합계 줄을 출력하라. 임시 폴더의 전체 경로는 매번 달라지므로 출력하지 말고 파일명만 출력하라. 읽기와 출력은 임시 폴더가 유지되는 with 블록 안에서 끝내라. 블록이 끝나면 파일이 정리되도록 하라.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 임시 폴더를 만들고 HTML을 저장한 뒤 같은 파일을 읽는 코드를 제안할 것이다. 파일명과 선택한 세 줄만 출력하며, 실행 종료 후 임시 파일은 남지 않는다.
③ 직접 확인할 것
python3 main.py를 실행한다. '저장 파일: report.html'과 제목, 간식 메모 행, 전체 합계가 출력되는지 확인한다. 아래 완성 코드로 맞췄다면 '실행 결과'의 여섯 줄과 정확히 같아야 한다.
저장하기 전 문자열만 확인하면 파일에 제대로 썼는지는 알 수 없다. 이번에는 저장한 내용을 다시 읽은 문자열에서 확인할 줄을 찾는다. 임시 폴더의 이름은 달라도 파일명과 내용이 같으면 결과 출력은 같게 유지할 수 있다.
④ 체크포인트
다시 읽은 파일에서 한국어와 이스케이프된 메모가 유지되고, 합계도 같으면 다음 단계로 간다.
단계 6. 변경 차이와 재실행 결과를 검토한다
① 보낼 요청문
완성한 main.py를 검토하라. HTML 표시를 추가하면서 기록 원본, 대상 월 필터, 금액 합산을 불필요하게 바꾸지 않았는지 변경 전후의 차이인 diff를 설명하라. 외부 패키지, 네트워크 호출, 현재 시각, 사용자 입력이 없는지도 확인하라. 빈 기록 목록으로 build_report를 호출했을 때 전체 합계 0원과 빈 표가 만들어지는지 별도로 확인하라. 확인용 변경은 되돌리고 최종 main.py와 예상 출력을 제시하라. 실제로 실행하지 못했다면 실행했다고 말하지 말라.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 기록을 바꾸는 코드가 없는지 살펴보고, 화면 생성과 파일 저장이 추가된 위치를 설명할 것이다. 빈 목록을 처리하는 부분과 직접 실행해야 할 확인 항목도 제시할 것이다.
③ 직접 확인할 것
먼저 python3 -W error -m py_compile main.py로 경고를 오류로 다루어 컴파일한다. 성공하면 별도 출력 없이 끝난다. 이어서 python3 main.py를 두 번 실행하고 두 결과가 아래 예상 출력과 같은지 비교한다. 편집기의 변경 비교 화면에서 escape가 중복 적용되지 않았는지도 살펴본다.
컴파일 확인은 Python 문법을 읽을 수 있는지 확인하는 작업이다. 합계가 맞는지나 메모가 올바르게 표시되는지까지 보장하지 않으므로 실행 결과도 따로 비교한다. AI가 검토를 마쳤다고 답하더라도 실제 파일과 출력에서 근거를 찾는다.
빈 목록도 직접 확인하려면 기록 목록을 잠시 비우고 실행한다. 보고서 함수는 빈 표와 0원을 만들지만, 간식 행을 찾아 출력하는 시연 부분에서는 해당 행을 찾지 못한다. 따라서 이 확인은 build_report([], MONTH)의 반환 문자열을 따로 출력하는 방식으로 요청한다. 확인 뒤에는 완성 코드로 복원한다. 일반적인 보고서 생성과 고정 예시를 보여 주는 출력 코드는 역할이 다르다.
④ 체크포인트
컴파일이 성공하고 두 번의 출력이 같으며, 변경 이유를 설명할 수 있으면 이번 따라하기를 마친다. 확인 목록을 별도 검사 코드로 정리하는 일은 다음 장에서 이어 간다.
완성 코드
다음 내용을 main.py로 저장한다. 문서 문자열을 만드는 함수와 임시 파일을 이용해 결과를 보여 주는 함수를 나눈다. 실행에 필요한 기록도 함께 들어 있으므로 앞 장 파일을 준비할 필요가 없다.
from html import escape
from pathlib import Path
from tempfile import TemporaryDirectory
MONTH = "2026-10"
RECORDS = [
{"date": "2026-10-03", "category": "식비",
"amount": 8000, "memo": "점심 & 커피"},
{"date": "2026-10-04", "category": "교통",
"amount": 12000, "memo": "교통카드 충전"},
{"date": "2026-10-05", "category": "식비",
"amount": 4000, "memo": '<간식> "빵"'},
]
def build_report(records, month):
selected = [
record for record in records
if record["date"].startswith(month + "-")
]
total = sum(record["amount"] for record in selected)
totals = {}
rows = []
for record in selected:
category = record["category"]
totals[category] = totals.get(category, 0) + record["amount"]
rows.append(
"<tr>"
f'<td>{escape(record["date"])}</td>'
f"<td>{escape(category)}</td>"
f'<td>{record["amount"]:,}원</td>'
f'<td>{escape(record["memo"])}</td>'
"</tr>"
)
largest = max(totals.values(), default=0)
bars = []
for category in sorted(totals):
amount = totals[category]
count = amount * 10 // largest if largest > 0 else 0
bar = "■" * count
bars.append(
f"<tr><td>{escape(category)}</td>"
f"<td>{amount:,}원</td><td>{bar}</td></tr>"
)
title = escape(f"{month} 가계부 보고서")
parts = [
"<!doctype html>",
'<html lang="ko">',
'<head><meta charset="utf-8">',
f"<title>{title}</title></head>",
"<body>",
f"<h1>{title}</h1>",
'<table><caption>지출 기록</caption>',
"<thead><tr><th>날짜</th><th>분류</th>"
"<th>금액</th><th>메모</th></tr></thead>",
"<tbody>",
*rows,
"</tbody></table>",
"<table><caption>분류별 지출</caption>",
"<thead><tr><th>분류</th><th>합계</th>"
"<th>상대 막대</th></tr></thead>",
"<tbody>",
*bars,
"</tbody></table>",
f"<p>전체 합계: {total:,}원</p>",
"</body></html>",
]
return "\n".join(parts)
def main():
document = build_report(RECORDS, MONTH)
with TemporaryDirectory() as folder:
path = Path(folder) / "report.html"
path.write_text(document, encoding="utf-8")
saved = path.read_text(encoding="utf-8")
lines = saved.splitlines()
print(f"대상 월: {MONTH}")
print(f"저장 파일: {path.name}")
print("저장 후 확인:")
print(next(line for line in lines if line.startswith("<h1>")))
print(next(line for line in lines if "<간식>" in line))
print(next(line for line in lines if line.startswith("<p>전체 합계:")))
if __name__ == "__main__":
main()
줄별 해설
처음 세 줄은 필요한 도구를 가져온다. escape는 표시할 문자열을 변환하고, Path는 파일 경로를 다루며, TemporaryDirectory는 임시 폴더를 만들고 정리한다. 모두 Python에 포함된 표준 라이브러리이므로 추가 설치가 없다.
MONTH와 RECORDS는 이번 실행의 고정 입력이다. 날짜를 연도-월-일 순서의 문자열로 통일했다. build_report 안의 startswith는 날짜가 대상 월과 하이픈으로 시작하는지 확인한다. 이 방식은 예제처럼 날짜 형식이 이미 맞춰져 있다는 전제에서 사용한다. 날짜의 유효성을 새로 검사하는 기능은 이번 범위에 넣지 않는다.
selected와 total은 보고서에 사용할 기록과 전체 합계다. selected를 만드는 과정에서 원본 목록에 항목을 추가하거나 삭제하지 않는다. total은 선택한 기록의 amount만 더하므로 화면 문자열의 쉼표나 '원' 표시에 영향을 받지 않는다.
totals.get(category, 0)은 아직 등장하지 않은 분류의 합계를 0부터 시작하게 한다. 같은 분류가 다시 나오면 기존 합계에 금액을 더한다. rows에는 기록 표의 행 문자열이 차례로 쌓인다. 날짜, 분류, 메모는 텍스트이므로 escape를 거치고, 정수인 금액은 표시 형식만 지정한다.
금액 뒤의 : , 표기는 코드에서 공백 없이 : ,의 두 기호를 붙인 형태로 사용한다. 실제 코드의 {amount:,}는 숫자에 천 단위 쉼표를 넣는다. 숫자 자체를 문자열로 바꿔 저장하는 것이 아니라 화면에 넣는 순간의 표현을 정하는 것이다.
largest와 count는 막대 길이를 정한다. max에 default=0을 주어 분류가 없어도 오류 없이 0을 얻는다. largest가 0보다 클 때만 나눗셈을 한다. //는 나눈 결과의 정수 몫을 구한다. 이 예제처럼 금액이 음수가 아니면 최대 열 개의 네모 문자를 만들 수 있다. 작은 금액은 몫이 0이 되어 막대가 안 보일 수도 있으므로 실제 금액 열을 함께 읽는다.
sorted(totals)는 분류 이름을 정렬한다. 기록이 처음 나타난 순서와 상관없이 분류 표의 순서를 정하기 위한 선택이다. 기록 표는 입력 순서를 유지하고 분류 표는 이름순으로 보여 준다는 차이를 기억한다.
parts에는 문서의 앞부분, 기록 행, 분류 행, 합계가 순서대로 들어간다. 목록 안의 *rows와 *bars는 다른 목록에 담긴 행들을 현재 목록에 펼쳐 넣는다. 마지막 join은 각 요소 사이에 줄바꿈을 넣어 하나의 문자열로 만든다. 긴 문자열 하나를 직접 고치는 것보다 행 단위로 내용을 찾기 쉽다.
문서의 문자 인코딩 선언은 브라우저에 UTF-8로 읽으라고 알려 준다. 파일을 쓰는 쪽의 encoding="utf-8"과 역할은 다르다. 하나는 파일에 저장할 바이트를 정하고, 다른 하나는 그 파일을 읽는 브라우저에 해석 방법을 알린다. 두 곳을 함께 맞춘다.
with TemporaryDirectory() 안에서는 폴더가 유지된다. 쓰기, 다시 읽기, 내용 확인을 모두 그 안에서 마친다. 블록이 끝나면 폴더와 파일이 정리된다. 전체 경로 대신 path.name을 출력하므로 임시 폴더 이름이 바뀌어도 출력은 같다.
next로 시작하는 출력 세 줄은 조건에 맞는 첫 줄을 찾는다. 이번 고정 기록에 제목과 간식 메모가 있다는 전제에서 사용하는 시연 코드다. 일반적인 검색 기능처럼 '찾지 못함'을 처리하는 코드는 아니다. 마지막 조건문은 main.py를 직접 실행했을 때 main을 호출한다.
실행 결과
main.py를 저장한 폴더에서 다음 명령을 차례로 실행한다. 명령 자체는 Python 파일에 넣지 않는다.
python3 -W error -m py_compile main.py
python3 main.py
첫 명령은 성공하면 아무것도 출력하지 않는다. 두 번째 명령의 예상 출력은 다음과 같다. 아래 내용은 브라우저 화면이 아니라 저장한 HTML의 원문 일부다. 따라서 꺾쇠괄호와 따옴표를 바꾼 표기가 그대로 보인다.
대상 월: 2026-10
저장 파일: report.html
저장 후 확인:
<h1>2026-10 가계부 보고서</h1>
<tr><td>2026-10-05</td><td>식비</td><td>4,000원</td><td><간식> "빵"</td></tr>
<p>전체 합계: 24,000원</p>
출력에서 <간식>는 브라우저가 읽으면 <간식>으로 표시된다. "은 따옴표로 표시된다. 파일 내용을 터미널에서 읽는 것과 브라우저가 문서로 해석하는 것을 구분해야 결과를 잘못 고치지 않는다.
실행 후 report.html을 현재 폴더에서 찾을 수 없는 것은 예정된 동작이다. 임시 폴더 안에 저장하고 확인한 뒤 정리했기 때문이다. 이번 확인의 근거는 파일을 다시 읽어 얻은 세 줄이며, 이 코드가 브라우저의 실제 배치까지 자동으로 확인한 것은 아니다.
실무에서 자주 틀리는 것
메모를 변환하지 않고 표에 넣는다
다음은 메모를 그대로 붙이는 잘못된 예다. 꺾쇠괄호가 포함된 메모는 브라우저가 문서 구조의 일부로 해석할 수 있다. 짧은 Python 예제도 각각 독립적으로 실행할 수 있다.
memo = '<간식> "빵"'
cell = f"<td>{memo}</td>"
print(cell)
고친 코드는 표시할 메모만 변환한 뒤 칸 안에 넣는다. 수동으로 몇 글자만 바꾸기보다 표준 라이브러리의 함수를 사용한다.
from html import escape
memo = '<간식> "빵"'
cell = f"<td>{escape(memo)}</td>"
print(cell)
이미 변환한 메모를 다시 변환한다
다음 코드는 같은 메모에 두 번 escape를 적용한다. 첫 변환에서 생긴 & 기호까지 다시 변환되어 브라우저에 원하지 않는 표기가 나타난다.
from html import escape
memo = escape("<간식>")
cell = f"<td>{escape(memo)}</td>"
print(cell)
고친 코드는 원본 문자열을 유지하고 HTML에 넣는 자리에서 한 번만 변환한다. 변환된 값을 기록 원본에 덮어쓰지 않는 것도 같은 이유다.
from html import escape
memo = "<간식>"
cell = f"<td>{escape(memo)}</td>"
print(cell)
합계가 0인데도 막대 비율을 계산한다
다음 코드는 최대 합계가 0이면 나눗셈 오류로 멈춘다. 표에 넣을 값이 없을 때도 계산식을 그대로 실행하는 것이 원인이다.
amount = 0
largest = 0
count = amount * 10 // largest
print("■" * count)
고친 코드는 나누기 전에 최대 합계를 확인한다. 빈 기록에서는 max의 default도 함께 필요하다는 점을 완성 코드와 비교한다.
amount = 0
largest = 0
count = amount * 10 // largest if largest > 0 else 0
print("■" * count)
임시 폴더가 정리된 뒤에 파일을 읽는다
다음 코드는 with 블록 밖에서 파일을 읽는다. 그 시점에는 임시 폴더가 정리되었으므로 파일을 찾지 못한다.
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
path = Path(folder) / "report.html"
path.write_text("<p>가계부</p>", encoding="utf-8")
print(path.read_text(encoding="utf-8"))
고친 코드는 읽기를 블록 안에서 마친다. 읽어 둔 문자열은 블록 밖에서도 사용할 수 있다. 파일의 수명과 메모리에 읽어 둔 문자열의 수명은 다르다.
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
path = Path(folder) / "report.html"
path.write_text("<p>가계부</p>", encoding="utf-8")
saved = path.read_text(encoding="utf-8")
print(saved)
한눈에 보기
| 작업 | 사용할 도구 | 확인할 결과 |
|---|---|---|
| 보고서 문자열 생성 | build_report | 제목, 기록 표, 분류 표, 전체 합계가 들어간다. |
| 텍스트 변환 | escape | 원본 메모를 보존하고 표시할 때 한 번 처리한다. |
| 문자 막대 생성 | 정수 몫과 문자열 반복 | 최대 분류는 열 칸이며 0으로 나누지 않는다. |
| 파일 저장과 읽기 | Path와 UTF-8 | 저장한 한국어와 특수문자가 유지된다. |
| 임시 파일 정리 | TemporaryDirectory | 읽기 확인을 마친 뒤 폴더가 정리된다. |
| AI 변경 검토 | 변경 비교와 재실행 | 원본 기록을 바꾸지 않고 예상 출력과 일치한다. |
이번에 만든 결과는 HTML 파일을 생성하는 프로그램이다. 웹 서버나 별도 화면 도구 없이도 보고서 문서를 만들 수 있다. 확인은 숫자, 텍스트 변환, 파일 저장을 나누어 살펴보면 된다. AI의 설명과 실제 코드가 다르면 실행한 파일을 기준으로 다시 확인한다.
연습 문제
- 기록 목록에 2026-10-06, 식비, 6000원, 메모 '저녁'을 추가한다. 전체 합계와 두 분류의 합계, 막대 개수를 먼저 손으로 구한 뒤 실행으로 확인한다.
- 마지막 기록의 메모를 '차 & <우유>'로 바꾼다. HTML 원문에 들어갈 표기를 적고, 간식 메모를 찾던 출력 조건도 새 메모에 맞게 바꾼다.
- 대상 월을 2026-11로 바꾼다. 보고서 함수는 그대로 두고 main의 확인 출력을 제목과 전체 합계만 찾도록 바꾼다. 기록이 없는 보고서가 생성되는지 확인한다.
정답과 해설
1. 식비는 18,000원, 교통은 12,000원, 전체 합계는 30,000원이다. 최대 합계가 18,000원이므로 식비 막대는 열 개다. 교통은 12,000 × 10을 18,000으로 나눈 정수 몫인 여섯 개다. 분류 표는 교통, 식비 순서를 유지한다. 나머지를 버리는 계산이므로 막대만으로 정확한 금액을 판단하지 않는다.
완성 코드의 RECORDS 목록에 딕셔너리 한 개를 추가하면 된다. 실행 출력의 전체 합계가 30,000원으로 바뀌는지 확인한다. 분류 표까지 확인하려면 저장한 문자열 전체를 잠시 출력하게 요청하고, 확인이 끝나면 원래의 일부 출력으로 되돌린다.
2. HTML 원문에는 차 & <우유>가 들어간다. 원본의 &는 &로, 꺾쇠괄호는 <와 >로 바뀐다. 브라우저에서는 원래 메모인 차 & <우유>로 보인다. 출력에서 찾을 문자열은 <간식> 대신 <우유>로 바꾼다. 찾는 조건을 그대로 두면 보고서 저장은 끝나도 시연 출력에서 해당 줄을 찾지 못한다.
메모를 바꾼 뒤 escape를 한 번 더 추가하는 수정은 필요하지 않다. 기존 변환 위치가 새 메모에도 적용된다. 변경 비교에서 입력 메모와 출력 검색 조건만 달라졌는지 살펴보면 불필요한 수정도 발견하기 쉽다.
3. 제목은 2026-11 가계부 보고서이고 전체 합계는 0원이다. 기록 표와 분류 표의 제목 및 열 이름은 남지만, 본문 행은 없다. selected가 빈 목록이므로 sum은 0을 만들고 largest도 default에 따라 0이 된다. 분류 반복문은 실행되지 않는다.
main에서 간식 메모를 찾는 print 한 줄을 제거하면 나머지 확인 출력을 사용할 수 있다. 빈 달에는 메모 행이 없기 때문이다. 이때 build_report를 고쳐 가짜 메모 행을 넣지 않는다. 실제 기록이 없는 상태를 그대로 보여 주는 것이 요구한 결과다. 연습을 마치면 대상 월과 출력문을 완성 코드대로 복원하고 다시 실행해 여섯 줄의 예상 출력과 비교한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.