<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>호두 아빠는 오늘도 코딩을 하지</title>
        <link>https://velog.io/</link>
        <description>기술적 혁신과 측정 가능한 성과를 추구하는 프론트엔드 개발자 양윤성입니다.</description>
        <lastBuildDate>Sat, 25 Oct 2025 08:01:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>호두 아빠는 오늘도 코딩을 하지</title>
            <url>https://images.velog.io/images/yunsungyang-omc/profile/eb6bafb8-c5c6-4208-b462-350b651dd836/이모지.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 호두 아빠는 오늘도 코딩을 하지. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yunsungyang-omc" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[(React) 서버 DTO, 그대로 사용할 것인가 변환할 것인가]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EC%99%80-%EC%84%9C%EB%B2%84-DTO-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B2%BD%EA%B3%84-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/React-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EC%99%80-%EC%84%9C%EB%B2%84-DTO-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B2%BD%EA%B3%84-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 25 Oct 2025 08:01:07 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 토스의 frontend-fundamentals 디스커션에서 아래와 같은 고민이 있더라구요.</p>
<blockquote>
<p>&quot;백엔드에서 준 데이터를 최대한 그대로 쓰는 게 맞아&quot; vs. &quot;프론트는 프론트 도메인으로 따로 가야지&quot;
<a href="https://github.com/toss/frontend-fundamentals/discussions/324">디스커션 링크</a></p>
</blockquote>
<p>특히 백엔드와 프론트엔드가 분리되어 개발하는 환경에서, <strong>서버에서 내려주는 데이터를 어떻게 다뤄야 할까</strong>에 대한 논쟁은 끝이 없죠.</p>
<p>저는 후자 쪽에 손을 들고 싶습니다. 하지만 <strong>무조건 변환 레이어가 정답이다</strong>라고 말하고 싶은 건 아니에요. 오늘은 <strong>어떤 상황에서 DTO에 직접 의존하는 게 위험한지</strong>, 그리고 <strong>언제 변환 레이어가 필요한지</strong> 제 경험을 공유해보려고 합니다.</p>
<h2 id="개발-단계에서-dto는-자주-변한다">개발 단계에서 DTO는 자주 변한다</h2>
<p>실제 프로젝트를 진행하다 보면 이런 경험 있으시죠?</p>
<p><strong>1주차:</strong></p>
<pre><code class="language-tsx">interface UserDTO {
  name: string;
  email: string;
}</code></pre>
<p><strong>2주차:</strong> 백엔드 개발자 :  &quot;아, 이름이 firstName, lastName으로 분리됐어요&quot;</p>
<pre><code class="language-tsx">interface UserDTO {
  firstName: string;
  lastName: string;
  email: string;
}</code></pre>
<p><strong>3주차:</strong> 백엔드 개발자 : &quot;email 필드 삭제하고 contactInfo로 통합했습니다&quot;</p>
<pre><code class="language-tsx">interface UserDTO {
  firstName: string;
  lastName: string;
  contactInfo: {
    email: string;
    phone: string;
  }
}</code></pre>
<p>이런 변경이 있을 때마다 프론트엔드는 어떻게 되나요?</p>
<pre><code class="language-tsx">// UserProfile.tsx
function UserProfile() {
  const { data: user } = useQuery([&#39;user&#39;], fetchUser);

  return (
    &lt;div&gt;
      &lt;h1&gt;{[user.name](http://user.name)}&lt;/h1&gt; {/* ❌ 타입 에러! */}
      &lt;p&gt;{[user.email](http://user.email)}&lt;/p&gt;  {/* ❌ 타입 에러! */}
    &lt;/div&gt;
  );
}

// UserCard.tsx
function UserCard() {
  const { data: user } = useQuery([&#39;user&#39;], fetchUser);

  return &lt;span&gt;{[user.name](http://user.name)}&lt;/span&gt;; {/* ❌ 또 타입 에러! */}
}

// UserMenu.tsx
function UserMenu() {
  const { data: user } = useQuery([&#39;user&#39;], fetchUser);

  return &lt;div&gt;{[user.email](http://user.email)}&lt;/div&gt;; {/* ❌ 또또 타입 에러! */}
}</code></pre>
<p>보이시나요? <strong>DTO를 직접 사용하는 모든 컴포넌트</strong>에서 에러가 터집니다.</p>
<p>개발 단계에서는 DTO가 2주에 한 번씩 변경되기도 하고, 운영 단계에서도 필요에 따라 자주 변경될 수 있어요.</p>
<h2 id="특히-앱에서는-더욱-치명적입니다">특히 앱에서는 더욱 치명적입니다</h2>
<p>웹 개발자라면 &quot;API 바뀌면 바로 배포하면 되지 않나?&quot;라고 생각할 수 있어요. 맞습니다. <strong>웹은 배포가 자유롭기 때문에</strong> DTO 변경에 상대적으로 유연하게 대응할 수 있죠.</p>
<p>하지만 <strong>모바일 앱은 완전히 다른 이야기</strong>입니다.</p>
<h3 id="앱은-영구적입니다">앱은 &quot;영구적&quot;입니다</h3>
<p>앱스토어나 플레이스토어에 배포된 순간부터, 그 앱은 개발자의 즉각적인 통제를 벗어납니다. 사용자가 언제 업데이트할지, 심지어 업데이트를 할지조차 보장할 수 없어요.</p>
<pre><code class="language-tsx">// 2024년 1월: v1.0.0 배포
interface UserDTO {
  name: string;
}

// 2024년 3월: v1.1.0 배포 - API 변경됨
interface UserDTO {
  firstName: string;
  lastName: string;
}
</code></pre>
<p>이 상황에서 문제는:</p>
<ul>
<li>v1.0.0을 쓰는 사용자가 여전히 50%</li>
<li>v1.1.0으로 업데이트한 사용자가 50%</li>
<li><strong>서버는 두 버전 모두를 지원해야 함</strong></li>
</ul>
<p>웹이었다면? 배포 한 번으로 모든 사용자가 즉시 새 코드를 사용합니다.</p>
<h3 id="ios는-리뷰-프로세스-android는-단계적-출시">iOS는 리뷰 프로세스, Android는 단계적 출시</h3>
<p>더 복잡한 것은:</p>
<ul>
<li><strong>iOS</strong>: Apple의 앱 리뷰가 2-3일 소요</li>
<li><strong>Android</strong>: 단계적 출시로 100% 배포까지 며칠 소요</li>
<li><strong>두 플랫폼 모두</strong>: 사용자의 자동 업데이트 비활성화 가능</li>
</ul>
<p>결과적으로 언제든 <strong>최소 3~4개 버전이 동시에 사용 중</strong>입니다.</p>
<h3 id="실제-사례-앱에서-dto에-직접-의존하면">실제 사례: 앱에서 DTO에 직접 의존하면</h3>
<p>앤드로이드 개발자의 경험을 공유한 글을 보면, API를 v3.5에서 v3.6으로 업그레이드했더니:</p>
<ul>
<li><code>/users</code> 엔드포인트 응답 구조 변경</li>
<li><strong>다른 2개 엔드포인트도 함께 변경됨</strong></li>
<li>앱이 예상치 못한 곳에서 크래시 발생</li>
</ul>
<p>&quot;그거 개발자 잘못 아니야?&quot;라고 할 수 있지만, 앱 개발자 입장에선 선택지가 없어요. 전역 API 버전을 올리면 예측할 수 없는 변경사항들이 한꺼번에 적용되니까요.</p>
<h3 id="해결책은-결국-변환-레이어">해결책은 결국 변환 레이어</h3>
<p>이런 상황에서 변환 레이어가 있다면:</p>
<pre><code class="language-tsx">// 서버 API가 v3.6으로 변경되어도
function serverToClient(dto: UserDTOv3_6): User {
  return {
    fullName: `${dto.firstName} ${dto.lastName}` // v1.0.0, v1.1.0 모두 대응
  };
}
</code></pre>
<p>앱의 모든 버전이 동일한 도메인 타입을 사용하고, 서버 응답만 버전별로 적절히 변환하면 됩니다.</p>
<p>웹에서는 선택사항일 수 있지만, <strong>앱에서 DTO에 직접 의존하는 것은 시한폭탄</strong>입니다. API 버전 관리와 변환 레이어는 거의 필수에 가깝습니다.</p>
<h2 id="그런데-정말-모든-프로젝트에서-문제가-될까요">그런데 정말 모든 프로젝트에서 문제가 될까요?</h2>
<p>사실 <strong>그렇지 않습니다.</strong> 제 경험상 프로젝트 상황에 따라 달라져요.</p>
<p><strong>소규모 팀에서 긴밀하게 일할 때</strong>는 DTO를 그대로 써도 괜찮아요. </p>
<p>백엔드 개발자와 한 방에서 일하면서 API 변경 전에 미리 논의할 수 있고, 변경 사항을 바로바로 공유하고 함께 대응할 수 있잖아요. API 개수도 적고, 사용하는 컴포넌트도 몇 개 안 되는 경우라면 오히려 <strong>심플하게 가는 게 좋을 수 있어요</strong>. 변환 레이어는 그냥 오버엔지니어링이 될 수 있죠.</p>
<p>하지만 제가 변환 레이어를 강력히 추천하는 케이스가 있어요.</p>
<p><strong>대규모 프로젝트에서 여러 API에 의존할 때</strong>요. </p>
<p>10개 이상의 서로 다른 API 엔드포인트를 사용한다거나, 외부 API나 파트너사 API 같은 통제 불가능한 API를 쓴다거나, 백엔드 팀이 여러 개로 나뉘어 있어서 변경 사항을 한눈에 파악하기 어려운 경우. 이런 상황에서는 <strong>각 API의 변경이 프론트엔드 전체에 파급효과</strong>를 일으킬 수 있어요.</p>
<p>특히 이런 경험 있으신가요?</p>
<pre><code class="language-tsx">// 프로필 API는 [user.name](http://user.name)
const { data: profile } = useQuery([&#39;profile&#39;], fetchProfile);

// 주문 API는 user.userName  
const { data: order } = useQuery([&#39;order&#39;], fetchOrder);

// 결제 API는 user.fullName
const { data: payment } = useQuery([&#39;payment&#39;], fetchPayment);</code></pre>
<p>같은 &quot;사용자 이름&quot;인데 <strong>API마다 필드명이 다른 경우</strong>... 정말 흔하죠. 이럴 때 컴포넌트에서 일일이 맞춰 쓰면 코드가 난잡해집니다.</p>
<pre><code class="language-tsx">// 😰 이런 코드를 여러 곳에서...
&lt;div&gt;
  {profile?.name || order?.userName || payment?.fullName}
&lt;/div&gt;</code></pre>
<h2 id="react-query-메인테이너도-데이터-변환을-이야기합니다">React Query 메인테이너도 데이터 변환을 이야기합니다</h2>
<p>TkDodo의 <a href="https://tkdodo.eu/blog/react-query-data-transformations">React Query Data Transformations</a> 블로그 포스트를 보면 이런 말이 나옵니다.</p>
<blockquote>
<p>&quot;Let&#39;s face it - most of us are not using GraphQL. If you are working with REST though, you are constrained by what the backend returns.&quot;</p>
</blockquote>
<p>REST API를 사용한다면, 백엔드가 주는 구조에 <strong>제약</strong>을 받는다는 거죠. 그래서 TkDodo는 데이터 변환을 위한 여러 방법을 제시하는데, 그중 <strong>가장 추천하는 방식</strong>이 바로 <code>select</code> 옵션입니다.</p>
<pre><code class="language-tsx">const useTodos = () =&gt; 
  useQuery({
    queryKey: [&#39;todos&#39;],
    queryFn: fetchTodos,
    select: (data) =&gt; transformData(data) // 여기서 변환!
  })</code></pre>
<p>이 방식의 장점은 변환된 데이터가 변하지 않으면 컴포넌트가 리렌더링되지 않고, 전체 데이터 중 필요한 부분만 선택적으로 구독할 수 있다는 점이에요. 그리고 <strong>타입 변환이 useQuery 레벨에서 끝난다</strong>는 게 큰 장점이죠.</p>
<h2 id="제가-실제로-사용하는-패턴">제가 실제로 사용하는 패턴</h2>
<p>그럼 실제로 어떻게 구성하면 좋을까요? 제가 쓰는 패턴을 보여드릴게요.</p>
<p>먼저 Repository 레이어를 구조화합니다.</p>
<pre><code class="language-tsx">// repositories/user/formatters.ts
import type { UserDTO } from &#39;@/types/dto&#39;;
import type { User } from &#39;@/types/domain&#39;;

export function toUser(dto: UserDTO): User {
  return {
    id: dto.id,
    fullName: `${dto.firstName} ${dto.lastName}`,
    email: dto.email,
  };
}
</code></pre>
<pre><code class="language-tsx">// repositories/user/api.ts
import type { UserDTO } from &#39;@/types/dto&#39;;

export async function fetchUser(id: string): Promise&lt;UserDTO&gt; {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}
</code></pre>
<p>그리고 Custom Hook에서 <strong>TkDodo가 추천한 select 옵션</strong>을 활용합니다.</p>
<pre><code class="language-tsx">// hooks/useUser.ts
import { useQuery } from &#39;@tanstack/react-query&#39;;
import { fetchUser } from &#39;@/repositories/user/api&#39;;
import { toUser } from &#39;@/repositories/user/formatters&#39;;

export function useUser(id: string) {
  return useQuery({
    queryKey: [&#39;user&#39;, id],
    queryFn: () =&gt; fetchUser(id),
    select: toUser, // ✅ Repository의 formatter를 select에서 사용
  });
}
</code></pre>
<p>컴포넌트는 도메인 타입만 사용합니다.</p>
<pre><code class="language-tsx">// components/UserProfile.tsx
function UserProfile({ userId }: Props) {
  const { data: user } = useUser(userId);

  return (
    &lt;div&gt;
      &lt;h1&gt;{user.fullName}&lt;/h1&gt; {/* ✅ 항상 동일한 인터페이스 */}
      &lt;p&gt;{user.email}&lt;/p&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>이제 백엔드에서 firstName, lastName을 fullName으로 통합하더라도 어떻게 될까요?</p>
<pre><code class="language-tsx">// 수정이 필요한 곳은 Repository의 formatter뿐!
// repositories/user/formatters.ts
export function toUser(dto: UserDTO): User {
  return {
    id: dto.id,
    fullName: dto.fullName, // ✅ 여기만 수정
    email: dto.email,
  };
}
</code></pre>
<p><strong>컴포넌트는 손도 안 댔습니다.</strong> 타입 에러도 나지 않아요.</p>
<p>이 패턴의 장점은:</p>
<ul>
<li><strong>변환 로직이 <code>formatters.ts</code>에 집중</strong>되어 관리가 쉽고</li>
<li><strong>React Query의 select 최적화</strong>를 그대로 활용하며</li>
<li><strong>API 호출과 변환 로직이 명확히 분리</strong>됩니다</li>
</ul>
<h2 id="이-접근의-진짜-장점-문제-발견-지점">이 접근의 진짜 장점: 문제 발견 지점</h2>
<p>변환 레이어를 두면 <strong>어디서 문제가 생기는지 명확</strong>해집니다.</p>
<p>DTO를 직접 사용하면 15개 컴포넌트에서 각각 타입 에러가 발생하고, 어디서 뭐가 잘못됐는지 파악하기 어려워요. 하지만 변환 레이어를 사용하면 <strong>formatter에서만 타입 에러가 발생</strong>하고, 한 곳만 보면 되고 수정도 한 곳만 하면 됩니다.</p>
<pre><code class="language-tsx">// ❌ DTO를 직접 사용하면
// UserProfile.tsx에서 에러
// UserCard.tsx에서 에러
// UserList.tsx에서 에러
// ... (15곳에서 에러)

// ✅ formatter를 사용하면
// repositories/user/formatters.ts에서만 에러!
</code></pre>
<p>특히 <strong>여러 API를 사용하는 대규모 프로젝트</strong>에서 이 차이가 크게 느껴집니다. 10개 API가 있다면:</p>
<ul>
<li>DTO를 직접 쓸 때: 10개 API × 평균 8개 컴포넌트 = <strong>80곳에서 에러</strong></li>
<li>formatter를 쓸 때: <strong>10개 formatter 파일에서만 에러</strong></li>
</ul>
<p>에러가 발생하는 위치가 80곳에서 10곳으로 줄어듭니다. 수정도 10곳만 하면 되고요.</p>
<h2 id="커뮤니티의-의견도-비슷합니다">커뮤니티의 의견도 비슷합니다</h2>
<p><a href="https://profy.dev/article/react-architecture-domain-entities-and-dtos">profy.dev의 &quot;React Architecture&quot;</a> 시리즈를 보면 이런 말이 나옵니다.</p>
<blockquote>
<p>&quot;By passing the data coming from the API directly to the components we tightly couple the UI to the server.&quot;</p>
</blockquote>
<p>API 데이터를 컴포넌트에 직접 전달하면 <strong>UI가 서버에 강하게 결합</strong>된다는 거죠.</p>
<p>Stack Overflow에도 &quot;Service layer returns DTO to controller but need it to return model for other services&quot;나 &quot;Should data transformation be on the front or on the back end?&quot; 같은 질문들이 정말 많더라고요. 그리고 답은 명확합니다. <strong>경계(boundary)에서 변환하라</strong>는 거예요. 프론트엔드 관점에서 경계는 API 호출 직후, 즉 Repository 레이어입니다.</p>
<h2 id="물론-트레이드오프는-있습니다">물론 트레이드오프는 있습니다</h2>
<p>&quot;그럼 보일러플레이트 코드가 엄청 늘어나지 않아요?&quot;</p>
<p>네, 맞습니다. 변환 함수를 작성하고, 도메인 타입을 정의하고, 추가 파일을 관리해야 하죠. 이게 비용입니다.</p>
<p>하지만 서버 변경에 대한 격리, 컴포넌트 안정성, 타입 안전성, 그리고 <strong>장기적인 유지보수 비용 감소</strong>라는 이득이 있어요.</p>
<p>그래서 제 생각에는:</p>
<ul>
<li><strong>3개 이하 API + 백엔드와 긴밀한 소통</strong> → DTO 직접 사용해도 괜찮음</li>
<li><strong>API가 안정적이고 자주 안 바뀌는 환경</strong> → 필요한 것만 부분적으로 변환</li>
<li><strong>중규모 프로젝트 + 활발한 개발 단계</strong> → 전체 변환 레이어 추천</li>
<li><strong>10개 이상 API + 여러 팀 의존</strong> → 변환 레이어 강력 추천 (거의 필수)</li>
</ul>
<h2 id="도입하려면-어떻게-하면-될까요">도입하려면 어떻게 하면 될까요?</h2>
<p>만약 이 글을 읽고 공감하셨다면, 이렇게 시작해볼 수 있어요.</p>
<p>먼저 도메인 타입을 정의하는 거죠.</p>
<pre><code class="language-tsx">// types/domain/user.ts
export interface User {
  id: string;
  fullName: string;
  email: string;
  phone: string | null;
}
</code></pre>
<p>그다음 Repository에 formatter를 추가하고요.</p>
<pre><code class="language-tsx">// repositories/user/formatters.ts
import type { UserDTO } from &#39;@/types/dto&#39;;
import type { User } from &#39;@/types/domain&#39;;

export function toUser(dto: UserDTO): User {
  return {
    id: dto.id,
    fullName: `${dto.firstName} ${dto.lastName}`,
    email: dto.contactInfo?.email ?? &#39;&#39;,
    phone: dto.contactInfo?.phone ?? null,
  };
}
</code></pre>
<p>기존 코드를 점진적으로 마이그레이션하면 됩니다.</p>
<pre><code class="language-tsx">// Before
const { data: user } = useQuery({
  queryKey: [&#39;user&#39;],
  queryFn: fetchUser,
});
// user는 UserDTO 타입

// After
const { data: user } = useQuery({
  queryKey: [&#39;user&#39;],
  queryFn: fetchUser,
  select: toUser, // ✅ formatter 추가
});
// user는 User 타입
</code></pre>
<p>타입이 달라지면서 컴포넌트에서 타입 에러가 날 텐데, 그걸 하나씩 수정하면 돼요. 이게 바로 <strong>컴파일 타임에 문제를 발견</strong>하는 거죠.</p>
<h2 id="마지막으로">마지막으로</h2>
<p>&quot;당신의 프로젝트는 몇 개의 API를 사용하고 있고, 백엔드 개발자와 얼마나 긴밀하게 소통하고 있나요?&quot;</p>
<p>이 질문에 대한 답이 결국 <strong>DTO를 직접 쓸지, 변환 레이어를 둘지</strong> 결정하는 기준이 될 것 같아요.</p>
<p>제가 이 글을 쓰게 된 이유는, 아마 저뿐만 아니라 많은 프론트엔드 개발자분들이 비슷한 고민을 하고 계실 것 같아서입니다. 모든 상황에 완벽한 답은 없지만, 각자의 프로젝트 상황에 맞는 선택을 하는 데 이 글이 조금이라도 도움이 되었으면 좋겠습니다.</p>
<p><a href="https://tkdodo.eu/blog/">TkDodo의 블로그</a>도 꼭 한번 읽어보세요. 정말 좋은 인사이트가 많더라고요.</p>
<p>궁금하신 점이나 다른 의견이 있으시면 언제든 댓글로 남겨주세요!</p>
<p>감사합니다.</p>
<hr>
<h2 id="참고자료">참고자료</h2>
<p>이 글을 작성하는 데 참고한 자료들입니다.</p>
<p><strong>React Query &amp; 데이터 변환</strong></p>
<ul>
<li><a href="https://tkdodo.eu/blog/react-query-data-transformations">React Query Data Transformations</a> - TkDodo</li>
<li><a href="https://profy.dev/article/react-architecture-api-layer">React Architecture: Server-Client State Separation</a> - Profy.dev</li>
</ul>
<p><strong>모바일 앱 API 버전 관리</strong></p>
<ul>
<li><a href="https://medium.com/@emadrazo/api-changes-that-wont-break-your-mobile-app-best-practices-for-cross-team-development-94d8e5761d35">API Changes That Won&#39;t Break Your Mobile App</a> - Eva Madrazo</li>
<li><a href="https://medium.com/webcom-engineering-and-product/5-things-to-remember-before-shipping-version-1-0-of-your-mobile-app-5a1676f0fdbf">5 Things to Remember Before Shipping Version 1.0 of your Mobile App</a> - Martin Rybak</li>
</ul>
<p><strong>커뮤니티 논의</strong></p>
<ul>
<li><a href="https://github.com/toss/frontend-fundamentals/discussions">토스 frontend-fundamentals 디스커션</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) React Query는 훌륭하지만, 인증 관리 매니저는 아닙니다]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-React-Query%EB%8A%94-%ED%9B%8C%EB%A5%AD%ED%95%98%EC%A7%80%EB%A7%8C-%EC%9D%B8%EC%A6%9D-%EA%B4%80%EB%A6%AC%EB%8A%94-%EC%95%84%EB%8B%99%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@yunsungyang-omc/React-React-Query%EB%8A%94-%ED%9B%8C%EB%A5%AD%ED%95%98%EC%A7%80%EB%A7%8C-%EC%9D%B8%EC%A6%9D-%EA%B4%80%EB%A6%AC%EB%8A%94-%EC%95%84%EB%8B%99%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Sat, 18 Oct 2025 06:40:37 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 정말 오랜만에 글을 쓰는 것 같네요. 마지막 포스팅 이후로 거의 1년 반 만입니다.</p>
<p>그동안 새로운 프로젝트에 참여하면서 다양한 경험을 쌓았는데요. 오늘은 그중에서도 React Query를 사용한 인증 처리에 대해 이야기해보려고 합니다.</p>
<p>최근 프로젝트 코드 리뷰를 진행하면서 흥미로운 패턴을 발견했는데요. React Query의 useQuery 훅을 사용해서 사용자 인증 상태를 관리하는 코드였습니다. 얼핏 보면 괜찮아 보이지만, 자세히 들여다보니 몇 가지 문제가 있더라고요.</p>
<p>이 글에서는 왜 useQuery가 인증 처리에 적합하지 않은지, 그리고 어떻게 개선할 수 있는지 제 경험을 공유해보려고 합니다.</p>
<h2 id="문제의-발단-이런-코드를-본-적-있으신가요">문제의 발단: 이런 코드를 본 적 있으신가요?</h2>
<pre><code class="language-typescript">export function useAuth() {
  const hasToken = !!tokenStorage.getAccess(); // 동기: 즉시 확인

  const { data: user, isLoading } = useGetMyInfo({
    enabled: hasToken, // 비동기: API 호출 대기
  });

  return { user, isLoading };
}</code></pre>
<p>코드 리뷰를 진행했던 프로젝트에서는 이렇게 useQuery로 사용자 정보를 가져와서 인증 여부를 확인하고 있었어요. 처음에는 &quot;React Query를 잘 활용하고 있네?&quot;라고 생각했는데, 실제 동작을 살펴보니 문제가 보이기 시작했습니다.</p>
<h2 id="인증과-사용자-정보-같은-건가요">인증과 사용자 정보, 같은 건가요?</h2>
<p>이 문제를 이해하려면 먼저 두 가지 개념을 구분해야 합니다.</p>
<p><strong>인증 확인이란?</strong></p>
<p>&quot;사용자가 로그인했는가?&quot;를 확인하는 것입니다. 이건 사실 아주 간단해요. localStorage나 Cookie에 저장된 토큰이 있는지만 확인하면 되거든요. 즉시 확인 가능하고, 서버에 물어볼 필요가 없습니다.</p>
<p><strong>사용자 정보 조회란?</strong></p>
<p>&quot;사용자의 프로필 데이터는 뭐지?&quot;를 확인하는 것입니다. 이건 서버에서 가져와야 하죠. 닉네임, 프로필 이미지, 이메일 같은 정보들이요. 당연히 API 호출이 필요하고 시간이 걸립니다.</p>
<p>프로젝트를 분석해보니 재미있는 사실을 발견했는데요.</p>
<p><strong>인증 확인이 필요한 곳</strong>은 3군데였습니다.</p>
<ul>
<li>보호된 페이지에 접근할 때 (AuthGuardProvider)</li>
<li>헤더에서 로그인/로그아웃 버튼을 보여줄 때</li>
<li>모임 참가 버튼을 활성화할 때</li>
</ul>
<p><strong>사용자 정보가 필요한 곳</strong>은 2군데였습니다.</p>
<ul>
<li>헤더에 프로필 이미지와 닉네임을 보여줄 때</li>
<li>마이페이지에서 전체 프로필을 보여줄 때</li>
</ul>
<p>보이시나요? 인증 확인은 필요한데 사용자 정보는 필요 없는 곳이 더 많다는 걸요.</p>
<h2 id="usequery로-인증을-처리하면-생기는-문제들">useQuery로 인증을 처리하면 생기는 문제들</h2>
<h3 id="1-불필요한-네트워크-요청">1. 불필요한 네트워크 요청</h3>
<p>사용자가 <code>/dashboard</code> 페이지에 접근하는 상황을 생각해볼까요?</p>
<pre><code>1. AuthGuardProvider가 마운트됩니다
2. tokenStorage.getAccess()로 토큰 확인 (0ms, 즉시!)
3. useGetMyInfo() API 요청 시작
4. 네트워크 왕복하며 대기... (200-300ms)
5. 서버에서 사용자 정보 받아옴
6. 받은 정보로 인증 여부 확인</code></pre><p>토큰이 이미 localStorage에 있는데, 굳이 서버에 물어봐야 할까요?</p>
<h3 id="2-깜빡이는-화면">2. 깜빡이는 화면</h3>
<p>실제로 AuthGuardProvider 코드를 보면 이렇게 되어 있었습니다.</p>
<pre><code class="language-typescript">export default function AuthGuardProvider({ children }) {
  const { user, isLoading } = useAuth();

  useEffect(() =&gt; {
    if (isLoading || !isProtectedRoute(pathname)) return;

    if (!user) {
      router.replace(`${PATH.SIGNIN}?redirect=${pathname}`);
    }
  }, [pathname, isLoading, user]);

  return &lt;&gt;{children}&lt;/&gt;;
}</code></pre>
<p>여기서 <code>isLoading</code>이 true인 동안에는 가드 체크를 하지 않습니다. 그 말은 즉, API 응답을 기다리는 200-300ms 동안 사용자는 빈 화면이나 로딩 스피너를 보게 된다는 거죠.</p>
<p>토큰 확인은 즉시 가능한데 말이에요.</p>
<h3 id="3-react-query의-본래-목적과-맞지-않습니다">3. React Query의 본래 목적과 맞지 않습니다</h3>
<p>React Query의 메인테이너인 TkDodo의 블로그를 보면 이런 말이 나옵니다.</p>
<blockquote>
<p>&quot;React Query is designed for asynchronous server state management.&quot;</p>
</blockquote>
<p>React Query는 서버 상태를 관리하기 위한 도구입니다. 그런데 인증 상태는 어떤가요?</p>
<ul>
<li>토큰은 이미 클라이언트에 있습니다 (localStorage/Cookie)</li>
<li>동기적으로 즉시 확인 가능합니다</li>
<li>서버에 물어볼 필요가 없습니다</li>
</ul>
<p>useQuery의 강력한 기능들(자동 리페칭, 캐싱, 리트라이 등)이 이 경우에는 오히려 복잡도만 높이는 거죠.</p>
<h3 id="4-인증-상태가-여기저기-흩어집니다">4. 인증 상태가 여기저기 흩어집니다</h3>
<p>useQuery를 사용하면 각 컴포넌트에서 독립적으로 인증을 확인하게 되는데요. 이게 문제가 됩니다.</p>
<pre><code class="language-typescript">// Header.tsx
function Header() {
  const { data: user } = useGetMyInfo({ enabled: !!token });
  // ...
}

// AuthGuard.tsx
function AuthGuard() {
  const { data: user } = useGetMyInfo({ enabled: !!token });
  // ...
}

// MyProfileButton.tsx
function MyProfileButton() {
  const { data: user } = useGetMyInfo({ enabled: !!token });
  // ...
}</code></pre>
<p>React Query가 queryKey로 캐싱을 해주긴 하지만, 각 컴포넌트가 마운트될 때마다 &quot;내가 인증 상태를 확인해야겠다&quot;고 생각하게 됩니다. 인증 로직이 컴포넌트 곳곳에 파편화되는 거죠.</p>
<p>이렇게 되면 몇 가지 문제가 생깁니다.</p>
<p><strong>로그아웃 처리가 복잡해집니다</strong></p>
<p>로그아웃을 하면 어떻게 해야 할까요? 모든 컴포넌트의 useQuery를 invalidate해야 합니다.</p>
<pre><code class="language-typescript">const logout = () =&gt; {
  tokenStorage.clear();
  queryClient.invalidateQueries([&#39;user&#39;]);
  // 혹시 빠뜨린 쿼리는 없을까?
}</code></pre>
<p><strong>동기화 이슈가 발생할 수 있습니다</strong></p>
<p>각 컴포넌트가 독립적으로 API를 호출하면, 타이밍에 따라 다른 결과를 받을 수 있습니다. 토큰이 만료되는 순간에 한 컴포넌트는 성공하고 다른 컴포넌트는 실패할 수도 있죠.</p>
<p><strong>&quot;진짜 인증 상태&quot;가 어디 있는지 불명확합니다</strong></p>
<p>인증 상태의 Single Source of Truth가 없어집니다. Header의 user가 진짜일까요? AuthGuard의 user가 진짜일까요? React Query 캐시가 진짜일까요?</p>
<pre><code class="language-typescript">// 인증 상태를 확인하려면 어디를 봐야 할까요?
const user1 = queryClient.getQueryData([&#39;user&#39;]); // React Query 캐시
const user2 = useGetMyInfo(); // 컴포넌트 A
const user3 = useGetMyInfo(); // 컴포넌트 B
// 진실은 어디에?</code></pre>
<p>이런 상황에서 디버깅을 해본 적 있으신가요? 한 컴포넌트에서는 로그인 상태인데 다른 컴포넌트에서는 로그아웃 상태로 보이는 버그요. 정말 찾기 어렵습니다.</p>
<h3 id="5-react-query-기능을-끄게-만듭니다">5. React Query 기능을 끄게 만듭니다</h3>
<p>실제로 이 문제를 겪은 개발자들은 결국 이런 코드를 작성하게 됩니다.</p>
<pre><code class="language-typescript">const { data: user } = useQuery([&#39;user&#39;], getUser, {
  staleTime: Infinity,
  cacheTime: Infinity,
  refetchOnMount: false,
  refetchOnWindowFocus: false,
  refetchOnReconnect: false,
});</code></pre>
<p>React Query의 모든 기능을 꺼버리는 거죠. 그럼 이제 질문이 생깁니다.</p>
<p>&quot;이 모든 기능을 꺼야 한다면, 왜 useQuery를 사용하나요?&quot;</p>
<h2 id="개발자들도-같은-고민을-했습니다">개발자들도 같은 고민을 했습니다</h2>
<p>제가 이 문제를 발견하고 리서치를 해보니, 많은 개발자들이 비슷한 고민을 하고 있더라고요.</p>
<p>TanStack Query의 GitHub Discussion에서 누군가 이런 질문을 했습니다.</p>
<blockquote>
<p>&quot;useQuery(&#39;users&#39;, getUser)를 여러 곳에서 사용해도 될까요? 아니면 한 번만 사용하고 다른 곳에서는 queryClient.getQueryData를 써야 할까요?&quot;</p>
</blockquote>
<p>그리고 Codemzy라는 개발자는 블로그에서 이렇게 말했습니다.</p>
<blockquote>
<p>&quot;사용자 데이터를 다시 요청할 이유가 없어요. 모든 API 호출에서 인증을 확인하니까요. 인증이 만료되면 API 응답에서 에러가 나거든요.&quot;</p>
</blockquote>
<p>결국 많은 개발자들이 <code>staleTime: Infinity</code>로 설정하고 React Query의 장점을 포기하게 되더라고요.</p>
<p>Stack Overflow에도 비슷한 질문들이 많습니다.</p>
<ul>
<li>&quot;React Query로 인증 상태를 어떻게 관리하나요?&quot;</li>
<li>&quot;isLoading 때문에 인증 가드 리다이렉트가 느려요&quot;</li>
<li>&quot;React Query가 사용자 데이터를 계속 리페칭해요&quot;</li>
</ul>
<p>이런 질문들 자체가 도구를 잘못된 용도로 사용하고 있다는 신호라고 생각합니다.</p>
<h2 id="그럼-어떻게-해야-할까요">그럼 어떻게 해야 할까요?</h2>
<p>커뮤니티에서 권장하는 방법은 관심사를 분리하는 것입니다. Zustand나 Context 같은 클라이언트 상태 관리 도구와 React Query를 함께 사용하는 거죠.</p>
<h3 id="zustand로-인증-상태-관리하기">Zustand로 인증 상태 관리하기</h3>
<pre><code class="language-typescript">import { create } from &quot;zustand&quot;;
import { tokenStorage } from &quot;@/lib/utils/token&quot;;

interface AuthStore {
  isAuthenticated: boolean;
  login: (token: string) =&gt; void;
  logout: () =&gt; void;
  validateToken: () =&gt; void;
}

export const useAuthStore = create&lt;AuthStore&gt;((set, get) =&gt; ({
  // 초기값: 토큰 존재 여부로 즉시 판단
  isAuthenticated: tokenStorage.hasValidAccess(),

  login: (token) =&gt; {
    tokenStorage.set(token);
    set({ isAuthenticated: true });
  },

  logout: () =&gt; {
    tokenStorage.clear();
    set({ isAuthenticated: false });
  },

  validateToken: () =&gt; {
    if (!tokenStorage.hasValidAccess()) {
      get().logout();
    }
  }
}));</code></pre>
<p>이렇게 하면 인증 상태를 즉시 확인할 수 있습니다.</p>
<h3 id="react-query는-사용자-정보가-필요할-때만">React Query는 사용자 정보가 필요할 때만</h3>
<pre><code class="language-typescript">export function useUserProfile() {
  const isAuthenticated = useAuthStore((s) =&gt; s.isAuthenticated);

  return useQuery({
    queryKey: [&#39;user&#39;, &#39;profile&#39;],
    queryFn: getUserProfile,
    enabled: isAuthenticated, // 인증된 경우에만
    staleTime: 5 * 60 * 1000, // 5분
    refetchOnWindowFocus: true, // 이제 이 기능들을 제대로 활용!
  });
}</code></pre>
<p>이제 React Query를 본래 목적대로 사용할 수 있습니다.</p>
<h3 id="authguard는-즉시-동작합니다">AuthGuard는 즉시 동작합니다</h3>
<pre><code class="language-typescript">export default function AuthGuardProvider({ children }) {
  const pathname = usePathname();
  const router = useRouter();
  const { isAuthenticated, validateToken } = useAuth();

  useEffect(() =&gt; {
    validateToken(); // 즉시 실행!

    if (!isProtectedRoute(pathname)) return;

    if (!isAuthenticated) { // isLoading 없음!
      router.replace(`${PATH.SIGNIN}?redirect=${pathname}`);
    }
  }, [pathname, isAuthenticated]);

  return &lt;&gt;{children}&lt;/&gt;;
}</code></pre>
<p>isLoading을 기다릴 필요가 없습니다. 인증 확인은 즉시 끝나니까요.</p>
<h2 id="성능-개선-효과">성능 개선 효과</h2>
<p>실제로 측정해보면 이 정도 차이가 납니다.</p>
<p><strong>기존 방식 (useQuery)</strong></p>
<ul>
<li>인증 확인: ~250ms (API 응답 대기)</li>
<li>초기 렌더링: isLoading 동안 대기</li>
<li>페이지 전환: 매번 잠재적 API 요청</li>
</ul>
<p><strong>개선 방식 (Zustand + React Query)</strong></p>
<ul>
<li>인증 확인: 0ms (즉시!)</li>
<li>초기 렌더링: 대기 없음</li>
<li>페이지 전환: 토큰 확인만</li>
</ul>
<p>250ms가 별거 아닌 것 같지만, 사용자 경험에서는 체감이 큽니다. 특히 페이지를 여러 번 이동할 때는 더욱 그렇죠.</p>
<h2 id="실제-프로젝트에-적용하기">실제 프로젝트에 적용하기</h2>
<p>제가 리뷰했던 프로젝트에 이 방식을 제안했을 때, 팀원들의 반응이 좋았습니다. 마이그레이션도 생각보다 간단했고요.</p>
<p><strong>1단계: Auth Store 만들기</strong></p>
<pre><code class="language-typescript">// src/store/authStore.ts
export const useAuthStore = create&lt;AuthStore&gt;((set) =&gt; ({
  isAuthenticated: tokenStorage.hasValidAccess(),
  login: (token) =&gt; {
    tokenStorage.set(token);
    set({ isAuthenticated: true });
  },
  logout: () =&gt; {
    tokenStorage.clear();
    set({ isAuthenticated: false });
  }
}));</code></pre>
<p><strong>2단계: useAuth 훅 수정하기</strong></p>
<pre><code class="language-typescript">// src/hooks/auth/useAuth.ts
export function useAuth() {
  const isAuthenticated = useAuthStore((s) =&gt; s.isAuthenticated);
  const logout = useAuthStore((s) =&gt; s.logout);

  return {
    isAuthenticated, // 즉시 사용 가능!
    logout
  };
}</code></pre>
<p><strong>3단계: 컴포넌트 업데이트하기</strong></p>
<pre><code class="language-typescript">// Before
const { user, isLoading } = useAuth();
if (isLoading) return &lt;Loading /&gt;;

// After
const { isAuthenticated } = useAuth();
// isLoading 없음! 즉시 사용 가능</code></pre>
<p>기존 코드를 크게 건드리지 않아도 되고, 점진적으로 마이그레이션할 수 있어서 좋았습니다.</p>
<h2 id="커뮤니티에서도-인정받은-패턴입니다">커뮤니티에서도 인정받은 패턴입니다</h2>
<p>Medium에서 여러 개발자들이 Zustand와 React Query를 조합한 사례를 공유하고 있습니다.</p>
<blockquote>
<p>&quot;Separation of Concerns: React Query는 서버 상태 관리에 탁월합니다. Zustand는 클라이언트 상태 관리에 적합하고요. 이 두 도구를 함께 사용하면 비즈니스 로직과 UI 상태를 서버 데이터와 명확히 분리할 수 있습니다.&quot;</p>
</blockquote>
<p>Doichev Kostia라는 개발자는 블로그에서 이렇게 말했습니다.</p>
<blockquote>
<p>&quot;제가 찾은 이상적인 해결책은 영구 저장소(쿠키)와 앱 내부 저장소를 함께 사용하는 것이었습니다. TypeScript와 React 컴포넌트 모두에서 구독할 수 있도록요. Zustand가 완벽한 선택이었습니다.&quot;</p>
</blockquote>
<p>실제로 많은 프로덕션 환경에서 이런 패턴을 사용하고 있더라고요.</p>
<ol>
<li>Zustand: 토큰 저장, 인증 상태 관리</li>
<li>React Query: 사용자 프로필, API 데이터 페칭  </li>
<li>Axios Interceptor: 토큰 자동 주입</li>
</ol>
<h2 id="마치며">마치며</h2>
<p>제가 이 글을 쓰게 된 이유는, 아마 저뿐만 아니라 많은 프론트엔드 개발자분들이 비슷한 고민을 하고 계실 것 같아서입니다.</p>
<p>React Query는 정말 훌륭한 도구입니다. 하지만 모든 상황에 완벽한 도구는 없죠. 중요한 건 각 도구의 목적을 이해하고 적재적소에 사용하는 것이라고 생각합니다.</p>
<h3 id="이-글의-핵심-요약">이 글의 핵심 요약</h3>
<p><strong>🔑 인증(로그인 여부) ≠ 사용자 정보(프로필 데이터)</strong></p>
<ul>
<li>인증 상태: 클라이언트 상태 → Zustand나 Context로 관리</li>
<li>사용자 정보: 서버 상태 → React Query로 관리</li>
<li>토큰: 이미 로컬에 있음 → 즉시 확인 가능</li>
</ul>
<p><strong>⚡ 성능과 사용자 경험 개선</strong></p>
<ul>
<li>인증 확인: API 응답 대기(250ms) → 즉시 확인(0ms)</li>
<li>불필요한 네트워크 요청 제거</li>
<li>페이지 전환 시 화면 깜빡임 없음</li>
</ul>
<p><strong>🛠️ 권장하는 접근 방식</strong></p>
<ol>
<li>Zustand로 인증 상태 관리 (isAuthenticated)</li>
<li>React Query는 사용자 정보가 필요할 때만 사용</li>
<li>각 도구를 본래 목적에 맞게 활용</li>
</ol>
<h3 id="제-의견이-정답은-아닙니다">제 의견이 정답은 아닙니다</h3>
<p>이 글에서 제시한 방법은 제가 프로젝트를 경험하면서 찾은 하나의 접근 방식입니다. 절대적인 정답이라고 할 수는 없어요. </p>
<p>프로젝트의 규모, 팀의 상황, 기술 스택에 따라 더 나은 방법이 있을 수 있습니다. 예를 들어:</p>
<ul>
<li>작은 프로젝트에서는 Context API만으로도 충분할 수 있습니다</li>
<li>이미 Redux를 사용 중이라면 Redux로 인증을 관리하는 게 나을 수도 있죠</li>
<li>SSR이 중요한 프로젝트라면 또 다른 고려사항이 있을 겁니다</li>
</ul>
<p>중요한 건 &quot;왜 이렇게 하는가?&quot;를 이해하는 것이라고 생각합니다. 제 글이 여러분의 프로젝트에서 더 나은 결정을 내리는 데 하나의 참고 자료가 되었으면 좋겠습니다.</p>
<p>마지막으로 한 가지 질문을 던지고 싶습니다.</p>
<p>&quot;토큰이 이미 로컬에 있는데, 서버에 사용자 정보를 요청해야만 인증 여부를 알 수 있나요?&quot;</p>
<p>제 답은 아니오입니다. 인증 상태는 즉시 확인할 수 있고, 사용자 정보는 필요할 때만 가져오면 됩니다. 하지만 여러분의 답은 다를 수 있고, 그것도 괜찮습니다.</p>
<p>이 글이 비슷한 고민을 하고 계신 분들께 도움이 되었으면 좋겠습니다. 궁금하신 점이나 다른 의견이 있으시면 언제든 댓글로 남겨주세요. 여러분의 경험과 생각도 듣고 싶습니다.</p>
<p>감사합니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://tkdodo.eu/blog/react-query-as-a-state-manager">TkDodo&#39;s Blog - React Query as a State Manager</a></li>
<li><a href="https://github.com/TanStack/query/discussions/1547">TanStack Query Discussion - Best practice handling auth state</a></li>
<li><a href="https://www.codemzy.com/blog/react-auth-context-vs-react-query">Codemzy - React auth: context vs React Query</a></li>
<li><a href="https://medium.com/@freeyeon96/zustand-react-query-new-state-management-7aad6090af56">Medium - Zustand + React Query: A New Approach to State Management</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) 그로스 해킹 프론트엔드 : GTM을 사용해 마케팅 지표 로깅하기]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-%EA%B7%B8%EB%A1%9C%EC%8A%A4-%ED%95%B4%ED%82%B9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C</link>
            <guid>https://velog.io/@yunsungyang-omc/React-%EA%B7%B8%EB%A1%9C%EC%8A%A4-%ED%95%B4%ED%82%B9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C</guid>
            <pubDate>Sat, 31 Aug 2024 09:02:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/7fad406c-1baa-4bf6-ae8d-0105f9d266bf/image.webp" alt=""></p>
<p>안녕하세요. 오늘은 그로스 해킹을 만난 프론트엔드라는 주제에 대해서 포스팅을 다뤄볼까 합니다. </p>
<p>올해 3월부터는 메인 스크럼 별도로 서브 스크럼으로 <strong>퍼널 스크럼</strong>에 참여하고 있는데요.
ios, android, 웹 프론트엔드 개발자들이 모여 각 클라이언트에 존재하는 커스텀 로깅 이벤트를 정리하고, 구글 애널리틱스에서 정의하는 추천 이벤트를 제대로 적용하는 업무를 하고 있어요. </p>
<hr>
<h2 id="퍼널-스크럼과-마구잡이로-추가된-커스텀-로깅-이벤트">퍼널 스크럼과 마구잡이로 추가된 커스텀 로깅 이벤트</h2>
<p>스크럼이 시작된 3월에는 웹 프론트엔드에서 관리하는 코드 베이스에는 정말 다양하고 많은 로깅 이벤트 함수들이 존재했는데요. </p>
<p>프론트엔드 개발환경이 모노레포로 전환되면서, 미쳐 공통 패키지로 모듈화되지 못한 코드들과 각 스크럼 별로 단순 클릭 수치를 기록하기 위해 스프린트 때마다 추가된 무수히 많은 커스텀 로깅 이벤트들로 코드 베이스가 아주 지저분해진 상황이었어요. </p>
<p>그래서 퍼널 스크럼 작업을 진행하면서, 문서가 안내하는 방식을 따라 유입, 탐색, 결정, 구매 등 이커머스에서 보이는 유저의 행동을 정확히 측정하고자 업무가 시작되었습니다.</p>
<hr>
<h2 id="google-tag-manager를-이용해서-로깅-이벤트-관리하기">Google Tag Manager를 이용해서 로깅 이벤트 관리하기</h2>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/ba7c33b5-7b85-4cbc-9ea0-0f5244b8cc5f/image.png" alt=""></p>
<p>프론트엔드 개발자로 팀에 합류하게 되면 어떤 조직이든 마케팅 툴을 연동하는 작업을 요청받은 적이  있을 거에요. 현재 아몬즈에서는 amplitude, GA4, Braze 등 다양한 마케팅 툴을 연동해 고객의 인터렉션을 수치화해 살펴보고 있습니다. </p>
<p>그런데, 각 마케팅 툴 별로 로깅 이벤트를 추가하고 코드를 별도로 관리하면 어떻게 될까요?</p>
<p>분명 코드는 굉장히 지저분해질 것이고 반 년 정도만 지나도 관심도가 떨어진 로깅 이벤트들은 코드 베이스 이 곳 저곳에 산개해서 방치될 것입니다. </p>
<p>이를 방지하기 위해서 아몬즈 프론트엔드 팀은 Google Tag Manager를 다양한 로깅 툴들의 중간 허브로 사용하고 있어요. </p>
<p>아주 간단합니다. </p>
<h3 id="구글-태그-매니저를-사용해-ga4-써드파티-연동하기">구글 태그 매니저를 사용해 GA4, 써드파티 연동하기</h3>
<p>1) 구글 태그 매니저에 이벤트 명과 이벤트 프로퍼티를 전송할 함수를 정의해 핸들러에 연동하고</p>
<pre><code class="language-ts">// react를 사용한다면 아주 간단하고 손쉽게 아래 패키지로 태그매니저를 연동할 수 있습니다.
import TagManager from &quot;react-gtm-module&quot;;

export const addToCart = (data: GTMAddToCartEventType): void =&gt; {
  const { price, products} = data;

  TagManager.dataLayer({
    dataLayer: {
      event: GTM_EVENT.ADD_TO_CART,
      currency: &quot;KRW&quot;,
      value: price,
      items: products,
    },
  });
};
</code></pre>
<pre><code class="language-ts">Button onClick={() =&gt; addToCart(data)}</code></pre>
<p>2) 구글 태그 매니저에서는 해당 이벤트로 인해 발동될 트리거를 정의합니다. 
<img src="https://velog.velcdn.com/images/yunsungyang-omc/post/5693d69c-84f7-42b7-aa4e-493d37f749b8/image.png" alt=""></p>
<p>3) GA4는 태그 유형에 이미 존재하지만, 그외 써드 파티를 사용해 로깅해야한다면 각 마케팅 툴에 맞게 템플릿을 정의하고, 태그 유형으로 맞춤 설정을 정의합니다.
<a href="https://blog.ab180.co/posts/amplitude-gtm-guide">AB180 Blog|구글 태그 매니저로 엠플리튜드 연동하기</a></p>
<p>4) 이벤트 매개변수에 트리거 발동 시 데이터 레이어로 전송된 프로퍼티를 연동해 해당 툴로 데이터를 전송할 수 있도록 연동해주면 모든 작업이 끝났습니다.
<img src="https://velog.velcdn.com/images/yunsungyang-omc/post/2fb7e5fe-0e8a-4f9d-ab15-45b8d009db86/image.png" alt=""></p>
<p>위와 같이 모든 마케팅 툴들은 로깅을 위한 흐름을 갖습니다. </p>
<p>따라서 각 마케팅 툴의 로깅함수들이 코드베이스에 돌아다니도록 방치하지 말고
트리거를 발동시키기 위한 함수와 트리거가 각각 1개씩만 존재해도 여러가지 이벤트를 연동할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/26c19817-2cee-4dee-aae1-dfe7746587cb/image.png" alt=""></p>
<hr>
<h3 id="이커머스에서-유저들의-행동-지표를-트래킹하기">이커머스에서 유저들의 행동 지표를 트래킹하기</h3>
<p>제가 참여하고 있는 퍼널 스크럼에서는 다양한 툴 중 글로벌 스탠다드인 GA4를 기준으로 데이터를 쌓고 있는데요. 이 중 GA4에서 추천 정의한 여러 이벤트 중 <strong>전자상거래 이벤트</strong>를 중심으로 유저들의 데이터를 기록하고 있습니다. </p>
<p>[GA4 공식문서 보기]
(<a href="https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?hl=en&amp;client_type=gtag">https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?hl=en&amp;client_type=gtag</a>)
<a href="https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#view_item_list">GA4 전자상거래 이벤트 레퍼런스 소개</a></p>
<p>수 많은 이벤트 중 몇가지 이벤트를 소개드려보겠습니다.</p>
<h3 id="view_item_list">view_item_list</h3>
<p>view_item_llist 이벤트는 문맥상과 같이 조회하고 있는 목록을 기록하기 위해 사용합니다. 
이커머스 사이트에는 여러가지 목록이 존재할 수 있는데요.
상품 목록, 리뷰 목록, 구매 목록, 홈에서 전시하고 있는 베너 목록 등 목록화해 상품 정보를 전달하고 있는 거의 모든 정보들을 view_item_list 이벤트들을 통해 트래킹할 수 있습니다. </p>
<h3 id="view_item">view_item</h3>
<p>view_item은 상세 정보를 담는 페이지에서 사용합니다. view_item_list를 통해 목록화된 정보 중 특정 아이템이 조회되었다는 것을 기록하기 위해 사용하는 이벤트 입니다. 배열의 아이템 중 한 한가지를 선택(select_item)해서 상세 페이지로 이동한다면 이 인터렉션을 기록하기 위해 view_item을 사용할 수 있습니다. </p>
<h3 id="select_item">select_item</h3>
<p>select_item은 view_item_list 이벤트를 통해 전달된 목록 정보 중 특정 아이템이 선택되었다는 것을 기록하기 위해 사용되는 이벤트입니다. 여기까지 읽어보셨다면 느낌이 오지 않으시나요?</p>
<p><code>탐색(view_item_list)</code> -&gt; <code>선택(select_item)</code> -&gt; <code>조화(view_item)</code>의 순으로 이벤트가 실행이 되면서 유저의 행동 지표가 기록되고 있는거죠. </p>
<p>이 행동 단계를 좀 더 전문적으로 표현하면 점점 많은 정보에서 좁혀지며 상세한 정보로 유입되었다는 의미로 &quot;깔때기(Funnel) 플로우를 타고 있다.&quot; 라고 표현할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/19e8746c-aa51-4a94-adfe-ab17ed24b673/image.png" alt=""></p>
<hr>
<h2 id="이커머스-funnel-플로우를-통해-약한-고리-찾아내기">이커머스 Funnel 플로우를 통해 약한 고리 찾아내기</h2>
<p><code>탐색(view_item_list)</code> -&gt; <code>선택(select_item)</code> -&gt; <code>조회(view_item)</code> 이후로도 퍼널 플로우를 탐색하기 위해 아래와 같은 다양한 이벤트를 연동하면 퍼널의 종착지인 구매 단계까지 유저의 행동 지표를 살펴볼 수 있습니다. </p>
<p><code>장바구니 넣기(add_to_cart)</code>
<code>장바구니 조회(view_cart)</code>
<code>결제 단계 진입(begin_checkout)</code>
<code>결제 완료(purchase)</code></p>
<p>그리고 GA4 전자상거래 탭과 탐색 보고서를 통해 특정 상품이 얼마나 클릭되었는지 혹은 구매되었는지, 장바구니에서 추가된 이후 이탈은 얼마나 발생하고 있는지를 측정할 수 있죠 </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/8f6c1d4b-d5b8-4522-961d-e1f8835f6ba5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/4c3e147d-6760-42f2-87f4-0663edae8e54/image.png" alt=""></p>
<p>위 지표는 블로깅을 위해 샘플링된 데이터지만,
좀 더 세분화해 보고서를 작성해본다면 </p>
<ul>
<li>의도된 상품이 유저들에게 잘 노출되고 클릭되고 있는지</li>
<li>장바구니에서 추가된 상품 숫자와 구매 숫자를 비교해 유저들이 왜 구매를 하지 않는지 </li>
<li>주로 어떤 목록에서 구매가 많이 일어나는지 </li>
</ul>
<p>등의 이커머스 서비스의 강한고리와 약한 고리를 데이터를 통해 추론해 개선 포인트를 마련할 수 있습니다. </p>
<hr>
<h2 id="마치며">마치며</h2>
<p>아마 저 뿐만 아니라, 많은 프론트엔드 개발자께서 생소한 개념의 로깅 이벤트를 연동하기 위해 노력하고 계실 것 같아 이 글을 작성해보았는데요. </p>
<p>다음에는 각 전자상거래의 주요 이벤트를 관찰하기 위해 로깅 함수를 추가하며 경험한 트러블 슈팅에 대해 몇 가지 팁을 작성해보려고 합니다. 이 글이 꼭 도움이 되길 바라며, 궁금하신 부분이 생기신다면 언제든지 댓글로 남겨주세요! 제 경험을 뜸뿍 공유드리겠습니다.</p>
<p>감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) 트러블 슈팅 : antd v4 Form 마이그레이션]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-antd-v-4-Form-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@yunsungyang-omc/React-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-antd-v-4-Form-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Sun, 04 Aug 2024 05:05:59 GMT</pubDate>
            <description><![CDATA[<p>아주 오랜만에 작성하는 벨로그 글입니다. 
미디엄으로 옮겨서 글을 작성하기도 하고, 주로 사내에서 발생한 트러블 슈팅은 노션에 작성하고 있어서 그간 글 작성이 뜸했는데요. 다시 심기 일전해서 제가 겪은 트러블 슈팅을 공유해서 다른 분께 도움을 드리려고 합니다. </p>
<p>이번 글은 <strong>사내 개발 환경 모던화</strong>의 일환으로 진행하고 있는 antd 라이브러리 버전업에거 겪었던 트러블 슈팅에 대한 글입니다. </p>
<h2 id="antd-v3에서-v4로-전환하기">ANTD V3에서 V4로 전환하기</h2>
<hr>
<p>아몬즈를 운영하는 비주얼의 프론트엔드 개발팀은 백 오피스 어드민 제품에 <strong>UI 디자인 라이브러러리로 antd를 사용</strong>하고 있습니다. 어드민 기능 개선은 꾸준히 이루어지고 있었지만, 적절한 시점을 놓쳐 상용 안정화 버전보다 무려 2단계나 낮은 V3버전을 사용하고 있는데요. </p>
<p>&quot;빠르고 쉽고 안정적으로&quot; 기능을 제공받기 위해 사용하는 <strong>antd가 현재에 와서는 오히려 최신 버전의 React 패키지로 나아가는데 큰 장애물</strong>이 되고 있었습니다. </p>
<p>올해부터 팀에서는 최신 버전의 리액트로 전환하고 더이상 유지보수되지 않거나, 결함이 있는 패키지를 차차 제거하고 있는데요. 기타 작업이 모두 진행된 지금 마지막 장애물로 antd 마이그레이션만 남겨두고 있습니다. </p>
<p>antd v3에서 v4로 전환하는건 몇 가지 컴포넌트에서 큰 변경점을 갖습니다. </p>
<p>아주 간단하게 요약하자면, </p>
<ul>
<li>테이블 컴포넌트는 헤드리스 컴포넌트 라이브러리인 React Table을 사용하도록 바뀌었구요,</li>
<li>Form 컴포넌트도 HOC(Higher Order Component) 패턴에서 useForm이라는 훅을 사용하도록 바뀌었지요.</li>
<li>그리고 className 선택자도 많이 변경되어, 혹시 reset.css나 글로벌 스타일 시트에서 선택자를 통해 커스텀 스타일을 적용하고 있다면.. 축하합니다. 변경점을 잘 찾아내서 모두 바꾸어주어야 해요.</li>
</ul>
<p>고로, 마이그레이션을 진행하게 된다면 위 3가지 컴포넌트를 변경점이 아주 큰 난관이 됩니다.</p>
<p><a href="https://4x.ant.design/docs/react/migration-v4">antd 마이그레이션 문서보기</a></p>
<h2 id="form-컴포넌트-라이브러리-전환하기">Form 컴포넌트 라이브러리 전환하기</h2>
<p><a href="https://antd-pixel.vercel.app/components/form/v3/">Form 컴포넌트 마이그레이션 가이드</a></p>
<h3 id="1-반드시-최상위-컴포넌트에서-form-컴포넌트로-래핑하기">1) 반드시 최상위 컴포넌트에서 Form 컴포넌트로 래핑하기</h3>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/d5d3f707-471d-4952-92b7-9515314ca1a8/image.png" alt=""></p>
<p>우선 HOC 패턴을 사용해 하위 컴포넌트까지 전달하는 form props를 전달하는 방식이 바뀌었는데요,</p>
<pre><code class="language-js">import { Form } form &#39;antd-v4&#39;;

const { form } = Form.useForm();</code></pre>
<p>위 처럼 훅을 불러내 하위 컴포넌트로 전달할 form 객체를 생성하고 <code>&lt;Form /&gt;</code> 컴포넌트로 래핑된 하위 컴포넌트들은 <code>&lt;Form.Item/&gt;</code> 컴포넌트로 래핑을 해주어야 합니다. </p>
<blockquote>
<p>이전 버전에서는 단지 HOC Form.create로 하위 컴포넌트를 묶기만해도 form 객체에 접근하고, 내부 상태를 이용가능했는데요. 변경된 버전에서는 반드시 최상위 컴포넌트를 Form으로 묶어주어야 form.getFieldsValue 등 인스턴스의 메소드에서 올바른 값을 얻을 수 있습니다. 만일 Form이 제대로 묶여있지 않으면 인스턴스에서 아무것도 조회되지 않기 때문에 동작이 제대로 되지 않습니다.</p>
</blockquote>
<h3 id="2-form-내부-상태를-조회할-때-유의할-점">2) Form 내부 상태를 조회할 때 유의할 점</h3>
<p>특정 폼 페이지의 유효성 검증과 버튼 등에 사용할 변수를 관리하는 커스텀 훅이 작성되어있다고 가정해봅시다. </p>
<pre><code class="language-js">const useCusotmForm = (form) =&gt; {
  const name = form.getFieldsValue({&#39;id&#39;, form});
  const password = form.getFieldValue({&#39;password&#39;,, form});

  const isValidName = !!name.length &amp;&amp; !form.getFieldError(&#39;name&#39;);
  const isValidPassword = !!password.length &amp;&amp; !form.getFieldError(&#39;password&#39;);


     return {
           isValidName,
           isValidPassword,
     }
 }</code></pre>
<p> V3를 사용하는 form이 위 같은 커스텀 훅을 사용해서 특정 필드에 대한 값이 있는지를 체크하고, 필드의 에러가 있는지 검증하는 로직을 갖추고 있었는데요, </p>
<p> V4로 전환한 이후에는 해당 값들을 <code>getFieldsValue</code>나 <code>getFieldValue</code>로 조회하면 <strong>리랜더링을 일으키지 않는 한 조회가 되지 않는 문제가 있었습니다</strong>. 하위 컴포넌트는 <code>&lt;Form.Item/&gt;</code>으로 묶여있고, <code>rules</code>와 <code>name</code> 필드가 모두 제대로 전달되고 있음을 확인해도, 값이 조회되지 않는 문제였는데요. </p>
<p> 만일 onChange나 onBlur 이벤트 발생할때마다 필드의 유효성 검증을 해야한다면 form.getFieldValue 대신 Form.useWatch를 사용해야 합니다. </p>
<p> 더불어 Form.Item으로 래핑한 컴포넌트에 rules를 전달할때도, </p>
<pre><code class="language-js">&lt;Form.Item
  label={label}
  name={name}
  rules={rules}
  validateTrigger={[&#39;onBlur&#39;, &#39;onChange&#39;]} // &lt;- 트리거 조건을 전달해야합니다.
&gt;</code></pre>
<p><code>validateTrigger</code> 옵션을 사용해 트리거 조건을 설정해야 최상단에서 <code>Form</code>의 값의 변화를 관측할 수 있습니다. </p>
<p><code>form.getFieldError</code>의 경우, 비동기적으로 발생하는 값의 변화를 아쉽게도 알아낼 수 있는 방법이 없는데요, 훅에서 해당 값에 의존하는 로직이 있다면, 이를 제거하고 <code>form.submit</code>시 발생하는 함수 내부에서 <code>form.valiadateFileds</code> 메서드를 통해 폼의 유효성 검증이 모두 완료되었음을 검증해야 합니다.</p>
<blockquote>
<p>더 이상 커스텀 훅 내부에서 특정 필드를 관측하거나 해당 필드의 에러를 알아내려고 시도하지 마세요! 에러는 Input이나 Select를 래핑하고 있는 Form.Items 컴포넌트에 전달한 rules로 유효성 검증을 하고, 최종적인 폼의 검증은 onFinish 함수에서 이뤄지게 해야 합니다.</p>
</blockquote>
<h3 id="3-onfinish-함수와-onfinishfailed-함수의-분리">3) onFinish 함수와 onFinishFailed 함수의 분리</h3>
<p>v3 버전에서는 <code>onFinish</code> 함수 내부에서 <code>try ... catch</code> 블록을 구성하거나 <code>then</code> 체이닝을 통해 에러를 핸들링 했는데요</p>
<pre><code class="language-js">// antd v3
const Demo = ({ form: { getFieldDecorator, validateFields } }) =&gt; {
  const onSubmit = e =&gt; {
    e.preventDefault();
    validateFields((err, values) =&gt; {
      if (!err) {
        console.log(&#39;Received values of form: &#39;, values);
      }
    });
  };

  return (
    &lt;Form onSubmit={onSubmit}&gt;
      &lt;Form.Item&gt;
        {getFieldDecorator(&#39;username&#39;, {
          rules: [{ required: true }],
        })(&lt;Input /&gt;)}
      &lt;/Form.Item&gt;
    &lt;/Form&gt;
  );
};

const WrappedDemo = Form.create()(Demo);</code></pre>
<p>V4 버전에서는 <code>onFinish</code> 함수와 <code>onFinishFailed</code> 함수가 분리되었습니다. </p>
<pre><code class="language-js">const Demo = () =&gt; {
  const onFinish = values =&gt; {
    console.log(&#39;Received values of form: &#39;, values);
  };

  const onFinishFailed = ({ errorFields }) =&gt; {
      form.scrollToField(errorFields[0].name);
  };

  return (
    &lt;Form onFinish={onFinish} onFinishFailed={onFinishFailed}&gt;
      &lt;Form.Item name=&quot;username&quot; rules={[{ required: true }]}&gt;
        &lt;Input /&gt;
      &lt;/Form.Item&gt;
    &lt;/Form&gt;
  );
};</code></pre>
<pre><code class="language-js">// antd v3
validateFields((err, value) =&gt; {
  if (!err) {
    // Do something with value
  }
});
To

// antd v4
validateFields().then(values =&gt; {
  // Do something with value
});</code></pre>
<p>v3에서는 <code>onSumit</code> 함수 내부에서 각 폼의 유효성을 검증하고, 콜백함수를 통해 에러핸들링을 했다면, 지금은 성공처리와 실패처리 역할을 하는 각 함수를 통해 보다 유연하게 코드를 작성할 수 있습니다. (가독성도 챙기구요)  </p>
<h3 id="4-rules에-validator를-전달한다면-이-함수는-반드시-promise를-리턴해야-합니다">4) rules에 validator를 전달한다면, 이 함수는 반드시 Promise를 리턴해야 합니다.</h3>
<p>더불어, 만약 <code>&lt;Form.Item /&gt;</code>에 전달하는 <code>rules</code>에 <code>validator</code>함수를 포함해 전달하고 있다면, 이 <code>validator</code>는 반드시 <code>Promise</code>를 반환하는 함수로 작성이 되어 있어야 합니다.</p>
<pre><code class="language-js">rules={
  [formRequiredRules(true, INVALID_REQUIRED_PASSWORD_MESSAGE, false),
  { validator: validateToConfirmNewPassword },
  { pattern: new RegExp(REX_PASSWORD), message: INVALID_MINIMUM_EIGHT_WORD_ENG_NUM_MESSAGE }]
}</code></pre>
<p>이 부분이 아주 절 골탕먹인 부분이었는데요, 
v3를 기준으로 작성된 코드는 프로미스를 리턴하도록 작성하지 않았어도 큰 문제가 없었겠지만,
v4에서는 전달된 <code>validator</code>가 프로미스를 리턴하지 않는다면 폼의 유효성 검증이 어디선가 블락이 되버리고,
<code>onFinish</code> 함수가 콜 되지 않습니다. 대신 <code>onFinishFailed</code>에서는 아무런 에러가 발생하지 않아 errorFields가 빈 배열로 관측됨에도 계속 이 함수가 콜되죠. </p>
<blockquote>
<p>개인적으로 아주 불편한 점이라고 생각합니다. 디버그도 어렵고요. 다시 강조해서 만일 validator로 전달하는 함수가 프로미스를 리턴하지 않도록 작성되어 있다면 아래의 예시처럼 바꾸어주세요</p>
</blockquote>
<pre><code class="language-js">const validateToConfirmNewPassword = (_, value) =&gt; {
    if (value) {
      return Promise.resolve();
    } else {
      return Promise.reject(new Error(&#39;비밀번호가 일치하지 않습니다.&#39;));
    }
  };</code></pre>
<hr>
<h2 id="마이그레이션-후기">마이그레이션 후기</h2>
<p>아직 현재 진행 중이지만, <code>antd Form</code>을 마이그레이션 한다면, 위 4가지 사항 정도를 필히 숙지하시는게 좋습니다. <code>antd</code>를 버전업하는 작업을 진행하고는 있지만, 전 개인적으로 <code>react-hooks-form</code>과 <code>formik</code>을 조합해 폼 컴포넌트를 구성하는게 훨씬 DX가 좋다고 느껴지네요. </p>
<p><code>antd</code>패키지의 4 버전도 현재는 레거시로 취급받고 있고, 향후 개발환경을 고려한다면 V5로 빌드업을 또 진행해야겠지만, MVP 레벨이 아닌 프로덕션을 운영 중인 회사의 제품에서 굳이 <code>antd</code> 패키지 사용을 고집할 필요가 있을까? 라는 생각이 드는 것도 사실입니다.</p>
<p>동료를 통해 알게 된 사실이지만, v5에서는 공식문서에서 컴포넌트를 래핑해 커스텀한 디자인을 입히지 말라고 명시되어 있다고도 하구요.</p>
<p>이 패키지에 대한 인기도 전과 같지 않고 shadn ui같은 훨씬 좋은 ui 라이브러리들이 존재하기도 하구요.</p>
<p>누군가에게 이 글이 필히 도움이 되었으면 좋겠습니다. 
추가적으로 질문 생기시면 댓글로 남겨주시거나 이메일을 주셔도 좋습니다. 
감사합니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2023년, 성장하는 한 해를 보냈다.]]></title>
            <link>https://velog.io/@yunsungyang-omc/23%EB%85%84-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@yunsungyang-omc/23%EB%85%84-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Mon, 01 Jan 2024 10:01:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/31781e37-c290-42c9-aefb-06e4b7714eb7/image.png" alt=""></p>
<p>근 1년이 넘는 시간 동안 벨로그에 글을 쓰지 못했네요.
많은 이벤트가 있었고, 다양한 느낀 점이 있었던 2023년이었습니다. 
그래서, 다시 벨로그에 장작도 땔 겸 23년의 회고록을 남겨보려고해요.</p>
<h2 id="이직을-했습니다-12월---2월">이직을 했습니다. (12월 - 2월)</h2>
<p>첫 회사가 시리즈 A 투자 유치 성공 이후 구성원을 많이 불렸지만,
새로운 비즈니스 모델을 만드는데 실패하고 투자 유치도 요원해지면서 
급격히 경영 사정이 어려워졌습니다.</p>
<p>4대보험 납부 지연을 시작으로ㅡ 화기애애했던 회사 분위기가 냉랭해지는게 느껴져서
과감히 이직을 준비했었는데요. </p>
<p>22년 10월부터 이력서를 업데이트 하고 이직을 준비한지 2개월차에
지금의 회사 채용 과제를 진행하게 되었습니다.</p>
<p>22년 12월은 여러 회사의 과제와 면접을 동시에 진행해야해서 너무 힘들었던 기억이 아직도 나요.
정말 추운 겨울이었지만 과제를 제출하고, 몇 번의 면접을 진행한 결과
지금의 회사를 만나게 되었습니다.</p>
<p>참! 전 지금 반지, 목걸이 등 보석 류를 취급하는 커머스 플랫폼 <strong>아몬즈</strong>를 
운영하는 비주얼이라는 회사에서 일하고 있습니다 :-)</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/790ad1a9-aa5b-4182-9bb4-ee812aa9444e/image.png" alt=""></p>
<hr>
<h2 id="팀-비주얼에서-안정적으로-성장-3월---현재까지">팀 비주얼에서 안정적으로 성장 (3월 - 현재까지)</h2>
<p>잡플래닛에서의 평가와는 달리 합류한 회사는 굉장히 좋은 문화를 가지고 있는 회사입니다. 
열정적이고 의욕적인 개발팀, 그 개발팀을 리드하는 리더들도 굉장히 좋은 분들입니다. 
코드 리뷰 뿐만 아니라, 안정적인 배포 과정, 수준 높은 코드 리뷰 등 
주니어 개발자인 제게 안정적인 성장환경을 제공하고 있어 의욕적으로 개발 업무를 이어나가고 있습니다. </p>
<h3 id="어드민-서비스에서-abacattribute-based-access-control-권한을-도입">어드민 서비스에서 ABAC(Attribute-Based Access Control) 권한을 도입</h3>
<p>새로운 회사에 합류 후 약 한 달 동안의 온보딩 과정을 거쳐, 내부 고객(비주얼 팀원들)을 위한 어드민 서비스를 고도화하는 업무를 진행하는 백오피스 스크럼에 합류하게 되었어요. 
3번 정도의 스크럼 기간 동안, 가장 기억 나는 것은 ABAC 기반 권한 체계를 도입했다는거에요.</p>
<p>기존에는 권한 체계가 1, 2, 3등급으로 나눠진 롤 베이스 권한 체계(RBAC)였지만,
새롭게 개선된 권한 체계는 상품, 주문, 정산 등 다양한 주제에서 조회 수정 생성 삭제 등 액션을 선택적으로 지정해서 권한을 부여하는 체계로 개편되었어요 :-) </p>
<p>이 과정에서 컨텍스트 기반 로그인 컨트롤도 함께 도입을 했습니다. 이 부분은 별도 포스팅으로 상세하게 글을 남길게요 :-)</p>
<h3 id="상품-스크럼에-합류해서-고객의-편의성을-개선하고-있습니다">상품 스크럼에 합류해서 고객의 편의성을 개선하고 있습니다.</h3>
<p>백 오피스 스크럼은 스크럼 백로그에 정도 이상의 업무가 쌓이거나, 시급한 개선을 요하는 경우에 진행되는 임시 스크럼이었습니다. 시급한 개선 포인트를 마무리한 이후에는 현재 후기, 상품 상세 페이지 등을 개선하는 상품 스크럼에 소속해서 외부 유저(아몬즈 웹, 모바일 서비스 이용 고객)의 편의성을 개선하는 업무를 진행하고 있어요!
<img src="https://velog.velcdn.com/images/yunsungyang-omc/post/d99c5c00-c66c-4ac5-9a16-1f13c99f2b61/image.png" alt="">
상품 상세 페이지에서 유저가 구매할 때 받게되는 구매혜택(최저가, 쿠폰, 포인트)을 자세하게 노출하고
<img src="https://velog.velcdn.com/images/yunsungyang-omc/post/e79b2ae7-733f-45d8-b593-f8c63f910ff5/image.png" alt="">
쿠폰을 상세페이지에서 다운로드할 수 있게하는 작업 등을 작년 하반기 동안 진행했습니다. 
<img src="https://velog.velcdn.com/images/yunsungyang-omc/post/c1cc2605-b0de-4be7-b760-77c4750c1776/image.png" alt="">
후기를 좀 더 고도화해서, 사이즈, 색상 등에 객관적인 평가지표를 라디오 버튼으로 선택하게 하는 리뷰 고도화 작업 또한 함께 진행했네요!</p>
<p>현재 아몬즈 서비스는 웹 / 앱 모두에서 제공하고 있어요!
그중 저를 포함해서 웹 프론트엔드 개발자는 모노레포로 구성된 웹 서비스와 모바일 서비스를 유지 보수 관리하고 있습니다! </p>
<p>웹 서비스는 좀 더 활성화되어있는 앱에 비해 수치가 보다 낮지만, 
24년도에는 웹 서비스에서의 구매 전환율을 전년보다 훨씬 높이는 것을 목표로 삼고 있어요!</p>
<hr>
<h2 id="개발자-커뮤니티에서-활동">개발자 커뮤니티에서 활동</h2>
<p>벨로그에 글을 쓰거나, 가끔 노마드 코더 슬랙방에서 의견을 주고 받다가
우연한 기회에 들어가게 된 개발바닥 오픈채팅방에서 다양한 개발자 분들을 만나 의견을 주고 받고 있습니다. </p>
<p>작년 1월부터 진행하게 된 <strong>JS 스터디 디스코드 방</strong>은 현재까지도 활동을 이어가고 있어요!
처음에는 각자 발표하고 싶었던 주제를 발표했지만,</p>
<p>작년 8월부터는 <strong>자바스크립트 모던 딥다이브</strong> 완독을 목표로 12월까지 함께 스터디를 이어 갔어요 :-)
지금은 타입스크립트를 함께 공부하고 있답니다. 짧게 끝날 줄 알았던 스터디가 이렇게 장수할 수 있는게 신기하네요</p>
<p>JS 스터디를 진행하면서도, 향로님과 호돌맨님께 실시간으로 받은 면접 첨삭의 은혜도 보은할 겸 
선한 영향력을 다른 분들에게 나누고 싶다는 생각이 들었어요.
그래서 다른 개발자 분들과 힘을 합쳐 면접 스터디 방의 멘토로 참여하기도 했습니다. </p>
<p>4개의 프론트엔드 면접 스터디 팀에는 저보다도 경력이 길었던 분들도 계셨는데요,
다년 간의 썰풀기(정훈장교 6년, 마케팅 3년)로 다져진 제게는 경력과 상관없이 멘토할 수 있는 포인트들이 있더라구요. 그래서 즐겁게 멘토링을 했던 것 같습니다. </p>
<p>모든 분들이 이직에 성공하시지는 않았지만, 
그래도 5분이 넘게 이직에 성공하셔서 꽤 뿌듯했던 기억으로 남아있네요!</p>
<p>지금은 2사로의 부방장으로 활동하고 있어요! 일이 바빠서, 채팅방을 자주 보지는 못하지만 좋은 글이 생기면 링크를 남기고 있으니 언제든지 찾아주시면 환영입니다! </p>
<hr>
<h2 id="2024년의-목표">2024년의 목표</h2>
<p>2024년에는 주니어 딱지를 떼기 위한 다양한 활동을 진행해보려고 해요! 
멘토링과 스터디 리드를 진행했지만, 
올해에는 좀 더 다양한 분들에게 도움을 줄 수 있는 방법을 찾아보려고 해요.</p>
<h3 id="사이드-프로젝트-진행하기"><strong>사이드 프로젝트 진행하기</strong></h3>
<p>작년 한 해 동안 좋은 개발팀에 소속되어,  1인분을 충실하게 담당하기에 주력했습니다. 
과연, 1인분을 충실하게 해냈느냐?에 대해 스스로 묻는다면 <strong>기본은 했다</strong>라고 말할 수 있을 것 같네요!</p>
<p>스타트업이지만, 스크럼 / 팀 / 실에서 제가 할 수 있는 모든 것을 결정할 수는 없다는 걸 깨달았어요!
그래서 기술적으로 도전해보고 싶은 포인트 혹은 경험을 좀 더 비동기적으로 쌓기 위해 사이드 프로젝트를 진행해보려고 합니다. 지금 생각하고 있는 건 기부 플랫폼인데요! 이건 자세한 아이디어를 별도 글로 풀도록 할게요!</p>
<h3 id="사이드-잡을-찾아보려고-해요">사이드 잡을 찾아보려고 해요!</h3>
<p>멘토링과 스터디 리드를  진행하면서, 취준생 혹은 동일한 개발에 흥미를 가진 사회인을 대상으로 수업을 진행해보고 싶어졌어요! 그래서 사이드 잡으로 <strong>멘토링 서비스</strong>에 참여하고 싶다는 생각과 목표를 가지고 준비를 하고 있어요</p>
<h3 id="팀-비주얼로써-좀-더-높은-목표에-도달할-수-있도록-기여하기">팀 비주얼로써 좀 더 높은 목표에 도달할 수 있도록 기여하기</h3>
<p>시리즈 B를 유치에 성공했고, 실제로 매출을 내고 있는 서비스를 운영하는 회사의 개발팀에 속해있지만, 전 아직도 목마르다는 생각을 하고 있어요! 무신사나 쿠팡, 컬리 같은 모두가 알고 선망하는 회사로 옮겨가는 것을 목표하기 보다는 지금의 회사를 위의 회사같이 선망받는 회사로 가꾸는 게 아직까지의 목표에요! </p>
<p>그래서 팀에서 한 명의 개발자로 책임을 다할 뿐만 아니라, 좋은 동료를 계속 소개하고 함께 서비스를 가꿔나가고 싶어요! 아마 이게 24년의 제 가장 큰 목표가 될 것 같아요!  팀 비주얼에서는 계속 좋은 동료를 찾고 있어요! 혹 관심 생기시면 지원해보시길 바랄게요!</p>
<p><a href="https://shue.notion.site/02cd717fc5af4ef0bf57967bb0c86166">비주얼 채용 페이지 확인하기</a></p>
<h2 id="모두-새해-복-많이-받으시고-더-힘찬-2024년-되길-바랄게요">모두 새해 복 많이 받으시고, 더 힘찬 2024년 되길 바랄게요!</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) 트러블 슈팅 : 카드리더기 프린터를 이용하는 환경에서 오토커팅 구현하기]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-Nicepay-%ED%8F%AC%EC%8A%A4%EA%B8%B0%EC%97%90%EC%84%9C-%ED%94%84%EB%A6%B0%ED%8A%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/React-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-Nicepay-%ED%8F%AC%EC%8A%A4%EA%B8%B0%EC%97%90%EC%84%9C-%ED%94%84%EB%A6%B0%ED%8A%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 06 Feb 2023 07:24:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/6906ffbb-ef2e-44e6-b9c0-08dc1a1725d6/image.png" alt=""></p>
<hr>
<p>최근 포스팅(<a href="https://velog.io/@yunsungyang-omc/React-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-POS-%ED%94%84%EB%A6%B0%ED%8C%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">리액트 프로덕트에서 POS 구현하기</a>)을 통해 웹 어플리케이션에서 POS 프린터 기능 구현에 대한 경험을 풀어냈습니다.</p>
<p>회사에 구비해 놓은 앱솔론 POS 프린터기에서 꽤 잘 작동했고, QA 팀의 검수도 통과해 큰 문제가 없을거라고 생각했었는데요? 문제가 일어나고 말았습니다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/7fe15bc1-ca01-4b67-b1f8-33c8b6488cf5/image.png" alt=""></p>
<p>기능을 구현하기에 앞서, 해당 프로덕트를 사용할 환경에서 사용하고 있는 동일한 기기를 구비해달라고 그렇게 요청드렸건만...</p>
<p>막상 현장에 실사 테스트에 나가보니, 매장에는 기대하던 POS 프린터기와 평범한 PC가 아닌 KCP-C3100이라고 부르는 카드 리더기와 OKPOS 단말기가 세팅되어 있었습니다. </p>
<p>(okpos 단말기는 식당에서 아주 많이 사용하는..네 그 기기가 맞습니다.) </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/4397b453-c9e5-4bf7-8670-0bbe151a1593/image.jpg" alt=""></p>
<p>OKPOS 단말기야 앞뒤로 디스플레이가 세팅된 일종의 PC라고 생각하면 문제될 것이 없는데, 카드 단말기가 말썽이었습니다. 분명, 앱솔론 POS 프린터에서 잘만 출력되는데, 이 카드리더기가 물고 있는 COM1 포트로 프린트 요청을 보내면 정상적인 텍스트 대신 <strong>****</strong>라는 별표가 대신 프린트됩니다. </p>
<h3 id="문제-1--okpos-단말기-내-nhn-포스-프로그램은-출력속도-38400을-지원한다">문제 1 : OKPOS 단말기 내 nhn 포스 프로그램은 출력속도 38400을 지원한다.</h3>
<p>이 문제를 해결하기 위해서는 kcp-c3100 가맹 안내 블로그에서 제공하고 있는 POS 기능 활성화 안내를 참고해야했습니다. <a href="https://blog.naver.com/teraonmaster/222394284389">링크</a></p>
<pre><code>특수키 입력 후 1200 
포스사용: 1번 선택 
단말 연동 POS 통신 방식: 1번 선택
통신포트선택: 2번 선택 
통신속도선택: 3번(38400) 선택 
한줄 인쇄 모드: 1번 선택
단말 연동 화면 표시시간 설정: 3초</code></pre><p>라이브러리에서 포트를 열때, 넘겨주고 있는 통신속도는 9600(<code>baudRate: 9600</code>)입니다. pos 프린터기에서는 9600속도를 일반적으로 가져가지만, 카드리더기인만큼 4배나 빠른 속도를 사용하는 것 같네요.</p>
<p>그래서 코드를 아래와 같이 수정해주었습니다. </p>
<pre><code class="language-js">  const onClickPrintHandler = async () =&gt; {
    const data = await render(UserReceipt({ orderinfo }));
    const port = await window.navigator?.serial?.requestPort();
    if (port.writable === null) {
      await port.open({ baudRate: 38400 }); // ← 9600을 38400으로
    }
    const writer = port.writable?.getWriter();
    if (writer !== null) {
      await writer!.write(data)
      writer!.releaseLock();
    }
    await port.close();
  };</code></pre>
<p>해당 방식대로 카드 리더기의 세팅을 손 봐주니, 프린트 기능은 작동합니다. 
하지만 문제 하나 건너 문제라더니, 또 다른 문제가 발생하더군요. </p>
<h3 id="문제-2--오토커팅이-작동되지-않는다">문제 2 : 오토커팅이 작동되지 않는다</h3>
<h4 id="1️⃣-cut함수를-function-b타입으로-바꾸기">1️⃣ cut함수를 Function B타입으로 바꾸기</h4>
<p>해당 이슈는 꽤 곤혹스러웠습니다. 인쇄물이 출력된다는 것은 print 관련 함수가 제대로 동작한다는 뜻일텐데, 유독 커팅 명령만 제대로 작동하지 않고 있었거든요. </p>
<p>주문이 몰리거나, 바쁠 시 매장에서 일일히 영수증을 가위로 자를 수는 없습니다. 그래서  반드시 해결해야 할 이슈라고 생각하고 매장 구석에 앉아 라이브러리를 뜯어보기 시작했습니다. </p>
<pre><code class="language-ts">// react-thermal-printer/packages/printer/src/BasePrinter.ts
  cut(): this {
    this.cmds.push({
      name: &#39;cut&#39;,
      data: cut(48),
    });
    return this;
  }</code></pre>
<pre><code class="language-ts">// react-thermal-printer/packages/printer/src/commands/cut.ts
import { GS } from &#39;./common&#39;;

/**
 * Select cut mode and cut paper
 * &lt;Function A&gt;
 * | Format   | Value    |
 * |---------|----------|
 * | ASCII   | GS V m  |
 * | Hex     | 1D 56 m |
 * | Decimal | 29 86 m |
 *
 * &lt;Function B, C, D&gt;
 * | Format   | Value    |
 * |---------|-----------|
 * | ASCII   | GS V m n  |
 * | Hex     | 1D 56 m n |
 * | Decimal | 29 86 m n |
 *
 * @see https://www.epson-biz.com/modules/ref_escpos/index.php?content_id=87
 */
export function cut(m: number, n?: number) {
  const cmd = [GS, 0x56, m];
  if (n != null) {
    cmd.push(n);
  }
  return cmd;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/e2ee2b02-746e-4cc2-af8b-2849a352fbd7/image.png" alt=""></p>
<p>위 코드와 epson 사의 프린트 기술 문서를 확인해 보니, 프린터 대부분이 커팅관련해서는 <code>Function A</code> 와 <code>Function B</code>를 지원하고 있었습니다. 그리고 라이브러리에는 <code>A</code>타입을 선택하는지 <code>cut</code>함수의 파라미터로 숫자<code>48</code>을 넘겨주고 있구요.</p>
<p>그래서 커팅함수의 타입을 B로 바꾸면 커팅기능이 제대로 작동할까 싶어,<code>BasePrinter.ts</code>의 <code>cut</code> 함수의 파라미터를 48에서 65와 0에서 250 사이의 정수를 선택해 넣어주었습니다. </p>
<pre><code class="language-ts">// react-thermal-printer/packages/printer/src/BasePrinter.ts
  cut(): this {
    this.cmds.push({
      name: &#39;cut&#39;,
      data: cut(65, 65),
    });
    return this;
  }</code></pre>
<p>이렇게 전달하니 분명 해결될 것 같았는데, 역시나 오토커팅은 해결되지 않습니다. 
그 순간 불현듯 머리를 스쳐가는게 하나 있었습니다. </p>
<h4 id="2️⃣-문제는-portclose-😤">2️⃣ 문제는 port.close() 😤</h4>
<pre><code class="language-js">  const onClickPrintHandler = async () =&gt; {
    const data = await render(UserReceipt({ orderinfo }));
    const port = await window.navigator?.serial?.requestPort();
    if (port.writable === null) {
      await port.open({ baudRate: 38400 }); // 9600에서 38400으로 수치 조절
    }
    const writer = port.writable?.getWriter(); // 1)
    if (writer !== null) {
      await writer!.write(data) // 2)
      writer!.releaseLock(); // 3)
    }
    await port.close(); // 4)
  };</code></pre>
<p>짧막하게 프린트 핸들러 함수를 설명하면, </p>
<pre><code>1) port를 열어 열거 가능한 객체를 나타내는 writer를 조회한다.

2) writer 객체의 프로퍼티 중에서 write의 파라미터로 
영수증 컴포넌트를 전달함으로서 프린트 함수가 작동한다. 

3) releaseLock()은 포트 요청에 외부 변인이 침범이 발생하지 않도록, 
포트 요청을 잠그는 역할을 한다.

4) 포트가 이미 열려있다면, 이후 프린트 요청 간 포트를 열려고 할 때 
에러가 나기 때문에, 포트를 닫아주어야 한다.</code></pre><p>라이브러리 샘플 예제에는 4번 항목이 존재하지 않습니다. </p>
<p>하지만, 포트를 열고난 후 닫아주지 않는다면 거듭 영수증 출력을 할 수 없을 뿐더러, POS기기에서도 카드 결제로 영수증을 출력하려 할때 제대로 작동하지 않게 됩니다. (모두 COM1 포트를 사용하고 있기 때문입니다.)</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/4f11d3f7-d10e-49cb-9d9b-75379a20fbfd/image.png" alt=""></p>
<p>위 코드의 함수는 사내에서 테스트 할 경우 아무런 문제가 없었지만, 실제 사용환경에서는 <code>port.close()</code> 명령이 영수증 출력에 채 끝나기도 전에 port를 닫도록 요청을 보내는듯 했습니다.</p>
<blockquote>
<p>await는 함수의 동기적 실행 순서를 보장할 뿐, 그 타이밍까지는 보장하지 않으니깐요</p>
</blockquote>
<p>하여 다음과 같이 코드를 수정했습니다. </p>
<pre><code class="language-ts">  const onClickPrintHandler = async () =&gt; {
    const data = await render(UserReceipt({ orderinfo }));
    const port = await window.navigator?.serial?.requestPort();
    if (port.writable === null) { // ← 최초 실행에만 포트를 연다.
      await port.open({ baudRate: 38400 });
    }
    const writer = port.writable?.getWriter();
    if (writer !== null) {
      await writer!.write(data).then(() =&gt; setTimeout(() =&gt; port.close(), 500)); // ←
      writer!.releaseLock();
    }
  };</code></pre>
<p>포트의 열거 가능한 객체 writer에 출력 요청을 보내고 난 후, 
<code>setTimeout</code>함수를 이용해 <code>port.close()</code>를 실행하도록 조치했습니다.
더불어 열거가능한 객체가 없을 때를 나타내는, 포트를 맨 처음 열때만 <code>port.open()</code>이 실행되도록 if 문으로 감싸주었습니다</p>
<p>이 조치를 통해 포트를 닫는 요청은 콜스택이 모두 비워질때까지 테스크 큐에서 기다렸다가 실행될 것입니다. 동기적으로 함수가 작동할 수 있도록 트릭을 이용해 프로그래밍해준 것이죠. </p>
<blockquote>
<p>이 결과 영수증 출력이 아주 잘 되었습니다.</p>
</blockquote>
<ul>
<li>500밀리 세컨즈를 인자로 넣어주었지만, 사실 0을 넣어도 결과는 마찬가지일겁니다.</li>
<li>if 문을 제거하더라도 함수는 잘 동작할 것 같네요. 포트는 언제나 닫히지깐요!</li>
</ul>
<p>결국, 영수증 출력이 되지 않는 범인은 <code>port.close()</code>이 한 줄이 원인이었네요. 😪</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/6fec468f-3743-4eb8-9e7b-8d4d7b9a1ea6/image.jpg" alt=""></p>
<p>이렇게 트러블 슈팅 성공!!</p>
<hr>
<h3 id="주니어는-트러블-슈팅에서-배운다">주니어는 트러블 슈팅에서 배운다.</h3>
<p>마블러스에서 마지막 테스크가 될 POS 프린터를 깔끔하게 마무리 지을 수 있어서, 굉장히 기분 좋은 트러블 슈팅 경험을 쌓게 되었습니다. 끝끝내 마무리 하지 못했다면 두고 두고 생각났을 것 같거든요.</p>
<p>라이브러리를 이용해 간단하게 구현될 수 있을 것 같은 문제도 때때로는 실행 환경에 따라서, 제대로 실행되지 않거나, 라이브러리 자체의 세팅 값을 바꿔주어야 할 경우도 많이 발생합니다. </p>
<p>그때는 좌절하지 말고, 적극적으로 트러블 슈팅에 몰입한다면 온전히 나만의 경험으로 만들 수 있을 뿐만 아니라, 라이브러리를 보다 정교화할 수 있는 경험을 쌓을 수 있게 될 것 같습니다. </p>
<p>아마, 윈도우 네이티브 앱 혹은 모바일 네이티브 앱으로 POS를 구현해본 분들은 많겠지만, 웹 클라이언트, okpos 그리고 카드리더기 환경을 이용하는 pos 영수증 출력 기능을 구현해본 사람은 많지 않을 것 같습니다. </p>
<p>위 조합은 자영업자들에게는 국룰 처럼 여겨지기에 누군가 이 블로그를 보고 동일한 과업을 받는다면 쉽게 해결할 수 있기를 기대하겠습니다. </p>
<p>긴 글 읽어주셔서 감사합니다 :-) </p>
<hr>
<h3 id="함께-읽어보면-좋을-자료">함께 읽어보면 좋을 자료</h3>
<p><a href="https://wicg.github.io/serial/#dom-serialport-writable">Web Serial API</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) 리엑트 프로덕트에서 소리 알람 기능 구현하기]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-%EB%A6%AC%EC%97%91%ED%8A%B8-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8%EC%97%90%EC%84%9C-%EC%86%8C%EB%A6%AC-%EC%95%8C%EB%9E%8C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/React-%EB%A6%AC%EC%97%91%ED%8A%B8-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8%EC%97%90%EC%84%9C-%EC%86%8C%EB%A6%AC-%EC%95%8C%EB%9E%8C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 14 Jan 2023 01:25:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/2e6fb90b-34dd-43ba-b633-4d2c8dc66e4a/image.png" alt=""></p>
<hr>
<p>직전 포스팅으로 <strong>&quot;리액트 프로덕트에서 프린트 기능 구현하기&quot;</strong>에 대한 글을 작성했었는데, 이번에도 추가 기능을 요청받아 구현한 이야기를 작성하게 되었습니다. </p>
<p>11월에 커머스 프로젝트가 고도몰로 넘어간 이후 <code>Jquery</code>길을 걷게될 줄만 알았는데, 다행히 React 라이브러리를 계속 갈고 닦고 있네요. </p>
<hr>
<h2 id="🧲-web-환경에서-소리-효과-구현하기">🧲 Web 환경에서 소리 효과 구현하기</h2>
<p>웹에서 오디오 기능을 활용하기 위해서 가장 쉬운 방법으로는 HTML태그 중 <code>&lt;audio /&gt;</code>태그를 사용하는 것입니다. </p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio">MDN 문서</a></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/764bbb9b-8521-4c08-900c-a1739a9f0410/image.png" alt=""></p>
<p>오디오 태그의 소스로 public 폴더 혹은 assets 폴더에 위치한 소리 파일의 경로를 넣어주면 아주 간단하게 구현가능합니다. </p>
<p>HTML 태그를 이용해서 오디오 기능을 구현할 수도 있지만, 
널리 쓰이는 라이브러리가 존재합니다. </p>
<h2 id="🔍-usesound-라이브러리">🔍 useSound 라이브러리</h2>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/e0e94781-5a0f-4ed8-a73e-cd11918955e2/image.png" alt=""></p>
<p><a href="https://www.npmjs.com/package/use-sound">npm</a> <a href="https://www.npmjs.com/package/use-sound">github</a></p>
<p>README 에서 소개하듯 리엑트에서 훅의 형태로 소리기능을 구현할 수 있는 아주 유용한 라이브러리입니다.  이 라이브러리를 사용하면 <strong>굳이 HTML audio 태그를 사용하지 않고도 특정 상황이나 이벤트에 오디오를 재생할 수 있는 기능을 추가</strong>해줄 수 있습니다. </p>
<h4 id="usesound-라이브러리-사용법">useSound 라이브러리 사용법</h4>
<pre><code class="language-js">import useSound from &#39;use-sound&#39;;

import boopSfx from &#39;../../sounds/boop.mp3&#39;;

const BoopButton = () =&gt; {
  const [play] = useSound(boopSfx);

  return &lt;button onClick={play}&gt;Boop!&lt;/button&gt;;
};</code></pre>
<p>사용방법은 엄청나게 심플합니다. 라이브러리와 소리파일을 임포트한 후 훅의 형태로 사용해주기만 하면 끝!인거죠. 이제 이 라이브러리를 이용해서 요구받은 &quot;소리알람&quot;기능을 구현해보려합니다.</p>
<hr>
<h3 id="🫥-구현할-soundalarm-요구사항">🫥 구현할 SoundAlarm 요구사항</h3>
<pre><code>- 주문이 접수되면 소리 알람으로 사용자에게 알려준다.
- 주문 탭(주문 대기 / 접수 / 픽업 요청 / 픽업) 어디에서라도 새로운 주문이 접수되면 소리 알람 기능이 제공되야 한다. </code></pre><p>위 요구사항을 충족할 수 있도록 구현된 벤더 어드민 프로덕트의 파일구조를 살펴보았습니다.</p>
<pre><code class="language-js">// index.js

const Dashboard = () =&gt; {
  const searchProps = {
    sort,
    setSort,
    searchType,
    setSearchType,
    searchString,
    setSearchString,
    searchDate,
    setSearchDate,
    searchStatus,
    setSearchStatus,
  };

  const { vendorState, getVendorOrderList} = useVendor();

  const {
    data: orderListData,
    refetch: refetchOrderList,
    dataUpdatedAt: updatedAtOrderListData,
  } = getVendorOrderList(
    {
      limit,
      page,
      sort,
      searchStatus,
      searchDate,
      searchType,
      searchString,
      selectedCategory,
    },
    {
      // Refetch the data every 10 second
      refetchInterval: 10000,
      onSuccess: () =&gt; {
        refetchOrderCount();
      },
    }

  return (
    &lt;Container&gt;
      &lt;VendorHeader auth={auth} /&gt;
      &lt;FixedStyledTabs /&gt;
      {CategoryList.map(
         (item, index) =&gt;
           (
             &lt;VendorOrderList /&gt;
            )
      )}
    &lt;/Container&gt;
  );
};

export default Dashboard;</code></pre>
<p>searchProps는 tab을 클릭할때마다 새롭게 설정이 되고, 이 새롭게 설정된 searchProps가 useQuery로 데이터를 패칭하는 함수 <code>getVendorOrderList</code>의 쿼리 키로 전달되는 구조로 데이터를 패칭하고 있군요. </p>
<p>패칭 결과로 전달된 <code>orderListData</code>가 옵션에 맞는 주문 리스트를 렌더링하도록 map 함수를 사용하고 있습니다. </p>
<p>여기서 한 가지 고민이 생깁니다. </p>
<blockquote>
<p>🧐 탭을 클릭할때마다 조건에 맞는 주문의 길이가 달라지는 거면, 요구사항 2번(탭이 어디에 위치하든, 새로운 주문이 접수되면 벨을 울린다.)을 어떻게 충족할 수 있을까? </p>
</blockquote>
<p>정답은 생각보다 간단했습니다. </p>
<p>api 요청결과 넘어오는 값 중 각 단계별로 주문의 수를 전달하는 배열이 존재했는데, 이 배열의 값 중 &quot;주문대기&quot;의 숫자를 비교해서 증감했을때만 벨을 울리면 되는 것입니다. </p>
<p>따라서, 이 배열의 값과 비교해야할 &quot;절대값&quot;을 다음과 같은 코드로 구현했습니다. </p>
<h3 id="😇-useref를-이용해서-절대값과-상대값-비교하기">😇 useRef를 이용해서 절대값과 상대값 비교하기</h3>
<p>React 함수 컴포넌트에서 ref 를 사용 할 때에는 useRef 라는 Hook 함수를 사용합니다. 이 ref의 current 값은 리랜더링되더라도 변하지 않기 때문에 절대값으로 사용하기 아주 유용합니다. </p>
<pre><code class="language-js">import React, { useRef, useEffect } from &quot;react&quot;;
import useSound from &quot;use-sound&quot;;
import bell from &quot;public/assets/sounds/bell.mp3&quot;;

const soundRef = useRef(undefined);
const [soundPlay] = useSound(bell);

// res의 data 결과를 담고있는 vendorState의 값중 orderTags는 각 상태별 주문의 수를 담고 있는 배열입니다. 
const getCountOfWaitOrder = () =&gt; {
  if (vendorState?.orderTags !== null &amp;&amp; vendorState?.orderTags) {
      const count = vendorState?.orderTags.find(
      (tag) =&gt; tag.slug === &quot;smartorder-wait&quot;
  )?.total;
  return count;
  }
};


useEffect(() =&gt; {
 const countOfWait = getCountOfWaitOrder();

 // 레프의 초기값(0)보다 최근 주문시간이 크고, 대기중인 주문의  // 카운트가 1이상일때 : 주문
 if (soundRef.current &lt; countOfWait) {
     // 사운드를 출력시키고, 주문 카운트를 ref로 등록시킨다.
     soundPlay();
     soundRef.current = countOfWait;
 } else {
    // 주문이 취소되거나, 상태가 변경되었을댄, 대기 중인 주문의 수가 더 적으므로, 줄어든 값을 레프로 등록시킨다.
    soundRef.current = countOfWait;
  }
}, [vendorState]);</code></pre>
<p>주문이 접수되면 해당 페이지에서는 다음과 같은 흐름이 일어납니다. </p>
<pre><code>1) 10초 마다 한 번씩 주문 상태의 값을 받아오는 api가 새로운 주문을 감지합니다. 

2) 업데이트 된 상태 &quot;vendorState&quot;의 프로퍼티 OrderTags의 값을 갱신시킵니다. 그리고 이 값은 함수 getCountofWaitOrder로 얻고 있는 &quot;대기중인 주문 건 수&quot;를 최신화합니다. 

3) useEffect 함수에서는 주시 중인 의존성 배열의 값이 변하였기 때문에 최산화된 &quot;대기 중인 주문&quot;의 수와 비교 절대값 ref을 비교하려고 합니다. </code></pre><p>근데, ref의 초기 값이 0이 아니라 undefined를 설정한 이유가 무엇일까요? </p>
<p>soundRef의 current의 값이 0으로 설정하면 다음과 같은 현상이 생깁니다.</p>
<pre><code>1) ref의 current 값은 0이다. 
2) 갱신된 대기 중인 주문의 count는 0보다 크다. 
3) 따라서, 페이지를 현재 경로로 다시돌아오게 되면 무조건 벨이 울린다. </code></pre><p>값이 0이기 때문에, 로고를 클릭해서 현재 위치를 다시 불러오면 10초에 한 번씩 카운트를 가져오는 현재의 로직 상 무조건 useEffect내의 로직이 발동되게 됩니다. </p>
<p>따라서, 주문이 추가되지 않아도 페이지에 첫 진입하거나, 경로로 재진입시 무조건 벨이 울리는 조건이 되어버리는 것이죠.</p>
<p>따라서, useRef의 값을 undefined로 설정해 soundRef의 값을 카운트가 업데이트되고 난 후에 할당되도록 로직을 구성했습니다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/542f1079-1ade-485e-a470-be8aa16420e9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/22752140-6991-49c9-ba88-85199e7ad3ac/image.png" alt=""></p>
<p>첫 렌더링 시 ref의 current 값을 의도적으로 undefined로 설정한 결과, 무조건 벨이 울리게 되는 현상을 방지하게 된 것이죠.</p>
<hr>
<h3 id="👹-구현결과">👹 구현결과</h3>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/7fc9b6b1-4fa1-47f8-9a71-aa78f1c94f71/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/bc838882-75fd-458b-a67b-b85efe4ef060/image.gif" alt=""></p>
<p>네 주문이 접수 된 후 아주 잘 소리 알람이 작동하고 있습니다. </p>
<hr>
<h2 id="📝-후기">📝 후기</h2>
<p>개인적으로는 이번 기능을 구현하면서, 함수의 실행 컨텍스트에 관해 몸으로 깨달을 수 있었던 것 같습니다. 
위 글에서는 아주 간단하고 명료하게 기능을 구현한 것 같지만, 
주시하고 있던 상태(주문 카운트)의 업데이트 시점을 제대로 적용하기 위한 많은 삽질이 있었거든요. </p>
<p>React로 프로그래밍한 코드는 반드시 함수의 실행 시점의 환경, 실행 컨텍스트와 Lexical Enviroment 등 실행을 위한 요소들을 수집해서 실행에 참고합니다. 따라서, 우리 머리속에서는 동기적으로 착착 잘 진행될 것 같은 코드도 경우에 따라서는 제대로 실행이 되지 않을 수도 있습니다. </p>
<p>다시 한 번 기초의 중요성을 깨닫게 되는 사례였습니다. </p>
<p>이 글이 누군가에게 반드시 도움이 되길 바라며, 이만 글을 맺습니다. 긴 글 읽어주셔서 감사합니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) 리액트 프로덕트에서 POS 프린트 기능 구현하기]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-POS-%ED%94%84%EB%A6%B0%ED%8C%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/React-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-POS-%ED%94%84%EB%A6%B0%ED%8C%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 27 Dec 2022 13:51:44 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/be91445a-6cf4-476f-88c6-aa4577e54484/image.png" alt=""></p>
<hr>
<p>회사에서 진행하는 커머스 프로젝트는 &quot;고도몰&quot;로 갈음처리되었지만, 
운 좋게도(?) 벤더용 어드민 기능은 어찌저찌 살아남아 벤더어드민 기능을 덧붙이는 작업을 12월 동안 진행하고 있습니다. </p>
<p>이번 콘텐츠는 가장 최근 올린 새로운 기능 중 <strong>&quot;POS 프린터 기능&quot;</strong>에 대한 글입니다. </p>
<p>이번 기능을 위해 오픈 소스 라이브러리 생태계에 기여해주신 &quot;나석주&quot;개발자님께 먼저 감사의 인사를 전합니다. (<a href="https://github.com/seokju-na/react-thermal-printer">깃헙 라이브러리 링크</a>]</p>
<hr>
<h2 id="🧲-들어가기에-앞서">🧲 들어가기에 앞서</h2>
<p>일반적인 프린터와는 다르게, 카드 영수증 출력이나 POS기의 주문을 출력하는 프린터는 &quot;써멀 프린터&quot;라고 부릅니다. </p>
<p>용지는 얇고 폭이 좁아서 일반적인 프린터의 그것과는 많이 다른데다, 윈도우7 혹은 심지어 윈도우 XP 환경에서 구동해야 하기 때문에 블루투스로 프린팅 기능을 지원하는 요즘과는 다르게 대부분 시리얼 포트 규격을 채택하고 있습니다. </p>
<p><em>(물론 USB 2.0을 지원하긴 합니다만,가상 포트 드라이버를 설치해서 사용합니다.)</em></p>
<p>때문에, &quot;프린팅 기능&quot;을 주문받았을때 가장 먼저 확인한 것은 POS 구동환경과 프린터 기기의 종류였습니다.</p>
<p>아니나 다를까, 제품을 사용할 협력사는 윈도우 7과 구형 써멀 프린터를 사용하고 있었습니다 😇</p>
<h2 id="🖨-리액트에서-프린트-기능을-구현하려면">🖨 리액트에서 프린트 기능을 구현하려면</h2>
<p>리액트에서 프린트 기능을 구현할 수 있는 대표적인 라이브러리가 있습니다 </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/9b406318-ba68-4dcc-afd5-3a802090442c/image.png" alt=""></p>
<p>바로 React to Print 라이브러리입니다. 
<a href="https://www.npmjs.com/package/react-to-print">링크</a></p>
<pre><code class="language-js">import React, { useRef } from &#39;react&#39;;
import { useReactToPrint } from &#39;react-to-print&#39;;

import { ComponentToPrint } from &#39;./ComponentToPrint&#39;;

const Example = () =&gt; {
  const componentRef = useRef();
  const handlePrint = useReactToPrint({
    content: () =&gt; componentRef.current,
  });

  return (
    &lt;div&gt;
      &lt;ComponentToPrint ref={componentRef} /&gt;
      &lt;button onClick={handlePrint}&gt;Print this out!&lt;/button&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p><code>react to print</code>라이브러리에서 소개하는 예시를 살펴보니
<code>useRef</code>를 이용해서 ref의 current 객체에 프린트하고자 하는 HTML 돔요소를 업데이트 시켜, 출력함수의 <code>content</code>키의 벨류로 전달하고 있습니다.</p>
<p>직관적이고 사용이 편하다는 생각이 듭니다. 하지만, 위 라이브러리를 사용해서  프린터 기능을 구현하더라도 시리얼 포트를 사용하는 써멀 프린터의 작동방식과는 사못 달라 적용하기 어렵다는 생각이 들었습니다. </p>
<p>바쁜 주방에서 하나하나 프린터 세팅을 다뤄가면서 출력을 할 수는 없을테니깐요 😪</p>
<h2 id="🧐-프린트-기능-팝업-호출없이-바로-출력을-할-수-없을까">🧐 프린트 기능 팝업 호출없이 바로 출력을 할 수 없을까?</h2>
<p>위 질문을 해결하기 위해 
다시 열심히 구글에 자료를 서칭한 결과, 엄청난 자료를 찾고야 말았습니다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/f3c1ccf4-ad3d-4dff-a311-d3c9adfd5bdf/image.png" alt=""></p>
<p>JSConf2022 행사에서 요기요 개발자 팀의 나석주 님께서 발표하신 세션을 찾은 것입니다. </p>
<p>위 콘텐츠 덕분에 훨씬 더 자세하게 써멀프린터의 구동환경을 알게 되었습니다. </p>
<p>더불어, 위 세션에서 발표하신 결과물을 npm에 배포해주신 덕분에 제가 필요로한 라이브러리를 바로 적용해볼 수 있었습니다. </p>
<h2 id="react-themal-print-라이브러리-적용하기">React themal Print 라이브러리 적용하기</h2>
<p><a href="https://www.npmjs.com/package/react-thermal-printer">링크</a></p>
<p>해당 라이브러리에 나와있는 깃헙 주소로 들어가면 README.md에 상세하게 라이브러리 사용법이 나와있습니다. 하여, 하단의 코드에서는 제가 라이브러리를 어떻게 적용했는지 소개하려합니다 </p>
<h3 id="1️⃣-영수증-출력을-담당할-컴포넌트-생성">1️⃣. 영수증 출력을 담당할 컴포넌트 생성</h3>
<p>먼저 주방에 전달될 주문지에 담을 정보를 나타내는 UserReceipt.jsx  파일을 먼저 생성했습니다.</p>
<pre><code class="language-js">// src/components/UserReceipt

import { Br, Cut, Line, Printer, Text, Row } from &quot;react-thermal-printer&quot;;
import moment from &quot;moment&quot;;
import { FlexBox } from &quot;src/components/flex-box&quot;;
import { Typography } from &quot;@mui/material&quot;;

const UserReceipt = (props) =&gt; {
  const { orderinfo } = props;

  if (!orderinfo) return &lt;&gt;&lt;/&gt;;

  return (
    &lt;Printer type=&quot;epson&quot; width={42} characterSet=&quot;korea&quot;&gt;
      &lt;FlexBox sx={{ alignItems: &quot;center&quot;, justifyContent: &quot;center&quot; }}&gt;
        &lt;Text&gt;주방주문서&lt;/Text&gt;
      &lt;/FlexBox&gt;
      &lt;Text size={{ width: 2, height: 2 }}&gt;
        [주문시간] {moment(orderinfo?.paymentDt).format(&quot;YYYY-MM-DD HH:mm&quot;)}
      &lt;/Text&gt;
      &lt;Text&gt;[오더번호] {String(orderinfo[0]).slice(-3)}&lt;/Text&gt;
      &lt;Line /&gt;
      &lt;Row left=&quot;메뉴명&quot; right=&quot;수량(구분)&quot; /&gt;
      &lt;Line /&gt;
      &lt;Br /&gt;
      &lt;FlexBox sx={{ flexDirection: &quot;column&quot; }}&gt;
        {orderItem &amp;&amp;
          orderItem?.map((item, index) =&gt; {
              return (
                &lt;&gt;
                  &lt;Row left={item.goodsNm} right={`${item.goodsCnt} (신규)`} /&gt;
                  {item.optionInfo !== null &amp;&amp; (
                    &lt;Row
                      left={`ㄴ ${item.optionInfo}`}
                      right={`${item.goodsCnt} (속성)`}
                    /&gt;
                  )}
                &lt;/&gt;
              );
          })}
        &lt;Br /&gt;
      &lt;/FlexBox&gt;
      &lt;Line /&gt;
      &lt;Text&gt;매장컵&lt;/Text&gt;
      &lt;Line /&gt;
      &lt;Br /&gt;
      &lt;Line /&gt;
      &lt;Cut /&gt;
    &lt;/Printer&gt;
  );
};

export default UserReceipt;</code></pre>
<p>코드 상단에서 임포트하고 있는 라이브러리의 <code>Line</code> <code>Br</code> <code>Row</code> <code>Printer</code> <code>Text</code> <code>Cut</code>등 기본 컴포넌트만 사용해서 구현해도 훌륭하게 영수증을 출력할 수 있습니다. </p>
<p>각 컴포넌트들은 시멘틱하게 네이밍 되어 있기 때문에 뜻 그대로의 기능을 제공합니다. </p>
<p>저 중 가장 중요한 것은 <code>Printer</code> 컴포넌트인데, 일반적인 Container 컴포넌트의 기능을 제공하는 것같지만, 프린터 기능에 관련된 props를 전달하는 아주 중요한 기능을 합니다. </p>
<h3 id="2️⃣-프린터-기능을-담당하는-함수에-jsx-엘리먼트-전달하기">2️⃣. 프린터 기능을 담당하는 함수에 JSX 엘리먼트 전달하기</h3>
<pre><code class="language-js">// src/components/VendorOrderList
import { render } from &quot;react-thermal-printer&quot;;
import UserReceipt from &quot;./UserReciept&quot;;

const VendorOrderListItem = (props) =&gt; {

      const orderInfo = props.orderInfo;

    const onClickPrintHandler = async () =&gt; {
      const data = await render(UserReceipt({ orderinfo }));
      const port = await window.navigator.serial.requestPort();
      await port.open({ baudRate: 9600 });
      const writer = port.writable?.getWriter();
      if (writer !== null) {
        await writer.write(data);
        await writer.releaseLock();
      }
      await port.close({ baudRate: 9600 });
  };

  return (
   &lt;Container&gt;
    ...
    &lt;Button
      onClick={async () =&gt; {
        await onClickPrintHandler();
        await toggleAlertDialog(&quot;smartorder-preparing&quot;);
      }}
     &gt; 접수하기 &lt;/Button&gt;
    ...
   &lt;/Container&gt;
 );
};</code></pre>
<p>버튼을 클릭하면 <strong>주문접수</strong>와 함께 주방에 있는 써멀프린터로 영수증이 전달될 수 있도록 onClick 함수를 구현해보았습니다. </p>
<p>라이브러리의 README.md의 예제에는 영수증 양식을 변수에 직접 할당해 <code>render</code>함수의 인자로 전달하고 있었지만, 관심사 분리를 위해 컴포넌트로 분리해 작업했습니다. </p>
<p>JSX 컴포넌트 또한 일종의 함수로 리턴 값으로 HTML Element를 리턴하기 때문에 함수의 형식으로 render 함수의 인자로 전달해주었습니다. </p>
<p><code>const data = await render(UserReceipt({orderInfo})</code></p>
<h3 id="3️⃣-시리얼-포트에-연결된-프린트로-작업-요청하기">3️⃣. 시리얼 포트에 연결된 프린트로 작업 요청하기</h3>
<p><code>window.navigator.serial.requestPort()</code>문을 통해 실제로는 usb에 연결되어 있지만, 드라이버를 통해 가상 포트에 할당되어 있는 프린터에 접근할 수 있습니다. (전 COM3 포트로 연결을 했습니다.)</p>
<p>함수의 흐름은 라이브러리가 제공한 가이드 대로 진행했지만,
한편으로는 주방 환경에서 영수증을 재 출력하거나 포트가 계속 열려있어 다른 출력요청과 충돌할 수 있다고 생각했습니다. </p>
<pre><code class="language-js">const onClickPrintHandler = async () =&gt; {
      const data = await render(UserReceipt({ orderinfo }));
      const port = await window.navigator.serial.requestPort();
      await port.open({ baudRate: 9600 });
      const writer = port.writable?.getWriter();
      if (writer !== null) {
        await writer.write(data);
        await writer.releaseLock();
      }
  // 작업을 마쳤다면 다시 포트를 닫습니다. 이 한줄로 충돌을 방지
      await port.close({ baudRate: 9600 });
  };</code></pre>
<p>하여, 하단에 한줄을 더 추가해 출력이 마치면 기존에 열어놓았던 포트 요청을 다시 닫도록 했습니다. </p>
<p><code>await port.close({ baudRate: 9600 })</code></p>
<hr>
<h2 id="🧾-출력-결과물">🧾 출력 결과물</h2>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/8a43c378-612a-4083-a90c-d2334045422a/image.jpg" alt=""></p>
<p>라이브러리를 발견한 순간부터 해당 기능을 구현하기까지 2시간 남짓 걸려서 테스트까지 모두 마칠 수 있었습니다. </p>
<p>웹에서 프린트 기능을 구현할 수 있냐는 리드 개발자 님의 우려를 빠르게 해치울 수 있어서 굉장히 통쾌한 기억으로 남을 것 같네요.</p>
<hr>
<h2 id="👹-오픈소스-라이브러리에-기여하자">👹 오픈소스 라이브러리에 기여하자</h2>
<p>이 블로그에 매번 콘텐츠를 작성하는 이유는 오픈 소스 라이브러리에 기여하기 위함입니다. 이번에는 현명하신 개발자 &quot;나석주&quot;님의 도움을 받아 업무를 일사천리로 해결할 수 있었습니다. </p>
<p>제가 도움을 받았듯이, 제가 발행하는 콘텐츠들이 다른 개발자 분들께 유용하게 다가갔으면 좋겠다는 마음을 다시 한 번 갖게 되었습니다. </p>
<hr>
<h2 id="이어지는-다음-포스팅">이어지는 다음 포스팅</h2>
<p><a href="https://velog.io/@yunsungyang-omc/React-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-Nicepay-%ED%8F%AC%EC%8A%A4%EA%B8%B0%EC%97%90%EC%84%9C-%ED%94%84%EB%A6%B0%ED%8A%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">(React) 트러블 슈팅 : 카드리더기 프린터를 이용하는 환경에서 오토커팅 구현하기</a></p>
<h2 id="라이브러리-링크-모음">라이브러리 링크 모음</h2>
<ul>
<li>React to Print : <a href="https://www.npmjs.com/package/react-to-print">링크</a></li>
<li>React Thermal Printer : <a href="https://www.npmjs.com/package/react-thermal-printer">링크</a></li>
<li>나석주님이 발표하신 JSConf 2022 세션 영상 보기 : <a href="https://www.youtube.com/watch?v=FFolxFrUgoQ">링크</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) 리액트 프로덕트에서 NICEPAY 결제 모듈 연동하기]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8%EC%97%90%EC%84%9C-Nicepay-%EA%B2%B0%EC%A0%9C-%EB%AA%A8%EB%93%88-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/React-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8%EC%97%90%EC%84%9C-Nicepay-%EA%B2%B0%EC%A0%9C-%EB%AA%A8%EB%93%88-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 20 Dec 2022 07:24:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/7e4c6034-dc71-474e-a465-2c6f01594763/image.png" alt=""></p>
<hr>
<p>3번째 커머스 프로젝트 &quot;푸드버스 스마트오더 어플리케이션&quot;을 개발했을때 겪은 우여곡절에 대한 글이다.</p>
<p>프로젝트 내내 결제 모듈 연동은 가장 중요한 기능이었기 때문에, 시니어 개발자께서 담당하셨다. 
하지만, 시니어께서 다른 프로젝트로 끌려간 기간동안, 
QA 단계에서 발생한 여러 에러와 성능이슈 때문에 코드 파악 및 기능 개선이 시급했졌다.
결국, 시니어를 기다리기 보다는 스스로 기능 개선을 하며 전체적인 리팩토링을 진행하게 되었다. </p>
<h2 id="nicepay-결제모둘-연동하기">NICEPAY 결제모둘 연동하기</h2>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/0411bf62-a16c-4608-9b3c-77cf300ce2cc/image.png" alt=""></p>
<p><a href="https://developers.nicepay.co.kr/manual-auth.php">공식메뉴얼</a></p>
<p>요즘 커머스 프로덕트를 개발할때는 아엠포트나 토스 페이먼츠, NHN 결제 시스템 등 조금 더 개발하기 수월한 환경을 제공하는 결제 모듈을 사용하려는 추세지만, 전통의 강자 &quot;나이스페이 결제 모듈&quot;은 저렴한 수수료와 레거시 개발 환경에 익숙한 인터페이스를 제공하고 있어 현재에도 폭넓게 사용되고 있다.</p>
<p>다만, 진행하고 있는 프로젝트는 <code>React</code> <code>Next.js</code> 기반으로 이루어져 있다보니, <code>JSP</code>예시 결제 모듈을 그대로 따르기엔 무리가 있었다. 하여, 하단부터는 초심자가 결제모듈을 연동하는데 참조할 수 있도록 차근 차근 단계를 밟아가려 한다.</p>
<h3 id="1️⃣-nicepay-스크립트-코드-import-하기">1️⃣ NICEPAY 스크립트 코드 import 하기</h3>
<p>결제창을 호출하기 위해서는 부모 컴포넌트(Next.js에서는 주로 이 작업을 Page 단에서 처리한다.)의 <code>&lt;Script /&gt;</code> 태그에 자바스크립트 코드를 임포트 해주어야 한다. </p>
<pre><code class="language-js">// src/pages/checkout/index.tsx

const Checkout = () =&gt; {
  ...
  return (
    &lt;CheckoutLayout&gt;
        &lt;Script
            src=&quot;https://web.nicepay.co.kr/v3/webstd/js/nicepay-3.0.js&quot;
            type=&quot;text/javascript&quot;
           /&gt;
        &lt;CheckoutForm /&gt;
    &lt;/CheckoutLayout&gt; 
  )
};</code></pre>
<p>이렇게 스크립트 코드를 임포트하게 되면 이제 Nicepay에서 제공하는 각종 함수(<code>goPay</code>, <code>niceSubmit</code>)를 사용할 수 있게 된다. </p>
<h3 id="2️⃣-gopay함수에-전달할-파라미터를-저장할-폼-엘리먼트-작성하기">2️⃣ <code>goPay()</code>함수에 전달할 파라미터를 저장할 폼 엘리먼트 작성하기</h3>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/d2471af3-7c18-40ac-9d7f-a4cd9fb9a6b8/image.png" alt=""></p>
<p>클라이언트에서 <code>goPay()</code>함수를 호출해 결제창 팝업을 호출하게 될때, <code>goPay()</code>함수의 파라미터로 결제에 필요한 정보를 form 데이터 형식에 담아 전달하게 된다. 하여 다음 단계에서는 <code>&lt;CheckoutForm /&gt;</code>컴포넌트 최하단에 정보를 담고 전달할 form 엘리먼트를 작성해줄 것이다.</p>
<pre><code class="language-js">// src/components/checkout/CheckoutForm.tsx
import React, { useState, useRef } from &#39;react&#39;;

const CheckoutForm = () =&gt; {
  const { form, setForm } = useState({}); 
  const formRef = useRef();
  ... 
  return (
    &lt;Container&gt;
      ...
      &lt;form
        name=&quot;payForm&quot;
        method=&quot;post&quot;
        action={ReturnURL}
        ref={formRef}
        acceptCharset=&quot;euc-kr&quot;
      &gt;
        &lt;input type=&quot;hidden&quot; name=&quot;GoodsName&quot; value={form.GoodsName} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;Amt&quot; value={form.Amt} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;MID&quot; value={form.MID} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;EdiDate&quot; value={form.EdiDate} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;Moid&quot; value={form.Moid} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;PayMethod&quot; value={form.PayMethod} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;MerchantKey&quot; value={form.MerchantKey} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;SignData&quot; value={form.SignData} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;BuyerName&quot; value={form.BuyerName} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;BuyerTel&quot; value={form.BuyerTel} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;BuyerEmail&quot; value=&quot;&quot; /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;ReturnURL&quot; value={form.ReturnURL} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;FailURL&quot; value={form.failURL} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;GoodsCl&quot; value={form.GoodsCl} /&gt;
      &lt;/form&gt;
    &lt;/Container&gt;
  )
}</code></pre>
<p>이제 우리는 useState를 통해 결제 api의 response로 받아온 값을 정형해 그 값을 useRef의 current값에 전달함으로써, 폼 데이터를 저장할 것이다. useRef를 이용해서 htmlElement를 조작하면, 업데이트 할 객체의 값을 자동으로 찾아 업데이트 해주기 때문에 상당히 유용하다. </p>
<p>[[React] ref로 HTML 엘리먼트에 접근/제어하기] (<a href="https://www.daleseo.com/react-refs/">https://www.daleseo.com/react-refs/</a>)</p>
<p><code>&lt;input /&gt;</code>엘리먼트의 각 값에 대한 정의는 아래와 같다. 이중 하나도 누락되지 않도록 전달되도록 주의해야 한다. 
<img src="https://velog.velcdn.com/images/yunsungyang-omc/post/80c33386-7887-4621-bfdc-5ddd12666dde/image.png" alt=""></p>
<h3 id="3️⃣-결제-성공-시에-실행할-콜백-함수-정의하기">3️⃣ 결제 성공 시에 실행할 콜백 함수 정의하기</h3>
<p>나이스 페이 결제 모듈을 불러오는데 성공하고, 결제 실패 유무에 따라 페이지를 라우팅하거나 Alert 메시지를 띄워줘야 할 경우를 고려해서 각각 상태에 대응하는 콜백함수를 정의 후 이를 전달할 수 있다.</p>
<pre><code class="language-js">//src/util/index.ts
export const convertFormToObj = (form) =&gt; {
    const obj = {};
    Object.keys(form).map((k) =&gt; {
      if (!parseInt(k)) return;
      const { name, value } = form[k];
      obj[name] = value;
    });
    return obj;
  };</code></pre>
<pre><code class="language-js">// src/components/checkout/CheckoutForm.tsx
import React, { useState, useEffect, useRef } from &#39;react&#39;;
import { useRouter } from &quot;next/router&quot;;
import { convertFormToObj } from &quot;src/util&quot;;

const CheckoutForm = () =&gt; {
    const { form, setForm } = useState({}); 
    const formRef = useRef();
    const router = useRouter();

    // 결제 후 성공 callback
    const nicepaySubmit = async () =&gt; {
      setAlertType(&quot;success&quot;);
      setAlertMsg(&quot;결제에 성공했습니다.&quot;);
      sendPaymentResult(true);
    };
    // 결제 후 실패 callback
    const nicepayClose = async () =&gt; {
      // TODO: 결제 실패시 처리
      setAlertType(&quot;error&quot;);
      setAlertMsg(&quot;결제를 다시 시도해주세요&quot;);
      sendPaymentResult(false);
    };
    const sendPaymentResult = async () =&gt; {
      const body = convertFormToObj(formRef.current);
      body.success = success;
      ...
      if (success) {
        window.deleteLayer();
        router.push(&quot;/payment/complete&quot;)
      }
    };

    useEffect(() =&gt; {
      if (Object.keys(form).length === 0) return;
      if (isMobile) {
        // 모바일 결제창 진입
        formRef.current.action = &quot;https://web.nicepay.co.kr/v3/v3Payment.jsp&quot;;
        formRef.current.acceptCharset = &quot;euc-kr&quot;;
        formRef.current.submit();
      } else {
          // PC 결제창 진입
        if (typeof window !== &quot;undefined&quot;) {
          window.nicepaySubmit = nicepaySubmit;
          window.nicepayClose = nicepayClose;
          window.goPay(formRef.current);
          }
      }
    }, [form]);

    return (
      &lt;Container&gt;
        ...
        &lt;form
          name=&quot;payForm&quot;
          method=&quot;post&quot;
          action={ReturnURL}
          ref={formRef}
          acceptCharset=&quot;euc-kr&quot;
        &gt;
          &lt;input type=&quot;hidden&quot; name=&quot;GoodsName&quot; value={form.GoodsName} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;Amt&quot; value={form.Amt} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;MID&quot; value={form.MID} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;EdiDate&quot; value={form.EdiDate} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;Moid&quot; value={form.Moid} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;PayMethod&quot; value={form.PayMethod} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;MerchantKey&quot; value={form.MerchantKey} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;SignData&quot; value={form.SignData} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;BuyerName&quot; value={form.BuyerName} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;BuyerTel&quot; value={form.BuyerTel} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;BuyerEmail&quot; value=&quot;&quot; /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;ReturnURL&quot; value={form.ReturnURL} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;FailURL&quot; value={form.failURL} /&gt;
          &lt;input type=&quot;hidden&quot; name=&quot;GoodsCl&quot; value={form.GoodsCl} /&gt;
        &lt;/form&gt;
      &lt;/Container&gt;
    );
};</code></pre>
<p>NICEPAY 결제모듈은 모바일과 PC 결제창 두 가지 타입으로 호출하는 방식을 구분되어 있다. 때문에 상태관리되고 있는 form 객체의 정보가 업데이트 될때 <code>useEffect</code>로 사이드 이펙트 제어를 분기처리해서 진행할 수 있다. </p>
<p>모바일 결제창 모듈은 <code>formRef</code>의 <code>current</code>객체를 참조하는 반면, PC 결제창 모듈은 window의 document 객체에 성공 콜백과 실패 콜백을 체이닝하고, <code>goPay()</code>함수를 호출하는 방식으로 구동한다.  </p>
<p>유틸 함수를 통해서 formRef 객체의 키 값중 success 값에 따라 transaction을 업데이트 하는 등의 추가 api 요청과 라우팅 등은 <code>sendPaymentResult()</code>함수에서 실행하도록 로직을 분리했다. </p>
<h3 id="4️⃣-결제-api의-response를-form-데이터로-정형해서-전달하기setform">4️⃣ 결제 api의 response를 form 데이터로 정형해서 전달하기(setForm)</h3>
<pre><code class="language-js">// src/hooks/useCheckout.ts
import React, { useState, useEffect, useRef } from &#39;react&#39;;
import { useMutation } from &quot;react-query&quot;;
import { useAtom } from &quot;jotai&quot;;
import { CartStateAtom, CartAllDataAtom } from &quot;src/atoms/Atoms&quot;;

const useCheckout = () =&gt; {
  const [state, setState] = useAtom(CartStateAtom);
  ...
  const checkoutForMe = () =&gt; {
    const mutation = useMutation(() =&gt; {
      // 결제 요청 후 200 status 를 반환받으면 state의 cart 값을 업데이트 시킨다.
      const res = await cartApi.checkoutForMe();
      return res.data;
    });
    return mutation
  };
  return {
    state,
    setState,
    checkoutForMe
  };
};</code></pre>
<pre><code class="language-js">// src/components/checkout/CheckoutForm.tsx
import useCheckout from &quot;src/hooks/useCheckout&quot;;

const CheckoutForm = () =&gt; {
    const { state, setstate, checkoutForMe } = useCheckout();
    const { mutate, isLoading, isSuccess, success } = checkoutForMe();

      const handleCheckout = () =&gt; {
      ...
      // 결제 관련 valiation 로직을 수행한 후 주문을 생성하는 api로 요청한다. 
      createOrder();
    }

    const createOrder = () =&gt; {
      try {
        mutate();
        // 결제 요청에 성공하면, 결제 유무를 판단하는 atom State의 불리언 값을 변경시킨다.
        setState((state) =&gt; ({
         ...state,
         isCheckout, !state.isCheckout;
        }));
      } catch (err) {
        console.log(err);
      }
    };

   const createForm = (order) =&gt; {
        const EdiDate = moment().format(&quot;YYYYMMDDHHMMSS&quot;);
        const MID = process.env.NEXT_PUBLIC_MID;
        const Moid = order._id;
        const TotalAmount = order.total.amount;
        const TotalTaxFree = order.totalTaxFree;
        const TotalTaxedAmount = TotalAmount - TotalTaxFree;
        const TotalSupplyAmt = Math.ceil(TotalTaxedAmount / 1.1);
        const TotalVAT = TotalTaxedAmount - TotalSupplyAmt;
        const Amt = TotalAmount + ShippingFee;
        const MerchantKey = process.env.NEXT_PUBLIC_MERCHANT_KEY;
        const TestData = EdiDate + MID + Amt + MerchantKey;
        const SignData = cryptoJs.SHA256(TestData).toString();
        const successUrl = encodeURIComponent(
          `${window.location.origin}/payment/complete?isMobile=${isMobile}`
        );
        const failUrl = encodeURIComponent(
          `${window.location.origin}/payment/complete?isMobile=${isMobile}`
        );
        const returnUrl = isMobile ? process.env.NEXT_PUBLIC_RETUN_Mobile_URL : process.env.NEXT_PUBLIC_RETURN_URL
        setReturnURL(returnUrl);
          // 파라미터로 전달된 값에 따라 전달할 정보를 담아 form 상태를 업데이트 시킨다.
        setForm({
        // 상품이름
        GoodsName,
        // 결제 금액
        Amt,
        // 상점 아이디
        MID,
        // 전문생성일시
        EdiDate,
        // 상점 주문번호 상점 고유 식별값
        Moid,
        MerchantKey,
        SignData,
        BuyerName: order.address.billing.name.full,
        BuyerTel: order.address.billing.name.phone,
        BuyerEmail: &quot;&quot;,
        // 아래 값들중 하나 [CARD, VBANK, SSG_BANK, GIFT_CULT]
        PayMethod: &quot;CARD&quot;,
        ReturnURL: returnUrl,
        failURL: failUrl,
        // 휴대폰 소액결제 추가 요청 파라미터 (0 = 컨텐츠, 1 = 현물)
        GoodsCl: 1,
        SupplyAmt: TotalSupplyAmt,
        GoodsVat: TotalVAT,
        TaxFreeAmt: TotalTaxFree,
      });
   }

  useEffect(() =&gt; {
      // 결제 성공과 mutation의 상태를 판단하는 지표를 사용해서 폼 데이터를 업데이트 하는 로직을 실행한다.
    if (state.order &amp;&amp; !isLoading &amp;&amp; state.isCheckout) {
      createForm(state.order)
    }
  }, [state.order]);

  return (
    &lt;Container&gt;
        &lt;ButtonWrap&gt;
            &lt;Button onClick={() =&gt; handleCheckout()} /&gt; 결제하기 &lt;/Button&gt;
        &lt;/ButtonWrap&gt;
        &lt;form&gt;
          ...
          &lt;/form&gt;
      &lt;/Container&gt;  
  );
};</code></pre>
<p>결제하기 버튼을 클릭해서 온클릭 함수<code>handleCheckout()</code> 를 실행하면 다음과 같은 로직이 실행될 것이다.</p>
<pre><code>1. 결제를 위한 validate 로직이 실행된다.
2. validate 완료 후 mutate 함수와 setState 함수를 실행시킨다. 
3. mutate 함수 결과에 따라 useEffect로 사이드를 제어해, createForm 함수를 실행시킨다.
4. createForm 함수의 결과로 상태 관리되는 form 객체의 값이 채워진다. 
5. useEffect의 디펜던시로 form을 넣었두었기 때문에 아래 로직이 실행되며, 결제 모듈을 불러온다.</code></pre><pre><code class="language-js">// src/components/checkout/CheckoutForm.tsx 

...
    useEffect(() =&gt; {
      if (Object.keys(form).length === 0) return;
      if (isMobile) {
        // 모바일 결제창 진입
        formRef.current.action = &quot;https://web.nicepay.co.kr/v3/v3Payment.jsp&quot;;
        formRef.current.acceptCharset = &quot;euc-kr&quot;;
        formRef.current.submit();
      } else {
          // PC 결제창 진입
        if (typeof window !== &quot;undefined&quot;) {
          window.nicepaySubmit = nicepaySubmit;
          window.nicepayClose = nicepayClose;
          window.goPay(formRef.current);
          }
      }
    }, [form]);
...</code></pre>
<hr>
<h2 id="🧐-정리하며">🧐 정리하며</h2>
<p>이해를 쉽게 하기 위해, 함수를 분리하지 않고 최대한 절차 중심으로 코드를 구성하려고 노력해보았다. 만약 여기서 더 리팩토링할 기회가 있다면, form 상태 또한 jotai의 useAtom을 이용해서 관리할 것이고, 따라서 <code>createForm</code> 함수 등 컴포넌트 뷰 단에 함께 존재하지 않아도 될 함수들은 관심사 분리를 해서 파일을 분리할 것 같다. </p>
<p>이 글을 통해 nicePay 결제 모듈을 연동하는 누군가는 반드시 그렇게 해보길 바란다. 
아쉽게도 이 프로젝트는 여기서 끝을 맺어, 수정이 어렵게 되었지만 조금 더 나은 방안이 떠오른다면 언제든지 댓글로 의견을 주면 정말 감사할 것 같다. </p>
<p>더불어, 누군가에게 이 글이 꼭 도움이 되길 바란다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/6fa5838a-a206-4af3-b7c0-082806630065/image.png" alt=""></p>
<hr>
<h3 id="전체-코드">전체 코드</h3>
<pre><code class="language-js">// src/hooks/useCheckout.ts
import React, { useState, useEffect, useRef } from &#39;react&#39;;
import { useMutation } from &quot;react-query&quot;;
import { useAtom } from &quot;jotai&quot;;
import { CartStateAtom, CartAllDataAtom } from &quot;src/atoms/Atoms&quot;;

const useCheckout = () =&gt; {
  const [state, setState] = useAtom(CartStateAtom);
  ...
  const checkoutForMe = () =&gt; {
    const mutation = useMutation(() =&gt; {
      // 결제 요청 후 200 status 를 반환받으면 state의 cart 값을 업데이트 시킨다.
      const res = await cartApi.checkoutForMe();
      return res.data;
    });
    return mutation
  };
  return {
    state,
    setState,
    checkoutForMe
  };
};</code></pre>
<pre><code class="language-js">//src/util/index.ts
export const convertFormToObj = (form) =&gt; {
    const obj = {};
    Object.keys(form).map((k) =&gt; {
      if (!parseInt(k)) return;
      const { name, value } = form[k];
      obj[name] = value;
    });
    return obj;
  };</code></pre>
<pre><code class="language-js">// src/pages/checkout/index.tsx
const Checkout = () =&gt; {
  ...
  return (
    &lt;CheckoutLayout&gt;
        &lt;Script
            src=&quot;https://web.nicepay.co.kr/v3/webstd/js/nicepay-3.0.js&quot;
            type=&quot;text/javascript&quot;
           /&gt;
        &lt;CheckoutForm /&gt;
    &lt;/CheckoutLayout&gt; 
  );
};</code></pre>
<pre><code class="language-js">// src/components/checkout/CheckoutForm.tsx
import React, { useState, useRef } from &#39;react&#39;;
import { useRouter } from &quot;next/router&quot;;
import useCheckout from &quot;src/hooks/useCheckout&quot;;
import { convertFormToObj } from &quot;src/util&quot;;

const CheckoutForm = () =&gt; {
    const { form, setForm } = useState({}); 
    const formRef = useRef();
    const router = useRouter();
    const { state, setstate, checkoutForMe } = useCheckout();

    // 결제 후 성공 callback
    const nicepaySubmit = async () =&gt; {
      setAlertType(&quot;success&quot;);
      setAlertMsg(&quot;결제에 성공했습니다.&quot;);
      sendPaymentResult(true);
    };

    // 결제 후 실패 callback
    const nicepayClose = async () =&gt; {
      // TODO: 결제 실패시 처리
      setAlertType(&quot;error&quot;);
      setAlertMsg(&quot;결제를 다시 시도해주세요&quot;);
      sendPaymentResult(false);
    };

    const sendPaymentResult = async () =&gt; {
      const body = convertFormToObj(formRef.current);
      body.success = success;
      ...
      if (success) {
        window.deleteLayer();
        router.push(&quot;/payment/complete&quot;)
      }
    };

      const handleCheckout = () =&gt; {
      ...
      // 결제 관련 valiation 로직을 수행한 후 주문을 생성하는 api로 요청한다. 
      createOrder();
    }

    const createOrder = () =&gt; {
      try {
        mutate();
        // 결제 요청에 성공하면, 결제 유무를 판단하는 atom State의 불리언 값을 변경시킨다.
        setState((state) =&gt; ({
         ...state,
         isCheckout, !state.isCheckout;
        }));
      } catch (err) {
        console.log(err);
      }
    };

    const createForm = (order) =&gt; {
      const EdiDate = moment().format(&quot;YYYYMMDDHHMMSS&quot;);
      const MID = process.env.NEXT_PUBLIC_MID;
      const Moid = order._id;
      const TotalAmount = order.total.amount;
      const TotalTaxFree = order.totalTaxFree;
      const TotalTaxedAmount = TotalAmount - TotalTaxFree;
      const TotalSupplyAmt = Math.ceil(TotalTaxedAmount / 1.1);
      const TotalVAT = TotalTaxedAmount - TotalSupplyAmt;
      const Amt = TotalAmount + ShippingFee;
      const MerchantKey = process.env.NEXT_PUBLIC_MERCHANT_KEY;
      const TestData = EdiDate + MID + Amt + MerchantKey;
      const SignData = cryptoJs.SHA256(TestData).toString();
      const successUrl = encodeURIComponent(
        `${window.location.origin}/payment/complete?isMobile=${isMobile}`
      );
      const failUrl = encodeURIComponent(
        `${window.location.origin}/payment/complete?isMobile=${isMobile}`
      );
      const returnUrl = isMobile ? process.env.NEXT_PUBLIC_RETUN_Mobile_URL : process.env.NEXT_PUBLIC_RETURN_URL
      setReturnURL(returnUrl);
      // 파라미터로 전달된 값에 따라 전달할 정보를 담아 form 상태를 업데이트 시킨다.
      setForm({
        // 상품이름
        GoodsName,
        // 결제 금액
        Amt,
        // 상점 아이디
        MID,
        // 전문생성일시
        EdiDate,
        // 상점 주문번호 상점 고유 식별값
        Moid,
        MerchantKey,
        SignData,
        BuyerName: order.address.billing.name.full,
        BuyerTel: order.address.billing.name.phone,
        BuyerEmail: &quot;&quot;,
        // 아래 값들중 하나 [CARD, VBANK, SSG_BANK, GIFT_CULT]
        PayMethod: &quot;CARD&quot;,
        ReturnURL: returnUrl,
        failURL: failUrl,
        // 휴대폰 소액결제 추가 요청 파라미터 (0 = 컨텐츠, 1 = 현물)
        GoodsCl: 1,
        SupplyAmt: TotalSupplyAmt,
        GoodsVat: TotalVAT,
        TaxFreeAmt: TotalTaxFree,
      });
   }

    useEffect(() =&gt; {
      // 결제 성공과 mutation의 상태를 판단하는 지표를 사용해서 폼 데이터를 업데이트 하는 로직을 실행한다.
      if (state.order &amp;&amp; !isLoading &amp;&amp; state.isCheckout) {
        createForm(state.order)
      }
    }, [state.order]);

    useEffect(() =&gt; {
      // 폼이 빈 객체일때는 실행하지 않는다.
      if (Object.keys(form).length === 0) return;
      if (isMobile) {
        // 모바일 결제창 진입
        formRef.current.action = &quot;https://web.nicepay.co.kr/v3/v3Payment.jsp&quot;;
        formRef.current.acceptCharset = &quot;euc-kr&quot;;
        formRef.current.submit();
      } else {
          // PC 결제창 진입
        if (typeof window !== &quot;undefined&quot;) {
          window.nicepaySubmit = nicepaySubmit;
          window.nicepayClose = nicepayClose;
          window.goPay(formRef.current);
          }
      }
    }, [form]);

  ... 
  return (
    &lt;Container&gt;
      ...
      &lt;ButtonWrap&gt;
         &lt;Button onClick={() =&gt; handleCheckout()} /&gt; 결제하기 &lt;/Button&gt;
      &lt;/ButtonWrap&gt;
      ...
      &lt;form
        name=&quot;payForm&quot;
        method=&quot;post&quot;
        action={ReturnURL}
        ref={formRef}
        acceptCharset=&quot;euc-kr&quot;
      &gt;
        &lt;input type=&quot;hidden&quot; name=&quot;GoodsName&quot; value={form.GoodsName} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;Amt&quot; value={form.Amt} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;MID&quot; value={form.MID} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;EdiDate&quot; value={form.EdiDate} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;Moid&quot; value={form.Moid} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;PayMethod&quot; value={form.PayMethod} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;MerchantKey&quot; value={form.MerchantKey} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;SignData&quot; value={form.SignData} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;BuyerName&quot; value={form.BuyerName} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;BuyerTel&quot; value={form.BuyerTel} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;BuyerEmail&quot; value=&quot;&quot; /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;ReturnURL&quot; value={form.ReturnURL} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;FailURL&quot; value={form.failURL} /&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;GoodsCl&quot; value={form.GoodsCl} /&gt;
      &lt;/form&gt;
    &lt;/Container&gt;
  )
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[(Zustand) 간단하게 Zustand 적용해보기]]></title>
            <link>https://velog.io/@yunsungyang-omc/Zustand-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-Zustand-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/Zustand-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-Zustand-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 19 Dec 2022 07:20:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/19ded547-cbae-4e84-9357-10ec3dc07633/image.png" alt=""></p>
<hr>
<p>이직을 준비하는 과정에서 새로운 상태관리를 적용해볼 필요가 있어, zustand를 사용했다. 
이전 진행했던 프로젝트에서 상태관리 라이브러리 Jotai를 사용해본 경험이 있어서 그리 어렵지 않게 사용할 수 있었다. 
(놀랍게도, 두 라이브러리 이름 모두 &quot;상태&quot; 이다. Zustand 제작에 Jotai 제작팀도 참여했다고 한다.)</p>
<hr>
<h2 id="간단하게-zustand-적용해보기">간단하게 Zustand 적용해보기</h2>
<p>Zustand 스토어를 만들때는<code>create</code>함수를 이용해서 상태와 상태를 변경하는 액션 함수를 정의한다. 이렇게 정의된 상태와 액션은 훅으로 리턴해서 사용할 수 있다. </p>
<pre><code class="language-ts">// src/store/useStore.ts
import create &quot;zustand&quot;
import { CartType } from &quot;src/types&quot;

export const useStore = create&lt;CartType&gt;(
    (set) =&gt; {
      // initial한 상태를 정의한다.
      cart: [],
    },
    // 상태를 변경하는 액션을 정의한다.
    addItem : (item) =&gt; ({
      set((state) =&gt; ({
        ....state.cart,
        cart: item,
      }));
    }),
      removeItem: (id) =&gt; {
        set((state) =&gt; ({
        cart: state.cart.filter((item) =&gt; item.id !== id),
      }));
    },  
  })
);</code></pre>
<p>먼저 create 함수를 통해서, initail한 상태를 정의 후에 이어 현재 상태를 인자로 넘겨주는 형식으로 다음 상태를 정의하는 액션을 정의할 수 있다.</p>
<p>스토어의 상태와 액션은  <code>Subscribe</code>함수를 사용해서 상태의 모든 변화 혹은 일부를 구독할 수 있지만, 나는 React 컴포넌트에서 직접 사용할 수 있도록 훅으로 만들었다.</p>
<p>위 상태와 액션은 컴포넌트 단에서 아래와 같이 사용할 수 있다. </p>
<pre><code class="language-ts">// src/components/cart.tsx

import React from &#39;react&#39;
import {useStore} from &#39;src/store/useStore&#39;

const CartItems = () =&gt; {
  const { cart, addItem, removeItem } = useStore();

  ...

  return(
    &lt;div&gt;
        {
          cart &amp;&amp; cart.map((item, index) =&gt; {
             return (
              &lt;&gt;
                &lt;ItemCard item={item} id={item.id}/&gt;
                &lt;div&gt;
                 &lt;button onClick={() =&gt; addItem()}&gt;담기 &lt;/button&gt;
                 &lt;button onClick={() =&gt; removeItem()}&gt;빼기&lt;/button&gt;
               &lt;/div&gt; 
             &lt;/&gt;
          )});
         }
    &lt;/div&gt;
  )    
}</code></pre>
<p>아주 간단하게 상태를 참조해서 카트의 아이템과 카트 아이템을 추가하거나 제거할 수 있는 컴포넌트를 렌더링해보았다. 
위처럼 Zustand는 상태와 액션을 훅의 형태로 이용할 수 있기 때문에 Redux의 그것과는 달리 보일러플레이트 없이 간결하게 상태를 이용하거나 변경할 수 있다.</p>
<hr>
<h3 id="zustand-persist">Zustand Persist</h3>
<p>전역상태는 페이지가 refresh 될때 초기화된다. 하여, 상태를 기억하게 하기 위해서 갖가지 방법이 동원되는데 Zustand에서는 <code>persist</code> 미들웨어를 지원해주고 있어 상태를 쉽게 보존할 수 있다. </p>
<pre><code class="language-ts">// src/store/useStore.ts
import create &quot;zustand&quot;
import { CartType } from &quot;src/types&quot;
import { persist } from &#39;zustand/middleware&#39;;
import { StateCreator } from &#39;zustand&#39;;
import { PersistOptions } from &#39;zustand/middleware&#39;;

type PersisType = (
  config: StateCreator&lt;CartType&gt;,
  options: PersistOptions&lt;CartType&gt;,
) =&gt; StateCreator&lt;CartType&gt;;


export const useStore = create&lt;CartType&gt;(
  //상태와 액션을 persist 미들웨어로 감싸줍시다.
 (persis as PersisType)(
    (set) =&gt; {
      // initial한 상태를 정의한다.
      cart: [],
    },
    // 상태를 변경하는 액션을 정의한다.
    addItem : (item) =&gt; ({
      set((state) =&gt; ({
        ....state.cart,
        cart: item,
      }));
    }),
      removeItem: (id) =&gt; {
        set((state) =&gt; ({
        cart: state.cart.filter((item) =&gt; item.id !== id),
      }));
    },  
  }),
  // persist 미들웨어로 Storage에 저장할 상태 명을 정의한다.
  {name :&#39;user_CartStore&#39;},
  ),
);</code></pre>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/67fbd9b8-4926-452c-a306-5b79b59e4959/image.png" alt=""></p>
<p>페이지를 리프레쉬했을때 이렇게 로컬스토리지에 정의한 키 네임으로 상태들이 값으로 잘 저장되고 있는 것을 확인할  수 있다. </p>
<h3 id="🧲--one-more-thing--nextjs에서-persist-값-사용하기">🧲  one more thing : Next.js에서 persist 값 사용하기</h3>
<p>Next.js에서 해당 값이 저장된 것을 확인할 수 있겠지만, 상태 그대로를 컴포넌트에 참고해서 사용할 수 없다.
아마 페이지를 리프레쉬하면,</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/b260e1f8-2148-49d6-ab0a-e63eeab06d19/image.png" alt=""></p>
<p>다음과 같은 에러메시지를 만나게 될 것이다.
Next.js는 서버사이드에서 페이지를 프리랜더링하게 되는데, 로컬스토리지에 있는 값은 클라이언트 사이드에서만 접근가능하다. 하여 undefined한 값들을 참조하게 되기 때문이다. </p>
<p>이를 해결하기 위해서 방법들을 찾던 중에 마침 비슷한 문제로 골머리를 앓던 분의 <a href="https://kbwplace.tistory.com/152">포스트</a>를 찾게 되어, 문제를 해결할 수 있었다. </p>
<pre><code class="language-ts">// src/hooks/useCart.ts
import { useEffect, useState } from &#39;react&#39;;
import { useStore } from &#39;src/store/useStore&#39;;
import { ProductInfoType } from &#39;src/type/index&#39;;

const useCart = () =&gt; {
  const [userCart, setUserCart] = useState&lt;ProductInfoType[]&gt;([]);
  const { cart } = useCartStore();

  useEffect(() =&gt; {
    const loacalCartData = localStorage.getItem(&#39;user_CartStore&#39;);
    if (loacalCartData !== null) {
      const restoreCartData = JSON.parse(loacalCartData);
      if (restoreCartData.state.cart.length !== 0) {
        setUserCart(restoreCartData.state.cart);
      }
    }
  }, []);
  // 해당 훅을 통해 카트 상태에 변경이 있었을 때 반영할 수 있따.
  useEffect(() =&gt; {
    setUserCart(cart);
  }, [cart]);

  return { userCart };
};

export default useCart;</code></pre>
<p>원 글의 저자도 밝혔듯이 명확하게 문제를 해결한 것은 아니지만, 로컬스토리지에 저장된 값을 반영해서 업데이트한 상태를 참고할 수 있어 최선의 방법인 것 같다. </p>
<hr>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/226ee986-500c-4dfd-9503-07525b60c551/image.png" alt=""></p>
<h3 id="🧐-과연-redux의-미래는">🧐 과연 Redux의 미래는..?</h3>
<p>여전히 많은 프로젝트에서 리덕스와 리덕스 툴킷이 사용되고 있고, 그에 따라 비동기를 제거하기 위한 <code>Redux thunk</code>나 <code>Redux saga</code>패턴을 적용해서 액션 함수를 정의하고 있다. </p>
<p>물론, 위의 방법대로 프로젝트를 구성한다고 해서 문제가 있는 것은 아니다. 다만, jotai, recoil, Zustand같은 차세대 상태관리 라이브러리의 출현과 React-Query라는 강력한 서버 상태 관리 라이브러리가 자리잡은 지금 굳이 Redux를 고집할 필요는 없다고 생각한다. </p>
<p>코드란 결국 수많은 라인을 한 데 뭉쳐 하나의 프로덕트를 구성하는 요소이고, 이 요소는 코드를 작성한 작성자 뿐만 아니라 공동작업자들도 직관적으로 확인할 수 있어야 한다. 그런 점에서 코드의 흐름이 간결할 뿐만 아니라, 보일러 플레이트를 최소화 할 수 있는 Zustand 등 차세대 라이브러리 삼총사를 필히 경험해보라 추천하고 싶다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(회고) 입사부터 현재까지]]></title>
            <link>https://velog.io/@yunsungyang-omc/%ED%9A%8C%EA%B3%A0-%EC%9E%85%EC%82%AC%EB%B6%80%ED%84%B0-%ED%98%84%EC%9E%AC%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@yunsungyang-omc/%ED%9A%8C%EA%B3%A0-%EC%9E%85%EC%82%AC%EB%B6%80%ED%84%B0-%ED%98%84%EC%9E%AC%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 06 Dec 2022 17:21:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/f6ce88a1-bae2-424b-9d01-7284faee9b41/image.png" alt=""></p>
<hr>
<h2 id="입사한-이후로-벌써-8개월이-지났습니다-😇">입사한 이후로 벌써 8개월이 지났습니다. 😇</h2>
<p>블로그 회고 마지막 글이 취업을 알리는 글이었는데, 벌써 8개월이 지났다. 입사하고 나서 나름 한 두 달 사이에는 블로그에 글도 작성했는데.. 본격적으로 제품 개발에 들어가고 나서는 <strong>정신적인 여유</strong>가 나지 않아 블로그 콘텐츠 작성에 소홀해질 수 밖에 없었다. </p>
<p>연봉 재협상 시즌인 만큼 현재까지 진행했던 성과를 바탕으로 회사와 이야기를 나눌 기회가 생겨 그간 진행했던 프로젝트를 모두 정리(<a href="https://hodoopapa.oopy.io/72ff1df6-4819-4c1b-a1fd-eb74bacf39ab">포트폴리오 링크</a>)하고, 기억이 잊혀지기 전에 회고를 남기게 되었다.</p>
<h3 id="4월--온보딩-및-nextjs-탐구하기-💻">4월 : 온보딩 및 Next.js 탐구하기 💻</h3>
<p>첫 입사 후 팀에서 진행되는 다양한 프로젝트에 대해서 현황 및 적용 스택 등에 대해서 안내를 받을 수 있었다.
작년 하반기에 시리즈 A를 막 유치한 회사답게 굉장히 다양한 프로젝트가 진행 중에 있었는데, 이중 <strong>커머스 프로젝트</strong>에 투입되었다. </p>
<p>단일 React 기반으로 제작이 거의 완료된 커머스 프로덕트가 어떤 이유로 엎어지고 처음부터 다시 시작되게 되었다. 기획이 준비되는 동안 손만 빨고 있을 수 없었기 때문에, react 앱을 <code>Next.js</code>로 마이그레이션을 해보라는 첫 업무를 받게 되었다. </p>
<p>(CSR 앱을 단순히 서버사이드 랜더링 가능하게 바꾸는 라이브러리 정도로 생각했었는데, 마이그레이션이 그리 호락호락 하지 않았던 것 같다. <a href="https://velog.io/@yunsungyang-omc/Next.js-%EC%97%90%EB%9F%AC%EB%A1%9C%EA%B7%B8-window-is-not-defined">관련 포스트 링크</a>)</p>
<h3 id="5월---8월--본격적으로-커머스-프로젝트-밈즈샵-개발-👨🏼💻">5월 - 8월 : 본격적으로 커머스 프로젝트, 밈즈샵 개발 👨🏼‍💻</h3>
<p>5월에 들어서자마자 본격적으로 커머스 프로젝트가 가동되었는데, 기존 앱의 한계로 지적되었던 SEO 최적화 부분을 개선해달라는 사업팀의 요구를 받아 서버사이드 랜더링을 구현가능하도록 도와주는 프레임워크 <code>Next.js</code>를 적용하기로 했다. </p>
<p>더불어, 패드 기반 서비스를 제공하는 회사이기 때문에, 패드와 모바일 기기에서 모두 반응형 UI가 원활하게 있게 해달라는 요청도 있어 Css 라이브러리로 <code>Material UI</code>를 사용하게 되었다.</p>
<p>상태관리로는 시니어 개발자 분들이 관심있게 학습하셨던 <code>Jotai</code>를 도입하기로 했고, 꽤 뿌듯하게도 서버 상태 관리를 위해 기존에 주로 사용하던 RTK가 아닌 <code>React Query</code>를 적용하자는 내 의견이 받아들여졌다. 🙇🏽‍♂️</p>
<p>엎고 새로 시작한 프로젝트이니 만큼 <strong>빠르게 개발이 완료</strong>되도록 일정이 굉장히 타이트했다. 
레이아웃을 잡을 시간도 없어서 MUI에서 유료로 제공하는 커머스 템플릿을 사용하기로 했고, 서버 팀의 Api도  막연하게 기다릴 수 있는 상황이 안돼 <code>Clayful의 헤드리스 커머스 api</code>를 사용해 기능을 구현하기로 했다.</p>
<p><em>*<em>(임시 방편이었지만, 결국 서버팀은 커머스 프로젝트에서 제외되었고 헤드리스 커머스 형태로 최종 진행된다.)
*</em></em></p>
<p>내가 맡은 부분은 처음에는 아주 간단한 <strong>&quot;마이페이지&quot;를 구현</strong>하는 것으로 시작해 <strong>취소 / 환불 기능을 전담</strong>하게 되었다.  UI가 당초 약속했던 것과는 다르게 기획팀에서 아주 다양하게 변주를 주었기 때문에 <strong>컴포넌트 재사용</strong>을 고려할 틈새도 없이 페이지를 찍어냈어야 했다는 점을 제외하고는 꽤 수월하게 개발을 진행할 수 있었다. </p>
<p><strong>Clayful</strong> 사에게는 유감이지만, 개발자 문서에서 제공하는 범위가 제한적이라 이메일 문의를 해가면서 많은 부분을 해결해야 했다. 8월 통째로 매달렸던 <strong>부분 환불 / 부분 취소</strong> 기능은 구현에 성공했지만, PO께서 결국 스펙아웃 처리를 해버려서 전부 들어내야 했다. 😤 많은 아쉬움이 남았다.</p>
<p>그래도 3개월 남짓한 기간동안, 헤드리스 커머스 형태의 프로덕트가 만들어질 수 있었다. </p>
<p><a href="https://meemzshop.com/">밈즈샵 링크</a></p>
<h3 id="9월---fb-협력사의-샵-제작하기-🎂">9월  : F&amp;B 협력사의 샵 제작하기 🎂</h3>
<p>밈즈샵의 런칭 이후 회사에서는 커머스 프로젝트를 조금 더 확장시키길 원했는데 메타버스 플랫폼 안에 <strong>푸드버스</strong>라는 환경을 만들고, 이 안에 많은 F&amp;B 브랜드를 입점시켜 진정 커머스 플랫폼으로 성장해나가길 원하는 것 같았다. </p>
<p>첫 번째 외주 프로젝트로 협력사 &quot;폴콘&quot;의 쇼핑몰 사이트를 구축해주기로 했다. 기존 밈즈샵 코드를 재활용하기로 했고, UI 변경도 거의 이루어지지 않아 큰 공수 없이 사이트를 런칭할 수 있었다.</p>
<h3 id="10월---11월----푸드버스-스마트오더-앱-제작하기-📲">10월 - 11월   : 푸드버스 스마트오더 앱 제작하기 📲</h3>
<p>브랜드 폴콘의 쇼핑몰 서비스 &quot;폴콘샵&quot;의 런칭 이후 
전국에 위치한 폴콘 카페에서 사용할 수 있는 스마트오더 애플리케이션을 제작해달라는 요청을 받게 되었다. 
(이만하면 SI 아닐까..)</p>
<p>애플리케이션을 제작해야 하는 부담스러운 업무가 웹팀에게 주어졌지만, 
밈즈샵의 코드를 이용해 제작을 진행하기로 해, 첫 삽은 큰 부담없이 수월하게 진행할 수 있었다.</p>
<p>밈즈샵 제작에는 총 6명의 개발자가 함께 협업을 진행했는데 이 서비스를 제작하는데는 겨우 3명, 
나중에는 벤더 측** 어드민 페이지를 담당하는 시니어 한 분과 유저 페이지를 담당하는 나 둘로만 진행**하게 되어 QA 이슈 처리 및 컴포넌트 고도화에 꽤 큰 고생을 하게 되었다. </p>
<p>밈즈샵 제작에는 주로 &quot;마이페이지&quot;, &quot;취소 / 환불&quot; 기능에 집중했지만, 이 시기에는** &quot;본인 인증&quot;, &quot;SNS 회원 로그인 연동&quot;, &quot;결제&quot; 등 커머스 프로젝트의 핵심 비즈니스 로직 대부분에 관여**를 하게 되서 개발 경험을 꽤 많이 축적할 수 있는 시간이 되었다. </p>
<p>다만, 정말 아쉽게도 회사에서 <strong>모종의 이유</strong>를 이유로 더 이상 <strong>clayful</strong>기반의 커머스 프로덕트를 신뢰할 수 없다고 해서, <em><strong>이 프로덕트는 완성은 했으나 런칭까지는 하지 못한 비운의 프로젝트가 되고 말았다.</strong></em> 🥲</p>
<p><a href="https://smartorder.vollkorn.land/vendor/6V63R7436SDY">스마트오더 웹 링크</a></p>
<hr>
<h2 id="✍🏼-어쩌다-보니-커머스-도메인의-한-사이클을-경험">✍🏼 어쩌다 보니 커머스 도메인의 한 사이클을 경험</h2>
<p>정리하고 보니 8개월이 넘는 기간동안 커머스 도메인의 비즈니스 로직 한 사이클을 모두 경험하게 되었다. 
짧은 기간동안 제품을 런칭하는 짜릿한 경험을 맛보기도 했지만,
그것보다 더 짧은 시간에 피땀눈물을 쏟아 제작한 애플리케이션을 엎어야 했던 불운을 경험하기도 했다. </p>
<p>그래도 실력있는 QA, PM, 함께 페어를 맞춘 시니어 개발자 분들 덕분에 
커머스 프로덕트에 대해서 참 많은 고민을 함께 나누고 적용해본 것 같다. </p>
<p>비록 경력적으로는 1년 미만의 주니어 프론트엔드 개발자지만, 
최신 스택 적용에 앞장 서서 가이드를 제시해 시니어 분들에게 작게나마 도움이 된 것도 꽤 인상적인 경험이었다. </p>
<h3 id="그렇다면-이제-어디로-next-step">그렇다면 이제 어디로? Next Step</h3>
<p>아쉽게 엎어진 스마트오더 애플리케이션은 스스로도 틈틈히 시간을 내서 기능을 더 붙여보려고 한다.
웹 팀에게 주어진 시간이 너무 제한적이었고, 앱 자체 기능은 클라이언트 팀의 힘을 빌어 Unity로 구현을 했다. 
하지만, 생각보다 구동이 매끄럽지 못하다는 생각이 들어서 앱 기능 모두 React Native로 전환해서 구현하고 싶다.
퀵 뷰 및 빠른 구매 기능도 적용해보지 못해 아쉬웠는데, 이 부분도 적용해봐야겠다.</p>
<h3 id="강력한-이직동기-형성">강력한 이직동기 형성</h3>
<p>풀스택 서비스 개발 → 헤드리스 커머스 개발 → <code>🧲고도몰 웹 빌더 템플릿 사용</code></p>
<p>결국, 커머스 프로젝트는 위의 단계를 밟아 기존 개발 인력의 리소스를 사용하는 대신 <strong>고도몰</strong> 이라는 쇼핑몰 호스팅 도메인의 템플릿을 구매해 수정 제작하는 것으로 결론이 났다. </p>
<p>고도몰 코드에 모던 스택이 적용되면 좋았을텐데, 서버는 php기반이고 프론트는 <code>jQuery</code>가 사용되고 있어서  경력적으로 웹팀 모두에게 굉장히 우려스러운 상황이다. </p>
<p>(템플릿을 사용하기 때문에, github을 이용할 수도 없고..고도몰 어드민 내 ftp 편집기를 통해 수정을 해야한다. 😇 환장하겠네...)</p>
<p>고모돌 전환 결정은 개발자인 내게 큰 좌절로 다가오고 있고, 현재에도 멘탈을 회복하기 위해 부정적인 생각을 긍정적으로 전환하기 위해 노력하고 있다. 🧘🏿‍♂️</p>
<p><strong>낮은 성장 가능성</strong>이라는 <strong>큰 이직동기</strong>가 생겼기 때문에 채용 플랫폼의 내 상태를 &quot;구직 중&quot;으로 돌려놓고 이력서를 업데이트하고 있다. </p>
<h2 id="아마도-다음-회고를-작성할때는-새로-이직한-직장에-관해-작성을-하게-되지-않을까-👹">아마도, 다음 회고를 작성할때는 새로 이직한 직장에 관해 작성을 하게 되지 않을까..? 👹</h2>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[(React) 커머스 프로젝트 : 결제 버튼 중복 클릭 방지하기]]></title>
            <link>https://velog.io/@yunsungyang-omc/React-%EC%BB%A4%EB%A8%B8%EC%8A%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B2%B0%EC%A0%9C-%EB%B2%84%ED%8A%BC-%EC%A4%91%EB%B3%B5-%ED%81%B4%EB%A6%AD-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/React-%EC%BB%A4%EB%A8%B8%EC%8A%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B2%B0%EC%A0%9C-%EB%B2%84%ED%8A%BC-%EC%A4%91%EB%B3%B5-%ED%81%B4%EB%A6%AD-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 21 Nov 2022 15:05:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/685da20c-645b-4fc6-8114-9510e15eb7ae/image.png" alt=""></p>
<hr>
<h2 id="유저의-이상-행동을-예측하기란">유저의 이상 행동을 예측하기란</h2>
<p>버그를 유발하는 유저의 &quot;이상행동&quot;을 예측하면서 코드를 작성하기란 여간 쉬운 일이 아니다. 
프로덕트를 제작하기 위한 일정도 빠듯하거니와 &quot;설마 이런 행동을 하겠어?&quot;라는 의심과 확신으로 
예외 처리를 고려하지 않게되는 것이 일반적이기 때문이다.  </p>
<p>1) &quot;결제하기&quot;버튼을 연타한다거나, 
2) PG 창을 띄워놓은 다음 뒤로 가기 버튼을 누른다거나 등의 이상 행동을 하는 유저는 그리 많지 않을 것이다. </p>
<p>하지만, 우리의 프로덕트는 작은 에러조차 허용해서는 안될 것이며, 특히나 &quot;결제&quot; 관련 이슈는 커머스 프로젝트에서 중대한 이슈로 받아들여지기 때문에 반드시 처리해야할 문제였다. </p>
<h2 id="🖱-결제하기-버튼-중복클릭-막기">🖱 결제하기 버튼 중복클릭 막기</h2>
<p>작성되어 있는 코드의 로직은 다음과 같았다. </p>
<pre><code>1) 유저가 &quot;결제하기&quot; 버튼을 누른다.
2) 버튼이 눌리면 onClick 이벤트로 상태 값에 저장된 정보를 바탕으로 checkout api로 요청을 보낸다. 
3) api의 res가 전달되면 data 값을 바탕으로 PG에 전달할 데이터 form을 만드는 함수가 실행된다.
4) form이 만들어지면, 이어서 PG로 form데이터를 전달하면서 PG창을 호출한다.</code></pre><p>흐름으로 보아 문제가 없는 로직같지만, &quot;결제하기&quot;부터 &quot;PG창 호출까지&quot;의 1-2초 간격동안 유저가 버튼을 적게는 두 번부터 많게는 5-6번 정도 클릭할 수 있는 것이 문제였다. (당연히 클릭한 만큼, api 요청이 이루어진 것도 코미디 😪)</p>
<p>이 문제를 해결하기 위해 자료를 서칭해보니, 몇 가지 해결책이 존재했다.</p>
<h3 id="첫-번째-시도--buttonref의-current를-이용해서-버튼을-disabled-처리하기">첫 번째 시도 : ButtonRef의 current를 이용해서 버튼을 disabled 처리하기</h3>
<p>ref의 current 객체는 리렌더링이 되더라도 상태가 초기화되지 않는다는 점을 이용해 버튼 상태를 조작할 수 있다.
버튼을 한 번 클릭한 이후로는 더 이상 클릭되지 않도록 current 객체에 diabled 값을 true로 지정해 처리를 해보았다.</p>
<pre><code class="language-js">  const buttonRef = useRef();
  // onClick 이벤트로 실행될 함수
  const handleCheckout = async () =&gt; {
    buttonRef.current.disabled = true;
    ...
  }

  return (
    &lt;&gt;
       &lt;Button
         ref={buttonRef}
            onClick={handleCheckout}     
        &gt;
   &lt;/&gt;
    ...
  )   
}</code></pre>
<p>👺 하지만, 이렇게 처리를 했을 경우에는 문제가 몇 가지 발생했는데..</p>
<p>만일 유저가 &quot;결제하기&quot;를 의도치 않고 <strong>실수로 버튼을 클릭</strong>했다거나, 결제에 필요한 <strong>데이터를 모두 입력하지 않고 &quot;결제하기&quot;버튼을 클릭했을 때 경고 메시지를 띄우는 등의 로직이 존재할 경우</strong>, 경고 메시지가 한 번 렌더링 된 이후에는 버튼이 정상적으로 작동하지 않는다.  </p>
<p>결국, 유저가 버튼을 &quot;1번만 클릭하게 만드는 것&quot;에는 성공했지만, 
유저의 편의성과 예측성 모두를 잃었기 때문에 결코 훌륭한 선택이었다고 할 수 없다. 다시 문제는 원점으로.. </p>
<h3 id="두-번째-시도--데이터-패칭-라이브러리의-상태값-이용하기">두 번째 시도 : 데이터 패칭 라이브러리의 상태값 이용하기</h3>
<p>올 해 들어 서버 상태 관리 라이브러리인 <strong>React Query</strong>를 아주 잘 이용하고 있다. GET 요청에는 useQuery나 useQueries를, POST나 UPDATE 등의 요청에는 useMutation를 이용해 코드를 직관적으로 작성할 수 있게 되었는데, 제공하는 옵션이 아주 유용해서 다양한 상황에서 요령껏 잘 이용하고 있다. </p>
<p>나는 이번 이슈에 <strong>데이터를 패칭하는 중임을 나타내는 상태값 isLoading</strong>을 이용해 버튼 대신 프로그레스 바를 렌더링한 다음, PG창이 요청되는 찰나의 시간동안에도 유저가 버튼을 더이상 클릭할 수 없도록 <strong>패칭 성공을 나타내는 isSuccess</strong> 또한 이용할 것이다.</p>
<pre><code class="language-js">import { useMutation } from &quot;react-query&quot;;

