Swift Property / Instance Method

swift 에서의 struct 또는 class 에서는 member variable 을 property 라고 한다. 이 Property 들은 상태를 체크 할수 있는 기능을 가지고 있다. 천천히 알아보자.

  • Store Property: member variable 결국, 상수 또는 변수를 저장한다고 보면된다. 이부분은 init() 에 instantiate 할때 설정을 해줘야한다.
  • Type Property: static variable 이다. 객채가 가지고 있는 변수라고 생각하면 된다. 여러가지의 Instantiate 을 해도 공유 되는 값이다.
  • Compute Property: 동적으로 계산하기 때문에, var 만 가능하며, getter / setter 를 만들어줄수있다. getter 는 필수 이며, setter 는 구현 필요없다. (즉 setter 가 없다면, 굳이 getter 를 사용할 필요 없다.)
  • Property Observer: 이건 Property 들의 상태들을 체크를 할 수 있다. 상속받은 저장/연산 Proprty 체크가 가능하며, willSetdidSet 으로 이루어져있다. willSet 같은 경우, 값이 변경되기 전에 호출이되고, didSet 은 값이 변경 이후에 호출한다. 접근은 newValue 와 oldValue 로 체크할수 있다.
  • Lazy Stored Property: 이 부분은 lazy 라는 Keyword 로 작성이되며, 값이 사용된 이후에 저장이 되므로, 어느정도의 메모리 효율을 높일수 있다.
import Foundation

struct AppleDevice {
    var modelName: String
    let releaseYear: Int
    lazy var care: String = "AppleCare+"
    
    /// Property Observer
    var owner: String {
        willSet {
            print("New Owner will be changed to \(newValue)")
        }
        
        didSet {
            print("Changed to \(oldValue) -> \(owner)")
        }
    }
    
    /// Type Property
    static let companyName = "Apple"
    
    /// Compute Property
    var isNew: Bool {
        releaseYear >= 2020 ? true : false
    }
}


var appDevice = AppleDevice(modelName: "AppleDevice", releaseYear: 2019, owner: "John")
print(appDevice.care)
appDevice.owner = "Park"

Instance Method 도 마찬가지이다. 위의 코드에 method 를 넣어보자. struct 일 경우에는 저장 property 를 method 에서 변경하려면, mutating keyword 가 필요하다. 그리고 다른건 static 함수이다. 이 부분에 대해서는 따로 설명하지 않겠다.

import Foundation

struct AppleDevice {
    var modelName: String
    let releaseYear: Int
    lazy var care: String = "AppleCare+"
    var price: Int
    
    /// Property Observer
    var owner: String {
        willSet {
            print("New Owner will be changed to \(newValue)")
        }
        
        didSet {
            print("Changed to \(oldValue) -> \(owner)")
        }
    }
    
    /// Type Property
    static let companyName = "Apple"
    
    /// Compute Property
    var isNew: Bool {
        releaseYear >= 2020 ? true : false
    }
    
    mutating func sellDevice(_ newOwner: String, _ price: Int) -> Void {
        self.owner = newOwner
        self.price = price
    }
    
    static func printCompanyName() {
        print(companyName)
    }
}


var appDevice = AppleDevice(modelName: "AppleDevice", releaseYear: 2019, price: 500, owner: "John")
print(appDevice.care)
appDevice.owner = "Park"
AppleDevice.printCompanyName()

ObservableObject, StateObject, EnvironmentObject

Before we start

Let’s review the @State keyword. In order for View to notice, that the value of @State change, the View is re-rendered & update the view. This is the reason why we can see the change of the value in the View.

StateObject & ObservableObject

Now, let’s talk about StateObject & ObservableObject. If we have a ViewModel, called FruitViewModel, as below. Let’s review the code. FruitViewModel is a class that conforms to ObservableObject protocol. It has two @Published properties: fruitArray & isLoading. This viewmodel will be instantiated in the ViewModel struct. This FruitViewModel also controls the data flow between the View and the ViewModel. Then we have navigation link to the SecondScreen struct. Then, we pass the FruitViewModel to the SecondScreen struct. In the SecondScreen struct, we have a button to go back to the ViewModel struct. In the SecondScreen, this can access the FruitViewModel’s properties (which in this case, fruitArray mainly).

There are two ways to instantiate the FruitViewModel. One is using @StateObject and the other is using @ObservedObject. For @StateObject, it’s used for the object that is created by the View. For @ObservedObject, it’s used for the object that is shared across the app. This means you can still use @ObservedObject for the object that is created by the View, but if it’s observableobject, it’s not going to be persisted. meaning the data will be changed when the view is changed. So, it will change everytime the view is changed where this wouldn’t be our case. So, that’s why we use @StateObject to keep the data persistence.

class FruitViewModel : ObservableObject {
    @Published var fruitArray: [FruitModel] = [] // state in class (alert to ViewModel)
    @Published var isLoading: Bool = false
    
    init() {
        getFruits()
    }
    
    func getFruits() {
        let fruit1 = FruitModel(name: "Banana", count: 2)
        let fruit2 = FruitModel(name: "Watermelon", count: 9)
        
        isLoading = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0){
            self.fruitArray.append(fruit1)
            self.fruitArray.append(fruit2)
            self.isLoading = false
        }
    }
    
    func deleteFruit(index: IndexSet) {
        fruitArray.remove(atOffsets: index)
    }


struct ViewModel: View {
    @StateObject var fruitViewModel: FruitViewModel = FruitViewModel()
    
    var body: some View {
        NavigationView {
            List {
                if fruitViewModel.isLoading {
                    ProgressView()
                    
                } else {
                    ForEach(fruitViewModel.fruitArray) { fruit in
                        HStack {
                            Text("\(fruit.count)")
                                .foregroundColor(.red)
                            Text(fruit.name)
                                .font(.headline)
                                .bold()
                        }
                    }
                    .onDelete(perform: fruitViewModel.deleteFruit)
                }
            }
            .listStyle(.grouped)
            .navigationTitle("Fruit List")
            .navigationBarItems(
                trailing: NavigationLink(destination: SecondScreen(fruitViewModel: fruitViewModel), label: { Image(systemName: "arrow.right")
                    .font(.title)})
            )
        }
    }
}
}

struct SecondScreen : View {
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var fruitViewModel: FruitViewModel
    var body: some View {
        ZStack {
            Color.green.ignoresSafeArea()
            VStack {
                Button(action: {
                    presentationMode.wrappedValue.dismiss()
                }, label: {
                    Text("Go Back")
                        .foregroundColor(.white)
                        .font(.largeTitle)
                        .fontWeight(.semibold)
                })
                
                VStack {
                    ForEach(fruitViewModel.fruitArray) { fruit in
                        Text(fruit.name)
                            .foregroundColor(.white)
                            .font(.headline)
                    }
                }
            }
        }
    }
}

EnvironmentObject

EnvironmentObject is a bit same as @ObservedObject. The difference is that it’s used for the object that is shared across the app. This means you can still use @ObservedObject for the object that is created by the View, but if it’s observableobject, only the subview can access the data. But if you use EnvironmentObject, the data will be shared across the app. Obviously there is downside to this, which means it’s slower than @ObservedObject. So if we have a hierchical structure, we can use EnvironmentObject to share the data across the app. (if only needed). So that the child view can access the data from the parent view. Otherwise, you can easily use @ObservedObject and pass this to child view.

The example code is as below

//
//  EnvironmentObject.swift
//  SwiftfulThinking
//
//  Created by Seungho Jang on 2/25/25.
//

import SwiftUI

// What if all child view want to access the Parent  View Model.
// Then use EnvironmentObject.
// You can certainly do pass StateObject / ObservedObject, but what
// if you have a hierchy views want to access the parent views.
// but might be slow
class EnvironmentViewModel: ObservableObject {
    @Published var dataArray: [String] = []
    
    init() {
        getData()
    }
    
    func getData() {
        self.dataArray.append(contentsOf: ["iPhone", "AppleWatch", "iMAC", "iPad"])
    }
}

struct EnvironmentBootCampObject: View {
    @StateObject var viewModel: EnvironmentViewModel = EnvironmentViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.dataArray, id: \.self) { item in
                    NavigationLink(
                        destination: DetailView(selectedItem: item),
                        label: {
                            Text(item)
                        })
                }
            }
            .navigationTitle("iOS Devices")
        }
        .environmentObject(viewModel)
    }
}

struct DetailView : View {
    let selectedItem: String
    var body: some View {
        ZStack {
            Color.orange.ignoresSafeArea()
            
            NavigationLink(
                destination: FinalView(),
                label: {
                    Text(selectedItem)
                        .font(.headline)
                        .foregroundColor(.orange)
                        .padding()
                        .padding(.horizontal)
                        .background(Color.white)
                        .cornerRadius(30)
                })
        }
    }
}

