<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>tech-hoon.devlog</title>
        <link>https://velog.io/</link>
        <description>제 삽질을 공유합니다.</description>
        <lastBuildDate>Sat, 02 Apr 2022 14:24:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. tech-hoon.devlog. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/tech-hoon" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[UI/UX] 모바일 디바이스  UI/UX 최적화]]></title>
            <link>https://velog.io/@tech-hoon/%EB%AA%A8%EB%B0%94%EC%9D%BC-%EB%94%94%EB%B0%94%EC%9D%B4%EC%8A%A4-UIUX-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@tech-hoon/%EB%AA%A8%EB%B0%94%EC%9D%BC-%EB%94%94%EB%B0%94%EC%9D%B4%EC%8A%A4-UIUX-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sat, 02 Apr 2022 14:24:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>PC와 태블릿, iOS, android 그리고 PWA 등 다양한 디바이스 환경에서 균일한 UI와 앱처럼 편리한 UX를 제공하기 위해 적용한 것들을 공유한다.</p>
</blockquote>
<h2 id="viewport-설정">viewport 설정</h2>
<p>viewport란 웹페이지가 사용자에게 보여지는 영역을 말한다. 유저마다 다양한 크기의 디바이스에서 접속하다 보니, 그에 맞는 viewport를 지정해주어야 한다. html5 기본 snippet으로도 제공하는 meta 태그이다.</p>
<pre><code class="language-html">&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no&quot;/&gt;</code></pre>
<ul>
<li><code>meta name=”viewport”</code>: viewport 선언</li>
<li><code>content=”width=device-width”</code>: 콘텐츠를 표현할 넓이를 디바이스 크기에 맞춘다</li>
<li><code>initial-scale=1</code>: 초기 크기 설정 (기본 꽉찬 화면)</li>
<li><code>minimum-scale=1</code>: 최소 크기 설정 (기본값: 0.25, 범위: 0~10.0)</li>
<li><code>maximum-scale=1</code>: 최대 크기 설정 (기본값: 0, 범위: 0~10.0)</li>
<li><code>user-scalable=no</code>: 사용자 단말의 확대기능 사용 유무 선언 (yes/no)</li>
</ul>
<h2 id="y축-스크롤-고정">y축 스크롤 고정</h2>
<p>이미지 캐러셀이 아닌 경우, 보통 y축으로만 스크롤 하기 때문에 x축은 화면의 크기를 넘어갈 경우가 거의 없다. </p>
<pre><code class="language-css">body {
    touch-action: pan-y
}</code></pre>
<ul>
<li><code>touch-action</code> : 어떤 요소 내에서 브라우저가 터치할 액션의 목록을 지정한다.</li>
<li><code>pan-y</code>: 특정 객체를 터치한 후, 수직 혹은 수평 방향으로만 스크롤의 범위를 제한한다. 이를 적용할 시, y축으로만 스크롤을 제한하여 세로로 스크롤 중에, x축으로 화면이 이동하는 것을 방지할 수 있다.</li>
</ul>
<p><img src="https://wit.nts-corp.com/wp-content/uploads/2021/07/01.gif" alt=""></p>
<p><img src="https://wit.nts-corp.com/wp-content/uploads/2021/07/02.gif" alt=""></p>
<p>참고: <a href="https://wit.nts-corp.com/2021/07/16/6397">https://wit.nts-corp.com/2021/07/16/6397</a></p>
<h2 id="상태표시줄-색상-지정">상태표시줄 색상 지정</h2>
<p><code>meta name=&#39;theme-color&#39;</code> : 상태표시줄 색상을 지정하는 <code>meta</code>태그 중 한 종류이다.</p>
<pre><code class="language-html">&lt;meta name=&#39;theme-color&#39; content=&#39;YOUR_COLOR&#39;/&gt;</code></pre>
<p><img src="https://velog.velcdn.com/cloudflare/tech-hoon/4149db96-a93e-42c9-8c44-76f804be7ead/Untitled.png" alt=""></p>
<p>모달창을 띄울때 상태표시줄 색상만 튀기 때문에, 모달시 dim된 색상으로 meta 태그를 바꿔주도록 했다. (react-helmet 이용)</p>
<pre><code class="language-html">&lt;meta name=&#39;theme-color&#39; content={modalOpened ? &#39;#7C7C7C&#39; : &#39;THEME_COLOR&#39;} /&gt;</code></pre>
<h2 id="ios-safari-주소-표시줄-색상-지정">iOS safari 주소 표시줄 색상 지정</h2>
<p>일반 상황에서는 앞선 <code>theme-color</code>로 지정한 색상이 적용되지만, <code>PWA</code>에서 full-screen를 적용하기 위해 <code>manifest</code> full-screen으로 설정한 경우, 화면이 상태표시줄까지 꽉찬 사이즈로 적용되고, 이 때 상태표시줄의 색상을 지정하는 iOS 전용 <code>meta</code>태그이다.</p>
<p><strong>manifest.json</strong></p>
<pre><code class="language-js">{
    &quot;name&quot;: &quot;YOUR_APP&quot;,

            ...

    &quot;display&quot;: &quot;full-screen&quot;
}</code></pre>
<p><strong>index.html</strong></p>
<pre><code class="language-html">&lt;meta name=&quot;apple-mobile-web-app-status-bar-style&quot; content=&quot;#ffffff&quot;&gt;</code></pre>
<h2 id="더블클릭터치시-글자-영역-선택-방지">더블클릭/터치시 글자 영역 선택 방지</h2>
<p>PC에서 특정 element에 더블클릭을 하거나, 더블터치를 하게 되면, 다음과 같이 block이 생기는 걸 볼 수 있다. 이는 텍스트에서는 유용하지만, 연속적인 터치가 필요하거나 이미지인 경우 몰입감을 저해할 수 있어 보인다. </p>
<p>이는 <code>user-select: none</code>으로 간단하게 처리할 수 있다.</p>
<pre><code class="language-css">.your_img {
    user-select:none
}</code></pre>
<p><img src="https://velog.velcdn.com/cloudflare/tech-hoon/6843e97f-2db6-434a-b884-cdde6925df8e/Untitled%20(1).png" alt=""></p>
<h2 id="ios-safari-x-100vh">iOS safari X 100vh</h2>
<p>PC나 안드로이드 환경에서는 height에 100vh를 적용할 시, 화면 뷰포트에 꽉차게 적용된다. 하지만 iOS safari 특성상 상단바와 하단바까지 포함한 값으로, 상단바나 하단바가 켜져있을 경우 정확한 height를 나타낼 수 없다. </p>
<p>hook을 만들어, 화면 크기가 resize될 때마다 변경된 화면 높이의 크기 맞게 vh를 설정해주었다. 잦은 resize로 성능에 문제가 생길 수 있기 때문에 debounce 처리를 해주었다.</p>
<pre><code class="language-tsx">import { debounce } from &#39;lodash&#39;;
import { useEffect } from &#39;react&#39;;

