<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>madstone-dev.log</title>
        <link>https://velog.io/</link>
        <description>기록보다 기력을</description>
        <lastBuildDate>Sat, 26 Feb 2022 14:36:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>madstone-dev.log</title>
            <url>https://images.velog.io/images/madstone-dev/profile/facf5833-50bc-477b-9253-ed62385bb14c/social.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. madstone-dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/madstone-dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[신규프로젝트 - 2. 언어팩 설정]]></title>
            <link>https://velog.io/@madstone-dev/%EC%8B%A0%EA%B7%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2.-%EC%96%B8%EC%96%B4%ED%8C%A9-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@madstone-dev/%EC%8B%A0%EA%B7%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2.-%EC%96%B8%EC%96%B4%ED%8C%A9-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sat, 26 Feb 2022 14:36:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 글의 주제
<code>Laravel</code> 언어 설정 변경하기</p>
</blockquote>
<h3 id="목표">목표</h3>
<ul>
<li>언어 설정 변경하기</li>
</ul>
<hr>
<h4 id="1-기본-언어-설정-변경하기">1. 기본 언어 설정 변경하기</h4>
<p>라라벨의 언어관련 설정은 <code>config/app,php</code> 에서 확인 할 수 있습니다.</p>
<p>기본적으로 언어는 <code>en</code> 으로 설정 되어 있습니다.</p>
<pre><code class="language-php">    &#39;locale&#39; =&gt; &#39;en&#39;,</code></pre>
<p>한글로 설정하고싶다면 해당 부분을 <code>ko</code> 로 변경할 수 있습니다.</p>
<br/>

