AI 개발 · 기본
바이브 코딩의 정석
계획 먼저 - 탐색·계획·구현·검증의 한 바퀴
파일을 고치기 전에 관련 코드를 살피고 계획만 받는 단계, 계획 검토에서 볼 것(바꿀 파일·기존 관례·빠진 경우), 단계마다 '확인 방법'을 붙인 계획, 승인 후 구현과 검증, 계획이 틀렸을 때 돌아가기, 완성 코드는 마감일 정렬 기능을 계획표대로 단계별로 추가하고 단계마다 확인 결과를 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 요청을 한 장짜리 기획서로 정리했다. 이제 그 기획서를 코드로 옮길 차례다. 그러나 요구 사항이 정리되었다고 해서 곧바로 파일을 고쳐도 되는 것은 아니다. 같은 기능도 기존 프로그램의 자료 모양과 출력 방식에 따라 바꿔야 할 위치가 달라진다. 먼저 관련 코드를 살피고, 무엇을 어떻게 바꿀지 계획으로 받은 다음, 그 계획을 검토하고 구현을 승인한다.
이 장에서는 작은 할 일 관리 도구에 마감일 정렬을 추가한다. 중요한 것은 정렬 함수 하나를 얻는 일이 아니다. 탐색에서 알아낸 사실을 계획에 연결하고, 계획의 각 단계에 확인 방법을 붙여 실제 실행 결과까지 이어 가는 연습이다. AI의 설명이 그럴듯한지보다 코드가 요구한 행동을 하는지 확인한다.
- 파일을 수정하기 전에 관련 코드와 기존 관례를 살피도록 요청한다.
- 계획에서 바꿀 파일, 유지할 관례, 빠진 경우를 찾아낸다.
- 각 구현 단계에 관찰할 수 있는 확인 방법을 붙인다.
- 승인한 계획에 따라 구현하고 실행 결과와 변경 내용을 검토한다.
- 계획의 전제가 틀렸다면 탐색과 계획으로 돌아간다.
문제 상황
혼자 쓰는 할 일 도구가 있다. 할 일은 사전으로 표현하고, 여러 사전을 리스트에 넣는다. 각 사전에는 번호를 나타내는 id, 제목을 나타내는 title, 마감일을 나타내는 due가 있다. 마감일은 "2026-10-10" 같은 문자열이며, 아직 정하지 않았다면 None이다. 현재 프로그램은 리스트에 들어 있는 순서대로 할 일을 출력한다.
독자는 AI에게 “마감일이 가까운 순서로 정렬해 줘”라고 요청한다. AI는 짧은 정렬 코드를 제시한다. 그런데 실행해 보니 마감일이 없는 항목에서 오류가 난다. 이를 고쳐 달라고 하자 이번에는 마감일이 없는 항목이 맨 앞에 나온다. 다시 고친 코드에서는 원본 리스트까지 정렬되어, 기존 등록 순서로 출력하던 부분의 결과가 달라진다.
문제는 Python이 정렬을 못 해서 생긴 것이 아니다. 요청에 담긴 기대와 기존 코드의 조건을 확인하지 않은 채 구현을 시작했기 때문이다. 마감일이 없는 항목을 어디에 놓을지, 같은 날짜의 항목은 어떤 순서를 유지할지, 원본 리스트를 바꿔도 되는지가 코드에 들어가기 전에 정해져야 한다.
앞 장의 기획서에 다음 조건이 있다고 가정한다. 날짜가 이른 할 일을 먼저 출력한다. 마감일이 없는 할 일은 뒤에 놓는다. 같은 마감일끼리는 입력 리스트의 순서를 유지한다. 원본 리스트의 순서는 바꾸지 않는다. 날짜 문자열이 잘못되었다면 조용히 미정으로 처리하지 않고 오류로 알린다. 이 조건을 기존 코드에 어떻게 반영할지 정하는 것이 이번 계획의 목적이다.
탐색으로 사실을 모으고 계획만 받는다
탐색은 수정할 부분과 그 주변 코드를 읽어 현재 상태를 알아보는 작업이다. 이때 AI에게 프로젝트 전체를 설명하라고 하기보다, 요청한 기능이 지나가는 길을 확인하게 한다. 이 예제라면 할 일 자료가 만들어지는 곳, 날짜가 저장되는 방식, 리스트가 출력되는 곳을 살피면 된다. 아직 파일을 고치지 않는다는 조건도 명확히 적는다.
앞서 정리한 마감일 정렬 요구 사항을 구현하려 한다. 지금은 파일을 수정하지 말고 관련 코드를 읽어라. 할 일 자료의 구조, 마감일이 없는 경우의 표현, 현재 출력 순서를 확인하라. 바꿀 파일과 함수, 유지할 기존 관례, 아직 확인되지 않은 조건을 적어라. 그다음 구현 계획만 제시하고, 각 단계에 확인 방법을 붙여라.
이 요청은 AI가 계획이라는 제목 아래 구현을 함께 진행하지 않도록 범위를 정한다. 계획을 받는 단계와 구현을 맡기는 단계는 서로 다른 일이다. 도구가 파일 수정 기능을 제공하더라도, 기능이 있다는 이유만으로 그 단계에서 수정할 필요는 없다. 이번에는 특정 제품의 화면이나 기능에 의존하지 않고 요청문으로 작업 범위를 구분한다.
탐색 결과에서는 사실과 추측을 나누어 읽는다. “마감일이 없으면 None을 넣는다”는 문장은 실제 자료를 확인했다면 사실이다. 반면 “번호가 작은 항목이 먼저 등록되었을 것이다”는 코드에 그런 보장이 없다면 추측이다. 같은 날짜의 순서를 번호순으로 바꾸는 계획은 이 추측에서 잘못 출발할 수 있다.
확인한 내용: 할 일은 사전의 리스트이며 날짜는 문자열 또는 None이다. 현재 출력은 리스트 순서를 따른다. 변경 대상은 main.py다. 정렬 결과를 새 리스트로 만들고 현재 출력 형식을 유지하는 방향으로 계획한다.
확인할 내용: 날짜 문자열은 YYYY-MM-DD 형식만 허용할지 정해야 한다. 같은 날짜는 번호순이 아니라 입력 리스트의 순서를 유지하는 것으로 이해했다.
이런 답을 받았다면 확인되지 않은 조건부터 정리한다. 이 장에서는 날짜 형식을 YYYY-MM-DD로 제한한다. 실제로 존재하는 날짜여야 하며, 빈 문자열도 마감일 없음으로 받아들이지 않는다. 마감일이 없다는 뜻은 None 하나로 표현한다. 자료의 의미가 여러 방식으로 섞이면 정렬뿐 아니라 출력과 검증도 복잡해지기 때문이다.
탐색의 결과는 길이보다 근거가 중요하다. AI가 “현재 구조에 맞춘다”고 말하면 어떤 구조를 보았는지 확인한다. 파일을 실제로 읽지 못한 상황에서 받은 계획은 현재 코드에 근거한 계획이 아니라 가정에 따른 초안이다. 이 차이를 알고 검토해야 수정 범위를 잘못 승인하지 않는다.
계획의 각 단계에 확인 방법을 붙인다
계획은 할 일을 나열한 문서가 아니다. 어디를 바꾸고, 무엇을 유지하며, 어떤 결과로 완료를 판단할지 연결한 문서다. “정렬 기능을 추가한다”는 문장만으로는 마감일이 없는 경우를 처리했는지 알 수 없다. “마감일이 없는 두 항목이 마지막에 오고 서로의 입력 순서를 유지하는지 확인한다”라고 쓰면 실행 결과로 판단할 수 있다.
계획을 읽을 때 먼저 바꿀 파일을 본다. 작은 정렬 기능을 추가하는데 저장 형식이나 파일 구성을 함께 바꾼다면 그 이유가 필요하다. 다음으로 기존 관례를 본다. 여기서 관례는 현재 코드가 일관되게 따르는 방식이다. 이 예제에서는 사전의 키 이름, None의 의미, 한 줄 출력 형식이 해당한다. 마지막으로 빠진 경우를 본다. 빈 리스트, 같은 날짜, 마감일 없음, 잘못된 날짜를 생각해 보면 계획의 빈틈이 드러난다.
| 관점 | 이 예제의 질문 | 수용할 계획 |
|---|---|---|
| 바꿀 파일 | 정렬 때문에 어디까지 수정하는가 | main.py의 날짜 해석, 정렬, 출력 연결 부분을 수정한다 |
| 기존 관례 | 자료와 출력의 의미가 유지되는가 | id·title·due를 유지하고 None은 마감일 없음으로 사용한다 |
| 빠진 경우 | 보통 자료 밖에서도 요구를 지키는가 | 같은 날짜, 미정, 빈 목록, 잘못된 날짜를 확인한다 |
이 장의 구현 계획은 다음과 같다. 한 파일 안에 함수들을 추가하되, 각 단계에서 그 단계의 조건을 확인한다. 날짜 해석이 틀린 상태에서 정렬 결과만 맞춰 놓으면 자료가 바뀌었을 때 다시 문제가 드러난다. 따라서 날짜 해석을 먼저 확인하고, 그 결과를 사용하는 정렬, 출력, 전체 연결 순서로 진행한다.
| 단계 | main.py에서 할 일 | 확인 방법 |
|---|---|---|
| 1. 날짜 해석 | 문자열을 날짜로 바꾸고 None을 구분한다 | 정상 날짜와 None을 확인하고 잘못된 날짜·형식을 거부한다 |
| 2. 정렬 | 정렬 기준을 만들고 새 리스트를 반환한다 | 번호 순서가 3, 4, 2, 1, 5인지 확인하고 원본과 비교한다 |
| 3. 출력 | 기존 한 줄 형식에 정렬 결과를 연결한다 | 첫 줄의 날짜와 제목, 미정 항목의 표시를 비교한다 |
| 4. 전체 검증 | 빈 목록과 실패 경우까지 함께 실행한다 | 빈 목록, 잘못된 날짜, 원본 보존을 확인하고 최종 목록을 출력한다 |
번호 순서만 확인하는 것은 충분하지 않다. 정렬 결과가 맞아도 원본 리스트가 함께 바뀌었을 수 있다. 반대로 원본이 그대로여도 같은 날짜의 두 항목이 뒤집혔을 수 있다. 요구 사항마다 다른 확인이 필요하다. 이번 자료에서 3번과 4번은 같은 날짜이고, 1번과 5번은 마감일이 없다. 이 배치는 두 종류의 순서 유지 조건을 한 번에 드러낸다.
확인 방법은 “테스트한다”처럼 넓은 표현으로 끝내지 않는다. 무엇을 넣고 무엇을 기대하는지 적는다. 다만 이 단계에서 복잡한 검사 체계를 만들 필요는 없다. 함수의 반환값과 기대값을 비교하는 작은 확인만으로도 계획의 조건이 구현에 반영되었는지 볼 수 있다. 자동으로 검사한 범위와 사람이 읽어야 하는 범위도 구분한다.
이 계획은 할 일 사전에 필요한 키가 이미 있다는 전제에서 출발한다. 제목이나 번호가 빠진 자료까지 검사하는 기능은 이번 요구에 넣지 않는다. 이런 전제를 적어 두면 빠뜨린 조건과 의도적으로 범위 밖에 둔 조건을 구분할 수 있다. 계획 검토는 가능한 모든 기능을 더하는 일이 아니라, 이번 요구를 수행하는 데 필요한 조건을 분명히 하는 일이다.
승인 후 구현하고 틀린 전제는 되돌린다
검토를 마쳤다면 구현 요청에 승인 범위를 적는다. 승인했다는 말은 이후의 모든 변경을 받아들이겠다는 뜻이 아니다. 지금 검토한 계획의 범위 안에서 구현을 시작해도 된다는 뜻이다. 구현 중 새로운 사실이 드러나면 계획을 다시 살필 수 있다.
위 계획을 승인한다. main.py에 네 단계대로 구현하라. 각 단계의 확인을 실행하고 결과를 알려라. 자료의 키와 기존 출력 형식은 유지하라. 계획의 전제와 실제 코드가 다르면 추가 변경을 진행하기 전에 차이와 수정 계획을 제시하라.
구현을 받은 뒤에는 실행 결과와 함께 변경 비교(diff)를 읽는다. 변경 비교는 수정 전후에 어떤 줄이 추가되거나 삭제되었는지 보여 주는 자료다. 이 예제에서는 날짜 해석 함수, 정렬 함수, 출력 연결 부분이 바뀌어야 한다. 요청하지 않은 저장 방식 변경이나 제목 수정이 들어갔다면 실행이 성공해도 그 이유를 확인한다.
계획이 틀릴 수도 있다. 탐색에서는 due가 문자열이라고 보았는데 실제 파일 읽기 함수가 날짜 객체를 반환할 수 있다. 이때 문자열이라는 전제를 유지하려고 여러 곳에 임시 변환을 덧붙이면 계획과 코드가 함께 흐려진다. 먼저 실제 자료가 만들어지는 위치로 돌아가고, 어떤 표현을 기준으로 삼을지 다시 정한다.
구현 중 확인한 내용: 파일에서 읽은 할 일의 due는 계획과 달리 날짜 객체다. 현재 날짜 해석 함수는 문자열을 전제로 한다. 읽기 함수와 출력 함수를 다시 살펴 자료 표현을 확인하고, 바꿀 위치와 확인 방법을 수정한 계획을 제시하겠다.
이 보고를 받으면 처음부터 모든 일을 다시 할 필요는 없다. 틀린 전제에 영향을 받는 단계만 찾는다. 자료 표현이 달라졌다면 날짜 해석과 정렬 기준은 다시 검토해야 하지만, 마감일이 없는 항목을 뒤에 둔다는 요구 자체는 유지된다. 수정한 계획을 승인한 뒤 해당 단계와 전체 연결을 다시 확인한다.
검증에서 실패했을 때도 같은 구분이 필요하다. 요구는 맞게 이해했지만 코드가 잘못되었다면 구현을 고친다. 요구 자체가 빠져 있거나 자료에 대한 가정이 틀렸다면 계획으로 돌아간다. 결과에 맞추려고 기대값을 바꾸면 실패의 원인을 숨기게 된다. 먼저 요구 사항과 탐색 근거를 다시 읽는다.
완성 코드
아래 프로그램을 main.py에 저장한다. Python 3.12 이상에서 표준 라이브러리만 사용한다. 날짜와 할 일 자료는 코드에 고정되어 있으며 현재 시각을 읽지 않는다. 파일 저장이나 사용자 입력도 없어서 실행하면 확인 결과와 정렬된 목록을 출력하고 끝난다.
이 코드는 네 단계의 완료 상태를 순서대로 확인하는 최종본이다. 실행하면서 자기 파일을 고치지는 않는다. 실제 구현에서는 날짜 해석을 추가하고 확인한 뒤 정렬을 추가하는 식으로 진행하며, 여기서는 그 단계별 확인을 한 프로그램에 남긴다. 화면의 통과 문장은 확인 조건이 실제로 참일 때만 출력된다.
from datetime import date
def check(condition, message):
if not condition:
raise AssertionError(message)
def parse_due(value):
if value is None:
return None
if not isinstance(value, str):
raise ValueError("마감일은 YYYY-MM-DD 문자열이어야 한다")
try:
parsed = date.fromisoformat(value)
except ValueError:
raise ValueError(f"잘못된 마감일: {value}") from None
if parsed.isoformat() != value:
raise ValueError(f"마감일 형식이 다르다: {value}")
return parsed
def due_key(task):
parsed = parse_due(task["due"])
if parsed is None:
return (True, date.max)
return (False, parsed)
def sort_tasks(tasks):
return sorted(tasks, key=due_key)
def format_tasks(tasks):
return [
f'{task["id"]} | {task["due"] or "미정"} | {task["title"]}'
for task in tasks
]
def expect_invalid(value):
try:
parse_due(value)
except ValueError:
return
raise AssertionError(f"잘못된 값을 받아들였다: {value}")
def main():
tasks = [
{"id": 1, "title": "책상 정리", "due": None},
{"id": 2, "title": "메모 분류", "due": "2026-10-15"},
{"id": 3, "title": "장보기", "due": "2026-10-10"},
{"id": 4, "title": "자료 읽기", "due": "2026-10-10"},
{"id": 5, "title": "아이디어 적기", "due": None},
]
original = [dict(task) for task in tasks]
check(parse_due("2026-10-10") == date(2026, 10, 10),
"정상 날짜 해석 실패")
check(parse_due(None) is None, "미정 해석 실패")
for value in ("2026-02-30", "20261010", "", 123):
expect_invalid(value)
print("1단계 통과: 날짜 해석과 잘못된 입력 거부")
ordered = sort_tasks(tasks)
check([task["id"] for task in ordered] == [3, 4, 2, 1, 5],
"정렬 순서 불일치")
check(tasks == original, "원본 자료 변경")
check(ordered is not tasks, "새 리스트를 반환하지 않음")
print("2단계 통과: 날짜순·동일 날짜 순서·미정 뒤 배치·원본 보존")
lines = format_tasks(ordered)
check(lines[0] == "3 | 2026-10-10 | 장보기", "첫 줄 출력 불일치")
check(lines[-2:] == ["1 | 미정 | 책상 정리",
"5 | 미정 | 아이디어 적기"], "미정 출력 불일치")
print("3단계 통과: 정렬 결과의 한 줄 출력")
check(format_tasks(sort_tasks([])) == [], "빈 목록 처리 실패")
try:
sort_tasks([{"id": 6, "title": "오류 예시", "due": "2026-02-30"}])
except ValueError:
pass
else:
raise AssertionError("정렬에서 잘못된 날짜를 받아들임")
check(tasks == original, "전체 실행 후 원본 자료 변경")
print("4단계 통과: 빈 목록·오류 전달·전체 원본 보존")
print("정렬된 할 일")
for line in lines:
print(line)
if __name__ == "__main__":
main()
줄별 해설
첫 줄은 datetime에서 date를 가져온다. date는 연월일을 하나의 값으로 표현한다. 문자열을 날짜 값으로 바꾸면 실제로 존재하는 날짜인지 확인할 수 있고 날짜끼리 비교할 수도 있다. 이 프로그램은 오늘 날짜를 구하지 않으므로 실행 날짜에 따라 결과가 달라지지 않는다.
check는 조건과 실패 설명을 받는다. 조건이 거짓이면 AssertionError라는 예외를 발생시킨다. 예외는 정상 실행을 계속할 수 없는 상황을 알리는 방식이다. 여기서는 확인 실패를 즉시 드러내는 용도로 사용한다. 통과 문장보다 앞에서 이 함수를 호출하므로, 조건이 실패했는데도 해당 단계가 통과했다고 출력하지 않는다.
parse_due는 먼저 None을 그대로 반환한다. 이것은 잘못된 날짜가 아니라 마감일 없음이라는 유효한 상태다. 이어서 문자열인지 확인한다. 숫자 같은 다른 자료를 넣으면 허용한 표현이 아니므로 ValueError를 발생시킨다. ValueError는 여기서 입력값의 내용이나 형식이 잘못되었음을 나타낸다.
date.fromisoformat은 문자열을 날짜로 해석한다. 존재하지 않는 날짜라면 예외가 나고, except ValueError 부분에서 읽기 쉬운 메시지로 바꾸어 전달한다. from None은 앞서 발생한 예외의 설명이 함께 이어지는 것을 숨긴다. 오류 자체를 없애는 문법은 아니다.
해석한 날짜의 isoformat() 결과를 원래 문자열과 비교하는 줄도 필요하다. 날짜로 해석할 수 있다는 조건과 이 도구가 정한 YYYY-MM-DD 형식이라는 조건은 다르다. "20261010"은 날짜로 해석되더라도 다시 만든 문자열과 같지 않으므로 거부한다. 이 비교로 허용 형식을 코드에 반영한다.
due_key는 정렬에 사용할 기준값을 만든다. 정렬 기준값은 항목 자체 대신 비교할 값이다. 여기서는 두 값을 묶은 튜플을 반환한다. Python은 튜플의 첫 번째 값을 먼저 비교하고 같으면 두 번째 값을 비교한다. 마감일이 있는 항목의 첫 값은 False, 없는 항목은 True다. 정렬에서는 False가 먼저 오므로 미정 항목이 뒤로 간다.
두 번째 값은 날짜다. 미정 항목에는 표현할 수 있는 가장 늦은 날짜인 date.max를 넣지만, 미정 여부는 첫 번째 값으로 이미 구분한다. 따라서 실제 마감일이 date.max와 같아도 미정 항목보다 앞에 온다. 서로 다른 의미를 날짜 하나로만 구분하지 않는 것이 이 기준의 핵심이다.
sort_tasks의 sorted는 새 리스트를 반환한다. 같은 기준값을 가진 항목은 원래 상대 순서를 유지한다. 이를 안정 정렬(stable sort)이라고 한다. 날짜가 같은 3번과 4번에 번호를 추가 비교 기준으로 넣지 않아도 입력 순서가 유지된다. 미정 항목끼리도 같은 원리가 적용된다.
format_tasks는 각 항목을 한 줄 문자열로 바꾼다. 대괄호 안의 for는 반복 결과를 새 리스트로 모으는 리스트 내포(list comprehension)다. due가 None이면 or "미정"에 따라 미정이라는 글자를 사용한다. 이 함수는 앞에서 날짜 검사를 마친 자료를 받는다는 전제가 있다. 빈 문자열도 화면에서는 미정으로 보일 수 있으므로, 출력 모양을 입력 검증 대신 사용해서는 안 된다.
expect_invalid는 잘못된 입력이 실제로 거부되는지 확인한다. 예외가 발생하면 기대한 결과이므로 돌아간다. 예외 없이 끝났다면 잘못된 값을 받아들인 것이므로 확인 실패를 발생시킨다. 실패해야 하는 입력을 검사할 때는 “오류가 안 났다”가 성공이 아니라는 점을 코드로 표현한 것이다.
main의 original은 비교용 자료다. dict(task)로 사전도 각각 복사한다. 이번 사전의 값은 정수·문자열·None이므로 이 복사로 비교 목적을 충족한다. 사전 안에 또 다른 리스트가 들어 있는 구조라면 복사 범위를 별도로 검토해야 한다.
네 단계의 확인은 계획표의 순서를 따른다. 첫 단계가 통과하면 날짜를 정렬 기준으로 사용하고, 두 번째가 통과하면 출력 문자열을 만든다. 마지막 단계에서는 빈 목록과 오류 전달을 확인하고 원본을 다시 비교한다. 새 리스트가 반환되어도 그 안의 사전은 원본과 공유된다. 이번 함수들은 사전을 수정하지 않지만, 이후 결과의 제목을 직접 바꾸는 기능을 추가한다면 원본 보존의 범위를 다시 정해야 한다.
마지막 조건문은 이 파일을 직접 실행했을 때 main을 호출한다. 따라서 실행 흐름은 자료 준비, 단계별 확인, 최종 출력으로 끝난다. 통과 결과는 이 프로그램에 들어 있는 사례를 확인했다는 뜻이다. 가능한 모든 자료를 증명했다는 뜻으로 확대하지 않는다.
실행 결과
터미널에서 다음 명령을 실행한다. 확인 조건이 모두 맞으면 아래 출력과 정확히 일치한다.
python3 main.py
1단계 통과: 날짜 해석과 잘못된 입력 거부
2단계 통과: 날짜순·동일 날짜 순서·미정 뒤 배치·원본 보존
3단계 통과: 정렬 결과의 한 줄 출력
4단계 통과: 빈 목록·오류 전달·전체 원본 보존
정렬된 할 일
3 | 2026-10-10 | 장보기
4 | 2026-10-10 | 자료 읽기
2 | 2026-10-15 | 메모 분류
1 | 미정 | 책상 정리
5 | 미정 | 아이디어 적기
3번과 4번은 같은 날짜이므로 원래의 상대 순서가 유지된다. 1번과 5번도 입력 순서를 유지하며 날짜가 있는 항목 뒤에 나온다. 출력에는 원본 리스트가 보이지 않지만 코드 안에서 비교했다. 눈에 보이는 목록과 내부 확인을 함께 보아야 계획의 조건을 검토할 수 있다.
이 예제는 경고를 유발하는 문자열 이스케이프 등을 사용하지 않는다. 실제 작업에서는 컴파일 확인, 실행 결과, 변경 비교를 함께 검토한다. 컴파일 성공은 문법을 확인한 결과이며, 마감일 정렬의 동작이 맞다는 근거는 별도의 실행 확인에서 얻는다.
실무에서 자주 틀리는 것
미정을 빈 문자열로 바꾸어 앞에 놓는다
다음 코드는 비교 오류를 피하려고 None을 빈 문자열로 바꾼다. 그러나 빈 문자열이 날짜 문자열보다 먼저 정렬되어 미정 항목이 앞에 나온다. 실행은 끝나지만 요구를 어긴다.
tasks = [
{"id": 1, "due": None},
{"id": 2, "due": "2026-10-10"},
]
ordered = sorted(tasks, key=lambda task: task["due"] or "")
print([task["id"] for task in ordered])
고친 코드는 미정 여부를 첫 기준으로 사용한다. lambda는 짧은 이름 없는 함수를 만드는 표현이다. 여기서는 비교를 위해 짧게 썼지만, 날짜 검사까지 필요한 완성 코드에서는 이름 있는 함수를 사용한다. 아래 예시는 날짜 문자열이 이미 검증되었다는 전제다.
tasks = [
{"id": 1, "due": None},
{"id": 2, "due": "2026-10-10"},
]
ordered = sorted(
tasks,
key=lambda task: (task["due"] is None, task["due"] or ""),
)
print([task["id"] for task in ordered])
첫 코드는 [1, 2]를, 고친 코드는 [2, 1]을 출력한다. 오류가 없다는 확인만으로는 이 차이를 잡지 못한다. 계획에 미정의 위치를 기대 결과로 적어 두어야 한다.
정렬 결과를 만들면서 원본도 바꾼다
리스트의 sort는 그 리스트 자체를 정렬한다. 화면의 순서는 맞아도 원본 보존 조건을 어길 수 있다. 다음 코드는 입력 자료가 날짜순으로 바뀐다.
tasks = [
{"id": 1, "due": "2026-10-15"},
{"id": 2, "due": "2026-10-10"},
]
tasks.sort(key=lambda task: task["due"])
print([task["id"] for task in tasks])
고친 코드는 sorted로 결과 리스트를 따로 만든다. 원본과 결과를 모두 출력하면 두 역할을 확인할 수 있다.
tasks = [
{"id": 1, "due": "2026-10-15"},
{"id": 2, "due": "2026-10-10"},
]
ordered = sorted(tasks, key=lambda task: task["due"])
print([task["id"] for task in tasks])
print([task["id"] for task in ordered])
원본은 [1, 2], 결과는 [2, 1]이다. 계획의 확인 방법을 “정렬 결과 확인”으로만 쓰면 원본 변경을 놓치기 쉽다. “원본의 순서와 내용도 비교한다”는 조건이 따로 있어야 한다.
잘못된 날짜를 미정으로 숨긴다
날짜 해석이 실패했을 때 None을 반환하면 프로그램을 계속 실행할 수는 있다. 그러나 사용자가 정하지 않은 날짜와 잘못 입력한 날짜가 같은 상태가 된다. 아래 코드에서는 존재하지 않는 날짜가 미정처럼 처리된다.
from datetime import date
def parse_due(value):
if value is None:
return None
try:
return date.fromisoformat(value)
except ValueError:
return None
print(parse_due("2026-02-30"))
고친 코드는 오류를 전달한다. 아래 실행 예시는 그 오류를 바깥에서 받아 메시지로 보여 준다. 실제 정렬 함수에서는 오류를 조용히 없애지 않고 호출한 곳까지 전달한다.
from datetime import date
def parse_due(value):
if value is None:
return None
parsed = date.fromisoformat(value)
if parsed.isoformat() != value:
raise ValueError("날짜 형식이 다르다")
return parsed
try:
parse_due("2026-02-30")
except ValueError:
print("잘못된 날짜를 거부했다")
계획에 잘못된 날짜의 처리 방식이 없었다면 먼저 그 조건을 정한다. 이미 거부하기로 정했다면 구현을 고친다. 실행을 통과시키려고 오류를 미정으로 바꾸는 것은 요구 사항을 바꾸는 일이다.
한눈에 보기
| 단계 | 남길 결과 | 다음으로 갈 조건 |
|---|---|---|
| 탐색 | 자료 구조, 관련 함수, 확인되지 않은 전제 | 사실과 가정이 구분되어 있다 |
| 계획 | 바꿀 파일, 유지할 관례, 단계별 확인 방법 | 빠진 경우를 검토하고 범위를 승인했다 |
| 구현 | 승인한 범위의 코드와 변경 비교 | 전제의 차이를 숨기지 않고 반영했다 |
| 검증 | 기대값과 실제 결과, 실패 조건의 확인 | 요구 사항에 대응하는 확인이 통과했다 |
| 되돌아가기 | 틀린 전제 또는 구현 오류의 구분 | 수정한 부분과 전체 연결을 다시 확인했다 |
계획 먼저라는 원칙은 계획을 길게 쓰라는 뜻이 아니다. 관련 코드를 읽고, 변경 범위를 좁히고, 완료를 판단할 방법을 정한 뒤 구현하라는 뜻이다. 다음 장에서는 이렇게 정한 작업을 더 작게 나누고 진행 상태를 남기는 방법을 다룬다.
연습 문제
- AI가 “main.py에 정렬을 추가하고 정상 동작을 확인하겠다”라는 계획을 제시했다. 이번 요구를 기준으로 추가해야 할 확인 방법을 세 가지 이상 적어라.
- 같은 날짜의 순서를 번호순으로 정렬하자는 제안을 받았다. 입력 순서를 유지한다는 요구와 어떤 차이가 있는지 설명하고, 그 차이를 드러내는 자료를 만들어라.
- 완성 코드에 마감일이 없는 항목만 두 개 있는 경우의 확인을 추가하라. 원래 입력 순서와 원본 보존을 모두 확인하고, 기존 실행 결과는 유지하라.
- 탐색 당시에는 미정을 None으로 표현한다고 보았지만 구현 중 빈 문자열도 발견했다. 바로 빈 문자열을 None으로 바꾸기 전에 확인할 위치와 수정 계획에 넣을 내용을 적어라.
정답과 해설
1. 날짜가 이른 순서인지, 같은 날짜의 입력 순서가 유지되는지, 미정 항목이 뒤에 오는지 확인해야 한다. 원본 리스트가 바뀌지 않는지, 빈 목록이 빈 결과를 반환하는지, 잘못된 날짜가 오류로 전달되는지도 확인한다. “정상 동작”이라는 표현을 구체적인 입력과 기대 결과로 바꾸는 것이 핵심이다.
2. 번호가 등록 순서를 뜻한다는 보장이 없으면 번호순과 입력 순서는 다르다. 아래 자료는 입력 순서가 8번, 2번이지만 번호순은 2번, 8번이다. 같은 날짜를 기준으로만 정렬하면 입력 순서가 유지된다.
tasks = [
{"id": 8, "due": "2026-10-10"},
{"id": 2, "due": "2026-10-10"},
]
ordered = sorted(tasks, key=lambda task: task["due"])
print([task["id"] for task in ordered])
출력은 [8, 2]다. 번호를 추가 기준으로 넣는 변경은 요구한 순서를 바꾸므로 계획 검토에서 제외해야 한다.
3. 다음 확인을 main의 4단계 통과 문장 앞에 넣을 수 있다. 확인을 추가하면서 출력 문장을 추가하지 않으므로 기존 예상 출력은 유지된다.
undated = [
{"id": 8, "title": "정리", "due": None},
{"id": 2, "title": "읽기", "due": None},
]
before = [dict(task) for task in undated]
result = sort_tasks(undated)
check([task["id"] for task in result] == [8, 2],
"미정 항목의 입력 순서 변경")
check(undated == before, "미정 목록의 원본 변경")
이 확인은 날짜가 있는 항목과 섞인 경우의 확인을 대체하지 않는다. 모든 항목이 미정인 경우를 추가로 확인한다. 자료의 번호를 입력 순서와 다르게 놓아야 실수로 번호순을 넣은 변경도 발견할 수 있다.
4. 빈 문자열을 만드는 입력 부분, 파일에서 읽는 부분, 미정을 출력하는 부분을 다시 살핀다. 빈 문자열이 이전 저장 자료의 표현인지 단순한 잘못된 입력인지 구분해야 한다. 수정 계획에는 받아들일 자료 표현, 바꿀 위치, None과 빈 문자열 각각의 확인 방법을 적는다. 기존 자료를 변환해야 한다면 그 작업을 이번 정렬 변경에 포함할지도 다시 정한다. 확인되지 않은 의미를 임의로 같게 만들지 않고, 발견한 사실을 계획에 반영한 뒤 구현을 이어 간다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.