Giter Club home page Giter Club logo

til's Issues

Optional

Optional

스크린샷 2022-06-06 오후 2 56 11

  • 우선 옵셔널의 원형을 확인해보면 놀랍게도 Optional이 열거형 그리고 제네릭(Wrapped)을 사용하고 있는 것을 확인 할 수 있다.
  • ExpressibleByNilLiteral 프로토콜을 채택하는 것을 확인 할 수 있다.
var optionalValue: Optional<Int> = nil

if optionalValue == .none {
  print("This Value is nil")
}

if optionalValue == nil {
  print("This Value is nil")
}

원형이 열거형이기 때문에 당연히 위와 같이 확인 해 볼 수 있다.

먼저 ExpressibleByNilLiteral 프로토콜의 의미를 확인해보자

ExpressibleByNilLiteral 프로토콜은 변수를 선언할 때 nil로 초기화 할 수 있음을 의미한다
위의 문장이 이해가 되지 않는다면 우리가 타입 추론을 통해서 변수나 상수를 초기화 했던 것을 생각해보면 된다.

var name = "Sangwon" // 어떻게 문자열 리터럴을 통해서 타입추론이 가능했을까?

스크린샷 2022-06-06 오후 3 20 11

이는 String이 ExpressibleByStringLiteral 프로토콜이 채택하기 때문에 가능했다.

public init(_ some: Wrapped) // Optional Value
public init(nilLiteral: ()) // nil Value

그리고 생성자를 확인해보면 위와 같이 2개의 생성자를 가지는 것을 확인 할 수 있다.

Swift Source Code 보충하기!

동시성 프로그래밍 - 02

Concurrency

1편에 이어서 두번째 정리글이다!

사실 강의를 빠르게 다 듣고 정리하면서 복습하는 느낌으로 하려고 했는데 생각보다 강의 내용이 어려워서 정리하면서 복습하고 강의 내용을 이어서 들어야 할것 같다 😂

복습

오늘은 GCD의 종류와 특성, 그리고 사용할 때 주의사항에 대해 정리해보려고 하는데 그 전에 이전에 다뤘던 내용을 한번 정리 해보자

  • 작업들을 여러 쓰레드에 분산 시켜서 효율적으로 동작 시키기 위해서 동시성 프로그래밍을 이용한다
  • iOS에서는 쓰레드를 직접 관리하지 않고 GCD/Operation Queue에 넣어주면 OS가 알아서 쓰레드에 일을 분배한다
  • GCD/Operation Queue 에는 Serial queue, Concurrent queue 라는 특성이 있다
  • 동기와 비동기는 어떤 작업을 큐로 보낼 때 해당 작업의 완료를 기다리거나 바로 다음 작업을 시작하는 것을 의미한다
  • 직렬과 동시는 큐의 특성으로 직렬 큐의 경우 큐에 할당된 작업을 하나의 쓰레드에서 처리하도록 하고 동시큐는 큐에 할당된 작업을 여러 쓰레드에서 동시에 처리할 수 있도록 한다

GCD의 종류와 특성

GCD(=Dispatch Queue) 는 크게 3가지 종류로 나눌 수 있습니다.

  • 메인 큐
  • 글로벌 큐
  • 프라이빗 큐(커스텀)

메인큐

메인 큐는 오직 한 개만 존재하고, Serial 특성을 가지고, 메인큐에 할당된 작업들은 메인 쓰레드에서 동작한다!

우리가 동시성 프로그래밍을 고려하지않고 단순히 코드를 작성하면 메인 쓰레드에서 모든 작업을 처리한다고 했었다. 그럼 우리가 별도의 처리를 하지 않으면 작업들이 메인 큐에 할당된다는 것을 알 수 있다.

//DispatchQueue.main.sync {
  print("Hello")
//}

코드로 확인해보면 위와 같이 우리가 별도의 작업을 하지 않으면 DispatchQueue.main.sync가 감싸진 형태로 동작하는 것이다.
(물론 실제로 저렇게 코드를 작성하면 메인쓰레드가 쓰레드-세이프 하지 않기 때문에 오류가 발생한다!)

정리해보면 메인 쓰레드도 하나 메인큐도 하나 존재한다. 그러니까 메인큐는 필연적으로 Serial한 특성을 가질 수 밖에 없다. 왜? 여러 쓰레드에 동시 작업을 넣을 수가 없으니까!!

메인 쓰레드 이야기가 나온김에 메인 쓰레드의 중요한 특성에 대해서 하나 짚고 넘어가면 UI 관련 작업은 메인 쓰레드에서 동작한다
이는 iOS뿐만 아니라 모든 OS에서 동일하다. 왜 그럴까? 만약 화면전환 관련 로직이 여러 쓰레드에서 처리되어서 순서가 보장되지 않게 되면 우리가 의도하지 않은 동작이 일어날 수 있다!


글로벌 큐

글로벌 큐는 Concurrent 특성을 가지고, QoS(=Quality of Service)를 통해서 작업의 중요도를 결정 할 수 있다.

QoS에는 6가지 종류가 있다.

  • userInteractive
  • userInitiated
  • default
  • utility
  • background
  • unspecified
let userInteractiveQueue = DispatchQueue.global(qos: .userInteractive)

우선순위가 높은것 부터 차례대로 적으면 위와 같다. 우리가 QoS를 설정만 해주면 우선 순위가 높은 일에 더 많은 쓰레드를 알아서 배치 해준다.


프라이빗 큐(커스텀 큐)

커스텀 큐는 기본적으로는 Serial한 특성을 가지지만 Concurrent 로 설정할 수 있고 QoS 또한 설정 해줄 수 있다.

let customQueue = DispatchQueue(label: "bran")
let customConcurrentQueue = DispatchQueue(label: "bran", attributes: .concurrent)

코드로 확인해보면 위와 같다. QoS를 우리가 따로 설정해주지 않으면 OS가 알아서 추론해준다!

✋ 주의사항

1) 메인쓰레드에서 다른 큐로 작업을 보낼 때 sync를 사용하면 안된다

아까 메인 큐에 대해서 설명할 때 메인 쓰레드에서 UI 관련된 작업이 진행된다고 했었는데 메인 쓰레드에서 어떤 작업을 큐에 sync 로 보내면 어떻게 될까?

스크린샷 2022-06-21 오후 10 53 35

만약 Concurrent 특성을 가지는 큐에 task1, task2 를 sync 하게 보냈다고 가정해보자.

스크린샷 2022-06-21 오후 10 55 10

Concurrent한 특성을 가지는 큐이기 때문에 여러 쓰레드에서 작업들을 처리하게 될것이다. 하지만 sync하게 보냈기 때문에 UI를 담당하고 있는 메인 쓰레드는 다음 큐에 보낸 작업들이 완료되기 전까지 다음 작업을 이어서 진행할 수 없다!

스크린샷 2022-06-21 오후 10 59 11

큐로 보낸 작업들이 만약 시간이 오래 걸리는 작업이라면 그 시간동안 앱은 멈춰버리는 현상이 발생하고 짧더라도 우리가 피하고 싶던 앱이 버벅이는 현상이 나타나게 된다! 그래서 메인 쓰레드에서 큐에 작업을 보낼 때는 항상 비동기를 보내야 한다!

2) 현재와 같은 큐에 sync로 작업을 보내면 안된다

스크린샷 2022-06-21 오후 11 06 45

우선 위와 같이 Task1 안에 Task2가 포함되어 있는 작업이 있다고 가정해보자. 현재 Task1을 Concurrent 특성을 가지는 큐에 비동기적으로 보낸 상태이다. 근데 Task1에 포함된 Task2를 Concurrent 특성을 가지는 큐에 동기적으로 보내면 어떻게 될까?

DispatchQueue.global().async {
// Task 1
  DispatchQueue.global().sync {
  // Task2
  }
}

코드로 표현하면 이런 상황이다.

스크린샷 2022-06-21 오후 11 25 18

Task2를 Concurrent 특성을 가진 Queue에 sync 방식으로 넣어줬기 때문에 쓰레드1은 해당 작업이 완료 될 때까지 Block 상태가 된다.
Concurrent 특성을 가진 큐이기 때문에 큐는 들어온 Task2를 쓰레드1, 쓰레드2, 쓰레드3 어디든 넣어 줄 수 있고 우리가 여기에 개입할 수는 없다.

그런데 쓰레드1에서 Task2가 할당되면 어떻게 될까?
스크린샷 2022-06-21 오후 11 34 35

Task2가 완료되기 전에는 Task1을 실행할 수 없는데 Task2가 멈춰있는 쓰레드에 할당되었기 때문에 완료될 수가 없다.
이러한 상황을 교착상태(데드락)이라고 한다!

물론 큐에서 다른 쓰레드로 Task2를 할당하면 위와 같은 상황이 발생하지 않지만 그럴 가능성이 존재한다. 따라서 현재와 같은 큐에 sync 로 작업을 보내면 안된다!

그럼 어떻게 위와 같은 상황을 피할 수 있을까?
global 큐를 사용하는 경우, QoS다른 큐를 생성해서 사용하면 쓰레드가 겹치지 않는다고 한다!

위와 동일한 이유로 메인 쓰레드에서 DispatchQueue.main.sync 를 사용하면 안된다. 이해 되겠죠?

  • 메인 쓰레드에서 메인 큐로 작업을 보낸다 동기적으로
  • 메인 큐는 다시 메인 쓰레드로 작업을 보낸다
  • 하지만 동기적으로 메인 큐에 작업을 보냈기 때문에 메인 쓰레드는 block 상태이다

해당 내용은 앨런님의 강의를 듣고 이해한 내용을 정리한 글입니다!!

DispatchGroup 까지 강의를 들었는데 최대한 빨리 복습하고 정리하도록 하겠습니다!

HTTP 웹 기본 지식 - 1

IP(=Internet Protocol)

  • 지정한 주소에 패킷단위로 데이터 전달

Untitled

IP 프로토콜만을 이용해서 데이터를 송,수신 하는 과정은 간단하게 생각하면 사용자가 온라인 쇼핑몰에서 물건을 주문하는 것과 유사합니다. 단순히 주소값을 입력해 물건을 주문하면 서버에서 해당 주문을 받고 사용자의 주소로 사용자가 요청한 자료를 보내주는 형태입니다.

이러한 IP 프로토콜에는 명확한 한계가 존재합니다.

1) 비연결성

클라이언트 → 서버, 서버 → 클라이언트 형태로 패킷을 보낼 때, 대상 서버가 패킷을 받을 수 있는 상태인지 확인하지 않습니다. 즉, IP 프로토콜을 통한 데이터 통신은 연결 상태를 확인하지 않고 일방적으로 데이터를 보내기 때문에 패킷을 받지 못하는 상황이 발생합니다

2) 비신뢰성

현재 그림에서는 클라이언트와 서버사이에 단순히 하나의 박스로 표현되어 있지만 실제 인터넷에서는 수많은 노드를 거쳐서 패킷이 전달됩니다. 여기서 발생하는 문제가 있습니다

  • 패킷 유실

수 많은 노드를 거치면서 서버로 보낸 패킷이 유실될 수 있습니다. (우리가 주문한 물건이 곤지암 버뮤다 물류센터에서 길을 잃는 것처럼) 하지만 서버는 클라이언트에서 어떤 패킷을 보냈는지에 대한 정보가 전혀 없기 때문에 우리가 보낸 패킷이 유실되었다는 사실 조차 확인할 수 없습니다.

  • 패킷 전달 순서

일반적으로 우리가 큰 데이터를 서버로 보내는 경우, 한 번에 모든 데이터를 보내기 보다는 데이터를 잘라서 보내게 되고, 그럼 데이터의 순서가 중요해집니다. 하지만 클라이언트 → 서버 로 통하는 노드는 하나가 아니기 때문에 우리가 먼저 보낸 데이터보다 이후에 보낸 데이터가 먼저 도착하는 경우가 발생합니다.

하지만 서버는 여러개의 데이터가 올거라는 사실조차 모르기 떄문에 이런 상황을 해결할 수 없습니다


TCP(=Transmission Control Protocol)

앞서 살펴본 IP의 문제점을 해결하기 위해서 탄생한게 TCP입니다. 그럼 앞선 문제들을 TCP를 이용하면 어떻게 해결되는지 확인해봅시다.

![모든 개발자를 위한 HTTP 웹 기본 지식 - 1강 강의자료]
Untitled 1

인터넷 프로토콜 4계층을 확인해봅시다.

우선, 앞서 살펴본 Internet Protocol을 사용하지만 TCP가 추가된것을 확인 할 수 있습니다. 기존의 IP 패킷을 TCP라는 상자로 포장해서 서버로 보내는 형태입니다.

1) 연결지향

IP에서의 비연결성을 해결할 수 있는 특징입니다.

TCP는 서버-클라이언트간 데이터를 주고받기 전에 먼저 서로 연결되어 있는지를 먼저 확인합니다.

연결시작 시점: 3 way handshak
연결종료 시점: 4 way handshake (#23, 관련 이슈)

그렇기 때문에 서버와 클라이언트간 연결이 정상적이지 않은 경우 데이터를 보내지 않습니다.

2) 데이터 전달 보장

IP에서의 패킷 유실을 해결할 수 있는 특징입니다.

서버-클라이언트 연결이 되어 있는 상태에서 클라이언트에서 서버로 어떤 정보를 보내고 서버가 해당 패킷을 받으면 클라이언트로 패킷을 받았다는 ack 보내줍니다.

만약 클라이언트가 패킷을 보내고 일정시간 서버로 부터 ack을 받지 못하는 경우에는 다시 패킷을 보내는 방식으로 데이터 전달을 보장할 수 있습니다.

3) 패킷 전달 순서

IP에서의 패킷 전달 순서 문제를 해결할 수 있는 특징입니다.

TCP를 통해 패킷의 순서를 미리 알고 있기 때문에 올바른 순서로 패킷이 오지 않은 경우, 다시 데이터를 요청하거나 내부적으로 조합해서 올바른 데이터를 받을 수 있습니다.


UDP(=User Datagram Protocol)

  • 연결성이 보장되지 않는다 → 신뢰성 x
  • 단순하고 빠르다
  • IP + Port

PORT

Untitled 2

클라이언트에서 여러 서버와 통신하고 있는 경우, 같은 IP에 여러 데이터가 오는데 어떻게 구분할 것인가?

패킷에 존재하는 port를 통해서 프로세스를 구분할 수 있다.


DNS(=Domain Name System)

우리가 www.google.com을 입력하면 DNS 서버에서 IP주소로 변환해서 웹사이트를 보여주고 있는것 처럼 IP 전화번호부로 생각하면 됩니다.

  • IP는 기억하기 어려움
  • IP는 변경될 수 있음

이러한 특징 때문에 일반적으로 IP주소 보다는 DNS서버에 IP를 등록하고 도메인 이름을 통해서 통신합니다.

[CS] WebSocket, Socket.io

WebSocket

WebSocket 과 Socket.io

지금까지 소켓 연결을 해본 프로젝트가 하나밖에 없어 정확하게 공부하지 못했는데 기본적으로 둘은 다르다.

궁금한것

  • 그럼 저 둘의 차이는 서버 구현에 따라 클라이언트에서 선택해야 하는것인가? (서버가 socket.io 라이브러리로 구현되어 있어야 클라이언트도 해당 라이브러리를 선택할 수 있는 것인가?)
  • StarScream 과 socket.io-swift 라이브러리의 차이도 위와 같은 기준으로 선택해야 하는 것인가?
  • WebSocket ping, pong, hand shake의 의미

WebSocket

서로 다른 컴퓨터끼리 소통하기 위한 프로토콜

  • HTML5 웹 표준 기술
  • 매우 빠르게 작동하며 통신할 때 아주 적은 데이터를 이용함
  • 이벤트를 단순히 듣고, 보내는 것만 가능함

Socket.io

양방향 통신을 위해 웹소켓 기술을 사용하는 라이브러리

  • 표준 기술이 아니며, 라이브러리임
  • 소켓 연결 실패 시 fallback을 통해 다른 방식으로 알아서 해당 클라이언트와 연결을 시도함
  • 방 개념을 이용해 일부 클라이언트에게만 데이터를 전송하는 브로드캐스팅이 가능함

단편적으로 봤을 때, 우리가 Socket 연결을 통해 얻으려는 이점이(양방향 통신, 실시간 네트워킹) 동일하기 때문에 비슷하게 생각했던것 같다.

웹소켓을 사용하기 이전에 양방향과 실시간 네트워킹을 위해서는 일정 주기로 API Call을 쏴서 결과를 확인하는 Polling 방식을 사용했는데 단순하게 생각해봐도 서버에 부하가 심해진다

이런 문제점을 해결하기 위해 나온것이 WebSocket이고 HTML5의 기술이기 때문에 오래된 버전의 웹 브라우저는 이를 지원하지 않기 때문에 Socket.io를 사용한다고 한다


1)

Socket.io를 사용하는 경우 js를 통해서만 개발 할 수 있으며, 서버는 socket.io를 클라이언트는 socket.io-client 라이브러리를 사용해야만 한다.

따라서 서버가 socket.io를 이용해서 개발되어 있지 않다면 클라이언트 단에서는 당연히 socket.io-client를 사용할 수 없다.

2)

1번과 일맥상통하는 부분이다. 물론 서버에서 socket.io로 만들었을 때, 클라이언트에서 URLSessionSocket이나 StarStream을 이용해서 소켓 통신을 할 수도 있지만 socket.io만이 가지고 있는 브로드캐스팅과 같은 기능이 정상적으로 동작할지는 미지수이며 socket.io를 사용하지 않는 편이 구현 난이도도 더 낮다.

따라서 서버에 맞춰 클라이언트에서 라이브러리나 프레임워크를 선택하는 것이 적절하다.

Swift WebSockets: Starscream or URLSession in 2021?

StarScream과 URLSessionSocket은 위의 글을 읽어보고 선택하면 될 것 같다.

간단하게 요약하면 URLSession은 Apple에서 계속해서 지원해주고 있고 Starscream은 최근 업데이트가 없다

URLSessionSocket이 iOS13과 함께 나온 기술이기 때문에 최소버전이 iOS13 이상이라면 URLSession을 사용하는 것도 나쁘지 않은 선택이다 (물론 receive를 재귀적으로 호출해야 하는 불편함 존재)

3)

사실 ping, pong이 hand shake 과정 중 일부라고 생각했었는데 둘은 완전 다르다.

일반적으로 서버와 클라이언트가 소켓으로 연결되고 난 이후에 둘 사이에 아무런 데이터 송, 수신이 없는 상태가 길어지면 서버에서 클라이언트와의 연결을 끊어낸다 (idle timeout)

이런 연결 종료를 막기 위해서 서버 or 클라이언트에서 서로 ping, pong을 주고 받으며 연결을 확인한다.

핸드 쉐이크 과정은 소켓 통신은 결국 TCP 프로토콜을 따르고 있기 때문에 소켓 연결을 시작할 때와 종료될 때, 핸드 쉐이크를 통해서 모두 데이터를 송,수신 할 준비가 되어있음을 보장하는 것을 의미힌다.

딥링크 - 01

딥링크

프로젝트 진행 중 FCM을 이용해 날라온 푸시를 클릭 했을 때 관련 내용을 앱 내에서 화면전환 해서 보여주는 기능이 필요했다. (ex 카카오톡 푸시를 눌렀을 때 해당 대화방으로 이동)

이런 기능등을 처리할 때 딥링크, 앱링크, 유니버셜 링크 등등을 들어보긴 했지만 한번도 구현해본적은 없어서 이번기회에 한번 정리해보려고 한다. SwiftUI를 이용해서 구현해야 한다는 제약사항이 있어 아쉽긴 하지만 기회가 된다면 UIKit을 이용한 프로젝트에도 정리하는 시간을 가져볼 예정이다.

용어정리 부터 하고 넘어가보자

딥링크

특정 주소 혹은 값을 입력하면 앱이 실행되거나 앱 내 특정 화면으로 이동시키는 기능을 수행

우선 딥링크, 앱링크, 유니버셜링크를 각각 개별적으로 생각하고 있었는데 딥링크라는 큰 범주안에서 앱링크, 유니버셜 링크로 나누어진다. (다이나믹 링크, deferred 딥링크는 추후 정리)

Untitled

그림으로 나타내면 위와 같은 형태이다. 기본적으로 특정 링크를 입력(클릭) 했을 때 개발자가 원하는 페이지로 사용자들을 안내하는 기술을 딥링크라고 하고, 딥링크를 구현하는 방법이 위와 같이 존재한다.

(앱링크는 AOS에서 사용하는 용어)

1) URL Scheme

일반적이고 초기에 사용된 딥링크 구현 방식으로 앱에 스킴값을 등록해서 사용한다.

{ Scheme : 앱을 구분 }:// { Path : 앱내 페이지 구분 }

kakaotalk://me
sms://

위의 링크를 사파리에 입력해보자. 카카오톡이 설치되어 있는 휴대폰일 경우 카카오톡이 열리는 것을 확인할 수 있을것이다.

간단하게 위의 내용을 한번 구현해보자(정대리님의 딥링크 강의를 정리한 내용입니다.)

