<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>2na</title>
        <link>https://velog.io/</link>
        <description>Studying Frontend 👩🏻‍💻</description>
        <lastBuildDate>Wed, 30 Jul 2025 12:53:43 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>2na</title>
            <url>https://velog.velcdn.com/images/y-eonee/profile/1636ba72-67f8-4913-ab03-6fc335fa738e/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 2na. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/y-eonee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[iOS] Signed URL 발급 & 이미지 저장 서버 통신하기 ]]></title>
            <link>https://velog.io/@y-eonee/iOS-Signed-URL-%EB%B0%9C%EA%B8%89-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%80%EC%9E%A5-%EC%84%9C%EB%B2%84-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@y-eonee/iOS-Signed-URL-%EB%B0%9C%EA%B8%89-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%80%EC%9E%A5-%EC%84%9C%EB%B2%84-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 30 Jul 2025 12:53:43 GMT</pubDate>
            <description><![CDATA[<p>이번 앱잼에서 이미지를 저장할 때에는 URL을 발급받고 버킷에 따로 저장을 하였습니다. 
그래서 Signed URL 발급을 위한 POST API가 따로 존재했습니다. </p>
<p>이미지 저장을 위한 API 호출 순서는 다음과 같습니다. </p>
<ol>
<li>버킷에 이미지 저장을 하기 위한 Signed URL 발급을 받기 위한 POST API를 호출합니다. 이미지는 UUID 키로 보내줍니다. </li>
<li>발급받은 Signed URL로, 이미지를 Data 형태로 PUT method로 보내줍니다. </li>
<li>그 후 이미지와 그외 저장할 내용들을 다시 POST합니다. </li>
</ol>
<h3 id="🛠-해결-과정">🛠 해결 과정</h3>
<p><strong>1. 이미지를 UUID 키로 바꾸기</strong> </p>
<ul>
<li><p>처음에 UUID 키라고 해서 좀 어렵게 생각했던 것 같습니다. 이미지 자체를 변환하는 것이 아니라, 이미지를 저장하는 버킷에서 이미지를 구분할 수 있도록 하는 고유한 키값을 만들어서 보내주는 것이라고 생각하였습니다. </p>
<pre><code class="language-swift">//WriteActiveQuestViewController
func saveQuest() {
  let uuidKey = UUID().uuidString
  ByeBooLogger.debug(&quot;UUID: \(uuidKey)&quot;)
  self.viewModel.action(.didTapCompleteButton(
      questID: self.questID,
      answer: self.answerText,
      emotionState: self.emotionState,
      image: self.image,
      imageKey: uuidKey)
  )
}</code></pre>
<p>뷰컨트롤러에서, 뷰모델의 action 함수를 호출하는 부분에서 UUID를 생성해서 저장할 내용들을 파라미터로 넘겨주었습니다.</p>
<pre><code class="language-swift">func postActiveQuest(
  questID: Int,
  answer: String,
  emotionState: String,
  image: Data,
  imageKey: String
) async throws {
  let url = try await makeSignedURL(imageKey: imageKey)

  try await putImage(signedURL: url, image: image)
  try await saveQuest(questID: questID, answer: answer, emotionState: emotionState, imageKey: imageKey)
}</code></pre>
</li>
</ul>
<p>Repository 안에 있는 하나의 함수에서, 세부 로직을 각각의 비동기 private 함수로 분리하여 순차적으로 호출하도록 구성했습니다.</p>
<p><strong>2. Signed URL 발급 API 호출</strong></p>
<pre><code class="language-swift">private func makeSignedURL(imageKey: String) async throws -&gt; String {
    let userID: Int = userDefaultService.load(key: .userID) ?? 1
    let signedURLRequestDTO = SignedURLRequestDTO(contentType: &quot;image/jpeg&quot;, imageKey: imageKey)

    let result = try await network.request(
        QuestAPI.images(userID: userID, request: signedURLRequestDTO),
        decodingType: SignedURLResponseDTO.self
   )

    return result.signedUrl
}</code></pre>
<p>UUID를 Request에 담아 Signed URL을 발급받는 POST API를 호출합니다.</p>
<p><strong>3. Signed URL로 PUT 메서드 호출</strong></p>
<pre><code class="language-swift">    enum QuestAPI {
        case checkQuest(userID: Int, questID: Int)
        case recording(userID: Int, questID: Int, request: SaveQuestRequestDTO)
        case active(userID: Int, questID: Int, request: SaveQuestActiveRequestDTO)
     ....
    }</code></pre>
<p>원래는 엔드포인트가 같은 api들끼리 enum을 나눠서 추상화 후 호출하는 방식으로 코드를 작성했었습니다. </p>
<pre><code class="language-swift">     private func putImage(signedURL: String, image: Data) async throws {
          try await network.request(image: image, signedURL: signedURL)
      }</code></pre>
<p>하지만 PUT 메서드를 보낼 때에는 Signed URL이 엔드포인트 그 자체가 되기 때문에 추상화하여 사용이 불가능했습니다. </p>
<p>  그래서 따로 Network service에 request 함수를 더 만들어서, PUT 메소드용으로 사용했습니다. </p>
<pre><code class="language-swift">/// 이미지 처리
func request(image: Data, signedURL: String) async throws {
    return try await withCheckedThrowingContinuation { continuation in
    AF.upload(
        image,
        to: signedURL,
         method: .put,
         headers: [&quot;Content-Type&quot;: &quot;image/jpeg&quot;]
    )
    .validate()
    .response { response in
        ByeBooLogger.network(response)
        if let error = response.error {
            ByeBooLogger.error(error)
            continuation.resume(throwing: ByeBooError.unknownError)
        } else {
            ByeBooLogger.debug(&quot;이미지 업로드 성공&quot;)
            continuation.resume()
        }
    }
}</code></pre>
<p>이렇게 이미지를 보내게 되면, 버킷에 이미지가 성공적으로 저장됩니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/3f716698-f3a2-4b6d-aedc-21adf1ed2708/image.png" alt=""></p>
<p><strong>4. 나머지 정보들 POST 하기</strong> </p>
<pre><code class="language-swift">    private func saveQuest(
            questID: Int,
            answer: String,
            emotionState: String,
            imageKey: String
        ) async throws {
            let userID: Int = userDefaultService.load(key: .userID) ?? 1
            let saveQuestActiveDTO = SaveQuestActiveRequestDTO(
                imageKey: imageKey,
                answer: answer,
                questEmotionState: emotionState
            )

            let _ = try await network.request(
                QuestAPI.active(userID: userID, questID: questID, request: saveQuestActiveDTO)
            )
        }</code></pre>
<p>처음에 만들었던 이미지 UUID 키를 다시 서버로 보내 나머지 정보들을 같이 POST 하면 성공적으로 이미지를 저장할 수 있게 됩니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/d0594d0c-f79b-4666-855d-7d92171d2fee/image.png" alt=""></p>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/3e3ad1d7-1b2c-4f32-9561-1e585f282412/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] UITextView fall back 폰트 적용하기 / flick 현상 해결]]></title>
            <link>https://velog.io/@y-eonee/iOS-UIText-fall-back-%ED%8F%B0%ED%8A%B8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@y-eonee/iOS-UIText-fall-back-%ED%8F%B0%ED%8A%B8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 30 Jul 2025 12:23:26 GMT</pubDate>
            <description><![CDATA[<h3 id="🔧-문제">🔧 문제</h3>
<p>UITextView에서 텍스트를 입력 시 글자가 계속 깜빡거리면서, 뷰에서 사라졌다가 생기는 것이 반복되는 현상입니다 .</p>
<h3 id="🧠-원인-분석">🧠 원인 분석</h3>
<p>텍스트를 하나하나 천천히 입력하면서 테스트를 했을 때, 뷰에서 안보이는 글자들은 모두 폰트에서 미지원되는 글자들이었습니다.</p>
<p>디자인시스템에서는 SUIT 폰트를 사용하고 있었기 때문에, 폰트 사이트 눈누에서, 지원되지 않는 글자가 어떤 것인지 확인하기 위해 먼저 테스트를 했습니다.</p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/7dfa4c0c-ce4c-4c03-803d-ed96a78e79d8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/b0a40edd-5d7d-493b-8b03-6f39dd62fb9c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/e03f6722-2ef4-49ea-8dfa-20ab0ef9ba2f/image.png" alt=""></p>
<p>‘고은학생’이라는 텍스트를 입력하는 과정에서, 이렇게 폰트가 깨지는 글자가 있습니다.</p>
<p>이경우 뷰에서 확인하게 되면 </p>
<p> <img src="https://velog.velcdn.com/images/y-eonee/post/13e2f679-d7d0-477c-a5fc-acf72f6cf5b7/image.gif" alt=""></p>
<p>뷰에서 사라지는 글자들은 모두 폰트에서 미지원되는 글자들임을 확인할 수 있었습니다. 사용자가 입력 중일때 글자가 깜빡거리는 것처럼 보여서 UX적 측면에서 좋지 않다고 느꼈습니다. </p>
<h3 id="🛠-해결-과정">🛠 해결 과정</h3>
<p>Fall back 폰트를 설정해주기 위해 Core Text를 이용하였습니다. 
Core Text는 폰트관련 저수준 인터페이스인데, UIFont보다 더욱 정교한 처리를 구현할 수 있다고 합니다. (<a href="https://developer.apple.com/documentation/coretext/">https://developer.apple.com/documentation/coretext/</a>) </p>
<pre><code class="language-swift">// String+

extension String {
    func canBeRendered(by font: UIFont) -&gt; Bool {
        let cfFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil)
        return CTFontGetGlyphWithName(cfFont, self as CFString) != 0
    }
}</code></pre>
<p>CTFont는 폰트를 Glyf(글리프)로 변환할 수 있습니다. </p>
<p>입력하고 싶은 글자의 폰트가 SUIT-Regular라면,  <code>CRFontCreateWithName</code> 으로를 SUIT-Regular를 글리프 버전으로 만듭니다. </p>
<p><code>CTFontGetGlyphWithName</code> 으로, 입력하고 싶은 글자가 ctFont 내에 있는지 확인하고 렌더링 여부에 따라 Bool 값을 리턴합니다. 그래서 읁, 핛 이런 글자들은 글리프에 없기 때문에 false가 리턴됩니다. </p>
<pre><code class="language-swift">// UITextViewDelegate 
func textViewDidChange(_ textView: UITextView) {
        if textView.text.count &gt; limitCount {
            textView.deleteBackward()
        }
//입력된 글자를 전부 받아옵니다 
        let fullText = textView.text ?? &quot;&quot;

// 지원안될 때 폰트를 설정합니다 (시스템폰트로 설정함)
        let fallbackFont = UIFont.systemFont(ofSize: 16)

        let suitFont = FontManager.body3R16.font

//입력된 글자를 스타일 설정 가능한 attributed string으로 초기화합니다       
        let attrStr = NSMutableAttributedString(string: fullText)

// 전체 글자를 돌면서 폰트 지원이 안되면 fallback폰트로 설정합니다 
        for (index, char) in fullText.enumerated() {
            let range = NSRange(location: index, length: 1)
            let fontToUse = String(char).canBeRendered(by: suitFont) ? suitFont : fallbackFont
            attrStr.addAttribute(.font, value: fontToUse, range: range)
            attrStr.addAttribute(.foregroundColor, value: UIColor.white, range: range)
        }

//최종적으로 글자를 할당합니다    
        textView.attributedText = attrStr

        count = textView.text.count
        textCount.text = &quot;(\(count)/\(limitCount))&quot;
        delegate?.changeStyle(count: count)
    }</code></pre>
<h3 id="✅-결과">✅ 결과</h3>
<p> <img src="https://velog.velcdn.com/images/y-eonee/post/fb8f6b5b-37f2-4b1e-9ce7-d467ad412729/image.gif" alt=""></p>
<p>+) 여담
안드로이드에서는 이 문제를 해결할 수 없어 결국 폰트가 프리텐다드로 전면 수정되었음... </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Swift] 초기화 ]]></title>
            <link>https://velog.io/@y-eonee/Swift-%EC%B4%88%EA%B8%B0%ED%99%94</link>
            <guid>https://velog.io/@y-eonee/Swift-%EC%B4%88%EA%B8%B0%ED%99%94</guid>
            <pubDate>Fri, 20 Jun 2025 03:26:08 GMT</pubDate>
            <description><![CDATA[<p>구조체와 클래스의 초기화 방법에 대해 작성했습니다. </p>
<h1 id="1️⃣-구조체">1️⃣ 구조체</h1>
<p>구조체를 초기화하는 방법은 다움과 같습니다. </p>
<p>우선 <code>초기화</code>라는 용어에 대해 알아보면!</p>
<blockquote>
<p>초기화란, 구조체, 열거형, 클래스의 인스턴스를 생성하는 것입니다.
초기화의 역할은 모든 프로퍼티를 기본값으로 초기화하는 것이며, 인스턴스 내에 기본값이 존재하지 않는 프로퍼티가 있을 경우 초기화에 실패하고, 인스턴스는 생성되지 않습니다.</p>
</blockquote>
<p>정리하자면, 초기화함수가 종료되기 전까지 구조체, 클래스 내에 있는 모든 속성값들을 기본값으로 초기화해야한다는 것입니다! </p>
<h3 id="1-선언과-동시에-프로퍼티에-기본값-넣기">1. 선언과 동시에 프로퍼티에 기본값 넣기</h3>
<pre><code class="language-swift">struct People {
    let name: String = &quot;nayeon&quot;
    let age: Int = 23
}</code></pre>
<p>변수를 선언함과 동시에 기본값을 넣어 초기화하는 방법입니다. </p>
<h3 id="2-프로퍼티-타입-옵셔널로-설정하기">2. 프로퍼티 타입 옵셔널로 설정하기</h3>
<pre><code class="language-swift">struct People {
    let name: String?
    let age: Int?
}</code></pre>
<p>프로퍼티를 옵셔널로 설정하면, 자동으로 <code>nil</code>로 초기화됩니다. </p>
<h3 id="3-init-함수에서-값-설정하기">3. init 함수에서 값 설정하기</h3>
<pre><code class="language-swift">struct People {
    let name: String?
    let age: Int?

    init(name: String) {
        self.name = name
        self.age = 23
    }
}</code></pre>
<p>init함수, 즉 생성자를 통해 초기화를 진행할 수 있는데요. 프로퍼티에 직접 값을 넣지 않고 생성자에서 프로퍼티를 초기화할 수 있습니다 .</p>
<h3 id="🔵-meberwise-initializers">🔵 Meberwise Initializers</h3>
<p>구조체는 사실 init도 작성하지 않고, 프로퍼티에 값을 넣어주지 않아도 에러가 나지 않습니다. 구조체는 <code>Memberwise intializer</code>라는 초기화를 기본적으로 제공하고 있기 때문인데요. </p>
<p>인스턴스의 생성을 위해 init 시 무조건 프로퍼티의 값을 설정할 수 있도록 구조체에서 생성자를 자동으로 제공합니다. </p>
<pre><code class="language-swift">struct People {
    let name: String
    let age: Int
}</code></pre>
<p>이렇게 init 함수도 작성하지 않았고, 프로퍼티에 값도 넣어주지 않았습니다. 한번 확인해볼게요!</p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/4fafaf07-4a46-44d1-968c-e94c86f9d8ed/image.png" alt="">
<del>코파일럿 쓰니까 밑에 자동으로 뜨는데 무시해주세요</del></p>
<p>이 init함수를 통해 초기화를 진행할 수 있습니다. 대신 여기서 주의할 점이 있는데요!</p>
<p>바로 프로퍼티가 <strong>선언된 순서</strong>를 지켜야한다는 점입니다. name → age 순으로 선언했는데, 초기화 파라미터에 age, name 이렇게 쓰게 되면 에러가 납니다. </p>
<p>또한, 생성자를 구조체 안에서 직접 만든 경우 memberwise initializer는 제공되지 않습니다! </p>
<h1 id="2️⃣-클래스">2️⃣ 클래스</h1>
<p>클래스를 초기화하는 방법은 다음과 같습니다. 큰 틀은 구조체와 비슷하지만, 몇가지 차이점이 있습니다. </p>
<h3 id="1-선언과-동시에-프로퍼티에-기본값-넣기-1">1. 선언과 동시에 프로퍼티에 기본값 넣기</h3>
<pre><code class="language-swift">class People {
    let name: String = &quot;nayeon&quot;
    let age: Int = 23
}</code></pre>
<p>변수를 선언함과 동시에 기본값을 넣어 초기화하는 방법입니다. </p>
<h3 id="2-프로퍼티-타입-옵셔널로-설정하기-1">2. 프로퍼티 타입 옵셔널로 설정하기</h3>
<pre><code class="language-swift">class People {
    var name: String?
    var age: Int?
}</code></pre>
<p>프로퍼티를 옵셔널로 설정하면, 자동으로 <code>nil</code>로 초기화됩니다. </p>
<p>대신! let은 옵셔널로 선언할 수 없습니다. 왜냐하면, class는 memberwise initializer가 제공되지 않기 때문입니다. let은 값을 바꿀 수 없는데, 옵셔널로 선언하게 되면 nil 외에는 값을 가질 수 없고, 심지어 값을 나중에 다시 생성해줄 수도 없기 때문입니다.  </p>
<h3 id="3-init-함수에서-값-설정하기-designated-initializers">3. init 함수에서 값 설정하기 (<strong>Designated Initializers)</strong></h3>
<pre><code class="language-swift">struct People {
    let name: String?
    let age: Int?

    init(name: String) {
        self.name = name
        self.age = 23
    }
}</code></pre>
<p>init함수, 즉 생성자를 통해 초기화를 진행할 수 있는데요. 프로퍼티에 직접 값을 넣지 않고 생성자에서 프로퍼티를 초기화할 수 있습니다.</p>
<p>이 방식으로 서브클래스 초기화를 하는 경우, 반드시 슈퍼클래스의 initializer를 호출해주어야 합니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/3455e185-39d8-4d52-8664-9da06b49b996/image.png" alt=""></p>
<h3 id="4-편의-생성자-convenience-initializers">4. 편의 생성자 (Convenience Initializers)</h3>
<p>모든 프로퍼티를 초기화하는 init 이니셜라이저(Designated Initializers)를 도와주는 역할입니다. init 이니셜라이저의 파라미터 중 원하는 값을 기본값으로 설정하는 방식입니다. 어쨌든 도와주는 역할이기 때문에 init 이니셜라이저는 필수적으로 구현되어야 합니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/51c5e316-ec47-42b4-a3a3-8c178668df26/image.png" alt="">
<code>Convenience Initializers</code> 가 불리는 순서인데요. 모로 가든 init 이니셜라이저가 마지막에 불리게 됩니다. 예제로 볼게요.</p>
<pre><code class="language-swift">class Person {
    var name: String
    var age: Int

    init(name: String, age: Int){
        self.name = name
        self.age = age
    }

    convenience init(name: String){
        self.init(name: name, age: 100)
    }

    convenience init(){
        self.init(name: &quot;unknown&quot;, age: 100)
    }
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/86578536-9108-4f38-9458-0458b9266f1c/image.png" alt=""></p>
<p>여기 보시면 편의생성자가 2개 생성되어있는데요.</p>
<p>편의 생성자 안에서는 self.init으로 init 이니셜라이저를 부르고 있습니다. </p>
<p>name이 파라미터에 있는 생성자랑, 파라미터에 아무것도 없는 생성자가 있습니다. </p>
<p>편의 생성자는 기본값을 설정해주는 생성자라고 이해하면 좀 더 쉬운데요. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/d4a012b8-d811-439a-aaa9-f059547aa48c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/e4b4ce62-feec-4509-b4dc-42aac570795c/image.png" alt=""></p>
<p>이렇게 3개의 객체를 생성해봤습니다. <code>nayeon</code> 객체에는 name, age를 다 줬고, <code>hyoeun</code> 객체에는 name만, <code>person</code>객체에는 아무것도 주지 않았습니다. </p>
<p>생성자에 들어간 파라미터에 따라 각기 다른 생성자가 불리게 된 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS/SwiftUI]  LazyVStack으로 Sticky Header 구현하기]]></title>
            <link>https://velog.io/@y-eonee/iOSSwiftUI-LazyVStack%EC%9C%BC%EB%A1%9C-Sticky-Header-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@y-eonee/iOSSwiftUI-LazyVStack%EC%9C%BC%EB%A1%9C-Sticky-Header-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 02 Jun 2025 17:29:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/y-eonee/post/ba7b3714-fa7c-4f75-a149-925d41babc55/image.png" alt=""></p>
<p>일반 Stack과 다르게, <code>Lazy</code> 키워드가 붙은 LazyVStack과 LazyHStack은 각 아이템이 필요할 때 생성됩니다. 세미나에서 배운 LazyHGrid, LazyVGrid와 비슷한 방식이라고 할 수 있습니다. 
VStak에 들어가는 내용이 엄청 많다면 Lazy로 관리해주는게 더 효율적이라고 합니다.</p>
<p>이번 주차 과제에서는 스티키헤더를 만들기 위해 LazyVStack의 pinnedViews를 이용했습니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/10cf9587-dfe0-43a3-b6dc-92f50c860177/image.png" alt=""></p>
<pre><code class="language-swift">
 ScrollView {
     LazyVStack(pinnedViews: [.sectionHeaders]) {
        HeaderView()

        Section(header: TabbarView(selectedTab: $selectedTab)) {...
        }
   }
   ...
}
</code></pre>
<p>pinnedViews 파라미터에는 두가지 타입이 있습니다.
<img src="https://velog.velcdn.com/images/y-eonee/post/1dcb24c5-e049-4a2e-a8ac-b0ea5cfb3ce1/image.png" alt=""></p>
<p>header와 footer가 있는데요. 위쪽에 만들어줄거기 때문에 section Header를 파라미터에 넣어줬습니다. </p>
<p>그리고 스티키헤더로 만들어줄 부분을 Section(header:)에 넣어줍니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/f2b20f2f-1d0a-4077-8e20-02830f39afac/image.gif" alt=""></p>
<h2 id="☄️발생했던-트러블-슈팅">☄️발생했던 트러블 슈팅</h2>
<p>스티키헤더 위의 세이프 에리아 부분에 콘텐츠가 계속 보였습니다. 탭뷰 부분만 검정색으로 처리해주어도 그 문제가 계속되었습니다.
<a href="https://babbab2.tistory.com/163">이 아티클</a>을 보고, ScrollView에 padding top을 줌으로써  뷰를 짤 때 safe are와 스크롤뷰가 처음부터 떨어져있도록 여백을 주어 해결했습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Swift 문법] 속성 (Property)]]></title>
            <link>https://velog.io/@y-eonee/Swift-%EC%86%8D%EC%84%B1-Property</link>
            <guid>https://velog.io/@y-eonee/Swift-%EC%86%8D%EC%84%B1-Property</guid>
            <pubDate>Mon, 02 Jun 2025 17:06:28 GMT</pubDate>
            <description><![CDATA[<p>프로퍼티의 종류에 대해 정리합니다 ✍🏻</p>
<h3 id="저장-속성">저장 속성</h3>
<p>값이 저장되는 일반적인 속성입니다. <code>var</code>, <code>let</code> 으로 선언 가능하며, 객체를 초기화할 때 각 저장속성은 반드시 값을 가져야합니다. (nil로 초기화하는 것도 가능함)</p>
<h3 id="지연-저장-속성">지연 저장 속성</h3>
<p><code>lazy</code> 키워드로 해당 속성의 초기화를 지연시킵니다.  그래서 인스턴스가 초기화되는 시점에 해당 속성이 초기화되는 것이 아닌 해당 속성에 접근하는 순간에 개별적으로 초기화됩니다. 그래서 let으로는 선언할 수 없습니다. (let은 초기화할 때 바로 값을 고정시켜주고 싶은데, lazy는 나중으로 값 초기화를 미루니깐..) </p>
<p>lazy를 사용하는 이유는 다음과 같이 정리해볼 수 있습니다. </p>
<ul>
<li><p>초기화 비용이 많이 필요한 뷰를 생성할 때</p>
<ul>
<li>바로 메모리에 올려버리면 생성 비용이 많이 들기 때문에 나중에 필요할 때 메모리에 올리는 방식으로 사용할 수 있습니다.</li>
</ul>
</li>
<li><p>항상 필요한 것이 아니고, 특정 시점 이후에 필요한 뷰를 생성할 때</p>
</li>
<li><p>다른 속성을 이용해야 할때</p>
<pre><code class="language-swift">  private let formatter = CalendarDateFormatter()
  private lazy var selectedIndex = formatter.getTodayDay()
  public lazy var selectedDate: String = weekDates[selectedIndex]</code></pre>
<ul>
<li><p>formatter를 사용하여 변수를 생성하고 있는데, 만약에 lazy 없이 그냥 생성하게 된다면</p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/ce8193d1-a30c-4e56-9b00-44b66fef33cf/image.png" alt=""></p>
<p>이런 에러가 나게 됩니다. self가 available하기 전에 초기화할 수 없다고 하는데요.</p>
</li>
<li><p>무슨 말이냐면….</p>
<p>  Swift에서는 모든 저장 속성이 초기화된 이후에서야 self(현재 인스턴스 자기 자신)가 유효해집니다.  원래는 self.fomatter.getTodayDay() 이렇게 써야하는데, 사실상 생략되어서 쓰이고 있는거죠. 그래서 self를 쓸 수 없는데 사용했다고 에러가 나는 것입니다. </p>
</li>
</ul>
</li>
</ul>
<h3 id="계산-속성">계산 속성</h3>
<p>속성의 형태를 가진 실질적 메소드입니다. 우리는 메소드를 만들 때, get, set 메소드를 많이 만들잖아요. 그런 비슷한 역할을 하는 속성입니다. </p>
<p>var로만 선언 가능하고, 자료형은 필수로 선언해야합니다. </p>
<pre><code class="language-swift">struct Rectangle {
    var width: Double
    var height: Double

    var area: Double {
        get {
            return width * height
        }
        set {
            height = newValue / width
        }
    }
}</code></pre>
<p>Rectangle이라는 구조체를 선언했고, 그 안에 area라는 계산 프로퍼티를 만들었습니다. </p>
<p>get 접근자는 area에 접근될때마다 area 값을 리턴해줍니다. set 접근자는 속성에 새로운 값이 할당되면 그 값(new Value)을 기반으로 다른 속성의 값을 조정할 수 있습니다. </p>
<pre><code class="language-swift">var rect = Rectangle(width: 5.0, height: 10.0)

print(&quot;첫번째 height: &quot;, rect.height)
print(&quot;첫번째 area: &quot;, rect.area)

rect.area = 100.0

print(&quot;두번째 height: &quot;, rect.height)
print(&quot;두번째 area: &quot;, rect.area)</code></pre>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/6ed02b57-9ea0-4b7c-b029-fbfe987c4544/image.png" alt=""></p>
<p>첫번째 area를 출력할 때는 get을 통해 접근하여 계산된 값을 리턴하게 됩니다. </p>
<p>그 이후에는 100으로 면적을 바꿔주었기 때문에, set에서 다른 속성들이 값이 바뀌게 됩니다. </p>
<h3 id="타입-속성">타입 속성</h3>
<p>타입 프로퍼티를 쉽게 말하면, 저장 속성과 계산 속성 앞에 <code>static</code> 키워드를 붙인 것입니다. 그러면 저장 타입 프로퍼티 &amp; 계산 타입 프로퍼티가 됩니다. 이렇게 static을 붙이게 되면 자동으로 lazy하게 작동합니다. </p>
<p>이렇게 static이 붙으면 전역변수처럼 사용되는데요. 코드를 통해 보겠습니다. </p>
<pre><code class="language-swift">class Person {
    static let name: String = &quot;nayeon&quot;
    var age: Int = 23
}

print(Person.name)

let person = Person()
print(person.age)</code></pre>
<p>name에만 static을 붙여 저장 타입 프로퍼티로 만들어줬고, age는 그냥 var로 선언해주었습니다. </p>
<p>이렇게 static으로 선언하게 되면 객체를 선언할 필요 없이 Person.name으로 바로 접근이 가능합니다. </p>
<p>반면에 static으로 선언하지 않은 age는 객체를 생성해야만 age에 접근이 가능합니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/86dd6791-c502-411c-a658-8c92a2b797f1/image.png" alt="">
person.name으로 하면 에러가 납니다</p>
<p>이때, 저장 타입 프로퍼티의 경우 항상 초기값을 선언해주어야합니다. 왜냐하면, 타입 프로퍼티는 초기화될 때 한번만 불려서 메모리에 올라가면 그 뒤로는 생성되지 않습니다. 대신 언제 어디서든 그 타입 프로퍼티에 접근할 수 있게 됩니다. 그래서 class 내의 init 함수와는 상관 없이 static으로 된 타입 프로퍼티는 무조건 초기값을 가져야합니다. </p>
<p>또한 위에서 static이 붙은 타입 프로퍼티는 lazy하게 작동한다고 했는데요. Person.name으로 불렸을 때, 그때 메모리에 올라가서 초기화됩니다. </p>
<p>하지만 static은 오버라이딩이 불가능합니다. 하지만! 앞에 static 대신 <strong><code>class</code> 키워드가 붙은 계산 타입 프로퍼티는 오버라이딩이 가능합니다.</strong> </p>
<pre><code class="language-swift">class Person {
    class var age: Int {
        get {
            return 23
        }
    }
}

class Nayeon: Person {
    override class var age: Int {
        get {
            return 24
        }
    }
}</code></pre>
<h3 id="속성-감시자-property-observer">속성 감시자 (Property Observer)</h3>
<p>프로퍼티 옵저버는 말그대로 프로퍼티를 감시하는데요. 내가 관찰하는 프로퍼티의 값이 변경되는 것을 감지하고 알려줍니다. 이 속성 감시자는 저장 프로퍼티에 한해 추가할 수 있습니다. </p>
<p>속성 감시자는 두가지가 있는데요.</p>
<ul>
<li><code>willSet</code> : 값이 저장되기 직전에 호출됨<ul>
<li>값이 저장되기 직전에, 새로운 값이 파라미터 newValue로 전달됩니다.</li>
</ul>
</li>
<li><code>didSet</code>: 새 값이 저장된 직후에 호출됨<ul>
<li>값이 저장된 직후에, 이전 값이 파라미터 oldValue로 전달됩니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-swift">var name: String = &quot;나연&quot; {
    willSet {
        print(&quot;현재 이름: \(name), 바뀔 이름 : \(newValue)&quot;)
    }
    didSet {
        print(&quot;현재 이름: \(name), 이전 이름 : \(oldValue)&quot;)
    }
}

name = &quot;효은&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/2f82b355-eee7-4d98-bc17-beb01117cf7a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS/Swift] 디코딩 에러 / 서버에서 데이터를 내려주지 않을 때 ]]></title>
            <link>https://velog.io/@y-eonee/iOSSwift-%EB%94%94%EC%BD%94%EB%94%A9-%EC%97%90%EB%9F%AC-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%82%B4%EB%A0%A4%EC%A3%BC%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C</link>
            <guid>https://velog.io/@y-eonee/iOSSwift-%EB%94%94%EC%BD%94%EB%94%A9-%EC%97%90%EB%9F%AC-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%82%B4%EB%A0%A4%EC%A3%BC%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C</guid>
            <pubDate>Mon, 26 May 2025 16:22:46 GMT</pubDate>
            <description><![CDATA[<p>네트워킹 세팅 시 공통적으로 사용되는 베이스 리스폰스를 지정해주었습니다.</p>
<pre><code class="language-swift">import Foundation

struct BaseResponse &lt;T: Decodable&gt;: Decodable {
    let code: String
    let message: String
    let data: T?
}
</code></pre>
<img src = "https://velog.velcdn.com/images/y-eonee/post/d8a12260-eaa7-4642-abc3-d9c70c26dad5/image.png" width = "720"/>

<p>하지만 일부 API에서 요청은 200으로 잘 되는데, 디코딩에러가 발생했습니다.</p>
<p>문제 상황은 다음과 같았습니다.</p>
<pre><code class="language-json">{
  &quot;code&quot;: &quot;s2040&quot;,
  &quot;message&quot;: &quot;요청이 성공했습니다.&quot;
}</code></pre>
<p>PATCH api에서는 서버에서 설정한 리스폰스에 data가 오지 않았습니다. 
베이스 리스폰스에는 data가 있는데, 리스폰스에는 data가 nil로 오는 것도 아니고 아예 오지 않았기 때문에 디코딩 에러가 발생했던 것입니다. </p>
<p>이런 경우 해결방법은 2가지가 있는데요.</p>
<blockquote>
<ol>
<li>서버에서 data를 nil로 내려달라고 한다.</li>
<li>클라이언트 측에서 분기처리를 해준다.</li>
</ol>
</blockquote>
<p>저는 2번의 방법으로 해결했습니다. </p>
<h1 id="디코딩에러-트러블슈팅">디코딩에러 트러블슈팅</h1>
<pre><code class="language-swift">struct VoidType: Decodable {}

final class SubTaskPatchService {
    let shared = BaseService.shared

    func patchSubTask(id: Int, request: SubTaskPatchRequest) async throws -&gt; VoidType {
        do {
            let response: VoidType = try await shared.request(
                endPoint: .patchSubTasks(id),
                body: request
            )
            return response
        } catch {
            throw error
        }
    }
}</code></pre>
<p>리스폰스 data 타입을 빈 구조체인 VoidType으로 설정해줍니다. 베이스 리스폰스에서 data를 T라는 제네릭타입으로 지정해주었습니다. 그래서 이 data의 타입을 빈 구조체로 지정해주는 것입니다!</p>
<pre><code class="language-swift"> do {
            print(&quot;type\(Response.self)&quot;)
            let decoded = try JSONDecoder().decode(BaseResponse&lt;Response&gt;.self, from: data)
            print(&quot;디코딩된 데이터는요: &quot;, decoded)

            if Response.self == VoidType.self {
                let successCodes = [&quot;s2000&quot;, &quot;s2010&quot;, &quot;s2040&quot;]
                guard successCodes.contains(decoded.code) else {
                    throw NetworkError.serverErrorMessage(decoded.message)
                }

                return (VoidType() as? Response) ?? {
                       fatalError(&quot;VoidType을 Response로 변환할 수 없습니다.&quot;)
               }()
            }

            guard let data = decoded.data else {
                throw NetworkError.noData
            }

            let successCodes = [&quot;s2000&quot;, &quot;s2010&quot;, &quot;s2040&quot;]
            guard successCodes.contains(decoded.code) else {
                throw NetworkError.serverErrorMessage(decoded.message)
            }

           return data
} </code></pre>
<p>그리고 리스폰스를 디코딩해주는 함수에서 data가 Void Type인 경우를 미리 분기처리해줍니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/fbe7832a-99d4-441f-ae20-f2a0320cdf88/image.png" alt=""></p>
<p>그러면 성공적으로 디코딩에러가 해결되는 것을 확인할 수 있습니다. </p>
<p><a href="https://github.com/SOPT-all/36-COLLABORATION-iOS-TODOMate/pull/35">관련 PR 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS/UIkit] 주간 캘린더 구현 ]]></title>
            <link>https://velog.io/@y-eonee/iOSUIkit-%EC%A3%BC%EA%B0%84-%EC%BA%98%EB%A6%B0%EB%8D%94-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@y-eonee/iOSUIkit-%EC%A3%BC%EA%B0%84-%EC%BA%98%EB%A6%B0%EB%8D%94-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 19 May 2025 18:22:41 GMT</pubDate>
            <description><![CDATA[<p>이번 합동세미나에서 저희 팀이 맡은 서비스는 투두메이트였습니다.
그 중 저는 캘린더를 가져오게 되었는데요..
캘린더 UI 작업이 얼추 완료되었는데, 정리 겸 회고 겸 아티클 작성겸... 여튼 작성해보려고합니다.</p>
<h1 id="1-uicollectionview로-캘린더-만들기">1. UICollectionView로 캘린더 만들기</h1>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/089f22ed-1f03-444d-9cab-00e01e590762/image.png" alt=""></p>
<p>이렇게 생긴 캘린더를 만들건데요.</p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/4a2b08e9-fa5c-4f35-ad1a-319e93564c45/image.png" alt="">
이렇게 구조를 짰고, 달력 부분은 컬렉션 뷰로 만들었습니다. </p>
<h3 id="☄️-여기서-생긴-트러블-슈팅">☄️ 여기서 생긴 트러블 슈팅..</h3>
<p>didSelectItmeAt 함수가 호출이 아예 안되는 이슈가 있었습니다. 알고보니.. 상단에 있는 자잘한 것들을 WeekBox 뷰로 묶고, 달력을 WeekCalendar 뷰로 묶었는데 그 뷰들에 height 제약을 걸어주지 않아서 생긴 문제였습니다. 높이제약을 잘 걸었는지 항상 확인합시다...</p>
<h1 id="2-calendar-class-만들기">2. Calendar Class 만들기</h1>
<p>달력과 관련된 일들을 처리하는 Calendar Formatter Class를 만들었습니다. 이곳에서 관련된 함수들을 만들어, 날짜를 받아오는 일들은 전부 이 클래스에서 처리했습니다. </p>
<pre><code class="language-swift">private let calendar: Calendar = {
            var calendar = Calendar(identifier: .gregorian)
            calendar.firstWeekday = 2
            return calendar
}()</code></pre>
<p>투두메이트는 월요일부터 시작하기 때문에 firsrtWeekday 속성을 수정했습니다. </p>
<pre><code class="language-swift"> private let calendarFormatter = DateFormatter()
 private let monthFormatter = DateFormatter()
 private let dateFormatter = DateFormatter()
 private var nowCalendarDate = Date()
 private var dates = [String]()

init() {
        calendarFormatter.dateFormat = &quot;yyyy-MM-dd&quot;
        monthFormatter.dateFormat = &quot;yyyy년 MM월&quot;
        monthFormatter.locale = Locale(identifier: &quot;ko_KR&quot;)
        dateFormatter.dateFormat = &quot;d&quot;
        dateFormatter.locale = Locale(identifier: &quot;ko_KR&quot;)
 }</code></pre>
<p>각자 용도에 맞는 formatter를 선언해주었습니다. </p>
<ul>
<li>monthFormatter : 월 텍스트를 받아올 때 사용합니다. </li>
<li>calendarFormatter : 현재 뷰에 보여지는 한 주간의 날짜를 yyyy-mm-dd 형식의     배열로 보여줄 때 사용합니다.</li>
<li>dateFormatter : 오늘 날짜에 관련된 일들을 처리할 때 사용합니다.</li>
</ul>
<h1 id="3-셀에-날짜-뿌려주기">3. 셀에 날짜 뿌려주기</h1>
<p>셀에 날짜를 뿌려줄 때 다음과 같이 작성했습니다. </p>
<pre><code class="language-swift">private func setDate(_ offset: Int) {
        weekDates = formatter.getWeekDateStringsOfWeek(offset)
        selectedIndex = formatter.getTodayDay()
        selectedDate = weekDates[selectedIndex]
        weekCollectionView.reloadData()
}

extension WeekCalendar: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -&gt; UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: WeekCollectionViewCell.identifier, for: indexPath) as? WeekCollectionViewCell else {
            return WeekCollectionViewCell()
        }

        let date = weekDates[indexPath.item]
        let isSelected = selectedIndex == indexPath.item
        cell.dataBind(date: date, isSelected: isSelected, index: indexPath.item)
        return cell
    }
    ...
}</code></pre>
<p>yyyy-mm-dd 형식으로 현재 주를 배열로 리턴해주는 함수 getWeekDateStringsOfWeek를 호출하여 weekDates 배열에 담아주었습니다. </p>
<pre><code class="language-swift">extension WeekCollectionViewCell {
    func dataBind(date: String, isSelected: Bool, index: Int) {
        let slicedDate = Int(date.suffix(2)) ?? 1
        dayLabel.text = &quot;\(slicedDate)&quot;

        if isSelected {
            dayLabel.backgroundColor = .black
            dayLabel.layer.cornerRadius = 10
            dayLabel.textColor = .white
        } else {
           changeDayLabelColor(index)
       }
    }
}
</code></pre>
<p>데이터 바인딩 할때, dd 만 가져올 수 있도록 추출해주었습니다. Int 형식으로 변환한 이유는 01, 02, 03.. 인 날짜일 때 1, 2, 3..으로 표시해주기 위해서입니다. </p>
<h1 id="4-주간-달력-이동하기">4. 주간 달력 이동하기</h1>
<p>위의 왼쪽 화살표 아이콘, 오른쪽 화살표 아이콘을 누르면 전주, 다음주로 달력을 이동해야합니다. </p>
<pre><code class="language-swift">func getWeekDateStringsOfWeek(_ offset: Int) -&gt; [String] {
        guard let targetDate = calendar.date(byAdding: .weekOfYear, value: offset, to: nowCalendarDate),
              let weekInterval = calendar.dateInterval(of: .weekOfYear, for: targetDate) else {
            return []
        }

        var weekDates: [String] = []
        for i in 0..&lt;7 {
            if let date = calendar.date(byAdding: .day, value: i, to: weekInterval.start) {
                weekDates.append(calendarFormatter.string(from: date))
            }
        }
        print(&quot;이번주는요 &quot;, weekDates)
        return weekDates
    }</code></pre>
