<?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>안녕하세요 개발자 윤승록입니다. 내 성장을 가시적으로 기록하기 위해 블로그를 운영중입니다. Dive your heads first! 너.믿.나.믿. (너를믿는나를믿어!)</description>
        <lastBuildDate>Mon, 12 Jan 2026 09:20:29 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>레테의 펜시브</title>
            <url>https://velog.velcdn.com/images/seungrok-yoon/profile/d58c8f85-a24a-4c4a-a3a8-0dc3264dade5/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 레테의 펜시브. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/seungrok-yoon" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[VPN 노드에서 실행되고 있는 내부용 서비스를 로컬머신에서 보는 법]]></title>
            <link>https://velog.io/@seungrok-yoon/VPN-%EB%85%B8%EB%93%9C%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89%EB%90%98%EA%B3%A0-%EC%9E%88%EB%8A%94-%EB%82%B4%EB%B6%80%EC%9A%A9-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%91%EA%B7%BC%ED%95%98%EB%82%98</link>
            <guid>https://velog.io/@seungrok-yoon/VPN-%EB%85%B8%EB%93%9C%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89%EB%90%98%EA%B3%A0-%EC%9E%88%EB%8A%94-%EB%82%B4%EB%B6%80%EC%9A%A9-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%91%EA%B7%BC%ED%95%98%EB%82%98</guid>
            <pubDate>Mon, 12 Jan 2026 09:20:29 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 사내 Synology NAS에서 돌아가는 Portainer GUI를 로컬머신의 브라우저에서 보고싶어 삽질한 이야기다.</p>