정대리님 강의 링크](https://www.youtube.com/watch?v=kjDl_15fOEQ)

Untitled 1

우선, 위와 같이 스킴을 등록해준다.

enum TabIdentifier: Hashable {
  case todo
  case profile
}

enum PageIdentifier: Hashable {
  case todoItem(id: UUID)
}

extension URL {
  var isDeepLink: Bool {
    return scheme == "deeplink-bran"
  }

  // 어떤 탭을 보여줄 것인가?
  var tabIdentifier: TabIdentifier? {
    guard isDeepLink else { return nil }

    // deeplink-bran:// { host }
    switch host {
    case "todo":
      return .todo
    case "profile":
      return .profile
    default:
      return nil
    }
  }

  //deeplink-bran://todo/ED7E277E-1A5E-4197-AF6D-1D9ED1BFFF9F
  var detailPage: PageIdentifier? {
    guard
      let tabIdentifier = tabIdentifier,
      pathComponents.count > 1,
      let uuid = UUID(uuidString: pathComponents[1])
    else { return nil }

    switch tabIdentifier {
    case .todo:
      return .todoItem(id: uuid)
    case .profile:
      return nil
    }
  }
}

URL이 들어온 경우 { Scheme }:// { Path } 형태의 URL인지 확인하고, Path를 통해서 사용자에게 적절한 페이지로 보내주는 과정이 필요하다.

위의 코드는 Scheme:// {TabIdentifier} / {PageIdentifier?} 형태로 들어온 URL에서 사용자를 적절한 페이지에 보내주기 위해 필요한 데이터들을 뽑아 낼 수 있도록 extension으로 구현한 코드이다.

struct TodoTabView: View {
  let todoModels: [TodoListItem] = [...]

  @State
  var selectedTab: TabIdentifier = .todo

  var body: some View {
    tabSection
      .onOpenURL { url in
        guard let tabId = url.tabIdentifier else { return }
        selectedTab = tabId
      }
  }
}

extension TodoTabView {
  @ViewBuilder
  var tabSection: some View {
    TabView(selection: $selectedTab) {
      TodoView(todoListItems: todoModels)
        .tabItem {
          Image(systemName: "tray")

          Text("TODO")
        }
        .tag(TabIdentifier.todo)

      ProfileView()
        .tabItem {
          Image(systemName: "person")

          Text("Profile")
        }
        .tag(TabIdentifier.profile)
    }
  }
}

우리가 설정한 Scheme://Path 형태의 URL이 들어왔을 경우, 어떻게 화면 전환를 처리해줄 수 있을까?

자세하게 보진 않았지만 UIKit에서는 SceneDelegate, AppDelegate에서 위의 내용을 처리해주고 있는것 같다. SwiftUI에서는 onOpenURL이라는 modifier를 통해서 간편하게 처리할 수 있다.

Brandnew-one/Practice-SwiftUI

해당 프로젝트는 위의 깃허브 링크에서 전체를 확인 할 수 있다. 위의 프로젝트에서 URL Scheme을 통해 개발자가 설정한 탭바 & 네비게이션 푸쉬가 정상적으로 동작하는 것을 확인 할 수 있다.

하지만 처음 딥링크 그림에서 개선 이 걸린다. URL Scheme에는 어떤 문제가 있어서 유니버셜 링크라는 개념이 나오게 된걸까? 이는 위의 프로젝트를 요리조리 테스트 해보다 보면 의외로 쉽게 발견할 수 있다.

  • 앱이 설치되어 있지 않는 경우 실행할 수 없음
  • 동일한 Scheme이 존재하는 경우 문제가 발생함

앱이 설치되어 있지 않는 경우, 주소가 유효하지 않기 때문에 Safari가 해당 페이지를 열 수 없다는 에러 메시지가 나오고, 번들 identifier를 바꿔서 동일한 Scheme이 동일한 여러개의 앱을 만들고 테스트 하면 어떤 앱이 열릴 지 우리가 보장할 수 없는 문제가 발생하는데 이 Scheme은 사용자가 사용하고 있는 앱에서 Identifiable를 보장 할 수 없다.

다음 토이 프로젝트에서는 유니버셜 링크를 간단하게 구현해보고 정리해볼 예정이다.

[SwiftUI] TextField, SecureField in Alert

Link 에서 확인할 수 있듯이 일반적으로 우리가 UIKit에서 사용했던 텍스트필드가 있는 AlertView를 SwiftUI에서는 16.0 미만에서는 지원하지 않는다

  1. UIViewControllerRepresentable을 이용 (랑크)
  2. SwiftUI 4.0 사용 (테스트 예정)

프로세스와 스레드

프로세스와 스레드

우선 키워드 중심으로 먼저 정리해보면 다음과 같다.

  • 프로그램(Program) : 어떤 작업을 위해 실행할 수 있는 파일
  • 프로세스(Process) : 운영체제로부터 자원을 할당받는 작업의 단위
  • 스레드(Thread) : 프로세스 내에서 실행되는 여러 흐름의 단위

프로세스는 각각의 독립된 메모리 영역 (Heap, Data, Stack, Code)를 OS로 부터 할당받고 기본적으로 프로세스당 최소 1개의 스레드를(메인스레드) 할당 받습니다. 각 프로세스는 별도의 주소공간에서 실행되고, 한 프로세스는 다른 프로세스의 메모리에 접근하기 위해서는 별도의 통신이 필요합니다.

스레드는 한 프로세스내에서 스택 영역만 따로 할당받고 Code, Heap, Data 영역을 공유받아 프로세스가 할당받은 작업을 수행한다. 스레드는 한 프로세스 내에 여러개가 존재할 수 있고 같은 프로세스에 있는 스레드들은 서로 자원을 공유할 수 있다.


멀티 프로세스와 멀티 스레드

  • 멀티-프로세스 : 하나의 응용 프로그램을 여러개의 프로세스로 구성해서 각 프로세스가 하나의 작업을 처리

멀티 프로세스의 경우 프로세스의 특성을 생각하면 장,단점을 파악하기 쉽다.

  • 프로세스는 다른 프로세스와 별도의 주소공간에서 실행된다 -> 여러개의 자식 프로세스 중 하나에 문제가 발생하면 해당 프로세스만 죽고 다른 프로세스에는 문제가 발생하지 않는다.
  • 한 프로세스에서 다른 프로세스 메모리에 접근하기 위해서는 별도의 통신이 필요하다 -> 프로세스끼리는 독립된 메모리 영역을 할당 받기 때문에 Context Switching이 발생하면 캐쉬에 있는 메모리를 모두 지우고 캐쉬정보를 다시 불러와야한다.(시간적을 손해)

Context Switching : CPU에서 여러 프로세스를 돌아가면서 작업을 처리하는 과정, 동작중인 프로세스가 해당 프로세스의 상태를 저장하고 대기하고 있는 다음 프로세스가 동작하면 이전에 보관했던 상태를 복구


  • 멀티-스레드 : 하나의 응용 프로그램을 여러 스레드로 구성하고 각 스레드가 하나의 작업을 처리

마차가지로 스레드의 특성에 맞춰서 멀티 스레드를 생각해보자

  • 같은 프로세스내 스레드들은 자원을 공유한다 -> 스레드간 데이터를 주고 받는 과정이 간단하므로 시스템 자원 소모가 줄어든다.
  • 여럭개의 프로세스를 생성하지 않아도 되기 때문에 자원을 할당하는 시스템 콜이 줄어들어 자원을 효율적으로 관리 할 수 있다.

이렇게만 보면 멀티-스레드 방식이 멀티-프로세스 방식보다 좋은점만 있는것 같지만 다양한 문제점들이 존재한다.

  • 멀티 스레드의 경우 자원을 공유하기 때문에 동기화 문제가 발생할 수 있다.
  • 하나의 스레드가 문제가 발생하면 해당 스레드를 소유한 프로세스 전체가 영향 받는다.

따라서, 멀티-스레드를 위해서는 주의 깊은 설계가 필요하다

RSA - 1

RSA

RSA도 지난번에 확인했던 ECDSA와 마찬가지로 비대칭키 암호화의 한 유형이다.

그렇기 때문에 암호화와 디지털 서명에 사용될 수 있는데 ECDSA를 통해서 디지털 서명과정을 구현해보았기 때문에 이번에는 RSA를 통해서 암호화, 복호화 과정을 한번 구현해보려고 한다.

  • Private Key, Public Key 생성
  • Public Key를 통한 암호화
  • Private Key를 통한 복호화

순서로 구현하면서 내용을 정리할 예정이다


1) Key Pair 생성

RSA가 뭔지도 모르는데 어떻게 만드나요?

ECDSA와 마찬가지로 애플에서 제공해주고 있다. 우리는 비대칭키 암호화에 대한 특성만 알고 있다면 전혀 문제없다.

import Foundation

final class RSA {
  private var privateKey: SecKey?
  private var publicKey: SecKey?
  private let dataStorage: KeyChainDataStorage

  init(
    dataStorage: KeyChainDataStorage
  ) {
    self.dataStorage = dataStorage
    if let privateKey = dataStorage.read(key: .rsa),
       let secKey = stringToSecKey(privateKey) {
      print("From KeyChain")
      self.privateKey = secKey
      self.publicKey = SecKeyCopyPublicKey(secKey)
    } else {
      print("From New")
      generateRSAKeyPair()
      saveRSAKey()
    }
  }

  // FIXME: -  Key Representation을 설정할 수 없는 문제점
  private func generateRSAKeyPair() {
    var error: Unmanaged<CFError>?
    let parameters: [CFString: Any] = [
      kSecAttrKeyType: kSecAttrKeyTypeRSA,
      kSecAttrKeySizeInBits: 2048,
      kSecAttrKeyClass: kSecAttrKeyClassPrivate
    ]
    guard
      let privateKey = SecKeyCreateRandomKey(parameters as CFDictionary, &error),
      let publicKey = SecKeyCopyPublicKey(privateKey)
    else { return }

    self.privateKey = privateKey
    self.publicKey = publicKey
  }
}

KeyChainDataStorage는 추후 키체인 저장에서 다루기로 하고 키 생성에서 확인해야 할 부분은

  • ECDSA와 달리 CryptoKit을 사용하고 있지 않다
  • Key의 타입이 SecKey 타입이다
  • CFError, CFString, CFDictionary 등등의 타입들은 무엇일까?

ECDSA 암호화 알고리즘 구현의 경우, CryptoKit을 이용했지만 RSA는 Security를 이용한다.

SecKey 타입도 Security에 정의된 클래스로 정확한 구현체는 확인할 수 없지만 암호화 키를 나타내는데 사용되고 이를 이용해서 암호화, 복호화, 서명들을 구현할 수 있다.

그리고 코드들에 CF가 붙은 타입들이 많은데 Core Foundation에서 제공하고 있는 데이터 타입으로 Swift Type과 함께 사용하기 위해서는 타입 캐스팅이 필요하다

키 생성 과정을 확인해보면 우리가 원하는 RSA Private Key 스펙을 CFDictionary에 담아서 Private Key를 만들고 Private Key를 통해서 Public Key를 생성하는 것을 확인할 수 있다.


ECDSA 알고리즘에서 키를 생성할 때, Representation을 설정할 수 있었는데 RSA에서는 어떻게 설정해야 할까?

공식 홈페이지랑 스택오버 플로우를 많이 뒤졌지만 생성하는 시점에 Representation을 설정하는 것은 따로 찾지 못했다.
있을 수 있습니다. 찾으면 수정하겠습니다!

사실 암호화 알고리즘은 단순 구현 보다는 서버와 스펙을 맞추는 과정이 중요한데 생성자를 찾지 못해서 개인적으로 굉장히 아쉬웠다.

https://github.com/TakeScoop/SwiftyRSA

위의 라이브러리를 사용하면 Representation 설정뿐만 아니라 encrypt/decrypt 과정에 swift에서 기본적으로 제공하지 않는 패딩 모드들도 제공하고 있으니 필요에 따라서 사용하면 될 것 같다.

프로세스 주소공간

프로세스는 운영체제로 부터 자원을 할당받는 작업의 단위이고
운영체제로 부터 할당받은 자원은 독립된 Heap, Data, Stack, Code 로 구성되어 있다고 학습했습니다.

오늘은 프로세스 메모리 영역에 대해서 좀 더 자세히 알아보겠습니다.

스크린샷 2022-03-27 오후 7 51 40

Code(TEXT)

  • 우리가 작성한 소스코드가 들어가는 부분으로 실행할 프로그램의 코드가 저장되는 영역
  • 함수, 제어문, 상수등이 여기에 지정된다
  • 컴파일 타임에 결정되고 중간에 코드를 바꿀 수 없도록 Read-Only

Data

  • 프로그램의 전역변수와 정적변수가 저장되는 영역 -> 프로그램이 실행되는 동안 항상 접근 가능한 변수가 저장
  • 데이터 영역은 프로그램의 시작과 함께 할당되며, 프로그램이 종료되면 소멸한다
  • 실행중에도 전역변수가 변경될 수 있으니 Read-Write
  • 초기화된 데이터는 Data 영역에 저장되고, 초기화 되지않은 데이터는 BSS 영역에 저장된다.

Stack

  • 함수의 호출관 관계되는 지역변수와 매개변수가 저장되는 영역
  • Stack은 함수의 호출과 함께 할당되며, 함수의 호출이 종료되면 소멸한다
  • Heap 영역에 생성된 Object 타입의 데이터 참조값이 할당된다. (인스터스 생성시 주소값이 Stack에 저장된다)
  • 메모리의 높은 주소에서 낮은 주소 방향으로 할당된다.
  • 컴파일 타임에 크기가 결정되기 때문에 무한히 할당할 수 없다 -> Stack Overflow

Heap

  • 런타임에 크기가 결정되는 메모리 영역이다.
  • 사용자에 의해 메모리 공간이 동적으로 할당되고 해제된다
  • 참조형의 데이터의 값이 저장된다. (인스턴스의 실제 데이터가 저장되는 공간)
  • Heap은 메모리의 낮은주소 부터 높은 주소 방향으로 할당된다.

HEAP, STACK 영역은 같은 공간을 공유하고 있다.

따라서 어느 한쪽의 영역이 상대공간을 침범하는 일이 생길 수 있고 이를 Heap Overflow, Stack Overflow 라고 한다.

기존의 지식과 조금 엮어서 풀어보겠습니다.

1 ) 코딩테스트 문제를 풀 때, 겪던 메모리 초과

보통 백준에서 알고리즘 문제를 풀 다 보면 심심찮게 메모리 초과를 경험할 수 있습니다. 물론 실제로 문제에서 요구하는 메모리 보다 더 많은 메모리를 사용하는 경우일 수도 있지만 대부분 지역변수로 할당된 메모리를 전역변수로 수정하면 위와 같은 문제를 피할 수 있었습니다.
이는 보통 OS에서 성능상의 이유로 Stack 영역에 메모리 제한을 걸어두기 때문입니다. (윈도우: 1MB, 리눅스: 8MB)
따라서 전역변수로 선언한 데이터들은 Data 영역에 저장되기 때문에 위와 같은 문제를 피할 수 있었습니다.

2) ARC

Swift에서는 클래스를 통해서 인스턴스를 생성할 경우

  • 실제 인스턴스 메모리는 Heap 영역
  • 메모리를 가리키는 주소값은 Stack 영역

에 저장되고 RC가 0이되는 순간에 자동으로 Heap 영역의 메모리를 해제하는 ARC 라는 기능이 있습니다.

C언어의 경우, Heap 영역 메모리를 사용하기 위해서는 Calloc, Malloc과 같은 함수를 통해서 사용자가 지정한 크기 만큼을 Heap 영역에 할당하고 Stack 영역에는 마찬가지로 주소값만을 가지고 있게 됩니다.
하지만 C에서는 메모리 누수를 피하기 위해서는 Swift와 달리 Heap영역 메모리 해제를 위해서 free 해주는 과정이 필요합니다.

동시성 프로그래밍 - 01

Concurrency

미루고 미루던 동시성 프로그래밍에 대해서 드디어 한번 자세하게 정리해보고자 한다!
강의와 블로그를 활용해서 휘발성 메모리를 가진 미래의 나를 위해 최대한 자세하고 이해하기 쉽게 풀어보려고 한다

다운로드

동시성 프로그래밍이 왜 필요힐끼?

동시성 프로그래밍이 필요한 이유는 단순하다. 동시성 프로그래밍을 이용하지 않고 메인 쓰레드에서 모든 작업을 수행하게 되면 느리다.
예를 들면 네트워크 통신을 통해 불러온 값을 테이블 뷰에 보여줄 때 스크롤을 하면 앱이 뚝뚝 끊기는 현상이다.

그럼 동시성 프로그래밍을 이용하면 위와 같은 문제를 어떻게 빠르게 처리할 수 있는걸까?
메인 쓰레드에서 모든 작업을 처리하는것은 실생활로 예를들면 아래 그림과 같다

스크린샷 2022-06-20 오후 6 58 25

불쌍한 노동자 한명만 열심히 일을 하고 있다. 나 못참아!!!

스크린샷 2022-06-20 오후 7 26 48

동시성 프로그래밍을 활용하면 위와 같이 한 노동자에게 작업이 몰리지 않고 이를 적절하게 동시에 수행할 수 있도록 작업을 분배할 수 있다

그럼 쓰레드(노동자)에게 작업을 일일이 직접 분배해야 하나요?

정말 감사하게도 우리는 직접적으로 쓰레드를 관리하지 않고, iOS에서 제공하는 대기행렬(Queue)에 보내기만 하면 된다.

스크린샷 2022-06-20 오후 8 15 53

  • GCD(=Dispatch Queue)
  • Operation Queue

iOS에서 제공하는 대기행렬은 크게 위의 두 종류이다!

우리는 작업들을 위의 큐에 넣어주면 각 큐의 특성에 맞게 OS에서 관리해준다!!

DispatchQueue.global().async {
  print("Task1")
  print("----------Task1---------")
}

사용예시를 보면
DispatchQueue(큐에 보낸다.).global()(global 이라는 큐에).async (비동기적으로){ 이하 작업을 } 으로 이해할 수 있다.

처음 공부할 때 나를 멘붕에 빠트렸던 동기/비동기 키워드가 보이는데 천천히 하나씩 풀어가보자!

동기와 비동기

놀랍게도 이전에 동기와 비동기에 대해서 간단하게 정리했던 적이 있다. 내 기억 어디간거야? 🤦

위와 같은 그림을 예제로 동기와 비동기를 이해해보자!
스크린샷 2022-06-20 오후 9 00 50

노동력 착취에 화가 나버린 메인 쓰레드가 Task1 을 큐에 보낸 상황이다.
하지만 메인 쓰레드가 해야하는 작업들(Task2, Task3, Task4)가 여전히 남아있죠?

이때 메인 쓰레드가 해야할 행동으로 올바른것은?

  • 아 몰랑 Task1이 완료될 때 까지 쉴래 (동기)
  • 어림도 없지 바로 Task2 시작 (비동기)

이게 동기와 비동기의 개념이다.

  • 동기는 메인쓰레드에서 큐로 보낸 작업이 완료될 때 까지 기다린다
  • 비동기는 메인쓰레드에서 큐로 보내고 작업 완료를 기다리지 않고 다음 작업을 시작한다.

사실 처음 공부할 때 비동기와 동기 그리고 동시와 직렬의 개념이 뒤죽박죽 섞이면서 위의 개념이 명확하게 확립되지 않았는데 이전에 우리가 기억하던 지식들을 배제하고 딱 저렇게 이해하자!

직렬과 동시 (Serial & Concurrent)

직렬과 동시는 큐의 특성에 관한것을 의미한다. 앞서 iOS에서 제공하는 큐는 크게 GCD, Operation Queue 2개로 나뉜다고 했는데 2개의 큐 모두 Serial, Concurrent로 사용할 수 있다.

Serial Queue에 여러 작업들을 보내게 되면 해당 큐는 작업들을 하나의 쓰레드에서 처리한다
Concurrent Queue에 여러 작업들을 보내게 되면 해당 큐는 작업들을 여러개의 쓰레드에서 처리한다

여기서 의문이 들 수 있다

  • 아니.. Serial Queue에 비동기적으로 여러 작업들을 처리하면 무슨 의미가 있는거야?

스크린샷 2022-06-20 오후 6 58 25

스크린샷 2022-06-20 오후 10 30 58

스크린샷 2022-06-20 오후 10 32 43

Serial Queue에 Task1,2,3를 async 하게 보냈다.
async하기 때문에 메인 쓰레드는 Task4를 바로 실행하게 될 것이고, Serial Queue는 작업들을 하나의 쓰레드에서 처리하기 때문에 파랭이 쓰레드가 큐에 있던 모든 작업들을 순차적으로 실행하게 된다.

  • 아니.. Concurrent Queue에 동기적으로 여러 작업들을 처리하면 무슨 의미가 있는거야?

스크린샷 2022-06-20 오후 6 58 25

스크린샷 2022-06-20 오후 10 39 07

스크린샷 2022-06-20 오후 10 41 05

Concurrent Queue에 Task 1,2,3를 sync하게 보냈다. Concurrent Queue에 이기 때문에 큐에 들어온 작업들은 동시적으로 여러 쓰레드에서 처리되고 sync 이기 때문에 큐에 들어갔던 작업들이 모두 완료되어야 task4가 실행된다


항상 헷갈렸던 개념을 다시 한번 정확하게 짚어 볼 수 있는 시간이였다.
Serial/Concurrent 와 Sync/Async 는 다른 개념이다!!

해당 내용은 앨런님의 강의를 듣고 이해한 내용을 정리한 글 입니다.

계속해서 정리 할 예정이고 틀리거나 부족한 부분이 있으면 언제든지 알려주세요!

Xcode 개발(배포)환경 나누기 - 2

개발환경 나누기 - 2

Custom Flag 설정

우리가 설정한 환경별로 분기처리를 하려면 어떻게 해야할까?

  • deploy phase (plist 사용)
  • 전처리문 사용

방법은 많지만 위의 두 방법을 통해서 간단하게 처리할 수 있다. 첫번쨰 방법은 지난번에 정리했기 때문에 두번쨰 방법에 대해서 정리해보자.

전처리문은 컴파일 이전에 처리되는 문장으로 주로 C언어에서 #defin으로 자주 사용했었는데 iOS에서는 어떻게 사용할까?

  • OS구분 및 버전 분기
  • Flag를 이용한 전처리
// MARK: - OS 분기처리
#if os(iOS)
print("iOS")
#elseif os(macOS)
print("macOS")
#else
print("other OS")
#endif

// MARK: - Flag 분기처리
#if DEBUG
print("Debug")
#else
print("Relase")
#endif

(전처리문이 뭐야? 했던 분들도 아마 위의 코드는 한번씩은 봤던 기억이 있을겁니다)

Swift에는 전처리기가 없다는 사실을 알고계셨나요?? -> 해당 내용은 빌드과정을 정리하면서 한번 정리해보겠습니다.

제목을 통해서도 알 수 있겠지만 우리는 Flag를 이용해서 개발환경별로 전처리문을 이용할 예정이다.

Untitled

프로젝트를 처음 생성하면 기본적으로 DEBUG Flag가 존재하는 것을 확인 할 수 있다.

세팅에서 바로 값을 수정해도 되지만 Build Configuration의 환경변수들을 .xconfig 파일에서 정리하고 설정하는 법을 지난번에 정리 했기 때문에 해당 방법을 적용해보자.

// MARK: - Prod.xconfig
PRODUCT_BUNDLE_IDENTIFIER = com.bran.envsetting.app
PRODUCT_NAME = EnvSetting
CUSTOM_FLAG = PROD
DEPLOY_PHASE = prod

// MARK: - Stage.xconfig
PRODUCT_BUNDLE_IDENTIFIER = com.bran.envsetting.stage.app
PRODUCT_NAME = EnvSetting-stage
CUSTOM_FLAG = STAGE
DEPLOY_PHASE = stage

// MARK: - Dev.xconfig
PRODUCT_BUNDLE_IDENTIFIER = com.bran.envsetting.dev.app
PRODUCT_NAME = EnvSetting-dev
CUSTOM_FLAG = DEV
DEPLOY_PHASE = dev
Untitled 1

방법 자체는 지난번 bundle identifier, app name 적용하는 방법과 완전히 동일하기 때문에 생략

struct ContentView: View {
  var body: some View {
    VStack {
      Image(systemName: "globe")
        .imageScale(.large)
        .foregroundColor(.accentColor)

      #if DEV
      Text("Hello, DEV world!")
      #elseif STAGE
      Text("Hello, STAGE world!")
      #else
      Text("Hello, world!")
      #endif
    }
    .padding()
  }
}

Run Script 분기처리