<p>offset을 주어서, 현재 날짜에서 얼마나 이동해야하는지를 체크하여 그에 따른 날짜를 리턴합니다. </p>
<pre><code class="language-swift">extension WeekCalendar: WeekBoxMoveButtonDelegate {
    func didTapPreMoveButton() {
        print(&quot;전주 보여주세요&quot;)
        offset -= 1
        setDate(offset)
    }

    func didTapRightMoveButton() {
        print(&quot;다음주 보여주세요&quot;)
        offset += 1
        setDate(offset)
    }
}</code></pre>
<p>weekbox 뷰 안의 아이콘을 눌렀을 때, delegate로 처리하여 각각 Offset 값을 다르게 주었습니다. </p>
<p><a href="https://github.com/SOPT-all/36-COLLABORATION-iOS-TODOMate/pull/23">관련 PR</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS/Swift] SwiftLint 설치 및 github Action CI 세팅하기]]></title>
            <link>https://velog.io/@y-eonee/iOSSwift-SwiftLint-%EC%84%A4%EC%B9%98-%EB%B0%8F-github-Action-CI-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@y-eonee/iOSSwift-SwiftLint-%EC%84%A4%EC%B9%98-%EB%B0%8F-github-Action-CI-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 12 May 2025 17:04:02 GMT</pubDate>
            <description><![CDATA[<p>이번 합동세미나에서는 SwiftLint를 사용하기로 했습니다!
SwiftLint란 무엇인지, 또 깃허브 액션과 연결하여 사용하는 방법에 대해 작성하겠습니다.</p>
<p>SwiftLint란 코드 스타일을 분석해주는 정적도구인데요. 
내가 작성한 코드들에서 규칙에 어긋나는 코드를 찾아내서 경고 또는 빌드 에러를 반환해 주기 때문에 일관된 코드 스타일을 유지하고, 코드 품질을 향상시킬 수 있습니다. 협업 시에 일관된 코드 스타일을 유지할 수 있어 편리하다고 합니다. </p>
<h1 id="swiftlint-설치하기">SwiftLint 설치하기</h1>
<p>저는 homebrew를 이용해서 설치했습니다. </p>
<pre><code>homebrew install swiftlint</code></pre><p>homebrew로 설치가 완료되면, xcode의 프로젝트에서 Build Phase에 들어가줍니다.
<img src="https://velog.velcdn.com/images/y-eonee/post/465d37a6-54bf-46f5-8d2e-9ac958481e18/image.png" alt="">
위에 +를 누르고, New Run Script Phase를 클릭해 새로운 스크립트를 만들어줍니다. </p>
<p>그리고 밑에 스크립트에 다음과 같이 작성해줍니다. 스크립트 이름은 SwiftLint Script로 바꿔줬습니다. </p>
<pre><code class="language-shell"># Type a script or drag a script file from your workspace to insert its path.
if [[ &quot;$(uname -m)&quot; == arm64 ]]; then
    export PATH=&quot;/opt/homebrew/bin:$PATH&quot;
fi

if which swiftlint &gt; /dev/null; then
  swiftlint
else
  echo &quot;warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint&quot;
fi
</code></pre>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/2caf1f29-f2c2-4beb-bdf3-1d7636342a02/image.png" alt=""></p>
<h1 id="swiftlintyml-작성하기">SwiftLint.yml 작성하기</h1>
<p>위의 단계들이 끝나면 규칙들을 작성해야합니다. 
프로젝트 루트에 .swiftlint.yml 파일을 만들어줍니다. 
<a href="https://realm.github.io/SwiftLint/rule-directory.html">Swiftlint 규칙은 여기서 확인할 수 있습니다</a></p>
<p>저희 팀의 코드컨벤션에 맞춰 어떤 규칙을 비활성화할지, 어떤 규칙을 추가할지에 대해 작성해주었습니다. </p>
<pre><code class="language-yml">disabled_rules:
    # 비활성화하고 싶은 규칙
    - identifier_name
    - line_length
    - type_name
    - legacy_constructor
    - unused_setter_value
    - void_function_in_ternary
    - nesting
    - comment_spacing
    - cyclomatic_complexity

opt_in_rules:
    - function_parameter_count
    - function_body_length
    - trailing_whitespace
    - empty_count
    - empty_string
    - closure_end_indentation
    - let_var_whitespace

included:
    - TODOMate
excluded:
    - TODOMate/App/AppDelegate.swift
    - TODOMate/App/SceneDelegate.swift
</code></pre>
<p>included에는 프로젝트 파일을 포함시켜주고, excluded에는 AppDelegate와 SceneDelegate를 작성해서 검사에서 제외될수 있게 합니다. </p>
<p>이후 Cmd+B로 빌드시켜보면 규칙이 어긋나는 부분에 경고가 나타나게 됩니다.
<img src="https://velog.velcdn.com/images/y-eonee/post/724cee05-6e9b-4365-9c04-ddc84d607d71/image.png" alt=""></p>
<h1 id="github-action-연결">Github Action 연결</h1>
<p>깃허브 내에 .github/workflows 경로로 .swiftlint.yml 파일을 만들어줍니다. 
<img src="https://velog.velcdn.com/images/y-eonee/post/a38b7f98-a09f-46e4-81f0-e8730568291f/image.png" alt=""></p>
<p>그리고 다음과 같이 파일을 작성합니다. </p>
<pre><code class="language-yml">name: SwiftLint

on:
  pull_request:
    paths:
      - &#39;.github/workflows/swiftlint.yml&#39;
      - &#39;.swiftlint.yml&#39;
      - &#39;**/*.swift&#39;

jobs:
  lint:
    name: Run SwiftLint
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Run SwiftLint using config file
        uses: norio-nomura/action-swiftlint@3.2.1
        with:
          args: --config TODOMate/.swiftlint.yml</code></pre>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/e98bf300-bf26-4f69-a2f4-898559d43062/image.png" alt="">
이렇게 하면 pr을 올렸을 때 swiftlint가 자동으로 코드를 검사하여, 통과하지 못하면 머지를 할 수 없게 됩니다.</p>
<h2 id="☄️-트러블슈팅">☄️ 트러블슈팅</h2>
<p>빌드했을 때는 잘 되는데, pr에 올라가면 검사를 통과하지 못하는 이슈가 발생했습니다.
이때, PR내의 File Change에 들어가면 어디서 통과하지 못했는지 볼 수 있는데요. 이 경우 AppDelegate와 SceneDelegate에서 계속 통과하지 못했습니다. 
이상하죠... yml 파일에서 분명 exclude시켰는데 말이죠..?</p>
<p>저희팀 천재리드에게 sos를 쳤는데 깃허브액션이 swiftlint yml 파일을 못찾는것 같다고 말해줬습니다. 
그래서 열심히 서치를 해보다가 <a href="https://velog.io/@hyesuuou/SwiftLint-Github-Action%EC%9C%BC%EB%A1%9C-%EC%B2%B4%ED%81%AC%ED%95%98%EA%B8%B0">이 아티클</a>을 발견했는데요!</p>
<p>문제는 바로 여기였습니다. </p>
<pre><code class="language-yml">steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Run SwiftLint using config file
        uses: norio-nomura/action-swiftlint@3.2.1
        with:
          args: --config .swiftlint.yml</code></pre>
<p>처음에는 args 부분에 정확한 경로명을 적어두지 않고, .swiftlint.yml만을 적어두었습니다. 
이곳의 경로명을 정확하게 </p>
<pre><code class="language-yml">TODOMate/.swiftlint.yml</code></pre>
<p>라고 작성해주니 성공적으로 CI 세팅을 마칠 수 있었습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Swift 문법] 열거형 (Enum)]]></title>
            <link>https://velog.io/@y-eonee/Swift-%EB%AC%B8%EB%B2%95-%EC%97%B4%EA%B1%B0%ED%98%95-Enum</link>
            <guid>https://velog.io/@y-eonee/Swift-%EB%AC%B8%EB%B2%95-%EC%97%B4%EA%B1%B0%ED%98%95-Enum</guid>
            <pubDate>Sun, 11 May 2025 12:14:50 GMT</pubDate>
            <description><![CDATA[<p>열거형은 연관성이 있는 값들을 모아둔 것입니다. 열거형은 일급객체로서 프로퍼티를 계산 및 추가적인 정보를 처리하는 인스턴스 메소드를 제공한다고 하는데요. 솔직히 무슨말인지 잘 모르겠으니까 처음부터 차근차근 정리해보겠습니다. </p>
<h2 id="0-들어가며">0. 들어가며</h2>
<h3 id="일급객체first-class-object란">일급객체(First-class Object)란?</h3>
<p>특정 조건을 만족하는 객체들을 일급객체라고 하는데요. 그 조건은 다음과 같습니다. </p>
<blockquote>
<ul>
<li>런타임 중 에도 객체가 생성이 가능해야한다.</li>
</ul>
</blockquote>
<ul>
<li>객체를 전달 인자로 전달 가능하다.</li>
<li>객체를 변수나 데이터 구조 안에 담을 수 있다.</li>
<li>객체를 리턴 값으로 사용 가능하다.</li>
</ul>
<p>⇒ 요약 : 변수나 상수처럼 사용할 수 있어야 한다!</p>
<h2 id="1-enum-정의하기">1. enum 정의하기</h2>
<h3 id="1-원시값이-없는-열거형">1) 원시값이 없는 열거형</h3>
<pre><code class="language-swift">enum Sopt {
    case plan
    case server
    case web
    case android
    case ios 
    case design
}</code></pre>
<pre><code class="language-swift">enum Sopt {
    case plan, server, web, android, ios, design
}</code></pre>
<p>위와 같이 열거형의 이름만 쓰고 선언해주면 원시값이 없는 열거형입니다. </p>
<p>실제로 사용할 때에는, 선언한 열거형이 하나의 자료형이 됩니다. </p>
<pre><code class="language-swift">var member1: Sopt = .ios</code></pre>
<p>이렇게 .을 통해 내가 선언한 case 내에서만 접근이 가능합니다. </p>
<h3 id="2-원시값이-있는-열거형">2) 원시값이 있는 열거형</h3>
<p>원시값(Raw Value)이 될 수 있는 자료형은 Number, Character, String 타입 총 3가지가 있습니다. </p>
<p>원시값을 가지고 싶다면, enum 선언 시 꼭 타입을 명시해주어야 하는데요!</p>
<h3 id="🔵-number-type">🔵 Number Type</h3>
<pre><code class="language-swift">enum Member: Int {
    case ob
    case yb
    case leader
}</code></pre>
<p>이렇게 Int를 명시해주게 되면, 가장 먼저 선언된 case부터 0, 1, 2.. 가 차례대로 들어가게 됩니다.</p>
<pre><code class="language-swift">enum Member: Int {
    case ob // 0 
    case yb = 2
    case leader // 3
}</code></pre>
<p>만약에 yb에 원시값을 2라고 지정해주고, leader에는 따로 원시값을 지정하지 않았다면 바로 이전 케이스의 원시값에서 +1된 값으로 세팅됩니다. </p>
<h3 id="✅-주의할점-">✅ 주의할점 !!</h3>
<p>만약에 Double 타입으로 enum을 선언했다고 가정해봅시다.</p>
<pre><code class="language-swift">enum Member: Double {
    case ob = 1.0
    case yb = 2.0
    case leader //error!
}</code></pre>
<p>Double이나 Float에서는 모든 case에 원시값을 선언해주지 않으면 에러가 나게 되는데요.</p>
<p>바로 이전 케이스 값에 +1을 더하기 때문에, 실수값에 정수를 더할 수 없기 때문에 에러가 나게 됩니다.</p>
<p>그래서 이 Double이나 Float으로 enum 타입을 지정했을 때, 원시값을 지정해주고 싶다면 모든 case에 원시값을 선언해주어야 합니다. 하지만! 굳이 생략을 하고 싶다면 이전 case의 값을 정수 지정해주면 에러 없이 사용할 수 있습니다. </p>
<h3 id="🔵-character-type">🔵 Character Type</h3>
<pre><code class="language-swift">enum Sopt: Character {
    case plan = &quot;p&quot;
    case server = &quot;s&quot;
    case web = &quot;w&quot;
    case android = &quot;a&quot;
    case ios = &quot;i&quot;
    case design = &quot;d&quot;
}</code></pre>
<p>Character로 enum의 타입을 지정해줄 경우 반드시 모든 case에 대해 원시값을 지정해주어야합니다. </p>
<p>아스키코드처럼 +1을 더하면 되지 않나? 라는 생각은 안됩니다.. (아까 Float와 비슷한 이유로 이전값이 정수가 아니기 때문에 계산이 불가능합니다)</p>
<h3 id="🔵-string-type">🔵 String Type</h3>
<p>String type은 원시값을 지정하지 않으면 case 이름과 동일한 원시값이 자동으로 지정됩니다. </p>
<pre><code class="language-swift">enum Sopt: String {
    case plan                //plan
    case server              //server
    case web                 //web
    case android             //android
    case ios = &quot;요아정&quot;
    case design              //design
}</code></pre>
<h3 id="✅-원시값-접근하기">✅ 원시값 접근하기</h3>
<p>이렇게 만들어진 원시값은 <code>rawValue</code> 라는 속성으로 접근해주면 됩니다! </p>
<pre><code class="language-swift">var 나연: Sopt = .ios
var 효은: Sopt = .server

