<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>d0or_hyeok.log</title>
        <link>https://velog.io/</link>
        <description>FE 탐구생활 🙂</description>
        <lastBuildDate>Wed, 31 Jul 2024 02:54:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>d0or_hyeok.log</title>
            <url>https://velog.velcdn.com/images/d0or_hyeok/profile/b04ec37c-eae9-49ae-8ed9-5a6cadc7c313/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. d0or_hyeok.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/d0or_hyeok" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[e-commerce 프로젝트 리팩토링]]></title>
            <link>https://velog.io/@d0or_hyeok/e-commerce-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-4j8i3jue</link>
            <guid>https://velog.io/@d0or_hyeok/e-commerce-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-4j8i3jue</guid>
            <pubDate>Wed, 31 Jul 2024 02:54:03 GMT</pubDate>
            <description><![CDATA[<h2 id="0-시작">0. 시작</h2>
<p>회사에 프론트엔드 개발자로 일하며 어느덧 2년차가 되어가는 와중에 npm 내가 쓰기위한 라이브러리를 배포해보는 개인 프로젝트들은 진행해 봤지만 취업준비를 했던 때 처럼 웹사이트를 만들어보는 프로젝트를 다시 한번 진행해보고 싶었다.</p>
<p>새로운 주제를 잡고 만들어 볼까 싶었지만 예전에 진행한 지금 와서 생각하면 아쉬웠던 프로젝트를 리팩토링 해야겠다는 마음에 시작하게되었다.</p>
<h2 id="1-기존-프로젝트-분석">1. 기존 프로젝트 분석</h2>
<h3 id="💡-주제-및-개기">💡 주제 및 개기</h3>
<ul>
<li>주제: 의류 쇼핑몰 (e-commerce)</li>
<li>하나의 사이트를 만들어보는 프로젝트를 진행해보고 싶었음</li>
<li>typescript, nextjs를 부딪치며 숙달하고자 진행</li>
</ul>
<h3 id="🛠-사용기술">🛠 사용기술</h3>
<ul>
<li>typescript</li>
<li>nextjs</li>
<li>redux toolkit</li>
<li>mongoDB</li>
<li>next-auth</li>
</ul>
<h3 id="⚙-기능">⚙ 기능</h3>
<ul>
<li>유저 및 배송지 관리</li>
<li>상품 (카테고리, 사이즈, 색상)</li>
<li>결제 및 환불 (부트페이 사용)</li>
<li>좋아요, 장바구니 기능</li>
<li>상품 리뷰</li>
</ul>
<h3 id="🔧-개선사항">🔧 개선사항</h3>
<p>기술에 대해 미숙한 상태로 진행한점과 기획과 명확한 기능 명세를 정리하지 않고 개발을 하다보니 그때 그때 생각난 기능을 개발하는데 급급했었던 프로젝트였다.
그러다보니 기능은 동작하지만 최적화를 하지못하여 느리고 버벅거리는 현상이 많았었다.</p>
<ul>
<li>기획을 제대로 하지 못하여 중간중간 데이터베이스를 수정하는 일이 잦았고 그에 따라 전체적인 코드들의 수정사항이 너무나도 많았었다.</li>
<li>typescript 에 익숙하지 않아 interface나 type들을 중복해서 만드는 일이 많았다.</li>
<li>react, nextjs, 상태관리에 대해 미숙했기에 전체적으로 렌더링 최적화를 진행하지 못했었다. <del>(이미지 최적화라도 했으면 좋았을텐데..)</del></li>
<li>데이터 캐싱을 신경쓰지 못하였다.</li>
</ul>
<p>다른 개선사항들도 있었겠지만 위 사항들이 가장 아쉬웠던 부분들이 아니었나 생각이 들었다.</p>
<h2 id="2-재기획">2. 재기획</h2>
<h3 id="🛠-기술">🛠 기술</h3>
<p>개발전에 사용할 기술을 다시 정리해보자</p>
<ul>
<li>typescript</li>
<li>nextjs </li>
<li>postgresql</li>
<li>prisma</li>
<li>상태관리 (고민중... zustand?)</li>
<li>next-auth</li>
<li>tailwind, postcss</li>
<li>eslint, prettier, stylelint, commitlint</li>
</ul>
<h3 id="⚙-기능-1">⚙ 기능</h3>
<p>기존 기능들에 실제 의류쇼핑몰들에 사용되는 기능들까지 추가하고자 한다.</p>
<ul>
<li>유저 (배송지 및 등록카드 관리)</li>
<li>상품 (색상, 카테고리, 사이즈, 재고관리)</li>
<li>할인, 포인트 및 쿠폰 기능</li>
<li>상품 리뷰 및 Q&amp;A</li>
<li>쇼핑몰 커뮤니티 (공지 및 Q&amp;A)</li>
<li>결제 (PG연동x) 및 환불</li>
<li>관리자 (상품, 게시글 관리 기능)</li>
</ul>
<p>필요한 기능 구현사항들 정리</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>기능</th>
<th>상세 기능</th>
<th>설명</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>1. 회원가입/로그인</td>
<td>1.1 이메일가입</td>
<td>1.1.1 약관동의</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td>1.1.2 아이디, 비밀번호, 이름, 휴대전화, 이메일</td>
<td>정보입력</td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td>1.1.3 가입완료</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>1.2 소셜로그인</td>
<td>구글, 네이버 등</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>1.3 비회원 주문조회</td>
<td>주문번호 및 주문비밀번호 입력</td>
<td></td>
<td></td>
</tr>
<tr>
<td>2. 네비게이션</td>
<td></td>
<td>브랜드, 남성, 여성, 룩북, 커뮤니티, 로고, 검색, 장바구니, 회원</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>검색</td>
<td>오버레이 형식, 인기키워드(#사용)</td>
<td></td>
<td></td>
</tr>
<tr>
<td>3. 푸터</td>
<td></td>
<td>로고, 정책, 몰정보, 고객센터, 몰sns</td>
<td></td>
<td></td>
</tr>
<tr>
<td>4. 홈</td>
<td>4.1 신상품</td>
<td>신상품 슬라이드</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>4.2 인기상품</td>
<td>인기상품 슬라이드 or 그리드</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>4.3 룩북</td>
<td>룩북 슬라이드</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>4.4 가입</td>
<td>비로그인 시 가입 환영 섹션</td>
<td></td>
<td></td>
</tr>
<tr>
<td>5. 상품조회</td>
<td>5.1 카테고리 선택</td>
<td>상단에 카테고리 선택</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>5.2 인기상품</td>
<td>인기상품 슬라이드</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>5.3 상품목록</td>
<td>그리드 형식으로 페이징 처리하여 보여주기</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>5.4 필터</td>
<td>상품검색필터</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>5.5 상품카드</td>
<td>위시리스트, 상품명, 가격 및 할인, 색상 종류, 썸네일</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td>위시리스트 토글, 썸네일하나 또는 2장(앞/뒤 이미지)</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td>할인중일 경우 남은 할인기간 표시</td>
<td></td>
<td></td>
</tr>
<tr>
<td>6. 상품상세</td>
<td>6.1 상품선택</td>
<td>상품 정보와 함께 상품 옵션선택 및 구매/장바구니/찜 기능</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>6.2 상품리뷰</td>
<td>회원 정보(이름, 키, 몸무게) 구매 정보 (색상,사이즈 등)</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td>구매한 회원만 리뷰 작성 가능</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>6.3 상품문의</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>7. 장바구니</td>
<td></td>
<td>회원/비회원 상관없이 조회 가능</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td>비회원일때 담은 정보 로그인 후에도 동기화</td>
<td></td>
<td></td>
</tr>
<tr>
<td>8. 커뮤니티</td>
<td>8.1 공지</td>
<td>몰 공지</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>8.2 리뷰</td>
<td>리뷰 모음</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>8.3 Q&amp;A</td>
<td>몰에대한 질문</td>
<td></td>
<td></td>
</tr>
<tr>
<td>9. 주문</td>
<td>9.1 배송지</td>
<td>배송지 입력/선택/추가</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>9.2 주문목록</td>
<td>상품목록나열</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>9.3 할인정보</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>9.4 결제정보</td>
<td>상품, 배송비, 할인정보, 포인트사용정보 등</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>9.5 포인트 적립</td>
<td>포인트 적립안내</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>9.6 결제수단</td>
<td>카드등록/선택 또는 무통장</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>9.7 약관동의</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>10. 마이페이지</td>
<td>10.1 쇼핑정보</td>
<td>주문/배송, 취소/반품</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>10.2 해택</td>
<td>적립, 쿠폰</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>10.3 활동정보</td>
<td>회원정보 수정, 주소관리, 리뷰관리, 문의관리, 찜/장바구니, 탈퇴</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h3 id="🔩-db-설계">🔩 DB 설계</h3>
<p>필요한 테이블 종류들만 간단히 나열</p>
<ul>
<li>유저</li>
<li>상품</li>
<li>상품상세(색상, 사이즈, 재고)</li>
<li>카테고리, 색상, 사이즈</li>
<li>상품 리뷰</li>
<li>상품문의, 상품문의 답변</li>
<li>위시리스트 (상품에대한 정보만)</li>
<li>장바구니 (상품상제정보까지)</li>
<li>쿠폰, 포인트</li>
<li>할인</li>
<li>주문 (여러 상품을 한번에 구매할때)</li>
<li>주문상세 (주문내의 각각 상품 정보 담김)</li>
<li>배송지</li>
<li>커뮤니티, 몰 문의</li>
<li>룩북</li>
</ul>
<h2 id="">...</h2>
<p>프로젝트를 시작하기 앞서 기획을 간단하게나마 정리를 해보니 이전에는 얼마나 막무가네로 시작했는지를 보고 어질어질한 느낌이다...</p>
<p>이제 간단하게 정리한 내용들을 기반으로 화면을 생각해보고 DB를 명확히 설계한 후에 프로젝트 진행을 할 것이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Scrollbar 만들기 (with Virtual scroll) ]]></title>
            <link>https://velog.io/@d0or_hyeok/Scrollbar-%EB%A7%8C%EB%93%A4%EA%B8%B0-with-Virtual-scroll</link>
            <guid>https://velog.io/@d0or_hyeok/Scrollbar-%EB%A7%8C%EB%93%A4%EA%B8%B0-with-Virtual-scroll</guid>
            <pubDate>Thu, 30 Nov 2023 06:04:57 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p><a href="https://velog.io/@d0or_hyeok/JS-Virtual-Scroll-%EB%A7%8C%EB%93%A4%EA%B8%B0">[Virtual Scroll 만들기]</a> 해당글의 virtual scroll을 구현하고나서 스크롤바도 직접 구현하면 재미있겠다!(?) 라는 생각으로 시작했다.</p>
<h3 id="💡-concept">💡 Concept</h3>
<blockquote>
<p>스크롤 영역에 마우스 호버 시 영역위에 스크롤이 보여지는 형태로 구현하자!</p>
</blockquote>
<h2 id="🔧-구현">🔧 구현</h2>
<p> 구현은 서론에서 언급한 글의 virtual scroll을 확장하여 구현하였다.</p>
<h3 id="이전-코드---virtual-scroll">이전 코드 - Virtual Scroll</h3>
<p>!codepen[niyzvcda-the-bashful/embed/LYqeYJZ?default-tab=js%2Cresult]</p>
<h3 id="템플릿-확장">템플릿 확장</h3>
<h5 id="html">HTML</h5>
<pre><code class="language-html">&lt;div class=&quot;wrapper&quot;&gt;
    &lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;scroll-content&quot;&gt;
        &lt;div class=&quot;content&quot;&gt;
        &lt;table role=&quot;presentation&quot;&gt;
            &lt;thead role=&quot;presentation&quot;&gt;&lt;/thead&gt;
            &lt;tbody role=&quot;presentation&quot;&gt;&lt;/tbody&gt;
            &lt;tfoot role=&quot;presentation&quot;&gt;&lt;/tfoot&gt;
        &lt;/table&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;!-- New Template --&gt;
    &lt;div class=&quot;sb&quot;&gt;
        &lt;div class=&quot;sb-thumb&quot;&gt;
        &lt;div class=&quot;sb-thumb-content&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h5 id="css">CSS</h5>
<pre><code class="language-css">/* ... */

.sb {
  position: absolute;
  pointer-events: auto;
  top: 0;
  right: 0;
  height: 100%;
  transition: background-color 0.5s linear 1s;
}

.sb:hover,
.sb:has(.sb-thumb-active) {
  background-color: rgba(207, 218, 233, 0.33);
  transition: background-color 0.15s linear 0.15s;
}

.sb .sb-thumb {
  width: 8px;
  height: 0;
  padding: 0 2px;
  transition:
    width 0.2s linear 0.15s,
    transform 0.1s ease-in-out;
}

.sb:hover .sb-thumb,
.sb .sb-thumb-active {
  width: 15px;
}

.sb-thumb-content {
  position: relative;
  width: 100%;
  height: 100%;
  transition: background-color 0.5s linear 1s;
}

.sb-thumb-content::after {
  content: &#39;&#39;;
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: transparent;
  transition: background-color 0.15s linear 0.15s;
}

.wrapper:hover .sb-thumb-content {
  transition: background-color 0.15s linear 0.15s;
  background-color: #b4b4b4;
}

.sb-thumb:is(:active, .sb-thumb-active) .sb-thumb-content::after {
  background-color: rgba(56, 149, 225, 0.8);
}</code></pre>
<h3 id="스크롤-막대의-높이-계산">스크롤 막대의 높이 계산</h3>
<p>너비와 높이, 데이터의 양이 고정된 템플릿에서 스크롤 막대의 높이를 계산한다.</p>
<pre><code class="language-javascript">const minScrollThumbHeight = 20 // 막대의 최소크기

const scrollThumbRatio = viewportHeight / scrollHeight; // 전체 컨텐츠와 보여지는 영역의 비율
const thumbHeight = viewportHeight * scrollThumbRatio
const scrollThumbHeight = Math.max(thumbHeight, minScrollThumbHeight)</code></pre>
<h3 id="스크롤-막대-이동거리-계산">스크롤 막대 이동거리 계산</h3>
<p>위에서 계산한 높이를 토대로 이동거리를 계산한다.</p>
<pre><code class="language-javascript">/**
 * @param {number} scrollTop
 */
function calculateTranslate(scrollTop) {
  const maxTranslateY = viewportHeight - scrollThumbHeight // 막대의 최대 이동거리
  const translateRatio = scrollTop / scrollHeight
  const translateY = Math.min(translateRatio * viewportHeight, maxTranslateY)
  return translateY
}</code></pre>
<h4 id="❗-문제점">❗ 문제점</h4>
<p>하지만 위와 같이 계산할 경우 thumbHeight &lt; scrollThumbHeight 경우에 offset의 끝에서 scrollThumbHeight - thumbHeight의 차이의 translate 영역에 해당하는 데이터는  maxTranslateY의 크기제한으로 인해 볼 수 없게되는 문제가 발생한다.</p>
<h4 id="🔨-개선">🔨 개선</h4>
<p>실제 개산한 막대크기와 최소 막대크기의 차이만큼을 viewportHeight에서 빼준 값을 스크롤바 영역의 길이로 계산하여 translate값을 구하면 위의 문제점을 해결할 수 있다. </p>
<pre><code class="language-javascript">const thumbDiff = scrollThumbHeight - thumbHeight의
const scrollbarHeight = viewportHeight - thumbDiff

/**
 * @param {number} scrollTop
 */
function calculateTranslate(scrollTop) {
  ...
  const translateY = Math.min(translateRatio * scrollbarHeight, maxTranslateY)
  ...
}</code></pre>
<h4 id="✔-결과---offsets-계산-함수와-병합">✔ 결과 - offsets 계산 함수와 병합</h4>
<pre><code class="language-javascript">// 스크롤 막대 높이 계산
const scrollThumbRatio = viewportHeight / scrollHeight;
const thumbHeight = viewportHeight * scrollThumbRatio;
const scrollThumbHeight = Math.max(thumbHeight, minScrollThumbHeight);
const scrollThumbDiff = scrollThumbHeight - thumbHeight;
const scrollbarHeight = viewportHeight - scrollThumbDiff;

/**
 * @param {number} scrollTop
 */
function calculateVariables(scrollTop) {
  const passNodeCount = Math.floor(scrollTop / rowHeight); // 지나온 노드의 개수
  const maxStartIndex = dataSize - visibleNodeCount; // startIndex의 최대값

  // 인덱스 계산
  let startIndex = Math.min(Math.max(passNodeCount - nodePadding, 0), maxStartIndex);
  const endIndex = startIndex + visibleNodeCount;
  const offsets = [startIndex, endIndex];

  // 스크롤 막대 이동거리 계산
  const maxTranslateY = viewportHeight - scrollThumbHeight;
  const translateRatio = scrollTop / scrollHeight;
  const translateY = Math.min(translateRatio * scrollbarHeight, maxTranslateY);

  return { offsets, translateY };
}</code></pre>
<p>함수가 병합되었으므로 해당 함수를 사용하던 onScrollChange 함수를 업데이트 해준다.</p>
<pre><code class="language-javascript">function onScrollChange(newScrollTop) {
  const { offsets, translateY } = calculateVariables(newScrollTop);
  render(offsets);
  $container.scrollTop = newScrollTop;
  $thumb.style.transform = `translateY(${translateY}px)`; // 막대 이동
}</code></pre>
<h3 id="스크롤-이벤트-등록">스크롤 이벤트 등록</h3>
<p>scrollTop에 따른 가변값을 계산 함수를 구현하였으니, 이제는 이벤트를 등록하여 실제로 움직일 수 있도록 할 차례이다.</p>
<p>하지만 여기서 우리는 의문을 가질 수 있다.</p>
<p><em>스크롤이 비활성화 되어있는 상태에서 어떻게 스크롤 이벤트를 발생시키지?</em></p>
<p>이제 대한 방안으로 나는 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event">Wheel event</a>를 사용하였다.</p>
<h4 id="wheel-scroll">Wheel Scroll</h4>
<p>MDN Web Docs를 보면 wheel event에는 delta라는 속성이 존재하는것을 볼 수 있다.
delta 값은 우리가 스크롤을 한번 이동하면 Chrome을 기준으로 100의 값이 발생한다.
(아래로 스크롤 시 100, 위로 스크롤 시 -100)</p>
<p>이 delta 값을 scrollTop에 더해주는 것으로 스크롤 이동을 구현하였다.</p>
<pre><code class="language-javascript">let scrollTop = 0

$container.addEventListener(&#39;wheel&#39;, (event) =&gt; {
  if (event.shiftKey === false) {
    const step = event.deltaY
    const moveScrollTop = scrollTop + step
    const newScrollTop = Math.max(Math.min(moveScrollTop, maxScrollTop), 0)
    // 스크롤이 가능할 경우 기본 스크롤 이벤트를 막아준다.
    // 외부 스크롤영역이 존재할 경우 같이 스크롤되는 문제를 방지하기 위함
    if (moveScrollTop &gt;= 0 &amp;&amp; moveScrollTop &lt;= maxScrollTop) event.preventDefault()
    scrollTop = newScrollTop
    onScrollChange(newScrollTop)
  }
}, { passive: false })</code></pre>
<h4 id="drag-scroll">Drag Scroll</h4>
<p>컨텐츠를 넘기기 위한 다른 방법으로는 스크롤 막대를 직접 움직여서 이동하는 방법이 존재한다.</p>
<p>이에 대한 구현사항은 다음과 같다.</p>
<pre><code class="language-javascript">/** @type {HTMLElement} */
const $html = document.querySelector(&#39;html&#39;);

/** @type {null | number} */
let dragStartY = null; // 드래그 시작 y 좌표
let prevUserSelect = &#39;&#39;; // document body&#39;s previous userSelect css property value


$scrollbar.addEventListener(&#39;mousedown&#39;, (event) =&gt; {
  if ($thumb.contains(event.target)) {
    // 스크롤바에서 스크롤막대에 마우스를 누른경우
    // 마우스를 누르기 시작한 시점의 Y좌표를 기억한다
    dragStartY = event.pageY;
    $thumb.style.transition = &#39;none&#39;;
    prevUserSelect = document.body.style.userSelect;
  }
});