struct FinalView: View {
    @EnvironmentObject var viewModel: EnvironmentViewModel
    var body: some View {
        ZStack {
            LinearGradient(gradient: Gradient(colors: [.blue, .red]),
                           startPoint: .topLeading,
                           endPoint: .bottomTrailing)
            .ignoresSafeArea()
            
            ScrollView {
                VStack(spacing: 20) {
                    ForEach(viewModel.dataArray, id: \.self) { item in
                        Text(item)
                    }
                }
            }
            .foregroundColor(.white)
            .font(.largeTitle)
        }
    }
}

At the end…

Why do we use StateObject & EnvironmentObject? It’s matter of the lifecycle of the object as well as the MVVM Architecture. The MVVM Architecture is a design pattern that separates the UI, the data, and the logic. The StateObject is used for the object that is created by the View. The EnvironmentObject is used for the object that is shared across the app.

Resource

CUDA Architecture and Memory Handling

바로 아래의 Diagram 을 살펴보자.

Architecture

위의 그림을 보자면, Source Code 에서 nvcc (nvidia) CUDA Compiler 가 CUDA 관련된 코드만 쏙 빼가서, 그부분만 컴파일을 하게 된다. Compile 을 한 이후에, executable code 만 GPU 에게 넘겨준다. 즉 전에 Post 에서 사용했던 __global__ 코드만 nvcc 가 가로채서 GPU 에서 실행을 했다고 생각을 하면된다. 그리고 남은거는, MSVC 또는 GNU 가 pure C++ Code 만 가져가서, CPU 에 실행한다고 볼수 있다.

여기에서 용어를 한번 정리를 한다면 …

  • CUDA Kernel: GPU 가 실행하는 작은(병렬) 프로그램
  • VRAM: CUDA 가 사용하는 메모리

직접적인 I/O 는 오로지 South PCI Slot 이므로 North PCI 에서는 안됨, 그래서 간접적으로 해야한다. 즉 이 말은 I/O 에서 받아오는것들을 Main Memory 로 들고 온이후에, CUDA Memory (VRAM) 으로 Copy 를 해주면 된다. 그래서 이것저것 GPU 에서 한 이후에, Main Memory 로 다시 넘겨주면 되는 형식이다. 즉 다시 정리를 하자면

  1. 외부 데이터로부터 메인메모리, 메인메모리부터 비디오 메모리 (Host CPU)
  2. CUDA Kernel 실행, 비디오 메모리 데이터 사용, GPU 로 병렬처리, 처리 결과는 비디오 메모리 (Device=Kernel Program)
  3. 비디오 메모리 -> 메인메모리, 외부로 보내거나, I/O 출력 (Host CPU)

이런식으로 3 단계로 일반적인 Step 이라고 볼수 있다.

Memory Handling

CPU 와 GPU 메모리는 공간이 분리되어있다는 걸 염두할 필요가 있다. 그리고 CPU 와 GPU 에서의 Memory 할당을 보자

메인메모리 할당/복사 C++ 함수 사용

void* malloc(size_t nBytes);
void free(void* ptr);
void* memset(void*ptr, int value, size_t count);
void* memcpy(void* dst, const void*src, size_t num);

Example:

int nbytes = 1024 * sizeof(int);
int *ptr = nullptr;
ptr = malloc(nbytes);
memset(ptr, 0, nbytes);
free(ptr);

비디오 메모리 할당/복사: 별도의 CUDA 함수 이용

cudaError_t cudaMalloc(void** dev_ptr, size_t nbytes);
cudaError_t cudaMemset(void* dev_ptr, int value, size_t count);
cudaError_t cudaFree(void* dev_ptr);
cudaError_t cudaMemcpy(void* dst, void* src, size_t nbytes, enum cudaMemcpyKind direction);

Example:

int nbytes = 1024 * sizeof(int);
int* dev_ptr = nullptr;
cudaMalloc((void**)&dev_ptr, nbytes);
cudaMemset(dev_ptr, 0, nbytes);
cudaFree(dev_ptr);

여기에서 cudaMemcpy 를 한번보자.

  • 이전 CUDA 함수들이 모두 종료되어야 복사가 시작된다.
  • copy 중에는 CPU Thread Pause, 작업이 완료되어야 리던한다.
  • host = CPU, main memory, RAM
  • device = CUDA, video memory, vram
  • enum cudaMemcpyKind
    • cudaMemcpyHostToDevice
    • cudaMemcpyDeviceToHost
    • cudaMemcpyDeviceToDevice
    • cudaMemcpyHostToHost

특별 이슈라고 말을 할수 있는건 아래와 같다.

  • Memory address 문제
  • 어느쪽 주소인지 ㅣ프로그래머가 구별
  • 반대쪽 Address 를 넣으면 System Crash 발생가능
  • 해결책: device 에서는 dev_ 사용

예제를 한번 보자. 자세하게 보면, 메모리를 할당할때, 간접적으로, dev_a 와 dev_b 를 받아주는걸 볼수 있다. 그리고, Host 에서 GPU 로 a 라는 걸 SIZE * sizeof(float) 만큼 할당해서, device 에 있는 dev_a 를 가르키게끔 되어있다. 그다음 dev_b 에서 dev_a 를 copy 한 이후에, dev_b 에 있는걸 b 로 Copy 하는 걸 볼 수 있다.

#include <stdio.h>
#include <cuda.h>
#include <cuda_runtime_api.h>
#include <cuda_runtime.h>

int main()
{
    const int SIZE = 8;
    const float a[SIZE] = { 1., 2., 3., 4., 5., 6., 7., 8. }; //src
    float b[SIZE] = { 0., 0., 0., 0., 0., 0., 0., 0. }; //dst

    printf("a = {%f,%f,%f,%f,%f,%f,%f,%f}\n", a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]);
    fflush(stdout);

    float* dev_a = nullptr;
    float* dev_b = nullptr;
    cudaMalloc((void**)&dev_a, SIZE * sizeof(float));
    cudaMalloc((void**)&dev_b, SIZE * sizeof(float));

    cudaMemcpy(dev_a, a, SIZE * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(dev_b, dev_a, SIZE * sizeof(float), cudaMemcpyDeviceToDevice);
    cudaMemcpy(b, dev_b, SIZE * sizeof(float), cudaMemcpyDeviceToHost);

    cudaFree(dev_a);
    cudaFree(dev_b);

    printf("b = {%f,%f,%f,%f,%f,%f,%f,%f}\n", b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]);
    fflush(stdout);
    return 0;
}

그렇다면, 코드 생성은 컴파일러 입장에서는, 어떤 코드는 CPU 로 가고, 어떤 코드는 GPU 로 가는지를 한 소스코드에서 판단을 해야한다. 즉 어디까지는 끊어서 이거는 내가 어디를 끊어야될지를 구분을 지어야한다. 방법으로틑 파일이 있다. 즉 어떤 파일은 CUDA 로 Compile 하게 끔, 다른 어떤 파일은 MSVC 로 Compile 하게끔 한다. 또 한줄씩 컴파일로 할때도 가능이 가능하다. 하지만 둘다 Bottleneck 이 존재한다. 파일로 할때는, 관리를 해줘야하며, 코드 라인으로 할때는 너무 하기에는 양이 너무 많다.

그래서 그 중간이 Function 이다 (어떠한 Cuda programming model 이라고 보면 좋을것 같다.) 즉 compilation unit 은 function 단위로 하게끔 되고, 각각의 function 들은 GPU 로 할지 CPU 로 할지가 결정된다! 어떻게 이걸 결정을 하느냐? 바로 PREFIX 이다. 즉 아래와 같이 어떤 컴파일러가 이 Function 을 가져갈지를 정한다.

Prefix 의 종류는 아래와같다.

  • __host__ : can be called by CPU (default, can be omitted) (called by host, excuted on host)
  • __device__: called from other GPU Functions, cannot be called by the CPU (called by device, executed on device)
  • __global__: launched by CPU, cannot be called from GPU, must return void (called by host, executed on device)
  • __host__ and __device__ qualifiers can be combined.

결국에 정리를 하자면, *__global__ defines kernel function

  • each “__” consists of two underscore character
  • A kernel function must return void

  • __device__ and __host__ can be used together, which means compiled twice(!), both cannot have their address taken!!

그리고 Restriction 이 존재한다. CUDA Language = C/C++ language with some restriction: (즉 병렬처리를 위해서 Bottleneck 을 만든 현상)

  • Can only access GPU Memory (CUDA memory, video memory)
    • in new versions, can access host memory directly, with performance drawback
    • No static Variables (No static variable declarations inside the function)
    • No recursion (it is possible in newer version)
    • No dynamic polymorphism

이렇게 해서, 일단 한단락을 마무리 지으려고 한다!

Resource

Courses

