<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ojl_0325.log</title>
        <link>https://velog.io/</link>
        <description>while(true){ 가족(); 건강(); 자기개발(); }</description>
        <lastBuildDate>Mon, 17 Mar 2025 07:48:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ojl_0325.log</title>
            <url>https://velog.velcdn.com/images/ojl_0325/profile/654f70cc-a31c-4203-b2bb-bbd0546f68f1/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ojl_0325.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ojl_0325" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Network Link Conditioner 설치 및 사용 방법]]></title>
            <link>https://velog.io/@ojl_0325/Network-Link-Conditioner-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@ojl_0325/Network-Link-Conditioner-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 17 Mar 2025 07:48:49 GMT</pubDate>
            <description><![CDATA[<p>설치하기:</p>
<p>Xcode를 실행합니다.
Xcode 메뉴에서 Xcode &gt; Open Developer Tool &gt; More Developer Tools... 선택
Apple Developer 웹사이트로 이동하여 로그인합니다.
&quot;Additional Tools for Xcode&quot;를 다운로드합니다.
다운로드한 .dmg 파일을 열고 &quot;Hardware&quot; 폴더 내의 &quot;Network Link Conditioner.prefpane&quot;을 더블클릭하여 설치합니다.</p>
<p>사용하기:</p>
<p>시스템 환경설정(또는 설정) &gt; Network Link Conditioner로 이동합니다.
프로필 선택:</p>
<p>EDGE (느린 모바일 네트워크)
3G (일반적인 모바일 네트워크)
DSL (느린 유선 인터넷)
High Latency DNS (DNS 지연이 있는 환경)
Very Bad Network (매우 나쁜 네트워크 상태)
100% Loss (모든 패킷 손실 - 오프라인 테스트)</p>
<p>커스텀 프로필 생성:</p>
<p>&quot;Manage Profiles...&quot; 버튼을 클릭합니다.
&quot;+&quot; 버튼을 눌러 새 프로필을 생성합니다.
다음 설정을 조정할 수 있습니다:</p>
<p>Uplink/Downlink Bandwidth (업로드/다운로드 대역폭)
Uplink/Downlink Delay (업로드/다운로드 지연 시간)
Packet Loss % (패킷 손실률)
DNS Delay (DNS 지연 시간)</p>
<p>활성화/비활성화:</p>
<p>&quot;On&quot; 체크박스를 체크하여 활성화합니다.
테스트 완료 후 반드시 &quot;Off&quot;로 설정하여 비활성화해야 합니다.</p>
<p>테스트 시나리오 예시</p>
<p>2G 네트워크 테스트:</p>
<p>Downlink: 약 50 Kbps
Uplink: 약 20 Kbps
Delay: 300-500ms</p>
<p>느린 3G 네트워크 테스트:</p>
<p>Downlink: 약 200 Kbps
Uplink: 약 100 Kbps
Delay: 200ms</p>
<p>불안정한 네트워크 테스트:</p>
<p>중간 대역폭 설정
높은 패킷 손실률(5-10%)
간헐적 지연 시간</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[행렬]]></title>
            <link>https://velog.io/@ojl_0325/%ED%96%89%EB%A0%AC</link>
            <guid>https://velog.io/@ojl_0325/%ED%96%89%EB%A0%AC</guid>
            <pubDate>Sat, 15 Feb 2025 08:59:01 GMT</pubDate>
            <description><![CDATA[<h1 id="선형대수학의-기초-스칼라-벡터-행렬의-세계">선형대수학의 기초: 스칼라, 벡터, 행렬의 세계</h1>
<p>선형대수학의 핵심 개념인 스칼라, 벡터, 행렬에 대해 알아보도록 하겠습니다. 이 개념들은 현대 과학과 공학의 근간이 되는 중요한 수학적 도구입니다.</p>
<h2 id="스칼라-scalar">스칼라 (Scalar)</h2>
<p>스칼라는 가장 단순한 수학적 객체로, 크기만을 가진 단일 숫자를 의미합니다. 우리 주변에서 흔히 볼 수 있는 예시를 살펴보도록 하겠습니다.</p>
<ul>
<li>온도: 25°C</li>
<li>질량: 5kg</li>
<li>시간: 3시간</li>
</ul>
<p>이처럼 스칼라는 하나의 숫자로 표현되는 양을 나타냅니다. 스칼라끼리의 기본적인 연산(덧셈, 뺄셈, 곱셈, 나눗셈)이 가능합니다.</p>
<h2 id="벡터-vector">벡터 (Vector)</h2>
<p>벡터는 크기와 방향을 모두 가지고 있는 수학적 객체입니다. 2차원 또는 3차원 공간에서 표현될 수 있습니다.</p>
<p>예를 들어 2차원 벡터 v = (3, 4)는 다음과 같은 정보를 담고 있습니다:</p>
<ul>
<li>x축 방향으로 3만큼</li>
<li>y축 방향으로 4만큼</li>
</ul>
<p>벡터는 다음과 같은 연산이 가능합니다:</p>
<ul>
<li>벡터의 덧셈과 뺄셈</li>
<li>스칼라와 벡터의 곱셈</li>
<li>내적과 외적</li>
</ul>
<h2 id="행렬-matrix">행렬 (Matrix)</h2>
<p>행렬은 숫자들을 직사각형 형태로 배열한 것입니다. m×n 행렬은 m개의 행과 n개의 열을 가집니다.</p>
<p>예를 들어 2×3 행렬은 다음과 같이 표현됩니다:</p>
<pre><code>[1  2  3]
[4  5  6]</code></pre><p>행렬은 다음과 같은 연산이 가능합니다:</p>
<ul>
<li>행렬의 덧셈과 뺄셈</li>
<li>행렬과 스칼라의 곱셈</li>
<li>행렬과 행렬의 곱셈</li>
<li>전치(transpose)</li>
<li>역행렬 계산</li>
</ul>
<h2 id="선형대수학에서의-활용">선형대수학에서의 활용</h2>
<p>이러한 개념들은 다양한 분야에서 활용되고 있습니다:</p>
<ul>
<li>컴퓨터 그래픽스: 3D 객체의 회전과 이동</li>
<li>기계학습: 데이터의 차원 축소와 특징 추출</li>
<li>물리학: 힘과 운동의 표현</li>
<li>경제학: 수요와 공급의 관계 모델링</li>
</ul>
<p>이처럼 선형대수학은 현대 과학기술의 핵심 도구로 자리잡고 있습니다. 기초 개념을 잘 이해하면 더 복잡한 응용분야로 나아갈 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[0. 서비스를 만들어보자 ]]></title>
            <link>https://velog.io/@ojl_0325/0.-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@ojl_0325/0.-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 15 Feb 2025 08:56:19 GMT</pubDate>
            <description><![CDATA[<p>서비스를 만들어볼 생각이다.</p>
<p>서비스를 만들면서 발생한 이슈와 생각의 흐름을 정리하고 기록하기 위한 글이다. </p>
<ul>
<li><p>우선 빠르게 만들어서 시장반응을 보고, 그에 대응하는 개인 프로젝트를 진행해볼 생각이다. </p>
</li>
<li><p>그럼 이 프로젝트를 통해서 얻고자 하는게 무엇인가?</p>
<ol>
<li>실제 운영 배포를 통한 다양한 경험</li>
<li>시각적 통계자료 및 운영중 발생하는 이슈를 통한 전략 구상 및 대응경험</li>
<li>광고 수익</li>
</ol>
</li>
</ul>
<ul>
<li><p>어떤 비즈니스 모델을 만들것인가?</p>
<p>-&gt; 최근 사람들이 금융에 부쩍 관심이 많아졌다.
-&gt; 그럼 이번 프로젝트에서는 처음 유입되는 흔히 말하는 주린이(?)들을 위한 앱을 만들어보자
-&gt; 너무 플랫하게 만들면 경쟁 서비스가 많고, 차별점이 필요하다.
-&gt; LLM을 통한 인공지능 부가서비스를 넣어서 차별화를 주면 좋을거 같은데</p>
</li>
<li><p>그럼 어떤 플랫폼을 선택할 것인가?
  -&gt; 앱, 웹 크게 2가지로 볼 수 있을거같은데</p>
<ul>
<li>앱의 장점</li>
</ul>
<ol>
<li>서버의 부하가 작을거같다라는 생각 ( 검증필요 )</li>
<li>Android iOS둘다 능수능란하게 가능하다.</li>
<li>웹뷰를 사용한 ChatGPT Crawling을 통해 잘만하면 인공지능을 무료로 사용할 수도 있을거같다.</li>
<li>애드센스를 달기 쉬울거같다. ( 검증필요 )</li>
</ol>
<ul>
<li>웹의 장점</li>
</ul>
<ol>
<li>Android iOS Widnows Mac 구애를 받지 않아 접근성이 매우 뛰어나다.</li>
</ol>
</li>
</ul>
<ul>
<li>일단 iOS - SwiftUI를 통해 빠르게 앱을 만들어보자.
  -&gt; 왜냐하면 Android는 최근 구글스토어에 등록시 20명 테스트가 필요하다는 꽤 까다로운 조건이 붙었다.
 -&gt; iOS를 선택한 이유중 하나는 개인적으로 appleDeveloper 관련해서 지불하지 않아도 되는 13만원을 지불해서이다 ( ㅋㅋ )
 -&gt; 그러면 차라리 그냥 웹뷰기반으로 앱을 만들어볼까, 그렇기엔 너무 UI/UX적으로 짜증나는 부분이 많을거같은데
 -&gt; 일단 iOS + 서버 + DB 이렇게 구현을 해보자</li>
</ul>
<ul>
<li>그럼 어떤 기능이 필요할까
  -&gt; 뉴스를 크롤링해오자, 그리고 출처를 표시해주고, 주린이들 수준에 맞는 뉴스로 재조립해서 원본뉴스와, 인공지능이 대답하는 글을보여주는것도 재밌겠다.</li>
</ul>
<ul>
<li><p>위 기능을 먼저 우선 만들어서 배포해보자 그럼 뭐가 필요할까</p>
<ul>
<li>iOS<blockquote>
<ul>
<li>앱 아이콘</li>
</ul>
</blockquote>
<ul>
<li>앱 스플래시 화면</li>
<li>액티비티 흐름도</li>
<li>앱 화면 구성도</li>
</ul>
</li>
<li>서버<blockquote>
<ul>
<li>뉴스 크롤링 서버</li>
</ul>
</blockquote>
<ul>
<li>뉴스 저장 DB</li>
<li>게시글 DB</li>
<li>API 서버</li>
<li>뉴스 가공 서버</li>
<li>통계 서버</li>
</ul>
</li>
</ul>
</li>
<li><p>추가적으로 고려해볼만한게 뭐가 있을까 우선순위를 정하지말고 일단 나열해보자
  -&gt; 회원가입을 통한 유저 확보
  -&gt; 구독 모델
  -&gt; CI/CD
  -&gt; 보안
  -&gt; 크롤링에 관한 법적검토
  -&gt; SNS 기능? -&gt; 채팅, 게시글올리기
  -&gt; 환율 및 각종 주가에대한 차트
  ...</p>
</li>
<li><p>아니다 너무 많이 생각하지말고 빠르게 강력한 기능 하나를 출시해보고 상황을 지켜보자</p>
</li>
<li><p>앱 초기 광고는 어떻게 해야할까, 사실 이건 랜덤인거같기도 하다. 여러 전략이 있겠지만 일단 앱을 완성도 있게 만들고, 노이즈마케팅을하던, 돈을 지불하고 광고를 맡기던 해봐야겠다.</p>
</li>
<li><p>바로 개발에 들어가보자</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[GoogleSTT 305Error ]]></title>
            <link>https://velog.io/@ojl_0325/GoogleSTT-305Error</link>
            <guid>https://velog.io/@ojl_0325/GoogleSTT-305Error</guid>
            <pubDate>Thu, 13 Feb 2025 04:09:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/06e9db5e-56fc-44b6-887a-e22beff9229f/image.png" alt=""></p>
<p>iOS,Android에서 GoogleSTT를 사용할 일이 생겼다.</p>
<p>문제는 스트림에대한 제한시간이 305초가 한계였다.</p>
<blockquote>
<p>Google 문서의 이 페이지에서 말씀하신 제한 시간이 5분인 것을 확인할 수 있습니다. <a href="https://cloud.google.com/speech-to-text/quotas?hl=en">https://cloud.google.com/speech-to-text/quotas?hl=en</a></p>
</blockquote>
<p>그래서 내가 우선적으로 생각한 해결 방안이다.
처음 iOS에서 구현할때 싱글톤패턴으로 구현하여
앱 델리게이트에서 2개이상의 AudioController를 부여받게될경우 에러가났다. 
따라서 해당 싱글톤패턴을 제거하고 새롭게 코드를 짜고, 아래와 같은 시나리오로 에러를 해결했다.</p>
<pre><code>1. 첫 번째 음성 인식기(speechRecognizer) 동작:
    - 처음 4분 30초 동안 음성을 인식하고 수집합니다
2. 전환 구간 설정:
    - 4분 30초가 되면 두 번째 음성 인식기(speechRecognizer2)가 활성화됩니다
    - 이때 10초간의 중요한 전환 기간이 있습니다 (4분 30초 ~ 4분 40초)
    - 이떄는 speechRecognizer과 speechRecognizer2가 동시에 음성을 수집합니다.
3. 전환 시나리오:
a) 4분 30초 ~ 4분 39초 사이에 번역이 발생하는 경우:

b) 4분 40초 이후에 번역이 발생하는 경우:
    - 첫 번째 음성 인식기(speechRecognizer)의 데이터를 사용하여 번역을 진행합니다
    - speechRecognizer의 데이터 + 추가 10초간 수집된 데이터를 speechRecognizer2로 전달합니다
    - 첫 번째 speechRecognizer는 종료됩니다
    - speechRecognizer2가 이어서 음성 인식을 담당합니다
4. 반복 과정:
    - speechRecognizer2도 동일한 과정을 반복합니다
    - 즉, 4분 30초 사용 후 다시 새로운 인식기로 전환하는 과정을 반복합니다</code></pre><p>그리고 더 찾아본결과 
아래는 구글 자체에서 해결한 방식이다.
연결을 브릿징스왑을 통해 해결한 방식이다.</p>
<blockquote>
<p><a href="https://cloud.google.com/speech-to-text/docs/transcribe-streaming-audio?hl=en#endless-streaming">https://cloud.google.com/speech-to-text/docs/transcribe-streaming-audio?hl=en#endless-streaming</a></p>
</blockquote>
<blockquote>
<p><a href="https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/speech/src/main/java/com/example/speech/InfiniteStreamRecognize.java">https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/speech/src/main/java/com/example/speech/InfiniteStreamRecognize.java</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로퍼티(Property)]]></title>
            <link>https://velog.io/@ojl_0325/%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EB%9E%98%ED%8D%BCProperty-Wrapper</link>
            <guid>https://velog.io/@ojl_0325/%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EB%9E%98%ED%8D%BCProperty-Wrapper</guid>
            <pubDate>Mon, 10 Feb 2025 01:01:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/60158529-351b-4154-8bb9-aae3e33ef728/image.png" alt=""></p>
