Menu

R의 for 반복문: 문법, seq_along, break, next, 그리고 반복하지 말아야 할 때

R에서 for 반복문이 동작하는 방식입니다. 벡터와 인덱스 순회, 벡터를 키우는 함정 없이 결과 모으기, next와 break, 그리고 벡터화 호출이 반복문보다 나은 때를 다룹니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

기본 for 반복문

R의 for 반복문은 벡터나 리스트의 각 원소마다 코드 블록을 한 번씩 실행합니다. 반복 변수가 각 값을 차례로 받습니다.

원소 세 개, 회차 세 번입니다. 준비할 카운터도, 유지할 조건도 없습니다. for (fruit in fruits)는 "fruits 안의 각 fruit에 대해"로 읽히고, 실제로 하는 일도 정확히 그것입니다. 같은 형태로 리스트의 원소, 숫자 수열(for (i in 1:10)), 또는 names()를 통한 이름들도 순회할 수 있습니다.

반복문 안에서의 출력에는 print()나 cat()이 필요합니다

콘솔에서는 x를 입력하면 값이 출력됩니다. 반복문 본문 안에서는 그렇지 않습니다. 자동 출력은 최상위 수준에서만 일어나고, 반복문 안의 단독 표현식은 평가된 뒤 버려집니다.

그래서 반복문이 "아무것도 안 할" 때 가장 먼저 확인할 것이 이것입니다. 값을 빠르게 확인할 때는 print()를, 출력 한 줄을 직접 구성할 때는 cat()을 쓰세요("\n"을 잊지 마세요. cat()은 붙여 주지 않습니다).

seq_along()으로 인덱스 순회하기

값뿐 아니라 위치가 필요할 때가 있습니다. 같은 인덱스로 다른 벡터에 써넣거나 번호가 매겨진 목록을 출력할 때죠. seq_along()으로 인덱스를 순회하세요.

오래된 코드에서는 for (i in 1:length(prices))를 보게 되는데, 여기에는 실제 버그가 숨어 있습니다. 벡터가 비어 있으면 length()가 0이고, 1:0은 "반복 없음"이 아닙니다. 콜론은 거꾸로 셉니다.

1:length(empty)를 도는 반복문은 두 번 실행되며 존재하지 않는 원소를 인덱싱합니다(empty[1]NA이고, 대입하면 유령 항목이 생깁니다). seq_along()은 빈 벡터에 대해 빈 수열을 돌려주므로 본문이 아예 실행되지 않습니다. seq_along()을 습관으로 만드세요. 벡터가 아니라 개수를 가지고 있을 때는 형제 함수 seq_len(n)이 같은 일을 합니다.

결과 모으기: 키우지 말고 미리 할당하세요

결과를 쌓아 올릴 때 처음 떠오르는 방식은 대개 이렇습니다.

squares <- c()
for (i in 1:10000) {
  squares <- c(squares, i^2)   # copies the ENTIRE vector every pass
}

동작하기는 하지만, c(squares, ...)는 매번 완전히 새로운 벡터를 할당하고 기존 원소를 모두 복사합니다. 끝날 때쯤이면 10,000개를 만들기 위해 약 5천만 개의 숫자를 복사한 셈입니다. 벡터를 키우는 이 안티패턴이 사람들이 R 반복문을 느리다고 말하는 가장 큰 이유입니다.

해결책은 결과를 처음부터 전체 크기로 만들어 두고 인덱스로 대입하는 것입니다.

숫자가 아닌 결과에도 같은 아이디어가 적용됩니다. character(n), logical(n), 또는 임의의 것들을 담는 리스트에는 vector("list", n)을 쓰세요. 미리 할당한 반복문은 충분히 빠릅니다.

next와 break

두 키워드가 본문 안에서 반복문을 조종합니다. next는 현재 회차를 포기하고 다음 원소로 건너뛰며, break는 반복문을 완전히 빠져나옵니다.

2 4 6 8이 출력됩니다. 홀수는 next가 건너뛰고, 10이 오면 출력 전에 break가 반복문을 끝냅니다. 예외 경우를 일찍 걸러 낼 때는 next를(본문을 덜 들여쓰게 해 줍니다), 찾던 것을 발견해 더 진행할 이유가 없을 때는 break를 쓰세요.

중첩 반복문

반복문 본문은 또 다른 반복문을 담을 수 있습니다. 바깥 반복문의 매 회차마다 안쪽 반복문이 끝까지 실행됩니다. 고전적인 예시는 구구단표입니다.