Prerequiste for CUDA

  • CUDA 를 설치하기 위해서 해야하는것을 간단히 소개 하겠다. CUDA 를, 즉 개발 환경을 설정하려면 아래의 목록 대로 설치 할 필요가 있다.
    • Visual Studio 2019/2022
    • Nvida Graphic App
    • CUDA Toolkit (***)
    • Nsight Visual Studio Editon Extension in Visual Studio
    • Nsight System
    • Nsight Compute
    • vcpkg (C++ Library, like pip)
  • vcpkg 에 필요한 라이브러리는 설명하지는 않겠다. 단 몇가지를 설치할 필요가 있다.
./vcpkg install vulkan:x64-windows, stb:x64-windows, glfw3:x64-windows, glm:x64-windows
./vcpkg install vulkan:x64-windows stb:x64-windows glfw3:x64-windows glm:x64-windows
./vcpkg install vulkan:x64-windows
./vcpkg install stb:x64-windows
./vcpkg install glfw3:x64-windows
./vcpkg install glm:x64-windows

CUDA: Hello World

위의 내용을 설치하지 않아도, cuda tool kit 이 설치가 완료 되었다고 한다고 하면, 굳이 할 필요 없다. Visual Studio 만으로도 충분히 사용할 수 있다. 일단 C 에 Program Files 안에 CUDA Toolkit 안에 있는 예제 .exe 파일을 돌려보거나, 설치가 되어있다고 하면, Project 를 생성할때 아래와 같이 사용할수 있다.

Profiling

그리고, 코드를 보면 cu 라는 확장자를 가지고 있다. 또 아래의 코드처럼 생성 이후에, 실행을 시켜보면. hello, CUDA 가 출력이 된다. 자 여기서, 분명 __global__ void hello(void) 쪽이 바로 CUDA 에서 실행되는 부분이다. 그리고 __global__ 이라는 것은 이 함수가 GPU 에서 실행될 것이라는 것을 의미한다. 그리고 이 함수는 모든 GPU 에서 실행될 것이다. 즉 하나의 설정자이다. CUDA 라는게 C++ 위에 올라가는거기때문에, editor 에서 에러 처럼 보일수 있다.. 이건 c/c++ 이 CUDA Kernel 을 포함시킨다를 의미한다.

그리고 <<>> 이 부분이 1 x 1 즉 1 개 Core 만 사용한다는 뜻이다. (Liunux 에서는 안들어갈수 있다.) 저걸 만약에 «1 , 8» 이라고 하면, 1 x 8 개의 Core 를 동시에 사용한다는 의미이다. 그리고 만약 «8, 2» 라고 한다면, 16 개의 Core 를 동시에 사용한다는 의미이다. 그리고 8 개의 세트를 두번씩 돌린다는 말이다.

#include <cstdio>

__global__ void hello(void)
{
    printf("hello, CUDA\n");
}

#include <vector>
int main()
{
    hello << <1, 1 >> > (); // parallel execution (call cuda) 
    return 0;
}

OS 에 상관 없이 돌려 보아야하기 때문에, Linux 에서 사용을 해보도록 하자. Linux 에서 사용하려면, cudaDeviceSynchronize() 를 사용해야한다. 이 함수는 모든 thread 가 끝날때까지 기다리는 함수이다. 그래서 이 함수를 사용하면, 모든 thread 가 끝날때까지 기다리기 때문에, 모든 thread 가 끝나고 나서야 다음 코드를 실행할수 있다.

#include <cstdio>

__global__ void hello(void)
{
    printf("hello, CUDA %d\n", threadIdx.x);
}

#include <vector>
int main()
{
    hello << <1, 8>> > ();
    #if defined(__linux__)
        cudaDeviceSynchronize();
    #endif
    fflush(stdout);
    return 0;
}

Resource

Courses

Hyukppenheim’s Easy Deep Learning

Book Cover

Motivation

대학원때 나는 Deep Learning 에 대해서 배우고, Protein Folding 관련되서도 연구를 해보았었다. 그때 참 나는 Deep Learning 이라는게 딱히 편하지 않았다. Data 뽑는것 부터해서, 교수님한테 Data 더 필요해야될것 같아요! 이러면서 굽신 굽신까지는 하지는 않았지만, 뭔가 마음도 편하지 않았고, 너무 오래걸린다는 의미에서는 참 굉장히 거리감이 느꼈었다. 그래서 개발로 변경하고 이렇게 왔었는데, 워낙 요즘 기술의 발전이 빠르다 보니, 나도 모르면 안되겠다라는 생각이 든다. 특히나 Parallel Processing 쪽으로 GPU 연산 쪽으로 더 가고 싶기 때문에, 기본적인 기본을 다시 Review 한답시고 요청을 드렸었다. 너무 친절하게도, 책 전체를 Review 하는게 아닌 챕터 별로 Review 를 하기 떄문에, 부담감을 덜었었다. 내가 Review 할 부분은 “Attenion 과 Self-Attention” 이 부분이다. (나는 Attention is all you need 의 읽기를 사실 포기 했었다… ㅎㅎ)

Comments & General Review

일단 경험상으로, Deep Learning 수업시간에 이야기 했던 부분이 뭐였냐면, 너가 어떤 Project 를 할건지에 따라서, Architecture, Loss function, Softmax, etc 등이 구분이될거다. 예를 들어서 one-to-many 를 하려면, softmax 를 사용해야한다 등.. 이 activation function (tanh) 이 (relu) 보다 좋은 이유는… 이러면서 이야기를 했었는데, 이런 Category 를 주어지면, 우리가 직접 찾지 않아도, 일단 이렇게 만들어보자 라는게 먼저 나온는데, 이 책은 그걸 확실하게 정리해준다. (확실하게 정리 해준 부분은 이전 챕터를 이해하다보면 정리가 된다.) 그리고 간결하다. RNN 을 거슬러 올라가면 LSTM 하고 GRU 가 있는데 이것부터 알아야하는것도 중요하다. 물론 어떤 역활을 하는건지는 중요하지만, 이 부분을 잠깐 알아두기 라는 방식으로 참고해서, 책을 읽었을때 집중력을 흐리지 않게 하는 부분은 참 좋았다.

다른 한점은 용어적인 부분을 Bold 식 또는 색깔을 다르게 해서, 이 부분이 핵심이라는걸 짚어주는데, 확실히 요즘 ChatGPT, DeepSeek 에 비롯된 AI 들이 OpenSource 로 많이 나와있고, 인터넷에 많은 Source 들이 돌아다니는데, 항상 용어의 문제가 있다. 어떤게 정확하게 맞는지 이해를 피해가는 용어들이 생각보다 많이 돌아다닌다. 예를 들어서, Tokenizing 과 Token, Tokenizer 에 대해서, 용어의 의미가 정확한 예제가 없으면, 잘 이해하기가 힘들다. (물론 Compiler 쪽을 공부했다면, 이해가 가능하지만, 입문서에서 다른 Resource 를 보고 공부하다보면 뜻이 흐려진다. 마치 RNN 의 한계점 처럼…)

  • Tokenizing: 데이터를 적절한 단위로 나누는 과정
  • Token: 나눠진 각 단위
  • Tokenizer: 데이터를 어떤 단위로 나누는 객체
  • Next Token Prediction: 그다음 Token 을 예측 (예제: 자동완성)

그리고 이에 덧붙여서, 이책에서는 Architecutre 를 정확하게 예시를 들어서 말해준다. RNN 같은 경우 각각의 h1, h2 표시, Input/output 문자등을 쉽게 설명을 해줘서 이부분에 있어서는 굉장히 잘표현했다.

RNN 부분에 있어서, 왜 한계점이 있는지를 두가지로 잘 표현했는데 Loss 부분의 편미분을 했을때, Gradient 가 각 입력 시점에 대해 불균형적 가중합으로 구해지는 문제 Activation Function: tanh 의 문제점 - Data 의 정보가 점점 뭉개지는 현상 이 부분을 수학적으로 잘표현되었다는게, 독자들에게 훨씬 쉽게 다가서려고 많이 노력했구나 라고 말을 해준다.

Test 환경과 Train 했을때의 문제점을 Case 별로 잘 설명해준다. 이건 사실 경험이 없으면 요약하기 정말 어려운 부분이다. 근데 장황하게 안쓰셨지만, 확실히 이건 전략적으로 작성한게 보인다. (너무 train 된것만 쓰다 보면, 말에 신뢰가 떨어질수 있으니)

Alt text