<h4 id="2-번역-제공하기">2. 번역 제공하기</h4>
<p>기본 언어 설정을 변경했다면 해당 언어에 맞는 언어팩을 제공해주어야합니다.</p>
<p>라라벨은 기본적으로 <code>lang</code> 폴더에 영어 관련 언어팩을 포함하고 있습니다.</p>
<p>다른 언어팩을 제공하려면 <code>lang</code> 폴더에 해당 언어로 <code>json</code> 파일을 생성해야합니다.</p>
<pre><code class="language-javascript">// lang/ko.json
{
  &quot;Whoops!&quot;: &quot;앗!&quot;,
  &quot;Hello!&quot;: &quot;안녕하세요!&quot;,
  &quot;Reset Password Notification&quot;: &quot;비밀번호 재설정 알림&quot;,
  &quot;You are receiving this email because we received a password reset request for your account.&quot;: &quot;귀하의 비밀번호 재설정 요청으로 인해 이 메일이 발송되었습니다.&quot;,
  &quot;Reset Password&quot;: &quot;비밀번호 재설정&quot;,
   // ...생략
}
</code></pre>
<p>언어팩을 제공하면 해당 언어에 맞게 메시지들이 변경되어 출력됩니다.</p>
<h4 id="3-예시">3. 예시</h4>
<p>라라벨의 인증 템플릿을 사용하면 영어로된 인증메일을 수신할 수 있습니다.</p>
<p>인증관련 메일은 <code>vendor/laravel/framework/src/Illuminate/Auth/Notifications</code> 폴더에서 기본 인증 메일 관련 기능을 확인 할 수 있습니다.</p>
<pre><code class="language-php">  protected function buildMailMessage($url)
  {
      return (new MailMessage)
          -&gt;subject(Lang::get(&#39;Reset Password Notification&#39;))
          -&gt;line(Lang::get(&#39;You are receiving this email because we received a password reset request for your account.&#39;))
          -&gt;action(Lang::get(&#39;Reset Password&#39;), $url)
          -&gt;line(Lang::get(&#39;This password reset link will expire in :count minutes.&#39;, [&#39;count&#39; =&gt; config(&#39;auth.passwords.&#39;.config(&#39;auth.defaults.passwords&#39;).&#39;.expire&#39;)]))
          -&gt;line(Lang::get(&#39;If you did not request a password reset, no further action is required.&#39;));
    }</code></pre>
<p><img src="https://images.velog.io/images/madstone-dev/post/aa0f2a0e-596e-4700-bd9c-be2d16ddcfe4/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-02-26%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.17.22.png" alt="한글로 전송된 인증메일"></p>
<p>기존에는 영어로 발송되던 메일이 한글로 전송되어 오는것을 확인 할 수 있습니다.</p>
<hr>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://laravel.com/docs/9.x/notifications#customizing-the-templates">라라벨 공식 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 협업 경험에 대해]]></title>
            <link>https://velog.io/@madstone-dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%98%91%EC%97%85-%EA%B2%BD%ED%97%98%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@madstone-dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%98%91%EC%97%85-%EA%B2%BD%ED%97%98%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Fri, 25 Feb 2022 06:23:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 글은 개인적인 경험에 의해 주관적으로 작성했습니다.
<em>과제물 관련 코드는 포함되지 않습니다.</em></p>
</blockquote>
<p>최근에 참여한 학습코스를 통해 협업을 할 기회가 생겼습니다.</p>
<p>이전 직장에서는 혼자서 개발하는 업무가 많았기 때문에 협업에 대한 경험이 전무했었는데요. 이번 기회를 통해 한 단계 성장할 수 있어서 매우 기뻤습니다.</p>
<hr>
<p>글 작성 시점까지 총 두가지의 프론트엔드 구현과제를 달성했습니다.</p>
<p>첫 과제는 상품에 대한 페이지 컴포넌트들이고, 두번째 과제는 하나의 기능 컴포넌트를 구현하는 과제였습니다.</p>
<h3 id="첫-과제의-경험">첫 과제의 경험</h3>
<p>처음 과제를 받았을 때 조금은 당황스러웠습니다.</p>
<p>그 이유는 기존 직장에서의 업무량에 비해 해당 과제의 양이 충분히 혼자서도 시간내에 감당할 수 있을 만큼 상당히 적었습니다.</p>
<p>그래서 <em>&#39;과연 이 정도의 분량을 7명이서 분담하는게 가능할까? 오히려 분담을 하는게 더 어려운일이 아닐까?&#39;</em> 라는 생각들이 들었습니다.</p>
<p>게다가 협업에 대한 경험이 전무했던지라, 규칙들을 사전에 정의하고 작업에 들어가야한다는 사실을 알고있음에도 당췌 어떤 규칙을 정의해야하는지 몰라 막막했었습니다.</p>
<h4 id="부딪혀보자">부딪혀보자</h4>
<p>제가 결정한 방법은 <em>&#39;우선은 부딪혀보자&#39;</em> 였습니다.</p>
<p>우선은 구성원들에게 기존에 익숙했던 기술스택에 대해 물어보았고, 구성원들의 대다수에게 익숙한 기술 스택을 사용해 작업을 진행하기로 했습니다.</p>
<p>구성원들은 다수가 <code>Styled-components</code>에 대한 사용경험이 있었고, 모두가 <code>React</code>를 메인 스택으로 사용하고 있었습니다.</p>
<p>해당 과제는 <code>Redux</code>의 사용이 필수였는데요. 대다수가 관련 경험이 없기 때문에 저와 다른 구성원 1명이 해당 라이브러리를 사용하는 코드를 작성하기로 했습니다.</p>
<h4 id="결과">결과</h4>
<p>이상하게 생각할수도 있지만 부딪혀 얻은 결과에 대해서는 상당히 만족하고 있습니다.</p>
<p>과제의 결과물 자체는 부족한 부분도 있지만, 과제 외적으로 얻은 경험들이 너무 많아서 결과물의 부족함에 대한 아쉬움은 전혀 없습니다.</p>
<p>특히 스스로의 부족함을 진단할 수 있어서 굉장히 좋았습니다.</p>
<blockquote>
<p>개인적으로 협업에 대한 경험을 얘기할 때 코드 컨벤션이나 코드 리뷰 등 다양한 협업 관련 키워드들에 대해서는 들어봤기에, 프로젝트를 시작하면 자연스레 효율적인 프로세스로 일이 진행 될 줄 알았으나, 막상 프로젝트가 시작되니 전혀 떠오르지 않았습니다.</p>
<p>특히 처음에 컨벤션을 맞추자고 언급을 해주신 구성원이 있었음에도 불구하고, 놓치고 진행을 하게 되었습니다.</p>
<p>그런 미숙함을 가지고 만들어낸 최종 결과물치곤 제법 동작은 잘 했습니다만, 유지보수는 글쎄요... 아마 굉장히 힘들것 같습니다.</p>
</blockquote>
<br/>

<h3 id="두번째는요">두번째는요?</h3>
<p>두번째 과제는 상당히 발전한 협업 프로세스를 통해 진행되었습니다.
그리고 해당 결과물에 대한 만족도는 최상입니다!</p>
<h4 id="어떤-방법을-사용했나요">어떤 방법을 사용했나요?</h4>
<blockquote>
<p>공유 문서를 작성했습니다!</p>
</blockquote>
<p><img src="https://images.velog.io/images/madstone-dev/post/7a2addf4-d88d-4a75-a234-9110d9d956c5/301511620970323414.jpeg" alt="포스트잇이 많이 붙어있는 이미지"></p>
<p><em>미디어 컨텐츠를 통해 이런 모습으로 아이디어 회의를 진행하는 개발자들을 본적있나요?</em></p>
<br/>

<p>프로젝트 시작 직후 Notion으로 공유문서를 만들었습니다.</p>
<p>저희가 공유 문서를 만든 목적은 <strong>명세분석 및 기획</strong> 이었는데요. 처음엔 해당 문서에 양식없이 각자가 생각하는 기능에 대한 설계를 마구잡이로 적기로 했습니다.</p>
<p>마인드 맵 처럼 아이디어를 모으는 단계인거지요!</p>
<p>그 후 중복되는 아이디어를 제거하고, 각 아이디어들을 자세하게 정리하여 결과적으로 꽤 괜찮은 명세 분석 문서를 얻게 되었습니다.</p>
<blockquote>
<p>개발 환경을 통일했습니다!</p>
</blockquote>
<p>개발을 할때는 환경세팅도 매우 중요한데요.</p>
<p>저희는 공유 문서를 작성하며 개발 환경에 대한 설정도 통일했습니다.</p>
<p>이후엔 코드 작성 스타일에 대한 규칙을 작성하여 모두가 공통된 환경에서 개발을 진행 할 수 있었습니다.</p>
<p>개발환경 및 코딩 스타일로 인해 발생할지도모를 문제점을 미리 제거한거지요! :)</p>
<p>이후의 프로세스에서는 Git 브랜치를 통해 프로젝트를 관리함으로써, 최종 결과물이 나오기까지의 모든과정이 원활하게 진행되었습니다.</p>
<hr>
<p>두 번의 과제 간 주어진 시간이 많지 않았음에도 불구하고 협업 프로세스에 있어 많은 경험과 개인적인 발전을 이루었습니다.</p>
<p>현재는 진행해야할 과제가 꽤 남아있는데요.</p>
<p>이후의 프로젝트을 진행하면서도 기존에 진행했던 프로세스를 잊지않고 꾸준히 적용하고 발전시킬 계획입니다.</p>
<p>이번 협업 과제는 정말 너무너무 소중하고 값진 경험이었으며, 해당 코스 이후에도 다양한 프로젝트를 통해 협업을 할 기회가 많이 생긴다면 행복할것 같습니다. :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[신규 프로젝트 준비 - 1. 셋업]]></title>
            <link>https://velog.io/@madstone-dev/%EC%8B%A0%EA%B7%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@madstone-dev/%EC%8B%A0%EA%B7%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Sun, 13 Feb 2022 13:13:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><em>해당 글의 주제</em>
<code>Laravel</code>, <code>Inertia</code>를 활용한 <code>React</code> <code>SSR</code> 셋업</p>
</blockquote>
<h3 id="목표">목표</h3>
<ul>
<li><code>React</code>를 모놀리스 방식으로 사용하여 서버와의 연결시간을 단축시키고 싶습니다.</li>
<li>인증관련 구현에 많은 시간을 빼앗기고 싶지 않기때문에 <code>Laravel Breeze</code>의 인증 스캐폴딩을 활용하려 합니다.</li>
<li>프론트엔드는 <code>Inertia</code> with <code>React</code>로 구성하여 <code>SSR</code> <code>CSR</code>의 장점을 모두 포함하려 합니다.</li>
</ul>
<hr>
<h3 id="준비물">준비물</h3>
<ul>
<li><a href="https://www.docker.com/get-started">Docker</a> 또는 <code>PHP</code>와 <a href="https://getcomposer.org/">Comperser</a> (해당 글은 <code>Docker</code>를 활용했습니다.)</li>
</ul>
<hr>
<h3 id="본격-셋업">본격 셋업</h3>
<blockquote>
<p>해당 프로젝트는 m1칩을 사용하는 Mac에서 진행하고 있습니다.</p>
</blockquote>
<h4 id="1-laravel-설치하기">1. Laravel 설치하기</h4>
<p>라라벨 설치는 매우 쉽습니다. <a href="https://laravel.com/docs/9.x/installation">라라벨 공식 문서</a>를 참고하여 설치합니다.</p>
<p>작년 상반기까지만해도 <code>Docker</code>와 <code>m1</code>칩이 제대로 호환되지 않아 <code>brew</code>를 활용해 <code>PHP</code>를 직접 설치했던 기억이 있는데요. 해당 글을 작성하는 시점엔 <code>Docker</code>의 실리콘칩 버전이 정식으로 출시하였기 때문에 개인적으로는 <code>Docker</code>를 활용하는것이 제일 편한 세팅방법이라고 생각합니다.</p>
<br/>

<h4 id="2-laravel-breeze">2. Laravel Breeze!!</h4>
<blockquote>
<p><code>Laravel Breeze</code> 설치시 인증관련 Model, View, Controller를 모두 수정하기 때문에 새프로젝트를 구성한 직후 설치해주는게 좋습니다.</p>
</blockquote>
<p>해당 프로젝트에서 라라벨을 사용하려는 이유 중 하나가 <a href="https://laravel.com/docs/9.x/starter-kits">Laravel Breeze</a> 입니다.</p>
<p><code>Laravel Breeze</code>는 회원가입, 로그인, 비밀번호 재설정, 이메일 확인, 비밀번호 확인과 같은 인증에 필요한 기본요소를 모두 갖추고 있는데요.</p>
<p>모델, 컨트롤러 외에도 <code>React</code>,<code>Vue</code>,<code>Livewire</code>,<code>Blade</code> 등 의 템플릿으로 프론트엔드의 스캐폴딩을 구성해주기 때문에 인증관련 구현에 시간을 거의 쓰지 않아도 됩니다.</p>
<p><img src="https://images.velog.io/images/madstone-dev/post/808e3749-7dc9-4f22-8b0d-d5f59e19c3db/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-02-13%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.48.47.png" alt=""></p>
<p><code>Laravel Breeze</code>가 제공하는 깔끔한 인터페이스. 😎</p>
<br/>

<h4 id="3-inertia">3. Inertia</h4>
<p><code>Inertia</code>는 <code>SPA</code>프레임워크를 서버와 함께 모놀리스 방식으로 사용할 수 있게 해주는 라이브러리입니다.</p>
<p>서버의 라우팅 방식을 그대로 사용하기때문에 별도의 <code>API</code>가 필요없으며, 서버와 <code>SPA</code>를 통째로 배포가능합니다.</p>
<p>또한 <code>SSR</code>를 지원합니다! 👍</p>
<p><a href="https://inertiajs.com/server-side-rendering">Inertia 공식 문서</a>를 따라 SSR 설정이 가능합니다.</p>
<br/>

<p>** 하지만 !! **</p>
<p>아무래도 <code>SSR</code>에 대한 지원을 출시한지 얼마되지 않았기 때문에 현재는 <code>React</code>관련 이슈가 있습니다. </p>
<p><a href="https://github.com/inertiajs/inertia/issues/1045">이슈</a> - <code>SSR</code> 설정시 <code>React</code>가 <code>route</code>를 찾기 못합니다. 🥲</p>
<p>그러나 위 이슈는 매우 간단한 과정을 통해 해결할 수 있으므로, 아래에 해결 방법을 공유합니다.</p>
<br/>

<p>** <code>React</code> <code>SSR</code> 해결방법 **</p>
<blockquote>
<p><code>route</code>를 수동으로 추가하여 해결하는 방법입니다.</p>
</blockquote>
<p>위에서 작성한 <code>SSR</code>을 포함한 셋업 과정을 모두 마치셨다면 <code>app/Http/Middleware</code> 경로에 <code>HandleInertiaRequests.php</code> 파일이 있을텐데요.</p>
<p>해당 파일의 <code>share</code> 함수를 아래와 같이 변경 해주어야합니다.</p>
<pre><code class="language-php">
use Tightenco\Ziggy\Ziggy;

...

public function share(Request $request)
{
    $ziggy = new Ziggy($group = null, $request-&gt;url()); 
    return array_merge(parent::share($request), [
        &#39;auth&#39; =&gt; [
            &#39;user&#39; =&gt; $request-&gt;user(),
        ],
        &#39;ziggy&#39; =&gt; $ziggy-&gt;toArray()
    ]);
}
</code></pre>
<p>그 다음 <code>resources/js/ssr.js</code> 파일을 아래와 같이 변경해주세요.</p>
<pre><code class="language-jsx">import React from &quot;react&quot;;
import ReactDOMServer from &quot;react-dom/server&quot;;
import { createInertiaApp } from &quot;@inertiajs/inertia-react&quot;;
import createServer from &quot;@inertiajs/server&quot;;
import route from &quot;ziggy-js&quot;;

createServer((page) =&gt;
    createInertiaApp({
        page,
        render: ReactDOMServer.renderToString,
        resolve: (name) =&gt; require(`./Pages/${name}`),
        setup: ({ App, props }) =&gt; {
            // route를 수동으로 추가합니다.
            const Ziggy = {
                ...props.initialPage.props.ziggy,
                location: new URL(props.initialPage.props.ziggy.url),
            };
            global.route = (name, params, absolute, config = Ziggy) =&gt;
                route(name, params, absolute, config);
            return &lt;App {...props} /&gt;;
        },
    })
);</code></pre>
<p><code>mix &amp;&amp; mix --mix-config=webpack.ssr.mix.js</code> 명령어로 빌드를 새로해준 다음 <code>node public/js/ssr.js</code> 명령어로 서버를 재시작하면 해당 이슈가 해결이 된 것을 확인 할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/madstone-dev/post/56347545-fb5c-48c7-8f47-3956558d84bd/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-02-13%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%209.23.48.png" alt=""></p>
<p>자바스크립트 사용을 중지해도 렌더링 된 <code>DOM</code>를 볼 수 있습니다. 👍</p>
<hr>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://github.com/inertiajs/inertia/issues/1083">Inertia Issues</a></li>
<li><a href="https://aaronfrancis.com/2022/using-ziggy-with-inertia-server-side-rendering">Using Ziggy with Inertia Server-Side Rendering</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[슬라이딩 퍼즐 만들기]]></title>
            <link>https://velog.io/@madstone-dev/%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%94%A9-%ED%8D%BC%EC%A6%90-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@madstone-dev/%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%94%A9-%ED%8D%BC%EC%A6%90-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 08 Feb 2022 18:41:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>슬라이딩 퍼즐을 만들어보았습니다. 😀
상세 코드는 <a href="https://github.com/madstone-dev/sliding-puzzle">깃허브</a>에서 확인하실 수 있습니다.</p>
</blockquote>
<h3 id="프로젝트-소개">프로젝트 소개</h3>
<ul>
<li><a href="https://madstone-dev.github.io/sliding-puzzle">슬라이딩 퍼즐</a> 입니다.</li>
<li>이미지를 등록하여 퍼즐을 만들 수 있습니다.</li>
<li>등록한 이미지를 제거할 수 있습니다.</li>
<li>3x3, 4x4, 5x5 난이도를 선택할 수 있습니다.</li>
<li>Reset 버튼을 통해 문제를 갱신할 수 있습니다.</li>
</ul>
<hr>
<img src="https://images.velog.io/images/madstone-dev/post/542c1a20-ef5f-4d86-a028-47795a3d5c3f/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-02-09%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.49.11.png" width="700" style="margin:0 auto" />

<hr>
<h3 id="어려웠던-점">어려웠던 점</h3>
<p>처음에는 퍼즐에 사용될 요소들을 무작정 섞기만 하면 될 줄 알았습니다.</p>
<p>하지만 신나게 퍼즐을 풀다보니 도저히 풀 수 없는 녀석이 튀어나오더라구요 🥲</p>
<p>무슨 이유일까 궁금해서 검색을 해보니 슬라이딩 퍼즐에도 풀 수 있는 문제에 대한 조건이 있었습니다.</p>
<p>이 글은 해당 문제를 해결한 방법에 대해 작성했습니다.</p>
<p><a href="https://natejin.tistory.com/22">슬라이드 퍼즐 알고리즘</a> 해당 글을 보고 슬라이딩 퍼즐의 조건을 참고 했습니다.</p>
<hr>
<h3 id="변수">변수</h3>
<p>퍼즐을 풀수있는 조건을 알기 전에 각 변수에 대한 설명이 필요합니다.</p>
<h4 id="1-inversion">1. inversion</h4>
<p><code>inversion</code>은 퍼즐의 변수를 1차원 배열로 초기화 했을때 요소 중 하나인 <code>a</code>와 <code>a</code>보다 뒤에있는 <code>b</code>를 비교했을 때 <code>a&gt;b</code>가 참인 경우의 수를 말합니다.</p>
<p>즉, <code>inversion</code>을 세는 경우는 아래와 같습니다.</p>
<ul>
<li>a의 인덱스는 b의 인덱스보다 커야한다.</li>
<li>a &gt; b 가 참이다.</li>
</ul>
<pre><code class="language-jsx">const puzzle = [1,2,3,4,5,9,6,7,8,10,12,11,13,14,15];</code></pre>
<p>위의 경우 처럼 초기화 했을때</p>
<ul>
<li>9는 뒤에 나오는 6,7,8 보다 크기 때문에 <code>inversion</code>을 <code>3</code>증가시킵니다.</li>
<li>12는 뒤에 나오는 11 보다 크기 때문에 <code>inversion</code>을 <code>1</code>증가시킵니다.</li>
<li>따라서 이 경우 <code>inversion</code>은 4입니다.</li>
</ul>
<br/>

<h4 id="2-n">2. N</h4>
<p>변수 <code>N</code>은 퍼즐의 너비입니다.</p>
<p>3x3 퍼즐의 경우 <code>N</code>은 <code>3</code>입니다.
4x4 퍼즐의 경우 <code>N</code>은 <code>4</code>입니다.</p>
<br/>

<h4 id="3-frombottom">3. fromBottom</h4>
<p>변수 <code>fromBottom</code>은 빈 타일이 바닥으로부터 얼마나 떨어져있는지를 나타냅니다.</p>
<ul>
<li>퍼즐 A<img src="https://images.velog.io/images/madstone-dev/post/35b77bb9-3b02-4428-8cea-0a533918a2d6/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-02-09%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%203.12.51.png" width="400" />

</li>
</ul>
<p>퍼즐A의 경우 빈칸은 바닥으로부터 두 번째에 있습니다. 즉 <code>fromBottom</code>은 <code>2</code>입니다.
<br/></p>
<ul>
<li>퍼즐 B<img src="https://images.velog.io/images/madstone-dev/post/f254276b-b4ae-4acc-901d-0224899c63b8/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-02-09%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%203.19.56.png" width="400" />
퍼즐B의 경우 빈칸은 바닥으로부터 네 번째에 있습니다. 즉 `fromBottom`은 `4`입니다.

</li>
</ul>
<br/>

<hr>
<h3 id="조건">조건</h3>
<p>풀 수 있는 퍼즐인지 아닌지를 판단하기 위해서는 다음과 같은 조건들이 필요했습니다.</p>
<ul>
<li><code>N</code>이 홀수이면 <code>inversion</code>수가 짝수여야 한다.</li>
<li><code>N</code>이 짝수이면 다음과 같은 경우 풀수 있다.<ul>
<li><code>fromBottom</code>이 짝수이며 <code>inversion</code>은 홀수이다.</li>
<li><code>fromBottom</code>이 홀수이며 <code>inversion</code>은 짝수이다.</li>
</ul>
</li>
<li>다른 모든 경우에는 퍼즐을 풀 수 없다.<br/>

</li>
</ul>
<p>위의 조건을 달성하기 위해 아래와 같은 <code>resetPuzzle</code> 함수를 작성했습니다.</p>
<pre><code class="language-jsx">// difficulty는 난이도별 퍼즐의 배열 초기값 입니다.
const resetPuzzle = useCallback((difficulty) =&gt; {
    let ok = false;
    let randomSet;
      // 풀 수 있는 퍼즐이 나올때까지 반복합니다.
    while (!ok) {
      let inversion = 0;
      // 퍼즐을 랜덤하게 섞습니다.
      randomSet = difficulty.tiles.sort(() =&gt; Math.random() - Math.random());
      // 각 퍼즐의 순서를 따라가며 inversion을 증가시킵니다.
      randomSet.forEach((item, index) =&gt; {
        for (let i = index; i &lt; randomSet.length; i++) {
          if (item &gt; randomSet[i]) {
            inversion++;
          }
        }
      });
      // difficulti.cut은 퍼즐의 너비입니다.
      const N = difficulty.cut;
      if (N % 2 !== 0) {
          // N이 홀수일때
        if (inversion % 2 === 0) {
          // inversion이 짝수이기 때문에 풀수있다.
          ok = true;
        }
      } else {
        // N이 짝수일때
        const fromBottom =
          difficulty.cut - Math.floor(randomSet.indexOf(EMPTY_TILE) / 4);
        // fromBottom이 짝수이고 inversion이 홀수이면 풀수있다.
        if (fromBottom % 2 === 0 &amp;&amp; inversion % 2 !== 0) {
          ok = true;
        }
        // fromBottom이 홀수이고 inversion이 짝수이면 풀수있다.
        if (fromBottom % 2 !== 0 &amp;&amp; inversion % 2 === 0) {
          ok = true;
        }
      }
    }
    return randomSet;
  }, []);</code></pre>
<hr>
<p>위와 같은 코드를 통해 풀 수 있는 퍼즐만 표시 할 수 있게 되었습니다. 😀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AutoComplete 컴포넌트 구현하기]]></title>
            <link>https://velog.io/@madstone-dev/AutoComplete-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@madstone-dev/AutoComplete-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 05 Feb 2022 16:25:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 글은 AutoComplete 컴포넌트에 대해서만 작성했습니다.</p>
<p><a href="https://velog.io/@madstone-dev/Modal-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">지난 글 보러가기 - Modal 컴포넌트</a></p>
</blockquote>
<p>상세코드는 <a href="https://github.com/madstone-dev/wanted_pre_onboarding">깃허브</a> 에서 열람하실 수 있습니다.</p>
<hr>
<h3 id="autocomplete">AutoComplete</h3>
<p><a href="https://www.w3.org/TR/wai-aria-practices-1.1/examples/combobox/aria1.0pattern/combobox-autocomplete-list.html">W3C - Combobox With List Autocomplete Example</a> 위 예시를 참고하여 자동완성을 구현하기 위한 사항들을 아래와 같이 정리했습니다.</p>
<ul>
<li>비어있는 input에서 아래 방향키로 리스트를 열 수 있어야 한다.</li>
<li>비어있는 input에서 아래 방향키를 눌렀을시 리스트의 첫아이템으로 포커스가 이동해야 한다.</li>
<li>상하 방향키로 리스트를 탐색할 수 있어야 한다.</li>
<li>Escape 키를 눌렀을시 input의 내용이 삭제되고 리스트가 닫혀야 한다.</li>
</ul>
<br/>
해당 조건들을 구현하기 위해 아래와 같은 방법을 사용했습니다.

<h4 id="1-키보드-이벤트-할당하기">1. 키보드 이벤트 할당하기</h4>
<pre><code class="language-jsx">const inputRef = useRef();
const liRefs = useRef([]);

const focusOnInput = () =&gt; {
  if (inputRef.current) {
    inputRef.current.focus();
  }
};

const onListClick = (item) =&gt; {
  setKeyword(item);
  setOpen(false);
  focusOnInput();
};

const ARROW_DOWN = &quot;ArrowDown&quot;;
const ARROW_UP = &quot;ArrowUp&quot;;
const ESCAPE = &quot;Escape&quot;;

const onInputkeyDown = (event) =&gt; {
  if (event.key === ARROW_DOWN) {
    setOpen(true);
    const first = liRefs.current[0];
    if (first) {
      first.focus();
    }
  }
  if (event.key === ESCAPE) {
    setOpen(false);
  }
};

const onListKeyDown = (event, index) =&gt; {
  const next = liRefs.current[index + 1];
  const prev = liRefs.current[index - 1];
  const first = liRefs.current[0];
  const last = liRefs.current[liRefs.current.length - 1];
  if (event.key === ARROW_DOWN) {
    event.preventDefault();
    if (next) {
      next.focus();
    } else {
      first &amp;&amp; first.focus();
    }
  }
  if (event.key === ARROW_UP) {
    event.preventDefault();
    if (prev) {
      prev.focus();
    } else {
      last &amp;&amp; last.focus();
    }
  }
  if (event.key === ESCAPE) {
    setKeyword(&quot;&quot;);
    if (inputRef.current) {
      focusOnInput();
    }
    setOpen(false);
  }
};</code></pre>
<p><code>input</code> 요소와 <code>ul</code>요소에 스크린리더에서 필요한 <code>aira</code> 속성들을 작성해주었습니다.
<code>input</code> 요소의 경우 <code>autocomplete</code> 속성은 <code>aria</code>에 해당하는 속성은 아니지만 브라우저가 기본으로 제공하는 자동완성 기능과 혼동될 수 있기에 <code>off</code>로 설정해주었습니다.</p>
<pre><code class="language-jsx">&lt;form className=&quot;relative&quot; onSubmit={onSubmit} ref={formRef}&gt;
  &lt;input
    ref={inputRef}
    type=&quot;search&quot;
    className=&quot;block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm&quot;
    placeholder={placeholder}
    value={keyword}
    onChange={keywordChange}
    onFocus={inputFocus}
    onKeyDown={onInputkeyDown}
    role=&quot;combobox&quot;
    aria-autocomplete=&quot;list&quot;
    aria-expanded={list.length &gt; 0 &amp;&amp; open ? &quot;true&quot; : &quot;false&quot;}
    autoComplete=&quot;off&quot;
    /&gt;

  {list.length &gt; 0 &amp;&amp; open &amp;&amp; (
    &lt;ul
      ref={ulRef}
      role=&quot;list&quot;
      className={`${
      absolute ? &quot;absolute&quot; : &quot;static&quot;
                } origin-top-left left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none max-h-80 overflow-y-auto`}
      &gt;
      {list.map((item, index) =&gt; (
        &lt;li key={index} role=&quot;option&quot; aria-selected={keyword === item}&gt;
          &lt;button
            ref={(el) =&gt; (liRefs.current[index] = el)}
            type=&quot;button&quot;
            className=&quot;block w-full px-4 py-2 text-sm text-left text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-50 focus:outline-none&quot;
            onClick={() =&gt; {
              onListClick(item);
            }}
            onKeyDown={(event) =&gt; {
              onListKeyDown(event, index);
            }}
            &gt;
            {item}
          &lt;/button&gt;
        &lt;/li&gt;
      ))}
    &lt;/ul&gt;
  )}
&lt;/form&gt;</code></pre>
<p>리스트는 포커스가 가능하도록 <code>button</code>요소를 <code>li</code>태그 내에 작성했습니다.
<code>button</code> 요소를 사용하는대신 <code>tabindex</code> 속성을 <code>li</code>요소에 작성할수도 있습니다.
<br/></p>
<p>각 <code>button</code> 요소는 <code>keyword</code>가 변경될 시 <code>liRefs.current</code>를 빈 배열로 초기화 시켜준 후 레퍼런스를 동적으로 할당시켜주었습니다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
  liRefs.current = [];
  if (!keyword.trim()) {
    setList(data);
    return;
  }
  const newList = data.filter(
    (item) =&gt; item.toLowerCase().indexOf(keyword.toLowerCase()) &gt;= 0
  );
  setList(newList);
}, [keyword, data]);

&lt;button ref={(el) =&gt; (liRefs.current[index] = el)} //...생략 /&gt;</code></pre>
<p>위 코드들을 통해 <code>input</code>과 <code>li</code> 요소 간 키보드를 사용한 접근이 가능해졌습니다.
<br/></p>
<h4 id="2-포커스-아웃-감지하기">2. 포커스 아웃 감지하기</h4>
<pre><code class="language-jsx">const formRef = useRef();
const ulRef = useRef();

const trackFocus = useCallback(
    (event) =&gt; {
      if (event.target.offsetParent !== formRef.current) {
        setOpen(false);
      }
    },
    [setOpen, formRef]
  );

const trackKey = useCallback(() =&gt; {
  setTimeout(() =&gt; {
    if (
      document.activeElement.offsetParent !== formRef.current &amp;&amp;
      document.activeElement.offsetParent !== ulRef.current
    ) {
      setOpen(false);
    }
  });
}, [setOpen, ulRef]);

useEffect(() =&gt; {
  document.addEventListener(&quot;click&quot;, trackFocus);
  document.addEventListener(&quot;keydown&quot;, trackKey);
  return () =&gt; {
    document.removeEventListener(&quot;click&quot;, trackFocus);
    document.removeEventListener(&quot;keydown&quot;, trackKey);
  };
}, [trackFocus, trackKey]);</code></pre>
<p><code>trackFocus</code>와 <code>trackKey</code> 함수를 작성했습니다.
전자의 경우 마우스 사용자를 위해 작성된 함수로써 전체요소를 감싸는<code>form</code> 요소 외부에 포커스가 위치할시 리스트를 닫아줍니다.</p>
<p>후자의 경우 키보드 사용자를 위해 작성된 전자와 같은 동작을하는 함수입니다.
해당 함수에서 <code>setTimeout</code>을 사용하는 이유는 <code>keydown</code> 이벤트의 동작이 <code>document.activeElement</code>를 찾는 동작보다 더 앞서 발생하기 때문에 비동기적으로 처리해주어야 <code>keydown</code> 이벤트의 동작이 끝난 후 현재 포커스된 요소를 찾아줍니다.</p>
<hr>
<p>위와 같은 방법을 통해 키보드 사용자와 마우스 사용자 모두 불편함 없이 자동완성 컴포넌트를 사용할수있게 되었습니다. 😄</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Modal 컴포넌트 구현하기]]></title>
            <link>https://velog.io/@madstone-dev/Modal-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@madstone-dev/Modal-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 04 Feb 2022 16:07:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원티드 프론트엔드 프리온보딩 코스(취업연계)에 지원했습니다.
