PH pullh
입문 블로그 / 조건문과 반복문이 갑자기 어려워질 때 보는 사고법
코드 기초 6분 Beginner

조건문과 반복문이 갑자기 어려워질 때 보는 사고법

문법은 이해했는데 문제만 나오면 막힌다면, 흐름을 머릿속에서 한 번에 너무 크게 보려 하기 때문일 수 있다.

조건과 반복 커버 이미지

조건문과 반복문은 초보자가 처음으로 흐름을 직접 다루는 순간입니다. 그래서 문법 자체보다 코드가 어떤 순서로 움직이는지 머릿속에 그리는 힘이 더 크게 작용합니다.

이 단계에서 흔한 실수는 문제 전체를 한 번에 이해하려는 것입니다. 실제로는 한 줄씩, 한 바퀴씩 좁혀 보면 훨씬 단순한 경우가 많습니다. 이 글에서는 요구사항 한 문장을 코드로 옮기는 과정을 그대로 따라가 보겠습니다.

문장 하나를 조건으로 옮겨 봅니다

이런 요구사항이 있다고 해 보겠습니다. "점수가 60점 이상이면 통과입니다. 단, 결석이 3회 이상이면 점수와 관계없이 재수강입니다." 한국어로는 한 문장이지만, 코드로 옮기려면 두 가지를 먼저 정해야 합니다. 어떤 조건이 있는가, 그리고 어떤 조건이 다른 조건을 덮어쓰는가입니다.

많은 입문자가 첫 번째만 보고 바로 씁니다. 요구사항에 등장한 순서대로 if와 elif를 늘어놓는 것입니다. 그러면 이런 코드가 나옵니다.

BEFORE — 조건이 뒤엉킨 버전

def result(score, absence):
    if score >= 60:
        return "통과"
    elif absence >= 3:
        return "재수강"
    else:
        return "재수강"

점수 80점에 결석 4회인 학생은 여기서 통과 처리됩니다.

이 코드는 문법 오류가 없고 대부분의 입력에서 그럴듯한 답을 냅니다. 그런데 요구사항의 "점수와 관계없이"라는 부분이 사라졌습니다. if score >= 60이 먼저 걸리기 때문에 결석 조건은 점수가 낮을 때만 확인됩니다. 조건문에서 순서는 장식이 아니라 우선순위 자체입니다.

요구사항 문장에서 "단"이나 "관계없이" 같은 표현이 나오면, 그 조건이 다른 조건보다 먼저 판정되어야 한다는 신호입니다. 그 순서를 코드에 그대로 반영하면 이렇게 됩니다.

AFTER — 우선순위를 드러낸 버전

def result(score, absence):
    # 다른 무엇보다 먼저 판정되는 규칙
    if absence >= 3:
        return "재수강"
    if score >= 60:
        return "통과"
    return "재수강"

print(result(80, 4))   # 재수강
print(result(80, 1))   # 통과
print(result(55, 0))   # 재수강

먼저 걸러야 할 것을 위로 올리면 else가 줄고 읽는 순서가 규칙의 순서와 같아집니다.

고친 코드가 더 나은 이유는 짧아서가 아닙니다. 코드를 위에서 아래로 읽은 순서가 요구사항을 말로 설명하는 순서와 같아졌기 때문입니다. 조건문을 쓸 때 이 일치를 확인하는 습관이 붙으면, 조건이 다섯 개로 늘어나도 크게 흔들리지 않습니다.

덧붙이면, 고친 코드에서는 else가 사라졌습니다. 앞의 조건들이 모두 return으로 끝나기 때문에 그 아래로 내려왔다는 사실 자체가 "위의 어느 것에도 해당하지 않음"을 뜻합니다. 이렇게 먼저 걸러 내고 빠져나가는 방식은 들여쓰기 깊이를 낮추고, 각 규칙을 독립적으로 읽을 수 있게 해 줍니다. 조건이 늘어날 때마다 오른쪽으로 밀려나던 코드가 위아래로만 길어지는 형태로 바뀝니다.

알아두면 좋은 점

