본문 바로가기
스파르타코딩 클럽/본 캠프

12. 스파르타 코딩클럽 [본캠프 - 온보딩 12일차]

by UDDT 2025. 3. 18.

12일차 - 강의 주차

강의 1. Swift 문법 종합반

 

 제네릭(범용타입)

   1. 제네릭 : 함수나 타입을 정의할 때 구체적인 데이터 타입을 명시하지 않고, 나중에 사용할 때 타입을 지정할 수 있게 해주는 기능

          - Swift에서 다양한 타입에 대한 유연하고 재사용 가능한 코드 작성 가능

          - 중복 코드를 줄이고, 명확하고 추상적인 방식으로 코드를 표현할 수 있음

          - 제네릭을 사용하면 하나의 함수, 구조체, 클래스, 열거형 등을 여러 타입에서 작동하도록 만들 수 있음

          - <타입 이름>으로 구현하며, 나중에 사용할 때 구체적인 타입 지정 

// 함수에서 사용하는 제네릭
func swapToValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var a = 10
var b = 20
swapToValues(&a, &b)
print("a: \(a), b: \(b)")
 /*
 a: 20
 b: 10
 */

var x = "Hello"
var y = "World"
swapToValues(&x, &y)
print("x: \(x), y: \(y)")
 /*
  x: World
  y: Hello
  */
// 구조체에서 사용하는 제네릭
struct Stack<Element> {
    var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        return items.popLast()
    }
}

var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
print(intStack.pop())
//Optional(20) : Element가 없을 수도 있어서 Optional Type

var stringStack = Stack<String>()
stringStack.push("Hello")
stringStack.push("World")
print(stringStack.pop())
//Optional("World")

 

 

   2. 제네릭 제약 : 특정 조건을 부여하여 제약을 거는 것

          - 특정 프로토콜을 준수하거나, 특정 클래스의 하위 클래스에서만 허용할 수 있음

// Comparable을 준수하는 타입만 허용
func findMinimumValue2<T>(array: [T]) -> T? where T: Comparable {
    guard !array.isEmpty else { return nil }
    return array.min()
}

// Comparable을 준수하는 타입만 허용(위와 동일한 코드)
func findMinimumValue<T: Comparable>(in array: [T]) -> T? {
    guard !array.isEmpty else { return nil }
    return array.min()
}

let intArray = [3, 1, 4, 1 ,5, 9]
if let minValue = findMinimumValue(in: intArray) {
    print("Minimum value: \(minValue)")
}
// 1

let stringArray = ["Apple", "Banana", "Kiwi"]
if let minValue = findMinimumValue(in: stringArray) {
    print("Minimum value: \(minValue)")
}
// Apple

 네트워크

   1. RESTful API 메서드

       - 서버와 클라이언트 간 데이터를 주고 받기 위한 표준화된 방법을 정의한 것

       - 주로 JSON이나 XML을 통해 데이터를 주고 받으며, 의도에 맞는 HTTP 메서드를 사용

       - 주로 사용되는 HTTP 메서드

          1. GET 

              - 서버에서 데이터를 읽어올 때 사용

              - 쿼리스트링으로 데이터를 전달

                 * URL 뒤에 ?를 붙인 후 key = value 형태로 데이터를 보냄. 여러개의 데이터를 보낼 때는 &으로 구분

          2. POST

              - 새로운 데이터 생성을 요청할 때 사용

              - Body에 데이터를 담아서 요청

          3. PUT

              - 기존 데이터를 수정하거나 업데이트할 때 사용

              - Body에 데이터를 담아서 요청

          4. PATCH

              - 기존 데이터의 일부를 수정할 때 요청

              - Body에 데이터를 담아서 요청

          5. DELETE

              -데이터 삭제를 요청할 때 사용

 

   2. URLSession : 서버와 네트워크 통신을 처리하는 핵심 객체

       - 네트워크 요청을 관리하고, 네트워크 작업을 실행하며 결과를 처리

       - 비동기적으로 네트워크 요청을 보내고, 응답을 받아서 이벤트를 처리함

       - URLSession 주요 구성 요소