각 행 i에 대해 안쪽 반복문이 모든 열 j를 훑고, 안쪽 반복문이 끝난 뒤 그 행의 줄바꿈이 출력됩니다. 중첩은 두 단계까지는 괜찮습니다. 세 단계 이상이면 본문이 함수가 되어야 한다는 신호이거나, 계산 전체가 outer()나 행렬 연산이 되어야 한다는 신호입니다.

중첩은 벡터들의 리스트를 순회하는 경우도 포함합니다. 바깥 반복문은 리스트를, 안쪽 반복문은(또는 더 나은 방법인 벡터화 호출은) 각 원소를 다룹니다.

결국 안쪽 반복문이 없다는 점에 주목하세요. mean()이 안쪽 벡터 전체를 한 번의 호출로 처리합니다. 이 관찰은 일반화되며, 그래서 솔직한 이야기로 넘어가게 됩니다.

반복하지 말아야 할 때

R은 벡터화된 언어입니다. 기본 연산이 이미 벡터 전체에 한 번에 동작합니다. 각 원소를 하나씩 변환하는 반복문은 종종 한 줄짜리를 길게 쓴 것에 불과합니다.

벡터화된 형태가 존재한다면 언제나 그쪽을 택하세요. 더 짧고, 틀리기 어렵고, 더 빠릅니다. 리스트의 모든 원소에 함수를 적용하는 일에는 apply 계열(sapply, lapply, vapply)이 같은 역할을 합니다.

하지만 이것을 "반복문은 나쁘다"로 맹신하지는 마세요. 회차가 이전 회차에 의존할 때(상태를 이어 가는 계산, 시뮬레이션), 파일 쓰기 같은 부수 효과를 다룰 때, 또는 단순히 반복문 쪽이 여러분과 독자가 한눈에 이해하는 형태일 때 반복문은 올바른 도구입니다. 명확한 반복문이 아무도 해석할 수 없는 영리한 한 줄보다 낫습니다.

핵심 정리

  • for (x in v) { ... }는 원소마다 본문을 한 번씩 실행합니다. 카운터 관리가 필요 없습니다.
  • 반복문 안에서는 print()cat()으로 명시적으로 출력하세요. 단독 표현식은 버려집니다.
  • 위치를 순회할 때는 1:length(v)가 아니라 seq_along(v)를 쓰세요. 전자는 빈 벡터에서 두 번 실행됩니다.
  • 결과는 numeric(n) / vector("list", n)으로 미리 할당하고 인덱스로 대입하세요. c()로 키우면 매 회차마다 전부 복사됩니다.
  • next는 한 회차를 건너뛰고 break는 빠져나옵니다. 벡터화 연산이 있으면 그쪽을 택하되, 읽기 좋은 반복문을 두려워하지는 마세요.

다음 순서는 원소마다 한 번이 아니라 무슨 일이 일어날 때까지 도는 반복문, 즉 whilerepeat입니다.

자주 묻는 질문

R에서 for 반복문은 어떻게 작성하나요?

for (x in v) { ... } 형태로 씁니다. 반복 변수 x가 벡터(또는 리스트) v의 각 원소를 차례로 받고, 본문이 원소마다 한 번씩 실행됩니다. 값 대신 위치를 순회하려면 for (i in seq_along(v))를 쓰고 v[i]로 인덱싱하세요.

R에서 1:length(v) 대신 seq_along을 쓰는 이유는 무엇인가요?

v가 비어 있으면 length(v)가 0이므로 1:length(v)1:0, 즉 두 원소 벡터 c(1, 0)이 되고, 존재하지 않는 원소를 상대로 본문이 두 번 실행됩니다. seq_along(v)는 빈 벡터에 대해 빈 수열을 돌려주므로 반복문이 올바르게 0번 실행됩니다.

R의 for 반복문이 왜 아무것도 출력하지 않나요?

R의 자동 출력은 콘솔 최상위 수준에서 입력한 표현식에만 일어납니다. 반복문 본문 안에서 x 같은 단독 표현식은 평가된 뒤 버려집니다. print(x)로 감싸거나, 출력 형식을 직접 정하고 싶다면 cat(...)을 사용하세요.

R의 for 반복문은 느린가요?

반복문 자체는 괜찮습니다. 느린 것은 result <- c(result, ...)처럼 반복문 안에서 벡터를 키우는 것으로, 매 회차마다 벡터 전체를 복사합니다. numeric(n)이나 vector("list", n)으로 미리 할당하고 인덱스로 대입하면 반복문 성능은 전혀 문제없습니다. 다만 벡터화 연산이나 apply 계열 호출이 같은 아이디어를 표현할 수 있다면 대개 그쪽이 더 빠르고 더 명확합니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기