const useScreenHeightResize = () =&gt; {
  const handleResize = debounce(() =&gt; {
    const vh = window.innerHeight * 0.01;
    document.documentElement.style.setProperty(&#39;--vh&#39;, `${vh}px`);
  }, 500);

  useEffect(() =&gt; {
    handleResize();
    window.addEventListener(&#39;resize&#39;, handleResize);
    return () =&gt; window.removeEventListener(&#39;resize&#39;, handleResize);
  }, []);
};

export default useScreenHeightResize;</code></pre>
<p>이를 지원하지 않는 브라우저에서는 100vh를 적용하고, 지원하는 브라우저에서는 받아온 <code>--vh</code>로 height를 설정해준다.</p>
<pre><code class="language-scss">.container {
    height: 100vh; // fallback
    height: calc(var(--vh) * 100);
}</code></pre>
<p>참고: <a href="https://velog.io/@edie_ko/Tip-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%97%90%EC%84%9C-100vh-%EC%A0%81%EC%9A%A9-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-iosandroid">https://velog.io/@edie_ko/Tip-모바일-브라우저에서-100vh-적용-오류-해결-iosandroid</a></p>
<h2 id="ios-select-태그-style-reset">iOS select 태그 style reset</h2>
<p>iOS에서 <code>select</code> 태그의 기본 설정이 적용되어 있어서, 스타일링을 해도 적용되지가 않는다. 다음과 같이 기본 <code>select</code> 태그의 기본 설정을 제거해 주어야 한다. </p>
<p><strong>GloabalStyles.tsx</strong></p>
<pre><code class="language-css">select {
    -webkit-appearance: none;
  -moz-appearance: none; 
  appearance: none;
}</code></pre>
<p><img src="https://velog.velcdn.com/cloudflare/tech-hoon/ee264b3d-f9fe-4476-aa34-7118f27bc9c9/Untitled%20(2).png" alt=""></p>
<p>참고: <a href="https://m.blog.naver.com/sudoku1/221413644751">https://m.blog.naver.com/sudoku1/221413644751</a></p>
<h2 id="ios에서-터치-시-생기는-테두리-영역-제거">iOS에서 터치 시 생기는 테두리 영역 제거</h2>
<p><img src="https://velog.velcdn.com/cloudflare/tech-hoon/5132b23b-df40-431e-b97b-aa8431ecc28c/Untitled%20(3).png" alt=""></p>
<p>iOS에서 element를 터치할 시, 터치한 element의 영역에 회색 테두리가 생긴다. 이는 터치한 영역을 사용자에게 알려줄 수 있는 장점이 있긴 하지만, 필자는 앱처럼 자연스럽게 보이는 걸 목표로 하기 때문에 이를 제거해주었다.</p>
<pre><code class="language-css">* { 
    -webkit-tap-highlight-color:transparent;
}</code></pre>
<h2 id="모달창-열려있을-때-스크롤-방지">모달창 열려있을 때 스크롤 방지</h2>
<p>모달창이 열려있을 때, 외부로 스크롤이 되는 것을 방지하기 위해, 다음과 같이 modal_opened 클래스를 동적으로 추가하고 삭제해주도록 작성한다.</p>
<pre><code class="language-css">body {
  overflow: auto; 
}

body.modal_opened {
  overflow: hidden; // 스크롤 방지
  touch-action: none; // 확대, 축소 비활성화
}</code></pre>
<h2 id="이미지-드래그-방지ghost-drag">이미지 드래그 방지(Ghost Drag)</h2>
<p>이미지 위에서 드래그 및 터치를 할 시 드래그 잔상이 생긴다. 이는 특히 이미지 슬라이드에서 불편한 UX를 제공한다. 다음과 같이 간단하게 개선할 수 있다.</p>
<pre><code class="language-css">.your_img {
    -webkit-user-drag: none;
  -khtml-user-drag: none;
  -moz-user-drag: none;
  -o-user-drag: none;
  user-drag: none;
}</code></pre>
<h2 id="모바일-hover-css로-pc-모바일-구분하기">모바일 hover (CSS로 PC, 모바일 구분하기)</h2>
<p><code>hover</code> 이벤트는 PC 환경에서만 발동해야 한다(정확히는 마우스). 하지만 hover 이벤트가 없는 모바일 환경에서도터치를 하면 <code>hover</code>에 적용된 속성이 발동한다.  이는 다음과 같이 적용하면, CSS에서 PC와 모바일을 구분할 수 있고, 따라서 괄호 안에 포함되는 스타일은 모바일 환경에서만 적용되는 스타일로 생각하면 된다.</p>
<pre><code class="language-css">@media (hover: hover) and (pointer: fine) {
    button:hover {
      transform: scale(150%);
    }
  }</code></pre>
<ul>
<li><code>hover:hover</code> : 기본 포인팅 장치가 특정 엘리먼트 위로 쉽게 hover할 수 있는 경우를 나타낸다.</li>
<li><code>pointer:fine</code> : 마우스와 같은 포인팅 장치의 존재 여부를 판별하는데 사용된다.</li>
</ul>
<p>참고: <a href="https://paperblock.tistory.com/164">https://paperblock.tistory.com/164</a></p>
<h2 id="느낀점">느낀점</h2>
<p>프로덕트에 애정을 가지고 개발하고, 사용자 레벨에서 사용하다 보면 사소한 것들도 눈에 띄곤 한다. 그러면서 눈에 띄는 것들을 하나하나 개선하게 된 것 같다.</p>
<p>특히 PC에서 개발자 도구로 디버깅 하다보면, 실제 모바일 환경과 다소 다른 점이 발견되어 적잖이 당황했다. </p>
<p>그 이후로는 사용하는 아이폰과 안드로이드 직접 연결하여 디버깅하는 것이 프로덕트의 UI와 UX를 향상하는 데에 많이 도움이 된 것 같다.</p>
<p><del>그리고 iOS는 너무 까다롭다</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Firebase] firestore trigger + slack 메시징 자동화 적용기]]></title>
            <link>https://velog.io/@tech-hoon/firestore-trigger-slack</link>
            <guid>https://velog.io/@tech-hoon/firestore-trigger-slack</guid>
            <pubDate>Thu, 17 Mar 2022 03:10:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>firestore에 특정 데이터가 등록될 시, slack으로 메시지가 전송되도록 자동화해보자</p>