<h1 id="swift-프로퍼티와-프로퍼티-래퍼-완벽-가이드">Swift 프로퍼티와 프로퍼티 래퍼 완벽 가이드</h1>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#%EA%B8%B0%EB%B3%B8-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0">기본 프로퍼티</a></li>
<li><a href="#swiftui-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EB%9E%98%ED%8D%BC">SwiftUI 프로퍼티 래퍼</a></li>
<li><a href="#%EC%8B%A4%EC%A0%84-%ED%99%9C%EC%9A%A9-%EC%98%88%EC%A0%9C">실전 활용 예제</a></li>
<li><a href="#%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EC%84%A0%ED%83%9D-%EA%B0%80%EC%9D%B4%EB%93%9C">프로퍼티 선택 가이드</a></li>
</ol>
<h2 id="기본-프로퍼티">기본 프로퍼티</h2>
<h3 id="1-저장-프로퍼티-stored-properties">1. 저장 프로퍼티 (Stored Properties)</h3>
<p>가장 기본적인 형태의 프로퍼티로, 값을 직접 저장하는 인스턴스 프로퍼티입니다.</p>
<pre><code class="language-swift">class Person {
    var name: String        // 변수 저장 프로퍼티
    let birthDate: Date     // 상수 저장 프로퍼티

    init(name: String, birthDate: Date) {
        self.name = name
        self.birthDate = birthDate
    }
}</code></pre>
<h3 id="2-연산-프로퍼티-computed-properties">2. 연산 프로퍼티 (Computed Properties)</h3>
<p>값을 저장하지 않고 계산된 값을 반환하는 프로퍼티입니다.</p>
<pre><code class="language-swift">struct Circle {
    var radius: Double

    var area: Double {
        get {
            return .pi * radius * radius
        }
        set {
            radius = sqrt(newValue / .pi)
        }
    }
}</code></pre>
<h3 id="3-타입-프로퍼티-type-properties">3. 타입 프로퍼티 (Type Properties)</h3>
<p>인스턴스가 아닌 타입 자체에 속하는 프로퍼티입니다.</p>
<pre><code class="language-swift">struct Math {
    static let pi = 3.141592653589793
    static var computedProperty: Int {
        return 42
    }
}</code></pre>
<h3 id="4-지연-저장-프로퍼티-lazy-stored-properties">4. 지연 저장 프로퍼티 (Lazy Stored Properties)</h3>
<p>필요한 시점에 초기화되는 프로퍼티입니다.</p>
<pre><code class="language-swift">class ExpensiveDataManager {
    lazy var data: [String] = {
        // 복잡한 초기화 로직
        return [&quot;Heavy&quot;, &quot;Data&quot;, &quot;Loading&quot;]
    }()
}</code></pre>
<h3 id="5-프로퍼티-옵저버-property-observers">5. 프로퍼티 옵저버 (Property Observers)</h3>
<p>프로퍼티 값의 변화를 관찰하고 반응하는 기능을 제공합니다.</p>
<pre><code class="language-swift">class StepCounter {
    var steps: Int = 0 {
        willSet {
            print(&quot;Will set steps to \(newValue)&quot;)
        }
        didSet {
            print(&quot;Steps changed from \(oldValue) to \(steps)&quot;)
        }
    }
}</code></pre>
<h2 id="swiftui-프로퍼티-래퍼">SwiftUI 프로퍼티 래퍼</h2>
<h3 id="1-state">1. @State</h3>
<p>View의 로컬 상태를 관리하는 프로퍼티 래퍼입니다.</p>
<pre><code class="language-swift">struct ContentView: View {
    @State private var isPlaying = false

    var body: some View {
        Button(action: { isPlaying.toggle() }) {
            Text(isPlaying ? &quot;Pause&quot; : &quot;Play&quot;)
        }
    }
}</code></pre>
<h3 id="2-binding">2. @Binding</h3>
<p>다른 View의 상태와 양방향 바인딩을 제공합니다.</p>
<pre><code class="language-swift">struct ChildView: View {
    @Binding var text: String

    var body: some View {
        TextField(&quot;Enter text&quot;, text: $text)
    }
}

