개발? 새발!
개발? 새발!

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

2025.12.14

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

관련 이미지 1

이미지 출처: 개발자 소들이 - 티스토리

이름 그대로 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

관련 이미지 2

이미지 출처: 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 같은 거)
• 분할 정복 알고리즘 (퀵소트, 머지소트)
• 백트래킹 문제 풀 때

대신 성능이 중요한 곳에서는 반복문으로 대체할 수 있는지도 꼭 같이 고민해보는 편이에요.

관련 이미지 3

이미지 출처: velog

마무리하며

오늘은 Swift 재귀함수에 대해 정리해봤어요!

재귀함수는 자기 자신을 호출하는 함수로, 종료 조건만 잘 설정하면 복잡한 문제도 우아하게 풀어낼 수 있습니다.

다만 성능 이슈나 스택 오버플로우 같은 문제가 생길 수 있으니, 상황에 맞게 사용하는 게 중요해요. 트리/그래프처럼 재귀 구조가 자연스러운 경우엔 재귀가 정말 강력하고, 단순 반복 작업은 for문이 더 나은 선택일 수 있거든요.

여기까지 읽으셨다면 이제 Swift 재귀함수 감 잡으신 거예요!
오늘도 즐거운 코딩 하세요 🙂


이전 글 보기

KMP(Kotlin Multiplatform)로 Android와 iOS 코드 공유하기 - 이제는 한 번만 짜도 되는 시대!

PyTorch 딥러닝 프레임워크, 연구부터 프로덕션까지 완벽 정복

이 블로그의 다음 이야기도 받아보세요

새 글 구독 (RSS)