</blockquote>
<h2 id="배경">배경</h2>
<p>기숙사생만 이용하는 커뮤니티 특성상, 기숙사생임을 인증하는 절차가 존재한다. 수많은 가입 인증 처리를 관리하기 위한 프로세스가 필요했다. 프로세스는 다음과 같다.</p>
<ol>
<li>사용자가 회원가입 인증용 이미지(이름표 사진)를 업로드한다. </li>
<li>업로드한 이미지와 사용자 정보를 admin slack 계정으로 메시지를 보낸다.</li>
<li>관리자 slack으로 전송된 사용자의 이미지와 사용자 정보가 일치함을 확인 후 회원가입을 승인한다.</li>
</ol>
<h2 id="slack-webhook">Slack Webhook</h2>
<p>먼저 slack webhook을 등록하고, webhook url을 복사한다 (<a href="https://jojoldu.tistory.com/552">참고</a>)</p>
<p>slack url이 외부로 노출되는 걸 방지하기 위해, <code>firebase functions:config:set</code>을 통해 firebase에 url을 등록한다.</p>
<pre><code class="language-bash">firebase functions:config:set slack.url=&quot;https://hooks.slack.com/services/[YOUR_ID]&quot;</code></pre>
<p>@slack/client npm 패키지를 설치한다.</p>
<pre><code class="language-bash">npm install @slack/client</code></pre>
<p>설치한 패키지를 불러오고, SlackWebhook을 선언한다.</p>
<pre><code class="language-jsx">// functions/src/index.ts