</blockquote>
<p>간단한 웹크롤러, 알림 서비스에 CI/CD를 붙이려고 부단히 노력했다.</p>
<h2 id="nas에-docker-registry-구축">NAS에 Docker registry 구축</h2>
<p>ECR, DockerHub를 사용하는 것은 사치라 생각해 <code>registy</code> 이미지를 이용해, NAS에 이미지 레지스트리를 구축하고, GithubAction은 여기에 빌드한 이미지를 push한다.</p>
<p>docker 를 이용한 CI/CD는 처음에는 어려웠지만, 막상 경험해보니 이만한 기술도 없다. docker 사랑해요. </p>
<h2 id="새로운-이미지-자동으로-pull받아-실행하기">새로운 이미지 자동으로 pull받아 실행하기</h2>
<p>이제 CICD의 마지막 관문, 어떻게 push된 이미지를 감지해서, NAS에서 새 이미지로 갈아끼워 실행할 것이냐?</p>
<p>선택지는 3가지 정도였다(from Gemini)</p>
<ul>
<li>WatchTower사용 (Polling. 내가 원하는 방식 아님)</li>
<li>Portainer 사용 (기존방식. 내가 원하는 감지 후 pull가능) =&gt; 원하는 기능이었지만, Portainer Community Edition에서는 막혀있는 기능이었다. 웹훅(Github Action마지막에, Portainer의 웹훅 URL을 등록하면, 여기로 POST 웹훅요청을 보내서, Portainer가 레지스트리에서 최신 이미지를 pull하는 기능) 기능또한 Community Edition에서는 지원하지않는다.</li>
<li>독자 스크립트 (스크립트 유지보수에 시간을 쓰고 싶지 않았다)</li>
</ul>
<p>이 세 방법 중, 한 번 설치해 본 적 있는 Portainer를 시도해보기로 했다. (Portainer 설치와 사용법은 다음에 정리하겠다)</p>
<p>GUI환경을 지원하니 최대한 GUI환경을 이용하고싶었으나 문제는 NAS는 현재 VPN Node라는것.</p>
<p>SSH 포트는 익명의 외부자에 노출하지 않는 것을 목적으로 VPN을 적용했다.</p>
<p>그렇기에 Portainer 포트를 외부에 노출시키면 나는 접근이 편리하겠지만, 다른 사람들도 접근할 수 있다는 점에서 부적합하다는 생각이 들었다.</p>
<h3 id="로컬포트포워딩-써보셨나요">로컬포트포워딩 써보셨나요?</h3>
<pre><code>docker exec -it [docker container] ssh -p &lt;SSHPortOfRemoteMachine&gt; -L 0.0.0.0:&lt;PortOfMyLocalMachine&gt;:127.0.0.1:&lt;PortOfInternalServiceOfRemoteMachine&gt; &lt;user&gt;@&lt;TailscaleIP&gt;</code></pre><p>VPN으로 원격 서버를 설정한 이유는, 서버를 무차별적인 외부 ssh 접근으로부터 막기위함이었다.</p>
<p>Tailscale을 사용해서, 내 Mac(로컬머신)과 원격 서버의 연결은 SSH로 가능했다.</p>
<p>엥, 그러면 Portainer같은 내부용 모니터링 서비스는 어디서 보나?</p>
<p>아아... SSH 는 <code>로컬포트포워딩(Local Port Forwarding)</code>을 <code>-L</code> 옵션으로 지원한다. </p>
<p>그래서 원격호스트에서 돌아가는 내부 서비스포트를 내 로컬 포트에 연결하면, 로컬머신의 localhost:&lt;원격호스트내부서비스포트&gt; 에서 바로 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WEB/DOM]-HTMLDivElement에서 EventTarget까지]]></title>
            <link>https://velog.io/@seungrok-yoon/WEBDOM-HTMLDivElement%EC%97%90%EC%84%9C-EventTarget%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@seungrok-yoon/WEBDOM-HTMLDivElement%EC%97%90%EC%84%9C-EventTarget%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Sat, 27 Dec 2025 04:34:15 GMT</pubDate>
            <description><![CDATA[<h2 id="htmldivelement-상속구조">HTMLDivElement 상속구조</h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget">EventTarget</a> - <a href="https://developer.mozilla.org/en-US/docs/Web/API/Node">Node</a> - <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element">Element</a> - <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement">HTMLElement</a> - <a href="https://developer.mozilla.org/ko/docs/Web/API/HTMLDivElement">HTMLDivElement</a></p>
<p> HTMLDivElement 는 속성과 매서드를 부모인 HTMLElement에서 상속을 받음. 따라서 HTMLElement를 살펴봐야한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(작성중) 비동기 네트워크 요청 과정에서 발생하는 에러처리를 더 잘하고 싶다feat. AxiosError 타입에 대해 ]]></title>
            <link>https://velog.io/@seungrok-yoon/%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9A%94%EC%B2%AD-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC%EB%A5%BC-%EB%8D%94-%EC%9E%98%ED%95%98%EA%B3%A0-%EC%8B%B6%EB%8B%A4feat.-AxiosError-%ED%83%80%EC%9E%85%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@seungrok-yoon/%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9A%94%EC%B2%AD-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC%EB%A5%BC-%EB%8D%94-%EC%9E%98%ED%95%98%EA%B3%A0-%EC%8B%B6%EB%8B%A4feat.-AxiosError-%ED%83%80%EC%9E%85%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Fri, 10 Jan 2025 02:01:44 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/axios/axios/issues/3612">https://github.com/axios/axios/issues/3612</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[git] 삭제된 원격브랜치를 로컬에도 반영하는 편한 방법]]></title>
            <link>https://velog.io/@seungrok-yoon/git-%EC%82%AD%EC%A0%9C%EB%90%9C-%EC%9B%90%EA%B2%A9%EB%B8%8C%EB%9E%9C%EC%B9%98%EB%A5%BC-%EB%A1%9C%EC%BB%AC%EC%97%90%EB%8F%84-%EB%B0%98%EC%98%81%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seungrok-yoon/git-%EC%82%AD%EC%A0%9C%EB%90%9C-%EC%9B%90%EA%B2%A9%EB%B8%8C%EB%9E%9C%EC%B9%98%EB%A5%BC-%EB%A1%9C%EC%BB%AC%EC%97%90%EB%8F%84-%EB%B0%98%EC%98%81%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 10 Sep 2024 06:34:05 GMT</pubDate>
            <description><![CDATA[<p>맨날 병합하고 삭제된 브랜치를 수동으로 삭제하다가 git prune도 귀찮아졌다. </p>
<p>그래서 아래 스크립트를 추가해서 사용중인데 괜찮은 것 같다.</p>
<p><code>pull-clean</code>을 하면 main브랜치로 옮겨가고, 원격에서 없어진 브랜치들을 로컬에서도 삭제하는 방식이다.</p>
<pre><code>&quot;list-gone-branches&quot;: &quot;git fetch -p &amp;&amp; git branch -vv | awk &#39;/: gone]/{print $1}&#39;&quot;,
&quot;delete-gone-branches&quot;: &quot;git fetch -p &amp;&amp; git branch -vv | awk &#39;/: gone]/{print $1}&#39; | xargs git branch -D&quot;,
&quot;pull-clean&quot;: &quot;git switch main | npm run delete-gone-branches&quot;</code></pre><p>=&gt; 2024.12.11 수정
위 스크립트에서 bash 가 한국어 설정이 되어 있을 때와 이미 브랜치가 삭제되어서 없는 상황에 대한 처리가 되지 않아 제대로 로컬 브랜치가 지워지지 않는 문제가 있었다.</p>
<p>정규식을 통해 한글과 영어로 된 브랜치 삭제/없음 케이스들을 처리하자. 영어로는 삭제됨과 없음이 &quot;gone&quot;으로 퉁쳐서 표현된다.</p>
<pre><code>&quot;delete-gone-branches&quot;: &quot;git fetch -p &amp;&amp; git branch -vv | awk &#39;/: 삭제됨]|: gone]|: 없음]/{print $1}&#39; | xargs git branch -D&quot;</code></pre><h2 id="더-좋은-방법이-있지-않을까">더 좋은 방법이 있지 않을까?</h2>
<p>있다. 내가 생각하기에는 아래 방법이 더 좋은 것 같다. </p>
<p>현재 husky를 활용해서 <code>pre-commit</code>과 <code>pre-push</code> 훅을 프리티어와 린트검사에 사용하고 있다. </p>
<p>그런데 이것 말고도 <code>post-merge</code>라는 훅이 하나 더 있다. 자세한 내용은 <a href="">(링크)깃북</a>을 읽어보고 일단 따라해보자. </p>
<h3 id="husky-설치-및-초기화">husky 설치 및 초기화</h3>
<pre><code>npm install husky --save-dev
npx husky install

yarn add husky -D 
yarn husky init</code></pre><h3 id="깃훅-추가">깃훅 추가</h3>
<p>파일 이름을 <code>post-merge</code>로 짓는다.</p>
<pre><code># .husky/post-merge
#!/bin/sh
. &quot;$(dirname &quot;$0&quot;)/_/husky.sh&quot;

# This command will be run after pulling changes
# npm 사용 시
npm run delete-gone-branches

#yarn 사용 시
yarn run delete-gone-branches</code></pre><p>위에 언급한 delete-gone-branches를 등록해주자.</p>
<h2 id="이제-사용해보자">이제 사용해보자</h2>
<p>브랜치 머지 후, main에서 pull을 받아보자.</p>
<p>나는 post-merge가 더 좋아보여서 사용했지만, post-checkout을 사용해도 괜찮아보인다.</p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://typicode.github.io/husky/get-started.html">Husky 공식문서- get-started</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Prisma] Prisma Client & Prisma Server(작성중)]]></title>
            <link>https://velog.io/@seungrok-yoon/Prisma-Prisma-Client-Prisma-Server</link>
            <guid>https://velog.io/@seungrok-yoon/Prisma-Prisma-Client-Prisma-Server</guid>
            <pubDate>Mon, 19 Aug 2024 02:49:52 GMT</pubDate>
            <description><![CDATA[<h1 id="prisma-클라이언트와-서버">Prisma 클라이언트와 서버</h1>
<h2 id="도입-배경">도입 배경</h2>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/cb8b3ffb-7206-4236-9036-43ddeae97e3c/image.png" alt=""></p>
<p>나는 솔직히 SQL에 문외한이다. 백엔드 아키텍처도 잘 모른다. 기껏 해봐야 정보처리기사 자격증을 취득할 때 공부한게 다였다. </p>
<p>그런데 이번 회사 신제품에 <code>Next.js</code> + <code>Supabase</code>를 기술스택으로 선정하여 빠른 MVP 구축을 개발하기로 협의되었다.</p>
<p>문제는 내가 회사 유일의 개발자라는 점ㅜㅜ. 그래서 SQL때문에 백엔드 개발이 늦어지면 안되겠다고 판단했다. 내 단점은 생각이 너무 많은 점인데, 백엔드 걱정하면서 또 코드 쓰기를 주저하길래 <code>일단 사용해보면서 익히기</code>로 하고 prisma를 설치해버렸다.</p>
<p>Prisma는 ORM type-safe하게 데이터베이스에 접근하는 방법이 있었다. 도입하여 공부중이다.</p>
<h2 id="nextjs에서의-prisma">Next.js에서의 Prisma</h2>
<p>Next.js에서 Prisma는 서버사이드 코드에서 활용할 수 있다.</p>
<p>클라이언트단에서 미리 내가 정해놓은 서버사이드 API로 요청을 보내고, 이 요청을 처리하는 코드에서 Prisma를 사용하여 Supabase로 트랜잭션 요청을 보내는 방식이다. 이 과정에서 <code>Prisma Client</code>가 활용이 된다.</p>
<p>Prisma Client 는 내가 사용하는 데이터베이스 스키마대로 쿼리를 만들어주는 <code>type-safe</code> 쿼리 빌더이다. 어렵다. 단순히 말해서 <a href="https://www.prisma.io/docs/orm/overview/prisma-in-your-stack/rest#supported-libraries">&quot;Prisma Client 는 데이터베이스에 쿼리를 보내는 역할만 한다(링크)&quot;</a>.</p>
<p><code>schema.prisma</code>파일에 내가 사용하는 데이터베이스 모델을 명시해주면 그에 따라서 맞는 API가 Prisma Client로 생성되는 것이다. </p>
<p>이 스키마 파일에 명시되는 모델은 <a href="https://www.prisma.io/docs/getting-started/quickstart">[Model의 두 가지 목적 - Prisma 공식문서참고]</a>이 있다.</p>
<ul>
<li>Prisma를 통해 통신할 데이터베이스 테이블을 표현</li>
<li>Prisma Client API 생성(generate)의 기반이 된다.</li>
</ul>
<pre><code class="language-cli">npx prisma generate</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 객체를 prop으로 하위 컴포넌트로 전달할 때 컴포넌트 메모]]></title>
            <link>https://velog.io/@seungrok-yoon/React-%EA%B0%9D%EC%B2%B4%EB%A5%BC-prop%EC%9C%BC%EB%A1%9C-%ED%95%98%EC%9C%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A1%9C-%EC%A0%84%EB%8B%AC%ED%95%A0-%EB%95%8C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A9%94%EB%AA%A8</link>
            <guid>https://velog.io/@seungrok-yoon/React-%EA%B0%9D%EC%B2%B4%EB%A5%BC-prop%EC%9C%BC%EB%A1%9C-%ED%95%98%EC%9C%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A1%9C-%EC%A0%84%EB%8B%AC%ED%95%A0-%EB%95%8C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A9%94%EB%AA%A8</guid>
            <pubDate>Tue, 13 Aug 2024 06:29:18 GMT</pubDate>
            <description><![CDATA[<h2 id="이전이야기">이전이야기</h2>
<p><a href="https://velog.io/@seungrok-yoon/React-%EC%A4%91%EC%B2%A9%EB%90%9C-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%9B%85%EC%9D%98-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81">[React] 중첩된 커스텀 훅의 리렌더링</a></p>
<h2 id="memo-를-활용하여-불필요한-렌더링-방지">memo 를 활용하여 불필요한 렌더링 방지</h2>
<pre><code class="language-tsx">import styled from &#39;@emotion/styled&#39;;
import useNestedHook from &#39;@Hook/nested-custom-hook/useNestedHook&#39;;
import { memo } from &#39;react&#39;;
import { v4 as uuidv4 } from &#39;uuid&#39;;

export default function NestedCustomHook() {
  const formId = `user_${uuidv4()}`;
  const { formValues, setter } = useNestedHook();

  return (
    &lt;VerticalFlexContainer&gt;
      &lt;form&gt;
        &lt;label htmlFor={`name_${formId}`}&gt;
          Name:&amp;nbsp;
          &lt;input
            id={`name_${formId}`}
            name=&quot;name&quot;
            type=&quot;text&quot;
            value={formValues.name}
            onChange={(e) =&gt; {
              setter.setName(e.target.value);
            }}
          /&gt;
        &lt;/label&gt;
        &lt;label htmlFor={`email_${formId}`}&gt;
          Email:&amp;nbsp;
          &lt;input
            id={`email_${formId}`}
            name=&quot;email&quot;
            type=&quot;text&quot;
            value={formValues.email}
            onChange={(e) =&gt; {
              setter.setEmail(e.target.value);
            }}
          /&gt;
        &lt;/label&gt;
      &lt;/form&gt;
      &lt;HorizontalFlexContainer gap=&quot;20px&quot;&gt;
        &lt;MemoizedSpan label=&quot;Name&quot; data={formValues.name} /&gt;
        &lt;MemoizedSpan label=&quot;email&quot; data={formValues.email} /&gt;
      &lt;/HorizontalFlexContainer&gt;
    &lt;/VerticalFlexContainer&gt;
  );
}

const MemoizedSpan = memo(
  ({ label, data }: { label: string; data: string }) =&gt; {
    console.log(&#39;MemoizedSpan&#39;, label, data);
    return (
      &lt;CustomSpan&gt;
        {label}: {data}
      &lt;/CustomSpan&gt;
    );
  }
);

const HorizontalFlexContainer = styled.div&lt;{ gap: string }&gt;`
  display: flex;
  gap: ${(props) =&gt; props.gap};
`;

const VerticalFlexContainer = styled.div`
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 12px;
`;

const CustomSpan = styled.span`
  display: inline-block;
  background-color: &#39;red&#39;;
`;
</code></pre>
<p><code>MemoizedSpan</code>이라는 컴포넌트를 임의로 선언해 보았다(emotion/styled는 무시하자). 공식문서대로라면, prop이 바뀌지 않는 부분의 컴포넌트는 리렌더링이 되지 않아야 한다.</p>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/3e1178e6-f3de-4909-b9d4-b9af72ef9472/image.gif" alt=""></p>
<p>실제로 잘 동작한다. Name 인풋을 수정할 때 email 쪽의 Span컴포넌트는 리렌더링 되지 않는다.</p>
<blockquote>
<p>물론 memo를 사용하더라도 memo된 컴포넌트의 내부 state가 변경되거나 사용 중인 context가 변경되면 리렌더링 된다는 점에는 주의해야 한다.</p>
</blockquote>
<h2 id="객체를-props로-넘기는-경우작성중">객체를 props로 넘기는 경우(작성중)</h2>
<p>객체를 props로 넘기는 경우는 컴포넌트를 작성하면서 자주 접하는 상황이다.</p>
<p>memo는 내부적으로 <code>Object.is</code> 로 비교를 하여 prop이 이전 렌더링 때의 prop과 동일한 지를 비교한다. </p>
<p>그러면 <code>Object.is</code>는 어떻게 동작을 할까?</p>
<h3 id="objectis">Object.is</h3>
<p>내가 궁금한 점은 중첩객체일 때 과연 Object.is가 제대로 동작하느냐였다. 실무에서 prop에 중첩객체를 전달해야 하는 경우가 잦았는데 이 경우에도 제대로 객체 간 비교를 수행하는지 궁금했다.</p>
<pre><code class="language-js">console.log(Object.is({ name: &#39;child1&#39; }, { name: &#39;child1&#39; }));
//false</code></pre>
<h3 id=""></h3>
<p><a href="https://ko.react.dev/reference/react/memo#minimizing-props-changes">props변경을 최소화하기</a></p>
<h2 id="나는-상태를-변경했는데-리렌더링이-되지-않는-경우">나는 상태를 변경했는데, 리렌더링이 되지 않는 경우</h2>
<h2 id="prop이-변경되지-않은-컴포넌트가-함께-리렌더링이-되는-경우">prop이 변경되지 않은 컴포넌트가 함께 리렌더링이 되는 경우</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 중첩된 커스텀 훅의 리렌더링]]></title>
            <link>https://velog.io/@seungrok-yoon/React-%EC%A4%91%EC%B2%A9%EB%90%9C-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%9B%85%EC%9D%98-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81</link>
            <guid>https://velog.io/@seungrok-yoon/React-%EC%A4%91%EC%B2%A9%EB%90%9C-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%9B%85%EC%9D%98-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81</guid>
            <pubDate>Mon, 12 Aug 2024 09:42:02 GMT</pubDate>
            <description><![CDATA[<h1 id="중첩된-커스텀-훅에서-상태-변화가-일어날-경우-리렌더링은-어떠한-방식으로-발생하는가">중첩된 커스텀 훅에서 상태 변화가 일어날 경우, 리렌더링은 어떠한 방식으로 발생하는가?</h1>
<p><a href="https://github.com/SeungrokYoon/react-from-scratch/issues/20">내 깃헙 이슈</a>
<a href="">PR</a></p>
<pre><code class="language-tsx">//NestedCustomHook 페이지
import styled from &#39;@emotion/styled&#39;;
import useNestedHook from &#39;@Hook/nested-custom-hook/useNestedHook&#39;;
import { v4 as uuidv4 } from &#39;uuid&#39;;

export default function NestedCustomHook() {
  const formId = `user_${uuidv4()}`;
  const { formValues, setter } = useNestedHook();

  return (
    &lt;VerticalFlexContainer&gt;
      &lt;form&gt;
        &lt;label htmlFor={`name_${formId}`}&gt;
          Name:&amp;nbsp;
          &lt;input
            id={`name_${formId}`}
            name=&quot;name&quot;
            type=&quot;text&quot;
            value={formValues.name}
            onChange={(e) =&gt; {
              setter.setName(e.target.value);
            }}
          /&gt;
        &lt;/label&gt;
        &lt;label htmlFor={`email_${formId}`}&gt;
          Email:&amp;nbsp;
          &lt;input
            id={`email_${formId}`}
            name=&quot;email&quot;
            type=&quot;text&quot;
            value={formValues.email}
            onChange={(e) =&gt; {
              setter.setEmail(e.target.value);
            }}
          /&gt;
        &lt;/label&gt;
      &lt;/form&gt;
      &lt;HorizontalFlexContainer gap=&quot;20px&quot;&gt;
        &lt;CustomSpan&gt;Name: {formValues.name}&lt;/CustomSpan&gt;
        &lt;CustomSpan&gt;Email: {formValues.email}&lt;/CustomSpan&gt;
      &lt;/HorizontalFlexContainer&gt;
    &lt;/VerticalFlexContainer&gt;
  );
}

const HorizontalFlexContainer = styled.div&lt;{ gap: string }&gt;`
  display: flex;
  gap: ${(props) =&gt; props.gap};
`;

const VerticalFlexContainer = styled.div`
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 12px;
`;

const CustomSpan = styled.span`
  display: inline-block;
  background-color: &#39;red&#39;;
`;
</code></pre>
<pre><code class="language-tsx">//useNestedCustomHook.tsx
import useEmail from &#39;./useEmail&#39;;
import useName from &#39;./useName&#39;;

interface FormValues {
  name: string;
  email: string;
}

interface Setter {
  setName: React.Dispatch&lt;React.SetStateAction&lt;string&gt;&gt;;
  setEmail: React.Dispatch&lt;React.SetStateAction&lt;string&gt;&gt;;
}

export default function useNestedHook(): {
  formValues: FormValues;
  setter: Setter;
} {
  console.log(&#39;useNestedHook Rerender&#39;);
  const { name, setName } = useName();
  const { email, setEmail } = useEmail();

  return { formValues: { name, email }, setter: { setName, setEmail } };
}
</code></pre>
<pre><code class="language-tsx">//useName.tsx
import { useState } from &#39;react&#39;;

export default function useName() {
  console.log(&#39;useName rerender&#39;);
  const [name, setName] = useState(&#39;&#39;);

  return { name, setName };
}
</code></pre>
<pre><code class="language-tsx">//useEmail.tsx
import { useState } from &#39;react&#39;;

export default function useEmail() {
  console.log(&#39;useEmail rerender&#39;);
  const [email, setEmail] = useState(&#39;&#39;);

  return { email, setEmail };
}
</code></pre>
<p>위 예시 코드는 React의 커스텀 훅 동작에 대한 이해를 위해서 예시로 작성한 코드이다. </p>
<p>실행을 하게 되면 아래와같은 리렌더링 양상을 보여준다.</p>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/075c9e1b-1462-49e4-8543-b043c1c03b87/image.gif" alt=""></p>
<h2 id="왜-이럴까">왜 이럴까?</h2>
<p><a href="https://react.dev/learn/render-and-commit">Render and Commit</a></p>
<p>정답은 리액트의 렌더링 과정에 있었다. </p>
<p>커스텀 훅은 상태 관리 로직을 재사용성을 위해 분리한 것에 지나지 않는다. 중첩된 커스텀훅을 호출하는 컴포넌트는 커스텀 훅의 상태들을 참조(구독)하고 있는 것이다.</p>
<h3 id="triggering-a-render">Triggering a render</h3>
<p>상태가 변경되어 트리거된 리렌더는 큐에 등록이 되고, 이후 상태가 변경되어 리렌더링이 필요한 컴포넌트가 <code>재호출</code>된다. 재호출된다는 것은 함수 내부 라인을 한 줄 한 줄 다 훑으며 실행이 된다는 것이다. 그렇기에 커스텀 훅에서 리턴된 상태값을 참조하는 페이지 컴포넌트가 리렌더링 대상이 되었다.</p>
<h3 id="rendering-the-component">Rendering the component</h3>
<p>첫 렌더링 시에는 루트 컴포넌트를 호출하고,</p>
<p>이후 렌더에서는 렌더링을 트리거한 상태 업데이트를 지닌 컴포넌트를 호출한다. 이 과정은 재귀적이어서, 만약 A컴포넌트가 호출되었다면, 하위 컴포넌트들도 다시 재호출된다. </p>
<p>이 작업은 한 눈에 <code>비효율적</code>임을 알 수 있다. </p>
<p>그래서 리액트는 useMemo, useCallback 등의 불필요한 리렌더링을 방지하는 API를 제공하고 있다.</p>
<h3 id="committing-to-the-dom">Committing to the DOM</h3>
<p>컴포넌트들을 렌더링(호출)했다면, 이제 DOM을 변경하게 된다. </p>
<ul>
<li><p>첫 렌더 시에는 appendChild() DOM API를 통해 화면에 생성한 노드를 추가한다. </p>
</li>
<li><p>리렌더링 시에는, DOM과 최신 렌더링 결과를 맞추기 위해 최소한의 작업을 진행한다. </p>
</li>
</ul>
<p>리액트는 렌더와 렌더 사이에 차이가 생긴 DOM 노드만 변경한다. 컴포넌트 호출은 하지만, Commit 단계에서 컴포넌트와 매칭되는 DOM을 조작하는 건 일부만 진행할 수 있다는 이야기이다.</p>
<h3 id="브라우저-페인트">브라우저 페인트</h3>
<p>DOM 업데이트를 하고 나면, 화면을 리페인트한다.</p>
<h2 id="나의-오개념">나의 오개념</h2>
<p>내부 커스텀 훅의 상태를 바꾸니까 리렌더링이 내부 커스텀 훅에서 먼저 일어날 것이라 생각한 것이 오개념이었다.</p>
<p>리렌더링은 별도의 큐잉으로 진행이된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Fetch API의 credentials]]></title>
            <link>https://velog.io/@seungrok-yoon/Fetch-API%EC%9D%98-credentials</link>
            <guid>https://velog.io/@seungrok-yoon/Fetch-API%EC%9D%98-credentials</guid>
            <pubDate>Fri, 09 Aug 2024 08:43:04 GMT</pubDate>
            <description><![CDATA[<h2 id="참고자료">참고자료</h2>
<p><a href="https://www.tutorialspoint.com/ajax/fetch_api_credentials.htm">tutorialspoint, Fetch API - Credentials</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">MDN, Using the Fetch API</a></p>
<h2 id="미안해-fetch-난-널-아직-잘-몰라">미안해 fetch... 난 널 아직 잘 몰라</h2>
<blockquote>
<p>나는 fetch API를 잘 모르는 것 같다. 항상 하던대로 url에 쿼리 스트링, body에 JSON.stringify()를 사용한 작업만 하던 내 스스로에 부족함을 느꼈다. fetch API에 대해 궁금해져 찾아본 내용을 기록해 본다.</p>
</blockquote>
<p>fetch API는 비동기로 동작하며, 요청에 대한 Response 객체를 리턴하는 Promise를 리턴하며, 해당 Promise가 resolve되었을 시에 얻게되는 Response객체의 비동기로 동작하는 json()매서드를 통해서 다시 body객체를 얻는 과정(헥헥...)은 프론트엔드 웹개발을 학습하다보면 자주 접하게 되는 부분이다.</p>
<p>그런데 내게 credentials옵션은 낯설었다.</p>
<pre><code>fetch(&#39;https://example.com/data&#39;, {
  credentials: &#39;include&#39;
})
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(data))
  .catch(error =&gt; console.error(&#39;Error:&#39;, error));</code></pre><p>credentials라는 옵션은 어떤 역할을 하며, 언제 사용해야할까?</p>
<h2 id="credentials-옵션">credentials 옵션</h2>
<p>개발을 하면서 옵션의 기본값을 알아두면 유용하다는 것을 절감하고 있다. 다행히도 credentials 옵션의 값은 3종류밖에 안된다.</p>
<ul>
<li><p>same-origin − (기본값)
페이지를 요청할 때 <code>same-origin</code> 대상의 요청에서만 credential 데이터를 전달하는 것이다. same origin URL들로부터 온 credentials 데이터만 사용한다.</p>
</li>
<li><p>omit − 브라우저가 리소스를 요청할 때 credentials를 제거한다.는 물론이고, Response의 credentials도 무시한다.</p>
</li>
<li><p>include − 이 옵션을 사용하면, <code>same-origin</code>일 때도, <code>corss-origin</code>일 때도 credentials를 포함하여 요청을 보내게 된다. 더불어 Response의 credentials도 사용한다.</p>
</li>
</ul>
<h2 id="credentials에는-어떤-데이터들이-해당되는가">credentials에는 어떤 데이터들이 해당되는가?</h2>
<p>그래, 요청을 보내고 받을 때 credentials를 사용할 지 말 지 여부를 판단하는 옵션인 것을 알았다. 그러면 credentials에는 어떤 데이터들이 해당이 될까?</p>
<p>Cookies − 쿠키라는 녀석들은 브라우저에 저장되는 작은 데이터이다. <code>same-origin</code> 요청일 때 담겨 보내진다. 쿠키는 세션 정보, 자주 사용되는 데이터, 등등 다양한 목적으로 사용이 된다. 만약 서버에서 Set-Cookie 헤더와 함께 사용되면, 응답을 받은 브라우저에서는 해당 오리진에 대한 쿠키를 브라우저에 저장하게 된다. 서버에서 브라우저에 전달하고자 하는 데이터가 있는 경우에 유용하겠다! 그러면 아무때나 Set-Cookie 헤더를 사용하면 브라우저에 쿠키를 저장할 수 있으려나...? 그건 더 공부를 해봐야겠다.</p>
<p>Authorization Headers − 서버의 보호된 자원에 접근하기 위해서 헤더에 포함시키는 데이터이다.</p>
<pre><code>Authorization: &lt;auth-scheme&gt; &lt;authorization-parameters&gt;</code></pre><p>이렇게 쓰이는데...많이 사용했었는데... Bearer 토큰 사용해 본 적이 있는데...  이 부분은 내가 더 공부를 해서 다른 포스팅으로 돌아오겠다. 꼭 돌아오겠다. 미안해 일단 넘어가자!</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization">MDN - Authorization</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication">MDN - HTTP Authentication</a></p>
<p>TLS Client Certificates − CA로부터 클라이언트 브라우저가 제공받은 디지털 인증서이다. 이것도 credentials에 포함이 된다니 의외였다. 
그럼 이 인증서는 어떻게 요청에 포함이 되려나....? 이 부분도 더 공부해서 돌아오겠다. 일단 넘어가자!</p>
<h2 id="실습">실습</h2>
<p>위에 첨부한 tutorialspoint에는 좋은 실습 코드가 있다. 한 번 옵션을 바꿔보면서 실행해보자. </p>
<p>먼저 내 credentials중 하나인 <code>cookie</code>가 실제로 옵션에 따라 포함되고 안되는지를 실행해보자.</p>
<p>여기 내 쿠키다.
<img src="https://velog.velcdn.com/images/seungrok-yoon/post/10d0616f-342e-4e20-b3b3-8ae49ebf57a4/image.png" alt=""></p>
<h3 id="same-origin-옵션">same-origin 옵션</h3>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/28120ec5-8692-40b1-b68e-d65063fc395e/image.png" alt=""></p>
<p>네트워크 탭의 Timing 탭 옆에 아무것도 없다.</p>
<h3 id="omit-옵션">omit 옵션</h3>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/860a6e8e-38bd-4a97-982b-132c6e50d280/image.png" alt=""></p>
<p>네트워크 탭의 Timing 탭 옆에 아무것도 없다.</p>
<h3 id="include-옵션">include 옵션</h3>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/d6c38d6a-e53e-4644-bd6a-915a4043e9d3/image.png" alt=""></p>
<p>띠용, 쿠키 탭이 생기고, 살펴보니 쿠키들이 Request에 담겨 함께 전달되는 모습을 확인할 수 있었다. 완전 신기하다.</p>
<h2 id="회고">회고</h2>
<p>Chat-GPT같은 생성형 AI 서비스에 물어보면 이런 기본적인 지식들은 손쉽게 얻을 수 있다. 그러면 이런 지식들을 블로그에 굳이 정리하는 의미가 있을까에 대한 고민이 문득 들었다.</p>
<p>이 고민은 사실 <code>내가 습득한 지식의 복습</code>선에서 깔끔하게 정리가 되었다.</p>
<p>내가 궁금한 부분을 검색하고, 내 언어로 직접 정리하는 작업은 <code>복습</code>이었다. 이렇게 블로그에 정리한 지식은 오롯이 나만의 지식으로서 내 기억 깊은 곳에 꽤나 오래 살아있었다. 물론 나도 사람이고 아직 꾸준히 학습하고 더 개발을 많이 해봐야 하기에 부족한 부분이 많은 글이다. 그래서 남들에게 보여주기 민망한 수준의 똥글을 적을 때도 있다. 그런 글을 우연히라도 방문하게 되는 독자분들에게는 죄송하다... 그렇지만 머리속에만 담아두려 하는 아이디어와 지식들이 언젠가는 휘발되는 것이 너무 아까울 때가 있다. 그러니 너그러이 이해해주셨으면 하는 바람이다 하하.</p>
<p>언젠가는 나도 나만의 노하우와 통찰이 담긴 글을 쓸 날이 오겠지.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React ErrorBoundary]]></title>
            <link>https://velog.io/@seungrok-yoon/React-ErrorBoundary</link>
            <guid>https://velog.io/@seungrok-yoon/React-ErrorBoundary</guid>
            <pubDate>Sun, 04 Aug 2024 12:31:29 GMT</pubDate>
            <description><![CDATA[<h1 id="error-boundary">Error Boundary</h1>
<p>| 자식 컴포넌트 트리 안에서 발생한 자바스크립트 에러를 잡아서, 로깅하고, fallback UI (에러가 발생한 컴포넌트 트리 대신 대체해서 보여줄 UI)를 보여주는 리액트 컴포넌트이다.</p>
<blockquote>
<p>단, 
(1) 이벤트 핸들러
(2) 비동기 코드 (setTimeout, requestAnimtionFrame 등)
(3) 서버사이드 렌더링
(4) 자식 컴포넌트들이 아니라, 에러 바운더리 자체에서 발생한 에러
들은 Error boundaries가 캐치해내지 못한다.</p>
</blockquote>
<p>공식문서에서 ErrorBoundary는 클래스 컴포넌트로 작성되어 있다.
아래코드는 공식문서의 예시 코드에 TS를 적용한 모습이다.</p>
<pre><code class="language-js">
import { Component, PropsWithChildren, ReactNode } from &#39;react&#39;;

interface ErrorBoundaryProps extends PropsWithChildren {
  fallback: ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
}

export default class ErrorBoundary extends Component&lt;
  ErrorBoundaryProps,
  ErrorBoundaryState
&gt; {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(_: Error): ErrorBoundaryState {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo): void {
    // Example &quot;componentStack&quot;:
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    console.error(&#39;Error caught by ErrorBoundary:&#39;, error);
    console.error(&#39;Component stack:&#39;, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return this.props.fallback;
    }

    return this.props.children;
  }
}</code></pre>
<p>ErrorBoundary 는 <code>getDerivedStateFromError()</code>라는 컴포넌트생명주기를 활용하여, 하위 자식 컴포넌트 트리에서 발생한 애러를 캐치할 수 있다.</p>
<pre><code class="language-js">static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }</code></pre>
<p>자식 컴포넌트에서 던져진(thrown)에러 이후에 이 생명주기 매서드가 발동된다. 그리고 그 에러가 매서드의 파라미터로 들어오게되고, 상태값인 hasError를 true로 변경하여 다음 렌더 때 fallbackUI를 보여주게 된다.</p>
<p><code>getDerivedStateFromError()</code>는 <code>render</code>단계에서 호출되기에, 사이드 이펙트들이 허용되지 않는다.</p>
<p><a href="https://github.com/SeungrokYoon/react-from-scratch/pull/19">UserManagement 컴포넌트 리팩토링</a> 하는 과정에서 Error boundary를 사용해보았다.</p>
<h2 id="추가적인-의문">추가적인 의문</h2>
<p>단순히 공식문서만보고 <code>아하, Error boundary를 쓰면 에러 발생시에 fallback UI를 렌더링해준대요</code> 라고 믿기에는 뭔가 성에 차지 않는다. </p>
<p>개발 단계에서 Error boundary가 실제로 잘 동작하는지 여부를 테스트하기위해서는 어떤 방법들이 있을까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Typescript] Module에 대한 공부 (1)]]></title>
            <link>https://velog.io/@seungrok-yoon/Typescript-Module%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%B5%EB%B6%80-1</link>
            <guid>https://velog.io/@seungrok-yoon/Typescript-Module%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%B5%EB%B6%80-1</guid>
            <pubDate>Sat, 13 Apr 2024 12:34:35 GMT</pubDate>
            <description><![CDATA[<h1 id="typescript---module">TypeScript - Module</h1>
<p>학습 출처: <a href="https://www.typescriptlang.org/docs/handbook/2/modules.html">[Typescript Handbook - Modules]</a></p>
<p>typescript를 사용하다보면 <code>tsconfig.json</code>에서 관련 설정들을 조정해주어야 할 때가 잦았다. 각 옵션이 무엇을 의미하는지, 또 어떻게 typescript는 동작을 하는지 조금 늦게나마 학습하고 기록으로 정리하는 중이다. </p>
<p>그 중, 자바스크립트에서의 <code>Module</code>개념은 반드시 알아야 하는 녀석이라 생각하여 우선적으로 정리한다.</p>
<h2 id="모듈이란">모듈이란</h2>
<blockquote>
<p>In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module.</p>
</blockquote>
<p>타입스트립트에서 모듈의 정의이다. 즉, 파일에 import, export문이 없으면 모듈이 아니다. 모듈은 각자의 스코프가 존재한다. 반면 모듈이 아닌 파일은 <code>script</code>라 부르며, 스크립트는 <code>전역 스코프</code>에서 접근이 가능하다. </p>
<p>우리는 개발 과정에서 거의 필연적으로 모듈을 호출하고, 모듈로 코드를 분리해 코드를 작성한다. typescript는 컴파일 과정을 통해 javascript로 변환된다. 타입스크립트의 tsc(typescript compiler)가 어떤 형태의 자바스크립트 코드를 읽어들여서 어떤 형태의 자바스크립트 코드로 변환할 지를 <code>tsconfig.json</code> 파일을 통해 지시받는다. 그래서 tsc가 모듈의 경로를 해석하여 적절한 파일로 연결해주고, 잘 컴파일 할 수 있도록 설정해주는 것이 중요하다. </p>
<p>그래서 tsconfig.json의 옵션들 중에서 모듈과 관련된 옵션들을 몇 개 추려서 공부해보았다.</p>
<h2 id="esm-cjs">ESM, CJS</h2>
<p>만약 어떤 파일(가령 .js 파일)이 ESM으로 결정되었으면, Node.js는 require와 CommonJS module을 평가 과정에 주입하지 않는다. 만약 해당 파일에서 require, CommonJS module을 사용하고 있다면 충돌이 발생한다. </p>
<p>반면, 어떤 파일이 CJS로 결정되었으면, 파일 내에서의 <code>import</code>, <code>export</code> 키워드의 사용은 충돌을 발생시킬 것이다.</p>
<p><a href="https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm">참고 - What the heck are CJS, AMD, UMD, and ESM in Javascript?</a></p>
<h2 id="typescript의-module-옵션">TypeScript의 &quot;module&quot; 옵션</h2>
<p><a href="https://stackoverflow.com/questions/71463698/why-we-need-nodenext-typescript-compiler-option-when-we-have-esnext">스택오버플로우 Why we need &quot;nodenext&quot; typescript compiler option when we have &quot;esnext&quot;?</a></p>
<p>이 질문글의 답변에는 typescript의 module 옵션에 대한 설명이 꽤나 친절하게 적혀있었다. 그런데도 나는 이해를 잘 하지 못했다. </p>
<p>그래서 공식 문서를 뒤져봤고, <a href="https://www.typescriptlang.org/docs/handbook/modules/theory.html#the-module-output-format">The module output format</a> 섹션을 읽어본 결과, </p>
<p>module 옵션은 컴파일러가 어떤 형태로 모듈을 내보낼지 결정하는 (emit)옵션이었다. </p>
<pre><code class="language-js">import sayHello from &quot;greetings&quot;;
sayHello(&quot;world&quot;);</code></pre>
<p>가령 위 코드는 tsconfig.json의 module 옵션에 따라 컴파일 결과가 달라질 수 있다는 것!</p>
<h2 id="typescriptdml-moduleresolution-옵션">TypeScriptdml &quot;moduleResolution&quot; 옵션</h2>
<blockquote>
<p>Module resolution is the process of taking a string from the import or require statement, and determining what file that string refers to.</p>
<p>TypeScript includes two resolution strategies: Classic and Node. Classic, the default when the compiler option module is not commonjs, is included for backwards compatibility. The Node strategy replicates how Node.js works in CommonJS mode, with additional checks for .ts and .d.ts.</p>
</blockquote>
<p>위 인용구는 Module의 import/require 구문의 문자열이 해당 문자열이 의미하는 파일로 결정되는지에 대한 과정인 <code>Module resolution</code> 에 대한 설명이다.</p>
<p>타입스크립트는 Classic, Node 이렇게 두 resolution 전략을 가지고 있다. Classic은 타입스크립트 1.6버전 이전에 사용된 옵션이라 모던 타입스크립트에서는 사용하지 않는 옵션이란다(<a href="https://www.typescriptlang.org/tsconfig#moduleResolution">tsconfig/moduleResolution</a>). Node 전략은 CommonJS mode에서 Node.js가 동작하는 방식을 그대로 따르는 것으로, <code>.ts</code>와 <code>.d.ts</code>파일을 통한 추가적인 체크를 하는 방식으로 이루어진단다.</p>
<p>내가 지금 진행하고 있는 <code>electron</code>프로젝트의 <code>tsconfig.json</code>를 예시로 한 번 살펴보자.</p>
<pre><code class="language-json">{
  &quot;compilerOptions&quot;: {
    &quot;incremental&quot;: true,
    &quot;target&quot;: &quot;es2022&quot;,
    &quot;module&quot;: &quot;commonjs&quot;,
    &quot;lib&quot;: [&quot;dom&quot;, &quot;es2022&quot;],
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;strict&quot;: true,
    &quot;sourceMap&quot;: true,
    &quot;moduleResolution&quot;: &quot;node&quot;,
    &quot;esModuleInterop&quot;: true,
    &quot;allowSyntheticDefaultImports&quot;: true,
    &quot;resolveJsonModule&quot;: true,
    &quot;allowJs&quot;: true,
    &quot;outDir&quot;: &quot;.erb/dll&quot;
  },
  &quot;exclude&quot;: [&quot;test&quot;, &quot;release/build&quot;, &quot;release/app/dist&quot;, &quot;.erb/dll&quot;]
}
</code></pre>
<p>moduleResolution이 &quot;node&quot;이다. 이 옵션에 대한 공식 문서의 설정을 살펴보면 아래와 같다.</p>
<blockquote>
<p>&#39;node16&#39; or &#39;nodenext&#39; for modern versions of Node.js. Node.js v12 and later supports both ECMAScript imports and CommonJS require, which resolve using different algorithms. These moduleResolution values, when combined with the corresponding module values, picks the right algorithm for each resolution based on whether Node.js will see an import or require in the output JavaScript code.</p>
<p>&#39;node10&#39; (previously called &#39;node&#39;) for Node.js versions older than v10, which only support CommonJS require. You probably won’t need to use node10 in modern code.</p>
</blockquote>
<p>조금 더 자세한 설명을 찾아볼까?</p>
<blockquote>
<p>node10: Formerly known as node, this is the unfortunate default when module is set to commonjs. It’s a pretty good model of Node.js versions older than v12, and sometimes it’s a passable approximation of how most bundlers do module resolution. It supports looking up packages from node_modules, loading directory index.js files, and omitting .js extensions in relative module specifiers. Because Node.js v12 introduced different module resolution rules for ES modules, though, it’s a very bad model of modern versions of Node.js. It should not be used for new projects.</p>
</blockquote>
<p>아하, 지금 module옵션에 commonjs가 설정되어 있어서 moduleResolution이 node로 설정되어 있구나. </p>
<p>그런데 현재 나는 electron ^26.2.1 버전을 사용중이고, 이 경우에 Node.js 버전은 18이다. 그러니, module옵션과 moduleResolution 옵션을 수정해도 될 것 같다.</p>
<h2 id="그러나-결과는-에러">그러나 결과는 에러!</h2>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/3a5581ac-5cad-4c63-84a3-c0d4dde1fa39/image.png" alt=""></p>
<p>에러가 났던 이유는, 내가 사용하는 보일러플레이트 프로젝트에서 webpack ts-loader의 compilerOption이 &quot;esnext&quot;로 되어있어 Node16옵션과 맞지 않았기 때문이다. </p>
<p>그런데 ts-loader? 이 녀석은 webpack과 관련된 주제라 오늘은 여기까지...후우...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내 electron 프로젝트가 패키징 되지 못했던 이유]]></title>
            <link>https://velog.io/@seungrok-yoon/%EB%82%B4-electron-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EA%B0%80-%ED%8C%A8%ED%82%A4%EC%A7%95-%EB%90%98%EC%A7%80-%EB%AA%BB%ED%96%88%EB%8D%98-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@seungrok-yoon/%EB%82%B4-electron-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EA%B0%80-%ED%8C%A8%ED%82%A4%EC%A7%95-%EB%90%98%EC%A7%80-%EB%AA%BB%ED%96%88%EB%8D%98-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 11 Apr 2024 15:27:47 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/67af40fa-223c-46d2-8f4e-1112c1392772/image.png" alt=""></p>
<p><code>electron-react-boilerplate</code> 프로젝트 기반으로 <code>데스크탑 어플리케이션</code>을 개발하면서 <code>내 프로젝트는 어떻게 실행파일로 변환되는가</code> 가 가장 궁금하고 신기했다. </p>
<p>데스크탑에서 실행가능한 형태로 변환하는 툴에는 대표적으로 electron-forge, electron-builder가 있는데 나는 electron-builder를 사용했다. </p>
<p>변환과정은 다음과 같았다.</p>
<p>React(리액트를 사용하는 경우) -&gt; BUILD (리액트 코드를 JS, HTML, CSS파일로 변환) -&gt; electron-builder를 통해 실행파일로 변환!(생각해보면 당연한 수순같기도...ㅎㅎ)</p>
<p>나는 웹개발 프로젝트를 만들 때 배포환경까지 세팅을 해놓고 프로젝트를 진행해야 안심이 되었다. 그래서 이번에도 보일러플레이트 프로젝트를 클론받자마자 <code>npm run package</code> 스크립트를 실행했는데 웬걸... 에러가 나면서 제대로 내가 원하는 옵션대로 빌드가 되지 않았다. </p>
<h2 id="원인">원인</h2>
<p>내 작업 환경은 Windows10 x64, VSCode로 진행하고 있었다. </p>
<p>문제는 내가 특정 스크립트를 실행할 때 충분한 권한이 없었던 것이었다.</p>
<h2 id="해결">해결</h2>
<p>VSCode를 관리자 권한으로 실행하여 문제를 해결했다.</p>
<p>지금은 <code>Code Signing</code> 단계를 거치지 않고 배포했기에, 아래와 같이 옵션을 설정해놓았다. 다다음주 쯤에는 인증서도 등록해서 어엿한 하나의 제품으로 배포해야 하는데 후움...잘 되려나? 모르겠다.</p>
<pre><code class="language-JSON"> &quot;win&quot;: {
      &quot;target&quot;: &quot;portable&quot;,
      &quot;verifyUpdateCodeSignature&quot;: false,
      &quot;sign&quot;: null
    },</code></pre>
<h2 id="참고">참고</h2>
<p><a href="https://god-logger.tistory.com/104#%EC%9B%90%EC%9D%B8-1">[Window] Electron Builder 실행시 cannot create symbolic link 이슈</a></p>
<p><a href="https://www.electron.build/configuration/configuration">electron-builder 설정 옵션 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[일렉트론 앱에서 production 환경인지, 개발 환경인지 아는 법]]></title>
            <link>https://velog.io/@seungrok-yoon/%EC%9D%BC%EB%A0%89%ED%8A%B8%EB%A1%A0-%EC%95%B1%EC%97%90%EC%84%9C-production-%ED%99%98%EA%B2%BD%EC%9D%B8%EC%A7%80-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9D%B8%EC%A7%80-%EC%95%84%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@seungrok-yoon/%EC%9D%BC%EB%A0%89%ED%8A%B8%EB%A1%A0-%EC%95%B1%EC%97%90%EC%84%9C-production-%ED%99%98%EA%B2%BD%EC%9D%B8%EC%A7%80-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9D%B8%EC%A7%80-%EC%95%84%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Mon, 08 Apr 2024 11:05:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/4cef7565-302d-4aac-9ffb-7f89217853f5/image.png" alt=""></p>
<p><code>Electron.App.isPackaged</code> 라는 boolean 값이 있다. 이 값을 활용해서 development환경과 production환경을 구분하여 로직을 짜보자.</p>
<p>dotenv를 사용하지 않아도 환경을 구분할 수 있어서 편리하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[electron-forge vs electron-builder]]></title>
            <link>https://velog.io/@seungrok-yoon/electron-forge-vs-electron-builder</link>
            <guid>https://velog.io/@seungrok-yoon/electron-forge-vs-electron-builder</guid>
            <pubDate>Sat, 06 Apr 2024 04:53:26 GMT</pubDate>
            <description><![CDATA[<p>앞선 포스팅에서 발견한 module not found 에러를 검색해보다가, <code>electron-builder</code>라는 기술을 알게 되었다. </p>
<p>어떤 기술이 electron으로 개발하고 빌드할 때 더 편리하냐인데,
흥미로워서 링크를 가져와봤다.</p>
<p><a href="https://www.reddit.com/r/electronjs/comments/pek7d5/electronbuilder_vs_electronforge/">[링크]electronbuilder_vs_electronforge</a></p>
<p>읽어보고 나니, 조금 더 많은 빌드 옵션을 제공하는 라이브러리는 electron-builder인듯 하다. </p>
<p>그런데 electron 공식문서에는 <a href="https://www.electronforge.io/">electron-forge</a>가 가장 처음 예시로 나와서 electron-forge를 사용했는데... 에러가 자꾸 나니까(물론 내가 뭔가를 잘못설정했을 가능성이 100%이지만...) electron-builder도 사용해보고 싶어진다. </p>
<p>보일러플레이트 레포들을 살펴보면서 디렉토리 구조와 웹팩 설정을 잘 살펴보면서 눈을 트여야겠다.</p>
<h2 id="빌드까지-가능한-유용한-보일러플레이트">빌드까지 가능한 유용한 보일러플레이트</h2>
<p><a href="https://www.electron.build/index.html#note-for-yarn-3">electron-builder 공식문서에서 소개하는 보일러플레이트</a></p>
<p>electron-builder에는 electron-builder를 활용한 다양한 보일러플레이트 프로젝트들을 소개해주고 있다. </p>
<p>그 중 나는 <a href="https://github.com/electron-react-boilerplate/electron-react-boilerplate">electron-react-boilerplate</a>를 선택했다. </p>
<p>현재 내가 설정한 회사 프로젝트는 electron + electron-forge + react + typescript + webpack + tailwindCSS 로 이루어져 있는데, 어떤 차이점이 있는지 비교해보고 싶어졌다.</p>
<h2 id="새로운-preload-스크립트-활용-패턴-발견">새로운 preload 스크립트 활용 패턴 발견</h2>
<p>electron-react-biolderplate의 preload 스크립트가 짜여진 방식이 매우매우 마음에 들어서 복붙해왔다. 내가 preload스크립트를 짤 때 스스로에게 불만이었던 점은 preload스크립트에 매 이벤트마다 이벤트 핸들러를 등록해야하는 점이었다. </p>
<p>그런데 보일러플레이트 코드에서는 ipcRenderer라는 객체에 실제 electron/ipcRenderer 객체를 래핑한 인터페이스 함수를 작성하여 리액트코드에서 조금 더 안전하게 ipcRenderer를 사용할 수 있도록 코드를 작성했다. </p>
<p>이 코드에서 export한 타입들은 기본적으로는 Window객체에 추가가 되지 않으니, preload.d.ts에서 Window 객체에 타입을 추가해준다.</p>
<pre><code class="language-ts">// Disable no-unused-vars, broken for spread args
/* eslint no-unused-vars: off */
import { contextBridge, ipcRenderer, IpcRendererEvent } from &#39;electron&#39;;

export type Channels = &#39;ipc-example&#39;;

const electronHandler = {
  ipcRenderer: {
    sendMessage(channel: Channels, ...args: unknown[]) {
      ipcRenderer.send(channel, ...args);
    },
    on(channel: Channels, func: (...args: unknown[]) =&gt; void) {
      const subscription = (_event: IpcRendererEvent, ...args: unknown[]) =&gt;
        func(...args);
      ipcRenderer.on(channel, subscription);

      return () =&gt; {
        ipcRenderer.removeListener(channel, subscription);
      };
    },
    once(channel: Channels, func: (...args: unknown[]) =&gt; void) {
      ipcRenderer.once(channel, (_event, ...args) =&gt; func(...args));
    },
  },
};

contextBridge.exposeInMainWorld(&#39;electron&#39;, electronHandler);

export type ElectronHandler = typeof electronHandler;
</code></pre>
<pre><code class="language-ts">//preload.d.ts
import { ElectronHandler } from &#39;../main/preload&#39;;

declare global {
  // eslint-disable-next-line no-unused-vars
  interface Window {
    electron: ElectronHandler;
  }
}

export {};
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node.js 모듈을 찾을 수 없는 일렉트론 에러]]></title>
            <link>https://velog.io/@seungrok-yoon/Node.js-%EB%AA%A8%EB%93%88%EC%9D%84-%EC%B0%BE%EC%9D%84-%EC%88%98-%EC%97%86%EB%8A%94-%EC%9D%BC%EB%A0%89%ED%8A%B8%EB%A1%A0-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@seungrok-yoon/Node.js-%EB%AA%A8%EB%93%88%EC%9D%84-%EC%B0%BE%EC%9D%84-%EC%88%98-%EC%97%86%EB%8A%94-%EC%9D%BC%EB%A0%89%ED%8A%B8%EB%A1%A0-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Sat, 06 Apr 2024 04:48:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/7f42ded8-230f-49e4-aea7-45729ca7bd38/image.png" alt=""></p>
<h2 id="원인">원인</h2>
<p>최종 번들된 코드에 node.js 의 모듈들이 포함되어 있지 않는 이유.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[난 분명히 tsconfig도 작성하고, webpack.renderer.config.ts도 작성했는데 왜 unable to resolve path to module eslint import/no-unresolved ???]]></title>
            <link>https://velog.io/@seungrok-yoon/%EB%82%9C-%EB%B6%84%EB%AA%85%ED%9E%88-tsconfig%EB%8F%84-%EC%9E%91%EC%84%B1%ED%95%98%EA%B3%A0-webpack.renderer.config.ts%EB%8F%84-%EC%9E%91%EC%84%B1%ED%96%88%EB%8A%94%EB%8D%B0-%EC%99%9C-unable-to-resolve-module</link>
            <guid>https://velog.io/@seungrok-yoon/%EB%82%9C-%EB%B6%84%EB%AA%85%ED%9E%88-tsconfig%EB%8F%84-%EC%9E%91%EC%84%B1%ED%95%98%EA%B3%A0-webpack.renderer.config.ts%EB%8F%84-%EC%9E%91%EC%84%B1%ED%96%88%EB%8A%94%EB%8D%B0-%EC%99%9C-unable-to-resolve-module</guid>
            <pubDate>Fri, 05 Apr 2024 05:52:57 GMT</pubDate>
            <description><![CDATA[<p>이거 깔고 .eslint.js에 추가로 설정하자.</p>
<pre><code>settings: {
    &#39;import/parsers&#39;: {
      &#39;@typescript-eslint/parser&#39;: [&#39;.ts&#39;, &#39;.tsx&#39;],
    },
    &#39;import/resolver&#39;: {
      typescript: {},
    },
  },</code></pre><p><a href="https://github.com/import-js/eslint-import-resolver-typescript">https://github.com/import-js/eslint-import-resolver-typescript</a></p>
<p>설정 바꾸고 <code>cmd</code> + <code>shift</code> + <code>p</code> =&gt; Reload Window 해서 제대로 설정이 적용되는지 확인해보자. 이거 때문에 헤매다가 세 시간 날렸다 우씨!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[electron] 라우팅]]></title>
            <link>https://velog.io/@seungrok-yoon/electron-%EB%9D%BC%EC%9A%B0%ED%8C%85</link>
            <guid>https://velog.io/@seungrok-yoon/electron-%EB%9D%BC%EC%9A%B0%ED%8C%85</guid>
            <pubDate>Thu, 04 Apr 2024 09:39:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/ff061df1-768e-4c55-b3b3-af9d134140ae/image.png" alt=""></p>
<p><a href="https://www.electronforge.io/config/plugins/webpack#how-do-i-do-virtual-routing">https://www.electronforge.io/config/plugins/webpack#how-do-i-do-virtual-routing</a></p>
<p>브라우저환경에서 사용하던 <code>createBrowserRouter</code>가 아닌, <code>MemoryRouter</code>로 페이지를 구현해야 한다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[node.js] Session]]></title>
            <link>https://velog.io/@seungrok-yoon/node.js-Session</link>
            <guid>https://velog.io/@seungrok-yoon/node.js-Session</guid>
            <pubDate>Tue, 02 Apr 2024 05:51:12 GMT</pubDate>
            <description><![CDATA[<h1 id="nodejs에서-session-id와-쿠키로-유저-인증을-관리해보자">node.js에서 session id와 쿠키로 유저 인증을 관리해보자</h1>
<h2 id="session이-뭔데">Session이 뭔데?</h2>
<p>세션은 동일한 클라이언트/유저(브라우저)와 서버(웹서버) 사이에서 여러 요청에 걸쳐 이루어지는 상호작용을 stateful하게 유지하는  데이터를 유지할 수 있도록 하는 매커니즘을 의미한다.</p>
<p>HTTP 요청은 클라이언트의 요청에 응답을 한 번 주고나면 더 이상 서버는 클라이언트와의 연결을 유지하지 않는 <code>비연결성</code>을 지니는 프로토콜이다. 이는 별도의 추가적인 장치 없이는 클라이언트가 서버로 보내는 매 HTTP 요청은 별도의 요청으로 취급된다는 것이다.</p>
<p>하지만 실제 세상에서는, 클라이언트가 보내는 요청들에 대해 웹서버가 연결성있게 처리해주어야 하는 요구사항들이 등장한다. 그래서 웹서버는 특정 유저와 관련된 행위를 지속적으로 추적할 필요가 있다. 가령 유저인증이 그렇다. 우리는 한 번 로그인을 하고 나서, 페이지 이동시마다 새로 로그인을 하지는 않는다. 로그인이 성공하고 나면, 우리는 <code>로그인 된 상태</code>로 페이지들을 노닐 수 있다. 서버에서 이 기능은 어떻게 구현된 것일까?</p>
<p>바로 세션을 통해서이다. </p>
<p>HTTP 웹서버는 클라이언트의 연결이 검증되면, 브라우저와 웹사이트 사이의 세션을 생성한다. 유저가 사이트에 로그인되고 나서 세션을 생성하는 경우도 있다.</p>
<p>후자의 경우에 웹서버는 로그인 정보를 기반으로 해당 클라이언트에 대한 세션(서버 나름의 자료구조)을 생성하고, <code>session-related</code>(세션과 관련된) 정보들을 특정한 데이터스토어(서버의 디렉토리구조, 또는 데이터베이스)에 저장함으로써 관리한다. </p>
<p>세션을 생성하고 나서, 서버는 해당 세션의 태그에 해당하는 <code>session ID - 세션아이디</code>를 Set-cookie 헤더에 <code>쿠키</code>로 담아 응답을 하게 된다.</p>
<p>응답을 받은 브라우저는 해당 세션아이디를 쿠키에 저장하고, 다음 요청때 헤더에 담아 서버와 통신하게 된다. </p>
<p>클라이언트가 서버로 다음 요청을 보낼 때, 쿠키에는 이전에 발급받은 세션아이디가 존재할 것이고, 서버는 쿠키에 담겨온 <code>세션아이디</code>를 통해 서버의 세션스토어(세션 데이터가 저장되는 공간)의 세션 데이터에 접근하여 해당 세션아이디에 해당하는 세션이 아직 유효한지 검증한다.</p>
<h2 id="pr">PR</h2>
<p><a href="https://github.com/SeungrokYoon/node-js-practice/pull/7">링크</a></p>
<h2 id="참고">참고</h2>
<p><a href="https://www.techtarget.com/searchsoftwarequality/definition/session-ID">[링크1] What is a session ID?</a></p>
<p><a href="https://www.youtube.com/watch?v=GhrvZ5nUWNg&amp;t=560s">[링크2] Difference between cookies, session and tokens</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[node.js 200제] body-parser 미들웨어와 form 의 기본 MIME-type]]></title>
            <link>https://velog.io/@seungrok-yoon/node.js-body-parser-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4</link>
            <guid>https://velog.io/@seungrok-yoon/node.js-body-parser-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4</guid>
            <pubDate>Mon, 25 Mar 2024 09:04:59 GMT</pubDate>
            <description><![CDATA[<h1 id="body-parser-미들웨어">body-parser 미들웨어</h1>
<p>내 방의 책장을 정리하다가 오~래 전에 사놓은 [node.js 200제]라는 책을 발견했다.학부시절 이 책을 기반으로 한 실습강의를 듣고, 웹개발자가 되기로 결정했던 날이 생각나 문득 책을 열어보았다. 교수님의 코드를 받아 적기만 하던 그 때의 나보다 지금의 나는 얼마나 성장해 있을지 나 스스로 느껴보고 싶기도 했다. </p>
<p>총 200개의 예제 중 160번예제까지는 모두 아는 것들이었다. 아하. 나는 모던자바스크립트 Depp Dive를 잘 공부해 놓았기에 자바스크립트 기초 문법은 다 알고, 잘 사용할 수 있는 내용들인데. 다행이다. 아, request response에 대해 알고 있구나, 다행이다. 아, 프로젝트 셋업에서 package.json이 하는 역할을 잘 알고, 적절하게 의존성 모듈을 설치할 줄 아는구나 다행이다. HTML 태그 종류와, 속성들을 잘 알고 있구나 휴...다행이다. 그래도 성장은 했구나.</p>
<p>160번 이후에는 express 모듈과 미들웨어를 활용한 라우팅 관련 실습예제들이었다. HTML Form에서 데이터를 보내면, 백엔드에서는 이 데이터를 어떻게 다루고, 응답은 어떻게 해 주는지 얼핏 알지만, 더 정확하게 알고싶어졌다. 이제 회사에서 백엔드 개발도 해야하기에 필요한 지식이었다.</p>
<p>그래서 161번부터 실습예제를 빠르게 따라해보고 있는 중이다.</p>
<h2 id="body-parser-미들웨어-1">body-parser 미들웨어</h2>
<p>express 모듈 기준에서 미들웨어는 &lt;request 수신&gt;- &lt;미들웨어&gt; - &lt;response 송신&gt; 그 사이에서 다양한 작업을 처리해주는 소프트웨어(로직)으로 이해했다.</p>
<p>redux-toolkit에서도 <a href="https://redux-toolkit.js.org/api/configureStore">스토어 셋업 시 미들웨어</a> 미들웨어 개념이 나와서 공부했었던 기억이 있기에, 미들웨어 그 자체가 무엇인지에 대해서 이해하는데 많은 시간이 들지는 않았다.</p>
<p>express모듈에는 다양한 미들웨어를 추가하여 사용할 수 있는데, 그 중 <code>body-parser</code>라는 미들웨어 모듈을 사용하는 예제가 있었다. </p>
<p>전체 코드는 다음과 같다.</p>
<p>&lt;login.html&gt;</p>
<pre><code class="language-html">
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Login page&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;Login&lt;/h1&gt;
    &lt;hr /&gt;
    &lt;form method=&quot;post&quot; action=&quot;/login&quot;&gt;
      &lt;table&gt;
        &lt;tr&gt;
          &lt;td&gt;
            &lt;label for=&quot;userId&quot;&gt;UserId&lt;/label&gt;
            &lt;input type=&quot;text&quot; name=&quot;userId&quot; /&gt;
          &lt;/td&gt;
          &lt;td&gt;
            &lt;label for=&quot;password&quot;&gt;PW&lt;/label&gt;
            &lt;input type=&quot;password&quot; name=&quot;password&quot; /&gt;
          &lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;&lt;/tr&gt;
      &lt;/table&gt;
      &lt;input type=&quot;submit&quot; value=&quot;confirm&quot; name=&quot;&quot; /&gt;
    &lt;/form&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>&lt;app.js&gt;</p>
<pre><code class="language-js">const path = require(&quot;path&quot;);
const express = require(&quot;express&quot;);
const bodyParser = require(&quot;body-parser&quot;);
const app = express();
const port = 5500;

app.use(express.static(path.join(__dirname, &quot;./html&quot;)));

/**
 * @description bodyParser.urlencoded 를 설정하지 않으면 앱이 동작을 하지 않는다.
 */
app.use(bodyParser.urlencoded());
app.use(bodyParser.json());

/**
 * 미들웨어를 여러개를 순차적으로 엮을 수 있다.
 */
app.use((request, response, next) =&gt; {
  console.log(request.body);
  console.log(&quot;first middleware&quot;);
  request.user1 = &quot;승록&quot;;
  next();
});

app.use((request, response, next) =&gt; {
  console.log(&quot;second middleware&quot;);
  request.user2 = &quot;승재&quot;;
  next();
});

app.use((request, response, next) =&gt; {
  console.log(&quot;third middleware&quot;);
  next();
});

app.get(&quot;/&quot;, (req, res) =&gt; {
  res.sendFile(path.join(__dirname, &quot;./html/index.html&quot;));
});

app.get(&quot;/login&quot;, (req, res) =&gt; {
  res.status(200).set(&quot;Content-Type&quot;, &quot;text/html&quot;);
  res.sendFile(path.join(__dirname, &quot;./html/login.html&quot;));
});

app.post(&quot;/login&quot;, (req, res) =&gt; {
  const { userId, password } = req.body;
  if (!userId || !password) {
    return res.status(400).json({ error: &quot;Missing id, pw&quot; });
  }
  res.status(200).set(&quot;Content-Type&quot;, &quot;application/json&quot;);
  const jsonData = {
    date: new Date(),
    userId,
    password,
  };
  res.json(jsonData);
});

app.get(&quot;/error&quot;, (req, res) =&gt; {
  res.status(404).send(&quot;&lt;h1&gt;404 ERROR&lt;/h1&gt;&quot;);
});

app.listen(port, () =&gt; {
  console.log(`Example app listening to port${port}`);
});
</code></pre>
<h2 id="문제상황">문제상황</h2>
<pre><code>app.use(bodyParser.urlencoded({ extended: false }));</code></pre><p>처음에는 위 구문 없이 앱을 구동했다. 그랬더니 request.body가 비어있는 객체가 나오는 것이 아닌가?</p>
<h2 id="원인">원인</h2>
<p>body-parser의 <code>urlencoded</code>의 <code>extended</code>옵션이 어떤 역할을 하는지 무엇인지 궁금해졌다. 대체 뭐길래 저게 있고 없고가 form에서 post요청으로 보낸 body의 데이터를 읽어서 채우고 말고를 결정하는 것인가? 책에는 나와있지 않아 직접 찾아보았다.</p>
<p><a href="https://github.com/expressjs/body-parser#readme">body-parser 레포지토리 링크</a></p>
<h2 id="밝혀진-진실">밝혀진 진실</h2>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/4431942c-8051-4b0c-bb8a-561537b5b367/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/ed463fc8-ba22-4ac8-ad80-fa1511285f59/image.png" alt=""></p>
<p><code>urlencoded 바디</code>만 파싱해서 만든 새로운 <code>body 객체</code>를 reqeust.body에 반영시키는 역할을 하는 미들웨어 기능이었다.</p>
<p>그 중 extended 옵션은 <code>querystring</code>라이브러리와 <code>qs</code>라이브러리 둘 중 어느 것을 사용하여 URL-encoded 데이터를 파싱할 지 고르는 옵션이었다. 결국 이 둘의 차이를 알고 있어야 사용할 수 있는 옵션이었던 것이다. </p>
<p>나는 여기서 세 가지 모르는 점이 생겼다.</p>
<ul>
<li>URL-encoded body object가 뭐야?</li>
<li>querystring 라이브러리는 뭐야?</li>
<li>qs 라이브러리는 뭐야?</li>
</ul>
<p>하지만 그 중에서 첫 번째 의문점만 이 글에서는 알아보았다.</p>
<h3 id="url-encoded-body-object">URL-encoded body object</h3>
<p>URL-encoded body 는 클라이언트가 서버로 key value쌍의 형식의 데이터를 URL-encoded 포맷으로 인코딩하여 보낸 데이터를 뜻한다. </p>
<p>여기서 URL encoding이란, 문자를 인터넷상에서 전송될 수 있는 형식으로 변환하는 것을 의미한다.</p>
<p>이렇게 인코딩된 문자들은 결국 클라이언트측에서 설정한 대로 작성되기에, 우리는 데이터의 출처인 클라이언트 코드를 봐야한다. </p>
<p>내 코드의 예시에서는 <code>&lt;form&gt;</code> 태그가 데이터의 출처니, form 에서 어떻게 입력값이 변환되어 서버로 전달되는지가 내 지식의 공백이겠다.</p>
<h3 id="html-form-태그의-데이터-전송-방식">HTML form 태그의 데이터 전송 방식</h3>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">Mozilla POST</a> 에서는 HTML form태그에서 수행되는 POST방식의 요청의 경우에, <code>content-type</code>을 <code>enctype</code>속성에 명시할 수 있다고 한다. </p>
<p>이 때 세 가지 <code>content-type</code>을 줄 수가 있는데, </p>
<ul>
<li>application/x-www-form-urlencoded</li>
<li>multipart/form-data</li>
<li>text/plain</li>
</ul>
<p>그 중에서, application/x-www-form-urlencoded형식을 채택한 경우에는 알파벳이 아닌 문자가 URL-encoded된다고 한다. 그리고 <code>application/x-www-form-urlencoded는 form 태그요소의 기본 MIME type</code>이다.</p>
<p>가령</p>
<pre><code class="language-html">&lt;form method=&quot;post&quot; action=&quot;/submit&quot;&gt;
  &lt;label for=&quot;username&quot;&gt;Username:&lt;/label&gt;
  &lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot;&gt;
  &lt;label for=&quot;password&quot;&gt;Password:&lt;/label&gt;
  &lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot;&gt;
  &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
&lt;/form&gt;</code></pre>
<p>이런 form이 있을 때, submit된 데이터는 아래의 형식으로 인코딩이 된다. 서버는 아래 형식으로 인코딩된 데이터를 <code>파싱</code>해서 원래 데이터 형식으로 복원하는 작업이 필요했던 것이다.</p>
<pre><code>username=user123&amp;password=secret123</code></pre><h2 id="결론">결론</h2>
<p>body-parser 모듈의 <code>urlencoded()</code> 메서드는 서버로 들어온  application/x-www-form-urlencoded 포맷의 데이터를 파싱하기 위한 작업을 하는 미들웨어함수였다. </p>
<p>더불어 <code>json()</code>메서드는 서버로 들어온 application/json 포맷의데이터를 파싱하기 위한 작업을 하는 미들웨어 함수였다.</p>
<p>이렇게 파싱이 된 데이터를 우리의 서버 코드는 그제서야 알아보고 후속 작업을 진행할 수 있는 것이었다. </p>
<p>아하~ 그랬군!</p>
<p><code>&lt;form&gt;</code>태그에 default MIME type 이 있다는 것을 처음 알아서 가장 유익했고, 서버는 다양한 형식으로 서버에 전송되는 데이터를 처리할 수 있어야 하기에, 대비가 되어있어야 한다는 점이 재미있었다.</p>
<p>오늘 참 유익했다~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] Postgresql (진행중)]]></title>
            <link>https://velog.io/@seungrok-yoon/DB-Postgresql</link>
            <guid>https://velog.io/@seungrok-yoon/DB-Postgresql</guid>
            <pubDate>Wed, 20 Mar 2024 02:00:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/seungrok-yoon/post/a18abfd7-6957-4dcc-b4d4-1df30d7ff07b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[python] class attribute vs instance attribute]]></title>
            <link>https://velog.io/@seungrok-yoon/python-class-attribute-vs-instance-attribute</link>
            <guid>https://velog.io/@seungrok-yoon/python-class-attribute-vs-instance-attribute</guid>
            <pubDate>Fri, 15 Mar 2024 12:24:09 GMT</pubDate>
            <description><![CDATA[<h1 id="잊지-않으려고-저장하는-chatgpt와의-대화">잊지 않으려고 저장하는 ChatGPT와의 대화</h1>
<p>Defining a signal in the <code>__init__</code> function of a class versus defining it outside the <code>__init__</code> function has a significant difference in terms of scope and visibility.</p>
<ol>
<li><strong>Defining in <code>__init__</code>:</strong><ul>
<li>When you define a signal in the <code>__init__</code> function of a class, it becomes an instance attribute of that class. This means that each instance of the class will have its own signal attribute.</li>
<li>Signals defined in <code>__init__</code> are accessible only within the instance of the class where they are defined. Other instances of the same class will have their own separate signal attributes.</li>
</ul>
</li>
</ol>
<p>Example:</p>
<pre><code class="language-python">from PySide6.QtCore import QObject, Signal

class MyClass(QObject):
    def __init__(self):
        super().__init__()
        self.my_signal = Signal()

# Usage
obj1 = MyClass()
obj2 = MyClass()

# obj1 and obj2 have their own separate instances of my_signal</code></pre>
<ol start="2">
<li><strong>Defining outside <code>__init__</code>:</strong><ul>
<li>When you define a signal outside the <code>__init__</code> function (as a class attribute), it becomes a class-level attribute.</li>
<li>Signals defined outside <code>__init__</code> are shared among all instances of the class. They behave like static members of the class.</li>
</ul>
</li>
</ol>
<p>Example:</p>
<pre><code class="language-python">from PySide6.QtCore import QObject, Signal

class MyClass(QObject):
    my_signal = Signal()

# Usage
obj1 = MyClass()
obj2 = MyClass()

# obj1 and obj2 share the same instance of my_signal</code></pre>
<p><strong>In Summary:</strong></p>
<ul>
<li>If you define a signal in the <code>__init__</code> function, each instance of the class will have its own signal attribute. This can be useful if you need different instances to emit signals independently.</li>
<li>If you define a signal outside the <code>__init__</code> function, it becomes a class-level attribute shared among all instances. This can be useful if you want all instances to share the same signal, such as for notifying about global events or state changes.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>