<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>제이크서 개발 블로그</title>
    <link>https://jake-seo-dev.tistory.com/</link>
    <description>✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.</description>
    <language>ko</language>
    <pubDate>Sat, 30 May 2026 03:45:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Jake Seo</managingEditor>
    <image>
      <title>제이크서 개발 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/4911996/attach/7281c112a14947cc9ae8a1f91bfb0694</url>
      <link>https://jake-seo-dev.tistory.com</link>
    </image>
    <item>
      <title>컴퓨터공학에서 쓰이는 회귀(Regression) 라는 용어에 대해</title>
      <link>https://jake-seo-dev.tistory.com/755</link>
      <description>&lt;h1&gt;회귀(Regression) 라는 용어에 대해&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;회귀 (回歸): 한 바퀴 돌아 제자리로 돌아오거나 돌아감.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;컴퓨터공학쪽에서 회귀(Regression)라는 단어는 보통 두가지 맥락 속에서 사용된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Machine Learning&lt;/li&gt;
&lt;li&gt;Software Testing&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Machine Learning 맥락에서의 회귀&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이 맥락에서는 회귀(Regression) 라는 단어 자체가 가진 본래의 의미에서 관계의 근사와 예측의 의미로 확장된다.&lt;/li&gt;
&lt;li&gt;여기서 회귀는 주로 변수들 간의 관계를 수학적으로 근사하고 결과 값을 예측하는 방법을 의미한다.&lt;/li&gt;
&lt;li&gt;독립 변수와 종속 변수 간의 관계를 수학적으로 모델링하여 종속 변수 값을 예측하거나 설명하는 분석 기법을 가리키는 용어로 사용된다.&lt;/li&gt;
&lt;li&gt;집값 예측을 예로 들면, 입력 변수(ex. 지리적 요인, 면적)에 따라 결과 변수(ex. 가격)을 예측하는 경우, 이를 회귀 모델을 구성한다고 표현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;회귀가 예측과 가까운 뜻을 가지게 된데는 역사적 맥락이 있다고 한다. 현대 통계학의 선구자 중 한명인 프랜시스 골턴은 &amp;quot;부모의 키가 극단적일 때 자식의 키는 부모의 키보다 평균에 더 가까운 키로 회귀한다(regression toward the mean)&amp;quot;는 사실을 발견했고, 이후 사람들은 회귀를 평균으로 되돌아가는 현상뿐 아니라, &amp;quot;변수 간의 관계를 수학적으로 모델링하여 값을 예측하는 기법&amp;quot;을 말할 때 썼다고 한다.&lt;/p&gt;
&lt;h2&gt;Software Testing 맥락에서의 회귀&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;원래의 회귀(Regression)의 의미 즉, 되돌아감의 의미로 사용된다.&lt;/li&gt;
&lt;li&gt;소프트웨어가 버그가 있던 시절로 돌아가는지를 검증하는 테스트를 이야기한다.&lt;/li&gt;
&lt;li&gt;개발이 앞으로 나아가야 하는데, 수정 후 예전 버그가 다시 등장했다면, &amp;quot;뒤로 회귀했다(regressed)&amp;quot; 라고 표현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI 서비스 개발 일기</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/755</guid>
      <comments>https://jake-seo-dev.tistory.com/755#entry755comment</comments>
      <pubDate>Thu, 6 Nov 2025 19:33:06 +0900</pubDate>
    </item>
    <item>
      <title>#1, LLM 이란?</title>
      <link>https://jake-seo-dev.tistory.com/754</link>
      <description>&lt;h2&gt;LLM이란?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://jakeseo.blog/ce2933c7403b6c572c92d49c6365ad105552ba7eb225733399511df1e84acb20.png&quot; alt=&quot;picture 5&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;대규모 언어 모델 (Large Language Model)&lt;/li&gt;
&lt;li&gt;심층 신경망 모델 (Deep Neural Network)&lt;ul&gt;
&lt;li&gt;여러개의 층으로 복잡한 관계까지 학습 가능한 모델&lt;ul&gt;
&lt;li&gt;얕은 모델은 단순 규칙만 학습 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;입력 -&amp;gt; 특징 추출 -&amp;gt; 출력으로 변환하는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대용량의 텍스트 데이터로 훈련&lt;ul&gt;
&lt;li&gt;인터넷에 공개된 전체 텍스트의 상당 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사람의 텍스트를&lt;ul&gt;
&lt;li&gt;이해&lt;/li&gt;
&lt;li&gt;생성&lt;/li&gt;
&lt;li&gt;응답&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;대규모란 어떤 뜻인가?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;모델 파라미터의 크기와 대량의 훈련 데이터셋을 의미&lt;/li&gt;
&lt;li&gt;수백 또는 수천억 개의 파라미터&lt;/li&gt;
&lt;li&gt;모델 파라미터는 시퀀스의 다음 단어를 예측하도록 훈련하는 과정에서 조정되는 신경망의 가중치 (weight)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;학습에서 머신러닝과 딥러닝의 차이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;머신러닝: 사람이 직접 특성을 추출&lt;/li&gt;
&lt;li&gt;딥러닝: 모델이 자동으로 학습&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;머신러닝에서는 사람이 직접 특성을 설계(feature engineering) 해야 하고, 딥러닝에서는 신경망이 데이터로부터 유용한 특성을 자동으로 학습(representation learning) 한다.&lt;/p&gt;
&lt;h3&gt;오해하면 안되는 부분&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;모델이 특성을 자동으로 학습하긴 하지만, 사람이 여전히 모델 구조, 데이터 전처리, 하이퍼파라미터를 설계해야 함&lt;/li&gt;
&lt;li&gt;즉, &amp;quot;수동 특성(feature engineering)&amp;quot;은 줄었지만 &amp;quot;모델 설계(model engineering)&amp;quot;는 여전히 인간의 몫&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;LLM 애플리케이션의 쓸모&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;텍스트 분석과 생성에 관련된 거의 모든 작업을 자동화하는데 매우 유용함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;LLM 구축 단계&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://jakeseo.blog/b943aabb471d80ca9fda211c3255b6fe612c384160a34036da6571cb3aff3d0b.png&quot; alt=&quot;picture 6&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사전 훈련과 미세 튜닝으로 구성됨&lt;ul&gt;
&lt;li&gt;사전 훈련: LLM 모델을 대규모 데이터셋에서 훈련시켜 언어에 대한 폭넓은 이해를 쌓는 초기 단계&lt;/li&gt;
&lt;li&gt;미세 튜닝: 사전 훈련된 파운데이션 모델을 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1단계: 사전 훈련&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;원시 텍스트인 대규모 텍스트 말뭉치(corpus)에서 훈련&lt;ul&gt;
&lt;li&gt;&amp;#39;원시&amp;#39;란 데이터에 레이블 정보가 없는 일반적인 텍스트&lt;/li&gt;
&lt;li&gt;ML의 지도 학습에선 정답(레이블)이 필요하지만, LLM은 입력 데이터로부터 레이블을 생성하는 자기 지도 학습(self-supervised learning)을 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;첫번째 훈련 단계를 사전 훈련(pretraining) 이라 부름&lt;/li&gt;
&lt;li&gt;이렇게 훈련된 모델을 베이스 모델(base model) 혹은 파운데이션 모델(foundation model) 이라 부름&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사전 훈련만 끝나도, 입력 데이터를 기반으로 다양한 작업을 해결 가능.&lt;br&gt;ex. 텍스트 훈련, 제로-샷, 퓨-샷&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;2단계: 미세 튜닝&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;이번엔 원시 텍스트가 아닌 레이블이 있는 데이터를 이용해 LLM을 추가적으로 훈련 가능&lt;ul&gt;
&lt;li&gt;이것을 미세 튜닝이라고 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인기 있는 미세 튜닝의 종류&lt;ul&gt;
&lt;li&gt;지시 미세 튜닝 (instruction fine-tuning)&lt;ul&gt;
&lt;li&gt;지시와 정답 쌍 (ex. 번역하기 위한 쿼리와 번역된 텍스트)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;분류 미세 튜닝 (classification fine-tuning)&lt;ul&gt;
&lt;li&gt;ex. 스팸인 메일 / 스팸이 아닌 메일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;트랜스포머 구조&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://jakeseo.blog/184b4909feed286178e0374d51042059b37dbcf6b5829ee1bea3be6e12f4b545.png&quot; alt=&quot;picture 7&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;대부분의 최신 LLM은 &lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;Attention is All You Need&lt;/a&gt; 에서 소개된 심층 신경망 구조인 트랜스포머(transformer)를 기반으로 함&lt;/li&gt;
&lt;li&gt;트랜스포머는 크게 인코더(encoder)와 디코더(decoder)로 구성됨&lt;ul&gt;
&lt;li&gt;인코더(encoder): 입력 문장의 각 토큰을 임베딩(숫자 벡터)로 바꾼 뒤, 위치 정보를 더해서 여러 층(layer)을 통과시키며 문장 전체의 표현을 만듦&lt;/li&gt;
&lt;li&gt;디코더(decoder): 이전에 생성한 단어들을 입력으로 받아 다음 단어를 하나씩 예측. 단, 미래 단어는 못보게 막아둠&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;셀프 어텐션이란?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;인코더와 디코더는 셀프 어텐션(Self-Attention) 매커니즘으로 연결된 많은 층으로 구성되어 있음&lt;ul&gt;
&lt;li&gt;모델이 시퀀스에 있는 서로 다른 단어 또는 토큰(token)에 상대적인 가중치를 부여할 수 있음&lt;/li&gt;
&lt;li&gt;모델이 입력데이터에서 긴 범위에 걸친 의존성과 맥락관계를 포착하여 일관성 있고 맥락에 맞는 출력을 생성할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;셀프 어텐션의 동작 방식&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;각 단어가 모든 단어를 훑어보고, 지금은 이 단어에 얼마나 주목해야 하는지 가중치(중요도)를 계산&lt;/li&gt;
&lt;li&gt;가중치로 다른 단어들의 정보를 모아서(가중 합) 그 단어의 새로운 표현을 만듦&lt;/li&gt;
&lt;li&gt;장점&lt;ul&gt;
&lt;li&gt;모든 단어가 서로 직접 소통하므로, 멀리 떨어진 관계(장거리 의존성)을 바로 포착 가능&lt;/li&gt;
&lt;li&gt;계산을 병렬화 할 수 있어서 GPU 에서 훨씬 빠르게 학습 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Multi-Head Attention 이란?&lt;ul&gt;
&lt;li&gt;여러명의 팀원이 다양한 관점으로 동시에 문장을 검토하는 것&lt;/li&gt;
&lt;li&gt;문법, 의미, 관계 같은 다양한 관점으로 바라보고 그 결과를 모아서 더 풍부한 표현을 만듦&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;BERT와 GPT&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;BERT: 인코더 모듈을 기반으로 함&lt;ul&gt;
&lt;li&gt;주어진 문장에서 마스킹 되거나 가려진 단어를 예측하는 마스킹 단어 예측에 특화&lt;/li&gt;
&lt;li&gt;감성 분석과 문서 분류를 포함해 텍스트 분류 작업에 강점을 가짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GPT: 디코더 모듈을 기반으로 함&lt;ul&gt;
&lt;li&gt;텍스트 생성 특화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;트랜스포머가 나온 맥락&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;기존 RNN의 한계 극복&lt;ul&gt;
&lt;li&gt;병럴 처리: 한 문장을 순차적으로 훑지 않고, 한번에 모든 단어의 관계를 계산하므로 학습 속도가 빠름&lt;/li&gt;
&lt;li&gt;장거리 의존성 처리: 멀리 있는 단어끼리도 직접 연결되므로 복잡한 문장 구조를 더 잘 포착&lt;/li&gt;
&lt;li&gt;확장성 문제: 문장이 매우 길어지면 모든 단어 쌍을 계산하므로 비용이 커지는 단점(그러나 이후 연구들이 이를 개선)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;GPT 구조&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf&quot;&gt;Improving Language Understanding by Generative Pre-Training&lt;/a&gt; 에서 소개&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/2203.02155&quot;&gt;Training language models to follow instructiosn with human feedback&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;자기지도 학습&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://jakeseo.blog/cf848df37347b2c186374d650199bfd8e2c1ef2597abeae0f477d2a36752fb23.png&quot; alt=&quot;picture 8&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;방대한 텍스트 데이터에서 다음 단어를 모델이 예측해야 할 레이블로 사용 가능 (Autoregressive model)&lt;/li&gt;
&lt;li&gt;이전 출력을 입력으로 사용해 미래를 예측함&lt;/li&gt;
&lt;li&gt;이전 시퀀스를 기반으로 다음 단어를 선택하는 식으로 출력 텍스트의 일관성을 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;GPT 구현 단계&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://jakeseo.blog/22ec18cb68d45c81b3758801b551ce029ce62c8aeb9859eb972f772cb572c92c.png&quot; alt=&quot;picture 9&quot;&gt;&lt;/p&gt;</description>
      <category>LLM 개발 일기</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/754</guid>
      <comments>https://jake-seo-dev.tistory.com/754#entry754comment</comments>
      <pubDate>Sun, 19 Oct 2025 13:44:46 +0900</pubDate>
    </item>
    <item>
      <title>#2, FastAPI 에서 pytest 를 이용할 때 환경변수 문제</title>
      <link>https://jake-seo-dev.tistory.com/753</link>
      <description>&lt;h2&gt;FastAPI 에서 pytest 를 이용할 때 환경변수 문제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;비즈니스 로직 메서드를 테스트하는데, &lt;code&gt;.env&lt;/code&gt; 에 정의된 값이 필요하게 됨&lt;/li&gt;
&lt;li&gt;settings 를 그냥 사용했더니 캐싱된 settings 값을 사용해서 &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt; 와 같은 환경 구분이 되지 않았음&lt;ul&gt;
&lt;li&gt;Settings 인스턴스를 이미 전역에서 만들어버려서, 테스트 환경에서 &lt;code&gt;.env&lt;/code&gt; 를 갈아끼워도 반영이 안됐음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;config.py에 미리 설정된 기본값인 &lt;code&gt;prod&lt;/code&gt; 를 사용하기 때문에 &lt;code&gt;get_settings()&lt;/code&gt; 를 이용해야, 호출할 때마다 확인이 가능했음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# settings.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    environment: str = &amp;quot;prod&amp;quot;

def get_settings() -&amp;gt; Settings:
    return Settings()  # 캐시 안하고 매번 새로 생성&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pytest 동작방식의 이해&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;pytest 를 처음 사용해봐서 동작방식을 잘 몰랐는데, FastAPI 에 있는 메서드의 통합 테스트를 하려면 약간의 조건들이 필요했다.&lt;/li&gt;
&lt;li&gt;pytest 는 base 가 되는 디렉토리 내부에 들어있는 test 파일을 모두 검출해서 실행한다.&lt;/li&gt;
&lt;li&gt;이 과정에서 fixture marker 가 달린 함수 등도 알아서 발견하고 실행했다.&lt;/li&gt;
&lt;li&gt;pytest 는 언어 윗단에서 돌아가는 별도의 실행 프레임워크로 동작했음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://chatgpt.com/share/68f1c7ca-b8ec-8011-8c6e-776964607cf8&quot;&gt;https://chatgpt.com/share/68f1c7ca-b8ec-8011-8c6e-776964607cf8&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;pytest 를 이용해 FastAPI 메서드를 통합테스트 하는 방법&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;FastAPI 앱은 &lt;code&gt;main.app&lt;/code&gt;을 엔트리 포인트로 실행하며, 앱 초기화 코드 및 &lt;code&gt;lifespan&lt;/code&gt; 이벤트 안의 로직(DB 커넥션, 상태값 설정 등)이 자동으로 수행&lt;/li&gt;
&lt;li&gt;그러나 pytest에서 단순히 테스트 함수를 실행하는 경우, 이러한 lifespan 이벤트와 초기화 코드는 &lt;strong&gt;기본적으로 실행되지 않음&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;따라서 &lt;code&gt;app.state&lt;/code&gt;에 저장된 설정값이나 DB 커넥션 등 &lt;strong&gt;앱 전역 컨텍스트에 의존하는 코드&lt;/strong&gt;는 테스트 시 초기화되지 않아 에러가 발생할 수 있음&lt;/li&gt;
&lt;li&gt;이 문제는 pytest의 fixture나 &lt;code&gt;with TestClient(app)&lt;/code&gt; 같은 lifespan 관리 컨텍스트를 통해 해결할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;동기 테스트&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;from fastapi.testclient import TestClient
from main import app

def test_endpoint():
    with TestClient(app) as client:
        response = client.get(&amp;quot;/ping&amp;quot;)
        assert response.status_code == 200&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;비동기 테스트&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import pytest
from httpx import AsyncClient
from asgi_lifespan import LifespanManager
from main import app