print(나연.rawValue)
print(효은.rawValue)</code></pre>
<p>만약 원시값이 존재하는 열거형이라면, <code>rawValue</code>속성을 이용해 생성자 바로 변수를 만들 수 있는데요 </p>
<pre><code class="language-swift">var 나연 = Sopt(rawValue: &quot;ios&quot;)
var 효은 = Sopt(rawValue: &quot;sserver&quot;)</code></pre>
<p>이렇게 없는 값을 rawValue에 넣어주게 되면 효은 이라는 변수는 nil을 리턴하게 됩니다. </p>
<p>예제를 통해서 알아보겠습니다!</p>
<h3 id="💬-예제">💬 예제</h3>
<pre><code class="language-swift">enum Sopt: String{
    case plan
    case server
    case web
    case ios = &quot;요아정&quot;
    case andriod
}
</code></pre>
<p>이렇게 작성된 enum을 만들어주었습니다. ios에만 “요아정”이라는 원시값을 주었어요.</p>
<pre><code class="language-swift">var nayeon: Sopt = .ios
print(nayeon)
print(nayeon.rawValue)
print(type(of: nayeon))</code></pre>
<p>실행결과를 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/3debc92e-aa9a-4bd2-af3e-5127ad4b217e/image.png" alt=""></p>
<p>요렇게 실행결과가 나오게 되는데요. nayeon이라는 변수에는 .ios라는 enum을 주었고, 원시값으로는 “요아정”을 지정했습니다. 타입을 확인해보면 Sopt라는 enum 타입이 나오게 됩니다. </p>
<pre><code class="language-swift">var hyoeun = Sopt(rawValue: &quot;sserver&quot;)
print(hyoeun)
print(type(of: hyoeun))</code></pre>
<p>“sserver”라는 없는 원시값으로 새로운 Sopt 타입의 변수를 선언해보겟습니다. </p>
<p>실행결과를 확인해보면
<img src="https://velog.velcdn.com/images/y-eonee/post/3b72c19f-6a60-498c-9566-3ca324206575/image.png" alt=""></p>
<p>hyoeun이라는 변수에는 아무것도 들어가있지 않습니다. sserver라는 원시값이 enum 내에 존재하지 않기 때문에 nil이 들어가게 되었습니다. 타입을 확인해보니 Sopt Optional 타입이네요. 없는 원시값을 넣어 변수를 생성하게 되면 리턴되는 enum은 옵셔널임을 알게 되었습니다. </p>
<h2 id="2-연관값-associated-values">2. 연관값 (<strong>Associated Values)</strong></h2>
<h3 id="1-배경">1) 배경</h3>
<p>솝트의 멤버에 관련된 Enum을 만들어서, yb와 ob case를 만들어서 원시값을 지정해주려고 합니다.</p>
<pre><code class="language-swift">enum SoptMember: String {
    case yb = &quot;ios, 23&quot;
    case ob = &quot;server, 24, 35&quot;
}</code></pre>
<p>하지만 모든 YB가 ios파트에 23살은 아닐거고, 모든 OB가 서버파트에, 최근활동기수가 35기에, 24살은 아닐 것입니다. 이렇게 원시값을 사용하게 될 경우, 모든 case가 동일한 원시값을 가져야하고, case별로 지정된 하나의 값만 가질 수 있습니다. 이럴 때 바로 연관값을 사용하게 됩니다. </p>
<h3 id="2-연관값을-가지는-enum-선언하기">2) 연관값을 가지는 Enum 선언하기</h3>
<pre><code class="language-swift">enum SoptMember {
    case yb(part: String, age: Int)
    case ob(part: String, age: Int, recent: Int)
}</code></pre>
<p>연관값을 선언할 때에는 case 옆에 <code>튜플</code> 형태로 원하는 타입을 명시하면 됩니다. </p>
<h3 id="3-연관값을-가지는-enum-생성하기">3) 연관값을 가지는 Enum 생성하기</h3>
<p>enum을 사용할 때에는 연관값을 함께 전달하여 생성하면 됩니다. </p>
<pre><code class="language-swift">let nayeon: SoptMember = .yb(part: &quot;ios&quot;, age: 23)
let hyoeun: SoptMember = .ob(part: &quot;server&quot;, age: 24, recent: 35)</code></pre>
<h3 id="4-switch-매칭">4) switch 매칭</h3>
<p>예제를 통해서 switch-case로 매칭하는 경우를 보겠습니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/3384a960-e870-41fa-a345-2748082f59e4/image.png" alt=""></p>
<p>선언했던 연관값 중 하나만으로도 조건을 걸 수 있습니다. </p>
<p><code>.yb(let part, let age)</code> 이렇게 case를 거는 경우, 연관값을 꺼내서 사용할 수 있게 됩니다. </p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/3f21b72e-e384-491d-b9e9-44e07dac127d/image.png" alt=""></p>
<p>이렇게 연관값 관련하여 아무 조건도 걸지 않으면 연관값을 무시하고 case값만 맞으면 바로 실행되게 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Swift 문법] 타입 선언]]></title>
            <link>https://velog.io/@y-eonee/Swift-%EB%AC%B8%EB%B2%95-%ED%83%80%EC%9E%85-%EC%84%A0%EC%96%B8</link>
            <guid>https://velog.io/@y-eonee/Swift-%EB%AC%B8%EB%B2%95-%ED%83%80%EC%9E%85-%EC%84%A0%EC%96%B8</guid>
            <pubDate>Tue, 06 May 2025 12:07:51 GMT</pubDate>
            <description><![CDATA[<h2 id="✅-타입선언">✅ 타입선언</h2>
<p>스위프트에서는 let, var로 상수&amp;변수를 선언하게 되는데요. </p>
<pre><code class="language-swift">let sopt
var ios</code></pre>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/be319ab7-1326-4319-8013-adfe7400e4a6/image.png" alt=""></p>
<p>이렇게만 작성하면 Type Annotation missing이라는 에러가 나게 됩니다. 에러를 해결하기 위해서는 변수의 타입을 명시적으로 지정해주어야 합니다. 컴파일러가 변수, 상수의 자료형이 무엇인지 알게 하기 위해서 타입을 명시해주어야합니다. </p>
<p>타입을 명시해주는 방법에는 Type Annotation, Type Inference로 2가지 방법이 있습니다. </p>
<h3 id="1-type-inference-타입추론">1) Type Inference (타입추론)</h3>
<p>타입 추론이란, 선언과 동시에 값을 초기화하는 것입니다. </p>
<pre><code class="language-swift">let name = &quot;nayeon&quot;</code></pre>
<p>컴파일러는 초기화된 “nayeon”이라는 값을 보고 타입을 String이라고 추론하여, name의 타입을 String으로 지정해주는 것입니다. </p>
<p>타입추론에 문제점이 있다면, 원하는 타입으로 추론되지 않을 가능성이 존재한다는 것입니다. 예를 들어, </p>
<pre><code class="language-swift">let num = 13.0</code></pre>
<p>나는 float 변수를 원했는데, 컴파일러는 이 변수를 double로 추론하여 지정해버리는 문제가 발생하게 됩니다. </p>
<p>그외에도 초기값이 없는 경우에도 문제가 발생하는데, 이 경우에 Type Annotation을 사용합니다. </p>
<h3 id="2-type-annotation-타입-주석">2) Type Annotation (타입 주석)</h3>
<p>타입 주석이란, 변수 선언 시 직접 자료형을 지정해주는 방법입니다. </p>
<pre><code class="language-swift">let 변수명: 자료형
var 변수명: 자료형</code></pre>
<p>위의 형식처럼  콜론 뒤에 자료형을 명시합니다. </p>
<p>타입 추론과 다르게, 타입을 유추할 필요가 없기 때문에 초기값이 없어도 됩니다. 또한, 컴파일러가 타입을 추론하는 시간이 더 짧다는 장점이 있습니다. </p>
<h2 id="✅-type-alias">✅ Type Alias</h2>
<p>Alias는 <code>별칭</code>이라는 뜻을 가지고 있습니다. 말 그대로 Type Alias는 타입에 붙일 수 있는 별칭, 약칭입니다. </p>
<p>주의할 점은, Type Alias는 새로운 타입을 만드는 것이 아닌, 기존 타입에 별명을 붙여 새로운 이름으로 부를 수 있게 한것입니다. 이렇게 사용하는 이유는 코드를 좀 더 간결하고, 가독성있게 작성하기 위해서입니다.</p>
<blockquote>
<p>typealias 별명 = 기존타입</p>
</blockquote>
<p>TypeAlias를 사용하는 방법에는 다양한 유형이 있습니다. </p>
<h3 id="1-기본-타입에-사용">1) 기본 타입에 사용</h3>
<pre><code class="language-swift">typealias Name = String
let name: Name = &quot;nayeon&quot;</code></pre>
<h3 id="2-사용자-정의-타입에-사용">2) 사용자 정의 타입에 사용</h3>
<pre><code class="language-swift">class Student { ... }
typealias Students = Array&lt;Student&gt; 
var arrStudents: Students = [] //Array&lt;Student&gt; 대신 사용</code></pre>
<h3 id="3-클로저에-사용">3) 클로저에 사용</h3>
<pre><code class="language-swift">func saveData(success: ((Int) -&gt; Int)?,
              failure: ((Error) -&gt; Void)?,
              progress: ((Double) -&gt; Void)?) {
}</code></pre>
<p>이런 복잡한 형식의 클로저를</p>
<pre><code class="language-swift">typealias Success = (Int) -&gt; Int
typealias Failure = (Error) -&gt; Void
typealias Progress = (Double) -&gt; Void

