<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hongkyu_mr_chu.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 12 Feb 2024 13:57:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hongkyu_mr_chu.log</title>
            <url>https://velog.velcdn.com/images/hongkyu_mr_chu/profile/abef8677-203e-4f9b-a49a-24bb91a41a75/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hongkyu_mr_chu.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hongkyu_mr_chu" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Docker + Git Actions]]></title>
            <link>https://velog.io/@hongkyu_mr_chu/Docker-Git-Actions</link>
            <guid>https://velog.io/@hongkyu_mr_chu/Docker-Git-Actions</guid>
            <pubDate>Mon, 12 Feb 2024 13:57:21 GMT</pubDate>
            <description><![CDATA[<h3 id="깃-허브에서-actions를-누른다">깃 허브에서 Actions를 누른다.</h3>
<ul>
<li>공식 독스 <a href="https://docs.github.com/en/actions/quickstart">https://docs.github.com/en/actions/quickstart</a></li>
</ul>
<p>docker-image.yml 파일을 작성합니다.</p>
<h3 id="배포할-브랜치-정하기">배포할 브랜치 정하기</h3>
<pre><code class="language-bash">name: Deploy Next.js App

on:
  push:
    branches:
      - main</code></pre>
<h2 id="jobs">JOBS</h2>
<pre><code class="language-bash">jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3</code></pre>
<ol>
<li><p><strong><code>jobs</code></strong>: 작업(job), 작업은 하나 이상의 단계<strong><code>steps</code></strong>로 구성됩니다.</p>
</li>
<li><p><strong><code>build-and-deploy</code></strong>: 작업의 이름!</p>
</li>
<li><p><strong><code>runs-on:</code></strong>: 이 작업이 실행될 환경, <strong><code>ubuntu-latest</code> 제 서버는 Oracle Ubuntu 입니다.</strong></p>
</li>
<li><p><strong><code>uses: actions/checkout@v3</code></strong>: 이 단계는 GitHub 리포지토리를 체크아웃하여 작업을 실행하는데 필요한 파일들을 가져옵니다. <strong><code>actions/checkout</code></strong> 라이브러리를 사용하고 있습니다.</p>
<ul>
<li><p>actions/checkout@v1 를 하면 아래와 같은 경고 정도 받을 수 있습니다.</p>
<blockquote>
<p>Node.js 16 actions are deprecated. Please update the following actions to use Node.js 20:</p>
</blockquote>
</li>
<li><p>라이브러리의 최신을 여기서 확인할 수 있습니다.</p>
<p><a href="https://github.com/actions/checkout">https://github.com/actions/checkout</a></p>
</li>
</ul>
</li>
</ol>
<h2 id="env-파일-설정">env 파일 설정</h2>
<ol>
<li>.env 파일을 만들어줍니다.<ul>
<li>secrets는 Settings ⇒ <strong>Actions secrets and variables 에서 환경변수처럼 만들 수 있습니다.</strong></li>
<li>.env 파일을 만들고 변수를 설정합니다.</li>
</ul>
</li>
</ol>
<pre><code class="language-bash">- name: Create .env file
  run: |
     touch .env
     echo &quot;NEXT_PUBLIC_API_KEY=${{ secrets.NEXT_PUBLIC_API_KEY }}&quot; &gt;&gt; .env
     echo &quot;NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}&quot; &gt;&gt; .env
     echo &quot;NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }}&quot; &gt;&gt; .env
     echo &quot;GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}&quot; &gt;&gt; .env
     echo &quot;GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}&quot; &gt;&gt; .env</code></pre>
<h2 id="docker-설정">Docker 설정</h2>
<ol>
<li><p><strong>Set up Docker</strong>: Docker를 설정, <strong><code>docker/setup-buildx-action@v3</code></strong> 액션을 사용하여 Docker를 설정. 이것은 Docker 빌드를 위한 도구인 Buildx를 설정하는 데 사용됩니다.</p>
<ul>
<li><p>최신 릴리즈가 v3인 것으로 보입니다.</p>
<p><a href="https://github.com/docker/setup-buildx-action">https://github.com/docker/setup-buildx-action</a></p>
<pre><code class="language-bash">  - name: Set up Docker
    uses: docker/setup-buildx-action@v3</code></pre>
</li>
</ul>
</li>
<li><p><strong>Login to DockerHub</strong>: Docker Hub에 로그인, <strong><code>docker/login-action@v3</code></strong> 액션을 사용하여 Docker Hub에 로그인합니다. 이 때 사용자 이름과 비밀번호가 필요하며, 여기서는 GitHub Secrets에 저장된 값들을 사용합니다.</p>
<ul>
<li><p>최신 역시나 v3이다.</p>
</li>
<li><p>DOCKER_USERNAME =  “이 값은 Docker 허브에 가입했을때 프로필에 username 입니다.”</p>
</li>
<li><p>DOCKER_PASSWORD =  “이 값은 Docker 허브에서 토큰을 설정해서 나온 토큰값입니다.”</p>
<p><a href="https://github.com/docker/login-action">https://github.com/docker/login-action</a></p>
<pre><code class="language-bash">  - name: Login to DockerHub
    uses: docker/login-action@v3
    with:
      username: ${{ secrets.DOCKER_USERNAME }}
      password: ${{ secrets.DOCKER_PASSWORD }}</code></pre>