// 결제관련 hook에 사용될 함수
const checkoutForMe = () =&gt; {
    const mutation = useMutation(async () =&gt; {
      try {
        const res = await cartApi.checkoutForMe({
          type: &quot;order&quot;,
          data: payload,
        });
        setState((state) =&gt; ({
          ...state,
          order: res.data.order,
        }));
        return res.data;
      } catch (err) {
        console.log(&quot;checkout err :&gt;&gt; &quot;, err)
      }
    });
  return mutation;
};

...

  // 결제 관련 페이지

  const { data, mutate, isLoading, error, isSuccess } = checkoutForMe();

  // api로 요청을 보낼 함수
  const createOrder = async () =&gt; {
    try {
      mutate();
    } catch (err) {
      console.log(&quot;checkout err :&gt;&gt; &quot;, err);
    }
  };

  // onClick 이벤트 함수
  const handleCheckout = () =&gt; {
    if (여러가지 예외처리 상황) {
      ...
    }
    createOrder();    
  }

  // api 조회 결과 200 코드를 받으면, 로딩 스피너를 보여주고 다음 폼으로 이동한다.
  if (isLoading) {
    return (
      &lt;FlexBox&gt;
          &lt;CircularProgress /&gt;
      &lt;/FlexBox&gt;
      );
  }

  return (
    ...
    &lt;Button
      onClick={handleCheckout}
      disabled={isSuccess}
    &gt;
    결제 하기
   &lt;/Button&gt;
  )
}</code></pre>
<p>위 코드처럼 처리한 결과 결제하기 버튼을 클릭하면 다음과 같이 작동한다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/c864b902-be99-4b93-bfa6-11804aa1c12d/image.gif" alt=""></p>
<p>클릭과 동시에 데이터 패칭 요청이 이뤄져서, loading 상태를 나타내는 프로그레스바가 렌더링되었다가, 패칭이 이뤄지면 결제하기 버튼을 disabled처리하기 때문에 유저는 더 이상 더블 클릭할 수 없게 되는 것이다!</p>
<p>이 외에도 이벤트 자체에 flag 변수를 두어 api 요청을 막는 등의 방법도 존재하지만, 시도 결과 loading 상태를 이용하는 것이 유저의 이상 행동을 방지하면서도 ui도 깔끔하게 처리해줘 가장 성공적인 결과물을 보여주었다.</p>
<hr>
<h2 id="개발은-qa-이후부터">개발은 QA 이후부터</h2>
<p>최근 팀의 프로젝트로 한창동안 진행해온 프로덕트의 릴리즈를 코 앞에 두고 있다. 꽤 오랜 기간동안 개발했지만, 모종의 이유로 헐거운 기획서를 바탕으로 프로젝트를 진행해야 했고, 당연하게도 QA 단계에서 발견되는 갖가지 에러로 버그 처리에 혼신을 다하고 있다. </p>
<p>유저의 이상 행동을 예측하고 테스트를 진행해주시는 QA 여러분들 덕분에 경험치가 팍팍 쌓여간다는 점에서 운이 좋다고 해야할지 😂</p>
<p>내일 혹은 모레 쯤 <strong>&quot;PG 창을 호출하고 뒤로 가기 버튼을 누르는 케이스&quot;</strong>를 처리했던 경험을 작성해봐야겠다. 
이 글이 나와 동일한 문제를 겪고 있는 누군가에게 큰 힘이 되었으면 하는 바라며 이만 글을 마친다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(Next.js) Google Analytics 이식하기]]></title>
            <link>https://velog.io/@yunsungyang-omc/Next.js-Google-Analystics-%EC%9D%B4%EC%8B%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/Next.js-Google-Analystics-%EC%9D%B4%EC%8B%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 23 May 2022 08:50:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/941b4a90-dae2-44ca-ba0b-7c3d902afa91/image.png" alt=""></p>