firebase, swiftgen과 같은 라이브러리나 다국어 파일 같은 경우 Build Phase에서 run script를 통해 프로젝트로 빌드하는 과정 중에 소스 파일들을 생성한다.

case "${CONFIGURATION}" in
  "Debug(Dev)" | "Release(Dev)" )
    echo "DEV" ;;
  "Debug(Stage)" | "Release(Stage)" )
    echo "STAGE" ;;
  "Debug" | "Release" )
    echo "PROD" ;;
*)
;;
esac

위와 같이 스크립트를 작성하고 build log에서 환경별로 다른 결과값이 출력되는 것을 확인할 수 있다.

Equatable

Equatable

코드를 보다보면 Equatable 프로토콜을 채택하는 것을 많이 확인할 수 있는데 사실 해당 프로토콜을 채택하면

== , != 과 같은 동등성을 비교할 수 있다 정도로만 알고 있는데 조금만 더 자세하게 알아보고자 한다.

let a: Int = 1
let b: Int = 2
if a == b {
  print("True")
}

위와 같이 a == b 가 가능했던건 Int(Struct)가 Equatable 프로토콜을 준수하고 있기 때문이다.

protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}

우리가 만든 클래스나 구조체의 인스턴스가 동일한지 확인하기 위해서는 Eqautable 프로토콜을 채택시켜야 한다.

import Foundation

class A: Equatable {

  var num: Int

  init(num: Int) {
    self.num = num
  }

  static func == (lhs: A, rhs: A) -> Bool {
    return lhs.num == rhs.num
  }
}

let a = A(num: 10)
let b = A(num: 10)
if a == b {
  print("equal") // equal
}

Equatable 프로토콜을 들여다보면 statuc func == (lhs: Self, rhs: Self) → Bool 메서드를 반드시 구현해야 함을 알 수 있다.

클래스A에 Equatable 프로토콜을 채택하고 클래스 A의 프로퍼티인 num이 동일한지를 기준으로 판별해서 Bool 값을 리턴하도록 함수를 구성하고 테스트 해보자

이때 lhs.num == rhs.num이 가능한 이유는 Int 타입이 Equatable 프로토콜을 이미 채택하고 있기 때문이다!

그럼 A를 통해 생성된 인스턴스를 직접 동등비교를 할 수 있는것을 확인할 수 있다.

정리하다보니 참조타입과 값타입에 대한 정확한 정리가 한번 필요할 것 같다!

Local Cocoapods 만들기

Local Cocoapods 만들기

이번 정리에서는 Cocoapod을 이용해서 라이브러리를 만들었을 때 얻을 수 있는 장,단점에 대해서는 정리하지 않고 Cocoapod을 이런식으로 이용할 수 있다 느낌으로 정리해볼 예정이다.

1) 필요성

작업하고 있는 프로젝트가 커지면서 자연스럽게 디자인 리소스 파일도 많아지고 그에 대응 되는 코드들도 많아지는 문제를 겪고 있었다. 물론 r.swift 같은 라이브러리를 통해서 리스소 관련 코드를 자동으로 생성할 수 있지만 안드로이드와 파일명을 맞추는데 어려움이 있었다.

또한 디자인 시스템 파일이 만들어지면서 디자인에서 공통적으로 사용되는 버튼, 팝업, 바텀시트 등등이 생기면서 이런 파일들을 한데 모아 모듈화 시킬 필요성을 느껴서 방법을 찾던중 위의 방법을 통해서 문제를 해결한 과정과 또 새롭게 발생한 문제점들을 아카이빙 해보고자 한다.


2) Local pod 만들기

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-04-04_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_1 23 08

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-04-04_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_1 28 28

터미널에서 위와 같이 pod lib create {라이브러리 명} 을 입력하면 프로젝트 파일이 하나 생성된다.

2-1) podspec 파일 설정하기

Pod::Spec.new do |s|
  s.name             = 'BranUI'
  s.version          = '0.1.0'
  s.summary          = 'A short description of BranUI.'

  s.description      = <<-DESC
  TODO: Add long description of the pod here.
  DESC

# 미설정 ->
  s.homepage         = 'https://github.com/Brandnew-one/BranUI'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'Brandnew-one' => 'cold929@naver.com' }
  s.source           = { :git => 'https://github.com/Brandnew-one/BranUI.git', :tag => s.version.to_s }
# <- 미설정

  s.ios.deployment_target = '15.0'

  s.source_files = 'BranUI/Classes/**/*'
  s.resources = "BranUI/Assets/*.xcassets"
  s.resource_bundles = {
    'BranUI' => ['BranUI/Assets/*']
  }
  s.dependency 'lottie-ios'
end

현재 cocoapods을 통해서 만들고 있는 라이브러리의 spec에 대한 정보가 저장되는 파일이다.

라이브러리 파일을 만들어서 배포하는게 아닌 local로 사용할 예정이기 때문에 미설정으로 표기된 부분을 제외하고 아래 부분에 집중해보자.

현재 라이브러리에서는 디자인 리소스 파일을 포함하고 있기 때문에 리소스 파일 디렉토리를 설정해야 하고 motion과 같은 애니메이션 효과를 위해서 Lottie 라이브러리를 추가해줄 예정이다.

Lottie를 추가했기 때문에 BranUI의 Podfile에서 Lottie를 설치하는 과정이 필요하다!

use_frameworks!

platform :ios, '10.0'

target 'BranUI_Example' do
  pod 'BranUI', :path => '../'

  target 'BranUI_Tests' do
    inherit! :search_paths

    pod 'lottie-ios'
    
  end
end

2-2) Resoucre 파일 설정 및 관련 코드 작성

❗️podspec 파일을 설정할 때, resouce와 source_file들의 디렉토리를 설정하는 부분이 있었는데 해당 디렉토리에 리소스 파일과 코드를 작성하고 .pbx 파일이 해당 파일을 읽어올 수 있도록 설정해줘야 한다

Untitled

Untitled 1

Development Pods 디렉토리에 저장된 파일들은 위와 같이 podspec에 정의된 위치에 있는 파일이어야 한다.

import Foundation

private class BranBundleClass {}

extension Bundle {
  class var branUI: Bundle {
    Bundle(for: BranBundleClass.self)
  }
}

현재 프로젝트의 Bundle 파일에 접근할 수 있도록 설정 해준다.

import SwiftUI
import UIKit

extension Image {
  fileprivate static let bundle: Bundle = .branUI
}

extension UIImage {
  fileprivate static let bundle: Bundle = .branUI

  fileprivate convenience init(
    _ name: String,
    bundle: Bundle
  ) {
    self.init(named: name, in: bundle, compatibleWith: nil)!
  }
}

extension Image {
  public static let foodCart: Image = Image(
    "icons8-food-cart",
    bundle: bundle
  )
}

extension UIImage {
  public static let foodCart: UIImage = UIImage(
    "icons8-food-cart",
    bundle: bundle
  )
}

3) Local pod 사용하기

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'BranTestProject' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Local Pods for BranTestProject
  pod 'BranUI', :path => "../BranUI/"

  # Pods for BranTestProject

end

실제 프로젝트에서 우리가 만든 라이브러리를 사용하기 위해서는 절대경로를 통해서 가져오면 된다.

.
├── git
│   ├── BranTestProject
│   │   ├── ...
│   │   .
│   │   .
│   │   .
│   │   
│   ├── BranUI
│   │   ├── ...

실제 프로젝트는 https://github.com/Brandnew-one/LocalPod 에서 확인할 수 있습니다.

시스템 콜

시스템콜이란 프로세스가 컴퓨터를 관리하는 시스템인 운영체제(OS)에게 어떤 기능을 사용하게 해달라고 요청할 떄 사용하는 방법입니다.

이전에 프로세스는 운영체제로 부터 자원을 할당받는 작업의 단위라고 공부 했는데 만약 프로세스가 자기 프로그램 이외의 특정 파일 데이터를 필요로 한 경우에는 어떻게 할까요?

프로세스는 다른 프로세스의 메모리에 접근할 수 없기 때문에 운영체제에 요청을 통해서 해당 데이터에 접근하게 되고 이런 요청을 시스템콜이라고 합니다.

일반적으로 운영체제는 User ModeKernel Mode로 독립된 동작 모드를 가지고 있습니다.
앞서 말한 상황처럼, 프로그램이 구동되는데 파일을 읽어 오거나, 쓰거나, 출력하는 부분은 Kernel Mode를 통해서 가능합나디.

즉, 정리하자면 시스템콜은 커널 영역의 기능을 사용자 모드가 사용가능 하도록 요청하는 것을 의미합니다.

[SwiftUI] TabBar Hidden

TabBar Hidden

SwiftUI를 이용해서 TabBar를 Hidden 시키는 경우 발생했던 버그를 공유해보고자 한다.

SwiftUI 3.0 기준으로 TabBar Hidden을 SwiftUI 자체적으로 지원해주지 않는다

(SwiftUI 4.0은 지원해준다. 애플놈들아..)

import SwiftUI
import UIKit

struct TabBarAccessor: UIViewControllerRepresentable {
  var callback: (UITabBar) -> Void
  private let proxyController = ProxyViewController()

  func makeUIViewController(context: UIViewControllerRepresentableContext<TabBarAccessor>) -> UIViewController {
    proxyController.callback = callback
    return proxyController
  }

  func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<TabBarAccessor>) {
  }

  // 탭바를 가져오기 위한 헬퍼 뷰컨
  private class ProxyViewController: UIViewController {
    var callback: (UITabBar) -> Void = { _ in }

    override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)

      if let tabBarController = self.tabBarController {
        callback(tabBarController.tabBar)
      }
    }
  }
}

extension View {
  func setTabBarVisibility(isHidden: Bool) -> some View {
    background(TabBarAccessor(callback: { tabBar in
      tabBar.isHidden = isHidden
      tabBar.layoutIfNeeded()
    }))
  }
}

SwiftUI에서 자체적으로 지원해주지 않기 때문에 UIViewController를 래핑시켜서 UIKit 기반으로 ViewWillAppear 시점에 해당 뷰컨에서 tabBar를 찾아서 클로저형태로 넘겨주도록 구성하고 ViewModifier를 통해 해당 tabBar를 hidden 시킬 수 있도록 하는 코드를 찾아서 사용하고 있다.

그래서 TabBar가 존재하는 RootView에서 push 되는 경우, 새롭게 나타날 화면에서 TabBar를 hidden 해서 사용하고 있다.

Simulator Screen Recording - iPhone 14 Pro - 2023-01-21 at 14 10 04

정상적으로 잘 동작하는 것 처럼 보였지만 위의 버그를 발견하고야 말았다…

push 된 화면의 최하단에 View를 그리면 TabBar는 보이지 않지만 기존의 TabBar만큼의 높이를 먹고 있다.

그리고 심지어 이 상태에서 background, inActive → active 상태로 전환이 발생하면 해당 높이가 사라진다.


background, inActive → active 상태로 변경될 때, SwiftUI View에서 정확하게 어떤 일이 발생하는지 알아보지는 않았지만 간단하게 야매(?)로 해결법을 찾아 정리해놓고자 한다.

Simulator Screen Recording - iPhone 14 Pro - 2023-01-29 at 21 48 36

영상을 통해서 어떤 방법을 이용했는지 짐작할 수 있을것 같은데 해결법은 bottom의 safeArea를 무시하는 것이다!

(심지어 위 영상에는 나와있지 않지만 Bottom SafeArea가 존재하지 않는 노치가 없는 모델에서도 동일한 문제가 발생하는데 동일하게 해결가능하다)

  • SafeArea를 무시하면 이전과 같이 active 상태로 변경되었을 때, TabBar영역의 높이가 사라지는 현상이 발생하지 않는것을 확인했다.
  • 앱이 켜지는 시점에 SafeArea Bottom Height를 EnvironmentValue로 저장시킨다.
  • 저장한 SafeArea Bottom Height 만큼 padding을 추가적으로 준다
  • TabBar가 Hidden되는 View에서 기존의 TabBar Hidden에 ignoreSafeArea(.bottom)을 추가한다.

여기서 주의할 점은 ViewModifier는 순서에 영향을 받기 때문에

safeArea Bottom height 만큼 padding을 주고 난 이후에 Tabbar를 hidden 시키면서 safeArea를 무시해야 한다!

import SwiftUI

extension UIApplication {
  var keyWindow: UIWindow? {
    connectedScenes
      .compactMap {
        $0 as? UIWindowScene
      }
      .flatMap {
        $0.windows
      }
      .first {
        $0.isKeyWindow
      }
  }
}

private extension UIEdgeInsets {
  var swiftUiInsets: EdgeInsets {
    EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
  }
}

private struct SafeAreaBottomKey: EnvironmentKey {
  static var defaultValue: CGFloat {
    UIApplication.shared.keyWindow?.safeAreaInsets.swiftUiInsets.bottom ?? 0.0
  }
}

extension EnvironmentValues {
  var safeAreaBottom: CGFloat {
    self[SafeAreaBottomKey.self]
  }
}
struct FriendDetailView: View {
  @Environment(\.safeAreaBottom)
  var safeAreaBottom

  var body: some View {
    VStack {
      Text("에러 케이스")
        .font(.title2)

      Spacer()

      Button(
        action: { },
        label: {
          Text("버튼의 위치가?")
            .font(.body)
        }
      )
    }
    .padding(.bottom, safeAreaBottom)
    .setTabBarVisibility(isHidden: true)
  }
}

Simulator Screen Recording - iPhone 14 Pro - 2023-01-29 at 22 25 48

정상동작 하는 것을 확인할 수 있다.
전체코드는 여기에서 확인 할 수 있습니다.

근본적인 해결책보다는 야매로 어떻게든 해결하는 방법을 찾았는데 해결방법을 아시는 분은 [email protected]으로 연락 주시면
정말 너무 감사하겠습니다

[weak self]

weak self 에 대하여 - 1

간략하고 이해한 내용 위주로 간단하게 정리하고 주말에 예제코드와 함께 더 상세히 작성해보겠습니다!

참고하면 좋을 WWDC21 참고하기
참고하면 좋을 블로그

Swift는 ARC를 통해서 객체에 대한 참조 카운트를 관리하고 0이 되면 자동으로 메모리를 해제 해주는 기능을 가지고 있다.
하지만 두개 이상의 객체가 서로에 대한 강한 참조를 가지면 순환참조가 발생하게 돼서 메모리 누수 현상이 일어나게 됩니다.

이런 현상을 막기 위해서 우리는 weak를 사용합니다.

일반적으로 탈출 클로저에서 [weak self]를 자주 접하게 됩니다. 탈출 클로저에서 값을 캡처하는 과정에 명시적으로 self를 작성해줘야 하는데 이때 self에 대한 순환참조가 발생할 수 있기 때문입니다.

HTTP 웹 기본 지식 - 7

HTTP 헤더 - 1

  • field-name: field-value (띄워쓰기 허용)
  • HTTP 전송에 필요한 모든 부가정보
  • 필요시 임의의 헤더 추가 가능
    • 유저 기기에 대한 정보를 헤더에 실어서 통신 했던 경험

HTTP 헤더 분류

RFC2616(과거)

  • General 헤더: 메시지 전체에 적용되는 정보
  • Request 헤더: 요청 정보
  • Response 헤더: 응답 정보
  • Entity 헤더: 엔티디 바디 정보
HTTP/1.1 200 OK // 시작라인
Content-Type: text/html;charset=UTF-8 // 엔티디 헤더
Content-Length: 3423

<html> // 엔티디 본문
...
</html>

HTTP 메시지 구조를 공부하면서 살펴봤듯이 시작라인, 헤더, 공백, 본문 구조로 구성되어 있다.

엔티디 헤더는 메시지 본문을 통해 전달하는 데이터를 해석할 수 있는 정보들을 제공한다.

RFC723x

2014년 이후로 HTTP 표준이 바뀌면서 위의 구조에서 Entity가 Representation으로 대체되는 큰 차이점이 발생했다. (완전히 1:1 대응되는 개념은 아님)

사실 둘의 정확한 차이점을 파악할 정도로 깊게 공부하지는 못했지만 간단하게 정리해보자면

Entity는 클라이언트와 서버간 주고 받는 데이터의 본질에 더 가까운 느낌인데 사실 해당 데이터를 어떻게 표현해서 주고 받을지 서로 약속하고 각자의 내부적은 로직을 통해 해당 데이터를 처리하면 되기 떄문에 클라이언트 서버간 데이터를 주고 받을 때 내부적으로 각자 해당 데이터를 어떤 형식으로 저장하고 있건 json 타입으로 주고 받는다는 약속을 한다. 정도의 느낌으로 받아들였는데 해당 내용은 좀 더 공부하다가 알게되는 내용이 있으면 정리해야겠다.

HTTP/1.1 200 OK // 시작라인
Content-Type: text/html;charset=UTF-8 // 표현 헤더
Content-Length: 3423

<html> // 표현 데이터
...
</html>

표현헤더

  • Content-Type: 표현 데이터의 형식

HTTP 메서드 활용을 정리하면서 정리했던 부분은 간단하게 다시 짚고 넘어가보자.

Content-Type은 HTTP 요청 또는 응답의 본문(content)의 표현 데이터 형식을 설명하고 MIME 타입을 이용해 미디어 타입을 명시한다.

  • Content-Encoding: 표현 데이터 인코딩

표현 데이터를 압축하기 위해서 사용

데이터를 읽는 쪽에서 인코딩 헤더의 정보를 이용해서 압축 해제 [ex) gzip, deflate, identity]

  • Content-Language: 표현 데이터의 자연언어

표현 데이터의 자연언어를 표현 [ex) ko, en, en-US]

  • Content-Length: 표현 데이터의 길이

바이트 단위, 전송 코딩을 사용하면 Cotent-Length를 사용하면 안됨 → 조금만 내리시면 해당 내용 정리


협상

클라이언트가 선호하는 표현 요청이라는 문장만 보면 정확하게 이해하기 힘들 수 있는데 예제를 들어서 같이 이해해보자

Untitled Untitled 1

위의 그림처럼 다국어를 지원하는 서버에서 클라이언트에 응답을 줄 때 요청시 협상 헤더를 사용하지 않으면 기본으로 지원하는 영어로 응답을 받게된다.

협상과 우선순위

  • Quality Values가 높을수록 우선순위
Untitled 2
  • 구체적일수록 우선순위
Accept: text/*, text/plain, text/plain;format=flowed, **/*

1)* text/plain;format=flowed
2) text/plain
3) text/*
4) **/**
q값을 따로 명시하지 않아도 구체적인 값이 우선순위를 가진다.

클라이언트가 서버로 받을 응답 값을 때 선호하는 형식을 지정하는 것이기 때문에 요청시에만 사용된다.


전송방식

  • 단순전송
    • Content에 대한 길이를 알고 있을 때 사용 한번에 요청하고 한번에 받음
  • 압축전송
    • Content-Encoding을 통해서 어떻게 압축되었는지를 표현해야한다.
  • 분할전송
    • Content-Length를 보내면 안된다.
Untitled 3
  • 범위전송
    • 범위를 지정해서 요청(이미지 같이 큰 파일을 받을 때 중간부터 다시 받는 경우 생각)

일반정보

  • Form
    • 유저 에이전트의 이메일 정보
  • Referer
    • 현재 요청된 페이지의 이전 웨 페이지 주소
    • 유입 경로를 분석할 수 있음
  • User-Agent
    • 클라이언트의 애플리케이션 정보 (서버로그에 Postman, Alamofire까지 찍히던거 생각)
    • 어떤 종류의 브라우저에서 장애가 발생하는지 파악 가능
  • Server
    • 요청을 처리하는 ORIGIN 서버의 소프트웨어 정보
  • Date
    • 메시지가 발생한 날짜와 시간

특별한 정보

  • Host
    • 요청한 호스트 정보
    • 하나의 IP주소에 여러 도메인이 적용되어 있을 수 있기 때문에 필수
    • 요청에서 사용
  • Location
    • 30x 응답 결과를 통해 리다익레트 시키는 경우 사용
    • 201에서 리소스 생성 위치를 알려줄 떄 사용
  • Retry-After
    • 유저 에이전트가 다음 요청을 하기전까지 기다려야 하는 시간
  • Authorization
    • 클라이언트 인증 정보를 서버에 전달할 때 사용
    • 401에서 유저 인증 실패시 WWW-Authenticate로 인증 방법에 대해 알려줌

쿠키

쿠키는 서버가 클라이언트에게 보내는 작은 데이터 조각

쿠키에 대해 이해하기 위해서는 쿠키를 왜 사용하는지를 먼저 이해할 필요가 있다.

HTTP 특성을 다시 들여다 보면

  • 클라이언트 - 서버 구조
  • 무상태, 비연결성

무상태(Stateless), 서버는 클라이언트의 상태를 저장하고 있지 않기 때문에 클라이언트는 이전 상태에 대한 정보를 함께 서버에 보내야 하는 불편함이 있었지만 스케일 아웃이 쉬워진다고 정리한바 있다.

그럼 이전 상태에 대한 정보를 서버에 넘기는 방법에는 무엇이 있을까?

간단하게 모든 요청에 이전 상태 정보를 넘길 수 있다. 하지만 웹 브라우저를 닫은 경우에는 어떻게 처리해야 할까?

이런 문제들을 간단하게 해결하기 위해 나온것이 쿠키이다.

Untitled 4

먼저 서버에서 클라이언트로 쿠키를 전달한다.

Untitled 5

이후 클라이언트에서 서버로 요청을 보낼 때, 쿠키에 있는 정보를 포함해서 보낸다.

쿠키 - 생명주기

그럼 웹 브라우저가 닫히면 쿠키는 어떻게 될까? 또 클라이언트에 저장된 쿠키는 언제까지 저장되는걸까?

서버에서 처음 클라이언트에게 쿠키를 보내줄 때 쿠키의 만료일을 설정할 수 있다.

Set-Cookie: expire=Sat,26-Dec-2020 04:39:23 GMT // 만료일이 되면 삭제
Set-Cookie: max-age=3600 // 3600초 이후 삭제
  • 세션 쿠키: 만료날짜를 생략하면 브라우저가 종료될 때 까지 유지
  • 영속 쿠키: 만료날짜를 입력하면 해당 날짜까지 유지

서버에서 만료일을 설정한다고 해서 클라이언트에서 쿠키 만료일이 되면 어떻게 자동으로 삭제할까?라는 의문이 들어서 검색했는데 브라우저의 쿠키 정책에 따라서 다를 수 있다고 한다.

쿠키 - 도메인, 경로

클라이언트에 저장된 쿠키 정보는 항상 서버에 전송되는데 현재 웹 사이트가 아닌 다른 사이트에서 받아 저장한 쿠키도 함께 전송되는걸까? (당연히 그러면 안되겠죠?)

domain=example.org // 문서 기준 도메인, 서브 도메인 포함 -> dev.example.org
domain(empty) // 해당 도메인에서만 접근 가능

도메인값을 적어주지 않더라도 기본적으로 해당 쿠키를 보내준 도메인에 요청을 보낼 때만 해당 쿠키를 사용한다.