struct ParentView: View {
    @State private var text = &quot;&quot;

    var body: some View {
        ChildView(text: $text)
    }
}</code></pre>
<h3 id="3-observable-ios-17">3. @Observable (iOS 17+)</h3>
<p>클래스의 상태 변화를 감지하고 UI를 업데이트합니다.</p>
<pre><code class="language-swift">@Observable class UserProfile {
    var name = &quot;Kim&quot;
    var age = 25

    func updateAge() {
        age += 1
    }
}</code></pre>
<h3 id="4-environment">4. @Environment</h3>
<p>시스템 환경 값에 접근합니다.</p>
<pre><code class="language-swift">struct ThemedText: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        Text(&quot;Current theme: \(colorScheme == .dark ? &quot;Dark&quot; : &quot;Light&quot;)&quot;)
            .foregroundColor(colorScheme == .dark ? .white : .black)
    }
}</code></pre>
<h3 id="5-appstorage">5. @AppStorage</h3>
<p>UserDefaults와 연동되어 데이터를 영구 저장합니다.</p>
<pre><code class="language-swift">struct SettingsView: View {
    @AppStorage(&quot;isDarkMode&quot;) private var isDarkMode = false

    var body: some View {
        Toggle(&quot;Dark Mode&quot;, isOn: $isDarkMode)
    }
}</code></pre>
<h3 id="6-scenestorage">6. @SceneStorage</h3>
<p>현재 scene의 상태를 저장합니다.</p>
<pre><code class="language-swift">struct DocumentView: View {
    @SceneStorage(&quot;selectedTab&quot;) private var selectedTab = 0

    var body: some View {
        TabView(selection: $selectedTab) {
            Text(&quot;Tab 1&quot;).tag(0)
            Text(&quot;Tab 2&quot;).tag(1)
        }
    }
}</code></pre>
<h3 id="7-fetchrequest">7. @FetchRequest</h3>
<p>Core Data 쿼리 결과를 관리합니다.</p>
<pre><code class="language-swift">struct TodoListView: View {
    @FetchRequest(
        sortDescriptors: [SortDescriptor(\Todo.date, order: .reverse)],
        animation: .default)
    private var todos: FetchedResults&lt;Todo&gt;

    var body: some View {
        List(todos) { todo in
            Text(todo.title ?? &quot;Untitled&quot;)
        }
    }
}</code></pre>
<h3 id="8-stateobject">8. @StateObject</h3>
<p>ObservableObject의 수명주기를 관리합니다.</p>
<pre><code class="language-swift">class DataModel: ObservableObject {
    @Published var value = 0
}

struct DataView: View {
    @StateObject private var model = DataModel()

    var body: some View {
        Text(&quot;Value: \(model.value)&quot;)
    }
}</code></pre>
<h2 id="실전-활용-예제">실전 활용 예제</h2>
<h3 id="사용자-프로필-관리-예제">사용자 프로필 관리 예제</h3>
<pre><code class="language-swift">@Observable class UserProfile {
    var name: String
    var email: String
    var preferences: Preferences

    struct Preferences {
        var isDarkMode: Bool
        var notificationsEnabled: Bool
    }

    init(name: String, email: String, preferences: Preferences) {
        self.name = name
        self.email = email
        self.preferences = preferences
    }
}

struct ProfileView: View {
    @State private var profile: UserProfile
    @AppStorage(&quot;lastLoginDate&quot;) private var lastLoginDate: Date?

    var body: some View {
        Form {
            Section(&quot;Personal Info&quot;) {
                TextField(&quot;Name&quot;, text: $profile.name)
                TextField(&quot;Email&quot;, text: $profile.email)
            }

            Section(&quot;Preferences&quot;) {
                Toggle(&quot;Dark Mode&quot;, isOn: $profile.preferences.isDarkMode)
                Toggle(&quot;Notifications&quot;, isOn: $profile.preferences.notificationsEnabled)
            }

            if let date = lastLoginDate {
                Section(&quot;Last Login&quot;) {
                    Text(date, style: .date)
                }
            }
        }
    }
}</code></pre>
<h2 id="프로퍼티-선택-가이드">프로퍼티 선택 가이드</h2>
<h3 id="언제-어떤-프로퍼티를-사용해야-할까요">언제 어떤 프로퍼티를 사용해야 할까요?</h3>
<ol>
<li><p><strong>@State 사용 시기</strong></p>
<ul>
<li>View 내부에서만 사용되는 간단한 데이터</li>
<li>해당 View의 생명주기와 함께하는 데이터</li>
<li>예: 토글 상태, 텍스트 필드 입력값</li>
</ul>
</li>
<li><p><strong>@Binding 사용 시기</strong></p>
<ul>
<li>부모 View의 상태를 자식 View에서 수정해야 할 때</li>
<li>양방향 데이터 바인딩이 필요할 때</li>
<li>예: 커스텀 컴포넌트의 상태 관리</li>
</ul>
</li>
<li><p><strong>@Observable 사용 시기</strong></p>
<ul>
<li>복잡한 데이터 모델이 필요할 때</li>
<li>여러 View에서 공유되는 데이터</li>
<li>예: 사용자 프로필, 앱 설정</li>
</ul>
</li>
<li><p><strong>@AppStorage 사용 시기</strong></p>
<ul>
<li>앱 재시작 후에도 유지되어야 하는 데이터</li>
<li>간단한 사용자 설정</li>
<li>예: 테마 설정, 사용자 기본 설정</li>
</ul>
</li>
<li><p><strong>@Environment 사용 시기</strong></p>
<ul>
<li>시스템 설정에 접근해야 할 때</li>
<li>전역적으로 공유되는 환경 값이 필요할 때</li>
<li>예: 다크 모드 감지, 디바이스 방향</li>
</ul>
</li>
</ol>
<h3 id="주의사항과-베스트-프랙티스">주의사항과 베스트 프랙티스</h3>
<ol>
<li><p><strong>성능 최적화</strong></p>
<ul>
<li>@State는 가능한 private으로 선언</li>
<li>불필요한 View 업데이트 방지를 위해 적절한 범위 설정</li>
<li>큰 데이터는 참조 타입으로 관리</li>
</ul>
</li>
<li><p><strong>메모리 관리</strong></p>
<ul>
<li>@StateObject는 View의 생명주기 동안 한 번만 생성</li>
<li>lazy var를 활용하여 필요할 때만 초기화</li>
<li>큰 데이터는 필요한 시점에 로드</li>
</ul>
</li>
<li><p><strong>코드 구조화</strong></p>
<ul>
<li>관련 프로퍼티들을 논리적으로 그룹화</li>
<li>명확한 네이밍 컨벤션 사용</li>
<li>문서화 주석 활용</li>
</ul>
</li>
</ol>
<h2 id="결론">결론</h2>
<p>Swift의 프로퍼티 시스템은 강력하고 유연합니다. 기본 프로퍼티부터 SwiftUI의 다양한 프로퍼티 래퍼까지, 각각의 용도에 맞게 적절히 선택하여 사용하면 효율적인 앱 개발이 가능합니다. 특히 SwiftUI에서는 프로퍼티 래퍼를 통해 상태 관리를 더욱 쉽고 직관적으로 할 수 있습니다. </p>
<p>이 가이드를 통해 다양한 프로퍼티의 특성과 활용법을 이해하고, 실제 프로젝트에서 적절히 활용하시기 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[순환참조]]></title>
            <link>https://velog.io/@ojl_0325/%EC%88%9C%ED%99%98%EC%B0%B8%EC%A1%B0</link>
            <guid>https://velog.io/@ojl_0325/%EC%88%9C%ED%99%98%EC%B0%B8%EC%A1%B0</guid>
            <pubDate>Fri, 24 Jan 2025 06:28:13 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/00aae273-0a40-4aef-976c-b705a7afda41/image.png" alt=""></p>
<p>Swift에서 순환참조(Retain Cycle)는 두 객체가 서로를 강하게 참조하여 메모리에서 해제되지 않는 현상을 말한다.</p>
<p>가장 흔한 예시는 클래스 간의 관계에서 발생하는데, 다음과 같은 상황이다.</p>
<pre><code>class Person {
    let name: String
    var apartment: Apartment?

    init(name: String) {
        self.name = name
    }

    deinit {
        print(&quot;\(name) is being deinitialized&quot;)
    }
}