func saveData(success: Success?,
              failure: Failure?,
              progress: Progress?) {
    // code ...
}</code></pre>
<p>typealias 선언을 통해 간단하게 바꿀 수 있습니다. </p>
<h3 id="4-프로토콜에-사용">4) 프로토콜에 사용</h3>
<p>여러개의 프로토콜을 채택하는 경우, 프로토콜을 결합한 typealias를 선언하여 간결하게 코드를 작성할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS/Uikit] UICollectionView 정리하기]]></title>
            <link>https://velog.io/@y-eonee/iOSUikit-UICollectionView-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@y-eonee/iOSUikit-UICollectionView-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 06 May 2025 12:01:08 GMT</pubDate>
            <description><![CDATA[<h2 id="0-들어가며">0. 들어가며</h2>
<p>공식문서에서는 UICollectionView를 </p>
<blockquote>
<p>An object that manages an ordered collection of data items and presents them using customizable layouts.</p>
</blockquote>
<p>라고 정의하고 있습니다. 정렬된 data item을 관리하고, 커스터미이징 가능한 레이아웃을 이용해서 아이템들을 화면에 출력하게 한다고 하는데요.
세로 스크롤만 가능한 UITableView와 다르게 UICollevctionView는 가로스크롤도 지원합니다. </p>
<p>이번 글에서는 티빙 홈화면 예제를 통해 UICollectionView의 사용에 대해 정리해보겠습니다.</p>
<p><img src="https://github.com/user-attachments/assets/1ca6cbc2-c84c-4a99-84ce-8ad14d381584" alt="Simulator Screen Recording - iPhone 16 Pro - 2025-05-03 at 09 44 18"></p>
<h2 id="1-컬렉션-뷰-객체-생성하기">1. 컬렉션 뷰 객체 생성하기</h2>
<p>먼저 컬렉션 뷰 객체를 만들어줍니다. </p>
<pre><code class="language-swift">private let todayRankingCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())</code></pre>
<p>뒤에 뭐가 많이 들어가죠..? 이게 무엇이냐면</p>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/439c2333-87b8-4f75-be53-3c882bc6aec5/image.png" alt=""></p>
<p>UICollectionView를 초기화할 때 필요한 것들인데요.
UITableView와 다르게 UICollectionView를 초기화할 때에는 반드시 layout이 필요합니다. UICollectionViewLayout은 컬렉션 뷰 내의 콘텐츠들이 어떻게 배치될지를 관리합니다.</p>
<p>그래서 UICollectionViewLayout도 지정해주려고 합니다. layout으로는 애플에서 기본적으로 제공하는 flowLayout을 사용하였습니다. </p>
<pre><code class="language-swift"> private func todayRankingSetFlowLayout(){
        let flowLayout = UICollectionViewFlowLayout()

        let screenWidth = UIScreen.main.bounds.width
        let cellHeight: CGFloat = 146
        let visibleItems: CGFloat = 2.3
        let spacing: CGFloat = 12
        let totalSpacing = spacing * (visibleItems - 1)
        let cellWidth = (screenWidth - totalSpacing) / visibleItems

        flowLayout.itemSize = CGSize(width: cellWidth, height: cellHeight)
        flowLayout.scrollDirection = .horizontal
        flowLayout.collectionView?.isScrollEnabled = true
        flowLayout.minimumLineSpacing = spacing
        self.todayRankingCollectionView.setCollectionViewLayout(flowLayout, animated: false)
    }</code></pre>
<ul>
<li><strong>scrollDirection</strong> : 스크롤의 방향입니다.  가로방향으로 스크롤할 것이기 때문에 horizontal로 설정했습니다. </li>
<li><strong>minimumLineSpacing</strong> : 셀 사이의 간격을 spacing(12)로 설정했습니다. </li>
<li>cellWidth를 계산하기 위해 (전체 스크린 width - 총 spacing) / 화면에 보일 아이템 개수 계산식을 사용했습니다. </li>
</ul>
<h2 id="2-컬렉션-뷰-셀-만들기">2. 컬렉션 뷰 셀 만들기</h2>
<h3 id="1-컬렉션-뷰-cell-파일-생성">1) 컬렉션 뷰 cell 파일 생성</h3>
<img src="https://velog.velcdn.com/images/y-eonee/post/66008761-3a5b-427c-96a9-209755532be1/image.png" width="400"/>
cell을 만들 때에는, New File From Template을 통해 파일을 생성해줍니다. 