쿠키 - 보안

항상 서버에 전송되는 값이기 때문에 보안에 취약하다. 최소한의 정보만 보내야 한다.

  • Secure를 적용하면 https인 경우에만 전송
  • HttpOnly: HTTP 전송에만 사용
  • SameSite: 요청 도메인과 쿠키에 설정된 도메인이 같은 경우에만 쿠키 전송

인터럽트

관련된 내용은 아니지만 학습하다 이 영상이 되게 유용했던거 같아서 공유해드립니다.

인터럽트

CPU가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치나 예외상황이 발생하여 처리가 필요한 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것

마이크로프로세서(MPU) : 컴퓨터의 핵심 기능인 기계어를 해석하고, 연산을 수행하는 기능만 가지고 있는 프로세서(주변장치가 있어야 동작/ 요즘에는 CPU와 동일한 의미로 사용하기도 합니다.)

스크린샷 2022-03-24 오후 5 52 20

그럼 위에 정의를 좀 풀어서 설명해보면 CPU가 일을 하고 있는데 다른 장치에서 예외상황이 발생해 작업이 필요한 순간 인터럽트를 발생 시켜서 CPU에게 그 일을 시킨다. 그 정도 의미로 생각할 수 있습니다.

그럼 왜 굳이 인터럽트를 발생시켜서 CPU한테 일을 시킬까요? -> CPU이 연산속도가 더 빠릅니다


인터럽트는 크게 2가지로 나눌 수 있습니다.

  • 하드웨어 인터럽트 : 키보드, 마우스와 같은 하드웨어가 발생시키는 인터럽트
  • 소프트웨어 인터럽트 : 프로그램 오류로 인해 예외상황이 발생 하거나 프로그램이 커널함수 사용을 위해 호출하는 System call 이 발생하는 경우
  • 예를 들면, 내부적인 타이머가 존재해서 5초마다 어떤 특정한 동작을 실행하는 경우 5초마다 소프트웨어 인터럽트를 발생시켜서 어떤 특정한 로직을 처리하도록 설정할 수 있습니다.
  • 어떤 특정한 하드웨어 버튼을 누를 때 마다 특정한 동작이 일어나도록 하고 싶을 경우 버튼을 누르면 하드웨어 인터럽트를 발생시켜서 특정 동작을 실행하도록 할 수 있습니다.

인터럽트 처리과정에 대해서 확인해보겠습니다.

1) 현재 실행중인 명령의 메모리 주소를 포함한 부가 정보를 저장한다

인터럽트가 발생하면 인터럽트 서비스를 실행시켜야 하기 때문에 현재 CPU에서 작업중이였던 정보를 저장하는 과정이 필요합니다. 이때 PCB를 통해서 현재 실행중이던 코드의 메모리 주소, 레지스터값, 하드웨어 상태를 저장합니다.

2) 인터럽트 처리(ISR)

운영체제는 미리 인터럽트 벡터를 가지고 있는데 인터럽트 벡터를 따라가면 실제 처리해야 할 코드는 인터럽트 핸들러라고 불리는 곳에 정의 되어 있고 이를 처리합니다.
학부시절 기억을 떠올려보면 인터럽트에 해당하는 주소값이 이미 설계할 때 미리 정해져있었던걸로 기억하는데 그 부분을 인터럽트 벡터라고 하는거 같습니다

3) 인터럽트 전으로 복원

인터럽트에서 요구하는 작업을 모두 완료하면 원래 CPU가 하던 작업을 이어서 하는 과정을 의미합니다.(아까 PCB에 저장해놨으니까 돌아올 수 있습니다.)


시스템콜에 대한 부분을 공부하면 인터럽트와의 차이점에 대해서 추가적으로 정리하도록 하겠습니다.

[SwiftUI] List Item을 NavigationLink로 만들 경우

LazyVStack - NavigationLink

LazyVstack안에 NavigationLink가 있는 경우 발생한 버그에 대해 정리해보고자 한다.

Simulator Screen Recording - iPhone 14 Pro - 2023-01-20 at 20 37 32

현재 사용자가 입력한 결과값에 따라 Media List를 받아오고 각 Cell을 클릭하면 DetailView로 Push 되고 있는 형태의 앱이다.

struct MediaListView: View {
  @StateObject
  var viewModel: MediaListViewModel

  let appContainer: AppContainer

  var body: some View {
    NavigationView {
      ScrollView {
        LazyVStack {
          ForEach(viewModel.output.medias, id: \.id) { media in
            NavigationLink(
              destination: {
                NavigationLazyView(
                  MediaDetailView(
                    viewModel: appContainer.mediaDetailViewModel(media)
                  )
                )
              },
              label: {
                MediaListItemView(media: media)
              }
            )
          }
        }
        .padding(.top, 20)
        .padding([.leading, .trailing], 12)
      }
      .navigationTitle("TV Search")
      .navigationBarTitleDisplayMode(.inline)
      .searchable(text: $viewModel.input.searchMediaSub.value)
    }
  }
}

SwiftUI의 NavigationLink는 사용자가 push하기 이전뷰에서도 push될 뷰를 미리 load하고 있는 문제점(?)이 있는데 이를 방지하고자 push되는 시점에서 뷰가 그려질 수 있도록 NavigaitonLazyView를 사용하고 있다.

하지만 위와 같이 코드를 작성하고, 기존에 리스트에 있는 cell을 클릭하는 시점에 사용자가 키보드를 통해 다른 값을 입력하면 자동으로 navigation pop이 되는 문제점이 있다.

입력값이 변화함에 따라서 기존의 list가 가지고 있던 cell이 사라지면서 navigationLink도 사라져 버린것이다!

이런 문제를 어떻게 해결할 수 있을까?

이런 문제가 발생하는 이유는 list의 cell이 navigationLink라는 View로 만들어졌기 때문이다. 따라서 문제를 해결하기 위해서 List의 cell Item 자체가 navigationLink가 되어서는 안된다.

import SwiftUI

struct MediaListView: View {
  @StateObject
  var viewModel: MediaListViewModel

  let appContainer: AppContainer

  var body: some View {
    NavigationView {
      ScrollView {
        LazyVStack {
          // Empty View
          NavigationLink(
            isActive: $viewModel.output.isNavigationShow,
            destination: {
              if let media = viewModel.output.selectedMedia {
                NavigationLazyView(
                  MediaDetailView(
                    viewModel: appContainer.mediaDetailViewModel(media)
                  )
                )
              }
            },
            label: { }
          )
          // List Cell Item
          ForEach(viewModel.output.medias, id: \.id) { media in
            MediaListItemView(media: media)
              .wrapToButton { viewModel.action(.navigationTapped(media)) }
          }
        }
        .padding(.top, 20)
        .padding([.leading, .trailing], 12)
      }
      .navigationTitle("TV Search")
      .navigationBarTitleDisplayMode(.inline)
      .searchable(text: $viewModel.input.searchMediaSub.value)
    }
  }
}

해결방법은 간단하다. List외부에 Empty View Label을 가지는 NavigationLink를 만들고, 해당 NavigationLink를 통해서 화면전환을 하면된다.

코드가 변경된 부분을 보자. 기존에는 List의 Item 자체가 NavigationLink였지만 단순히 Button 기능을 하는 버튼으로 변경된 것을 확인할 수 있다.

import Combine
import Foundation

final class MediaListViewModel: ViewModelType {
  private let searchMediaUsecase: SearchMediaUseCase

  struct Input {
    var searchMediaSub = BindingSubject<String>(value: "")
    fileprivate let searchButtonSub = PassthroughSubject<Void, Never>()
    fileprivate let navigationSub = PassthroughSubject<Media, Never>()
  }

  enum Action {
    case searchButtonTapped
    case navigationTapped(Media)
  }

  func action(_ action: Action) {
    switch action {
    case .searchButtonTapped:
      input.searchButtonSub.send()
    case .navigationTapped(let media):
      input.navigationSub.send(media)
    }
  }

  struct Output {
    var medias: [Media] = []
    var isNavigationShow: Bool = false
    var selectedMedia: Media?
  }

  var input = Input()
  @Published var output = Output()
  var cancellables = Set<AnyCancellable>()

  init(searchMediaUsecase: SearchMediaUseCase) {
    self.searchMediaUsecase = searchMediaUsecase
    transform()
  }

  func transform() {
    input.searchMediaSub.subject
      .debounce(for: 0.5, scheduler: RunLoop.main)
      .flatMap { [weak self] search -> AnyPublisher<MediaPage, Never> in
        guard let self = self else {
          return Just(MediaPage(page: -1, totalPages: -1, medias: [])).eraseToAnyPublisher()
        }
        return self.searchMediaUsecase.tvExcute(query: search, page: 1)
          .replaceError(with: MediaPage(page: -1, totalPages: -1, medias: []))
          .eraseToAnyPublisher()
      }
      .map { $0.medias }
      .sink(receiveValue: { [weak self] in
        guard let self = self else { return }
        self.output.medias = $0
      })
      .store(in: &cancellables)

    input.navigationSub
      .sink(receiveValue: { [weak self] in
        guard let self = self else { return }
        self.output.selectedMedia = $0
        self.output.isNavigationShow = true
      })
      .store(in: &cancellables)
  }
}

뷰모델의 프레젠테이션 로직을 확인해보면, 사용자가 DetailView를 보기 위해서 버튼을 누르면 해당 버튼을 그릴 때 사용되었던 Media를 subject를 통해 넘겨주고 navigationLink의 isActive를 변경한다.
Simulator Screen Recording - iPhone 14 Pro - 2023-01-20 at 21 17 30

문제가 해결된것을 확인할 수 있다.

전체 프로젝트는 여기서 확인할 수 있습니다

대칭키와 비대칭키

대칭키 암호화

Untitled

암호화와 복호화에 같은 같은 키를 사용하는 알고리즘을 의미한다.

즉, 클라이언트에서 대칭키 알고리즘을 통해서 Request Body를 암호화 시켜서 보내게 되는 경우 서버에서 해당 내용을 복호화 하기 위해서는 클라이언트에서 암호화하는데 사용했던 키를 서버에도 보내줘야 한다

그럼 머리속에서 이런 의문이 자연스럽게 떠오를 수 있다.

  • 클라이언트에서 그럼 서버로 대칭키는 어떻게 보내줘야 하는 걸까?
  • 보안을 위해서 Request Body를 암호화 했는데 복호화를 위한 키를 아무런 보안 없이 보내면 암호화는 의미가 없어질 수 있지 않을까?

실제로 대칭키를 암호화를 사용할 때, 대칭키를 안전하게 전달하는 것이 가장 중요한 부분이다.

실제 보안에 대해 조예가 깊지 않아 대칭키를 어떻게 안전하게 전달하지에 대한 다양한 방법에 대해서는 알지 못하지만 암호화-복호화할 내용을 대칭키 암호화를 이용하고 대칭키 암호화를 다시 비대칭키 암호화를 통해서 서버에 보내는 방식을 사용했다.

이러한 보안상 단점에도 대칭키 암호화를 사용하는 이유는 비대칭키 암호화에 비해 연산 속도가 빠르다

대표적인 대칭키 알고리즘으로는

  • DES
  • AES
  • SEED

등등이 있고 AES 알고리즘 구현에 대해 정리해볼 예정이다.


비대칭키 암호화(공개키)

Untitled 1

비대칭키 암호화는 암호화와 복호화에 서로 다른 키를 사용하는 알고리즘을 의미한다

위의 그림을 예시로 들면

  • 서버에서 암호화 복호화를 위한 Key Pair를 생성한다.
  • 개인키는 서버에 저장되고 클라이언트에 공개키를 제공한다
  • 클라이언트에서 공개키를 통해 Request Body를 암호화 한다
  • 서버는 암호화된 데이터를 개인키를 통해서 복호화한다

공개키를 통해 암호화된 데이터는 개인키를 통해서만 복호화 할 수 있기 때문에 위와 같은 과정이 가능하다

그럼 개인키를 통해서 암호화하고 공개키를 통해서 복호화를 할 수는 없을까? 라는 의문이 들 수 있다.

공개키는 누구나 얻을 수 있기 때문에 누구나 암호화 된 데이터를 복호화 할 수 있개 때문에 보안적인 측면에서는 권장되지 않지만 암호화 된 데이터를 복호화 했을 때 이전 데이터와 같은지만 확인하면 되는 디지털 서명에서 사용된다.

  • A는 개인키로 서명하려는 메시지를 암호화하고 원본과 서명 데이터를 보낸다
  • B는 공개키를 통해서 해당 메시지를 복호화 한다
  • 복호화한 데이터와 원본 데이터가 일치하는지 확인한다

B입장에서 공개키를 통해서 복호화가 성공적으로 이루어지면 A가 해당 내용을 보냈음이 보장되고 복호화 한 데이터와 원본이 같으면 데이터에 위조가 없음이 보장된다.

앞서 살펴본 대칭키에 비해 보안적으로 장점이 있지만 연산량이 많아 느리다는 단점이 있다.

대표적인 알고리즘으로

  • RSA
  • DSA, ECDSA

등등이 있고 RSA 알고리즘을 통한 암호화-복호화 과정, ECDSA를 통한 서명, 서명검증 과정을 구현해볼 예정이다.

클로저 - 02

클로저를 이용한 저장 프로퍼티 초기화

let testLabel: UILabel = {
    let label = UILabel()
    label.text = "Lunch Screen"
    label.font = .systemFont(ofSize: 30, weight: .bold)
    label.textColor = .black
    return label
  }()

UIKit에서 뷰를 코드로 짜보신 분들은 아마 위와 같은 형식으로 뷰 컴포넌트들을 만든 코드를 많이 보셨을겁니다. (저는 사실 스유만 해서 잘 모릅니다 헤헿)

  • 왜 저렇게 UILabel 타입의 인스턴스가 생성될 수 있는지
  • 위의 방식과 연산 프로퍼티의 차이점이 무엇인지

에 대해서 정리해볼 예정입니다.


1) 클로저를 이용한 저장 프로퍼티 초기화

위의 코드를 통해서 어떻게 UILabel 인스턴스가 생성되는지 바로 이해하셨나요? 질문을 바꿔서 ()가 있어야 하는 이유를 정확히 파악하셨나요?

아마 클로저에 대해 제대로 학습 하셨던 분들은 왜 질문에 대답하시는데 큰 어려움이 없었을 것 같습니다.

하나씩 뜯어 보겠습니다.

// 1)	
	{
    let label = UILabel()
    label.text = "Lunch Screen"
    label.font = .systemFont(ofSize: 30, weight: .bold)
    label.textColor = .black
    return label
  }

우선 위의 저 { } 로 감싸진 1번 코드뭉치(?)의 타입이 무엇 일까요?

// 2)
let b = { () -> UILabel in
    let label = UILabel()
    label.text = "Lunch Screen"
    label.font = .systemFont(ofSize: 30, weight: .bold)
    label.textColor = .black
    return label
}

아마 2번 코드를 보는 순간 1번 코드 뭉치의 타입이 () → UILabel 타입의 클로저임을 눈치 채셨을겁니다.

틀리셨다고 해도 전혀 걱정하실 필요가 없습니다. 사실 1번 코드는 Swift도 타입추론을 못하는 코드니까요 ㅎㅎ

왜 1번 코드를 Swift가 타입추론에 실패하는지는 마지막에 따로 정리하기로 하고 하던 정리를 이어가봅시다.

let testLabel: UILabel = {
    let label = UILabel()
    label.text = "Lunch Screen"
    label.font = .systemFont(ofSize: 30, weight: .bold)
    label.textColor = .black
    return label
  }()

이제 ()가 붙어야만 하는 이유를 아시겠나요? () → UILabel 타입의 클로저를 실행시킨 결과값을 testLabel에 넣어주고 있는 거죠!!

그럼 클로저를 통해서 연산된 결과를 이용해서 프로퍼티를 초기화 시키고 있는데 연산 프로퍼티와의 차이점은 무엇일까요?


2) 연산 프로퍼티와의 차이

차이점은 연산의 횟수에 있습니다. 연산 프로퍼티는 호출될 때마다 연산된 결과를 반환하는 반면 클로저를 통해 초기화된 저장 프로퍼티는 최초 초기화 시점에 한번 연산되고 이후에는 저장된 값을 사용하는 차이가 있습니다.


let a = { 
    let label = UILabel()
    label.text = "Lunch Screen"
    label.font = .systemFont(ofSize: 30, weight: .bold)
    label.textColor = .black
    return label
}

Swift는 a 의 타입을 추론하지 못합니다. (컴파일 하시면 타입 에러가 발생합니다)

클로저의 인자가 없고 반환타입을 알 수 있는데 왜 () → UILabel이라는 타입을 추론하지 못할까요?

UILabel의 인스턴스를 클로저 내부에서 return하고 있기 때문입니다. 클로저 내부의 독립적인 지역변수의 label을 외부에서 접근할 수 없기 때문에 클로저 외부의 입장에서는 타입 추론을 할 수 없습니다!!

동일한 이유로

let testLabel: UILabel = {
    let label = UILabel()
    label.text = "Lunch Screen"
    label.font = .systemFont(ofSize: 30, weight: .bold)
    label.textColor = .black
    return label
  }()

에서도 UILabel이라는 타입을 명시해줘야 합니다!!


참고 블로그