구현 과제를 달성하며 해결하려 했던 문제들에 대해 이야기를 하고자합니다.</p>
<p>해당 글은 Modal 컴포넌트에 대해서만 작성했습니다.</p>
</blockquote>
<h3 id="구현-과제">구현 과제</h3>
<ul>
<li>Toggle</li>
<li><strong>Modal</strong> (해당 글의 주제)</li>
<li>Tab</li>
<li>Tag</li>
<li><a href="https://velog.io/@madstone-dev/AutoComplete-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">AutoComplete</a></li>
<li>ClickToEdit</li>
</ul>
<hr>
<p>가이드라인에서는 위 컴포넌트 중 2가지 이상만 구현해도 좋다고했으나, 그 이상을 구현할 경우 가산점이 붙는다고하여 모두 제작하게 되었습니다.</p>
<p>상세코드는 <a href="https://github.com/madstone-dev/wanted_pre_onboarding">깃허브</a> 에서 열람하실 수 있습니다.</p>
<hr>
<blockquote>
<p>구현범위가 어디까지인가요?</p>
</blockquote>
<p>구현 과제 가이드라인에서는 예시 GIF를 제공해줬지만 GIF만으로는 정확한 구현 범위를 알기가 힘들었습니다.
하여, 기존에 자주 사용하던 라이브러리를 참고하여 최대한 웹접근성을 지키는 방향으로 충실히 구현하고자 했습니다.</p>
<h3 id="modal">Modal</h3>
<p><a href="https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html">W3C - Modal Dialog Example</a> 위 예시를 참고하여 모달을 구현하기 위한 사항들을 아래와 같이 정리했습니다.</p>
<ul>
<li>대화 상자가 화면의 100%를 채워야한다.</li>
<li>일부 모바일 장치에서 발생하는 배경 움직임을 숨겨야한다.</li>
<li>포커스는 대화 상자내에서만 이동해야한다.</li>
<li>대화 상자를 숨기면 기존에 대화 상자를 호출했던 버튼으로 포커스가 이동해야한다.</li>
<li>Escape 키를 통해 대화 상자를 닫아야한다.</li>
</ul>
<br/>
위 목록 중 특히 집중했던 사항은 대화 상자내에서만 이동하는 포커스였는데요.