조건을 다 쓴 뒤에는 각 분기마다 실제로 그리로 가는 입력을 하나씩 만들어 보세요. 위 예시의 세 줄짜리 print가 그것입니다. 어떤 분기로 가는 입력을 도저히 못 만들겠다면, 그 분기는 절대 실행되지 않는 죽은 코드일 가능성이 큽니다.

반복문은 쓰기 전에 세 가지를 정합니다

반복문이 어려운 이유는 문법이 아니라, 머릿속에서 여러 바퀴를 동시에 상상하려 들기 때문입니다. 100번 도는 코드를 한꺼번에 그리려 하면 누구든 헷갈립니다. 대신 쓰기 전에 세 가지만 말로 정하면 대부분 정리됩니다.

무엇을 도는가. 리스트인지, 숫자 범위인지, 파일의 줄인지를 먼저 정합니다. 이것이 정해지면 for를 쓸지 while을 쓸지가 거의 자동으로 결정됩니다. 돌 대상이 이미 손에 있으면 for, 언제 끝날지 모르면 while입니다.

언제 멈추는가. for는 대상이 끝나면 자동으로 멈추지만 while은 내가 멈춰야 합니다. 조건 안에서 쓰는 값이 루프 안에서 실제로 바뀌는지 확인하지 않으면 무한 루프가 됩니다. 무한 루프는 대개 문법 실수가 아니라 변수를 갱신하는 줄을 빠뜨린 결과입니다.

돌면서 무엇이 쌓이는가. 합계인지, 걸러진 목록인지, 찾은 첫 항목인지를 정하고 그 변수를 루프 밖에서 먼저 만들어 둡니다. 이 변수를 루프 안에서 선언하면 매 바퀴 초기화되어 결과가 사라집니다. 초보 단계의 반복문 버그 중 상당수가 여기에서 나옵니다. 변수를 상태로 읽는 관점은 변수는 결국 상태에 대한 이야기입니다에서 더 다뤘습니다.

경계 실수도 같은 방식으로 잡습니다. 리스트의 마지막 항목이 빠지거나 하나 더 도는 문제는, 첫 바퀴와 마지막 바퀴만 손으로 짚어 보면 거의 즉시 드러납니다. 열 개짜리 리스트를 가정하고 인덱스가 0에서 시작해 몇에서 끝나는지 종이에 적어 보는 것이 디버거보다 빠를 때가 많습니다.

TRACE

numbers = [3, 5, 8]
total = 0                 # 루프 밖에서 준비

for n in numbers:
    if n % 2 == 0:
        total = total + n

# 1바퀴: n=3, 홀수 -> total 0
# 2바퀴: n=5, 홀수 -> total 0
# 3바퀴: n=8, 짝수 -> total 8
print(total)              # 8

막히면 이렇게 바퀴마다 값을 주석으로 적어 봅니다. 세 줄이면 대부분 원인이 보입니다.

조건이 셋을 넘으면 표로 먼저 정리합니다

조건이 두 개일 때는 머리로 됩니다. 세 개를 넘어가면 경우의 수가 빠르게 늘어나서, 코드를 쓰면서 동시에 따지려고 하면 반드시 빠뜨리는 조합이 생깁니다. 이때는 코드를 잠깐 멈추고 입력 조합을 표로 적는 편이 빠릅니다.

앞의 예시를 조금 늘려 보겠습니다. 점수, 결석, 과제 제출 여부 세 가지가 있다고 하면 조합은 여덟 가지입니다. 여덟 줄을 적어 놓고 각 줄의 답을 채워 보면, 요구사항에 답이 정해져 있지 않은 칸이 드러납니다. 그 빈칸이 바로 질문해야 할 지점입니다. 코드를 쓰다가 발견하면 이미 절반쯤 짠 뒤라 되돌리기가 번거롭지만, 표에서 발견하면 아직 아무것도 안 짠 상태입니다.