import SlackWebhook = require(&#39;@slack/client&#39;);
const IncomingWebhook = SlackWebhook.IncomingWebhook;
const config = functions.config();
const slackUrl = config.slack.url;
const Slack = new IncomingWebhook(slackUrl);

            ...</code></pre>
<h2 id="firestore-functions">firestore-functions</h2>
<p><code>users</code> 컬렉션에 새로운 user 데이터가 생성될 때, <code>firestore-functions</code>의 <code>onCreate</code> 내부에 있는 동작이 트리거된다. <code>Slack.send</code>를 통해 원하는 메시지를 전송하도록 하였다.</p>
<pre><code class="language-jsx">// functions/src/index.ts

        ...

export const userCreated = functions
  .region(&#39;asia-northeast3&#39;)
  .firestore.document(&#39;users/{userId}&#39;)
  .onCreate((snapshot) =&gt; {
    const { email, name, resident_auth_image, uid } = snapshot.data();

    Slack.send({
      blocks: [
        {
          type: &#39;section&#39;,
          text: {
            type: &#39;mrkdwn&#39;,
            text: &#39;새로운 사용자가 등록되었습니다.&#39;,
          },
        },
        {
          type: &#39;section&#39;,
          text: {
            type: &#39;mrkdwn&#39;,
            text: `*이름:*\n${name}\n*아이디:*\n${uid} ... 사진*:${resident_auth_image}\n`,
          },
                            ...
                }
            ]
        });
});</code></pre>
<p>터미널에 다음 코드를 실행하여, 작성한 코드를 firebase에 배포한다.</p>
<pre><code class="language-bash">firebase deploy --only &quot;functions:userCreated&quot;</code></pre>
<h2 id="데모">데모</h2>
<p><img src="https://user-images.githubusercontent.com/19265753/164648176-0fd5292b-beed-4562-95a0-794bcda2318e.gif" alt="firebase-slack"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Recoil] selector를 이용한 API 캐싱(feat. useRecoilRefresher)]]></title>
            <link>https://velog.io/@tech-hoon/recoil-selector-api-caching</link>
            <guid>https://velog.io/@tech-hoon/recoil-selector-api-caching</guid>
            <pubDate>Thu, 17 Mar 2022 02:21:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>recoil의 selector를 이용하여 동일한 API의 요청에 대해서는 값을 캐싱을 하여, 불필요한 API 요청을 줄여보자.</p>
