콘텐츠로 이동

03. R 제어문

제어문(control statement)은 프로그램의 실행 흐름을 조건이나 반복에 따라 다르게 만들어주는 문법입니다. R의 제어문은 크게 조건문과 반복문으로 나뉩니다.

  • 조건문 — 조건에 따라 실행할 코드를 선택
  • if()
  • ifelse()
  • switch()
  • 반복문 — 같은 코드를 여러 번 반복 실행
  • for()
  • while()
  • repeat()

3.1 if()

if()가 사용되는 방식은 3가지입니다.

  • 조건이 참(TRUE)이면 해당 명령어를 실행
  • else를 붙여, 조건이 참이 아니면 다른 명령어를 실행
  • else if를 붙여, 여러 조건을 순서대로 검사
if (조건) {조건이 만족되면 실행}
if (조건) {조건이 만족되면 실행} else {조건이 만족되지 못하면 실행}
if (조건1) {조건1 만족 실행} else if (조건2) {조건2 만족 실행} else {불만족 실행}
# x가 0보다 크면 양수
x <- 7
if (x > 0) {
  print("양수")
}
#> [1] "양수"
# x가 0보다 크면 양수, 그렇지 않으면 음수
x <- -3
if (x > 0) {
  print("양수")
} else {
  print("음수")
}
#> [1] "음수"
# x가 90 이상이면 A, ..., 60 이상이면 D, 나머지(60미만)는 F
x <- 92
if (x >= 90) {
  print("A")
} else if (x >= 80) {
  print("B")
} else if (x >= 70) {
  print("C")
} else if (x >= 60) {
  print("D")
} else {
  print("F")
}
#> [1] "A"

아래처럼 브라켓({}) 없이도 if()를 쓸 수 있지만, 나중에 코드를 읽을 때 실행 범위가 헷갈리기 쉬우므로 추천하지 않습니다. 위 예제들처럼 브라켓을 쓰는 방식을 기본으로 삼으시길 권합니다.

# {}를 사용하지 않는 코드는 나중에 이해하기 어려우므로 추천하지 않습니다.
x <- 5
if (x > 0) print("양수") else print("음수")
#> [1] "양수"

조건이 2개 이상이거나 조건 결과를 반대로 뒤집어야 할 때는 논리 연산자를 함께 씁니다. &는 "그리고(AND)", |는 "또는(OR)", !는 조건을 반대로 뒤집는 역할을 합니다.

연산자 의미 수식 예
& AND x > 3 & x < 7
| OR x < 3 | x > 7
! NOT !(x > 3)
x <- 25
y <- 75
if (x > 0 & y < 100) {
  print("TRUE 입니다")
} else {
  print("FALSE 입니다")
}
#> [1] "TRUE 입니다"

주의 — if() 조건은 반드시 길이 1이어야 합니다 if()의 조건 자리에는 길이가 1인 논리값만 올 수 있습니다. R 4.2.0부터는 길이 2 이상인 벡터를 조건으로 넣으면 경고가 아니라 아예 오류가 발생하도록 강화되었습니다.

x <- c(TRUE, FALSE)
if (x) print("실행됨")
#> Error in if (x) print("실행됨") : the condition has length > 1

조건에 벡터가 들어갈 가능성이 있다면 any()나 all()로 미리 길이 1의 논리값으로 요약해서 써야 합니다. 벡터 전체를 조건별로 판단하고 싶다면 if() 대신 아래에서 다룰 ifelse()를 쓰는 것이 정답입니다.

주의 — } 다음 줄에 else를 두면 오류가 날 수 있습니다 아래 코드처럼 함수 본문({})이나 다른 블록 안에 들어 있지 않은, 최상위 레벨의 if() {...} 뒤에 줄을 바꿔서 else를 쓰면 콘솔에서든 스크립트를 실행해서든 오류가 납니다. R이 }까지를 이미 완결된 하나의 명령문으로 보고 먼저 실행해버린 뒤, 다음 줄의 else를 아무 짝도 없는 채로 만나기 때문입니다.

x <- 5
if (x > 0) {
  print("양수")
}
else {
  print("음수 또는 0")
}
#> [1] "양수"
#> Error: unexpected 'else' in "else"

