Swift 재귀함수 완벽 정복! 나를 부르는 함수의 마법 🪄

오늘은 Swift에서 재귀함수(Recursive Function)를 깔끔하게 정리해보려고 해요.
재귀함수라고 하면 왠지 어렵고 복잡하게 느껴질 수 있는데요, 사실은 자기 자신을 다시 호출하는 함수일 뿐이에요. 다만, 제대로 활용하면 코드가 정말 간결해지고 표현도 우아해져서 알아두면 꽤 유용하답니다.
그럼 바로 Swift 재귀함수의 세계로 들어가볼게요!
재귀함수가 뭐냐구요?
재귀함수는 말 그대로 자기 자신을 호출하는 함수예요.
“어? 함수가 자기 자신을 부른다고??” 싶을 수 있는데, 생각보다 활용처가 많습니다!
재귀함수의 핵심은 딱 두 가지예요:
1. 종료 조건 (Base Case) - 언제 멈출지 정하기!
2. 재귀 호출 (Recursive Case) - 어떤 형태로 자기 자신을 다시 부를지!
종료 조건이 없으면 무한 루프처럼 계속 돌아가다가 스택 오버플로우로 앱이 터질 수 있어요. 꼭 주의해야 합니다!
가장 기본적인 예제 - 팩토리얼
팩토리얼은 재귀함수 설명할 때 빠질 수 없는 대표 예제죠.
5! = 5 × 4 × 3 × 2 × 1 = 120
이걸 재귀로 생각해보면, 5! = 5 × 4! 이고, 4! = 4 × 3! 처럼 계속 쪼개지는 구조예요.
func factorial(_ n: Int) -> Int {
// 종료 조건: 0! = 1
if n <= 1 {
return 1
}
// 재귀 호출: n! = n × (n-1)!
return n * factorial(n - 1)
}
print(factorial(5)) // 120
print(factorial(3)) // 6
이미지 출처: 개발자 소들이 - 티스토리
이름 그대로 factorial 함수가 자기 자신(factorial)을 다시 호출하고 있죠.
어떻게 동작하는지 한 번 흐름을 따라가 보면요,
factorial(5)
└─ 5 * factorial(4)
└─ 4 * factorial(3)
└─ 3 * factorial(2)
└─ 2 * factorial(1)
└─ 1 반환!
이렇게 쭉 내려가다가 n이 1이 되면 종료 조건에 걸려서 1을 반환하고, 그 결과가 역순으로 곱해지면서 다시 올라오는 방식이에요.
피보나치 수열로 재귀 이해하기
피보나치도 재귀함수로 표현하기 정말 좋은 예제예요.
피보나치 수열은 앞의 두 수를 더해서 다음 수를 만드는 수열이죠.
0, 1, 1, 2, 3, 5, 8, 13, 21...
func fibonacci(_ n: Int) -> Int {
// 종료 조건
if n <= 1 {
return n
}
// 재귀 호출
return fibonacci(n - 1) + fibonacci(n - 2)
}
print(fibonacci(6)) // 8
print(fibonacci(10)) // 55
다만 이 방식은 실제로는 비효율적이에요.
왜냐하면 같은 값을 계속 반복해서 계산하거든요. 예를 들어 fibonacci(5)를 구하는 과정에서 fibonacci(3)을 여러 번 다시 계산하는 식으로요.
그래서 실무에서는 메모이제이션(Memoization)을 적용하거나, 경우에 따라 반복문으로 바꿔서 구현하는 편이 더 나을 때도 많습니다.
배열 관련 재귀함수 예제
배열의 모든 요소를 더하는 함수도 재귀로 만들어볼게요.
func sumArray(_ arr: [Int]) -> Int {
// 종료 조건: 빈 배열이면 0 반환
if arr.isEmpty {
return 0
}
// 첫 번째 요소 + 나머지 배열의 합
return arr[0] + sumArray(Array(arr.dropFirst()))
}
let numbers = [1, 2, 3, 4, 5]
print(sumArray(numbers)) // 15
배열의 첫 요소를 더하고, 나머지 배열에 대해 다시 sumArray를 호출하는 구조예요. 배열이 비면 0을 반환하면서 재귀가 종료됩니다!
문자열 뒤집기도 재귀로!
재귀함수는 문자열 처리에도 꽤 유용하게 쓸 수 있어요.
func reverseString(_ str: String) -> String {
// 종료 조건
if str.count <= 1 {
return str
}
// 첫 글자를 제외한 나머지를 뒤집고 + 첫 글자를 뒤에 붙임
let firstChar = str.first!
let remaining = String(str.dropFirst())
return reverseString(remaining) + String(firstChar)
}
print(reverseString("Swift")) // tfiwS
print(reverseString("Hello")) // olleH
이미지 출처: Seol-Hee! 💫 - 티스토리
"Swift"가 어떻게 뒤집히는지 흐름을 보면요,
reverseString("Swift")
└─ reverseString("wift") + "S"
└─ reverseString("ift") + "w"
└─ reverseString("ft") + "i"
└─ reverseString("t") + "f"
└─ "t" (종료!)
이후에는 역순으로 합쳐지면서 최종적으로 "tfiwS"가 됩니다.
실전 예제 - 이진 탐색 트리 순회
이제 조금 더 실무적인 예제로 넘어가볼게요.
이진 트리를 순회할 때 재귀함수가 정말 유용한데요, 트리 구조 자체가 재귀적으로 정의되는 구조라서 자연스럽게 재귀가 잘 맞아요.
class TreeNode {
var value: Int
var left: TreeNode?
var right: TreeNode?
init(_ value: Int) {
self.value = value
}
}
// 중위 순회 (In-order)
func inorderTraversal(_ node: TreeNode?) {
guard let node = node else { return }
inorderTraversal(node.left)
print(node.value, terminator: " ")
inorderTraversal(node.right)
}
// 트리 구성
let root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(6)
root.left?.left = TreeNode(1)
root.left?.right = TreeNode(3)
inorderTraversal(root) // 1 2 3 4 6
왼쪽 서브트리 → 현재 노드 → 오른쪽 서브트리 순서로 방문하는 중위 순회예요. 재귀 없이 구현하려면 스택을 사용해야 하는데, 재귀로 하면 이렇게 간단하게 표현할 수 있죠.
디렉토리 탐색하기
파일 시스템 탐색에서도 재귀함수는 찰떡입니다.
import Foundation
func listFiles(at path: String, depth: Int = 0) {
let fileManager = FileManager.default
guard let contents = try? fileManager.contentsOfDirectory(atPath: path) else {
return
}
for item in contents {
let fullPath = (path as NSString).appendingPathComponent(item)
let indent = String(repeating: " ", count: depth)
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: fullPath, isDirectory: &isDirectory) {
print("\(indent)📁 \(item)")
if isDirectory.boolValue {
// 디렉토리면 재귀 호출!
listFiles(at: fullPath, depth: depth + 1)
}
}
}
}
디렉토리 안에 또 디렉토리가 있으면, 그 안으로 계속 들어가면서 탐색하게 되는 구조예요.
재귀함수 사용할 때 주의할 점!!!!!
1️⃣ 종료 조건 꼭 만들기
종료 조건이 없으면 무한 호출로 이어져서 스택 오버플로우로 앱이 죽을 수 있어요. 꼭꼭 확인해야 합니다!
2️⃣ 성능 고려하기
재귀는 함수 호출 오버헤드가 있어서 반복문보다 느릴 수 있어요. 특히 호출 깊이가 깊어질수록 더 부담이 커집니다. 피보나치처럼 중복 계산이 많으면 더 비효율적이구요.
3️⃣ 스택 크기 제한
재귀 호출이 너무 깊어지면 스택 메모리가 부족해질 수 있어요. Swift는 기본적으로 꼬리 재귀 최적화(Tail Call Optimization)를 지원하지 않아서 더 조심해야 합니다.
4️⃣ 가독성 vs 효율성
재귀는 코드가 간결하고 직관적일 수 있지만, 항상 최선은 아니에요. 상황에 따라 반복문과 재귀 중 더 적절한 방법을 선택하는 게 중요합니다.
반복문으로 바꿔보기
사실 모든 재귀함수는 반복문으로 바꿀 수 있어요.
팩토리얼을 반복문으로 바꾸면 이렇게 됩니다.
func factorialIterative(_ n: Int) -> Int {
var result = 1
for i in 1...n {
result *= i
}
return result
}
print(factorialIterative(5)) // 120
어때요? 훨씬 효율적이죠?
그럼 재귀는 왜 쓰냐구요? 트리나 그래프처럼 재귀적 구조를 다룰 때, 또는 문제 자체가 재귀적으로 정의될 때는 코드가 훨씬 직관적이고 이해하기 쉬워지기 때문이에요.
실무에서 언제 재귀함수 쓸까?
제 경험상 이런 경우에 재귀함수를 많이 사용합니다:
• JSON 파싱할 때 중첩된 구조 처리
• 트리 구조 데이터 다룰 때 (파일 시스템, 조직도 등)
• 그래프 탐색 알고리즘 (DFS 같은 거)
• 분할 정복 알고리즘 (퀵소트, 머지소트)
• 백트래킹 문제 풀 때
대신 성능이 중요한 곳에서는 반복문으로 대체할 수 있는지도 꼭 같이 고민해보는 편이에요.

이미지 출처: velog
마무리하며
오늘은 Swift 재귀함수에 대해 정리해봤어요!
재귀함수는 자기 자신을 호출하는 함수로, 종료 조건만 잘 설정하면 복잡한 문제도 우아하게 풀어낼 수 있습니다.
다만 성능 이슈나 스택 오버플로우 같은 문제가 생길 수 있으니, 상황에 맞게 사용하는 게 중요해요. 트리/그래프처럼 재귀 구조가 자연스러운 경우엔 재귀가 정말 강력하고, 단순 반복 작업은 for문이 더 나은 선택일 수 있거든요.
여기까지 읽으셨다면 이제 Swift 재귀함수 감 잡으신 거예요!
오늘도 즐거운 코딩 하세요 🙂
이전 글 보기
▶ KMP(Kotlin Multiplatform)로 Android와 iOS 코드 공유하기 - 이제는 한 번만 짜도 되는 시대!