표를 다 채우고 나면 코드가 표를 그대로 옮긴 형태가 됩니다. 어떤 줄이 다른 줄을 흡수하는지도 눈에 보여서, 여덟 개의 분기가 실제로는 세 개로 줄어드는 경우가 흔합니다. 이 과정을 거치면 조건문은 "머리를 굴려서 맞히는 것"이 아니라 "이미 정해진 답을 옮겨 적는 것"이 됩니다. 그리고 이 표는 나중에 테스트 케이스를 만들 때 그대로 재료가 됩니다. 표의 한 줄이 테스트 한 개입니다. 이 연결이 궁금하다면 아주 작은 케이스로 테스트 시작하기를 보시면 됩니다.

이 조언을 너무 밀어붙이면 생기는 일

조건을 잘게 나누고 중첩을 없애라는 말은 널리 통하지만, 항상 맞지는 않습니다. 규칙으로 굳혀 놓고 기계적으로 적용하면 오히려 읽기 어려운 코드가 나오는 상황이 두 가지 있습니다.

중첩이 오히려 읽기 쉬운 경우가 있습니다. 서로 관련 없는 조건 다섯 개가 나란히 있는 것보다, 큰 조건 안에 작은 조건이 들어 있는 구조가 실제 규칙과 더 닮았다면 그대로 두는 편이 낫습니다. 중첩을 없애려고 조건을 and로 길게 이어 붙이면, 한 줄이 화면을 넘어가면서 어느 조건이 왜 붙었는지 알 수 없게 됩니다. 목표는 중첩을 줄이는 것이 아니라 규칙을 드러내는 것입니다.

언어가 주는 표현이 항상 더 나은 것도 아닙니다. 파이썬의 리스트 컴프리헨션이나 자바스크립트의 map과 filter는 반복문을 한 줄로 줄여 줍니다. 익숙해지면 확실히 읽기 좋습니다. 다만 입문 단계에서 이것부터 쓰기 시작하면, 중간 값이 눈에 안 보여서 결과가 이상할 때 어디를 봐야 할지 모르게 됩니다. 한 줄짜리 표현에는 print를 끼워 넣을 자리가 없습니다.

그래서 순서를 권한다면, 먼저 일반 반복문으로 풀고 결과가 맞는 것을 확인한 뒤에 한 줄로 줄여 보는 쪽입니다. 이 과정을 몇 번 거치면 두 형태가 같은 일을 한다는 감각이 생기고, 그때부터는 어느 쪽을 써도 됩니다. 반대로 처음부터 짧게 쓰는 데 집착하면, 실력이 아니라 취향만 먼저 생깁니다.

자주 하는 실수

반복문 안에서 돌고 있는 리스트 자체를 수정하면 항목이 건너뛰어집니다. 걸러 내고 싶다면 새 리스트를 만들어 담고, 원본은 그대로 두세요. 이 증상은 에러 없이 조용히 틀린 답을 내기 때문에 찾기가 특히 까다롭습니다.

막혔을 때 스스로에게 던지는 질문

  • 이 요구사항에서 다른 것보다 먼저 판정되어야 하는 조건은 무엇인가?
  • 각 분기로 실제로 들어가는 입력을 하나씩 만들 수 있는가?
  • 이 루프는 무엇을 돌고, 언제 멈추고, 무엇을 쌓는가?
  • 결과를 담는 변수를 루프 밖에서 준비했는가?
  • 첫 바퀴와 마지막 바퀴의 값을 손으로 적어 봤는가?

문법 자체를 다시 확인하고 싶다면 Python 기초 가이드나 JavaScript 기초 가이드를 열어 두고 예제를 바꿔 가며 돌려 보는 편이 좋습니다. 오류 메시지가 나오는데 읽는 법이 낯설다면 에러 메시지를 처음 읽을 때가 먼저입니다.

흐름을 읽는 힘은 복잡한 문제를 작은 순서로 자르는 힘에서 나옵니다.

코드 기초

Next Read

함수는 언제 나누는 게 좋을까

함수를 잘게 쪼개야 한다는 말은 자주 듣지만, 초보자는 어디서 끊어야 할지 감이 잘 안 온다. 가장 실용적인 기준만 골랐다.