class Apartment {
    let number: String
    var tenant: Person?

    init(number: String) {
        self.number = number
    }

    deinit {
        print(&quot;Apartment \(number) is being deinitialized&quot;)
    }
}</code></pre><p>이렇게 코드를 작성하고 다음과 같이 사용하면 순환참조가 발생한다.</p>
<pre><code>var john: Person? = Person(name: &quot;John&quot;)
var unit4A: Apartment? = Apartment(number: &quot;4A&quot;)

john?.apartment = unit4A
unit4A?.tenant = john

john = nil
unit4A = nil</code></pre><p>위 코드에서 john과 unit4A를 nil로 설정해도 deinit이 호출되지 않는다. 
왜냐하면 각 객체가 서로를 강하게 참조하고 있기 때문이다.</p>
<p>이를 해결하기 위해서는 다음과 같은 방법들을 사용할 수 있다.</p>
<blockquote>
<p>weak 참조: 약한 참조를 사용하면 참조 카운트를 증가시키지 않는다.</p>
</blockquote>
<pre><code>class Apartment {
    let number: String
    weak var tenant: Person?  // weak 키워드 사용
    ...
}</code></pre><blockquote>
<p>unowned 참조: 참조하는 인스턴스가 항상 존재한다고 확신할 때 사용한다.</p>
</blockquote>
<pre><code>class Customer {
    let name: String
    var card: CreditCard?
    ...
}

class CreditCard {
    let number: String
    unowned let customer: Customer  // unowned 키워드 사용
    ...
}</code></pre><blockquote>
<p>클로저에서는 캡처 리스트를 사용해서 약한 참조를 만들 수 있다.</p>
</blockquote>
<pre><code>class ViewController {
    var handler: (() -&gt; Void)?

    func setupHandler() {
        handler = { [weak self] in
            self?.doSomething()
        }
    }
}</code></pre><p>이러한 방법들을 사용하면 순환참조를 피하고 메모리 누수를 방지할 수 있어. 
특히 델리게이트 패턴이나 클로저를 사용할 때는 항상 순환참조 가능성을 고려해야 해야한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발자의 일상을 편하게 만드는 개발 도구]]></title>
            <link>https://velog.io/@ojl_0325/%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%9D%BC%EC%83%81%EC%9D%84-%ED%8E%B8%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%9C%EB%8A%94-%EA%B0%9C%EB%B0%9C-%EB%8F%84%EA%B5%AC</link>
            <guid>https://velog.io/@ojl_0325/%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%9D%BC%EC%83%81%EC%9D%84-%ED%8E%B8%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%9C%EB%8A%94-%EA%B0%9C%EB%B0%9C-%EB%8F%84%EA%B5%AC</guid>
            <pubDate>Thu, 23 Jan 2025 03:48:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/34e2d047-d5a2-4139-b3bb-bdfad2307038/image.png" alt=""></p>
<p>매일 반복되는 개발 작업을 더 효율적으로 만들기 위해 우리 팀이 사용하는 도구들을 정리해봤다.</p>
<blockquote>
<p>CI/CD 환경</p>
</blockquote>
<p>GitHub Actions로 다음과 같은 자동화 파이프라인을 구축했다:</p>
<p>PR 생성 시 자동 테스트 실행
main 브랜치 머지 시 스테이징 자동 배포
태그 생성 시 운영 환경 자동 배포</p>
<p>코드 품질 관리 도구
SonarQube를 사용해서 다음을 체크한다:</p>
<p>코드 커버리지 측정
코드 스멜 검사
복잡도 분석</p>
<blockquote>
<p>IDE와 플러그인</p>
</blockquote>
<p>VS Code를 메인 IDE로 사용하고 있다. 필수 플러그인은 다음과 같다:</p>
<p>GitLens: 깃 히스토리와 변경사항을 쉽게 확인한다
ESLint: 코드 스타일을 일관되게 유지한다
Prettier: 코드 포맷팅을 자동화한다
Docker: 컨테이너 관리를 IDE에서 직접 한다</p>
<blockquote>
<p>터미널 환경</p>
</blockquote>
<p>iTerm2: macOS용 터미널 에뮬레이터다
Oh My Zsh: 터미널을 더 편리하게 사용할 수 있다
tmux: 터미널 세션을 관리한다</p>
<blockquote>
<p>디버깅 도구</p>
</blockquote>
<p>Chrome DevTools: 프론트엔드 디버깅에 사용한다
Postman: API 테스트를 쉽게 할 수 있다
Docker Desktop: 컨테이너 상태를 모니터링한다</p>
<blockquote>
<p>문서화 도구</p>
</blockquote>
<p>Notion: 팀 위키와 문서를 관리한다
Swagger: API 문서를 자동으로 생성한다
Mermaid: 다이어그램을 코드로 그린다</p>
<blockquote>
<p>도입 효과</p>
</blockquote>
<p>이러한 도구들을 도입한 후의 변화다:</p>
<p>개발 시간이 20% 정도 단축됐다
코드 품질이 일관되게 유지된다
팀원 간 협업이 더 수월해졌다</p>
<blockquote>
<p>다음 도입 예정 도구</p>
</blockquote>
<p>Datadog: 통합 모니터링 도구
k9s: 쿠버네티스 CLI 관리 도구
GitHub Copilot: AI 코드 어시스턴트</p>
<p>도구는 그저 도구일 뿐이지만, 적절한 도구를 잘 활용하면 개발자의 삶이 한결 편해진다. 
새로운 도구들을 계속 탐구하고 적용해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swift에서 GPT API 요청 보내기]]></title>
            <link>https://velog.io/@ojl_0325/Swift%EC%97%90%EC%84%9C-GPT-API-%EC%9A%94%EC%B2%AD-%EB%B3%B4%EB%82%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ojl_0325/Swift%EC%97%90%EC%84%9C-GPT-API-%EC%9A%94%EC%B2%AD-%EB%B3%B4%EB%82%B4%EA%B8%B0</guid>
            <pubDate>Wed, 22 Jan 2025 08:25:45 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/eca2f0d3-6238-4104-8bcb-33f651a77070/image.png" alt=""></p>
<p>Swift에서  API요청을 보낼 일이 생겼다.</p>
<p>따라서 그에 관한 정리된 코드를 작성한다.</p>
<blockquote>
<p>Request,Response 모델을 정의 &amp; 에러 핸들링값을 enum으로 정의한다.</p>
</blockquote>
<pre><code>
import Foundation

enum HTTP_ERROR_RESPONSE: Identifiable {
    case NetworkError, TimeOut, JsonDecodeErrorm, BASE64DecodeError, InvalidURL, ETC, NoneData
    var id: Self { self }
}
struct AiModel: Decodable {
    let openai: String
}

struct ChatGPTResponse: Codable {
    let choices: [Choice]
}

struct Choice: Codable {
    let message: GPTMessage
}

struct GPTMessage: Codable {
    let content: String
}

struct Message: Identifiable, Equatable {
    let id = UUID()
    let content: String
    let isUser: Bool

    static func == (lhs: Message, rhs: Message) -&gt; Bool {
        return lhs.content == rhs.content &amp;&amp;
               lhs.isUser == rhs.isUser
    }
}

</code></pre><blockquote>
<p>URL session으로 요청 및 응답을 받는다.</p>
</blockquote>
<pre><code>// URL이 유효한지 확인
        guard let url = URL(string: urlString) else { // OpenAI URL
            //                updateError(&quot;Invalid URL&quot;)
            self.errorMessage = .InvalidURL
            self.isLoading = false
            return
        }