[[스위프트(Swift) 프로그래밍] - Closure를 이용해 저장 프로퍼티 초기화할 때 들었던 의문점들](https://jayb-log.tistory.com/259)

[Why can't the Swift compiler infer this closure's type?](https://stackoverflow.com/questions/42534207/why-cant-the-swift-compiler-infer-this-closures-type)

ECDSA - 2

Signature 생성

비대칭키 암호화 알고리즘을 통해서 암호화-복호화, 서명 생성 및 검증 두 가지를 수행할 수 있다

지난번 대칭키와 비대칭키의 차이점에 대해 정리하면서 서명 생성 및 검증에 대해 간단하게 정리했는데 해당 내용을 구현하면서 더 자세하게 정리해보자

  • 서명하려는 메시지를 해시함수를 적용해 특정 길이의 데이터로 변경한다
  • Private Key를 통해서 해시된 메시지를 암호화 한다

지난번에도 설명했었지만 일반적으로 Public Key를 통해서 암호화, Private Key를 통해서 복호화를 진행하지만 서명 생성 및 검증과정에서는 반대다.

  // MARK: - Signature Base64
	/// Representation은 현재 상황에 맞게 선택
	/// 어떤 타입의 String을 생성할지는 상황에 맞게 base64 or Hex String
  func makeSignatureBase64(
    plainString: String
  ) -> String? {
    if let data = plainString.data(using: .utf8),
       let sig = try? privateKey.signature(for: SHA256.hash(data: data)) {
      return sig.Representation.base64EncodedString()
    } else {
      return nil
    }
  }

코드를 확인해보면 서명하려는 메시지를 데이터 타입으로 변경하고, 해시 함수를 적용한 뒤 서명을 만들어 내는 것을 확인할 수 있다.

(Swift에서 Hash 함수는 256, 384, 512 세 가지를 제공하고 따로 256이 디폴트로 설정되어 있다)

클라이언트에서 신경써줄 부분은 서명을 Validation 하는 곳에서

  • 몇 bit 짜리 해시 함수를 사용하고 있는가?
  • 서명된 데이터를 base64 String, hexString중 어떤 타입을 사용하고 있는가?

정도를 신경써주면 된다.


Signature 검증

서명을 검증 하는 과정 자체는

  • Private Key를 통해 암호화된 서명을 Public Key를 통해 복호화 되는지?
  • 복호화 된 서명이 원본과 동일한지?

두 가지로 굉장히 간단하고 심지어 P256.PublicKey에 메서드로 이미 구현되어 있다.

우리에게 문제는

  • 서명을 보내는 사람의 Public Key
  • 서명 내용
  • 암호화 된 서명

서버로 부터 받아온 위의 세 가지를 Swift의 P256.PublicKey, P256.Signing.ECDSASignature 타입으로 어떻게 변환해야 하는가?이다. 이는 상황마다 다를것 같은데 서버에서 보내는 주는 타입을 Data 타입으로 변경시키고 이를 이용해 PublicKey, Signing.ECDSASignature 타입으로 초기화 하면 된다.

func validate(
    publicKey: String,
    plainString: String,
    sig: String
  ) -> Bool {
  // sigType: String to P256.Signing.ECDSASignature Type
  // pubType: String to P256.Signing.PublicKey Type
  let digest = SHA256.hash(data: plainData)
  P256.Signing.PublicKey.isValidSignature(sigType, for: digest)
}

ECDSA를 통한 키 생성, 서명 생성 및 검증을 구현해보았는데 사실 정리하고 보면 굉장히 간단한데 익숙하지 않은 내용이라 시간이 좀 걸렸다.

사실 정리한 내용이외에 비대칭키의 Private Key를 iOS에서 어디에 저장할 것인가? 에 대한 고민을 많이 했었는데 키체인 만한 곳이 없는것 같다. 키 저장 관련에 대해서는 우선 암호화 알고리즘을 모두 정리하고 난 이후에 정리할 예정이다.

이제 다음에는 RSA를 통한 암호화-복호화과정에 대해 정리하고, AES를 통한 암호화-복호화를 정리할 예정이다.

레퍼런스

애플 공식문서 - 1
애플 공식문서 - 2

HTTP 웹 기본 지식 - 3

HTTP

TCP/IP 와 HTTP

지난 시간까지 TCP/IP, UDP와 같은 데이터 전송 프로토콜에 대해서 공부했는데 HTTP에 대한 내용을 정리하기 이전에 둘의 차이를 명확히 짚고 넘어가고자 한다.

결론부터 말하자면 HTTP는 TCP/IP 보다 상위 개념이다.

Untitled
  • 응용계층
    • HTTP, FTP, SMTP등 네트워크를 사용하는 응용 프로그램
  • 전송계층
    • TCP, UDP등 시스템을 연결하고 데이터를 전송
  • 인터넷계층
    • IP 데이터를 정의하고 데이터 경로를 라우팅
  • 물리계층
    • 하드웨어 네트워크

TCP/IP 4계층 관점에서 바라보면 HTTP는 응용계층에 속하고 TCP,UDP는 전송계층, IP는 인터넷 계층에 속해있는것을 확인할 수 있다.

  1. HTTP 계층에서 HTTP 메시지를 작성
  2. TCP 계층에서 HTTP 메시지를 패킷으로 분해
  3. IP계층에서 전송위치를 확인
  4. 네트워크를 통하여 전송

HTTP의 기반 프로토콜이 TCP라는 의미는 위와 같이 HTTP 통신의 일련의 과정에 TCP를 사용한다는 의미라고 이해했다. (물론 UDP를 사용할 수도 있다)

→ 해당 내용 관련해서는 공부하면서 계속 추가할 예정입니다.


HTTP(=Hypter Text Transfer Protocol)

인터넷에서 데이터를 주고받는 프로토콜로 거의 모든 형태의 데이터를 주고 받을 수 있고 Web에서는 대부분 HTTP 프로토콜을 사용하고 있다.

HTTP 버전

  • HTTP 1.1 → 가장 많이 사용되는 버전이고 TCP를 기반으로 하고 있음
  • HTTP 2.0 → 1.1버전에서 성능 개선
  • HTTP 3.0 → TCP가 아닌 UDP를 이용해서 속도 개선(hand shake 과정 x)

(구글에서 F12를 통해서 HTTP 버전을 확인할 수 있음)

HTTP 특징

  • 클라이언트 - 서버 구조
  • 무상태(=Stateless), 비연결성
  • HTTP 메시지

간단하게 정리하면 위의 내용이고 HTTP의 특징에 대해서 차례대로 하나씩 정리해보자.


클라이언트 - 서버 구조

  • 클라이언트에서 서버에 요청을 보내고 대기
  • 서버가 요청에 해당하는 결과값을 응답

서버에서 앱의 비즈니스 로직을 처리하고 클라이언트(앱)는 결과값에 따라 UI를 그려준다.

클라이언트-서버 구조가 고도화 되면 위와 같이 클라이언트와 서버의 역할 분리를 통해서 독립적으로 발전할 수 있다. (심지어 모바일은 웹과 달리 구글이나 애플의 심사과정도 있기 때문에 비즈니스 로직을 서버에서 처리해주면 문제가 발생하더라도 심사 없이 서버 수정만으로 문제를 해결할 수 있다.)


무상태(=Stateless)

서버가 클라이언트의 상태를 보존하지 않는다. 해당 문장만 보고 이해하기는 어려우니까 예시를 들어서 이해해보자.

Stateful

/// 점원이 바뀌지 않는 경우
고객: 노트북 얼마에요?
점원(A): 380만원이요

고객: 1개 주세요
점원(A): 일시불인가요? 할부인가요?

고객: 36개월 무이자 할부요
점원(A):

점원이 이전 고객의 요청에 대한 상태를 알고 있기 때문에 가능한 대화이다. 만약 점원이 중간중간 바뀐다면 어떻게 될까?

/// 점원이 바뀌는 경우
고객: 노트북 얼마에요?
점원(A): 380만원이요

고객: 1개 주세요
점원(B): ..?1개 구매하시는 건가요?

고객: 36개월 무이자 할부요
점원(C): ...? 무슨 제품 몇개를 36개월 할부로 구매하시는건가요?

점원이 이전 상태들에 대한 정보가 없기 때문에 정상적인 대화가 불가능 하고 이전 상태에들에 대한 정보를 다시 물어야 고객의 요구를 들어줄 수 있다.

Stateless

/// 점원이 바뀌지 않는 경우
고객: 노트북 얼마에요?
점원(A): 380만원이요

고객: 노트북 1개 주세요
점원(A): 일시불인가요? 할부인가요?

고객: 노트북 136개월 무이자 할부요
점원(A):
/// 점원이 바뀌는 경우
고객: 노트북 얼마에요?
점원(A): 380만원이요

고객: 노트북 1개 주세요
점원(B): 일시불인가요? 할부인가요?

고객: 노트북 136개월 무이자 할부요
점원(C):

무상태인 경우에는 중간에 다른 점원으로 바뀌더라도 고객이 이전 상태에 대한 정보과 함께 요청하기 때문에 고객의 요구를 들어 줄 수 있다.

이제 고객과 점원이 아닌 클라이언트와 서버로 생각해보자.

  • 중간에 점원이 바뀌더라도 고객의 요구를 들어줄 수 있다는 것은 클라이언트의 요청을 같은 기능을 하는 서버 중 아무 서버나 응답값을 줄 수 있다는 것을 의미한다.
  • 중간에 서버가 오류가 발생하는 경우에도 중계서버를 통해서 다른 서버로 부터 결과값을 받아오는 것만으로 문제를 해결할 수 있다
  • 스케일 아웃(=수평 확장)이 쉬워진다.

비연결성

HTTP는 요청을 주고 받을 때만 연결을 유지하고 응답을 주고 받은 이후에는 연결이 끊어진다.

  • 서버와의 연결을 계속 유지하지 않기 때문에 서버 자원을 효율적으로 사용할 수 있다
  • but, 요청-응답마다 TCP/IP 연결을 새로 맺어야 한다(handshake 과정 필요)
  • 이런 낭비를 막기 위해서 HTTP 지속 연결을 통해서 문제 해결

HTTP 메시지

Untitled 1 Untitled 2
  • 시작라인
    • 요청
      • method / request target / http version
    • 응답
      • http version/ status code / reason - phrase
  • 헤더
    • fieldname: field value
      • http 전송에 필요한 부가 정보들
  • 바디
    • 실제로 전송할 데이터

클로저 - 03

@autoclosure

아마 Swift로 코드를 많이 접해보신 분들은 자주 사용하는 메서드의 원형을 관찰해보면 @autoclosure라는 attribute를 본적이 있을텐데요. 혹시 어떤 의미를 가지는지도 정확히 알고 계신가요?? (저는 몰랐답니다~)

{ } 없이 표현식만 적어도 클로저를 래핑해주는 기능

자동으로 클로저를 만들어주는 기능이 대체 왜 필요한걸까요❓ 예시를 통해서 한번 이해 해봅시다.

func goodMorning(morning: Bool, whom: String) {
    if morning {
        print("Good Morning, \(whom)")
    }
}

func giveName() -> String {
    print(#function, ": Called")
    return "Bran"
}

goodMorning(morning: true, whom: "신상") // Good Morning, 신상
goodMorning(morning: false, whom: giveName()) // giveName(): Called

매개변수 whom의 타입의 String 타입이기 때문에 String 타입을 반환하는 giveName()이라는 메서드의 반환값을 매개변수로 사용할 수 있습니다.

현재 메서드 구조에서는 whom 매개변수를 실제로 사용하지 않는 시점에도 값을 복사해서 가져오는 문제점이 있습니다. whom이라는 매개변수는 morning이 true인 경우에만 사용하면 되는데 메서드가 호출되는 시점에 인자giveName()이라는 메서드가 실행되는 것을 확인할 수 있죠.

위의 문제를 어떻게 해결할 수 있을까요? 사실 방법이야 다양하게 있겠지만 호출되기 전까지는 내부 코드를 실행시키지 않는 클로저의 특성을 이용하면 쉽게 해결할 수 있습니다. Lazy evaluation

func goodMorning(morning: Bool, whom: () -> String) {
    if morning {
        print("Good Morning, \(whom())")
    }
}

func giveName() -> String {
    print(#function, ": Called")
    return "Bran"
}

goodMorning(morning: true, whom: "신상") // Error
goodMorning(morning: true, whom: {"신상"}) // Good Morning, 신상
goodMorning(morning: false, whom: giveName) // 

morning이 false인 경우에는 () → String 타입의 메서드가 실행되지 않았음을 확인할 수 있습니다.

(=클로저 형태의 파라미터 whom은 인자가 파라미터에 전달되는 시점이 아닌 함수내부에서 호출되는 시점에 실행되게 되었습니다.)

하자만, whom 파라미터의 타입이 명시적인 클로저로 변경되면서 이전의 메서드처럼 String 타입의 상수나 변수를 인로자 넘겨줄 수 없게 되었는데 이때 autoclosure를 사용하면 문제를 해결할 수 있습니다.

func goodMorning3(morning: Bool, whom: @autoclosure () -> String) {
    if morning {
        print("Good Morning, \(whom())")
    }
}

goodMorning3(morning: false, whom: "신상") //
goodMorning3(morning: true, whom: "네카") // Good Morning, 신상
goodMorning3(morning: true, whom: giveName()) // giveName() : Called
																							// Good Morning, Bran

즉 autoclosure를 이용하면 우리가 따로 { } 없이 표현식만 적어도 자동으로 클로저로 래핑해줍니다.


autoclosure를 사용할 때 주의할 점

1) 파라미터 타입은 클로저

함수의 파라미터가 autoclosure attribute를 이용하고 있는 경우, 함수의 원형을 잘 들여다 보지 않으면 함수의 파라미터 타입이 클로저의 반환타입으로 착각할 수 있습니다.

메서드를 사용할때 인자는 반환타입의 상수나 변수를 적어도 컴파일 에러가 발생하지 않으니까요..

해당 사실을 모르고 파라미터의 타입을 잘못 인지하는 것도 충분히 문제가 될 수 있지만 더 큰 문제는 실제 타입이 클로저라는데 있습니다. 앞서서 클로저는 사용하면 호출되기 전까지는 실행되지 않는 **Lazy evaluation 특징이 있다고 정리했습니다.

그럼 코드에 따라서 클로저의 **Lazy evaluation 특징 때문에 예상치 못한 동작이 발생할 수 있습니다.

2) autoclosure 는 argument 를 가지지 않으며 리턴값이 있어야합니다.

() → T 형태의 클로저만 사용가능합니다.


참고 블로그

[@autoclosure what, why and when](https://medium.com/ios-os-x-development/https-medium-com-pavelgnatyuk-autoclosure-what-why-and-when-swift-641dba585ece)

Hashable

Hashable

Hash에 대해 먼저 간략하게 살펴보면 아래 그림과 같다 (바킹독 알고리즘 강의 내용)

스크린샷_2022-05-18_오후_11 27 07

결국 우리가 사용할 어떤 key 값을 Hash function을 통해서 고정된 길의 데이터로 mapping 해서 Hash value를 얻어내는 것을 의미한다.

왜 굳이 Hash Value를 얻는 과정이 필요할까? 라는 의문이 든다면 위의 예시를 생각해보면 된다.

만약 16자리의 카드번호을 이용해서 해당 카드를 사용하는 사람을 찾는 문제를 생각해보자!

다양한 방법이 있겠지만 시간복잡도가 O(1)이 되는 방법들을 생각해보면 단순하게 Counting Sort와 유사하게 0~9999 9999 9999 9999 크기의 String 타입 배열을 가지고 있으면 인덱스에 카드번호를 넣어줌으로서 O(1) 의 복잡도로 해당 카드번호를 사용하는 사람을 찾을 수 있다.

하지만 위의 방법은 메모리를 굉장히 많이 사용한다는 단점이 있다. 이때 Hash의 개념을 이용하면 메모리는 절약하면서 O(1)의 시간복잡도를 가지도록 할 수 있다.

다양한 Hash function이 있겠지만 단순하게 예를 들면 카드의 앞의 4자리만을 위와 같은 방법으로 저장해서 구분한다고 생각해보자. 이렇게 임의의 길이의 데이터를 고정된 길이의 데이터로 mapping 하는 함수가 Hash function이다.

그렇게 되면 중복되는 문제가 생길 수 있지 않을까? 하는 의문이 생길 수 있다. Hash function을 이용하게 되면 예상했던 것 처럼 중복이 발생해서 충동 이 생기게 되고 이런 충돌이 필연적이다.

이런 충돌을 피하기 위해서 다양한 기법들이 존재한다. (Open Addressing, Chaning)


그럼 Hashable 프로토콜을 채택한다는 것은 어떤 key 값을 해쉬함수를 통해서 hash value로 만들어 낼 수 있다. 어떤 key 값을 고유한 hash value (Int)로 만들어 낼 수 있음을 의미한다.

protocol Hashable: Equatable {
  var hashValue: Int { get } // Deprecated
  func hash(into hasher: inout Hasher)
}

프로토콜을 살펴보면 앞서 공부한 Equatable 프로토콜을 채택하고 있고 hash 함수를 구현해야 하는 것을 확인 할 수 있다.

Swift의 기본 자료형들은 이미 Hashable 프로토콜을 채택하고 있다. (그래서 우리가 딕셔너리 타입을 사용할 때 key 값으로 사용할 수 있었고, Set의 자료형으로 사용할 수 있었다)

구조체, 열거형, 클래스와 같이 우리가 커스텀하게 만드는 경우 Hashable을 채택하는 방법을 살펴보자

struct

// 구조체 내의 프로퍼티가 모두 기본 자료형인 경우, Hashable 채택만으로 가능
struct Human: Hashable {
  let name: String
  let height: Double
}

let a = Human(name: "bran", height: 27)
let b = Human(name: "bran", height: 27)
if a == b {
  print("오잉 구조체는 값타입이라 Equable을 만족하고 있는건가?")
}

var dic: [Human : Int]

구조체의 저장프로퍼티는 모두 Hashable을 준수해야 한다.

구조체는 Swift 4.1 이후로 Hashable을 채택만해주면 된다.

enum

// 연관값이 없는 열거형은 Hashable을 채택하지 않아도 자동으로 구현
enum Gender {
  case male
  case female
}

var myDic: [Gender : Int]

enum GenderwithAge: Hashable {
  case male(age: Int)
  case female(man: Human)
}

연관값이 없는 열거형의 경우 Hashable을 이미 채택하고 있고, 연관값이 있는 경우 연관값은 모두 Hashable을 준수해야 한다.

class

// 클래스가 Hashable하기 위해서는 프로토콜을 채택하고
class Man {
  let name: String = "Bran"
  let age: Int = 27
}

// Equatable 프로토콜을 채택하고
extension Man: Equatable {
  static func == (lhs: Man, rhs: Man) -> Bool {
    return lhs.name == rhs.name && lhs.age == rhs.age
  }
}

// hash 함수를 구현해야 한다
extension Man: Hashable {
  func hash(into hasher: inout Hasher) {
    hasher.combine(name)
    hasher.combine(age)
  }
}

클래스는 Equatable 프로토콜을 준수하고 hash 함수를 가지고 있어야 한다. hash 함수는 Hasher의 combine을 사용해서 쉽게 구현할 수 있다.

(combine에는 해당 타입의 모든 저장 프로퍼티를 전달, 해당 프로퍼티는 Hashable을 준수하고 있어야 함)

ECDSA - 1

ECDSA

ECDSA(Elliptic Curve Digital Signature Algorithm)는 비대칭키 암호화의 한 유형으로, 타원 곡선 암호를 기반으로 한 디지털 서명 알고리즘입니다.

ECDSA 알고리즘이 어떤 방식으로 구현되어서 암호화를 하는지에 대해서는 정확히 모른다. 목표는 ECDSA를 Swift를 통해서 구현해서 이용하는데 중점을 두고 정리해보고자 한다

비대칭키 암호화, 디지털 서명 알고리즘

ECDSA 설명에서 다른 부분은 정확히 모르더라도 2개의 키워드를 통해서 어떤 메서드들이 필요한지 유추할 수 있다.

  • Private Key, Public Key Pair 생성
  • Private Key를 통한 Signature 생성
  • Public Key를 통한 Signature Validation

대칭키와 비대칭키에 대해 정리하면서 다뤘던 부분들입니다

순차적으로 하나씩 구현해보면서 간단하게 정리해보자


1) Key Pair 생성

ECDSA가 어떤 알고리즘인지 모르는데 어떻게 만드나요?

아주 다행히도 애플은 암호화를 위해 CryptoKit 프레임워크를 제공해주고 있고 사용하는 스펙에 맞게 P256, P348, P521, Curve25519를 선택하면 된다.

Untitled

(각 숫자가 의미하는건 Key Size로 이미 구현된곳과 맞춰줘야 한다. ECDSA256은 P256을 사용하면 된다)

import CryptoKit
import Foundation

final class ECDSA {
  private let privateKey: P256.Signing.PrivateKey
  private let publicKey: P256.Signing.PublicKey

  // MARK: - Generate ECDSA Key Pair
  init() {
    self.privateKey = .init()
    self.publicKey = privateKey.publicKey
  }

  func getPrivateKey() -> String {
    privateKey.derRepresentation.base64EncodedString()
  }

  func getPublicKey() -> String {
    publicKey.derRepresentation.base64EncodedString()
  }
}

코드에서 확인해야 될 부분은

  • private key를 통해서 public key를 생성할 수 있다
  • private key의 representation 종류

첫번째는 코드를 통해서 바로 확인할 수 있지만 두번째 representation의 종류는 서버나 이미 구현된 곳과 형태를 맞춰서 사용해야 한다.

처음 구현할 때 이부분에서 애를 좀 먹었는데 간단하게 종류와 특성을 확인해보자

/// Creates a P-256 private key for signing from a collection of bytes.
///
/// - Parameters:
///   - rawRepresentation: A raw representation of the key as a collection of
/// contiguous bytes.
public init<Bytes>(rawRepresentation: Bytes) throws where Bytes : ContiguousBytes

/// Creates a P-256 private key for signing from a Privacy-Enhanced Mail
/// PEM) representation.
///
/// - Parameters:
///   - pemRepresentation: A PEM representation of the key.
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
public init(pemRepresentation: String) throws

/// Creates a P-256 private key for signing from a Distinguished Encoding
/// Rules (DER) encoded representation.
///
/// - Parameters:
///   - derRepresentation: A DER-encoded representation of the key.
@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *)
public init<Bytes>(derRepresentation: Bytes) throws where Bytes : RandomAccessCollection, Bytes.Element == UInt8

/// - Parameters:
///   - x963Representation: An ANSI x9.63 representation of the key.
public init<Bytes>(x963Representation: Bytes) throws where Bytes : ContiguousBytes

우선 종류만 보면 4가지의 Representation이 존재하고 pemRepresentation을 제외하고 나머지는 Data Type임을 알 수 있다.

1) rawRepresentation

의미 그대로 별다른 규칙없이 단순히 Data Type으로 생성하는 것을 의미한다.

서버에서 다른 Representation을 사용하고 있더라도 raw에서 변환이 가능하기 때문에 클라이언트에서 Private Key를 생성하고 저장할 때는 rawRepresentation 을 사용하는 것을 추천한다

2) pemRepresentation

pem은 텍스트 기반의 암호화 형식으로 Base64로 인코딩된 데이터를 시작과 끝에 특정한 태그를 붙여 표현한다.

pem 형식으로 public key를 생성하면 String Type으로 키가 생성되고 키의 처음과 끝에 - - -Private Key - - -와 같은 태그가 생성되는 것을 확인할 수 있다.

3) derRepresentation

Creates a P-256 private key for signing from a Distinguished Encoding

특정한 태그 및 길이정보를 포함해서 데이터를 직렬화 해서 저장한다 (사실 정확한 의미는 모르겠습니다)

사실 derRepresentation과 x963Representation이 어떤 의미를 가지는지는 모르지만 Data Type을 base64String 이나 hexString으로 변환해서 이미 구현된 부분과 길이를 비교하면 어떤 Representation을 사용하고 있는지 확인해 볼 수 있다.


ECDSA 키 생성 및 저장에 대해서 정리해보면,

  • Private Key, Public Key는 pair로 생성되며 Private Key를 통해서 Public Key를 생성할 수 있다.
  • 키 생성시 다양한 종류의 Representation이 있는데 base64String, hexString 등으로 변환해 이미 구현된 곳과 길이를 비교해서 찾을 수 있다.
  • Private Key는 보통 KeyChain을 통해서 저장하게 되는데 rawRepresentation을 통해서 다른 타입으로 변환이 가능하므로 저장은 rawRepresentation으로 한다

[iOS] WebSocket - CLOSE_WAIT

CLOSE_WAIT

WebSocket을 연결해서 테스트 해보던 중 발생했던 버그(?)를 정리해보고자 한다.

  • StarScream, URLSessionSocket을 통해 구현하는 과정보다는 현상에 대해 정리할 예정이다.
  • Socket 연결 테스트는 업비트를 이용

우선 업비트 소켓을 터미널에서 테스트 해보자

Untitled

< {
"type":"ticker",
"code":"KRW-BTC",
"opening_price":29398000,
"high_price":29528000,
"low_price":29350000,
"trade_price":29468000,
"prev_closing_price":29420000.00000000,
"acc_trade_price":39734960333.20671000,
"change":"RISE",
"change_price":48000.00000000,
"signed_change_price":48000.00000000,
"change_rate":0.0016315432,
"signed_change_rate":0.0016315432,
"ask_bid":"BID",
"trade_volume":0.01749575,
"acc_trade_volume":1349.62787321,
"trade_date":"20230205",
"trade_time":"103530",
"trade_timestamp":1675593330855,
"acc_ask_volume":647.32427056,
"acc_bid_volume":702.30360265,
"highest_52_week_price":57678000.00000000,
"highest_52_week_date":"2022-03-28",
"lowest_52_week_price":20700000.00000000,
"lowest_52_week_date":"2022-12-30",
"market_state":"ACTIVE",
"is_trading_suspended":false,
"delisting_date":null,
"market_warning":"NONE",
"timestamp":1675593330915,
"acc_trade_price_24h":81916979573.45246000,
"acc_trade_volume_24h":2780.05924818,
"stream_type":"SNAPSHOT"
}>

소켓 연결이 되면 위와 같은 값들이 실시간으로 들어오는 것을 확인 할 수 있다.

그리고 홈페이지에서 설명한 idle timeOut를 테스트 하기 위해서 소켓 연결 후, 120초 간 데이터 송 수신이 없는 상태로 대기 해보았다. (ping, pong x)

Untitled 1

약 120초가 지나면 DisConnected 되는것을 확인할 수 있다.


이제 위의 소켓 연결을 Starscream을 이용해 구현하고 테스트 해보자

final class SocketManager {
  static let shared = SocketManager()
  private var socket: WebSocket?

  private init() {
    setupWebSocket()
  }

  deinit {
    socket?.delegate = nil
  }

  private func setupWebSocket() {
    let url = URL(string: "wss://api.upbit.com/websocket/v1")!
    var request = URLRequest(url: url)
    request.timeoutInterval = 5
    socket = WebSocket(request: request)
  }

  func connect() {
    socket?.delegate = self
    socket?.connect()
  }

  func disconnect() {
    socket?.disconnect()
  }

  private func sendMessage(_ message: String) {
    socket?.write(string: message)
  }

  private func sendRequest() -> String {
    """
    [{"ticket":"\(UUID())"},{"type":"orderbook","codes":["KRW-BTC"]}]
    """
  }
}

extension SocketManager: WebSocketDelegate {
  func didReceive(event: WebSocketEvent, client: WebSocket) {
    switch event {
    case .connected(let headers):
      print("websocket is connected: \(headers)")
    case .disconnected(let reason, let code):
      print("websocket is disconnected: \(reason) with code: \(code)")
    case .text(let text):
      print("received text: \(text)")
    case .binary(let data):
      print("Received data: \(data.count)")
    case .ping(_):
      print("Ping: ")
    case .pong(_):
      print("Pong: ")
    case .viabilityChanged(_):
      print("viabilityChanged")
    case .reconnectSuggested(_):
      print("reconnectSuggested")
    case .cancelled:
      print("websocket is canclled")
    case .error(let error):
      print("websocket is error = \(error!)")
    }
  }
}

터미널에서 본 결과와 마찬가지로 소켓 연결을 하고, 티켓을 메시지로 보내면 동일한 결과값을 받을 수 있다.

image

하지만 idle timeout를 테스트 해보면, 클라이언트 단에서 CloseWait 상태로 무한히 대기하고 있는 문제점을 발견했다.