<img src="https://velog.velcdn.com/images/y-eonee/post/348cac40-479a-4393-ac65-b017cea268d1/image.png" width="400"/>
Cocoa Touch Class를 선택 후 
<img src="https://velog.velcdn.com/images/y-eonee/post/7ea834cf-fd22-4be7-b2d5-a4b67e6bfb3e/image.png" width="400"/>

<p>subclass에서 UICollectionViewCell을 선택 후 만들어줍니다. </p>
<h3 id="2-식별자-생성하기">2) 식별자 생성하기</h3>
<pre><code class="language-swift">static let identifier: String = &quot;RankingCollectionViewCell&quot;</code></pre>
<p>컬렉션뷰가 사용되는 뷰컨트롤러에 이 cell파일을 등록하기 위해서, cell 파일 내에 식별자를 만들어줍니다. </p>
<h3 id="3-cell-item의-autolayout-잡기">3) cell item의 AutoLayout 잡기</h3>
<img src = "https://velog.velcdn.com/images/y-eonee/post/d8325852-e2f6-4de3-a6c6-56d3c5efff81/image.png" width = "200" />
cell의 layout을 잡아줍니다. snapkit을 이용해서 잡아주었습니다. 

<h3 id="4-data-bind-함수-만들기">4) data bind 함수 만들기</h3>
<pre><code class="language-swift">extension RankingCollectionViewCell {
    func dataBind(_ itemData: MovieRankingModel){
        rankingNumber.text = String(itemData.ranking)
        movieImage.image = itemData.movieImg
    }
}</code></pre>
<p>데이터 바인딩 함수를 만들어줍니다. 여기서는 더미데이터를 사용했기 때문에 Model 파일에서 만든 데이터를 cell 내의 UIComponent와 바인딩했습니다. </p>
<h2 id="3-cell과-view-controller-연결하기">3. cell과 view controller 연결하기</h2>
<p>다시 컬렉션뷰를 만든 뷰컨으로 돌아와서, 아까 만든 collection view cell과 뷰컨을 연결해주겠습니다. </p>
<h3 id="1-register-함수-작성">1) register 함수 작성</h3>
<p>아까 cell 파일에서 식별자를 만들어주었습니다. 이 식별자를 이용해 어떤 collection view에 어떤 cell을 사용할지 등록하는 과정을 거쳐야 합니다. </p>
<pre><code class="language-swift"> private func registerTodayRankingCell(){
        todayRankingCollectionView.register(RankingCollectionViewCell.self, forCellWithReuseIdentifier: RankingCollectionViewCell.identifier)
    }</code></pre>