<hr>
<h2 id="들어서며">들어서며</h2>
<p>Google Analystic 등 웹(앱) 서비스를 이용하는 유저의 행동을 분석하는 마케팅 툴을 팀 프로젝트에 이식하게 되었다. 이중 내가 당담했던 툴들을 Google Analytics와 Google Adsence, 네이버 프리미엄 로그 분석이었다. </p>
<p>이 중 Google의 마케팅 툴을 이식하는 방법에 대해 포스팅을 진행하려 한다. </p>
<hr>
<blockquote>
<h3 id="google-analytics">Google Analytics</h3>
<p>구글 에널리스틱스는 웹(앱)에 대한 방문자의 데이터를 수집해서 분석함으로써 온라인 비즈니스의 성과를 측정하고 개선하는데 사용하는 로그분석 툴이다. </p>
</blockquote>
<h2 id="nextjs-프로젝트에-google-analytics-이식하기">Next.js 프로젝트에 Google Analytics 이식하기</h2>
<p>React 프로젝트에서 GA를 이식할때 많이들 <code>react-ga</code>라이브러리를 사용하고는 한다. 그러나, GA가 업그레이드 되면서 이전 GA의 베이스가 되었던 유니버셜 에널리스틱스 속성을 사용하려면 GA 계정에서 별도 세팅을 해주어야 한다. 이에 따라 GA 추적 코드 형식도 <code>UA-XXXXXXXX-X</code>에서 <code>G-XXXXXXXXXX</code>로 변경되었다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/2aa33231-3073-4979-b99b-362214553bd0/image.png" alt=""></p>
<p>유럽권에서 개인정보를 활용한 마케팅에 대해 규제를 진행하고 있어 향후 구글은 기존의 방식을 버리고 새로운 방식으로 마케팅 지원을 할 수 있도록 방향을 전환하고 있다. 따라서, 변화에 대응할 수 있도록 보수적으로 이식을 진행하고자 라이브러리 없이 GA를 이식하는 방향으로 작업을 진행했다. </p>
<h3 id="step-1-env-파일에-ga-추적-코드-복사하기">Step 1. <code>.env</code> 파일에 GA 추적 코드 복사하기</h3>
<p>.env.local</p>
<pre><code>NEXT_PUBLIC_GOOGLE_ANALYTICS=&quot;code&quot;</code></pre><h3 id="step-2-_appjs-파일에-ga-추적-코드-추가하기">Step 2. <code>_app.js</code> 파일에 GA 추적 코드 추가하기</h3>
<p>Next.js에서 <head> 태그나 <body> 태그 안에 들어갈 내용들을 커스텀할 때는 주로 <code>_document.js</code>파일을 사용한다. 아마 대부분 포스팅에서도 <head> 엘리먼트에 GA 관련 코드를 이식하기 위해  <code>_document.js</code>를 커스텀 하고 있을 것이다. </p>
<p>하지만, <a href="https://github.com/vercel/next.js/tree/canary/examples/with-google-analytics">vercel/Next.js</a>의 샘플 코드를 확인하면 <code>_app.js</code>에서 커스텀을 진행하고 있다.</p>
<p>직접 프로젝트에서 <code>_document.js</code>파일에 이식을 진행해보면 린트에서 다음과 같이 경고하는 것을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/8b27e2e4-18d2-46a3-aeb8-029f4bca6727/image.png" alt=""></p>
<p>아마 이것은 Next 11이 릴리즈 되고나서  추가된 next/Script 때문인것으로 보이는데, Next.js 공식 홈페이지는 이에 관해서 이렇게 설명하고 있다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/8dec75e3-43db-4f35-8727-6d103db1d764/image.png" alt=""></p>
<p>Next.js는 SSR(ServerSideRendering) 구현하고 있는 프레임웍이고 서버단에서 실행되는 코드에서 window객체를 참고하는 코드를 실행하면 <code>window is not defined</code>란 에러를 띄운다. </p>
<p><a href="https://velog.io/@yunsungyang-omc/Next.js-%EC%97%90%EB%9F%AC%EB%A1%9C%EA%B7%B8-window-is-not-defined">관련 포스팅 보기</a></p>
<pre><code class="language-js"> // ga 관련 태그  
window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag(&#39;js&#39;, new Date());
  gtag(&#39;config&#39;, &#39;${gtag.GA_TRACKING_ID}&#39;, {
    page_path: window.location.pathname,
  });</code></pre>