Untitled 2

TCP 종료시점의 4way-handshake 과정에서 살펴보면,

서버가 연결을 종료하기 때문에 서버가 Active close(왼쪽), 클라이언트가 Passive close(오른쪽)이다.

현재 서버에서 종료하겠다는 Fin을 보내고 클라이언트에서 해당 신호를 받고 CloseWait 상태로 변경된 상태로 머물고 있는 상황이다. 그럼 서버도 클라이언트도 종료할 소켓 연결을 계속 종료하지 못하는 걸까?

다행히 그건 아니다. Active Close 에서는 Fin 신호를 보내고 일정 시간 동안 응답을 받지 못하면 연결을 종료한다. 문제는 서버에서 소켓 연결을 끊었음에도 불구하고 클라이언트단의 CloseWait은 남는것이다.

(물론 반대의 상황도 문제가 된다)


지금 당장 TCP 핸드쉐이크를 구현할 자신도 없거니와… 시간도 없었기 때문에 급한대로 URLSession을 이용했을 때의 결과를 살펴보았다.

image_(1)

❓❓❓❓

정상적으로 소켓 연결이 끊어지는 것을 확인 할 수 있었다.

심지어 Starscream 3.1.1 버전을 사용한 경우에도 정상적으로 동작하는 것을 확인했다.

3.x → 4.x Starscream 으로 가면서 라이브러리가 많이 바뀌어서 정확한 원인을 파악하지는 못했지만 아래와 같은 문제로 예상한다.

public convenience init(request: URLRequest, certPinner: CertificatePinning? = FoundationSecurity(), compressionHandler: CompressionHandler? = nil, useCustomEngine: Bool = true) {
        if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *), !useCustomEngine {
            self.init(request: request, engine: NativeEngine())
        } else if #available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *) {
            self.init(request: request, engine: WSEngine(transport: TCPTransport(), certPinner: certPinner, compressionHandler: compressionHandler))
        } else {
            self.init(request: request, engine: WSEngine(transport: FoundationTransport(), certPinner: certPinner, compressionHandler: compressionHandler))
        }
    }

현재 4.x starscream의 경우, socket을 init하는 시점에 userCustomEngine을 디폴트 파라미터로 사용하도록 설정되어서 TCPTransport를 사용하는 WSEngine을 사용하게 된다.

socket = WebSocket(request: request, engine: NativeEngine()) 으로 테스트 해본 결과 정상적으로 소켓 연결이 종료되는 것을 확인할 수 있었다.

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public class NativeEngine: NSObject, Engine, URLSessionDataDelegate, URLSessionWebSocketDelegate {
    private var task: URLSessionWebSocketTask?
    weak var delegate: EngineDelegate?

    public func register(delegate: EngineDelegate) {
        self.delegate = delegate
    }

    public func start(request: URLRequest) {
        let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
        task = session.webSocketTask(with: request)
        doRead()
        task?.resume()
    }

    public func stop(closeCode: UInt16) {
        let closeCode = URLSessionWebSocketTask.CloseCode(rawValue: Int(closeCode)) ?? .normalClosure
        task?.cancel(with: closeCode, reason: nil)
    }

    public func forceStop() {
        stop(closeCode: UInt16(URLSessionWebSocketTask.CloseCode.abnormalClosure.rawValue))
    }

    public func write(string: String, completion: (() -> ())?) {
        task?.send(.string(string), completionHandler: { (error) in
            completion?()
        })
    }

    public func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?) {
        switch opcode {
        case .binaryFrame:
            task?.send(.data(data), completionHandler: { (error) in
                completion?()
            })
        case .textFrame:
            let text = String(data: data, encoding: .utf8)!
            write(string: text, completion: completion)
        case .ping:
            task?.sendPing(pongReceiveHandler: { (error) in
                completion?()
            })
        default:
            break //unsupported
        }
    }

    private func doRead() {
        task?.receive { [weak self] (result) in
            switch result {
            case .success(let message):
                switch message {
                case .string(let string):
                    self?.broadcast(event: .text(string))
                case .data(let data):
                    self?.broadcast(event: .binary(data))
                @unknown default:
                    break
                }
                break
            case .failure(let error):
                self?.broadcast(event: .error(error))
            }
            self?.doRead()
        }
    }

    private func broadcast(event: WebSocketEvent) {
        delegate?.didReceive(event: event)
    }
    
    public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
        let p = `protocol` ?? ""
        broadcast(event: .connected([HTTPWSHeader.protocolName: p]))
    }
    
    public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        var r = ""
        if let d = reason {
            r = String(data: d, encoding: .utf8) ?? ""
        }
        broadcast(event: .disconnected(r, UInt16(closeCode.rawValue)))
    }
}

NativeEngine 구현체를 확인해보면 URLSession을 사용하고 있다. (테스트를 위해 만든 코드와 싱크로율 99%)

URLSessionSocket을 iOS13 부터 지원하면서, StarScream도 URLSession을 사용하도록 변경된것 같다.

Engine이 정확하게 어떤 역할을 담당해주고 있는지 까지는 파악하지 못했지만 TCP 종료시점의 핸드쉐이크 과정에 영향을 미치고 있고, Starscream 4.x 버전을 사용한다면 NativeEngine을 사용해야 한다.

@ChaNoo97

RSA - 2

2) Encrypt

일반적으로 비대칭키 알고리즘에서는 공개키를 통해서 암호화를 진행하고 비밀키를 통해서 복호화를 진행한다.

(서명과 검증과정에서는 비밀키를 통해서 서명과정을 암호화 시켜서 보내고 공개키를 통해서 복호화를 진행한다)

→ 이에 대한 자세한 내용은 대칭키와 비대칭키 알고리즘에 대한 정리한 내용을 확인하면 된다.

/// RSA - Encrypt plain string to rsa base64 String
  func encryptByRSA(
    plainString: String
  ) -> String? {
    var error: Unmanaged<CFError>?
    let algorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256

    guard
      let publicKey = self.publicKey,
      SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm),
      let plainData = plainString.data(using: .utf8),
      let ciperData = SecKeyCreateEncryptedData(
        publicKey,
        algorithm,
        plainData as CFData,
        &error
      )
    else { return nil }

    return cfDataTobase64String(ciperData)
  }

Public Key를 통해서 평문을 암호화 하는 구현 자체는 코드를 통해서도 쉽게 확인할 수 있고 Security에서 제공해주고 있기 때문에 간단하다. 하지만 ECDSA와 마찬가지로 암호화 알고리즘은 구현 자체보다는 서버 혹은 이미 구현된 곳과 스펙을 맞추는 과정이 중요하다.

RSA 알고리즘을 통한 암호화 복호화 과정에서는 패딩옵션을 설정해 줄 수 있는데 이를 다른 곳과 동일하게 설정해야 서로 정상적인 encrypt, decrypt를 수행할 수 있다.

코드에서도 확인할 수 있듯이 SecKeyAlgorithm 설정을 통해서 다양한 패딩, 해시함수 옵션들이 있는데 이 중에서 다른 곳과 동일한 옵션을 찾아서 사용해야 한다.

(GPT 코드를 참고했을 때 SecPadding 을 통해서 패딩옵션을 설정하는 코드들이 많았는데 아쉽게도 iOS15 이후에 deprecated 되었다)

코드를 보면 encrypt 하려는 평문을 데이터로 만들고 public key를 이용해서 우리가 설정한 옵션을 통해서 encrypt를 수행한 결과가 CFData type으로 ciperData에 담기는 것을 확인할 수 있다.

일반적으로 서버와 통신할 때 Data Type보다는 Hex String, Base64String 타입으로 보내준다.

private func cfDataTobase64String(
    _ cfData: CFData
  ) -> String? {
    let data = cfData as Data
    let base64String = data.base64EncodedString()
    return base64String
  }

3) Decrypt

/// RSA - Decrypt rsa base64 String to plain String
  func decryptByRSA(
    encodedString: String
  ) -> String? {
    var error: Unmanaged<CFError>?
    let algorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256

    guard
      let privateKey = self.privateKey,
      SecKeyIsAlgorithmSupported(privateKey, .decrypt, algorithm),
      let encodedData = base64StringToCFData(encodedString),
      let ciperData = SecKeyCreateDecryptedData(
        privateKey,
        algorithm,
        encodedData,
        &error
      )
    else { return nil }

    return cfDataToString(ciperData)
  }

복호화 하는 코드는 암호화된 String을 CFData로 변경하고 private key를 사용한다는 점 이외에는 encrypt과 별반 다르지 않다.

private func cfDataToString(
    _ cfData: CFData
  ) -> String? {
    let length = CFDataGetLength(cfData)
    if let bytes = CFDataGetBytePtr(cfData) {
      let data = Data(bytes: bytes, count: length)
      return String(data: data, encoding: .utf8)
    } else {
      return nil
    }
  }

decrypt 결과로는 base64 String타입이 아닌 String 평문이 나오도록 설정해야 한다.


RSA를 사용하는 경우

  • 서버에서 RSA Public Key를 받는 경우
  • 서버에 RSA Public Key를 보내는 경우

크게 두 가지 형태가 존재하게 되는데 전자의 경우에는 서버로 보내는 request body를 암호화 시키는 로직, 후자는 서버로 부터 암호화 된 response body를 복호화 시키는 로직이 필요하다.

전자의 경우에는 서버로 부터 public key를 SecKey 타입으로 변경하는 로직이 추가적으로 구현되어야 한다. 서버에서 어떤 형식으로 public key가 올지는 상황마다 다르겠지만 이 과정에서 representation을 설정할 수 없는 문제 때문에 라이브러리 선택을 결정했다.

private func stringToSecKey(
    _ privateKey: String
  ) -> SecKey? {
    let keyAttributes: [CFString: Any] = [
      kSecAttrKeyType: kSecAttrKeyTypeRSA,
      kSecAttrKeyClass: kSecAttrKeyClassPublic,
      kSecAttrKeySizeInBits: 2048
    ]
    var error: Unmanaged<CFError>?
    if let privateKeyData = Data(base64Encoded: privateKey),
       let secKey = SecKeyCreateWithData(
        privateKeyData as CFData,
        keyAttributes as CFDictionary,
        &error
       ) {
      return secKey
    } else {
      return nil
    }
  }

만약 raw Representation을 사용하고 있다면 위의 코드를 통해서 SecKey를 생성할 수 있다.

HTTP 웹 기본 지식 - 2

URI와 웹 브라우저 요청 흐름

URI(Uniform Resource Identifier)

회사에서 서버 개발자들이 사용하는 URI와 URL에 대한 차이점을 확인해보자

Untitled

  • URI는 단어 의미 그대로 리소스를 식별하는 Identifier를 의미
  • URL, URN은 URI라는 큰 범위안에 포함된다

URL(=Locater)

  • 특정 서버의 리소스가 있는 위치를 지정
  • 우리가 일반적으로 보는 http://naver.com 이런것들을 의미한다

URN(=Name)

  • 리소스에 대한 이름을 의미
  • 위치는 변할 수 있지만, 이름은 변하지 않기 때문에 리소스 위치가 변하더라도 문제가 발생하지 않는다
  • 하지만 실제 이름만으로 리소스를 찾을 수 있는 방법이 보편화 되지 않았다.

정리하자면 아래와 같다

  • URI(리소스 식별)은 크게 URL, URN으로 나눠진다
  • URN은 보편화 되지 않아서 일반적으로 URL을 사용한다
  • 엄밀하게 보면 URL → URI (o), URI → URL (x) 형태지만 일반적으로 URI ~= URL 처럼 사용된다. (해당 강의에서는 URI = URL로 의미한다)

URL

Untitled 1

  • Scheme: 일반적으로 프로토콜을 작성한다 ex) http, https, ftp
  • Userinfo: URL에 사용자 정보를 포함해서 인증하는 경우에 사용한다 (거의 사용하지 않음)
  • Host: IP 주소혹은 지난 시간에 배운 DNS를 이용한 도메인명을 적는다.
  • Port: 지난 시간에 배운 Port를 의미 http = 80, https = 443를 사용 (일반적으로 생략)
  • Path: 리소스 경로를 의미
  • Query
    • key-value 형태로 존재
    • ?로 시작하고 &로 추가할 수 있음
    • query parm, query string 이라고도 부름 (value값이 전부 string으로 들어가기 때문)
    • 경험상 GET에서 자주 사용했었음
  • Fragment
    • html 내부 북마크 (링크 들어가면 특정위치로 가던것들)
    • 서버에 전송되는 값이 아님

제네릭 타입

제네릭 타입

Type Parameter

코드를 보다보면 함수과 같이 <>로 감싸진 형태를 쉽게 찾아볼 수 있다. 이때 T는 타입 파라미터로 어떤 타입의 ‘이름'을 의미한다.

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

말그대로 어떤 ‘타입’ 을 의미하기 때문에 컴파일 과정에서 해당 타입이 어떤 타입인지 결정되지 않고 런타임 과정에서 타입을 결정한다

예를 들어서, C++ STL에 있는 Stack을 구현한다면 아래와 같이 제네릭 타입을 이용해서 타입에 무관한 Stack을 만들 수 있다.

struct Stack<T> {
    var items = [T]()
    mutating func push(item: T) {
        items.append(item)
    }
    mutating func pop() -> T {
        return items.removeLast()
    }
}

Type Constraint

아래와 같은 형태는

struct TextFieldCustomViewWrapper<KeyboardView: View>: UIViewRepresentable {
}

KeyboardView 라는 타입파라미터를 사용하는데 KeyboardView는 View 프로토콜을 채택하고 있는 어떤 타입

을 의미한다. 이렇게 특정 프로토콜을 준수해야만 제네릭을 사용할 수 있도록 타입 제약을 줄 수 있다.

Swift 의 Dictionary 구조체를 들여다 보면 아래와 같이 타입 제약이 걸려있는 것을 확인할 수 있다.

@frozen struct Dictionary<Key, Value> where Key : Hashable

즉 Key가 Hashable 프로토콜을 채택하는 타입인 경우에만 사용할 수 있도록 제약이 걸려있다.

Associated Type

protocol Publisher {
  associatedtype Output
  associatedtype Failure : Error
  func receive<S>(subscriber: S) 
		where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

프로토콜을 보다보면 associatedType이라는 키워드를 볼 수 있다. associated Type 이니까 우리가 일반적으로 생각하는 Type임을 유추할 수 있다.

예시와 함께 보면 어떤 의미를 가지는지 알 수 있다.

protocol BranProtocol {
	associatedtype MyType
	var name: MyType { get }
}

struct Bran: BranProtocol {
	var name: String {
		return "Sangwon"
	}
}

BranProtocol을 채택하면 MyType의 name을 정의해야 하는데 String 타입이외에도 다른 다양한 타입의 name이 될 수 있는 가능성이 있을 때 위와 같이 associatedtype을 사용할 수 있다.

(프로토콜에서 사용하는 제네릭 타입으로 생각!)

위의 제네릭에서 봤던 예제와 마찬가지로 연관타입에도 프로토콜을 통해서 제약을 줄 수 있다.

HTTP 웹 기본 지식 - 6

HTTP 상태코드

클라이언트가 서버로 보낸 요청의 처리 상태를 알려주는 기능으로 아래와 같이 5가지로 분류해서 사용한다.

  • 1xx (Informational)
  • 2xx (Successful)
  • 3xx (Redirection)
  • 4xx (Client Error)
  • 5xx (Server Error)

(1xx는 거의 사용되지 않기 때문에 생략)


2xx - 성공

200 OK

클라이언트가 서버로 보낸 요청을 성공해서 서버가 클라이언트로 보내줄 때의 상태코드를 의미

201 Created

요청이 처리되어서 새로운 리소스가 생성되었음을 의미

POST를 통해서 새로운 리소스를 DB에 생성하는 경우를 가정해보자.

PUT과 달리 POST를 통해 리소스를 생성하는 경우 클라이언트에서는 리소스의 URI를 모른다. 이때 성공적으로 리소스가 생성되면 해당 리소스의 URI를 응답의 헤더에 (Location)에 실어서 보내준다.

202 Accepted

요청이 접수되었으나 처리가 완료되지 않은 상태

  • 배치처리에서 사용

204 No Content

서버가 요청을 성공적으로 수행했지만, 클라이언트에 보낼 데이터가 없음

앱에서 유저가 게시글을 수정하고 저장 버튼을 누른 경우를 생각해보자. PUT, POST, PATCH 메서드를 이용해 게시글 수정을 서버가 요청받고 해당 작업을 완료 되었음을 표현할 때 클라이언트에게 어떤 데이터를 줘야할까?

200 OK에 성공했음을 의미하는 표현을 담아서 보낼 수도 있겠지만 사실 성공했음라는 여부만 필요한 것이지 어떠한 데이터도 필요하지 않다. 이런 경우에 204 No Content를 사용한다.

사실 클라이언트 개발 입장에서는 위와 같은 상황에서 200, 204든 상관없이 Response Body값을 디코딩하는 과정없이 상태코드만을 이용해서 유저에게 성공, 실패 여부를 알려주는 방식으로 개발하면 될것 같은데 204라는 상태코드의 의미를 통해서 더 명확히 할 수 있겠다라는 생각이 들었다.


3xx - 리다이렉션

클라이언트가 보낸 요청을 완료하기 위해서 유저 에이전트(어플리케이션)의 추가 조치 필요를 의미

리다이렉트(Re-Direct)

단어의미 그대로 다시 방향을 가리키는것을 의미한다.

웹 브라우저는 3xx 응답의 결과에 Location 헤더가 있으면 Location 위치로 자동 이동한다.

Untitled

출석 이벤트 페이지가 변경되었을 때를 생각해보자. 유저가 만약 북마크된 이전의 이벤트 페이지에 들어갔을 경우 서버에서 300번대 상대코드와 새로운 이벤트 페이지를 보내주고 유저의 URL을 새로운 이벤트 페이지로 리다이렉트 시킨다.

Untitled 1

영구 리다이렉션

301 Moved Permanently

리다이렉트시 요청 메서드가 GET으로 변하고 메시지 바디가 제거될 수 있음

항상 메서드가 변하고 본문이 제거되는 것은 아니지만 대부분 해당 방식을 사용하고 있다.

308 Permanet Redirect

리다이렉트시 요청 메서드와 메시지 바디 유지

둘의 차이를 이전과 동일한 환경에서 유저가 특정 이벤트 페이지의 내용을 작성해서 확인버튼을 눌렀을 때를 예시로 들어보자.

// MARK: - 301

// 유저 요청 - URL:/event
POST /event HTTP/1.1

name=hello&age=20

// 응답
HTTP/1.1 301 Moved Permanently
Location: /new-event

// 유저 요청 - URL:/event-new
GET /new-event HTTP/1.1
// MARK: - 308

// 유저 요청 - URL:/event
POST /event HTTP/1.1

name=hello&age=20

// 응답
HTTP/1.1 301 Moved Permanently
Location: /new-event

// 유저 요청 - URL:/event-new
POST /event HTTP/1.1

name=hello&age=20

301의 경우 리다이렉트 이후 요청하는 메서드가 GET으로 바뀌고 메시지가 사라진것을 확인 할 수 있다. 즉 새롭게 리다이렉션된 페이지에서 다시 이벤트를 위한 정보를 입력하고 POST를 요청해야 한다.

일시적인 리다이렉션

302 Found

리다이렉트시 메서드가 GET으로 변하고 본문이 제거될 수 있음 (301과 유사)

307 Temporary Redirect

리다이렉트시 요청 메서드와 본문 유지

303

리다이렉트시 요청 메서드가 GET으로 변경

307, 303은 301과 마찬가지로 스펙상으로는 요청 메서드가 변경되지는 않지만 거의 모든 웹 브라우저들이 GET으로 바꾸고 있다. 이러한 모호함을 없애기 위해서 스펙상 확실히 요청 메서드가 바뀌는 303, 그렇지 않은 307이 등장했다.

그럼 영구적인 리다이렉션과 일시적인 리다이렉션의 차이점이 무엇일까?

리소스의 URI가 영구적으로 변경되는지 일시적으로 변경되는지를 기준으로 나눠진다. 하지만 클라이언트 입장에서 변경된 URI로 리다이렉션 되는 결과는 같은데 정확하게 어떤 차이가 있는걸까?

결론적으로 말하면 이 둘을 구별하는 주체는 사람이 아니라 검색엔진이고 클라이언트에서는 차이를 알 수 없다.

영구 다이렉션인 경우에는 검색엔진에서도 URL이 변경되고 일시적인 다이렉션에서는 검색엔진에서는 URL이 변경되지 않는다.

🌐 301 vs 302 상태 코드 차이점 (SEO)

위의 블로그에서는 캐싱에 대한 차이도 있는데 해당 부분은 캐싱에 대해 좀 더 공부하고 다시 정리

PRG(Post Redirect Get)

웹 브라우저에서 새로고침을 하면 마지막 요청을 다시 보내게 되는데 POST 메서드로 음식을 주문하고 새로고침 하면 어떻게 될까? → 중복 주문이 될 수 있다.

이런 경우에 302(303)를 이용해서 주문 결과 화면을 GET 메서드로 리다이렉트 시켜준다. 이후 새로고침해도 결과하면을 GET으로 다시 조회하기 때문에 중복 주문을 방지할 수 있다.


4xx - 클라이언트 오류

클라이언트 요청이 잘못되어서 서버가 요청을 수행할 수 없는 상태

클라이언트 오류와 서버오류를 구분하는 기준은 동일한 데이터로 재시도 일어났을 때 클라이언트 오류는 이미 잘못된 데이터를 보내고 있기 때문에 계속해서 실패하고 서버 오류는 서버의 상태가 안정화(?)되면 복구될 수 있다는 차이가 있다.

400 Bad Request

클라이언트가 잘못된 요청을 해서 서버가 요청을 처리할 수 없음

401 Unauthorized

클라이언트가 해당 리소스에 대한 인증이 필요함

  • Authoriazation(인가): 특정 리소스에 접근할 수 있는 권한
  • Authentication(인증): 본인 확인(로그인)

오류 메시지가 Unauthorized가(인가)로 되어있지만 인증에 대한 오류를 나타냄

403 Forbidden

서버가 요청을 이해했지만 승인을 거부함 → 접근 권한이 불충분한 경우

404 Not Found

요청 리소스가 서버에 없음


5xx - 서버 오류

500 Internal Server Error

서버 내부 문제로 오류 발생

애매하면 500번 에러를 주는데 이는 순수한 서버오류인 경우에만 사용하도록 지향해야 한다.

예를 들어서 19세 미만의 사용자가 주류를 주문했을 때 서버에서 유저의 나이 때문에 정상적인 결과를 출력하지 못하는 경우에는 2xx, 4xx를 사용해서 표현하는 것이 일반적인다.

200 vs 404

503 Service Unavailable

서비스 이용 불가

일시적인 과부하 혹은 예정된 작업으로 서버를 다운 시켜놓은 경우 사용하고 Retry-After 헤더로 복구 시간을 보낼 수도 있다.

Xcode 개발(배포)환경 나누기 - 1

개발환경 나누기

Xcode를 통해서 개발환경을 왜 나눌까?

일반적으로 개인 프로젝트 수준에서 서버가 환경별로 분리되어 있지 않기 때문에 개발환경을 따로 나눠서 작업을 해본 경험이 없었다. (물론 광고를 붙이는 경우에는 필요할것 같다)

하지만 일반적인 회사 프로젝트를 진행하게 되면 개발환경을 나눌 필요성이 생기게 된다.