func fetchPosts() {
    print("Starting API Call")

    let task = URLSession.shared.dataTask(with: URL(string: "https://uddt.tistory.com")!) { (data, response, error) in
        if let error = error {
            print(error.localizedDescription)
            return
        }

        guard let data = data else {
            print("No data")
            return
        }
        // data를 사용하여 작업 진행
        print("API Success Do Something with Data")
    }

    task.resume() // 작업 시작

    print("API Call Started")
}

fetchPosts()

 

 

       - URLSessionTask

          1. 위 코드에서 URLSession.shard.dataTask(...)의 반환 타입으로 서버에 데이터를 요청하는 비동기 작업을 담당

          2. dataTask(with:) 메서드는 URL을 통해 데이터를 요청하고, 클로저 내부에서 서버의 응답을 처리

          3. task.resume()는 실제로 네트워크 요청을 시작하는 부분

               * URLSessionTask는 기본적으로 멈춘 상태에서 시작되므로, resume()을 호출해줘야 작업이 진행됨

 

       - URLSessionConfiguration

          1. 위 코드에서는 URLSession.shared를 사용하여 기본 설정을 가진 세션을 사용하고 있음

          2. 만약 타임아웃, 캐시 정책 등을 설정하고 싶다면, URLSessionConfiguration을 사용해 별도로 세션을 구성할 수 있음

let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30.0
let session = URLSession(configuration: config)

 

   3. Result Type

       - 비동기 작업이나 에러가 발생할 수 있는 작업의 결과를 명확하게 처리하기 위해 사용되는 열거형(enum) Type

       - 작업이 성공했을 떄와 실패했을 때의 결과를 한번에 처리하도록 설계됨(가독성 ↑, 에러처리 직관적)

@frozen enum Result<Success, Failure> where Failure : Error, Success : ~Copyable {
    case success(Success)

    case failure(Failure)
}
func divide(_ numerator: Int, by denominator: Int) -> Result<Int, Error> {
    guard denominator != 0 else {
        return .failure(NSError(domain: "나누기 오류", code: 1, userInfo: [NSLocalizedDescriptionKey : "0으로 나눌 수 없습니다."]))
    }
    return.success(numerator / denominator)
}

let result = divide(10, by: 2)
// Result is 5

switch result {
case .success(let value):
    print("Result is \(value)")
case .failure(let error):
    print("Failed with error: \(error.localizedDescription)")
}

let errorResult = divide(5, by: 0)
// Failed with error: 0으로 나눌 수 없습니다.

switch errorResult {
case .success(let value):
    print("Result is \(value)")
case .failure(let error):
    print("Failed with error: \(error.localizedDescription)")
}

 

   4. Codable : 네트워크에서 자주 사용되는 protocol

       - 서버에 전송된 문자 형식의 데이터를 Swift 인스턴스로 변환

       - 또는 Swift 인스턴스를 서버에 전송할 형식(JSON, XML) 형식으로 변환을 도와주는 프로토콜

typealias Codable = Decodable & Encodable

 

      - Decodable : JSON, XML의 데이터 형식을 Swift 인스턴스로 디코딩(병렬화)할 수 있게 함

      - Encodable : Swift 인스턴스를 JSON, XML의 형식으로 인코딩(직렬화)할 수 있게 함

// Encodable : 여러 개의 값을 갖고 있는 객체를 한 줄의 JSON 데이터로 변경!
struct Person: Codable {
    let name: String
    let age: Int
}

let person = Person(name: "Uddt", age: 20)
if let jsonData = try? JSONEncoder().encode(person),
   let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}
//{"name":"Uddt","age":20}


// Decodable : 한줄로 길게 저장되어 있는 JSON 데이터를 여러 개의 값을 갖고 있는 객체로 병렬화!
let jsonString = """
    {"name":"Uddt","age":20}
"""