<p>따라서, 윈도우를 참조하고 있는 코드가 클라이언트 단에서 안정적으로 실행될 수 있도록 개선된 것으로 보인다. </p>
<h3 id="_appjs와-nextscript"><code>_app.js</code>와 next/Script</h3>
<p>vercel에서 제안하는 샘플 코드는 아래와 같다. </p>
<pre><code class="language-js">import { useEffect } from &#39;react&#39;
import Script from &#39;next/script&#39;
import { useRouter } from &#39;next/router&#39;
import * as gtag from &#39;../lib/gtag&#39;

const App = ({ Component, pageProps }) =&gt; {
  const router = useRouter()

  return (
    &lt;&gt;
      {/* Global Site Tag (gtag.js) - Google Analytics */}
      &lt;Script
        strategy=&quot;afterInteractive&quot;
        src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
      /&gt;
      &lt;Script
        id=&quot;gtag-init&quot;
        strategy=&quot;afterInteractive&quot;
        dangerouslySetInnerHTML={{
          __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag(&#39;js&#39;, new Date());
            gtag(&#39;config&#39;, &#39;${gtag.GA_TRACKING_ID}&#39;, {
              page_path: window.location.pathname,
            });
          `,
        }}
      /&gt;
      &lt;Component {...pageProps} /&gt;
    &lt;/&gt;
  )
}</code></pre>
<h3 id="nextscript">next/Script</h3>
<p><code>next/Script</code>는 개발자들이 third-party 스크립트들의 로딩 우선순위를 설정할 수 있어 로딩 성능을 향상시킬 수 있다. <code>next/Head</code>를 <code>_document.js</code>에서만 사용할 수 있다면, <code>next/Script</code>는 <code>_app.js</code>에서만 위치시킬 수 있다. </p>
<p>next/Script를 어떻게 적용시킬 것인지(Strategy)에 대한 옵션 값들은 다음과 같다. </p>
<ul>
<li><p><strong>BeforeInteractive</strong></p>
</li>
<li><p><em>페이지가 상호작용하기 전에 가져오거나 실행되어야 할 필요*</em>가 있는 중요한 스크립트에 사용하는 옵션이다. 이 옵션이 적용된 스크립트들은 초기 HTML에 삽입되어 자체 번들 JavaScript가 실행되기 전에 실행된다. </p>
</li>
<li><p>*<em>AfterInteractive (default) *</em> </p>
</li>
<li><p><em>태그 매니저나 분석툴처럼 해당 페이지의 상호작용 이후에 가져오고 실행되는 스크립트의 경우 사용*</em>한다. 이 스크립트들은 클라이언트 사이드에 삽입되어 hydration(html이 그려지고 난 후 이벤트 핸들러가 추가된 시점) 이후에 실행된다. </p>
</li>
<li><p><strong>lazyOnload</strong>
SNS 챗 같이 유휴 시간동안 기다릴 수 있는 스크립트의 경우 적용한다. </p>
</li>
</ul>
<p>이외에도 다양하게 적용할 수 있도록 지원하고 있다.  <a href="https://nextjs.org/docs/basic-features/script">공식문서 살펴보기</a></p>
<h3 id="step-3-조회수-측정하기">Step 3. 조회수 측정하기</h3>
<pre><code class="language-js">  useEffect(() =&gt; {
    const handleRouteChange = (url) =&gt; {
      gtag.pageview(url)
    }
    router.events.on(&#39;routeChangeComplete&#39;, handleRouteChange)
    router.events.on(&#39;hashChangeComplete&#39;, handleRouteChange)
    return () =&gt; {
      router.events.off(&#39;routeChangeComplete&#39;, handleRouteChange)
      router.events.off(&#39;hashChangeComplete&#39;, handleRouteChange)
    }
  }, [router.events])</code></pre>
<p>위 코드를 <code>_app.js</code>컴포넌트에 위치 시킴으로써 이벤트(유입, 이탈) 유형에 따라 조회수를 측정할 수 있다. </p>
<hr>
<h2 id="bonus--google의-다른-툴-추가-적용하기">Bonus : Google의 다른 툴 추가 적용하기</h2>
<p>만일 GA 뿐만 아니라, 구글 애드센스 등의 다른 툴을 프로젝트에 적용시켜야 한다면 마찬가지로 <code>_app.js</code>에
next/Script 컴포넌트를 사용해 적용할 수 있다. </p>
<p>만일 GoogleAds를 붙여야 한다면, GA가 적용된 코드 최하단에 gtag(&#39;confing&#39;, &#39;Google ADS코드&#39;)를 삽입하면 적용 가능하다. 각 트래킹 코드를 분석해 관련 툴에서 로그를 파싱해 기록한다. </p>
<pre><code class="language-js">&lt;Script
    id=&quot;gtag-init&quot;
    strategy=&quot;afterInteractive&quot;
    dangerouslySetInnerHTML={{
    __html: `
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag(&#39;js&#39;, new Date());
      gtag(&#39;config&#39;, &#39;${ga.GA_TRACKING_ID}&#39;, {
      page_path: window.location.pathname,
      });
      gtag(&#39;config&#39;, &#39;GOOGLE_ADS_TRACKINGCODE&#39;); &lt;-
      `,
    }}
  /&gt;  </code></pre>
<hr>
<h2 id="마치며">마치며</h2>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/ad30d71d-ea60-456e-a33c-c79b15cdf893/image.jpeg" alt=""></p>
<p>GA는 마케터였던 과거에도 날 애먹이더니, 이번에도 쉽지 않았다. 여러가지 포스팅에서 이전의 방식으로 GA를 적용하도록 안내하고 있어 적잖히 혼동을 주었기 때문이다. </p>
<p>공식 문서에 안내하는 방식 외에도 GA 등 마케팅 툴을 적용하는 방식은 다양하다. 하지만, 향후 보수성과 퍼포먼스를 고려한다면 공식문서가 제안하는 방향으로 이식을 진행하는 것이 합리적인 결정이라는 생각이 들었다. </p>
<p>이번에도 다시 한 번 더 <strong>공식문서의 중요성</strong>을 확인했다. </p>
<hr>
<h2 id="참조--읽어보면-좋은-글들">참조 / 읽어보면 좋은 글들</h2>
<ul>
<li>[Next.js 스크립트 컴포넌트] (<a href="https://themarketer.tistory.com/82">https://themarketer.tistory.com/82</a>)</li>
<li>[Next.js의 _document와 _app에 대하여] (<a href="https://merrily-code.tistory.com/154">https://merrily-code.tistory.com/154</a>)</li>
<li><a href="https://mnxmnz.github.io/nextjs/google-analytics/">[NextJS] Next 프로젝트에서 Google Analytics 적용하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(Next.js) Pre-rendering : getServerSideProps]]></title>
            <link>https://velog.io/@yunsungyang-omc/Next.js-Pre-rendering-getServerSideProps</link>
            <guid>https://velog.io/@yunsungyang-omc/Next.js-Pre-rendering-getServerSideProps</guid>
            <pubDate>Thu, 19 May 2022 09:57:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/95e53de6-a0fb-407d-a81c-ea04f4f31cb7/image.png" alt=""></p>
<p>이 포스팅은 <a href="https://velog.io/@yunsungyang-omc/next.js-Pre-rendering-Static-Generation">(Next.js) Pre-rendering : Static Generation</a>와 이어진 글입니다. </p>
<hr>
<h2 id="다시-들어서며">다시 들어서며</h2>
<p>앞서 살펴보았듯이, 정적인 요소를 담은 페이지를 렌더링하기 위해서 <code>getStaticProps</code>를 사용한다는 것을 알게 되었다. 여기서 정적인 요소를 담은 페이지로 마케팅 페이지, 블로그 포스트, 커머스 페이지 중 상품 상세 페이지 등을 말한다. </p>
<p>이어 이번 포스팅에서 알아볼 내용은 <code>getServerSideProps</code>이다. </p>
<h3 id="언제-사용하는-것이-좋을까">언제 사용하는 것이 좋을까?</h3>
<p>이전 포스팅을 통해 알아본 <code>getStaticProps</code>는 빌드 시에 데이터를 가져온다. 때문에 빌드되고 나면 그 데이터는 조작되지 않는다. 따라서, 최신화된 정보를 업데이트해 보여줘야 한다면 <code>getStaticProps</code>보다는 요청할때마다 데이터를 최신화할 수 있는 <code>getServerSideProps</code>를 사용해야 한다. </p>
<p>팀 프로젝트에서 상품 상세페이지를 Next.js로 컨버팅해야할 일이 있었는데 일반적으로 상품 상세 페이지는 <code>getStaticProps</code>를 이용할 것을 권장하고 있었다. 하지만, 해당 페이지에서 고객 리뷰, 세일 적용 가격 등 동적으로 구성해야할 요소들이 존재하고 있어서 <code>getServerSideProps</code>를 이용하기로 결정했다. </p>
<h2 id="getserversideprops와-react-query">getServerSideProps와 React Query</h2>
<p>팀 프로젝트는 상태관리를 위해 <code>redux-toolkit</code>을 사용하고 있다. <code>getServerSideProps</code>에 다른 상태관리 라이브러리를 시범적으로 적용해볼 수 있을 것 같아, 서버 스테이트를 다루는데 탁월한 기능을 제공하는 React Query를 사용해보기로 했다. (경우에 따라서는, vercel에서 제공하는 useSwr도 적용해볼만 할 것 같다.)</p>
<h3 id="nextjs에-react-query-적용하기">Next.js에 React Query 적용하기</h3>
<p>리액트 앱과 마찬가지고 리액트 쿼리도 앱 자체를 Provider로 감싸준 후 context api처럼 context 객체를 앱 전체에서 접근할 수 있도록 사전 작업을 해주어야 한다. </p>
<h4 id="_appjs">_app.js</h4>
<pre><code class="language-js">import { Hydrate, QueryClient, QueryClientProvider } from &#39;react-query&#39;;
import { ReactQueryDevtools } from &#39;react-query/devtools&#39;;

const App = (props) =&gt; {
  const { Component, pageProps } = props;

  const [queryClient] = useState(() =&gt; new QueryClient());

  return (
    &lt;QueryClientProvider client={queryClient}&gt;
      &lt;Hydrate state={pageProps.dehydratedState}&gt;
        &lt;Provider store={store}&gt;
            &lt;CssBaseline /&gt;
            &lt;Component {...pageProps} /&gt;
        &lt;/Provider&gt;
      &lt;/Hydrate&gt;
      &lt;ReactQueryDevtools /&gt;
    &lt;/QueryClientProvider&gt;
  );
};

export default App;</code></pre>
<p>context Provider처럼 <code>QueryClientProvider</code>로 감싸고, 다시 <code>Hydrate</code>로 감싸주었다.
이를 통해 Hydrate(업데이트되거나 패칭된)된 server state를 앱 전반에서 접근할 수 있게 되었다.</p>
<p>여기서 컴포넌트들이 접급하게 될 상태들은 pageProps 객체의 dehyderatedState에 내장된 값들이다. </p>
<h4 id="srcutilhooksusefeeddispatchjs">src/util/hooks/useFeedDispatch.js</h4>
<pre><code class="language-js">import { useDispatch, useSelector } from &#39;react-redux&#39;;
import { useQuery } from &#39;react-query&#39;;

export const useFeedDispatch = () =&gt; {
    const dispatch = useDispatch();

    const fetchFeedList = async (feedOrderType, page) =&gt; {
        try {
            ...
            const { data } = await axios({
                url: `${BASE_URL}/feeds?${queryString}`,
                method: &#39;GET&#39;,
            });
            return data;
        } catch (err) {
            consoloe.log(&#39;err&#39;, err);
        }
    };

    const useFeedList = () =&gt; {
        return useQuery([&#39;feedList&#39;], () =&gt; fetchFeedList());
    };
export { useFeedList, fetchFeedList };</code></pre>
<p>해당 파일에서는 서버 통신으로 데이터를 받아 <code>useQuery</code>를 이용해 &#39;feedList&#39;라는 키를 갖는 쿼리 객체 속에 리스폰스로 넘어온 데이터를 담아줄 것이다. </p>
<h4 id="srcpagesitemidjs">src/pages/item/[id].js</h4>
<pre><code class="language-js">import { dehydrate, QueryClient, useQuery } from &#39;react-query&#39;;
import { fetchFeedList } from &#39;src/util/hooks/useFeedDispatch&#39;;


const FeedDetail = () =&gt; {

      const { isLoading, error, isData } = useQuery(&#39;feedList&#39;, () =&gt;
        fetchFeedList(),
    );

      if (isLoading) return &lt;div&gt;Loading&lt;/div&gt;;
    if (error) return &#39;error has occured&#39;;

    return (
          ...
    )

export default FeedDetail;

export async function getServerSideProps() {
    const queryClient = new QueryClient();

    await queryClient.prefetchQuery(&#39;feedList&#39;, () =&gt;
        fetchFeedList(&#39;SELLS_MOST&#39;, 1),
    );

    return {
        props: {
            dehydrateState: dehydrate(queryClient),
         },
    };
}</code></pre>
<p><code>getStaticProps</code>함수에서는 props에 데이터 객체를 할당해 jsx 컴포넌트의 매개변수로 전달해 사용하고 있지만, <code>getServerSideProps</code>에서 쿼리 객체를 사용할때는 굳이 props를 매개변수로 전달할 필요가 없다. <strong>왜냐하면 이미 <code>_app.js</code>에서 Hydrate 프로바이더로 컴포넌트를 감싸주고 있어 접근이 가능하기 때문이다.</strong></p>
<p>따라서 위처럼 <code>getServerSideProps</code> 함수 내에서 데이터를 받아오는 <code>fetchFeedList</code>함수를 실행 후 <code>props: {dehydrateState: dehydrate(queryClient) }</code>라고 작성만 해주면 jsx 컴포넌트 내부에서 useQuery로 받아온 값들을 사용할 수 있다. </p>
<p><a href="https://prateeksurana.me/blog/mastering-data-fetching-with-react-query-and-next-js/">구동 프로세스 자세히 알아보기 : Mastering data fetching with React Query and Next.js</a></p>
<hr>
<h2 id="마치며">마치며</h2>
<p>만일, 리액트 쿼리를 이용하지 않고 상태관리를  redux로 관리할 경우에는  <strong><em>_app.js</em></strong>에 redux Wrapper로 감싸주는 등의 추가 작업이 역시 필요하다. (redux는 역시 보일러 플레이트를 좋아해..)</p>
<p><a href="https://simsimjae.medium.com/next-redux-wrapper%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0-5d0176209d14">next-redux-wapper에 관해 알아보기</a></p>
<p>리액트 쿼리를 이용할 경우, server state와 client state로 state 관리를 위한 관심사를 분리할 수 있고 패칭된 데이터를 알아서 로컬 캐싱해주는 등의 장점이 있기 때문에 프로젝트에 적용하면 장점이 많을 것이라는 생각이 들었다. </p>
<p>프로젝트에 적용하기 위해 참고한 많은 글들이 타입스크립트를 기준으로 작성되어 있어, 내 경우처럼 리액트와 next.js를 결합한 프로젝트를 진행하는 누군가가 있다면 도움이 되길 바라는 마음을 담아 이 글을 맺는다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/c1646a09-d53e-4864-ac8a-7f1c74552e3d/image.png" alt=""></p>
<hr>
<h3 id="읽어보면-좋은-글들">읽어보면 좋은 글들</h3>
<p><a href="https://react-query.tanstack.com/guides/ssr">리액트 쿼리 공식 가이드</a>
<a href="https://gingerkang.tistory.com/123">[React Query] Next.js + React Query로 무한 스크롤과 SSR 구현하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(Next.js) Pre-rendering : Static Generation]]></title>
            <link>https://velog.io/@yunsungyang-omc/next.js-Pre-rendering-Static-Generation</link>
            <guid>https://velog.io/@yunsungyang-omc/next.js-Pre-rendering-Static-Generation</guid>
            <pubDate>Thu, 19 May 2022 08:39:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/13568d9e-50e6-489d-aa45-a308dcda6b81/image.png" alt=""></p>
<hr>
<h2 id="들어서며">들어서며</h2>
<p><code>Next.js</code>의 Dynamic Routing은 <code>react-router-dom</code>을 사용하지 않고도 아주 아주 손쉽게 라우팅을 프로젝트에 적용할 수 있다. src 디렉토리에서 page 폴더에 js 파일만 넣어놓으면 알아서 라우팅되기 때문에 next.js를 입문하는 뉴비 개발자들에게 가장 널리 알려진 기능이기도 하다. </p>
<p>하지만 Dynamic Routing만 이용하기 위해 <code>Next.js</code>를 이용한다는 것은 <code>Next.js</code>의 본질을 제대로 이해하지 못한 것이다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/9869bd98-e678-4b6d-aaf8-9960aec114c0/image.png" alt=""></p>
<p><code>Next.js</code>는 서버 사이드 랜더링을 구현하기 위함이지, 손쉽게 라우팅을 구현하기 위한 것이 아니다!
하여, 본 포스팅에서는 <code>Next.js</code>의 주요한 기능인 <strong>getStaticProps</strong>와 <strong>getServerSideProps</strong>에 대해 다뤄보려고 한다.</p>
<h3 id="getstaticsideprops와-getserversideprops">getStaticSideProps와 getServerSideProps</h3>
<p>아주 짧게 요약하면, 이 두 가지 기능을 사용하는 이유는 PreRendering을 적용하기 위함이다. </p>
<h4 id="prerendering">PreRendering</h4>
<p>프리랜더링이란 <code>SSR(ServerSideProps)</code>를 구현하는 Next.js의 가장 큰 특징이다. React로 구성된 웹/앱은 <code>CSR(Client Side Rendering)</code>방식을 사용한다. CSR은 <strong>초기 랜더링 시 자바스크립트 파일을 모두 로드</strong>해오기 때문에, 초기 렌더링의 사용자 경험이 SSR에 비해 좋지 못하다. </p>
<p>때문에 사용자 반응이 중요하고, 검색엔진 최적화(search engine optimization, SEO)가 중요한 도메인에서는 서버 사이드 랜더링이 줄 수 있는 이점이 중요하다. </p>
<p>SSR은 <strong>미리 서버에서 HTML파일을 랜더링해 클라이언트로 전송</strong>해주기 때문에 초기랜더링 속도가 빠르다. 하지만 매번 서버에 요청을 보내서 화면을 받아오기 때문에, 화면 깜빡임이 많아지는 단점 또한 존재한다. </p>
<p>여기서, 프리 랜더링이 빛을 발휘하게 된다. <strong>Next.js는 필요한 화면만큼을 렌더링하고 나머지는 차차 자바스크립트 파일을 받아와 클라이언트 측에 랜더링을 맡기는 절충의 형태</strong>를 가지고 있다. </p>
<blockquote>
<p>보다 쉽게 표현하자면, 처음에는 SSR을 나중에는 CSR 방식을 사용한다는 것이다. 문자 그대로 이해하자! 미리 랜더링하는 것!</p>
</blockquote>
<h4 id="prerendering-적용하기">PreRendering 적용하기</h4>
<p>Next.js는 프리랜더링을 위해 정적 생성 방식과 서버사이드 랜더링 방식을 제공하고, 각각의 경우에 따라 사용할 것을 권장하고 있다. </p>
<h4 id="정적-생성방식-static-generation---getstaticprops">정적 생성방식 (Static Generation - getStaticProps)</h4>
<p>정적 생성 방식은 페이지의 콘텐츠가 외부 데이터에 연동될 때 사용할 수 있는데, 대략 마케팅 페이지, 블로그 포스트, E-커머스의 상품 페이지, term 같은 정적 텍스트가 주가 되는 페이지를 랜더링할때 유용하게 사용할 수 있다.  </p>
<p>내 경우에는 프로젝트에서 &quot;규정&quot;과 관련된 텍스트들을 랜더링할때 사용할 수 있었다.</p>
<pre><code class="language-js">import Term from &quot;src/constants/terms/Term&quot;;
import { Grid } from &quot;@mui/material&quot;;
import { serialize } from &quot;next-mdx-remote/serialize&quot;;
import { MDXRemote } from &quot;next-mdx-remote&quot;;

const ServiceTerms = ({ source }) =&gt; ( // &lt;--- 이 부분을 주목하시라
  &lt;Grid container direction=&quot;column&quot; flexWrap=&quot;nowrap&quot;&gt;
    &lt;Grid container direction=&quot;row&quot;&gt;
      &lt;MDXRemote {...source} /&gt;
    &lt;/Grid&gt;
  &lt;/Grid&gt;
);

export async function getStaticProps() {
  // MDX text - can be from a local file, database, anywhere
  const source = serviceTerm;
  const mdxSource = await serialize(source);
  return { props: { source: mdxSource } };
}

export default ServiceTerms;</code></pre>
<p>자주 바뀌지 않는 규정 텍스트 뭉치를 상수 폴더 안에서 보관하고 있었는데, 이에 대해 마크다운 문법까지 적용해 프리랜더링 하기 위해 Next.js의 공식문서에서 사용하기를 권장하고 있는 <code>next-mdx-remote</code>라이브러리를 사용했다.</p>
<p><code>mdxSource</code>로 정의한 변수를 props의 벨류로 적용 후, 상단의 JSX 함수의 매개변수로 전달해주면 프리랜더링이 아주 간단하게 적용된다. </p>
<p><code>getStaticProps</code>함수는 params를 받아 그에 해당하는 경로(path)도 받아오는데, 캐싱까지 지원되기 때문에 페이지를 이동할 때 굉장히 빠른 렌더링 속도를 보여준다. 만일 데이터를 적용할 필요없이 단순히, path만 얻고 싶다면, <code>getStaticPath</code>를 사용할 수 있다. 하지만 대게는, 경로 뿐만 아니라 이에 연동된 데이터까지 적용하기 때문에 주로 getStaticProps를 사용하게 될 것이다. </p>
<h4 id="sample-code">sample code</h4>
<pre><code class="language-js">const SamplerPost ({ post}) =&gt; {
  ...

}

export async function getStaticPaths() {
  const res = await fetch(&#39;https://.../posts&#39;);
  const posts = await res.json();
  // 원하는 post로 구성된 paths를 pre rendering할 수 있다.  
  const paths = posts.map((post) =&gt; ({
    params: {id: post.id}
  }));
  // { fallback : false }는 이외 다른 경로를 404에러 처리한다는 것을 의미한다.
  return { paths, fallback: false }
}

export async function getStaticProps({params}) {
  const res = await fetch(&#39;https://.../popst/${params.id}&#39;);
  const post = await res.json();

  return { props: {post}}
}

export default SamplePost;</code></pre>
<hr>
<h2 id="더-읽어보면-좋은-글">더 읽어보면 좋은 글</h2>
<ul>
<li><a href="https://nextjs.org/blog/markdown">Next.js에서 마크다운 문법 적용하기</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(라이브러리) Strapi : for Headless Commerce]]></title>
            <link>https://velog.io/@yunsungyang-omc/%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-Strapi-for-Headless-Commerce</link>
            <guid>https://velog.io/@yunsungyang-omc/%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-Strapi-for-Headless-Commerce</guid>
            <pubDate>Wed, 18 May 2022 02:34:10 GMT</pubDate>
            <description><![CDATA[<p><strong>Strapi Node.js 기반 오픈 소스 헤드리스 CMS다.</strong></p>
<blockquote>
<p><strong>CMS(Contents Management System)란?</strong>
DB에 데이터를 전달할 수 있도록 웹 관리자 화면을 제공하고,
웹 관리자에서 내용을 입력하면 CMS가 가진  프론트엔드 UI를 통해 코딩없이 웹사이트를
만들 수 있도록 솔루션을 제공하는 것을 말한다. </p>
</blockquote>
<ul>
<li>Strapi는 Front UI 대신 API 제공해준다. 따라서 Strapi에 Headless가 붙는다. 이렇듯 strapi는 DB, 웹관리자, API까지 쉽게 구축을 도와주는 BFF(Backedn for Frontend)로서 역할을 제공한다. </li>
</ul>
<p>strapi를 설치하면 웹관리자 화면에서 DB 생성 및 REST API를 생성할 수 있다. 
언제든지 API 파일에 액세스할 수 있고 커스텀할 수 있다. 더불어, 고급 필터링, 정렬 및 페이지 매김, 인증 관리 등을 지원한다.  </p>
<hr>
<h3 id="strapi-실행하기">strapi 실행하기</h3>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/f522eccd-e729-454a-b69a-a529c975aa1f/image.png" alt=""></p>
<pre><code>strapi develop</code></pre><h3 id="strapi-가능-간략-요약">strapi 가능 간략 요약</h3>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/7dcb592d-b6d4-4a26-92ed-abb272e485e9/image.png" alt=""></p>
<p>스트라피 로그인</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/db3b652d-7bc7-44a1-9d3b-b4666dbb235f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/5afb1e44-1f6b-4433-9adc-a3bb1cca24bc/image.png" alt=""></p>
<p>스트라피는 모델링을 쉽게 해주는 워크스페이스를 제공한다. 여기서 컬랙션은 모델을 말한다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/df5b3b63-6cf0-4e00-9891-5b6416ac1792/image.png" alt=""></p>
<p>필요한 필드를 선택해서 만들 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/9e060f88-0150-4459-8f39-22f3c1e7ecb9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/8ecfffe4-7f7e-42f9-a5c7-9d86a96d0ab6/image.png" alt=""></p>
<p>모델만 생성해도 REST API가 기본적으로 만들어진다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/71bd7b17-473b-4265-adcd-caedca82ae08/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/9b727ae9-1c39-4d13-a7c6-4a59ce5b4a57/image.png" alt=""></p>
<p>원하는 롤도 생성할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/49fa68cc-7923-4adf-8e18-1b0dc5cba375/image.png" alt=""></p>
<p>어드민 계정으로 데이터도 쉽게 접근이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/46b84162-2d72-4d7b-a8b7-77e3d73eaa7e/image.png" alt=""></p>
<p>소셜 로그인도 쉽게 구현할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/72496153-9ec8-43f8-a202-21eaea1b2646/image.png" alt=""></p>
<p>기본적으로 제공하는 api들이 풍부해서 개발시간을 획기적으로 줄여줄 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(Next.js) 에러로그 : React does not recongnize the 'X' Prop on a DOM element ...]]></title>
            <link>https://velog.io/@yunsungyang-omc/Next.js-%EC%97%90%EB%9F%AC%EB%A1%9C%EA%B7%B8-React-does-not-recongnize-the-X-Prop-on-a-DOM-element-</link>
            <guid>https://velog.io/@yunsungyang-omc/Next.js-%EC%97%90%EB%9F%AC%EB%A1%9C%EA%B7%B8-React-does-not-recongnize-the-X-Prop-on-a-DOM-element-</guid>
            <pubDate>Thu, 28 Apr 2022 03:30:21 GMT</pubDate>
            <description><![CDATA[<hr>
<p>MUI v5는 Emotion을 기본 스타일 엔진으로 사용하고 있다. MUI의 기본 스타일 구성요소를 사용할때는 꽤나 잘 적용되기 때문에 일관적인 스타일링을 구성할때 유용하다. </p>
<p>다만, v5로 변경되면서 아직 최적화가 제대로 되지 않은 것인 스타일 API를 사용할때 특정사례에서 에러가 생길 수 있다. 하여, mui v5에서는 커스텀 컴포넌트를 구성할때 <code>sx</code>프랍을 사용해서 커스텀할 것을 권고하고 있다. </p>
<p>진행하고 있는 프로젝트에서는 mui v5를 사용하고 있지만, 몇 구간에서는 emotion 스타일 엔진에서 제공하고 있는 <code>styled</code>API를 이용하고 있는데, 이곳에서 쉽게 이해할 수 없는 에러가 발생했다. </p>
<hr>
<h3 id="에러--react-does-not-recongnize-the-x-prop-on-a-dom-element-if-you-intentionally-want-to-to-appear-in-the-dom-as-a-custom-attribute-spell-it-as-lowercas-x-instead">에러 : React does not recongnize the &#39;X&#39; Prop on a DOM element. if you intentionally want to to appear in the DOM as a custom attribute, spell it as lowercas &#39;x&#39; instead.</h3>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/87a4aa0e-9e28-4248-bd22-794e1f0fc01b/image.png" alt=""></p>
<p>일반적으로 JS에서 prop을 네이밍할때, 카멜케이스를 적용해서 네이밍을 한다. 이전에 리액트로 프로젝트를 구성할때는 아무런 문제가 없는 부분이었는데 이번 프로젝트에서는 유독 몇 구간에서 이러한 에러가 발생했다.</p>
<p><a href="https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;blogId=ege1001&amp;logNo=220466932974">카멜케이스 네이밍 문법 살펴보기</a></p>
<pre><code class="language-js">import { Swiper } from &#39;swiper/react&#39;;
import { styled } from &#39;@mui/system&#39;;

export const Banner = styled(&#39;div&#39;) ((props) =&gt; ({
    backgroundImage: `url(${props.bgImgUrl})`,
    ...
    ...(props.isTopBanner
        ? {
                height: &#39;100%&#39;,
          }
        : {
                height: &#39;172px&#39;,
                marginRight: &#39;20px&#39;,
                borderRadius: &#39;7px&#39;,
          }),
}));</code></pre>
<p>에러의 발생 구간을 살펴보았더니, <code>styled</code>API를 사용해서 스타일드 컴포넌트를 구성하고 있는 곳에서 에러가 발생하고 있다. 컴포넌트의 props를 통해 선택적으로 스타일링을 구사하는 부분에서 문제가 생기고 있는데, 이에 관해서 mui 이슈란에서 활발히 논의가 되고 있는 것을 확인할 수 있었다.</p>
<p><a href="https://github.com/mui/material-ui/issues/15614">mui 깃헙 관련 이슈 살펴보기</a>
<a href="https://github.com/mui/material-ui/issues/27512">mui 깃헙 SSR in makeStyle 이슈 살펴보기</a></p>
<p>두 번째 링크는 서버사이드 랜더링에서 makeStyle을 적용했을때 className이 제대로 매치되지 않아 생기는 에러에 관해서 토론을 나누는 거인데, 결국 두 링크 모두 mui v4에서 mui v5로 버전업 되면서 생겨난 문제라는 공통점을 가진다. </p>
<p><strong>다시 말하지만 mui v5는 이모션 스타일 엔진을 사용하고 있다.</strong> 하여, 스타일드 컴포넌트의 방식을 사용한 구간에서 여러 에러가 발생하고 있고 현재에도 다양한 논의가 진행 중에 있다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/e851dd85-6c50-4fea-a800-beb8b4a4ce8f/image.png" alt=""></p>
<h4 id="대충-요약하자면-나는-스타일드-컴포넌트에서-사용하는-패턴에-익숙해져있는데-mui-v5의-방식은-너무-불편하다고">대충 요약하자면, 나는 스타일드 컴포넌트에서 사용하는 패턴에 익숙해져있는데, mui v5의 방식은 너무 불편하다고!</h4>
<p>라는 유저의 불만섞인 코맨트도 살펴볼 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/aa31c666-ebd5-4a5d-95da-f4e9d6f875ae/image.png" alt=""></p>
<p>이게 관해 mui 측에서는 <code>withConfig</code>만 추가해주면 되고, 사용하는제 아무 문제가 없다고 답변을 남기고 있다. (하지만, 실제로 사용해보면 전혀 그렇지 않은걸..?)</p>
<hr>
<h3 id="mui-v5에서-제안하고-있는-스타일드-컴포넌트-사용법">mui v5에서 제안하고 있는 스타일드 컴포넌트 사용법</h3>
<p><a href="https://mui.com/system/styled/#styled-component-options-styles-component">공식문서 살펴보기</a></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/3e50e70c-08f7-4687-ba84-2d13be39a605/image.png" alt=""></p>
<p>프로젝트에서 사용하고 있는 코드도 mui v5에서 제안하고 있는대로 프랍을 파라미터로 전달해서 사용하고 있다. 하지만, props의 카멜 케이스 네이밍에서 경고가 발생하고 있다. </p>
<h3 id="해결책">해결책</h3>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/f5d8ca82-767e-49f3-90b4-20130c374d5c/image.png" alt=""></p>
<p>mui에서는 이를 의식해 몇 가지 해결책을 제시하고 있는데, <code>theme</code>을 사용하거나, sx프랍을 사용해서 커스텀할 것, 마지막으로 <code>shoudForwarProp</code>옵션을 설정하는 것이다.</p>
<blockquote>
<p><strong>options.shouldForwardProp</strong> ((prop: string) =&gt; bool [optional]): Indicates whether the prop should be forwarded to the Component.</p>
</blockquote>
<p><code>shoudForwarProp</code>은 프랍을 컴포넌트의 구성 요소로 지정할 것인지에 대해 true와 false를 지정할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/ce92236e-e07e-4d37-a576-225c151fefbd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/c18d3583-15f6-4c6e-a8af-b96c9c4f9fb4/image.png" alt=""></p>
<p>커스텀 theme에서 지정된 sx, color, variant의 값이 스타일드 div에 전파되지 않고, 바탕색과 패딩이 커스텀 값으로 생성된 것을 확인할 수 있다. </p>
<hr>
<h2 id="👹-결론">👹 결론</h2>
<p>이 문제를 해결하기 위해 팀장님께서 제시해주신 방법은 <code>shoudForwarProp</code> 값에 false를 줘서 props을 전달할대 생기는 에러를 차단하는 것이었다. </p>
<p><a href="https://stackoverflow.com/questions/71451558/getting-warning-from-props-passed-in-mui-styled-components-related-to-react-not">비슷하게 해결한 케이스 살펴보기</a></p>
<pre><code class="language-js">export const Banner = styled(&quot;div&quot;, { shouldForwardProp: false })((props) =&gt; ({
  backgroundImage: `url(${props.bgImgUrl})`,
  / ... /
  ...(props.isTopBanner
    ? {
        height: &quot;100%&quot;,
      }
    : {
        height: &quot;172px&quot;,
        marginRight: &quot;20px&quot;,
        borderRadius: &quot;7px&quot;,
      }),
}));</code></pre>
<p>이렇게 코드를 수정하고 나니 더 이상 콘솔창에 경고가 발생하지 않았다. </p>
<hr>
<p>사실 이 경고창이 콘솔에 발생하더라도, 기능 구현에는 아무런 영향이 없었다. 때문에, 무시하고 넘어갈 수도 있었지만, 몇 가지 프랍만으로도 콘솔창을 경고가 가득 채우다보니 개발 효율이 떨어지는 것을 느낄 수 있었다.</p>
<p>이 에러를 수정하는데 꽤나 고생을 해야 했지만, mui v5에서 겪을 수 있는 이슈에 대해서 보다 잘 이해하게 되었다. 꽤 흔하게 발생하는 이슈임에도 불구하고 명쾌하게 해결책을 제시하는 글을 찾아볼 수 없었기 때문에 이 포스팅을 남긴다. </p>
<p>반드시 누군가에게 큰 도움이 되었으면 좋겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(Next.js) Next 앱에서 material ui와 styled-component를 적용하기]]></title>
            <link>https://velog.io/@yunsungyang-omc/next.js-Next-%EC%95%B1%EC%97%90%EC%84%9C-material-ui%EC%99%80-styled-component%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsungyang-omc/next.js-Next-%EC%95%B1%EC%97%90%EC%84%9C-material-ui%EC%99%80-styled-component%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 25 Apr 2022 06:45:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/583fe5d4-7933-4044-895f-17359a01ee9b/image.png" alt=""></p>
<hr>
<p>약 2주 전부터 팀에서 내게 할당된 프로젝트로 기존에 리액트 프로젝트로 구성된 웹 서비스를 next.js 기반 서비스로 컨버팅하는 업무를 수행하고 있다.</p>
<p>클라이언트 렌데링으로 구동되는 리액트 앱을 서버 사이드 랜더링으로 구동하는 넥스트 앱으로 컨버팅하는 것은 공식문서가 제안하는 방법처럼 그리 만만하지 않았다. </p>
<p>이전 작성한 글처럼, 서비스가 최초 랜더링될 때 window 객체를 참조할 수 없기에 관련 로직들을 수정해야 했고,  CSS 스타일링 또한 리액트 앱에서 당연히 순조롭게 구성되던 방식들에서 에러가 뿜어지고는 했다. </p>
<p>이 글은 Next.js에서 스타일드 컴포넌트와 mui를 동시에 적용하기 위해 자료를 리서치하다 모은 집단지성의 흔적을 담은 글이다. </p>
<p>결론적으로, ** mui v5와 스타일드 컴포넌트의 조합은 과도기적인 단계에 있고 이 둘을 조합해서 사용하는 것은 그리 좋은 선택이 아닌 것 같다는 생각이 든다.**</p>
<p><em>*<em>그럼에도 불구하고, 스타일드 컴포넌트를 사용하고 싶다면 이 포스팅의 결론부분으로 빠르게 스크롤을 내리길 바란다. 그곳에 해답이 있다. *</em></em></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/954aab7a-9d8b-4d0c-b927-db686524d093/image.png" alt=""></p>
<p>간단한 임무가 될 줄 알았지만, 아주 피똥을 싸야했던 어느 미군 부대의 이야기를 담은 미드.. 이 프로젝트를 하면서 내내 이 드라마가 생각이 났다.</p>
<hr>
<h2 id="nextjs에서-mui5-적용하기">Next.js에서 mui5 적용하기</h2>
<p><em><strong>이 포스팅은 mui v5 버전 기준으로 작성되었습니다.</strong></em></p>
<p>기존 mui 버전 4에서 버전 5으로 패치업되면서 몇 가지 방식이 변화되었는데 생각보다 중요한 문제였다. </p>
<p>mui5는 emotion 라이브러리를 기반으로 빌드업되었고, CSS 스타일을 생성한다. 따라서 mui5를 next.js 앱에 적용하기 위해서는 emotion의 cache와 server 패키치가 설치되어야 한다.</p>
<blockquote>
<p>👹 <strong>필수로 설치되야할 패키지들</strong></p>
</blockquote>
<ul>
<li>@emotion/cache</li>
<li>@emotion/react</li>
<li>@emotion/server</li>
<li>@emotion/styled</li>
</ul>
<p>👺 emotion 라이브러리 : 이모션 라이브러리는 css-in-js 형식으로 스타일을 사용할 수 있게 도와준다. className이 자동으로 부여되기 때문에 스타일이 겹칠 염려가 없다. 재사용이 가능하고, prop과 조건에 따른 스타일을 지정가능하다.</p>
<p>만일 프로젝트에서 mui4를 사용하고 있다면,레거시로 정의된 패키지들을 제거해주는 것이 좋다.<br><code>yarn remove @material-ui/core @material-ui/icons @material-ui/lab @material-ui/pickers</code></p>
<p>자 이제, mui를 설치해주자.
<code>npm i @mui/material</code>혹은<code>yarn add @mui/material</code></p>
<hr>
<h3 id="step-1-mui-테마-만들어주기">Step 1. MUI 테마 만들어주기</h3>
<p><code>styles/theme.js</code> 파일을 생성 후 다음과 같이 입력한다.</p>
<pre><code class="language-js">import { createTheme, responsiveFontSizes } from &quot;@mui/material/styles&quot;;
import { deepPurple, amber } from &quot;@mui/material/colors&quot;;

// Create a theme instance.
let theme = createTheme({
  palette: {
    primary: deepPurple,
    secondary: amber,
  },
});

theme = responsiveFontSizes(theme);

export default theme;</code></pre>
<hr>
<h3 id="step-2-createemotioncache">Step 2. createEmotionCache</h3>
<p><code>util/createEmotionCache.js</code> src 안에 utl 폴더(경우에 따라서는 styles)를 생성하고, <code>createEmotionCache.js</code> 파일을 생성한다. </p>
<p>이 파일은 앱이 css 스타일을 어떻게 적용해야 할지 빠르게 인식할 수 있도록 도와주는 역할을 한다.  </p>
<pre><code class="language-js">import createCache from &#39;@emotion/cache&#39;;

export default function createEmotionCache() {
  return createCache({ key: &#39;css&#39; });
}</code></pre>
<hr>
<h3 id="step-3-_appjs에서-적용하기">Step 3. _app.js에서 적용하기</h3>
<p>앞서 만들어준 테마를 앱에 적용시키기 위해서 캐치를 생성하고, 이를 캐치프로바이더로 일괄적으로 적용시킬 수 있다. </p>
<pre><code class="language-js">import React from &#39;react&#39;;
import { CacheProvider } from &#39;@emotion/react&#39;;
import { ThemeProvider, CssBaseline } from &#39;@mui/material&#39;;

import createEmotionCache from &#39;../util/createEmotionCache&#39;;
import theme from &#39;../styles/theme&#39;;
import &#39;../styles/globals.css&#39;;

const clientSideEmotionCache = createEmotionCache();

const MyApp = (props) =&gt; {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;

  return (
    &lt;CacheProvider value={emotionCache}&gt;
      &lt;ThemeProvider theme={lightTheme}&gt;
        &lt;CssBaseline /&gt;
        &lt;Component {...pageProps} /&gt;
      &lt;/ThemeProvider&gt;
    &lt;/CacheProvider&gt;
  );
};

export default MyApp;</code></pre>
<ul>
<li><p>프로젝트를 실행하면 브라우저마다 각기 다른 기본 css가 적용되어 있을 것이다. 서비스를 제공할대 모든 브라우저에서 일관적인 스타일을 보여줘야 하는데, 이때 <code>&lt;CssBaseLine /&gt;</code>을 사용한다. </p>
</li>
<li><p>앱에서의 스타일링은 주로 mui 컴포넌트 내부에서 인라인 방식으로 스타일 객체를 작성하는 것이 권장되기 때문에 next.js에서 css를 사용하기 위해 사용하는또 다른 방식인 <code>module.css</code> 파일을 제거할 수 있다. </p>
</li>
<li><p>사용하지 않는 파일들은 제거해주자. 
  <code>rm styles/Home.module.css</code>
  <code>rm pages/api/hello.js</code></p>
</li>
</ul>
<p>👹 react-app에서 글로벌 스타일을 적용하기 위해서는 <code>createGlobalStyle()</code>을 이용했지만,  next.js에서는 <code>styles/golobal.css</code>파일에 공통적으로 적용할 스타일을 넣어주면 된다. </p>
<hr>
<h3 id="step-4-_documentjs-파일-커스텀하기">Step 4. _document.js 파일 커스텀하기</h3>
<p><code>_document</code>는 서버 사이드 랜더링에 관여하는 로직 혹은 정적인 페이지를 로드하는데 사용되는 로직을 추가하는데 사용한다. 따라서, 서버 사이드 랜더링 지원을 위해 이 작업이 <strong>필수적</strong>으로 필요하다. </p>
<ul>
<li><p>👺 이 작업을 해주지 않는다면, 서버에서 받아온 html, css / 클라이언트에서 렌더링한 html, css가 달라 경고를 띄우게 된다. 따라서, 서버와 클라이언트의 싱크를 맞추기 위해 이 작업이 필요하다. </p>
</li>
<li><p>mui는 <a href="https://fonts.google.com/specimen/Roboto">Roboto</a> 폰트를 디폴트로 사용한다. 따라서, npm i @fontsource/roboto or yarn add @fontsource/roboto를 입력해 디폴트로 사용할 폰트를 설치해주는 것이 좋다. </p>
</li>
</ul>
<pre><code class="language-js">import * as React from &#39;react&#39;;
import Document, { Html, Head, Main, NextScript } from &#39;next/document&#39;;
import createEmotionServer from &#39;@emotion/server/create-instance&#39;; // 서버사이드 렌더링 시 캐칭된 css 옵션을 이용할 수 있게 해준다.
import createEmotionCache from &#39;../util/createEmotionCache&#39;; // css 옵션을 파싱해서 하나의 객체로 묶어주는 역할을 한다. 

import theme from &#39;../styles/theme&#39;;

export default class MyDocument extends Document {
  render() {
    return (
      &lt;Html lang=&quot;en&quot;&gt;
        &lt;Head&gt;
          &lt;link
            rel=&quot;stylesheet&quot;
            href=&quot;https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap&quot;
          /&gt;
        &lt;/Head&gt;
        &lt;body&gt;
          &lt;Main /&gt;
          &lt;NextScript /&gt;
        &lt;/body&gt;
      &lt;/Html&gt;
    );
  }
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it&#39;s compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx) =&gt; {
  const originalRenderPage = ctx.renderPage;
  const cache = createEmotionCache(); // 캐치된 객체를 정의해주고, 
  const { extractCriticalToChunks } = createEmotionServer(cache); // 서버사이드 랜더링 시 할당된 스타일 객체를 스타일 오브젝트 객체에 입혀줄 것이다.

  ctx.renderPage = () =&gt;
    originalRenderPage({
      enhanceApp: (App) =&gt; (props) =&gt; 
        &lt;App emotionCache={cache} {...props} // 리덕스 스토어를 적용하는 방식과 유사하다.
      /&gt;,
    });

  const initialProps = await Document.getInitialProps(ctx);
  const emotionStyles = extractCriticalToChunks(initialProps.html); 
  const emotionStyleTags = emotionStyles.styles.map((style) =&gt; ( // 스타일을 파싱해서 묶어주는 역할을 한다.
    &lt;style
      data-emotion={`${style.key} ${style.ids.join(&#39; &#39;)}`}
      key={style.key}
      // eslint-disable-next-line react/no-danger
      dangerouslySetInnerHTML={{ __html: style.css }}
    /&gt;
  ));
  return {
    ...initialProps,
    // Styles fragment is rendered after the app and page rendering finish.
    styles: [...React.Children.toArray(initialProps.styles), ...emotionStyleTags],
  };
};</code></pre>
<p>여기까지 세팅을 제대로 마쳤다면, 이제 next.js 앱에서 mui5를 적용해서 빠르게 앱을 만들 수 있다. 
다만, 내 경우처럼 레거시에서 스타일드 컴포넌트를 사용하고 있다면 추가적으로 작성해줘야 할 것들이 있다.</p>
<hr>
<h2 id="nextjs에서-styled-components-적용하기">Next.js에서 styled-components 적용하기</h2>
<p>이 문제 때문에 꽤나 골치를 겪었는데,
일부 상황에서는 <code>styled-components</code>가 제대로 적용되지 않는 문제가 발생하고는 했다.
아마, Next.js는 기본적으로 서버 사이드 환경에서 미리 랜더링을 제공하는데, 서버 사이드 랜더링 시에 <code>styled-components</code>가 함께 적용되어야 하는, 그것이 제대로 되지 않는 문제였다. </p>
<p>이 문제에 관해서는 mui github issue에서 최근까지도 논의가 되고 있는데, 뾰족하게 해결이 되지 않은 것 같다. 하여, mui 5에서는 emotion을 이용해서 next.js에 프레임웍을 적용할 것을 권장하고 있다. </p>
<p><a href="https://github.com/mui/material-ui/issues/29742">관련 이슈 살펴보기 : [styled-engine-sc] MUI + styled-components + Next.js CSS rules ordering issue</a></p>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/66c9b816-2d5f-4f4a-ba36-a99a5ca52acf/image.png" alt=""></p>
<hr>
<p>때문에 Next.js에서 <code>styled-components</code>를 쓰기 위해서는 bable-plugin 설치가 필요하다. </p>
<h3 id="step-1-babel-plugin-설치">Step 1) babel-plugin 설치</h3>
<pre><code class="language-js">// with npm 
npm install @babel-plugin-styled-components --save-dev

// width yarn 
yarn add @babel-plugin-styled-components --dev</code></pre>
<p><code>@babel-plugin-styled-components</code>를 설치 후 루트 디렉토리에 <code>.babelrc</code>파일을 생성해준다.</p>
<pre><code class="language-js">// .babelrc
{
    &quot;presets&quot;: [&quot;next/babel&quot;],
      &quot;plugins&quot;: [
        [&quot;styled-components&quot;, { &quot;ssr&quot;: true }]
    ]
}</code></pre>
<h3 id="stpe-2-documenttsx-파일-커스텀하기">Stpe 2) <em>document.tsx 파일 커스텀하기</em></h3>
<pre><code class="language-js">// _document.tsx
import Document, { DocumentContext } from &#39;next/document&#39;
import { ServerStyleSheet } from &#39;styled-components&#39; // 서버사이드 랜더링이 가능하도록 설정

export default class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet() // 스타일드 컴포넌트 스타일 시트
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =&gt;
        originalRenderPage({
          enhanceApp: (App) =&gt; (props) =&gt;
            sheet.collectStyles(&lt;App {...props} /&gt;),
        })

      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: (
          &lt;&gt;
            {initialProps.styles} 
            {sheet.getStyleElement()} // 스타일드 컴포넌트 스타일 시트를 스타일 객체로 등록한다.
          &lt;/&gt;
        ),
      }
    } finally {
      sheet.seal()
    }
  }
}</code></pre>
<hr>
<h2 id="🧲-결론--emotion-styled를-사용하자">🧲 결론 : emotion styled를 사용하자</h2>
<p>결론을 우선 말하면, 위처럼 <code>styled-components</code>를 사용가능하도록 세팅을 해주더라도,
mui와 <code>styled-components</code>를 아주 깔끔하게 조합하기란 쉽지 않은 일이었다. </p>
<p>next.js를 만들어낸 vercel과 mui 양쪽의 공식 깃헙 예제를 살펴보면 emotion을 사용할 것을 권고 있다. 따라서, next.js에서 스타일드 컴포넌트를 사용하고 싶다면 <code>@emotion/react</code>를 사용하는 것이 정신건강에 좋다. </p>
<p><code>@emotion/react</code>이 라이브러리는 css prop 지원하고 ssr시 아무런 설정이 필요없다. </p>
<p>앞서 언급하지 못했지만, mui4에서 5로 버전업하면서 생긴 주요한 변화로</p>
<p>mui 컴포넌트를 커스텀하는 방식이었던 <code>makeStyle()</code>이 폐지되었다는 것이다. 따라서 커스텀이 필요하다면 sx 객체 안에 스타일링 객체를 넣던가, emotion의 styled()를 이용해서 스타일드 컴포넌트를 만들어주는 방식으로 진행하게 된다. </p>
<p><a href="https://www.reddit.com/r/reactjs/comments/uabs6g/material_ui_react_18/">관련 링크 살펴보기</a></p>
<pre><code class="language-js">import { styled } from &#39;@mui/system&#39;;

