<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>taemin-jang.log</title>
        <link>https://velog.io/</link>
        <description>하루하루 공부한 내용 기록하기</description>
        <lastBuildDate>Tue, 29 Oct 2024 13:12:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>taemin-jang.log</title>
            <url>https://velog.velcdn.com/images/taemin-jang/profile/b304e843-0c4f-49a9-b188-ddbb70b92ec5/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. taemin-jang.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/taemin-jang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Safari 브라우저 window.open 팝업 차단 이슈]]></title>
            <link>https://velog.io/@taemin-jang/PASS-%EC%9D%B8%EC%A6%9D</link>
            <guid>https://velog.io/@taemin-jang/PASS-%EC%9D%B8%EC%A6%9D</guid>
            <pubDate>Tue, 29 Oct 2024 13:12:42 GMT</pubDate>
            <description><![CDATA[<p>회사 프로젝트 오픈 베타를 앞두고 테스트 기간 중에 중요도가 높은 이슈가 발생했다.</p>
<blockquote>
<p>Safari 브라우저에서 PASS 본인인증 팝업이 뜨지 않는 이슈였다.</p>
</blockquote>
<p>중요도가 높았던 이유는 회원가입이나 요금제를 신청하거나 마이페이지에서 수정할 때 본인인증이 필수 프로세스이며 아이폰, 맥북에서 Safari를 주로 사용하는 사용자들도 있기 때문이다.</p>
<p>Safari에서 본인인증 버튼을 클릭했을 때 <code>팝업이 차단되었다</code>는 문구가 떴다. 맥북에서는 해당 문구를 확인하고 차단된 팝업을 허용하면 되지만, 아이폰에서는 버튼을 클릭했을 때 아무런 동작을 하지 않아서 사용자가 왜 안되는지 알 수 없는 상태였다.</p>
<h3 id="원인-파악">원인 파악</h3>
<p>PASS 본인인증 프로세스는 간단하게 설명하면 다음처럼 진행된다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/5d6dd7c8-1845-4134-ab8a-bb0e1c3ea1d9/image.png" alt=""></p>
<ol>
<li>클라이언트로부터 이벤트 발생 시 서버에게 본인인증 html 파일을 요청</li>
<li>전달받은 html 파일을 팝업창으로 띄움</li>
<li>본인인증을 마친 클라이언트의 결과값을 콜백으로 전달받아 결과에 맞게 처리</li>
</ol>
<p>대부분의 복잡한 인증 로직은 서버에서 처리해주므로 프론트에서 처리할건 html 파일을 팝업창으로 보여주는 것 뿐이다.</p>
<p>팝업창을 띄우는 코드는 다음과 같다.</p>
<pre><code class="language-js">const openPassPopup = async (action = &#39;signup&#39;) =&gt; {
    try {
        const res = await fetch(&#39;서버 API&#39;)
        return new Promise((resolve, reject) =&gt; {
            if (res.code === 200 &amp;&amp; res.data) {
                const listener = (event) =&gt; {
                    window.removeEventListener(&#39;message&#39;, listener)
                    if (event?.data?.message === &#39;PassPopupClosed&#39;) {
                        resolve(event.data.result)
                    } else {
                        reject(null)
                    }
                }
                window.removeEventListener(&#39;message&#39;, listener)
                window.addEventListener(&#39;message&#39;, listener, false)
                const authWindow = window.open(
                    &#39;&#39;,
                    &#39;_blank&#39;,
                    &#39;width=500, height=550, top=0, left=100, fullscreen=no, menubar=no, status=no, toolbar=no, titlebar=yes, location=no, scrollbar=no&#39;
                )
                if (authWindow) {
                    authWindow.document.open()
                    authWindow.document.write(data)
                    authWindow.document.close()
                }
            } else {
                reject(null)
            }
        })
    } catch (error) {
        throw error
    }
}</code></pre>
<p>위 코드를 간단하게 하면 아래 코드처럼 동작하고, 이는 사파리 브라우저에서 정상적으로 동작하지 않게 된다. </p>
<pre><code class="language-js">const openPassPopup = async (action = &#39;signup&#39;) =&gt; {
  await 비동기
  window.open(&#39;&#39;, &#39;_blank&#39;) // 동작 안함
  // window.open(url, target, windowFeatures)
}</code></pre>
<p>그 이유는 사파리 브라우저에서는 비동기 호출 후 이루어지는 <code>window.open()</code> 호출을 차단하기 때문이다.</p>
<blockquote>
<p>사파리에서는 사용자 클릭 이벤트에서만 window.open을 통한 팝업을 허용하며, 보안 정책 중 하나로 script의 팝업 호출을 제한하고 있다.</p>
</blockquote>
<h3 id="해결-방법">해결 방법</h3>
<p>비동기 호출 하기 전에 <code>window.open</code>을 호출하고 전달 받은 html 파일을 보여주는 방식으로 해결했다.</p>
<pre><code class="language-js">const openPassPopup = async (action = &#39;signup&#39;) =&gt; {
    try {
          const authWindow = window.open(
            &#39;&#39;,
            &#39;_blank&#39;,
            &#39;width=500, height=550, top=0, left=100, fullscreen=no, menubar=no, status=no, toolbar=no, titlebar=yes, location=no, scrollbar=no&#39;
        )
        const res = await fetch(&#39;서버 API&#39;)
        return new Promise((resolve, reject) =&gt; {
            if (res.code === 200 &amp;&amp; res.data) {
                ...

                if (authWindow) {
                    authWindow.document.open()
                    authWindow.document.write(data)
                    authWindow.document.close()
                }
            } else {
                reject(null)
            }
        })
    } catch (error) {
        throw error
    }
}</code></pre>
<p>이렇게 팝업창을 미리 띄우고, api 호출로 받아온 데이터를 받아온다.</p>
<p>띄운 팝업창에 <code>document.open()</code>으로 기존에 있던 페이지 내용을 지우고 <code>document.write(data)</code>로 받아온 html 파일로 내용을 작성한 뒤 <code>document.close()</code>로 페이지를 완성함으로써 추가로 write를 할 수 없는 상태이다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/e8ecb18c-abee-4b5a-872f-21ce4cfcf8a9/image.png" alt=""></p>
<h3 id="추가-이슈-사항들">추가 이슈 사항들</h3>
<p><code>window.open</code>할 때 target을 <code>&#39;_blank&#39;</code>로 작성하고 팝업을 띄우면, 버튼을 클릭할 때마다 중복된 팝업이 계속 뜨는 버그가 발생하게 된다.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/open#reuse_existing_windows_and_avoid_target_blank">MDN에서도 target=&#39;_blank&#39;는 피하는 것을 권고하고 있다.</a></p>
<p>그래서 다음과 같이 수정하여 해결했다.</p>
<pre><code class="language-js">const authWindow = window.open(
    &#39;&#39;,
    &#39;passPopup&#39;,
    &#39;width=500, height=550, top=0, left=100, fullscreen=no, menubar=no, status=no, toolbar=no, titlebar=yes, location=no, scrollbar=no&#39;
)</code></pre>
<p>이렇게 함으로써 중복으로 뜨는 이슈는 해결했지만, 현재 창이 떠있을 때 한 번 더 클릭하면 에러가 발생하게 된다.</p>
<p>이를 해결하기 위해 팝업창이 있으면 닫고 다시 띄우도록 수정해줬다.</p>
<pre><code class="language-js">let authWindow: Window | null

if (authWindow) {
  authWindow.close()
}

authWindow = window.open(
    &#39;&#39;,
    &#39;passPopup&#39;,
    &#39;width=500, height=550, top=0, left=100, fullscreen=no, menubar=no, status=no, toolbar=no, titlebar=yes, location=no, scrollbar=no&#39;
)

...</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[클라우드 및 보안 용어 정리]]></title>
            <link>https://velog.io/@taemin-jang/cloud</link>
            <guid>https://velog.io/@taemin-jang/cloud</guid>
            <pubDate>Thu, 03 Oct 2024 10:20:14 GMT</pubDate>
            <description><![CDATA[<p>현재 회사에서 IDaaS 프로젝트를 진행하고 있습니다. 관련 클라우드 및 보안 용어들이 많아서 이번 기회에 간단히 정리해봤습니다.</p>
<h1 id="idaas란">IDaaS란?</h1>
<p>Identity as a Service의 줄임말인 IDaaS는 SaaS의 한 유형으로 <code>클라우드 기반 중앙 관리 시스템</code>입니다.</p>
<p>IDaaS 서비스의 목적은 <code>사용자의 신원을 확인</code>하여 소프트웨어 애플리케이션, 파일 또는 기타 리소스에 따라 적합한 <code>엑세스 권한을 적시에 제공</code>하는 것입니다.</p>
<blockquote>
<p>클라우드 서비스 모델(<code>SaaS, PaaS, IaaS, FaaS</code>)을 생각하면 이해하기 쉽습니다.</p>
</blockquote>
<p>주로 제공하는 기능으로 다음과 같이 있습니다.</p>
<ul>
<li>IAM(Identity and Access Management)</li>
<li>SSO(Single Sign-On)</li>
<li>MFA(Adaptive Multi-Factor Authentication)</li>
<li>보고 및 분석</li>
<li>솔루션/앱 연동 및 요금 비교</li>
<li>비정상적인 접속 차단</li>
<li>인사 동기화</li>
</ul>
<h2 id="iam">IAM</h2>
<p>ID 및 액세스 관리(identity and access management, IAM)는 올바른 엔티티(개인 또는 장치)가 올바를 리소스(애플리케이션 또는 데이터)를 적시에 원하는 디바이스로 방해 없이 사용할 수 있게 해주는 보안 정책입니다.</p>
<p>IAM은 IT 관리자가 단일 디지털 ID(테넌트 ID)를 각 엔티티(사용자 또는 장치)에 지정하고, 로그인 시 사용자를 인증하며, 지정된 리소스에 접근할 수 있는 권한을 부여하는 과정들을 모니터링 및 관리할 수 있는 시스템과 프로세스로 구성되어 있습니다.</p>
<p>주요 기능</p>
<ul>
<li>사용자 관리: 사용자 계정 생성, 수정, 삭제</li>
<li>접근 권한 관리: 사용자 역할에 따라 특정 리소스에 대한 접근 권한을 설정</li>
<li>정책 관리: 보안 정책 및 규정을 설정하고 관리</li>
<li>감사 및 컴플라이언스: 사용자 활동 및 접근 기록을 추적하여 규정 준수를 지원</li>
</ul>
<blockquote>
<p>IAM의 예시로 AWS IAM이 있으며, AWS 리소스에 대한 접근을 관리할 수 있도록 도와줍니다. </p>
</blockquote>
<h2 id="sso">SSO</h2>
<p>SSO(Single Sign-On) 솔루션은 사용자가 하나의 자격증명으로 필요한 모든 애플리케이션에 접근할 수 있도록하는 것으로, 간편 로그인을 지원합니다.</p>
<p>이를 통해 여러 사용자 이름과 비밀번호를 기억할 필요가 없고, 생산성이 높아지며 원활한 환경을 제공합니다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/a945812d-18e2-4507-a1d1-0ae1464d2b93/image.png" alt=""></p>
<h3 id="idp">IdP</h3>
<p>Identity Provider, ID 공급자(IdP)는 사용자의 디지털 ID를 저장하고 관리하여, 이를 기반으로 다양한 서비스에 대한 접근을 제공하는 시스템입니다.</p>
<p>주요 기능</p>
<ul>
<li>사용자 인증</li>
<li>아이덴티티 관리</li>
<li>SSO 지원</li>
<li>SAML, OAuth, OpenID Connect 지원</li>
</ul>
<blockquote>
<p>대표적인 예시로 Google이 있습니다.</p>
<p>Google은 Google 계정을 통해 다양한 Google 서비스(예: Gmail, Google Drive)에 SSO 기능을 제공합니다.</p>
</blockquote>
<p>IdP가 사용자 인증을 처리한 뒤 SSO는 인증 정보를 가지고 여러 서비스 접근에 사용합니다.</p>
<h4 id="saml">SAML</h4>
<p>SAML(Security Assertion Markup Language)은 인증 정보 제공자와 서비스 제공자 간의 인증 및 인가 데이터를 교환하기 위한 XML 기반의 표준 데이터 포맷이다.</p>
<p>SAML은 인증 정보를 XML 포맷으로 생성하고, 이 XML 데이터를 암호화하여 제 3자에게 노출시키지 않고 전달할 수 있다.</p>
<h4 id="oidc">OIDC</h4>
<p>OIDC(OpenID Connect)는 OAuth 2.0위에 구축된 인증 프로토콜로 Json Web Token(JWT) 표준을 사용하는 완전한 인증 및 권한 부여 프로토콜입니다.</p>
<blockquote>
<p>OIDC는 Authentication(인증)을 위한 기술이고, OAuth는 Authorization(인가)을 위한 기술입니다.</p>
<ul>
<li>인증은 사용자가 누구인지 확인하는 절차</li>
<li>인가는 사용자가 요청한 자원에 대해 권한이 있는지 확인하는 절차</li>
</ul>
</blockquote>
<h4 id="ldap">LDAP</h4>
<p>LDAP(Lightweight Directory Access Protocol)은 사용자가 네트워크 상에서 조직, 구성원 등에 대한 데이터를 찾는데 도움이 되는 프로토콜입니다. </p>
<p>LDAP는 경량화된 버전의 디렉터리 서비스 프로토콜로, 효율적이고 빠른 데이터 접근을 제공합니다.</p>
<ul>
<li><p>디렉터리 서비스: LDAP는 사용자, 그룹, 컴퓨터 및 기타 리소스에 대한 정보를 저장하는 데이터베이스 역할을 합니다. 이러한 정보는 계층적 구조로 조직됩니다.</p>
</li>
<li><p>객체와 속성: LDAP 디렉터리는 객체(예: 사용자, 그룹)와 그 객체의 속성(예: 이름, 이메일, 전화번호)으로 구성됩니다. 각 객체는 고유한 DN(Distinguished Name)으로 식별됩니다.</p>
</li>
<li><p>계층 구조: LDAP 디렉터리는 트리 구조로 구성되어 있어, 조직의 구조를 쉽게 표현할 수 있습니다. 예를 들어, 부서별로 그룹화하여 관리할 수 있습니다.</p>
</li>
</ul>
<p>LDAP는 디렉터리 내에서 정보를 효율적으로 검색할 수 있는 기능을 제공하며, 사용자 및 리소스 정보를 추가, 수정, 삭제하는 중앙 집중식 데이터 관리가 가능합니다.</p>
<h2 id="mfa">MFA</h2>
<p>MFA(Multi-Factor Authentication), 다중 인증은 애플리케이션에 접근하기 위해 사용자 이름과 비밀번호의 조합보다 더 안전하게 사용자 신원을 확인하는 방법입니다.</p>
<p>MFA의 한 유형인 2FA는 비밀번호외에 추가 인증 요소를 포함한 인증 프로세스입니다.</p>
<p>2FA 인증 요소에는 다음과 같이 있습니다.</p>
<ul>
<li>SMS 기반</li>
<li>보안 앱 (Google Authenticator)</li>
<li>FIDO (패스키, 암호화 키, 얼굴, 지문 등)</li>
<li>Sound-Proof: 장치에 내장된 마이크에 주변 소음으로 확인하는 방법</li>
<li>QR 코드</li>
</ul>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://www.okta.com/kr/identity-101/idaas/">okta - IDaaS란? IDaas의 특징 및 이점 알아보기</a></li>
<li><a href="https://www.entrust.com/ko/resources/learn/what-is-identity-as-a-service">entrust - IDaaS(Identity as a Service)란 무엇인가요?</a></li>
<li><a href="https://jumpcloud.com/blog/iam-vs-idaas">jumpcloud - IAM vs. IDaaS: What’s the Difference?</a></li>
<li><a href="https://www.cloudflare.com/ko-kr/learning/access-management/what-is-identity-and-access-management/">cloudflare - ID 및 액세스 관리(IAM)란 무엇입니까?</a></li>
<li><a href="https://www.cloudflare.com/ko-kr/learning/access-management/what-is-an-identity-provider/">cloudflare - ID 공급자(IdP)는 무엇입니까?</a></li>
<li><a href="https://www.cloudflare.com/ko-kr/learning/access-management/what-is-sso/">cloudflare - SSO란? | 싱글 사인온의 작동 방식</a></li>
<li><a href="https://www.cloudflare.com/ko-kr/learning/access-management/what-is-multi-factor-authentication/">cloudflare - 다단계 인증(MFA)이란?</a></li>
<li><a href="https://www.ibm.com/kr-ko/products/verify-saas/advanced-authentication">IBM - 어디서나 가능한 다단계 인증(MFA)</a></li>
<li><a href="https://gruuuuu.github.io/security/ssofriends/">호다닥 공부해보는 SSO와 친구들 (SAML, OAuth, OIDC)</a></li>
<li><a href="https://www.okta.com/kr/identity-101/whats-the-difference-between-oauth-openid-connect-and-saml/">okta - OAuth, OpenID Connect, SAML의 특징 및 차이점</a></li>
<li><a href="https://docs.redhat.com/ko/documentation/red_hat_single_sign-on/7.6/html/server_administration_guide/ref-saml-vs-oidc_server_administration_guide">red hat - 10.3. SAML과 OpenID Connect 비교</a></li>
<li><a href="https://www.redhat.com/ko/topics/security/what-is-ldap-authentication">red hat - LDAP(Lightweight Directory Access Protocol) 인증이란?</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP]]></title>
            <link>https://velog.io/@taemin-jang/HTTP</link>
            <guid>https://velog.io/@taemin-jang/HTTP</guid>
            <pubDate>Wed, 11 Sep 2024 13:10:29 GMT</pubDate>
            <description><![CDATA[<h2 id="이번에-학습할-내용">이번에 학습할 내용</h2>
<ul>
<li>메시지가 어떻게 흘러가는가</li>
<li>HTTP 메시지 시작줄, 헤더, 본문</li>
<li>요청과 응답 메시지의 차이</li>
<li>요청 메시지의 메서드들</li>
<li>응답 메시지의 상태 코드들</li>
</ul>
<h2 id="메시지의-흐름">메시지의 흐름</h2>
<p>메시지는 원 서버 방향을 인바운드로 송신하고, 모든 처리가 끝난 뒤에 메시지가 사용자 에이전트 방향인 아웃바운드로 이동한다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/00482796-b222-478a-858e-bd75ba89c539/image.png" alt=""></p>
<p>HTTP 메시지는 요청 메시지 또는 응답 메시지에 관계없이 다운스트림으로 흐른다.</p>
<p>즉, 메시지의 발송자는 수신자의 업스트림이 된다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/5218a68c-b858-4129-8853-d9d2acd49b19/image.png" alt=""></p>
<p>HTTP 메시지는 시작줄, 헤더 블록, 본문으로 이루어진다.</p>
<p>시작줄은 어떤 메시지인지를, 헤더 블록은 속성을, 본문은 데이터를 담고 있다. 본문은 아예 없을 수도 있다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/4fd38fca-b5e7-47c3-a6a1-065831dc7083/image.png" alt=""></p>
<p>시작줄과 헤더는 줄 단위로 분리된 아스키 문자열이다.</p>
<p>각 줄은 캐리지 리턴과 개행 문자로 구성된 두 글자의 줄바꿈 문자열이며, 이를 <code>CRLF</code>라고 쓴다.</p>
<h3 id="메시지-문법">메시지 문법</h3>
<p>요청 메시지의 형식은 다음과 같다.</p>
<pre><code class="language-HTTP">&lt;메서드&gt; &lt;요청 URL&gt; &lt;버전&gt;
&lt;헤더&gt;

&lt;엔터티 본문&gt;</code></pre>
<p>응답 메시지의 형식은 다음과 같다.</p>
<pre><code class="language-HTTP">&lt;버전&gt; &lt;상태 코드&gt; &lt;사유 구절&gt;
&lt;헤더&gt;

&lt;엔터티 본문&gt;</code></pre>
<ul>
<li>메서드: 클라이언트 측에서 서버가 리소스에 대해 수행하길 바라는 동작으로 <code>GET</code>, <code>HEAD</code>, <code>POST</code>등이 있다.</li>
<li>요청 URL: 요청 대상이 되는 리소스를 지칭하는 완전한 URL 혹은 URL의 경로 구성 요소다. (절대 경로, 상대 경로 해당)</li>
<li>버전: 메시지에서 사용 중인 HTTP의 버전이다. 버전의 형식 <code>HTTP/&lt;메이저&gt;, &lt;마이너&gt;</code></li>
<li>상태 코드: 요청 중에 무엇이 일어났는지 설명하는 세 자리 숫자이다. 보통 각 코드의 첫 번째 자릿수는 상태의 일반적인 분류를 나타낸다.</li>
<li>사유 구절: 숫자로 된 상태 코드를 사람이 이해하기 위해 설명해주는 짧은 문구다. <code>HTTP/1.0 200 OK</code></li>
<li>헤더: 헤더는 0개 이상으로 이루어지며 요청과 응답의 추가 정보를 더해준다. 기본적으로 이름/값 쌍의 목록이다.</li>
<li>엔터티 본문: 임의의 데이터 블록을 포함하며, 모든 메시지가 엔터티 본문을 갖는 것은 아니다.</li>
</ul>
<h2 id="메서드">메서드</h2>
<h3 id="안전한-메서드-safe-method">안전한 메서드 (Safe Method)</h3>
<p><code>GET</code>과 <code>HEAD</code> 메서드는 안전한 메서드라고 볼 수 있는데, HTTP의 요청의 결과로 서버에 어떤 작용도 없음을 의미한다.</p>
<p>즉, HTTP 요청의 결과로 서버에서 어떠한 일도 일어나지 않는다는 것이다.</p>
<p>안전한 메서드가 서버에 작용을 유발하지 않는다는 보장은 없지만, 안전하지 않은 메서드가 사용될 때 사용자들에게 그 사실을 알려주도록 설계 해야한다.</p>
<h3 id="get">GET</h3>
<p><code>GET</code> 메서드는 가장 흔히 쓰이는 메서드이며, 주로 서버에게 리소스를 달라고 요청하기 위해 쓰인다.</p>
<h3 id="head">HEAD</h3>
<p><code>HEAD</code> 메서드는 정확히 <code>GET</code>처럼 행동하지만, 서버는 응답으로 헤더만 반환한다. 엔터티 본문은 반환되지 않는다.</p>
<p>이는 클라이언트가 리소스를 실제로 가져올 필요 없이 헤더만 조사하는 역할을 한다</p>
<ul>
<li>리소스를 가져오지 않고, 헤더를 통해 타입 등을 알아낼 수 있다.</li>
<li>응답의 상태 코드를 통해, 리소스가 존재하는지 확인할 수 있다.</li>
<li>헤더를 확인하여 리소스가 변경되었는지 검사할 수 있다.</li>
</ul>
<h3 id="put">PUT</h3>
<p><code>GET</code> 메서드는 서버로부터 문서를 읽는 반면에, <code>PUT</code> 메서드는 서버에 문서를 쓰는 역할을 한다.</p>
<p>서버가 요청의 본문을 가지고 새 문서를 만들거나, 이미 URL이 존재한다면 본문을 사용해서 교체해준다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/c15f940f-a78d-427e-ab3b-d2bef5f6235c/image.png" alt=""></p>
<h3 id="post">POST</h3>
<p><code>POST</code> 메서드는 서버에 입력 데이터를 전송하기 위해 설계되었다. 특히 HTML에 Form을 지원할 때 흔히 사용된다.</p>
<blockquote>
<p>PUT과 POST에 가장 큰 차이점은 동일한 요청(문서 생성)을 여러번 보낼 때 PUT 메서드는 교체를 하지만, POST는 요청에 맞게 여러번 생성하게 된다.
이런 특성으로 PUT 메서드는 멱등하고, POST는 멱등하지 않다.</p>
</blockquote>
<p>입력 받은 Form 데이터는 서버로 전송되며, 서버는 이를 모아 필요로 하는 곳에 보낸다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/a419bcb3-906d-407a-a3df-dd373548d472/image.png" alt=""></p>
<h3 id="trace">TRACE</h3>
<p>클라이언트가 어떤 요청을 할 때, 그 요청은 방화벽, 프록시, 게이트웨이 등의 애플리케이션을 통과할 수 있다.</p>
<p>각각을 통과할 때 HTTP 요청을 수정할 수 있는데, <code>TRACE</code> 메서드는 클라이언트에게 자신의 요청이 서버에 도달했을 때 어떻게 보이게 되는지를 알려준다.</p>
<p>따라서 <code>TRACE</code> 메서드는 주로 진단을 위해 사용된다. 예를 들면 요청이 의도한 요청/응답 연쇄 과정을 거쳐가는지 검사할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/82f78f70-85d1-41f7-bf5c-4bcbcb74b3db/image.png" alt=""></p>
<p>응답 헤더를 보면 <code>Via</code>헤더가 있는데 이를 통해 <code>1.1 proxy3.company.com</code> 프록시 서버를 거쳐간 것을 알 수 있다.</p>
<h3 id="options">OPTIONS</h3>
<p><code>OPTIONS</code> 메서드는 웹 서버에게 여러 가지 종류의 지원 범위에 대해 물어보는 역할을 한다. 즉, 서버에게 특정 리소스에 대해 어떤 메서드가 지원되는지 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/6bd8c73b-6131-42c6-a971-23a7fa8d8531/image.png" alt=""></p>
<h3 id="delete">DELETE</h3>
<p><code>DELETE</code> 메서드는 서버에게 요청 URL로 지정한 리소스를 삭제할 것을 요청한다. 하지만 클라이언트는 삭제가 수행되는 것을 무조건 보장하지 못한다.</p>
<p>이유는 HTTP 명세 중 서버가 클라이언트에게 알리지 않고 요청에 대해 무시하는 것을 허용하기 때문이다.</p>
<h3 id="확장-메서드">확장 메서드</h3>
<p>HTTP는 필요에 따라 확장해도 문제가 없도록 설계되어 있다. 새로운 기능을 추가해도 과거에 구현한 소프트웨어들의 오동작을 유발하지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[URL과 리소스]]></title>
            <link>https://velog.io/@taemin-jang/URL%EA%B3%BC-%EB%A6%AC%EC%86%8C%EC%8A%A4</link>
            <guid>https://velog.io/@taemin-jang/URL%EA%B3%BC-%EB%A6%AC%EC%86%8C%EC%8A%A4</guid>
            <pubDate>Wed, 21 Aug 2024 13:50:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>다음은 HTTP 완벽 가이드 책을 읽고 정리한 내용입니다.</p>
</blockquote>
<h3 id="이번에-학습할-내용">이번에 학습할 내용</h3>
<ul>
<li>URL 문법, 여러 URL 컴포넌트가 어떤 의미를 가지고 무엇을 수행하는가</li>
<li>상대 URL과 확장 URL 같은 단축 URL</li>
<li>URL의 인코딩과 문자 규칙</li>
</ul>
<h1 id="url">URL</h1>
<p>URL은 통합 자원 식별자(Uniform Resource Identifier) 혹은 URI라고 불리는 부분집합이다.</p>
<p>URI는 URL과 URN으로 구성된 종합적인 개념으로, URN은 이름만으로 리소스를 식별하며, URL은 위치로 리소스를 식별한다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/8d2105d2-0c12-43d8-be75-245fe32047dc/image.png" alt=""></p>
<p>대부분 URL과 URI를 하나로 취급해서 설명한다.</p>
<h2 id="url-문법">URL 문법</h2>
<p>대부분의 URL Scheme 문법은 일반적으로 9개 부분으로 나뉜다.
<code>&lt;스킴&gt;://&lt;사용자 이름&gt;:&lt;비밀번호&gt;@&lt;호스트&gt;:&lt;포트&gt;/&lt;경로&gt;;&lt;파라미터&gt;?&lt;질의&gt;#&lt;프래그먼트&gt;</code></p>
<p>위의 문법을 모두 가지는 URL은 거의 없고, <code>스킴</code>, <code>호스트</code>, <code>경로</code>가 가장 중요하다.</p>
<table>
<thead>
<tr>
<th>컴포넌트</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>스킴</td>
<td>리소스를 가져오려면 어떤 프로토콜을 사용하여 서버에 접근해야하는지 가리킴</td>
</tr>
<tr>
<td>사용자 이름</td>
<td>몇몇 스킴은 리소스에 접근을 하기 위해 사용자 이름이 필요함</td>
</tr>
<tr>
<td>비밀번호</td>
<td>사용자의 비밀번호를 가리키며, 사용자 이름에 콜론으로 이어서 기술</td>
</tr>
<tr>
<td>호스트</td>
<td>리소스를 호스팅하는 서버의 호스트 명이나 IP 주소</td>
</tr>
<tr>
<td>포트</td>
<td>리소스를 호스팅하는 서버가 열어놓은 포트번호 (HTTP 기본 포트 번호 80)</td>
</tr>
<tr>
<td>경로</td>
<td>서버 내 리소스가 서버 어디에 있는지를 가리킴, 서버와 스킴에 따라 문법이 다름</td>
</tr>
<tr>
<td>파라미터</td>
<td>특정 스킴들에서 입력 파라미터를 기술하는 용도</td>
</tr>
<tr>
<td>질의</td>
<td>스킴에서 애플리케이션에 파라미터를 전달하는데 쓰임</td>
</tr>
<tr>
<td>프래그먼트</td>
<td>리소스의 조각이나 일부분을 가리키는 이름, 서버에 전달되지 않음</td>
</tr>
</tbody></table>
<h3 id="사용자-이름과-비밀번호">사용자 이름과 비밀번호</h3>
<p>FTP 서버와 같이 데이터에 접근하기 위해서는 사용자 이름과 비밀번호를 URL에 전달해야한다.</p>
<p><code>ftp://ftp.prep.ai.mit.edu/pub/gnu</code>
<code>ftp://anonymous:my_passwd@ftp.prep.ai.mit.edu/pub/gnu</code>
<code>ftp://joe:joespasswd@www.joes-hardware.com/sales_info.txt</code>
위에 첫번째 예시처럼 사용자 이름이나 비밀번호 없이 접근할 수 있다.</p>
<p>근데 만약 사용자 이름과 비밀번호를 요구하지만, 입력하지 않은 경우 기본 값으로 사용자 이름은 <code>anonymous</code>이고, 비밀번호는 브라우저마다 다르다.</p>
<p>사용자 이름과 비밀번호를 기술한 경우는 3번째 예시처럼 사용자 이름은 <code>joe</code>이고, 비밀번호는 <code>joespasswd</code>가 된다.</p>
<h3 id="파라미터">파라미터</h3>
<p>URL의 파라미터는 애플리케이션이 서버에 정확한 요청을 하기 위해 필요한 입력 파라미터를 받는데 사용한다.</p>
<p>이름/값 쌍의 리스트로 URL 나머지에 <code>;</code> 문자로 구분하여 기술한다.</p>
<p><code>ftp://prep.ai.mit.edu/pub/gnu;type=d</code></p>
<p>이 예시의 경우 이름은 <code>type</code>이고, 값은 <code>d</code>인 한 개의 파라미터를 전달한다.</p>
<p><code>http:/www.joes-hardware.com/hammers;sale=false/index.html;graphics=true</code></p>
<p>위 URL에는 hammers와 index.html이라는 두 개의 경로 조각이 있다.</p>
<p>hammers 경로 조각은 값이 false인 sale이라는 파라미터를 가지고, index.html 경로 조각은 값이 true인 graphics 파라미터를 가지게 된다.</p>
<h3 id="프래그먼트">프래그먼트</h3>
<p>리소스의 특정 부분을 가리킬 수 있도록, URL은 프래그먼트를 제공한다.</p>
<p>예를 들어 URL은 HTML 문서에 있는 특정 이미지나, 일부분을 가리킬 수 있다.</p>
<p><code>http://www.joes-hardware.com/tools.html#drills</code></p>
<p>위 예시는 drills라는 프래그먼트로 해당 웹 서버에 위치한 <code>/tools.html</code> 웹 페이지의 일부를 가리킨다.</p>
<p>프래그먼트는 클라이언트에서만 다루고, <strong>서버에는 전달하지 않는다.</strong></p>
<h2 id="단축-url">단축 URL</h2>
<h3 id="상대-url">상대 URL</h3>
<p>URL은 상대 URL과 절대 URL 두 가지로 나뉜다. (상대 경로, 절대 경로)</p>
<p>지금까지의 예시는 절대 URL이며, 절대 URL은 리소스에 접근하는데 필요한 모든 정보를 가지고 있다.</p>
<p>상대 URL은 모든 정보를 담고 있지 않으며, URL을 짧게 표기하는 방식이다.</p>
<p><code>http://www.joes-hardware.com/tools.html</code> - 현재 URL (기저 URL)
<code>http://www.joes-hardware.com/hammers.html</code>
2개의 URL이 있고, 현재 URL은 tools.html이다.</p>
<p>해당 HTML 파일안에 <code>./hammers.html</code>을 가리키는 하이퍼링크가 이런식으로 작성되어 있다면, 이는 문서의 URL을 기준으로 상대 URL로 작성된 것이다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/5aa12cd0-ca6a-46b8-a9f8-646d3ca9a510/image.png" alt=""></p>
<h3 id="url-확장">URL 확장</h3>
<p>어떤 브라우저들은 URL을 입력한 다음이나 입력하는 도중에 자동으로 URL이 확장된다. (자동완성)</p>
<p>자동으로 URL이 확장되기 때문에 URL 전체를 입력하지 않아도 되어 빠르고 편리하게 도와준다.</p>
<p>이러한 확장은 2가지 기능으로 나뉘어진다.</p>
<ul>
<li><strong>호스트명 확장</strong>
호스트명 확장 기능을 지원하는 브라우저는 단순한 휴리스틱만 사용해서 확장할 수 있다.
<code>yahoo</code>를 입력하면, 브라우저는 호스트 명에 자동으로 <code>www.</code>와 <code>.com</code>을 붙여서 <code>www.yahoo.com</code>을 만든다.</li>
<li><strong>히스토리 확장</strong>
과거에 사용자가 방문했던 URL의 기록을 저장해 놓았다가, URL을 입력하면 입력된 URL의 앞 글자들을 포함하는 완결된 형태의 URL들을 선택하게 도와준다.
<code>http://www.joes-</code>처럼 이전에 방문했던 URL의 시작 부분을 입력하면, 브라우저는 <code>http://www.joes-hardware.com</code>을 보여주게 된다.</li>
</ul>
<h2 id="url의-인코딩-규칙">URL의 인코딩 규칙</h2>
<h3 id="url-문자-집합">URL 문자 집합</h3>
<p>컴퓨터 시스템의 기본 문자 집합은 보통 영어 중심으로 설정되어 <code>US-ASCII</code> 문자 집합을 사용해왔다.</p>
<p>하지만 전 세계 사람들이 사용하다보니, 각 나라의 언어의 문자들은 <code>US-ASCII</code>가 지원하지 않는다.</p>
<p>따라서 이러한 문자들을 지원하기 위해, URL 설계자들은 URL에 이스케이프 문자열을 쓸 수 있게하여 <code>US-ASCII</code>에서 지원하지 않는 문자들을 인코딩할 수 있게 했다.</p>
<blockquote>
<p>지원되지 않는 문자에는 공백도 포함된다. 따라서 공백은 <code>%20</code>으로 처리된다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/a6ee0949-d5cd-42f1-8f75-eb8ce1777c4c/image.png" alt=""></p>
</blockquote>
<h3 id="예약어">예약어</h3>
<p>몇몇 문자는 URL 내에서 예약어로 사용된다.</p>
<p>따라서 아래 표에 있는 예약어를 사용하기 위해서는 반드시 사용전에 인코딩해야 한다.</p>
<table>
<thead>
<tr>
<th>문자</th>
<th>선점 및 제한</th>
</tr>
</thead>
<tbody><tr>
<td>%</td>
<td>인코딩된 문자에 사용할 이스케이프 토큰</td>
</tr>
<tr>
<td>/</td>
<td>경로에 있는 경로 세그먼트를 나누는 용도</td>
</tr>
<tr>
<td>.</td>
<td>경로에서 사용</td>
</tr>
<tr>
<td>..</td>
<td>경로에서 사용</td>
</tr>
<tr>
<td>#</td>
<td>프래그먼트의 문자</td>
</tr>
<tr>
<td>?</td>
<td>질의 문자열의 문자</td>
</tr>
<tr>
<td>;</td>
<td>파라미터의 문자</td>
</tr>
<tr>
<td>:</td>
<td>스킴, 사용자 이름/비밀번호, 호스트/포트의 문자</td>
</tr>
<tr>
<td>$ , +</td>
<td>선점</td>
</tr>
<tr>
<td>@ &amp; =</td>
<td>특정 스킴에서 특별한 의미로 사용</td>
</tr>
<tr>
<td>{}~[]`</td>
<td>게이트웨이와 같은 여러 전송 에이전트에서 불안전하게 다루기에 제한</td>
</tr>
<tr>
<td>&lt;&gt;&quot;</td>
<td>안전하지 않음, 반드시 인코딩해서 사용해야하는 문자</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Polling 구현]]></title>
            <link>https://velog.io/@taemin-jang/Polling</link>
            <guid>https://velog.io/@taemin-jang/Polling</guid>
            <pubDate>Thu, 08 Aug 2024 10:42:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/taemin-jang/post/84a6a765-677f-4dcb-9f96-025966a22b30/image.png" alt=""></p>
<p>회사에서 인사 동기화를 구현하는 과정에서 인사 정보가 동기화되었는지 주기적으로 확인해야하는 상황이 생겼습니다.</p>
<p>간단한 프로세스는 인사 정보를 매핑하고, 수동 인사 동기화를 요청합니다.</p>
<p>요청 받은 서버는 수동 인사 동기화를 진행을 시작하고,</p>
<p>요청에 대한 반환값으로는 진행 중이면 P, 동기화가 완료되면 S, 실패하면 E를 반환합니다.</p>
<p>이때 주기적으로 확인해야할 필요가 있으므로 Polling 구현해서 이를 해결했습니다.</p>
<h2 id="polling이란">Polling이란?</h2>
<p>이전에 웹소켓을 공부할 때 잠깐 알아본 기억이 있었는데, 다시 한 번 알아보겠습니다.</p>
<p>폴링은 <code>클라이언트</code>가 일정 주기로 서버에 필요한 데이터를 요청하는 것을 의미합니다.</p>
<p>JS에서는 setTimeout, setInterval 등으로 이를 구현할 수 있습니다.</p>
<p>변경사항이 없어도 계속 요청을 보내기 때문에 서버에 부담을 줄 수 있고, 요청하는 주기가 짧아질수록 부하는 커지며, HTTP Connection을 맺기 위한 비용이 계속 발생합니다.</p>
<h2 id="long-polling">Long Polling</h2>
<p>클라이언트가 일정 주기마다 요청을 보내지만 서버가 응답을 바로 전달하지 않는 방식을 의미합니다.</p>
<ol>
<li>클라이언트가 서버에게 요청을 보냅니다.</li>
<li>서버는 즉시 요청을 처리하고 응답을 보내지 않습니다.</li>
<li>서버에서 특정 이벤트가 발생하면 응답을 보내고, 서버의 응답이 없으면 타임 아웃이 발생합니다.</li>
<li>응답을 받은 클라이언트는 다시 서버에게 요청합니다.</li>
</ol>
<p>롱 폴링으로 처리하면 폴링의 단점을 보완할 수 있습니다.</p>
<p>그런데도 폴링으로 하게 된 이유는 폴링은 서버에서 별도의 추가적인 작업이 필요없지만, 롱 폴링은 서버에서 추가적인 작업이 필요합니다.</p>
<p>해당 요청을 즉시 처리하지 않기 때문입니다.</p>
<p>따라서 추가적인 작업이 필요없는 폴링 방식을 선택하게 되었습니다.</p>
<h2 id="polling-구현">Polling 구현</h2>
<p>위에서 말했듯이 setTimeout, setInterval으로 구현할 수 있습니다.</p>
<h3 id="settimeout">setTimeout</h3>
<pre><code class="language-jsx">const timeoutPolling = (func, timeout) =&gt; {
  setTimeout(() =&gt; {
    func();
    timeoutPolling(func, timeout);
  }, timeout);
}

timeoutPolling(() =&gt; console.log(&#39;hi&#39;), 1000)</code></pre>
<p>timeoutPolling함수를 재귀적으로 호출하게 됩니다.</p>
<blockquote>
<p>setTimeout 함수는 콜백 함수 실행 시간과 상관없이 콜백 함수 실행 간격이 일정하게 보장됩니다.
즉, 콜백 함수가 끝난 시점을 기준으로 시간을 체크합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/33e4a929-5776-4297-b69b-f2fb267c2c2b/image.png" alt=""></p>
<h3 id="setinterval">setInterval</h3>
<pre><code class="language-jsx">const intervalPolling = (func, interval, maxAttempts = -1) =&gt; {
  let attempts = 0;
  let intervalId = setInterval(() =&gt; {
    if (maxAttempts === attempts) {
      clearInterval(intervalId);
      return;
    }
    attempts++;
    func()
  }, interval);
};</code></pre>
<blockquote>
<p>setInterval 함수는 콜백 함수의 실행 시간이 길면 콜백 함수 실행 간격이 짧아지게 됩니다.
즉, 콜백 함수가 시작한 시점을 기준으로 시간을 체크합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/e266cb92-e3bd-49ec-a275-af29432d935c/image.png" alt=""></p>
<h3 id="while-loop">while loop</h3>
<p>setTimeout, setInterval 말고 while loop를 사용하는 방법도 있습니다. JS에는 타이머가 따로 제공되지 않기 때문에 Promise를 사용해서 직접 구현해야 합니다.</p>
<pre><code class="language-jsx">const delay = (timeout = 1000) =&gt; {
  return new Promise((resolve) =&gt; {
    setTimeout(resolve, timeout);
  });
};

const delayPolling = async (func, validateFunc, timeout) =&gt; {
  let result = await func();
  while (!validateFunc(result)) {
    await delay(timeout);
    try {
      result = await func();
    } catch (e) {
      console.error(e.message);
    }
  }
  return result;
};</code></pre>
<p>이 중 어떤 방식을 선택해도 상관없지만, 익숙한 방식인 while loop로 진행했습니다.</p>
<pre><code class="language-jsx">const DEFAULT_TIME = 1000

const delay = (timeout = DEFAULT_TIMEOUT) =&gt; {
    return new Promise((resolve) =&gt; {
        setTimeout(resolve, timeout)
    })
}

const polling = async (callback: Function, validate: (status: string) =&gt; boolean, timeout: number) =&gt; {
    let result = await callback()
    while (!validate(result)) {
        await delay(timeout)
        result = await callback()
        if (result === &#39;E&#39;) {
            throw Error(&#39;status&#39;)
        }
    }
    return result
}

const hrInfoSyncMapping = async () =&gt; {
    const getSyncStatus = async () =&gt; {
        const key = &#39;/api~&#39;
        try {
            const data = await $http.post(key, {
                body: {
                    ...
                }
            })
            if (data.code === 200) {
                return data.data.status
            }
        } catch (error) {
            console.error(&#39;--- 인사 정보 상태 catch error&#39;, error)
        }
    }

    const isNextStep = (status: string) =&gt; {
        return status === &#39;S&#39;
    }

    const resPolling = await polling(getSyncStatus, isNextStep, DEFAULT_TIMEOUT)

    ...
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[1. Java와 JavaScript의 관계는 햄과 햄스터의 관계]]></title>
            <link>https://velog.io/@taemin-jang/YDNJSY-Chapter-1</link>
            <guid>https://velog.io/@taemin-jang/YDNJSY-Chapter-1</guid>
            <pubDate>Wed, 31 Jul 2024 15:27:27 GMT</pubDate>
            <description><![CDATA[<p>You Don&#39;t Know JS Yet을 읽으면서 정리한 내용입니다.</p>
<p>1장은 JavaScript의 배경지식에 대한 설명으로 시작합니다.</p>
<h3 id="javascript-이름의-유래">JavaScript 이름의 유래</h3>
<p>JavaScript라는 이름은 마케팅 목적으로 사람들을 속이기 위해 고안한 이름인 것을 알고 계셨나요?</p>
<p>처음에 브렌든 아이크는 <a href="https://github.com/mochajs/mocha">Mocha</a>라고 부르다가 넷스케이프에서 <a href="https://livescript.net/">LiveScript</a>로 불렀고, 그 때 당시 Java가 큰 인기를 끌면서 마케팅 효과를 노려 이후에는 JavaScript로 공식 명칭이 되었습니다.</p>
<blockquote>
<p>브랜든 아이크는 JavaScript를 개발한 넷스케이프 프로그래머이다.</p>
<p>스크립트라는 단어가 <code>가벼운 프로그램</code>이라는 뜻으로 유행했고, 자바 개발자들에게 어필하기 위한 목적으로 만들어졌다.</p>
</blockquote>
<h3 id="tc39-절차">TC39 절차</h3>
<p>TC39에서 지정하고 ECMA 표준 기구에 의해 공식화된 명칭은 <code>ECMAScript</code>입니다.</p>
<blockquote>
<p>TC39는 JS를 관리하는 기술 운영 위원회로, JS의 공식 명세를 관리합니다.</p>
</blockquote>
<p>TC39에서 모든 제안을 5단계로 이루어지며 (0 ~ 4단계), 각 단계상 절차는 <a href="https://tc39.es/process-document/">여기</a>에서 확인할 수 있습니다.</p>
<p>간략하게 다음과 같은 절차로 진행됩니다.</p>
<table>
<thead>
<tr>
<th align="center">Stage</th>
<th align="center">Description</th>
</tr>
</thead>
<tbody><tr>
<td align="center">0: Strawperson</td>
<td align="center">스펙에 추가 혹은 변경을 위한 초기 아이디어</td>
</tr>
<tr>
<td align="center">1: Proposal</td>
<td align="center">정식 제안, 문제점 또는 일반적인 요구사항, 솔루션 등</td>
</tr>
<tr>
<td align="center">2: Draft</td>
<td align="center">초안으로써 문법, 의미, API에 대한 정확한 설명 및 실험적인 구현</td>
</tr>
<tr>
<td align="center">3: Candidate</td>
<td align="center">최종 스테이지, 유저로부터의 피드백과 개선 준비</td>
</tr>
<tr>
<td align="center">4: Finished</td>
<td align="center">스펙의 최신 초안에 포함</td>
</tr>
</tbody></table>
<h3 id="js와-웹-브라우저">JS와 웹 브라우저</h3>
<p>대부분 명세서에 정의된 JS와 브라우저 엔진에서 돌아가는 JS는 동일합니다.</p>
<p>TC39 절차를 통해 새로운 동작이 추가 및 수정 등 명세서가 개정됐을 때, 기존 브라우저 엔진에서 실행되던 JS의 작동 방식과 다른 경우가 발생하기도 합니다.</p>
<p>이렇게 불일치가 발생하면 TC39는 종종 기존 결정을 철회하고 명세서를 웹에 맞춥니다.</p>
<p>실제 사례로 배열 메서드 <code>contains()</code>를 추가하려다 몇몇 JS 프레임워크에서 충돌이 나서 해당 메서드명을 <code>includes()</code>로 수정한 사례가 있습니다.</p>
<p>또한 배열 메서드 <code>flatten()</code>이 제안되었다가 <code>flat()</code>으로 변경된 사례도 있습니다.</p>
<h3 id="모든-코드가-명세서대로-동작하는-것은-아닙니다">모든 코드가 명세서대로 동작하는 것은 아닙니다.</h3>
<p>브라우저 개발자 도구의 콘솔이나 Node.js의 REPL이 js 환경이라는 가정하에 JS 코드를 작성합니다.</p>
<blockquote>
<p><strong>REPL</strong>(Read-Evaluation-Print-Loop)은 사용자가 특정 코드를 입력하면 그 코드를 평가하고 코드의 실행 결과를 출력해주는 것을 반복해주는 환경을 말한다.</p>
<p>node.js를 설치한뒤 cmd에서 node 키워드로 REPL 환경에 접속이 가능하다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/b7624db4-cb90-4484-a8bf-c7b3c8df8934/image.png" alt="REPL"></p>
</blockquote>
<p>하지만 이 둘은 만들어진 목적이 다릅니다. 개발자 도구는 말 그대로 개발자를 위한 도구이기 때문에 명세서에 정의된 명세를 항상 엄격하게 준수할 것이라고 생각하면 안됩니다.</p>
<p>콘솔에 입력한 코드는 JS 엔진이 <code>.js</code> 파일을 다루는 방식과 동일한 방식으로 처리되지 않습니다.</p>
<p>콘솔에 목적은 개발자가 짧은 코드를 입력했을 때 그 결과를 즉시 확인하기 위함인 것을 인지해야합니다.</p>
<h3 id="js-패러다임">JS 패러다임</h3>
<p>JS는 다중 패러다임 언어라 절차적, 객체 지향, 함수형 스타일 코드를 모두 작성할 수 있습니다.</p>
<ul>
<li>절차적 프로그래밍은 코드가 탑다운이고, 선형적으로 구조화되며, 프로시저라 불리는 코드 단위에 미리 정해진 일련의 연산을 작성합니다.</li>
<li>객체 지향 프로그래밍은 클래스 기준으로 코드를 구조화하며, 클래스에는 로직과 데이터가 정의됩니다.</li>
<li>함수형 프로그래밍은 코드를 함수 단위로 구조화하며, 함수는 부수 효과가 없는 순수 함수여야 합니다. 또한 함수 자체가 값으로 취급된다는 특징이 있습니다.</li>
</ul>
<h3 id="하위-호환성과-상위-호환성">하위 호환성과 상위 호환성</h3>
<p>JS는 하위 호환성은 지원하지만, 상위 호환성은 지원하지 않습니다.</p>
<p><code>하위 호환성</code>이란 이전 버젼에 구현된 스펙들에 대해 다음 버젼에서도 정상 작동된다는 것을 보장한다는 것입니다.</p>
<p>이로인해 1995년에 작성한 코드가 시간이 흘러도 무조건 작동한다는 것을 보장받게 됩니다.</p>
<p><code>상위 호환성</code>이란 새로운 버전에 들어간 스펙을 사용한 코드를 이전 버전으로 돌려도 에러 없이 돌아가는 것입니다.</p>
<p>대표적으로 <code>HTML</code>과 <code>CSS</code>는 상위 호환성을 지원하여, 2010년에 만들어진 브라우저에서 2019년에 추가된 기능을 사용할 때 웹 페이지가 망가지지는 않습니다.</p>
<p>브라우저는 인식이 불가능한 HTML/CSS는 건너 뛰고 인식 가능한 HTML/CSS는 명세서에 따라 처리하기 때문입니다.</p>
<blockquote>
<p>상위 호환성을 지원하지 않는 JS에서 하위 버전에서 실행 시켜야될 경우 어떻게 해결해야 할까요?</p>
<p>대표적인 해결방법으로는 <code>Transpiling</code>과 <code>Polyfill</code>이 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[비동기 프로그래밍과 에러 핸들링]]></title>
            <link>https://velog.io/@taemin-jang/%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81</link>
            <guid>https://velog.io/@taemin-jang/%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81</guid>
            <pubDate>Wed, 24 Jul 2024 16:16:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.youtube.com/watch?v=o9JnT4sneAQ">FEConf) 유인동님의 ES6+ 비동기 프로그래밍과 실전 에러 핸들링</a> 영상을 정리한 내용입니다.</p>
</blockquote>
<h3 id="문제-발생">문제 발생</h3>
<p>이미지의 높이를 더하는 코드를 작성해보려고 합니다.</p>
<pre><code class="language-js">    const imgs = [
      {name: &#39;image1&#39;, url:&#39;https://picsum.photos/id/1/5000/3333&#39;},
      {name: &#39;image2&#39;, url:&#39;https://picsum.photos/id/2/5000/3333&#39;},
      {name: &#39;image3&#39;, url:&#39;https://picsum.photos/id/3/5000/3333&#39;},
      {name: &#39;image4&#39;, url:&#39;https://picsum.photos/id/4/5000/3333&#39;}
    ]

    function f1() {
      imgs
        .map(({url}) =&gt; {
          let img = new Image();
          img.src = url;
          return img;
        })
        .map(img =&gt; img.height)
        .forEach(height =&gt; console.log(height))
    }

    f1();</code></pre>
<p>이미지의 높이를 가져오기 위해 <code>new Image()</code>로 이미지를 생성해줬습니다. 그리고 <code>img.height</code>의 값을 로그로 찍어보지만 가져올 수 없습니다.</p>
<p>이유는 이미지 생성은 했지만, 이미지를 불러오진 않았기 때문입니다.</p>
<pre><code class="language-js">    const loadImage = url =&gt; {
      let img = new Image();
      img.src = url;
      img.onload = () =&gt; console.log(img.height)
      return img
    }

    loadImage(imgs[0].url); // 3333</code></pre>
<p>이미지의 높이를 구할 수 있는 시점은 <code>img.onload</code> 즉, 이미지가 로드 되었을 때 가져올 수 있게 됩니다.</p>
<p>이 시점을 외부에 알려줘야 이미지들의 높이를 더할 수 있는데, 이를 <code>Promise</code>를 사용하면 해결할 수 있습니다.</p>
<pre><code class="language-js">    const loadImage = url =&gt; new Promise(resolve =&gt; {
      let img = new Image();
      img.src = url;
      img.onload = () =&gt; resolve(img)
      return img
    })

    loadImage(imgs[0].url).then(img =&gt; console.log(img.height)); // 3333</code></pre>
<p>이렇게 resolve를 사용하면 쉽게 구할 수 있습니다.</p>
<p>그럼 이제 만들어논 loadImage로 변경해주면 잘 동작할까요?</p>
<pre><code class="language-js">    const imgs = [
      {name: &#39;image1&#39;, url:&#39;https://picsum.photos/id/1/5000/3333&#39;},
      {name: &#39;image2&#39;, url:&#39;https://picsum.photos/id/2/5000/3333&#39;},
      {name: &#39;image3&#39;, url:&#39;https://picsum.photos/id/3/5000/3333&#39;},
      {name: &#39;image4&#39;, url:&#39;https://picsum.photos/id/4/5000/3333&#39;}
    ]

    const loadImage = url =&gt; new Promise(resolve =&gt; {
      let img = new Image();
      img.src = url;
      img.onload = () =&gt; resolve(img)
      return img
    })

    function f1() {
      imgs
        .map(({url}) =&gt; loadImage(url))
        .map(img =&gt; img.height)
        .forEach(height =&gt; console.log(height))
    }

    f1(); // undefined * 4</code></pre>
<p>loadImage는 <code>Promise</code> 객체를 반환해서 undefined가 찍히게 됩니다.</p>
<p>이를 해결하기 위해 <code>async await</code>을 사용하면 어떨까요?</p>
<pre><code class="language-js">    function f1() {
      imgs
        .map( async ({url}) =&gt; await loadImage(url))
        .map(img =&gt; img.height)
        .forEach(height =&gt; console.log(height))
    }

    f1(); // undefined * 4

    // ====================

    function f1() {
      imgs
        .map( async ({url}) =&gt; {
            const img = await loadImage(url);
            return img.height;
        })
        .forEach(async height =&gt; console.log(await height)) // 3333 * 4
    }</code></pre>
<p>분명 <code>Promise</code> 객체에 <code>async await</code>을 해주면 풀리는 걸로 알고 있는데 결과는 그대로 undefined만 찍히게 됩니다.</p>
<p>forEach안에서도 <code>async await</code>을 해주면 높이가 잘 찍히게 됩니다.</p>
<p>그럼 이제 이미지들의 높이 합을 구해볼까요.</p>
<pre><code class="language-js">    async function f1() {
         const total = await imgs
        .map( async ({url}) =&gt; {
            const img = await loadImage(url);
            return img.height;
        })
        .reduce(async (total, height) =&gt; await total + await height, 0); 
      console.log(total) // 13332
    }</code></pre>
<p>이렇게 코드를 작성하면 동작은 하지만 이 코드는 아슬아슬하게 동작하는 코드입니다.</p>
<p>왜 아슬아슬한 코드일까요?</p>
<p>만약 imgs 중 이미지 url 하나를 수정하고 코드를 실행하면 에러도 안나고 결과 값도 찍히지도 않는 이상한 동작을 하게 됩니다.</p>
<p>이럴 때 에러 핸들링(<code>try-catch문</code>)을 사용해줘야 합니다.</p>
<p>에러를 잡으려면 에러가 발생해야하는데, 위 코드는 에러가 발생하지 않는 코드입니다.</p>
<pre><code class="language-js">const imgs = [
      {name: &#39;image1&#39;, url:&#39;https://picsum.photos/id/1/5000/3333&#39;},
      {name: &#39;image2&#39;, url:&#39;https://picsum.photo/id/2/5000/3333&#39;}, // 수정한 URL
      {name: &#39;image3&#39;, url:&#39;https://picsum.photos/id/3/5000/3333&#39;},
      {name: &#39;image4&#39;, url:&#39;https://picsum.photos/id/4/5000/3333&#39;}
    ]

    const loadImage = url =&gt; new Promise((resolve, reject) =&gt; {
      let img = new Image();
      img.src = url;
      img.onload = () =&gt; resolve(img);
      img.onerror = () =&gt; reject(img);
      return img
    })

    async function f1() {
         const total = await imgs
        .map( async ({url}) =&gt; {
          try{
            const img = await loadImage(url);
            return img.height;
          } catch(error) {
            console.error(error)
          }
        })
        .reduce(async (total, height) =&gt; await total + await height, 0); 
      console.log(total) // NaN
    }

    f1();</code></pre>
<p>이미지의 url이 올바르지 않아 에러가 발생하면 이벤트를 <code>onerror</code>로 받고 <code>reject</code>로 에러를 발생시키고 try-catch문에서 에러를 잡아서 <code>NaN</code>이라는 값이 찍혔습니다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/d67c520b-8cee-4b80-bdd6-c7b4e5fa14aa/image.png" alt=""></p>
<p>만약 에러가 났을 때 <code>NaN</code>이 아닌 0이 찍히게 만든다면 다음처럼 수정하면 됩니다.</p>
<pre><code class="language-js">    const loadImage = url =&gt; new Promise((resolve, reject) =&gt; {
      let img = new Image();
      console.log(&#39;이미지 로드&#39;, url)
      img.src = url;
      img.onload = () =&gt; resolve(img);
      img.onerror = () =&gt; reject(img);
      return img
    })

    async function f1() {
      try{
        const total = await imgs
         .map( async ({url}) =&gt; {
           try{
             const img = await loadImage(url);
             return img.height;
           } catch(error) {
             console.error(error);
             throw error;
           }
         })
         .reduce(async (total, height) =&gt; await total + await height, 0); 
       console.log(total)
      } catch(error) {
        console.log(0);
      }
    }

    f1();</code></pre>
<p>2번째 이미지로 인해 에러가 발생했고, 에러 핸들링을 했는데도 불구하고 그 뒤에 있는 코드까지 실행이 되고 있습니다.</p>
<p>이 코드는 에러 핸들링은 했지만 버그가 있는 코드로써 이전 코드들 보다 더 심각한 코드가 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/df76f3d5-0813-4ab3-9866-8e47a50dd23e/image.png" alt=""></p>
<p>만약 get으로 호출하는 이미지가 아닌 post와 같은 <code>side effect</code>를 일으켜서 문제가 될 수 있고, 또한 get이어도 많은 양의 코드를 호출한다면 충분히 부하를 일으킬 수 있습니다.</p>
<h3 id="문제-해결">문제 해결</h3>
<pre><code class="language-js">    // 제너레이터로 map 추상화
    function* map(f, iter) {
      for(const a of iter) {
        // 부수효과를 내부에서 일으키지 않고 바깥으로 발생 시키기
        yield a instanceof Promise ? a.then(f) : f(a);
      }
    }

    async function f2() {
      let acc = 0;
      for (const a of map((img) =&gt; img.height, map(({url}) =&gt; loadImage(url), imgs))) {
        acc = acc + await a;
      }
      console.log(acc);
    }

    f2(); // 13332</code></pre>
<p>제너레이터로 map을 추상화시키고 <code>yield</code> 키워드를 통해 부수효과를 내부에서 일으키지 않고 바깥으로 발생 시키도록 작성했습니다.</p>
<p>또한 a가 Promise일 경우 함수 합성을 통해 비동기와 동기일 때 처리해줬습니다.</p>
<p>위 코드를 한 번 더 개선해보겠습니다.</p>
<pre><code class="language-js">    // 제너레이터로 map 추상화
    function* map(f, iter) {
      for(const a of iter) {
        // 부수효과를 내부에서 일으키지 않고 바깥으로 발생 시키기
        yield a instanceof Promise ? a.then(f) : f(a);
      }
    }

    async function reduceAsync(f, acc, iter) {
      for (const a of iter) {
        acc = f(acc, await a);
      }
      return acc;
    }

    const f2 = (imgs) =&gt; 
        reduceAsync((a,b) =&gt; a + b, 0, 
          map((img) =&gt; img.height, 
            map(({url}) =&gt; loadImage(url), imgs)))


    f2(imgs);</code></pre>
<p><code>reduceAsync</code>라는 순수 함수를 만들어서 좀 더 안정적인 코드가 되었습니다.</p>
<p>또한 <code>async await</code>을 없애고 함수 표현식으로 간결하게 가독성을 높이고, imgs를 인자로 제한해줬습니다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/6049d279-796e-450b-9052-337821c65c36/image.png" alt=""></p>
<p>에러가 나는 2번째 이미지 까지 동작을 하고 그 이후에는 동작하지 않으며, 에러를 발생시키고 있습니다.</p>
<pre><code class="language-js">    async function f2(imgs) {
      try {
      console.log(
        await reduceAsync((a,b) =&gt; a + b, 0, 
          map((img) =&gt; img.height, 
            map(({url}) =&gt; loadImage(url), imgs))))
      } catch (error) {
        console.error(error);
      }
    }

    f2(imgs);</code></pre>
<p>에러가 발생한다고해서 순수 함수 내에 try-catch문으로 에러 핸들링을 하기도 하는데 이 방법이 좋다고 생각하지 않습니다.</p>
<p>즉, 에러 핸들링을 하지 않는 코드가 좋은 코드라고 생각합니다.</p>
<pre><code class="language-js">    async function f2(imgs) {
        await reduceAsync((a,b) =&gt; a + b, 0, 
          map((img) =&gt; img.height, 
            map(({url}) =&gt; loadImage(url), imgs))))
    }

    f2(imgs).catch(_ =&gt; 0).then(log); // 에러가 안나는 코드 (13332)
    f2(imgs1).catch(_ =&gt; 0).then(log); // 잘못된 인자로 에러가 나는 코드 (0)</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/6e55838a-e6ce-4e26-9c47-6eb48dffc496/image.png" alt=""></p>
<p>순수 함수는 순수 함수 그 자체가 가장 좋고 에러를 발생시키게만 만들어주면 됩니다.</p>
<p>에러가 발생할 것 같다고 해서 순수 함수 내에 미리 에러 핸들링을 하는 것은 불필요한 것이고, 해당 함수의 사용을 제약하는 것이며, 에러를 숨기는 행동입니다.</p>
<p>에러 핸들링은 부수효과를 일으키는 코드 주변에 작성하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[다운로드 기능 구현]]></title>
            <link>https://velog.io/@taemin-jang/%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@taemin-jang/%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 16 Jul 2024 10:56:00 GMT</pubDate>
            <description><![CDATA[<p>이번 프로젝트에서 다운로드 기능을 구현한 내용을 정리해보려고 한다.</p>
<p>다운로드를 하기 위해서는 필요한 내용들이 있다.</p>
<p>HTTP Headers, MIME Type, Blob, URL.createObjectURL, a 태그 등이 있으니 하나씩 알아보도록 하자.</p>
<h2 id="http-headers">HTTP Headers</h2>
<p>HTTP Headers에는 다양한 헤더들이 존재하지만, 그 중 다운로드 기능 시 필요한 헤더들만 알아볼 것이다.</p>
<h3 id="content-disposition">Content-Disposition</h3>
<p>일반적인 HTTP 응답에서 해당 헤더는 컨텐츠가 브라우저 내부에 보여줄지(<code>inline</code>) 혹은 다운로드되거나 로컬에 저장할 용도인지(<code>attachment</code>) 알려주는 헤더다.</p>
<p>또한 데이터가 너무 클 경우 <code>multipart/form-data</code> 의 하위 파트로 활용(<code>form-data</code>)될 수 있다.</p>
<p>헤더에서 사용되는 파라미터는 <code>inline</code>, <code>form-data</code>, <code>attachment</code>, <code>name</code>, <code>filename</code> 이 쓰이고, <code>name</code>과 <code>filename</code>은 필수 파라미터는 아니다.</p>
<p><strong>파일 다운로드 형식 예시</strong></p>
<pre><code class="language-json">Content-Disposition: inline // 기본값
Content-Disposition: attachment // 반드시 다운로드 받아야함
Content-Disposition: attachment; filename=&quot;filename.jpg&quot; // 저장 시 해당 이름으로</code></pre>
<p>아래는 <code>cool.html</code>이라는 파일을 다운로드하라는 응답이다.</p>
<p><code>Content-Type</code>은 MIME Type으로 지정되고, 해당 파일과 타입이 일치해야한다.</p>
<pre><code class="language-json">200 OK
Content-Type: text/html; charset=utf-8
Content-Disposition: attachment; filename=&quot;cool.html&quot;
Content-Length: 21</code></pre>
<p><strong>multipart 형식 예시</strong></p>
<pre><code class="language-json">Content-Type: multipart/form-data;boundary=&quot;boundary&quot;
Content-Disposition: form-data
Content-Disposition: form-data; name=&quot;fieldName&quot;
Content-Disposition: form-data; name=&quot;fieldName&quot;; filename=&quot;filename.jpg&quot;</code></pre>
<p>아래는 총 2개의 데이터로 쪼개져서 전달된 것이고, <code>Content-Type</code>헤더에 <code>boundary</code>가 있다.</p>
<p><code>boundary</code>의 역할은 조각난 데이터의 처음과 끝을 알 수 있게 해주는 역할이다.</p>
<p><code>boundary</code>는 유니크해야하므로 대부분 UUID로 만든다.</p>
<p><code>--boundary</code>는 전송되는 파일 데이터의 구분자다.</p>
<p><code>--boundary--</code>는 쪼개진 데이터의 요청(<code>body</code>)의 끝을 알려주는 것이다. </p>
<pre><code class="language-json">POST /test.html HTTP/1.1
Host: example.org
Content-Type: multipart/form-data;boundary=&quot;boundary&quot;

--boundary
Content-Disposition: form-data; name=&quot;field1&quot; 

value1
--boundary 
Content-Disposition: form-data; name=&quot;field2&quot;; filename=&quot;example.txt&quot; 

value2
--boundary-- </code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/8c469c7f-e774-4512-a445-214b6b1ac1c2/image.png" alt=""></p>
<h3 id="content-type">Content-Type</h3>
<p>해당 헤더는 리소스의 Media Type을 나타내기 위해 사용된다.</p>
<p>즉, 반환된 컨텐츠의 유형이 실제로 무엇인지를 알려준다.</p>
<aside>
💡 Media Type(MIME Type)은 파일의 형식을 나타내는 문자열로 파일과 같이 송신되며, content의 형식을 나타내기 위해 사용

<p>ex) 오디오 파일 - audio/ogg, 그림 파일은 image/png</p>
</aside>

<p>어떤 경우에는 MIME 스니핑을 해서 이 헤더의 값을 따르지 않을 수도 있다.</p>
<p>이를 막기 위해 <code>X-Content-Type-Options</code> 헤더를 <code>nosniff</code>로 설정할 수 있다.</p>
<p><strong>형식 예시</strong></p>
<pre><code class="language-json">Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something</code></pre>
<p><strong>자주 사용하는 MIME Type의 예시</strong></p>
<table>
<thead>
<tr>
<th>타입</th>
<th>설명</th>
<th>서브타입</th>
</tr>
</thead>
<tbody><tr>
<td>text</td>
<td>텍스트를 포함하는 모든 문서</td>
<td>text/plain, text/html, text/css, text/javascript</td>
</tr>
<tr>
<td>image</td>
<td>모든 종류의 이미지</td>
<td>image/gif, image/png, image/jpeg, image/bmp, image/webp</td>
</tr>
<tr>
<td>audio</td>
<td>모든 종류의 오디오 파일</td>
<td>audio/midi, audio/mpeg, audio/webm, audio/ogg, audio/wav</td>
</tr>
<tr>
<td>video</td>
<td>모든 종류의 비디오 파일</td>
<td>video/webm, video/ogg</td>
</tr>
<tr>
<td>application</td>
<td>모든 종류의 이진 데이터</td>
<td>application/octet-stream, application/vnd.mspowerpoint, application/xhtml+xml, application/xml, application/pdf</td>
</tr>
</tbody></table>
<p>적합한 MIME Type을 가진 리소스만이 HTML 엘리먼트 내에서 인식되어 사용할 수 있다. 예로 video나 audio가 이에 해당된다.</p>
<p>또한 파일 업로드나 다운로드 구현 시 file type 파라미터로 MIME Type이 필요하다.</p>
<p>자세한 <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">MIME Type의 종류</a>를 확인할 수 있다.</p>
<h2 id="blob">Blob</h2>
<p>Blob이란 Binary Large Object(이진 대형 객체)의 약자로 이진 데이터를 나타내는 JavaScript 객체다.</p>
<p>주로 텍스트, 이미지 같은 이진 데이터를 다룰때나 파일 관련 작업에서 유용하게 사용된다.</p>
<h3 id="blob-생성자">Blob 생성자</h3>
<p>Blob 객체는 생성자를 사용하여 생성할 수 있다.</p>
<pre><code class="language-jsx">// new Blob(array, options)
const obj = { hello: &quot;world&quot; };
const blob = new Blob([JSON.stringify(obj, null, 2)], { type: &quot;application/json&quot; });</code></pre>
<ul>
<li><code>array</code> : Blob 객체에 포함시킬 배열 또는 데이터</li>
<li><code>options</code> : MIME Type이나 다른 설정을 담고있는 객체(선택 값)</li>
</ul>
<h3 id="속성">속성</h3>
<ul>
<li><code>size</code> : Blob 객체 바이트(Byte) 단위 사이즈</li>
<li><code>type</code> : 객체의 MIME Type, 타입을 알 수 없는 경우 빈 문자열</li>
</ul>
<h3 id="blob-url">Blob URL</h3>
<p>Blob 객체를 가리키는 URL을 생성하기 위해 URL 객체의 정적 메소드로 <code>URL.createObjectURL</code>과 <code>URL.revokeObjectURL</code>을 사용할 수 있다,</p>
<ul>
<li><p><code>URL.createObjectURL</code></p>
<p>  Blob 객체나 File 객체를 나타내는 URL을 포함한 DOMString을 생성한다.</p>
<p>  이 Blob URL은 생성된 window의 document에서만 유효하며, 다른 window에서 재활용할 수 없고 URL의 수명이 한정되어 있다.</p>
<p>  <strong>Blob URL 형태</strong></p>
<pre><code class="language-json">  blob:http://localhost:1234/28ff8746-94eb-4dbe-9d6c-2443b581dd30</code></pre>
<p>  <strong>Blob URL 활용</strong></p>
<pre><code class="language-html">  &lt;img src=&quot;blob:http://localhost:1234/28ff8746-94eb-4dbe-9d6c-2443b581dd30&quot; alt=&quot;Blob URL Image&quot; /&gt;</code></pre>
</li>
<li><p><code>URL.revokeObjectURL</code></p>
<p>  <code>URL.createObjectURL</code>을 통해 생성한 기존 URL을 해제한다.</p>
<p>  기존 URL을 해제하지 않으면 유효한 URL로 판단하여 자바스크립트 엔진에서 GC되지 않아 메모리 누수가 될 수 있기 때문에 URL을 바인딩한 후에는 해제하는 것이 좋다.</p>
<pre><code class="language-jsx">  // Create Blob URL
  const blobUrl = window.URL.createObjectURL(blob);

  // Revoke Blob URL after DOM updates..
  window.URL.revokeObjectURL(blobUrl);</code></pre>
</li>
</ul>
<h2 id="a-태그"><code>&lt;a&gt;</code> 태그</h2>
<p>a 태그에는 <code>download</code>라는 속성이 존재하는데 이는 <code>href</code>에 지정된 파일을 다운로드해주는 기능이다.</p>
<p><strong>사용 방법</strong></p>
<pre><code class="language-html">&lt;a href=&#39;fileurl&#39; download&gt; &lt;!-- fileurl의 파일 이름으로 다운로드 --&gt;

&lt;a href=&#39;fileurl&#39; download=&quot;sample&quot;&gt; &lt;!-- fileurl의 sample파일 명으로 다운로드 --&gt;</code></pre>
<p>이렇게 a태그에 직접 인라인으로 걸어줄 수 있고, JavaScript로 작성할 수도 있다.</p>
<pre><code class="language-jsx">const url = URL.createObjectURL(blob)
const link = document.createElement(&#39;a&#39;)
link.href = url
link.setAttribute(&#39;download&#39;, `${fileName}`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)</code></pre>
<h2 id="다운로드-기능-구현">다운로드 기능 구현</h2>
<p>위 내용들을 기반으로 다운로드 기능을 fetch API로 사용해서 구현해보겠습니다.</p>
<pre><code class="language-jsx">let fileName;

const options = {
    method: &#39;POST&#39;,
    headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;
    }
};

const key = &#39;다운로드 API URI&#39;;

fetch(key, options)
    .then(response =&gt; {
        const contentDisposition = response.headers.get(&#39;Content-Disposition&#39;);
        fileName = contentDisposition.split(&#39;filename=&#39;)[1].replace(/[&#39;&quot;]/g, &#39;&#39;);
        return response.blob();
    })
    .then(blobData =&gt; {
        const blob = new Blob([&#39;\ufeff&#39;, blobData], { type: file.fileType });
        const url = URL.createObjectURL(blob);
        const link = document.createElement(&#39;a&#39;);
        link.href = url;
        link.setAttribute(&#39;download&#39;, fileName);
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
    })
    .catch(error =&gt; {
        console.error(&#39;Error:&#39;, error);
    });</code></pre>
<p>여기서 blob 배열에 <code>\ufeff</code>를 추가한 이유는 csv나 exel 파일 같은 경우 한글이 꺠져서 저장되는 이슈가 있었다.</p>
<p>발생하는 이유는 <code>바이트 순서 표식(BOM)</code>에 의해 UTF-8로 인식되지 않아 깨지는 것이고, 해결하기 위해서는 <code>\ufeff</code>를 데이터 맨 앞에 추가하면 된다.</p>
<h2 id="참고">참고</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Content-Type">https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Content-Type</a>
<a href="https://developer.mozilla.org/ko/docs/Glossary/MIME_type">https://developer.mozilla.org/ko/docs/Glossary/MIME_type</a>
<a href="https://developer.mozilla.org/ko/docs/Web/API/Blob">https://developer.mozilla.org/ko/docs/Web/API/Blob</a>
<a href="https://heropy.blog/2019/02/28/blob/">https://heropy.blog/2019/02/28/blob/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript 에러 처리]]></title>
            <link>https://velog.io/@taemin-jang/JavaScript-error-handling</link>
            <guid>https://velog.io/@taemin-jang/JavaScript-error-handling</guid>
            <pubDate>Thu, 04 Jul 2024 15:14:38 GMT</pubDate>
            <description><![CDATA[<p>에러가 발생하지 않는 코드를 작성하는 것은 불가능하고, 에러가 발생했을 때 적절한 처리가 없다면 프로그램은 강제 종료된다.</p>
<pre><code class="language-js">console.log(&#39;start&#39;);

foo(); // ReferenceError: foo is not defined
// 발생한 에러를 방치하면 프로그램은 강제 종료

// 에러로 인해 프로그램이 강제 종료되어 아래 코드는 실행되지 않음
console.log(&#39;end&#39;);</code></pre>
<p><code>try ... catch</code> 문을 사용해 발생한 에러에 적절하게 처리하면 프로그램이 강제 종료되지 않고 계속해서 코드를 실행시킬 수 있다.</p>
<pre><code class="language-js">console.log(&#39;start&#39;);

try {
  foo(); 
} catch (error) {
  console.error(&#39;에러 발생&#39;, error);
  // 에러 발생 ReferenceError: foo is not defined
}

// 에러 처리로 인해 프로그램이 강제 종료되지 않음
console.log(&#39;end&#39;);</code></pre>
<p>또는 API 호출로 값을 접근할 수 없거나 DOM에서 요소 노드를 찾을 수 없어 <code>null</code>이나 <code>undefined</code>를 반환하는 경우 추가 처리를 해주지 않으면 에러로 이어질 가능성이 있다.</p>
<p>if문으로 처리하거나 단축 평가 또는 옵셔널 체이닝 연산자를 사용해 에러 처리를 할 수 있다.</p>
<pre><code class="language-js">// DOM에 button 요소가 존재하지 않는 경우 querySelector 메서드는 에러를 발생시키지 않고 null을 반환
const $button = document.querySelector(&#39;button&#39;); // null

// 만약 옵셔널 체이닝 연산자를 사용하지 않았다면 에러가 발생함
$button?.classList.add(&#39;disabled&#39;); // undefined</code></pre>
<p>이처럼 에러나 예외적인 상황에 대응하지 않는다면 프로그램은 강제 종료된다.</p>
<p>에러나 예외적인 상황은 너무나 다양하고 많기 때문에 대응 없이 프로그램이 강제 종료된다면 원인을 파악하여 대응하기 어렵다.</p>
<h2 id="try--catch--finally-문">try ... catch ... finally 문</h2>
<p>3개의 코드 블록으로 구성되며, <code>finally</code> 문은 불필요하다면 생략 가능하다.</p>
<pre><code class="language-js">try {
  // 실행할 코드 (에러가 발생할 가능성이 있는 코드)
} catch (err) {
  // try 코드 블록에서 에러가 발생하면 이 코드 블록의 코드가 실행됨
  // err에는 try 코드 블록에서 발생한 Error 객체가 전달
} finally {
  // 에러 발생과 상관없이 반드시 한 번 실행됨
}</code></pre>
<p><code>try</code> 코드 블록이 먼저 실행되고, <code>try</code> 코드 블록에서 에러가 발생하면 <code>catch</code> 코드 블록에서 err 변수에 전달된다.</p>
<p><code>finally</code> 코드 블록은 에러 발생과 상관없이 반드시 한 번 실행된다.</p>
<p>이렇게 <code>try ... catch ... finally</code>문으로 에러 처리하면 프로그램이 강제 종료되지 않는다.</p>
<h2 id="error-객체">Error 객체</h2>
<p><code>Error</code> 생성자 함수는 에러 객체를 생성하며, Error 생성자 함수에는 에러를 상세히 설명하는 에러 메시지를 인수로 전달할 수 있다.</p>
<pre><code class="language-js">const error = new Error(&#39;invalid&#39;);</code></pre>
<p>에러 객체는 <code>message</code> 프로퍼티와 <code>stack</code> 프로퍼티를 갖는다.</p>
<ul>
<li><code>message</code> 프로퍼티 : 인수로 전달한 에러 메시지 값</li>
<li><code>stack</code> 프로퍼티 : 에러를 발생시킨 콜 스택의 호출 정보</li>
</ul>
<h3 id="error-생성자-함수">Error 생성자 함수</h3>
<p>에러 객체의 프로토 타입은 모두 <code>Error.prototype</code>을 상속 받는다.</p>
<table>
<thead>
<tr>
<th align="center">생성자 함수</th>
<th align="center">인스턴스</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Error</td>
<td align="center">일반적 에러 객체</td>
</tr>
<tr>
<td align="center">SyntaxError</td>
<td align="center">자바스크립트 문법에 맞지 않는 문을 해석할 때 발생하는 에러 객체</td>
</tr>
<tr>
<td align="center">ReferenceError</td>
<td align="center">참조할 수 없는 식별자를 참조했을 때 발생하는 에러 객체</td>
</tr>
<tr>
<td align="center">TypeError</td>
<td align="center">피연산자 또는 인수의 데이터 타입이 유효하지 않을 때 발생하는 에러 객체</td>
</tr>
<tr>
<td align="center">RangeError</td>
<td align="center">숫자 값의 허용 범위를 벗어났을 때 발생하는 에러 객체</td>
</tr>
<tr>
<td align="center">URIError</td>
<td align="center">encodeURI 또는 decodeURI 함수에 부적절한 인수를 전달했을 때 발생하는 에러 객체</td>
</tr>
<tr>
<td align="center">EvalError</td>
<td align="center">eval 함수에서 발생하는 에러 객체</td>
</tr>
<tr>
<td align="center">AggregateError</td>
<td align="center">하나의 동작이 여러 개의 오류를 발생시킬 때 여러 오류를 하나의 오류로 감싸는 에러 객체</td>
</tr>
</tbody></table>
<pre><code class="language-js">1 @ 1; // SyntaxError: Invalid or unexpected token
foo(); // ReferenceError: foo is not defined
null.foo; // TypeError: Cannot read property &#39;foo&#39; of null
new Array(-1); // RangeError: Invalid array length
decodeURIComponent(&#39;%&#39;); // URIError: URI malformed
Promise.any([Promise.reject(new Error(&quot;some error&quot;))]) // AggregateError: All promises were rejected</code></pre>
<h2 id="throw-문">throw 문</h2>
<p>Error 생성자 함수로 에러 객체를 생성한다고 에러가 발생하는 것은 아니다.</p>
<p>에러 객체 생성과 에러 발생은 의미가 다르기 때문이다.</p>
<pre><code class="language-js">try {
  // 에러 객체를 생성한다고 에러가 발생하는 것은 아님
  new Error(&#39;something wrong&#39;);
  console.log(&#39;try 코드 블록&#39;);
} catch (error) {
  console.log(error);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/db27da9e-c653-449c-8b31-aee3c079c96a/image.png" alt=""></p>
<p>에러를 발생시키려면 <code>try</code> 코드 블록에서 <code>throw</code> 문으로 에러 객체를 던져야 한다.</p>
<pre><code class="language-js">try {
  // 에러 객체를 던지면 catch 코드 블록이 실행
  throw new Error(&#39;something wrong&#39;);
  console.log(&#39;try 코드 블록&#39;);
} catch (error) {
  console.error(error);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/5367c1c1-1322-4d9c-8618-fd98b7bf5aa4/image.png" alt=""></p>
<p><code>throw</code> 문의 표현식은 어떤 값이라도 상관 없지만 일반적으로 에러 객체를 지정한다.</p>
<p>에러를 던지면 <code>catch</code> 문의 에러 변수가 생성되고 던져진 에러 객체가 할당되면서 <code>catch</code> 코드 블록이 실행된다.</p>
<blockquote>
<p>발생한 에러를 콘솔로 보고 싶으면 log가 아닌 error로 보는 것이 좋다.</p>
<p>console.log는 일반적인 값을 보기 위한 목적이고, console.error는 발생한 error의 값을 보기 위해서이다.</p>
</blockquote>
<h2 id="에러의-전파">에러의 전파</h2>
<p>에러는 호출자 방향으로 전파된다. 즉, 콜 스택의 아래 방향<code>(= 실행 중인 실행 컨텍스트가 푸시되기 직전에 푸시된 실행 컨텍스트 방향)</code>으로 전파된다.</p>
<pre><code class="language-js">const foo = () =&gt; {
  // 에러 객체를 생성하고 throw 문으로 에러 발생
  throw Error(&#39;foo에서 발생한 에러&#39;); // 4
};

const bar = () =&gt; {
  foo(); // 3
};

const baz = () =&gt; {
  bar(); // 2
};

try {
  baz(); // 1
} catch (err) {
  console.error(err);
}</code></pre>
<p>함수의 호출 순서는 <code>1 -&gt; 2 -&gt; 3 -&gt; 4</code>이고, foo 함수가 <code>throw</code>한 에러는 호출자에게 전파되어 전역에서 캐치된다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/b5c2dee3-8924-4777-8cb0-1c222c29e10b/image.png" alt=""></p>
<p>이처럼 throw 된 에러를 캐치하지 않으면 호출자 방향으로 전파되고, throw 된 에러를 어느 컨텍스트에서도 캐치하지 않으면 프로그램은 강제 종료된다.</p>
<p>주의할 점은 비동기 함수인 <code>setTimeout</code>이나 <code>프로미스 후속 처리 메서드의 콜백 함수</code>는 호출자가 없다는 것이다.</p>
<p>태스크 큐나 마이크로태스크 큐에 일시 저장되었다가 <code>콜 스택이 비어있으면</code> 이벤트 루프에 의해 콜 스택으로 푸시되어 실행된다.</p>
<p>이 때 콜 스택에 푸시된 콜백 함수의 실행 컨텍스트는 콜 스택의 가장 하부에 존재하기 때문에 에러를 전파할 호출자가 존재하지 않고 해당 실행 컨텍스트에서 에러 처리를 하지 않으면 프로그램은 강제 종료된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript 비동기 프로그래밍]]></title>
            <link>https://velog.io/@taemin-jang/JavaScript-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</link>
            <guid>https://velog.io/@taemin-jang/JavaScript-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</guid>
            <pubDate>Sat, 29 Jun 2024 02:46:18 GMT</pubDate>
            <description><![CDATA[<p>함수가 실행되려면 실행 컨텍스트 스택에 함수 실행 컨텍스트가 푸시되는 것을 의미한다.</p>
<p>이로인해 함수는 호출된 순서대로 순차적으로 실행되는 것이다.</p>
<blockquote>
<p><strong>자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택을 갖는다.</strong></p>
<p>싱글 스레드 방식은 한 번에 하나의 태스크만 실행할 수 있다.</p>
</blockquote>
<p>자바스크립트 엔진은 싱글 스레드 방식으로 동작하기 때문에 처리 시간이 오래 걸리는 태스크를 실행하는 경우 <code>블로킹(작업 중단)</code>이 발생한다.</p>
<pre><code class="language-js">// sleep 함수는 일정 시간(delay)이 경과된 이후에 콜백 함수를 호출한다.
function sleep(func, delay) {
  // Date.now()는 현재 시간을 숫자(ms)로 반환한다.
  const delayUntil = Date.now() + delay;

  // 현재 시간에 delay를 더한 delayUntil이 현재 시간보다 작으면 계속 반복한다.
  while (Date.now() &lt; delayUntil);

  // 일정 시간이 경과한 이후에 콜백 함수를 호출한다.
  func();
}

function foo() {
  console.log(&#39;foo&#39;);
}

function bar() {
  console.log(&#39;bar&#39;);
}

// sleep 함수는 3초 이상 실행된다.
sleep(foo, 3 * 1000);

// bar 함수는 sleep 함수의 실행이 종료된 이후 호출되어 3초 이상 블로킹 된다.
bar();

// (3초 경과 후) foo 호출 -&gt; bar 호출</code></pre>
<p>이처럼 현재 실행 중인 태스크가 종료될 때까지 다음에 실행될 태스크가 대기하는 방식을 <code>동기(synchronous) 처리 방식</code>이라고 한다.</p>
<p>동기 처리 방식은 태스크를 순서대로 처리해 실행 순서가 보장된다는 장점은 있지만, 처리 시간이 긴 태스크가 앞에 있을 경우 뒤에 있는 태스크는 블로킹되는 단점이 있다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/3eb335ce-17f9-417d-b0e1-726504fcd378/image.png" alt=""></p>
<pre><code class="language-js">// 타이머 함수인 setTimeout으로 변경

function foo() {
  console.log(&#39;foo&#39;);
}

function bar() {
  console.log(&#39;bar&#39;);
}

// 타이머 함수 setTimeout은 일정 시간이 경과된 이후에 콜백 함수 foo를 호출한다.
// setTimeout은 bar 함수를 블로킹하지 않는다.
setTimeout(foo, 3 * 1000);
bar();

// bar 호출 -&gt; (3초 경과 후) foo 호출</code></pre>
<p>이처럼 현재 실행 중인 테스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하는 방식을 <code>비동기(asynchronous) 처리 방식</code>이라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/eab12618-949a-4b6b-b8d6-31ddd6584d85/image.png" alt=""></p>
<p>비동기 처리 방식은 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 바로 실행하므로 블로킹이 발생하지 않는다는 장점이 있지만, 태스크의 실행 순서가 보장되지 않는 단점이 있다.</p>
<blockquote>
<p><strong>타이머 함수, HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작한다.</strong> </p>
</blockquote>
<h2 id="이벤트-루프와-태스크-큐">이벤트 루프와 태스크 큐</h2>
<p>HTML 요소가 애니메이션 효과를 통해 움직이면서 이벤트를 처리하기도 하고, HTTP 요청을 통해 서버로부터 데이터를 가지고 오면서 렌더링하기도 한다.</p>
<p>이처럼 자바스크립트의 동시성을 지원하는 것이 바로 <code>이벤트 루프</code>다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/25295418-578d-468f-a0e7-7696e7c3e5b2/image.png" alt=""></p>
<p>구글의 V8 자바스크립트 엔진을 비롯해 대부분의 자바스크립트 엔진은 크게 <code>콜 스택</code> 영역과 <code>힙</code> 영역으로 구분할 수 있다.</p>
<ul>
<li>콜 스택 (call stack)
소스 코드 평가 과정에서 생성된 실행 컨텍스트가 추가되고 제거되는 스택 자료구조인 실행 컨텍스트 스택이 바로 콜 스택이다.</li>
<li>힙 (heap)
힙은 객체가 저장되는 메모리 공간으로 콜 스택의 요소인 실행 컨텍스트는 힙에 저장된 객체를 참조한다.</li>
</ul>
<p>비동기 처리에서 소스코드 평가와 실행을 제외한 모든 처리는 브라우저 또는 Node.js가 담당한다.</p>
<p>이러한 브라우저 환경은 태스크 큐와 이벤트 루프를 제공한다.</p>
<ul>
<li>태스크 큐 (task queue/event queue/callback queue)
setTimeout이나 setInterval과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역이다. </li>
<li>이벤트 루프 (event loop)
콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 그리고 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인한다.</li>
</ul>
<p>만약 콜 스택이 비어 있고, 태스크 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적(FIFO방식)으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킨다.</p>
<p>즉, 태스크 큐에 있던 비동기 함수들이 콜 스택으로 이동한 뒤 실행되는 방식이 비동기 처리 방식이다.</p>
<blockquote>
<p><strong>정리</strong></p>
<p>자바스크립트는 싱글 스레드 방식 =&gt; 자바스크립트 엔진이 싱글 스레드 방식으로 동작</p>
<p>브라우저는 <strong>멀티 스레드</strong>로 동작한다.</p>
<p>따라서 브라우저와 내장된 자바스크립트 엔진이 협력하여 비동기로 동작할 수 있는 것이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript 타이머]]></title>
            <link>https://velog.io/@taemin-jang/JavaScript-Timer</link>
            <guid>https://velog.io/@taemin-jang/JavaScript-Timer</guid>
            <pubDate>Fri, 28 Jun 2024 15:28:36 GMT</pubDate>
            <description><![CDATA[<p>함수를 명시적으로 호출하면 함수가 즉시 실행된다.</p>
<p>만약 일정 시간 뒤에 호출되도록 예약하려면 타이머 함수를 사용하면 된다.</p>
<h2 id="타이머-함수">타이머 함수</h2>
<h3 id="settimeout--cleartimeout">setTimeout / clearTimeout</h3>
<p>setTimeout 함수의 첫 번째 인수로 전달 받은 콜백 함수는 두 번째 인수로 전달 받은 시간(ms, 1/1000초) 이후 단 한 번 실행되도록 호출 스케줄링된다.</p>
<pre><code class="language-js">const timeoutId = setTimeout(func|code[, delay, param1, param2, ...]);

// 1초(1000ms) 후 타이머가 만료되면 콜백 함수가 호출된다.
setTimeout(() =&gt; console.log(&#39;Hi!&#39;), 1000);

// 1초(1000ms) 후 타이머가 만료되면 콜백 함수가 호출된다.
// 이 때 콜백 함수에 &#39;Lee&#39;가 인수로 전달된다.
setTimeout((name) =&gt; console.log(`Hi! ${name}.`), 1000, &#39;Lee&#39;);

// 두 번째 인수(delay)를 생략하면 기본값 0이 지정된다.
setTimeout(() =&gt; console.log(&#39;Hi!&#39;));</code></pre>
<ul>
<li>func: 타이머가 만료된 뒤 호출될 콜백 함수</li>
<li>delay: 타이머 만료 시간(ms). delay 값을 생략한 경우 기본값 0이 지정된다.<blockquote>
<p>delay 시간이 설정된 타이머가 만료된다해서 콜백 함수가 즉시 호출되는 것이 보장되지는 않는다.</p>
<p>delay 시간은 테스크 큐에 콜백 함수를 등록하는 시간을 지연할 뿐이기 때문이다.</p>
</blockquote>
</li>
<li>param1: 호출 스케줄링된 콜백 함수에 전달해야 할 인수가 존재하는 경우 세 번째 이후의 인수로 전달이 가능하다.</li>
</ul>
<p>setTimeout 함수는 생성된 타이머를 식별할 수 있는 고유한 타이머 id를 반환하고, 이 값은 <code>브라우저 환경인 경우에는 숫자</code>이며 <code>Node.js 환경인 경우 객체</code>다.</p>
<p>또한 타이머 id를 clearTimeout 함수의 인수로 전달하면 타이머를 취소할 수 있다.</p>
<pre><code class="language-js">// 1초(1000ms) 후 타이머가 만료되면 콜백 함수가 호출된다.
// setTimeout 함수는 생성된 타이머를 식별할 수 있는 고유한 타이머 id를 반환한다.
const timerId = setTimeout(() =&gt; console.log(&#39;Hi!&#39;), 1000);

// 타이머가 취소되면 setTimeout 함수의 콜백 함수가 실행되지 않는다.
clearTimeout(timerId);</code></pre>
<h3 id="setinterval--clearinterval">setInterval / clearInterval</h3>
<p>setInterval 함수의 콜백 함수는 두 번째 인수로 전달받은 시간이 경과할 때마다 반복 실행되도록 호출 스케줄링된다.</p>
<p>setInterval 함수의 전달한 인수는 setTimeout 함수와 동일하다.</p>
<pre><code class="language-js">const timeoutId = setInterval(func|code[, delay, param1, param2, ...]);

let count = 1;

// 1초(1000ms) 후 타이머가 만료될 때마다 콜백 함수가 호출된다.
// setInterval 함수는 생성된 타이머를 식별할 수 있는 고유한 타이머 id를 반환한다.
const timerId = setInterval(() =&gt; {
  console.log(count); // 1 2 3 4 5

  // count가 5가 되면 clearInterval 함수의 인자로 timerId를 전달하여 타이머를 취소한다.
  if (count++ === 5) clearInterval(timerId);
}, 1000);</code></pre>
<p>setInterval 함수도 고유한 타이머 Id를 반환하고, 이를 clearInterval 함수에 인수로 전달하면 타이머를 취소할 수 있다.</p>
<h2 id="디바운스와-스로틀">디바운스와 스로틀</h2>
<p>scroll, resize, input, mousemove와 같은 이벤트는 짧은 시간 간격으로 연속해서 발생한다.</p>
<p>이러한 이벤트에 바인딩한 이벤트 핸들러는 과도하게 호출되고 성능에 문제를 일으킬 수 있다.</p>
<p>디바운스와 스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화해서 과도한 이벤트 핸들러의 호출을 방지하는 프로그래밍 기법이다.</p>
<p><strong>예시</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/0a6ea477-1190-444d-8047-c230b5a971a7/image.gif" alt="debounce-throttle"></p>
<h3 id="디바운스">디바운스</h3>
<p>짧은 시간 간격으로 이벤트가 연속으로 발생하면 <code>이벤트 핸들러를 호출하지 않다가</code> 일정 시간이 경과한 이후에 이벤트 핸들러가 <code>한 번만</code> 호출되도록 한다.</p>
<p>대표적인 예시로, 검색할 때 입력 필드 input 이벤트가 짧은 시간 간격 연속으로 발생하는 예시다.</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;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;input type=&quot;text&quot;&gt;
  &lt;div class=&quot;msg&quot;&gt;&lt;/div&gt;
  &lt;script&gt;
    const $input = document.querySelector(&#39;input&#39;);
    const $msg = document.querySelector(&#39;.msg&#39;);

    const debounce = (callback, delay) =&gt; {
      let timerId;

      // debounce 함수는 timerId를 기억하는 클로저를 반환한다.
      return event =&gt; {
        // delay가 경과하기 이전에 이벤트가 발생하면 이전 타이머를 취소하고 새로운 타이머로 재설정한다.
        // 따라서 delay시간 보다 짧은 간격으로 이벤트가 발생하면 callback 호출되지 않는다.
        if (timerId) clearTimeout(timerId)
        timerId = setTimeout(callback, delay, event)
      }
    }

    // debounce 함수가 반환하는 클로저가 이벤트 핸들러로 등록된다.
    // 300ms보다 짧은 간격으로 input 이벤트가 발생하면 debounce 함수의 콜백 함수는
    // 호출되지 않다가 300ms 동안 input 이벤트가 더 이상 발생하지 않으면 한 번만 호출된다.
    $input.oninput = debounce(e =&gt; {
      $msg.textContent = e.target.value;
    }, 300)
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/4e293bd1-4a8c-403e-899a-99efdb9e04a7/image.gif" alt=""></p>
<p>input 이벤트는 사용자가 텍스트 입력 필드에 값을 입력할 때마다 연속해서 발생한다.</p>
<p>만약 input 핸들러에서 사용자가 입력한 값에 맞는 자동 완성을 추천해주는 것과 같은 무거운 처리를 수행한다면 사용자가 입력을 완료하지 않았어도 fetch 요청이 전송될 것이다.</p>
<p>이러한 요청은 서버에도 불필요한 처리이므로 사용자가 입력을 완료했을 때 한 번만 실행되도록 처리하는 것이 바람직하다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/7465d30c-4258-43b0-885b-0bf214d83c18/image.png" alt=""></p>
<p>디바운스를 활용한 다양한 예시는 resize 이벤트 처리, input 입력 필드 자동 완성, 버튼 중복 클릭 방지 처리 등에 유용하게 사용된다.</p>
<h3 id="스로틀">스로틀</h3>
<p>짧은 시간 간격으로 이벤트가 연속해서 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 한다.</p>
<p>예를 들어, scroll 이벤트가 짧은 시간 간격으로 연속해서 발생되는 경우다.</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;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;style&gt;
    .container {
      width: 300px;
      height: 300px;
      background-color: rebeccapurple;
      overflow: scroll;
    }

    .content {
      width: 300px;
      height: 1000vh;
    }
  &lt;/style&gt;
  &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;content&quot;&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div&gt;
    일반 클릭 이벤트 카운터
    &lt;span class=&quot;normal-count&quot;&gt;0&lt;/span&gt;
  &lt;/div&gt;
  &lt;div&gt;
    스로틀 클릭 이벤트 카운터
    &lt;span class=&quot;throttle-count&quot;&gt;0&lt;/span&gt;
  &lt;/div&gt;
  &lt;script&gt;
    const $container = document.querySelector(&#39;.container&#39;);
    const $normalCount = document.querySelector(&#39;.normal-count&#39;);
    const $throttleCount = document.querySelector(&#39;.throttle-count&#39;);

    const throttle = (callback, delay) =&gt; {
      let timerId;

      // throttle 함수는 timerId를 기억하는 클로저를 반환한다.
      return event =&gt; {
        // delay가 경과하기 이전에는 동작하지 않는다.
        // delay가 경과했을 때 이벤트가 발생하면 새로운 타이머를 재설정한다.
        // 따라서 delay 간격으로 callback이 호출된다.
        if (timerId) return;
        timerId = setTimeout(() =&gt; {
          callback(event);
          timerId = null;
        }, delay, event)
      }
    }

    let normalCount = 0;
    $container.addEventListener(&#39;scroll&#39;, () =&gt; {
      $normalCount.textContent = ++normalCount;
    });

    let throttleCount = 0;
    // throttle 함수가 반환하는 클로저가 이벤트 핸들러로 등록된다.
    $container.addEventListener(&#39;scroll&#39;, throttle(() =&gt; {
      $throttleCount.textContent = ++throttleCount;
    }, 100));
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/478d6606-24f0-4637-8d13-2232a30b3285/image.gif" alt=""></p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/ceb43bca-62ca-4a51-8e4b-236b7c2ac733/image.png" alt=""></p>
<p>이처럼 스트롤은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화해서 일정 시간 간격으로 이벤트를 호출하는 방식으로 scroll 이벤트 처리, 무한 스크롤 등에 유용하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DOM]]></title>
            <link>https://velog.io/@taemin-jang/DOM</link>
            <guid>https://velog.io/@taemin-jang/DOM</guid>
            <pubDate>Fri, 21 Jun 2024 14:53:43 GMT</pubDate>
            <description><![CDATA[<p>노드 객체에 대한 정보를 취득하려면 다음과 같은 노드 정보 프로퍼티를 사용해야 한다.</p>
<h3 id="nodeprototypenodetype">Node.prototype.nodeType</h3>
<p>노드 타입 상수를 반환하는 프로퍼티고, Node에 정의되어 있다.</p>
<ul>
<li>Node.ELEMENT_NODE: 요소 노드 타입을 나타내는 상수 1을 반환</li>
<li>Node.TEXT_NODE: 텍스트 노드 타입을 나타내는 상수 3을 반환</li>
<li>Node.DOCUMENT_NODE: 문서 노드 타입을 나타내는 상수 9를 반환</li>
</ul>
<h3 id="nodeprototypenodename">Node.prototype.nodeName</h3>
<p>노드의 이름을 문자열로 반환한다.</p>
<ul>
<li>요소 노드: 대문자 문자열로 태그 이름(ex. <code>ul</code>, <code>li</code>등)을 반환</li>
<li>텍스트 노드 타입: 문자열 <code>#text</code>를 반환</li>
<li>문서 노드 타입: 문자열 <code>#document</code>를 반환</li>
</ul>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;div id=&#39;foo&#39;&gt;Hello&lt;/div&gt;
  &lt;/body&gt;
  &lt;script&gt;
    // 문서 노드의 노드 정보를 취득한다.
    console.log(document.nodeType); // 9
    console.log(document.nodeName); // #document

    // 요소 노드의 노드 정보를 취득한다.
    const $foo = document.getElementById(&#39;foo&#39;);
    console.log($foo.nodeType); // 1
    console.log($foo.nodeName); // DIV

     // 텍스트 노드의 노드 정보를 취득한다.
    const $textNode = #foo.firstChild;
    console.log($textNode.nodeType); // 3
    console.log($textNode.nodeName); // #text
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/82be9e64-7292-488c-addf-11cd460c4a73/image.png" alt=""></p>
<h3 id="nodevalue">nodeValue</h3>
<p>노드 정보 프로퍼티는 모두 읽기 전용 접근자 프로퍼티다.</p>
<p><code>Node.prototype.nodeValue</code> 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로써 참조와 할당 모두 가능하다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;div id=&#39;foo&#39;&gt;Hello&lt;/div&gt;
  &lt;/body&gt;
  &lt;script&gt;
    // 문서 노드의 nodeValue 프로퍼티를 참조한다.
    console.log(document.nodeValue); // null

    // 요소 노드의 nodeValue 프로퍼티를 참조한다.
    const $foo = document.getElementById(&#39;foo&#39;);
    console.log($foo.nodeValue); // null

     // 텍스트 노드의 nodeValue 프로퍼티를 참조한다.
    const $textNode = #foo.firstChild;
    console.log($textNode.nodeValue); // Hello
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p>노드 객체의 nodeValue 프로퍼티를 참조하면 노드 객체의 값을 반환하는데, 노드 객체의 값은 노드 텍스트이다.</p>
<p>따라서 텍스트 노드가 아닌 노드의 nodeValue 프로퍼티를 참조하면 null을 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/132cea4f-f6d1-4089-a41c-1951e07a5491/image.png" alt=""></p>
<h3 id="textcontent">textContent</h3>
<p><code>Node.prototype.textContent</code> 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 모두 취득하거나 변경한다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;div id=&#39;foo&#39;&gt;
      Hello
      &lt;span&gt;world!&lt;/span&gt;
    &lt;/div&gt;
  &lt;/body&gt;
  &lt;script&gt;
    // #foo 요소 노드의 텍스트를 모두 취득한다.
    const $foo = document.getElementById(&#39;foo&#39;);
    console.log($foo.textContent); // Hello world!
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p>이 때 HTML 마크업이 포함되어 있어도 무시하고 문자열인 텍스트만 취득할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/07721bd2-f15b-4bc8-ae12-faace48dc877/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/d3c24fc8-f005-4c69-ac43-186c7736ca88/image.png" alt=""></p>
<blockquote>
<p>textContent와 유사한 동작을 하는 innerText 프로퍼티가 있지만 다음과 같은 이유로 사용하지 않는 것이 좋다.</p>
<ul>
<li>innerText 프로퍼티는 CSS에 의해 비표시(<code>visibility: hidden;</code>)로 지정된 요소 노드의 텍스트를 반환하지 않는다.</li>
<li>innerText 프로퍼티는 CSS를 고려해야 하므로 textContent 프로퍼티보다 느리다.</li>
</ul>
</blockquote>
<h2 id="dom-조작">DOM 조작</h2>
<p>DOM 조작은 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드를 삭제 또는 교체하는 것을 말한다.</p>
<p>DOM 조작에 의해 새로운 노드가 추가되거나 삭제되면 리플로우와 리페인트가 발생하는 원인이 되어 성능에 영향을 준다.</p>
<blockquote>
<p><strong>리플로우와 리페인트</strong></p>
<ul>
<li>리플로우: 요소 추가, 제거, 크기 및 위치 변경과 같이 레이아웃이 변경될 때 발생합니다.</li>
<li>리페인트: 요소의 색상, 배경과 같이 웹 페이지의 스타일이 변경될 때 발생합니다.</li>
</ul>
</blockquote>
<h3 id="innterhtml">innterHTML</h3>
<p><code>Element.prototype.innerHTML</code> 프로퍼티는 setter와 getter모두 존재하는 접근자 프로퍼티로서 요소 노드의 HTML 마크업을 취득하거나 변경한다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;div id=&#39;foo&#39;&gt;
      Hello
      &lt;span&gt;world!&lt;/span&gt;
    &lt;/div&gt;
  &lt;/body&gt;
  &lt;script&gt;
    // #foo 요소 노드의 텍스트를 모두 취득한다.
    const $foo = document.getElementById(&#39;foo&#39;);
    console.log($foo.innerHTML); // Hello &lt;span&gt;world!&lt;/span&gt;
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p>앞서 살펴본 <code>textContent</code> 프로퍼티는 HTML 마크업을 무시하고 텍스트만 반환하지만, <code>innerHTML</code> 프로퍼티는 HTML 마크업이 포함된 문자열 그대로 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/298700a3-8edd-4b95-9190-091cb311949e/image.png" alt=""></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;div id=&#39;foo&#39;&gt;
      Hello
      &lt;span&gt;world!&lt;/span&gt;
    &lt;/div&gt;
  &lt;/body&gt;
  &lt;script&gt;
    const $foo = document.getElementById(&#39;foo&#39;);
    $foo.innerHTML = &#39;Hello span&gt;there!&lt;/span&gt;&#39;
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p>이렇게 <code>innerHTML</code> 프로퍼티를 사용하면 간단히 DOM 조작이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/a25f5941-178b-4d75-a635-720857005b3c/image.png" alt=""></p>
<p>간단히 DOM 조작이 가능하다는 의미는 위험할 수 있다. 만약 사용자로부터 입력받은 데이터를 그대로 <code>innerHTML</code> 프로퍼티에 할당하는 것은 <strong>크로스 사이트 스크립팅 공격 (XSS)</strong>에 취약하다.</p>
<blockquote>
<p><strong>크로스 사이트 스크립팅 공격</strong></p>
<p>브라우저에 스크립트가 실행되도록 해서 사용자의 세션을 가로채거나 웹사이트를 변조, 악의적인 콘텐츠를 삽입 등과 같은 공격을 의미합니다.</p>
</blockquote>
<p>하지만 HTML5는 <code>innerHTML</code> 프로퍼티로 삽입된 script 요소 내의 자바스크립트 코드를 실행하지 않는다.</p>
<p>script 요소 없이도 XSS 공격은 가능하다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;div id=&#39;foo&#39;&gt;
      Hello
    &lt;/div&gt;
  &lt;/body&gt;
  &lt;script&gt;
    // 에러 이벤트를 강제로 발생시켜서 자바스크립트 코드가 실행된다.
    const $foo = document.getElementById(&#39;foo&#39;);
    $foo.innerHTML = &#39;&lt;img src=&quot;x&quot; onerror=&quot;alert(document.cookie)&quot;&gt;&#39;;
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p><code>innerHTML</code> 프로퍼티를 사용하면 DOM 조작이 간편하지만 XSS 공격에 취약하기 때문에 주의해야 한다.</p>
<blockquote>
<p><strong>HTML 새니티제이션</strong></p>
<p>HTML 새니티제이션은 사용자로부터 입력받은 데이터에 의해 발생할 수 있는 XSS 공격을 예방하기 위해 위험을 제거하는 기능을 말한다.</p>
<p>새니티제이션 함수를 직접 구현도 가능하지만, <code>DOMPurify</code> 라이브러리를 사용하는 것을 권장한다.</p>
<pre><code class="language-js">DOMPurify.sanitize(&#39;&lt;img src=&quot;x&quot; onerror=&quot;alert(document.cookie)&quot;&gt;&#39;);
// =&gt; &lt;img src=&quot;x&quot;&gt;</code></pre>
</blockquote>
<h3 id="insertadjacenthtml-메서드">insertAdjacentHTML 메서드</h3>
<p><code>Element.prototype.insertAdjacentHTML(position, DOMString)</code> 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입할 수 있다.</p>
<ul>
<li>position은 DOMString을 전달할 위치로 문자열을 전달하는데 아래 이미지처럼 4가지로 정의되어 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/64cac31b-fa02-4597-9dc8-e43034148dd7/image.png" alt=""></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;!-- beforebegin --&gt;
    &lt;div id=&#39;foo&#39;&gt;
      &lt;!-- afterbegin --&gt;
      Hello
      &lt;!-- beforeend --&gt;
    &lt;/div&gt;
    &lt;!-- afterend --&gt;
  &lt;/body&gt;
  &lt;script&gt;
    const $foo = document.getElementById(&#39;foo&#39;);

    $foo.insertAdjacentHTML(&#39;beforebegin&#39;, &#39;&lt;p&gt;beforebegin&lt;/p&gt;&#39;);
    $foo.insertAdjacentHTML(&#39;afterbegin&#39;, &#39;&lt;p&gt;afterbegin&lt;/p&gt;&#39;);
    $foo.insertAdjacentHTML(&#39;beforeend&#39;, &#39;&lt;p&gt;beforeend&lt;/p&gt;&#39;);
    $foo.insertAdjacentHTML(&#39;afterend&#39;, &#39;&lt;p&gt;afterend&lt;/p&gt;&#39;);
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p><code>insertAdjacentHTML</code> 메서드는 <code>innerHTML</code> 프로퍼티와 다르게 요소를 추가할 때 기존 요소에는 영향을 주지 않고 새로 추가되는 요소만 파싱하여 효율적이고 빠르다.</p>
<p>하지만 <code>innerHTML</code> 프로퍼티와 마찬가지로 HTML 마크업 문자열을 파싱하기 때문에 XSS 공격에 취약하다.</p>
<h3 id="createelementtagname">createElement(tagName)</h3>
<p><code>Document.prototype.createElement(tagName)</code> 메서드는 요소 노드를 생성하여 반환한다.</p>
<pre><code class="language-js">// 요소 노드 생성
const $li = document.createElement(&#39;li&#39;);

// 생성된 요소 노드는 자식 노드를 가지고 있지 않다.
console.log($li.childNodes); // NodeList []</code></pre>
<p><code>createElement</code> 메서드로 생성한 요소 노드는 자식 노드를 가지고 있지 않고 홀로 존재하는 상태다.</p>
<h3 id="createtextnodetext">createTextNode(text)</h3>
<p><code>Document.prototype.createTextNode(text)</code> 메서드는 텍스트 노드를 생성하여 반환한다.</p>
<pre><code class="language-js">// 텍스트 노드 생성
const textNode = document.createTextNode(&#39;Banana&#39;);</code></pre>
<p>텍스트 노드 또한 자식 노드로 추가되지 않고 홀로 존재하는 상태라 텍스트 노드를 요소 노드에 추가하는 처리가 별도로 필요하다.</p>
<h3 id="appendchildchildnode">appendChild(childNode)</h3>
<p><code>Node.prototype.appendChild(childNode)</code> 메서드는 매개변수 childNode에게 인수로 전달한 노드를 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 추가한다.</p>
<pre><code class="language-js">// 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/8081f27c-45f7-456a-9693-bddac0bc4d46/image.png" alt=""></p>
<p>이렇게 요소 노드와 텍스트 노드는 부자 관계로 연결되었지만 아직 DOM에 추가되지 않은 상태다.</p>
<p>위 예제처럼 요소 노드에 자식 노드가 하나도 없는 경우에는 <code>textContent</code> 프로퍼티를 사용하여 추가하는 편이 더욱 간편하다.</p>
<pre><code class="language-js">// 텍스트 노드를 생성하여 요소 노드의 자식 노드로 추가
$li.appendChild(document.createTextNode(&#39;Banana&#39;));

// $li 요소 노드의 자식 노드가 하나도 없으면 위 코드와 동일하게 동작한다.
$li.textContent = &#39;Banana&#39;;</code></pre>
<h3 id="노드-생성과-추가-과정">노드 생성과 추가 과정</h3>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;ul id=&#39;fruits&#39;&gt;
      &lt;li&gt;Apple&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/body&gt;
  &lt;script&gt;
    const $fruits = document.getElementById(&#39;fruits&#39;);

    // 요소 노드 생성
    const $li = document.createElement(&#39;li&#39;);

    // 텍스트 노드 생성
    const textNode = document.createTextNode(&#39;Banana&#39;);

    // 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
    $li.appendChild(textNode);

    // $li 요소 노드를 $fruits 요소 노드의 마지막 자식 노드로 추가
    $fruits.appendChild($li);
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/fd25f020-2a58-4e52-97d8-5e674bbf4476/image.png" alt=""></p>
<p>이 과정에서 새롭게 생성한 요소 노드 1개가 DOM에 추가되는데 이 때 리플로우와 리페인트가 1번 실행된다.</p>
<p>만약 요소 노드가 여러개 생성되어 DOM에 여러번 추가되면 리플로우와 리페인트도 여러번 실행되므로 성능에 좋지 않다.</p>
<h3 id="insertbeforenewnode-childnode">insertBefore(newNode, childNode)</h3>
<p><code>appendChild</code> 메서드는 인수로 전달받은 노드를 자신이 호출한 노드의 마지막 자식 노드로 DOM에 추가한다.</p>
<p>이때 노드 위치를 지정할 수 없고 항상 마지막에 추가하게 되는데 <code>Node.prototype.insertBefore(newNode, childNode)</code> 메서드는 첫 번째 인수로 전달 받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입한다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;ul id=&#39;fruits&#39;&gt;
      &lt;li&gt;Apple&lt;/li&gt;
      &lt;li&gt;Banana&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/body&gt;
  &lt;script&gt;
    const $fruits = document.getElementById(&#39;fruits&#39;);

    // 요소 노드 생성
    const $li = document.createElement(&#39;li&#39;);

    // 텍스트 노드 생성
    const textNode = document.createTextNode(&#39;Orange&#39;);

    // 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
    $li.appendChild(textNode);

    // $li 요소 노드를 $fruits 요소 노드의 마지막 자식 노드 앞에 삽입
    $fruits.insertBefore($li, $fruits.lastElementChild);
  &lt;/script&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/8612143f-337c-40d5-b7f9-bbbd379279eb/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이터러블]]></title>
            <link>https://velog.io/@taemin-jang/%EC%9D%B4%ED%84%B0%EB%9F%AC%EB%B8%94</link>
            <guid>https://velog.io/@taemin-jang/%EC%9D%B4%ED%84%B0%EB%9F%AC%EB%B8%94</guid>
            <pubDate>Thu, 13 Jun 2024 15:23:19 GMT</pubDate>
            <description><![CDATA[<h1 id="이터레이션-프로토콜-iteration-protocols">이터레이션 프로토콜 (Iteration protocols)</h1>
<p>이터레이션 프로토콜은 내장 객체 또는 구문이 아닌 프로토콜로써 몇가지 규칙에 따라 모든 객체에서 구현될 수 있다.</p>
<p>ES6에서 순회 가능한(Iterable) 자료 구조를 만들기 위해 도입 됐다.</p>
<p>이터레이션 프로토콜에는 이터러블 프로토콜과 이터레이터 프로토콜이 있다.</p>
<h2 id="이터러블-프로토콜-iterable-protocol">이터러블 프로토콜 (Iterable protocol)</h2>
<p>이터러블은 <code>for ... of</code>문으로 순회할 수 있으며 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용할 수 있다.</p>
<p>이터러블은 <code>Symbol.iterator</code>를 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속 받은 <code>Symbol.iterator</code>메서드를 호출하여 구현할 수 있다.</p>
<h2 id="이터레이터-프로토콜-iterator-protocol">이터레이터 프로토콜 (Iterator protocol)</h2>
<p>이터러블의 <code>Symbol.iterator</code>메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다.</p>
<p>이터레이터는 next 메서드를 가지고 있어 호출하면 이터러블을 순회하고, value와 done 프로퍼티를 갖는 이터레이터 <code>result</code> 객체를 반환한다.</p>
<ul>
<li><code>value</code>는 현재 순회중인 이터러블의 값을 반환</li>
<li><code>done</code>은 이터러블의 순회 완료 여부를 반환</li>
</ul>
<blockquote>
<p>즉, 이터레이터는 이터러블의 요소를 탐색하기 위한 포인터 역할을 한다.</p>
</blockquote>
<p><code>for ... of</code>문은 <code>[Symbole.iterator]()</code> 메소드의 호출로 시작해서 이터러블 값을 <code>next()</code>를 반복적으로 호출하여 순회한다. 순회가 끝나면 <code>done: true</code>를 반환한다.</p>
<pre><code class="language-js">[Symbol.iterator]()

// 아래 처럼 next 메소드를 통해 이터러블 요소를 탐색하기 위한 포인터 되어있다.
{
  next() {
    return { value: any, done: boolean }
  }
}

// 배열은 이터러블이다.
const array = [1, 2, 3];

// 이터러블은 Symbol.iterator 메소드를 소유한다.
// Symbol.iterator 메소드는 이터레이터를 반환한다.
let iter = array[Symbol.iterator]();

// 이터레이터는 next 메소드를 소유한다.
// next 메소드는 이터레이터 result 객체를 반환한다.
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: 3, done: false}
console.log(iter.next()); // {value: undefined, done: true}</code></pre>
<h3 id="symboliterator">[Symbol.iterator]</h3>
<p><code>Symbol.iterator</code>를 프로퍼티 key로 사용한 메소드를 가진 객체는 이터러블이다.</p>
<pre><code class="language-js">const iterable1 = {};

iterable1[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

console.log([...iterable1]); // [1, 2, 3]</code></pre>
<blockquote>
<p><strong><code>.iterator()</code>가 아닌 <code>[Symbol.iterator]</code> 메소드를 사용하는 이유</strong></p>
<p>기존에 사용 중인 객체가 이미 <code>.iterator()</code> 메소드를 갖고 있다면 문제가 될 수 있다.</p>
<p>따라서 평범한 문자열 대신 <code>Symbol</code>을 사용하여 고유한 메소드를 정의해준 것이다.</p>
</blockquote>
<pre><code class="language-js">const isIterable = v =&gt; v !== null &amp;&amp; typeof v[Symbol.iterator] === &#39;function&#39;;

// 배열, 문자열, Map, Set 등은 이터러블이다.
isIterable([]); // true
isIterable(&#39;&#39;); // true
isIterable(new Map()); // true
isIterable(new Set()); // true
isIterable({}); // false</code></pre>
<h1 id="빌트인-이터러블">빌트인 이터러블</h1>
<p>자바스크립트는 이터레이션 프로토콜을 준수한 객체인 빌트인 이터러블을 제공한다.</p>
<table>
<thead>
<tr>
<th align="center">빌트인 이터러블</th>
<th align="center">Symbol.iterator 메서드</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Array</td>
<td align="center">Array.prototype[Symbol.iterator]</td>
</tr>
<tr>
<td align="center">String</td>
<td align="center">String.prototype[Symbol.iterator]</td>
</tr>
<tr>
<td align="center">Map</td>
<td align="center">Map.prototype[Symbol.iterator]</td>
</tr>
<tr>
<td align="center">Set</td>
<td align="center">Set.prototype[Symbol.iterator]</td>
</tr>
<tr>
<td align="center">TypeArray</td>
<td align="center">TypeArray.prototype[Symbol.iterator]</td>
</tr>
<tr>
<td align="center">arguments</td>
<td align="center">arguments[Symbol.iterator]</td>
</tr>
<tr>
<td align="center">DOM 컬렉션</td>
<td align="center">NodeList.prototype[Symbol.iterator]  HTMLCollection.prototype[Symbol.iterator]</td>
</tr>
</tbody></table>
<h1 id="for--of-문">for ... of 문</h1>
<p><code>for (변수 선언문 of 이터러블) { ... }</code>이와같이 사용할 수 있다.</p>
<pre><code class="language-js">// 배열은 이터러블
for (const item of [1, 2, 3]) {
  // item 변수에 순차적으로 1, 2, 3이 할당 된다.
  console.log(item); // 1 2 3
}

// 위 동작을 for문으로는 다음처럼 표현할 수 있다.
const iterable = [1, 2, 3];

// 이터러블의 Symbol.iterator 메서드를 호출하여 이터레이터를 생성한다.
const iterator = iterable[Symbol.iterator]()

for ( ;; ) {
  // next 메서드를 호출하여 이터러블을 순회한다.
  const res = iterator.next();

  // result 객체의 done 프로퍼티 값이 true이면 이터러블 순회를 중단한다.
  if (res.done) break;

  const item = res.value;
  console.log(item); // 1 2 3
}</code></pre>
<h1 id="이터러블과-유사-배열-객체">이터러블과 유사 배열 객체</h1>
<p>유사 배열 객체는 배열처럼 인덱스로 프로퍼티 값에 접근 가능하고 length 프로퍼티를 갖는 객체를 말한다.</p>
<p>유사 배열 객체도 length 프로퍼티를 갖기 때문에 for 문으로 순회할 수 있고, 인덱스를 나타내는 숫자 형식의 문자열 프로퍼티를 키로 가지고 있기 때문에 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있다.</p>
<pre><code class="language-js">// 유사 배열 객체
const arrayLike = {
  0: 1,
  1: 2,
  2: 3,
  length: 3
}

// 유사 배열 객체는 length 프로퍼티를 갖기 때문에 for문으로 순회할 수 있다.
for (let i = 0; i &lt; arrayLike.length; i++) {
  console.log(arrayLike[i]); // 1 2 3
}</code></pre>
<p>하지만 유사 배열 객체는 이터러블이 아는 일반 객체다.</p>
<pre><code class="language-js">// Symbol.iterator 메서드가 없기 때문에 for ... of 문으로 순회할 수 없다.
for (const item of arrayLike) {
  console.log(item); // 1 2 3
} // TypeError: arrayLike is not iterable</code></pre>
<p>하지만 arguments, NodeList, HTMLCollection은 유사 배열 객체이면서 이터러블이다.</p>
<p>이유는 ES6에서 이터러블이 도입되면서 해당 객체에 <code>Symbol.iterator</code> 메서드를 구현했기 때문에 이터러블이 되었다.</p>
<pre><code class="language-js">// 유사 배열 객체
const arrayLike = {
  0: 1,
  1: 2,
  2: 3,
  length: 3
}

// Array.from을 통해 유사 배열 객체를 배열로 변환할 수 있다.
const arr = Array.from(arrayLike);
console.log(arr); // [1, 2, 3]

for (const item of arr) {
  console.log(item); // 1 2 3
}</code></pre>
<h1 id="사용자-정의-이터러블">사용자 정의 이터러블</h1>
<p>일반 객체에 이터레이션 프로토콜을 준수하도록 구현하면 사용자 정의 이터러블이 된다.</p>
<pre><code class="language-js">// 피보나치 수열을 구현한 사용자 정의 이터러블
const fibonacci = {
  // Symbol.iterator 메서드를 구현하여 이터러블 프로토콜 준수
  [Symbol.iterator]() {
    let [pre, cur] = [0, 1];
    const max = 10;

    return {
      next() {
        [pre, cur] = [cur, pre + cur];
        return { value: cur, done: cur &gt;= max }
      }
    }
  }
}

for (const num of fibonacci) {
  console.log(num); // 1 2 3 5 8
}

const arr = [...fibonacci];
console.log(arr); // [1, 2, 3, 5, 8]

const [first, second, ...rest] = fibonacci;
console.log(first, second, rest); // 1 2 [3, 5, 8]</code></pre>
<p>만들어진 사용자 정의 이터러블을 for ... of 문으로 순회할 때마다 next 메서드가 호출되고, done 프로퍼티가 true 값을 가질 때 까지 반복한다.</p>
<p>이처럼 이터러블은 for ... of 문뿐만 아니라 스프레드 문법, 배열 디스트럭처링 할당에도 사용 가능하다.</p>
<p>max는 10으로 고정되어 있는데 이를 인수로 전달받아 이터러블을 반환하는 함수로 만들 수 있다.</p>
<pre><code class="language-js">// 피보나치 수열을 구현한 사용자 정의 이터러블
const fibonacci = function (max) {
  let [pre, cur] = [0, 1];
  // Symbol.iterator 메서드를 구현하여 이터러블 프로토콜 준수
  return {
      [Symbol.iterator]() {    
      return {
          next() {
          [pre, cur] = [cur, pre + cur];
          return { value: cur, done: cur &gt;= max }
        }
      }
    }
  }
}

for (const num of fibonacci(10)) {
  console.log(num); // 1 2 3 5 8
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript 클로저]]></title>
            <link>https://velog.io/@taemin-jang/JavaScript-%ED%81%B4%EB%A1%9C%EC%A0%80</link>
            <guid>https://velog.io/@taemin-jang/JavaScript-%ED%81%B4%EB%A1%9C%EC%A0%80</guid>
            <pubDate>Thu, 06 Jun 2024 14:18:30 GMT</pubDate>
            <description><![CDATA[<p>클로저는 자바스크립트 고유의 개념이 아니며, 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.</p>
<p>클로저를 이해하기 위해서는 핵심 내용인<code>렉시컬 환경</code>에 대해 알고 넘어가야 한다.</p>
<h2 id="렉시컬-스코프">렉시컬 스코프</h2>
<p>자바스크립트 엔진은 함수를 어디서 호출했는지(동적 스코프)가 아닌, <code>함수를 어디에 정의했는지</code>에 따라 상위 스코프를 결정하는데 이를 렉시컬 스코프(정적 스코프)라 한다.</p>
<pre><code class="language-js">const x = 1;

function foo() {
    const x = 10;
      bar();
}

function bar() {
    console.log(x);
}

foo(); // ?
bar(); // ?</code></pre>
<p>여기서 함수 foo와 bar는 전역에서 정의되었기 때문에, 정의되는 시점에서 함수의 상위 스코프는 전역으로 결정되고 변하지 않는다.</p>
<p>따라서 함수 bar를 호출하면 변수 x를 bar 스코프 내에서 찾고 없으면 스코프 체인을 통해 상위 스코프인 전역 변수 x의 값을 참조해서 1이 찍힌다.</p>
<p>함수 foo 또한 bar를 호출하고 있는데 호출에 의해 상위 스코프 참조는 변경되지 않기 때문에 동일한 1을 출력된다.</p>
<h2 id="함수-객체의-내부-슬롯-environment">함수 객체의 내부 슬롯 [[Environment]]</h2>
<p>함수는 정의된 환경과 호출되는 환경이 다를 수 있다.</p>
<p>호출과는 상관 없이 상위 스코프를 기억해야 하므로 상위 스코프의 참조를 함수의 내부 슬롯<code>[[Environment]]</code>에 저장하게 된다.</p>
<p>예를 들어, 전역에서 정의된 함수 선언문은 전역 코드가 평가되는 시점에 평가되어 함수 객체를 생성한다.</p>
<p>생성된 함수 객체 내부 슬롯 <code>[[Environment]]</code>에는 실행 컨텍스트의 렉시컬 환경인 전역 렉시컬 환경의 참조가 저장된다.</p>
<p>만약 함수 내부에서 정의된 함수 표현식은 외부 함수가 실행되는 시점에 평가되어 함수 객체를 생성한다.</p>
<p>외부 함수 렉시컬 환경의 참조가 생성된 함수 객체 내부 슬롯에 저장된다.</p>
<p>따라서 위 예제를 보면 foo와 bar 함수는 전역에서 함수 선언문으로 정의되었기 때문에 내부 슬롯 <code>[[Environment]]</code>에는 전역 렉시컬 환경의 참조가 저장된다.</p>
<h2 id="클로저와-렉시컬-환경">클로저와 렉시컬 환경</h2>
<p>위에서 설명한 렉시컬 스코프와 함수 객체의 내부 슬롯에 대한 개념을 이해했다면 클로저를 이해할 수 있다.</p>
<pre><code class="language-js">const x = 1;

function outer() {
  const x = 10;
  const inner = funtion () {
    console.log(x);
  }
  return inner;

const innerFunc = outer();
innerFunc(); // ?</code></pre>
<p>outer 함수를 호출하면 중첩 함수인 inner를 반환하고 생명 주기를 마감한다.</p>
<p>이때 outer 함수의 지역 변수 x도 같이 생명 주기를 마감하고, 더이상 outer 함수의 지역 변수 x에는 접근할 수 없다.</p>
<p>하지만 innerFunc 함수를 실행하면 outer 함수의 지역 변수 x 값을 출력하게 된다.</p>
<p>이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.</p>
<p>이러한 중첩 함수를 클로저라고 부른다.</p>
<blockquote>
<p>outer 함수의 생명 주기가 종료되면 해당 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거된다.</p>
<p>outer 함수의 실행 컨텍스트에서 제거는 되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.</p>
<p>outer 함수의 렉시컬 환경은 inner 함수의 <code>[[Environment]]</code> 내부 슬롯에 참조되고 있고 inner 함수도 전역 변수 innerFunc에 참조되고 있기 때문에 가비지 컬렉션의 대상이 되지 않아 메모리 공간에서 제거되지 않는다.</p>
</blockquote>
<p>클로저의 조건은 다음과 같다.</p>
<ol>
<li>중첩 함수로 선언되었는가?</li>
<li>상위 스코프의 식별자를 참조하고 있는가?</li>
<li>중첩 함수가 외부 함수보다 더 오래 유지되는가? (오래 유지하려면 중첩 함수가 외부 함수의 반환값이어야 한다.)</li>
</ol>
<p>위 3가지 조건에 하나라도 부합되지 않으면 클로저가 아니다.</p>
<p>또한 클로저는 상위 스코프 식별자가 x, y, z가 있고 그 중 x만 참조하고 있다면 x만 기억하고 그 외 식별자는 기억하지 못한다.</p>
<p>이렇게 클로저에 의해 참조되는 상위 스코프의 변수를 <code>자유 변수</code>라고 부른다.</p>
<p>클로저는 상위 스코프를 기억해야 하므로 불필요한 메모리 낭비라고 생각할 수도 있지만, 클로저가 참조하고 있지 않은 식별자는 기억하지 않으므로 불필요한 메모리 낭비가 아니다.</p>
<p>따라서 클로저는 필요하다면 적극적으로 활용하는 것이 좋다.</p>
<h2 id="클로저-활용하기">클로저 활용하기</h2>
<p>주로 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다.</p>
<p>즉, 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.</p>
<pre><code class="language-js">const increase = (function () {
  let num = 0;

  // 클로저
  return function () {
    return ++num;
  };
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3</code></pre>
<p>즉시 실행 함수가 실행되고 반환한 함수가 increase 변수에 할당된다.</p>
<p>increase 변수에 할당된 함수는 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저가 된다.</p>
<p>num 변수는 외부에서 직접 접근할 수 없는 private 변수이므로 안전하게 상태를 변경할 수 있다.</p>
<pre><code class="language-js">const counter = (function () {
  let num = 0;

  return {
    // num : 0 프로퍼티는 public 하므로 은닉되지 않는다.
    increase() {
      return ++num;
    },
    decrease() {
      return num &gt; 0 ? --num : 0;
    }
  };
}());

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2

console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0</code></pre>
<p>즉시 실행 함수가 반환하는 객체 리터럴은 코드 블록이 아니므로 별도의 스코프를 생성하지 않는다.</p>
<p>다음은 함수형 프로그래밍에서 클로저를 활용하는 예제다.</p>
<pre><code class="language-js">// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
function makeCounter(predicate) {
  let counter = 0;

  return function () {
    counter = predicate(counter);
    return counter;
  };
}

function increase(n) {
  return ++n;
}

function decrease(n) {
  return --n;
}

const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동되지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2</code></pre>
<p>makeCounter 함수는 인자로 전달받은 함수를 합성하여 자신이 반환하는 값으로 함수의 동작을 변경할 수 있다.</p>
<p>이 때 주의할 점은 makeCounter 함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다.</p>
<p>increaser와 decreaser 함수는 makeCounter 함수를 호출 할 때마다 함수 객체를 생성하고 반환한 후 소멸된다. 따라서 각각 생성된 함수 객체는 <code>[[Environment]]</code> 내부 슬롯에 makeCounter 함수의 렉시컬 환경을 기억하고 있기 때문에 counter 변수는 공유 되지 않는다.</p>
<p>만약 독립된 counter가 아닌 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 한다.</p>
<pre><code class="language-js">function counter = (function () {
  let count = 0;

  return function (predicate) {
    count = predicate(count);
    return count;
  };
}());

function increase(n) {
  return ++n;
}

function decrease(n) {
  return --n;
}

console.log(counter(increase)); // 1
console.log(counter(increase)); // 2

console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0</code></pre>
<h2 id="캡슐화와-정보-은닉">캡슐화와 정보 은닉</h2>
<p><code>캡슐화</code>는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다.</p>
<p>캡슐화는 주로 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 <code>정보 은닉</code>이라고 한다.</p>
<p>정보 은닉은 외부에 공개하지 않고 감추어 적절치 못한 접근으로 부터 객체의 상태가 변경되는 것을 보호하고, 객체 간의 결합도를 낮추는 효과가 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript Function]]></title>
            <link>https://velog.io/@taemin-jang/JavaScript-Function</link>
            <guid>https://velog.io/@taemin-jang/JavaScript-Function</guid>
            <pubDate>Fri, 17 May 2024 15:16:00 GMT</pubDate>
            <description><![CDATA[<h1 id="함수란">함수란?</h1>
<p>입력 값을 받은 뒤 출력 값을 반환하는데, 입력으로 전달 받는 변수를 <code>매개 변수</code>, 함수 호출 시 입력하는 값을 <code>인수</code>,  함수 내부에서 처리하고 나온 출력 값을 <code>반환 값</code>이라고 한다.</p>
<p>함수 또한 여러 개 존재할 수 있어서 특정 함수를 구별하기 위해 식별자를 함수의 이름으로 사용할 수 있다.</p>
<pre><code class="language-js">// 함수 정의
function add(x, y) {
  return x + y;
}

// 함수 호출
var result = add(2, 5);

console.log(result); // 7</code></pre>
<h2 id="함수를-왜-사용하는가">함수를 왜 사용하는가?</h2>
<p>함수는 필요할 때 여러 번 호출할 수 있다는 특징이 있다.</p>
<ul>
<li>하나의 함수로 동일한 작업에서 사용하여 코드의 재사용성</li>
<li>하나의 코드에서 수정함으로써 여러 곳에서 수정할 필요없는 유지보수 편의성</li>
<li>적절한 함수의 이름으로 코드의 가독성</li>
</ul>
<p>이와 같은 이유로 함수를 사용하고, 함수는 객체지만 일반 객체와 다르다.</p>
<p>일반 객체는 호출할 수 없지만 함수는 호출할 수 있고, 함수는 다른 함수의 인수로 전달할 수 있는 <code>일급 객체</code>이다.</p>
<h2 id="함수-정의">함수 정의</h2>
<p>함수를 정의하는 방법에는 4가지가 있다.</p>
<pre><code class="language-js">// 함수 선언문
function add(x, y) {
  return x + y;
}

// 함수 표현식
var add = function (x, y) {
  return x + y;
};

// Function 생성자 함수
var add = new Function(&#39;x&#39;, &#39;y&#39;, &#39;return x + y&#39;);

// 화살표 함수(ES6)
var add = (x, y) =&gt; x + y;</code></pre>
<h3 id="함수-선언문">함수 선언문</h3>
<p>함수 선언문은 함수 이름을 생략할 수 없다.</p>
<p>또한 함수 선언문은 표현식이 아닌 문이다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/89522d0b-99bf-4006-ae1f-05de567313ea/image.png" alt=""></p>
<p>크롬 콘솔에서 함수 선언문을 실행하면 undefined가 출력되고, 표현식인 문인 화살표 함수는 함수가 출력되는 것을 볼 수 있다.</p>
<pre><code class="language-js">function foo() { console.log(&#39;foo&#39;); }

foo(); // foo

(function bar() { console.log(&#39;bar&#39;); });

bar(); // ReferenceError: bar is not defined</code></pre>
<h3 id="함수-표현식">함수 표현식</h3>
<p>자바스크립트의 함수는 값처럼 변수에 할당 할 수도, 프로퍼티 값이 될 수도, 배열의 요소가 될 수도 있다.</p>
<p>이러한 성질을 갖는 객체를 일급 객체라 하며, 함수 리터럴로 생성한 함수 객체를 변수에 할당하는 방식을 함수 표현식이라 한다.</p>
<p>함수 표현식은 함수 이름을 생략할 수 있다.</p>
<pre><code class="language-js">// 익명 함수 표현식
var add = function (x, y) {
  return x + y;
}

// 기명 함수 표현식
var add = function foo (x, y) {
  return x + y;
}

console.log(add(2,5)); // 7

console.log(foo(2,5)); // ReferenceError: foo is not defined</code></pre>
<h3 id="함수-호이스팅">함수 호이스팅</h3>
<pre><code class="language-js">// 함수 참조
console.dir(add); // f add(x, y)
console.dir(sub); // undefined

// 함수 호출
console.log(add(2, 5)); // 7
console.log(sub(2, 5)); // TypeError: sub is not a function

// 함수 선언문
function add(x, y) {
  return x + y;
};

// 함수 표현식
var sub = function (x, y) {
  return x - y;
}</code></pre>
<p>위 예제를 보면 함수 선언문은 이전에 호출이 가능하지만, 함수 표현식은 이전에 호출할 수 없다.</p>
<p>그 이유는 각각의 함수 생성 시점이 다르기 때문이다.</p>
<p>모든 선언문은 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행이 되면서 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 생성된 함수 객체를 할당하게 된다.</p>
<p>따라서 함수 선언문은 이전에 함수를 참조 및 호출할 수 있으며 이처럼 함수 선언문이 코드의 상단에 끌어 올려진 것처럼 동작하는 것을 <code>함수 호이스팅</code>이라고 한다.</p>
<p>함수 표현식은 var 키워드로 선언했기 때문에 undefined로 초기화 되고, 아직 함수 객체가 할당되지 않았기 때문에 함수 호출에서는 타입 에러가 발생하는 것이다.</p>
<p>즉 함수 표현식으로 함수를 정의하면 함수 호이스팅이 발생하는 것이 아니라 변수 호이스팅이 발생하게 되며, 이로 인해 함수 선언문 대신 함수 표현식을 사용할 것을 권장한다.</p>
<h3 id="function-생성자-함수">Function 생성자 함수</h3>
<p>Function 생성자 함수에 매개 변수 목록과 함수 몸체를 문자열로 전달하면서 new 연산자와 함께 호출하면 함수 객체가 생성된다.</p>
<p>new 연산자 없이 호출해도 결과는 동일하게 동작한다.</p>
<pre><code class="language-js">var add = new Function(&#39;x&#39;, &#39;y&#39;, &#39;return x + y&#39;);

console.log(add(2, 5)); // 7</code></pre>
<p>Function 생성자 함수는 클로저를 생성하지 않는 등 함수 선언문이나 표현식과 다르게 동작한다.</p>
<h3 id="화살표-함수">화살표 함수</h3>
<p>ES6에서 도입된 함수로 <code>function</code> 키워드 대신 화살표 <code>=&gt;</code>를 사용해 좀 더 간략한 방법으로 함수를 선언할 수 있고, 항상 익명 함수로 정의한다.</p>
<pre><code class="language-js">const add = (x, y) =&gt; x + y;
console.log(add(2, 5)); // 7</code></pre>
<p>화살표 함수는 생성자 함수로 사용할 수 없고, 기존 함수와 this 바인딩 방식이 다르며, prototype 프로퍼티가 없고, arguments 객체를 생성하지 않는 점이 특징이다.</p>
<h2 id="함수-호출">함수 호출</h2>
<p>함수를 호출할 때 함수는 매개변수의 개수와 인수의 개수가 일치하는지 체크하지 않는다.</p>
<p>따라서 일치하지 않아도 에러가 발생하지 않고, 인수가 부족해서 매개변수에 인수가 할당되지 않으면 해당 값은 undefined다.</p>
<pre><code class="language-js">function add(x, y) {
  return x + y;
}

console.log(add(2)); // 2 + undfined가 되므로 NaN이 반환된다.
console.log(add(&#39;a&#39;, &#39;b&#39;)); // &#39;ab&#39;</code></pre>
<p>또한 자바스크립트는 동적 타입 언어로써 매개변수의 타입을 사전에 지정할 수 없다.</p>
<p>따라서 적절한 인수가 전달되었는지 확인하는 법은 다음과 같다.</p>
<pre><code class="language-js">function add(x, y) {
  if (typeof x !== &#39;number&#39; || typeof y !== &#39;number&#39;) {
    // 매개변수를 통해 전달된 인수의 타입이 부적절한 경우 에러를 발생
    throw new TypeError(&#39;인수는 모두 숫자 값이어야 한다.&#39;);
  }
  return x + y;
}

console.log(add(2)); // TypeError: 인수는 모두 숫자 값이어야 한다.
console.log(add(&#39;a&#39;, &#39;b&#39;)); // TypeError: 인수는 모두 숫자 값이어야 한다.</code></pre>
<p>이처럼 함수 내부에서 적절한 인수가 전달되었는지 확인하지만 부적절한 호출을 사전에 방지할 수 없고, 에러는 런타임에서 발생하게 된다. 따라서 타입스크립트를 사용하면 컴파일 시점에서 부적절한 호출을 방지할 수 있다.</p>
<p>만약 매개변수보다 인수가 더 많은 경우 초과된 인수는 암묵적으로 arguments 객체의 프로퍼티로 보관된다.</p>
<pre><code class="language-js">function add(x, y) {
  console.log(arguments); // Arguments(3) [2, 5, 10, callee: f, Symbol(Symbol.iterator): f]
  return x + y;
}

console.log(add(2, 5, 10)); // 7</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/cc3872f6-9d3a-4eca-b221-97da5b97b573/image.png" alt=""></p>
<p>arguments 객체는 매개 변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용하게 사용할 수 있다.</p>
<pre><code class="language-js">function add(x, y, z) {
  x = x || 0;
  y = y || 0;
  z = z || 0;
  return x + y + z;
}

// ES6에서 도입된 매개변수 기본값 지정
function add(x = 0, y = 0, z = 0) {
  return x + y + z;
}

console.log(add(1, 2, 3)); // 6
console.log(add(1, 2)); // 3
console.log(add(1)); // 1
console.log(add()); // 0</code></pre>
<p>이렇게 사용하면 인수 체크 및 초기화를 시킬 수 있다.</p>
<h2 id="참조에-의한-전달과-외부-상태의-변경">참조에 의한 전달과 외부 상태의 변경</h2>
<p>매개변수도 함수 몸체 내부에서 변수와 동일하게 취급되므로 매개변수 또한 타입에 따른 전달 방식을 그대로 따른다.</p>
<blockquote>
<p>원시 값(primitive)은 값에 의한 전달(pass by value)
객체 값(object)은 참조에 의한 전달(pass by reference)</p>
</blockquote>
<pre><code class="language-js">// 매개변수 primitive는 원시 값, obj는 객체를 전달 받음
function changeVal(primitive, obj) {
  primitive += 100;
  obj.name = &#39;kim&#39;;
}

// 외부 상태
let num = 100;
let person = { name: &#39;lee&#39; };

console.log(num); // 100
console.log(person); // {name: &#39;lee&#39;}

// 원시 값은 값 자체가 복사되어 전달되고 객체는 참조 값이 복사되어 전달된다.
// 원시 값은 재할당이 이뤄지고 객체는 변경 가능한 값이므로 직접 변경
changeVal(num, person);

// 원시 값은 원본이 훼손되지 않는다.
console.log(num); // 100

// 객체는 원본이 훼손된다. 부수 효과 발생
console.log(person); // {name: &#39;kim&#39;}</code></pre>
<p>원시 타입 인수는 값 자체가 복사되어 매개변수에 전달되기 때문에 함수 내부에서 매개변수 값을 변경해도 원본은 훼손되지 않는다. (부수 효과 발생하지 않음)</p>
<p>하지만 객체 타입의 인수는 참조 값이 복사되어 매개변수에 전달되기 때문에 함수 내부에서 참조 값을 변경할 경우 원본이 훼손된다. (부수 효과 발생함)</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/c9ab2d2d-526e-4ee8-8966-2226fcdbbcd1/image.png" alt=""></p>
<p>이처럼 함수에 객체를 전달하고 함수 내부에서 객체 값이 변경되면 상태 변화를 추적하기 어렵고, 코드의 복잡성이 높아지고 버그가 일어날 수 있다.</p>
<p>이를 해결하기 위해서는 객체를 불변 객체로 만들어 사용해야 하는데, 불변 객체로 만들기 위해서는 깊은 복사를 통해 새로운 객체를 생성하고 재할당을 통해 교체해야 한다.</p>
<blockquote>
<p>이처럼 외부 상태를 변경하지 외부 상태에 의존하지 않는 함수를 순수 함수라고 한다.</p>
</blockquote>
<h2 id="다양한-함수-형태">다양한 함수 형태</h2>
<h3 id="즉시-실행-함수iife-immediately-invoked-function-expression">즉시 실행 함수(IIFE, Immediately Invoked Function Expression)</h3>
<p>함수 정의와 동시에 즉시 호출하는 함수를 즉시 실행 함수라고 하며, 단 한번만 호출되고 다시 호출할 수 없다.</p>
<pre><code class="language-js">// 익명 즉시 실행 함수
(function() {
   var a = 3;
   var b = 5;
   return a * b;
}());

// 기명 즉시 실행 함수
(function foo() {
   var a = 3;
   var b = 5;
   return a * b;
}());

foo(); // ReferenceError: foo is not defined

function() { // SyntaxError: Function statements require a function name
   var a = 3;
   var b = 5;
   return a * b;
}();

function foo () { 
   var a = 3;
   var b = 5;
   return a * b;
}(); // SyntaxError: Unexpected token &#39;)&#39; 
// function foo() {}(); =&gt; function foo() {};();</code></pre>
<p>이처럼 즉시 실행 함수는 반드시 <code>그룹 연산자 (...)</code>로 감싸야 한다.</p>
<p>함수 선언문은 함수 이름을 생략할 수 없고, 함수 선언문이 끝나는 위치(중괄호 뒤에) 세미콜론이 암묵적으로 추가되기 때문에 위와 같은 에러가 발생하게 된다.</p>
<p>즉 그룹 연산자로 함수를 묶는 이유는 함수 리터럴을 평가해서 함수 객체를 생성하기 위해서다.</p>
<h3 id="재귀-함수">재귀 함수</h3>
<p>함수가 자기 자신을 호출하는 것을 재귀 호출이라고 한다.</p>
<p>재귀 함수는 자기 자신을 호출하는 함수를 말하며 주로 반복되는 처리를 위해 사용한다.</p>
<pre><code class="language-js">// 일반적인 반복 함수
function coundown(n) {
    for (var i=n; i&gt;0; i--) console.log(i);
}

// 재귀 함수
function  countdownRecursive(n) {
    if (n==0) return
    console.log(n)
    countdownRecursive(n-1)
}</code></pre>
<p>재귀 함수는 무한 호출이 될 수 있으므로 반드시 종료 조건을 넣어줘야한다. </p>
<p>재귀함수에 return문이 없었다면, -무한대까지 계속 반복하다가 스택오버플로 에러가 발생한다. </p>
<p>이런 점에서 코드를 직관적이고 간결하게 짤 수 있을 때만 잘 선택해서 사용해야한다.</p>
<h3 id="중첩-함수">중첩 함수</h3>
<p>함수 내부에 정의된 함수를 중첩 함수 또는 내부 함수라 한다.</p>
<p>중첩 함수를 포함하는 함수를 외부 함수라 부르며, 중첩 함수는 외부 함수 내에서만 호출할 수 있다.</p>
<pre><code class="language-js">function outer() {
  var x = 1;

  // 중첩 함수
  function inner() {
    var y = 2;
    // 외부 함수의 변수를 참조할 수 있다.
    console.log(x + y); // 3
  }

  inner();
}

outer();</code></pre>
<h3 id="콜백-함수">콜백 함수</h3>
<p>함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수라고하며, 매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수라고한다.</p>
<p>고차 함수는 콜백 함수를 자신의 일부분으로 합성하며 고차 함수는 매개변수를 통해 전달받은 콜백 함수의 호출 시점을 결정해서 호출한다.</p>
<p>콜백 함수는 고차 함수에 의해 호출되며 이때 고차 함수는 필요에 따라 함수에 인수를 전달할 수 있다.</p>
<pre><code class="language-js">// 외부에서 전달받은 f를 n만큼 반복 호출한다
function repeat(n, f) {
  for (var i = 0; i &lt; n; i++) {
    f(i); // i를 전달하면서 f를 호출
  }
}

var logAll = function (i) {
  console.log(i);
};

// 반복 호출할 함수를 인수로 전달한다.
repeat(5, logAll); // 0 1 2 3 4

var logOdds = function (i) {
  if (i % 2) console.log(i);
};

// 반복 호출할 함수를 인수로 전달한다.
repeat(5, logOdds); // 1 3</code></pre>
<p>콜백 함수는 함수형 프로그래밍 패러다임뿐만 아니라 비동기 처리(이벤트 처리, Ajax 통신, 타이머 함수 등)에 활용되는 중요한 패턴이다.</p>
<pre><code class="language-js">//콜백 함수를 사용한 이벤트 처리
//myButton 버튼을 클릭하면 콜백 함수 실행
document.getElementbyId(&#39;myButton&#39;).addEventListener(&#39;click&#39;, function () {
    console.log(&#39;button clicked!&#39;);
});

// 콜백 함수를 사용한 비동기 처리
// 1초 후에 메시지 출력
setTimeout(function () {
    console.log(&#39;1초 경과&#39;);
});</code></pre>
<h2 id="순수-함수와-비순수-함수">순수 함수와 비순수 함수</h2>
<h3 id="순수-함수">순수 함수</h3>
<p>함수형 프로그래밍에서 어떤 외부 상태에 의존하지도 않고 변경하지도 않는, 즉 부수 효과가 없는 함수다.</p>
<pre><code class="language-js">var count = 0; // 현재 카운트를 나타내는 상태

// 순수 함수 increase는 동일한 인수가 전달되면 언제나 동일한 값을 반환한다.
function increase(n) {
  return ++n;
}

// 순수 함수가 반환한 결과값을 변수에 재할당해서 상태를 변경
count = increase(count);
console.log(count) // 1

count = increase(count);
console.log(count) // 2</code></pre>
<h3 id="비순수-함수">비순수 함수</h3>
<p>외부상태에 의존하거나 외부 상태를 변경하는, 즉 부수 효과가 있는 함수다.</p>
<pre><code class="language-js">var count = 0; // 현재 카운트를 나타내는 상태: increase 함수에 의해 변화한다.

// 비순수 함수
function increase() {
  return ++count; // 외부 상태에 의존하며 외부 상태를 변경한다.
}

// 비순수 함수는 외부 상태(count)를 변경하므로 상태 변화를 추적하기 어려워진다.
increase();
console.log(count) // 1

increase();
console.log(count) // 2</code></pre>
<blockquote>
<p><strong>함수형 프로그래밍</strong></p>
<p>함수형 프로그래밍은 순수 함수와 보조 함수의 조합을 통해 외부 상태를 변경하는 부수 효과를 최소화해서 불변성을 지향하는 프로그래밍 패러다임이다.
자바스크립트는 멀티 패러다임 언어이므로 객체지향 프로그래밍뿐만 아니라 함수형 프로그래밍을 적극적으로 활용하고 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript 연산자]]></title>
            <link>https://velog.io/@taemin-jang/JavaScript-%EC%97%B0%EC%82%B0%EC%9E%90</link>
            <guid>https://velog.io/@taemin-jang/JavaScript-%EC%97%B0%EC%82%B0%EC%9E%90</guid>
            <pubDate>Thu, 09 May 2024 15:40:55 GMT</pubDate>
            <description><![CDATA[<h1 id="연산자란">연산자란?</h1>
<p>하나 이상의 표현식을 대상으로 산술, 할당, 비교, 논리, 타입, 지수 연산등을 수행해 하나의 값을 만들며, 연산의 대상을 피연산자라고 한다.</p>
<p>또한 피연산자는 값으로 평가될 수 있는 표현식이다.</p>
<h2 id="산술-연산자">산술 연산자</h2>
<p>피연산자를 대상으로 수학적 계산을 수행해 새로운 숫자 값을 만드는 연산자를 의미한다.</p>
<p>만약 산술 연산이 불가능한 경우 NaN을 반환한다.</p>
<h3 id="이항-산술-연산자">이항 산술 연산자</h3>
<p>2개의 피연산자를 산술 연산하여 숫자 값을 만든다.</p>
<p>모든 이항 산술 연산자는 부수 효과(side effect)가 없다.</p>
<p>즉, 산술 연산을 해도 피연산자의 값이 바뀌지 않고 새로운 값을 만든다.</p>
<table>
<thead>
<tr>
<th align="center">이항 산술 연산자</th>
<th align="center">의미</th>
<th align="center">부수 효과</th>
<th align="center">예시</th>
</tr>
</thead>
<tbody><tr>
<td align="center">+</td>
<td align="center">덧셈</td>
<td align="center">X</td>
<td align="center"><code>5 + 2 =&gt; 7</code></td>
</tr>
<tr>
<td align="center">-</td>
<td align="center">뺄셈</td>
<td align="center">X</td>
<td align="center"><code>5 - 2 =&gt; 3</code></td>
</tr>
<tr>
<td align="center">*</td>
<td align="center">곱셈</td>
<td align="center">X</td>
<td align="center"><code>5 * 2 =&gt; 10</code></td>
</tr>
<tr>
<td align="center">/</td>
<td align="center">나눗셈</td>
<td align="center">X</td>
<td align="center"><code>5 / 2 =&gt; 2.5</code></td>
</tr>
<tr>
<td align="center">%</td>
<td align="center">나머지</td>
<td align="center">X</td>
<td align="center"><code>5 % 2 =&gt; 1</code></td>
</tr>
</tbody></table>
<h3 id="단항-산술-연산자">단항 산술 연산자</h3>
<p>1개의 피연산자를 산술 연산하여 숫자 값을 만든다.</p>
<table>
<thead>
<tr>
<th align="center">단항 산술 연산자</th>
<th align="center">의미</th>
<th align="center">부수 효과</th>
<th align="center">예시</th>
</tr>
</thead>
<tbody><tr>
<td align="center">++</td>
<td align="center">증가</td>
<td align="center">O</td>
<td align="center"><code>x++ =&gt; x + 1</code></td>
</tr>
<tr>
<td align="center">--</td>
<td align="center">감소</td>
<td align="center">O</td>
<td align="center"><code>x-- =&gt; x - 1</code></td>
</tr>
<tr>
<td align="center">+</td>
<td align="center">문자열 숫자, 불리언을 숫자 타입으로 변환</td>
<td align="center">X</td>
<td align="center"><code>+&#39;10&#39; =&gt; 10</code></td>
</tr>
<tr>
<td align="center">-</td>
<td align="center">양수 -&gt; 음수, 음수 -&gt; 양수</td>
<td align="center">X</td>
<td align="center"><code>+(-10) =&gt; -10</code></td>
</tr>
</tbody></table>
<p>증가/감소(++/--) 연산자는 위치에 따라 연산이 다르게 수행된다</p>
<pre><code>x = 5;
// 선할당 후증감
result = x++;
console.log(result, x); // 5 6
result = x--;
console.log(result, x); // 6 5

// 선증감 후할당
result = ++x;
console.log(result, x); // 6 6
result = --x;
console.log(result, x); // 5 5</code></pre><p>또한 + 연산자는 피연사자 중 하나 이상이 문자열인 경우 문자열 연결 연산자로 동작한다.</p>
<pre><code class="language-js">&#39;1&#39; + 2; // &#39;12&#39;
1 + &#39;2&#39;; // &#39;12&#39;</code></pre>
<h2 id="할당-연산자">할당 연산자</h2>
<p>우항에 있는 피연산자의 결과 값을 좌항에 있는 변수에 할당한다.</p>
<p>따라서 할당 연산자는 좌항에 있는 변수의 값이 변하는 부수 효과가 있다.</p>
<table>
<thead>
<tr>
<th align="center">할당 연산자</th>
<th align="center">예시</th>
<th align="center">동일 표현</th>
<th align="center">부수 효과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">=</td>
<td align="center">x = 5</td>
<td align="center">x = 5</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">+=</td>
<td align="center">x += 5</td>
<td align="center">x = x + 5</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">-=</td>
<td align="center">x -= 5</td>
<td align="center">x = x - 5</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">*=</td>
<td align="center">x *= 5</td>
<td align="center">x = x * 5</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">/=</td>
<td align="center">x /= 5</td>
<td align="center">x = x / 5</td>
<td align="center">O</td>
</tr>
<tr>
<td align="center">%=</td>
<td align="center">x %= 5</td>
<td align="center">x = x % 5</td>
<td align="center">O</td>
</tr>
</tbody></table>
<p>이러한 할당문은 표현식으로 할당된 값으로 평가되어 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-js">console.log(x = 10); // 10

// 연쇄 할당, 오른쪽에서 왼쪽으로 진행
a = b = c = 0;
console.log(a, b, c); // 0 0 0</code></pre>
<h2 id="비교-연산자">비교 연산자</h2>
<p>좌항과 우항의 피연산자를 비교한 뒤 결과를 불리언 값으로 변환한다.</p>
<p>불리언 값이기 때문에 if문이나 for문과 같은 제어문의 조건식으로 주로 사용한다.</p>
<h3 id="동등일치-비교-연산자">동등/일치 비교 연산자</h3>
<p>동등 비교 연산자와 일치 비교 연산자의 차이는 엄격함의 정도가 다르다.</p>
<p>동등 비교 연산자(==)는 느슨한 비교로 값만 같으면 되지만, 일치 비교 연산자(===)는 엄격한 비교로 값과 타입이 같아야 한다.</p>
<table>
<thead>
<tr>
<th align="center">비교 연산자</th>
<th align="center">의미</th>
<th align="center">예시</th>
<th align="center">설명</th>
<th align="center">부수 효과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">==</td>
<td align="center">동등 비교</td>
<td align="center">x == y</td>
<td align="center">x와 y의 값이 같음</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">===</td>
<td align="center">일치 비교</td>
<td align="center">x === y</td>
<td align="center">x와 y의 값과 타입이 같음</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">!=</td>
<td align="center">부동등 비교</td>
<td align="center">x != y</td>
<td align="center">x와 y의 값이 다름</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">!==</td>
<td align="center">불일치 비교</td>
<td align="center">x !== y</td>
<td align="center">x와 y의 값과 타입이 다름</td>
<td align="center">X</td>
</tr>
</tbody></table>
<p>동등 비교 연산자는 좌항과 우항의 피연산자를 비교할 때 암묵적 타입 변환을 통해 타입을 일치 시킨 뒤 값을 비교하여 서로 다른 타입이더라도 값이 같다면 true를 반환하게 된다.</p>
<pre><code class="language-js">5 == 5; // true

// 타입은 다르지만 암묵적 타입 변환으로 타입을 일치 시키면 값이 일치하기 때문에 true다.
5 == &#39;5&#39;; // true

// 안티 패턴
&#39;0&#39; == &#39;&#39;; // false
0 == &#39;&#39;; // true
0 == &#39;0&#39;; // true
false == &#39;false&#39; // false
false == &#39;0&#39; // true
false == null // false
false == undefined // false</code></pre>
<p>동등 비교 연산자는 이러한 안티 패턴이 있기에 예측하기 어려운 결과를 만들 수 있으므로 일치 비교 연산자를 사용하는 것이 좋다.</p>
<p>일치 비교 연산자는 암묵적 타입 변환 없이 좌항과 우항의 타입과 값이 동일해야 true를 반환한다.</p>
<pre><code class="language-js">5 === 5; // true

// 타입이 서로 다르기 때문에 false다.
5 === &#39;5&#39;; // false

// NaN은 자신과 일치하지 않는 유일한 값이다.
NaN === NaN; // false

// isNaN 함수를 통해 NaN인지 확인이 가능하다.
isNaN(NaN); // true
isNaN(10); // false
isNaN(1 + undefined); // true

// 양의 0과 음의 0의 비교는 모두 true를 반환한다.
0 === -0; // true
0 == -0; // true</code></pre>
<blockquote>
<p>Object.is 메서드는 일치 비교 연산자와 비슷하게 정확한 비교 결과를 반환하지만 조금 다른점이 있다.
<code>Object.is(-0, +0) =&gt; false</code>
<code>Object.is(NaN, NaN) =&gt; true</code>
이 두개를 제외하고는 일치 비교 연산자와 동일한 결과를 반환한다.</p>
</blockquote>
<h3 id="대소-관계-비교-연산자">대소 관계 비교 연산자</h3>
<p>피연산자의 크기를 비교하여 불리언 값을 반환한다.</p>
<table>
<thead>
<tr>
<th align="center">대소 관계 비교 연산자</th>
<th align="center">예시</th>
<th align="center">설명</th>
<th align="center">부수 효과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">&gt;</td>
<td align="center">x &gt; y</td>
<td align="center">x가 y보다 크다</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">&lt;</td>
<td align="center">x &lt; y</td>
<td align="center">x가 y보다 작다</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">&gt;=</td>
<td align="center">x &gt;= y</td>
<td align="center">x가 y보다 크거나 같다</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">&lt;=</td>
<td align="center">x &lt;= y</td>
<td align="center">x가 y보다 작거나 같다</td>
<td align="center">X</td>
</tr>
</tbody></table>
<h2 id="삼항-조건-연산자">삼항 조건 연산자</h2>
<p>조건식의 결과 값에 따라 반환할 값을 결정한다.</p>
<p>사용법은 <code>조건식 ? 조건식이 true일 때 반환 값 : 조건식이 false일 때 반환 값</code>으로 표현한다.</p>
<p>if - else는 표현식이 아니지만 삼항 조건 연산자는 표현식으로 사용할 수 있다.</p>
<pre><code class="language-js">const x = 2;
let result = x % 2 ? &#39;홀수&#39; : &#39;짝수&#39;;

console.log(result); // 짝수

// 아래와 동일하게 동작한다.
if(x % 2) result = &#39;홀수&#39;;
else result = &#39;짝수&#39;;

console.log(result); // 짝수
</code></pre>
<h2 id="논리-연산자">논리 연산자</h2>
<p>우항과 좌항의 피연산자를 논리 연산한다.</p>
<p>만약 부정 논리 연산자의 경우 우항의 피연산자를 논리 연산한다.</p>
<table>
<thead>
<tr>
<th align="center">논리 연산자</th>
<th align="center">의미</th>
<th align="center">부수 효과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">||</td>
<td align="center">논리합(OR)</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">&amp;&amp;</td>
<td align="center">논리곱(AND)</td>
<td align="center">X</td>
</tr>
<tr>
<td align="center">!</td>
<td align="center">부정(NOT)</td>
<td align="center">X</td>
</tr>
</tbody></table>
<pre><code class="language-js">// 논리합
true || false; // true
false || true; // true
// 위 두 개의 비교는 같은 true를 반환한다.
// 하지만 첫 번째는 앞의 값이 true이기 때문에 뒤에 오는 값은 검사하지 않고 true를 반환한다.
// 두 번째는 앞의 값이 false이기 떄문에 뒤에 값까지 검사해서 true를 반환한다.

// 논리곱
true &amp;&amp; false; // false
false &amp;&amp; true; // false
// 여기서도 같은 false를 반환한다.
// 하지만 첫 번째는 앞이 true이기 때문에 뒤에 값까지 확인해야 결과를 알 수 있다.
// 두 번째는 앞이 false이기 떄문에 뒤에 값은 확인하지 않아도 false가 된다.</code></pre>
<h2 id="쉼표-연산자">쉼표 연산자</h2>
<p>왼쪽 피연산자부터 차례대로 피연산자를 평가하여 마지막 피연산자의 평가가 끝나면 마지막 피연산자의 결과를 반환한다.</p>
<pre><code class="language-js">var x, y, z;

x = 1, y = 2, z = 3; // 마지막 피연산자의 값 3을 반환</code></pre>
<h2 id="그룹-연산자">그룹 연산자</h2>
<p>소괄호로 감싸는 그룹 연산자는 자신의 피연산자인 표현식을 가장 먼저 평가한다.</p>
<p>그룹 연산자를 사용하면 연산자의 우선 순위를 조절할 수 있고, 그룹 연산자가 가장 높다.</p>
<pre><code class="language-js">10 * 2 + 3; // 23

10 * (2 + 3); //50</code></pre>
<h2 id="typeof-연산자">typeof 연산자</h2>
<p>피연산자의 데이터 타입을 문자열로 반환한다.</p>
<p>typeof 연산자는 7가지 타입 <code>string</code>, <code>number</code>, <code>boolean</code>, <code>undefined</code>, <code>symbol</code>, <code>object</code>, <code>function</code> 중 하나를 반환한다.</p>
<p>typeof 연산자로 null을 연산하면 <code>object</code>를 반환하는데, 이는 자바스크립트의 첫 번째 버그다. 하지만 기존 코드에 영향을 줄 수 있어 아직까지 수정되지 못하고 있다.</p>
<p>따라서 값이 null 타입인지 확인할 때는 일치 비교 연산자(===)을 사용해야 한다.</p>
<blockquote>
<p>선언하지 않은 식별자를 typeof 연산자로 연산하면 undefined를 반환하는 것도 주의하자.</p>
</blockquote>
<h2 id="지수-연산자">지수 연산자</h2>
<p>ES7에서 도입되었으며 좌항의 피연산자에 대해 우항의 피연산자를 지수 거듭 제곱하여 숫자 값을 반환한다.</p>
<pre><code class="language-js">2 ** 2; // 4
2 ** 2.5; // 5.65685424949238
2 ** 0; // 1
2 ** -2; // 0.25</code></pre>
<p>지수 연산자 이전에는 <code>Math.pow</code> 메서드를 통해 값을 반환했다.</p>
<h2 id="그-외의-연산자">그 외의 연산자</h2>
<table>
<thead>
<tr>
<th align="center">연산자</th>
<th align="center">개요</th>
<th align="center">예시</th>
</tr>
</thead>
<tbody><tr>
<td align="center">?.</td>
<td align="center">옵셔널 체이닝 연산자</td>
<td align="center">user?.data?.name =&gt; undefined 에러가 발생하지 않음</td>
</tr>
<tr>
<td align="center">??</td>
<td align="center">null 병합 연산자</td>
<td align="center">a ?? b =&gt; a가 null이나 undefined가 아니면 a 그 외는 b</td>
</tr>
<tr>
<td align="center">delete</td>
<td align="center">프로퍼티 삭제</td>
<td align="center">delete user.name =&gt; true값 반환하면 정상적으로 user객체의 name 프로퍼티를 삭제</td>
</tr>
<tr>
<td align="center">new</td>
<td align="center">생성자 함수를 호출할 때 사용하여 인스턴스 생성</td>
<td align="center">new function()</td>
</tr>
<tr>
<td align="center">instanceof</td>
<td align="center">좌변의 객체가 우변의 생성자 함수와 연결된 인스턴스인지 판별</td>
<td align="center">obj instanceof Class =&gt; obj가 Class를 상속받으면 true 반환</td>
</tr>
<tr>
<td align="center">in</td>
<td align="center">프로퍼티 존재 확인</td>
<td align="center">&quot;key&quot; in object =&gt; object에 key가 있으면 true 없으면 false</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 13 Server Actions]]></title>
            <link>https://velog.io/@taemin-jang/Next.js-13-Server-Actions</link>
            <guid>https://velog.io/@taemin-jang/Next.js-13-Server-Actions</guid>
            <pubDate>Sat, 04 May 2024 15:32:12 GMT</pubDate>
            <description><![CDATA[<p>서버 액션은 서버에서 실행되는 비동기 함수로써, 서버 및 클라이언트 컴포넌트에서 form 제출 및 데이터 변경을 처리하는데 사용할 수 있다.</p>
<p>즉 API를 생성하지 않더라도 함수 수준에서 서버에 직접 접근해 데이터 요청 등을 수행할 수 있다.</p>
<h3 id="규칙">규칙</h3>
<p>서버 액션을 만들려면 <code>async</code> 함수 내부 또는 파일 상단에 <code>use server</code> 지시어로 선언해야한다.</p>
<pre><code class="language-tsx">// Server Component
export default function Page() {
  // Server Action
  async function create() {
    &#39;use server&#39;

    // ...
  }

  return (
    // ...
  )
}</code></pre>
<p>또는 파일 상단에 선언하면 클라이언트 컴포넌트나 서버 컴포넌트에서 가져다 사용할 수 있다.</p>
<pre><code class="language-tsx">// app/actions.ts
&#39;use server&#39;

export async function create() {
  // ...
}

// app/ui/button.tsx
import { create } from &#39;@/app/actions&#39;

export function Button() {
  return (
    // ...
  )
}</code></pre>
<h2 id="forms">Forms</h2>
<p>React는 HTML form 요소를 확장하여 <code>action</code> 프로퍼티로 서버 액션을 호출할 수 있다.</p>
<p>form에서 action이 호출되면 <code>FormData</code> 객체를 자동으로 전달 받고, 필드를 관리하기 위해 <code>useState</code>를 사용할 필요가 없이 <code>FormData</code> 메서드를 사용하여 데이터를 추출할 수 있다.</p>
<pre><code class="language-tsx">export default function Page() {
  async function createInvoice(formData: FormData) {
    &#39;use server&#39;

    const rawFormData = {
      customerId: formData.get(&#39;customerId&#39;),
      amount: formData.get(&#39;amount&#39;),
      status: formData.get(&#39;status&#39;),
    }

    // mutate data
    // revalidate cache
  }

  return &lt;form action={createInvoice}&gt;...&lt;/form&gt;
}</code></pre>
<h3 id="추가-인수-전달">추가 인수 전달</h3>
<p>만약 서버 액션에 추가 인수를 전달하기 위해서는 <code>bind</code>메서드를 사용해서 전달할 수 있다.</p>
<pre><code class="language-tsx">function AddToCart({productId}) {
  async function addToCart(productId, formData) {
    &quot;use server&quot;;
    //...
  }
  const addProductToCart = addToCart.bind(null, productId);
  return (
    &lt;form action={addProductToCart}&gt;
      &lt;button type=&quot;submit&quot;&gt;Add to Cart&lt;/button&gt;
      //&lt;input type=&quot;hidden&quot; name=&quot;productId&quot; value={productId} /&gt;
    &lt;/form&gt;
  );
}</code></pre>
<h3 id="useformstatus">useFormStatus</h3>
<p>Form이 제출되는 동안 보류 중인 상태를 표시하기 위해 <code>useFormStatus</code> 훅을 사용할 수 있다.</p>
<p>해당 훅은 특정 Form의 상태를 반환하기 때문에 <code>&lt;form&gt;</code> 요소의 자식으로 정의되어야 한다.</p>
<p>또한 훅은 클라이언트 컴포넌트에서 사용해야한다.</p>
<pre><code class="language-tsx">// app/submit-button.tsx
&#39;use client&#39;

import { useFormStatus } from &#39;react-dom&#39;

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    &lt;button type=&quot;submit&quot; disabled={pending}&gt;
      Add
    &lt;/button&gt;
  )
}

// app/page.tsx
import { SubmitButton } from &#39;@/app/submit-button&#39;
import { createItem } from &#39;@/app/actions&#39;

// Server Component
export default async function Home() {
  return (
    &lt;form action={createItem}&gt;
      &lt;input type=&quot;text&quot; name=&quot;field-name&quot; /&gt;
      &lt;SubmitButton /&gt;
    &lt;/form&gt;
  )
}</code></pre>
<h3 id="network">Network</h3>
<p>서버 액션을 통해 구현한 <a href="https://nextjs-serveractions.vercel.app/">데모 사이트</a>가 있다.</p>
<p>여기서 todo title과 body를 입력하고 <code>Add</code> 버튼을 클릭하고 네트워크 탭을 보면 다음처럼 보인다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/a093178b-fa5a-455f-9ba1-22ec4a75cdad/image.png" alt="network"></p>
<p>페이로드를 보면 ACTION_ID라는 서버 액션과 입력한 데이터를 볼 수 있다.</p>
<p>서버 액션을 실행하면 클라이언트에서 현재 라우트 주소와 ACTION_ID, 입력한 데이터를 보내게 된다.</p>
<p>그러면 서버에서 요청 받은 데이터를 바탕으로 실행해야 하는 서버 액션을 찾고 서버에서 직접 실행한다.</p>
<p><code>use server</code>로 선언 되어 있는 내용은 클라이언트 번들링에 포함되지 않고 서버에서만 실행되고, 따라서 CORS 이슈가 발생하지 않는다.</p>
<h2 id="form이-아닌-요소">Form이 아닌 요소</h2>
<p>보통 <code>&lt;form&gt;</code> 요소 내에서 서버 액션을 사용하지만, 이벤트 핸들러나 useEffect와 같은 코드 내에서도 호출할 수 있다.</p>
<pre><code class="language-tsx">// app/like-button.tsx
&#39;use client&#39;

import { incrementLike } from &#39;./actions&#39;
import { useState } from &#39;react&#39;

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    &lt;&gt;
      &lt;p&gt;Total Likes: {likes}&lt;/p&gt;
      &lt;button
        onClick={async () =&gt; {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      &gt;
        Like
      &lt;/button&gt;
    &lt;/&gt;
  )
}</code></pre>
<p><code>onClick</code>과 같은 이벤트 핸들러에서 서버 액션을 호출하여 좋아요 수를 늘릴 수 있다.</p>
<h2 id="revalidating">Revalidating</h2>
<p><code>revalidatePath API</code>를 사용하여 서버 액션 내에서 Next.js 캐시 유효성을 재검증할 수 있다.</p>
<pre><code class="language-tsx">&#39;use server&#39;

import { revalidatePath } from &#39;next/cache&#39;

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
  revalidatePath(&#39;/posts&#39;)
}</code></pre>
<p>또한 <code>revalidateTag API</code>도 동일하게 동작한다.</p>
<pre><code class="language-tsx">&#39;use server&#39;

import { revalidateTag } from &#39;next/cache&#39;

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
  revalidateTag(&#39;posts&#39;)
}</code></pre>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations">Next.js - server actions</a></li>
<li><a href="https://github.com/UGoingNoWhereBoy/Nextjs-Server-Actions">Nextjs-server-actions Github Example</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 13 App Router와 RSC 이해하기]]></title>
            <link>https://velog.io/@taemin-jang/next-13-app-router-rsc</link>
            <guid>https://velog.io/@taemin-jang/next-13-app-router-rsc</guid>
            <pubDate>Mon, 22 Apr 2024 09:44:29 GMT</pubDate>
            <description><![CDATA[<h1 id="app-디렉토리">App 디렉토리</h1>
<p>Next.js 13버전 이전까지 공통 레이아웃을 구성하려면 _app 파일 내에서만 가능했다.</p>
<p>하지만 그 방식 마저도 각 페이지별로 서로 다른 레이아웃을 적용하는데도 무리가 있었다.</p>
<blockquote>
<p>_app: app은 페이지를 초기화하기 위한 용도로 사용되며, 다음과 같은 작업이 가능하다.</p>
<ul>
<li>페이지 변경 시에 유지하고 싶은 레이아웃</li>
<li>페이지 변경 시 상태 유지</li>
<li>페이지간 추가적인 데이터 삽입</li>
<li>global CSS 주입</li>
</ul>
</blockquote>
<p>이러한 한계를 극복하기 위해 13버전 이후부터 App 디렉토리가 나오게 됐다.</p>
<h2 id="라우팅">라우팅</h2>
<p>기존 13버전 이전에는 라우팅을 /pages로 정의하던 방식이 /app 디렉터리로 이동했다.</p>
<blockquote>
<p><strong>Next.js 12이하</strong>: /pages/a/b/index.tsx =&gt; /a/b로 라우팅</p>
<p><strong>Next.js 13 app</strong>: /app/a/b =&gt; /a/b로 라우팅</p>
</blockquote>
<p>Next.js 13의 app 디렉토리 내부 파일명은 라우팅 명칭에 아무런 영향이 없고, app 내부에서 가질 수 있는 파일명은 예약어로 제한되어있다.</p>
<h3 id="layouttsx">layout.tsx</h3>
<p>layout 파일은 페이지의 기본적인 레이아웃을 구성한다. 해당 폴더에 layout이 있다면 그 하위 폴더 및 주소에 모두 영향을 미친다.</p>
<pre><code class="language-tsx">// app/layout.ts
import { ReactNode } from &#39;react&#39;;

export default function AppLayout({ children }: { children: ReactNode }) {
  return (
    &lt;html lang=&quot;ko&quot;&gt;
      &lt;head&gt;
        &lt;title&gt;안녕하세요&lt;/title&gt;
      &lt;/&gt;
      &lt;body&gt;
        &lt;h1&gt;웹 페이지입니다.&lt;/h1&gt;
        &lt;main&gt;{children}&lt;/main&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}

// app/blog/layout.tsx
import { ReactNode } from &#39;react&#39;;

export default function BlogLayout({ children }: { children: ReactNode }) {
  return &lt;section&gt;{children}&lt;/section&gt;
}</code></pre>
<p>루트 layout은 모든 페이지에 영향을 미치는 공통 레이아웃으로 단 하나의 layout만 만들 수 있다.</p>
<p>이처럼 주소별 공통 레이아웃을 만들 수 있고, layout 파일 내에서는 HTML에서 기본으로 제공하는 <code>&lt;html/&gt;</code> 태그 등을 추가하고 수정할 수 있다.</p>
<blockquote>
<p><strong>layout 파일에서 주의할 점</strong></p>
<ul>
<li>app 디렉터리 내부에서 사용되는 예약어로, 무조건 layout.{js|jsx|ts|tsx}로 사용해야 한다.</li>
<li>layout은 children을 props로 받아서 렌더링해야 한다. (children은 같은 경로 page.tsx에 작성된 내용을 전달받게 된다.)</li>
<li>layout 내부에서도 API요청과 같은 비동기 작업 수행이 가능하다. (서버 컴포넌트이므로 <code>use client</code>를 사용할 수 없다.)</li>
</ul>
</blockquote>
<h3 id="pagetsx">page.tsx</h3>
<p>app 디렉토리 안에서 page도 예약어이며, 일반적으로 이전에 다뤘던 페이지를 의미한다.</p>
<pre><code class="language-tsx">export default function BlogPage() {
  return &lt;&gt;블로그 글 내용&lt;/&gt;
}</code></pre>
<p>page가 받는 props는 다음과 같다.</p>
<ul>
<li><strong>params</strong>: 옵셔널 값으로, <code>[...id]</code>와 같은 동적 라우트 파라미터를 사용할 경우 해당 파라미터 값이 들어온다.</li>
<li><strong>searchParams</strong>: QueryString 값이 들어오게 된다. 예시로 <code>?a=1&amp;b=2</code>로 접근하면 <code>{a: &#39;1&#39;, b:&#39;2&#39; }</code>라는 객체 값이 들어온다. 해당 값은 page 내부에서만 수행가능하다.</li>
</ul>
<blockquote>
<p><strong>page 파일에서 주의할 점</strong></p>
<ul>
<li>app 디렉토리 내부에서 사용되는 예약어로, 무조건 page.{js|jsx|ts|tsx}로 사용해야 한다.</li>
<li>내부에서 반드시 export default로 내보내는 컴포넌트가 있어야 한다.</li>
</ul>
</blockquote>
<h3 id="errortsx">error.tsx</h3>
<p>해당 라우팅 영역에서 사용되는 공통 에러 컴포넌트로, 특정 라우팅별로 서로 다른 에러 UI를 렌더링할 수 있다.</p>
<pre><code class="language-tsx">&#39;use client&#39;

import { useEffect } from &#39;react&#39;

exort default function Error({ error, reset }: { error: Error, reset: () =&gt; void }) {
  useEffect(() =&gt; {
    console.log(&#39;logging error:&#39;, error);
  }, [error])

  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;strong&gt;Error:&lt;/strong&gt; {error?.message}
      &lt;/div&gt;
      &lt;div&gt;
        &lt;button onClick={() =&gt; reset()}&gt;에러 리셋&lt;/button&gt;
      &lt;/div&gt;
    &lt;/&gt;
  )
}</code></pre>
<p>2개의 props를 전달받는다.</p>
<p><code>error</code>는 에러 정보를 담고 있고,<code>reset</code>은 에러 바운더리를 초기화할 수 있다.</p>
<blockquote>
<p><strong>error 파일 주의할 점</strong></p>
<ul>
<li>에러 바운더리는 클라이언트에서만 작동하므로 error 컴포넌트도 클라이언트 컴포넌트여야 하기 때문에 상단에 <code>use client</code>를 작성했다.</li>
<li>같은 수준의 layout에서 에러가 발생할 경우 해당 error 컴포넌트로 이동하지 않는다. 그 이유는 아래와 같은 구조로 페이지가 렌더링되기 때문이다.</li>
</ul>
</blockquote>
<pre><code class="language-tsx"> &lt;Layout&gt;
  &lt;Error&gt;
    {children}
  &lt;/Error&gt;
 &lt;/Layout&gt;</code></pre>
<blockquote>
<p>만약 Layout에서 발생한 에러를 처리하려면 상위 컴포넌트에서 error를 사용하거나 app의 루트 에러처리를 하는 app/global-error.js 페이지를 사용하면 된다.</p>
</blockquote>
<h3 id="not-foundtsx">not-found.tsx</h3>
<p>not-found 파일은 특정 라우팅 하위의 주소를 찾을 수 없는 404 페이지를 렌더링할 때 사용한다.</p>
<pre><code class="language-tsx">export default function NotFound() {
  return (
    &lt;&gt;
      &lt;h2&gt;Not Found&lt;/h2&gt;
      &lt;p&gt;404&lt;/p&gt;
       &lt;/&gt;
  )
}</code></pre>
<p>error와 마찬가지로 전체 애플리케이션에서 404를 노출하려면 <code>app/not-found.tsx</code>를 생성하면 된다.</p>
<h3 id="loadingtsx">loading.tsx</h3>
<p>loading은 리액트 Suspense를 기반으로 해당 컴포넌트가 불러오는 중임을 나타낼 때 사용한다.</p>
<pre><code class="language-tsx">export default function Loading() {
  return &#39;Loading...&#39;;
}</code></pre>
<h3 id="routets">route.ts</h3>
<p>Next.js 13.4.0에서 app 디렉토리가 정식으로 출시되면서 /app/api 기준으로 디렉토리 라우팅을 지원하며 route.ts로 통일했다.</p>
<pre><code class="language-ts">// /api/users/[id]/route.ts

import { NextRequest } from &#39;next/server&#39;;

export async function GET(request: NextRequest, context: { params: { id: string }}) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${context.params.id}`);
  // ...
  return new Response(JSON.stringify(result), {
    status: 200,
    headers: {
      &#39;content-type&#39;: &#39;application/json&#39;
    },
  })
}

export async function HEAD(request: NextRequest) {}

export async function POST(request: NextRequest) {}

export async function PUT(request: NextRequest) {}

export async function DELETE(request: NextRequest) {}

export async function PATCH(request: NextRequest) {}

export async function OPTIONS(request: NextRequest) {}</code></pre>
<p>이렇게 route.ts 파일 내에서 REST API의 HTTP 메서드명을 예약어로 선언해두면 HTTP 요청에 맞게 해당 메서드를 호출한다.</p>
<p><code>route.ts</code> 파일이 있는 폴더 안에는 <code>page.tsx</code>가 존재할 수 없다.</p>
<ul>
<li><strong>request</strong>: NextRequest 객체이며, fetch의 Request를 확장한 Next.js만의 Request이다. 이 객체에는 cookie, headers뿐 아니라 nextUrl 같은 주소 객체도 확인할 수 있다.</li>
<li><strong>context</strong>: params만을 가지고 있는 객체로, 동적 라우팅 파라미터 객체가 포함된다. <code>/app/api/users/[id]/route.ts</code>라면 <code>id</code>값을 사용할 수 있다.</li>
</ul>
<h1 id="리액트-서버-컴포넌트-rsc">리액트 서버 컴포넌트 (RSC)</h1>
<p>리액트 18에서 새로 도입된 리액트 서버 컴포넌트는 서버 사이드 렌더링과 완전히 다른 개념이다.</p>
<p>기존 리액트로 앱을 개발할 때 문제가 있다.</p>
<ol>
<li><p>컴포넌트가 렌더링될 때 레이아웃이 갑자기 이동되는 문제</p>
<pre><code class="language-jsx">&lt;CourseWrapper&gt;
&lt;CourseList /&gt; // fetch로 데이터 호출
&lt;Testimonials /&gt; // fetch로 데이터 호출
&lt;/CourseWrapper&gt;</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/layoutshift-1.gif" alt="fetch">
위 그림과 같이 <code>Testimonials</code> 컴포넌트가 먼저 렌더링되고 그 다음에 <code>CourseList</code> 컴포넌트가 렌더링되면서 레이아웃이 이동하는 문제가 발생한다.</p>
</li>
<li><p>Waterfall 문제</p>
<pre><code class="language-jsx">function Course() {
return (
 &lt;CourseWrapper&gt; // fetch로 데이터 호출
   &lt;CourseList /&gt; // fetch로 데이터 호출
   &lt;Testimonials /&gt; // fetch로 데이터 호출
 &lt;/CourseWrapper&gt;
)
}</code></pre>
<p>부모 컴포넌트인 <code>CourseWrapper</code>가 먼저 호출하고 데이터를 가져오는 동안 자식 컴포넌트의 렌더링은 일어나지 않는다.</p>
</li>
</ol>
<p>이처럼 현재 작업을 시작하기 위해 이전 작업이 완료되기를 기다리는 것을 <code>Waterfall</code>이라고 부른다.</p>
<p>이러한 Waterfall 문제를 해결하기 위해 각각의 컴포넌트에서 호출하는 것이 아닌 부모 컴포넌트에서 단일 호출하도록 네트워크 호출 로직을 끌어 올릴 수 있다.</p>
<ol start="3">
<li><p>유지 보수 문제</p>
<pre><code class="language-jsx">function Course() {
 const info = fetchAllDetails();
 return(
     &lt;CourseWrapper
         ino={info.wrapperInfo} &gt;
         &lt;CourseList
             ino={info.listInfo} /&gt;
         &lt;Testimonials
             ino={info.testimonials} /&gt;
     &lt;/CourseWrapper&gt;     
 )
}</code></pre>
<p>이렇게 최상위 컴포넌트에서 API 호출을 하고 자식 컴포넌트에 Props로 전달하면 자식 컴포넌트는 부모 컴포넌트에 종속되어 재사용하기 어려워지는 유지 보수 측면에서 문제가 발생할 수 있다.</p>
</li>
<li><p>성능 비용 문제
리액트는 CSR로써 클라이언트에서 애플리케이션을 로드하기 위해 필요한 리소스를 다운로드하게 된다.</p>
</li>
</ol>
<p>만약 해당 애플리케이션에 종속되어 있는 외부 라이브러리가 많다면, 해당 리소스를 다운로드 하는 시간이 더욱 길어지고 이는 UX에 좋지 않은 영향을 미칠 수 있다.</p>
<p>이러한 문제들을 해결하기 위해 리액트 서버 컴포넌트가 등장하게 된다.</p>
<h2 id="서버-컴포넌트">서버 컴포넌트</h2>
<p>서버와 클라이언트 모두에서 컴포넌트를 렌더링할 수 있는 기법을 서버 컴포넌트라고 한다.</p>
<p>서버에서 할 수 있는 일은 서버가 처리하고, 그 외는 클라이언트인 브라우저에서 수행한다.</p>
<p>이러한 서버 컴포넌트는 서버에 있으므로 데이터를 더 빨리 가져올 수 있으며, 파일 시스템 및 데이터 저장소와 같은 서버 인프라에 접근할 수 있다.</p>
<p>여기서 명심해야할 것은 클라이언트 컴포넌트는 서버 컴포넌트를 import할 수 없다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/69e743ab-aecd-4f69-ac29-010f7f14bd71/image.png" alt="RSC"></p>
<p>이런 구조가 가능한 이유는 <code>children</code>으로 자주 사용하는 <code>ReactNode</code>에 있다.</p>
<pre><code class="language-tsx">// ClientComponent.tsx
&#39;use client&#39;
// import ServerComponent from &#39;./ServerComponent 이렇게 클라이언트 컴포넌트에서 서버 컴포넌트를 불러올 수 없다.
export default function ClientComponent({ children }: { children: React.ReactNode }) {
  return (
    &lt;div&gt;
      &lt;h1&gt;클라이언트 컴포넌트&lt;/h1&gt;
      {children}
    &lt;/div&gt;
  )
}

// ServerComponent.tsx
export default function ServerComponent() {
  return &lt;span&gt;서버 컴포넌트&lt;/span&gt;
}

// ParentServerComponent.tsx
// 이 컴포넌트는 서버 컴포넌트 or 클라이언트 컴포넌트일 수 있기에 두 컴포넌트 모두 사용할 수 있다.
import ClientComponent from &#39;./ClientComponent&#39;;
import ServerComponent from &#39;./ServerComponent&#39;;
export default function ParentServerComponent() {
  return (
    &lt;ClientComponent&gt;
      &lt;ServerComponent/&gt;
    &lt;/ClientComponent&gt;
  )
}</code></pre>
<p><strong>서버 컴포넌트</strong></p>
<ul>
<li>요청이 오면 그 순간 서버에서 딱 한 번 실행되기에 상태를 가질 수 없다. 따라서 리액트에서 사용하는 훅을 사용할 수 없다.</li>
<li>렌더링 생명주기도 사용할 수 없다.</li>
<li>effect나 state에 의존하는 Custom hook도 사용할 수 없고, 서버에서 제공할 수 있는 기능만 사용하는 훅은 사용할 수 있다.</li>
<li>브라우저에서 실행되지 않으므로 DOM API나 window, document에 접근할 수 없다.</li>
<li>DB, 파일 시스템 등 서버에 있는 데이터를 async/await으로 접근할 수 있다.</li>
<li>다른 서버 컴포넌트나 클라이언트 컴포넌트 렌더링이 가능하다.</li>
</ul>
<p><strong>클라이언트 컴포넌트</strong></p>
<ul>
<li>브라우저 환경에서만 실행되므로 서버 컴포넌트를 불러오거나, 서버 전용 훅 등을 불러올 수 없다.</li>
<li>클라이언트 컴포넌트가 자식으로 서버 컴포넌트를 갖는 구조는 가능하다.<blockquote>
<p>서버 컴포넌트를 직접 호출하는 것은 브라우저 환경이라 컴포넌트 트리를 생성하는게 불가능하지만, 자식으로 가지면 클라이언트는 <code>이미 만들어진</code> 서버 컴포넌트 트리를 보여주기만 하면 되기 때문에 가능하다.</p>
</blockquote>
</li>
<li>일반적으로 우리가 아는 리액트 컴포넌트와 같고, 상태관리 훅, 브라우저 API를 사용할 수 있다.</li>
</ul>
<p><strong>공용 컴포넌트</strong></p>
<ul>
<li>이 컴포넌트는 서버와 클라이언트 모두 사용 가능하다.</li>
<li>기본적으로 모든 컴포넌트를 공용으로 판단하고 명시적으로 <code>use client</code>를 작성하면 클라이언트 컴포넌트라고 판단한다.</li>
</ul>
<h2 id="리액트-서버-컴포넌트와-서버-사이드-렌더링">리액트 서버 컴포넌트와 서버 사이드 렌더링</h2>
<p>리액트는 기본적으로 클라이언트 사이드 렌더링(CSR)으로 동작한다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/b21b63ef-5273-4a96-9017-84fd439e568a/image.png" alt="CSR"></p>
<p>애플리케이션 페이지 진입 시 HTML, 자바스크립트와 모든 데이터가 로드되고 컴포넌트 렌더링이 끝나기 전까지 사용자는 아무런 기능이 없는 빈 화면만 보게 된다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/62f85526-e5ff-4ee3-9b0e-5059316fffe4/image.png" alt="SSR"></p>
<p>반면 서버 사이드 렌더링(SSR)은 서버에서 먼저 HTML로 렌더링 한다.</p>
<p>그리고 자바스크립트 번들을 모두 다운로드하고 하이드레이션이 완료되어야 정상적으로 페이지가 동작하게 된다.</p>
<p>그렇다면 리액트 서버 컴포넌트는 서버 사이드 렌더링과 어떻게 다를까?</p>
<ul>
<li>서버 컴포넌트의 코드는 클라이언트로 전달되지 않지만, SSR의 모든 컴포넌트 코드는 자바스크립트 번들에 포함되어 클라이언트로 전송된다.</li>
<li>서버 컴포넌트는 클라이언트 상태를 유지하며 refetch 될 수 있다. 그 이유는 HTML이 아닌 특별한 형태로 컴포넌트를 전달하기 때문에 클라이언트 상태를 유지하며 여러번 데이터를 가져오고 리렌더링하여 전달할 수 있다.
하지만 SSR의 경우 HTML로 전달되기 때문에 refetch를 하게 되면 HTML 전체를 리렌더링하므로 클라이언트 상태를 유지할 수 없다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/b37808c6-2241-4d11-b0d2-e51809f73644/image.png" alt="RSC_RESPONSE"></li>
</ul>
<p>서버 사이드 렌더링으로 초기 HTML 페이지를 빠르게 보여주고, 서버 컴포넌트로는 클라이언트로 전송되는 자바스크립트 번들 사이즈를 감소시킨다면 사용자에게 훨씬 빠르게 인터렉티브한 페이지를 제공할 수 있다.</p>
<h1 id="nextjs에서-rsc">Next.js에서 RSC</h1>
<p>Next.js 13 App 디렉토리에서는 과거 서버 사이드 렌더링과 정적 페이지 제공을 위해 이용하던 <code>getServerSideProps</code>, <code>getStaticProps</code>, <code>getInitialProps</code>가 /app 디렉토리 내부에서는 삭제됐다.</p>
<h2 id="fetch-api의-확장">Fetch API의 확장</h2>
<p>그 대신 모든 데이터 요청은 웹에서 제공하는 표준 API인 fetch를 기반으로 이뤄진다.</p>
<pre><code class="language-tsx">async function getData() {
  const result = await fetch(&#39;https://api.example.com/&#39;);

  if (!result.ok) {
    // 이렇게 에러를 던지면 가장 가까운 에러 바운더리에 전달된다.
    throw new Error(&#39;데이터 불러오기 실패&#39;);
  }
  return result.json();
}

// async 서버 컴포넌트 페이지
export default async function Page() {
  const data = await getData();

  return (
    &lt;main&gt;
      &lt;Children data={data} /&gt;
    &lt;/main&gt;
  )
}</code></pre>
<p>또한 fetch API를 확장해 같은 서버 컴포넌트 트리 내에서 동일한 요청이 있다면 재요청이 발생하지 않도록 중복 제거했다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/52063232-be72-4e28-98c8-c2c7f7fefd28/image.png" alt="fetch"></p>
<p>React Query와 비슷하게 fetch 요청에 대한 내용을 서버에서 캐싱하여 클라이언트에서 별도의 요청이 없는 이상 데이터를 최대한 캐싱해서 중복된 요청을 방지한다.</p>
<h2 id="정적-렌더링과-동적-렌더링">정적 렌더링과 동적 렌더링</h2>
<p>Next.js 13에서는 정적인 라우팅은 기본적으로 빌드 타임에 렌더링을 미리 해두고 캐싱해서 재사용할 수 있게 한다.</p>
<p>동적인 라우팅에 대해서는 서버에 매번 요청이 올 때마다 컴포넌트를 렌더링한다.</p>
<pre><code class="language-jsx">  // 주소가 정적으로 결정돼 있기 때문에 정적 렌더링으로 동작한다.
  const staticRendering = await fetch(&#39;https://jsonplaceholder.typicode.com/posts&#39;);
  // no-cache 옵션으로 캐싱하지 않도록 설정해서 동적 렌더링으로 동작한다.
  // Next.js에서 제공하는 {next: {revalidate: 0}} 옵션을 사용해도 동일하게 동작한다.
  // 또한 Next.js에서 제공하는 next.headers나 next/cookie를 사용해도 동적 렌더링으로 동작한다.
  const dynamicRendering = await fetch(&#39;https://jsonplaceholder.typicode.com/posts&#39;, {cache: &#39;no-cache&#39;}); </code></pre>
<p>만약 해당 데이터의 유효한 시간을 정해두고 해당 시간이 지나면 다시 데이터를 불러와 렌더링하려면 revalidate를 사용하면 된다.</p>
<pre><code class="language-tsx">// app/page.tsx

// revalidate라는 변수를 선언해서 페이지별로 정의 가능
export const revalidate = 60;

// fetch 옵션으로 제공하는 것도 가능
  const revalidateRendering = await fetch(&#39;https://jsonplaceholder.typicode.com/posts&#39;, {next: {revalidate: 60}}); </code></pre>
<ol>
<li>최초 요청 시 미리 정적으로 캐시한 데이터를 보여준다.</li>
<li>이 캐시된 요청은 revalidate에 선언된 값만큼 유지된다.</li>
<li>만약 해당 시간이 지나도 일단 캐시된 데이터를 보여주지만, 백그라운드에서 다시 데이터를 불러온다.</li>
<li>요청한 작업이 성공적이면 캐시된 데이터를 갱신하고, 그렇지 않다면 과거 데이터를 보여준다.</li>
</ol>
<h2 id="스트리밍">스트리밍</h2>
<p>waterfall 문제를 스트리밍을 활용하면 해결할 수 있다.</p>
<p>모든 컴포넌트를 기다리는 대신, 컴포넌트가 완성되는 대로 클라이언트에 보여주면 사용자는 페이지가 완성될 때까지 기다릴 필요가 없고, 페이지가 로딩 중이라는 인식을 명확하게 심어줄 수 있다.</p>
<ul>
<li>경로에 loading.tsx 생성
loading은 예약어로, 렌더링이 완료되기 전에 보여줄 수 있는 컴포넌트다.
자동으로 다음 구조처럼 Suspense와 같이 배치된다.<pre><code class="language-tsx">&lt;Layout&gt;
&lt;Header /&gt;
&lt;SideNav /&gt;
&lt;Suspense fallback={&lt;Loading /&gt;}&gt;
  &lt;Page /&gt;
&lt;/Suspense&gt;
&lt;/Layout&gt;</code></pre>
</li>
</ul>
<p>또는 Suspense를 직접 배치하면서 더욱 효율적으로 사용할 수 있다.</p>
<h3 id="참고">참고</h3>
<ul>
<li>모던 리액트 Deep Dive</li>
<li><a href="https://nextjs.org/docs/app/building-your-application/caching">Next.js - caching</a></li>
<li><a href="https://tech.kakaopay.com/post/react-server-components/">kakaopay - React 18: 리액트 서버 컴포넌트 준비하기</a></li>
<li><a href="https://www.freecodecamp.org/korean/news/how-to-use-react-server-components/">React 서버 컴포넌트를 사용해야 하는 이유와 방법</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트 배포 방법 (with Next.js App Router)]]></title>
            <link>https://velog.io/@taemin-jang/%ED%94%84%EB%A1%A0%ED%8A%B8-%EB%B0%B0%ED%8F%AC-%EB%B0%A9%EB%B2%95-with-Next.js-App-Router</link>
            <guid>https://velog.io/@taemin-jang/%ED%94%84%EB%A1%A0%ED%8A%B8-%EB%B0%B0%ED%8F%AC-%EB%B0%A9%EB%B2%95-with-Next.js-App-Router</guid>
            <pubDate>Sat, 20 Apr 2024 17:00:33 GMT</pubDate>
            <description><![CDATA[<p>CSR과 SSG 렌더링 방식의 배포는 대부분 SSR 배포와는 다르게 어렵지 않다.</p>
<p>SSR렌더링 방식을 지원하는 Next.js에서 App Router를 배포해보는 방식에 대해 알아보려고한다.</p>
<h1 id="saas-서비스">SaaS 서비스</h1>
<p>SaaS란 Software as a Service의 줄임말로  브라우저를 통해 사용자에게 애플리케이션을 제공하고, 해당 서비스를 사용하면 서비스 유지 관리 방식이나 인프라 관리 방식에 대해 고민할 필요 없이 사용이 가능하다.</p>
<p>주로 이러한 SaaS 서비스는 정적 사이트를 쉽게 배포할 수 있다.</p>
<h2 id="netlify">Netlify</h2>
<p>대표적으로 Netlify는 프론트엔드 스택(JAMstack)으로만 구성된 정적 애플리케이션을 배포하는 용도로 최적화된 서비스다.</p>
<h3 id="추가-기능">추가 기능</h3>
<p>배포외에도 추가 기능을 제공해준다.</p>
<ul>
<li>알림: 배포와 관련된 알림을 제공, 배포 성공 및 실패와 같은 내용들</li>
<li>도메인 연결: 별도로 구매한 외부 도메인을 Netlify DNS를 통해 연결 가능</li>
<li>서비스 통합(Integrations): 다양한 서비스를 프로젝트와 연계 가능</li>
<li>서버리스 함수: 별도의 서버 구축 없이 서버에서 실행할 수 있는 함수 사용 가능</li>
<li>Identity: 인증이 포함된 웹서비스를 쉽게 처리 가능</li>
<li>사용자 초대: 한 번 생성된 팀으로 다른 개발자를 초대해 동시에 관리 가능</li>
</ul>
<h2 id="가격">가격</h2>
<p>기본적으로 무료지만 몇가지 제약 사항이 존재한다.</p>
<ul>
<li>대역폭: 월 대역폭이 최대 100GB로 제한</li>
<li>빌드 시간: 빌드에 소비할 수 있는 시간이 최대 300분</li>
<li>동시 빌드: 한 번에 한 곳만 빌드 가능</li>
</ul>
<h2 id="vercel">Vercel</h2>
<p>Vercel은 Next.js를 비롯한 Turborepo, SWC를 만든 회사이며, 클라우드 플랫폼 서비스다.</p>
<p>Vercel은 Next.js 프로젝트를 별도의 설정 없이 쉽게 배포할 수 있다.</p>
<h3 id="추가-기능-1">추가 기능</h3>
<ul>
<li>알림: 특정 타깃 브랜치에 커밋이나 PR이 발생하면 알림을 보내주는 기능</li>
<li>도메인 연결: 별도로 구매한 도메인을 연결해서 사용 가능</li>
<li>Serverless Function: 서버 없이 함수를 클라우드에 구축해서 실행 가능<blockquote>
<p>여기서 특이한 점은 Next.js에서 사용하는 app/api도 서버리스 함수로 구분되어 접근 로그나 오류 등을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/6eaf2dba-e2bb-44d1-8f9c-be70a1b500cc/image.png" alt="Serverless_Function"></p>
</blockquote>
</li>
<li>다양한 템플릿 지원: 별도의 코드 작성 없이 구축할 수 있는 기본적인 웹 사이트를 제공 (블로그, 쇼핑몰 등)</li>
</ul>
<h3 id="가격-1">가격</h3>
<p>무료로 사용이 가능하지만 약간의 제약이 있다.</p>
<ul>
<li>대역폭: 월 대역폭이 최대 100GB로 제한</li>
<li>이미지 최적화: Vercel은 사이트에서 제공해주는 이미지 최적화를 사용하는데, 이 최적화 이미지가 1000개로 제한</li>
<li>서버리스 함수: 함수의 총 실행 시간이 100GB로 제한, 함수 실행 시간은 10초 이내로 제한(만약 10초 이상 걸리면 타임아웃 처리)</li>
<li>동시 빌드: 동시에 하나만 빌드 가능</li>
<li>배포: 하루에 100개로 제한</li>
</ul>
<h1 id="aws">AWS</h1>
<h2 id="amplify">Amplify</h2>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/0ef0d97f-308c-4547-ae68-854b1ac36ebf/image.png" alt="amplify"></p>
<p>AWS 기반의 풀스택 애플리케이션을 빠르게 구성할 수 있도록 도와주는 프레임워크다.</p>
<p>Amplify를 이용하면 풀 스택 웹 및 모바일 애플리케이션을 손 쉽게 빌드 및 배포할 수 있다.</p>
<p>또한 서버리스로써 실제 사용한만큼의 비용만 지불하여 가격적인 부담도 덜 수 있다.</p>
<h3 id="1-amplify-hosting">1. Amplify Hosting</h3>
<p>다음은 실제 AWS Amplify를 통해 배포하는 과정이다.</p>
<blockquote>
<p><strong>AWS Amplify 콘솔에 접속하여 Amplify Hosting 시작하기 버튼을 클릭</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/b7d12cba-717e-4ad0-96a4-e60449bebe5b/image.png" alt="amplify1"></p>
</blockquote>
<blockquote>
<p><strong>Next.js 프로젝트 있는 저장소를 선택 후 계속 버튼 클릭</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/3f79ef4d-d6ba-4cc8-9d7d-953214e82ed7/image.png" alt="amplify2"></p>
</blockquote>
<blockquote>
<p><strong>Github에 로그인한 뒤 AWS Amplify에게 접근 권한 허용</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/6fd5d236-4345-49a6-8e04-35f5b8500ea6/image.png" alt="amplify3"></p>
</blockquote>
<blockquote>
<p><strong>배포할 Next.js 프로젝트 Repository 선택</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/f336b2f5-8858-42ba-94fd-9f7092fd276e/image.png" alt="amplify4"></p>
</blockquote>
<blockquote>
<p><strong>선택이 완료되면 AWS Amplify에서 리포지토리와 브랜치를 선택</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/c1c495a9-1d65-4463-b479-45230b7333fd/image.png" alt="amplify5"></p>
</blockquote>
<blockquote>
<p><strong>앱 이름 확인 후 고급 설정에서 환경 변수가 있으면 추가하고 저장 및 배포</strong>
[주의할 점] Next.js로 배포할 경우 환경 변수명 앞에 <code>NEXT_PUBLIC_</code>를 붙여서 사용해야한다.
<img src="https://velog.velcdn.com/images/taemin-jang/post/b4140fd9-e330-4054-a6d3-697c83e2f4a2/image.png" alt="amplify6"></p>
</blockquote>
<blockquote>
<p><strong>생성이 완료되면 Amplify가 자동으로 빌드 및 배포 진행</strong>
만약 빌드가 실패되면 빌드를 눌러서 들어가면 로그 확인 가능
<img src="https://velog.velcdn.com/images/taemin-jang/post/23281d66-78be-4724-8ab6-6c7a3ba8462c/image.png" alt="amplify7"></p>
</blockquote>
<blockquote>
<p><strong>배포가 완료되면 아래 주소를 클릭</strong>
<a href="https://main.d2vc02picn4kra.amplifyapp.com/wdlist">Next.js로 개발한 원티드 클론 사이트 배포</a>
<img src="https://velog.velcdn.com/images/taemin-jang/post/1b63e815-bf36-4965-b64d-12f5b75a5b61/image.png" alt="amplify8"></p>
</blockquote>
<blockquote>
<p><strong>배포가 정상적으로 된 것을 확인</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/53644aa4-19a5-4389-acc4-9a3e72696efa/image.png" alt="amplify9"></p>
</blockquote>
<h3 id="2-dev-브랜치-추가">2. Dev 브랜치 추가</h3>
<p>Production 단계의 배포가 아닌, 개발환경에서 배포도 관리할 수 있다.</p>
<blockquote>
<p><strong>Amplify 애플리케이션 대시보드에서 브랜치 연결 클릭</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/129baa2e-db46-4ebb-8915-2d5319544213/image.png" alt="amplify10"></p>
</blockquote>
<blockquote>
<p><strong>원하는 브랜치 선택 후 저장 및 배포 (개발 테스트를 위해 develop)</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/e69706ba-5a97-4bfc-bfd8-f8d8e3801ee7/image.png" alt="amplify11"></p>
</blockquote>
<blockquote>
<p><strong>main 브랜치와 같이 CI/CD 파이프라인 동작을 확인 가능</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/f1cb933b-e764-42ca-b9de-a5ed505478c1/image.png" alt="amplify12"></p>
</blockquote>
<h3 id="3-미리-보기">3. 미리 보기</h3>
<p>Amplify는 미리 보기 기능을 제공하여, Pull Request 요청을 병합하기 전에 변경 사항을 미리 볼 수 있다.</p>
<blockquote>
<p><strong>미리 보기 활성화 버튼을 클릭하면 볼 수 있다</strong>
<img src="https://velog.velcdn.com/images/taemin-jang/post/4c5743fc-636c-41e2-8f3e-0bfa0926b695/image.png" alt="amplify13"></p>
</blockquote>
<p>하지만 Github 애플리케이션이 설치 된 상태에서만 가능하다고 한다.</p>
<h3 id="배포-후기">배포 후기</h3>
<p>AWS Amplify로 Next.js 13 App Router를 배포해 봤다.</p>
<p>Amplify에 모든 기능을 사용해보진 않았지만 빌드 및 배포할 때 만큼은 어렵지 않게 배포할 수 있었다.</p>
<p>Amplify에서 제공하는 기능과 AWS 서비스들을 함께 이용하면 프로젝트 개발 환경을 쉽게 구축할 수 있어보인다.</p>
<p><strong>정리</strong>
정리하자면, 서비스별로 제공하는 기능과 가격을 비교해보고 현재의 상황과 Next.js 버전등을 고려해서 판단해야한다.</p>
<p>한 번 선택한 서비스를 변경하는 것은 쉽지 않기 때문이다.</p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://docs.aws.amazon.com/ko_kr/amplify/latest/userguide/environment-variables.html#amplify-console-environment-variables">Amplify 환경 변수</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 외부 상태 관리 훅 톺아보기]]></title>
            <link>https://velog.io/@taemin-jang/React-Hook%EA%B3%BC-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@taemin-jang/React-Hook%EA%B3%BC-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sat, 13 Apr 2024 16:52:39 GMT</pubDate>
            <description><![CDATA[<p>상태를 관리할 때 상태의 위치에 따라 관리할 수 있는 방법이 다르다.</p>
<h2 id="usestate와-usereducer">useState와 useReducer</h2>
<p>가장 기본적인 방법은 <code>useState</code>와 <code>useReducer</code>로 상태를 생성하고 관리할 수 있다.</p>
<p>다음은 <code>useState</code>로 <code>useCounter</code>라는 Custom Hook을 만든 코드다.</p>
<pre><code class="language-tsx">function useCounter(initCount: number = 0) {
  const [counter, setCounter] = useState(initCount);

  function inc() {
    setCounter(prev =&gt; prev + 1); 
  }

  return {counter, inc};
}

function Counter1() {
  const { counter, inc } = useCounter();

  return (
      &lt;&gt;
      &lt;h3&gt;Counter1: {counter}&lt;/h3&gt;
      &lt;button onClick={inc}&gt;+&lt;/button&gt;
    &lt;/&gt;
  )
}

function Counter2() {
  const { counter, inc } = useCounter();

  return (
      &lt;&gt;
      &lt;h3&gt;Counter2: {counter}&lt;/h3&gt;
      &lt;button onClick={inc}&gt;+&lt;/button&gt;
    &lt;/&gt;
  )
}</code></pre>
<p>useCounter라는 훅이 없었다면 각 컴포넌트에 같은 내용을 구현해야한다.</p>
<p>Custom Hook을 통해 코드를 격리하여 함수형 컴포넌트라면 어디서든 손쉽게 재사용 할 수 있다.</p>
<p><code>useReducer</code> 또한 지역 상태를 관리할 수 있는 훅으로, 실제로 <code>useState</code>는 <code>useReducer</code>로 구현됐다.</p>
<pre><code class="language-tsx">type Initializer&lt;T&gt; = T extends any ? T | ((prev: T) =&gt; T) : never;

function useStartWithUseReducer&lt;T&gt;(initialState: T) {
  const [state, dispatch] = useReducer(
    (prev: T, action: Initializer&lt;T&gt;) =&gt;
        typeof action === &#39;function&#39; ? action(prev) : action,
    initiaState,
    )

  return [state, dispatch];
}</code></pre>
<p><code>useState</code>를 <code>useReducer</code>로 구현한 예제다.</p>
<p>첫 번째 인수는 <code>reducer</code>로 action이 함수면 prev를 인자로 넘겨서 실행하고, 아니면 기존 값 그대로 반환한다.</p>
<p><code>useReducer</code> 또한 <code>useState</code>로 작성할 수 있다.</p>
<pre><code class="language-jsx">function useReducerWithUseState(reducer, initialState, initializer) { 
  const [state, setState] = useState( initializer ? () =&gt; initializer(initialState): initialState)

  const dispatch = useCallback(
    (action) =&gt; setState((prev) =&gt; reducer(prev, action)),
    [reducer],
  )

  return [state, dispatch];
}</code></pre>
<p>약간의 구현상 차이만 있을 뿐, 두 Hook 모두 지역 상태 관리를 위해 만들어졌다.</p>
<p>Hook만 가지고는 상태 관리의 모든 문제를 해결해주지 않는다.</p>
<p>예를 들어, <code>useCounter</code>는 선언될 때마다 새롭게 초기화되어, 컴포넌트별로 상태의 파편화를 만들어버린다.</p>
<p>즉, 지역 상태는 해당 컴포넌트 내에서만 유효하고 서로 다른 컴포넌트에서 하나의 상태를 바라보는게 어렵다.</p>
<h3 id="state-끌어올리기">state 끌어올리기</h3>
<p>만약 2개의 서로다른 컴포넌트에서 하나의 상태 값인 counter를 동일하게 바라보기 위해서는 상태를 컴포넌트 밖으로 한 단계 끌어올리는 <code>state 끌어올리기</code>가 있다.</p>
<pre><code class="language-tsx">function Counter1({ counter, inc }: { counter: number; inc: () =&gt; void }) {
  return (
      &lt;&gt;
      &lt;h3&gt;Counter1: {counter}&lt;/h3&gt;
      &lt;button onClick={inc}&gt;+&lt;/button&gt;
    &lt;/&gt;
  )
}

function Counter2({ counter, inc }: { counter: number; inc: () =&gt; void }) {
  return (
      &lt;&gt;
      &lt;h3&gt;Counter2: {counter}&lt;/h3&gt;
      &lt;button onClick={inc}&gt;+&lt;/button&gt;
    &lt;/&gt;
  )
}

function Parent() {
  const { counter, inc } = useCounter();

  return (
    &lt;&gt;
      &lt;Counter1 counter={counter} inc={inc} /&gt;
      &lt;Counter2 counter={counter} inc={inc} /&gt;
    &lt;/&gt;
  )
}</code></pre>
<p>이전에는 각 Counter 컴포넌트마다 <code>useCounter</code>를 사용해서 상태를 관리했었는데, 한 단계 위인 Parent 컴포넌트에서 <code>useCounter</code>를 사용해서 하위 컴포넌트의 props로 제공했다.</p>
<p>이렇게 해줌으로써 하나의 <code>useCounter</code>를 사용해 두 컴포넌트에서 동일한 상태를 관리할 수 있다.</p>
<p>상태 끌어올리기를 통해 문제를 해결할 수 있었지만, 하나의 컴포넌트에서 사용중이던 상태가 다른 컴포넌트에서 동일한 상태로 사용되어야할 경우 부모 컴포넌트로부터 props 형태로 전달 받아야한다는 점은 좋지 못하다.</p>
<p>만약 서로 다른 컴포넌트의 부모가 최상위 컴포넌트일 경우 props로 전달하게 되면 props drilling 이슈가 발생할 수 있다. (컴포넌트 트리를 재설계해야할 수도 있다.)</p>
<blockquote>
<p>Props Drilling이란?</p>
<p>Props를 상위 컴포넌트에서 하위 컴포넌트로 전달하는 것을 의미한다.</p>
<p>만약 props로 전달하는 개수가 10개 이상으로 많은 과정을 거치게 되면 해당 props를 추적하기 힘들어지는 문제가 발생하게 된다.</p>
</blockquote>
<h2 id="usestate의-상태를-바깥으로-분리">useState의 상태를 바깥으로 분리</h2>
<p>리액트에서 제공하는 <code>useState</code>가 아닌, 아예 외부로 <code>counter.ts</code>라는 파일을 만들고 <code>useState</code>와 동일하게 동작하도록 코드를 짜서 직접 상태를 관리하면 어떨까?</p>
<p>이러한 방식은 <code>리액트 환경</code>에서 동작하지 않는다.</p>
<p>코드 상으로 문제가 없지만, 여기서 가장 큰 문제는 상태가 변경됐을 때 컴포넌트가 리렌더링되지 않는다는 점이다.</p>
<p>그럼 <code>useState</code>를 리렌더링하는 역할로 잡고, 상태는 외부 파일에서 관리하면 어떨가?</p>
<pre><code class="language-ts">// counter.ts
export type State = { counter: number };

let state: State = {
  counter: 0,
};

export function get(): State {
  return state;
}

type Initializer&lt;T&gt; = T extends unknown ? T | ((prev: T) =&gt; T) : never;

export function set&lt;T&gt;(nextState: Initializer&lt;T&gt;) {
  state = typeof nextState === &#39;function&#39; ? nextState(state) : nextState;
}

// Counter.tsx
const Counter = () =&gt; {
  const state = get();
  const [count, setCount] = useState(state);

  function handleClick() {
    set((prev: State) =&gt; {
      const newState = { counter: prev.counter + 1 };
      setCount(newState);
      return newState;
    });
  }
  return (
    &lt;&gt;
      &lt;h1&gt;Counter&lt;/h1&gt;
      &lt;h3&gt;{count.counter}&lt;/h3&gt;
      &lt;button onClick={handleClick}&gt;+&lt;/button&gt;
    &lt;/&gt;
  );
};

// Home.tsx
const Home = () =&gt; {
  const state = get();
  const [count, setCount] = useState(state);

  function handleClick() {
    set((prev: State) =&gt; {
      const newState = { counter: prev.counter + 1 };
      setCount(newState);
      return newState;
    });
  }
  return (
    &lt;&gt;
      &lt;h1&gt;Home&lt;/h1&gt;
      &lt;h3&gt;{count.counter}&lt;/h3&gt;
      &lt;button onClick={handleClick}&gt;+&lt;/button&gt;
      &lt;Counter /&gt;
    &lt;/&gt;
  );
};</code></pre>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/833a2a78-2ba0-4a28-a9c6-61acca1902c9/image.gif" alt="gif1"></p>
<p>여기서 문제점은 상태가 외부 파일에서 관리되고 있지만, 리렌더링을 위해 각 컴포넌트 내부에 동일한 useState가 존재해 비효율적인 방식이다.</p>
<p>또한 실제로 버튼을 클릭하면 setState로 인해 상태가 변했을 때 리렌더링이 발생해 값이 변하는 것을 볼 수 있고, 같은 상태를 공유하고 있지만 동시에 렌더링이 되지 않는 문제가 발생한다.</p>
<p><code>useState</code>로 컴포넌트를 리렌더링해서 최신값을 가져오는 방식은 해당 컴포넌트 내에서만 유효하다. 따라서 반대쪽에 있는 다른 컴포넌트에서는 여전히 리렌더링을 일으킬게 없기 때문에 버튼을 클릭해도 다른 컴포넌트는 렌더링이 되지 않는다.</p>
<p>이러한 한계를 통해 함수 외부에서 상태를 참조하고 이를 통해 렌더링까지 일어나려면 다음과 같은 조건이 만족해야한다.</p>
<ol>
<li>window나 global에 있을 필요는 없지만 외부 어딘가에 상태를 두고 여러 컴포넌트가 같이 쓸 수 있어야 한다.</li>
<li>외부에 있는 상태를 사용하는 컴포넌트는 상태의 변경을 알아챌 수 있어야 하고, 상태가 변경될 때마다 리렌더링이 일어나야 한다. 또한 이 상태 감지는 상태를 참조하는 모든 컴포넌트에서 동일하게 작동해야 한다.</li>
<li>상태가 원시 값이 아닌 객체인 경우, 객체안에 현재 사용중이지 않는 값이 변하더라도 리렌더링이 발생해서는 안된다.
예를 들어, <code>{a: 1, b: 2}</code>라는 객체 상태가 있고, 컴포넌트에서 a를 2로 업데이트 했을 때 b만 참조하고 있는 컴포넌트에서는 리렌더링이 일어나면 안된다.</li>
</ol>
<h3 id="외부-상태-관리---createstore">외부 상태 관리 - createStore</h3>
<p>위 조건을 만족시키는 외부 상태 관리를 만들어보자.</p>
<pre><code class="language-ts">type Initializer&lt;T&gt; = T extends unknown ? T | ((prev: T) =&gt; T) : never;

type Store&lt;State&gt; = {
  get: () =&gt; State // 항상 최신 값을 가져옴
  set: (action: Initializer&lt;State&gt;) =&gt; State // 기존 useState와 동일한 역할
  subscribe: (callback: () =&gt; void) =&gt; () =&gt; void // 해당 store의 변경을 감지하고 싶은 컴포넌트들을 등록
};

export const createStore = &lt;State&gt;(
  initialState: Initializer&lt;State&gt;,
): Store&lt;State&gt; =&gt; {
  // store 내부에서 상태 관리
  let state = typeof initialState !== &#39;function&#39; ? initialState : initialState();

  // 콜백 함수를 저장하는 곳
  const callbacks = new Set&lt;() =&gt; void&gt;();

  const get = () =&gt; state;
  const set = (nextState: State | ((prev: State) =&gt; State)) =&gt; {
    state = typeof nextState === &#39;function&#39; ? (nextState as (prev: State) =&gt; State)(state) : nextState;

    // 값이 변경됐으므로 콜백 목록을 순회하면서 모든 콜백을 실행한다.
    callbacks.forEach((callback) =&gt; callback());

    return state;
  }

  // 
  const subscribe = (callback: () =&gt; void) =&gt; {
    // 콜백 등록
    callbacks.add(callback);

    // 클린업 실행 시 삭제해 반복적으로 추가되는 것 방지
    return () =&gt; {
      callbacks.delete(callback);
    }
  }

  return {get, set, subscribe};
}</code></pre>
<p><code>craeteStore</code>는 자신이 관리해야하는 상태를 내부 변수(state)로 가진 후, get 함수로 해당 변수의 최신 값을 가져올 수 있다.</p>
<p>또한 set 함수로 내부 변수를 최신 값으로 변경할 수 있고, 이 과정에서 등록된 콜백을 모조리 실행함으로써 컴포넌트의 렌더링을 유도한다.</p>
<h3 id="usestore">useStore</h3>
<p>이제 <code>createStore</code>로 만들어진 store의 값을 참조하고, 이 값에 변경에 따라 컴포넌트 렌더링을 발생시킬 사용자 정의 훅 <code>useStore</code>를 만들어보자.</p>
<pre><code class="language-ts">// useStore.tsx
const useStore = (store: Store&lt;State&gt;) =&gt; {
  // 컴포넌트의 렌더링 유도
  const [state, setState] = useState&lt;State&gt;(() =&gt; store.get());

  useEffect(() =&gt; {
    const unsubscribe = store.subscribe(() =&gt; {
      setState(store.get());
    })

    return unsubscribe;
  }, [store])
  return [state, store.set] as const;
}</code></pre>
<p>여기서 중요한 점은 useEffect 안에 작성된 부분이다.</p>
<p>useEffect는 store의 현재 값을 가져와 setState를 subscribe에 콜백 함수로 등록했다.</p>
<p>그 이유는 store 내부에서 값이 변경될 때마다 등록된 모든 callback함수를 실행하고, useStore에서 store 값이 변경되면 useEffect에서 감지하게 된다.</p>
<p>store의 값이 변경될 때마다 state의 값이 변경되는 것을 보장할 수 있다.</p>
<p>또한 클린업 함수를 통해 useEffect의 작동이 끝난 이후에는 콜백에서 해당 함수를 제거해 계속 쌓이는 것을 방지할 수 있다.</p>
<pre><code class="language-tsx">// Home.tsx
const store = createStore({ counter: 0 });

const Home = () =&gt; {
  const [state, setState] = useStore(store);

  function handleClick() {
    setState(prev =&gt; ({ counter: prev.counter + 1 }));
  }

  return (
    &lt;&gt;
      &lt;h1&gt;Home&lt;/h1&gt;
      &lt;h3&gt;{state.counter}&lt;/h3&gt;
      &lt;button onClick={handleClick}&gt;+&lt;/button&gt;
      &lt;Counter store={store} /&gt;
    &lt;/&gt;
  );
};

// Counter.tsx
const Counter = (props: { store: Store&lt;{ counter: number }&gt; }) =&gt; {
  const { store } = props;
  const [state, setState] = useStore(store);

  function handleClick() {
    setState(prev =&gt; ({ counter: prev.counter + 1 }));
  }
  return (
    &lt;&gt;
      &lt;h1&gt;Counter&lt;/h1&gt;
      &lt;h3&gt;{state.counter}&lt;/h3&gt;
      &lt;button onClick={handleClick}&gt;+&lt;/button&gt;
    &lt;/&gt;
  );
};</code></pre>
<p>실제로 실행해보면 store의 상태가 변경됨과 동시에 두 컴포넌트가 정상적으로 리렌더링 되는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/40dd1d37-08b3-48b4-b34f-64bc6e1912e0/image.gif" alt="gif2"></p>
<p>하지만 여기서도 문제가 발생할 수 있다.</p>
<p>store의 구조가 원시값이면 상관없지만 객체인 경우 일부 값만 변경한다면, 현재는 store의 값이 변경되면 무조건 리렌더링이 발생하게 된다.</p>
<h3 id="usestoreselector">useStoreSelector</h3>
<p>따라서 useStore에서 원하는 값만 변경됐을 때 리렌더링이 되도록 <code>useStoreSelector</code> Hook으로 수정해보자.</p>
<pre><code class="language-tsx">export type State = { counter: number; text: string };

const useStoreSelector = (
  store: Store&lt;State&gt;,
  selector: (state: State) =&gt; unknown, // store 값에서 어떤 값을 가져올지 정의하는 함수
) =&gt; {
  const [state, setState] = useState(() =&gt; selector(store.get()));

  useEffect(() =&gt; {
    const unsubscribe = store.subscribe(() =&gt; {
      const value = selector(store.get());
      setState(value);
    });

    return unsubscribe;
  }, [store, selector]);
  return state;
};</code></pre>
<p>이전 <code>useStore</code>와 차이점은 두 번째 인수로 selector라는 함수를 받는다.</p>
<p>setState는 값이 변경되지 않으면 리렌더링을 수행하지 않으므로 store의 값이 변경되더라도 <code>selector(store.get())</code>가 변경되지 않으면 리렌더링이 일어나지 않는다.</p>
<pre><code class="language-tsx">// Home.tsx
const store = createStore({ counter: 0, text: &#39;hi&#39; });

const Home = () =&gt; {
  return (
    &lt;&gt;
      &lt;h1&gt;useStoreSelector 사용했을 때&lt;/h1&gt;
      &lt;Text store={store} /&gt;
      &lt;Counter store={store} /&gt;
    &lt;/&gt;
  );
};

// Text.tsx
const Text = (props: { store: Store&lt;{ counter: number; text: string }&gt; }) =&gt; {
  const { store } = props;
  const text = useStoreSelector(
    store,
    useCallback(state =&gt; state.text, []),
  );
  function handleChange(e: ChangeEvent&lt;HTMLInputElement&gt;) {
    store.set(prev =&gt; ({ ...prev, text: e.target.value }));
  }

  return (
    &lt;&gt;
      &lt;h1&gt;Text&lt;/h1&gt;
      &lt;h3&gt;{text}&lt;/h3&gt;
      &lt;input value={text} onChange={handleChange} /&gt;
    &lt;/&gt;
  );
};

// Counter.tsx
const Counter = (props: {
  store: Store&lt;{ counter: number; text: string }&gt;;
}) =&gt; {
  const { store } = props;
  const counter = useStoreSelector(
    store,
    useCallback(state =&gt; state.counter, []),
  );

  function handleClick() {
    store.set(prev =&gt; ({ ...prev, counter: prev.counter + 1 }));
  }
  return (
    &lt;&gt;
      &lt;h1&gt;Counter&lt;/h1&gt;
      &lt;h3&gt;{counter}&lt;/h3&gt;
      &lt;button onClick={handleClick}&gt;+&lt;/button&gt;
    &lt;/&gt;
  );
};</code></pre>
<p>여기서 주의할 점은 selector를 컴포넌트 밖에 선언하거나, 밖에 선언이 안된다면 useCallback을 사용해 참조를 고정시켜줘야한다. 만약 이를 해주지 않으면 컴포넌트가 리렌더링될 때마다 함수도 계속 재생성되어 리렌더링이 발생할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/taemin-jang/post/8ba08e35-6b2a-4dc7-88d7-857328617618/image.gif" alt=""></p>
<p>이제 select해서 필요한 값만 변경됐을 때 리렌더링이 발생한다.</p>
<p>React 외부 상태 관리 훅을 직접 구현해보면서 값을 어떻게 관리해야하고, 상태의 변경에 대한 추적과 리렌더링이 발생되는 과정에 대해 알아봤습니다.</p>
]]></description>
        </item>
    </channel>
</rss>