<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>heyday_xz.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 20 Mar 2026 08:16:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>heyday_xz.log</title>
            <url>https://velog.velcdn.com/images/heyday_xz/profile/541c1d9a-dfd9-4b54-b9e0-80454af28ade/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. heyday_xz.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/heyday_xz" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[결혼한지 2년도 넘었는데 아직도 모바일청첩장 만드는 사람 (부제: 스몰빅웨딩 모청 리뉴얼)]]></title>
            <link>https://velog.io/@heyday_xz/%EA%B2%B0%ED%98%BC%ED%95%9C%EC%A7%80-2%EB%85%84%EB%8F%84-%EB%84%98%EC%97%88%EB%8A%94%EB%8D%B0-%EC%95%84%EC%A7%81%EB%8F%84-%EB%AA%A8%EB%B0%94%EC%9D%BC%EC%B2%AD%EC%B2%A9%EC%9E%A5-%EB%A7%8C%EB%93%9C%EB%8A%94-%EC%82%AC%EB%9E%8C-%EB%B6%80%EC%A0%9C-%EC%8A%A4%EB%AA%B0%EB%B9%85%EC%9B%A8%EB%94%A9-%EB%AA%A8%EC%B2%AD-%EB%A6%AC%EB%89%B4%EC%96%BC</link>
            <guid>https://velog.io/@heyday_xz/%EA%B2%B0%ED%98%BC%ED%95%9C%EC%A7%80-2%EB%85%84%EB%8F%84-%EB%84%98%EC%97%88%EB%8A%94%EB%8D%B0-%EC%95%84%EC%A7%81%EB%8F%84-%EB%AA%A8%EB%B0%94%EC%9D%BC%EC%B2%AD%EC%B2%A9%EC%9E%A5-%EB%A7%8C%EB%93%9C%EB%8A%94-%EC%82%AC%EB%9E%8C-%EB%B6%80%EC%A0%9C-%EC%8A%A4%EB%AA%B0%EB%B9%85%EC%9B%A8%EB%94%A9-%EB%AA%A8%EC%B2%AD-%EB%A6%AC%EB%89%B4%EC%96%BC</guid>
            <pubDate>Fri, 20 Mar 2026 08:16:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>그러니까 나는 아직도 모바일청첩장을 만들고 있다.