제일 좋았던건 transformer 에 대한 설명을 천천히 한계로부터 발전되어왔고, 그것에 대한 원리를 잘 설명 해주었던것 같다. 예를 들어서, context vector 와 Encoding 에서의 embeding vector h_n 들 Decoding 에서의 s_n 들을 그림으로 잘설명을 해주셔서 확실히 이해했던 부분이 있었지만, 갑작스런 C4 는 조금 집중을 흐리게했다. (c1, c2, c3 는 뭔데? 라는 질문을 할수 있는데) 이 부분은 확실히 attention 부분에 있어서, 간소화하기 힘든 부분들을 어쩔수 없이 하나의 예제로 가져간걸로 보인다. 하지만 아래의 그림을 보면, 이 부분을 자세하게 어떤 부분이 Query 고 이부분이 Key, 고 이부분이 Value 인지를 통해서 확실히 이해했다고 볼수 있다.

Alt text

전반적으로 문제, 그리고 Resercher 들의 해결 방법, 하지만 또 한계점에 대해서는 확실히 표현하고 있어서, 이부분을 딥러닝을 입문하는 사람이 보았을때 이해가 안될수도 있지만 흐름을 이해할수 있고, Detail 한 부분은 독자들에게 맡기는 부분도 있을것 같다. 이렇게 사실 좋은 입문서가 있었더라면, 장황한 동영상 하나보다는 훨씬 나은 점이 있다! 예제 Figure 그리고 피할수 없는 수학들을 잘 설명 해주다 보면, 아 뭐 이정도면 알겠다라는 식으로 쉽게 지식을 확장하거나, Application 을 만드는데 사용하거나 할수 있을것 같다라는게 총평이다.

조금 아쉬웠을수도 있고, 최대한 입문서에 맞게 쓰려고 하는것도 기준점을 두고 있으니까라고 이해했지만, 내적을 사용하는 이유에 대해서는 배경설명이 조금 필요한것 같기는 하다. 이건 어느정도 선형 대수의 Background 를 이야기하면 좋았지 않을까이다.

Alt text

Reference

React Native Motivation & Installation

React Native 시작 및 동기부여

사실 Interactive Web 을 사용하고 싶었고, 뭔가 항상 가지고 있었던, Front-end 는 별로야. 너무 볼것도 많고, 디자인 할것도 많고, Core Value 가 없어보여.. 이런말만 했었는데, 요즘은 Spatial Computing 이 되게 중요하지 않나? 라고 생각해서 mobile 로 할수 있는게 뭘까? 하니 What is Spatial Computing. 읽어보면,

It’s the purset form of “blending technology into the world

라고 작성이 되어있다. 즉 우리가 사용하는 Computer Hardware 를 사라지게 하며, digital 현상으로 볼수 있는, machine 으로 부터 Output 만 보는 형태! 라고 볼수 있다. Application 으로는 VR/AR/MR 등이 있으며, 아래와 같이 정의한다.

  • VR: VR places the user in another location entirely. Whether that location is computer-generated or captured by video, it entirely occludes the user’s natural surroundings.

  • AR: In augmented reality - like Google Glass or the Yelp app’s monocle feature on mobile devices - the visibile natural world is overlaid with a layer of digital content.

  • MR: In technologies like Magic Leap’s virtual objects are integrated into - and responsive to- the natural world. A virtual ball under your desk, for example, would be blocked from view unless you bent down to look at it. In theory, MR could become VR in a dark room.

좋은 실무 예제로는 Youtube 이 있을것 같다.

Before getting into develop mode

일단 개발환경을 앞서서, 어떻게 XR 과 관련된 개발을 찾아보자. 일단 ARKit, RealityKit 같은 경우는 ARKit 은 증강현실 프레임워크 이고, RealityKit 는 3D Rendering Framework 이다. RealityKit 이 ARKit on top (ARSession) 에 실행된다고 보면 된다. RealityKit 은 rendering 할수 있는 engine 이 존재하고, physics 또는 animation 을 담당한다. 언어로는 swift / object-c 가 있다. 물론 Framework (Unity)를 껴서 개발은 가능하다. 더 자세한건 여기 Forum 에서 보면 될것 같다. 그리고 RealityKit 과 Metal Computing Shader 와는 같이 작동하고, API 를 사용해서 렌더링 성능을 향상 시킨다.

그리고 OpenXR 같은 경우 vulkan 을 만든 chronus group 에서 만들어졌고, 뭐 C/C++ 을 사용한다. 그리고 나머지는 WebXR 이다. 웹브라우저에서 XR 을 지원하기 위해서 만들어졌으며, WebGL 함께해서 갭라을 한다고 하자.

물론 Metal 을 공부하는것도 나쁘진 않지만, 기본적인 구조는 DirectX11/12 Graphics Pipeline 는 같은것 같다?(이건 확인 필요!)

Setting up IOS(VM) Dev on Windows or MacOS directly

이 부분은 굉장히 까다로웠다. 일단 기본적으로 환경설정을 고려할때, 굳이 macOS 를 Base 로 쓰고 싶지 않았다. VMWare 설치 및 환경설정 (단 여기서, .iso file 은 unlocker 에 있는 .iso) 파일을 설치하도록 하자. 그리고 Resolution Setting 여기를 확인해보자. 가끔씩 VMWare 가 금쪽이 같은 면 이있지만 App 을 Build 하고 코드 작성하는데 크게 문제가 있지는 않은것 같다. 그리고 XCode 를 혹시 모르니까 설치를 해놓자. 설치하는데 시간을 뻇기는건 어쩔수 없는거긴 하지만, 너무 비효율적이고, 길어진다. 인터넷 같은 경우에는 VMWare Player 세팅에서, NAT: Used to share the host's IP address 만 해놓으면 괜찮다.

그리고 부가적으로, Homebrew 를 설치하자. /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)".

그이후에 https://reactnative.dev/docs/set-up-your-environment?os=macos&platform=ios 여기에서 Environment 를 설정해주자.

brew install node
brew install watchman
ruby - v
brew install rbenv
rbenv install -l
rbenv install {latest_version}  # check
rbenv global {latest_version}   # check
sudo gem install bundler

설치 되었다면 확인 하자!

node - v
nvm - v
npm - v

vim ~/.zprofile # or .zsrc / .bashrc

# add this into .zprofile & .zsrc & .bashrc
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"  # This loads nvm
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"  # This loads nvm bash_completion

source ~/.zprofile  # .zprofile, .zsrc, .bashrc

nvm install --lts
node - v
nvm - v
npm - v

이후에는 XCode 설치 및 설정을 한다. XCode -> Settings -> Command Line Tool 최신으로 바꿔준다. 그리고 IOS Simulator 를 설치하면 끝이다.

Project Setting

프로젝트 생성 관련 및 개발환경 관련된건 두가지가 있다. ExpoReact Native CLI 가 있는데, 자세한건 이 링크 간단하게 말하면, Expo 는 개발 환경 초기설정을 단순화하고 개발 속도가 빠르다는 장점이 있다. Expo go 앱이 있다면 프로젝트 실행이 된다. 하지만 제공되는 API 만 사용해야되고 Native Module 이 없기 때문에, 기술 구현상 어려운 부분이 있다. 그리고 Package 볼때, Expo 에 사용될수 있는지 확인 해야 한다.

React Native 같은 경우 Native module 을 사용할수 있고, 다양한 라이브러리 사용 가능하다. 기본적으로 제공되는 라이브러리가 없다 보니, 대부분 기능 구현에 있어서는 직접 설치해야한다 하지만 장점으로는 유지보수가 잘되어있다고 한다. (이건 잘모름) 배포 하기 위해서는 Android Studio 나 XCode 가 있어야한다. 이 글을 보게 되면 어떤걸로 개발할지가 뭔가 잘 나와있다. 결국에는 요약한건 이거다. (React Native itself is an abstraction over the native layer and expo adds another abstraction on top of that one ... learning react native cli first can help you with debugging issues when using expo)

Create Project

처음 하기에는 expo 로 한다고 했는데, 나는 장기적으로 보기 때문에, cli 로 했다. expo 로 개발하려면 expo 개발환경 설정 참조하자.

기존에 react-native-cli 를 전역(global) 로 설치한적 이 있으면 깔끔하게 지워주자. 그리고 project 를 생성하자.

npm uninstall -g react-native-cli @react-native-community/cli
npx @react-native-community/cli@latest init {project_name}

의존설 설치 관련되서는 npm install 이나 yarn 을 설정하면 된다. 그 이후 cocoapod 를 설치 하면된다.

npm install
cd ios 
bundle exec pod install # or pod install

코드 서명하는법은 skip 하도록 하겠다. 그리고 yarn start 하고 i 를 누르거나 npx react-native run-ios 하면 아래와 같은 그림이 나올거다.

alt text

만약하다가 error code 65 가 나온다면 아래와 같이 해주자