if let jsonData = jsonString.data(using: .utf8),
   let person = try? JSONDecoder().decode(Person.self, from: jsonData) {
    print(person)
}
//Person(name: "Uddt", age: 20)

 

 오류처리(예외처리)

    - 예외처리 : 프로그램이 정상적으로 실행되지 않을 때 발생할 수 있는 에러를 처리하고, 안전하게 대응하는 방법

       1. do, try, catch 구문을 사용하여 오류를 처리하고 관리 가능

         * try, catch : try 키워드를 사용하여 에러가 발생할 수 있는 코드를 실행하고 catch 블럭에서 에러를 처리할 수 있음

           (단, catch 블럭에서는 특정 오류만 처리 가능)

       2.  try?, try!와 같이 에러를 간편하게 처리할 수도 있음

          * 이 때는 do, catch를 사용하지 않아도 됨

       3. throws

           - 함수의 파라미터 뒤에 throws 키워드를 사용하여 함수가 에러를 던질 수 있다는 것을 명시하는 것

           - 함수 내부에서 에러가 발생하면 throws를 사용하여 에러를 던짐

           - 에러는 함수를 호출한 곳에서 처리해야 함

// 에러를 던지는 함수 선언 : 파라미터 괄호 뒤에 throws 입력
func fetchData(url: String) throws -> String {
    guard let url = URL(string: url) else {
        throw NSError(domain: "Invalid URL", code: 4040, userInfo: nil)
    }
    return "Data"
}

 

       4. do, try, catch 구문을 사용하여 에러를 핸들링하는 방법

// do, try, catch를 사용하여 에러를 핸들링하는 코드
enum NetworkError: Error {
    case badURL(String)
    case requestFailed
}

do {
    let data = try fetchData(url: "https://uddt.tistory.com")
    print("Data received: \(data)")
} catch NetworkError.badURL(let stringValue) {
    print("Invalid URL.")
    print(stringValue)
} catch NetworkError.requestFailed {
    print("Request failed")
} catch {
    print("Unknown error: \(error)") // 위의 catch에서 잡히지 않은 나머지 error
}

/*
catch에 특정 에러만 처리할 수 있음
catch의 코드 블럭 안에서 error 변수에 접근하여 작업할 수 있음
하단의 코드처럼 catch에서 error 변수의 이름을 변경할 수 있음
*/

catch(let customError) {
    print("Unknown error: \(customError)")
}

 

       5. try? try!를 사용하는 방법

// do catch를 생략하고 try? try!를 사용하는 방법
let data = try? fetchData(from: "https://invalid.url")
// try?는 에러가 발생하면 nil을 리턴
print(data ?? "No data") 
// No data


let data = try! fetchData(from: "https://valid.url")
// try!는 에러가 발생하면 크래시가 발생
print(data)

/*
try?는 에러가 발생하면 nil을 반환하여 간단하게 처리할 수 있지만 
에러의 구체적인 원인을 파악하기 어려움

try!는 에러가 발생하면 앱이 크래시가 나므로 조심해서 사용
*/

 

 고차함수

    - 고차함수 : 다른 함수를 인자로 받거나, 함수를 반환하는 함수

       * Swift는 일급 객체를 지원하므로, 함수를 인자로 넘기거나 반환값으로 사용 가능

       1. map : 배열의 각 요소를 변환하여 새로운 배열을 생성 

           - 각 요소를 변형하는 작업을 반복하고, 변형된 값을 새로운 배열로 반환

let numbers = [1, 2, 3, 4]
let squaredNumbers = numbers.map { $0 * $0 }
print(squaredNumbers)

// 만약 map이 없으면
let numbers = [1, 2, 3, 4]
var squaredNumbers: [Int]()
for number in numbers {
    squaredNumbers.append(number * number)
}

 

       2. filter : 배열에서 주어진 조건에 맞는 요소들만 걸러내어 새로운 배열로 반환

let numbers = [1, 2, 3, 4]
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers)
// [2, 4]

 

       3. reduce : 배열의 모든 요소를 하나로 합쳐 하나의 값을 반환

           * 초기 값을 설정해줘야 함

let numbers = [1, 2, 3, 4]
let sum = numbers.reduce(0) { $0 + $1 }

print(sum) // 10