export const ContentContainer = styled(&#39;div&#39;)({
    width: &#39;100%&#39;,
    backgroundColor: &#39;white&#39;,
    overflowY: &#39;auto&#39;,
    display: &#39;flex&#39;,
    flexDirection: &#39;column&#39;,
    justifyContent: &#39;space-between&#39;,
});</code></pre>
<hr>
<h2 id="retrospect">Retrospect</h2>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/882a42cc-41b1-4497-b28d-bd2d906f52b1/image.png" alt=""></p>
<p>emotion을 이용해서 <code>styled-components</code>를 사용할 수 있게 되었지만, mui를 사용하고 있는 이상, 굳이 이 두 가지를 섞어서 프로젝트를 진행하는 것이 효율적인가라는 점에서는 스스로 의심이 된다.</p>
<p>더불어, emotion은 자바스크립트로 작성되어 있기 때문에 타입스크립트로 프로젝트를 진행해야 한다면, 더더욱이 mui를 next.js 앱에서 사용하는 것이 어려울 수 있다고 생각한다. </p>
<p>디자이너가 팀에 편성되어 있어서 스타일링 프레임워크를 고민하지 않아도 된다면, 스타일드 컴포넌트를 사용할 것 같다. 혹여나 프레임워크를 적용해야 한다면, 아직 사용해보지는 못했지만 테일윈드 CSS를 고려해볼 것 같다. </p>
<hr>
<h2 id="출처">출처</h2>
<ul>
<li><a href="https://www.reddit.com/r/reactjs/comments/uabs6g/material_ui_react_18/">Material ui, react 18 : r/reactjs - Reddit</a></li>
<li><a href="https://kyounghwan01.github.io/blog/React/next/mui/#%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5">next.js에서 mui 사용하기</a></li>
<li><a href="https://www.ansonlowzf.com/create-a-website-with-material-ui-v5-nextjs/">Create A Simple Production Website With Material-UI v5 &amp; Nextjs.</a></li>
<li><a href="https://github.com/mui/material-ui/issues/29428/">#29428 Nextjs + Mui V5 emotion example is still showing className mismatch error</a></li>
<li><a href="https://github.com/mui/material-ui/issues/29742">#29742 [styled-engine-sc] MUI + styled-components + Next.js CSS rules ordering issue</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(Next.js) 에러로그 : window is not defined]]></title>
            <link>https://velog.io/@yunsungyang-omc/Next.js-%EC%97%90%EB%9F%AC%EB%A1%9C%EA%B7%B8-window-is-not-defined</link>
            <guid>https://velog.io/@yunsungyang-omc/Next.js-%EC%97%90%EB%9F%AC%EB%A1%9C%EA%B7%B8-window-is-not-defined</guid>
            <pubDate>Thu, 14 Apr 2022 04:19:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/7727140d-45cb-4f64-a71b-0016ac5d1fc4/image.png" alt=""></p>