<p>아래에서 설명하는 방법들을 통해 해당 사항을 구현했습니다.</p>
<h4 id="1-포커스-가능한-요소들-정하기">1. 포커스 가능한 요소들 정하기</h4>
<pre><code class="language-jsx">const [prevActiveEl, setPrevActiveEl] = useState();
const closeBtn = useRef();
const [lastFocusableEl, setLastFocusableEl] = useState();
const contentRef = useRef();

const setLastFocus = useCallback(() =&gt; {
  const focusableEls = [
    ...contentRef.current.querySelectorAll(
      &#39;a[href], button, input, textarea, select, details, [tabindex]:not([tabindex=&quot;-1&quot;])&#39;
    ),
  ].filter(
    (el) =&gt; !el.hasAttribute(&quot;disabled&quot;) &amp;&amp; !el.getAttribute(&quot;aria-hidden&quot;)
  );
  const lastEl = focusableEls[focusableEls.length - 1];
  if (lastEl) {
    setLastFocusableEl(lastEl);
  }
}, [contentRef]);

useEffect(() =&gt; {
  if (open &amp;&amp; closeBtn.current) {
    setPrevActiveEl(document.activeElement);
    closeBtn.current.focus();
  }
  if (open &amp;&amp; contentRef.current) {
    setLastFocus();
  }
}, [open, contentRef]);</code></pre>
<p>제가 만든 모달은 <code>useState</code>를 사용하는 변수 <code>open</code>을 통해 열리고 닫히는데요. <code>open</code>이 <code>false</code>일 때 모달요소가 렌더링 되는것을 방지하기 위해 <code>open</code>이 <code>true</code>인 경우에만 <code>Modal 컴포넌트</code>를 렌더링하도록 했습니다.</p>
<p>때문에 <code>open</code>이 <code>false</code>일 때는 <code>contentRef.current</code>가 존재하지 않습니다.</p>
<br/>