  • 서버가 환경마다 값이 다를 수 있다.
  • 각 환경마다 배포를 달리 할 수 있다.

회사마다 다르겠지만 서버와 DB가 dev, stage, production 정도로 나누어져 있을 것이다. 그에 따라서 당연히 환경별로 서버명, 도메인, IP, 접속계정 등 프로퍼티 값이 다르다.

배포 관점에서도 일반적으로 dev를 통해 개발을 진행하고 stage로 qa작업을 진행하고 최종적으로 prod로 배포를 진행하게 되는데 qa에서 발견된 버그들을 수정하면서 동일 버전 내에서 다른 빌드번호가 작업들이 testflight에 쌓이게 된다.

이때 환경분리 없이 작업하다가 에러가 있는 버전을 배포하게 되는 날에는 상상하고 싶지 않다.

이런 문제점들을 개발환경을 나눠서 환경별로 다른 url을 가지도록 설정하고, 환경별로 다른 name, bundle identifier를 설정함으로서 해결할 수 있다.


그럼 어떻게 나눌까?

  • Target
  • Build Configuration

물론 Target을 통해서 환경별로 다른 product가 생성되도록 설정해 개발환경을 나눌수도 있지만 일반적으로 무료/유료 버전 구분, 위젯등 같은 앱이지만 다른 사용성을 제공할 때 사용한다.

(관련 공식 문서를 알고계시면 연락 부탁드립니다!!)

따라서 하나의 Target의 Produect를 Build Configuration을 통해 개발환경을 분리하는 것이 일반적이다.


Build Configuration

Build Configuaration는 빌드할 때 환경을 제어하는 값 정도로 생각하면 된다.

Untitled Untitled 1

프로젝트에서 기본적으로 Debug, Release가 설정되어 있고, 스킴에서 빌드와 배포에 따라 다른 configuartion을 사용하고 있는것을 확인할 수 있다.

그럼 configuration을 이용해서 개발환경을 나눠보자.

  • dev, stage, prod를 위한 debug/release 를 만든다. (상황에 맞게 사용)
  • dev, stage, prod에 해당하는 configuration을 사용한 scheme을 만든다.
  • dev, stage, prod 각각 xconfig 파일을 통해 환경별 값을 세팅한다.

크게 세 가지 과정으로 나누어서 정리할 예정이고 혹시나 빠진부분 있다면 김종권님의 블로그를 참고하면 된다.

Untitled 2

기존의 Debug, Release를 복사해서 dev, stage, prod 를 만든다. (dev release는 상황에 맞게 만든다.)

![Untitled
Untitled 3
]

그리고 dev, stage, prod에 해당하는 스킴을 생성한다. 스킴을 생성하는 이유는 환경별로 edit 스킴을 통해서 build configuration을 수정하는 과정이 번거롭고 실수를 줄이기 위해서 미리 각 환경에 맞는 스킴을 생성한다.

(처음에는 스킴을 통해서 개발환경을 나눈다고 생각했었는데 스킴은 빌드할 때 어떤 build configuration 이용할지 선택하는 것이지 스킴을 통해 개발환경을 나눈다는 표현은 반쪽 자리 정답인것 같다.)

Untitled 4 Untitled 5

이제 각 환경별로 xconfig 파일을 생성하고 환경에 맞게 build configuration에서 xconfig 파일을 설정해준다.


환경 변수 설정

여기까지 작업을 마치면 이제 모든 세팅이 끝났고 xconfig 파일을 통해서 환경별로 값을 세팅하는 과정만 남았다.

  • 환경별로 생성되는 앱을 다르게 → 환경별 배포 가능
  • 환경별로 세팅되는 API 주소값이 다르게 → 환경별 서버 다르게 사용 가능

먼저 환경별로 생성되는 앱을 다르게 설정해보자.

// Prod.xconfig
PRODUCT_BUNDLE_IDENTIFIER = com.bran.envsetting.app
PRODUCT_NAME = EnvSetting
DEPLOY_PHASE = prod

// Stage.xconfig
PRODUCT_BUNDLE_IDENTIFIER = com.bran.envsetting.stage.app
PRODUCT_NAME = EnvSetting-stage
DEPLOY_PHASE = stage

// Dev.xconfig
PRODUCT_BUNDLE_IDENTIFIER = com.bran.envsetting.dev.app
PRODUCT_NAME = EnvSetting-dev
DEPLOY_PHASE = dev

환경별 .xconfig 파일에서 환경별로 다른 앱을 빌드할 수 있도록 고유한 identifier값들을 설정해준다.

Untitled 6

앞서서 우리가 환경에 맞게 build configuration을 xconfig 파일의 값을 따르도록 설정했기 때문에 여기까지만 작업을 해도 프로젝트 빌드 세팅에서 값이 변경되어 있는 것을 확인할 수 있다.

그럼 미리 정의한 스킴별로 테스트 해보면 결과는? → 반영되지 않는다.

이유는 단순하다. 타겟의 설정 값들은 바뀌지 않은채로 여전히 남아있기 때문이다.

타겟의 빌드 설정값들은 프로젝트의 값들을 상속 받고 타겟의 product의 빌드 설정값은 타겟의 설정값을 따른다.

Untitled 7

따라서 타겟의 설정에서 우리가 설정한 환경변수 값을 사용하도록 설정하는 과정이 필요하다. 이제 다시 스킴별로 테스트하면 환경별로 서로 다른 앱 3개가 생성되는 것을 확인할 수 있다.

이제 배포하는 경우를 가정해보자.

Bundle Identifier가 다르기 때문에 TestFlight에서 Stage, Prod를 혼동해 실수할 일은 물리적으로 없어진다. 하지만 QA작업을 하다보면 Stage, Prod의 버전이나 빌드넘버가 상이해지는데 이를 어떻게 관리해야 할까?

물론, 브랜치 전략을 통해서 관리할 수도 있지만 xcode에서 빌드넘버, 버전은 .pbx 파일을 통해서 관리되기 때문에 실수하기 쉽다.

Untitled 8

의외로 간단한데 우리가 설정한 configuration 별로 버전, 빌드넘버를 다 따로 설정할 수 있다.

환경별로 API값이 다르게 설정해보자

info.plist에 환경별로 우리가 원하는 값을 xconfig에서 설정하고 앱이 시작하는 시점에서 info.plist 값을 불러오면 된다.

enum DeployPhase: String {
  case prod
  case stage
  case dev
}

final class Configuration {
  private init() { }

  private static let configKey = "DeployPhase"

  // MARK: - Get Current DeployPhase
  static func getDeployPhase() -> DeployPhase {
    guard
      let configValue = Bundle.main.object(forInfoDictionaryKey: configKey) as? String,
      let phase = DeployPhase(rawValue: configValue)
    else {
      return DeployPhase.dev
    }
    return phase
  }
}
Untitled 9
import SwiftUI

@main
struct EnvSettingApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onAppear {
          print(Configuration.getDeployPhase())
        }
    }
  }
}

겪었던 문제들

  • 환경별 custom flag 사용
  • 환경별 run script 분기처리
  • 환경별 firebase 설정

등등 다양한 문제들을 겪고 나름대로 해결한 방법은 추후에 하나씩 정리할 예정이다.


참고

Xcode Target

iOS 프로젝트 배포 환경별 build 세팅하기

[SwiftUI] NavigationLink in List Item

List Item 안에 버튼이 여러개 있는 경우

List Item 내부에 NavigationLink가 있는 경우

List Item 내부에 NavigationLink가 여러개 있는 경우

정리하기

Xib와 Nib

Xib와 Nib

스토리보드의 뷰컨트롤러와 Custom View의 xib파일이 컴파일 이후에 nib파일로 변경되는 것을 확인했는데 xib와 nib은 무엇이 다른것일까?

Nib(=NeXTSETP Interface Builder)

Untitled 4

https://dalgonakit.tistory.com/82

  • 화면을 구성하는 클래스들을 바이너리 형태의 압출 파일로 저장

Xib(=Xcode Interface Builder)

Untitled 5
  • 화면을 구성하는 클래스들을 XML 형태로 저장

두 파일이 생긴 형태를 보면 확실하게 차이를 알 수 있다. Nib은 바이너리 형태로 Xib는 XML 형태로 Interface Builder를 저장하고 있다.


Xib 파일을 컴파일 하면 결국 Nib파일이 생성되는데 왜 Xcode에서는 Xib 파일만 생성하고 관리하도록 지원하고 있을까?

이유는 버전관리에 있다. 물론 스토리보드나 Xib를 사용하는 시점에서 버전관리가 불편하지만 우리가 읽고 전혀 이해하기 힘든 바이너리 형태의 소스코드 보다는 xml 형태로 버전관리를 하는게 더 편하기 때문이다.

(그리고 Xcode에서는 Xib파일을 xml이 아닌 그래픽 형태로 수정할 수 있도록 지원하고 있다)

Enum - Associated Value

enum State {
  case reday
  case stop
  case go
}

let state: State = .go

if state == .go {
  print("True")
}

일반적으로 열거형을 사용할 때 위와 같이 case를 직접 비교해서 사용할 수 있었습니다. 연관값을 사용하는 경우에는 어떻게 될까요?

enum States: Equatable {
  case ready(String)
  case stop(Int)
  case go
}

let states: States = .ready("Ready")

if states == .ready("Ready") {
  print("True")
} else {
  print("False")
}

연관값을 사용하는 경우 위와 같은 case 비교가 불가능해집니다. Equtable 프로토콜을 채택하면 가능하긴 합니다.

if case let .ready("Ready") = states {
  print("True")
}

AES

AES

AES(Advanced Encryption Standard)는 현재 가장 널리 사용되는 대칭키 암호화 알고리즘 중 하나입니다.

ECDSA, RSA 같은 비대칭키 알고리즘과 마찬가지로 우리는 AES가 정확하게 어떤 알고리즘인지가 중요한게 아니다.

  • 암호화, 복호화에 동일한 키가 사용된다.
  • 서버에 키를 안전하게 보내야 한다

대칭키 알고리즘이므로 위의 두가지 정도만 머리속에서 떠오르면 된다.

  • AES Key 생성
  • AES를 이용한 Encrypt, Decrypt

순서대로 구현하면서 간단하게 정리해보자.

정리하기에 앞서서 라이브러리 없이 CryptoKit을 이용해서 AES를 구현하는 과정을 정리할건데 개인적으로 추천하지 않는다. CryptoKit을 이용해 암호화/복호화를 진행하면 AES.GCM.SealedBox 타입이 필연적으로 필요하게 되는데 서버와 스펙을 맞추거나 서버와 함께 암호화/복호화를 진행하기 너무 어려워진다.

따라서 개념정도만 익히고 아래 링크를 참고해서 CommonCrypto를 이용하거나 CryptoSwift 라이브러리를 사용하는 것을 적극 추천한다.

AES encryption in Swift
https://github.com/krzyzanowskim/CryptoSwift


1) AES 키 생성

AES는 우리가 이전에 봤던 비대칭키 알고리즘과 달리 대칭키 알고리즘이기 때문에 하나의 비밀키만 생성하면 된다. 그리고 IV(Initial Vector)에 따라 같은 키로 같은 평문을 암호화 하더라도 결과가 달라지게 된다.

결국 암호화/복호화 과정을 위해서는 대칭키, IV가 필요하다

final class AES256 {
  private let dataStorage: KeyChainDataStorage
  private var key: SymmetricKey?
  private var iv: AES.GCM.Nonce?

  init(
    dataStorage: KeyChainDataStorage
  ) {
    self.dataStorage = dataStorage
    generateKey()
  }

  /// Make AES256 RandomKey, 16 Byte inital vector
  private func generateKey() {
    let refStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    var randomStr = ""
    for _ in 0..<16 {
      randomStr += String(refStr.randomElement()!)
    }
    let randomIV: [UInt8] = Array(randomStr.utf8)

    do {
      self.key = SymmetricKey(size: .bits256)
      self.iv = try AES.GCM.Nonce(data: randomIV)
    } catch let err {
      print("Make IV Error:", err.localizedDescription)
    }
  }
}

AES256을 사용하면 32Byte의 랜덤한 키, 16Byte의 랜덤한 IV가 생성된다.

일반적으로 String 타입 하나당 1Byte의 크기를 가지고 있기 때문에 32, 16의 길이를 가지게 된다.

위의 코드에서 굳이 String의 범위를 09, azA~Z로 둔 이유는 utf8 변환시 해당 범위가 아닌 경우 가변적으로 한 글자당 2Byte의 크기를 사용할 수 있기 때문에 의도적으로 길이를 제한했다.


2) Encrypt, Decrypt

/// AES - Encrypt plain string to rsa base64 String
  func encryptByAES(
    plainString: String
  ) -> AES.GCM.SealedBox? {
    guard
      let key = key,
      let iv = iv,
      let data = plainString.data(using: .utf8),
      let sealedBox = try? AES.GCM.seal(data, using: key, nonce: iv)
    else { return nil }

    return sealedBox
  }

Encryption의 경우 앞서 생성한 대칭키와 iv를 통해서 암호화를 진행하는 것을 확인할 수 있다.

여기서 중요한 것은 암호화 결과로 나오는 Type이 AES.GCM.SealedBox라는 것이다.

/// AES - Decrypt rsa base64 String to plain String
  func decryptByAES(
    encryptedBox: AES.GCM.SealedBox
  ) -> String? {
    guard
      let key = key,
      let sealedBox = try? AES.GCM.SealedBox(
        nonce: encryptedBox.nonce,
        ciphertext: encryptedBox.ciphertext,
        tag: encryptedBox.tag
      ),
      let decryptedData = try? AES.GCM.open(sealedBox, using: key)
    else { return nil }

    return String(data: decryptedData, encoding: .utf8)
  }

복호화 과정에서는 파라미터 타입이 AES.GCM.SealedBox인것을 확인할 수 있다.

그리고 AES.GCM.SealedBox 를 생성할때, tag가 들어가는데 이는 Encryption을 통해 생성된 AES.GCM.SealedBox 의 tag와 동일해야 한다.

이런 문제점 때문에 Encryption의 결과값을 String이 아닌 AES.GCM.SealedBox 로 사용했다.

클라이언트에서만 AES 알고리즘을 사용하는 경우에는 문제가 되지 않지만 서버와 통신하는 경우에는 서버와AES.GCM.SealedBox 타입을 주고받지 않고서는 서로 암호화/복호화가 불가능해지기 때문에 치명적인 문제가 될 수 있다.

물론 상황에 따라 다르겠지만 일반적으로는 Base64String, HexString 타입으로 암호화 결과값을 주고받기 때문에 라이브러리를 사용하는것을 추천한다.


3) 키 전송

대칭키의 경우 서버에 키를 어떻게 안전하게 보낼것인가? 도 굉장히 중요한 문제이다.

물론 클라이언트 개발자 혼자서 고민할 문제는 아니지만 일반적으로는 대칭키를 비대칭키를 통해 암호화 하고 비대칭키 공개키를 함께 보내서 서버에서 복호화해서 키를 얻어내는 방식을 많이 사용하고 있는것 같다.

(사실 그것밖에 안해봄)

스토리보드와 Xib

스토리보드와 xib

일반적으로 스토리보드에서 여러 뷰 컨트롤러들을 만들어서 앱의 화면과 전체적인 플로우를 구성하고 table view cell, collection view cell, custom view들을 xib파일로 만들어서 사용하는데 이 둘은 정확하게 어떤 차이가 있는 것일까?

Untitled Untitled 1

우선 컴파일 이후 생성되는 파일을 확인해보면 xib 파일은 nib으로 스토리보드는 stroyboardc로 변경되는 것을 확인할 수 있다.

storyboardc 파일에 생성된 저 의문의 nib파일들은 무엇일까?

<viewController storyboardIdentifier="Test" id="hef-Go-4NS" customClass="TestViewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">

<viewController id="BYZ-38-t0r" customClass="ViewController">

안타깝게도 nib파일을 열어보는건 유로 프로그램이라 확인을 해보지 못했지만 스토리보드 파일을 소스코드로 확인해보면 viewcontroller와 view의 ID로 파일명이 생성된것을 확인할 수 있다.

Untitled 2

storyboardc 파일에 함께 생성되는 info.plist 파일을 확인해보면 아래 3개의 파일명이UIViewControllerIdentifiersToNibNames인 것을 확인할 수 있다. 그리고 해당 value 값은 storyboard ID의 값과 동일한것도 확인할 수 있다.

@IBAction func mainButtonTapped(_ sender: UIButton) {
    print("Touch Event")
    let sb = UIStoryboard(name: "Main", bundle: nil)
    let vc = sb.instantiateViewController(withIdentifier: StaticTableViewController.storyboardID)
    navigationController?.pushViewController(vc, animated: true)
  }

그럼 이제 스토리보드에서 Segue가 아닌 navigation push로 구현할 때 identifier를 설정해야 했던 이유를 명확히 알 수 있다!

그럼 xib 파일을 컴파일 했을때도 nib파일이 생성되고 스토리보드를 컴파일 했을 때도 nib파일들과 info.plist 파일이 생성되는데 그 둘의 차이는 무엇일까?

// SB
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB">
// Xib
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB">

스토리보드와 xib파일을 모두 소스코드로 확인해보면 xml로 표현된 xib파일임을 알 수 있다.

그럼 스토리보드와 xib는 무엇이 다른걸까?

Untitled 3

우리가 일반적으로 xib파일로 생성하는 cell, custom view들은 하나의 뷰 정보만 포함하고 있는 반면 스토리보드는 여러 뷰 컨트롤러 xib파일들과 뷰 관계정보들을 포함하고 있는 파일로 구성되어 있다

HTTP 웹 기본 지식 - 4

HTTP - Method

클라이언트 개발자로 백엔드 개발자가 만들어준 API를 쓰면서 대충 알고 있던 HTTP Method를 제대로 정리해보자


API 설계

  • 회원 목록 조회
  • 회원 조회
  • 회원 등록
  • 회원 수정
  • 회원 삭제

위와 같은 기능을 가진 API URL 만들어달라는 요청을 받았을 때 URL을 어떻게 만들어야 할까?

  • 회원 목록 조회 /read-member-list
  • 회원 조회 /read-member-by-id
  • 회원 등록 /create-member
  • 회원 수정 /update-member
  • 회원 삭제 /delete-member

위와 같이 네이밍을 통해서 URL을 설계하면 어떨까? URL 형태만으로 우리가 원하는 동작 자체를 알 수 있으니까 괜찮지 않을까?

결론부터 말하면 NO이다. URI를 설계할 때 가장 중요한 포인트는 리소스 식별이다.

위의 모든 기능에서 회원 에 대한 작업을 하고 있음을 알 수 있고 회원 = 리소스임을 알 수 있다. 따라서 회원이라는 리소스를 URI에 매핑 시켜서 식별할 수 있는 형태로 구성되어야 한다.

  • 회원 목록 조회 /members
  • 회원 조회 /members/{id}
  • 회원 등록 /members/{id}
  • 회원 수정 /members/{id}
  • 회원 삭제 /members/{id}

그럼 회원 조회, 등록, 수정, 삭제와 같은 행위는 어떻게 구분해야 하는 걸까?

→ 이때 사용되는 것이 HTTP Method이다.

간단하게 정리하자면

  • URI 설계시 우선적으로 리소스와 행위를 구분해야 한다
  • 리소스를 식별할 수 있도록 URI를 만들고
  • 행위를 기준으로 URI의 메서드를 설계한다

HTTP Method

  • GET

  • POST

  • PUT

  • PATCH

  • DELETE

  • HEAD

  • OPTIONS

  • CONNECT

  • TRACE

메서드의 종류는 위와 같고 아래 4개는 한번도 본적이 없었는데 존재 정도만 알고 넘기면 될것 같다.


GET

  • 리소스 조회
  • 서버에 데이터 전달 시 query 파라미터 사용
  • 메시지 바디를 이용해서 서버에 데이터를 넘길 수도 있지만 HTTP 버전마다 다르다.

POST

  • 요청 데이터를 처리
  • 메시지 바디를 통해 서버로 데이터 전달

POST는 요청 데이터 처리뿐만 아니라 새로운 리소스를 등록하는 등 거의 모든 역할을 수행할 수 있다.

PUT

  • 리소스를 완전히 대체(=덮어쓰기)
    • 리소스가 있으면 대체
    • 리소스가 없으면 생성
  • 클라이언트가 리소스 위치를 알고 URI를 지정

이전 면접에서 PUT, POST의 차이점에 대해서 질문 받은 적이 있었는데 이제 이렇게 답변할 수 있을 것 같다.

둘 다 서버에 어떤 리소스를 (대체, 생성) 할 때 사용할 수 있다는 공통점이 있지만 POST의 경우 URI에 해당 리소스의 위치를 모르고 PUT은 URI에 해당 리소스 위치를 명시해야 한다는 차이가 있다.

POST/members
Content-Type: json
{
	"username": "young",
	"age": 20
}
// members/100에 해당 데이터를 이용해서 생성

PUT/members/100
Content-Type: json
{
	"username": "young",
	"age": 20
}
// members/100를 해당 데이터로 대체

그리고 완전히 대체 한다는 의미는 해당 위치의 리소스를 일부분만 변경할 수 없음을 의미한다.

// members/100(DB)
{
	"username": "young",
	"age": 20
}

PUT/members/100
Content-Type: json
{
	"age": 30
}
// members/100를 해당 데이터로 대체

// members/100(DB)
{
	"age": 30
}

위와 같이 age의 값만 부분 수정할 수는 없고 완전히 대체해서 username의 값이 사라진다.

PATCH

  • 리소스 부분 변경
  • PATCH가 없는 HTTP인 경우 POST를 사용

위의 PUT에서 부분 변경이 불가능했던 부분을 PATCH를 통해서 해결할 수 있다.

DELETE

  • 리소스 제거

HTTP Method 속성

  • 안전: 호출해도 리소스가 변경되지 않는지?
  • 멱등: 1번 호출과 n번 호출의 결과가 같은지?
  • 캐시가능: 응답 결과 리소스를 캐시해서 사용해도 되는지?

GET, POST, PUT, PATCH, DELETE 5개의 메서드를 기준으로 3개의 속성값이 어떻게 될까?

Safe

POST, PUT, PATCH, DELETE 메서드는 호출할 때 마다 리소스가 변경될 수 있는 가능성이 있지만 GET은 리소스를 조회만 하기 때문에 안전하다