// HTTP 요청 설정
        var request = URLRequest(url: url)
        request.httpMethod = &quot;POST&quot;  // HTTP 메소드를 POST로 설정
        request.addValue(&quot;Bearer \(apiKey)&quot;, forHTTPHeaderField: &quot;Authorization&quot;)  // 인증 헤더 추가 및 API키 추가
        request.addValue(&quot;application/json&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)  // Content-Type 헤더 설정
        request.timeoutInterval = 10  // 타임아웃 시간을 10초로 설정

        // 모델 이름 설정
        var modelName = &quot;&quot;
        if gptmodel {
            modelName = &quot;gpt-4o-2024-11-20&quot;  // GPT-4 모델 사용
        } else {
            modelName = &quot;gpt-4o-mini&quot;  // GPT-4 미니 모델 사용
        }
        // API 요청 본문 구성
        let requestBody: [String: Any] = [
            &quot;model&quot;: modelName,
            &quot;messages&quot;: [
                [&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: systemPrompt],  // 시스템 프롬프트
                [&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: userPrompt]       // 사용자 프롬프트
            ]
        ]

        // 타임아웃 작업 아이템 선언
        var timeoutWorkItem: DispatchWorkItem?

        do {
            // 요청 본문을 JSON 데이터로 변환
            let jsonData = try JSONSerialization.data(withJSONObject: requestBody)
            request.httpBody = jsonData

            // API 요청 태스크 생성
            let task = Task {
                do {
                    // API 요청 실행
                    let (data, response) = try await URLSession.shared.data(for: request)

                    // 타임아웃 작업 취소
                    timeoutWorkItem?.cancel()

                    // HTTP 응답 확인
                    guard let httpResponse = response as? HTTPURLResponse else {
                        timeoutWorkItem?.cancel()
                        DispatchQueue.main.async { [weak self] in
                            self?.isLoading = false
                            self?.errorMessage = .NetworkError
                        }
                        return
                    }

                    // 응답 상태 코드가 200(성공)인 경우
                    if httpResponse.statusCode == 200 {
                        // JSON 응답 디코딩 및 처리
                        if let jsonResult = try? JSONDecoder().decode(ChatGPTResponse.self, from: data),
                           let responseContent = jsonResult.choices.first?.message.content {
                            DispatchQueue.main.async { [weak self] in
                                let botMessage = Message(content: responseContent, isUser: false)
                                self?.isLoading = false
                                self?.receimessages = botMessage.content
                            }
                        } else {
                            // JSON 디코딩 실패 처리
                            timeoutWorkItem?.cancel()
                            DispatchQueue.main.async { [weak self] in
                                self?.isLoading = false
                                self?.errorMessage = .JsonDecodeErrorm
                            }
                        }
                    } else {
                        // HTTP 오류 응답 처리
                        timeoutWorkItem?.cancel()
                        DispatchQueue.main.async { [weak self] in
                            self?.isLoading = false
                            self?.errorMessage = .NetworkError
                        }
                    }
                } catch URLError.timedOut {
                    // 타임아웃 오류 처리
                    timeoutWorkItem?.cancel()
                    DispatchQueue.main.async { [weak self] in
                        self?.isLoading = false
                        self?.errorMessage = .TimeOut
                    }
                } catch {
                    // 기타 오류 처리
                    timeoutWorkItem?.cancel()
                    DispatchQueue.main.async { [weak self] in
                        self?.isLoading = false
                        self?.errorMessage = .ETC
                    }
                }
            }

            // 타임아웃 처리를 위한 DispatchWorkItem 설정
            timeoutWorkItem = DispatchWorkItem { [weak self] in
                task.cancel()  // 태스크 취소
                DispatchQueue.main.async {
                    self?.isLoading = false
                    self?.errorMessage = .TimeOut
                }
            }

            // 10초 후에 타임아웃 작업 실행
            if let timeoutWorkItem = timeoutWorkItem {
                DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: timeoutWorkItem)
            }

        } catch {
            // JSON 직렬화 오류 처리
            timeoutWorkItem?.cancel()
            DispatchQueue.main.async { [weak self] in
                self?.isLoading = false
                self?.errorMessage = .JsonDecodeErrorm
            }
        }</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[DispatchWorkItem]]></title>
            <link>https://velog.io/@ojl_0325/DispatchWorkItem</link>
            <guid>https://velog.io/@ojl_0325/DispatchWorkItem</guid>
            <pubDate>Tue, 21 Jan 2025 11:02:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/a24fe6d9-a14f-4fd6-9f54-ffc525b8a25c/image.png" alt=""></p>
<p>DispatchWorkItem은 작업을 처리하기 위한 항목을 나타내는 클래스다.</p>
<blockquote>
<ol>
<li>주요 용도</li>
</ol>
</blockquote>
<p>작업 단위의 캡슐화: 실행할 작업을 객체로 감싸서 관리한다
지연 실행: 특정 시간 후에 실행할 작업을 예약한다
작업 취소: 예약된 작업을 필요할 때 취소할 수 있다</p>
<blockquote>
<ol start="2">
<li>일반적인 구성요소</li>
</ol>
</blockquote>
<pre><code>public class DispatchWorkItem {
    private Runnable task;        // 실행할 작업
    private long delay;           // 지연 시간
    private boolean isCancelled;  // 취소 상태

    // 작업 실행 메서드
    public void execute() {
        if (!isCancelled) {
            task.run();
        }
    }

    // 작업 취소 메서드
    public void cancel() {
        isCancelled = true;
    }
}</code></pre><blockquote>
<ol start="3">
<li>활용 예시</li>
</ol>
</blockquote>
<pre><code>// 작업 생성
DispatchWorkItem workItem = new DispatchWorkItem(() -&gt; {
    System.out.println(&quot;작업 실행&quot;);
}, 1000); // 1초 후 실행

// 작업 예약
scheduler.schedule(workItem);

// 필요시 작업 취소
workItem.cancel();</code></pre><blockquote>
<ol start="4">
<li>장점</li>
</ol>
</blockquote>
<ul>
<li>작업의 모듈화: 각 작업을 독립적으로 관리할 수 있다</li>
<li>유연한 실행 제어: 실행 시점과 취소를 쉽게 제어할 수 있다</li>
<li>코드 재사용성: 동일한 작업을 여러 곳에서 재사용할 수 있다</li>
</ul>
<p>이러한 DispatchWorkItem은 주로 백그라운드 작업 처리, 타이머 기반 작업, 비동기 프로그래밍 등에서 유용하게 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebRTC 동작과정 [P2P]]]></title>
            <link>https://velog.io/@ojl_0325/WebRTC-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95-P2P</link>
            <guid>https://velog.io/@ojl_0325/WebRTC-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95-P2P</guid>
            <pubDate>Mon, 20 Jan 2025 03:02:14 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/40795477-c9fb-4002-aafc-0d3bb4182844/image.png" alt=""></p>
<blockquote>
<p>시그널링</p>
</blockquote>
<p>두 피어가 서로 연결하려면 먼저 정보를 교환해야 한다
SDP라는 프로토콜로 서로 지원하는 미디어 형식이나 네트워크 정보를 주고받는다
보통 웹소켓 같은 시그널링 서버를 통해 이 정보를 교환한다</p>
<blockquote>
<p>NAT 통과하기</p>
</blockquote>
<p>STUN 서버로 각자의 공인 IP 주소를 확인한다
직접 연결이 안 되면 TURN 서버가 중계한다
ICE라는 프로세스로 가장 좋은 연결 경로를 찾아낸다</p>
<blockquote>
<p>연결 과정은 이렇게 된다</p>
</blockquote>
<p>클라이언트 A가 Offer SDP를 보내고
클라이언트 B가 Answer SDP로 응답하고
서로 ICE Candidate 정보를 주고받아서
최종적으로 P2P 연결이 만들어진다</p>
<blockquote>
<p>데이터 주고받기</p>
</blockquote>
<p>영상/음성은 MediaStream API로 전송한다
다른 데이터는 RTCDataChannel로 직접 보낸다
UDP로 실시간 통신이 가능하다</p>
<p>이렇게 연결되면 서버 없이도 브라우저끼리 직접 데이터를 주고받을 수 있다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI View Modifier 정리]]></title>
            <link>https://velog.io/@ojl_0325/SwiftUI-View-Modifier-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ojl_0325/SwiftUI-View-Modifier-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 18 Jan 2025 08:00:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/8eedd3b4-3eef-4926-af02-69a5c4f50558/image.png" alt=""></p>
<p>View Modifier가 너무 많아질 경우 
코드를 어떻게 정리할까 조금 고민이 있었는데 그에 관한 정리다.</p>
<p>아래는 몇가지 방법론이다.</p>
<blockquote>
<p>Extension으로 묶기</p>
</blockquote>
<pre><code>extension View {
    func commonCardStyle() -&gt; some View {
        self
            .padding()
            .background(Color.white)
            .cornerRadius(10)
            .shadow(radius: 5)
    }
}

// 사용
struct ContentView: View {
    var body: some View {
        Text(&quot;Hello&quot;)
            .commonCardStyle()
    }
}</code></pre><blockquote>
<p>ViewBuilder로 컴포넌트화</p>
</blockquote>
<pre><code>struct CardView&lt;Content: View&gt;: View {
    let content: Content

    init(@ViewBuilder content: () -&gt; Content) {
        self.content = content()
    }

    var body: some View {
        content
            .padding()
            .background(Color.white)
            .cornerRadius(10)
    }
}

// 사용
CardView {
    Text(&quot;Hello&quot;)
}</code></pre><blockquote>
<p>Group으로 modifier 그룹화</p>
</blockquote>
<pre><code>Text(&quot;Hello&quot;)
    .Group {
        if isHighlighted {
            $0.foregroundColor(.red)
                .bold()
        } else {
            $0.foregroundColor(.gray)
        }
    }
    .padding()</code></pre><blockquote>