rm -rf ~/Library/Developer/XCode/DerivedData`
sudo killall Simulator
rm -rf ~/Library/Developer/CoreSimulator/Cashes

그리고 아래와 같은 그림으로 나올거다. 주의점으로는 절대 빌드 할때 급하게 exit 하지 말자! 알아서 잘될거다.

alt text alt text

이로 끝! 긴여정이 끝났다. 개발만 해보자!

Hello World

자 일단 크게 변경 할점은 App.tsx JSX 로 되어있는 부분에 function APP() 이부분을 수정을 하면 Hello World 를 볼수 있다.

더 자세한건 Integration with existing app 이걸 보면 될것 같다.

Resource

Combine in swift

Combine 을 알기전에…

Combine 을 알기전에 앞서서, Combine 이 나오기전에 어떻게 비동기 event 를 처리했는지 보자. 아래의 코드는 간단한 Fake Jason Data 를 Load 해서 UI 에 뿌리는 용도이다. @escaping 이라는걸 간단하게 이야기하자면, 일단 함수안에 completionHandler 가 매개상수로 들어오고, 이 closure 는 함수가 시작한 이후에 바로 실행시킬수 있게끔 되어있다. 만약 asyncAfter 를 사용하게 되면, 함수는 끝나는데 closure 가 살아있을수가 없기 때문에, @escaping 사용해서 closure 가 비동기로 사용할수 있게끔 만들어주는거다. 이를 통해서 비동기 처리를 통해서 Data 를 받아 올수 있었다.

import SwiftUI

struct PostModel: Identifiable, Codable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
}

class DownloadwWithEscapingViewModel : ObservableObject {
    @Published var posts: [PostModel] = []
    init(){
        getPosts()
    }
    
    func getPosts(){
        guard let url = URL(string : "https://jsonplaceholder.typicode.com/posts") else { return }
        downloadData(fromURL: url) { (returnedData) in
            if let data = returnedData {
                guard let newPosts = try? JSONDecoder().decode([PostModel].self, from: data) else { return }
                DispatchQueue.main.async{ [weak self] in
                    self?.posts = newPosts
                }
            }
        }
    }
    
    func downloadData(fromURL url: URL, completionHandler: @escaping (_ data: Data?) -> Void) {
        URLSession.shared.dataTask(with: url) { (data, res, err) in
            guard
                let data = data,
                err == nil,
                let res = res as? HTTPURLResponse,
                res.statusCode >= 200 && res.statusCode < 300 else {
                print("No data.")
                completionHandler(nil)
                return
            }
            completionHandler(data)
        }.resume() // start
    }
}

struct DownloadWithEscapingBootcamp: View {
    @StateObject var vm = DownloadwWithEscapingViewModel()
    
    var body: some View {
        List {
            ForEach(vm.posts) { post in
                VStack(alignment: .leading){
                    Text(post.title)
                        .font(.headline)
                    Text(post.body)
                        .foregroundColor(.gray)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
    }
}

#Preview {
    DownloadWithEscapingBootcamp()
}

Combine

애플 Dev 공식문서에 보면…

The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.

즉 Publisher 와 Subscriber 가 있고, Publisher 는 Data 를 방출하거나, 뭔가의 완료의 Signal 보내는 역활을 하고, Subscriber 는 Publisher 가 쏴준 Data 나 완료 Signal 을 받는 역활을 한다. 그리고 그 사이에 Operator 가 있는데 Publisher 가 생성하는 이벤트를 처리하는 역활을 한다. 이때 연산자(map, filter, reduce) 를 사용할수 있다.

기본적인 예시는

import Combine

let publisher = [10, 20, 30, 40, 50].publisher 

publisher
    .map { $0 * 2 } // Operator 를 통해서 값 변환
    .sink { print($0) } // 값을 recevied

Publisher 연산자에도 (Just, Sequence, Future, Fail, Empty, Deferred, Record) 등이 있다. Just 를 사용한 Publisher 를 사용해보자.

import Combine
let justPublisher = Just(20)

justPublisher
    .map { $0 + 50 }
    .sink { print($0) }

아래의 코드는 여기에서 "https://jsonplaceholder.typicode.com/todos/1" todo 를 하나 가지고 와서, Data 를 Publisher 를 통해서 가지고 온이후에 subscriber 로 receive 받은 이후, UI 를 main thread 에서 update 를 해주는 코드이다.

struct PostModel: Identifiable, Codable {
    let title: String
}

class DownloadTodoViewModel : ObservableObject {
    @Published var todos: [PostModel] = []
    var cancellables = Set<AnyCancellable>()
    init() {
        getTodo()
    }

    func getTodo() {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1") else {return}

        URLSession.shared.dataTaskPublisher(for:url)
            .subscribe(on: DispatchQueue.global(qos: .background))
            .receive(on: DispatchQueue.main)
            .tryMap { (data, response) -> Data in
                guard let response = response as ? HTTPURLResponse,
                    response.statusCode >= 200 & response.statusCode < 300 else {
                        throw URLError(.badServerResponse)
                    }
                    return data
            }
            .decode(type: PostModel.self, decoder: JSONDecoder())
            .sink { (completion) in

            } receiveValue: { [weak self] (returnTodo) in 
                self?.todos.append(returnTodo)
            }
            .store(in: &cancellables)
    }
}

그리고 Timer Publisher 의 사용법을 하려고 한다. Timer Thread 의 on 은 어떤 RunLoop 에서 Timer 를 사용할지를 정해줄수 있다. 여기에서는 main thread 를 사용하고, 어떠한 방식으로 RunLoop 을 실행할건지를 넣어주는데 default 값을 넣어주었다. 여기에서 필수적으로 알아야하는건 Timer 가 반환하는 Publisher 는 ConnectablePublisher 이며, 이 Publisher 를 subscribe 해서 실행하려면, connect()autoconnect() 를 사용(첫 subscriber 구독 할때). sink 로 receive 해서 print 를 하면된다. 그리고 main thread 가 기다리면서, 5 초 뒤에 subscribe 취소 해주면된다.

import Combine

let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
var cancellable: AnyCancellable?

cancellable = timer
    .sink { print($0) }

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    cancellable?.cancel()
}

Resource

MultiThreading & Generic

MultiThreading & Generic in Swift

어떤 Language 가 됬든 일단 방대한 Data 를 Loading 을 해야하거나, 어떤 통신에 맞물려서 상태 return 받을 때 main thread 에서 모든걸 하게 되면, Performance 가 떨어진다. Swift 에서는 이걸 어떻게 해결하는지, 동작 방법 및 실제 구현해서 App 에서 어떻게 Profiling 을 하는지도 봐보자.

GCD (Grand Centeral Dispatch)

일단 Multithreading 을 알기 이전에 Grand Central Dispatch 에 대한 용어 부터 보자. wiki 에서 나와있는것 처럼 multi-core processor 와 other symmetric multiprocessing system 을 최적화하는걸 support 하기위해서 만들어졌고, Thread Pool Pattern 으로 Task 기반으로 병렬화를 진행한다. Thread Pool Pattern 생소할수 있는데, Thread Pool 은 결국에는 Thread(일용직) 들을 위한 직업 소개소라고 생각하면 된다. 여러개의 Thread 대기 하고 있다가 할 일이 들어오면, 대기했던애가 들어와서 일(실행) 하게 되는거라고 볼수 있다. Thread Pool 은 Queue 기반으로 만들면된다. 그래서 Swift 에서는 DispatchQueue 를 사용해서 이를 해결한다. 쉽게 말해서 Task 에 대한 병렬 처리 또는 (비)동기 처리 를 총괄하는 것이 GCD 라고 볼수 있다. 아래의 그림을 보면 간략하게 GCD 가 뭔지를 대충 알 수 있고, DispatchQueue, DispatchWorkItem, DispatchGroup(thread group?) 등을 볼수 있다. (참고: Ref)

GCD 에서 제공 하는 Thread 를 살짝 살표 보자면 Main(Serial) 은 UiKit 이나 SwiftUI 의 모든 요소를 담당한다고 볼수 있고, Global(Concurrent) 같은 경우는 system 전체에서 공유가 되며, 병렬적으로 실행되지만 QoS 따라서 prioirity 를 지정할수 있다.

Priority 위의 그림에서 Interactive 가 Highest Priority 를 가지고, 아래로 갈수록 우선순위가 낮아진다. (참조: Energy Efficiency Guide for iOS Apps) 참조한글을 보면 Use Case 별로 아주 잘 나와있다.

DispatchQueue

Apple Developer Doc 에 찾아보다 보니 DisptchQueue 라는걸 이렇게 설명한다. An ojbect that manages the execution of tasks serially or concurrently on your apps main thread on a background 마치 QT 하고 비슷한 역활을 하는구나라고 볼수 있다. DispatchQueue 는 결국엔 어떤한 work 에 해당되는 item 들이 있다보면, 그 work 의 실행을 Thread Pool 에 넘겨서, executuion 된다고 볼수 있다.

Thread 를 이야기할때는 내가 짠 프로그램이 Thread Safe 한지를 Check 를 해야하는데, 이 DispatchQueue 는 Thread-Safe 한다고 한다. (즉 Thread 들이 한곳에 접근 가능하다는 뜻이다.)

위에 GCD Image 를 보면 Serial 과 Concurrent 로 나눠지는데, Serial 순차적으로 Task 진행 (전에 있던 Task 가 끝난 이후), Concurrent 는 작업이 끝날때까지 기다리지 않고, 병렬 형태로 동시에 진행이다. 결국엔 이게 asyncsync 키워드로 나눠진다.

일단 DispatchQueue 사용법을 봐보자. 일단 print 된걸 보면 제일 마지막에 Main Thread 가 돌아가고, Aync 로 돌리기 때문에 개념상으로는, for-loop 과 다른 background 와 userInteractive 용 thread 가 동시에 돌리는걸 볼수 있다. 그리고 계속 돌리다보면, Output 은 다를것이다. 하지만 확인할수 있는건 userInteractive 가 background 보다는 더 빨리 돈다는걸 확인할 수 있다.

import Foundation

func foo() {
    for i in 1...5 {
        print(i)
    }
}

func fooo(){
    for i in 6...9 {
        print(i)
    }
}

// Closure
DispatchQueue.main.async {
    print("Main Thread \(Thread.current)")
    print("UI Update")
    foo()
}

DispatchQueue.global(qos: .background).async {
    print("Background Thread \(Thread.current)")
    foo()
}

DispatchQueue.global(qos: .userInteractive).async {
    print("User Interactive Thread \(Thread.current)")
    foo()
}

for item in 10...20{
    print(item)
}

/* Output
10
User Interactive Thread <NSThread: 0x60000171b100>{number = 7, name = (null)}
11
6
7
8
Background Thread <NSThread: 0x60000172c080>{number = 8, name = (null)}
1
12
13
14
15
16
17
18
19
2
20
9
3
4
5
Main Thread <_NSMainThread: 0x600001704080>{number = 1, name = main}
UI Update
1
2
3
4
5
*/

Another Example

import Foundation

func loadData(completionHandler: @escaping (_ msg: String) -> ()){
    DispatchQueue.global().async {
        print("Start Loading Data", Thread.current)
        sleep(3)
        
        DispatchQueue.main.async {
            completionHandler("Finished Loading")
        }
    }
}

loadData { msg in print(msg, Thread.current)}

Application 구현

예를 들어서 어떤 Data 를 다운로드 받아서 display 를 한다고 하자. 물론 어떤 Loader 로 부터 다운로드 받아서 fetch 하기는 하는데, 여기에서는 간단하게, 한군데에서 하고, downloadData method 자체를 private 으로 구분해주자. 각 class 역활은 BackgroundThreadViewModel class 는 fetch, download 를 하고 async 로 Data 를 Download 받고 fetch 로 UI 에 다가 download 된 데이터를 뿌려준다라고 보면될것 같다. 일단 .background thread 에서 돌리는거 하나 `main 에서 UI update 해주는걸 생각하면 될것 같다.