<h3 id="2-delegate-datasource-채택하기">2) delegate, dataSource 채택하기</h3>
<pre><code class="language-swift">todayRankingCollectionView.delegate = self
todayRankingCollectionView.dataSource = self</code></pre>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/3098b8ad-6186-4c4e-933d-15526d847db3/image.png" alt="">
UICollectionViewDelegate는 컬렉션 뷰 내에서 아이템과 사용자간 인터랙션을 관리하는 프로토콜입니다. 스크롤되었을 때 등의 작업을 이 프로토콜에서 합니다. 
<img src="https://velog.velcdn.com/images/y-eonee/post/862325e1-43b0-4e43-800c-88f5b248af6a/image.png" alt="">
UICollectionViewDataSource는 컬렉션 뷰 내에서 데이터와 셀을 관리하는 프로토콜입니다. 총 보여지는 셀은 몇개인지, 셀이 선택되었을지 등의 작업을 이 프로토콜이 합니다. 
우리는 셀을 원하는대로 동작시키기 위해 이 프로토콜들을 채택하여 이곳에 역할을 위임해주려고 합니다. </p>
<h3 id="3-delegate-datasource-함수-구현하기">3) delegate, dataSource 함수 구현하기</h3>
<p>dataSource프로토콜에는 필수적으로 구현해야하는 메소드가 존재합니다. cellForItemAt, numberOfItemInSection 이라는 메소드인데요.</p>
<pre><code class="language-swift">extension HomeTableViewCell : UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -&gt; Int {
        todayRankingCollectionView :
            return movieRankingList.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -&gt; UICollectionViewCell {
         guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RankingCollectionViewCell.identifier, for: indexPath) as? RankingCollectionViewCell else {
                return RankingCollectionViewCell()
        }

       cell.dataBind(movieRankingList[indexPath.item])
       return cell
}</code></pre>
<ul>
<li><p>** numberOfItemsInSection** : 이 섹션 (컬렉션 뷰)에서 아이템을 총 몇개 표시할 지에 대한 메소드입니다. 여기서는 dummy로 가져온 데이터의 count만큼을 리턴하였습니다.</p>
</li>
<li><p><strong>cellForItemAt</strong> : 화면에 셀이 보여야할 때 이 함수가 호출됩니다. </p>
<ul>
<li><p>이때, dequeueReusableCell로, 재사용 큐에서 셀을 꺼내고 셀이 존재한다면 </p>
</li>
<li><p>indexPath로 아이템을 구분해서 데이터바인딩을 해주고 셀을 리턴해줍니다. </p>
<img src="https://velog.velcdn.com/images/y-eonee/post/f702e648-72d5-4c32-8202-ccbb05a51c62/image.png" width = "400" /></li>
<li><p>메모리 낭비를 줄이기 위해 재사용 큐를 사용합니다. 스크린 밖으로 셀이 나간다면 재사용큐에 다시 셀이 들어가고, 화면에 보이게 된다면 다시 재사용큐에서 셀이 나와 화면에 보이게 됩니다. </p>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[졸업작품 회고 -3] sse로 알림 전송받기 ]]></title>
            <link>https://velog.io/@y-eonee/%EC%A1%B8%EC%97%85%EC%9E%91%ED%92%88-%ED%9A%8C%EA%B3%A0-3-sse%EB%A1%9C-%EC%95%8C%EB%A6%BC-%EC%A0%84%EC%86%A1%EB%B0%9B%EA%B8%B0</link>
            <guid>https://velog.io/@y-eonee/%EC%A1%B8%EC%97%85%EC%9E%91%ED%92%88-%ED%9A%8C%EA%B3%A0-3-sse%EB%A1%9C-%EC%95%8C%EB%A6%BC-%EC%A0%84%EC%86%A1%EB%B0%9B%EA%B8%B0</guid>
            <pubDate>Tue, 01 Apr 2025 12:14:19 GMT</pubDate>
            <description><![CDATA[<p>이번 프로젝트에서는 알림 시스템을 사용하기 위해 sse를 사용하였다. </p>
<h3 id="오류의-배경">오류의 배경</h3>
<p><img src="https://velog.velcdn.com/images/y-eonee/post/37a792ee-ed9e-4fff-889d-266695817ba1/image.png" alt=""></p>
<p>메인페이지가 있고, 메일컴포넌트가 있었다. 
메인페이지에서 sse 구독을 하고, 메일컴포넌트에서 sse 알림을 리스너하도록 했는데, 연결 후 핑이 잘 오는 것은 확인되지만 실질적인 데이터는 오지 않았다. </p>
<p>이것의 문제점은 바로 
내 방식처럼 따로 다른 컴포넌트에 코드를 작성하게 되면 리스너가 등록되기 전에 데이터가 전송되어 클라이언트에서 확인이 불가능하다는 것이었다..!!!</p>
<h3 id="해결">해결</h3>
<p>그래서 메인 페이지에서 sse 구독 및 리스너까지 하고, 메일컴포넌트에는 props로 데이터를 넘겨주는 것으로 수정했다. </p>
<pre><code class="language-javascript">useEffect(() =&gt; {
        if (!userId) return;

        const source = new EventSourcePolyfill(`${SERVER_URL}/sse/subscribe/${userId}`, {
          withCredentials: true,
        }); //eventsourcepolyfill에서는 withCredential을 따로 설정해서 보내주어야한다!

        source.onopen = () =&gt; {
          console.log(&quot;✅ SSE 연결 성공!&quot;);
        };

        source.addEventListener(&quot;init&quot;, (event) =&gt; {
            console.log(&quot;🟢 연결 메시지 (init):&quot;, event.data); // &quot;connected&quot;
        });

        source.addEventListener(&quot;onetime_event&quot;, (event) =&gt; {
            console.log(&quot;📨 수신된 데이터:&quot;, event.data);
            if (!event.data) {
                console.log(&quot;⚠️ onetime_event에 data가 없습니다:&quot;, event);
                return;
            }
            const newAlarms = JSON.parse(event.data);

            setAlarmList((prev) =&gt; [...new Set([...prev, ...newAlarms])]);
            setOneTimeAlarmList((prev) =&gt; [...new Set([...prev, ...newAlarms])]); 
            setHasReceivedOneTimeEvent(true);
            console.log(oneTimeAlarmList, hasReceivedOneTimeEvent, postNextChapterCalled);
        });

        source.addEventListener(&quot;regular_event&quot;, (event) =&gt; {
            console.log(&quot;📨 수신된 데이터:&quot;, event.data);
            if (!event.data) {
                console.log(&quot;⚠️ regular_event에 data가 없습니다:&quot;, event);
                return;
            }
            setAlarmList((prev) =&gt; [...new Set([...prev, ...JSON.parse(event.data)])]);


        });

        return () =&gt; source.close();
    }, [userId]);</code></pre>
<img src="https://velog.velcdn.com/images/y-eonee/post/7dbd4672-93b7-42ef-a4af-a2b2c800d2d1/image.png" width="400"/>

<p>데이터가 잘 오는것이 확인되었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[졸업작품 회고 -2] withCredential 403 Forbidden 해결]]></title>
            <link>https://velog.io/@y-eonee/%EC%A1%B8%EC%97%85%EC%9E%91%ED%92%88-%ED%9A%8C%EA%B3%A0-2-withCredential-403-Forbidden-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@y-eonee/%EC%A1%B8%EC%97%85%EC%9E%91%ED%92%88-%ED%9A%8C%EA%B3%A0-2-withCredential-403-Forbidden-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 01 Apr 2025 11:57:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/y-eonee/post/eb3ee5da-cfe8-472c-9097-d3b056d03fbb/image.png" alt="">
