<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mask_vvs.log</title>
        <link>https://velog.io/</link>
        <description>hello world!</description>
        <lastBuildDate>Thu, 14 Mar 2024 17:16:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>mask_vvs.log</title>
            <url>https://images.velog.io/images/mask_vvs/profile/4ba7797f-1c86-4f35-8656-1e92d93c8dea/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. mask_vvs.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mask_vvs" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[20240315]]></title>
            <link>https://velog.io/@mask_vvs/20240315-sy9yo2ag</link>
            <guid>https://velog.io/@mask_vvs/20240315-sy9yo2ag</guid>
            <pubDate>Thu, 14 Mar 2024 17:16:37 GMT</pubDate>
            <description><![CDATA[<p>PostgreSQL의 기본적인 내용들을 학습했다.</p>
<h3 id="1-관계형-db는-무엇인지">1. 관계형 DB는 무엇인지</h3>
<p>관계형 DB는 데이터베이스 내부의 테이블 간의 관계를 명시하여 구조화된 방식으로 데이터를 관리 및 저장할 수 있게 하는 DBMS를 의미한다.</p>
<ul>
<li>테이블 간의 관계란 잘 아는대로 1:1 / 1:M / M:N을 의미하며 이 관계를 정의짓기 위해 각 테이블은 PK / FK라는 constraint 컬럼이 존재한다.</li>
<li>데이터는 구조화된 형식으로 관리되기 때문에 특정 테이블의 row, 혹은 column과 연결짓기 쉬운 형태를 가진다. -&gt; 쿼리를 통해 데이터를 관리하기 쉬워진다.</li>
<li>row와 column으로 구성된다고 표현한다. (쉽게말해서 row는 각 객체들, column은 모델의 필드를 의미)</li>
<li>관계형 DB에서는 ACID 원칙이 잘 지켜진다.</li>
</ul>
<h3 id="2--관계형-db와-nosql의-차이는-무엇인지">2.  관계형 DB와 NoSQL의 차이는 무엇인지</h3>
<ul>
<li>관계형 DB에서 구조화된 데이터를 저장하는 것과 반대로 NoSQL은 비구조화 혹은 반(semi)구조화 형식의 데이터를 저장한다.</li>
<li>NoSQL 데이터베이스에서는 유연한 데이터 모델들을 사용한다. (JSON이나 key-value 쌍 등).</li>
<li>이로 인해 더윽 복잡한 형태의 데이터를 유연하게 저장할 수 있지만 데이터 지속성과 무결성을 관리하기 어렵다는 특징도 있다.</li>
</ul>
<h3 id="3-postgresql이란-무엇이며-특징은-무엇인지">3. PostgreSQL이란 무엇이며 특징은 무엇인지</h3>
<ul>
<li>관계형 DB 중의 하나.</li>
<li>그 중에서도 수직적 확장성 (CPU / RAM 등의 리소스를 증가시키는)에 특화되어 있다.</li>
<li>다른 관계형 DB들과 비교했을 때 동시성 관리에 특화되어 있어 다수의 유저가 동시에 디비에 접근하는 서비스에 사용하면 유용하다.</li>
<li>데이터 타입이 훨씬 자유롭다.</li>
<li>쿼리 최적화에 용이하다.</li>
<li>모델 관계형 데이터베이스이다.<ul>
<li>유저 정의 데이터 타입이 존재한다.</li>
<li>상속이 가능하다.</li>
<li>다형성이 지원된다.<ul>
<li>따라서 함수나, 오퍼레이터 등을 만들어 사용할 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<p>그 밖에 psql 세팅 및 pgadmin을 이용하여 샘플 데이터 로딩까지 완료</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 푸시(Web Push Notification)를 사용해봅시다.]]></title>
            <link>https://velog.io/@mask_vvs/%EC%9B%B9-%ED%91%B8%EC%8B%9CWeb-Push-Notification%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B4%85%EC%8B%9C%EB%8B%A4</link>
            <guid>https://velog.io/@mask_vvs/%EC%9B%B9-%ED%91%B8%EC%8B%9CWeb-Push-Notification%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B4%85%EC%8B%9C%EB%8B%A4</guid>
            <pubDate>Thu, 31 Aug 2023 14:57:48 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>회사에서 만들고 있는 서비스는 paid 마케팅을 하고 있지 않다. 따라서 앱 서비스에서 사용하는 푸시는 주요한 마케팅 수단이다. 유저에게 직접적으로 메시지를 전달할 수 있고, 무엇보다도 푸시 알림 sass 이용료 등을 제외하면 무료이기 때문이다.</p>
<p>그렇지만 현재 만들고 있는 서비스의 MAU에서 앱 사용자들이 차지하는 비율은 크지 않다. 상당수의 유저는 웹을 통해 우리 서비스를 사용하고 있다. 앞서 말했듯 앱 푸시를 주요 마케팅 수단으로 활용하고 있지만 훨씬 많은 유저를 대상으로 마케팅을 할 수 있다면 좋겠다는 생각이 들었다가 이전에 어떤 영상에서 잠깐 소개했던 웹 푸시가 떠올랐다.</p>
<p>앱 푸시 알림에 사용하는 원시그널이라는 서비스에서도 웹 푸시 기능을 지원하고 있는 만큼 실제 마케팅 수단으로 활용될 수 있는 기술이라는 생각이 있었고, WWDC 2022에서 발표된 바와 같이 mac os / ios도 지원하는 기술이라 잘만 파악해두면 우리 서비스에도 적용할 수도 있겠다는 생각이 들었다.</p>
<p>따라서 (1) 웹 푸시가 어떻게 동작하는지를 큰 틀에서 학습하고 (2) 여러 웹 푸시 sass 들을 살펴보며 실무에 웹 푸시를 사용할 수 있을지 생각해보고자 한다. 검색을 좀 해보니 PWA나 서비스 워커 등 다른 개념들에 대한 이해가 조금 더 필요하긴 한데, 이번 글에서는 학습한 내용 중 &#39;어떻게 웹 푸시를 보낼 수 있는가?&#39;에 해당하는 부분만 먼저 단순하게 정리해보고자 한다.</p>
<h3 id="overview">Overview</h3>
<p>먼저 웹 푸시가 동작하는 큰 틀을 간단하게 이해할 필요가 있다.</p>
<pre><code class="language-md">    +----------------+      +--------------+       +-------------+
    |  UA (Browser)  |      | Push Service |       | Application |
    +----------------+      +--------------+       |   Server    |
        |                       |                   +-------------+
        |      Subscribe        |                      |
        |----------------------&gt;|                      |
        | subscription resource |                      |
        |&lt;=====================&gt;|                      |
        |                       |                      |
        |          Distribute Push Resource            |
        |---------------------------------------------&gt;|
        |                       |                      |
        :                       :                      :
        |                       |     Push Message     |
        |    Push Message       |&lt;---------------------|
        |&lt;----------------------|                      |
        |                       |                      |