</li>
</ul>
</li>
<li><p><strong>Build and push Docker image</strong>: Docker 이미지를 빌드하고 Docker Hub에 푸시(push)하는 단계입니다. <strong><code>docker/build-push-action@v3</code></strong> 액션을 사용하여 이미지를 빌드하고, 푸시합니다. 여기서 사용되는 옵션들은 다음과 같습니다:</p>
<ul>
<li><p><strong><code>context</code></strong>: Docker 빌드 컨텍스트를 지정합니다. 보통 현재 디렉토리를 의미하는 <strong><code>.</code></strong>을 사용합니다.</p>
</li>
<li><p><strong><code>file</code></strong>: 사용할 Dockerfile의 경로를 지정합니다.</p>
</li>
<li><p><strong><code>push</code></strong>: 이미지를 푸시할 것인지 여부를 나타내는 부울 값입니다. 여기서는 <strong><code>true</code></strong>로 설정되어 있으므로 이미지를 푸시합니다.</p>
</li>
<li><p><strong><code>tags</code></strong>: 빌드한 이미지에 지정할 태그를 지정합니다. 여기서는 GitHub Secrets에 저장된 Docker 사용자 이름을 포함한 태그를 사용합니다. 보통은 <strong><code>username/repository:tag</code></strong> 형식으로 사용됩니다.</p>
</li>
<li><p>DOCKER_USERNAME =  “이 값은 Docker 허브에 가입했을때 프로필에 username 입니다.”</p>
<pre><code class="language-bash">  - name: Build and push Docker image
    uses: docker/build-push-action@v3
    with:
      context: .
      file: ./Dockerfile
      push: true
      tags: ${{ secrets.DOCKER_USERNAME }}/nextjs-app:latest</code></pre>
</li>
</ul>
</li>
</ol>
<p><a href="https://github.com/marketplace/actions/docker-login">Docker Login - GitHub Marketplace</a></p>
<h2 id="deploy-to-server"><strong>Deploy to Server</strong></h2>
<p>서버에 배포하는 단계입니다. <strong><code>appleboy/ssh-action@master</code></strong> 액션을 사용하여 SSH를 통해 서버에 접속하고, 배포 스크립트를 실행합니다. 이 때 사용되는 옵션들은 다음과 같습니다</p>
<pre><code class="language-bash">- name: Deploy to Server
  uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SERVER_IP }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SERVER_SSH_KEY }}
        script: |
          cd 경로
          docker pull ${{ secrets.DOCKER_USERNAME }}/nextjs-app:latest
          docker stop nextjs-app || true
          docker rm nextjs-app || true
          docker run -d --name nextjs-app -p 80:3000 ${{ secrets.DOCKER_USERNAME }}/nextjs-app:latest</code></pre>