@pytest.mark.asyncio
async def test_async_endpoint():
    async with LifespanManager(app):
        async with AsyncClient(app=app, base_url=&amp;quot;http://test&amp;quot;) as ac:
            response = await ac.get(&amp;quot;/ping&amp;quot;)
            assert response.status_code == 200&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;그러나 main.app 을 전부 로드했을 때의 문제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;너무 느리다&lt;/li&gt;
&lt;li&gt;특히 tracing 도구 때문에 더 느린듯&lt;ul&gt;
&lt;li&gt;회사에서 트레이싱 도구를 사용하는데, 앱 라이프사이클 초기에 로드하는 과정이 있음&lt;/li&gt;
&lt;li&gt;진짜 너무 느리니 반드시 필요할 때만 로드해야 함&lt;ul&gt;
&lt;li&gt;유닛테스트 초기화 과정에 10초씩 걸림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;메서드 유닛 테스트를 하기 위한 방법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;환경변수가 유닛테스트가 꼭 필요한 경우&lt;ul&gt;
&lt;li&gt;함수 호출 유닛 테스트에서는 필요한 환경변수는 &lt;code&gt;get_settings()&lt;/code&gt; 로 임시로 불러오기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비즈니스 로직 메서드를 다루는 서비스 레이어에선 메서드 설계 자체를 환경변수를 불러오기보단 의존성을 주입받거나 파라미터로 넘기는 식으로 구성&lt;ul&gt;
&lt;li&gt;이렇게 코드를 작성하면 parameterized 테스트나 여러 테스트를 깔끔하게 구성 가능함&lt;/li&gt;
&lt;li&gt;환경변수를 불러오고 넘겨주는 부분은 상위계층인 router 에서 작성하면 될 것 같음&lt;ul&gt;
&lt;li&gt;어차피 router 는 통합테스트의 영역이라 개별 유닛 테스트로 실행해볼 일이 많지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import pytest
from myapp.service import calculate_price

@pytest.mark.parametrize(&amp;quot;amount,tax,expected&amp;quot;, [
    (1000, 0.1, 1100),
    (1000, 0.2, 1200),
])
def test_calculate_price(amount, tax, expected):
    assert calculate_price(amount, tax) == expected&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;요약&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;문제점&lt;/th&gt;
&lt;th&gt;해결 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;환경 변수&lt;/td&gt;
&lt;td&gt;Settings 인스턴스 캐싱으로 환경 전환이 반영되지 않음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get_settings()&lt;/code&gt;로 매번 생성, &lt;code&gt;dependency_overrides&lt;/code&gt; 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifespan&lt;/td&gt;
&lt;td&gt;pytest에서 lifespan 이벤트가 자동 실행되지 않음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;with TestClient(app)&lt;/code&gt; 또는 &lt;code&gt;LifespanManager&lt;/code&gt; 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;전체 앱 로드로 테스트 느림&lt;/td&gt;
&lt;td&gt;유닛/통합 테스트를 구분하여 설계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유닛 테스트 설계&lt;/td&gt;
&lt;td&gt;환경 변수 직접 참조로 테스트 복잡&lt;/td&gt;
&lt;td&gt;파라미터/의존성 주입 방식으로 단순화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;lazy import 를 이용해 환경변수 문제를 해결하는 방법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;lazy import 를 통해서도 환경변수 문제를 해결할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;실행 순서 이해&lt;/h3&gt;
&lt;p&gt;pytest 가 실행될 때 순서는 아래와 같음&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1단계: 테스트 파일 임포트 (test_service.py)&lt;ul&gt;
&lt;li&gt;파일 상단의 import 문들이 실행됨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;from app.domains.vetter_conversation.service import VetterConversationService&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2단계: &lt;code&gt;conftest.py&lt;/code&gt; 의 &lt;code&gt;setup_environment&lt;/code&gt; fixture 실행&lt;ul&gt;
&lt;li&gt;&lt;code&gt;load_dotenv()&lt;/code&gt; 호출&lt;/li&gt;
&lt;li&gt;환경변수 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;3단계: 테스트 함수 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1단계에서 환경변수가 필요한데, 환경변수를 로드하는 2단계가 더 나중에 실행됨&lt;/p&gt;
&lt;h3&gt;실행순서 변경&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;파일 최상단의 import 를 클래스 내부의 fixture 로 이동&lt;/li&gt;
&lt;li&gt;import 를 먼저 실행하는 게 아니라, fixture 에서 환경변수가 로드된 이후에 import 를 실행하도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  # 파일 상단 (pytest가 파일을 로드하자마자 실행됨)
  from app.domains.vetter_conversation.service import VetterConversationService&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  class TestGetMessagesForFrontend:
      @pytest.fixture
      def service(self):
          &amp;quot;&amp;quot;&amp;quot;VetterConversationService 인스턴스&amp;quot;&amp;quot;&amp;quot;
          # 환경 변수 로드 후 임포트 (지연 임포트)
          from app.domains.vetter_conversation.service import VetterConversationService
          return VetterConversationService()&lt;/code&gt;&lt;/pre&gt;</description>
      <category>AI 서비스 개발 일기</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/753</guid>
      <comments>https://jake-seo-dev.tistory.com/753#entry753comment</comments>
      <pubDate>Sat, 18 Oct 2025 10:16:19 +0900</pubDate>
    </item>
    <item>
      <title>#1, 왜 쓰게 되었는가?</title>
      <link>https://jake-seo-dev.tistory.com/752</link>
      <description>&lt;h2&gt;왜 쓰게 되었는가?&lt;/h2&gt;
&lt;p&gt;2023년부터인가 ChatGPT 가 한창 뜨고 나서부터 블로그 글을 올리지 않았던 것 같다&lt;/p&gt;
&lt;p&gt;글을 올리지 않았던 이유는 내 블로그는 지식을 잘 정리해둔 사전같은 블로그를 표방했는데, 사전 형태의 지식은 ChatGPT가 훨씬 많이 알고 있었기 때문, 그래서 당시에 블로그에 글을 쓸 동기를 잃어버렸었다&lt;/p&gt;
&lt;h2&gt;GPT 이후에 내 글은 어떤 의미를 가질까?&lt;/h2&gt;
&lt;p&gt;근데 다시 블로그에 글을 쓰러 돌아오게 된 이유는 여러가지가 있다&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;글쓰기가 사실 생각의 도구였다는 것&lt;/li&gt;
&lt;li&gt;개인의 경험은 충분히 쓰여질 가치가 있다고 생각이 든 것&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이런 이유로 다시 블로그에 글을 쓸 것 같다&lt;/p&gt;
&lt;h2&gt;글의 목적은?&lt;/h2&gt;
&lt;p&gt;글의 목적은 글을 쓰게 된 이유처럼&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;글을 쓰며 여러가지 생각을 해보기 위해&lt;/li&gt;
&lt;li&gt;내가 겪었던 문제를 해결했다면 해결한 것으로 그냥 지나치지 않고 글을 쓰며, 장기기억으로 옮겨보기 위해&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;아마 이런 목적을 갖고 글을 쓸 것이다&lt;/p&gt;
&lt;h2&gt;글의 주제&lt;/h2&gt;
&lt;p&gt;글 주제는 아무래도 저번달부터 회사에서 AI스쿼드라는 것에 참여하여 AI서비스를 개발하고 있는데, 이 AI서비스를 개발하며 있었던 일들을 쓰게 될 것 같다&lt;/p&gt;
&lt;p&gt;아니면 AI서비스를 개발하다가 배운 곁가지 지식들?을 일기 형식으로 써나가 볼 것 같다&lt;/p&gt;
&lt;p&gt;이미 이전에도 재미있는 일들이 이미 꽤 많았는데 기록하지 못해서 안타깝다&lt;/p&gt;
&lt;p&gt;중요한건 지금부터니까 여러가지 써봐야지&lt;/p&gt;
&lt;h2&gt;광고에 대해&lt;/h2&gt;
&lt;p&gt;아 그리고 블로그 광고는 전부 떼버렸다&lt;/p&gt;
&lt;p&gt;내가 보기에도 광고 때문에 글 읽기가 불편하면 광고를 다는 것 자체가 잘못됐다는 생각이 많이 들어서...&lt;/p&gt;
&lt;p&gt;붙여도 나중에 붙일 것 같은데, 글 읽는데 방해되지 않는 위치에만 광고가 나오도록 세심히 주의를 기울여서 다시 붙일 예정이다.&lt;/p&gt;</description>
      <category>AI 서비스 개발 일기</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/752</guid>
      <comments>https://jake-seo-dev.tistory.com/752#entry752comment</comments>
      <pubDate>Fri, 17 Oct 2025 02:05:23 +0900</pubDate>
    </item>
    <item>
      <title>Route 53 의 호스팅 영역 (Hosted zone) 이란?</title>
      <link>https://jake-seo-dev.tistory.com/751</link>
      <description>&lt;h2&gt;ROUTE 53의 호스팅 영역 (Hosted zone) 이란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;현실의 주소록 같은 것&lt;/li&gt;
&lt;li&gt;웹사이트 이름과 웹사이트가 있는 IP 주소를 적어둠&lt;/li&gt;
&lt;li&gt;이름이 다르지만 실제론 DNS 서버의 역할과 흡사함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;역할&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;웹사이트 이름 (ex. &lt;a href=&quot;http://www.xxx.com&quot;&gt;www.xxx.com&lt;/a&gt;) 을 보고 실제 IP 주소로 연결해줌&lt;/li&gt;
&lt;li&gt;AWS 가 이 주소록을 대신 관리해주고 있는 것&lt;/li&gt;
&lt;li&gt;기본적으로 DNS 서버와 흡사하나 트래픽 관리, 헬스 체크, 지역 기반 라우팅 등 고급 기능을 더 제공하는 것만 다름&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/AWS</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/751</guid>
      <comments>https://jake-seo-dev.tistory.com/751#entry751comment</comments>
      <pubDate>Fri, 2 Aug 2024 18:36:00 +0900</pubDate>
    </item>
    <item>
      <title>ReferenceError: File is not defined 에러 해결 방법</title>
      <link>https://jake-seo-dev.tistory.com/750</link>
      <description>&lt;h2&gt;ReferenceError: File is not defined 에러&lt;/h2&gt;
&lt;p&gt;이 에러를 만났다면, 아마 Node.js 에서 File 객체를 이용한 코드를 작성했을 것이다&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (something instanceof File) {
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;해결 방법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 버전을 20.9 윗버전으로 업그레이드 하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sveltejs/kit/issues/10946&quot;&gt;참고 링크&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;노드 버전 업그레이드 없이 해결하는 방법&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@web-std/file&quot;&gt;web-std&lt;/a&gt; 의존성을 설치하면 된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>자바스크립트</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/750</guid>
      <comments>https://jake-seo-dev.tistory.com/750#entry750comment</comments>
      <pubDate>Wed, 1 May 2024 16:09:10 +0900</pubDate>
    </item>
    <item>
      <title>깃허브 소셜 로그인 구현하기</title>
      <link>https://jake-seo-dev.tistory.com/749</link>
      <description>&lt;h2&gt;GitHub 로그인, 회원가입 구현하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;깃헙 소셜 로그인은 OAuth 표준을 따른다.&lt;/li&gt;
&lt;li&gt;다른 소셜 로긴도 OAuth 를 따르니 비슷하게 구현이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이 포스팅에 쓰인 코드는 &lt;a href=&quot;https://nomadcoders.co/carrot-market&quot;&gt;노마드코더-캐럿마켓 클론코딩&lt;/a&gt; 에서 배운 코드를 참고했다.&lt;br&gt;단, 비슷하긴 하나, 완전히 동일하지 않고 몇몇 부분이 다르다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;GitHub 로그인, 회원가입 절차 살펴보기&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1712926719908_1712926721433.png&quot; alt=&quot;picture 5&quot;&gt;&lt;/p&gt;
&lt;h3&gt;시퀀스 다이어그램으로 살펴보기&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1713108759280_1713108760349.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant User as 사용자
    participant App as 애플리케이션
    participant Google as 구글 서버

    User-&amp;gt;&amp;gt;App: 구글 로그인 요청
    App-&amp;gt;&amp;gt;Google: 인증 코드 요청 (client_id, redirect_uri, scope)
    Google-&amp;gt;&amp;gt;User: 로그인 및 권한 승인 화면 표시
    User-&amp;gt;&amp;gt;Google: 로그인 및 권한 승인
    Google-&amp;gt;&amp;gt;App: 인증 코드 전달 (redirect_uri로)
    App-&amp;gt;&amp;gt;Google: 액세스 토큰 요청 (client_id, client_secret, 인증 코드)
    Google-&amp;gt;&amp;gt;App: 액세스 토큰 및 리프레시 토큰 전달
    App-&amp;gt;&amp;gt;Google: 사용자 정보 요청 (액세스 토큰 사용)
    Google-&amp;gt;&amp;gt;App: 사용자 정보 전달
    App-&amp;gt;&amp;gt;User: 로그인 완료 및 서비스 제공&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;깃헙 홈페이지에서 애플리케이션 등록하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/settings/applications/new&quot;&gt;홈페이지 링크&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1712676163944_1712676164778.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;여기서 등록한 앱은 추후에 &lt;a href=&quot;https://github.com/settings/developers&quot;&gt;Settings / Developer Settings&lt;/a&gt; 에서 확인할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;OAuth 의 흐름&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps&quot;&gt;홈페이지 링크&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사실 흐름 자체는 맨 위에 있는 &lt;code&gt;APP&lt;/code&gt;, &lt;code&gt;GITHUB&lt;/code&gt; 과 화살표가 있는 이미지로 이해하는 게 더 이해하기 쉽다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ol&gt;
&lt;li&gt;사용자가 애플리케이션에 접속함&lt;/li&gt;
&lt;li&gt;사용자를 GitHub 으로 이동시키고 인증을 요청함&lt;/li&gt;
&lt;li&gt;사용자가 GitHub 아이디를 입력하고 인증을 완료함&lt;/li&gt;
&lt;li&gt;사용자는 GitHub 에 의해 다시 해당 애플리케이션으로 리다이렉션됨&lt;/li&gt;
&lt;li&gt;이제 애플리케이션은 사용자의 액세스 토큰으로 GitHub API 에 엑세스함&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1712676541484_1712676542207.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;1단계: 사용자를 GitHub 으로 이동시키고 인증 요청하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;사용자를 깃허브의 인증 페이지로 보낸다.&lt;/li&gt;
&lt;li&gt;인증페이지로 보내면서, 내 앱의 &lt;code&gt;Client ID&lt;/code&gt;, &lt;code&gt;Scope&lt;/code&gt;, 기타 옵션들을 파라미터로 함께 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;GET https://github.com/login/oauth/authorize&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;라우터 구현하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;라우터를 구현하여 사용자를 GitHub 으로 이동시키고 인증을 요청할 것이다.&lt;ul&gt;
&lt;li&gt;라우터를 구현 안하고 그냥 파라미터와 함께 사용자를 넘겨버려도 어차피 &lt;code&gt;callback URL&lt;/code&gt; 로 돌아오게 되어있으니 사실 상관없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client_id&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;, &lt;code&gt;allow_signup&lt;/code&gt; 을 파라미터로 제공함과 동시에 사용자를 깃허브 인증페이지로 보낸다.&lt;/li&gt;
&lt;li&gt;인증이 완료되면 깃허브 애플리케이션을 생성할 때 세팅해둔 &lt;code&gt;callback URL&lt;/code&gt; 로 돌아오게 된다.&lt;ul&gt;
&lt;li&gt;나의 경우 아래 스크린샷에 보이듯 사용자는 &lt;code&gt;http://localhost:3000/github/complete&lt;/code&gt; 로 돌아오게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1712845916913_1712845917601.png&quot; alt=&quot;picture 2&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { NextResponse } from &amp;quot;next/server&amp;quot;;

export function GET() {
  const baseUrl =
    &amp;quot;https://github.com/login/oauth/authorize&amp;quot;;

  // 파라미터의 프로퍼티와 관련 정보 링크
  // https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
  const params = {
    client_id: process.env.GITHUB_CLIENT_ID!,
    // redirect_uri: Github 페이지에서 이미 세팅했기 때문에 필요 없음,
    scope: &amp;quot;read:user,user:email&amp;quot;, // 필요한 부분만 요청하기
    // 스코프 설명 관련 링크: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
    allow_signup: &amp;quot;true&amp;quot;, // GitHub 비회원이더라도 바로 가입해서 서비스를 이용할 수 있도록하기
  };

  const formattedParams = new URLSearchParams(
    params
  );

  const redirectUrl = `${baseUrl}?${formattedParams}`;

  return NextResponse.redirect(redirectUrl);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2단계: Code 파라미터를 AccessToken 으로 교환하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;사용자는 이전에 앱에 설정해두었던 callback URL 로 돌아오게 되는데, &lt;code&gt;code&lt;/code&gt; 라는 파라미터 값과 함께 돌아온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1712846131832_1712846132583.png&quot; alt=&quot;picture 3&quot;&gt;&lt;/p&gt;
&lt;h3&gt;액세스 토큰 받기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://github.com/login/oauth/access_token&lt;/code&gt; 에 POST 요청을 보내고 위에서 받은 CODE 를 파라미터 값으로 보내면 액세스 토큰을 얻을 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#step-3-app-polls-github-to-check-if-the-user-authorized-the-device&quot;&gt;깃허브 공식 문서&lt;/a&gt; 에 자세한 내용이 적혀있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1712924949377_1712924950417.png&quot; alt=&quot;picture 4&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { notFound } from &amp;quot;next/navigation&amp;quot;;
import {
  NextRequest,
  NextResponse,
} from &amp;quot;next/server&amp;quot;;
import axios from &amp;quot;axios&amp;quot;;

export async function GET(request: NextRequest) {
  const code =
    request.nextUrl.searchParams.get(&amp;quot;code&amp;quot;);

  if (!code) {
    return notFound();
  }

  const accessTokenUrl =
    &amp;quot;https://github.com/login/oauth/access_token&amp;quot;;

  const headers = {
    Accept: &amp;quot;application/json&amp;quot;,
  };

  const params = {
    client_id: process.env.GITHUB_CLIENT_ID!,
    client_secret:
      process.env.GITHUB_CLIENT_SECRET!,
    code,
  };

  const accessTokenResponse = await axios.post(
    accessTokenUrl,
    params,
    {
      headers,
    }
  );

  const accessTokenData =
    accessTokenResponse.data;

  if (accessTokenData.error) {
    return NextResponse.json(
      {
        message:
          &amp;quot;인증에 실패하였습니다. 다시 시도해주세요&amp;quot;,
      },
      {
        status: 400,
      }
    );
  }

  return NextResponse.json({ accessTokenData });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;깃허브 API 에 회원정보 요청하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;발급받은 액세스 토큰을 이용하여 GitHub API 에 회원정보를 요청할 수 있다.&lt;/li&gt;
&lt;li&gt;인증한 유저의 깃허브 로그인 아이디, 닉네임, 팔로워 등의 회원정보를 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Authorization: Bearer OAUTH-TOKEN
GET https://api.github.com/user&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;API 로 얻은 회원정보를 통해 로그인 혹은 회원가입 처리하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;받게된 회원정보를 기반으로 로그인 혹은 회원가입 처리를 하면 된다.&lt;ul&gt;
&lt;li&gt;해당 github ID 로 이미 가입된 계정이 있다면 로그인을 수행한다.&lt;/li&gt;
&lt;li&gt;해당 github ID 로 가입된 내역이 없다면, 회원가입 후 로그인을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단, 회원가입 시에는 반드시 정책을 잘 정해야 한다.&lt;ul&gt;
&lt;li&gt;깃허브 ID 를 그대로 유저 ID 와 동일하게 사용하면 문제가 된다.&lt;/li&gt;
&lt;li&gt;디비에 GITHUB_ID 와 같은 컬럼을 만들고 거기다가 아이디를 기록하고 유저 ID 는 깃허브를 이용해 가입했음을 알 수 있는 방식으로 하는 것이 좋다.&lt;ul&gt;
&lt;li&gt;나는 아래 소스코드에서 &lt;code&gt;@GITHUB_ID&lt;/code&gt; 와 같은 방식으로 아이디를 저장해두었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  notFound,
  redirect,
} from &amp;quot;next/navigation&amp;quot;;
import {
  NextRequest,
  NextResponse,
} from &amp;quot;next/server&amp;quot;;
import axios from &amp;quot;axios&amp;quot;;
import db from &amp;quot;@/lib/db&amp;quot;;
import { loginByUserId } from &amp;quot;@/lib/session&amp;quot;;

interface IAccessTokenData {
  access_token?: string;
  token_type?: string;
  scope?: string;
  error?: string;
  error_description?: string;
  error_url?: string;
}

export async function GET(request: NextRequest) {
  const code =
    request.nextUrl.searchParams.get(&amp;quot;code&amp;quot;);

  if (!code) {
    return notFound();
  }

  const accessTokenUrl =
    &amp;quot;https://github.com/login/oauth/access_token&amp;quot;;

  const headers = {
    Accept: &amp;quot;application/json&amp;quot;,
  };

  const params = {
    client_id: process.env.GITHUB_CLIENT_ID!,
    client_secret:
      process.env.GITHUB_CLIENT_SECRET!,
    code,
  };

  const accessTokenResponse = await axios.post(
    accessTokenUrl,
    params,
    {
      headers,
    }
  );

  const { data }: { data: IAccessTokenData } =
    accessTokenResponse;

  if (!data || data.error) {
    return NextResponse.json(
      {
        message:
          &amp;quot;인증에 실패하였습니다. 다시 시도해주세요&amp;quot;,
      },
      {
        status: 400,
      }
    );
  }

  // fetch 를 사용하는 경우, no-cache 옵션이 필요하다.
  const { data: gitUser } = await axios.get(
    &amp;quot;https://api.github.com/user&amp;quot;,
    {
      headers: {
        Authorization: `Bearer ${data.access_token}`,
      },
    }
  );

  const { id, avatar_url, login } = gitUser;

  // 가입된 회원이 있는지 찾기
  const user = await db.user.findUnique({
    where: {
      github_id: login,
    },
    select: {
      id: true,
    },
  });

  // 가입된 회원이 있다면 로그인 처리 후 메인페이지 이동
  if (user) {
    await loginByUserId(user.id);
    return redirect(&amp;quot;/&amp;quot;);
  }

  // 가입된 회원이 없다면 회원가입 처리 후 profile 페이지 이동
  const newUser = await db.user.create({
    data: {
      github_id: login,
      avatar: avatar_url,
      username: `@GITHUB_${id}`,
    },
  });

  await loginByUserId(newUser.id);
  return redirect(&amp;quot;/profile&amp;quot;);
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프레임워크/next.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/749</guid>
      <comments>https://jake-seo-dev.tistory.com/749#entry749comment</comments>
      <pubDate>Sat, 13 Apr 2024 21:30:18 +0900</pubDate>
    </item>
    <item>
      <title>아이언 세션 (Iron session) 이란 무엇인가? (쉬운 설명과 보안 관련 생각해볼점)</title>
      <link>https://jake-seo-dev.tistory.com/748</link>
      <description>&lt;h2&gt;iron session 이란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;안전하고 무상태인 쿠키 기반의 세션 라이브러리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;더 쉬운 말로 설명하면?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;사용자가 입력한 암호를 통해 쿠키값을 암호화하고 복호화해주는 것이다.&lt;ul&gt;
&lt;li&gt;쿠키는 원래 클라이언트에게 공개되고 클라이언트가 임의로 변경할 수 있는 값인데 이에 보안을 주기 위해서 암호화/복호화를 하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;작동 방식&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;세션 데이터가 서명되고 암호화된 쿠키에 저장된다.&lt;/li&gt;
&lt;li&gt;서버 코드에서 네트워크가 필요 없는 상태 비저장 방식으로 디코딩된다.&lt;ul&gt;
&lt;li&gt;Ruby on Rails 에서 쓰는 것과 동일한 기술이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;특징&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;서버리스 환경에 적합하다.&lt;ul&gt;
&lt;li&gt;세션 데이터를 쿠키에 저장하여 별도의 서버측 저장소 필요 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자 패스워드가 없어도 안전하게 암호화가 가능하다.&lt;/li&gt;
&lt;li&gt;미들웨어 하나로 쉽게 세션 기능을 추가할 수 있어서 러닝커브가 작다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;인증 프로세스 예시&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;사용자가 아이디/패스워드를 입력해 로그인한다.&lt;/li&gt;
&lt;li&gt;서버에서는 사용자의 아이디/패스워드를 조회하고 일치한다면 사용자의 아이디를 문자열로 반환한다. ex. &lt;code&gt;kakao123&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;사용자의 아이디 문자열이 브라우저로 전해진다.&lt;/li&gt;
&lt;li&gt;브라우저는 사용자의 아이디 문자열을 Iron Session 에 개발자가 정한 시크릿키를 넣어 암호화하고 쿠키에 저장한다.&lt;/li&gt;
&lt;li&gt;브라우저에는 암호화된 사용자 아이디 쿠키가 존재한다.&lt;/li&gt;
&lt;li&gt;사용자의 인증정보가 필요할 때마다 브라우저는 암호화된 쿠키를 서버로 보낸다.&lt;/li&gt;
&lt;li&gt;서버는 Iron Session 과 개발자가 정한 시크릿키를 이용하여 쿠키를 복호화한다. ex. &lt;code&gt;kakao123&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;사용자의 아이디가 &lt;code&gt;kakao123&lt;/code&gt; 이란 것을 알아낸 서버는 DB 에서 사용자 테이블에 &lt;code&gt;kakao123&lt;/code&gt; 를 조건으로 입력해 쿼리를 날린다.&lt;/li&gt;
&lt;li&gt;서버는 아이디를 가진 사용자의 이름, 이메일주소 등 필요한 정보를 가져올 수 있다.&lt;/li&gt;
&lt;li&gt;사용자는 서비스 이용을 끝마치고 로그아웃을 누른다.&lt;/li&gt;
&lt;li&gt;암호화된 세션은 브라우저에서 지워지고 로그아웃 처리가 되고 끝난다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;예시 코드 스니펫&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 세션 쿠키 생성
const session = await getIronSession(cookies(), {
  // 세션 쿠키의 이름은 karrot 이 된다.
  cookieName: &amp;quot;karrot&amp;quot;,
  // 쿠키의 복호화를 위해 필요한 문자열을 .env 에서 가져온다.
  password: process.env.COOKIE_PASSWORD!,
});

// user.id 에는 실제 계정의 id 값이 들어있다.
// session.id 에 user.id 를 할당하면
// 이는 iron-session 을 통해 암호화된 값으로 karrot 세션 쿠키에 들어갈 것이다.
//@ts-ignore
session.id = user.id;

// 세션 저장
await session.save();&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;코드 실행 결과 예시&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;세션용 쿠키가 생성된 것을 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1712075045555_1712075046195.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h2&gt;보안과 관련해 생각해볼만한 점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;아이언 세션의 쿠키가 해커에게 노출되면 어떻게 해야 할까?&lt;ul&gt;
&lt;li&gt;해커는 쿠키를 저장해두었다가 계속 악용할지도 모르는 일이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;조치 1: 아이피와 에이전트를 함께 암호화한다.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;이용자가 접근한 아이피와 에이전트를 함께 암호화하여 쿠키에 저장해둔다.&lt;/li&gt;
&lt;li&gt;해당 쿠키로 인증을 요청하면, 요청한 아이피와 에이전트를 함께 확인하여 해당 사용자가 맞는지 확인한다.&lt;/li&gt;
&lt;li&gt;만일, 다른 브라우저로 들어왔거나, 다른 아이피로 해당 쿠키를 이용한다면 새로 인증을 받아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;조치 2: 키 로테이션을 수행한다.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1시간 마다 시크릿 키를 바꿔준다.&lt;/li&gt;
&lt;li&gt;시크릿 키를 사용한지 30분쯤에 새로운 시크릿 키를 발급한다.&lt;/li&gt;
&lt;li&gt;30분간 이전 시크릿 키와 새 시크릿 키 두벌을 사용한다.&lt;/li&gt;
&lt;li&gt;30분의 시간동안 이전 시크릿 키를 사용하여 접근한 사용자에게 새 시크릿 키를 이용한 암호화 쿠키를 새로 발급해준다.&lt;/li&gt;
&lt;li&gt;30분이 지나 새로운 시크릿 키가 발급되면, 이전 시크릿 키를 이용해 만든 암호화 키를 사용하는 사용자는 더이상 해당 시크릿 키를 사용할 수 없게 된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>자바스크립트/라이브러리</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/748</guid>
      <comments>https://jake-seo-dev.tistory.com/748#entry748comment</comments>
      <pubDate>Wed, 3 Apr 2024 21:16:14 +0900</pubDate>
    </item>
    <item>
      <title>Node.js 프로젝트에서 Husky 와 commitlint 를 이용하여 커밋 메세지 포맷 제한하기</title>
      <link>https://jake-seo-dev.tistory.com/747</link>
      <description>&lt;p&gt;&lt;strong&gt;Husky 와 commitlint 를 이용하여 커밋 메세지 포맷 제한하기&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;프로세스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Husky 설치: Husky 를 설치한다.&lt;/li&gt;
&lt;li&gt;Husky 세팅: 필요한 커밋 전 명령어 및 커밋 검증 명령어 등을 세팅한다.&lt;/li&gt;
&lt;li&gt;테스트: 세팅이 잘 되었는지 테스트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Husky 설치&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://typicode.github.io/husky/get-started.html&quot;&gt;공식문서 중 get-started&lt;/a&gt; 에 나와있는대로 설치 후 초기화를 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;설치 명령어&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install --save-dev husky&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;초기화 명령어&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx husky init&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Husky 세팅&lt;/h2&gt;
&lt;h3&gt;pre-commit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pre-commit&lt;/code&gt; 에서는 커밋 전 실행할 명령어에 대해 세팅할 수 있다.&lt;/li&gt;
&lt;li&gt;기본으로는 &lt;code&gt;npm test&lt;/code&gt; 가 세팅되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm test&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;commit-msg&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;커밋 메세지를 검증할 룰을 세팅할 수 있다.&lt;/li&gt;
&lt;li&gt;난 udacity rules 를 따르지 않는 커밋 메세지를 모두 차단하고 싶었다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.husky&lt;/code&gt; 내부에 최초엔 &lt;code&gt;commit-msg&lt;/code&gt; 파일이 존재하지 않지만, 직접 생성하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;생성 이후 아래의 내용을 입력해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/sh
. &amp;quot;$(dirname &amp;quot;$0&amp;quot;)/_/husky.sh&amp;quot;

npx --no-install commitlint --edit&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/conventional-changelog/commitlint&quot;&gt;commitlint&lt;/a&gt; 의존성을 이용할 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @commitlint/config-conventional @commitlint/cli -D&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;프로젝트 루트에는 룰을 설정하는 &lt;code&gt;.commitlintrc.json&lt;/code&gt; 파일을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;extends&amp;quot;: [&amp;quot;@commitlint/config-conventional&amp;quot;],
  &amp;quot;rules&amp;quot;: {
    &amp;quot;type-enum&amp;quot;: [
      2,
      &amp;quot;always&amp;quot;,
      [
        &amp;quot;ci&amp;quot;,
        &amp;quot;chore&amp;quot;,
        &amp;quot;docs&amp;quot;,
        &amp;quot;ticket&amp;quot;,
        &amp;quot;feat&amp;quot;,
        &amp;quot;fix&amp;quot;,
        &amp;quot;perf&amp;quot;,
        &amp;quot;refactor&amp;quot;,
        &amp;quot;revert&amp;quot;,
        &amp;quot;style&amp;quot;
      ]
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;테스트&lt;/h2&gt;
&lt;h3&gt;컨벤션을 따르지 않는 커밋 메세지를 입력했을 때&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1708480470266_1708480472046.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;h3&gt;컨벤션을 따르는 커밋 메세지를 입력했을 때&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1708480574117_1708480575958.png&quot; alt=&quot;picture 2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;컨벤션&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional#type-enum&quot;&gt;깃 커밋 메세지 컨벤션&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;레퍼런스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://typicode.github.io/husky/how-to.html&quot;&gt;공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@sijokoothur/linting-git-commit-messages-using-husky-a-guide-to-cleaner-version-control-2ac37605433a&quot;&gt;허스키를 이용해 깃 커밋메세지 린팅하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>NODEJS</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/747</guid>
      <comments>https://jake-seo-dev.tistory.com/747#entry747comment</comments>
      <pubDate>Wed, 21 Feb 2024 11:42:40 +0900</pubDate>
    </item>
    <item>
      <title>도커 컴포즈 (Docker Compose) 란? 도커 컴포즈 사용법</title>
      <link>https://jake-seo-dev.tistory.com/746</link>
      <description>&lt;h2&gt;도커 컴포즈란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;보통 개발할 때 로컬 환경이나 개발 환경에서 가상의 DB를 띄우기 위해서 많이 사용한다.&lt;/li&gt;
&lt;li&gt;도커 컴포즈는 단일 호스트 환경에서 동작하고, 고가용성도 지원하지 않기 때문에 운영에서는 비슷한 역할을 하지만 고가용성을 지원하는 쿠버네티스를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;도커 컴포즈 이용하기 (Node.js 기준)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;프로젝트 메인 디렉토리에 &lt;code&gt;docker-compose.yaml&lt;/code&gt; 파일을 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704625784617_1704625785629.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h2&gt;도커 컴포즈 문법에 맞게 docker-compose.yaml 파일 작성&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/compose-file/&quot;&gt;Docker Compose 공식문서&lt;/a&gt; 에 작성법이 있긴 한데, 예제를 보거나 GPT 를 이용하는 것이 빠를 것 같다.&lt;/li&gt;
&lt;li&gt;Postgres Yaml 파일 작성 방식은 &lt;a href=&quot;https://hub.docker.com/_/postgres&quot;&gt;Postgres 도커 페이지&lt;/a&gt; 에서 참고할 수있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services: # 자동완성됨
  postgres: # 우리가 지어준 서비스의 이름
    image: postgres:15 # 도커 허브에서 정보를 가져와서 자동완성됨
    restart: always
    volumes: # 도커 이미지 안에 존재했던 정보를 실제 디렉토리와 매핑. 도커를 종료해도 데이터가 남도록 함.
      - ./postgres-data:/var/lib/postgresql/data # 콜론을 기준으로 왼쪽은 현재 위치, 오른쪽은 이미지 안에 존재하는 위치. 둘의 싱크를 맞춰주겠다.
    ports:
      # 로컬 환경과 이미지의 두 포트를 연동함
      - &amp;quot;5431:5432&amp;quot; # 로컬의 5431을 도커의 5432와 연결, 로컬에서 연결할 땐 포트 충돌 방지를 위해 5431 로 바인딩
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GPT 에 물어본 예제&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;아래와 같이 프롬프트를 작성하면, 기작성된 yaml 파일을 받을 수 있고, 여기서 모르는 뜻을 물어봐서 수정해보면 될 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;아래는 내 질문 예시다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;Give me a yaml file example of docker compose. I want to create latest version of postgresql container. set username and password to postgres. i want to use default port number.&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;답변&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Sure, I can help you with that. Below is an example of a Docker Compose YAML file for setting up a PostgreSQL container with the latest version. The username and password are set to &amp;quot;postgres&amp;quot;, and it uses the default PostgreSQL port, which is 5432.&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &amp;quot;3.8&amp;quot;
services:
  postgres:
    image: postgres:latest
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - &amp;quot;5432:5432&amp;quot;
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구성은 다음을 수행합니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;버전&lt;/strong&gt;: 작성 파일 형식의 버전을 지정합니다. 이 예에서는 버전 &amp;#39;3.8&amp;#39;이 사용됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;서비스&lt;/strong&gt;: 실행할 서비스를 정의합니다. 이 경우 PostgreSQL 서비스만 정의됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;이미지&lt;/strong&gt;: Docker Hub의 최신 버전의 PostgreSQL 이미지를 사용합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;환경&lt;/strong&gt;: 환경 변수를 설정합니다. &lt;code&gt;POSTGRES_USER&lt;/code&gt;와 &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;는 모두 &amp;#39;postgres&amp;#39;로 설정됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ports&lt;/strong&gt;: 컨테이너의 기본 PostgreSQL 포트(5432)를 호스트에 매핑합니다. 이렇게 하면 호스트 머신의 포트 5432를 사용하여 컨테이너 내부에서 실행 중인 PostgreSQL 서버에 연결할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;볼륨&lt;/strong&gt;: 데이터베이스 데이터를 유지할 볼륨을 마운트합니다. &amp;#39;postgres_data&amp;#39;는 볼륨의 이름이고, &lt;code&gt;/var/lib/postgresql/data&lt;/code&gt;는 컨테이너에서 PostgreSQL이 데이터를 저장하는 경로입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 구성은 &lt;code&gt;docker-compose.yml&lt;/code&gt;이라는 파일에 저장할 수 있습니다. 이 파일이 있는 디렉터리로 이동하여 &lt;code&gt;docker-compose up&lt;/code&gt; 명령을 실행하면 PostgreSQL 컨테이너가 시작됩니다. 그러면 PostgreSQL 이미지가 없는 경우 다운로드되고 지정된 설정으로 컨테이너가 시작됩니다.&lt;/p&gt;
&lt;h2&gt;도커 컴포즈 실행&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;위에 나온 것처럼&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose up&lt;/code&gt; 명령어를 통해 실행 가능하다.&lt;ul&gt;
&lt;li&gt;단, 데스크탑용 도커가 설치되어 있고 실행중인 상태에서 입력해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/도커</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/746</guid>
      <comments>https://jake-seo-dev.tistory.com/746#entry746comment</comments>
      <pubDate>Sun, 18 Feb 2024 22:37:39 +0900</pubDate>
    </item>
    <item>
      <title>브라우저 History API 를 활용하는 방법</title>
      <link>https://jake-seo-dev.tistory.com/745</link>
      <description>&lt;h2&gt;History API 로 작업하기 (History API 활용 방법)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;History API 를 이용하면, 브라우저의 세션 기록과 상호작용할 수 있다.&lt;/li&gt;
&lt;li&gt;세션 기록이란, 브라우저에서 사용자가 방문했던 페이지를 기록한 것을 말한다.&lt;/li&gt;
&lt;li&gt;일반 사용자는 앞으로 가기, 뒤로 가기 버튼을 통해 세션 기록을 이용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;History API 제공 메서드&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;페이지를 이동하는 메서드는 직관적으로 이해가 쉬우나, 기록된 페이지를 수정하는 메서드들은 잘 활용하기 위해 약간의 학습이 필요하다.&lt;/li&gt;
&lt;li&gt;이 API 의 주요 목적은 &lt;code&gt;fetch()&lt;/code&gt; 같은 메서드를 통해 업데이트 되는 SPA(Single Page Application)와 같은 웹사이트를 지원하기 위한 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;기록된 페이지로 이동&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;History.back()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;History.forward()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;History.go()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;기록된 페이지를 수정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;History.pushState()&lt;/code&gt;: 세션 기록에 새 항목을 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;History.replaceState()&lt;/code&gt;: 세션 기록 항목을 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SPA 와 세션 히스토리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MPA (Multi Page Application) 는 매번 페이지가 변화한다.&lt;/li&gt;
&lt;li&gt;SPA 는 현재 상태를 유지하면서 필요한 부분을 변화시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SPA 에서 문제가 되는 부분&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;페이지가 바뀌고 뒤로가기 버튼을 눌렀을 때, 이 전 렌더링 단계로 가는 것이 아니라 아예 최초의 페이지로 돌아간다.&lt;/li&gt;
&lt;li&gt;이를 해결하기 위해 &lt;code&gt;pushState()&lt;/code&gt;, &lt;code&gt;replaceState()&lt;/code&gt;, &lt;code&gt;popState&lt;/code&gt; 이벤트가 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;pushState() 사용 예제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;아래는 &lt;code&gt;pushState()&lt;/code&gt; 의 사용 예제인데, 3가지 인자를 갖는다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;json&lt;/code&gt;: 해당 렌더 시점에서 사용한 상태 값을 기억해둔다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;: 레거시 사이트와 이전 버전의 호환성을 위해 필요하며, 항상 빈 문자열이어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;creature&lt;/code&gt;: 엔트리의 URL 로 사용된다. 브라우저의 URL 바에 표기되며, 페이지에서 발생하는 모든 HTTP 요청에서 Referer(&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer&lt;/a&gt;) 헤더 값으로 사용된다.&lt;ul&gt;
&lt;li&gt;이 값은 페이지와 무조건 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Same-origin_policy&quot;&gt;same-origin&lt;/a&gt; 이어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;document.addEventListener(&amp;quot;click&amp;quot;, async (event) =&amp;gt; {
  const creature = event.target.getAttribute(&amp;quot;data-creature&amp;quot;);
  if (creature) {
    event.preventDefault();
    try {
      const response = await fetch(`creatures/${creature}.json`);
      const json = await response.json();
      displayContent(json);
      // Add a new entry to the history.
      // This simulates loading a new page.
      history.pushState(json, &amp;quot;&amp;quot;, creature);
    } catch (err) {
      console.error(err);
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;popState() 사용 예제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;popstate&lt;/code&gt; 이벤트는 뒤로 가기 혹은 앞으로 가기를 수행했을 때 일어난다.&lt;/li&gt;
&lt;li&gt;이전 &lt;code&gt;pushState()&lt;/code&gt; 에서 기록해둔 state 를 통해 다시 이전의 화면을 복원할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;document&lt;/code&gt;&lt;/strong&gt; 가 아닌 &lt;strong&gt;&lt;code&gt;window&lt;/code&gt;&lt;/strong&gt; 객체에 걸어야 함을 잘 살피자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Handle forward/back buttons
window.addEventListener(&amp;quot;popstate&amp;quot;, (event) =&amp;gt; {
  // If a state has been provided, we have a &amp;quot;simulated&amp;quot; page
  // and we update the current page.
  if (event.state) {
    // Simulate the loading of the previous page
    displayContent(event.state);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;replaceState() 사용 예제&lt;/h2&gt;
&lt;p&gt;시나리오&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;초기에 페이지 로드가 발생한다.&lt;/li&gt;
&lt;li&gt;사용자는 SPA 내부의 링크를 클릭하여 다른 페이지로 이동한다. &lt;code&gt;pushState()&lt;/code&gt; 를 사용하여 히스토리 항목을 추가한다.&lt;/li&gt;
&lt;li&gt;뒤로가기 버튼을 누른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 때, 초기 상태로 돌아가고 싶지만, 동일한 문서 탐색이라 초기 페이지로 돌아가지 않는다.&lt;br&gt;이 상황을 &lt;code&gt;replaceState()&lt;/code&gt; 를 통해 해결할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Create state on page load and replace the current history with it
const image = document.querySelector(&amp;quot;#photo&amp;quot;);
const initialState = {
  description: document.querySelector(&amp;quot;#description&amp;quot;).textContent,
  image: {
    src: image.getAttribute(&amp;quot;src&amp;quot;),
    alt: image.getAttribute(&amp;quot;alt&amp;quot;),
  },
  name: &amp;quot;Home&amp;quot;,
};
history.replaceState(initialState, &amp;quot;&amp;quot;, document.location.href);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;레퍼런스&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API&lt;/a&gt;&lt;/p&gt;</description>
      <category>프론트엔드/Web API</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/745</guid>
      <comments>https://jake-seo-dev.tistory.com/745#entry745comment</comments>
      <pubDate>Thu, 15 Feb 2024 11:06:55 +0900</pubDate>
    </item>
    <item>
      <title>History API 란? (간단 정리)</title>
      <link>https://jake-seo-dev.tistory.com/744</link>
      <description>&lt;h2&gt;History API 란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;History&lt;/code&gt; 인터페이스는 브라우저의 세션 기록, 현재 페이지를 불러온 탭 혹은 프레임의 방문기록을 조작할 수 있는 방법을 제공한다.&lt;ul&gt;
&lt;li&gt;브라우저에서 뒤로가기와 같은 액션을 수행했을 때 방문하는 페이지를 조작하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;history&lt;/code&gt; 는 싱글톤 인스턴스로 글로벌에 1개만 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;인스턴스 프로퍼티&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;History&lt;/code&gt; 인터페이스는 어떤 속성도 상속하지 않는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;length&lt;/code&gt;: 세션 기록에 몇개의 페이지가 있는지의 길이이다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration&quot;&gt;scrollRestoration&lt;/a&gt;: 해당 세션으로 갔을 때 스크롤까지 복원할지를 설정한다. &lt;code&gt;auto&lt;/code&gt; 혹은 &lt;code&gt;manual&lt;/code&gt; 이 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state&lt;/code&gt;: 히스토리 스택의 맨 위에 있는 상태를 나타내는 값을 반환한다. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History/pushState&quot;&gt;pushState()&lt;/a&gt; 혹은 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState&quot;&gt;replaceState()&lt;/a&gt; 를 사용할 때까지는 null 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;인스턴스 메서드&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;back()&lt;/code&gt;: 뒤로 가기. &lt;code&gt;history.go(-1)&lt;/code&gt; 과 같다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forward()&lt;/code&gt;: 앞으로 가기. &lt;code&gt;history.go(1)&lt;/code&gt; 과 같다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go()&lt;/code&gt;: 현재 페이지의 상대적 위치로 가기. 이전은 &lt;code&gt;-1&lt;/code&gt;, 앞으로는 &lt;code&gt;1&lt;/code&gt;, 범위를 넘어서면 무시된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pushState()&lt;/code&gt;: 데이터를 세션 기록 스택에 푸시한다. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API&quot;&gt;Working with the History API&lt;/a&gt; 문서 참조&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replaceState()&lt;/code&gt;: 히스토리 스택의 가장 최근 항목을 지정된 데이터로 업데이트한다. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API&quot;&gt;Working with the History API&lt;/a&gt; 문서 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;추가로 봐야 할 문서&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jake-seo-dev.tistory.com/745&quot;&gt;브라우저 히스토리 API 를 활용하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;공식문서&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/History&quot;&gt;MDN History&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>자바스크립트/개념</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/744</guid>
      <comments>https://jake-seo-dev.tistory.com/744#entry744comment</comments>
      <pubDate>Wed, 14 Feb 2024 11:42:23 +0900</pubDate>
    </item>
    <item>
      <title>브라우저 라우팅과 해시 라우팅의 차이 (Browser Routing 과 Hash Routing 의 차이)</title>
      <link>https://jake-seo-dev.tistory.com/743</link>
      <description>&lt;h2&gt;브라우저 라우터 (Browser Router)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;모던 웹브라우저에서 제공하는 &lt;a href=&quot;https://jake-seo-dev.tistory.com/744&quot;&gt;히스토리 API&lt;/a&gt; 를 활용하여 히스토리 스택을 조작한다.&lt;ul&gt;
&lt;li&gt;히스토리 스택에는 브라우저 탭이나 창에서 방문한 URL 이 들어있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;해시 라우팅과 비교하면 해시(#)가 없는 깔끔한 URL 을 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;깔끔한 URL: URL 이 깔끔해진다.&lt;/li&gt;
&lt;li&gt;SEO 친화적: URL 이 깔끔하여 SEO 에 친화적이다.&lt;/li&gt;
&lt;li&gt;SSR 지원: 서버가 미리 렌더링된 초기 페이지를 반환할 수 있어서 SEO 를 용이하게 하고 처음 방문하는 방문자의 로딩 시간을 개선한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;서버 구성: 서버에서 모든 요청을 index.html 로 라우팅되도록 해야 SPA 라우팅이 올바른 뷰를 렌더링 할 수 있다.&lt;/li&gt;
&lt;li&gt;브라우저 호환성: 일부 구형 브라우저는 히스토리 API 를 지원하지 않을 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;해시 라우터 (Hash Router)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;해시(#)를 이용하여 경로를 만든다.&lt;/li&gt;
&lt;li&gt;브라우저의 해시 변경 이벤트에 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;단순성: 서버측 라우팅 구성이 필요 없다.&lt;/li&gt;
&lt;li&gt;호환성: 히스토리 API 없이도 구형 브라우저를 포함한 거의 모든 브라우저에서 작동한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;URL에 #이 붙음: UX 및 SEO 관점에서 덜 매력적이다.&lt;/li&gt;
&lt;li&gt;클라이언트에 제약받음: 모든 라우팅 로직이 클라이언트 측에서 처리되므로 초기 로드 최적화나 SSR 에 적합하지 않을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;결정에 중요한 포인트&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;UX 중요성&lt;ul&gt;
&lt;li&gt;URL 의 깔끔함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;구형 브라우저 호환성&lt;/li&gt;
&lt;li&gt;배포의 단순성&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프론트엔드</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/743</guid>
      <comments>https://jake-seo-dev.tistory.com/743#entry743comment</comments>
      <pubDate>Wed, 14 Feb 2024 11:41:28 +0900</pubDate>
    </item>
    <item>
      <title>MVC 패턴이란? 짧게 설명하기 / (Model, View, Controller 패턴이란?)</title>
      <link>https://jake-seo-dev.tistory.com/742</link>
      <description>&lt;h2&gt;MVC 패턴이란? (Model, View, Controller 패턴이란?)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;화면 개발에서 많이 쓰이는 디자인 패턴이다.&lt;/li&gt;
&lt;li&gt;MVC 는 각각 &lt;code&gt;Model&lt;/code&gt;, &lt;code&gt;View&lt;/code&gt;, &lt;code&gt;Controller&lt;/code&gt; 를 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;모델 (Model) 이란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;앱의 데이터를 저장하고 관리하는 역할&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;데이터를 관리하는 역할이다.&lt;/li&gt;
&lt;li&gt;API, DB, 로컬 저장소 등에 있는 데이터를 가져와 앱에서 사용할 수 있는 모양으로 만든다.&lt;/li&gt;
&lt;li&gt;데이터를 추가하거나 수정하거나 삭제할 수 있는 방법도 알고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;뷰 (View) 란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사용자에게 보여지는 화면을 구성하고 업데이트하는 역할&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;화면을 관리하는 역할이다.&lt;/li&gt;
&lt;li&gt;HTML, CSS, JS 를 포함한다.&lt;/li&gt;
&lt;li&gt;데이터를 받아 화면에 UI 를 렌더링한다.&lt;/li&gt;
&lt;li&gt;이벤트가 발생했음을 감지하고 컨트롤러에 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;컨트롤러 (Controller) 란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;모델과 뷰 사이의 중개자 역할 및 사용자 상호작용을 처리&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;뷰에서 전달한 이벤트에 따라 모델이 애플리케이션 상태를 관리할 수 있게 해준다.&lt;/li&gt;
&lt;li&gt;모델이 가진 데이터를 뷰에게 전달해서 화면을 만든다.&lt;/li&gt;
&lt;li&gt;모델과 뷰는 독립적으로 존재한다.&lt;/li&gt;
&lt;li&gt;컨트롤러는 독립적인 모델과 뷰를 연결하고 움직이는 주체다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;다이어그램으로 살펴보기&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1709035683327_1709035684495.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h2&gt;코드로 살펴보기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Model
const model = {
  data: [],
  getData() {
    // API 호출 또는 로컬 스토리지에서 데이터 로드
  },
  saveData() {
    // API 호출 또는 로컬 스토리지에 데이터 저장
  },
};

// View
const view = {
  render() {
    // 모델의 데이터를 기반으로 HTML 템플릿 렌더링
  },
  update() {
    // 모델 변경 시 화면 동적 업데이트
  },
};

// Controller
const controller = {
  init() {
    // 모델 데이터 로드
    model.getData();
    // 뷰와 모델 연결
    view.render(model.data);
  },
  onUserAction(action) {
    // 사용자 입력 처리
    switch (action) {
      case &amp;quot;ADD_TODO&amp;quot;:
        // 모델에 데이터 추가 요청
        model.addData(data);
        // 뷰 업데이트
        view.update();
        break;
      // ...
    }
  },
};

// 앱 시작
controller.init();&lt;/code&gt;&lt;/pre&gt;</description>
      <category>디자인 패턴</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/742</guid>
      <comments>https://jake-seo-dev.tistory.com/742#entry742comment</comments>
      <pubDate>Mon, 12 Feb 2024 01:25:29 +0900</pubDate>
    </item>
    <item>
      <title>Nest.js 웹소켓 (WebSockets) 이란? + 사용법</title>
      <link>https://jake-seo-dev.tistory.com/741</link>
      <description>&lt;h2&gt;웹소켓 게이트웨이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;웹소켓 게이트웨이란, 웹소켓에서 REST Controller 와 비슷한 역할을 해주는 것이라고 보면 된다.&lt;ul&gt;
&lt;li&gt;단, 진입점이 단순히 엔드포인트가 아니라 이벤트(namespace)이며, 뒤에 추가 path 가 있는 게 아니라 추가 event 가 더 있는 것으로 보면 된다.&lt;/li&gt;
&lt;li&gt;웹소켓에서 엔드포인트는 단순히 처음 connection 을 맺을 때 한번만 쓰인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@WebSocketGateway()&lt;/code&gt; 데코레이터를 단 클래스이다.&lt;/li&gt;
&lt;li&gt;게이트웨이는 플랫폼에 구애받지 않으므로 어댑터가 생성되면 모든 WebSockets 라이브러리와 호환된다.&lt;ul&gt;
&lt;li&gt;기본 지원은 &lt;a href=&quot;https://github.com/socketio/socket.io&quot;&gt;socket.io&lt;/a&gt; 와 &lt;a href=&quot;https://github.com/websockets/ws&quot;&gt;ws&lt;/a&gt; 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1707504870155_1707504871038.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h2&gt;의존성 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ npm i --save @nestjs/websockets @nestjs/platform-socket.io&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;기본 사용법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;기본으로 HTTP 와 동일하게 &lt;code&gt;80&lt;/code&gt; 포트를 사용하고, 게이트웨이에 사용될 네임스페이스를 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;게이트웨이는 기존 모듈에서 참조되기 전까지는 인스턴스화되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@WebSocketGateway(80, { namespace: &amp;#39;events&amp;#39; })&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;2번째 인자엔 네임스페이스 뿐만 아니라 다양한 옵션을 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@WebSocketGateway(81, { transports: [&amp;#39;websocket&amp;#39;] })&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;실제로 특정 메세지를 구독하는 예시이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@SubscribeMessage&lt;/code&gt; 와 &lt;code&gt;@MessageBody&lt;/code&gt; 모두 &lt;code&gt;@nestjs/websockets&lt;/code&gt; 패키지에 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// events.gateway.ts
@SubscribeMessage(&amp;#39;events&amp;#39;)
handleEvent(@MessageBody() data: string): string {
  return data;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;모듈에 게이트웨이 등록 방법&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@Module({
  providers: [EventsGateway],
})
export class EventsModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;데코레이터 사용하지 않기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;데코레이터는 사용하지 않지만, 같은 결과를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;그러나 데코레이터를 사용하지 않으면, 소켓 인스턴스를 모킹해야 하기 때문에 테스트에서 좋지 않다.&lt;/li&gt;
&lt;li&gt;연결된 소켓 인스턴스에 접근하려면 &lt;code&gt;@ConnectedSocket()&lt;/code&gt; 데코레이터를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@SubscribeMessage(&amp;#39;events&amp;#39;)
handleEvent(@MessageBody(&amp;#39;id&amp;#39;) id: number): number {
  // id === messageBody.id
  return id;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@SubscribeMessage(&amp;#39;events&amp;#39;)
handleEvent(client: Socket, data: string): string {
  return data;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;비동기 응답&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;async&lt;/code&gt; 키워드 혹은 &lt;code&gt;Observable&lt;/code&gt; 을 활용해 비동기 응답이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@SubscribeMessage(&amp;#39;events&amp;#39;)
onEvent(@MessageBody() data: unknown): Observable&amp;lt;WsResponse&amp;lt;number&amp;gt;&amp;gt; {
  const event = &amp;#39;events&amp;#39;;
  const response = [1, 2, 3];

  return from(response).pipe(
    map(data =&amp;gt; ({ event, data })),
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;라이프사이클 훅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OnGatewayInit&lt;/code&gt;: &lt;code&gt;afterInit()&lt;/code&gt; 메서드를 강제로 구현. 라이브러리별 서버 인스턴스를 인수로 받는다. (필요하다면 스프레드)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OnGatewayConnection&lt;/code&gt;: &lt;code&gt;handleConnection()&lt;/code&gt; 메서드를 강제로 구현. 라이브러리에 특화된 클라이언트 소켓 인스턴스를 인수로 받는다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OnGatewayDisconnect&lt;/code&gt;: &lt;code&gt;handleDisconnect()&lt;/code&gt; 메서드를 강제로 구현. 라이브러리에 특화된 클라이언트 소켓 인스턴스를 인수로 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Broadcasting&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;나를 제외한 모두에게 메세지를 보내는 것을 Broadcasting 이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;socket.to(&amp;quot;room 이름&amp;quot;).emit(&amp;quot;receive_message&amp;quot;, &amp;quot;메세지&amp;quot;);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프레임워크/Nest.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/741</guid>
      <comments>https://jake-seo-dev.tistory.com/741#entry741comment</comments>
      <pubDate>Sun, 11 Feb 2024 13:46:26 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트의 Socket.io 라이브러리란? (웹소켓 통신 라이브러리)</title>
      <link>https://jake-seo-dev.tistory.com/740</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1707505329122_1707505329938.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WebSocket 프로토콜을 사용해서 만든 양방향 통신이다.&lt;/li&gt;
&lt;li&gt;웹에서 일반적으로 사용하는 HTTP 는 기본적으로 클라이언트가 서버에게 요청하는 관점이라면, WebSocket 은 서로서로 요청하는 양방향이다.&lt;/li&gt;
&lt;li&gt;low-latency (낮은 지연), bi-directional (양방향), event-based (이벤트 기반) 로 클라이언트와 서버가 통신할 수 있게 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;기본 통신 예제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;emit()&lt;/code&gt; 은 메세지를 보낸다. (연결된 모든 소켓에게 보낸다.)&lt;ul&gt;
&lt;li&gt;&lt;code&gt;boradcast.emit()&lt;/code&gt; 이란 것도 있는데, 이는 나 빼고 모두에게 메세지를 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;on()&lt;/code&gt; 은 메세지를 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;서버 코드&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { Server } from &amp;quot;socket.io&amp;quot;;
// 웹소켓 서버를 생성
const io = new Server(3000);

io.on(&amp;quot;connection&amp;quot;, (socket) =&amp;gt; {
  // 클라이언트로 메세지 보내기
  socket.emit(&amp;quot;hello from server&amp;quot;, &amp;quot;hello i am a server&amp;quot;);

  // 클라이언트로부터 메세지 받기
  socket.on(&amp;quot;hello from client&amp;quot;, (...args) =&amp;gt; {
    console.log(&amp;quot;클라이언트로부터 메세지 받음&amp;quot;, args); // args -&amp;gt; hello i am a client
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;클라이언트 코드&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { io } from &amp;quot;socket.io-client&amp;quot;;
// 웹소켓 서버에 연결
const socket = io(&amp;quot;ws://localhost:3000&amp;quot;); // -&amp;gt; 클라이언트가 일단 연결하면 서로 메세지를 보내거나 받을 수 있다.

// 서버로 메세지 보내기
socket.emit(&amp;quot;hello from client&amp;quot;, &amp;quot;hello i am a client&amp;quot;);

// 서버로부터 메세지 받기
socket.on(&amp;quot;hello from server&amp;quot;, (...args) =&amp;gt; {
  console.log(&amp;quot;서버로부터 메세지 받음&amp;quot;, args); // args -&amp;gt; hello i am a server
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Acknowledgement (접수 통지, 승인)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;메세지를 받고 서버쪽으로 응답을 전달할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;socket.emit(&amp;quot;channel&amp;quot;, &amp;quot;I am fine thank you&amp;quot;, (response) =&amp;gt; {
  // &amp;quot;and you?&amp;quot;
  console.log(response);
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;socket.on(&amp;quot;channel&amp;quot;, (message, callback) =&amp;gt; {
  console.log(message);
  callback(&amp;quot;and you?&amp;quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Namespace 와 Room&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Namespace&lt;/code&gt; 로 1차로 격리된 공간을 만든다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Room&lt;/code&gt; 으로 2차로 격리된 공간을 만든다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Namespace1&lt;/code&gt; 의 &lt;code&gt;Room1&lt;/code&gt; 과 &lt;code&gt;Namespace2&lt;/code&gt; 의 &lt;code&gt;Room1&lt;/code&gt; 은 완전히 다른 공간이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1707508224566_1707508225591.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;h3&gt;서버에서 namespace 와 room 만들기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;io.of()&lt;/code&gt; 메서드를 통해 namespace 를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;socket.join()&lt;/code&gt; 메서드를 통해 연결된 소켓을 room 에 넣을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const io = require(&amp;quot;socket.io&amp;quot;)(3000); // Initialize Socket.IO on port 3000

// Create namespace1
const namespace1 = io.of(&amp;quot;/namespace1&amp;quot;);
namespace1.on(&amp;quot;connection&amp;quot;, (socket) =&amp;gt; {
  console.log(&amp;quot;A user connected to namespace1&amp;quot;);

  // Join room1
  socket.join(&amp;quot;room1&amp;quot;);
  // You can emit to room1 in namespace1 like this:
  namespace1
    .to(&amp;quot;room1&amp;quot;)
    .emit(&amp;quot;hello&amp;quot;, &amp;quot;Hello to everyone in room1 of namespace1&amp;quot;);

  // Join room2
  socket.join(&amp;quot;room2&amp;quot;);
  // Similarly, emit to room2 in namespace1
  namespace1
    .to(&amp;quot;room2&amp;quot;)
    .emit(&amp;quot;hello&amp;quot;, &amp;quot;Hello to everyone in room2 of namespace1&amp;quot;);

  socket.on(&amp;quot;disconnect&amp;quot;, () =&amp;gt; {
    console.log(&amp;quot;A user disconnected from namespace1&amp;quot;);
  });
});

// Create namespace2
const namespace2 = io.of(&amp;quot;/namespace2&amp;quot;);
namespace2.on(&amp;quot;connection&amp;quot;, (socket) =&amp;gt; {
  console.log(&amp;quot;A user connected to namespace2&amp;quot;);

  // Join room1
  socket.join(&amp;quot;room1&amp;quot;);
  // Emit to room1 in namespace2
  namespace2
    .to(&amp;quot;room1&amp;quot;)
    .emit(&amp;quot;hello&amp;quot;, &amp;quot;Hello to everyone in room1 of namespace2&amp;quot;);

  // Join room2
  socket.join(&amp;quot;room2&amp;quot;);
  // Emit to room2 in namespace2
  namespace2
    .to(&amp;quot;room2&amp;quot;)
    .emit(&amp;quot;hello&amp;quot;, &amp;quot;Hello to everyone in room2 of namespace2&amp;quot;);

  socket.on(&amp;quot;disconnect&amp;quot;, () =&amp;gt; {
    console.log(&amp;quot;A user disconnected from namespace2&amp;quot;);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;클라이언트에서 각 room 에 연결하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;실제로 클라이언트를 어떤 room 에 보낼지는 서버에서 조작한다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;socket.join()&lt;/code&gt; 을 실행해 특정 room 에 넣는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;클라이언트에서는 room 을 정할 수 있는 기능이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Connect to namespace1
const namespace1 = io.connect(&amp;quot;http://localhost:3000/namespace1&amp;quot;);

// Once connected, join room1
namespace1.on(&amp;quot;connect&amp;quot;, () =&amp;gt; {
  console.log(&amp;quot;Connected to namespace1&amp;quot;);
  // namespace1.emit(&amp;quot;join&amp;quot;, &amp;quot;room1&amp;quot;); // Assuming the server handles a &amp;#39;join&amp;#39; event to add the socket to a room

  // Listen for messages from the server
  namespace1.on(&amp;quot;hello&amp;quot;, (msg) =&amp;gt; {
    console.log(&amp;quot;Message from room1 in namespace1:&amp;quot;, msg);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Connect to namespace2
const namespace2 = io.connect(&amp;quot;http://localhost:3000/namespace2&amp;quot;);

// Once connected, join room2
namespace2.on(&amp;quot;connect&amp;quot;, () =&amp;gt; {
  console.log(&amp;quot;Connected to namespace2&amp;quot;);
  // namespace2.emit(&amp;quot;join&amp;quot;, &amp;quot;room2&amp;quot;); // Similarly, assuming the server handles a &amp;#39;join&amp;#39; event

  // Listen for messages from the server
  namespace2.on(&amp;quot;hello&amp;quot;, (msg) =&amp;gt; {
    console.log(&amp;quot;Message from room2 in namespace2:&amp;quot;, msg);
  });
});&lt;/code&gt;&lt;/pre&gt;</description>
      <category>자바스크립트/라이브러리</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/740</guid>
      <comments>https://jake-seo-dev.tistory.com/740#entry740comment</comments>
      <pubDate>Sat, 10 Feb 2024 05:03:40 +0900</pubDate>
    </item>
    <item>
      <title>Nest.js 미들웨어 (Middleware) 란?</title>
      <link>https://jake-seo-dev.tistory.com/739</link>
      <description>&lt;h2&gt;미들웨어 (Middleware) 란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;라우트 핸들러 앞에 호출되는 함수&lt;/li&gt;
&lt;li&gt;요청-응답에서 요청 및 응답 객체와 &lt;code&gt;next()&lt;/code&gt; 미들웨어 함수에 접근 가능&lt;/li&gt;
&lt;li&gt;다음 미들웨어 함수는 일반적으로 &lt;code&gt;next&lt;/code&gt; 라는 변수로 표기됨&lt;/li&gt;
&lt;li&gt;기본적으로 &lt;a href=&quot;https://expressjs.com/en/guide/using-middleware.html&quot;&gt;express&lt;/a&gt; 의 미들웨어와 동일&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;express 의 미들웨어 소개&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;어떤 코드든 실행한다.&lt;/li&gt;
&lt;li&gt;요청-응답 객체를 변환한다.&lt;/li&gt;
&lt;li&gt;요청-응답 사이클을 종료한다.&lt;/li&gt;
&lt;li&gt;스택에서 다음 미들웨어 함수를 호출한다.&lt;/li&gt;
&lt;li&gt;다음 미들웨어 함수에 제어권을 넘기기 위해 &lt;code&gt;next()&lt;/code&gt; 를 호출해야 한다. 그렇지 않으면 요청이 중단된 채로 유지된다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;next()&lt;/code&gt; 를 꼭 불러줘야 한다는 점이 &lt;code&gt;Interceptor&lt;/code&gt; 나 &lt;code&gt;Guard&lt;/code&gt; 와 같은 것과 구분된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;미들웨어는 요청에 제공하는 기능 중 올인원 솔루션 같은 느낌이다.&lt;br&gt;제약받지 않고 많은 기능을 구현할 수 있다.&lt;br&gt;&lt;code&gt;Guard&lt;/code&gt; 나 &lt;code&gt;Interceptor&lt;/code&gt; 에서 했던 것들을 &lt;code&gt;Middleware&lt;/code&gt; 를 통해 구현할 수는 있지만, 목적에 맞게 사용하는 것이 좋음&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;미들웨어가 가장 많이 쓰이는 순간은?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;자바스크립트에 미리 구현되어 있는 CORS 모듈이나 helmet 모듈은 미들웨어로 구현된 경우가 많다.&lt;/li&gt;
&lt;li&gt;미리 구현된 범용 미들웨어들을 이용하는 경우가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;다이어그램&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1707493357397_1707493359021.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h2&gt;라이프사이클&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1707493418730_1707493419769.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;구현&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;함수 혹은 &lt;code&gt;@Injectable()&lt;/code&gt; 데코레이터가 있는 클래스에서 사용자 정의 Nest 미들웨어를 구현한다.&lt;ul&gt;
&lt;li&gt;함수라면, 특별한 요구사항이 없다.&lt;/li&gt;
&lt;li&gt;클래스라면, &lt;code&gt;NestMiddleware&lt;/code&gt; 인터페이스를 구현해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Injectable, NestMiddleware } from &amp;quot;@nestjs/common&amp;quot;;
import { Request, Response, NextFunction } from &amp;quot;express&amp;quot;;

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(&amp;quot;Request...&amp;quot;);
    next();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;의존성 주입&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Nest 미들웨어는 의존성 주입을 완벽하게 지원한다.&lt;/li&gt;
&lt;li&gt;동일한 모듈 내에서 사용할 수 있는 종속성을 주입할 수 있다.&lt;/li&gt;
&lt;li&gt;다른 것들과 마찬가지로 &lt;code&gt;constructor&lt;/code&gt; 를 통해 주입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;모듈에 미들웨어 적용하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Module()&lt;/code&gt; 데코레이터에는 미들웨어를 위한 자리가 없다.&lt;/li&gt;
&lt;li&gt;모듈 클래스의 &lt;code&gt;configure()&lt;/code&gt; 메서드를 통해 세팅이 가능하다.&lt;/li&gt;
&lt;li&gt;미들웨어를 포함하는 모듈은 &lt;code&gt;NestModule&lt;/code&gt; 을 반드시 구현해야 한다.&lt;/li&gt;
&lt;li&gt;아래는 &lt;code&gt;AppModule&lt;/code&gt; 수준에서 &lt;code&gt;LoggerMiddleware&lt;/code&gt; 를 세팅하는 코드다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;cats&lt;/code&gt; 경로에 로거 미들웨어를 적용하는 예제 코드&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Module, NestModule, MiddlewareConsumer } from &amp;quot;@nestjs/common&amp;quot;;
import { LoggerMiddleware } from &amp;quot;./common/middleware/logger.middleware&amp;quot;;
import { CatsModule } from &amp;quot;./cats/cats.module&amp;quot;;

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(&amp;quot;cats&amp;quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;cats&lt;/code&gt; 경로로 가는 &lt;code&gt;GET&lt;/code&gt; 메서드에만 미들웨어를 적용하는 예제 코드&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  Module,
  NestModule,
  RequestMethod,
  MiddlewareConsumer,
} from &amp;quot;@nestjs/common&amp;quot;;
import { LoggerMiddleware } from &amp;quot;./common/middleware/logger.middleware&amp;quot;;
import { CatsModule } from &amp;quot;./cats/cats.module&amp;quot;;

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: &amp;quot;cats&amp;quot;, method: RequestMethod.GET });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;참고&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;위에선 &lt;code&gt;configure()&lt;/code&gt; 메서드가 동기 함수지만 &lt;code&gt;async&lt;/code&gt; 를 통해 비동기로도 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MiddlewareConsumer&lt;/code&gt; 를 통해 미들웨어를 커스터마이즈 하고 싶다면 글로벌 미들웨어를 꺼야 한다. &lt;code&gt;NestFactory.create()&lt;/code&gt; 를 통해 앱을 생성할 때 &lt;code&gt;bodyParser&lt;/code&gt; 에 &lt;code&gt;false&lt;/code&gt; 플래그를 주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;라우트 와일드카드 (Route wildcards)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;아래처럼 경로를 패턴으로 매칭시켜도 잘 동작한다.&lt;/li&gt;
&lt;li&gt;정규표현식 쓰듯이 쓰면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;forRoutes({ path: &amp;quot;ab*cd&amp;quot;, method: RequestMethod.ALL });&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Middleware consumer (미들웨어 컨슈머, 소비자)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;헬퍼 클래스다.&lt;/li&gt;
&lt;li&gt;미들웨어를 관리하기 위한 몇가지 내장 메서드를 제공한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forRoutes()&lt;/code&gt; 는 복수의 문자열을 받을 수도 있고, &lt;code&gt;RouteInfo&lt;/code&gt; 오브젝트를 받을 수도 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apply()&lt;/code&gt; 도 여러 개의 미들웨어를 받을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Module, NestModule, MiddlewareConsumer } from &amp;quot;@nestjs/common&amp;quot;;
import { LoggerMiddleware } from &amp;quot;./common/middleware/logger.middleware&amp;quot;;
import { CatsModule } from &amp;quot;./cats/cats.module&amp;quot;;
import { CatsController } from &amp;quot;./cats/cats.controller&amp;quot;;

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(CatsController);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Excludes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;특정 경로가 미들웨어에 적용되지 않도록 제외할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pillarjs/path-to-regexp#parameters&quot;&gt;path-to-regexp&lt;/a&gt; 규칙을 통해 정규표현식으로 제외할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: &amp;quot;cats&amp;quot;, method: RequestMethod.GET },
    { path: &amp;quot;cats&amp;quot;, method: RequestMethod.POST },
    &amp;quot;cats/(.*)&amp;quot;
  )
  .forRoutes(CatsController);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Functional Middleware (함수형 미들웨어)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;간단한 미들웨어를 작성할 때 굳이 클래스로 작성할 필요 없이 함수형 미들웨어를 이용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Request, Response, NextFunction } from &amp;quot;express&amp;quot;;

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 적용
consumer.apply(logger).forRoutes(CatsController);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;미들웨어 여러개 적용하기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;글로벌 미들웨어&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main.ts&lt;/code&gt; 에서 &lt;code&gt;app.use()&lt;/code&gt; 를 통해 적용하면 글로벌로 적용된다.&lt;/li&gt;
&lt;li&gt;글로벌 미들웨어에서는 DI 컨테이너에 접근할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;레퍼런스&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.nestjs.com/middleware&quot;&gt;https://docs.nestjs.com/middleware&lt;/a&gt;&lt;/p&gt;</description>
      <category>프레임워크/Nest.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/739</guid>
      <comments>https://jake-seo-dev.tistory.com/739#entry739comment</comments>
      <pubDate>Sat, 10 Feb 2024 01:13:01 +0900</pubDate>
    </item>
    <item>
      <title>Nest.js 인터셉터 (Interceptor) 란?</title>
      <link>https://jake-seo-dev.tistory.com/738</link>
      <description>&lt;h2&gt;인터셉터&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Nest.js 의 인터셉터 클래스는&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Injectable&lt;/code&gt; 데코레이터를 추가해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NestInterceptor&lt;/code&gt; 인터페이스를 상속해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1707313548049_1707313549858.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;p&gt;인터셉터는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Aspect-oriented_programming&quot;&gt;Aspect Oriented Programming&lt;/a&gt; 기술에서 영감을 받았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메서드 실행 전 후에 추가 로직 바인딩&lt;/li&gt;
&lt;li&gt;함수 결과를 변환&lt;/li&gt;
&lt;li&gt;함수 예외를 변환&lt;/li&gt;
&lt;li&gt;함수 동작을 확장&lt;/li&gt;
&lt;li&gt;조건부 함수 완전 재정의 (ex. 캐싱 목적)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;기초&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;인터셉터는 &lt;code&gt;intercept()&lt;/code&gt; 메서드를 구현해야 한다.&lt;/li&gt;
&lt;li&gt;메서드엔 두개의 인자가 있다.&lt;ul&gt;
&lt;li&gt;1번째 인자, &lt;a href=&quot;https://docs.nestjs.com/fundamentals/execution-context&quot;&gt;ExecutionContext&lt;/a&gt;: 요청의 타입 정보나 호출된 컨트롤러 메서드 정보 등을 가져올 수 있다.&lt;ul&gt;
&lt;li&gt;ex) http 에서 헤더, 쿠키 등의 정보 그리고 컨트롤러 메서드 정보&lt;/li&gt;
&lt;li&gt;이는 &lt;a href=&quot;https://docs.nestjs.com/fundamentals/execution-context#argumentshost-class&quot;&gt;ArgumentsHost&lt;/a&gt; 를 상속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2번째 인자, &lt;code&gt;Call handler&lt;/code&gt;: &lt;code&gt;handle()&lt;/code&gt; 메서드를 이용해 본래 호출된 메서드를 매핑할 때 사용한다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Observable&lt;/code&gt; 을 반환하여 RxJS 연산자로 응답을 추가 조작할 수 있다.&lt;/li&gt;
&lt;li&gt;객체지향에서는 이 &lt;code&gt;handle()&lt;/code&gt; 메서드를 포인트컷이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;예제1: 로깅&lt;/h2&gt;
&lt;h3&gt;인터셉터 작성&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from &amp;quot;@nestjs/common&amp;quot;;
import { Observable } from &amp;quot;rxjs&amp;quot;;
import { tap } from &amp;quot;rxjs/operators&amp;quot;;

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;any&amp;gt; {
    console.log(&amp;quot;Before...&amp;quot;);

    const now = Date.now();
    return next
      .handle()
      .pipe(tap(() =&amp;gt; console.log(`After... ${Date.now() - now}ms`)));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;컨트롤러에 적용&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@UseInterceptors(LoggingInterceptor)
export class CatsController {}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;결과&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Before...
After... 1ms&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;글로벌 적용&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app.module.ts
import { Module } from &amp;quot;@nestjs/common&amp;quot;;
import { APP_INTERCEPTOR } from &amp;quot;@nestjs/core&amp;quot;;

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;예제2: 응답 매핑 (변환)&lt;/h2&gt;
&lt;h3&gt;데이터를 객체 안에 넣기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from &amp;quot;@nestjs/common&amp;quot;;
import { Observable } from &amp;quot;rxjs&amp;quot;;
import { map } from &amp;quot;rxjs/operators&amp;quot;;

export interface Response&amp;lt;T&amp;gt; {
  data: T;
}

@Injectable()
export class TransformInterceptor&amp;lt;T&amp;gt;
  implements NestInterceptor&amp;lt;T, Response&amp;lt;T&amp;gt;&amp;gt;
{
  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable&amp;lt;Response&amp;lt;T&amp;gt;&amp;gt; {
    return next.handle().pipe(map((data) =&amp;gt; ({ data })));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;data&amp;quot;: []
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;null 인 경우 공백 출력하기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from &amp;quot;@nestjs/common&amp;quot;;
import { Observable } from &amp;quot;rxjs&amp;quot;;
import { map } from &amp;quot;rxjs/operators&amp;quot;;

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;any&amp;gt; {
    return next.handle().pipe(map((value) =&amp;gt; (value === null ? &amp;quot;&amp;quot; : value)));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;예제3: 예외 재정의&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  BadGatewayException,
  CallHandler,
} from &amp;quot;@nestjs/common&amp;quot;;
import { Observable, throwError } from &amp;quot;rxjs&amp;quot;;
import { catchError } from &amp;quot;rxjs/operators&amp;quot;;

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;any&amp;gt; {
    return next
      .handle()
      .pipe(catchError((err) =&amp;gt; throwError(() =&amp;gt; new BadGatewayException())));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;예제 4: 스트림 오버라이딩&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;호출하는 메서드를 보고 응답이 이미 연산된 결과면 캐시된 결과를 내보낼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from &amp;quot;@nestjs/common&amp;quot;;
import { Observable, of } from &amp;quot;rxjs&amp;quot;;

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;any&amp;gt; {
    const isCached = true;
    if (isCached) {
      return of([]);
    }
    return next.handle();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;예제 5: 타임아웃 구성하기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  RequestTimeoutException,
} from &amp;quot;@nestjs/common&amp;quot;;
import { Observable, throwError, TimeoutError } from &amp;quot;rxjs&amp;quot;;
import { catchError, timeout } from &amp;quot;rxjs/operators&amp;quot;;

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;any&amp;gt; {
    return next.handle().pipe(
      timeout(5000),
      catchError((err) =&amp;gt; {
        if (err instanceof TimeoutError) {
          return throwError(() =&amp;gt; new RequestTimeoutException());
        }
        return throwError(() =&amp;gt; err);
      })
    );
  }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프레임워크/Nest.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/738</guid>
      <comments>https://jake-seo-dev.tistory.com/738#entry738comment</comments>
      <pubDate>Wed, 7 Feb 2024 23:10:33 +0900</pubDate>
    </item>
    <item>
      <title>도커 컨테이너의 라이프사이클 (Docker container lifecycle)</title>
      <link>https://jake-seo-dev.tistory.com/737</link>
      <description>&lt;h2&gt;컨테이너의 라이프사이클&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1707149344117_1707149345021.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker run&lt;/code&gt; 은 실제론 &lt;code&gt;docker create&lt;/code&gt; 와 &lt;code&gt;docker start&lt;/code&gt; 가 하나로 합쳐진 명령이다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker create&lt;/code&gt; 까지는 격리된 공간만 만들어놓는다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker start&lt;/code&gt; 단계에서 실제로 메모리와 CPU 를 사용하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker pause&lt;/code&gt; 명령어는 컨테이너를 일시정지 시키는데, 이 때는 컨테이너의 상태를 기억해야 해서 메모리는 사용하지만 CPU 는 사용하지 않는 상태가 된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker stop&lt;/code&gt; 명령어는 컨테이너에서 실행 중이던 프로세스를 완전히 중지시킨 것이고, CPU 도 메모리도 사용하지 않는다.&lt;ul&gt;
&lt;li&gt;다시 시작하면 컨테이너가 처음부터 다시 시작된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker rm&lt;/code&gt; 은 모든 단계에서 실행 가능하다.&lt;ul&gt;
&lt;li&gt;단, 실행 중인 컨테이너를 삭제하려면 &lt;code&gt;docker rm -f&lt;/code&gt; 명령어를 이용해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/도커</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/737</guid>
      <comments>https://jake-seo-dev.tistory.com/737#entry737comment</comments>
      <pubDate>Tue, 6 Feb 2024 01:24:41 +0900</pubDate>
    </item>
    <item>
      <title>2가지 파일 업로드 방식 알아보기 (feat. pre-uploading (프리 업로드, 먼저 업로드) 방식과 일반 업로드 방식)</title>
      <link>https://jake-seo-dev.tistory.com/736</link>
      <description>&lt;h2&gt;업로드 방식&lt;/h2&gt;
&lt;h3&gt;일반적인 파일 업로드 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;제목, 내용, 파일을 한 번에 받아 업로드한다.&lt;/li&gt;
&lt;li&gt;업로드 버튼을 눌른 이후에 업로드가 시작되었을 때, 사용자는 앱이 약간 느리다고 느낄 수 있다.&lt;/li&gt;
&lt;li&gt;파일이 여러개가 되거나 첨부파일의 용량이 커질수록 이러한 불편을 느낄 확률이 커진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;먼저 업로드 하는 방식 (Pre-uploading, Background uploading)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;첨부파일을 선택하는 순간에 이미 업로드는 시작된다.&lt;/li&gt;
&lt;li&gt;업로드된 파일은 임시 폴더에 잠시 저장된다.&lt;/li&gt;
&lt;li&gt;첨부파일 업로드 뒤에 게시글 작성 시 첨부파일 경로만 추가해준다.&lt;/li&gt;
&lt;li&gt;S3 presigned url 을 사용하면 많이 사용되는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;장단점 비교&lt;/h2&gt;
&lt;h3&gt;일반적인 파일 업로드 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;체감 속도: 길다&lt;/li&gt;
&lt;li&gt;서버 과부하: 한 번의 요청만 받게 되므로 상대적으로 유리하다.&lt;/li&gt;
&lt;li&gt;엔드포인트 관리: 파일 업로드 관련 세팅을 매번 해주어야 한다.&lt;/li&gt;
&lt;li&gt;파일 관리: 잉여 파일이 생길 가능성이 적다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;먼저 업로드 하는 방식 (Pre-uploading, Background uploading)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;체감 속도: 이미지가 먼저 골라지고 글을 쓰는 방식일 때, 글을 쓰는 동안 이미지가 업로드 되어 체감 속도가 매우 빠르다.&lt;/li&gt;
&lt;li&gt;서버 과부하: 이미지 선택 시마다 업로드가 진행되어 부하가 더 크다. 이미지를 올리려다가 마는 경우, 서버의 리소스만 사용한다.&lt;/li&gt;
&lt;li&gt;엔드포인트 관리: 공통 이미지 업로드 엔드포인트로 모든 이미지 업로드를 한번에 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;파일 관리: 선택한 이미지를 삭제하거나 변경했을 때 쓸모없는 파일이 생길 수 있어서 주기적으로 삭제해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;추가적으로 올리려는 파일에 에러가 있는 경우, 먼저 업로드하는 방식에서는 오류를 미리 알려줄 수 있기 때문에 유리하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;업로드 시에 네트워크 부하에 대처하기도 용이하다. 파일의 사이즈를 미리 알고 부하를 예측한 뒤에 리소스를 효율적으로 배분할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>잡다한 백엔드 지식</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/736</guid>
      <comments>https://jake-seo-dev.tistory.com/736#entry736comment</comments>
      <pubDate>Mon, 5 Feb 2024 20:47:20 +0900</pubDate>
    </item>
    <item>
      <title>Cgroup, 씨그룹, c그룹 이란 무엇인가?</title>
      <link>https://jake-seo-dev.tistory.com/735</link>
      <description>&lt;h2&gt;cgroup 이란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;제어 그룹 (control group) 의 줄임말이다.&lt;/li&gt;
&lt;li&gt;프로세스의 리소스 사용량 (CPU, 메모리, 디스크 I/O, 네트워크) 등을 제한하고 격리하는 Linux 커널 기능이다.&lt;/li&gt;
&lt;li&gt;Docker 와 같이 컨테이너화된 환경에서 필요한 격리 및 리소스 제어를 제공하는데 필수적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;핵심 개념&lt;/h2&gt;
&lt;h3&gt;리소스 제한&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;프로세스의 사용자 정의 그룹 간에 CPU 시간, 시스템 메모리, 네트워크 대역폭과 같은 리소스를 할당할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;우선순위 지정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;서로 다른 리소스 그룹의 우선순위를 정의할 수 있다.&lt;/li&gt;
&lt;li&gt;부하가 높을 때 어떤 애플리케이션이 더 많이 리소스를 확보할지 설정이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;감사 (Audit)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;여러 작업 그룹에서 사용하는 리소스에 대한 자세한 계정을 제공한다.&lt;/li&gt;
&lt;li&gt;모니터링 및 감사 목적에 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;제어&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;제어 프로세스 그룹을 고정하고 다시 시작하고 할당된 리소스를 동적으로 변경할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;작동 방식&lt;/h2&gt;
&lt;h3&gt;계층 구조와 그룹&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;cgroup 시스템은 계층적으로 구성된다. 그룹은 각각 다른 리소스 제한을 가진 서브 그룹을 갖는다. 각 그룹은 프로세스의 집합이며 각 프로세스는 하나의 cgroup 의 멤버만 가능하다. (여러 cgroup 의 멤버가 될 수 없다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;컨트롤러&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CPU, 메모리, blkio(Block I/O) 등의 하위 시스템 혹은 컨트롤러를 사용한다.&lt;/li&gt;
&lt;li&gt;각 컨트롤러는 한가지 유형의 시스템 리소스를 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;메모리 컨트롤러는 메모리 사용량을 제한하고, CPU 컨트롤러는 CPU 사용량을 제한하는 등의 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;cgroupfs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;cgroup 은 groupfs 라는 의사 파일 시스템을 통해 사용자 공간에 노출된다.&lt;/li&gt;
&lt;li&gt;사용자는 cgroupfs 의 파일을 읽거나 쓰면서 cgroup 과 상호작용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Docker 및 컨테이너에서 사용되는 방식&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;각 컨테이너는 격리된 cgroup 에서 실행된다.&lt;/li&gt;
&lt;li&gt;컨테이너는 할당된 리소스에만 접근이 가능하다.&lt;/li&gt;
&lt;li&gt;한 컨테이너가 리소스를 고갈시켜도 다른 컨테이너에 영향을 미치지 않는다.&lt;/li&gt;
&lt;li&gt;시스템 관리자는 리소스 할당 및 우선순위를 세부적으로 제한 가능하다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>리눅스와 유닉스</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/735</guid>
      <comments>https://jake-seo-dev.tistory.com/735#entry735comment</comments>
      <pubDate>Sun, 4 Feb 2024 14:40:31 +0900</pubDate>
    </item>
    <item>
      <title>컨테이너 런타임 (Container Runtime) 이란 무엇인가? 그리고 runc</title>
      <link>https://jake-seo-dev.tistory.com/734</link>
      <description>&lt;h2&gt;컨테이너 런타임이란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;도커에서 직접 커널과 통신하며 실제로 격리된 공간을 만드는 역할을 한다.&lt;/li&gt;
&lt;li&gt;호스트 운영체제에서 컨테이너를 실행하고 컨테이너 이미지를 관리함&lt;/li&gt;
&lt;li&gt;실행, 일시 중지, 중지와 네임스페이스, cgroup, 네트워크 인터페이스 관리와 같은 저수준 작업을 처리&lt;/li&gt;
&lt;li&gt;컨테이너의 수명 주기를 담당&lt;/li&gt;
&lt;li&gt;도커는 처음에 LXC 라는 런타임을 사용했지만 나중에 libcontainer 라는 자체 런타임을 개발하고 나중에 runc 로 발전&lt;ul&gt;
&lt;li&gt;점진적으로 가볍고 유연하고 안정적이 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;runc 런타임이란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OCI 스펙에 따라 컨테이너를 생성하고 실행하기 위한 CLI 도구임.&lt;/li&gt;
&lt;li&gt;Docker 및 기타 컨테이너 플랫폼이 내부적으로 사용하는 런타임이고 컨테이너 실행을 위한 사실상의 표준.&lt;/li&gt;
&lt;li&gt;단순성, 이식성, OCI 런타임 사양 준수에 중점을 두고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;runc 가 하는 일들&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;컨테이너 구성&lt;/li&gt;
&lt;li&gt;네임스페이스 및 C그룹: PID, 네트워크 IPC 등 네임스페이스와 C그룹을 설정. 네임스페이스는 컨테이너가 자신의 리소스 집합만 볼 수 있도록 격리를 제공하고, C 그룹은 컨테이너가 사용할 수 있는 리소스(CPU, 메모리, I/O 등)를 제한하고 모니터링함&lt;/li&gt;
&lt;li&gt;루트 파일 시스템: 루트 파일 시스템을 마운트함. 이 파일시스템은 보통 지정된 프로세스를 실행하는데 필요한 파일과 디렉터리가 포함된 이미지임.&lt;/li&gt;
&lt;li&gt;프로세스 실행: 격리된 환경 내에서 지정된 프로세스를 실행.&lt;/li&gt;
&lt;li&gt;라이프사이클 관리: 인스턴스 생성, 실행, 일시중지, 재개 등&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/도커</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/734</guid>
      <comments>https://jake-seo-dev.tistory.com/734#entry734comment</comments>
      <pubDate>Sun, 4 Feb 2024 14:19:33 +0900</pubDate>
    </item>
    <item>
      <title>도커의 아키텍처</title>
      <link>https://jake-seo-dev.tistory.com/733</link>
      <description>&lt;h2&gt;도커의 역할&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;커널의 컨테이너 가상화 기술을 사용자가 손쉽게 활용할 수 있게 하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;도커 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1706985184035_1706985184917.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;컨테이너 엔진: 사용자의 요청을 받아 컨테이너를 관리하는 역할&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jake-seo-dev.tistory.com/734&quot;&gt;컨테이너 런타임&lt;/a&gt;: 직접 커널과 통신하며 실제로 격리된 공간을 만드는 역할&lt;ul&gt;
&lt;li&gt;도커는 runc 라는 컨테이너 런타임을 사용&lt;/li&gt;
&lt;li&gt;runc 는 &lt;a href=&quot;http://www.opennaru.com/kubernetes/open-container-initiative/&quot;&gt;OCI (Open Container Initiative)&lt;/a&gt; 를 준수함&lt;/li&gt;
&lt;li&gt;무조건 runc 를 사용해야 하는 것은 아니지만 도커에서 기본으로 사용하는 컨테이너 런타임임&lt;/li&gt;
&lt;li&gt;리눅스 커널 버전마다 사용법이 다른데, runC는 cgroup, namespace 와 같은 기술을 버전에 구애받지 않고 사용할 수 있도록 LXC 혹은 libvirt 를 사용하여 간접적으로 관리함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;도커 클라이언트 서버 구조&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1706986063206_1706986064273.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;도커는 클라이언트 서버구조로 동작한다.&lt;/li&gt;
&lt;li&gt;사용자가 명령어를 입력하면 CLI 는 API 에 맞게 요청을 만들어 도커 데몬에 전달한다.&lt;ul&gt;
&lt;li&gt;도커 데몬은 컨테이너 런타임을 통해 컨테이너를 조작하고 CLI 로 응답을 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;도커는 보통 JSON 형태로 응답을 한다.&lt;ul&gt;
&lt;li&gt;CLI 에서는 CLI 가 이를 보기 좋은 방식으로 다시 변경하여 이용자에게 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Host OS 는 도커 데몬과 소통하며 컨테이너를 관리한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/도커</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/733</guid>
      <comments>https://jake-seo-dev.tistory.com/733#entry733comment</comments>
      <pubDate>Sun, 4 Feb 2024 03:52:39 +0900</pubDate>
    </item>
    <item>
      <title>컨테이너 가상화란?</title>
      <link>https://jake-seo-dev.tistory.com/732</link>
      <description>&lt;h2&gt;컨테이너 가상화&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jake-seo-dev.tistory.com/730&quot;&gt;하이퍼바이저 방식&lt;/a&gt;보다 더 가볍고 더 빠르다.&lt;/li&gt;
&lt;li&gt;리눅스 커널이 제공하는 &lt;a href=&quot;https://jake-seo-dev.tistory.com/731&quot;&gt;LXC, 리눅스 컨테이너&lt;/a&gt; 라는 자체 격리 기술에서 출발했다.&lt;ul&gt;
&lt;li&gt;커널 자체의 공간만 사용하여도 격리된 공간을 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;커널의 Namespace 와 Cgroups 를 이용한다.&lt;ul&gt;
&lt;li&gt;Namespace: 프로세스, 하드드라이브, 네트워크, 사용자, 호스트 네임처럼 리소스를 나누는 기준의 역할&lt;/li&gt;
&lt;li&gt;Cgroups: 메모리, CPU, Disk, Network Bandwidth 처럼 리소스의 사용량을 배분하는 기술&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LXC 라는 기술을 통해 분리된 공간 각각을 컨테이너라고 부른다.&lt;/li&gt;
&lt;li&gt;Host OS 의 커널을 공유하는데, 이게 컨테이너 가상화의 가장 큰 특징 중 하나이다.&lt;ul&gt;
&lt;li&gt;Guest OS 를 사용하는 하이퍼바이저 방식보다 오버헤드가 작다.&lt;ul&gt;
&lt;li&gt;하이퍼바이저 방식은 Guest OS 의 커널에서 시스템 콜을 하고 그게 번역이 되어 Host OS 에 전달되는 오버헤드가 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/도커</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/732</guid>
      <comments>https://jake-seo-dev.tistory.com/732#entry732comment</comments>
      <pubDate>Sun, 4 Feb 2024 03:23:17 +0900</pubDate>
    </item>
    <item>
      <title>LXC (Linux Container, 리눅스 컨테이너) 란?</title>
      <link>https://jake-seo-dev.tistory.com/731</link>
      <description>&lt;h2&gt;LXC (Linux Containers) 란?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1706984491509_1706984492508.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LXC 는 리눅스 컨테이너 (Linux Containers) 란 뜻이다.&lt;/li&gt;
&lt;li&gt;리눅스 커널에 내장된 경량 가상화 기술이다.&lt;/li&gt;
&lt;li&gt;단일 호스트에서 여러 개의 격리된 Linux 시스템 (컨테이너) 을 실행할 수 있다.&lt;/li&gt;
&lt;li&gt;c그룹 및 네임스페이스 격리를 사용하여 독립적인 컨테이너가 단일 Linux 인스턴스 내에서 실행되도록 함으로써 가상머신(VM) 을 시작하고 유지 관리하는데 드는 오버헤드를 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;특징&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;리소스 효율성 및 속도: VM과 비교했을 때 호스트 시스템의 커널을 공유하므로 오버헤드가 적고 빨리 부팅할 수 있다.&lt;/li&gt;
&lt;li&gt;격리: 컨테이너 내부의 프로세스는 다른 컨테이너나 호스트 시스템에서 실행중인 프로세스를 보거나 상호작용할 수 없다.&lt;/li&gt;
&lt;li&gt;보안: AppArmor, SElinux 프로파일, cgroups, 네임스페이스 격리 등을 지원&lt;/li&gt;
&lt;li&gt;호환성: 호스트 시스템의 커널이 필요한 기능을 지원하는 경우 Linux 에서 실행되는 모든 소프트웨어를 호스팅할 수 있다.&lt;/li&gt;
&lt;li&gt;네트워크 관리: 컨테이너가 자체 사설 네트워크, IP 주소, 포트 매핑 등을 가질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;여담&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;도커는 LXC 에서 출발했으며, 때에 따라 LXC 가 Docker 보다 낫기도 하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;위키백과 링크&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/LXC&quot;&gt;https://en.wikipedia.org/wiki/LXC&lt;/a&gt;&lt;/p&gt;</description>
      <category>인프라/도커</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/731</guid>
      <comments>https://jake-seo-dev.tistory.com/731#entry731comment</comments>
      <pubDate>Sun, 4 Feb 2024 03:22:12 +0900</pubDate>
    </item>
    <item>
      <title>하이퍼바이저 (Hypervisor) 란 무엇인가?</title>
      <link>https://jake-seo-dev.tistory.com/730</link>
      <description>&lt;h2&gt;가상화 기술 용어&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Host OS: 가상화 기술을 이용하고 있는 호스트의 운영체제&lt;/li&gt;
&lt;li&gt;Guest OS: 가상화 기술에서 실행되고 있는 운영체제&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;하이퍼바이저&lt;/h2&gt;
&lt;h3&gt;하이퍼바이저의 동작 원리&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;OS 엔 하드웨어를 사용하기 위해 커널이라는 중요한 도구가 설치되어 있음&lt;/li&gt;
&lt;li&gt;커널은 System call 이란 것을 이용하여 OS 에 리소스 사용 요청을 전달함&lt;/li&gt;
&lt;li&gt;System call 을 통해 하드웨어 자원을 이용할 수 있음&lt;/li&gt;
&lt;li&gt;하이퍼바이저에서 Host OS 와 Guest OS 종류가 다르면 상호 호환이 되지 않음&lt;ul&gt;
&lt;li&gt;여기서 하이퍼바이저가 다른 커널 간의 언어를 통역하는 역할을 하게 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가상머신은 하드웨어 리소스 사용을 위해 Host OS 로 시스템 콜을 보냄&lt;ul&gt;
&lt;li&gt;이 시스템 콜은 하이퍼바이저에 의해 해석되어 Host OS 에 맞는 시스템 콜로 변함&lt;/li&gt;
&lt;li&gt;Host 커널은 이를 이용해 하드웨어 자원을 VM 에 할당시켜줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1706983422505_1706983424263.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;그림 출처: &lt;a href=&quot;https://www.researchgate.net/publication/367954159_A_Novel_Approach_to_Prevent_Cloud_Infrastructure_against_Cache_Attacks&quot;&gt;https://www.researchgate.net/publication/367954159_A_Novel_Approach_to_Prevent_Cloud_Infrastructure_against_Cache_Attacks&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;하이퍼바이저의 역할&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;이 기종 커널 간 요청 전달&lt;/li&gt;
&lt;li&gt;가상머신 리소스 할당량 관리&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;하이퍼바이저의 종류&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;vmware (보통 기업용, 유료)&lt;/li&gt;
&lt;li&gt;virtualbox (오픈소스, 무료)&lt;/li&gt;
&lt;li&gt;rhev (red hat)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/도커</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/730</guid>
      <comments>https://jake-seo-dev.tistory.com/730#entry730comment</comments>
      <pubDate>Sun, 4 Feb 2024 03:07:13 +0900</pubDate>
    </item>
    <item>
      <title>.bashrc 파일이란?</title>
      <link>https://jake-seo-dev.tistory.com/729</link>
      <description>&lt;h2&gt;.bashrc 란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Bash&lt;/code&gt; 가 시작될 때마다 실행되는 스크립트다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bash&lt;/code&gt; 는 대화형 셸 세션이다.&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jake-seo-dev.tistory.com/728&quot;&gt;bash, 그리고 Shell 이란 무엇인가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[[002.bash-란-무엇인가]]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bash&lt;/code&gt; 에 무언가 개인화된 기능을 넣을 때 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;.bashrc 의 일반적인 용도&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;환경변수 설정 (environment variables)&lt;/li&gt;
&lt;li&gt;명령의 별칭 만들기 (alias)&lt;/li&gt;
&lt;li&gt;기본 편집기 설정&lt;/li&gt;
&lt;li&gt;Bash 프롬프트 사용자 지정&lt;/li&gt;
&lt;li&gt;PATH 변수에 디렉터리 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;.bashrc&lt;/code&gt; 는 &lt;code&gt;Bash&lt;/code&gt; 셸에서만 동작한다. Zsh 나 Fish 와 같은 다른 셸엔 자체 구성파일이 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;.bashrc 파일의 작성 예제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;alias&lt;/code&gt;: 편의를 위한 별칭 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PATH&lt;/code&gt;: 사용자의 &lt;code&gt;/bin&lt;/code&gt; 디렉터리가 있는 경우 이를 포함하도록 PATH 업데이트&lt;/li&gt;
&lt;li&gt;등등등...&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# .bashrc

# Alias definitions.
alias ll=&amp;#39;ls -la&amp;#39;  # List all files in long format
alias grep=&amp;#39;grep --color=auto&amp;#39;  # Colorize grep output
alias df=&amp;#39;df -h&amp;#39;  # Disk space in human-readable format
alias rm=&amp;#39;rm -i&amp;#39;  # Interactive mode for rm

# PATH: Add user&amp;#39;s private bin if it exists
if [ -d &amp;quot;$HOME/bin&amp;quot; ] ; then
    PATH=&amp;quot;$HOME/bin:$PATH&amp;quot;
fi

# Environment Variables
export EDITOR=&amp;#39;vim&amp;#39;  # Set default editor to vim
export GIT_EDITOR=&amp;#39;vim&amp;#39;  # Set Git editor to vim

# Bash Prompt
export PS1=&amp;#39;\[\e[0;33m\]\u@\h:\[\e[0;36m\]\w\[\e[m\]\$ &amp;#39;  # Colorized Bash prompt

# Load custom functions, if any
if [ -f &amp;quot;$HOME/.bash_functions&amp;quot; ]; then
    . &amp;quot;$HOME/.bash_functions&amp;quot;
fi

# Enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
    test -r ~/.dircolors &amp;amp;&amp;amp; eval &amp;quot;$(dircolors -b ~/.dircolors)&amp;quot; || eval &amp;quot;$(dircolors -b)&amp;quot;
    alias ls=&amp;#39;ls --color=auto&amp;#39;
fi

# Add git branch if its present to PS1
parse_git_branch() {
    git branch 2&amp;gt; /dev/null | sed -e &amp;#39;/^[^*]/d&amp;#39; -e &amp;#39;s/* \(.*\)/ (\1)/&amp;#39;
}
export PS1=&amp;quot;\[\e[0;33m\]\u@\h\[\e[0m\]:\[\e[0;36m\]\w\[\e[0;32m\]\$(parse_git_branch)\[\e[0m\]\$ &amp;quot;

# Check if window size has changed after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize&lt;/code&gt;&lt;/pre&gt;</description>
      <category>리눅스와 유닉스</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/729</guid>
      <comments>https://jake-seo-dev.tistory.com/729#entry729comment</comments>
      <pubDate>Sun, 4 Feb 2024 00:00:55 +0900</pubDate>
    </item>
    <item>
      <title>Bash, 그리고 Shell 이란 무엇일까?</title>
      <link>https://jake-seo-dev.tistory.com/728</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Shell 의 일종이다.&lt;/li&gt;
&lt;li&gt;Shell 이란 운영체제에서 커널과 이용자 사이에 끼어 이용자의 명령어를 해석하고 처리 결과를 나타내주는 시스템 프로그램을 말한다.&lt;ul&gt;
&lt;li&gt;운영체제만 딸랑 있으면 운영체제 내부에 있는 파일 접근, 명령 수행 등이 매우 어려운데 그 사이 브로커 역할을 해주는 것이 Shell 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bash 는 &lt;strong&gt;B&lt;/strong&gt;ourne &lt;strong&gt;A&lt;/strong&gt;gain &lt;strong&gt;SH&lt;/strong&gt;ell 을 줄여서 표현한 것이다.&lt;/li&gt;
&lt;li&gt;Bash 외에도 csh, ksh, zsh 등 많은 셸들이 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bash 의 기능들&lt;/h2&gt;
&lt;h3&gt;CLI (Command-line Interface)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;파일 관리, 프로그램 실행, 디렉터리 탐색 등의 작업 수행이 가능한 텍스트 인터페이스를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;스크립팅 언어 (Scripting Language)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;셸 스크립트를 작성할 수 있게 해준다.&lt;/li&gt;
&lt;li&gt;파일 루핑, 분기(if/else), 순차적 명령어 실행 등의 기능이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;환경 사용자 지정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.bashrc&lt;/code&gt; (개별 사용자용) 및 &lt;code&gt;/etc/bash.bashrc&lt;/code&gt; (전체 사용자용) 구성 파일을 편집하여 환경을 사용자 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;긴 명령에 대한 별칭을 정하거나, 사용자 지정 함수를 등록하는 등의 행위를 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;파이핑 및 리다이렉션&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;|&lt;/code&gt; 명령어를 사용하여 여러 명령을 서로 연결하고 리다이렉션 (&lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; 등) 을 사용하여 파일 또는 다른 명령과 직접 입출력을 주고받을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;명령 기록&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;실행했던 명령어를 기록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;작업 제어&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;동시에 여러 프로세스를 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;백그라운드 프로세스를 실행하거나 포그라운드로 가져오거나 중지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>리눅스와 유닉스</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/728</guid>
      <comments>https://jake-seo-dev.tistory.com/728#entry728comment</comments>
      <pubDate>Sat, 3 Feb 2024 23:56:42 +0900</pubDate>
    </item>
    <item>
      <title>Nest.js 의 Class Validator 란?</title>
      <link>https://jake-seo-dev.tistory.com/727</link>
      <description>&lt;h2&gt;Class Validator 란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;nestjs 파이프 아래에 속하는 개념이다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nestjs.com/pipes#class-validator&quot;&gt;Class Validator 공식문서&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/typestack/class-validator?tab=readme-ov-file#validation-decorators&quot;&gt;더 상세한 Typescript Validator 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데코레이터를 통해 주로 DTO 내부에 있는 프로퍼티들의 값을 검증하는데 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;예시&lt;/h2&gt;
&lt;h3&gt;글로벌로 적용하기 위해 필요한 설정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main.ts&lt;/code&gt; 에 다음 코드를 반드시 설정해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { NestFactory } from &amp;quot;@nestjs/core&amp;quot;;
import { AppModule } from &amp;quot;./app.module&amp;quot;;
import { ValidationPipe } from &amp;quot;@nestjs/common&amp;quot;;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 앱 전반에서 Validation 을 사용 가능
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DTO 작성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@IsString()&lt;/code&gt;, &lt;code&gt;@IsNotEmpty()&lt;/code&gt; 등의 validator 를 통해 제약조건을 건다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@IsString()&lt;/code&gt;: 문자열만 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@IsNotEmpty()&lt;/code&gt;: 반드시 입력해야 하는 필수 값&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { IsNotEmpty, IsString } from &amp;quot;class-validator&amp;quot;;

export class CreatePostDto {
  @IsString()
  @IsNotEmpty()
  title: string;

  @IsString()
  content: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;컨트롤러&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Body&lt;/code&gt; 부분을 작성한 Dto 타입으로 정의해주면, DTO 는 위에&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@Post()
@UseGuards(AccessTokenGuard)
postPosts(@User(&amp;#39;id&amp;#39;) id: number, @Body() body: CreatePostDto) {
  return this.postsService.createPost(id, body);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;검증 후 메세지 변경하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;데코레이터의 인자에 &lt;code&gt;{}&lt;/code&gt; 로 옵션을 넣어줄 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message&lt;/code&gt; 를 추가하면, 해당 메세지로 검증 결과를 받아볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { IsNotEmpty, IsString } from &amp;quot;class-validator&amp;quot;;

export class CreatePostDto {
  @IsString({
    message: &amp;quot;제목은 문자로 입력해주어야 합니다.&amp;quot;,
  })
  @IsNotEmpty({
    message: &amp;quot;제목은 반드시 입력해야 합니다.&amp;quot;,
  })
  title: string;

  @IsString()
  content: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ValidationPipe 은 어떻게 구현되어 있을까?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Global 하게 적용되어 모든 &lt;code&gt;class-validator&lt;/code&gt; 가 이 로직에 맞춰 동작하게 만들어준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from &amp;quot;@nestjs/common&amp;quot;;
import { validate } from &amp;quot;class-validator&amp;quot;;
import { plainToInstance } from &amp;quot;class-transformer&amp;quot;;

@Injectable()
export class ValidationPipe implements PipeTransform&amp;lt;any&amp;gt; {
  // 비동기로 작성되어 있음
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToInstance(metatype, value);
    const errors = await validate(object);
    if (errors.length &amp;gt; 0) {
      throw new BadRequestException(&amp;quot;Validation failed&amp;quot;);
    }
    return value;
  }

  // 네이티브 JS 타입인 경우 검사를 하지 않음
  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;기타 옵션들&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export interface ValidationOptions {
  /**
   * 배열에서 각각의 아이템들이 검증되어야 하는 경우
   */
  each?: boolean;
  /**
   * 검증 실패 시 보여줄 메세지, `ValidationArguments` 를 통해 함수로 작성도 가능하다.
   */
  message?: string | ((validationArguments: ValidationArguments) =&amp;gt; string);
  /**
   * 이 검증에 사용될 검증 그룹 (아마 검증 그룹이라는 걸 만들어서 여러개의 검증을 묶을 수 있는듯하다.)
   */
  groups?: string[];
  /**
   * 검증 그룹 상관없이 항상 검증을 하는가?
   */
  always?: boolean;
  context?: any;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프레임워크/Nest.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/727</guid>
      <comments>https://jake-seo-dev.tistory.com/727#entry727comment</comments>
      <pubDate>Fri, 2 Feb 2024 22:28:03 +0900</pubDate>
    </item>
    <item>
      <title>Nest.js 의 가드(Guard) 란?</title>
      <link>https://jake-seo-dev.tistory.com/726</link>
      <description>&lt;h2&gt;Guard (가드) 란?&lt;/h2&gt;
&lt;h3&gt;개념&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Nest.js 에서 요청이 실제로 처리되기 전에 보통 권한이 있는지 확인하는 방패막이(Guard) 역할을 해주는 컴포넌트다.&lt;/li&gt;
&lt;li&gt;가드가 없었다면 그냥 컨트롤러로 전해질수도 있는 요청에 이 가드가 중간 브로커 역할을 해서 인증 정보를 확인하거나 사용자 정보를 넣어주는 역할을 하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1706880162445_1706880164296.png&quot; alt=&quot;picture 2&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Nest.js 의 라이프사이클로 보는 가드(Guard)의 위치&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;세부 설명&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CanActive&lt;/code&gt; 인터페이스를 구현하고, &lt;code&gt;@Injectable&lt;/code&gt; 데코레이터가 붙은 클래스이다.&lt;/li&gt;
&lt;li&gt;런타임에 존재하는 조건에 따라 요청이 라우트 핸들러에 의해 처리될지 여부를 결정한다.&lt;ul&gt;
&lt;li&gt;조건이란, 권한, 역할(ROLE), ACLs(Access Control Lists) 등을 말한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;흔히 Authorization 이라 불리는 것들과 연관이 깊다.&lt;/li&gt;
&lt;li&gt;Express 애플리케이션에서는 미들웨어가 이 역할을 한다.&lt;ul&gt;
&lt;li&gt;미들웨어로도 처리가 가능하지만, Guard 는 &lt;code&gt;ExecutionContext&lt;/code&gt; 인스턴스에 접근이 가능해서 다음에 무엇이 실행될지 알 수 있는 것이 차이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1705127370985_1705127372465.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h2&gt;예제 인증 가드 구현&lt;/h2&gt;
&lt;h3&gt;BasicTokenGuard&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Basic Token 을 통해 인증을 진행하는 가드의 구현 예제이다.&lt;/li&gt;
&lt;li&gt;마지막에 &lt;code&gt;request.user&lt;/code&gt; 에 사용자 정보를 넣어주는데 &lt;code&gt;@Controller&lt;/code&gt; 메서드에서 이 가드를 거치면, &lt;code&gt;@Request&lt;/code&gt; 데코레이터를 통해 해당 사용자 객체에 접근할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@Injectable()
export class BasicTokenGuard implements CanActivate {
  constructor(private readonly authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    const req = context.switchToHttp().getRequest();

    const rawToken = req.headers[&amp;quot;authorization&amp;quot;];

    if (!rawToken) {
      throw new UnauthorizedException(&amp;quot;인증 토큰이 존재하지 않습니다.&amp;quot;);
    }

    const token = this.authService.extractTokenFromHeader(rawToken, false);

    const { email, password } = this.authService.decodeBasicToken(token);

    const user = this.authService.authenticateWithEmailAndPassword({
      email,
      password,
    });

    req.user = user;

    return true;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;BearerTokenGuard&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BearerTokenGuard&lt;/code&gt; 는 사실상 상속용으로 구현했고, 실제로 사용되는 건 &lt;code&gt;RefreshTokenGuard&lt;/code&gt; 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from &amp;quot;@nestjs/common&amp;quot;;
import { AuthService, JwtPayload } from &amp;quot;../auth.service&amp;quot;;
import { UsersService } from &amp;quot;src/users/users.service&amp;quot;;
import { UsersModel } from &amp;quot;src/users/entities/users.entity&amp;quot;;

interface BearerGuardRequest {
  token: string;
  tokenType: JwtPayload[&amp;quot;type&amp;quot;];
  user: UsersModel;
  headers: {
    authorization: string;
  };
}

@Injectable()
export class BearerTokenGaurd implements CanActivate {
  constructor(
    private readonly authService: AuthService,
    private readonly usersService: UsersService
  ) {}

  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    const req: BearerGuardRequest = context.switchToHttp().getRequest();

    const rawToken = req.headers[&amp;quot;authorization&amp;quot;];

    if (!rawToken) {
      throw new UnauthorizedException(&amp;quot;인증 토큰이 존재하지 않습니다.&amp;quot;);
    }

    const token = this.authService.extractTokenFromHeader(rawToken, true);

    const payload = this.authService.verifyToken(token);

    req.token = token;
    req.tokenType = payload.type;
    req.user = await this.usersService.getUserByEmail(payload.email);

    return true;
  }
}

@Injectable()
export class AccessTokenGuard extends BearerTokenGaurd {
  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    await super.canActivate(context);

    const req: BearerGuardRequest = context.switchToHttp().getRequest();

    if (req.tokenType !== &amp;quot;access&amp;quot;) {
      throw new UnauthorizedException(&amp;quot;액세스 토큰이 아닙니다!&amp;quot;);
    }

    return true;
  }
}

@Injectable()
export class RefreshTokenGuard extends BearerTokenGaurd {
  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    await super.canActivate(context);

    const req: BearerGuardRequest = context.switchToHttp().getRequest();

    if (req.tokenType !== &amp;quot;refresh&amp;quot;) {
      throw new UnauthorizedException(&amp;quot;리프레시 토큰이 아닙니다!&amp;quot;);
    }

    return true;
  }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프레임워크/Nest.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/726</guid>
      <comments>https://jake-seo-dev.tistory.com/726#entry726comment</comments>
      <pubDate>Fri, 2 Feb 2024 22:23:44 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot3 (스프링 부트3) 에서 Log4j2 세팅 방법</title>
      <link>https://jake-seo-dev.tistory.com/725</link>
      <description>&lt;h2&gt;Spring Boot3 에서 Log4j2 세팅 방법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;버전은 스프링 부트 3 현재 최신 버전 (2024-02-02) 을 기준으로 한다.&lt;/li&gt;
&lt;li&gt;패키지 관리 도구는 Gradle 이용을 기준으로 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;스프링부트 기본 제공 Logback 을 의존성에서 제거&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring Boot 의 기본 로깅 도구 Logback 을 의존성에서 제거한다.&lt;/li&gt;
&lt;li&gt;dependencies 아래에 configurations 영역을 추가해주고 아래 내용을 붙여넣는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;dependencies {
  // ...
}

// 추가
configurations {
    all*.exclude module: &amp;#39;spring-boot-starter-logging&amp;#39;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;의존성 제거 전&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1706754494701_1706754496201.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;h3&gt;의존성 제거 후&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;spring-boot-starter-logging&lt;/code&gt; 이 사라진 것을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1706755385829_1706755389547.png&quot; alt=&quot;picture 2&quot;&gt;&lt;/p&gt;
&lt;h2&gt;log4j2 의존성 추가하기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;dependencies {
    implementation &amp;#39;org.springframework.boot:spring-boot-starter-log4j2&amp;#39;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;환경별 log4j2.xml 파일 작성하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이번 경우엔 ERROR 레벨부터 로깅하는 운영 환경과 INFO 레벨부터 로깅하는 개발 환경이 필요했다.&lt;/li&gt;
&lt;li&gt;운영 환경에서는 기본 정책으로 매 시간마다 파일을 롤링한다.&lt;/li&gt;
&lt;li&gt;개발 환경에서는 기본 정책으로 더 짧게 매 분마다 파일을 롤링한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;log4j2-dev.xml&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;개발 환경의 XML 파일을 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;Configuration status=&amp;quot;INFO&amp;quot;&amp;gt;
    &amp;lt;Properties&amp;gt;
        &amp;lt;Property name=&amp;quot;LOG_PATH&amp;quot;&amp;gt;logs&amp;lt;/Property&amp;gt;
        &amp;lt;Property name=&amp;quot;DEFAULT_PATTERN&amp;quot;&amp;gt;[%d{yyyy-MM-dd HH:mm:ss.SSS}] %5p ${sys:PID} --- [%15.15t] %-40.40c{1.} : %m%n%xwEx&amp;lt;/Property&amp;gt;
        &amp;lt;Property name=&amp;quot;COLOR_PATTERN&amp;quot;&amp;gt;[%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint}] %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c}{cyan} %clr{:}{faint} %m%n%xwEx&amp;lt;/Property&amp;gt;
        &amp;lt;Property name=&amp;quot;TEST_PATTERN&amp;quot;&amp;gt;%d %5p [%c] %m%n&amp;lt;/Property&amp;gt;
    &amp;lt;/Properties&amp;gt;

    &amp;lt;Appenders&amp;gt;
        &amp;lt;Console name=&amp;quot;ConsoleAppender&amp;quot; target=&amp;quot;SYSTEM_OUT&amp;quot; follow=&amp;quot;true&amp;quot;&amp;gt;
            &amp;lt;PatternLayout pattern=&amp;quot;${COLOR_PATTERN}&amp;quot;/&amp;gt;
        &amp;lt;/Console&amp;gt;

        &amp;lt;RollingFile name =&amp;quot;RollingFileAppender&amp;quot;&amp;gt;
            &amp;lt;FileName&amp;gt;${LOG_PATH}/log4j2.log&amp;lt;/FileName&amp;gt;
            &amp;lt;FilePattern&amp;gt;${LOG_PATH}/log4j2.%d{yyyy-MM-dd-hh-mm}.%i.log&amp;lt;/FilePattern&amp;gt;
            &amp;lt;PatternLayout&amp;gt;
                &amp;lt;Pattern&amp;gt;${DEFAULT_PATTERN}&amp;lt;/Pattern&amp;gt;
            &amp;lt;/PatternLayout&amp;gt;
            &amp;lt;Policies&amp;gt;
                &amp;lt;SizeBasedTriggeringPolicy size=&amp;quot;100MB&amp;quot;/&amp;gt;
                &amp;lt;TimeBasedTriggeringPolicy interval = &amp;quot;1&amp;quot; modulate = &amp;quot;true&amp;quot;/&amp;gt;
            &amp;lt;/Policies&amp;gt;
        &amp;lt;/RollingFile&amp;gt;

        &amp;lt;RollingFile name =&amp;quot;RollingFileErrorAppender&amp;quot;&amp;gt;
            &amp;lt;FileName&amp;gt;${LOG_PATH}/log4j2_error.log&amp;lt;/FileName&amp;gt;
            &amp;lt;FilePattern&amp;gt;${LOG_PATH}/log4j2_error.%d{yyyy-MM-dd-hh-mm}.%i.log&amp;lt;/FilePattern&amp;gt;
            &amp;lt;PatternLayout&amp;gt;
                &amp;lt;Pattern&amp;gt;${DEFAULT_PATTERN}&amp;lt;/Pattern&amp;gt;
            &amp;lt;/PatternLayout&amp;gt;
            &amp;lt;Policies&amp;gt;
                &amp;lt;SizeBasedTriggeringPolicy size=&amp;quot;100MB&amp;quot;/&amp;gt;
                &amp;lt;TimeBasedTriggeringPolicy interval = &amp;quot;1&amp;quot; modulate = &amp;quot;true&amp;quot;/&amp;gt;
            &amp;lt;/Policies&amp;gt;
        &amp;lt;/RollingFile&amp;gt;
    &amp;lt;/Appenders&amp;gt;


    &amp;lt;Loggers&amp;gt;
        &amp;lt;!-- Root : 일반적인 로그 정책, 반드시 하나 정의해야함 --&amp;gt;
        &amp;lt;Root level=&amp;quot;INFO&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileErrorAppender&amp;quot; level=&amp;quot;ERROR&amp;quot; /&amp;gt;
        &amp;lt;/Root&amp;gt;

        &amp;lt;!-- springframework logger --&amp;gt;
        &amp;lt;Logger name=&amp;quot;org.springframework&amp;quot; level=&amp;quot;INFO&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot; /&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;!-- jdbc logger --&amp;gt;
        &amp;lt;Logger name=&amp;quot;jdbc.sqlonly&amp;quot; level=&amp;quot;OFF&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.sqltiming&amp;quot; level=&amp;quot;INFO&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.resultsettable&amp;quot; level=&amp;quot;INFO&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.audit&amp;quot; level=&amp;quot;OFF&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.resultset&amp;quot; level=&amp;quot;OFF&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.connection&amp;quot; level=&amp;quot;OFF&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;
    &amp;lt;/Loggers&amp;gt;
&amp;lt;/Configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;log4j2-prod.xml&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;운영 환경의 XML 파일을 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;Configuration status=&amp;quot;INFO&amp;quot;&amp;gt;
    &amp;lt;Properties&amp;gt;
        &amp;lt;Property name=&amp;quot;LOG_PATH&amp;quot;&amp;gt;logs&amp;lt;/Property&amp;gt;
        &amp;lt;Property name=&amp;quot;DEFAULT_PATTERN&amp;quot;&amp;gt;[%d{yyyy-MM-dd HH:mm:ss.SSS}] %5p ${sys:PID} --- [%15.15t] %-40.40c{1.} : %m%n%xwEx&amp;lt;/Property&amp;gt;
        &amp;lt;Property name=&amp;quot;COLOR_PATTERN&amp;quot;&amp;gt;[%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint}] %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c}{cyan} %clr{:}{faint} %m%n%xwEx&amp;lt;/Property&amp;gt;
        &amp;lt;Property name=&amp;quot;TEST_PATTERN&amp;quot;&amp;gt;%d %5p [%c] %m%n&amp;lt;/Property&amp;gt;
    &amp;lt;/Properties&amp;gt;

    &amp;lt;Appenders&amp;gt;
        &amp;lt;Console name=&amp;quot;ConsoleAppender&amp;quot; target=&amp;quot;SYSTEM_OUT&amp;quot; follow=&amp;quot;true&amp;quot;&amp;gt;
            &amp;lt;PatternLayout pattern=&amp;quot;${COLOR_PATTERN}&amp;quot;/&amp;gt;
        &amp;lt;/Console&amp;gt;

        &amp;lt;RollingFile name =&amp;quot;RollingFileAppender&amp;quot;&amp;gt;
            &amp;lt;FileName&amp;gt;${LOG_PATH}/log4j2.log&amp;lt;/FileName&amp;gt;
            &amp;lt;FilePattern&amp;gt;${LOG_PATH}/log4j2.%d{yyyy-MM-dd-hh}.%i.log&amp;lt;/FilePattern&amp;gt;
            &amp;lt;PatternLayout&amp;gt;
                &amp;lt;Pattern&amp;gt;${DEFAULT_PATTERN}&amp;lt;/Pattern&amp;gt;
            &amp;lt;/PatternLayout&amp;gt;
            &amp;lt;Policies&amp;gt;
                &amp;lt;SizeBasedTriggeringPolicy size=&amp;quot;100MB&amp;quot;/&amp;gt;
                &amp;lt;TimeBasedTriggeringPolicy interval = &amp;quot;1&amp;quot; modulate = &amp;quot;true&amp;quot;/&amp;gt;
            &amp;lt;/Policies&amp;gt;
        &amp;lt;/RollingFile&amp;gt;

        &amp;lt;RollingFile name =&amp;quot;RollingFileErrorAppender&amp;quot;&amp;gt;
            &amp;lt;FileName&amp;gt;${LOG_PATH}/log4j2_error.log&amp;lt;/FileName&amp;gt;
            &amp;lt;FilePattern&amp;gt;${LOG_PATH}/log4j2_error.%d{yyyy-MM-dd-hh}.%i.log&amp;lt;/FilePattern&amp;gt;
            &amp;lt;PatternLayout&amp;gt;
                &amp;lt;Pattern&amp;gt;${DEFAULT_PATTERN}&amp;lt;/Pattern&amp;gt;
            &amp;lt;/PatternLayout&amp;gt;
            &amp;lt;Policies&amp;gt;
                &amp;lt;SizeBasedTriggeringPolicy size=&amp;quot;100MB&amp;quot;/&amp;gt;
                &amp;lt;TimeBasedTriggeringPolicy interval = &amp;quot;1&amp;quot; modulate = &amp;quot;true&amp;quot;/&amp;gt;
            &amp;lt;/Policies&amp;gt;
        &amp;lt;/RollingFile&amp;gt;
    &amp;lt;/Appenders&amp;gt;


    &amp;lt;Loggers&amp;gt;
        &amp;lt;!-- Root : 일반적인 로그 정책, 반드시 하나 정의해야함 --&amp;gt;
        &amp;lt;Root level=&amp;quot;ERROR&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileErrorAppender&amp;quot; level=&amp;quot;ERROR&amp;quot; /&amp;gt;
        &amp;lt;/Root&amp;gt;

        &amp;lt;!-- springframework logger --&amp;gt;
        &amp;lt;Logger name=&amp;quot;org.springframework&amp;quot; level=&amp;quot;INFO&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot; /&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;!-- jdbc logger --&amp;gt;
        &amp;lt;Logger name=&amp;quot;jdbc.sqlonly&amp;quot; level=&amp;quot;OFF&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.sqltiming&amp;quot; level=&amp;quot;INFO&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.resultsettable&amp;quot; level=&amp;quot;INFO&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.audit&amp;quot; level=&amp;quot;OFF&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.resultset&amp;quot; level=&amp;quot;OFF&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;

        &amp;lt;Logger name=&amp;quot;jdbc.connection&amp;quot; level=&amp;quot;OFF&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;ConsoleAppender&amp;quot;/&amp;gt;
            &amp;lt;AppenderRef ref=&amp;quot;RollingFileAppender&amp;quot;/&amp;gt;
        &amp;lt;/Logger&amp;gt;
    &amp;lt;/Loggers&amp;gt;
&amp;lt;/Configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;application.yml 파일 설정하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;application.yml 파일에서 각 profile 마다 사용할 log4j2 설정파일을 명시해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;로컬 환경 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;spring:
  config:
    activate:
      on-profile: local
logging:
  config: classpath:log4j2-dev.xml&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;운영 환경 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;spring:
  config:
    activate:
      on-profile: prod
logging:
  config: classpath:log4j2-prod.xml&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프레임워크/스프링 부트</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/725</guid>
      <comments>https://jake-seo-dev.tistory.com/725#entry725comment</comments>
      <pubDate>Fri, 2 Feb 2024 16:49:01 +0900</pubDate>
    </item>
    <item>
      <title>Nest.js 의 파이프(Pipes) 란?</title>
      <link>https://jake-seo-dev.tistory.com/724</link>
      <description>&lt;h2&gt;파이프 (Pipe) 란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Injectable&lt;/code&gt; 데코레이터가 붙은 &lt;code&gt;PipeTransform&lt;/code&gt; 인터페이스를 구현하는 클래스로&lt;/li&gt;
&lt;li&gt;요청 데이터 변환 및 유효성 검사에 사용한다&lt;/li&gt;
&lt;li&gt;핸들러가 호출되기 직전 라우트 핸들러가 처리중인 Argument 를 대상으로 작동한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;주요 기능&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;변환 (Transformation): 입력 데이터를 원하는 형태로 변환 가능&lt;ul&gt;
&lt;li&gt;ex) 문자열 -&amp;gt; 정수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;유효성 검사 (Validation): 입력 데이터가 올바르지 않은 경우 예외 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;파이프가 실행되는 영역&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;파이프는 예외 영역 (Exceptions zone) 에서 실행되어 파이프가 던지는 예외는 예외 계층에서 처리된다. 파이프에서 예외가 발생하면 컨트롤러 메서드가 이어서 실행되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;다이어그램 살펴보기&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1705113804485_1705113806352.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;클라이언트: Nest.js 서버의 Route Handler 로 요청을 보냄&lt;/li&gt;
&lt;li&gt;필터: 라우트 핸들러까지의 경로에 존재함&lt;ul&gt;
&lt;li&gt;Http 컨텍스트에서의 예외를 포착하고 예외를 처리할 때마다 실행됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파이프: 라우트 핸들러 바로 앞에 위치하여 데이터를 변환하거나 유효성 검사를 함&lt;ul&gt;
&lt;li&gt;모든 미들웨어 다음으로 실행되고 각 요청에 대한 라우트 핸들러및 모든 인터셉터가 실행되기 전에 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Get&lt;/code&gt; 라우트 핸들러: Get 요청을 처리하는 컨트롤러 메서드를 나타냄&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;필터와 파이프를 서로 다른 위치에 놓은 이유는 요청 처리 파이프라인에서 필터와 파이프의 순서와 각기 다른 역할을 보여주기 위해서임&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;작동 방식&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;실행 지점: 가드 뒤와 인터셉터 및 라우트 핸들러 앞에서 실행&lt;/li&gt;
&lt;li&gt;데이터 흐름: 쿼리 파라미터, 패스 파라미터, 요청 본문 등의 입력 데이터를 받아 처리하고 변환된 데이터를 반환하거나 유효성 검사에 문제가 있는 경우 예외를 발생&lt;/li&gt;
&lt;li&gt;의존성 주입: &lt;code&gt;@Injectable&lt;/code&gt; 이 붙었기 때문에 서비스처럼 의존성 주입 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Pipe 의 종류&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;빌트인 파이프(Built-in Pipes): Nest.js 에서는 자주 사용되는 Validation, Transformation 에 대한 몇가지 표준 파이프를 제공함.&lt;ul&gt;
&lt;li&gt;이는 &lt;a href=&quot;https://docs.nestjs.com/pipes#built-in-pipes&quot;&gt;공식문서&lt;/a&gt;에서 확인 가능함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;커스텀 파이프(Custom Pipes): &lt;code&gt;PipeTransform&lt;/code&gt; 인터페이스와 &lt;code&gt;transform&lt;/code&gt; 메서드를 구현하여 생성하는 것도 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1705110765008_1705110767198.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h2&gt;파이프 스코프&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;전역 스코프 (Global Scope): &lt;code&gt;main.ts&lt;/code&gt; 에서 &lt;code&gt;app.useGlobalPipes(new YourPipe())&lt;/code&gt; 를 사용해 전역적으로 적용 가능&lt;/li&gt;
&lt;li&gt;컨트롤러 스코프 (Controller Scope): 컨트롤러 내 모든 라우트 핸들러에 파이프 적용 가능&lt;/li&gt;
&lt;li&gt;파라미터 스코프 (Parameter Scope): 라우트 핸들러의 특정 매개변수에 파이프 적용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드 예제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;쿼리 파라미터에 커스텀 파이프인 &lt;code&gt;UppercasePipe&lt;/code&gt; 를 적용하는 예이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { PipeTransform, Injectable, ArgumentMetadata } from &amp;quot;@nestjs/common&amp;quot;;

@Injectable()
export class UppercasePipe implements PipeTransform&amp;lt;string&amp;gt; {
  transform(value: string, metadata: ArgumentMetadata): string {
    return value.toUpperCase();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@Controller(&amp;quot;example&amp;quot;)
export class ExampleController {
  @Get()
  getData(@Query(&amp;quot;param&amp;quot;, UppercasePipe) param: string) {
    // &amp;#39;param&amp;#39; will be in uppercase
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;파이프의 또 다른 장점 (예외 처리)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ParseIntPipe&lt;/code&gt; 를 통해 요청의 쿼리 파라미터 등을 변환하면, 오류 시 미리 작성된 예외가 던져진다.&lt;/li&gt;
&lt;li&gt;Client 는 아래의 예외를 보고 무엇이 잘못됐는지 추측할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;message&amp;quot;: &amp;quot;Validation failed (numeric string is expected)&amp;quot;,
  &amp;quot;error&amp;quot;: &amp;quot;Bad Request&amp;quot;,
  &amp;quot;statusCode&amp;quot;: 400
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;파이프는 데이터 변환 및 유효성 검사를 도와주는 Nest.js 의 기능이다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프레임워크/Nest.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/724</guid>
      <comments>https://jake-seo-dev.tistory.com/724#entry724comment</comments>
      <pubDate>Sat, 13 Jan 2024 10:58:00 +0900</pubDate>
    </item>
    <item>
      <title>웹에서 자주 보게 되는 RFC 문서란 무엇일까?</title>
      <link>https://jake-seo-dev.tistory.com/723</link>
      <description>&lt;h2&gt;RFC 문서란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;개발에서 말하는 RFC 문서는 IETF(Internet Engineering Task Force) 와 ISOC(Internet Society)에서 발행하는 기술적이고 조직적인 메모다.&lt;/li&gt;
&lt;li&gt;프로토콜, 절차, 프로그램 및 개념들을 포함하는 인터넷 관련 내용을 다룬다.&lt;/li&gt;
&lt;li&gt;많은 인터넷 표준은 RFC 로 문서화가 되어있고, 인터넷 표준을 연구하는 사람은 누구나 RFC 문서를 작성할 수 있다.&lt;/li&gt;
&lt;li&gt;RFC 는 Request for Comment (의견 요청) 의 약자로 인터넷 관련 문제에 대한 피드백과 토론을 요청하는 문서의 초기 의도를 반영한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;RFC 문서의 주요 내용&lt;/h2&gt;
&lt;h3&gt;표준화 프로세스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;모든 RFC 가 표준이 되진 않지만, 상당 수는 인터넷 프로토콜 및 기술에 대한 공식 표준이 된다.&lt;/li&gt;
&lt;li&gt;표준화 과정에는 개발, 검토, 다양한 위원회와 광범위한 인터넷 커뮤니티의 승인이 포함된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;RFC 문서의 유형&lt;/h3&gt;
&lt;p&gt;상태와 목적에 따라 나뉜다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Standard Track: 표준으로 인정받은 상태, 오류가 발견돼도 수정되지 않고 다른 RFC 번호를 가진 문서로 새로 출판됨&lt;ul&gt;
&lt;li&gt;제안 표준 (Proposed Standard): 표준 문서로 인정받은 첫 단계, 시험 중으로 계속 개정이 필요&lt;/li&gt;
&lt;li&gt;초안 표준 (Draft Standard): 적어도 2개 이상의 다른 코드로 구현, 상호 운용에 대한 충분한 필드 구현이 됐으나, 테스트가 더 필요&lt;/li&gt;
&lt;li&gt;인터넷 표준 (Internet Standard): 안정적으로 동작하는 것을 확인, 성공적으로 구현되어 사용 중인 것들&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Informational: 인터넷 주제에 대한 일반적인 정보&lt;/li&gt;
&lt;li&gt;Experimental: 실험적 작업, 테스트와 평가를 위함&lt;/li&gt;
&lt;li&gt;Best Current Practice(BCP): 모범 사례 절차와 지침을 명시함&lt;/li&gt;
&lt;li&gt;Historic: 오래되었거나 더이상 사용되지 않는 RFC 는 기록으로 분류된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;RFC 문서의 특징&lt;/h2&gt;
&lt;h3&gt;공개 프로세스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;독립 연구자, 조직, IETF 작업 그룹 등 다양한 출처에서 기여와 제안을 할 수 있는 개방형 프로세스가 특징이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;광범위한 주제&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;TCP/IP, 이메일용 SMTP, 웹용 HTTP 같은 기본 인터넷 프로토콜부터 모범 사례 및 운영 절차에 대한 가이드라인까지 광범위하게 다룬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;접근성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;대중에게 무료로 제공되어 인터넷 표준과 정보의 투명성 광범위한 접근성을 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;기타&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RFC 는 인터넷 기술 및 프로토콜 개발, 표준화, 이해에 매우 중요하다.&lt;/li&gt;
&lt;li&gt;이는 안정적, 상호 운용 가능한 글로벌 인터넷 인프라 개발 유지를 위한 공동의 노력이다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>네트워크</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/723</guid>
      <comments>https://jake-seo-dev.tistory.com/723#entry723comment</comments>
      <pubDate>Sat, 13 Jan 2024 10:45:48 +0900</pubDate>
    </item>
    <item>
      <title>Authentication 앞에 Basic 혹은 Bearer 를 붙이는 이유 (인증 방식)</title>
      <link>https://jake-seo-dev.tistory.com/722</link>
      <description>&lt;h2&gt;원본 글&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://velog.io/@tosspayments/Basic-%EC%9D%B8%EC%A6%9D%EA%B3%BC-Bearer-%EC%9D%B8%EC%A6%9D%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83&quot;&gt;토스 페이먼츠 Basic Bearer 설명 포스팅&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;HTTP 통신 방법&lt;/h2&gt;
&lt;p&gt;Authentication 앞에 Basic 과 Bearer 를 붙이는 이유는 정해진 하나의 통신 프레임워크이기 때문이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;참고&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Authentication&quot;&gt;모질라 HTTP 인증 프레임워크 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7235&quot;&gt;웹 표준 RFC 7235&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Basic 인증&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;사용자ID:비밀번호&lt;/code&gt; 문자열을 &lt;code&gt;Basic&lt;/code&gt; 이라는 문자와 함께 인증 헤더에 입력한다.&lt;/li&gt;
&lt;li&gt;자세한 내용은 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7617&quot;&gt;웹 표준 RFC 7617&lt;/a&gt; 에 나와있다.&lt;/li&gt;
&lt;li&gt;Base64 는 디코딩이 가능하기 때문에 반드시 HTTPS, SSL/TLS 로 통신해야 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Authorization: Basic base64({USERNAME}:{PASSWORD})&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;간단하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;사용자 목록에서 권한을 확인하는데 시간이 많이 걸릴 수 있다.&lt;ul&gt;
&lt;li&gt;사용자가 많으면 부각되는 단점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;권한을 정교하게 제어하기 어려울 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bearer 인증&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oauth.net/2/&quot;&gt;OAuth2.0&lt;/a&gt; 프레임워크에서 사용하는 토큰 인증 방식이다.&lt;/li&gt;
&lt;li&gt;자세한 내용은 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6750&quot;&gt;RFC 6750&lt;/a&gt; 에 정의되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Authorization: Basic base64({USERNAME}:{PASSWORD})&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;OAuth 2.0 프레임워크란?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;소셜로긴 등의 서비스에 이용됨&lt;/li&gt;
&lt;li&gt;구글 등 특정 회사 내의 데이터 일부를 서드파티 서비스와 공유하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;OAuth2.0 의 구성요소&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;클라이언트: 사용자 정보를 접근하는 서드파티 서비스&lt;/li&gt;
&lt;li&gt;리소스 소유자: 사용자&lt;/li&gt;
&lt;li&gt;인증 서버: 클라이언트의 접근을 관리하는 서버&lt;/li&gt;
&lt;li&gt;리소스 서버: 리소스 소유자의 데이터를 관리하는 서버&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;사용 프로세스&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;리소스 소유자의 동의가 확인되면 인증 서버가 클라이언트에게 액세스 토큰을 발급해줌&lt;/li&gt;
&lt;li&gt;클라이언트는 액세스 토큰을 사용해서 리소스 서버에 보호된 데이터를 불러옴&lt;ul&gt;
&lt;li&gt;보호된 데이터란, 구글의 닉네임, 프로필사진 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1705067539055_1705067541533.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Bearer 토큰&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;불투명 문자열: 16진수 문자열 혹은 JWT 등&lt;ul&gt;
&lt;li&gt;중요한 건 해석할 수 없는 형태여야 함&lt;/li&gt;
&lt;li&gt;사용자의 정보를 전달하면 안됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1705067761319_1705067762882.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;토스 페이먼츠의 사용 사례&lt;/h2&gt;
&lt;p&gt;토스페이먼츠에서는 두가지 다 사용 중&lt;/p&gt;
&lt;h3&gt;Basic&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;코어 API 의 경우 사용자의 ID 를 시크릿 키로 Basic 인증을 함&lt;/li&gt;
&lt;li&gt;HTTPS/SSL 통신을 강제하고 있어서 안전함&lt;/li&gt;
&lt;li&gt;결제, 결제 취소, 현금영수증 발급 등&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Authorization: Basic dGVzdF9za19PeUwwcVo0RzFWT0xvYkI2S3d2cm9XYjJNUVlnOg==&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Bearer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;브랜드 페이의 경우 브랜드 페이 서비스 자체가 OAuth 프레임워크를 사용하고 더 안전하게 고객 정보를 보관하기 위해 Bearer 인증방식을 사용함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Authorization: Bearer Xy0E4Dv5qpMGjLJoQ1aVZ5449xB1P3w6KYe2RNgOWznZb7Bm&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;레퍼런스&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://velog.io/@tosspayments/Basic-%EC%9D%B8%EC%A6%9D%EA%B3%BC-Bearer-%EC%9D%B8%EC%A6%9D%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83&quot;&gt;토스 페이먼츠 Basic Bearer 설명 포스팅&lt;/a&gt;&lt;/p&gt;</description>
      <category>보안</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/722</guid>
      <comments>https://jake-seo-dev.tistory.com/722#entry722comment</comments>
      <pubDate>Sat, 13 Jan 2024 10:38:35 +0900</pubDate>
    </item>
    <item>
      <title>@Transactional 애노테이션 정리</title>
      <link>https://jake-seo-dev.tistory.com/721</link>
      <description>&lt;h2&gt;@Transactional 애노테이션이란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring 에게 트랜잭션 경계를 관리하도록 메타 정보를 주는 것이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt; 이 붙은 메서드가 호출됐을 때, 스프링은 새 트랜잭션을 시작한다.&lt;/li&gt;
&lt;li&gt;메서드가 성공적으로 완료되면 트랜잭션이 커밋된다.&lt;/li&gt;
&lt;li&gt;예외가 발생하면 트랜잭션이 롤백된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;@Transactional 애노테이션으로 설정할 수 있는 것들&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
// 여기에도 쓸 수 있고, 여기에 쓰면 클래스 내부의 모든 메서드에 적용된다.
public class YourService {

    private final YourRepository yourRepository;

    // Constructor injection of the repository
    public YourService(YourRepository yourRepository) {
        this.yourRepository = yourRepository;
    }

    @Transactional
    public void yourTransactionalMethod() {
        // Multiple repository operations that should be part of a single transaction
        yourRepository.method1();
        yourRepository.method2();
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;전파 동작 (Propagation Behavior)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션 컨텍스트가 이미 있을 때, 메서드가 기존 트랜잭션 내에서 실행될지 아니면 새 트랜잭션을 시작할지 지정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;격리 수준 (Isolation Level)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;한 트랜잭션에서 액세스하는 데이터가 다른 트랜잭션으로부터 격리되는 방식을 결정할 수 있다.&lt;/li&gt;
&lt;li&gt;dirty read, non-repeatable read, phantom read 와 같은 문제를 방지하는데 매우 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;읽기 전용 최적화 (Read-Only Optimization)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션을 읽기 전용으로 표시하면 성능을 최적화할 수 있다.&lt;/li&gt;
&lt;li&gt;일부 DB 에서는 읽기 전용 트랜잭션을 사용하면 커밋/롤백을 위해 변경 내역을 추적할 필요가 없어 더욱 효율적으로 실행할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;예외 처리 (Exception Handling)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;롤백을 트리거할 예외를 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;Spring 에서는 런타임 예외와 Unchecked 예외는 롤백을 유발하지만, Checked 예외는 롤백을 유발하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드 예제를 통해 @Transactional 의 옵션 알아보기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;propagation&lt;/code&gt;: 전파에 관한 옵션&lt;ul&gt;
&lt;li&gt;트랜잭션이 이미 진행중일 때 트랜잭션에 합류할지, 새 트랜잭션을 시작할지 등..&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isolation&lt;/code&gt;: 고립과 트랜잭션 간의 데이터 가시성 결정&lt;ul&gt;
&lt;li&gt;커밋된 데이터만 읽도록 할지 등...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readOnly&lt;/code&gt;: 트랜잭션을 읽기 전용으로 만들어 성능 최적화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timeout&lt;/code&gt;: 트랜잭션 완료 제한시간을 걸어둠&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rollbackFor&lt;/code&gt;: 어떤 예외로 트랜잭션이 롤백될지 정의&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noRollbackFor&lt;/code&gt;: 트랜잭션이 롤백되지 않아야 하는 예외를 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;

@Service
public class TransactionalService {

    @Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.READ_COMMITTED,
        readOnly = false,
        timeout = 30,
        rollbackFor = Exception.class,
        noRollbackFor = IllegalArgumentException.class
    )
    public void exampleTransactionalMethod() {
        // Business logic here
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;언제 써야하는가?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;리포지토리 계층이 아닌 서비스 계층에서 &lt;code&gt;@Transactional&lt;/code&gt; 애노테이션을 적용하는 것이 가장 좋다.&lt;ul&gt;
&lt;li&gt;서비스 메서드에서 리포지토리 호출을 포함하는 로직이 있을 때 비즈니스 로직 및 데이터베이스 작업을 캡슐화 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 무결성을 유지할 때 필수적이다.&lt;ul&gt;
&lt;li&gt;서비스 수준 트랜잭션으로 래핑하면 모든 DB 작업이 단위로 성공적으로 완료되거나 오류 발생 시 롤백되어 데이터 무결성을 유지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;적절한 트랜잭션 관리, 데이터 무결성 및 일관성 유지에 필요하다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프레임워크/스프링 프레임워크</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/721</guid>
      <comments>https://jake-seo-dev.tistory.com/721#entry721comment</comments>
      <pubDate>Mon, 8 Jan 2024 17:28:40 +0900</pubDate>
    </item>
    <item>
      <title>Nest.js Request Lifecycle 과 기본 아키텍처</title>
      <link>https://jake-seo-dev.tistory.com/720</link>
      <description>&lt;h2&gt;Nest.js 기본 아키텍처 살펴보기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;아래는 참고용 Request Lifecycle 아키텍처 그림이다.&lt;/li&gt;
&lt;li&gt;Controller, Service, Repository 가 요청 로직을 실제로 처리하는 부분이다.&lt;ul&gt;
&lt;li&gt;나머지는 없어도 동작을 하지만 요청 로직을 실제로 처리하는 부분은 반드시 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704527795431_1704527796429.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;h3&gt;미들웨어&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;request 와 resoponse 그리고 &lt;code&gt;next()&lt;/code&gt; 함수에 접근할 수 있는 함수를 구성할 때 사용된다.&lt;/li&gt;
&lt;li&gt;로깅이나 요청 검증 등에 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;가드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;인증 및 권한 부여에 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CanActivate&lt;/code&gt; 인터페이스를 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;인터셉터&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;메서드 실행 전 후에 추가 로직을 바인딩한다.&lt;/li&gt;
&lt;li&gt;메서드 결과를 변형한다.&lt;/li&gt;
&lt;li&gt;응답을 확장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;파이프&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;라우트 핸들러가 처리할 인수에 대해 작동한다.&lt;/li&gt;
&lt;li&gt;데이터 유효성 검사 혹은 변환에 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PipeTransform&lt;/code&gt; 인터페이스를 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;컨트롤러 (Controller)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Controller&lt;/code&gt; 데코레이터가 달린 클래스.&lt;/li&gt;
&lt;li&gt;인바운드 요청을 처리하고 응답을 생성한다.&lt;/li&gt;
&lt;li&gt;HTTP 컨트롤러는 HTTP 메서드와 경로에 따른 처리를 한다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nestjs.com/microservices/basics&quot;&gt;마이크로서비스 컨트롤러&lt;/a&gt;는 다양한 전송을 통해 실행되는 요청과 이벤트에 응답한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;필터&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;HTTP 요청을 실행하는 동안 발생하는 예외를 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExceptionFilter&lt;/code&gt; 인터페이스를 구현하여 사용자 정의 예외 필터를 정의 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;모듈 (Module)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Module&lt;/code&gt; 데코레이터가 달린 클래스.&lt;/li&gt;
&lt;li&gt;앱 구조를 스코프로 구성하는데 사용.&lt;/li&gt;
&lt;li&gt;컨트롤러와 프로바이더는 선언된 모듈에 의해 범위가 지정된다.&lt;/li&gt;
&lt;li&gt;모듈과 해당 클래스(Controller 와 Provider)는 Nest.js 가 의존성 주입을 수행하는 방식을 결정하는 그래프를 형성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;프로바이더 (Provider)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Injectable()&lt;/code&gt; 로 데코레이팅되어 Nest의 의존성 주입 시스템의 일부가 된다.&lt;/li&gt;
&lt;li&gt;Nest 에 내장된 의존성 주입 시스템 (DI System) 을 사용하여 생성자 매개변수 주입을 통해 다른 클래스에 주입할 수 있다.&lt;/li&gt;
&lt;li&gt;주입 시, 주입되는 클래스의 모듈 범위 내에 표시되어야 한다.&lt;ul&gt;
&lt;li&gt;프로바이더를 같은 모듈 스코프에 정의한다.&lt;/li&gt;
&lt;li&gt;한 모듈 스코프에서 프로바이더를 내보내고 해당 모듈을 주입되는 클래스의 모듈 스코프로 가져오기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Global()&lt;/code&gt; 데코레이터를 사용하여 글로벌로 표시된 모듈에서 프로바이더를 내보내기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서비스, 리포지토리, 팩토리, 헬퍼 등을 포괄하는 광범위한 카테고리다.&lt;/li&gt;
&lt;li&gt;컨트롤러와 다른 프로바이더에 프로바이더를 주입할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;의존성 주입&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Nest 는 강력한 의존성 주입 시스템을 사용하여 다양한 컴포넌트 간의 종속성을 정의한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;마이크로서비스&lt;/h3&gt;
&lt;p&gt;TCP, Redis, MQTT 와 같은 다양한 전송 계층을 사용하는 마이크로 서비스 아키텍처를 지원한다.&lt;/p&gt;</description>
      <category>프레임워크/Nest.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/720</guid>
      <comments>https://jake-seo-dev.tistory.com/720#entry720comment</comments>
      <pubDate>Sat, 6 Jan 2024 16:58:14 +0900</pubDate>
    </item>
    <item>
      <title>Nest.js 소개</title>
      <link>https://jake-seo-dev.tistory.com/719</link>
      <description>&lt;h2&gt;Nest.js 소개&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 로 만듦&lt;/li&gt;
&lt;li&gt;차세대 Typescript 를 이용&lt;/li&gt;
&lt;li&gt;HTTP 서버 프레임워크인 Express 를 사용, Fastify 로도 사용 가능&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nestjs.com/first-steps#platform&quot;&gt;공식 문서&lt;/a&gt;에서는 모든 노드 HTTP 프레임워크에서 작동한다고 한다.&lt;/li&gt;
&lt;li&gt;기본은 &lt;code&gt;@nestjs/platform-express&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아키텍처 설계 문제를 해결하기 위해 나옴&lt;/li&gt;
&lt;li&gt;서버 아키텍처 때문에 테스트하기 쉽고, 디커플링 잘되고, 유지보수 편함&lt;/li&gt;
&lt;li&gt;결국 Express + 아키텍처&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;초기 파일 구성&lt;/h2&gt;
&lt;p&gt;아래 디렉토리 구성은 각 모듈을 전용 디렉토리에 저장하는 관례를 따르기 위해 초기에 잡힌다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt; 디렉토리 내부&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main.ts&lt;/code&gt;: 엔트리 파일 역할. 코어 함수인 &lt;code&gt;NestFactory&lt;/code&gt; 를 사용하여 Nest Application Instance 를 만듦&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.service.ts&lt;/code&gt;: 하나의 메서드가 있는 기본 서비스&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.module.ts&lt;/code&gt;: 애플리케이션의 루트 모듈, 의존성을 관리하는 파일.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.controller.ts&lt;/code&gt;: 하나의 라우트가 들어있는 기본 컨트롤러&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.controller.spec.ts&lt;/code&gt;: 컨트롤러에 대한 유닛 테스트들&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt; 디렉토리 내부&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jest-2e2.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.e2e-spec.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;main.ts 살펴보기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;create()&lt;/code&gt; 메서드는 &lt;code&gt;INestApplication&lt;/code&gt; 인터페이스를 충족하는 애플리케이션 오브젝트를 만든다.&lt;/li&gt;
&lt;li&gt;앱을 만들다 오류가 발생하면 앱은 코드 1과 함께 종료된다. 오류를 발생시키고 싶으면 &lt;code&gt;abortOnError&lt;/code&gt; 옵션을 활성화해야 한다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NestFactory.create(AppModule, { abortOnError: false })&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { NestFactory } from &amp;quot;@nestjs/core&amp;quot;;
import { AppModule } from &amp;quot;./app.module&amp;quot;;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}

bootstrap();&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;앱 시작 방법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;기본적으론 &lt;code&gt;npm run start&lt;/code&gt; 명령어로 실행할 수 있다.&lt;/li&gt;
&lt;li&gt;개발 프로세스 속도를 높이려면 &lt;code&gt;npm run start -- -b swc&lt;/code&gt; 명령어로 실행하면 SWC builder 를 사용하여 더 빨라진다고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;린팅과 포맷팅 (Linting and Formatting)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Nest.js 는 기본 &lt;a href=&quot;https://www.npmjs.com/package/eslint&quot;&gt;eslint&lt;/a&gt; 와 &lt;a href=&quot;https://www.npmjs.com/package/prettier&quot;&gt;prettier&lt;/a&gt; 패키지를 사용한다.&lt;/li&gt;
&lt;li&gt;아래 명령어를 통해서 실행 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Lint and autofix with eslint
$ npm run lint

# Format with prettier
$ npm run format&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;공식문서&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.nestjs.com/&quot;&gt;https://docs.nestjs.com/&lt;/a&gt;&lt;/p&gt;</description>
      <category>프레임워크/Nest.js</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/719</guid>
      <comments>https://jake-seo-dev.tistory.com/719#entry719comment</comments>
      <pubDate>Sat, 6 Jan 2024 16:57:41 +0900</pubDate>
    </item>
    <item>
      <title>이벤트 루프는 어떻게 non-blocking 연산을 막힘없이 처리하는가?</title>
      <link>https://jake-seo-dev.tistory.com/718</link>
      <description>&lt;h2&gt;자바스크립트 이벤트 루프 파헤치기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이를 이해하기 위해서는 자바스크립트 런타임 환경에서의 추가적인 컴포넌트의 역할을 알아야 한다.&lt;/li&gt;
&lt;li&gt;추가적인 컴포넌트란 Web APIs (브라우저에서) 나 C++ APIs (Node.js) 를 말하며 이를 알아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;자바스크립트 런타임 환경 컴포넌트 4가지&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Heap: 메모리 할당이 이루어지는 곳&lt;/li&gt;
&lt;li&gt;Stack: 스택 프레임 (함수 호출) 이 저장되고 관리되는 곳&lt;/li&gt;
&lt;li&gt;Task Queue: 비동기 작업의 콜백이 여기에 대기함&lt;/li&gt;
&lt;li&gt;Event Loop: 호출 스택과 작업의 대기열을 확인하는 오케스트레이터&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Non-Blocking I/O 는 어떻게 동작하는가?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;비동기 호출: HTTP 요청, 타이머, Node.js 의 파일 I/O 와 같은 비동기 함수가 호출되면 Javascript 는 이 작업을 자체 스레드 밖인 브라우저나 Node.js 로 전송하여 환경에서 제공하는 다른 스레드에서 처리하도록 한다.&lt;/li&gt;
&lt;li&gt;계속 실행: 이런 연산이 비동기적으로 처리되는 동안 자바스크립트의 호출 스택은 연산이 완료될 때까지 기다리지 않고 다른 코드를 계속 실행할 수 있다. 이를 논 블로킹이라고 한다.&lt;/li&gt;
&lt;li&gt;콜백 및 작업 대기열: 비동기 작업이 완료되면 해당 콜백 함수는 작업 대기열에 배치된다.&lt;/li&gt;
&lt;li&gt;이벤트 루프의 역할: 지속적으로 호출 스택을 확인하며, 호출 스택이 비어 있으면 이벤트 루프는 작업 대기열에서 첫번째 콜백을 가져와 호출 스택에 푸시한다.&lt;ul&gt;
&lt;li&gt;호출 스택이 비어있다는 것은 모든 동기식 코드가 실행을 완료했다는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;예제 시나리오&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;아래 예제 시나리오 코드를 실행하면 &lt;code&gt;Sync job&lt;/code&gt; 이 먼저 출력된 뒤 비동기 호출인 &lt;code&gt;Async job&lt;/code&gt; 이 출력된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setTimeout()&lt;/code&gt; 내부에 있는 콜백은 비동기 작업으로 처리된다.&lt;/li&gt;
&lt;li&gt;자바스크립트는 별도의 스레드에서 작업을 처리하는 브라우저 API 혹은 Node.js API 에 이를 전달한다.&lt;/li&gt;
&lt;li&gt;동기 코드는 기다리지 않고 계속 실행된다.&lt;/li&gt;
&lt;li&gt;동기 코드 실행이 끝나면, 이벤트 루프는 호출 스택이 비어있음을 확인하고 작업 대기열에서 첫번째 콜백을 가져와 호출 스택에 푸시한다.&lt;/li&gt;
&lt;li&gt;마침내 비동기 코드가 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;setTimeout(() =&amp;gt; {
  console.log(&amp;quot;Async job&amp;quot;);
});

console.log(&amp;quot;Sync job&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;주요 내용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이벤트 루프: 이벤트 루프는 비동기 연산을 오프로드하고 콜백이 실행되는 시점을 관리하여 논블로킹 I/O 를 가능하게 한다.&lt;/li&gt;
&lt;li&gt;자바스크립트의 역할: 싱글 스레드로 유지된다.&lt;ul&gt;
&lt;li&gt;비동기 연산을 처리하기 위한 멀티 스레드 동작을 제공하는 것은 런타임 환경이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;효율성: 이러한 설계 덕분에 자바스크립트는 많은 수의 입출력 작업을 효율적으로 처리할 수 있고, 이는 웹서버와 같은 네트워크 집약적 애플리케이션에서 특히 유리하다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>자바스크립트/개념</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/718</guid>
      <comments>https://jake-seo-dev.tistory.com/718#entry718comment</comments>
      <pubDate>Thu, 4 Jan 2024 23:11:16 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트의 싱글 스레드 모델과 이벤트 루프</title>
      <link>https://jake-seo-dev.tistory.com/717</link>
      <description>&lt;h2&gt;싱글 쓰레드 모델과 Event Loop&lt;/h2&gt;
&lt;p&gt;논블로킹과 Event Loop 에 대한 간단한 설명&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Blocking 요청과 Non-Blocking 요청에 대한 처리가 다름&lt;/li&gt;
&lt;li&gt;Non-Blocking 요청이 들어왔을 때는 이벤트 루프에 담아서 처리함&lt;/li&gt;
&lt;li&gt;Blocking 요청일 때는 워커 스레드에게 맡김&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;자바스크립트의 싱글 스레드 모델&lt;/h2&gt;
&lt;h3&gt;작동 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;싱글 스레드: 한 번에 하나의 연산을 단일 시퀀스로 처리함&lt;/li&gt;
&lt;li&gt;실행 컨텍스트: 자바스크립트 코드가 실행되면, &amp;quot;실행 컨텍스트 (Execution Context)&amp;quot; 에서 실행함. 실행 컨텍스트는 코드가 실행되는 순서를 처리함.&lt;/li&gt;
&lt;li&gt;호출 스택 (Call Stack): 실행 컨텍스트 관리를 위해 콜스택을 사용함. 콜스택은 프로그램에서 현재 위치를 기록하는 데이터 구조로 사용됨. 가장 최근에 호출된 함수가 가장 상위에 있음. 함수가 완료되면, 스택에서 제거되고 프로그램은 중단된 부분부터 실행함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;단순성: 교착 상태(Deadlock)나 경쟁 상태(Race Condition) 같은 복잡한 문제 처리가 필요 없음. 프로그래밍 모델을 단순화 가능.&lt;ul&gt;
&lt;li&gt;그래도 CPU 를 사용하는 작업은 여전히 이벤트 루프에서 실행되어 병목 현상이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;최신 버전의 Node.js 는 실험적 기능으로 작업자 스레드(Worker Threads) 라는 것을 도입하여 일정 수준의 멀티 스레딩을 허용한다. 이를 이용하면 자체적인 복잡성이 수반된다.&lt;/li&gt;
&lt;li&gt;또, Node.js 는 CPU 집약적인 작업을 처리하기 위해서 자식 프로세스를 생성할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일관성: 실행 순서의 일관성을 보장. 이는 웹앱의 UI 및 상태 관리에 중요함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;작업 차단: 한 번에 한가지 작업만 수행 가능해서 오래 걸리는 작업이 메인 스레드를 차단해 UI가 응답하지 않아 사용자 환경이 저하될 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;자바스크립트의 이벤트 루프&lt;/h2&gt;
&lt;h3&gt;작동 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;논 블로킹 I/O: 자바스크립트는 싱글 스레드임에도 불구하고 non-blocking I/O 연산을 수행할 수 있음. 이는 자바스크립트가 비동기 콜백을 사용할 수 있게 해주는 이벤트 루프를 통해 이뤄짐.&lt;/li&gt;
&lt;li&gt;이벤트 루프 매커니즘: 이벤트 루프는 호출 스택이 비어있는지 지속적으로 확인함. 비어 있으면 메세지 큐에서 실행을 기다리는 보류 중인 콜백이 있는지 확인. 대기 중인 콜백이 있으면 콜백을 제거하여 호출 스택으로 푸시한 다음에 실행.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;비동기 동작: 자바스크립트가 메인 스레드를 차단하지 않고, 웹 API 요청과 같은 긴 I/O 작업을 비동기적으로 수행할 수 있게 해줌.&lt;/li&gt;
&lt;li&gt;성능 향상: 이벤트 루프의 non-blocking 특성으로 인해 웹앱의 성능이 향상되고 UX 가 좋아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;디버깅의 복잡성: 비동기 코드는 비선형적인 실행 흐름을 가져 이해와 디버깅이 어려울 수 있다.&lt;/li&gt;
&lt;li&gt;콜백 지옥의 가능성: 콜백에 대한 의존도가 높으면 콜백 지옥을 겪을 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>자바스크립트/개념</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/717</guid>
      <comments>https://jake-seo-dev.tistory.com/717#entry717comment</comments>
      <pubDate>Thu, 4 Jan 2024 23:10:41 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트에서 머신 코드 대신 바이트 코드를 사용하는 이유</title>
      <link>https://jake-seo-dev.tistory.com/716</link>
      <description>&lt;h2&gt;V8 Engine 이 Machine Code 대신 Byte Code 를 사용한 이유&lt;/h2&gt;
&lt;h3&gt;이식성 (Portability)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;바이트 코드는 머신 코드보다 이식성이 뛰어남&lt;/li&gt;
&lt;li&gt;머신 코드는 프로세스 아키텍처에 따라 다르므로 CPU 마다 다를 수 있음&lt;/li&gt;
&lt;li&gt;바이트 코드는 호환되는 모든 머신이 있는 모든 머신에서 실행 가능&lt;/li&gt;
&lt;li&gt;JS 를 다양한 기기와 운영체제에서 실행할 수 있게 해줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;보안&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;머신 코드보다 바이트 코드로 먼저 컴파일하면 추상화 계층이 늘어나 보안이 강화될 수 있음&lt;/li&gt;
&lt;li&gt;머신별 취약점을 악용하기 어려워짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;최적화 기회 (Optimization Opportunities)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;바이트 코드로 컴파일한 다음 실행하며 핫 경로(자주 실행되는 코드)를 최적화하여 머신 코드로 컴파일하는 JIT 이라는 기술을 사용함&lt;ul&gt;
&lt;li&gt;JIT 을 통해 V8 은 정적으로 예측하기 어려운 런타임 동작을 기반으로 코드를 최적화 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;리소스 효율성 (Resource Efficiency)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;머신 코드로 직접 컴파일하는 것은 CPU 와 메모리 집약적임&lt;/li&gt;
&lt;li&gt;바이트 코드를 사용하면 초기 오버헤드를 줄이며 JS 를 빠르게 컴파일하고 실행할 수 있어 모바일 기기와 같이 리소스가 제한된 환경에서 성능이 향상됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;반복적 개발 (Iterative Development)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JS 가 사용되는 웹 환경에선 코드가 자주 수정되고 자주 다시 실행되는 특성이 있음&lt;/li&gt;
&lt;li&gt;바이트 코드는 컴파일 속도가 머신 코드보다 빨라 이점이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;자바스크립트의 동적 특성 (Dynamic Nature of Javascript)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JS 는 런타임에 변경될 수 있는 평가, 동적 타이핑, 객체 형태 같은 기능을 갖춘 매우 동적인 언어임&lt;/li&gt;
&lt;li&gt;동적인 언어를 효율적인 머신 코드로 직접 컴파일하긴 쉽지 않음&lt;/li&gt;
&lt;li&gt;바이트 코드가 이러한 동적 측면을 유리하게 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;디버깅 및 툴링 (Debugging and Tooling)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;바이트 코드는 디버깅 및 툴링과 관련해 작업하기가 더 쉬움&lt;/li&gt;
&lt;li&gt;바이트코드는 일반적으로 머신 코드보다 가독성과 이해도가 더 높기 때문에 개발자가 디버깅할 때 필수적&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이식성, 보안, 최적화 기회, 리소스 효율성, 반복적 개발, JS 의 동적 특성, 디버깅 및 툴링의 이유가 있음&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>자바스크립트/개념</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/716</guid>
      <comments>https://jake-seo-dev.tistory.com/716#entry716comment</comments>
      <pubDate>Thu, 4 Jan 2024 21:48:33 +0900</pubDate>
    </item>
    <item>
      <title>TS033. 타입스크립트 제네릭(Generic) 기본 사용법</title>
      <link>https://jake-seo-dev.tistory.com/715</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Generic 함수에서 사용하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제네릭은 타입 안정성을 유지하면서 재사용 가능하고 일관된 코드를 작성하도록 도와주는 기능이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제네릭 기본 사용법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수명 옆에 &amp;lt;타입변수명&amp;gt; 을 명시하여 어떤 타입을 사용할지 명시한다.&lt;/li&gt;
&lt;li&gt;해당 &amp;lt;타입변수명&amp;gt;을 매개변수 타입이나 반환 타입에서 이용할 수 있다.&lt;/li&gt;
&lt;li&gt;호출할 때는 함수명 옆에 타입변수명이 아닌 사용할 타입명을 명시하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;function foo&amp;lt;T&amp;gt;(value: T): T {
  return value;
}

// 호출할 때는 함수명 옆에 타입변수명이 아닌 사용할 타입명을 명시하면 된다.
foo&amp;lt;string&amp;gt;(&quot;abc&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;여러개의 제네릭 타입 사용하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;,&lt;/code&gt; 를 이용하여 몇개든 이어주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;function bar&amp;lt;Type1, Type2, Type3&amp;gt;(a: Type1, b: Type2, c: Type3) {
  return {
    a,
    b,
    c,
  };
}

bar&amp;lt;number, string, boolean&amp;gt;(10, &quot;10&quot;, true);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제네릭이 해결하는 문제: 타입에 특화된 함수를 일반화&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션에서 작성한 함수의 타입을 일반화하여 같은 로직을 여러 타입에 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제네릭 적용 전&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;number 에도 이 로직을 적용하고 string 에도 적용하고 싶어서 두가지 타입의 중복된 함수를 선언한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function firstNumber(arr: number[]): number {
  return arr[0];
}

function firstString(arr: string[]): string {
  return arr[0];
}

// 각 타입에 대해 다른 함수를 사용한다.
let numbers1 = [1, 2, 3];
let firstNum1 = firstNumber(numbers1); // 숫자라 firstNumber 를 사용

let strings1 = [&quot;hello&quot;, &quot;world&quot;];
let firstStr1 = firstString(strings1); // 문자라 firstString 을 사용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제네릭 적용 후&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 타입에서 작동할 수 있는 단일 함수를 작성할 수 있다.&lt;/li&gt;
&lt;li&gt;숫자나 문자열과 같은 구체적인 타입을 지정하는 대신 T로 표시 되는 타입 변수를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function first&amp;lt;T&amp;gt;(arr: T[]): T {
  return arr[0];
}

// 각 타입에 대해 같은 함수를 사용한다.
let numbers2 = [1, 2, 3];
let firstNum2 = first(numbers2); // T is number

let strings2 = [&quot;hello&quot;, &quot;world&quot;];
let firstStr2 = first(strings2); // T is string&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제네릭을 사용하는 이유&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재사용성: 함수나 클래스를 한번 작성하고 다른 유형에 계속 재활용이 가능하다.&lt;/li&gt;
&lt;li&gt;타입 안정성 유지: 타입스크립트가 여전히 타입을 검사하고 잘못된 타입에 오류를 표기해준다.&lt;/li&gt;
&lt;li&gt;유연성: 타입 안정성을 희생하지 않고도 코드를 더 유연하게 작성 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;class Car {
  brand: string;
  carName: string;

  constructor(brand: string, codeName: string) {
    this.brand = brand;
    this.carName = codeName;
  }
}

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클래스 생성자를 제네릭으로 적용하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성자의 타입 시그니처 표현 방법이 &lt;code&gt;new (매개변수): 반환 타입&lt;/code&gt; 이었던 것을 기억하자.&lt;/li&gt;
&lt;li&gt;모든 클래스 인스턴스가 사실 타입스크립트와 자바스크립트 관점에선 객체(&lt;code&gt;{}&lt;/code&gt;)였음을 상기해보면 편하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extends&lt;/code&gt; 이후 &lt;code&gt;new (...args: any[]): {}&lt;/code&gt; 는 결국 모든 클래스의 생성자 시그니처를 품는다고 생각하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;function instantiator&amp;lt;T extends { new (...args: any[]): {} }&amp;gt;(
  constructor: T,
  ...args: any[]
) {
  return new constructor(...args);
}

console.log(instantiator(Car, &quot;현대&quot;, &quot;소나타&quot;));
console.log(instantiator(Person, &quot;Jake Seo&quot;, 10));&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/715</guid>
      <comments>https://jake-seo-dev.tistory.com/715#entry715comment</comments>
      <pubDate>Mon, 1 Jan 2024 19:07:28 +0900</pubDate>
    </item>
    <item>
      <title>TS032. 타입스크립트의 Visibility 키워드들</title>
      <link>https://jake-seo-dev.tistory.com/714</link>
      <description>&lt;h2&gt;Visibility Keyword&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;자바스크립트엔 없고 타입스크립트에만 존재하는 개념이다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public&lt;/code&gt;: (기본값) 어디서든 접근 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected&lt;/code&gt;: 현재 클래스 및 하위 클래스에서 접근 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt;: 현재 클래스 내에서 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Class {
  public property1 = 1;
  protected property2 = 2;
  private property3 = 3;
  #property4 = 4; // 자바스크립트에서 지원하는 private property

  // 클래스 내부 메서드 접근 테스트
  method() {
    this.property1; // 접근 가능
    this.property2; // 접근 가능
    this.property3; // 접근 가능
    this.#property4; // 접근 가능
  }
}

class ChildClass extends Class {
  // 하위 클래스 내부 메서드 접근 테스트
  method() {
    this.property1; // 접근 가능
    this.property2; // 접근 가능
    this.property3; // 접근 불가
    this.#property4; // 접근 불가
  }
}

const childInstance = new ChildClass();
childInstance.property1; //접근 가능
childInstance.property2; //접근 불가
childInstance.property3; //접근 불가
childInstance.#property4; //접근 불가&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/714</guid>
      <comments>https://jake-seo-dev.tistory.com/714#entry714comment</comments>
      <pubDate>Mon, 1 Jan 2024 02:36:18 +0900</pubDate>
    </item>
    <item>
      <title>TS031. 타입스크립트의 Abstract Class</title>
      <link>https://jake-seo-dev.tistory.com/713</link>
      <description>&lt;h2&gt;Abstract Class&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;보통 OOP 에서 추상 클래스는 클래스의 형태만 제공하고 실제 인스턴스화는 불가능한 클래스이다.&lt;/li&gt;
&lt;li&gt;상속하여 인스턴스화 가능한 클래스를 만들 때 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;일반 사용법&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;abstract class AbstractClass {
  id: number;

  constructor(id: number) {
    this.id = id;
  }
}

// 인스턴스화를 시도할 시에 에러가 난다.
const abstract = new AbstractClass(123);

// 보통 이렇게 상속을 하여 쓴다.
class Class extends AbstractClass {}

// 당연히 상속한 클래스는 인스턴스화가 가능하다.
const class1 = new Class(100);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;abstract method 와 함께 사용하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;abstract method 는 시그니처만 정의하는 메서드다.&lt;/li&gt;
&lt;li&gt;구현은 상속받은 클래스에서 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;abstract class AbstractWithAbstractMethod {
  abstract abstract_method(): string;
}

class Class2 extends AbstractWithAbstractMethod {
  abstract_method(): string {
    return &amp;quot;HELLO&amp;quot;;
  }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/713</guid>
      <comments>https://jake-seo-dev.tistory.com/713#entry713comment</comments>
      <pubDate>Mon, 1 Jan 2024 02:28:01 +0900</pubDate>
    </item>
    <item>
      <title>TS030. 타입스크립트 클래스에서 메서드, 속성 오버라이딩</title>
      <link>https://jake-seo-dev.tistory.com/712</link>
      <description>&lt;h2&gt;메서드 오버라이드&lt;/h2&gt;
&lt;p&gt;메서드 오버라이드는 아래의 규칙만 따르면 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부모 메서드와 반환 타입이 일치해야 한다.&lt;/li&gt;
&lt;li&gt;부모 메서드에 필수인 파라미터들이 존재해야 한다.&lt;/li&gt;
&lt;li&gt;부모 메서드에 없는 파라미터가 자식 메서드에서 필수로 지정되면 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Car {
  drive(name: string) {
    return &amp;quot;부릉부릉&amp;quot;;
  }
}

// 1번 규칙 위배
class Sonata1 extends Car {
  drive(name: string) {
    return true;
  }
}

// 2번 규칙 위배
class Sonata2 extends Car {
  drive(name: number) {
    return &amp;quot;부릉부릉&amp;quot;;
  }
}

// 3번 규칙 위배
class Sonata3 extends Car {
  drive(name: string, turbo: boolean) {
    return &amp;quot;부릉부릉&amp;quot;;
  }
}

// 정상 케이스
class SonataGood extends Car {
  drive(name: string, turbo?: boolean) {
    if (turbo) {
      return `${name}, 부릉부릉 터보.`;
    }

    return `${name}, 부릉부릉.`;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;속성 오버라이드&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;속성의 오버라이드는 타입을 narrowing 하는 경우에 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Car2 {
  name: string | number;

  constructor(name: string | number) {
    this.name = name;
  }
}

class Sonata4 extends Car2 {
  name: string; // string | number -&amp;gt; string 으로 내로잉

  constructor(name: string) {
    super(name);
    this.name = name;
  }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/712</guid>
      <comments>https://jake-seo-dev.tistory.com/712#entry712comment</comments>
      <pubDate>Mon, 1 Jan 2024 01:33:46 +0900</pubDate>
    </item>
    <item>
      <title>TS029. 타입스크립트 클래스 상속</title>
      <link>https://jake-seo-dev.tistory.com/711</link>
      <description>&lt;h2&gt;클래스 상속&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;타입스크립트의 상속도 일반 자바스크립트 클래스 상속과 동일하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  hello() {
    return `안녕하세요. ${this.name}`;
  }
}

class Developer extends Person {
  language: string;

  constructor(name: string, language: string) {
    super(name);
    this.language = language;
  }

  develop() {
    return `${this.name} (이)가 개발을 시작하였습니다. 언어는 ${this.language} 입니다.`;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;부모 인스턴스와 자식 인스턴스 할당하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;자식 타입의 변수에 부모 타입 할당 불가능&lt;/li&gt;
&lt;li&gt;덜 구체적인 타입 (부모) 가 정보 없이 더 구체적인 타입 (자식) 이 되는 것은 불가능&lt;/li&gt;
&lt;li&gt;부모 타입의 변수에 자식 타입 할당 가능&lt;/li&gt;
&lt;li&gt;더 구체적인 타입 (자식) 이 덜 구체적인 타입 (부모) 가 되는 것은 가능&lt;/li&gt;
&lt;li&gt;여기까지는 거의 다른 OOP 언어와 동일하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;let parent1: Person = new Person(&amp;quot;Jake&amp;quot;);
let child1: Developer = new Developer(&amp;quot;Jake Seo&amp;quot;, &amp;quot;java&amp;quot;);
let parent2: Person = new Person(&amp;quot;Jake&amp;quot;);
let child2: Developer = new Developer(&amp;quot;Jake Seo&amp;quot;, &amp;quot;java&amp;quot;);

// 자식 타입의 변수에 부모 타입 할당 불가능
// 덜 구체적인 타입 (부모) 가 정보 없이 더 구체적인 타입 (자식) 이 되는 것은 불가능
child1 = parent1; // ERROR
// 부모 타입의 변수에 자식 타입 할당 가능
// 더 구체적인 타입 (자식) 이 덜 구체적인 타입 (부모) 가 되는 것은 가능
parent2 = child2; // 정상&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;타입스크립트 상속 할당이 조금 다른 점: 타입스크립트에서는 시그니처만 본다.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;이번엔 자식 부모 양쪽으로 할당이 가능하다.&lt;/li&gt;
&lt;li&gt;왜냐면, 타입스크립트는 오직 타입만 보기 때문에 시그니처가 같으면 같은 타입이다.&lt;/li&gt;
&lt;li&gt;age 가 옵셔널이기 때문에 구조가 같아지는 것이 가능&lt;/li&gt;
&lt;li&gt;보통 다른 언어에서는 이러한 기능을 지원하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Parent {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Child extends Parent {
  age?: number;

  constructor(name: string, age: number) {
    super(name);
    this.age = age;
  }
}

let parent3: Parent = new Parent(&amp;quot;Jake&amp;quot;);
let child3: Child = new Child(&amp;quot;Jake&amp;quot;, 10);

parent3 = child3; // 정상
child3 = parent3; // 정상&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/711</guid>
      <comments>https://jake-seo-dev.tistory.com/711#entry711comment</comments>
      <pubDate>Sun, 31 Dec 2023 22:27:56 +0900</pubDate>
    </item>
    <item>
      <title>TS028. 타입스크립트 인터페이스를 클래스에 사용하기</title>
      <link>https://jake-seo-dev.tistory.com/710</link>
      <description>&lt;h2&gt;클래스의 인터페이스 구현&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;타입스크립트에서 인터페이스를 일반적인 객체지향의 인터페이스 개념처럼 사용할 수 있다.&lt;ul&gt;
&lt;li&gt;클래스가 약속된 시그니처의 형태를 맞추는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;implements&lt;/code&gt; 키워드를 이용하여 인터페이스의 구현을 강제할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface Person {
  name: string;
  hello(): string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;implements Person&lt;/code&gt; 을 덧붙여서 인터페이스 구현을 선언했으므로 &lt;code&gt;name&lt;/code&gt; 과 &lt;code&gt;hello()&lt;/code&gt; 프로퍼티를 반드시 구현해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Worker implements Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  hello() {
    return `안녕. 난 ${this.name}.`;
  }

  work() {
    return `${this.name} 은(는) 일을 합니다.`;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;다중 인터페이스 구현&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;인터페이스는 몇개든 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface Walker {
  walk(): string;
}

// 인터페이스는 몇개든 구현 가능
class Jake implements Person, Walker {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  hello(): string {
    return `안녕, 난 ${this.name}`;
  }

  walk(): string {
    return `${this.name}. 걷는다.`;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;2개의 인터페이스를 intersection (&lt;code&gt;&amp;amp;&lt;/code&gt;) 을 통해 합친 타입을 통해서도 다중 인터페이스 구현이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type WalkingMan = Person &amp;amp; Walker;

class Jack implements WalkingMan {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  hello(): string {
    return `안녕, 난 ${this.name}`;
  }

  walk(): string {
    return `${this.name}. 걷는다.`;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;인터페이스의 프로퍼티가 중복될 때&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;다중 상속할 인터페이스가 중복된 프로퍼티 이름을 갖는 것을 주의해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface Student {
  id: string;
}

interface Resident {
  id: number;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;primitive 타입을 두개 intersection 하면 &lt;code&gt;never&lt;/code&gt; 타입이 되기 때문에 가능한 타입이 없다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;any&lt;/code&gt; 로 지정하면 어찌저찌 되긴 하는데 타입스크립트를 쓰는 의미가 사라진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Troi implements Student, Resident {
  id: string | number; // ERROR: 둘 중 어떤 쪽이 되어도 한 쪽을 만족하지 못함
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;인터페이스로 생성자 표현하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;인터페이스로 생성자를 표현하고 싶을 땐, &lt;code&gt;new&lt;/code&gt; 키워드를 이용하면 된다.&lt;/li&gt;
&lt;li&gt;반환 타입은 정의한 클래스 타입을 그대로 입력해주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Developer {
  name: string;
  language: string;

  constructor(name: string, language: string) {
    this.name = name;
    this.language = language;
  }
}

interface DeveloperConstructor {
  new (name: string, language: string): Developer;
}

function createDeveloper(
  constructor: DeveloperConstructor,
  name: string,
  language: string
) {
  return new constructor(name, language);
}

console.log(createDeveloper(Developer, &amp;quot;jake&amp;quot;, &amp;quot;java&amp;quot;));&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/710</guid>
      <comments>https://jake-seo-dev.tistory.com/710#entry710comment</comments>
      <pubDate>Sun, 31 Dec 2023 22:06:46 +0900</pubDate>
    </item>
    <item>
      <title>2023년 전체 회고</title>
      <link>https://jake-seo-dev.tistory.com/709</link>
      <description>&lt;h2&gt;2023년 전체 회고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;특별한 내용이나 반성해야 할 점, 개선해야 할 점 보다는 그냥 말 그대로 어떤 일이 있었는지만 적어보려 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;트위터를 시작했다.&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;태어나서 처음으로 SNS 에 여러가지 글을 써봤다.&lt;/li&gt;
&lt;li&gt;최초의 팔로워는 &amp;#39;넬송이&amp;#39; 님이었는데 현재는 계정을 삭제하시거나 비활성화 계정으로 돌리신 것 같다.&lt;/li&gt;
&lt;li&gt;개발자 계정으로 활동해야지 하고 마음을 먹었으나 나중엔 일상의 잡다한 이야기를 올리는 것이 더 재밌다고 생각되어 여러가지 이야기를 올리고 있다.&lt;ul&gt;
&lt;li&gt;사실 이와 관련되어 깨달은 점이 아주 많은데 회고록에 다 적기에는 너무 길다.&lt;/li&gt;
&lt;li&gt;핵심만 짚자면, 남의 눈치는 아주 안 봐서도 안되며 아주 많이 봐서도 안된다는 것이다. 적당히 눈치보면서 살자.&lt;ul&gt;
&lt;li&gt;적정지점을 찾는 것은 물론 매우 어려운 일이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재는 약 1년만에 3000명이 넘는 분이 팔로우를 해주셨다.&lt;/li&gt;
&lt;li&gt;사람들과 소통하는 것이 적적하고 외로웠던 내 삶에 큰 힘이 되는 것 같다.&lt;/li&gt;
&lt;li&gt;SNS 를 통해 사람들을 실제로 만나기도 했다. 좋은 분들도 많이 알게 됐다. 대단한 분들도 엄청나게 많다.&lt;/li&gt;
&lt;li&gt;하지만 SNS 에서 나오는 도파민은 역시 무섭다.&lt;/li&gt;
&lt;li&gt;어느새 Notification 창을 새로고침하는 나를 발견했을 때 그렇게 느꼈다.&lt;/li&gt;
&lt;li&gt;결국 모든 알람을 껐고, 현재는 나름대로 통제하면서 즐기는 중이다.&lt;/li&gt;
&lt;li&gt;나에게 좋은 영향력을 주었던 모든 사람들에게 너무 감사하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704008178500_1704008179734.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;2022년 10월 첫 팔로워가 생긴 모습이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;여자친구와 헤어졌다.&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2년간 교제하던 여자친구와 4월달에 헤어졌다.&lt;/li&gt;
&lt;li&gt;회자정리 거자필반이라는 말은 귀로 들었을 때는 그냥 그러려니 했는데 나이가 들어감에 따라 몸으로 느낀다.&lt;/li&gt;
&lt;li&gt;어릴 땐 모든 인연을 붙잡으려고 애를 썼는데 요즘은 보내줄 인연을 잘 보내주는 게 진짜 건강한 것이라고 생각한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704008609103_1704008609961.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;새 집으로 이사했다.&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;새 집은 방이 2개 있는 14평짜리 집으로 이전에 살던 집에 비해 꽤 넓다.&lt;/li&gt;
&lt;li&gt;새 집도 자잘한 문제는 있지만, 이전에 겪던 많은 문제들이 나타나지 않아 훨씬 삶의 질이 높아졌다.&lt;/li&gt;
&lt;li&gt;내가 돈을 버는 가장 큰 이유 중 하나는 역시 집을 사기 위해서인 것 같다.&lt;/li&gt;
&lt;li&gt;나만의 공간에서 보호받고 있다는 느낌이 나를 너무 안정적인 기분으로 만들고 나는 그 기분이 너무 좋다.&lt;/li&gt;
&lt;li&gt;세탁실이 꽤 넓어서 세탁기, 건조기 세트를 구매했는데 올해에 한 아주 만족스러운 소비 중 하나이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704009162643_1704009164078.png&quot; alt=&quot;picture 2&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704010058925_1704010060282.png&quot; alt=&quot;picture 4&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;새 건조기의 모습, 건조기는 어느정도 거거익선이 성립한다. 이불을 빨아야 하기 때문이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;책을 많이 읽게 되었다.&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;원래 1년에 1~2권도 잘 안 읽는데 어쩌다 보니 올해는 책을 많이 읽게 됐다.&lt;/li&gt;
&lt;li&gt;SNS 를 시작한 것과 시너지 효과가 생겨 SNS 에 리뷰도 나름대로 열심히 업로드했다.&lt;/li&gt;
&lt;li&gt;개발만 공부하다 다른쪽 지식을 공부해보니 엄청 재밌어서 사실 한동안 개발 공부는 좀 소홀했다.&lt;/li&gt;
&lt;li&gt;한번 책 읽는 재미를 알게 되니 앞으로도 종종 읽게 될 것 같다.&lt;/li&gt;
&lt;li&gt;출판사들에게 소소하게 책 협찬을 받기도 했다.&lt;ul&gt;
&lt;li&gt;협찬받은 책들도 내 취향에 맞는 것들이었어서 재밌게 읽었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;베스트를 꼽자면 &amp;#39;거인의 노트&amp;#39;, &amp;#39;사람은 어떻게 배우고 생각하고 기억하는가&amp;#39;, &amp;#39;생각에 관한 생각&amp;#39;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704010151713_1704010153629.png&quot; alt=&quot;picture 5&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;도둑맞은 집중력에는 재밌는 문장이 많았다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;달리게 되었다.&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;문득 살이 너무 많이 쪘다는 것을 느꼈다.&lt;/li&gt;
&lt;li&gt;20대 초반의 나는 54~55kg 정도로 너무 마른 체형이었다.&lt;/li&gt;
&lt;li&gt;20대 후반이 되면서 75kg 정도가 되었다.&lt;/li&gt;
&lt;li&gt;중간점에서 타협하고 싶어서 60~65kg 정도까지만 빼자고 마음을 먹었다.&lt;/li&gt;
&lt;li&gt;난 원래 걷는 걸 좋아해서 무작정 1시간 걸어야겠다고 마음먹고 걸었다.&lt;/li&gt;
&lt;li&gt;걷는 건 지방을 태우는데 효율이 그다지 좋지 않아서 뛰기 시작했다.&lt;/li&gt;
&lt;li&gt;7월인가 8월부터 격일로 10km 를 계속 뛰었다.&lt;/li&gt;
&lt;li&gt;65kg 까지 감량에 성공했다.&lt;/li&gt;
&lt;li&gt;뛰는 건 생각보다 도움되는 부분이 많다.&lt;ul&gt;
&lt;li&gt;머리도 좋게 만들어주고 체력 증진에도 도움이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;뛰는 건 생각보다 주의할 부분도 많다.&lt;ul&gt;
&lt;li&gt;제대로 자세를 개선해나가며 뛰지 않으면 무릎이 아프고 허리에도 안 좋을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과만 놓고 보자면 뛰길 잘했다. 새로운 취미가 하나 더 생겼다.&lt;/li&gt;
&lt;li&gt;러닝크루도 가입하긴 했지만 역시 정신적인 안정을 가져오는 건 혼자 달리기이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704009929598_1704009930839.png&quot; alt=&quot;picture 3&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;달리기에 관련된 책도 한권 읽게 됐는데 재밌었다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;치밀하게 메모하게 되었다&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;말 그대로 메모가 좀 더 치밀해졌다.&lt;/li&gt;
&lt;li&gt;메모 자체에 대한 공부를 아주 많이 했다.&lt;/li&gt;
&lt;li&gt;무언가 까먹지 않기 위해 하는 게 메모인줄만 알았는데 부수적인 효과가 아주 많았다.&lt;ul&gt;
&lt;li&gt;일기를 쓰면 나 자신에 대해 더욱 잘 알게 된다.&lt;ul&gt;
&lt;li&gt;쓰지 않으면 알지 못하는 나의 모습이 참 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;뇌에 있던 지식을 다시 정리하는 과정이 되기도 한다.&lt;/li&gt;
&lt;li&gt;성취감을 주는 도구가 되기도 한다.&lt;/li&gt;
&lt;li&gt;허무함에 빠지지 않게 하는 도구가 되기도 한다.&lt;/li&gt;
&lt;li&gt;인생을 조금 더 재밌게 만들기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;회사에서 다양한 내부활동에 참여했다.&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;사실 내가 진짜 회사일을 엄청 열심히 했냐고 내 자신에게 묻는다면 답은 정말 &amp;#39;아니오&amp;#39; 이다.&lt;/li&gt;
&lt;li&gt;난 아직도 내 집중력을 통제하는데 어려움을 겪고 있고, 마음은 항상 열심히 하려고 하지만 잘 실행이 안되는 타입이다.&lt;/li&gt;
&lt;li&gt;그래도 나름대로 애썼고 어느정도 할만큼은 했다.&lt;/li&gt;
&lt;li&gt;인공지능 학습조직에 참여해서 월급 외에 여러가지 추가적인 수당을 받기도 했다.&lt;ul&gt;
&lt;li&gt;학습조직에서 발표했던 내용이 반응이 좋아 사내강연을 추가적으로 진행하기도 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;회사에서 민폐는 되지 않도록 노력하고 있는데, 상황이 어려운건지 내가 그만큼의 능력이 안되는건지 조금 힘들다. 내년은 좀 더 잘해봐야지.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1704010469609_1704010471306.png&quot; alt=&quot;picture 6&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;회사에서 간이 포상금을 받기도 했다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;내 생각에 2024년에 내가 개선해야 할 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;목적을 가지고 공부하기&lt;ul&gt;
&lt;li&gt;단순히 &amp;#39;스프링을 배운다&amp;#39; &amp;lt; 이런 것보단 &amp;#39;배워서 xx를 적용하겠다&amp;#39; 와 같이 변경&lt;/li&gt;
&lt;li&gt;이렇게 공부하는게 효율이 훨씬 좋았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;감정이 격해지는 것 더 주의하기&lt;ul&gt;
&lt;li&gt;이건 마음가짐으로 되는 것보다는 평소에 잠을 잘자고 쓸데없는 걱정을 줄이는게 가장 효과가 큰 것 같다.&lt;/li&gt;
&lt;li&gt;그래도 역시 인지를 하고 있는 편이 인지가 없는 편보단 훨씬 나으니 인지를 하고 있자.&lt;/li&gt;
&lt;li&gt;나는 감정이 격해지면 평소에 발휘하던 능력의 50% 도 힘든 사람인 것 같다는 것을 깨달아서 더 그렇다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;트위터 적당히하기&lt;ul&gt;
&lt;li&gt;특히 의미 없이 Notifications 를 새로고침하는 일은 그만두자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;개선할 점은 많은 인간이지만 막상 생각나는 게 없으니 여기서 마치자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>회고/연간 회고</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/709</guid>
      <comments>https://jake-seo-dev.tistory.com/709#entry709comment</comments>
      <pubDate>Sun, 31 Dec 2023 17:22:49 +0900</pubDate>
    </item>
    <item>
      <title>TS027. 타입스크립트에서의 클래스 선언 방식</title>
      <link>https://jake-seo-dev.tistory.com/708</link>
      <description>&lt;h2&gt;타입스크립트에서의 클래스 선언&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;자바스크립트와 동일하다.&lt;/li&gt;
&lt;li&gt;단, 타입스크립트에서 클래스를 이용할 때는 초기화 등을 실수로 까먹는 일을 방지할 수 있다.&lt;ul&gt;
&lt;li&gt;컴파일 타임에 에러를 통해 알려주기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만들어진 클래스는 당연히 타입으로 이용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  hello() {
    return `안녕 내 이름은 ${this.name} 이고, 나이는 ${this.age} 살이야.`;
  }
}

const person: Person = new Person(&amp;quot;Jake Seo&amp;quot;, 10);

// 자바스크립트에서는 존재하지 않는 메서드를 실행해도 에러가 나지 않는데,
// 타입스크립트에서는 이런 부분들을 잘 짚어준다.
person.anyFunction(); // ERROR: property anyFunction does not exist.&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Readonly 프로퍼티 이용하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;변화하면 안되는 프로퍼티에 &lt;code&gt;readonly&lt;/code&gt; 를 부여할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Person {
  readonly name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  hello() {
    return `안녕 내 이름은 ${this.name} 이고, 나이는 ${this.age} 살이야.`;
  }
}

const person: Person = new Person(&amp;quot;Jake Seo&amp;quot;, 10);

// ERROR: Can not assign to read-only property.
person.name = &amp;quot;jack&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;다양한 프로퍼티 초기화 방법&lt;/h2&gt;
&lt;p&gt;초기화 하지 않는 방법이 2가지 있고, 초기화하지 않아도 되는 프로퍼티를 생성하는 방법이 2가지 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;생성자 초기화&lt;/li&gt;
&lt;li&gt;선언과 동시에 초기화&lt;/li&gt;
&lt;li&gt;Optional 로 초기화 하지 않아도 되는 프로퍼티 생성&lt;/li&gt;
&lt;li&gt;type of undefined 로 초기화 하지 않아도 되는 프로퍼티 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Person {
  // 생성자 초기화
  name: string;
  // 선언과 동시에 초기화
  age: number = 33;
  // Optional: 초기화를 해도 되고 안 해도 됨
  nickname?: string;
  // type of undefined: 초기화를 해도 되고 안 해도 됨
  nationality: string | undefined;

  constructor(name: string) {
    this.name = name;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;! 기호를 이용해 초기화 됐음을 알려주기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; 기호는 특수한 상황에 쓰이는데, 타입스크립트가 초기화를 인식하지 못하는 곳에서 초기화가 일어나는 경우 사용자가 직접 타입스크립트 컴파일러에 초기화됐음을 알리는데 사용한다.&lt;/li&gt;
&lt;li&gt;이를테면 &lt;code&gt;constructor()&lt;/code&gt; 메서드가 아닌 다른 내부 메서드에서 초기화가 진행된다면 &lt;code&gt;!&lt;/code&gt; 기호가 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class SeoPerson {
  name!: string;

  constructor(name: string) {
    this.initSeoPerson(name);
  }

  initSeoPerson(name: string) {
    this.name = name + &amp;quot; Seo&amp;quot;;
  }

  hello() {
    return `안녕 내 이름은 ${this.name} 야.`;
  }
}

const seoPerson: SeoPerson = new SeoPerson(&amp;quot;Jake&amp;quot;);
console.log(seoPerson.hello());&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;타입과 값으로서의 클래스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;클래스는 타입이 될 수도 있고 값이 될 수도 있다.&lt;/li&gt;
&lt;li&gt;자바스크립트에서 클래스는 타입이 될 수 있는데 이 타입은 특정 시그니처의 객체라고 보면 된다.&lt;ul&gt;
&lt;li&gt;그래서 타입을 변경하지 않고 같은 시그니처의 객체로 재할당도 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  hello() {
    return `안녕. 난 ${this.name}.`;
  }
}

// 클래스이자 타입이 된다.
let person: Person = new Person(&amp;quot;Jake Seo&amp;quot;);

// 실제 자바스크립트에서는 객체와 클래스가 같기 때문에 시그니처만 같으면 객체로 재할당이 가능하다.
person = {
  name: &amp;quot;Jake Seo&amp;quot;,
  hello() {
    return `안녕. 난 ${this.name}.`;
  },
};&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/708</guid>
      <comments>https://jake-seo-dev.tistory.com/708#entry708comment</comments>
      <pubDate>Sun, 31 Dec 2023 10:56:21 +0900</pubDate>
    </item>
    <item>
      <title>TS026. 타입스크립트 오브젝트의 Key-Value Mapping 알아보기</title>
      <link>https://jake-seo-dev.tistory.com/707</link>
      <description>&lt;h2&gt;타입스크립트의 키 값 매핑 (Key-Value Mapping)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;타입스크립트엔 키-값 매핑이라는 개념이 존재한다.&lt;/li&gt;
&lt;li&gt;특정 타입의 키와 다른 타입의 값을 통해 타입을 정의할 수 있는 강력한 기능이다.&lt;/li&gt;
&lt;li&gt;일반적으로 아래와 같은 문법을 띈다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface MyObject {
  [key: KeyType]: ValueType;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;예제: 키-값 매핑으로 이전 타입 재활용하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;아래와 같이 &lt;code&gt;User&lt;/code&gt; 타입, &lt;code&gt;Post&lt;/code&gt; 타입, &lt;code&gt;ApiResponse&lt;/code&gt; 타입을 정의했다.&lt;/li&gt;
&lt;li&gt;여기서 &lt;code&gt;UserApiResponse&lt;/code&gt; 라는 더 구체적인 타입을 새로 정의해야 하는데, 이 때 키-값 매핑을 활용하여 &lt;code&gt;ApiResponse&lt;/code&gt; 에서 정의했던 내용을 재활용 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type User = {
  name: string;
  age: number;
  nickname: string;
  password: string;
};

type Post = {
  title: string;
  content: string;
};

type ApiResponse = {
  user: User;
  users: User[] | undefined;
  admin: User | null;
  posts: Post[];
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;재활용을 함으로써 갖는 이점은 &lt;code&gt;ApiResponse&lt;/code&gt; 의 형태가 변했을 때 재활용한 &lt;code&gt;UserApiResponse2&lt;/code&gt; 에도 동일한 변화가 가해진다는 것이다.&lt;/li&gt;
&lt;li&gt;DRY 원칙을 위배하지 않는 타입스크립트 코딩이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 재활용 안하는 버전
type UserApiResponse1 = {
  user: User;
  users: User[] | undefined;
  admin: User | null;
};

// 재활용 하는 버전
type UserApiResponse2 = {
  user: ApiResponse[&amp;quot;user&amp;quot;];
  users: ApiResponse[&amp;quot;users&amp;quot;];
  admin: ApiResponse[&amp;quot;admin&amp;quot;];
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;루핑(Looping) 이용하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;위에 키값을 하나씩 입력하던 것과 같다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;in&lt;/code&gt; 을 이용해 키를 looping 하는 것은 실전에서 아주 많이 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type UserApiResponse3 = {
  [k in &amp;quot;user&amp;quot; | &amp;quot;users&amp;quot; | &amp;quot;admin&amp;quot;]: ApiResponse[k];
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;유틸리티 타입 (Utility Types) 이용하기&lt;/h2&gt;
&lt;h3&gt;Pick 이용하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;위에서 &lt;code&gt;in&lt;/code&gt; 키워드를 통해 키값을 하나씩 입력하던 것과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type PickedUserApiResponse = Pick&amp;lt;ApiResponse, &amp;quot;user&amp;quot; | &amp;quot;admin&amp;quot; | &amp;quot;users&amp;quot;&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Omit 이용하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;제외할 것 빼고 다 가져올 때 유용하다.&lt;/li&gt;
&lt;li&gt;그러나 &lt;code&gt;Pick&lt;/code&gt; 과 달리 나중에 원본에 무언가 추가됐을 때 유의해야 할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type OmitUserApiResponse = Omit&amp;lt;ApiResponse, &amp;quot;posts&amp;quot;&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;keyof 키워드를 통해 객체의 키값들을 유니온으로 가져오기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;객체의 키 값을 모두 union 으로 가져올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type AllKeys = keyof ApiResponse;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1703957840661_1703957841480.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;AllKeys&lt;/code&gt; 를 타입으로 한 &lt;code&gt;key&lt;/code&gt; 가 어떻게 자동완성되는지 볼 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;keyof 를 통해 루핑 돌아보기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type UserApiStatus4 = {
  [k in keyof ApiResponse]: ApiResponse[k];
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;keyof 에서 하나만 제외하기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type UserApiResponse5 = {
  [k in Exclude&amp;lt;keyof ApiResponse, &amp;quot;posts&amp;quot;&amp;gt;]: ApiResponse[k];
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;전부 옵셔널로 만들기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type UserApiResponse6 = {
  [k in Exclude&amp;lt;keyof ApiResponse, &amp;quot;posts&amp;quot;&amp;gt;]?: ApiResponse[k];
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;유니온 타입의 key-value mapping 을 이용한 예시&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;유니온 타입의 key-value mapping 을 하면 결과도 union 을 건 것과 동일하다.&lt;/li&gt;
&lt;li&gt;단순히 union 을 이용하기보다 의미론적인 결과를 도출할 때 유용할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface Person {
  code: string;
  age: number;
}

interface Company {
  code: number;
  location: string;
}

type Sponsor = Person | Company;
type SponsorCode = Sponsor[&amp;quot;code&amp;quot;]; // string | number 의 union 타입이 된다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1703958157195_1703958158049.png&quot; alt=&quot;picture 1&quot;&gt;&lt;/p&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/707</guid>
      <comments>https://jake-seo-dev.tistory.com/707#entry707comment</comments>
      <pubDate>Sun, 31 Dec 2023 02:44:08 +0900</pubDate>
    </item>
    <item>
      <title>TS025. 타입스크립트 옵셔널 vs  Type | Undefined 의 차이</title>
      <link>https://jake-seo-dev.tistory.com/706</link>
      <description>&lt;h2&gt;Optional vs Or undefined&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;옵셔널의 타입 추론이 &lt;code&gt;number | undefined&lt;/code&gt; 로 되는데, 그러면 실제로 &lt;code&gt;number | undefined&lt;/code&gt; 라고 쓰는 것과 무슨 차이일까?&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 옵셔널 이용
interface Person1 {
  name: string;
  age?: number;
}

// 옵셔널 이용하지 않음
interface Person2 {
  name: string;
  age: number | undefined;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;아래 보면 추론된 타입이 &lt;code&gt;number | undefined&lt;/code&gt; 인 것을 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/n00nietzsche/posting_cdn@main/images/pic_1703944901801_1703944902577.png&quot; alt=&quot;picture 0&quot;&gt;&lt;/p&gt;
&lt;h2&gt;결과&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;옵셔널을 이용하면 프로퍼티 입력 자체를 생략 가능하다.&lt;/li&gt;
&lt;li&gt;옵셔널을 이용하지 않고 &lt;code&gt;number | undefined&lt;/code&gt; 를 이용하면 &lt;code&gt;undefined&lt;/code&gt; 를 입력하더라도 무조건 입력을 해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const person1: Person1 = {
  name: &amp;quot;김똘똘&amp;quot;,
};

const person2: Person2 = {
  name: &amp;quot;김똘똘&amp;quot;,
  age: undefined,
};&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Typescript</category>
      <author>Jake Seo</author>
      <guid isPermaLink="true">https://jake-seo-dev.tistory.com/706</guid>
      <comments>https://jake-seo-dev.tistory.com/706#entry706comment</comments>
      <pubDate>Sat, 30 Dec 2023 23:03:12 +0900</pubDate>
    </item>
  </channel>
</rss>