</code></pre>
<ul>
<li><strong>브라우저 (사용자)</strong></li>
<li>푸시 알림을 사용자에게 전달할 <strong>푸시 서비스</strong></li>
<li>푸시 알림을 발송할 <strong>서버</strong></li>
</ul>
<p>위와 같이 세 주체로 이루어져있는데 (1) 서버에서 대상(사용자)과 내용이 담긴 메시지 데이터를 푸시 서비스에 전달하면, (2) 푸시 서비스에서 사용자 정보를 식별 후 목적지(즉 브라우저)로 전달하는 것이 큰 흐름이다.</p>
<p>구체적으로 이 과정은 <a href="https://tools.ietf.org/html/draft-ietf-webpush-protocol">웹 푸시 프로토콜</a>이라는 규약을 통해 진행된다.</p>
<p>(1) 유저가 푸시 서비스를 구독하면, 푸시 서비스는 그 유저에게 private한 구독 정보(subscription resource)를 전달한다.
(2) 유저는 서버에 이 구독 정보를 전달한다. 그럼 푸시를 보내는 주체(서버)는 해당 유저에게 푸시를 보내기 위해 필요한 유저 식별 값을 얻게 된다.
(3) 서버에서 푸시를 보낼 때는 메시지를 푸시 서비스에 보낸다. 이 때 (2)를 통해 얻은 유저 식별값을 함께 보낸다.
(4) 푸시 서비스는 서버로 부터 받은 유저 식별값을 가지고 어떤 유저에게 푸시를 전달해야 할 지 결정한다.</p>
<p>이 때 푸시 메시지 또한 당연히 보안의 대상이기 때문에 암호화가 필요하다.</p>
<ul>
<li>(1) 단계에서 유저는 푸시 서비스를 구독할 때 <a href="https://datatracker.ietf.org/doc/html/draft-ietf-webpush-vapid-01">VAPID</a>라는 인증방식의 공개 키를 전달한다(실제 구현에서 VAPID 인증 방식의 공개 키와 비공개 키는 서버에서 생성 후 유저에게 전달해줘야 한다).</li>
<li>(4) 단계에서 서버는 푸시 정보가 담긴 토큰을 VAPID의 비공개 키로 암호화하여 푸시 서비스에 전달한다.</li>
<li>(5)에서 푸시 서비스는 유저의 공개 키를 가지고 서버에서 받은 토큰을 복호화한다. 이를 통해 서버와 메시지에 대한 유효성 검사를 할 수 있게 된다.</li>
</ul>
<h3 id="웹-푸시-구현-환경-세팅">웹 푸시 구현 환경 세팅</h3>
<p>이번 글에서 Application Server에 해당 하는 서버는 구현하지 않는다. 주 학습 목표가 웹이기도 하고 실제 웹 푸시를 발송하고 확인하는 과정을 보다 간편하게 하기 위함이다.</p>
<ol>
<li>구글의 코드랩 레포지토리 중 하나인 <a href="https://github.com/GoogleChromeLabs/web-push-codelab/tree/master">https://github.com/GoogleChromeLabs/web-push-codelab/tree/master</a> 를 클론 받는다. 이 패키지에는 기본적인 아이콘이나 html 파일이 정의되어 있다.</li>
<li><a href="https://simplewebserver.org/">https://simplewebserver.org/</a> 에서 간단한 서버 실행 어플리케이션을 다운 받은 후 홈페이지에 나와 있는대로 서버를 실행시킨다. <code>Folder path</code>는 1을 통해 받은 디렉토리의 <code>app</code> 폴더를 설정하면 된다.</li>
</ol>
<p>이러면 환경 세팅이 완료된다.</p>
<p><code>http://localhost:8080/</code>로 접속하면 아래와 같은 화면이 보인다.
<img src="https://velog.velcdn.com/images/mask_vvs/post/adf81581-3e0b-4f18-93e2-1a2206568bf6/image.png" alt=""></p>
<p>개발자 도구를 켠 뒤 <code>Application</code> -&gt; <code>Service Workers</code>를 클릭한 뒤 이미지와 같이 <code>Update on reload</code>를 활성화 시켜주면 준비가 완료된다. <code>Service Worker</code>에 대해서는 바로 다음 절에서 설명한다.</p>
<h3 id="service-worker-등록">Service worker 등록</h3>
<p>앱 서비스를 생각해보면 앱 서비스를 사용하고 있지 않아도 푸시 메시지를 수신한다. 웹 푸시는 어떨까? 브라우저가 실행되고 있지 않아도 푸시 메시지를 수신할 수 있을까?</p>
<p>service worker(이하 서비스워커)는 이를 가능하게 해준다. 서비스워커는 워커의 일종으로 브라우저 단에서 클라이언트와 서버 사이를 중개하는 프록시 미들웨어로서 동작한다.</p>
<p>이러한 특성에 의거해 네트워크에 연결되지 않아 서버에 API 요청을 보낼 수 없더라도 서비스워커 단에서 특정 리소스들을 캐싱하고 있다가 마치 API 요청이 성공적으로 수행되고 응답을 받아온 것처럼 브라우저에 리소스를 전달할 수 있다. 이러한 특성은 <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps">PWA</a>의 핵심 요소이다.</p>
<p>PWA에 대해서는 추후 별도의 포스트에서 다뤄보기로 하고, 웹 푸시에서 서비스워커는 <strong>푸시 서비스</strong>로서 동작한다. 따라서 우리는 푸시 서비스에 구독하듯이 서비스워커에 브라우저를 구독시켜야 한다. 이를 위해서는 먼저 서비스워커를 &#39;등록&#39;해야 한다.
(이번 글에서는 서비스 워커에 대해서도 웹 푸시를 보내는데 필요한 정도만 언급하고 자세한 내용은 추후 별도의 포스트에서 다룬다. 참고할 수 있는 좋은 자료는 다음과 같다: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">MDN</a>, <a href="https://web.dev/learn/pwa/service-workers/">web dev</a>)</p>
<p>구글의 코드랩 레포지토리를 잘 클론받았다면 <code>app/sw.js</code> 파일을 확인할 수 있을 것이다. 우리는 이 파일에 서비스워커에 관련된 코드들을 작성할 것이다.</p>
<p>우선은 html 로드 시 실행되는 <code>scripts/main.js</code> 파일에서 먼저 서비스워커를 등록해보자.</p>
<pre><code class="language-js">// app/scripts/main.js
async function registerServiceWorker() {
  if (!(&quot;serviceWorker&quot; in navigator)) return;

  console.log(&quot;Service Worker and Push are supported&quot;);

  swRegistration = await navigator.serviceWorker.getRegistration();
  if (!swRegistration) {
    swRegistration = await navigator.serviceWorker.register(&quot;sw.js&quot;);
    console.log(&quot;Service Worker is registered&quot;, swRegistration);
  } else {
    console.log(&quot;Service Worker is already registered&quot;, swRegistration);
  }
};
registerServiceWorker();</code></pre>
<p>먼저 브라우저가 서비스워커를 지원하는지 확인한다.</p>
<p>지원한다면 <code>navigator.serviceWorker.getRegistration</code> 함수를 통해 서비스워커의 등록정보를 확인한다. 만약 아직 서비스워커가 등록되지 않았다면 <code>navigator.serviceWorker.register(&quot;sw.js&quot;);</code> 코드를 통해 서비스워커를 등록하고 <code>swRegistration</code> 변수에 해당 등록 객체를 저장한다. 위에서도 언급했듯이 <code>sw.js</code> 파일을 통해 서비스워커의 동작을 정의한다.</p>
<h3 id="버튼-활성화">버튼 활성화</h3>
<p>우리의 웹 사이트에서 <code>ENABLE PUSH MESSAGING</code> 버튼은 비활성화 되어있다. 이를 활성화 시키는 코드를 작성하자.</p>
<p>마찬가지로 <code>main.js</code> 파일에 다음 두 함수를 추가한다.</p>
<pre><code class="language-js">async function initializeUI() {
  const subscription = await swRegistration.pushManager.getSubscription();
  isSubscribed = subscription;

  console.log(isSubscribed ? &#39;User iS subscribed.&#39; : &#39;User is NOT subscribed.&#39;);

  updateBtn();
}

function updateBtn() {
  if (isSubscribed) {
    pushButton.textContent = &#39;Disable Push Messaging&#39;;
  } else {
    pushButton.textContent = &#39;Enable Push Messaging&#39;;
  }

  pushButton.disabled = false;
}</code></pre>
<p>이후 <code>initializeUI</code> 함수를 <code>registerServiceWorker</code> 함수 몸체 최하단에 추가한 후 새로고침을 하면 웹 사이트가 다음과 같이 변한다.</p>
<p><img src="https://velog.velcdn.com/images/mask_vvs/post/735ff674-6486-4b0a-87f6-aa1b28ad6c93/image.png" alt=""></p>
<p>또한 처음과 달리 개발자 도구에서 등록한 서비스워커를 확인할 수도 있다. 만약 디버깅 등의 이유로 등록해둔 서비스워커를 직접 해제하고 싶다면 이미지 우측의 <code>Unregister</code> 버튼을 클릭한 뒤 새로고침하면 된다.
<img src="https://velog.velcdn.com/images/mask_vvs/post/c5631713-308c-4208-8831-70af5cbde183/image.png" alt=""></p>
<h3 id="서비스워커-구독하기">서비스워커 구독하기</h3>
<p>브라우저는 푸시 서비스를 구독해야 한다. 하지만 우리의 <code>ENABLE PUSH MESSAGING</code> 버튼은 활성화 되었지만 어떠한 동작도 하지 않는 상태이다. 해당 버튼을 클릭 시 구독을 수행해보자.</p>
<p>먼저 <code>initializeUI</code> 함수에서 버튼에 클릭 이벤트 리스너를 달아준다.</p>
<pre><code class="language-js">async function initializeUI() {
  pushButton.addEventListener(&#39;click&#39;, function() {
    pushButton.disabled = true;
    if (isSubscribed) {
      unsubscribeUser();
    } else {
      subscribeUser();
    }
  });

  const subscription = await swRegistration.pushManager.getSubscription();
  isSubscribed = subscription;
  updateSubscriptionOnServer(subscription);

  console.log(isSubscribed ? &#39;User IS subscribed.&#39; : &#39;User is NOT subscribed.&#39;);

  updateBtn();
}</code></pre>
<p>먼저 <code>pushButton</code> element에 클릭 이벤트를 달아준다. 구독되지 않았다면 <code>subscribeUser</code> 함수를 호출하여 브라우저를 구독한다.</p>
<pre><code class="language-js">async function subscribeUser() {
  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
  const subscription = await swRegistration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: applicationServerKey
  })

  if (subscription) {
    console.log(&#39;User is subscribed.&#39;);
    updateSubscriptionOnServer(subscription);

    isSubscribed = true;
  } else {
    console.error(&#39;Failed to subscribe the user: &#39;, error);
  }

  updateBtn();
}</code></pre>
<p>서비스워커 등록 객체(<code>swRegistration</code>에는 <a href="https://developer.mozilla.org/en-US/docs/Web/API/PushManager">pushManager 객체</a>가 존재한다. 이 인터페이스의 <code>subscribe</code> 함수를 통해 구독할 수 있다.</p>
<p><code>applicationServerKey</code>는 위에서 언급한 VAPID의 공개 키를 주입하면 된다. <a href="https://web-push-codelab.glitch.me/">이 사이트</a>에서 임시로 공개 키와 비공개 키를 생성할 수 있다.</p>
<p>구독이 되면 푸시 서비스를 통해 받은 구독 정보를 서버에 전송한다. 하지만 이번 글에서는 서버를 구현하지 않으므로 실제 서버 API를 호출하지는 않고 구독 정보를 웹 사이트에 보여주도록 한다.</p>
<pre><code class="language-js">function updateSubscriptionOnServer(subscription) {
  // TODO: Send subscription to application server

  const subscriptionJson = document.querySelector(&#39;.js-subscription-json&#39;);
  const subscriptionDetails =
    document.querySelector(&#39;.js-subscription-details&#39;);

  if (subscription) {
    subscriptionJson.textContent = JSON.stringify(subscription);
    subscriptionDetails.classList.remove(&#39;is-invisible&#39;);
  } else {
    subscriptionDetails.classList.add(&#39;is-invisible&#39;);
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/mask_vvs/post/35190819-3ea7-4732-977a-50f966139f0c/image.png" alt=""></p>
<p>구독 해제도 실제 서버와 연결되지는 않기 때문에 <code>subscribeUser</code>와 거의 동일하다.</p>
<pre><code class="language-js">function unsubscribeUser() {
  swRegistration.pushManager.getSubscription()
  .then(function(subscription) {
    if (subscription) {
      return subscription.unsubscribe();
    }
  })
  .catch(function(error) {
    console.log(&#39;Error unsubscribing&#39;, error);
  })
  .then(function() {
    updateSubscriptionOnServer(null);

    console.log(&#39;User is unsubscribed.&#39;);
    isSubscribed = false;

    updateBtn();
  });
}</code></pre>
<p>푸시 기능을 사용하기 위해서는 브라우저의 알림 권한이 필요하다. 버튼 클릭 시 알림 권한이 없다면 구독되어선 안될 것이므로 다음과 같이 <code>updateBtn</code> 함수를 수정하자.</p>
<pre><code class="language-js">function updateBtn() {
  if (Notification.permission === &#39;denied&#39;) {
    pushButton.textContent = &#39;Push Messaging Blocked&#39;;
    pushButton.disabled = true;
    updateSubscriptionOnServer(null);
    return;
  }

  if (isSubscribed) {
    pushButton.textContent = &#39;Disable Push Messaging&#39;;
  } else {
    pushButton.textContent = &#39;Enable Push Messaging&#39;;
  }

  pushButton.disabled = false;
}</code></pre>
<h3 id="푸시-이벤트-핸들링">푸시 이벤트 핸들링</h3>
<p>이제 서비스워커의 동작을 정의해야 한다. 서버에서 푸시 메시지를 전송하면 서비스워커는 브라우저의 <code>Notification</code> API를 사용하여 해당 푸시 메시지를 알림의 형태로 바꿔준다.</p>
<pre><code class="language-js">// sw.js
self.addEventListener(&#39;push&#39;, function(event) {
  console.log(&#39;[Service Worker] Push Received.&#39;);
  console.log(`[Service Worker] Push had this data: &quot;${event.data.text()}&quot;`);

  const title = &#39;Push Codelab&#39;;
  const options = {
    body: &#39;Yay it works.&#39;,
    icon: &#39;images/icon.png&#39;,
    badge: &#39;images/badge.png&#39;
  };

  event.waitUntil(self.registration.showNotification(title, options));
});</code></pre>
<p>서비스워커의 동작을 정의한 파일에서 <code>self</code> 객체는 서비스워커를 의미한다.
여기에서는 여러가지 이벤트에 대한 리스너를 정의할 수 있는데, 그 중 <code>push</code> 이벤트의 이벤트 리스너를 작성하여 서버에서 푸시 요청이 왔을 때 알림을 띄우려고 한다.</p>
<p>이는 <code>registration.showNotification</code> 함수를 통해서 실행한다.
<code>event.waitUntil</code>는 서비스워커의 생명주기와 관련된 함수이다. 이 글에서는 서비스워커에 대한 글이 아니지만 간단하게 설명하자면, <code>waitUntil</code> 함수는 <code>Promise</code> 객체를 인자로 받아서 <code>Promise</code>가 처리될 때까지 서비스워커가 동작하도록 한다.</p>
<p>여기까지 코드를 작성했다면 새로고침 후 개발자도구의 서비스워커 탭에서 <code>push</code> 이벤트를 직접 트리거하여 테스트해볼 수 있다.
<img src="https://velog.velcdn.com/images/mask_vvs/post/20fde002-6eed-40d2-aee1-b12072dce90d/image.png" alt=""></p>
<p>위 이미지와 같이 <code>Push</code> 부분에 푸시 메시지를 작성하고 옆의 버튼을 클릭하면 다음과 같이 웹 푸시가 발송되고 콘솔 창에서 입력한 푸시 메시지를 확인할 수 있다!
<img src="https://velog.velcdn.com/images/mask_vvs/post/0d2b88a2-e34d-494c-ab78-05e877357c35/image.png" alt="">
<a href="https://app.slack.com/t/modoodoc/login/z-app-168678765859-6018836049047-40f0de192e0520c4095cfdbd470f5e4aeb709732d1824274256bd14d0d1e76b4?s=slack&amp;x=x-p168678765859-2255575342918-6033372434498">https://app.slack.com/t/modoodoc/login/z-app-168678765859-6018836049047-40f0de192e0520c4095cfdbd470f5e4aeb709732d1824274256bd14d0d1e76b4?s=slack&amp;x=x-p168678765859-2255575342918-6033372434498</a></p>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><a href="https://codelabs.developers.google.com/codelabs/push-notifications/#0">https://codelabs.developers.google.com/codelabs/push-notifications/#0</a></li>
<li><a href="https://geundung.dev/114">https://geundung.dev/114</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Re-engageable_Notifications_Push">https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Re-engageable_Notifications_Push</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps">https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps</a> - <a href="https://developer.mozilla.org/en-US/docs/Web/API/PushManager">https://developer.mozilla.org/en-US/docs/Web/API/PushManager</a></li>
<li><a href="https://web.dev/learn/pwa/service-workers/">https://web.dev/learn/pwa/service-workers/</a></li>
<li><a href="https://tools.ietf.org/html/draft-ietf-webpush-protocol">https://tools.ietf.org/html/draft-ietf-webpush-protocol</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/draft-ietf-webpush-vapid-01">https://datatracker.ietf.org/doc/html/draft-ietf-webpush-vapid-01</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 동시성이란?]]></title>
            <link>https://velog.io/@mask_vvs/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@mask_vvs/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Thu, 29 Jun 2023 19:22:20 GMT</pubDate>
            <description><![CDATA[<h1 id="머리말">머리말</h1>
<p>두달쯤 전인가 회사 업무를 수행하면서 난해하고 해결하기 어려운 문제를 마주쳤다.</p>
<p>특정 페이지에 접근할 때마다 다음과 같은 에러가 발생했고, nextjs에서 띄워주는 에러 알림 창으로 인해 개발에 매우 방해가 되고 있었다.</p>
<blockquote>
<p>This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.</p>
</blockquote>
<p>처음에는 왜 이런 문제가 발생하는지 몰랐기도 하고, 스테이징 환경에 올려서 사용할 때는 아무런 방해가 되지 않았기에 에러창을 꺼가는 불편함을 감수해가며 오랫동안 방치해두었다.
(그리고 리액트 깃헙을 뒤져보았더니 메인테이너들이 이 문제는 리액트의 결함이고 고칠 예정이라고 하기도 했다...!)</p>
<p>최종 배포 전 수정을 하며 여차저차 문제를 파악했다. 아이콘이 여러번 업데이트 되는데 이 과정에서 안전한 동시성의 조건을 충족하지 못한것이 직접적인 원인이었는데, 아이콘이 리렌더링 되는 과정이 너무 복잡해 어떤 업데이트로 인해 발생하는 것인지 명확히 확인하지 못했을 뻔더러 제대로 이해하지 못한 채 <code>Suspense</code> 컴포넌트를 사용한 것이 문제였다. 이참에 이해에 어려움을 겪었던 동시성의 기본 개념에 대해 다시 학습하며 정리하려 한다.</p>
<h2 id="리액트의-동시성concurrency이란">리액트의 동시성(Concurrency)이란?</h2>
<p>동시성(Concurrency)이란 리액트 18 버전의 주요 업데이트 사항 중 하나이다.
동시성이라는 단어 자체가 18 버전 이전의 리액트 생태계에서는 생소한데, 등장 배경을 알기 위해서는 리액트가 화면을 렌더링 해주는 과정에 대해 조금 이해하고 있어야 한다.</p>
<p>나도 정확하게 알지 못해 추후 다시 학습할 내용이지만, 일단 리액트에서 업데이트가 발생하여 화면이 변경될 때 <em>각 업데이트는 동기적으로 동작</em> 한다.</p>
<p><img src="https://user-images.githubusercontent.com/810438/121394782-9be1e380-c949-11eb-87b0-40cd17a1a7b0.png" alt="동기적인 대화"></p>
<p>각각의 업데이트가 동기적으로 진행되는 것을 다른 친구들과 순서대로 대화하는 것에 비유한다면 위와 같이 수행되는 것이다. 이는 당연하게도 최적화 문제로 이어진다. 대화할 친구가 10명이라면 10번째 친구는 하염없이 기다려야 할 것이다. 10번째 친구가 하려는 대화가 정말 중요하고 급한일이라면? 낭패다.</p>
<p>리액트의 저수준 동작 방식까지는 알지 못하더라도 리액트를 통해 서비스를 만들어보았다면 클릭 하나, 키보드 입력 하나에 정말 많은 업데이트가 생기기도 한다는 것을 알고 있을 것이다. 실제 서비스 단에서 이렇게 동기적으로 동작하는 것은 서비스를 사용하는 유저에게 매우 부정적인 경험으로 이어질 수 있다.</p>
<p>기존에 리액트는 내부적으로 업데이트를 모아두어 한 번의 렌더링에 반영하는 batching 기능(이 역시 리액트 18 버전에서는 auto-batching으로 더욱 최적화 되었지만 이 글에서는 다루지 않는다. <a href="https://github.com/reactwg/react-18/discussions/21">참고</a>) 을 지원하는 한편, 개발자는 <code>setTimeout</code>을 통해 업데이트 순서를 조정하거나  <code>debounce</code>, <code>throttle</code> 등을 사용하여 업데이트의 횟수 자체를 제한함으로써 성능 최적화를 도모할 수 있었다.
<br /></p>
<p>이번 글의 주제인 동시성에서는 다음과 같이 대화가 이루어진다.
<img src="https://user-images.githubusercontent.com/810438/121394880-b4ea9480-c949-11eb-989e-06a95edb8e76.png" alt="동시적인 대화"></p>
<p>실제로 두명과 동시에 대화하는 것이 아니라, 대화의 중요도(긴급함)에 따라 Alice와 Bob과 번갈아가며 얘기하는 것이다. 여기에서의 핵심은 <strong>더 중요한 대화를 하기위해, 즉 더 중요한 업데이트를 처리하기 위해 현재의 업데이트를 잠시 미뤄둔다는 것</strong>이다.</p>
<p>싱글 스레드로 동작하는 자바스크립트에서는 <code>window.requestIdleCallback</code>와 같은 메서드를 통해 이를 구현할 수 있다. 이 메서드는 스레드가 Idle 상태가되면 인자로 넘겨받은 콜백함수를 실행한다. 이렇게 함으로써 업데이트가 발생한 시점에 관계없이 중요한 업데이트부터 적용할 수 있는 것이다.</p>
<p>이상의 얘기를 간단하게 코드로 표현하면 다음과 같다.</p>
<ul>
<li><p>업데이트가 동기적으로 이루어지는 경우</p>
<pre><code class="language-typescript">function renderBlocking(Component) {
  for (let Child of Component) {
      renderBlocking(Child);
  }
}</code></pre>
</li>
<li><p><code>window.requestIdleCallback</code>을 사용하여 중요도에 따라 업데이트 순서를 조정하는 경우</p>
<pre><code class="language-typescript">function renderConcurrent(Component) {
  // stale 되어 필요없는 업데이트가 되었다면 ex. 같은 state에 여러 번의 수정이 발생한 경우
  if (isCancelled) return;

  for (let Child of Component) {
      requestIdleCallback(() =&gt; renderConcurrent(Child));
  }
}</code></pre>
<p>다만 <code>window.requestIdleCallback</code> 메서드는 사파리에서 지원되지 않는 등 호환성의 문제가 있기 때문에 실제 리액트 팀이 선택한 방식은 다른 것인 것 같다.</p>
</li>
<li><blockquote>
<p>(<a href="https://github.com/facebook/react/blob/5309f102854475030fb91ab732141411b49c1126/packages/scheduler/src/forks/Scheduler.js#L209">이 로직</a>이 그것인 것 같은데 나중에 다시 살펴봐야지)</p>
</blockquote>
</li>
</ul>
<h2 id="there-is-no-concurrent-mode-there-are-only-concurrent-features">&quot;there is no concurrent mode, there are only concurrent features&quot;</h2>
<p>처음 리액트 팀은 리액트 18 버전 자체가 내부적으로 동시성을 충족하며 동작하기를 의도했고, 이를 <code>Concurrent Mode</code>라고 불렀다. 하지만 Concurrent Mode로 전환하는 과정에서 리액트 팀의 의도와 다르게 이전 버전 리액트와 호환 문제가 생기면서 결국 Concurrent Mode는 포기하게 된다(근데 지금도 이름은 동시성 모드이긴 하지만 내부 동작은 동시성이 디폴트가 아니라고 한다).</p>
<p>더불어 동시성을 다루는 일, 즉 최적화 작업에 대한 책임을 개발자에게 주기 위해서 Concurrent Mode가 아닌 Concurrent features라고 표현한다.</p>
<p>Concurrent features에 포함된 훅 중 제일 기본이 되는 <code>startTransition</code>에 대해 알아보려 한다.</p>
<h3 id="starttransition">startTransition</h3>
<p>먼저 transition이라는 개념을 알아야 한다. 리액트 팀에서는 상태 업데이트를 두가지 범주로 구분한다.</p>
<ol>
<li><strong>urgent updates</strong>: 타이핑, 클릭, 누르기 등의 직접적인 상호작용을 반영하는 업데이트</li>
<li><strong>transition updates</strong>: 하나의 뷰에서 다른 뷰로의 UI 전환 업데이트</li>
</ol>
<p>동시성이라는 개념을 처음 접했을 때는 UI가 빠르게 업데이트 되는 것이 서비스의 성능이라고 생각해 왜 UI 전환 업데이트가 non-urgent 취급을 받는지 잘 이해가 되지 않았는데, 다음의 예시를 스스로 떠올려보며 납득하게 되었다.</p>
<p>인풋창에 검색하고, 인풋의 값으로 자동 완성 검색어를 만드는 기능을 생각해보자.</p>
<ul>
<li>유저가 검색 인풋창을 클릭하고 검색어를 입력하는 동안 (1) 커서가 깜빡일 것이고, (2) 유저가 입력한 내용이 인풋창에 반영된다. 이 둘은 <strong>긴급한(urgent) 업데이트</strong>이다. 만약 이 둘이 곧바로 적용되지 않는다면? 유저는 해당 서비스가 동작하지 않거나, 렉 걸린다고 생각할 것이다.</li>
<li>반면 검색을 클릭한 뒤에 자동으로 만들어진 검색어 리스트가 노출되는 것은 정도가 심하지 않다면 얼마간 지연되도 유저는 이를 감수한다. 정보를 가져오는(만드는) 시간이 있다고 생각하기 때문이다. 때로는 debounce 등의 목적으로 개발자가 지연을 의도하기도 한다.</li>
</ul>
<pre><code class="language-typescript">/**
 * 리액트 18 버전 이전에는 다음 상태 업데이트가 같은 렌더링에 반영되기 때문에
 * 유저의 타이핑이 검색창에 바로 반영되지 않고 자동완성검색어가 만들어진 뒤에
 * 함께 적용된다.
 */

function onChangeInput(value) {
    setInputValue(value);
      setAutoCompletedSearchQuery(
      makeAutoCompletedSearchQuery(value)
    )
}</code></pre>
<p>위에서 정의한 범주에 따르면 <code>setInputValue</code>는 urgent update에, <code>setAutoCompletedSearchQuery</code>는 transition update에 해당한다. 후자의 반영을 잠시 미루고 전자만 빠르게 렌더링하기 위해 리액트 18 버전에서 제공하는 <code>startTransition</code> 함수를 사용할 수 있다.</p>
<pre><code class="language-typescript">import { startTransition } from &#39;react&#39;;

function onChangeInput(value) {
  setInputValue(value);
  startTransition(() =&gt; {
    setAutoCompletedSearchQuery(
      makeAutoCompletedSearchQuery(value)
    )
  });
}</code></pre>
<p>사용은 위에서 언급한 <code>window.requestIdleCallback</code> 메소드와 비슷한데, <code>startTransition</code> 함수에 콜백으로 상태 업데이트 함수를 넣어주면 된다.</p>
<p>여기까지 보다보면 &#39;<code>setInputValue</code>만 먼저 실행해서 렌더링하고 싶은 거면 <code>setAutoCompletedSearchQuery</code>를 <code>setTimeout</code>의 콜백함수로 넘겨줘도 되는거 아니야?&#39;라고 생각할 수도 있다.</p>
<p>하지만 <code>startTransition</code>와 <code>setTimeout</code>은 다르다.</p>
<ol>
<li>우선 나중으로 스케줄링되는 <code>setTimeout</code>과 달리 <code>startTransition</code>의 실행 자체는 동기적으로 바로 일어난다. 다만 그 내부의 모든 업데이트가 <code>transitions</code>로 마킹될 뿐이다. 리액트는 이 정보를 어떻게 업데이트를 렌더링할지 결정하는데 사용한다. 이는 <code>setTimeout</code>으로 감싼 것에 비해 좀 더 빠르게 업데이트를 렌더링한다는 것을 의미하는데, 기기의 성능에 따라 두 업데이트 간의 딜레이가 많이 차이 나더라도 UI는 반응성을 유지할 것이다.</li>
<li><code>setTimeout</code>의 콜백함수는 여전히 동기적으로 작동한다. 즉, 콜백함수에서 페이지의 UI를 크게 바꾼다면 그동안 유저가 계속 타이핑하고 있더라도 이 업데이트의 반영이 늦어질 수 있다. 하지만 <code>startTransition</code>로 감싸진 업데이트는 중단될 수 있기 때문에 완성된 자동 검색어 리스트가 현재 검색어와는 다르다면 해당 리스트를 뷰에 반영하지 않을 수 있다.</li>
<li><code>setTimeout</code>은 단순히 딜레이만 주기 때문에 로딩바 등을 보여주는 시점을 결정하기 위해 비동기 코드를 작성해야 한다. 하지만 <code>startTransition</code>의 경우 업데이트가 실제 발생되기 전 대기되고 있는 상태를 알 수 있는 값 <code>isPending</code>을 추가한 훅 <code>useTransition</code>을 사용할 수 있다.</li>
</ol>
<p><code>useTransition</code>의 용례와 주의할 점은 <a href="https://react.dev/reference/react/useTransition">공식문서 링크</a>로 대체한다.</p>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://www.bbss.dev/posts/react-concurrency/">https://www.bbss.dev/posts/react-concurrency/</a> (메인 참고 자료)
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback">https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback</a>
<a href="https://github.com/reactwg/react-18/discussions/46#discussioncomment-846786">https://github.com/reactwg/react-18/discussions/46#discussioncomment-846786</a>
<a href="https://github.com/reactwg/react-18/discussions/64">https://github.com/reactwg/react-18/discussions/64</a>
<a href="https://github.com/reactwg/react-18/discussions/41">https://github.com/reactwg/react-18/discussions/41</a>
<a href="https://react.dev/reference/react/useTransition">https://react.dev/reference/react/useTransition</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react-native-webview (2) 웹 코드에서 앱 코드로 데이터 요청하기]]></title>
            <link>https://velog.io/@mask_vvs/react-native-webview-2-%EC%9B%B9%EC%97%90%EC%84%9C-%EC%95%B1%EC%9C%BC%EB%A1%9C-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%A0%84%EB%8B%AC-%ED%9B%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B0%98%ED%99%98%EB%B0%9B%EA%B8%B0</link>
            <guid>https://velog.io/@mask_vvs/react-native-webview-2-%EC%9B%B9%EC%97%90%EC%84%9C-%EC%95%B1%EC%9C%BC%EB%A1%9C-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%A0%84%EB%8B%AC-%ED%9B%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B0%98%ED%99%98%EB%B0%9B%EA%B8%B0</guid>
            <pubDate>Sun, 30 Apr 2023 14:59:13 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>react-native-webview 라이브러리를 활용하여 웹앱에서 데이터를 주고받는 방법에 대해 설명한 <a href="https://velog.io/@mask_vvs/react-native-webview-1-%EC%9B%B9%EB%B7%B0%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%9B%B9%EA%B3%BC-%EC%95%B1-%EC%82%AC%EC%9D%B4%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A3%BC%EA%B3%A0-%EB%B0%9B%EA%B8%B0">지난 글</a>에 이은 두번째 react-native-webview 활용 방법에 대한 글이다. 이번 글을 잘 이해하려면 지난 글에서 다룬 내용을 숙지하고 있어야 한다.</p>
<h2 id="문제-상황-정의">문제 상황 정의</h2>
<p>지난 글에서는 웹, 앱에서 각각 <code>postMessage</code>, <code>receiveMessage</code> 메서드를 활용하여 앱에서 웹뷰를 통해 웹사이트에 방문했을 때 (1) 웹 -&gt; 앱, (2) 앱 -&gt; 웹으로 메시지를 주고 받는 방법에 대해 다루었다.
이 때 웹과 앱 사이에서 주고 받는 메시지는 단발적으로만 전달되었는데, 지난 글 말미에 언급했듯이 이런 방식은 웹뷰를 사용할 때 특정 케이스를 커버할 수 없게 된다.</p>
<p>유저 핸드폰 앨범에 있는 사진을 받아와 onSuccess 콜백 함수의 인자로 넘겨주는 함수 <code>getPhotos</code>를 구현한 아래 코드를 보자.</p>
<pre><code class="language-javascript">export const getPhotos = (onSuccess) =&gt; {
  if (isReactNativeWebView()) {
    // 앱으로 이미지를 받아오라는 메시지를 전달
    webviewBridge.postMessage(&quot;photo&quot;);

    // photos라는 변수에 이미지 데이터가 담기면 다시 웹단 코드로 처리
    onSuccess(photos);
    return;
  }

  ...
};</code></pre>
<ul>
<li>핸드폰 앨범에 있는 사진에 접근해야 하는 행동은 브라우저에서 처리할 수 없기 때문에 우선 앱에서 접속한 경우를 분기처리해야 한다.</li>
<li>앱에서 사진을 받아오는 작업을 처리하도록 <code>postMessage</code> 메시지를 보낸다.</li>
</ul>
<p>이후 앱에서는 어떻게 일이 처리될까? 이전 글에서 다룬 일반적인 메시지 전달 방식을 생각해보자.
(1) 앱에서 유저에게 사진 접근 권한을 묻는다.
(2) 유저는 권한을 승인하고 사진을 선택한다.
(3) 앱에서 웹뷰 컴포넌트에 연결해둔 <code>webviewRef</code> 객체의 <code>postMessage</code> 메서드를 활용하여 선택된 사진 데이터를 웹 코드로 넘겨준다.</p>
<p>여기서 중요한 건 &#39;(3)에서 웹으로 넘겨준 사진 데이터에 어떻게 접근할 수 있는가?&#39;이다. 메시지는 <code>{type, data}</code> 형태의 객체로 전달되며, 메시지가 전달되는 경우 해당 type과 미리 매핑된 핸들러 함수가 실행된다. 즉, <u><strong>(3)에서 웹으로 전달한 메시지의 타입과 연결된 핸들러에서만 사진 데이터에 접근할 수 있다</strong></u>는 것이다.</p>
<p>여기까지 이해가 되었다면 위의 <code>getPhotos</code> 함수를 다시보자</p>
<pre><code class="language-javascript">export const getPhotos = (onSuccess) =&gt; {
  if (isReactNativeWebView()) {
    // 앱으로 이미지를 받아오라는 메시지를 전달
    webviewBridge.postMessage(&quot;photo&quot;);

    // photos라는 변수에 이미지 데이터가 담기면 다시 웹단 코드로 처리
    onSuccess(photos);
    return;
  }

  ...
};</code></pre>
<p>결국 <code>onSuccess</code>의 인자로 넘겨주기 위해<code>postMessage</code> 함수를 실행한 이후에 <code>photos</code>라는 변수에 사진 데이터 값이 담겨있어야 하는데, 상술했듯 사진 데이터는 앱에서 웹으로 보낸 메시지의 타입과 연결된 핸들러에서만 접근이 가능하다.</p>
<p>자, 그럼 이제 문제가 분명해졌다.
(1) 어떻게 하면 <code>getPhotos</code> 함수에서 앱으로 메시지를 보낸 후 비동기적으로 앱에서 리턴해오는 사진 데이터 값에 접근할 수 있을까?
(2) 어떻게 하면 <code>onSuccess</code> 함수가 실행되기 전에 사진 데이터를 받아와서 해당 데이터에 접근할 수 있다고 보장할 수 있을까?</p>
<h2 id="문제-해결하기">문제 해결하기</h2>
<p>어떻게 이 문제를 해결할 지 본격적으로 고민하기 전에 나는 먼저 문제가 해결된 상황을 상상해봤다.
나는 <code>getPhotos</code> 함수를 어떤 방식으로 구현하고 싶은걸까?</p>
<p>답은 간단했다. 네트워크 요청을 처리하는 비동기 함수를 사용하는 것처럼 만들고 싶었다.</p>
<pre><code class="language-javascript">export const getPhotos = async (onSuccess) =&gt; {
  if (isReactNativeWebView()) {
    // 앱으로 이미지를 받아오라는 메시지를 전달
    const photos = await webviewBridge.postMessage(&quot;photo&quot;);

    // photos라는 변수에 이미지 데이터가 담기면 다시 웹단 코드로 처리
    onSuccess(photos);
    return;
  }

  ...
};</code></pre>
<p>(1) 어떻게 하면 <code>getPhotos</code> 함수에서 앱으로 메시지를 보낸 후 비동기적으로 앱에서 리턴해오는 사진 데이터 값에 접근할 수 있을까?
-&gt; 기존의<code>postMessage</code> 함수는 메시지를 앱단으로 전달하고 종료된다. 별다른 반환값이 없는 함수인데, 서버에서 데이터를 받아오기 위해 네트워크 요청을 보내는 것처럼 <code>postMessage</code> 함수 실행 이후 내가 원하는 데이터를 반환받으면 된다.</p>
<p>(2) 어떻게 하면 <code>onSuccess</code> 함수가 실행되기 전에 사진 데이터를 받아와서 해당 데이터에 접근할 수 있다고 보장할 수 있을까?
-&gt; <code>postMessage</code> 함수에 await를 걸어서 photos 변수에 값을 할당한 이후에 <code>onSuccess</code> 함수를 실행시킨다.</p>
<p>자바스크립트의 <code>Promise</code> 객체와 지난 게시글에서 학습했던 observer를 활용해서 해당 기능을 구현할 수 있을 것 같아 전체 플로우를 보다 구체적으로 생각해보았다.</p>
<ol>
<li>앱으로 메시지를 보낸 후, 데이터가 필요한 경우 <code>postMessage</code>를 비동기 함수로 만든다. 이 때, 마치 네트워크 API를 호출하는 것처럼 데이터를 받을 때까지 pending 상태로 만들어둔다.</li>
<li>앱에서 데이터 처리가 완료되면 웹뷰로 해당 데이터를 돌려준다.</li>
<li>웹에서 해당 데이터를 받았다는 것이 확인되면, 이에 반응하여 pending 상태였던 <code>postMessage</code>를 데이터와 함께 resolve 시킨다.</li>
<li><code>postMessage</code>의 반환을 기다리고 있던 <code>getPhotos</code> 함수에서는 해당 데이터를 가지고 <code>onSuccess</code> 콜백 함수를 실행한다.</li>
</ol>
<p>각각의 단계를 보다 구체적으로 살펴보자.</p>
<h3 id="1-postmessage-비동기-함수로-만들기">1. <code>postMessage</code> 비동기 함수로 만들기</h3>
<p>제일 먼저 해야 할 것은 <code>postMessage</code> 함수를 네트워크 요청처럼 비동기 함수로 만드는 일이다. 그래야 photos 변수에 값이 할당되기 전에 onSuccess 함수가 실행되지 않는다는 것을 보장할 수 있다.</p>
<p>먼저 <code>window.ReactNativeWebview.postMessage</code>를 비동기 함수로 감싸줘야 하는데 이를 구현하기 위해 추가로 해줘야 할 작업이 있다.</p>
<pre><code class="language-javascript">const postMessage = async (
  type,
  data?,
  options,
) =&gt; {
  window.ReactNativeWebView?.postMessage(JSON.stringify({ type, data }));
  if (options?.isAsyncPostMessage) {
    const result = await handlePostMessageResult(type);
    return result;
  }

return null;
};

const handlePostMessageResult = async (type) =&gt; {
  const wrappedPromise = getWrappedPromiseObj(messageMap[type]);

  const result = await wrappedPromise.promise;
  return result;
};

export const getWrappedPromiseObj = (type) =&gt; {
  let resolvePromise;
  let rejectPromise;

  const promise = new Promise((resolve, reject) =&gt; {
    resolvePromise = resolve;
    rejectPromise = reject;
  });

  const wrappedPromise = {
    promise,
    type,
    resolve: resolvePromise,
    reject: rejectPromise,
  };

  return wrappedPromise;
};</code></pre>
<p><strong>postMessage</strong>
먼저 데이터 반환이 필요한 <code>postMessage</code>를 구분해주기 위해 <code>options</code> 파라미터를 추가한다. <code>isAsyncPostMessage</code>가 <code>true</code>라면 데이터 반환이 필요한 <code>postMessage</code>로 간주된다. 그럼 <code>handlePostMessageResult</code> 함수가 리턴될 때까지 대기하게되고(<code>await</code>) <code>handlePostMessageResult</code>가 반환되면 그제서야 <code>postMessage</code> 함수가 종료된다.
-&gt; 이것으로 가장 바깥 함수인 <code>postMessage</code>를 비동기 함수로 만들 수 있다.</p>
<p><strong>handlePostMessageResult</strong>
<code>postMessage</code> 함수의 비동기적인 특성은 사실상 <code>handlePostMessageResult</code> 함수에서 기인한다.
 해당 함수에서는 <code>postMessage</code> 함수에서 넘겨주는 메시지 타입을 Promise로 감싸서 해당 Promise가 resolve 되길 기다렸다가 종료된다.</p>
<p><strong>getWrappedPromiseObj</strong>
메시지 객체를 Promise 객체로 감싸서 반환하는 함수이다. 해당 Promise가 어떠한 타입의 메시지를 처리하는 것인지를 나타내기 위해 type 값을 선언했다. 또한 해당 Promise의 resolve, reject를 Promise 바깥에서도 처리할 수 있는 구조를 만들기 위해 <code>resolvePromise</code>, <code>rejectPromise</code> 변수를 활용 및 <code>promise</code> 바깥으로 노출시켰다. 이렇게 구현한 이후는 추후 설명할 observer 패턴에서 사용하기 위함이다.</p>
<p>해당 단계에서 작업한 것을 정리하자면 (1) 메시지를 Promise로 변환하고, (2) 해당 Promise가 종료될 때까지 <code>postMessage</code> 함수를 pending 상태로 유지하면서 반환하지 않게 만듦으로써 <code>potsMessage</code> 함수를 비동기 함수로 변환하였다.</p>
<p>그럼 Promise 함수는 언제, 어떻게 resolve 될 수 있을까? 또한 위에서 문제를 정의할 때 앱에서 넘겨준 사진 데이터는 메시지의 해당 타입과 연결된 핸들러에서만 접근할 수 있는데 <code>postMessage</code> 함수에서 어떻게 접근해서 해당 데이터를 반환해준다는 것일까?</p>
<h3 id="2-웹에서-해당-데이터를-받았다는-것이-확인되면-이에-반응하여-pending-상태였던-postmessage를-데이터와-함께-resolve-시킨다">2. 웹에서 해당 데이터를 받았다는 것이 확인되면, 이에 반응하여 pending 상태였던 postMessage를 데이터와 함께 resolve 시킨다.</h3>
<ul>
<li>앱에서 넘겨준 사진 데이터는 메시지의 타입과 연결된 핸들러에서만 접근할 수 있다.</li>
<li>그리고 우리는 위에서 메시지의 타입을 감싼 Promise 객체를 생성하고, <code>postMessage</code> 함수가 해당 Promise 객체의 상태가 바뀔때까지 리턴되지 않도록 처리했다.</li>
</ul>
<p>이 두가지 정보를 종합하면, 문제 해결의 실마리를 얻을 수 있다.
<u><strong>메시지의 타입과 연결된 핸들러에서 postMessage가 기다리고 있는 Promise 객체를 종료시킬 수 있게 하는 것이다.</strong></u></p>
<p>지난 글에서 정의한 <code>receiveMessage</code> 함수이다.</p>
<pre><code class="language-javascript">const receiver = platform === ios ? window : document;

receiver.addEventListener(&#39;message&#39;, (event) =&gt; {
  const { type, data } = JSON.parse(event.data);

  // 여기에서 Promise 객체에 접근할 수 있다면?

  const handler = receiveWebviewMessageMap.get(type);
  handler?.(data);
});</code></pre>
<p>주석이 쓰여진 위치에서는 앱에서 보낸 메시지의 타입과 데이터에 접근할 수 있다. 즉 해당 위치에서 Promise 객체에 접근하여 사진 데이터를 resolve의 인자로 넘겨준다면 최종적으로 postMessage가 사진 데이터를 반환할 수 있게되는 것이다.</p>
<p>이 때 <code>postMessage</code>와 <code>receiveMessage</code>라는 서로 다른 함수에서 같은 Promise 객체를 공유할 어떠한 공간이 필요한데, 이를 위해서 사용한 것이 observer 패턴이 되겠다.
사내 코드에 적용된 부분이라 더 디테일하게 설명하기는 어렵지만, 간략하게 설명하자면 <code>postMessage</code>에서는 옵저버에 객체를 등록하고 <code>receiveMessage</code> 에서는 타입을 활용하여 해당 타입에 등록되어있는 Promise 객체들을 종료시키는 구조이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20230412]]></title>
            <link>https://velog.io/@mask_vvs/20230412</link>
            <guid>https://velog.io/@mask_vvs/20230412</guid>
            <pubDate>Wed, 12 Apr 2023 02:32:00 GMT</pubDate>
            <description><![CDATA[<ol>
<li>schema.org
 structured data는 html 태그로 전부 나타내지 못하는 의미를 검색 엔진에게 추가로 전달하기 위한 구조화된 데이터이다. 구글에선 리치 스니펫에 많이 사용되는데 이런 정보들을 검색엔진에 제공하는 방법을 표준화하기 위해 구글, 야후, ms 등이 공동으로 개발한 일종의 표준화 및 문서화 작업 프로젝트이다.</li>
<li>AMP
 리치 스니펫말고, 리치 카드라는 것이 있는데 이걸 활용하기에 좋은 컴포넌트들.. next랑도 연동이 되는 것 같고 한 번 공부를 해볼 필요가 있을 것 같다. 리치 스니펫보다 리치 카드를 더 밀어주는 것 같기도 하고...?</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[react-native-webview (1) 웹뷰를 통해 웹과 앱 사이에서 데이터 주고 받기]]></title>
            <link>https://velog.io/@mask_vvs/react-native-webview-1-%EC%9B%B9%EB%B7%B0%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%9B%B9%EA%B3%BC-%EC%95%B1-%EC%82%AC%EC%9D%B4%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A3%BC%EA%B3%A0-%EB%B0%9B%EA%B8%B0</link>
            <guid>https://velog.io/@mask_vvs/react-native-webview-1-%EC%9B%B9%EB%B7%B0%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%9B%B9%EA%B3%BC-%EC%95%B1-%EC%82%AC%EC%9D%B4%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A3%BC%EA%B3%A0-%EB%B0%9B%EA%B8%B0</guid>
            <pubDate>Thu, 30 Mar 2023 18:29:32 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>회사에서 프론트엔드 코드를 다시 쓰는 리빌딩 프로젝트를 진행 중이다.
가장 큰 목적 중 하나는 개발 생산성을 높이는 것인데, 이를 위해 nextjs로 웹 개발을 하고 앱은 React Native와 react-native-webview를 사용해서 nextjs 프로젝트를 띄워주는 웹앱 형태로 서비스를 구현하고 있다.</p>
<p>react-native-webview는 모바일 플랫폼 네이티브 웹뷰를 react native의 컴포넌트로 추상화하여 제공하는 라이브러리이다. 기본적으로 웹앱에서는 웹뷰를 통해 웹 프로젝트를 띄워주는 간접적인 기능만 구현하지만, 때때로 웹뷰로 보여지는 웹 프로젝트와 앱 프로젝트 간 데이터를 주고받아야 하는 경우가 있다.
앱의 자동 로그인 기능이 그 예시인데, 자동 로그인을 하고 있는 앱을 켤 경우 로드되는 웹 프로젝트는 로그인 정보를 가지고 있지 않기 때문에 앱에서 로그인이 되었다는 어떤 정보를 웹으로 넘겨줘야 할 것이다. 또 다른 주요한 예시로는 모바일 기기의 권한 활용에 관련된 여러 기능인데, 웹 프로젝트는 브라우저를 통해 여러가지 권한을 얻는 반면 앱에서는 모바일 기기를 통해 동의를 얻어야 하므로 웹 프로젝트의 권한 요청이 모바일 기기의 권한 요청으로 이어져야 한다.</p>
<p>이번 글과 다음 글에서는 이 react-native-webview 라이브러리를 사용해서 웹앱을 구현할 때 데이터를 주고 받는 방식에 대한 설명과 경험을 작성하려고 한다.</p>
<p>먼저 이번 글에서는 일반적으로 데이터를 주고 받는 방법을 주로 다룰 것이고, 다음 글에서는 이에 더해 데이터를 주고 받는 조금은 특별한 케이스에 대해 다룰 예정이다.</p>
<h2 id="react-native-webview-설치">react-native-webview 설치</h2>
<p>사실 설치하는 방법은 라이브러리 레포지토리에 잘 나와있긴 하지만(<a href="https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md">참고</a>), 그래도 간단하게 정리한다.</p>
<ol>
<li><p>react-native-webview 설치
<code>yarn add react-native-webview</code>
물론 npm을 사용해도 설치가 가능하다.</p>
</li>
<li><p>네이티브 의존성 링크
RN은 자바스크립트로 작성하지만 결국 네이티브에서 실행된다. 따라서 RN 라이브러리는 자바스크립트 라이브러리이기 때문에, 단순하게 라이브러리를 설치한다고 실제 모바일 기기에서 실행되지 않는다. 실제 동작을 위해서는 라이브러리에서 지원하는 기능들을 구동시키기 위해 네이티브 의존성들을 설치해줘야 한다. 아마 이 글을 사보고 웹뷰를 사용해보는 사람들의 경우 이런 링크를 수동으로 할 필요는 없겠지만, ios와 android 별로 추가 작업이 필요하다.</p>
</li>
</ol>
<ul>
<li><p>CocoaPods를 사용하는 경우, <code>react-native link</code>는 podfile을 업데이트 시켜준다. 따라서 업데이트된 podfile의 의존성들을 설치해주어야 한다.
<code>pod install</code></p>
</li>
<li><p>6.X.X 이상 버전의 react-native-webview 라이브러리를 사용한다면 RN 프로젝트 <code>android/gradle.properties</code> 파일에 다음 코드를 추가해주어야 한다.</p>
<p>  android.useAndroidX=true
  android.enableJetifier=true</p>
</li>
</ul>
<ol start="3">
<li>컴포넌트 사용
설치가 되었으면 라이브러리에서 제공하는 웹뷰 컴포넌트를 사용하면 된다. 공식 문서에서 제공하는 예시는 다음과 같다.<pre><code class="language-jsx">import React, { Component } from &#39;react&#39;;
import { WebView } from &#39;react-native-webview&#39;;
</code></pre>
</li>
</ol>
<p>class MyWeb extends Component {
  render() {
    return (
      &lt;WebView
        source={{ uri: &#39;<a href="https://infinite.red&#39;">https://infinite.red&#39;</a> }}
        style={{ marginTop: 20 }}
      /&gt;
    );
  }
}</p>
<pre><code>
react-native-webview의 공식문서는 클래스 컴포넌트를 사용한 예시만 나와있어서 불편한 점이 꽤 많긴 하지만 우선 위 컴포넌트의 `uri`에 웹뷰를 통해 보고자 하는 웹 페이지의 주소를 적어두면 앱에서 웹을 볼 수 있게 된다.

웹뷰가 제공하는 기능도 문서에 잘 정리되어 있고([참고](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md)), 웹뷰를 활용하는 기능 별 보다 자세한 가이드도 제시되어 있다([참고](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Guide.md)).

## 데이터 주고 받기

그럼 이제 본격적으로 글의 주제에 관해 얘기해보자.

아까 예시로 들었던 자동 로그인의 경우에는 **앱 -&gt; 웹**으로 &#39;자동 로그인 된 상태&#39;를 나타내는 데이터를 전달해주어야 한다. 반면 권한 요청의 경우에는 웹 프로젝트 코드에서 권한을 요청하는 기능을 수행할 때 실제로는 앱에서 권한을 요청해야 하므로 **웹 -&gt; 앱**으로 데이터를 전달해야 한다.

이렇게 웹앱에서 웹과 앱 사이에서 데이터를 주고 받기 위해서 웹뷰를 활용할 수 있다.
사실 이 방법에 대해서는 공식 문서에서도 잘 소개해주고 있긴하다([참고](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Guide.md#communicating-between-js-and-native)). 하지만 내가 사용하는 방식은 조금 다르기도 하고, 어차피 이번 글에서 다루는 내용 자체는 소개와 안내 정도의 의미를 갖는 정도이다,.

세가지 토픽을 소개하고 이를 중점으로 설명하려고 한다.
### 1. 웹 페이지를 앱(웹뷰)에서 접속한 것을 어떻게 알 수 있나요? 
제일 먼저 웹 서비스 자체는 자신이 앱에서 웹뷰를 통해 보여지든지 데스크탑의 브라우저를 통해 실행되든지 실행에 있어서 차이가 없다. 하지만 우리는 어떤 플랫폼 (앱 &lt;-&gt; 나머지)에서 실행되는지에 따라 다른 처리를 해줘야 하기 때문에 웹 코드에서 &#39;앱에서 웹뷰를 통해 보여지고 있다&#39;는 정보를 얻어야 한다.

이는 `window` 객체에 `ReactNativeWebview` 값이 있는지 여부를 확인해보면 알 수 있다. react-native-webview를 통해 웹에 접속한 경우 `window.ReactNativeWebview` 객체가 존재하고, 이 값을 사용하여 필요한 분기 처리 등을 수행하면 된다.

우리 회사는 nextjs로 웹 프로젝트를 구현했는데, 이런 경우에는 단순히 `window.ReactNativeWebview`를 참조하면 참조 에러가 발생하게 된다. nextjs에서는 서버 사이드 렌더링을 제공하는데, 서버에는 `window` 객체가 존재하지 않기 때문이다. 이러한 문제를 처리하는 방법이 여러가지가 있겠지만, 제일 간단한 것은 해당 값의 참조를 `useEffect` 등의 훅 안에서 처리하는 방법이다. `useEffect`는 클라이언트 사이드에서 실행되는 것이 보장되기 때문에 `window` 객체가 있다는 것 역시 보장된다.

다음과 같은 커스텀 훅을 만들어서 사용할 수 있다.
```javascript
const useIsReactNativeWebview = () =&gt; {
  const [isReactNativeWebview, setIsReactNativeWebview] = useState(false);

  useEffect(() =&gt; {
      if (window.isReactNativeWebview) setIsReactNativeWebview(true);
  }, []);

  return isReactNativeWebview;
}</code></pre><h3 id="2-웹에서-앱으로-어떻게-데이터를-전송할-수-있나요">2. 웹에서 앱으로 어떻게 데이터를 전송할 수 있나요?</h3>
<p>이건 두 파트로 좀 더 상세하게 나눌 수 있다.</p>
<p><strong>1. 웹 -&gt; 앱으로 데이터 보내기</strong>
상술한 <code>window.ReactNativeWebview</code> 객체에는 <code>postMessage</code> 라는 메서드가 존재한다. 해당 메서드의 인자로 넘겨주는 데이터는 현재 웹 페이지를 띄워주는 웹뷰(= 앱)에 전달 된다. 이 때 전달하는 데이터는 항상 직렬화 해주어야 한다.</p>
<p>또한 여러가지 유형의 데이터를 전송할텐데 앱에서 전송된 데이터를 구분하여 처리하기 위해 전달할 데이터의 <code>type</code>을 함께 전달한다. 코드 예시는 다음과 같다.</p>
<pre><code class="language-jsx">const postMessage = (type, data) =&gt; {
  window.ReactNativeWebView?.postMessage(JSON.stringify({ type, data }));
};</code></pre>
<p><code>postMessage</code> 함수를 실행할 때 발생시켜야 할 여러가지 side effect 들을 함께 처리할 수 있기 때문에 wrapper 함수를 사용한다.</p>
<p><strong>2. 웹에서 전달한 메시지를 앱에서 받기</strong></p>
<p>(이번에는 함수형 컴포넌트로) 다시 웹뷰 컴포넌트를 보자.</p>
<pre><code class="language-jsx">const Webview = () =&gt; (
  &lt;WebView
    source={{ uri: &#39;https://infinite.red&#39; }}
    style={{ marginTop: 20 }}
    onMessage={({ nativeEvent }) =&gt; {
      const { type, data } = JSON.parse(nativeEvent.data);

      const handler = receiveWebviewMessageMap.get(type);
      handler?.(data);
    }}
  /&gt;  
);</code></pre>
<p><code>Webview</code> 컴포넌트의 <code>onMessage</code> 속성에는 웹에서 <code>postMessage</code>를 통해 데이터를 전달했을 때 실행되는 콜백함수를 정의할 수 있다.</p>
<p>웹에서 데이터를 직렬화 한 다음 전달했으니, 해당 값을 역직렬화하여 type와 data 값을 구한다. 그리고 type에 해당하는 핸들러를 실행시켜주면 되는데 if 문을 난사하는 것보다 타입과 핸들러를 매핑시켜놓은 객체를 따로 관리하면 코드를 보다 깔끔하게 분리하여 관리할 수 있다.</p>
<h3 id="3-앱에서-전송하는-데이터를-웹에서-어떻게-받을-수-있나요">3. 앱에서 전송하는 데이터를 웹에서 어떻게 받을 수 있나요?</h3>
<p>마찬가지로 두 가지로 나누어 보자.</p>
<p><strong>1. 앱 -&gt; 웹으로 데이터 보내기</strong>
위 공식문서에 나와 있는 것과 같이 <code>Webview</code> 컴포넌트의 <code>injectJavaScript</code> 속성을 사용하면 웹뷰에 실행될 자바스크립트 코드를 직접 주입 시키는 형태로 데이터를 전달할 수 있다.</p>
<p>해당 방식의 예시는 다음과 같다.</p>
<pre><code class="language-jsx">import React, { Component } from &#39;react&#39;;
import { View } from &#39;react-native&#39;;
import { WebView } from &#39;react-native-webview&#39;;

export default class App extends Component {
  render() {
    const runFirst = `
      document.body.style.backgroundColor = &#39;red&#39;;
      setTimeout(function() { window.alert(&#39;hi&#39;) }, 2000);
      true; // note: this is required, or you&#39;ll sometimes get silent failures
    `;
    return (
      &lt;View style={{ flex: 1 }}&gt;
        &lt;WebView
          source={{
            uri: &#39;https://github.com/react-native-webview/react-native-webview&#39;,
          }}
          onMessage={(event) =&gt; {}}
          injectedJavaScript={runFirst} // injectJavaScript 속성과는 다르지만 비슷하게 동작한다.
        /&gt;
      &lt;/View&gt;
    );
  }
}</code></pre>
<p>하지만 나는 다른 방식을 사용한다. <code>Webview</code> 컴포넌트에 ref를 붙이면 ref를 이용하여 웹 -&gt; 앱으로 데이터 보내는 것과 비슷한 방식으로 데이터를 전달할 수 있다.</p>
<pre><code class="language-jsx">const index = () =&gt; {
    const webviewRef = useRef();

    const postMessage = ({ type, data }) =&gt; {
        if (!ref.current) return;

          webviewRef.postMessage(JSON.stringify({ type, data }));
    };

    return (
        &lt;WebView
          ref={webviewRef}
        /&gt;
    );
}</code></pre>
<p><strong>2. 앱에서 전달한 데이터를 웹에서 받기</strong></p>
<p>기본적인 메커니즘은 앱에서 데이터를 받는 것과 비슷하다.
앱에서 <code>postMessage</code>를 통해 데이터를 보내는 것이 하나의 이벤트가 되어 웹 코드에서는 이벤트에 반응하도록 코드를 작성하면 된다. 즉, 이벤트 리스너를 달아주면 된다.</p>
<pre><code class="language-javascript">const receiver = platform === ios ? window : document;

receiver.addEventListener(&#39;message&#39;, (event) =&gt; {
  const { type, data } = JSON.parse(event.data);

  const handler = receiveWebviewMessageMap.get(type);
  handler?.(data);
});</code></pre>
<p>함수의 몸체 부분은 앱에서 데이터를 전달받았을 때 처리하는 코드와 동일하다.
여기서 눈여겨 봐야 할 것은 <code>addEventListener</code>의 주체인 <code>receiver</code> 라는 값이다. 대단한 값은 아니고 모바일 기기의 플랫폼에 따라 서로 다른 값을 부여해야 하는데, 이를 하나의 변수에 담아서 사용한 것이다. <code>platform === ios</code> 부분은 일반적으로 user agent의 값을 확인해서 ios / android를 구분한다.</p>
<h2 id="마치며">마치며</h2>
<p>이렇게 웹뷰를 사용해서 웹 &lt;-&gt; 앱 코드 간에 데이터를 공유하는 기본적인 방법에 대해 알아보았다. 간단한 코드로 데이터를 공유할 수 있지만 타입 스크립트를 활용하거나, 실무에서 사용하다보면 전체적인 코드가 보다 복잡해지긴 한다.
그래도 사실 구조 자체는 간단하기도 하고 데이터를 보내고 받는 코드가 웹, 앱에서 거의 비슷해서 크게 어려운 점은 없을 것이라 생각한다.</p>
<p>다음 글에서는 데이터를 공유하는 보다 특수한 케이스에 대해서 다룰 예정이다. 이 글에서 소개한 방식은 (1) 한 플랫폼에서 다른 플랫폼으로 데이터를 전달하는 것과 (2) 데이터를 받아서 처리하는 코드가 분리되어 있다.
만약, 유저의 사진첩에서 사진을 받아와서 해당 데이터를 처리하는 기능을 웹뷰를 통해 구현한다고 하면 어떻게 할 수 있을까?</p>
<pre><code class="language-javascript">// 웹 코드

1. 유저의 사진첩에서 데이터를 가져온다. // 사진에 접근하는 권한을 요청하고 사진을 받아오는 과정은 네이티브(앱)에서 처리해야 한다.
2. 사진 데이터를 가공한다.</code></pre>
<p>다음의 순서로 처리하면 된다고 생각 될 것이다.
(1) 1을 수행하기 위해 웹에서는 <code>postMessage</code>로 사진 데이터가 필요하다는 정보를 앱으로 보낸다.
(2) 앱의 <code>onMessage</code>에서 타입을 확인 후, 사진에 접근하는 권한을 요청하고 유저가 사진을 선택한다.
(3) 앱에서 웹으로 <code>postMessage</code>를 통해 사진 데이터를 전달한다.
(4) 웹에서는 <code>receiver.addEventListener</code>를 통해 등록한 핸들러에서 사진 데이터를 전달받아 2를 수행한다.</p>
<p>여기서 문제는 2가 실행되기 전에 1이후 (2), (3)이 모두 수행되어야 한다는 점이다. (2), (3)이 실행되기 전까지 코드는 2번 라인으로 넘어가지 않고 1번에서 대기하고 있어야 하기 떄문이다. 1에 해당하는 <code>postMessage</code>는 호출되고 끝인데, 어떻게하면 2가 실행될 때 데이터를 받아온 상태라는 것을 보장할 수 있을까?</p>
<p>나는 <code>postMessage</code>를 호출 했을 때, 반환값(사진)을 받을 수 있다면 위 기능을 구현할 수 있겠다고 생각했다. 다음 글에서는 이 방법 &#39;postMessage에서 반환값 받기&#39;를 주제로 글을 작성할 예정이다.
-&gt; <a href="https://velog.io/@mask_vvs/react-native-webview-2-%EC%9B%B9%EC%97%90%EC%84%9C-%EC%95%B1%EC%9C%BC%EB%A1%9C-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%A0%84%EB%8B%AC-%ED%9B%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B0%98%ED%99%98%EB%B0%9B%EA%B8%B0">다음 글</a></p>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><a href="https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md">https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md</a></li>
<li><a href="https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md">https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md</a></li>
<li><a href="https://github.com/react-native-webview/react-native-webview/blob/master/docs/Guide.md">https://github.com/react-native-webview/react-native-webview/blob/master/docs/Guide.md</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20230306]]></title>
            <link>https://velog.io/@mask_vvs/20230306</link>
            <guid>https://velog.io/@mask_vvs/20230306</guid>
            <pubDate>Mon, 06 Mar 2023 23:17:20 GMT</pubDate>
            <description><![CDATA[<p>RN 안드로이드 빌드를 처음부터 하면서 알았던 내용들</p>
<ul>
<li>aab와 apk의 차이<ul>
<li>apk : Android Application Package</li>
<li>aab: Android App Bundle =&gt; 앱의 서명 및 다양한 번들을 합치는 과정을 구글 플레이스토어에 맡길 수 있다는 차이점. 다만 플레이스토어에 종속되고 서명 역시 구글에서 진행되기 때문에 보안 문제가 있을 수도..!</li>
</ul>
</li>
<li>gradle 설정의 각종 옵션들<ul>
<li>증분 빌드</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[반응형 프로그래밍이란?]]></title>
            <link>https://velog.io/@mask_vvs/%EB%B0%98%EC%9D%91%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@mask_vvs/%EB%B0%98%EC%9D%91%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Tue, 28 Feb 2023 13:15:53 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@mask_vvs/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Observer-pattern-1">Observer pattern의 개념에 대해 간단하게 정리한 지난 게시글</a>을 작성하며 &#39;반응형&#39;이라는 키워드에 대해 알아보고 싶었다.</p>
<p>이번 게시글에서는 반응형 프로그래밍이 뭔지 정리하고 다음 게시글에서는 프론트엔드 개발에 있어서 반응형 프로그래밍의 의미와 영향, 그리고 내가 주로 사용하는 리액트와 반응형 프로그래밍의 관계에 대해 정리할 계획이다.</p>
<h2 id="프로그래밍-패러다임이란">프로그래밍 패러다임이란?</h2>
<blockquote>
<p>프로그래밍 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 해 주고, 결정하는 역할을 한다. 예를 들어 객체지향 프로그래밍은 프로그래머들이 프로그램을 상호작용하는 객체들의 집합으로 볼 수 있게 하는 반면에, 함수형 프로그래밍은 상태값을 지니지 않는 함수값들의 연속으로 생각할 수 있게 해준다. <a href="https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D_%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84#:~:text=%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%20%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84%EC%9D%80%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%97%90%EA%B2%8C%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%98%20%EA%B4%80%EC%A0%90%EC%9D%84%20%EA%B0%96%EA%B2%8C%20%ED%95%B4%20%EC%A3%BC%EA%B3%A0%2C%20%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94%20%EC%97%AD%ED%95%A0%EC%9D%84%20%ED%95%9C%EB%8B%A4.%20%EC%98%88%EB%A5%BC%20%EB%93%A4%EC%96%B4%20%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%80%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EB%93%A4%EC%9D%B4%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%EC%9D%84%20%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9%ED%95%98%EB%8A%94%20%EA%B0%9D%EC%B2%B4%EB%93%A4%EC%9D%98%20%EC%A7%91%ED%95%A9%EC%9C%BC%EB%A1%9C%20%EB%B3%BC%20%EC%88%98%20%EC%9E%88%EA%B2%8C%20%ED%95%98%EB%8A%94%20%EB%B0%98%EB%A9%B4%EC%97%90%2C%20%ED%95%A8%EC%88%98%ED%98%95%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%80%20%EC%83%81%ED%83%9C%EA%B0%92%EC%9D%84%20%EC%A7%80%EB%8B%88%EC%A7%80%20%EC%95%8A%EB%8A%94%20%ED%95%A8%EC%88%98%EA%B0%92%EB%93%A4%EC%9D%98%20%EC%97%B0%EC%86%8D%EC%9C%BC%EB%A1%9C%20%EC%83%9D%EA%B0%81%ED%95%A0%20%EC%88%98%20%EC%9E%88%EA%B2%8C%20%ED%95%B4%EC%A4%80%EB%8B%A4.">위키백과</a></p>
</blockquote>
<p> 반응형 프로그래밍에 대해 알아보기 전에 프로그래밍 패러다임에 대해 찾아봤다. 명령형, 선언형, 객체 지향형 등 여러 패러다임이 있다는 것을 알고 있고, 각각의 패러다임에 어울리는 언어가 있다는 건 알고 있지만 교과서 지식처럼만 외워왔던 것 같다.</p>
<p> 예를 들어, 객체 지향형의 특징이 뭔지, 메서드를 어떻게 분리하고 구성하는지와 같은 것들은 책을 통해서 학습하기도 하고 여러 코드도 봐서 익숙했다. 하지만 &#39;프로그램을 설계하고 구현할 때 객체 지향형 프로그래밍 방식을 택하는게 어떤 의미를 갖는지&#39;에 대해서는 명확하게 답변하지 못했다.</p>
<p>아무튼, 이번에 다시 찾아보면서 스스로 정리한 바는 프로그래밍 패러다임이란 프로그램을 바라보는 하나의 &#39;가치관&#39;이라는 것이다. 딱 위의 문구를 보고 이해가 팍 된 것 같았는데, 사실 정말 많은 자료나 책에서 이미 봐왔던 문구라서 조금 머쓱하긴하다. 별것 아닌 개념일 수도 있지만 조금씩이나마 내공이 쌓여서 이제야 제대로 이해된 것 같기도 하고?</p>
<h2 id="반응형-프로그래밍">반응형 프로그래밍</h2>
<blockquote>
<p><em>In computing, reactive programming is a <strong>declarative programming paradigm</strong> concerned with <strong>data streams</strong> and <strong>the propagation of change</strong>.</em> (<a href="https://en.wikipedia.org/wiki/Reactive_programming#:~:text=In%20computing%2C%20reactive%20programming%20is%20a%20declarative%20programming%20paradigm%20concerned%20with%20data%20streams%20and%20the%20propagation%20of%20change.">wikipedia</a>)</p>
</blockquote>
<p>반응형 프로그래밍 역시 프로그램을 바라보는 가치관 중 하나이다.
그럼 이 가치관은 어떠한 요소들을 중요하게 다루고 있을까? 그 요소들은 다음과 같다.</p>
<ul>
<li>선언적 프로그래밍 패러다임</li>
<li>데이터 스트림(data stream)</li>
<li>변화의 전파(the propagation of change)</li>
</ul>
<h3 id="선언적-프로그래밍-패러다임">선언적 프로그래밍 패러다임</h3>
<p>자, 프로그래밍 패러다임이 어떤 말인지 이제야 이해가 되었으니 이번엔 &#39;선언적&#39; 프로그래밍 패러다임의 차례이다.</p>
<p>사실 이것도 명령형 프로그래밍 패러다임과 대비되는 것으로 Javascript의 <code>forEach</code> 같은 Array 메서드들이 그 예시이고 등등... 알고 있는 지식은 있었지만 이해가 제대로 되지 않던 개념인데 이번에 어느정도 이해가 된 것 같아 정리해본다.</p>
<p>그동안 선언적 혹은 선언형 프로그래밍이 잘 이해가지 않았던 이유는 용어가 너무 모호하기 때문에다, &#39;선언형&#39;이라는 용어와 내가 알고 있는 프로그래밍의 개념이 잘 매칭되지 않았고 그 반대의 유형으로 일컬어지는 &#39;명령형&#39;까지 생각해보면 더 모호했다.</p>
<p>결국 이해한 바는 어떠한 프로그램을 구현할 때, &#39;자신의 책임(기능)을 수행하는 방법&#39;을 코드로 작성하면 명령형 프로그래밍이라고 할 수 있고 &#39;책임을 수행했을 때의 모습&#39;을 코드로 작성한다면 선언형 프로그래밍이라고 할 수 있다.</p>
<p>아 뭔가 내가 참고한 자료들의 용어를 그대로 쓰지 않고 나만의 언어로 바꿔서 작성하려니까 다시 모호해진다... 아직 내공이 부족하긴 한가보다.</p>
<p>이럴 때 유용하게 사용할 수 있는 것이 바로 예시니까, 내가 주로 사용하는 리액트의 예시를 생각해봤다.</p>
<pre><code class="language-javascript">const App = () =&gt; {
      const [num, setNum] = useState(2);

    return (
      &lt;div&gt;{num}&lt;/div&gt;
    );
}</code></pre>
<p>화면에 변경이 가능한 숫자를 띄우고 싶다고 하자. 그럼 책임을 &#39;UI를 그리는 것&#39;이라고 할 수 있다.</p>
<p>선언형 프로그래밍을 지향하는 리액트에서는 위와 같은 컴포넌트를 작성하면 된다. 이렇게 컴포넌트를 작성해두면 <code>num</code>의 값이 바뀔 때마다 화면의 숫자도 바뀌게 된다.</p>
<p>우리의 관심은 숫자를 어떻게 바꾸는 지가 아니라 &#39;<strong>화면에 해당 숫자가 떠있는 것(UI를 그리는 것)</strong>&#39;에 있으므로, 숫자가 바뀐 값이 화면에 반영되는 과정과 방법이 아닌 화면에 그려지는 UI의 모습을 기술하는 리액트의 JSX 문법은 선언형 프로그래밍에 해당한다고 할 수 있다.</p>
<p>한편, 바뀐 숫자의 값을 다음과 같은 방법으로 변경할 수도 있다.</p>
<pre><code class="language-javascript">    // 1. 바뀐 숫자를 담을 tag Element를 가져오고
    const divElem = document.getElementById(&#39;numDiv&#39;);

    // 2. 새로운 숫자를 해당 element에 넣어준다.
    divElem.innerText = newNumber;</code></pre>
<p>위 방법에서는 바뀐 숫자를 화면에 반영해주는 과정을 코드로 구현하고 있다. 이렇게 자신의 책임을 수행하는 방법을 코드로 작성하는 방식이 명령형 프로그래밍에 해당한다고 볼 수 있다.</p>
<h3 id="반응형-프로그래밍-1">반응형 프로그래밍</h3>
<p>반응형 프로그래밍으로 돌아와서, 이제 <strong>데이터 흐름</strong>과 <strong>변화의 전파</strong>에 대해 알아보자.</p>
<ul>
<li>선언적 프로그래밍 패러다임</li>
<li>데이터 흐름 (data stream)</li>
<li>변화의 전파(the propagation of change)</li>
</ul>
<pre><code class="language-javascript">const App = () =&gt; {
      const [num, setNum] = useState(2);
      const [num2, setNum2] = useState(1);

      useEffect(() =&gt; {
        setNum2(prevState =&gt; prevState + num);
    }, [num]);

    return (
      &lt;div&gt;{num} {num2}&lt;/div&gt;
    );
}</code></pre>
<p>위 리액트 컴포넌트를 보자, <code>useEffect</code> 훅에서는 <code>num</code>의 값이 바뀔 때마다 <code>num2</code>의 값도 같이 바꿔주고 있다.</p>
<p>즉, <code>useEffect</code>를 통해서 (1) <code>num</code> 값의 <strong>변화</strong>가 (2) <code>num2</code>에 <strong>전파</strong>되는 <strong>데이터의 흐름</strong>이 만들어진 것이다.</p>
<p>이와 같은 과정을 조금 더 확장해 &#39;프로그래밍을 이런 관점에서 수행하는 것&#39;이 반응형 프로그래밍이라고 할 수 있겠다.</p>
<p>여기서 Observer 패턴을 간단하게 구현한 <a href="https://velog.io/@mask_vvs/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Observer-pattern-1">지난 게시글</a>과 연결점을 생각해보자.</p>
<pre><code class="language-javascript">const orderManager = new OrderManager();

orderManager.subscribe(new SteakSession());
orderManager.subscribe(new PastaSession());

orderManager.takeOrder({
  pasta: &#39;봉골레&#39;,
  steak: &#39;티본&#39;,
});</code></pre>
<p>위 코드에서는</p>
<ol>
<li>봉골레 파스타와 티본 스테이크라는 새로운 데이터가 추가되면 (데이터의 변화)</li>
<li>해당 데이터의 변화가 스테이크 세션과 파스타 세션에 전파되는 흐름이 생긴다.</li>
</ol>
<p>이제 Observer 패턴이 반응형 프로그래밍 방식의 구현체라는 것을 확인할 수 있다.</p>
<p>반응형 프로그래밍에 대해 잘 몰랐더라도 프론트엔드 개발자라면 반응형 프로그래밍에 어느정도는 익숙할텐데, 바로 DOM에서 사용하는 이벤트리스너가 반응형으로 동작하기 때문이다.</p>
<p>ex. <code>document.addEventListener(&#39;click&#39;, () =&gt; {...});</code></p>
<ol>
<li>특정 dom 요소의 클릭 이벤트(데이터) 발생</li>
<li>클릭 이벤트의 생성(데이터의 변화)가 전파</li>
<li>해당 이벤트에 연결된 핸들러의 실행</li>
</ol>
<p><br/><br/><br/></p>
<p>참고 자료)
<a href="https://yozm.wishket.com/magazine/detail/1334/">https://yozm.wishket.com/magazine/detail/1334/</a></p>
<p><a href="https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D_%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84">https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D_%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84</a></p>
<p><a href="https://ko.wikipedia.org/wiki/%EC%84%A0%EC%96%B8%ED%98%95_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D">https://ko.wikipedia.org/wiki/%EC%84%A0%EC%96%B8%ED%98%95_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</a></p>
<p><a href="https://en.wikipedia.org/wiki/Programming_paradigm">https://en.wikipedia.org/wiki/Programming_paradigm</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[디자인 패턴 - Observer pattern]]></title>
            <link>https://velog.io/@mask_vvs/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Observer-pattern-1</link>
            <guid>https://velog.io/@mask_vvs/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Observer-pattern-1</guid>
            <pubDate>Tue, 31 Jan 2023 14:37:54 GMT</pubDate>
            <description><![CDATA[<p>개발을 하다보면 어떤 기능을 구현하기 위한 여러가지 방법이 떠오를 때가 있다.
그러면 보통 유지보수 및 확장성, 로직의 명료함 등등을 고려하여 그 중 하나로 선택 후 구현을 하는데 아직까지는 스스로의 선택에 100% 확신을 갖지는 못하고 있다.</p>
<p>개발 중 이러한 작업을 마치고 나면 자연스럽게 디자인 패턴에 대한 관심으로 이어진다.
오늘은 그 시작으로 Observer 패턴에 대해 공부해보았다.</p>
<h3 id="behavioral-patterns-행위-패턴">behavioral patterns (행위 패턴)</h3>
<p>행위 패턴이란 객체가 데이터를 공유하는 방법이나 객체 사이에서 데이터를 교환하는 방법에 대한 가이드를 제공한다. 데이터나 태스크의 책임을 분배하면서도 각 책임간의 결합도를 최소화 하려는 목적이 있는 패턴이라고 한다. </p>
<h3 id="observer-pattern-옵저버-패턴">Observer pattern (옵저버 패턴)</h3>
<p>옵저버 패턴은 말그래도 어떠한 객체의 상태를 관찰하다가 상태 변화가 감지되었을 때 해당 사실을 관찰자들에게 전파하는 패턴이다.</p>
<p>즉, 관찰자(observer)와 관찰 대상 객체간의 관계를 구조화하는 것이다. 옵저버 패턴에서 사용하는 주요한 개념을 먼저 짚고 넘어가보자.</p>
<ol>
<li><strong>observer</strong>
 관찰자는 특정 객체의 상태 변화를 기다렸다가, 상태 변화가 발생했을 때 특정 행동을 수행하는 일종의 &#39;구독자&#39;이다.</li>
<li><strong>observable</strong>
 observable은 옵저버가 구독하고 있는 대상으로서 다음과 같은 기능을 수행할 수 있다.<ul>
<li><code>subscribe</code>: 특정 옵저버를 observable 객체에 구독시킨다.</li>
<li><code>notify</code>: 현재 객체를 구독하고 있는 옵저버들에게 상태의 변경을 전달한다.</li>
</ul>
</li>
</ol>
<p>간단한 상황을 전제하고 패턴을 사용해서 코드를 짜보자.</p>
<p>파스타, 스테이크 세션으로 나누어 조리를 하는 레스토랑에서 주문이 들어올 때마다 각 세션에서 조리해야 할 메뉴를 알고 싶다고 가정한다.</p>
<p>먼저 observer 객체는 다음과 같이 구성된다.</p>
<pre><code class="language-javascript">
class SteakSession {
  constructor() {
    this.menu = null;
  }

  cook(order) {
    this.menu = order.steak;
    console.log(`스테이크 세션: ${order.steak} 조리 시작합니다.`);
  }
}

class PastaSession {
  constructor() {
    this.menu = null;
  }

  cook(order) {
    this.menu = order.pasta;
    console.log(`파스타 세션: ${order.pasta} 조리 시작합니다.`);
  }
}</code></pre>
<p>각 세션에서 cook이라는 행동을 수행하면 인자로 넘겨받는 주문 중 자기 세션의 메뉴를 조리한다는 정보를 출력한다.</p>
<p>다음으로 해당 observer들이 구독할 observable 객체는 다음과 같이 만들 수 있다.</p>
<pre><code class="language-javascript">class OrderManager {
  constructor () {
    this.sessions = [];
  }

  subscribe(session) {
    this.sessions.push(session);
  }

  takeOrder(order) {
    this.sessions.forEach(session =&gt; session.cook(order));
  }
}</code></pre>
<p>내부적으로 구독하는 옵저버들의 리스트를 관리하고, 구독 중인 옵저버들에게 데이터의 변화를 알릴 수 있는 메서드를 가지고 있다.
(전제된 상황에 맞추어 <code>observers</code> -&gt; <code>sesions</code>, <code>notify</code> -&gt; <code>takeOrder</code> 로 이름 변경)</p>
<pre><code class="language-javascript">const orderManager = new OrderManager();

orderManager.subscribe(new SteakSession());
orderManager.subscribe(new PastaSession());

orderManager.takeOrder({
  pasta: &#39;봉골레&#39;,
  steak: &#39;티본&#39;,
});</code></pre>
<p>만약 누군가 새로운 주문을 하게 된다면(상태 변화), 구독된 각 세션에서는 주문 중 자기 세션의 메뉴를 조리하기 시작한다(상태 변화를 감지 후 행동 수행).</p>
<pre><code class="language-text">스테이크 세션: 티본 조리 시작합니다.
파스타 세션: 봉골레 조리 시작합니다.</code></pre>
<p>예시 코드로 옵저버 패턴의 사용 방식을 확인해보았으니 장단점을 다음과 같이 정리할 수 있을 것 같다.</p>
<ol>
<li>장점<ul>
<li><strong>관심사와 책임의 분리</strong>
observer 객체와 observable 객체는 강하게 결합되어 있지 않아 언제든지 분리될 수 있어 데이터의 변경와 데이터를 처리하는 로직, 로직을 관리하는 책임을 유연하게 관리할 수 있다.</li>
</ul>
</li>
</ol>
<ol start="2">
<li>단점<ul>
<li><strong>메모리 누수 우려</strong>
자바스크립트에서는 가비지 컬렉터를 통해 메모리를 관리한다. 따라서 옵저버가 더이상 특정 객체를 구독할 필요가 없음에도 구독이 해지되지 않는다면, 계속 참조(구독)되고 있기 때문에 해당 옵저버의 메모리가 해제되지 않는다는 점을 조심해야 한다.</li>
</ul>
</li>
</ol>
<br />
<br />


<h3 id="추가로-궁금한-점">추가로 궁금한 점</h3>
<p>이전까지 디자인 패턴의 학습을 미루어왔던 가장 큰 이유는 어떠한 패턴을 학습한 것을 실제 나의 개발에 어떻게 반영할 지, 그리고 내가 사용하고 있는 기능에 어떻게 반영되어 있는지 감이 잘 오지 않았기 때문이다.</p>
<p>이런 학습은 패턴을 익힌다고 하더라도 실제로 내가 패턴을 활용하는데에는 큰 도움을 주지 못할 것 같아서, 이어지는 다음 글에서는 옵저버 패턴이 <strong>(1) 내 개발에 어떻게 적용될 수 있을지와 (2) 내가 사용하는 기능에 어떻게 활용되고 있는지</strong> 를 보다 깊게 살펴보려고 한다.</p>
<p>대략 글의 소재는 다음과 같다.</p>
<ol>
<li><strong>반응형</strong></li>
</ol>
<p>결국 특정 상태의 변화와 이 변화의 전파에 관한 패턴이라는 점에서 &#39;반응형&#39;이라는 키워드가 떠올랐고, 위키에 반응형 프로그래밍을 검색해본 결과 기본 개념과 맞닿아 있는 것 같았다.</p>
<blockquote>
<p><em>In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change.</em> (wikipedia)</p>
</blockquote>
<p>그래서 찾아보니 RxJS 자체가 Observer 패턴을 구현한 라이브러리라고 한다. 그동한 반응형 프로그래밍의 얘기는 많이 들어왔었는데, 이전에 반응형 프로그래밍 관련 세미나를 엄청 어렵게 들었던 기억이 있어 학습은 미루고 있었다.</p>
<p>다음 글에서 반응형 프로그래밍이란 어떠한 것인지 대략적인 그림을 파악하고자 한다.</p>
<br />

<ol start="2">
<li><strong>리액트 훅</strong></li>
</ol>
<p>반응형에 초점을 두고 조금 더 생각해보니 주로 사용하는 리액트의 <code>useEffect</code> 훅과도 유사한 점이 있는 것 같았다.</p>
<pre><code class="language-javascript">const Example = () =&gt; {
      useEffect(() =&gt; {
        grillSteak(order.steak);
          cookPasta(order.pasta);
    }, [order]);

    return &lt;div&gt;example&lt;/div&gt;
}</code></pre>
<p>의존성 배열에 포함된 값이 변경될 때마다 (상태 변화), callback 함수가 실행되는 것이 옵저버 패턴에서 구독 후 상태 변화를 전파 받는 것과 유사한 것 같았는데 옵저버 패턴과 어떠한 점이 다른지, 다르다면 왜 옵저버 패턴을 사용하지 않았는 지 등을 학습하려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django M:N 필드로 order_by 걸기 (annotate, Subquery, OuterRef 사용) ]]></title>
            <link>https://velog.io/@mask_vvs/Django-MN-%ED%95%84%EB%93%9C%EB%A1%9C-orderby-%EA%B1%B8%EA%B8%B0-annotate-Subquery-OuterRef-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@mask_vvs/Django-MN-%ED%95%84%EB%93%9C%EB%A1%9C-orderby-%EA%B1%B8%EA%B8%B0-annotate-Subquery-OuterRef-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Wed, 23 Feb 2022 15:42:58 GMT</pubDate>
            <description><![CDATA[<p>첫 직장 입사 8개월차 주니어 프론트엔드 개발자의 업무 일지 겸 기록입니다.
혹시 내용을 잘못 알고 있다거나, 더 좋은 방법이 떠오르신다면 댓글에 남겨주세요. 
정말 많은 도움이 될 거에요. 미리 감사드립니다! 🙏</p>
<h2 id="결론">결론</h2>
<h3 id="정보">정보</h3>
<p>장고에서 <code>order_by</code> 를 사용하면 모델의 쿼리셋을 특정 필드를 기준으로 정렬한다.</p>
<p>하지만 정렬하고자 하는 모델과 M:N 관계를 맺는 모델의 many-to-many 필드를 기준으로 삼는 경우 정렬 후 중복된 데이터가 생길 수 있다.
(M:N이기 때문에) 여러 객체와 동시에 연결이 되어 있는 경우 어떤 객체를 기준으로 정렬을 해야할 지 알 수 없기에 해당 객체 각각에 대한 데이터를 모두 만들어 정렬하기 때문이다.</p>
<p>이러한 문제를 해결하기 위해 Subquery를 사용하여 m2m 관계인 모델의 객체 중 원하는 객체의 정보만 담은 annotate 필드를 만들었고 최종적으로 이 값을 기준으로 <code>order_by</code> 메서드를 실행하여 중복된 데이터를 제거하는데 성공했다.</p>
<h3 id="느낀점">느낀점</h3>
<ol>
<li>다양한 방법을 계속 떠올리자. 문제의 원인이 처음에 생각했던 원인 중에 있을 것이라고 단정해서 결국 문제를 해결하는 시간이 훨씬 길어졌다.</li>
<li>스택오버플로우는 정답이 아니다. 아이디어만 얻고 문서를 통하거나 다른 방법으로 내가 얻은 아이디어가 맞는지 검증을 거쳐야 한다. 항상 결국 모자란 건 시간이기 때문에 쉽지는 않겠지만, 스택오버플로우나 구글링을 통해 얻은 지식이 사실이라고 단정하는 생각은 경계해야 한다는 생각이 들었다.</li>
</ol>
<h3 id="추후-액션">추후 액션</h3>
<p>공부하고나면 정리 후 링크로 연결해 둘 생각이다.</br></p>
<ol>
<li>count와 len의 차이에 대해서 알아보자. 프론트에서 쿼리셋의 count를 찍었을 때와 for 루프를 돌 때 실제 쿼리셋의 객체 갯수가 달랐던 점, for문 후에 count를 다시 찍어보면 값이 바뀌어 있던 점이 제일 혼란스러웠던 부분이다. 아마도 count는 실제 데이터를 로드하지 않고, for 루프를 돌 때 실제로 쿼리가 실행되면서 캐싱되는 과정을 놓쳐 혼란스러웠던 것 같다. 이 부분을 다시 면밀하게 공부해보자.</li>
</ol>
<p>문제를 겪고 해결한 과정을 조금 더 자세하게 적어보고자 한다.</p>
<hr>
<p>개발을 하던 도중 의사 모델의 쿼리셋을 정렬해야 할 일이 생겼다.
정렬 기준은 의사 모델과 M:N 관계로 결합된 <code>Specialty</code> 모델의 <code>name</code> 필드.</p>
<p>한참 뒤에 알았지만 수시간 동안 계속된 삽질의 시작은 큰 고민 없이 작성한 아래의 정렬 코드였다.
<code>sorted_doctors = doctors.order_by(&#39;specialties&#39;, &#39;name&#39;)</code></p>
<p>그리고 나서 프론트 작업을 하는데 웬걸, 일부 데이터가 중복으로 포함되어 있어 실제 데이터의 수보다 더 많은 데이터가 렌더링되고 있었다.
의심되는 부분이 두 가지가 있었다.</p>
<ol>
<li>병원의 의사 데이터 설정이 잘못되었다.</li>
<li>프론트 코드 어딘가에서 잘못된 로직 등의 이유로 데이터가 복사되고 있다.</br>

</li>
</ol>
<p><strong>병원의 의사 데이터 설정이 잘못되었다.</strong>
처음에는 병원의 의사 데이터가 잘못(중복되어) 설정되어 있다고 생각했다. 하지만 그것은 나의 바람이었을뿐. 어드민을 통해 확인한 결과 금방 데이터 설정에는 문제가 없음을 알 수 있었다.</p>
<p>다른 가능성을 생각하지 못하고 두 가지 의심 중 하나가 원인이라고 생각하고 있었기에 이후 2번 의심이 원인이라고 확정하게 된다....</p>
<p><strong>프론트 코드 어딘가에서 잘못된 로직 등의 이유로 데이터가 복사되고 있다.</strong></p>
<p>그 다음으로는 프론트에서 실수로 데이터를 복사하는 부분이 있을거라고 의심하고 한참을 뒤져보았다.</p>
<p>원인을 알 수 없었고, 오히려 이상한 점은 데이터에 <code>.count</code> 를 찍어보면 정상적인 데이터의 갯수가 나오고 있다는 점이었다.</p>
<p>그러니까 데이터의 갯수는 문제가 없다고 나오는데 해당 데이터로 for 루프를 돌리면 실제 데이터 갯수 + 중복된 데이터 갯수 만큼 돌아가는 것이었다.</p>
<p>정말 알쏭달쏭한 문제였다. 또 장고와 (특히) 장고 템플릿에 익숙하지 않았기에 내가 모르는 무언가가 작용하여 데이터가 증가했다고 생각했고, 구글링과 장고 독스를 뒤지며 해결책을 찾으려고 애썼다.
</br></p>
<p>결국 한참을 고민하다가, 어쨌든 정렬이 된 데이터임에도 쿼리셋에 중복 데이터의 인덱스가 서로 달랐다는 점이 의아해서 정렬코드를 다시 확인해보았다. 그리고 구글링을 하기 시작했고 나와 같은 문제를 겪고 있다는 글과 문제의 원인을 발견했다.</p>
<p><img src="https://images.velog.io/images/mask_vvs/post/d60b1480-6130-4e0a-bd55-4278f45a3423/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-02-24%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.17.07.png" alt="질문"></p>
<p><img src="https://images.velog.io/images/mask_vvs/post/f2b1a94f-60ba-4f94-9a82-6b6c38f82a64/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-02-24%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.18.18.png" alt="답변"></p>
<p>(출처 <a href="https://stackoverflow.com/questions/23600299/order-by-on-many-to-many-field-results-in-duplicate-entries-in-queryset">https://stackoverflow.com/questions/23600299/order-by-on-many-to-many-field-results-in-duplicate-entries-in-queryset</a>)</p>
<p>그리고 나서 어드민을 통해 다시 데이터를 확인해보니 이제서야 원인이 분명해졌다. 중복으로 데이터가 생성된 의사 객체의 경우 연결된 Specialty가 2개였다.</p>
<p>그 이후로는 해야할 것이 분명해서 독스를 뒤지기 시작했다.</p>
<ol>
<li>우선 annotate로 중복된 값이 없는 필드를 만들고 이 값을 기준으로 정렬해줘야겠다고 생각했다.
 <code>doctors.(first_specialty=&quot;&quot;).order_by(&#39;first_specialty&#39;)</code> <br></li>
<li>그리고 나서는 <code>first_specialty</code> 에 넣어줄 값을 만들어야 했는데, 연결된 <code>Specialty</code> 객체가 몇 개이든 간에 내가 원하는 조건을 만족하는 객체의 이름을 넣어주면 되었다.
찾아보니 장고의 Subquery 메서드를 사용하면 쿼리셋 안에서 쿼리를 또 할 수가 있었고, annotate와 함께 사용되는 경우가 많은 듯 하였다.</li>
</ol>
<p>이와 관련한 좋은 예제가 독스에 있어 기록한다.</p>
<pre><code>from django.db.models import OuterRef, Subquery
newest = Comment.objects.filter(post=OuterRef(&#39;pk&#39;)).order_by(&#39;-created_at&#39;)
Post.objects.annotate(newest_commenter_email=Subquery(newest.values(&#39;email&#39;)[:1]))</code></pre><p>나의 코드는 이렇게 수정되었다.</p>
<pre><code>doctors.annotate(
    first_specialty = Subquery(
        Specialty.objects.filter(doctors__id=OuterRef(&#39;id&#39;)).values(&#39;name&#39;)[:1]
    )
).order_by(&#39;first_specialty&#39;)</code></pre><p>여기서 OuterQuery는 Subquery의 바깥 쿼리의 필드를 참조할 수 있게 해준다. 그러니까 위의 코드에서는 OuterRef(&#39;id&#39;)를 통해 의사 객체의 id 값을 참조할 수 있는 것이다. 작성한 서브쿼리를 통해 첫번째 Specialty 객체의 name을 first_specialty 필드에 저장할 수 있게 되었고, 이 값을 기준으로 정렬을 하니 중복되었던 데이터가 없어졌다.</p>
<hr>
<h2 id="출처">출처</h2>
<p><a href="https://stackoverflow.com/questions/23600299/order-by-on-many-to-many-field-results-in-duplicate-entries-in-queryset">스택오버플로우 질문</a>
<a href="https://docs.djangoproject.com/en/3.2/ref/models/expressions/#:~:text=Subquery()%20expressions%C2%B6">Django docs</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2019 kakao blind recruitment : 무지의 먹방 라이브
]]></title>
            <link>https://velog.io/@mask_vvs/2019-kakao-blind-recruitment-%EB%AC%B4%EC%A7%80%EC%9D%98-%EB%A8%B9%EB%B0%A9-%EB%9D%BC%EC%9D%B4%EB%B8%8C</link>
            <guid>https://velog.io/@mask_vvs/2019-kakao-blind-recruitment-%EB%AC%B4%EC%A7%80%EC%9D%98-%EB%A8%B9%EB%B0%A9-%EB%9D%BC%EC%9D%B4%EB%B8%8C</guid>
            <pubDate>Mon, 28 Jun 2021 18:51:17 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<p><a href="https://programmers.co.kr/learn/courses/30/lessons/42891#">https://programmers.co.kr/learn/courses/30/lessons/42891#</a></p>
<p>평소 식욕이 왕성한 무지는 자신의 재능을 뽐내고 싶어 졌고 고민 끝에 카카오 TV 라이브로 방송을 하기로 마음먹었다.</p>
<p>그냥 먹방을 하면 다른 방송과 차별성이 없기 때문에 무지는 아래와 같이 독특한 방식을 생각해냈다.</p>
<p>회전판에 먹어야 할 N 개의 음식이 있다.
각 음식에는 1부터 N 까지 번호가 붙어있으며, 각 음식을 섭취하는데 일정 시간이 소요된다.
무지는 다음과 같은 방법으로 음식을 섭취한다.</p>
<p>무지는 1번 음식부터 먹기 시작하며, 회전판은 번호가 증가하는 순서대로 음식을 무지 앞으로 가져다 놓는다.
마지막 번호의 음식을 섭취한 후에는 회전판에 의해 다시 1번 음식이 무지 앞으로 온다.
무지는 음식 하나를 1초 동안 섭취한 후 남은 음식은 그대로 두고, 다음 음식을 섭취한다.
다음 음식이란, 아직 남은 음식 중 다음으로 섭취해야 할 가장 가까운 번호의 음식을 말한다.
회전판이 다음 음식을 무지 앞으로 가져오는데 걸리는 시간은 없다고 가정한다.
무지가 먹방을 시작한 지 K 초 후에 네트워크 장애로 인해 방송이 잠시 중단되었다.
무지는 네트워크 정상화 후 다시 방송을 이어갈 때, 몇 번 음식부터 섭취해야 하는지를 알고자 한다.
각 음식을 모두 먹는데 필요한 시간이 담겨있는 배열 food_times, 네트워크 장애가 발생한 시간 K 초가 매개변수로 주어질 때 몇 번 음식부터 다시 섭취하면 되는지 return 하도록 solution 함수를 완성하라.</p>
<p><strong>제한사항</strong></p>
<ul>
<li>food_times 는 각 음식을 모두 먹는데 필요한 시간이 음식의 번호 순서대로 들어있는 배열이다.</li>
<li>k 는 방송이 중단된 시간을 나타낸다.</li>
<li>만약 더 섭취해야 할 음식이 없다면 -1을 반환하면 된다.</li>
</ul>
<p><strong>정확성 테스트 제한 사항</strong></p>
<ul>
<li>food_times 의 길이는 1 이상 2,000 이하이다.</li>
<li>food_times 의 원소는 1 이상 1,000 이하의 자연수이다.</li>
<li>k는 1 이상 2,000,000 이하의 자연수이다.</li>
</ul>
<p><strong>효율성 테스트 제한 사항</strong></p>
<ul>
<li>food_times 의 길이는 1 이상 200,000 이하이다.</li>
<li>food_times 의 원소는 1 이상 100,000,000 이하의 자연수이다.</li>
<li>k는 1 이상 2 x 10^13 이하의 자연수이다.</li>
</ul>
<p><strong>입출력 예</strong></p>
<table>
<thead>
<tr>
<th align="center">food_times</th>
<th align="center">k</th>
<th align="center">result</th>
</tr>
</thead>
<tbody><tr>
<td align="center">[3, 1, 2]</td>
<td align="center">5</td>
<td align="center">1</td>
</tr>
</tbody></table>
<p><strong>입출력 예 설명</strong>
입출력 예 #1</p>
<ul>
<li>0~1초 동안에 1번 음식을 섭취한다. 남은 시간은 [2,1,2] 이다.</li>
<li>1~2초 동안 2번 음식을 섭취한다. 남은 시간은 [2,0,2] 이다.</li>
<li>2~3초 동안 3번 음식을 섭취한다. 남은 시간은 [2,0,1] 이다.</li>
<li>3~4초 동안 1번 음식을 섭취한다. 남은 시간은 [1,0,1] 이다.</li>
<li>4~5초 동안 (2번 음식은 다 먹었으므로) 3번 음식을 섭취한다. 남은 시간은 [1,0,0] 이다.</li>
<li>5초에서 네트워크 장애가 발생했다. 1번 음식을 섭취해야 할 때 중단되었으므로, 장애 복구 후에 1번 음식부터 다시 먹기 시작하면 된다<h1 id="풀이-코드">풀이 코드</h1>
</li>
</ul>
<pre><code class="language-python">def solution(food_times, k):
    from collections import deque
    if sum(food_times) &lt;= k:
        return -1

    food_times = [(i+1, v) for i, v in enumerate(food_times)]
    food_times = deque(sorted(food_times, key=lambda x: x[1]))
    past_time = 0
    while k:
        temp_v = food_times[0][1] - past_time
        amount = temp_v * len(food_times)
        if amount &lt;= k and len(food_times) &gt; 1:
            k -= amount
            past_time += temp_v
            food_times.popleft()
        else:
            k  = k%len(food_times)
            break
    food_times = sorted(list(food_times), key=lambda x: x[0])
    return food_times[k][0]</code></pre>
<h1 id="해설">해설</h1>
<h3 id="1">1.</h3>
<p>문제를 읽었을 때 제일 먼저 큐를 이용해서 푸는 방법을 떠올렸다.</p>
<ol>
<li>처음 음식의 번호와 그 음식을 섭취하는데 소요되는 시간을 튜플로 만들어 큐에 저장하고</li>
<li>네트워크 장애가 발생하기 전 매 초마다 음식을 꺼내서 남은 시간을 차감하고, 아직 남아있다면 다시 큐에 넣는다.</li>
<li>네트워크 장애가 끝난 후(for문의 종료)에도 큐에 남아 있는 원소가 있다면 음식을 다 먹지 못한 것이므로 제일 앞의 원소를 뽑아서 인덱스를 반환한다.</li>
</ol>
<p>코드로 구현한 바는 다음과 같다.</p>
<pre><code class="language-python">def solution(food_times, k):
    from collections import deque
    q = deque()
    answer = -1

    for i, v in enumerate(food_times):
        q.append((i+1, v))

    for i in range(1, k+1):
        if not q:
            break
        now_i, now_v = q.popleft()
        if now_v-1 &gt; 0:
            q.append((now_i, now_v-1))

    if q:
        answer = q.popleft()[0]
    return answer</code></pre>
<p>인덱스를 수정하면서 런타임 에러를 잡고나니 정확도 100%는 어렵지 않게 달성할 수 있었다.
하지만 이렇게 풀게 되면 k의 값만큼 for 문을 돌게 되므로 k의 값이 최대 2x10^13인 효율성 테스트는 당연히 통과하지 못한다.</p>
<h3 id="2">2.</h3>
<p>효율성 테스트를 통과하기 위해서는 수학적으로 계산할 방법을 찾아야 하는 듯 싶었다.</p>
<p>한동안 생각한 후에 <strong>k의 값을 줄이면 되지 않을까</strong> 생각했다.</p>
<p><strong>key point:</strong>
<em>음식의 수가 n개라고 했을 때, n초 후에는 food_times 리스트에 있는 모든 음식의 섭취 시간이 1씩 줄어들게 되고, 다시 첫번째 음식을 먹을 차례가 된다.</em></p>
<p>이를 활용하여 다음과 같은 로직을 생각했다.</p>
<ol>
<li>먼저 모든 음식을 섭취하는데 소요하는 시간이 k보다 작거나 같을 경우에는 네트워크 장애 이후 시점에서 더이상 먹을 음식이 없다는 것을 의미하므로 바로 -1을 리턴하고 함수를 끝낸다.</li>
<li>food_times 리스트를 인덱스를 포함한 튜플의 리스트로 재할당하고, 시간을 기준으로 정렬한다.</li>
<li>가장 섭취 소요 시간이 적은 (첫번째 원소의 시간 * 남은 음식의 갯수)을 계산하고, 이 값을 amount라고 한다.</li>
<li>amount가 k보다 작아지는 시점이 되면, 더 이상 food_lists의 요소를 삭제할 수 없게 된다. 따라서 남은 음식의 소요 시간을 고려하지 않고 바로 k의 값을 간략화 시켜주면 된다. 이를 위해 k를 food_times의 길이로 모듈러 연산을 해주고, 남은 리스트에서 계산된 값의 인덱스를 갖는 음식이 최종적으로 네트워크 장애 이후 먹어야 할 음식이 된다.</li>
</ol>
<p>코드로 구현한 바는 다음과 같다.</p>
<pre><code class="language-python">def solution(food_times, k):
    if sum(food_times) &lt;= k:
        return -1

    food_times = [(i+1, v) for i, v in enumerate(food_times)]
    food_times.sort(key=lambda x : x[1])
    while k:
        temp_v = food_times[0][1]
        amount = temp_v * len(food_times)
        if amount &lt;= k and len(food_times) &gt; 1:
            k -= amount
            food_times = [(i, v-temp_v) for i, v in food_times][1:]
        else:
            k  = k%len(food_times)
            break

    return food_times[k][0]</code></pre>
<p>테스트 케이스를 돌려보니 효율성 테스트 이전에 정확도 테스트에서 일부를 제외하곤 대부분의 결과가 실패였다. </p>
<p>원인을 알 수 없어 로직도 살펴보고, 실패하는 테스트 케이스를 만들어보면서 왜 틀릴까 수도 없이 점검하다가 결국 원인을 찾았다!</p>
<p>바로 <code>return</code> 부분이 문제였다. 나는 food_times 리스트를 그대로 정렬하여 사용하다보니 음식의 순서가 뒤죽박죽 섞여서, 충분히 작아진 k의 값을 이용하여 다음 차례가 될 음식을 리턴해야 하는 부분에서 잘못된 음식을 리턴하고 있던 것이다. </p>
<p>따라서 <code>return</code> 문 이전에 food_times 리스트 안에 있는 각 요소의 첫번째 원소(음식의 번호)를 기준으로 정렬하는 코드를 추가했더니 정확도 테스트를 통과했다.</p>
<pre><code class="language-python">def solution(food_times, k):
    if sum(food_times) &lt;= k:
        return -1

    food_times = [(i+1, v) for i, v in enumerate(food_times)]
    food_times.sort(key=lambda x : x[1])
    while k:
        temp_v = food_times[0][1]
        amount = temp_v * len(food_times)
        if amount &lt;= k and len(food_times) &gt; 1:
            k -= amount
            food_times = [(i, v-temp_v) for i, v in food_times[1:]]
        else:
            k  = k%len(food_times)
            break
    food_times.sort(key=lambda x: x[0])
    return food_times[k][0]</code></pre>
<p>하지만 한 경우를 제외하고 효율성 테스트에서 모두 시간초과가 떠버렸다..</p>
<p>k의 값을 많이 줄였다고 생각했으나 인풋이 200,000개이고 각각의 값이 1~200,000인 극단적인 케이스에서는 while 문이 20만번 돌고 그 안에서 food_times의 값을 재할당하면서 for문이 돌기 때문에 얼추 20만+(20만-1)/2번, 약 200억 번의 루프가 돌게 되는 셈이다.</p>
<p>결국, <strong>핵심은 amount를 계산하는데 필요한 음식을 얻기 위해 for문이 아닌 다른 방식을 사용하는 것이었다.</strong> 이를 위해 매 번 가장 적은 소요 시간을 가진 음식을 트래킹해주어야 했고, 동시에 이미 소요된 시간을 함께 고려해주어야 했다. </p>
<p>이를 해결하기 위해 내가 짠 코드는 다음과 같다.</p>
<pre><code class="language-python">def solution(food_times, k):
    if sum(food_times) &lt;= k:
        return -1

    food_times = [(i+1, v) for i, v in enumerate(food_times)]
    food_times.sort(key=lambda x : x[1])
    past_time = 0
    while k:
        temp_v = food_times[0][1] - past_time
        amount = temp_v * len(food_times)
        if amount &lt;= k and len(food_times) &gt; 1:
            k -= amount
            past_time += temp_v
            del food_times[0]
        else:
            k  = k%len(food_times)
            break
    food_times.sort(key=lambda x: x[0])
    return food_times[k][0]</code></pre>
<p>하지만 효율성 테스트 케이스 하나만 더 통과될 뿐 여전히 시간초과가 발생했다.
코드를 한참을 들여다보다가 리스트의 0번째 요소를 삭제하는 비용이 무척 크다는 생각이 들었다. 파이썬에서는 리스트가 &#39;배열 + 링크드 리스트&#39;의 구조라고 알고 있는데, <code>pop()</code> 연산이 배열에서 원소를 삭제하는 것처럼 진행될 수도 있다는 생각이 들어 <code>deque</code>를 사용했다.</p>
<pre><code class="language-python">def solution(food_times, k):
    from collections import deque
    if sum(food_times) &lt;= k:
        return -1

    food_times = [(i+1, v) for i, v in enumerate(food_times)]
    food_times = deque(sorted(food_times, key=lambda x: x[1]))
    past_time = 0
    while k:
        temp_v = food_times[0][1] - past_time
        amount = temp_v * len(food_times)
        if amount &lt;= k and len(food_times) &gt; 1:
            k -= amount
            past_time += temp_v
            food_times.popleft()
        else:
            k  = k%len(food_times)
            break
    food_times = sorted(list(food_times), key=lambda x: x[0])
    return food_times[k][0]</code></pre>
<p>최종적으로 해당 코드로 문제를 풀 수 있었다. 더 깔끔하게 코드를 줄일 수 있겠지만 너무 지쳐서 여기까지 하기로 했다. 내일 다른 사람들의 풀이를 찾아봐야지. 
배열보다 queue에서 원소 삭제의 비용이 훨씬 적다는 것을 이미 너무 잘 알고 있었는데, 문제에서 해당 개념을 떠올리고 적용하기까지 너무 오랜 시간이 걸렸던 문제였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 해시 : 전화번호 목록]]></title>
            <link>https://velog.io/@mask_vvs/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%95%B4%EC%8B%9C-%EC%A0%84%ED%99%94%EB%B2%88%ED%98%B8-%EB%AA%A9%EB%A1%9D</link>
            <guid>https://velog.io/@mask_vvs/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%95%B4%EC%8B%9C-%EC%A0%84%ED%99%94%EB%B2%88%ED%98%B8-%EB%AA%A9%EB%A1%9D</guid>
            <pubDate>Thu, 24 Jun 2021 09:23:58 GMT</pubDate>
            <description><![CDATA[<p>키포인트: 문자열 리스트 정렬!</p>
<pre><code class="language-python">def solution(phone_book):
    answer = True
    phone_book.sort()
    l = len(phone_book)
    for i in range(0, l-1):
        if len(phone_book[i]) &lt; len(phone_book[i+1]) and phone_book[i+1].startswith(phone_book[i]):
            return False

        # for j in range(i+1, l):
        #     if phone_book[i][0] != phone_book[j][0]:
        #         break
        #     if phone_book[j].startswith(phone_book[i]):
        #         return False
    return answer</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버 사이드 렌더링 개념 정리]]></title>
            <link>https://velog.io/@mask_vvs/%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@mask_vvs/%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 12 Feb 2021 09:49:21 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<p>본 글은 유튜브 크리에이터 <strong>&#39;드림코딩 by 엘리&#39;</strong>님의 21.02.10일자 영상</p>
<blockquote>
<p><em><a href="https://www.youtube.com/watch?v=iZ9csAfU5Os">서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리)</a></em> </p>
</blockquote>
<p>을 보고 정리한 글임을 밝힌다. 본 글에서는 영상에 나온 개념을 이미지 없이 <strong>정리</strong>한 글이다. 이미지와 함께 더 나은 이해를 바란다면 해당 영상을 보면 좋을 것 같다.</p>
<h2 id="목차">목차</h2>
<ol>
<li>웹의 역사 (SPA 시대까지)</li>
<li>CSR (Client Side Rendering)</li>
<li>SSR (Server Side Rendering)</li>
<li>TTV(Time To View) / TTI (Time To Interact)</li>
<li>SSG (Static Site Generation)</li>
</ol>
<h3 id="웹의-역사">웹의 역사</h3>
<ol>
<li><p>1990년 중반 : static sites
서버에 미리 html 파일들을 만들어두고, 경로에 따라 적절한 html 파일을 렌더링 해주는 방식. 서버와 클라이언트가 html 파일을 주고 받는 것이므로 매번 페이지 전체가 업데이트(즉, 새로고침) 되어 사용성이 좋지 못하다는 단점이 있다.</p>
</li>
<li><p>1996년 : <code>&lt;iframe&gt;</code>
문서 내에서 다른 문서를 도입 할 수 있는 <code>&lt;iframe&gt;</code> 태그의 도입으로 페이지 전체가 아닌, 부분적으로 업데이트 할 수 있게 되었다.</p>
</li>
<li><p>1998년 : XMLHttpRequest API
<code>fetch</code>의 원조 격이라 할 수 있는 XMLHttpRequest API가 개발되었다. 이로 인해 서버에서 html 파일 전체가 아니라, <code>json</code> 포맷으로 필요한 데이터만 받아올 수 있게 되었다. 자바스크립트를 활용해 이렇게 받아온 데이터를 동적으로 html 요소를 만들어 페이지에 업데이트 한다. 새로고침으로 사용성이 저하되었던 문제가 해결되기 시작한 것이다.</p>
</li>
<li><p>2005년 : AJAX(Asynchronous JavaScript and XML)
XMLHttpRequest의 방식이 공식적으로 <code>AJAX</code>라는 이름을 갖게 되며 구글에선 이를 이용하여 gmail, google maps와 같은 웹 어플리케이션을 만든다. 이것이 현재 널리 쓰이고 있는 <strong>SPA (Single Page Application)</strong>이다. SPA는 static sites와는 달리 서버에서 html 파일을 받아오지 않고 사용자가 하나의 페이지에 머무르면서 필요한 데이터만 받아와 부분적으로 업데이트한다. </p>
</li>
</ol>
<h3 id="csr-client-side-rendering">CSR (Client Side Rendering)</h3>
<p>SPA 트렌드와 cpu 성능의 향상, js의 표준화 및 강력해진 커뮤니티를 바탕으로 <code>Angular</code>, <code>React</code>, <code>Vue</code>와 같은 프레임 워크가 등장하게 되고 <strong>클라이언트 사이드 렌더링 시대</strong>로 접어들게 된다.</p>
<p>CSR이란, 쉽게 말해 &#39;서버에서 다 해먹는 것&#39;이라고 한다. 페이지를 부분적으로 업데이트하는 모든 과정이 클라이언트 사이드에서 이루어지는 것이라고 생각하면 될 것 같다. 제일 처음에 서버에서 클라이언트로 보내주는 html 파일에는 다음과 같은 기본적인 구조만이 담겨있다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot; /&gt;
    &lt;meta name=&quot;description&quot;
          content=&quot;Amazing web site&quot; /&gt; 
    &lt;title&gt;App&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script src=&quot;app.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>따라서 처음에는 빈 화면만 보이다가 링크된 <code>app.js</code>를 서버로 받게 되는데 이때, <code>app.js</code>에 어플리케이션에서 필요한 로직뿐만 아니라 어플리케이션을 구동하는 프레임워크 및 라이브러리의 소스 코드들도 모두 포함되어 있다. 
그러다보니 파일 크기가 매우 커져 다운로드 받는 데에 시간이 소요된다는 특성이 있다. 만약 추가로 데이터가 필요하다면 서버로부터 &#39;데이터만 받아온 후에&#39; 소스 코드가 모두 로드되어있는 클라이언트 측에서 동적으로 html 요소를 만들어 최종적으로 사용자가 어플리케이션을 보게 된다.</p>
<p><strong>단점</strong>
이런 CSR 방식의 단점으로는 크게 두 가지를 꼽을 수 있다.</p>
<ol>
<li><p>첫 로딩이 오래 걸린다.
: 위에서 언급했듯이 자바스크립트 파일 하나에 각종 로직과 프레임워크 / 라이브러리 소스 코드를 모두 받아온 후에 데이터로 html 요소를 가공해서 사용자가 보게되다보니 처음 화면을 보기 전까지 시간이 소요된다는 단점이 있다. 보통의 경우는 이 시간이 매우 길진 않겠지만, 짧은 시간이라도 사용자의 경험에는 치명적일 수 있어 중요하게 생각해 볼 요소인 것 같다.</p>
</li>
<li><p>SEO가 썩 좋지 못하다.
: SEO는 Search Engine Optimization의 줄임말로, 검색 엔진에 효과적으로 노출되도록 만드는 작업이라고 할 수 있다. 영상을 보기 전까지는 SEO라는 것의 의미만 알고 있어서 단순하게 &#39;검색에 용이하게 키워드 설정 및 태그 활용만 적절하게 하면 되지 않을까?&#39;라고 생각했는데, <strong>실제 페이지가 로드되는 과정과 깊게 연관되어 있다는 것을 새로 알게 되었다.</strong> 우리가 구글이나 네이버에서 검색을 하게 되면 서버에 등록된 웹사이트들의 html 요소들을 살펴보며 검색어와의 매칭을 확인한다고 한다. 그런데 CSR에서는 처음에 받아오는 <code>html</code> 파일에 거의 정보가 담겨있지 않으므로 당연히 SEO에 불리할 수 밖에 없다.</p>
</li>
</ol>
<h3 id="ssr-server-side-rendering">SSR (Server Side Rendering)</h3>
<p>위에서 다룬 CSR의 문제점을 보완하기 위해 1990년 중반에 활용했던 static sites의 영감을 받은 SSR 즉, <em>서버 사이드 렌더링</em> 이 도입된다. 클라이언트에서 모든 것을 처리하는 방식과 다르게 서버에서 데이터를 가지고 html 파일을 만들고, 이를 동적으로 제어할 수 있는 일부 소스코드와 함께 클라이언트 측으로 보내준다. 그럼 클라이언트 측에서는 잘 만들어진 html 파일을 받아와 바로 사용자에게 보여줄 수 있게 된다.</p>
<p><strong>장점</strong>
CSR의 단점을 보완했기 때문에 다음과 같은 장점을 지닌다.</p>
<ol>
<li>첫번째 페이지 로딩이 빠르다</li>
<li>모든 컨텐츠가 html에 담겨 있으므로 SEO에 좀 더 효율적이다.</li>
</ol>
<p>하지만, CSR의 단점을 보완했다고 해서 SSR이 완벽한 것은 아니다.
<strong>단점</strong></p>
<ol>
<li>블링킹 이슈(blinking issue)
: 결국 SSR의 과정을 살펴보면 static sites와 마찬가지로 사용자의 클릭에 해당하는 html 파일을 다시 서버에서 받아오기 때문에 새로고침이 되어 사용자 경험이 좋지 않을 수 있다.</li>
<li>서버에 과부화가 걸리기 쉽다.
: 렌더링의 많은 과정이 서버 사이드에서 일어나기 때문에 특히 사용자가 많은 제품일수록 서버의 부담이 커진다.</li>
<li><strong>서비스의 동적 활용에 딜레이가 있다. (Need to wait before interacting)</strong>
: 영상을 제작하신 엘리님의 말씀에 따르면 가장 치명적인 단점이라고 한다. 위에서 html 파일과 이를 동적으로 제어할 수 있는 일부 자바스크립트 소스 코드를 함께 클라이언트 측으로 보내준다고 했다. 여기서 중요한 점은 &#39;일부&#39;라는 것이다. 페이지를 사용하는데 필요한 여러 코드가 모두 보내진 것은 아니기 때문에, 어떤 기능을 사용하기 위해서는 서버에서 해당 동작을 위한 자바스크립트 파일을 요청해 다운로드 하는 시간이 필요한 것이다. 그래서 클릭을 하더라도 반응이 없는 경우가 생길 수 있다.</li>
</ol>
<h3 id="ttv와-tti">TTV와 TTI</h3>
<p>SSR 방식의 세번째 단점을 좀 더 잘 이해하기 위해선 <em>Time To View(사용자가 웹 사이트를 볼 수 있는 시점)</em> 와  <em>Time To Interact(사용자가 웹 사이트와 상호작용을 할 수 있는 시점)</em> 라는 개념을 알고 있어야 한다.</p>
<p>먼저, CSR과 SSR을 시간 순서로 분석해보면 다음과 같다.</p>
<p><strong>CSR</strong></p>
<ol>
<li>사이트 접속</li>
<li>서버에서 인덱스 파일을 받아 온다. (이 때는 컨텐츠가 없는 상태라 빈 화면으로 보인다.)</li>
<li>html 파일에 링크 되어 있는 웹사이트에 필요한 모든 로직이 담겨 있는 자바스크립트 파일을 요청한다.</li>
<li>동적으로 html 요소를 생성할 수 있는 웹 어플리케이션 로직(<code>angular</code>, <code>react</code>, <code>vue</code>와 같은 것들)이 담긴 자바스크립트 파일을 가져온다. =&gt; <strong>TTV, TTI</strong></li>
</ol>
<p>이 과정 속에서 TTV와 TTI는 <strong>동시에 4번이다</strong>.  왜냐하면 html 요소가 생성되어 사용자가 볼 수 있는 컨텐츠가 생기며(TTV), 웹 사이트에 필요한 로직 역시 미리 로드 되어 있었기 때문에(3번) 렌더링 된 페이지에서 동적으로 활용이 가능해지기 때문이다(TTI).</p>
<p>따라서 CSR을 제작한다면, 필요한 자바스크립트 코드를 번들링해서 한 번에 보내주는 4번 단계에서 어떻게 하면 자바스크립트 코드를 효율적으로 분할해서 첫번째 로딩에 필요한 필수적인 코드만을 보낼 수 있을지 고민할 필요가 있다.
(CSR에서 <code>app.js</code>를 다운 받는다고 해서 모든 자바스크립트 코드를 한 번에 받아오는 것이라고 생각했는데 분할을 언급하는 것으로 보아 &#39;마지막에 자바스크립트 코드를 받아온다&#39;라고 이해하는게 더 정확한 것 같다.)
<br></p>
<p><strong>SSR</strong></p>
<ol>
<li>사이트 접속</li>
<li>서버에서 이미 잘 만들어진 인덱스 파일을 받아온다. =&gt; <strong>TTV</strong></li>
<li>자바스크립트 파일을 서버에서 받아온다.</li>
</ol>
<p>CSR과 다르게 SSR에서는 TTV의 시점이 2번 단계이다. 서버에서 이미 사용자가 볼 수 있는 html 파일을 만들어 보내주었기 때문이다. 하지만 이 단계가 <strong>TTI</strong>인 것은 아니다. 아직 인터랙션을 수행할 자바스크립트 코드가 없기 때문이다. 인터랙션은 자바스크립트 파일을 서버에서 받아온 이후부터 가능하다(3번 이후).
즉, SSR은 TTV와 TTI 사이의 공백 기간이 꽤 긴 편이다.</p>
<p>따라서 SSR의 경우에는 VVT와 VVI 사이의 공백을 줄이는 것이 핵심이다. 또한 블링크 이슈와 관련하여 매끄러운 UI와 UX를 제공할 수 있을지 고민하는 것도 중요하다.</p>
<h3 id="ssg-static-site-generation">SSG (Static Site Generation)</h3>
<p>요즘은 CSR이나 SSR만을 선택하지 않고 SSG라는 것을 활용하기도 한다. SSG의 특성은 무엇일까? 예를 들어, 리액트는 CSR에 특화된 라이브러리이지만 <code>Gatsby</code>라는 라이브러리와 함께 사용한다면 리액트 웹어플리케이션의 웹페이지를 정적으로 미리 생성해서 서버에 배포해 놓을 수가 있다. 잘 이해한게 맞나 싶지만, TTV까지 시간이 걸린다는 CSR의 문제점을 보완할 수 있다. </p>
<p>이렇게 만들어진 웹사이트들이 모두 정적인 것은 아니다. 서버에서 추가적으로 데이터를 받아오거나, 동적으로 처리해야 되는 로직이 있다면 JS 파일을 함께 가지고 있을 수 있기 때문에 동적인 요소도 충분히 추가 및 활용할 수 있다고 한다. (=&gt; 이 부분은 잘 이해가 되지 않아 나중에 따로 조사가 필요할 것 같다. 지금 드는 생각으로는 정적으로 html 파일과 이 페이지에 활용되는 JS 파일을 함께 만들어두어 관리하기 때문에 인터랙션이 가능하다고 말하는 것 같다)</p>
<p>개츠비 외에도 리액트와 함께<code>Next.js</code>도 많이 사용한다고 한다. <code>Next.js</code>는 강력한 SSR을 지원하는 라이브러리였지만 SSG도 지원하고 CSR를 적절히 섞어 목적에 맞게 활용하도록 지원한다고 한다.</p>
<h3 id="마무리하며">마무리하며</h3>
<p>리액트를 공부하며 자주 접하던 용어들을 이번 기회로 정리할 수 있었다. 물론 각 개념을 깊게 이해한 것은 아니지만, 지금 단계에선 이 정도로 알아두는 것도 큰 도움이 될 것이라 생각한다. </p>
<p>SPA의 역사와 CSR과 SSR의 차이점을 정리하며 <code>trade-off</code>를 다시 한 번 생각하게 되었다. 사회와 기술은 발전할 때 마다 그 부작용이 있기 마련이지만 전체적으로 보면 이전 보다 나아지는 방향을 향하고 있다고 생각했다. 자료구조를 공부하며 어떠한 선택은 절대적으로 나은 것이 아니라 선택의 문제라는 <code>trade-off</code> 개념을 자주 접하며 &#39;시간 상으로 나중에 등장한 것이 무조건 더 좋은 것은 아니다&#39;라는 생각을 자주 했는데 이번 CSR과 SSR의 차이가 딱 그런 것 같다는 생각을 했다. 특히 나는 SSR이라는 용어가 더 익숙해 &#39;현재 프론트엔드의 가장 발전된 방식이 SSR인가 보다.&#39;라는 오해를 하고 있었는데 잘못 생각하고 있다는 것을 아는 기회가 되었다.</p>
<p>항상 &#39;구현해 내는 것&#39;에 만족하지 않고 &#39;특성에 맞게 <strong>잘</strong> 구현&#39;하고자 하는데 &#39;사이트를 이용하기 위해 필요한 데이터들을 어떤 방식으로 가공하는가&#39;라는 중요한 특성 하나를 알게 되어 만족스럽다.</p>
]]></description>
        </item>
    </channel>
</rss>