document.body.addEventListener(&#39;mousemove&#39;, (event) =&gt; {
  // 문서에서 마우스를 움직일경우
  if (typeof dragStartY === &#39;number&#39;) {
    // 시작Y좌표가 설정되어 있다면 스크롤 이동을 시작한다.
    event.preventDefault();
    document.body.style.userSelect = &#39;none&#39;; // 스크롤 이동시 텍스트가 선택되는것을 방지
    const currentY = event.pageY;
    const translateDelta = currentY - dragStartY; // 막대가 이동할 거리
    onTranslate(translateDelta); // 막대 이동
    dragStartY = currentY; // 드래그 시작위치를 이동한 위치로 변경
  }
});

/**
 * @param {number} translateDelta
 */
function onTranslate(translateDelta) {
  const { translateY } = calculateVariables(scrollTop);
  // 현재 막대위치에서 이동할 거리를 더하여 새롭게 이동할 scrollTop을 역산한다.
  const newScrollTop = Math.max(((translateY + translateDelta) * scrollHeight) / scrollbarHeight, 0);
  onScrollChange(newScrollTop);
  scrollTop = newScrollTop;
}

$html.addEventListener(&#39;mouseup&#39;, () =&gt; {
  // 문서 전체에 대해서
  if (typeof dragStartY === &#39;number&#39;) {
    // 시작Y좌표가 설정 되어있다면 막대이동을 종료시킨다.
    document.body.style.userSelect = prevUserSelect; // 기본 userSelect 값으로 초기화
    dragStartY = null;
    $thumb.style.transition = &#39;&#39;;
  }
});</code></pre>
<h4 id="click-scroll">Click Scroll</h4>
<p>마지막 스크롤 이동방법으로 막대가 아닌 scrollbar 영역을 클릭하여 막대를 클릭한 위치까지 이동시키는 방법이 있다.</p>
<pre><code class="language-javascript">let isTracking = false; // scrolling flag
let trackId = -1; // interval id