import SwiftUI

class BackgroundThreadViewModel : ObservableObject {
    @Published var dataArray: [String] = []
    
    func fetchData() {
        // Background Thread
        // DispatchQueue.global().asyncs
        DispatchQueue.global(qos: .background).async {
            let newData = self.downloadData()
            
            // Main Thread Update (UI)
            DispatchQueue.main.async {
                self.dataArray = newData
            }
        }
    }
    
    private func downloadData() -> [String] {
        var data: [String] = []
        for x in 0..<50 {
            data.append("\(x)")
        }
        return data
    }
}

struct BackgroundThreadBootcamp: View {
    @StateObject var vm = BackgroundThreadViewModel()
    
    var body: some View {
        ScrollView {
            LazyVStack(spacing: 10){
                Text("LOAD DATA")
                    .font(.largeTitle)
                    .fontWeight(.semibold)
                    .onTapGesture {
                        vm.fetchData()
                    }
                ForEach(vm.dataArray, id: \.self) { item in
                    Text(item)
                        .font(.headline)
                        .foregroundColor(.red)
                }
            }
        }
    }
}

#Preview {
    BackgroundThreadBootcamp()
}

Preview

자.. 여기에서 할수 체크할수 있는건 build 를 해보고 돌려보는거다. 아래의 그림을 보면 Main Thread 1 에서 첫 Loading 과 그리고 뿌려질때의 spike 가 보이는걸로 보이고, thread 3 에서 이제 downloading 하는걸 볼수 있다. 그 이외에 background thread 도 아마 관찰이 가능할거다. 여기에서 중요한점은 무조건 thread 를 많이 사용하면 좋지 않다라는 점과 developement doc 에서도 sync 로 했을경우에 deadlock 현상이 나타날수 있다는거만 주의하면 과부하가 잃어나지 않는 앱을 만들수 있을것이다.

Profiling

실제 Image Loader 를 만들어본다고 하자. 총 3 가지의 방법이 있다고 한다. escaping, async, combine 형태로 아래의 코드를 봐보자. 배경설명은 이러하다. URL 로 부터, 서버에서 Image 를 가져와서 화면에 뿌려주는 그런 앱을 작성한다고 하자. 일단 URL 과 UImage 를 받았을때의 Handler 를 작성한걸 볼수 있다. Data 를 못받으면 nil 로 return 을 하고, 아니면 Data 를 받아서 UIImage 로 변경해주는 코드이고, response error handling 도 안에 있다.

일단 기본적으로 escape 를 사용한걸 보면, URLSession.shared.dataTask 자체가 closure 형태로 전달로 받고, .resume() method 를 반드시 작성해줘야하며, 하나의 background thread 로 동작한다. 그리고 completionHandler 를 통해서 image 를 받을시에 UIImage 와 함께 error 코드를 넘겨준다. (void return). 그 이후 image 를 fetch 한 이후에 main thread 를 update 해야 UI 에서 보여지기 시작한다.

이것만 봤을때는 코드가 잘작동은 되겠지만, 별로 깔끔하지못하다. 그 아래 코드는 combine 이다. 위의 Escaping 코드를 본다고 하면, combine 도 not so bad 이다. 정확한건 combine 이라는 개념만 이해하면 잘작성할수 있을것 같다.

마지막으로는 async 를 사용한 데이터 처리이다. URLSession.shared.data(from: url, delegate: nil) 여기 함수 signature 을 보면 data(from: URL) async throw -> (Data, URLResponse Description ...to load data using a URL, creates and resume a URLSessionDataTask internally... 라고 나와있다. 즉 이 함수를 호출하게 되면 바로, URLSession.shared.data(from: url, delegate: nil) 호출하고 tuple() return 을 받지만, response 가 바로 안올수도 있기 때문에 await 이라는 keyword 가 필요하다. 그래서 await 을 사용하게 되면, 결국엔 response 가 올때까지 기다리겠다라는 뜻이다. 그 이후에 downloadWithAync() 를 호출할때, concurrency 를 만족하기위해서 여기에서도 async keyword 가 필요하다. 그렇다면 마지막으로 main thread 에서 어떻게 UI Update 를 할까? 라고 물어본다면, 답변으로 올수 있는 방법은 DispatchQueue.main.async { self?.image = image } 하지만 아니다. main thread 에서 이걸 처리를 하려면, .appear 부분에서 Task 로 받아서 await 으로 처리해주면 된다. 빌드 이후에 Warning 이 뜰수도 있는데 이부분은 Actor 라는걸로 처리를 하면된다. 물론 Actor 라는건 이 post 에서 벗어난 내용이지만 따로 정리를 해보려고 한다.

import SwiftUI
import Combine

class DownloadImagesAsyncImageLoader {
    let url = URL(string: "https://picsum.photos/200")!
    
    func handleResponse(data: Data?, res: URLResponse?) -> UIImage? {
        guard
            let data = data,
            let image = UIImage(data: data),
            let res = res as? HTTPURLResponse,
            // res code
            res.statusCode >= 200 && res.statusCode < 300 else {
            return nil
        } 
        return image
    }
    
    // escaping
    func downloadWithEscaping(completionHandler: @escaping (_  image: UIImage?, _ error: Error?) -> ()) {
        // async code
        URLSession.shared.dataTask(with: url) { [weak self] data, res, err in
            let image = self?.handleResponse(data: data, res: res)
            completionHandler(image, err)
                return
        }
        .resume()
    }
    
    // Combine
    func downloadWidthCombine() -> AnyPublisher<UIImage?, Error>{
        URLSession.shared.dataTaskPublisher(for: url)
            .map(handleResponse)
            .mapError({ $0 })
            .eraseToAnyPublisher()
    }
    
    // Async
    func downloadWithAync() async throws -> UIImage? {
        do {
            let (data, res) = try await URLSession.shared.data(from: url, delegate: nil)
            return handleResponse(data: data, res: res)
        } catch {
            throw error
        }
    }
}

class DownloadImagesAsyncViewModel : ObservableObject {
    @Published var image: UIImage? = nil
    let loader = DownloadImagesAsyncImageLoader()
    var cancellables = Set<AnyCancellable>()
    
    func fetchImage() async {
        
        // escape
        loader.downloadWithEscaping {
            [weak self] image, error in
            DispatchQueue.main.async {
                self?.image = image
            }
        }
        
        // combine
        loader.downloadWidthCombine()
            .receive(on: DispatchQueue.main)
            .sink { _ in } receiveValue: { [weak self] image in
                    self?.image = image
            }
            .store(in: &cancellables)
        
        // async 
        let image = try? await loader.downloadWithAync()
        // Error -> run this main thread
        // To resolve this issue: key concept for actor
        await MainActor.run {
            self.image = image
        }
    }
}

struct DownloadImagesAsync: View {
    @StateObject private var vm = DownloadImagesAsyncViewModel()
    
    var body: some View {
        ZStack {
            if let image = vm.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 250, height:250)
            }
        }
        .onAppear(){
            Task {
                // get into async task
                await vm.fetchImage()
            }
        }
    }
}