<hr>
<p>최근 합류한 팀에서 첫 업무로 리액트 프로젝트를 Next.js 프로젝트로 이식하는 작업을 맡게 되었다.</p>
<p>최대한 업무의 공수를 줄일 수 있는 마이그레이션(정작 시도를 해보면, 쉽지 않다는 것을 알 수 있다.)과 새롭게 프로젝트를 설치하고 시작하는 인스텔레이션 두 방향으로 고민을 했다.</p>
<p>잠깐의 고민 끝에 프로젝트의 구조가 간단하지 않아 구조를 파악할 수 있는 후자가 시간은 더 걸리겠지만, 사용하지 않는 라이브러리나 변수를 제거하고 스토리보드 라이브러리도 이식할 수 있겠다 싶어서 후자의 방향으로 프로젝트를 시작하게 되었다. </p>
<p>Next.js는 리액트 라이브러리(라지만, 프레임워크라고 설명하는 분들도 많다. 하지만, 나는 라이브러리라고 생각한다)와는 달리 프레임워크다. </p>
<p>따라서, 반드시 지켜야 할 룰들이 있다보니 프로젝트의 구조를 전체적으로 이해하기 위해 노마드 코더 니꼬쌤의 강의를 수강하고 전체적인 진행방향을 이해하고 프로젝트를 진행했다. </p>
<p>이 글은 프로젝트 초반부인 현재 가장 큰 난제였던 문제에 대해 다룬 글이다. 
아마, Next.js를 처음 다루는 누군가에게는 반드시 도움이 될 것 같다.</p>
<hr>
<h3 id="😱-window-is-not-defined">😱 window is not defined</h3>
<p>Next.js는 리액트 프레임워크이고 프리랜더링을 지원한다. 프리랜더링을 위해 Next.js는 페이지를 랜더링할 때, SEO 최적화와 성능 개선을 위해 HTML을 먼저 생성한다는 이야기가 된다.</p>
<p>서버사이드 랜더링의 특징을 차용하고 있기 때문에, window를 참조하는 이벤트를 실행하는 코드를 사용하면 다음과 같은 에러를 만날 수 있다. </p>
<pre><code class="language-js">// components/Scroll.js
window.addEventListener(&quot;scroll&quot;, function() {
  console.log(&quot;scroll!&quot;)
});</code></pre>
<p><img src="https://velog.velcdn.com/images/yunsungyang-omc/post/3dce776f-8af9-4e7b-97bb-95db6fc63976/image.png" alt="">
Next.js는 기본적으로 Node.js 환경에서 동작한다. 
따라서, window 객체는 아직 정의되어 있는 것이 아니다. window는 오직 브라우저에서 참조가능한 것이기 때문이다. 이점에서 클라이언트 사이드 렌더링으로 진행되는 리액트 앱과 서버 사이드 렌더링 형식을 지원하는 Next.js의 차이점이 분명하게 드러난다. </p>
<p>window를 쓰려면 process.browser로 브라우저 상태인지 확인하는 등의 참고 작업이 필요하다. </p>
<p>이 점을 해결하기 위해 다양한 포스팅을 참조했다. 공통적으로 제시하는 해결방법은 다음과 같다.</p>
<hr>
<h3 id="1-첫-번째-해결책--type-of-분기처리-해주기">1. 첫 번째 해결책 : type of 분기처리 해주기</h3>
<pre><code class="language-js">if (typeof window !== &quot;undefined&quot;) {
  // Client-side-only code
}
// or
if (typeof window === &#39;undefined&#39;) return;
</code></pre>
<p>위 방법으로 클라이언트에서 렌더링할 때까지 기다렸다가, window를 참조할 수 있는 시점에 블락 안에 있는 코드를 실행하는 방식이다.</p>
<h3 id="2-두-번째-해결책--useeffect-안에서-코드를-사용하기">2. 두 번째 해결책 : useEffect 안에서 코드를 사용하기</h3>
<pre><code class="language-js">// components/Scroll.js
import React, { useEffect } from &quot;react&quot;;

 const Scroll = () =&gt; {
  useEffect(function mount() {
    function onScroll() {
      console.log(&quot;scroll!&quot;);
    }
    window.addEventListener(&quot;scroll&quot;, onScroll);
    return function unMount() {
      window.removeEventListener(&quot;scroll&quot;, onScroll);
    };
  });
  return null;
}
export default Scroll</code></pre>
<pre><code class="language-js">// pages/index.js

import Scroll from &quot;../components/Scroll&quot;;

export default function Home() {
  return (
    &lt;div style={{ minHeight: &quot;1000px&quot; }}&gt;
      &lt;h1&gt;Home&lt;/h1&gt;
      &lt;Scroll /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>useEffect 훅을 사용해서 브라우저에 렌더링 되는 시점에 코드를 실행시키는 것이다.
클라이언트 사이드에서 실행되어야 할 코드를 useEffect안에 고립시키고, 서버사이드에서 실행되어야 할 코드를 <code>getServerSideProps</code>단에 분리시키는 것이다. </p>
<pre><code class="language-js">const MyPage = () =&gt; {
  useEffect(() =&gt; {
    // client side stuff
    // window is accessible here.
  }, [])

  return (
    &lt;div&gt; ... &lt;/div&gt;
  )
}

MyPage.getServerSideProps = async () =&gt; {
  // server side stuff
}</code></pre>
<h3 id="3-세-번째-해결책--dynamic-사용하기">3. 세 번째 해결책 : Dynamic 사용하기</h3>
<p><a href="https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr">공식문서에서 제안하는 방법 살펴보기</a></p>
<pre><code class="language-js">// components/Scroll.js

function onScroll() {
  console.log(&quot;scroll!&quot;);
}

window.addEventListener(&quot;scroll&quot;, onScroll);

export default function Scroll() {
  return null;
}</code></pre>
<pre><code class="language-js">// pages/index.js

import dynamic from &quot;next/dynamic&quot;;

const Scroll = dynamic(
  () =&gt; {
    return import(&quot;../components/Scroll&quot;);
  },
  { ssr: false }
);

export default function Home() {
  return (
    &lt;div style={{ minHeight: &quot;1000px&quot; }}&gt;
      &lt;h1&gt;Home&lt;/h1&gt;
      &lt;Scroll /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>서버 사이드 랜더링 단에서 false 옵션을 주어, 클라이언트에서 랜더링될때 해당 컴포넌트가 마운트되고, 그 시점에서 코드가 실행되는 것이다. 2번째 방법과 유사한 방법으로 useEffect를 사용하지 않고도 구현가능한 방법이다.</p>
<hr>
<h2 id="jsencrypt-라이브러리를-사용했을-때-해결책">JSencrypt 라이브러리를 사용했을 때 해결책</h2>
<p>위에서 제기한 방법들은 공통적으로 서버 사이드 랜더링 단에서 코드가 실행되지 않도록 옵션을 두는 것이다. 하지만, 내 경우에는 커스텀 훅에서 사용하고 있는 <code>JSencrypt</code>라이브러리에서 에러가 발생하고 있어서 3가지 방법 중 어느 하나를 마땅하게 적용하기 어려웠다. </p>
<p>팀에서 컨플루언스에 에러로그를 작성하고 있는데, 다행히 컨플루언스에 팀장님께서 해결방법을 기록해두셨다. (😃 역시 기록은 생산을 낳는다더니..!)</p>
<h4 id="원래-코드">원래 코드</h4>
<pre><code class="language-js">import { JSEncrypt } from &#39;jsencrypt
const encrypt = new JSEncrypt()
const encryptedPassword = encrypt.encrypt(password)</code></pre>
<h4 id="import를-사용해서-해결하기">import를 사용해서 해결하기</h4>
<pre><code class="language-js">const JSEncrypt = (await import(&quot;jsencrypt&quot;)).default
const encrypt = new JSEncrypt()
const encryptedPassword = encrypt.encrypt(password)</code></pre>
<p>라이브러리가 임포트 될때까지 기다렸다가, 임포트되면 JSencrypy객체의 default값을 참고하는 것이다. 이 점에서 3번째 방식으로 제시된 dynamic import와 유사하다고 생각했다. </p>
<hr>
<h2 id="정리">정리</h2>
<p>Next.js의 캐릭터를 제대로 파악할 수 있는 문제였다고 생각한다. 물론, 검색 결과를 도입해서 해결하지는 못했지만, 팀 프로젝트의 이슈를 공유해야할 필요성에 대해서 몸으로 체감할 수 있는 기회익시도 했다. </p>
<p><code>Jsencrypt</code>라이브러리를 사용하는 누군가.. next.js를 마이그레이션하는 업무를 맡게된다면 도움이 되었으면 하는 마음을 담아 글을 남긴다. </p>
<h2 id="참고">참고</h2>
<ul>
<li><p><a href="https://dev.to/vvo/how-to-solve-window-is-not-defined-errors-in-react-and-next-js-5f97">https://dev.to/vvo/how-to-solve-window-is-not-defined-errors-in-react-and-next-js-5f97</a></p>
</li>
<li><p><a href="https://pks2974.medium.com/nextjs-%EB%A1%9C-static-site-%EB%A7%8C%EB%93%A4%EA%B8%B0-f9ab83f29e7">NextJS 로 Static Site 만들기</a></p>
</li>
<li><p><a href="https://stackoverflow.com/questions/49411796/how-do-i-detect-whether-i-am-on-server-on-client-in-next-js">Stack overflow 포스팅</a></p>
</li>
<li><p><a href="https://stackoverflow.com/questions/55151041/window-is-not-defined-in-next-js-react-app">Stack overflow 포스팅2</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(TS) Typescript : 간단요약]]></title>
            <link>https://velog.io/@yunsungyang-omc/TS-Typescript-%EA%B0%84%EB%8B%A8%EC%9A%94%EC%95%BD</link>
            <guid>https://velog.io/@yunsungyang-omc/TS-Typescript-%EA%B0%84%EB%8B%A8%EC%9A%94%EC%95%BD</guid>
            <pubDate>Wed, 06 Apr 2022 09:25:15 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.youtube.com/watch?v=xkpcNolC270">코딩 애플 원문 보기</a></p>
<hr>
<h2 id="타입스크립트란">타입스크립트란?</h2>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/8acd6fd2-50a4-470f-9ae4-5ac3ef5fbf9e/Screen%20Shot%202022-04-06%20at%205.05.17%20PM.png" alt=""></p>
<p>쉽게 말해 자바스크립트의 타입 부분을 강화한 업그레이드 버전이라고 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/ad5be0b7-077f-41a5-8229-ccfd773d7ad9/Screen%20Shot%202022-04-06%20at%205.06.08%20PM.png" alt=""></p>
<p>자바스크립트는 자료의 타입을 변환해주는 다이나믹 타이핑이 가능한데, 이러한 편리성이 코드를 만 줄 이상 넘어가는 실무단계에서는 도리어 불편을 주기도 한다. </p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/60b5a3a0-8d83-45b6-a598-b2405bbfe25a/Screen%20Shot%202022-04-06%20at%205.06.17%20PM.png" alt=""></p>
<p>타입스크립트는 자바스크립트의 유연성을 덜고, 엄격하게 타입을 지정함으로써 오류를 줄일 수 있다. </p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/722c1fbc-87b7-4345-a489-a8cb4c77ad43/Screen%20Shot%202022-04-06%20at%205.06.43%20PM.png" alt=""></p>
<p>자바스크립트의 추상적인 에러메시지도 타입스크립트에서는 </p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/cc2e5d43-f11c-4de0-9a59-7e05eb08655d/Screen%20Shot%202022-04-06%20at%205.07.35%20PM.png" alt=""></p>
<p>굉장히 구체적이기 때문에 에러핸들링이 쉬워진다. (토익점수 ㅎㅎ..)</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/6ddd0b82-5c20-4c25-8b57-a3ab6682445f/Screen%20Shot%202022-04-06%20at%205.08.52%20PM.png" alt=""></p>
<p>타입스크립트 설치는 간단하지만, 노드js는 반드시 최신 버전으로 설치돼 있어야 한다. (주된 에러의 원인)</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/6191a265-fa50-43da-a7a1-25f3a1dfb7bb/Screen%20Shot%202022-04-06%20at%205.09.31%20PM.png" alt=""></p>
<p>파일의 확장자로 .ts를 사용한다. </p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/ab9c2fe8-0dc7-4fde-bf41-8af368b06e66/Screen%20Shot%202022-04-06%20at%205.09.42%20PM.png" alt=""></p>
<p>프로젝트 루트 디렉토리에 tsconfig.json을 생성한다. </p>
<h3 id="tsconfigjson">tsconfig.json</h3>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/d3fa1400-8ab5-4e45-aebe-027c4d38ff01/Screen%20Shot%202022-04-06%20at%205.09.54%20PM.png" alt=""></p>
<p>브라우저는 타입스크립트가 아닌 자바스크립트 파일을 읽는다. 때문에 파일 변환이 필요하다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/139b59c8-09b0-484a-a0bd-85f3961bd264/Screen%20Shot%202022-04-06%20at%205.10.26%20PM.png" alt=""></p>
<p>터미널에서 tsc-w를 입력하면 자동으로 트랜스파일링을 진행한다. 에디터가 종료되기 전까지 다른 세팅이 없다면 계속 작동한다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/b0f2e67d-bacb-44b8-9f5a-9c1c99e20d3b/Screen%20Shot%202022-04-06%20at%205.11.02%20PM.png" alt=""></p>
<p>컴파일링 옵션을 설정은 루트 디렉토리에 위치한 파일에 설정할 수 있다. (잊지말자)</p>
<hr>
<h2 id="타입-지정-문법">타입 지정 문법</h2>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/6d1c98ea-4215-433f-a871-00085ea07a53/Screen%20Shot%202022-04-06%20at%205.12.14%20PM.png" alt=""></p>
<p>타입은 다양하게 지정할 수 있다. 만약 string으로 타입을 지정했다면,</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/b1c95579-9639-4f9c-ad2e-b4dd134681fc/Screen%20Shot%202022-04-06%20at%205.11.47%20PM.png" alt=""></p>
<p>변수로 문자열만 들어올 수 있다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/8403ec47-476a-4390-a9c8-1e72c5be626e/Screen%20Shot%202022-04-06%20at%205.12.39%20PM.png" alt=""></p>
<p>배열은 문자 ‘array’가 아니라 대괄호([])를 사용한다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/b498be6f-a1e9-4113-8bda-e46b6530f599/Screen%20Shot%202022-04-06%20at%205.13.05%20PM.png" alt=""></p>
<p>배열의 개별 요소에 대해서도 타입을 지정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/db0e5929-a7fd-46ff-b792-2c4a42426fdd/Screen%20Shot%202022-04-06%20at%205.13.38%20PM.png" alt=""></p>
<p>객체도 배열처럼 타입을 지정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/e6124487-b322-4395-a717-7101b6ec455c/Screen%20Shot%202022-04-06%20at%205.13.56%20PM.png" alt=""></p>
<p>optional 속성은 ? 을 이용해서 지정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/1855a568-b0c3-4cf8-b08f-940d42ccd341/Screen%20Shot%202022-04-06%20at%205.14.21%20PM.png" alt=""></p>
<p>한 가지 타입이 아니라, 복수의 타입을 지정해야 한다면 유니온 타입(||, or)으로 지정할 수 있다. </p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/71cd2a05-e9ff-4a74-98a9-6cc28febef15/Screen%20Shot%202022-04-06%20at%205.15.16%20PM.png" alt=""></p>
<p>타입은 변수에 담아서 쓸 수 있다. </p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/c67d46ed-21e9-4204-86e5-6dbbe120be9e/Screen%20Shot%202022-04-06%20at%205.15.42%20PM.png" alt=""></p>
<p>배열과 객체와 마찬가지로 함수도 타입을 지정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/5f049e37-6e25-498a-9ad4-001136a6a569/Screen%20Shot%202022-04-06%20at%205.15.59%20PM.png" alt=""></p>
<p>함수의 리턴값도 타입을 지정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/a662bd75-a40c-46fa-92b2-95e338d2f496/Screen%20Shot%202022-04-06%20at%205.16.47%20PM.png" alt=""></p>
<p>배열의 각 요소에 대해서 타입을 지정해 엄격하게 관리할 수 있다 : tuple타입 (파이썬의 향기가..)</p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/20670b89-16ed-419c-8c66-9d0f739b75ef/Screen%20Shot%202022-04-06%20at%205.18.00%20PM.png" alt=""></p>
<p>객체에 타입 지정해야할 속성이 너무 많으면, key와 value의 타입을  미리 지정해주면 된다. </p>
<p><img src="https://velog.velcdn.com/cloudflare/yunsungyang-omc/32fb72cb-4763-44d9-ae6d-8cd550bedfd2/Screen%20Shot%202022-04-06%20at%205.18.48%20PM.png" alt=""></p>
<p>클래스도 타입 지정이 가능하다.</p>
]]></description>
        </item>
    </channel>
</rss>