<h2 id="selector">selector</h2>
<p><code>selector</code>란 구독하고 있는 <code>atom</code>에 변화가 생길 때마다 새로운 값을 리턴해주는 순수 함수이다. 즉, <code>get</code>을 통해 가져온 값은 의존성을 갖고 있어, 구독하는 값이 변할 때마다 새로운 값을 갱신한다.</p>
<p>이를 단순히 <code>get</code>에 <code>async</code>와 <code>await</code>을 추가함으로서 비동기를 처리할 수 있는데, 여기서 중요한 점은 위와 반대로 구독하는 <code>atom</code> 값이 변하지 않을 경우, 동일한 값을 리턴한다는 점이다. 즉, 파라미터가 동일하다면, 캐시된 값을 리턴한다.</p>
<pre><code class="language-tsx">export const mySelector = selector({
  key: &#39;my_selector&#39;,
  get: async ({ get }) =&gt; {
    const user = get(userState);
    const posts = await getMyPosts(user.id);
    return posts;
  },
});</code></pre>
<h2 id="캐시-값-갱신">캐시 값 갱신</h2>
<p>그렇다면 구독하는 <code>atom</code>과는 별개로, <code>selector</code>의 값을 필연적으로 갱신해줘야 할 경우에는 어떻게 해야 할까?</p>
<h3 id="version-parameter">Version Parameter</h3>
<p>가장 단순한 방법으로는, <code>Date</code> time stamp를 파라미터로 넣어서 의존성을 주입시키는 것이다. 참고로<code>selectorFamily</code>를 사용해서 <code>selector</code>에 원하는 파라미터를 넣을 수 있다. 마찬가지로 이 파라미터도 의존성을 갖기 때문에, 파라미터 값이 달라지면 그 값으로 갱신된다.</p>
<p>하지만 이 방식대로 하면, 매 API 요청 마다 새로운 API 요청으로 인식될텐데, 이는 캐싱을 사용하지 않던 기존 방식과 큰 차이가 없을 것이다.</p>
<pre><code class="language-tsx">// selector 정의부
export const productAsyncState = selectorFamily({
  key: &#39;productAsyncState&#39;,
  get:
    ({ ver }) =&gt;
    async ({ get }) =&gt; {
      const idx = get(productIdxState);
      return await getProductDetail(idx, ver);
    },
});</code></pre>
<pre><code class="language-tsx">// 호출부
const date = useRecoilValue(productAsyncState({ ver: Date.now() }));</code></pre>
<h3 id="userecoilrefresher">useRecoilRefresher</h3>
<p>따라서, 원하는 시기에 캐시를 강제로 갱신시켜주는 방법이 필요한데, <code>recoil</code> 0.50 버전부터 <code>useRecoilRefresher</code>라는 기능이 unstable 딱지를 달고 릴리즈되었다. </p>
<p>캐시를 갱신해줘야하는 경우에 이 함수를 호출하면, 기존 캐시를 제거하고 <code>atom</code>에 변화가 생기지 않더라도 새로운 값으로 갱신한다.</p>
<p>데이터 갱신이 필요한 곳 마다 사용할 것이기 때문에 <code>hook</code>으로 만들어주었다.</p>
<pre><code class="language-tsx">import { RecoilValue, useRecoilRefresher_UNSTABLE } from &#39;recoil&#39;;

const useRecoilCacheRefresh = (state: RecoilValue&lt;any&gt;) =&gt; {
  const refresher = useRecoilRefresher_UNSTABLE(state);
  return refresher;
};