/*
reduce(0) { $0 } 은 기존에 저장하고 있는 것
reduce(0) { $1 } 은 배열에서 받은 요소들
처음 실행하면 
$0에 0이 들어가고, $1에는 1이 들어감
{ $0 + $1 } 하면 1이 됨
$0에 1이 들어가면, $1에는 2가 들어가고 
{ $0 + $1 } 하면 3이 됨
... 해서
결국 10이 되는 것
*/

 

       4. compactMap : 옵셔널 값을 언래핑하고 nil을 제거한 배열을 반환

let stringNumbers = ["1", "2", "three", "4", "five"]

// 여기서 "three"와 "five"는 Int Type으로 바뀌지 못해 nil이 됨
let numbers = stringNumbers.compactMap { Int($0) }
print(numbers)
//[1, 2, 4]

 

       5. flatMap : 중첩된 컬렉션을 평평하게 만드는데 사용

           * 2차원 배열을 1차원 배열로 만드는데 사용

let nestedArray = [[1, 2, 3], [4, 5], [6, 7, 8]]
let flatArray = nestedArray.flatMap { $0 }
print(flatArray)
// [1, 2, 3, 4, 5, 6, 7, 8]

 

 RxSwift

    - 반응형 프로그래밍 : 이벤트가 발생할 때 해당 이벤트를 처리하는 프로그래밍 방법

       * 이벤트가 발생하는 곳을 구독하고 있다가 이벤트가 발생하면 그 때 동작을 진행(비동기 작업을 간결하게 처리, 상태 변화에 반응)

 

    1. RxSwift란?

        - 자주 사용하는 서드파티 라이브러리(애플에서 제공하는 기능은 아님)

       * 애플에서 제공하는 Combine과 RxSwift의 Combine의 사용 방법이 비슷함

        - Reactive eXtension(ReactiveX)란 무엇인가?

           마이크로소프트에서 개발한 비동기 이벤트의 핸들링을 도와주는 오픈소스 라이브러리

        - RxSwift는 ReactiveX를 Swift로 이식한 것!

 

    2. RxSwift 이해하기

        - 데이터스트림을 구독(관찰)하고 있다가 이벤트가 방출되면 작업을 진행하는 것 

          (언젠가 이벤트가 발생될 데이터 스크림을 관찰하고 있는 것)

        - Observer, Observable, Operator로 구성되어 있음

 

    3. Observer, Observable, Operator

        3-1. Observable : 데이터 스트림과 이벤트 방출을 하는 역할

                - onNext : 아이템 방출

                - onCompleted : complete 되었을 떄 방출

                - onError : 에러가 발생했을 때 방출

                - onDisposed : 옵저버가 처분되었을 떄 방출

 

        3-2. Observer : 구독자(관찰자)

               - 데이터 스트림을 관찰하고 있다가 이벤트가 방출되면 처리하는 역할

               - subscribe(onNext, onCompleted, onError, onDisposed)


        3-3. Operator : 연산자

               - RxSwift의 사용을 도와줌

               - Observable 상에서 동작하고, Observable을 리턴

               * Operator가 엄청 많기 때문에, 모든 Operator를 외울 필요가 없음(사이트를 보고 찾아보고 검색)

               - Operator 중에는 map, filter도 있음

               - 알아두면 좋은 Operator : Debounce, Throttle, CombineLatest, Zip, merge

               

               - map : 방출된 이벤트를 각각 변환해주는 역할

                  [map 사진]

Observable.range(start: 1, count: 5)
    .map{ $0 * 2 }
    .subscribe{ print($0) }

 

               - filter: 방출되는 이벤트를 필터하는 역할

                  [filter 사진]

Observable.range(start: 1, count: 5)
    .filter { $0 % 2 == 0 }
    .subscribe{ print($0) }

 

        4. ReactiveX 마블다이어그램 보는 방법

            https://reactivex.io/documentation/observable.html

 

ReactiveX - Observable

Observable In ReactiveX an observer subscribes to an Observable. Then that observer reacts to whatever item or sequence of items the Observable emits. This pattern facilitates concurrent operations because it does not need to block while waiting for the Ob

