[내일배움캠프 13~19주차](leafLog) 2.초기세팅 및 학습

yeseul jang·2026년 4월 3일

내일배움캠프

목록 보기
28/32

swift-dependencies 깃허브 주소 바로가기

📌 swift-dependencies

  • 전역 상태처럼 편하게 사용하면서도, 테스트에서는 쉽게 교체할 수 있도록 만든 DI 도구이다.(라이브러리)
  • 그 전까지는 DI를 하기 위해서 init을 통해 넘겨서 화면바뀌면 push하며 같이 VM를 넘겨 왔다. 하지만 화면의 수가 많아지고 더 효율적으로 관리해보고 싶어서 해당 라이브러리를 쓰기로 결정했다.
  • 이것만 보고 팀원분들이 swift-dependencies사용법을 다른 학습 없이 사용할 수 있도록 최대한 자세히 작성하려고 한다.

🔍 기본 개념(왜 DI (Dependency Injection)를 하는가)

  • 예를 들어 NetworkManger를 특정 VM에서 사용하면 가장 단순하게 한다면 이런식으로 하게 된다
  • 그럼 아예 소유하게 되어서 교체를 하거나 테스트에 어려움이 생긴다.
class NetworkManager {
    func fetch() { ... }
}

class ViewModel {
    let NetworkManager = NetworkManager() // 직접 생성 (문제)
}

🔍 protocol, init으로 의존성 주입하기

  • 그래서 이런 식으로 의존성을 주입하는 방식을 사용해왔다.
  • 이런식으로 NetworkManager프로토콜을 채택한 객체는 어떤것이든 쓰일 수 있도록 바꾸면 교체와 테스트가 더 용이하다.
protocol NetworkManaging {
    func fetch()
}

class NetworkManager: NetworkManaging {
    func fetch() { ... }
}

class ViewModel {
    private let networkManager: NetworkManaging

    init(networkManager: NetworkManaging) {
        self.networkManager = networkManager
    }
}
  • 사용하는 쪽
class ViewController: UIViewController {
    private let viewModel: ViewModel // 뷰모델 선언