<p><code>contentRef</code>는 모달내 콘텐츠를 감싸는 요소인데요.</p>
<pre><code class="language-jsx">&lt;div ref={contentRef}&gt;{children}&lt;/div&gt;</code></pre>
<p><code>open</code>이 <code>true</code>일 때 <code>setLastFocus</code>를 통해 포커스가 가능한 모든 요소를 탐색하고 해당 요소 중 마지막 요소를 <code>lastFocusableEl</code>에 저장해주었습니다.</p>
<p>그리고 <code>prevActiveEl</code>에 모달이 열리기전 마지막으로 접근했던 요소를 저장해주고 <code>closeBtn.current</code>로 포커스를 이동시켰습니다.</p>
<br/>

<p><code>closeBtn.current</code>는 닫기 버튼인데요. 모달이 열린 후 바로 닫기 버튼으로 포커스를 이동시키는는 이유는 키보드를 사용하는 사용자가 오접근하게될시 닫기버튼으로 빠르게 이동시켜주는 것이 중요하기 때문입니다.</p>
<br/>


<pre><code class="language-jsx">if (prevActiveEl) {
  prevActiveEl.focus();
}</code></pre>
<p>모달이 닫힐 때는 <code>prevActiveEl</code>을 활용하여 처음 모달을 호출했던 버튼으로 포커스를 이동시켜주었습니다.</p>
<h4 id="2-포커스트랩-배치하기">2. 포커스트랩 배치하기</h4>
<pre><code class="language-jsx">const focusTrapHead = useRef();
const focusTrapFoot = useRef();