<a href="https://github.com/heejin-hwang/mobile-wedding-invitation">내 결혼에 사용했던 청첩장</a>으로부터 시작해 <a href="https://invitation.smallbigwedding.kr/dearletter_sample">외주 청첩장</a>을 만들고, 그 청첩장을 최첨단 수동(..)으로 만들다가 최근 리뉴얼을 마친 이야기.</p>
</blockquote>
<p>🔗 서비스 구경하기: <a href="https://invitation.smallbigwedding.kr">스몰빅웨딩 청첩장 페이지</a></p>
<p>애석하게도 스몰빅웨딩 모바일 청첩장 ver.1은 최첨단 수동 방식이었다.
유저에게 폼으로 주문 정보를 받아, 내가 직접 제작했다. 나름 비용을 줄이겠다고 cli로 json을 만드는 웃픈 자동화도 했었다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/8cb158fc-4d95-4c03-abf5-e6862442d83f/image.gif" alt=""></p>
<p>유저가 수정을 원하면 개발자가 대기하고 있다가 수정해주는 꽤 비효율적인 구조였지만, 당시에는 그게 최선이었다.
경력 몇 년 되지 않은 프론트엔드 개발자가 백엔드, 인증/인가, 권한 분리까지 다루기에는 쉽지 않았기 때문이다.</p>
<h1 id="왜-리뉴얼을-시작했나">왜 리뉴얼을 시작했나</h1>
<p>수작업으로 제작하다 보니 가격은 높아질 수밖에 없었고, 고객도 많지 않았다.</p>
<p>그 사이 비슷한 형태의 청첩장들도 생겨났다.
디자인이 비슷하거나 문구가 그대로 복사된 경우도 있었다.</p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/69532731-426a-4343-b9fd-2b73988407ce/image.png" alt="">
이건 지금 봐도 황당하다. 사장님이 실제 청첩장에 사용했던 문구, 이름까지 그대로 베껴서, 카피를 해도 성의없게 카피했다고 생각했다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/d3b6da60-288c-4957-a0d3-5c1338d8a608/image.gif" alt="">
<img src="https://velog.velcdn.com/images/heyday_xz/post/2fc49faf-070b-44e7-9397-f1688ea162fb/image.gif" alt=""></p>
<p>이 케이스는 심지어, 나에게 외주를 문의했다가 안하기로 했던 분인데 이렇게 스몰빅웨딩 청첩장의 포인트를 고대로 베껴가서 황당 + 신기했다.</p>
<p>다른 길로 샜지만 다시 돌아와서, <strong>“새로운 청첩장이 넘쳐나는 시기에 비싼 가격에 자동화가 안되면 유저에게 어필하기가 어렵겠다.”</strong> 는 생각이 계속 들었다.</p>
<p>그래서 아무도(사장님도) 시키지 않았지만, 순전히 나를 위해 리뉴얼을 시작했다.</p>
<h1 id="리뉴얼-과정">리뉴얼 과정</h1>
<p>기존 코드는 말 그대로 JSON 데이터를 가져다가 “보여주기만 하는 화면”이었다.</p>
<p>차라리 처음 만들 때부터 AI로 딸깍 시작했으면 좋았을텐데, 이 프로젝트를 시작하던 시기에는 완전 휴먼 메이드 코드로 비즈니스 로직과 뷰 로직이 명확히 분리되지 않은 상태로 얽혀 있었다.</p>
<p>이번 리뉴얼의 가장 큰 목표는 (유저 눈에는 보이지 않겠지만)
<strong>preview와 실제 view에서 모두 사용할 수 있는 재사용 가능한 컴포넌트 구조로 분리하는 것</strong>이었다.</p>
<p>그리고 두 번째 목표는
<strong>유저가 직접 데이터를 생성하고 수정할 수 있도록 구조를 바꾸고, 이후 확장이 가능한 형태로 만드는 것</strong>이었다.</p>
<p>이 과정에서 가장 많은 시간이 들었고, 실제로 여러 번 엎었다.</p>
<p>그 외 기능적으로</p>
<ul>
<li>form 페이지를 구성했다. 데스크탑에서는 사이드 패널, 모바일에서는 bottom sheet에 미리보기가 가능하도록 구현했다.</li>
<li>생성된 데이터를 기반으로 수정 / 삭제 기능을 추가했다.</li>
<li>인증 기반으로 로그인 기능을 도입했다.</li>
<li>유저별 데이터 접근을 제어하기 위해 권한 분리를 적용했다.</li>
<li>운영을 위한 관리자 페이지를 별도로 구성했다.</li>
<li>사용자 행동을 확인하기 위해 GA와 Clarity를 연동했다.</li>
</ul>
<p>QA와 보안검수도 진행했다. 
나름 꼼꼼하게 본다고 했는데도 확인할수록 수정할 부분이 계속 나왔다.</p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/509df8e1-0f2b-45d8-9cfe-492f8ad5239b/image.gif" alt=""></p>
<h1 id="그래서-지금은">그래서 지금은?</h1>
<p>기존에는 단순히 view만 보여주던 화면이었는데,
리뉴얼하면서 login, mypage, 생성/수정 페이지, 삭제 기능이 생겼다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/b9ea91ef-a550-45da-beea-f14a748b8b79/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/396e30bf-a5d3-48bd-9480-965dbc91fcc6/image.gif" alt=""></p>
<p>로그인 버튼만 덩그러니 있던 페이지도 랜딩페이지처럼 꾸몄다. 
샘플 청첩장을 임베디드해서 실제 화면이 움직이는 것처럼 보이도록 만들었다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/5d835e66-cfa3-4e83-b693-0d8c914840e0/image.gif" alt=""></p>
<h1 id="마무리">마무리</h1>
<p>여전히 유저는 없다.</p>
<p>하지만 1년 넘게 마음속 부채처럼 쌓아두었던 일을 해냈다는 점에서 만족스럽다.</p>
<p>이번 프로젝트를 하며 더더욱 AI의 영향력을 느꼈다.
AI가 있어서 좋기도 하고, 한편으로는 위협처럼 느껴지기도 한다.
그래도 지금의 나에게는 도움이 되는 쪽이 더 크다.</p>
<p>코드를 읽지 못해도 개발할 수 있다는 이야기들이 많아졌지만,
코드를 읽을 수 있기에 수정이 훨씬 수월하고, 원하는 방향으로 채찍질하기도 쉽다.
그런 점에서 개발자는 AI 발전의 가장 큰 수혜자 중 하나가 아닐까 싶다.</p>
<p>🔗 서비스 구경하기: <a href="https://invitation.smallbigwedding.kr">스몰빅웨딩 청첩장 만들기 페이지</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도파민 싹 도는 프로젝트 후기🎶]]></title>
            <link>https://velog.io/@heyday_xz/%EB%8F%84%ED%8C%8C%EB%AF%BC-%EC%8B%B9-%EB%8F%84%EB%8A%94-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@heyday_xz/%EB%8F%84%ED%8C%8C%EB%AF%BC-%EC%8B%B9-%EB%8F%84%EB%8A%94-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 23 Jun 2025 01:48:36 GMT</pubDate>
            <description><![CDATA[<p>5월 24일부터 6월 3일까지 약 열흘간 운영한 대선 <a href="https://0603.mytinypage.xyz/">공약 매칭 서비스</a>가 총 2.5만 명의 방문자를 기록하며 마무리됐다.(아직 열려있어요🤓) 짧은 기간이었지만 생각보다 많은 관심을 받았고, 실험적으로 시도해본 여러 UX 요소나 표현 방식에 대해 꽤 많은 인사이트를 얻을 수 있었다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/19be7726-b2b8-42bc-a2c1-ecb31db2cce5/image.png" alt=""></p>
<h1 id="유저-피드백">유저 피드백</h1>
<p>서비스 운영 기간 동안 벨로그, 링크드인을 통해 다음과 같은 피드백을 받을 수 있었다:</p>
<ul>
<li>어려운 정책 용어에 대한 설명이 있었으면 좋겠다는 의견</li>
<li>남은 문항 수가 보이면 좋겠다는 요청 (반영 완료)</li>
<li>결과 화면에서 퍼센트보다는 후보별 공약과 연결된 정보가 더 직관적이었다는 제안 (일부 반영)</li>
<li>결과 비교 토글 UI 개선에 대한 구체적인 요청 (반영 완료)</li>
<li>택소노미 분류 체계에 대한 피드백</li>
</ul>
<p>실제로 반영할 수 있는 것들은 빠르게 수정했지만, 용어 설명처럼 시간이 더 필요한 개선 사항은 아쉽게도 이번에 반영하지 못했다. 택소노미에 대한 부분은 생각도 못했다. 다음 대선 때도 비슷한 프로젝트를 한다면 꼭 넣어야겠다. </p>
<h1 id="느낀-점">느낀 점</h1>
<h2 id="1-ga는-꼭-심자">1. GA는 꼭 심자</h2>
<p>매일 Google Analytics에 들어가 방문 수, 공유 수 등을 확인하면서 작은 도파민을 얻었다. 서비스의 실제 반응을 수치로 확인할 수 있다는 건 꽤 의미가 있었다.</p>
<h2 id="2-헷갈릴-때는-ab-테스트를-해보자">2. 헷갈릴 때는 A/B 테스트를 해보자</h2>
<p>사용자 흐름에 헷갈림이 있었던 구간에 A/B 테스트를 적용해봤다. 단순한 YES/NO 선택 UI와 중요도, 선호도를 복합적으로 체크하는 UI를 두고 고민했는데, A/B 테스트 후 둘 다 적용하게 되었다. 그리고 결과적으로는 대통령 선거라는 중요한 이벤트인만큼 유저들은 <strong>&#39;단순명확한 UI&#39;보다 &#39;정확한 결과&#39;</strong>를 더 선호했다. 실험해보지 않았다면 놓쳤을 사실이다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/73ec97c4-4555-4a09-9964-e36425986001/image.png" alt=""></p>
<h2 id="3-워딩의-힘">3. 워딩의 힘</h2>
<p>워딩, 후킹 포인트에 대한 고민을 많이 했다. 특히 &#39;정밀한 설문&#39;이라는 표현을 썼는데, 만약 &#39;복잡한 설문&#39;이나 &#39;어려운 설문&#39;이라고 표현했다면 유저들의 선택이 달라졌을지도 모르겠다. 작은 단어 하나가 사용자 흐름에 꽤 큰 영향을 줄 수 있다는 걸 다시 느꼈다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/15eee5d9-5f21-43f0-a4fd-0629d2e69475/image.png" alt=""></p>
<h1 id="마무리">마무리</h1>
<p>이번 대선은 국민들의 관심이 특히 뜨거웠던 선거였고, 그 덕분에 짧은 기간임에도 많은 사람들이 서비스를 찾아왔다. 매일 GA를 확인하면서 방문 수와 공유 수를 보는 재미가 있었고, 실험하고 개선하는 과정도 꽤 즐거웠다.</p>
<p>앞으로도 삶에 도움이 되면서도, 심미적으로 아름다운 서비스를 만들고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[나와 가장 가까운 후보는?🗳️]]></title>
            <link>https://velog.io/@heyday_xz/%EB%82%98%EC%99%80-%EA%B0%80%EC%9E%A5-%EA%B0%80%EA%B9%8C%EC%9A%B4-%ED%9B%84%EB%B3%B4%EB%8A%94</link>
            <guid>https://velog.io/@heyday_xz/%EB%82%98%EC%99%80-%EA%B0%80%EC%9E%A5-%EA%B0%80%EA%B9%8C%EC%9A%B4-%ED%9B%84%EB%B3%B4%EB%8A%94</guid>
            <pubDate>Sat, 24 May 2025 10:17:18 GMT</pubDate>
            <description><![CDATA[<p>고등학생인 제 동생은 이번에 선거권이 생겨 첫 투표를 하게 되었습니다.
설레어하며 누굴 뽑아야 할지 고민하더라구요.
그래서 동생을 위해 (만드는 김에 모두가 보면 좋은) <a href="https://pledge-tinder.vercel.app/">공약 매칭 서비스</a>를 만들었습니다.</p>
<h1 id="서비스-소개">서비스 소개</h1>
<p><strong>‘나와 가장 가까운 후보는?’</strong>은 틴더 앱처럼
공약을 하나씩 넘기며 동의/비동의 혹은 선호도를 입력하면
가장 가치관이 비슷한 후보를 매칭해주는 웹 서비스입니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/da5b43c9-dafe-4f16-890c-453f94a7cf34/image.png" alt="">
<strong>두 가지 설문 방식으로 준비해봤어요.</strong></p>
<ul>
<li>간단한 설문: 동의 / 비동의 중 선택하는 직관적인 방식</li>
<li>정밀한 설문: 중요도와 5단계 선호도를 함께 입력해 보다 정확한 결과를 도출하는 방식</li>
</ul>
<p>처음에는 한가지 방식으로 디벨롭하려고 친구들에게 A/B 테스트를 하다보니 직관적이고 편한건 전자인데, 신뢰도는 후자가 더 높아 선호도가 비슷하더라구요.
그래서 두 가지 설문 모드를 모두 제공하게 되었어요.
<img src="https://velog.velcdn.com/images/heyday_xz/post/94268ed6-22e4-4a4f-9acd-381e797af07e/image.png" alt=""></p>
<h1 id="공약-데이터는-이렇게-정리했어요">공약 데이터는 이렇게 정리했어요</h1>
<p>후보자들의 정책을 요약하고, 카테고리별로 비교 및 질문을 만들기 위한 구조화 작업을 했습니다.</p>
<p>먼저 <a href="https://policy.nec.go.kr/">중앙선거관리위원회 정책·공약마당</a>에서 후보자별 공약 데이터를 가지고 왔고, AI에게 각 후보가 말하는 공약이 어떤 카테고리인지 카테고리 정리를 먼저 시켰습니다.
(같은 카테고리인데 후보별로 카테고리를 다르게 설정했기 때문에..)
<img src="https://velog.velcdn.com/images/heyday_xz/post/a9c5ba5e-91bc-4897-bfdc-615ef54663e1/image.png" alt=""></p>
<p>통일된 카테고리로 만든 후 동일 주제에 대해 후보별 공통점과 차이점을 분석했습니다. 여기서 특별히 입장이 갈리는 항목에 대해 질문을 만들어내는 방식으로 데이터를 정제했습니다.</p>
<h1 id="기획--디자인-포인트">기획 &amp; 디자인 포인트</h1>
<p>너무 진지하거나 무거운 분위기보다는 모든 연령이 쉽게 접근하게 하기 위해 쉬운(?) UI/UX가 되도록 기획했어요.
글자도 깨지지 않는 선에서 크게 만들었어요.
처음 의도는 틴더앱처럼 스와이프해서 선택을 하는 것이었는데, 정밀한 방식은 화면에 버튼이 많다보니 스와이프가 어려워, 간단한 방식에만 스와이프가 적용되었어요.</p>
<p>TMI지만 제가 알록달록한 걸 좋아하는 사람이라, 곳곳에 원색을 포인트로 사용했습니다.</p>
<p>후보자들의 증명사진을 바탕으로 3d캐릭터를 ChatGPT에게 생성해달라고 하여 썸네일과 메인화면에 나란히 넣어주었습니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/42fdf8d8-7e74-4b4c-9258-9e1e83ce96c3/image.jpg" alt=""></p>
<h1 id="사용-기술">사용 기술</h1>
<ul>
<li>React + TypeScript</li>
<li>Vite로 번들링</li>
<li>Firebase로 Google Analytics 연동</li>
<li>Vercel로 배포</li>
</ul>
<h1 id="마무리">마무리</h1>
<p>이번 프로젝트는 특히 대부분의 코딩은 커서가 하고 저는 디테일한 부분을 입맛대로 수정하는 방식으로 진행했는데요. 
&#39;나는 이제 AI 없이 코딩 못하는 사람인가&#39; 라는 생각을 하면서도 그래도 이런 번뜩이는 아이디어는 인간에게서만 나올 수 있다며 스스로를 위로했답니다.
또 새롭고 재미있는 프로젝트를 진행하면 좋겠네요.</p>
<p>편하게 사용해보시고, 궁금한 점이나 개선 의견이 있다면 언제든지 알려주세요!
👉 <a href="https://pledge-tinder.vercel.app/">나와 가장 가까운 후보는?</a></p>
<hr>
<h3 id="-527-업데이트-사항">* 5/27 업데이트 사항</h3>
<ol>
<li>질문 일부를 업데이트했습니다.</li>
<li>설문 화면에 진행도를 추가했습니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/b0bf25eb-24f7-4dd2-b4f6-5ec13c8c87d0/image.png" alt=""></li>
<li>결과 화면에 1위 호부 사진 추가, 후보별 핵심 태그를 추가 했습니다. (사진은 예시 화면입니다.)
<img src="https://velog.velcdn.com/images/heyday_xz/post/24c40938-8e38-4063-94b6-3acd9edc66ca/image.png" alt=""></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트 회고: 재밌는 걸 만들면 재밌다.]]></title>
            <link>https://velog.io/@heyday_xz/%EC%BA%90%EB%A6%AD%ED%84%B0-fq0djff4</link>
            <guid>https://velog.io/@heyday_xz/%EC%BA%90%EB%A6%AD%ED%84%B0-fq0djff4</guid>
            <pubDate>Sun, 16 Mar 2025 08:44:11 GMT</pubDate>
            <description><![CDATA[<p>가볍게 발 걸치고 있는 <a href="https://www.smallbigwedding.kr/">스몰빅웨딩</a>과 한달간 👉🏻 <a href="https://customcharacter.smallbigwedding.kr/">커스텀 웨딩 캐릭터 만들기</a> 👈🏻 프로젝트를 진행했다.</p>
<p>이번 프로젝트의 목적은 <strong>브랜드 널리 알리기</strong>. 
개발은 100%, 기획과 마케팅 일부를 함께 했다.</p>
<h2 id="화면-상세">화면 상세</h2>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/fafe0789-71bd-4ff8-b441-bddc1e0797ba/image.png" alt="">
<img src="https://velog.velcdn.com/images/heyday_xz/post/2db56801-c3e5-4047-8a79-8a7f74829fb1/image.png" alt="">
<img src="https://velog.velcdn.com/images/heyday_xz/post/c5a5c239-d848-4b74-9a8e-7c54e5a4879a/image.png" alt=""></p>
<h2 id="사용한-개발도구">사용한 개발도구</h2>
<ul>
<li>프레임워크: 문신템 React와 TypeScript</li>
<li>번들러: Vite<ul>
<li>이번 프로젝트는 SSR이나 API 라우팅이 필요없는 순수 클라이언트 앱이라 Vite를 선택</li>
<li>Vercel에서 배포할 때 빌드~배포까지 20초도 안걸리는게 기분이 넘 조음..</li>
</ul>
</li>
<li>배포: Vercel</li>
<li>스타일링: 순수 CSS<ul>
<li>프로젝트 규모가 작고, 정적 이미지가 많고, 초기에 빠르게 렌더링 되어야 하기 때문에 별도 css 라이브러리를 사용하지 않았음.</li>
</ul>
</li>
</ul>
<h2 id="이번에-새로-시도해본-것">이번에 새로 시도해본 것</h2>
<h4 id="캔버스-이미지-합성--다운로드-기능"><strong>캔버스 이미지 합성 + 다운로드 기능</strong></h4>
<p>다 다른 레이어인데 캔버스에서 한장으로 합친다.
이번 프로젝트 아니었으면 몰랐을 것 같다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/15bb6918-7a99-4332-94e5-f9a2f77ead4f/image.png" alt=""></p>
<h4 id="ga-연동">GA 연동</h4>
<p>유입도 궁금하지만, 실제로 상품까지 어떻게 연결되는지 궁금해서 붙여봤다.
오픈 2일차인데 유입은 꽤 많은듯하다!
<img src="https://velog.velcdn.com/images/heyday_xz/post/9b4fb0e1-a3ca-4e01-aaa1-05ea4d9bd507/image.png" alt="">
<img src="https://velog.velcdn.com/images/heyday_xz/post/d74a3d91-0e55-4978-8b92-c89146606e96/image.png" alt=""></p>
<p>유입에 비해 상품 사이트로의 연결량은 아직 많지 않지만 그래도 광고 돌리지 않고 이정도라면 만족스럽다.</p>
<h2 id="배포-후-이슈들">배포 후 이슈들</h2>
<p>(코드상태 빼곤) 완벽하다고 생각했는데 오픈하고 발견한</p>
<h4 id="다운로드-안됨-이슈">다운로드 안됨 이슈</h4>
<p>오픈 후 링크를 쓰레드, 인스타그램에 올렸는데, 인앱브라우저로 오픈 시 다운로드가 안되는 이슈가 있었다. 이미 오픈은 했지만 급하게 기획을 변경해 외부 브라우저로 열도록 추가 안내를 했다. (깨알 디테일 링크 복사버튼 넣기)
<img src="https://velog.velcdn.com/images/heyday_xz/post/0e0b398b-bd00-4e6a-b296-c8ced3c7589d/image.png" alt=""></p>
<p>다음 프로젝트를 오픈할 때는 <strong>여는 환경에 대한 테스트</strong>도 잊지 말아야 겠다고 생각했다.</p>
<h4 id="complete-로-접근시-404이슈"><code>/complete</code> 로 접근시 404이슈</h4>
<p><code>/</code>가 아닌 하위 URL로 접근할 경우, Vercel이 404를 띄웠다.
찾아보니 SPA의 라우팅 처리가 아닌 해당 리소스를 찾으려고 하기 때문에 404 에러가 발생한다고 했다. 이럴 때는 Vercel에 내 SPA 앱의 인덱스 경로를 설정해 주면 된다.</p>
<p><a href="https://vercel.com/docs/project-configuration#rewrites">vercel rewrites</a>를 참고해 적용했다.
<code>/complete</code>로 접근해도 <code>/</code>로 가도록 수정했다.</p>
<h2 id="느낀점">느낀점</h2>
<p>커서랑 함께 개발했는데, 이제 혼자 개발하라고 한다면..?
경계가 되면서도 너무 편해서 안쓸 수가 없다.</p>
<p>다만, 그냥 커서가 적어주는대로 쓰면 내가 원하는 코드로 만들 수 없다.
코드의 품질을 유지하려면 검토하고 깨끗하게 다듬는 과정이 필수임을 다시 깨닫게 되었다.</p>
<p>이제 <del>커서랑 함께 만든 똥을 치우러</del> 리팩토링 하러 가야지..</p>
<p>기획 전반에 참여해서 더욱 재밌는 프로젝트였다.
선택 가능한 아이템을 꾸준히 업데이트해서 사용자 유입을 늘리고, 상품까지 연결되면 좋겠다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/8dea17c3-fe18-40b0-a487-8cb7723f59fb/image.gif" alt="">
<strong>예쁜 나만의 청첩장을 만들고 싶다면 <a href="https://www.smallbigwedding.kr/">스몰빅웨딩 홈페이지로 가기</a></strong>
*<em>귀여운 나만의 웨딩캐릭터를 만들고 싶다면 <a href="https://customcharacter.smallbigwedding.kr/">캐릭터 만들기 사이트로 가기</a> *</em></p>
<p><strong>많관부<del>~</del>😎😎</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Array.from() 의 재발견]]></title>
            <link>https://velog.io/@heyday_xz/JS-Array.from-%EC%9D%98-%EC%9E%AC%EB%B0%9C%EA%B2%AC</link>
            <guid>https://velog.io/@heyday_xz/JS-Array.from-%EC%9D%98-%EC%9E%AC%EB%B0%9C%EA%B2%AC</guid>
            <pubDate>Sun, 20 Oct 2024 14:51:33 GMT</pubDate>
            <description><![CDATA[<p>제가 개발한 청첩장에는 이모지 컨페티 기능이 있어요.</p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/8031682d-2e22-437e-9586-24071155b113/image.gif" alt=""></p>
<p>이 이모지들은 고객님들의 구글 폼에 <code>string</code> 으로 받고 있어요.
공백도 들어올 수 있었어요.
<img src="https://velog.velcdn.com/images/heyday_xz/post/8f9d833d-e483-4cf0-b6bf-2fd6156b043b/image.png" alt=""></p>
<p>주문이 별로 없었을 때는 요걸 하나하나 쪼개서 수동으로 <code>JSON</code> 형태로 만들었는데요.
[&quot;😀&quot;,&quot;🤓&quot;,&quot;🥰&quot;,&quot;😛&quot;,&quot;😎&quot;]</p>
<p>하다보니 이걸 수동으로 하고 있지 하는 생각이 드는 거에요. 
그래서 이모지 분리를 하는 페이지를 만들기로 했어요.</p>
<p><code>공백 제거</code>하고, <code>split</code>하면 되겠죠?</p>
<pre><code class="language-javascript">const stringToEmojiList = (input: string): string[] =&gt; {
        const noSpaceInput = input.replace(/\s/g, &#39;&#39;);
        return noSpaceInput.split(&#39;&#39;).filter(char =&gt; char.trim() !== &#39;&#39;);
};</code></pre>
<p><code>replace</code>로 중간에 들어올 수 있는 공백을 미리 <code>&#39;&#39;</code>로 변환하고, <code>split</code>해서 배열로 만들어봅니다.
그리고 <code>console.log(JSON.stringify(emojiList, null, 2))</code>으로 출력해봤더니..</p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/6729d0af-d4b5-4f80-9eb8-3ac45aa8d9ff/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/3ec49119-e919-4458-a2f3-232b8fbd11b8/image.png" alt=""></p>
<p>어라라 제가 원한건 이게 아닌데용..
유니코드 포인트가 개별적으로 쪼개지는 문제가 발생합니다.</p>
<p>gpt에게 도움을 요청했습니다.
<code>Array.from()</code>이라는 답을 줍니다.</p>
<p>이모지는 일반 문자가 아닌 유니코드로 표현되는 경우가 많기 때문에 단일 문자를 처리할 때처럼 할 수 없고, 이를 해결하기 위해 유니코드 서로게이트 페어를 올바르게 처리해야 합니다.</p>
<p>아래처럼 <code>Array.from()</code>을 사용하면 이모지 하나하나를 올바르게 분리하여 배열로 처리할 수 있습니다.</p>
<pre><code class="language-javascript">const stringToEmojiList = (input: string): string[] =&gt; {
        const noSpaceInput = input.replace(/\s/g, &#39;&#39;);
        return Array.from(noSpaceInput).filter(char =&gt; char.trim() !== &#39;&#39;);
};</code></pre>
<p>결과는?
<img src="https://velog.velcdn.com/images/heyday_xz/post/c05966d6-1a47-48e6-b487-58032ff91904/image.png" alt="">
오 잘 쪼개지네요.</p>
<h2 id="arrayfrom의-어떤-점-때문에-이런게-가능할까요">Array.from의 어떤 점 때문에 이런게 가능할까요?</h2>
<p><code>Array.from()</code>이 유니코드를 올바르게 처리할 수 있는 이유는, 이 함수가 <strong>유니코드 써로게이트 페어(surrogate pairs)</strong> 를 자동으로 인식하고 하나의 완전한 문자(이모지 등)를 하나의 요소로 처리하기 때문입니다.</p>
<h3 id="유니코드-써로게이트">유니코드 써로게이트</h3>
<p>페어란?
일반적인 문자는 하나의 코드 유닛(UTF-16 코드)으로 표현되지만, 이모지나 일부 복잡한 문자는 두 개의 코드 유닛을 합쳐 하나의 문자로 표현됩니다. 예를 들어, <code>😀</code> 이모지는 실제로 두 개의 코드 유닛으로 구성됩니다:</p>
<ul>
<li><code>\uD83D</code> (고위 써로게이트)</li>
<li><code>\uDE00</code> (저위 써로게이트)
일반적인 <code>split()</code> 함수나 <code>charAt()</code> 같은 문자열 처리 함수는 문자열을 코드 유닛 단위로 나누기 때문에, 이모지가 하나의 문자로 처리되지 않고 두 개의 코드 유닛으로 나뉘는 문제가 발생합니다.</li>
</ul>
<h3 id="arrayfrom의-역할">Array.from()의 역할</h3>
<p><code>Array.from()</code>은 단순히 문자열을 코드 유닛 단위가 아닌 <strong>문자 단위</strong>로 처리합니다. 즉, UTF-16 써로게이트 페어를 자동으로 인식하여 하나의 써로게이트 페어를 <strong>하나의 문자</strong>로 처리할 수 있게 해줍니다. 결과적으로 <code>Array.from()</code>을 사용하면 이모지를 하나의 문자로 인식하고, 이모지와 같은 복잡한 유니코드 문자를 제대로 분리할 수 있습니다.</p>
<p>예시</p>
<pre><code class="language-javascript">const input = &#39;😀😀😀&#39;;
console.log(input.split(&#39;&#39;)); // [&quot;\uD83D&quot;, &quot;\uDE00&quot;, &quot;\uD83D&quot;, &quot;\uDE00&quot;, &quot;\uD83D&quot;, &quot;\uDE00&quot;]
console.log(Array.from(input)); // [&quot;😀&quot;, &quot;😀&quot;, &quot;😀&quot;]</code></pre>
<hr>
<p>그럼 실제로 고객님들이 많이 사용하시는 이모지를 넣어볼까요. 👰🏻‍♀️🤵🏻</p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/c19b5e6b-fc69-4771-997c-4b8b2b2c2782/image.png" alt="">
오....... 이건 또 뭘까요
다시 gpt에게 물어봅니다.</p>
<p><code>👰🏻‍♀️</code>와 <code>🤵🏻</code> 같은 이모지는 단일 이모지가 아니라 여러 유니코드 코드 포인트가 결합된 이모지입니다. 예를 들어 <code>👰🏻‍♀️</code>는 다음과 같은 구성 요소로 이루어져 있습니다:</p>
<ul>
<li>기본 이모지 <code>👰</code> (Bride)</li>
<li>스킨 톤 수정자 <code>🏻</code> (Light Skin Tone)</li>
<li>성별 기호 <code>♀️</code> (Female Sign)</li>
</ul>
<p>이처럼 여러 코드 포인트가 결합된 이모지를 하나의 유니코드 문자열로 표현하는데, 이를 <strong>ZWP (Zero Width Joiner, 0폭 조인자)</strong> 를 통해 처리합니다. ZWP는 여러 개의 개별 이모지를 하나로 결합할 때 사용됩니다.</p>
<p>기존의 <code>Array.from()</code>로는 이 경우에도 이모지를 올바르게 분리할 수 없습니다. 이러한 경우에는 유니코드에서 제공하는 <strong>grapheme clusters(문자 클러스터)</strong> 를 처리할 수 있는 라이브러리를 사용해야 합니다.</p>
<p>해결 방법: grapheme-splitter 라이브러리 사용
grapheme-splitter는 이모지를 포함한 모든 문자를 하나의 그래프 클러스터로 처리하는 유용한 라이브러리입니다. 이 라이브러리를 사용하면 👰🏻‍♀️ 같은 복잡한 이모지도 하나의 단위로 처리할 수 있습니다.</p>
<hr>
<p>여튼 돌아돌아 다른 방법을 찾아 결국 순수하게 자바스크립트로 문제를 해결하진 못했지만,
Array.from에 대해 다시 찾아보게 되었어요.</p>
<h1 id="arrayfrom">Array.from()</h1>
<p><code>Array.from()</code>은 유사 배열 객체나 이터러블 객체를 배열로 변환하는 메서드입니다. 주로 배열처럼 동작하지만 실제 배열은 아닌 객체를 배열로 변환할 때 사용됩니다. 간단히 말해, 이 메서드는 <strong>배열이 아닌 것을 배열로 바꿔주는 도구</strong>입니다.</p>
<h2 id="주요-사용-예시">주요 사용 예시</h2>
<h3 id="1-유사-배열-객체를-배열로-변환">1. 유사 배열 객체를 배열로 변환</h3>
<p>유사 배열 객체는 배열처럼 보이지만, 실제 배열은 아닌 객체입니다. 예를 들어, <code>arguments</code>나 <code>NodeList</code> 등이 이에 해당합니다. Array.from()을 사용해 이를 배열로 변환할 수 있습니다.</p>
<pre><code class="language-javascript">function example() {
  console.log(arguments); // 유사 배열 객체
  const argsArray = Array.from(arguments); // 배열로 변환
  console.log(argsArray); // 배열
}

example(1, 2, 3); // 출력: [1, 2, 3]</code></pre>
<h3 id="2-이터러블-객체를-배열로-변환">2. 이터러블 객체를 배열로 변환</h3>
<p>이터러블 객체는 반복 가능한 객체로, <code>Set</code>, <code>Map</code> 등이 이에 해당합니다. <code>Array.from()</code>을 사용해 이들을 배열로 변환할 수 있습니다.</p>
<pre><code class="language-javascript">const set = new Set([1, 2, 3]);
const arrayFromSet = Array.from(set);
console.log(arrayFromSet); // 출력: [1, 2, 3]</code></pre>
<h3 id="3-문자열을-배열로-변환">3. 문자열을 배열로 변환</h3>
<p>문자열도 이터러블 객체이므로 <code>Array.from()</code>을 통해 각 문자로 구성된 배열을 만들 수 있습니다.</p>
<pre><code class="language-javascript">const str = &quot;hello&quot;;
const arrayFromString = Array.from(str);
console.log(arrayFromString); // 출력: [&#39;h&#39;, &#39;e&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;]</code></pre>
<h3 id="4-배열의-각-요소를-매핑하면서-변환">4. 배열의 각 요소를 매핑하면서 변환</h3>
<p><code>Array.from()</code>의 두 번째 인자로 함수를 전달하면, 배열을 생성하면서 각 요소를 변환할 수 있습니다. <code>map()</code>과 비슷하게 동작합니다.</p>
<pre><code class="language-javascript">const array = Array.from([1, 2, 3], x =&gt; x * 2);
console.log(array); // 출력: [2, 4, 6]</code></pre>
<h3 id="주로-쓰는-상황">주로 쓰는 상황</h3>
<ul>
<li>DOM에서 여러 요소를 선택할 때 <code>NodeList</code>는 배열이 아닌 유사 배열 객체입니다. 이때 배열 메서드를 사용하려면 <code>Array.from()</code>으로 배열로 변환해야 합니다.</li>
</ul>
<pre><code class="language-javascript">const buttons = document.querySelectorAll(&#39;button&#39;);
const buttonsArray = Array.from(buttons);
buttonsArray.forEach(button =&gt; console.log(button.textContent));</code></pre>
<ul>
<li><code>Set</code>이나 <code>Map</code> 같은 이터러블 객체를 순서가 있는 배열로 변환하고자 할 때도 많이 사용됩니다.</li>
</ul>
<p><code>Array.from()</code>은 배열을 다루는 상황에서 유용한 도구이며, 특히 유사 배열 객체를 다룰 때 매우 자주 쓰입니다.</p>
<hr>
<p><code>string</code>을 쪼개서 배열로 만드는 걸 보고 코딩테스트 때 <code>split()</code>을 쓰던 제가 떠오르더라구요. 
그러면 <code>split()</code>과 <code>Array.from()</code> 중에 뭐를 사용하면 좋을까요?</p>
<h1 id="arrayfrominput-vs-inputsplit">Array.from(input) vs input.split(&#39;&#39;)</h1>
<h2 id="arrayfrominput">Array.from(input)</h2>
<p>문자열을 이터러블 객체로 간주하고, 각 문자를 배열로 변환합니다.
유니코드 문자나 이모지를 처리할 때 <code>Array.from()</code>이 더 안전합니다. 왜냐하면 이모지는 하나의 문자로 보이지만, 두 개의 코드 유닛으로 표현되기도 하기 때문입니다.</p>
<pre><code class="language-javascript">const input = &quot;hello&quot;;
const array = Array.from(input); 
console.log(array); // 출력: [&#39;h&#39;, &#39;e&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;]
</code></pre>
<h2 id="inputsplit">input.split(&#39;&#39;)</h2>
<p>문자열을 빈 문자열 <code>&#39;&#39;</code>을 기준으로 쪼개서 배열로 변환하는 방법입니다.
일반적인 ASCII 문자(영어 알파벳 등)를 처리할 때는 <code>split(&#39;&#39;)</code>이 더 직관적이고 빠를 수 있습니다.</p>
<pre><code class="language-javascript">const input = &quot;hello&quot;;
const array = input.split(&#39;&#39;); 
console.log(array); // 출력: [&#39;h&#39;, &#39;e&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;]</code></pre>
<h2 id="성능과-상황에-따른-선택">성능과 상황에 따른 선택</h2>
<p>일반적인 상황: 문자열이 단순하고 유니코드 복합 문자를 포함하지 않는다면, <code>split(&#39;&#39;)</code>이 더 짧고 간결하기 때문에 자연스럽게 사용할 수 있습니다.
복잡한 문자열(이모지 포함): 만약 문자열에 이모지나 유니코드 문자가 포함될 가능성이 있다면, <strong>Array.from()</strong> 을 사용하는 것이 더 안전합니다.</p>
<pre><code class="language-javascript">const emojiString = &quot;😊&quot;; 
console.log(emojiString.split(&#39;&#39;)); // 출력: [&#39;�&#39;,&#39;�&#39;] (깨짐)
console.log(Array.from(emojiString)); // 출력: [&#39;😊&#39;] (정상 처리)</code></pre>
<hr>
<p>이렇게 프로젝트를 하다가 <code>Array.from()</code>을 재발견하는 시간을 가졌습니다.
설마 저만 모르는 거 아니겟쬬..?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[청첩장 만들었다가 강연을 하게 되었다.]]></title>
            <link>https://velog.io/@heyday_xz/%EC%B2%AD%EC%B2%A9%EC%9E%A5-%EB%A7%8C%EB%93%A4%EC%97%88%EB%8B%A4%EA%B0%80-%EA%B0%95%EC%97%B0%ED%95%98%EA%B3%A0-%EC%98%A8-%EC%8D%B0-%ED%91%BC%EB%8B%A4-%E3%85%8B%E3%85%8B</link>
            <guid>https://velog.io/@heyday_xz/%EC%B2%AD%EC%B2%A9%EC%9E%A5-%EB%A7%8C%EB%93%A4%EC%97%88%EB%8B%A4%EA%B0%80-%EA%B0%95%EC%97%B0%ED%95%98%EA%B3%A0-%EC%98%A8-%EC%8D%B0-%ED%91%BC%EB%8B%A4-%E3%85%8B%E3%85%8B</guid>
            <pubDate>Sun, 13 Oct 2024 13:51:19 GMT</pubDate>
            <description><![CDATA[<p>지난 프론트 개발자의 모바일 청첩장 만들기 의 뒷 이야기를 적어보려고 합니다.</p>
<p>감사하게도 이전에 작성했던 <a href="https://velog.io/@heyday_xz/FE-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EB%AA%A8%EB%B0%94%EC%9D%BC%EC%B2%AD%EC%B2%A9%EC%9E%A5">FE 개발자의 모바일청첩장</a> 후기를 많은 분들이 좋아해주셨어요. 
제가 생각했던 것 이상으로 벨로그 글 하트, Github Star와 fork도 많이 받았답니다.</p>
<p>그리고 뜻밖의 기회도 얻게 되었어요.
모두의연구소 대전에서 강연 제안을 받게 되었습니다.</p>
<h3 id=""><strong>???</strong></h3>
<p>처음 담당자 분께 메일을 받았을 땐 물음표 그 자체였습니다.
저는 그냥 일반인인데요...? 내가 강연을...?
강연을 한다고 해도, 이게 할 만한 내용일까?</p>
<p>그래도 내가 언제 강연을 해보겠어 하는 마음과 준비하는 과정에서 배울 점이 있을 거라고 생각하고 승낙하게 됩니다.</p>
<blockquote>
</blockquote>
<p>이 글은 강연 준비 과정과 후기에 대한 글입니다.
강연에 대한 내용은 없습니다.
대신 하단에 자료가 열려 있으니 관심 있으신 분들은 구경오세요.ㅎㅎ</p>
<hr>
<h1 id="준비-과정">준비 과정</h1>
<h2 id="대상과-주제">대상과 주제</h2>
<p>처음에 생각했던 주제는 <strong>비개발자도 개발할 수 있다! 모바일청첩장</strong> 이었어요.
개발자가 듣기에 강연이 너무 쉽지 않을까 생각했거든요.</p>
<p>그런데, 사전 준비에 필요한 것들을 정리하다 보니까 개발자가 아니라면 준비과정에서부터 막힐 것 같다고 생각이 들었어요.</p>
<p>그래서 주제를 변경하게 됩니다.
키워드는 <strong>오픈소스 사이드 프로젝트 만들기</strong>로 <code>사이드 프로젝트</code>에 무게를 실어서 강연을 준비하게 되었어요.</p>
<p>최종 강연 대상은 <code>개발자</code>가 되었고, 강연은 1부와 2부로 나누어 준비하게 되었습니다.</p>
<p>1부는 사이드 프로젝트를 하게 된 과정(블로그에도 일부 담긴)과 솔직한 이야기를 나누고
2부는 오픈소스 프로젝트 포크해서 실제 배포하는 과정까지 실습해 보기로 했어요.</p>
<h2 id="자료-준비">자료 준비</h2>
<p>막상 자료를 준비하다 보니, 하고 싶은 얘기가 많았어요.
흔한(?) 청첩장 개발이 오픈소스가 된 이야기, 개발 과정과 홍보 과정, 프로젝트를 통해 배운 점, 알게 된 점 등등이요.
강연을 하게 될 줄은 몰랐지만, 과거의 내가 개발 과정을 조금씩 기록해두었더라구요.
헨델과 그레텔의 빵조각을 줍듯 제가 뿌려놓은 빵조각을 주으며 슬라이드를 만들었습니다.</p>
<p>그리고 개발자라면 누구나 따라할 수 있도록 아주아주 상세한 실습자료를 (남편이) 만들었어요.
한 단계, 한 단계 따라할 수 있도록 캡쳐하면서 설명을 쓰는 건 생각보다 품이 많이 드는 과정이었어요.</p>
<p>이렇게 정리를 하기 전에는 내가 했던 일을 작게 여겼는데, <code>생각보다 대단하잖아 나?</code> 라는 생각을 했습니다.</p>
<p>또 결혼을 준비할 때도 각자 잘하는 걸 담당해서 팀플한다는 느낌이 들었는데, 이번에 다시 한번 팀플의 바이브를 느꼈어요.</p>
<p>그리고 모두의연구소 대전 담당자분께서 <a href="https://event-us.kr/modu/event/90704">이벤트 페이지</a>를 멋지게 만들어서 공유해주셨습니다.
두둥!
<img src="https://velog.velcdn.com/images/heyday_xz/post/e9a73bfb-127b-4920-af61-22f286fe1940/image.png" alt=""></p>
<hr>
<h1 id="강연-당일">강연 당일</h1>
<h2 id="강연-분위기">강연 분위기</h2>
<p>생각보다 많은 분들이 찾아주셨어요.
금요일 7시라면 고된 평일을 마무리하며 한껏 풀어지고 싶은 시간대인데..!
결혼을 준비하고 계신 분, 프리랜서이신 분, 취업을 준비하는 대학생 분들, 심지어 강연을 위해 서울에서 대전으로 와주신 분까지...
강연이 처음이라 뚝🤖딱🤖거렸는데 따뜻한 눈으로(?) 봐주셔서 분위기는 좋았던 것 같아요.</p>
<h2 id="예상치-못한-상황">예상치 못한 상황</h2>
<p>2부 실습시간이었어요. fork 후 npm install 부터 막히는 건 예상하지 못했어요. 
심지어 윈도우 환경 테스트도 하고 갔는데 말이죠. 
다행히 Github의 Codespace 서비스를 이용해, npm 설치가 안된다거나 git 이슈가 있는 분들의 문제를 해결할 수 있었어요.</p>
<p>참가자들의 개발환경이나 실습 진행 속도가 다를 것은 예상했지만 그럼에도 당황하긴 했어요.
그래도 실습자료를 잘 만들어두었기에 설명을 우선 들으시고, 강연이 끝나고도 들어가서 보실 수 있도록 안내해드렸어요.</p>
<h2 id="질문">질문</h2>
<p>강연이 끝나고 Q&amp;A 시간을 가졌어요. 
프로젝트의 구조나 기술을 선택함에 있어서의 기준, 프리랜서로서 구체적인 계획, 기획자나 디자이너 없이 어떻게 개발하는지와 같은 질문들을 주셨어요.</p>
<p>개발할 때는 빨리 빨리, 부딪히면서 진행을 했었는데, 오히려 강연을 준비하고 질문에 답변을 하는 과정에서 내가 왜 그렇게 했었는지 앞으로는 어떻게 하면 좋을지 고민하게 되더라구요.</p>
<p>강연이 끝나고 돌아와서도 저에게 숙제로 남았답니다.</p>
<hr>
<h1 id="그래서">그래서</h1>
<h2 id="강연을-통해-얻은-것">강연을 통해 얻은 것</h2>
<p>프로젝트를 마무리 했을 때도 뿌듯함을 느꼈지만, 이걸 다른 사람에게 설명하기 위해 프로젝트를 다시 돌아보는 과정에서 내가 프로젝트를 통해 얻은 게 많았다는 것을 느꼈어요.</p>
<p>원하는 걸 만들기 위해 공부하고, 트러블 슈팅 과정에서 자신감도 얻고, 실제 포크해서 사용해주시는 분들을 보며 새로운 아이디어를 얻기도 하고, 제 프로젝트를 플레이그라운드 삼아서 새로운 시도를 할 기회도 얻었더라구요.</p>
<p>강연을 준비하지 않았더라면 내가 해낸 게 무엇인지 살펴볼 기회가 없었을 거예요.</p>
<h2 id="앞으로의-계획">앞으로의 계획</h2>
<p>많이 부족했지만, 재밌는 강연이었다고 말씀해주신 분들, 또 컨트리뷰트 거리가 있는지 여쭤봐주시는 분도 있어서 감사하고 재미있는 경험이었어요.</p>
<p>이번 경험을 바탕으로 새로운 사이드 프로젝트를 기획해보기도 하고, 새로운 기술도 적용해보고, 나만의 우물을 벗어나기 위해 다른 분들과도 함께 프로젝트를 진행해봐도 좋을 것 같다고 생각했습니다.</p>
<h2 id="강연-자료">강연 자료</h2>
<blockquote>
</blockquote>
<p><a href="https://bit.ly/4h0JED3">1부 강연 링크</a>
<a href="https://bit.ly/4gXiuNr">2부 실습 링크</a>
자료는 계속해서 열어둘 생각입니다.
혹시 개발하다가 이슈가 있으시다면 깃허브에 이슈 등록해주세요~!</p>
<hr>
<p>말하고 글 쓰는데 특별한 재능이 없어 후기를 작성하는 데도 오랜 시간이 걸렸네요.</p>
<p>이런 일반인(?)에게 강연 기회를 제공한 모두의연구소 대전 담당자님,
실습과 실습자료를 담당해준 남편에게도 감사의 말을 전하며 글을 마무리 해봅니다. </p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/c3702589-27de-49d5-844f-3f738dca368a/image.jpg" alt=""></p>
<p>그럼 이만 총총.</p>
<h2 id="ps">P.S.</h2>
<p><strong>OPEN TO WORK</strong> 상태입니다.
<a href="https://bit.ly/4h0K5gF">링크드인 링크</a>
친구 신청도 좋고, 일자리 제안도 좋습니다 :D</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 자바스크립트 엔진과 자바스크립트 런타임의 차이점 (번역)]]></title>
            <link>https://velog.io/@heyday_xz/JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%97%94%EC%A7%84%EA%B3%BC-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%9F%B0%ED%83%80%EC%9E%84%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B2%88%EC%97%AD</link>
            <guid>https://velog.io/@heyday_xz/JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%97%94%EC%A7%84%EA%B3%BC-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%9F%B0%ED%83%80%EC%9E%84%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B2%88%EC%97%AD</guid>
            <pubDate>Sun, 01 Sep 2024 14:51:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://humanwhocodes.com/blog/2024/03/javascript-engines-runtimes/">자바스크립트 엔진과 자바스크립트 런타임의 차이점</a> 에 대한 글을 번역했습니다.</p>
</blockquote>
<p><code>자바스크립트 엔진</code>과 <code>자바스크립트 런타임</code> 이라는 용어가 &quot;<strong>자바스크립트를 실행하는 프로그램</strong>&quot; 이라는 의미로 혼용되어 사용되는 것을 들어본 적이 있을 것입니다. 
이들은 종종 V8, Node.js 또는 기타 관련 프로그램의 조합을 참조하여 혼용되기도 합니다. 
하지만 자바스크립트 엔진과 자바스크립트 런타임에는 범위와 기능 면에서 상당한 차이가 있습니다. 
이 차이를 이해하는 것은 자바스크립트 언어 전체를 잘 이해하는 데 중요합니다. 
<code>엔진</code>과 <code>런타임</code>이 무엇인지 논의하기 전에 엔진과 런타임을 함께 자주 사용하는 몇가지 용어를 정의하는 것이 좋을 것입니다. 바로<code>ECMAScript</code>와 <code>Javascript</code> 입니다.</p>
<h1 id="ecmascript란">ECMAScript란?</h1>
<p>1996년, Netscape(넷스케이프)와 Sun Microsystems(썬 마이크로시스템즈)는 비영리 표준 단체인 ECMA 인터내셔널에 자바스크립트 표준화에 대해 문의했습니다.
그 노력의 결과로 1997년에 발표된 ECMA-262는 Javascript 구현 방식을 정의하는 사양입니다.
Sun이 Javascript 상표를 기부하지 않기로 하면서 이 사양은 다른 이름을 가져야 했고, ECMA-262는 ECMAScript 언어 사양이 되었습니다. 따라서 <code>ECMAScript</code>는 <code>ECMA-262에 명시된 언어의 이름</code>입니다.</p>
<p>ECMAScript는 어디에 내장되어 있든 상관없이(구현을 내장한 프로그램을 호스트라고 합니다.) 구현 방식을 준수해야하는 자바스크립트의 핵심 기능을 정의합니다. 
심지어 초기에 넷스케이프는 브라우저 뿐만 아니라 서버에서도 자바스크립트를 사용하고자 했으며, ECMAScript는 두 구현의 핵심 역할을 했습니다. 따라서 ECMAScript에는 웹 관련 기능이나 데이터를 입/출력하는 방법이 포함되어 있지 않습니다.(이런 기능들은 호스트에 의해 제공되어야 합니다.)
즉 ECMAScript에는 Object, Array, Promise와 같은 표준 전역 클래스는 포함되어 있지만 HTMLElement, setTimeout, fetch()는 포함되어 있지 않습니다.</p>
<h1 id="javascript란">JavaScript란?</h1>
<p>자바스크립트는 공식적인 정의는 없지만 ECMAScript 언어의 상위 집합으로 널리 이해되고 있습니다.
즉, <strong>자바스크립트는 다른 사양 및 기타 기능을 더해 ECMAScript를 구현</strong>합니다.
초기 형태에서 자바스크립트는 ECMAScript와 DOM과 같은 웹 기반 API, 히스토리 API와 같은 브라우저 기반 API를 더한 것으로 간주되었습니다.
오늘날 자바스크립트는 ECMAScript와 기타 호스트 제공 API의 모든 조합으로 간주됩니다.
여기에는 브라우저용 웹 전용 API와 Node.js 와 같은 호스트용 서버 전용 API가 포함됩니다.</p>
<h1 id="javascript-engine-이란">JavaScript engine 이란?</h1>
<p>일반적으로 자바스크립트 엔진이라고 불리는 것은 추가 기능 없이(또는 그다지 많지 않게) ECMA-262를 구현하기 때문에 ECMAScript 엔진이라고 부르는 것이 더 정확할 수 있습니다. 
<strong>자바스크립트 엔진은 호스트에 내장되어 입/출력을 위한 추가 기능을 정의</strong>합니다. 
가장 잘 알려진 Javascript 엔진은 다음과 같습니다.</p>
<ul>
<li><p>V8: Chromium 프로젝트의 자바스크립트 엔진으로 만들어졌으며 현재 Node.js와 Deno에서도 사용되고 있습니다. Edge와 Opera는 Chromium을 기반으로 하기 때문에 V8은 가장 자주 사용되는 자바스크립트 엔진입니다.</p>
</li>
<li><p>SpiderMonkey: Firefox를 위한 자바스크립트 엔진입니다.</p>
</li>
<li><p>JavaScriptCore: MacOS와 iOS에서 Safari용 자바스크립트 엔진으로 만들어졌으며, Bun에서도 사용됩니다.</p>
</li>
</ul>
<p>자바스크립트 엔진은 ECMAScript만 구현하고 호스트에 의해 확장되도록 되어 있기 때문에 다양한 런타임 환경에서 사용할 수 있습니다.</p>
<h1 id="javascript-runtime-이란">JavaScript runtime 이란?</h1>
<p><strong>자바스크립트 런타임은 자바스크립트 엔진을 내장한 프로그램이라는 뜻의 ECMAScript 호스트</strong>입니다.
chrome, firefox, edge, safari, Node.js, Deno, Bun은 모두 자바스크립트 엔진을 내장하고 자바스크립트를 통해 액세스할 수 있는 추가 기능을 정의하기 때문에 자바스크립트 런타임입니다.
<strong>웹 브라우저는 DOM 및 기타 웹 API를 구현</strong>하는 반면 <strong>서버 측 런타임은 파일 시스템 액세스를 구현</strong>합니다.</p>
<p>자바스크립트 런타임에 추가할 수 있는 추가 기능에 대한 규칙은 없으며, 런타임 개발자가 스스로 결정할 수 있습니다. 이것이 바로 Node.js, Deno, Bun이 모두 다른 방식으로 파일 시스템을 구현하는 이유이며, Deno가 처음에는 자체 HTTP 클라이언트를 구현하기로 결정한 반면 Node.js는 fetch()와 같은 웹 API를 선호하기로 한 이유입니다.(이후 Node.js도 fetch()를 채택했습니다.)
하지만 자바스크립트 런타임을 독특하게 만드는 것은 자바스크립트 API 뿐만이 아닙니다. 자바스크립트 엔진을 사용하는 방식도 마찬가지입니다.</p>
<p>예를 들어 런타임이 자바스크립트 실행과 다른 작업 수행 사이를 전환할 수 있도록 하는 프로세스인 이벤트 루프는 ECMA-262에 정의되어 있지 않으므로 어떤 자바스크립트 엔진에서도 구현되어 있지 않습니다. 자체 이벤트 루프를 구현하는 것은 각 자바스크립트 런타임에 달려 있습니다. 웹 브라우저에는 HTML사양에 정의된 이벤트 루프 버전이 있지만 Node.js와 같은 서버 측 런타임은 자체적으로 정의합니다. 이벤트 루프는 자바스크립트 런타임에는 필요하지 않지만 범용 자바스크립트 런타임에서 찾을 수 있습니다.</p>
<h1 id="요약">요약</h1>
<p>자바스크립트 엔진과 자바스크립트 런타임은 서로 관련이 있지만 동일하지는 않습니다.
자바스크립트 엔진은 ECMA-262 표준에 정의된 대로 ECMAScript를 구현합니다.
ECMA-262는 입력 또는 출력에 대한 유도없이 자바스크립트의 핵심 기능을 정의합니다.
자바스크립트 런타임은 자바스크립트 엔진을 내장하고 입력 및 출력을 위한 추가 기능과 함께 런타임에 필요한 다른 모든 기능을 보강하는 ECMAScript 호스트입니다.
추가 기능에는 웹 브라우저의 DOM 또는 서버 측 런타임의 파일 시스템 액세스가 포함될 수 있습니다.
런타임은 다른 표준을 따라야 할 의무가 없으며 필요에 따라 자체 API를 정의할 수 있기 때문에 Node.js, Deno, Bun은 모두 서로 다른 파일 시스템 API를 가지고 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Event Looping in JS - Part 1 (번역)]]></title>
            <link>https://velog.io/@heyday_xz/JS-aftfs31w</link>
            <guid>https://velog.io/@heyday_xz/JS-aftfs31w</guid>
            <pubDate>Sun, 25 Aug 2024 11:07:07 GMT</pubDate>
            <description><![CDATA[<p><a href="https://gurindernarang.medium.com/event-looping-in-js-part-1-3cb8fa08121a">이벤트 루프</a>에 대한 미디움 글을 번역했습니다.</p>
<blockquote>
<p>이벤트 루프는 코드 실행을 처리하는 연속적인 프로세스로, call stack, microtask queue, callback queue를 관리하여 자바스크립트에서 비동기 연산을 효율적으로 실행할 수 있도록 합니다.</p>
</blockquote>
<hr>
<p>이벤트 루프의 개념을 이해하기 위해 먼저 자바스크립트 코드의 내부 작동을 이해해야 합니다.
이 프로세스에 적극적으로 기여하는 다음의 기본 요소를 살펴보겠습니다.</p>
<ol>
<li>Memory Heap 메모리 힙</li>
<li>Global Execution Context(GEC) 전역 실행 컨텍스트</li>
<li>Call Stack 호출 스택</li>
<li>Task Queue - Callback Queue + Micro Task Queue 태스크 큐</li>
</ol>
<hr>
<h1 id="memory-heap">Memory Heap</h1>
<blockquote>
<p>자바스크립트에서 메모리 힙은 프로그램 실행 중 런타임 환경(예: 웹 브라우저 또는 Node.js)이 변수, 객체, 함수를 위해 공간을 할당하는 메모리 영역입니다.</p>
</blockquote>
<p>메모리 힙에 대한 핵심 포인트 입니다.</p>
<h4 id="1-dynamic-memory-allocation-동적-메모리-할당">1. Dynamic Memory Allocation (동적 메모리 할당)</h4>
<p>힙은 프로그램 실행 중에 생성되는 객체와 데이터 구조에 대한 메모리를 동적으로 할당하는 역할을 담당합니다.
함수 호출과 로컬 변수에 대한 정적 메모리 할당을 처리하는 스택과는 대조적입니다.</p>
<h4 id="2-objects-and-variables-객체와-변수">2. Objects and variables (객체와 변수)</h4>
<p>객체, 배열 및 기타 복잡한 데이터 구조는 힙에 저장됩니다. 새 키워드나 객체 리터럴 등을 통해 자바스크립트에서 객체를 생성하면 해당 객체에 대한 메모리가 힙에 할당됩니다.</p>
<h4 id="3-garbage-collection-가비지-컬렉션">3. Garbage Collection (가비지 컬렉션)</h4>
<p>자바스크립트는 자동 가비지 컬렉션을 사용하여 힙의 메모리를 관리합니다. 가비지 컬렉션은 더 이상 사용되지 않는 메모리를 식별하고 해제하여 메모리 누수를 방지합니다. 이 과정에는 참조되지 않은 객체를 식별하고 해당 메모리를 회수하는 과정이 포함됩니다.</p>
<h4 id="4-reference-counting-참조-카운팅">4. Reference Counting (참조 카운팅)</h4>
<p>자바스크립트에서 가비지 컬렉션에 사용되는 기술 중 하나는 참조 카운팅입니다. 힙의 각 객체에는 참조 카운트가 있으며, 객체의 참조 카운트가 0으로 떨어지면 (프로그램에서 더 이상 도달할 수 없음을 의미) 해당 객체에 대한 메모리가 해제됩니다.</p>
<h4 id="5-memory-leaks-메모리-누수">5. Memory Leaks (메모리 누수)</h4>
<p>가비지 컬렉션은 메모리 관리에 도움이 되지만, 자바스크립트에서 의도치 않게 메모리 누수가 발생할 수 있습니다. 이는 객체에 대한 참조가 필요 이상으로 오래 지속되어 가비지 컬렉터에 의해 수집되지 않을 때 발생할 수 있습니다.</p>
<p>메모리 힙의 작동방식을 이해하는 것은 특히 대규모 어플리케이션이나 장기간 실행되는 프로세스를 다룰 때 효율적인 자바스크립트 코드를 작성하는데 중요합니다. 개발자는 객체 수명을 염두에 두고 참조를 주의 깊게 관리하며 잠재적인 메모리 누수 시나리오에 유의해야 합니다.</p>
<hr>
<h1 id="global-excution-contextgec-전역-실행-컨텍스트">Global Excution Context(GEC) 전역 실행 컨텍스트</h1>
<blockquote>
<p>자바스크립트의 모든 작업은 실행 컨텍스트 내에서 이루어집니다. JS 코드가 실행될 때마다 전역 실행 컨텍스트가 생성됩니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/05ac1d3a-8cae-44c5-a0e6-b010f16f0781/image.png" alt=""></p>
<p>전역 실행 컨텍스트는 코드가 실행되는 가장 바깥쪽 컨텍스트 또는 스코프 입니다. 자바스크립트 프로그램이 실행되면 자바스크립트 엔진은 전역 실행 컨텍스트를 생성하여 전역 범위를 관리합니다. 전역 실행 컨텍스트를 생성하는 동안에는 두 단계가 있습니다.</p>
<h2 id="초기화-단계생성-단계">초기화 단계/생성 단계</h2>
<p>전역 실행 컨텍스트를 생성하는 동안 변수 및 함수 선언이 올라가고 메모리 공간이 할당되는 생성 단계가 발생합니다. 이 단계에서는 변수가 정의되지 않은 상태(undefined)로 초기화 됩니다. (호이스팅)</p>
<h2 id="실행-단계">실행 단계</h2>
<p>초기화 단계가 끝나면 실제 코드가 위에서 아래로 실행됩니다. 이 단계에서 할당과 함수 호출이 수행됩니다.</p>
<hr>
<h1 id="call-stack-호출-스택">Call Stack 호출 스택</h1>
<p>자바스크립트에서 호출 스택은 프로그램에서 함수 호출을 추적하는 데이터 구조입니다. 이는 마지막에 호출된 함수가 가장 먼저 제거되는 LIFO(Last In, First Out) 방식으로 작동합니다.
함수가 호출되면 새 프레임이 호출 스택에 푸시되고, 함수가 반환되면 해당 프레임이 스택에서 팝아웃됩니다.</p>
<pre><code class="language-javascript">function firstfunction() {
  secondFunction();
  console.log(&quot;Inside firstFunction&quot;);
}

function secondFunction() {
  thirdFunction();
  console.log(&quot;Inside secondFunction&quot;);
}

function thirdFunction() {
  console.log(&quot;Inside thirdFunction&quot;);
}

firstFunction();</code></pre>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/c423980d-2eb3-49c4-832a-a8a600e75e5e/image.png" alt=""></p>
<ol>
<li>메모리 힙의 모든 함수 선언은 <strong>생성/초기화 단계</strong>에서 메모리를 사용합니다.</li>
<li>초기화 단계가 끝나고 <strong>실행 단계</strong>에서 <code>firstFuction()</code> 이 호출됩니다.</li>
<li>처음에 호출스택에는 익명(anonymous)/main() 함수가 있었습니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/191b41fd-056a-4556-84ee-23f12773dc68/image.png" alt=""></p>
<p><code>firstFunction()</code>이 호출되고나면 위 코드의 트리거 지점인 빈 호출스택으로 푸시됩니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/b272d57d-9baa-418e-80a0-e09de1526948/image.png" alt=""></p>
<ol start="4">
<li><p><code>firstFunction()</code> 내부의 첫번째 줄은 <code>secondFuction()</code>이고, 이 줄이 호출되면 <code>secondFunction()</code>이 호출 스택으로 푸시되고, 그럼 호출 스택은 다음과 같아집니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/4f12ba3b-1ef7-4764-aaf6-f17549aa5814/image.png" alt=""></p>
</li>
<li><p><code>secondFunction()</code> 내에서 <code>thirdFunction()</code>이 호출되고 호출 스택은 다음과 같이 됩니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/ae620b13-8961-4ce7-8205-d9e1fb149c30/image.png" alt=""></p>
</li>
<li><p><code>thridFunction()</code> 내부에서는 &quot;console.log()&quot; 문이 실행되고 그 이후에는 코드에 실행할 다른 내용이 없습니다. 따라서 함수는 거기서 끝나고 호출 스택의 맨 위에서 제거됩니다.
호출 스택은 <code>firstFunction</code> -&gt; <code>secondFunction()</code>이 됩니다.</p>
</li>
<li><p>이제 <code>secondFunction()</code>이 맨 위에 있고 호출 스택에 의해 실행되고 있으며, <code>secondFunction()</code> 내부의 &quot;console.log()&quot; 문이 실행됩니다. <code>secondFunction()</code>에 대한 실행이 완료되면 호출 스택에서도 제거되고 호출 스택에는 <code>firstFunction()</code>만 남게 됩니다.</p>
</li>
<li><p><code>firstFunction()</code>도 마찬가지이며 실행이 끝나면 호출스택에서 제거됩니다. 그리고 다시 호출 스택이 비워집니다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/23a21020-5688-43dd-8ddc-a6c3f6e83ec3/image.png" alt=""></p>
<hr>
<h1 id="task-queues-태스크-큐">Task Queues 태스크 큐</h1>
<p>자바스크립트에서 Task queue는 비동기 작업을 처리하는데 중요한 이벤트 루프 메커니즘의 일부입니다. 이벤트 루프는 메시지 큐에서 새 이벤트를 지속적으로 확인하고 순차적으로 실행하는 프로세스입니다. 태스크 큐는 이 이벤트 루프 내의 큐 중 하나입니다.</p>
<p>태스크 큐를 더 넓은 개념에 적용하는 방법은 다음과 같습니다.</p>
<h4 id="1-call-stack">1. Call Stack</h4>
<p>호출 스택은 프로그램에서 현재 실행 중인 함수를 추적하는 데이터 구조입니다. 함수가 호출되면 스택으로 푸시되고 완료되면 스택에서 튕겨져 나옵니다.</p>
<h4 id="2-aips-and-callbacks">2. AIPs and Callbacks</h4>
<p>네트워크 요청과 같은 비동기 작업을 자바스크립트에서 수행하면 브라우저의 웹 API가 자바스크립트 런타임 외부에서 이런 작업을 처리합니다. 이러한 작업이 완료되면 콜백 함수가 작업 태크스 큐에 배치됩니다.</p>
<h4 id="3-task-queue">3. Task queue</h4>
<p>태스크 큐는 이벤트 루프에서 처리할 콜백 함수 및 이벤트를 보관하는 큐입니다. 태스크 큐의 콜백은 호출 스택이 비워진 후에 처리됩니다. 이렇게 하면 오래 실행되는 동기식 작업이 다른 작업의 실행을 차단하지 않습니다. 이러한 작업에는 비동기 작업(예: <code>setTimeout</code>, AJAX 요청, 이벤트 핸들러)의 콜백 뿐 아니라 다른 유형의 작업도 포함될 수 있습니다.</p>
<h4 id="4-micro-task-queue--job-queue">4. Micro-task Queue / Job Queue</h4>
<p>&quot;잡 큐&quot;는 호출 스택의 각 작업이 끝날 때 실행되는 작업을 포함하는 특정 유형의 대기열입니다. 마이크로 태스크에는 일반적으로 Promise(<code>then</code>, <code>catch</code>)의 콜백, Node.js의 <code>process.nextTick</code> 및 <code>MutationObserver</code> API가 포함됩니다.</p>
<p>이러한 컴포넌트가 함께 동작하는 방식을 이해하면 코드가 비동기 작업을 효과적으로 처리할 수 있도록 하는 자바스크립트의 중요한 개념인 이벤트 루프를 이해하는데 도움이 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI 코드리뷰 붙여보기]]></title>
            <link>https://velog.io/@heyday_xz/AI-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0-%EB%B6%99%EC%97%AC%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@heyday_xz/AI-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0-%EB%B6%99%EC%97%AC%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 17 Aug 2024 06:53:56 GMT</pubDate>
            <description><![CDATA[<p>혼자하는 프로젝트다보니, 코드리뷰의 아쉬움이 있습니다.
<a href="https://coderabbit.ai/">CodeRabbit</a>이라는 AI 코드리뷰 서비스를 알게 되었고, 14일 Free trial을 해보기로 했습니다.</p>
<p>적용 과정은 생각보다 더 간단합니다!</p>
<h2 id="coderabbit-도입">CodeRabbit 도입</h2>
<p>먼저, 서비스에 가입하고, CodeRabbit에서 내 깃허브 레포에 접근할 수 있도록 권한을 줍니다.</p>
<p>원하는 레포를 추가하고 나면 대시보드에 아래와 같이 뜨게 됩니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/0f1b4af2-6707-440f-a80a-cd26010f5839/image.png" alt=""></p>
<h3 id="리뷰-방식-설정">리뷰 방식 설정</h3>
<p>대시보드의 레포 오른쪽에 수정 버튼을 클릭해서 UI로 수정할 수도 있고,
<img src="https://velog.velcdn.com/images/heyday_xz/post/d787accc-25fb-4512-9f46-661409fa0b1c/image.png" alt=""></p>
<p>프로젝트에 yaml 파일을 추가해서 수정할 수도 있습니다.
저는 프로젝트 루트에 <code>.coderabbit.yaml</code> 파일을 추가해보았어요.</p>
<pre><code class="language-yaml"># yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: &quot;ko-KR&quot;
early_access: false
reviews:
  profile: &quot;chill&quot;
  request_changes_workflow: false
  high_level_summary: true
  poem: true
  review_status: true
  collapse_walkthrough: false
  auto_review:
    enabled: true
    drafts: false
chat:
  auto_reply: true</code></pre>
<p>언어와 리뷰 분위기, 리뷰용 프로필, 리뷰 워크플로우 등을 설정할 수 있어요.
저는 언어만 한국어로 바꾸고, 그 외는 기본 설정을 사용했어요.</p>
<p>각 기능에 대한 설명은 <a href="https://docs.coderabbit.ai/configure-coderabbit">공식문서</a>에 친절하게 안내되어 있습니다.</p>
<h2 id="이제-잘-동작하는지-볼까요">이제 잘 동작하는지 볼까요?</h2>
<p>yaml 파일을 추가한 pr에 대한 리뷰입니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/917fbafc-32f8-49a5-a4ac-16b3ceef5f45/image.png" alt=""></p>
<p>pr에 대한 요약과 시를 한편 써주었네요.</p>
<p>추가할 기능이 없어서 컴포넌트의 스타일을 수정해봤습니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/28c1910f-7c35-41e7-b151-ae6621de5e90/image.png" alt="">
저의 pr을 요약해주고, 또 한편의 시가 나왔습니다 ㅋㅋㅋ
<img src="https://velog.velcdn.com/images/heyday_xz/post/6c4329e4-e16c-426b-85a6-725881493868/image.png" alt="">
잘된 변화에 대해서는 칭찬도 해주네요. 기분이 좋아집니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/b035c7b8-530a-4167-8ff6-5b7db44acc49/image.png" alt=""></p>
<h2 id="후기">후기</h2>
<p>도입이 쉬워서 5분만에 적용할 수 있었습니다.
혼자 개발하느라 놓쳤던 부분에 대해 도움을 받을 수 있겠다는 생각이 듭니다.
얼른 중요한 기능을 추가해서 상세한 리뷰(와 시 한편)를 받아보고 싶어요.
팀에서 사용한다면, 리뷰에 시간을 아낄 수도 있을 것 같습니다 :D </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 호이스팅(Hoisting)]]></title>
            <link>https://velog.io/@heyday_xz/JS-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85Hoisting</link>
            <guid>https://velog.io/@heyday_xz/JS-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85Hoisting</guid>
            <pubDate>Sun, 11 Aug 2024 14:32:10 GMT</pubDate>
            <description><![CDATA[<h1 id="호이스팅">호이스팅</h1>
<p>호이스팅(Hoisting)은 Javascript의 기본 동작 방식으로, 변수 선언과 함수 선언이 실제 코드 실행 전에 해당 스코프의 최상단으로 끌어올려지는 것처럼 동작하는 현상이다.
실제로 코드가 이동하는 건 아니고, 자바스크립트의 실행 컨텍스트가 변수를 메모리 공간에 미리 할당하기 때문에 발생하는 현상이다.</p>
<p>그러나 변수의 초기화는 호이스팅되지 않고, 선언만 호이스팅된다.</p>
<h2 id="변수-호이스팅">변수 호이스팅</h2>
<p>변수 호이스팅은 <code>var</code>, <code>let</code>, <code>const</code> 로 선언된 변수에서 다르게 나타난다.</p>
<h3 id="var-변수의-호이스팅"><code>var</code> 변수의 호이스팅</h3>
<pre><code class="language-javascript">console.log(a); // undefined
var a = 5;
console.log(a); // 5</code></pre>
<p>위의 코드에서 <code>var a = 5;</code> 는 <code>var a;</code> 와 <code>a = 5;</code>로 분리된다.
<code>var a;</code> 는 최상단으로 호이스팅되고 <code>a = 5;</code> 는 원래 위치에 남아있게 된다.
따라서 첫 번째 <code>console.log(a);</code> 는 <code>undefined</code> 를 출력한다.</p>
<h3 id="호이스팅-된-형태">호이스팅 된 형태:</h3>
<pre><code class="language-javascript">var a;
console.log(a); // undefined
a = 5;
console.log(a); // 5</code></pre>
<h3 id="let-과-const-의-호이스팅"><code>let</code> 과 <code>const</code> 의 호이스팅</h3>
<p><code>let</code>과 <code>const</code>로 선언된 변수도 호이스팅되지만, <code>var</code> 와는 달리 초기화 전에 접근하려 하면 <code>ReferenceError</code>가 발생한다. 이를 &quot;일시적 사각지대(Temporal Dead Zone, TDZ)&quot;라고 한다.</p>
<pre><code class="language-javascript">console.log(b); // ReferenceError: Cannot access &#39;b&#39; before initialization
let b = 10;</code></pre>
<h2 id="함수-호이스팅">함수 호이스팅</h2>
<p>함수 선언문으로 정의된 함수는 코드의 최상단으로 완전히 끌어올려지기 때문에, 함수 선언 전에 해당 함수를 호출할 수 있다.</p>
<pre><code class="language-javascript">console.log(myFunc()); // &quot;Hello, World!&quot;

function myFunc() {
  return &quot;Hello, World!&quot;;

}</code></pre>
<p>위의 코드에서 <code>function myFunc() { ... }</code> 전체가 호이스팅되기 때문에 <code>console.log(myFunc());</code>가 정상적으로 실행된다.</p>
<h3 id="호이스팅-된-형태-1">호이스팅 된 형태:</h3>
<pre><code class="language-javascript">function myFunc() {
  return &quot;Hello, World!&quot;;
}

console.log(myFunc()); // &quot;Hello, World!&quot;</code></pre>
<p>하지만 함수 표현식(<code>var</code>, <code>let</code>, <code>const</code>에 함수 리터럴을 할당하는 경우)은 변수 호이스팅 규칙을 따른다.</p>
<pre><code class="language-javascript">console.log(myFunc()); // TypeError: myFunc is not a function

var myFunc = function () {
  return &quot;Hello, World!&quot;;
}</code></pre>
<p>위 코드에서 <code>var myFunc</code>가 호이스팅 되었지만, 초기화 전에 함수 호출이 시도되었기 때문에 <code>undefined</code>가 <code>function</code>으로 호출되어 <code>TypeError</code>가 발생한다.</p>
<h2 id="일시적-사각지대temporal-dead-zone-tdz">일시적 사각지대(Temporal Dead Zone, TDZ)</h2>
<p>일시적 사각지대는 <code>let</code> 과 <code>const</code> 변수에서 발생하며, 변수 선언이 실제로 이루어지기 전까지 그 변수에 접근할 수 없는 구간을 의미한다.</p>
<pre><code class="language-javascript">{
    // TDZ starts at beginning of scope
    console.log(x); // ReferenceError: Cannot access &#39;x&#39; before initialization
    let x = 30; // End of TDZ
      console.log(x); // 30
}
</code></pre>
<p><code>TDZ</code> 는 코드가 작성된 위치가 아닌 실행 순서에 따라 영역이 달라진다.</p>
<pre><code class="language-javascript"> // TDZ starts at beginning of scope
function printValue() {
    console.log(value); // ReferenceError: Cannot access &#39;value&#39; before initialization
}

printValue();

let value = 10; // End of TDZ

printValue(); // 10</code></pre>
<p>첫번째 <code>printValue</code> 함수를 호출했을 때, <code>value</code> 변수는 아직 선언되지 않았다. 자바스크립트 엔진은 이 변수가 <code>TDZ</code> 에 있다고 판단하고 <code>ReferenceError</code>를 발생시킨다.
<code>let value = 10</code> 을 선언하면서 TDZ가 해제되고, 두번째 <code>printValue</code> 함수를 호출했을 때 정상적으로 <code>10</code> 이 출력된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TS] readonly와 const, as const]]></title>
            <link>https://velog.io/@heyday_xz/TS-readonly%EC%99%80-const-as-const</link>
            <guid>https://velog.io/@heyday_xz/TS-readonly%EC%99%80-const-as-const</guid>
            <pubDate>Sun, 04 Aug 2024 14:34:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>질문을 받으니까 헷갈렸던 readonly와 const, as const의 차이점을 정리해본다.</p>
</blockquote>
<h1 id="readonly">readonly</h1>
<p>readonly 키워드는 <strong>객체 프로퍼티나 배열 요소를 변경할 수 없도록</strong> 하는 데 사용된다.</p>
<h4 id="예제">예제</h4>
<pre><code class="language-typescript">interface Person {
  readonly name: string;
  age: number;
}

const person: Person = { name: &quot;John&quot;, age: 30 };
// person.name = &quot;Peter&quot;; // Error: Cannot assign to &#39;name&#39; because it is a read-only property.
person.age = 31; // OK</code></pre>
<p>여기서 name 프로퍼티는 읽기 전용이므로 값을 변경할 수 없다.</p>
<h1 id="const">const</h1>
<p>const는 변수 선언 시 사용되어 변수를 재할당할 수 없게 만든다.
그러나 <strong>const로 선언된 객체나 배열의 내부 프로퍼티는 변경할 수 있다</strong>!</p>
<h4 id="예제-1">예제</h4>
<pre><code class="language-typescript">const arr = [1, 2, 3];
// arr = [4, 5, 6]; Error: Assignment to constant variable.
arr.push(4); // OK
console.log(arr); // [1, 2, 3, 4]

const obj = { name: &quot;Alice&quot; };
// obj = { name: &quot;Mike&quot; }; // Error: Assignment to constant Variable.
obj.name = &quot;Mike&quot;; // OK
console.log(obj); // { name: &quot;Mike&quot; }</code></pre>
<h1 id="as-const">as const</h1>
<p>as const는 객체 리터럴을 불변으로 만드는 데 사용된다.
<strong>객체의 모든 프로퍼티를 읽기 전용으로</strong> 만들고, 가능한 가장 좁은 타입(리터럴 타입)을 지정한다.</p>
<h4 id="예제-2">예제</h4>
<pre><code class="language-typescript">const directions = {
  Up: &quot;UP&quot;,
  Down: &quot;DOWN&quot;,
  Left: &quot;LEFT&quot;,
  Right: &quot;RIGHT&quot;
} as const;

// directions.Up = &quot;Down&quot;; // Error: Cannot assign to &#39;Up&#39; because it is a read-only property.

type Direction = typeof directions[keyof typeof directions];

let move: Direction = directions.Up;
console.log(move); // &quot;UP&quot;</code></pre>
<p>as const 를 사용하여 directions 객체의 모든 프로퍼티를 읽기 전용으로 만들었기 때문에, 내부 프로퍼티 변경이 불가능하다.
각 프로퍼티의 타입을 문자열 리터럴 타입으로 지정한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[react] 청첩장 사이트 리팩토링 하기]]></title>
            <link>https://velog.io/@heyday_xz/react-%EC%B2%AD%EC%B2%A9%EC%9E%A5-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@heyday_xz/react-%EC%B2%AD%EC%B2%A9%EC%9E%A5-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 27 Jul 2024 13:51:41 GMT</pubDate>
            <description><![CDATA[<p>저의 <a href="https://velog.io/@heyday_xz/FE-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EB%AA%A8%EB%B0%94%EC%9D%BC%EC%B2%AD%EC%B2%A9%EC%9E%A5">모바일 청첩장</a> 개발 후, 감사하게도  <a href="https://invitation.smallbigwedding.kr/dearletter_sample">스몰 빅웨딩 모바일 청첩장</a> 외주를 맡아 개발 중입니다. 1차 개발 완료 후, 지속적으로 주문이 들어오고 있어요.
<img src="https://velog.velcdn.com/images/heyday_xz/post/2f4b522a-3e26-4f20-9586-3658ad06f980/image.gif" alt=""></p>
<p>다만 1인 개발이다보니, 리뷰를 받지 못하고 머지해버린 코드들이 항상 마음에 걸렸습니다.
<img src="https://velog.velcdn.com/images/heyday_xz/post/7fc20949-4c03-48d2-9412-c1c8c0bab1ff/image.png" alt=""></p>
<p>미뤄왔던 리팩토링을 해나가는 과정을 하나씩 올려볼까 합니다.</p>
<p>제가 생각하는 리팩토링 꼭지는 다음과 같습니다.</p>
<blockquote>
<ol>
<li>구조 변경</li>
<li>관심사 분리</li>
<li>스타일링 깔끔하게 관리하기</li>
</ol>
</blockquote>
<p>작게라도 빠르게 시작할 수 있는 2번을 먼저 진행하기로 했습니다. 
결국은 관심사 분리를 해야 파일이 나눠지고 결과적으론 구조 변경도 더 쉬워질 거라고 생각해서요!</p>
<hr>
<h2 id="갤러리-요구사항">갤러리 요구사항</h2>
<p>제가 개발한 갤러리의 요구사항은 아래와 같습니다.</p>
<ol>
<li>화살표 버튼 클릭으로 슬라이드.</li>
<li>사진 클릭시 모달 오픈.</li>
<li>모달에서 버튼 클릭으로 사진 슬라이드.
3-1. 모달 외부의 슬라이드와 연동하여 같이 움직여야 함.</li>
<li>모달에서 터치 드래그로 사진 슬라이드.</li>
</ol>
<h2 id="총체적-난국이었던-before">총체적 난국이었던 before</h2>
<p>코드를 보여드릴 수 없지만 요구사항을 만족하기 위한 기능들이 한 파일에 들어 있었습니다.</p>
<pre><code class="language-typescript">function Slider() {
  // 갤러리에 쓰일 사진을 불러내기
  ...

  // 슬라이드와 관련된 코드들
  const nextSlide = () =&gt; {
    ...
  };

  const prevSlide = () =&gt; {
    ...
  };

  // 모달과 관련된 코드들
  const [isModalOpen, setIsModalOpen] = useState(false);
  ...

  // 터치 슬라이드와 관련된 코드들
  ...

  return (
    &lt;Container&gt;
      &lt;SliderContainer ref={slideRef} currentSlide={currentSlide}&gt;
        {imageUrls.map((img, i) =&gt; (
          ...
        ))}
      &lt;/SliderContainer&gt;
      {isModalOpen &amp;&amp; (
        &lt;&gt;
          &lt;Modal show={isModalOpen}&gt;
            &lt;ModalSliderWrapper&gt;
              &lt;SliderContainer
                ref={modalRef}
                ...
              &lt;/SliderContainer&gt;
            &lt;/ModalSliderWrapper&gt;

            &lt;CloseButton onClick={closeModal}&gt;&amp;times;&lt;/CloseButton&gt;
            &lt;PrevButton onClick={prevSlide}&gt;&amp;lsaquo;&lt;/PrevButton&gt;
            &lt;NextButton onClick={nextSlide}&gt;&amp;rsaquo;&lt;/NextButton&gt;
          &lt;/Modal&gt;
        &lt;/&gt;
      )}

      &lt;ArrowWrapper&gt;
        &lt;Button onClick={prevSlide}&gt;
          &lt;ArrowLeft /&gt;
        &lt;/Button&gt;

        &lt;div&gt;
          {currentSlide + 1}/{photoNum}
        &lt;/div&gt;
        &lt;Button onClick={nextSlide}&gt;
          &lt;ArrowRight /&gt;
        &lt;/Button&gt;
      &lt;/ArrowWrapper&gt;
    &lt;/Container&gt;
  );
}

export default Slider;
</code></pre>
<h2 id="과정">과정</h2>
<p>이 기능들을 분리하기로 했습니다.</p>
<h3 id="리팩토링-방안">리팩토링 방안</h3>
<blockquote>
<p>1, 3번에서 클릭으로 사진 슬라이드가 되도록 하는 기능이 중복되었기 때문에 이 부분은 훅으로,
2번의 모달이 열려있는지 여부를 다른 컴포넌트에서 필요로 하기 때문에 프로바이더로 뺀다.</p>
</blockquote>
<h3 id="모달-프로바이더-만들고-컨텍스트-분리">모달 프로바이더 만들고 컨텍스트 분리</h3>
<p>ModalContext와 ModalProvider를 만들고, 기존의 코드에선 상태를 얻을 수 있도록 수정했습니다.</p>
<pre><code class="language-typescript">import React, { createContext, ReactNode, useContext, useState } from &#39;react&#39;;

interface ModalContextType {
  isModalOpen: boolean;
  openModal: () =&gt; void;
  closeModal: () =&gt; void;
}

const ModalContext = createContext&lt;ModalContextType | undefined&gt;(undefined);

export const ModalProvider = ({ children }: { children: ReactNode }) =&gt; {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const openModal = () =&gt; setIsModalOpen(true);
  const closeModal = () =&gt; setIsModalOpen(false);

  return (
    &lt;ModalContext.Provider value={{ isModalOpen, openModal, closeModal }}&gt;
      {children}
    &lt;/ModalContext.Provider&gt;
  );
};

export const useModalContext = () =&gt; {
  const context = useContext(ModalContext);
  if (!context) {
    throw new Error(&#39;useModalContext must be used within a ModalProvider&#39;);
  }
  return context;
};</code></pre>
<h3 id="useslider-훅-만들어서-슬라이드와-관련된-코드-분리">useSlider 훅 만들어서 슬라이드와 관련된 코드 분리</h3>
<p>슬라이드와 관련된 코드들을 분리했습니다.</p>
<pre><code class="language-typescript">export const useSlider = () =&gt; {
  // 갤러리에 쓰일 사진을 불러내기
  ...

  // 슬라이드와 관련된 코드들
  const nextSlide = () =&gt; {
    ...
  };

  const prevSlide = () =&gt; {
    ...
  };

  return {
    currentSlide,
    setCurrentSlide,
    imageUrls,
    photoNum,
    nextSlide,
    prevSlide,
  };
};
</code></pre>
<h2 id="after">after</h2>
<pre><code class="language-typescript">...
import { useModalContext } from &#39;@contexts/modalContext&#39;;
import { useSlider } from &#39;@hooks/useSlider&#39;;
...

function Slider() {
  ...
  const { isModalOpen, openModal, closeModal } = useModalContext();
  const {
    currentSlide,
    imageUrls,
    photoNum,
    nextSlide,
    prevSlide,
    isPrevInvisible,
    isNextInvisible,
  } = useSlider();

  ...

  useEffect(() =&gt; {
    ...
  }, [isModalOpen]);

  return (
    ...
  );
}

export default Slider;</code></pre>
<p>한개의 파일에 얽혀있던 코드가 관심사에 따라 ModalContext.tsx, ModalProvider.tsx, Slider.tsx, useSlider.ts로 쪼개지면서 (조금이나마) 유지보수에 용이해졌습니다. </p>
<p>완벽하게 정리되진 않았지만, 뷰에 충실한 컴포넌트로 1차 정리함으로써 마음이 조금 편안해졌네요.
<img src="https://velog.velcdn.com/images/heyday_xz/post/5a2f0cbc-fe89-4cc2-86d6-38a8b6d536ec/image.png" alt=""></p>
<p>추후에 여력이 된다면 갤러리 기능을 잘 분리해서 라이브러리로 만들어보면 좋겠습니다. :D</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FE 개발자의 모바일청첩장]]></title>
            <link>https://velog.io/@heyday_xz/FE-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EB%AA%A8%EB%B0%94%EC%9D%BC%EC%B2%AD%EC%B2%A9%EC%9E%A5</link>
            <guid>https://velog.io/@heyday_xz/FE-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EB%AA%A8%EB%B0%94%EC%9D%BC%EC%B2%AD%EC%B2%A9%EC%9E%A5</guid>
            <pubDate>Wed, 08 May 2024 05:36:18 GMT</pubDate>
            <description><![CDATA[<p>뜬금없는 포스팅이라 부끄럽지만, 오랜 연애 끝에 결혼했습니다.
결혼하는 김에, 개발자의 특전(?)이라는 모바일 청첩장 직접 만들기 저도 해보았는데요.</p>
<p>과정과 결과물, 후기 공유해봅니다.</p>
<blockquote>
<p><strong>바쁘신 분들을 위한 요약</strong></p>
</blockquote>
<ul>
<li><a href="https://happily-ever-after.vercel.app/">모바일청첩장</a> / <a href="https://github.com/heejin-hwang/mobile-wedding-invitation">Github Repo</a></li>
<li>핵심 기능 : 갤러리 / 토스페이, 카카오페이 연동 / 계좌번호 복사 / 지도 API 연동 / 좋아요 + 컨페티 / 방명록</li>
</ul>
<h1 id="과정">과정</h1>
<h2 id="왜-모바일-청첩장을-만들게-되었나">왜 모바일 청첩장을 만들게 되었나</h2>
<p>저는 종이청첩장을 셀프로 제작했습니다. 
기존 청첩장 업체의 청첩장들엔 불필요한 정보가 많다고 생각했기 때문이에요.</p>
<p>제가 담고 싶었던 정보(초대문구, 호스트정보, 장소)와 모바일 청첩장의 QR만 담아 간결하게 만들고 싶었습니다. 부가적인 정보는 모청에 다 담아낼 수 있으니까요!
종이 청첩장은 <a href="https://www.smallbigwedding.kr/WeddingStationery">스몰빅웨딩</a> 의 무료템플릿을 수정해서 만들었습니다.</p>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/caa8ab61-9f7d-4dfe-ae0b-4a8322abbd39/image.png" alt=""></p>
<p>뒷면 디자인이 모청으로 사용하면 딱 예쁠 것 같았어요. 
마침 사이드 프로젝트도 하나 하려고 했는데 겸사겸사 재밌게 되었지요.</p>
<h2 id="담고-싶었던-내용">담고 싶었던 내용</h2>
<ul>
<li>대문 사진</li>
<li>초대 문구</li>
<li>호스트</li>
<li>구글캘린더 추가하기</li>
<li>갤러리</li>
<li>계좌 정보(+토스, 카카오페이 연동)</li>
<li>지도(+네이버지도, 카카오맵 가는 길 검색)</li>
<li>방명록</li>
<li>좋아요(컨페티), 공유, 위로 가기 기능을 담은 플로팅바</li>
</ul>
<h2 id="스택--라이브러리-선정">스택 &amp; 라이브러리 선정</h2>
<p>이미 기획과 디자인을 고민하는 과정에서 시간을 많이 사용했기 때문에
<strong>1. 익숙한 언어로 2. 빠르게 3. 귀엽게</strong>
만들려고 마음 먹었고, 직접 구현보단 만들어져 있는 라이브러리를 이용하기로 했습니다.</p>
<ul>
<li>vite / React / Typescript : 정적페이지이고 SSR도 필요 없으니 익숙한 리액트로 결정.</li>
<li>emotion/StyledComponent : 처음엔 tailwindCSS를 사용해보려고 했다가 익숙하지 않음, 가독성, 재사용성 이슈로 익숙한 emotion으로 결정...</li>
<li>firebase : 좋아요 카운트와 방명록 기능을 위해 추가.</li>
<li>vercel : url에 vercel이 붙어도 상관없었기 때문에 편리한 배포를 위해 사용.</li>
<li><a href="https://www.npmjs.com/package/react-photoswipe-gallery">react-photoswipe-gallery</a> : 갤러리 라이브러리</li>
<li><a href="https://zeakd.github.io/react-naver-maps/">react-naver-maps</a> : 네이버지도 API를 직접 설정하려면 꽤 공수가 드는데, 리액트에서 쉽게 사용할 수 있도록 만들어진 라이브러리!</li>
<li><a href="https://www.npmjs.com/package/js-confetti">js-confetti</a> : 컨페티 라이브러리</li>
<li><a href="https://www.npmjs.com/package/vite-plugin-svgr">vite-plugin-svgr</a> : vite에서 svg를 사용하기 위해 사용.</li>
</ul>
<h2 id="하고-싶었던-것--알게된-키워드">하고 싶었던 것 &amp; 알게된 키워드</h2>
<h3 id="늘어나는-테두리-이미지--border-image">늘어나는 테두리 이미지 : border image</h3>
<p>종이 청첩장에서 사용된 이미지는 가로 세로가 고정이었는데, 화면에 맞춰서 가로와 세로 비율이 늘어나도록 하고 싶었습니다.</p>
<p>저도 처음 써봐서 이렇게 쓰는게 맞는지는 잘 모르겠지만, css <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/border-image">border-image</a> 프로퍼티를 이용해서 이미지 테두리를 적용했습니다.</p>
<pre><code>border-image-source: url(&#39;/background.png&#39;); /* 이미지 경로 설정 */
border-image-slice: 30% 49%; /* 이미지의 크기 설정 */
border-image-width: 280px; /* 테두리 이미지의 너비 설정 */</code></pre><h3 id="좋아요-공유-위로-가기-기능을-플로팅바로-만들기">좋아요, 공유, 위로 가기 기능을 플로팅바로 만들기</h3>
<p>단 초대문구나 갤러리를 볼때 방해되지 않도록, 갤러리의 일정부분을 넘어가면 나오도록 만들려고 했습니다.</p>
<p>특정 위치에서부터 fixed된 컴포넌트가 보이게 하고 싶어서 useRef를 이용했습니다.</p>
<pre><code class="language-jsx">const [isVisible, setIsVisible] = useState(false);
  const galleryRef = useRef(null);

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

  const checkScrollPosition = () =&gt; {
    if (galleryRef.current) {
      const { offsetTop } = galleryRef.current;
      const scrollPosition = window.scrollY;

      if (scrollPosition &gt;= offsetTop) {
        setIsVisible(true);
      } else {
        setIsVisible(false);
      }
    }
  };

// 위치가 필요한 부분에 ref 달고
&lt;Wrapper ref={galleryRef}&gt;
&lt;/Wrapper&gt;

// 실제 사용하는 곳에서는 isVisible 옵션에 따라서 display 할지 말지
&lt;FloatingBar isVisible={isVisible} /&gt;</code></pre>
<hr>
<h1 id="결과물">결과물</h1>
<blockquote>
<p>공유버전을 위해 새로 레포를 팠습니다. 스타일도 더 좋게(?) 변경했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/4b7fb8d9-de87-41e9-88b1-e626cfb68d0b/image.gif" alt=""></p>
<p>귀여운 컨페티가 팡팡팡 🎉🎉🎉
<img src="https://velog.velcdn.com/images/heyday_xz/post/79f7058e-ba8e-4b47-ab53-a05ad90c19d5/image.gif" alt=""></p>
<p>실제 버전 청첩장에는 하트를 누를때마다 컨페티가 터지면서 숫자가 올라가도록 했습니다!
<img src="https://velog.velcdn.com/images/heyday_xz/post/f8a40ac6-967f-448c-ac4f-0c967e482b89/image.png" alt=""></p>
<hr>
<h1 id="후기">후기</h1>
<h2 id="어려웠던-점">어려웠던 점</h2>
<h3 id="디자인">디자인</h3>
<p>종이 청첩장과 동시에 기획했기 때문에 무료템플릿을 발견하기 전까진 엄청나게 못생긴 화면이었습니다. 아마 그 디자인이었다면 오픈소스로 만들 생각은 못했을 거에요.
종이 청첩장 디자인이 없었다면 산으로 갔을 디자인...이었습니다.</p>
<p>프론트엔드 개발자로서 어떤게 예쁜지, 무엇과 잘 어울리는지, 어떤 형태가 편리한지는 알아도 디자인은 역시 다른 영역이라는 것을 깨달았습니다.</p>
<h2 id="좋았던-점">좋았던 점</h2>
<p>코드 리뷰를 받을 수 없어서 코드의 퀄리티나 완성도는 떨어지지만, 어쨌든 프로젝트를 처음부터 끝까지 기획하고 진행했다는 점에서 큰 의의가 있었습니다. 막연한 두려움을 이겨내고 나도 할 수 있구나를 깨달았달까요. 트러블슈팅과정에서 자신감도 얻었습니다.</p>
<p>또 제 청첩장을 좋게 봐주셔서, 종이청첩장 무료템플릿을 이용했던 스몰빅웨딩에서 저에게 외주 프로젝트를 주셨습니다. (자랑)
Next 14버전을 공부하면서 적용해볼 겸 만들었습니다.
[스몰빅웨딩 모바일 청첩장 샘플] (<a href="https://invitation.smallbigwedding.kr/dearletter_sample">https://invitation.smallbigwedding.kr/dearletter_sample</a>) 도 구경해보세요!</p>
<h3 id="혹시나-모바일-청첩장을-만드실-분들을-위한-제언">혹시나 모바일 청첩장을 만드실 분들을 위한 제언</h3>
<p>돈을 받는 창구가 다양하면 좋지 않겠나 하고 토스, 카카오페이, 계좌번호 복사 기능 다 담아보았는데요. 실제로 받아본 결과 창구가 많은 건 큰 의미가 없었습니다. 편하게 계좌번호 복사정도만 이용하시면 좋을 것 같아요.</p>
<h1 id="마무리">마무리</h1>
<p>긴 글을 읽어주셔서 감사합니다.
미흡하지만 처음부터 오픈소스로 만들려고 구상하고 열심히 만들었어서 애정이 가득합니다.
코드리뷰 적극 환영하며, 이슈를 발견하시면 깃헙이슈에 남겨주세요!</p>
<p>더불어, 행복하게 잘 살겠습니다. 🤵🏻 💌 👰🏻‍♀️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSR / SSR with Next.js]]></title>
            <link>https://velog.io/@heyday_xz/CSR-SSR-with-Next.js</link>
            <guid>https://velog.io/@heyday_xz/CSR-SSR-with-Next.js</guid>
            <pubDate>Fri, 30 Jun 2023 08:45:46 GMT</pubDate>
            <description><![CDATA[<p>원티드 프론트엔드 챌린지 7월 - CSR / SSR with Next.js</p>
<h3 id="csrclient-side-rendering이란-무엇이며-그것의-장단점에-대하여-설명해주세요">CSR(Client-side Rendering)이란 무엇이며, 그것의 장단점에 대하여 설명해주세요.</h3>
<p><img src="https://velog.velcdn.com/images/heyday_xz/post/1a31d3c3-8adf-4c3a-9f7e-77ba255926a9/image.png" alt="">
클라이언트 사이드 렌더링은 브라우저가 서버로부터 HTML, JS를 받아와 동적으로 렌더링한다. 
페이지에 렌더링 후 상호작용에 따라 필요한 데이터를 서버에 요청한다.</p>
<h4 id="장점">장점</h4>
<ul>
<li>서버가 빈 뼈대만 들어있는 html 넘겨주는 역할만 수행하기 때문에 서버 부하가 적다. </li>
<li>page 전체를 요청하지 않고, 유저 행동에 따라 변화된 부분만 re-rendering이 진행되기때문에 SSR보다 빠른 interaction이 가능하다.</li>
<li>Lazy loading을 지원한다.</li>
<li>클라이언트에서 연산, 라우팅 등을 직접 처리하기 때문에 반응속도가 빠르다.</li>
<li>js가 동적으로 돔을 생성하기 때문에 html은 js 로직은 완전이 연결된 상태라 사용자가 보는 시점과 이용할 수 있는 시점이 동일하다. TTV === TTI</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>요청이 전달되기 전까지 body가 비어있기때문에 SEO가 효율적으로 동작하지 않아 검색 노출 면에서 불리하다.</li>
<li>초기 로딩이 오래 걸릴 수 있다. long TTV(Time To View) short TTI(Time To Interact)</li>
</ul>
<h3 id="spasingle-page-application로-구성된-웹-앱에서-ssrserver-side-rendering이-필요한-이유에-대하여-설명해주세요">SPA(Single Page Application)로 구성된 웹 앱에서 SSR(Server-side Rendering)이 필요한 이유에 대하여 설명해주세요.</h3>
<ol>
<li><p>SEO 최적화
서버에서 렌더링된 페이지를 사용하면, 검색엔진 크롤러가 웹페이지의 콘텐츠와 메타태그를 쉽게 읽을 수 있어 웹사이트 검색 엔진 순위를 향상시킬 수 있다.</p>
</li>
<li><p>성능 및 초기 로딩 시간 개선
SPA는 종종 크기가 크고 다운로드 시간이 길어져 지연이 발생할 수 있는데, SSR을 사용하면 서버가 클라이언트에게 완전히 렌더링된 페이지를 전송하기 때문에, 애플리케이션을 백그라운드에서 로딩하는 동안 페이지를 보고 상호작용을 시작할 수 있다.</p>
</li>
<li><p>소셜 미디어 미리보기 제공
링크를 소셜 미디어 플랫폼에 공유할 때, 해당 플랫폼은 웹페이지의 콘텐츠 미리보기를 생성하려고 시도하는데, 대부분의 플랫폼은 자바스크립트를 실행하지 않기 때문에 SSR 없이는 SPA에서 미리보기를 생성할 수 없다. SSR을 사용하면 이런 플랫폼들이 웹사이트의 콘텐츠를 쉽게 파악하고 미리보기를 생성할 수 있다.</p>
</li>
<li><p>더 나은 사용자 경험 제공
SPA가 동작하는 데 필요한 자바스크립트가 로딩되는 동안 사용자에게 빈 화면이나 로딩 화면을 보여주는 대신, SSR을 사용하면 완전히 렌더링된 상호작용 가능한 페이지를 즉시 제공할 수 있다.</p>
</li>
</ol>
<h3 id="nextjs-프로젝트에서-yarn-startor-npm-run-start-스크립트를-실행했을-때-실행되는-코드를-nextjs-github-레포지토리에서-찾은-뒤-해당-파일에-대한-간단한-설명을-첨부해주세요">Next.js 프로젝트에서 yarn start(or npm run start) 스크립트를 실행했을 때 실행되는 코드를 Next.js Github 레포지토리에서 찾은 뒤, 해당 파일에 대한 간단한 설명을 첨부해주세요.</h3>
<p><a href="https://nextjs.org/docs/getting-started">https://nextjs.org/docs/getting-started</a> (Next.js 세팅 가이드)
<a href="https://github.com/vercel/next.js/">https://github.com/vercel/next.js/</a> (Next.js Github 레포지토리)</p>
<p>Next.js 프로젝트에서 <code>yarn start</code>를 하게 되면</p>
<pre><code class="language-json">// package.json
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;, 
    &quot;lint&quot;: &quot;next lint&quot;
  },</code></pre>
<p><code>next start</code> 가 실행된다.</p>
<p>Next.js Github 레포의 <a href="https://github.com/vercel/next.js/blob/canary/packages/next/src/cli/next-start.ts">packages/next/src/cli/next-start.ts</a> 에서 어떤 코드들이 실행되는지 확인할 수 있다.
4번까지의 내용은 <a href="https://velog.io/@rnrn99/next-start%EB%A5%BC-%EC%9E%85%EB%A0%A5%ED%96%88%EC%9D%84-%EB%95%8C-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EC%9D%BC%EB%93%A4">여기서</a> 참고했다.</p>
<h4 id="1-유효한-옵션-타입-명시-및-유효하지-않은-입력에-대한-에러처리">1. 유효한 옵션 타입 명시 및 유효하지 않은 입력에 대한 에러처리</h4>
<pre><code class="language-Typescript">  const validArgs: arg.Spec = {
    // Types
    &#39;--help&#39;: Boolean,
    &#39;--port&#39;: Number,
    &#39;--hostname&#39;: String,
    &#39;--keepAliveTimeout&#39;: Number,

    // Aliases
    &#39;-h&#39;: &#39;--help&#39;,
    &#39;-p&#39;: &#39;--port&#39;,
    &#39;-H&#39;: &#39;--hostname&#39;,
  }
  let args: arg.Result&lt;arg.Spec&gt;
  try {
    args = arg(validArgs, { argv })
  } catch (error) {
    if (isError(error) &amp;&amp; error.code === &#39;ARG_UNKNOWN_OPTION&#39;) {
      return printAndExit(error.message, 1)
    }
    throw error
  }</code></pre>
<h4 id="2-help-명령어-입력시-프린트">2. help 명령어 입력시 프린트</h4>
<pre><code class="language-Typescript">  if (args[&#39;--help&#39;]) {
    console.log(`
      Description
        Starts the application in production mode.
        The application should be compiled with \`next build\` first.

      Usage
        $ next start &lt;dir&gt; -p &lt;port&gt;

      &lt;dir&gt; represents the directory of the Next.js application.
      If no directory is provided, the current directory will be used.

      Options
        --port, -p          A port number on which to start the application
        --hostname, -H      Hostname on which to start the application (default: 0.0.0.0)
        --keepAliveTimeout  Max milliseconds to wait before closing inactive connections
        --help, -h          Displays this message
    `)
    process.exit(0)
  }</code></pre>
<h4 id="3-서버-실행에-필요한-변수선언-입력받은-값-또는-기본값으로-선언-초기화">3. 서버 실행에 필요한 변수선언. 입력받은 값 또는 기본값으로 선언, 초기화</h4>
<pre><code class="language-Typescript">  const dir = getProjectDir(args._[0])
  const host = args[&#39;--hostname&#39;]
  const port = getPort(args)</code></pre>
<h4 id="4-keepalivetimeout-옵션-처리-에러-발생시-문구-프린트-후-실행-종료">4. keepAliveTimeout 옵션 처리. 에러 발생시 문구 프린트 후 실행 종료</h4>
<pre><code class="language-Typescript">  const keepAliveTimeoutArg: number | undefined = args[&#39;--keepAliveTimeout&#39;]
  if (
    typeof keepAliveTimeoutArg !== &#39;undefined&#39; &amp;&amp;
    (Number.isNaN(keepAliveTimeoutArg) ||
      !Number.isFinite(keepAliveTimeoutArg) ||
      keepAliveTimeoutArg &lt; 0)
  ) {
    printAndExit(
      `Invalid --keepAliveTimeout, expected a non negative number but received &quot;${keepAliveTimeoutArg}&quot;`,
      1
    )
  }

  const keepAliveTimeout = keepAliveTimeoutArg
    ? Math.ceil(keepAliveTimeoutArg)
    : undefined</code></pre>
<h4 id="5-config-객체-가져오기">5. config 객체 가져오기</h4>
<pre><code class="language-Typescript">  const config = await loadConfig(
    PHASE_PRODUCTION_SERVER,
    resolve(dir || &#39;.&#39;),
    undefined,
    undefined,
    true
  )</code></pre>
<h4 id="5-1-loadconfig-는-이곳에서-확인할-수-있다">5-1. <a href="https://github.com/vercel/next.js/blob/canary/packages/next/src/server/config.ts#L721">loadConfig</a> 는 이곳에서 확인할 수 있다.</h4>
<p>config file 이 있을 경우엔 그 파일을 사용하고 없으면 defaultConfig를 쓴다.</p>
<pre><code class="language-Typescript">const completeConfig = assignDefaults(
    dir,
    defaultConfig,
    silent
  ) as NextConfigComplete
  completeConfig.configFileName = configFileName
  setHttpClientAndAgentOptions(completeConfig)
  return completeConfig</code></pre>
<p>항상 assignDefaults를 호출하여 위와 같은 설정을 보장한다.
next.config.js가 없어도 reactRoot를 정확하게 업데이트할 수 있다.</p>
<h4 id="6-지금까지-입력했던-정보들과-config로-startserver-실행">6. 지금까지 입력했던 정보들과 config로 startServer 실행</h4>
<pre><code class="language-Typescript">  await startServer({
    dir,
    isDev: false,
    hostname: host,
    port,
    keepAliveTimeout,
    useWorkers: !!config.experimental.appDir,
  })
}</code></pre>
<h4 id="6-1-startserver-는-이곳에서-확인할-수-있다">6-1. <a href="https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/start-server.ts#L33">startServer</a> 는 이곳에서 확인할 수 있다.</h4>
<pre><code class="language-Typescript">// setup server listener as fast as possible
  const server = http.createServer(async (req, res) =&gt; {
    try {
      if (handlersPromise) {
        await handlersPromise
        handlersPromise = undefined
      }
      sockets.add(res)
      res.on(&#39;close&#39;, () =&gt; sockets.delete(res))
      await requestHandler(req, res)
    } catch (err) {
      res.statusCode = 500
      res.end(&#39;Internal Server Error&#39;)
      Log.error(`Failed to handle request for ${req.url}`)
      console.error(err)
    }
  })</code></pre>
<p>여기서 서버 객체를 만들고 router worker와 router worker를 위한 proxy도 만드는 걸로 보인다. </p>
<pre><code class="language-Typescript">      const routerWorker = new Worker(renderServerPath, {
        numWorkers: 1,
        // TODO: do we want to allow more than 10 OOM restarts?
        maxRetries: 10,
        forkOptions: {
          execArgv: await genRouterWorkerExecArgv(
            isNodeDebugging === undefined ? false : isNodeDebugging
          ),
          env: {
            FORCE_COLOR: &#39;1&#39;,
            ...((initialEnv || process.env) as typeof process.env),
            PORT: port + &#39;&#39;,
            NODE_OPTIONS: getNodeOptionsWithoutInspect(),
            ...(process.env.NEXT_CPU_PROF
              ? { __NEXT_PRIVATE_CPU_PROFILE: `CPU.router` }
              : {}),
            WATCHPACK_WATCHER_LIMIT: &#39;20&#39;,
          },
        },
        exposedMethods: [&#39;initialize&#39;],
      }) as any as InstanceType&lt;typeof Worker&gt; &amp; {
        initialize: typeof import(&#39;./render-server&#39;).initialize
      }
      ...</code></pre>
<p>서버 종료시 소켓과 워커의 실행도 종료하는 코드도 확인할 수 있다.</p>
<pre><code class="language-Typescript">// return teardown function for destroying the server
  async function teardown() {
    server.close()
    sockets.forEach((socket) =&gt; {
      sockets.delete(socket)
      socket.destroy()
    })

    if (worker) {
      await worker.end()
    }
  }
  return teardown</code></pre>
<p>아직 Next.js에 대한 내공이 없어 코드상 어떤 코드가 어떤 역할을 하는지 파악하기 어려운 부분이 있는데, 앞으로의 공부를 해서 빈 부분을 열심히 채워나가야겠다!</p>
]]></description>
        </item>
    </channel>
</rss>