reactivex.io

 

 

    5. Hot Observable vs Cold Observable

        1. Hot Observable : 옵저버가 옵저버블을 구독한 순간부터 default 이벤트를 방출

            - 예시 : 슬랙 채널에 초대받아서 들어오면, 앞에서 사람들이 작성한 채팅을 볼 수 있음

                        (처음에 입장(구독)할 때 값을 받을 수 있는 것)

       2. Cold Observable : 구독한 순간 default 이벤트가 없고 구독하고 나서 이벤트가 방출하면 이벤트를 받을 수 있음

            - 예시 : 오픈 카톡방에 들어가면, 앞에서 사람들이 작성한 채팅을 볼 수 없음

                        (처음에 입장(구독)한 후에 방출된 이벤트만 처리하는 것)

 

    6. Subject(Observable + Observer) : 옵저버이면서 옵저버블 역할을 할 수 있음

        1. Behavior Subject

            - 구독을 시작하면 값을 받을 수 있음

            - 처음 생성할 때 default 값을 제공해야 함

       2. Publish Subject

            - 구독을 시작하면 값을 제공하지 않고, 이벤트가 방출될 때까지 기다림

            - default 값을 제공하지 않아도 됨

        그외 AsyncSubject, Replay Subject도 있음 

       3. 구독 취소 방법

Observable<Int>
    .interval(.seconds(1), scheduler: MainScheduler.asyncInstance)
    .subscribe { value in
        print("onNext: \(value)")
    } onError: { error in
        print(error)
    } onCompleted: {
        print("Completed")
    } onDisposed: {
        print("Disposed")
    }
    
/*
onNext: 0
onNext: 1
onNext: 2
onNext: 3
onNext: 4
onNext: 5
onNext: 6

구독을 취소하지 않으면 영원히 1초에 한번씩 이벤트를 방출함
*/
public func subscribe(
    onNext: ((Elemnet) -> Void)? = nil,
    onError: ((Swift.Error) -> Void)? = nil,
    onCompleted: (() -> Void)? = nil,
    onDisposed: (() -> Void)? = nil
) -> Disposable {

}

 

 

       * 하나씩 구독을 취소 방법

let dispose = Observable<Int>
    .interval(.seconds(1), scheduler: MainScheduler.asyncInstance)
    .subscribe { value in
        print("onNext: \(value)")
    } onError: { error in
        print(error)
    } onCompleted: {
        print("Completed")
    } onDisposed: {
        print("Disposed")
    }

// 10초 뒤에 옵저버 사용을 종료
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
    dispose.dispose()
}

 

       * 한번에 여러개의 구독을 취소하는 법 : RxSwift의 DisposeBag 사용

var disposeBag = DisposeBag()
Observable<Int>
    .interval(.seconds(1), scheduler: MainScheduler.asyncInstance)
    .subscribe { value in
        print("onNext: \(value)")
    } onError: { error in
        print(error)
    } onCompleted: {
        print("Completed")
    } onDisposed: {
        print("Disposed")
    } .disposed(by: disposeBag)

DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
    disposeBag = DisposeBag()
}

 

강의 2. iOS 앱 개발 입문 

 카운터 앱 개발 개요

    - 앱 요구 사항

예시 이미지

카운터 앱 :
 1. 숫자를 띄울 라벨
     - Int형, 0부터 시작
     - textColor : White
     - font : boldSystem 폰트, size = 45
     - textAlignment : Center
     - width : 80
     - constraint : superView의 center와 같게 설정
2. 감소, 증가 버튼
     - backgroundColor : 감소 버튼은 red, 증가 버튼은 blue
     - textColor : white
     - width : 80
     - height : 30
     - cornerRadius : 8
     - constraint : centerY는 숫자 라벨과 같게 설정
                            감소버튼은 라벨로부터 왼쪽으로 32 떨어지게 설정
                            증가버튼은 라벨로부터 오른쪽으로 32 떨어지게 설정

 

 스토리보드 UI로 개발

    1. main StoryBoard의 Background 컬러 변경

 

    2. Label 추가 후 설정 변경

 

    3. 넓이 제약 추가

 

    4. 위치 제약 추가

 

    5. 버튼 2개 추가

 

    6. 버튼 속성 변경

   * 여기서 Button Type을 Custom으로 바꿔주지 않으면 Build 후에 검정색 Text로 바뀜

 

    7. 버튼 제약 추가

 

    8. 버튼에 cornerRadius 적용

 

    9. 나머지 버튼에도 제약 추가, cornerRadius 적용

 

    10. Label Outlet으로 연결

 

    11. Button도 Outlet으로 연결

 

    12. Button을 Action으로 연결

 

    13. 코드 추가

 

    14. 테스트

 