// 마우스가 스크롤바영역 밖으로 나가거나 마우스 클릭을 종료하면
// 해당위치까지의 추적을 종료시킨다
$scrollbar.addEventListener(&#39;mouseleave&#39;, () =&gt; (isTracking = false));
$scrollbar.addEventListener(&#39;mouseup&#39;, () =&gt; (isTracking = false));
$scrollbar.addEventListener(&#39;mousedown&#39;, (event) =&gt; {
  // 스크롤에 마우스를 누른경우
  event.stopPropagation();
  clearInterval(trackId); // interval 초기화
  // 마우스를 누른 위치가 막대영역과 겹쳐있다면 이동을 종료
  if ($thumb.contains(event.target)) return (isTracking = false);
  isTracking = true;

  const offset = event.offsetY;
  const { translateY } = calculateVariables(scrollTop);

  const minOffset = Math.max(offset - scrollThumbHeight, 0); // 막대가 아래로 이동할때 멈출 최소 offset
  // 현재막대를 기준으로 위쪽을 클릭하였다면 -1 아래를 클릭하였다면 1
  const multiplier = offset &lt; (translateY + translateY + scrollThumbHeight) / 2 ? -1 : 1;
  const delta = 100 * multiplier; // 델타값 생성
  trackId = setInterval(() =&gt; {
    if (!isTracking) clearInterval(trackId);
    // 새로운 스크롤 위치를 계산하여 이동
    scrollTop = Math.max(Math.min(scrollTop + delta, maxScrollTop), 0);
    onScrollChange(scrollTop);
    const changeY = calculateVariables(scrollTop).translateY;
    // 막대가 마우스를 누른영역과 겹칠경우 이동을 종료시킨다.
    if ((multiplier &gt; 0 &amp;&amp; changeY &gt; minOffset) || (multiplier &lt; 0 &amp;&amp; changeY &lt; offset)) isTracking = false;
  }, 33);
});</code></pre>
<h2 id="✨-결과">✨ 결과</h2>
<p>!codepen[niyzvcda-the-bashful/embed/ExreMXy?default-tab=js%2Cresult]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Virtual Scroll 만들기]]></title>
            <link>https://velog.io/@d0or_hyeok/JS-Virtual-Scroll-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@d0or_hyeok/JS-Virtual-Scroll-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 17 Nov 2023 01:19:54 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>회사에서 진행하는 프로젝트에서 10,000개 이상의 데이터를 표출하는 일이 생겼었다. 