    init() { // VC가 만들어질 때 받아옴
        let networkManager = NetworkManager()
        self.viewModel = ViewModel(networkManager: networkManager)
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
  • 하지만 feature가 많아질수록 매번 생성자 파라미터로 전달해야하는 번거로움이 생긴다.

📌 swift-dependencies 사용법

  • 전체적으로는 DependencyKeyliveValue를 정의하고, 그걸 DependencyValues에 연결해서 어디서든 꺼내 쓰는 구조!!

🔍 1. 의존성 타입 만들기

  • 기능 단위의 클라이언트 타입을 struct로 감싸서 등록하는 패턴
    • 🤔 클라이언트??
      • 서버나 기능을 요청해서 사용하는 쪽
      • 이 기능을 사용하는 입장(ViewModel 등)에서 보는 API 묶음
  • 어떤 기능이 있는지 정의만 해준다.
  • 실제 구현을 숨기고 기능만 노출하는 인터페이스 역할
  • 프로토콜 기반의 DI에서의 프로토콜의 역할이라고 할 수 있음
import Dependencies

struct NetworkClient {
    var fetch: () -> Void
}

🔍 2. DependencyKey를 채택해서 liveValue 등록하기

  • liveValue?
    - 실제 앱에서 사용하는 진짜 구현
  • DependencyKey를 채택해서 실제 앱에서 쓸 기본값을 등록하는 단계이다.
  • 실제 동작 자체를 구현함!
extension NetworkClient: DependencyKey {
    static let liveValue = NetworkClient(
        fetch: {
            print("real fetch") // 이해를 돕기위한 코드
        }
    )
}
  • 지금 단계는 DependencyKeyliveValue를 정의하는 단계

🔍 3. DependencyValueskey 추가

  • DependencyValues라는 저장소에 내가 만든 의존성을 등록하는 단계

📘 접근 방식 추가

extension DependencyValues {
    var networkClient: NetworkClient {
        get { self[NetworkClient.self] }
        set { self[NetworkClient.self] = newValue }
    }
}
  • 기존에 있는 DependencyValues라는 저장소에 networkClient라는 접근 방법을 추가하는 것
  • DependencyKey,DependencyValues를 보면 알겠지만 내부적으로 딕셔너리와 작동원리는 비슷한 구조
  • 하지만 타입 안정성을 보장하는 것이 핵심 차이

📘 상세 설명

get { self[NetworkClient.self] }
  • 위의 코드는 "NetworkClient타입에 해당하는 값을 꺼내라"라는 의미
  • NetworkClient.self이게 키 역할
set { self[NetworkClient.self] = newValue }
  • “NetworkClient 타입에 값을 저장해라”라는 의미

📘 왜 이렇게 복잡하게 저장하는데?

  • 일반 딕셔너리같은거에 [String: Any]이렇게 저장한다면 타입이 안정해져서 의도하지 않은 결과가 나올 수 있음
  • 하지만 이렇게 하면 NetworkClient.self로 불러와서 NetworkClient 타입만 가능

🔍 4. ViewModel에서 사용

  • 이제 init(networkManager:)과 같은 코드는 사라짐
import Dependencies

final class ViewModel {
    @Dependency(\.networkClient) private var networkClient

    func load() {
        networkClient.fetch()
    }
}
  • private var networkClient는 이제 위에 정의해놓은 networkClient를 통해서 가져오게 됨

🔍 어떤게 바뀐 것일까?

📘 init DI

let networkManager = NetworkManager()
let viewModel = ViewModel(networkManager: networkManager)

📘 swift dependency 적용 후

let viewModel = ViewModel()
viewModel.load()
  • 외부에서 매번 주입하지 않아도 됨
  • 다른 설정을 하지 않는다면 상단의 "2. live 값 등록하기" 에서 정의한 liveValue를 사용하게 됨

📘 알아두면 유용한 팁

  • 필요한 곳에서 @Dependency로 사용 가능
  • 테스트와 같은 특정 흐름에서 override가 쉬움
  • Date, UUID, Clock, queue 같은 값도 제어 가능

🔍 테스트시 이렇게 사용

  • withDependencies로 특정 범위에서 의존성을 override하는 방식
import Dependencies

let viewModel = withDependencies {
    $0.networkClient = NetworkClient(
        fetch: {
            print("mock fetch")
        }
    )
} operation: {
    ViewModel()
}

viewModel.load()
  • 테스트 코드에서 바로 어떤 동작을 하게 만들기
withDependencies {
    $0.networkClient.fetch = {
        print("mock fetch")
    }
} operation: {
    let viewModel = ViewModel()
    viewModel.load()
}

📌 패키지에 DependenciesMacros는 뭐지?

  • Macro는 컴파일 타임에 코드 생성(런타임 비용 없음)
@Dependency(\.networkClient) var network
  • 등록하고 사용할 때 이런식으로 단순 키워드+변수선언 코드를 쓴다.
  • 근데 사실 이 한줄은 아래의 코드를 줄인 것이다. 그리고 이걸 Macro가 해줌
var network: NetworkClient {
    get { DependencyValues.current.networkClient }
}

📘 원래 이렇게 씀

import Dependencies

final class ViewModel {
    @Dependency(\.networkClient) private var networkClient

    func load() {
        networkClient.fetch()
    }
}

📘 macro 없을 때 이렇게 됨

class ViewModel {
    let network: NetworkClient

    init() {
        self.network = DependencyValues.current.networkClient
    }
}

📘 macro 쓰는 이유?

  • 코드 길어짐
  • 매번 직접 꺼내야 함
  • 테스트 바꾸기 번거러움

📌 마지막 정리

🔍 전체 구조 한 줄 정리

  1. Client 정의 (기능 정의)
  2. DependencyKey로 기본값 등록
  3. DependencyValues에 연결
  4. @Dependency로 꺼내 사용

📘 Dependencis 폴더에 NetworkClient.swift 파일 추가

import Dependencies
import Foundation

// 1. 의존성 타입 만들기
struct NetworkClient {
    var load: @Sendable () throws -> Response
}

// 2. `DependencyKey`를 채택해서 `liveValue` 등록하기
extension NetworkClient: DependencyKey {
    static let liveValue = Self(
        load: {
            try DataService().load()
        }
    )
    
    // 테스트 할 때 넣을 코드
    static let testValue = Self(
        loadMenu: unimplemented("\(Self.self).loadMenu")
    )
}

// 3. `DependencyValues`에 `key` 추가
extension DependencyValues {
    var networkClient: NetworkClient {
        get { self[CoffeeMenuClient.self] }
        set { self[CoffeeMenuClient.self] = newValue }
    }
}

📘 실제 사용 시

import Dependencies

final class ViewModel {
    @Dependency(\.networkClient) private var networkClient

    func load() {
        networkClient.fetch()
    }
}
profile
iOS 개발

0개의 댓글