#Preview {
    DownloadImagesAsync()
}

Generic

사실 cpp 에서는 Meta programming 이라고도 한다. swift 에서도 generic 을 일단 지원한다. 어떤 타입에 의존하지 않고, 범용적인 코드를 작성하기 위해서 사용된다.

이런건 코드로 보면 빠르다.

func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var a = 10
var b = 20
swapValues(&a, &b)
print(a, b)

struct Stack<T>{
    // Generic Type(T) Array
    private var elements : [T] = []

    mutating func push(_ value: T) {
        elements.append(value)
    }

    // std::optional T (null)  | swift (nil)
    mutating func pop() -> T? {
        // exception
        guard !elements.isEmpty else {
            return nil
        }
        return elements.popLast()
    }
    
    var top: T?{
        return elements.last
    }
    
    func printStack(){
        if elements.isEmpty{
            print("Stack is Empty")
        } else {
            print("Stack: \(elements)")
        }
    }
}
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)

if let item = intStack.pop() {
    print("Pooped Item : \(item)")
}

Reference

Closure & OOP

Swift 에서의 Closure 를 문법을 배우고, 활용한다.

Closure 을 알기 이전에…

C++ 에 있는 Function Type 을 이해하면 편하다. Swift 에서도 Function Type 이 존재한다. 아래의 코드를 보면 Function Type Declaration 이 존재한다. (Int, Int) -> Int 하고 addTwoInts 라는 함수를 참조한다. (여기에서 참조). 즉 swift 가 할당을 허락한다. 라는 뜻. 저 불편한 var 로 할당된걸, 함수로 표현하게 되면, (Int, Int) -> Int 자체를 함수의 Parameter 로 넘겨줄수도 있다.

// Function Type Example

func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}

func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    return a * b;
}

func printHelloWorld() {
    print("hello, world")
}

var mathFunction: (Int, Int) -> Int = addTwoInts
print("Result : \(mathFunction(2, 3))")

func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFunction(a, b))")
}

그리고 다른 함수의 반환 타입으로 함수 타입을 설정할수 있다. 반환하는 함수의 반환 화살표를 -> 사용해서 함수타입으로 사용할수 있다.

// Function Type Example

func stepForward(_ input: Int) -> Int {
    return input + 1
}
func stepBackward(_ input: Int) -> Int {
    return input - 1
}

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    return backward ? stepBackward : stepForward
}

var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)

여기서 (Int) 라는 chooseStepFunction 안에 있는 함수의 Return 을 의미하고, Parameter 는 Boolean 값으로 넘겨주며, return 을 그 다음 화살표인 Int 로 한다는 뜻 이다.

아직 코드가 간결하지 않다, Nested 함수로 해보자. 함수안에 함수를 작성하는게 불필요할수도 있지만, Closure 을 이해하기전에 필요한 정보이다. 아래의 코드를 보면, currentValue > 0 이면 False 를 반환하고, chooseStepFunction(backward) 가 stepForward 함수를 반환한 이후, 반환된 함수의 참조값이 moveNearerToZero 에 저장된다.

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    func stepForward(input: Int) -> Int { return input + 1 }
    func stepBackward(input: Int) -> Int { return input - 1 }
    return backward ? stepBackward : stepForward
}

var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)

while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}

Closure in Swift

C++ 에서 Closure 라고 하면, 주로 Lambda Expression (Lambda) 를 Instance 화 시켰다고 볼수있다. 즉 위에서 본것 처럼, swift 에서도 똑같은 의미를 가지고 있다. 자 중첩함수에서 본것 처럼 chooseStepFunction 이라는 함수 이름이 존재했다. 그리고 값을 (=) 캡쳐해서 currentValue 를 Update 하였다. closure 는 결국 값을 캡처할수 있지만, 이름이 없는게 Closure 의 개념이다.