기존에 개발된 코드에는 데이터의 개수만큼 tr 요소를 innerHTML로 삽입해 주고 있었고,</p>
<pre><code class="language-javascript">// Example
const $tableBody = document.querySelector(&#39;table tbody&#39;)
$tableBody.innerHTML = datas.map((data, index) =&gt; {
  `&lt;tr&gt;
     &lt;td&gt;${index}&lt;/td&gt;
     &lt;td&gt;${data}&lt;/td&gt;
   &lt;/tr&gt;`
}).join(&#39;&#39;)</code></pre>
<p>이로 인해 DOM을 그리는 과정에서 시간이 5초 이상 걸리거나 심할 경우 브라우저 탭이 먹통이 되어 고생했던 적이 있었다.</p>
<p>위의 문제를 해결하기 위한 방법으로는 Paging, Virutal  Scroll이 있었고 다행히도 회사에서 구매한 라이브러리 중에 가상스크롤을 지원하는 DataGrid가 있어서 빨리 해결할 수 있었고 나중에는 직접 구현해 봐야겠다는 마음을 먹고 구현하게 되었다.</p>
<h2 id="🤔-virtual-scroll">🤔 Virtual Scroll?</h2>
<p>Virutal scroll은 많은 데이터에서 사용자가 볼 수 있는 영역내에서 볼 수 있을만큼의 데이터만 표출하여 DOM을 그리는데 사용되는 리소스를 줄이는 기법이다.</p>
<p><img src="https://velog.velcdn.com/images/d0or_hyeok/post/73b26159-b922-48e0-8bc7-3fc254129402/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/d0or_hyeok/post/7882fe4c-668f-4f15-9282-ca0e26741675/image.gif" alt=""></p>
<p align="center">
[이미지출처: <a target="_blank" href="https://blog.logrocket.com/virtual-scrolling-core-principles-and-basic-implementation-in-react/">Virtual scrolling: Core principles and basic implementation in React</a>]
</p>


<h2 id="📝-용어-정의">📝 용어 정의</h2>
<p>구현에 앞서 사용할 변수들의 명칭을 다음과 같이 정의하였다.</p>
<p><img src="https://velog.velcdn.com/images/d0or_hyeok/post/6a41ef03-37c3-47ac-8fb7-a6971049fe51/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>변수명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>rowHeight</td>
<td>한개의 행(로우) 높이</td>
</tr>
<tr>
<td>totalRowHeight</td>
<td>전체 데이터가 렌더링되었을 때의 높이</td>
</tr>
<tr>
<td>scrollHeight</td>
<td>= totalRowHeight</td>
</tr>
<tr>
<td>scrollTop</td>
<td>현재 지나온 높이</td>
</tr>
<tr>
<td>viewport</td>
<td>사용자에게 보이는 영역</td>
</tr>
<tr>
<td>nodeCount</td>
<td>viewport 내에서 최대로 보여질 수 있는 행(로우) 개수</td>
</tr>
<tr>
<td>nodePadding</td>
<td>nodeCount와 함께 추가로 함께 보여질 행(로우) 개수</td>
</tr>
<tr>
<td>visibleNodeCount</td>
<td>nodePadding을 포함한 보여질 개수</td>
</tr>
<tr>
<td>scrollThumbHeight</td>
<td>스크롤막대 높이</td>
</tr>
<tr>
<td>translateY</td>
<td>스크롤 막대가 지나온 높이</td>
</tr>
<tr>
<td>scrollbarHeight</td>
<td>스크롤바의 높이</td>
</tr>
<tr>
<td>startIndex</td>
<td>visibeNodeCount에 속하는 처음 데이터의 인덱스</td>
</tr>
<tr>
<td>endIndex</td>
<td>visibleNodeCount에 속하는 마지막 데이터의 다음 인덱스</td>
</tr>
</tbody></table>
<h2 id="🔧-구현">🔧 구현</h2>
<p>이제 Virtual scroll을 구현하기 위해서 우리는 scrollTop값의 변화에 따라 offset(startIndex, endIndex)를 구하여 데이터를 표출할 것이다.</p>
<blockquote>
<p>필자는 스크롤 또한 div 태그를 통하여 별도로 구현했기에 scrollTop값 변화에 따른 translateY, scrollThumbHeight, startIndex, endIndex 등의 값을 직접 계산하였음</p>
</blockquote>
<h3 id="템플릿">템플릿</h3>
<p>구현에 앞서 우선 템플릿 만든다.</p>
<h5 id="html">HTML</h5>
<pre><code class="language-html">&lt;!-- Virtual scroll wrapper --&gt;
&lt;div class=&quot;wrapper&quot;&gt;
  &lt;!-- Scrollable container --&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;scroll-content&quot;&gt;
      &lt;div class=&quot;content&quot;&gt;
        &lt;table&gt;
          &lt;!-- Virtual top area --&gt;
          &lt;thead&gt;&lt;/thead&gt;
          &lt;!-- Data area --&gt;
          &lt;tbody&gt;&lt;/tbody&gt;
          &lt;!-- Virtual bottom area --&gt;
          &lt;tfoot&gt;&lt;/tfoot&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h5 id="css">CSS</h5>
<pre><code class="language-css">*,
*::before,
*::after {
  box-sizing: border-box;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
  table-layout: fixed;
  width: 100%;
}

td,
th {
  padding: 0;
}

td {
  padding: 3px 6px;
}

[aria-hidden=&#39;true&#39;] {
  display: none !important;
}

/* Table border */

table tr,
table td {
  border: 1px solid #d3d3d3;
  color: #333;
  font-size: 14px;
}

/* Body */

.wrapper {
  position: relative;
  width: 300px;
  height: 320px;
}

.container {
  height: calc(100% + 1px);
  margin-top: -1px;
  overflow: auto;
  width: 100%;
  border: 1px solid #d3d3d3;
  border-left-width: 0;
  border-right-width: 0;
}

.scroll-content {
  position: relative;
  overflow-anchor: none;
  min-width: 100%;
  min-height: 100%;
  display: block;
  float: left;
}

.scroll-content::before,
.scroll-content::after {
  display: table;
  content: &#39;&#39;;
  line-height: 0;
}

.scroll-content::after {
  clear: both;
}

.content {
  min-height: 100%;
  overflow-anchor: none;
  position: relative;
}</code></pre>
<h3 id="불변값-설정">불변값 설정</h3>
<p>너비와 높이가 고정된 템플릿을 기준으로 구할 수 있는 불변값을 먼저 설정한다. <em>(데이터의 개수는 원하는데로 설정)</em></p>
<pre><code class="language-javascript">const viewportHeight = 320 // rowHeight * 10
const rowHeight = 32    // 로우 높이
const nodePadding = 5    // 위아래에 추가로 같이 렌더링할 로우 개수

const datas = Array.from({ length: 100 }, (_, i) =&gt; i)
const dataSize = datas.length // 100
const totalRowHeight = rowHeight * dataSize // 3200
const scrollHeight = totalRowHeight

// 최대 scrollTop
const maxScrollTop = Math.max(scrollHeight - viewportHeight, 0)

// 화면에 보이는 개수
const nodeCount = Math.ceil(viewportHeight / rowHeight) // 10
// 실제 보여지는 개수
const visibleNodeCount = nodeCount + nodePadding * 2 // 20</code></pre>
<h3 id="scrolltop에-따른-offsets-구하기">scrollTop에 따른 offsets 구하기</h3>
<p>scrollTop은 내가 지나온 영역의 높이와 같다.
따라서 지나온 데이터의 개수를 구하는 공식은 다음과 같다.</p>
<blockquote>
<p>지나간 데이터의 개수 = 지나온 영역의 높이 / 한개의 행 높이</p>
</blockquote>
<p>이를 통해 다음과 같이 offsets을 구하는 함수를 구현하였다.</p>
<pre><code class="language-javascript">/**
 * @param {number} scrollTop
 */
function calculateOffsets(scrollTop) {
  const passNodeCount = Math.floor(scrollTop / rowHeight) // 지나온 노드의 개수
  const maxStartIndex = dataSize - visibleNodeCount // startIndex의 최대값

  // 인덱스 계산
  let startIndex = Math.min(Math.max(passNodeCount - nodePadding, 0), maxStartIndex)
  const endIndex = startIndex + visibleNodeCount
  const offsets = [startIndex, endIndex]

  return offsets
}</code></pre>
<h3 id="dom에-데이터-렌더링">DOM에 데이터 렌더링</h3>
<p>위의 과정으로 계산한 offsets를 사용하여 DOM에 데이터를 표출할 수 있어야한다.
나는 그려지지 않는 데이터들에 대해서 높이값을 설정해주고, 변화하는 offsets에 대응하여 로우를 동적으로 추가해주는 함수를 작성하였다.</p>
<pre><code class="language-javascript">const $tbody = document.querySelector(&#39;table tbody&#39;)
const $thead = $tbody.previousElementSibling
const $tfoot = $tbody.nextElementSibling

const rowMap = {} // 렌더링 된 요소를 저장할 변수

/**
 * @param {[number, number]} offsets 
 */
function render(offsets) {
  const [startIndex, endIndex] = offsets

  // 상단 하단 여백 설정
  const virtualTopHeight = Math.max(startIndex * rowHeight, 0)
  const virtualBottomHeight = Math.max((dataSize - endIndex) * rowHeight, 0)

  if (virtualTopHeight === 0) $thead.innerHTML = &#39;&#39;
  else $thead.innerHTML = `&lt;tr role=&quot;row&quot; style=&quot;height:${virtualTopHeight}px&quot;&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;`
  if (virtualBottomHeight === 0) $tfoot.innerHTML = &#39;&#39;
  else $tfoot.innerHTML = `&lt;tr role=&quot;row&quot; style=&quot;height:${virtualBottomHeight}px&quot;&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;`

  // 데이터 로우 렌더링
  datas.forEach((data, index) =&gt; {
      const rowindex = index + 1
      const mapItem = rowMap[rowindex]
      if (startIndex &lt; rowindex &amp;&amp; index &lt; endIndex) {
          // 보여질 데이터인 경우
          if (!mapItem) {
              // DOM에 추가되지 않았다면 추가해준다.
              const $tr = document.createElement(&#39;tr&#39;)
              $tr.role = &#39;row&#39;
              $tr.ariaRowIndex = `${rowindex}`
              $tr.style.height = `${rowHeight}px`
              $tr.innerHTML = `&lt;td role=&quot;gridcell&quot;&gt;Row ${rowindex}&lt;/td&gt;`

              const $after = document.querySelector(`tr[aria-rowindex=&quot;${rowindex}&quot;]`) // 그려질 로우의 다음에 올 노드
              if ($after) $tbody.insertBefore($tr, $after)
              else {
                  // 가장 가까운 인덱스가 큰 요소를 찾아 해당 요소 전에 삽입
                  const $next = Array.from($tbody.childNodes).find(($row) =&gt; rowindex &lt; Number($row.ariaRowIndex))
                  $tbody.insertBefore($tr, $next ?? null)
              }
              rowMap[rowindex] = $tr // 맵에 요소 저장
          }
      } else if (mapItem) {
          // 보이지 않는 데이터인 경우
          mapItem.remove() // DOM에 그려진 요소가 있다면 삭제
          delete rowMap[rowindex] // 맵에서 삭제
      }
  })
}

render(calculateOffsets(0)) // 초기 화면 렌더링</code></pre>
<h3 id="scrollbar와-연동">Scrollbar와 연동</h3>
<p>마지막으로 스크롤영역에 스크롤 이벤트를 통하여 scrollTop 변화에 따라 데이터를 렌더링 할 수 있도록 해준다.</p>
<pre><code class="language-javascript">const $container = document.querySelector(&#39;.container&#39;)
$container.addEventListener(&#39;scroll&#39;, (event) =&gt; {
  render(calculateOffsets(event.currentTarget.scrollTop))
});</code></pre>
<h2 id="✨-데모">✨ 데모</h2>
<p>!codepen[niyzvcda-the-bashful/embed/LYqeYJZ?default-tab=js%2Cresult]</p>
<h2 id="♻️-next">♻️ Next?</h2>
<p>이렇게 그리 길지 않은 코드로 virtual scroll을 구현할 수 있었다.</p>
<p>이 글에서는 container의 scrollTop을 연동하여 사용하였지만 스크롤을 따로 두고 컨텐츠의 translate를 조정하여 데이터를 표현하는 방법도 있고 다양한 구현방식이 존재할 것이다. 더 좋은 방식이 찾으면 그걸로도 구현해볼 예정이다...</p>
<p>중간에도 언급하였듯이 나는 스크롤바 또한 가상으로 만들어 구현하였기에 다음글에는 virtual scroll + virtual scrollbar를 구성하는 방법에 대해 올리도록 하겠다.</p>
<hr>
<p>[참조]</p>
<ul>
<li><a href="https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib">Build your Own Virtual Scroll - Part I</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Custom Server - Express]]></title>
            <link>https://velog.io/@d0or_hyeok/Next.js-Custom-Server-Express-83inziuj</link>
            <guid>https://velog.io/@d0or_hyeok/Next.js-Custom-Server-Express-83inziuj</guid>
            <pubDate>Sun, 15 Jan 2023 01:48:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/d0or_hyeok/post/8ed2c4a5-3151-4296-8564-b1ca261ef7a7/image.PNG" alt="next+express"></p>
<p>Next.js를 Express 서버와 함께 실행시키는 방법입니다.</p>
<h2 id="설치">설치</h2>
<h3 id="crn---nextjs-프로젝트를-생성">CRN - Next.js 프로젝트를 생성</h3>
<pre><code class="language-shell">npx create-next-app@latest

# √ What is your project named? ... client
# √ Would you like to use TypeScript with this project? ... No
# √ Would you like to use ESLint with this project? ... Yes</code></pre>
<table align="center">
    <tr>
        <th>기존 폴더 구조</th>
        <th>src 사용</th>
    </tr>
    <tr>
        <td><img src="https://github.com/d0orHyeok/Express-Next.js-Boiler-Plate/raw/main/docs/images/1-next.PNG"></td>
        <td><img src="https://github.com/d0orHyeok/Express-Next.js-Boiler-Plate/raw/main/docs/images/1-next-src.PNG"></td>
    </tr>
</table>

<h3 id="express-설치">Express 설치</h3>
<pre><code class="language-shell">npm install --save express dotenv

# 서버 자동 재시작을 위한 패키지
npm install --save-dev nodemon</code></pre>
<h2 id="express-서버-생성">Express 서버 생성</h2>
<p>Express 서버파일을 작성합니다.</p>
<table>
<tr>
<td>

<ul>
<li>생성한 Next.js 프로젝트 폴더에 server 폴더 생성</li>
<li>server.js 파일에 Express 서버를 실행하기 위한 코드 작성</li>
</ul>
</td>
<td><img src="https://github.com/d0orHyeok/Express-Next.js-Boiler-Plate/raw/main/docs/images/2-server.PNG"></td>
</tr>
</table>

<pre><code class="language-js">// server.js
const express = require(&#39;express&#39;);
const dotenv = require(&#39;dotenv&#39;);
dotenv.config({ path: &#39;.env&#39; }); // 환경변수 사용

const app = express();
const port = process.env.PORT; //.env 파일에서 설정해준다

app.set(&#39;port&#39;, port);
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use(&#39;/&#39;, (req, res, next) =&gt; {
  res.send(&#39;hello!&#39;);
});

app.listen(app.get(&#39;port&#39;), () =&gt; {
  console.log(`Express server listen port:${port}`);
  console.log(`http://localhost:${port}`);
});</code></pre>
<h2 id="nextjs-서버-설정">Next.js 서버 설정</h2>
<p>server/server.js 파일에 next.js 서버 설정을 추가합니다.</p>
<pre><code class="language-shell"># .env
SERVER_PORT=[YOUR_PORT]</code></pre>
<pre><code class="language-js">// server.js
const express = require(&#39;express&#39;);
const dotenv = require(&#39;dotenv&#39;);
dotenv.config({ path: &#39;.env&#39; }); // 환경변수 사용
const path = require(&#39;path&#39;);

/** Create Express */
const app = express();

/** Next.js 모듈 가져오기 */
const next = require(&#39;next&#39;);
const { parse } = require(&#39;url&#39;);

/** Next.js 설정 */
const port = process.env.SERVER_PORT;
/**
 * 개발환경이아니라면 dev 옵션을 false 로 설정하고
 * 서버 시작전에 next build 를 실행해준다.
 */
const nextApp = next({ dev: true, port });
const handle = nextApp.getRequestHandler();

nextApp
  .prepare()
  .then(() =&gt; {
    /** Express Settings */
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));
    /** static 경로 설정 */
    app.use(express.static(path.join(__dirname, &#39;../&#39;, &#39;public&#39;)));

    /** Express Router Settings */
    app.use(&#39;/api&#39;, (req, res, next) =&gt; {
      res.send(&#39;hello!&#39;);
    });

    /** Next.js Routing */
    app.get(&#39;/&#39;, (req, res) =&gt; {
      const parsedUrl = parse(req.url, true);
      const { pathname, query } = parsedUrl;
      nextApp.render(req, res, pathname, query);
    });
    app.get(&#39;*&#39;, (req, res) =&gt; {
      return handle(req, res);
    });

    app.listen(port, () =&gt; {
      console.log(`Express server listen port:${port}`);
      console.log(`http://localhost:${port}`);
    });
  })
  .catch((ex) =&gt; {
    console.error(ex.stack);
    process.exit(1);
  });

module.exports = app;</code></pre>
<h2 id="실행-설정">실행 설정</h2>
<p>package.json 파일의 시작명령어 script를 Express를 사용하도록 수정합니다.</p>
<pre><code class="language-json">// package.json
...,
&quot;scripts&quot;: {
    &quot;dev&quot;: &quot;node server/server.js&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;lint&quot;: &quot;next lint&quot;
  },
...</code></pre>
<h3 id="nodemon-사용">nodemon 사용</h3>
<pre><code class="language-json">// package.json
...,
&quot;scripts&quot;: {
    &quot;dev&quot;: &quot;nodemon server/server.js&quot;,
  },
...</code></pre>
<h3 id="nodemonjson-사용">nodemon.json 사용</h3>
<ul>
<li>nodemon.json 파일을생성</li>
</ul>
<pre><code class="language-json">// nodemon.json
{
  &quot;watch&quot;: [&quot;server/server.js&quot;, &quot;server/src&quot;],
  &quot;exec&quot;: &quot;node server/server.js&quot;,
  &quot;ext&quot;: &quot;js json&quot;
}</code></pre>
<ul>
<li>script 수정</li>
</ul>
<pre><code class="language-json">// package.json
...,
&quot;scripts&quot;: {
    &quot;dev&quot;: &quot;nodemon&quot;,
  },
...</code></pre>
<h2 id="시작">시작</h2>
<p> 모든 설정이 완료되었습니다!
 이제 시작명령어를 입력하고 Express를 Custom 서버로 사용하는 Next.js를 시작해보세요.</p>
<pre><code class="language-shell">npm run dev</code></pre>
<img src="https://github.com/d0orHyeok/Express-Next.js-Boiler-Plate/raw/main/docs/images/3-page.PNG">

<h2 id="참조">참조</h2>
<blockquote>
<ul>
<li><a href="https://nextjs.org/docs">Next.js 공식문서</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://nextjs.org/docs/advanced-features/custom-server">Next.js Custom Server</a></li>
<li><a href="https://github.com/d0orHyeok/Express-Next.js-Boiler-Plate">d0orHyeok | Express-Next.js-Boiler-Plate</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>