<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Dico\'s_footPrint.log</title>
        <link>https://velog.io/</link>
        <description>깨진 창문을 내버려 두지 말기</description>
        <lastBuildDate>Tue, 01 Jul 2025 15:02:19 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Dico\'s_footPrint.log</title>
            <url>https://velog.velcdn.com/images/grinding_hannah/profile/77646980-ec9f-42ec-9e4e-4d0a0c3f4ab9/image.PNG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Dico\'s_footPrint.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/grinding_hannah" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[SEO] 삭제된 페이지 구글 검색에서 완전히 제거하기 (인덱싱 삭제) ]]></title>
            <link>https://velog.io/@grinding_hannah/prevent-indexing-of-deleted-page</link>
            <guid>https://velog.io/@grinding_hannah/prevent-indexing-of-deleted-page</guid>
            <pubDate>Tue, 01 Jul 2025 15:02:19 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>준비중인 상태를 알려주던 <code>/coming_soon</code> 이라는 페이지가 더 이상 사용되지 않게 되어 페이지를 내린상태였다. 
정확히는 추후 재사용을 염두에 두고 라우팅은 보존한 채, 다음 조치들을 취해놓은 상태였다: </p>
<ul>
<li>robots.txt에서 크롤링 차단 (<code>Disallow: /coming_soon</code> 명시)</li>
<li>페이지에 직접 접근 시 middleware를 사용해 404로 리다이렉트</li>
<li><code>&lt;meta name=&quot;robots&quot; content=&quot;noindex, nofollow&quot;&gt;</code> 태그 적용</li>
</ul>
<p>3중 차단을 해두었으니 철저히 진입을 막고 있을거란 믿음이 있었지만...
그럼에도 불구하고 페이지는 구글에서 여전히 인덱싱 되고 있다는 리포트를 받았다 🫠</p>
<h2 id="원인">원인</h2>
<p>알고 보니 오히려 robots.txt에서 해당 페이지를 Disallow 처리하면 <strong>Googlebot이 페이지 자체에 접근할 수 없기 때문에 설정해둔 <code>noindex, nofollow</code> 태그를 읽지 못한다고 한다.</strong> 
결국 중복 차단으로 인해 기존에 인덱싱된 페이지가 계속 남아 있게 되는 문제가 발생할 수 있는 것이다! </p>
<p>즉, *<em><code>noindex</code>를 제대로 적용하려면 robots.txt에서 <code>Disallow</code>를 제거하고, 일시적으로 페이지 접근을 허용한 뒤 다시 삭제요청을 해야한다. *</em></p>
<h2 id="해결방법">해결방법</h2>
<p><strong>1. robots.txt에서 해당 페이지의 Disallow 제거</strong></p>
<pre><code class="language-text"># 삭제
# Disallow: /coming_soon
</code></pre>
<p><strong>2. 페이지에 noindex, nofollow 유지</strong></p>
<pre><code class="language-javascript">&lt;NextSeo path=&quot;coming_soon&quot; lang={i18n.language} noindex nofollow /&gt;</code></pre>
<p><strong>3. middleware로 페이지 직접 접근 시 404 반환 유지</strong></p>
<pre><code class="language-javascript">export function middleware(request: NextRequest) {
  // ...생략
  if (request.nextUrl.pathname === &#39;/coming_soon&#39;) {
    return Response.redirect(new URL(`/${locale}/404`, request.nextUrl.href));
  }
  return NextResponse.rewrite(request.nextUrl);
}
</code></pre>
<p><strong>4. Google Search Console에서 URL 삭제 요청</strong></p>
<p>1) Google Search Console 접속 &gt; <code>삭제</code> 메뉴 진입 &gt; <code>임시 삭제 항목</code> 탭 &gt; <code>새 요청</code> 버튼 클릭 &gt; <code>일시적으로 URL 삭제</code> 탭 &gt; URL 입력 &gt; <code>이 URL만 삭제</code> 혹은 <code>이 접두어가 포함된 모든 URL 삭제</code> 선택 &gt; <code>다음</code> 클릭 
<img src="https://velog.velcdn.com/images/grinding_hannah/post/32448fa9-cda4-4114-b236-487c7695055d/image.png" alt=""></p>
<p>2) URL 확인 후 <code>요청 제출</code> 클릭 
<img src="https://velog.velcdn.com/images/grinding_hannah/post/7ae8349d-7a69-46e0-9f86-f45182bd2837/image.png" alt=""></p>
<blockquote>
<p>💡fyi. 삭제 제출 시 임시로 6개월간 삭제되며, 이후 Googlebot이 페이지를 다시 크롤링했을 때 <code>noindex</code> 처리된 페이지로 인식되면 완전 삭제됨.</p>
</blockquote>
<hr>
<h3 id="reference">Reference</h3>
<p><a href="https://support.google.com/webmasters/answer/9689846?hl=ko#clear_cache_request&amp;zippy=%2C%EC%86%8D%EC%84%B1-%EC%86%8C%EC%9C%A0%EC%9E%90%EC%9D%98-%EC%82%AD%EC%A0%9C-%EC%9A%94%EC%B2%AD-%EB%B3%B4%EA%B8%B0">Search Console 도움말 &gt; URL 완전히 삭제하기</a>
<a href="https://developers.google.com/search/docs/crawling-indexing/ask-google-to-recrawl?sjid=15423480764383137409-NC&amp;visit_id=638869376906706714-5794890&amp;rd=1&amp;hl=ko">Google 검색 센터 &gt; Google에 URL 재크롤링 요청하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SEO] Google Search Console 활용하기 (a.k.a GSC와 함께한 삽질기록)]]></title>
            <link>https://velog.io/@grinding_hannah/SEO-Google-Search-Console-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/SEO-Google-Search-Console-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 15 May 2025 06:25:18 GMT</pubDate>
            <description><![CDATA[<p>배너 이미지 출처: <a href="https://www.trailblaze.marketing/blog/google-search-console">https://www.trailblaze.marketing/blog/google-search-console</a></p>
<hr>
<p>구글 서치 콘솔에서는 도메인을 등록하면 문제가 생길때마다 관련 이슈를 메일로 보내주는데, 
경험했던 구글 서치 콘솔 에러들을 유형별로 정리 해보았다:  </p>
<h1 id="적절한-표준-태그가-포함된-대체-페이지">적절한 표준 태그가 포함된 대체 페이지</h1>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/47188472-331a-41e9-8dc0-7eedb607db19/image.png" alt=""></p>
<p>여기서 <strong>&quot;적절한 표준 태그가 포함된 대체 페이지&quot;</strong>란 <strong>canonical tag</strong>를 말한다. </p>
<blockquote>
<h3 id="canonical-tag-">*<em>Canonical Tag: *</em></h3>
<p>웹사이트에서 <code>&lt;link rel=&quot;canonical&quot; href=&quot;https://example.com/page/&quot; /&gt;</code> 형식으로 사용되는 HTML 태그로, 유사하거나 중복된 콘텐츠를 가진 여러 URL이 있을 때, 구글에게 &quot;이 URL이 원본(또는 선호되는) 버전이다&quot;라고 알려주는 페이지다. </p>
</blockquote>
<p>예를 들어, </p>
<ul>
<li>다국어를 지원하지만 모든 페이지가 완벽히 번역되지 않아 여러 페이지가 동일한 컨텐츠를 보여주고 있을 때</li>
<li>동일한 제품이 여러 카테고리에 있어 다른 URL로 접근 가능할 때</li>
<li>모바일과 데스크톱 버전의 페이지가 별도로 있을 때</li>
<li>매개변수가 다른 URL들이 동일한 콘텐츠를 제공할 때 (예: ?color=red, ?size=large 등)<blockquote>
</blockquote>
이런 경우 정규화 태그를 사용하여 구글에게 <strong>&quot;이 여러 URL 중에서 이 URL이 대표 URL이다&quot;</strong>라고 알려주는 목적으로 사용된다. </li>
</ul>
<div style="backgroundColor:#FEF3E2; padding:10px 30px">

<h4 id="span-stylecolorfa812f🤔-그래서-canonical-tag가-뭘-어쨌다는-거죠span"><span style="color:#FA812F">🤔 그래서... canonical tag가 뭘 어쨌다는 거죠??</span></h4>
<p>부연 설명을 하자면, 위 서비스의 경우 <u>10여개의 언어를 지원하는 &#39;다국어&#39;서비스</u>이다. 따라서 언어별로 URL이 각각 생겨나게 되는데, <u>URL이 다르면 구글에서는 모두 다른 페이지로 인식을 한다.</u>
(ex. <code>www.abc.com/en-US/promotion</code> -&gt; 영어 페이지, <code>www.abc.com/ko-KR/promotion</code> -&gt; 한국어 페이지)
그런데 완벽하게 번역이 적용된 페이지도 있고, 현재 번역작업이 진행중이라 백업 언어인 영어로 표기되는 페이지들도 다수 있었던 것! 구글의 봇의 입장에서는 URL만 다를뿐, 영어로 된 내용이 같으니 중복페이지로 인식을 했던 것이다!💡 
그래서 이렇게 피치못하게 URL이 다른 중복이나 유사 페이지가 생기는 경우에는 구글에게 선호되는 버전을 canonical 태그로 알려주는 것이 중요하다.⭐️⭐️⭐️ </p>
</div>  

<p>다시 돌아와서, 
구글 서치콘솔에서 &quot;적절한 표준 태그가 포함된 대체 페이지&quot;라는 메일이 올때는 다음 중 하나일 가능성이 높다: </p>
<ol>
<li><p><strong>구글이 canonical 태그를 인식하고 있다는 의미</strong>
 이미 canonical tag를 올바르게 설정해 놓았고, 구글은 이것을 인식해서 지정한 대로 원본 URL을 보고 있다는 의미로, _긍정적인 신호_이다. </p>
</li>
<li><p><strong>구글이 자체적으로 canonical을 결정했다는 의미</strong>
 canonical tag를 설정하지 않았거나, 설정을 했더라도 구글이 다른 페이지를 더 적절한 원본이라 판단해 자체적으로 canonical을 결정했다는 의미이다. _만약 설정한 caconical tag를 완전히 무시하고 다른 페이지를 선택했다면 웹사이트의 구조나 canonical tag 설정부분을 재검토해볼 필요가 있다.  _</p>
</li>
</ol>
<p>(이 부분부터 보여지는 재검사 프로세스는 모든 이슈 공통) 
수정 작업이 완료되었다면 <code>수정 결과 확인</code> 버튼으로 재검사 요청을 할 수 있다. 
재검사가 시작되면 <code>유효성 검사 상태:시작됨</code> 이 보여진다. 
<img src="https://velog.velcdn.com/images/grinding_hannah/post/1aa64707-acf4-4013-b41e-3d238b0ec1ca/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/ff83c521-e54e-4840-841f-1cbf0f4c4474/image.png" alt=""></p>
<p>검사가 시작되면 아래 메일을 보내주고, 
<img src="https://velog.velcdn.com/images/grinding_hannah/post/08983e43-858e-4aa9-86d1-2df71d081936/image.png" alt=""></p>
<p>그리고 정상적으로 반영이 되었다면 반영결과를 알려주는 메일을 구글 서치 콘솔에서 보내준다. </p>
<h1 id="도메인에서-리뷰-스니펫-구조화된-데이터-문제가-감지됨">{도메인}에서 리뷰 스니펫 구조화된 데이터 문제가 감지됨</h1>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/ae51bb3a-38bc-4ac7-a27b-8e5e2b4cdd60/image.png" alt=""></p>
<p>위 이슈는 헤더에 스키마로 넣었던 review rating 정보가 구글이 지정한 rating 범위를 벗어났을 때 나타났다. </p>
<ul>
<li>지정했던 평점: 10점 만점 </li>
<li>구글 평점: 5점 만점</li>
</ul>
<pre><code class="language-js">// LD+JSON (schema) 중 해당부분 추출
 {
      &#39;@context&#39;: &#39;https://schema.org&#39;,
      &#39;@type&#39;: &#39;Product&#39;,
      name: `${partnerName}`,
      aggregateRating: {
        &#39;@type&#39;: &#39;AggregateRating&#39;,
        ratingValue: `${
          !reviewData?.ratings.overall || reviewData?.ratings.overall === 0
            ? 1
            : reviewData?.ratings.overall / 2
        }`, // 구글이 인정하는 평점은 5.0 만점이므로 2로 나누어 준다. 값이 없거나 0인 경우 1로 처리 (default: 10점 만점)
        reviewCount: `${
          Number(reviewData?.serviceDetails.reviewCount || 0) === 0
            ? 1
            : Number(reviewData?.serviceDetails.reviewCount || 1)
        }`, // 리뷰 수가 0인 경우 1로 처리
      },
    },


</code></pre>
<blockquote>
<h3 id="💡-ratingvalue와-reviewcount-등록-조건">💡 ratingValue와 reviewCount 등록 조건:</h3>
<p>ratingValue: 최대 <code>5</code>이하의 점수
reviewCount: 최소 <code>1</code>개 이상</p>
</blockquote>
<p>그래서 1/2값으로 변환된 평점으로 수정해서 다시 검사를 실행한 결과, 정상적으로 반영이 되었다! 
<img src="https://velog.velcdn.com/images/grinding_hannah/post/fe631401-0d04-4ebd-bb16-cbdf19784709/image.png" alt=""></p>
<blockquote>
<h3 id="제품-스니펫-product-rich-snippet"><strong>제품 스니펫 (Product Rich Snippet)</strong></h3>
<p>정식 명칭은 &quot;Product Rich Snippet&quot;. 흔히 알고 있는 스키마 마크업과도 일맥상통한다. 
스키마 마크업이 Google에 의해 인식되면 제품 스니펫(가격·재고 등)이 검색 결과에 노출되는 것! 
즉, <strong>스키마는 마크업, 스니펫은 결과물</strong>!</p>
</blockquote>
<div style="backgroundColor:#FEF3E2; padding:10px 30px">

<h4 id="span-stylecolorfa812f🤔-근데제품-스니펫을-꼭-넣어야-하나요-span"><span style="color:#FA812F">🤔 근데...제품 스니펫을 꼭 넣어야 하나요? </span></h4>
<p>  결론부터 말하자면, <span style="color:#D84040"><u>&quot;아니요, &#39;꼭&#39; 넣을 필요는 없습니다.&quot;</u></span>
하지만 클릭전환율을 하나라도 올리고 싶다면, <span style="color:#3E7B27"><u>&quot;네, 넣으세요.&quot;</u></span></p>
<p>제품 스니펫을 넣어야겠다는 생각이 닿은 데에는 이런 플로우가 있었다:
➡️ 검색결과에서 클릭하고 싶은 페이지란 어떤 페이지일까? 
➡️ 정보를 얻기 위한 검색이라면, 신뢰도가 높고 사용자가 많은 페이지를 클릭 하지 않을까?
➡️ 그럼 어떤 검색결과가 신뢰도가 높아보이고, 사용자가 많아 보일까?
➡️ 검색결과에 정보가 더 많이 보여지게 해야겠다! </p>
<p>이런 이유로 타사이트의 리뷰와 평점을 남기는 커뮤니티 서비스의 성격상, 별점과 리뷰수를 검색결과에 보여줄 수 있을 것이란 결론이 났던 것이다. </p>
<ul>
<li><a href="https://developers.google.com/search/docs/appearance/structured-data/search-gallery?hl=ko">스니펫으로 또 뭘 보여줄 수 있죠? - 구글 공식 가이드라인</a></li>
</ul>
<p>리뷰 수 &amp; 평점 제품 스니펫이 적용된 검색결과 예시(*아래 사진은 본문의 서비스와 무관 합니다) 🔽:
<img src="https://velog.velcdn.com/images/grinding_hannah/post/59abf858-49b4-415a-bb67-14b96c814167/image.png" alt=""></p>
</div>


<p>스니펫을 설정해서 배포하더라도 실제 검색결과에 노출이 되기 까지는 시간이 N주 ~ 1달가량 소요되는데, 
잘 적용이 되고나면 Google Search Console의 사이드바에도 해당 메뉴가 생겨난다. 
아래와 같이 사이드바에 못보던 메뉴가 생겼다면, <u>적어도 구글이 인지를 했다는 것</u>!</p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/e2330866-7696-4dac-90b5-d3af2829c3b0/image.png" alt=""></p>
<h1 id="페이지-색인이-생성되지-않는-새로운-이유">페이지 색인이 생성되지 않는 새로운 이유</h1>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/fca2c92e-4a06-411d-9b18-5ac6dd23337f/image.jpg" alt=""></p>
<p>페이지가 색인(a.k.a 인덱싱)되지 않는 이유는 다양하다. 
Google Search Console의 색인 생성 보고서에서는 어떤 이유로 페이지가 색인이 되지 않는 지를 보여준다. </p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/275cf424-600f-4fdd-8151-976d1d5cc893/image.jpg" alt=""></p>
<p>보안상의 이유(ex. 로그인이 되어야 볼 수 있는 페이지)나 모종의 이유로 임시로 사용자 유입을 막아야 한다면 여러가지 방법으로 페이지 색인을 막을 수 있다. </p>
<p>의도와는 상관없이 Google Search Console에서는 (어떠한 기준이나 주기로 이메일을 보내주는 지 정확히 알 수는 없지만) 페이지 미노출이 의도된 액션이 아닐 걸 우려해서 주기적으로 이메일로 노티를 준다.
이번에도 색인이 생성되지 않았다며 이메일을 받았고, 위 경우는 의도된 색인방지 조치가 있었기 때문이었다:</p>
<blockquote>
<h3 id="미노출-페이지-색인-방지하기">미노출 페이지 색인 방지하기</h3>
</blockquote>
<ol>
<li>Sitemap에서 페이지 제거 </li>
<li>robots.txt에 <code>Disallow: {경로}</code> 표기</li>
<li>URL로 직접 접근 시 404 페이지 혹은 메인 페이지로 리라우팅 될 수 있게 미들웨어 설정 (선택)</li>
<li>페이지<code>&lt;Head&gt;</code>태그 내부에 <code>&lt;meta name=&quot;robots&quot; content=&quot;noindex, nofollow&quot; /&gt;</code> 메타태그 넣어주기</li>
</ol>
<p>이 중에서도 2번 혹은 4번 때문에 받은 이슈 메일으로 보여졌는데 <code>심각하지 않은 문제</code>라고 한다.
(정말 영향을 크게 미치는 심각한 문제는 <strong>&quot;심각&quot;</strong>이라는 표현이 그대로 사용된다😅) </p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/7e5488c3-b709-4290-97b8-a9408bff6d9e/image.jpg" alt=""></p>
<p>자세한 내용을 보니 해당 페이지는 위의 <code>페이지 색인 방지하기</code> 4가지 방법이 모두 적용된 페이지였는데, 추후 재사용 될 경우를 위해서 페이지 자체는 삭제하지 않았다보니, URL이 살아있는 걸로 판단해 위와 같은 경고를 보낸 것으로 보여진다.</p>
<p>그래서 무시하고 넘어갈 수도 있었겠지만, 가능한 추가조치를 모색하다가 이미 색인된 내역 안에서 삭제를 요청하는 삭제 기능을 사용해보았다:  </p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/f7147ee9-c0e3-4d25-89a7-9cd5addeb4d3/image.jpg" alt=""></p>
<p>적용 결과는 TBC...</p>
<hr>
<ul>
<li>참고: <a href="https://developers.google.com/search/docs/appearance/structured-data/review-snippet?hl=ko">Google 검색 센터 - 구조화된 리뷰 스니펫 데이터 가이드</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SEO] 이걸로 AI한테 우리 서비스를 학습시킬 수 있다구요?: llms.txt 적용기]]></title>
            <link>https://velog.io/@grinding_hannah/SEO-llms.txt-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/SEO-llms.txt-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Tue, 13 May 2025 15:13:23 GMT</pubDate>
            <description><![CDATA[<p>배너 이미지 출처: <a href="https://derivatex.agency/seo/llms-txt-seo-ai-search">https://derivatex.agency/seo/llms-txt-seo-ai-search</a></p>
<hr>
<p>요즘 AI 크롤러를 제어할 수 있다고 하는 <code>llms.txt</code> 가 화제가 되면서,
&quot;우리 콘텐츠도 AI 학습에 쓰이고 있을까?&quot; 라는 고민을 해보았다. 
정말로 <code>llms.txt</code>가 도움이 될까?
궁금증을 갖고 직접 도입하며 실험해보았다 💡 </p>
<hr>
<p>그 전에, 나처럼 백지상태였던 개발자들을 위해 
아주 아주 간단히 <code>llms.txt</code>에 대한 간략한 정보를 짚고 넘어가려고 한다: </p>
<h1 id="📌-llmstxt란">📌 llms.txt란?</h1>
<ul>
<li><code>llms.txt</code>는 웹사이트 루트 디렉토리에 두는 Markdown 형식의 파일</li>
<li>GPT, Claude, Gemini 같은 LLM(혹은 Large Language Model, AI)이 사이트의 구조나 핵심 콘텐츠를 더 잘 이해하도록 돕는 목적</li>
<li><code>robots.txt</code>가 <strong>검색 엔진(SEO)용</strong>이라면, <code>llms.txt</code>는 <strong>AI 모델용</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/dabaf7ee-87bc-405c-a593-754757373c3d/image.png" alt=""></p>
<h1 id="🔧-llmstxt-샘플">🔧 llms.txt 샘플</h1>
<pre><code class="language-markdown"># 프로젝트 또는 사이트 이름

&gt; 이 사이트에 대한 간단한 요약 설명입니다.

## 문서

- [사용자 가이드](https://example.com/guide.md): 사이트 사용 방법에 대한 자세한 설명
- [API 문서](https://example.com/api.md): API 엔드포인트 및 사용법

## 예제

- [샘플 코드](https://example.com/sample-code.md): 주요 기능을 보여주는 코드 예제

## 선택 사항

- [추가 자료](https://example.com/additional.md): 참고할 수 있는 추가적인 자료</code></pre>
<ul>
<li>요약 + 핵심 문서 링크만 제공해도 충분, 전체 sitemap을 다 넣을 필요는 없음</li>
<li>핵심은 LLM이 따라가기 쉬운 구조를 만들고 주요 진입점만 안내해주는 것</li>
<li>더 알아보고 싶다면? <a href="https://llmstxt.org/">llms.txt에 관한 표준을 제안하는 문서</a> 참고</li>
</ul>
<h1 id="🤔-공식-표준인가요">🤔 공식 표준인가요?</h1>
<ul>
<li>❌ 도입을 독려하는 중이지만 <strong>아직 표준은 아니다</strong>.</li>
<li>✅ 그러나 점차 도입되는 중</li>
</ul>
<h1 id="🏢-그럼-llmstxt는-누가-참고하나요">🏢 그럼 llms.txt는 누가 참고하나요?</h1>
<ul>
<li><strong>Anthropic (Claude), Stripe, Mintlify, Cursor</strong> 등의 기술 기업들이 사용중<ul>
<li><a href="https://docs.anthropic.com/llms.txt">Anthropic의 llms.txt 파일 보기</a></li>
</ul>
</li>
<li><code>llms.txt</code> 사용 사이트 목록: <a href="https://directory.llmstxt.cloud/">directory.llmstxt.cloud</a></li>
</ul>
<h1 id="🤖-gpt도-llmstxt를-참고하나요">🤖 GPT도 llms.txt를 참고하나요?</h1>
<ul>
<li>❌ <strong>직접적으로는 참고하지 않음</strong>. 2023년까지의 공개된 웹/문서 기반 학습하는 중</li>
<li>✅ 실시간 크롤링이나 자동 분석은 하지 않지만, 사용자가 <code>llms.txt</code> 내용이나 <code>llms.txt</code>의 URL을 붙여주면 문맥 이해에는 활용 가능. </li>
</ul>
<h1 id="🧩-활용-시나리오--해볼-수-있는-일">🧩 활용 시나리오 / 해볼 수 있는 일?</h1>
<ul>
<li>우리 서비스나 제품 문서를 <strong>LLM 친화적</strong>으로 만들고 싶을 때</li>
<li>개발자 문서, API 문서, 가이드 페이지 등이 대상</li>
<li>특히 AI 기반 검색, 챗봇, RAG 구조에 사용하는 서비스라면 적극 고려해볼 만함 </li>
<li>AI가 우리 콘텐츠를 더 잘 이해하도록 도와줄 수 있는 <strong>간단하고 실용적</strong>인 방법</li>
<li>프론트와 백 모두 아래 관점에서 <code>llms.txt</code>를 지원하는 게 좋다:<ul>
<li>프론트는 <strong>문서 구조와 UX 관점</strong></li>
<li>백엔드는 <strong>자동화와 배포 관점</strong></li>
</ul>
</li>
</ul>
<h3 id="🧑💻-프론트엔드">🧑‍💻 프론트엔드</h3>
<ul>
<li><strong>어떤 문서를 LLM에 노출할지 결정</strong>하거나 문서 링크 구조를 설계하는 데 관여</li>
<li>주요 문서 경로 선정 (<code>/blog</code>, <code>/partners</code>, etc.)</li>
<li>링크 구조와 문서 포맷 구성</li>
<li>Next.js는 <strong>루트가 아닌 public 폴더 하위</strong>에 <code>llms.txt</code> 파일 추가하기</li>
<li><code>robots.txt</code> 파일에 <code>llms.txt</code> 의 위치를 명시하는 것도 좋은 방법! <blockquote>
<p>robots.txt는 이렇게: </p>
<pre><code class="language-robots.txt">User-agent: *
Disallow: /private/

# LLMs 규칙은 아래 참고
Sitemap: https://yourdomain.com/llms.txt</code></pre>
</blockquote>
</li>
</ul>
<h3 id="🧑💻-백엔드">🧑‍💻 백엔드</h3>
<ul>
<li><code>llms.txt</code>를 <strong>빌드 시 자동 생성하거나 배포 경로에 포함</strong>하는 로직 구현 가능</li>
<li>RAG, AI 문서 검색 API를 도입하는 경우 백엔드에서 문서 소스와 연동하는 역할 필요 (AI가 버전별 정보를 학습하는 데에 도움이 됨) </li>
<li><code>llms.txt</code> 자동 생성/업데이트 스크립트 작성 추천</li>
<li>정적 파일 서버에 <code>/llms.txt</code> 노출</li>
<li>AI 인덱싱용 API 구축 시 연결</li>
</ul>
<hr>
<h1 id="👀-llmstxt-는-어떻게-보나요">👀 llms.txt 는 어떻게 보나요?</h1>
<p>올바른 위치에 설정이 되었다면 <code>{도메인}/llms.txt</code>에 접근이 되는 지 브라우저에서 확인! </p>
<h1 id="⛑️-llmstxt를-적용했다면-whats-next">⛑️ llms.txt를 적용했다면? What’s next?</h1>
<ol>
<li>AI에게 학습할 수 있도록 주소 알려주기! (현재는 AI에게 직접 제출할 수 있는 포멧이 없음) </li>
</ol>
<p>*<em>예시 - ChatGPT *</em>
<img src="https://velog.velcdn.com/images/grinding_hannah/post/9209fa3d-0cc3-48eb-94b4-689bb95621e1/image.png" alt="">
2. 이걸 기억해줘 라고 요청하기! 
<img src="https://velog.velcdn.com/images/grinding_hannah/post/6d8e7dd8-6925-4f15-8fc9-8fbfe5499212/image.png" alt=""></p>
<h3 id="하지만😓">하지만....😓</h3>
<ul>
<li>현재(2025년 5월 기준)로서는 Claude 조차도 <strong>영구적으로 학습을 하거나 자동으로 학습하지는 않는 것</strong>으로 보여짐. </li>
<li>AI들이 공통적으로 주장하는 바는 <strong>llms.txt 파일의 URL을 직접 제공하면 읽어와서 안내</strong>하는 것 정도는 가능. </li>
<li>웹사이트에 대해 물어보면 대부분 검색된 결과를 기반으로 안내를 해주기 때문에 llms.txt 상의 내용이 우선순위로 보여지지는 않음. 
<img src="https://velog.velcdn.com/images/grinding_hannah/post/71fee56f-63fb-4661-bc4b-b28573ecc8a6/image.png" alt=""></li>
<li>실제로 <code>{도메인명}/llms.txt</code> 경로로 Production에 파일을 추가해서 배포한 다음, AI를 학습시켰지만, 새로운 대화를 요청해 해당 서비스에 대해 물었을 때는 <strong>검색된 결과에 더 유사한 내용을 안내해주었다</strong>. </li>
</ul>
<h1 id="✅-결론">✅ 결론</h1>
<ul>
<li><strong>_(아직까지는) 결국 사이트의 SEO나 블로그 등을 개선해서 검색 엔진에 잘 노출되게 하는 것이 가장 좋은 방법! _</strong></li>
</ul>
<hr>
<h1 id="reference">Reference</h1>
<ul>
<li><a href="https://llmstxt.org/">llms.txt에 관한 표준을 제안하는 문서</a></li>
<li><a href="https://ahrefs.com/blog/what-is-llms-txt/">What Is llms.txt, and Should You Care About It?</a></li>
<li><a href="https://docs.anthropic.com/llms.txt">Anthropic의 llms.txt</a></li>
<li><a href="https://directory.llmstxt.cloud/">/llms.txt directory</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SEO] 수치화할 수 있는 개발자가 되자: 프론트엔드를 위한 GA4 / GTM 왕초보 가이드라인]]></title>
            <link>https://velog.io/@grinding_hannah/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-GA4-GTM-%EC%82%AC%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@grinding_hannah/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-GA4-GTM-%EC%82%AC%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Tue, 29 Apr 2025 13:54:48 GMT</pubDate>
            <description><![CDATA[<p>배너 이미지 출처: <a href="https://www.gunotes.com/ga4-gtm">https://www.gunotes.com/ga4-gtm</a></p>
<hr>
<p>프론트엔드 개발자라면 어렵지 않게 마케팅측에서 GTM이나 GA 세팅 요청을 받게 된다. 
셋업에서 더 나아가 GA에서 수집된 데이터를 활용, 분석할 줄 아는 프론트엔드 개발자가 되어보고자, 
개발자를 위한 GA4/GTM 사용 가이드를 작성해보았다 :)  </p>
<hr>
<h1 id="ga4-이벤트-종류">GA4 이벤트 종류</h1>
<p>GA4의 이벤트는 크게 4종류로 나눌 수 있는데, 
개별 프로젝트의 성격에 따라 필요한 이벤트를 취사선택 할 것! </p>
<h3 id="자동-수집-이벤트-automatically-collected-events">자동 수집 이벤트 (Automatically collected events)</h3>
<p>앱/웹에는 각각 GA4 연동 시 자동으로 수집되는 이벤트들이 있다. 
<a href="https://support.google.com/analytics/answer/9234069">애널리틱스 자동 수집 이벤트 공식문서</a>를 참고하면 각 이벤트가 트리거 되는 시점과 매개변수를 확인할 수 있다. </p>
<h3 id="향상된-이벤트-enhanced-events">향상된 이벤트 (Enhanced events)</h3>
<p>코드 변경 없이 웹 데이터 스트림 옵션 설정만으로 내 컨텐츠와 유저와의 상호작용을 측정할 수 있다. 
<a href="https://support.google.com/analytics/answer/9216061">향상된 측정 이벤트 공식문서</a>에서 데이터 스트림 설정 방법을 확인할 수 있다. </p>
<h3 id="추천-이벤트-recommended-events">추천 이벤트 (Recommended events)</h3>
<p>웹사이트가 특정 목적(온라인 판매, 리드 생성, 게임)에 부합한다면, GA4에서 추천하는 이벤트를 사용하면 좋다. 상세한 내용은 <a href="https://support.google.com/analytics/answer/9267735">추천 이벤트 공식문서</a>참고. </p>
<h3 id="커스텀-이벤트-custom-events">커스텀 이벤트 (Custom events)</h3>
<p>미리 정의된 이벤트들 중에 어디에도 속하지 않는 액션을 분석하고 싶다면, 커스텀 이벤트를 생성해 수집할 수 있다. 
상세한 내용은 <a href="https://support.google.com/analytics/answer/12229021">커스텀 이벤트 공식문서</a>참고. </p>
<h1 id="dimensions--metrics">Dimensions &amp; Metrics</h1>
<p>GA4에서 수집된 이벤트, 매개변수, 사용자 속성 등 모든 데이터는 <strong>Dimensions</strong> 와 <strong>Metrics</strong>라는 항목으로 보고서를 생성하는 데에 사용할 수 있다. </p>
<blockquote>
<p>❓그렇다면 <strong>Dimensions</strong>는 뭐고, <strong>Metrics</strong>는 또 뭘까? </p>
<ul>
<li><strong><code>Dimensions</code></strong>:</li>
<li>*&quot;누가(who)&quot;, &quot;무엇을(what)&quot;, &quot;어디에서(where)&quot;**에 해당하는 데이터 측정기준</li>
<li><strong><code>Metrics</code></strong>: </li>
<li>*&quot;얼마나(how many)&quot;**에 해당하는 데이터 측정항목</li>
</ul>
<p>좀 더 개발자스럽게 재표현해보자면, <code>Metrics</code>는 <strong>number 타입 데이터</strong>이고, <code>Dimensions</code>는 <strong>number 타입을 제외한 그 외 모든 타입의 데이터</strong>가 된다. 실제로 <code>Metrics</code>는 숫자형 데이터&#39;만&#39; 받을 수 있다. 숫자가 아닌 데이터를 <code>Metrics</code>로 등록하면 가차없이 &#39;0&#39;이 나온다.</p>
<p>Ex. 결제 금액은 <code>Metric</code>, 결제된 상품은 <code>Dimension</code></p>
</blockquote>
<p>자동 수집 이벤트로 수집되는 이벤트와 매개변수는 보고서에서 기본적으로 dimensions와 metrics로 사용할 수 있게 추가되어 있다. 
하지만 <strong>커스텀 이벤트(혹은 커스텀 매개변수도!)의 경우에는 직접 추가를 해줘야한다</strong>.  
그래야지만 보고서에서 원하는 커스텀 이벤트를 dimensions와 metrics로 사용할 수가 있다.</p>
<h3 id="커스텀-이벤트-직접-dimensions--metrics로-등록하기">커스텀 이벤트 직접 Dimensions &amp; Metrics로 등록하기</h3>
<p>커스텀 이벤트를 보고서에서 보고 싶다면 Custom Definitions에서 직접 Dimensions와 Metrics로 등록하는 단계를 &#39;반드시&#39; 거쳐야한다.
이걸 모르고 이벤트를 등록해놓고는 GA4에서는 보이지를 않아서 마냥 이벤트가 안쌓여서 그렇겠거니.. 하며 허송세월한 시간이 적지 않다 🫠 </p>
<p><strong>Custom Definitions 등록 방법:</strong>
Admin(좌측 사이드바 하단 톱니바퀴 아이콘 클릭) 페이지 👉 Data Display의 Custom definitions 페이지 👉 Custom metrics 탭(혹은 Custom dimensions)으로 이동 👉 <code>Create custom metric</code> 버튼 클릭
<img src="https://velog.velcdn.com/images/grinding_hannah/post/733c4f4e-a6fb-4587-bf94-74d41471ffdf/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/5c3361b4-c863-47fa-b47b-37bb7df5608c/image.png" alt=""></p>
<blockquote>
<p>💡<strong>REMINDER</strong>
만약 <strong>스트링 타입</strong>의 데이터를 보내고 싶다면, <code>Custom dimensions</code>로 정의해줘야 한다.
나는 실수로 모든 타입을 <code>Custom metrics</code>에 등록하는 바람에 변경이 필요했다ㅠㅠ </p>
</blockquote>
<p>그런데 <code>Custom metrics</code>와 <code>Custom dimensions</code>는  <strong>한 번 등록하면 삭제하는 기능이 없으므로</strong> 하려면 살짝 이름을 바꿔서 수정해두는 꼼수를 쓸 수밖에 없다고 한다. 혹은 우측의 점 세개 모양 아이콘을 눌러 <strong>Archive</strong>를 하면 다시 새로운 항목으로 등록이 가능하다.</p>
<blockquote>
</blockquote>
<h1 id="불필요한-데이터-필터링-하기">불필요한 데이터 필터링 하기</h1>
<p>수집된 데이터에서 내부 트래픽 혹은 개발용 트래픽을 제외하고 싶다면 필터링 메뉴로 필터 조건을 추가하면 된다. </p>
<ul>
<li><a href="https://www.youtube.com/watch?v=9Uokq4bhHjE">공식영상가이드</a></li>
</ul>
<h3 id="개발용-트래픽-제외">개발용 트래픽 제외</h3>
<p>Admin 페이지 👉 Data Filters 👉 <code>Create data filter</code> 버튼 클릭 👉 <code>Developer traffic</code> 타입 선택 및 옵션 설정 👉 <code>Create</code> 버튼 클릭</p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/f789b88c-3e9a-4bdf-b7fb-548422e55905/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/3c233ab5-c939-47d1-95dd-ef38091ac84f/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/287d131f-f277-4bfc-b69c-b3a25408b92c/image.png" alt=""></p>
<blockquote>
<p><strong>Filter state 종류:</strong></p>
<ul>
<li><code>Testing</code> : 데이터는 보고서에 표시되나 &quot;Test data filter name=Developer traffic&quot; 이라는 dimension으로 식별 가능</li>
<li><code>Active</code> : 완전 필터링 되어 보고서에 표시되지 않음 </li>
<li><code>Inactive</code> : 필터링 기능 비활성화</li>
</ul>
</blockquote>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/1e5d980c-6ccc-42fb-abda-8694db925edb/image.png" alt=""></p>
<h3 id="내부-트래픽-제외">내부 트래픽 제외</h3>
<p>내부 트래픽 제외 옵션을 등록하는 방법은 기본적으로 개발용 트래픽을 등록하는 방법과 동일하다.
다만, <strong>IP 주소를 등록</strong>해줘야 한다. </p>
<p><strong>내부 IP 등록하는 방법:</strong></p>
<p>Admin 페이지 👉 Data streams 👉 등록된 Stream 선택 👉 Configure tag settings 선택 👉 <code>Show more</code>로 메뉴 더보기 👉 Define internal traffic 선택 👉 <code>Create</code> 버튼 클릭 👉 IP address is in range (CIDR notation) 선택 (IP 복수 등록 옵션) </p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/21355861-4a3c-434e-8058-26ad3c5820c9/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/63cfdff0-bbbd-40b0-a023-8882319355a9/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/58851c7e-c6d2-4ac5-a1ff-a400a67b85e1/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/2f5ce8c6-56e3-4e8b-b2b4-8295901806ad/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/9b60a5e8-eae7-4d7f-a8d5-5bade3cc7f1c/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/7b5d99a0-6a36-4b78-9f72-8db56c994fa0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SEO] Next.js SEO 최적화 실전 적용기]]></title>
            <link>https://velog.io/@grinding_hannah/nextjs-seo-guide</link>
            <guid>https://velog.io/@grinding_hannah/nextjs-seo-guide</guid>
            <pubDate>Tue, 08 Apr 2025 13:57:52 GMT</pubDate>
            <description><![CDATA[<h3 id="💡-글을-시작하며">💡 글을 시작하며</h3>
<p>이 글은 <strong>Next.js 13</strong> 버전 <strong>Pages Router</strong>기반으로 만든 다국어 웹 서비스에 직접 SEO전략들을 적용해보며 검색 결과 개선과정을 정리한 경험 기록입니다. 
다양한 검색 엔진이 있지만, 이 글은 <strong>Google검색</strong>에 초점이 맞춰져 있습니다.<br>실제 서비스에서 마주한 문제들을 하나씩 해결해가며 가설 세우고, 적용하고, 데이터로 검증하는 과정을 담았습니다.</p>
<p>저 역시도 아직 <code>가설-검증-실패or성공-또 다른 가설..(반복)</code>의 과정을 반복하며 시행착오를 겪는 중이라 여전히 완벽한 정답이 아닐 수 있으니, 혹시 잘못된 부분이 보이거나 더 좋은 방법이 있다면 댓글로 피드백 주시면 감사하겠습니다 🙇‍♀️</p>
<p>SEO가 낯설거나 실무 적용이 막막한 분들께, 이 글이 조금이나마 도움이 되길 바랍니다!</p>
<hr>
<h1 id="span-stylecolor-169976지난날-시도했던-seo-최적화-전략들span"><span style="color: #169976">지난날 시도했던 SEO 최적화 전략들</span></h1>
<h2 id="span-stylecolor-1dcd9f1-ssr-기반-파트너-상세-페이지-컨텐츠-확장span"><span style="color: #1DCD9F">1. SSR 기반 파트너 상세 페이지 컨텐츠 확장</span></h2>
<h3 id="📌-필요한-정보-충분히-보여주고-있을까">📌 <strong>필요한 정보, 충분히 보여주고 있을까?</strong></h3>
<p>우리 서비스는 여러 사이트(이하 머천트)를 소개하는 중개형 플랫폼이에요.
각 머천트마다 고유한 상세 페이지가 있었지만, 처음엔 콘텐츠가 너무 빈약하다고 느꼈어요.
단순히 이름, 로고, 링크 몇 개만으로는 유입도 어렵고, 들어온 유저도 금방 나가버릴 것 같았거든요.</p>
<p>마침 마케팅 팀에서 머천트별로 정보를 잘 정리한 Word 파일 자료를 갖고 있었고,
<strong>“이걸 그냥 상세 페이지에 넣어보자!”</strong>는 게 저희의 출발점이었어요.
한 줄이라도 더 보여주면 검색에도 걸릴 확률이 높아지고, 정보성 페이지로서의 신뢰도도 올라갈 테니까요.</p>
<p>그런데 문제는 양이 너무 많다는 거였죠.
&#39;스크롤이 너무 길어져도 괜찮을까?&#39; 라는 고민도 잠깐 있었지만,
정보 제공이 핵심인 페이지라면, 오히려 길어지는 게 장점이 될 수도 있겠다고 판단했어요.</p>
<p>그래서 선택한 방식은:</p>
<ol>
<li><strong>Word(<code>.docx</code>) 파일을 <code>.mdx</code> 포맷으로 변환할 수 있는 스크립트를 작성하고,</strong></li>
<li><strong>변환된 <code>.mdx</code> 파일을 <a href="https://github.com/hashicorp/next-mdx-remote"><code>next-mdx-remote</code></a>를 사용해 SSR 기반 Component로 렌더링,</strong></li>
<li><strong>그 결과물을 상세 페이지에 그대로 노출시키는 구조로 구성했어요.</strong></li>
</ol>
<p>덕분에 각 머천트 페이지에는 실제로 유의미한 검색 텍스트들이 다수 포함되게 되었고,
페이지 자체도 <strong>&quot;정보가 많은 곳&quot;</strong>으로서 역할을 하게 되었습니다!</p>
<h3 id="🎯-검색에서-잘-걸리는-키워드-전략적으로-넣기">🎯 <strong>검색에서 잘 걸리는 키워드 전략적으로 넣기!</strong></h3>
<p>SEO 콘텐츠를 작성할 때 가장 먼저 고민해야 하는 건 사용자가 <strong>실제로 어떤 키워드로 검색해서 들어 오는지</strong>일텐데요,
저희는 이걸 파악하기 위해 &#39;Ahrefs&#39;라는 SEO 분석 툴을 사용하고 있어요.
유입된 <strong>Organic Keyword(자연 검색 키워드)</strong> 데이터를 확인할 수 있어서,
해당 키워드를 기반으로 콘텐츠를 조금씩 조금씩 보완해왔어요.</p>
<p>예를 들어, 특정 머천트 페이지에 <code>&quot;서비스명 + 위치&quot;</code>, <code>&quot;서비스명 후기&quot;</code> 같은 키워드로 유입이 발생하고 있었다면, 해당 <strong>키워드를 페이지 내에 자연스럽게 포함시키는 식</strong>이었어요.</p>
<p>그렇지만 꼭 생각한 키워드로만 유입되는 건 아니었어요.
예상하지 못했던 키워드로도 유입이 발생하는 경우가 있었고,
그럴 땐 ‘검색 수요가 있다는 거니까 내용을 더 보완해보자’는 식으로 접근했어요.</p>
<p>무엇보다 중요한 점은, 단순히 키워드를 채워 넣기 보다는
<strong>검색 의도에 맞는 정보를 맥락에 맞게 자연스럽게 녹여내는 것</strong>이 가장 중요하다는 생각이 듭니다!⭐️
그래야 <strong>의미 있는 사용자가 페이지에 머무는 시간(Dwell Time)</strong>이 길어지고, 
이 역시 <strong>컨텐츠의 품질 지표로 작용해 SEO에도 좋은 영향을 주기 때문</strong>이에요.</p>
<h2 id="span-stylecolor-1dcd9f2-json-ld-기반-구조화-데이터-적용-rich-snippets-노출-목표span"><span style="color: #1DCD9F">2. JSON-LD 기반 구조화 데이터 적용 (Rich Snippets 노출 목표)</span></h2>
<h3 id="📦-스키마도-종류가-많던데-뭘-써야-하지">📦 스키마도 종류가 많던데, 뭘 써야 하지?</h3>
<p>스키마 적용은 검색 결과를 좀 더 풍부하게 보여주는 것을 목표로 하고 있어요. 
구글에서는 스키마를 어떻게 작성해야 하는 지에 대해 공식 가이드라인도 제공하고 있답니다: 
<a href="https://developers.google.com/search/docs/appearance/structured-data/search-gallery?hl=ko">👉 Google 검색에서 지원하는 구조화된 데이터 마크업</a></p>
<p>적용하는 스키마의 type에 따라 검색 결과에서 미리보기 형태로 노출되는 요소들이 달라지기 때문에, 
<strong>서비스의 성격에 따라 스키마를 선별해서 적용하는 것이 중요</strong>하다는 생각이 듭니다.</p>
<p>저희 서비스에서는 아래의 스키마 타입들을 적용했어요: </p>
<ul>
<li><p><a href="https://developers.google.com/search/docs/appearance/structured-data/organization?hl=ko">Organization / WebSite</a>:
브랜드나 사이트 자체에 대한 정보를 검색엔진에 명확히 전달하기 위해</p>
</li>
<li><p><a href="https://developers.google.com/search/docs/appearance/structured-data/breadcrumb?hl=ko">BreadcrumbList</a>:
블로그 콘텐츠나 카테고리 구조가 있는 페이지에 적용해서, 
자연스럽게 사이트 회유(다른 페이지 탐색) 가능성을 높이고, 
시각적으로 검색 결과를 더 풍성하고 신뢰감 있어 보이게 하기 위해</p>
</li>
<li><p><a href="https://developers.google.com/search/docs/appearance/structured-data/faqpage?hl=ko">FAQPage</a>:
자주 묻는 질문을 FAQ 형태로 스키마에 포함시켜, 
검색 결과에서 사용자의 궁금증을 미리 풀어주기 위해</p>
</li>
</ul>
<blockquote>
<p>🚨 <span style="color: red"><strong>짚고갈 점!</strong></span>
구조화 데이터를 적용했다고 해서, <em>&quot;반드시&quot; 검색 결과에 리치 스니펫이 노출되는 것은 아닙니다.</em>
구글은 구조화 데이터를 단순히 &quot;참고용&quot;으로 활용할 뿐,
실제로 노출할지 여부는 <strong>자체 알고리즘의 판단에 따라 결정</strong>한다고 해요.  </p>
</blockquote>
<p>예를 들어 FAQPage 스키마를 넣었더라도,
페이지 콘텐츠의 품질이나 주제의 적절성, 사용자 만족도 등 여러 기준을 종합적으로 고려해
구글이 판단하기에 &quot;리치 스니펫으로 노출할 가치가 있다&quot;고 생각해야지만 실제 검색 결과에 반영됩니다.</p>
<blockquote>
</blockquote>
<p>그래서 스키마를 넣는 것만으로 끝이 아니라,
페이지 자체의 콘텐츠 품질, 메타 정보, 유저 행동 데이터 등과 함께 <strong>총체적으로 관리하는 게 중요</strong>합니다💡</p>
<h3 id="🔧-직접-짤까-라이브러리를-쓸까">🔧 직접 짤까, 라이브러리를 쓸까?</h3>
<p>SEO를 개선하면서 Next.js 프로젝트에서 정말 효자같은 라이브러리가 하나 있었는데요, 바로 <a href="https://github.com/garmeeh/next-seo"><strong>next-seo</strong></a>입니다.
(이후 내용에서도 종종 등장할 예정이라, 여기서는 간단히만 언급하고 넘어갈게요 👀)</p>
<p>next-seo에서도 JSON-LD 스키마를 위한 컴포넌트를 제공하기 때문에 next-seo를 통해 주입할 지, 아니면 직접 작성을 할 지도 고민이 되었어요. </p>
<h4 id="next-seo로-삽입하기">&lt; next-seo로 삽입하기 &gt;</h4>
<pre><code class="language-javascript">import { FAQPageJsonLd } from &#39;next-seo&#39;

&lt;FAQPageJsonLd
  mainEntity={[
    {
      questionName: &#39;이 서비스는 어떻게 이용하나요?&#39;,
      acceptedAnswerText: &#39;회원가입 후 로그인하면 다양한 혜택을 이용할 수 있습니다.&#39;,
    },
    {
      questionName: &#39;서비스는 무료인가요?&#39;,
      acceptedAnswerText: &#39;기본 이용은 무료이며, 일부 프리미엄 기능은 유료입니다.&#39;,
    },
  ]}
/&gt;</code></pre>
<h4 id="직접-삽입하기">&lt; 직접 삽입하기 &gt;</h4>
<p>※ 구조화 데이터는 <code>&lt;script type=&quot;application/ld+json&quot;&gt;</code> 내부에 순수 JSON 문자열로 삽입해야 하므로, React에서는 <code>dangerouslySetInnerHTML</code>을 사용합니다.</p>
<pre><code class="language-javascript">const dynamicJsonLdObject = {
  &quot;@context&quot;: &quot;https://schema.org&quot;,
  &quot;@type&quot;: &quot;FAQPage&quot;,
  &quot;mainEntity&quot;: [
    {
      &quot;@type&quot;: &quot;Question&quot;,
      &quot;name&quot;: &quot;이 서비스는 어떻게 이용하나요?&quot;,
      &quot;acceptedAnswer&quot;: {
        &quot;@type&quot;: &quot;Answer&quot;,
        &quot;text&quot;: &quot;회원가입 후 로그인하면 다양한 혜택을 이용하실 수 있습니다.&quot;
      }
    },
    {
      &quot;@type&quot;: &quot;Question&quot;,
      &quot;name&quot;: &quot;서비스는 무료인가요?&quot;,
      &quot;acceptedAnswer&quot;: {
        &quot;@type&quot;: &quot;Answer&quot;,
        &quot;text&quot;: &quot;기본적인 이용은 무료이며, 일부 프리미엄 기능은 유료입니다.&quot;
      }
    }
  ]
}

return (
  &lt;Head&gt;
    &lt;script
      type=&quot;application/ld+json&quot;
      dangerouslySetInnerHTML={{ __html: JSON.stringify(dynamicJsonLdObject) }}
    /&gt;
  &lt;/Head&gt;
)</code></pre>
<p>실제 서비스에서는 페이지마다 구조화 데이터의 구성도 다르고 조건 분기나 동적 데이터 삽입이 필요한 경우가 많았기 때문에 결국 JSON-LD 코드를 직접 작성해서 <code>_app.tsx</code>에서 주입하는 방식을 선택했습니다.</p>
<p>이렇게 했을 때의 장점은 <strong>스키마와 관련된 데이터들을 한 곳에서 관리할 수 있고</strong>,
그리고 스키마가 <strong>SSR 환경에서 더욱 명확히 크롤러에게 전달</strong> 될거라고 기대할 수 있어요.</p>
<blockquote>
<p>⁉️ *<em>next-seo 를 사용하면 SSR 환경에서 스키마 삽입이 안되나요? *</em></p>
</blockquote>
<p>아니에요, 
next-seo의 JSON-LD 컴포넌트는 SSR, SSG 환경에서도 <code>&lt;script type=&quot;application/ld+json&quot;&gt;</code> 태그로 잘 렌더링됩니다!</p>
<blockquote>
</blockquote>
<p>다만 동적으로 가져오는 데이터(ex. API 호출 결과로 만든 JSON-LD)를 next-seo 컴포넌트에 넘기면, <strong>렌더링 시점에 따라 CSR이 되어 크롤러가 스키마를 읽지 못하는 문제가 생길 수 있어서</strong> 페이지별로 조건 분기나 동적 내용 삽입이 필요한 경우에는 직접 구조화 데이터를 작성하고 주입하는 방식이 제어와 유연성 면에서 더 실용적일 거라고 생각했답니다. 🥸</p>
<h2 id="span-stylecolor-1dcd9f3-canonical-url-설정-및-중복-페이지-대응-span"><span style="color: #1DCD9F">3. Canonical URL 설정 및 중복 페이지 대응 </span></h2>
<h3 id="⚠️-중복-페이지-문제-어떻게-해결할까">⚠️ 중복 페이지 문제, 어떻게 해결할까?</h3>
<p>앞서 언급했듯이 저희 서비스는 10여개의 언어를 지원하는 다국어 웹사이트를 운영하고 있어요.
하나의 페이지를 다양한 언어로 제공하기 위해, 하나의 번역키(key)에 여러 개의 번역값(value)이 매핑되어 있어서, 
사용자가 선택한 locale에 맞는 번역값을 받아와 페이지를 렌더링하게 됩니다.</p>
<p>그런데 페이지 수와 지원언어가 많다 보니, 번역값 입력 작업이 끝나지 않은 페이지들의 경우, 
영어페이지가 아니어도 백업(back-up / default) 언어인 영어로 노출이 되고 있었어요.</p>
<p>문제는 여기서부터 발생했어요❗️
구글은 <strong>URL이 다르면 다른 페이지로 인식</strong>합니다.
그래서 locale이 다른 <code>/en-US/partner/abc</code> 와 <code>/ko-KR/partner/abc</code>를 서로 다른 페이지로 인식하죠.
하지만 <strong>미번역 페이지는 영어로 노출되고 있었기 때문에 중복 콘텐츠(Duplicate Content)로 간주되어서 검색 노출에 부정적인 영향</strong>을 주고 있었어요. </p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/ab865106-b946-4e33-8822-41ef6b1ac405/image.png" alt="">
<img src="https://velog.velcdn.com/images/grinding_hannah/post/26599b44-19e0-4648-9340-b17aa1d7b5d5/image.png" alt=""></p>
<p>당장 번역값을 넣을 수 없어도 SEO를 해치는 것만큼은 막아야 했기 때문에! 
저희는 <strong>Canonical 태그를 설정</strong>했습니다.
서로 다른 언어 URL이 있더라도 구글에 <strong>&quot;이 페이지의 원본은 이거예요!&quot;</strong>라고 대표 URL을 명시적으로 알려주는 방식이죠.
즉, <em><strong>&quot;중복처럼 보여도 중복 페이지가 아닙니다!&quot;</strong></em> 라고 구글에게 알려주는 방법이에요. </p>
<p>Next.js에서는 next-seo를 통해서 Canonical 태그를 간단히 설정할 수 있어요: 
👉 <a href="https://github.com/garmeeh/next-seo?tab=readme-ov-file#canonical-url">next-seo 공식문서 - Canonical 태그</a> </p>
<p>Canonical 태그 설정은 <a href="https://ahrefs.com/">Ahrefs</a>의 언어별 Canonical 구조를 참고했는데요,
다국어 SEO를 고려한 대표적인 성공 사례 중 하나이기도 해서, 실제 운영 중인 사이트를 벤치마킹하는 데 좋은 참고가 되었습니다.</p>
<p>※ ahrefs.com 의 canonical 태그: </p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/858ab455-1c07-4668-a27d-3dd5c613ad53/image.png" alt=""></p>
<h2 id="span-stylecolor-1dcd9f4-multilingual-seo-적용-span"><span style="color: #1DCD9F">4. Multilingual SEO 적용 </span></h2>
<h3 id="🌍-다국어-페이지-구글에-제대로-인식시키려면">🌍 다국어 페이지, 구글에 제대로 인식시키려면?</h3>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/e648182f-d44b-4ecd-ad69-6f35e20cfd78/image.png" alt=""></p>
<p>다국어 서비스에서 개발자도구를 열면 위와 같이 설정된 태그들을 어렵지 않게 볼 수 있는데요, 
이게 다 뭐하는 태그일까...싶지만, 다국어 페이지를 운영한다면 <em><strong>반드시</strong></em> 설정해줘야 하는 옵션입니다. </p>
<p>하나씩 살펴보면:</p>
<ul>
<li><code>hreflang={locale}</code>: 서로 다른 언어 버전의 페이지가 동일한 컨텐츠임을 명시하는 태그로, <strong>구글이 사용자의 언어와 지역에 맞는 페이지를 우선적으로 매칭</strong>해서 노출 </li>
<li><code>hreflang=&quot;x-default&quot;</code>: <strong>사용자의 언어 설정과 일치하는 페이지가 없을 때 보여줄 기본 페이지</strong>를 지정하는 용도</li>
<li><code>rel=&quot;alternate&quot;</code>: <code>hreflang</code>을 쓸 때 같이 지정되는 속성으로, <strong>&quot;이 페이지는 이런 언어 버전도 있어요&quot;라는 의미</strong>를 검색엔진에게 전달</li>
</ul>
<p>이 부분 역시 next-seo가 제공하는 <a href="https://github.com/garmeeh/next-seo?tab=readme-ov-file#alternate">languageAlternate</a> 라는 옵션을 통해 간단히 구현할 수 있어요:</p>
<pre><code class="language-javascript">import { NextSeo as Seo } from &#39;next-seo&#39;;

// (...생략) 

const NextSeo = ({ path }: NextSeoProps) =&gt; {
  const { i18n } = useTranslation();
  const currentLanguage = i18n.language;

   // x-default 값은 사용자의 언어 설정과 일치하는 페이지가 없을 때 
   // 보여줄 기본 언어(en-US) 버전을 지정합니다.  
  const reciprocal = [
    { hrefLang: &#39;x-default&#39;, href: getHrefLang({ path, lang: &#39;en-US&#39; }) },
  ];

  // 모든 지원 언어에 대한 alternates 생성
  const alternates = supportedLngs.map((lang) =&gt; ({
    hrefLang: lang,
    href: getHrefLang({ path, lang }),
  }));

  return (
    &lt;Seo
      canonical={getHrefLang({ path, lang: currentLanguage })}
      languageAlternates={[...reciprocal, ...alternates]}
    /&gt;
  );
};
</code></pre>
<blockquote>
<p>🚩 <strong>자주 하는 실수: canonical + hreflang 충돌</strong>
canonical이 <code>/en-US/page</code>로 설정되어 있는데, <code>/ko-KR/page</code>에도 canonical이 <code>/en-US/page</code>로 되어 있으면 구글은 &quot;한국어 페이지도 결국 영어 페이지가 대표구나&quot;라고 생각함 → <strong>한국어 페이지 노출 안 됨!</strong> </p>
<p>그래서 다국어 페이지에서는 보통 <strong>canonical은 자기 자신을 가리키고, hreflang으로 서로 연결하는 게 일반적인 패턴</strong>이에요.</p>
</blockquote>
<h2 id="span-stylecolor-1dcd9f5-sitemap-최적화-및-제출span"><span style="color: #1DCD9F">5. Sitemap 최적화 및 제출</span></h2>
<h3 id="🗺️-sitemap은-왜-만들어야-할까">🗺️ Sitemap은 왜 만들어야 할까?</h3>
<p>웹사이트 구조가 복잡하거나, 내부 링크만으로는 도달하기 어려운 페이지가 있다면 검색 엔진이 모든 페이지를 자동으로 찾아내기 어렵다고 해요. 
이 때 검색 엔진이 우리 사이트를 더 빠르고 정확하게 인식하도록 도와주는 것이 바로 sitemap.xml!</p>
<h4 id="✅-sitemap의-장점">✅ <strong>sitemap의 장점:</strong></h4>
<ul>
<li>더 빠르게 인덱싱할 수 있도록 유도</li>
<li>숨겨진 페이지나 동적으로 생성된 페이지도 누락 없이 포함</li>
</ul>
<p><code>lastmod</code> 정보를 포함하면, 업데이트된 페이지를 우선적으로 다시 크롤링하도록 유도할 수 있어요. 
즉, sitemap은 단순한 페이지 목록이 아니라, &#39;검색 노출의 효율성을 높여주는 중요한 SEO 도구&#39;랍니다.</p>
<h3 id="🙋🏻♀️-크롤러가-자주-찾아오는-sitemap이란">🙋🏻‍♀️ 크롤러가 자주 찾아오는 sitemap이란?</h3>
<p>결론부터 말하자면, <strong>&quot;<code>&lt;lastmod&gt;</code> 값이 자주 갱신되는 sitemap&quot;, &quot;실제로 컨텐츠가 자주 바뀌는 URL이 포함된 sitemap&quot;</strong> 이라고 해요.</p>
<p>초기에는 sitemap을 수동(manual)으로 관리했어요.
필요할 때마다(예: 새로운 페이지 추가나 삭제 등) sitemap.xml을 생성하는 스크립트를 실행해 XML 파일을 업데이트하고, 그걸 함께 배포하는 방식이었죠. </p>
<p>하지만 운영을 할 수록 수동 업데이트 방식이 점점 비효율적으로 느껴졌어요.
그래서 Next.js에서 제안하는 방식처럼 sitemap.xml을 <a href="%5D(https://nextjs.org/learn/seo/xml-sitemaps)"><strong>동적(dynamic)</strong> 생성 방식</a>으로 변경했습니다❗️
<code>pages/sitemap.xml.js</code> 파일에서 <code>getServerSideProps()</code>를 활용해 <strong>페이지 요청 시점에 실시간으로 sitemap을 생성</strong>하고 XML로 응답하는 구조로 개선을 진행했어요. </p>
<h4 id="✅-동적dynamic-sitemap-생성-방식의-장점">✅ 동적(dynamic) sitemap 생성 방식의 장점:</h4>
<ul>
<li>페이지가 새로 추가되거나 수정되어도 sitemap이 자동으로 최신 상태를 유지</li>
<li><code>lastmod</code> 값도 서버에서 동적으로 삽입되어 항상 최신 날짜를 반영</li>
</ul>
<p>특히 <code>lastmod</code>는 <em>검색 엔진이 해당 페이지를 다시 크롤링할 필요가 있는지 판단하는 기준이 되기 때문에</em>, 최신 상태로 유지하는 것이 빠른 인덱싱과 업데이트 반영에 효과적이에요.</p>
<h3 id="🚀-직접-제출-vs-기다리기">🚀 직접 제출 vs. 기다리기</h3>
<p>sitemap.xml 페이지를 만들어서 배포해두면, 언젠가는 구글이 알아서 sitemap 경로를 찾아 크롤링하게 되어 있어요 (...그런데 언제쯤? 🙄)
하지만 기다리지 않고 Google Search Console(GSC)에 sitemap을 직접 제출하는 방법도 있죠!</p>
<p>크롤러가 sitemap을 읽어줄 때까지 기다리는 건 반영시기를 예측하기가 어렵지만, 
sitemap을 직접 제출하면 역으로 알릴 수 있기 때문에, <strong>크롤러가 더 빠르게 새로운 페이지를 인덱싱할 확률이 높아져요</strong>. 
또 GSC에서 정확히 몇 개의 URL이 제출되었고, 몇 개가 인덱싱되었는지 상태를 모니터링할 수 있다는 장점도 있어요.</p>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/c75d0eee-b8e5-4126-ad9e-ffe6707dab72/image.png" alt=""></p>
<p>블로그 페이지처럼 개발팀에서 배포하지 않아도 추가될 수 있는 정적 페이지들이 있을 수 있기 때문에
저는 주기적으로 sitemap을 GSC에 제출해서 알려주고 있어요. </p>
<h2 id="span-stylecolor-1dcd9f6-페이지-체계url-구조-리디자인span"><span style="color: #1DCD9F">6. 페이지 체계/URL 구조 리디자인</span></h2>
<h3 id="📬-url만-바꿨을-뿐인데-검색-노출이-달라진다고">📬 URL만 바꿨을 뿐인데 검색 노출이 달라진다고?</h3>
<p>저희 서비스의 파트너 상세 페이지는 처음엔 단순한 ID 기반 URL 구조를 가지고 있었어요.
예를 들어, <code>/partners/12/34</code> 같은 형태였죠.
개발 관점에서는 깔끔하고 구조적으로도 정리된 URL이지만, SEO 관점에서는 아무런 의미를 담고 있지 않은 숫자 나열일 뿐이었어요.</p>
<p>그래서 검색 노출 개선을 위해, URL 구조를 다음과 같이 바꾸게 됐습니다:</p>
<ul>
<li><code>/partners/12/34</code> ➡️ <code>/partners/velog/12/34</code> </li>
</ul>
<p>중간에 파트너명(<code>/velog</code>)을 끼워 넣는 구조로 바꾸게 되었는데요, 이유는 단순해요. 
<strong>검색 결과에 &#39;파트너명&#39;을 직접 노출시키고 싶었기 때문</strong>이에요. </p>
<blockquote>
<p>🔍 <strong>왜 URL을 이렇게 바꾸는 게 더 유리할까?</strong></p>
<p>검색엔진은 <strong>URL 안에 포함된 단어</strong>도 <strong>콘텐츠의 주요 키워드 중 하나로 인식</strong>합니다.
따라서 URL 경로에 검색되었으면 하는 브랜드명이나 서비스명 같은 키워드를 포함해두면,
해당 키워드로 검색했을 때 <strong>더 잘 노출될 가능성이 높아져요</strong>. </p>
</blockquote>
<p>또한 사용자의 입장에서도 <code>/partners/chrome/12/34</code>처럼 브랜드명을 포함한 URL이
<code>/partners/12/34</code>보다 어떤 페이지인지 직관적으로 이해하기 쉽기 때문에,
<strong>클릭 유도율(CTR)</strong>에도 긍정적인 영향을 줄 수 있어요.</p>
<blockquote>
<p>물론 이 구조가 반드시 검색 순위를 올려준다고 보장할 순 없지만,
의미 없는 숫자보다 의미 있는 키워드가 들어간 URL이 더 유리하다는 건 SEO에서 오래된 정설 중 하나랍니다 🤫</p>
</blockquote>
<h3 id="🧱-h1부터-h6까지-태그-순서가-seo에-영향을-줄까">🧱 h1부터 h6까지, 태그 순서가 SEO에 영향을 줄까?</h3>
<p>페이지 구조를 정리하면서 가장 먼저 신경 쓴 부분은 heading 태그의 순서와 위치였어요.</p>
<p>기본적인 원칙은 이렇습니다:</p>
<ul>
<li>✅ 모든 페이지에는 최소 하나의 <code>h1</code> 태그가 있어야 하며,
그 <code>h1</code>은 페이지의 가장 상단에, 가장 먼저 나타나야 한다.</li>
</ul>
<p><code>h1</code>은 해당 페이지의 핵심 주제를 검색 엔진과 사용자 모두에게 전달하는 역할을 해요.
그리고 검색 엔진은 단순히 태그의 존재 여부뿐 아니라, <strong>페이지 내 시각적 순서도 참고</strong>합니다.
즉, 페이지 상단에 <code>h1</code>이 있어야 하고, 실제 사용자 화면에서도 가장 먼저 노출되는 것이 좋아요.</p>
<blockquote>
<p>🤔 <strong>왜 등장 &#39;순서&#39;가 중요할까?</strong></p>
</blockquote>
<p>검색 엔진은 <strong>페이지를 위에서 아래로, 왼쪽에서 오른쪽으로</strong> 읽습니다.
<code>h1</code>이 아래쪽에 있거나, <code>h2</code>보다 늦게 등장하면 문서의 구조가 혼란스럽다고 인식할 수 있어요.
이는 SEO 신호로도 부정적인 영향을 줄 수 있습니다.</p>
<blockquote>
<p>그래서 저는 <strong><code>h1</code>을 페이지마다 반드시 하나씩</strong>, 가능한 한 시각적으로도 상단에 배치하도록 했습니다.
그리고 그 아래에는 <code>h2</code>, <code>h3</code> 순서로 내용을 구분하면서 계층 구조를 명확히 했어요.</p>
</blockquote>
<p>그래서 저는 <code>h1</code>을 페이지마다 반드시 하나씩,
가능한 한 시각적으로도 상단에 배치하도록 했습니다.
그리고 그 아래에는 <code>h2</code>, <code>h3</code> 순서로 내용을 구분하면서 계층 구조를 명확히 했어요.</p>
<blockquote>
<p>🍯 <span style="color: #FF9F00"><strong>TIP</strong></span>: *<em>구조 분석 더 쉽게 하는 방법 *</em>
<a href="https://chromewebstore.google.com/detail/seo-meta-in-1-click/bjogjfinolnhfhkbipphpdlldadpnmhc?pli=1">SEO Meta in 1 Click (Chrome 확장 프로그램)</a>에서는 현재 페이지에 사용된 h1~h6 태그의 개수, 순서, 위치를 한눈에 확인할 수 있어요❗️ 
<img src="https://velog.velcdn.com/images/grinding_hannah/post/94dfc861-d4da-41a0-86a8-dc0048bda0c3/image.png" alt=""></p>
</blockquote>
<hr>
<h1 id="span-stylecolor-169976seo-전략-적용-전후-비교-수치로-보는-변화span"><span style="color: #169976">SEO 전략 적용 전후 비교: 수치로 보는 변화</span></h1>
<p>SEO 개선을 위한 작업들은 오랜 시간에 걸쳐 조금씩 꾸준하게 진행해온 일이었기 때문에 데이터가 충분히 누적되기 전까지는 개선정도를 시작화하기가 어려운 부분이 있었는데요.</p>
<p>하지만 몇 개월의 시간이 지난 지금, 
실제로 얼마나 변화가 있었는 지 비교를 위해 본격적으로 SEO 구조 개선을 시작하기 전과 후를 기준으로 해서 현재까지 개선된 <strong>총 6개월간의 지표 변화</strong>를 알아보겠습니다 📊</p>
<h2 id="span-stylecolor-1dcd9fahrefsgoogle-search-console-기반-주요-지표-비교span"><span style="color: #1DCD9F">Ahrefs/Google Search Console 기반 주요 지표 비교</span></h2>
<p><img src="https://velog.velcdn.com/images/grinding_hannah/post/4b72303d-9939-4b97-8984-b7fef755f35c/image.png" alt=""></p>
<h3 id="📊-google-search-console-검색결과-seo-개선-전후-3개월-비교">📊 Google Search Console 검색결과 SEO 개선 전후 3개월 비교</h3>
<table>
<thead>
<tr>
<th>지표</th>
<th>개선 전 3개월</th>
<th>개선 후 3개월</th>
<th>변화 폭</th>
<th>해석</th>
</tr>
</thead>
<tbody><tr>
<td>클릭 수</td>
<td>492</td>
<td><span style="color: #FE5D26"><strong>1,180</strong></span></td>
<td><span style="color: #FE5D26">🔺 <strong>+140% 증가</strong></span></td>
<td><span style="color: #FE5D26"><strong>실제 검색 유입 수가 2배 이상 증가</strong></span></td>
</tr>
<tr>
<td>노출 수</td>
<td>37,100</td>
<td><span style="color: #FE5D26"><strong>126,000</strong></span></td>
<td>🔺 <span style="color: #FE5D26"><strong>+239% 증가</strong></span></td>
<td><span style="color: #FE5D26"><strong>검색 결과에 노출된 횟수 3배 이상 증가</strong></span></td>
</tr>
<tr>
<td>CTR</td>
<td>1.3%</td>
<td>0.9%</td>
<td>🔻 -0.4% 하락</td>
<td>노출 증가에 비해 클릭률은 낮아짐</td>
</tr>
<tr>
<td>평균 순위</td>
<td>30.9</td>
<td>35.4</td>
<td>🔻 -4.5 하락</td>
<td>전체 키워드 기준 평균 순위는 다소 떨어짐</td>
</tr>
</tbody></table>
<p>최근 3개월간의 SEO 개선 결과를 이전 3개월과 비교해본 결과,
<span style="color: #FE5D26"><strong>검색 클릭 수는 약 2.4배, 검색 노출 수는 약 3.4배 증가</strong></span>했어요.</p>
<p>특히 콘텐츠 확장, 구조화 데이터, sitemap 자동화 등 다양한 개선 작업 이후
검색 노출이 크게 증가했고, 검색 유입 수 또한 눈에 띄게 상승했다는 걸 알 수 있었어요.</p>
<p>반면 CTR(노출된 횟수 중 실제로 클릭된 비율)은 1.3% → 0.9%로 다소 하락했고,
평균 순위도 30.9위에서 35.4위로 소폭 낮아졌지만,
이는 노출 키워드 폭이 넓어졌기 때문에 상대적으로 클릭률이 낮아진 것일 수 있어요. 
그리고 이런 경우 <strong>&#39;롱테일 키워드&#39;</strong> 비중이 증가한 것으로도 해석할 수 있다고 합니다. </p>
<blockquote>
<p>📈 <strong>‘롱테일 키워드 비중이 증가했다’는 건 무슨 뜻?</strong></p>
</blockquote>
<p>기존에는 주로 브랜드명, 서비스명 등 짧고 인기 있는 키워드 위주로 노출되던 페이지가,
보다 구체적이고 긴 문장형 검색어에서도 노출되기 시작했다는 의미</p>
<blockquote>
<p>ex. <code>SEO</code> 로만 유입되던 페이지가 이제는 <code>SEO 올리는 방법</code>, <code>프론트엔드 SEO 개선</code> 등으로도 유입된다는 뜻</p>
<p><em>왜 좋은 현상일까?</em>
롱테일 키워드는 경쟁이 약하고 전환율이 높기 때문에 전체 유입량이 자연스럽게 늘어나요. 
그리고 컨텐츠가 검색자의 구체적인 질문/의도에 맞게 잘 구성됐다는 증거이기도 합니다 :) </p>
</blockquote>
<p>👉 <span style="color: #4D55CC"><strong>결론적으로, 더 많은 키워드에서 넓은 노출이 일어나고 있고, 
CTR과 상위 노출 키워드 비중을 높이는 2단계 최적화가 필요한 시점이라고 볼 수 있어요 🌈</strong></span></p>
<hr>
<h1 id="span-stylecolor-222222referencespan"><span style="color: #222222">Reference</span></h1>
<p> <a href="https://developers.google.com/search/docs/appearance/structured-data/search-gallery?hl=ko">Google 검색에서 지원하는 구조화된 데이터 마크업</a>
 <a href="https://github.com/garmeeh/next-seo">next-seo github</a>
 <a href="https://ahrefs.com/ko/">Ahrefs.com</a>
 <a href="https://nextjs.org/learn/seo/xml-sitemaps">Next.js Sitemap.xml</a>
 <a href="https://chromewebstore.google.com/detail/seo-meta-in-1-click/bjogjfinolnhfhkbipphpdlldadpnmhc?pli=1">SEO Meta 익스텐션</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 달력 컴포넌트 만들기 회고록]]></title>
            <link>https://velog.io/@grinding_hannah/TIL-%EB%8B%AC%EB%A0%A5-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@grinding_hannah/TIL-%EB%8B%AC%EB%A0%A5-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Mon, 20 Sep 2021 00:18:25 GMT</pubDate>
            <description><![CDATA[<p>React로 UI 라이브러리 <em>사용없이</em> Moment.js만으로 달력 컴포넌트 만들기 📅</p>
<hr>
<h1 id="설계">설계</h1>
<h3 id="컴포넌트는-어떻게-나눌까">컴포넌트는 어떻게 나눌까?</h3>
<p>여지껏 기획된 화면 위에 네모를 그리는 방식으로 으레 컴포넌트 설계를 해왔었는데 최근에 피드백을 받으며 크게 깨닫게 된 사실이 있다: <em>*<em>UI를 기준으로 컴포넌트를 쪼개는 건 좋은 방법이 아니다!! *</em></em>
그래서 앞으로 컴포넌트를 설계할 때 고려할 기준 2가지를 정했다:</p>
<blockquote>
<ol>
<li><span style="color:#3B3B98"><strong>행위</strong></span>가 들어가면 컴포넌트화 하자.</li>
<li>각 컴포넌트가 <span style="color:#3B3B98"><strong>어떤 상태</strong></span>를 가져야 하는지를 계획하자.<ul>
<li><code>상태</code>란? 꼭 변해야만 상태는 아니다. 요일(월, 화, 수...)과 같은 데이터는 &quot;정적상태&quot;를 가진 컴포넌트가 될 수 있다. </li>
</ul>
</li>
</ol>
</blockquote>
<ul>
<li><p><strong>공유하는 상태를 기준으로 만든 첫 설계도</strong> 
<img src="https://images.velog.io/images/grinding_hannah/post/64915719-0afc-4876-80c9-ef79b1552897/image.png" alt="">
위에서 보여지는 노란색 글씨는 공유될 것이라고 예상되었던 상태들의 모양이다. 
결국 구현하는 과정에서 상태에 프로퍼티들이 계속 추가되어서 상태는 설계된 모습과는 많이 달라졌다. 하지만 이렇게 기준을 세우고 나니 기본적으로 컴포넌트 계층 구조는 확립이 되어서 첫 설계도와 같은 구조를 갖게 되었다. </p>
</li>
<li><p><strong>최종 컴포넌트 설계도</strong>
<img src="https://images.velog.io/images/grinding_hannah/post/f0d79b5b-2000-40f5-bd42-9560a3180ee6/image.png" alt=""></p>
</li>
</ul>
<h3 id="날짜는-어떻게-채울까">날짜는 어떻게 채울까?</h3>
<p>방법은 여러가지인 것 같아 나만의 가설을 세워보기로 했다. </p>
<p><img src="https://images.velog.io/images/grinding_hannah/post/ca74c2c8-539e-4cf2-af1b-122414cc4fea/image.png" alt="">
<img src="https://images.velog.io/images/grinding_hannah/post/82ad78d3-0dfa-4a88-b0e5-6d8d8e97af8a/image.png" alt="">
배열로 관리되는 날짜부분을 분석해보니 <code>지난 달의 마지막 날짜들</code> + <code>이번 달의 모든 날짜들</code> + <code>다음 달의 처음 날짜들</code> 의 조합이라는 결론이 난다. 
각각의 배열을 만들어서 하나의 배열로 합쳐준 뒤 map을 돌려 하나의 컴포넌트에 전부 보여지게 만들자. 
그렇다면 각 날짜들을 구하기 위해 필요한 정보는 무엇일까?</p>
<h4 id="이번-달의-모든-날짜들-datesofcurrmonth"><strong>이번 달의 모든 날짜들 (datesOfCurrMonth)</strong></h4>
<p>요일에 알맞게 차례대로 날짜를 채워넣으려면 <strong>첫 날(매월 1일)이 무슨 요일인지</strong>만 알면된다.
· 필요한 정보: <code>firstDayOfThisMonth</code></p>
<h4 id="지난-달의-마지막-날짜들-datesoflastmonth"><strong>지난 달의 마지막 날짜들 (datesOfLastMonth)</strong></h4>
<p>이번 달의 1일이 무슨 요일인지에 따라 채워줄 지난 달의 날짜 갯수가 달라진다. 
일주일은 7일이기 때문에 필요한 지난 달의 날짜 갯수는 7 - <strong>이번달 1일의 요일(인덱스 정보)</strong>이 된다.
그리고 지난 달의 마지막 날로부터 필요한 날짜 갯수만큼 countdown 해야하기 때문에 <strong>지난 달의 마지막 날짜정보</strong>가 필요하다. 
두 정보가 구해지면 배열에 들어갈 가장 첫 날짜(지난 달의 마지막 날짜 - 이번달 1일의 요일 인덱스 +1)를 찾아서 마지막 날짜에 도달할 때까지 하나씩 더해주며 배열에 push한다.<br>· 필요한 정보: <code>lastDateOfLastMonth</code>, <code>firstDayOfThisMonth</code></p>
<h4 id="다음-달의-처음-날짜들-datesofnextmonth"><strong>다음 달의 처음 날짜들 (datesOfNextMonth)</strong></h4>
<p>다음 달의 날짜들은 무조건 1부터 채워지기 때문에 필요한 날짜 갯수만 알면 된다. 
이 또한 <strong>이번 달의 마지막 날이 무슨 요일(인덱스)인지</strong> 알면 채워줘야 할 다음 달의 날짜 갯수, 즉, 배열의 길이가 계산된다(6 - 이번 달 마지막 날의 요일 인덱스). 
· 필요한 정보: <code>lastDayOfThisMonth</code></p>
<h1 id="momentjs-사용하기">Moment.js 사용하기</h1>
<h3 id="momentjs를-사용한-이유">Moment.js를 사용한 이유</h3>
<p><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date" target="_blank">Date객체</a>와 <a href="https://momentjs.com/docs/#/get-set/" target="_blank">Moment.js</a>사용을 두고 고민을 많이 했지만 결국 Moment를 사용하기로 결정한 데에는 이유가 있었다. 
<em>Moment는 내가 원하는 형식으로 값을 반환해줘서 가공없이 바로 사용이 가능하기 때문!</em> 
JavaScript의 Date객체를 사용할 때는 사람이 읽을 수 있는 문자열의 형식으로 바꾸기 위해 <code>toDateString()</code>나 <code>toTimeString()</code>메서드로 한 번 더 데이터를 바꿔줘야 하는 번거로움이 있다.
그런데 Moment의 경우 <code>format()</code>으로 원하는 데이터의 형태(ex.<code>YYYY-MM-DD</code>, <code>DD</code> 등)만 직접 받을 수가 있어서 추가적인 데이터 가공이 필요하지 않다. </p>
<h3 id="사용방법">사용방법</h3>
<p>하지만 Moment.js도 처음 사용해보는 지라 사용법이 익숙치가 않아서 <a href="https://momentjs.com/docs/#/get-set/" target="_blank">공식문서의 Docs</a>를 많이 참고했다. 
설계 단계에서 필요한 데이터가 무엇인지 전부 뽑아놓았던 터라 원하는 데이터가 제공이 되는지만 확인하면 되었다. </p>
<h4 id="import"><strong>import</strong></h4>
<pre><code class="language-javascript">import * as moment from &quot;moment&quot;;</code></pre>
<h4 id="현재-보여지는-달의-마지막-날짜"><strong>현재 보여지는 달의 마지막 날짜</strong></h4>
<blockquote>
<ul>
<li>마지막 날짜를 구하는 방법:
<code>moment([2021, 0, 31]).month(1).format(&quot;YYYY-MM-DD&quot;); // 2021-02-28</code></li>
</ul>
</blockquote>
<pre><code class="language-js"> const lastDateOfCurrMonth = moment([currYear, 0, 31])
    .month(currMonth - 1)
    .format(&quot;DD&quot;); //&quot;DD&quot;는 일자에 해당하는 정보만 반환해준다.</code></pre>
<h4 id="이전-달의-마지막-날짜"><strong>이전 달의 마지막 날짜</strong></h4>
<pre><code class="language-js">  const lastDateOfLastMonth = moment([currYear, 0, 31])
    .month(currMonth - 2)
    .format(&quot;DD&quot;);</code></pre>
<h4 id="현재-보여지는-달-1일의-요일"><strong>현재 보여지는 달 1일의 요일</strong></h4>
<pre><code class="language-js">const firstDayOfThisMonth = moment([currYear, currMonth - 1, 1]).day();</code></pre>
<h4 id="현재-보여지는-달-마지막-날의-요일"><strong>현재 보여지는 달 마지막 날의 요일</strong></h4>
<pre><code class="language-js"> const lastDayOfThisMonth = moment([
    currYear,
    currMonth - 1,
    lastDateOfCurrMonth,
  ]).day();</code></pre>
<h1 id="custom-hook-만들기">Custom Hook 만들기</h1>
<ul>
<li><strong>Calendar 계층 구조</strong>
<img src="https://images.velog.io/images/grinding_hannah/post/a0d49d20-28a9-4834-bfaf-6fbd875a2ccb/image.png" alt=""></li>
</ul>
<p>Redux로 전역상태를 만들기 전, 로컬에서 필요한 상태를 정의해서 내려주는 방식을 사용했다. 
<code>Calendar</code>가 캘린더를 구성하는 가장 상위 컴포넌트이고, <code>CalendarButtons</code>와 <code>DatesOfMonth</code>는 공유하는 상태가 있어서 달력에 관한 로직을 전부 부모 컴포넌트인<code>Calendar</code>에서 관리했었다. </p>
<p>이 때 마주했던 문제:</p>
<ol>
<li><strong>날짜관련 로직이 하나둘 추가되다 보니 Calendar 컴포넌트가 상당히 커졌다.</strong></li>
<li>*<em>하위 컴포넌트로 전달하는 props가 너무 많아졌다. *</em></li>
</ol>
<p>그래서 <strong>실제로 쓰이는 곳에서만 관련 함수를 import 해서 쓸 수 있는 <code>useCalendar.js</code> custom hook</strong>을 만들어 캘린더 로직과 관련된 부분을 전부 migrate했다. 
동시에 Redux를 설치해 공통으로 쓰이는 상태값에 대해서는 전역(global)으로 관리될 수 있도록 했는데, <code>useCalendar</code> 내부에서 <code>useSelector()</code>로 필요한 상태만 가져오는 것이 가능해졌다.
기존에 props로 전달했었던 로컬 상태와 계산 로직이 custom hook으로 옮겨가면서 컴포넌트가 심플해졌고, props의 갯수가 상당부분 줄어들었다. </p>
<ul>
<li>*<em>커스텀 훅을 만들기 전과 후 <code>Calendar.jsx</code> 비교 *</em>
Before(좌) ➡️ After(우) 
<img src="https://images.velog.io/images/grinding_hannah/post/88e4b317-bb90-41c4-8349-3c60a9c8facf/image.png" alt="">
<img src="https://images.velog.io/images/grinding_hannah/post/9c2558f1-3dd6-4a10-9d37-7943cf63a45a/image.png" alt=""></li>
</ul>
<h1 id="button-컴포넌트-만들기">Button 컴포넌트 만들기</h1>
<p>이번 캘린더 컴포넌트에는 (개별 날짜를 감싸는 버튼을 제외하고)3개의 버튼이 필요한데, 스타일으로 variation을 준다면 버튼자체를 컴포넌트화해서 재사용할 수 있을 것이란 생각이 들었다. 
다음은 구현한 Button컴포넌트의 모습이다:</p>
<pre><code class="language-js">import React from &quot;react&quot;;
import { css } from &quot;styled-components&quot;;
import { style } from &quot;./ButtonStyles&quot;;

const { PrevArrow, NextArrow, StyledButton } = style;

const ICON = {
  prev: PrevArrow,
  next: NextArrow,
};

const VARIANT = {
  previous: css`
    margin: 0.5rem 0 0.5rem 0.5rem;
  `,

  next: css`
    margin: 0.5rem;
  `,

  thisMonth: css`
    width: 70px;
    margin: 0.5rem;
    font-size: 0.8rem;
  `,
};

const Button = ({ icon, name, handleClickFunc, children }) =&gt; {
  const varientStyle = VARIANT[name];
  const contentsSelector = (icon) =&gt; {
    if (icon) return React.createElement(ICON[icon]);
    else return children;
  };

  return (
    &lt;StyledButton varientStyle={varientStyle} onClick={handleClickFunc}&gt;
      {contentsSelector(icon)}
    &lt;/StyledButton&gt;
  );
};

export default Button;</code></pre>
<h3 id="☄️트러블슈팅">☄️트러블슈팅</h3>
<p>세 버튼의 차이점은 버튼의 컨텐츠이기 때문에 <code>contentsSelector</code>라는 함수를 정의해 버튼 이름을 기준으로 컨텐츠를 다르게 반환할 수 있게 했다. 그런데 화살표 버튼의 경우 꺽쇠모양의 SVG파일이 컨텐츠로 들어가야 했고,<code>return ICON[icon]</code>은 계속 에러를 반환했다. 
알고보니 <code>{contentsSelector(icon)}</code> 이 부분에서는 컴포넌트를 반환해주기를 기대하는데, SVG 파일 자체만으로는 컴포넌트가 아니라 그저 SVG파일 그 자체에 불과했기 때문이었다. 
따라서 <code>React.createElement()</code>를 사용해 컴포넌트화 시켜 반환해줌으로써 에러를 해결했다. </p>
<blockquote>
<p> 각 JSX 엘리먼트는 단지 React.createElement()를 호출하는 편리한 문법에 불과합니다. 
-from. <a href="https://ko.reactjs.org/docs/react-api.html" target="_blank">React 공식홈</a></p>
</blockquote>
<h1 id="배포하기">배포하기</h1>
<h3 id="github-pages-배포방법">github pages 배포방법</h3>
<ol>
<li><p><code>release</code>(배포용) 브랜치 생성 후 build 파일 생성
build파일이 만들어지면 commit 후 원격 리포지토리에 push한다. </p>
<pre><code class="language-js">//build 파일 생성
yarn build</code></pre>
</li>
<li><p>github repository &gt; Settings &gt; Pages에서 release브랜치로 주소 발급받기
<img src="https://images.velog.io/images/grinding_hannah/post/d76d7948-d65a-455c-8016-0f8b5282f65d/image.png" alt=""></p>
</li>
<li><p>gh-pages 패키지를 설치</p>
<pre><code class="language-js">//글로벌로 gh-pages 패키지 설치
npm install -g gh-pages </code></pre>
</li>
<li><p>package.json 파일을 열어 script 설정 + &quot;homepage&quot; 주소 추가</p>
<pre><code class="language-js">//package.json
{...,
&quot;scripts&quot;: {
     ...
   &quot;predeploy&quot;: &quot;yarn build&quot;, //로컬에서 build 진행 =&gt; build 디렉토리가 생성됨
   &quot;deploy&quot;: &quot;gh-pages -d build&quot; //build 디렉토리의 html을 배포하겠다는 뜻 (d === directory)
},...
&quot;homepage&quot;: &quot;https://ha3158987.github.io/Paywork_Calendar/&quot;
}</code></pre>
</li>
<li><p>Pages에서 배포 Source 브랜치를 gh-pages로 설정
<img src="https://images.velog.io/images/grinding_hannah/post/ed276437-fd0a-41de-90c2-2d87801554a2/image.png" alt=""></p>
</li>
<li><p>yarn build로 predeploy 실행</p>
<pre><code class="language-js">yarn build</code></pre>
</li>
</ol>
<h3 id="☄️트러블슈팅-1">☄️트러블슈팅</h3>
<p>yarn으로 모든 패키지를 관리해왔기 때문에 gh-pages 패키지 또한 <code>yarn add gh-pages --dev</code>로 설치를 시도했었는데, <code>yarn build</code> 스크립트를 입력해 빌드를 시도하자 gh-pages가 없다는 에러메세지가 나왔다. 
검색결과 gh-pages는 <strong>글로벌로 설치</strong>해야 하는 것으로 확인해 <code>npm install -g gh-pages</code>로 재설치한 후 실행했더니 정상적으로 빌드가 진행됐다. </p>
<p>또 하나, 처음 repository에 Source 브랜치를 설정하면서 디렉토리 설정이 잘못되는 바람에 배포된 주소에서 프로젝트가 아닌 readme가 보여지는 문제가 있었다. 
디렉토리 설정을 다시 한 다음에 배포를 다시 시도해봤지만 브라우저 상에는 여전히 리드미만 보여져서 <code>개발자도구</code> &gt; <code>Application</code> &gt; <code>Storage</code> &gt; <code>Clear site data</code> 로 <strong>스토리지에 저장된 브라우징 데이터를 초기화</strong> 한 후 다시 실행해 보았다. 
<img src="https://images.velog.io/images/grinding_hannah/post/899be43b-cfc2-49f9-b6a0-27fb477f930d/image.png" alt="">
정상적으로 배포 성공! 🌈</p>
<h1 id="아웃풋">아웃풋</h1>
<h2 id="a-hrefhttpsha3158987githubiopaywork_calendar-target_blank✨-배포링크-✨a"><a href="https://ha3158987.github.io/Paywork_Calendar/" target="_blank">✨ 배포링크 ✨</a></h2>
<p><img src="https://images.velog.io/images/grinding_hannah/post/fabda1df-8408-49d5-8360-79b8728f12d5/image.png" alt=""></p>
<hr>
<h2 id="reference">Reference</h2>
<p><a href="https://www.daleseo.com/react-button-component/">https://www.daleseo.com/react-button-component/</a>
<a href="https://dev-yakuza.posstree.com/ko/react/github-pages/">https://dev-yakuza.posstree.com/ko/react/github-pages/</a>
<a href="https://velog.io/@byjihye/react-github-pages">https://velog.io/@byjihye/react-github-pages</a>
<a href="https://ko.reactjs.org/docs/react-api.html">https://ko.reactjs.org/docs/react-api.html</a>
<a href="https://medium.com/react-native-seoul/react-%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-02-react-createelement%EC%99%80-react-component-%EA%B7%B8%EB%A6%AC%EA%B3%A0-reactdom-render%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-41bf8c6d3764">https://medium.com/react-native-seoul/react-%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-02-react-createelement%EC%99%80-react-component-%EA%B7%B8%EB%A6%AC%EA%B3%A0-reactdom-render%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-41bf8c6d3764</a>
<a href="https://www.howtogeek.com/664912/how-to-clear-storage-and-site-data-for-a-single-site-on-google-chrome/">https://www.howtogeek.com/664912/how-to-clear-storage-and-site-data-for-a-single-site-on-google-chrome/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL]  codesandbox 에서 local 전환기]]></title>
            <link>https://velog.io/@grinding_hannah/TIL-codesandbox-%EC%97%90%EC%84%9C-local-%EC%A0%84%ED%99%98%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/TIL-codesandbox-%EC%97%90%EC%84%9C-local-%EC%A0%84%ED%99%98%EA%B8%B0</guid>
            <pubDate>Sat, 21 Aug 2021 12:56:00 GMT</pubDate>
            <description><![CDATA[<h2 id="코드-샌드박스의-리액트-환경을-로컬에서-돌려보자"><strong>코드 샌드박스의 리액트 환경을 로컬에서 돌려보자</strong></h2>
<h3 id="1-npm-install이-안되는-문제가-발생했다">1. <code>npm install</code>이 안되는 문제가 발생했다.</h3>
<p><img src="https://images.velog.io/images/grinding_hannah/post/7266734b-7bfe-4174-8838-09c257b0165e/image-20210821214000243.png" alt=""></p>
<ul>
<li>원인 : 타입스크립트의 버전이 맞지 않은 것.</li>
<li>해결책 : 필요한 설치를 위해 <code>--legacy-peer-deps</code> 옵션으로 설치를 해보자<ul>
<li><code>--force</code><ul>
<li>The <code>f</code> or <code>-force</code> argument will force npm to fetch remote resources even if a local copy exists on disk.  </li>
</ul>
</li>
<li><code>--legacy-peer-deps</code> <ul>
<li><code>--legacy-peer-deps</code>: ignore all peerDependencies when installing, in the style of npm version 4 through version 6.</li>
<li>peerDependencies : 일반적으로 dependency는 내가 만든 모듈에서 사용하는 패키지들을 지정하는 반면, peerDependencies는 반대로 내가 만든 모듈이 다른 모듈과 함께 동작할 수 있다는 호환성을 표시하는 것이다. 마치 gulp가 있다면 내가 만든 모듈은 gulp의 플러그인 중 하나인 것이다. 이때 내가 만든 모듈이 gulp의 모든 버전이 아니라 1.3 버전과만 동작한다면 그런 정보를 표시해야하는데 이때 사용하는 것이 peerDependencies 인 것이다  </li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<ul>
<li><strong>npm github blog</strong><pre><code>      - You have the option to retry with --force to bypass the conflict or --legacy-peer-deps command to ignore peer dependencies entirely (this behavior is similar to versions 4-6).</code></pre></li>
</ul>
</blockquote>
<br>


<h3 id="2-이후-실행에서는-다음의-문제가-발생하였다">2. 이후 실행에서는 다음의 문제가 발생하였다.</h3>
<pre><code class="language-shell">    $ npm start

    &gt; todo-list@1.0.0 start
    &gt; react-scripts start

    /Users/Downloads/todo-list/node_modules/react-scripts/scripts/utils/verifyTypeScriptSetup.js:239
          appTsConfig.compilerOptions[option] = value;
                                              ^

    TypeError: Cannot assign to read only property &#39;jsx&#39; of object &#39;#&lt;Object&gt;&#39;
        at verifyTypeScriptSetup (/Users/Downloads/todo-list/node_modules/react-scripts/scripts/utils/verifyTypeScriptSetup.js:239:43)
        at Object.&lt;anonymous&gt; (/Users/Downloads/todo-list/node_modules/react-scripts/scripts/start.js:31:1)
        at Module._compile (node:internal/modules/cjs/loader:1091:14)
        at Object.Module._extensions..js (node:internal/modules/cjs/loader:1120:10)
        at Module.load (node:internal/modules/cjs/loader:971:32)
        at Function.Module._load (node:internal/modules/cjs/loader:812:14)
        at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
        at node:internal/main/run_main_module:17:47</code></pre>
<ul>
<li>원인 : <code>react-scripts</code> 버전이 상용버전보다 낮기 때문<ul>
<li>프로젝트에 설정해 둔 <code>react-scripts</code> 의 버전은 4.0.0 버전이었다.</li>
</ul>
</li>
<li>해결책 : 해당 버전을 CRA버전과 맞춰보자.</li>
</ul>
<br>

<h3 id="3-오-코드가-올라갔다-하지만-분명-문제가-없는-코드인데-문제가-발생했다">3. 오! 코드가 올라갔다. 하지만 분명 문제가 없는 코드인데 문제가 발생했다.</h3>
<p><img src="https://images.velog.io/images/grinding_hannah/post/3b3dff6b-c78b-4bd0-9be9-7b672c299157/image-20210821220055421.png" alt=""></p>
<ul>
<li>원인 : <em>추측이다</em>. 에러를 내는 것은 타입스크립트 프로젝트에 대한 lint 단계에서의 에러였다. 그렇다면 lint에서 어떤 문제가 있는 것이 아닐까?</li>
</ul>
<p>그러다가 이런 이슈를 발견했다. 
<a href="https://github.com/babel/babel-eslint/issues/832">https://github.com/babel/babel-eslint/issues/832</a></p>
<p>이슈에서는 deprecated된 babel-eslint를 사용하고 있어서 에러가 났던 것이라 하고 있었다.</p>
<p>그래서 내 node_modules를 확인해보니 아니 여기에 babel-eslint가 자리하고 있는게 아닌가!
<img src="https://images.velog.io/images/grinding_hannah/post/c8b9be0e-9a80-4d80-b320-3b8e0a774a8a/image-20210821220624905.png" alt=""></p>
<ul>
<li>해결책 : <code>babel-eslint</code> 를 <code>@babel/eslint-parser</code>로 바꿔보자<ul>
<li><code>npm uninstall babel-eslint babel-eslint-plugin</code></li>
<li><code>npm install --save-dev @babel/eslint-parser @babel/eslint-plugin</code></li>
</ul>
</li>
</ul>
<p><img src="https://images.velog.io/images/grinding_hannah/post/04a4f679-de71-4396-8723-2be7793200d5/image-20210821221608800.png" alt=""></p>
<ul>
<li>설치 후 parser를 지정해주자</li>
</ul>
<pre><code class="language-js">// .eslintrc
{
   &quot;parser&quot;: &quot;@babel/eslint-parser&quot;
}</code></pre>
<br>

<h3 id="4-그러고-나니-에러는-달라졌지만-아예-컴파일조차-되지-않았다">4. 그러고 나니 에러는 달라졌지만, 아예 컴파일조차 되지 않았다.</h3>
<pre><code class="language-shell">    Failed to compile.

    src/App.tsx
      Line 0:  Parsing error: No Babel config file detected for /Users/Downloads/todo-list/src/App.tsx. Either disable config file checking with requireConfigFile: false, or configure Babel so that it can find the config files

    src/components/common/Loading.tsx
      Line 0:  Parsing error: No Babel config file detected for /Users/Downloads/todo-list/src/components/common/Loading.tsx. Either disable config file checking with requireConfigFile: false, or configure Babel so that it can find the config files

    src/components/common/Spinner.tsx
      Line 0:  Parsing error: No Babel config file detected for /Users/Downloads/todo-list/src/components/common/Spinner.tsx. Either disable config file checking with requireConfigFile: false, or configure Babel so that it can find the config files
</code></pre>
<ul>
<li>추측하기로는 <code>@babel/eslint-parser</code> 를 사용하기로 했는데 babel.config.js 파일같은 config 파일이 없는 것으로 보였다.</li>
<li>원인 : babel.config를 지정하거나 requireconfigFile 옵션을 설정하지 않은 것.</li>
<li>해결책 : 기본적으로 코드샌드박스 환경도 CRA를 타고 온 것이라 바벨 세팅을 함부로 바꿀 수 없다는 생각이 들었다. 따라서 파서 옵션을 바꿔줬다.</li>
</ul>
<pre><code class="language-js">    // .eslintrc
    {
      &quot;parser&quot;: &quot;@babel/eslint-parser&quot;,
      &quot;parserOptions&quot;: {
        &quot;requireConfigFile&quot;: false,
      },
    }
</code></pre>
<br>

<h3 id="5-에러-로그가-바뀌었다">5. 에러 로그가 바뀌었다.</h3>
<pre><code class="language-shell">    Failed to compile.

    src/App.tsx
      Line 11:4:  Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): &#39;jsx, flow, typescript&#39; (11:4)

    src/components/common/Loading.tsx
      Line 5:  Parsing error: Unexpected reserved word &#39;interface&#39;. (5:0)

    src/components/common/Spinner.tsx
      Line 8:  Parsing error: Unexpected reserved word &#39;interface&#39;. (8:0)</code></pre>
<ul>
<li><p><code>.tsx</code>에서 에러를 내는 것을 보니 이것은 typescript 관련된 셋업이 되지 않아서이다. 이상하다. 분명 타입스크립트 셋업은 되어있을텐데. 여기서 감을 잡았다. 우리는 eslint의 설정만 잡아주면 되는 것이었다.</p>
<ul>
<li>원인 : @babel/eslint-parser의 옵션을 리액트, 타입스크립트 셋업을 하지 않은 것</li>
<li>해결책 : react, ts 를 적용한 babel의 옵션을 담은 형태로 lint 셋업하기</li>
</ul>
</li>
</ul>
<pre><code class="language-js">        // .eslintrc
        {
          &quot;parser&quot;: &quot;@babel/eslint-parser&quot;,
          &quot;plugins&quot;: [&quot;@babel&quot;],
          &quot;parserOptions&quot;: {
            &quot;requireConfigFile&quot;: false,
            &quot;babelOptions&quot;: {
              &quot;presets&quot;: [&quot;@babel/preset-react&quot;, &quot;@babel/preset-typescript&quot;]
           },
          },
        }
</code></pre>
<ul>
<li>이를 위해 필요한 babel preset을 함께 설치한다.</li>
<li><code>npm install @babel/preset-typescript @babel/preset-env @babel/preset-react --save-dev</code></li>
</ul>
<br>

<h3 id="짠"><strong>짠!</strong></h3>
<p><img src="https://images.velog.io/images/grinding_hannah/post/bdeae044-2846-41d2-8fb9-a100e2350caf/image.png" alt=""></p>
<h3 id="끝👋"><strong>끝👋</strong></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 비동기와 이벤트루프]]></title>
            <link>https://velog.io/@grinding_hannah/JavaScript-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%99%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A3%A8%ED%94%84</link>
            <guid>https://velog.io/@grinding_hannah/JavaScript-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%99%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A3%A8%ED%94%84</guid>
            <pubDate>Fri, 13 Aug 2021 16:31:40 GMT</pubDate>
            <description><![CDATA[<h2 id="동기--비동기">동기 &amp; 비동기</h2>
<h3 id="자바스크립트는-single-thread">자바스크립트는 Single Thread?</h3>
<blockquote>
<p><strong>자바스크립트 엔진는 단 하나의 실행 컨텍스트 스택(Stack)을 갖는다.</strong> 
한 번에 하나의 태스크만 실행할 수 있는 <strong>싱글 쓰레드(Single Thread)</strong> 방식으로 동작하는 것이다. </p>
</blockquote>
<p>자바스크립트에서 함수가 실행되려면 각 함수의 &quot;실행 컨텍스트&quot;가 아래의 그림과 같이 Stack에 푸시되어야 한다. 
자바스크립트 엔진은 단 하나의 실행 컨텍스트 Stack을 갖고 있기 때문에 동시에 2개 이상의 함수를 실행할 수가 없다. 
함수들은 Stack의 최상위에서부터 차례대로(LIFO - Last in, first out) 팝되어 실행된다. </p>
<blockquote>
<img src="https://miro.medium.com/max/4800/1*rRoLpv-Zrmpa-srNhwlbvA.gif" />
</blockquote>
<p>*gif 출처: https://medium.com/jspoint/how-javascript-works-in-browser-and-node-ab7d0d09ac2f</p>



<h3 id="blocking--non-blocking">Blocking &amp; Non-blocking</h3>
<p>한 번에 하나의 태스크만 실행할 수 있는 싱글 쓰레드 방식이기 때문에, 처리에 시간이 걸리는 태스크를 실행할 경우, <strong>블로킹(Blocking)</strong>이 발생되어 해당 함수의 처리가 끝날 때까지 작업이 중단된다. 
이처럼 <strong>동기 처리 방식</strong>은 앞선 태스크가 종료될 때까지 이후 태스크들이 블로킹이 된다.  </p>
<blockquote>
<p><img src="https://images.velog.io/images/grinding_hannah/post/d0c2050c-2d1f-4e7d-9020-571977262f18/image.png" alt="">*그림 출처: 이웅모, 『모던 자바스크립트 Deep Dive』, 위키북스(2020)</p>
</blockquote>
<p>반면, <strong>비동기 처리 방식</strong>은 현재 실행중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하므로 블로킹이 발생하지 않는다(<strong>Non-blocking</strong>).</p>
<blockquote>
<p><img src="https://images.velog.io/images/grinding_hannah/post/bd39a290-104f-47a6-818e-9b96f148dd2f/image.png" alt="">*그림 출처: 이웅모, 『모던 자바스크립트 Deep Dive』, 위키북스(2020)</p>
</blockquote>
<p>그렇다면_ <strong>어떻게 자바스크립트 엔진이 비동기 처리 방식으로 동작할 수 있는 걸까?</strong> _
정답은 바로 <strong>이벤트 루프(Event Loop)</strong>에 있다! </p>
<hr>
<h2 id="이벤트-루프">이벤트 루프</h2>
<p>&quot;이벤트 루프는 자바스크립트에 <strong>동시성(concurrency)</strong>을 지원한다.&quot;</p>
<h3 id="브라우저에서의-자바스크립트-작동방식">브라우저에서의 자바스크립트 작동방식</h3>
<blockquote>
<img src="https://miro.medium.com/max/1400/1*lZ-KXoVNUSOwaq7q8zUBDg.png" />
</blockquote>
<p>*그림 출처: https://medium.com/jspoint/how-javascript-works-in-browser-and-node-ab7d0d09ac2f</p>

<ul>
<li><strong><code>Web APIs</code></strong>: DOM관련 작업, window 관련 작업(setTimeout, setInterval 등), AJAX요청 관련 작업(fetch, XMLHttpRequest 등)과 같은 기능들을 자바스크립트와 함께 연동하여 사용할 수 있게 브라우저에서 제공하는 추가 리소스.</li>
<li><strong><code>Event Loop</code></strong>: Stack에 현재 실행중인 실행 컨텍스트가 있는지, 그리고 callback queue에 대기 중인 함수가 있는지 반복적으로 확인한다. 만일 Stack이 비어있고, 대기 중인 함수가 있다면 이벤트 루프는 선입선출(FIFO - First in, first out) 방식으로 callback queue에 있는 함수를 Stack으로 이동시킨다. 이때 Stack으로 옮겨간 함수는 실행된다. </li>
<li><strong><code>callback queue</code></strong>: 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역.</li>
</ul>
<pre><code class="language-js">function one() {
  console.log(&quot;setTimeout 콜백 실행&quot;);
}

function two() {
  console.log(&quot;two 실행&quot;);
}

setTimeout(one, 0);
two();

/*
출력결과:
&quot;two 실행&quot;
&quot;setTimeout 콜백 실행&quot;
*/</code></pre>
<p>위 예시를 기준으로 동작순서를 알아보자!</p>
<ol>
<li>전역 실행 컨텍스트가 생성되고 Stack에 푸시된다. </li>
<li>전역 코드가 실행되어 <code>setTimeout</code> 함수가 호출된다. 이 때 <code>setTimeout</code> 함수의 함수 실행 컨텍스트가 생성되고 Stack에 푸시되어 실행된다. 그리고 Web API인 타이머 함수가 설정된다. </li>
<li>브라우저와 자바스크립트 엔진은 다음의 태스크들을 <strong>동시에</strong> 처리한다:<ul>
<li>브라우저는 타이머를 설정하고 타이머의 만료를 기다린다. 타이머가 만료되면 콜백함수인  <code>one</code>을 callback queue에 푸시한다. callback queue에 푸시된 <code>one</code>함수는 Stack이 빌때까지 대기하게 된다. <ul>
<li><code>two</code>함수가 호출되어 함수 실행 컨텍스트가 생성되고, Stack에 푸시된다. 이후 <code>two</code>함수가 종료되면 Stack에서 팝된다. 이 때 <code>one</code>함수는 아직 callback queue에서 대기중인 상태이다.</li>
</ul>
</li>
</ul>
</li>
<li>전역 코드 실행이 종료되고 전역 실행 컨텍스트가 Stack에서 팝된다. 이제 Stack에는 아무런 실행 컨텍스트도 존재하지 않게 된다. </li>
<li>이벤트 루프가 Stack이 비었음을 감지하고, callback queue에서 대기 중인 <code>one</code>을 Stack에 푸시한다. 이후 <code>one</code>실행이 종료되면 Stack에서 팝된다. </li>
</ol>
<p>사실 자바스크립트 엔진은 Heap과 Stack으로만 구성되어 있고, <strong>단순히 태스크가 요청되면 Stack을 통해 요청된 작업을 순차적으로 실행할 뿐이다</strong>. 
비동기 처리에서 스스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 <strong>브라우저</strong> 또는 <strong>Node.js</strong>가 담당한다. </p>
<p><strong>즉, 자바스크립트 엔진은 싱글 쓰레드로 동작하지만 브라우저는 멀티 쓰레드로 동작하는 것!</strong>⭐️⭐️⭐️ </p>
<br> 

<h3 id="job-queue-aka-micro-task-queue">Job Queue (a.k.a Micro Task Queue)</h3>
<p>위 그림에서는 등장하지 않지만 실은 Callback Queue와 비슷한 역할을 하는 <strong>&quot;Job Queue&quot;</strong>라는 것이 존재한다. HTML스펙에서는 <strong>microtask queue</strong>라는 이름으로 불리기도 한다. </p>
<p>ES6에서 소개된 Job Queue는 <strong>Promise를 사용할 때 콜백 함수 역할을 하는 thenable한 함수들이 추가되는 곳</strong>이다.
이 함수들은 Callback Queue가 아닌 Job Queue에 추가되었다가 Event Loop에 의해 Stack으로 옮겨져 실행된다. </p>
<blockquote>
<p>📝 <strong>사알짝 맛보는 Promise 후속 처리 메서드</strong> </p>
</blockquote>
<ul>
<li>프로미스의 처리 결과를 가지고 무엇을 할 것인지를 결정</li>
<li>프로미스 후속 처리 메서드: <code>then</code>, <code>catch</code>, <code>finally</code></li>
<li>언제나 Promise를 반환한다. = <strong>Promise Chaining</strong>을 쓸 수 있는 이유!<pre><code class="language-js">promiseGet(`${url}/posts/1`)
  .then({ userId } =&gt; promiseGet(`${url}/users/${userId}`))
  .then(userInfo =&gt; console.log(userInfo))
  .catch(err =&gt; console.error(err));</code></pre>
</li>
</ul>
<p>📌 Promise의 후속 처리 메서드(<code>then</code>, <code>catch</code>, <code>finally</code>)의 콜백 함수: <strong>Job queue / micro task queue</strong> 에 저장.
📌 비동기 함수, 이벤트 핸들러의 콜백함수: <strong>Callback queue / task queue</strong> 에 저장.</p>
<p>📌 Job Queue 와 Callback Queue의 우선순위: <strong>Job Queue</strong> &gt; *<em>Callback Queue *</em></p>
<p>Job Queue와 Callback Queue의 우선순위는 <a href="https://www.jsv9000.app/" target="_blank"><strong>이곳</strong></a>에서 시각화해 볼 수 있다.  </p>
<hr>
<h2 id="정리">정리</h2>
<p>👨: &quot;자, 이벤트 루프에 대해 아는대로 설명해 보세요.&quot; </p>
<p>🙋🏻‍♀️: &quot;넵, 동기함수들이 실행 컨텍스트를 생성해서 스택에 푸시되는 반면, 비동기 함수들의 콜백함수들은  callback queue 혹은 job queue로 푸시됩니다.</p>
<p>이벤트 루프는 Stack에 쌓인 동기 함수들이 모두 실행되어 Stack이 비는 때를 기다렸다가, job queue와  callback queue에서 순서대로 콜백함수들을 꺼내어 Stack으로 넣어주는 역할을 합니다.</p>
<p>자바스크립트가 싱글 쓰레드 방식으로 작동함에도 불구하고 이러한 비동기적 처리가 가능한 이유는 이벤트 루프가 브라우저의 구성요소이기 때문입니다. 즉, 자바스크립트 엔진은 싱글 쓰레드 방식으로 작동하지만, 브라우저는 멀티 쓰레드로 작동하기 때문입니다.&quot;</p>
<hr>
<h2 id="reference">Reference</h2>
<p>*본 포스팅은 아래 서적/사이트들을 참고 및 인용하여 작성되었습니다.
학습단계로 잘못된 정보가 있을 수 있습니다. 잘못된 부분에 대해 알려주시면 곧바로 정정하도록 하겠습니다 😊</p>
<p><a href="https://medium.com/sjk5766/javascript-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%95%B5%EC%8B%AC-event-loop-%EC%A0%95%EB%A6%AC-422eb29231a8">https://medium.com/sjk5766/javascript-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%95%B5%EC%8B%AC-event-loop-%EC%A0%95%EB%A6%AC-422eb29231a8</a>
<a href="https://medium.com/jspoint/how-javascript-works-in-browser-and-node-ab7d0d09ac2f">https://medium.com/jspoint/how-javascript-works-in-browser-and-node-ab7d0d09ac2f</a>
이웅모, 『모던 자바스크립트 Deep Dive』, 위키북스(2020)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Web] 큰 틀로 알아보는 브라우저 작동 원리]]></title>
            <link>https://velog.io/@grinding_hannah/Web-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@grinding_hannah/Web-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Fri, 13 Aug 2021 06:20:54 GMT</pubDate>
            <description><![CDATA[<h2 id="웹-브라우저가-하는-일">웹 브라우저가 하는 일</h2>
<p>한 마디로 정의하면, </p>
<blockquote>
<p>웹 페이지를 다운 받아 렌더링하고 보여주는 일. </p>
</blockquote>
<h3 id="개발자도구로-알아보는-웹-페이지-다운-프로세스">(개발자도구로 알아보는) 웹 페이지 다운 프로세스</h3>
<p><code>개발자도구 &gt; Network 탭 &gt; 새로고침(F5)</code> 으로 아래 화면 확인 가능.
<img src="https://images.velog.io/images/grinding_hannah/post/db7506ae-70a6-45e2-b98f-129d1a369805/image.png" alt=""></p>
<p><code>수신한 데이터 크기 !== 웹페이지 총 크기</code>인 이유는, 
웹 서버가 <strong>압축된 리소스</strong>를 전송했기 때문. </p>
<p>Response Headers의 content-encoding 항목에 &quot;<strong>gzip</strong>&quot; 이라고 써있다면 압축된 파일이라는 뜻.</p>
<p><img src="https://images.velog.io/images/grinding_hannah/post/cd157af3-88a3-4a1a-a520-b14635de5c47/image.png" alt=""></p>
<hr>
<h2 id="웹-브라우저의-구성">웹 브라우저의 구성</h2>
<div style="text-align:center">
<img src="https://image.toast.com/aaaabcy/post/1608995695700browser_structure-min.png" />
* 이미지 출처: https://chanyeong.com/blog/post/43
</div>

<h4 id="span-stylecolor58b19f1-user-interface-span"><span style="color:#58B19F">1. User Interface </span></h4>
<p>주소 표시줄, 이전/다음/새로고침 버튼 등 웹 페이지를 제외하고 <strong>사용자와 상호작용</strong>하는 사용자 인터페이스</p>
<h4 id="span-stylecolor58b19f2-rendering-engine-span📌📌📌"><span style="color:#58B19F">2. Rendering Engine </span>📌📌📌</h4>
<p>브라우저 엔진으로부터 전달받은 <strong>HTML과 CSS를 파싱</strong>하여 요청한 웹페이지를 표시한다.</p>
<p>만일 브라우저 엔진으로부터 URI값만 전달 받았다면, URI를 통신파트에 전달해 데이터를 받아오고, JS엔진을 통해 받아온 JS파일을 파싱한 후, 생성된 Render Tree를 UI백엔드 파트에 전달한다. </p>
<p>Safari는 Webkit, Firefox는 Gecko, Chrome은 Blink에 해당.</p>
<h4 id="span-stylecolor58b19f3-browser-enginespan"><span style="color:#58B19F">3. Browser Engine</span></h4>
<p><strong>UI와 렌더링 엔진을 연결</strong>하는 브라우저 엔진.
  사용자가 UI 주소표시줄에 URI를 입력하면, URI를 전달받은 브라우저 엔진이 자료 저장소에서 해당 URI에 알맞는 자료를 찾는다. 
  그리고 해당 자료들을 렌더링 엔진에 전달한다. </p>
<p>  만일 자료 저장소에 저장된 자료들이 <strong>없다면</strong>, 브라우저 엔진은 <strong>URI값만 렌더링 엔진에 전달</strong>한다. </p>
<h4 id="span-stylecolor58b19f4-networking-파트span"><span style="color:#58B19F">4. Networking 파트</span></h4>
<p>각종 <strong>네트워크 요청을 수행</strong>하는 통신파트. 
서버에서 받은 URI에 해당하는 응답 데이터를 렌더링 엔진에게 돌려주는 역할. </p>
<h4 id="span-stylecolor58b19f5-ui-백엔드-파트span"><span style="color:#58B19F">5. UI 백엔드 파트</span></h4>
<p>체크박스나 버튼과 같은 <strong>기본적인 위젯을 그려줌</strong>.</p>
<h4 id="span-stylecolor58b19f6-data-persistencespan"><span style="color:#58B19F">6. Data Persistence</span></h4>
<p>localStorage나 Cookie와 같이 <strong>보조 기억장치에 데이터를 저장</strong>하는 자료저장소.
자주 받아오는 자료를 저장해두어 서버에 반복적으로 요청하는 작업을 줄임. 
캐싱(Caching)이 이루어지는 곳.</p>
<h4 id="span-stylecolor58b19f7-javascript-interpreterspan"><span style="color:#58B19F">7. Javascript Interpreter</span></h4>
<p><strong>자바스크립트 코드를 실행</strong>하는 JS엔진.
Chrome의 경우 V8을 사용.</p>
<hr>
<p>웹 브라우저의 구조 중에서도 FE개발자가 특히 잘 알아야 하는 건, </p>
<h2 id="렌더링-엔진이-하는-일">렌더링 엔진이 하는 일</h2>
<ul>
<li>HTML, CSS, JS, 이미지 등 웹페이지에 포함된 모든 요소들을 <strong>화면에 보여준다</strong>.</li>
<li><em>업데이트가 필요할 때, 효율적으로 렌더링을 할 수 있도록 *</em>자료 구조를 생성**한다. </li>
</ul>
<blockquote>
<p>*업데이트가 필요한 경우: 입력 발생, 스크롤 발생, 애니메이션 동작, 비동기 요청으로 인한 데이터 로딩 등.</p>
</blockquote>
<h3 id="critical-rendering-path">Critical Rendering Path</h3>
<div style="text-align:center">
<img src="https://www.igvita.com/posts/12/doc-render-js.png"/>
* 이미지 출처: https://calendar.perfplanet.com/2012/deciphering-the-critical-rendering-path/
</div>


<h4 id="span-stylecolor38ada91-브라우저에서-사용자가-요청한-웹페이지에-문서를-불러오고-parsing"><span style="color:#38ada9">1. 브라우저에서 사용자가 요청한 웹페이지에 문서를 불러오고 <strong>Parsing</strong></h4>
<p>전달 받은 자료는 어휘 분석을 통해서 HTML5 표준에 지정된 <strong>고유한 토큰으로 변환</strong>된다. </span>
이 과정에서 필요한 CSS나 JS파일을 불러오기도 함.</p>
<h4 id="span-stylecolor38ada92-lexing을-통해-토큰이-노드-객체로-변환되어-속성과-규칙을-정함span"><span style="color:#38ada9">2. <strong>Lexing</strong>을 통해 토큰이 <strong>노드 객체로 변환</strong>되어 속성과 규칙을 정함</span></h4>
<h4 id="span-stylecolor38ada93-각-노드가-서로-연관성을-갖도록-dom-tree를-생성span"><span style="color:#38ada9">3. 각 노드가 서로 연관성을 갖도록 <strong>DOM Tree</strong>를 생성</span></h4>
<h4 id="span-stylecolor38ada94-css로는-cssom-tree를-생성-span"><span style="color:#38ada9">4. CSS로는 CSSOM Tree를 생성 </span></h4>
<p>HTML이 DOM Tree로 만들어지는 것처럼, CSS도 Parsing과 Lexing을 거쳐 <strong>CSSOM Tree</strong>가 만들어진다.</p>
<h4 id="span-stylecolor38ada95-dom-tree와-cssom-tree를-합쳐서-render-tree를-생성span"><span style="color:#38ada9">5. DOM Tree와 CSSOM Tree를 합쳐서 Render Tree를 생성</span></h4>
<p>화면에 표시되어야 할 <strong>모든 노드의 컨텐츠와 스타일 정보를 포함하는 트리</strong>가 만들어진다. </p>
<h4 id="span-stylecolor38ada96-layout-reflow-배치span"><span style="color:#38ada9">6. Layout (Reflow) 배치</span></h4>
<p>뷰포트 내에서 <strong>노드들의 정확한 위치와 크기를 계산</strong>하는 과정.
텍스트나 요소의 박스가 화면에서 차지하는 영역이나 <strong>여백, 스타일 속성이 계산</strong>된다. 
이 때 em은 px단위로 변환된다. </p>
<p>📝 : 요소의 크기나 위치가 바뀔 때, 브라우저 창 크기가 바뀔때 다시 발생</p>
<h4 id="span-stylecolor38ada97-paint-로-화면-그리기span"><span style="color:#38ada9">7. Paint 로 화면 그리기</span></h4>
<p>렌더 트리에 포함된 요소들이나 텍스트, 이미지들이 <strong>실제 픽셀로 그려진다</strong>. </p>
<p>📝 : 배경 이미지나 텍스트 색상, 그림자 등 레이아웃의 수치를 변화시키지 않는 스타일의 변경이 일어났을 때 다시 발생</p>
<h4 id="span-stylecolor079992-8-composition-으로-레이어-합성하기-span"><span style="color:#079992">++ 8. Composition 으로 레이어 합성하기 </span></h4>
<p>크롬의 경우, Layout과정 이후에 정해진 기준에 따라 <strong>필요하면 브라우저가 Layer를 생성</strong>한다. 
Layer도 트리형태로 구성이 되어 프린팅 과정에서** 하나의 비트맵으로 합성해 페이지를 완성**한다.
Layout과 Paint를 수행하지 않고 레이어의 합성만 발생하기 때문에 성능상 가장 큰 이점을 가짐.</p>
<hr>
<h2 id="reference">Reference</h2>
<p>*본 포스팅은 아래 서적/사이트들을 참고 및 인용하여 작성되었습니다.
학습단계로 잘못된 정보가 있을 수 있습니다. 잘못된 부분에 대해 알려주시면 곧바로 정정하도록 하겠습니다 😊
<a href="https://www.youtube.com/watch?v=sJ14cWjrNis&t=610s" target="_blank">[10분 테코톡] 체프의 브라우저 렌더링</a>
<a href="https://www.youtube.com/watch?v=cCbAJY1riDc&t=73s" target="_blank">웹 브라우저의 기본 원리</a>
<a href="https://www.youtube.com/watch?v=oLC_QYPmtS0" target="_blank">Browser rendering process 1편 - Browser 구성 요소</a>
<a href="https://d2.naver.com/helloworld/59361">https://d2.naver.com/helloworld/59361</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] React Pagination 구현에 대한 성찰 ]]></title>
            <link>https://velog.io/@grinding_hannah/TIL-React-Pagination-%EA%B5%AC%ED%98%84%EC%97%90-%EB%8C%80%ED%95%9C-%EC%84%B1%EC%B0%B0</link>
            <guid>https://velog.io/@grinding_hannah/TIL-React-Pagination-%EA%B5%AC%ED%98%84%EC%97%90-%EB%8C%80%ED%95%9C-%EC%84%B1%EC%B0%B0</guid>
            <pubDate>Sun, 08 Aug 2021 03:03:16 GMT</pubDate>
            <description><![CDATA[<p>난생 처음 8명이서 한 팀이 되어 과제를 진행했다.</p>
<p>&#39;이렇게 많은 인원이 어떻게 누구 하나 소외되는 사람 없이 각자의 역할을 제대로 해낼 수 있게 분업을 할 수가 있을까?&#39; 
라는 고민이 있었지만, 페이지 별로, 혹은 기능별로 분업이 순조롭게 이어졌다.</p>
<p>스스로 로직을 고민하는 경험이 필요하다고 느껴오고 있었던만큼, 나는 이번에 어드민 페이지의 아이디들을 페이지별로 불어오는 페이지네이션(Pagination)을 맡겠다고 했다.</p>
<hr>
<h2 id="span-stylecolor2980b9설계--구현순서span"><span style="color:#2980b9">설계 / 구현순서</span></h2>
<p>페이지네이션을 구현하려면 _무엇을 상태로써 관리해야 할까? _
대략적으로 구상해본 구현순서는 아래와 같다: </p>
<ul>
<li>테이블에 들어갈 <strong>전체 사용자 데이터</strong>를 받아온다. </li>
<li><strong>한 페이지당 보여지는 사용자id 갯수</strong>를 지정한다.</li>
<li>전체 사용자id 갯수를 페이지당 보여지는 사용자id 갯수로 나눈 값을 <strong>전체 페이지 수(totalPage)</strong>로 저장한다. </li>
<li>화면에 <strong>한 번에 보여질 페이지 숫자의 갯수(PAGES_PER_LIST)를 지정</strong>한 후 해당 페이지들을 저장하는 배열을 만든다. <strong>PAGES_PER_LIST는 화살표 버튼이 눌릴 때마다 totalPage와 비교</strong>하여 보여져야 할 새로운 페이지 배열을 만든다. </li>
<li>현재 보여져야 할 데이터는 <strong>현재 페이지 번호 - 1한 값과 한 페이지 당 보여져야 하는 id의 갯수를 곱한 값</strong>부터, <strong>현재 페이지 번호와 한 페이지 당 보여져야 하는 id 갯수를 곱한 값</strong>까지 전체 id배열에서 slice해온 결과가 된다.  </li>
</ul>
<hr>
<h2 id="span-stylecolorff6b81시행착오의-기록-🤕span"><span style="color:#ff6b81">시행착오의 기록 🤕</span></h2>
<h4 id="하나">하나,</h4>
<p><span style="color:#0984e3">테이블과 페이지네이션이 특정 상태들을 공유해야만 한다</span>는 사실을 간과해버리고 <code>Pagination.jsx</code>에 상태와 기능들을 구현했다. 
테이블에 페이지마다 다른 데이터를 보여주려고 하니 그제서야 상태가 알맞지 않은 곳에서 관리되고 있다는 걸 깨달았고, 테이블과 페이지네이션을 함께 가지고 있는 상위 컴포넌트인<code>AccountManagement.jsx</code>로 상태와 기능들을 마이그레이션 해야했다...😂 </p>
<p>설계를 할 때는 어떤 컴포넌트들이 상태를 공유하고, 이 상태가 관리되어야 하는 최상위 컴포넌트는 어디인지를 구체적으로 계획을 세우자!</p>
<h4 id="둘">둘,</h4>
<p>페이지가 선택될 때마다 테이블에 보여지는 데이터를 바꿔주기 위해서는 이전에 보여졌던 데이터의 시작점과 종료점을 찾아야 한다. 
그 기준은 인덱스가 되므로, 이전 페이지의 시작 인덱스와 종료 인덱스를 상태로써 저장해두었다가 인덱스를 기준으로 다시 선택된 페이지의 데이터를 전체 데이터 배열에서 잘라오려고 했다.
(예를 들면 <code>const [firstIndex, setFirstIndex] = useState(0);</code> 이런식으로...) </p>
<p>하지만 <span style="color:#0984e3">한 페이지 당 보여져야 하는 데이터의 갯수만 지정이 된다면 현재 페이지번호를 곱해서 필요한 인덱스를 판단할 수가 있었다.</span></p>
<pre><code class="language-js">  const currentPageData = tableData.slice(
    (currentPage - 1) * ITEMS_PER_PAGE,
    currentPage * ITEMS_PER_PAGE,
  );
</code></pre>
<p>이렇게 구현할 경우 로직이 훨씬 간단해 진다는 걸 알 수 있었다! </p>
<h4 id="셋">셋,</h4>
<p>현재 선택 가능하게 보여지는 5개의 페이지 중 마지막 페이지가 있는지 없는지를 판단하는 로직을 구현하기 위해서 매번 현재 보여지는 페이지 배열의 마지막 요소에 <code>PAGES_PER_LIST</code>를 더해주며 검사를 했었는데, 
팀원분의 리팩토링 덕분에 <span style="color:#0984e3">useState()로 만들어지는 set함수가 이전상태를 parameter로 받아올 수 있단 걸 알게 되었다</span>! </p>
<pre><code class="language-js">//초기값
  const [showingNum, setShowingNum] = useState({
    start: 1,
    end: PAGES_PER_LIST,
  });

  useEffect(() =&gt; {
    const lessThanFive = totalPage &lt;= PAGES_PER_LIST;
    lessThanFive
      ? setShowingNum(prev =&gt; ({ ...prev, start: 1, end: totalPage }))
      : setShowingNum(prev =&gt; ({ ...prev, start: 1, end: PAGES_PER_LIST }));
  }, [totalPage]);
</code></pre>
<p>이전 상태와의 직접적인 비교가 가능해지면서 코드가 훨씬 간결하고 명확해졌다. :)</p>
<p>+<strong>추가</strong> 
이전 상태 (<code>prev</code>)를 parameter로 넘겨줄 수 있기 때문에 <strong>set함수에 이전 상태를 인자로 받는 함수를 인자로써 전달해줄 수도 있다.</strong></p>
<pre><code class="language-js">//왼쪽 화살표가 눌렸을 때
  const changePageNumbersBackward = () =&gt; {
    currentPage &gt; PAGES_PER_LIST &amp;&amp;
      setShowingNum(prev =&gt; arrowHandler(prev, -1, totalPage));📌📌📌
  };

//오른쪽 화살표가 눌렸을 때
  const changePageNumberForward = () =&gt; {
    showingNum.end &lt;= totalPage &amp;&amp;
      setShowingNum(prev =&gt; arrowHandler(prev, 1, totalPage));📌📌📌
  };

//util의 arrowHandler 함수
const arrowHandler = (prev, sign, totalPage) =&gt; {
  const PAGES_PER_LIST = 5;
  const nextIndex = prev.end + PAGES_PER_LIST;
  let res;
  if (sign === 1) {
    res = nextIndex &gt; totalPage ? totalPage : nextIndex;
  } else if (sign === -1) {
    res =
      prev.end - prev.start &lt; 4
        ? prev.start + 4 - PAGES_PER_LIST
        : prev.end - PAGES_PER_LIST;
  }
  return { ...prev, start: prev.start + PAGES_PER_LIST * sign, end: res };
};

export default arrowHandler;

</code></pre>
<hr>
<h2 id="결과물">결과물</h2>
<h3 id="ui">UI</h3>
<p>테이블에 들어가는 데이터를 10개 단위로 페이징 처리한다.
<img src="https://images.velog.io/images/grinding_hannah/post/1b21fff2-b420-4fcc-a266-be149cad852f/image.png" alt=""></p>
<h3 id="code">Code</h3>
<p>페이지네이션이 필요한 페이지는 관리자(Admin)페이지에서 아이디를 테이블로 관리하는 부분. 
테이블에 들어갈 데이터를 페이지 단위로 보여줘야 하기에 대략적인 구조는 이렇다: </p>
<blockquote>
<p>*부모 컴포넌트 &gt; 자식 컴포넌트 
<code>Admin.jsx</code> &gt; <code>AccountManagement.jsx</code> &gt; <code>Pagination.jsx</code> &gt; <code>PageButton.jsx</code> </p>
</blockquote>
<h4 id="accountmanagementjsx">AccountManagement.jsx</h4>
<ul>
<li><code>totalPage</code>: 현재 데이터에서 10개씩 데이터를 쪼갰을 때 나올 수 있는 총 페이지의 수</li>
<li><code>currentPage</code>: 현재 선택된 페이지의 숫자<pre><code class="language-js">const ITEMS_PER_PAGE = 10;
</code></pre>
</li>
</ul>
<p>export default function AccountManagement() {
  const [totalPage, setTotalPage] = useState(1);
  const [currentPage, setCurrentPage] = useState(1);
  const [tableData, setTableData] = useState([]);</p>
<p>  useEffect(() =&gt; {
    setTableData(localStorageHelper.getItem(&#39;userInfo&#39;) || []);
  }, []);</p>
<p>  useEffect(() =&gt; {
    const lastPage = Math.ceil(tableData.length / ITEMS_PER_PAGE);
    setTotalPage(lastPage ? lastPage : 1);
  }, [tableData]);</p>
<p>  const handleOnSearch = useCallback(result =&gt; {
    setTableData(result);
  }, []);</p>
<p>  const currentPageData = tableData.slice(
    (currentPage - 1) * ITEMS_PER_PAGE,
    currentPage * ITEMS_PER_PAGE,
  );</p>
<p>  return (
    <TableContainer>
      //부분 생략...
      <Table
        dataProps={dataProps}
        currentPageData={currentPageData}
        tableData={tableData}
        setTableData={setTableData}
      />
      <Pagination
        totalPage={totalPage}
        currentPage={currentPage}
        setCurrentPage={setCurrentPage}
      />
    </TableContainer>
  );
}</p>
<pre><code>

#### Pagination.jsx

ex. `&lt;` 6, 7, 8, 9, 10 `&gt;`
- `showingNum`: 사용자가 선택할 수 있게 한번에 화면에 보여지는 페이지들. 이 경우 한번에 5개 페이지에 대한 페이지 숫자들이 보여진다. 
위 예제에서 숫자들(6, 7, 8, 9, 10)에 해당.
- `isFirstPage`: 현재 보여지는 5개의 페이지 옵션 중 가장 첫 페이지.
위 예제에서는 6.
- `isLastPage`: 현재 보여지는 5개의 페이지 옵션 중 가장 마지막 페이지.
위 예제에서는 10.

```js
import { arrowHandler, getEmptyArray } from &#39;./utils&#39;;

const PAGES_PER_LIST = 5;

export default function Pagination({ totalPage, currentPage, setCurrentPage }) {
  const [showingNum, setShowingNum] = useState({
    start: 1,
    end: PAGES_PER_LIST,
  });

  const changePageNumbersBackward = () =&gt; {
    currentPage &gt; PAGES_PER_LIST &amp;&amp;
      setShowingNum(prev =&gt; arrowHandler(prev, -1, totalPage));
  };

  const changePageNumberForward = () =&gt; {
    showingNum.end &lt;= totalPage &amp;&amp;
      setShowingNum(prev =&gt; arrowHandler(prev, 1, totalPage));
  };

  useEffect(() =&gt; {
    const lessThanFive = totalPage &lt;= PAGES_PER_LIST;
    lessThanFive
      ? setShowingNum(prev =&gt; ({ ...prev, start: 1, end: totalPage }))
      : setShowingNum(prev =&gt; ({ ...prev, start: 1, end: PAGES_PER_LIST }));
  }, [totalPage]);

  useEffect(() =&gt; {
    setCurrentPage(showingNum.start);
  }, [showingNum, setCurrentPage]);

  const isFirstPage = showingNum.start === 1;
  const isLastPage = showingNum.end === totalPage;
  const pages = getEmptyArray(showingNum.start, showingNum.end);

  return (
    &lt;PageListContainer&gt;
      &lt;ArrowButton
        type=&quot;back&quot;
        inActive={isFirstPage}
        disabled={isFirstPage}
        changePageNumbersBackward={changePageNumbersBackward}
      /&gt;
      {pages.map((page, idx) =&gt; {
        return (
          &lt;PageButton
            key={`pageNumber-${idx + 1}`}
            page={page}
            setCurrentPage={setCurrentPage}
            isActive={page === currentPage}
          /&gt;
        );
      })}
      &lt;ArrowButton
        type=&quot;next&quot;
        inActive={isLastPage}
        disabled={isLastPage}
        changePageNumberForward={changePageNumberForward}
      /&gt;
    &lt;/PageListContainer&gt;
  );
}
</code></pre><h4 id="pagebuttonjsx">PageButton.jsx</h4>
<pre><code class="language-js">function PageButton({ page, setCurrentPage, isActive }) {
  const handleClickButton = () =&gt; {
    setCurrentPage(page);
  };

  return (
    &lt;PageButtonContainer isActive={isActive}&gt;
      &lt;StyledButton onClick={handleClickButton} isActive={isActive}&gt;
        {page}
      &lt;/StyledButton&gt;
    &lt;/PageButtonContainer&gt;
  );
}
</code></pre>
<hr>
<h2 id="reference">Reference</h2>
<p><a href="https://chanhuiseok.github.io/posts/react-12/">https://chanhuiseok.github.io/posts/react-12/</a>
<a href="https://chanhuiseok.github.io/posts/react-13/">https://chanhuiseok.github.io/posts/react-13/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Intersection Observer 와 React에 대해]]></title>
            <link>https://velog.io/@grinding_hannah/Intersection-Observer-%EC%99%80-React%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@grinding_hannah/Intersection-Observer-%EC%99%80-React%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Wed, 28 Jul 2021 15:30:16 GMT</pubDate>
            <description><![CDATA[<p>무한 스크롤을 구현하려다 날밤새게 만든 Intersection Observer, 
useEffect에서 무한히 생겨나던 fetch요청... 
기억이 휘발되기 전 삽질의 흔적을 남겨보자 🙉</p>
<hr>
<h1 id="intersection-observer로-무한-스크롤-구현하기">Intersection Observer로 무한 스크롤 구현하기</h1>
<p>Intersection Observer를 사용해 구현된 <code>CardListContainer</code>:</p>
<pre><code class="language-js">const CardListContainer = () =&gt; {
  const [cardListInfo, setCardListInfo] = useState([]);
  const [page, setPage] = useState(1);
  const [io, setIo] = useState(null);

  const fetchAPIData = async () =&gt; {
    const data = await API.get.comments(page);
    setCardListInfo([...cardListInfo, ...data]);
  }

  const registerObservingEl = (el) =&gt; {
    io.observe(el);
  }

  useEffect(() =&gt; {
    const targetIO = new IntersectionObserver(entries =&gt; {
      entries.forEach(entry =&gt; {
        if (entry.isIntersecting) {
          setPage(page + 1);
          if (io !== null) io.disconnect();
        }
      })
    })
    setIo(targetIO);
    fetchAPIData();
  }, [page])

  return (
    !cardListInfo.length
    ? &lt;&gt;&lt;/&gt;
    : &lt;CardListPresenter cardListInfo={cardListInfo} registerObservingEl={registerObservingEl} /&gt;
  )
}

export default CardListContainer;</code></pre>
<h2 id="구현전략">구현전략</h2>
<ol>
<li><p>intersectionObserver의 인스턴스를 생성하고, 콜백함수로 관찰대상 요소가 들어올 경우 어떤 후속처리를 할 지 정의해준다.</p>
<ul>
<li>useEffect에서 선언.</li>
</ul>
</li>
<li><p>관찰대상 DOM요소를 등록하는 함수를 정의한다.</p>
</li>
</ol>
<pre><code class="language-js">  const registerObservingEl = (el) =&gt; {
    io.observe(el);
  }</code></pre>
<ol start="3">
<li>위 등록 함수를 관찰대상 DOM이 생성되는 곳까지 props로 내려준다.<ul>
<li><strong>props</strong>로 <code>CardPresenter</code>컴포넌트까지 <code>registerObservingEl</code>함수를 내려줌.<pre><code class="language-js">useEffect(() =&gt; {
const targetIO = new IntersectionObserver(entries =&gt; {
 entries.forEach(entry =&gt; {
   if (entry.isIntersecting) {
     //다음 페이지의 card데이터를 fetch 해온다.
     setPage(page + 1);
     if (io !== null) io.disconnect();
   }
 })
})
setIo(targetIO);
fetchAPIData();
}, [page])</code></pre>
</li>
</ul>
</li>
</ol>
<h2 id="intersection-observer-인스턴스-만들기">Intersection Observer 인스턴스 만들기</h2>
<p><code>new</code>로 Intersection Observer의 인스턴스 객체를 생성하면, 이 객체는 관찰 대상 요소와 viewport와의 교차영역에 대한 변화를 <strong>비동기적으로 감지</strong>해준다. 
(기본적으로는 viewport가 되지만 option으로 root를 설정해주면 이 또한 바꿀 수 있다)</p>
<blockquote>
<p>이 때 전달해 줄 수 있는 인자는 2가지:</p>
</blockquote>
<ul>
<li>callback 함수</li>
<li>(선택) option 객체</li>
</ul>
<pre><code class="language-js">const targetIO = new IntersectionObserver(entries =&gt; {
      entries.forEach(entry =&gt; {
        //후속처리 함수가 들어가는 자리
          if (io !== null) io.disconnect();
        }
      }, {/* option객체 */})</code></pre>
<p>위 코드의 callback함수가 받는 <code>entries</code>는 어떻게 들어오는 걸까?
그래서 사용되는 것이 <strong><code>.observe()</code></strong>메서드인 것! </p>
<h2 id="observe">.observe()</h2>
<p>생성된 Intersection Observer 인스턴스 객체는 편의상 <code>io</code>라고 부르자.
<code>io.observe(돔 요소);</code>를 하게되면 해당 돔 요소는 <code>io</code>객체가 <strong>관찰하는 대상으로 등록</strong>이 된다. </p>
<pre><code class="language-js">  const registerObservingEl = (el) =&gt; {
    io.observe(el);
  }</code></pre>
<p>이번 과제에서는 &#39;<em>viewport에 카드리스트의 가장 마지막 카드가 들어오면 후다닥 다음 페이지의 API를 fetch해서 보여주자!</em>&#39;라는 것이 목표였던만큼,<code>observe</code>메서드를 props로 전달해주기 위해 별도의 함수표현식으로 만들었다.</p>
<h2 id="disconnect">.disconnect()</h2>
<p>관찰을 멈추게 하는 메서드. 
등록되었던 관찰대상과 <code>io</code>와의 커넥션을 끊는다.</p>
<h3 id="💡-span-stylecolorsalmon이-부분에서-겪었던-시행착오span">💡 <span style="color:salmon"><strong>이 부분에서 겪었던 시행착오</strong></span></h3>
<p> 처음에는 <code>useState()</code>를 이용해 상태로 <code>io</code>객체를 관리하면 될 것이라 생각했다❗️</p>
<pre><code class="language-js"> const [io, setIO] = useState(null);</code></pre>
<p> 당연히<code>setIO</code>가 호출되고 리렌더링 되는 순간 <strong>앞전에 생겨난 <code>io</code>객체가 garbage collecting되어 사라질꺼라 생각했지만</strong> network탭을 통해 요청내역을 보니 그렇지가 않았다 😱</p>
<p><img src="https://images.velog.io/images/grinding_hannah/post/7d3b89c7-57a6-4c76-bf2b-adc366c535c2/%E1%84%86%E1%85%AE%E1%84%92%E1%85%A1%E1%86%AB%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%A9%E1%86%AFdisconnect%E1%84%8B%E1%85%A5%E1%86%B9%E1%84%8B%E1%85%B5.gif" alt="">
<span style="color:#ff7675">스크롤이 내려갈 때 뿐만 아니라 올라갈 때도 get요청이 가고 있다는 건 이전 렌더 때 생겨난 <code>io</code>가 여전히 관찰을 이어가고 있다는 것! _없어지지 않았다는 사실 _!!</span> </p>
<p>그래서 #destroy intersection observer(<a href="https://stackoverflow.com/questions/61968215/problem-with-intersection-oberserver-api-useeffect-hook-triggering-callback-tw" target="_blank">스택오버플로우 참고</a>) 라는 키워드로 검색을 해보니 <code>io</code>객체를 <strong>삭제하는 방법 대신 <code>disconnect</code>라는 메서드로 연결을 끊는 방법을 추천</strong>하고 있었다. 실제로 mdn 스펙상 <code>destroy</code>메서드는 구현되지 않은 것을 확인할 수 있었다. </p>
<p>그래서 <code>io</code>객체의 처리를 useState()의 상태로 다룰 때, 만들어진<code>io</code>객체가 있다면(a.k.a 이전에 만들어진 <code>io</code>객체) <code>io.disconnect()</code> 로 연결을 끊어주는 방식으로 변경했다. </p>
<pre><code class="language-js">const newIO = new IntersectionObserver(...);

...

if (io !== null) io.disconnect();
setIO(newIO);</code></pre>
<p><img src="https://images.velog.io/images/grinding_hannah/post/5721f046-810a-4b4f-a1c0-6a53213d6586/ezgif.com-gif-maker%20(4).gif" alt=""></p>
<p>이제는 스크롤을 올려도 get요청이 번복되지 않는다!!</p>
<p>😳 이번에야말로 최적화에 한 걸음 다가간 것이 아닐까 (흐뮷)
<code>#Intersection Observer는 삭제말고 연결끊기</code></p>
<p>끝!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] JavaScript로 component 만들기 회고록]]></title>
            <link>https://velog.io/@grinding_hannah/JavaScript-JavaScript%EB%A1%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/JavaScript-JavaScript%EB%A1%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 10 Jul 2021 07:39:31 GMT</pubDate>
            <description><![CDATA[<p>React 개발만 근 2달을 하다가 다시 Vanilla JavaScript로 코드를 짜려니 다시 머리가 백지가 된 기분이다.🤯</p>
<p>최근 여러번의 미션을 진행하면서 겪은 실수들을 문서화 하지 않았던 점을 뼈저리게 후회했기 때문에 지금부터는 사소한 시행착오도 기록을 해보려고 한다 ✍🏻</p>
<hr>
<h1 id="javascript로-react-컴포넌트의-흐름-만들기">JavaScript로 React 컴포넌트의 흐름 만들기</h1>
<p>리액트로 최근 몇 달을 개발했는데 JavaScript로 리액트의 흐름을 만들려니 또 백지가 된다... 내가 리액트의 Life-cycle에 대해 진득히 공부해 본 적이 있었던가...?🤦🏻‍♀️</p>
<p><a href="https://www.youtube.com/watch?v=ltw4FYagLfM" target="_blank">제로초님의 강의</a>에 따르면 Class에서의 리액트 life cycle은 다음과 같이 진행된다.</p>
<blockquote>
<ul>
<li>기본 흐름: <code>constructor</code>  ➡️ <code>render</code> ➡️ <code>ref</code> ➡️ <code>componentDidMount</code> </li>
</ul>
</blockquote>
<ul>
<li>setState / props가 바뀔때: 기본 흐름 ➡️ <code>shouldComponentUpdate</code> ➡️ <code>render</code> ➡️ <code>componentDidUpdate</code><blockquote>
<ul>
<li>상위 컴포넌트가 하위 컴포넌트를 없앴을 때: 기본 흐름 ➡️ <code>componentWillUnmount</code> ➡️ <code>소멸</code> </li>
</ul>
</blockquote>
</li>
</ul>
<p><img src="https://images.velog.io/images/grinding_hannah/post/e2349359-9041-492d-ad55-a74a33e1ccc5/image.png" alt=""></p>
<p>이러한 흐름을 참고해서 만들어본 구조는 다음과 같다: </p>
<pre><code class="language-js">class Component {
  constructor(){
      this.setState();
  }

  setState(){
      this.render();
  }

  render(){
      this.didMount():
  }

  didMount(){
      this.setState();
  }
}
</code></pre>
<p>위 클래스는 아래와 같이 진행된다. </p>
<blockquote>
<p><code>new</code> 인스턴스 생성 -&gt; <code>setState()</code> -&gt; <code>render()</code> -&gt; <code>didMount()</code> -&gt; <code>setState()</code> -&gt; <code>render()</code>...필요시 반복...</p>
</blockquote>
<p><code>didMount()</code>, <code>setState()</code>, <code>render()</code> 가 무한반복 되는 현상을 방지하려면 아래와 같이 <code>didMount()</code> 메서드 안에서 선택적으로 <strong><code>setState()</code>가 실행되어야 하는 조건</strong>을 만들어야 한다.  </p>
<pre><code class="language-javascript">  async didMount() {
    if (this.state.movieList) return; 📌 //이렇게!
    const movieList = await fetchData(&#39;recommended-movies&#39;);

    this.setState({ movieList : movieList });
  }
</code></pre>
<hr>
<h1 id="시행착오의-기록들">시행착오의 기록들</h1>
<hr>
<h1 id="그-외">그 외...</h1>
<h2 id="span-stylecolor54a0ffinnerhtml-vs-appendspan"><span style="color:#54a0ff">innerHTML vs. append()</span></h2>
<pre><code class="language-js">const $app = document.querySelector(&quot;#app&quot;);
const $test = document.createElement(&quot;div&quot;);
$test.innerHTML = `&lt;h2&gt;Hello World&lt;/h2&gt;`;</code></pre>
<p>위 코드처럼 <code>$app</code>이라는 DOM요소에 새로운 DOM요소를 자식으로 추가하기 위해 아래와 같이<code>innerHTML</code>을 사용하고자 했다.</p>
<pre><code class="language-js">$app.innerHTML = $test; ❌</code></pre>
<p>그러자 이런 화면이 떴다...
<img src="https://images.velog.io/images/grinding_hannah/post/05082e05-73b5-437b-8c45-0c8b6e0e0ba8/image.png" alt=""></p>
<p>DOM은 <strong>OBJECT</strong>다! 그리고 <code>innerHTML</code>은 TEXT를 다루기 때문에 <strong>string을 받는다</strong>!!</p>
<p>화면을 update 할 때에는 innerHTML을 쓰지 않는다! 
template literal로 string을 만들어서 한꺼번에 추가하면 요소를 하나씩 개별적으로 다루기가 어렵기 때문. </p>
<p>반면 <code>append()</code>는 DOM을 하나의 <strong>LIST</strong>로 쓴다.</p>
<pre><code class="language-js">$app.append($test); ⭕️</code></pre>
<blockquote>
<p>💡 <strong>DOM을 추가할 수 있는 2가지 방법</strong>:</p>
<ul>
<li><code>append()</code> :<ul>
<li>dom에 dom을 붙임 </li>
<li>event를 걸어서 dom을 추가할 때 적절</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li><code>insertAdjacentHTML()</code> :<ul>
<li>dom에 text를 붙임. text이기 때문에 <strong>dom에 붙기 &#39;전&#39;까지 &quot;event를 걸 수가 없다&quot;</strong>! </li>
<li>render에서 insertAdjacentHTML()를 해주고, didMount에서 이벤트를 걸어야하는 이유.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="span-stylecolor54a0fftypemodulespan"><span style="color:#54a0ff">type=&quot;module&quot;</span></h2>
<p>script에 type을 생략하면** 그냥 JavaScript**가 된다. </p>
<p>만약 <strong><code>type=&quot;module&quot;</code></strong>이라고 명시해주지 않으면 <strong>Cannot use import statement outside a module</strong>라는 에러가 난다. 
ES6 harmony module을 쓰려면 아래와 같이 type을 선언해줘야함.</p>
<pre><code class="language-html">  &lt;script type=&quot;module&quot; src=&quot;./index.js&quot;&gt;&lt;/script&gt;</code></pre>
<hr>
<h2 id="span-stylecolor54a0ff디렉토리-구조-public--span"><span style="color:#54a0ff">디렉토리 구조: public  </span></h2>
<p><code>public</code>에는 무엇을 넣어야 하는 걸까? 그럼 <code>src</code>에는?<br>도통 기준이 확실하게 잡히지를 않아서 나름의 기준을 세워보았다. </p>
<p><code>public</code> 폴더의 역할은 static한 파일들을 모으는 곳. 
<strong>static</strong>하다는 의미는 단순히 &#39;정적&#39;이라는 뜻만 있는 것이 아니라 <strong>entry point로써의 역할을 수행하는 것</strong>들을 의미함.</p>
<p>이미지나 <code>&lt;script&gt;</code>태그에 src로 연결된 index.js 파일처럼 브라우저에 첫 화면을 HTML로 그려줄 때에 필요한 부분들로, 
HTML에 original하게 존재하는 것들이라고 생각하면 좋음.</p>
<p><img src="https://images.velog.io/images/grinding_hannah/post/a06eeb99-16c5-463f-8f51-9e77c9b5c05a/image.png" alt=""></p>
<p>index.html에 <code>&lt;script type=&quot;module&quot; src=&quot;./index.js&quot;&gt;&lt;/script&gt;</code> 로 지정된  index.js는 <code>public</code> 디렉토리 하위에 있지만, 
index.js를 통해 import 되는 App.js 파일은 <code>src</code>디렉토리 하위에 있는 것을 알 수 있다.</p>
<hr>
<h2 id="span-stylecolor54a0ffclass의-멤버변수span"><span style="color:#54a0ff">Class의 멤버변수</span></h2>
<p>아래와 같이 <code>constructor()</code> 함수 내부에 <strong>멤버 변수</strong>(ex.<code>this.$target</code>)를 선언하면, 
해당 클래스로 생성되는 모든 인스턴스가 멤버변수를 사용할 수 있게된다. 상속!</p>
<p>반면, <code>var</code>, <code>let</code>, <code>const</code>로 선언하는 변수는 상속되지 않는다. 로컬로만 사용가능.</p>
<pre><code class="language-js">class App {
  constructor({ $target }) {
    this.$target = $target; //멤버변수는 Class에서만 쓰인다. 
    this.setState();
  }

  setState(){
    this.render();
  }

  render () {
    //인자를 전달할 때 키값을 고정해서 변수이름을 유지한다.
    new MainPage({ $target: this.$target })
  }
}</code></pre>
<p><code>render()</code> 함수 내부에서도 멤버변수로 접근해서 key-value로 저장한 객체를 인자로 전달한다.
이때 <code>$target</code> 이라는 <strong>key로 전달되는 value는 클래스마다 재할당 해줄 수 있기 때문에</strong> 재사용성이 올라간다.  </p>
<hr>
<h1 id="reference">Reference</h1>
<p><a href="https://www.zerocho.com/category/React/post/579b5ec26958781500ed9955">https://www.zerocho.com/category/React/post/579b5ec26958781500ed9955</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] nodemon, ts-node 모듈 설치하기]]></title>
            <link>https://velog.io/@grinding_hannah/TypeScript-nodemon-ts-node-%EB%AA%A8%EB%93%88-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/TypeScript-nodemon-ts-node-%EB%AA%A8%EB%93%88-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 19 May 2021 16:47:54 GMT</pubDate>
            <description><![CDATA[<p>*본 포스팅은 React와 TypeScript가 설치되었다는 가정하에 작성되었습니다 :)</p>
<p>  +<strong>PLUS</strong>:
  CRA로 React와 TypeScript <a href="https://create-react-app.dev/docs/adding-typescript/" target="_blank">설치하는 방법</a></p>
<hr>
<p>일반적으로 타입스크립트로 코드를 작성할 경우, 실행 시 2-step이 필요하다. </p>
<p><strong>Step 1</strong>. TypeScript =&gt; JavaScript 컴파일 
<code>tsc practice.ts</code> 
<strong>Step 2</strong>. 컴파일 된 JavaScript 실행
<code>node practice.js</code></p>
<p>하지만 Live compile을 세팅하면 매번 컴파일을 해야하는 번거로움을 덜 수 있다. 
타입스크립트로 개발 시 Live compile 환경 세팅하는 방법을  알아보자❗️ </p>
<h2 id="span-stylecolor22a6b3ts-node-설치하기span"><span style="color:#22a6b3">ts-node 설치하기<span></h2>
<blockquote>
<p><strong>CLI</strong>에서 입력:
  <code>npm install ts-node --save-dev</code></p>
</blockquote>
<h2 id="span-stylecolor22a6b3nodemon-설치하기span"><span style="color:#22a6b3">nodemon 설치하기<span></h2>
<blockquote>
<p><strong>CLI</strong>에서 입력:
<code>npm install nodemon --save-dev</code></p>
<p><strong>package.json</strong> 에서 <strong>script</strong> 추가:</p>
<pre><code class="language-js"> &quot;scripts&quot;: {
    &quot;start&quot;: &quot;npm run build:live&quot;,
    &quot;build&quot;: &quot;tsc -p .&quot;,
    &quot;build:live&quot;: &quot;nodemon --watch &#39;src/**/*.ts&#39; --exec &#39;ts-node&#39; src/index.ts&quot;
  }</code></pre>
</blockquote>
<pre><code>
  이 때, 경로를 나타내는 `src/**/*.ts`는 node_modules나 package.json이 설치된 위치를 기준으로 기재한다. 
  위 예제는 package.json와 src가 같은 디렉토리 내에 위치하고, src 하위의 모든 ts확장자를 가진 파일을 컴파일할 것을 뜻한다.
  `src/index.ts`는 진입점이 되는 파일을 의미한다.




  정상적으로 설치가 되었다면 package.json은 다음과 같이 보여진다:
  ```js
  {
//생략
    &quot;scripts&quot;: {
      &quot;start&quot;: &quot;npm run build:live&quot;,
      &quot;build&quot;: &quot;tsc -p .&quot;,
      &quot;build:live&quot;: &quot;nodemon --watch &#39;*.ts&#39; --exec &#39;ts-node&#39; practice.ts&quot;,
      &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;&amp; exit 1&quot;
    },
    &quot;devDependencies&quot;: {
      &quot;nodemon&quot;: &quot;^2.0.7&quot;,
      &quot;ts-node&quot;: &quot;^9.1.1&quot;,
      &quot;typescript&quot;: &quot;^4.2.4&quot;
    }
}
</code></pre><p>  이제 <strong><code>npm start</code></strong>로 실행을 시키면, <img src="https://images.velog.io/images/grinding_hannah/post/02688b7d-08dd-4b26-82a0-6351f1d311eb/image.png" alt="">
  <img src="https://images.velog.io/images/grinding_hannah/post/134ba10e-88f4-4049-990e-6284f1621519/image.png" alt=""></p>
<p> 타입스크립트 파일을 수정할 때마다 실시간으로 compile된 결과를 보여주는 것을 확인할 수 있다❗️😃</p>
<hr>
<h2 id="reference">Reference</h2>
<p> <a href="https://radlohead.gitbook.io/typescript-deep-dive/nodejs">https://radlohead.gitbook.io/typescript-deep-dive/nodejs</a>
  <a href="https://create-react-app.dev/docs/adding-typescript/">https://create-react-app.dev/docs/adding-typescript/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Algorithm/JavaScript] 비밀지도]]></title>
            <link>https://velog.io/@grinding_hannah/AlgorithmJavaScript-%EB%B9%84%EB%B0%80%EC%A7%80%EB%8F%84</link>
            <guid>https://velog.io/@grinding_hannah/AlgorithmJavaScript-%EB%B9%84%EB%B0%80%EC%A7%80%EB%8F%84</guid>
            <pubDate>Wed, 21 Apr 2021 16:19:49 GMT</pubDate>
            <description><![CDATA[<p>Programmers Level.1 Question Review 🧩</p>
<hr>
<p>문제출처: <a href="https://programmers.co.kr/learn/courses/30/lessons/17681">https://programmers.co.kr/learn/courses/30/lessons/17681</a></p>
<h3 id="문제">문제</h3>
<h4 id="a-hrefhttpsprogrammerscokrlearncourses30lessons17681-target_blank문제-보러가기a"><a href="https://programmers.co.kr/learn/courses/30/lessons/17681" target="_blank">문제 보러가기</a></h4>
<hr>
<h3 id="제출-답안">제출 답안</h3>
<pre><code class="language-js">function solution(n, arr1, arr2) {
  //이진수 변환 + 모자란 숫자자리만큼 앞에 &#39;0&#39; 붙이기 + 한 글자 단위로 쪼개기
    let newArr1 = arr1.map(v =&gt; (v.toString(2).padStart(n, 0).split(&#39;&#39;)));
    let newArr2 = arr2.map(v =&gt; (v.toString(2).padStart(n, 0).split(&#39;&#39;)));

  //이중배열을 순회하면서 1과 0 중 하나라도 1이 있다면 1 반환
  //#은 1로, 0은 공백으로 치환 + 배열의 글자 합쳐서 하나의 문자열로 반환
    return newArr1.map((arr, i) =&gt; {
        return arr.map((letter, j) =&gt; String(letter | newArr2[i][j]).replace(/1/g, &#39;#&#39;).replace(/0/g, &#39; &#39;));
    }).map(arr =&gt; arr.join(&#39;&#39;));
}
</code></pre>
<hr>
<h3 id="오늘의-lesson">오늘의 Lesson</h3>
<ul>
<li><strong>비트 연산자(<code>|</code>)</strong>는 <strong>OR 조건</strong>과 유사하다. 반면 비트 연산자(<code>|</code>) 는 boolean이 아니라 <strong>십진수</strong>를 반환한다. <code>(1|0)</code> 이면 1이 true이기 때문에  1을 반환. (or은 하나라도 true이면 true값을 바로 반환하기 때문에)
<a href="https://www.youtube.com/watch?v=TqAeqncjS6s&t=2s" target="_blank">JS비트연산자 설명 동영상</a>
<a href="https://medium.com/gdana/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%B9%84%ED%8A%B8-%EC%97%B0%EC%82%B0%EC%9E%90-5f772ffa35e8" target="_blank">비트연산자 참고 자료</a></li>
<li><strong><code>padStart()</code></strong>는 특정 문자를 원하는 길이만큼 문자열 &#39;앞&#39;에 더해준다. &#39;뒤쪽&#39;에 더해져야 한다면 <strong><code>padEnd()</code></strong>메서드를 사용할 수 있다.
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/padStart" target="_blank">padStart() MDN</a>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd" target="_blank">padEnd() MDN</a></li>
<li>정규표현식에서 숫자는 특수문자도 아니고, 문자 그대로 해석되어야 하는 대상이 아니기 때문에 백슬래시(<code>\</code>)는 사용하지 않는다. 그냥 <code>/1/g</code> 면 1을 찾아준다.
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Regular_Expressions" target="_blank">정규표현식 MDN</a></li>
<li>비트 연산자가 사용된 다른 사람의 풀이:<pre><code class="language-js">function solution(n, arr1, arr2) {
  return arr1.map((v, i) =&gt; addZero(n, (v | arr2[i]).toString(2)).replace(/1|0/g, a =&gt; +a ? &#39;#&#39; : &#39; &#39;));
}
</code></pre>
</li>
</ul>
<p>const addZero = (n, s) =&gt; {
    return &#39;0&#39;.repeat(n - s.length) + s;
}
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Algorithm/JavaScript] 신규 아이디 추천]]></title>
            <link>https://velog.io/@grinding_hannah/AlgorithmJavaScript-%EC%8B%A0%EA%B7%9C-%EC%95%84%EC%9D%B4%EB%94%94-%EC%B6%94%EC%B2%9C</link>
            <guid>https://velog.io/@grinding_hannah/AlgorithmJavaScript-%EC%8B%A0%EA%B7%9C-%EC%95%84%EC%9D%B4%EB%94%94-%EC%B6%94%EC%B2%9C</guid>
            <pubDate>Tue, 20 Apr 2021 17:33:39 GMT</pubDate>
            <description><![CDATA[<p>Programmers Level.1 Question Review ✏️</p>
<hr>
<p>문제출처: <a href="https://programmers.co.kr/learn/courses/30/lessons/72410">https://programmers.co.kr/learn/courses/30/lessons/72410</a></p>
<h3 id="문제">문제</h3>
<h4 id="a-hrefhttpsprogrammerscokrlearncourses30lessons72410-target_blank문제-보러가기a"><a href="https://programmers.co.kr/learn/courses/30/lessons/72410" target="_blank">문제 보러가기</a></h4>
<blockquote>
<ul>
<li>1단계 new_id의 모든 대문자를 대응되는 소문자로 치환합니다.</li>
</ul>
</blockquote>
<ul>
<li>2단계 new_id에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.</li>
<li>3단계 new_id에서 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.</li>
<li>4단계 new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.</li>
<li>5단계 new_id가 빈 문자열이라면, new_id에 &quot;a&quot;를 대입합니다.</li>
<li>6단계 new_id의 길이가 16자 이상이면, new_id의 첫 15개의 문자를 제외한 나머지 문자들을 모두 제거합니다.
   만약 제거 후 마침표(.)가 new_id의 끝에 위치한다면 끝에 위치한 마침표(.) 문자를 제거합니다.</li>
<li>7단계 new_id의 길이가 2자 이하라면, new_id의 마지막 문자를 new_id의 길이가 3이 될 때까지 반복해서 끝에 붙입니다.</li>
</ul>
<hr>
<h3 id="제출답안">제출답안</h3>
<pre><code class="language-js">function solution(new_id) {
    //1단계, 2단계
    new_id = new_id.toLowerCase().replace(/[^a-z\d\-\_\.]/g, &quot;&quot;);
    //3단계
    new_id = new_id.replace(/\.+\./g, &#39;.&#39;);
    //4단계 - / 는 or을 나타냄
    new_id = new_id.replace(/^\.|\.$/g, &quot;&quot;);
    //5단계
    if(new_id === &#39;&#39;) new_id = &quot;a&quot;;
    //6단계
    if(new_id.length &gt;= 16) {
        //ew_id = new_id.replace(/a-z/g, &quot;&quot;)
        new_id = new_id.slice(0, 15).replace(/\.$/g, &#39;&#39;);
    //7단계
    }
    if(new_id.length &lt;= 2) {
            const lastLetter = new_id[new_id.length - 1];
            const requiredCount = 3 - new_id.length;
            for(let i = 0; i &lt; requiredCount; i++){
                new_id += lastLetter;
            }
    }
    return new_id;
}</code></pre>
<hr>
<h3 id="오늘의-lesson">오늘의 Lesson</h3>
<ul>
<li>대괄호(<code>[]</code>) 안의 조건을 찾는다. (<code>a-z</code>는 문자열,<code>d</code>는 숫자 찾기)</li>
<li>캐럿(<code>^</code>)을 쓰면 <code>!</code>를 사용하는 것과 같이 불일치조건을 반환하게 된다. </li>
<li>캐럿(<code>^</code>)의 또 다른 의미는 <strong>가장 첫번째 자리</strong>가 된다. 즉, <code>^\.</code>은 가장 첫번째 자리에 <code>.</code>가 있는 지를 확인하는 것. </li>
<li>반면<code>$</code>는 마지막 자리를 체크. 즉, <code>\.$</code>는 마지막 자리에 <code>.</code>가 있는 지를 확인하는 것. </li>
<li><code>|</code>는 OR 연산자의 역할. </li>
<li><code>+</code>를 사용해서 연속되는 문자를 검색할 수 있다. </li>
<li><code>\s</code>는 공백을 의미한다.</li>
<li>연속 검색은<code>{최소, 최대}</code> 로 표현된다. 예)<code>\.{3,}</code>은 <code>.</code>이 3개 이상 (연속해서)오는 경우 전부를 검색. <code>\.{3, 6}</code>은 <code>.</code>이 3개 이상 6개 이하로 오는 경우 전부를 검색.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ETC] 기술 문서 읽기 ]]></title>
            <link>https://velog.io/@grinding_hannah/ETC-%EA%B8%B0%EC%88%A0-%EB%AC%B8%EC%84%9C-%EC%9D%BD%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/ETC-%EA%B8%B0%EC%88%A0-%EB%AC%B8%EC%84%9C-%EC%9D%BD%EA%B8%B0</guid>
            <pubDate>Wed, 07 Apr 2021 07:33:09 GMT</pubDate>
            <description><![CDATA[<h4 id="📜-매일-읽는-기술문서-업데이트-하기✏️">📜 매일 읽는 기술문서 업데이트 하기✏️</h4>
<hr>
<h3 id="2021년-4월">2021년 4월</h3>
<ul>
<li><p>7일 (수): 
<a href="https://d2.naver.com/helloworld/1848131#ch3-1" target="_blank">React 적용 가이드 - React와 Redux / 네이버 D2 블로그</a>
<a href="https://d2.naver.com/helloworld/1848131#ch3-1" target="_blank">React 적용 가이드 - React작동방법 / 네이버 D2 블로그</a>
&#39;React와  Redux&#39;에서는 create-react-app으로 리액트를 시작하는 방법(개발환경구축), 폴더구조와 각 폴더가 하는 역할에 대한 가이드, 미들웨어와 비동기 처리, 디버깅 도구 등의 사용 가이드를 제공한다. 그리고 Redux 사용 시 state가 변경되는 과정과 브라우저에서 이벤트를 받아 뷰를 변경하는 과정을 설명한다. 
&#39;React작동방법&#39;에서는 Virtual DOM과 이벤트 시스템에 관해 설명한다. DOM을 렌더링 하는 과정과 브라우저가 이벤트를 처리하는 방법에 대해 설명한다. 
📝 : 리액트가 작동되는 원리에 대해 자세히 설명하고 있어 리액트를 보다 깊이있게 이해하는 데에 도움이 될 것 같다. 그런데 아직 리액트로 코드를 작성해본 경험이 많이 없어서 본문을 완전히 이해하기에는 무리가 있었다. 코딩을 병행하면서 관련된 내용을 다시 보면 학습에 큰 도움이 될 것 같다.  </p>
</li>
<li><p>8일 (목):
<a href="https://ui.toast.com/weekly-pick/ko_20200916" target="_blank">리액트 useEffect: 개발자가 알아야 할 네가지 팁 / Toast UI Weekly Picks</a> 
리액트 훅인 useEffect를 사용할 때에 개발자가 유의해야 할 점을 4가지로 나누어 제시한다.
요약본 - <a href="https://kowoohyuk.tistory.com/115?category=850023" target="_blank">제이슨의 포스팅</a>😃
📝 : useEffect를 학습한 후 사용할 때에 다시 읽어볼 것! </p>
</li>
<li><p>9일 (금): 
<a href="https://hewonjeong.github.io/deep-dive-how-do-react-hooks-really-work-ko/" target="_blank">[번역] 심층 분석: React Hook은 실제로 어떻게 동작할까?</a> 
React Hook 사용시에는 &quot;<strong>최상위에서만 호출</strong>&quot;, &quot;<strong>오직 React 함수 내에서만 호출</strong>&quot;이라는 규칙을 따라야 하는데, 이러한 규칙이 필요한 이유에 대해 React Hook이 작동되는 원리를 제시한다. 
그 과정에서 리액트 훅 안에서 클로저(Closure)가 어떤 역할을 하는지를 중심으로 Hook의 내부 동작원리를 단계별로 설명한다. 
📝 : 리액트 사용방법을 익히는 것이 아니라 동작원리를 익혀야지만 리액트를 제대로 사용한다고 할 수 있을 것이다. 리액트 훅 사용에 앞서서 꼼꼼히 여러 번 읽어볼 필요가 있는 글. </p>
</li>
<li><p>12일 (월):
<a href="https://woowabros.github.io/experience/2020/05/13/birth-of-team-culture.html" target="_blank">팀 문화의 탄생 / 우아한 형제들 기술 블로그</a> </p>
</li>
<li><p><em>Keep*</em> : 잘하고 있는 점. 계속 했으면 좋겠다 싶은 점.</p>
</li>
<li><p><em>Problem *</em>: 뭔가 문제가 있다 싶은 점. 변화가 필요한 점.</p>
</li>
<li><p><em>Try *</em>: 잘하고 있는 것을 더 잘하기 위해서, 문제가 있는 점을 해결하기 위해서 우리가 시도해 볼 것들 (이 중에서 2, 3개를 골라 Action 아이템으로 만들고, 실제로 수행해보기)
주석에는 &#39;무엇&#39;이 아니라 &#39;<strong>왜</strong>&#39;를 적기.
📝 : 팀 프로젝트를 시작한 지 2주차에 접어들면서 어떻게하면 팀 프로젝트를 더 잘 할 수 있을까에 대한 고민이 더해졌다. 우아한 형제들에서 추구하는 팀 문화는 무엇인지 알아보면서 그들이 채택한 방법들 중 우리의 상황에도 적용해볼 수 있는 것들을 골라 시도해보아야겠다는 생각을 했다. </p>
</li>
<li><p>13월 (화):
<a href="https://analogcoding.tistory.com/181" target="_blank">styled-components 를 사용하는 8가지 이유 -번역</a>
React로 CSS를 사용하는 방법 중 가장 많이 채택되는 방법은 Styled-component 라이브러리를 사용하는 방법이다. 왜 Styled-component가 가장 많은 관심을 받는지 그 이유를 CSS-in-JS에 초점을 맞춰 설명한 글. 
같이 참고하면 좋은 글:
<a href="https://itchallenger.tistory.com/m/159" target="_blank">Styled-component 네이밍 컨벤션 팁</a>
<a href="https://d0gf00t.tistory.com/22" target="_blank">CSS-in-JS란?</a>
📝 : 생각해보면 미션 구현에 급급해서 간단히 사용방법만 익히고 Styled-component를 쓰고 있었는데, 어떤 장점을 가졌는지, 왜 쓰는지에 대한 고민은 해본적이 없었는데 생각을 해보게 만드는 글이었다. </p>
</li>
<li><p>14일 (수):
<a href="https://medium.com/daangn/deploy-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5-%ED%99%9C%EC%9A%A9-%EB%B0%A9%EB%B2%95-545f278ca878" target="_blank">deploy 브랜치 전략 활용 방법 / 당근마켓 팀블로그 </a>
팀 프로젝트처럼 여러 사람이 함께 개발을 할 때, 차례를 기다리지 않고 테스트 서버에서 각자 개발한 feature를 어떻게 테스팅 할 수 있는지 &#39;deploy 브랜치&#39;라는 브랜치 관리 전략을 제시한다. 
📝 : 배포는 아직 친숙하지 않은 영역이라 생각해보지 못했던 부분인데 협업을 할 때 git으로 생길 수 있는 문제들은 정말 무궁무진하다는 생각이 든다. deploy 브랜치 전략이라는 게 있단 걸 알게 되었으니 추후 협업 시 도움이 될 것이라 예상해본다.  </p>
</li>
<li><p>19일 (월):
<a href="https://javaplant.tistory.com/18" target="_blank">HTTP 응답코드 메소드 정리 GET, POST, PUT, PATCH, DELETE, TRACE, OPTIONS</a>
HTTP 응답코드에 대해 보기 쉽게 정리되어 있는 문서.
📝 : 서버에 데이터를 요청하는 부분의 미션의 필수조건이 되어가면서 http 요청/응답코드에 대한 지식이 필요함을 느끼고있다. 북마크하고 필요할 때마다 볼 것!</p>
</li>
<li><p>20일 (화):
<a href="https://evan-moon.github.io/2020/03/02/what-is-knowing/">나는 프론트엔드를 안다고 말할 수 있을까?</a>
프론트엔드 개발자를 어떻게 정의할 수 있을지, 어디까지가 프론트엔드 개발자의 역할이고 어디까지가 퍼블리셔의 역할인지, 내 앎의 깊이는 어디까지인지를 생각해보게 만드는 글이다. 
📝 : 그동안 당연하게만 생각해왔었던 부분들에 대해 질문을 던져보게 하는 글. 철학적인 관점에서도 프로그래밍을 바라볼 수 있다는 것이 흥미로웠고, 팀원들과 함께 각자의 감상평을 나눠볼 수 있어 흥미로웠다. </p>
</li>
<li><p>21일 (수):
<a href="https://subicura.com/2016/06/20/server-side-rendering-with-react.html" target="_blank">왜 React와 서버 사이드 렌더링인가?</a>
React가 &#39;왜&#39; 좋은 프레임워크인지, 이전에는 불가능하던 &#39;무엇&#39;을 가능하게 했는지에 대한 배경과, React가 채택한 서버사이드 렌더링의 장점을 함께 설명한다. 
📝 : 당연히 알아야만 할 것 같지만 사실 리액트 이전에 사용되던 프레임워크들을 사용한 경험이 전무한 상태에서 리액트부터 학습하다보니 리액트만의 강점이 무엇인지, 무엇이 리액트를 만들게 했는지 몸소 느끼기가 어려운 것 같다. 빠르게 변화해온 프론트엔드의 흐름을 모두 상세히 알기는 어렵겠지만 현재 사용하는 프레임워크의 탄생배경을 이해하는 데에 도움이 되는 글이었다.</p>
</li>
<li><p>22일 (목):
<a href="https://overreacted.io/ko/a-complete-guide-to-useeffect/" target="_blank">useEffect 완벽 가이드</a>
“컴포넌트의 랜더링 안에 있는 모든 함수는 (이벤트 핸들러, 이펙트, 타임아웃이나 그 안에서 호출되는 API 등) <strong>랜더(render)가 호출될 때 정의된 props와 state 값을 잡아둔다</strong>. 하나의 랜더링 스코프 안에서 props와 state는 변하지 않은 값으로 남아있다.”
사전에 잡아둔 값이 아니라 <strong>최신의 값</strong>을 쓰고 싶다면, 제일 쉬운 방법은 <code>ref</code>를 이용하는 것!
📝 : 리액트를 사용한다면 반드시 한번쯤 꼭 읽어봐야할 문서! 리액트가 작동되는 원리와 방법을 이해하기 쉽게 설명하기 때문에 초보자가 읽으면서 학습하기에도 적당하다. 다만 아티클의 길이가 굉장하고 본문에 링크가 걸린 다른 아티클들의 양도 적지 않아서 한번에 다 읽기는 어려우므로 틈틈이 읽을 것. 기억하면 좋을 중요정보들이 많다. </p>
</li>
<li><p>23일 (금):
<a href="https://ui.toast.com/weekly-pick/ko_20201022" target="_blank">글자 4개로 리액트 컴포넌트를 최적화하는 방법</a>
<code>useState()</code>의 인자로 화살표함수(<code>() =&gt;</code>)를 넘겨줌으로써 불필요한 연산을 줄이고 리액트 함수 컴포넌트의 성능을 향상시키는 &#39;지연초기화&#39; 방법을 소개한다.  </p>
</li>
<li><p><em>지연초기화*</em>: 직접적인 값 대신 함수를 <code>useState()</code>의 인자로 넘기는 것. 지연 초기화는 상태가 최초로 생성될 때만 실행되고, 이후 발생하는 리렌더링에서는 실행되지 않는다. 하지만 단순한 원시값이거나 이미 계산된 변수인 경우 오히려 매번 함수를 만드는 비용이 발생하므로 부적절. _비용이 큰 계산_에 적절하다.
📝 : <code>useState()</code>을 사용할 때 늘 인자로 직접적인 값만을 전달했었는데, &quot;함수&quot;를 전달할 수 있을 것이라는 생각을 해보지 못했었는데, 이렇게 컴포넌트를 최적화할 수 있다니 신기하게 느껴졌다. 화살표함수가 암묵적으로 값을 반환한다는 점 또한 알게 되었다.
최초 렌더링 시에만 값을 찾아야 하는 경우에 유용하게 사용할 수 있을 것 같다.</p>
</li>
<li><p>26일 (월): 
<a href="https://ui.toast.com/weekly-pick/ko_20200219" target="_blank">자바스크립트는 무엇으로 구성되어있을까?</a>
JS를 공부한다면 반드시 알아야 하는 개념들을 정리해둔 문서. 
📝 : JS를 사용하는 프론트엔드 개발자로 면접을 보게 된다면 반드시 설명할 수 있어야 하는 개념들이라는 생각이 든다. 과연 나는 할 수 있을까? 기본중의 기본이지만 막상 입밖으로 자연스레 나오지 않는..이런 개념들에 대해 내가 할 수 있는 답변들을 정리해야 할 필요가 있다 정말. </p>
</li>
<li><p>28일 (수):
<a href="https://overreacted.io/ko/goodbye-clean-code/" target="_blank">잘가, 클린 코드 / Overreacted</a>
중복 코드를 제거하려고 팀원들의 동의없이 코드를 수정했다 겪었던 일화를 소개하면서 무조건적으로 클린 코드를 추구하는 것이 능사는 아님을 전달해준다. 클린코드를 배우되, 얽메이지는 말자.
📝 : 약간의 강박처럼 미션을 할 때도 늘 클린코드를 짜야한다는 생각이 있는데, 클린코드가 무조건 좋은 게 아니라니... 충격까지는 아니지만 새로운 관점으로 생각해보게 하는 계기가 되었다. 클린코드의 정의도 사실 모호하지만 무엇보다 &#39;클린코드&#39; &lt; &#39;협업&#39; 이라는 생각이 다시 한 번 들었다. </p>
</li>
<li><p>29일(목) &amp; 30일(금): 
<a href="https://simsimjae.medium.com/%EC%95%A0%EC%9E%90%EC%9D%BC-%EB%B0%A9%EB%B2%95%EB%A1%A0-753368aa3058" target="_blank">애자일 방법론</a>
현대 소프트웨어 개발에서 강조하는 &#39;애자일 방법론&#39;의 장점과 진행방법을 구체적이고 다양하게 제시한다. 
애자일이 추구하는 가치: &quot;처음부터 완벽한 계획&quot;이 아닌 &quot;<strong>간소한 계획과 유연한 대처</strong>&quot;
📝 : 개발공부를 하게되면 꼭 한번은 들어보는 용어가 &#39;애자일&#39;이지만 이게 정확히 뭘 의미하는지, 어떻게 실행해야 하는지는 알지 못했었다. 알고보니 코쿼에서 당연한듯 해온 칸반이나 스크럼이 애자일 방식의 하나였으니 나도 모르게 어느정도 애자일 방법론을 동기들과 해온거구나.. 라는 생각을 했다. 추상적으로만 생각했던 개념이 머릿속에서 좀 더 명확해졌고, 실행방법들이 구체적으로 정리되어 있어서 참고하며 다음 협업 시 추가로 적용할 수 있는 부분들을 실행해보면 좋을 것 같다.  </p>
</li>
</ul>
<h3 id="2021년-5월">2021년 5월</h3>
<ul>
<li><p>3일 (월):
<a href="https://helloworld.kurly.com/blog/thinking-in-react/#hook" target="_blank">React 이해하기 / 마켓컬리 Tech Blog</a>
React의 간단한 소개부터 useReducer, useMemo까지 속성으로 리액트를 소개하는 아티클. hook에 대해 좀 더 깊이있게 알아보기 위해서는 <a href="https://blog.logrocket.com/use-hooks-and-context-not-react-and-redux/" target="_blank">여기 블로그</a>도 추천!
📝 : 이제 본격적으로 React의 여러가지 hook을 사용해야 하는 때가 와버렸기 때문에 입문용으로 어떤 hook이 있는지 알아보기에 괜찮음. </p>
</li>
<li><p>4일 (화):
<a href="https://juicyjerry.tistory.com/148" target="_blank">리액트 최적화 요약집</a>
리액트에서 <strong>렌더링이 되는 대표적인 조건 5가지</strong>: </p>
</li>
<li><p>state 변경이 있을 때</p>
</li>
<li><p>부모 컴포넌트가 렌더링 될 때</p>
</li>
<li><p>새로운 props이 들어올 때</p>
</li>
<li><p>shouldComponentUpdate에서 true가 반환될 때</p>
</li>
<li><p>forceUpdate가 실행될 때</p>
</li>
<li><p><em>useCallback*</em> &amp; <strong>useMemo</strong>를 사용하는 이유와 사용시 장점과 함께 리액트 앱을 최적화 하는 방법을 소개한다.
📝 : hook 사용법을 익히기에도 시간이 부족해서 최적화는 생각지도 못했었다. 그래도 최종적으로는 최적화 또한 고려해야 할테니 그 때 다시 들여다본다면 리팩토링 시 큰 도움이 될 것 같다는 생각을 했다.</p>
</li>
<li><p>6일 (목):
<a href="https://d2.naver.com/helloworld/8540176" target="_blank">flexbox로 만들 수 있는 10가지 레이아웃 / 네이버 D2 블로그</a>
CSS flex의 속성을 어떻게 사용해야 하는지 flex가 필요한 구체적인 상황과 코드예시, UI까지 포함해 자세히 알려주는 아주 멋진 flex 사용가이드 :)
📝 : flex박스는 쉬운 듯해서 자주 쓰지만 정작 속성들을 자세히 알고 쓰지는 못했던 것 같다(물론 머릿속에 지우개가 들어있어 돌아서면 잊어버리는 탓도 있다). 늘 주먹구구식으로 필요할때만 급하게 검색해서 쓰곤 했는데, 정리가 아주 잘 되어있는 자료가 생겨서 자주 찾게 될 것 같다. </p>
</li>
<li><p>10일 (월) 
<a href="https://13akstjq.github.io/react/2019/11/08/React-Transition-Group-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0.html" target="_blank">react-transition-group으로 router간 animation구현하기</a>
react-transition-group이라는 모듈을 React router를 사용하면서 transition 애니메이션에 적용하는 예시를 들어 설명한다. 
또 다른 animation 모듈: <a href="https://github.com/pmndrs/react-spring" target="_blank">React Spring</a>
📝 : 역시나 리액트에도 손쉽게 애니메이션을 적용할 수 있게 하는 툴이 많다. 아직 라우터를 지정하기 전이지만 라우터를 적용하면서 애니메이션을 구현해야 할 일이 온다면 유용하게 쓰일 것 같다. 일단은 Styled-component부터 마스터하는 걸로... 😬 </p>
</li>
<li><p>11일 (화)
<a href="https://medium.com/watcha/%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%8B%AC%EB%8F%84%EC%9E%88%EB%8A%94-%EC%9D%B4%EC%95%BC%EA%B8%B0-2f7d797e008a" 
target="_blank">깊은 복사와 얕은 복사에 대한 심도있는 이야기 / WATCHA</a></p>
</li>
<li><p>*&lt;얕은 복사&gt;**
<code>Array.prototype.slice()</code> : 중첩구조까지는 복사를 제대로 할 수 없다.
<code>Spread Operator(...)</code> : 복사하려는 객체가 iterable 이라면 복사가능. 그러나 중첩구조는 복사 불가능.</p>
</li>
<li><p>*&lt;깊은 복사&gt; **
<code>JSON.parse(JSON.stringify(arr))</code> : 중첩구조 복사 <strong>가능</strong>. 객체 순환을 통해 값을 옮겨담는 과정이 아니라, 문자열로 변환하면서 데이터 타입이 &quot;원시값&quot;의 형태로 변하기 때문.
=&gt; 자바스크립트의 모든 복사 방식은 사실 깊은 복사를 수행할 수 없다. 내부적으로 그렇게 모델링 되어있기 때문에 사용자의 편의를 위해 일부 라이브러리들이 그 역할을 대신한다. 
깊은 복사를 하기 전에 과연 깊은 복사가 그렇게 필요한 것인지 아키텍처 관점에서 다시 한 번 생각해봐야 한다. 
📝 : 얕은 복사와 깊은 복사에 대해서는 어렴풋이 알고 있었지만 자바스크립트가 실제로 깊은 복사를 할 수 없게 만들어졌다는 것은 처음 알게 된 사실이었다. 그냥 사용할 것이 아니라 다소 사소하게 느껴질 지 모르지만 차이를 알고 사용하는 것이 중요할 것 같다. </p>
</li>
<li><p>12일 (수)
<a href="https://ui.toast.com/weekly-pick/ko_20180911" target="_blank">이벤트 리스너 캐시를 이용한 React 성능 향상 / toast UI</a>
이벤트 핸들러 함수가 매 render마다 새롭게 생성되어 새로운 메모리 공간을 차지하게 되는 문제에 대한 해결책을 제시한다. 
📝 : 최근에 리액트 데이터 캐싱에 대해 관심이 생겨서 미션에도 useMemo()를 적용해보려고 공부하는 중이다. 고르는 데에 시간이 꽤나 쏟으면서 나름대로 신중히 고른 포스팅이었지만 예시가 클래스로 구현되어 있기도 하고 내가 알고싶던 부분과는 차이가 좀 있어서 아쉽게도 그다지 와닿지는 않았던 것 같다. 그치만 언젠가 여러개의 이벤트리스너를 참조할 일이 있다면 성능을 고려하면서 적용해볼 수 있을 것 같다는 생각을 했다.</p>
</li>
<li><p>13일 (목)
<a href="https://ui.toast.com/weekly-pick/ko_20200213" target="_blank">리액트 어플리케이션 구조 - 아토믹 디자인</a>
원자(Atoms), 분자(Molecules), 유기체(Organisms), 템플릿(Templates), 페이지(Pages) 단위로 UI 요소들을 나누어 효과적인 인터페이스 시스템을 만드는 디자인 모델을 소개한다.
📝 : 이전에 스노우님한테 코드 리뷰를 받으면서 &#39;아토믹 디자인&#39;을 처음 알게 되었다. MVC디자인 패턴이 익숙하기도 했고, 아직은 한번도 프로젝트에 아토믹 디자인을 적용해본 적이 없어서 아토믹 디자인의 장단점은 어떨지 궁금해진다. 다음 리액트 프로젝트에서는 아토믹 디자인으로 재사용성을 높여봐야겠다. </p>
</li>
<li><p>17일 (월)
<a href="https://brunch.co.kr/@redwit/1" target="_blank">우리가 TypeScript로 갈아탄 이유 / 레드윗러</a>
타입스크립트의 안정성을 중심으로 타입스크립트 사용을 도입하게 된 배경과 그 이유를 설명한다.
📝 : 타입스크립트를 이제 막 배우기 시작한 입장에서 JS와 비교해 어떤 장단점이 있는지, 어떤 점을 보완해줄 수 있는지를 알게되는 것이 의미가 있었다. </p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React.JS] React 개발환경 구축하기]]></title>
            <link>https://velog.io/@grinding_hannah/React.JS-React-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/React.JS-React-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 05 Apr 2021 09:15:31 GMT</pubDate>
            <description><![CDATA[<h1 id="create-react-app으로-셋업하기">create-react-app으로 셋업하기</h1>
<p>facebook에서 제공하는 <strong>create-react-app</strong> 툴을 사용하면 손쉽게 리액트 개발환경을 구축할 수 있다.
▶︎ <a href="https://github.com/facebook/create-react-app" target="_blank">create-react-app 사용안내 페이지 링크</a></p>
<h3 id="설치-방법">설치 방법</h3>
<ol>
<li><strong>프로젝트 폴더를 생성(<code>mkdir [폴더명]</code>)</strong></li>
<li>*<em>프로젝트 폴더로 이동(<code>cd [폴더명]</code>) *</em></li>
<li><strong><code>npx create-react-app [프로젝트명]</code> 로 해당 폴더에서 create-react-app 설치</strong>
이번에는 강의의 폴더구조와 일치하는 폴더구조를 만들기 위해 뒤에 <code>--scripts-version 1.1.5</code>라는 부분을 더했지만 생략가능하다. 
<img src="https://images.velog.io/images/grinding_hannah/post/5a8b254b-05e1-4d7f-9753-c8407d97d9a2/image.png" alt="">
설치가 성공적으로 완료되면 아래와 같은 메세지가 나온다.😃
<img src="https://images.velog.io/images/grinding_hannah/post/634c9b65-b832-45b1-ae7e-f9432989cf8a/image.png" alt=""></li>
<li><strong>프로젝트 폴더로 이동한 후 <code>npm start</code>로 실행한다.</strong>
이 때부터 변경을 감지해서 최신사항을 보여주게 되므로 위 명령어를 입력한 터미널은 실행된 상태로 유지해야한다.
(종료는 <code>command</code> + <code>c</code>)
<img src="https://images.velog.io/images/grinding_hannah/post/f174d45f-f79b-4ebd-8868-248773627cb6/image.png" alt=""></li>
</ol>
<p><strong>localhost:3000</strong>에서 React가 실행된 것을 확인할 수 있다! 
<img src="https://images.velog.io/images/grinding_hannah/post/886abc8e-b8a8-495c-8d62-4c353395cb22/image.png" alt=""></p>
<h3 id="구조-살펴보기">구조 살펴보기</h3>
<p>create-react-app이 만들어주는 폴더 구조는 아래와 같다. 
<img src="https://images.velog.io/images/grinding_hannah/post/cd000cae-5adc-41ca-935f-6c937c1ba390/image.png" alt="">
<strong>package.json</strong>파일 내용:
<img src="https://images.velog.io/images/grinding_hannah/post/f10e78f4-d215-4a91-b8b4-7c26c2c38a68/image.png" alt="">
<code>npm start</code>로 실행이 가능하고, 현재는 보여지지 않지만 <code>npm build</code>를 입력하면 번들링 된 파일을 확인할 수 있게 된다. </p>
<p><strong>public</strong> 폴더: 
웹서버가 궁극적으로 서버역할을 하게 되는 루트 폴더(root folder)이다. 
수정가능한 파일들을 가지고 있으며, <em>script 파일들은 <strong>src폴더</strong> 하위에 추가되어 있다</em>. 
<img src="https://images.velog.io/images/grinding_hannah/post/6818cc38-1a33-49bb-a0be-dec44d576308/image.png" alt="">
<strong>menifest.json</strong>파일은 앱에서 사용되는 메타 데이터들이 정의되어 있다.
<img src="https://images.velog.io/images/grinding_hannah/post/937a94bd-eb7c-419b-a957-f7d6172b2449/image.png" alt="">
그 외에
<strong>registerServiceWorker.js</strong>파일은 스크립트 파일을 미리 캐싱하는 역할을 하고, 
<strong>App.test.js</strong>파일은 원하는 단위(ex. 컴포넌트)별로 테스팅할 수 있게 해준다. </p>
<h1 id="폴더나누기-추천">폴더나누기 (추천)</h1>
<blockquote>
<p>이 섹션의 내용은 &#39;<a href="https://d2.naver.com/helloworld/1848131#ch3-1" target="_blank">React 적용 가이드 - React와 Redux</a>&#39; 포스팅에서 발췌한 내용입니다. </p>
</blockquote>
<h2 id="1-action-폴더">1. Action 폴더</h2>
<p>애플리케이션에서 사용하는 <strong>명령어(action type)</strong>와 <strong>API 통신</strong>과 같은 작업을 하는 <strong>액션 메서드(action creator)</strong>를 모아 둔 폴더. 
비즈니스 로직을 주로 액션 메서드에 개발하기 때문에 액션 메서드는 컴포넌트의 재활용을 높이고 코드를 관리하는 데 중요한 역할을 한다. 
액션 메서드에서는 <strong>리듀서(reducer)</strong>로 데이터 생성을 요청한다. </p>
<h2 id="2-component-폴더">2. Component 폴더</h2>
<p>React 컴포넌트로 구성된 폴더. 보통 <strong>도메인 별로 구분</strong>한다.
ex. 하위 폴더이름: footer, header, todolist, etc.
컴포넌트 폴더는 다시 <strong>컨테이너 컴포넌트(container component)</strong>와 <strong>프레젠테이션 컴포넌트(presentational component)</strong>를 구분해서 개발한다.</p>
<h4 id="i-컨테이너-컴포넌트container-component"><strong>I. 컨테이너 컴포넌트(container component)</strong></h4>
<p>여러 개의 프레젠테이션 컴포넌트로 구성돼 있으며, 데이터나 공통으로 관리해야 하는 객체, 컴포넌트 간의 인터랙션 등을 관리하는 컴포넌트다. UI 컴포넌트인 <strong>프레젠테이션 컴포넌트를 컨테이너 컴포넌트에서 관리한다</strong>고 생각하면 된다. </p>
<h4 id="ii-프레젠테이션-컴포넌트presentational-component"><strong>II. 프레젠테이션 컴포넌트(presentational component)</strong></h4>
<p>프레젠테이션 컴포넌트는 일반적으로 알고 있는 UI 컴포넌트라 생각하면 된다. </p>
<p>더 자세한 내용은 <a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.c26mmyryu" target="_blank">여기</a>참고.</p>
<h2 id="3-reducer-폴더">3. Reducer 폴더</h2>
<p>리듀서는 <strong>액션 메서드에서 변경한 상태를 받아 기존의 상태를 새로운 상태로 변경하는 일</strong>을 한다. reducer 폴더는 <strong>action 폴더와 같이 하나로</strong> 만들기도 하지만 <strong>도메인별로 구분</strong>해 만들기도 한다.
리듀서는 <strong>스토어(store)를 새로 변경</strong>하는데, 입력받는 state와 반환하는 state가 <strong>항상 같은 순수 함수</strong>로 구현돼 있다. 그렇기 때문에 Redux로 이전의 state를 추적해 시간 여행을 하는 도구를 만들 수 있다.</p>
<h2 id="4-store-폴더">4. Store 폴더</h2>
<p>store 폴더에는 <strong><code>index.js</code> 파일 하나</strong>만 있으며, 주로 <strong>미들웨어를 설정하는 일</strong>을 한다. 
비동기 통신을 사용하기 위에 redux-thunk 라이브러리를 설정하거나, state의 변경 내역을 관리하기 위해 react-router-redux 라이브러리를 추가하거나, 디버깅을 위해 react-devtool을 설정하는 일을 주로 한다.</p>
<hr>
<h1 id="reference">Reference</h1>
<p><a href="https://github.com/facebook/create-react-app">https://github.com/facebook/create-react-app</a>
<a href="https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/8090848#overview">https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/8090848#overview</a>
<a href="https://d2.naver.com/helloworld/1848131#ch3-1">https://d2.naver.com/helloworld/1848131#ch3-1</a>
<a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.c26mmyryu">https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.c26mmyryu</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] Local Storage 사용하기]]></title>
            <link>https://velog.io/@grinding_hannah/JavaScript-Local-Storage-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/JavaScript-Local-Storage-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 18 Mar 2021 10:12:02 GMT</pubDate>
            <description><![CDATA[<h2 id="localstorage">localStorage</h2>
<p>로컬 스토리지는 HTML5에서 추가된 저장소로 브라우저에 위치한다.
간단한 키와 값을 저장할 수 있고, 로컬 스토리지에 저장된 데이터는 <strong>사용자가 지우지 않는 이상 계속 브라우저에 남아있다.</strong>
로컬 스토리지는 window 객체 안에 들어있고, &#39;<strong>Storage</strong>&#39;객체를 상속받기 때문에 메소드를 사용할 수 있다. </p>
<br>

<h2 id="데이터-저장하기">데이터 저장하기</h2>
<blockquote>
<p><strong>Syntax</strong>:
localStorage.setItem(&#39;키&#39;, &#39;값&#39;);</p>
</blockquote>
<p>로컬 스토리지에 데이터를 저장할 때 쓰인다. 
key-value 순서대로 인자를 받으며, 값으로는 string, boolean, number, null, undefined  등을 저장할 수 있지만 모두 <strong>문자열로 변환된다.</strong> <strong>key도 문자열로 변환된다.</strong></p>
<p>위 문법처럼 key-value 형식으로 풀어서 여러 개를 저장할 수도 있지만, 객체를 통째로 저장하려면 <span style="color:red"><strong>JSON.stringify()</strong></span>를 사용해 객체를 그대로 문자열화 해주어야 한다. </p>
<h4 id="❗️-브라우저에서-저장된-내역-보기❗️">❗️** 브라우저에서 저장된 내역 보기**❗️</h4>
<p>크롬 개발자도구 열기 → Application 탭 → 왼쪽 메뉴에서 Storage &gt; Local Storage 클릭</p>
<pre><code class="language-js">//Ex.
//*store은 객체를 요소로 가지고 있는 배열. 코드는 생략되었으니 아래 브라우저 화면 참조.
const setStorageItem: (name, item) =&gt; localStorage.setItem(name, JSON.stringify(item))
setStorageItem(&#39;store&#39;, store); </code></pre>
<p><img src="https://images.velog.io/images/grinding_hannah/post/5fe46b46-1a0d-462c-9281-d4eb8473bf00/image.png" alt=""></p>
<br>

<h2 id="데이터-조회하기가져오기">데이터 조회하기(가져오기)</h2>
<blockquote>
<p><strong>Syntax</strong>:
localStorage.getItem(&#39;키&#39;);</p>
</blockquote>
<p><code>localStorage.setItem(&#39;키&#39;, &#39;값&#39;);</code>로 저장된 데이터는 key와 위 메소드를 사용해서 가져올 수 있다. 
<strong>JSON.stringify()</strong>를 통해 객체형식으로 저장했다면, 조회 역시 <span style="color:red"><strong>JSON.parse()</strong></span>를 해주어야한다. </p>
<hr>
<h1 id="reference">Reference</h1>
<p><a href="https://www.zerocho.com/category/HTML&amp;DOM/post/5918515b1ed39f00182d3048">https://www.zerocho.com/category/HTML&amp;DOM/post/5918515b1ed39f00182d3048</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Brainstorming] 자생사업  "잇다" 플랫폼 만들기  ]]></title>
            <link>https://velog.io/@grinding_hannah/Brainstorming-%EC%9E%90%EC%83%9D%EC%82%AC%EC%97%85-%EC%9E%87%EB%8B%A4-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/Brainstorming-%EC%9E%90%EC%83%9D%EC%82%AC%EC%97%85-%EC%9E%87%EB%8B%A4-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 15 Mar 2021 02:59:36 GMT</pubDate>
            <description><![CDATA[<h2 id="기획">기획</h2>
<h3 id="목적">목적</h3>
<ul>
<li>모티브는 KBS 프로그램, &#39;동행&#39;. 스토리텔링 + 판매플랫폼 구축
<a href="http://program.kbs.co.kr/1tv/culture/accompany/pc/index.html" target="blank">&#39;동행&#39; 공식사이트</a></li>
<li>1차 산업에 종사하면서 상품성이 있는 제품들을 생산하고 있지만 소비자와의 접점이 없어 생활고가 있는 사람들에게 판매 플랫폼을 제공한다. 주문을 받을 수 있는 시스템을 만들어서 소비자와 직접적인 거래가 가능할 수 있는 판매루트를 구축해준다. </li>
<li>현대적인 웹사이트에 사용되는 기능들을 직접 구현해보는 경험을 가진다. </li>
</ul>
<h3 id="주요-기능">주요 기능</h3>
<ul>
<li>장바구니 기능 
✅ &nbsp;선택된 아이템을 로컬 스토리지(혹은 캐시 스토리지)에 저장한다.
✅ &nbsp;추가된 아이템들의 누적된 금액을 보여준다.</li>
<li>클릭 시 관련 페이지로 전환 기능
✅ &nbsp;제품 클릭 시 상세 페이지로 전환 
✅ &nbsp;브랜드 이야기 클릭 시 소개 페이지로 전환 </li>
<li>UI에 애니메이션 적용
✅ &nbsp;장바구니 아이콘 클릭 시 장바구니에 담긴 내용(반쪽짜리 장바구니 페이지)이 toggle된다.</li>
<li>제품 검색 기능 (디바운스, 스로틀 기능 적용해보기)</li>
<li>반응형 사이트로 만들기</li>
</ul>
<h3 id="구현-순서">구현 순서</h3>
<h4 id="backend">Backend</h4>
<ul>
<li>Express 서버 구축 </li>
</ul>
<h4 id="frontend">Frontend</h4>
<ul>
<li>home페이지 만들기 
1) header - 슬로건 + 플랫폼 소개 
2) body - 판매중인 제품들을 한눈에 볼 수 있는 섹션 </li>
<li>브랜드 이야기 페이지 만들기</li>
<li>장바구니 토글 페이지 만들기</li>
</ul>
<h3 id="주의-사항">주의 사항</h3>
<ul>
<li><code>querySelector</code>, <code>querySelectorAll</code>등의 직접적인 메소드 사용은 안된다. 
유사하게 동작하는 트리탐색 함수를 직접 구현하기.</li>
</ul>
<hr>
<h2 id="ui">UI</h2>
<h3 id="홈-페이지">&lt; 홈 페이지 &gt;</h3>
<p><img src="https://images.velog.io/images/grinding_hannah/post/ae65563a-2b86-417d-afe5-31763fcd62b6/image.png" alt="">
<img src="https://images.velog.io/images/grinding_hannah/post/fdf8af5b-4bb8-498a-b43e-264138acf11e/image.png" alt=""></p>
<h3 id="제품-소개-페이지">&lt; 제품 소개 페이지 &gt;</h3>
<p><img src="https://images.velog.io/images/grinding_hannah/post/ce33f0ff-881e-4d4d-a301-ef55aac5a7f8/image.png" alt="">
<img src="https://images.velog.io/images/grinding_hannah/post/a890a839-540a-40b3-961d-cf5e1504d872/ezgif.com-gif-maker%20(3).gif" alt=""></p>
<h3 id="브랜드-스토리-페이지">&lt; 브랜드 스토리 페이지 &gt;</h3>
<h2 id=""><img src="https://images.velog.io/images/grinding_hannah/post/ddc80d6d-250a-4703-8e3b-5b0bc1d75fd2/image.png" alt=""></h2>
<h2 id="저장소">저장소</h2>
<p><a href="https://github.com/ha3158987/fe-w6-free-style" target="_blank">&#39;잇다&#39; 플랫폼 repository</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] Prototype 핵심내용 이해하기]]></title>
            <link>https://velog.io/@grinding_hannah/JavaScript-Prototype-%ED%95%B5%EC%8B%AC%EB%82%B4%EC%9A%A9-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grinding_hannah/JavaScript-Prototype-%ED%95%B5%EC%8B%AC%EB%82%B4%EC%9A%A9-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 12 Mar 2021 12:03:57 GMT</pubDate>
            <description><![CDATA[<h2 id="자바스크립트는-프로토타입-기반-언어">자바스크립트는 &#39;프로토타입 기반 언어&#39;</h2>
<p>자바스크립트는 흔히 <strong>프로토타입 기반 언어(prototype-based language)</strong>라 불린다. 객체들이 메소드와 속성들을 상속받기 위한 템플릿으로써 <strong>프로토타입 객체(prototype object)</strong>를 가진다는 의미다. 
프로토타입 객체도 다시 상위 프로토타입으로부터 상속받을 수 있고, 그 상위 프로토타입 객체도 마찬가지이므로 <strong>프로토타입 체인(prototype chain)</strong>이라 부른다. </p>
<p>사실 생성자함수는 인스턴스에 메소드와 속성을 상속해준 뒤에도 <strong>여전히 해당 메소드와 속성을 가지고 있다</strong>. 따라서 &quot;상속&quot; 보다는 <strong>&quot;behavior delegation&quot;</strong>혹은 <strong>&quot;위임&quot;</strong>의 개념에 더욱 가깝다. </p>
<p>상속되는 속성과 메소드들은 각 객체에 정의되어 있는 것이 아니라, 
생성자함수의  prototype이라는 속성에 정의되어 있다. 
즉, 객체 인스턴스가 <strong>생성자함수와 연결되는 것이 아니라 생성자함수의 prototype과 연결되는 것이다!</strong></p>
<hr>
<h2 id="constructor-prototype-instance">Constructor, prototype, instance</h2>
<p>Constructor와 prototype, 그리고 객체 instance의 관계를 도식화하면 아래와 같다.</p>
<p><img src="https://images.velog.io/images/grinding_hannah/post/89534db8-776a-4ccc-9b87-ce646b9733c1/image.png" alt=""></p>
<blockquote>
<ul>
<li>어떤 <strong>생성자 함수(Constructor)</strong>를 <strong><code>new</code></strong> 연산자와 함께 호출하면</li>
</ul>
</blockquote>
<ul>
<li>Constructor에서 정의된 내용을 바탕으로 새로운 <strong>인스턴스(instance)</strong>가 생성된다.</li>
<li>이때 instance에서는 <strong><code>__proto__</code></strong>라는 프로퍼티가 자동으로 부여되는데, </li>
<li>이 프로퍼티는 Constructor의 <strong>prototype</strong>이라는 프로퍼티를 참조한다.</li>
<li><code>__proto__</code>프로퍼티는 생략 가능하도록 구현돼 있기 때문에 <strong>생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 된다.</strong></li>
</ul>
<pre><code class="language-js">//ex.
const arr = [1, 2]</code></pre>
<p>arr는 생성자함수 Array()의 instance이다. 그리고 Object()는 Array()의 constructor이다.
<img src="https://images.velog.io/images/grinding_hannah/post/9d1baef5-d9e7-4e41-be0f-33deebb9232c/image.png" alt=""></p>
<hr>
<h2 id="dunder-proto">Dunder proto</h2>
<blockquote>
<p><code>__proto__</code>: <strong>Dunder proto</strong>; exposes the value of the internal [[Prototype]] of an object. For objects created using an object literal, this value is <code>Object.prototype</code>. For objects created using array literals, this value is <code>Array.prototype</code>. For functions, this value is <code>Function.prototype</code>.
-<em>MDN</em></p>
</blockquote>
<p><strong>ES5.1 *<em>명세에는 <code>__proto__</code>가 아니라 *</em><code>[[prototype]]</code></strong>이라는 명칭으로 정의돼 있습니다. <strong><code>__proto__</code>라는 프로퍼티는 사실 브라우저들이 <code>[[prototype]]</code>을 구현한 대상에 지나지 않았습니다. ** ... **ES6</strong>에서는 이를 브라우저에서 동작하는 레거시 코드에 대한 호환성 유지 차원에서 정식으로 인정하기 시작했습니다. 다만 어디까지나 브라우저에서의 호환성을 고려한 지원일 뿐 권장되는 방식은 아니며, 브라우저가 아닌 다른 환경에서는 얼마든지 이 방식이 지원되지 않을 가능성이 열려있습니다. 
-<em>코어자바스크립트</em></p>
<p>Dunder proto(<code>__proto__</code>) 프로퍼티는 <strong>생성자 함수의 프로퍼티</strong>를 가리킨다. 
생성자 함수의 인스턴스가 생성될 때마다 다른 프로퍼티 &amp; 메소드와 함께 <strong>dunder proto 도 인스턴스에 복사된다</strong>. 
학습목적으로 이해는 하되, 실무에서 사용하지는 말 것❗️
대신에 <code>Object.getPrototypeOf()</code> 혹은 <code>Object.create()</code>를 이용하기.</p>
<pre><code class="language-javascript">//만일 &#39;john&#39;객체가 생성자함수&#39;Person&#39;의 인스턴스라면, 
john.__proto__ === Person.prototype      //true </code></pre>
<hr>
<h1 id="프로퍼티-위임상속">프로퍼티 위임(상속)</h1>
<p>본격적으로 자바스트립트에서 프로퍼티를 위임하는 방법을 알아보자!</p>
<h2 id="new-생성자함수">new 생성자함수();</h2>
<p>첫번째는<code>new</code>키워드를 이용해 인스턴스를 생성하는 방법이다.</p>
<blockquote>
<p>new constructor[([arguments])]</p>
</blockquote>
<p>아래의 예시를 통해 이를 시각화 해보고자 한다. </p>
<pre><code class="language-javascript">//생성자함수 Person
const Person = function (name, yearOfBirth, job) {
  this.name = name;
  this.yearOfBirth = yearOfBirth;
  this.job = job;
};

Person.prototype.calculateAge = function () {
  console.log(2016 - this.yearOfBirth);
};

Person.prototype.lastName = &quot;Smith&quot;;

//Person의 인스턴스 john
const john = new Person(&quot;John&quot;, 1990, &quot;teacher&quot;);</code></pre>
<p>위 예시에서는 <code>new</code> 키워드를 이용해 인스턴스인 <code>john</code>을 생성했다.
아래와 같이 개발자도구의 console은 prototype 연쇄를 보다 직관적으로 보여준다.
<img src="https://images.velog.io/images/grinding_hannah/post/ebfa2f02-a44c-4fec-a021-2607d514b6cb/image.png" alt=""></p>
<p>인스턴스는 부모객체 혹은 생성자함수의 prototype 객체에 접근할 수 있기 때문에 
일치하는 프로퍼티명이 나올 때까지, 혹은 연쇄가 끝날 때까지 <strong>prototype 연쇄를 따라 올라간다</strong>.</p>
<pre><code class="language-javascript">/*lastName은 Person.prototype에 위치한 프로퍼티이다.*/
console.log(john.lastName);       //Smith</code></pre>
<p><strong>최상위 prototype</strong> 연쇄는 내장객체인 <code>Object.prototype</code>이다. 
따라서 <strong>연쇄가 끝나는 지점</strong> 또한 <code>Object.prototype</code>이다. 
<strong>+PLUS</strong>:<code>Object.prototype.__proto__;</code>는 <code>null</code>을 반환한다. 
만일 연쇄 끝에 이르러서도 원하는 프로퍼티가 발견되지 않으면 <code>undefined</code>를 반환한다. </p>
<pre><code class="language-javascript">console.log(john.age);            //undefined </code></pre>
<p><code>john</code>객체는 <code>Object.prototype</code> 의 <code>hasOwnProperty()</code> 메소드 또한 사용할 수 있는데,
이것은 특정 프로퍼티가 <code>john</code>객체 내부에 존재하는지, 아니면 상위 prototype 객체에 존재하는지를 구분한다.</p>
<pre><code class="language-javascript">/*job은 john내부에 위치한 프로퍼티이다.*/
john.hasOwnProperty(&#39;job&#39;);       //true

/*lastName은 Person.prototype에 위치한 프로퍼티이다.*/
john.hasOwnProperty(&#39;lastName&#39;);  //false</code></pre>
<hr>
<h2 id="objectcreate">Object.create();</h2>
<p>prototype을 상속받는 방법에는 <code>new</code>키워드를 이용해 인스턴스를 생성하는 방법 이외에도, <code>Object.create()</code>를 이용하는 방법이 있다. </p>
<blockquote>
<p>Object.create(proto[, propertiesObject])</p>
</blockquote>
<p><code>new</code>키워드를 이용하면 생성자함수의 prototype 프로퍼티를 그대로 상속받지만, <code>Object.create()</code>를 이용해 객체 생성 시, 
<strong>1) 빈 객체<code>{}</code>를 반환한다.</strong>
<strong>2) Object.create()의 인자로 들어오는 것이 생성되는 빈 객체의 prototype이 된다. 
즉, 어떤 객체가 prototype이 되어야 하는 지 정확히 지정하는 것이 가능하다!</strong>
복잡하게 얽힌 prototype 연쇄도 보다 간단하게 만들 수 있다는 장점이 있어 가장 많이 쓰이는 방법이다.</p>
<pre><code class="language-javascript">const personProto = {
  calculateAge: function () {
    console.log(2016 - this.yearOfBirth);
  },
};

/*객체 john 생성*/
const john = Object.create(personProto);
john.name = &quot;John&quot;;
john.yearOfBirth = 1990;
john.job = &quot;teacher&quot;;

/*객체 jane 생성 - 두번째 매개변수 주목!*/
const jane = Object.create(personProto, {
  name: { value: &quot;Jane&quot; },
  yearOfBirth: { value: 1969 },
  job: { value: &quot;designer&quot; },
});</code></pre>
<p><code>john</code>은 <code>Object.create()</code>의 첫번째 인자인 <code>personProto</code>에서 직접적으로 prototype을 상속받는다. 
그리고 <strong>나머지 프로퍼티들은 동적으로 생성</strong>하여 추가하였다. </p>
<p>반면,<code>jane</code>은 <strong>필요한 프로퍼티들을 가진 객체를 **<code>Object.create()</code></strong>의 두번째 인자로 전달<strong>받았다. 
이 때 주의할 점은, **값을 <code>{}</code>으로 감싸야한다</strong>는 점이다.</p>
<p><img src="https://images.velog.io/images/grinding_hannah/post/a75924c6-8c67-4635-8015-7f66d7a7b579/image.png" alt=""></p>
<p>console 창을 통해 확인해보면 두 객체 모두 같은 동일한 prototype을 가진 객체가 되었음을 알 수 있다. </p>
<pre><code class="language-javascript">john.calculateAge();       //26
jane.calculateAge();       //47</code></pre>
<hr>
<h2 id="prototype은-가족이다-👨👩👧">prototype은 가족이다?! 👨‍👩‍👧</h2>
<p>프로토타입을 설명하는 자료는 다양하게 있지만, 
그 중에서도 가장 기억에 남으면서도 이해가 쏙쏙 되었던 것이 <strong>&#39;Family-oriented Prototype&#39;</strong> 컨셉이라, 여기서 잠깐 되짚어보고 가려고 한다! </p>
<p>Prototype을 가족관계에 비유하면, </p>
<blockquote>
<ul>
<li>모든 생성자함수는 <strong>아내</strong>이다. prototype(남편)의 입장에서<code>constructor</code>메소드는 아내를 가리킨다.</li>
</ul>
</blockquote>
<ul>
<li>모든 prototype객체는 <strong>남편</strong>이다. 인스턴스(자녀)의 입장에서 <code>__proto__</code>는 아빠를 가리킨다.</li>
<li>남편과 아내는 <strong>1:1</strong>로 매칭되어있다. </li>
<li>생성자함수로 만들어진 인스턴스는 <strong>자녀</strong>다. </li>
<li>모든 자녀는 <strong>아빠(prototype객체)로부터 속성을 빌려쓸 수 있다</strong>.  </li>
</ul>
<p><img src="https://images.velog.io/images/grinding_hannah/post/1529bf49-7551-4ada-aa86-9916baf1a68d/image.png" alt=""></p>
<p>예를 들어 아래와 같이<code>Dog</code>이라는 생성자 함수가 있다고 하자. </p>
<pre><code class="language-javascript">//생성자함수 = 엄마
function Dog() {
  this.name = &quot;Freddie&quot;;
  this.color = &quot;brown&quot;;
  this.numLegs =4; 
}

//Dog의 인스턴스 = 자녀 
let hound = new Dog();
</code></pre>
<p>생성자함수인 <code>Dog</code>은 <strong>엄마</strong>가된다. 
엄마로 인해 만들어진 <code>hound</code>인스턴스는 <strong>자녀</strong>가 된다. 
그리고 <code>Dog</code>과 동시에 생겨난  <code>Dog.prototype</code>은  <code>Dog</code>의 <strong>남편</strong>이자 <code>hound</code>의 <strong>아빠</strong>가 되는 것이다. </p>
<p>만일 <code>hound</code>가 원하는 속성을 자기 내부에 가지고 있지 않다면, 아빠인 <code>Dog.prototype</code>은 <code>hound</code>에게 자신의 속성들을 빌려줄 것이다!</p>
<hr>
<h1 id="prototype-지정과-constructor">prototype 지정과 constructor</h1>
<h2 id="생성자함수명prototype--constructor-생성자함수명">생성자함수명.prototype = {constructor: 생성자함수명}</h2>
<p>위 비유에서 쓰인 예제를 통해 설명을 이어가자면, 
<code>Dog</code>의 인스턴스인 <code>hound</code>는  새로운 빈 객체로 생겨나는 동시에 <code>Dog</code>내부에서 <code>this.__</code>로 정의된 속성들을 할당받게 된다. </p>
<p>앞서 언급되었던 것처럼 아내와 남편은 1:1매칭이므로, 모든 함수는 prototype객체를 내장한 채로 태어난다. 
생성자함수 <code>Dog</code>도 prototype객체(남편)를 갖고 있으며,<code>hound</code>는 자신의 아빠인 <code>Dog.prototype</code>에 접근할 수 있게 된다. </p>
<p>우리가 <code>Dog</code>의 모든 인스턴스들이 사용할 수 있도록 생성자함수에 속성을 추가하고자 할 때, </p>
<pre><code class="language-javascript">Dog.prototype.bark = &quot;wuff&quot;;</code></pre>
<p>간단히 위와 같이 추가할 수도 있지만, 
여러 개의 속성을 한꺼번에 추가할 때에는 <strong>새로운 객체를 prototype에 할당하는 방법도 있다.</strong></p>
<pre><code class="language-javascript">Dog.prototype = {
  hasTail: &quot;Yes&quot;, 
  eat: function() {
    console.log(&quot;nom nom nom&quot;);
  },
  describe: function() {
    console.log(&quot;My name is &quot; + this.name);
  }
};</code></pre>
<p>그런데 사실 이 방법은 아래와 같이 <strong>기존 <code>constructor</code>속성을 지워버리는 부작용이 있다.</strong></p>
<pre><code class="language-javascript">Dog.prototype.constructor;   //function Object(){...} (Uh-oh❗️🤭)</code></pre>
<p><code>prototype.constructor</code>라면 prototype객체의 아내를 가리키게 되는 격인데, 반환값은 <code>Dog</code>이 아니라 <code>Object(){...}</code>가 되어있다! 
이건 명백히 옳지않은 반환값이므로 객체 내부에 <strong><code>constructor: 생성자함수명</code> 이라는 코드를 추가</strong>해야지만 <code>constructor</code> 속성의 의도치 않은 변화를 바로잡을 수가 있다.</p>
<blockquote>
<p>객체를 prototype에 할당할 때에는, <code>constructor: 생성자함수명;</code>코드를 포함시킨다.</p>
</blockquote>
<pre><code class="language-javascript">Dog.prototype = {
  constructor: Dog,  //&lt;--***************다시 원래대로 돌려놓기*****************
  hasTail: &quot;Yes&quot;, 
  eat: function() {
    console.log(&quot;nom nom nom&quot;);
  },
  describe: function() {
    console.log(&quot;My name is &quot; + this.name);
  }
};

Dog.prototype.constructor;    //function Dog(){...}   (😊)</code></pre>
<hr>
<h2 id="subtypeprototype--objectcreatesupertypeprototype-그리고-subtypeprototypeconstructor--subtype">subtype.prototype = Object.create(supertype.prototype); 그리고 subtype.prototype.constructor = subtype;</h2>
<blockquote>
<p><code>subtype</code>은 <strong>자식</strong>이 되는 객체를 가리킨다. 
<code>supertype</code>은 <strong>부모</strong>가 되는 객체를 가리킨다. </p>
</blockquote>
<p>아래와 같이 <code>subtype.prototype = Object.create(supertype.prototype);</code>를 통해 객체들을 prototype chain(프로토타입 연쇄; 여기서는 부모-자식객체 간 프로퍼티 위임을 받을 수 있는 관계성을 말함)으로 연결해줄 수 있다. </p>
<pre><code class="language-javascript">/*supertype:Animal, subtype:Dog*/
Dog.prototype = Object.create(Animal.prototype);

/*supertype:Dog, subtype:beagle*/
let beagle = new Dog();</code></pre>
<blockquote>
<p>특이한 점은, 
한 객체가 다른 객체에게 prototype을 위임할 때 <code>supertype</code>객체의 <code>constructor</code>속성이 함께 위임된다는 것이다.
따라서 <strong>override 된 <code>constructor</code>속성을 <code>subtype.prototype.constructor = subtype;</code>으로 반드시 정정해줘야 한다.</strong>⭐️⭐️⭐️ </p>
</blockquote>
<p>그래서 현재상태에서는 <code>beagle</code>인스턴스가 <code>constructor</code>로 <code>Dog</code>생성자함수의 상위객체인 <code>Animal</code> 을 가리키고 있는 것을 확인할 수 있다.</p>
<pre><code class="language-javascript">console.log(beagle.constructor);    //function Animal(){...}</code></pre>
<p>여기서 다시 family-oriented prototype이론을 적용해보자면, <code>beagle</code> 은  <code>Dog</code>의 자녀가 된다. 
그리고 <code>Dog</code>은 <code>Animal.prototype</code>의 자녀가 된다. </p>
<p><code>beagle</code>은 <code>constructor</code>라는 속성이 없기 때문에 <code>beagle.constructor</code>가 값을 반환할 수 있는 이유는 <code>beagle</code>이 아빠인 <code>Dog.prototype</code>객체로부터 속성을 빌려올 수 있기 때문이다. </p>
<p>따라서 원래대로라면 <code>beagle.constructor</code>는 <strong>외할머니(<code>Animal</code>)가 아닌 엄마(<code>Dog</code>)를 반환하는 것이 맞다!!</strong></p>
<p>그러니 인위적이지만 우리는 다시 <code>Dog.prototype</code>의 아내을 <code>Dog</code>으로 되돌려줄 필요가 있다❗️</p>
<pre><code class="language-javascript">/*prototype.constructor에 올바른 값 할당하기*/
Dog.prototype.constructor = Dog;    
console.log(beagle.constructor);    //function Dog(){...}  짜잔!원래대로 돌아왔다👍
</code></pre>
<hr>
<h1 id="prototype-과-this">prototype 과 this</h1>
<h2 id="생성자함수명callthis">생성자함수명.call(this);</h2>
<blockquote>
<p>하위계층의 객체들이 <strong>공통의 속성을 갖고 있으면서도 각각의 고유한 속성을 갖고 있는 경우, <code>생성자함수명.call(this);</code>로 중복되는 코드를 대체할 수 있다</strong>. </p>
</blockquote>
<p>예를 들어, </p>
<pre><code class="language-javascript">function Carrier (name){
  this.name = name;                           //&lt;-name
}

Carrier.prototype.move = function () {        //&lt;-move
  console.log(&quot;move&quot;);
};

function Vehicle (name, wheels) {
  this.name = name;                           //&lt;-name 반복 
  this.numOfWheels = wheels;
}

Vehicle.prototype.move = function () {        //&lt;-move 반복
  console.log(&quot;move&quot;);
}

Vehicle.prototype.hasWheels = function () {
  console.log(&quot;Yes, it has wheels.&quot;);
}

var helicopter = new Carrier(&quot;Black Hawk&quot;);
var car = new Vehicle(&quot;Hummer&quot;, 4);
</code></pre>
<p>이런 코드가 있다고 하자. 
생성자함수인 <code>Carrier</code> 와<code>Vehicle</code>은 각자 <code>helicopter</code>와 <code>car</code>라는 인스턴스를 가지고 있다. 
아래 그림처럼 두 인스턴스는 모두 <code>name</code>과 <code>move</code>라는 속성을 가지고 있다. (편의상 <code>move</code>도 가지고 있다고 하였다. 정확히 말하면 <code>move</code> 는 가지고 있지 않지만 <em>사용할 수 있다.</em>)
<img src="https://images.velog.io/images/grinding_hannah/post/127648a3-bf17-4752-a6c7-606f060d205e/image.png" alt=""></p>
<p>하지만 현재는<code>name</code>과 <code>move</code>를 정의하는 코드가 반복되고 있다. 
자세히보면 <code>Carrier</code>가 가지고 있는 모든 속성들은 <code>Vehicle</code>이 가지고 있기때문에, <code>Vehicle</code>은 <code>Carrier</code>로부터 필요한 속성을 상속받으면 된다!</p>
<p>*<em>Carrier(상위객체) &gt; Vehicle(하위객체) *</em></p>
<p>중복되는 코드 <code>this.name = name;</code>은  <code>Carrier.call(this, name);</code>로 변경한다. 
<strong><code>Carrier.call(this, name);</code> 내부의 this를 알기 위해서는 이 코드를 가지고 있는<code>Vehicle</code>이 실행된 방법을 찾아야한다. <code>new</code>키워드로 실행된 <code>Vehicle</code>의 this는 <code>Vehicle</code>의 인스턴스인 <code>car</code>가 된다.</strong> </p>
<p>그리고 <code>move</code>의 경우, 게시글 상단의 Object.create() 부분에서 설명했던 것처럼
1)<code>Vehicle.prototype.move</code>를 삭제하고<code>Vehicle.prototype = Object.create(Carrier.prototype);</code>로 <strong><code>Vehicle</code>의 prototype을 <code>Carrier.prototype</code>으로 지정해준다</strong>. 
2) <strong>prototype 객체가 대체되면서 사라진 <code>constructor</code>속성은 다시 <code>Vehicle.prototype.constructor = Vehicle;</code>로 재할당 해준다</strong>.
이 방법으로는 <code>move</code>속성을 위임할 수 있다. </p>
<p>위 방법들을 적용해 코드를 정정하면 다음과 같다:</p>
<pre><code class="language-javascript">function Carrier (name){
  this.name = name;
}

Carrier.prototype.move = function () {
  console.log(&quot;move&quot;);
};

function Vehicle (name, wheels) {
  Carrier.call(this, name);                
  //.call(this); 로 대체 
  this.numOfWheels = wheels;
}

Vehicle.prototype = Object.create(Carrier.prototype);
Vehicle.prototype.constructor = Vehicle;
//Carrier.prototype을 vehicle.prototype의 prototype으로 지정해주기!!
//바뀐 constructor 속성은 올바르게 재할당해주기!!

Vehicle.prototype.hasWheels = function () {
  console.log(&quot;Yes, it has wheels.&quot;);
}

var helicopter = new Carrier(&quot;Black Hawk&quot;);
var car = new Vehicle(&quot;Hummer&quot;, 4);</code></pre>
<p>결과적으로 중복되는 코드를 없앴음에도<code>helicopter</code>와 <code>car</code>는 전부 정정 전과 동일하게 생성이 되었다!! </p>
<hr>
<h2 id="remember">Remember...</h2>
<p>prototype위임 프로세스를 알아보는 과정에서 <code>constructor</code>속성을 다루기는 했지만, 가급적 <code>constructor</code>메소드나 <code>__proto__</code>는 <strong>사용하지 않아야 한다.</strong>(사용하지 말 것!) 
앞서 설명한 것처럼 의도치않은 변경으로 혼선을 야기할 가능성도 있을 뿐더러, 이제는 type 확인에<code>Array.isArray()</code>나 <code>typeof</code>등의 메소드로 대체할 수 있기 때문이다. 
<em>무분별한 사용은 독이 된다!!</em></p>
<hr>
<h2 id="reference">Reference</h2>
<p>*본 포스팅은 아래 사이트들을 참고 및 인용하여 작성되었습니다. 
학습단계로 잘못된 정보가 있을 수 있습니다. 잘못된 부분에 대해 알려주시면 곧바로 정정하도록 하겠습니다 😊
<a href="https://velog.io/@jakeseo_me/2019-05-03-1005-%EC%9E%91%EC%84%B1%EB%90%A8-evjv7dy8vh">https://velog.io/@jakeseo_me/2019-05-03-1005-%EC%9E%91%EC%84%B1%EB%90%A8-evjv7dy8vh</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto">https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto</a>
<a href="https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Object_prototypes">https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Object_prototypes</a>
<a href="https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/object-oriented-programming/remember-to-set-the-constructor-property-when-changing-the-prototype">https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/object-oriented-programming/remember-to-set-the-constructor-property-when-changing-the-prototype</a>
정재남, 『코어 자바스크립트 - 핵심 개념과 동작 원리로 이해하는 자바스크립트 프로그래밍』, 위키북스(2019)</p>
]]></description>
        </item>
    </channel>
</rss>