export default useRecoilCacheRefresh;</code></pre>
<pre><code class="language-tsx">const myPostsCacheRefresher = useRecoilCacheRefresh(myPostsState);</code></pre>
<h2 id="적용">적용</h2>
<p>위의 내용을 토대로 실 상황에 적용해보자. </p>
<p>게시글을 올렸을 때, 내 작성글 페이지에 갱신되어야 하는 상황이다. 게시글을 삭제했을 때도 마찬가지로, 값이 갱신되어 내 작성글에서 제거되어야 한다. </p>
<p>이 두 상황을 제외하면 항상 같은 데이터를 가질 것이기 때문에, 여기에 <code>selector</code>를 이용하여 값을 캐싱한다.</p>
<h3 id="selector-1">selector</h3>
<p><code>get</code>으로 로그인한 사용자 정보를 가져와 의존성을 주입시킨다. 가져온 사용자의 id를 토대로, 내 작성글을 불러와 리턴해준다.</p>
<pre><code class="language-tsx">export const myPostsState = selector({
  key: &#39;posts/myPosts&#39;,
  get: async ({ get }) =&gt; {
    const loginUser = get(loginUserState);
    const posts = await getMyPosts(loginUser.uid);
    return posts;
  },
});</code></pre>
<h3 id="게시글-등록-및-삭제-시-값-갱신">게시글 등록 및 삭제 시 값 갱신</h3>
<p>위에서 만든 <code>userRecoilCacheRefresh</code>를 통해, 강제 캐시값 갱신이 필요한 컴포넌트에서 사용해준다.</p>
<pre><code class="language-tsx">// 내 작성글 값에 변화가 이루어지는 컴포넌트

const myPostsCacheRefresher = useRecoilCacheRefresh(myPostsState);

...

const onUploadPost = async () =&gt;{
    await uploadPost( ); // 내 작성글 업로드
    myPostsCacheRefresher();
}

...

const onDeletePost = async (id) =&gt;{
    await deletePost(id); // 내 작성글 삭제
    myPostsCacheRefresher();
}</code></pre>
<h3 id="사용-부분">사용 부분</h3>
<p><code>selector</code>를 사용하면, 해당 값이 사용되는 컴포넌트를 <code>React.suspense</code> 로 감싸서 사용해야한다. 혹은 <code>useRecoilValueLoadable</code>를 사용해서, <code>selector</code>의 <code>state</code>에 맞게 분기 처리해서 사용해줘도 된다. </p>
<p>필자는 <code>state</code>가 loading일 때는 skeleton ui를 보여주고, <code>state</code>가 불러와졌을 때는 값을 꺼내서 사용해주었다.</p>
<pre><code class="language-tsx">// MyPosts.tsx 내 작성글 페이지

const MyPosts = () =&gt; {
  const postsLoadable = useRecoilValueLoadable(myPostsState);

  return (
    &lt;S.Container&gt;
      {postsLoadable.state === &#39;loading&#39; ? (
        &lt;S.PostCardSkeleton /&gt;
      ) : (
        &lt;CardContainer posts={postsLoadable.contents} /&gt;
      )}
    &lt;/S.Container&gt;
  );
};</code></pre>
<h2 id="결과">결과</h2>
<p>이처럼, 데이터가 변경되는 상황이 적고 예측이 가능한 경우에, 평상시엔 값을 캐싱하여 캐시된 값을 리턴해주고, 데이터가 변경되는 시점에는 강제로 캐시를 갱신해주는 방식으로 사용하면 불필요한 API 호출양도 줄이고, 빠른 속도로 데이터를 제공할 수 있기에 시도해보기 좋은 방법 같다.</p>
<h3 id="적용-전">적용 전</h3>
<p><img src="https://images.velog.io/images/tech-hoon/post/b1ae3b0c-e183-4854-91a8-092b7da91d8e/before%20caching.gif" alt=""></p>
<h3 id="적용-후">적용 후</h3>
<p><img src="https://images.velog.io/images/tech-hoon/post/f8ceb75c-c4bf-4df9-8769-27ee32f90c71/after%20caching.gif" alt=""></p>
<h2 id="참고">참고</h2>
<p><a href="https://velog.io/@yiyb0603/Selector%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B0%92-%EC%BA%90%EC%8B%B1%ED%95%98%EA%B8%B0">https://velog.io/@yiyb0603/Selector를-이용하여-데이터-값-캐싱하기</a>
<a href="https://www.youtube.com/watch?v=0-UaleJZOw8">https://www.youtube.com/watch?v=0-UaleJZOw8</a>
<a href="https://recoiljs.org/ko/docs/api-reference/core/useRecoilRefresher/">https://recoiljs.org/ko/docs/api-reference/core/useRecoilRefresher</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UI/UX] Skeleton UI 적용기]]></title>
            <link>https://velog.io/@tech-hoon/skeleton-ui</link>
            <guid>https://velog.io/@tech-hoon/skeleton-ui</guid>
            <pubDate>Tue, 01 Feb 2022 16:55:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>따분한 로딩창 말고, 사용자 친화적인 Skeleton UI로 사용자 이탈율을 줄여보자.</p>