반면 함수 본문처럼 더 바깥쪽 {}로 감싸인 블록 안에서는 }와 else가 줄바꿈되어 있어도 문제없이 동작합니다. 바깥쪽 중괄호가 닫히기 전까지는 R이 전체를 하나의 미완성 구문으로 보고 계속 이어서 읽기 때문입니다.

f <- function(x) {
  if (x > 0) {
    print("양수")
  }
  else {
    print("음수 또는 0")
  }
}
f(5)
#> [1] "양수"

가장 안전한 습관은 else를 항상 } 바로 뒤 같은 줄에 붙여 쓰는 것입니다: } else {.

참고 — %||% 연산자 (R 4.4.0+) 객체가 NULL일 때만 대체값을 쓰고 싶다면 if (is.null(x)) 대체값 else x라고 길게 쓰는 대신, R 4.4.0부터 base R에 추가된 %||% 연산자를 쓸 수 있습니다. (그 전까지는 tidyverse의 rlang 패키지에서만 제공되던 기능입니다.) NULL인지만 검사할 뿐 길이 0인 벡터는 그대로 통과시킨다는 점에 유의하세요.

default_title <- NULL
default_title %||% "제목 없음"

x <- numeric(0)
x %||% "대체값"
#> [1] "제목 없음"
#> numeric(0)

3.2 ifelse()

실무에서는 벡터(예: 여러 학생의 점수, 데이터 프레임의 한 열) 전체를 한꺼번에 "조건을 만족하면 이 값, 아니면 저 값"으로 바꿔야 하는 경우가 훨씬 많습니다. 벡터의 원소마다 if()를 반복해서 적용하려고 for() 반복문을 직접 짜는 것은 번거롭고 느립니다.

ifelse(조건, 조건이 참일 때 값, 조건이 거짓일 때 값)은 조건 자리에 벡터를 그대로 넣을 수 있고, 결과도 원소 개수만큼의 벡터로 돌려줍니다.

  • ifelse(test, yes, no)는 논리 벡터 test의 각 원소가 TRUE면 yes의 대응 원소를, FALSE면 no의 대응 원소를 골라 test와 같은 길이의 벡터로 반환합니다. test의 원소가 NA이면 결과도 NA가 됩니다. if()와 달리 조건의 길이 제한이 없다는 점이 핵심 차이입니다.
x <- 5
ifelse(x %% 2 == 0, "짝수", "홀수")
#> [1] "홀수"
x <- c(1, 2, 3, 4, 5, NA, 7)
ifelse(x %% 2 == 0, "짝수", "홀수")
#> [1] "홀수" "짝수" "홀수" "짝수" "홀수" NA     "홀수"

결측치(NA)가 있는 자리는 조건을 판단할 수 없으므로, 결과도 그대로 NA로 남습니다.

ifelse()를 중첩하여 사용할 수도 있습니다.

scores <- c(95, 82, 71, 55)
grade <- ifelse(scores >= 90, "A",
          ifelse(scores >= 80, "B",
           ifelse(scores >= 70, "C", "F")))
grade
#> [1] "A" "B" "C" "F"

다만 이렇게 ifelse()를 3단, 4단으로 중첩하면 코드가 금방 읽기 어려워집니다. 분기가 세 갈래 이상이고 조건식이 아니라 정해진 값(지시값) 단위로 나뉘는 상황이라면, 다음 절의 switch()가 더 적합할 수 있습니다.

3.3 switch()

정해진 몇 가지 지시값(예: "한국인", "영국인", "미국인")에 따라 다른 코드를 실행해야 할 때, if() ... else if() ... else if() ...를 계속 이어 쓰면 조건식(x == "...")이 매번 반복되어 코드가 장황해집니다.

switch()는 지시값 하나를 보고 그에 대응하는 코드만 골라 실행합니다.

  • switch(지시값, 지시값1 = 실행1, 지시값2 = 실행2, 지시값3....)
x <- "한국인"
switch(x,
       "영국인" = print("영국인입니다!"),
       "한국인" = print("한국인입니다!"),
       "미국인" = print("미국인입니다!"),
       stop("지시값이 정확하지 않습니다!")
)
#> [1] "한국인입니다!"

지시값을 문자 대신 숫자로 주면, 나열된 순서(위치) 기준으로 골라줍니다.

x <- 2
switch(x,
       "영국인" = print("영국인입니다!"),
       "한국인" = print("한국인입니다!"),
       "미국인" = print("미국인입니다!"),
       stop("지시값이 정확하지 않습니다!")
)
#> [1] "한국인입니다!"

아래는 R 도움말(?switch)에 나오는 예제를 응용한 것으로, 평균·중위수 등 계산 방식을 문자열로 골라서 쓰도록 만든 함수입니다.

center <- function(x, type) {
  switch(type,
         mean = mean(x),
         median = median(x),
         trimmed = mean(x, trim = .1),
         stop("type 값을 잘못 입력하였습니다!")
         )
}
set.seed(123)
x <- sample(x = 1:30, size = 10, replace = TRUE)
center(x, "mean")
#> [1] 13.7

center(x, "median")
#> [1] 14.5

center(x, 2)  # median 호출
#> [1] 14.5

여러 지시값이 같은 코드를 실행해야 한다면, = 오른쪽을 비워두어 "다음 지시값으로 그대로 흘러 내려가게(fall-through)" 만들 수 있습니다. C 언어의 switch와 같은 방식입니다.

legs <- function(x) {
  switch(x,
         "소" = ,
         "말" = ,
         "개" = 4,
         "사람" = ,
         "닭" = 2,
         "식물" = 0,
         stop("모르는 대상입니다")
  )
}
legs("소")
#> [1] 4

legs("개")
#> [1] 4

legs("사람")
#> [1] 2

legs("식물")
#> [1] 0

switch(EXPR, ...)는 EXPR(길이 1의 문자 또는 정수)에 대응하는 ...의 값을 실행하고 반환합니다. EXPR이 문자인데 이름이 일치하는 지시값이 없거나, 정수인데 범위를 벗어나면 위 예제들처럼 마지막에 이름 없는 인자(대개 stop())를 넣지 않는 한, 오류 없이 NULL을 보이지 않게(invisibly) 반환합니다.

switch("영어", "한국어" = "한국인", "일본어" = "일본인")
#> (아무것도 출력되지 않음 — NULL을 invisible하게 반환)

그래서 실무에 사용할 때는 항상 마지막 자리에 stop("...")을 넣어, 예상 밖의 지시값이 들어왔을 때 조용히 NULL이 반환되어 뒤에서 원인 모를 오류로 이어지는 상황을 막는 것이 안전합니다.


3.4 for()

for()는 정해진 시퀀스(벡터, 리스트 등)를 처음부터 끝까지 순회하며 반복 실행할 때 사용합니다.

for (item in vector) {반복 실행}
# 1부터 5까지 출력
for (i in 1:5) {
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# x의 각 요소를 순서대로 출력
x <- LETTERS[1:5]
for (i in 1:5) {
  print(x[i])
}
#> [1] "A"
#> [1] "B"
#> [1] "C"
#> [1] "D"
#> [1] "E"

seq_along(x)는 x의 길이만큼 일련번호(1:length(x)와 같은 결과)를 만들어줍니다.

x <- LETTERS[1:5]
for (i in seq_along(x)) {
  print(x[i])
}
#> [1] "A"
#> [1] "B"
#> [1] "C"
#> [1] "D"
#> [1] "E"

주의 — 1:length(x) 대신 seq_along(x)를 쓰세요 겉보기엔 1:length(x)와 seq_along(x)가 똑같아 보이지만, x가 빈 벡터(길이 0)일 때 결과가 완전히 달라집니다. length(x)가 0이면 1:length(x)는 1:0, 즉 c(1, 0)이 되어버려서, 반복이 없어야 정상인데 엉뚱하게 두 번 반복됩니다.

x <- numeric(0)
for (i in 1:length(x)) {
  print(i)
}
#> [1] 1
#> [1] 0

# seq_along()은 길이 0이면 반복 자체가 일어나지 않아 안전합니다
for (i in seq_along(x)) {
  print(i)
}
#> (아무것도 출력되지 않음)

함수 안에서 for()에 넘길 벡터가 비어 있을 가능성이 조금이라도 있다면, 1:length(x)가 아니라 seq_along(x)(또는 seq_len(length(x)))를 쓰는 습관을 들이는 것이 안전합니다.

for()에서 사용하는 반복 변수(아래 예제의 i)는 이미 다른 곳에서 같은 이름으로 쓰이고 있다면 값이 덮어써지므로 주의해야 합니다.

# for 함수에서 사용된 아이템 변수 i는 변함
i <- 55
print(i)
for (i in 1:3) {
  print(paste0(i, "입니다~"))
}
print(i)
#> [1] 55
#> [1] "1입니다~"
#> [1] "2입니다~"
#> [1] "3입니다~"
#> [1] 3

반복을 강제로 제어하고 싶을 때는 next와 break를 사용합니다. next는 현재 반복만 건너뛰고 다음 반복으로 넘어가며, break는 for() 전체를 즉시 빠져나옵니다.

# 3미만이면 이후 명령을 실행하지 않고 다음으로 넘어가고
# 5초과이면 for 반복에서 빠져 나옴
for (i in 1:10) {
  if (i < 3) {
    next
  }
  print(i)
  if (i >= 5) {
    break
  }
}
#> [1] 3
#> [1] 4
#> [1] 5

for() 안에서 산출되는 결과를 저장할 변수(아래 예제의 out)는 반복 전에 미리 크기를 정해 만들어 두어야 오류 가능성도 낮아지고 속도도 빨라집니다.

set.seed(1)
means <- c(1, 30, 50)
sds <- c(1, 2, 3)
out <- vector("list", length(means))
for (i in seq_along(means)) {
  out[[i]] <- rnorm(5, means[[i]], sds[[i]])
}
print(out)
#> [[1]]
#> [1] 0.3735462 1.1836433 0.1643714 2.5952808 1.3295078
#>
#> [[2]]
#> [1] 28.35906 30.97486 31.47665 31.15156 29.38922
#>
#> [[3]]
#> [1] 54.53534 51.16953 48.13628 43.35590 53.37479

for() 안에 다시 for()를 중첩할 수도 있습니다. 참고로 seq_along(x) == seq_len(length(x))입니다.

x <- matrix(1:6, nrow = 2, ncol = 3)
for (i in seq_len(nrow(x))) {
  for (j in seq_len(ncol(x))) {
    print(x[i, j])
  }
}
#> [1] 1
#> [1] 3
#> [1] 5
#> [1] 2
#> [1] 4
#> [1] 6

R 전문가들은 R의 벡터 연산 특성을 살려 처리 성능을 높이려면, 가급적 for() 반복문보다는 sapply()·vapply()·lapply() 같은 apply 계열 함수나 Map() 같은 함수형 도구를 이용한 벡터 기반 반복을 추천합니다.

참고 — for·while·repeat의 반환값 for(), while(), repeat()는 모두 NULL을 보이지 않게(invisibly) 반환합니다. 즉 반복문 자체는 "값"을 만들어내는 것이 아니라 부수효과(출력, 객체 수정 등)를 위해 쓰는 문법입니다. 또한 for()가 끝난 뒤 반복 변수는 시퀀스의 마지막 원소 값을 그대로 가지고 있으며, 시퀀스의 길이가 0이었다면(=한 번도 반복하지 않았다면) NULL이 됩니다.

for (i in 1:5) {}
i
#> [1] 5

x0 <- numeric(0)
for (i in x0) {}
i
#> NULL

3.5 while()

while()은 for()와 비슷하지만 더 유연합니다. 정해진 시퀀스가 아니라, 조건이 참인 동안 계속 반복합니다.

while (조건문) {반복 실행}
i <- 1
while (i < 3) {
  print(i)
  i <- i + 1
}
#> [1] 1
#> [1] 2
count <- 3
while (count <= 5) {
  print(c(count, count - 1, count - 2))
  count <- count + 1
}
#> [1] 3 2 1
#> [1] 4 3 2
#> [1] 5 4 3
set.seed(123)
x <- 5
while (x >= 3 & x <= 10) {
  coin <- rbinom(1, 1, 0.5)  # 0과 1 무작위로 추출
  if (coin == 1) {
    x <- x + 1
  } else {
    x <- x - 1
  }
}
print(x)
#> [1] 11

3.6 repeat()

repeat()도 for()나 while()처럼 반복 작업에 쓰이지만, 조건 검사 없이 무조건 반복한다는 점이 다릅니다. break를 만날 때까지 계속 반복하므로, 무한 루프에 빠지지 않으려면 반드시 break를 포함해야 합니다.

repeat { 
  반복 실행
  if (조건) { break }
}
# 1 ~ 5까지 출력 (x가 6이 되면 반복 종료)
x <- 1
repeat {
  print(x)
  x <- x + 1
  if (x == 6) { break }
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5