<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>miin-hyukkk.log</title>
        <link>https://velog.io/</link>
        <description>Born to be FE developer 🧑🏻‍💻</description>
        <lastBuildDate>Thu, 07 May 2026 05:14:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>miin-hyukkk.log</title>
            <url>https://velog.velcdn.com/images/miin-hyukkk/profile/0a853158-5a13-4872-988a-8bd9de3766dd/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. miin-hyukkk.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/miin-hyukkk" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[GitHub App으로 다른 레포지토리에 PR 생성하기]]></title>
            <link>https://velog.io/@miin-hyukkk/GitHub-App%EC%9C%BC%EB%A1%9C-%EB%8B%A4%EB%A5%B8-%EB%A0%88%ED%8F%AC%EC%A7%80%ED%86%A0%EB%A6%AC%EC%97%90-PR-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@miin-hyukkk/GitHub-App%EC%9C%BC%EB%A1%9C-%EB%8B%A4%EB%A5%B8-%EB%A0%88%ED%8F%AC%EC%A7%80%ED%86%A0%EB%A6%AC%EC%97%90-PR-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 07 May 2026 05:14:00 GMT</pubDate>
            <description><![CDATA[<p>이번 작업의 목표는 GitHub Actions에서 <strong>다른 레포지토리로 PR을 자동 생성</strong>하는 것이다.</p>
<p>현재 구조는 다음과 같다.</p>
<pre><code class="language-txt">벽촌 프론트엔드 모듈 레포지토리
↓
npm 배포 완료
↓
회사 홈페이지 레포지토리(Ingensrota)에 PR 생성</code></pre>
<p>여기서 중요한 점은 Action이 실행되는 레포지토리와 PR을 생성해야 하는 레포지토리가 다르다는 것이다.</p>
<p>그래서 기본 <code>GITHUB_TOKEN</code>만으로는 권한이 부족.</p>
<h4 id="기본-github_token이란">기본 <code>GITHUB_TOKEN</code>이란?</h4>
<blockquote>
<ul>
<li>GitHub Actions가 실행될 때 GitHub에서 자동으로 생성해주는 비밀번호(인증 토큰)</li>
</ul>
</blockquote>
<ul>
<li>별도로 설정하지 않아도 워크플로우(yml 파일)가 돌아가는 동안에는 메모리에 자동으로 생성되었다가, 작업이 끝나면 사라짐</li>
</ul>
<p>이를 해결하기 위해 GitHub App을 만들고, 해당 App에 대상 레포지토리 접근 권한을 부여한 뒤, GitHub Actions에서 그 App 권한으로 PR을 생성하도록 설정한다.</p>
<h4 id="github-app이란">GitHub App이란?</h4>
<blockquote>
<ul>
<li>GitHub API를 호출하기 위한 전용 인증 주체</li>
</ul>
</blockquote>
<ul>
<li>특정 사용자의 권한을 빌리는 것이 아니라, App 자체가 고유한 ID를 가짐</li>
<li>필요한 권한만 세밀하게 설정해서 사용 가능(ex. Pull Requests: Read &amp; Write)</li>
</ul>
<h3 id="정리">정리</h3>
<p>현재 작업은 레포지토리 A(벽촌)에서 발생한 이벤트로 레포지토리 B(Ingensrota)의 데이터를 조작해야함</p>
<p>*<em>GITHUB_TOKEN의 한계 *</em>
이 토큰은 레포지토리 A 안에서만 유효한 일회성 토큰입니다. B 레포지토리에 접근할 권한이 아예 없음</p>
<p><strong>GitHub App의 해결책</strong></p>
<ul>
<li>App 생성</li>
<li>이 App을 레포지토리 B에 설치하고 권한을 부여</li>
<li>레포지토리 A의 Action 실행 시점에 이 App의 정보를 이용해 접근 토큰(Installation Access Token)을 발급</li>
<li>이 토큰은 레포지토리 B에 대한 권한이 있으므로 PR 생성이 가능</li>
</ul>
<hr>
<h3 id="전체-흐름">전체 흐름</h3>
<pre><code class="language-txt">1. GitHub App 생성
2. GitHub App에 필요한 권한 부여
3. Client ID와 Private Key 발급
4. 대상 레포지토리에 GitHub App 설치
5. Action이 실행되는 레포지토리에 Secrets / Variables 등록
6. GitHub Actions에서 App 권한으로 PR 생성</code></pre>
<hr>
<h2 id="1-github-app-만들기">1. GitHub App 만들기</h2>
<p>먼저 organization 관리자 계정으로 접속</p>
<ol>
<li>GitHub 우측 상단 프로필 이미지를 클릭한다.</li>
<li><strong>Settings</strong>로 이동한다.</li>
<li>왼쪽 메뉴 하단의 <strong>Developer settings</strong>를 클릭한다.</li>
<li>왼쪽 메뉴에서 <strong>GitHub Apps</strong>를 선택한다.</li>
<li>우측 상단의 <strong>New GitHub App</strong> 버튼을 클릭한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/3f27eb5c-dc68-4b73-b073-081400defe8f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/39b92b9b-a7d9-4264-8eb0-2a3de18c2875/image.png" alt=""></p>
<hr>
<h2 id="2-github-app-기본-정보-입력">2. GitHub App 기본 정보 입력</h2>
<p>GitHub App 생성 화면에서 기본 정보를 입력한다.</p>
<h3 id="github-app-name">GitHub App name</h3>
<p>원하는 이름을 입력하면 된다.</p>
<p>예시는 다음과 같다.</p>
<pre><code class="language-txt">ingensrota-pr-updater-bot</code></pre>
<p>이 이름은 나중에 GitHub App 목록에서 구분하기 위한 이름이다.</p>
<h3 id="homepage-url">Homepage URL</h3>
<p>필수값이기 때문에 아무 URL이나 입력해도 된다.</p>
<p>예를 들어 GitHub 프로필 주소나 회사 홈페이지 주소를 넣으면 된다.</p>
<pre><code class="language-txt">https://github.com/RevolutionaryWarrior</code></pre>
<h3 id="webhook">Webhook</h3>
<p>이번 작업에서는 GitHub App의 Webhook 기능을 사용하지 않는다.</p>
<p>따라서 <strong>Webhook - Active 체크를 해제</strong>한다.</p>
<pre><code class="language-txt">Webhook Active 해제</code></pre>
<hr>
<h2 id="3-권한-설정하기">3. 권한 설정하기</h2>
<p>이제 가장 중요한 권한 설정을 해야 한다.</p>
<p>GitHub App이 다른 레포지토리에 브랜치를 만들고, 파일을 수정하고, PR을 생성하려면 해당 권한이 필요하다.</p>
<p>GitHub App 생성 화면에서 <strong>Repository permissions</strong> 섹션을 찾는다.</p>
<p>그리고 아래 권한을 설정한다.</p>
<table>
<thead>
<tr>
<th>권한</th>
<th>설정값</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>Contents</td>
<td>Read and write</td>
<td>대상 레포지토리에 브랜치를 만들고 파일을 커밋하기 위해 필요</td>
</tr>
<tr>
<td>Pull requests</td>
<td>Read and write</td>
<td>대상 레포지토리에 PR을 생성하기 위해 필요</td>
</tr>
<tr>
<td>Metadata</td>
<td>Read-only</td>
<td>GitHub App 기본 권한으로 보통 자동 설정됨</td>
</tr>
</tbody></table>
<p>정리하면 다음과 같다.</p>
<pre><code class="language-txt">Repository permissions

Contents      → Read and write
Pull requests → Read and write
Metadata      → Read-only</code></pre>
<p>권한 설정을 마쳤다면 페이지 하단의 <strong>Create GitHub App</strong> 버튼을 클릭한다.</p>
<hr>
<h2 id="4-client-id-확인하기">4. Client ID 확인하기</h2>
<p>GitHub App 생성이 완료되면 App 설정 페이지로 이동한다.</p>
<p>상단의 <strong>About</strong> 섹션에서 <strong>Client ID</strong> 값을 확인할 수 있다.</p>
<p>이 값은 나중에 GitHub Actions에서 사용할 예정이다.</p>
<pre><code class="language-txt">INGENSROTA_APP_CLIENT_ID</code></pre>
<p>예시는 다음과 같은 형태다.</p>
<pre><code class="language-txt">Iv23xxxxxxxxxxxxxxxxx</code></pre>
<p>이 값은 Action이 실행되는 레포지토리(벽촌프론트엔드모듈)의 <strong>Variables</strong>에 등록</p>
<hr>
<h2 id="5-private-key-발급하기">5. Private Key 발급하기</h2>
<p>같은 GitHub App 설정 페이지에서 아래로 스크롤하면 <strong>Private keys</strong> 섹션이 있다.</p>
<p>여기서 <strong>Generate a private key</strong> 버튼을 클릭한다.</p>
<h4 id="private-key란">Private Key란?</h4>
<blockquote>
<ul>
<li>Private Key(<code>____.pem</code> 파일)는 유출되면 안 되는 마스터키</li>
</ul>
</blockquote>
<ul>
<li>키를 사용해 &quot;지금부터 1시간만 유효한 임시 토큰을 줘&quot;라고 요청하는 방식 사용</li>
</ul>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/b94d97d0-9519-4875-a0ee-9b5d8dd190fe/image.png" alt=""></p>
<p>버튼을 누르면 <code>.pem</code> 확장자를 가진 파일이 다운로드된다.</p>
<p>이 파일을 VS Code나 텍스트 에디터로 열면 다음과 같은 형태의 private key가 들어있다.</p>
<pre><code class="language-txt">-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----</code></pre>
<p>이때 중요한 점은 <code>BEGIN</code>부터 <code>END</code>까지 전체 내용을 그대로 복사해야 한다는 것이다.</p>
<pre><code class="language-txt">-----BEGIN RSA PRIVATE KEY-----
내용 전체
-----END RSA PRIVATE KEY-----</code></pre>
<p>이 값은 Action이 실행되는 레포지토리(벽촌프론트엔드모듈)의 <strong>Secrets</strong>에 등록한다.</p>
<pre><code class="language-txt">INGENSROTA_APP_PRIVATE_KEY</code></pre>
<hr>
<h2 id="6-대상-레포지토리에-github-app-설치하기">6. 대상 레포지토리에 GitHub App 설치하기</h2>
<p>GitHub App을 만들었다고 해서 바로 모든 레포지토리에 접근할 수 있는 건 아님</p>
<p>GitHub App이 실제로 접근해야 하는 레포지토리에 App을 설치해야됨</p>
<p>이번 작업에서는 <code>RevolutionaryWarrior/Ingensrota</code> 레포지토리에 PR을 생성해야 하므로, 해당 레포지토리에 App을 설치한다.</p>
<ol>
<li>방금 만든 GitHub App 설정 페이지로 이동한다.</li>
<li>왼쪽 사이드바에서 <strong>Install App</strong> 메뉴를 클릭한다.</li>
<li>organization 또는 계정 우측의 <strong>Install</strong> 버튼을 클릭한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/3b1e0494-2972-44f9-92f5-55b386113e0d/image.png" alt=""></p>
<p>설치 화면에서는 모든 레포지토리가 아니라, 필요한 레포지토리만 선택하는 것이 좋다.</p>
<p>따라서 아래 옵션을 선택한다.</p>
<pre><code class="language-txt">Only select repositories</code></pre>
<p>그리고 대상 레포지토리인 <code>RevolutionaryWarrior/Ingensrota</code>를 선택한다.</p>
<pre><code class="language-txt">RevolutionaryWarrior/Ingensrota</code></pre>
<p>선택 후 <strong>Install</strong> 버튼을 눌러 설치를 완료한다.</p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/e0f48c98-6399-4cbb-b375-bccde274e3ac/image.png" alt=""></p>
<hr>
<h2 id="7-action이-실행되는-레포지토리에-값-등록하기">7. Action이 실행되는 레포지토리에 값 등록하기</h2>
<p>이제 GitHub Actions가 GitHub App 권한을 사용할 수 있도록 값을 등록해야 함</p>
<p>여기서 주의할 점은 값을 등록하는 위치다.</p>
<p>값은 PR이 열릴 레포지토리가 아니라, <strong>Action이 실행되는 레포지토리</strong>에 등록해야 한다.</p>
<p>이번 경우에는 <code>벽촌 프론트엔드 모듈</code> 레포지토리의 Actions에서 인젠스로타 레포지토리에 PR을 열 예정이므로, 값은 벽촌 프론트엔드 모듈 레포지토리에 등록한다.</p>
<p>이동 경로는 다음과 같다.</p>
<pre><code class="language-txt">Action이 실행되는 레포지토리
→ Settings
→ Secrets and variables
→ Actions</code></pre>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/318a2c39-a80c-4021-aba0-b6f3069f7058/image.png" alt=""></p>
<hr>
<h2 id="8-variables-등록하기">8. Variables 등록하기</h2>
<p>먼저 <strong>Variables</strong> 탭으로 이동한다.</p>
<p>그리고 <strong>New repository variable</strong> 버튼을 클릭한다.</p>
<p>아래 값을 등록한다.</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td><code>INGENSROTA_APP_CLIENT_ID</code></td>
<td>GitHub App에서 복사한 Client ID</td>
</tr>
</tbody></table>
<p>예시는 다음과 같다.</p>
<pre><code class="language-txt">Name:
INGENSROTA_APP_CLIENT_ID

Value:
Iv23xxxxxxxxxxxxxxxxx</code></pre>
<p>Client ID는 민감한 private key는 아니므로 Secret이 아니라 Variable에 등록해도 된다.</p>
<hr>
<h2 id="9-secrets-등록하기">9. Secrets 등록하기</h2>
<p>다음으로 <strong>Secrets</strong> 탭으로 이동한다.</p>
<p>그리고 <strong>New repository secret</strong> 버튼을 클릭한다.</p>
<p>아래 값을 등록한다.</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td><code>INGENSROTA_APP_PRIVATE_KEY</code></td>
<td><code>.pem</code> 파일 안의 전체 텍스트</td>
</tr>
</tbody></table>
<p>예시는 다음과 같다.</p>
<pre><code class="language-txt">Name:
INGENSROTA_APP_PRIVATE_KEY

Value:
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----</code></pre>
<p>Private Key는 외부에 노출되면 안 되는 값이므로 반드시 Secret으로 등록해야 한다.</p>
<hr>
<h2 id="최종-설정-정리">최종 설정 정리</h2>
<p>여기까지 완료되면 설정 값은 다음과 같이 준비된다.</p>
<pre><code class="language-txt">Repository Variable
- INGENSROTA_APP_CLIENT_ID

Repository Secret
- INGENSROTA_APP_PRIVATE_KEY</code></pre>
<p>그리고 GitHub App은 <code>RevolutionaryWarrior/Ingensrota</code> 레포지토리에 설치되어 있어야 한다.</p>
<p>즉, 최종적으로 필요한 조건은 다음과 같다.</p>
<pre><code class="language-txt">1. GitHub App 생성 완료
2. Contents Read and write 권한 부여
3. Pull requests Read and write 권한 부여
4. Private Key 발급 완료
5. 대상 레포지토리인 Ingensrota에 App 설치 완료
6. Action 실행 레포지토리에 Client ID / Private Key 등록 완료</code></pre>
<hr>
<h2 id="이-설정이-필요한-이유">이 설정이 필요한 이유</h2>
<p>처음에는 단순히 GitHub Actions에서 다른 레포지토리에 PR을 열면 된다고 생각할 수 있다.</p>
<p>하지만 실제로는 권한 문제가 생긴다.</p>
<p>Action이 실행되는 레포지토리의 기본 <code>GITHUB_TOKEN</code>은 기본적으로 해당 레포지토리 안에서 동작하도록 만들어져 있다.</p>
<p>반면 이번 작업은 다음과 같이 동작해야 한다.</p>
<pre><code class="language-txt">벽촌 프론트엔드 모듈 레포지토리
↓
인젠스로타 레포지토리에 브랜치 생성
↓
md 파일 추가 또는 수정
↓
PR 생성</code></pre>
<p>따라서 인젠스로타 레포지토리에 접근할 수 있는 별도의 권한이 필요하다.</p>
<p>이 역할을 GitHub App이 담당한다.</p>
<p>GitHub App을 사용하면 필요한 레포지토리에만 제한적으로 권한을 부여할 수 있고, 필요한 권한도 <code>Contents</code>, <code>Pull requests</code>처럼 세부적으로 설정할 수 있다.</p>
<p>개인 계정의 PAT를 사용하는 것보다 관리하기도 좋고, 권한 범위도 더 명확하게 제한할 수 있다.</p>
<hr>
<h2 id="완료-후-기대-동작">완료 후 기대 동작</h2>
<p>설정이 끝난 뒤 GitHub Actions를 다시 실행하면, Action은 GitHub App 권한을 사용해서 인젠스로타 레포지토리에 접근할 수 있다.</p>
<p>이제 다음과 같은 작업이 가능해진다.</p>
<pre><code class="language-txt">1. 인젠스로타 레포지토리 checkout
2. 새로운 작업 브랜치 생성
3. md 파일 생성 또는 수정
4. commit &amp; push
5. main 브랜치 기준으로 PR 생성</code></pre>
<p>즉, 벽촌 프론트엔드 모듈이 배포된 이후 회사 홈페이지 레포지토리에 자동으로 문서 수정 PR을 열 수 있는 준비가 끝난 것이다.</p>
<hr>
<h2 id="정리-1">정리</h2>
<p>이번 글에서는 GitHub Actions에서 다른 레포지토리에 PR을 만들기 위해 GitHub App을 설정하는 과정을 정리했다.</p>
<p>핵심은 다음과 같다.</p>
<pre><code class="language-txt">기본 GITHUB_TOKEN만으로는 다른 레포지토리 PR 생성이 제한될 수 있다.
그래서 GitHub App을 만들고,
대상 레포지토리에 설치한 뒤,
GitHub Actions에서 해당 App 권한을 사용하도록 설정한다.</code></pre>
<p>이번 설정이 끝나면 이후 자동화 흐름에서 인젠스로타 레포지토리에 PR을 여는 작업을 안정적으로 처리할 수 있다.</p>
<p>다음 단계에서는 이 PR 안에 변경된 모듈에 해당하는 <code>md</code> 문서를 자동으로 생성하는 작업을 붙일 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 모듈 문서 자동화 작업 정리]]></title>
            <link>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%AA%A8%EB%93%88-%EB%AC%B8%EC%84%9C-%EC%9E%90%EB%8F%99%ED%99%94-%EC%9E%91%EC%97%85-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%AA%A8%EB%93%88-%EB%AC%B8%EC%84%9C-%EC%9E%90%EB%8F%99%ED%99%94-%EC%9E%91%EC%97%85-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 07 May 2026 05:11:06 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 팀에서는 공통으로 사용할 수 있는 모듈을 따로 관리하고 있다.</p>