</blockquote>
<h2 id="1-skeleton-ui란">1. Skeleton UI란?</h2>
<p>스켈레톤 UI는 실제 데이터가 렌더링 되기 전에 보이게 될 화면의 윤곽을 먼저 그려주는 로딩 애니메이션이다. 
사용자의 이탈을 막고, ‘어떤 것들이 보여질 것이다’라고 미리 알려주는 효과를 준다. 
기존 Spinner에 비해 훨씬 사용자 친화적이고, 사용자 이탈율도 실제로 적다고 한다.</p>
<h2 id="2-구현">2. 구현</h2>
<h3 id="skeleton-item">Skeleton Item</h3>
<p>먼저 Skeleton UI를 적용할 컨텐츠에 공통적으로 사용되는 Item을 정의한다.
CSS <code>animation</code>을 이용하여, 일정한 시간동안, 배경색의 투명도가 반복적으로 바뀌도록 한다.</p>
<pre><code class="language-jsx">import styled from &#39;styled-components&#39;;

const SkeletonItem = styled.div`
  width: 100%;
  height: 30px;
  background-color: #f2f2f2;
  position: relative;
  overflow: hidden;
  border-radius: 4px;

  @keyframes skeleton-gradient {
    0% {
      background-color: rgba(165, 165, 165, 0.1);
    }
    50% {
      background-color: rgba(165, 165, 165, 0.3);
    }
    100% {
      background-color: rgba(165, 165, 165, 0.1);
    }
  }

  &amp;:before {
    content: &#39;&#39;;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    animation: skeleton-gradient 1.5s infinite ease-in-out;
  }
`;

export default SkeletonItem;</code></pre>
<h3 id="card-skeleton">Card Skeleton</h3>
<p>앞서 정의한 Skeleton Item을 활용하여, 게시물 카드의 Skeleton UI를 구현한다.</p>
<pre><code class="language-jsx">import styled from &#39;styled-components&#39;;
import SkeletonItem from &#39;./components/SkeletonItem&#39;;

interface Props {}

const CardSkeleton = (props: Props) =&gt; {
  return (
    &lt;Wrapper&gt;
      {new Array(6).fill(&#39;&#39;).map((_, i) =&gt; (
        &lt;Card key={i}&gt;
          &lt;Title /&gt;
          &lt;Content /&gt;
          &lt;Bottom&gt;
            &lt;Circle /&gt;
            &lt;Title /&gt;
          &lt;/Bottom&gt;
        &lt;/Card&gt;
      ))}
    &lt;/Wrapper&gt;
  );
};

const Wrapper = styled.ul`
  ...
`;

const Card = styled.li`
  ...
`;

const Title = styled(SkeletonItem)``;

const Content = styled(SkeletonItem)`
  height: 130px;
`;

const Bottom = styled.div`
  display: flex;
  gap: 12px;
`;

const Circle = styled(SkeletonItem)`
  width: 35px;
  height: 30px;
  border-radius: 50%;
  background-color: #f2f2f2;
  position: relative;
  overflow: hidden;
`;

export default CardSkeleton;</code></pre>
<h3 id="기존-컴포넌트의-layout-재사용">기존 컴포넌트의 Layout 재사용</h3>
<p>Skeleton UI는 적용하고자 할 컴포넌트와 가능한 비슷한 것이 좋은 사용자 경험을 제공할 수 있다. 그런데 만약, 기존 컴포넌트의 디자인이 수정된다면, 매번 Skeleton UI를 수정해야한다.</p>
<p>이를 개선하기 위해 <code>PostCard</code>와 공통적으로 사용하는 레이아웃을 모듈화한 <code>Layouts</code>을 import하여 필요한 컴포넌트를 재사용하는 방식으로 리팩토링하였다.</p>
<pre><code class="language-jsx">import styled from &#39;styled-components&#39;;
import SkeletonItem from &#39;./components/SkeletonItem&#39;;
import { Layouts as S } from &#39;components/common/PostCard/Layouts&#39;;

interface Props {}