로컬 상태에서 모든 api를 호출할 때마다 403 Forbidden이 뜨는 에러가 발생했다. 
백엔드에서는 http only 쿠키를 사용한다고 해서 클라이언트에서는 withCredential을 설정해서 보내주어야 했다. </p>
<p><a href="https://securityinit.tistory.com/248">https://securityinit.tistory.com/248</a>
클라이언트에서는 위 글을 참고하여 apiClient.jsx를 작성했고, 모든 api에 withCredential을 붙여서 보낼 수 있도록 설정했다. + 401, 400에러를 캐치하여 액세스 토큰 재발급 및 리프레시토큰 만료시 로그아웃되도록 연결했다. </p>
<h3 id="해결방법-로컬">해결방법 (로컬)</h3>
<pre><code class="language-javascript">export const apiClient = axios.create({
    baseURL: &quot;/api&quot;,
    withCredentials: true  // ✅ 모든 요청에 쿠키 포함
});</code></pre>
<p>baseURL에 /api가 붙어서 갈 수 있도록 설정했다.
api를 호출할 때는, </p>
<pre><code class="language-javascript">const response = await apiClient get (&quot;/auth/refresh&quot;);</code></pre>
<p>apiClient를 불러와서 뒤에 엔드포인트만 작성했다. </p>
<p>프록시 설정도 추가했다. </p>
<pre><code class="language-javascript">import { defineConfig } from &#39;vite&#39;
import react from &#39;@vitejs/plugin-react&#39;

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      &#39;/api&#39;: {
        target: &#39;https://nsmaker.o-r.kr&#39;, // 백엔드 서버 주소 
        changeOrigin: true,
        rewrite: path =&gt; path.replace(/^\/api/, &#39;&#39;),
        ws: true,
        configure: (proxy) =&gt; {
          proxy.on(&#39;proxyReq&#39;, (proxyReq, req) =&gt; {
            if (req.url?.includes(&#39;/sse/subscribe&#39;)) {
              proxyReq.setHeader(&#39;Accept&#39;, &#39;text/event-stream&#39;);
              proxyReq.setHeader(&#39;Cache-Control&#39;, &#39;no-cache&#39;);
              proxyReq.setHeader(&#39;Connection&#39;, &#39;keep-alive&#39;);
            }
          });
        }
      }
    }
  }
})</code></pre>
<p>그리고 혹시 몰라 크롬 브라우저의 Third-party cookie를 허용했다. 
이렇게 하면 개발자도구-쿠키에서 액세스토큰, 리프레시토큰까지 잘 오는 것을 확인할 수 있었다.</p>
<h3 id="해결방법-배포">해결방법 (배포)</h3>
<pre><code class="language-javascript">export const apiClient = axios.create({
    baseURL: import.meta.env.VITE_SERVER_URL,
    withCredentials: true  // ✅ 모든 요청에 쿠키 포함
});
</code></pre>
<p>배포시에는 baseURL만 수정하였다. </p>
<p>이렇게 했는데도 안되면 백엔드에서 문제가 일어난 것....
삼일을 골머리앓던 에러였는데 백엔드쪽에서 무엇인가 문제 해결 + 클라이언트 세팅으로 해결했다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[졸업작품 회고 -1] React cookie 사용하여 아이디 저장하기]]></title>
            <link>https://velog.io/@y-eonee/%EC%A1%B8%EC%97%85%EC%9E%91%ED%92%88-%ED%9A%8C%EA%B3%A0-1-React-cookie-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%95%84%EC%9D%B4%EB%94%94-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@y-eonee/%EC%A1%B8%EC%97%85%EC%9E%91%ED%92%88-%ED%9A%8C%EA%B3%A0-1-React-cookie-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%95%84%EC%9D%B4%EB%94%94-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 17 Mar 2025 16:27:27 GMT</pubDate>
            <description><![CDATA[<p>로그인 시 아이디가 저장되는 기능을 구현하기 위해 React-cookie를 사용하였다. </p>
<p>1) react cookie를 install한다. </p>
<pre><code>npm install react-cookie
</code></pre><p>2) 코드 </p>
<pre><code class="language-javascript">const [saveID, setSaveID] = useState(false); //아이디 저장 여부 
const [cookies, setCookie, removeCookie] = useCookies([&quot;rememberEmail&quot;]); //remeberEmail 이름으로 쿠키 저장