Idempotent

  • GET: 1번 조회하나 n번 조회하나 결과값이 동일하다
  • PUT: 1번 대체한 이후부터 이후에 동일한 요청을 계속해도 결과값은 동일하다
  • DELETE: 1번 삭제 이후부터 N번 요청해도 결과값은 여전히 삭제된 상태로 동일
  • POST: 결제 요청을 가정하면 1번 요청과 n번 요청의 결과는 다르다 (중복 결제 발생)

멱등성이 있다는 것을 클라이언트에서 서버가 정상 응답을 주지 못했을때, 클라이언트가 동일한 요청을 자동으로 해도 되는지 (n번)에 대한 기준이 된다.

Cacheable

GET, HEAD, POST, PATCH 4가지가 이론상 캐싱 가능하다 하지만 POST, PATCH는 메시지 바디 까지 캐시 키로 고려해야 하기 때문에 일반적으로 GET, HEAD 2개만 캐싱에 이용하고 있다.

[WKWebView] Dynamic Height in SwiftUI

WKWebView Dynamic Height

프로젝트 구현사항 중 서버로 부터 받은 html을 WebView를 통해서 보여줘야했는데 이때 겪었던 이슈들을 정리해보고자 한다.
기본적으로 WKWebView를 SwiftUI에서 제공해주지 않기 때문에 UIViewRepresentable 프로토콜을 이용해서 구현해야 하기 때문에 우선 UIKit으로 먼저 구현해보고자 한다.

import UIKit
import WebKit

class ViewController: UIViewController {
  let webView = WKWebView()

  override func viewDidLoad() {
    super.viewDidLoad()
    setupView()
    setupConstraints()
    setupWebView()
  }

  func setupView() {
    webView.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = .white
    view.addSubview(webView)
  }

  func setupConstraints() {
    NSLayoutConstraint.activate([
      webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
      webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      webView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
    ])
  }

  func setupWebView() {
    let testHTML =
    """
    ... HTML ...
    """
    webView.navigationDelegate = self
    webView.loadHTMLString(testHTML, baseURL: nil)
  }
}

위와 같이 webView의 height constraints를 설정하지 않고, html을 로드하면 어떻게 될까?
사실 처음에는 top, leading, trailing constraints만 설정해주면 height은 webView에서 load한 html을 자동으로 먹지 않을까? 생각했는데 어림도 없었다.

실제로 처음에 webView에 height을 설정하면 해당 크기 만큼의 WebView만 생성되고 만약 실제 load되는 html이 해당 크기보다 크다면 스크롤 되는 것을 확인할 수 있다.

스크롤 없이 html이 load 되는 순간에 높이를 계산해서 dynamic 하게 height을 설정하려면 어떻게 해야할까?

결론 부터 얘기하자면, WKWebView Delegate 중 정확하게 html이 load되는 시점에 호출되는 메서드는 존재하지 않는다.

WKWebView이전의 UIWebView에는 webViewDidFinishLoad라는 메서드를 통해서 가능했지만 어째서 인지 WKWebView에는 없다...

그래도 스택오버플로우에서 어떻게든 구현한 사람들의 방법을 이용해서 야매(?)로라도 이를 구현해보고자 한다.
스택 오버플로우
에서 제시하는 방법은 크게 두 가지가 존재한다.

  • DispatchQueue.main.asyncAfter
  • evaluateJavaScript(""document.readyState")

요약하면 WKNavigationDelegate에 존재하는 didFinish 시점에 + 두 가지 방법을 이용해 scrollView의 content height을 계산해 이를 webView의 height으로 바꿔치기 하는 방법이다.

사실 두번째 방법은 js를 잘 모르는 나로서는 해당 스크립트가 정확히 어떤 시점에 호출되는지 몰라 정확한 방법인지 아직 판단을 하지 못했고,
첫번째 방법은 didFinish가 호출되는 시점으로 부터 n초 후에는 html load가 완료되었을 것이라는 막연한 믿음을 가지고 구현하는 방법이기 때문에 개인적으로 두 방법 모두 근본적인 해결 방법은 아니라고 생각한다. (근본적인 해결방법을 찾게 되면 다시 또 정리할 예정이다.)

extension ViewController: WKNavigationDelegate {
  func webView(
    _ webView: WKWebView,
    didFinish navigation: WKNavigation!
  ) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
      guard let self = self else { return }
      NSLayoutConstraint.activate([
        self.webView.heightAnchor.constraint(equalToConstant: self.webView.scrollView.contentSize.height)
      ])
    }
  }
}

첫번째 방법을 이용해서 구현하면 위와 같다. (0.3초 안에 load가 완료되지 않는 case는 위와 같은 방법으로 해결 할 수 없다)

위의 WebView를 SwiftUI에서 구현해보자

struct HtmlView: UIViewRepresentable {
  let htmlContent: String
  let webView = WKWebView()

  @ObservedObject
  var viewModel: HTMLStringViewModel

  func makeUIView(context: Context) -> WKWebView {
    webView.scrollView.isScrollEnabled = false
    webView.scrollView.panGestureRecognizer.isEnabled = false
    webView.scrollView.delegate = context.coordinator
    webView.navigationDelegate = context.coordinator
    webView.isUserInteractionEnabled = false
    webView.loadHTMLString(htmlContent, baseURL: nil)
    return webView
  }

  func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<HtmlView>) {
    guard context.coordinator.webView == nil else { return }
    context.coordinator.webView = uiView
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(parent: self)
  }

  class Coordinator: NSObject {
    var webView: WKWebView?
    var parent: HtmlView

    init(parent: HtmlView) {
      self.parent = parent
    }
  }
}

extension HtmlView.Coordinator: UIScrollViewDelegate {
  func viewForZooming(in scrollView: UIScrollView) -> UIView? {
    return nil
  }
}

extension HtmlView.Coordinator: WKNavigationDelegate {
  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
      guard let self = self else { return }
      self.parent.viewModel.heightSubject.send(webView.scrollView.contentSize.height)
    }
  }
}

final class HtmlViewModel: ObservableObject {
  let heightSubject: PassthroughSubject<CGFloat, Never>

  init(_ sub: PassthroughSubject<CGFloat, Never>) {
    self.heightSubject = sub
  }
}

SwiftUI에서는 View가 구조체 타입이기 때문에 Delegate 패턴을 Coordinator를 통해 채택한다는 것만 제외하면 기본적인 구현과정은 사실 UIKit과 완전히 동일하다.

위의 내용을 구현할 때, HtmlView를 선언하는 부분에서 .frame() modifier를 통해서 높이를 dynamic하게 설정하기 위해서 Subject를 사용했다.
말로만 설명하면 되게 헷갈릴 수 있으니 코드와 함께 확인해보자

// View 내부
HtmlView(
  htmlContent: ---html---,
  viewModel: HtmlViewModel(viewModel.input.---sub)
)
.frame(height: viewModel.output.testHeight)
// View의 ViewModel 내부
input.--tSub
      .sink(receiveValue: { [weak self] in
        guard let self = self else { return }
        self.output.testHeight = $0
      })
      .store(in: &cancellables)

정리하면 아래와 같다.

  1. HtmlView를 사용하는 View의 ViewModel에서 subject를 HtmlViewModel로 subject를 넘겨준다.
  2. HtmlView의 Coordinator에서 HtmlViewModel의 subject로 load된 html의 높이를 넘겨준다.
  3. View의 ViewModel에서 subject를 구독하고 있기 때문에, 값이 들어오면 @published output 값으로 dynamic한 height을 넘겨주고, View는 이 값을 통해 View를 re-rendering 한다.

여전히 n초 이내에 html이 load 되지 않는 webView에 대해서는 불가능 하다는 큰 문제가 남아 있지만, 해당 케이스를 제외하면 모두 정상적으로 동작하는 것을 확인할 수 있다. (실제 프로젝트에서 webView가 그리는 영역이 굉장히 작기 때문에 문제가 되진 않지만 언제든지 문제가 될 수 있다.)

우선, 위와 같이 문제를 해결했는데 여전히 찝찝한 부분들이 남아있다. 하지만 아직까지 완벽한 방법은 찾지 못한 상태이다 ㅠ
혹시라도 더 좋은 방법이 떠오르거나 찾게 되면 다시 정리할 예정이다

이 글을 보다 더 좋은 아이디어가 있으신 분들은 [email protected]으로 연락주시면 정말 진짜 정말 진짜 감사합니다.

HTTP 웹 기본 지식 - 8

HTTP 헤더 - 캐시

캐시

  • 캐시는 데이터나 리소스를 일시적으로 저장하여 반복적인 요청에 대한 응답 속도를 향상시키는 메커니즘
  • 데이터나 값을 미리 복사해 놓는 임시 장소

처음에 캐싱을 한다는 의미를 캐싱할 정보를 캐시 메모리에 올린다고만 생각했는데 캐시 메모리는 이런 역할을 하는 물리적 장치를 의미하는 것이고 캐싱 자체는 메모리(메모리 캐시)나 디스크(디스크 캐시) 어디에서든 할 수 있다.

HTTP 통신에서 캐시를 이용하는 경우를 확인해보자
Untitled

클라이언트가 서버에게 용량이 큰 이미지를 요청했다. 1분후에 클라이언트가 동일한 이미지를 요청하면 어떻게 될까? 서버는 다시 동일한 이미지를 클라이언트에게 응답값으로 준다.

  • 데이터가 변경되지 않았음에도 데이터를 다운받아야 한다
  • 비용이 증가한다
  • 느린 유저 경험(카카오톡 프로필을 눌러 프로필 화면이 나올때마다 데이터를 다운 받으면 ?)

캐시를 통해서 이런 문제를 해결할 수 있다.
Untitled 1

Untitled 2

처음 서버에서 클라이언트에게 응답을 줄 때 캐시 유효 시간을 주면 해당 시간이 초과되기 전까지는 네트워크 통신없이 캐시에 저장된 데이터를 사용한다. 그리고 유효시간이 끝난 경우에는 다시 클라이언트에서 서버로 이미지를 요청한다.

여기서 성능을 더 올릴 수 있는 방법이 없을까? 캐시 유효시간이 초과됐지만 서버에서 리소스가 변경되지 않았을 경우에는 여전히 서버로 부터 이미지를 다시 다운 받을 필요가 없지 않을까?

→ 조건부 요청을 통해서 해결


검증 헤더와 조건부 요청

캐시 유효시간이 끝나서 서버에 다시 요청을 하면

  • 서버에서 기존 데이터를 변경함 ⇒ 캐시 데이터 사용 불가능
  • 서버에서 기존 데이터를 변경하지 않음 ⇒ 캐시 데이터 사용 가능

우리는 후자의 경우에는 여전히 캐시 데이터를 사용해서 네트워크 통신을 줄이고 싶다. 그럼 캐시에 저장된 데이터와 서버의 데이터가 같다는 것을 어떻게 확인 할 수 있을까?

Untitled 3

처음 서버에서 클라이언트에게 캐시 유효기간과 데이터가 마지막으로 수정된 시간 값을 함께 준다.

Untitled 4

이제 캐시 유효시간이 끝난 시점에 클라이언트는 서버에게 데이터가 마지막으로 수정된 시간을 헤더에 넣어서 같이 요청한다.

Untitled 5

그럼 서버에서 해당 리소스의 변경 시점과 이를 비교해서 리소스의 변경이 있었다면 다시 응답으로 해당 리소스를 넘겨주고 변경이 없는 경우에는 304 상태코드를 통해서 클라이언트에 저장된 데이터를 재사용 해도 됨을 알려주고 캐시 유효기간을 갱신시켜준다.

이때 물론 요청-응답 과정이 있기 때문에 네트워크 통신이 없는것은 아니지만 HTTP 메시지에 기존에 다운 받던 데이터가 없기 때문에 용량이 매우 작아진다.


위의 방법을 사용하면 마지막으로 수정된 시간을 기준으로 데이터 변경 유무를 파악하는데 실제 데이터는 변경되지 않았는데 수정된 시간만 바뀐 경우에는 데이터를 다시 다운받는 문제가 발생한다. 또는 실제로 주석같은 정보만 바뀌고 크게 영향이 없는 변경만 있던 경우도 기존 캐시에 저장된 데이터를 재활용하지 못한다.

이런 문제점들을 해결하기 위해서 Last-Modified가 아닌 ETag를 사용한다.

Entity-Tag는 데이터에 매칭 되는 이름으로 파일을 기준으로 해시를 생성한다. (물론 임의의로 설정할 수도 있음)

Untitled 6 Untitled 7

이제 캐시 유효시간이 초과된 경우 변경된 시간 기준이 아닌 ETag값을 기준으로 캐시 데이터가 서버의 데이터가 동일한지를 판단할 수 있다.


검증헤더와 조건부 요청

  • 서버 → 클라이언트 (검증헤더)
    • 캐시 데이터와 서버 데이터가 같은지 검증하는 데이터
    • ETag, Last-Modified
  • 클라이언트 → 서버 (조건부 요청)
    • 현재 캐시에 저장된 데이터를 재활용 해도 되는지에 따른 분기
    • If-Modified-Since: Last-Modified
    • If-None-Match: ETag

캐시 제어 헤더

Cache-Control

  • Cache-Control: max-age
    • 캐시 유효 시간, 초 단위값
  • Cache-Control: no-cache
    • 데이터는 캐시해도 되지만, 항상 워서버에 검증
  • Cache-Control: no-store
    • 민감한 정보가 있기 때문에 캐시하면 안됨
  • Cache-Control: must-revalidate
    • 캐시 유효시간 만료후 최초 조회시 원서버에 검증
    • 원서버 접근 실패시 504 (no-cache와의 차이점)
  • Cache-Control: public
    • 응답이 public 캐시에 저장되도 됨
  • Cache-Control: private
    • 응답이 사용자만을 위한값으로 private(기본값)

Pragma

HTTP 1.0 하위 호환을 위해서 사용

// 확실한 캐시 무효화
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache

Expires

캐시 만료일 지정(날짜), 유연한 Cache-Control: max-age(초단위) 사용권장, 둘을 함께 사용하면 Expires무시

N코어 M스레드

N코어 M스레드

CPU에서 N코어의 M스레드는 물리적인 코어의 개수와 논리적인 코어의 개수를 의미한다

모든 프로그램이 실행되기 위해서는 결국 메모리에 올라와 CPU 자원을 통해 연산작업이 필요하다.

하지만 고전적인 CPU는 이런 연산작업을 수행할 수 있는게 하나밖에 없었지만 최근에는 코어의 등장으로 이런 물리적인 연산을 수행할 수 있는 유닛이 CPU내부에 여러개 존재하게 되었다.

코어의 등장에 따라서 여러 프로세스의 작업을 동시에 처리하는 병렬처리가 가능하게 되었다.

Untitled

그럼 우리가 CPU를 살 때 접하는 스레드는 무엇을 의미하는 것일까?

결론부터 이야기 하면, 이때 스레드는 OS가 인식하는 논리적인 코어를 의미한다.

예시를 통해서 스레드의 개념과 하이퍼 쓰레딩을 이해 해보자.

코어의 등장으로 코어의 개수 만큼 프로세스의 작업을 동시 처리하는 병렬 처리가 가능해졌다. 일반적으로 이런 코어의 개수가 많아지면 속도가 증가하는 것을 기대할 수 있다. (항상 그렇지 않습니다..)

그럼 여기서 한정된 자원으로 코어의 개수를 늘릴 수 있는 방법이 없을까?

여기서 등장하는 기술이 하이퍼쓰레딩이다.

하이퍼 쓰레딩이 정확하게 어떤 개념으로 동작하는지까지는 찾아보지 못했지만 
CPU가 하이퍼 쓰레딩 기술을 이용해서 OS에게 코어의 개수를 속인다 정도로 이해하면 될 것 같다.

즉 4코어 8스레드의 cpu는 실제 물리적으로는 4개의 코어를 가지고 있지만 OS상에서는 8개의 논리적 코어를 가지게 되어서 8개의 작업을 병렬적으로 실행할 수 있음을 의미한다.


해당 내용과 별개로 개인적으로 스레드라는 용어를 너무 복합적으로 사용하고 있어서 개념을 잡기 어려웠었는데 하드웨어적인 스레드와 소프트웨어적인 스레드를 구분해서 이해할 필요가 있다.

  • 소프트웨어적 스레드는 프로세스를 구성하는 실행 흐름의 단위를 의미한다.
  • 하드웨어적 스레드는 논리적인 코어의 개수를 의미한다.

참고 블로그

딥링크 - 02

왜 .onOpenURL이 동작하지 않을까?

프로젝트 레포

토이 프로젝트를 작성하고 회사 코드에 딥링크를 적용하는 과정 중에 겪은 버그를 정리하고자 한다.
(사실 거의 한달 정도 지난것 같은데 너무 정신없이 바빴다.)

결론부터 얘기하자면 SwiftUI 기반이 아닌 UIKit 기반의 RootView를 바꾸는 동작이 문제였다.

let window = UIApplication.shared
              .connectedScenes
              .flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
              .first { $0.isKeyWindow }

guard let window = window else { return }
let tabViewModel = TestTabViewModel()
window.rootViewController = UIHostingController(
  rootView: TestTabView().environmentObject(tabViewModel)
)

window.makeKeyAndVisible()
UIView.transition(
  with: window,
  duration: 0.5,
  options: .transitionCrossDissolve,
  animations: nil,
  completion: nil
)

기존에는 특정 프로세스가 끝나면 위와 같은 방식으로 RootView를 바꿔주는 방식을 이용해서 화면 전환을 하고 있었다.

정확하게 정리하면

  • Lottie를 이용한 스플래쉬 뷰를 실행
  • 해당 뷰에서 처음 앱이 실행될 때 필요한 정보를 서버로 부터 받아옴
  • 토큰이 유효한지를 확인

위의 3가지 로직을 타면서 성공 유무에 따라 시작 하는 화면을 스플래쉬 뷰에서 UIKit 기반의 RootView를 바꿔주는 방법을 사용하고 있었다.

하지만 위와 같은 방식으로는 우리가 토이 프로젝트에서 확인했던 .openURL 이 동작하지 않는다 ㅠㅠ
사실 정확한 이유와 함께 정리하고 싶었는데 밀린 일이 산더미라 우선 해결방법이라도 정리해놓고자 한다.

struct AppRootView: View {
  @StateObject
  var viewModel = AppRootViewModel()

  var body: some View {
    Group {
      switch viewModel.nextView {
      case .splash:
        SplashView()
      case .main:
        TodoTabView()
      }
    }
  }
}
final class AppRootViewModel: ObservableObject {
  enum NextView {
    case splash
    case main
//    case login
  }

  @Published var nextView: NextView = .splash

  init() {
    fetchInitInfo()
  }
}

extension AppRootViewModel {
// BL에 따라 처리
  func fetchInitInfo() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
      guard let self = self else { return }
      self.nextView = .main
    }
  }
}

토이 프로젝트라 Lottie 애니메이션이나 뷰모델의 비즈니스 로직은 전부 생략했다.

기존의 RootViewCon을 바꾸는 로직을 걷어내고 AppRootView를 만들어서 비즈니스 로직의 결과에 따라 nextView를 바꿔주는 방식을 사용했다.

HTTP 웹 기본 지식 - 5

HTTP Method 활용

클라이언트 → 서버 데이터 전송

  • 쿼리 파라미터를 통한 데이터 전송
    • GET
  • 메시지 바디를 통한 데이터 전송
    • POST, PUT, PATCH

(경험에 비추어 보면),

일반적으로 GET을 통해서 API 통신을 하면 쿼리 파라미터를 통해 필터링 옵션을 넣어주면 최종적인 URI 형태가 우리가 넣어준 쿼리 파라미터가 {key - value} 쌍으로 들어간다. (HTTP 버전에 따라 메시지 바디 사용 가능)

POST, PUT, PATCH 같은 메서드들은 클라이언트에서 서버에 보내려는 데이터를 json으로 인코딩 시켜서 메시지 바디에 넣어서 데이터 전송을 한다.


데이터 조회

  • 데이터 조회는 일반적으로 GET 메서드를 사용
  • 일반적으로 정적인 데이터를 요청하는 경우에는 쿼리 파라미터 없이 리소스 경로로 단순하게 조회 가능
  • 동적인 데이터를 조회하는 경우 쿼리 파라미터 기반으로 필터를 적용

HTML Form 데이터 전송

  • GET, POST 메서드만 사용가능

웹 프론트엔드 개발자가 아니라서 HTML Form 데이터 전송에 대해서 처음 들어봤는데 HTML Form에 사용자가 입력한 데이터를 서버에 전송하는 방식이 따로 있다 정도로 이해했다.

강의에서 해당내용을 설명 하면서 HTML Form에서 사용하는 Content-Type에 대해 설명하는 부분이 있는데 이 부분을 한번 짚고 넘어가보자

  • application/x-www-form-urlencoded
    • form의 내용을 메시지 바디를 통해 key = value 형태로 전송
    • 전송 데이터 url encoding
  • multipart/form-data
    • 바이너리 데이터 전송(업로드)
    • 다른 종류의 여러 파일 폼 전송 가능
Untitled Untitled 1

(익숙하죠..?)

Content-Type은 HTTP 요청 또는 응답의 본문(content)의 미디어 타입을 나타내는 헤더 필드로 클라이언트가 서버로 전송하는 데이터의 형식을 명시하고, 서버가 클라이언트에게 반환하는 데이터의 형식을 알려줍니다.

Content-Type: media-type

**media-type**은 MIME(Multipurpose Internet Mail Extensions) 타입으로 데이터 미디어 타입을 명시하는 문자열

요약하자면

  • Contenty-Type에 명시된 데이터 형식으로 서버 <-> 클라이언트 데이터를 주고 받는다.
  • Content-Type 헤더에 명시되는 데이터 형식은 MIME 타입이다.

HTTP API 데이터 전송

  • 앱 클라이언트 → 서버 데이터 전송하는 형식
  • 서버 → 서버
  • Content-Type은 일반적으로 application/json

지난 시간에 정리했던 HTTP API 설계의 연장선이 되는 부분인데 복습하는 겸 다시 한번 정리해보자

POST와 PUT의 차이점

  • POST와 PUT 모두 서버에 어떤 리소스를 생성(등록, 수정) 할 수 있음
  • POST는 클라이언트가 URI 경로를 모른다
    • 서버가 새로 등록된 리소스 URI 생성 → 컬렉션
  • PUT은 클라이언트 URI 경로를 알고있다.
    • 클라이언트가 직접 리소스의 URI를 지정 → 스토어

일반적으로 컬렉션 방식을 사용한다.

PUT 과 PATCH

  • PUT은 리소스를 완전하게 대체 →부분 수정 불가능
  • PATCH는 부분 수정 가능

DB의 어떤 데이터를 수정하는 API는 일반적으로 PATCH, POST를 통해서 설계하는데 PUT을 이용할 수 있지만 부분 수정이 불가능하고 모든 데이터를 다시 보내줘야 하기 때문

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.