<p>기능별로 modifier를 별도 메서드로 분리</p>
</blockquote>
<pre><code>struct ContentView: View {
    var body: some View {
        Text(&quot;Hello&quot;)
            .applyTextStyle()
            .applyCardStyle()
    }

    private func applyTextStyle&lt;T: View&gt;(_ view: T) -&gt; some View {
        view
            .font(.title)
            .foregroundColor(.blue)
    }

    private func applyCardStyle&lt;T: View&gt;(_ view: T) -&gt; some View {
        view
            .padding()
            .background(Color.white)
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[서버]]></title>
            <link>https://velog.io/@ojl_0325/%EC%84%9C%EB%B2%84</link>
            <guid>https://velog.io/@ojl_0325/%EC%84%9C%EB%B2%84</guid>
            <pubDate>Fri, 17 Jan 2025 04:01:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/043db8b7-7cf7-482f-99db-cc5f338f1f2f/image.png" alt=""></p>
<p>오늘은 뭔가 서버에 관한 이야기가 하고싶었다.</p>
<p>이제까지 직접 사용해본 서버목록으로 나눠보자면</p>
<ul>
<li>Apache - c10k문제 </li>
<li>NginX - c10k문제 구조적해결이라고해야하나.. 아무튼 해결하려고 노력함. 물론 양이 늘어남에 따라 한계는 있음</li>
<li>litespeed - http3.0지원했던걸로 기억</li>
<li>h2o - 벤치 마크상 http2기준 가장 빠른 웹 서버</li>
<li>NodeJS - 채팅서버, 웹서버로 쓰기 편한듯</li>
<li>FastAPI - Image to Image Image to Video같은거 할때 사용했음</li>
<li>Photon - 유니티 게임 서버로 사용 UDP지원해서 확실히 유니티측 게임서버는 포톤을 쓰는게 좋아보임</li>
<li>Xammpp - 윈도우에서만 사용해봄, 간단한 웹 서버 열기에 좋은듯 GUI가 잘되어있음</li>
<li>Caddy - 벤치 마크용으로 사용해본 서버 장점이 기억이 잘 안나네..</li>
<li>Lighttpd - 저전력서버로 기억,..</li>
<li>직접만든서버
  Java Stream기반 채팅 서버
  Java Stream기반 이미지 전송서버
  Java Stream기반 STT to animation..? </li>
</ul>
<hr>
<p>그 외에는 기억은 안 나지만
친구들이랑 같이 하려고 만든 게임서버다수..? ( 마인크래프트, 좀 보이드, 메이플.. 등등 )</p>
<p>위 생각보다 많은 서버를 썼는데 그중 가장 많이 사용한건 아무래도 Nginx인거같다.
아무래도 간단하고, 강력하고(잘 죽지않는다), 검색도 용이해 이슈도 금방 해결한다.
즉 되게 안정적이고, 무료다. 다만 HTTP2.0까지만 지원한다는게 조금 아쉽다면 아쉬운점?</p>
<p>그외에 다른 웹 서버들도 HTTP3.0이나 퀵 프로토콜을 맛이나 봐보자 라는 생각으로 써봤는데
결론적으론 굳이? 였다. 확실히 2HandShake로 끝난다는건 장점인건 알겠으나, 드라마틱한 차이는 잘 모르겠다.</p>
<p>뭔가 네트워크통신이 중요한 서비스라면 고려해볼법하지만, 일반적인 서비스를 기준으로 했을떄는 HTTP2.0도 아직 훌륭하다고 생각이 든다. 
( 설정도 복잡하고, 에러도 많고, 이슈해결방법도 많이 있진 않다, 뭔가 정형화가 안된느낌이라고해야하나, 지원하는 웹 서버 자체가 드물다보니 그럴 수 있지 않나 생각이 든다. )</p>
<p>아무튼 뭔가 코드를 올려야할거같은데, 오늘따라 귀찮아서 그냥 이렇게 글로 때워본다.</p>
<p>글을 쓰다보니, 한창 공부할때 keepalive, 논블로킹, 이벤트 드리븐,동기&amp;비동기, http역사, 데이터를 보내는 원리 등등 공부했던 기록이 새록새록난다.</p>
<p>결론적으로 이런 서버를 만드는 사람들도 대단하고, 역시 기술이란건 한번에 뙇 하고 나오는게 아니라 개선하고 보완하고, 더 나은 기술로 진보하는거같다.  참 재밌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVVM 패턴을 Swift로 구현하는 방법]]></title>
            <link>https://velog.io/@ojl_0325/MVVM-%ED%8C%A8%ED%84%B4%EC%9D%84-Swift%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-154eb8kn</link>
            <guid>https://velog.io/@ojl_0325/MVVM-%ED%8C%A8%ED%84%B4%EC%9D%84-Swift%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-154eb8kn</guid>
            <pubDate>Thu, 16 Jan 2025 00:44:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/29856119-da95-4ef9-b401-930e70e034a7/image.png" alt=""></p>
<pre><code>// MARK: - Model
struct User {
    let id: Int
    let name: String
    let email: String
}

// MARK: - ViewModel
class UserViewModel {
    // Output
    @Published private(set) var userName: String = &quot;&quot;
    @Published private(set) var userEmail: String = &quot;&quot;
    @Published private(set) var isLoading: Bool = false

    // Input
    func fetchUserData() {
        isLoading = true

        // API 호출을 시뮬레이션
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
            guard let self = self else { return }

            // 실제로는 네트워크 요청을 통해 데이터를 받아옴
            let user = User(id: 1, name: &quot;홍길동&quot;, email: &quot;hong@example.com&quot;)

            self.updateUserInfo(with: user)
            self.isLoading = false
        }
    }

    private func updateUserInfo(with user: User) {
        userName = user.name
        userEmail = user.email
    }
}

// MARK: - View
class UserViewController: UIViewController {
    private let viewModel = UserViewModel()
    private var cancellables = Set&lt;AnyCancellable&gt;()

    private let nameLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private let emailLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private let loadingIndicator: UIActivityIndicatorView = {
        let indicator = UIActivityIndicatorView(style: .medium)
        indicator.translatesAutoresizingMaskIntoConstraints = false
        return indicator
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        bindViewModel()
        viewModel.fetchUserData()
    }

    private func setupUI() {
        view.backgroundColor = .white

        view.addSubview(nameLabel)
        view.addSubview(emailLabel)
        view.addSubview(loadingIndicator)

        NSLayoutConstraint.activate([
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20),

            emailLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),

            loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    private func bindViewModel() {
        // Combine을 사용한 데이터 바인딩
        viewModel.$userName
            .sink { [weak self] name in
                self?.nameLabel.text = name
            }
            .store(in: &amp;cancellables)

        viewModel.$userEmail
            .sink { [weak self] email in
                self?.emailLabel.text = email
            }
            .store(in: &amp;cancellables)

        viewModel.$isLoading
            .sink { [weak self] isLoading in
                if isLoading {
                    self?.loadingIndicator.startAnimating()
                } else {
                    self?.loadingIndicator.stopAnimating()
                }
            }
            .store(in: &amp;cancellables)
    }
}</code></pre><p>MVVM 패턴의 각 구성요소를 하나씩 살펴보자.</p>
<blockquote>
<p>Model 계층</p>
</blockquote>
<p>User 구조체로 데이터 모델을 정의했다.
간단히 id, name, email 속성만 가지고 있음</p>
<blockquote>
<p>ViewModel 계층</p>
</blockquote>
<p>UserViewModel 클래스가 비즈니스 로직을 담당한다.
@Published 프로퍼티래퍼를 사용해서 데이터 변경을 View에 자동으로 알려줌
Input으로 fetchUserData() 메서드를 제공
Output으로 userName, userEmail, isLoading 상태값을 제공</p>
<blockquote>
<p>View 계층</p>
</blockquote>
<p>UserViewController가 UI를 담당
ViewModel의 데이터를 observing하면서 UI 업데이트
Combine 프레임워크로 데이터 바인딩 구현
SwiftUI에서는 onChange로 감지해도 됨</p>
<blockquote>
<p>주요 특징들:</p>
</blockquote>
<p>View는 ViewModel만 참조하고 Model을 직접 알지 못함
데이터 흐름이 단방향임 (ViewModel → View)
ViewModel은 View에 대해 전혀 모름 (의존성 분리)
테스트하기 쉬운 구조</p>
<p>실제 사용시에는 더 복잡한 비즈니스 로직이나 네트워크 통신 등이 추가될 수 있어. 하지만 이 기본 구조를 잘 이해하면 확장하기 쉽다.</p>
<p>예를 들어 사용자 정보를 수정하는 기능을 추가하고 싶다면:</p>
<pre><code>// ViewModel에 Input 추가
func updateUserName(_ name: String) {
    userName = name
    // API 호출 로직
}

// View에서 호출
nameTextField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)

@objc private func nameChanged(_ sender: UITextField) {
    viewModel.updateUserName(sender.text ?? &quot;&quot;)
}</code></pre><p>이런 식으로 기능을 쉽게 확장할 수 있.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[단위테스트]]></title>
            <link>https://velog.io/@ojl_0325/%EB%8B%A8%EC%9C%84%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@ojl_0325/%EB%8B%A8%EC%9C%84%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Wed, 15 Jan 2025 02:40:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/f2d7f234-093d-44d3-bb9b-407e66cd2b9d/image.png" alt=""></p>
<p>단위 테스트(Unit Testing)는 소프트웨어 개발에서 가장 기본적이고 중요한 테스트 방법 중 하나</p>
<ul>
<li>코드의 가장 작은 단위(보통 함수나 메서드 수준)를 독립적으로 테스트하는 방법</li>
<li>특정 함수가 예상대로 동작하는지 확인하기 위해 다양한 입력값을 넣고 그 결과를 검증</li>
<li>자동화된 방식으로 실행되어 코드 변경 시 빠르게 문제를 발견</li>
</ul>
<blockquote>
<p>XCTest를 이용한 단위테스트</p>
</blockquote>
<pre><code>import XCTest

class Calculator {
    func add(_ a: Int, _ b: Int) -&gt; Int {
        return a + b
    }
}