Closure 의 Expression 은 아래와 같다. (<#parameters#>) -> <#return type#> Closure 의 Head 이며, <#statements#> 는 Closure 의 Body 이다. Parameter 와 Return Type 둘다 없을수도 있다.

{ (<#parameters#>) -> <#return type#> in
   <#statements#>
}

주의점이 하나 있는데, 예를들어서 아래의 코드를 본다고 하자. 첫번째 print 를 했을때는 "Hello, Nick" 이라는게 나온다. 하지만 두번째 Print 에서는 error: extraneous argument label 'name:' in call print(closure(name: "Jack")) 라는 Error 가 뜬다. Closure 에서는 argument label 이 없다. 이 점에 주의하자. 일반 함수 Call 과 다르다. 여기에서 또 봐야할점은 Closure Expression 을 상수에 담았다.(***)

let closure = { (name: String) -> String in
    return "Hello, \(name)"
}

print(closure("Nick"))
print(closure(name: "Jack"))

그리고 Function Type 을 이해했다라고 한다면, Function Return Type 으로 Closure 을 return 할수 있으며, 함수의 Parameter Type 으로도 Closure 가 전달이 가능하다. 여기서 첫번째로는 아까 그냥 함수를 작성할때와는 다르게 Argument Label 이 없어야한다고 하지 않았나? 근데 실제 Arugment Label 이 없으면 missing arugment label 'closure' 이라는 Error 를 내뱉는다. 즉 에는 Argument Label 이 Parameter 로 전달됬다는걸 볼수 있다. 그리고 return 같은 경우는 위의 Function Type 을 이해했다면 충분히 이해할수 있는 내용이다.

// function Type: Closure as Parameter
func doSomething(closure: ()->()){
    closure()
}

doSomething(closure: {() -> () in print("Hello!")
})

// function Type: Closure as Return
func doSomething() -> () -> () {
    
    return { () -> () in
        print("Hello Nick!")
    }
}

var closure = doSomething()
closure()

클로져의 용도는 간단하면서 복잡한데, 주로 Multithreading 에서 안전하게 State 관리를 하기 쉽다. 하지만 관리 하기 쉽다는건 항상 뭔가의 Performance 가 조금 Expensive 하다는 점이다. 조금 더 자세한걸 알면 Functor 를 보면 될것 같다.

Closure Example Code

// Closure 
let numbers = [1,3,5,7,9]
let doubled = numbers.map { $0 * 2 } // m
print(doubled)

var counter = 0
let incrementCounter = {
    counter += 1
}
incrementCounter()
incrementCounter()
print(counter) // 출력: 2

OOP

OOP 는 생각보다 간단하다. 객체 지향적이라는 말이긴한데. 결국엔 Class 로 Instance 들을 쉽게 관리한다라는 말이다. 반댓말로는 절차 지향적이라는 말이 있다.

원칙의로는 Encapsulation, Inheritance, Polymorphism, Abstraction 있다.

class Animal {
    var name : String
    
    init(name : String){
        self.name = name
    }

    func makeSound(){}
}

class Dog : Animal {
    override func makeSound(){
        print("Bark!")
    }
}

class Cat : Animal {
    override func makeSound(){
        print("Meow!")
    }
}

let cat = Cat(name: "jack")
cat.makeSound()

let dog = Dog(name: "Nick")
dog.makeSound()

Reference

Data Structure & Memory Management in Swift

일단 Swift 안에서는, Array, Queue, Stack 이 Data Structure 이 있다. 뭔가 c++ 처럼 Library 를 지원 queue 나 stack 을 지원하나 싶었는데? 없다고 한다 ref 그래서 직접 구현하란다. 하지만 Deque<Element>, OrderedSet<Element>, OrderedDictionary<key, value>, HeapCollection Package 에 있다고 한다. 흠 왜? Array 하고 Sets 는 주면서? 조금 찾아 보니, Array, Set 의 최소한의 자료구조만 표준으로 제공하고, 다른건 Pacakaging 해서 쓰란다. 그리고 생각보다 Generic 도 잘되어있지만, 역시 CPP 하고 비교했을때는 불편한점이 있긴한것같다.

Array & Sets

  • 둘다 Randomly Accessible 하다. 이 말은 Array 같은 경우, 순차적으로 메모리에 저장되므로, 어떤 특정 Index 에서 접근 가능. Set 같은 경우, Hash Table 기반으로 구현되어 있어서 var mySet: Set = [10, 20, 30] .contain() 라는 함수로 있으면 True 없으면 False return 을 한다.
  • 왜? 삽입/삭제시 성능 저하? => 찾아서 지워야할텐데, Element 가 마지막이면 O(n) 이니까, 그래서 Set 사용하면 되겠네
  • Set 과 Array 의 차이점: Set 은 unordered array 는 ordered.

Example Code

import Dispatch
let array = Array(1...1_000_000)
let set: Set = Set(array)
let target = 999_999

let startArray = DispatchTime.now()
print(array.contains(target)) // O(n)
let endArray = DispatchTime.now()
let nanoTimeArray = endArray.uptimeNanoseconds - startArray.uptimeNanoseconds
let timeIntervalArray = Double(nanoTimeArray) / 1_000_000_000
print("Array Execution Time: \(timeIntervalArray) seconds")

let startSet = DispatchTime.now()
print(set.contains(target)) // O(1)
let endSet = DispatchTime.now()
let nanoTimeSet = endSet.uptimeNanoseconds - startSet.uptimeNanoseconds
let timeIntervalSet = Double(nanoTimeSet) / 1_000_000_000
print("Set Execution Time: \(timeIntervalSet) seconds")

### 결과 ### 
true
Array Execution Time: 0.044636319 seconds
true
Set Execution Time: 5.1e-06 seconds

Queue & Stack

  • 예제 코드들 보니까, Generic 도 사용할수 있다. 그리고 Swift 에서 특별하다고 생각했던게, 요청하지 않는 한 value type 인 애들의 속성값 변경을 허용하지 않는다는거, 즉 Instance method 에서 수정할수 없다는거, 특이하구나…

Queue

  • FIFO (First In, First Out) 구조 (ex: printer & BFS)

Example Code

struct Queue<T>{
    // Generic Type(T) Array
    private var elements : [T] = []

    mutating func enqueue(_ value: T) {
        elements.append(value)
    }

    // std::optional T (null)  | swift (nil)
    mutating func dequeue() -> T? {
        // exception
        guard !elements.isEmpty else {
            return nil
        }
        return elements.removeFirst()
    }

    var head : T? {
        return elements.first
    }

    var tail : T? {
        return elements.last
    }
    
    func printQueue(){
        if elements.isEmpty {
            print("Queue is Empty")
        } else {
            print("Current Queue: \(elements)")
        }
    }
}

var queue = Queue<String>()
queue.enqueue("Nick")
queue.enqueue("Kayle")
queue.enqueue("Juan")

queue.printQueue()

if let serving = queue.dequeue() {
    print(serving) // Optional("Nick")
}
if let nextToServe = queue.head { //Optional("Kayle")
    print(nextToServe)
}

queue.printQueue()

Stack

  • LIFO (Last In, Last Out) 구조 (call stack)

Example Code

struct Stack<T>{
    // Generic Type(T) Array
    private var elements : [T] = []

    mutating func push(_ value: T) {
        elements.append(value)
    }

    // std::optional T (null)  | swift (nil)
    mutating func pop() -> T? {
        // exception
        guard !elements.isEmpty else {
            return nil
        }
        return elements.popLast()
    }
    
    var top: T?{
        return elements.last
    }
    
    func printStack(){
        if elements.isEmpty{
            print("Stack is Empty")
        } else {
            print("Stack: \(elements)")
        }
    }
}

var cookieJar = Stack<String>()
cookieJar.push("chocolate")
cookieJar.push("walnut")
cookieJar.push("oreo")

cookieJar.printStack()

if let popItem = cookieJar.pop() {
    print(popItem)
}

cookieJar.printStack()

if let topItem = cookieJar.top {
    print(topItem)
}

Memory Management in Swift

  • IPhone 이라는 어떤한 Device 를 놓고 봤을때, Storing Data 의 방법은 두가지 있을것 같다.
  1. Disk
  2. RAM
  • 만약 App 을 실행시킨다고 했을때, executable instructions 이 RAM 에 올라가고, system OS 에서 RAM 의 덩어리 일부분(Heap)을 Claim 하면서, App 을 실행시킨다. 그래서 앱에서 실행시키는 모든 Instance 들이 Life cycle 을 가지게 된다. C/CPP 에서도 마찬자기로 malloc / new / delete heap 영역에서의 memory management 를 프로그래머가 해주니까 뭐 말이된다.
  • Swfit 에서는 Memory Management 는 ARC 에서 해준다.결국에는 모든 Instance 들이 reference count 라는걸 가지고있고, 그 reference count 는 properties, constants, and variable 들에 strong reference 로 잡혀져 있다. 그래서 ref count 가 0 일이 될때 메모리 해제된다! 완전 Smart Pointer 잖아! 또 궁금한게, Garbage Collection 이라는 Keyword 도 무시할수 없는건데, 이것도 전부다 ARC 에서 한다고 한다 그리고 Ownership 도 생각해봐야할 문제 인것 같다.

Reference -> Coupling -> Dependency

  • Reference 를 생각하고 개발하다보면, 결국에 오는건 Coupling / Dependency / Circular Dependency 문제이다. 그래서 C++ 에서는 Interface 사용하거나, weak_ptr 사용해서, Strong Count 를 안하게 하는 방법이 있다.
  • swift 에서는 Weak Reference 나 Unowned Reference 를 사용한다고 한다. 바로 예제코드를 보자.
class Person {
    var name: String
    var pet: Pet? // optional 
    init(name: String)
    {
        self.name = name
    }
    
    deinit {
        print("\(name) is destructed")
    }
}

class Pet {
    var name: String
    var owner: Person? // optional 
    
    init(name: String)
    {
        self.name = name
    }
    
    deinit {
        print("\(name) is destructed")
    }
}

var person: Person? = Person(name:"Nick")
var pet: Pet? = Pet(name: "Jack")

// Circular Dependency
person?.pet = pet
pet?.owner = person

person = nil
pet = nil
  • 와 근데 아무런 Error 안나오는게 사실이냐…? 아니면 Online Compiler 라서 그런가보다 하고 넘기긴했는데.. 좋지는 않네. 뭐 근데 정확한건, deinit() 호출 안되니까 해제가 안됬음을 확인할수 있다.

  • 해결하려면, weak 키워드 사용하면 된다. 아래의 코드를 보자.
    class Pet {
      weak var owner: Person?
    }
    
  • 이걸로 변경하면, 서로의 deinit() 호출되면서 Nick 먼저 해제, 그 다음 Pet 해제 형식으로 된다.

  • 다른 하나방법은 unowned 키워드 사용하면 된다.
    class Pet{
      unowned var owner: Person
    }
    

Difference Between Unowned and weak

  • weak 는 Optional 이고, Optional 일 경우에는 Unwrapped 을 해줘야한다.(이말은 Optional 값이 nil 이 아닐 경우에 Safe 하게 unwrap 해줘야하는거) 참조된 객체가 해제되면 nil 로 설정된다. 즉 객체가 해제 되어야하는 상황에 쓸것이다.
  • unowned 는 Optional 이 아니다. 그리고 참조된 객체가 해제되면 RunTime Error 가 발생한다. (즉 이말은 unowned reference 는 항상 Value 를 갖기를 원한다. = 이거 좋음), weak 와 다르게 unowned unwrap 할필요가없다. 항상 Value 를 가지고 있기 때문이다.
  • 자세한건 What Is the Difference Between Weak and Unowned References in Swift
class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is destructed")
    }
}

class Pet1 {
    var name: String
    weak var owner: Person?  // weak 참조는 옵셔널 타입

    init(name: String, owner: Person?) {
        self.name = name
        self.owner = owner
    }
    deinit {
        print("\(name) is destructed")
    }
}

class Pet2 {
    var name: String
    unowned var owner: Person
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }
    deinit {
        print("\(name) is destructed")
    }
}

var person: Person? = Person(name: "Nick")
var pet1: Pet1? = Pet1(name: "weak dog", owner: person!)
var pet2: Pet2? = Pet2(name: "unowned dog", owner: person!)

// safe unwrap
if let owner = pet1?.owner {
    print(owner.name) 
}

print(pet2!.owner.name) 

person = nil
pet1 = nil
pet2 = nil

Resource

Pagination