const focusLastEl = useCallback(
  (event) =&gt; {
    if (event.target === focusTrapHead.current) {
      if (lastFocusableEl) {
        lastFocusableEl.focus();
      } else {
        closeBtn.current.focus();
      }
    }
  },
  [focusTrapHead, lastFocusableEl]
);

const focusFirstEl = useCallback(
  (event) =&gt; {
    if (event.target === focusTrapFoot.current) {
      closeBtn.current.focus();
    }
  },
  [focusTrapFoot]
);

useEffect(() =&gt; {
  const focusHead = focusTrapHead.current;
  const focusFoot = focusTrapFoot.current;
  if (focusHead) {
    focusHead.addEventListener(&quot;focusin&quot;, focusLastEl);
  }
  if (focusFoot) {
    focusFoot.addEventListener(&quot;focusin&quot;, focusFirstEl);
  }
  return () =&gt; {
    if (focusFoot) {
      focusFoot.removeEventListener(&quot;focusin&quot;, focusLastEl);
    }
    if (focusHead) {
      focusHead.removeEventListener(&quot;focusin&quot;, focusFirstEl);
    }
  };
}, [focusTrapHead, focusTrapFoot, open, focusFirstEl, focusLastEl]);
</code></pre>
<p>모달내에서 포커스가 빠져나가지 못하게하려면 포커스트랩의 배치도 중요합니다.</p>
<p>저는 <code>focusTrapHead</code>와 <code>focusTrapFoot</code> 두개를 만들어 각각 다이얼로그 요소내의 최상단과 최하단에 위치시켰습니다.</p>
<p><code>Tab</code>키를 사용해 포커스 이동시 순차적으로 이동하기 때문에 최상단과 최하단에 배치하는것이 좋다고 생각했습니다.</p>
<pre><code class="language-jsx">&lt;div
  ref={focusTrapHead}
  tabIndex={0}
  className=&quot;fixed bg-transparent -top-10&quot;
  style={focusTrapStyle}
  /&gt;