class CalculatorTests: XCTestCase {
    var calculator: Calculator!

    override func setUp() {
        super.setUp()
        calculator = Calculator()
    }

    func testAdd() {
        // Given
        let a = 5
        let b = 3

        // When
        let result = calculator.add(a, b)

        // Then
        XCTAssertEqual(result, 8)
    }
}</code></pre><blockquote>
<p>ViewModel 테스트</p>
</blockquote>
<pre><code>class WeatherViewModel: ObservableObject {
    @Published var temperature: Double = 0.0

    func convertToFahrenheit(_ celsius: Double) -&gt; Double {
        return (celsius * 9/5) + 32
    }
}

class WeatherViewModelTests: XCTestCase {
    var viewModel: WeatherViewModel!

    override func setUp() {
        super.setUp()
        viewModel = WeatherViewModel()
    }

    func testTemperatureConversion() {
        // Given
        let celsius = 20.0

        // When
        let fahrenheit = viewModel.convertToFahrenheit(celsius)

        // Then
        XCTAssertEqual(fahrenheit, 68.0)
    }
}</code></pre><blockquote>
<p>Preview Provider를 활용한 UI 테스트:</p>
</blockquote>
<pre><code>struct ContentView: View {
    @StateObject var viewModel = WeatherViewModel()

    var body: some View {
        Text(&quot;\(viewModel.temperature)°C&quot;)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}</code></pre><p>SwiftUI 테스트 시 주요 고려사항</p>
<blockquote>
<p>비즈니스 로직 분리</p>
</blockquote>
<ul>
<li>View에서 비즈니스 로직을 분리하여 ViewModel이나 별도 타입으로 구성</li>
<li>순수 로직은 단위 테스트가 용이하도록 설계</li>
</ul>
<blockquote>
<p>의존성 주입</p>
</blockquote>
<pre><code>struct ContentView: View {
    @StateObject var viewModel: WeatherViewModel // 테스트를 위해 주입 가능

    init(viewModel: WeatherViewModel = WeatherViewModel()) {
        _viewModel = StateObject(wrappedValue: viewModel)
    }
}</code></pre><blockquote>
<p>비동기 테스트</p>
</blockquote>
<pre><code>func testAsyncOperation() async throws {
    // Given
    let expectation = XCTestExpectation(description: &quot;Async operation&quot;)

    // When
    await viewModel.fetchWeatherData()

    // Then
    XCTAssertNotNil(viewModel.temperature)
    expectation.fulfill()
}</code></pre><blockquote>
<p>환경값(Environment) 테스트</p>
</blockquote>
<pre><code>struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        Text(&quot;Hello&quot;)
            .foregroundColor(colorScheme == .dark ? .white : .black)
    }
}

// 테스트
func testColorScheme() {
    let view = ContentView()
        .environment(\.colorScheme, .dark)
    // 테스트 로직
}</code></pre><blockquote>
<p>테스트 작성 시 기억할 점</p>
</blockquote>
<ul>
<li>Given-When-Then 패턴 사용</li>
<li>각 테스트는 독립적이어야 함</li>
<li>테스트 이름은 명확하게 작성</li>
<li>실패 케이스도 테스트</li>
</ul>
<blockquote>
<p>Given-When-Then 패턴</p>
</blockquote>
<pre><code>func testExample() {
    // Given (준비)
    // 테스트에 필요한 초기 조건과 데이터를 설정
    let calculator = Calculator()
    let input = 5

    // When (실행)
    // 테스트하려는 동작을 실행
    let result = calculator.square(input)

    // Then (검증)
    // 결과를 검증
    XCTAssertEqual(result, 25)
}</code></pre><p>각 부분의 역할:</p>
<ul>
<li>Given: 테스트를 위한 전제 조건을 설정 (객체 생성, 초기값 설정 등)</li>
<li>When: 테스트하려는 실제 동작을 수행</li>
<li>Then: 예상한 결과가 나왔는지 확인</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Custom Navigation Swipe 뒤로가기]]></title>
            <link>https://velog.io/@ojl_0325/Custom-Navigation-Swipe-%EB%92%A4%EB%A1%9C%EA%B0%80%EA%B8%B0</link>
            <guid>https://velog.io/@ojl_0325/Custom-Navigation-Swipe-%EB%92%A4%EB%A1%9C%EA%B0%80%EA%B8%B0</guid>
            <pubDate>Tue, 14 Jan 2025 05:15:47 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/d6a94ed6-1c59-4ecd-ae65-bacd5dcc68d5/image.png" alt=""></p>
<p>SwiftUI에서 Custom NavigationTitleView를 사용하게되면</p>
<p>예쁘게 상단 바 타이틀을 바꿀 수 있지만
back버튼을 hidden처리하기 때문에 필연적으로 Swipe기능이 막히게된다.
이는 UX적으로 좋지 못하다.</p>
<p>따라서 아래 익스텐션을 사용하면 hidden처리하더라도 뒤로가기를 사용할 수 있다.
물론 저 extenstion도 마찬가지로 커스텀해서 특정뷰에서는 막긴해야하니, </p>
<p>여러모로 SwiftUI는 편리하면서도 손이 많이간다.</p>
<pre><code>extension UINavigationController: ObservableObject, UIGestureRecognizerDelegate {
    open override func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -&gt; Bool {
        return viewControllers.count &gt; 1
    }
}
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Unity Awake와 Start의 차이]]></title>
            <link>https://velog.io/@ojl_0325/Unity-Awake%EC%99%80-Start%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@ojl_0325/Unity-Awake%EC%99%80-Start%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Mon, 13 Jan 2025 03:49:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/d24760de-97a1-47bf-8767-626506bbeff4/image.png" alt=""></p>
<p>Unity에서 빈번히 사용하는 Update()와 달리 Awake()와 Start()는 Initalization 구역으로 스크립트의 초기화에 이용된다.</p>
<p>단, 호출의 순서는 Awake() -&gt; Start()로 Awake()가 먼저 호출되는데, </p>
<p>둘의 차이는 Awake는 모든 오브젝트가 초기화되고 호출되고, Start는 스크립트 인스턴스가 활성화된 경우에만 호출된다는 점이다.</p>
<p>간단히 말하면, Awake는 항상 Start 함수의 이전 및 프리팹의 인스턴스화 직후에 호출</p>
<p>Start는 첫 번째 프레임의 업데이트 전에 Start가 호출된다.</p>
<p>만약 Start만으로 모든 스크립트를 초기화한다고 생각해보자.</p>
<p>UnitA와 EnemyA에게 적용한 스크립트가 각각 다르고 유닛들을 관리하는 Controller의 Start에서 각 유닛을 Instantiate()하고 스크립트 정보를 컴포넌트로 가져온다고 생각해보자.</p>
<p>Controller가 각 유닛을 인스턴스화 하기 때문에 가장 먼저 호출되는 Start는 Controller의 start이다.</p>
<p>두 유닛을 소환한다면 두 유닛의 Start는 Controller의 start가 끝나고 호출될까? 아니면 먼저 호출될까?</p>
<p>경험해본 바로는 Controller의 start가 먼저 실행되는 것 같지만, 순서는 확정할 수 없을 것 같다.</p>
<p>본인의 경우는 결국 Controller의 Start는 자신이 인스턴스화한 유닛의 스크립트들이 start로 초기화된 정보가 없기 때문에 nullreference오류를 발생시켰다.</p>
<p>이런 경우와 같이 스크립트간의 참조를 설정을 하기 위해서는 Awake()를 사용해주어야 한다.</p>
<p>Awake는 모든 오브젝트가 초기화되고 호출되기 때문에 다른 스크립트와 연결을 보장할 수 있다.</p>
<p>다른 스크립트들을 많이 참조하는 프로젝트일수록 Awake()의 사용을 권장해야 할 것 같다.</p>
<p>이렇게만 보면 Start를 굳이 사용해야 하나 생각이 들긴하는데, 모든 초기화가 끝난 후 다음 순서로 실행할 필요가 있는 작업에는 Start를 사용해보면 편할 듯하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[클립보드 복사 및 붙여넣기]]></title>
            <link>https://velog.io/@ojl_0325/%ED%81%B4%EB%A6%BD%EB%B3%B4%EB%93%9C-%EB%B3%B5%EC%82%AC-%EB%B0%8F-%EB%B6%99%EC%97%AC%EB%84%A3%EA%B8%B0</link>
            <guid>https://velog.io/@ojl_0325/%ED%81%B4%EB%A6%BD%EB%B3%B4%EB%93%9C-%EB%B3%B5%EC%82%AC-%EB%B0%8F-%EB%B6%99%EC%97%AC%EB%84%A3%EA%B8%B0</guid>
            <pubDate>Mon, 13 Jan 2025 03:47:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/6f10bfd9-34a2-42b3-a1d4-52675da7c095/image.png" alt=""></p>