<ul>
<li><strong><code>host</code></strong>: 서버의 IP 주소를 지정합니다.<ul>
<li><strong>SERVER_IP</strong> = “이 값은 서버 ip 입니다.”</li>
</ul>
</li>
<li><strong><code>username</code></strong>: 서버에 접속할 때 사용할 사용자 이름입니다.<ul>
<li><strong>SERVER_USER</strong> = “서버 만들 때 사용했던 이름”</li>
</ul>
</li>
<li><strong><code>key</code></strong>: SSH 키를 지정합니다. 이것은 서버에 접속하기 위한 인증에 사용됩니다. 보통은 SSH 키의 내용을 secrets에 저장합니다.<ul>
<li>SERVER_SSH_KEY = “<strong><code>-----BEGIN OPENSSH PRIVATE KEY-----</code></strong> 부터 <strong><code>-----END OPENSSH PRIVATE</code>&quot; (개인 키 열어보시면 요런 형태)</strong></li>
</ul>
</li>
<li><strong><code>script</code></strong>: 서버에 실행할 스크립트를 정의합니다. 이 스크립트는 서버에서 실행됩니다. 여기서는 다음과 같은 작업들이 수행됩니다:<ul>
<li>cd 프로젝트 위치</li>
<li><strong><code>docker pull ${{ secrets.DOCKER_USERNAME }}/nextjs-app:latest</code></strong>: Docker Hub에서 최신 이미지를 다운로드합니다.</li>
<li><strong><code>docker stop nextjs-app || true</code></strong>: 실행 중인 Docker 컨테이너를 중지합니다. 만약 해당 컨테이너가 실행 중이지 않다면 에러가 발생하지 않도록 <strong><code>true</code></strong>를 추가하여 무시합니다.</li>
<li><strong><code>docker rm nextjs-app || true</code></strong>: 중지된 Docker 컨테이너를 제거합니다. 마찬가지로 에러를 무시합니다.</li>
<li><strong><code>docker run -d --name nextjs-app -p 80:3000 ${{ secrets.DOCKER_USERNAME }}/nextjs-app:latest</code></strong>: 새로운 Docker 컨테이너를 실행합니다. 여기서는 포트 80으로 외부와 통신할 수 있도록 설정되어 있습니다.</li>
</ul>
</li>
</ul>
<h2 id="빌드-과정-확인">빌드 과정 확인</h2>
<p>파일을 업데이트 되면 빌드 배포 과정을 아래 그림처럼</p>
<p><del>vercel, 젠킨스, 넷틀리파이처럼</del> 실패 한 부분을 확인 할 수있다.
<img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/6c5ff74e-e320-4eeb-a01b-070ffdf5d75a/image.png" alt="">
<img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/8c9eb7ad-9d54-4ff7-9ef6-3f5e3ab9c05a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker + NEXT.js]]></title>
            <link>https://velog.io/@hongkyu_mr_chu/Docker-NEXT.js</link>
            <guid>https://velog.io/@hongkyu_mr_chu/Docker-NEXT.js</guid>
            <pubDate>Fri, 09 Feb 2024 14:02:04 GMT</pubDate>
            <description><![CDATA[<p>😄이 글은 Oracle + 우분투로 서버를 구축한 상태에서 시작합니다.</p>
<h3 id="마이그레이션-이유-vercel504">마이그레이션 이유: Vercel:504</h3>
<p>최근 프로젝트를 Vercel에 배포하게 되면 로컬 로그인, 로컬 회원가입이 504 Gateway Timeout 오류를 반환하였습니다. </p>
<p>문제의 원인은 Vercel의 무료 플랜과 관련된 자원 제한이 주요 원인 중 하나로 추정되었습니다. 실제로 Vercel Pro 플랜으로 업그레이드 한 후, 문제가 해결되었습니다.</p>
<blockquote>
<p>Vercel: <a href="https://vercel.com/pricing">Serverless functions have a timeout of only 10 seconds</a> (300 seconds if you upgrade to pro plan)</p>
</blockquote>
<p>위의 글에서 볼수 있듯이 Vercel의 무료 플랜에서는 서버리스 함수의 실행 시간이 10초로 제한됩니다. 
그래서 제 해결책은 자체 서버를 구축하여 데이터베이스 요청의 처리 속도와 효율성을 향상시키는 것입니다. </p>
<h3 id="도커-설정법">도커 설정법</h3>
<ol>
<li><p><strong>Docker 설치를 위한 패키지 업데이트</strong></p>
<pre><code class="language-bash"> sudo apt-get update</code></pre>
</li>
<li><p><strong>필요한 패키지 설치</strong></p>
<pre><code class="language-bash"> sudo apt-get install apt-transport-https ca-certificates curl software-properties-common</code></pre>
</li>
<li><p><strong>Docker의 공식 GPG 키 추가</strong></p>
<pre><code class="language-bash"> curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -</code></pre>
</li>
<li><p><strong>Docker 저장소를 APT 소스에 추가</strong></p>
<pre><code class="language-bash"> sudo add-apt-repository &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot;</code></pre>
</li>
<li><p><strong>Docker 패키지 업데이트 및 설치</strong></p>
<pre><code class="language-bash"> sudo apt-get update
 sudo apt-get install docker-ce</code></pre>
</li>
<li><p><strong>Docker가 정상적으로 설치되었는지 확인</strong></p>
<pre><code class="language-bash"> sudo docker run hello-world</code></pre>
</li>
<li><p><strong>Docker 이미지 빌드</strong> </p>
<ul>
<li><p>폴더 만들어주시고 깃 클론으로 프로젝트 가져옵시다.</p>
</li>
<li><p>.env 파일도 꼭 설정해주시고요.</p>
</li>
<li><p>Dockerfile도 있어야합니다.</p>
<pre><code class="language-bash">mkdir project
cd project
git clone ‘url’</code></pre>
<pre><code class="language-bash">sudo docker build -t nextjs-app .</code></pre>
</li>
</ul>
</li>
<li><p><strong>Docker 컨테이너 실행</strong></p>
<ul>
<li><p>본인은 nginx포트가 80을 이미 사용중이여서 변경하였습니다.</p>
<pre><code>sudo docker run -d -p 3000:3000 nextjs-app
sudo docker run -d -p 80:3000 nextjs-app</code></pre></li>
<li><p>자신의 컨테이너를 확인하고 싶다면</p>
<pre><code>sudo docker ps</code></pre></li>
<li><p>컨테이너 이름 또는 ID로 여러가지를 할 수 있다. stop,remove</p>
<pre><code>sudo docker stop 컨테이너
sudo docker rm 컨테이너</code></pre></li>
</ul>
</li>
<li><p><strong>Oracle Cloud Infrastructure의 VCN 설정</strong></p>
<p> <strong>포트 80을 외부 접속 개방하기</strong></p>
<p> Security List</p>
<ul>
<li><p>Source CIDR: 0.0.0.0/0, 모든 IP에서 오는 요청</p>
</li>
<li><p>IP Protocol: TCP</p>
</li>
<li><p>Destination Port Range: 80, 개방할 포트</p>
<pre><code class="language-css">sudo docker run -d -p 80:3000 nextjs-app</code></pre>
</li>
</ul>
</li>
</ol>
<ol start="10">
<li><p>이제 브라우저에 IP주소로 접속시 프로젝트를 잘 확인할 수 있습니다.</p>
</li>
<li><p>가비아에서 도메인을 삽니다. (이벤트 하는 이름으로 사면 좀더 싸요.)</p>
</li>
<li><p>DNS 관리 ⇒ DNS 설정 ⇒ 도메인을 연결 ⇒ 레코드 추가</p>
</li>
</ol>
<pre><code>| A | @ | 당신의 ip 주소 |
| --- | --- | --- |
| CNAME | www | ott-we-t.com. |</code></pre><ol start="3">
<li><a href="http://ott-we-t.com">http:ott-we-t.com</a> 접속</li>
</ol>
<p>다음은 <strong>Git Actions</strong>를 활용해서 자동배포
<a href="https://velog.io/@hongkyu_mr_chu/Docker-Git-Actions">https://velog.io/@hongkyu_mr_chu/Docker-Git-Actions</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[dangerouslySetInnerHTML did not match ]]></title>
            <link>https://velog.io/@hongkyu_mr_chu/dangerousSlysetInnerHTML-did-not-match</link>
            <guid>https://velog.io/@hongkyu_mr_chu/dangerousSlysetInnerHTML-did-not-match</guid>
            <pubDate>Wed, 24 Jan 2024 05:51:26 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-발단">문제 발단</h3>
<p>OTT 공유 플랫폼 <a href="https://we-t-ott.vercel.app/">[We T]</a> 프로젝트 (간단하게 게시판)에서 게시글에 html 태그를 이용하여 스타일을 더 하기로 하였다.
글쓰기의 에디터영역에서 글을 작성할때 html태그를 포함한 형태로 DB에 보냈다.
홈영역에서 게시글을 미리보기로 보여주고 있는데 이 부분에서 문제가 발생하였다.</p>
<p><img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/883db179-4530-41e8-a338-8830c84c354a/image.png" alt=""></p>
<pre><code>&lt;p 
  className=&quot;text-base line-clamp-3 text-ellipsis text-gray-700 no-style&quot; 
  dangerouslySetInnerHTML={{__html: item.content}}&gt;
&lt;/p&gt;</code></pre><h3 id="문제-인식">문제 인식</h3>
<p>오류를 확인해 보면 dangerousSlysetInnerHTML&#39; did not match. Server: &#39;&#39; Client: &#39;&lt;&gt;&lt;/&gt;&#39; </p>
<p>서버(Server)에서 pre-rendering 된 html은 “” 이고 클라이언트(Client)는 “&lt;&gt; 왓챠 …&lt;/&gt;” 이런식이니 서로 일치하지 않고 있다는 소리이다.</p>
<p>We T 프로젝트의 홈 ‘/’ 인덱스 페이지는 아래 그림처럼 SSG로 정적 생성된 페이지이다.<img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/dd68239d-f371-4fc0-95ae-2d836663ce9e/image.png" alt=""></p>
<p>getStaticProps로 데이터를 이용한 정적 생성을 하고 있다.</p>
<pre><code class="language-tsx">export const getStaticProps = async () =&gt; {
  let client = await connectDB;
  const db = client.db(&#39;forum&#39;);
  let data = await db.collection(&#39;post&#39;).find().toArray();
  return { props: { data: JSON.parse(JSON.stringify(data)) } };
};</code></pre>
<p>그렇다면 사전 렌더링을 하는 과정에서 문제가 있는 것일까? 
DB에 html 태그에 클로징 태그 &lt;/&gt; 가 빠진 것이 있는지 체크해보았다.
하지만 이 문제도 아니였다.</p>
<h2 id="첫번째-시도---ssr--false">첫번째 시도 - SSR : false</h2>
<pre><code>const ListComponent = dynamic(() =&gt; import(&#39;components/home/ListComponent&#39;), { ssr: false })</code></pre><p>인덱스에 있는 컴포넌트를 사전 렌더링(pre-rendering)하지 않기로 하였다.
이렇게 하니까 mis match가 일어나지 않았고 오류가 사라졌다.
하지만 이렇게 클라이언트 단에서만 존재하게 되면 <em>SEO에 좋지 않고 초기 렌더링도 길어질 수 있다.</em></p>
<p>자 이제 남들은 대체 어떻게 했는지 찾아보자…</p>
<h2 id="html5">HTML5</h2>
<p>나는 마크업 퍼블리싱 일도 많이 했고 링크드인 HTML5테스트도 통과하여 HTML을 좀 안다고 생각하였다. </p>
<p>인터넷을 통해 알게 된 사실은 나는 아직도 HTML에 대해 무지 하다는 것이다.<img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/7885916e-39ab-4ef5-9d00-790e2057ce06/image.png" alt="">
<a href="https://www.w3.org/TR/html401/struct/text.html#h-9.3.1">https://www.w3.org/TR/html401/struct/text.html#h-9.3.1</a></p>
<p>W3C(W3에서) p 태그에 대해 이렇게 말하고 있다.</p>
<p>HTML에서 <strong><code>&lt;p&gt;</code></strong> 요소의 종료 태그는 생략될 수 있는데, 이는 <strong><code>&lt;p&gt;</code></strong> 요소가 <strong><code>address</code></strong>, <strong><code>article</code></strong>, <strong><code>aside</code></strong>, <strong><code>blockquote</code></strong>, <strong><code>div</code></strong>, <strong><code>dl</code></strong>, <strong><code>fieldset</code></strong>, <strong><code>footer</code></strong>, <strong><code>form</code></strong>, <strong><code>h1</code></strong>, <strong><code>h2</code></strong>, <strong><code>h3</code></strong>, <strong><code>h4</code></strong>, <strong><code>h5</code></strong>, <strong><code>h6</code></strong>, <strong><code>header</code></strong>, <strong><code>hgroup</code></strong>, <strong><code>hr</code></strong>, <strong><code>main</code></strong>, <strong><code>nav</code></strong>, <strong><code>ol</code></strong>, <strong><code>p</code></strong>, <strong><code>pre</code></strong>, <strong><code>section</code></strong>, <strong><code>table</code></strong>, 또는 <strong><code>ul</code></strong> 요소에 바로 이어질 경우, 혹은 부모 요소에 더 이상의 콘텐츠가 없고 그 부모 요소가 <strong><code>&lt;a&gt;</code></strong> 요소가 아닐 경우에 해당합니다.</p>
<p><strong><code>&lt;p&gt;</code></strong> 요소는 문단을 나타냅니다. 이 요소는 블록 레벨 요소(자기 자신인 <strong><code>&lt;p&gt;</code></strong> 포함)를 포함할 수 없습니다. 저희는 작성자들이 빈 <strong><code>&lt;p&gt;</code></strong> 요소를 사용하는 것을 권장하지 않습니다. 사용자 에이전트는 빈<strong><code>&lt;p&gt;</code></strong> 요소를 무시해야 합니다.</p>
<p>실습으로 확인해보자.
내가 이렇게 마크업을 한다면</p>
<pre><code class="language-html">&lt;p&gt;hello&lt;div&gt;mr.chu&lt;/div&gt;&lt;/p&gt;</code></pre>
<p>이렇게 출력된다.
p태그 안에 <strong><code>div</code></strong> 와 같은 요소가 들어오면 p 태그는 종료 태그가 만들어진다.</p>
<p><img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/0adb8b4a-a389-498c-8aae-3ab2274645c0/image.png" alt=""></p>
<p>결론적으로 <strong><code>&lt;p&gt;</code></strong> 태그는 블록 레벨 요소를 포함할 수 없고 빈<strong><code>&lt;p&gt;</code></strong> 태그의 사용을 권장하지 않는다.</p>
<ul>
<li>문단이 단독으로 콘텐츠를 포함하고 있음을 명확하게 하기 위해서다.</li>
<li>마크업을 피하고, 콘텐츠의 의미와 구조를 더 명확하게 하기 위함이다.</li>
</ul>
<h2 id="결론">결론</h2>
<p>내가 겪은 오류는 next.js의 pre-rendering 된 부분과 client 에 렌더링 된 부분이 일치 하지 않았던 것이고 이 문제의 해결법은 dangerouslySetInnerHTML을 잘못 사용했거나 SSG사용 방식이 잘못되었던게 아니라</p>
<pre><code class="language-tsx">&lt;div 
  className=&quot;text-base line-clamp-3 text-ellipsis text-gray-700 no-style&quot; 
  dangerouslySetInnerHTML={{__html: item.content}}
/&gt;</code></pre>
<p>마크업에 대한 이해가 부족했던 것이고 해결책은 <strong>dangerousSlysetInnerHTML를 쓸때는 **<code>div</code></strong> 태그 안에 쓰는게 좋을것이다.**</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[font.json 최적화(글리프 압축)]]></title>
            <link>https://velog.io/@hongkyu_mr_chu/font.json-%EC%B5%9C%EC%A0%81%ED%99%94%EA%B8%80%EB%A6%AC%ED%94%84-%EC%95%95%EC%B6%95</link>
            <guid>https://velog.io/@hongkyu_mr_chu/font.json-%EC%B5%9C%EC%A0%81%ED%99%94%EA%B8%80%EB%A6%AC%ED%94%84-%EC%95%95%EC%B6%95</guid>
            <pubDate>Sat, 30 Dec 2023 15:05:00 GMT</pubDate>
            <description><![CDATA[<h3 id="포인트문제-해결법">포인트(문제 해결법)</h3>
<ul>
<li>필요한 글자 모양(<strong>glyph)</strong>을 정리하자.</li>
<li>Json 파일에 필요한 문자만 남기고 지우자.</li>
</ul>
<h3 id="문제-발단">문제 발단</h3>
<p><a href="https://mrchu.netlify.app/"><img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/030486c0-14be-4de7-b48d-fd5d433c0c65/image.webp" alt=""></a></p>
<p>근래 포트폴리오(모음) 사이트를 업데이트 하면서 3D 텍스트를 넣었습니다.
라이브러리가 추가된 만큼 성능 개선을 하던 중</p>
<p>프로젝트에 사용되는 글자 모양은 ‘M’, ‘r’, ‘.’ ‘C’, ‘h’, ‘u’일 뿐인데…</p>
<p>Pretendard.json 파일(11.3MB)의 용량을 줄일 수 없을까?라는 생각이 들었습니다.</p>
<p>웹 폰트에서도 <strong>글리프 압축</strong>이 존재하니까요.
Pretendard.json 파일을 확인해 보니 웹폰트 처럼 글리프 속성으로 모양을 정하고 있었습니다.
<img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/bdc47db8-0125-4102-9c4b-d02a3c9ca913/image.webp" alt=""></p>
<p>오호 그렇다면 여기서 내가 필요한 것들만 두고 필요없는 것들을 제거하면 되겠다는 생각이 들었습니다.</p>
<pre><code class="language-jsx">const modifiedGlyphs = {};  //수정된 글리프를 저장할 곳

for (let i = 0; i &lt; targetText.length; i++) {
    const char = targetText[i];
  const charKey = char in fontData.glyphs ? char : char.toUpperCase();
  if (charKey in fontData.glyphs) {
     modifiedGlyphs[charKey] = fontData.glyphs[charKey];
  }
}</code></pre>
<p>변수 fontData에 폰트를 담고 반복문을 통해서 </p>
<p>대,소문자를 먼저 체크하고</p>
<p><strong><code>charKey</code></strong>가 <strong><code>fontData.glyphs</code></strong> 객체에 존재하는 키라면, 해당 글리프를 <strong><code>modifiedGlyphs</code></strong> 저장합니다.</p>
<p>잘될 것이라 생각했는데, 만들어낸 json 파일은 오류가 났습니다.
알고 보니 Pretendard.json 파일에는 글리프 말고도 다른 여러 속성들이 필요합니다.
<img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/56a5a752-7a73-4b43-a51b-73352ceaba1b/image.webp" alt=""></p>
<p>속성은 폰트마다 다를수도 있으니 글리프가 아닌 속성들은 그대로 복사해 가져와야 겠습니다.</p>
<pre><code class="language-jsx">const modifiedFontData = {
    ...fontData,
  glyphs: modifiedGlyphs
};</code></pre>
<p>11.3MB 에서 5KB 까지 줄였네요.!
<img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/2ee9f061-caf5-4169-9813-4f0c60e66243/image.webp" alt=""></p>
<p>혹시 나중에도 변환 코드를 사용할지 모르니까, 깃허브에 배포해놨습니다.
<a href="https://chuhongkyu.github.io/font-json/"><img src="https://velog.velcdn.com/images/hongkyu_mr_chu/post/c4b96917-35be-44b1-886e-f67daafa5346/image.webp" alt="">
</a>
<a href="https://chuhongkyu.github.io/font-json/">https://chuhongkyu.github.io/font-json/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSR 페이지 전환 모션
(Recat Router v6.4)]]></title>
            <link>https://velog.io/@hongkyu_mr_chu/CSR-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A0%84%ED%99%98-%EC%8B%AC%EB%A6%AC%EC%8A%A4</link>
            <guid>https://velog.io/@hongkyu_mr_chu/CSR-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A0%84%ED%99%98-%EC%8B%AC%EB%A6%AC%EC%8A%A4</guid>
            <pubDate>Sat, 25 Nov 2023 07:56:25 GMT</pubDate>
            <description><![CDATA[<p>구현 링크 (<a href="https://4mryxj.csb.app/">https://4mryxj.csb.app/</a>)</p>
<h2 id="안녕하세요">안녕하세요</h2>
<p>이 글에서는 리액트 라우터 돔v6.4 createBrowserRouter에서 페이지 전환시 어떻게 framer-motion의 exit를 활용하는지 이야기 해보려 합니다.</p>
<p>_ Exit animations
<strong><code>AnimatePresence</code></strong> works by detecting when <strong>direct children</strong> are removed from the React tree.
Any <strong><code>motion</code></strong> components contained by the removed child that have an <strong><code>exit</code></strong> prop will fire that animation before the entire tree is finally removed from the DOM.
<strong>Note:</strong> Direct children must each have a unique <strong><code>key</code></strong> prop so <strong><code>AnimatePresence</code></strong> can track their presence in the tree._</p>
<p>AnimatePresence는 React 트리에서 직접적인 자식이 제거될 때 작동합니다. 그렇기 때문에 트리에서 존재 여부를 추적할 수 있도록 고유한 key 속성을 가져야 합니다.</p>
<p>그래서 페이지 전환시 AnimatePresence의 자식 요소가 변했다는것을 알려주기 위해 자식요소로 Layout을 만들어 pathname으로 key값을 줍니다.</p>
<h3 id="nextjs-예시">NEXT.js 예시</h3>
<pre><code class="language-jsx">//Root
&lt;RecoilRoot&gt;
  &lt;HeadComponent/&gt;
    &lt;AnimatePresence initial={false} exitBeforeEnter&gt;
         &lt;Component {...pageProps} key={router.pathname} /&gt;
    &lt;/AnimatePresence&gt;
&lt;/RecoilRoot&gt;

//Layout
export default function Layout({children}:LayoutComponent) {
    return (
        &lt;motion.div 
            initial={{opacity:0}} 
            animate={{opacity:[0,1]}} 
            exit={{opacity: [1,0]}}&gt;
            {children}
        &lt;/motion.div&gt;
    );
}</code></pre>
<h2 id="browserrouter">BrowserRouter</h2>
<p>그렇다면 리액트 라우터 돔에서는 어떻게 해왔을까요?
중첩 라우팅 예시를 확인해 보겠습니다.</p>
<pre><code class="language-jsx">//Root
&lt;BrowserRouter&gt;
   &lt;Routes&gt;
      &lt;Route index path=&quot;/&quot; element={&lt;Index /&gt;} /&gt;
      &lt;Route path=&quot;/*&quot; element={&lt;NotFound /&gt;} /&gt;
      &lt;Route path=&quot;/home/*&quot; element={&lt;Home /&gt;} /&gt;
  &lt;/Routes&gt;
&lt;/BrowserRouter&gt;

//Home
export function Home() {
    const location = useLocation()
    return(
        &lt;&gt;
        &lt;Nav/&gt;
        &lt;Content/&gt;
        &lt;AnimatePresence initial={false}&gt;
           &lt;Routes location={location} key={location.pathname}&gt;
             &lt;Route path=&quot;resume&quot; element={&lt;Resume /&gt;} /&gt;
             &lt;Route path=&quot;about&quot; element={&lt;About /&gt;} /&gt;
             &lt;Route path=&quot;github&quot; element={&lt;GitHub /&gt;} /&gt;
             &lt;Route path=&quot;game_app&quot; element={&lt;Others /&gt;} /&gt;
             &lt;Route path=&quot;project&quot; element={&lt;Project /&gt;} /&gt;
             &lt;Route path=&quot;detail/:id&quot; element={&lt;Detail /&gt;} /&gt;
           &lt;/Routes&gt;
        &lt;/AnimatePresence&gt;
        &lt;/&gt;
    )
}</code></pre>
<p>Data Api가 존재하지 않는 BrowserRouter 컴포넌트로 작업할 때
Routes에 key로 location.pathname를 할당하는 형식으로 작업 했습니다.</p>
<h2 id="그러나-createbrowserrouter-">*<em>그러나 (createBrowserRouter) *</em></h2>
<p>v6.4부터 data Api를 지원하는 createBrowserRouter로 작업해야 하는 경우가 생겼습니다.</p>
<p><em>An <code>&lt;Outlet&gt;</code> should be used in parent route elements to render their child route elements. This allows nested UI to show up when child routes are rendered. If the parent route matched exactly, it will render a child index route or nothing if there is no index route.</em></p>
<p>하위 경로 요소를 렌더링하려면 상위 경로 요소에서 <code>&lt;Outlet&gt;</code> 를 사용해야 합니다.
그래서 아래와 같이 구성 합니다.</p>
<pre><code class="language-jsx">
//Root
const router = createBrowserRouter(routerInfo)

const root = ReactDOM.createRoot(document.getElementById(&#39;root&#39;));
root.render(
  &lt;React.StrictMode&gt;
     &lt;RouterProvider router={router} /&gt;
  &lt;/React.StrictMode&gt;
);

//router.js 
export const routerInfo = [
    {
      path: &quot;/&quot;,
      element: &lt;App/&gt;,
      errorElement: &lt;NotFound/&gt;,
      children: [
        {
            path: &quot;home&quot;,
            element: &lt;Home/&gt;,
            errorElement: &lt;div&gt;error&lt;/div&gt;,
            children: [
              {
                  path: &quot;resume&quot;,
                  element: &lt;Resume /&gt;,
              },
              {
                 path: &quot;detail/:id&quot;,
                 element: &lt;Detail /&gt;,
                                 loader: getDetailPage
              },
...

//Home
export function Home() {
    const location = useLocation()
    return(
        &lt;&gt;
        &lt;Nav/&gt;
        &lt;Content/&gt;
        &lt;AnimatePresence initial={false}&gt;
           &lt;Outlet/&gt;
        &lt;/AnimatePresence&gt;
        &lt;/&gt;
    )
}</code></pre>
<p>그렇다면 이제 어디에 키를 주어 AnimatePresence의 트리에 변화를 알릴까요?</p>
<p>Outlet에 key 값을 줘봅시다.</p>
<pre><code class="language-tsx">export function Home() {
    const location = useLocation()
    return(
        &lt;&gt;
        &lt;Nav/&gt;
        &lt;Content/&gt;
        &lt;AnimatePresence initial={false}&gt;
           &lt;Outlet key={loaction.pathname}/&gt;
        &lt;/AnimatePresence&gt;
        &lt;/&gt;
    )
}</code></pre>
<p>React Developer Tools를 보면 Outlet에 key 값이 들어갔지만 framer-motion에 exit가 일어나지 않습니다.</p>
<h2 id="outlet은-대체-무엇일까">Outlet은 대체… 무엇일까?</h2>
<pre><code class="language-tsx">interface OutletProps {
  context?: unknown;
}
declare function Outlet(
  props: OutletProps
): React.ReactElement | null;</code></pre>
<p><a href="https://github.com/remix-run/react-router/discussions/8008#discussioncomment-1280897">[v6] First-class animation primitives · remix-run/react-router · Discussion #8008</a></p>
<p>링크에 글을 보고 알게 된 것은</p>
<p><strong><code>Outlet</code></strong>은 <strong><code>location</code></strong>이 변경될 때마다 다음에 렌더링할 컴포넌트를 찾아 렌더링하는 역할을 합니다. <strong><code>Outlet</code></strong> 자체는 내부적으로 상태를 유지하지 않고, <strong><code>location</code></strong>이 변경되면 해당 위치에 따라 매번 렌더링을 수행합니다.</p>
<p><strong><code>Outlet</code></strong>은 단순히 중첩된 라우트 트리에서 현재 위치에 해당하는 하위 라우트를 찾아 렌더링하는 것이라는 겁니다.</p>
<p>따라서 <strong><code>Outlet</code></strong>은 상태를 가지지 않고 <strong><code>location</code></strong>에 의존하여 동적으로 컴포넌트를 렌더링하는 역할을 합니다. 상위 컴포넌트에서 <strong><code>location</code></strong>이 변경될 때마다 <strong><code>Outlet</code></strong>은 해당 위치에 따라 다음에 렌더링할 컴포넌트를 찾아 렌더링합니다.</p>
<p>그렇다면 우리는 어떻게 해야하는가?</p>
<pre><code class="language-jsx">function AnimatedOutlet() {
  let [context] = useState(useOutlet());
  return context.outlet;
}</code></pre>
<p>글에서 처럼  useOutlet()을 사용하여 AnimatedOutlet 컴포넌트를 활용합시다.</p>
<pre><code class="language-jsx">const AnimatedOutlet = () =&gt; {
  const o = useOutlet();
  const [outlet] = useState(o);

  return &lt;&gt;{outlet}&lt;/&gt;;
};</code></pre>
<h2 id="animatedoutlet의-역할은">AnimatedOutlet의 역할은</h2>
<ul>
<li><strong>현재 라우트의 Outlet을 가져오기:</strong> <strong><code>useOutlet</code></strong>을 호출하여 현재 라우트에 해당하는 <strong><code>Outlet</code></strong> 컴포넌트를 얻습니다.</li>
<li><strong>상태로 Outlet 관리:</strong> <strong><code>useState</code></strong>를 사용하여 <strong><code>outlet</code></strong>을 상태로 관리합니다. 이는 리액트 컴포넌트의 상태로써 <strong><code>outlet</code></strong>이 변경될 때마다 컴포넌트가 리렌더링되도록 합니다.</li>
<li><strong>UI에 Outlet 렌더링:</strong> JSX에서 <strong><code>{outlet}</code></strong>을 통해 현재 라우트에 해당하는 <strong><code>Outlet</code></strong> 컴포넌트를 렌더링합니다. 이로써 현재 라우트의 컴포넌트가 동적으로 표시됩니다.</li>
</ul>
<pre><code class="language-tsx">&lt;AnimatePresence mode=&quot;wait&quot;&gt;
    &lt;AnimatedOutlet key={location.pathname} /&gt;
&lt;/AnimatePresence&gt;</code></pre>
<h2 id="요약">요약</h2>
<p>React Router v6.4 최신버젼에서 Data Api 지원하는 createBrowserRouter를 사용할 때 Outlet 컴포넌트를 사용하게 되는데 Outlet은 라우트가 변경될 때마다 단순히 새로운 컴포넌트를 렌더링합니다. 그래서 Outlet을 상태관리하는 AnimatedOutlet을 만들고 key 값을 사용하여 AnimatePresence가 라우트 변경을 감지하도록 합니다. 이로써 라우트 변경 시에 리마운트를 통한 애니메이션(exit) 효과를 얻을 수 있습니다.</p>
<h2 id="코드-소스">코드 소스</h2>
<p>아래는 아래와 같은 뎁스가 있는 화면전환을 코드샌드박스에 올려 놨습니다.</p>
<ul>
<li>index<ul>
<li>home<ul>
<li>login</li>
<li>more</li>
</ul>
</li>
<li>mypage</li>
</ul>
</li>
</ul>
<p><a href="https://codesandbox.io/p/sandbox/routing-motion-4mryxj?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clpdlsidb01t73b6j5pnade1m%2522%252C%2522sizes%2522%253A%255B70%252C30%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clpdlsida01t43b6jqi1hcok0%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clpdlsida01t63b6joa66483h%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clpdlsida01t53b6j2u0nknsb%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B59.12336992480133%252C40.87663007519867%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clpdlsida01t43b6jqi1hcok0%2522%253A%257B%2522id%2522%253A%2522clpdlsida01t43b6jqi1hcok0%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clpdlqcq401so3b6jljei20jf%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fpublic%252Findex.html%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522activeTabId%2522%253A%2522clpdlqcq401so3b6jljei20jf%2522%257D%252C%2522clpdlsida01t53b6j2u0nknsb%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clpdl2bsw00053b6jkfzujanv%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%2522%257D%252C%257B%2522type%2522%253A%2522SANDBOX_INFO%2522%252C%2522id%2522%253A%2522clpdlphqp01jb3b6jbyq4stx8%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%252C%2522id%2522%253A%2522clpdlsida01t53b6j2u0nknsb%2522%252C%2522activeTabId%2522%253A%2522clpdl2bsw00053b6jkfzujanv%2522%257D%252C%2522clpdlsida01t63b6joa66483h%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clpdlsida01t63b6joa66483h%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A17.059659090909093%257D">https://codesandbox.io/p/sandbox/routing-motion-4mryxj?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clpdlsidb01t73b6j5pnade1m%2522%252C%2522sizes%2522%253A%255B70%252C30%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clpdlsida01t43b6jqi1hcok0%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clpdlsida01t63b6joa66483h%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clpdlsida01t53b6j2u0nknsb%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B59.12336992480133%252C40.87663007519867%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clpdlsida01t43b6jqi1hcok0%2522%253A%257B%2522id%2522%253A%2522clpdlsida01t43b6jqi1hcok0%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clpdlqcq401so3b6jljei20jf%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fpublic%252Findex.html%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522activeTabId%2522%253A%2522clpdlqcq401so3b6jljei20jf%2522%257D%252C%2522clpdlsida01t53b6j2u0nknsb%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clpdl2bsw00053b6jkfzujanv%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%2522%257D%252C%257B%2522type%2522%253A%2522SANDBOX_INFO%2522%252C%2522id%2522%253A%2522clpdlphqp01jb3b6jbyq4stx8%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%252C%2522id%2522%253A%2522clpdlsida01t53b6j2u0nknsb%2522%252C%2522activeTabId%2522%253A%2522clpdl2bsw00053b6jkfzujanv%2522%257D%252C%2522clpdlsida01t63b6joa66483h%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clpdlsida01t63b6joa66483h%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A17.059659090909093%257D</a></p>
]]></description>
        </item>
    </channel>
</rss>