코드베이스 기본 세팅

    1. main StoryBoard 삭제

 

    2. info > storyboard Name 삭제

 

    3. project > Info.plist Values > UIKit Main Storyboard File Base Name "Main" text 삭제

 

    4. SceneDelegate에서 rootView 설정

 

 코드베이스로 구현하기(with SnapKit)

    1. SnapKit 세팅

    2. UI 만드는 코드 추가

class ViewController: UIViewController {

    private var number: Int = 0

    let label = UILabel()
    let plusButton = UIButton()
    let minusButton = UIButton()


    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()

    }

    private func configureUI() {
        view.backgroundColor = .black

        label.text = "\(number)"
        label.textColor = .white
        label.font = .boldSystemFont(ofSize: 45)
        label.textAlignment = .center

        plusButton.setTitle("증가", for: .normal)
        plusButton.setTitleColor(.white, for: .normal)
        plusButton.backgroundColor = .blue
        plusButton.layer.cornerRadius = 8

        minusButton.setTitle("감소", for: .normal)
        minusButton.setTitleColor(.white, for: .normal)
        minusButton.backgroundColor = .red
        minusButton.layer.cornerRadius = 8

        [label, minusButton, plusButton].forEach { view.addSubview($0) }

        label.snp.makeConstraints {
            $0.width.equalTo(80)
            $0.center.equalToSuperview()
        }

        plusButton.snp.makeConstraints {
            $0.width.equalTo(80)
            $0.height.equalTo(30)
            $0.centerY.equalToSuperview()
            $0.leading.equalTo(label.snp.trailing).offset(32)
        }

        minusButton.snp.makeConstraints {
            $0.width.equalTo(80)
            $0.height.equalTo(30)
            $0.centerY.equalToSuperview()
            $0.trailing.equalTo(label.snp.leading).offset(-32)
        }
    }
}

 

    3. Action하도록 동작하는 코드 추가

// configureUI() 함수 안에 추가
plusButton.addTarget(self, action: #selector(plusButtonTapped), for: .touchDown)
minusButton.addTarget(self, action: #selector(minusButtonTapped), for: .touchDown)

// @objc func 추가
    @objc
    private func minusButtonTapped() {
        self.number -= 1
        label.text = "\(number)"
    }

    @objc
    private func plusButtonTapped() {
        self.number += 1
        label.text = "\(number)"
    }

 

    4. 테스트

 

 초기화 버튼 추가하기

    1. 코드 추가

// ViewController 클래스에 버튼 인스턴스 1개 더 생성
    let initButton = UIButton()

// configureUI() 함수에 코드 추가
        initButton.setTitle("초기화", for: .normal)
        initButton.setTitleColor(.white, for: .normal)
        initButton.backgroundColor = .gray
        initButton.layer.cornerRadius = 8
        initButton.addTarget(self, action: #selector(initButtonTapped), for: .touchDown)
        
// subView 리스트에 initButton 추가
        [label, minusButton, plusButton, initButton].forEach { view.addSubview($0) }
        
        
        
// 제약 추가
        initButton.snp.makeConstraints {
            $0.width.equalTo(90)
            $0.height.equalTo(30)
            $0.centerX.equalToSuperview()
            $0.top.equalTo(label.snp.bottom).offset(40)
        }
        
// Action을 위한 함수 추가
    @objc
    private func initButtonTapped() {
        self.number = 0
        label.text = "\(number)"
    }

 

    2. 테스트

최근댓글

최근글

skin by © 2024 ttuttak