<p>가능하면 꾸준히 글을 써야곘다는 생각으로
오늘도 글을 올린다. 
아래는 클립보드 복사 및 붙여넣기를 작성해놓은것이다.</p>
<p>activity_main.xml</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;LinearLayout
  xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  android:orientation=&quot;vertical&quot;
  android:padding=&quot;20dp&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;&gt;

    &lt;EditText
      android:layout_marginTop=&quot;50dp&quot;
      android:id=&quot;@+id/editText&quot;
      android:hint=&quot;텍스트를 입력해주세요&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;/&gt;

    &lt;Button
      android:layout_marginTop=&quot;20dp&quot;
      android:id=&quot;@+id/btnCopy&quot;
      android:text=&quot;복사하기&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;/&gt;

    &lt;Button
      android:layout_marginTop=&quot;10dp&quot;
      android:id=&quot;@+id/btnPaste&quot;
      android:text=&quot;붙여넣기&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;/&gt;
&lt;/LinearLayout&gt;</code></pre><p>MainActivity.java</p>
<pre><code>public class MainActivity extends AppCompatActivity {

  private EditText editText;
  private Button btnCopy, btnPaste;
  private ClipboardManager clipboardManager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);


    editText = findViewById(R.id.editText);
    btnCopy = findViewById(R.id.btnCopy);
    btnPaste = findViewById(R.id.btnPaste);

    clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);

    // 텍스트 복사하기
    btnCopy.setOnClickListener(view -&gt; {
      String textToCopy = editText.getText().toString();
      ClipData clipData = ClipData.newPlainText(&quot;text&quot;, textToCopy);
      clipboardManager.setPrimaryClip(clipData);
      Toast.makeText(MainActivity.this, &quot;텍스트가 복사되었습니다!&quot;, Toast.LENGTH_SHORT).show();
    });

    // 텍스트 붙여넣기
    btnPaste.setOnClickListener(view -&gt; {
      if (clipboardManager.hasPrimaryClip() &amp;&amp; clipboardManager.getPrimaryClipDescription().hasMimeType(&quot;text/plain&quot;)) {
        ClipData.Item item = clipboardManager.getPrimaryClip().getItemAt(0);
        editText.setText(item.getText().toString());
        Toast.makeText(MainActivity.this, &quot;텍스트가 붙여넣기 되었습니다!&quot;, Toast.LENGTH_SHORT).show();
      } else {
        Toast.makeText(MainActivity.this, &quot;클립보드에 복사된 텍스트가 없습니다!&quot;, Toast.LENGTH_SHORT).show();
      }
    });


  }

}</code></pre><p>ClipboardManager 선언 : clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);</p>
<p>텍스트 복사 : clipboardManager.setPrimaryClip(clipData);</p>
<p>텍스트 붙여넣기 : clipboardManager.getPrimaryClip().getItemAt(0).getText();</p>
<p>데이터 유효성 검사 : if (clipboardManager.hasPrimaryClip())</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[combineText]]></title>
            <link>https://velog.io/@ojl_0325/combineText</link>
            <guid>https://velog.io/@ojl_0325/combineText</guid>
            <pubDate>Sat, 11 Jan 2025 03:37:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/6cea62b6-0f67-4acf-bb9b-d1db50076c59/image.png" alt=""></p>
<p>이 코드는 사용자의 키보드 입력과 음성 인식(STT) 입력을 동시에 관리하는 바인딩을 구현한 것이다.
텍스트 변경이 키보드에 의한 것인지 음성 인식에 의한 것인지 구분하여 적절히 처리한다.</p>
<p>현재 진행하고있는 프로젝트에서 자주쓰이는 코드라 정의해본다.</p>
<pre><code>// private 접근자를 가진 combinedText라는 Binding&lt;String&gt; 타입의 computed property를 정의
private var combinedText: Binding&lt;String&gt; {
   // Binding 객체를 생성하여 반환. getter와 setter를 포함
   Binding(
       // getter: reWriteText와 STTtempText를 합쳐서 반환
       get: { reWriteText + STTtempText },

       // setter: 새로운 값이 들어올 때 실행되는 로직
       set: { newValue in
           // 새로운 값의 길이가 기존 텍스트 길이의 합과 다르다면 (키보드 입력이 있었다는 의미)
           if newValue.count != reWriteText.count + STTtempText.count {
               // 마이크가 활성화되어 있다면
               if micisPressed {
                   // 마이크 비활성화
                   micisPressed = false
                   // 음성 인식 중지
                   speechRecognizer.stopTranscribing()
               }
               // 새로운 값을 reWriteText에 저장
               reWriteText = newValue
               // STTtempText 초기화
               STTtempText = &quot;&quot;
               return
           }

           // 새로운 값의 길이가 reWriteText 길이보다 크거나 같은 경우
           if newValue.count &gt;= reWriteText.count {
               // 새로운 값에서 reWriteText 길이만큼 앞부분을 제외한 나머지 부분
               let sttPart = String(newValue.dropFirst(reWriteText.count))

               // sttPart가 현재 STTtempText와 다르다면
               if sttPart != STTtempText {
                   // STTtempText를 새로운 sttPart로 업데이트
                   STTtempText = sttPart
               } else {
                   // 같다면 전체 텍스트를 reWriteText에 저장
                   reWriteText = newValue
                   // STTtempText 초기화
                   STTtempText = &quot;&quot;
               }
           } else {
               // 새로운 값의 길이가 reWriteText보다 작은 경우
               // 새로운 값을 reWriteText에 저장
               reWriteText = newValue
               // STTtempText 초기화
               STTtempText = &quot;&quot;
           }
       }
   )
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[cornerRadius]]></title>
            <link>https://velog.io/@ojl_0325/cornerRadius</link>
            <guid>https://velog.io/@ojl_0325/cornerRadius</guid>
            <pubDate>Fri, 10 Jan 2025 01:40:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/d568580c-0f9d-4e09-b00f-8d22fb6311ff/image.png" alt=""></p>
<pre><code>// 특정 모서리만 둥글게 만들기 위해 필요합니다
extension View {
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner, strokeColor: Color, lineWidth: CGFloat) -&gt; some View {
        let shape = RoundedCorner(radius: radius, corners: corners)
        return self
            .clipShape(shape)
            .overlay(
                shape
                    .stroke(strokeColor, lineWidth: lineWidth)
            )
    }
}

struct RoundedCorner: Shape {
    var radius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners

    func path(in rect: CGRect) -&gt; Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))

        return Path(path.cgPath)
    }

}</code></pre><p>위 코드는 특정뷰를 둥글게 처리하기 위함으로 만들어진 코드다.</p>
<p>문제가 있다.</p>
<p>상하좌우 사각형이 있을때 특정 부분을 지우고싶다면?
생각을 해봤는데 그냥 외곽선을 그라데이션을 준다음 
.clear처리해버리면 
굳이 새로운 수식이나 코드를 안짜도 될거라는 생각이 들었다.</p>
<p>꽤 재밌게 생각이 났던 꼼수다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[튜플]]></title>
            <link>https://velog.io/@ojl_0325/%ED%8A%9C%ED%94%8C</link>
            <guid>https://velog.io/@ojl_0325/%ED%8A%9C%ED%94%8C</guid>
            <pubDate>Wed, 08 Jan 2025 05:26:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ojl_0325/post/a45b2c4e-25fe-444e-bd8b-dd21e69668ed/image.png" alt=""></p>
<p>최근에 iOS 로컬데이터를 다루는데 있어서 데이터 구조는 조금 복잡한? 하지만 기능 자체는 간단한거를 구현해야할 일이 생겼다.</p>
<p>그래서 Model을 만들어서 할까하다가. 다른 방식은 없을까를 고민해보다가 튜플이라는것을 알게되었다.</p>
<ul>
<li><strong>Tuple</strong><blockquote>
<p> 튜플은 여러 값을 하나의 복합적인 값으로 그룹화하는 방법, 각각 다른 타입의 값들을 담을 수 있고, 순서가 있는 것이 특징</p>
</blockquote>
</li>
</ul>
<pre><code>// 기본적인 튜플 선언
let coordinates = (x: 3, y: 4)

// 각 요소에 접근하기
print(coordinates.x)  // 3
print(coordinates.y)  // 4

// 타입이 다른 값들도 담을 수 있음
let person = (name: &quot;김철수&quot;, age: 25, isStudent: true)
print(person.name)      // &quot;김철수&quot;
print(person.age)       // 25
print(person.isStudent) // true

// 튜플 분해
let (x, y) = coordinates
print(x)  // 3
print(y)  // 4

// 특정 값만 무시하고 싶을 때는 _ 사용
let (name, _, isStudent) = person</code></pre><ul>
<li><strong>특징</strong></li>
<li>고정된 수의 값들을 저장할 수 있다</li>
<li>각 값은 서로 다른 타입일 수 있다</li>
<li>요소에 이름을 붙일 수 있다</li>
<li>함수에서 여러 값을 한번에 반환할 때 유용하다</li>
</ul>
<ul>
<li>** 예시**</li>
</ul>
<pre><code>func getUserInfo() -&gt; (name: String, age: Int) {
    return (&quot;홍길동&quot;, 30)
}

let userInfo = getUserInfo()
print(userInfo.name)  // &quot;홍길동&quot;
print(userInfo.age)   // 30</code></pre><ul>
<li>** 결론**</li>
</ul>
<blockquote>
<p>튜플은 간단한 데이터 그룹화에 적합하지만, 
복잡한 데이터 구조가 필요한 경우에는 구조체(struct)나 클래스(class)를 사용하는 것이 더 좋다.</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>