<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yerim.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 09 Sep 2025 15:46:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yerim.log</title>
            <url>https://velog.velcdn.com/images/moon-yerim/profile/536089f2-c034-4bc6-ad33-fc52d2ccde07/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yerim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/moon-yerim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[npm] vercel의 지원중단으로 node버전 업데이트하기]]></title>
            <link>https://velog.io/@moon-yerim/npm-vercel%EC%9D%98-%EC%A7%80%EC%9B%90%EC%A4%91%EB%8B%A8%EC%9C%BC%EB%A1%9C-node%EB%B2%84%EC%A0%84-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@moon-yerim/npm-vercel%EC%9D%98-%EC%A7%80%EC%9B%90%EC%A4%91%EB%8B%A8%EC%9C%BC%EB%A1%9C-node%EB%B2%84%EC%A0%84-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 09 Sep 2025 15:46:52 GMT</pubDate>
            <description><![CDATA[<p>vercel에서 node v18의 지원을 중단해서 배포를 할 수 없는 상태가 되었다. 최근에 배포를 하지 않아 미리 공지를 확인하지 못했다😱</p>
<p>node v22로 업그레이드하면서 라이브러리 버전을 확인해야 했다.</p>
<h2 id="로컬에서-먼저-확인하기">로컬에서 먼저 확인하기</h2>
<p>먼저 로컬에서 nvm으로 node를 버전업하고 빌드 에러를 확인했다.</p>
<pre><code class="language-shell">nvm install v22
nvm use v22</code></pre>
<p>그런데... 너무 잘 됨.. 빌드 성공
<code>npm outdated</code> 명령어를 실행했을 때 버전업이 필요해 보이는 라이브러리가 많아 걱정했는데 바로 빌드를 성공해서 오히려 당황스러웠다.</p>
<h2 id="vercel에서-확인하기">vercel에서 확인하기</h2>
<p>그런데!! vercel에서 node 버전 업 후 redeploy를 하니 빌드 에러 발생🚨
<img src="https://velog.velcdn.com/images/moon-yerim/post/56eb1106-9b32-4487-8b45-8e134485a61f/image.png" alt=""></p>
<p>에러를 확인해 보니 <code>npm install</code> 중에 발생했는데, 로컬에서는 아무리 <code>npm install</code>을 다시 해봐도 에러가 나지 않았다.</p>
<p>vercel의 에러 로그를 gpt에게 물어봤더니 다음과 같이 알려줬는데</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/19a5045e-4bcf-43fa-8fe7-6cffb2ede973/image.png" alt=""></p>
<p>내용을 확인하고 나니, 얼마 전 env가 제대로 인식되지 않아 프로젝트를 삭제하고 다시 clone 한 적이 있었는데, (변경된 코드가 하나도 없는데 딱 한 개의 환경변수만 인식 되지 않았다.) 그때도 <code>npm install</code> 시 canvas 라이브러리 설치 문제였던 게 생각났다.</p>
<pre><code>brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman python-setuptools</code></pre><p><a href="https://www.npmjs.com/package/canvas">npm - canvas</a>
gpt 말대로 canvas는 위와 같이 종속 라이브러리를 설치해 줘야 하는데, 로컬에는 설치되어 있지만 vercel의 리눅스 환경에는 설치되어 있지 않아 발생한 문제였다.</p>
<p>그렇게 버전업을 하려고 찾던 중 발견한 사실.. canvas를 사용하지 않는다.. <code>DOM &lt;canvas&gt;</code>를 사용하는 부분은 있는데, 라이브러리를 사용하고 있는 코드는 없었다.</p>
<p>그래서 다른 라이브러리에 종속된 건가 싶어 아래 명령어로 확인해 봤다. </p>
<pre><code class="language-shell">npm ls canvas</code></pre>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/6f9f3ef5-8d67-4619-8a5e-2b0827851a2b/image.png" alt=""></p>
<p>jsdom에서 사용하고 있는데 중복으로 설치되어 있어 최종적으로 해당 의존성은 남기고 직접 설치한 canvas만 제거했다.</p>
<p>아마 이전에 사용하던 라이브러리가 더 이상 사용하지 않는데 제거되지 않은 모양이다.🥲
제거 후 vercel에서 확인하니 빌드 성공!</p>
<p>테스트하니 기능적으로도 문제가 발생하지 않아 배포했다.</p>
<hr>
<p>수정이 필요한 부분이 있어 꼭 배포를 해야 하는 상황이었다.
그래서 최소한의 변경으로 빌드 성공 후 바로 배포했는데, 이어서 다른 라이브러리들도 버전 확인 후 업데이트를 진행해야 할 듯하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[css] flex item의 center 와 overflow scroll이 충돌날 때]]></title>
            <link>https://velog.io/@moon-yerim/css-flex-item%EC%9D%98-center-%EC%99%80-overflow-scroll%EC%9D%B4-%EC%B6%A9%EB%8F%8C%EB%82%A0-%EB%95%8C</link>
            <guid>https://velog.io/@moon-yerim/css-flex-item%EC%9D%98-center-%EC%99%80-overflow-scroll%EC%9D%B4-%EC%B6%A9%EB%8F%8C%EB%82%A0-%EB%95%8C</guid>
            <pubDate>Fri, 06 Jun 2025 15:26:26 GMT</pubDate>
            <description><![CDATA[<p><code>justify-content: center</code>와 <code>overflow: scroll</code>을 함께 사용하니 스크롤이 처음부터 되지 않고 잘리는 문제가 발생했다.</p>
<p>이는 기본적으로 자식 엘리먼트가 부모 엘리먼트 기준 가운데 정렬되어있기 때문에, 브라우저는 부모 엘리먼트 상단으로 튀어나간 자식엘리먼트의 부분은 overflow로 인지하지 않는다. 그래서 아무리 왼쪽으로 스크롤을 해도, 자식 엘리먼트의 끝부분까지 스크롤이 되지 않았다.</p>
<p>이럴 때 <code>safe</code> 키워드를 사용하면 flex-start처럼 가장 처음부터 정상적으로 스크롤이 되도록 할 수 있다.</p>
<pre><code>justify-content: safe center;
</code></pre><p>그런데 크로스 브라우징을 고려할 필요가 있을 듯하다.
<a href="https://caniuse.com/?search=flex%20safe">https://caniuse.com/?search=flex%20safe</a></p>
<p>결국, 크로스 브라우징 때문에 margin으로 정렬하도록 변경했다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://velog.io/@9rganizedchaos/Flexbox-Align-items-center%EC%99%80-overflow-scroll%EC%9D%B4-%EA%B2%B0%ED%95%A9%EB%90%A0-%EB%95%8C">Flexbox Align-items center와 overflow scroll이 결합될 때!
</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[css] 반응형되는 말줄임]]></title>
            <link>https://velog.io/@moon-yerim/css-%EB%B0%98%EC%9D%91%ED%98%95-%EB%A7%90%EC%A4%84%EC%9E%84</link>
            <guid>https://velog.io/@moon-yerim/css-%EB%B0%98%EC%9D%91%ED%98%95-%EB%A7%90%EC%A4%84%EC%9E%84</guid>
            <pubDate>Sun, 27 Apr 2025 15:46:28 GMT</pubDate>
            <description><![CDATA[<pre><code>flex: 1;
display: -webkit-box;
text-overflow: ellipsis;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
word-break: break-all;</code></pre><p><code>flex:1</code> 은 필요에 따라 하는 듯, 없어도 해결이 됨.
<code>display: -webkit-box</code> 필수</p>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://jenny0520.tistory.com/158">글자 말 줄임 처리(feat. 반응형)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[세미나] [구름커밋] 시니어 개발자와 주니어 개발자는 어떻게 다를까]]></title>
            <link>https://velog.io/@moon-yerim/%EC%8B%9C%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%99%80-%EC%A3%BC%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8B%A4%EB%A5%BC%EA%B9%8C</link>
            <guid>https://velog.io/@moon-yerim/%EC%8B%9C%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%99%80-%EC%A3%BC%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8B%A4%EB%A5%BC%EA%B9%8C</guid>
            <pubDate>Mon, 31 Mar 2025 09:08:36 GMT</pubDate>
            <description><![CDATA[<ul>
<li>구름 커밋</li>
<li>겉포장지만 꾸미는 것이 다가 아니다.(FSD, Next.js 등 화려하기만 한 이력서 X)</li>
<li>조타이를 사용한다 → 왜? 타고가며 근원 적인 질문에 답해나가기</li>
<li>나보다 압도적으로 잘하는 코드를 봐야 배울 수 있다</li>
<li>꾸준히 양질(내 실력보다 약간 어려운)의 지식 얻기<ul>
<li>약간 어려운 난이도의 기술 도서 읽기</li>
<li>Frontend-Fundamentals</li>
</ul>
</li>
<li>내가 할 수 없던 것에 도전해야 성장한다</li>
<li>지식을 당연하게 받아들이는 것으로는 성장할 수 없다</li>
<li>두뇌가 풀가동되는 상태를 만들어야 한다</li>
<li>내 학습 시간 돌아보기<ul>
<li>내가 공부할 때 두뇌에 땀이 나고 있는가?</li>
<li>내가 공부하고 있는 내용의 난이도는 적당히 어려운가?</li>
<li>내가 아직 자신이 없는 개발 영역은 어디인가?</li>
</ul>
</li>
<li>두뇌 풀가동시키기<ul>
<li>이게 무슨 말인지 발표할 수 있을 정도로 이해해 보기</li>
<li>내가 지금까지 알고 있었던 내용과 연결시켜 보기</li>
<li>내가 짜고 있는 코드와 연결시켜 보기</li>
</ul>
</li>
<li>어떤 것을 아는 것을 넘어서 다음에 행동하는 것</li>
<li>성과가 지식에서 온 경험이 별로 없는 듯, 행동 변화가 더 큰 것 같다.</li>
</ul>
<h2 id="코딩을-잘하려면">코딩을 잘하려면</h2>
<ul>
<li>멋있는 것보다는 알맹이를<ul>
<li>꼬리 질문을 통해 학습하고 내 색깔 찾기</li>
</ul>
</li>
<li>압도적으로 좋은 것 많이 보기<ul>
<li>꾸준하게 내가 몰랏던 양질의 자료 접하기</li>
</ul>
</li>
<li>두뇌 풀가동시키기<ul>
<li>당연한 내용에 고개를 끄덕이는 것이 아니라 자신없는영역에 도전하기</li>
</ul>
</li>
<li>적용하기<ul>
<li>지식을 얻는 것에서 멈추지 않고 실제로 행동 계획 세우기</li>
</ul>
</li>
</ul>
<ul>
<li><p>나를 어떻게 더 발전시킬 것인가</p>
</li>
<li><p>변화에 열려있기</p>
<ul>
<li>무엇이든 개선할 수 있다, 나에게 맞는 방법으로</li>
</ul>
</li>
<li><p>꾸준함</p>
<ul>
<li>차이를 만드는 것은 재능보다는 꾸준함</li>
<li>피드백과 칭찬으로 꾸준히 하기<ul>
<li>스스로 행동을 피드백, 축하하기</li>
<li>외부 피드백 받기</li>
</ul>
</li>
<li>함께 꾸준히 하기</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[web] 사용자 경험 개선으로 관련 고객 문의 0건 만들기]]></title>
            <link>https://velog.io/@moon-yerim/web-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B2%BD%ED%97%98-%EA%B0%9C%EC%84%A0%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A0%A8-%EA%B3%A0%EA%B0%9D-%EB%AC%B8%EC%9D%98-0%EA%B1%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@moon-yerim/web-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B2%BD%ED%97%98-%EA%B0%9C%EC%84%A0%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A0%A8-%EA%B3%A0%EA%B0%9D-%EB%AC%B8%EC%9D%98-0%EA%B1%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 01 Mar 2025 12:13:04 GMT</pubDate>
            <description><![CDATA[<p>서비스 기능 중 DOM을 이미지로 다운받게하는 기능이 있었다.
해당 기능을 배포하고 나서 이미지 다운로드가 되지 않는다는 문의가 하루 평균 3건 이상은 들어왔다. 또한 위 문의와 함께 사파리에서 다운로드 받은 이미지 파일을 못 찾겠다는 문의도 비슷하게 들어왔다.</p>
<p>개수로 보면 적어 보일지 몰라도 두 문의를 합쳐 그날 하루 CS의 30% 이상 차지하고 있었다. 매일매일 빠짐없이 들어온 이 문제를 해결한 방법을 공유하려 한다.🪄</p>
<h2 id="문제-1-카카오톡-인앱-브라우저">문제 1: 카카오톡 인앱 브라우저</h2>
<p>프론트에서 구현하는 이미지 다운로드 기능은 보통 라이브러리를 사용하게 되고 브라우저 정책에 따라 라이브러리가 지원하지 않거나 다르게 동작할 수 있다.</p>
<p><a href="https://youtu.be/92GV_IhpiBw?si=yueNTrkBpb1e_b1X">DOM을 이미지로 만들기 X같은 이유와 해결법
</a></p>
<p>이미지 다운로드가 되지 않는다는 문의가 들어오면 고객에게 어떤 앱을 사용했냐고 여쭤봤는데 모두 카카오톡 인앱 브라우저를 통해 해당 기능을 사용한 경우였다.</p>
<p>원래 우리 서비스는 특정 페이지를 제외하고는 카카오톡 인앱 브라우저로 사용자가 유입되는 경우가 없었기 때문에 크로스 브라우징을 크게 고려하지 않았는데,
알림톡으로 안내하는 플로우가 추가되면서 알림톡을 통해 서비스에 접근하면 카카오톡 인앱 브라우저로 사용자가 들어오는 것이었다.</p>
<p>처음엔 팝업을 통해 다른 브라우저 사용을 안내할까 했는데,
사용자 입장에서 다른 브라우저로 다시 접속하는 게 번거롭기도 하고, 다른 기능이 추가되는 경우도 고려했을 때 카카오톡 인앱 브라우저를 지원하지 않는 게 낫겠다고 판단했다.</p>
<p>그래서 url 스킴을 사용해 카카오톡 브라우저로 서비스에 접속할 경우 해당 페이지를 외부 브라우저를 통해 띄우도록 했다.
<a href="https://burndogfather.com/271">카카오톡/라인 인앱브라우저에서 외부브라우저 띄우기</a></p>
<p>처음엔 알림톡 버튼 링크를 수정할까 했는데 그럴 경우 알림톡마다 설정을 해야 했기에 그보다 근본적으로 서비스 내에서 로직이 실행될 수 있도록 할 필요가 있었다.</p>
<p><strong>최종적으로 미들웨어에서 카카오톡 인앱 브라우저를 감지하면 특정 페이지로 리다이렉트하고, 그 페이지에서 url 스킴을 사용해 외부 브라우저로 보내도록 했다.</strong></p>
<pre><code class="language-javascript">export async function middleware(req: NextRequest) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });

  const userAgent = req.headers.get(&#39;user-agent&#39;) || &#39;&#39;;
  const isKakaoTalk = userAgent.includes(&#39;KAKAOTALK&#39;);

  // 카카오톡 브라우저로 접속한 경우 noactive페이지로 리다이렉트
  if (isKakaoTalk) {
    const currentUrl = req.nextUrl.href;
    const url = req.nextUrl.clone();
    url.pathname = &#39;/noactive&#39;;
    url.search = &#39;&#39;;

    const response = NextResponse.redirect(url);
    response.cookies.set(&#39;referer&#39;, currentUrl);
    response.cookies.get(&#39;referer&#39;);
    return response;
  }
 }</code></pre>
<p>next.js의 미들웨어에서 카카오톡 인앱 브라우저를 감지해 noactive 페이지로 리다이렉트를 시킨다. <strong>사용자가 외부 브라우저에서도 원했던 페이지로 바로 접근할 수 있도록 referer를 쿠키에 저장해둔다.</strong></p>
<pre><code class="language-javascript">import { ReactElement, useEffect } from &#39;react&#39;;
import { useCookies } from &#39;react-cookie&#39;;

const NoActive = () =&gt; {
  const [cookies] = useCookies([&#39;referer&#39;]);

  useEffect(() =&gt; {
    const referer = cookies[&#39;referer&#39;];

    if (referer) {
      const kakaoUrl = `kakaotalk://web/openExternal?url=${encodeURIComponent(
        referer,
      )}`;
      window.location.href = kakaoUrl;
    }
  }, []);

  return &lt;&gt;&lt;/&gt;;
};

export default NoActive;
</code></pre>
<p>noactive 페이지에 접속하면 외부 브라우저로 연결해 준다.</p>
<p>🚨 미들웨어에서 바로 외부 브라우저로 연결하게 되면 문제가 있다.
뒤로 가기를 할 경우 다시 카카오톡 인앱 브라우저로 돌아오는데, 이때 마지막 접속 페이지가 그대로 보인다.
이 때문에 확실하게 사용할 수 없다는 걸 보이기 위해 빈 페이지인 noactive 페이지가 마지막이 될 수 있도록 리다이렉트 후 외부 브라우저를 연결하도록 했다.</p>
<h2 id="문제-2-모바일-사파리-다운로드-파일-경로">문제 2: 모바일 사파리 다운로드 파일 경로</h2>
<p>갤럭시의 경우 크롬에서 파일을 저장하면 갤러리에 바로 저장된다. 하지만 아이폰의 경우 다운로드하면 갤러리가 아닌 별도의 경로에 저장된다. 이 때문에 고객이 다운로드한 파일을 못 찾겠다는 문의가 매일 들어왔고, 문의가 들어올 경우 아래 링크를 직접 안내했다.</p>
<p><a href="https://support.apple.com/ko-kr/102440">iPhone 또는 iPad에서 다운로드 항목을 찾을 수 있는 위치</a></p>
<p>이를 해결하기 위해 토스트 팝업의 내용을 수정했다.
<strong>원래 토스트 팝업의 문구는 모든 브라우저에서 동일했는데 분기 처리를 통해 사파리에서는 위 링크로 접속할 수 있도록 링크가 연결된 버튼을 추가했다.</strong></p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/5a59ebeb-1100-41aa-add1-95e032606c66/image.png" alt=""></p>
<pre><code class="language-javascript">  const showDownloadCompleteToast = () =&gt; {
    if (isIPhone) {
      toast({
        description: (
          &lt;div&gt;
            &lt;p&gt;{MESSAGE.DOWNLOAD_COMPLETE_NOTICE}&lt;/p&gt;
            &lt;Button asChild variant=&quot;outline&quot;&gt;
             //다운로드 파일 경로 안내 링크
              &lt;a
                href=&quot;https://support.apple.com/ko-kr/102440&quot;
                target=&quot;_blank&quot;
              &gt;
                다운로드 된 이미지를 찾을 수 없나요?
              &lt;/a&gt;
            &lt;/Button&gt;
          &lt;/div&gt;
        ),
        variant: &#39;success&#39;,
      });
    } else {
      toast({
        description: MESSAGE.DOWNLOAD_COMPLETE_NOTICE,
        variant: &#39;success&#39;,
      });
    }
  };</code></pre>
<h2 id="관련-문의-0">관련 문의 0</h2>
<p>수정 후 해당 문의들은 더 이상 들어오지 않았다 👏👏👏👏👏👏👏👏</p>
<p>사실 처음에 이미지 다운로드 기능을 구현하면서 이 이슈가 예견되긴 했다. 그래서 검색하다 url 스킴으로 외부 브라우저를 연결할 수 있는 방법을 알게 되었고.</p>
<p>그리고 논의를 했는데 아직 벌어지지 않은 이슈였고 해당 기능을 사용자가 얼마나 많이 이용할지 알 수 없었기에 먼저 배포를 해보자고 결론을 내려 그대로 진행한 것이었다.</p>
<p>기능 사용에 대한 데이터도 수집하고 있었지만, 이슈에 대한 문의를 통해 사용자가 이 기능을 원한다는 것을 확실히 알게 되었다. 또한, 덕분에 상세페이지에도 해당 기능 제공에 대한 내용을 추가할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[js] clip-path를 지원하는 이미지 다운로드 만들기(DOM을 이미지로)]]></title>
            <link>https://velog.io/@moon-yerim/js-clip-path%EB%A5%BC-%EC%A7%80%EC%9B%90%ED%95%98%EB%8A%94-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0DOM%EC%9D%84-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A1%9C</link>
            <guid>https://velog.io/@moon-yerim/js-clip-path%EB%A5%BC-%EC%A7%80%EC%9B%90%ED%95%98%EB%8A%94-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0DOM%EC%9D%84-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A1%9C</guid>
            <pubDate>Mon, 27 Jan 2025 07:47:37 GMT</pubDate>
            <description><![CDATA[<p>서비스에 HTML로 구현된 컴포넌트를 이미지로 다운로드할 수 있는 기능이 있다.
기존 코드는 html2canvas 라이브러리를 사용해 구현했었다.
그런데 최근 디자인 타입을 추가하며 기능을 테스트했더니 정상적으로 동작하지 않았다.🔥</p>
<h1 id="문제">문제</h1>
<h2 id="html2canvas">html2canvas</h2>
<p>기존에 html2canvas 라이브러리를 선택했던 이유는 다음과 같다.</p>
<pre><code>- html-to-image : 서체가 적용되지 않은 채로 이미지로 변환 됨
- dom-to-image : 사파리에서 이미지가 출력 안 됨, 이슈에 동일한 내용 많음, 리드미에 사파리 지원 안 한다고 적혀있음
- dom-to-image-more : dom-to-image와 동일하게 사파리 지원 안 한다고 적혀있음</code></pre><p>많이 언급되는 라이브러리들을 검토했을 때 위와 같은 문제가 있었다.
html2canvas는 크기가 크다는 단점이 있지만, 이는 문제가 되지 않는다고 느껴졌고 테스트 결과 변환 속도도 빨랐다. 그리고 중요한 건 사파리를 무조건 지원해야 했다.</p>
<p>새로운 디자인에는 클리핑 패스 기능을 사용해 이미지를 하트 모양으로 크롭한 부분이 있는데, 이는 SVG의 <code>clipPath</code> 기능으로 구현했다.</p>
<pre><code class="language-jsx">            &lt;div className=&#39;photo__box&#39;&gt;
                &lt;svg
                    xmlns=&quot;http://www.w3.org/2000/svg&quot;
                    viewBox=&quot;0 0 300 318&quot;
                    className=&quot;mask-svg&quot;
                  &gt;
                    &lt;defs&gt;
                      &lt;clipPath id=&quot;frame&quot;&gt;
                        &lt;path
                          fillRule=&quot;evenodd&quot;
                          clipRule=&quot;evenodd&quot;
                          d=&quot;M150 50.599C138.49...&quot;
                        /&gt;
                      &lt;/clipPath&gt;
                    &lt;/defs&gt;
                  &lt;/svg&gt;

                  &lt;div className=&quot;image-wrapper&quot;&gt;
                    &lt;img
                      src={imageUrl}
                      alt=&quot;메인사진&quot;
                    /&gt;
                  &lt;/div&gt;
               &lt;/div&gt;</code></pre>
<pre><code class="language-css">          photo__box {
            clip-path: url(#frame);
            -webkit-clip-path: url(#frame);
           }</code></pre>
<p>문제가 된 부분은 아래의 왼쪽처럼 하트 모양이 있는 부분을 clip-path 방식으로 구현했는데, 이미지로 다운로드하면 적용되지 않은 채로 다운로드되었다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/dfa3192f-295c-41cb-9459-92dab71550c8/image.jpg" alt=""></p>
<h1 id="해결">해결</h1>
<h2 id="modern-screenshot">modern-screenshot</h2>
<p>dom-to-image로 테스트해보니 하트 모양은 정상적으로 이미지로 변환되었지만, 크롬에서만 동작했다.
역시나 사파리에서 정상적으로 동작하지 않았으며, 깃허브 이슈를 찾아봐도 해결 방법을 찾지 못했다.
그러던 중 발견한 한 개의 이슈:</p>
<p><a href="https://github.com/tsayen/dom-to-image/issues/453">Most of the issues have been fixed in forked library modern-screenshot</a></p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/d6e1e91b-ae5b-457c-bed7-d7a5dc64a289/image.png" alt="">
npm에 검색해보니 생각보다 다운로드 수도 많음... clip-path가 정상적으로 적용되는 것도 확인했다.</p>
<h2 id="modern-screenshot의-문제">modern-screenshot의 문제</h2>
<p>이미지로 변환이 잘되어 이 라이브러리를 사용하려 했으나, 이후에도 여러 문제를 해결해야 했다.</p>
<h3 id="사파리에서-발생하는-이슈1">사파리에서 발생하는 이슈1</h3>
<p>dom-to-image에서는 이미지 리소스를 다운로드하지 못하는 이슈가 있었는데, 이 라이브러리도 동일한 이슈가 있었다.</p>
<p>dom-to-image의 깃허브 이슈에서 두 번 실행하라는 내용이 많았는데, 그땐 동작하지 않았으나 <code>modern-screenshot에서는 두 번 실행하면 정상적으로 작동</code>했다.
그런데 두 번 실행하니까 속도가 느려진다.. 그래서 원래 4가지 이미지를 한 번에 다운받게 했었는데 필요한 사진만 각각 다운받을 수 있도록 수정했다.</p>
<h3 id="">?</h3>
<p>사파리에서 리소스를 다운로드할 때 시간이 오래 걸리며 생기는 오류라는 것 같은데, 내부적으로 수정할 수 없어 이렇게 해결해야 하는 건가? 싶지만 왜 두 번 실행하면 제대로 동작하는지는 알 수 없었다.</p>
<h3 id="사파리에서-발생하는-이슈2">사파리에서 발생하는 이슈2</h3>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/603f791c-87e9-484d-aeb5-a535c73babca/image.png" alt=""></p>
<p>문제 1을 해결하고 나니 사파리에서 <code>Not allowed to load local resource</code>라는 에러가 발생하며 이미지가 정상적으로 변환되지 않는 이슈가 발생했다.</p>
<p>그런데 어떤 디자인 타입은 정상적으로 동작하고, 어떤 타입은 동작하지 않았다.
오류에 적힌 data URL이 SVG라 동작하지 않는 케이스와 동작하는 케이스를 추려 <code>사용된 SVG의 차이점을 비교</code>했지만, 원인을 알 수 없었다.</p>
<p>결국 동작하지 않는 케이스의 코드를 하나씩 지워본 결과, SVG가 아닌 일반 텍스트 요소를 지우니 다시 동작했는데 알고 보니 <code>Pretendard 서체를 사용하는 디자인 타입만 동작하지 않는 것</code>이었다.</p>
<pre><code>@import url(&#39;https://fastly.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css&#39;);</code></pre><blockquote>
<p>@import는 브라우저가 외부 CSS 파일을 가져오도록 요청하는 방식인데, 이 요청이 CORS 정책의 영향을 받습니다. 만약 Pretendard의 호스트(여기서는 fastly.jsdelivr.net)가 올바른 CORS 헤더를 설정하지 않았다면, 브라우저가 해당 리소스를 차단할 수 있습니다.
리소스를 로드하지 못하거나 403(Forbidden) 또는 다른 HTTP 오류가 발생합니다.
출처: GPT</p>
</blockquote>
<p>global.css에 프리텐다드 서체가 @import 방식으로 불러오고 있었는데 이 부분이 이미지로 변환 시 브라우저 보안 문제가 발생한 것 같다. <code>해당 부분을 @font-face 방식으로 수정해 오류를 해결</code>할 수 있었다.</p>
<h3 id="-1">?</h3>
<p>GPT가 css 파일을 불러오는 방식으로 되어있어 이미지 변환 시 문제가 발생한 거라고 하는데 CSS 파일을 불러오는 것과 폰트 파일 경로로 불러오는 게 무슨 차이가 있는 걸까..?</p>
<h3 id="모바일-크롬에서-발생하는-이슈">모바일 크롬에서 발생하는 이슈</h3>
<p>모바일 크롬에서 텍스트가 줄바꿈되어 버리는 문제가 있었는데, 이는 <code>white-space: nowrap;</code>을 사용해 해결할 수 있었다.
(나는 줄바꿈이 필요 없었지만 여러 줄의 텍스트인 경우 이 방법으로는 해결할 수 없지 않을까?)</p>
<h2 id="마무리">마무리</h2>
<p>html2canvas를 사용할 때보다 변환 속도가 느려진 점은 아쉬웠지만, 앞으로 추가될 수 있는 다양한 디자인 타입을 고려했을 때 꼭 해결해야 하는 과제였다.</p>
<p>이 과정에서 이미지를 개별 다운로드하는 방식으로 UI를 수정하며 고객들에게 불편함을 줄 수 있을까 걱정했지만, 오히려 필요한 이미지만 선택적으로 다운로드할 수 있는 기능으로 개선되면서 예상치 못한 긍정적인 결과를 얻을 수 있었다.👍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[mui] mui 번들 크기 줄이기로 시작했으나 없애버리고 메모리 줄이기]]></title>
            <link>https://velog.io/@moon-yerim/next.js</link>
            <guid>https://velog.io/@moon-yerim/next.js</guid>
            <pubDate>Wed, 16 Oct 2024 07:39:28 GMT</pubDate>
            <description><![CDATA[<p>앞에서 OOM으로 인해 페이지가 새로고침 된다는 현상을 해결하는 중이었다고 했다.
하지만 힙 메모리의 사용량이 아직 해결되지 않아서 이어서 해결해야 했다.</p>
<p><a href="https://velog.io/@moon-yerim/react-html-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%B5%9C%EC%A0%81%ED%99%94">[react, html] DOM 조작으로 인한 메모리 문제 해결</a></p>
<h3 id="해결-과정">해결 과정</h3>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/eea6d85a-0d4e-4028-b211-788f398161d2/image.png" alt=""></p>
<p>크롬의 메모리 탭에서는 3가지의 프로파일링 방식을 제공한다. 문의가 3번 들어왔는데 특정 동작을 했을 때 새로고침이 된다는 내용으로 동일했다.
그래서 동작을 재연하면서 <code>타임라인의 할당 계측</code>과 <code>성능 탭</code>을 사용해서 측정했다.</p>
<p>그러다 더 이상 눈에 띄는 게 없어 <code>힙 스냅샷</code>으로 측정을 해봤다.
측정해 봤더니 사용한 적 없는 mui 컴포넌트가 엄청 많은 거다. 세상에😱</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/225a4e0e-41ff-491b-a882-93a7b9482a0b/image.png" alt=""></p>
<ul>
<li>Default import vs Named import
<img src="https://velog.velcdn.com/images/moon-yerim/post/ed606197-2f79-482a-88b1-405e611e86fe/image.png" alt=""></li>
</ul>
<p>mui는 import를 <code>default</code> 방식으로 해야 필요한 컴포넌트만 가져온다.
우리 프로젝트에 군데군데 <code>named</code> 방식을 사용하는 곳이 있었고 해결을 위해 babel 설정을 했으나 바로 해결되지 않았다.</p>
<ul>
<li>babel 설정 방법: <a href="https://mui.com/material-ui/guides/minimizing-bundle-size/">mui - Minimizing bundle size
</a></li>
</ul>
<p>이때부터 추가로 고민을 했다. 현재 프로젝트 상황은</p>
<blockquote>
<ol>
<li>문제가 되는 컴포넌트들은 추후에 <code>디자인 리뉴얼</code>이 계획되어 있다.</li>
<li>ui 라이브러리를 교체했고, 새로 구현되는 컴포넌트는 <code>shadcn</code>으로 구현된다.(mui는 점차적으로 없애는 중)</li>
<li>당장의 조치를 취해서 메모리 이슈 문의는 더 이상 발생하지 않고 있다. (근본적 문제 해결을 위해 작업중)</li>
</ol>
</blockquote>
<p>이 상황에서 디자인 작업을 위한 시간을 하루만 더 들이면 많은 부분을 한 번에 해결할 수 있을 것 같다는 판단이 들었다.
그래서 트리쉐이킹 설정을 더 찾기보다 mui를 덜어내는 방향으로 정했다.</p>
<p>해당 컴포넌트들 내부에 있던 모든 mui 코드를 shadcn을 사용하는 것으로 수정하고 불필요한 상태들을 정리했다.</p>
<h3 id="비교">비교</h3>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/81000a97-d6b0-453c-8549-d7feee1572b8/image.png" alt=""></p>
<p>힙 스냅샷을 다시 측정해 보니 메모리를 차지하고 있던 mui 컴포넌트가 없어졌다.</p>
<ul>
<li>수정전
<img src="https://velog.velcdn.com/images/moon-yerim/post/0351c5b2-7d0c-42a4-a94c-d1b2bf64bc00/image.png" alt=""></li>
<li>수정후
<img src="https://velog.velcdn.com/images/moon-yerim/post/208f76aa-99a4-49c8-a197-f4ed417d43e4/image.png" alt="">
JS 힙의 사용량이 <code>45.1MB -&gt; 7.8MB까지 80% 감소</code>했다.</li>
</ul>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><a href="https://mui.com/material-ui/guides/minimizing-bundle-size/">mui - Minimizing bundle size
</a></li>
<li><a href="https://velog.io/@leejaehyuck9/Next.js%EC%97%90%EC%84%9C-Mui-%EB%B2%88%EB%93%A4-%ED%81%AC%EA%B8%B0-%EC%A4%84%EC%9D%B4%EB%8A%94-%EB%B2%95">Mui 번들 크기 줄이는 법</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[react, html] DOM 조작으로 인한 메모리 문제 해결]]></title>
            <link>https://velog.io/@moon-yerim/react-html-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@moon-yerim/react-html-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Thu, 10 Oct 2024 11:06:56 GMT</pubDate>
            <description><![CDATA[<p>페이지가 새로고침된다는 오류가 접수돼서 메모리 최적화를 진행했다.</p>
<h3 id="오류-발생">오류 발생</h3>
<p>사진이 많은 팝업 컴포넌트를 열면 페이지가 새로고침된다는 오류가 접수되었다.</p>
<h3 id="해결-과정">해결 과정</h3>
<blockquote>
<p><strong>가설 1</strong>
사진이 많아서 메모리를 너무 많이 사용한다.</p>
</blockquote>
<p>처음에는 오류가 발생한 동작을 타임라인으로 메모리 검사를 진행했다. 그런데 메모리를 많이 차지하는 부분은 대부분 라이브러리 내부에서 발생하고 있었다.</p>
<p>사진의 개수를 줄이면 새로고침 현상이 발생하지 않았다. 그래서 사진과 관련된 부분을 집중적으로 살펴보았으나, 대부분 UI 라이브러리와 Swiper를 사용한 코드였다.</p>
<blockquote>
<p><strong>가설 2</strong>
이미 메모리가 많이 사용 중인 상태에서 사진이 있는 팝업까지 더해져 한계치에 도달한 것이다.</p>
</blockquote>
<p>오류를 캡처한 영상을 검토한 결과, 페이지에서 스크롤을 내린 후 팝업 트리거를 클릭하자마자 새로고침이 발생했다. 이를 통해 메모리를 이미 많이 사용한 상태에서 팝업이 추가되면서 OOM(Out of Memory)이 발생한 것으로 추측했다.</p>
<p>페이지에서 팝업을 열지 않고 가만히 둔 상태로 메모리 검사를 진행해 보니 메모리 그래프가 꾸준히 증가하는 것을 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/b401ca12-b5e6-424f-9215-60cc2578dda6/image.png" alt=""></p>
<p>특히 메모리가 급격히 증가하는 부분을 분석해 보니, 디데이를 카운트다운하는 로직에서 문제가 발생하고 있었다.
<img src="https://velog.velcdn.com/images/moon-yerim/post/83d9f033-9aa2-4af8-b52e-7ce0370f4c86/image.png" alt=""></p>
<p>해당 로직의 일부이다. 1초마다 setInterval로 실행되며, innerHTML을 통해 DOM을 조작하는 방식이었다. 생략된 부분에선 타이머가 useEffect 내부에서 실행되며 클린업 함수도 작성되어 있다.</p>
<pre><code class="language-javascript"> const timer = setInterval(() =&gt; {
      if (timerElement) {
        const { time, dDay } = showRemaining();
        const timerNumbers = timerElement.querySelectorAll(&#39;.timer__number&#39;);
        const timerInfoToArray = Object.entries(time);
        const dDayElement = timerElement.querySelector(&#39;.d-day&#39;);

        if (dDayElement) {
          dDayElement.innerHTML = `${dDay}`;
        }

        Array.from(timerNumbers).forEach((item, index) =&gt; {
          item.innerHTML = `${timerInfoToArray[index][1]}`;
        });
      }
    }, 1000);</code></pre>
<p>문제는 여기서 innerHTML을 사용하면서 메모리 할당이 불필요하게 반복되었다는 점이다. 😱</p>
<blockquote>
</blockquote>
<ol>
<li><strong>기존 내용 삭제</strong>: <code>innerHTML</code>을 사용하면 해당 요소의 모든 기존 자식 노드가 삭제됩니다.</li>
<li><strong>새로운 DOM 트리 생성</strong>: 새로운 HTML 문자열을 파싱 하여 해당 요소의 새로운 자식 노드들이 생성되고, DOM 트리에 삽입됩니다.</li>
<li><strong>메모리 재할당</strong>: 이 과정에서 기존의 DOM 노드들은 해제되고, 새로운 DOM 노드들이 메모리에 할당됩니다.</li>
</ol>
<p>단순 텍스트만 수정하고 싶다면 기존의 노드를 새로 만들지 않고, 기존 텍스트 노드를 업데이트하는 방식으로 동작하는 <code>textContent</code> 또는 <code>innerText</code>를 사용하는 것이 더 효율적이라고 한다.(나는 사용하지 않았다.)</p>
<p>Next.js를 사용하기 때문에 DOM을 직접 조작할 필요가 없어 <code>상태로 관리</code>할 수 있도록 로직을 수정했다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/f4b6626b-d65f-48ff-91c1-b50820876f80/image.png" alt=""></p>
<p>또한, 매초 리렌더링이 발생하는 문제를 해결하기 위해 <code>React.memo()</code>를 사용해 자주 변하지 않는 일, 시간, 분 등의 값이 매번 렌더링 되지 않도록 최적화했다.</p>
<h3 id="비교">비교</h3>
<ul>
<li>수정전
<img src="https://velog.velcdn.com/images/moon-yerim/post/0a29d091-1dca-4510-ab10-553f3b417a3a/image.png" alt=""></li>
</ul>
<ul>
<li>수정후
<img src="https://velog.velcdn.com/images/moon-yerim/post/0351c5b2-7d0c-42a4-a94c-d1b2bf64bc00/image.png" alt=""></li>
</ul>
<p>JS 힙 메모리 그래프는 아직 개선이 필요하지만, 노드 메모리 사용량이 크게 감소하면서 문제를 많이 해결할 수 있었다.</p>
<h3 id="추가">추가</h3>
<p>JS 힙 메모리 최적화는 다음 글에서 해결되었다.
<a href="https://velog.io/@moon-yerim/next.js">mui 번들 크기 줄이기로 시작했으나 없애버리고 메모리 줄이기</a></p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://blog.eunsukim.me/posts/debugging-javascript-memory-leak-with-chrome-devtools">Chrome DevTools로 JS 메모리 누수(Memory Leak) 디버깅하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 디자인]]></title>
            <link>https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-%EB%94%94%EC%9E%90%EC%9D%B8</link>
            <guid>https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-%EB%94%94%EC%9E%90%EC%9D%B8</guid>
            <pubDate>Sat, 28 Sep 2024 09:30:13 GMT</pubDate>
            <description><![CDATA[<p>요즘 정말 많은 디자인 ai 서비스가 있다. 찾아보니 진짜 많음
무슨 툴을 쓰던지 처음 써보는 거라서 유튜브에서 봤던 서비스들 중에서 골라서 이것저것 써봤다.</p>
<h2 id="디자인">디자인</h2>
<h3 id="creatie">creatie</h3>
<p><a href="https://youtu.be/bI7ZfJMi6Pk?si=qtVoR8PCbIgjP-vf">100% 무료! 4개월 만에 1등 한 AI 디자인 툴은 세계 1위 피그마를 이길 수 있을까?</a></p>
<p>요즘 아주 핫하다고 해서 써봤다.
영역을 잡으면 해당 영역에 흔히 사용되는 ui를 생성해 주는 느낌이다.
빈 영역에서 시작해서 그런지 내가 필요한 요소에 엄청 적합하다 이런 느낌은 안 들었다. 그걸 하나하나 바꾸면 그냥 와이어 프레임을 고쳐서 디자인하는 거랑 다를 게 없다 싶어서 패스</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/895ebf2d-3075-48d9-90ab-8d679a9477c7/image.gif" alt=""></p>
<h3 id="uizard">uizard</h3>
<p>uizard는 프롬프트를 작성하면 앱 하나를 만들어주는 느낌으로 페이지별 ui를 촤르륵 작성해 준다.
근데 치명적인 게 너무 안 이쁨
디자인 상관없다! ai가 최대한 많이 만들어줘! 일단 만들고 본다!
이런 상황이면 오히려 더 빠를 수 있겠다 싶었으나, 나는 디자인을 보자마자 다른 서비스를 찾았다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/b5ac5e89-a9fc-486e-a4ca-1bff963aa661/image.png" alt=""></p>
<h3 id="galileo-ai">Galileo AI</h3>
<p><a href="https://youtu.be/_6k1k7NtZRI?si=PWgC76Xl8x6SfEWE">이제 AI랑 대화하면서 1분 만에 앱/웹 디자인이 가능하네..</a></p>
<p>테스트 후 현재 결제해서 사용 중인 서비스이다. 사용법이 미드저니랑 거의 유사해서 아주 쉽다!
그리고 무엇보다 디자인이 정말 깔끔하게 나온다.👍
<img src="https://velog.velcdn.com/images/moon-yerim/post/5f5353b0-0df2-4e33-8311-97a27c9ff0a1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/62be3da8-df97-4122-b115-7ad3ef35a95c/image.png" alt=""></p>
<p>진짜 디자인 너무 잘 나옴 대박 ai가 만든 디자인에서 색상 바꾸고, 서체 프리텐다드로 바꾸고, mvp 단계에서 구현할 기능만 남겼다.</p>
<p>미드저니처럼 나온 결과물을 다시 수정하고 할 수 있지만 복잡한 부분도 아닌데 원하는 대로 결과는 안 나오고 크레딧만 줄어드는 거 같아서 그냥 피그마에서 직접 수정했다.</p>
<p>간격, 여백, 글자 크기를 수정해야 하면 어렵고 오래 걸리는데,
이 부분이 잘 나와서 개발자의 사이드 프로젝트로 사용하기에 차고 넘친다.
이 서비스 발견해서 너무 행복하다.. 증말...🙇‍♀️</p>
<ul>
<li>다른 예시들도 엄청 깔끔하다.
<img src="https://velog.velcdn.com/images/moon-yerim/post/493eac4f-de71-48bb-81b5-4a3819974dfc/image.png" alt=""></li>
</ul>
<h2 id="스타일-가이드">스타일 가이드</h2>
<p>creatie에 ai가 스타일 가이드를 만들어주는 기능이 있다.
피그마 파일을 .fig 확장자로 내보내면 creatie에 바로 임포트 할 수 있다.(반대는 안됨🥲) 가져와서 기능을 사용해 봤다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/83674d26-dbfa-46af-b31a-84d79d24f459/image.gif" alt=""></p>
<p>이것도 어마어마함...</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/f9be7d39-fde3-4a57-a400-992af356c843/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/c2d4fa08-49b9-4fc7-ac02-0eb2d6de165e/image.png" alt=""></p>
<p>하지만 스타일 가이드는 디자이너가 디자인을 할 때 사용해 줘야 의미가 있는 건데 Gailieo에서 디자인을 하니까 과연 사용 가능할까 싶다. 컬러는 내가 수정할 때 사용하면 될 것 같기도🧐</p>
<h2 id="마무리">마무리</h2>
<p>Gailieo AI를 활용해 서비스를 사용해보니, 굳이 와이어프레임을 만들어야 했나 싶다. 와이어프레임이 있어서 이미지를 프롬프트에 사용하긴 했지만, 갈릴레오에 텍스트로 프롬프트를 작성하는 것과 비교해 볼 때, Wireframe Designer를 쓴다는 게 큰 차이가 있는지 의문이 들었다.</p>
<p>지금까지 AI가 제시한 단계를 따라 기획부터 디자인까지 진행해 봤다. 이렇게 AI로 기획하고 노코드 툴로 서비스를 만든다면, 정말 간단한 앱은 찍어낼 수 있지 않을까 하는 생각이 든다.😱 (아직 사용해 보진 않았지만, 그냥 떠올려본 생각이다.)</p>
<p>AI가 주는 가치는 그야말로 생산성의 혁명인 것 같다.</p>
<p><a href="https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-1">숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 서비스 기획</a>
<a href="https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-%ED%99%94%EB%A9%B4-%EA%B8%B0%ED%9A%8D">숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 화면 기획, 유저스토리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[js] 이벤트 타겟이 아닌 요소에 이벤트 발생시키기]]></title>
            <link>https://velog.io/@moon-yerim/js-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%83%80%EA%B2%9F%EC%9D%B4-%EC%95%84%EB%8B%8C-%EC%9A%94%EC%86%8C%EC%97%90-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%EC%83%9D%EC%8B%9C%ED%82%A4%EA%B8%B0</link>
            <guid>https://velog.io/@moon-yerim/js-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%83%80%EA%B2%9F%EC%9D%B4-%EC%95%84%EB%8B%8C-%EC%9A%94%EC%86%8C%EC%97%90-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%EC%83%9D%EC%8B%9C%ED%82%A4%EA%B8%B0</guid>
            <pubDate>Fri, 27 Sep 2024 10:34:58 GMT</pubDate>
            <description><![CDATA[<h2 id="구현해야-할-기능">구현해야 할 기능</h2>
<ul>
<li>배경으로 깔려있는 이미지의 위치를 드래그해서 이동할 수 있어야 한다.
<img src="https://velog.velcdn.com/images/moon-yerim/post/e0575f1f-f6cd-464c-9971-fc96ddc4aa06/image.gif" alt=""></li>
</ul>
<h2 id="구현">구현</h2>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/dc6349ba-ed39-4410-9dcb-b737dc8479ff/image.png" alt=""></p>
<p>두 요소가 겹쳐져 있어 배경 이미지는 클릭 이벤트가 발생할 수 없다. 하지만 이미지 위치를 조정하기 위해선 이미지에 드래그 이벤트를 발생시켜야 한다.</p>
<p>최상위 컨테이너 요소에 마우스 클릭 이벤트가 발생하면 배경 이미지에 드래그 이벤트를 발생시키면 될 것 같았다.</p>
<p>찾아보니 자바스크립트는 커스텀 이벤트를 생성하고 수동으로 이벤트를 발생시킬 수 있다. 🧐</p>
<h3 id="커스텀-이벤트-생성">커스텀 이벤트 생성</h3>
<p>드래그 이벤트를 생성해 준다. 이때 이벤트 객체도 직접 넣어줘야 한다.
이미지 위치를 옮기기 위해서 필요한 값들을 마우스(터치) 이벤트 객체에서 뽑아내서 전달했다.</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent">DragEvent: DragEvent() constructor
</a></li>
</ul>
<pre><code class="language-javascript">  const createCustomDragEvent = (
    event: MouseEvent | DragEvent | TouchEvent,
    eventType: &#39;dragstart&#39; | &#39;dragover&#39; | &#39;drop&#39;,
    dataTransfer: DataTransfer,
  ) =&gt; {
    let clientY;
    let clientX;

    if (event instanceof MouseEvent) {
      // 마우스 이벤트인 경우
      clientY = event.clientY;
      clientX = event.clientX;
    } else {
      // 터치 이벤트인 경우
      const touch = event.touches[0] || event.changedTouches[0];
      clientY = touch?.clientY;
      clientX = touch?.clientX;
    }

    return new DragEvent(eventType, {
      bubbles: true,
      cancelable: true,
      clientY,
      clientX,
      dataTransfer,
    });
  };</code></pre>
<h3 id="수동으로-이벤트-발생시키기">수동으로 이벤트 발생시키기</h3>
<ul>
<li><a href="https://developer.mozilla.org/ko/docs/Web/API/EventTarget/dispatchEvent">EventTarget.dispatchEvent()</a></li>
</ul>
<pre><code class="language-javascript">    const dataTransfer = new DataTransfer();

    dropzone.addEventListener(&#39;mousedown&#39;, (e) =&gt; {
      const dragStartEvent = createCustomDragEvent(
        e,
        &#39;dragstart&#39;,
        dataTransfer,
      );
      draggableImage.dispatchEvent(dragStartEvent);
    });

    draggableImage.addEventListener(&#39;dragstart&#39;, handleDragStart);
</code></pre>
<p>컨테이너 요소(=dropzone)에 mousedown 이벤트가 발생하면 draggableImage 요소에 dragStart 이벤트가 발생한다.</p>
<p>이미지를 이동하기 위해 dragstart, dragover, drop 이렇게 세 가지 이벤트를 사용해야 해서 동일한 방법으로 커스텀 이벤트를 수동으로 발생시켰다.</p>
<pre><code class="language-javascript">    // PC를 위한 마우스 이벤트
    dropZone.addEventListener(&#39;mousedown&#39;, (e) =&gt; {
      const dragStartEvent = createCustomDragEvent(
        e,
        &#39;dragstart&#39;,
        dataTransfer,
      );
      draggableImage.dispatchEvent(dragStartEvent);
    });

    document.addEventListener(&#39;mousemove&#39;, (e) =&gt; {
      const dragOverEvent = createCustomDragEvent(e, &#39;dragover&#39;, dataTransfer);
      dropZone.dispatchEvent(dragOverEvent);
    });

    document.addEventListener(&#39;mouseup&#39;, (e) =&gt; {
      const dropEvent = createCustomDragEvent(e, &#39;drop&#39;, dataTransfer);
      dropZone.dispatchEvent(dropEvent);
    });

    // 모바일을 위한 터치 이벤트
    dropZone.addEventListener(
      &#39;touchstart&#39;,
      (e) =&gt; {
        if (e.target === dropZone || dropZone.contains(e.target as Node)) {
          e.preventDefault();
        }

        const dragStartEvent = createCustomDragEvent(
          e,
          &#39;dragstart&#39;,
          dataTransfer,
        );
        draggableImage.dispatchEvent(dragStartEvent);
      },
      { passive: false },
    );

    document.addEventListener(
      &#39;touchmove&#39;,
      (e) =&gt; {
        if (e.target === dropZone || dropZone.contains(e.target as Node)) {
          e.preventDefault();
        }

        const dragOverEvent = createCustomDragEvent(
          e,
          &#39;dragover&#39;,
          dataTransfer,
        );
        dropZone.dispatchEvent(dragOverEvent);
      },
      { passive: false },
    );

    document.addEventListener(&#39;touchend&#39;, (e) =&gt; {
      const dropEvent = createCustomDragEvent(e, &#39;drop&#39;, dataTransfer);
      dropZone.dispatchEvent(dropEvent);
    });

    //이미지 위치를 이동하는 드래그 이벤트
    draggableImage.addEventListener(&#39;dragstart&#39;, handleDragStart);
    dropZone.addEventListener(&#39;dragover&#39;, handleDragOver);
    dropZone.addEventListener(&#39;drop&#39;, handleDrop);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[react] 카카오맵 사용 시 메모리 누수 잡기]]></title>
            <link>https://velog.io/@moon-yerim/react-%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@moon-yerim/react-%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Fri, 27 Sep 2024 08:17:38 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>카카오맵을 사용하는 부분에서 메모리가 터지는 에러를 발견했다.</p>
<p>기존 코드를 살펴보니 useEffect 내부에 카카오맵 객체를 생성하는 코드가 모두 함께 있었다.</p>
<p>그리고 의존성 배열에 값을 추가해 사용자 입력값이 바뀌면 useEffect가 재실행 되면서 매번 객체가 생성되도록 작성되어 있었다. 언마운트 할 때 참조 해제도 되어있지 않았다.</p>
<pre><code class="language-javascript">  useEffect(() =&gt; {
    const kakao = (window as any).kakao;

    if (!kakao) return;

    kakao.maps.load(function () {
      // 주소-좌표 변환 객체 생성
      const geocoder = new kakao.maps.services.Geocoder();

      const mapContainer = document.getElementById(&#39;&#39;),

      // 지도 생성
      const map = new kakao.maps.Map(mapContainer!, mapOption);

      const renderMap = () =&gt; {
        //좌표 생성
        const coords = new kakao.maps.LatLng(latitude, longitude);

        // 표시할 마크 생성
        const marker = new kakao.maps.Marker({});

        // 장소 인포 생성
        const infowindow = new kakao.maps.InfoWindow({});
        infowindow.open(map, marker);
        };
    });
  }, [a, b, c]);</code></pre>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/22b38b93-3376-42a2-a630-65c575e83e28/image.gif" alt=""></p>
<p>입력이 바뀔 때마다 무한 생성 중...</p>
<h2 id="수정-1">수정 1</h2>
<p>지도 객체를 여러 번 생성할 필요가 없었기 때문에 useRef로 객체를 저장하고 의존성 배열을 비워 한 번만 생성되도록 코드를 수정했다.</p>
<pre><code class="language-javascript">  const infoWindow = useRef&lt;kakao.maps.InfoWindow | null&gt;(null);
  const marker = useRef&lt;kakao.maps.Marker | null&gt;(null);
  const map = useRef&lt;kakao.maps.Map | null&gt;(null);
  const coords = useRef&lt;kakao.maps.LatLng | null&gt;(null);
  const zoomControl = useRef&lt;kakao.maps.ZoomControl | null&gt;(null);
  const mapContainer = useRef&lt;HTMLDivElement&gt;(null);

 useEffect(() =&gt; {
    if (!window.kakao) return;

    window.kakao.maps.load(() =&gt; {
      const MapContainer = mapContainer.current;

      coords.current = new kakao.maps.LatLng();
      marker.current = new window.kakao.maps.Marker();
      infoWindow.current = new window.kakao.maps.InfoWindow();
      map.current = new window.kakao.maps.Map();
      zoomControl.current = new kakao.maps.ZoomControl();
    });

    return () =&gt; {
      marker.current?.setMap(null);
      infoWindow.current?.close();
      marker.current = null;
      map.current = null;
      infoWindow.current = null;
    };
  }, []);</code></pre>
<h2 id="수정-2">수정 2</h2>
<p>메모리 누수가 발생하던 코드는 입력값이 바뀌면 카카오맵과 관련된 모든 객체를 새로 생성하도록 되어있었다.
카카오맵에서 제공되는 api로 객체를 새롭게 생성할 필요 없이 값을 변경할 수 있다.</p>
<p>그래서 api들을 사용하고 각각의 입력값 별로 useEffect를 분리해 동작할 수 있게 수정했다.</p>
<pre><code class="language-javascript">  useEffect(() =&gt; {
    if (name &amp;&amp; map.current &amp;&amp; infoWindow.current) {
      infoWindow.current?.setContent(infoWindowTemplate(name));
    }
  }, [name]);

  useEffect(() =&gt; {
    if (level &amp;&amp; map.current) {
      map.current?.setLevel(level);
    }
  }, [level]);

  useEffect(() =&gt; {
    if (coordinates &amp;&amp; map.current &amp;&amp; marker.current &amp;&amp; infoWindow.current) {
      const moveLatLon = new kakao.maps.LatLng(
        coordinates.latitude,
        coordinates.longitude,
      );
      map.current?.setCenter(moveLatLon);
      marker.current?.setPosition(moveLatLon);
      infoWindow.current?.open(map.current, marker.current);
    }
  }, [coordinates]);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 화면 기획, 유저스토리]]></title>
            <link>https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-%ED%99%94%EB%A9%B4-%EA%B8%B0%ED%9A%8D</link>
            <guid>https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-%ED%99%94%EB%A9%B4-%EA%B8%B0%ED%9A%8D</guid>
            <pubDate>Mon, 16 Sep 2024 18:45:33 GMT</pubDate>
            <description><![CDATA[<p>나는 크게 서비스 기획도 ui/ux 디자인도 공부해 본 적이 없다. (과거에 옥외광고물 디자인을 2~3년 했었고, 디자인을 조금 좋아한다.)</p>
<p>개발을 해야 하는데 기획이 없으면 뭘 만들어야 할지 모르겠고, 디자인을 해야 하는데 뭐부터 시작할지 정리가 안돼서 와이어프레임을 그려봤고, 팀원들끼리 핀트가 안 맞다 싶으면 유저스토리를 작성했다. 그러다 보니 알음알음 찾아보고 간단하게 작성해 본 정도였다.</p>
<p>거의 그런 식이었다. 그래서 이번 프로젝트를 할 때 ai를 사용해 보고 싶었다. 결국은 필요한 저 과정들을 ai가 도와주면 빨리할 수 있을 것 같았다. (특히 ui 생각할 땐 너무 힘들다. 안 써본 종류의 서비스라면 더더욱 🥲)</p>
<h2 id="1-사이트맵">1. 사이트맵</h2>
<pre><code>화면 기획 구성
- 홈 화면 (Dashboard)
- 사용 시간 상세 화면 (Usage Details)
- 목표 설정 화면 (Goal Setting)
- 알림 관리 화면 (Notification Management)
- 리포트 및 분석 화면 (Report &amp; Analytics)
- 설정 화면 (Settings)</code></pre><p>기획안을 토대로 gpt가 구성해 준 페이지들은 위와 같았는데 결과적으로 총 4페이지로 정했다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/611a3e61-0376-4b6a-89d0-5105d82c8094/image.png" alt=""></p>
<pre><code>홈 화면 (Dashboard)
- 오늘의 총 사용 시간
- 앱별 사용 시간 리스트
- 중독 경고 알림
- 이동: 통계 화면, 목표 설정 화면, 설정 화면

통계 화면 (Statistics)
- 기간 선택 필터 (일간, 주간, 월간)
- 사용 시간 그래프 (막대 그래프 또는 원형 그래프)
- 앱별 사용 시간 리스트
- 이동: 홈 화면, 목표 설정 화면, 설정 화면

목표 설정 화면 (Goal Setting)
- 일일/주간 목표 시간 설정 (슬라이더)
- 목표 설정 상태 (시각적 표시)
- 알림 옵션 (토글 버튼)
- 이동: 홈 화면, 통계 화면, 설정 화면

설정 화면 (Settings)
- 테마 설정 (다크 모드, 라이트 모드)
- 데이터 동기화 옵션 (주기 설정)
- 앱 정보 및 도움말
- 이동: 홈 화면, 통계 화면, 목표 설정 화면</code></pre><h2 id="2-와이어프레임">2. 와이어프레임</h2>
<p><a href="https://maily.so/tipster/posts/829653b7">🔥 AI를 활용해 만드는 와이어프레임은 어떤 모습일까?
</a></p>
<ul>
<li>사이트맵, 유저 플로우, 와이어프레임을 한 번에 만들 수 있는, <code>Flowmapp</code></li>
<li>서비스 소개를 입력하면 와이어프레임을 만들어주는, <code>Wireframe Designer</code></li>
</ul>
<p>원래 시작부터 와이어프레임을 그리려다 <code>Flowmapp</code> 서비스를 사용하고자 사이트맵을 작성한 거였다. 그런데 사용법을 잘 몰라서 <code>Wireframe Designer</code>로 계획 변경 🤯</p>
<h3 id="플러그인이-만들어준-와이어프레임">플러그인이 만들어준 와이어프레임</h3>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/3d88d7f8-3ad9-4d0b-b369-6cc8e5be9d7a/image.png" alt=""></p>
<p>그래도 작성한 사이트맵이 요긴하게 쓰였다. 디자인하기 전에 와이어프레임이 정리를 위해 필요한 것처럼 사이트맵은 와이어프레임 전에 생각 정리를 위해 필요한가 보다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/ad178aeb-d466-46ba-89a6-158b5841897d/image.png" alt=""></p>
<h3 id="수정된-와이어프레임">수정된 와이어프레임</h3>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/8c5aafd9-148d-4b0c-8da8-8abf2a8aa0d7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/b0e8d211-2b9e-42da-965d-711d9549317d/image.png" alt=""></p>
<p>많이 수정된 듯하지만 ai가 만들어준 와이어프레임 덕분에 훨씬 빨리 수정할 수 있었다. 심지어 오토 레이아웃까지 다 적용돼 있기 때문에 수정할 때 정말 편하다. 컴포넌트들도 깔끔해서 뭔가... 이대로 만들어도 안 되나? 하는 생각도 했다. (직접 만든 요소도 있다.)</p>
<h2 id="3-유저스토리">3. 유저스토리</h2>
<p>와이어프레임을 작성하고 깨달았다. 유저플로우랑 유저스토리 작성 안 했다는걸...🫣
디자인할 때 에러 케이스 처리할 수 있게 만들어둬야지! 하고 언제 에러 케이스 넣을지 보려 했더니 유저스토리가 없네...ㅎㅎ 그래서 와이어프레임 수정하면서 자꾸 이 기능 필요한가 아닌가 고민했나 보다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/60f9efa5-b3a3-4242-b791-6f0381e9b55e/image.png" alt=""></p>
<p>gpt가 작성해 준 유저스토리에 와이어프레임을 보면서 내용을 추가했다.
최소 기능만 남겼다고 생각했는데 와이어프레임을 작성해 보니 통계 기능은 당장 필요한 것 같다는 생각이 들지 않아서 우선순위를 뒤로 미뤘다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/1a5165a8-d96a-4f79-8010-5ad6a931bc35/image.png" alt=""></p>
<h2 id="마무리">마무리</h2>
<p>유저플로우는 앱이 복잡하지 않아서 그런지 유저스토리를 적고 나니 크게 필요하단 생각이 들지 않아 작성하지 않았다.</p>
<p>정답은 없겠지만 보통 유저스토리나 유저플로우를 작성하고 화면 기획에 들어가는 경우가 많다고 하던데,</p>
<p>나는 유저스토리가 필요한 목적이 기획보다 구현할 기능 목록이라 그런지 화면을 보면서 유저스토리를 적게 되는 것 같다. 화면 보면서 할 일 정리하는 느낌?</p>
<p>이젠 ai와 함께하는 디자인 차례!🤩</p>
<p><a href="https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-1">숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 서비스 기획</a>
<a href="https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-%EB%94%94%EC%9E%90%EC%9D%B8">숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 디자인</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 서비스 기획]]></title>
            <link>https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-1</link>
            <guid>https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-1</guid>
            <pubDate>Mon, 16 Sep 2024 10:59:29 GMT</pubDate>
            <description><![CDATA[<p>할 건 많은데 한 번 시작하면 끊을 수 없는 숏폼... 시작하면 3시간 순삭 되는 숏폼 퇴치를 위한 앱을 만들어 보자.</p>
<p>이 앱의 시작은 지극히 나의 개인적인 경험에서 시작되었다.</p>
<ol>
<li>숏폼에 빠져 누워있었다.</li>
<li>전화가 왔다.</li>
<li>전화를 마쳤다.</li>
<li>할 일을 하러 갔다.</li>
</ol>
<p>이런 경험이 몇 번 있었다. 숏폼에 빠져있다 언젠가 잘못 맞춰둔 알람이 울려서 정신을 차리고, 어떤 앱의 푸시 알림이 떠서 정신 차리게 되는 경험이</p>
<p>그래서 숏폼에 빠져있을 때 정신을 차릴 수 있는 계기를 만들자! 해서 시작되었다.
그리하여 이름이 정신 차려....</p>
<p>빠른 기획을 위해 gpt와 함께 합니다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/e9708e6e-c7d4-4430-9866-c854867e5274/image.png" alt=""></p>
<pre><code>---------------gpt가 알려준 기획 과정---------------

1. 문제 정의
2. 타겟 사용자 정의
3. 핵심 기능 선정
4. 경쟁 서비스 분석
5. 기술 스택 선정
6. 와이어프레임 &amp; 프로토타입 제작
7. MVP(최소 기능 제품) 설정
8. 비즈니스 모델 구상</code></pre><h2 id="1문제-정의">1.문제 정의</h2>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/9a5bc9b1-b809-4a03-a7ca-0016770862a8/image.png" alt=""></p>
<h3 id="사용자-불편-및-니즈-발견">사용자 불편 및 니즈 발견</h3>
<p><strong>(GPT)</strong>
사용자들은 숏폼 콘텐츠를 시청할 때, 자신이 생각했던 것보다 더 오랜 시간을 소비하고 있으며, 이로 인해 일상적인 일정에 차질이 생기거나 생산성이 떨어지는 문제를 겪고 있다.</p>
<p><strong>(나)</strong>
특히 퇴근하고 나서 에너지 소모가 커 자기 통제력이 떨어진 시간대에 중독에서 빠져나오기 힘들다. 잠을 늦게 자서 다음날 일상에 영향을 미치기도 함</p>
<ul>
<li>숏폼 서비스 중 시간제한 설정 기능이 있다.
  틱톡, 유튜브</li>
<li>숏폼 서비스 중 시간제한 설정 기능이 없다.
  인스타그램, 네이버
  (다만, 운영체제에서 제공하는 사용 시간제한 기능이 있음.)</li>
</ul>
<ol>
<li>각 앱마다 따로 설정을 해야 하는 것이 불편하다. 한 곳에서 통합 관리했으면 함.</li>
<li>네이버 앱은 브라우저로 사용하기 때문에 시간제한 설정을 걸 수 없음</li>
</ol>
<h3 id="해결하고자-하는-목표">해결하고자 하는 목표</h3>
<p><strong>(GPT)</strong>
사용자가 스스로 숏폼 콘텐츠 중독에서 빠져나올 수 있도록 하는 계기 제공.
강제적이거나 일방적인 제한보다는 사용자가 스스로 선택하게끔 돕는 도구나 경험 제공.</p>
<p><strong>(나)</strong>
서비스를 사용하지 못하게 하기에는 유튜브가 단순 흥미 위주의 콘텐츠뿐만 아니라 지식을 얻는 창고로 사용되는 경우도 많으며, 네이버의 경우 브라우저로 사용하고 있기에 앱 사용을 막을 순 없음.
보통 콘텐츠를 보면서 즐거우나 한 편으로는 할 일을 미루고 있다는 죄책감이 들 때가 있음 이 죄책감을 시각화해 충격을 줘 숏폼 시청을 중단하는 것이 목표.</p>
<h2 id="2-타겟-사용자-정의">2. 타겟 사용자 정의</h2>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/2a4541ac-b7a3-442b-a871-168a322c6858/image.png" alt=""></p>
<h3 id="주-타겟-사용자-그룹">주 타겟 사용자 그룹:</h3>
<blockquote>
<p><strong>숏폼 콘텐츠 소비가 많은 젊은 층</strong></p>
</blockquote>
<ul>
<li>연령대: 16~30세</li>
<li>직업:  학생, 대학생, 직장 초년생</li>
<li>디지털 기기 사용 패턴: 스마트폰을 자주 사용하며, 주로 짧은 시간에 - 여러 콘텐츠를 빠르게 소비함.</li>
<li>특징: 숏폼 콘텐츠의 즉각적인 자극(도파민 분비)으로 인해 쉽게 중독되고, 시간 관리에 어려움을 겪음.</li>
<li>니즈: 효과적인 시간 관리, 생산성 향상, 중독에서 벗어나기 위한 지원.</li>
</ul>
<blockquote>
<p><strong>디지털 웰빙에 관심이 있는 사람들</strong></p>
</blockquote>
<ul>
<li>연령대: 25~40세</li>
<li>직업: 직장인, 프리랜서</li>
<li>디지털 기기 사용 패턴: 하루 대부분을 컴퓨터와 스마트폰을 사용하는 직업군으로, 주기적인 휴식이나 디지털 디톡스를 원함.</li>
<li>특징: 기술에 친숙하며, 자신의 스마트폰 사용 패턴에 대해 의식하고 있음. 숏폼 콘텐츠를 시간 가는 줄 모르고 소비하는 습관에서 벗어나고 싶어함.</li>
<li>니즈: 생산성 향상, 더 나은 시간 관리, 집중력 유지.</li>
</ul>
<h3 id="페르소나-예시">페르소나 예시</h3>
<blockquote>
<p><strong>김민준 (20세, 대학생)</strong></p>
</blockquote>
<ul>
<li>배경: 대학에 다니는 김민준은 틈날 때마다 인스타그램과 틱톡에서 숏폼 영상을 본다. 시간 가는 줄 모르고 보고 있다 보면 중요한 과제를 미루거나 수면 시간이 줄어드는 경우가 많다.</li>
<li>목표: 숏폼 콘텐츠 중독에서 벗어나 시간을 효율적으로 사용하고 싶지만, 스스로 조절이 어렵다고 느낀다.</li>
<li>니즈: 일정 시간이 지나면 중독되기 전에 알려주거나, 자극적인 영상에서 잠시 벗어나 집중할 수 있도록 돕는 기능.</li>
</ul>
<blockquote>
<p><strong>이서영 (32세, 마케터)</strong></p>
</blockquote>
<ul>
<li>배경: 마케팅 업무를 하는 이서영은 일과 중에도 자주 인스타그램 릴스를 보곤 한다. 업무에 집중해야 할 시간이 많지만, 짧은 휴식이 길어지는 경우가 많아 생산성이 떨어진다고 느낀다.</li>
<li>목표: 자신의 숏폼 콘텐츠 시청 시간을 더 잘 관리하고, 효율적인 휴식 시간을 가질 수 있는 방법을 원한다.</li>
<li>니즈: 숏폼 영상을 일정 시간 동안 시청한 후 스스로 멈출 수 있는 경고나, 콘텐츠 소비 시간을 관리할 수 있는 도구.</li>
</ul>
<h2 id="3-핵심-기능-선정">3. 핵심 기능 선정</h2>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/f1438261-c4a6-425c-a6ba-cf6352ae90d3/image.png" alt=""></p>
<h3 id="1-사용-시간-모니터링-및-시각화">1. 사용 시간 모니터링 및 시각화</h3>
<ul>
<li><p>기능 설명: 사용자가 숏폼 콘텐츠를 시청하는 시간을 실시간으로 모니터링하고, 시각적으로 보여주는 기능.</p>
</li>
<li><p>기능 특징:
사용자는 자신이 콘텐츠를 몇 시간 동안 소비했는지 확인 가능.</p>
</li>
<li><p>사용자에게 주는 가치: 사용자가 자신이 얼마나 많은 시간을 숏폼 콘텐츠에 투자했는지 인지하게 함으로써 중독성을 자각하게 도와줌.</p>
</li>
<li><p>기술 구현 예시: 앱 내 대시보드를 통해 실시간 사용 시간을 그래프나 차트로 시각화하여 제공.</p>
</li>
</ul>
<h3 id="2-일정-시간-경과-후-푸시-알림">2. 일정 시간 경과 후 푸시 알림</h3>
<ul>
<li><p>기능 설명: 사용자가 일정 시간 동안 숏폼 콘텐츠를 소비했을 때 알림을 통해 휴식 또는 콘텐츠 소비 중단을 유도.</p>
</li>
<li><p>기능 특징:
사용자가 스스로 시간을 설정하거나, 기본적으로 30분 또는 1시간 단위로 알림을 설정 가능.
푸시 알림 클릭 시 즉시 앱 종료 옵션, 잠시 휴식 모드로 전환 옵션,
n 분 후 알림 옵션 제공</p>
</li>
<li><p>사용자에게 주는 가치: 사용자가 시간을 잊고 콘텐츠에 몰입했을 때, 휴식을 취하거나 사용 습관을 개선할 수 있는 계기 제공.</p>
</li>
<li><p>기술 구현 예시: Firebase Cloud Messaging 또는 Apple Push Notification을 통해 알림 제공.</p>
</li>
</ul>
<h2 id="기능-세부-정의">기능 세부 정의</h2>
<h3 id="사용-시간-모니터링-및-시각화-기능-동작-정의">사용 시간 모니터링 및 시각화 기능 동작 정의</h3>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/848091d1-9bab-4cd4-b79a-0ae623ae0158/image.png" alt=""></p>
<ol>
<li><p>사용 시간 모니터링
 앱 사용 시간 추적</p>
<ul>
<li><p>앱 포그라운드 및 백그라운드 감지: 앱이 포그라운드로 전환될 때부터 사용 시간을 추적하기 시작합니다. 앱이 백그라운드로 전환되면 사용 시간을 중지하고, 경과된 시간을 기록합니다.</p>
</li>
<li><p>사용 시간 기록: 사용 시간이 기록될 때마다 경과된 시간을 누적하여 저장합니다.</p>
</li>
</ul>
</li>
<li><p>알림 트리거</p>
<ul>
<li>특정 조건(예: 연속 사용 30분 경과, 하루 목표 시간 초과 등)을 감지하여 사용자에게 알림을 보냅니다.</li>
</ul>
</li>
<li><p>데이터 시각화
 시각적 요소</p>
<ul>
<li><p>앱 별 사용 시간: 가장 많이 사용된 앱의 순위와 사용 시간을 표시합니다.</p>
</li>
<li><p>홈 화면: 오늘의 총 사용 시간과 가장 많이 사용한 앱을 시각화하여 보여줍니다.</p>
</li>
<li><p>상세 화면: 사용자가 특정 기간(일간, 주간)을 선택하면 해당 기간의 사용 시간 데이터를 차트로 표시합니다.</p>
</li>
</ul>
</li>
<li><p>사용자 인터랙션</p>
<ul>
<li>사용자가 직접 목표 시간을 설정하거나 변경할 수 있도록 UI를 제공합니다.</li>
</ul>
</li>
</ol>
<h3 id="푸시-알림-기능-동작-정의">푸시 알림 기능 동작 정의</h3>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/e605cc10-b918-431a-ac97-0e30588b2c1a/image.png" alt=""></p>
<ol>
<li><p>시간 경과에 따른 중독 경고 알림</p>
<ul>
<li><p>일정 시간이 지나면 알림을 통해 사용자가 오랫동안 숏폼 콘텐츠에 몰입되어 있다는 사실을 알려줌.</p>
</li>
<li><p>알림은 단순 경고가 아닌, 사용자의 행동을 변화시킬 수 있는 유도형 메시지로 설계.</p>
</li>
</ul>
</li>
<li><p>알림 타이밍 설정
 사용자 맞춤형으로 알림을 보낼 수 있도록 설계    </p>
<ul>
<li><p>일정 시간 동안 연속으로 숏폼 콘텐츠를 시청할 때 (예: 30분, 1시간 등)</p>
</li>
<li><p>사용자가 설정한 목표 시간(예: 하루 1시간)을 초과했을 때</p>
</li>
</ul>
</li>
</ol>
<blockquote>
<p>ex)</p>
</blockquote>
<ul>
<li>&quot;30분 동안 숏폼 영상을 시청 중입니다. 오늘 할 일이 있었지 않나요? 👀&quot;</li>
<li>&quot;1시간이 지나가고 있어요! 정신 차리세요! ⛔📵 &quot;</li>
<li>&quot;벌써 n 번째 알림을 무시했어요! 이제부터 5분마다 알려드릴게요! ⏰&quot;</li>
</ul>
<ol start="3">
<li><p>푸시 알림 디자인</p>
<ul>
<li>시각적 요소를 포함하여 사용자에게 즉각적인 경각심을 줄 수 있도록 이미지, 텍스트와 더불어 아이콘, 이모티콘을 사용해 감정적으로도 자극</li>
</ul>
</li>
<li><p>푸시 알림의 동작</p>
<ul>
<li><p>숏폼 서비스에 접속하면 설정한 시간을 알려주는 알림을 보낸다.</p>
</li>
<li><p>사용자가 설정한 사용 기간이 지나면 알림을 보내기 시작한다.</p>
</li>
<li><p>3번까진 다시 알림을 위한 선택권을 준다.</p>
<blockquote>
<p>ex) 30분 후, 10분 후</p>
</blockquote>
</li>
<li><p>3번이 지나면 다음 주기를 줄여 알림 빈도를 높인다.</p>
<blockquote>
<p>ex) 10분 -&gt; 5분 -&gt; 1분</p>
</blockquote>
</li>
</ul>
</li>
</ol>
<h2 id="5-기술-스택-선정">5. 기술 스택 선정</h2>
<blockquote>
<p><strong>필수조건</strong></p>
</blockquote>
<ul>
<li>javascript 사용</li>
<li>react 생태계와 호환이 많이 될수록 좋음</li>
</ul>
<p>내가 만들고 싶어서 만드는 프로젝트지만 업무 외적으로 이것저것 적용해 보며 공부하려는 이유가 있기 때문에 필수조건이 있었다. Flutter가 쉽다고 들었지만 앱이라면 React Native여야 한다!</p>
<ul>
<li>백엔드: Supabase (관계형 데이터베이스 접해보고 싶음)</li>
<li>프론트엔드: React Native</li>
<li>상태 관리: 미정</li>
<li>데이터 페칭 및 동기화: React Query(공부하고 싶어서)</li>
<li>개발 환경 및 배포: Expo (RN치면 Expo 밖에 안 나옴...정보 많은 걸 선택)</li>
<li>ui 라이브러리: 미정 (사용한다면 Gluestack?)</li>
</ul>
<h2 id="마무리">마무리</h2>
<p>몇 단계가 빠졌는데 내 목적에선 크게 중요한 기획 영역이 아닌 것 같아서 간단하게 넘어갔다.</p>
<blockquote>
</blockquote>
<p><strong>4. 경쟁 서비스 분석</strong></p>
<ul>
<li>찾아본 바로는 어플을 못쓰게 하는 건 많은데 이렇게... 강력하게 정신차리라는 문구로 알림을 보내는 어플은 없는 것 같다. 세션으로 사용 시간을 알려주는 앱은 있었다.</li>
<li>사용해 보고 싶은 기술을 써보자 + 내 입맛에 맞는 어플을 만들자 하는 목적이 크다 보니 경쟁 서비스 분석보단 어떻게 만들어야 할지 찾아봤다.<blockquote>
</blockquote>
</li>
<li><em>6. 와이어프레임 &amp; 프로토타입 제작*</em></li>
<li>ai 디자인 서비스 사용해 보고 싶다. 다음 디자인 편에서 이야기할 예정<blockquote>
</blockquote>
</li>
<li><em>7. MVP(최소 기능 제품) 설정*</em></li>
<li>이미 최소 기능만 남겨둔 상태라고 생각한다. (GPT가 더 많은 기능을 알려줬었다.)<blockquote>
</blockquote>
</li>
<li><em>8. 비즈니스 모델 구상*</em></li>
<li>당장은 학습용 개인 프로젝트의 목적이 커 스토어에 올릴 계획이 없다.</li>
</ul>
<p>GPT가 제시해 준 내용을 정리해서 적었지만 내용을 다 적다 보니 엄청 길어졌다.👻</p>
<p>혼자 프로젝트하면서 이렇게 페르소나까지 생각해 본 건 처음인 것 같다.
스토어에 올리지 않을 프로젝트 이렇게까지 기획해서 뭐하나 싶기도 했지만,
왜 이 앱을 만들고 싶었는지 뭘 만들고 싶은지 생각을 정리하는 데 도움이 되었다.</p>
<p>다음은 ai와 함께 디자인을 시도해볼 예정!</p>
<p><a href="https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-%ED%99%94%EB%A9%B4-%EA%B8%B0%ED%9A%8D">숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 화면 기획, 유저스토리</a>
<a href="https://velog.io/@moon-yerim/%EC%88%8F%ED%8F%BC-%EC%A4%91%EB%8F%85%EC%9D%84-%EB%81%8A%EC%9E%90%EC%A0%95%EC%8B%A0%EC%B0%A8%EB%A0%A4-%EA%B0%9C%EB%B0%9C-with-ai-%EB%8F%99%EB%A3%8C-%EB%94%94%EC%9E%90%EC%9D%B8">숏폼 중독을 끊자(정신차려!) 개발 with ai 동료 - 디자인</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[html] 오픈그래프를 지정했는데 인스타그램에서 보이지 않을 때]]></title>
            <link>https://velog.io/@moon-yerim/html-%EC%98%A4%ED%94%88%EA%B7%B8%EB%9E%98%ED%94%84%EB%A5%BC-%EC%A7%80%EC%A0%95%ED%96%88%EB%8A%94%EB%8D%B0-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8%EC%97%90%EC%84%9C-%EB%B3%B4%EC%9D%B4%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C</link>
            <guid>https://velog.io/@moon-yerim/html-%EC%98%A4%ED%94%88%EA%B7%B8%EB%9E%98%ED%94%84%EB%A5%BC-%EC%A7%80%EC%A0%95%ED%96%88%EB%8A%94%EB%8D%B0-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8%EC%97%90%EC%84%9C-%EB%B3%B4%EC%9D%B4%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C</guid>
            <pubDate>Wed, 04 Sep 2024 09:19:21 GMT</pubDate>
            <description><![CDATA[<p>분명 오픈그래프를 설정해뒀는데 인스타그램에서 링크 공유 시 미리 보기가 보이지 않는다는 문의를 받았다.</p>
<p>카카오톡에 공유할 때는 잘 된다. 근데 인스타그램에서 공유하면 주소만 띡 하고 보내짐...</p>
<ul>
<li><p>카카오톡 링크 공유 (카카오로 공유하기 X, 링크 복붙)
<img src="https://velog.velcdn.com/images/moon-yerim/post/3ba95cfb-3bf5-4fcc-b88e-bf168cd47e0f/image.png" alt=""></p>
</li>
<li><p>인스타그램 링크 공유
<img src="https://velog.velcdn.com/images/moon-yerim/post/2f51e055-1226-49e3-afb3-23857a5d4c22/image.png" alt=""></p>
</li>
</ul>
<p>그래서 <a href="https://developers.facebook.com/tools/debug/">페이스북 공유디버거</a>를 확인해봤더니 og:image는 인식을 못하고 있었다. description도 지정되있는데 못가져오는듯 하고.. 아래 사진의 빨간 박스 쳐둔 부분을 보면 이유를 알려주는데 크롤링 하는 봇을 우리 사이트에서 허용을 안해둬서 생긴 문제인 것 같았다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/c22c378c-b254-4031-bdc7-1c5ce5c55deb/image.png" alt=""></p>
<h2 id="해결">해결</h2>
<p>robots.txt에 페이스북 크롤링 봇을 허용해 주는 설정을 추가했다.</p>
<pre><code class="language-txt">User-agent: facebookexternalhit
Allow: /

User-agent: Facebot
Allow: /</code></pre>
<p>이제 잘 나온다(만족)</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/10578f98-b079-4804-b6e6-1a502ae7d433/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next.js] getStaticProps는 페이지 컴포넌트에서만 동작한다]]></title>
            <link>https://velog.io/@moon-yerim/next.js-getStaticProps%EB%8A%94-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C%EB%A7%8C-%EB%8F%99%EC%9E%91%ED%95%9C%EB%8B%A4</link>
            <guid>https://velog.io/@moon-yerim/next.js-getStaticProps%EB%8A%94-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C%EB%A7%8C-%EB%8F%99%EC%9E%91%ED%95%9C%EB%8B%A4</guid>
            <pubDate>Mon, 26 Aug 2024 12:07:34 GMT</pubDate>
            <description><![CDATA[<p>getStaticProps를 사용해 새로운 페이지를 구현하고 있었다. 그런데 값이 props로 넘어오지 않았다. 다른 곳에서 사용하는 코드와 동일한 방법인데...... 왜........ 처음엔 값이 안 넘어오는 줄 알았지만 터미널에 콘솔조차 찍히지 않길래 not working으로 검색했더니... 이런...
<img src="https://velog.velcdn.com/images/moon-yerim/post/e20eec04-2529-43e4-8d0b-3f93e7391b10/image.png" alt=""></p>
<p><a href="https://stackoverflow.com/questions/72315270/nextjs-getstaticprops-not-working-in-my-component">https://stackoverflow.com/questions/72315270/nextjs-getstaticprops-not-working-in-my-component</a></p>
<p>두둥</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next.js, middleware] public 디렉토리 내부 소스의 응답이 307이 뜰 때]]></title>
            <link>https://velog.io/@moon-yerim/next.js-public-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EB%82%B4%EB%B6%80-%EC%86%8C%EC%8A%A4%EC%9D%98-%EC%9D%91%EB%8B%B5%EC%9D%B4-307%EC%9D%B4-%EB%9C%B0-%EB%95%8C</link>
            <guid>https://velog.io/@moon-yerim/next.js-public-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EB%82%B4%EB%B6%80-%EC%86%8C%EC%8A%A4%EC%9D%98-%EC%9D%91%EB%8B%B5%EC%9D%B4-307%EC%9D%B4-%EB%9C%B0-%EB%95%8C</guid>
            <pubDate>Sun, 25 Aug 2024 15:17:32 GMT</pubDate>
            <description><![CDATA[<p>해결법은 무척 간단하다. 미들웨어에 public디렉토리를 거치지 않도록 설정해 주면 된다.</p>
<p>처음에 middleware를 내가 설정한 게 아니라 생각을 못 하고 next.js 관련된 이슈를 찾으려 했는데 나와 똑같은 문제를 올려둔 <code>next-auth</code>의 깃허브 이슈를 발견했다.</p>
<ul>
<li><a href="https://github.com/nextauthjs/next-auth/issues/8578">307 Status Code(Temporary Redirect) on Logo in singin Page</a></li>
</ul>
<p>웃긴 게 제목이 내 상황과 100%로 일치ㅋㅋㅋㅋㅋㅋ
나도 헤더 컴포넌트를 수정하는데 로고 이미지가 로그인이 된 상태에서는 잘 나오는데 로그아웃만 하면 이미지가 나오지 않았고 확인해 보니 응답이 307이 뜨고 있었다.</p>
<p>깃허브 이슈에서는 이미지 파일을 추가하라는데 나는 public 디렉토리를 추가해 줬다.</p>
<pre><code class="language-javascript">export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - public
     * 미들웨어를 거치지 않는 경로들
     */
    &#39;/((?!public|$).*)&#39;,
  ],
};
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next.js, react-hook-form] 폼 입력 페이지 이탈 방지 기능 구현]]></title>
            <link>https://velog.io/@moon-yerim/next.js-react-hook-form-%ED%8F%BC-%EC%9E%85%EB%A0%A5-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%9D%B4%ED%83%88-%EB%B0%A9%EC%A7%80-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@moon-yerim/next.js-react-hook-form-%ED%8F%BC-%EC%9E%85%EB%A0%A5-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%9D%B4%ED%83%88-%EB%B0%A9%EC%A7%80-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 25 Aug 2024 15:02:55 GMT</pubDate>
            <description><![CDATA[<p>폼 입력 페이지에서 사용자가 작성 중인 내용을 실수로 잃어버리는 것은 끔찍한 경험이다. 우리 프로젝트는 길면 1시간도 넘게 폼을 사용하기 때문에 더더욱 필요한 부분이었다.
이런 상황을 방지하기 위해, 사용자가 입력하던 내용을 실수로 날려버리지 않도록 이탈 방지 기능을 구현했다.</p>
<p>고려해야 할 시나리오는 다음과 같다.
**</p>
<ol>
<li>브라우저 닫기: 사용자가 브라우저를 닫으려 할 때.</li>
<li>페이지 새로고침: 사용자가 페이지를 새로고침할 때.</li>
<li>페이지 이동: 사용자가 다른 페이지로 이동할 때 (예: 뒤로 가기, 앞으로 가기, URL 직접 입력 등).</li>
</ol>
<p>**</p>
<h2 id="beforeunload로-페이지-이탈-방지하기">beforeunload로 페이지 이탈 방지하기</h2>
<p>먼저 beforeunload 이벤트를 사용하면 1번과 2번을 시나리오를 막을 수 있다. 이 이벤트는 사용자가 페이지를 떠나려고 할 때 브라우저가 경고 메시지를 표시하도록 한다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/3ef650f5-fb7f-4317-9c1f-380c20699a0a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/984ce9dd-53d1-4427-a760-6650e573720f/image.png" alt=""></p>
<pre><code class="language-javascript">  useEffect(() =&gt; {
    const handleBeforeunload = (e: {
      preventDefault: () =&gt; void;
      returnValue: string;
    }) =&gt; {
      e.preventDefault();
      e.returnValue = &#39;&#39;;
    };

   window.addEventListener(&#39;beforeunload&#39;, handleBeforeunload);

    return () =&gt; {
      window.removeEventListener(&#39;beforeunload&#39;, handleBeforeunload);
    };
  }, []);</code></pre>
<p>여기까지 막으면 다 막혔다고 생각했지만 SPA에서는 끝이 아니다!</p>
<h2 id="spa에서-페이지-이탈-방지하기">SPA에서 페이지 이탈 방지하기</h2>
<p>우리 프로젝트의 폼 입력 페이지에는 제작 내역 페이지로 이동하는 next.js의 <code>Link</code>를 사용한 버튼이 있다. 위의 <code>beforeunload</code>로 기능을 구현하고 페이지 이동을 했더니 이벤트가 동작하지 않는다!</p>
<p>SPA를 풀어쓰면 단일 페이지 앱! 당연히 페이지 이동이 없다. 그래서 이벤트가 발생이 안된다.
Next.js에서 제공하는 router의 이벤트를 사용해서 페이지 이동을 감지할 수 있다.</p>
<pre><code class="language-javascript">  useEffect(() =&gt; {
    const handleBlockPopState = ({ url }: { url: string }) =&gt; {
      if (router.asPath !== url) {
        window.history.pushState(null, &#39;&#39;, router.asPath);
      }
      return true;
    };

    const handleBeforeHistoryChange = () =&gt; {
      if (
        window.confirm(
          &#39;변경사항이 저장되지 않을 수 있습니다. 사이트에서 나가시겠습니까?&#39;,
        )
      ) {
        router.events.emit(&#39;routeChangeComplete&#39;);
      } else {
        router.events.emit(&#39;routeChangeError&#39;);
      }
    };

    const handleRouteChangeError = () =&gt; {
      throw &#39;Route change aborted.&#39;;
    };

      router.events.on(&#39;beforeHistoryChange&#39;, handleBeforeHistoryChange);
      router.events.on(&#39;routeChangeError&#39;, handleRouteChangeError);
      router.beforePopState(handleBlockPopState);

    return () =&gt; {
      router.events.off(&#39;beforeHistoryChange&#39;, handleBeforeHistoryChange);
      router.events.off(&#39;routeChangeError&#39;, handleRouteChangeError);
      router.beforePopState(() =&gt; true);
    };
  }, []);</code></pre>
<h3 id="handlebeforehistorychange">handleBeforeHistoryChange</h3>
<p><a href="https://nextjs.org/docs/pages/api-reference/functions/use-router#routerevents">Next.js 공식문서 router.events</a></p>
<blockquote>
<p>beforeHistoryChange(url, { shallow }) - Fires before changing the browser&#39;s history(브라우저 히스토리가 변경되기 전에 발동)</p>
</blockquote>
<p>beforeHistoryChange 이벤트 발생 시에 컨펌창을 띄우고, 컨펌 창의 값에 따라 뒤로 가기를 마저 완료하거나 에러를 일으키도록 한다.</p>
<h3 id="handleblockpopstate">handleBlockPopState</h3>
<p>뒤로 가기를 하고 팝업을 띄우면 <code>beforeHistoryChange</code>에 잡히기 전 Url이 바뀌어버려서 취소를 눌러도 뒤로 가기 한 후의 주소가 나타난다. 이를 방지하기 위한 함수이다.</p>
<ul>
<li><p>localhost:3000로 페이지는 그대로인데 url이 바뀌어 버림
<img src="https://velog.velcdn.com/images/moon-yerim/post/797f7a35-7c89-466c-a394-bb81f4a035c8/image.png" alt=""></p>
</li>
<li><p>localhost:3000/editor 현재 페이지의 url을 유지
<img src="https://velog.velcdn.com/images/moon-yerim/post/45144317-b46f-44c6-aa1a-6f1b593712a4/image.png" alt=""></p>
</li>
</ul>
<h3 id="handleroutechangeerror">handleRouteChangeError</h3>
<p>팝업에서 취소를 눌렀을 때 에러를 던져 페이지 이동을 막기 위한 함수이다. 이 함수가 없다면 취소를 눌러도 뒤로 가기가 동작해버린다.</p>
<h2 id="폼-입력에-변경사항이-있을-때만-뒤로-가기-방지-기능이-동작하도록-하기">폼 입력에 변경사항이 있을 때만 뒤로 가기 방지 기능이 동작하도록 하기</h2>
<p>입력한 게 없다면 폼을 이탈해도 무방하다. 이를 위해 커스텀 훅에서 외부로부터 불리언 값을 받아, 해당 값에 따라 이벤트를 발생시킬지 여부를 결정하도록 추가로 구현한다. 이 불리언 값은 입력값이 변경되었는지를 나타낸다.</p>
<pre><code class="language-javascript">const usePreventFormExit = (isEnabled = true) =&gt; {
  const router = useRouter();

  useEffect(() =&gt; {
    const handleBeforeunload = (e: {
      preventDefault: () =&gt; void;
      returnValue: string;
    }) =&gt; {
      e.preventDefault();
      e.returnValue = &#39;&#39;;
    };

    const handleBlockPopState = ({ url }: { url: string }) =&gt; {
      if (router.asPath !== url) {
        window.history.pushState(null, &#39;&#39;, router.asPath);
      }
      return true;
    };

    const handleBeforeHistoryChange = () =&gt; {
      if (
        window.confirm(
          &#39;변경사항이 저장되지 않을 수 있습니다. 사이트에서 나가시겠습니까?&#39;,
        )
      ) {
        router.events.emit(&#39;routeChangeComplete&#39;);
      } else {
        router.events.emit(&#39;routeChangeError&#39;);
      }
    };

    const handleRouteChangeError = () =&gt; {
      throw &#39;Route change aborted.&#39;;
    };

    if (isEnabled) {
      window.addEventListener(&#39;beforeunload&#39;, handleBeforeunload);
      router.events.on(&#39;beforeHistoryChange&#39;, handleBeforeHistoryChange);
      router.events.on(&#39;routeChangeError&#39;, handleRouteChangeError);
      router.beforePopState(handleBlockPopState);
    } else {
      window.removeEventListener(&#39;beforeunload&#39;, handleBeforeunload);
      router.events.off(&#39;beforeHistoryChange&#39;, handleBeforeHistoryChange);
      router.events.off(&#39;routeChangeError&#39;, handleRouteChangeError);
      console.log(window.history);
      console.log(router.asPath);
      router.beforePopState(() =&gt; true);
    }

    return () =&gt; {
      window.removeEventListener(&#39;beforeunload&#39;, handleBeforeunload);
      router.events.off(&#39;beforeHistoryChange&#39;, handleBeforeHistoryChange);
      router.events.off(&#39;routeChangeError&#39;, handleRouteChangeError);
      router.beforePopState(() =&gt; true);
    };
  }, [isEnabled]);
};</code></pre>
<p>현재 프로젝트에서는 react-hook-form을 사용하고 있어 <code>isDirty</code>값을 사용해 판단한다. formState에 다른 상태들도 있어 고려해 봤는데 isDirty로 충분하다 판단했다.</p>
<pre><code class="language-javascript">const isEnabled =
    methods.formState.isDirty &amp;&amp; !methods.formState.isSubmitting;

usePreventFormExit(isEnabled);</code></pre>
<h2 id="마무리">마무리</h2>
<p>처음에 beforeunload만 달면 끝난다!라고 단순히 생각했으나 왜 안돼? 가 계속 발생한 폼 이탈 방지...잡을 수 있는 부분은 최대한 잡아서 구현했는데 빠진 게 있다면 알려주시면 감사드립니다..🙇‍♀️</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://1ilsang.dev/posts/use-prevent-leave">페이지 이탈시 확인 컨펌창 만들기</a></li>
<li><a href="https://dev.to/juanmtorrijos/how-to-add-the-changes-you-made-may-not-be-saved-warning-to-a-nextjs-app-with-react-hook-form-3ibh">How to add the &quot;Changes you made may not be saved&quot; warning to a Next.js app with React Hook Form</a></li>
<li><a href="https://keeper.tistory.com/58">Next router 이동 막기 _알림창</a></li>
<li><a href="https://blog.hwahae.co.kr/all/tech/9249">React Hook Form의 isDirty와 dirtyFields를 알아보자</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[css] 여백은 어느 컴포넌트에 적용해야 할까?]]></title>
            <link>https://velog.io/@moon-yerim/CSS-%EC%97%AC%EB%B0%B1%EC%9D%80-%EC%96%B4%EB%8A%90-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@moon-yerim/CSS-%EC%97%AC%EB%B0%B1%EC%9D%80-%EC%96%B4%EB%8A%90-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 29 Jul 2024 10:48:24 GMT</pubDate>
            <description><![CDATA[<h2 id="여백은-어느-컴포넌트에-적용하는-것이-맞을까">여백은 어느 컴포넌트에 적용하는 것이 맞을까?</h2>
<p>UI 구현을 하다 보면 컴포넌트에 여백(margin)을 어떻게 적용해야 할지 고민이 될 때가 많았다. 특히 재사용 할 컴포넌트라면 사용하는 위치에 따라 여백이 달라져 수정을 하게되는 경험이 많아서 CSS를 작성할 때마다  고민이 되는 부분이었다.</p>
<p>예를 들어 A 페이지에서는 20px의 여백이 필요하지만, B 페이지에서는 10px 필요한 경우이다.
<img src="https://velog.velcdn.com/images/moon-yerim/post/e3546843-dc0c-46d8-87f6-1dda51796c3e/image.jpg" alt=""></p>
<p>처음에는 컴포넌트에 직접적으로 여백을 적용한 뒤, 다른 페이지에서 재사용할 경우 해당 페이지의 다른 컴포넌트로 여백을 옮기곤 했다.
<img src="https://velog.velcdn.com/images/moon-yerim/post/8647fd57-0b80-4785-a66a-fc9d73a039a2/image.jpg" alt=""></p>
<p>이 방식의 문제는 여백을 옮긴 컴포넌트 1, 컴포넌트 4도 어딘가에서 재사용될 경우 발생한다.
컴포넌트 1이 재사용 된다면 여백을 또 어디로 옮겨야 할까?</p>
<h2 id="발전1">발전1</h2>
<p>정말 정말 처음에는 컴포넌트 내부에 css로 여백을 선언하다 이런 상황을 몇 번 겪고는 컴포넌트를 호출할 때 여백을 props로 넘겨주기로 했다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/151922dc-affa-41b4-82cf-ed7e77f74a52/image.jpg" alt=""></p>
<p>타입스크립트에서 컴포넌트를 선언할 때 <code>HTMLElement</code>의 type들을 상속받으면 className을 직접 선언하지 않아도 props로 넘겨줄 수 있기 때문에 이 방법이 유효했다. 그런데 모든 컴포넌트가 <code>HTMLElement</code> 타입을 상속받지 않을 수 있다. 그런 경우에는 또 어디에 여백을 주냐 하는 고민이 생겼다.</p>
<h2 id="발전2">발전2</h2>
<p>이런 케이스가 생기다 보니 컴포넌트 자체에 여백을 직접 주지 않는 방법을 생각해 봤다. A 페이지 B 페이지마다 여백이 다르다는 건 그 여백들은 해당 페이지 내부에서만 필요한 정보(?)라고 생각했다.</p>
<p>그러다 Container, Presentation 패턴을 보고 Presentation 컴포넌트를 만들어 배치, 여백 관련 스타일을 적용하면 되겠다는 생각이 들었다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/1d47af4b-ec55-4fe7-aae1-f9b84724b877/image.jpg" alt=""></p>
<pre><code class="language-jsx">const SomePage = () =&gt; (
    &lt;div className=&quot;max-w-[980px] mx-auto mt-6 mb-16&quot;&gt;
      &lt;div className=&quot;px-4&quot;&gt;
        &lt;div className=&quot;mb-4&quot;&gt;
          &lt;TypographyH4&gt;제목&lt;/TypographyH4&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div className=&quot;mt-4&quot;&gt;
        &lt;MyComponent/&gt;
      &lt;/div&gt;
    &lt;/div&gt;
);</code></pre>
<p>이렇게 레이아웃 관련 스타일을 적용하는 컴포넌트를 만들어서 사용했다. 그러니 또 다른 페이지를 구현해도 그 페이지를 위한 Presentation 컴포넌트를 만들어 사용하면 되고 기존 재사용 컴포넌트는 수정할 필요가 없어져 유지 보수가 쉬워졌다.😎</p>
<p>유지 보수가 유리해진 건 좋은데 단순 스타일을 위한 태그가 너무 많아지는 것 아닌가 하는 고민이 되긴 한다. 또한 Container, Presentation 패턴을 사용하면 파일이 많아지고 코드 베이스가 복잡해질 수도 있다고 하는데 이 부분은 프로젝트가 커질 경우라 현재 우리 프로젝트에서는 문제보단 유지 보수가 용이해진 장점이 더 크다.</p>
<p>결론적으로, 현재 프로젝트에서는 컴포넌트의 여백 같은 레이아웃 관련 부분을 부모 요소, Presentation 컴포넌트에 위임하는 방식으로 코드를 작성하려 한다. 추가적으로 패턴에 대한 공부를 더 해볼 필요성이 느껴진 고민이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발 환경] 모바일 브라우저에서 디버깅 하기]]></title>
            <link>https://velog.io/@moon-yerim/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%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-%EB%94%94%EB%B2%84%EA%B9%85-%ED%95%98%EA%B8%B0ngrok</link>
            <guid>https://velog.io/@moon-yerim/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%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-%EB%94%94%EB%B2%84%EA%B9%85-%ED%95%98%EA%B8%B0ngrok</guid>
            <pubDate>Sat, 29 Jun 2024 07:20:21 GMT</pubDate>
            <description><![CDATA[<p>웹에서 디버깅을 하면 대부분 해결이 되어서 잘 쓰지 않았는데 모바일 환경이 중요한 서비스를 개발하다 보니 필요성이 느껴져 세팅을 해보았다.</p>
<ul>
<li><a href="https://www.youtube.com/watch?v=0B5pNAdxYA0">모바일 크롬에서 개발자 도구 사용하기</a></li>
</ul>
<p>연결 자체는 위 영상을 참고하면 된다. 연결하고 다른 서비스를 개발자 도구로 구경하는 것도 흥미롭다.</p>
<h3 id="ios-테스트">ios 테스트</h3>
<ul>
<li><a href="https://cishome.tistory.com/251">IOS safari로 개발자 도구 사용</a></li>
<li><a href="https://velog.io/@jyooj08/%ED%81%AC%EB%A1%AC%EC%97%90%EC%84%9C-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-IOS-%EC%9B%B9%EB%B7%B0-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0">크롬에서 간단하게 IOS 웹뷰 테스트하기
</a></li>
</ul>
<h2 id="베타-서버에-배포하지-않고-모바일에서-테스트하기">베타 서버에 배포하지 않고 모바일에서 테스트하기</h2>
<p>연결해도 로컬에서 개발 중이면 모바일에서 접속이 안되니까 <code>ngrok</code>을 이용해 외부에 로컬 서버를 공개해 접속할 수 있도록 해줘야 한다.</p>
<ul>
<li><a href="https://hudi.blog/ngrok/">ngrok - 포트포워딩 없이 외부에 로컬서버 공개하기</a></li>
</ul>
<p>사용법은 쏘이지 사이트에 사용법 따라 하자</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/9ace9cc5-64c3-493f-ad68-19f67b566151/image.png" alt=""></p>
<h2 id="문제">문제</h2>
<p>네이버 로그인, 카카오 맵 api는 등록해둔 url만 통신을 허용해 준다. cors 에러 발생!
ngrok을 그냥 사용하면 서버를 실행할 때마다 도메인이 계속해서 바뀌기 때문에 고정 도메인이 필요했다.</p>
<h2 id="해결">해결</h2>
<p>마침 ngrok은 static 도메인을 무려 무료 플랜에서도 하나 준다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/384bcc00-708e-414d-871f-3a9a2c979723/image.png" alt=""></p>
<p>New Domain 눌러서 도메인 하나 추가해 준다. 그리고 사용하는 api 사이트로 가서 이 도메인을 등록해 주면 끝!</p>
<pre><code>ngrok http --domain=도메인 로컬포트번호</code></pre><p>이 명령어로 실행하면 항상 동일한 도메인으로 실행된다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/312dac0e-b079-46bf-8787-a879282effb3/image.png" alt=""></p>
<p>테스트할 때마다 베타 서버를 사용했는데 QA 단계 전, 개발 도중 논의할 때 등 배포 없이 바로바로 확인하고 수정 가능해서 아주 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[html] 이미지 로딩 최적화(picture 태그, webp 확장자, lazy 로딩)]]></title>
            <link>https://velog.io/@moon-yerim/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@moon-yerim/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Tue, 04 Jul 2023 12:32:47 GMT</pubDate>
            <description><![CDATA[<p>페이지를 배포하고 나서 로딩 속도를 개선할 필요가 있음을 느꼈다. 바로 다음 작업에 들어가기도 해야 해서 당장 개선할 수 있는 부분이 이미지였다. 이미지 개선을 통해 로딩 속도를 개선한 내용을 공유하려 한다.</p>
<h2 id="원래-사용하던-방법">원래 사용하던 방법</h2>
<pre><code class="language-html">&lt;img class=&quot;m_none&quot; src=&quot;경로1&quot; alt=&quot;대체 텍스트&quot;&gt;
&lt;img class=&quot;m_show pc_none&quot; src=&quot;경로2&quot; alt=&quot;대체 텍스트&quot;&gt;</code></pre>
<p>기존에 회사에서 사용하던 방법이다. 이전에 사용하던 방법을 나도 따라서 사용했었다.</p>
<p>사실 데스크탑 사이즈에서는 <code>경로1</code>의 이미지만 필요하다. 하지만 위의 방법으로 작성된 코드는 <code>경로1</code>, <code>경로2</code>의 이미지를 모두 로드 받는다. 필요 없는 <code>경로2</code>를 받는 만큼 로딩이 느려진다.</p>
<p>랜딩 페이지는 이미지를 사용하는 부분이 많다. 그런 부분들이 모두 이렇게 작성되어 있다고 생각하면 로딩 속도는 훨씬 느려지게 된다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/f196e538-48e0-480a-b506-81745c0c154f/image.png" alt=""></p>
<h2 id="picture-태그">picture 태그</h2>
<p>picture 태그는 노마드 코더 유튜브에서 보게 됐다. 그리고 랜딩 페이지 작업할 때 다른 사이트들을 참고하는데 패스트캠퍼스를 보니 picture 태그를 사용하는 것을 보고 이번에 로딩 속도 개선을 위해 적용하면 좋겠다 싶었다.</p>
<p>picture 태그는 내부에 있는 source 태그들과 img 태그 중에서 조건에 맞는 이미지만 로드해 준다.
source 태그는 여러 개를 작성해 조건을 달아줄 수 있고, img 태그는 source 태그들의 조건을 모두 만족하지 못할 경우 default 이미지로 동작하게 된다.</p>
<ul>
<li>패스트캠퍼스 100가지 시나리오로 학습하는 프론트엔드 강의 랜딩 페이지에서 사용 중
<img src="https://velog.velcdn.com/images/moon-yerim/post/abe312d7-1ff7-4758-bce8-dd356b48b6b6/image.png" alt=""></li>
</ul>
<h2 id="webp-확장자">webp 확장자</h2>
<p>그리고 위 사진에 첨부된 이미지의 확장자를 보면 webp를 사용하고 있다.</p>
<p>webp는 구글에서 만든 이미지 확장자이다 png처럼 투명 배경을 지원하고 gif처럼 애니메이션도 지원한다.
webp 확장자는 png, jpg보다 용량을 2~30% 줄일 수 있다고 한다.</p>
<h2 id="사용가능한가-크로스-브라우징">사용가능한가? (크로스 브라우징)</h2>
<p>그래도 무작정 사용할 순 없으니 호환성이 얼마나 되는지 찾아봤다.
<img src="https://velog.velcdn.com/images/moon-yerim/post/eefeb1a0-55f7-4f82-aa75-fb8797afd919/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/107404b6-fb72-458b-bb45-18aac071f28e/image.png" alt=""></p>
<p>picture 태그는 can i use에서 96.5%의 브라우저에서 지원한다고 하고 webp는 96.6%가 지원한다고 한다.
그런데... webp 확장자에 대해 검색하면 safari에서 지원하지 않는다는 글이 많은 것이다...</p>
<p>더 찾아봤더니 ios14부터는 safari에서 webp를 지원한다고 한다. 그럼 ios14 이상의 점유율은 얼마인가?!</p>
<p>이 부분은 애플 공식 사이트에서 알려준다.</p>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/24011e55-14f3-440b-a81d-95220de48d17/image.png" alt=""></p>
<p>ios15~16이 98%, 그 외는 2%...라서 webp를 사용하기로 결정했다.
(작업할 땐 94%, 그 외 6%였는데 그새 점유율이 늘었나 보다.)</p>
<blockquote>
<p>참고로 아직 safari에서 애니메이션으로 webp를 사용하면 버벅임이 발생할 수 있다고 한다. 그래서 gif는 대체하지 않았다.</p>
</blockquote>
<h2 id="lazy-로딩">lazy 로딩</h2>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/0960784c-8b5e-4d1c-a8cb-234323d34806/image.gif" alt=""></p>
<p>laze 로딩은 말 그대로 로딩을 지연하는 것이다. 위 gif를 보면 스크롤을 내리면서 이미지가 로드가 되는데 처음부터 페이지 내의 모든 이미지를 로드 받는 것이 아니라 이미지가 필요할 때가 되어서야 로드를 받는 것이다.</p>
<h2 id="코드">코드</h2>
<p>적용하면 이런 모습이다. 좋은 게 source 태그에는 클래스나 alt를 적지 않아도 img 태그의 속성들로 적용된다.</p>
<pre><code class="language-html">&lt;picture&gt;
   &lt;source media=&quot;(max-width: 576px)&quot; srcset=&quot;경로1&quot;&gt;
   &lt;img class=&quot;content_img&quot; srcset=&quot;경로2&quot; alt=&quot;타이틀 이미지&quot; loading=&quot;lazy&quot;&gt;
&lt;/picture&gt;</code></pre>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/moon-yerim/post/b6637413-a21f-468d-a045-5c431b4551c3/image.png" alt=""></p>
<p>결과를 라이트 하우스로 측정해 봤다. <code>performance가 50 -&gt; 65</code>로 상승했다. 물론 65점도 개선할 부분이 많은 점수지만... 당장 할 수 있는 부분인 이미지만 최적화했는데도 15점이 올랐다.</p>
<p><code>LCP가 18.9s -&gt; 2.3s</code>로 향상된 걸 볼 수 있는데 이 부분이 가중치가 25%로 높은 편이라 performance 점수에 영향을 많이 미쳤나 보다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://helloinyong.tistory.com/297">웹 성능 최적화를 위한 Image Lazy Loading 기법</a></li>
<li><a href="https://velog.io/@dmchoi224/%EC%9B%B9-%EC%B5%9C%EC%A0%81%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%A0%81%EC%9A%A9">picture 태그를 사용한 브라우저 별 이미지 최적화</a></li>
<li><a href="https://seons-dev.tistory.com/entry/%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EA%B7%B9%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%A4%84%EC%9D%B4%EB%8A%94-%EB%B0%A9%EB%B2%95-WebP">포스팅을 할 때 사진을 극적으로 줄이는 완벽한 방법! WebP (단순 용량 압축 X )</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>