<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_leewoooo.log</title>
        <link>https://velog.io/</link>
        <description>leewoooo</description>
        <lastBuildDate>Thu, 02 Jan 2025 00:06:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev_leewoooo.log</title>
            <url>https://images.velog.io/images/dev_leewoooo/profile/c30e46ab-df16-47a2-8f78-2ae55ebc4bb8/다운로드.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev_leewoooo.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_leewoooo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[flutter web 배포 (with cloudflare)]]></title>
            <link>https://velog.io/@dev_leewoooo/flutter-web-%EB%B0%B0%ED%8F%AC-with-cloudflare</link>
            <guid>https://velog.io/@dev_leewoooo/flutter-web-%EB%B0%B0%ED%8F%AC-with-cloudflare</guid>
            <pubDate>Thu, 02 Jan 2025 00:06:10 GMT</pubDate>
            <description><![CDATA[<h1 id="cloudflare-page를-이용한-flutter-web-배포">cloudflare page를 이용한 flutter web 배포</h1>
<p>개요: 기존 flutter web을 github page를 이용하여 배포를 진행하고 있었지만 초기 로딩 속도에 대한 불편함이 있어 다른 배포 플랫폼을 찾던 중 cloudflare page에 테스트를 진행해보고자 함.</p>
<br>

<h2 id="cloudflare-page-vs-github-page">cloudflare page vs github page</h2>
<p>[Compare Center](<a href="https://bejamas.io/compare/cloudflare-pages-vs-github-pages">Cloudflare Pages vs GitHub Pages</a>)에서 cloudflare page vs github page를 비교한 자료를 보면 아래와 같이 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/dd15a8b9-b30d-4d32-a405-c9299f80ef90/image.png" alt=""></p>
<br>

<h2 id="deploy-하기">Deploy 하기</h2>
<p>cloudflare page에 배포를 하기 위해서는 아래와 같은 순서를 가진다. (필자는 저장소를 github을 이용하고 있다.)</p>
<ol>
<li>cloudflare에 repository 연결</li>
<li>빌드 설정 및 구성</li>
<li>배포 확인</li>
</ol>
<br>

<h3 id="cloudflare에-repository-연결">cloudflare에 repository 연결</h3>
<p>cloudflare에 로그인 후 좌측 사이드 바에 Workers 및 Pages 메뉴에서 cloudflare page 기능을 이용할 수 있다.</p>
<p>해당 메뉴를 선택한 후 github에 있는 저장소를 연결할 수 있도록 ui를 제공해준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/f3b7eca0-f74b-4d31-b84d-f64cf2b011eb/image.png" alt=""></p>
<p>github의 저장소 중 배포할 저장소만 선택해주면 해당 단계는 끝이난다.</p>
<br>

<h3 id="빌드-설정-및-구성">빌드 설정 및 구성</h3>
<p>리포지토리를 선택 후 다음으로 넘어가면 해당 프로젝트를 빌드 할 때 사용되는 명령어 및 빌드 된 파일의 경로를 설정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/70ba8938-e9d5-4153-9cfc-5febdf81cea8/image.png" alt=""></p>
<p>위 사진에서 보면 프레임워크 선택 시 추천 빌드 명령을 cloudflare에서 제공을 해준다. </p>
<p>하지만 flutter web 같은 경우에는 정식적으로는 지원하지 않아 빌드명령을 직접 작성해야 한다.</p>
<pre><code class="language-bash">if cd flutter; then # 1
    git pull &amp;&amp; cd ..;
else
    git clone https://github.com/flutter/flutter.git;
fi &amp;&amp; \
flutter/bin/flutter clean &amp;&amp; \ # 2
flutter/bin/flutter config --enable-web &amp;&amp; \ # 3
flutter/bin/flutter build web --web-renderer canvaskit --release # 4</code></pre>
<ol>
<li>현재 flutter의 설치 여부를 확인하여 있으면 최신 상태로 pull을 하고 없으면 clone을 받아 설치를 한다.</li>
<li>프로젝트 빌드에 대한 캐시를 삭제한다.</li>
<li>flutter 프로젝트에서 web을 활성화 한다.</li>
<li>flutter를 빌드한다. (renderer 옵션은 canvaskit 이용)</li>
</ol>
<p>위 명령어를 빌드명령에 작성 후 flutter web 같은 경우 build/web에 빌드 결과가 담기기 때문에 빌드 출력 디렉터리에 입력해주면 된다.</p>
<p>추가적으로 환경변수 또한 정의 후 배포가 가능하다.</p>
<p>버전에 따라 <code>--web-renderer</code> 명령어를 찾지 못한다는 오류를 뱉을 수 있으니 확인 후 진행하는 것이 좋다.</p>
<br>

<h3 id="배포-확인">배포 확인</h3>
<p>설정을 완료 후 조금 기다리면 배포가 정상적으로 진행 된 것을 볼 수 있다. 진행 된 내용은 Workers 및 Pages의 개요에서 확인 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/d9c358cb-a561-4580-ae26-e7a9b8acc60a/image.png" alt=""></p>
<br>

<p>배포 된 프로젝트의 상세에 들어가서 배포 된 URL을 확인할 수 있으며 배포 로그 또한 확인이 가능하다. 도메인을 연결하는 것 또한 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/ef7900ac-ef58-4824-8888-aa679bb4750c/image.png" alt=""></p>
<br>

<h2 id="마무리">마무리</h2>
<p>github page를 이용하여 배포를 할 때는 github action을 이용하여 빌드하고 빌드 한 내용을 다시 deply 하는 과정을 겪었다.</p>
<p>하지만 cloudflare 같은 경우 배포하는 과정이 생각보다 간편했으며 별도의 pipeline을 구축하지 않아도 된 다는 점이 간편했던 것 같다. </p>
<p>main branch의 변경이 발생 되면 이를 확인하여 자동으로 배포해주는 점이 매우 매력적이었다.</p>
<p>속도적인 측면은 토이 프로젝트를 진행하면서 비교해 볼 예정이다.</p>
<br>

<h2 id="references">REFERENCES</h2>
<ul>
<li><a href="https://sarthaknp.medium.com/deploying-your-flutter-web-app-on-cloudflare-pages-0d4a30caa99f">https://sarthaknp.medium.com/deploying-your-flutter-web-app-on-cloudflare-pages-0d4a30caa99f</a></li>
<li><a href="https://bejamas.io/compare/cloudflare-pages-vs-github-pages">https://bejamas.io/compare/cloudflare-pages-vs-github-pages</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[flutter web으로 모바일 청첩장 만들기]]></title>
            <link>https://velog.io/@dev_leewoooo/flutter-web%EC%9C%BC%EB%A1%9C-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%B2%AD%EC%B2%A9%EC%9E%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_leewoooo/flutter-web%EC%9C%BC%EB%A1%9C-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%B2%AD%EC%B2%A9%EC%9E%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 30 Jul 2024 06:19:38 GMT</pubDate>
            <description><![CDATA[<h1 id="flutter-web으로-모바일-청첩장-만들기">flutter web으로 모바일 청첩장 만들기</h1>
<p>flutter를 이용해서 모바일 청첩장을 만들면서 알게 된 점을 정리하고자 한다.</p>
<p>청첩장 링크: <a href="https://leewoooo.github.io/wedding/#">모바일 청첩장</a></p>
<br>

<h2 id="flutter를-선택한-이유">flutter를 선택한 이유</h2>
<p>web 쪽에서는 아직 <code>js</code>진영이 두텁다 보니 <code>flutter web</code>을 이용해서 만들어보겠다는 시도가 조금은 어리석을 수도 있었을 것이다.</p>
<p>다른 개발자 분들의 모바일 청첩장을 봐도 대부분 <code>next</code> 혹은 <code>react</code>를 이용해 개발 된 레퍼런스들을 볼 수 있었다. <del>(간간히 flutter로 개발 된 모바일 청첩장도 있었고 만날 때 마다 반가웠다.)</del></p>
<p>프로젝트를 시작할 때 언어와 프레임워크를 선택함에 있어 요구사항은 아래와 같았다.</p>
<ol>
<li><p>개발에 있어서 퍼포먼스를 조금 더 낼 수 있는 언어와 프레임 워크는 무엇인가?</p>
<ul>
<li>이전 공부 한 내용들 + 검색을 하면서 진행할 수 있을 거라 판단</li>
</ul>
</li>
<li><p>Deadline이 나름 정해져 있는데 해당 기간 안에 퀄리티를 뽑으면서 완성 시킬 수 있는 언어와 프레임 워크는 무엇인가?</p>
<ul>
<li><code>next</code>와 <code>react</code>로 가능하겠지만 <code>flutter</code>를 이용한 것 만큼 <code>ui</code>적으로 <code>css</code>를 다룰 수 없을 것으로 판단하였음.</li>
</ul>
</li>
<li><p>이번 건 뿐만 아니라 추 후 토이프로젝트에 적용 가능한 기술인가?</p>
<ul>
<li>다음 프로젝트를 앱개발로 생각을 하고 있어서 적용 가능하다고 판단</li>
</ul>
</li>
</ol>
<p><code>flutter</code>는 위 3개의 요구사항을 전부 만족 할 수 있다고 판단하여 선택을 하게 되었다.</p>
<br>

<h2 id="flutter로-구현하는-모바일-우선-레이아웃">flutter로 구현하는 모바일 우선 레이아웃</h2>
<p>모바일 청첩장이다 보니 모바일 우선 레이아웃을 구현해야 했다.</p>
<p>먼저 <code>css</code>를 이용해서는 <code>max-width</code>와 <code>margin</code>을 이용하여 구현이 가능했었다.</p>
<p><code>flutter</code>에서는 <code>Container</code>에 <code>BoxConstraints</code>를 이용하여 최대 및 최소의 <code>width</code>를 적용해주었으며 <code>height</code>는 <code>MediaQuery</code>를 이용하여 기기의 <code>height</code>를 부여하였다.</p>
<p><code>css</code>의 <code>margin</code>으로 가운데 정렬을 하던 부분은 <code>Container</code>를 <code>Center</code>로 감싸주었다.</p>
<pre><code class="language-dart">return Container(
  child: Center(
    child: ConstrainedBox(
      constraints: BoxConstraints(
        maxWidth: 430,
        minWidth: 340,
        minHeight: MediaQuery.of(context).size.height,
      ),
      child: Scaffold(...),
    ),
  ),
);</code></pre>
<p>위와 같이 구현 후 <code>child</code>에 <code>Scaffold</code> 위젯을 선언하여 구현해 나가기 시작했다.</p>
<p><del>모바일 우선 레이아웃이 flutter에서는 불가능 할 거 같아서 flutter를 포기하려 했었다.</del></p>
<br>

<h2 id="ogopen-graph-오픈그래프"><code>og</code>(open graph), 오픈그래프</h2>
<p>모바일 청첩장이다 보니 카카오톡을 이용하여 링크를 전달하는 경우가 많다. 링크가 공유 될 때 해당 링크의 <code>title</code>, <code>description</code>, <code>image</code>와 함께 공유하고 싶다는 생각을 하며 검색하던 도중 <code>og</code>태그에 대해 알게 되었다.</p>
<br>

<blockquote>
<p>og 태그란 웹 페이지의 메타데이터를 정의하는 태그 중 하나이다. 이 태그는 페이스북에서 개발되었으며, 웹사이트가 소셜 미디어에 공유될 때 해당 웹페이지의 제목, 설명, 이미지 등을 정의할 수 있다.사용자가 클릭하기 전에 크롤러가 해당 웹사이트를 방문하여 이 태그를 크롤링하여 미리보기로 나타나는 정보를 지정하고 띄워준다.</p>
</blockquote>
<p><code>flutter web</code>에서 <code>og tag</code>를 설정하는 방법은 간단하다. 프로젝트에 <code>web</code> 디렉토리에 <code>index.html</code>이 있는데 해당 <code>html</code>안에 적용해주면 된다.</p>
<pre><code class="language-html">&lt;!-- index.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;!-- ... --&gt;
    &lt;!-- og --&gt;
    &lt;meta property=&quot;og:title&quot; content=&quot;우길❤️은하 결혼식에 초대합니다&quot; /&gt;
    &lt;meta property=&quot;og:description&quot; content=&quot;모바일 청첩장&quot; /&gt;
    &lt;meta property=&quot;og:type&quot; content=&quot;website&quot; /&gt;
    &lt;meta property=&quot;og:url&quot; content=&quot;https://leewoooo.github.io/wedding/#&quot; /&gt;
    &lt;meta
      property=&quot;og:image&quot;
      content=&quot;https://leewoooo.github.io/wedding/assets/assets/images/grid_asset_2.jpeg&quot;
    /&gt;
    &lt;!-- ... --&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script src=&quot;flutter_bootstrap.js&quot; async&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><code>og tag</code>를 위와 같이 정의 후 배포하면 링크를 공유했을 때 이쁘게 공유 되는 것을 볼 수 있을 것이다.</p>
<br>

<h2 id="flutter-web의-장단점"><code>flutter web</code>의 장,단점</h2>
<p><code>flutter</code>로 개발을 하였을 때 장,단점은 확실했던 것 같다.</p>
<h3 id="장점">장점</h3>
<p>이번에 개발하면 서 느낀 것은 확실 히 개발 속도가 <code>next</code>로 시도했을 때 보다 빠르게 나왔다. 모바일 청첩장을 만드는 데 총 든 개발시간이 2일보다 조금 덜 걸린 것 같다. (디테일 제외)</p>
<p>또한 앱 개발을 위해 배웠 던 개념을 그대로 사용할 수 있어서 좋았다.</p>
<h3 id="단점">단점</h3>
<p><code>custom initialization</code>을 구현했다고 하더라도 느린 초기 로딩 속도는 어쩔 수 없는 것 같다.</p>
<p>그리고 디버깅에도 개발자 도구를 통해 디버깅이 익숙한 필자한테는 디버깅 측면에서도 조금은 낯설게 느껴지는 부분이 있었다.</p>
<br>

<h2 id="결론">결론</h2>
<p>걱정했던 것에 비해 모바일 청첩장 프로젝트를 잘 마무리 할 수 있었다. 회사에서 사용하는 메인 기술 스택이 아니였지만 개인적으로 스터디 하고 적용하면서 성장이 있는 것 같다.</p>
<p>이러 한 경험들을 하기 위해 토이 프로젝트를 하는 것이 아닐까 하는 생각이 든다.</p>
<p>무엇보다 중요한 것은 <strong>끝 맺음을 할 수 있는 힘</strong>인거 같다. 결국 배포를 하지 않으면 공개되지 않고 주머니 안에만 가지고 있게 되는데 주머니에 있을 때는 그 것을 결과라고 보기는 어려울 것이다. 나 이외의 사람들에 대한 피드백은 더더욱 받기 힘들 뿐더러..</p>
<p>결국 서비스라는 것이 그런 것이지 않을까 한다. 소소한 토이프로젝트였지만 배포 후 반응을 보며 더 고도화해나가는 작업을 통해 또 한번 배워갈 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[flutter initialization 적용하기]]></title>
            <link>https://velog.io/@dev_leewoooo/flutter-initialization-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_leewoooo/flutter-initialization-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 26 Jul 2024 07:30:28 GMT</pubDate>
            <description><![CDATA[<h1 id="flutter-web-app-initialization-적용하기">flutter web app initialization 적용하기</h1>
<p>flutter로 만든 web app에 처음 진입했을 때 보여줄 loading indicator를 적용하자.</p>
<p>해당 글은 flutter version 3.22 이상 환경에서 작성하였으며 3.22 이하의 경우 <a href="https://docs.flutter.dev/platform-integration/web/initialization-legacy">legacy문서</a>를 확인하면 된다.</p>
<br>

<h2 id="goal">Goal</h2>
<ul>
<li>공식문서를 보며 web app이 켜질 때 이벤트 처리에 대한 내용 이해하기</li>
<li>웹의 리소스를 다운로드 하는 동안 보여질 loading indicator 적용하기</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p>flutter Web를 빌드할때 dart2js가 dart 코드 전체를 javascript로 컴파일한 다음 결과를 build/web/main.dart.js로 저장한다.</p>
<p>index.html에서는 js를 불러와서 화면을 그리게 되는데 이 때 사용자에게는 계속해서 하얀색의 화면이 노출되게 되는데 사용자 경험을 좋지 않게 만들고 있다.</p>
<p>이것을 해결하기 위해 <code>initialization</code>를 적용해보고자 한다.</p>
<br>

<h2 id="initialization"><code>initialization</code></h2>
<p><a href="https://docs.flutter.dev/platform-integration/web/initialization">공식문서</a>를 보면 웹 앱이 초기화 될 때 커스텀을 할 수 있는 방법을 제공해주고 있다.</p>
<br>

<h3 id="프로젝트-생성-및-초기-구조">프로젝트 생성 및 초기 구조</h3>
<p>먼저 프로젝트를 생성하였을 때 <code>web</code>디렉토리에 있는 <code>index.html</code>의 코드를 보면 아래와 같이 스크립트 하나가 정의되어 있는 것을 볼 수 있다.</p>
<pre><code class="language-html">&lt;html&gt;
  &lt;!-- ... --&gt;
  &lt;body&gt;
    &lt;script src=&quot;flutter_bootstrap.js&quot; async&gt;&lt;/script&gt;
  &lt;/body&gt;
  &lt;!-- ... --&gt;
&lt;/html&gt;</code></pre>
<p>위 코드에서 볼 수 있듯 <code>flutter_bootstrap.js</code>로 <code>initialization</code>을 처리할 수 있는데 해당 파일은 flutter의 web이 빌드 될 때 build 파일 안에 자동으로 생긴다. 해당 파일의 역할은 <strong>앱을 초기화 하고 실행하는데 필요한 정보들이 들어있는 파일</strong>이라고 생각하면 된다.</p>
<br>

<p>만약 <code>web</code> 디렉토리 내부에 <code>flutter_bootstrap.js</code>를 새로 정의했다면 빌드 시 <strong>오버라이딩</strong>되며 적용된다. 기본적으로 flutter에서 빌드 시 자동으로 생성해주는 <code>flutter_bootstrap.js</code>는 아래와 같다.</p>
<pre><code class="language-js">// 1
{{flutter_js}}

// 2 
{{flutter_build_config}}

// 3
_flutter.loader.load();</code></pre>
<ol>
<li><p><code>flutter_js</code>는 <code>FlutterLoader</code> object를 사용 가능하게 만들어준다. <code>FlutterLoader</code> 내부에는 <code>_flutter.loader</code>가 글로벌 변수로 존재한다.</p>
</li>
<li><p><code>flutter_build_config</code>는 빌드 과정에서 생성된 메타데이터를 설정하는 역할을 한다. 앱이 정상적으로 부트스트랩되기 위해 <code>FlutterLoader</code>에 데이터를 제공해준다.</p>
</li>
<li><p>앱을 초기화 하기 호출되는 함수이며 초기화 시 사용할 옵션들을 받는다.</p>
<ul>
<li>config: 앱의 구성</li>
<li>onEntrypointLoaded: 엔진 초기화 준비가 되면 호출 될 콜백함수</li>
<li>serviceWorkerSettings: 서비스 워커 구성</li>
</ul>
</li>
</ol>
<br>

<h3 id="onentrypointloaded"><code>onEntrypointLoaded</code></h3>
<p>목표를 이루기 위해서 중요하게 볼 부분이 <code>onEntrypointLoaded</code> 콜백이다. 위에서도 적은 것 처럼 엔진의 초기화 준비가 되면 해당 콜백이 호출되게 되는데 해당 콜백 내부에서 <code>runApp()</code>이 호출 되기 전까지 유저에게 흰 화면이 보여지게 되는 것이다.</p>
<p>앱이 실행 되기 전에 초기화 및 로딩 처리에 대한 부분을 해당 콜백에서 정의하면 된다.</p>
<br>

<h3 id="loading-indicator적용하기"><code>loading indicator</code>적용하기</h3>
<p><a href="https://docs.flutter.dev/platform-integration/web/initialization#example-display-a-progress-indicator">progress-indicator</a>예시에 나온 것 처럼 먼저 <code>document.body</code>에 <code>div</code>를 심고 해당 요소를 <code>loading indicator</code>로 사용하고자 한다.</p>
<pre><code class="language-js">// fluter_bootstrap.js
{{flutter_js}}
{{flutter_build_config}}


const loadingDiv = document.createElement(&quot;div&quot;);
loadingDiv.className = &quot;loading&quot;;
document.body.appendChild(loadingDiv);
const loaderDiv = document.createElement(&quot;div&quot;);
loaderDiv.className = &quot;loader&quot;;
loadingDiv.appendChild(loaderDiv);

// Customize the app initialization process
_flutter.loader.load();</code></pre>
<br>

<p>필자는 <a href="https://css-loaders.com/pulsing/">css-loaders</a>에서 <code>loading indicator</code>의 css를 가져와 사용하으며 해당 코드를 <code>index.html</code>에 추가하였다.</p>
<pre><code class="language-css">/* style.css */
.loading {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0;
  position: absolute;
  top: 50%;
  left: 50%;
  -ms-transform: translate(-50%, -50%);
  transform: translate(-50%, -50%);
}


.loader {
  width: 50px;
  aspect-ratio: 1;
  color:#dc1818;
  background:
    radial-gradient(circle at 60% 65%, currentColor 62%, #0000 65%) top left,
    radial-gradient(circle at 40% 65%, currentColor 62%, #0000 65%) top right,
    linear-gradient(to bottom left, currentColor 42%,#0000 43%) bottom left ,
    linear-gradient(to bottom right,currentColor 42%,#0000 43%) bottom right;
  background-size: 50% 50%;
  background-repeat: no-repeat;
  position: relative;
}
.loader:after {
  content: &quot;&quot;;
  position: absolute;
  inset: 0;
  background: inherit;
  opacity: 0.4;
  animation: l3 1s infinite;
}
@keyframes l3 {
  to {transform:scale(1.8);opacity:0}
}</code></pre>
<br>

<p>위와 같이 작성하면 앱에 들어왔을 때 해당 <code>loading indicator</code>가 보여질 것이다. <code>loading indicator</code>는 엔진이 준비되기 전까지 보여주고 사라져야 하기 때문에 <code>onEntrypointLoaded</code> 콜백을 이용하여 <code>loading indicator</code>로 사용되는 <code>div</code>요소를 제거해주면 된다.</p>
<pre><code class="language-js">{{flutter_js}}
{{flutter_build_config}}


const loadingDiv = document.createElement(&quot;div&quot;);
loadingDiv.className = &quot;loading&quot;;
document.body.appendChild(loadingDiv);
const loaderDiv = document.createElement(&quot;div&quot;);
loaderDiv.className = &quot;loader&quot;;
loadingDiv.appendChild(loaderDiv);

_flutter.loader.load({
  onEntrypointLoaded: async function (engineInitializer) {
    const appRunner = await engineInitializer.initializeEngine();
    if (document.body.contains(loadingDiv)) {
      document.body.removeChild(loadingDiv);
    }
    await appRunner.runApp();
  },
});
</code></pre>
<br>

<h2 id="references">REFERENCES</h2>
<ul>
<li><a href="https://docs.flutter.dev/platform-integration/web/initialization">https://docs.flutter.dev/platform-integration/web/initialization</a></li>
<li><a href="https://css-loaders.com/pulsing/">https://css-loaders.com/pulsing/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[flutter web을 배포해보자 (with github page)]]></title>
            <link>https://velog.io/@dev_leewoooo/flutter-web%EC%9D%84-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90-with-github-page</link>
            <guid>https://velog.io/@dev_leewoooo/flutter-web%EC%9D%84-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90-with-github-page</guid>
            <pubDate>Wed, 24 Jul 2024 15:04:10 GMT</pubDate>
            <description><![CDATA[<h1 id="flutter-web을-github-page에-배포하기">flutter web을 github Page에 배포하기</h1>
<p>flutter web을 이용해 개발 중인 토이프로젝트를 배포하기 위한 스터디 (with github action)</p>
<br>

<h2 id="goal">Goal</h2>
<ul>
<li>flutter를 github page를 통해 배포하기</li>
<li>스터디 한 내용을 문서화 하기</li>
</ul>
<br>

<h2 id="github-action에서-사용-할-토큰-발급">github action에서 사용 할 토큰 발급</h2>
<p>github action을 이용하여 배포를 할 것이기 때문에 먼저 action에서 사용할 키를 발급받아야 한다.</p>
<p><a href="https://github.com/settings">settings</a>에서 제일 하단에 있는 <a href="https://github.com/settings/apps">Developer Settings</a>에서 발급을 받을 수 있다. 작성일 기준으로 <code>Personal access tokens</code>를 발급받는 방법을 2가지 제공하고 있다.</p>
<ol>
<li>Fine-grained tokens (Beta)</li>
<li>Tokens (classic)</li>
</ol>
<p>필자는 2번을 선택하였으며 이유는 1번 방법이 베타이기도 하고 웹을 한번 배포하고 토큰에 대한 신경을 쓰고 싶지 않은데 1번의 경우 <strong>만료일이 없는 토큰을 제공하지 않기 떄문이다.</strong></p>
<br>

<h3 id="토큰-발급">토큰 발급</h3>
<p><a href="https://github.com/settings/tokens">Tokens</a>에 들어가서 우측 상단에 있는 Generate new token을 누르면 아래와 같은 화면을 볼 수 있다.</p>
<p>이 때 중요한 것은 action을 통해 배포할 때 만료일을 <code>No expiration</code>으로 설정 후 해당 토큰의 권한을 설정할 수 있는데 배포를 위해 <code>repo</code>, <code>workflow</code> 두 개를 선택 후 만들면 된다.</p>
<p><strong>생성 후 발급되는 토큰은 잘 메모해두자!</strong></p>
<img src='https://velog.velcdn.com/images/dev_leewoooo/post/ba2cf968-7ffb-4163-99c9-6e7fda4268ab/image.png'>

<br>

<h3 id="repository-토큰-설정">repository 토큰 설정</h3>
<p>생성 된 토큰을 배포할 repository에 정의해주면 토큰 발급이 끝나게 된다. 배포 할 repository로 이동하여 아래에 버튼을 눌러 이전에 발급한 토큰을 정의하면 된다.</p>
<p>추가할 때 이름은 action을 작성할 때 사용 될 예정이다.</p>
<img src='https://velog.velcdn.com/images/dev_leewoooo/post/c13bae97-8c16-45df-aa0e-103dc013310c/image.png'>

<br>

<h2 id="action-작성하기">action 작성하기</h2>
<p>github action은 위치가 정해져있다. 프로젝트의 root에서 <code>.github</code> 디렉토리를 만들고 하위에 <code>workflows</code>를 작성한 후 하위로 action 파일들을 작성해나가면 된다. (이름은 크게 중요하지 않음.)</p>
<pre><code>// ex
.github
    ㄴ workflows
        ㄴ web_static_deploy.yml</code></pre><p>flutter web을 배포하기 위한 스크립트는 아래와 같다.</p>
<pre><code class="language-yaml">name: deploy flutter web to github pages

# 1
on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # 1
      # https://github.com/marketplace/actions/checkout
      - name: Checkout
        uses: actions/checkout@v4

      # 2
      # https://github.com/marketplace/actions/flutter-action
      - name: Setup flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
          flutter-version: 3.19.0

      # 3
      - name: Build web
        run: flutter build web --base-href &quot;/${{ github.event.repository.name }}/&quot;

      # 4
      # https://github.com/marketplace/actions/github-pages-action
      - name: Deploy to github pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.STATIC_DEPLOY_KEY }}
          publish_dir: ./build/web</code></pre>
<ol>
<li><p>action에서 배포 대상의 repository의 코드를 가져온다.</p>
</li>
<li><p>action에서 flutter를 사용하기 위해 flutter를 설치한다.</p>
</li>
<li><p>설치한 flutter를 이용하여 repository의 코드를 build 한다. 이 때 빌드 옵션으로 <code>--base-href</code> 옵션을 줄 수 있다.</p>
</li>
<li><p>github page에 배포를 진행한다.</p>
</li>
</ol>
<br>

<h2 id="flutter-base-href-설정하기">flutter base href 설정하기</h2>
<p>flutter web 프로젝트에 <code>index.html</code>을 보면 아래와 같이 되어있는 부분을 확인해보자.</p>
<pre><code class="language-html">&lt;!--
  If you are serving your web app in a path other than the root, change the
  href value below to reflect the base path you are serving from.

  The path provided below has to start and end with a slash &quot;/&quot; in order for
  it to work correctly.

  For more details:
  * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

  This is a placeholder for base href that will be replaced by the value of
  the `--base-href` argument provided to `flutter build`.
--&gt;
&lt;base href=&quot;$FLUTTER_BASE_HREF&quot; /&gt;</code></pre>
<p>직역을 하자면 web을 호스팅 할 때 서버의 root가 아닌 경우 href를 수정하라는 내용이다. github page로 배포 되는 web의 root 주소는 <code>https://${Owner}.github.io/${Repository}</code>와 같이 생겼다.</p>
<p>그렇기 때문에 href를 <code>&lt;base href=&quot;/${repository명}/&quot;&gt;</code>과 같이 수정해줘야 한다.</p>
<p>하지만 해당 부분을 web build 할 때 정의하여 사용할 수 있다. <a href="#action-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0">action 작성하기</a>에서 작성했던 것 처럼 <code>--base-href &quot;/${{ github.event.repository.name }}/&quot;</code>와 같이 작성해주면 빌드 시 <code>index.html</code>에 <code>&lt;base href=&quot;/${repository명}/&quot;&gt;</code> 부분에 정의된 것을 볼 수 있다.</p>
<br>

<h2 id="action-확인하기">action 확인하기.</h2>
<p>action이 성공적으로 돌면 자동으로 <code>gh-pages</code>라는 브랜치에 현재 내용들을 커밋 후 푸쉬하는 것을 볼 수 있다.</p>
<img src ='https://velog.velcdn.com/images/dev_leewoooo/post/2e5afca9-8f2e-4020-ba3e-d511888bc4a5/image.png'>

<h3 id="repository-설정하기">repository 설정하기</h3>
<p>action이 끝나면 배포가 성공한 줄 알았지만 한 단계 더 남아있었다. repository의 설정에 <code>Pages</code>의 사이드 메뉴에가서 해당 branch로 github pages를 배포하겠다고 설정해주어야 한다.</p>
<img src='https://velog.velcdn.com/images/dev_leewoooo/post/bd8f4f63-4ab1-41b4-bbea-4de2915feb86/image.png'>

<p>위와 같이 설정하면 action이 하나 더 실행되는데 해당 action이 끝나면 최종적으로 배포가 완료된다.</p>
<img src='https://velog.velcdn.com/images/dev_leewoooo/post/118fcca3-fb4f-40bd-bb61-d4e0d71be4c3/image.png'>

<br>

<h2 id="결과">결과</h2>
<img src='https://velog.velcdn.com/images/dev_leewoooo/post/5146b64a-062b-491d-b5b3-f2d2b21c7239/image.png'>

<br>

<h2 id="references">REFERENCES</h2>
<ul>
<li><a href="https://velog.io/@hetarho/Fluttergithub-action%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-gh-page-%EB%A1%9C-web-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0#flutter-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0">https://velog.io/@hetarho/Fluttergithub-action%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-gh-page-%EB%A1%9C-web-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0#flutter-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[supabase 회원가입하기 (with flutter)]]></title>
            <link>https://velog.io/@dev_leewoooo/Flutter-Google-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%95%98%EA%B8%B0-with-supabase</link>
            <guid>https://velog.io/@dev_leewoooo/Flutter-Google-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%95%98%EA%B8%B0-with-supabase</guid>
            <pubDate>Wed, 08 May 2024 15:36:25 GMT</pubDate>
            <description><![CDATA[<h1 id="supabase-회원가입하기-with-flutter">supabase 회원가입하기 (with flutter)</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>Supabase와 이메일을 이용하여 회원가입 처리하기</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p>Flutter를 이용하여 토이프로젝트를 진행할 때 회원가입에 대한 처리를 하기 위해 supabase를 이용하였으며 해당 내용을 글로 정리하기 위함.</p>
<br>

<h2 id="회원가입-흐름-with-이메일">회원가입 흐름 (with 이메일)</h2>
<p>supabase에서는 여러가지의 회원가입 방법을 제공해주지만 필자는 이메일을 이용하여 회원가입에 대한 처리를 하였다.</p>
<p>supabase를 이용하여 회원가입을 하는 Process는 아래와 같다.</p>
<br>

<pre><code>유저의 정보 입력 ==&gt; 이메일 중복 가입 Validation ==&gt; 회원가입 완료, 사용자의 이메일 검증</code></pre><br>

<h3 id="1-유저의-정보-입력">1. 유저의 정보 입력</h3>
<p>유저에가 input으로 받을 정보는 아래와 같다. 회원가입에 필요한 정보들은 <strong>이메일, 비밀번호 이 2가지를 필수로 받는다.</strong></p>
<p>추가적으로 사용자의 이름을 입력받아 supabase의 <code>auth.users</code>테이블에 <code>raw_user_meta_data</code>에 넣을 것이다. (<code>raw_user_meta_data</code>에는 추 후 oauth를 이용할 때 유저의 정보들이 이 <code>column</code>으로 들어온다.)</p>
<p>먼저 구현 된 UI는 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/12e88f43-b925-4037-a7c1-8f9f1f641bb3/image.png" alt=""></p>
<p>총 3개의 input을 입력 받도록 하고 <code>form</code> 위젯을 이용하여 validation을 진행한다.</p>
<br>

<h3 id="2-이메일-중복-가입-validation">2. 이메일 중복 가입 Validation</h3>
<p>이메일을 이용하여 회원가입을 할 때 사용자의 이메일 검증이 정상적으로 완료되지 않으면 <strong>동일한 이메일로 계속해서 회원가입이 진행된다.</strong> (비밀번호를 다르게 입력하여도 초반에 입력한 비밀번호는 초기화 되지 않는다.)</p>
<p>즉 같은 이메일로 supabase에서 제공하는 <code>auth.signUp</code>를 호출해도 별도의 <code>exception</code>이 존재하지 않는다는 것이다.</p>
<pre><code class="language-dart">// 이 부분을 계속해서 통과한다. signUp이 별도의 exception을 발생시키지 않음.
final response = await onSubmit() async {
  final response = await supabase.auth.signUp(
    email: email,
    password: password,
    data: {&#39;full_name&#39;: name},
  );
  // ...
}</code></pre>
<p><a href="https://github.com/supabase/auth/issues/1517">github-issue</a>를 보면 이슈가 닫혔다 열렸다를 계속 반복하고 있지만 해당 이슈의 댓글 중 선택한 해결방법은 아래와 같다. (<a href="https://github.com/supabase/auth/issues/1517#issuecomment-2040989651">해결방법-댓글</a>)</p>
<pre><code class="language-dart">final isNotExist = response.user?.identities != null;
final isEmptyList = response.user!.identities!.isNotEmpty;
if (isNotExist &amp;&amp; isEmptyList) {
  // already email exists
}</code></pre>
<p><code>auth.signUp</code>을 호출하여 <code>response</code>를 얻을 수 있으며 <code>response</code>의 타입은 <code>supabase</code>에서 제공하는 <code>AuthResponse</code>타입이다.</p>
<p><code>response</code>에는 <code>user</code>라는 <code>property</code>가 있고 <code>user</code>안에 <code>identities</code>라는 <code>property</code>가 있다.</p>
<p>결론은 <strong><code>identities</code>를 이용하여 validation 처리를 할 수 있다.</strong></p>
<ol>
<li><code>identities</code>의 배열이 비어있다. -&gt; 이미 회원가입이 되어있는 User다.</li>
<li><code>identities</code>의 배열이 비어있지 않다. -&gt; 회원가입이 가능하다.</li>
</ol>
<br>

<h3 id="3-회원가입-완료-사용자의-이메일-검증">3. 회원가입 완료, 사용자의 이메일 검증</h3>
<br>

<p>위와 같이 회원가입을 하게 되면 데이터베이스(pg)의 <code>auth.users</code>테이블에 한 개의 <code>row</code>가 생성되게 된다. </p>
<p>이 때 생성된 <code>raw_user_meta_data</code>의 <code>column</code>을 확인해보면 <code>jsonb</code>형식으로 데이터가 들어가 있는데 <code>json</code> 데이터 중 <code>key</code>가 <code>email_verified</code>인 프로퍼티가 있을 것이다. (이 <code>column</code>에 회원가입 시 입력받은 이름이 포함되어있다.)</p>
<p>또한 그 값이 <code>false</code>로 되어 있을텐데 아직 <strong>사용자 이메일의 유효성 검증이 통과하지 않았다는 것이다.</strong></p>
<p>해당 값을 <code>true</code>로 변경하기 위해서는 유저가 회원가입 시 입력한 이메일의 메일함에서 컨펌 이메일의 링크를 클릭하면 된다.</p>
<p>추가로 <strong>이메일이 검증되어 있지 않은 상태로 로그인을 시도</strong>하면 <code>Email not confirmed</code> 에러 메세지를 확인할 수 있다.</p>
<p>컨펌이메일은 아래와 같으며 <code>auth.signUp</code>이 호출되면 발송된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/29449df3-992c-4e3c-8b4b-fe2d858ceb69/image.png" alt=""></p>
<h3 id="주의사항-컨펌메일-발송">주의사항 (컨펌메일 발송)</h3>
<p>회원가입시 컨펌 이메일을 발송하는데 별도의 SMTP 서버를 supabase에 정의하지 않으면 <strong>발송제한이 있다.</strong></p>
<p>때문에 추가로 SMTP 서버를 제공하여 사용하는 것을 추천한다. 이 내용에 관련되서는 <a href="https://velog.io/@dev_leewoooo/supabase%EC%97%90-Customer-SMTP-Provider-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-with-Resend">이전 글</a>에 작성해두었다.</p>
<p>SMTP를 설정하면 컨펌 메일을 발송하는 것에 대해 편안함을 느낄 수 있을 것이다.</p>
<br>

<h2 id="references">REFERENCES</h2>
<ul>
<li><a href="https://velog.io/@dev_leewoooo/supabase%EC%97%90-Customer-SMTP-Provider-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-with-Resend">https://velog.io/@dev_leewoooo/supabase%EC%97%90-Customer-SMTP-Provider-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-with-Resend</a></li>
<li><a href="https://supabase.com/docs/guides/auth/passwords?queryGroups=language&amp;language=dart&amp;queryGroups=flow&amp;flow=implicit#signing-up-with-an-email-and-password">https://supabase.com/docs/guides/auth/passwords?queryGroups=language&amp;language=dart&amp;queryGroups=flow&amp;flow=implicit#signing-up-with-an-email-and-password</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[supabase에 Customer SMTP Provider 적용하기 (with Resend)]]></title>
            <link>https://velog.io/@dev_leewoooo/supabase%EC%97%90-Customer-SMTP-Provider-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-with-Resend</link>
            <guid>https://velog.io/@dev_leewoooo/supabase%EC%97%90-Customer-SMTP-Provider-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-with-Resend</guid>
            <pubDate>Tue, 26 Mar 2024 15:22:32 GMT</pubDate>
            <description><![CDATA[<h2 id="goal">Goal</h2>
<ul>
<li>supabase에 개발 모드에서 발송 제한걸려 있는 부분 해결하기</li>
<li>supabase에 Custom SMTP Provider를 적용하여 authorization 관련 email을 처리할 수 있는 구조 만들기</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p>토이프로젝트에서 Supabase를 이용하여 이메일을 이용한 회원가입 및 인증처리를 구현하다 아쉬운 점을 발견하였다.</p>
<p>그것은 바로 빌트인 되어있는 이메일 서비스는 제한이 있다는 것이다. 그 제한은 생각보다 상당히 강력했다. </p>
<p><strong>시간 당 보낼 수 있는 이메일은 3개로 제한이 되어있다는 것이다. 회원가입을 테스트 하면서 3번의 이메일을 발생하면 다음 테스트까지 3시간을 기다려야한다는 것이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/e6df4395-60cc-40dc-b60f-3c3e2bc01e88/image.png" alt=""></p>
<p>추가적으로 Production level에서는 사용할 수 없으며 해당 제한을 변경하고 싶다면 Customer SMTP Provider 연결은 불가피 해보였다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/86e24bc7-85de-4272-ae5e-4bb7e243b0f9/image.png" alt=""></p>
<p>제한한 횟수를 전부 소진한 이 후 메일을 발송하려 하면 아래와 같은 Error를 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/db65d42f-5a76-4643-8237-d7db2daaed26/image.png" alt=""></p>
<br>

<h2 id="customer-smtp-provider-선택">Customer SMTP Provider 선택</h2>
<p>supabase에서 SMTP Provider로 추천하는 선택지는 3가지가 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/35a4b3b4-a3be-4600-907c-af93fa34e3b6/image.png" alt=""></p>
<p>필자가 SMTP Provider를 선택하는 기준은 아래와 같았다.</p>
<ol>
<li>supabase와 연동이 쉽게 이뤄졌으면 좋겠다.</li>
<li>무료 plan이 존재하며 개발을 진행 할 때 충분히 이용할 수 있었으면 좋겠다.</li>
<li>발송 한 메일에 대한 이력 및 Log를 보기 간편했으면 좋겠다.</li>
</ol>
<p>위 조건들을 대입해 본 결과 필자는 <strong>Resend</strong>를 선택하게 되었다.</p>
<p><strong>Resend</strong>를 선택한 이유는 위 3가지를 전부 충족시켰다.</p>
<ol>
<li>supabase와 연동을 이미 콘솔에서 제공하고 있다.</li>
<li>무로 plan을 사용할 때 월에 3,000건, 시간당 100건 까지 제공하여 충분했다.</li>
<li>Dashboard를 제공하여 메일의 발송 및 상태를 확인할 수 있었다.</li>
</ol>
<p>아래는 Resend의 가격 정책이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/2947991d-3d79-44f8-a41a-4083d16eae8e/image.png" alt=""></p>
<br>

<h2 id="resend에-도메인-등록하기">Resend에 도메인 등록하기</h2>
<p>필자는 위 과정을 진행하기 전까지 가지고 있는 도메인이 없어 <a href="https://hosting.kr/">hosting.kr</a>에서 도메인 구매를 진행하였다. (<del>저렴한 도메인 중 하나를 골라 가져왔다.</del>)</p>
<p>Resend에서 도메인을 등록하는 순서는 다음과 같다.</p>
<ol>
<li>도메인 등록</li>
<li>DNS 설정하기</li>
<li>등록 한 도메인 검증</li>
</ol>
<br>

<h3 id="1-도메인-등록">1. 도메인 등록</h3>
<p>가지고 있는 도메인 혹은 구메한 도메인을 Resend의 도메인 Tab에 들어가 등록하면 된다.</p>
<p>현재 무료 Plan을 이용하고 있기 때문에 1개의 도메인만 등록이 가능하며 region 또한 ua-east-1로 고정되어 있다.</p>
<br>

<h3 id="2-dns-설정하기">2. DNS 설정하기</h3>
<p>도메인을 등록하면 해당 도메인의 대시보드를 확인할 수 있다. 대시보드에는 MX, TXT 설정을 할 수 있는 정보가 적혀있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/d452f99b-843f-4778-a3ab-9b9de973e809/image.png" alt=""></p>
<p>위에 보여지는 MX 및 TXT를 도메인을 구매한 곳에서 등록을 해주면 된다. Hosting.kr 같은 경우 아래 영역에서 입력이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/95f019cc-2858-4e4f-bd3b-e61afd195b93/image.png" alt=""></p>
<br>

<h3 id="3-등록-한-도메인-검증">3. 등록 한 도메인 검증</h3>
<p>설정이 전부 완료 되었다면 아까 대시보드에서 우측 상단에 있는 <strong>Verifiy DNS Records</strong>를 누르면 상태가 전부 <strong>Pending</strong> 상태로 변경 될 것이다.</p>
<p>이 후 시간이 지나면 유효성 검증이 완료 되고 상태가 <strong>Verified</strong>로 변경 된 것을 볼 수 있을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/44ebf99f-d16a-4e01-ae32-824a93f46895/image.png" alt=""></p>
<br>

<h2 id="resend와-supabase-연결하기">Resend와 supabase 연결하기</h2>
<p>Resend에 가입을 하고 setting 탭에 들어가보면 아래와 같이 <strong>Intergrations</strong> 라는 탭이 있다.</p>
<p>해당 탭을 누르면 아래와 같이 supabase와 연결할 수 있는 콘솔을 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/20644af6-77e8-46cc-8120-c438d868accf/image.png" alt=""></p>
<p>supabase와 연결하기를 누르면 Resend가 어떠한 권한들을 이용할 것인지와 supabase의 어떠한 organization과 연결할 것 인지 선택해주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/7da3a854-35e2-4e3d-8b22-7d467a70d394/image.png" alt=""></p>
<p>organization 선택이 완료되면 다음으로 넘어가게 되는데 적용할 프로젝트 및 도메인을 선택하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/caa0beca-2028-4ac4-8a94-4c96d6149d5c/image.png" alt=""></p>
<p>마지막으로 SMTP 설정만 정의하면 된다. 여기서 메일 발송하는 발송자의 이름 및 이메일을 정의할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/15224c26-cded-4da3-8057-f24fcf799e94/image.png" alt=""></p>
<p>이 후 <strong>Configure SMTP integration</strong>을 누르면 완료 된다.</p>
<br>

<h2 id="supabase-smtp-설정-확인하기">supabase SMTP 설정 확인하기</h2>
<p>Resend에서 integration이 정상적으로 완료 되었다면 supabase에서는 자동으로 Custom SMTP Provider 사용 옵션이 활성화 되어 있고 Sender email 및 Sender name이 Resend에서 정의 한 대로 보여질 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/424a68ef-70f8-423a-acbb-a16ac9e3433f/image.png" alt=""></p>
<p>추가적으로 시간 당 3건으로 제한되어 있던 메일 발송 건 수도 정의가 가능하다.</p>
<p>Resend에서는 무료 plan을 이용할 때 100건을 제공하니 100건으로 정의해보겠다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/eed925c6-8c6c-4ee2-8253-a64f1094440f/image.png" alt=""></p>
<br>

<h2 id="메일-발송-확인-및-log-보기">메일 발송 확인 및 Log 보기</h2>
<p>메일 발송 건수는 Resend 대시보드에서 아래와 같이 쉽게 확인이 가능하다. 그래프로 제공되며 원하는 범위 지정 또한 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/f5151850-c623-4c94-ae10-191058bb5629/image.png" alt=""></p>
<p>로그는 로그 탭으로 들어가면 아래와 같이 리스트를 제공해준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/8f85cc19-5cba-40fd-9bbf-c7be1f08697b/image.png" alt=""></p>
<p>메일을 발송할 때 마다 하나의 ROW가 생성되며 들어가면 해당 건에 대한 상세를 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/1197c170-0e8d-48d1-97ca-7b72f28c52ee/image.png" alt=""></p>
<br>

<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/dfeb805a-f52f-4337-a522-a071d4609df3/image.png" alt=""><img src="https://velog.velcdn.com/images/dev_leewoooo/post/e7143c6a-5a52-4c62-9a80-b7c5f8a9777d/image.png" alt=""></p>
<p>기존에는 위 사진과 같이 왔다면 설정이 끝나고 다시 시도해보면 아래와 같이 정의한 이름과 동이한 발신자에게 메일이 도착한 것을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/58e1bae6-56e5-4af2-af20-93099421413c/image.png" alt=""></p>
<br>

<h2 id="요약">요약</h2>
<ol>
<li>도메인 구매 (있을 경우 생략)</li>
<li>DNS 설정하기</li>
<li>Resend에서 supabase integration 설정 (프로젝트, 도메인, 발신자 정보 정의)</li>
<li>supabase limit 변경</li>
</ol>
<br>

<h2 id="references">REFERENCES</h2>
<ul>
<li><a href="https://resend.com/home">https://resend.com/home</a></li>
<li><a href="https://supabase.com/docs/guides/auth/auth-smtp">https://supabase.com/docs/guides/auth/auth-smtp</a></li>
<li><a href="https://www.youtube.com/watch?v=51vzcGEmjRI">https://www.youtube.com/watch?v=51vzcGEmjRI</a></li>
<li><a href="https://www.youtube.com/watch?v=7HNJLUMV_TY">https://www.youtube.com/watch?v=7HNJLUMV_TY</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter 프로젝트에서 script를 정의해서 사용하기 (like package.json)]]></title>
            <link>https://velog.io/@dev_leewoooo/Flutter-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-script%EB%A5%BC-%EC%A0%95%EC%9D%98%ED%95%B4%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-like-package.json-80kwd1u9</link>
            <guid>https://velog.io/@dev_leewoooo/Flutter-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-script%EB%A5%BC-%EC%A0%95%EC%9D%98%ED%95%B4%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-like-package.json-80kwd1u9</guid>
            <pubDate>Wed, 20 Mar 2024 11:17:17 GMT</pubDate>
            <description><![CDATA[<h2 id="goal">Goal</h2>
<ul>
<li><code>node.js</code>기반의 프로젝트들 처럼 <code>pubspec.yaml</code> 파일에 스크립트 정의하여 사용하기.</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p>Flutter 프로젝트를 진행하면서 node.js 기반처럼 스크립트를 정의해두고 불러와 사용할 수 있는 방법이 없는지 찾던 도중 해당 기능을 제공해주는 derry, rps, flutter_scripts 등의 라이브러리을 찾을 수 있었다. </p>
<p>가장 직관적인 <code>derry</code>를 이용하여 구현해보기로 하였다. (대부분 라이브러리들이 1버전을 넘지 못했으며 그나마 <code>derry</code>가 1버전을 넘겼으며 github start 수도 가장 많았다.)</p>
<br>

<h2 id="라이브러리-설치">라이브러리 설치</h2>
<p>라이브러리를 설치하는건 간단하다. 프로젝트에 추가를 하는 것이 아니라 <code>dart</code>에 글로벌로 설치하여 사용하면 된다.</p>
<pre><code class="language-zsh">dart pub global activate derry</code></pre>
<p>처음으로 라이브러리를 설치하는 경우 <code>pub cache</code>의 경로가 환경변수로 잡혀있지 않을 것이다.</p>
<p>그런경우 <code>pub cache</code>의 경로를 아래와 같이 추가해주면 된다.</p>
<pre><code class="language-zsh"># pub cache
export PATH=&quot;$PATH&quot;:&quot;$HOME/.pub-cache/bin&quot;</code></pre>
<br>

<h2 id="스크립트-정의하기">스크립트 정의하기</h2>
<p>라이브러리 doc를 확인하며 <code>pubspec.yaml</code>파일에 스크립트를 추가하면 된다. <a href="https://velog.io/@dev_leewoooo/Flutter-dotenv%EB%A1%9C-%ED%99%98%EA%B2%BD-%EB%82%98%EB%88%84%EA%B8%B0">이전 글</a>에서 프로젝트를 실행시킬 때 환경변수를 입력해 실행 시킬 수 있었던 것을 볼 수 있을 것이다.</p>
<pre><code class="language-zsh">flutter run --dart-define=env=dev</code></pre>
<p>위 스크립트를 <code>derry</code>를 이용하여 정의하면 아래와 같다.</p>
<pre><code class="language-yaml">scripts:
    run_dev: flutter run --dart-define=env=dev</code></pre>
<p>위와 같이 정의 후 터미널에서 <code>derry run_dev</code>를 입력하면 프로젝트가 정상적으로 실행되는 것을 볼 수 있다.</p>
<p>추가적으로 필자는 supabase 관련 스크립트를 정의할 때도 사용하였는데 <code>derry</code>는 <code>nested script</code>를 지원하기 때문에 아래와 같은 형식으로 스크립트 작성이 가능하다.</p>
<pre><code class="language-zsh">@pubspec.yaml
scripts:
    supabase:
      migration:
        - supabase migration new $1
      # ...

@zsh
derry run supabase migration -- ${마이그레이션 스크립트 명}

# ex
derry run supabase migration -- create_profile_table</code></pre>
<p>derry를 이용할 때 <code>argument</code>를 이용하고 싶다면 <code>$순번</code>를 스크립트에 정의하고 실행할 때 <code>-- ${value}</code>를 순차적으로 입력하면 된다.</p>
<br>

<h2 id="references">REFERENCES</h2>
<ul>
<li><a href="https://pub.dev/packages/derry">derry</a></li>
<li><a href="https://github.com/frencojobs/derry/issues/63">derry_arguments_issue</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter에  supabase 적용하기]]></title>
            <link>https://velog.io/@dev_leewoooo/flutter%EC%97%90-supabase-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_leewoooo/flutter%EC%97%90-supabase-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 19 Mar 2024 16:06:48 GMT</pubDate>
            <description><![CDATA[<h1 id="flutter에-supabase-적용하기">Flutter에 supabase 적용하기</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>Flutter 프로젝트에 supabase를 적용하여 client 인스턴스 생성하기</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p>이전 flutter로 토이프로젝트를 시작할 때 현재 Backend 개발자로 현업을 진행하고 있기 때문에 <strong>풀스택</strong>으로 진행하고자 하였다.</p>
<p>하지만 FE도 구현하고 BE도 구현하고자 하다보니 두 마리의 토끼를 둘 다 놓쳐 해당 프로젝트를 마무리 짓지 못한 경험이 있다.</p>
<p>그러다보니 인증, DB연결과 같은 역할을 해주는 서비스를 찾다 <code>firebase</code>와 <code>supabase</code>를 찾게 되었다. </p>
<p>결론부터 이야기하자면 필자는 <code>supabase</code>를 선택하였으며 선택한 이유는 아래와 같다.</p>
<ol>
<li>오픈소스</li>
<li><code>Postgresql</code>를 이용 (<code>firebase</code>는 <code>NoSql</code>기반)</li>
</ol>
<p>이 2가지가 <code>supabase</code>를 선택하는데 많은 영향을 주었다. (나중에 서비스가 커지게 된다면 <code>NoSql</code>보다 <code>PG</code>가 마이그레이션 하기 수월할 것도 포함이다.)</p>
<br>

<h2 id="프로젝트-생성">프로젝트 생성</h2>
<p>프로젝트 생성은 간단하다. 깃허브 계정을 이용하여 <code>supabase</code>에 가입하였으며 Organization을 생성한 후 project를 생성해주기만 하면 된다.</p>
<p><code>Region</code>은 다양하게 제공되며 필자는 <code>Seoul</code>을 선택하였다. DB 비밀번호도 설정이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/9cd50643-266f-4f3e-9ff5-9a2bdba3058f/image.png" alt=""></p>
<br>

<h2 id="flutter-프로젝트에-적용하기">Flutter 프로젝트에 적용하기</h2>
<p>Flutter에서 <code>supabase</code>를 이용하려면 아래와 같은 명령어를 이용하여 라이브러리를 추가하면 된다. </p>
<pre><code class="language-zsh">flutter pub add supabase_flutter</code></pre>
<p>라이브러리를 추가하였다면 project를 생성하였을 때 부여받은 <code>Project URL</code>과 <code>API Key</code>를 이용하여 Flutter에 적용할 수 있다.</p>
<p>2개의 키는 프로젝트 Home에서 조금만 내리면 확인이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/5ce51873-b0bc-4d10-9aa1-f694621d46c6/image.png" alt=""></p>
<p>App이 실행되는 <code>main.dart</code>로 이동하여 아래와 같은 코드만 추가해주면 적용이 끝난다.</p>
<pre><code class="language-dart">@ constant.dart
// 1
final SUPABASE_URL = dotenv.get(&#39;SUPABASE_URL&#39;);
final SUPABASE_API_KEY = dotenv.get(&#39;SUPABASE_API_KEY&#39;);

@ main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  const envDivision = String.fromEnvironment(&#39;env&#39;, defaultValue: &#39;dev&#39;);
  await dotenv.load(fileName: &#39;./env/.env.$envDivision&#39;);

  // 2
  await Supabase.initialize(
    url: SUPABASE_URL,
    anonKey: SUPABASE_API_KEY,
    debug: dotenv.get(&#39;FLUTTER_ENV&#39;, fallback: &#39;development&#39;) == &#39;development&#39;,
  );
  runApp(const MyApp());
}

// 3
final supabase = Supabase.instance.client;</code></pre>
<ol>
<li>상수를 별도로 관리하는 파일을 만들어 dotenv를 이용하여 <code>supabase</code>의 key값들을 정의한다.</li>
<li><code>supabase</code>라이브러리를 이용하여 <code>supabase</code>를 초기화 해준다.</li>
<li><code>supabase</code>의 client 인스턴스를 변수에 선언한다.</li>
</ol>
<p>추가적으로 <code>supabase</code>를 초기화를 할 때 부여할 수 있는 옵션들이 있다 해당 옵션들은 <a href="https://supabase.com/docs/reference/dart/initializing">supabase_flutter_initializing</a>에서 확인이 가능하다.</p>
<br>

<h2 id="결과">결과</h2>
<p>정상적으로 key값들을 이용하여 <code>supabase</code>의 인스턴스화를 성공했다면 터미널에서 아래와 같은 로그를 볼 수 있을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/c3a1904f-c516-4025-958f-d1c09c3bd0a4/image.png" alt=""></p>
<br>

<h2 id="references">REFERENCES</h2>
<ul>
<li><a href="https://supabase.com/docs/reference/dart/start">supabase_flutter_docs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter dotenv로 환경 나누기]]></title>
            <link>https://velog.io/@dev_leewoooo/Flutter-dotenv%EB%A1%9C-%ED%99%98%EA%B2%BD-%EB%82%98%EB%88%84%EA%B8%B0</link>
            <guid>https://velog.io/@dev_leewoooo/Flutter-dotenv%EB%A1%9C-%ED%99%98%EA%B2%BD-%EB%82%98%EB%88%84%EA%B8%B0</guid>
            <pubDate>Tue, 19 Mar 2024 15:18:23 GMT</pubDate>
            <description><![CDATA[<h2 id="flutter-dotenv-설정하기">Flutter DotEnv 설정하기</h2>
<h2 id="goal">Goal</h2>
<ul>
<li>Flutter에서 env를 이용하여 환경변수 불러오기</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p>다른 언어들을 사용하다 보면 환경변수 설정을 코드 Level에서 정의하는 것이 아니라 <code>.env</code>파일을 구성하여 애플리케이션이 Load 될 때 읽어와 사용한다.</p>
<p>Flutter에서는 어떻게 하는지 간단히 알아보겠다.</p>
<br>

<h2 id="how">How</h2>
<p>역시 다른 언어들의 라이브러리와 동일하게 <code>dotenv</code>를 제공해주는 라이브러릴 쉽게 찾을 수 있었다.</p>
<br>

<h3 id="pubspecyaml에-라이브러리-추가하기">pubspec.yaml에 라이브러리 추가하기</h3>
<pre><code class="language-yaml">dependencies:
flutter:
    sdk: flutter
# ... 
flutter_dotenv: ^5.1.0</code></pre>
<h3 id="pubspecyaml에-env-파일을-assets에-추가하기">pubspec.yaml에 <code>.env</code> 파일을 assets에 추가하기</h3>
<p><code>.env</code>파일을 Flutter에 포함될 수 있도록 assets에 추가한다.</p>
<pre><code class="language-yaml">flutter:
  # ...  
  assets:
    - .env</code></pre>
<h3 id="maindart에서-env파일-로드하기">main.dart에서 <code>.env</code>파일 로드하기</h3>
<pre><code class="language-dart">// --dart-define=SOME_VAR=SOME_VALUE
void main() async {
  // 1
  WidgetsFlutterBinding.ensureInitialized();

  // 2
  const envName = String.fromEnvironment(&#39;env&#39;, defaultValue: &#39;dev&#39;);


  // 3
  await dotenv.load(fileName: &#39;./env/env.$envName&#39;);


  runApp(const _App());
}</code></pre>
<ol>
<li><p><code>WidgetsFlutterBinding.ensureInitialized();</code>는 Flutter <strong>프레임워크가 준비가 되어있는지 Check하는 코드이다.</strong> <code>flutter_dotenv</code> 라이브러리는 flutter 위에서 동작되다 보니 Flutter가 준비되어야 사용이 가능하기 때문이다.</p>
</li>
<li><p><code>dotenv</code>를 사용하는 면에서는 필수적인 코드는 아니지만 앱을 실행할 때 환경에 따라 다른 <code>env</code>를 사용하기 위해 정의하였다. <code>String.fromEnvironment</code> 코드를 통해 앱을 실행할 때 정의한 변수를 가져올 수 있으며 변수를 정의하는 방법은 아래와 같다.</p>
</li>
</ol>
<pre><code class="language-zsh">flutter run --dart-define=SOME_VAR=SOME_VALUE --dart-define=OTHER_VAR=OTHER_VALUE

# or

flutter build --dart-define=SOME_VAR=SOME_VALUE --dart-define=OTHER_VAR=OTHER_VALUE</code></pre>
<ol start="3">
<li><code>flutter_dotenv</code> 라이브러리를 이용하여 미리 정의한 환경변수 파일을 load한다.</li>
</ol>
<br>

<h2 id="사용하기">사용하기</h2>
<p>사용하는 곳에서는 아래와 같이 <code>get</code>을 이용하면 된다. 만약 <code>NullSafe</code>를 사용하고자 한다면 <code>maybeGet</code>을 이용하면 된다.</p>
<p>추가적으로 없을 때 default 환견변수를 <code>fallback</code>이라는 인자를 정의하여 더욱 안전하게 사용할 수 있다.</p>
<pre><code class="language-dart">dotenv.get(&#39;환경변수 키 값&#39;);

dotenv.maybeGet(&#39;환경변수 키 값&#39;, fallback: null)</code></pre>
<br>

<h2 id="references">REFERENCES</h2>
<ul>
<li><a href="https://pub.dev/packages/flutter_dotenv">flutter-dotenv</a></li>
<li><a href="https://api.flutter.dev/flutter/dart-core/String/String.fromEnvironment.html">flutter-doc(String.fromEnvironment)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter 웹 호스팅하기 (with Firebase hosting)]]></title>
            <link>https://velog.io/@dev_leewoooo/Flutter-%EC%9B%B9-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0-with-Firebase-hosting</link>
            <guid>https://velog.io/@dev_leewoooo/Flutter-%EC%9B%B9-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0-with-Firebase-hosting</guid>
            <pubDate>Tue, 19 Mar 2024 07:10:48 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<blockquote>
<p>사이드프로젝트로 Flutter를 이용한 앱을 계획하고 있다. Flutter는 멀티플랫폼을 지원하기 때문에 관리자 웹 페이지를 flutter로 만들 계획에 있으며 hosting을 Firebase를 이용하여 진행할 계획이다.</p>
</blockquote>
<br>

<h2 id="goal">Goal</h2>
<ul>
<li>flutter의 Web을 Firebase를 이용하여 Hosting하기</li>
</ul>
<br>

<h2 id="firebase-프로젝트-생성하기">Firebase 프로젝트 생성하기.</h2>
<p>Firebase 프로젝트는 간단하다. 스탭별로 하나 씩 알아보려 한다.</p>
<h3 id="step1-프로젝트-명-정의">step.1 (프로젝트 명 정의)</h3>
<img src='https://velog.velcdn.com/images/dev_leewoooo/post/e1beb64b-e4a6-4ce2-8c5c-3329304d8df9/image.png'>


<h3 id="step2-애널리틱스-설정">step.2 (애널리틱스 설정)</h3>
<p>애널리틱스 설정은 필수는 아니지만 Firebase에서는 권장을 하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/9bc3b2c1-0978-4cfd-864d-ebb6cee251ba/image.png" alt=""></p>
<h3 id="step3-기다리기">step.3 (기다리기)</h3>
<p>다음은 기다리는 역할만 남아있다. 기다리면 프로비저닝 단계를 거처 Firebase 신규 프로젝트가 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/064a78ce-c409-4230-a128-6370abb6e41c/image.png" alt=""></p>
<p>위와 같은 Dashboard가 보이면 생성은 성공된 것이며 우리가 대시보드에서 관심가지고 볼 내용은 노랑색으로 하이라이트 되어 있는 <strong>요금제</strong>만 확인하면 된다. (<del>무료 요금제인지 확인이 가장중요하다.</del>)</p>
<br>

<h2 id="hosting-설정하기">Hosting 설정하기</h2>
<p>우측 사이드 바에서 사진과 같이 <strong>Hosting</strong>이라는 버튼을 클릭하여 설정을 시작할 수 있다.</p>
<blockquote>
<p>사이드 바 -&gt; Hosting</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/6740f5b1-c4a4-47ef-9381-15a85543b655/image.png" alt=""></p>
<h3 id="step1-firebase-tools-설치하기">step.1 (firebase-tools 설치하기)</h3>
<p>firebase의 cli를 이용하기 위해 <strong>firebase-tools</strong>를 설치해야 한다. 기본적으로 <code>npm</code>을 사용하기 때문에 컴퓨터에 <code>Node.js</code>가 설치되어 있어야 한다.</p>
<p><code>npm</code>을 이용하여 <code>global</code>로 firebase-tools를 설치한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/d1d1a33c-da86-4a7d-b3e3-2c75452ef3d9/image.png" alt=""></p>
<h3 id="step2-firebase-로그인하기">step.2 firebase 로그인하기</h3>
<p>터미널을 프로젝트의 루트로 이동시켜 아래와 같은 명령어를 입력한다.</p>
<pre><code class="language-zsh">firebase login</code></pre>
<p>입력하면 터미널에 아래와 같은 내용들이 나타나게 되며 <strong>error reporting</strong>에 참석 여부를 물으며 Firebase Cli를 이용할 Google 계정으로 로그인하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/c309b3be-afe4-4781-a27c-a986a7702320/image.png" alt=""></p>
<p>로그인이 성공 되면 성공한 계정 이메일과 <strong>Success! Logged in as ${이메일}</strong>이 터미널에 노출된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/a3effd0a-9690-4def-91d2-3a14b4b2db2a/image.png" alt=""></p>
<h3 id="step3-firebase-설정-추가하기">step.3 firebase 설정 추가하기</h3>
<p>로그인이 완료 되었으면 firebase 설정파일을 프로젝트에 추가해야 한다. <strong>프로젝트의 루트위치에서 아래와 같은 명령어를 입력한다.</strong></p>
<pre><code class="language-zsh">firebase init</code></pre>
<p>로그인이 정상적으로 되었다면 아래와 같은 로고가 보이며 계속 할건지에 대한 여부를 묻는다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/d724bc6a-4326-4151-beda-1954859c73e3/image.png" alt=""></p>
<p>이 후 현재 프로젝트에 firebase의 어떠한 기능을 적용시킬지에 대한 선택을 하게 된다. 필자는 호스팅을 할 것이기 때문에 아래와 같은 옵션을 선택했다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/01679bec-012f-49f6-b06b-e5641c3cc1e5/image.png" alt=""></p>
<p>firebase의 기능을 선택했다면 firebase의 프로젝트와 현재 WebProject를 연결하는 설정을 하면 된다.</p>
<p>위에서 <strong>프로젝트를 생성하고 왔기 때문에</strong> <code>Use an existing project</code>를 선택 후 <code>cli</code>에서 보여지는 firebase 프로젝트 명을 선택하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/3b9b3992-ec8a-4e88-a8bc-0ae2742c7794/image.png" alt=""></p>
<p>프로젝트까지 설정이 완료 되었다면 마지막으로 호스팅 설정만 남았다. 설정을 할 것은 다음과 같다.</p>
<ol>
<li>public directory : 간단하게 이야기하면 빌드가 되어 <code>index.html</code>의 위치를 정의할 수 있으며 추 후 <code>firebase.json</code>에서 재 설정이 가능하다.</li>
<li>SPA 여부 : 현재 web을 spa로 정의할 것인가 여부</li>
<li>CI/CD 여부 : GitHub을 통해 CI/CD를 설정할 것인지 여부</li>
<li>overwrite 여부 : 이미 빌드 파일이 존재 할 경우 덮어 쓸 것인지 여부</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/19513c56-7048-4a59-aa4c-566a63d8aaba/image.png" alt=""></p>
<p>위와 같이 <code>Firebase initialization complete!</code>를 보면 정상적으로 현재 프로젝트에 설정이 완료된 것이다.</p>
<br>

<h2 id="firebase-배포하기">firebase 배포하기</h2>
<p>배포는 간단하다. 아래와 같은 명령어를 입력하면 firebase에서 <code>init</code>명령어로 설정한 내용을 토대로 배포를 진행한다. 아래와 같은 명령어를 입력한다.</p>
<pre><code class="language-zsh">firebase deploy --only hosting</code></pre>
<p><code>only hosting</code> 플래그로 실행하면 호스팅 콘텐츠 및 구성만 배포 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/ba670ae8-8520-42c1-97aa-3bb4390f5f5c/image.png" alt=""></p>
<p>배포가 성공적으로 완료되면 CLI에 해당 Hosting을 관리해주는 콘솔 URL과 배포 된 URL이 노출 되는 것을 확인할 수 있다.</p>
<p>이 후 URL로 접속을 하게 되면 정상 배포를 확인할 수 있다.</p>
<br>

<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/dev_leewoooo/post/0682900d-81ac-44bb-8737-a410d99fcb77/image.png" alt=""></p>
<br>

<h2 id="reference">Reference</h2>
<ul>
<li><p><a href="https://firebase.google.com/docs/hosting/quickstart?hl=ko&amp;_gl=1*ndmuro*_up*MQ..*_ga*MTE0Nzg3Mjg3MS4xNzEwNDgzNjU5*_ga_CW55HF8NVT*MTcxMDQ4MzY1OS4xLjAuMTcxMDQ4MzY1OS4wLjAuMA..">firebase doc</a></p>
</li>
<li><p><a href="https://firebase.google.com/docs/hosting/frameworks/flutter?hl=ko">firebase flutter web</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nginx History]]></title>
            <link>https://velog.io/@dev_leewoooo/Nginx-History</link>
            <guid>https://velog.io/@dev_leewoooo/Nginx-History</guid>
            <pubDate>Wed, 29 Mar 2023 13:10:34 GMT</pubDate>
            <description><![CDATA[<h1 id="nginx의-역사에-대해-알아보자">Nginx의 역사에 대해 알아보자</h1>
<p>Nginx의 탄생 배경 및 구조에 대해서 알아보기O</p>
<br>

<h2 id="why">Why?</h2>
<p>시작하려는 사이드 프로젝트에 Nginx를 적용하고자 하기 때문에 Nginx에 대한 개요를 알아보고 내부 구조를 이해하기 위함</p>
<br>

<h2 id="goal">Goal</h2>
<ul>
<li>Nginx가 왜 나오게 되었는지에 대한 이해하기</li>
<li>Nginx의 내부 구조가 어떻게 생겼는지 이해하기</li>
</ul>
<br>

<h2 id="웹-서버의-발전의-연대기를-통해-nginx를-이해하기">웹 서버의 발전의 연대기를 통해 Nginx를 이해하기</h2>
<p>웹 서버가 발전해온 과정을 통해 Nginx를 이해하고자 한다. Nginx가 나오기 이전에 사용되던 Apache Http Server와 비교해보고자 한다.</p>
<br>

<h2 id="1995년">1995년</h2>
<p>&quot;Apache HTTP Server&quot;(이 하 아파치 서버)가 세상에 최초 공개 된 해이다. 아파치 서버가 세상에 나오기 전에는 &quot;NCSA HTTPd&quot;가 존재하였다.</p>
<p>하지만 &quot;NCSA HTTPd&quot;는 버그가 많아 사용에 불편함이 많았다. &quot;NCSA HTTPd&quot;의 문제점을 개선하고 발전시켜 개발 된 것이 아파치 서버 인 것이다.</p>
<br>

<h3 id="아파치-서버의-구조">아파치 서버의 구조</h3>
<p>아파치 서버는 Client에게 요청이 들어오면 커넥션을 형성하기 위해 프로세스를 형성한다.</p>
<p>즉 새로은 요청이 들어올 때 마다 프로세스를 형성하게 된다. 이는 유닉스 계열의 OS가 네트워크 커넥션 형성을 하는 모델을 그대로 적용한 것이다.</p>
<p>커넥선을 형성하는 과정이 시간이 오래걸리다 보니 <code>PREFORK</code> 사용하였다. <strong>미리 프로세스를 형성한 후</strong> 요청이 들어오면 만들어진 프로세스를 사용하는 것이다.</p>
<p>하지만 <strong>미리 만들어진 프로세스가 전부 할당되었다면 아파치 서버는 새로운 프로세스를 만들어 커넥션 형성과정을 진행하게 된다.</strong></p>
<p><img src="https://user-images.githubusercontent.com/74294325/228261618-793e9d37-b824-464c-8457-f0a27f669dae.png" alt="apache"></p>
<p>이러한 구조는 <strong>개발하기 쉽다는 장점을 가지고 있어 개발자가 새로운 모듈을 개발하여 추가하기 좋은 구조를 가지게 되었으며</strong> 아파치 서버가 인기를 얻을 수 있는 비결이였다.</p>
<br>

<h2 id="1999년">1999년</h2>
<p>이 시기는 인터넷 트래픽이 증가하는 추세였다. 이전에는 그 당시 기술로 감당할 수 있는 정도였지만 컴퓨터가 보급되고 요청이 많아짐에 따라 서버에 문제가 발생하기 시작하였다.</p>
<p>문제는 서버에 동시에 연결되어 있는 커넥션이 많아져서 일정 커넥션 수가 넘어가면 커넥션을 맺을 수 없는 문제가 발생하게 되었다. 이를 <code>C10K(Connection 10000 Problem)</code>문제라고 한다.</p>
<p>이 당시에는 매번 커넥션을 맺는 것이 속도도 느리고 비효율적이라고 판단하여 <code>HTTP Header</code>중 <code>Keep-Alive</code> 헤더를 이용하여 한번 연결한 커넥션을 유지하여 사용하는 것이 통상적이였다.</p>
<p>그렇기 때문에 Client수가 많아지고 동시에 연결되어 있는 커넥션 수가 많아지다 보니 일정 갯수의 커넥션이 형성된 이 후에 서버에서 커넥션을 형성하지 못하는 상황이 놓이게 된 것이다</p>
<p><img src="https://user-images.githubusercontent.com/74294325/228266168-d7a6145f-edab-45cd-b189-2ca3e337ab3a.png" alt="c10k"></p>
<p>그렇다면 하드웨어가 문제였을까? 아니다 하드웨어는 그 당시 엄청난 속도로 발전되고 있었으며 충분한 스팩을 가지고 있었다.</p>
<p>문제는 앞에서 살펴본 아파치 서버 구조를 확인해보면 어디서 문제가 발생하는지 확인할 수 있게 된다.</p>
<p>아파치 서버는 <a href="#1995%EB%85%84">1995년</a>에서 보았듯 요청이 들어오면 커넥션 형성을 위해 프로세스를 형성한다고 하였다. <strong>그렇다 Client가 많아진 시점에 프로세스가 계속해서 생성되다 보니 서버 메모리 부족현상이 발생하게 된다.</strong></p>
<p>또한 각 프로세스 간 작업을 진행하기 위해 <strong>CPU에서 <code>Context Switching이 계속해서 발생하다 보니 CPU에도 부하가 많아진 것이</code></strong>문제가 된 것이였다.</p>
<p>쉽게 말해 아파치 서버는 <strong>동시 커넥션을 처리하기에는 구조적으로 부적한 것이였다.</strong></p>
<br>

<h2 id="2004년">2004년</h2>
<p>2004년에 드디어 Nginx가 나오게 된다. <strong>초창기 Nginx는 아파치 서버를 대체하기 위해 나온것이 아니라 아파치 서버를 보완하기 위해 나온 것 이였다.</strong></p>
<p>초창기 사용은 아래와 같은 구조를 띄고 있었다.</p>
<p><img src="https://user-images.githubusercontent.com/74294325/228268971-65c2dfd9-4618-4bfe-8921-cf338e12c04e.png" alt="nginx"></p>
<p>이렇게 되면 <strong>이전 아파치 서버가 감당해야 했던 동시 커넥션을 Nginx가 감당하면서 아파치 서버의 부하를 크게 줄일 수 있다.</strong></p>
<p>Nginx는 또한 웹서버의 역할을 감당할 수 있기 때문에 정적 파일들을 Client에게 서빙이 가능했다.</p>
<br>

<h3 id="그렇다면-nginx는-어떻게-수-많은-동시-커넥션을-관리할-수-있을까">그렇다면 Nginx는 어떻게 수 많은 동시 커넥션을 관리할 수 있을까?</h3>
<p><img src="https://user-images.githubusercontent.com/74294325/228275789-38757e68-df87-42b6-a668-49585be3f3f8.png" alt="nginx_process"></p>
<p>Nginx에는 기본적으로 2가지의 프로세스가 존재한다.</p>
<ol>
<li>Master Process</li>
<li>Worker Process</li>
</ol>
<br>

<h4 id="master-process">Master Process</h4>
<p>Master Process가 하는 역할은 간단하다. <code>Config</code>파일을 읽어 Worker Process를 생성하거나 Update하는 일을 한다.</p>
<br>

<h4 id="worker-process">Worker Process</h4>
<p>사용자에 요청에 맞게 커넥션을 맺고 관리하며 요청을 처리하는 프로세스가 Worker Process가 하는 역할이다.</p>
<p><strong>Worker Process는 생성될 때 각자 지정된 Listen 소켓을 지정받게 된다. 지정 받은 소켓에서는 Client에게 요청이 들어오면 커넥션을 형성하고 요청을 처리한며 <code>Keep-Alive</code>만큼 커넥션을 유치하고 만료 된 커넥션의 연결을 끊는다.</strong></p>
<p>아파치 서버는 하나의 요청에는 하나의 프로세스가 담당하였었는데 <strong>Nginx는 하나의 Worker Process가 하나의 요청만 담당하지 않는다.</strong></p>
<p><strong>이미 연결되어 있는 커넥션에서 요청이 없다면 새로운 요청에 대한 커넥션을 형성하거나 이미 연결되어 있는 커넥션으로 부터 들어오는 요청을 처리하게 된다.</strong></p>
<p>NginX에서는 커넥션에 대한 관리, 요청 처리와 같은 작업을 이벤트라고 정의하며 OS 커넉에서 <code>Queue</code>형태로 Worker Process에게 전달되며 처리 될 때 까지 비동기 방식으로 대기하게 된다.</p>
<p>Worker Process는 <code>Queue</code>에서 이벤트를 하나의 쓰래드로 이벤트를 꺼내어 처리해 나간다.</p>
<p>이러 한 방식을 채택함으로 Worker Process가 쉬지 않고 일한다는 장점을 가지게 될 수 있다.</p>
<p>만약 이벤트 중 시간이 오래걸리는 이벤트가 들어오게 된다면 또한 그 작업을 다른 이벤트들과 동일하게 처리한다면 <strong><code>Queue</code>의 특성 상 <code>FIFO</code>구조이다 보니 시간이 오래 걸리는 구조의 작업이 끝날 때 까지 블로킹 될 것이다.</strong></p>
<p>Nginx는 위와 같이 시간이 오래걸리는 이벤트같은 경우 <strong>별도의 쓰래드 풀을 형성하여 위임하게 된다.</strong></p>
<p>Worker Process는 통상적으로 CPU의 코어만큼 생성을 하게되는데 이를 통해 얻을 수 있는 장점으로는 아파치 서버에서 빈번하게 일어나던 <code>Context Switching</code>을 줄여 <strong>CPU의 부하 또한 줄일 수 있게 되어 성능을 높일 수 있다.</strong></p>
<p>이 구조가 바로 Nginx가 채택한 <code>Event Driven Model</code>이다.</p>
<p>Nginx의 이러한 구조는 단점도 존재한다. <strong>만약 기능을 추가하기 위해 Nginx를 종료하게 된다면? Worker Process가 관리하던 커넥션 및 요청을 처리하지 못하게 된다.</strong></p>
<br>

<h2 id="2008년">2008년</h2>
<p>스마트 폰이 보급되면서 인터넷의 사용이 더 증가되게 되었다. 즉 <strong>동시 커넥션을 더 많이 맺게 되는 계기가 되었으며</strong> 브라우저에서도 빠르게 리소스를 가져오기 위해 여러 커넥션을 동시에 맺게 되는 상황들이 벌어지고 있었다.</p>
<p>결국 동시 커넥션을 처리해야하는 서버들이 증가하였고 회사들은 Nginx에 눈을 돌리게 되게 되면서 Nginx가 인터넷 트래픽에 관여하는 비중이 증가하면서 지금과 같은 자리를 차지하게 되었다.</p>
<br>

<h2 id="nginx-vs-아파치-서버">Nginx vs 아파치 서버?</h2>
<p>아파치 서버와 Nginx는 대립관계 보다는 각자 탄생하게 된 목적이 달랐던 것이다.</p>
<p>아파치 서버가 주로 사용되던 환경에서는 이전 &quot;NCSA HTTPd&quot;를 사용할 때 경험했던 버그와 불완전한 서버 운영을 보완하기 위해 안정성과 확장성을 중요시 하게 되던 시기였고</p>
<p>Nginx가 주로 사용되고 있는 환경에서는 동시 커넥션 및 성능과 같은 것들을 신경써야 하는 시기이기 때문이라 생각한다. (<del>밴치마킹 결과 Nginx가 더 좋은건 안 비밀</del>)</p>
<br>

<h2 id="느낀-점">느낀 점</h2>
<p>실무에서 Nginx를 직접 만저볼 경험이 없던지라 사이드 프로젝트에서 적용해보고 싶은 마음에 Nginx의 탄생 과정 및 개요, 내부구조에 대한 자료를 찾던 중 <a href="https://www.youtube.com/watch?v=6FAwAXXj5N0&amp;ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC">우테코 - Nginx</a>를 만날 수 있어서 참 감사했다. 15분의 영상이지만 Nginx에 대한 이해도를 높이기에는 충분했던 것 같다.</p>
<p>무엇보다 기술을 선택할 때 해당 기술에 대한 배경을 알고있으면 어떠한 상황에서 적용하면 좋겠구나 라는 판단이 조금 더 명확해진다는 확신을 얻었다.</p>
<p>TODO: 프로젝트에 적용할 때 Nginx를 사용한 사용법들을 정리해보기.</p>
<br>

<h2 id="references">References</h2>
<ul>
<li><p><a href="https://www.nginx.com/">https://www.nginx.com/</a></p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=6FAwAXXj5N0&amp;ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC">https://www.youtube.com/watch?v=6FAwAXXj5N0&amp;ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Pg Listen & Notify]]></title>
            <link>https://velog.io/@dev_leewoooo/%EB%A6%AC%EC%8A%A8-%EB%85%B8%ED%8B%B0%ED%8C%8C%EC%9D%B4-%EC%9E%84%EC%A0%80</link>
            <guid>https://velog.io/@dev_leewoooo/%EB%A6%AC%EC%8A%A8-%EB%85%B8%ED%8B%B0%ED%8C%8C%EC%9D%B4-%EC%9E%84%EC%A0%80</guid>
            <pubDate>Tue, 21 Mar 2023 00:47:11 GMT</pubDate>
            <description><![CDATA[<h1 id="postgresql-listen--notify">Postgresql Listen &amp; Notify</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>pg에서 비동기식 처리는 어떻게 하는지에 대한 이해</li>
<li>Listen &amp; Notify의 사용방법</li>
</ul>
<br>

<h2 id="데이터베이스의-비동기식-처리">데이터베이스의 비동기식 처리</h2>
<p>만약 데이터베이스에 새 자료가 등록된 것을 확인하고 싶다면 어떻게 해야할까?</p>
<ol>
<li><p>주기적으로 해당 테이블의 마지막 자료를 확인한다. 조회를 하였을 때 현재의 데이터와 마지막 자료가 다르다면 새로운 자료가 등록되었다고 판단한다.</p>
</li>
<li><p>테이블에 트리거를 등록하여 새 자료가 등록되면 해당 데이터를 타 테이블에 저장하고 해당 테이블을 1번 방식처럼 주기적으로 확인한다.</p>
</li>
</ol>
<p>위와 같은 방법을 이용하면 <strong>데이터 베이스 자원을 필요 이상으로 사용하게 된다.</strong> 테이블에 과도한 접근이 일어나며 특정 이벤트를 검사하는 주기를 짧게 잡기도 어렵다.</p>
<p>그래서 이러한 작업의 처리는 대게 응용 프로그램의 도움을 받는다. 물론 서버가 이런 비동기식 작업들에 대한 다양한 기능을 제공한다면 보다 쉽게 구현할 수 있다.</p>
<p>Postgresql에서는 대표적으로 Listen &amp; Notify를 제공한다.</p>
<p>위의 내용 까지가 Postgresql에서 공식적으로 제공하는 데이터 베이스 비동기식 처리이다.</p>
<p><strong>하지만</strong> 내가 원하는 건 하나의 테이블에 데이터가 저장 되면 저장 이벤트를 듣고 있던 listener가 저장 된 데이터를 가지고 후 처리를 하고 싶은 것이다. (그래서 아마 트리거 + 트리거 함수 + Listen &amp; Notify의 조합이 될 것 같다.)</p>
<br>

<h2 id="listen--notify">Listen &amp; Notify</h2>
<p>Postgresql에서 Listen &amp; Notify를 이용하면 한 쪽에서는 어떤 채널에 어떤 내용을 공지하고(Notify), 반대 쪽에서는 채널을 듣고(Listen)있다가 작업을 할 수 있다.</p>
<p>Postgresql 공식 문서에서 해당 기능을 통해 구현이 가능한 예시들을 설명해주고 있는데 내가 Listen &amp; Notify를 이용하여 궁극적으로 구현하고자 하는 내용이 적혀있어서 기뻤다.</p>
<p><img src="https://user-images.githubusercontent.com/74294325/223598215-472d5659-9ea5-4de1-9218-17d7349dcd0d.png" alt="image"></p>
<br>

<h3 id="notify">Notify</h3>
<p>응용 프로그램에서 Notify는 Listen 보다 구현하는 부분이 간단하다.</p>
<pre><code class="language-sql">NOTIFY 채널이름, &#39;내용&#39;</code></pre>
<p>위와 같은 쿼리만 Postgresql 서버로 보내면 된다. 여기서 문제는 채널 이름의 규칙과, 내용의 구칙 설계를 Listen을 하는 쪽과 잘 맞춰야 한다.</p>
<br>

<h3 id="listen">Listen</h3>
<p>Listen 쪽에서는 아래와 같은 Query를 이용하여 특정 채널에 대한 내용을 얻어올 수 있다.</p>
<pre><code class="language-sql">LISTEN 채널이름</code></pre>
<p>Listen을 하기 위해서는 <strong>감시 작업(polling)</strong>을 해야한다. 대부분의 Postgresql 클라이언트 라이브러리들은 해당 기능을 APi로 제공해주고 있다.</p>
<br>

<h2 id="주의">주의</h2>
<ul>
<li><p>Listen &amp; Notify 작업은 <code>connection.poll()</code>같은 <strong>각 언어 별 클라이언트 라이브러리에서 제공하는 API를 이용하여 작업 비용을 효율화</strong> 할 수 있다.</p>
</li>
<li><p>Listen 하나도 없는 Notify는 <strong>버려진다.</strong></p>
</li>
</ul>
<br>

<h2 id="usage">Usage</h2>
<p>위에서 전반적인 내용을 알아봤으니 사용법을 하나씩 알아보기를 원한다. Listen &amp; Notify를 하기 위해 필요한 요소는 아래와 같다.</p>
<blockquote>
<p>채널을 통해 Listen &amp; Notify 예제가 아닌 테이블에 데이터가 Insert 되면 해당 데이터를 Listen &amp; Notify를 이용하여 Client에게 전달하는 예제이다.</p>
</blockquote>
<ol>
<li><p>이벤트 주체 테이블</p>
</li>
<li><p>테이블에 데이터가 Insert 되었을 때 Notify를 할 trigger function</p>
</li>
<li><p>trigger</p>
</li>
<li><p>Notify를 듣고있는 Listener (Client)</p>
</li>
</ol>
<br>

<h2 id="table-이벤트-주체-테이블">Table (이벤트 주체 테이블)</h2>
<p>이벤트 주체 테이블은 크게 특이점이 없다. 사실 모든 테이블이 이벤트 주체 테이블이 될 수 있다.</p>
<p>예제에서는 간단하게 테이블 아래와 같이 테이블을 구성하겠다.</p>
<pre><code class="language-sql">CREATE SEQUENCE IF NOT EXISTS tmp_notify_id_seq;

CREATE TABLE IF NOT EXISTS tmp_notify (
    id          BIGINT          NOT NULL        DEFAULT nextval(&#39;tmp_notify_id_seq&#39;) PRIMARY KEY,
    content     TEXT            NOT NULL,
    created_at  TIMESTAMPTZ     NOT NULL        DEFAULT now()
);</code></pre>
<br>

<h2 id="trigger-function-테이블에-데이터가-insert-되었을-때-notify를-할-함수">trigger function (테이블에 데이터가 Insert 되었을 때 Notify를 할 함수)</h2>
<p>PL/pgSQL은 데이터 변경 또는 DB 이벤트에 대한 트리거 기능을 정의하는데 사용할 수 있다.</p>
<p>트리거 함수는 <code>CREATE FUNCTION</code>의 파라미터를 없이 선언하고 RETURN에 <code>trigger</code> 또는 <code>event_trigger</code>를 선언으로 생성할 수 있다.</p>
<pre><code class="language-sql">CREATE OR REPLACE FUNCTION fn_notify_trigger() RETURNS trigger AS $$ -- 1
BEGIN -- 2
    PERFORM pg_notify(&#39;tmp_notify&#39;,row_to_json(NEW)::text); -- 3
    RETURN NULL;
END; -- 4
$$
LANGUAGE plpgsql;</code></pre>
<ol>
<li><p>trigger function를 정의를 시작하는 선언문이다. trigger function의 이름을 <code>fn_notify_trigger</code>로 지정하고 있다.</p>
</li>
<li><p>함수의 시작을 명시한다.</p>
</li>
<li><p><code>PERFORM</code> 명령어를 이용하여 결과가 없는 함수를 실행시키며 <code>pg_notify</code> 함수를 이용하여 새로 Insert 된 데이터를 Notify한다. <code>tmp_notify</code>라는 채널명에 payload를 <code>row_to_json</code>함수를 이용하여 보낸다. 이 때 Notify의 payload는 문자열로 보내야 하기 때문에 <code>::text</code>로 캐스팅해서 보내준다.</p>
<blockquote>
<p>payload <br>알림과 함께 전달할 &quot; 페이로드 &quot; 문자열 입니다. 단순 문자열 리터럴로 지정해야 합니다. 기본 구성에서는 8000바이트보다 짧아야 합니다. (바이너리 데이터나 많은 양의 정보를 전달해야 한다면 데이터베이스 테이블에 넣고 레코드의 키를 보내는 것이 가장 좋습니다.)<br><a href="https://www.postgresql.org/docs/current/sql-notify.html">https://www.postgresql.org/docs/current/sql-notify.html</a></p>
</blockquote>
<blockquote>
<p>여기서 NEW는 대상 테이블의 Operation에 따라 다른 값이 들어온다. Insert의 경우 새로 생성된 Row의 데이터가 들어오게 된다.</p>
</blockquote>
</li>
<li><p>함수의 종료를 명시한다.</p>
</li>
</ol>
<br>

<h2 id="trigger-테이블에-insert-될-때-자동으로-동작할-작업">trigger (테이블에 Insert 될 때 자동으로 동작할 작업)</h2>
<p>trigger 함수를 만들었으니 테이블에서 Insert 될 때 자동으로 동작할 작업을 정의한다. 특정 테이블의 Event가 있을 때 DB에서는 <code>trigger</code>라고 부르며 위에서 정의한 trigger function을 이용하게 된다.</p>
<pre><code class="language-sql">CREATE TRIGGER tmp_notify_trigger AFTER INSERT ON tmp_notify -- 1
FOR EACH ROW -- 2
EXECUTE FUNCTION fn_notify_trigger(); -- 3</code></pre>
<ol>
<li><p>trigger를 <code>tmp_notify_trigger</code>라는 이름으로 생성하며 <code>tmp_notify</code> 테이블의 Insert 이 후 트리거가 될 거라는 것을 명시한다.</p>
</li>
<li><p>trigger 이벤트를 해당 테이블의 모든 행에 대해 실행 시킨다는 것을 명시한다.</p>
</li>
<li><p>trigger가 실행 되면 위에서 정의한 <code>fn_notify_trigger</code>를 실행시킨다는 것을 명시한다.</p>
</li>
</ol>
<br>

<h2 id="listener-client">Listener (Client)</h2>
<p>Notify 부분은 준비가 완료 되었으니 Listen 쪽을 정의하고자 한다. 간단하게 <code>NestJs</code>를 이용하여 예제를 만들 것이며 <code>TypeOrm</code>으로 DB의 <code>Connection</code>을 맺을 것이다. (<code>pg</code>를 이용하여 Connection을 맺고 진행해도 된다.)</p>
<br>

<h3 id="typeorm을-이용하여-connection-맺기">TypeOrm을 이용하여 Connection 맺기</h3>
<p>해당 부분은 <code>NestJs</code>공식 문서에 잘 설명이 되어있기 때문에 코드로만 첨부하겠다. - <a href="https://docs.nestjs.com/techniques/database#typeorm-integration">TypeOrm Integration</a> 예제에서 DB Driver 라이브러리만 <code>pg</code>로 변경하면 된다.</p>
<pre><code class="language-ts">// app.module.ts
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: &quot;postgres&quot;,
      host: &quot;localhost&quot;,
      port: 5432,
      username: &quot;postgres&quot;,
      password: &quot;password&quot;,
      database: &quot;listen_notify&quot;,
    }),
  ],
})
export class AppModule {}</code></pre>
<br>

<h3 id="connection에서-pg-client-얻기">Connection에서 Pg Client 얻기</h3>
<p><code>TypeOrm</code>을 이용하여 Root에서 <code>Connection</code>을 맺으면 하위 Provider에서는 <code>dataSource</code>를 <code>DI</code>받을 수 있다.</p>
<p><code>dataSource</code>에서 <code>QueryRunner</code>를 생성 후 <code>connect()</code> API를 호출하면 현재 <code>Connection</code>을 맺은 DB의 Client를 얻어올 수 있다. 즉 현재 Postgresql을 이용하고 있기 때문에 <code>connect()</code>를 호출 하면 <code>Postgresql</code>의 Client를 얻어올 수 있다.</p>
<p><code>connect()</code>를 호출 하면 <code>connection pool</code>에서 <code>Connection</code>을 생성하여 Return해준다.</p>
<pre><code class="language-ts">// QueryRunner
/**
 * Creates/uses database connection from the connection pool to perform further operations.
 * Returns obtained database connection.
 */
connect(): Promise&lt;any&gt;;

// app.module.ts
export class AppModule implements OnModuleInit, OnModuleDestroy {
  private readonly queryRunner: QueryRunner;

  constructor(private readonly dataSource: DataSource) {
    this.queryRunner = dataSource.createQueryRunner();
  }

  async onModuleInit() {
    const client = (await this.queryRunner.connect()) as Client;
    //...
  }
}</code></pre>
<br>

<h3 id="client를-이용하여-listen-하기">Client를 이용하여 Listen 하기</h3>
<p><code>pg</code> Client를 이용하여 Notify에서 정의한 <code>tmp_notify</code> 채널을 구독할 것이다. 또한 구독을 시작하였으니 채널에서 넘어오는 payload를 사용할 수 있다.</p>
<pre><code class="language-ts">const client = (await this.queryRunner.connect()) as Client;

await client.query(&quot;LISTEN tmp_notify&quot;);  // 1
client.on(&quot;notification&quot;, (data: any) =&gt; { // 2
  // do something
});</code></pre>
<ol>
<li>Notify하는 채널에 대한 구독을 시작합니다.</li>
<li>Notify가 되었을 때 실행 될 CallBack을 정의할 수 있다.</li>
</ol>
<br>

<h3 id="결과">결과</h3>
<p><code>tmp_notify</code>테이블에 데이터가 Insert 되면 trigger가 동작하여 Notify를 하게 되며 Notify를 한 후 <code>NestJs</code>에서 정의한 Listener가 해당 데이터를 가져다가 사용하게 된다.</p>
<pre><code class="language-bash"># Notify
insert into tmp_notify (f_input_date, &quot;data&quot;) values (&#39;20230311&#39;, &#39;Notify &amp; Listen Example&#39;::text);

# Listen
NotificationResponseMessage {
  length: 128,
  processId: 9556,
  channel: &#39;tmp_notify&#39;,
  payload: &#39;{
        &quot;f_input_date&quot;:&quot;20230311&quot;,
        &quot;data&quot;:&quot;Notify &amp; Listen Example&quot;,
        &quot;created_at&quot;:&quot;2023-03-11T15:55:18.423159+09:00&quot;
        }&#39;,
  name: &#39;notification&#39;
}</code></pre>
<br>

<h2 id="정리">정리</h2>
<p><img src="https://user-images.githubusercontent.com/74294325/223642314-69b830c9-8888-420a-b4d3-bd10f96cf27b.png" alt="notify_listen"></p>
<p>정리하자면 위와 같은 구조를 갖게 된다. 어떠한 요청에 의해 서버에서 <strong>Postgresql에 Data를 Insert하였을 때</strong> Postgresql는 해당 데이터를 <strong>특정 채널에 Notify</strong> 하고 그 <strong>채널을 Listen하는 Listener들이 Notify 된 데이터를 가져다가 사용</strong>하게 되는 구조로 정리할 수 있을 것 같다. (<strong>Insert에 한정 된 이야기가 아니다.</strong>)</p>
<br>

<h2 id="reference">Reference</h2>
<ul>
<li><p><a href="https://postgresql.kr/blog/pg_listen_notify.html">https://postgresql.kr/blog/pg_listen_notify.html</a></p>
</li>
<li><p><a href="http://postgresql.kr/docs/current/sql-listen.html">http://postgresql.kr/docs/current/sql-listen.html</a></p>
</li>
<li><p><a href="http://postgresql.kr/docs/current/sql-notify.html">http://postgresql.kr/docs/current/sql-notify.html</a></p>
</li>
<li><p><a href="https://www.postgresql.kr/docs/current/plpgsql-trigger.html">https://www.postgresql.kr/docs/current/plpgsql-trigger.html</a></p>
</li>
<li><p><a href="https://m-falcon.tistory.com/528">https://m-falcon.tistory.com/528</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJs에서 class-validator의 ErrorMessage는 어떻게 만들어질까?]]></title>
            <link>https://velog.io/@dev_leewoooo/NestJs%EC%97%90%EC%84%9C-class-validator%EC%9D%98-ErrorMessage%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%A7%88%EA%B9%8C</link>
            <guid>https://velog.io/@dev_leewoooo/NestJs%EC%97%90%EC%84%9C-class-validator%EC%9D%98-ErrorMessage%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%A7%88%EA%B9%8C</guid>
            <pubDate>Sat, 18 Mar 2023 16:14:52 GMT</pubDate>
            <description><![CDATA[<h1 id="nestjs에서-validationpipe는-어떻게-errorresponse를-생성할까-with-class-validator">NestJs에서 ValidationPipe는 어떻게 ErrorResponse를 생성할까? (with class validator)</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>NestJs에서 사용되는 <code>ValidationPipe</code>의 Error Response 생성의 이해</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p><code>class-validator</code>를 이용하여 <code>RequestBody</code>에 대한 유효성을 검증하였다. <code>ErrorResponse</code>를 통일하는 작업을 진행하려다 보니 <code>class-validator</code>에서는 어떠한 방식으로 <code>ErrorResponse</code>를 만들어서 보여줄까? 궁금증이 발생하여 라이브러리를 뜯어보기로 하였다.</p>
<br>

<h2 id="class-validator">class-validator</h2>
<p>NestJs에서는 요청이 들어오는 Request에 대한 Validation을 주로 <code>class-validator</code>를 이용하여 유효한지 검증을 하게 된다.(<a href="https://docs.nestjs.com/techniques/validation">NestJs Validation</a>)</p>
<pre><code class="language-ts">class CreateExampleRequest {
  @IsString()
  readonly title: string;

  @IsString()
  readonly content: string;

  //...
}</code></pre>
<br>

<p>다음 <code>main.ts</code>에서 아래와 같은 코드를 추가하면 들어오는 요청에 대해 validation 데코레이터가 붙어있는 프로퍼티에 대한 유효성 검증을 할 수 있다.</p>
<pre><code class="language-ts">// main.ts
async function bootstrap() {
  //...
  app.useGlobalPipes(new ValidationPipe({ transform: true })); // 1
  //...
}

// 유효하지 않을 때 Error Response
{
    &quot;statusCode&quot;: 400,
    &quot;message&quot;: [
        &quot;title should not be empty&quot;,
        &quot;content should not be empty&quot;,
    ],
    &quot;error&quot;: &quot;Bad Request&quot;
}</code></pre>
<ol>
<li>요청으로 들어오는 RequestBody에 대한 유효성 검증을 추가하고 <code>{ transform: true }</code> 옵션을 추가함으로 유효성 검증이 끝난 <code>object</code>를 <code>controller</code>에서 정의한 <code>class</code>로 받을 수 있도록 한다.</li>
</ol>
<br>

<p><strong>그렇다면 어떻게 위와 같은 Response를 Client에서 어떻게 만들어서 보내는 걸까?</strong></p>
<br>

<h2 id="nestjscommon의-validationpipe-구현체-찾기">@nestjs/common의 ValidationPipe 구현체 찾기</h2>
<p><code>ValidationPipe</code>는 <code>@nestjs/common</code>모듈에 속해있다. 그렇기 때문에 Nestjs를 설치한 프로젝트라면 당연히 들어있을 것이다. NestJs는 기본적으로 <code>typescript</code>가 적용된 프로젝트이기 때문에 진짜 구현체를 확인하려면 <code>.js</code>파일을 확인하면 된다.</p>
<pre><code class="language-bash">node_modules
ㄴ common
    ㄴ pipes
        ㄴ validation.pipe.d.ts
        ㄴ validation.pipe.js # 구현 코드가 들어있는 파일
// ...</code></pre>
<p>이제 <code>validation.pipe.js</code>를 통해 어떻게 ErrorResponse가 어떻게 만들어지는지 확인해보자.</p>
<br>

<h2 id="validationpipe">ValidationPipe</h2>
<p>필자는 Response가 만들어지는 과정이 궁금하기 때문에 <code>ValidationPipe</code>에서 <code>createExceptionFactory</code>를 조금 집중해서 보려고 한다.</p>
<p><code>main.ts</code>에서 <code>ValidationPipe</code>를 생성할 때 <code>exceptionFactory</code>라는 옵션을 줄 수 있다. 해당 Option의 파라미터로는 <code>(errors: ValidationError[]) =&gt; any;</code> 형태의 콜백을 받게 되며 파라미터로 들어오는 <strong><code>errors</code>에는 유효성을 검사에 실패한 프로퍼티의 정보와 Error 정보를 가지고 있다.</strong> 만약 요청 데이터가 전부 유효하다면 <code>createExceptionFactory</code>는 호출되지 않는다.</p>
<p>여기서 <code>ValidationError</code>의 타입을 추적하기 위해 <code>validation-error.interface.d.ts</code>파일을 확인하면 다음과 같다.</p>
<pre><code class="language-ts">export interface ValidationError {
  target?: Record&lt;string, any&gt;;
  property: string;
  value?: any;
  constraints?: {
    [type: string]: string;
  };
  children?: ValidationError[];
  contexts?: {
    [type: string]: any;
  };
}</code></pre>
<br>



<p>추가적으로 <code>ValidationPipe</code>의 ErrorResponse를 사용자가 커스텀이 가능하다. 만약 해당 옵션을 부여하지 않으면 <code>ValidationPipe</code>는 빌트인 되어 있는 <code>createExceptionFactory</code>를 사용하게 된다.</p>
<pre><code class="language-js">let ValidationPipe = class ValidationPipe {
    constructor(options) {
        // ...
        this.exceptionFactory = options.exceptionFactory || this.createExceptionFactory();
        // ...
    }</code></pre>
<br>

<p>그럼 이제 빌트인 되어 있는 <code>createExceptionFactory</code>를 확인하기 위해 <code>validation.pipe.js</code>파일을 살펴보자.</p>
<pre><code class="language-js">//validation.pipe.js
createExceptionFactory() {
    return (validationErrors = []) =&gt; {
        if (this.isDetailedOutputDisabled) { // 1
            return new http_error_by_code_util_1.HttpErrorByCode[this.errorHttpStatusCode]();
        }
        const errors = this.flattenValidationErrors(validationErrors); // 2
        return new http_error_by_code_util_1.HttpErrorByCode[this.errorHttpStatusCode](errors); // 3
    };
}</code></pre>
<br>

<h3 id="1-if-thisisdetailedoutputdisabled">1. <code>if (this.isDetailedOutputDisabled)</code></h3>
<p><code>isDetailedOutputDisabled</code>옵션 또한 마찬가지로 <code>ValidationPipe</code>를 생성할 때 부여할 수 있는 옵션이다. <code>disableErrorMessages</code>옵션을 <code>true</code>로 주게되면 Client에게 유효하지 않은 요청에 대한 응답을 보낼 때 Error 메세지는 응답하지 않게 된다. 즉 아래와 같이 <code>statusCode</code>와 <code>error</code>만 응답되게 된다.</p>
<pre><code class="language-ts">{
    &quot;statusCode&quot;: 400,
    &quot;error&quot;: &quot;Bad Request&quot;
}</code></pre>
<p><code>HttpErrorByCode</code>는 <code>@nest/common</code>에 <code>util</code>에 들어있으며 Http Status 코드가지고 <code>NestJs</code>에 빌트인 되어있는 <code>HttpException</code>과 매핑 해주는 역할을 한다.</p>
<p>참고로 <code>ValidatonPipe</code>의 기본 Http Status 코드는 400이며 <code>ValidationPipe</code>를 생성할 때 이 또한 커스텀 할 수 있다.</p>
<br>

<h3 id="2-const-errors--thisflattenvalidationerrorsvalidationerrors">2. <code>const errors = this.flattenValidationErrors(validationErrors)</code>;</h3>
<p>ErrorResponse가 어떻게 만들어지는지 알고자 한다면 중점적으로 봐야하는 부분이다.</p>
<p><code>flattenValidationErrors</code>는 아래와 같이 생겼다.</p>
<pre><code class="language-js">flattenValidationErrors(validationErrors) {
    return (0, iterare_1.iterate)(validationErrors) // 1
        .map(error =&gt; this.mapChildrenToValidationErrors(error)) // 2
        .flatten() // 3
        .filter(item =&gt; !!item.constraints) // 4
        .map(item =&gt; Object.values(item.constraints)) // 5
        .flatten() // 6
        .toArray(); // 7
}</code></pre>
<ol>
<li><p>인자로 들어온 <code>validationErrors</code>를 순회하는 API를 사용하기 위해 <code>iterate</code>라이브러리로 한번 래핑한다.</p>
</li>
<li><p><code>map</code>을 이용하여 <code>validationErrors</code>의 요소들을 순회하며 <code>mapChildrenToValidationErrors</code>를 실행한다. (자세한 건 아래서 확인해보자.)</p>
</li>
<li><p><code>mapChildrenToValidationErrors</code>의 결과를 가지고 <code>flatten</code>을 이용하여 배열을 평탄화 한다. 그 이유는 유효성 검증에 대상이 되는 <strong>프로퍼티에 여러개의 유효성 검증 데코레이터가 적용되어 있으며 <code>Nested Object</code> 혹은 <code>Array</code>에 대한 Validation도 진행해야 하기 때문이다.</strong></p>
</li>
<li><p><code>validationErrors</code>에서 실제로 Client에게 보여지는 message는 <code>constraints</code>이기 때문에 <code>constraints</code>가 있는 error만 필터링 한다.</p>
</li>
<li><p>error에서 <code>constraints</code>만 <code>map</code>을 이용하여 뽑아온다.</p>
</li>
<li><p>Client에게 응답되는 ErrorResponse에는 <strong>1차원 배열 안에 유효성 검증에 실패한 모든 Error 메세지가 포함되기 때문에 평탄화를 진행한다.</strong></p>
</li>
<li><p>배열로 변경 후 리턴하게 된다.</p>
</li>
</ol>
<br>

<h3 id="3-return-new-http_error_by_code_util_1httperrorbycodethiserrorhttpstatuscodeerrors">3. <code>return new http_error_by_code_util_1.HttpErrorByCode[this.errorHttpStatusCode](errors)</code>;</h3>
<p>2번에서 리턴된 Error들의 <code>constraints</code>를 받아 <strong><code>NestJs</code>의 빌트인 된 <code>HttpException</code>의 생성자와 같이 호출되면서 <code>message</code>라는 프로퍼티에 들어가 응답이 되게 되는 것이다.</strong></p>
<br>

<h2 id="flattenvalidationerrors-에서-사용된-method"><code>flattenValidationErrors</code> 에서 사용된 <code>method</code></h2>
<p>Error에 대한 평탄화를 위해 <code>validationErrors</code>를 순회하며 내부적으로 호출되는 함수이며 최종적인 ErrorMessage를 만드는 과정들이 들어있다. <code>flattenValidationErrors</code>를 간단히 정리하면 유효성 검증에 실패한 모든 프로퍼티에 대한 Error Message를 정리하고 역할을 한다. <strong>여기서 말한 모든 프로퍼티에는 데코레이터가 여러 개 적용된 프로퍼티, <code>Nested Object</code>, <code>Array</code> 등등에 해당된다.</strong></p>
<br>


<h3 id="mapchildrentovalidationerrors">mapChildrenToValidationErrors</h3>
<p><code>mapChildrenToValidationErrors</code>는 <code>validationErrors</code>이 순회하면서 가장 먼저 호출되는 api이다. 생김세는 아래와 같다.</p>
<pre><code class="language-js">mapChildrenToValidationErrors(error, parentPath) {
    if (!(error.children &amp;&amp; error.children.length)) { // 1
        return [error];
    }
    const validationErrors = []; // 2
    parentPath = parentPath // 3
        ? `${parentPath}.${error.property}`
        : error.property;
    for (const item of error.children) { // 4
        if (item.children &amp;&amp; item.children.length) {
            validationErrors.push(...this.mapChildrenToValidationErrors(item, parentPath));
        }
        validationErrors.push(this.prependConstraintsWithParentProp(parentPath, item));
    }
    return validationErrors;
}</code></pre>
<ol>
<li><p>인자로 들어온 error가 children을 가지고 있지 않는다면 즉 중첩되어 있는 Error를 가지고 있지 않다면 Return한다. (Error  가 없는 경우)</p>
</li>
<li><p>새로운 Error 배열을 생성한다.</p>
</li>
<li><p>부모의 경로를 정의한다. 인자로 들어온 <code>parentPath</code>의 존재 유무로 결정된다. <code>parentPath</code>의 쓰임세는 <code>prependConstraintsWithParentProp</code>에서 확인할 수 있다.</p>
</li>
<li><p>인자로 들어온 error를 순회하면서 계속해서 내부 Error를 탐색해 나간다. 여기서 자식이 있는 경우와 자식이 없는 경우로 나뉘어지는데 유효성 검증 대상의 프로퍼티가  데코레이터가 여러 개 적용된 프로퍼티, <code>Nested Object</code>, <code>Array</code> 등등 경우가 다양하게 있기 때문에 <code>mapChildrenToValidationErrors</code>를 <strong>재귀하면서 최하위까지 타고 들어간다.</strong></p>
</li>
</ol>
<br>

<h3 id="prependconstraintswithparentprop">prependConstraintsWithParentProp</h3>
<p><code>prependConstraintsWithParentProp</code>는 간단하게 이야기 하면 Error로 응답되는 Message의 Key값에 부모가 있다면 부모의 이름을 붙혀주는 역할 및 key 값에 걸맞는 constraints를 매핑해주는 역할이다. 이 후 인자로 들어온 <strong>error에 프로퍼티에 <code>constraints</code>를 추가하거나 덮어 쓰여지게 된다.</strong></p>
<pre><code class="language-js">prependConstraintsWithParentProp(parentPath, error) {
    const constraints = {};
    for (const key in error.constraints) {
        constraints[key] = `${parentPath}.${error.constraints[key]}`;
    }
    return Object.assign(Object.assign({}, error), { constraints });
}</code></pre>
<p>즉 <code>Array</code>와 <code>Nested Object</code>의 경우가 이에 해당하며 해당 로직을 거쳐야 아래와 같은 ErrorResponse의 형태를 띄게 된다.</p>
<pre><code class="language-ts">// EX
&quot;message&quot;: [
    // ...
    &quot;person.name must be a string&quot;,
]</code></pre>
<br>

<h2 id="정리">정리</h2>
<p>라이브러리의 코드를 하나씩 살펴보면서 <code>NestJs</code>가 <code>class-validator</code>를 통해 들어온 Error를 어떻게 가공하고 사용하는지에 대해 알아 볼 수 있었다. 이 과정을 통해 라이브러리의 코드를 열어본다는 자신감을 얻을 수 있었으며 추 후 <code>NestJs</code>에서 Error 공통화를 할 때 조금 더 도움이 될 수 있을 것 같다는 생각을 하게 되었다. (<del>정작 <code>class-validator</code>에서 Error를 어떻게 만들어 CallBack으로 던지는지에 대한 내용은 없다...</del>)</p>
<br>

<h2 id="reference">Reference</h2>
<ul>
<li><a href="https://docs.nestjs.com/techniques/validation">https://docs.nestjs.com/techniques/validation</a></li>
<li><a href="https://github.com/typestack/class-validator">https://github.com/typestack/class-validator</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJs Transaction Decorator 만들기]]></title>
            <link>https://velog.io/@dev_leewoooo/NestJs-Transaction-Decorator-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_leewoooo/NestJs-Transaction-Decorator-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 13 Feb 2023 00:22:51 GMT</pubDate>
            <description><![CDATA[<h1 id="transaction-decorator-만들기">Transaction Decorator 만들기</h1>
<blockquote>
<p>현재 글은 <code>Postgres</code> + <code>TypeORM</code> 기준으로 작성되었습니다. 하지만 아래 개념을 이용하여 다른 ORM에도 적용이 가능합니다. (MikroORM 등등...)</p>
</blockquote>
<h2 id="테스트-코드">테스트 코드</h2>
<ul>
<li><p><a href="https://github.com/i-am-a-toy/pyc_backend_v2/blob/main/src/core/decorator/transactional.decorator.spec.ts">Transactional Decorator 테스트 코드</a></p>
</li>
<li><p><a href="https://github.com/i-am-a-toy/pyc_backend_v2/blob/main/src/core/database/typeorm/generic-typeorm.repository.spec.ts">Generic Repository 테스트 코드</a></p>
</li>
</ul>
<br>

<h2 id="goal">Goal</h2>
<ul>
<li>AOP 개념을 이용하여 Transaction 데코레이터 만들기</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p><code>TypeORM</code>을 이용하여 Transaction을 이용하는 방법은 <a href="https://velog.io/@dev_leewoooo/TypeORM%EC%97%90%EC%84%9C-Transaction%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">이전 글</a>에서도 작성하였듯 대표적으로 2가지가 있다. (3가지 이지만 Nest 공식 Doc에서는 2가지 방법을 추천하고 있음.)</p>
<ol>
<li><p><code>QueryRunner</code>를 이용하여 <code>Transaction</code></p>
</li>
<li><p><code>transaction</code> method를 이용</p>
</li>
</ol>
<p>필자는 주로 <code>QueryRunner</code>를 이용하여 <code>Transaction</code>을 사용하는 방법을 이용하고 있었다. 그러다 보니 <code>Transaction</code>을 사용하는 곳에 코드는 거의 필수적으로 아래와 같은 모양을 띄게 되었다.</p>
<pre><code class="language-ts">async saveWithQueryRunner(user: Users){
  const queryRunner = this.connection.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    const saved = await queryRunner.manager.save(user);
    //...
    await queryRunner.commitTransaction();
  } catch (e) {
    await queryRunner.rollbackTransaction();
  } finally {
    await queryRunner.release();
  }
}</code></pre>
<br>

<p>위와 같은 코드들이 반복되니 method들이 크기가 커지기도 하고 <code>try</code> 절에 들어가는 비지니스 로직을 한눈에 보기 어려운 부분들도 발생하게 되었다.</p>
<p>게다가 <code>Layer</code>별 분리가 되지 않아 <code>Service Layer</code>에서 <code>Query</code>를 작성하는 등등에 문제가 발생하였다... (<code>CustomRepository</code>를 이용하는 방법을 택하지 않는 이상 동일하며 그곳에서도 try~catch는 동일할 것이다.)</p>
<p>그래서 이러한 부분들을 어떻게 처리할 수 있을까 고민하다 이전 <a href="https://www.youtube.com/watch?v=AHSHjCVUsu8">NestJS MeetUp</a>에서 발표자료를 참고하여 <code>AOP</code>를 이용한 <code>Transaction</code>처리를 하기로 결정하였다. (<del>spring에서 @Transactional 어노테이션처럼 사용하고 싶엇다..</del>)</p>
<br>

<h2 id="cls-hooked">cls-hooked</h2>
<p><code>Node</code>진영에서는 <code>single thread</code>이기 때문에 <code>spring boot</code>에서의 <code>thread local</code>같은 개념이 없다.</p>
<blockquote>
<p><code>thread local</code> : 간단히 설명하자면 thread 정보를 key로 하여 값을 저장해두며 같은 thread에서만 접근이 가능한 공간이다.</p>
</blockquote>
<p>그래서 <code>Node</code>진영에서는 <code>thread local</code>과 비슷한 성격을 띄는 <code>cls-hooked</code>라는 라이브러리를 이용한다.</p>
<p><code>cls-hooked</code>는 요청이 들어올 때 마다 <code>Namespace</code>라는 곳에 <code>context</code>를 생성하여 해당 요청만 접근할 수 있는 공간을 만들어준다. 그리고 요청이 끝나게 되면 <code>finally</code> 구문에서 해당 <code>context</code>를 닫아준다. 이를 이용하여 <strong>요청이 들어오면 해당 요청에서만 사용할 <code>entityManager</code>를 생성하여 <code>Namespace</code>에 넣어 줄 것이다.</strong></p>
<pre><code class="language-ts">// @ cls-hooked
Namespace.prototype.run = function run(fn) {
  let context = this.createContext();
  this.enter(context);
  try {
    // ...
  } catch (exception) {
    throw exception;
  } finally {
    // ...
    this.exit(context);
  }
};</code></pre>
<br>

<h2 id="흐름도">흐름도</h2>
<p><img src="https://user-images.githubusercontent.com/74294325/212851534-7a7fc205-48c1-43e7-be88-317a63fb7b5b.png" alt="image"></p>
<blockquote>
<p>Transaction 개요 : <a href="https://www.youtube.com/watch?v=AHSHjCVUsu8">NestJS MeetUp</a></p>
</blockquote>
<br>

<p>이론적으로 위와 같이 요청이 들어오면 <code>Namespace</code>를 생성해준 후 <code>Namespace</code>안에서 <code>Transaction</code>을 처리하고 요청이 완료되면 <code>Namespace</code>를 닫아주면 된다.</p>
<br>

<h3 id="간단한-사용-후기">간단한 사용 후기</h3>
<p><strong>직접 사용해보니 요청이 들어오면 <code>Namespace</code> 존재 여부를 확인 후 없으면 생성하고 있으면 존재하는 <code>Namespace</code>를 사용하면 된다.</strong></p>
<p>요청 마다 <code>Namespace</code>를 생성하게 되면 기존 <code>Namespace</code>에 생성된 <code>context</code>가 없어지고 새로운 <code>Namespace</code>로 덮어쓰여지기 때문에 <strong>요청을 처리 중인 <code>context</code>가 사라져 자신만의 <code>context</code>를 찾지 못하는 문제가 발생하게 된다.</strong></p>
<br>

<h2 id="transaction-데코레이터-작성"><code>Transaction</code> 데코레이터 작성</h2>
<p><code>Transaction</code> 데코레이터를 작성하는 순서는 아래와 같다.</p>
<ol>
<li><p><code>Namespace</code>를 생성 후 <code>EntityManager</code>를 심어주는 <code>Middleware</code></p>
</li>
<li><p><code>Namespace</code>에 있는 <code>EntityManager</code>에 접근할 수 있는 헬퍼</p>
</li>
<li><p><code>Transaction Decorator</code></p>
</li>
</ol>
<br>

<h3 id="namespace를-생성-후-entitymanager를-심어주는-middleware"><code>Namespace</code>를 생성 후 <code>EntityManager</code>를 심어주는 <code>Middleware</code></h3>
<p><code>Namespace</code>를 미들웨어에서 생성하는 이유는 <code>NestJs</code> 요청 라이프사이클을 보면 알 수 있듯이 <strong>제일 먼저 요청이 거치게 되는 곳이다.</strong></p>
<p><img src="https://user-images.githubusercontent.com/74294325/212854334-7aa772c0-afdb-4d96-adde-cb6ede015ea0.png" alt="image"></p>
<br>

<p><code>Middleware</code>에서 <code>Namespace</code>가 존재 유무를 판단하여 생성 혹은 가져와 해당 <code>Namespace</code>에 <code>EntityManager</code>를 넣어준다. (이 때 <code>EntityManager</code>를 <code>DI</code>받게 되는데 <code>ORM</code> 모듈을 <code>appModule</code>에 등록해놓았기 때문에 주입이 가능한 것이다.)</p>
<pre><code class="language-ts">@Injectable()
export class TransactionMiddleware implements NestMiddleware {
  constructor(private readonly em: EntityManager) {} // 1

  use(_req: Request, _res: Response, next: NextFunction) {
    const namespace = getNamespace(PYC_NAMESPACE) ?? createNamespace(PYC_NAMESPACE);
    return namespace.runAndReturn(async () =&gt; {
      Promise.resolve()
        .then(() =&gt; this.setEntityManager()) // 2
        .then(next); // 3
    });
  }

  private setEntityManager() {
    const namespace = getNamespace(PYC_NAMESPACE)!;
    namespace.set&lt;EntityManager&gt;(PYC_ENTITY_MANAGER, this.em);
  }
}</code></pre>
<br>

<ol>
<li><p><code>ORM</code> Module에서 export 하고 있는 <code>EntityManager</code>를 주입받는다.</p>
</li>
<li><p>주입받은 <code>EntityManager</code>를 <code>Namespace</code>에 넣어준다. 이 때 넣어준 <code>EntityManager</code>는 요청의 <code>context</code>에 들어가게 된다.</p>
</li>
<li><p>미들웨어 다음에 실행될 함수를 실행시켜준다.</p>
</li>
</ol>
<br>

<h3 id="namespace에-있는-entitymanager에-접근할-수-있는-헬퍼"><code>Namespace</code>에 있는 <code>EntityManager</code>에 접근할 수 있는 헬퍼</h3>
<p>헬퍼의 역할은 단순하다. <code>Namespace</code>가 <code>active</code>상태인지 확인하고 해당 <code>Namespace</code>에서 현재 요청이 접근할 수 있는 <code>context</code>에 접근하여 <code>EntityManager</code>를 꺼내오는 역할을 한다.</p>
<pre><code class="language-ts">@Injectable()
export class TransactionManager {
  getEntityManager(): EntityManager {
    const nameSpace = getNamespace(PYC_NAMESPACE);
    if (!nameSpace || !nameSpace.active) throw new InternalServerErrorException(`${PYC_NAMESPACE} is not active`);
    return nameSpace.get(PYC_ENTITY_MANAGER);
  }
}</code></pre>
<br>

<h3 id="transaction-decorator"><code>Transaction Decorator</code></h3>
<p>이제 각 Method 위에 붙일 데코레이터를 작성하면 된다. (데코레이터 작성법에 대한 것은 현재 글에서 서술하지 않겠다. 공식 문서를 참조하여 작성하였음. <a href="https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators">Typescript - Method Decorator</a>)</p>
<pre><code class="language-ts">export function Transactional() {
  return function (_target: Object, _propertyKey: string | symbol, descriptor: TypedPropertyDescriptor&lt;any&gt;) {
    // 1
    const originMethod = descriptor.value;

    // 2
    async function transactionWrapped(...args: unknown[]) {
      // 3
      const nameSpace = getNamespace(PYC_NAMESPACE);
      if (!nameSpace || !nameSpace.active) throw new InternalServerErrorException(`${PYC_NAMESPACE} is not active`);

      // 4
      const em = nameSpace.get(PYC_ENTITY_MANAGER) as EntityManager;
      if (!em) throw new InternalServerErrorException(`Could not find EntityManager in ${PYC_NAMESPACE} nameSpace`);

      // 5
      return await em.transaction(async (tx: EntityManager) =&gt; {
        nameSpace.set&lt;EntityManager&gt;(PYC_ENTITY_MANAGER, tx);
        return await originMethod.apply(this, args);
      });
    }

    // 5
    descriptor.value = transactionWrapped;
  };
}</code></pre>
<br>

<ol>
<li><p>인자로 들어오는 <code>PropertyDescriptor</code>에서 원본 메소드를 꺼내온다.</p>
</li>
<li><p>원본 메소드를 래핑 할 <code>Transaction Method</code>를 정의 한다.</p>
</li>
<li><p>현재 <code>namespace</code>가 존재하는지 혹은 <code>namespace</code>가 활성화 되어있는지 검증한다.</p>
</li>
<li><p><code>namespace</code>가 존재한다면 <a href="#namespace%EB%A5%BC-%EC%83%9D%EC%84%B1-%ED%9B%84-entitymanager%EB%A5%BC-%EC%8B%AC%EC%96%B4%EC%A3%BC%EB%8A%94-middleware">이전 <code>Middleware</code></a>서 넣어준 <code>EntityManager</code>를 꺼낸다.</p>
</li>
<li><p><code>EntityManager</code>의 <code>transaction</code>메소드를 실행시킨 후 <code>callback</code>에서 <a href="#namespace%EC%97%90-%EC%9E%88%EB%8A%94-entitymanager%EC%97%90-%EC%A0%91%EA%B7%BC%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%ED%97%AC%ED%8D%BC"><code>Transaction</code> 헬퍼</a>에서 꺼내 쓸 수 있도록 <code>callback</code>인자로 받은 <code>Transaction</code>이 시작된 <code>EntityManager</code>를 넣어준다.</p>
</li>
<li><p>이 후 <code>PropertyDescriptor</code>의 value 즉 <strong>추 후 실행 될 메소드를 <code>Transaction Method</code>로 변경해준다.</strong></p>
</li>
</ol>
<br>

<h2 id="repository에-transaction-entitymanager-사용하기">Repository에 Transaction EntityManager 사용하기</h2>
<p><code>@Transactional</code> 데코레이터를 사용하면 이제 <code>namespace</code>에 <code>Transaction</code>이 시작된 <code>Entity Manager</code>가 들어가는 것을 데코레이터 작성을 하면서 알아봤다.</p>
<p>이제 <code>CustomRepository</code>에서 해당 <code>Entity Manager</code>를 꺼내서 사용하기면 하면 된다. 이 때를 위 해 <a href="#namespace%EC%97%90-%EC%9E%88%EB%8A%94-entitymanager%EC%97%90-%EC%A0%91%EA%B7%BC%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%ED%97%AC%ED%8D%BC">헬퍼</a>를 작성한 것이다. <code>TransactionManager</code>를 <code>CustomRepository</code>에 프레임워크의 힘을 빌려 <code>DI</code>를 해주면 된다.</p>
<p>필자는 <code>abstract class</code>를 이용하여 <strong>기초적은 CRUD method는 구현하고 각 <code>CustomRepository</code>에서 필요한 기능만 구현하도록 처리하였으며 <a href="#namespace%EC%97%90-%EC%9E%88%EB%8A%94-entitymanager%EC%97%90-%EC%A0%91%EA%B7%BC%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%ED%97%AC%ED%8D%BC">헬퍼</a>를 <code>abstract class</code>에서 <code>DI</code>받았다.</strong></p>
<p>추가적으로 만약 <strong><code>@Transactional</code> 데코레이터를 사용하지 않고 <a href="#namespace%EC%97%90-%EC%9E%88%EB%8A%94-entitymanager%EC%97%90-%EC%A0%91%EA%B7%BC%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%ED%97%AC%ED%8D%BC">헬퍼</a>를 통해 Repository를 가져오게 되면 <code>Transaction</code>이 열리지 않은 상태의 EntityManger를 사용한다.</strong></p>
<pre><code class="language-ts">export abstract class GenericTypeOrmRepository&lt;T extends RootEntity&gt; {
  // 1
  constructor(@Inject(TransactionManager) private readonly txManger: TransactionManager) {}

  // 2
  abstract getName(): EntityTarget&lt;T&gt;;

  async save(t: T | T[]): Promise&lt;void&gt; {
    await this.getRepository().save(Array.isArray(t) ? t : [t]);
  }

  async findById(id: number): Promise&lt;T | null&gt; {
    const findOption: FindOneOptions = { where: { id } };
    return this.getRepository().findOne(findOption);
  }

  async remove(t: T | T[]): Promise&lt;void&gt; {
    await this.getRepository().remove(Array.isArray(t) ? t : [t]);
  }

  // 3
  protected getRepository(): Repository&lt;T&gt; {
    return this.txManger.getEntityManager().getRepository(this.getName());
  }
}</code></pre>
<br>

<ol>
<li><p><a href="#namespace%EC%97%90-%EC%9E%88%EB%8A%94-entitymanager%EC%97%90-%EC%A0%91%EA%B7%BC%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%ED%97%AC%ED%8D%BC">헬퍼</a>를 생성자 주입받는다.</p>
</li>
<li><p><code>EntityManager</code>를 통해 Repository를 가져올 때 필요한 <code>Token</code>을 얻어오기 위해 <strong>해당 class를 상속받는 <code>class</code>에게 구현을 위임한다.</strong></p>
</li>
<li><p>상속하는 <code>class</code>에서 구현한 <code>getName()</code>을 통해 각각의 해당하는 <code>Entity</code>의 <code>Repository</code>를 얻어온다.</p>
</li>
</ol>
<br>

<p>그럼 구현 <code>class</code>에서는 아래와 같이 <code>getName()</code>만 구현하면 된다.</p>
<pre><code class="language-ts">class MockRepository extends GenericTypeOrmRepository&lt;Mock&gt; {
  getName(): EntityTarget&lt;Mock&gt; {
    return Mock.name;
  }
}</code></pre>
<br>

<h2 id="테스트-코드-1">테스트 코드</h2>
<p>테스트를 위한 <code>Class</code>를 작성하고 그 안에서 <code>@Transactional</code> 데코레이터를 사용하는 간단한 함수를 구현하였다.</p>
<pre><code class="language-ts">class Greeting {
  @Transactional()
  greeting() {
    console.log(&#39;Hello Transactional Decorator&#39;);
  }
}</code></pre>
<p><code>greeting</code> method를 호출할 때 정상적으로 트랜젝션 안에서 실행되는지 검증하는 테스트 코드는 아래와 같으며 TypeORM의 DataSource를 생성하기 위해 <code>sqlite3</code>을 사용하였다. (실패 케이스에 대한 테스트 코드는 <a href="https://github.com/i-am-a-toy/pyc_backend_v2/blob/main/src/core/decorator/transactional.decorator.spec.ts">transactional.decorator.spec.ts</a> 에서 확인할 수 있다.)</p>
<pre><code class="language-ts">it(&#39;entityManager가 있는 경우 (정상)&#39;, async () =&gt; {
  //given
  const mock = new Greeting();
  const namespace = createNamespace(PYC_NAMESPACE); // 1

  // 2
  const dataSource = await new DataSource({
    type: &#39;sqlite&#39;,
    database: &#39;:memory:&#39;,
    synchronize: true,
    logging: true,
  }).initialize();
  const em = dataSource.createEntityManager();

  await expect(
    namespace.runPromise(async () =&gt; {
      namespace.set&lt;EntityManager&gt;(PYC_ENTITY_MANAGER, em); // 3
      await Promise.resolve().then(mock.greeting); // 4
    }),
  ).resolves.not.toThrowError(); // 5
});</code></pre>
<ol>
<li><p>Namespace를 생성 한다.</p>
</li>
<li><p>TypeORM의 <code>DataSource</code>를 생성 후 <code>init</code>해준다.</p>
</li>
<li><p>1번에서 생성한 Namespace에 EntityManager를 세팅한다.</p>
</li>
<li><p><code>greeting</code>를 호출한다.</p>
</li>
<li><p>호출 한 결과에서는 <code>Error</code>를 발생시키지 않는다.</p>
</li>
</ol>
<br>

<p><code>TypeORM</code>의 Logging 옵션을 키고 테스트를 실행시킨 결과이다. <code>greeting</code>이 정상적으로 호출 된 것을 확인할 수 있으며 <code>BEGIN TRANSACTION</code>과 <code>COMMIT</code>이 <code>greeting</code>의 호출 결과를 감싸고 있는 것을 볼 수 있을 것이다.</p>
<p><img src="https://user-images.githubusercontent.com/74294325/217986001-8a38ca9e-b6ac-4524-be70-af0c1ddeb90a.png" alt="image"></p>
<br>

<h2 id="reference">REFERENCE</h2>
<ul>
<li><p><a href="https://github.com/Jeff-Lewis/cls-hooked">https://github.com/Jeff-Lewis/cls-hooked</a></p>
</li>
<li><p><a href="https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators">https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators</a></p>
</li>
<li><p><a href="https://www.youtube.com/@nestjskorea/videos">https://www.youtube.com/@nestjskorea/videos</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[고정소수점, 부동소수점]]></title>
            <link>https://velog.io/@dev_leewoooo/%EA%B3%A0%EC%A0%95%EC%86%8C%EC%88%98%EC%A0%90-%EB%B6%80%EB%8F%99%EC%86%8C%EC%88%98%EC%A0%90-76uc5iej</link>
            <guid>https://velog.io/@dev_leewoooo/%EA%B3%A0%EC%A0%95%EC%86%8C%EC%88%98%EC%A0%90-%EB%B6%80%EB%8F%99%EC%86%8C%EC%88%98%EC%A0%90-76uc5iej</guid>
            <pubDate>Wed, 28 Dec 2022 12:48:17 GMT</pubDate>
            <description><![CDATA[<h1 id="실수-표현-방식">실수 표현 방식</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>프로그래밍 안에서 소수점을 사용하는 방식을 이해하기</li>
</ul>
<br>

<h2 id="개요">개요</h2>
<p>프로그래밍 언어 안에서는 실수를 표현할 때 주로 <strong>부동소수점</strong>을 사용하게 된다.
대표적으로 Java와 float와 double을 주로 사용하고 Javascript에서는 number를 이용한다.</p>
<p>하지만 이러한 타입들을 이용하여 연산을 할 때 오차가 발생할 수 있기 때문에 신경써야 한다.</p>
<br>

<h2 id="이진기수법">이진기수법</h2>
<p>컴퓨터는 0과 1로 이루어진(2진법) 기계어를 사용한다. 하지만 사람은 수를 표현할 때 기본적으로 10진법을 이용한다.</p>
<p>정수의 경우에는 간단하게 10진수의 숫자를 2진수로 표현하기 쉽다.</p>
<p>예를들어 10진수인 18을 2진수로 변환하면 <code>10010</code>이 될 것이다.</p>
<p>정수는 위와 같이간단하지만 컴퓨터에서 실수 (real number)를 표현하려면 어떻게 할까?</p>
<p>정수 부분은 위에서와 같이 손쉽게 2진수로 변환을 하면 된다. 문제는 소수점인데 정수부와 동일하게 숫자를 하나씩 2진수로 바꿔버리면 안되나 생각을 할 수 있지만 그렇게 하면 <strong>서로 다른 10진수 숫자가 2진수로 변환되었을때 중복이 되는 문제가 발생한다.</strong></p>
<pre><code>1.9 =&gt; 1.1001
1.41 =&gt; 1.100 1</code></pre><p>그렇기 때문에 실수는 정수의 정 반대의 연산을 하게 된다. 정수는 2진법으로 변경하기 위해 2로 나눠가는 반면 실수는 2를 곱해가면서 1이나 0을 얻어오게 된다.</p>
<pre><code>0.625

0.625 * 2 = 1.25 =&gt; 1을 빼고 나머지 0.25
0.25 * 2 = 0.5  =&gt; 0을 빼고 나머지 0.5
0.5 * 2 = 1  =&gt; 1을 배고 나머지 0</code></pre><p>위와 같이 2를 계속해서 곱해가다 <strong>0이나오면 종료하고</strong> 결과를 위에서 부터 읽어준다 즉 <code>0.625</code>는 <code>0.101</code>이 된다.</p>
<br>

<h2 id="고정소수점-fixed-point">고정소수점 (Fixed Point)</h2>
<p>고정소수점 표현 방식이라는 것은 쉽게 말해 10진수를 2진수로 바꾸고 그것을 그대로 사용하는 방식이다.</p>
<p>예를 들어 <code>118.625(</code>라는 실수가 있으며 해당 실수를 2진수로 변환하면 <code>1110110.101</code>이 되며 이것을 그대로 저장하게 된다.</p>
<p>16비트 체계를 쓴다고 하면 <code>01110110.10100000</code>이와 같이 저장을 하게 되는데</p>
<p>첫번째 bit는 부호 비트이며 나머지는 정수부와 소수부로 채워지게 된다.</p>
<p><strong>소수부의 경우 앞에서부터 채우게 되며 남는 뒷자리는 다 0으로 채우게 된다.</strong></p>
<p>이러한 고정 소수점 방식은 구현하기 편하지만 <strong>사용하는 비트 수 대비 표현할 수 있는 수의 범위가 적고 정밀도가 낮기 때문에 실수를 다룰 필요가 있는 서비스에서는 거의 사용되지 않는다.</strong></p>
<br>

<h2 id="부동-소수점-floating-point">부동 소수점 (Floating Point)</h2>
<p>부동 소수점 방식은 실수를 2진법으로 변경한 것을 그대로 사용하는 것이 아니라 몇가지 과정을 추가적으로 거친 후 저장을 하게 된다.</p>
<br>

<h3 id="정규화">정규화</h3>
<p>정규화라는 단어는 수학이나 컴퓨터 분야에서 다양한 의미로 쓰이지만 여기서 말하는 정규화라는 것은 2진수를 <code>1.xxx... * 2^n</code> 꼴로 변환하는 것을 말한다.</p>
<p>변환하는 방법은 간단하다. 정수부에서 1만 남을 때 까지 소수점을 왼쪽으로 <strong>이동</strong>시키고 이동한 칸 수 만큼 위의 식에서 n자리에 집어넣으면 된다. (<strong>정수부가 0인 경우에는 오른쪽으로 이동한다.</strong>)</p>
<p>예를 들어 위에서 봤던 <code>1110110.101</code>을 정규화 하면 <code>1.110110101 * 2 ^ 6</code>가 된다.</p>
<br>

<h3 id="ieee-754-부동소수점-표현">IEEE 754 부동소수점 표현</h3>
<p>IEEE 표준에 따르면 부동소수점 방식으로 실수를 저장하는데 32비트 혹은 64비트가 사용되며 32비트 기준으로 아래 그림과 같은 구조를 가지게 된다.</p>
<img src = https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/General_floating_point_ko.svg/500px-General_floating_point_ko.svg.png>

<br>

<p>부호 비트는 고정소수점과 동일하게 0이면 양수, 1이면 음수를 의미한다.</p>
<p><strong>정규화 된 결과 소수점 오른쪽에 있는 숫자들을 왼쪽부터 그대로 넣으면 되며 남은 자리는 0으로 그대로 채우게 된다.</strong></p>
<p>지수부는 위의 계산결과(<code>1.110110101 * 2 ^ 6</code>)에서 n의 위치에 있는 <code>6</code>를 2진수로 바꾼 <code>10</code>을 그냥 넣는 것이 아닌 <code>bias</code>라고 하는 지정된 숫자를 더한 다음에 넣어야 한다.</p>
<p>IEEE 표준에서는 32비트를 쓰는 경우 <code>bias</code>를 <code>127</code>로 규정하고 있다. 따라서 32비트 기준 <code>6 + 127 = 133</code>를 2진수로 바꾼 <code>10000101</code>가 들어가게 된다.</p>
<p>그럼 <code>bias</code>를 왜 쓸까? 그 이유는 지수가 음수가 될 수도 있어서 그렇다. 예를들어 <code>0.000101</code>이라는 이진수가 있을 때 정수부가 0이기 때문에 왼쪽이 아닌 오른쪽으로 소수점을 밀게 되는데 그럼 <code>1.01 * 2 ^ -4</code>가 된다.</p>
<p>위의 결과 (<code>1.01 * 2 ^ -4</code>)에서 <code>-4</code>를 지수에 어떻게 저장할 것인가? <strong>맨 앞 부호비트가 있다고 해도 해당 부호 비트는 지수부 만을 위한 부호비트가 아니다.</strong> 그렇기 때문에 <code>bias</code>을 더하여 32비트 기준 <code>0 ~ 127</code> 구간은 음수, <code>128 ~ 255</code> 구간은 양수를 표현하도록 만든 것이다.</p>
<br>

<h3 id="단정도-배정도">단정도, 배정도</h3>
<p>위에서 살펴본 것과 같이 32비트 체계를 32비트 단정도 (Single-Precision), 64비트 체계를 64비트 배전도 (Double-Precision) 라고 부른다.</p>
<p>프로그래밍 언어에서 흔히 접할 수 있는 실수형타입 <code>float, double</code>는 각각 전자, 후자에 해당한다.</p>
<p><code>float</code>는 부동소수점 방식을 사용하는 기본형이라는 의미로 <code>floating point</code>에서 따왔으며 <code>double</code>는 64비트 배정도를 사용한다는 의미로 <code>double-precision</code>에서 따왔을 것이라 추측된다.</p>
<p><code>double</code>는 64비트 체계에서 지수부가 11비트, 가수부가 52이다. 지수부가 <code>2 ^ 11</code> 즉 2048개의 수를 표현할 수 있으므로 <code>0 ~ 1023</code>구간은 음수, <code>1024 ~ 2047</code> 구간은 양수 지수를 의미하며 <code>bias</code>는 1023이 된다.</p>
<br>

<h2 id="reference">REFERENCE</h2>
<ul>
<li><p><a href="https://ko.wikipedia.org/wiki/%EA%B3%A0%EC%A0%95%EC%86%8C%EC%88%98%EC%A0%90">https://ko.wikipedia.org/wiki/%EA%B3%A0%EC%A0%95%EC%86%8C%EC%88%98%EC%A0%90</a></p>
</li>
<li><p><a href="https://ko.wikipedia.org/wiki/%EB%B6%80%EB%8F%99%EC%86%8C%EC%88%98%EC%A0%90">https://ko.wikipedia.org/wiki/%EB%B6%80%EB%8F%99%EC%86%8C%EC%88%98%EC%A0%90</a></p>
</li>
<li><p><a href="https://ko.wikipedia.org/wiki/IEEE_754">https://ko.wikipedia.org/wiki/IEEE_754</a></p>
</li>
<li><p><a href="https://gsmesie692.tistory.com/94">https://gsmesie692.tistory.com/94</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nginx log를 모니터링 해보자]]></title>
            <link>https://velog.io/@dev_leewoooo/Nginx-log%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90-weomskcq</link>
            <guid>https://velog.io/@dev_leewoooo/Nginx-log%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90-weomskcq</guid>
            <pubDate>Tue, 27 Dec 2022 00:19:55 GMT</pubDate>
            <description><![CDATA[<h1 id="nginx-log-with-prometheus-grafana">Nginx Log with Prometheus, Grafana</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>Nginx의 AccessLog를 노출 시켜 Prometheus, Grafana를 이용하여 시각화 하기</li>
</ul>
<br>

<h2 id="nginx의-log">Nginx의 Log</h2>
<p>nginx는 요청이 들어올 때 마다 Log가 생성된다. nginx를 설치 후 별도의 설정을 하지 않는다면 <code>nginx.conf</code>파일에 <code>http</code>블록에 아래와 같이 로그에 대한 기본설정이 들어있을 것이다.</p>
<pre><code class="language-conf">http {
    ...
    log_format  main  &#39;$remote_addr - $remote_user [$time_local] &#39;
                      &#39;&quot;$request&quot; $status $body_bytes_sent &#39;
                      &#39;&quot;$http_referer&quot; &quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#39;;

    access_log  /var/log/nginx/access.log  main;
    ...
}</code></pre>
<p>기본적으로 Nginx의 포맷에는 <code>$request.time</code>이 빠져있다. 공식문서에 따름면 <code>$request.time</code>를 아래와 같이 설명하고 있다.</p>
<blockquote>
<p>$request_time – Full request time, starting when NGINX reads the first byte from the client and ending when NGINX sends the last byte of the response body</p>
</blockquote>
<p>직역하자면 클라이언트에게 처음 요청이 들어와 RequestBody을 읽기 시작해서 ResponseBody의 마지막 byte가 쓰여지기 까지의 시간을 의미한다.</p>
<p>그렇기에 <code>nginx</code>의 로그 포멧에 추가해 주자. 최종적인 로그 포멧은 아래와 같다. (추가적으로 필요한 것은 공식문서를 참고하여 추가하면 된다.)</p>
<pre><code class="language-conf">log_format  main    &#39;$remote_addr - $remote_user [$time_local] &#39;
                    &#39;&quot;$request&quot; $status $body_bytes_sent &#39;
                    &#39;&quot;$http_referer&quot; &quot;$http_user_agent&quot; &quot;$request_time&quot;&#39;;</code></pre>
<br>

<h2 id="export-시키기">Export 시키기</h2>
<p><a href="https://velog.io/@dev_leewoooo/Nginx-Metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90">이전 글</a>과 동일하게 Nginx의 로그를 사용하려면 exporter가 필요하다. 역시 이 또한 존재하기 때문에 가져다가 사용하면 된다. <a href="https://github.com/martin-helmich/prometheus-nginxlog-exporter">prometheus-nginxlog-exporter</a></p>
<pre><code class="language-bash"># 설치 기준 v1.10.0
wget https://github.com/martin-helmich/prometheus-nginxlog-exporter/releases/download/v1.10.0/prometheus-nginxlog-exporter_1.10.0_linux_amd64.tar.gz

tar -xvf prometheus-nginxlog-exporter_1.10.0_linux_amd64.tar.gz

# Result
logger/
|-- LICENSE
|-- README.adoc
`-- prometheus-nginxlog-exporter</code></pre>
<p>압축을 해제하면 <code>prometheus-nginxlog-exporter</code> 실행파일이 존재한다.</p>
<p>이전 metrics 정보를 export 시킬 때와 다르게 exporter를 실행시킬 때 설정을 적용시킬 수 있는 <code>.yml</code>을 별도로 작성할 수 있다. 해당 설정 파일에서는 port, endPoint, format, log파일 위치 등을 지정할 수 있다.</p>
<pre><code class="language-yml">listen:
  # port: exporter를 노출시킬 port 설정 기본포트는 4040
  port: 4040
  # address: bindin 할 IP
  address: &quot;0.0.0.0&quot;
  # metrics_endpoint: metrics 정보를 서빙할 endPoint 기본설정은 &quot;/metrics&quot;
  metrics_endpoint: &quot;/metrics&quot;

# consul 사용여부
consul:
  enable: false

namespaces:
  # name: 추 후 Prometheus에서 query할 때 사용된다.
  - name: nginx
    # format: Nginx에서 사용하는 format을 동일하게 작성해준다.
    format: &#39;$remote_addr - $remote_user [$time_local] &quot;$request&quot; $status $body_bytes_sent &quot;$http_referer&quot; &quot;$http_user_agent&quot; &quot;$request_time&quot;&#39;
    # source: log 파일의 위치를 정의한다.
    source:
      files:
        - /var/log/nginx/access.log</code></pre>
<p>설정파일이 전부 작성되었다면 실행파일을 설정파일과 같이 실행시켜 준 후 curl로 요청을 보내면 정상적으로 작동하는 것을 확인할 수 있다.</p>
<pre><code class="language-bash">$ ./prometheus-nginxlog-exporter -config-file prometheus-nginxlog-exporter.yml

$ curl localhost:4040/metrics

# 결과
# HELP nginx_http_response_count_total Amount of processed HTTP requests
# TYPE nginx_http_response_count_total counter
nginx_http_response_count_total{method=&quot;GET&quot;,status=&quot;304&quot;} 63
# HELP nginx_http_response_size_bytes Total amount of transferred bytes
# TYPE nginx_http_response_size_bytes counter
nginx_http_response_size_bytes{method=&quot;GET&quot;,status=&quot;304&quot;} 0
# HELP nginx_http_response_time_seconds Time needed by NGINX to handle requests
# TYPE nginx_http_response_time_seconds summary
nginx_http_response_time_seconds{method=&quot;GET&quot;,status=&quot;304&quot;,quantile=&quot;0.5&quot;} 0
nginx_http_response_time_seconds{method=&quot;GET&quot;,status=&quot;304&quot;,quantile=&quot;0.9&quot;} 0
nginx_http_response_time_seconds{method=&quot;GET&quot;,status=&quot;304&quot;,quantile=&quot;0.99&quot;} 0
nginx_http_response_time_seconds_sum{method=&quot;GET&quot;,status=&quot;304&quot;} 0
nginx_http_response_time_seconds_count{method=&quot;GET&quot;,status=&quot;304&quot;} 63
# HELP nginx_http_response_time_seconds_hist Time needed by NGINX to handle requests
# TYPE nginx_http_response_time_seconds_hist histogram
nginx_http_response_time_seconds_hist_bucket{method=&quot;GET&quot;,status=&quot;304&quot;,le=&quot;0.005&quot;} 63
nginx_http_response_time_seconds_hist_sum{method=&quot;GET&quot;,status=&quot;304&quot;} 0
nginx_http_response_time_seconds_hist_count{method=&quot;GET&quot;,status=&quot;304&quot;} 63
# HELP nginx_parse_errors_total Total number of log file lines that could not be parsed
# TYPE nginx_parse_errors_total counter
nginx_parse_errors_total 0
...</code></pre>
<h2 id="prometheus-연결">Prometheus 연결</h2>
<p><a href="https://velog.io/@dev_leewoooo/Nginx-Metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90#prometheus">이전 글</a>에서 작성한 것과 같이 연결하면 된다. 설치는 생략하고 <code>scrape_configs</code> 부분을 설정해주면 된다.</p>
<pre><code class="language-yml"># ...
scrape_configs:
  - job_name: &quot;nginx&quot;

    static_configs:
      - targets: [&quot;localhost:4040&quot;]
# ...</code></pre>
<p>연결 후 프로메테우스에 접속해보면 정상적으로 수집하고 있는 것을 볼 수 있다.</p>
<p><img src="https://user-images.githubusercontent.com/74294325/206985219-8b393dd8-8413-4551-82bc-44ac9b3c82ad.png" alt="image"></p>
<br>

<h2 id="grafana-연결">Grafana 연결</h2>
<p><a href="https://velog.io/@dev_leewoooo/Nginx-Metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90#grafana">이전 글</a>에서 크게 벗어나는 것이 없으며 대쉬보드는 <a href="https://grafana.com/grafana/dashboards/6482-nginx-log-metrics/">NGINX Log Metrics</a>를 가져다 사용했다.</p>
<br>

<h3 id="예상치-못한-삽질">예상치 못한 삽질</h3>
<p>위의 대쉬포드를 가져다가 사용할 때 삽질을 조금 하게 됬는데 <a href="#export-%EC%8B%9C%ED%82%A4%EA%B8%B0">prometheus-nginxlog-exporter</a>에서 설정파일에 <code>namespaces</code>에서 <code>name</code>값을 설정할 수 있었다.</p>
<p>여기서 처음에 <code>nginx</code>가 아니라 커스텀하게 이름을 주었었는데 이미 만들어져 있는 <a href="https://grafana.com/grafana/dashboards/6482-nginx-log-metrics/">NGINX Log Metrics</a>대쉬보드를 사용하려니 이름이 일치되지 않았던 문제가 있었다.</p>
<p>해당 대쉬보드는 <code>name</code>값을 <code>nginx</code>를 default 값으로 쓰고 있었기 때문에 해당 이름의 일치가 되지 않는 다면 <code>promql</code>을 이용하여 Query를 날릴 때 정상적으로 데이터를 가져오지 못할 것이다.</p>
<br>

<p>최종적으로 Nginx의 log에 대해 그라파나를 이용해 시각화 한것이다.</p>
<p><img src="https://user-images.githubusercontent.com/74294325/206986672-61ef2a3d-b776-4449-a67a-9da3c16ff6d7.png" alt="image"></p>
<h2 id="references">References</h2>
<ul>
<li><p><a href="https://www.nginx.com/blog/using-nginx-logging-for-application-performance-monitoring/">https://www.nginx.com/blog/using-nginx-logging-for-application-performance-monitoring/</a></p>
</li>
<li><p><a href="https://github.com/martin-helmich/prometheus-nginxlog-exporter">https://github.com/martin-helmich/prometheus-nginxlog-exporter</a></p>
<ul>
<li><p><a href="https://github.com/gurumee92/getting-started-prometheus/blob/master/docs/part2/04_service_metric_monitoring_01/README.md">https://github.com/gurumee92/getting-started-prometheus/blob/master/docs/part2/04_service_metric_monitoring_01/README.md</a></p>
</li>
<li><p><a href="https://blog.ruanbekker.com/blog/2020/04/25/nginx-metrics-on-prometheus-with-the-nginx-log-exporter/">https://blog.ruanbekker.com/blog/2020/04/25/nginx-metrics-on-prometheus-with-the-nginx-log-exporter/</a></p>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis의 metrics를 모니터링해보자.]]></title>
            <link>https://velog.io/@dev_leewoooo/Redis%EC%9D%98-metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@dev_leewoooo/Redis%EC%9D%98-metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 13 Dec 2022 07:46:33 GMT</pubDate>
            <description><![CDATA[<h1 id="redis-metrics-with-prometheus-grafana">Redis Metrics with Prometheus, Grafana</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>Redis의 Metrics를 노출 시켜 Prometheus, Grafana를 이용하여 시각화 하기</li>
</ul>
<br>

<h2 id="grafana에서-제공하는-plugin을-이용하여-손쉽게-적용하기">Grafana에서 제공하는 Plugin을 이용하여 손쉽게 적용하기</h2>
<p>Grafana에서는 Redis의 Metric 정보를 제공하는 Plugin을 제공한다. - <a href="https://redisgrafana.github.io/">Redis Plugins</a></p>
<p><img src="https://user-images.githubusercontent.com/74294325/207247268-c9c7c66a-cfb7-4190-a086-712fbd6824d6.png" alt="image"></p>
<p>위의 사진과 같이 Redis에 대한 Plugin을 설치 후 접속정보만 넣어주면 손쉽게 Redis에 대한 시각화를 시킬 수 있다.</p>
<p>적용 순서는 아래와 같다.</p>
<ol>
<li><p>plugin 설치</p>
</li>
<li><p>datasource 생성</p>
</li>
<li><p>plugin의</p>
</li>
</ol>
<h3 id="datasource-생성-후-결과-확인">Datasource 생성 후 결과 확인</h3>
<p>plugin을 정상적으로 설치하면 Datasource를 선택할 수 있는 List에 Redis가 생긴다. Redis를 선택 후 아래의 사진에서 접속정보를 누른 후 plugin에서 제공하는 Dashboard를 이용하면 끝나게 된다.</p>
<ul>
<li><p>접속 정보 입력</p>
<p><img src="https://user-images.githubusercontent.com/74294325/207247605-2801da23-9b4b-44cb-bf7d-63c407e37b0f.png" alt="image"></p>
</li>
<li><p>결과화면</p>
<p><img src="https://user-images.githubusercontent.com/74294325/207249080-0b8df30c-d3ba-48c7-800d-26e5e3b7b10d.png" alt="test"></p>
</li>
</ul>
<br>

<h3 id="이럼-끝일까">이럼 끝일까?</h3>
<p>생각보다 간단하다고 적용하기 쉬워서 너무 좋은데? 라고 했지만 query를 이용하여 데이터를 조회하려 하다 보니 <strong>query를 사용하지 못하고 가져올 수 있는 데이터에 한계가 있다고 생각해서 결국 export를 찾아보게 되었다...</strong></p>
<br>

<h2 id="redis_exporter">Redis_exporter</h2>
<p><a href="https://velog.io/@dev_leewoooo/Nginx-Metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90#nginx-prometheus-exporter">이전 글</a> nginx-prometheus-exporter를 설치한 것을 참조하여 <a href="https://github.com/oliver006/redis_exporter">redis_exporter</a>를 설치하면 된다.</p>
<pre><code># 설치 기준 v1.45.0
wget https://github.com/oliver006/redis_exporter/releases/download/v1.45.0/redis_exporter-v1.45.0.linux-arm.tar.gz

tar -zxvf redis_exporter-v1.45.0.linux-arm.tar.gz

# Result
exporter/
|-- LICENSE
|-- README.md
`-- redis_exporter</code></pre><p>압축을 해재 후 redis의 Connection 정보와 같이 실행시키면 redis의 metrics 정보를 손쉽게 얻어올 수 있다.</p>
<pre><code class="language-bash"># help
$ ./redis_exporter --help

# result
...
-redis.addr string
    Address of the Redis instance to scrape (default &quot;redis://localhost:6379&quot;)
-redis.password string
    Password of the Redis instance to scrape
-web.listen-address string
    Address to listen on for web interface and telemetry. (default &quot;:9121&quot;)
-web.telemetry-path string
    Path under which to expose metrics. (default &quot;/metrics&quot;)
...

# excute
./redis_exporter -redis.addr=${레디스 접속 address} -redis.password=${레디스 password}</code></pre>
<br>

<h3 id="테스트">테스트</h3>
<p>정상적으로 exporter를 실행시켰다면 <code>http://localhost:9121/metrics</code>로 <code>curl</code>을 이용해 요청을 보내보자. <code>:9121</code>로 요청을 보내면 html을 return 해주고 <code>:9121/metrics</code>로 요청하면 redis의 metrics 정보를 받을 수 있다.</p>
<p><strong>추 후 프로메테우스에서는 <code>:9121/metrics</code>의 정보를 이용하게 된다.</strong></p>
<pre><code class="language-bash"># exporter
$ curl http://localhost:9121


# result
&lt;html&gt;
&lt;head&gt;&lt;title&gt;Redis Exporter v1.45.0&lt;/title&gt;&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Redis Exporter v1.45.0&lt;/h1&gt;
&lt;p&gt;&lt;a href=&#39;/metrics&#39;&gt;Metrics&lt;/a&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<br>

<h2 id="prometheus-연결">Prometheus 연결</h2>
<p>exporter와 동일하게 <a href="https://velog.io/@dev_leewoooo/Nginx-Metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90#prometheus">이전 글</a>에서 설치는 해봤으니 <code>prometheus.yml</code>의 설정하는 것만 해당 글에서 작성하겠다.</p>
<pre><code class="language-yml"># ...
scrape_configs:
  - job_name: &quot;redis&quot;
    static_configs:
      - targets: [&quot;localhost:9121&quot;]</code></pre>
<br>

<h2 id="grafana-연결">Grafana 연결</h2>
<p><a href="https://velog.io/@dev_leewoooo/Nginx-Metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90#grafana">이전 글</a>에서 크게 벗어나는 것이 없으며 대쉬보드는 <a href="https://grafana.com/grafana/dashboards/11692-redis-dashboard-for-prometheus-redis-exporter-1-x/">redis-exporter-1-x</a>를 가져다 사용했다.</p>
<br>

<h3 id="결과">결과</h3>
<p><img src="https://user-images.githubusercontent.com/74294325/207256164-ac23d10a-968b-4b75-9450-5aeb89558b67.png" alt="test"></p>
<h2 id="reference">Reference</h2>
<ul>
<li><p><a href="https://github.com/oliver006/redis_exporter">https://github.com/oliver006/redis_exporter</a></p>
</li>
<li><p><a href="https://redisgrafana.github.io/">https://redisgrafana.github.io/</a></p>
</li>
<li><p><a href="https://grafana.com/grafana/dashboards/11692-redis-dashboard-for-prometheus-redis-exporter-1-x/">https://grafana.com/grafana/dashboards/11692-redis-dashboard-for-prometheus-redis-exporter-1-x/</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nginx Metrics를 모니터링 해보자]]></title>
            <link>https://velog.io/@dev_leewoooo/Nginx-Metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@dev_leewoooo/Nginx-Metrics%EB%A5%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 12 Dec 2022 02:36:42 GMT</pubDate>
            <description><![CDATA[<h1 id="nginx-metrcis-with-prometheus-grafana">Nginx Metrcis with Prometheus, Grafana</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>Nginx Exporter를 이용하여 Prometheus에서 Nginx의 상태를 모니터링</li>
<li>Grafana를 이용하여 Prometheus에서 수집한 데이터를 시각화</li>
</ul>
<br>

<h2 id="nginx의-metric-정보-수집">Nginx의 Metric 정보 수집</h2>
<p>이전 Nginx를 설치 후 Nginx의 config를 설정하여 Nginx를 통해 NestJs에 접속할 수 있는 방법을 알아봤다. 오늘은 Nginx의 Metric 정보를 수집하는 방법을 알아보자.</p>
<p>Nginx의 Metric 정보를 수집하기 위해서는 필요한 것은 아래와 같다.</p>
<ul>
<li>Nginx</li>
<li>nginx-prometheus-exporter</li>
<li>Prometheus</li>
<li>Grafana</li>
</ul>
<p>위의 구성들에 대한 인프라 스트럭처는 아래의 사진과 같다. (<strong>외부에 Grafana만 노출시킨 후 Grafana에서 인증처리를 하고 프로메테우스와 Exporter는 내부에 두어 사용하면 될 것 같다.</strong>)</p>
<br>

<p><img src="https://user-images.githubusercontent.com/74294325/206655015-b5e1d461-7751-42f8-9635-dc1d1e700fc7.png" alt="infra"></p>
<br>

<h2 id="nginx의-메트릭-정보-노출시키기">Nginx의 메트릭 정보 노출시키기</h2>
<p>Nginx 즉 웹서버의 메트릭 정보를 얻으려면 <code>stub_status</code> 모둘을 활성화 시켜줘야 한다. 오픈 소스로 공개된 Nginx와 상용 버전에서 수집할 수 있는 메트릭 정보는 차이가 있다. (상용에 비해 조금 적다...)</p>
<p><code>stub_status</code>를 활성화 시켰다고 메트릭 정보가 자동으로 수집되는 것이아니다. 이를 위해 아래에서 설치할 <code>nginx-prometheus-exporter</code>를 이용하게 되는 것이다.</p>
<p>그럼 nginx의 설정을 이용하여 <code>stub_status</code>를 활성화 시켜보자. 최대한 <code>nginx.conf</code>를 수정하지 않기 위해 <code>/conf.d</code>에 <code>metrics.conf</code>를 생성하여 아래와 같이 작성하였다.</p>
<pre><code class="language-conf">server {
  listen 80;
  server_name localhost;

  location /metrics {
    stub_status on; # stub_status 활성화
    allow all; # allow 접근을 허용할 주소 설정
    # deny 접근을 허용하지 않을 주소 설정
  }
}</code></pre>
<p>위와 같이 설정을 하고 Nginx를 재시작하면 <code>http://localhost/metrics</code>로 접근할 수 있으며 <code>curl</code>을 이용해 요청을 해보면 아래와 같이 응답이 오는 것을 확인할 수 있다.</p>
<pre><code class="language-bash">$ curl localhost/metrics

# Result
Active connections: 2
server accepts  handled requests
39      39      366
Reading: 0 Writing: 1 Waiting: 1</code></pre>
<br>

<h2 id="install">Install</h2>
<p>이전 글에서 Nginx를 설치하는 것은 다뤘으니 Nginx를 제외한 3개의 서비스를 설치해보자.</p>
<p>설치할 때 물론 Docker를 이용할 수 있지만 Linux 환경에 직접 설치하는 방법을 이용할 것이다.</p>
<br>

<h2 id="wget">Wget</h2>
<p>리눅스 환경에서는 wget을 이용하여 다운로드를 할 수 있다. 만약 해당 환경에 wget이 없다면 sudo apt-get install wget을 통해 설치해주자.</p>
<pre><code class="language-bash">sudo apt-get update &amp;&amp; sudo apt-get install wget</code></pre>
<br>

<h2 id="nginx-prometheus-exporter">nginx-prometheus-exporter</h2>
<p><code>prometheus-nginxlog-exporter</code>의 역할은 쉽게 말해 Nginx에서 노출시킨 메트릭 정보를 Prometheus가 수집할 수 있도록 하는 역할을 한다.</p>
<pre><code class="language-bash"># 설치 기준 v0.11.0
wget https://github.com/nginxinc/nginx-prometheus-exporter/releases/download/v0.11.0/nginx-prometheus-exporter_0.11.0_linux_386.tar.gz

tar -xvf nginx-prometheus-exporter_0.11.0_linux_386.tar.gz

# Result
exporter/
|-- CHANGELOG.md
|-- LICENSE
|-- README.md
`-- nginx-prometheus-exporter</code></pre>
<p>압축을 해제하면 <code>nginx-prometheus-exporter</code> 실행파일이 생긴다. 해당 파일을 실행할 때 중요한 옵션이 2가지가 있다. (추가 적인 내용은 <code>nginx-prometheus-exporter --help</code>를 입력하면 확인할 수 있다.)</p>
<ol>
<li><p>-nginx.scrape-uri: nginx의 메트릭 정보를 가져올 주소를 지정할 수 있다. 위에서 설정한 대로 <code>http://localhost/metrics</code>를 지정하면 된다.</p>
</li>
<li><p>-web.listen-address: <code>nginx-prometheus-exporter</code>를 외부로 노출시킬 주소를 지정할 수 있다. default는 <code>:9113</code>이다.</p>
</li>
</ol>
<br>

<p>실행 후 <code>curl</code>로 요청을 보낸 결과다. (아래 사진은 5555포트로 노출시켜 브라우저로 실행한 결과이다.)</p>
<p><img src="https://user-images.githubusercontent.com/74294325/206660265-796929a8-b022-43f2-b30e-3e427d378bd2.png" alt="image"></p>
<br>

<h2 id="prometheus">Prometheus</h2>
<p><a href="https://github.com/prometheus/prometheus">프로메테우스</a>는 메트릭을 수집 및 시각화, 알림 서비스 등등 제공하는 오픈 소스 모니터링 시스템이다. </p>
<pre><code class="language-bash"># 설치 기준 v2.40.5
wget https://github.com/prometheus/prometheus/releases/download/v2.40.5/prometheus-2.40.5.freebsd-386.tar.gz

tar -xvf prometheus-2.40.5.freebsd-386.tar.gz

# Result
prometheus-2.40.5.linux-386/
|-- LICENSE
|-- NOTICE
|-- console_libraries
|-- consoles
|-- data
|-- nohup.out
|-- prometheus
|-- prometheus.yml
`-- promtool</code></pre>
<p><code>prometheus.yml</code>는 prometheus의 설정 파일이다. 해당 파일에서 메트릭 정보를 수집할 Target들을 설정할 수 있다. 자세한 설정은 <a href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/">prometheus-configuration</a>에서 확인할 수 있다.</p>
<p>프로메테우스와 <code>nginx-prometheus-exporter</code>를 연결해주려면 아래와 같이 <code>prometheus.yml</code>의 <code>scrape_configs</code>에 <code>job_name</code>과 <code>static_configs</code>에 <code>targets</code>를 <code>nginx-prometheus-exporter</code>의 주소로 지정해준다.</p>
<pre><code class="language-yaml">global:
  scrape_interval: 15s
  evaluation_interval: 15s
scrape_configs:
  - job_name: &quot;nginx&quot;

    static_configs:
      - targets: [&quot;localhost:9113&quot;]</code></pre>
<br>

<p>이 후 실행할 수 있다. <code>nginx-prometheus-exporter</code>와 마찬가지로 프로메테우스 실핼할 때 외부로 노출 시킬 주소를 설정할 수 있는 옵션이 있다. (추가 적인 내용은 <code>prometheus --help</code>를 입력하면 확인할 수 있다.)</p>
<ol>
<li>--web.listen-address: 프로메테우스를 외부로 노출시킬 주소를 지정할 수 있다. default는 <code>:9090</code>이다.</li>
</ol>
<p>프로메테우스를 실행 후 접속을 하면 아래와 같은 결과를 얻을 수 있다. <code>nginx</code>라는 <code>job_name</code>을 설정했기 때문에 <code>nginx</code>라는 <code>job</code>이 생성되어 있으며 정상적으로 <code>nginx-prometheus-exporter</code>와 연결되어 있는 것을 확인할 수 있다.</p>
<p><img src="https://user-images.githubusercontent.com/74294325/206663085-30c1f6a9-acef-4e89-99a4-7b03289e0e2f.png" alt="image"></p>
<br>

<h2 id="grafana">Grafana</h2>
<p>주로 프로메테우스는 메트릭 정보를 수집하는 역할만 하고 시각화는 하지 않는다. 따라서 프로메테우스에서 수집한 메트릭 정보를 시각화하기 위해서는 그라파나와 같은 대시보드를 제공하는 도구를 사용해야 한다.</p>
<pre><code class="language-bash">wget https://dl.grafana.com/enterprise/release/grafana-enterprise-9.3.1.linux-amd64.tar.gz

tar -zxvf grafana-enterprise-9.3.1.linux-amd64.tar.gz

# Result
grafana-9.3.1/
|-- LICENSE
|-- NOTICE.md
|-- README.md
|-- VERSION
|-- bin # 실행 파일
|-- conf # 설정 파일
|-- data
|-- plugins-bundled
|-- public
`-- scripts</code></pre>
<p>설치가 완료 되었으면 <code>conf</code> 폴더에 들어가서 <code>defaults.ini</code> 파일을 수정함으로 그라파나의 설정을 변경할 수 있다. 설정 중 현재는 포트만 변경하여 그라파나를 실행하려 한다. 기본 설정 포트는 3000번이다.</p>
<pre><code class="language-bash">...
#################################### Server ##############################
[server]
# The http port to use
http_port = ${변경할 포트}
...</code></pre>
<p>이 후 <code>bin</code>안에 있는 <code>grafana-server</code>를 실행시키면 그라파나가 실행된다. 실행 후 datasource에 아래와 같이 프로메테우스의 주소를 입력ㅎ고 저장하면 프로메테우스의 메트릭 정보를 그라파나에서 시각화 할 수 있다.</p>
<p><img src="https://user-images.githubusercontent.com/74294325/206668184-cff39908-bc27-4100-ae45-8e8cc7dad469.png" alt="image"></p>
<p>정상적으로 연결이 되었으면 누군가가 잘 만들어 놓은 대시보드를 가져와서 사용할 수 있다. <a href="https://grafana.com/grafana/dashboards/12708-nginx/">NGINX exporter</a>에서 <code>Prometheus Stats</code>를 가져와서 사용해보자.</p>
<p>최종 결과물은 아래와 같다.</p>
<p><img src="https://user-images.githubusercontent.com/74294325/206668755-62f2fdb2-d321-438b-be05-e051b1f68934.png" alt="image"></p>
<br>


<h2 id="references">References</h2>
<ul>
<li><p><a href="https://gurumee92.tistory.com/229">https://gurumee92.tistory.com/229</a></p>
</li>
<li><p><a href="https://velog.io/@sojukang/%EC%84%B8%EC%83%81%EC%97%90%EC%84%9C-%EC%A0%9C%EC%9D%BC-%EC%89%AC%EC%9A%B4-Prometheus-Grafana-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%84%A4%EC%A0%95-NGINX%ED%8E%B8">https://velog.io/@sojukang/%EC%84%B8%EC%83%81%EC%97%90%EC%84%9C-%EC%A0%9C%EC%9D%BC-%EC%89%AC%EC%9A%B4-Prometheus-Grafana-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%84%A4%EC%A0%95-NGINX%ED%8E%B8</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nginx를 통해 서버에 요청을 해보자. (With NestJs)]]></title>
            <link>https://velog.io/@dev_leewoooo/Nginx%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%84%9C%EB%B2%84%EC%97%90-%EC%9A%94%EC%B2%AD%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90.-With-NestJs</link>
            <guid>https://velog.io/@dev_leewoooo/Nginx%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%84%9C%EB%B2%84%EC%97%90-%EC%9A%94%EC%B2%AD%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90.-With-NestJs</guid>
            <pubDate>Fri, 02 Dec 2022 09:26:35 GMT</pubDate>
            <description><![CDATA[<h1 id="nginx-with-nestjs">Nginx with NestJs</h1>
<h2 id="goal">Goal</h2>
<ul>
<li>Nginx를 Web Server로 이용하여 NestJS와 연결해보기. (최대한 Linux 환경에서 테스트 하기 위해 Docker로 ubuntu를 실행시켜 환경 구축)</li>
</ul>
<br>

<h2 id="docker로-ubuntu-실행">Docker로 ubuntu 실행</h2>
<p>DockerHub에서 ubuntu 이미지를 받아서 실행시킨다. 도커 컨테이너를 생성하면서 해당 컨테이너에 접속할 수 있도록 옵션을 추가한다.</p>
<p>ubuntu 버전은 <code>20.04</code>를 기준으로 하였으며 -it 옵션을 준 이유는 해당 컨테이너가 실행되고 계속 돌 수 있도록 하기 위해서 이다. 자세한 건 (<a href="https://www.popit.kr/%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%B2%98%EC%9D%8C-docker-%EC%A0%91%ED%95%A0%EB%95%8C-%EC%98%A4%EB%8A%94-%EB%A9%98%EB%B6%95-%EB%AA%87%EA%B0%80%EC%A7%80/">개발자가 처음 Docker 접할때 오는 멘붕 몇가지</a> 참조)</p>
<p>마지막으로 80번 포트를 컨테이너 내부 80 포트와 바인딩 되어 Nginx가 정상적으로 설치되었는 지 확인하기 위해 <code>-p 80:80</code> 옵션을 부여해준다.</p>
<p>해당 컨테이너를 빠져나갈 때는 <code>ctrl + p + q</code> 명령어(<strong>컨테이너 종료하지 않고 빠져나오기</strong>) 로 빠져 나가면 ubuntu 컨테이너는 종료되지 않고 계속해서 실행된다. (<code>exit</code>로 나갈 경우 컨테이너 종료.)</p>
<pre><code class="language-bash">docker run -p 80:80 -d -it --name ubuntu_test ubuntu:20.04</code></pre>
<br>

<h2 id="ubuntu에-nginx-설치">Ubuntu에 Nginx 설치</h2>
<p>정상적으로 ubuntu 컨테이너에 접속을 하고 아래와 같은 명령어를 작성하면 Nginx가 설치된다.</p>
<pre><code class="language-bash">sudo apt-get install nginx</code></pre>
<p>하지만 항상 한번에 설치되는 법은 많지 않다. 나는 여기서 2가지의 문제를 만났다.</p>
<h3 id="1-e-unable-to-locate-package-xxx">1. E: Unable to locate package xxx...</h3>
<p>package의 레포지토리가 정상적으로 접근이 되지 않을 때 발생하는 Error이다. 그렇기 때문에 레포지토리를 업데이트 해주면 된다.</p>
<pre><code class="language-bash">apt update</code></pre>
<h3 id="2-bashcommand-not-found-sudo">2. bash:command not found sudo</h3>
<p>sudo package가 설치되어 있지 않아 발생하는 Error이다. 간단하게 sudo package를 설치해주면 된다.</p>
<pre><code class="language-bash">apt-get install sudo</code></pre>
<p>위 과정까지 완료 후 아래와 같은 명령어를 입력하면 Nginx가 정상적으로 설치된다. 설치를 하면서 TimeZone 설정을 물어본다. 나는 Asia/Seoul로 설정했다.</p>
<br>

<h2 id="nginx-접속해보기">Nginx 접속해보기</h2>
<p>위와 같이 정상적으로 Nginx를 설치하였다면 브라우저에 <code>localhost</code>라고 입력하면 아래와 같이 Nginx 화면이 나온다.</p>
<img src = https://user-images.githubusercontent.com/74294325/205224474-4d24e675-554d-4d59-b8db-a4a7de9ec227.png>

<br>

<h2 id="master-process와-worker-process">Master Process와 Worker Process</h2>
<p>기본적으로 Nginx가 실행되게 되면 Master Process와 Worker Process가 생성된다. Master Process는 Worker Process를 관리하고 Worker Process는 실제로 Client의 요청을 처리한다.</p>
<p>즉 아래와 같은 그림 처럼 Client의 요청을 Master Process가 받아서 Worker Process에게 전달하고 Worker Process가 요청을 처리한 후 결과를 Client에게 전달한다.</p>
<p>worker_processes의 갯수는 <code>nginx.conf</code>의 <code>worker_processes auto;</code> 라는 option을 볼 수 있는데 해당 option을 통해 Worker Process를 몇 개 생성할 것인지 정의할 수 있다.</p>
<p>보통 <code>auto</code>를 주게 되면 CPU의 코어 수 만큼 Worker Process가 생성된다.</p>
<p>Master Process와 Worker Process는 Tree 구조를 가지게 되며 아래와 같은 명령어를 입력하면 현재 Master Process와 Worker Process의 목록을 확인할 수 있다.</p>
<pre><code class="language-bash">sudo ps aux --forest | grep nginx

# result
# sudo ps aux --forest | grep nginx
root       865  0.0  0.0  53248  1492 ?        Ss   15:04   0:00 nginx: master process nginx
www-data   866  0.0  0.0  53572  3316 ?        S    15:04   0:00  \_ nginx: worker process
www-data   867  0.0  0.0  53572  3316 ?        S    15:04   0:00  \_ nginx: worker process</code></pre>
<br>

<h2 id="nginx의-기본-설정을-확인해보자">Nginx의 기본 설정을 확인해보자.</h2>
<p>Nginx를 설치하게 되면 기본적으로 <code>/etc/nginx/nginx.conf</code> 파일이 생성된다. 이 파일을 확인해보면 아래와 같은 내용이 있다. (<code>nginx.conf</code>의 위치를 찾아보려면 아래와 같은 명령어를 입력하여 확인할 수 있다.)</p>
<pre><code class="language-bash"># 실행결과
# /etc/nginx/nginx.conf

sudo find / -name nginx.conf</code></pre>
<br>

<h2 id="nginx로-들어오는-요청을-nestjs로-전달하기-서버에-nestjs가-실행되고-있다고-가정">Nginx로 들어오는 요청을 Nestjs로 전달하기. (서버에 Nestjs가 실행되고 있다고 가정)</h2>
<p>unbuntu를 실행할 때 80번 포트를 열어주었기 때문에 80번 포트를 통해 들어오는 요청을 내부 3000번 포트로 전달해주는 설정을 하면 된다.</p>
<p>기본적인 <code>nginx.conf</code>의 Http 블록에는 아래와 같이 <code>/etc/nginx/conf.d/*.conf</code> 파일을 include 하고 있다.</p>
<p>해당 경로에 커스텀 한 설정을 하여 Nginx를 구성할 수 있다. 아래와 같이 <code>nestjs.conf</code> 파일을 생성하고 아래와 같이 설정을 추가하면 된다.</p>
<pre><code class="language-bash">server {
  listen 80;
  server_name localhost;

  location / {
      proxy_pass http://localhost:3000/;
  }
}</code></pre>
<p>위의 config 파일을 간단하게 보면 80번 포트로 들어오는 요청을 <code>localhost:3000</code>으로 전달해주는 설정이다.</p>
<br>

<h2 id="nginx-재시작-후-확인하기">Nginx 재시작 후 확인하기.</h2>
<p>Nginx에 새로 생성한 커스텀한 설정을 반영하기 위해서는 Nginx를 재시작 해야한다. 우분투 버전에 따라 <code>systemctl</code> or <code>service</code> 명령어를 사용하여 Nginx를 재시작 할 수 있다.</p>
<pre><code class="language-bash"># service
sudo service nginx restart

# systemctl
sudo systemctl restart nginx</code></pre>
<p>재 시작이 완료되면 <code>http://localhost</code>로 접속하면 아래와 같이 Nestjs 기본 API가 호출되어 결과가 전될되는 것을 볼 수 있다.</p>
<img src = https://user-images.githubusercontent.com/74294325/205256866-914b17d8-2700-48d0-bf68-53fc99ecaf43.png>

<br>

<h2 id="정리">정리</h2>
<p>Docker를 이용하여 Nginx를 설치하고 Nestjs를 실행시켜 Nginx를 통해 Nestjs로 요청을 전달하는 과정을 정리해보았다.</p>
<p>EC2도 동일하겠지만 Docker를 이용하여 하나의 Linux안에서 Nginx의 포트만 외부에 열어주고 NestJs 서버는 외부에 열어주지 않았다. (현재 글에서는 ubuntu를 실행시킬 때 port option으로 처리)</p>
<p>이렇게 하면 WAS 서버가 외부에 노출되지 않고 Nginx를 통해서만 NestJs 서버에 접속을 할 수 있게 된다.</p>
<h2 id="reference">Reference</h2>
<ul>
<li><p><a href="https://hub.docker.com/_/nginx">Nginx Docker Hub</a></p>
</li>
<li><p><a href="https://www.popit.kr/%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%B2%98%EC%9D%8C-docker-%EC%A0%91%ED%95%A0%EB%95%8C-%EC%98%A4%EB%8A%94-%EB%A9%98%EB%B6%95-%EB%AA%87%EA%B0%80%EC%A7%80/">https://www.popit.kr/%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%B2%98%EC%9D%8C-docker-%EC%A0%91%ED%95%A0%EB%95%8C-%EC%98%A4%EB%8A%94-%EB%A9%98%EB%B6%95-%EB%AA%87%EA%B0%80%EC%A7%80/</a></p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=hA0cxENGBQQ">https://www.youtube.com/watch?v=hA0cxENGBQQ</a></p>
</li>
<li><p><a href="https://whatisthenext.tistory.com/123">https://whatisthenext.tistory.com/123</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Forward Proxy, Reverse Proxy에 관하여]]></title>
            <link>https://velog.io/@dev_leewoooo/Forward-Proxy-Reverse-Proxy%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@dev_leewoooo/Forward-Proxy-Reverse-Proxy%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Fri, 02 Dec 2022 01:58:44 GMT</pubDate>
            <description><![CDATA[<h1 id="proxy">Proxy</h1>
<h2 id="why">Why?</h2>
<ul>
<li>Nginx를 이용하기 전 Proxy 개념에 대한 정리하기 위함.</li>
</ul>
<br>

<h2 id="proxy란">Proxy란?</h2>
<p>프록시 서버는 클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 점속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램을 가리킨다.</p>
<p>프록시(Proxy)란 <strong>대리</strong> 라는 의미를 갖고 있으며, 서버와 서버사이의 중계 역할을 한다고 보면 된다. 프록시를 사용하는 이유는 보안상의 이유로 직접 통신할 수 없는 두 점사이에서 대리로 통신을 수행하며 <strong>보안성</strong>, <strong>성능</strong>, <strong>안정성</strong>을 향상 시키기 위해서 이다.</p>
<p>보통 웹은 클라이언트에서 서버로, 서버에서 클라이언트로 통신하며 데이터를 전달한다. <strong>이 때 필연적으로 중복되는 데이터를 반복하여 전달하는 상황이 발생하게 된다.</strong> 이렇게 동일한 요청을 매번 처리하는 것은 리소스 낭비와 부하로 이어지게 된다.</p>
<p>이러한 문제를 해결하기 위해 클라이언트와 서버 사이에 프록시 서버를 배치하여 <strong>중복 요청에 대하여 동일한 응답을 할 수 있다면, 클라이언트에겐 빠른 속도의 서비스 및 서버에게 불필요한 부하를 줄이는 효과를 낼 수 있다.</strong></p>
<br>

<h2 id="proxy의-종류">Proxy의 종류</h2>
<p>프록시 서버는 네트워크 상 어디에 위치하느냐, 혹은 어느 방향으로 데이터를 제공하느냐에 따라 <strong>Forward Proxy</strong>, <strong>Reverse Proxy</strong>로 나뉜다.</p>
<br>

<h2 id="forward-proxy">Forward Proxy</h2>
<img src = https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Open_proxy_h2g2bob.svg/1920px-Open_proxy_h2g2bob.svg.png>

<p>포워드 프록시 서버는 위의 그림과 같이 클라이언트 바로 뒤에 놓여 있다. <strong>클라이언트가 요청을 할 경우 요청을 하는 서버로 바로 가는 것이 아니라 요청 서버의 주소를 포워드 프록시에 전달을 하게 된다.</strong></p>
<p>이 후 포워드 프록시가 요청 서버로 부터 리소스를 응답받아 클라이언트에게 전달을 해준다.</p>
<h3 id="forward-proxy의-장점">Forward Proxy의 장점</h3>
<ul>
<li><p><strong>보안</strong> : 클라이언트는 <strong>포워드 프록시를 통해서만 외부에 요청을 하기 때문에 클라이언트가 해당 서버 혹은 웹 사이트에 직접적으로 접근하는 것을 방지할 수 있다.</strong> 대표적인 예가 클라이언트가 특정 사이트에 접근할 수 없도록 막을 수 있는 기능이다.</p>
</li>
<li><p><strong>캐싱</strong> : 클라이언트가 요청을 할 경우, <strong>포워드 프록시는 해당 요청을 캐싱하여 다른 클라이언트 혹은 동일한 요청이 들어올 경우 캐싱된 데이터를 전달해준다.</strong> 이렇게 되면 클라이언트는 캐싱된 데이터를 받아오기 때문에 서버에 부하를 줄일 수 있다.</p>
</li>
<li><p><strong>암호화</strong> : 클라이언트의 요청은 <strong>포워드 프록시 서버를 통과할 때 암호화 된다.</strong> 암호화 된 요청은 다른 서버를 통과할 때 필요한 최소한의 정보만 갖게 되는데, 이는 클라이언트의 ip를 숨길 수 있다는 장점이 있다. <strong>요청을 받은 서버에서 ip를 역 추적해도 포워드 프록시 서버의 ip만 알 수 있게 된다.</strong></p>
</li>
</ul>
<br>

<h2 id="reverse-proxy">Reverse Proxy</h2>
<img src = https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Reverse_proxy_h2g2bob.svg/1920px-Reverse_proxy_h2g2bob.svg.png>

<p>리버스 프록시는 위의 그림처럼 웹 서버 앞에 놓여 있는 것을 이야기 한다. 클라이언트는 웹 서비스를 접근할 때 <strong>웹 서버에 요청을 하는 것이 아닌 프록시로 요청을 하게 되고, 프록시가 웹 서버에 요청을 하여 리소스에 접근하는 방식이다.</strong></p>
<p>내부 서버가 직접 서비스를 제공해도 되지만 이렇게 구성하는 이유는 보안 때문이다. 네트워크 환경에서 <strong>DMZ</strong>가 존재한다. 웹 서버를 DMZ에 놓고 사용을 할 수 있지만 웹 서버는 통상적으로 DB에 연결이 되어 있으며 웹 서버에 대한 공격이 DB 까지 이어질 수 있다.</p>
<p><strong>따라서 통상적으로 리버스 프록시를 DMZ에 두고 실제 서비스 서버는 내부망에 위치시킨 후 서비스를 제공한다.</strong></p>
<blockquote>
<p>DMZ : 내부, 외부 네트워크 둘 다 접근할 수 있는 공간</p>
</blockquote>
<br>

<h3 id="reverse-proxy의-장점">Reverse Proxy의 장점</h3>
<ul>
<li><p><strong>로드 밸런싱</strong> : 서버의 트래픽이 몰리게 되면 하나의 서버로는 해당 요청을 감당하기 힘들어 지는 경우가 발생한다. 이럴 때 여러 대의 서버를 두고 요청을 분산시키는 것이 로드 밸런싱이다. 리버스 프록시는 여러 대의 서버에 <strong>분산시켜 요청을 보내게 된다. 이렇게 하면 서버의 부하를 분산시킬 수 있다.</strong></p>
</li>
<li><p><strong>보안</strong> : 리버스 프록시는 웹 서버 앞에 놓이기 때문에 웹 서버에 직접 접근하는 것이 아닌 리버스 프록시를 통해 접근하게 된다. 이렇게 되면 <strong>웹 서버의 IP를 노출시키지 않을 수 있기 때문에</strong> 웹 서버에 대한 1차적인 공격을 막을 수 있다. (Proxy 서버의 IP만 노출)</p>
</li>
<li><p><strong>캐싱</strong> : 리버스 프로시 또한 프록시 서버에 캐싱되어 있는 데이터를 사용하여 클라이언트에 대한 요청을 처리할 수 있게 된다. (<strong>포워드 프록시 캐싱과 비슷한 기능이기도 하며 프록시 서버의 본래 기능이다.</strong>)</p>
</li>
<li><p><strong>SSL Offloading</strong> : SSL Offloading은 SSL 암호화를 해제하는 기능이다. SSL 암호화는 서버와 클라이언트 간에 데이터를 주고 받을 때 암호화를 하는 것이다. SSL Offloading은 리버스 프록시 서버가 SSL 암호화를 해제하고 웹 서버에는 암호화가 해제된 데이터를 전달하는 것이다. 이렇게 되면 웹 서버는 SSL 암호화를 해제하는 부담이 없어지기 때문에 <strong>웹 서버의 부하를 줄일 수 있다.</strong></p>
</li>
</ul>
<br>

<h2 id="간단한-정리">간단한 정리</h2>
<p>Forward Proxy 서버는 클라이언트 앞에 놓여져 있는 반면 Reverse Proxy 서버는 웹 서버 앞에 놓여 있다는 차이점이 있다.</p>
<p>Forward Proxy는 <strong>내부망에서 외부망으로 접근할 때 사용</strong>되고 Reverse Proxy는 <strong>외부망에서 내부망으로 접근할 때 사용</strong>된다.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9D%EC%8B%9C_%EC%84%9C%EB%B2%84">https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9D%EC%8B%9C_%EC%84%9C%EB%B2%84</a></li>
<li><a href="https://ko.wikipedia.org/wiki/%EB%A6%AC%EB%B2%84%EC%8A%A4_%ED%94%84%EB%A1%9D%EC%8B%9C">https://ko.wikipedia.org/wiki/%EB%A6%AC%EB%B2%84%EC%8A%A4_%ED%94%84%EB%A1%9D%EC%8B%9C</a></li>
<li><a href="https://inpa.tistory.com/entry/NETWORK-%F0%9F%93%A1-Reverse-Proxy-Forward-Proxy-%EC%A0%95%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EC%A0%95%EB%A6%AC#%ED%94%84%EB%A1%9D%EC%8B%9C(Proxy)_%EB%9E%80">https://inpa.tistory.com/entry/NETWORK-%F0%9F%93%A1-Reverse-Proxy-Forward-Proxy-%EC%A0%95%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EC%A0%95%EB%A6%AC#%ED%94%84%EB%A1%9D%EC%8B%9C(Proxy)_%EB%9E%80</a>?</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>