<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Danna Devlog</title>
        <link>https://velog.io/</link>
        <description>요즘은 https://welcometodannas.tistory.com/에 더 많은 글을 씁니다.</description>
        <lastBuildDate>Sun, 26 Mar 2023 14:08:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Danna Devlog</title>
            <url>https://images.velog.io/images/danna-lee/profile/b494f743-49ab-4264-9d83-bd07791ba245/sloth profile picture.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Danna Devlog. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/danna-lee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Swift로 인스타그램 스토리 하트 파티클 애니메이션 만들기]]></title>
            <link>https://velog.io/@danna-lee/Swift%EB%A1%9C-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8-%EC%8A%A4%ED%86%A0%EB%A6%AC-%ED%95%98%ED%8A%B8-%ED%8C%8C%ED%8B%B0%ED%81%B4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@danna-lee/Swift%EB%A1%9C-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8-%EC%8A%A4%ED%86%A0%EB%A6%AC-%ED%95%98%ED%8A%B8-%ED%8C%8C%ED%8B%B0%ED%81%B4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sun, 26 Mar 2023 14:08:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/danna-lee/post/fbdff651-44b2-43f3-8dd8-aee75fe86f1d/image.gif" alt=""></p>
<p>인스타그램을 하면서 항상 궁금했다.
내 스토리에 친구들이 하트를 눌러보면 보이는 저 하트가 올라가는 애니메이션은 어떻게 구현한 걸까?</p>
<p>유니티로는 파티클 애니메이션을 구현해 본 적이 있는데, Swift로 네이티브 iOS 앱을 만들 때는 어떻게 만들어야 할지 감도 안 왔다.</p>
<p>이것저것 찾아보니 방법에는 크게 두 가지가 있었다.
CAEmitterLayer와 SpriteKit에 포함되어 있는 SKEmitterNode다.</p>
<h2 id="skemitternode">SKEmitterNode</h2>
<p>위에서 잠깐 설명했듯, Swift로 게임을 만들 수 있게 지원하는 SpriteKit에 포함되어 있다.
인스펙터에서 실제 파티클이 생성되는 모습을 보며 값들을 조정해줄 수 있어 편리하다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/d7c1bc28-76c6-4a31-8ec5-83bd4bab3931/image.png" alt=""></p>
<p>하지만 UIView에 올리기 위해서는 SKView라는 SpriteKit의 뷰를 하나 만들어 SpriteKit Scene을 얹고 이를 뷰에 얹어주게 되어 있어 사실상 자연스러운 구조는 아니다.</p>
<p>그렇다고 해서 <strong>실제 사용되는 메모리 양을 보니 CAEmitter와 SKEmitter 간 의미있는 수준의 차이는 없었다</strong>. 그냥 SKEmitter는 게임을 위해 만들어진 탓에 non-game 앱에서는 구조가 조금 안 예쁠 뿐인 것 같다. (CAEmitter는 일반적인 UIView 위에 레이어 하나만 더해주면 된다.) CAEmitter로 구현이 가능한 경우라면 Non-game에서 굳이 SKEmitter를 썼을 때의 메리트를 찾지 못했다.</p>
<p>그래서 나는 SKEmitterNode로 대략적인 파라미터들의 값과 작동원리를 파악한 후 실제 구현은 CAEmitterLayer로 했다.</p>
<p>이 글에서 SKEmitterNode를 따로 설명하지는 않을 예정이다. 대신 <a href="https://www.youtube.com/watch?v=zyly5HhA6ao">이 영상</a>에 잘 설명되어 있다.</p>
<h2 id="caemitterlayer">CAEmitterLayer</h2>
<p>CAEmitterLayer는 View 위에 얹어지는 Layer의 한 종류로,
CAEmitterCell로 생성되는 파티클들을 Layer 위에 그려내는 방식이다.
CAEmitterLayer와 CAEmitterCell에 설정할 수 있는 프로퍼티가 너무 많아서 일단 코드를 써보며 하나하나 살펴보는 게 훨씬 빠르게 먹힐 것이다.</p>
<h3 id="위로-올라가는-하트-만들기-방향-각도-설정">위로 올라가는 하트 만들기 (방향, 각도 설정)</h3>
<p>우선 CAEmitterLayer 하나를 만들어준다.
이게 뭐라고 이해하는 데 많은 시간을 할애하는 것보다는 지금은 그냥 파티클이 뿌려지는 하나의 캔버스라고 생각하면 쉽다.
이 캔버스에서는 파티클이 어떤 모양으로 뿌려질지, 얼마나 많은 파티클이 뿌려질지, 어느 위치에 뿌려질지 등을 정해줄 수 있다.
자세한 프로퍼티는 Zedd님이 잘 정리해두셨다. (<a href="https://zeddios.tistory.com/428">Zedd 블로그</a>)</p>
<pre><code class="language-swift">let heartEmitter = CAEmitterLayer()
heartEmitter.emitterPosition = CGPoint(x: view.bounds.width/2, y: view.bounds.height/2)
heartEmitter.emitterSize = CGSize(width: 100, height: 100)
heartEmitter.emitterShape = .circle</code></pre>
<p>이렇게 쓰고 실행시켜봤자 아무 일도 일어나지 않을 것이다.
우리는 지금 캔버스 하나를 만들었을 뿐이고, 실제로 파티클 하나하나가 어떤 특성을 가지는지는 CAEmitterCell에서 정의해주어야 하기 때문이다.
CAEmitterCell에서는 파티클 이미지, 유지 시간, 속도, 나아가는 방향과 각도 등을 정의해줄 수 있다.</p>
<p>아래 코드에서는 예시로 인스타그램처럼 일렁이며 올라가는 하트 셀을 구현해보았다.</p>
<pre><code class="language-swift">let heartCell = CAEmitterCell()
heartCell.contents = UIImage(named: &quot;heart.png&quot;)?.cgImage // 이미지
heartCell.birthRate = 5 // 초당 생성 개수
heartCell.lifetime = 1.0 // 셀 유지 시간
heartCell.velocity = 100 // 속도
heartCell.emissionRange = .pi / 5 // 생성 각도
heartCell.emissionLongitude = .pi / -2 // 생성 각도</code></pre>
<p>그 후에 우리가 만든 Layer에 Cell을 얹혀주고,
그 Layer을 원하는 뷰의 subLayer로 더해주면 된다.</p>
<pre><code class="language-swift">heartEmitter.emitterCells = [heartCell]
view.layer.addSublayer(heartEmitter)</code></pre>
<p>여기까지 코드를 쓰면 아래와 같은 모습이 된다. (<a href="https://github.com/dannaward/ios-lab/commit/8170d996521b9593dda66056f3aba25c5944e028">현재까지의 코드</a>)</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/b0d2cc21-6d9d-4997-a6e7-3c8c031bafc3/image.gif" alt=""></p>
<h3 id="위로-올라갈수록-옅어지게-만들기">위로 올라갈수록 옅어지게 만들기</h3>
<p>지금도 충분히 일렁거리며 잘 올라가고 있지만, 어딘가 어색하다.
인스타그램의 하트와는 다르게 올라가며 툭툭 갑자기 사라지는 느낌이 든다.
위로 올라갈수록 alpha 값이 옅어지게 만들어 이를 해결해볼 수 있다.</p>
<p>위로 올라갈수록 alpha 값이 조정되는 건 셀 하나하나의 고유 값이 변경되는 것이므로 Layer가 아닌 Cell의 프로퍼티를 변경해주면 된다.</p>
<pre><code class="language-swift">heartCell.alphaRange = 0.3
heartCell.alphaSpeed = -0.5</code></pre>
<p>alphaRange는 해당 셀이 0부터 얼마까지의 알파 값을 가질 수 있느냐를 의미한다.
여기서는 0부터 0.3 사이의 값을 가지게 했다.</p>
<p>-0.5의 값을 준 alphaSpeed는 시간이 갈수록 알파 값을 줄여줄 것이다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/fe7a36b5-7be5-4abf-91b7-099045e8cb2e/image.gif" alt=""></p>
<p>위로 올라가며 서서히 사라지는 모습이 되었다. (<a href="https://github.com/dannaward/ios-lab/commit/95a49af3d5356f5e28ca71d3a2a2ca93f764cf3b">현재까지의 코드</a>)</p>
<h3 id="크기-변화-주기">크기 변화 주기</h3>
<p>이제 제법 인스타그램과 비슷해졌는데, 재밌는 걸 좀 더 붙여보려 한다.
셀이 모두 같은 크기엔 게 심심하니 크기에 변화를 줘볼 예정이다.</p>
<p>아래 값들로 변화를 주었다.
시작하는 크기, 크기 변화 범위, 시간이 흐를수록 얼마나 크기를 빠르게 변화시킬 건지 등을 설정해줄 수 있다.</p>
<pre><code class="language-swift">heartCell.scale = 0.15
heartCell.scaleRange = 1
heartCell.scaleSpeed = 0.5</code></pre>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/71e9c49c-fd62-4642-a99b-a64d2a02e193/image.gif" alt=""></p>
<p>크기가 좀 더 다양한 하트가 되었다.</p>
<h3 id="이모지를-emittercell로-사용할-수-있을까">이모지를 EmitterCell로 사용할 수 있을까?</h3>
<p>이쯤 되면 궁금한 게 생긴다.
이미지가 아닌 텍스트도 EmitterCell로 사용할 수 있을까?
텍스트도 사용할 수 있다면, 이미지를 하나하나 찾지 않고 기본 이모지를 활용해 더 재미난 파티클 애니메이션을 만들어볼 수 있을 것이다.</p>
<p>당연히 가능하다.</p>
<pre><code class="language-swift">heartCell.contents = {
    let emoji = &quot;🎉&quot;
    let font = UIFont.systemFont(ofSize: 10)
    let size = emoji.size(withAttributes: [.font: font])
    let renderer = UIGraphicsImageRenderer(size: size)
    let image = renderer.image { context in
        emoji.draw(at: .zero, withAttributes: [.font: font])
    }
    return image.cgImage
}()</code></pre>
<p>다만, 텍스트 자체로 contents 프로퍼티에 넣어주니 이모지는 나오지 않았고,
텍스트 이모지를 이미지로 렌더링 한 후 cgImage로 변환시켜주는 과정이 필요했다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/c589b7db-0993-43b5-8bce-92e15267c344/image.gif" alt=""></p>
<p>이모지도 잘 나오는 모습이다. (더 많은 파티클이 나오게끔 파라미터를 조금 조정해주었다) (<a href="https://github.com/dannaward/ios-lab/commit/67f5bfe64bcc30e06cf00a0ca67c80c361b63f44">현재까지의 코드</a>)</p>
<h3 id="이모지-섞기">이모지 섞기</h3>
<p>여기까지 오니 욕심이 나기 시작한다.
꼭 한 종류의 이모지만 나와야 할까? 여러 종류의 이모지가 한 번에 섞여 나오면 더 멋있지 않을까?</p>
<p>물론 이것도 가능하다.</p>
<p>하나의 CAEmitterLayer는 여러 개의 CAEmitterCell을 가질 수 있다.
이 점을 활용하면 된다.</p>
<p>신경 써야 할 것이 많지는 않다.</p>
<ol>
<li>이모지 개수만큼 Cell이 생성되게 할 것</li>
<li>이모지 개수가 많아져도 실제 스폰 되는 개수는 변하지 않게 할 것</li>
</ol>
<pre><code class="language-swift">let emojiStrings = [&quot;❤️&quot;, &quot;💙&quot;, &quot;💛&quot;, &quot;💜&quot;, &quot;🤎&quot;]

var emitterCells: [CAEmitterCell] = [] // 셀들을 담을 곳을 만들고
for emoji in emojiStrings { // 각 이모지에 대해 셀 생성
    let emitterCell = CAEmitterCell()
    emitterCell.contents = {
        let font = UIFont.systemFont(ofSize: 20)
        let size = emoji.size(withAttributes: [.font: font])
        let renderer = UIGraphicsImageRenderer(size: size)
        let image = renderer.image { context in
            emoji.draw(at: .zero, withAttributes: [.font: font])
        }
        return image.cgImage
    }()
    emitterCell.birthRate = 30 / Float(emojiStrings.count) // 생성 개수는 이모지 개수로 나눠준다
    // (다른 프로퍼티들은 생략)

    emitterCells.append(emitterCell)
}

emitterLayer.emitterCells = emitterCells</code></pre>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/b062bebe-7283-4ccc-82d8-add43578ad51/image.gif" alt=""></p>
<p>여기까지 쓰면 이런 다채로운 하트 이모지들을 만날 수 있다. (<a href="https://github.com/dannaward/ios-lab/commit/440c3e36d710e60ca8a005d5f883db23c473c04e">현재까지의 코드</a>)</p>
<h2 id="정리하며">정리하며</h2>
<p>여기선 인스타 스토리 하트 파티클을 만든다는 목표로 하나의 형태의 파티클을 만들어봤지만, 실제로 CAEmitter를 잘 활용하면 눈/비 내리는 효과, 폭발 효과, 반딧불 등 예쁜 이펙트를 많이 만들어낼 수 있다.</p>
<p>이것저것 만져보면 재밌기도 하고, 생각보다 러닝커브가 높지 않다는 걸 느낄 수 있을 것이다.</p>
<p>아래는 전체 코드다.</p>
<pre><code class="language-swift">//
//  InstaStyleParticleVC.swift
//  iOSLab
//
//  Created by Danna Lee on 2023/03/26.
//

import UIKit

class InstaStyleParticleVC: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        generateParticle()
    }
}

