<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 독학러의 학습블로그</title>
    <link>https://jmin-developer.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 15 Apr 2026 01:49:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JiminiOS</managingEditor>
    <image>
      <title>개발 독학러의 학습블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/4874661/attach/4a9fe26ed3894714a92152874d115cb9</url>
      <link>https://jmin-developer.tistory.com</link>
    </image>
    <item>
      <title>SOLID에 대해서 알아보자</title>
      <link>https://jmin-developer.tistory.com/67</link>
      <description>&lt;h1 id=&quot;단일-책임-원칙-single-responsibility-principle&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;단일 책임 원칙 (Single Responsibility Principle)&lt;/h1&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클래스는 한 가지 이유로 변경되어야 합니다&lt;/p&gt;
&lt;h2 id=&quot;개념&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;개념&lt;/h2&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각각의 클래스가 프로그램이 제공하는 기능의 단 하나의 부분만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;책임&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;지도록 하세요&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 원칙의 목적은 복잡성을 줄이는 것입니다&lt;br /&gt;프로그램이 성장하고 병경되면서 클래스들은 너무 커질것입니다&lt;br /&gt;그렇게 될 경우 코드 탐색은 매우 느려질것이고, 클래스나 프로그램의 전체를 훑어봐야 특정 코드를 찾을 수 있게 되겠죠&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 클래스가 너무 많은 작업을 수행 할 경우, 그 중 하나가 변경될 때마다 클래스를 변경해야 합니다. 그럴경우 변경할 생각이 없던 클래스의 다른 부분이 망가질 위험이 있습니다&lt;/p&gt;
&lt;h2 id=&quot;실생활-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;실생활 예시&lt;/h2&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신은 세계의 다양한 요리 메뉴가 있는 음식점을 오픈했습니다&lt;br /&gt;당신은 중식, 양식, 일식을 모두 다 잘하는 요리사 한명을 고용할지 각각의 요리사 세명을 고용할지 고민중입니다&lt;br /&gt;결국 당신의 선택은 요리사 세명을 고용하는 것이였습니다&lt;br /&gt;요리사한테 문제가 생겼을경우 한명일때보다는 세명일때 더욱 유연하게 대처할 수 있기 때문입니다&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;예를들면 양식 요리사한테 문제가 생겼을경우 일단 중식, 일식만 판매한다던가 양식 요리사만 교체한다든가 할 수 있겠죠&lt;br /&gt;하지만 요리사가 한명일경우에는 중식, 양식, 일식을 모두 다 할 수 있는 요리사를 새로 구하기 전까지는 장사를 못할겁니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;코드-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;코드 예시&lt;/h2&gt;
&lt;h3 id=&quot;잘못된-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;잘못된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;class Handler {
    func handle() {
        let data = fetchData()
        let newData = createNewData(data)
        uploadData(newData)
    }
    
    func fetchData() -&amp;gt; Data {
        // fetchData logic
        return Data()
    }
    
    func processingData(_ data: Data) -&amp;gt; Data {
        // processing logic
        return Data()
    }
    
    func uploadData(_ data: Data) {
        // uploadDatalogic
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 클래스는 3가지 일을 한다&lt;br /&gt;1. data fetch&lt;br /&gt;2. fetch한 데이터로 새로운 데이터 만들기&lt;br /&gt;3. 만든 새로운 데이터를 업로드&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 책임들을 각각 작은 객체로 분리하자&lt;/p&gt;
&lt;h3 id=&quot;옳게된-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;옳게된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;class Handler {
    let apiHandler: APIHandler
    let processingHandler: ProcessingHandler
    
    init(apiHandler: APIHandler, processingHandler: ProcessingHandler) {
        self.apiHandler = apiHandler
        self.processingHandler = processingHandler
    }
    
    func fetchAndUploadProcessingData() {
        let data = apiHandler.fetchData()
        let processingData = processingHandler.processingData(data)
        apiHandler.uploadData(processingData)
    }
    
}

class APIHandler {
    func fetchData() -&amp;gt; Data {
        // fetch logic
        return Data()
    }
    
    func uploadData(_ data: Data) {
        // upload logic
    }
}

class ProcessingHandler {
    func processingData(_ data: Data) -&amp;gt; Data {
        // processing logic
        return Data()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화 해야 한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스의 수정 이유는 단 하나여야 한다&lt;/li&gt;
&lt;li&gt;하나의 클래스는 하나의 책임을 가져야 한다&lt;/li&gt;
&lt;li&gt;하나의 책임이 여러개의 클래스에 나뉘어 있어서도 안된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 id=&quot;개방폐쇄-원칙-openclosed-principle&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;개방/폐쇄 원칙 (Open/Closed Principle)&lt;/h1&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;개념&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;개념&lt;/h2&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;기존의 코드를 변경하지 않으면서 기능을 추가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;할 수 있어야 합니다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 원칙의 목적은 새로운 기능을 구현할 때 기존 코드가 깨지지 않도록 하는 것입니다&lt;br /&gt;클래스는 확장할 수 있을때, 자식클래스를 생성할 수 있을때, 기초 행동을 재정의 하고 새로운 메서드 및 필드를 추가하는 등 원하는 모든 작업을 수행할 수 있을때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Open(개방)&lt;/b&gt;되어있다고 할 수 있습니다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동시에 클래스가 다른 클래스에 의해 사용될 준비가 100% 되었다면 해등 클래스는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Closed(폐쇄)&lt;/b&gt;되었다고 할 수 있습니다&lt;br /&gt;이때 사용될 준비가 되었다는 것은 클래스의 인터페이스가 명확하게 정의되어 있으며 미래에 변경되지 않는다는 뜻입니다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클래스가 이미 개발, 테스트, 검토의 단계를 마쳤고 이미 다른곳에서 사용되는 경우 해당 클래스의 변경은 위험합니다&lt;br /&gt;클래스의 변경이 필요한 경우에는 해당 클래스를 직접 변경하는것이 아닌 자식클래스를 만든 후 원하는 부분들을 재정의해야합니다&lt;br /&gt;그렇게 변하는 기능 및 새로운 기능을 추가하면서도 원래 클래스를 손상하지 않게 됩니다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단 클래스에 버그가 있을 경우 그 문제를 수정하려고 자식클래스를 만들지 말고 그냥 가서 수정하세요&lt;br /&gt;자식클래스는 부모 클래스의 문제들에 대하여 책임을 져서는 안됩니다.&lt;/p&gt;
&lt;h2 id=&quot;코드-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;코드 예시&lt;/h2&gt;
&lt;h3 id=&quot;잘못된-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;잘못된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;class Handler {
    func handle() {
        let data = fetchData()
        let newData = createNewData(data)
        uploadData(newData)
    }
    
    func fetchData() -&amp;gt; Data {
        // fetchData logic
        return Data()
    }
    
    func processingData(_ data: Data) -&amp;gt; Data {
        // processing logic
        return Data()
    }
    
    func uploadData(_ data: Data) {
        // uploadDatalogic
    }
}

class AClass {
	let handler = Handler()
    
    func handle() {
    	handler.handle()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Handler 객체는 원격 스토리지에서 파일을 fetch 해와서 사용한다&lt;br /&gt;Handler 객체를 BClass 라는 새로운 클래스에서도 사용할려고 한다&lt;br /&gt;BClass 에서는 파일을 원격 스토리지가 아닌 로컬스토리지에서 fetch 해줄려 한다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 Handler 클래스를 수정해줬다&lt;br /&gt;하지만 이는 변경에 닫혀있어야 하는 개방/폐쇄 원칙에 어긋난다&lt;/p&gt;
&lt;h3 id=&quot;옳게된-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;옳게된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;protocol Handler {
	func handle()
}

class RemoteHandler {
	func handle() {
        let data = fetchData()
        let newData = createNewData(data)
        uploadData(newData)
    }
    
    func fetchData() -&amp;gt; Data {
        // fetchData logic
        return Data()
    }
    
    func processingData(_ data: Data) -&amp;gt; Data {
        // processing logic
        return Data()
    }
    
    func uploadData(_ data: Data) {
        // uploadDatalogic
    }
}

class LocalHandler {
	func handle() {
        let data = fetchData()
        let newData = createNewData(data)
        uploadData(newData)
    }
    
    func fetchData() -&amp;gt; Data {
        // fetchData logic
        return Data()
    }
    
    func processingData(_ data: Data) -&amp;gt; Data {
        // processing logic
        return Data()
    }
    
    func uploadData(_ data: Data) {
        // uploadDatalogic
    }
}

class AnotherHandler: Handler {
	override func fetchData(_ flag: Bool) -&amp;gt; Data {
		// fetchData logic
		return Data()
	}
}

class BClass {
	let handler: Handler
    
    init(handler: Handler) {
    	self.handler = handler
	}
    
    func handle() {
    	handler.handle()
    }
}

class BClass {
	let handler: Handler
    
	init(handler: Handler) {
    	self.handler = handler
	}
    
    func handle() {
    	handler.handle()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Handler 라는 프로토콜이 있고 실제 메서드 구현은 해당 프로토콜을 채택한 구상객체에서 하게 된다&lt;br /&gt;이제 새로운 handler 메서드를 구현할경우 이미 구현된 클래스를 변경할 필요 없이 Handler 프로토콜에서 새로운 클래스를 파생할 수 있다&lt;/p&gt;
&lt;h3 id=&quot;잘못된-예시-1&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;잘못된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;struct Dog {
	let name: String
	let age: Int    
}

class Zoo {
    var animals: [Dog] = []
    
    func addAnimal(_ animal: Dog) {
    	animals.append(dog)
	}
    
    func hello() {
    	print(&quot;멍멍&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동물원 객체가 있다&lt;br /&gt;해당 동물원은 예산이 없어서 강아지밖에 없다&lt;br /&gt;따라서 현재 코드는 기능에 문제가 없다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 나중에 동물원이 돈을 많이 벌어서 새로운 동물들을 들여온다고 생각해보자&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 Zoo 객체는 강아지 타입밖에 받아들일 수 없어 필연적으로 Zoo 객체를 수정해야 한다&lt;/p&gt;
&lt;h3 id=&quot;옳게된-예시-1&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;옳게된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;protocol Animal {
	var name: String { get }
    var age: Int { get set }
    
    func hello()
}

struct Dog: Animal {
	var name: String
    var age: Int
    
    func hello() {
    	print(&quot;멍멍&quot;)
    }
}

struct Tiger: Animal {
	var name: String
    var age: Int
    
    func hello() {
    	print(&quot;어흥&quot;)
    }
}

class Zoo {
	var animals: [Animal] = []
    
    func add(_ animal: Animal) {
    	animals.append(animal)
    }
    
    func hello(_ animal: Animal) {
    	animal.hello()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Animal 프로토콜을 생성하고 실제 동물 구상 객체들이 Animal 프로토콜을 채택하게 해 주었다&lt;br /&gt;Zoo 클래스는 동물 구상 객체에 의존하지 않고 Animal 프로토콜을 의존해주도록 했다&lt;br /&gt;이제 동물원에 새로운 동물을 들여오더라 하더라도 Animal 이라는 프로토콜을 채택하고 있으면 Zoo 객체에서 받아들일 수 있기때문에 Zoo 객체를 수정해줄 필요가 없어진다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 id=&quot;확장에-열려있다&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;확장에 열려있다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈의 확장성을 보장한다&lt;/li&gt;
&lt;li&gt;새로운 변경사항이 발생할 경우 유연하게 코드를 추가함으로써 애플리케이션의 기능을 큰 힘을 들이지 않고 확장할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;변경에-닫혀있다&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;변경에 닫혀있다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 직접적으로 수정하는것은 제한해야 한다&lt;/li&gt;
&lt;li&gt;새로운 변경 사항이 발생했을때 객체를 직접 수정해야 한다면 새로운 변경사항에 대해 유연하게 대응할 수 없는 애플리케이션이다&lt;/li&gt;
&lt;li&gt;따라서 객체를 직접 수정하지 않고도 변경사항을 적용할 수 있도록 설계해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 id=&quot;리스코프-치환-원칙-liskov-substitution-principle&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;리스코프 치환 원칙 (Liskov Substitution Principle)&lt;/h1&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자식 클래스는 부모 클래스의 행동과 계속 호환되어아 햡니다&lt;/p&gt;
&lt;h2 id=&quot;개념&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;개념&lt;/h2&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;서브타입은 언제나 기반타입으로 교체&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;할 수 있어야 한다&lt;br /&gt;즉, 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용했을 때 코드가 원래 의도대로 작동해야 한다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;리스코프 치환 원칙같은 경우 여러 방식으로 해석 가능한 다른 원칙들과 달리 형식적인 요구사항이 있다&lt;/p&gt;
&lt;h3 id=&quot;1-자식-클래스의-메서드의-매개변수-유형들은-부모-클래스의-메서드의-매개변수-유형들보다-더-span-style--colorred-추상적이거나-추상화-수준이-같아야-한다-span&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 자식 클래스의 메서드의 매개변수 유형들은 부모 클래스의 메서드의 매개변수 유형들보다 더&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;추상적이거나 추상화 수준이 같아야 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;class Animal { }
class Dog: Animal { }
class Bulldog: Dog { }

class ParentClass {
	func feed(_ a: Dog) { }
}	
    
class FirstChildClass: ParentClass {
	func feed(_ a: Animal) { }
}
    
class SecondChildClass: ParentClass {
	func feed(_ a: Bulldog) { }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부모클래스는 Dog 타입에 먹이를 주는 함수가 있다&lt;br /&gt;첫번째 자식클래스에서는 매개변수로 Dog 의 상위클래스인 Animal을 받는다&lt;br /&gt;두번째 자식클래스에서는 매개변수로 Dog 의 하위클래스인 Bulldog을 받는다&lt;br /&gt;만약 부모클래스의 객체 대신 자식클래스의 객체를 전달한다고 생각해보자&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫번째 자식클래스는 모든 동물들에게 먹이를 줄 수 있으므로 클라인트가 전달하는 모든 Dog 에게 먹이를 줄 수 있다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 두번째 자식클래스에서는 매개변수를 불독으로만 제한했다&lt;br /&gt;따라서 해당 메서드는 불독 이외의 다른 종의 Dog에는 먹이를 주지 못하며 부모클래스와 호환되지 못한다&lt;/p&gt;
&lt;h3 id=&quot;2-자식클래스의-메서드의-반환-유형은-부모클래스의-메서드의-반환-유형의-span-style--colorred-하위유형이거나-일치해야-한다-span&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 자식클래스의 메서드의 반환 유형은 부모클래스의 메서드의 반환 유형의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;하위유형이거나 일치해야 한다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;class Animal { }
class Dog { }
class Booldog { }
  
class ParentClass {
	func buyDog() -&amp;gt; Dog {
		return Dog()
	}
}	
    
class FirstChildClass: ParentClass {
	func buyDog() -&amp;gt; Animal {
		return Animal()
	}
}
    
class SecondChildClass: ParentClass {
	func buyDog() -&amp;gt; Booldog {
		return Booldog()
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부모클래스에는 buyDog() 이라는 메서드가 있고 Dog을 반환한다&lt;br /&gt;첫번째 자식클래스는 Dog의 하위유형인 Animal 을 반환하고 두번재 자식클래스는 상위유형인 Bulldog 을 반환한다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫번째 자식클래스는 어떠한 동물이든 다 반환한다&lt;br /&gt;Dog을 위해 설계된 구조에 알 수 없는 다른 동물을 받기때문에 문제가 된다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 두번째 자식클래스에서는 Booldog을 반환한다&lt;br /&gt;이는 Dog이니까 아무문제 없다&lt;/p&gt;
&lt;h3 id=&quot;3-자식클래스의-메서드는-부모-클래스에서-던지지-않을거라-예상되는-span-style--colorred예외-유형에러를-던져서는-안됩니다span&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 자식클래스의 메서드는 부모 클래스에서 던지지 않을거라 예상되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;예외 유형(에러)를 던져서는 안됩니다&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 예외 유형들은 부모 메서드가 이미 던질 수 있는 예외 유형들의 하위유형 혹은 일치해야 합니다&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;enum SomeError: Error {
	case someError
}

class ParentClass {
	func doSomething(_ a: String) { }
}

class ChildClass {
	func doSomething(_ a: String) throws {
		if a &amp;gt; 10 {
			throw SomeError.someError
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예상치 못한 예외는 앱 전체를 충돌시킬 수 있다&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 현대 프로그래밍 언어들은 위 규칙들이 언어에 내장되어 있어서 해당 규칙들을 위반하는 프로그램은 컴파일 할 수 없다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터 설명하는 규칙들은 컴파일러로 못잡는 규칙들이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;4-자식클래스는-사전-조건들을-span-style--colorred-강화해서는-안된다-span&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. 자식클래스는 사전 조건들을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;강화해서는 안된다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;enum SomeError: Error {
	case someError(String)
}

class ParentClass {
    func doSomething(_ a: Int) throws {
        if a &amp;lt; 0 {
            throw SomeError.someError(&quot;음수이면 안됩니다&quot;)
        }
    }
}

class FirstChildClass: ParentClass {
    override func doSomething(_ a: Int) throws {
        if a &amp;lt;= 0 {
            throw SomeError.someError(&quot;0보다 커야합니다&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부모클래스의 doSomething 함수는 파라미터로 받은 숫자가 음수이면 안된다는 조건이 있다&lt;br /&gt;자식클래스에서 doSomething 함수를 재정의하면서 0이면 안된다는 조건이 추가됐다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 함수에 음수들이 전달될때 잘 작동하던 클라이언트 코드는 이 자식 클래스 객체와 작업하기 시작하면 문제가 생길 수 있다&lt;br /&gt;부모클래스와 동일한 수준의 조건을 기대하고 사용하는 프로그램 코드에서 예상치 못한 문제가 발생할 수 있기 때문이다&lt;/p&gt;
&lt;h3 id=&quot;5-자식클래스는-사후-조건들을-span-style--colorred약화해서는-안된다span&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5. 자식클래스는 사후 조건들을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;약화해서는 안된다&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;enum SomeError: Error {
    case someError(String)
}

class ParentClass {
	func doSomething(_ a: Int) throws -&amp;gt; Int {
		if a &amp;lt; 0 {
		throw SomeError.someError(&quot;음수이면 안됩니다&quot;)
        }        
        return a
	}
}

class ChildClass: ParentClass {
    override func doSomething(_ a: Int) throws -&amp;gt; Int {
        return a
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부모클래스의 doSomething 함수는 반환할 값이 유효한 값인지 검사하고 있다&lt;br /&gt;자식클래스는 doSomething 함수를 재정의하면서 해당 조건을 제거하여 조건을 약화시켰다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 역시 음수를 반환할거라고 예상하지 못하는 클라이언트 코드에서는 오작동을 일으킬것이다&lt;/p&gt;
&lt;h3 id=&quot;6-부모-클래스의-불변속성들은-span-style--colorred보존되어야-한다span&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6. 부모 클래스의 불변속성들은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;보존되어야 한다&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;불변속성이란 객체가 해당 객체로 이해되기 위해 갖추어야 하는 조건들이다&lt;br /&gt;즉 부모 클래스의 데이터의 값의 조건은 자식 클래스에서도 계속 유지되어야 한다는 것이다&lt;/p&gt;
&lt;pre class=&quot;haxe&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;class ParentClass {
	var num: Int = .zero
	var _num: Int {
		get {
			return num
		}
		set {
			if newValue &amp;gt;= 0 {
				num = newValue
			}
		}
	}
}

class ChildClass: ParentClass {
	func doSomething(_ a: Int) {
		num = a
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부모클래스의 num 변수는 항상 0 혹은 양수만을 가질 수 있다&lt;br /&gt;그러나 자식클래스의 doSomething 함수에서 아무런 조건없이 num에 값을 할당해주고 있다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그로인해 num 에 음수가 할당될 수 없다는 부모클래스의 불변속성이 깨져버렸다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;A가 B를 상속받았으면 B로서도 역할을 할 수 있어야 한다&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1 id=&quot;인터페이스-분리-원칙-interface-segregation-principle&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;인터페이스 분리 원칙 (Interface Segregation Principle)&lt;/h1&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트들은 자신이 사용하지 않는 메서드에 의존하도록 강요되어서는 안된다&lt;/p&gt;
&lt;h2 id=&quot;개념&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;개념&lt;/h2&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인터페이스를 잘게 분리함으로써,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;클라이언트의 목적과 용도에 적합한 인터페이스만을 제공&lt;/span&gt;&lt;/b&gt;하는 것이다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클래스 상속은 하나의 부모 클래스만 가질 수 있도록 하지만 동시에 구현할 수 있는 인터페이스(프로토콜) 의 수를 제한하지는 않는다&lt;br /&gt;따라서 서로 관련 없는 많은 메서드들을 하나의 인터페이스에 집어넣을 필요가 없으며 이를 더 정제된 인터페이스로 나눠야한다&lt;br /&gt;필요하다면 그 전부를 단일 클래스에서 구현할 수 있다&lt;/p&gt;
&lt;h2 id=&quot;코드-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;코드 예시&lt;/h2&gt;
&lt;h3 id=&quot;잘못된-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;잘못된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;protocol Gesture {
	func didTap()
    func didSwipe()
    func didScroll()
}

class Button: Gesture {
	func didTap() {
    	// tap Gesture logic
    }
    func didSwipe() {
    	// not used
    }
    func didScroll() {
    	// not used
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 인터페이스(프로토콜)에 Gesture 관련 모든 메서드들이 들어있다&lt;br /&gt;따라서 해당 인터페이스를 구현하는 구상클래스에서 해당 메서드가 필요하지 않더라 하더라도 구현을 해줘야 한다&lt;/p&gt;
&lt;h3 id=&quot;옳게된-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;옳게된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;protocol TapGesture {
	func didTap()
}
protocol SwipeGesture {
	func didSwipe()
}
protocol ScrollGesture {
	func didScroll()
}

class Button: TapGesture {
	func didTap() {
    	// tap Gesture logic
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각각의 Gesture를 분리하고 사용하는 기능을 가지고 있는 프로토콜만 채택하여 구현해주었다&lt;br /&gt;이로인해 원하는 기능만 구현이 가능해졌다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 새로운 gesture 가 필요하다면 해당 프로토콜을 채택하여 구현해주면 될것이다&lt;/p&gt;
&lt;h2 id=&quot;주의점&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;주의점&lt;/h2&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한번 인터페이스를 분리하여 구성해놓았다면 더이상 분리해서는 안된다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이미 구현되어 있는 프로젝트에 또 인터페이스를 분리한다면 이미 해당 인터페이스를 구현하고 있는 클래스 및 클라이언트에 문제가 생길 수 있다&lt;br /&gt;또한 인터페이스가 너무 많아진다면 코드는 더욱 복잡해진다는 사실을 잊지 말고 균형을 유지해야한다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 id=&quot;의존관계-역전-원칙-dependency-inversion-principle&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;의존관계 역전 원칙 (Dependency Inversion Principle)&lt;/h1&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;상위 계층 클래스들은 하위 계층 클래스들에 의존해서는 안됩니다&lt;/p&gt;
&lt;h2 id=&quot;개념&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;개념&lt;/h2&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;객체에서 어떤 Class를 참조해서 사용하는 상황이 발생한다면 그 Class를 직접 참조하는 것이 아닌 그&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;대상의 상위요소(추상클래스 or 인터페이스)로 참조&lt;/span&gt;&lt;/b&gt;해야 한다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로 클래스는 다음 두 계층으로 분류가 가능하다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;하위 계층 클래스들&lt;/b&gt;은 디스크의 작업, 네트워크 통신 등과 같은 기본 작업을 구현한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상위 계층 클래스들&lt;/b&gt;은 하위 계층 클래스들이 무언가를 하도록 지시하는 복잡한 비즈니스 로직을 포함한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새 시스템에서 개발을 시작할때 보통은 하위 계층 클래스들은 먼저 디자인 한 다음 상위 계층 클래스들을 디자인 하기 시작한다&lt;br /&gt;이는 해당 시점에 하위 계층 기능들이 구현되지 않았거나 명확하지 않기 때문에 상위 계층에서 무엇이 가능한지 확신할 수 없기 때문이다&lt;br /&gt;이러한 접근 방식은 비즈니스 로직 클래스들이 하위 계층 클래스들에 의존하게 되는 경향이 있다&lt;br /&gt;이렇게 될 경우 하위 계층에 변화가 있을 때마다 클라이언트 혹 상위 계층의 코드를 자주 수정해야 한다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;의존관계 역전 원칙은 이러한 의존 관계의 방향을 바꾸는 것이다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;1. 상위계층 클래스가 의존하는 하위계층 작업의 인터페이스를 되도록 비즈니스 용어를 사용해 설명해야 한다
2. 구상 하위 계층 클래스 대신 이러한 인터페이스에 의존하는 상위 계층 클래스들을 만들 수 있다
3. 하위 계층 클래스들이 이러한 인터페이스들을 구현하면 이들은 비즈니스 로직 계층에 의존하게 되어 원래 의존관계의 
   방향이 역전된다&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;코드-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;코드 예시&lt;/h2&gt;
&lt;h3 id=&quot;잘못된-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;잘못된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;class Bus { 
	var str = &quot;버스&quot;
}

class Commute {
	var bus: Bus?
    
	func boarding(bus: Bus) {
    	self.bus = bus
    }
    
    func goToWork() {
    	if let bus = bus {
    		print(&quot;\(bus.str)을(를) 타고 출근&quot;)
        } else {
        	print(&quot;걸어서 출근&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 상황에서 Commute(상위계층)은 Bus(하위계층)라는 구체적인 객체에 의존하고 있다&lt;br /&gt;만약 버스를 놓쳐서 택시를 타는경우 Commute 클래스를 수정해줘야 한다&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;class Bus {
	var str = &quot;버스&quot;
}

class Taxi {
	var str = &quot;택시&quot;
}

class Commute {
	var bus: Bus?
    var taxi: Taxi?
    
    func boarding(bus: Bus) {
    	self.bus = bus
    }
    
    func boarding(taxi: Taxi) {
    	self.taxi = taxi
    }
    
    func goToWork() {
    	if let bus = bus {
        	print(&quot;\(bus.str)을(를) 타고 출근&quot;)
        } else if let taxi = taxi {
			print(&quot;\(taxi.str)을(를) 타고 출근&quot;)
        } else {
        	print(&quot;걸어서 출근&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Commute라는 상위계층 클래스가 변하기 쉬운 하위계층에 의존하게 되면 하위계층 변화의 영향에 직접적으로 노출된다&lt;br /&gt;따라서 탈 수 있는 대중교통이 늘때마다 Commute클래스는 계속해서 수정될것이다&lt;/p&gt;
&lt;h3 id=&quot;옳게된-예시&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;옳게된 예시&lt;/h3&gt;
&lt;pre class=&quot;swift&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;protocol PublicTransport {
	var str: String { get }
}

class Bus: PublicTransport {
	var str = &quot;버스&quot;
}

class Taxi: PublicTransport {
	var str = &quot;택시&quot;
}

class Commute {
	var publicTransport: PublicTransport?
    
    func boarding(publicTransport: PublicTransport) {
    	self.publicTransport = publicTransport
    }
    
    func goToWork() {
    	if let pt = publicTransport {
        	print(&quot;\(publicTransport.str)을(를) 타고 출근&quot;)
        } else {
        	print(&quot;걸어서 출근&quot;)
        }
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;더이상 Commute 클래스는 구체적인 객체들에 의존하지 않는다&lt;br /&gt;대신 PublicTransport라는 추상적인 개념에 의존하고 있다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 탈수있는 대중교통이 늘더라도 하위계층 클래스가 PublicTransport를 채택하고 있는 이상 Commute 객체를 수정하지 않더라도 문제가 없을것이다&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존관계 역전 원칙은 종종 개방/폐쇄 원칙과 함께 진행된다&lt;br /&gt;당신은 하위 계층 클래스를 확장하여 기존 클래스들을 손상하지 않고 다른 비즈니스 로직 클래스들과 함께 사용할 수 있다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하위 계층의 클래스가 상위 계층의 추상화에 의존하는것이 의존관계 역전 원칙의 핵심이다&lt;/p&gt;</description>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/67</guid>
      <comments>https://jmin-developer.tistory.com/67#entry67comment</comments>
      <pubDate>Thu, 27 Jun 2024 06:53:30 +0900</pubDate>
    </item>
    <item>
      <title>SwiftData에 대해서 간단히 알아보자</title>
      <link>https://jmin-developer.tistory.com/66</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;WWDC2023에서 새롭게 발표된 SwiftData를 간단하게 사용해봤습니다. 설명이 자세하지 못하거나 부정확할 수 있습니다&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftData는 Xcode beta 15.0 이상, iOS beta 17.0, Swift 5.9 이상부터 사용 가능합니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;1-swiftdata란&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;1. SwiftData란?&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duIhza/btsIe3lxFqw/3XPPVaprdpEHhpy2hGXhMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duIhza/btsIe3lxFqw/3XPPVaprdpEHhpy2hGXhMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duIhza/btsIe3lxFqw/3XPPVaprdpEHhpy2hGXhMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduIhza%2FbtsIe3lxFqw%2F3XPPVaprdpEHhpy2hGXhMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;407&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앱이 생성하거나 소비하는 데이터를 모델링하는 여러 사용자 지정 유형을 정의할 가능성이 높습니다. 예를 들어, 여행 앱은 여행, 항공편 및 예약된 숙박 시설을 나타내는 클래스를 정의할 수 있습니다. SwiftData를 사용하면 해당 데이터를 빠르고 효율적으로 유지하여 앱 실행에서 사용할 수 있으며, SwiftUI와 프레임워크의 통합을 활용하여 해당 데이터를 다시 가져와 화면에 표시할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설계에 따라, SwiftData는 기존 모델 클래스를 보완합니다. 이 프레임워크는 스위프트 코드에서 앱의 스키마를 표현적으로 설명할 수 있는 매크로 및 속성 래퍼와 같은 도구를 제공하여 모델 및 마이그레이션 매핑 파일과 같은 외부 종속성에 대한 의존성을 제거합니다.&lt;/p&gt;
&lt;h1 id=&quot;2-예시&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;2. 예시&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 예시에서는 SwiftData 를 이용해서 간단한 todo 앱을 만들어 볼거다&lt;br /&gt;SwiftUI 및 UIKit 에서의 사용법을 알아볼것이며 먼저 SwiftUI로 설명 후 UIKit에서의 차이점을 설명하겠다&lt;/p&gt;
&lt;h2 id=&quot;21-swiftui&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2.1 SwiftUI&lt;/h2&gt;
&lt;h3 id=&quot;211-model&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.1.1 Model&lt;/h3&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;model은 macro이다&lt;br /&gt;macro에 대한 설명은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/&quot;&gt;여기&lt;/a&gt;서 볼 수 있다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모델클래스는 자동으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;PersistentModel&lt;span&gt;&amp;nbsp;&lt;/span&gt;및&lt;span&gt;&amp;nbsp;&lt;/span&gt;Observable&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로토콜을 채택한다&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PersistentModel 프로토콜은 SwiftData가 Swift 클래스를 저장된 모델로 관리할 수 있는 인터페이스이며 AnyObject를 채택하고 있다&lt;br /&gt;&lt;br /&gt;따라서 @model 은 struct에서 사용하지 못한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;lasso&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Model
class TodoItem {
    let id: UUID
    
    var title: String
    var isCompleted: Bool
    var createdAt: Date
    
    init(title: String, isCompleted: Bool, createdAt: Date) {
        self.id = UUID()
        self.title = title
        self.isCompleted = isCompleted
        self.createdAt = createdAt
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;212-configure-model-storage&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.1.2 Configure Model Storage&lt;/h3&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앱은 런타임에 어떤 모델을 유지하고 기본 스토리지에 사용할 구성을 알아야 한다&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@main
struct SwiftDataExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [TodoItem.self])
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본 스토리지를 설정하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;modelContainer를 사용한다&lt;br /&gt;모든 중첩된 뷰가 제대로 구성된 환경을 사용할 수 있도록 뷰 계층의 맨 위에 추가해야 한다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;modelContainer(for:inMemory:isAutosaveEnabled:isUndoEnabled:onSetup:)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;for modelType: 모델 컨테이너를 만드는 데 사용되는 스키마를 정의하는 모델 유형&lt;/li&gt;
&lt;li&gt;inMemory: 컨테이너가 데이터를 메모리에만 저장해야 하는지여부
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 컨테이너는 모델 데이터를 디스크에 지속적으로 저장한다. 만약 앱 수명동안 메모리에만 데이터를 저장할려면 해당 파라미터에 false를 전달해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;isAutosaveEnabled: 자동 저장을 활성화할려면 true
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;context에 변화가 생겼을때 디스크에 자동저장된다. 만약 해당 파라미터에 false가 전달된다면 디스크에 저장하기 위해 save() 함수를 호출해 줘야 할것이다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;onSetup: 컨테이너 생성이 성공 or 실패했을 때 호출되는 콜백&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container는 한 번만 생성된다&lt;br /&gt;뷰가 처음 생성된 후 modelType 및 inMemory 파라미터에 전달되는 새로운 값은 무시된다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;213-fetch-models&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.1.3 Fetch Models&lt;/h3&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;런타임에 모델 클래스의 인스턴스를 관리하려면 모델 데이터와 모델 컨테이너와의 조정을 담당하는 객체인&lt;span&gt;&amp;nbsp;&lt;/span&gt;model Context&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 있어야 한다&lt;br /&gt;SwiftUI에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Environment(\.modelContext)를 사용하면 된다&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Environment(\.modelContext) private var context&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SwiftData는 모델 데이터를 가져오기를 수행하기 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;Query PropertyWrapper&lt;span&gt;&amp;nbsp;&lt;/span&gt;및&lt;span&gt;&amp;nbsp;&lt;/span&gt;FetchDescription&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 제공한다&lt;br /&gt;FetchDescription은 밑에 UIKit에서 알아보겠다&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Query(sort: [SortDescriptor&amp;lt;TodoItem&amp;gt;(\TodoItem.title, order: .forward), SortDescriptor&amp;lt;TodoItem&amp;gt;(\TodoItem.createdAt, order: .forward)], animation: .default) var allTodoItems: [TodoItem]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;sort는 SortDescriptor 의 배열이며 모델을 가져올때 정렬조건이다&lt;br /&gt;정렬조건이 하나일 경우에는&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;@Query(sort: \.title, order: .forward, animation: .default) var allTodoItems: [TodoItem]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게도 사용 가능하다&lt;br /&gt;@Model 메크로는 클래스에 Observable 프로토콜을 채택하므로 가져온 인스턴스에 변경이 발생하면 뷰를 새로 고칠 수 있다&lt;/p&gt;
&lt;h3 id=&quot;214-save-models&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.1.4 Save Models&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;let todo = TodoItem(title: todoTitle, isCompleted: isCompleted, createdAt: Date())
context.insert(todo)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;todo 인스턴스를 생성 후 context에 insert 해준다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 2.1.2 과정에서 isAutosaveEnabled를 true 로 해주었다면 컨텍스트는 인스턴스의 변경사항을 자동으로 추적하고 디스크에 저장할 것 이다&lt;br /&gt;만약 false로 해주었다면 적절한 시점에 save() 메서드를 호출하여 저장해주면 된다&lt;/p&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 저장 외에도 변경, 삭제가 가능하며&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/jmindeveloper/SwiftDataExample/tree/master&quot;&gt;전체코드&lt;/a&gt;에서 확인 가능하다&lt;/p&gt;
&lt;h2 id=&quot;22-uikit&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2.2 UIKit&lt;/h2&gt;
&lt;h3 id=&quot;221-modelcontainer&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.2.1 ModelContainer&lt;/h3&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SwiftUI에서는 modelContainer 로 뷰에 직접 container를 주입해 주었다&lt;br /&gt;하지만 UIKit에서는 modelContainer 를 사용하지 못하기 때문에 container를 직접 생성해 줘야 한다&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;let container = try? ModelContainer(for: [TodoItem.self])&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;222-fetchdescriptor&quot; style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.2.2 fetchDescriptor&lt;/h3&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;UIKit에서는 Query Property Wrapper를 지원하지 않는다&lt;br /&gt;따라서 위에서 본&lt;span&gt;&amp;nbsp;&lt;/span&gt;FetchDescription를 사용해야 한다&lt;/p&gt;
&lt;pre class=&quot;roboconf&quot; style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;code&gt;func fetchContext() {
	let fetchDescriptor = FetchDescriptor&amp;lt;TodoItem&amp;gt;(sortBy: [SortDescriptor&amp;lt;TodoItem&amp;gt;(\TodoItem.title, order: .forward), SortDescriptor&amp;lt;TodoItem&amp;gt;(\TodoItem.createdAt, order: .forward)])
	self.todoItems = (try? context?.fetch(fetchDescriptor)) ?? []
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;query property wrapper 랑 거의 동일하다&lt;br /&gt;다만 fetchDescriptor에서는 fetch 조건, fetchLimit, fetchOffset 등 좀더 세세한 설정을 할 수 있다&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetchDescriptor에 대해 더 자세히 알려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.apple.com/documentation/swiftdata/fetchdescriptor&quot;&gt;클릭&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 fetchDescriptor 는 context의 변화를 알려주지는 않는것 같다 (제가 못찾는 것일수도)&lt;br /&gt;따라서 context가 변할때마다 위 함수를 실행시켜줘서 view를 업데이트 해주고 있다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고&lt;br /&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swiftdata/preservingyourappsmodeldataacrosslaunches#Save-models-for-later-use&quot;&gt;https://developer.apple.com/documentation/swiftdata/preservingyourappsmodeldataacrosslaunches#Save-models-for-later-use&lt;/a&gt;&lt;br /&gt;전체코드&lt;br /&gt;&lt;a href=&quot;https://github.com/jmindeveloper/SwiftDataExample/tree/master&quot;&gt;https://github.com/jmindeveloper/SwiftDataExample/tree/master&lt;/a&gt;&lt;/p&gt;</description>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/66</guid>
      <comments>https://jmin-developer.tistory.com/66#entry66comment</comments>
      <pubDate>Thu, 27 Jun 2024 06:51:09 +0900</pubDate>
    </item>
    <item>
      <title>SwiftUI halfmodal 구현 feat UIKit</title>
      <link>https://jmin-developer.tistory.com/65</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 15 이상부터는 하프모달을 지원하지만 그 이하버전에는 직접 구현해야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 State에 따라 view를 보여주고 안보여주고 이런식으로 구현했으나 탭바가 있을경우 문제가 됐다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 기존 UIKit 코드에서 사용하던 halfmodal도 있고해서 그냥 해당 코드를 SwiftUI에서도 사용할 수 있도록 마이그레이션했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 기존 UIKit에서 사용하던 half modal&lt;/p&gt;
&lt;pre id=&quot;code_1719438258510&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class HalfModalViewController: UIViewController {
    
    private var contentView: UIView
    
    private let dimmedView: UIView = {
        let view = UIView()
        view.backgroundColor = .black
        view.alpha = 0
        
        return view
    }()
    
    private let containerView: UIView = {
        let view = UIView()
        view.layer.masksToBounds = true
        
        return view
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setSubViews()
        connectTarget()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        presentAnimation()
    }
    
    private let parentVC: UIViewController?
    private var currentHeight = CGFloat.zero
    private var defaultHeight = CGFloat.zero
    private var dismissHeight: CGFloat {
        return defaultHeight - 5
    }
    var isScrollEnable: Bool
    var isDimmTouched: Bool
    
    private var maximumHeight: CGFloat {
        return UIScreen.main.bounds.height - view.safeAreaInsets.top
    }
    
    init(contentView: UIView,
         parentVC: UIViewController?,
         isScrollEnable: Bool,
         isDimmTouched: Bool = true
    ) {
        self.isScrollEnable = isScrollEnable
        self.parentVC = parentVC
        self.contentView = contentView
        self.isDimmTouched = isDimmTouched
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }
    
    // MARK: - Method
    func showToast(style: AIMCareMSKToast.ToastStyle, message: String) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            guard let self = self else { return }
            self.view.toast(style: style, message: message, bottomPosition: self.contentView.frame.height + 8, showDuration: 0.3, duration: 3)
        }
    }
    
    @discardableResult
    func changeView(view: UIView, isScrollEnable: Bool? = nil, isDimmTouched: Bool? = nil) -&amp;gt; HalfModalViewController {
        let halfModalVC = HalfModalViewController(contentView: view, parentVC: self.parentVC, isScrollEnable: isScrollEnable ?? self.isScrollEnable, isDimmTouched: isDimmTouched ?? self.isDimmTouched)
        halfModalVC.modalPresentationStyle = .overFullScreen

        self.dismiss(animated: false) { [weak self] in
            guard let self = self else {
                return
            }
            
            self.parentVC?.present(halfModalVC, animated: false)
        }
        
        return halfModalVC
    }
    
    private func presentAnimation(dimmViewAnimation: Bool = true) {
        containerView.snp.remakeConstraints {
            $0.bottom.horizontalEdges.equalToSuperview()
            $0.height.equalTo(contentView.snp.height)
        }
        
        UIView.animate(withDuration: 0.2 , delay: 0, options: .curveEaseOut) {
            self.view.layoutIfNeeded()
            if dimmViewAnimation {
                self.dimmedView.alpha = 0.8
            }
        } completion: { [weak self] isSuccess in
            guard let self = self else { return }
            if isSuccess {
                self.currentHeight = self.contentView.frame.height
                self.defaultHeight = self.contentView.frame.height
            }
        }
    }
    
//    override func dismiss(animated flag: Bool, completion: (() -&amp;gt; Void)? = nil) {
//        if flag {
//            dismissAnimation()
//        } else {
//            super.dismiss(animated: flag, completion: completion)
//        }
//    }
    
    func dismissAnimation(completion: (() -&amp;gt; Void)? = nil) {
        containerView.snp.remakeConstraints {
            $0.bottom.horizontalEdges.equalToSuperview()
            $0.height.equalTo(0)
        }
        
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
            self.view.layoutIfNeeded()
            self.dimmedView.alpha = 0
        } completion: { [weak self] _ in
            self?.dismiss(animated: false) {
                completion?()
            }
        }
    }
    
    private func heightConstraintsChangeAnimation(newHeight height: CGFloat) {
        contentView.snp.remakeConstraints {
            $0.horizontalEdges.bottom.equalToSuperview()
            $0.height.equalTo(height)
        }
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
            self.view.layoutIfNeeded()
        }
    }
    
    // MARK: - Method
    func pushViewController(_ vc: UIViewController) {
        dismiss(animated: false)
        parentVC?.navigationController?.pushViewController(vc, animated: true)
    }
    
    func presentViewController(_ vc: UIViewController) {
        dismiss(animated: false)
        parentVC?.present(vc, animated: true)
    }
    
    // MARK: - Target
    private func connectTarget() {
        let dimmedViewTapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmedViewTapGestureAction(_:)))
        dimmedView.addGestureRecognizer(dimmedViewTapGesture)
        let contentViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(contentViewPanGestureAction(_:)))
        contentView.addGestureRecognizer(contentViewPanGesture)
    }
    
    @objc private func dimmedViewTapGestureAction(_ sender: UITapGestureRecognizer) {
        if isDimmTouched {
            dismissAnimation()
        }
    }
    
    @objc private func contentViewPanGestureAction(_ sender: UIPanGestureRecognizer) {
        let panOringin = sender.translation(in: view)
        let isDraggingDown = panOringin.y &amp;gt; 0
        let newHeight = currentHeight - panOringin.y
        
        switch sender.state {
        case .changed:
            if newHeight &amp;lt; maximumHeight, isScrollEnable {
                contentView.snp.remakeConstraints {
                    $0.bottom.horizontalEdges.equalToSuperview()
                    $0.height.equalTo(newHeight)
                }
            }
        case .ended:
            if newHeight &amp;lt; dismissHeight {
                dismissAnimation()
            } else if newHeight &amp;gt; defaultHeight, !isDraggingDown, isScrollEnable {
                heightConstraintsChangeAnimation(newHeight: maximumHeight)
                currentHeight = maximumHeight
            } else if newHeight &amp;gt; defaultHeight, isDraggingDown, isScrollEnable {
                heightConstraintsChangeAnimation(newHeight: defaultHeight)
                currentHeight = defaultHeight
            }
        default:
            break
        }
    }
    // MARK: - UI
    private func setSubViews() {
        view.backgroundColor = .clear
        [dimmedView, containerView].forEach {
            view.addSubview($0)
        }
        containerView.addSubview(contentView)
        setConstraints()
    }
    
    private func setConstraints() {
        dimmedView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        
        containerView.snp.makeConstraints {
            $0.bottom.horizontalEdges.equalToSuperview()
            $0.height.equalTo(0)
        }
        
        contentView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. View to UIView 코드&lt;/p&gt;
&lt;pre id=&quot;code_1719438309197&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class HostingView&amp;lt;T: View&amp;gt;: UIView {
    
    private(set) var hostingController: UIHostingController&amp;lt;T&amp;gt;
    
    var rootView: T {
        get { hostingController.rootView }
        set { hostingController.rootView = newValue }
    }
    
    init(rootView: T, frame: CGRect = .zero) {
        hostingController = UIHostingController(rootView: rootView)
        
        super.init(frame: frame)
        
        backgroundColor = .clear
        hostingController.view.backgroundColor = backgroundColor
        hostingController.view.frame = self.bounds
        hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        
        addSubview(hostingController.view)
    }
    
    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. SwiftUI의 View를 확장해서 halfModal 을 present해주는 코드&lt;/p&gt;
&lt;pre id=&quot;code_1719438393667&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension View {
	func presentHalfModal&amp;lt;Content: View&amp;gt;(
        height: CGFloat,
        scrollEnable: Bool = false,
        @ViewBuilder _ content: @escaping (() -&amp;gt; Content)
    ) {
        let view = HostingView(rootView: content())
        view.snp.makeConstraints {
            $0.width.equalTo(screenSize.width)
            $0.height.equalTo(height)
        }
        view.backgroundColor = .white
        view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        view.layer.cornerRadius = 28
        view.layer.masksToBounds = true
        let vc = HalfModalViewController(contentView: view, parentVC: nil, isScrollEnable: scrollEnable)
        vc.modalPresentationStyle = .overFullScreen
        UIApplication.topViewController()?.present(vc, animated: false)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI의 버튼의 액션이나 상태가 변했을때 presentHalfModal 함수를 호출해주면 정상적으로 작동한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI같은경우 아직 버그도 존재하고 불편한점도 있기 때문에 UIKit을 잘 섞어서 사용하는게 효율적이라 생각한다&lt;/p&gt;</description>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/65</guid>
      <comments>https://jmin-developer.tistory.com/65#entry65comment</comments>
      <pubDate>Thu, 27 Jun 2024 06:47:48 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] Combine 오퍼레이터에서 async 메서드 사용하기</title>
      <link>https://jmin-developer.tistory.com/64</link>
      <description>&lt;pre id=&quot;code_1671616814455&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension Publisher {
    func asyncMap&amp;lt;T&amp;gt;(
        _ transform: @escaping (Output) async -&amp;gt; T
    ) -&amp;gt; Publishers.FlatMap&amp;lt;Future&amp;lt;T, Never&amp;gt;, Self&amp;gt; {
        flatMap { value in
            Future { promise in
                Task {
                    let output = await transform(value)
                    promise(.success(output))
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/64</guid>
      <comments>https://jmin-developer.tistory.com/64#entry64comment</comments>
      <pubDate>Wed, 21 Dec 2022 19:00:18 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] 참조타입 메모리주소 확인</title>
      <link>https://jmin-developer.tistory.com/63</link>
      <description>&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;print(Unmanaged.passUnretained(someVar).toOpaque())&lt;/code&gt;&lt;/pre&gt;</description>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/63</guid>
      <comments>https://jmin-developer.tistory.com/63#entry63comment</comments>
      <pubDate>Fri, 11 Nov 2022 11:09:22 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Toast 메세지 구현</title>
      <link>https://jmin-developer.tistory.com/62</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;toast.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfZQ4g/btrN40LwSSn/Rr4Kc9VYJwj3JKzlw6a7wk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfZQ4g/btrN40LwSSn/Rr4Kc9VYJwj3JKzlw6a7wk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfZQ4g/btrN40LwSSn/Rr4Kc9VYJwj3JKzlw6a7wk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bfZQ4g/btrN40LwSSn/Rr4Kc9VYJwj3JKzlw6a7wk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;257&quot; height=&quot;556&quot; data-filename=&quot;toast.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. label 패딩주기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스트메세지가 label크기랑 딱 맞으면 안이쁘기때문에 패딩을 줌&lt;/p&gt;
&lt;pre id=&quot;code_1665211417739&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PaddingLabel: UILabel {
    private var padding = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
    
    override var intrinsicContentSize: CGSize {
        var contentSize = super.intrinsicContentSize
        contentSize.height += padding.top + padding.bottom
        contentSize.width += padding.left + padding.right
        
        return contentSize
    }
    
    convenience init(text: String) {
        self.init()
        self.numberOfLines = 0
        self.textAlignment = .center
        self.text = text
        let contentSize = intrinsicContentSize
        self.frame.size = contentSize
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 토스트 메세지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UIView를 extension 해서 만들음&lt;/p&gt;
&lt;pre id=&quot;code_1665211519695&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/// message: message
/// bottomPosition: view의 bottom에서 얼마나 떨어질지
/// showDuration: 보여지고 사라지는 시간
/// duration: 보여지는 시간
/// autoHide: 자동으로 사라질지 여부 (해당값이 true일경우 duration은 무의미)
@discardableResult
func toast(
	message: String,
	bottomPosition: CGFloat,
	showDuration: TimeInterval,
    duration: TimeInterval = 0,
	autoHide: Bool = true
) -&amp;gt; UIView {
	let label = PaddingLabel(text: message)
    label.text = message
    let x = (frame.width / 2) - (label.frame.width / 2)
	label.frame.origin = CGPoint(x: x, y: frame.height - bottomPosition)
        
    label.backgroundColor = UIColor.black.withAlphaComponent(0.5)
    label.textColor = .white
	label.layer.cornerRadius = label.frame.height / 2
	label.layer.masksToBounds = true
	showToast(view: label, showDuration: showDuration, duration: duration, autoHide: autoHide)
        
    return label
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1665211643789&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/// 토스트 보이기
    func showToast(view: UIView, showDuration: TimeInterval, duration: TimeInterval, autoHide: Bool) {
        addSubview(view)
        view.alpha = 0
        
        UIView.animate(withDuration: showDuration, delay: 0, options: [.curveEaseIn]) {
            view.alpha = 1
        } completion: { [weak self] isFinish in
            guard let self = self else { return }
            if autoHide &amp;amp;&amp;amp; isFinish {
                self.hideToast(view: view, hideDuration: showDuration, duration: duration)
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1665211678402&quot; class=&quot;groovy&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/// 토스트 사라지기
    /// (해당 함수를 직접 호출할경우 duration만큼 delay후 사라짐)
    func hideToast(view: UIView, hideDuration: TimeInterval, duration: TimeInterval = 0) {
        UIView.animate(withDuration: hideDuration, delay: duration, options: [.curveEaseOut]) {
            view.alpha = 0
        } completion: { _ in
            view.removeFromSuperview()
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 사용&lt;/h4&gt;
&lt;pre id=&quot;code_1665211721925&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; @IBAction func toast(_ sender: UIButton) {
        guard let text = tf.text,
              !text.isEmpty else {
            return
        }
        
        view.toast(message: text, bottomPosition: 300, showDuration: 0.5, duration: 2)
    }
    
    @IBAction func showToast(_ sender: UIButton) {
        guard let text = tf.text,
              !text.isEmpty else {
            return
        }
        
        toastView = view.toast(message: text, bottomPosition: 300, showDuration: 0.5, autoHide: false)
    }
    
    @IBAction func hideToast(_ sender: UIButton) {
        guard let toast = toastView else {
            return
        }
        
        view.hideToast(view: toast, hideDuration: 0.5)
        toastView = nil
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;toast함수의 audoHide 가 true일경우 보여주고 사라지는것까지 자동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;false일경우 보여주는건 자동이지만 사라지는건 hideToast함수를 직접 실행해줘야함&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jmindeveloper/Toast&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/jmindeveloper/Toast&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1665211789586&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jmindeveloper/Toast: ios toastMessage&quot; data-og-description=&quot;ios toastMessage. Contribute to jmindeveloper/Toast development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jmindeveloper/Toast&quot; data-og-url=&quot;https://github.com/jmindeveloper/Toast&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/O948Y/hyP3xgbS9u/KcFdv1k8QZD7uPPcsk10C1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jmindeveloper/Toast&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jmindeveloper/Toast&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/O948Y/hyP3xgbS9u/KcFdv1k8QZD7uPPcsk10C1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jmindeveloper/Toast: ios toastMessage&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ios toastMessage. Contribute to jmindeveloper/Toast development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ios 개발/iOS</category>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/62</guid>
      <comments>https://jmin-developer.tistory.com/62#entry62comment</comments>
      <pubDate>Sat, 8 Oct 2022 15:49:58 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] 그날 그시간 출시 회고</title>
      <link>https://jmin-developer.tistory.com/61</link>
      <description>&lt;h1&gt;그날 그시간 - 시간의 기록&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-02 오후 2.45.36.png&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;1030&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPPoq/btrLe10WCQw/IbHYOkyfdkRKGkRz5B3v2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPPoq/btrLe10WCQw/IbHYOkyfdkRKGkRz5B3v2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPPoq/btrLe10WCQw/IbHYOkyfdkRKGkRz5B3v2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPPoq%2FbtrLe10WCQw%2FIbHYOkyfdkRKGkRz5B3v2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;384&quot; data-filename=&quot;스크린샷 2022-09-02 오후 2.45.36.png&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;1030&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;기획&lt;/h1&gt;
&lt;p&gt;시간단위로 기록이 가능한 다이어리앱을 만들기로 생각했다&lt;br&gt;앱의 홈화면에서 오늘 하루 있었던 일을 시간단위로 기록을 하고 하루 전체의 기록을 남길 수 있다&lt;br&gt;이렇게 남긴 기록들은 메뉴에서 검색이 가능하고 월별로 모아서 볼 수 있도록 하기로 했다&lt;/p&gt;
&lt;h1&gt;개발&lt;/h1&gt;
&lt;h3&gt;데이터 저장&lt;/h3&gt;
&lt;p&gt;Realm이랑 CoreData 중에서 고민을 했다&lt;br&gt;iCloud로 백업을 할 생각이였기때문에 iCloud랑 호환성이 좋은 CoreData로 결정했다&lt;/p&gt;
&lt;h3&gt;외부 라이브러리&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Snapkit&lt;/code&gt;, &lt;code&gt;Toast&lt;/code&gt;, &lt;code&gt;SideMenu&lt;/code&gt;, &lt;code&gt;CombineCocoa&lt;/code&gt;를 사용했다&lt;br&gt;FSCalendar를 사용할려 했으나 레이아웃 이슈로 캘린더를 직접 구현했다&lt;br&gt;찾아봤는데 같은 케이스의 이슈는 있어도 해결법은 못찾았다&lt;/p&gt;
&lt;h1&gt;느낀점&lt;/h1&gt;
&lt;p&gt;3번째 앱 출시이다&lt;br&gt;말이 3번째 출시이지 전에 두개 앱은 어떻게 심사에 통과했는지 의문이 들정도로 버그투성이에 레이아웃이슈도 있는터라 제대로 준비한 첫번째 앱이라 할 수 잇다&lt;br&gt;그만큼 애정이 가고 아직 추가하고 싶은 기능이 몇몇 있다  &lt;/p&gt;
&lt;p&gt;개발하면서 코드의 재사용성, 모듈화에 신경을 쓴다고 썼는데 만족스럽지가 않다&lt;br&gt;지속적으로 유지보수 및 리팩토링을 진행할 계획이다&lt;br&gt;또한 CloudKit을 사용한 만큼 애플기기간의 동기화에 이점이 있기때문에 아이패드앱이나 맥용 앱으로도 출시할 마음은 있지만 시간이 될지 모르겠다&lt;/p&gt;</description>
      <category>ios 개발/개인프로젝트</category>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/61</guid>
      <comments>https://jmin-developer.tistory.com/61#entry61comment</comments>
      <pubDate>Fri, 2 Sep 2022 15:16:49 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] 지뢰찾기를 만들어보았다</title>
      <link>https://jmin-developer.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;종종 지뢰찾기를 한다&lt;br /&gt;하다보니 만들 수 있겠다 싶었고 만들었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지뢰찾기의 중요로직만 구현하였으며 난이도, 타이머등 부가적인 요소는 구현하지 않았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;게임 스크린샷&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Shot - iPhone 13 - 2022-08-24 at 15.54.46.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLRlBQ/btrKvcVqf8E/hWj3YqyJOa7ccgiCt4XFGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLRlBQ/btrKvcVqf8E/hWj3YqyJOa7ccgiCt4XFGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLRlBQ/btrKvcVqf8E/hWj3YqyJOa7ccgiCt4XFGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLRlBQ%2FbtrKvcVqf8E%2FhWj3YqyJOa7ccgiCt4XFGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;283&quot; height=&quot;612&quot; data-filename=&quot;Simulator Screen Shot - iPhone 13 - 2022-08-24 at 15.54.46.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Simulator Screen Shot - iPhone 13 - 2022-08-24 at 15.55.11.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSQufk/btrKuykDaqu/hAjR41VMaiN3E39n3FRUKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSQufk/btrKuykDaqu/hAjR41VMaiN3E39n3FRUKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSQufk/btrKuykDaqu/hAjR41VMaiN3E39n3FRUKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSQufk%2FbtrKuykDaqu%2FhAjR41VMaiN3E39n3FRUKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;306&quot; height=&quot;662&quot; data-filename=&quot;Simulator Screen Shot - iPhone 13 - 2022-08-24 at 15.55.11.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;맵 상태&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.23.47.png&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;1532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sqYmA/btrKuV7kh5W/90jFXW4ZW05uqHK61aMkh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sqYmA/btrKuV7kh5W/90jFXW4ZW05uqHK61aMkh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sqYmA/btrKuV7kh5W/90jFXW4ZW05uqHK61aMkh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsqYmA%2FbtrKuV7kh5W%2F90jFXW4ZW05uqHK61aMkh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;668&quot; height=&quot;1302&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.23.47.png&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;1532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵의 상태이다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mine - 지뢰가 있는 맵&lt;/li&gt;
&lt;li&gt;empty - 비어있는 맵&lt;/li&gt;
&lt;li&gt;nearMine(count: Int) - 근처에 지뢰가 있는 맵 연관값으로 근처에 있는 지뢰의 개수를 가지고 있다&lt;/li&gt;
&lt;li&gt;flag - 깃발이 꽃혀있는 맵&lt;/li&gt;
&lt;li&gt;nonOpen - 아직 터치를 안해서 무엇이 있지 모르는 맵&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산프로퍼티로 해당 맵의 근처지뢰계수, 이미지, 이미지 컬러를 받아올 수 있다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지뢰생성&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.21.11.png&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF8FjU/btrKq4ELyPZ/iTFwh5DFTUKvCufKFGbs40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF8FjU/btrKq4ELyPZ/iTFwh5DFTUKvCufKFGbs40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF8FjU/btrKq4ELyPZ/iTFwh5DFTUKvCufKFGbs40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF8FjU%2FbtrKq4ELyPZ%2FiTFwh5DFTUKvCufKFGbs40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;434&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.21.11.png&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;1244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;랜덤한 위치에 지뢰를 생성한다&lt;br /&gt;게임을 시작하고 첫 터치를 했을때 호출되며 게임의 편의를 위해 첫 터치한 지점포함 주위 9개의 맵에는 지뢰가 생성될수 없게 해줬다&lt;br /&gt;중복값을 없애기 위해 Set을 사용했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.30.00.png&quot; data-origin-width=&quot;1936&quot; data-origin-height=&quot;972&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xjeHa/btrKq5KoOil/yHwQscf8SOBSnidSK3pFKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xjeHa/btrKq5KoOil/yHwQscf8SOBSnidSK3pFKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xjeHa/btrKq5KoOil/yHwQscf8SOBSnidSK3pFKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxjeHa%2FbtrKq5KoOil%2FyHwQscf8SOBSnidSK3pFKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;762&quot; height=&quot;383&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.30.00.png&quot; data-origin-width=&quot;1936&quot; data-origin-height=&quot;972&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵에 생성된 지뢰의 위치를 적용해준다&lt;br /&gt;그와 동시에 nearMineMap을 계산해서 적용해준다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.33.10.png&quot; data-origin-width=&quot;2464&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5ZNqW/btrKuyrkG3r/lF6Wnb67MIj1NUnTq1Yw71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5ZNqW/btrKuyrkG3r/lF6Wnb67MIj1NUnTq1Yw71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5ZNqW/btrKuyrkG3r/lF6Wnb67MIj1NUnTq1Yw71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5ZNqW%2FbtrKuyrkG3r%2FlF6Wnb67MIj1NUnTq1Yw71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;896&quot; height=&quot;297&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.33.10.png&quot; data-origin-width=&quot;2464&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;navitateAroundMap(location:_:) 함수이다&lt;br /&gt;location의 상하좌우 및 대각선을 탐색하는 함수이다&lt;br /&gt;탐색한 맵이 전체 맵의 범위를 넘어서지 않는다면 action 클로저를 실행한다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈맵 탐색&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지뢰찾기 맵에서 어떠한 지점을 터치했을때 해당 지점만 열리는게 아니고 연쇄적으로 비어있는 맵이 열리게 된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.37.41.png&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0cax0/btrKtBhu6TT/ErwCuWrhn1B1VvAI0oiXa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0cax0/btrKtBhu6TT/ErwCuWrhn1B1VvAI0oiXa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0cax0/btrKtBhu6TT/ErwCuWrhn1B1VvAI0oiXa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0cax0%2FbtrKtBhu6TT%2FErwCuWrhn1B1VvAI0oiXa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2102&quot; height=&quot;914&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.37.41.png&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전형적인 dfs이다&lt;br /&gt;nearMineMap이 나올때까지 상하좌우, 대각선 방향으로 재귀호출을 하며 nearMineMap이 나오면 해당 맵까지 오픈하고 return한다&lt;br /&gt;한번 탐색한곳은 다시 탐색하지 못하도록 visitedMap을 만들어줬다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;flag&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 지점에 지뢰가 있다고 생각하면 flag를 세울 수 있다&lt;br /&gt;nearMineMap의 count와 근처 flag의 개수가 일치할때 해당 nearMineMap을 터치시 일부 맵을 오픈한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.45.03.png&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7W5rU/btrKuv2rrE8/Qp3sRZuwAK7LraAT2VzRak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7W5rU/btrKuv2rrE8/Qp3sRZuwAK7LraAT2VzRak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7W5rU/btrKuv2rrE8/Qp3sRZuwAK7LraAT2VzRak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7W5rU%2FbtrKuv2rrE8%2FQp3sRZuwAK7LraAT2VzRak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1826&quot; height=&quot;1702&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.45.03.png&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isValidNearMineCount(location:) 함수는 nearMineMap의 count와 근처 flag의 개수를 비교하는 함수이다&lt;br /&gt;findEmptyMapAndValidNearMineMap(location:) 함수에서 isValidNearMineCount(location:)가 실행되고 true가 리턴되면 맵을 오픈하는 작업을 한다&lt;br /&gt;터치 지점의 근처 맵이 전부 nearMineMap이라면 그 맵들만 오픈하고 만약 emptyMap이 나오게 된다면 findEmptyMap(location:) 함수가 호출되어 연쇄적으로 비어있는 맵을 전부 오픈하게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 만약 flag가 실제 지뢰가 있는 위치가 아니라면 맵 오픈과정에서 지뢰가 터지게 되고 게임이 오바가 된다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;game clear&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.52.25.png&quot; data-origin-width=&quot;1750&quot; data-origin-height=&quot;1552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zTMVe/btrKuwG7faq/AmHEhmMbDkOg8jVRVLo8v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zTMVe/btrKuwG7faq/AmHEhmMbDkOg8jVRVLo8v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zTMVe/btrKuwG7faq/AmHEhmMbDkOg8jVRVLo8v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzTMVe%2FbtrKuwG7faq%2FAmHEhmMbDkOg8jVRVLo8v0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1750&quot; height=&quot;1552&quot; data-filename=&quot;스크린샷 2022-08-24 오후 3.52.25.png&quot; data-origin-width=&quot;1750&quot; data-origin-height=&quot;1552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 아직 오픈이 안된 맵이 모두 지뢰일경우 및 모든 flag가 올바른 지뢰일경우 혹은 둘 다일경우에는 게임 성공이다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jmindeveloper/Minesweeper&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/jmindeveloper/Minesweeper&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1661324053850&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jmindeveloper/Minesweeper&quot; data-og-description=&quot;Contribute to jmindeveloper/Minesweeper development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jmindeveloper/Minesweeper&quot; data-og-url=&quot;https://github.com/jmindeveloper/Minesweeper&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GyaMO/hyPyGYOr2N/baa0IBd5LmbPW9ZQBAyGL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jmindeveloper/Minesweeper&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jmindeveloper/Minesweeper&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GyaMO/hyPyGYOr2N/baa0IBd5LmbPW9ZQBAyGL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jmindeveloper/Minesweeper&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to jmindeveloper/Minesweeper development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ios 개발/개인프로젝트</category>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/60</guid>
      <comments>https://jmin-developer.tistory.com/60#entry60comment</comments>
      <pubDate>Wed, 24 Aug 2022 15:54:22 +0900</pubDate>
    </item>
    <item>
      <title>[Swift] 백준 1406 에디터 - 시간복잡도에 익숙해지자</title>
      <link>https://jmin-developer.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1406&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/1406&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1660015553392&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1406번: 에디터&quot; data-og-description=&quot;첫째 줄에는 초기에 편집기에 입력되어 있는 문자열이 주어진다. 이 문자열은 길이가 N이고, 영어 소문자로만 이루어져 있으며, 길이는 100,000을 넘지 않는다. 둘째 줄에는 입력할 명령어의 개수&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1406&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1406&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fRleF/hyPmg7h1LR/tdVB9RxR82O6JN376TAVk1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1406&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1406&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fRleF/hyPmg7h1LR/tdVB9RxR82O6JN376TAVk1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1406번: 에디터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에는 초기에 편집기에 입력되어 있는 문자열이 주어진다. 이 문자열은 길이가 N이고, 영어 소문자로만 이루어져 있으며, 길이는 100,000을 넘지 않는다. 둘째 줄에는 입력할 명령어의 개수&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제의 입력은 500,000이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해당 문제는 O(n)의 시간복잡도로 충분히 풀 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1660015637631&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var str = Array(readLine()!).map { String($0) }
let m = Int(readLine()!)!
var cursor = str.count

for _ in 0..&amp;lt;m {
    let command = readLine()!.split(separator: &quot; &quot;).map { String($0) }
    
    switch command[0] {
    case &quot;L&quot;:
        if cursor &amp;gt; 0 {
            cursor -= 1
        }
    case &quot;D&quot;:
        if cursor &amp;lt; str.count {
            cursor += 1
        }
    case &quot;B&quot;:
        if cursor &amp;gt; 0 {
            str.remove(at: cursor - 1)
            cursor -= 1
        }
    case &quot;P&quot;:
        str.insert(command[1], at: cursor)
        cursor += 1
    default: break
    }
}

let result = str.reduce(&quot;&quot;) {
    $0 + $1
}

print(result)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문을 한번밖에 사용하지 않았기때문에 이 코드의 시간복잡도는 O(n)일줄 알았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시간초과로 문제를 틀렸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유를 곰곰히 생각해봤다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열의 remove(at:) 및 insert(_:at:) 메서드는 시간복잡도가 O(n)이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 위 코드의 시간복잡도는 O(n^2) 이 되는것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 스택을 두개 만들어서 다시 구현을 했다&lt;/p&gt;
&lt;pre id=&quot;code_1660015786619&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var left = Array(readLine()!)
var right = [Character]()

for _ in 0..&amp;lt;Int(readLine()!)! {
    let command = readLine()!
    
    switch command.first! {
    case &quot;L&quot;:
        if !left.isEmpty {
            right.append(left.removeLast())
        }
    case &quot;D&quot;:
        if !right.isEmpty {
            left.append(right.removeLast())
        }
    case &quot;B&quot;:
        if !left.isEmpty {
            left.removeLast()
        }
    case &quot;P&quot;:
        left.append(command.last!)
    default: break
    }
}

print(String(left + right.reversed()))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열의 append(_:) 및 removeLast() 메서드는 시간복잡도가 O(1)이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 위 코드가 바로 O(n) 짜리 코드인것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 짠 코드뿐만이 아닌 언어에 구현되있는 메서드의 시간복잡도도 고려를 해야한다&lt;/p&gt;</description>
      <category>코딩테스트/Swift</category>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/59</guid>
      <comments>https://jmin-developer.tistory.com/59#entry59comment</comments>
      <pubDate>Tue, 9 Aug 2022 12:31:16 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] Diffable DataSource에 관해서 간략히 알아보자</title>
      <link>https://jmin-developer.tistory.com/58</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-5-6e9e954789.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEAQRu/btrHM9ho3Le/oaU06IdYbYUgJzYEsgDYo1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEAQRu/btrHM9ho3Le/oaU06IdYbYUgJzYEsgDYo1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEAQRu/btrHM9ho3Le/oaU06IdYbYUgJzYEsgDYo1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bEAQRu/btrHM9ho3Le/oaU06IdYbYUgJzYEsgDYo1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;303&quot; height=&quot;539&quot; data-filename=&quot;ezgif-5-6e9e954789.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TableViw&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;혹은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CollectionView&lt;/b&gt;를 그리기 위한 데이터를 관리하고 UI를 업데이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 해당 글에선 tableView로 설명 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 DataSource와 달리 달라진 부분을 추적하여 자연스럽게 UI를 업데이트 (애니메이션 효과)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 필요함?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 dataSoruce는 데이터가 업데이트되면 **tableView.reloadData()**로 동기화를 해 주었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위 메서드는 tableView를 한번에 업데이트 하므로 애니메이션 효과가 적용이 안되 사용자 경험에 나쁘다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 DiffableDataSource같은 경우는 변경된 데이터가 자연스러운 애니메이션 효과로 적용되는것을 볼 수 있다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DiffableDataSource를 사용함으로서 얻게되는 이점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가적인 작업이 없어도 자연스러운 애니메이션효과가 적용된다&lt;/li&gt;
&lt;li&gt;개선된 DataSource는 완벽하게 동기적인 버그나 예외, 충돌을 피할 수 있게 해준다&lt;/li&gt;
&lt;li&gt;UI 데이터의 동기화 부분 대신 앱의 동적인 데이터와 내용에 집중할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 사용함?&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DiffableDataSource를 tableView에 연결
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DiffableDataSource는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Generic Class&lt;/b&gt;이다&lt;/li&gt;
&lt;li&gt;해당 Generic 타입은 Hashable를 준수해야 한다&lt;/li&gt;
&lt;li&gt;그외에는 일반 DataSource의 cellForItemAt 메서드를 사용할때처럼 하면 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;private func configureDiffableDataSource() {
		dataSource = UITableViewDiffableDataSource&amp;lt;TableViewSection, Item&amp;gt;(tableView: tableView) { table, indexPath, item -&amp;gt; UITableViewCell? in
				guard let cell = self.tableView.dequeueReusableCell(withIdentifier: &quot;cell&quot;, for: indexPath) as? Cell else { return UITableViewCell() }
				cell.label.text = item.content
				return cell
		}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dataSource에 snapshot 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tableView에 보여줄 data가 바뀔때마다 새로운 Snapshot을 만들어서 dataSource에 apply 해줘야 한다&lt;/li&gt;
&lt;li&gt;snapshot은 section 및 item으로 구성되있다&lt;/li&gt;
&lt;li&gt;section, item을 추가, 삭제하여 표시할 내용을 구성한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private func applySnapshot() {
		var snapshot = NSDiffableDataSourceSnapshot&amp;lt;TableViewSection, Item&amp;gt;()
    snapshot.appendSections([.main])
    snapshot.appendItems(tableViewItem)
    self.dataSource?.apply(snapshot, animatingDifferences: true)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Hashable 해야함?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apply시 각 hashValue를 비교하여 바뀐부분을 인지하기 때문&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만약 Hashable 프로토콜을 채택했지만 hashValue가 같다면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 죽음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 각 인스턴스마다 hashValue가 다르게 해줘야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 좋은 방법은 타입에 UUID 프로퍼티를 하나 넣어주는것임&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;struct Item: Hashable {
    // id로 모든 Item 인스턴스가 같은 값을 가지지 못하도록 해줌
    let id = UUID().uuidString
    let content: String
    
    init(content: String) {
        self.content = content
    }
    
    // 해쉬함수
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

// 만약 id가 없어 content로만 hashValue를 만든다면
// content가 같은 인스턴스들은 hashValue가 같기때문에
// 앱이 죽음ㅋ
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 코드 보러가기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jmindeveloper/DiffableDataSource&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/jmindeveloper/DiffableDataSource&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1658308602416&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jmindeveloper/DiffableDataSource&quot; data-og-description=&quot;Contribute to jmindeveloper/DiffableDataSource development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jmindeveloper/DiffableDataSource&quot; data-og-url=&quot;https://github.com/jmindeveloper/DiffableDataSource&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ctzAc0/hyO8V9ViMb/fGzmsSyuFMOUWrlNuABzb1/img.png?width=1200&amp;amp;height=600&amp;amp;face=958_121_1040_211&quot;&gt;&lt;a href=&quot;https://github.com/jmindeveloper/DiffableDataSource&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jmindeveloper/DiffableDataSource&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ctzAc0/hyO8V9ViMb/fGzmsSyuFMOUWrlNuABzb1/img.png?width=1200&amp;amp;height=600&amp;amp;face=958_121_1040_211');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jmindeveloper/DiffableDataSource&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to jmindeveloper/DiffableDataSource development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notion에서 보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://serious-hamburger-920.notion.site/DiffableDataSource-1d51afdc631844bf9cd637237c4b307b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://serious-hamburger-920.notion.site/DiffableDataSource-1d51afdc631844bf9cd637237c4b307b&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1658308638932&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;DiffableDataSource&quot; data-og-description=&quot;개요&quot; data-og-host=&quot;serious-hamburger-920.notion.site&quot; data-og-source-url=&quot;https://serious-hamburger-920.notion.site/DiffableDataSource-1d51afdc631844bf9cd637237c4b307b&quot; data-og-url=&quot;https://serious-hamburger-920.notion.site/1d51afdc631844bf9cd637237c4b307b&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bhw1Ug/hyPaBPwydA/SwRKo8oeaN7emPXe5UuPl1/img.png?width=1298&amp;amp;height=208&amp;amp;face=0_0_1298_208,https://scrap.kakaocdn.net/dn/wME1q/hyPanKuZoy/3IGMc2hvP5ncKibRskgVK1/img.png?width=1298&amp;amp;height=208&amp;amp;face=0_0_1298_208&quot;&gt;&lt;a href=&quot;https://serious-hamburger-920.notion.site/DiffableDataSource-1d51afdc631844bf9cd637237c4b307b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://serious-hamburger-920.notion.site/DiffableDataSource-1d51afdc631844bf9cd637237c4b307b&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bhw1Ug/hyPaBPwydA/SwRKo8oeaN7emPXe5UuPl1/img.png?width=1298&amp;amp;height=208&amp;amp;face=0_0_1298_208,https://scrap.kakaocdn.net/dn/wME1q/hyPanKuZoy/3IGMc2hvP5ncKibRskgVK1/img.png?width=1298&amp;amp;height=208&amp;amp;face=0_0_1298_208');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;DiffableDataSource&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;serious-hamburger-920.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ios 개발/iOS</category>
      <author>JiminiOS</author>
      <guid isPermaLink="true">https://jmin-developer.tistory.com/58</guid>
      <comments>https://jmin-developer.tistory.com/58#entry58comment</comments>
      <pubDate>Wed, 20 Jul 2022 18:17:30 +0900</pubDate>
    </item>
  </channel>
</rss>