<p>현재 모듈은 크게 네 가지로 나뉜다.</p>
<ul>
<li><code>basic-ui</code></li>
<li><code>core</code></li>
<li><code>hooks</code></li>
<li><code>utils</code></li>
</ul>
<p>이 모듈들은 프론트엔드 개발을 하면서 반복적으로 사용되는 UI, 로직, 훅, 유틸 함수들을 모아둔 패키지다.</p>
<p>각 모듈은 npm에 배포되어 있고, 필요한 프로젝트에서 설치해서 사용할 수 있다.</p>
<p>예를 들어 <code>hooks</code> 패키지에는 여러 프로젝트에서 공통으로 사용할 수 있는 React Hook들이 들어있고, <code>utils</code>에는 공통 유틸 함수들이 들어있다.</p>
<p>즉, 프로젝트마다 같은 코드를 반복해서 만들지 않고, 공통 모듈로 분리해서 재사용하기 위한 구조</p>
<hr>
<h2 id="왜-문서-자동화가-필요했나">왜 문서 자동화가 필요했나?</h2>
<p>기존에는 이 모듈들에 대한 사용 설명서가 Notion에 정리되어 있었다.</p>
<p>예를 들면 다음과 같은 내용이다.</p>
<ul>
<li>이 모듈이 어떤 역할을 하는지</li>
<li>언제 사용하는지</li>
<li>어떻게 사용하는지</li>
<li>전달해야 하는 인자는 무엇인지</li>
<li>반환값은 무엇인지</li>
<li>사용 예시는 어떤지</li>
</ul>
<p>하지만 앞으로는 이 설명서를 회사 홈페이지인 <strong>인젠스로타</strong>에 보여주려고 한다.</p>
<hr>
<h2 id="현재-레포지토리-구조">현재 레포지토리 구조</h2>
<p>이번 작업과 관련된 레포지토리는 두 개다.</p>
<h3 id="1-벽촌-프론트엔드-모듈-레포지토리">1. 벽촌 프론트엔드 모듈 레포지토리</h3>
<p>공통 모듈이 관리되는 레포지토리다.</p>
<p>여기에는 다음과 같은 패키지들이 있다.</p>
<pre><code>@byuckchon-frontend/basic-ui
@byuckchon-frontend/core
@byuckchon-frontend/hooks
@byuckchon-frontend/utils</code></pre><p>각 패키지는 변경이 생기면 <code>pnpm changeset</code>을 통해 버전이 올라가고, npm에 배포된다.</p>
<p>예를 들어 <code>hooks</code> 패키지에 새로운 훅이 추가되었다면, <code>@byuckchon-frontend/hooks</code> 패키지만 patch, minor, major 중 하나로 버전이 올라간다.</p>
<h3 id="2-회사-홈페이지-레포지토리">2. 회사 홈페이지 레포지토리</h3>
<p>회사 홈페이지인 <strong>인젠스로타</strong> 레포지토리다.</p>
<p>모듈 사용 설명서는 이 레포지토리 안에 <code>md</code> 파일로 관리되고, 홈페이지에서 해당 문서를 읽어서 화면에 보여줌</p>
<hr>
<h2 id="기존-배포-흐름">기존 배포 흐름</h2>
<p>현재 벽촌 프론트엔드 모듈 레포지토리의 배포 흐름은 다음과 같다.</p>
<pre><code class="language-tsx">작업 브랜치
↓
main 머지
↓
CI/CD 실행
↓
changeset 기반 version package PR 생성
↓
version package PR 머지
↓
npm 배포</code></pre>
<p>즉, 개발자가 모듈을 수정하거나 새로운 모듈을 추가한 뒤 main에 머지하면, changeset이 변경된 패키지를 감지한다.</p>
<p>그리고 해당 패키지의 버전을 올리는 PR이 자동으로 생성된다.</p>
<p>이 PR을 머지하면 실제 npm 배포가 진행된다.</p>
<hr>
<h2 id="자동화하려는-부분">자동화하려는 부분</h2>
<p>이번에 하고 싶은 작업은 크게 두 가지다.</p>
<h3 id="첫-번째-작업">첫 번째 작업</h3>
<p>npm 배포가 완료된 후, 같은 organization에 있는 <strong>인젠스로타 레포지토리</strong>로 PR을 자동으로 여는 작업!</p>
<p>예를 들어 <code>@byuckchon-frontend/hooks</code> 패키지가 배포되었다면, 배포가 끝난 뒤 인젠스로타 레포지토리에 새로운 PR이 열리도록 하는 것이다.</p>
<p>이 PR은 단순히 레포지토리를 연결하는 용도가 아니라, 배포된 모듈의 설명서 변경 작업을 담기 위한 PR이다.</p>
<p>흐름으로 보면 다음과 같다.</p>
<pre><code>벽촌 프론트엔드 모듈 배포 완료
↓
배포된 패키지 정보 확인
↓
인젠스로타 레포지토리에 작업 브랜치 생성
↓
인젠스로타 레포지토리에 PR 생성</code></pre><p>이 첫 번째 작업에 대한 자세한 내용은 이전 게시글에서 확인 가능!</p>
<blockquote>
<p><a href="https://velog.io/@miin-hyukkk/GitHub-App%EC%9C%BC%EB%A1%9C-%EB%8B%A4%EB%A5%B8-%EB%A0%88%ED%8F%AC%EC%A7%80%ED%86%A0%EB%A6%AC%EC%97%90-PR-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0">GitHub App으로 다른 레포지토리에 PR 생성하기</a></p>
</blockquote>
<hr>
<h3 id="두-번째-작업">두 번째 작업</h3>
<p>두 번째 작업은 열린 PR 안에, 수정되거나 추가된 모듈에 해당하는 <code>md</code> 파일을 자동으로 작성되는 작업</p>
<p>예를 들어 <code>hooks</code> 패키지에 <code>useScrollTop</code>이라는 훅이 새로 추가되었다고 가정해보자.</p>
<p>그러면 npm 배포 이후 인젠스로타 레포지토리에 PR이 열리고, 그 PR 안에는 <code>useScrollTop.md</code> 같은 설명서 파일이 자동으로 생성되어야 한다.</p>
<p>예시는 다음과 같다.</p>
<pre><code>@byuckchon-frontend/hooks
└── useScrollTop 추가</code></pre><p>배포 후 인젠스로타 레포지토리에는 다음과 같은 문서가 생성됨</p>
<pre><code>docs/hooks/useScrollTop.md</code></pre><p>그리고 이 <code>md</code> 파일 안에는 훅에 대한 설명이 들어감</p>
<p>예를 들면 아래와 같은 구조</p>
<blockquote>
<h1 id="usescrolltop">useScrollTop</h1>
</blockquote>
<h2 id="훅의-목적">훅의 목적</h2>
<p>스크롤 위치에 따라 상단 이동 버튼을 보여주고, 버튼 클릭 시 지정된 위치로 스크롤을 이동시키기 위한 훅입니다.</p>
<h2 id="사용-상황">사용 상황</h2>
<p>긴 리스트나 상세 페이지처럼 사용자가 아래로 많이 스크롤하는 화면에서 사용할 수 있습니다.</p>
<h2 id="사용-예시">사용 예시</h2>
<p>...</p>
<h2 id="전달-인자">전달 인자</h2>
<p>...</p>
<h2 id="반환값">반환값</h2>
<p>...</p>
<blockquote>
</blockquote>
<p>즉, 모듈 코드가 변경되면 그에 맞는 문서도 같이 갱신되도록 만드는 것이 목표다.</p>
<p>이 두 번째 작업은 다음 게시글에서 자세히 다룰 예정이다.</p>
<blockquote>
<p>다음 게시글: 링크 추가 예정</p>
</blockquote>
<hr>
<h2 id="최종-목표">최종 목표</h2>
<p>최종적으로 만들고 싶은 흐름은 다음과 같다.</p>
<pre><code class="language-tsx">1. 벽촌 프론트엔드 모듈에서 코드 수정 또는 신규 모듈 추가
2. main 머지
3. changeset 기반 version package PR 생성
4. version package PR 머지
5. npm 배포
6. 배포 완료 후 인젠스로타 레포지토리에 PR 생성
7. 변경된 모듈에 해당하는 md 설명서 자동 생성
8. 홈페이지에서 해당 md 파일을 읽어 문서 페이지로 노출</code></pre>
<p>이렇게 되면 모듈을 수정하거나 새로 추가할 때마다 문서를 따로 수동으로 작성하지 않아도 된다.</p>
<p>물론 처음부터 완벽한 문서를 만들기는 어렵겠지만, 최소한 기본 설명서 초안을 자동으로 만들 수 있다면 이후 수정 작업은 훨씬 줄어든다.</p>
<hr>
<h2 id="이-작업을-통해-기대하는-점">이 작업을 통해 기대하는 점</h2>
<p>이번 자동화를 통해 기대하는 점은 크게 세 가지다.</p>
<h3 id="1-문서-누락-방지">1. 문서 누락 방지</h3>
<p>공통 모듈은 만들어졌지만 사용법이 정리되어 있지 않으면, 결국 만든 사람만 사용할 수 있는 코드가 되기 쉽다.</p>
<p>자동으로 문서 초안이 생성되면 최소한 문서가 아예 없는 상태는 막을 수 있다.</p>
<h3 id="2-모듈-사용성-개선">2. 모듈 사용성 개선</h3>
<p>문서가 홈페이지에 정리되어 있으면 팀원들이 훨씬 쉽게 모듈을 찾아보고 사용할 수 있다.</p>
<p>특히 <code>hooks</code>, <code>utils</code>처럼 사용 빈도가 높은 모듈은 예시 코드와 함께 정리되어 있는 것만으로도 재사용성이 크게 좋아진다.</p>
<h3 id="3-유지보수-비용-감소">3. 유지보수 비용 감소</h3>
<p>모듈이 변경될 때마다 사람이 직접 문서를 찾아 수정하는 방식은 오래 유지하기 어렵다.</p>
<p>코드 변경 → 배포 → 문서 PR 생성까지 자동화해두면 문서 관리 비용을 줄일 수 있다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 작업은 단순히 md 파일을 자동으로 만드는 작업이라기보다는, 공통 모듈의 배포 흐름과 문서화 흐름을 연결하는 작업이다.</p>
<p>기존에는 모듈 배포와 문서 관리가 따로 움직임</p>
<p>앞으로는 모듈이 배포되면, 그 변경사항을 기반으로 회사 홈페이지 문서도 같이 갱신되는 구조</p>
<p>이번 글에서는 전체 구조와 작업 방향을 정리했고, 실제 구현은 두 단계로 나눠서 진행</p>
<ol>
<li>npm 배포 완료 후 인젠스로타 레포지토리에 PR을 여는 작업</li>
<li>열린 PR 안에서 변경된 모듈에 대한 md 문서를 자동 생성하는 작업</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[OTA 업데이트 적용 개선 정리]]></title>
            <link>https://velog.io/@miin-hyukkk/OTA-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%A0%81%EC%9A%A9-%EA%B0%9C%EC%84%A0-%EC%A0%95%EB%A6%AC%EA%B0%9C%EC%84%A0-%EB%B2%84%EC%A0%84-%EC%B6%94%ED%9B%84-%EC%A0%95%EB%A6%AC-%EC%8B%9C-%EB%91%98-%EC%B7%A8%ED%95%A9%ED%95%B4%EC%84%9C-%ED%95%98%EB%82%98%EB%A1%9C-%EB%B3%91%ED%95%A9%ED%95%98%EC%85%88</link>
            <guid>https://velog.io/@miin-hyukkk/OTA-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%A0%81%EC%9A%A9-%EA%B0%9C%EC%84%A0-%EC%A0%95%EB%A6%AC%EA%B0%9C%EC%84%A0-%EB%B2%84%EC%A0%84-%EC%B6%94%ED%9B%84-%EC%A0%95%EB%A6%AC-%EC%8B%9C-%EB%91%98-%EC%B7%A8%ED%95%A9%ED%95%B4%EC%84%9C-%ED%95%98%EB%82%98%EB%A1%9C-%EB%B3%91%ED%95%A9%ED%95%98%EC%85%88</guid>
            <pubDate>Fri, 24 Apr 2026 03:07:09 GMT</pubDate>
            <description><![CDATA[<h1 id="ota-업데이트-적용-개선-정리">OTA 업데이트 적용 개선 정리</h1>
<h2 id="1-기존-ota-기능-구현-상태">1. 기존 OTA 기능 구현 상태</h2>
<p>기존 앱은 <code>expo-updates</code>를 사용하고 있어서, 스토어 심사 없이도 JS 코드 변경사항을 OTA로 배포할 수 있는 상태였다.</p>
<p>즉 운영 중 긴급 수정이 필요할 때, 앱스토어/플레이스토어 재심사를 기다리지 않고 빠르게 반영할 수 있다는 장점이 있었다.</p>
<p>하지만 여기서 중요한 점은 <strong>OTA 배포와 OTA 적용은 같은 일이 아니라는 것</strong>이다.</p>
<p>React Native 앱은 실행 시점에 <strong>하나의 JS 번들</strong>을 메모리에 올려서 동작한다.</p>
<p>그래서 서버에 새 OTA를 배포했다고 해도, 이미 실행 중인 앱이 그 새 JS 번들로 자동 교체되지는 않는다.</p>
<p>즉 실제 적용은 아래 흐름을 거쳐야 한다.</p>
<pre><code class="language-tsx">const update = await Updates.checkForUpdateAsync();

if (update.isAvailable) {
  await Updates.fetchUpdateAsync();
  await Updates.reloadAsync();
}</code></pre>
<p>정리하면:</p>
<ul>
<li>OTA를 배포했다고 바로 화면이 바뀌는 것은 아님</li>
<li>앱이 새 JS 번들을 확인해야 하고</li>
<li>그 번들을 다운로드해야 하고</li>
<li>마지막으로 다시 로드되어야 실제 화면에 반영됨</li>
</ul>
<hr>
<h2 id="2-실제-사용자-불편"><strong>2. 실제 사용자 불편</strong></h2>
<p>상용 앱을 운영하다 보면 심사 없이 빠르게 반영해야 하는 수정이 자주 생긴다.</p>
<p>문제는 OTA를 배포해도 사용자 입장에서는 다음과 같은 불편이 생길 수 있다는 점이었다.</p>
<ul>
<li>배포는 끝났는데 앱을 껐다 켜기 전까지 변경사항이 안 보임</li>
<li>어떤 경우에는 앱을 한 번 켰다가 다시 꺼야 OTA가 적용됨</li>
<li>스토어 업데이트나 재설치 직후에도 첫 실행에서는 이전 화면이 보이고, 다음 실행에서야 바뀌는 경우가 있었음</li>
<li>사용자 입장에서는 “업데이트됐다면서 왜 안 바뀌지?”라고 느끼게 됨</li>
</ul>
<p>즉 운영 입장에서는 배포가 끝났는데, 사용자 입장에서는 적용이 지연되는 문제가 있었고, 이게 UX 측면에서 꽤 좋지 않았다.</p>
<hr>
<h2 id="3-해결책-모색"><strong>3. 해결책 모색</strong></h2>
<p>이 문제를 해결하기 위해 처음에는 몇 가지 방향을 고민했다.</p>
<ul>
<li>앱 시작 시 OTA를 먼저 확인하고 바로 적용</li>
<li>앱이 백그라운드에 오래 있었을 때 다시 OTA를 확인</li>
<li>기준 시간이 지나면 앱을 강제로 종료</li>
<li>업데이트가 없더라도 일단 리로드</li>
</ul>
<p>이 중에서 가장 중요한 판단 기준은 UX였다.</p>
<h3 id="31-앱-강제-종료는-적절하지-않다고-판단"><strong>3.1 앱 강제 종료는 적절하지 않다고 판단</strong></h3>
<p>백그라운드에 오래 있었다고 앱을 강제로 종료하는 방식은 일반 서비스 앱 UX로는 어색하다고 봤다.</p>
<p>이유:</p>
<ul>
<li>사용자는 결제, 카톡, 지도, 사진첩 등으로 잠깐 다른 앱에 갔다 올 수 있음</li>
<li>돌아왔는데 앱이 아예 종료돼 있으면 맥락이 끊김</li>
<li>보안 앱이 아니라면 “종료”보다 “필요할 때 다시 로드”가 더 자연스러움</li>
</ul>
<p>그래서 방향은 아래처럼 정리했다.</p>
<ul>
<li>앱 시작 시 OTA를 먼저 확인</li>
<li>백그라운드 복귀 시에도 조건부로 OTA를 확인</li>
<li>업데이트가 있을 때만 리로드</li>
<li>업데이트가 없으면 그대로 유지</li>
</ul>
<h3 id="32-timeout-정책"><strong>3.2 timeout 정책</strong></h3>
<p>백그라운드 체류 시간을 어느 정도로 볼지도 정리가 필요했다.</p>
<p>이 부분은 기획자와 상의한 뒤, <strong>상용 기준 timeoutMs는 1시간</strong>으로 정리했다.</p>
<p>테스트 과정에서는 더 짧은 값으로 검증할 수 있지만, 실제 운영 기준은 1시간으로 두는 방향이다.</p>
<p>구조도 timeout 값을 루트에서 넘기는 형태로 만들어서 이후 조정이 쉽도록 했다.</p>
<hr>
<h2 id="4-해결책을-찾고-구현한-방향"><strong>4. 해결책을 찾고 구현한 방향</strong></h2>
<p>최종적으로는 <strong>앱 시작 시 OTA 확인</strong>과 <strong>백그라운드 복귀 시 OTA 확인</strong>을 분리해서 구현했다.</p>
<h3 id="41-앱-시작-시-ota-확인"><strong>4.1 앱 시작 시 OTA 확인</strong></h3>
<p>앱 시작 시에는 useLaunchOtaReload 훅에서 OTA를 확인하도록 했다.</p>
<p>핵심 흐름은 아래와 같다.</p>
<p>`if (!Updates.isEnabled) {
  setIsLaunchGateVisible(false);
  return;
}</p>
<p>const update = await Updates.checkForUpdateAsync();</p>
<p>if (!update.isAvailable) {
  setIsLaunchGateVisible(false);
  return;
}</p>
<p>await Updates.fetchUpdateAsync();
await reloadApp();`</p>
<p>의도:</p>
<ul>
<li>앱이 시작되면 OTA를 먼저 확인</li>
<li>새 OTA가 있으면 다운로드 후 바로 리로드</li>
<li>없으면 그냥 앱 진입</li>
</ul>
<h3 id="42-launchgate를-둔-이유"><strong>4.2 LaunchGate를 둔 이유</strong></h3>
<p>앱 시작 시 메인 UI를 먼저 렌더링한 뒤 OTA를 체크하면,</p>
<p>사용자 입장에서는 앱이 한 번 떴다가 다시 켜지는 느낌이 날 수 있다.</p>
<p>그래서 메인 화면 대신 잠깐 LaunchGate를 보여주고, 그 사이에 OTA를 확인하도록 했다.</p>
<p>즉 흐름은:</p>
<ol>
<li>앱 시작</li>
<li>LaunchGate 표시</li>
<li>OTA 확인</li>
<li>업데이트 있으면 다운로드 후 리로드</li>
<li>업데이트 없으면 게이트 종료 후 메인 UI 진입</li>
</ol>
<p>이렇게 하면 “앱이 두 번 켜지는 느낌”을 줄일 수 있다.</p>
<hr>
<h2 id="5-백그라운드-복귀-시-ota-적용"><strong>5. 백그라운드 복귀 시 OTA 적용</strong></h2>
<p>앱이 백그라운드에 있다가 다시 돌아왔을 때도, 오래 머물렀다면 OTA를 다시 확인하도록 했다.</p>
<p>핵심 코드는 아래 흐름이다.</p>
<p>`if (nextAppState === &#39;background&#39; &amp;&amp; prevAppState !== &#39;background&#39;) {
  backgroundEnteredAtRef.current = Date.now();
  return;
}</p>
<p>if (
  prevAppState !== &#39;background&#39; ||
  nextAppState !== &#39;active&#39; ||
  backgroundEnteredAtRef.current === null ||
  isReloadingRef.current
) {
  return;
}</p>
<p>const elapsedMs = Date.now() - backgroundEnteredAtRef.current;
backgroundEnteredAtRef.current = null;</p>
<p>if (elapsedMs &lt;= timeoutMs) return;</p>
<p>const update = await Updates.checkForUpdateAsync();
if (!update.isAvailable) return;</p>
<p>await Updates.fetchUpdateAsync();
await reloadApp();`</p>
<p>의도:</p>
<ul>
<li>앱이 background로 내려갈 때 시간 기록</li>
<li>다시 active가 되었을 때만 계산</li>
<li>기준 시간 이하라면 아무 것도 안 함</li>
<li>기준 시간 초과라면 OTA 확인</li>
<li>새 업데이트가 있을 때만 다운로드 후 리로드</li>
</ul>
<p>즉 <strong>백그라운드 오래 있었다고 무조건 리로드하는 구조는 아니다.</strong></p>
<hr>
<h2 id="6-적용하다-보며-생긴-의문과-정리"><strong>6. 적용하다 보며 생긴 의문과 정리</strong></h2>
<h3 id="61-배포했는데-왜-바로-안-바뀌지"><strong>6.1 “배포했는데 왜 바로 안 바뀌지?”</strong></h3>
<p>이건 JS 번들 구조 때문이라고 이해했다.</p>
<ul>
<li>앱은 실행될 때 하나의 JS 번들을 메모리에 올림</li>
<li>OTA 배포는 서버에 새 번들을 올리는 일</li>
<li>이미 실행 중인 앱이 그 번들로 자동 교체되는 것은 아님</li>
<li>결국 새 번들을 받아오고 다시 로드해야 반영됨</li>
</ul>
<p>즉 “배포 완료”와 “사용자 화면 반영 완료”는 다르다.</p>
<hr>
<h3 id="62-백그라운드-시간-기록은-성능-부담이-크지-않나"><strong>6.2 백그라운드 시간 기록은 성능 부담이 크지 않나?</strong></h3>
<p>결론은 <strong>크지 않다</strong>였다.</p>
<p>이유:</p>
<ul>
<li>AppState는 상태가 바뀔 때만 이벤트 발생</li>
<li>하는 일은 Date.now() 한 번 기록하는 정도</li>
<li>useRef에 저장하므로 리렌더도 없음</li>
</ul>
<p>즉 부담이 큰 부분은 시간 기록이 아니라,</p>
<p>실제 OTA 확인/다운로드/리로드 쪽이다.</p>
<hr>
<h3 id="63-기준-시간을-넘겼는데-업데이트가-없으면-리로드되나"><strong>6.3 기준 시간을 넘겼는데 업데이트가 없으면 리로드되나?</strong></h3>
<p>이 부분은 중요한 요구사항이었다.</p>
<p>결론은 <strong>리로드되지 않도록 정리했다.</strong></p>
<p>현재 흐름:</p>
<ul>
<li>기준 시간 이하 -&gt; 리로드 안 함</li>
<li>기준 시간 초과 + 업데이트 없음 -&gt; 리로드 안 함</li>
<li>기준 시간 초과 + OTA 확인 실패 -&gt; 리로드 안 함</li>
<li>기준 시간 초과 + 업데이트 있음 -&gt; 다운로드 후 리로드</li>
</ul>
<p>즉 “업데이트 있을 때만 리로드” 요구사항에 맞췄다.</p>
<hr>
<h3 id="64-앱-시작-ota와-백그라운드-ota가-겹쳐서-두-번-리로드되지-않나"><strong>6.4 앱 시작 OTA와 백그라운드 OTA가 겹쳐서 두 번 리로드되지 않나?</strong></h3>
<p>이 부분도 실제로 우려했던 포인트였다.</p>
<p>결론은 보통 <strong>두 번 리로드되지는 않는다.</strong></p>
<p>이유:</p>
<ul>
<li>앱 시작 시에는 LaunchGate가 떠 있는 동안 useBackgroundTimeoutReset를 비활성화</li>
<li>그래서 시작 중에는 background 훅이 개입하지 않음</li>
<li>background 훅으로 리로드가 일어난 뒤 앱이 다시 시작되면 useLaunchOtaReload가 다시 체크하긴 함</li>
<li>하지만 이미 최신 OTA로 올라온 상태라면 보통 업데이트 없음으로 끝남</li>
</ul>
<p>즉:</p>
<ul>
<li>일반적으로는 1번 리로드 + 1번 재체크</li>
<li>보통은 추가 리로드까지 이어지지 않음</li>
</ul>
<p>둘이 겹치지 않도록 실제 연결도 아래처럼 구성했다.</p>
<p>`const { isLaunchGateVisible } = useLaunchOtaReload();</p>
<p>useBackgroundTimeoutReset({
  timeoutMs: BACKGROUND_TIMEOUT_MS,
  enabled: !isLaunchGateVisible,
});`</p>
<p>의도:</p>
<ul>
<li>LaunchGate가 보이는 동안에는 background 훅 비활성화</li>
<li>시작 OTA와 복귀 OTA가 동시에 개입하지 않도록 제어</li>
</ul>
<hr>
<h3 id="65-안드로이드-첫-실행에서-background-훅이-잘못-동작한-이유는"><strong>6.5 안드로이드 첫 실행에서 background 훅이 잘못 동작한 이유는?</strong></h3>
<p>실제로 안드로이드에서 앱을 처음 설치해 실행했는데,</p>
<p>초기 상태 변화가 background -&gt; active처럼 인식되는 문제가 있었다.</p>
<p>그래서 두 가지를 정리했다.</p>
<ol>
<li>LaunchGate가 내려간 뒤에만 background 훅 활성화</li>
<li>진짜 background -&gt; active 전환일 때만 복귀로 인정</li>
</ol>
<p>핵심 코드는 아래다.</p>
<p>`const prevAppState = appStateRef.current;
appStateRef.current = nextAppState;</p>
<p>if (nextAppState === &#39;background&#39; &amp;&amp; prevAppState !== &#39;background&#39;) {
  backgroundEnteredAtRef.current = Date.now();
  return;
}</p>
<p>if (
  prevAppState !== &#39;background&#39; ||
  nextAppState !== &#39;active&#39; ||
  backgroundEnteredAtRef.current === null ||
  isReloadingRef.current
) {
  return;
}`</p>
<p>이렇게 해서 첫 실행 중 발생하는 초기 상태 변화는 무시하고,</p>
<p>실제로 백그라운드에 갔다 온 경우만 감지하도록 만들었다.</p>
<hr>
<h2 id="7-최종적으로-구현한-내용-정리"><strong>7. 최종적으로 구현한 내용 정리</strong></h2>
<h3 id="71-앱-시작-시"><strong>7.1 앱 시작 시</strong></h3>
<ul>
<li>useLaunchOtaReload에서 OTA 확인</li>
<li>LaunchGate를 먼저 보여줌</li>
<li>업데이트가 있으면 다운로드 후 리로드</li>
<li>업데이트가 없으면 메인 UI 진입</li>
</ul>
<h3 id="72-백그라운드-복귀-시"><strong>7.2 백그라운드 복귀 시</strong></h3>
<ul>
<li>앱이 background로 갈 때 시간 기록</li>
<li>다시 active로 돌아올 때 체류 시간 계산</li>
<li>기획자와 협의한 상용 기준 timeout은 1시간</li>
<li>기준 시간 초과 + OTA 있음 -&gt; 다운로드 후 리로드</li>
<li>업데이트 없음 / 확인 실패 -&gt; 리로드 안 함</li>
</ul>
<h3 id="73-둘이-겹치지-않도록-한-점"><strong>7.3 둘이 겹치지 않도록 한 점</strong></h3>
<ul>
<li>LaunchGate가 보이는 동안에는 background 훅 비활성화</li>
<li>이전 AppState를 기억해서 진짜 background -&gt; active만 처리</li>
<li>isReloadingRef로 같은 흐름의 중복 실행 방지</li>
<li>background 리로드 후 launch 훅이 다시 체크하더라도, 보통은 추가 리로드 없이 종료</li>
</ul>
<hr>
<h2 id="8-결론"><strong>8. 결론</strong></h2>
<p>이번 작업의 핵심은 단순히 OTA를 쓰는 것이 아니라,</p>
<p><strong>사용자가 직접 앱을 껐다 켜지 않아도 최대한 자연스럽게 최신 JS 번들이 적용되도록 만드는 것</strong>이었다.</p>
<p>정리하면:</p>
<ul>
<li>기존 OTA 기능은 이미 가능했지만 적용 UX가 좋지 않았음</li>
<li>사용자들은 앱을 껐다 켜야 반영되는 점에서 불편을 겪었음</li>
<li>앱 시작 시 OTA 확인 + 백그라운드 복귀 시 OTA 확인 구조로 개선</li>
<li>LaunchGate를 두어 시작 시 깜빡임/이중 진입 느낌을 줄임</li>
<li>background timeout은 기획자와 협의해 상용 기준 1시간으로 정리</li>
<li>두 훅이 겹치지 않도록 제어했고, 우려되던 포인트들도 코드 기준으로 모두 확인함</li>
</ul>
<p>즉 이번 개선은</p>
<p><strong>“배포는 되었지만 반영은 늦는 문제”를 줄이고, 사용자가 더 자연스럽게 최신 화면을 보게 만드는 방향으로 OTA 적용 흐름을 정리한 작업</strong>이라고 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OTA 업데이트 적용 흐름 정리]]></title>
            <link>https://velog.io/@miin-hyukkk/OTA-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%A0%81%EC%9A%A9-%ED%9D%90%EB%A6%84-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@miin-hyukkk/OTA-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%A0%81%EC%9A%A9-%ED%9D%90%EB%A6%84-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 24 Apr 2026 01:31:30 GMT</pubDate>
            <description><![CDATA[<h2 id="0-실제로-있었던-사용자-불편">0. 실제로 있었던 사용자 불편</h2>
<p>상용 앱을 운영하다 보면, 앱 심사를 다시 거치지 않고 빠르게 반영해야 하는 수정이 생긴다.<br>이럴 때 OTA 업데이트는 굉장히 유용하지만, 실제 사용자 입장에서는 아래 같은 불편이 발생할 수 있었다.</p>
<ul>
<li>수정사항이 배포되었는데도 앱을 껐다 켜기 전까지는 반영되지 않음</li>
<li>어떤 경우에는 앱을 한 번 실행한 뒤, 다시 껐다 켜야 그제서야 OTA가 적용됨</li>
<li>사용자는 이미 최신이라고 생각하는데 화면은 여전히 예전 UI를 보여줌</li>
<li>운영자는 “배포는 끝났는데 왜 안 바뀌냐”는 문의를 받게 됨</li>
</ul>
<p>예를 들어 운영 중 <code>로그인</code> 텍스트를 긴급하게 수정해 <code>로그인 2</code>로 반영했다고 해도, 사용자가 앱을 다시 실행하지 않으면 이전 번들이 계속 보일 수 있다.<br>심지어 앱을 새로 설치하거나 업데이트한 직후에도, 첫 실행에서는 이전 JS 번들이 올라오고 그 다음 실행에서야 최신 OTA가 적용되는 상황이 생길 수 있다.</p>
<p>이 부분이 UX 측면에서 특히 좋지 않았던 이유는, 사용자가 보기엔 “업데이트가 됐다면서 왜 화면은 안 바뀌지?” 혹은 “왜 한 번 더 껐다 켜야 하지?”처럼 느껴지기 때문이다.</p>
<hr>
<h2 id="1-왜-이런-일이-생기는가">1. 왜 이런 일이 생기는가</h2>
<p>이 현상을 이해하려면 <strong>앱이 실행될 때 어떤 코드가 실제로 올라가는지</strong>를 먼저 봐야 한다.</p>
<p>React Native + Expo 환경에서 화면을 그리는 핵심은 결국 <strong>JS 번들(JavaScript bundle)</strong> 이다.<br>앱이 켜질 때는 이 JS 번들이 로드되어 실행되고, 사용자는 그 번들 기준의 화면을 보게 된다.</p>
<p>여기서 중요한 점은 다음과 같다.</p>
<h3 id="11-앱은-이미-메모리에-올라간-js-번들로-동작한다">1.1 앱은 이미 메모리에 올라간 JS 번들로 동작한다</h3>
<p>앱이 한 번 실행되면, 그 시점에 로드된 JS 번들이 메모리에 올라가서 계속 동작한다.<br>즉, 앱이 켜진 뒤에 서버에 더 새로운 OTA가 생긴다고 해서, 현재 실행 중인 JS 런타임이 자동으로 그 코드로 바뀌지는 않는다.</p>
<p>정리하면:</p>
<ul>
<li>앱 실행 시점에 어떤 JS 번들이 올라왔는지가 중요함</li>
<li>실행 도중 새 OTA를 받아도, 현재 런타임은 기존 번들로 계속 동작할 수 있음</li>
<li>새 번들을 실제로 쓰려면 <code>reload</code> 또는 다음 앱 실행이 필요함</li>
</ul>
<h3 id="12-ota는-배포와-적용이-같은-일이-아니다">1.2 OTA는 “배포”와 “적용”이 같은 일이 아니다</h3>
<p>OTA를 publish 했다고 해서 바로 사용자의 화면이 바뀌는 것은 아니다.</p>
<p>중간 단계가 필요하다.</p>
<ol>
<li>앱이 OTA 서버에 새 업데이트가 있는지 확인</li>
<li>있으면 그 번들을 다운로드</li>
<li>그 번들을 실제로 사용하도록 앱을 다시 로드</li>
</ol>
<p>즉, OTA는 크게 두 단계로 나뉜다.</p>
<ul>
<li><strong>배포:</strong> 서버에 새 JS 번들을 올림</li>
<li><strong>적용:</strong> 사용자의 앱이 그 번들을 내려받고 실제로 다시 실행함</li>
</ul>
<p>여기서 사용자가 “배포했는데 왜 안 바뀌냐”라고 느끼는 이유가 생긴다.<br>운영자 입장에서는 배포가 끝났지만, 사용자 앱 입장에서는 아직 적용이 끝난 게 아니기 때문이다.</p>
<pre><code>const update = await Updates.checkForUpdateAsync();

if (update.isAvailable) {
  await Updates.fetchUpdateAsync();
  await Updates.reloadAsync();
}
</code></pre><h3 id="13-앱-설치-직후--스토어-업데이트-직후에도-같은-문제가-생길-수-있다">1.3 앱 설치 직후 / 스토어 업데이트 직후에도 같은 문제가 생길 수 있다</h3>
<p>이 부분도 헷갈리기 쉬웠다.</p>
<p>앱을 새로 설치하거나 스토어 업데이트를 받더라도, 앱이 처음 켜질 때는 보통 그 시점에 포함된 <strong>내장 번들(embedded bundle)</strong> 이나 이미 저장된 번들로 실행될 수 있다.<br>그리고 앱이 켜진 뒤 OTA를 확인해서 최신 JS 번들을 다운로드할 수 있다.</p>
<p>그런데 이 시점에서 바로 <code>reload</code>를 하지 않으면,</p>
<ul>
<li>첫 실행은 예전 번들</li>
<li>두 번째 실행에서야 새 OTA 반영</li>
</ul>
<p>같은 흐름이 생길 수 있다.</p>
<p>그래서 “업데이트를 했는데도 한 번 더 껐다 켜야 반영된다”는 상황이 실제로 발생할 수 있다.</p>
<hr>
<h2 id="2-이번에-해결하려던-문제">2. 이번에 해결하려던 문제</h2>
<p>이번 작업에서 해결하려던 건 단순히 “OTA를 쓰자”가 아니었다.<br>핵심은 <strong>사용자가 껐다 켜야만 반영되는 느낌을 줄이는 것</strong>이었다.</p>
<p>내가 정리한 문제는 크게 두 가지였다.</p>
<ul>
<li>앱 시작 시 새 OTA가 있으면 바로 적용되게 만들기</li>
<li>앱이 백그라운드에 오래 있다가 다시 돌아왔을 때도 새 OTA가 있으면 적용되게 만들기</li>
</ul>
<p>즉, 사용자가 굳이 수동으로 앱을 완전히 종료했다가 다시 실행하지 않아도, 가능한 자연스럽게 최신 번들을 보게 하는 흐름을 만드는 것이 목적이었다.</p>
<hr>
<h2 id="3-먼저-이해한-expo-updates-기본-흐름">3. 먼저 이해한 <code>expo-updates</code> 기본 흐름</h2>
<p><code>expo-updates</code>는 대략 아래 순서로 동작한다.</p>
<ol>
<li><code>checkForUpdateAsync()</code></li>
<li>새 업데이트가 있으면 <code>fetchUpdateAsync()</code></li>
<li>실제 반영은 <code>reloadAsync()</code></li>
</ol>
<p>이 흐름에서 핵심은, <strong>업데이트 확인과 실제 반영은 별개</strong>라는 점이다.</p>
<ul>
<li><p><code>checkForUpdateAsync()</code><br>서버에 새로운 OTA가 있는지 확인</p>
</li>
<li><p><code>fetchUpdateAsync()</code><br>새 OTA 번들을 내려받음</p>
</li>
<li><p><code>reloadAsync()</code><br>현재 앱을 새 번들 기준으로 다시 실행</p>
</li>
</ul>
<p>즉, 새 OTA를 다운로드했다고 해서 곧바로 화면이 바뀌는 것이 아니라,<br>결국 새 JS 번들을 쓰도록 다시 로드해야 한다.</p>
<hr>
<h2 id="4-dev--preview--production-차이도-중요했다">4. dev / preview / production 차이도 중요했다</h2>
<p>이 작업을 하면서 dev 환경과 실제 OTA 환경은 동일하지 않다는 점도 분명히 봐야 했다.</p>
<h3 id="dev-환경">dev 환경</h3>
<ul>
<li><code>expo-updates</code>가 실제 OTA처럼 동작하지 않을 수 있음</li>
<li><code>Updates.isEnabled</code>가 <code>false</code>일 수 있음</li>
<li><code>checkForUpdateAsync()</code>를 실제 OTA 검증 기준으로 그대로 믿기 어려움</li>
</ul>
<h3 id="preview--production-환경">preview / production 환경</h3>
<ul>
<li>실제 EAS Update 채널 기준으로 OTA 동작 확인 가능</li>
<li>내가 원하는 “배포 후 사용자 앱에서 내려받고 적용” 흐름은 여기서 봐야 함</li>
</ul>
<p>즉, <strong>진짜 OTA 검증은 preview / production에서 보는 게 맞다.</strong></p>
<hr>
<h2 id="5-처음-고민했던-ux-방향">5. 처음 고민했던 UX 방향</h2>
<p>처음에는 여러 선택지가 있었다.</p>
<ul>
<li>앱 시작 시 OTA 있으면 바로 적용</li>
<li>백그라운드 오래 있었으면 그냥 리로드</li>
<li>백그라운드 오래 있었으면 앱을 아예 종료</li>
<li>앱 시작 후 메인 화면을 먼저 보여주고 나중에 OTA 체크</li>
</ul>
<p>정리해보니 UX상 가장 자연스러운 방향은 아래였다.</p>
<ul>
<li>앱 시작 시 OTA를 먼저 확인</li>
<li>백그라운드에 오래 있었다가 돌아오면 OTA를 다시 확인</li>
<li>업데이트가 있을 때만 리로드</li>
<li>업데이트가 없으면 앱은 그대로 유지</li>
<li>앱을 강제로 종료하지는 않음</li>
</ul>
<p>특히 “오래 있었으니 앱을 죽인다”는 방식은 일반 서비스 앱 UX로는 어색했다.<br>보안 앱이라면 가능하지만, 일반 서비스 앱에서는 사용자가 “왜 앱이 종료됐지?”라고 느낄 가능성이 크다.</p>
<hr>
<h2 id="6-그래서-최종-구조는-이렇게-정리했다">6. 그래서 최종 구조는 이렇게 정리했다</h2>
<h3 id="a-앱-시작-시-ota-확인">A. 앱 시작 시 OTA 확인</h3>
<p><code>useLaunchOtaReload</code></p>
<p>역할:</p>
<ul>
<li>앱 시작 시 1회 OTA 확인</li>
<li>업데이트가 있으면 다운로드 후 리로드</li>
<li>없으면 그냥 앱 진입</li>
</ul>
<pre><code>if (!Updates.isEnabled) {
  setIsLaunchGateVisible(false);
  return;
}

const update = await Updates.checkForUpdateAsync();

if (!update.isAvailable) {
  setIsLaunchGateVisible(false);
  return;
}

await Updates.fetchUpdateAsync();
await reloadApp();
</code></pre><h3 id="b-백그라운드-복귀-시-ota-확인">B. 백그라운드 복귀 시 OTA 확인</h3>
<p><code>useBackgroundTimeoutReset</code></p>
<p>역할:</p>
<ul>
<li>앱이 백그라운드로 내려갈 때 시간 기록</li>
<li>다시 <code>active</code>가 되었을 때 체류 시간 계산</li>
<li>기준 시간 초과 + OTA 있음 -&gt; 다운로드 후 리로드</li>
<li>OTA 없음 -&gt; 그대로 유지</li>
</ul>
<pre><code>if (nextAppState === &#39;background&#39; &amp;&amp; prevAppState !== &#39;background&#39;) {
  backgroundEnteredAtRef.current = Date.now();
  return;
}

if (
  prevAppState !== &#39;background&#39; ||
  nextAppState !== &#39;active&#39; ||
  backgroundEnteredAtRef.current === null ||
  isReloadingRef.current
) {
  return;
}

const elapsedMs = Date.now() - backgroundEnteredAtRef.current;
backgroundEnteredAtRef.current = null;

if (elapsedMs &lt;= timeoutMs) {
  return;
}

const update = await Updates.checkForUpdateAsync();

if (update.isAvailable) {
  await Updates.fetchUpdateAsync();
  await reloadApp();
}
</code></pre><h3 id="c-시작-게이트-화면">C. 시작 게이트 화면</h3>
<p><code>LaunchGate</code></p>
<p>역할:</p>
<ul>
<li>앱 시작 직후 메인 UI를 바로 보여주지 않음</li>
<li>OTA 확인이 끝날 때까지 중간 게이트 역할</li>
<li>사용자 입장에서 “앱이 두 번 켜지는 느낌”을 줄여줌</li>
</ul>
<hr>
<h2 id="7-왜-launchgate가-필요했는가">7. 왜 <code>LaunchGate</code>가 필요했는가</h2>
<p>처음에는 <code>useLaunchOtaReload</code>만 있으면 된다고 생각했지만, 그렇게 하면 문제가 있었다.</p>
<p>흐름이 이렇게 된다.</p>
<ol>
<li>기존 번들로 앱 화면이 먼저 뜸</li>
<li>그 뒤 OTA 체크</li>
<li>새 OTA 있으면 다운로드</li>
<li>다시 리로드</li>
</ol>
<p>이 경우 사용자는 “앱이 한 번 떴다가 다시 켜지는 느낌”을 받을 수 있다.<br>그래서 메인 UI를 먼저 보여주지 않고, <code>LaunchGate</code>를 먼저 보여주면서 OTA를 확인하도록 바꿨다.</p>
<p>이렇게 하면 사용자는</p>
<ul>
<li>앱이 아직 준비 중이라고 느끼고</li>
<li>메인 화면이 깜빡이며 두 번 뜨는 느낌은 줄어든다</li>
</ul>
<hr>
<h2 id="8-showsplash-이미지에-의존하면-안-된다고-생각한-이유">8. <code>showSplash</code> 이미지에 의존하면 안 된다고 생각한 이유</h2>
<p>처음엔 “기존 스플래시 이미지를 그대로 더 오래 보여주면 되지 않나?”라고 생각할 수 있었다.<br>하지만 장기적으로는 그 방식이 핵심은 아니라고 봤다.</p>
<p>이유:</p>
<ul>
<li>특정 스플래시 이미지는 나중에 바뀔 수 있음</li>
<li>OTA 로직이 특정 디자인 자산에 종속되면 유지보수가 애매해짐</li>
<li>핵심은 이미지가 아니라 “메인 UI 진입 전에 잠깐 막아주는 gate” 구조임</li>
</ul>
<p>즉, 개념적으로는 <strong>스플래시 이미지 연장</strong>이 아니라 <strong>런치 게이트</strong>가 핵심이다.<br>다만 UX상 자연스럽게 보이게 하기 위해 실제 화면은 기존 스플래시와 연결되는 형태로 맞췄다.</p>
<hr>
<h2 id="9-백그라운드-시간-기록-자체는-부담이-큰가">9. 백그라운드 시간 기록 자체는 부담이 큰가?</h2>
<p>이 부분도 따로 생각해봤는데, 결론은 <strong>거의 부담이 없다</strong>였다.</p>
<p>이유:</p>
<ul>
<li><code>AppState</code> 이벤트는 상태가 바뀔 때만 발생</li>
<li>백그라운드 진입 시 <code>Date.now()</code> 한 번 기록하는 정도</li>
<li><code>useRef</code>에 저장하므로 리렌더도 없음</li>
</ul>
<p>즉, 성능적으로 비용이 큰 쪽은 시간 기록이 아니라 아래였다.</p>
<ul>
<li><code>Alert</code></li>
<li><code>checkForUpdateAsync()</code></li>
<li><code>fetchUpdateAsync()</code></li>
<li><code>reloadAsync()</code></li>
</ul>
<p>그래서 “백그라운드에 갈 때마다 시간 기록” 자체는 부담이 크지 않다고 정리했다.</p>
<hr>
<h2 id="10-안드로이드-첫-실행에서-background-훅이-잘못-동작한-이유">10. 안드로이드 첫 실행에서 background 훅이 잘못 동작한 이유</h2>
<p>실제로 안드로이드에서 앱을 처음 설치하고 실행했는데도 <code>백그라운드 복귀 감지</code> 훅이 동작하는 현상이 있었다.</p>
<p>원인을 보니:</p>
<ul>
<li>앱 시작 중 발생하는 초기 <code>AppState</code> 변화까지</li>
<li>훅이 복귀 이벤트처럼 받아버리고 있었다</li>
</ul>
<p>그래서 아래처럼 수정했다.</p>
<ul>
<li><code>LaunchGate</code>가 끝난 뒤에만 <code>useBackgroundTimeoutReset</code>이 활성화되도록 변경</li>
<li>훅 내부에서도 <code>background -&gt; active</code> 전환일 때만 진짜 복귀로 인정</li>
</ul>
<p>즉, 지금은 <strong>실제로 백그라운드에 갔다 온 경우만 감지</strong>하도록 더 엄격해졌다.</p>
<pre><code>if (nextAppState === &#39;background&#39; &amp;&amp; prevAppState !== &#39;background&#39;) {
  backgroundEnteredAtRef.current = Date.now();
  return;
}

if (
  prevAppState !== &#39;background&#39; ||
  nextAppState !== &#39;active&#39; ||
  backgroundEnteredAtRef.current === null ||
  isReloadingRef.current
) {
  return;
}

const elapsedMs = Date.now() - backgroundEnteredAtRef.current;
backgroundEnteredAtRef.current = null;

if (elapsedMs &lt;= timeoutMs) {
  return;
}

const update = await Updates.checkForUpdateAsync();

if (update.isAvailable) {
  await Updates.fetchUpdateAsync();
  await reloadApp();
}
</code></pre><hr>
<h2 id="11-안드로이드-3버튼-내비게이션-이슈도-있었다">11. 안드로이드 3버튼 내비게이션 이슈도 있었다</h2>
<p><code>LaunchGate</code>에서 스플래시 이미지를 중앙 정렬했더니, 안드로이드 3버튼 내비게이션 환경에서는 이미지가 위로 밀려 보였다.</p>
<p>이건 완전히 어쩔 수 없는 현상은 아니고, safe area 기준 정렬 때문에 생기는 시각적 문제였다.</p>
<p>그래서 아래처럼 보정했다.</p>
<ul>
<li>안드로이드 bottom inset 값을 읽음</li>
<li>스플래시 이미지를 그 값의 일부만큼 아래로 내림</li>
<li>로딩 인디케이터는 bottom inset 위로 올림</li>
</ul>
<p>즉, 기술적인 중앙이 아니라 <strong>사용자가 보기에 자연스러운 시각 중심</strong>으로 맞췄다.</p>
<hr>
<h2 id="12-지금-구조에서-진짜-2번-리로드가-일어나는가">12. 지금 구조에서 진짜 2번 리로드가 일어나는가</h2>
<p>처음에는 이런 걱정이 생겼다.</p>
<ul>
<li>백그라운드 훅이 OTA 확인 후 리로드</li>
<li>앱이 다시 시작됨</li>
<li>다시 시작되면 <code>useLaunchOtaReload</code>도 실행됨</li>
<li>그러면 리로드가 또 되는 것 아닌가?</li>
</ul>
<p>정리해보면 <strong>보통은 2번 리로드가 아니다.</strong></p>
<p>흐름:</p>
<ol>
<li>백그라운드 훅이 OTA 확인</li>
<li>새 OTA 있으면 다운로드 후 리로드</li>
<li>앱이 최신 번들로 다시 시작</li>
<li>시작 훅이 다시 OTA 확인</li>
<li>이미 최신이라면 “업데이트 없음”</li>
</ol>
<p>즉, <strong>1번 리로드 + 1번 재체크</strong>에 가깝다.<br>예외적으로 리로드 직후 더 새 OTA가 서버에 올라오면 한 번 더 리로드될 가능성은 있지만, 일반적인 흐름은 아니다.</p>
<hr>
<h2 id="13-preview-빌드에서-텍스트를-바꿔서-ota-테스트할-때-헷갈린-점">13. preview 빌드에서 텍스트를 바꿔서 OTA 테스트할 때 헷갈린 점</h2>
<p>예를 들어 <code>로그인</code>을 <code>로그인 2</code>로 바꿔서 확인한다고 했을 때, 여기서 중요한 건 “무엇을 테스트하는가”였다.</p>
<h3 id="그냥-새-preview-빌드를-만드는-경우">그냥 새 preview 빌드를 만드는 경우</h3>
<ul>
<li>새 빌드 안에 이미 <code>로그인 2</code> 코드가 들어감</li>
<li>설치하자마자 바로 <code>로그인 2</code>가 보임</li>
<li>이건 OTA 확인이 아니라 “새 바이너리 빌드 반영 확인”</li>
</ul>
<h3 id="ota-자체를-테스트하는-경우">OTA 자체를 테스트하는 경우</h3>
<ul>
<li>먼저 기존 코드가 들어간 preview 빌드를 설치</li>
<li>그 다음 같은 채널/runtime에 <code>로그인 2</code> 코드로 OTA publish</li>
<li>앱 실행 또는 복귀 시 OTA를 통해 <code>로그인 2</code>가 보이는지 확인</li>
</ul>
<p>즉, <strong>빌드 한 번으로 바로 <code>로그인 2</code>가 보인다면 그건 OTA가 아니라 새 빌드 결과일 가능성이 크다.</strong></p>
<hr>
<h2 id="14-현재-내가-이해한-최종-흐름">14. 현재 내가 이해한 최종 흐름</h2>
<h3 id="앱-시작-흐름">앱 시작 흐름</h3>
<ol>
<li>앱 시작</li>
<li><code>LaunchGate</code> 표시</li>
<li><code>useLaunchOtaReload</code> 실행</li>
<li>OTA 확인</li>
<li>업데이트 있으면 다운로드 후 리로드</li>
<li>업데이트 없으면 게이트 종료 후 메인 UI 진입</li>
</ol>
<h3 id="백그라운드-복귀-흐름">백그라운드 복귀 흐름</h3>
<ol>
<li>앱이 background로 내려갈 때 시간 기록</li>
<li>다시 active가 되면 체류 시간 계산</li>
<li>기준 시간 이하 -&gt; 아무 것도 안 함</li>
<li>기준 시간 초과 -&gt; OTA 확인</li>
<li>업데이트 있으면 다운로드 후 리로드</li>
<li>업데이트 없으면 현재 화면 유지</li>
</ol>
<hr>
<h2 id="15-지금-구조가-ux상-괜찮다고-본-이유">15. 지금 구조가 UX상 괜찮다고 본 이유</h2>
<p>최종적으로 이 구조가 괜찮다고 본 이유는 아래다.</p>
<ul>
<li>앱 시작 시 메인 화면이 먼저 뜨지 않아서 어색함이 줄어듦</li>
<li>백그라운드 복귀 시에도 무조건 리로드하지 않고 OTA 있을 때만 반영</li>
<li>앱을 강제로 종료하지 않아서 일반 서비스 앱 UX와 더 잘 맞음</li>
<li>안드로이드 첫 실행 오탐과 하단 내비게이션 시각 이슈도 같이 잡음</li>
</ul>
<p>즉, “무조건 새로고침”이 아니라<br><strong>필요할 때만, 사용자가 덜 어색하게 느끼는 방식으로 새 JS 번들을 적용하는 구조</strong>라고 이해하면 될 것 같다.</p>
<hr>
<h2 id="16-앞으로-남은-정리-포인트">16. 앞으로 남은 정리 포인트</h2>
<p>지금은 테스트를 위해 디버깅용 <code>Alert</code>가 들어가 있다.<br>테스트가 끝나면 제거할 예정이다.</p>
<p>남은 정리 포인트:</p>
<ul>
<li>디버깅용 <code>Alert</code> 제거</li>
<li>필요하면 background 리로드 직후 1회 launch OTA 체크 skip 여부 검토</li>
<li>상용 기준 timeout 값 재조정</li>
</ul>
<hr>
<h2 id="17-한-줄-결론">17. 한 줄 결론</h2>
<p>이번에 정리한 방향은 이렇다.</p>
<p><strong>OTA는 “배포했다고 바로 반영되는 것”이 아니라, 새 JS 번들을 확인하고 내려받은 뒤 다시 로드해야 적용된다. 그래서 앱 시작 시에는 LaunchGate에서 먼저 OTA를 확인하고, 백그라운드에서 오래 있었다가 돌아왔을 때도 OTA가 있을 때만 리로드하도록 구성해, 사용자가 굳이 여러 번 앱을 껐다 켜지 않아도 최대한 자연스럽게 최신 화면을 보게 하는 것이 목표였다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[결제 위젯 paymentsData 파싱 오류 해결 
]]></title>
            <link>https://velog.io/@miin-hyukkk/%EA%B2%B0%EC%A0%9C-%EC%9C%84%EC%A0%AFpaymentsData%ED%8C%8C%EC%8B%B1-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@miin-hyukkk/%EA%B2%B0%EC%A0%9C-%EC%9C%84%EC%A0%AFpaymentsData%ED%8C%8C%EC%8B%B1-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 22 Apr 2026 02:32:09 GMT</pubDate>
            <description><![CDATA[<h1 id="결제-위젯-paymentsdata-파싱-오류-해결-과정"><strong>결제 위젯 paymentsData 파싱 오류 해결 과정</strong></h1>
<h2 id="문제-상황"><strong>문제 상황</strong></h2>
<p>결제 위젯 URL에서 paymentsData에 들어가는 orderName 값이 아래처럼 %를 포함할 때 오류가 발생했다.</p>
<p><code>맛있는 다라딸기! 59%%%</code></p>
<p>실제 URL에서는 %가 URL 인코딩되어 아래처럼 보였다.</p>
<p><code>...paymentsData=%7B...%22orderName%22%3A%22...59%25%25%25%22%7D</code></p>
<p>이 상태에서 웹 결제 위젯 페이지가 파라미터를 파싱할 때 에러가 났다.</p>
<hr>
<h2 id="처음-확인한-내용"><strong>처음 확인한 내용</strong></h2>
<p>앱 쪽 결제 진입 코드는 아래 흐름이었다.</p>
<ol>
<li>Cart.tsx (line 285) 에서 결제 데이터 객체를 JSON.stringify(data)로 문자열화</li>
<li>Payments.tsx (line 46) 에서 encodeURIComponent(paymentsData)로 URL query param에 넣음</li>
<li>웹뷰가 /payments-widgets 페이지를 열어 해당 query param을 읽음</li>
</ol>
<p>즉 앱 쪽에서는 paymentsData를 <strong>정상적으로 한 번만 인코딩</strong>하고 있었다.</p>
<hr>
<h2 id="웹에서-확인한-원인"><strong>웹에서 확인한 원인</strong></h2>
<p>문제가 있던 웹 코드는 PaymentsWidgets.tsx (line 52) 의 이 부분이었다.</p>
<p><code>const parsedPaymentsData = JSON.parse(
  decodeURIComponent(paymentsDataParam),
);</code></p>
<p>여기서 핵심은:</p>
<ul>
<li>URLSearchParams.get(&#39;paymentsData&#39;) 는 이미 URL 디코딩이 끝난 값을 반환한다.</li>
<li>그런데 그 값을 다시 decodeURIComponent(...) 하면, JSON 안에 들어 있던 %%% 를 또 해석하려고 하면서 URI malformed 가 발생한다.</li>
</ul>
<p>즉 문제는 URL 자체가 아니라, <strong>웹에서 한 번 더 디코딩한 것</strong>이었다.</p>
<hr>
<h2 id="실제로-파라미터가-어떻게-보였는지"><strong>실제로 파라미터가 어떻게 보였는지</strong></h2>
<p>URL에는 이런 식으로 들어간다.</p>
<p><code>paymentsData=%7B%22orderId%22%3A%22ORD-20260422-0007%22%2C%22customerKey%22%3A%22USER-20251226-0004%22%2C%22orderName%22%3A%22맛있는+다라딸기!+59%25%25%25%22%7D</code></p>
<p>하지만 브라우저에서 아래 코드를 실행하면:</p>
<p><code>const searchParams = new URLSearchParams(window.location.search);
const paymentsDataParam = searchParams.get(&#39;paymentsData&#39;);</code></p>
<p>paymentsDataParam 값은 이미 이렇게 된다.</p>
<p><code>&#39;{&quot;orderId&quot;:&quot;ORD-20260422-0007&quot;,&quot;customerKey&quot;:&quot;USER-20251226-0004&quot;,&quot;orderName&quot;:&quot;맛있는 다라딸기! 59%%%&quot;}&#39;</code></p>
<p>따라서 이 값은 바로 JSON.parse(...) 하면 된다.</p>
<hr>
<h2 id="중간에-검토했던-방안"><strong>중간에 검토했던 방안</strong></h2>
<p>처음에는 혹시 과거 호환성이나 다른 진입 경로 때문에 <strong>이중 인코딩된 값</strong>도 들어올 수 있을 가능성을 열어두고, 아래처럼 방어 코드를 잠깐 고려했다.</p>
<p><code>const parsePaymentsDataParam = (value: string) =&gt; {
  try {
    return JSON.parse(value);
  } catch {
    return JSON.parse(decodeURIComponent(value));
  }
};</code></p>
<p>이 방식은 다음 상황을 대비한 것이다.</p>
<ul>
<li>누군가 이미 encodeURIComponent 된 값을 다시 encodeURIComponent 해서 넘긴 경우</li>
<li>직접 URL을 조립해서 잘못 호출한 경우</li>
<li>다른 진입점이 존재하는 경우</li>
</ul>
<hr>
<h2 id="왜-그-방어-코드를-최종적으로-제거했는지"><strong>왜 그 방어 코드를 최종적으로 제거했는지</strong></h2>
<p>확인 결과 현재 구조는 다음과 같았다.</p>
<ul>
<li>직접 URL 호출 없음</li>
<li>다른 진입점 없음</li>
<li>앱에서 웹뷰로 들어오는 경로 하나만 존재</li>
<li>해당 경로에서 paymentsData는 정확히 한 번만 인코딩됨</li>
</ul>
<p>이 조건이면 decodeURIComponent fallback을 남겨두는 것보다, 현재 계약에 맞게 단순하게 처리하는 편이 더 맞다.</p>
<p>이유는:</p>
<ul>
<li>잘못된 입력이 조용히 통과하는 것을 막을 수 있음</li>
<li>나중에 인코딩 규약이 깨졌을 때 더 빨리 발견 가능</li>
<li>현재 실제 동작 계약과 코드가 정확히 일치함</li>
</ul>
<hr>
<h2 id="최종-수정-내용"><strong>최종 수정 내용</strong></h2>
<p>PaymentsWidgets.tsx (line 52) 에서 아래처럼 수정했다.</p>
<h3 id="변경-전"><strong>변경 전</strong></h3>
<p><code>const parsedPaymentsData = JSON.parse(
  decodeURIComponent(paymentsDataParam),
);</code></p>
<h3 id="변경-후"><strong>변경 후</strong></h3>
<p><code>const parsedPaymentsData = JSON.parse(paymentsDataParam) as PaymentsData;</code></p>
<p>이후 orderName 이 맛있는 다라딸기! 59%%% 여도 정상 처리된다.</p>
<hr>
<h2 id="as-paymentsdata-에-대한-판단"><strong>as PaymentsData 에 대한 판단</strong></h2>
<p>as PaymentsData 는 런타임 검증이 아니라 TypeScript 타입 단언이다.</p>
<p>의미는:</p>
<ul>
<li>없어도 동작 자체는 가능</li>
<li>있으면 이후 코드에서 타입 추론이 조금 더 명확해짐</li>
<li>실제 데이터 검증 역할은 하지 않음</li>
</ul>
<p>즉 현재 수정의 본질은 as PaymentsData 가 아니라, <strong>decodeURIComponent 를 제거한 것</strong>이다.</p>
<hr>
<h2 id="이번에-정리된-결론"><strong>이번에 정리된 결론</strong></h2>
<ol>
<li>앱은 paymentsData 를 정상적으로 한 번만 인코딩하고 있었다.</li>
<li>웹에서 URLSearchParams.get() 으로 꺼낸 값은 이미 디코딩된 상태였다.</li>
<li>그 값을 다시 decodeURIComponent() 해서 % 포함 상품명에서 URI malformed 가 발생했다.</li>
<li>직접 URL 호출이나 다른 진입점이 없으므로 fallback 없이 JSON.parse(paymentsDataParam) 만 사용하는 것이 가장 정확하다.</li>
</ol>
<hr>
<h2 id="최종-상태"><strong>최종 상태</strong></h2>
<p>현재 웹 결제 위젯 페이지는 아래 규칙으로 동작하면 된다.</p>
<pre><code>const searchParams = new URLSearchParams(window.location.search);
const paymentsDataParam = searchParams.get(&#39;paymentsData&#39;);

if (!paymentsDataParam) {
  // 예외 처리
}

const parsedPaymentsData = JSON.parse(paymentsDataParam);</code></pre><p>이 구조면 맛있는 다라딸기! 59%%% 같은 값도 문제없이 처리된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FCM & 딥링크]]></title>
            <link>https://velog.io/@miin-hyukkk/%E3%85%81%E3%84%B4%E3%85%87%E3%84%B9</link>
            <guid>https://velog.io/@miin-hyukkk/%E3%85%81%E3%84%B4%E3%85%87%E3%84%B9</guid>
            <pubDate>Wed, 12 Nov 2025 02:31:17 GMT</pubDate>
            <description><![CDATA[<h3 id="fcmfirebase-cloud-messaging">FCM(Firebase Cloud Messaging)</h3>
<p>구글에서 제공하는 푸시 알림 인프라</p>
<ul>
<li>서버 → Firebase → 각 기기(안드로이드/iOS)로 메시지 전달.</li>
<li>Expo에서는 <code>expo-notifications</code> 라이브러리를 통해 FCM을 사용.</li>
</ul>
<h3 id="딥링크-deep-link">딥링크 (Deep Link)</h3>
<p>푸시 알림 클릭 시 앱의 특정 화면으로 이동하게 해주는 데이터.</p>
<p>알림에는 단순히 텍스트말고도 <code>data</code>에 <code>{ type, target }</code> 같이 정보를 담아 보냄.</p>
<ul>
<li>예: <code>{ type: &#39;ORDER&#39;, target: &#39;12345&#39; }</code> → 해당 주문 상세 페이지 열기</li>
</ul>
<hr>
<h3 id="firebase-콘솔">Firebase 콘솔</h3>
<p>Firebase 프로젝트 생성</p>
<p>안드로이드 앱 등록 <code>google-services.json</code> 다운로드</p>
<p>iOS 앱 등록 <code>GoogleService-Info.plist</code> 다운로드</p>
<h3 id="expo-설정">Expo 설정</h3>
<p><code>expo-notifications</code>, <code>expo-device</code>, <code>expo-constants</code> 설치</p>
<p><code>app.json</code>에 FCM 관련 설정 추가 (Expo가 빌드 시 포함시켜줌)</p>
<h1 id="앱실행--푸쉬알림-전송-과정">앱실행 ~ 푸쉬알림 전송 과정</h1>
<p>앱에서 FCM 토큰을 발급(<code>registerForPushNotificationsAsync</code>), 기기정보 수집(<code>getDeviceInfo</code>)</p>
<p>↓</p>
<p>사용자 로그인 시 백엔드에게 전달(<code>sendNotificationToken</code>)</p>
<p>↓</p>
<p>백엔드에서는 이벤트 발생 시 특정 혹은 전체 사용자에 대해 FCM 서버에 요청보냄</p>
<p>↓</p>
<p>FCM 서버는 안드로이드 기기에는 직접 전달 iOS 기기에는 APNs를 거쳐서 전달</p>
<h3 id="앱에서-fcm-토큰을-발급registerforpushnotificationsasync">앱에서 FCM 토큰을 발급(<code>registerForPushNotificationsAsync</code>)</h3>
<p>FCM 토큰을 <code>expo-notifications</code>로 발급받음</p>
<p>이때 EAS 프로젝트 ID가 필요하기 때문에 <code>expo-constants</code>로 설정값을 읽어서 같이 넘겨줌</p>
<p><strong>이 토큰을 알아야 서버가 특정 사용자 기기만 골라서 알림을 보낼 수 있음</strong></p>
<pre><code class="language-tsx">export async function registerForPushNotificationsAsync() {
  let token;

  if (Platform.OS === &#39;android&#39;) {
    await Notifications.setNotificationChannelAsync(&#39;default&#39;, {
      name: &#39;default&#39;,
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: &#39;#FF231F7C&#39;,
    });
  }

  if (Device.isDevice) {
    const { status: existingStatus } =
      await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;

    if (existingStatus !== &#39;granted&#39;) {
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }

    if (finalStatus !== &#39;granted&#39;) {
      console.log(&#39;Failed to get push token for push notification!&#39;);
      return;
    }

    token = await Notifications.getExpoPushTokenAsync({
      projectId: Constants.expoConfig?.extra?.eas?.projectId,
    });
  } else {
    console.log(&#39;Must use physical device for Push Notifications&#39;);
  }

  return token;
}</code></pre>
<h3 id="기기정보-수집getdeviceinfo">기기정보 수집(<code>getDeviceInfo</code>)</h3>
<p><code>expo-device</code> 로 디바이스 정보 가져옴</p>
<pre><code class="language-bash">// 기기 정보 가져오기
export async function getDeviceInfo() {
  const deviceType = await Device.getDeviceTypeAsync();
  const deviceName = Device.deviceName;
  const osName = Device.osName;
  const osVersion = Device.osVersion;
  const brand = Device.brand;
  const manufacturer = Device.manufacturer;
  const modelName = Device.modelName;

  // 고유 식별자 생성
  const uniqueId = `${brand}-${modelName}-${osVersion}-${deviceName}`
    .replace(/\s+/g, &#39;-&#39;)
    .toLowerCase();

  return {
    installationId: uniqueId,
    deviceName,
    deviceType,
    osName,
    osVersion,
    brand,
    manufacturer,
    modelName,
  };
}</code></pre>
<h3 id="사용자-로그인-시-백엔드에게-전달sendnotificationtoken">사용자 로그인 시 백엔드에게 전달(<code>sendNotificationToken</code>)</h3>
<p>이제 로그인 할때 <code>fcmToken</code>이랑 <code>deviceInfo</code>를 백엔드에게 보내줌</p>
<pre><code class="language-bash">export const sendNotificationToken = async (body: {
  fcmToken: string;
  deviceInfo: string;
}) =&gt; {
  return await baseInstance.post(&#39;/api/notification/token&#39;, body);
};
</code></pre>
<p>그럼 백엔드에서 FCM API를 호출해서 앱기기로 알림을 전달함</p>
<h1 id="푸쉬-알림-수신-과정">푸쉬 알림 수신 과정</h1>
<p>알림을 받을 때 앱이 가지는 상태는 총 세가지임</p>
<ul>
<li>앱이 켜져있을 때</li>
<li>앱이 백그라운드에 있을 때</li>
<li>앱이 완전히 꺼져있을 때</li>
</ul>
<p>알림이 도착했을 때 실행할 함수(<code>onNotificationReceived</code>), 
알림을 클릭했을 때 실행할 함수(<code>onNotificationResponseReceived</code>) </p>
<pre><code class="language-tsx">export function setupNotificationListeners(
  onNotificationReceived: (notification: Notifications.Notification) =&gt; void,
  onNotificationResponseReceived: (
    response: Notifications.NotificationResponse,
  ) =&gt; void,
) {

  // 안드로이드 cold-start
    Notifications.getLastNotificationResponseAsync().then((lastResponse) =&gt; {
      if (lastResponse) {
        onNotificationResponseReceived(lastResponse);
      }
    });

  const notificationListener = Notifications.addNotificationReceivedListener(
    onNotificationReceived,
  );

  const responseListener =
    Notifications.addNotificationResponseReceivedListener(
      onNotificationResponseReceived,
    );

  return () =&gt; {
    Notifications.removeNotificationSubscription(notificationListener);
    Notifications.removeNotificationSubscription(responseListener);
  };
}</code></pre>
<pre><code class="language-tsx">// app/_layout.tsx
const unsubscribe = setupNotificationListeners(
  (notification: any) =&gt; {
    console.log(&#39;알림 수신:&#39;, notification.request.content.data);
  },
  (response: any) =&gt; {
    console.log(&#39;알림 클릭:&#39;, response.notification.request.content.data);

    const deepLinkData = extractDeepLinkData(response);

    if (deepLinkData) {
      const currentTabDeepLinkHandler = (global as any)[
        `handleDeepLinkFromNotification_${currentTab}`
      ];
      if (currentTabDeepLinkHandler) {
        currentTabDeepLinkHandler(deepLinkData);
      } else {
        console.log(
          `${currentTab} 탭의 딥링크 처리 함수가 등록되지 않았습니다.`,
        );
      }
    } else {
      console.log(&#39;딥링크 데이터가 없습니다.&#39;);
    }
  },
);</code></pre>
<h3 id="앱이-켜져있을-때foreground">앱이 켜져있을 때(Foreground)</h3>
<p><code>onNotificationReceived</code> 실행됨</p>
<p>사용자가 알림 클릭 시 <code>onNotificationResponseReceived</code> 실행됨</p>
<h3 id="앱이-백그라운드에-있을-때">앱이 백그라운드에 있을 때</h3>
<p>알림은 OS가 표시</p>
<p><code>onNotificationReceived</code> 실행X</p>
<p>사용자가 알림 클릭 시 <code>onNotificationResponseReceived</code> 실행됨</p>
<h3 id="앱이-완전히-꺼져있을-때">앱이 완전히 꺼져있을 때</h3>
<p><strong>iOS</strong></p>
<p>알림은 OS가 표시</p>
<p><code>onNotificationReceived</code> 실행X</p>
<p>사용자가 알림 클릭 시 <code>onNotificationResponseReceived</code> 실행됨</p>
<p><strong>Android</strong>
알림은 OS가 표시</p>
<p><code>onNotificationReceived</code> 실행X</p>
<p>사용자가 알림 클릭 → 앱 실행되지만, listener(<code>onNotificationResponseReceived</code>)는 자동 실행되지 않음</p>
<p>아래 이 코드를 호출해서 마지막 알림 응답을 수동으로 꺼내와야 함</p>
<pre><code class="language-tsx">Notifications.getLastNotificationResponseAsync().then((lastResponse) =&gt; {
  if (lastResponse) {
    onNotificationResponseReceived(lastResponse);
  }
});</code></pre>
<p>알림이 도착했을때 실시간 UI반영이 필요없다면 <code>onNotificationReceived</code> 는 없어도됨.(마켓D도 필요없음)</p>
<h1 id="딥링크">딥링크</h1>
<p>푸쉬알림을 클릭했을 때 </p>
<p>백엔드에서 보내준 데이터를 받아 현재 탭의 웹뷰에 postMessage로 전달해서 해당 페이지를 열도록 해야됨</p>
<p>푸쉬알림 클릭</p>
<p>↓</p>
<p><code>extractDeepLinkData</code> 로 데이터 뽑아냄 </p>
<p>↓</p>
<p>현재 탭에 맞는 딥링크처리함수를 전역객체(<code>global</code>)에서 꺼내옴 ex)<code>handleDeepLinkFromNotification_Home</code></p>
<p>↓</p>
<p>해당 함수 실행 → <code>useDeepLink</code> 내부의 <code>handleDeepLinkFromNotification</code> 동작</p>
<p>↓</p>
<p>이미 처리된 알림(<code>lastHandledId</code>)인지 검사. (홈탭일때 이미 처리한 알림인데 다른 탭에서 </p>
<p>새로운 알림이면 메세지 객체 생성</p>
<p>↓</p>
<p>WebView 준비 여부 확인</p>
<p>준비됨(앱이 백그라운드에 있거나, 켜져있을 경우) → <code>webviewRef.postMessage()</code> 로 곧바로 전달</p>
<p>준비 안 됨(앱이 완전히 꺼져있을 경우) → <code>pendingDeepLink</code>에 임시 저장</p>
<p>↓</p>
<p>WebView가 준비 완료 신호(<code>setIsWebviewReady(true)</code>) 보내면 <code>pendingDeepLink</code> 실행</p>
<p><strong>앱이 켜져있거나, 백그라운드에 있을때</strong></p>
<p>앱이 켜져있으니, 웹뷰도 이미 READY 상태 → 푸쉬알림 클릭 →<code>handleDeepLinkFromNotification_home</code> 호출 → <code>isWebviewReadyRef.current === true</code> 라서 바로 웹뷰로 postMessage 전송</p>
<p><strong>앱이 꺼져있을때</strong></p>
<p>푸쉬알림 클릭 → 앱실행 →앱코드에서 useDeepLink 실행 → 아직 READY 상태 아님 → pendingDeepLink에 저장 → 앱이 열리면서 웹쪽에서 <code>WEB_MSG.READY</code> 메시지 보냄 → 아래 코드 실행</p>
<pre><code class="language-bash">if (pendingDeepLink) {
  webviewRef.current?.postMessage(JSON.stringify(pendingDeepLink));
}</code></pre>
<pre><code class="language-tsx">const useDeepLink = (webviewRef: React.RefObject&lt;WebView&gt;, type: string) =&gt; {
  const isWebviewReadyRef = useRef&lt;boolean&gt;(false);

  const {
    lastHandledId,
    setLastHandledId,
    pendingDeepLink,
    setPendingDeepLink,
  } = useDeepLinkStore();

  const handleDeepLinkFromNotification = (deepLinkData: any) =&gt; {
    if (!deepLinkData || !deepLinkData.target) return;

    // 이미 처리된 딥링크 무시
    if (lastHandledId === deepLinkData.target) return;

    // 즉시 lastHandledId 설정하여 중복 처리 방지
    setLastHandledId(deepLinkData.target);

    const message = {
      type: APP_MSG.DEEP_LINK,
      data: {
        type: deepLinkData.type,
        target: deepLinkData.target,
      },
    };

    if (isWebviewReadyRef.current) {
      webviewRef.current?.postMessage(JSON.stringify(message));
      setPendingDeepLink(null);
    } else {
      // 앱 꺼져있을 경우
      setPendingDeepLink(message);
    }
  };


  // 현재 탭(type)에 맞는 딥링크 처리 함수를 global 객체에 등록
  useEffect(() =&gt; {
    (global as any)[`handleDeepLinkFromNotification_${type}`] =
      handleDeepLinkFromNotification;
    return () =&gt; {
      delete (global as any)[`handleDeepLinkFromNotification_${type}`];
    };
  }, [type, lastHandledId]);

  return {
    pendingDeepLink,
    setIsWebviewReady: (ready: boolean) =&gt; {
      isWebviewReadyRef.current = ready;
    },
  };
};
</code></pre>
<pre><code class="language-tsx">  // home탭
  const { pendingDeepLink, setIsWebviewReady } = useDeepLink(
    webviewRef,
    &#39;home&#39;,
  );

  // order탭
   const { setIsWebviewReady } = useDeepLink(webviewRef, &#39;order&#39;);

  // myPage탭
   const { setIsWebviewReady } = useDeepLink(webviewRef, &#39;myPage&#39;);

  // setting탭
   const { setIsWebviewReady } = useDeepLink(webviewRef, &#39;setting&#39;);
</code></pre>
<h3 id="전역함수여야-하는-이유"><strong>전역함수여야 하는 이유</strong></h3>
<p>푸시 알림은 _layout.tsx처럼 앱 최상위에서 수신함</p>
<p>여기서는 단순히 알림이 왔고, 딥링크데이터가 뭔지 정도만 확인 가능함</p>
<p>하지만 딥링크 실행 위치는 각 탭임. 그러나 _layout.tsx는 각 탭의 WebView ref를 직접 알지못함.</p>
<p>즉 알림은 위에서 받는데 실행은 아래서 해야하기 때문에 전역에 함수를 등록해놓는거임</p>
<p>각 탭은 자기 webview  ref를 알고 있으니까 전역에 자기 전용함수를 등록해놓는거임.</p>
<pre><code class="language-tsx">(global as any)[`handleDeepLinkFromNotification_Home`] = (deepLinkData) =&gt; {
  homeWebviewRef.current?.postMessage(JSON.stringify(deepLinkData));
};</code></pre>
<p><em>layout.tsx는 알림이 오면 현재 탭 이름 확인하고, 전역에서 `handleDeepLinkFromNotification</em>${currentTab}`를 꺼내 실행</p>
<h3 id="탭마다-해야되는-이유"><strong>탭마다 해야되는 이유</strong></h3>
<p>탭마다 독립적인 웹뷰를 가지고 있어서  알림 클릭 시 어떤 탭의 웹뷰로 메세지를 보낼지 구분해야됨</p>
<p>근데 만약 탭마다 구분안하고 공통으로 써버리면, 마지막탭에서 등록된 함수가 덮어씌워버림</p>
<p>ex)</p>
<ul>
<li>처음에 Home 탭이 실행되면 → Home의 ref로 등록됨</li>
<li>나중에 MyPage 탭이 실행되면 → Home의 ref는 사라지고 MyPage ref로 덮어쓰기됨</li>
</ul>
<p>홈탭이 열려있어도 딥링크가 mypage로 가버림</p>
<h3 id="앱이-완전히-꺼져있을-때-딥링크가-작동하지-않았던-이유"><strong>앱이 완전히 꺼져있을 때 딥링크가 작동하지 않았던 이유</strong></h3>
<p>웹뷰가 아직 준비되지 않았는데 postMessage를 보내버려서 작동하지 않았었음.</p>
<p>웹뷰 준비 전이면 ref객체에 딥링크정보 저장해둠으로써 해결</p>
<h3 id="홈탭에서-딥링크메세지를-받고-처리했는데-다른탭-클릭-시-해당-딥링크가-또-실행됨"><strong>홈탭에서 딥링크메세지를 받고 처리했는데 다른탭 클릭 시 해당 딥링크가 또 실행됨</strong></h3>
<p>알림 응답 객체(NotificationResponse)는 한번 쓰고 사라지지 않고, 앱이 살아있는 동안 유지됨</p>
<p>새 listener가 등록되면, Expo가 이 객체를 또 전달함</p>
<p>앱이 꺼져있는 상태에서 알림이 도착 → 알림 클릭 → 앱 실행. 홈탭 실행 → 딥링크 페이지 이동→</p>
<p>다시 다른 탭 클릭 시 딥링크 페이지 또 이동 → 무한반복</p>
<p>다른탭의 웹뷰 로드되면서 useDeepLink(&quot;MyPage&quot;) 훅이 자기 전용 핸들러 등록 
여전히 남아있는 알림 응답 객체 때문에 listener가 다시 불림</p>
<p>그래서 중복처리 해줘야함</p>
<h3 id="fcmfirebase-cloud-messaging-1">FCM(Firebase Cloud Messaging)</h3>
<p>구글에서 제공하는 푸시 알림 인프라</p>
<ul>
<li>서버 → Firebase → 각 기기(안드로이드/iOS)로 메시지 전달.</li>
<li>Expo에서는 <code>expo-notifications</code> 라이브러리를 통해 FCM을 사용.</li>
</ul>
<h3 id="딥링크-deep-link-1">딥링크 (Deep Link)</h3>
<p>푸시 알림 클릭 시 앱의 특정 화면으로 이동하게 해주는 데이터.</p>
<p>알림에는 단순히 텍스트말고도 <code>data</code>에 <code>{ type, target }</code> 같이 정보를 담아 보냄.</p>
<ul>
<li>예: <code>{ type: &#39;ORDER&#39;, target: &#39;12345&#39; }</code> → 해당 주문 상세 페이지 열기</li>
</ul>
<hr>
<h3 id="firebase-콘솔-1">Firebase 콘솔</h3>
<p>Firebase 프로젝트 생성</p>
<p>안드로이드 앱 등록 <code>google-services.json</code> 다운로드</p>
<p>iOS 앱 등록 <code>GoogleService-Info.plist</code> 다운로드</p>
<h3 id="expo-설정-1">Expo 설정</h3>
<p><code>expo-notifications</code>, <code>expo-device</code>, <code>expo-constants</code> 설치</p>
<p><code>app.json</code>에 FCM 관련 설정 추가 (Expo가 빌드 시 포함시켜줌)</p>
<h1 id="앱실행--푸쉬알림-전송-과정-1">앱실행 ~ 푸쉬알림 전송 과정</h1>
<p>앱에서 FCM 토큰을 발급(<code>registerForPushNotificationsAsync</code>), 기기정보 수집(<code>getDeviceInfo</code>)</p>
<p>↓</p>
<p>사용자 로그인 시 백엔드에게 전달(<code>sendNotificationToken</code>)</p>
<p>↓</p>
<p>백엔드에서는 이벤트 발생 시 특정 혹은 전체 사용자에 대해 FCM 서버에 요청보냄</p>
<p>↓</p>
<p>FCM 서버는 안드로이드 기기에는 직접 전달 iOS 기기에는 APNs를 거쳐서 전달</p>
<h3 id="앱에서-fcm-토큰을-발급registerforpushnotificationsasync-1">앱에서 FCM 토큰을 발급(<code>registerForPushNotificationsAsync</code>)</h3>
<p>FCM 토큰을 <code>expo-notifications</code>로 발급받음</p>
<p>이때 EAS 프로젝트 ID가 필요하기 때문에 <code>expo-constants</code>로 설정값을 읽어서 같이 넘겨줌</p>
<p><strong>이 토큰을 알아야 서버가 특정 사용자 기기만 골라서 알림을 보낼 수 있음</strong></p>
<pre><code class="language-tsx">export async function registerForPushNotificationsAsync() {
  let token;

  if (Platform.OS === &#39;android&#39;) {
    await Notifications.setNotificationChannelAsync(&#39;default&#39;, {
      name: &#39;default&#39;,
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: &#39;#FF231F7C&#39;,
    });
  }

  if (Device.isDevice) {
    const { status: existingStatus } =
      await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;

    if (existingStatus !== &#39;granted&#39;) {
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }

    if (finalStatus !== &#39;granted&#39;) {
      console.log(&#39;Failed to get push token for push notification!&#39;);
      return;
    }

    token = await Notifications.getExpoPushTokenAsync({
      projectId: Constants.expoConfig?.extra?.eas?.projectId,
    });
  } else {
    console.log(&#39;Must use physical device for Push Notifications&#39;);
  }

  return token;
}</code></pre>
<h3 id="기기정보-수집getdeviceinfo-1">기기정보 수집(<code>getDeviceInfo</code>)</h3>
<p><code>expo-device</code> 로 디바이스 정보 가져옴</p>
<pre><code class="language-bash">// 기기 정보 가져오기
export async function getDeviceInfo() {
  const deviceType = await Device.getDeviceTypeAsync();
  const deviceName = Device.deviceName;
  const osName = Device.osName;
  const osVersion = Device.osVersion;
  const brand = Device.brand;
  const manufacturer = Device.manufacturer;
  const modelName = Device.modelName;

  // 고유 식별자 생성
  const uniqueId = `${brand}-${modelName}-${osVersion}-${deviceName}`
    .replace(/\s+/g, &#39;-&#39;)
    .toLowerCase();

  return {
    installationId: uniqueId,
    deviceName,
    deviceType,
    osName,
    osVersion,
    brand,
    manufacturer,
    modelName,
  };
}</code></pre>
<h3 id="사용자-로그인-시-백엔드에게-전달sendnotificationtoken-1">사용자 로그인 시 백엔드에게 전달(<code>sendNotificationToken</code>)</h3>
<p>이제 로그인 할때 <code>fcmToken</code>이랑 <code>deviceInfo</code>를 백엔드에게 보내줌</p>
<pre><code class="language-bash">export const sendNotificationToken = async (body: {
  fcmToken: string;
  deviceInfo: string;
}) =&gt; {
  return await baseInstance.post(&#39;/api/notification/token&#39;, body);
};
</code></pre>
<p>그럼 백엔드에서 FCM API를 호출해서 앱기기로 알림을 전달함</p>
<h1 id="푸쉬-알림-수신-과정-1">푸쉬 알림 수신 과정</h1>
<p>알림이 도착했을 때 실행할 함수(<code>onNotificationReceived</code>), 
알림을 클릭했을 때 실행할 함수(<code>onNotificationResponseReceived</code>) </p>
<pre><code class="language-tsx">export function setupNotificationListeners(
  onNotificationReceived: (notification: Notifications.Notification) =&gt; void,
  onNotificationResponseReceived: (
    response: Notifications.NotificationResponse,
  ) =&gt; void,
) {

  // 안드로이드 cold-start
    Notifications.getLastNotificationResponseAsync().then((lastResponse) =&gt; {
      if (lastResponse) {
        onNotificationResponseReceived(lastResponse);
      }
    });

  const notificationListener = Notifications.addNotificationReceivedListener(
    onNotificationReceived,
  );

  const responseListener =
    Notifications.addNotificationResponseReceivedListener(
      onNotificationResponseReceived,
    );

  return () =&gt; {
    Notifications.removeNotificationSubscription(notificationListener);
    Notifications.removeNotificationSubscription(responseListener);
  };
}</code></pre>
<pre><code class="language-tsx">// app/_layout.tsx
const unsubscribe = setupNotificationListeners(
  (notification: any) =&gt; {
    console.log(&#39;알림 수신:&#39;, notification.request.content.data);
  },
  (response: any) =&gt; {
    console.log(&#39;알림 클릭:&#39;, response.notification.request.content.data);

    const deepLinkData = extractDeepLinkData(response);

    if (deepLinkData) {
      const currentTabDeepLinkHandler = (global as any)[
        `handleDeepLinkFromNotification_${currentTab}`
      ];
      if (currentTabDeepLinkHandler) {
        currentTabDeepLinkHandler(deepLinkData);
      } else {
        console.log(
          `${currentTab} 탭의 딥링크 처리 함수가 등록되지 않았습니다.`,
        );
      }
    } else {
      console.log(&#39;딥링크 데이터가 없습니다.&#39;);
    }
  },
);</code></pre>
<h3 id="앱이-켜져있을-때foreground-1">앱이 켜져있을 때(Foreground)</h3>
<p><code>onNotificationReceived</code> 실행됨</p>
<p>사용자가 알림 클릭 시 <code>onNotificationResponseReceived</code> 실행됨</p>
<h3 id="앱이-백그라운드에-있을-때-1">앱이 백그라운드에 있을 때</h3>
<p>알림은 OS가 표시</p>
<p><code>onNotificationReceived</code> 실행X</p>
<p>사용자가 알림 클릭 시 <code>onNotificationResponseReceived</code> 실행됨</p>
<h3 id="앱이-완전히-꺼져있을-때-1">앱이 완전히 꺼져있을 때</h3>
<p><strong>iOS</strong></p>
<p>알림은 OS가 표시</p>
<p><code>onNotificationReceived</code> 실행X</p>
<p>사용자가 알림 클릭 시 <code>onNotificationResponseReceived</code> 실행됨</p>
<p><strong>Android</strong>
알림은 OS가 표시</p>
<p><code>onNotificationReceived</code> 실행X</p>
<p>사용자가 알림 클릭 → 앱 실행되지만, listener(<code>onNotificationResponseReceived</code>)는 자동 실행되지 않음</p>
<p>아래 이 코드를 호출해서 마지막 알림 응답을 수동으로 꺼내와야 함</p>
<pre><code class="language-tsx">Notifications.getLastNotificationResponseAsync().then((lastResponse) =&gt; {
  if (lastResponse) {
    onNotificationResponseReceived(lastResponse);
  }
});</code></pre>
<p>알림이 도착했을때 실시간 UI반영이 필요없다면 <code>onNotificationReceived</code> 는 없어도됨.(마켓D도 필요없음)</p>
<h1 id="딥링크-1">딥링크</h1>
<p>푸쉬알림을 클릭했을 때 </p>
<p>백엔드에서 보내준 데이터를 받아 현재 탭의 웹뷰에 postMessage로 전달해서 해당 페이지를 열도록 해야됨</p>
<p>푸쉬알림 클릭</p>
<p>↓</p>
<p><code>extractDeepLinkData</code> 로 데이터 뽑아냄 </p>
<p>↓</p>
<p>현재 탭에 맞는 딥링크처리함수를 전역객체(<code>global</code>)에서 꺼내옴 ex)<code>handleDeepLinkFromNotification_Home</code></p>
<p>↓</p>
<p>해당 함수 실행 → <code>useDeepLink</code> 내부의 <code>handleDeepLinkFromNotification</code> 동작</p>
<p>↓</p>
<p>이미 처리된 알림(<code>lastHandledId</code>)인지 검사. (홈탭일때 이미 처리한 알림인데 다른 탭에서 </p>
<p>새로운 알림이면 메세지 객체 생성</p>
<p>↓</p>
<p>WebView 준비 여부 확인</p>
<p>준비됨(앱이 백그라운드에 있거나, 켜져있을 경우) → <code>webviewRef.postMessage()</code> 로 곧바로 전달</p>
<p>준비 안 됨(앱이 완전히 꺼져있을 경우) → <code>pendingDeepLink</code>에 임시 저장</p>
<p>↓</p>
<p>WebView가 준비 완료 신호(<code>setIsWebviewReady(true)</code>) 보내면 <code>pendingDeepLink</code> 실행</p>
<p><strong>앱이 켜져있거나, 백그라운드에 있을때</strong></p>
<p>앱이 켜져있으니, 웹뷰도 이미 READY 상태 → 푸쉬알림 클릭 →<code>handleDeepLinkFromNotification_home</code> 호출 → <code>isWebviewReadyRef.current === true</code> 라서 바로 웹뷰로 postMessage 전송</p>
<p><strong>앱이 꺼져있을때</strong></p>
<p>푸쉬알림 클릭 → 앱실행 →앱코드에서 useDeepLink 실행 → 아직 READY 상태 아님 → pendingDeepLink에 저장 → 앱이 열리면서 웹쪽에서 <code>WEB_MSG.READY</code> 메시지 보냄 → 아래 코드 실행</p>
<pre><code class="language-bash">if (pendingDeepLink) {
  webviewRef.current?.postMessage(JSON.stringify(pendingDeepLink));
}</code></pre>
<pre><code class="language-tsx">const useDeepLink = (webviewRef: React.RefObject&lt;WebView&gt;, type: string) =&gt; {
  const isWebviewReadyRef = useRef&lt;boolean&gt;(false);

  const {
    lastHandledId,
    setLastHandledId,
    pendingDeepLink,
    setPendingDeepLink,
  } = useDeepLinkStore();

  const handleDeepLinkFromNotification = (deepLinkData: any) =&gt; {
    if (!deepLinkData || !deepLinkData.target) return;

    // 이미 처리된 딥링크 무시
    if (lastHandledId === deepLinkData.target) return;

    // 즉시 lastHandledId 설정하여 중복 처리 방지
    setLastHandledId(deepLinkData.target);

    const message = {
      type: APP_MSG.DEEP_LINK,
      data: {
        type: deepLinkData.type,
        target: deepLinkData.target,
      },
    };

    if (isWebviewReadyRef.current) {
      webviewRef.current?.postMessage(JSON.stringify(message));
      setPendingDeepLink(null);
    } else {
      // 앱 꺼져있을 경우
      setPendingDeepLink(message);
    }
  };


  // 현재 탭(type)에 맞는 딥링크 처리 함수를 global 객체에 등록
  useEffect(() =&gt; {
    (global as any)[`handleDeepLinkFromNotification_${type}`] =
      handleDeepLinkFromNotification;
    return () =&gt; {
      delete (global as any)[`handleDeepLinkFromNotification_${type}`];
    };
  }, [type, lastHandledId]);

  return {
    pendingDeepLink,
    setIsWebviewReady: (ready: boolean) =&gt; {
      isWebviewReadyRef.current = ready;
    },
  };
};
</code></pre>
<pre><code class="language-tsx">  //home탭
  const { pendingDeepLink, setIsWebviewReady } = useDeepLink(
    webviewRef,
    &#39;home&#39;,
  );

  //order탭
   const { setIsWebviewReady } = useDeepLink(webviewRef, &#39;order&#39;);

  //myPage탭
   const { setIsWebviewReady } = useDeepLink(webviewRef, &#39;myPage&#39;);

  //setting탭
   const { setIsWebviewReady } = useDeepLink(webviewRef, &#39;setting&#39;);
</code></pre>
<h3 id="전역함수여야-하는-이유-1"><strong>전역함수여야 하는 이유</strong></h3>
<p>푸시 알림은 _layout.tsx처럼 앱 최상위에서 수신함</p>
<p>여기서는 단순히 알림이 왔고, 딥링크데이터가 뭔지 정도만 확인 가능함</p>
<p>하지만 딥링크 실행 위치는 각 탭임. 그러나 _layout.tsx는 각 탭의 WebView ref를 직접 알지못함.</p>
<p>즉 알림은 위에서 받는데 실행은 아래서 해야하기 때문에 전역에 함수를 등록해놓는거임</p>
<p>각 탭은 자기 webview  ref를 알고 있으니까 전역에 자기 전용함수를 등록해놓는거임.</p>
<pre><code class="language-tsx">(global as any)[`handleDeepLinkFromNotification_Home`] = (deepLinkData) =&gt; {
  homeWebviewRef.current?.postMessage(JSON.stringify(deepLinkData));
};</code></pre>
<p><em>layout.tsx는 알림이 오면 현재 탭 이름 확인하고, 전역에서 `handleDeepLinkFromNotification</em>${currentTab}`를 꺼내 실행</p>
<h3 id="탭마다-해야되는-이유-1"><strong>탭마다 해야되는 이유</strong></h3>
<p>탭마다 독립적인 웹뷰를 가지고 있어서  알림 클릭 시 어떤 탭의 웹뷰로 메세지를 보낼지 구분해야됨</p>
<p>근데 만약 탭마다 구분안하고 공통으로 써버리면, 마지막탭에서 등록된 함수가 덮어씌워버림</p>
<p>ex)</p>
<ul>
<li>처음에 Home 탭이 실행되면 → Home의 ref로 등록됨</li>
<li>나중에 MyPage 탭이 실행되면 → Home의 ref는 사라지고 MyPage ref로 덮어쓰기됨</li>
</ul>
<p>홈탭이 열려있어도 딥링크가 mypage로 가버림</p>
<h3 id="앱이-완전히-꺼져있을-때-딥링크가-작동하지-않았던-이유-1"><strong>앱이 완전히 꺼져있을 때 딥링크가 작동하지 않았던 이유</strong></h3>
<p>웹뷰가 아직 준비되지 않았는데 postMessage를 보내버려서 작동하지 않았었음.</p>
<p>웹뷰 준비 전이면 ref객체에 딥링크정보 저장해둠으로써 해결</p>
<h3 id="홈탭에서-딥링크메세지를-받고-처리했는데-다른탭-클릭-시-해당-딥링크가-또-실행됨-1"><strong>홈탭에서 딥링크메세지를 받고 처리했는데 다른탭 클릭 시 해당 딥링크가 또 실행됨</strong></h3>
<p>알림 응답 객체(NotificationResponse)는 한번 쓰고 사라지지 않고, 앱이 살아있는 동안 유지됨</p>
<p>새 listener가 등록되면, Expo가 이 객체를 또 전달함</p>
<p>앱이 꺼져있는 상태에서 알림이 도착 → 알림 클릭 → 앱 실행. 홈탭 실행 → 딥링크 페이지 이동→</p>
<p>다시 다른 탭 클릭 시 딥링크 페이지 또 이동 → 무한반복</p>
<p>다른탭의 웹뷰 로드되면서 useDeepLink(&quot;MyPage&quot;) 훅이 자기 전용 핸들러 등록 
여전히 남아있는 알림 응답 객체 때문에 listener가 다시 불림</p>
<p>그래서 중복처리 해줘야함</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ios17.6 앱튕김]]></title>
            <link>https://velog.io/@miin-hyukkk/ios17.6-%EC%95%B1%ED%8A%95%EA%B9%80</link>
            <guid>https://velog.io/@miin-hyukkk/ios17.6-%EC%95%B1%ED%8A%95%EA%B9%80</guid>
            <pubDate>Wed, 12 Nov 2025 02:30:53 GMT</pubDate>
            <description><![CDATA[<p>문제 발생 확인
<img src="https://velog.velcdn.com/images/miin-hyukkk/post/d422ce50-b8b3-40df-849f-3bc95fff1c30/image.png" alt=""><img src="https://velog.velcdn.com/images/miin-hyukkk/post/9684ace8-f7f6-400f-a2f2-11b3825ccd24/image.png" alt=""></p>
<p>ios17.6 앱튕김으로 원인 좁힘</p>
<p>ios 17이하에서 지속적으로 앱 크러쉬 발생.</p>
<p>버전을 낮춰서 빌드를 해봐도 계속 빌드 실패</p>
<p>빌드 성공해도 16이하는 애플쪽에서 submit 막아둠</p>
<p>동생의 실기기가 ios 17.6버전이라 내부 테스터로 등록 후 개발자에게 로그 제출하도록 함.</p>
<p>문제 확인</p>
<ul>
<li>iOS 내부 프레임워크인 SwiftUICore.framework를 찾을 수 없어서 앱이 바로 종료</li>
<li>Expo로 빌드를 하더라도, 내부에 iOS 네이티브 코드(모듈)가 포함되고 그중 일부가 SwiftUI 기반일 수 있음</li>
<li>Xcode 16(2024~25)로 빌드 시 iOS 18 SDK가 적용됨, 그러면 SwiftUICore가 자동 의존되어 iOS 17 기기에서는 못 찾고 크러시가 나는 상황.</li>
</ul>
<p>구글링을 통해 사례를 한번 쭉 정리해보기로 결정</p>
<p>같은 사례로 보인글들과 그 글의 댓글, 대댓글을 모두 직접 스크랩해서 사례별로 중복제거없이 쭉 정리</p>
<h2 id="🧩-사례-1--jaxter2024-첫-번째-글-본문-capacitor-version">🧩 사례 1 – Jaxter2024 (첫 번째 글 본문: Capacitor Version…)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>Capacitor 6, Xcode 16을 사용하는 <strong>꽤 복잡한 SwiftUI + UIKit 프로젝트</strong>.</li>
<li>iOS 18 시뮬레이터에서는 정상 실행.</li>
<li><strong>iOS 17 및 그 이하에서 앱이 런치 직후 크래시</strong>.</li>
<li>크래시 이유: 앱이 <strong>SwiftUICore 라이브러리를 로드하려 하지만</strong>, SwiftUICore는 <strong>iOS 18에서 새로 도입된 프레임워크</strong>라 예전 iOS에는 없어 크래시가 난다고 설명함.</li>
<li>UIViewRepresentable 같은 SwiftUI 컴포넌트를 사용하면 이 크래시가 재현됨.</li>
<li>exyte/SVGView 같은 SPM 패키지도 같은 방식으로 크래시를 유발한다고 적음.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>글에서는 “<strong>UIViewRepresentable 등 SwiftUICore를 필요로 하는 SwiftUI 컴포넌트 사용</strong>”과</li>
<li>“<strong>Xcode 16이 SwiftUICore가 런타임에 있을 것이라고 가정하고 빌드한다</strong>”는 점을 문제의 근원으로 적고 있음.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>글에서 제시한 임시 해결책:<ul>
<li>앱 타깃의 <strong>Build Settings → Other Linker Flags (OTHER_LDFLAGS)</strong> 에<ul>
<li><code>weak_framework SwiftUICore</code> 를 추가.</li>
</ul>
</li>
<li>이렇게 하면 SwiftUICore에 <strong>약한 링크(weak link)</strong> 를 걸어, 해당 프레임워크가 없는 iOS에서도 크래시 없이 실행 가능하다고 설명.</li>
</ul>
</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>SwiftUICore는 iOS 18부터 존재하는데,</li>
<li>Xcode 16이 앱을 빌드할 때 SwiftUICore가 <strong>iOS 17 이하에도 존재한다고 잘못 가정</strong>하고,</li>
<li>이 때문에 iOS 17 기기에서 <strong>SwiftUICore가 없어서 크래시</strong>가 발생한다고 글에서 설명함.</li>
<li>또한 이 현상이 <strong>Capacitor를 사용할 때만 재현되고, 순수 Swift 프로젝트에서는 재현되지 않는다</strong>고 적어, Capacitor가 문제를 악화시키는 것 같다고 쓰여 있음.</li>
</ul>
<hr>
<h2 id="🧩-사례-2--netural-philipp-gabriel-첫-번째-글-댓글-1">🧩 사례 2 – netural-philipp-gabriel (첫 번째 글 댓글 1)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“우리는 같은 문제를 겪고 있다”고 명시.</li>
<li>즉, Jaxter2024가 보고한 것과 <strong>동일한 크래시</strong>를 경험 중이라고 말함.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>별도 기술 없음. “같은 문제(same problem)”라고만 언급.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li><p>댓글에서 직접 한 말:</p>
<ul>
<li>“워크어라운드 제공한 @Jaxter2017(=2024)에게 감사”</li>
</ul>
</li>
<li><p>즉, <strong>Jaxter가 제시한 <code>weak_framework SwiftUICore</code> 워크어라운드를 사용했다</strong>는 의미로 적혀 있음.</p>
<p>  (댓글 안에서 다른 해결책은 언급하지 않음.)</p>
</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>별도로 원인을 설명하지 않고, “이건 진짜 나쁘다(심각하다)” 정도만 언급.</li>
</ul>
<hr>
<h2 id="🧩-사례-3--jcesarmobile-첫-번째-글-댓글-2">🧩 사례 3 – jcesarmobile (첫 번째 글 댓글 2)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>같은 이슈에 대해 코멘트하면서, 이 문제는 <strong>Apple/Xcode 버그</strong>라고 말함.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li><p>“Capacitor 앱들이 사용하는 것처럼 <strong>scheme 이름을 <code>App</code>으로 설정할 때 어떤 네이밍 충돌이 있는 것 같다</strong>”고 적음.</p>
<p>  (scheme = Xcode에서 실행/빌드에 사용되는 스킴 이름.)</p>
</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>해결책으로 제시한 것:<ul>
<li>Capacitor 문서의 <strong>“Renaming your app”</strong> 섹션을 따라 <strong>스킴 이름을 변경</strong>하라고 안내.<ul>
<li>링크: <code>https://capacitorjs.com/docs/v5/ios/configuration#renaming-your-app</code></li>
</ul>
</li>
</ul>
</li>
<li>또 하나의 관찰:<ul>
<li>“이 문제는 <strong>디버그 빌드에서만 발생하고, 릴리즈 빌드에서는 앱이 크래시하지 않았다</strong>”고 적음.</li>
</ul>
</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>“<strong>Apple/Xcode 버그</strong>”라고 명시.</li>
<li>구체적으로는 스킴 이름이 <code>App</code>일 때 <strong>이름 충돌(name conflict)</strong> 이 일어나는 것 같다고 적혀 있음.</li>
</ul>
<hr>
<h2 id="🧩-사례-4--netural-philipp-gabriel-첫-번째-글-댓글-3">🧩 사례 4 – netural-philipp-gabriel (첫 번째 글 댓글 3)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>jcesarmobile에게 재질문:<ul>
<li>“이게 정말 알려진 Apple 버그인가?”</li>
<li>“이미 피드백이 제출된 상태인가?”</li>
</ul>
</li>
</ul>
<p><strong>🔹 문제를 유발한 부분 / 해결책 / 원인</strong></p>
<ul>
<li>별도의 새로운 문제, 해결책, 원인은 제시하지 않음.</li>
<li>단순 질문 댓글.</li>
</ul>
<hr>
<h2 id="🧩-사례-5--mattsbennett-첫-번째-글-댓글-4">🧩 사례 5 – mattsbennett (첫 번째 글 댓글 4)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“이 문제는 <strong>우리의 릴리즈 빌드에도 영향을 미쳤다</strong>”고 말함.</li>
<li>즉, 어떤 프로젝트에서는 <strong>릴리즈 빌드도 크래시</strong>했다고 언급.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분 / 해결책 / 원인</strong></p>
<ul>
<li>댓글 안에서 구체적인 원인이나 해결책은 언급하지 않음.</li>
<li>“릴리즈 빌드도 영향을 받았다”는 사실만 말함.</li>
</ul>
<hr>
<h2 id="🧩-사례-6--ionitron-bot-첫-번째-글-댓글-5">🧩 사례 6 – ionitron-bot (첫 번째 글 댓글 5)</h2>
<p><strong>🔹 내용 요약</strong></p>
<ul>
<li>이슈를 잠그고, 최신 Capacitor 버전에서 동일 문제가 있으면 새 이슈를 만들라고 안내하는 봇 메시지.</li>
</ul>
<p><strong>🔹 문제 상황 / 해결책 / 원인</strong></p>
<ul>
<li>실제 크래시 사례나 해결책, 원인을 서술하지 않음.</li>
<li>관리용 안내만 있음.</li>
</ul>
<hr>
<h2 id="🧩-사례-7--zer0x-두-번째-글-본문-uikitswift--swiftui-혼합-프로젝트">🧩 사례 7 – zer0X (두 번째 글 본문: UIKit/Swift + SwiftUI 혼합 프로젝트)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>대부분 UIKit/Swift 코드 + 일부 SwiftUI가 얹힌 <strong>복잡한 프로젝트</strong>.</li>
<li>여러 SPM 모듈과 내부 CocoaPods 라이브러리도 링크되어 있음.</li>
<li><strong>Xcode 15</strong>에서는 문제 없이 빌드되고, <strong>iOS 17.4 시뮬레이터에서 정상 실행</strong>.</li>
<li><strong>Xcode 16.0 Beta</strong>에서:<ul>
<li>iOS 18 시뮬레이터에서는 정상 실행.</li>
<li><strong>iOS 17.4 시뮬레이터에서 앱 시작 시 크래시</strong>.</li>
</ul>
</li>
<li>크래시 로그:<ul>
<li><code>Library not loaded: /System/Library/Frameworks/SwiftUICore.framework/SwiftUICore</code></li>
<li><code>SwiftUI_Common.framework</code>에서 SwiftUICore를 참조하려다 실패.</li>
<li>iOS 17.4 런타임 루트에도 SwiftUICore가 없다는 메시지.</li>
</ul>
</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>글 작성자는<ul>
<li>“Xcode가 앱을 빌드할 때 <strong>SwiftUICore.framework가 디바이스에 있을 것이라고 생각하는 것 같다</strong>”</li>
<li>그러나 SwiftUICore는 <strong>iOS 18에서 새로 생긴 프레임워크</strong>라 iOS 17.4에는 없다고 설명.</li>
</ul>
</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>이 본문 글에서는 <strong>구체적인 해결책을 아직 제시하지 않음</strong>.</li>
<li>“어떤 빌드 설정을 확인해야 할지 조언을 구한다”고만 되어 있음.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>명시적으로:<ul>
<li>“Xcode가 SwiftUICore가 런타임에 존재할 것이라고 잘못 가정하기 때문에, iOS 17.4에는 없어서 크래시가 발생한다”고 추정.</li>
</ul>
</li>
<li>또한 깨끗한 신규 Xcode 16 프로젝트에서는 재현되지 않는다고 밝힘.</li>
</ul>
<hr>
<h2 id="🧩-사례-8--tbaigner-두-번째-글-댓글-1-aug-24">🧩 사례 8 – tbaigner (두 번째 글 댓글 1, Aug ’24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“우리도 <strong>같은 크래시 로그를 가진 유사한 문제</strong>를 겪었다”고 말함.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>자기 경우 문제의 원인은<ul>
<li><strong>어떤 모듈 안에서 <code>UIViewControllerRepresentable</code> 구현을 사용한 것</strong>이라고 작성.</li>
</ul>
</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>해당 코드를 <strong>메인 앱 소스 트리로 옮기자 문제는 ‘해결됐다’</strong>고 말함.</li>
<li>다만, 코드 분리 관점에서는 바람직하지 않지만, 현재로서는 이 워크어라운드로 버티고 있다고 적음.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>“우리의 경우 문제의 근원은 <strong>모듈에서 사용된 UIViewControllerRepresentable 구현</strong>이었다”고 명시.</li>
</ul>
<hr>
<h2 id="🧩-사례-9--tbaigner-두-번째-글-댓글-2-sep-24">🧩 사례 9 – tbaigner (두 번째 글 댓글 2, Sep ’24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>다시 같은 스레드에서 zer0X에게 추가 정보 제공.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>“우리 쪽에서 크래시를 일으킨 모듈 이름에 <strong>언더스코어(_)가 들어 있었다</strong>”고 적음.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>해당 모듈 이름에서 <strong>언더스코어를 제거하여 이름을 바꾸자 문제가 해결되었다</strong>고 명시.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>“크래시를 일으킨 모듈 이름에 언더스코어가 있었다”는 점을 원인으로 적고 있음.</li>
</ul>
<hr>
<h2 id="🧩-사례-10--gravitybytes-두-번째-글-댓글-sep-24">🧩 사례 10 – GravityBytes (두 번째 글 댓글, Sep ’24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>자신의 팀도 <strong>같은 문제</strong>를 겪었다고 말함.</li>
<li>Xcode 16에서 <strong>런타임 ‘library not found’ 에러</strong>를 해결하기 위해 여러 모듈 이름을 바꿔야 했다고 설명.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>문제가 발생했을 때 모듈 이름:<ul>
<li><code>UIKit_Core</code></li>
<li><code>SwiftUI_Core</code></li>
<li><code>UI_Charts</code></li>
</ul>
</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>이 모듈들을 다음과 같이 이름 변경:<ul>
<li><code>UIKit_Core</code> → <code>CoreUIKit</code></li>
<li><code>SwiftUI_Core</code> → <code>CoreSwiftUI</code></li>
<li><code>UI_Charts</code> → <code>ChartsUI</code></li>
</ul>
</li>
<li>이렇게 바꾸고 나서 “<strong>우리 앱은 이제 iOS 17에서 실행된다</strong>”고 적음.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>글 안에서 “원인”을 따로 정의하진 않고,</li>
<li>“이 모듈들을 저렇게 이름 바꾸자 문제가 해결되었다”고만 서술.</li>
</ul>
<hr>
<h2 id="🧩-사례-11--jaxter2024-두-번째-글-댓글-sep-24">🧩 사례 11 – Jaxter2024 (두 번째 글 댓글, Sep ’24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>tbaigner와 GravityBytes가 말한 <strong>언더스코어 관련 워크어라운드는 자신의 경우에는 해당되지 않았다</strong>고 밝힘.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>구체적인 새 원인은 언급하지 않음.</li>
<li>다만 동일한 SwiftUICore 관련 크래시 케이스라는 전제.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>자신의 경우에는<ul>
<li>앱 타깃의 <strong>Build Settings → Other Linker Flags (OTHER_LDFLAGS)</strong> 에<ul>
<li><code>weak_framework SwiftUICore</code> 를 추가해야만 문제를 해결했다고 명시.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>“언더스코어 워크어라운드와 달리, 나의 경우는 <strong>weak framework 설정이 필요했다</strong>”는 점만 적어 둠.</li>
<li>Capacitor도 사용 중이라고 밝히고, Capacitor에 공식 버그 리포트를 올렸다고 링크로 안내.</li>
</ul>
<hr>
<h2 id="🧩-사례-12--surendrak11-두-번째-글-댓글-대댓글-1-feb-25">🧩 사례 12 – SurendraK11 (두 번째 글 댓글 대댓글 1, Feb 25)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>Jaxter2024가 말한 해결책을 적용하려다 문제를 겪음.</li>
<li><code>weak_framework SwiftUICore</code> 를 추가하면<ul>
<li>“<strong>Unknown argument: &#39;-weak_framework SwiftUICore&#39;</strong>”라는 컴파일 에러가 난다고 말함.</li>
</ul>
</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>Other Linker Flags에 <code>weak_framework SwiftUICore</code> 를 한 줄로 넣은 구성.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>이 첫 번째 댓글에는 아직 해결책이 없음.</li>
<li>“뭐가 빠졌는지 모르겠다”고만 말함.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>컴파일러가 <code>weak_framework SwiftUICore</code> 를 인식하지 못한 것처럼 보인다는 사실만 언급.</li>
</ul>
<hr>
<h2 id="🧩-사례-13--surendrak11-두-번째-글-댓글-대댓글-2-같은-날">🧩 사례 13 – SurendraK11 (두 번째 글 댓글 대댓글 2, 같은 날)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>위에서 말한 컴파일 에러를 해결했다고 후속 댓글로 알림.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li><code>weak_framework SwiftUICore</code> 를 한 줄에 통째로 넣은 방식.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>해결 방법:<ul>
<li><strong>Other Linker Flags에서 명령 전체를 두 줄로 나누어 입력</strong>해야 했다고 설명.<ul>
<li>1번째 줄: <code>weak_framework</code></li>
<li>2번째 줄: <code>SwiftUICore</code></li>
</ul>
</li>
</ul>
</li>
<li>이렇게 나누어 넣으니 weak framework 설정이 성공했다고 말함.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>“전체 명령을 두 줄로 나누어야 한다”는 점을 깨닫고 해결했다고만 서술.</li>
</ul>
<hr>
<h2 id="🧩-사례-14--vlad-lytyvnenko-두-번째-글-댓글-sep-24">🧩 사례 14 – Vlad Lytyvnenko (두 번째 글 댓글, Sep ’24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>자신의 경우에는 <strong>앱 타깃 이름이 문제의 원인</strong>이었다고 설명.</li>
<li>타깃 이름이 그냥 <code>&quot;App&quot;</code> 이었다고 밝힘.</li>
<li>이 이름 때문에 문제가 발생했고, 타깃 이름을 바꾸자 문제가 해결되었다고 함.</li>
<li>증상: <code>.zIndex(1)</code> modifier를 사용할 때 앱이 크래시했다고 적음.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li><strong>앱 타깃 이름이 <code>App</code></strong> 인 것.</li>
<li>그리고 <code>.zIndex(1)</code> modifier 사용 시 크래시가 나는 현상이 있었다고 설명.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li><strong>앱 타깃 이름을 다른 이름으로 변경</strong>하자 문제가 해결되었다고 명시.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>“이것은 어떤 이름 충돌(name collision)인 것 같다”고 적음.</li>
<li><code>.zIndex(1)</code> 뿐만 아니라 다른 modifier도 문제를 일으킬 수 있을 것 같다고 덧붙임.</li>
</ul>
<hr>
<h2 id="🧩-사례-15--adrian_dev78-vlad-댓글에-대한-대댓글-sep-24">🧩 사례 15 – adrian_dev78 (Vlad 댓글에 대한 대댓글, Sep 24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“이게 오늘 나를 살렸다. 우리 문제도 정확히 이것이었다”고 말함.</li>
<li>즉, <strong>같은 문제</strong>, 같은 증상.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li><p>자신의 프로젝트에서도 <strong>타깃 이름이 문제</strong>였다고 언급.</p>
<p>  (Vlad의 케이스와 동일하다고만 말함.)</p>
</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>그래서 타깃 이름을 변경했고, 그것이 해결책이었다고 말함.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>“타깃 이름이 문제였고, App이라는 이름이 좋은 접근이라고 생각했는데 놀랍다”고만 언급.</li>
</ul>
<hr>
<h2 id="🧩-사례-16--khusainova-vlad-댓글-대댓글-oct-24">🧩 사례 16 – Khusainova (Vlad 댓글 대댓글, Oct 24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“나도 해결됐다! 정말 고맙다”고 말함.</li>
<li>Vlad의 방법을 그대로 쓴 것으로 보인다고 언급.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분 / 해결책 / 원인</strong></p>
<ul>
<li>별도로 구체화하지 않고, Vlad의 해결책이 자신에게도 통했다고만 말함.</li>
<li>즉, <strong>앱 타깃 이름 변경이 자신의 문제도 해결했다</strong>는 의미.</li>
</ul>
<hr>
<h2 id="🧩-사례-17--sayaleepote-vlad-댓글-대댓글-oct-24">🧩 사례 17 – sayaleepote (Vlad 댓글 대댓글, Oct 24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“이것이 우리 쪽의 해결책이기도 했다. 고맙다”고 말함.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분 / 해결책 / 원인</strong></p>
<ul>
<li>역시 Vlad의 방법을 사용했다고만 언급.</li>
<li>구체적으로는, <strong>타깃 이름 변경이 해결책이었다</strong>는 의미로 읽힌다.</li>
</ul>
<hr>
<h2 id="🧩-사례-18--ggiri-vlad-댓글-대댓글-mar-25">🧩 사례 18 – ggiri (Vlad 댓글 대댓글, Mar 25)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“이것이 우리 문제의 해결책이기도 했다. 고맙다”고 말함.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분 / 해결책 / 원인</strong></p>
<ul>
<li>동일하게, Vlad가 말한 <strong>앱 타깃 이름 변경</strong>이 자신들의 문제도 해결했다고만 서술.</li>
</ul>
<hr>
<h2 id="🧩-사례-19--aurelienlajoinie-vlad-댓글-대댓글-apr-25">🧩 사례 19 – AurelienLajoinie (Vlad 댓글 대댓글, Apr 25)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“정말 고맙다, Vlad! Xcode 16으로 업그레이드한 후 Capacitor 앱에서 같은 오류가 났다”고 말함.</li>
<li>크래시 원인을 찾기 힘들었다고 덧붙임.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분 / 해결책 / 원인</strong></p>
<ul>
<li><p>구체적인 표현은 없지만, Vlad에게 특별히 감사하고 있으므로</p>
<p>  <strong>Vlad가 말한 앱 타깃 이름 변경이 자신의 Capacitor 앱에서도 문제를 해결해 줬다</strong>는 내용을 밝힘.</p>
</li>
</ul>
<hr>
<h2 id="🧩-사례-20--anton-ogarkov-두-번째-글-댓글-oct-24">🧩 사례 20 – anton-ogarkov (두 번째 글 댓글, Oct ’24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“SwiftUI를 사용하는 프레임워크가 있는 <strong>모든 모듈형(modular) 프로젝트에서 이 문제가 재현된다</strong>”고 말함.</li>
<li>“새 프로젝트에서도 이 문제를 재현할 수 있었다”고 적음.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>“SwiftUI를 사용하는 Framework가 있는 모듈형 프로젝트”라는 구조 자체를 문제를 재현시키는 조건으로 적음.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li>해결책은 제시하지 않고, “피드백 티켓을 제출했다”고만 밝힘.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>별도의 원인 분석은 없이, “이 구조에서 재현된다”고만 서술.</li>
</ul>
<hr>
<h2 id="🧩-사례-21--michaelfromtempleton-anton-댓글-대댓글-oct-24">🧩 사례 21 – michaelfromtempleton (anton 댓글 대댓글, Oct 24)</h2>
<p><strong>🔹 내용</strong></p>
<ul>
<li>“피드백 티켓 제출 후 업데이트된 내용 있느냐, 나도 같은 문제를 겪고 있다”고 묻는 댓글.</li>
</ul>
<p><strong>🔹 문제 / 해결책 / 원인</strong></p>
<ul>
<li>자신의 문제가 동일하다고만 말하고,</li>
<li>별도의 해결책이나 원인 설명은 없음.</li>
</ul>
<hr>
<h2 id="🧩-사례-22--gregios-두-번째-글-댓글-oct-24">🧩 사례 22 – gregios (두 번째 글 댓글, Oct ’24)</h2>
<p><strong>🔹 문제 상황</strong></p>
<ul>
<li>“우리도 같은 문제를 겪고 있다”고 말함.</li>
<li>환경: Xcode 16.0, 앱은 iOS 18과 iOS 16에서는 잘 작동하지만, <strong>iOS 17에서만 크래시</strong>.</li>
</ul>
<p><strong>🔹 문제를 유발한 부분</strong></p>
<ul>
<li>자신의 경우, 문제를 일으킨 프레임워크 이름은 <strong><code>UI.framework</code></strong> 였다고 밝힘.</li>
</ul>
<p><strong>🔹 바꾸니 해결된 것 / 해결책</strong></p>
<ul>
<li><code>UI.framework</code> 의 이름을 <strong><code>CoreUI</code></strong> 로 변경하자 앱이 정상적으로 동작했다고 말함.</li>
<li>또한, 2년 전에 <code>Settings.framework</code> 를 프로젝트에 포함했을 때도 비슷한 문제를 겪었다고 언급.</li>
</ul>
<p><strong>🔹 글에서 말한 원인</strong></p>
<ul>
<li>“SwiftUI와 UI 둘 다 프레임워크 이름의 접두사(prefix)가 될 수 없는 것 같다”고 적음.</li>
<li>구체적인 내부 원인은 설명하지 않고, 이름이 문제라고만 서술.</li>
</ul>
<p>여기에서 말한 모든 문제를 체크</p>
<p>weak_framework말고는 나한테 해당하는 문제는 없었음</p>
<p>weak_framework을 app.json에 아무리 넣어도 빌드 후 ios17환경에서 키면 바로 튕김</p>
<p>Xcode를 직접 열어보기로 결정</p>
<p>EAS prebuild로 ios폴더 생성후 ios폴더 내부 D.xcworkspace를 XCode로 열음</p>
<p>그리고  ios17버전 기기 시뮬레이터로 실행해봄. crashlog와 함께 튕김.(실제 기기랑 같은 오류)</p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/4121bc88-a26a-4735-b85b-3009af4da1b3/image.png" alt=""></p>
<p>보다시피 app.json에 추가를 했는데도 불구하고  아래 두개가 안들어가있음.</p>
<pre><code class="language-tsx">-weak_framework
SwiftUICore</code></pre>
<p>Debug, Release에 각각 넣음
<img src="https://velog.velcdn.com/images/miin-hyukkk/post/52a7d3dc-1eff-4670-8c4e-b74f3f20b2c1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/06f5442a-e627-4d13-9f3c-2b95cdeaa969/image.png" alt=""></p>
<p>넣고 Xcode에서 ios17버전 기기 시뮬레이터로 실행해봄.</p>
<p>잘 실행됨</p>
<p>이제 내가 해야하는건 </p>
<p>Xcode에서 + 버튼 눌러 넣은 것처럼, prebuild 후에도 자동으로 동일하게 Other Linker Flags에 -weak_framework SwiftUICore가 들어가도록 만들어야함</p>
<p>즉: <strong>app.json / plugins 설정만으로 Xcode가 동일한 설정을 자동 생성하게 하고 싶은 것.</strong></p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/73c6ac91-f219-4003-9756-72faa47860b4/image.png" alt=""></p>
<p>플러그인 파일을 하나 만듦
빌드된 iOS 앱이 특정 프레임워크를 선택적(weakly)으로 링크하도록 Xcode 프로젝트 설정을 수정하는 역할</p>
<pre><code class="language-tsx">const { withXcodeProject } = require(&#39;@expo/config-plugins&#39;);

const WEAK_FLAG = &#39;-weak_framework&#39;;
const FRAMEWORK_NAME = &#39;SwiftUICore&#39;;

function addWeakSwiftUICoreFlag(project) {
  const configurations = project.pbxXCBuildConfigurationSection();

  for (const key in configurations) {
    const cfg = configurations[key];
    if (typeof cfg !== &#39;object&#39;) continue;
    const buildSettings = cfg.buildSettings;
    if (!buildSettings) continue;

    let flags = buildSettings.OTHER_LDFLAGS;

    // 기존에 아무 플래그가 없으면 새로 세팅
    if (!flags) {
      buildSettings.OTHER_LDFLAGS = [WEAK_FLAG, FRAMEWORK_NAME];
      continue;
    }

    // string일 수도 있고 array일 수도 있어서 정규화
    if (typeof flags === &#39;string&#39;) {
      flags = [flags];
    }

    const joined = flags.join(&#39; &#39;);
    // 이미 SwiftUICore 관련 플래그가 있으면 스킵
    if (joined.includes(&#39;SwiftUICore&#39;)) {
      continue;
    }

    flags.push(WEAK_FLAG, FRAMEWORK_NAME);
    buildSettings.OTHER_LDFLAGS = flags;
  }

  return project;
}

module.exports = function withWeakSwiftUICore(config) {
  return withXcodeProject(config, (config) =&gt; {
    config.modResults = addWeakSwiftUICoreFlag(config.modResults);
    return config;
  });
};</code></pre>
<p>app.json을 아래처럼 수정</p>
<pre><code class="language-tsx">{
  &quot;expo&quot;: {
    &quot;name&quot;: &quot;마켓:D&quot;,
    &quot;slug&quot;: &quot;marketd&quot;,
    &quot;version&quot;: &quot;1.0.14&quot;,
    &quot;orientation&quot;: &quot;portrait&quot;,
    &quot;scheme&quot;: &quot;com.byuckchon.marketd&quot;,
    &quot;userInterfaceStyle&quot;: &quot;automatic&quot;,
    &quot;newArchEnabled&quot;: true,
    &quot;icon&quot;: &quot;./assets/images/app-icon-512.png&quot;,

....

    &quot;plugins&quot;: [
      &quot;./plugins/withFirebaseModularPods&quot;,
      &quot;./plugins/withWeakSwiftUICore&quot;, // 해당 줄 추가
      &quot;expo-router&quot;,
      &quot;@react-native-firebase/app&quot;,
      &quot;@react-native-firebase/crashlytics&quot;,
      [
        &quot;expo-splash-screen&quot;,
        {
          &quot;image&quot;: &quot;./assets/images/splash-image.png&quot;,
          &quot;imageWidth&quot;: 300,
          &quot;resizeMode&quot;: &quot;contain&quot;,
          &quot;backgroundColor&quot;: &quot;#ED6D01&quot;
        }
      ],
      [
        &quot;@react-native-seoul/naver-login&quot;,
        {
          &quot;urlScheme&quot;: &quot;com.byuckchon.marketd&quot;
        }
      ],
....

}</code></pre>
<p>이렇게 수정 후 ios 폴더 삭제 후</p>
<pre><code class="language-tsx">npx expo prebuild --clean</code></pre>
<p>프리빌드한다음 Xcode에서 열어보면 아래처럼 weak-framework가 포함된걸 확인 가능</p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/cf590dcc-e532-43af-ae0e-0cc0778269f6/image.png" alt=""></p>
<p>이제 production 빌드 후 submit 한 뒤에 ios 17실제 기기에서 확인한 결과…..</p>
<p>아래처럼 잘 나옴 드디어… 시바</p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/1389f047-5054-4d71-8fa6-8ecfba7e9b6a/image.png" alt=""></p>
<p>주변에 ios17 기기가 동생밖에 없어서 동생을 테스터로 등록하고 매번 빌드마다 요청</p>
<p>해결완료
<img src="https://velog.velcdn.com/images/miin-hyukkk/post/2f33df9e-69db-4a99-bcbc-0904527398de/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] 다이나믹 임포트]]></title>
            <link>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%8B%A4%EC%9D%B4%EB%82%98%EB%AF%B9-%EC%9E%84%ED%8F%AC%ED%8A%B8</link>
            <guid>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%8B%A4%EC%9D%B4%EB%82%98%EB%AF%B9-%EC%9E%84%ED%8F%AC%ED%8A%B8</guid>
            <pubDate>Tue, 17 Sep 2024 04:32:38 GMT</pubDate>
            <description><![CDATA[<p>다이나믹 임포트 활용하기</p>
<h2 id="취지">취지</h2>
<p>물론 내가 다이나믹 임포트 처리를 진행할 컴포넌트 크기가 미미해서, 번들사이즈에 크게 영향을 줄지는 의문이었다.
하지만 이것을 진행했던 취지는 번들 사이즈를 줄여본 경험을 해보고 싶어서였다.</p>
<h2 id="들어가기-전에">들어가기 전에</h2>
<p>hydrate라는 개념, 그리고 리엑트에서의 hydrate과 next.js에서의 hydrate의 차이를 명확히 알고 해당 작업을 진행해야 함 -&gt; <a href="https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Hydrate">링크</a></p>
<p>Dynamic Import는 첫 렌더링 시에 바로 보여줄 필요가 없는 요소들을 걷어내어 성능을 향상시키는 방식임.</p>
<p>내 프로젝트에서는 Footer에 적용할 예정이므로 Footer로 예를 들어보겠음</p>
<blockquote>
<p>초기 Preredering 시 Footer에 대한 컴포넌트는 완전히 제외된 상태로 렌더링을 한 뒤, Client Side 단에서 실제 Footer 열게 되면, 서버 단에 해당 Footer 관련 Chunk File을 요청하여 별도 렌더링을 진행하게 됨</p>
</blockquote>
<p>개념을 명확히 알면 여기까지는 그렇게 어려운 작업이 아님</p>
<h2 id="작업-진행-순서">작업 진행 순서</h2>
<ol>
<li>footer를 그냥 import</li>
<li>footer를 dynamic import</li>
<li>footer를 dynamic import + IntersectionObserver</li>
</ol>
<h3 id="1-footer를-그냥-import">1. footer를 그냥 import</h3>
<p><code>ANALYZE=true yarn build</code>로 번들사이즈를 체크
(<code>scr/componets/Layout</code> 내부에 <code>Footer</code>컴포넌트가 존재함)
<img src="https://velog.velcdn.com/images/miin-hyukkk/post/1b42a06c-41ee-4e84-9998-1727ff0636a3/image.png" alt=""></p>
<h3 id="2-footer를-dynamic-import">2. footer를 dynamic import</h3>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/50ae34ac-3273-483e-aacb-6fd3badbe558/image.png" alt=""></p>
<pre><code>const Footer = dynamic(() =&gt; import(&quot;@/components/Layout/Footer&quot;), {
  ssr: false,
});</code></pre><p>해당 경로의 번들사이즈가 15.35KB-&gt; 11.41KB로 줄어든걸 확인할 수 있음.</p>
<p>마찬가지로 네트워크 창에서 확인해보면 아래 이미지와 같이 chunk파일이 분리된걸 확인할 수 있음.
<img src="https://velog.velcdn.com/images/miin-hyukkk/post/12ad4dcb-1ec6-45aa-bc98-1973d9773133/image.png" alt=""></p>
<p>하지만 내가 원하는 것은 사용자의 뷰포인트에 <code>Footer</code>가 등장하기 전까지 청크 파일이 생성되지 않는 것이였음</p>
<h3 id="3-footer를-dynamic-import--intersectionobserver">3. footer를 dynamic import + IntersectionObserver</h3>
<blockquote>
<p><code>IntersectionObserver</code>는 웹 API로, 요소가 뷰포트(브라우저의 가시 영역)와 교차하는 시점을 관찰하고 이를 기반으로 특정 작업을 수행</p>
</blockquote>
<pre><code>useEffect(() =&gt; {
  const footerPlaceholder = document.getElementById(&quot;footer-placeholder&quot;);

  if (footerPlaceholder) {
    const observer = new IntersectionObserver(
      (entries) =&gt; {
        if (entries[0].isIntersecting) {
          setLoadFooter(true);
          observer.disconnect();
        }
      },
      { rootMargin: &quot;100px&quot; } // 이 설정으로 footer가 뷰포트에 들어오기 100px 전에 로드됩니다.
    );

    observer.observe(footerPlaceholder);

    // 컴포넌트가 unmount될 때 observer 해제
    return () =&gt; observer.disconnect();
  }
}, []);
</code></pre><p>해당 코드를 통해 footer가 뷰포인트에 등장 전에는 chunk 파일을 요청하지 못하도록 제어함.</p>
<blockquote>
<p>네트워크 탭을 확인하면, <code>document</code>에서는 <code>Footer</code>가 빠져있고, 뷰포인트에 등장 <code>100px</code>전에 <code>Footer</code> 파일이 생성되는 것을 확인 가능함</p>
</blockquote>
<h3 id="4-dynamic-import--seo">4. dynamic import + SEO</h3>
<p><strong>만약, Footer에 SEO에 영향을 주는 요소가 있어서 Footer요소가 html에 포함되길 원하다면 어떻게 해야할까?</strong></p>
<blockquote>
<p>next.js에서 다이나믹 임포트로 <code>Footer</code> 요소를 불러오게된다면 PreRendering 된 Document에서도 보이지 않게 됨</p>
</blockquote>
<blockquote>
<p><strong>기존 html에서는 footer요소가 들어가서 seo에 영향을 주면서 동시에 footer Chunk File이 필요한 순간에 Lazy 하게 요청</strong>을 하는 것임</p>
</blockquote>
<p>구글링을 해보니, 해당 기능을 구현하려면, 설정파일들에 커스텀이 필요한 것을 알았다.
구글링과 gpt를 이용해서 구현해보려했으나, 깊은 이해가 없이는 구현이 힘들것 같다는 판단이 들었다.</p>
<p>(기본 제공되는 기능으로는 dynamic만 되고, html에는 안들어감...)</p>
<p>별도의 Webpack 플러그인이나 빌드 시 커스텀 처리가 필요</p>
<hr>
<p>다음 포스트에서 해당 기능을 구현해보며 이해한 내용을 작성해볼 예정.......
.
.
.
끝🫠</p>
<p>참고한 블로그 : <a href="https://helloinyong.tistory.com/323">https://helloinyong.tistory.com/323</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] 회고록...]]></title>
            <link>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Mon, 02 Sep 2024 03:12:28 GMT</pubDate>
            <description><![CDATA[<h1 id="회고록">회고록</h1>
<h2 id="프로젝트-취지">프로젝트 취지</h2>
<p>2개의 프로젝트를 진행해봤으니, 마지막으로 사용자가 실제로 존재하는 프로젝트를 진행해보고 싶다는 생각을 함. 
친구의 sns 계정을 이용해서 쇼핑몰을 오픈해보려고함.</p>
<p>처음 기획할때는 쇼핑몰이니 의류들을 보여주는 shop페이지, 마이페이지 등등 가볍게 생각하고 진행.</p>
<p>하지만 관리자페이지 구현,결제 시스템 연동, 쇼핑몰 특성상 많은 이미지를 딜레이없이 보여주는 등 생각할 것들이 굉장히 많았음....</p>
<p>그 전 프로젝트에서는 넘어갔단 작은 이슈들에 대해 정말 사용자가 있다고 생각하니 넘어갈 수 있는 문제가 하나도 없었고, + 처음 6개월정도로 기획했던 프로젝트는 1년이 되어서야 마무리 됨</p>
<h2 id="초기-기획의도와-비교했을-때-바뀐-점">초기 기획의도와 비교했을 때 바뀐 점</h2>
<p>친구가 sns활동을 하면서 장기적으로 사용할 쇼핑몰을 만들려고 했으나 여러 비용적인 문제로 이벤트성으로 한달정도만 이벤트성으로 쇼핑몰을 오픈하기로 결정 -&gt; 주 컨텐츠가 ‘<strong>이벤트</strong>’로 바뀜</p>
<p>다양한 sns 홍보 + 친구 sns 계정 팔로워수로 인해 약 1500명의 회원이 생성됐고, 약 2만건의 이벤트참여율이 발생함.</p>
<h2 id="seo향상을-위해-노력한-이유---쇼핑몰이니까">SEO향상을 위해 노력한 이유   “쇼핑몰이니까&quot;</h2>
<p>csr만 작업을 해온 나에겐 첫 도전이였음.</p>
<p>구글에 어떻게 하면 우선순위로 검색이 되고, 어떤 메타태그를 사용해야 페이지 노출 정도를 높일 수 있고 등등에 대해 많이 고민을 함.</p>
<p>어떤 데이터는 SSR로, 또 어떤 데이터는 SSG로 구현을 해야겠다 등등 많은 고민을 함.</p>
<h2 id="초기-뷰포인트-로딩시간--너무-오래걸림---lighthouse-점수">초기 뷰포인트 로딩시간  너무 오래걸림 -&gt; lighthouse 점수</h2>
<p>페이지를 로딩하는 데 너무 오랜 시간이 걸려 뭐가 문제인지 고민을 좀 해봄</p>
<p>비록 결과적으로는 많은 도움이 되지는 않았지만, 경험이라 생각하고 번들사이즈를 줄이려고 노력을 해봄.</p>
<blockquote>
<ol>
<li>사용자의 뷰포인트에 당장 등장하지 않는 요소는 나중에 hydrate해보자!</li>
<li>일부 사용자에게만 보이는 팝업창 요소는 다이나믹 임포트로 구현해보자!</li>
<li>이미지를 전부 next/image로 변경!</li>
<li>폰트를 otf-&gt;wott2로 변경!</li>
</ol>
</blockquote>
<p>결과적으로 눈의 띄게 큰 변화를 준 것은 3,4번이였음</p>
<h2 id="테스트-코드의-중요성을-피부로-직접-깨달음">테스트 코드의 중요성을 피부로 직접 깨달음</h2>
<p>회사 지원을 하다보면 여기저기 테스트코드에 대한 언급이 많음.
그리하여 직전 프로젝트의 경우 jest를 통해 unit test를 진행하고 cypress를 통해 e2e테스트를 진행함.
하지만 하면서도 이 테스트코드가 정말 도움이 될까라는 생각이 많이 들었음. 왜냐면 이미 잘될거를 알았으니까. 그런데 시간도 많이 드니까.</p>
<p>이런 생각을 가진채로 이번 프로젝트의 경우 정말 일부의 기능만 e2e테스트를 진행함.</p>
<p>로그인, 결제, 장바구니 위 세기능에 대해만 작성함</p>
<p><strong>그리고 오픈함</strong></p>
<p>이벤트는 약 4일간 진행되었고 매일매일 약 1000명이 넘는 사람들이 우리의 페이지를 방문함. 그러던 도중 백엔드에서 정말 사소한 이슈가 발생했고, 해당 코드 한줄을 고침으로써 사용자의 정보가 담긴 디비가 꼬이는 일이 발생함.</p>
<p>남들은 어떻게 생각할지 모르겠지만 나는 테스트커버리지가 높은 코드라면 해당 이슈들이 발생하지 않았을 것이라고 생각이 들었음.</p>
<p>결국 사이드 이펙트 체크가 정말 중요하다는 거임. 사용자가 많아져서 미처 발견하지 못한 이슈가 발생하거나, 작은 기능하나가 추가될때 나머지 기능들이 원활하게 돌아가는지 메인에 코드가 푸쉬되고 빌드가 되기전에 개발자가 알아야한다는 거임.</p>
<h2 id="새로운-테스트코드-툴--api-요청-수-확인-후-이상-감지">새로운 테스트코드 툴? -&gt;api 요청 수 확인 후 이상 감지</h2>
<p>마찬가지로 프론트엔드 쪽에서도 이벤트 참여이후 응모하기 버튼이 즉시 멈추지않아서 급하게 조건문없이 refetch코드를 작성했다가, 응모내역조회가 응모상품페이지 방문 시 10번 이상 발생함. </p>
<p>코드 추가 후 같은 api가 중복호출되는 경우를 잡을 수는 없을까?
e2e테스트 코드에서 해당 api가 한번만 불리는게 맞는지 체크하는 코드를 작성해보면 어떨까?</p>
<blockquote>
<p><strong>경험삼아 메인코드에 푸쉬전에 api 요청 수 확인해서 한기능에 한번씩 요청을 하고 있는지 테스트코드를 추가해보기로 결정</strong></p>
</blockquote>
<h2 id="유지보수의-중요성">유지보수의 중요성</h2>
<p>1년동안 길어지면서 7~8개월 전에 작성했던 코드에 대해 직관적으로 이해가 되지 않는 경우들이 발생. 
<strong>일반적인 네이밍과 구조!</strong>가 얼마나 중요한지 알게됨</p>
<p>.
.
.</p>
<p>친구의 계정 덕분에 너무너무 귀중한 경험을 했고, 이력서에 잘 반영해봐야겠다. 끝!🫠</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우타스] 타입 조합]]></title>
            <link>https://velog.io/@miin-hyukkk/%EC%9A%B0%ED%83%80%EC%8A%A4-%ED%83%80%EC%9E%85-%EC%A1%B0%ED%95%A9</link>
            <guid>https://velog.io/@miin-hyukkk/%EC%9A%B0%ED%83%80%EC%8A%A4-%ED%83%80%EC%9E%85-%EC%A1%B0%ED%95%A9</guid>
            <pubDate>Tue, 27 Aug 2024 03:50:50 GMT</pubDate>
            <description><![CDATA[<p>좀 심화한 타입 검사에 필요한 지식들</p>
<h2 id="교차-타입intersection">교차 타입(Intersection)</h2>
<p>여러 가지 타입을 결합하여 하나의 단일 타입으로 만들 수 있음
&amp;를 사용해서 표기함. <code>A&amp;B</code>
결과물로 탄생한 단일 타입에는 타입별칭(type alias)을 붙일 수 있음
<code>type ProductItemWithDiscount = ProductItem &amp; { discountAmount : number }</code></p>
<h2 id="유니온-타입">유니온 타입</h2>
<p><code>A|B</code> 타입 A나 B중 하나에 해당</p>
<h2 id="인덱스-시그니처">인덱스 시그니처</h2>
<p>특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 있을 때 사용하는 문법</p>
<pre><code>interface IndexSignatureEx2 {
  [key: string]: number | boolean;
  length: number;
  isValid: boolean;
  name: string; // 에러 발생
}</code></pre><p>인덱스 시그니처의 키가 <code>string</code>일 때는 <code>number | boolean</code> 타입이 오게끔 선언되어 있어서 에러 발생</p>
<h2 id="인덱스드-엑세스-타입">인덱스드 엑세스 타입</h2>
<p>다른 타입의 특정 속성이 가지는 타입을 조회,추출하기 위해 사용됨</p>
<pre><code>type Person = {
  name: string;
  age: number;
  location: string;
};

// 인덱스드 엑세스 타입을 사용하여 &#39;Person&#39; 타입의 &#39;name&#39; 속성의 타입을 가져옵니다.
type NameType = Person[&#39;name&#39;]; // NameType은 string 타입이 됩니다.
</code></pre><h2 id="맵드-타입mapped-types">맵드 타입(Mapped Types)</h2>
<p>자바스크립트 map : 배열 A를 기반으로 새로운 배열 B를 만들어내는 배열 메서드
마찬가지로 맵드 타입은 다른 타입을 기반으로 한 타입을 선언할 때 사용하는 문법
인덱스 시그니처 문법을 사용해서 반복적인 타입 선언을 효과적으로 줄일 수 있음</p>
<pre><code>type Example = {
  a: number;
  b: string;
  c: boolean;
};

type Subset&lt;T&gt; = {
  [K in keyof T]?: T[K];
};

const aExample: Subset&lt;Example&gt; = { a: 3 };
const bExample: Subset&lt;Example&gt; = { b: &quot;hello&quot; };
const acExample: Subset&lt;Example&gt; = { a: 4, c: true };</code></pre><p><code>Subset&lt;T&gt;</code>는 <strong>T</strong>타입의 모든 속성을 선택적으로 포함할 수 있는 새로운 타입을 만듦</p>
<p>맵드 타입이 실제로 사용된 예시
배민 선물하기 서비스에는 &#39;바텀시트&#39;라는 컴포넌트개 존재. 밑에서부터 스르륵 올라오는 모달. 이 바텀시트는 선물하기 서비스의 최근 연락처 목록,카드 선택, 상품 선택 등 여러 지면에서 사용됨. 바텀시트마다 각각 resolver, isOpened 등의 상태를 관리하는 스토어가 필요한데 이 스토어의 타입(BottomSheetMap)을 선언해줘야 함. 
이때 이 타입에 존재하는 모든 키에 대해 일일이 스토어를 만들어줄 수도 있지만 불필요한 반복이 발생함.</p>
<p>이럴때 인덱스 시그니처 문법을 사용해서 BottomSheetMap을 기반으로 각 키에 해당하는 스토어를 선언할 수 있음.</p>
<pre><code>const BottomSheetMap = {
  RECENT_CONTACTS: RecentContactsBottomSheet,
  CARD_SELECT: CardSelectBottomSheet,
  SORT_FILTER: SortFilterBottomSheet,
  PRODUCT_SELECT: ProductSelectBottomSheet,
  REPLY_CARD_SELECT: ReplyCardSelectBottomSheet,
  RESEND: ResendBottomSheet,
  STICKER: StickerBottomSheet,
  BASE: null,
};

// BOTTOM_SHEET_ID는 BottomSheetMap에서 정의된 키들 중 하나만을 허용하는 타입
export type BOTTOM_SHEET_ID = keyof typeof BottomSheetMap;

// 불필요한 반복이 발생한다
type BottomSheetStore = {
  RECENT_CONTACTS: {
    resolver?: (payload: any) =&gt; void;
    args?: any;
    isOpened: boolean;
  };
  CARD_SELECT: {
    resolver?: (payload: any) =&gt; void;
    args?: any;
    isOpened: boolean;
  };
  SORT_FILTER: {
    resolver?: (payload: any) =&gt; void;
    args?: any;
    isOpened: boolean;
  };
  // ...
};

// Mapped Types를 통해 효율적으로 타입을 선언할 수 있다
type BottomSheetStore = {
  [index in BOTTOM_SHEET_ID]: {
    resolver?: (payload: any) =&gt; void;
    args?: any;
    isOpened: boolean;
  };
};</code></pre><h2 id="템플릿-리터럴-타입">템플릿 리터럴 타입</h2>
<p>자바스크립트의 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법</p>
<pre><code>type Stage =
  | &quot;init&quot;
  | &quot;select-image&quot;
  | &quot;edit-image&quot;
  | &quot;decorate-card&quot;
  | &quot;capture-image&quot;;
type StageName = `${Stage}-stage`;
// ‘init-stage’ | ‘select-image-stage’ | ‘edit-image-stage’ | ‘decorate-card-stage’ | ‘capture-image-stage’</code></pre><h2 id="제네릭">제네릭</h2>
<p>제네릭은 정적 언어에서 다양한 타입 간에 <strong>재사용성</strong>을 높이기 위해사용하는 문법임.
<code>사전적 의미</code> : 특징이 없거나 일반적인 것</p>
<p>한마디로 일반화된 데이터 타입
내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워둔 다음에, 실제로 그 값을 사용할 때 외부에서 <strong>타입 변수 자리에 타입을 지정하여 사용</strong>하는 방식</p>
<p>보통 타입 변수명으로 <strong><code>T(Type)</code>, <code>E(Element)</code>, <code>K(Key)</code>, <code>V(Value)</code></strong> 등 한글자로 된 이름을 주로 사용</p>
<p>제네릭은 any랑 다름. any처럼 아무 타입이나 무분별하게 받는 게 아니라, 배열 생성 시점에 원하는 타입으로 특정할 수 있음. 다시 말해 제네릭을 사용하면 배열 요소가 전부 동일한 타입이라고 보장할 수 있음</p>
<p>제네릭 함수를 호출할 때 반드시 꺾쇠괄호(&lt;&gt;)안에 타입을 명시해야 하는 것은 <strong>아님</strong>. 타입을 명시하는 부분을 생략하면 컴파일러가 인수를 보고 타입을 추론해줌. <strong>타입 추론 가능한 경우엔 생략 가능!</strong></p>
<pre><code>function identity&lt;T&gt;(value: T): T {
  return value;
}

// 타입을 명시하지 않음. 컴파일러가 &#39;number&#39; 타입으로 추론.
const num = identity(42); // num의 타입은 number

// 타입을 명시하지 않음. 컴파일러가 &#39;string&#39; 타입으로 추론.
const str = identity(&quot;hello&quot;); // str의 타입은 string</code></pre><p>제네릭에 기본 값 추가도 물론 가능!</p>
<p>함수나 클래스 등의 내부에서 제네릭을 사용할 때 <strong>어떤 타입이든 될 수 있다는 개념</strong>을 알고 있어야해
특정한 타입에서만 존재하는 멤버를 <strong>참조하려고 하면 안됨</strong>. 배열에만 존재하는 length 속성을 제네릭에서 참조하려고 하면 당연히 에러가 발생함. 당연히 안됨 ㅇㅇ</p>
<pre><code>function exampleFunc2&lt;T&gt;(arg: T): number {
  return arg.length; // 에러 발생: Property ‘length’ does not exist on type ‘T’
}</code></pre><p><code>제네릭 타입은 호출 시점에 어떤 구체적인 타입으로 결정되기 때문에, T에 대해 length 속성이 정의되어 있는지 여부를 컴파일러가 알 수 없음</code></p>
<p>이럴땐!.. 제네릭 꺾쇠괄호 내부에 <strong>&#39;length속성을 가진 타입만 받는다&#39;</strong> 라는 제약을 걸어주면 사용할 수 있음.</p>
<pre><code>interface TypeWithLength {
  length: number;
}

function exampleFunc2&lt;T extends TypeWithLength&gt;(arg: T): number {
  return arg.length;
}</code></pre><p>화살표 함수에 제네릭 사용하면 에러 발생함. JSX의 태그와 제네릭의 꺾쇠괄호를 혼동해버림.
extends키워드 사용하던지, 일반 함수 사용하던지!</p>
<h2 id="제네릭-사용하기">제네릭 사용하기</h2>
<ul>
<li>함수의 제네릭</li>
<li>호출 시그니처의 제네릭</li>
<li>제네릭 클래스</li>
<li>제한된 제네릭</li>
<li>확장된 제네릭</li>
</ul>
<p>제네릭의 장점이 뭐냐하면...
<strong>다양한 타입을 받게함</strong>으로써 <strong>코드를 효율적으로 재사용</strong>할 수 있다는 것!
그렇다면 현업에서는 언제 가장 많이 쓰일까?</p>
<h3 id="api-응답-값-타입을-지정할-때">API 응답 값 타입을 지정할 때!</h3>
<pre><code>export interface MobileApiResponse&lt;Data&gt; {
  data: Data;
  statusCode: string;
  statusMessage?: string;
}</code></pre><p>API응답 값에 따라 달라지는 data를 <strong>제네릭 타입 Data</strong>로 선언하고 있음
이렇게 만든 <code>MobileApiResponse</code>는 실제 API 응답값의 타입을 지정할때 아래처럼 사용됨</p>
<pre><code>export const fetchPriceInfo = (): Promise&lt;MobileApiResponse&lt;PriceInfo&gt;&gt; =&gt; {
  const priceUrl = &quot;https: ~~~&quot;; // url 주소

  return request({
    method: &quot;GET&quot;,
    url: priceUrl,
  });
};

export const fetchOrderInfo = (): Promise&lt;MobileApiResponse&lt;Order&gt;&gt; =&gt; {
  const orderUrl = &quot;https: ~~~&quot;; // url 주소

  return request({
    method: &quot;GET&quot;,
    url: orderUrl,
  });
};</code></pre><p>제네릭이 과하게 사용되면 가독성을 해치기 때문에 코드를 읽고 타입을 이해하기가 어려워짐
복잡한 제네릭은 <strong>의미 단위로 분할</strong>해서 사용하는게 좋다</p>
<p>.
.
.
끝
🫠</p>
<p>출처: 우아한 타입스크립트 with 리엑트</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우타스] TS만의 독자적 타입 시스템]]></title>
            <link>https://velog.io/@miin-hyukkk/%EC%9A%B0%ED%83%80%EC%8A%A4-TS%EB%A7%8C%EC%9D%98-%EB%8F%85%EC%9E%90%EC%A0%81-%ED%83%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@miin-hyukkk/%EC%9A%B0%ED%83%80%EC%8A%A4-TS%EB%A7%8C%EC%9D%98-%EB%8F%85%EC%9E%90%EC%A0%81-%ED%83%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Mon, 26 Aug 2024 14:55:41 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기전에">들어가기전에</h2>
<p>자바스크립트에 any타입이 있는가?
any타입은 타입스크립트에만 존재하는 독자적인 타입 시스템으로 간주됨</p>
<p>하지만 any타입의 개념은 이미 자바스크립트에서 사용됨. 이름 그대로 어떤 타입이든 매핑할 수 있는 성질을 가지고 있고, 이건 원래 자바스크립트의 사용방식과 일치하기 때문</p>
<p><strong>지금 알아볼 모든 타입 시스템은 타입스크립트에만 존재하는 키워드이지만 그 개념은 자바스크립트에 기인한 타입시스템이라는 점을 인지하자</strong></p>
<h2 id="any타입">any타입</h2>
<p>any타입은 타입스크립트로 달성하고자 하는 정적 타이핑을 무색하게 만듦
any타입은 지양해야할 패턴.회피하는 것이 좋은 습관</p>
<p><code>tsconfig.json</code>에서 <code>noImplicitAny</code>옵션을 활성화하면 any타입에 대한 경고를 발생시킬 수 있음</p>
<p><strong>하지만 어쩔 수 없이 사용해야할 때가 있음</strong></p>
<h3 id="개발-단계에서-임시로-값을-지정해야-할때">개발 단계에서 임시로 값을 지정해야 할때</h3>
<p>세부항목에 대해 아직 타이빙 확정되지 않은 경우</p>
<h3 id="어떤-값을-받아올지-또는-넘겨줄-지-정할-수-없을때">어떤 값을 받아올지 또는 넘겨줄 지 정할 수 없을때</h3>
<pre><code>type FeedbackModalParams = {
    show: boolean;
    content: string;
    cancelButtonText?: string;
    confirmButtonText?: string;
    beforeOnClose?: () =&gt; void;
    action?: any;
  };</code></pre><p>acition속성이 any로 선언된 것을 볼 수 있음. action속성은 모달 차을 그릴 때 실행될 함수를 의미함
모달창을 화면에 그릴 때 다양한 범주의 액션에 따라 읹자의 개수나 타입을 일일이 명시하기 힘들 수 있음</p>
<h3 id="값을-예측할-수-없을-때-암묵적으로-사용">값을 예측할 수 없을 때 암묵적으로 사용</h3>
<p>그래도 지양해라~</p>
<h2 id="unknown-타입">unknown 타입</h2>
<p>any와 유사하게 <strong>모든 타입의 값이 할당&#39;될&#39;</strong> 수 있음
그러나 any를 제외한 다른 타입으로 선언된 변수에는 unknown <strong>타입 값을 할당&#39;할&#39; 수 없음</strong></p>
<pre><code>let unknownValue: unknown;

unknownValue = 100; // any 타입과 유사하게 숫자이든
unknownValue = &quot;hello world&quot;; // 문자열이든
unknownValue = () =&gt; console.log(&quot;this is any type&quot;); // 함수이든상관없이할당이가능하지만

let someValue1: any = unknownValue; // (O) any 타입으로 선언된 변수를 제외한 다른 변수는 모두 할당이 불가
let someValue2: number = unknownValue; // (X)
let someValue3: string = unknownValue; // (X)</code></pre><p>무엇이 할당될지 아직 모르는 상태의 타입을 말함 -&gt; any와 똑같아보일 수 있음
왜 unknown이 추가된걸까?</p>
<pre><code>// 할당하는 시점에서는 에러가 발생하지 않음
const unknownFunction: unknown = () =&gt; console.log(&quot;this is unknown type&quot;);

// 하지만 실행 시에는 에러가 발생; Error: Object is of type &#39;unknown&#39;.ts (2571)
unknownFunction();</code></pre><p>보면 할당할때는 에러가 안생기는데 실행하면 에러가 생김. 이거말고도 객체 내부에 접근하는 모든 시도에서 에러가 발생할거임.</p>
<p>unknown타입으로 선언된 변수는 값을 가져오거나 내부 속성에 접근할 수 없음</p>
<p><strong>any타입과 비교를 해보자</strong></p>
<blockquote>
<p>any타입은 어떤 값이든 허용됨. 
any타입을 모두 허용하기 때문에 할당하는 시점에 any로 사용을 하고, 나중에 실행 시에 타입을 특정해주지 않아도 에러가 발생하지 않는다. 
결국 깜빡하고 타입 지정을 누락할 경우 어떤 값이든 전달될 수 있기 때문에 런타임에 버그가 발생할 가능성이 높아진다는 얘기임</p>
</blockquote>
<p>이런 상황을 보완하기 위해 등장한 타입이 unknown타입이다. any타입과 유사하지만 타입검사를 강제하고 타입이 식별된 후에 사용 가능하기 때문에 any타입보다 안전함.</p>
<p><strong>결론 : 데이터 구조 파악이 힘든 경우 any보다는 unknown타입을 써라!</strong></p>
<h2 id="void타입">void타입</h2>
<p>함수에 전달되는 매개변수의 타입과 반환하는 타입을 지정해야 함
타입스크립트에서 함수가 어떤 값을 반환하지 않는 경우에는 void를 지정하여 사용해야됨</p>
<p>일반적으로 함수 자체를 다른 함수의 인자로 전달하는 경우가 아니라면 void타입은 잘 명시하지 않음
왜냐면 반환문 없을때 알아서 void로 추론해주기 때문에</p>
<h2 id="never타입">never타입</h2>
<p>값을 반환할 수 없는 타입을 말함
<strong>값을 반환하지 않는 것, 값을 반환할 수 없는 것</strong>을 명확히 구분해야됨</p>
<h3 id="에러를-던지는-경우">에러를 던지는 경우</h3>
<p>자바스크립트에서는 런타임에 의도적으로 에러를 발생시키고 캐치할 수 있음. throw 키워드를 사용하면 에러를 발생시키는데, 이는 값을 반환하는 거로 간주하지 않아서 타입은 never임</p>
<h3 id="무한히-함수가-실행되는-경우">무한히 함수가 실행되는 경우</h3>
<p>말그대로 무한루프니까 값을 반환을 못하니 타입이 never</p>
<p>never타입은 모든 타입의 하위 타입임. 즉, never자신을 제외한 어떤 타입도 never타입에 할당될 수 없음
심지어 any타입도 안됨</p>
<p>따라서 타입스크릡트에서는 조건부 타입을 결정할 때 특정 조건을 만족하지 않는 경우에 엄격한 타입 검사 목적으로 never타입을 명시적으로 사용하기도 함</p>
<h2 id="array타입">Array타입</h2>
<p>배열 타입을 가리키는 Array키워드는 자바스크립트에서도 확인할 수 있음.
근데 왜 소개하냐. 엄밀히 말하면 JS는 배열을 객체에 속하는 타입으로 분류함. 즉, JS는 배열을 단독으로 자료형으로 국한하지 않음
타입스크립트에서 Array라는 타입을 사용하기 위해서는 타입스크립트의 특수한 문법을 함꼐 다뤄야 함</p>
<p>타입스크립트는 배열의 크기까지 제한하지는 않지만 정적 타입의 특성을 살려 <strong>명시적인 타입을 선언하여 해당 타입의 원소를 관리하는 것을 강제</strong>함</p>
<p>배열 타입 선언에는 두가지 방식이 있음</p>
<pre><code>const array: number[] = [1,2,3];
const array: Array&lt;number&gt; = [1,2,3]; // 제네릭</code></pre><p>차이점은 딱히 없음 개인의 선호,팀 컨벤션 따르거나 혼용하면 됨</p>
<p>만약!..숫자형과 문자열 등 여러 타입을 모두 관리해야 하는 배열을 선언하려면 <strong>유니온 타입</strong>을 사용</p>
<pre><code>const array1: Array&lt;number | string) = [1,&quot;string&quot;]
const array2: number[] | string[] = [1,&quot;string]

const array3: (numver | array)[] = [1,&quot;string&quot;]
</code></pre><p>튜플은 길이까지 제한하여 원소 개수와 타입을 보장함. ex) <code>let tuple: [number] = [1]</code></p>
<p><strong>튜플의 쓰임새</strong></p>
<blockquote>
<p>리엑트 hook요소 중 useState는 튜플 타입을 반환함. 첫 원소는 훅으로부터 생성 및 관리되는 상태 값을 의미, 두번째 원소는 해당 상태를 조작할 수 있는 세터를 의미함.
배열 원소의 자리마다 명확한 의미를 부여하기 때문에 컴폰넌트에서 사용하지 않는 값에 접근하는 오류를 방지해줌. 
<strong>구조분해 할당</strong>을 하니까 사용자가 자유롭게 이름도 정의할 수 있음</p>
</blockquote>
<p>구조분해 할당은 배열말고 객체에 대해서도 적용 가능. 물론 객체의 경우 사전에 선언된 속성의 이름을 통해 값을 가져오므로 튜플보다 유언성은 다소 떨어짐</p>
<pre><code>const useStateWithObject = (initialValue: any) =&gt; {
    ...
    return {value,setValue}
}

const [value,setValue] =useStateWithObject(false); // 정의된 속성이름으로 가져와야됨
const [vaue: userName, setValue: setUsername} = useStateWithObject(&#39;&#39;) //정 하고싶으면 일차적으로 먼저 접근 후 다른 이름 지정해야됨</code></pre><h2 id="enum-타입">enum 타입</h2>
<p>열거형이라고도 부르는 타입스크립트에서 지원하는 트구샇ㄴ 타입
일종의 구조체를 만드는 타입 시스템
enum을 사용하여 열거형을 정의할 수 있는데 열거형은 각각의 멤버를 가지고 있음. 명명한 각 멤버의 값을 스스로 추론함. 기본적인 추론방식은 숫자 0부터 1씩 늘려가며 값을 할당하는 것!</p>
<pre><code>enum ItemStatusType {
  DELIVERY_HOLD = &quot;DELIVERY_HOLD&quot;, // 배송 보류
  DELIVERY_READY = &quot;DELIVERY_READY&quot;, // 배송 준비 중
  DELIVERING = &quot;DELIVERING&quot;, // 배송 중
  DELIVERED = &quot;DELIVERED&quot;, // 배송 완료
}

const checkItemAvailable = (itemStatus: ItemStatusType) =&gt; {
  switch (itemStatus) {
    case ItemStatusType.DELIVERY_HOLD:
    case ItemStatusType.DELIVERY_READY:
    case ItemStatusType.DELIVERING:
      return false;
    case ItemStatusType.DELIVERED:
    default:
      return true;
  }
};</code></pre><p>itemStatus의 타입이 문자열로 지정된 경우와 비교해보자</p>
<p><strong>타입 안정성</strong>
ItemStatusType에 명시되지 않은 다른 문자열은 인자로 받을 수 없음. 따라서 타입 안정성이 우수함.</p>
<p><strong>명확한 의미 전달과 높은 응집력</strong>
ItemStatusType 타입이 다루는 값이 무엇인지 명확함.</p>
<p>열거형은 관련이 높은 멤버를 모아 문자열 상수처럼 사용하고자 할 때 유용하게 쓰임</p>
<p>역방향 접근을 막기 위해 <strong>const enum</strong>으로 선언함, but 숫자 상수로 관리된느 열거형은 다른 값을 할당하거나 접근할 때 못 막음. 반면 문자열은 접근을 막아줌</p>
<p>따라서 문자열 상수 방식으로 열거형을 사용하는 것이 안전함</p>
<pre><code>const enum NUMBER {
  ONE = 1,
  TWO = 2,
}
const myNumber: NUMBER = 100; // NUMBER enum에서 100을 관리하고 있지 않지만 이는 에러를 발생시키지 않는다

const enum STRING_NUMBER {
  ONE = &quot;ONE&quot;,
  TWO = &quot;TWO&quot;,
}
const myStringNumber: STRING_NUMBER = &quot;THREE&quot;; // Error</code></pre><p>.
.
.
끝
🫠</p>
<p>출처: 우아한 타입스크립트 with 리엑트</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우타스] 원시 타입, 객체 타입]]></title>
            <link>https://velog.io/@miin-hyukkk/%EC%9B%90%EC%8B%9C-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@miin-hyukkk/%EC%9B%90%EC%8B%9C-%ED%83%80%EC%9E%85</guid>
            <pubDate>Fri, 23 Aug 2024 11:56:45 GMT</pubDate>
            <description><![CDATA[<p>자바스크립트에서 값은 타입을 가지지만 변수는 별도의 타입을 가지지않음
타입스크립트는 이 변수에 타입을 지정할 수 있는 타입 시스템 체계를 구축함
-&gt; 특정 타입을 지정한 변수에 해당 타입의 값만 할당 가능</p>
<h1 id="원시-타입">원시 타입</h1>
<h2 id="boolean">boolean</h2>
<p>자바스크립트에는 boolean 원시 값은 아니지만 형 변환을 통해 true/false로 취급되는 Truthy/Falsy 값이 존재함 
ex&gt; <code>0, -0, undefined</code></p>
<p>이 값은 boolean 원시 값이 아니므로 타입스크립트에서도 boolean 타입에 해당하지 않음</p>
<h2 id="undefined">undefined</h2>
<p>정의되지 않음, 초기화되지 않은 값
변수 선언만 한 경우</p>
<pre><code>type Person {
    name: string;
    job?: string;</code></pre><p>Person 타입의 job속성의 경우 undefined 할당가능</p>
<h2 id="null">null</h2>
<p>오직 null만 할당 가능
보통 빈값을 할당해야 할 때 null을 사용
이때 사용된 null은 명시적 의도적으로 값이 아직 비어있음을 보여줌</p>
<p><strong>undefined vs null</strong>
자바스크립트에서는 흔히 값이 없다는 것을 나타낼때 이 둘을 혼용함
심지어 ==연산자로 둘 비교하면 true값을 뱉음
하지만 엄연히 따로 존재하는 원시 값이기에 서로의 타입에 할당할 수 없음</p>
<pre><code>type Person1 = {
  name: string;
  job?: string;
};

type Person2 = {
  name: string;
  job: string | null;
};</code></pre><p><code>undefined</code> : job속성이 있을 수 도 없을 수도...
<code>null</code> : job속성을 사람마다 가지고 있지만 값이 비어있을 수도....</p>
<h2 id="number">number</h2>
<p>모든 숫자는 number 타입에 할당 가능
숫자에 해당하는 원시 값 중 <code>NaN</code>이나 <code>Infinity</code>도 포함됨</p>
<h2 id="bigint">bigInt</h2>
<p>ES2020에 새롭게 도입된 데이터 타입. 타입스크립트 3.2버전부터 사용 가능
이전의 js에서는 가장 큰수인 <code>Number.MAX_SAFE_INTEGER</code>를 넘어가는 값을 처리할 수 없었는데 <code>bigInt</code>를 사용하면 이보다 큰 수 처리가능
<code>Number</code>, <code>bitInt</code>는 엄연히 서로 다른 타입이기에 상호작용은 불가능</p>
<h2 id="string">string</h2>
<h2 id="symbal">symbal</h2>
<p>ES2015에서 도입된 데이터 타입으로 Symbol()함수를 사용하면 어떤 값과도 중복되지 않는 유일한 값을 생성할 수 있음</p>
<pre><code>const MOVIE_TITLE = Symbol(&quot;title&quot;);
const MUSIC_TITLE = Symbol(&quot;title&quot;);
console.log(MOVIE_TITLE === MUSIC_TITLE); // false</code></pre><h1 id="객체-타입">객체 타입</h1>
<p>위 7가지 원시 타입에 속하지 않는 값은 모두 객체 타입으로 분류할 수 있음</p>
<h2 id="object">object</h2>
<p><code>object</code>타입은 가급적 사용하지 말도록 권장함. <code>any</code>타입과 유사하게 객체에 해당하는 모든 타입 값을 유동적으로 할당 가능해서 <strong>정적 타이핑</strong>의 의미가 없어져버림</p>
<h2 id="">{}</h2>
<p>중괄호는 자바스크립트에서 객체 리터럴 방식으로 객체를 생성할 때 사용함
빈 객체 타입을 지정하기 위해서는 {}보다는 유틸리티 타입으로 Record&lt;string, never&gt;처럼 사용하는 게 바람직함</p>
<h2 id="array">array</h2>
<p>자바스크립트의 배열 자료구조는 원소를 자유롭게 추가하고 제거할 수 있음
그리고 타입 제한 없이 다양한 값을 다룸, 즉 하나의 배열 안에 숫자, 문자열과 같은 서로 다른 값이 들어있을 수 있음</p>
<p><strong>하지만 이런 쓰임은 타입스크립트가 추구하는 정적 타이핑 방향과 맞지 않음</strong>
그래서 타입스크립트에서는 배열을 array라는 별도 타입으로 다룸
타입스크립트 배열 타입은 하나의 타입 값만 가질 수 있음</p>
<p>Array키워드 사용 or 대괄호([]) 사용</p>
<h2 id="type과-interface키워드">type과 interface키워드</h2>
<p>object타입은 실무에서 잘 안씀.
흔히 객체를 타이핑하기 위해 자주 사용하는 키워드로 type과 interface가 있음.
중괄호를 사용한 객체 리터럴 방식으로 타입을 매번 일일이 지정하기에는 중복적인 요소가 많음. 그래서 type,interface를 아래처럼 씀</p>
<pre><code>type NoticePopupType = {
  title: string;
  description: string;
};

interface INoticePopup {
  title: string;
  description: string;
}
const noticePopup1: NoticePopupType = { /* ... */ };
const noticePopup2: INoticePopup = { /* ... */ };</code></pre><p>일반적으로 타입스크립트에서는 변수 타입을 명시적으로 선언하지 않아도 컴파일러가 타입을 추론함 -&gt; 타입 유추
<strong>명시적으로 할건지, 타입추론을 컴파일러한테 맡길지는 개인취향 or 팁의 컨벤션을 따르자</strong></p>
<p>둘 다 쓸 수 있을 때는 내 기준을 만들거나 팀 컨벤션 따르자 -&gt; <code>책 p76</code></p>
<h2 id="function">function</h2>
<p>함수를 별도 함수 타입으로 지정
자바스크립트에서 typeof 연산자로 확인한 function이라는 키워드 자체를 카입으로 사용하지 않음
둘째, 함수는 매개변수 목록을 받을 수 있는데 이걸 별도 타입으로 지정해야됨
반환값 있으면 반환값에 대한 타이핑이 필요</p>
<pre><code>function add(a: number, b: number): number {
  return a + b;
}</code></pre><p><strong>호출 시그니처</strong>(함수 타입을 정의할 때 사용하는 문법)를 정의하는 방식으로 한다~ 아래처럼</p>
<pre><code>type add = (a: number, b: number) =&gt; number;
</code></pre><p>끝
.
.
.
🫠</p>
<p>출처 : 우아한 타입스크립트 with 리엑트</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우타스] 타입 시스템]]></title>
            <link>https://velog.io/@miin-hyukkk/%EC%9A%B0%ED%83%80%EC%8A%A4%ED%83%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@miin-hyukkk/%EC%9A%B0%ED%83%80%EC%8A%A4%ED%83%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Fri, 09 Aug 2024 00:30:31 GMT</pubDate>
            <description><![CDATA[<h2 id="정적-타입과-동적-타입">정적 타입과 동적 타입</h2>
<p>타입을 결정하는 시점에 따라 타입을 분류</p>
<p><strong>정적 타입(static type)</strong> 
모든 변수의 타입이 컴파일 타임에 결정. 코드 수준에서 타입을 명시해줘야 함
번거롭지만 컴파일타임에 타입에러를 발견할 수 있기 때문에 프로그램의 안정성을 보장</p>
<p><strong>동적 타입(dynamic type)</strong>
변수 타입이 런타임에서 결정됨. 개발자가 직접 타입 정의 X. </p>
<h2 id="강타입과-약타입">강타입과 약타입</h2>
<p><strong>암묵적 타입 변환</strong>
개발자가 의도적으로 타입을 명시하거나 바꾸지 않았는데도 컴파일러 또는 엔진 등에 의해서 런타임에 타입이 자동으로 변경되는 것</p>
<p><strong>강타입</strong>
서로 다른 타입을 갖는 값끼리 연산 시도하면 에러 발생
ex) <code>파이썬</code>, <code>루비</code>, <code>타입스크립트</code>
<strong>약타입</strong>
서로 다른 타입을 갖는 값끼리 연산 시도하면 내부적으로 판단해서 특정 값의 타입을 변환하여 연산을 수행
ex) <code>c++</code>, <code>자바</code>, <code>자바스크립트</code></p>
<h2 id="컴파일-방식">컴파일 방식</h2>
<p><strong>일반적인 컴파일의 의미</strong>
사람이 이해할 수 있는 코드를 컴퓨터가 이해할 수 있는 기계어로 바꿔주는 과정
c#,Java(고수준 언어) -&gt; 컴퓨터가 해석할 수 있는 바이너리 코드(저수준 언어)</p>
<p>but, 타입스크립트의 컴파일 결과물은 자바스크립트 파일임
왜냐 타입스크립트는 사람이 이해하기 쉬운 방식으로 코드를 작성하려고 나온게 아니기 때문임
<strong>JS의 컴파일 타임에 런타임 에러를 사전에 잡아내기 위한 것</strong>임.</p>
<p>-&gt; 타입스크립트를 컴파일하면 타입이 모두 제거된 JS소스코드만 남게됨</p>
<h1 id="타입-시스템">타입 시스템</h1>
<h2 id="구조적-타이핑-구조적-서브-타이핑">구조적 타이핑, 구조적 서브 타이핑</h2>
<p>타입스크립트는 구조로 타입을 구분함. 이것을 구조적 타이핑이라고 함</p>
<p><strong>구조적 서브 타이핑</strong>
타입스크립트의 타입은 값의 집합으로 생각할 수 있음.
<strong>타입은 단지 집합에 포함되는 값이고 특정 값은 많은 집합에 포함될 수 있다.</strong>
따라서 타입스크립트에서는 특정 값이 string 또는 number 타입을 동시에 가질 수 있음.</p>
<p>이런 TS의 타입 시스템을 지탱하고 있는 개념이 바로 <strong>구조적 서브 타이핑</strong>임
: 객체가 가지고 있는 속성을 바탕으로 타입을 구분하는 것. 이름이 다른 객체라도 속성이 동일하다면 TS는 서로 호환이 가능한 동일한 타입으로 여김</p>
<pre><code>interface Pet {
  name: string
}

interface Cat {
  name: string
  age: number
}

let pet: Pet;
let cat: Cat = { name: &quot;Zag&quot;, age: 2 };

// ✅ OK
pet = cat;</code></pre><p>Cat은 Pet과 다른 타입으로 선언. 하지만 Pet이 가지고 있는 name속성을 가지고 있음.
따라서 Cat타입으로 선언한 cat을 Pet타입으로도 선언한 pet에 할당 가능</p>
<p><strong>JS</strong> - <code>덕타이핑</code> - <code>런타임에 검사</code>
<strong>TS</strong> - <code>구조적 타이핑</code> - <code>컴파일타임에 검사</code>
두 방식 모두 객체가 가진 속성을 기반으로 타입을 검사</p>
<h2 id="ts의-점진적-타입-확인">TS의 점진적 타입 확인</h2>
<p>필요에 따라 타입 선언 생략을 허용하는 방식</p>
<pre><code>function add(x, y) {
  return x + y;
}

// 위 코드는 아래와 같이 암시적 타입 변환이 일어난다.
function add(x: any, y: any): any</code></pre><p>그러나 TS는 컴파일타임에 프로그램의 모든 타입을 알고 있을 때 최상의 결과를 보여줌
그리하여, TS컴파일옵션에 <code>noImplicitAny = true</code>로 설정하는 것이 좋음</p>
<h2 id="값-vs-타입">값 vs 타입</h2>
<p>값은 프로그램이 처리하기 위해 메모리에 저장하는 모든 데이터
객체 역시 값. JS에서는 함수도 값. 모든 것이 객체인 언어이기 때문에 런타임에 객체로 변환됨</p>
<p>ex)</p>
<pre><code>const goWork = function (developer) {
    console.log(`tired ${developer}`);
}</code></pre><p>JS대신 타입스크립트에서는  변수,매개변수,객체 속성 등에 : type 형태로 타입을 명시함
ex) <code>const a:number = 223</code></p>
<p><code>type</code>,<code>interface</code>키워드로도 가능</p>
<p>값 공간과 타입 공간의 이름은 서로 충돌하지 않기 때문에 타입과 변수를 같은 이름으로 정의할 수 있음
<strong>타입스크립트 문법인 type으로 선언한 내용은 JS 런타임에서 제거되기 때문</strong></p>
<p>함수의 매개변수처럼 여러개의 심볼이 함께 쓰인다면 타입과 값을 명확하게 구분해야 함
TS는 개발자가 작성한 코드 문맥을 파악해서 스스로 값,타입을 해석 -&gt; <strong>구분해서 작성해야함</strong></p>
<p>ex)</p>
<pre><code>function email(options: { person: Person; subject: string; body: string }) {}</code></pre><p>타입스크립트에서 구조분해할당하면 <code>Person</code>과 <code>string</code>이 값의 관점에서 해석됨.
그니까 <code>Person</code>,<code>string</code>이 값 공간에 있는 것으로 해석한다는 거임.</p>
<p>개발자의 의도는 매개변수 객체의 속성인 <code>perosn</code>을 <strong>Person타입</strong>, <code>subject</code>,<code>body</code>는 <strong>string타입</strong>으로 설정해서 제한하는 건데,Person과 string이 값의 관점에서 해석해버림.</p>
<p>그래서 값과 타입을 구분해서 작성해야됨</p>
<p><strong>like this</strong></p>
<pre><code>function email({
  person,
  subject,
  body
}:{
  person: Person;
  subject:string;
  body:string;
}){
  ~~~~~~
}</code></pre><p>class나 enum은 값으로도 타입으로도 사용됨</p>
<p>ex) </p>
<pre><code>const me: Developer = new Developer(&quot;zig&quot;,&quot;frontend&quot;)
          -&gt;타입          -&gt;값</code></pre><p>enum을 자주 사용하는 팀도 있고 아닌 팀들도 있는 듯!...</p>
<p>.
.
.</p>
<p>출처: 우아한 타입스크립트 with 리엑트</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우타스] 타입스크립트는 좋은 선택지]]></title>
            <link>https://velog.io/@miin-hyukkk/%EC%9A%B0%ED%83%80%EC%8A%A4-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%A2%8B%EC%9D%80-%EC%84%A0%ED%83%9D%EC%A7%80</link>
            <guid>https://velog.io/@miin-hyukkk/%EC%9A%B0%ED%83%80%EC%8A%A4-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%A2%8B%EC%9D%80-%EC%84%A0%ED%83%9D%EC%A7%80</guid>
            <pubDate>Wed, 07 Aug 2024 01:14:11 GMT</pubDate>
            <description><![CDATA[<p>앞으로 우타스를 읽으면서 중요한 내용들을 내 말로 적어놓을 계획!...</p>
<h1 id="타입스크립트의-등장">타입스크립트의 등장</h1>
<p>먼저...</p>
<h2 id="웹개발-역사를-간략히-알아보자">웹개발 역사를 간략히 알아보자!</h2>
<p><strong>JS가 어떻게 브라우저에서 널리 사용되기 시작했을까?</strong></p>
<blockquote>
<p>두 경쟁 업체에서 각자의 브라우저에 새로운 기능을 빠르게 늘리기 시작. 
추가된 기능은 각자의 브라우저에서만 동작(다르게 동작 or 크로스 브라우징 이슈).
따라서 개발자는 따로 개발해야하는 어려움이 있었고, 따라서 JQuery와 같이 호환성 고민필요없는 라이브러리가 유행하게됨</p>
</blockquote>
<p>하지만 이런 라이브러리에 기댈 수는 없었고 표준화된 JS의 필요성이 제기됨
*<em><code>ECScript</code>라는 이름으로 자바스크립트 표준화를 공식화
*</em></p>
<h2 id="웹사이트에서-웹-애플리케이션으로의-전환">웹사이트에서 웹 애플리케이션으로의 전환</h2>
<p><strong>웹사이트</strong> </p>
<blockquote>
<p>수집된 데이터 및 정보를 특정 페이지에 표시하기 위한 정적인 웹
단뱡향으로 정보 제공, 상호작용X, 콘텐츠가 동적으로 업데이트X</p>
</blockquote>
<p><strong>웹 애플리케이션</strong></p>
<blockquote>
<p>쌍방향 소통의 웹</p>
</blockquote>
<p>웹 서비스는 점차 페이지에서 애플리케이션의 특성을 가지게됨
<code>이유</code> : 디바이스 댜양성 증가. 데이터 폭발적 증가. UI/UX 다양 및 복잡</p>
<p>그리하여 <strong>CBD방법론</strong>이 등장 </p>
<blockquote>
<p><strong>컴포넌트 베이스 개발</strong> : 재사용할 수 잇는 컴포넌트를 개발 또는 조합해서 개발 </p>
</blockquote>
<p>여튼 이런 개발 생태계 발전과 동적 웹서비스의 수요 증가로 인해 JS개발자가 증가하게 됐다~</p>
<h2 id="js의-한계">JS의 한계</h2>
<p><strong>동적 타입 언어</strong>
변수에 타입을 명시적으로 지정하지 않고 코드가 실행되는 <strong>런타임</strong>에 변숫값이 할당될 때 해당 값의 타입에 따라 변수 타입이 결정됨</p>
<p>뭔말이냐하면
변수 a의 타입이 <code>number</code>인지 <code>string</code>인지는 실제 코드가 동작할 때 a에 값이 할당되는 그 순간에 그 값이 1인지 &#39;1&#39;인지에 따라 결정된다는 거임</p>
<pre><code>const sumNumber = (a,b) =&gt;{
    return a + b;
}

sumNumber(100) // NaN
sumNumber(&#39;a&#39;,&#39;b&#39;) // ab</code></pre><p>JS에서는 해당 코드가 오류가 안난다. 왜? 동적 타입 언어니까.
<code>subNumber</code>함수를 호출할 때 사용되는 인수 값에 따라 <code>a</code>와 <code>b</code>의 타입이 결정됨.
심지어 하나만 전달했을때는 나머지 하나를 적절한 타입인 <code>NaN</code>으로 형변환한 후에 실행해버림</p>
<p>해당 함수를 짠 개발자의 의도는 <strong>두 숫자의 합을 구하는 함수</strong>이다.
그러나 하나의 숫자만 전달해도 오류없이 NaN값을 뱉고, 문자열의 합을 구하는데에도 쓰임 -&gt; <strong>개발자의 의도와 다르게 동작할 수 있음</strong></p>
<p>이러한 문제들을 다양한 방법으로 해결하려고 했고 여러 해결방안 뒤에 
.
.
.
JS의 슈퍼셋 언어인 <strong>타입스크립트</strong>가 등장함</p>
<p>책에서 말하기를 &#39;자바스크립트는 사용하면 안 된다. 무조건 타입스크립트를 사용해야 한다&#39;라는 의미로 받아들이지 말라고 한다.</p>
<p>단지 <strong>좋은 선택지</strong>가 될 수 있다는 것을 의미</p>
<p>.
.
.</p>
<p>출처: 우아한 타입스크립트 with 리엑트</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS]This]]></title>
            <link>https://velog.io/@miin-hyukkk/JSThis</link>
            <guid>https://velog.io/@miin-hyukkk/JSThis</guid>
            <pubDate>Mon, 05 Aug 2024 08:28:38 GMT</pubDate>
            <description><![CDATA[<h1 id="this">This</h1>
<p><strong>기술 질문</strong></p>
<p><strong>JavaScript 에서 Function안의 this란?</strong></p>
<blockquote>
<p>현재 함수를 부른 객체가 누구인지를 나타내며 고정값이 아닙니다. 함수 내 this는 함수가 호출되는 방식에 따라 결정됩니다.</p>
</blockquote>
<h3 id="예시를-통해-this가-뭔지-감을-잡아보자"><strong>예시를 통해 this가 뭔지 감을 잡아보자!</strong></h3>
<p><strong>this</strong>는 기본적으로 <strong>window</strong>임</p>
<pre><code class="language-jsx">this; // Window {}</code></pre>
<p>일반 함수 내에서 혼자 <strong>this</strong>를 선언하면, 그 <strong>this</strong>는 <strong>window</strong> 객체를 가리킴</p>
<pre><code class="language-jsx">function myFunction() {
  console.log(this);
}
myFunction();</code></pre>
<p>객체의 메서드 내에서 <strong>this</strong>는 그 메서드가 <strong>속한 객체</strong>를 가리킴</p>
<p>객체 메서드 <strong>sayHello</strong> 안의 <strong>this</strong>는 객체 <strong>myObject</strong>를 가리킴 (호출 시 내부적으로 바꿔줌)</p>
<pre><code class="language-jsx">const myObject = {
  name: &#39;민혁&#39;,
  sayHello: function() {
    console.log(this.name);
  }
};

myObject.sayHello();</code></pre>
<p><strong>but, 아래처럼하면??</strong></p>
<pre><code class="language-jsx">var sayHello2 = myObject.sayHello()
sayHello2(); </code></pre>
<p>이 경우 <strong>sayHello2</strong>는 <strong>myObject.sayHello 메서드의 참조를 변수에 할당</strong>한거임</p>
<p>이 할당된 함수 <strong>sayHello2</strong>는 독립적인 함수가 되고, myObject 객체와 연결되어 있지 않음</p>
<p>따라서 전역으로 <strong>sayHello2</strong>를 호출하면, <strong>this</strong>는 기본적으로 <strong>전역 객체(window)를 참조함</strong>.</p>
<p>브라우저 환경에는 당연히 name이라는 속성이 없고, 따라서 undefined를 출력함</p>
<p>대강 봤을때, JS에서  함수 내 this는 함수가 호출되는 방식에 따라 결정된다는 걸 알 수 있음.</p>
<p>.
.
.</p>
<h3 id="객체-메서드에서의-this"><strong>객체 메서드에서의 this</strong></h3>
<pre><code class="language-jsx">function getThis() {
  return this;
}

const obj1 = { name: &quot;obj1&quot; };
const obj2 = { name: &quot;obj2&quot; };

obj1.getThis = getThis;
obj2.getThis = getThis;

console.log(obj1.getThis()); // { name: &#39;obj1&#39;, getThis: [Function: getThis] }
console.log(obj2.getThis()); // { name: &#39;obj2&#39;, getThis: [Function: getThis] }</code></pre>
<p><strong>obj1.getThis()</strong>와 <strong>obj2.getThis()</strong>는 각각 obj과 obj2를 this로 참조</p>
<h3 id="프로토타입-체인에서의-this"><strong>프로토타입 체인에서의 this</strong></h3>
<p>함수가 객체의 프로토타입 체인 상에서 호출될 때도, this는 호출하는 객체를 참조함</p>
<pre><code class="language-jsx">const obj1 = {
  name: &quot;obj1&quot;,
  getThis() {
    return this;
  },
};

const obj3 = {
  __proto__: obj1,
  name: &quot;obj3&quot;,
};

console.log(obj3.getThis()); // { name: &#39;obj3&#39; }</code></pre>
<p>JS의 모든 객체는 다른 객체를 참조하는 프로토타입을 가질 수 있음</p>
<p>예제에서 obj3는 obj1을 프로토타입으로 설정함. 이를 통해 obj3은 obj1의 속성과 메서드를 상속받음</p>
<p><strong>작동방식</strong></p>
<blockquote>
<ol>
<li>JS엔진은 obj3객체에서 getThis를 찾으려고 시도</li>
<li>없으니까, JS엔진은 obj3의 프로토타입인 obj1로 가서 getThis를 찾음</li>
</ol>
</blockquote>
<h3 id="strict-mode에서의-this"><strong>Strict mode에서의 this</strong></h3>
<p><strong>strict mode에서는</strong> 다음과 같은 상황에 애초에 this가 undefined를 가리킴</p>
<p><strong>strict mode가 아닐경우</strong>, this는 기본적으로 전역 객체를 가리킴</p>
<p><strong>strict mode에서 this가 undefined를 가리키는 이유</strong></p>
<p>: 코드 작성자가 의도치 않게 전역 객체의 속성을 수정하거나 접근할 수 있기 때문</p>
<h3 id="콜백-함수의-this">콜백 함수의 this</h3>
<p><strong>strict mode</strong></p>
<pre><code class="language-jsx">function logThis() {
  &quot;use strict&quot;;
  console.log(this); // 100
}

[1, 2, 3].forEach(logThis, 100); // 100, 100, 100</code></pre>
<p>this가 명시적으로 설정되지 않으면 undefined가 되지만, 여기서는 thisArg를 사용함.</p>
<p>그래서 thisArg로 설정된 값(100)이 this로 사용됨</p>
<p><strong>non strict mode</strong></p>
<pre><code class="language-jsx">function logThis() {
  console.log(this); // 100
}

[1, 2, 3].forEach(logThis, 100); // [Number: 100],[Number: 100],[Number: 10</code></pre>
<p>여기서는 this가 전역인 window 값이지만 thisArg를 사용함으로서,  thisArg(100)는 Number객체로 변환됨</p>
<p><strong>이게 가능한 이유</strong></p>
<p>배열의 반복 메서드(<code>forEach</code>, <code>map</code>, <code>filter</code> 등)와 <code>Set.prototype.forEach()</code> 메서드는 <code>thisArg</code> 매개변수를 받아 콜백 함수의 <code>this</code> 값을 설정할 수 있음</p>
<p><strong><code>JSON.parse</code>와 <code>JSON.stringify</code>의 콜백</strong></p>
<pre><code class="language-jsx">const json = &#39;{&quot;name&quot;:&quot;Alice&quot;,&quot;age&quot;:25}&#39;;

const parsed = JSON.parse(json, function(key, value) {
  console.log(this); // &#39;this&#39;는 현재 처리 중인 객체
  return value;
});

const obj = { name: &quot;Bob&quot; };
const jsonString = JSON.stringify(obj, function(key, value) {
  console.log(this); // &#39;this&#39;는 obj
  return value;
});</code></pre>
<p>JSON의 <code>parse</code>와 <code>stringify</code> 메서드는 콜백의 this를 처리 중인 객체로 설정함</p>
<h3 id="화살표-함수의-this">화살표 함수의 this</h3>
<blockquote>
<ul>
<li>상위 스코프의 this를 그대로 사용함</li>
</ul>
</blockquote>
<ul>
<li>자기 자신만의 this를 가지지 않음</li>
<li>화살표 함수가 정의된 위치의 this를 그대로 사용</li>
</ul>
<p><strong>즉, 화살표 함수가 만들어질때의 this를 기억하고, 그 값을 계속 사용함</strong></p>
<p>화살표함수는 어떻게 호출을 하던 <strong>this</strong>가 바뀌지 않음. </p>
<p><strong>this</strong>는 항상 함수가 정의될때의 <strong>상위 스코프의 this를 유지</strong></p>
<pre><code class="language-jsx">const cat = {
  name: &#39;meow&#39;;
  callName: () =&gt; console.log(this.name);
}

cat.callName();    // undefined</code></pre>
<p><strong>callName</strong>메소드의 this는 자신을 호출한 객체 <strong>cat</strong>이 아니라 함수 선언 시점의 상위 스코프, 즉 <strong>전역객체</strong>를 가리킴. </p>
<p>(strictmode라면 undefined이고, non strictmode라면 window일거임!)</p>
<p><strong><em>따라서 저럴 경우는 일반 함수로 callName을 선언해야됨</em></strong></p>
<h3 id="생성자-함수에서-this">생성자 함수에서 this</h3>
<p><strong>기본 생성자 함수</strong>:  <strong>new</strong>와 함께 호출하면, 새 객체가 만들어지고 <strong>this</strong>는 그 새 객체를 가리킴</p>
<pre><code class="language-jsx">function C() {
  this.a = 37; // 새로 만들어진 객체에 &#39;a&#39;라는 프로퍼티를 37로 설정
}

let o = new C(); // &#39;new&#39;를 사용해 C 함수 호출
console.log(o.a); // 37</code></pre>
<p><strong>객체 반환</strong>: 생성자 함수가 객체를 반환하면, 새로 만든 객체 대신 반환된 객체가 사용됨</p>
<p>.
.
.
끝🫠</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS]Closures(클로저)]]></title>
            <link>https://velog.io/@miin-hyukkk/JSClosures</link>
            <guid>https://velog.io/@miin-hyukkk/JSClosures</guid>
            <pubDate>Mon, 05 Aug 2024 08:22:50 GMT</pubDate>
            <description><![CDATA[<h1 id="closures"><strong>Closures</strong></h1>
<p><strong>기술질문</strong> </p>
<p><strong>클로저가 뭔가요?</strong></p>
<blockquote>
<p>클로저는 함수와 그 함수가 정의된 렉시컬 환경이 결합된 상태를 말합니다.
이 상태로 인해 함수는 자신이 정의될 당시의 변수를 기억하고, 그 변수에 접근할 수 있는 것입니다.</p>
</blockquote>
<p><strong>MDN 공식 문서</strong></p>
<blockquote>
<p>클로저는 함수와 그 함수가 선언된 주변 상태(렉시컬 환경, 그 환경에 있는 변수들)를 함께 묶은 조합임.
쉽게 말해 클로저는 내부함수가 외부함수의 스코프에 접근할 수 있게 해주는 메커니즘임. </p>
</blockquote>
<p><strong>자바스크립트에서는 함수가 생성될 때마다 클로저가 만들어진다고 보면됨.</strong></p>
<h3 id="일단-렉시컬-스코핑에-대해-가볍게-알아보자"><strong>일단 렉시컬 스코핑에 대해 가볍게 알아보자</strong></h3>
<p>렉시컬 스코핑은 자바스크립트가 변수를 찾는 방식임. </p>
<p>함수가 정의된 <strong>위치</strong>에 따라 변수의 스코프가 결정되고, 함수가 실행될 때, 해당 함수가 어디에서 정의되었는지에 기반에 외부 변수에 접근 가능함.</p>
<p>이런 접근 가능성은 함수가 호출되는 시점이 아니라 <strong>정의되는 시점에 결정</strong>됨</p>
<h3 id="클로저는-다음-세가지-요소가-결합된-상태"><strong>클로저는 다음 세가지 요소가 결합된 상태</strong></h3>
<blockquote>
<ul>
<li>내부 함수 : 외부 함수의 스코프 안에서 정의된 함수</li>
</ul>
</blockquote>
<ul>
<li>외부 함수의 변수: 내부 함수가 접근할 수  있는 외부 함수의 변수들</li>
<li>환경 : 내부 함수가 정의될 때의 외부 함수의 스코프</li>
</ul>
<h3 id="클로저의-주요-포인트"><strong>클로저의 주요 포인트</strong></h3>
<blockquote>
<ul>
<li>내부 함수가 외부 함수의 변수를 참조할 수 있어야함</li>
</ul>
</blockquote>
<ul>
<li>외부함수의 실행이 끝난 후에도 변수 접근이 유지됨</li>
</ul>
<p><strong>외부함수의 실행이 끝난 후에도 변수 접근이 유지됨:</strong></p>
<blockquote>
<p>일반적인 함수는 실행이 끝나면 메모리에서 해당 함수의 스코프 및 변수는 사라짐.
하지만 외부함수의 변수를 내부함수가 참조한 형태에서는 외부함수의 호출이 끝난 후에도, 내부함수(클로저)가 외부함수의 스코프를 계속 참조하고 있으므로, 메모리에서 사라지지 않음</p>
</blockquote>
<p>⇒  <code>inner</code>함수가 <code>outer</code>함수 변수 참조하고 있어서, <code>outer</code>함수 실행이 끝난 뒤에도 <code>outer</code>함수의 변수들을 기억하고 있다는 뜻</p>
<h3 id="클로저-작동-예제1"><strong>클로저 작동 예제1</strong></h3>
<pre><code class="language-jsx">function outer() {
  let outerVar = &quot;I am outside!&quot;;

  function inner() {
    console.log(outerVar); // innerFunction이 외부 함수의 변수에 접근
  }

  return inner; // 클로저를 반환
}

const myClosure = outer(); // outer 실행 후 innerFunction 반환
myClosure();</code></pre>
<p><code>outerFunction</code>의 실행이 끝난 후에도 <code>innerFunction</code>이 <code>outerVar</code>을 계속 참조할 수 있기 때문에 클로저가 형성돼.</p>
<h3 id="클로저-작동-예제2"><strong>클로저 작동 예제2</strong></h3>
<pre><code class="language-jsx">function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12</code></pre>
<p><code>makeAdder</code>는 x라는 매개변수를 받아서 x와 y를 더하는 함수를 반환함 → 이 반환된 함수는 x+y를 반환함</p>
<blockquote>
<p><code>makeAdder(5)</code>를 호출하면, 내부에서는 x가 5로 설정된 함수를 반환함.
이 함수는 x=5가 포함된 렉시컬 환경을 기억하고 있고, 이 반환된 함수가 <code>add5</code>에 할당됨</p>
</blockquote>
<blockquote>
<p><code>makeAdder(10)</code>을 호출하면, 내부에서 x가 10으로 설정된 함수를 반환함.
이 함수는 x=10이 포함된 렉시컬 환경을 기억하고 있고, 이 반환된 함수가 add10에 할당됨</p>
</blockquote>
<p><strong>add5와 add10은 동일한 함수 본문을 사용하고 있지만, 각기 다른 렉시컬 환경을 가지고 있음</strong></p>
<p><strong>클로저 덕분에 add5와 add10은 자신이 정의될 때의 x값을 계속해서 사용 가능한거임</strong></p>
<h3 id="결론"><strong>결론</strong></h3>
<p>클로저는 함수의 생성 시점에서의 상태를 “<strong>기억</strong>”하고, 나중에 호출되었을 때 그 상태를 <strong>유지</strong>하며 사용할 수 있게 해준다</p>
<p>.
.
.</p>
<p>끝🫠</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] 변경에 유연한 컴포넌트]]></title>
            <link>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B3%80%EA%B2%BD%EC%97%90-%EC%9C%A0%EC%97%B0%ED%95%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</link>
            <guid>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B3%80%EA%B2%BD%EC%97%90-%EC%9C%A0%EC%97%B0%ED%95%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</guid>
            <pubDate>Wed, 27 Mar 2024 04:31:31 GMT</pubDate>
            <description><![CDATA[<h1 id="변경에-유연">변경에 유연</h1>
<blockquote>
<ol>
<li>Headless 기반의 추상화</li>
<li>한가지 역할만 하기</li>
<li>도메인 분리하기</li>
</ol>
</blockquote>
<p>컴포넌트는 무엇을 할까?</p>
<blockquote>
<p><strong>데이터 관리</strong> : 외부에서 들어오는 데이터, 상태 같은 내부 데이터를 관리
<strong>UI</strong> :  그 데이터를 어떻게 보여질지를 정의
<strong>유저와 상호작용</strong></p>
</blockquote>
<h2 id="headless-기반의-추상화">Headless 기반의 추상화</h2>
<h3 id="데이터-추상화">데이터 추상화</h3>
<p>디자인에 의존하는 UI를 컴포넌트가 관리하는 &#39;데이터&#39;와 분리해보자</p>
<p>내가 제작한 로그인 페이지를 예로 살펴보자</p>
<p>데이터와 UI를 분리해보자</p>
<p><strong>로그인 정보에 필요한 데이터를 추상화</strong>해서 <code>useLogin</code>라는 <code>hook</code>으로 표현해줌</p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/7022ce39-46b9-472d-a088-b91cff223ed4/image.png" alt=""></p>
<p>이제 이 <code>Login</code>이라는 컴포넌트에서는 컴포넌트에서는 <code>hook</code>에서 반환된 값을 <strong>어떻게 보여질지</strong>만 정의하면됨</p>
<p>로그인 정보를 관리하는 창 혹은 로그인 관련된 정보를 보여줘야 하는 창에서는 이 <code>useLogin hook</code>을 가져다 사용 할 수 있음.</p>
<p>지금 이렇게 한 작업은 ui를 관심사에서 제거한 작업임. =&gt; Headless</p>
<p>오로지 데이터에만 집중해서 모듈화를 진행. 한가지 문제에만 집중하기 때문에 더 많은 곳에서 사용할 수 있고, 다른 변경으로부터 격리시킬 수 있음.</p>
<h3 id="동작-추상화">동작 추상화</h3>
<p>로그인, 회원가입, 주문페이지 등등 쇼핑몰 프로젝트 내부 다양한 페이지에서 input창의 쓰이고 그럴때마다 input에 관련된 동일한 동작들에 대한 코드가 반복됨 =&gt; 컴포넌트 내부에 여러가지 로직이 들어가게 되고 결국 복잡해짐</p>
<p>데이터 부분을 추상화했던 것처럼 상호작용 부분을 추상화</p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/df2f52fd-263b-4e48-9f23-3fe0058d2111/image.png" alt="">
: 메모이제이션을 활용해 input을 추상화</p>
<h2 id="한가지-역할만-하기">한가지 역할만 하기</h2>
<p>변경에 유연해지려면 각 컴포넌트들이 한가지 역할만 하는게 중요함.
결국 같은 UI 내부에 있어도 각 컴포넌트들은 기능별로, 데이터별로 분리되어 있어야 하고, 그에 따라 서로의 역할을 몰라야함.</p>
<p>그래야 변경에 유연하게 컴포넌트를 제작할 수 있음.</p>
<h2 id="도메인-분리하기">도메인 분리하기</h2>
<p>모든 컴포넌트에서 데이터로 접근할 수 있지만, 컴포넌트를 주입 받듯 데이터를 주입 받자 -&gt; 도메인을 분리</p>
<blockquote>
<p>이해하기 쉽게 설명을 해보자면, <strong>컴포넌트에서 도메인에 해당하는 정보들을 걷어내는거임</strong>. <code>A 컴포넌트</code>와 <code>B 컴포넌트</code>는 각각 다른 데이터를 통해 비슷한 UI를 보여준다고 가정해보자. 만약 <code>A 컴포넌트</code> 제작 시, <code>A 컴포넌트</code>의 데이터들에 국한되게 컴포넌트들을 네이밍한다면, 그대로 <code>B 컴포넌트</code>에 <strong>해당 컴포넌트를 재사용하기는 어려워질 것</strong>이다. </p>
</blockquote>
<p>정리하자면, <strong>컴포넌트에서 도메인에 해당하는 정보, 즉 데이터에 관련된 정보를 걷어내서 네이밍을 일반적으로 함으로써 여러 페이지에서 재사용이 가능</strong>해지는 것이다.</p>
<ul>
<li><p>컴포넌트 인터페이스는 일반적일수록 이해하기 쉬움</p>
<ul>
<li>사람은 배경지식으로 무언가를 해석함 
=&gt; 최대한 일반적인 단어로 표현해야 의도를 드러내기 쉽다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>실제 프로젝트를 하면서도 네이밍을 일반적이지 않게 했다가, 나중에 비슷한 UI를 제작하고 해당 인터페이스를 가져다 사용하려고 할때, 네이밍을 다시 일반적이게 수정한 경험이 수두룩하다.</p>
</blockquote>
<p>결국 표준에 가까울수록 많은 사람들이 쉽게 이해할 수 있음.</p>
<hr>
<blockquote>
<p>*<em>구현해야하는 기능이 이미 있듯이 작성하고, 그것을 사용하듯이 개발해라
*</em></p>
</blockquote>
<p>🫠</p>
<p>출처 : <a href="https://www.youtube.com/watch?v=fR8tsJ2r7Eg">https://www.youtube.com/watch?v=fR8tsJ2r7Eg</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] Hydrate]]></title>
            <link>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Hydrate</link>
            <guid>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Hydrate</guid>
            <pubDate>Wed, 21 Feb 2024 08:44:22 GMT</pubDate>
            <description><![CDATA[<h1 id="hydrate">Hydrate</h1>
<p><strong>:HTML코드와 React인 JS코드를 서로 매칭시키는 과정</strong>
서버사이드에서 클라이언트 사이드한테 렌더링 된 정적 페이지, 번들링된 JS파일 보냄. 클라이언트는 받아서 HTML,JS 서로 매칭</p>
<p><strong>정적인 페이지에 물을 주어 동적으로 만듦</strong></p>
<p>이것에 대해 알아보기 전 리엑트의 웹페이지 구성원리에 대해 알아보자</p>
<h3 id="react에서의-웹페이지-구성-원리">React에서의 웹페이지 구성 원리</h3>
<blockquote>
<ol>
<li>도메인 주소(<a href="http://www.naver.com)%EC%97%90">www.naver.com)에</a> 접속</li>
<li>DNS 서버는 해당 도메인 주소에 맞는 IP주소로 요청을 보내줌</li>
<li>서버는 클라이언트에게 HTML documentI(index.html), JS파일(App.js)를 보내줌</li>
<li>서버로부터 받아온 파일들로 Render Tree 구성하고 웹화면을 렌더링</li>
</ol>
</blockquote>
<p>React는 JS파일만을 이용하여 웹화면을 구성하는 원리를 가지고 있음.</p>
<p>그래서 실제 HTML코드는 안에 내용이 하나도 없음</p>
<p><strong>단순뼈대만 있는 HTML document</strong>와 <strong>JS파일</strong>을 클라이언트에게 보내면, 클라이언트는 이 JS코드들을 통해 웹화면을 렌더링하며 페이지를 그리게됨</p>
<p>public/index.html은 알다시피 아무내용없고 뼈대만 있음.
src/index.js의 자바스크립트 코드에서 모든 화면을 렌더링 한 뒤 HTML DOM요소 중 root라는 아이디를 가진 엘리먼트를 찾아서 하위로 주입하게 된다.</p>
<h3 id="nextjs에서의-웹페이지-구성-원리">Next.js에서의 웹페이지 구성 원리</h3>
<p>리엑트와는 다르게 Next.js는 클라이언트에게 웹 페이지를 보내기전에 서버 사이드 단에서 미리 웹페이지를 Pre-Rendering 함.
그리고 그렇게 생성된 HTML document를 클라이언트에게 전송함.
즉, React처럼 빈껍데기인 index.html이 아니라 pre-rendering된 HTML 파일을 보내는 거임</p>
<p>하지만 알다시피, 현재 클라이언트가 받은 웹 페이지는 단순히 웹 화면만 보여주는 HTML이다.
자바스크립트 요소는 하나도 없음(=JS모듈,이벤트리스너들이 DOM요소에 적용안된 상태)</p>
<p>프로젝트를 실행하고 네트워크 탭을 누르면</p>
<p><img src="https://velog.velcdn.com/images/miin-hyukkk/post/cb9c598a-9dcb-4cda-9eeb-44ebe0366a0a/image.png" alt=""></p>
<p>맨처음 Pre-rendering된 document 파일을 받고, 이후에 <strong>렌더링된 JS파일</strong>들이 Chunk단위로 다운로드 되는것을 볼 수 있음.</p>
<p>그리고 이 JS코드들이 이전에 보내진 HTML DOM 위에서 <strong>한번 더 렌더링</strong>을 하면서, 각자 자기 자리를 찾아가며 매칭이 됨</p>
<p>-&gt;이 과정을 <strong>Hydrate</strong>이라고 부름</p>
<p>✔️  Pre-rendering한 Document는 모든 JS요소들이 빠진 가벼운 상태이므로 클라이언트에게 빠른 로딩이 가능함. 두번 렌더링이 일어나는 단점을 보완하고도 남음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] 웹 동작 과정]]></title>
            <link>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9B%B9-%EB%8F%99%EC%9E%91-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@miin-hyukkk/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9B%B9-%EB%8F%99%EC%9E%91-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Wed, 21 Feb 2024 08:03:31 GMT</pubDate>
            <description><![CDATA[<p>최근 next.js 프로젝트에서 웹페이지 성능을 올리기 위해 이것저것 하다 기초적인 지식이 탄탄하지 않다는 느낌을 받아 이 포스트를 작성</p>
<h3 id="웹페이지가-렌더링-되는-과정">웹페이지가 렌더링 되는 과정</h3>
<blockquote>
<ol>
<li>HTML parser가 HTML을 바탕으로 DOM tree를 그림</li>
<li>CSS parser가 CSS를 바탕으로 CSSOM을 그림</li>
<li>DOM에 CSSOM을 적용하여 Reder Tree를 그림</li>
<li>Render Tree를 바탕으로 Painting하여 실제 화면에 렌더링</li>
</ol>
</blockquote>
<h3 id="dom이란"><strong>DOM이란?</strong></h3>
<p>웹 브라우저와 Javascript가 HTML을 이해하기 쉽도록 트리 구조로 파싱하여 만든 구조화된 표현, 즉 객체입니다.</p>
<p><strong>그저 텍스트에 불과한 HTML을 HTML파서가 DOM이라는 트리구조의 객체로 변환함</strong></p>
<blockquote>
<p><strong>누구를</strong> :  HTML을
<strong>누가</strong>: HTML파서가<br><strong>무엇으로</strong> : DOM이라는 트리구조로</p>
</blockquote>
<p>DOM: 브라우저와 Javascript가 이해할 수 있는 구조</p>
<h3 id="cssom이란"><strong>CSSOM이란?</strong></h3>
<p>DOM에 CSS를 입힌것. DOM에 CSS가 적용된 객체 모델</p>
<p>ex&gt; <code>&lt;p style=&quot;display: none;&quot;&gt;How are you?&lt;/p&gt;</code><br>→ <strong>DOM에는 포함</strong>, but <strong>CSSOM에는 포함X</strong></p>
<blockquote>
<p><strong>웹을 동적으로 제어한다 = DOM을 제어한다</strong>
JS가 DOM을 동적으로 제어할 수 있게, DOM API 라는 것이 기본적으로 제공됨</p>
</blockquote>
<h3 id="jquery-등장-한계점"><strong>JQuery 등장, 한계점</strong></h3>
<blockquote>
<ul>
<li>JS라이브러리, DOM 조작 중심 개발 방식 → 애플리케이션의 상태관리 복잡하게 만듦→ 유지보수 어려움</li>
</ul>
</blockquote>
<ul>
<li>모듈화 부족 → 코드 재사용성 떨어짐, 유지보수 어려움</li>
<li>성능 문제 → 동적인 DOM이 잦은 요즘에는 많은 연산량으로 성능 하락</li>
</ul>
<h3 id="virtual-dom-등장"><strong>Virtual DOM 등장</strong></h3>
<p>직접적인 DOM조작 → 성능저하 → Virtual DOM 등장</p>
<p>Virtual DOM:  실제 DOM을 추상화한 메모리 내 표현</p>
<blockquote>
<ul>
<li>조작이 편함 : 리모컨이 TV조작하는거마냥, 복잡한 내부구조 몰라도 원하는 결과를 얻음</li>
</ul>
</blockquote>
<ul>
<li>성능 최적화 : Virtual DOM은 변경사항을 메모리에 빠르게 적용 → 실제 DOM에는 Diffing알고리즘을 사용하여 최소한의 변경사항으로 계산</li>
<li>자동화된 프로세스 : 리엑트 같은 라이브러리는 이 전체 과정을 자동화함. 개발자가 할 일이라고는 상태관리와 상태변화에 따라 UI를 어떻게 변경할지 선언 뿐임</li>
</ul>
<p><strong>React에서의 Virtual DOM</strong></p>
<p>리엑트에서 값이 변할때 화면의 깜빡임 없이 빠르게 값이 변경되는건 모두 Virtual DOM 때문임
가상 돔을 실제 돔과 비교해 다른 부분만 빠르게 반영해 새로 화면에 렌더링해줌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로젝트] 서스펜스와 SSR]]></title>
            <link>https://velog.io/@miin-hyukkk/%EC%84%9C%EC%8A%A4%ED%8E%9C%EC%8A%A4%EC%99%80-SSR</link>
            <guid>https://velog.io/@miin-hyukkk/%EC%84%9C%EC%8A%A4%ED%8E%9C%EC%8A%A4%EC%99%80-SSR</guid>
            <pubDate>Tue, 13 Feb 2024 06:13:26 GMT</pubDate>
            <description><![CDATA[<p>SSR을 사용하면 JavaScript 번들이 로드되고 실행되기 전에 사용자가 페이지의 콘텐츠를 볼 수 있음</p>
<p>목적 : 앱의 사용자가 콘텐츠를 더 빨리 보고 훨씬 더 빠르게 상호 작용을 시작</p>
<h3 id="이-부분을-공부하고-프로젝트에-적용해야겠다고-생각한-이유"><strong>이 부분을 공부하고 프로젝트에 적용해야겠다고 생각한 이유</strong></h3>
<p>유튜브,네이버 등등 타 사이트의 경우, 느린 네트워크 환경에서도 <strong>뷰포인트의 일부분을 먼저 보여줌</strong>으로써 사용자에게 이 페이지가 멈추지 않음을 느끼게 해줌
일부분이 아직 로딩 스페너가 돌아가고 있고, 이미지,썸네일의 경우 스켈레톤 처리 되어 있어도, 헤더나 메뉴의 경우 클릭이 가능함
이렇게 사용자가 페이지가 멈추지 않았음을 느끼게 하는게 UX측면에서 굉장히 중요하다는 것을 깨달음</p>
<h3 id="ssr만을-사용했을때-문제점"><strong>SSR만을 사용했을때 문제점</strong></h3>
<p>상호작용 가능한 상태를 만들려면 모든것을 수화(Hydrate)해야함
즉, 수화가 시작된 후에는(컴포넌트 함수를 호출한 후에는) 전체 트리에 대해 이 작업이 완료될 때까지 React가 멈추지 않음. 그래서 모든 컴포넌트가 수화될때까지 기다려야함</p>
<p><strong>댓글기능에는 비용이 많이드는 렌더링 로직이 있다고 가정해보자.</strong> </p>
<p>내 컴퓨터에서는 빠르게 작동할 수 있지만, 저사양 기기에서는 그 로직을 실행하는데에 비용이 많이 들고, 화면이 멈출 수 있음. 물론 이런 기능은 Server Components를 사용하는 게 이상적임. 하지만 일부 로직은 이벤트 핸들러를 사용하는게 불가피해서 어쩔 수 없음. <strong>어쨋든, 수화가 시작되면 전체 트리가 수화될때까지 사용자는 네비게이션바, 헤더, 사이드바 등등과 상호작용할 수가 없음</strong>. 사용자는 사이드바나 네비게이션바와 상호작용하여(홈을 누른다던지, 다른 마이페이지로 이동한다던지) 이 페이지를 완전히 벗어나고 싶지만, 계속해서 수화가 일어나고 있어서 현재 페이지에 머물 수 밖에 없음</p>
<p><strong>결국 둘 중 선택해야함</strong>
댓글을 서버 출력에서 제외하여 사용자가 JS가 로드될때까지 댓글을 못보게 하거나, 
서버출력에 포함해서 댓글이 로드되고 전체 트리를 렌더링될때까지 다른 HTML과의 상호작용을 지연시키거나</p>
<p>이런 것들은** waterfall<strong>한 구조때문임
**데이터 패칭(서버)-&gt;HTML로 렌더링(서버)-&gt;코드 로드(클라이언트)-&gt; 수화(클라이언트)</strong>
앞 단계가 완료되기전까지 다음 단계를 실행할 수 없음</p>
<p>결국 해결책은 화면의 일부에 대해 각 단계를 수행할 수 있도록 작업을 분리하는 것임
React18부터 이게 가능한  &lt;<strong>Suspense</strong>&gt;가 정식 적용됨</p>
<h3 id="suspense-사용"><strong>Suspense 사용</strong></h3>
<p>댓글을 서스펜스로 감싼다</p>
<pre><code>&lt;Suspense fallback={&lt;Spinner /&gt;}&gt;
   &lt;Comments /&gt;
&lt;/Suspense&gt;</code></pre><p><code>&lt;Comments&gt;</code>를 <code>&lt;Suspense&gt;</code>로 래핑함으로써, React에게 댓글을 스트리밍하기 위해 기다릴 필요가 없다고 알림. 대신, React는 댓글 대신 플레이스홀더(스피너)를 보냄</p>
<p>초기 HTML에서 Comments는 빠지고 그자리를 spinner가 대체함
댓글에 대한 데이터가 준비되면 React는 추가 HTML을 동일한 스크림으로 보냄</p>
<p>그러면 React자체가 클라이언트에 로드되기 전에 댓글에 대한 HTML이 팝업됨</p>
<p>이제 문제 하나가 해결됨
<strong>화면의 일부가 초기 HTML을 지연시킬 경우 모든 HTML을 지연시키거나 HTML에서 제외할지 선택할 필요가 없게됨</strong></p>
<h4 id="hydrating-the-page-before-all-the-code-has-loaded"><strong>Hydrating the page before all the code has loaded</strong></h4>
<p>초기 HTML을 더 일찍 보낼 수 있지만, 댓글의 JavaScript코드가 로드될 떄까지 클라이언트에서 앱을 수화할 수 없음. 코드 크기가 크면 시간 많이걸림.
그래서 이런 대규모 번들을 피하기 위해 &#39;코드 분할&#39;을 사용해야함.</p>
<p><code>React.lazy</code>를 사용하여 기본 번들에서 Comments코드를 분리</p>
<p> 이렇게 React 18 <code>&lt;Suspense&gt;</code> 에서는 댓글 위젯이 로드되기 전에 앱을 하이드레이션할 수 있음</p>
<h3 id="결론">결론</h3>
<ul>
<li>HTML을 보내기 전에 더 이상 모든 데이터가 서버에 로드될 때까지 기다릴 필요가 없어짐</li>
<li>하이드레이션을 시작하기 위해 더 이상 모든 JavaScript가 로드될 때까지 기다릴 필요가 없음</li>
<li>더 이상 페이지와 상호작용하기 위해 모든 구성 요소가 하이드레이션 될때까지 기다릴 필요가 없음</li>
</ul>
<p>🫠
출처  : <a href="https://github.com/reactwg/react-18/discussions/37#top">https://github.com/reactwg/react-18/discussions/37#top</a></p>
]]></description>
        </item>
    </channel>
</rss>