extension InstaStyleParticleVC {
    private func generateParticle() {
        let emitterLayer = CAEmitterLayer()
        emitterLayer.emitterPosition = CGPoint(x: view.bounds.width/2, y: view.bounds.height - 100)
        emitterLayer.emitterSize = CGSize(width: 100, height: 100)
        emitterLayer.emitterShape = .point

        let emojiStrings = [&quot;🥳&quot;, &quot;👏🏻&quot;, &quot;🍾&quot;, &quot;🎉&quot;]

        var emitterCells: [CAEmitterCell] = []
        for emoji in emojiStrings {
            let emitterCell = CAEmitterCell()
            emitterCell.contents = {
                let font = UIFont.systemFont(ofSize: 20)
                let size = emoji.size(withAttributes: [.font: font])
                let renderer = UIGraphicsImageRenderer(size: size)
                let image = renderer.image { context in
                    emoji.draw(at: .zero, withAttributes: [.font: font])
                }
                return image.cgImage
            }()
            emitterCell.birthRate = 30 / Float(emojiStrings.count)
            emitterCell.lifetime = 1.0
            emitterCell.velocity = 300
            emitterCell.velocityRange = 50
            emitterCell.emissionRange = .pi / 5
            emitterCell.emissionLongitude = .pi / -2
            emitterCell.alphaRange = 0.3
            emitterCell.alphaSpeed = -0.5
            emitterCell.scale = 0.15
            emitterCell.scaleRange = 1
            emitterCell.scaleSpeed = 0.5

            emitterCells.append(emitterCell)
        }

        emitterLayer.emitterCells = emitterCells
        view.layer.addSublayer(emitterLayer)
    }
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/43f96f01-7264-4b7d-94d6-4f393d50c72e/image.gif" alt=""></p>
<p>신나게 자축하며 끝</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[최근에 읽은 양질의 iOS 개발 아티클 모음]]></title>
            <link>https://velog.io/@danna-lee/%EC%B5%9C%EA%B7%BC%EC%97%90-%EC%9D%BD%EC%9D%80-%EC%96%91%EC%A7%88%EC%9D%98-iOS-%EA%B0%9C%EB%B0%9C-%EC%95%84%ED%8B%B0%ED%81%B4-%EB%AA%A8%EC%9D%8C</link>
            <guid>https://velog.io/@danna-lee/%EC%B5%9C%EA%B7%BC%EC%97%90-%EC%9D%BD%EC%9D%80-%EC%96%91%EC%A7%88%EC%9D%98-iOS-%EA%B0%9C%EB%B0%9C-%EC%95%84%ED%8B%B0%ED%81%B4-%EB%AA%A8%EC%9D%8C</guid>
            <pubDate>Sun, 12 Mar 2023 13:42:11 GMT</pubDate>
            <description><![CDATA[<p>iOS 개발 트렌드를 쫓아가기 위해 일주일에 한 번 날아오는 뉴스레터 <a href="https://iosdevweekly.com/issues">iOS Dev Weekly</a>를 구독하고 있는데, 정말 양질의 개발 아티클이 선별되어 날아온다.
그렇게 선별되어 날아온 아티클 중 관심있게 읽었던 글들을 공유해보려 한다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/a51ca25b-275f-4bc0-a979-ee0a3570051e/image.png" alt=""></p>
<ol>
<li><a href="https://engineering.fb.com/2023/02/06/ios/facebook-ios-app-architecture/?utm_campaign=iOS%2BDev%2BWeekly&amp;utm_medium=email&amp;utm_source=iOS%2BDev%2BWeekly%2BIssue%2B596">The evolution of Facebook’s iOS app architecture</a>
페이스북 iOS 앱은 Meta에서 관리하는 모바일 코드베이스 중 가장 오래된 코드베이스다. 이 아티클은 2014년부터 현재까지 메타에서 일하고 있는 <a href="https://www.linkedin.com/in/dustin-shahidehpour-a087a53b/">Dustin Shahidehpour</a>라는 사람이 쓴 아티클로, 2012년에 전체 코드를 모두 새로 쓴 이후 어떤 의사 결정 과정에 의해 코드 베이스가 발전되어 왔고, 지금의 구조를 가지게 되었는지 상세히 설명하고 있다.
흥미로운 점은, 페이스북 iOS 앱에는 Apple SDK가 거의 하나도 쓰이지 않았다고 한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/324f235c-c170-40b4-91a6-b5185c08ec3b/image.png" alt="">
2. <a href="https://blog.eidinger.info/save-money-when-using-github-actions-for-ios-cicd?utm_campaign=iOS%2BDev%2BWeekly&amp;utm_medium=email&amp;utm_source=iOS%2BDev%2BWeekly%2BIssue%2B594">Save money when using GitHub Actions for iOS CI/CD</a>
깃허브 프라이빗 레포지토리의 액션에서 iOS 베이스 코드 CI/CD를 구축할 때 MacOS를 빌드 머신으로 사용하면 리눅스 베이스 러너보다 10배 가량 높은 비용이 청구된다. 위 아티클에선 다양한 케이스를 들어 빌드 비용을 아끼는 법을 설명한다. 하지만 위 아티클에는 Self-hosted runner에 대한 설명이 없어 <a href="https://docs.github.com/en/actions/hosting-your-own-runners/using-self-hosted-runners-in-a-workflow?utm_campaign=iOS%2BDev%2BWeekly&amp;utm_medium=email&amp;utm_source=iOS%2BDev%2BWeekly%2BIssue%2B594">이 문서</a>도 같이 보면 좋다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/5ac70e6d-766e-4eda-bb54-efff4d84e694/image.png" alt="">
3. <a href="https://xcode.tips/find-problematic-constraint/?utm_campaign=iOS%2BDev%2BWeekly&amp;utm_medium=email&amp;utm_source=iOS%2BDev%2BWeekly%2BIssue%2B591">Find Problematic Constraint</a>
단 한 장의 직관적인 사진인데, iOS 뉴비라면 모두가 고통받고 있을 만한 문제의 솔루션을 잘 제시한 사진이다. constraint가 중첩되거나 누락되었을 때 콘솔에 찍히는 에러메시지로부터 정확히 어떤 제약에서 문제가 생긴건지 바로 알 수 있는 방법을 안내한다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/12aa3484-2e5d-4d48-8679-b0be83f04e53/image.png" alt="">
4. <a href="https://www.revenuecat.com/blog/engineering/ios-in-app-subscription-tutorial-with-storekit-2-and-swift/?utm_campaign=iOS%2BDev%2BWeekly&amp;utm_medium=email&amp;utm_source=iOS%2BDev%2BWeekly%2BIssue%2B589">iOS in-app subscription tutorial with StoreKit 2 and Swift</a>
RevenueCat이라는 서비스를 만드는 회사에서 써낸 아티클이다. StoreKit2를 사용한 iOS의 인앱결제 개발 방법을 설명하고 있다. 결제 관련 유틸리티를 만들고 있는 회사지만 자사 제품 광고보다는 설명 그 자체에 많은 공을 들인 걸 알 수 있는 자세한 설명에 감탄했다. 끝은 결국 이 방법의 한계에 대해 설명하며 자사 제품을 쓰면 그 한계를 어떻게 극복할 수 있는지 설명하고 있지만, 그 전까지만 봐도 충분히 매력있는 아티클이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS+Unity] 유니티 뷰 위에 Native iOS UI 얹고 효율적으로 관리하기]]></title>
            <link>https://velog.io/@danna-lee/iOSUnity-%EC%9C%A0%EB%8B%88%ED%8B%B0-%EB%B7%B0-%EC%9C%84%EC%97%90-Native-iOS-UI-%EC%96%B9%EA%B3%A0-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@danna-lee/iOSUnity-%EC%9C%A0%EB%8B%88%ED%8B%B0-%EB%B7%B0-%EC%9C%84%EC%97%90-Native-iOS-UI-%EC%96%B9%EA%B3%A0-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 26 Feb 2023 13:52:16 GMT</pubDate>
            <description><![CDATA[<p>유니티에서도 꽤나 괜찮은 크로스플랫폼 빌드를 지원하고 있다. 물론, iOS 플랫폼도 예외는 아니다. 설정도 그닥 어렵지 않고 클릭 몇 번이면 유니티에서 작성한 코드가 iOS 앱으로 빌드된다.</p>
<p>하지만, 하나 무시할 수 없는 문제가 있다.
유니티는 게임엔진이지, UI에 최적화 되어 있는 것은 아니기 때문에 유니티에서 작업한 UI는 어쩔 수 없이 조금 뿌옇게 화면에 보이게 된다. 유니티가 최근 새로 밀고 있는 UI Toolkit도 예외는 아니다.
문제가 되는 건 구현해야 하는 게 게임 형식의 앱이 아닌 일반 앱의 UI를 가져야 할 때다. 3D 뷰가 필요하지만 게임이 아닌 심플한 메타버스 형식의 앱들이 그 예시가 될 수 있을 것 같다. 이렇게 유니티로만 만들어진 게임이 아닌 앱들의 UI는 섬세하지 않은 눈을 가진 사람도 가장자리나 색 표현이 좀 뿌연데? 하는 느낌을 받을 수 있을 정도다.</p>
<p>그래서 Swift UIKit으로 구현한 iOS 앱 베이스에 일부 3D 뷰만 유니티로 구현하며, 유니티 위에 뜨는 버튼도 네이티브에서 구현하기로 결정했다.</p>
<h2 id="unity-vc에-대한-이해">Unity VC에 대한 이해</h2>
<p>iOS에서 Unity 뷰를 띄울 때는 unity as a library 방식을 이용한다. 간단히 말하면 유니티를 라이브러리처럼 빌드해서 iOS 네이티브 프로젝트에 얹는 방식이다. uaal에 대한 설명은 유니티에서 공식적으로 공개한 <a href="https://github.com/Unity-Technologies/uaal-example">uaal example github</a>에 자세히 나와있다.</p>
<p>iOS 네이티브 코드에서 show unity를 하게 되면 Unity ViewController(줄여서 UnityVC라고 하겠습니다)가 생기고 그 위에 유니티에서 구현한 뷰가 그려지는 방식이다. 이렇게 만들어진 뷰컨트롤러는 우리가 네이티브에서 뷰컨트롤러를 사용하듯 사용할 수 있다. 해당 뷰컨트롤러에 뷰를 자유롭게 추가할 수도 있고 없앨 수도 있다는 의미다.</p>
<h2 id="unity-뷰-위에-네이티브-버튼-얹기">Unity 뷰 위에 네이티브 버튼 얹기</h2>
<p>UnityViewController의 존재에 대해 알았으니 이제 그 뷰컨트롤러 위에 네이티브 버튼을 어떻게 얹을지 고민하면 된다.
버튼 하나만 만들어서 UnityVC에 위치를 계산해 넣어줄 수도 있을 거고, 화면 전체 크기의 뷰를 만들어 그 위에 필요한 UI를 모두 얹어준 뒤 화면 전체 크기만큼 그 뷰를 얹어줄 수도 있을 것이다.
어떤 방법을 택하든 생각해줘야 할 것은 같다.</p>
<ol>
<li>UnityVC를 가져올 수 있어야 하고,</li>
<li>그 위에 addSubview를 할 수 있어야 한다.</li>
</ol>
<h3 id="unityvc-가져오기">UnityVC 가져오기</h3>
<p>위에서 언급한 uaal-example 코드대로 iOS 네이티브(Swift)+Unity 프로젝트를 세팅했다면, <a href="https://github.com/Unity-Technologies/uaal-example/blob/master/NativeiOSApp/NativeiOSApp/MainViewController.mm">MainViewController.mm</a> 파일을 찾을 수 있을 것이다. </p>
<p>여기 보면 ufw가 정의되어 있는 모습을 볼 수 있는데, 이를 중심으로 유니티 프레임워크가 관리된다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/c156d0ed-3246-4523-a8a6-add304093ecf/image.png" alt=""></p>
<p>그리고 타고 들어가다 보면 appController의 이름으로 관리되는 UnityAppController를 볼 수 있고, 또 그 UnityAppControllersms rootViewController를 관리하고 있다.
바로 이 rootViewController를 가져오면 우리는 이 뷰컨트롤러를 UnityViewController로 사용할 수 있게 된다. (그 위에 텍스트, 버튼, 뷰를 얹을 수 있게 된다.)</p>
<p>아래처럼 접근해주면 된다.</p>
<pre><code class="language-c">- (UIViewController*)getUnityController {
    return [[[self ufw] appController] rootViewController];
}</code></pre>
<h3 id="unitymanager-구축하기">UnityManager 구축하기</h3>
<p>어느 VC에서든 유니티 뷰에 접근할 일이 있는데, 유니티 관련 로직이 uaal example에서처럼 특정 VC 내에서만 관리된다면, 개발에 큰 어려움이 있을 것이다.
그래서 iOS native에 Unity를 통합한 사례들을 보면 UnityManager를 싱글톤으로 구현해 접근을 관리하고 있다. UnityManager를 구축하는 방법은 설명한 블로그가 많기 때문에 여기서는 따로 언급하지 않고, <a href="https://medium.com/mop-developers/launch-a-unity-game-from-a-swiftui-ios-app-11a5652ce476">예시 블로그</a>를 첨부하겠다.</p>
<p>UnityManager를 구축했다면, 그 안에 위에서 정의한 getUnityController 메서드를 위치시키고, 어디서든 유니티 컨트롤러를 부를 수 있게 해주면 된다.
예시 블로그와 다르게 나는 UnityManager을 objc 코드로 구현했고, objective-c와 swift가 통신할 수 있게 브릿지를 구축해 swift 코드에서 사용할 수 있게 했다.
어디서든 아래의 코드를 입력하면 현재 떠있는 UnityVC를 가져올 수 있다.</p>
<pre><code class="language-swift">let unityVC: UIViewController? = UnityManager.sharedInstance().getUnityController()</code></pre>
<h3 id="vc-위에-뷰-추가하기">VC 위에 뷰 추가하기</h3>
<p>ViewController까지 가져왔다면 그 위에 뷰를 추가하는 건 일도 아니다.
그냥 항상 하듯이 addSubview()를 이용해주면 된다.
예를 들어, UnityVC 크기만한 뷰를 위에 얹는 건 아래 코드처럼 쓸 수 있다.</p>
<pre><code class="language-swift">let unityVC = UnityManager.sharedInstance().getUnityController()

let view = TempView(frame: (unityVC?.view.bounds)!)
unityVC?.view.addSubview(view)</code></pre>
<br/>

<h2 id="unityvc-위-네이티브-뷰-관리하기">UnityVC 위 네이티브 뷰 관리하기</h2>
<p>버튼 하나씩 UnityVC 위에 추가해줄 수도 있지만, 전체화면 크기의 뷰 단위로 UnityVC 위에 추가하고 삭제하며 관리하는 게 더 효과적이다.
구현해야 하는 UI 특성상 상태에 따라 UI 버튼 구성 전체가 바뀌어야 하는 경우가 많기 때문이다.
그러나, 이 위에 얹어지는 뷰를 뷰컨트롤러로 만들 수는 없는데, 유니티뷰까지 터치이벤트까지 가야 하기 때문이다. UnityVC 위에 overlay로 ViewController를 얹을 경우 유니티에서 터치이벤트를 관리할 수 없는 한계가 있었다.</p>
<p>View로 관리하면 가장 큰 문제가 있는데, 바로 네비게이션 컨트롤러나 modal present, dismiss 같은 뷰컨트롤러 방식대로 VC 스택 관리를 할 수 없다는 것이다.
직접 View를 얹고 삭제하며 관리해주어야 한다.
그래서 선택한 방법은 VC처럼 View의 계층과 생성 삭제를 관리해주는 싱글톤 ViewManager를 만들어 관리하는 방법이었다.</p>
<p>나는 이 관리 모듈을 <code>UnityOverlayViewManager.swift</code> 로 이름 지었다.
이 매니저가 해야 할 일은 크게 3개가 있다.</p>
<ol>
<li>뷰 전환하기</li>
<li>현재 있는 뷰 위에 뷰 하나 더 쌓기</li>
<li>뷰 삭제하기</li>
</ol>
<h3 id="unityoverlayviewmanager에서-뷰-전환하기">UnityOverlayViewManager에서 뷰 전환하기</h3>
<p>예를 들어 네비게이션 컨트롤러 위에서 관리되는 ViewController에서의 뷰 전환은 <code>navigationController?.pushViewController(vc, animated: true)</code>와 같은 코드로 간단하게 구현할 수 있다.
UnityOverlayViewManager에서의 뷰 전환도 이처럼 간단하게 호출할 수 있도록 구현했다.</p>
<p>뷰를 전환할 때 홈을 중심으로 버튼을 누르면 UI가 전환 되었다 돌아와야 하는 구조로 되어 있다. 따라서 홈은 계속해서 추가, 삭제 되지 않아야 했다.</p>
<p>다른 뷰로 전환될 때 홈은 hidden 처리 되고, 홈이 아닌 다른 뷰들은 removeFromSuperView 되게 구현했다.
모든 뷰는 싱글톤 UnityOverlayViewManager의 viewStack으로 관리될 수 있도록 설계했다.</p>
<pre><code class="language-swift">private var viewStack: [UIView] = []</code></pre>
<br/>

<p>우선 홈이 처음 로드될 때 우리 매니저 위에서 관리되어야 했다.</p>
<pre><code class="language-swift">/// 가장 처음 홈이 로드될 때 호출
public func initializeHome() {
    let home = HomeView(frame: (unityVC?.view.bounds)!)
    unityVC?.view.addSubview(home)
    viewStack.append(home)
}</code></pre>
<p>위 코드를 보면 홈뷰를 로드해 유니티에 addSubview 한 후 viewStack에 추가하는 모습을 볼 수 있다.
<br/></p>
<p>그리고 전환하는 로직은 아래 코드처럼 쓸 수 있다.
기본적인 메커니즘은 다음과 같다.</p>
<ol>
<li><p>홈은 뷰 추가, 삭제하지 않는다. 대신 isHidden true, false 처리를 한다.</p>
</li>
<li><p>홈이 아닌 모든 뷰는 unityVC 위에 추가/삭제 한다.</p>
</li>
<li><p>unityVC 위 모든 뷰는 viewStack으로 관리되고 있어야 한다.</p>
<pre><code class="language-swift">/// 홈으로 전환하기
public func presentHome() {
 guard let firstView = viewStack[0] as? HomeView else { return }

 firstView.isHidden = false // 홈은 addSubview 대신 숨김 false 처리
 removeAllSubviews() // 홈 이외에 모든 뷰는 삭제
}
</code></pre>
</li>
</ol>
<p>/// (홈이 아닌) 아예 새로운 뷰로 전환하기
public func present(_ newView: UIView) {
    if let _ = newView as? HomeView {
        fatalError(&quot;HomeView로 전환할 때는 presentHome()을 사용하세요.&quot;)
    }</p>
<pre><code>let homeView = viewStack[0]
homeView.isHidden = true // 홈은 뷰 삭제 대신 숨김 처리

viewStack.append(newView) // 새로운 뷰를 뷰 스택에 추가
unityVC?.view.addSubview(newView) // 새로운 뷰를 unityVC에 추가

removeAllSubviews(includeLast: false) // 홈과 방금 추가한 뷰를 제외하고 모두 삭제</code></pre><p>}</p>
<pre><code>```swift
/// [모든 뷰 지우기]
/// includeLast: 다음 뷰 호출 로직이 사라져야 할 뷰 내에 있을 때를 위한 안전장치
private func removeAllSubviews(includeLast: Bool = true) {
    if includeLast {
        for view in viewStack[1...] {
            view.removeFromSuperview()
        }
    } else {
        for view in viewStack[1...].dropLast() {
            view.removeFromSuperview()
        }
    }
}</code></pre><p>모든 뷰를 지우는 부분에서 왜 싱글톤 매니저에서 뷰스택을 관리해야 하는지 알 수 있다.
뷰 스택으로 관리되지 않으면 매번 unityVC.view.subviews로 뷰를 가져와야 할 텐데, 뷰를 추가할 때마다 viewStack에 그 reference를 등록해둔 덕에 바로바로 접근해 삭제가 가능하다.</p>
<p>하나 이상해 보일 수 있는 부분이 있을 수 있는데, present 로직에서 왜 모든 뷰를 삭제한 후에 새로운 뷰를 더하지 않고, 새로운 뷰를 더한 후에 나머지 뷰들을 삭제하냐는 것이다.
그 이유는 예를 들어 A라는 뷰가 있고, A라는 뷰가 삭제되고 B라는 뷰로 전환되어야 하는 경우가 있다고 가정한다. 그럼 A라는 뷰에서 B 뷰로 전환하는 과정에서 B 뷰가 추가되기 전에 A 뷰가 삭제되는 경우가 생긴다. 이 경우를 위해 우선 필요한 모든 뷰를 addSubview 한 후 사용하지 않는 뷰를 삭제하는 순서로 로직을 전개했다.</p>
<p>해당 메서드를 사용해 실제로 뷰를 전환하려면 간단하게 아래처럼 쓸 수 있다.</p>
<pre><code class="language-swift">UnityOverlayViewManager.shared.presentHome() // 홈으로 전환
UnityOverlayViewManager.shared.present(TempView()) // 홈 이외의 뷰로 전환</code></pre>
<h3 id="현재-있는-뷰-위에-뷰-하나-더-쌓기">현재 있는 뷰 위에 뷰 하나 더 쌓기</h3>
<p>위처럼 present까지 구현했다면 뷰를 하나 더 쌓는 건 일도 아니다.
그냥 unityVC에 뷰를 하나 더해주고 뷰 스택에 추가해주면 된다.</p>
<pre><code class="language-swift">/// 현재 있는 뷰 위에 뷰 쌓기
public func addView(_ newView: UIView) {
    viewStack.append(newView)
    unityVC?.view.addSubview(newView)
}</code></pre>
<h3 id="뷰-삭제하기">뷰 삭제하기</h3>
<p>뷰 삭제하기도 마찬가지로 복잡하지 않다.
모든 뷰를 삭제하고 viewStack에서도 제거해주면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 블로그 글 지지리도 안 쓰는 개발자, 글또를 시작하며...]]></title>
            <link>https://velog.io/@danna-lee/%EA%B0%9C%EB%B0%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B8%80-%EC%A7%80%EC%A7%80%EB%A6%AC%EB%8F%84-%EC%95%88-%EC%93%B0%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B8%80%EB%98%90%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@danna-lee/%EA%B0%9C%EB%B0%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B8%80-%EC%A7%80%EC%A7%80%EB%A6%AC%EB%8F%84-%EC%95%88-%EC%93%B0%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B8%80%EB%98%90%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0</guid>
            <pubDate>Fri, 10 Feb 2023 15:47:06 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, 개발 블로그와 담 쌓은 개발자입니다.
개발자가 코드만 쓰는 사람이라 하기엔 글로 무언가를 설명하고 기록해야 할 일이 많아 이번에 글또(글쓰는 또라이들)이라는 개발자 글쓰기 커뮤니티에 가입했습니다.</p>
<p>반 년동안 2주에 한 번 글을 적어야 하는데, 그 첫 발걸음으로 다짐을 모두가 보는 이곳에 박제해볼까 합니다.</p>
<ol>
<li><p><strong>가장 단순하지만, 6개월 동안 한 번도 빠지지 않고 글을 적어내 보고 싶습니다.</strong> 글 쓰는 것을 좋아했지만 이제는 많이 멀어져 버려, 그 과정에서 잃어버린 자신감을 되찾는 것이 목표입니다.</p>
</li>
<li><p><strong>그리고 그다음의 목표는 개발 생태계에 기여할 수 있는 의미 있는 글을 적는 것입니다.</strong> 별 것 아니지만 IT동아리 활동을 하며 신입회원들을 대상으로 git 세션을 주최한 적이 있었습니다. 그때 자료를 준비하며 적었던 블로그 글에 만 명이 넘는 사람이 방문했고, 심지어는 링크를 알려준 적 없던 지인의 북마크에도 들어가 있는 모습을 보았습니다. 그때 글로써 개발 생태계에 기여하는 뿌듯함을 처음 경험했습니다. 단순히 보증금을 위해 글자 채우기로 적는 글이 아니라, 제 글이 작게나마 한국 개발 생태계의 선순환에 한 발 보탰으면 하는 마음으로 글을 적어나가고 싶습니다.</p>
</li>
<li><p><strong>욕심을 좀 내보자면, 글에 철학을 녹여내는 것입니다.</strong> 무엇이든 본질을 찾아가 천천히 생각하고 정리하는 것을 좋아합니다. 개발도 예외는 아니었습니다. 회사에서도, 친구들과 프로젝트를 할 때도 항상 이유 있는 결정을 하고자 했고, 그 과정에서의 사고 과정, 고려된 것들, 도출된 결론을 종종 공유했습니다. 이 과정에서 빈 공간이 많이 채워지고, 서로의 생각이 한 곳으로 얼라인 될 수 있어 함께 일하는 분들은 제가 이렇게 공유하는 시간을 좋아했습니다. 이 점이 저와 함께 일하는 분들뿐만 아니라 개발을 공부하는 분들에게도 어쩌면 도움이 될 수 있겠다 생각이 듭니다. 개발 스택과 같은 결정을 할 때 단순히 유행을 따라 선택하는 것보다 이유 있는 선택을 돕기 위해 본질부터 잘 고찰하고 정리한 글을 적고 싶습니다.</p>
</li>
<li><p><strong>마지막으로, 다양한 경험을 가진 분들을 알아가고 싶습니다.</strong> 제 글만 적어내려가는 게 아니라 6개월 동안은 다른 분들의 글도 많이 읽고, 피드백을 남겨보려 합니다. 다른 사람들은 어떤 생각을 하고 사는지, 어떤 일을 하고 있는지 많은 이야기를 듣고, 저도 제가 가진 경험과 인사이트를 함께 공유하고 싶습니다.</p>
</li>
</ol>
<p>글또 활동을 하면서 해이해질 때마다 이 글을 보고 다시금 마음을 잡으러 들어오겠습니다.</p>
<p>그럼 파이팅!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity Multiplayer] Boss Room 게임 분석 - 1.2 Scene Script 분석]]></title>
            <link>https://velog.io/@danna-lee/Unity-Multiplayer-Boss-Room-%EA%B2%8C%EC%9E%84-%EB%B6%84%EC%84%9D-1.2-Scripts-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@danna-lee/Unity-Multiplayer-Boss-Room-%EA%B2%8C%EC%9E%84-%EB%B6%84%EC%84%9D-1.2-Scripts-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Tue, 16 Aug 2022 14:42:59 GMT</pubDate>
            <description><![CDATA[<p>전 편인 <a href="https://velog.io/@danna-lee/Unity-Multiplayer-Boss-Room-%EA%B2%8C%EC%9E%84-%EB%B6%84%EC%84%9D-1.1-Scene-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC">1.1 Scenes 디렉토리</a>에서 플레이어가 게임 플레이 시 마주할 장면 순서대로 Scene의 구조를 살펴보았습니다.</p>
<p>이번에는 그 뒤에서 어떤 로직들이 일어나고 있는지 더 자세히 분석하려 합니다.</p>
<p>저번의 방식과 똑같이 시간 순서대로 나열하려면 필요없는 로직부를 너무 많이 설명하게 될 것 같아 이번에는 주요 아키텍쳐나 구현 방식 중심으로 적어보겠습니다.</p>
<br>

<h1 id="scene-관련-로직">Scene 관련 로직</h1>
<h2 id="scene-이동">Scene 이동</h2>
<h3 id="startup---mainmenu-이동">Startup -&gt; MainMenu 이동</h3>
<p>전 글에서도 잠깐 언급했듯, 모든 건 Startup Scene에서 시작합니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/0d432dec-a88c-49ff-a5ea-0aa482991831/image.png" alt=""></p>
<p>하지만 Startup 씬을 보면 이렇게 로딩 페이지만 있고,
실제로는 어떻게 메인 메뉴 씬으로 넘어가게 되는지 알지 못합니다.</p>
<p>그 해답은 script에 있는데요,
hierarchy를 보면 ApplicationController라는 게 가장 위에 보입니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/9324f907-7390-4e51-ae40-4bb1f7e38838/image.png" alt=""></p>
<p>스크립트로 들어가보니 상단부에 summary가 있고, ApplicationController가 어떤 역할을 하고 있는지에 대한 설명이 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/72d09b8d-d382-4803-8012-97c83ef063e7/image.png" alt=""></p>
<blockquote>
<p>&quot; An entry point to the application, where we bind all the common dependencies to the root DI scope. &quot;<br>
<em>-&gt; 애플리케이션의 엔트리포인트이며, root DI(Dependency injection; 의존성 주입) scope에 공통적으로 사용되는 모든 객체의 의존성을 정의하는 곳이다.</em></p>
</blockquote>
<p>쉽게 말해, 애플리케이션 실행 주기 전반에 걸쳐 사용될 것들의 초기 세팅을 하는 곳이라 이해했습니다.</p>
<p>그래서 코드 구조를 보면 이벤트 함수인 Awake()에 의존성 주입과 관련된 세팅 코드가 있고, 모든 세팅이 끝난 이후에 불리는 Start()에 비로소 MainMenu를 부르는 코드가 적혀 있습니다.</p>
<pre><code class="language-cs">namespace Unity.Multiplayer.Samples.BossRoom.Shared
{
    /// &lt;summary&gt;
    /// An entry point to the application, where we bind all the common dependencies to the root DI scope.
    /// &lt;/summary&gt;
    public class ApplicationController : MonoBehaviour
    {
        private void Awake()
        {
            Application.wantsToQuit += OnWantToQuit;

            DontDestroyOnLoad(gameObject);
            DontDestroyOnLoad(m_UpdateRunner.gameObject);

            var scope = DIScope.RootScope;

            scope.BindInstanceAsSingle(this);
            scope.BindInstanceAsSingle(m_UpdateRunner);
            scope.BindInstanceAsSingle(m_GameNetPortal);
            scope.BindInstanceAsSingle(m_ClientNetPortal);
            scope.BindInstanceAsSingle(m_ServerGameNetPortal);

            // ...

            Application.targetFrameRate = 120;
        }

        private void Start()
        {
            SceneManager.LoadScene(&quot;MainMenu&quot;); // MainMenu Scene으로 이동
        }
    }
}</code></pre>
<p>여기서 잠깐 Awake와 Start 이벤트 함수가 불리는 정확한 시점에 관한 <a href="https://docs.unity3d.com/Manual/ExecutionOrder.html">유니티 문서</a>를 보고 가겠습니다.</p>
<blockquote>
<p><strong>Awake</strong>: This function is always called before any Start functions and also just after a prefab is instantiated. (If a GameObject is inactive during start up Awake is not called until it is made active.) <br>
<em>-&gt; Start 함수 실행 전, 프리팹이 초기화 되고 난 즉시 불린다.</em></p>
</blockquote>
<blockquote>
<p><strong>Start</strong>: Start is called before the first frame update only if the script instance is enabled.<br>
<em>-&gt; 첫 프레임이 업데이트 되기 전에 불린다. 단, 스크립트 인스턴스가 활성화 되었을 때만 불린다.</em></p>
</blockquote>
<p>따라서 의존성 주입 등의 세팅 작업이 Awake에서 끝나면, Start 함수에 정의된 MainMenu를 호출하는 코드가 실행되게 되고, 바로 메인메뉴로 이동하게 되는 것입니다.</p>
<br>

<h2 id="팝업-표출">팝업 표출</h2>
<h3 id="mainmenu의-ip로-시작-팝업">MainMenu의 &#39;IP로 시작 팝업&#39;</h3>
<p>메인메뉴에서 두 번째 버튼(START WITH DIRECT IP)을 누르면 특정 IP를 지정해 게임을 호스트하거나 참여할 수 있는 팝업이 뜹니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/c5b4bcdf-2d48-4ffe-a13e-38559729a29e/image.png" alt=""><img src="https://velog.velcdn.com/images/danna-lee/post/9351a76c-3d9d-4bcc-97cf-3b3410719a31/image.png" alt=""></p>
<p>이 팝업을 띄우는 로직을 살펴보려 합니다.</p>
<p>MainMenu Scene hierarchy를 보면 맨 위에 MainMenuState를 볼 수 있습니다. 상태값에 따라 MainMenu 각 버튼을 비롯한 UI를 변화시키는 로직이 담긴 스크립트입니다. 
MainMenuState의 inspector를 보면 ClientMainMenuState.cs 스크립트와 연결이 되어 있는 것을 확인할 수 있습니다. 
여기서 다시 &#39;IP로 시작 팝업&#39; 관련 로직은 serializedField로 관리되고 있는 IPUIMediator에 연결된 IPPopup에서 관장하고 있겠네요.
IPPopup은 IPUIMediator.cs라는 스크립트에 연결된 것까지 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/efb94250-2b5a-4c5d-8da0-c92103f45055/image.png" alt=""></p>
<p>IPUIMediator.cs에 들어가보면 긴 코드가 보이지만, 
우리가 집중해야 할 건 몇 줄 안 됩니다.
중요한 코드만 모아 보겠습니다.</p>
<pre><code class="language-cs">namespace Unity.Multiplayer.Samples.BossRoom.Visual
{
    public class IPUIMediator : MonoBehaviour
    {
        [SerializeField] CanvasGroup m_CanvasGroup; // 팝업 캔버스

        public void Show()
        {
            m_CanvasGroup.alpha = 1f;
            m_CanvasGroup.interactable = true;
            m_CanvasGroup.blocksRaycasts = true;

            DisableSignInSpinner();
        }

        public void Hide()
        {
            m_CanvasGroup.alpha = 0f;
            m_CanvasGroup.interactable = false;
            m_CanvasGroup.blocksRaycasts = false;
        }
    }
}</code></pre>
<p>팝업을 어떻게 띄우는지 보려고 했던 것이므로,
팝업을 띄우고 없애는 로직 이외에는 다 지워봤습니다.</p>
<p>상당히 직관적인 코드입니다.
없애고 나타내야 할 때 알파값을 조정하고, 사용자 인터랙션을 끄고 켜고, raycast를 막고 푸는 작업을 합니다. </p>
<p>팝업은 기본적으로 보이는 상태고,
IPUIMediator.cs 스크립트의 Awake()에서 Hide() 되고 있습니다.</p>
<pre><code class="language-cs">namespace Unity.Multiplayer.Samples.BossRoom.Visual
{
    public class IPUIMediator : MonoBehaviour
    {
        void Awake()
        {
            Hide();
        }
    }
}</code></pre>
<p>기본적으로는 Hide 되어 있는 팝업이지만, START WITH DIRECT IP 버튼을 누르면 팝업이 나와야 합니다.</p>
<p>이 로직은 Hierarchy의 IP Start Button 프리팹의 버튼 컴포넌트 &gt; On Click에서 시작점을 찾아볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/474b0d2a-0103-40bd-a9ae-5bc3bdb4a3dd/image.png" alt=""></p>
<p>버튼이 클릭되면 ClientMainMenuState.cs에 정의되어 있는 OnDirectIPClicked라는 메서드를 부르라고 되어 있네요.</p>
<p>따라가보면 이런 코드를 볼 수 있습니다.</p>
<pre><code class="language-cs">namespace Unity.Multiplayer.Samples.BossRoom.Client
{
    public class ClientMainMenuState : GameStateBehaviour
    {
        [SerializeField] LobbyUIMediator m_LobbyUIMediator;
        [SerializeField] IPUIMediator m_IPUIMediator;

        public void OnDirectIPClicked()
        {
            m_LobbyUIMediator.Hide();
            m_IPUIMediator.Show();
        }
    }
}</code></pre>
<p>로비 팝업 (맨 위 버튼 누르면 나오는 팝업)이 혹시 있다면 숨기고,
IP 팝업을 나타나게 하라는 간단한 코드입니다.</p>
<p>각각 Hide와 Show를 따라 들어가보면 또 알파값, 유저인터랙션, 래이케스트 값을 조정하는 코드가 있겠죠.</p>
<p>이렇게 팝업을 나타나게 하는 로직을 살펴봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/9351a76c-3d9d-4bcc-97cf-3b3410719a31/image.png" alt=""></p>
<p>이제는 팝업이 사라지는 로직을 살펴보겠습니다. 간단합니다.</p>
<p>만약 우상단의 X 버튼이 클릭된다면 Cancel Button 오브젝트의 Button &gt; On Click에서 Hide() 메서드를 불러줍니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/2d28ca9d-940f-4442-8cc1-94f5fccef977/image.png" alt=""></p>
<p>그런데 보니 Hide만 불리고 있는 게 아니라
CancelConnectionWindow라는 메서드도 함께 불리고 있습니다.</p>
<p>얘는 뭘까요?</p>
<pre><code class="language-cs">namespace Unity.Multiplayer.Samples.BossRoom.Visual
{
    public class IPUIMediator : MonoBehaviour
    {
        // To be called from the Cancel (X) UI button
        public void CancelConnectingWindow()
        {
            RequestShutdown();
            m_IPConnectionWindow.CancelConnectionWindow();
        }

        void RequestShutdown()
        {
            if (m_ConnectionManager &amp;&amp; m_ConnectionManager.NetworkManager)
            {
                m_ConnectionManager.RequestShutdown();
            }
        }
    }
}</code></pre>
<p>간단히 말해 네트워크 통신 중이었다면 연결을 끊고, 
로딩창도 숨기라는 뜻입니다.</p>
<p>자세한 건 직접 코드를 살펴보시면 쉽게 이해하실 수 있을 것 같습니다. (코드를 타고타고 들어가다 보면 관련된 코드가 너무 많아 상세히 적지 않겠습니다)</p>
<p>이렇게 팝업 띄우고 없애기를 모두 살펴보았습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity Multiplayer] Boss Room 게임 분석 - 1.1 Scene 디렉토리]]></title>
            <link>https://velog.io/@danna-lee/Unity-Multiplayer-Boss-Room-%EA%B2%8C%EC%9E%84-%EB%B6%84%EC%84%9D-1.1-Scene-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC</link>
            <guid>https://velog.io/@danna-lee/Unity-Multiplayer-Boss-Room-%EA%B2%8C%EC%9E%84-%EB%B6%84%EC%84%9D-1.1-Scene-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC</guid>
            <pubDate>Fri, 12 Aug 2022 13:09:33 GMT</pubDate>
            <description><![CDATA[<h1 id="scenes-폴더">Scenes 폴더</h1>
<p>본격적으로 폴더 하나하나를 뜯어보겠습니다.</p>
<p>우선 Scenes 폴더 내부는 아래처럼 이루어져 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/c6f362ab-5404-443d-a3fa-e93bb763959f/image.png" alt=""></p>
<h2 id="startup-scene">Startup Scene</h2>
<p>Startup에서 게임을 시작할 수 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/fbd25786-5b9a-42ed-a7b4-66d006150650/image.png" alt=""></p>
<!--
TODO: 여기에 로딩이 어떻게 메인메뉴 화면으로 넘어가는지 적기
-->

<h2 id="mainmenu-scene">MainMenu Scene</h2>
<p>바로 MainMenu Scene으로 넘어가는데요,
<img src="https://velog.velcdn.com/images/danna-lee/post/b546d039-8a74-49d7-8379-7b24bd1d3f70/image.png" alt=""></p>
<p>복잡하게 떠있는 팝업을 잠깐 가려보면, 이런 화면을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/f44f9b37-cdd7-45f2-a0b1-6d0fe7557e99/image.png" alt=""></p>
<p>게임을 시작하기 위해 사용자가 가장 먼저 보게 되는 장면입니다.</p>
<p>중앙 부분에 버튼이 크게 세 개 있는데,</p>
<p><strong>[START WITH LOBBY]</strong>
START WITH LOBBY 버튼은 유니티의 Lobby, Relay와 연동하면 활성화되며,
로비에 방을 만들어 게임을 시작하는 방식입니다.</p>
<blockquote>
<p>처음 들어오면 첫 번째 버튼이 비활성화 되어 있을 텐데, <br></p>
</blockquote>
<ol>
<li>본인의 유니티 대시보드로 가서</li>
<li>새 프로젝트 생성</li>
<li>Multiplayer &gt; Lobby, Relay 활성화</li>
<li>Unity 에디터 프로젝트 세팅 &gt; Services에서 계정 연결  <br>
해주면 버튼이 활성화되고, 친구들과 로비를 만들어 플레이할 수 있게 됩니다.
자세한 내용은 Boss room 공식 문서에서 찾아볼 수 있습니다. <br></li>
</ol>
<p>+) 설정 해주지 않아도 두 번째 버튼을 눌러 게임을 해 볼 수 있습니다.</p>
<p><strong>[START WITH DIRECT IP]</strong>
이 방법은 별도의 서버 없이 로컬호스트로 로컬에서 게임을 시작하는 방법이나 포트포워딩을 통해 포트 번호로 직접 게임에 참여하는 방법입니다.
로컬호스트로 자신의 ip가 서버와 동시에 클라이언트가 되어 바로 게임을 해볼 수 있습니다.</p>
<p><strong>[CHANGE PROFILE]</strong>
프로필을 생성, 변경할 수 있는 버튼입니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/d0308d72-c33d-4253-abc0-db62db2b1234/image.png" alt=""></p>
<p>각각 Lobby Start Button, IP Start Button, Profile Button으로 만들어져 있고,
각 버튼에 다른 클릭 액션이 매핑되어 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/e32d9b4a-a440-4f75-8f0d-280be9a7c153/image.png" alt=""></p>
<p>예시로 Lobby Start BUtton &gt; Button 컴포넌트의 On click인데,
클릭이 되었을 때 ClientMainMenuState.cs 파일의 OnStartClicked 메서드를 트리거 하고 있는 것을 확인할 수 있습니다.</p>
<pre><code class="language-cs">namespace Unity.Multiplayer.Samples.BossRoom.Client
{
    public class ClientMainMenuState : GameStateBehaviour
    {
        // ...

        public void OnStartClicked()
        {
            m_LobbyUIMediator.ToggleJoinLobbyUI();
            m_LobbyUIMediator.Show();
        }

        // ...
    }
}</code></pre>
<p>여기서 <code>m_LobbyUIMediator</code>는 SerializedField로 정의되어 UI 상에서 연결이 되어 있는데,
MainMenuState 프리팹에서 ClientMainMenuState.cs 파일에 연동되어 있는 컴포넌트를 보면,
LobbyPopup에 연결되어 있는 걸 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/02f9bf07-4862-43d9-87d8-d3d6343b0255/image.png" alt=""></p>
<p>LobbyPopup은 다시 LobbyUIMediator.cs 파일에 연동되어 있으며,
ToggleJoinLobbyUI()가 이렇게 구현되어 있습니다.</p>
<pre><code class="language-cs">namespace Unity.Multiplayer.Samples.BossRoom.Visual
{
    public class LobbyUIMediator : MonoBehaviour
    {
        public void ToggleJoinLobbyUI()
        {
            m_LobbyJoiningUI.Show();
            m_LobbyCreationUI.Hide();
            m_JoinToggleHighlight.SetToColor(1);
            m_JoinToggleTabBlocker.SetToColor(1);
            m_CreateToggleHighlight.SetToColor(0);
            m_CreateToggleTabBlocker.SetToColor(0);
        }
    }
}</code></pre>
<p><code>m_</code>으로 선언되어 있는 것들은 역시 SerializedField로 정의되어 스크립트 컴포넌트에서 연결이 되어 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/3e94c0bc-3bb8-48e9-9b10-70e135c3cdf7/image.png" alt=""></p>
<p>즉, START WITH LOBBY 버튼이 클릭되면,
연결되어 있는 각 오브젝트를 보여주고, 숨기고, 색을 설정하는 작업을 하게 됩니다.</p>
<p>프로젝트 구조만 간단히 알아보려고 했으니 
버튼은 여기까지만 하고, 다른 씬으로 넘어가보겠습니다.</p>
<h2 id="charselect-scene">CharSelect Scene</h2>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/6e63cc21-88d3-4a26-9462-d4ce85e2c3d7/image.png" alt=""></p>
<p>게임 방에 참여하면 캐릭터를 고르게 됩니다.</p>
<p>모든 플레이어가 캐릭터를 고르고 READY! 버튼을 누르면 게임이 시작됩니다.</p>
<h2 id="bossroom-scene">BossRoom Scene</h2>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/97544c55-14e3-4386-9a5f-fe039bf25c44/image.png" alt=""></p>
<p>테스트 플레이 할 때 마음대로 게임을 설정할 수 있는 Debug Cheat 팝업과 게임 설명 팝업이 나옵니다.
이 팝업을 잠시 꺼보면 게임의 맵이 보입니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/d87d8dd8-c982-460b-b320-ff1afd9eecf9/image.png" alt=""></p>
<p>BossRoom에는 특이한 점이 하나 있는데,
최상위 게임오브젝트가 하나인 다른 씬들과는 다르게 총 4개의 최상위 게임 오브젝트로 이루어져 있다는 점입니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/60ba9afd-e0dc-40fb-9fc5-4f4a7c2fa9f9/image.png" alt=""></p>
<p>그 이유는 하나처럼 보이는 던전 맵이 총 세 부분으로 나누어져 있기 때문인데,
<img src="https://velog.velcdn.com/images/danna-lee/post/b8999fc9-4516-47f1-abbf-39b6e2dce81e/image.png" alt=""><img src="https://velog.velcdn.com/images/danna-lee/post/579054db-41bf-4e61-97e7-40494ae8d72a/image.png" alt=""><img src="https://velog.velcdn.com/images/danna-lee/post/9bb85980-4838-40a0-8e64-0a8e01cd7524/image.png" alt=""></p>
<p>플레이어들이 게임에 입장하면 가장 처음 스폰되는 Entrance, 
문을 열고 들어가면 나오는 Transition,
보스가 등장하는 BossRoom으로 구성됩니다.</p>
<p>이들이 왜 구분되어 있고, 어떻게 하나처럼 유기적으로 동작하는지는 나중에 Transition 방식을 살펴보는 글에서 자세히 적겠습니다.</p>
<h2 id="postgame-scene">PostGame Scene</h2>
<p>게임이 끝나면 게임 결과를 알려주고 메인 메뉴로 나가거나 다시 게임할 수 있게 하는 씬입니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/db88daea-391e-4086-a32c-66bff8a5b952/image.png" alt=""></p>
<p>이렇게 게임 진행 순서에 따라 씬을 정리해보았습니다.</p>
<p>다음 글에서는 Scripts 폴더를 살펴보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity Multiplayer] Boss Room 게임 분석 - 1. 프로젝트 구조]]></title>
            <link>https://velog.io/@danna-lee/Unity-Multiplayer-Boss-Room-%EA%B2%8C%EC%9E%84-%EB%B6%84%EC%84%9D-1.-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@danna-lee/Unity-Multiplayer-Boss-Room-%EA%B2%8C%EC%9E%84-%EB%B6%84%EC%84%9D-1.-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Fri, 12 Aug 2022 13:04:31 GMT</pubDate>
            <description><![CDATA[<p>유니티는 Multiplayer 게임을 지원하기 위해 <a href="https://docs-multiplayer.unity3d.com/netcode/current/about">Netcode for GameObjects</a>라는 걸 지원하고 있는데, 
데모 프로젝트인 <a href="https://unity.com/demos/small-scale-coop-sample">Boss Room</a>이라는 게임도 함께 만들어두었습니다. 
물론, 깃허브에 소스코드도 모두 공개해주었습니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/6733173c-d746-4600-bb6b-571579e42df3/image.png" alt=""> <img src="https://velog.velcdn.com/images/danna-lee/post/5a4fb0da-2c93-4138-abad-b2baa6685b4f/image.png" alt=""></p>
<p>유니티로 멀티플레이를 처음 구현해보는 사람에게는 교과서같은 프로젝트입니다. </p>
<p>유니티를 공부할 때 Docs나 강의, 책을 보고 공부하는 것도 좋지만, 가끔은 이렇게 잘 만들어진 소스코드 전체를 확인하면서 흐름을 잡아가는 것도 좋아 보입니다.</p>
<p>이 보스룸 프로젝트를 분석해 도움이 될 만하거나 흥미로운 지점들을 계속해서 써내려 갈 예정인데, 
이 포스팅에서는 프로젝트의 가장 기본이 되는 구조부터 뜯어보겠습니다.</p>
<p>글을 읽기 전에 실제로 게임을 한 판 하고 오시면 이해에 도움이 됩니다.
2P 이상 참여해야 플레이가 가능합니다. (한 명이 버튼 누르고 문 열어줄 때 나머지 한 명이 문을 통과해야 함)</p>
<br>

<h1 id="assets-폴더">Assets 폴더</h1>
<p>Assets 폴더는 씬, 개발자가 작성한 소스코드, 프리팹이 모두 모여 있는 폴더입니다.
유니티 개발을 하면 대부분의 작업은 이 Assets 폴더 안에서 하게 됩니다.</p>
<p>전체 파일의 구조는 아래와 같지만, 너무 방대하니 조금씩 잘라서 설명하겠습니다.</p>
<pre><code>
.
├── Animations
│   └── UI
├── Fonts
├── GameData
│   ├── Action
│   │   ├── Archer
│   │   ├── Boss
│   │   ├── General
│   │   ├── Imp
│   │   ├── Mage
│   │   ├── Rogue
│   │   ├── Tank
│   │   └── VandalImp
│   ├── Avatars
│   ├── Character
│   │   ├── Archer
│   │   ├── Imp
│   │   ├── ImpBoss
│   │   ├── Mage
│   │   ├── Rogue
│   │   ├── Tank
│   │   └── VandalImp
│   ├── Collections
│   ├── Game
│   │   ├── EnemySpawner
│   │   └── SpawnedEnemy
│   ├── GameEvents
│   ├── Shared
│   ├── Systems
│   ├── Transforms
│   └── UI
├── Material
│   ├── Characters
│   │   └── Toon
│   └── Dungeon
├── Models
│   ├── Animated
│   └── Animation Controllers
├── Prefabs
│   ├── Actions
│   ├── CharGFX
│   │   ├── CharacterGraphics
│   │   │   └── CharacterSelect
│   │   └── Head
│   ├── Character
│   ├── Dungeon
│   │   └── Dungeon Pieces
│   │       ├── animated
│   │       ├── crystal
│   │       ├── pillar
│   │       └── pots
│   ├── Game
│   │   └── StaticNetworkObjects
│   ├── GameCam
│   ├── Menus
│   ├── State
│   └── UI
├── Scenes
│   ├── BossRoom
│   └── PostGame
├── Scripts
│   ├── ApplicationLifecycle
│   │   └── Messages
│   ├── Audio
│   ├── CameraController
│   ├── Editor
│   │   └── Readme
│   ├── Gameplay
│   │   ├── Action
│   │   ├── Configuration
│   │   ├── ConnectionManagement
│   │   ├── DebugCheats
│   │   ├── GameState
│   │   ├── GameplayObjects
│   │   │   ├── AnimationCallbacks
│   │   │   ├── Audio
│   │   │   ├── Character
│   │   │   │   └── AI
│   │   │   └── RuntimeDataContainers
│   │   ├── Input
│   │   ├── Messages
│   │   └── UI
│   │       └── Lobby
│   ├── Infrastructure
│   │   ├── PubSub
│   │   └── ScriptableObjectArchitecture
│   │       └── Editor
│   ├── Navigation
│   ├── UnityServices
│   │   ├── Auth
│   │   ├── Infrastructure
│   │   │   └── Messages
│   │   └── Lobbies
│   │       └── Messages
│   ├── Utils
│   │   └── NetworkOverlay
│   └── VisualEffects
├── Shaders
├── Sounds
│   ├── Character
│   │   ├── Archer
│   │   ├── Boss
│   │   ├── Imp
│   │   ├── Mage
│   │   ├── Rogue
│   │   ├── Shared
│   │   └── Tank
│   ├── Env
│   └── Music
├── StreamingAssets
├── Tests
│   └── Runtime
├── TextMesh Pro
│   ├── Documentation
│   ├── Fonts
│   ├── Resources
│   │   ├── Fonts &amp; Materials
│   │   ├── Sprite Assets
│   │   └── Style Sheets
│   ├── Shaders
│   └── Sprites
├── Textures
│   ├── Characters
│   ├── Environment
│   └── UI
│       └── Unity
├── URP
│   └── Mobile
└── VFX
    ├── Materials
    ├── Meshes
    ├── Prefabs
    │   ├── Archer
    │   ├── Boss
    │   ├── Caster
    │   ├── Environment
    │   ├── Imp
    │   │   └── Throw_bomb
    │   ├── Rogue
    │   ├── Shared Hero FX
    │   ├── Tank
    │   └── UI
    ├── Shaders
    └── Textures</code></pre><br>

<p>다른 걸 다 잘라내고 Assets 하위에 위치한 폴더만 살펴보면,
Assets 폴더의 하위는 아래와 같은 서브 폴더들로 구성되어 있습니다.</p>
<pre><code>.
├── Animations
├── Fonts
├── GameData
├── Material
├── Models
├── Prefabs
├── Scenes
├── Scripts
├── Shaders
├── Sounds
├── StreamingAssets
├── Tests
├── TextMesh Pro
├── Textures
├── URP
└── VFX</code></pre><p>대중적인 구조에서 크게 벗어나지 않는 파일 구조를 가지고 있습니다.
게임 씬 저장을 위한 Scenes,
스크립트 저장을 위한 Scripts,
프리팹 저장을 위한 Prefabs, 등등 ...
익숙한 구조입니다. </p>
<p>TextMesh Pro가 익숙하지 않으시다면, 기존의 유니티 UI 텍스트를 완벽히 대체하며 더 고도화된 기능을 제공하는 플러그인이고,
(원래는 유니티 에셋 스토어에서 유료로 팔고 있던 에셋인데 유니티에서 인수해 무료로 제공하게 되었다고 합니다.)</p>
<p>이어지는 글에서 프로젝트 구조를 하나하나 뜯어 살펴보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node.js + TypeScript를 heroku로 배포하기 ]]></title>
            <link>https://velog.io/@danna-lee/Node.js-TypeScript%EB%A5%BC-heroku%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@danna-lee/Node.js-TypeScript%EB%A5%BC-heroku%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 21 Jul 2022 16:55:44 GMT</pubDate>
            <description><![CDATA[<p>서버는 항상 배포가 일이다..
남의 컴퓨터를 쓰는 일은 역시 쉽지 않다.</p>
<p>웹 프론트엔드 개발 후 vercel로 배포를 하면서 버튼 하나로 배포 되는 건 진짜 신세계다 생각하고 있었는데, 몇 달 전 서버에서는 heroku가 간편한 배포를 지원하고 있다는 걸 알게 되었다.</p>
<p><a href="">https://www.heroku.com</a></p>
<p>다양하게 커스텀하기에는 한계가 있지만 간편하게 서버를 띄우고 싶을 땐 꽤 유용하다. 무려 https로 배포해준다.</p>
<br>

<h3 id="헤로쿠-가입">헤로쿠 가입</h3>
<p>만약 헤로쿠가 처음이라면 <a href="">여기</a>로 들어가서 가입하면 된다.</p>
<br>

<h3 id="앱-생성">앱 생성</h3>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/b6e536df-bdd6-4fb3-a2ba-c6e942c45a02/image.png" alt="">
바로 create new app을 눌러 앱을 생성한다.
만약 계정에 앱이 하나도 없는 상태라면 조금 더 큰 버튼으로 떴던 기억이 난다. 어떻게 생겼든 create new app이라고 쓰인 버튼을 눌러주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/a9692094-6d28-42a2-9401-d199176b159c/image.png" alt="">
앱 이름을 정하고 지역 설정을 해준다.
지역은 United States, Europe 두 개밖에 없어서 나는 미국으로 해줬다.</p>
<p>그 후 바로 Create app 버튼을 눌러준다.</p>
<p>무료계정에서 앱은 최대 100개까지 생성할 수 있다고 한다. (비인증 시 5개)</p>
<br>

<h3 id="프로젝트-연결-깃허브">프로젝트 연결 (깃허브)</h3>
<p>프로젝트 파일이 깃허브에 있다면 바로 연결해줄 수 있다. 가장 간편한 방법이다.
<img src="https://velog.velcdn.com/images/danna-lee/post/8e0b7f38-e953-4501-bbd5-1495dcd379c9/image.png" alt=""></p>
<p>Deploy 탭에서 Deployment method를 보면 세 가지 옵션이 있는데,
그 중 GitHub을 클릭해준다.</p>
<p>Connect to GitHub 쪽을 보면 계정을 선택할 수 있다.
개인계정/organization 모두 선택 가능하다.
만약 원하는 organization이 보이지 않는다면, 아래 Ensure Heroku Dashboard has team access 버튼을 눌러 권한을 부여해준다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/302361c9-3ae6-4072-af68-64a9db883cea/image.png" alt=""></p>
<p>버튼이 만약 Grant라면 내가 organization의 오너라 바로 권한을 부여할 수 있는 경우고, Request라면 나는 member라서 organization의 오너에게 대신 권한 허용 알림이 가게 된다.</p>
<p>권한 허용을 해주고 나면 해당 오가니제이션이 헤로쿠에서 성공적으로 읽힐 것이다.
<img src="https://velog.velcdn.com/images/danna-lee/post/86f22df8-43c4-4c89-ad2a-cd6b2a8e6e11/image.png" alt=""></p>
<p>원하는 오가니제이션 혹은 개인 계정을 선택해주고 서버 코드가 있는 repo-name을 검색한다. 
<img src="https://velog.velcdn.com/images/danna-lee/post/b078d4f2-92d0-47aa-a06a-f20b44faef6e/image.png" alt=""></p>
<p>레포를 찾았다면 Connect를 눌러준다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/fd6834d5-ff45-4b77-968c-44957bd40443/image.png" alt=""></p>
<p>성공적으로 연결되면 여러 설정을 할 수 있게 되는데,
develop 브랜치에 머지가 되면 자동으로 배포하는 옵션 (Automatic deploys)를 설정해주었다.</p>
<p>만약 github action 등으로 수행되는 CI 단계가 있다면 Wait for CI to pass before deploy 설정을 꼭 해주는 것이 좋다.</p>
<p>Manual deploy에서 특정 브랜치를 선택해 즉시 배포할 수도 있다.</p>
<br>

<h3 id="프로젝트-연결-cli">프로젝트 연결 (CLI)</h3>
<p>만약 코드가 GitHub에 있지 않다면, CLI로 연결해줄 수 있다.</p>
<p>Deployment method에서 Heroku Git을 선택하면 방법이 안내되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/4c2578bc-82f1-446a-a4db-06605738ed58/image.png" alt=""></p>
<p>우선 <a href="https://devcenter.heroku.com/articles/heroku-cli">Herocu CLI</a>를 설치해줘야 한다. </p>
<p>맥을 쓴다면 terminal에서 <code>brew tap heroku/brew &amp;&amp; brew install heroku</code> 명령어로 설치할 수 있다.</p>
<p>설치가 되었다면 terminal에서 heroku 로그인을 해주어야 한다.
<code>heroku login</code>을 해서 시키는대로 하면 로그인이 된다. (브라우저에서 로그인을 시킬 것이다)</p>
<p>그 후 terminal에서 코드가 있는 곳으로 이동해서
<code>heroku git:remote -a [앱 이름]</code>을 입력해준다.
여기서 <code>[앱 이름]</code>은 우리가 헤로쿠에서 만든 앱 이름이다.
git으로 관리되고 있는 로컬의 코드를 헤로쿠 깃(리모트)와 연결하는 과정이다.</p>
<p>만약 로컬에 코드가 없다면 위 명령어 대신 <code>heroku git:clone -a [앱 이름]</code>을 해주면 된다. 클론을 받아왔다면 <code>cd [앱 이름]</code> 명령어로 해당 폴더로 이동한다.</p>
<p>그런 후에는 헤로쿠 깃으로 푸시해주면 바로 빌드가 되는데, 푸시는 <code>git push heroku master</code>로 간단히 할 수 있다.
만약 현재 로컬의 브랜치가 master가 아니라면 에러가 뜰 텐데, 그러면 <code>git push heroku [현재 브랜치 이름]:master</code> 명령어를 써주면 된다.
예를 들어 develop 브랜치에서 작업하고 있다면, <code>git push heroku develop:mater</code> 이런 식이다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/1afeebea-4a20-4d53-aca2-4f533198d918/image.png" alt=""></p>
<p>그럼 자동으로 빌드가 시작된다.
빌드에서 에러가 나도 괜찮다. 아직 자잘한 설정을 안 맞춰준 탓이다.</p>
<p>만약 여기까지 했는데 빌드가 되고 API 호출도 성공적으로 된다면 나는 정말 운 좋은 사람이다 세 번 외치고 뒤로가기 버튼을 누르면 된다.</p>
<br>

<h3 id="procfile-생성">Procfile 생성</h3>
<p>Procfile은 헤로쿠 서버가 시작될 때 수행할 명령을 적어주는 파일이다.
루트에 파일을 만들어주면 된다.</p>
<p>상황에 맞게
<code>web:node dist</code>나 <code>web:npm start</code>를 해주면 된다.</p>
<p>뒤에서 node BuildPack을 추가해 줄 텐데, 빌드팩이 있으면 Procfile은 없어도 된다고 하니 넘어가도 된다.</p>
<br>

<h3 id="packagejson-확인">Package.json 확인</h3>
<p>헤로쿠 서버는 typescript를 처리하지 못한다.
그래서 tsc로 javascript 전환 후 dist 파일을 node로 컴파일 해야 한다.</p>
<pre><code class="language-JSON">// package.json

&quot;scripts&quot;: {
  &quot;start&quot;: &quot;node dist/server.js&quot;,
  &quot;build&quot;: &quot;tsc&quot;,
  &quot;postinstall&quot;: &quot;npm run build&quot;,
},</code></pre>
<p>나는 이 조합을 선호한다.</p>
<p>빌드 시에 <code>tsc</code>로 javascript로 전환하고,
start 시에 <code>node dist/server.js</code>를 하는 방법이다.
<code>dist</code>나 <code>server.js</code>는 자신의 프로젝트에서 설정한 폴더/파일 이름을 넣어주면 된다.</p>
<p>postinstall이 중요한데,
postinstall을 생략하면 build 후에 멈춰버리는 현상이 생긴다.
tsc를 실행하고 아무것도 실행되지 않는 상태로 빌드가 얼어버리는 것이다.
꼭 postinstall 설정을 해주자.</p>
<p>(나는 프로젝트에서 yarn을 쓰고 있는데, 이상하게 yarn은 또 안 될 때가 있어서 heroku 서버에서는 npm run build를 해주고 있다.)</p>
<p>그 외에 엔진 버전(node, yarn 등)을 명시하라는 에러가 날 수도 있는데,
그럴 때는 package.json 최상단에 아래 코드를 추가해준다.
프로젝트 세팅에서 사용하는 버전에 맞게 써주면 된다. 대부분은 아래와 같을 것이다.</p>
<pre><code class="language-json">// package.json

&quot;engines&quot;: {
  &quot;node&quot;: &quot;16.x&quot;
  &quot;yarn&quot;: &quot;1.x&quot;
},</code></pre>
<br>

<h3 id="buildpack-추가">Buildpack 추가</h3>
<p>Node를 쓰는 프로젝트는 빌드팩을 추가해주어야 한다.
터미널 해당 프로젝트 폴더 경로에서 <code>heroku buildpacks:set heroku/nodejs</code>로 추가해줄 수 있다.</p>
<p>혹은 경로를 찾아가지 않고도 <code>-a [앱 이름]</code> 옵션을 줘도 된다.</p>
<p>추가된 노드 빌드팩은 헤로쿠 대시보드의 세팅에서 확인할 수 있다.
<img src="https://velog.velcdn.com/images/danna-lee/post/318b06e6-591d-4cea-b63a-9d861576f2f2/image.png" alt=""></p>
<br>

<h3 id="환경-변수-등록">환경 변수 등록</h3>
<p>만약 프로젝트가 환경변수를 사용하고 있다면 (DB 정보 등) 헤로쿠에 이 정보도 등록해주어야 한다.</p>
<p>보통은 .env 파일을 gitignore 해 사용하지만, 헤로쿠에는 안타깝게도 파일을 올릴 수 없다. (ec2와 가장 크게 다른 점이다)</p>
<p>헤로쿠는 매번 푸시가 될 때마다 모든 파일을 덮어쓰기 때문에 서버 컴퓨터에 접속해 .env 파일을 등록했다고 하더라도 다음 빌드에서 삭제될 것이다.</p>
<p>따라서 사이트에서 하나하나 등록해주어야 한다. (구글에 찾아보면 다른 방법도 많았는데, 뭔갈 많이 설치해야 하고 잘 안 되는 경우도 많아서 나는 사이트에서 등록해주는 편이 가장 속 편했다)</p>
<p>헤로쿠 사이트 &gt; 대시보드 &gt; 우리가 쓰는 앱으로 들어가면,
Setting에서 Config Vars 항목을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/376ba1de-ed0d-4b6c-8865-e28a001e5017/image.png" alt=""></p>
<p>여기가 바로 환경변수를 등록해주는 곳이다.</p>
<p>Reveal Config Vars를 눌러서 key와 value를 각각 등록해주면 된다.</p>
<br>

<h3 id="빌드-확인">빌드 확인</h3>
<p>여기까지 따라왔다면 빌드를 한 번 해보자.
헤로쿠 깃의 브랜치로 푸시하면 바로 빌드가 진행된다.
<code>git push heroku [현재 브랜치 이름]:master</code> 명령어로 푸시할 수 있다.
<img src="https://velog.velcdn.com/images/danna-lee/post/bb829bce-350c-4276-8cac-963eb915d631/image.png" alt=""></p>
<p>빌드가 성공하면 빌드 성공이라는 메시지와 함께
배포된 링크도 함께 나온다.</p>
<h3 id="로그-확인하기">로그 확인하기</h3>
<p>API 호출 로그는 <code>heroku logs --tail -a [앱 이름]</code>으로 확인해 볼 수 있다.
만약 terminal에서 해당 프로젝트 폴더 위치에 있다면 <code>-a [앱 이름]</code>을 생략하고 <code>heroku logs --tail</code>만 입력해도 된다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/38079b9c-4294-4c8c-a1e8-23f8d5e87a68/image.png" alt=""></p>
<p>이런 식으로 로그가 들어온다.</p>
<br>


<h3 id="팁-계정에서-빌드-가능한-횟수를-초과했다는-에러-해결-방법">팁) 계정에서 빌드 가능한 횟수를 초과했다는 에러 해결 방법</h3>
<p>CLI로 빌드를 열심히 돌리다 보면 갑자기 계정에서 빌드 가능한 횟수를 초과했다는 에러가 나기도 한다.</p>
<p>당황하지 말고,
이전에 실행했던 빌드가 끝나지 않은 상태로 갇혀있거나 아직 빌드 진행 중인 경우에 이런 에러가 뜬다.</p>
<p><code>heroku builds:cancel -a [앱 이름]</code>으로 이전에 실행된 빌드를 강제 종료할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity에서 Swift 코드로 iOS native 구현하기(feat. HealthKit)]]></title>
            <link>https://velog.io/@danna-lee/Unity%EC%97%90%EC%84%9C-Swift-%EC%BD%94%EB%93%9C%EB%A1%9C-iOS-native-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0feat.-HealthKit</link>
            <guid>https://velog.io/@danna-lee/Unity%EC%97%90%EC%84%9C-Swift-%EC%BD%94%EB%93%9C%EB%A1%9C-iOS-native-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0feat.-HealthKit</guid>
            <pubDate>Wed, 06 Jul 2022 06:13:23 GMT</pubDate>
            <description><![CDATA[<p>Unity가 다양한 플랫폼과의 호환을 거의 완벽할 정도로 구사하고 있긴 하지만, 아무래도 코드 내에 native 코드가 필요할 때가 아예 없는 건 아닙니다.
특정 OS에서 프레임워크 API로 내려주고 있는 코드를 쓰고 싶을 때, 즉 iOS의 경우에는 -Kit 형태로 이름이 붙은 HomeKit, HealthKit, MapKit, ARKit 등등의 프레임워크를 쓰는 경우가 좋은 예시가 될 것 같습니다.</p>
<p>Unity로 개발하던 중에 iOS의 HealthKit과 연동해 기기 사용자의 건강 정보를 읽고 업데이트 하고 싶었는데, 한글 자료는 물론 외국 자료도 마땅치 않더라구요. 그래서 작성하는 글입니다.</p>
<p>기본적으로 Unity 프로젝트에 Swift 코드를 임베드 하는 방법을 설명하고, HealthKit 연동 방법까지 공유합니다.</p>
<br/>

<h1 id="0️⃣-unity와-ios-네이티브-프레임워크-연동">0️⃣ Unity와 iOS 네이티브 프레임워크 연동</h1>
<h2 id="희망편---주류-프레임워크">희망편 - 주류 프레임워크</h2>
<p>네이티브 alert 팝업, 갤러리/카메라 권한 사용을 위한 UIImagePickerController와 그 delegate 등은 워낙 Unity에서도 많이 사용되는 네이티브 프레임워크라 에셋 스토어에 무료 또는 아주 싼 가격에 <a href="https://assetstore.unity.com/packages/tools/integration/native-gallery-for-android-ios-112630?locale=ko-KR#description">플러그인</a>이 공유 되고 있습니다.
이런 플러그인들은 비교적 최근까지 잘 관리가 되어 있고, 사용자 층이 두터워 커뮤니티 내에서 질문과 응답이 활발하게 오가기 때문에 디버깅도 간편합니다.</p>
<p>간단히 다운 받아서 시키는대로 설정한 후 시키는대로 코드를 쓰면 어려운 단계 없이 바로 동작합니다.</p>
<p>unity와 iOS 네이티브 프레임워크 연동 희망편입니다.</p>
<br/>

<h2 id="절망편---비주류-프레임워크">절망편 - 비주류 프레임워크</h2>
<p>하지만 내가 쓰려 하는 프레임워크가 이렇게 자주 쓰이는 프레임워크가 아니라면?
제목에도 있는 HealthKit 역시 swift로 개발하는 iOS 네이티브 내에서도, 물론 Unity에서 iOS 앱 최적화를 할 때도 비주류에 속합니다.
사실 비주류라는 용어도 부끄러울 만큼 google에 그 어떤 자료도 나오지 않습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/f3969976-1a64-419a-ae86-5b5a2891b502/image.png" alt=""></p>
<p>사진에 캡쳐되어 있는 게 의미있는 검색 결과의 전부입니다.</p>
<p>그나마 <a href="https://assetstore.unity.com/packages/tools/integration/behealthkit-39962#description">BEHealthKit</a>이라는 플러그인이 하나 나오긴 하는데, 1 seat 당 40달러라는 무서운 가격을 가지고 있고 공식적으로 제공하는 <a href="http://beliefengine.com/BEHealthKit/1.11/">docs</a>가 가격에 비해 친절하지 않았습니다.</p>
<p>결국 5만원을 지불하고도 stackoverflow와 기타 개발 커뮤니티를 전전하며 디버깅 해야 할 상황이 올 것 같았습니다. </p>
<p>결정적으로 BEHealthKit을 사용하다 <a href="https://stackoverflow.com/questions/64991606/healthkit-capabilities-are-not-adding-through-unity-c-sharp-script">stackoverflow에 질문한 이렇게 긴 글</a>에
<img src="https://velog.velcdn.com/images/danna-lee/post/0bf4fc49-6bb6-41f7-b01a-7d3a6df4df70/image.png" alt=""></p>
<p>답변이 딱 하나 달렸고, 그 마저도 2줄짜리에, 질문자의 댓글이 <code>Yes, But...</code>으로 시작하는 절망적인 케이스였습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/41760da8-eda0-4e08-822c-857f84d2984a/image.png" alt=""></p>
<p>그렇다고 Unity에서 HealthKit 연동을 시도한 흔적(그 흔한 가이드 블로그글, 스택오버플로우 질문, 유니티 포럼 질문)이 많은 편도 아니었습니다. 다 합쳐서 겨우 다섯 손가락을 채울 정도였습니다.</p>
<p>이게 비주류의 슬픔인가?</p>
<p>결국 접근 방법을 바꾸기로 했습니다.</p>
<p>5만원을 주고도 오랫동안 헤매야 할 미래가 눈앞에 훤히 보였고, 그렇게 고생해봤자 HealthKit 연동 이외에는 쓸 구석이 없는 플러그인이라면 과감히 포기하기로 했습니다. 기왕 고생할 거 제대로 고생해서 HealthKit 뿐만 아니라 다른 Swift 코드도 쓰고 싶을 때 언제든지 써서 임베드 할 수 있는 확장성 있는 기반을 다져놓자 다짐했습니다.</p>
<br/>
<br/>


<h1 id="1️⃣-유니티-프로젝트에-swift-코드-임베드-하기">1️⃣ 유니티 프로젝트에 Swift 코드 임베드 하기</h1>
<p>Swift로 iOS 네이티브 앱 개발을 할 때 HealthKit을 써본 적이 있기 때문에 Swift 코드로는 HealthKit 연동이 매우 간단하다는 걸 알고 있었습니다.</p>
<p><strong>그럼 그냥 Unity 프로젝트에서 Swift가 해석되게 하면 되는 거 아닌가?</strong> 라는 바보같고도 단순한 질문에서 시작되었습니다.
말처럼 간단하지 않았거든요 ㅎ</p>
<br/>

<h3 id="-왜-유니티에서-swift-코드를-쓰지-못할까">+) 왜 유니티에서 Swift 코드를 쓰지 못할까?</h3>
<p>이 질문은 <strong>왜 unity에서 네이티브 최적화를 할 때는 objective-C를 쓸까?</strong>라는 질문의 연장선에 있습니다. </p>
<p>iOS 개발을 조금이라도 찍먹해본 사람은 다 알고 있듯 Objective-C는 이제 레거시 취급을 받고 있고, 예전에 개발되어 그나마 남아있는 Objective-C 코드들도 다 Swift로 리팩토링 하는 추세입니다. </p>
<p>그런데도 왜 유니티와 iOS 키워드로 구글에 검색하면 다 Objective-C 코드만 나오는지 궁금했습니다. 1-2년 내에 쓰인 비교적 최근 글들임에도 불구하고요.</p>
<p>(아래는 꼭 iOS 플러그인을 만드는 데 Objective-C를 써도 괜찮을 경우 참고할 만한 괜찮은 자료들입니다)
<a href="https://www.youtube.com/watch?v=krerK59xVPI">[YouTube] Unity3d iOS Plugins - How To Create An iOS Plugin With Unity3d ?</a>
<a href="https://www.youtube.com/watch?v=nnd6fLDgujg">[YouTube] Unity3d iOS Plugins - How To Call Native iOS Alerts From Unity ?</a>
<a href="https://medium.com/@rolir00li/integrating-native-ios-code-into-unity-e844a6131c21">[Medium] Integrating native iOS code into Unity</a></p>
<p>유니티(C#)은 스위프트와 바로 소통하지 못합니다.
그래서 우리는 Objective-C를 브릿지로 이용해야 합니다.</p>
<p>C#은 Objective-C와 소통할 수 있고, Swift 역시 Objective-C로 소통할 수 있으니 C#과 Swift 사이에 Objective-C라는 통역관을 하나 두는 것과 같습니다.</p>
<p>따라서 Unity에서는 Swift를 아예 쓰지 못한다라기보다는 Objective-C를 거치지 않고는 쓰지 못한다라는 표현이 더 정확합니다.</p>
<p>이 점을 고려하며 Swift 함수를 Unity에서 실행시키는 방법에는 몇 가지가 있습니다.
그 중 가장 간편하게 사용할 수 있었던 방법을 몇 가지 소개하겠습니다.</p>
<br/>

<h2 id="1-swift-package---framework---unity-plugin">1. Swift Package -&gt; Framework -&gt; Unity Plugin</h2>
<p><a href="https://docs.unity3d.com/Manual/PluginsForIOS.html">유니티 공식문서</a>와 더불어 어느 한 <a href="https://www.youtube.com/watch?v=1APcNcLAYhQ">유튜브 영상</a>에서 설명하고 있는 방법입니다.</p>
<h3 id="1-xcode에서-package-생성">1) Xcode에서 Package 생성</h3>
<p>Xcode에서 상단바 File &gt; New &gt; Package를 눌러 새로운 패키지를 생성합니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/d0fa337b-de07-48ba-97a7-841bac66d8a5/image.png" alt=""></p>
<p>원하는 위치에 원하는 이름으로 패키지를 생성합니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/043de170-b23d-4c0d-9133-891fed8f5ec4/image.png" alt=""></p>
<br/>

<h3 id="2-보일러-플레이트-정리">2) 보일러 플레이트 정리</h3>
<p>처음 Package가 만들어지면 크게 Source 폴더, Tests 폴더가 생길 것입니다.
하지만, Unity에서 빌드할 때는 패키지를 프레임워크로 추출해서 사용할 것이기 때문에 test 코드는 아예 실행되지 않습니다.
따라서 Test 코드 보일러 플레이트를 삭제해주도록 합시다. (사진에서 드래그 된 부분을 모두 삭제해주면 됩니다)
<img src="https://velog.velcdn.com/images/danna-lee/post/112a3475-a3c9-43b1-8479-11ed9d9f62a3/image.png" alt=""></p>
<p>마찬가지로, Source 폴더를 보면 패키지와 같은 이름의 파일이 하나 생성되어 있는데, 이 보일러 플레이트 코드도 지워줍니다. (드래그 된 부분을 지워주면 됩니다)
<img src="https://velog.velcdn.com/images/danna-lee/post/c8b31771-21cc-4624-8126-f3b5de989ed2/image.png" alt=""></p>
<p>이렇게 두 파일을 각각 class, struct가 정의된 상태로 만들면 본격적으로 코드 쓸 준비가 끝납니다.</p>
<br/>

<h3 id="3-메서드-정의-파일-구현">3) 메서드 정의 파일 구현</h3>
<p>JS로 리액트 개발을 할 때 index.js 파일과 비슷한 역할을 한다고 생각하면 좋습니다.
해당 패키지 내에 어떤 메서드가 있는지 정의하고 라우팅 하는 역할을 합니다.</p>
<p>원하는 이름으로 파일을 하나 생성해줍니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/ecdaffe0-7bdc-44b4-ad04-180d220d7ba1/image.png" alt=""></p>
<p>그리고 메서드를 하나 만들어 볼게요.
이 메서드는 유니티 프로젝트의 C# 코드에서 바로 호출할 메서드이므로, 이름에 <code>Swift</code>, <code>NativeiOS</code> 등을 포함해 이 메서드는 유니티 프로젝트 밖에 있음을 암시하는 이름이면 좋습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/4967ae80-b66e-432a-9c48-98e0af607a0f/image.png" alt=""></p>
<p>그리고 별다른 로직 없이 바로 Swift Package Source 파일 내에 정의된 메서드를 호출해 줍니다.
실제 로직은 <code>NativeiOSCode</code>, 즉 패키지 이름으로 생성된 파일에서 구현합니다.</p>
<p>위 코드를 보면 익숙하지 않은 부분이 한 줄 있을 텐데요,
<code>@_cdecl()</code>은 C 베이스 코드에서 Swift 함수를 호출할 때 사용하는 <strong>비공식 attribute</strong>입니다.
<a href="https://forums.swift.org/t/formalizing-cdecl/40677/44">공식화 하자는 논의</a>는 꽤 있었으나, 플랫폼 호환성이나 안정성 면에서 아직은 공식화가 되지 않고 있습니다. bridging-header를 사용해 C 베이스 언어들과 소통하는 방법이 꾸준히 추천되고 있는 듯합니다.</p>
<p>어쨌든, <code>@_cdecl(&quot;메서드 이름&quot;)</code> 형태로 메서드 위에 적어주면 C 베이스 언어에서 Swift 코드와 소통할 수 있게 됩니다.
그래서 위 코드 이미지에서는 <code>@_cdecl(&quot;NativeiOSCode_runNativeCode&quot;)</code>로 적어주었습니다.</p>
<p>하지만 위에서도 잠깐 언급했듯, 컴파일 단계에서 안정성을 보장하지는 않습니다. (라고 하지만 스위프트 포럼을 보니 심각한 문제가 일어난 적은 없어 보입니다)</p>
<br/>

<h3 id="4-실제-로직-메서드-구현">4) 실제 로직 메서드 구현</h3>
<p>메서드 정의 파일에서 <code>NativeiOSCode_runNativeCode</code> 메서드를 만들고 <code>NativeiOSCode.runNativeCode()</code>로 어떤 메서드를 호출해주었는데요, 여기까지 하면 에러가 난 상태일 겁니다. 바로 해당 메서드가 구현이 되지 않은 상태이기 때문입니다.</p>
<p>다시 NativeiOSCode 파일(패키지 이름으로 생성된 파일)로 가 runNativeCode()라는 함수를 구현해줍니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/ee5b9dca-a97c-465f-aaed-c6804c9bae35/image.png" alt=""></p>
<p>간단히 콘솔에 프린트만 하는 코드를 작성했습니다.</p>
<p>물론, 매개변수가 있는 경우도 정상적으로 동작합니다.</p>
<br/>

<h3 id="5-xcodeproj-파일-생성">5) xcodeproj 파일 생성</h3>
<p>이 상태로 Finder에서 폴더에 들어가보면 xcodeproj 파일이 없는 걸 볼 수 있습니다.
간단한 명령어로 하나 만들어 줄 수 있습니다.</p>
<p>터미널에서 우리가 만든 Swift Package가 있는 폴더로 가 아래 명령어를 입력합니다.
<code>swift package generate-xcodeproj --skip-extra-files</code>
<img src="https://velog.velcdn.com/images/danna-lee/post/98c7565c-39cf-4d95-88f4-90ceb0481eeb/image.png" alt=""></p>
<p>명령어가 성공적으로 실행되면 해당 폴더에 project 파일이 생성됩니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/3326ec8c-67bb-46c7-8d83-786e1b4b51d2/image.png" alt=""></p>
<br/>

<h3 id="6-패키지-빌드">6) 패키지 빌드</h3>
<p>xcodeproj 파일을 생성하고 나면 패키지를 빌드해 프레임워크 파일을 추출할 수 있습니다.
<code>xcodebuild -project [패키지이름].xcodeproj -scheme [패키지이름]-Package -configuration Release -sdk iphoneos CONFIGURATION_BUILD_DIR=.</code></p>
<p>하나 하나 뜯어보면
<code>-project [패키지이름].xcodeproj</code>: [패키지이름] 프로젝트 파일에 대해
<code>-scheme [패키지이름]-Package</code>: 그 중 [패키지이름] 스키마를 (한 프로젝트 파일에 여러 스키마가 존재할 수 있습니다)
<code>-configuration Release</code>: 릴리즈 모드로
<code>-sdk iphoneos</code>: iPhoneOS(iOS)에서 사용할 sdk를
<code>CONFIGURATION_BUILD_DIR=.</code>: 현재 폴더로 빌드한다.</p>
<p>[패키지이름] 프로젝트 파일 중 [패키지이름] 스키마를 릴리즈 모드로 iPhoneOS(iOS)에서 사용할 sdk를 현재 폴더에 빌드한다.
라는 뜻의 명령어가 됩니다.</p>
<p>명령어가 성공적으로 실행되면, framework 파일과 dSYM 파일이 생성됩니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/a654fff1-a0bb-4625-9af6-c7d02fed9bc4/image.png" alt=""></p>
<p>이 중 framework 파일이 우리가 유니티 프로젝트에서 플러그인으로 사용할 파일이 됩니다.</p>
<br/>

<h3 id="7-unity로-프레임워크-import">7) Unity로 프레임워크 import</h3>
<p>Unity 프로젝트의 Assets 폴더 안에 Plugins  &gt; iOS 폴더를 만들고, 그 안에 framework 파일을 import 해줍니다.
간단히 드래그 앤 드롭 해주면 됩니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/8828ec36-93ca-44a6-9665-28cfa61f793c/image.png" alt=""></p>
<p>그 후 해당 framework 파일의 inspector로 가서 Platform settings &gt; Add to Embedded Binaries 체크박스를 체크해주세요.
그 후 Apply를 눌러 적용시켜줍니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/42185df1-4472-46a0-b4ff-a3788815758b/image.png" alt=""></p>
<br/>

<h3 id="8-unity-c-script에서-swift-메서드-호출">8) Unity C# Script에서 Swift 메서드 호출</h3>
<p>원하는 Script에서 Swift 메서드를 호출해줍시다.</p>
<p>우선 외부 메서드를 우리가 script에서 쓸 수 있게 import 해주는 과정이 필요합니다.</p>
<pre><code class="language-cs">using System.Runtime.InteropServices;

public class Showcase : MonoBehaviour 
{
    #if UNITY_IOS
        [DllImport(&quot;__Internal&quot;)]
        private static extern void NativeiOSCode_runNativeCode();
    #endif

    void Start()
    {
    }

    // (하략)
}</code></pre>
<p>코드를 조금 뜯어보자면, <code>DllImport()</code>는 외부 플랫폼의 플러그인을 동적으로 불러올 때 사용합니다. 괄호 안에 플러그인 이름을 적어주면 됩니다.
하지만 우리 코드는 <code>DllImport(&quot;__Internal&quot;)</code>로 쓰여있는 걸 볼 수 있는데요, 여기서 <code>__internal</code>은 정적으로 연결된 네이티브 코드를 쓸 때 사용합니다. 
(더 자세한 설명은 <a href="https://docs.unity3d.com/es/2018.4/Manual/NativePlugins.html">유니티 공식문서</a>에서 확인할 수 있습니다)</p>
<blockquote>
<p>Note that when using Javascript you will need to use the following syntax, where DLLName is the name of the plug-in you have written, or “__Internal” if you are writing statically linked native code:</p>
</blockquote>
<pre><code class="language-cs">@DllImport (DLLName)
static private function FooPluginFunction () : float {};</code></pre>
<p>그리고 함수 선언부의 <code>extern</code> 키워드는 해당 메서드가 유니티 프로젝트의 바깥에 있을 때, 즉 외부 플러그인을 불러올 때 사용합니다.</p>
<p>iOS 네이티브 플러그인은 실제 디바이스 배포 중에만 호출할 수 있기 때문에 이 코드는 특정 플랫폼에서만 실행될 수 있도록 래핑해주는 단계가 필요합니다. 그래서 우리는 코드를 <code>#if UNITY_IOS</code>로 감싸주었습니다.</p>
<p>그 후에는 코드를 바로 호출하기보다 내부에서 처리로직을 한 번 더 두어 안전한 호출을 해봅시다.</p>
<pre><code class="language-cs">public class Showcase : MonoBehaviour 
{
    void Start()
    {
        runNativeCode();
    }

    private void runNativeCode()
    {
        #if UNITY_IOS
            NativeiOSCode_runNativeCode();
        #else
            Debug.Log(&quot;No iOS Device Found&quot;);
        #endif
    }
}</code></pre>
<p>유니티의 이벤트 함수인 <code>Start()</code>에서 <code>runNativeCode()</code>를 불러 스크립트가 실행될 때 실행되도록 해주었고,
실제 <code>runNativeCode()</code> 안에는 별다른 로직 없이 플랫폼 래핑과 분기처리만 해주었습니다.</p>
<p>❗️주의: 플랫폼 래핑 코드를 <code>Start()</code> 안에 쓰면 NoEntry 에러가 나니 꼭 <code>runNativeCode()</code> 안에서 해주어야 합니다.</p>
<p>여기까지 코드를 작성했다면, 전체 코드는 다음과 같습니다.</p>
<pre><code class="language-cs">using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;

public class Showcase : MonoBehaviour 
{
    #if UNITY_IOS
        [DllImport(&quot;__Internal&quot;)]
        private static extern void NativeiOSCode_runNativeCode();
    #endif

    void Start()
    {
        runNativeCode();
    }

    private void runNativeCode()
    {
        #if UNITY_IOS
            NativeiOSCode_runNativeCode();
        #else
            Debug.Log(&quot;No iOS Device Found&quot;);
        #endif
    }
}</code></pre>
<br/>

<h3 id="9-unity-빌드">9) Unity 빌드</h3>
<p>다음은 Unity에서 iOS 플랫폼으로 빌드를 해주면 됩니다. (컴퓨터에 Xcode가 설치되어 있어야 합니다)
<img src="https://velog.velcdn.com/images/danna-lee/post/c34595a1-d7d1-4784-ada9-5488e24ff981/image.png" alt=""></p>
<p>만약 Xcode에서 signing 설정이 안 되어 있다면 로그인 후 프로젝트 세팅에서 Signing &amp; Capabilities 설정을 해줍니다.</p>
<p>설정 후 실행시켜보면 우리가 Swift로 쓴 함수가 실행돼 로그가 잘 찍히고 있는 모습을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/d4d1cb9f-7027-44b1-854e-b82946680f2a/image.png" alt=""></p>
<br/>

<h2 id="2-swift-framework---bridge---unity-plugin">2. Swift Framework -&gt; Bridge -&gt; Unity Plugin</h2>
<p>두 번째 방법은 <a href="https://habibur-rahman-ovie.medium.com/unity-native-ios-plugin-bridge-between-swift-and-unity3d-d88861d0f456">이 미디엄</a>에서 설명하고 있는 방법입니다.</p>
<h3 id="1-xcode에서-ios-framework-생성">1) Xcode에서 iOS Framework 생성</h3>
<p>새로운 프로젝트를 생성해봅시다.
iOS &gt; Framework를 클릭하고 마음에 드는 이름으로 프로젝트를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/2c1cc3f7-2c2a-441d-8d6e-b9e45c67e3b3/image.png" alt=""></p>
<br/>

<h3 id="2-bridge-생성">2) Bridge 생성</h3>
<p>자동으로 Source와 Products가 생성되어 있을 텐데요, (최근 Xcode 업데이트로 Products 파일이 생략되었을 수 있으나 중요하지 않습니다. 없는 상태로 계속 진행하셔도 됩니다.)
Source 폴더에서 이미 생성되어 있는 <code>[프레임워크이름].swift</code> 파일 이외에 <code>[프레임워크이름]Bridge.mm</code> 파일과 <code>[프레임워크이름]-Bridging-Header.h</code> 파일을 생성해주어야 합니다.</p>
<p><code>[프레임워크이름]Bridge.mm</code> 파일은 조금 헷갈릴 수 있는데, cmd + n 혹은 New &gt; File을 누른 후 Objective-C 파일을 선택하면 .m 파일이 생성됩니다. 그럼 파일 이름 변경을 눌러 .mm 파일로 바꿔주면 됩니다.</p>
<blockquote>
<p>.mm 파일은 Objective-C를 컴파일 할 때 C++로 컴파일 되고, .m 파일은 C로 컴파일 된다는 차이가 있습니다. (<a href="https://en.wikipedia.org/wiki/Objective-C#Objective-C.2B.2B">위키피디아</a> 참고)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/danna-lee/post/f1e5a1c1-89d7-4a87-9355-8a1738ee2f69/image.png" alt=""></p>
<br/>

<h3 id="3-swift-메서드-작성">3) Swift 메서드 작성</h3>
<p>위에서 소스 파일을 세 개 만들었는데요, 순서대로 코드를 작성해보겠습니다.</p>
<p><strong>[.swift 파일] - 실제 로직</strong>
[프레임워크이름].swift 파일에 원하는 로직의 메서드를 작성합니다.</p>
<pre><code class="language-swift">import Foundation

@objc public class UnityPlugin : NSObject {

    @objc public static let shared = UnityPlugin()
    @objc public func AddTwoNumber(a:Int,b:Int ) -&gt; Int {

        let result = a+b;
        return result;
    }
}</code></pre>
<p>싱글톤 객체로 선언하였고, 내부 swift 로직이 아닌 objc-briding-header에 의해 읽힐 메서드이니 <code>@objc</code> 키워드를 붙여주었습니다.</p>
<br/>

<p><strong>[.mm 파일] - 유니티와 소통할 수 있게 하는 브릿지</strong>
.mm 파일은 이렇게 작성해주었습니다.</p>
<pre><code class="language-cpp">#import &lt;Foundation/Foundation.h&gt;
#include &quot;UnityFramework/UnityFramework-Swift.h&quot;

extern &quot;C&quot; {

#pragma mark - Functions

    int _addTwoNumberInIOS(int a , int b) {

        int result = [[UnityPlugin shared] AddTwoNumberWithA:(a) b:(b)];
        return result;
    }
}</code></pre>
<p><code>extern &quot;C&quot;</code>는 C++ (.cpp) 또는 Objective-C++ (.mm)를 사용하여 플러그인을 실행할 경우 <a href="https://en.wikipedia.org/wiki/Name_mangling">네임 맹글링 문제</a>를 피하기 위해 함수를 C링크에 선언하는 방법입니다.</p>
<p>그 후 함수 이름을 선언하고, 싱글톤으로 선언된 .swift 파일에서 만들었던 메서드를 불러주었습니다.
여기서 매개변수와 리턴값은 유니티의 호출 코드와 소통합니다.</p>
<br/>

<p><strong>[.h 파일] - 헤더 파일</strong>
.h 파일은 생성하면 코드가 자동으로 입력되어 있을 텐데요, 아래처럼 잘 생성이 되었다면 건들지 않아도 됩니다.</p>
<pre><code class="language-c">#ifndef [프레임워크이름]_Bridging_Header_h
#define [프레임워크이름]_Bridging_Header_h


#endif /* [프레임워크이름]_Bridging_Header_h */
</code></pre>
<br/>

<h3 id="4-유니티-프로젝트에-플러그인-세팅">4) 유니티 프로젝트에 플러그인 세팅</h3>
<p>유니티 프로젝트의 Assets 폴더 안에 Plugins라는 폴더를 생성합니다.
그 안에 iOS 폴더를 생성하고, Xcode에서 작성한 Source 파일 전체를 해당 폴더 안으로 가져옵니다. Source 폴더 안에 우리가 Xcode에서 만든 3개의 파일이 있는 상태여야 합니다.
그 후 Editor 폴더를 추가하고, SwiftPostProcess.cs 파일을 생성해줍니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/667e822d-2a05-4b12-bbb4-d673b40610d0/image.png" alt=""></p>
<br/>


<h3 id="5-swiftpostprocesscs-설정">5) SwiftPostProcess.cs 설정</h3>
<p>위에서 생성한 <code>SwiftPostProcess.cs</code> 파일로 들어갑니다.</p>
<p>그리고 그 파일에 아래 코드를 복사해서 붙여넣어주세요.
[프레임워크이름] 부분을 아까 Xcode에서 생성한 자신의 프레임워크 이름으로 바꿔주면 됩니다. 제 경우에는 UnityIosPlugin입니다.</p>
<pre><code class="language-cs">using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.Diagnostics;

using System.IO;
using System.Linq;

public static class SwiftPostProcess
{

    [PostProcessBuild]
    public static void OnPostProcessBuild(BuildTarget buildTarget, string buildPath)
    {
        if (buildTarget == BuildTarget.iOS)
        {
            var projPath = buildPath + &quot;/Unity-Iphone.xcodeproj/project.pbxproj&quot;;
            var proj = new PBXProject();
            proj.ReadFromFile(projPath);

            var targetGuid = proj.TargetGuidByName(PBXProject.GetUnityTestTargetName());

            proj.SetBuildProperty(targetGuid, &quot;ENABLE_BITCODE&quot;, &quot;NO&quot;);

            proj.SetBuildProperty(targetGuid, &quot;SWIFT_OBJC_BRIDGING_HEADER&quot;, &quot;Libraries/Plugins/iOS/[프레임워크이름]/Source/UnityPlugin-Bridging-Header.h&quot;);

            proj.SetBuildProperty(targetGuid, &quot;SWIFT_OBJC_INTERFACE_HEADER_NAME&quot;, &quot;[프레임워크이름]-Swift.h&quot;);

            proj.AddBuildProperty(targetGuid, &quot;LD_RUNPATH_SEARCH_PATHS&quot;, &quot;@executable_path/Frameworks $(PROJECT_DIR)/lib/$(CONFIGURATION) $(inherited)&quot;);
            proj.AddBuildProperty(targetGuid, &quot;FRAMERWORK_SEARCH_PATHS&quot;,
                &quot;$(inherited) $(PROJECT_DIR) $(PROJECT_DIR)/Frameworks&quot;);
            proj.AddBuildProperty(targetGuid, &quot;ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES&quot;, &quot;YES&quot;);
            proj.AddBuildProperty(targetGuid, &quot;DYLIB_INSTALL_NAME_BASE&quot;, &quot;@rpath&quot;);
            proj.AddBuildProperty(targetGuid, &quot;LD_DYLIB_INSTALL_NAME&quot;,
                &quot;@executable_path/../Frameworks/$(EXECUTABLE_PATH)&quot;);
            proj.AddBuildProperty(targetGuid, &quot;DEFINES_MODULE&quot;, &quot;YES&quot;);
            proj.AddBuildProperty(targetGuid, &quot;SWIFT_VERSION&quot;, &quot;4.0&quot;);
            proj.AddBuildProperty(targetGuid, &quot;COREML_CODEGEN_LANGUAGE&quot;, &quot;Swift&quot;);

            proj.WriteToFile(projPath);
        }
    }

}</code></pre>
<p>이 파일이 없으면 매번 iOS 빌드를 하고 난 후에 pbxproj(프로젝트 세팅 파일)에 들어가서 빌드 프로퍼티를 수정, 생성 해줘야 하는데요, PostProcessBuild를 통해 빌드가 끝나면 자동으로 빌드 프로퍼티를 입력해주는 역할을 합니다.</p>
<p>이 파일에서 objc bridging header의 위치를 가리켜주었고, 제대로 실행될 수 있게 해주었습니다.</p>
<p>혹 권한이라든지 프로젝트 세팅 파일에 더 설정해주어야 할 내용이 생긴다면 <code>proj.WriteToFile(projPath);</code> 위에 적어주면 됩니다.</p>
<br/>

<h3 id="6-unity-c-script에서-swift-메서드-호출">6) Unity C# Script에서 Swift 메서드 호출</h3>
<p>이제 원하는 스크립트 파일에서 스위프트 함수를 불러주기만 하면 됩니다.</p>
<p>패키지를 사용했던 첫번째 방법과 마찬가지로 코드를 작성해보겠습니다. 위와 같은 로직이므로 설명은 하지 않습니다.</p>
<pre><code class="language-cs">using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

public class PluginHelper : MonoBehaviour
{
    #if UNITY_IOS
        [DllImport(&quot;__Internal&quot;)]
        private static extern int _addTwoNumberInIOS(int a, int b);
    #endif

    void Start()
    {
        AddTwoNumber();
    }

    public void AddTwoNumber()
    {
        #if UNITY_IOS
            int result = _addTwoNumberInIOS(10, 5);
            Debug.log(result)
        #else
            Debug.Log(&quot;No iOS Device Found&quot;);
        #endif
    }
}</code></pre>
<br/>

<h3 id="7-unity-빌드">7) Unity 빌드</h3>
<p>유니티에서 iOS를 하고 나면 플러그인에 우리가 만든 프레임워크가 플러그인으로 잘 들어가있는 걸 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/b761d217-911f-40d6-8bc9-f803bb3d44ff/image.png" alt=""></p>
<p>Xcode에 로그도 잘 찍힙니다.</p>
<br/>

<h3 id="주의사항-xcode-작업-코드-빌드-전-꼭-unity로-옮기기">주의사항) XCode 작업 코드 빌드 전 꼭 Unity로 옮기기</h3>
<p>Swift 코드를 수정할 일이 생기면 유니티 에디터에서는 swift 코드 수정을 못 하기 때문에 편의상 iOS 빌드가 된 후 생긴 Xcode 프로젝트 파일로 열게 될 텐데요, 여기서 소스 코드를 수정하고 나면 꼭 빌드 전 Unity 소스파일로 옮겨주어야 합니다. 그냥 빌드를 하면 Xcode 프로젝트 파일은 덮어쓰기가 되기 때문에 힘들게 쓴 코드가 다 날아가는 대참사가 날 수 있습니다. <del>경험담입니다.</del></p>
<br/>

<h1 id="2️⃣-unity에서-ios-healthkit-사용하기">2️⃣ Unity에서 iOS HealthKit 사용하기</h1>
<p>여기까지 따라왔다면 Unity에서 Swift 메서드를 부를 수 있는 상태가 되었습니다.
굉장히 자유롭게 Swift 코드를 쓸 수 있는 상태가 된 겁니다.</p>
<p>그럼 원래 우리의 목표였던 대망의 HealthKit 연동을 해보겠습니다. 이미 Swift 연동을 해놓았으므로 Swift로 iOS 네이티브 코드를 구현하던 것과 같이 구현하면 됩니다.</p>
<p>전 위에서 설명한 방법 중 두번째 방법이었던 프레임워크 생성 후 소스코드를 복사하는 방법으로 구현했습니다.</p>
<p>프레임워크 소스코드 중 <code>.swift</code> 파일에 <code>import HealthKit</code>을 해주고 원하는 로직을 작성합니다. Swift로 HealthKit을 다루는 법은 좋은 아티클과 <a href="https://developer.apple.com/documentation/healthkit/samples/reading_and_writing_healthkit_series_data">애플이 제공하는 데모코드</a>가 많으므로 여기서 자세하게 다루지는 않겠습니다. 구글에 <strong>iOS HealthKit swift</strong> 키워드로 찾아보시기를 추천드립니다.</p>
<p>우리가 신경써줘야 할 것은 이 swift 파일에서 HealthKit을 임포트 했을 때 그걸 Unity가 알아들을 수 있냐는 부분인데, 그 설정을 SwiftPostProcess.cs 파일에서 해주면 됩니다.</p>
<p>파일에 아래 코드를 추가해줍니다.</p>
<pre><code class="language-cs">var manager = new ProjectCapabilityManager(projPath, &quot;Entitlements.entitlements&quot;, null, proj.GetUnityMainTargetGuid());
manager.AddHealthKit();
manager.WriteToFile();</code></pre>
<p>Entitlement에 HealthKit을 추가해주는 방법으로, <code>.AddHealthKit()</code>은 유니티가 iOS와의 호환성을 위해 Capability를 쉽게 등록할 수 있도록 제공하는 메서드입니다. 만약 HealthKit이 아닌 다른 Kit을 등록하고자 할 때도 마찬가지로 <code>manager.Add~</code>로 쓸 수 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/99785465-50c1-42e5-b637-fcdfb1133a74/image.png" alt=""></p>
<p>이 부분이 추가된 SwiftPostProcess.cs의 전체코드는 다음과 같습니다.</p>
<pre><code class="language-cs">using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.Diagnostics;

using System.IO;
using System.Linq;

public static class SwiftPostProcess
{

    [PostProcessBuild]
    public static void OnPostProcessBuild(BuildTarget buildTarget, string buildPath)
    {
        if (buildTarget == BuildTarget.iOS)
        {
            var projPath = buildPath + &quot;/Unity-Iphone.xcodeproj/project.pbxproj&quot;;
            var proj = new PBXProject();
            proj.ReadFromFile(projPath);

            var targetGuid = proj.TargetGuidByName(PBXProject.GetUnityTestTargetName());

            proj.AddFrameworkToProject(targetGuid, &quot;HealthKit.framework&quot;, false);

            proj.SetBuildProperty(targetGuid, &quot;ENABLE_BITCODE&quot;, &quot;NO&quot;);

            proj.SetBuildProperty(targetGuid, &quot;SWIFT_OBJC_BRIDGING_HEADER&quot;, &quot;Libraries/10Etc/Plugins/iOS/EmbeddedSwift/Source/EmbeddedSwift-Bridging-Header.h&quot;);

            proj.SetBuildProperty(targetGuid, &quot;SWIFT_OBJC_INTERFACE_HEADER_NAME&quot;, &quot;EmbeddedSwift-Swift.h&quot;);

            proj.AddBuildProperty(targetGuid, &quot;LD_RUNPATH_SEARCH_PATHS&quot;, &quot;@executable_path/Frameworks $(PROJECT_DIR)/lib/$(CONFIGURATION) $(inherited)&quot;);
            proj.AddBuildProperty(targetGuid, &quot;FRAMERWORK_SEARCH_PATHS&quot;,
                &quot;$(inherited) $(PROJECT_DIR) $(PROJECT_DIR)/Frameworks&quot;);
            proj.AddBuildProperty(targetGuid, &quot;ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES&quot;, &quot;YES&quot;);
            proj.AddBuildProperty(targetGuid, &quot;DYLIB_INSTALL_NAME_BASE&quot;, &quot;@rpath&quot;);
            proj.AddBuildProperty(targetGuid, &quot;LD_DYLIB_INSTALL_NAME&quot;,
                &quot;@executable_path/../Frameworks/$(EXECUTABLE_PATH)&quot;);
            proj.AddBuildProperty(targetGuid, &quot;DEFINES_MODULE&quot;, &quot;YES&quot;);
            proj.AddBuildProperty(targetGuid, &quot;SWIFT_VERSION&quot;, &quot;4.0&quot;);
            proj.AddBuildProperty(targetGuid, &quot;COREML_CODEGEN_LANGUAGE&quot;, &quot;Swift&quot;);

            proj.WriteToFile(projPath);

            var manager = new ProjectCapabilityManager(projPath, &quot;Entitlements.entitlements&quot;, null, proj.GetUnityMainTargetGuid());
            manager.AddHealthKit();
            manager.WriteToFile();
        }
    }
}</code></pre>
<br/>

<p>또, 권한 요청이 있을 경우에 관련 설명을 추가해주어야 하는데, 이 역시도 PostProcess에서 해주면 간편합니다.</p>
<pre><code class="language-cs">rootDict.SetString(&quot;NSHealthShareUsageDescription&quot;, &quot;You can check your exercise record on Apple Fitness&quot;);
rootDict.SetString(&quot;NSHealthUpdateUsageDescription&quot;, &quot;You can check your exercise record on Apple Fitness&quot;);</code></pre>
<p>이렇게 적어주면 iOS 빌드가 되었을 때 세팅 파일에 잘 등록되어 있는 모습을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/danna-lee/post/2a425a15-9039-4cd5-a25b-b574468748c4/image.png" alt=""></p>
<br/>
<br/>
<br/>
<br/>

<p>이렇게 하면 <strong>Unity에서 iOS의 HealthKit 코드를 Swift로 적기</strong>의 대장정이 끝이 납니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python Flask) cannot import name 'Flask' from partially initialized module 'flask' 에러 해결]]></title>
            <link>https://velog.io/@danna-lee/Python-Flask-cannot-import-name-Flask-from-partially-initialized-module-flask-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@danna-lee/Python-Flask-cannot-import-name-Flask-from-partially-initialized-module-flask-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Thu, 09 Dec 2021 02:36:05 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python">from flask import Flask

app = Flask(__name__)


@app.route(&#39;/&#39;)
def hello_world():
    return &#39;Hello, World!&#39;


if __name__ == &quot;__main__&quot;:
    app.run()
</code></pre>
<p>파이썬으로 API 서버를 구축하기 위해 이렇게 간단한 코드를 짜서 실행시켜봤는데, <code>ImportError: cannot import name &#39;Flask&#39; from partially initialized module &#39;flask&#39; (most likely due to a circular import)</code> 에러가 났다.</p>
<p>뭐지.. 또 M1이 모듈 설치 이상하게 해놓고 객기 부리나.. 했는데</p>
<p><img src="https://images.velog.io/images/danna-lee/post/b347218e-0bf1-451c-8701-27088aa7ee1e/image.png" alt=""></p>
<p>ㅋㅋㅋㅋ 옙.
간단한 이유였다.</p>
<p>파일명을 flask.py가 아닌 다른 것으로 바꾸기만 하면 해결된다.</p>
<br/>

<p>저를 5초만에 구해준 위 스택오버플로우 링크는 <a href="https://stackoverflow.com/questions/61032702/importerror-cannot-import-name-flask-from-partially-initialized-module-flask">https://stackoverflow.com/questions/61032702/importerror-cannot-import-name-flask-from-partially-initialized-module-flask</a> 여기입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS) ViewController의 생명주기 공부에서 시작한 ViewController와 View에 대한 고찰]]></title>
            <link>https://velog.io/@danna-lee/iOS-ViewController%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@danna-lee/iOS-ViewController%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Tue, 07 Dec 2021 22:21:31 GMT</pubDate>
            <description><![CDATA[<p>공부는 다른 분들의 블로그 글 말고 공식문서로 하자!
읽기 쉽게 쓰인 글보다 제작자의 의도를 파악할 수 있는 공식문서 읽기 도전
유혹을 뿌리치기 쉽지 않다..</p>
<p>사실 UIViewController 공식문서는 iOS 개발을 시작할 때쯤 (그러니까 아마 작년 이맘때..) 읽어본 적이 있다. 근데 사실 온전히 이해를 못 했었다. 이제 막 걸음마 배우고 있는 단계에 육상선수의 트레이닝법을 설명한 글을 읽고 있었던 것 같은 느낌.
이제 달릴 줄 아는 단계니까 다시 읽어보면 느낌이 또 다르겠지?!</p>
<p>뷰컨트롤러의 생명주기에 대해 공부하기 전에 먼저 ViewController가 뭔지부터 천천히 읽어보고 공부해봤습니다.</p>
<p><code>참고한 공식 문서</code>
<a href="https://developer.apple.com/documentation/uikit/uiviewcontroller">🖥 UIViewController</a>
<a href="https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/index.html#//apple_ref/doc/uid/TP40007457">🖥 View Controller Programming Guide for iOS</a>
<a href="https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview">🖥 loadView()</a></p>
<br/>

<h3 id="uiviewcontroller의-역할">UIViewController의 역할</h3>
<blockquote>
<p>A view controller’s main responsibilities include the following:</p>
</blockquote>
<ul>
<li><p>Updating the contents of the views, usually in response to changes to the underlying data.</p>
</li>
<li><p>Responding to user interactions with views.</p>
</li>
<li><p>Resizing views and managing the layout of the overall interface.</p>
</li>
<li><p>Coordinating with other objects—including other view controllers—in your app.</p>
</li>
<li><p>데이터에 변동이 일어나면 뷰 내용 업데이트 하기</p>
</li>
<li><p>유저 인터랙션(터치, 드래그 등) 처리하기</p>
</li>
<li><p>뷰 크기 조정, 레이아웃 관리하기</p>
</li>
<li><p>다른 뷰컨트롤러 등의 객체들과 상호작용 하기</p>
</li>
</ul>
<p>유저는 한 번에 한 뷰컨트롤러의 뷰만 볼 수 있지만, 그 뷰 컨트롤러 이외에도 다른 뷰 컨트롤러들이 존재할 수 있다. (공식문서에서는 &#39;하나의 뷰 컨트롤러에서 여러 항목을 테이블뷰로 보여주고 있으면 다른 한 뷰 컨트롤러에서 선택된 항목들만 보여주는 뷰가 존재할 수 있다&#39;라고 설명하고 있네요.) 이렇게 뷰 컨트롤러는 새로운 뷰를 보여주기 위해 다른 뷰 컨트롤러를 띄우거나(present), 다른 뷰컨트롤러의 컨테이너 역할을 하면서 뷰에 애니메이션을 적용시키는 역할을 하기도 한다.
<br/></p>
<p><strong>[ViewController의 종류]</strong></p>
<blockquote>
<p>There are two types of view controllers:</p>
</blockquote>
<ul>
<li><p>Content view controllers manage a discrete piece of your app’s content and are the main type of view controller that you create.</p>
</li>
<li><p>Container view controllers collect information from other view controllers (known as child view controllers) and present it in a way that facilitates navigation or presents the content of those view controllers differently.</p>
</li>
<li><p><code>Content view controllers</code>: 앱의 구성요소들을 관리. 메인이 되는 뷰 컨트롤러.</p>
</li>
<li><p><code>Container view controllers</code>: 다른 뷰 컨트롤러(child view controllers; 자식 뷰 컨트롤러)로부터 정보를 수집. 뷰 사이의 이동을 돕거나 자식 뷰 컨트롤러의 내용을 여러 방면에서 보여주는 방식으로 뷰를 띄움. </p>
</li>
</ul>
<p>(말이 어려웠지만, Container view controller는 위에서 설명한 것처럼 애니메이션을 보여준다든지, 다른 뷰로 넘어가는 그 과정을 관장하는 것 같습니다. container view controller의 예시로는 UINavigationController, UITabBarController, UISplitViewController 등이 있습니다.)</p>
<p>대부분의 앱은 이 두 종류의 뷰 컨트롤러가 적절하게 섞여 만들어진다.</p>
<br/>

<h3 id="뷰-관리">뷰 관리</h3>
<p>아무래도 뷰 컨트롤러의 가장 중요한 역할은 뷰의 계층을 관리하는 것이다.(<a href="https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/index.html#//apple_ref/doc/uid/TP40007457">라고 애플이 그랬어요</a>)</p>
<blockquote>
<p>The most important role of a view controller is to manage a hierarchy of views. Every view controller has a single root view that encloses all of the view controller’s content. To that root view, you add the views you need to display your content.</p>
</blockquote>
<p>모든 뷰 컨트롤러는 뷰 컨트롤러 위에 있는 모든 뷰들을 담는 하나의 root view를 가지고 있다. 그래서 우리는 그 root view에 우리가 보여주고자 하는 내용을 얹으면 된다.
<img src="https://images.velog.io/images/danna-lee/post/bdbe6337-9dab-49ff-bbe2-79bd0a68762d/image.png" alt="">
위 사진은 뷰 컨트롤러와 뷰의 관계를 그린 모식도입니다.
보면, 뷰 컨트롤러가 하나의 root view를 가지고 있고, 우리가 화면에 띄우고자 하는 view들이 모두 그 root view 위에 얹어져 있는, 하나의 계층을 이루고 있는 모습을 확인할 수 있어요.</p>
<blockquote>
<p>The view controller always has a reference to its root view and each view has strong references to its subviews.</p>
</blockquote>
<p>뷰 컨트롤러는 항상 root view를 참조하고(가리키고) 있고, 모든 뷰는 그 subview(뷰 계층에서 하단에 위치하는 뷰; 아마 자식 뷰)를 강하게 참조하고 있다.</p>
<p>뷰 컨트롤러와 뷰들의 강한 관계성을 설명하고 있는 듯합니다.</p>
<blockquote>
<p>A content view controller manages all of its views by itself. A container view controller manages its own views plus the root views from one or more of its child view controllers. The container does not manage the content of its children. It manages only the root view, sizing and placing it according to the container’s design. Figure 1-2 illustrates the relationship between a split view controller and its children. The split view controller manages the overall size and position of its child views, but the child view controllers manage the actual contents of those views.</p>
</blockquote>
<p>content view controller는 그 위의 모든 뷰를 스스로 관리한다. container view controller는 자신의 뷰 + 자식 뷰 컨트롤러들의 root view들까지 관리한다. content 자체에는 관여하지 않고, 오직 root view만 컨트롤한다.
사진을 보면 split view controller와 자식 뷰 컨트롤러들을 보여주고 있는데, 최상단의 split view controller는 자식 뷰들의 전체적인 크기와 위치를 관리하고 있지만, 자식 뷰 컨트롤러 위에 있는 실제 보여지는 내용(뷰)들은 자식 컨트롤러가 직접 관리하고 있다.
<img src="https://images.velog.io/images/danna-lee/post/731f5278-1538-4ca9-a75c-298cf0b9ed61/image.png" alt=""></p>
<p>content view controller와 container view controller의 차이를 설명하기 위해 split view controller가 등장했습니다. 모식도를 보면, split view controller 위에 두 개의 자식 뷰 컨트롤러가 얹어져 있는 모습을 볼 수 있습니다. 여기서 강조하고 있는 건, 자식 뷰 컨트롤러의 위치와 크기(즉 어떻게 배치되어 있느냐겠지요)는 container view controller인 split view controller에서 관리하지만, 실제 유저가 보는 화면의 내용 그 자체는 content view controller인 자식 뷰 컨트롤러들 (View Controller A, View Controller B)가 띄우고 있다는 것을 확인할 수 있습니다. 쉽게 말해 IBOutlet 같은 걸 자식 뷰 컨트롤러들에서 정의하고 레이아웃도 거기서 잡아준다는 걸 이야기하고 있는 것 같아요.</p>
<blockquote>
<p>View controllers load their views lazily. Accessing the view property for the first time loads or creates the view controller’s views. There are several ways to specify the views for a view controller:</p>
</blockquote>
<ul>
<li>Specify the view controller and its views in your app’s Storyboard. Storyboards are the preferred way to specify your views. With a storyboard, you specify the views and their connections to the view controller. You also specify the relationships and segues between your view controllers, which makes it easier to see and modify your app&#39;s behavior.
To load a view controller from a storyboard, call the instantiateViewController(withIdentifier:) method of the appropriate UIStoryboard object. The storyboard object creates the view controller and returns it to your code.</li>
<li>Specify the views for a view controller using a Nib file. A nib file lets you specify the views of a single view controller but does not let you define segues or relationships between view controllers. The nib file also stores only minimal information about the view controller itself.
To initialize a view controller object using a nib file, create your view controller class programmatically and initialize it using the init(nibName:bundle:) method. When its views are requested, the view controller loads them from the nib file.</li>
<li>Specify the views for a view controller using the loadView() method. In that method, create your view hierarchy programmatically and assign the root view of that hierarchy to the view controller’s view property.</li>
</ul>
<p>뷰 컨트롤러는 뷰를 lazy하게 로드한다. 뷰 프로퍼티에 처음 접근할 때에야 비로소 뷰 컨트롤러에 뷰를 로드하고 만드는 작업을 하게 된다. 뷰 컨트롤러에 어떤 뷰를 띄워야 할지 정해줄 수 있는 방법이 있다.</p>
<ul>
<li>스토리보드에서 뷰 컨트롤러 위에 뷰를 직접 얹기. 
뷰와 뷰 컨트롤러를 바로 연결해줄 수 있음. 뷰 컨트롤러 간 관계성이나 세그도 정해줄 수 있음. 보기 쉽고 수정하기 쉬워짐.
특정 스토리보드에 있는 뷰 컨트롤러를 로드하기 위해서는 해당 UIStoryboard 객체에서instantiateViewController(withIdentifier:) 메서드를 호출해주면 됨.</li>
<li>Nib file 활용하기.
nib으로 뷰 컨트롤러 위에 해당 뷰를 띄울 수는 있지만, 세그나 뷰 컨트롤러 사이의 관계성을 정의하지는 못함.
nib파일로 뷰 컨트롤러에 뷰를 띄우기 위해서는 init(nibName:bundle:)를 활용. 해당 코드로 뷰가 불러지면 뷰 컨트롤러가 그 nib 파일을 로드하게 됨.</li>
<li>loadView() 메서드 활용.
뷰 계층을 코드로 짜서 해당 뷰 계층의 root view를 뷰 컨트롤러에 장착.</li>
</ul>
<p>첫 번째 방법인 스토리보드에서 직접 작업해주는 방법은 제가 가장 많이 쓰고 있는 방법이기도 합니다. 공식문서에서도 언급했다시피, 가장 보편적으로 쓰이고 있는 방법이 아닐까 합니다. </p>
<pre><code class="language-swift">/// 다른 스토리보드에 있는 뷰 컨트롤러를 호출할 때는 storyboard 이름을 명시함
let storyboard = UIStoryboard(name: &quot;Main&quot;, bundle: nil)
guard let dvc = storyboard.instantiateViewController(withIdentifier: &quot;ViewController&quot;) as? ViewController else { return }

/// 동일 스토리보드에 있는 뷰 컨트롤러를 호출할 때는 storyboard 정의를 생략함
self.instantiateViewController(withIdentifier: &quot;ViewController&quot;) as? ViewController else { return }</code></pre>
<p>이런 식의 코드를 보신 적이 있을 겁니다.
이 코드는 Main 스토리보드 파일에 있는 ViewController 뷰 컨트롤러를 불러오라는 뜻의 코드입니다. 그리고 ViewController에는 사용자에게 보여주고자 하는 뷰들이 짜여져 있겠지요. 이게 바로 첫 번째 방법에서 설명하고 있는 스토리보드를 이용해 뷰컨트롤러 위에 뷰를 띄우는 방법입니다.</p>
<p>두 번째 방법은 nib 파일(xib)에 뷰를 만들어 불러와주는 코드입니다.</p>
<pre><code class="language-swift">let vc = NibViewController(nibName: &quot;NibViewController&quot;, bundle: nil)</code></pre>
<p>이런 식으로 사용되는 코드입니다. 스토리보드 방식과 큰 차이는 없지만, 스토리보드가 제공하는 GUI 상에서 드래그 해 세그를 연결해주는 건 못 한다는 점이 다릅니다.</p>
<p>마지막 방법인 loadView()를 활용하는 방법은 개인적으로 쓰여진 코드를 읽어본 적만 있고 써본 적은 없는 방식입니다.
loadView()를 사용할 일이 많이 없었던 이유는 저는 항상 GUI상에 실제로 존재하는 뷰 컨트롤러를 생성했기 때문인데, (많이들 쓰시는 그 방법입니다) <img src="https://images.velog.io/images/danna-lee/post/12132f03-abfe-4ee5-958c-8e1108f146d6/image.png" alt="">
이 방법으로 뷰 컨트롤러를 생성하면 위에서도 잠깐 언급했다시피 하나의 view(root view)가 자동으로 함께 생성됩니다. 하지만 스토리보드나 xib를 사용하지 않고 코드로 뷰 컨트롤러를 만들게 되면 그 안에 뷰가 자동으로 생성되지 않기 때문에 뷰를 직접 만들어줘야 하고, 이때 그 뷰를 만들어주는 방법이 loadView()를 사용하는 방법입니다.
공식문서에서는 이 loadView()를 지금 설명하는 예시처럼 코드로 직접 ViewController를 만들어주는 상황이 아니면 직접 호출하지 말라고 이야기 하고 있습니다.</p>
<pre><code>import UIKit

class NoGUIViewController: UIViewController {
    override func loadView() {
        super.loadView()
        self.view.backgroundColor = .gray
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}</code></pre><p>공식 문서에서도 언급하고 있듯, 이 세 방법은 모두 같은 결과를 가져오기 때문에 상황에 맞게 골라 사용하면 됩니다.</p>
<blockquote>
<p>A view controller is the sole owner of its view and any subviews it creates. It is responsible for creating those views and for relinquishing ownership of them at the appropriate times such as when the view controller itself is released. If you use a storyboard or a nib file to store your view objects, each view controller object automatically gets its own copy of these views when the view controller asks for them. However, if you create your views manually, each view controller must have its own unique set of views. You cannot share views between view controllers.</p>
</blockquote>
<p>뷰 컨트롤러는 그 뷰와 서브뷰들의 오직 하나의 주인이다. 그래서 적절한 타이밍(예를 들면 뷰 컨트롤러 자체가 사라질 때)에 그 뷰들을 만들거나 제거하는 일을 한다. 
스토리보드나 nib을 사용해 뷰를 만든다면 뷰 컨트롤러가 요구할 때 자동으로 뷰의 복사본을 만들어내지만, 코드로 짠 뷰는 모든 뷰 컨트롤러에서 다 다른 각자의 뷰들을 가지고 있어야 한다. 뷰를 다른 뷰 컨트롤러와 같이 사용할 수 없다는 의미다.</p>
<p>iOS 개발을 하며 항상 아쉬웠던 부분을 짚어줬습니다. 반복되는 뷰가 있으면 A라는 뷰 컨트롤러에서 만들어서 B라는 뷰 컨트롤러에서도 쓸 수 있으면 좋은데, 뷰는 오로지 그 뷰 컨트롤러에 종속되어야 하기 때문에 다른 뷰 컨트롤러에서는 사용할 수 없다고 이야기하고 있습니다.
(그래서 저는 xib로 custom view를 만든 후 delegate를 활용하는 방법으로 재사용이 필요한 헤더 등을 제작하고 있긴 합니다)</p>
<br/>

<h3 id="viewcontroller-생명주기">Viewcontroller 생명주기</h3>
<p>공식문서에서는 <strong>Handling View-Related Notifications</strong> 타이틀로 소개되고 있는 부분입니다.</p>
<p><img src="https://images.velog.io/images/danna-lee/post/2cc7faff-ca85-41b3-a8c0-5a4322cdda96/image.png" alt=""></p>
<blockquote>
<p>When the visibility of its views changes, a view controller automatically calls its own methods so that subclasses can respond to the change. Use a method like viewWillAppear(_:) to prepare your views to appear onscreen, and use the viewWillDisappear(_:) to save changes or other state information.</p>
</blockquote>
<p>뷰의 보이는 상태가 변화하면, 뷰 컨트롤러는 자동으로 관련한 메서드를 호출해 서브클래스들이 변화에 반응할 수 있도록 한다. viewWillAppear(<em>:)과 같은 메서드를 활용해 뷰가 스크린에 띄워지는 것을 준비하고, viewWillDisappear(</em>:)로 변화나 상태 정보를 저장할 수 있다.</p>
<p><img src="https://images.velog.io/images/danna-lee/post/76dd8608-c36f-4553-b2d1-c15f7c295528/image.png" alt=""></p>
<p>Xcode 상의 코드에서 viewDidLoad를 마우스 우클릭하고 jump to definition을 해보면, 주석에 아래처럼 쓰여있는 것을 확인할 수 있습니다.</p>
<p><strong>1. init</strong>
<strong>2. loadView</strong></p>
<pre><code>- (void)loadView; // This is where subclasses should create their custom view hierarchy if they aren&#39;t using a nib. Should never be called directly.</code></pre><p>위에서 계속 나왔다시피 코드로 뷰 컨트롤러를 생성하게 되면 불러야 하는 메서드입니다. </p>
<p><strong>3. viewDidLoad</strong></p>
<pre><code class="language-swift">- (void)viewDidLoad; // Called after the view has been loaded. For view controllers created in code, this is after -loadView. For view controllers unarchived from a nib, this is after the view is set.</code></pre>
<p>즉, 뷰 로드가 끝나자마자 불리고, 만약 뷰컨트롤러를 코드로 짰다면 loadView 다음에 불린다고 합니다. 만약 스토리보드나 nib 파일로 뷰 컨트롤러를 만들었다면 그 뷰가 셋 되고 난 다음에 불리겠지요.
따라서 리소스 초기화, 초기 화면 구성 등을 할 때 용이하게 사용할 수 있습니다.
다만, 뷰가 처음 만들어질 때 딱 한 번만 실행되는 메서드이므로, 뷰가 나타날 때마다 초기화 되어야 하는 리소스나 바뀌어야 하는 화면 구성이 있다면 해당 메서드는 바람직하지 않습니다. (뒤에서 설명하겠지만, 이런 경우에는 viewWillAppear를 활용하면 좋습니다)</p>
<p><strong>4. viewWillAppear</strong></p>
<pre><code class="language-swift">- (void)viewWillAppear:(BOOL)animated // Called when the view is about to made visible. Default does nothing</code></pre>
<p>뷰가 나타나기 직전에 호출됩니다. 뷰가 이제 나타날 거라는 신호를 뷰 컨트롤러에게 알려줍니다. viewDidLoad와 다른 점은 viewDidLoad는 로드될 때 딱 한 번 불리고, viewWillAppear는 쉽게 말해 눈에 보이기 직전에 호출 됩니다.
뷰가 눈에서 사라진다고 해서 항상 unload가 되지 않습니다. 사용자의 눈에 나타났다, 없어졌다 하는 상태를 관리하기 좋은 메서드입니다.</p>
<p><strong>5. viewDidAppear</strong></p>
<pre><code class="language-swift">- (void)viewDidAppear:(BOOL)animated; // Called when the view has been fully transitioned onto the screen. Default does nothing</code></pre>
<p>뷰가 나타난 직후 호출됩니다. </p>
<p><strong>6. viewWillDisappear</strong></p>
<pre><code class="language-swift">- (void)viewWillDisappear:(BOOL)animated; // Called when the view is dismissed, covered or otherwise hidden. Default does nothing</code></pre>
<p>뷰가 눈에서 사라지기 직전에 호출됩니다. 여기서 &#39;사라진다&#39;는 것은 dismiss 되거나, 다른 뷰에 의해 가려지거나 하는 상황을 의미합니다.</p>
<p><strong>7. viewDidDisappear</strong></p>
<pre><code class="language-swift">- (void)viewDidDisappear:(BOOL)animated; // Called after the view was dismissed, covered or otherwise hidden. Default does nothing</code></pre>
<p>뷰가 눈에서 사라진 직후에 호출됩니다.</p>
<p><strong>8. viewDidUnload</strong></p>
<pre><code class="language-swift">- (void)viewDidUnload API_DEPRECATED(&quot;&quot;, ios(3.0, 6.0)) API_UNAVAILABLE(tvos); // Called after the view controller&#39;s view is released and set to nil. For example, a memory warning which causes the view to be purged. Not invoked as a result of -dealloc.</code></pre>
<br/>

<h3 id="메모리-관리">메모리 관리</h3>
<blockquote>
<p>Memory is a critical resource in iOS, and view controllers provide built-in support for reducing their memory footprint at critical times. The UIViewController class provides some automatic handling of low-memory conditions through its didReceiveMemoryWarning() method, which releases unneeded memory.</p>
</blockquote>
<p>메모리 관리를 위해 UIViewController 클래스에서 메모리가 부족할 경우를 핸들링 할 수 있게 해주는 didReceiveMemoryWarning() 메서드를 제공하고 있다. 해당 메서드는 필요없는 메모리를 해제시킨다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS 개발자라면 뭘 설명할 수 있어야 할까?]]></title>
            <link>https://velog.io/@danna-lee/iOS-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@danna-lee/iOS-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 07 Dec 2021 09:35:50 GMT</pubDate>
            <description><![CDATA[<p>지금까지 iOS 앱 개발을 꽤 많이 해 왔고 꾸준히 해왔지만, project-driven으로 학습을 하다 보니 이론적인 부분에서 질문이 들어왔을 때 내가 완벽히 이해하고 설명할 수 있는지에 대한 확신이 없었다. 그게 지금까지 나에게 항상 아픈 손가락이었고, 언젠가는 꼭 날 잡고 공부하고 싶은 부분이었다.</p>
<p>마침, iOS 기술 면접을 볼 일이 생겨 이 기회에 이론적인 개념을 정리해보려 한다.
<br/></p>
<h2 id="ios-개발-기본편">iOS 개발 (기본편)</h2>
<h3 id="1-view-controller-view의-생명주기에-대해-설명하세요">1. View Controller, View의 생명주기에 대해 설명하세요.</h3>
<p><a href="https://velog.io/@danna-lee/iOS-ViewController%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0">ViewController의 생명주기 공부에서 시작한 ViewController와 View에 대한 고찰</a></p>
<h3 id="2-app의-생명주기에-대해-설명하세요">2. App의 생명주기에 대해 설명하세요.</h3>
<h3 id="3-scene-delegate에-대해-설명하세요">3. scene delegate에 대해 설명하세요.</h3>
<h3 id="4-arcautomatic-reference-counting에-대해-설명하세요">4. ARC(Automatic Reference Counting)에 대해 설명하세요.</h3>
<h3 id="5-weak와-strong-unowned에-대해-설명하세요">5. Weak와 Strong, unowned에 대해 설명하세요.</h3>
<h3 id="6-escaping-closure에-대해-설명하세요">6. Escaping Closure에 대해 설명하세요.</h3>
<h3 id="7-타입-캐스팅을-할-때-사용하는-키워드인-as-as-as-이-셋의-차이를-설명하세요">7. 타입 캐스팅을 할 때 사용하는 키워드인 as, as?, as! 이 셋의 차이를 설명하세요.</h3>
<h3 id="8-swift에서-class와-struct의-차이를-설명하세요">8. Swift에서 Class와 Struct의 차이를 설명하세요.</h3>
<p><a href="https://velog.io/@danna-lee/Swift-%EA%B5%AC%EC%A1%B0%EC%B2%B4%EC%99%80-%ED%81%B4%EB%9E%98%EC%8A%A4">Swift) 구조체와 클래스</a></p>
<h3 id="9-frame-과-bounds-의-차이를-설명하세요">9. Frame 과 Bounds 의 차이를 설명하세요.</h3>
<h3 id="10-delegate-패턴에-대해-설명하세요">10. delegate 패턴에 대해 설명하세요.</h3>
<h3 id="11-delegate-vs-block-vs-notification의-차이에-대해-설명하세요">11. Delegate vs Block vs Notification의 차이에 대해 설명하세요.</h3>
<h3 id="12-스토리보드를-이용했을때의-장단점을-설명하세요">12. 스토리보드를 이용했을때의 장단점을 설명하세요.</h3>
<h3 id="13-safearea에-대해서-설명하세요">13. Safearea에 대해서 설명하세요.</h3>
<h3 id="14-코코아-프레임워크에-대해-설명하세요">14. 코코아 프레임워크에 대해 설명하세요.</h3>
<h3 id="15-옵셔널-바인딩에-대해-설명하세요">15. 옵셔널 바인딩에 대해 설명하세요.</h3>
<h3 id="16-lazy-키워드에-대해-설명하세요">16. lazy 키워드에 대해 설명하세요.</h3>
<h3 id="17-실제-디바이스가-없을-경우-개발-환경에서-할-수-있는-것과-없는-것을-설명하세요">17. 실제 디바이스가 없을 경우 개발 환경에서 할 수 있는 것과 없는 것을 설명하세요.</h3>
<br/>
<br/>

<h2 id="ios-개발-심화편">iOS 개발 (심화편)</h2>
<h3 id="1-mvc-mvvm-등-디자인-패턴에-대해-설명하세요">1. MVC, MVVM 등 디자인 패턴에 대해 설명하세요.</h3>
<h3 id="2-rxswift에-대해-설명하세요">2. RxSwift에 대해 설명하세요.</h3>
<h3 id="3-swift로-동기비동기-처리하는-방법들에-대해-설명하세요">3. Swift로 동기/비동기 처리하는 방법들에 대해 설명하세요.</h3>
<br/>
<br/>

<h2 id="cs-이론-질문">CS 이론 질문</h2>
<h3 id="1-동기-비동기에-대해-설명하세요">1. 동기 비동기에 대해 설명하세요.</h3>
<h3 id="2-스레드에-대한-개념을-설명하세요">2. 스레드에 대한 개념을 설명하세요.</h3>
<h3 id="3-선언형-절차형-프로그래밍에-대해-설명하세요">3. 선언형, 절차형 프로그래밍에 대해 설명하세요.</h3>
<h3 id="4-call-by-value-call-by-reference의-차이에-대해-설명하세요">4. call by value, call by reference의 차이에 대해 설명하세요.</h3>
<h3 id="5-순수함수-익명함수-고차함수에-대해-설명하세요">5. 순수함수, 익명함수, 고차함수에 대해 설명하세요.</h3>
<h3 id="6-overloading-overriding에-대해-설명하세요">6. overloading, overriding에 대해 설명하세요.</h3>
<br/>
<br/>

<p><code>👉🏻 질문을 수집하는 과정에서 참고한 곳들</code>
<a href="https://ugly-developer.tistory.com/5">https://ugly-developer.tistory.com/5</a>
<a href="https://github.com/JeaSungLEE/iOSInterviewquestions">https://github.com/JeaSungLEE/iOSInterviewquestions</a>
<a href="https://github.com/JaeYeopHan/Interview_Question_for_Beginner/tree/master/iOS">https://github.com/JaeYeopHan/Interview_Question_for_Beginner/tree/master/iOS</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Apple Developer Academy 온라인 설명회 요약]]></title>
            <link>https://velog.io/@danna-lee/Apple-Developer-Academy-%EC%98%A8%EB%9D%BC%EC%9D%B8-%EC%84%A4%EB%AA%85%ED%9A%8C-%EC%9A%94%EC%95%BD</link>
            <guid>https://velog.io/@danna-lee/Apple-Developer-Academy-%EC%98%A8%EB%9D%BC%EC%9D%B8-%EC%84%A4%EB%AA%85%ED%9A%8C-%EC%9A%94%EC%95%BD</guid>
            <pubDate>Wed, 01 Dec 2021 11:41:49 GMT</pubDate>
            <description><![CDATA[<p>한국에 처음 생긴 Apple Developer Academy @ POSTECH 온라인 설명회를 들으며 간단히 정리해보았습니다.</p>
<br/>

<h2 id="1-열정">1. 열정</h2>
<ul>
<li>당신의 열정은 무엇입니까?</li>
<li>열정을 가지고 있다면 그 열정을 우리가 발산하게 해줄 것이고, 열정을 발산할 곳을 아직 찾지 못했다면 우리가 그것을 찾는 데 도움을 줄 것입니다.</li>
<li>정해진 과제는 없습니다. 우리의 역할은 단지 당신이 열정을 발산할 수 있도록 돕는 것뿐입니다.</li>
</ul>
<br/>

<h2 id="2-아카데미와-관련해">2. 아카데미와 관련해</h2>
<ul>
<li><p>하나의 커뮤니티를 가지고 교육과 네트워크를 제공하고 있습니다.</p>
</li>
<li><p>9개월 간의 교육</p>
</li>
<li><p>한국은 2번째로 큰 아카데미가 될 거예요</p>
</li>
<li><p>아카데미의 다양성을 존중하기 위해 노력합니다. (인종, 교육적 배경, 사회적 배경, 성별 등)</p>
<ul>
<li>지난 몇 년간 아카데미의 여성 비율이 6배 증가했습니다</li>
<li>17-57세까지 다양한 연령대가 있습니다</li>
<li>다양한 직업을 가진 사람들이 함께하고 있습니다</li>
<li>그들의 공통점은 열정을 가지고 있었다는 것입니다</li>
</ul>
</li>
<li><p>과제는 없습니다.</p>
<ul>
<li>과제라고 부를 게 없죠, 그냥 앱을 만들어서 앱스토어에 내면 됩니다.</li>
<li>취업할 때 &#39;당신의 앱 개발 능력은 어느 정도입니까?&#39; 라는 질문에 &#39;전 제 앱이 있고, 7천 다운로드가 넘어갑니다&#39;라고 답하는 것보다 더 좋은 답변은 없겠죠</li>
</ul>
</li>
<li><p>졸업생 성과</p>
<ul>
<li><p>WWDC</p>
<ul>
<li>1주간의 개발자 컨퍼런스</li>
<li>WWDC에서 장학금 받은 사람이 431명이나 나왔습니다</li>
</ul>
</li>
<li><p>아카데미 졸업생에 의해 162개의 새로운 회사가 창업되었습니다.</p>
</li>
<li><p>그래서 하고 싶은 얘기는, 당신이 아카데미를 통해 성장해서 취업을 하든 창업을 하든 취업을 하다 창업을 하든 무엇이든 할 수 있다는 겁니다</p>
<br/>

</li>
</ul>
</li>
</ul>
<h2 id="3-커리큘럼-t자형-전문가">3. 커리큘럼: T자형 전문가</h2>
<ul>
<li>design, develop, business + professional skills</li>
<li>사람들을 리드하고, 협업하고, 사람들에게 영향을 주고, 설득을 하고, 어떻게 프로세스를 관리하는가를 배웁니다</li>
<li>커리큘럼이라고 부르기보다는 &#39;learning experience(학습 경험)&#39;라고 얘기할게요</li>
<li>디자인, 개발, 사업을 모두 할 줄 알아야 하겠죠.</li>
<li>모두가 갖춰야 할 기본적인 능력을 갖추게 될 것입니다.<ul>
<li>그렇게 기본적인 것을 배우다 보면 각자 두각을 나타내는 영역이 다를 것이고, 그 분야를 더 깊게 공부할 수 있어야 합니다. 그래서 내 path를 쌓아가야 해요</li>
</ul>
</li>
<li>아카데미에서 같은 걸 배우고 있더라도 아무도 같은 일을 하고 있는 사람이 없을 겁니다<ul>
<li>우리가 하라고 해서 하는 게 아니라 당신이 원해서 해야 합니다</li>
</ul>
</li>
<li>Engage, Investigate, Act</li>
</ul>
<p><img src="https://images.velog.io/images/danna-lee/post/c845cda5-f283-49d7-b493-f8add0a61ef7/image.png" alt=""></p>
<ul>
<li>커리큘럼이 있지만 모든 사람이 다 같은 걸 배우진 않을 겁니다.<ul>
<li>심지어는 같은 프로젝트 안에서도 누구는 백엔드에 집중하고 누군가는 인터페이스에 집중하는 등 다 다른 걸 배우고 있을 거예요</li>
</ul>
</li>
</ul>
<p><img src="https://images.velog.io/images/danna-lee/post/e868a188-b716-44a5-99c1-98c89c293105/image.png" alt=""></p>
<ul>
<li>브릿지는 회고와 앞으로의 방향성을 잡아가는 데 도움을 줄 겁니다</li>
</ul>
<br/>

<h2 id="4-질의응답">4. 질의응답</h2>
<ol>
<li>강의 언어?<ol>
<li>한국어 50/영어 50, 발표를 하거나 멘토와 소통을 할 때 영어를 써야 할 경우가 있을 겁니다</li>
</ol>
</li>
<li>나이 제한?<ol>
<li>만 19세 이상 (2022.03.01. 기준)</li>
</ol>
</li>
<li>swift 할 줄 알아야 하나요?<ol>
<li>스위프트가 prerequisite이 아닙니다</li>
<li>스위프트를 할 줄 안다면 좋겠지만 그게 이 사람이 열정을 가졌다는 것의 증거가 되지 않을 수도 있습니다</li>
</ol>
</li>
<li>swift만 배우나요?<ol>
<li>swift가 메인 언어가 되겠지만, 여전히 현업에서 C++ 등 중요한 언어들이 많으므로 방대하게 다루려 합니다.</li>
</ol>
</li>
<li>강의자에 대해?<ol>
<li>우리는 강의자를 멘토라고 부릅니다</li>
<li>다양한 배경의 12-14명(세션)이 있을 예정이고, personal 교육을 제공할 예정입니다.</li>
<li>동료와 함께 성장하고 멘토의 팁을 들으세요</li>
<li>모르겠어라고 얘기할 때 앉아서 같이 고민해보자라고 대답할 수 있는 사람</li>
</ol>
</li>
<li>교육 비용<ol>
<li>교육비 전액 지원, 정착지원금(100만원) 지원, 장비(맥북프로, 최신 아이폰 등) 대여</li>
<li>기숙사 거주 원할 경우 한 달 30만원 내외</li>
</ol>
</li>
<li>트랙<ol>
<li>트랙을 선택하긴 하지만 거기에 얽매일 필요는 없습니다</li>
</ol>
</li>
<li>ML<ol>
<li>머신러닝이 커리큘럼 안에 있긴 하지만, 모두가 배울 필요는 없습니다</li>
</ol>
</li>
<li>아카데미 밖 사람과 협업해 앱을 제작해도 되나요?<ol>
<li>바깥의 리소스를 활용하는 건 가능합니다</li>
<li>하지만 바깥 사람과 팀을 이루어 협업하면 안됩니다</li>
</ol>
</li>
<li>회사나 학교에 다니고 있으면 어떡하나요?<ol>
<li>아침 8-12부터나 오후 4시간에 참가해야 합니다</li>
<li>애플에서 스튜디오에 직접 오는 걸 강력하게 권장합니다</li>
</ol>
</li>
<li>다른 나라에서 진행된 아카데미의 성과<ol>
<li>아시아의 경우, 졸업생의 98프로가 취업에 성공</li>
<li>취업 후에도 더 빠른 승진을 함</li>
<li>작년 128개의 회사가 캠퍼스에 학생들의 성과를 보기 위해 방문</li>
<li>코로나 때문에 일할 준비가 된 졸업생들은 평균 1.5개의 회사에서 입사 제안을 받았음. 코로나 이전에는 평균 3개였음.</li>
</ol>
</li>
<li>경쟁률?<ol>
<li>3% 합격, 10% 합격</li>
<li>근데 지역에 따라 많이 달라짐</li>
</ol>
</li>
</ol>
<br/>

<h2 id="지원">지원</h2>
<ul>
<li>누구?<ul>
<li>19세 이상 누구나</li>
<li>코딩 할 줄 몰라도 됩니다. 그거 배우려고 오는 거예요.</li>
<li>다양한 교육적 배경의 사람들이 왔으면 합니다.</li>
<li>다양성을 추구합니다.</li>
</ul>
</li>
<li>포트폴리오<ul>
<li>당신이 할 수 있는 걸 보여주세요</li>
<li>당신이 가진 열정을 증명해보세요</li>
<li>디자이너면 작품을 보여주시고, 뮤지션이면 음악을 들려주세요, 개발자면 깃헙을 보여주세요</li>
</ul>
</li>
<li>온라인 테스트<ul>
<li>logic and problem solving (not tech related)</li>
<li>basic programming</li>
<li>bonus part) 자료를 먼저 주면 당신이 읽어서 공부해올 수 있는지, 주어지는 것만 받아먹는 사람인지 능동적인 학습을 하는 사람인지 확인</li>
<li>디자이너라면 당연히 컴퓨터 관련해서는 아무것도 못 할 테니까 당연히 다르게 채점합니다</li>
<li>하지만 T자형 인재상과 맞춰 디자이너지만 코딩을 할 줄 안다면 너무너무 좋겠죠</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS 개발 swift로 연월(년, 월)만 설정할 수 있는 UIPickerView 커스텀 하기]]></title>
            <link>https://velog.io/@danna-lee/iOS-%EA%B0%9C%EB%B0%9C-swift%EB%A1%9C-%EC%97%B0%EC%9B%94%EB%85%84-%EC%9B%94%EB%A7%8C-%EC%84%A4%EC%A0%95%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-UIPickerView-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@danna-lee/iOS-%EA%B0%9C%EB%B0%9C-swift%EB%A1%9C-%EC%97%B0%EC%9B%94%EB%85%84-%EC%9B%94%EB%A7%8C-%EC%84%A4%EC%A0%95%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-UIPickerView-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 31 Oct 2021 11:57:10 GMT</pubDate>
            <description><![CDATA[<p>생각보다 개발을 하다보면 pickerView를 쓸 일이 꽤 많은 것 같다.</p>
<p>그래서 보통 피커뷰로 날짜를 설정하려고 하면 iOS에서 기본으로 제공하는 UIDatePicker을 사용하는데, 예쁘고 간편하긴 하지만 커스텀이 제한적이라 실제로 서비스에서 사용해본 적은 많이 없는 것 같다.</p>
<p>대신 커스텀이 용이한 UIPickerView로 UIDatePicker를 흉내내는 방식의 구현을 자주 사용한다.</p>
<p>iOS 프로젝트 개발에서 생각보다 많을 것 같지만, 한글로 된 블로그 글이 몇 개 없는 것 같아 정리해보는 오늘의 주제는 아래처럼 정했다.</p>
<blockquote>
<p>UIPickerView로 년, 월만 선택 가능한 피커 만들기</p>
</blockquote>
<h3 id="1-텍스트필드-얹기">1. 텍스트필드 얹기</h3>
<p>피커뷰를 뷰에 컴포넌트로 얹을 수도 있지만, 보통은 코드로 많이 생성한다.
(뇌피셜로는 뷰에 얹어서 isHidden 처리 해주는 것보다 코드로 설정해주는 게 훨씬 간단하기 때문일 것 같다)</p>
<p>피커뷰를 코드로 만들기 위해서는 뜬금없지만 뷰에 UITextField가 있는 상태여야 한다. 추후에 이 텍스트필드에 입력수단으로 피커뷰를 이용할 계획이다. (텍스트필드의 커서는 없애서 UIButton처럼 구동하게 할 생각이므로 같은 동작을 하는 버튼은 없어도 된다. 버튼 대신 텍스트필드를 얹자)</p>
<p>나는 스토리보드에서 <code>yearTextField</code>라는 텍스트필드를 만들고 IBOutlet을 연결해주었다.</p>
<pre><code class="language-swift">@IBOutlet var yearTextField: UITextField!</code></pre>
<h3 id="2-피커뷰-코드-쓰기">2. 피커뷰 코드 쓰기</h3>
<p>그 후에는 이제 생성한 텍스트필드의 입력 수단으로 이용될 피커뷰를 제작하면 되는데, 이때는 스토리보드에서 아무런 작업 없이 코드만 추가해주면 된다.</p>
<pre><code class="language-swift">override func viewDidLoad() {
    super.viewDidLoad()

    createPickerView()
}

/// 피커뷰 생성
func createPickerView() {
    /// 피커 세팅
    let pickerView = UIPickerView()
    pickerView.delegate = self
    pickerView.dataSource = self
    yearTextField.tintColor = .clear

    /// 텍스트필드 입력 수단 연결
    yearTextField.inputView = pickerView
}

extension FundManagerThinkVC: UIPickerViewDelegate, UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -&gt; Int {
        return 2 /// 년, 월 두 가지 선택하는 피커뷰
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -&gt; Int {
        switch component {
        case 0:
            return availableYear.count /// 연도의 아이템 개수
        case 1:
            return allMonth.count /// 월의 아이템 개수
        default:
            return 0
        }
    }

    /// 표출할 텍스트 (2020년, 2021년 / 1월, 2월, 3월, 4월 ... )
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -&gt; String? {
        switch component {
        case 0:
            return &quot;\(availableYear[row])년&quot;
        case 1:
            return &quot;\(allMonth[row])월&quot;
        default:
            return &quot;&quot;
        }
    }

    /// 피커뷰에서 선택된 행을 처리할 수 있는 메서드
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {

        switch component {
        case 0:
            selectedYear = availableYear[row]
        case 1:
            selectedMonth = allMonth[row]
        default:
            break
        }
    }
}</code></pre>
<p>이렇게 하면 텍스트필드를 클릭했을 때 피커뷰가 올라온다. </p>
<h3 id="3-툴바-코드-쓰기">3. 툴바 코드 쓰기</h3>
<p>하지만 확인 버튼이 없네..? 피커뷰는 어떻게 내리지?</p>
<p>그 처리를 위해 툴바를 추가해준다.
참고로 툴바는 입력창 위에 취소, 확인 버튼 있는 부분이다.
<img src="https://images.velog.io/images/danna-lee/post/3c331654-1a61-4f60-82c9-e0944eafe3cf/image.png" alt=""></p>
<p>툴바 코드를 추가한 피커뷰 코드는 다음과 같다.</p>
<pre><code class="language-swift">/// 피커뷰 생성
func createPickerView() {
    /// 피커 세팅
    let pickerView = UIPickerView()
    pickerView.delegate = self
    pickerView.dataSource = self
    yearTextField.tintColor = .clear

    /// 툴바 세팅
    let toolBar = UIToolbar()
    toolBar.sizeToFit()

    let btnDone = UIBarButtonItem(title: &quot;확인&quot;, style: .done, target: self, action: #selector(onPickDone))
    let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    let btnCancel = UIBarButtonItem(title: &quot;취소&quot;, style: .done, target: self, action: #selector(onPickCancel))
    toolBar.setItems([btnCancel , space , btnDone], animated: true)
    toolBar.isUserInteractionEnabled = true

    /// 텍스트필드 입력 수단 연결
    yearTextField.inputView = pickerView
    yearTextField.inputAccessoryView = toolBar
}

// 피커뷰 &gt; 확인 클릭
@objc func onPickDone() {
    /// 확인 눌렀을 때 액션 정의 -&gt; 아래 코드에서는 라벨 텍스트 업데이트
    yearLabel.text = &quot;\(selectedYear)년&quot;
    monthLabel.text = &quot;\(selectedMonth)월&quot;

    yearTextField.resignFirstResponder() /// 피커뷰 내림
}

// 피커뷰 &gt; 취소 클릭
@objc func onPickCancel() {
    yearTextField.resignFirstResponder() /// 피커뷰 내림
}</code></pre>
<p>부가 설명을 하자면, 
확인 버튼, 취소 버튼을 설정하는 부분에서 
<code>let btnDone = UIBarButtonItem(title: &quot;확인&quot;, style: .done, target: self, action: #selector(onPickDone))</code>
이 코드에서 <code>title</code>은 버튼 텍스트를,
<code>style</code>은 버튼의 형태, 코드에서 <code>.</code>을 눌러보면 모든 옵션을 볼 수 있는데 done과 plain이 있다<img src="https://images.velog.io/images/danna-lee/post/0043d4ee-a963-49ab-b514-4f9ddd7defb4/image.png" alt="">
그리고 <code>action: #selector()</code>에서는 버튼이 눌렸을 때 어떤 액션을 취할 것인지 @objc func를 넣을 수 있는 형태로 되어 있다.
여기서는 onPickDone이라는 메서드를 정의했고, </p>
<pre><code class="language-swift">// 피커뷰 &gt; 확인 클릭
@objc func onPickDone() {
    /// 확인 눌렀을 때 액션 정의 -&gt; 아래 코드에서는 라벨 텍스트 업데이트
    yearLabel.text = &quot;\(selectedYear)년&quot;
    monthLabel.text = &quot;\(selectedMonth)월&quot;

    yearTextField.resignFirstResponder() /// 피커뷰 내림
}</code></pre>
<p>이러한 액션들을 하고 있다.
다른 부분은 마음대로 써도 되지만, <code>textField.resignFirstResponder()</code> 부분은 꼭 써줘야 확인을 눌렀을 때 피커뷰가 사라진다.</p>
<p>같은 원리로 취소 버튼도 만들어주면 된다.</p>
<h3 id="4-날짜-계산">4. 날짜 계산</h3>
<p>사실 2번까지만 해도 피커뷰를 띄울 수는 있지만,
조건이 하나 더 붙은 상황이라고 가정해보자.</p>
<blockquote>
<p>2020년 1월부터 현재까지만 선택 가능하게 해주세요</p>
</blockquote>
<p>라는 조건이 붙었을 때 우리는 아 날짜 계산을 해야겠다라고 생각하게 된다.</p>
<p>계산은 오늘 날짜를 계산해주는 <code>Date()</code>라는 걸 활용할 예정이다.</p>
<pre><code class="language-swift">var availableYear: [Int] = []
var allMonth: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
var selectedYear = 0
var selectedMonth = 0
var todayYear = &quot;0&quot;
var todayMonth = &quot;0&quot;

/// 가능한 날짜 설정
func setAvailableDate() {
    /// 선택 가능한 연도 설정
    let formatterYear = DateFormatter()
    formatterYear.dateFormat = &quot;yyyy&quot;
    todayYear = formatterYear.string(from: Date())

    for i in 2020...Int(todayYear)! {
        availableYear.append(i)
    }

    /// 선택 가능한 달 설정
    let formatterMonth = DateFormatter()
    formatterMonth.dateFormat = &quot;MM&quot;
    todayMonth = formatterMonth.string(from: Date())

    selectedYear = Int(todayYear)!
    selectedMonth = Int(todayMonth)!
}</code></pre>
<h3 id="5-미래-날짜-선택-막기">5. 미래 날짜 선택 막기</h3>
<p>마지막으로 해줘야 할 것이 미래 날짜를 선택했을 때 막는 기능이다.
생각보다 간단하다.
기본으로 제공되는 <code>pickerView.selectRow()</code>를 이용하면 된다.
그러면 우리가 원하는 행으로 피커 휠을 돌릴 수 있다.</p>
<p>pickerView didselectRow에 다음 코드를 추가해주었다.</p>
<pre><code class="language-swift">if (Int(todayYear) == selectedYear &amp;&amp; Int(todayMonth)! &lt; selectedMonth) {
        pickerView.selectRow(Int(todayMonth)!-1, inComponent: 1, animated: true)
        selectedMonth = Int(todayMonth)!
}</code></pre>
<p>여기서 맨 처음 인자로는 돌릴 행(몇 월인지 - <code>Int(todayMonth)!-1</code>)을 <code>inComponent</code>에는 돌릴 열(이 코드에서 년도면 0, 월이면 1)을, 마지막으로는 돌아가는 애니메이션을 넣을지 말지를 설정해주면 된다.</p>
<p>그러면 만약 오늘이 2021년 10월이라고 할 때, 사용자가 2021년 11월을 선택하면 2021년 10월로 자동으로 돌려주게 된다.</p>
<h3 id="부록-전체-코드">부록) 전체 코드</h3>
<pre><code class="language-swift">var availableYear: [Int] = []
var allMonth: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
var selectedYear = 0
var selectedMonth = 0
var todayYear = &quot;0&quot;
var todayMonth = &quot;0&quot;

@IBOutlet var yearTextField: UITextField!

override func viewDidLoad() {
    super.viewDidLoad()

    setAvailableDate()
    createPickerView()
}

/// 피커뷰 생성
func createPickerView() {
    /// 피커 세팅
    let pickerView = UIPickerView()
    pickerView.delegate = self
    pickerView.dataSource = self
    yearTextField.tintColor = .clear

    /// 툴바 세팅
    let toolBar = UIToolbar()
    toolBar.sizeToFit()

    let btnDone = UIBarButtonItem(title: &quot;확인&quot;, style: .done, target: self, action: #selector(onPickDone))
    let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    let btnCancel = UIBarButtonItem(title: &quot;취소&quot;, style: .done, target: self, action: #selector(onPickCancel))
    toolBar.setItems([btnCancel , space , btnDone], animated: true)
    toolBar.isUserInteractionEnabled = true

    /// 텍스트필드 입력 수단 연결
    yearTextField.inputView = pickerView
    yearTextField.inputAccessoryView = toolBar
}

// 피커뷰 &gt; 확인 클릭
@objc func onPickDone() {
    /// 확인 눌렀을 때 액션 정의 -&gt; 아래 코드에서는 라벨 텍스트 업데이트
    yearLabel.text = &quot;\(selectedYear)년&quot;
    monthLabel.text = &quot;\(selectedMonth)월&quot;

    yearTextField.resignFirstResponder() /// 피커뷰 내림
}

// 피커뷰 &gt; 취소 클릭
@objc func onPickCancel() {
    yearTextField.resignFirstResponder() /// 피커뷰 내림
}

/// 가능한 날짜 설정
func setAvailableDate() {
    /// 선택 가능한 연도 설정
    let formatterYear = DateFormatter()
    formatterYear.dateFormat = &quot;yyyy&quot;
    todayYear = formatterYear.string(from: Date())

    for i in 2020...Int(todayYear)! {
        availableYear.append(i)
    }

    /// 선택 가능한 달 설정
    let formatterMonth = DateFormatter()
    formatterMonth.dateFormat = &quot;MM&quot;
    todayMonth = formatterMonth.string(from: Date())

    selectedYear = Int(todayYear)!
    selectedMonth = Int(todayMonth)!
}

extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -&gt; Int {
        return 2
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -&gt; Int {
        switch component {
        case 0:
            return availableYear.count
        case 1:
            return allMonth.count
        default:
            return 0
        }
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -&gt; String? {
        switch component {
        case 0:
            return &quot;\(availableYear[row])년&quot;
        case 1:
            return &quot;\(allMonth[row])월&quot;
        default:
            return &quot;&quot;
        }
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {

        switch component {
        case 0:
            selectedYear = availableYear[row]
        case 1:
            selectedMonth = allMonth[row]
        default:
            break
        }

        if (Int(todayYear) == selectedYear &amp;&amp; Int(todayMonth)! &lt; selectedMonth) {
            pickerView.selectRow(Int(todayMonth)!-1, inComponent: 1, animated: true)
            selectedMonth = Int(todayMonth)!
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[제목 뒤에 복사 버튼 넣어주세요]]></title>
            <link>https://velog.io/@danna-lee/%EC%A0%9C%EB%AA%A9-%EB%92%A4%EC%97%90-%EB%B3%B5%EC%82%AC-%EB%B2%84%ED%8A%BC-%EB%84%A3%EC%96%B4%EC%A3%BC%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@danna-lee/%EC%A0%9C%EB%AA%A9-%EB%92%A4%EC%97%90-%EB%B3%B5%EC%82%AC-%EB%B2%84%ED%8A%BC-%EB%84%A3%EC%96%B4%EC%A3%BC%EC%84%B8%EC%9A%94</guid>
            <pubDate>Sun, 24 Oct 2021 14:50:33 GMT</pubDate>
            <description><![CDATA[<p>외주 하면 항상 상상치도 못한 곳에서 위기에 봉착하는 것 같다.</p>
<p>이번에는 제목 바로 뒤에 복사 버튼을 넣어달라는 요청을 받았다.
<img src="https://images.velog.io/images/danna-lee/post/3ebaecc3-0b6c-4f63-9f05-f16e4a664fdc/image.png" alt="">이런 식으로 이름 옆에 복사 버튼을 넣어서 제목을 복사할 수 있게 해야 하는데, 제목은 한 줄이 될지, 두 줄이 될지, 세 줄이 될지 모르는 상태였다.</p>
<p>복사 버튼을 추가하기 전 제목 부분의 구조는 line이 0으로 설정된 UIlabel로 구현되어 있었고, 제약 역시 superview에 leading, trailing 모두 상수값 0으로 잡혀 있는 상태였다. 여기서 superview는 제목 부분(제목, 펀드코드)를 감싸는 titleContain UIView안에 있었다. UIView는 디바이스 전체 너비를 superview로 두고 상수값으로 leading, trailing이 잡혀있는 상태.</p>
<p>주절주절 말이 많았지만, 정리하자면, <img src="https://images.velog.io/images/danna-lee/post/e75fc22c-dc15-4cdd-9df4-4b53d2d77384/Screen%20Shot%202021-10-24%20at%2011.32.55%20PM.png" alt=""> 제목 라벨의 영역이 이렇게 잡혀있는 상태였다.</p>
<p>그러면 복사 버튼 위치는 어떻게 설정해야 하지?</p>
<p>고민하다 처음 선택했던 방법은 (바보같았던 방법이다)
버튼에 title, image를 함께 넣으면 타이틀 뒤에 이미지가 온다는 것을 활용하려 했다.
하지만 ... 뜻대로 보여지지 않았고(멀티라인 때문이었다), 타이틀이 왼쪽에, 버튼이 오른쪽에 오는 아주 기이한 형태가 되었다.</p>
<p>그러다가 구세주 블로그 글을 하나 발견했는데,</p>
<blockquote>
<p><a href="https://zeddios.tistory.com/406">https://zeddios.tistory.com/406</a></p>
</blockquote>
<p>&#39;UIButton, UILabel에 이미지 추가&#39;라는 매력적인 제목을 가지고 있었다.
홀린듯이 들어갔고!!! 그때 이 블로그 글을 클릭한 나를 칭찬하게 됐다.  </p>
<p>결론부터 얘기하자면 이런 코드로 성공했다.</p>
<pre><code class="language-swift">// attributedString 선언
let attributedString = NSMutableAttributedString(string: &quot;&quot;)

// 붙일 이미지 선언
let imageAttachment = NSTextAttachment()
imageAttachment.image = UIImage(named: &quot;IconCopy&quot;)

// 텍스트, 이미지 순서대로 append해주기
attributedString.append(NSAttributedString(string: fundTitle))
attributedString.append(NSAttributedString(attachment: imageAttachment))

// 대상 라벨에 attributedText 속성 연결
titleLabel.attributedText = attributedString </code></pre>
<p>간단히 설명하자면, attributedString을 속성을 이용해 텍스트 뒤에 이미지를 붙여주는 방식이다.</p>
<p>NSMutableattributedString을 자주 쓰지만, image를 attatch하는 기능이 있는지는 처음 알았다 ㅎㅎ</p>
<p>나는 기존 구조를 변경하기 힘들어 titleLabel과 사이즈, 위치를 동일하게 설정한 button 위에 titleLabel을 얹는 방식으로 했지만, 시간이 여유로워지면 button에 setTitle하는 방식으로 리팩토링 할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swift로 라이브러리 없이 무한 배너 구현하기 (feat. CollectionView)]]></title>
            <link>https://velog.io/@danna-lee/Swift%EB%A1%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%97%86%EC%9D%B4-%EB%AC%B4%ED%95%9C-%EB%B0%B0%EB%84%88-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-feat.-CollectionView</link>
            <guid>https://velog.io/@danna-lee/Swift%EB%A1%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%97%86%EC%9D%B4-%EB%AC%B4%ED%95%9C-%EB%B0%B0%EB%84%88-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-feat.-CollectionView</guid>
            <pubDate>Thu, 14 Oct 2021 12:19:39 GMT</pubDate>
            <description><![CDATA[<p>외주 하는 곳에서 이번에는 실시간 검색어 애니메이션을 구현해달라고 했다.
<img src="https://images.velog.io/images/danna-lee/post/b9fdd493-f4b6-44d9-890f-a83671b02529/Oct-14-2021%2020-25-15.gif" alt="">
이렇게 자동으로 롤링 되는 애니메이션이다.</p>
<h3 id="👉🏻-무한-배너가-어려운-이유">👉🏻 무한 배너가 어려운 이유</h3>
<p>얼핏 보면 쉬워보이지만, 따져야 할 것들이 조금 있어 까다로운 기능이다.</p>
<ol>
<li>무한으로 셀이 생성돼야 한다</li>
<li>자동으로 스크롤 돼야 한다</li>
</ol>
<p>사실 작년 이맘때에 이 기능을 구현해본 적이 있는데,
그때는 라이브러리를 사용했었다.</p>
<p>하지만 내가 지금 하고 있는 프로젝트는 이미 프로젝트 자체가 너무 무겁기도 하고 더이상의 라이브러리는 안 쓰거나 최소화하는 게 나을 거 같아 라이브러리 없이 야매로 이 기능을 구현해보기로 했다.
(사실 말이 장황했지만 그냥 고집인 거 같기도 하다)</p>
<h3 id="👉🏻-야매-로직">👉🏻 야매 로직</h3>
<p>일단 CollectionView를 사용하기로 결정했고,
내가 머리로 짠 로직은 이렇다.</p>
<ol>
<li>보여지는 셀이 2개이므로 그거보다 하나 더 많은 3개의 셀을 만든다.</li>
<li>마지막 셀로 넘어갔을 때 재빠르게 첫번째 셀로 되돌린다 (여기서 animation: false 설정해서 되돌아가는 게 안 보이게)</li>
</ol>
<h3 id="1-셀을-자동으로-스크롤-해주는-코드">(1) 셀을 자동으로 스크롤 해주는 코드</h3>
<p>우선, 셀 세 개를 넘기는 코드는 이렇게 작성했다.</p>
<pre><code class="language-swift">func weatherTimer() {
    let _: Timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (Timer) in
        self.weatherMove()
    }
}

func weatherMove() {
    // 다음 페이지로 전환
    currPage += 1
    fundWeatherCollectionView.scrollToItem(at: NSIndexPath(item: nowPage, section: 0) as IndexPath, at: .bottom, animated: true)
}</code></pre>
<p>변수로 var currPage = 0을 선언해주고,
viewDidLoad에서 <code>weatherTime()</code>을 호출해주면, 설정된 Timer에 따라 weatherMove() 메서드를 2초마다 불러주게 된다.</p>
<p><code>Timer.scheduledTimer()</code>을 더 자세히 살펴보면,
<code>withTimeInterval</code>은 타이머를 실행할 시간(초)를, <code>repeats</code>는 true일 때 해당 타이머를 계속해서 반복하고, false일 때는 1번 실행하고 끝난다.</p>
<p><code>weatherMove()</code> 메서드에서는 currPage를 하나씩 증가시킨 후,
currPage로 이동시키는 scrollToItem()을 사용한다.
*** 여기서 animated: true로 설정하면 스크롤 되는 애니메이션이 보이고, false로 설정하면 보이지 않는다.</p>
<h3 id="2-마지막-셀에서-첫-셀로-돌려주는-코드">(2) 마지막 셀에서 첫 셀로 돌려주는 코드</h3>
<p>여기서 포인트는 아무도 모르게 돌려야 한다는 것이다.
위에서 잠깐 힌트가 나왔는데, scrollToItem에서 animated를 false로 설정해주면, 아무도 모르게 되돌릴 수 있을 것이다.</p>
<p>그래서 되돌리는 코드를</p>
<pre><code class="language-swift">func scrollTofirstIndex() {
    fundWeatherCollectionView.scrollToItem(at: NSIndexPath(item: 0, section: 0) as IndexPath, at: .top, animated: false)
    currPage = 0
}</code></pre>
<p>이렇게 작성했다.</p>
<h3 id="3-타이밍-맞추기">(3) 타이밍 맞추기</h3>
<p>마지막 셀로 갔다가 첫 셀로 돌아오는데, 이게 만약 똑같이 2초의 텀을 가지고 있다면, 아무리 우리가 아무도 모르게 첫 셀로 돌려놨다고 해도 사용자는 똑같은 셀을 4초 동안 봐야 하는 문제가 있다. 그래서 타이머 영향을 받지 않으면서 눈속임하는 마지막 코드가 필요했다.</p>
<p>사실 가장 많이 헤맨 부분이기도 한데, 이스케이핑 클로저도 써보고 코드의 순서도 바꿔보았지만, 결국 내가 선택한 방법은 <code>DispatchQueue.main.asyncAfter</code>로 delay를 주는 방법이다.</p>
<p>그 이유는 2-&gt;3번째 셀 넘어갈 때의 애니메이션이 보여야 하고, 그 후 바로 아무도 모르게 1번째 셀로 넘어와야 하기 때문에 delay를 줘서 애니메이션만 보여지게 한 후 바로 1번째 셀로 돌리는 방법을 사용했다.</p>
<pre><code class="language-swift">if self.currPage == self.topWeatherData.count-1 {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
        self.scrollTofirstIndex()
    }
}</code></pre>
<p>이런 식으로 현재 보여지고 있는 페이지가 마지막 페이지일 때, 0.3초의 딜레이(페이지가 넘어가는 애니메이션이 보여지는 시간)을 주고 바로 첫번째 인덱스로 돌아가는 코드다.</p>
<h3 id="4-전체-코드">(4) 전체 코드</h3>
<p>이런 식으로 구현을 하니까 정말 나조차도 모르게 부드럽게 구현이 되었다.
전체 코드는 다음과 같다.</p>
<pre><code class="language-swift">var currPage: Int = 0

override func viewDidLoad() {
    weatherTimer()
}

func weatherTimer() {
    let _: Timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (Timer) in
        self.weatherMove()
    }
}

func weatherMove() {
    currPage += 1
    fundWeatherCollectionView.scrollToItem(at: NSIndexPath(item: currPage, section: 0) as IndexPath, at: .bottom, animated: true)

    if self.currPage == self.topWeatherData.count-1 {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            self.scrollTofirstIndex()
        }
    }
}

func scrollTofirstIndex() {
    fundWeatherCollectionView.scrollToItem(at: NSIndexPath(item: 0, section: 0) as IndexPath, at: .top, animated: false)
    currPage = 0
}</code></pre>
<p>끊어서 설명해서 코드가 길어보였지만, 코드는 정말 짧다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[노션 태그 개수 순으로 정렬하기]]></title>
            <link>https://velog.io/@danna-lee/%EB%85%B8%EC%85%98-%ED%83%9C%EA%B7%B8-%EA%B0%9C%EC%88%98-%EC%88%9C%EC%9C%BC%EB%A1%9C-%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@danna-lee/%EB%85%B8%EC%85%98-%ED%83%9C%EA%B7%B8-%EA%B0%9C%EC%88%98-%EC%88%9C%EC%9C%BC%EB%A1%9C-%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 13 Oct 2021 10:28:50 GMT</pubDate>
            <description><![CDATA[<p>노션을 다루다 보면 태그 개수 순으로 정렬할 일이 생각보다 많아서
내가 다시 찾아보려고 정리하는 글 🙃</p>
<h3 id="1-태그-개수-column-를-만들고-property-type을-formula로-설정한다">1. &quot;태그 개수&quot; column 를 만들고 property type을 formula로 설정한다.</h3>
<p><img src="https://images.velog.io/images/danna-lee/post/63e3e91c-f6be-41f1-b133-65d220ba6bf6/image.png" alt=""></p>
<br/>

<h3 id="2-태그의-개수를-세고-싶은-column의-이름을-넣어-수식을-만든다">2. 태그의 개수를 세고 싶은 &quot;column의 이름&quot;을 넣어 수식을 만든다<img src="https://images.velog.io/images/danna-lee/post/f20d3225-63be-439e-a30e-14f2ad314d48/image.png" alt=""></h3>
<p><code>length(replaceAll(prop(&quot;column 이름&quot;), &quot;(.+?)(?:,|$)&quot;, &quot;*&quot;))</code> -&gt; 여기서 &quot;column 이름&quot; 부분만 수정하면 됩니다)</p>
<ul>
<li>ex) 위 사진의 경우에는 <code>length(replaceAll(prop(&quot;제출 과제&quot;), &quot;(.+?)(?:,|$)&quot;, &quot;*&quot;))</code></li>
</ul>
<br/>

<h3 id="3-굳이-볼-필요-없는-column이니까-숨긴다">3. 굳이 볼 필요 없는 column이니까 숨긴다<img src="https://images.velog.io/images/danna-lee/post/87d82d54-31bb-4153-8ed2-00db9b0e7738/image.png" alt=""></h3>
<h3 id="4-태그-개수를-sort-기준에-넣는다">4. 태그 개수를 sort 기준에 넣는다<img src="https://images.velog.io/images/danna-lee/post/93a9bb43-7dcb-4844-a904-cc4e69a1d810/image.png" alt=""></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS 앱 키워드 버블 차트 개발기 (Swift로 버블 차트 구현하기)]]></title>
            <link>https://velog.io/@danna-lee/iOS-%EC%95%B1-%ED%82%A4%EC%9B%8C%EB%93%9C-%EB%B2%84%EB%B8%94-%EC%B0%A8%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EA%B8%B0-Swift%EB%A1%9C-%EB%B2%84%EB%B8%94-%EC%B0%A8%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@danna-lee/iOS-%EC%95%B1-%ED%82%A4%EC%9B%8C%EB%93%9C-%EB%B2%84%EB%B8%94-%EC%B0%A8%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EA%B8%B0-Swift%EB%A1%9C-%EB%B2%84%EB%B8%94-%EC%B0%A8%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 10 Oct 2021 14:58:03 GMT</pubDate>
            <description><![CDATA[<p>외주로 iOS 앱 개발을 하고 있는데, 키워드 버블을 구현해달라고 한다.
<img src="https://images.velog.io/images/danna-lee/post/4e6fc9fe-d02a-4e19-b1cd-84c874963a2a/image.png" width="300">
대충 이렇게 생긴 ..</p>
<p>당연히 레퍼런스는 없었다.
어딘가 한 번쯤 보게 생겼는데, 했을 때쯤에는 웹에서는 많이 봤지만 앱에서는 한 번도 본 적이 없다는 것을 깨달은 후였다.</p>
<p>막막했다.</p>
<p>막막하게 한 3일 정도 라이브러리 서치와 어떻게 구현할 수 있을지, javascript로 구현해서 web뷰 이식을 하는 방법이 나을지 별의 별 궁리를 다 했다.</p>
<p>그러다가 기적처럼 라이브러리 딱 하나를 발견했다.
<a href="https://www.highcharts.com/">https://www.highcharts.com/</a>
Highcharts라는 다양한 차트 라이브러리였다. 이미 웹 프론트엔드, 안드로이드, iOS 환경 모두를 지원하고 있었다. 깃허브를 잠시 살펴 보니 마지막 업데이트를 한지 1달도 채 되지 않을 만큼 꾸준히 업데이트가 일어나고 있었고, 차트 분야에서는 사용자도 꽤 많은 것 같았다. API docs도 꽤 논리적이고 꼼꼼하게 구성되어 있었다.</p>
<p>그 중에서도 내가 키워드 버블을 구현하기 위해 사용했던 라이브러리 모듈은 Packed bubble chart이다.
<a href="https://www.highcharts.com/demo/ios/packed-bubble">https://www.highcharts.com/demo/ios/packed-bubble</a>
<img src="https://images.velog.io/images/danna-lee/post/1012f1d1-6f35-4a1e-a0ab-ca30a60b1928/image.png" alt=""></p>
<p>코드도 예시가 나와 있어서 사실 docs 없이도 눈치껏 할 수 있을 것 같았다.</p>
<pre><code class="language-swift">import Highcharts
import UIKit

class ViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()

    let chartView = HIChartView(frame: view.bounds)

    let options = HIOptions()

    let chart = HIChart()
    chart.type = &quot;packedbubble&quot;
    chart.height = &quot;100%&quot;
    options.chart = chart

    let title = HITitle()
    title.text = &quot;Carbon emissions around the world (2014)&quot;
    options.title = title

    let tooltip = HITooltip()
    tooltip.useHTML = true
    tooltip.pointFormat = &quot;&lt;b&gt;{point.name}:&lt;/b&gt; {point.value}m CO&lt;sub&gt;2&lt;/sub&gt;&quot;
    options.tooltip = tooltip

    let plotOptions = HIPlotOptions()
    plotOptions.packedbubble = HIPackedbubble()
    plotOptions.packedbubble.minSize = &quot;30%&quot;
    plotOptions.packedbubble.maxSize = &quot;120%&quot;
    // plotOptions.packedbubble.zMin = 0
    // plotOptions.packedbubble.zMax = 1000
    plotOptions.packedbubble.layoutAlgorithm = HILayoutAlgorithm()
    plotOptions.packedbubble.layoutAlgorithm.splitSeries = &quot;false&quot;
    plotOptions.packedbubble.layoutAlgorithm.gravitationalConstant = 0.02

    let dataLabels = HIDataLabels()
    dataLabels.enabled = true
    dataLabels.format = &quot;{point.name}&quot;
    dataLabels.filter = HIFilter()
    dataLabels.filter.property = &quot;y&quot;
    dataLabels.filter.operator = &quot;&gt;&quot;
    dataLabels.filter.value = 250
    dataLabels.style = HIStyle()
    dataLabels.style.color = &quot;black&quot;
    dataLabels.style.textOutline = &quot;none&quot;
    dataLabels.style.fontWeight = &quot;normal&quot;

    plotOptions.packedbubble.dataLabels = [dataLabels]

    options.plotOptions = plotOptions

    let europe = HIPackedbubble()
    europe.name = &quot;Europe&quot;

    let germany = HIData()
    germany.name = &quot;Germany&quot;
    germany.value = 767.1

    let belgium = HIData()
    belgium.name = &quot;Belgium&quot;
    belgium.value = 97.2

    // 중략 (버블 선언부)

    let korea = HIData()
    korea.name = &quot;Korea&quot;
    korea.value = 610.1

    asia.data = [nepal, georgia, bruneiDarussalam, kyrgyzstan, afghanistan, myanmar, mongolia, sriLanka, bahrain, yemen, jordan, lebanon, azerbaijan, singapore, hongKong, syria, dPRKorea, israel, turkmenistan, oman, qatar, philippines, kuwait, uzbekistan, iraq, pakistan, vietnam, unitedArabEmirates, malaysia, kazakhstan, thailand, taiwan, indonesia, saudiArabia, japan, china, india, russia, iran, korea]

    options.series = [europe, africa, oceania, northAmerica, southAmerica, asia]

    chartView.options = options

    self.view.addSubview(chartView)</code></pre>
<p>사이트에 공개돼있는 해당 사진을 만들기 위한 swift 코드다.</p>
<p>기본적으로 JS 베이스로 되어있는 코드고, swift로 접근할 수 있게 만들어진 프레임워크로 코드를 짜면 그게 js로 변환되어 웹뷰로 띄우는 구조로 되어 있는 것 같았다.</p>
<p>하이차트로 swift 개발을 한 선례는 많이 없었지만, 곧 쉽게 커스텀 할 수 있게 되었는데, Js로 나와있는 예시를 swift로 다 변환할 수 있는 이 구조를 알면 쉬웠다.</p>
<p>예시를 하나 들자면, 만약 버블의 투명도를 조절하는 Js 코드 예시가 아래처럼 나와있다면, </p>
<pre><code class="language-javascript">Highcharts.chart(&#39;container&#39;, {
    plotOptions: {
        series: {
            marker: {
                fillOpacity: &#39;#FFFFFF&#39;, // 투명도 조절하는 부분
            }
        }
    },
});</code></pre>
<p>swift에서 쓸 때는 HIMarker 클래스를 marker 변수에 상속해준 후, 내가 기존에 선언해놓은 plotOptions.packedbubble에 해당 marker를 계속해서 이어주면 된다. 
js의 계층은 그대로 따르되, 클래스 변수를 선언하는 과정이 한 번 더 있어야 하는 것이다.</p>
<pre><code class="language-swift">/// 동그라미 투명도 조절하기
let marker = HIMarker()
plotOptions.packedbubble.marker = marker
plotOptions.packedbubble.marker.fillOpacity = 1</code></pre>
<p>이걸 알기 전에는 계속 프로퍼티가 nil이 떠서 고생했었는데, <code>let marker = HIMarker()</code> 한 줄에 모든 것이 해결되었다. 신기하다.</p>
<p>그래서 나는 햄버거바 숨기기, 크레딧 text 수정, 버블 투명도 조절, 타이틀 디자인, 카테고리 숨기기, 클릭 이벤트 넣기 등의 커스텀을 했다.</p>
<pre><code class="language-swift">/// 햄버거바 숨기기
let exporting = HIExporting()
exporting.enabled = false
options.exporting = exporting

/// 크레딧 자리에 기준날짜 띄우기
let credits = HICredits()
credits.text = &quot;\(getDateToday()) 기준&quot;
options.credits = credits

/// 동그라미 투명도 조절하기
let marker = HIMarker()
plotOptions.packedbubble.marker = marker
plotOptions.packedbubble.marker.fillOpacity = 1

/// 타이틀 디자인
let style = HICSSObject()
title.style = style
title.style.fontSize = &quot;15&quot;
title.style.fontWeight = &quot;bold&quot;

/// 카테고리 숨기기
let legend = HILegend()
legend.enabled = false
options.legend = legend

/// 클릭 이벤트
let chartSeries = HISeries()
plotOptions.series = chartSeries

let chartPoint = HIPoint()
plotOptions.series.point = chartPoint

let chartEvents = HIEvents()
plotOptions.series.point.events = chartEvents

let chartFunction = HIFunction(closure: { context in
  guard let context = context else { return }
  let bubbleIndex: Int = context.getProperty(&quot;this.index&quot;) as! Int
  // 클릭이벤트 처리 로직
}, properties: [&quot;this.index&quot;])

plotOptions.series.point.events.click = chartFunction</code></pre>
<p>이렇게 커스텀을 해서 결국
<img src="https://images.velog.io/images/danna-lee/post/8e142d80-5456-42a3-ad19-ad6762dbcbd0/image.png" alt="">
이런 식으로 서버에서 넘어오는 데이터를 버블차트에 띄울 수 있게 되었다!!</p>
<p>아쉬웠던 건,</p>
<ol>
<li>회색인 버블은 약간 진한 색의 데이터라벨을 띄우고 싶었고, 색이 있는 버블은 흰색의 데이터 라벨을 띄우고 싶었는데, 그 부분을 커스텀하는 법을 모르겠다는 것</li>
<li>데이터 라벨의 글자 크기를 버블 크기에 따라 바꾸고 싶은데, 거기까지는 커스텀을 하지 못했다는 것</li>
</ol>
<p>커스텀 할 수는 있을 것 같은데 서버 데이터도 받아오고 해야 해서 너무 하드코딩이 될 것 같아서 하지 않았는데 ..
나중에 시간이 더 많다면 이 부분 커스텀을 더 해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] xib 뷰 위에 얹은 테이블뷰에는 테이블뷰셀을 추가할 수 없다?!]]></title>
            <link>https://velog.io/@danna-lee/xib-%EB%B7%B0-%EC%9C%84%EC%97%90-%EC%96%B9%EC%9D%80-%ED%85%8C%EC%9D%B4%EB%B8%94%EB%B7%B0%EC%97%90%EB%8A%94-%ED%85%8C%EC%9D%B4%EB%B8%94%EB%B7%B0%EC%85%80%EC%9D%84-%EC%B6%94%EA%B0%80%ED%95%A0-%EC%88%98-%EC%97%86%EB%8B%A4</link>
            <guid>https://velog.io/@danna-lee/xib-%EB%B7%B0-%EC%9C%84%EC%97%90-%EC%96%B9%EC%9D%80-%ED%85%8C%EC%9D%B4%EB%B8%94%EB%B7%B0%EC%97%90%EB%8A%94-%ED%85%8C%EC%9D%B4%EB%B8%94%EB%B7%B0%EC%85%80%EC%9D%84-%EC%B6%94%EA%B0%80%ED%95%A0-%EC%88%98-%EC%97%86%EB%8B%A4</guid>
            <pubDate>Mon, 27 Sep 2021 10:11:27 GMT</pubDate>
            <description><![CDATA[<h3 id="prologue-어쩌다-">Prologue) 어쩌다 .....</h3>
<p>나는 원래 개발할 때 xib를 웬만하면 사용하지 않는 편인데,
이번에 외주 맡게 된 앱의 기존 코드를 보니 xib 덩어리라 
&quot;그래,,, 이 기회에 xib 연습이라도 더 해보자&quot; 하는 마음으로 새로운 뷰를 xib로 구현하고 있다.</p>
<p>그러던 중, 상상치도 못 한 난관에 봉착했는데,
<img src="https://images.velog.io/images/danna-lee/post/330595bc-6728-4684-bed4-4ed85218b510/image.png" alt="">
바로 xib 위에 얹은 테이블뷰에는 원래 개발하던 식으로 그 안에 tableViewCell을 얹고 나서 UI 작업을 하지 못 하는 거였다! 자꾸 TableView 안으로 cell이 안 들어가길래 넣으려고 요리조리 해봤지만, 절대 안 들어갔다 ..</p>
<br/>

<p>사실 테이블뷰도 기존 스토리보드 위 viewController에서 보던 거랑 조금 다르게 생기긴 했었다. (거기서부터 불안했음)
<img src="https://images.velog.io/images/danna-lee/post/5a5e634c-7ff0-4b4b-a271-207bbf93af49/image.png" width="50%"></p>
<p>조금 알아보니, 
<a href="https://stackoverflow.com/questions/41715537/not-able-to-add-static-uitableviewcell-into-tableview">https://stackoverflow.com/questions/41715537/not-able-to-add-static-uitableviewcell-into-tableview</a>
원래 xib에 있는 테이블뷰에는 커스텀 셀을 넣을 수 없고, 따로 xib로 제작해서 넣어야 한다고 한다.</p>
<p>오히려 좋아 ...
이 기회에 xib 마스터 해버리자.</p>
<br/>
<br/>

<h3 id="1-tableviewcell-만들기">1. TableViewCell 만들기<img src="https://images.velog.io/images/danna-lee/post/7321cb8b-09af-4588-87f1-9e29c8ed8b8b/image.png" alt=""></h3>
<p>원래처럼 테이블뷰 셀을 만든다. 
하나 달라진 게 있다면, Also create XIB file에 체크를 해줘야 xib 파일이 함께 만들어진다.</p>
<p>그러고 Next를 클릭한다.
<img src="https://images.velog.io/images/danna-lee/post/75131a7e-dd1a-428a-ad36-59d0f72c39ca/image.png" width="80%">이렇게 파일이 두 개 만들어졌다면 성공!</p>
<h3 id="2-테이블뷰셀-퍼블리싱">2. 테이블뷰셀 퍼블리싱<img src="https://images.velog.io/images/danna-lee/post/61d9aecc-fcc6-4679-ab08-f6854498bc46/image.png" alt=""></h3>
<p>원하는대로 테이블뷰셀을 만듭니다! 뚝딱뚝딱 🛠</p>
<h3 id="3-테이블뷰셀-identifier-지정">3. 테이블뷰셀 identifier 지정<img src="https://images.velog.io/images/danna-lee/post/eee5d552-9004-4a90-8bf2-c7692ae71191/image.png" alt=""></h3>
<p>1) TableViewCell 클릭
2) Attributes Inspector 클릭
3) identifier 적어주기</p>
<p>이 단계를 꼭 해주셔야 테이블뷰셀을 찾을 때, 테이블뷰셀이 재사용될 때, 해당 셀을 올바르게 불러올 수 있습니다.
identifier는 통상적으로 테이블뷰셀의 이름과 동일하게 짓는 경우가 가장 많습니다! 
아마 식별하기 편해서 그런 거겠죠?</p>
<h3 id="4-테이블뷰셀-장착">4. 테이블뷰셀 장착!</h3>
<p>이제 다시 원래 테이블뷰가 있던 뷰로 돌아와서
<code>viewDidLoad()</code>나 원하는 곳에</p>
<pre><code class="language-swift">// TableViewCell 불러오기
let nibName = UINib(nibName: &quot;FundDropdownTVC&quot;, bundle: nil)
fundTableView.register(nibName, forCellReuseIdentifier: &quot;FundDropdownTVC&quot;)</code></pre>
<p>이 코드를 추가해줍니다.
<code>nibName: &quot;[identifier]&quot;</code> 에는 3번에서 설정했던 identifier를 넣어주세요.</p>
<p>그 후 tableview datasource cellForRowAt에는 이렇게 적어주면 됩니다. 원래랑 똑같음!</p>
<pre><code class="language-swift">func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
    // 테이블뷰 셀 불러오기
    let cell = tableView.dequeueReusableCell(withIdentifier: &quot;FundDropdownTVC&quot;, for: indexPath) as! FundDropdownTVC

    // 테이블뷰셀에 넣을 데이터 설정
    cell.textLabel?.text = temp[indexPath.row]

    return cell
}</code></pre>
<h3 id="5-실행시키고-감탄하기">5. 실행시키고 감탄하기</h3>
<p>이렇게 하면 끝!!!</p>
<br/>
<br/>
<br/>

<p><code>👉🏻 참고한 친절한 글들 👈🏻</code>
<a href="https://dongminyoon.tistory.com/50">https://dongminyoon.tistory.com/50</a>
<a href="https://sunidev.github.io/ios/make-tableview-of-xib/">https://sunidev.github.io/ios/make-tableview-of-xib/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 실수로 .xcworkspace 파일을 .gitignore 해버렸다]]></title>
            <link>https://velog.io/@danna-lee/iOS-%EC%8B%A4%EC%88%98%EB%A1%9C-.xcworkspace-%ED%8C%8C%EC%9D%BC%EC%9D%84-.gitignore-%ED%95%B4%EB%B2%84%EB%A0%B8%EB%8B%A4</link>
            <guid>https://velog.io/@danna-lee/iOS-%EC%8B%A4%EC%88%98%EB%A1%9C-.xcworkspace-%ED%8C%8C%EC%9D%BC%EC%9D%84-.gitignore-%ED%95%B4%EB%B2%84%EB%A0%B8%EB%8B%A4</guid>
            <pubDate>Sat, 18 Sep 2021 18:27:36 GMT</pubDate>
            <description><![CDATA[<p>새벽에 졸면서 개발하다가 실수로 .xcworkspace 파일 전체를 .gitignore 해버렸다. 더 가관인 건 그 이후로 커밋을 한 20개 날렸고, 메인 브랜치에 pr 후 머지까지 한 상태에서 발견했다.</p>
<p>저같이 바보같은 행동 하실 분은 없겠지만 비슷한 에러를 겪게 되실 누군가를 위해 기록으로 남깁니다. 사실 심장 떨어질 뻔 한 거 기억하고 싶어서 쓰는 글이기도 합니다.</p>
<br/>

<p>pr 머지 완료 후 새로운 브랜치에서 프로젝트 파일을 열었는데,
<img src="https://images.velog.io/images/danna-lee/post/33c79fc4-2f29-4bc5-bb2b-84b27e606e17/image.png" alt="">
아무것도 없었다..</p>
<p>처음에는 그냥 xcode 버그인 줄 알고 xcuserdata도 지웠다 켜보고 노트북도 껐다 켜봤는데, 저 상태 그대로였다. 멘붕 ... 거짓말 안 치고 한 20번 껐다 켰다.</p>
<p>설상가상 깃에는 .xcworkspace 파일 전체가 안 올라가 있는 상황이라 살릴 수 있는 방법도 없었다. 살리려면 저 20개의 커밋을 리셋해야 하는 상황..!</p>
<p>온 몸에 기운이 다 빠져서 그냥 잘까 하다가 오늘 해결 못 하면 내일은 더 하기 싫어질 거 같아서 일단 깃에 올려뒀던 다른 프로젝트 파일을 살펴봤다. .xcworkspace 파일 안에 대체 뭐가 있는지!!!!</p>
<p>그랬더니 심상치 않은 녀석 하나를 발견할 수 있었다. <img src="https://images.velog.io/images/danna-lee/post/c4f1bc03-15de-4904-bfd9-500af26de3da/image.png" alt="">요 녀석!!</p>
<p>누가 봐도 .xcworkspace에서 불러올 프로젝트 파일, Pod 파일을 명시하고 있는 파일이었다. 흐흐</p>
<p>근데 내 건?
vi 에디터로 열어보니 아무것도 없었다. FileRef가 없고 그냥 워크스페이스만 덩그러니 있는 상태.. 그래서 옛 커밋을 돌려보며 저 파일이 살아있는 커밋을 찾아냈다.</p>
<p>임시 브랜치를 하나 만들어서 그 커밋으로 리셋한 후, 저 파일 내용만 복사해와서 붙여넣어줬다.
<img src="https://images.velog.io/images/danna-lee/post/5e267372-86cb-4e86-9842-e921191e90fe/image.png" alt="">
그랬더니..!!!!!
<img src="https://images.velog.io/images/danna-lee/post/8558032c-5065-4671-8b31-d806d61e437f/image.png" alt="">
열렸다 ... ㅠ</p>
<br/>
<br/>
<br/>

<p>오늘도 알차게 삽질했다!
졸려도 gitignore 할 때는 정신 차리고 하자!</p>
]]></description>
        </item>
    </channel>
</rss>