const PostCardSkeleton = (props: Props) =&gt; {
  return (
    &lt;S.Wrapper&gt;
      {new Array(6).fill(&#39;&#39;).map((_, i) =&gt; (
        &lt;CardSkeleton key={i}&gt;
          &lt;Title /&gt;
          &lt;Content /&gt;
          &lt;Bottom&gt;
            &lt;Circle /&gt;
            &lt;Title /&gt;
          &lt;/Bottom&gt;
        &lt;/CardSkeleton&gt;
      ))}
    &lt;/S.Wrapper&gt;
  );
};

const CardSkeleton = styled(S.PostCard)`
  gap: 20px;
  cursor: default;
`;

const Title = styled(SkeletonItem)``;

const Content = styled(SkeletonItem)`
  height: 130px;
`;

const Bottom = styled.div`
  display: flex;
  gap: 12px;
`;

const Circle = styled(SkeletonItem)`
  width: 35px;
  height: 30px;
  border-radius: 50%;
  background-color: #f2f2f2;
  position: relative;
  overflow: hidden;
`;

export default PostCardSkeleton;</code></pre>
<h2 id="3-결과">3. 결과</h2>
<p>렌더링 될 컨텐츠를 미리 그려줌으로써, 사용자는 현재 로딩중임을 인지하고 로딩 시간을 보다 친화적으로 받아들일 수 있을 것이다.
<img src="https://images.velog.io/images/tech-hoon/post/328bb3ab-bf64-482c-a474-8a29bda1fa17/ezgif.com-gif-maker%20(3).gif" alt="skeleton ui"></p>
<h3 id="참고">참고</h3>
<p><a href="https://ui.toast.com/weekly-pick/ko_20201110">https://ui.toast.com/weekly-pick/ko_20201110</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS] scroll-snap으로 부드러운 스크롤 표현하기]]></title>
            <link>https://velog.io/@tech-hoon/CSS-scroll-snap</link>
            <guid>https://velog.io/@tech-hoon/CSS-scroll-snap</guid>
            <pubDate>Sun, 01 Aug 2021 13:34:31 GMT</pubDate>
            <description><![CDATA[<h1 id="scroll-snap이란">scroll-snap이란?</h1>
<blockquote>
<p>CSS 단 두 줄로, 부드러운 스크롤의 carousel을 만들 수 있다!</p>
</blockquote>
<ul>
<li>scroll-snap은 사용자가 터치 혹은 스크롤 조작을 하였을 때 offset을 설정할 수 있는 속성이다.</li>
<li>이를 통해 스와이프 하거나, 스크롤 할 때 자연스러운 효과를 낼 수 있다.</li>
</ul>
<h2 id="scroll-snap-type">scroll-snap-type</h2>
<ul>
<li>축과 엄격도를 선언해주는 속성</li>
<li><strong>부모 컨테이너</strong>에 지정</li>
<li>proximity : 기본값, 축에 따라서 자동으로 맞추어져 스냅</li>
<li>madatory : 항상 스냅</li>
</ul>
<pre><code class="language-css">.parent-container{
    scroll-snap-type: x mandatory; // x축으로 항상 스냅
    scroll-snap-type: y mandatory; // y축으로 항상 스냅
}</code></pre>
<h2 id="scroll-snap-align">scroll-snap-align</h2>
<ul>
<li>스냅이 되었을 때 정렬해주는 속성</li>
<li><strong>자식 요소</strong>에 지정</li>
<li>스크롤시 지정해준 속성의 위치로 곧바로 이동한다</li>
</ul>
<pre><code class="language-css">.item {
    scroll-snap-align: start;
    scroll-snap-align: center;
    scroll-snap-align: end;
}</code></pre>
<h3 id="💡-flex-none">💡 flex: none</h3>
<p>부모에서 <code>display: flex</code> 를 지정하고, 자식에서 <code>flex: none</code> 을 지정하지 않을 시, 
부모 크기에 맞춰 자식 요소들이 수축될 수 있다.</p>
<h3 id="💡--webkit-scrollbar">💡 &amp;::-webkit-scrollbar</h3>
<p>이를 <code>display:none</code> 으로 설정할 시, 해당요소(&amp;)의 스크롤바를 제거할 수 있다.</p>
<h2 id="예시">예시</h2>
<p>!codepen[taekhun/embed/MWmXpXK?default-tab=result]</p>
]]></description>
        </item>
    </channel>
</rss>