&lt;button
  ref={closeBtn}
  type=&quot;button&quot;
  className=&quot;rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500&quot;
  onClick={() =&gt; setOpen(false)}
  &gt;

  // ...생략

  &lt;div ref={contentRef}&gt;{children}&lt;/div&gt;
  &lt;div
    ref={focusTrapFoot}
    tabIndex={0}
    className=&quot;fixed bg-transparent -top-10&quot;
    style={focusTrapStyle}
    /&gt;</code></pre>
<p><code>focusTrapHead</code>와 <code>focusTrapFoot</code> 두 요소 모두 <code>tabIndex</code>를 <code>0</code>으로 두어 포커스가 가능합니다.</p>
<p>각 트랩으로 이동시 <code>focusTrapHead</code>는 마지막요소 또는 닫기 버튼으로 이동시켜주고 <code>focusTrapFoot</code>은 최상단트랩의 다음 요소인 닫기 버튼으로 이동시켜주어 포커스가 모달의 외부로 빠져나가지 않게 도와줍니다.</p>
<hr>
<p>위와 같은 방법을 통해 모달 내의 포커스를 유지 할 수 있었으며, 키보드를 사용하는 유저들도 불편함 없이 모달 컴포넌트를 사용할수있게 되었습니다. 😃</p>
]]></description>
        </item>
    </channel>
</rss>