useEffect(() =&gt; {
        /*저장된 쿠키값이 있으면 check 이모티콘 TRUE 및 email에 값 세팅*/
        if (cookies.rememberEmail !== undefined) {
            setEmail(cookies.rememberEmail); //이메일 값 저장 
            setSaveID(true); //check 이모티콘 true
        }
    }, []);

const handleSaveID =(newSaveID)=&gt;{
    if (newSaveID) {
        setCookie(&quot;rememberEmail&quot;, email, { path: &quot;/&quot;, expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) }); //7일 유지
       } else {
        removeCookie(&quot;rememberEmail&quot;); 
 }
</code></pre>
<pre><code class="language-javascript">//css
&lt;CheckImoji
    onClick={() =&gt; {
          setSaveID((prev) =&gt; { // useState는 비동기적으로 동작 =&gt; 다음 렌더링에서 새로운 값이 적용
          const newSaveID = !prev;
          handleSaveID(newSaveID); //새로운 값을 계산함 
          return newSaveID;
      });
  }} /&gt;</code></pre>
<p>3) App.js를 &lt; CookieProvider &gt;로 감싸주어야한다. </p>
<h3 id="결과">결과</h3>
<p>한번 로그인 성공 후 다시 로그인페이지에 가보면 이메일 필드와 아이디저장 체크표시가 잘 세팅되어있는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/y-eonee/post/2eac71b8-cc7a-43d9-8102-5f7fa336d53a/image.png" alt="">
개발자도구 &gt; Application &gt; Cookie에서 쿠키를 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/y-eonee/post/5631f103-4db4-4a77-9091-a77fa6615bfb/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] 이벤트 핸들러 함수의 () 유무 차이 ]]></title>
            <link>https://velog.io/@y-eonee/React-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%95%B8%EB%93%A4%EB%9F%AC-%ED%95%A8%EC%88%98%EC%9D%98-%EC%9C%A0%EB%AC%B4-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@y-eonee/React-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%95%B8%EB%93%A4%EB%9F%AC-%ED%95%A8%EC%88%98%EC%9D%98-%EC%9C%A0%EB%AC%B4-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Mon, 10 Feb 2025 15:46:53 GMT</pubDate>
            <description><![CDATA[<p>onClick 이벤트를 작성할 때 </p>
<pre><code> const GoResetPW =()=&gt;{
        navigate(&quot;/profile/changePW&quot;);
 }
 &lt;span onClick={GoResetPW()}&gt;비밀번호 변경&lt;/span&gt;</code></pre><p>이렇게 onclick 이벤트 때 호출할 함수 뒤에 ()를 붙이게 되면 페이지가 새로고침되자마자 경로가 이동하게 된다. </p>
<pre><code>&lt;span onClick={GoResetPW}&gt;비밀번호 변경&lt;/span&gt;</code></pre><p>하지만 이런 식으로 함수 뒤에 ()를 삭제하고 호출하게 되면 정상적으로 onclick 이벤트가 작동한다. </p>
<p>그 이유는 ()를 붙인 코드는 즉시 함수가 실행되기 때문이다. 컴포넌트가 렌더링되면서 즉시 함수가 실행되기 때문에 페이지가 새로고침되자마자 바로 경로가 이동하게 된다. 
onclick 이벤트를 정상적으로 작동하기 위해서는 ()를 빼고 함수 참조만 넘겨주어야 한다.</p>
]]></description>
        </item>
    </channel>
</rss>