<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>lucky-jun.log</title>
        <link>https://velog.io/</link>
        <description>Lucky한 프론트 개발자 🍀 natcho9010@gmail.com</description>
        <lastBuildDate>Wed, 06 Nov 2024 04:54:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>lucky-jun.log</title>
            <url>https://velog.velcdn.com/images/lucky-jun/profile/2ae54645-c9ee-4bcf-9ae0-3220ba0780b7/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. lucky-jun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lucky-jun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Local LLM with LangChain]]></title>
            <link>https://velog.io/@lucky-jun/Local-LLM-with-LangChain</link>
            <guid>https://velog.io/@lucky-jun/Local-LLM-with-LangChain</guid>
            <pubDate>Wed, 06 Nov 2024 04:54:25 GMT</pubDate>
            <description><![CDATA[<p>LLM API를 이용해 원하는 기능을 만들고 사용해보고 싶지만 항상 비용이 걱정됩니다.</p>
<p>비용 문제도 해결하고 직접 학습도 할 수 있는 매력적인 Local LLM(LLaMa3)과 함께 LangChain의 주요 내용들을 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/99829f6d-9cf5-4b24-8d56-f3c911269114/image.png" alt=""></p>
<h1 id="langchain">LangChain</h1>
<p>ChatGPT를 필두로 한 LLM은 굉장히 유용한 툴입니다.</p>
<p>하지만, 아직은 사용자가 수많은 정보들을 정확하게 입력해줘야 원하는 결과를 얻을 수 있습니다.</p>
<p>context와 입출력 format을 더 자세히 제공할 수록 더 빠르게 구체적이고 좋은 결과를 줍니다.</p>
<p>또한, 얻은 결과는 사람들이 직접 가공하여 사용하여야 합니다.</p>
<p>여기서 LangChain이라는 툴이 유용하게 사용될 수 있습니다. LLM이 뇌라면 LangChain은 LLM의 손, 발, 눈, 코, 입이 되어줍니다.</p>
<p><a href="https://python.langchain.com/docs/introduction/">LangChain</a></p>
<h1 id="local-llm-실행-wollama">Local LLM 실행 (w.Ollama)</h1>
<p>LangChain 사용에 앞서 로컬에서도 LLM을 사용할 수 있는 방법을 알아보겠습니다.</p>
<h2 id="ollama">Ollama</h2>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/91b30d83-9cfe-43c7-b8fe-01044fa25c45/image.png" alt=""></p>
<p><strong>LLM을 로컬에서 실행할 수 있도록 도와주는 툴입니다.</strong> cli 툴로 개발자들에게 특히 유용하게 사용될 수 있는 툴입니다.</p>
<p><a href="https://ollama.com/">Ollama</a></p>
<ul>
<li>Ollama에 등록된 LLM을 로컬에 pull 받습니다.</li>
</ul>
<pre><code class="language-shell">$ ollama pull llama3</code></pre>
<ul>
<li>Ollama를 실행합니다.</li>
</ul>
<p>default로 <code>http://127.0.0.1:11434</code>에 ollama와 통신할 수 있는 API가 열립니다.</p>
<pre><code class="language-shell">$ ollama serve</code></pre>
<ul>
<li>curl로 ollama와 통신, LLM과 대화</li>
</ul>
<p>open된 ollama 서버에 요청을 보내고, 결과를 받습니다.</p>
<p><strong>요청시 ollama가 가지고 있는 LLM model을 지정해줘야 합니다.</strong></p>
<pre><code class="language-shell">$ curl http://localhost:11434/api/generate -d &#39;{
   &quot;model&quot;: &quot;llama3&quot;,
   &quot;prompt&quot;: &quot;Hello LLaMA!&quot;
}&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/09e11f07-4926-4ce4-bdd6-38cc0b404361/image.gif" alt=""></p>
<ul>
<li>혹은 langchain_ollama를 활용해 통신, LLM과 대화</li>
</ul>
<p>curl이외에 LangChain에서 제공하는 ollama 통신 모듈을 활용해 통신할 수도 있습니다.</p>
<pre><code class="language-python"># Python
from langchain_ollama.llms import OllamaLLM

# Ollama 통신 객체 생성 및 초기화
llm = OllamaLLM(model=&quot;llama3&quot;)

# prompt와 함께 Ollama의 LLM과 통신
prompt = &quot;Hello LLaMA!&quot;
response = llm.invoke(prompt)
# 결과 출력
print(f&#39;{response=}&#39;)</code></pre>
<h1 id="template--parser">Template &amp; Parser</h1>
<p>기본적으로 LLM의 입력과 출력은 Text 혹은 Image와 같이 컴퓨터가 온전히 이해할 수 있는 형태의 정보들을 받거나 출력할 수 있습니다.</p>
<p>즉, Python의 Dictionary나 Array같은 프로그래밍 언어 만의 자료구조 형태로는 입력과 출력을 할 수 없다는 뜻입니다.</p>
<p>따라서, <strong>LLM이 이해할 수 있는 형태로 입력을 변환하여 주고, LLM이 출력한 결과를 다시 활용하기 좋은 형태로 변환해주는 작업이 필요합니다.</strong></p>
<p>LangChain에선 <strong>입력은 Prompt Template</strong>, <strong>출력은 Output parser</strong>의 형태로 이를 제공합니다.</p>
<p>(현 포스팅에선 Text 형태의 LLM 입,출력만 다룹니다.)</p>
<h2 id="prompt-template">Prompt Template</h2>
<p>LLM 입력으로 제공할 prompt Template를 만들 수 있습니다.</p>
<p>특히, 입력 설명, 출력 format 설정 및 설명, 역할 설정 그리고 출력 예시들(shots)과 같이 개별 대화마다 필요한 설명들(System Prompt)처럼 반복되는 부분을 템플릿화하여 사용할 수 있습니다.</p>
<pre><code class="language-python">from langchain_community.llms import Ollama
from langchain_core.prompts import PromptTemplate

prompt_template = PromptTemplate.from_template(&quot;&quot;&quot;
You are a software assistant, especially dealt with python and TypeScript(+ node.js).
Answer the following questions with the best of your knowledge and explanation in readable format like indented bullet points with clean codes.

{question}
&quot;&quot;&quot;)
# prompt 완성
prompt = prompt_template.invoke({ &quot;question&quot;: &quot;What is the difference between Python and TypeScript?&quot; })

# LLM에 prompt 전달
llm = Ollama(model=&quot;llama3&quot;)
llm.invoke(prompt)
print(f&#39;{response=}&#39;)</code></pre>
<p>LangChain에는 PromptTemplate이외에도 ChatPromptTemplate, FewShotPromptTemplate 등이 다양한 prompt template이 있습니다.</p>
<p>목적에 맞게 사용해주시면 됩니다.</p>
<h2 id="output-parser">Output parser</h2>
<p>LLM의 출력을 런타임에서도 다루기 쉬운 형태로 변환해주는 작업이 필요합니다.</p>
<p>이번 포스팅에선 PydanticOutputParser를 사용해보겠습니다.</p>
<pre><code class="language-python"># string 형태의 LLM 출력을 파싱하기 위한 OutputParser
from langchain_core.output_parsers import PydanticOutputParser

class Joke(BaseModel):
    setup: str = Field(description=&quot;The setup to the joke&quot;)
    punchline: str = Field(description=&quot;The punchline to the joke&quot;)

parser = PydanticOutputParser(pydantic_object=Joke)

template = &quot;Answer the user query.\n{format_instructions}\n{query}&quot;
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt])

prompt = chat_prompt.invoke({
    &quot;query&quot;: &quot;What is a really funny joke about Python programming?&quot;,
    &quot;format_instructions&quot;: parser.get_format_instructions()
})
print(f&#39;{prompt=}&#39;)</code></pre>
<p>프롬프트가 아래와 같이 준비됩니다.</p>
<pre><code class="language-text">Answer the user query.
The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {&quot;properties&quot;: {&quot;foo&quot;: {&quot;title&quot;: &quot;Foo&quot;, &quot;description&quot;: &quot;a list of strings&quot;, &quot;type&quot;: &quot;array&quot;, &quot;items&quot;: {&quot;type&quot;: &quot;string&quot;}}}, &quot;required&quot;: [&quot;foo&quot;]}
the object {&quot;foo&quot;: [&quot;bar&quot;, &quot;baz&quot;]} is a well-formatted instance of the schema. The object {&quot;properties&quot;: {&quot;foo&quot;: [&quot;bar&quot;, &quot;baz&quot;]}} is not well-formatted.

Here is the output schema:</code></pre>
<p>{&quot;properties&quot;: {&quot;setup&quot;: {&quot;description&quot;: &quot;The setup to the joke&quot;, &quot;title&quot;: &quot;Setup&quot;, &quot;type&quot;: &quot;string&quot;}, &quot;punchline&quot;: {&quot;description&quot;: &quot;The punchline to the joke&quot;, &quot;title&quot;: &quot;Punchline&quot;, &quot;type&quot;: &quot;string&quot;}}, &quot;required&quot;: [&quot;setup&quot;, &quot;punchline&quot;]}</p>
<pre><code>What is a really funny joke about Python programming?</code></pre><p>프롬프트를 LLM에 전달하고, 결과를 파싱합니다.</p>
<pre><code class="language-python">result = llm.invoke(prompt)
print(f&#39;{result=}&#39;)

try:
    joke_object = parser.parse(result)
    print(joke_object.setup)
    print(joke_object.punchline)
except Exception as e:
    print(e)</code></pre>
<p>LLM이 준 결과가 아래와같이 나옵니다.</p>
<pre><code class="language-text">Here&#39;s the output:

```json
{
    &quot;properties&quot;: {
      &quot;setup&quot;: {
        &quot;description&quot;: &quot;The setup to the joke&quot;,
      &quot;title&quot;: &quot;Setup&quot;,
      &quot;type&quot;: &quot;string&quot;
    },
    &quot;punchline&quot;: {
        &quot;description&quot;: &quot;The punchline to the joke&quot;,
      &quot;title&quot;: &quot;Punchline&quot;,
      &quot;type&quot;: &quot;string&quot;
    }
  },
  &quot;required&quot;: [&quot;setup&quot;, &quot;punchline&quot;],
  &quot;setup&quot;: &quot;Why do programmers prefer dark mode so much?&quot;,
  &quot;punchline&quot;: &quot;Because light attracts bugs.&quot;
}</code></pre>
<pre><code>
아래와같이 파싱되어 나옵니다.

```python
print(joke_object.setup)
print(joke_object.punchline)

# Why do programmers prefer dark mode so much?
# Because light attracts bugs.</code></pre><h1 id="lcellangchain-expression-language">LCEL(LangChain Expression Language)</h1>
<p>프롬프트, 모델, 출력 파서 등의 구성 요소를 파이프 연산자( | )를 사용해서 단일 체인으로 구성하는 LangChain에서 지원하는 표현 방식입니다.</p>
<p>연속적인 함수 실행엔 메소드 체이닝 방법도 있지만, 그보다 훨씬 가독성이 좋은 방법이라고 할 수 있습니다.</p>
<p><strong>주의할 점으로 LCEL 파이프라인 시작시 랭체인에서 제공하는 함수 중, 파이프라인에 사용할 수 있는 함수를 사용해 시작해야합니다.</strong></p>
<pre><code class="language-python">from langchain_core.runnables import RunnableLambda

# 단일 파이프라인, + 1 -&gt; * 2 순서로 실행
sequence = RunnableLambda(lambda x: x + 1) | (lambda x: x * 2)
print(sequence.invoke(1)) # 4

# 병렬 파이프라인, + 1 -&gt; * 2, * 5 순서로 실행
sequence2 = RunnableLambda(lambda x: x + 1) | {
  &#39;mul_2&#39;: RunnableLambda(lambda x: x * 2),
  &#39;mul_5&#39;: RunnableLambda(lambda x: x * 5)
}
res = sequence2.invoke(1) # { &#39;mul_2&#39;: 4, &#39;mul_5&#39;: 10 }

# 파이프라인끼리 연결
sequence3 = sequence2 | RunnableLambda(lambda x: x[&#39;mul_2&#39;] + x[&#39;mul_5&#39;])
res = sequence3.invoke(1) # 14</code></pre>
<p>prompt 생성부터 출력까지의 과정을 LCEL로 표현해보겠습니다.</p>
<h2 id="lcel-scope">LCEL scope</h2>
<p>LCEL 파이프라인 구현시 주의해야할 점은 파이프라인 바로 전 단계의 출력의 결과만을 사용할 수 있다는 것 입니다.</p>
<p>대표적으로 <code>itemgetter</code> 함수와 <code>RunnablePassthrough</code>가 있습니다.</p>
<pre><code class="language-python">from langchain_core.runnables import RunnableLambda, RunnablePassthrough
from operator import itemgetter

def prompt_lambda(prompt: str) -&gt; str: # Fake LLM for the example
    return {&quot;prompt&quot;: prompt, &quot;with_system_prompt&quot;: f&#39;Answer the user query. query : {prompt}&#39;}

chain = RunnableLambda(prompt_lambda) | {
    # RunnablePassthrough는 앞 pipeline의 output을 그대로 받을 수 있다.
    &#39;original&#39;: RunnablePassthrough(), # Original LLM output
    &#39;system_prompt&#39;: itemgetter(&#39;with_system_prompt&#39;) # &#39;Answer the user query. query : {prompt}&#39;
} | RunnablePassthrough()

res = chain.invoke(&quot;What is the difference between Python and TypeScript?&quot;)
print(f&#39;{res=}&#39;)
# RunnablePassthrough는 바로 앞 pipeline의 output을 그대로 받아 출력한다.
# res=
# { &#39;original&#39;: {
#     &#39;prompt&#39;: &#39;What is the difference between Python and TypeScript?&#39;,
#     &#39;with_system_prompt&#39;: &#39;Answer the user query. query : What is the difference between Python and TypeScript?&#39;
#   },
#   &#39;system_prompt&#39;: &#39;Answer the user query.\nWhat is the difference between Python and TypeScript?&#39;
# }</code></pre>
<h2 id="lcel-with-llm">LCEL with LLM</h2>
<p>LLM과 함께 파이프라인을 구현해보겠습니다.</p>
<pre><code class="language-python">prompt1 = ChatPromptTemplate.from_template(&quot;what is the city {person} is from?&quot;)
prompt2 = ChatPromptTemplate.from_template(
    &quot;what country is the city {city} in? respond in {language}&quot;
)

model = OllamaLLM(model=&quot;llama3.1&quot;)

# LLM에서 받은 응답을 Parser로 변환해줘야 pipeline에서 사용할 수 있다.
chain1 = prompt1 | model | StrOutputParser()

chain2 = (
    # chain2 내부에 chain1을 이었다. 일종의 chain depth
    {&quot;city&quot;: chain1, &quot;language&quot;: itemgetter(&quot;language&quot;)}
    | prompt2
    | model
    | StrOutputParser()
)

res = chain2.invoke({&quot;person&quot;: &quot;obama&quot;, &quot;language&quot;: &quot;korean&quot;})
print(f&#39;{res=}&#39;)
# res=
# 오바마(Barrack Obama) 대통령의 생모지국은? 그것은 하와이(Hawaii)입니다.
# 그는 1961년 8월 4일 Honolulu, Hawaii에서 Kapi&#39;olani Medical Center for Women and Children에서 태어났습니다.
# 하지만 그의 고향 또는 성장한 곳은 일리노이주 시카고(Cicago, Illinois)입니다! 오바마는 대부분의 인생을 시카고에서 보냈으며, 그는 하와이 생모지국보다 시카고에 더 많은 친감을 느꼈습니다.</code></pre>
<h3 id="etc">etc...</h3>
<p>그 외 RemoteRunnable, RunnableParallel 등등... LCEL 파이프라인에서 사용가능한 다양한 Runnable이 있습니다.</p>
<h1 id="ref">ref</h1>
<ul>
<li><a href="https://www.samsungsds.com/kr/insights/the-concept-of-langchain.html">랭체인의 개념과 이해</a></li>
<li><a href="https://www.samsungsds.com/kr/insights/what-is-langchain.html">랭체인이란 무엇인가?</a></li>
<li><a href="https://velog.io/@kwon0koang/%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C-Llama3-%EB%8F%8C%EB%A6%AC%EA%B8%B0#-%EC%B0%B8%EC%A1%B0">랭체인(LangChain) 정리 (LLM 로컬 실행 및 배포 &amp; RAG 실습)</a></li>
<li><a href="https://ollama.com/">Ollama</a></li>
<li><a href="https://python.langchain.com/docs/introduction/">LangChain</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Finite State Machine (with XState)]]></title>
            <link>https://velog.io/@lucky-jun/Finite-State-Machine-with-XState</link>
            <guid>https://velog.io/@lucky-jun/Finite-State-Machine-with-XState</guid>
            <pubDate>Mon, 16 Sep 2024 08:58:57 GMT</pubDate>
            <description><![CDATA[<p>애플리케이션이 발전하다보면 수많은 state가 생기게 됩니다. 그러다보면 여러 state간의 관계가 생기고 복잡해집니다. 단순히 로직으로 이를 관리하다보면 코드가 복잡해지고 유지보수가 어려워집니다.</p>
<p>상태 하나가 추가되면 다른 상태들과의 관계를 고려해야하고, 그럼 상태 하나가 추가될 때마다 복잡도가 기하급수적으로 증가합니다.</p>
<p>이런 복잡한 문제를 깔끔하게 해결하기 위해, Finite State Machine을 사용해 보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/b9fd508d-e60e-439e-b5ed-96b4d29d283a/image.webp" alt="finite state machine"></p>
<h1 id="state-machine">State Machine</h1>
<p><strong>State machine(상태 머신)</strong>은 시스템이 가질 수 있는 모든 상태와 상태 간의 전환을 명확하게 모델링하는 개념입니다. 시스템은 <strong>주어진 순간에 하나의 상태만</strong> 가질 수 있으며, 특정 이벤트가 발생할 때 정의된 전환에 따라 다른 상태로 변경됩니다.</p>
<p>정리하면 상태 머신은 다음과 같은 특징을 가집니다.</p>
<ol>
<li>한번에 하나의 상태만 가질 수 있습니다.</li>
<li>상태(state): 시스템이 가질 수 있는 모든 상태들을 정의합니다.</li>
<li>이벤트(event): 상태 전이를 일으키는 트리거, 즉 상태 변화를 유도하는 조건입니다.</li>
<li>전이(transition): 이벤트가 발생했을 때 시스템이 현재 상태에서 다른 상태로 이동하는 과정을 말합니다.</li>
</ol>
<p>상태 머신은 일종의 프레임워크처럼 이벤트에 따른 상태를 명확하고 예측 가능하게 만들어, 복잡한 상태 관리를 단순화하는 데 유용합니다.</p>
<h2 id="finite-state-machine">Finite State Machine</h2>
<p>이번에 사용하는 state machine은 정확히는 Finite State Machine입니다.</p>
<p>여기서 Finite는 유한한 이라는 뜻으로, 상태가 유한하다는 것을 의미합니다.</p>
<p>위키에 FSM의 <a href="https://ko.wikipedia.org/wiki/%EC%9C%A0%ED%95%9C_%EC%83%81%ED%83%9C_%EA%B8%B0%EA%B3%84">정의</a>가 잘 정리돼있지만, 간단히 요약하면 <strong>제한된 구조와 제한된 상태들을 몇가지 제약으로 복잡하게 얽힌 흐름을 단순하고 직관적으로 표현하는 방법</strong>입니다.</p>
<p>반대로 Infinite라면 무한한 상태를 가지게 되는데, 현실적으로 무한한 상태를 단순하고 직관적으로 표현하는 것은 불가능합니다. 예시로 Finite State Machine을 쓰지 않은 날 것 그대로의 로직이 Infinite State Machine이라고 할 수 있을 것 같습니다.</p>
<p>때문에 애플리케이션에서는 보다 직관적이고 단순하게 상태의 흐름을 표현할 수 있는 Finite State Machine가 더 적합하다고 할 수 있습니다.</p>
<h1 id="xstate">XState</h1>
<p>state machine을 구현하는 방법엔 여라가지가 있지만, 이번엔 TypeScript 환경에서 <a href="https://stately.ai/docs">XState</a>라는 라이브러리를 사용했습니다. (V.5 버전을 기준으로 합니다.)</p>
<p>XState는 JS / TS로 작성된 라이브러리로 State Machine을 정의하고, 상태 전이를 관리하는 데 도움을 주는 라이브러리입니다.</p>
<p>굉장히 장점이 많은 라이브러리라고 생각합니다. 편리한 디버깅 툴(Inspector, XState VS Code extension), 정의한 상태 머신을 시각적으로 보여주고 테스트 해볼 수 있는 툴 <a href="https://stately.ai/viz">Stately Visualizer</a>, 풍부한 예시(Stately Studio)들이 있어 개발 편의성이 높습니다.</p>
<p>또한, React, Vue, Svelte 등의 UI 라이브러리 혹은 프레임워크들과도 쉽게 연동할 수 있습니다.</p>
<h1 id="example">Example</h1>
<p>XState로 간단한 Finite State Machine을 구현해보기 위해 간단한 예시를 만들어 구현해보았습니다.</p>
<h2 id="예시-시나리오">예시 시나리오</h2>
<p>예시는 결제 진행으로 크게 1. 결제 준비 2. 결제 진행 단계로 나뉩니다.</p>
<p>결제 준비 단계에선 비회원, 회원 상태를 가지고 있고, 비회원과 회원 상태에 따라 결제 준비 단계가 약간 다르게 진행됩니다.</p>
<p>그리고 결제 진행 단계에선 비동기 처리 상태에 따라 상태와 UI가 변경되고 결제가 완료되면 종료됩니다.</p>
<h3 id="demo">Demo</h3>
<p>간단히 어떤 예시인지 완성된 데모를 살펴보겠습니다.</p>
<table>
<thead>
<tr>
<th>non-member</th>
<th>member</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/lucky-jun/post/89a724db-2ac4-48af-99d6-1dfef445d217/image.gif" alt="non_member_example"></td>
<td><img src="https://velog.velcdn.com/images/lucky-jun/post/a7d10f5e-9449-48b5-bc2c-78050552dbc6/image.gif" alt="member_example"></td>
</tr>
</tbody></table>
<h2 id="상태-머신이-없을-때">상태 머신이 없을 때</h2>
<p>먼저 상태 머신이 없을 때의 코드를 살펴보겠습니다.</p>
<p>상태 머신이 상태를 관리하지 않기 때문에, 우선 상태를 선언해야 합니다. 필요한 상태는 다음과 같습니다.</p>
<pre><code class="language-tsx">  // 회원인가 아닌가?
  const [isMember, setIsMember] = useState(false);

  // 동의절차는 정의된 순서 그대로 순차적으로 진행됩니다.
  type AgreeProcess = &#39;user-info-input&#39; | &#39;user-info-consent&#39; | &#39;payment-consent&#39; | &#39;payment&#39;;
  // 동의 절차를 하나씩 순차적으로 하나씩 추가해서 보여줘야 함.
  const [agreeProcess, setAgreeProcess] = useState&lt;AgreeProcess | undefined&gt;();</code></pre>
<p>비회원이면 정보 입력을 위한 입력창을 띄워줍니다. 입력이 완료되면 회원과 동일한 동의 절차를 시작합니다.</p>
<p>여기서 현재 state에 따라서 다음 절차를 파악하는 등, 동의 절차 state를 컴포넌트에서 모두 수동으로 관리해야합니다.</p>
<pre><code class="language-tsx">  // 회원인 경우와 아닌 경우 시작하는 동의 절차 상태가 다름
  &lt;Button onClick={() =&gt; {
    // 현재 상태에 따라 느슨하게 연결된 상태 변경 로직
    if (isMember) {
      setAgreeProcess(&#39;user-info-consent&#39;);
    } else {
      setAgreeProcess(&#39;user-info-input&#39;);
    }
  }}&gt;
    결제를 진행합니다.
  &lt;/Button&gt;</code></pre>
<pre><code class="language-tsx">  // 상태에 따른 동의 절차 컴포넌트 추가
  const agreeComponents: ReactNode[] = [];

  if (!isMember) {
    agreeComponents.push(
      &lt;InputContainer
        key=&quot;user-info-input&quot;
        onClick={() =&gt; {
          setAgreeProcess(&#39;user-info-consent&#39;);
        }}
      /&gt;
    );
  }

  if (agreeProcess === &#39;user-info-consent&#39;) {
    agreeComponents.push(
      &lt;div key=&quot;user-info-consent&quot;&gt;
        &lt;FormControlLabel
        control={
          &lt;Checkbox
            onChange={(e) =&gt; {
              const { checked } = e.target;
              if (checked) {
                setAgreeProcess(&#39;payment-consent&#39;);
              }
            }}
            color=&quot;primary&quot;
          /&gt;
        }
        label=&quot;사용자 정보에 동의하시겠습니까?&quot;
        /&gt;
      &lt;/div&gt;
    );
  }

  // 동일하게 진행...</code></pre>
<p>결제 동의 절차가 끝나고 본격적으로 결제 요청이 시작되는 비동기 작업의 경우 아래와 같은 코드로 관리됩니다.</p>
<pre><code class="language-tsx">// 결제 요청 여부
  const [isFetched, setIsFetched] = useState(false);
  // 비동기 결제를 요청하고 3초를 client에서 스스로 측정해서 보여줘야 함.
  const [isLoading, setIsLoading] = useState(false);</code></pre>
<p>동일하게 모든 상태 변화를 native 로직을 통해 관리합니다.</p>
<pre><code class="language-tsx">  const handlePaymentButtonClick = useCallback(async () =&gt; {
    // 모든 상태 변화를 수동으로 관리
    setIsFetched(true);

    const loadingTimer = setTimeout(() =&gt; {
      setIsLoading(true);
    }, 3000);

    try {
      await mockPaymentFetching(() =&gt; {
        clearTimeout(loadingTimer);
        setIsFetched(false);
        setIsLoading(false);
      });
    } catch {
      clearTimeout(loadingTimer);
      setIsFetched(false);
      setIsLoading(false);
    }
  }, []);

  // 결제 시작 버튼
  &lt;Button variant=&#39;contained&#39; onClick={() =&gt; {
    handlePaymentButtonClick();
  }}&gt;결제 시작&lt;/Button&gt;

  // 결제 진행 상태 표시 모달
  &lt;Dialog open={isFetched}&gt;
    &lt;Box&gt;
      &lt;CircularProgress /&gt;
      {isLoading ? &lt;div style={{ color: &#39;red&#39; }}&gt;잠시만 기다려 주세요...&lt;/div&gt; : &lt;div&gt;결제중...&lt;/div&gt;}
    &lt;/Box&gt;
  &lt;/Dialog&gt;</code></pre>
<p>상위 코드는 모든 상태관리 로직과 View가 한 곳에 모여있어 가독성 및 관리가 어렵습니다.</p>
<p>또한, 엄격한 규칙없이 native 로직으로 느슨하게 관리하다보면, 코드의 의도를 알기도 어렵고 이로인해 잘못된 코드 수정으로 버그가 발생할 여지가 많아져 유지보수가 어려워집니다. (협업의 어려움 가중)</p>
<h2 id="상태-머신을-사용할-때">상태 머신을 사용할 때</h2>
<p>이제 XState를 사용하여 상태 머신을 구현하고 상위 코드를 리팩토링해보겠습니다.</p>
<p>먼저 결제 동의 절차를 위한 상태 머신을 정의합니다.</p>
<pre><code class="language-tsx">export const paymentMachine = createMachine({
  id: &#39;payment&#39;,
  initial: &#39;Init&#39;,
  context: {
    // 이 사람이 회원인지 아닌지 기억한다.
    isMember: false,
  },
  states: {
    // !! : 명확한 상태 순서 정의
    Init: {
      on: {
        next: [
          {
            // 회원일 때만 유저 동의 절차로 이동
            guard: (context) =&gt; context.context.isMember,
            target: &#39;UserInfoConsent&#39;,
          },
          {
            // 비회원일 땐 유저 정보 입력으로 이동
            guard: (context) =&gt; !context.context.isMember,
            target: &#39;UserInfoInput&#39;,
          },
        ],
        stay: {
          target: &#39;Init&#39;,
        },
        setMember: {
          actions: assign({
            isMember: ({ context, event }) =&gt; event?.isMember ?? context.isMember
          }),
        }
      },
    },
    UserInfoInput: {
      on: { next: &#39;UserInfoConsent&#39; },
    },
    UserInfoConsent: {
      on: { next: &#39;PaymentConsent&#39; },
    },
    PaymentConsent: {
      on: { next: &#39;Payment&#39; },
    },
    Payment: {
      type: &#39;final&#39;,
    },
  },
});</code></pre>
<p>이제 상태 머신을 컴포넌트에 적용해보겠습니다.</p>
<pre><code class="language-tsx">  import { useMachine } from &#39;@xstate/react&#39;;

  // !! : 필요한 여러 상태들이 컴포넌트에서 보이지 않음.
  const [snapshot, send] = useMachine(paymentMachine, {});

  const curState = snapshot.value;
  const isMember = snapshot.context.isMember;

  // 상태에 따른 동의 절차 컴포넌트 추가
  const agreeComponents: ReactNode[] = [];

  if (!isMember) {
    agreeComponents.push(
      &lt;InputContainer
        key=&quot;user-info-input&quot;
        onClick={() =&gt; {
          console.log(&#39;click&#39;)
          send({ type: &#39;next&#39; })
        }}
      /&gt;
    );
  }

  if (curState === &#39;UserInfoConsent&#39;) {
    agreeComponents.push(
      &lt;div key=&quot;user-info-consent&quot;&gt;
        &lt;FormControlLabel
        control={
          &lt;Checkbox
            color=&quot;primary&quot;
            onChange={(e) =&gt; {
              const { checked } = e.target;
              if (checked) {
                // !!: 추상화 된 상태 머신 변환 메소드
                send({ type: &#39;next&#39; });
              }
            }}
          /&gt;
        }
        label=&quot;사용자 정보에 동의하시겠습니까?&quot;
        /&gt;
      &lt;/div&gt;
    );
  }

  // 동일하게 진행...</code></pre>
<p>결제 요청 비동기 작업의 경우는 아래와 같이 변경됩니다.</p>
<pre><code class="language-tsx">// 로딩표시를 위한 상태 머신으로 항상 재사용할 수 있도록 설계
// 머신 종료 상태인 type: final을 적용하면 상태 머신 재사용이 힘듦.
export const paymentLoadingMachine = createMachine({
  id: &#39;payment&#39;,
  initial: &#39;Init&#39;,
  states: {
    Init: {
      on: { next: &#39;PaymentSend&#39; },
    },
    PaymentSend: {
      on: { next: &#39;Init&#39; },
      after: {
        // !! : 내부적인 상태 전환을 컴포넌트에서 해줄 필요가 없음.
        3000: &#39;Loading&#39; // 3000ms (3초) 후 Loading으로 자동 전환
      },
    },
    Loading: {
      on: { next: &#39;Init&#39; },
    }
  }
});</code></pre>
<p>상태 머신을 컴포넌트에 적용합니다.</p>
<pre><code class="language-tsx">  // 결제 요청 했다고 가정하는 비동기 함수
  async function mockPaymentFetching() {
    return new Promise((resolve) =&gt; {
      setTimeout(() =&gt; {
        console.log(&#39;done&#39;);
        resolve(&#39;done&#39;);
      }, 5000);
    });
  }

  const [paymentLoadingSnapshot, paymentLoadingSend] = useMachine(paymentLoadingMachine, {});

  const isFetched = paymentLoadingSnapshot.value !== &#39;Init&#39;;
  const isLoading = paymentLoadingSnapshot.value === &#39;Loading&#39;;
  const isDialogOpen = isFetched;

  const handlePaymentButtonClick = useCallback(async () =&gt; {
    if (isFetched) return;

    // !! : 훨씬 간결해진 코드
    paymentLoadingSend({ type: &#39;next&#39; });
    try {
      await mockPaymentFetching();
      paymentLoadingSend({ type: &#39;next&#39; });
    } catch {
      paymentLoadingSend({ type: &#39;next&#39; });
    }
  }, [isFetched, paymentLoadingSend]);

  &lt;Button variant=&#39;contained&#39; onClick={() =&gt; {
    handlePaymentButtonClick();
  }}&gt;결제 시작&lt;/Button&gt;

  &lt;Dialog open={isDialogOpen}&gt;
    &lt;Box&gt;
      &lt;CircularProgress /&gt;
      {isLoading ? &lt;div style={{ color: &#39;red&#39; }}&gt;잠시만 기다려 주세요...&lt;/div&gt; : &lt;div&gt;결제중...&lt;/div&gt;}
    &lt;/Box&gt;
  &lt;/Dialog&gt;</code></pre>
<p>상태 머신을 적용했을 때, 상태 머신의 엄격한 규칙으로 인해 코드의 의도가 더 명확해지고, 컴포넌트에서 상태를 다루는 여러 복잡한 로직들이 많이 사라졌습니다.</p>
<h1 id="finite-state-machinew-xstate-후기">Finite State Machine(w. XState) 후기</h1>
<p>이번에 간단한 예시 상황을 만들고 XState로 코드를 리팩토링 해보면서 상태 머신의 장점과 단점을 경험했습니다.</p>
<h2 id="장점">장점</h2>
<p>제가 느낀 <strong>장점은 꽤 컷으며</strong>, 상태 머신만으로도 웹 애플리케이션의 모든 상태를 관리할 수 있을 정도로 장점이 강력한 도구였다고 느꼈습니다.</p>
<p>명확한 상태 표현으로 <strong>협업에도 큰 도움</strong>이 될 것 같다고 생각했습니다.</p>
<ul>
<li>명확한 상태의 흐름과 상태간의 관계 표현으로 인한 가독성 향상.</li>
<li>불가능한 상황을 방지하는 엄격한 규칙으로 인한 버그 방지.</li>
</ul>
<h2 id="단점">단점</h2>
<p>처음 경험해 본 FSM을 설계함에도 고려해야될 것이 많아 복잡했고, XState를 처음 사용할 때 쉽지 않았습니다.</p>
<p>FSM을 구현하기 위한 메소드와 제약 사항 같은 옵션들이 정말 많아서, 이를 확인하고 익히는 데만 시간이 좀 걸렸습니다.</p>
<p>이런 학습 비용은 FSM과 XState의 단점이라고 생각합니다.</p>
<p>또한 마치 리덕스처럼 간단한 상태 머신을 만드는데도 필요한 코드가 많아 처음 상태 머신을 만들 때 조금 번거로웠습니다.</p>
<p>필요한 코드가 많고 적용할 수 있는 옵션들이 만큼 설계가 잘못되어 여러 context가 얽히게 되면 상태 머신을 사용하지 않을 때보다 더 복잡해질 수 있다고 느꼈습니다.</p>
<p>따라서 상태 머신은 Visualizer 툴등을 이용해 디버깅 및 관리하면서 간결하게 유지하는 것이 중요하다고 생각했습니다.</p>
<ul>
<li>상태 머신과 XState를 잘 활용하기 위해선 어느정도 학습 시간이 필요. (학습 비용 있음)<ul>
<li>팀 간 노하후 공유로 어느정도 완화 가능.</li>
</ul>
</li>
<li>redux처럼 상태 머신만을 위한 boilerplate 코드가 꽤 많이 필요하다.<ul>
<li>스니펫 활용으로 생산성 향상 가능.</li>
</ul>
</li>
<li>상태 머신을 사용할 때는 상태 머신의 상태를 최대한 직관적이고 간결하게 유지하고, 상태 머신에 불필요한 context가 서로 얽히지 않도록 주의해야한다.<ul>
<li>Visualizer를 잘 활용하면 복잡한 상태 머신을 시각적으로 확인할 수 있어 디버깅과 리팩토링에 도움이 될 수 있다.</li>
</ul>
</li>
</ul>
<h1 id="conclusion">Conclusion</h1>
<p>XState에선 FSM 구현을 위해 제공하는 메소드와 기능들이 정말 많지만, 이번 포스팅에선 정말 기본적인 기능들만 사용해보았습니다.</p>
<p>아직 잘 모르는 기능과 옵션들도 많아 앞으로 계속해서 사용해보며 익혀나가야겠습니다. (필요하다면 후속 포스팅에서 이어나가보겠습니다.)</p>
<p>처음에 익숙해지기는 어렵지만, 장점이 뚜렷하여 적절히 활용하면 점점 더 성장하고 복잡해지는 프로덕트 유지보수에 큰 도움이 될 것 같습니다.</p>
<p>감사합니다. 🙏</p>
<h1 id="ref">ref</h1>
<ul>
<li><p><a href="https://xstate.js.org/docs/">XState</a></p>
</li>
<li><p><a href="https://fe-developers.kakaoent.com/2022/220922-make-cart-with-xstate/">자바스크립트로 만든 유한 상태 기계 XState - 카카오 엔터테인먼트</a></p>
</li>
<li><p><a href="https://geekyants.com/blog/introduction-to-state-machines-in-react-with-xstate">Introduction to State Machines in React with XState - GeekyAnts</a></p>
</li>
<li><p><a href="https://kyleshevlin.com/guidelines-for-state-machines-and-xstate/">Guidelines for State Machines and XState - Kyle Shevlin</a></p>
</li>
<li><p><a href="https://dev.to/gtodorov/sustainable-xstate-machines-2065">Sustainable xState machines - dev.to</a></p>
</li>
<li><p><a href="https://ko.wikipedia.org/wiki/%EC%9C%A0%ED%95%9C_%EC%83%81%ED%83%9C_%EA%B8%B0%EA%B3%84">유한 상태 기계 - 위키피디아</a></p>
</li>
<li><p><a href="https://christoph.hashnode.dev/xstate-dependency-inversion">Testable XState-Machines: Combining XState with Dependency Inversion - Christoph Fricke</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSE(Server-Sent events) with EventSource]]></title>
            <link>https://velog.io/@lucky-jun/Server-Sent-events-with-EventSource</link>
            <guid>https://velog.io/@lucky-jun/Server-Sent-events-with-EventSource</guid>
            <pubDate>Thu, 22 Feb 2024 08:11:11 GMT</pubDate>
            <description><![CDATA[<h1 id="sseserver-sent-events-with-eventsource">SSE(Server-Sent events) with EventSource</h1>
<p>보통 HTTP 통신은 요청 1번에 응답 1번을 받습니다.</p>
<p>하지만 SSE(Server-Sent events) 한번의 연결로 여러번의 응답을 실시간으로 받을 수 있습니다.</p>
<p>이번 글에선 EventSource Web API를 활용해 SSE를 이용하는 방법을 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/569d7767-e564-4364-aae8-560bb7096a4b/image.avif" alt=""></p>
<h1 id="sseserver-sent-events란">SSE(Server-Sent events)란?</h1>
<p>SSE(Server-Sent events)는 HTTP 기반으로, 서버의 데이터를 실시간으로 streaming하는 기술입니다.</p>
<p>실시간 통신의 대명사인 webSocket과 다른 점은 서버에서 클라이언트로의 <strong>단방향 스트리밍</strong>이라는 점입니다.</p>
<p>ref. <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events">SSE의 HTTP spec</a></p>
<h2 id="polling과-websocket">polling과 webSocket</h2>
<p>SSE이외에 지속 통신 방법은 두가지가 있습니다. polling과 webSocket입니다.</p>
<h3 id="vs-polling">vs polling</h3>
<p>poling은 client가 직접 일정 주기로 요청해서 서버 data를 pull하는 방식으로 주기적인 HTTP 통신 요청을 해줘야 하기 때문에 리소스가 낭비됩니다.</p>
<p>반면, <strong>SSE는 한번의 요청 및 연결 성립으로 실시간 통신이 가능합니다.</strong></p>
<h3 id="vs-websocket">vs webSocket</h3>
<p>webSocket은 SSE와는 다르게 양방향 통신이 가능하고, 한번의 연결로 여러번의 요청과 응답이 가능합니다. (정확히는, webSocket은 연결후에도 계속 handshake를 주고받습니다.)</p>
<p>하지만, webSocket만의 protocol이 따로 있으며, <strong>양방향 통신이 반드시 필요한 경우가 아니라면, SSE를 사용하는 것이 더 간단하고 효율적</strong>입니다.</p>
<p>또한, <strong>webSocket은 fireWall에 의한 통신 issue</strong>가 있을 수 있는 반면, SSE는 그렇지 않습니다.</p>
<h2 id="open-connection-제한과-http2">Open connection 제한과 HTTP/2</h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource - mdn</a> 문서에선 SSE를 사용하기 전에, 브라우저의 연결 제한을 확인하라고 합니다.</p>
<p>크롬 및 파이어폭스 브라우저는 &quot;한 브라우저 + domain&quot; 당 open connection을 최대 6개까지만 유지할 수 있도록 제한하고 있습니다.</p>
<p>즉, SSE를 연결하는 동일한 주소의 페이지가 있을 경우 6개 탭까지만 연결되고 그 이후엔 추가적인 연결이 제한된다는 것입니다.</p>
<p>하지만, HTTP/2를 사용하면, default 100개까지 open connection을 유지할 수 있습니다.</p>
<h1 id="eventsource">EventSource</h1>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource</a>는 SSE 연결을 편리하게 할 수 있도록 해주는 Web API입니다.</p>
<p>클라이언트에서 SSE를 연결을 위해 여러가지를 알아보면 바로 나오는 것이 MDN에서 소개하는 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">Using_server-sent_events</a>입니다.</p>
<p>EventSource로 서버와 SSE 연결을 하는 방법을 알려줍니다.</p>
<p>ref. <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface">EventSource HTML spec</a></p>
<h2 id="eventsource의-특징">EventSource의 특징</h2>
<p>EventSource API는 고유한 특징과 한계가 있습니다.</p>
<p>사용할 때, 이러한 특징과 한계를 고려해야 합니다!</p>
<ul>
<li>GET 요청입니다.</li>
<li>연결이 끊기면 자동으로 다시 연결합니다.</li>
<li>withCredentials 옵션을 통해 CORS 정책을 준수할 수 있습니다.</li>
<li>SSE 연결 요청시, header를 수정할 수 없습니다. 🤯 (<a href="https://github.com/whatwg/html/issues/2177">Setting headers for EventSource</a> discussion에 의하면 앞으로도 header 수정 기능을 추가할 생각은 없는 것 같습니다. 😇)</li>
</ul>
<h1 id="sse-example">SSE example</h1>
<p>간단한 예시와 함께 SSE를 사용하는 방법을 알아보겠습니다. (feat. EventSource)</p>
<h2 id="server">Server</h2>
<p>express.js로 예시 서버를 만들어 봤습니다.</p>
<p>SSE 연결을 위해 줘야하는 응답은 간단합니다.</p>
<pre><code class="language-javascript">res.setHeader(&quot;Content-Type&quot;, &quot;text/event-stream&quot;);
res.setHeader(&quot;Cache-Control&quot;, &quot;no-cache&quot;);
res.setHeader(&quot;Connection&quot;, &quot;keep-alive&quot;);
res.flushHeaders();</code></pre>
<p>이렇게 header를 달아 응답을 주면, client와 event stream 연결이 성립됩니다.</p>
<p>이 후, 같은 API에서 원하는 데이터와 함께 응답을 보내면 실시간으로 client에 전송이 됩니다.</p>
<pre><code class="language-javascript">// send server sent events
let counter = 0;
const interValID = setInterval(() =&gt; {
  counter++;
  if (counter &gt; 5) {
    clearInterval(interValID);
    // if connection is closed, retry after 10s
    res.write(&quot;retry: 10000\n&quot;);
    res.end();
    return;
  }

  ///// real-time sent data /////
  res.write(`id: ${counter}\n`);
  res.write(`data: ${JSON.stringify({ num: counter })}\n\n`);
  ///////////////////////////////

}, 1000);

// If client closes connection, stop sending events
res.on(&quot;close&quot;, () =&gt; {
  console.log(&quot;client dropped me&quot;);
  clearInterval(interValID);
  res.end();
});</code></pre>
<h2 id="client">Client</h2>
<p>EventSource를 통해 서버와 SSE 연결을 맺고, 실시간으로 전송되는 이벤트를 listen하는 코드입니다.</p>
<pre><code class="language-javascript">let closeTime = 0;
const sseDataElement = document.getElementById(&#39;sse&#39;);
const eventSource = new EventSource(`https://localhost:3000/slack/sse`, {
  withCredentials: true,
});

eventSource.addEventListener(&#39;message&#39;, (event) =&gt; {
  sseDataElement.innerHTML += event.data + &#39;&lt;br&gt;&#39;;
});

eventSource.addEventListener(&#39;open&#39;, (event) =&gt; {
  const openTime = Date.now().getTime();
  console.log(&#39;open sse connection&#39;, openTime);
  if (closeTime &gt; 0) console.log(&#39;reopen At : &#39;, closeTime - openTime);
});

// we can use custom event!
eventSource.addEventListener(&#39;custom&#39;, (event) =&gt; {
  console.log(&#39;custom event&#39;, event);
});

eventSource.onerror = (error) =&gt; {
  console.error(&#39;SSE Error:&#39;, error);
  closeTime = Date.now().getTime();
  console.log(&#39;close sse connection At : &#39;, closeTime);
};</code></pre>
<h2 id="demo">Demo</h2>
<p>사실 EventSource 문서만 봐선 어떻게 사용해야할지 명확하게 알기 힘들어, 간단한 예시로 직접 해봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/5878623a-01ba-4daa-8e09-1ca91ed9687a/image.gif" alt=""></p>
<p>network 탭을 보면, 한번의 요청으로 여러번의 응답을 받는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/236d2618-4e98-467b-9707-840ca717e47c/image.png" alt=""></p>
<h3 id="retry">retry</h3>
<p>서버에서 ms과 함께 retry 메세지를 전달하면, 연결이 끊겼을 때 재요청하는 시간을 조정할 수 있습니다.</p>
<pre><code class="language-javascript">// retry in 1s
res.write(&quot;retry: 1000\n&quot;);</code></pre>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/aaf887d4-8c79-46ed-b2df-5ca42d3afb94/image.gif" alt=""></p>
<p>이번엔 3초로 설정해보겠습니다.</p>
<pre><code class="language-javascript">// retry in 3s
res.write(&quot;retry: 3000\n&quot;);</code></pre>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/07f690db-cfaf-415d-9608-9083545e11f1/image.gif" alt=""></p>
<h3 id="custom-event">Custom Event</h3>
<p>default인 message event 외에도 직접 추가한 custom event를 사용할 수 있습니다.</p>
<p>event가 명시돼있지 않는 경우는 모두 message event입니다.</p>
<pre><code class="language-javascript">///// real-time sent data /////
res.write(&quot;event: custom\n&quot;);
res.write(`id: ${counter}\n`);
res.write(`data: ${JSON.stringify({ num: counter })}\n\n`);
///////////////////////////////</code></pre>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/08a29a2e-8c65-4e05-9c96-b85264a157da/image.gif" alt=""></p>
<p>network 탭에서 event type이 custom으로 바뀐 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/1abc9ad5-073a-4d35-af3e-a6aa62edddba/image.png" alt=""></p>
<h1 id="eventsource의-한계와-polyfills">EventSource의 한계와 polyfills</h1>
<p>Web API로 주어지는 EventSource는 편리하지만, <strong>GET 요청 밖에 안되는 점</strong>, <strong>header 수정이 안되는 점</strong> 등의 한계가 있습니다.</p>
<p>polyfill 라이브러리를 통해 이 한계를 극복할 수 있습니다.</p>
<p><a href="https://github.com/Yaffle/EventSource">event-source-polyfill</a></p>
<p><a href="https://github.com/Azure/fetch-event-source">@microsoft/fetch-event-source</a></p>
<p>혹은, ReadableStream 혹은 xhr.onprogress로 직접 구현할 수도 있습니다. 👍</p>
<p>아래 코드는 fetch와 ReadableStream을 활용한 SSE 구현 예시입니다. <a href="https://velog.io/@april_5/React-Server-Sent-EventsSSE-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">ref</a></p>
<pre><code class="language-javascript">const fetchSSE = () =&gt; {
    fetch(url, {
      method: &#39;POST&#39;,
      headers: {
        token: authToken,
        &#39;Content-Type&#39;: &#39;application/json; charset=utf-8&#39;,
      },
      body: {
         // ...
      },
     })
      .then((response) =&gt; {
        const reader = response.body!.getReader();
        const decoder = new TextDecoder();

        const readChunk = () =&gt; {
          return reader.read().then(appendChunks);
        };

        const appendChunks = (result) =&gt; {
          const chunk = decoder.decode(result.value || new Uint8Array(), {
            stream: !result.done,
          });
          const parseData = JSON.parse(chunk);
          // do something with parseData

          if (!result.done) {
            return readChunk();
          }
        };

        return readChunk();
      })
      .then(() =&gt; {
        // when it&#39;s done
      })
      .catch((e) =&gt; {
        // error
      });
};</code></pre>
<h1 id="conclusion">Conclusion</h1>
<p>SSE의 개념과 EventSource를 통해 실시간 통신을 하는 방법을 알아보았습니다.</p>
<p>EventSource API가 모든 것을 해결해주기 보단 이를 보완한 여러 해결책들이 많은 것으로 보아 앞으로도 여러가지 해결할 문제들이 많을 것 같아 관심있게 지켜보고 해결해야할 문제가 있다면 적극적으로 해결해보려고 합니다. 😁</p>
<h1 id="ref">Ref.</h1>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">https://developer.mozilla.org/en-US/docs/Web/API/EventSource</a></p>
<p><a href="https://blog.logrocket.com/using-fetch-event-source-server-sent-events-react/">https://blog.logrocket.com/using-fetch-event-source-server-sent-events-react/</a></p>
<p><a href="https://kwseo.github.io/2017/03/25/sse/">https://kwseo.github.io/2017/03/25/sse/</a></p>
<p><a href="https://surviveasdev.tistory.com/entry/%EC%9B%B9%EC%86%8C%EC%BC%93-%EA%B3%BC-SSEServer-Sent-Event-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">https://surviveasdev.tistory.com/entry/%EC%9B%B9%EC%86%8C%EC%BC%93-%EA%B3%BC-SSEServer-Sent-Event-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js의 Proxy]]></title>
            <link>https://velog.io/@lucky-jun/proxy-nextjs</link>
            <guid>https://velog.io/@lucky-jun/proxy-nextjs</guid>
            <pubDate>Tue, 23 Jan 2024 14:34:11 GMT</pubDate>
            <description><![CDATA[<h1 id="nextjs의-proxy-기능">Next.js의 proxy 기능</h1>
<p>Next.js는 기본적으로 웹 프론트도 가능하지만, 서버까지 함께있는 풀스택 프레임워크 입니다.</p>
<p>여기서 Next.js가 제공하는 서버의 여러가지 proxy 기능을 활용하여 다양한 것을 할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/4b6087c2-f81c-4b15-8564-55bf466ea1b1/image.webp" alt="reverseProxy"></p>
<!--more-->

<h1 id="proxy란">Proxy란?</h1>
<p>Proxy는 클라이언트와 서버 사이에 위치하여, 클라이언트의 요청을 받아서 서버에 전달하고, 서버의 응답을 받아서 클라이언트에 전달하는 중계자 서버 역할을 합니다.</p>
<p>Proxy 서버는 그 위치에 따라 2가지로 나뉩니다.</p>
<h2 id="forward-vs-reverse-proxy">Forward vs Reverse Proxy</h2>
<p>Forward는 클라이언트 바로 뒤에 위치합니다.</p>
<p>반면 Reverse Proxy는 서버 바로 앞에 위치합니다. (여기서 Reverse는 &quot;반대&quot;라는 의미가 아닌, &quot;배후, 뒤쪽&quot;라는 의미입니다.)</p>
<p><strong>Next.js가 제공하는 Proxy는 서버 바로 앞에 위치하기 때문에 Reverse Proxy입니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/517399fd-1b21-4787-abe5-b0a33ccd4600/image.jpeg" alt="forwardVsReverse"></p>
<h1 id="redirect-vs-rewrite">redirect vs rewrite</h1>
<p>Next.js에서 제공하는 proxy 기능은 <strong>redirect</strong>와 <strong>rewrite</strong>가 있습니다.</p>
<p>redirect는 말 그대로 redirect, rewrite는 forwarding과 대응한다고 할 수 있겠네요!</p>
<h2 id="redirect-vs-forwarding">redirect vs forwarding</h2>
<p>redirect와 forwarding은 백엔드에서 자주 사용되는 용어입니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/ed866bbf-8938-4337-a037-3d36784849bd/image.png" alt="redirectVsForwarding"></p>
<p>간단히 이렇게 설명할 수 있습니다.</p>
<ul>
<li>Redirect : 요청 -&gt; 다른 곳으로 요청하라고 응답 (code: 3xx) -&gt; <strong>클라이언트에서</strong> 다시 요청 -&gt; 응답</li>
<li>Forwarding : 요청 -&gt; <strong>서버내에서</strong> 다른 곳에 요청하고 응답 받아옴 -&gt; 그대로 결과 응답</li>
</ul>
<h2 id="example">Example</h2>
<p>적용 예시를 보시겠습니다. 😎</p>
<h3 id="redirect">redirect</h3>
<p>redirect를 적용한 코드와 페이지 스샷입니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/ad5b14db-19b9-4b7b-8273-5f87a19bc006/image.png" alt="redirectCode"></p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/fcacb56f-0be9-432a-add2-fed9fbfb7b8f/image.png" alt="redirectEx"></p>
<p>여기서! permanent 옵션에 따라 307, 308로 응답코드가 나눠집니다.</p>
<p>이는, 웹표준을 준수한 것으로 자세한 내용은 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/300">MDN, 300 Multiple Choices</a>를 참고해주세요.</p>
<h3 id="rewrite">rewrite</h3>
<p>rewrite를 적용한 코드와 페이지 스샷입니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/d2b0db31-efe0-4625-a979-cb421d8c1cc5/image.png" alt="rewriteCode"></p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/1d4fe6fc-cf97-4517-b694-5daaec73726e/image.png" alt="rewriteEx"></p>
<h3 id="dynamic-path--외부-url">dynamic path &amp; 외부 url</h3>
<p>고정 url뿐만 아니라, dynamic path와 외부 url도 적용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/6f013da7-1b81-48f0-a394-fa9f5520e42d/image.png" alt="redirectDynamicCode"></p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/e691c7f2-1ee4-463e-88c9-ef25c3979594/image.gif" alt="redirectDynamic"></p>
<h2 id="other-options">Other options</h2>
<p>위에서 보셨듯이 source로 proxy할 요청 url을 지정했는데요.</p>
<p>추가 조건을 주어 proxy 대상을 더 세세히 판단할 수 있습니다.</p>
<p><code>has</code>로 must have를 <code>missing</code>으로 must not have를 지정할 수 있습니다.</p>
<p>v.14.1 기준 4가지 타입을 적용할 수 있습니다.</p>
<ul>
<li>header</li>
<li>cookie</li>
<li>query</li>
<li>host</li>
</ul>
<p>자세한 건 -&gt; <a href="https://nextjs.org/docs/pages/api-reference/next-config-js/rewrites#header-cookie-and-query-matching">Next.js, rewrite header-cookie-and-query-matching</a></p>
<h1 id="beforefiles-afterfiles-fallback">beforeFiles, afterFiles, fallback</h1>
<p><strong>rewrite에만 있는 옵션</strong>으로, 좀 더 세세하게 rewrite를 적용할 수 있습니다.</p>
<h2 id="nextjs의-요청-및-응답-순서">Next.js의 요청 및 응답 순서</h2>
<p>Next.js에 어떤 요청이 오면, 다음과 같은 순서로 처리됩니다.</p>
<ol>
<li>Header check/response</li>
<li>redirect check/response</li>
<li><strong>beforeFiles</strong> check/response</li>
<li>static files check/response</li>
<li><strong>afterFiles</strong> check/response</li>
<li>Dynamic route page check/response</li>
<li><strong>fallback</strong> check/response</li>
</ol>
<p>이 순서를 잘 파악해서 사용하면 좋을 것 같습니다.</p>
<h3 id="beforefiles">beforeFiles</h3>
<p>정적 파일들이 등록된 api를 확인하기 전에 proxy가 적용됩니다.</p>
<p>아래와 같이 설정했다고 가정해보겠습니다.</p>
<pre><code class="language-javascript">rewrites: async ()  =&gt; ({
  beforeFiles: [{
    source: &#39;/test/:path*&#39;,
    destination: &#39;/proxy/:path*&#39;,
  }],
})</code></pre>
<p><code>/test</code>는 정적 컴포넌트가 있는 경로입니다.</p>
<p>여기서 <strong><code>${url}/test</code>에 요청하면 <code>/test</code>에 등록된 정적 컴포넌트가 아닌 <code>/proxy</code>로 요청이 proxy됩니다.</strong></p>
<h3 id="afterfiles">afterFiles</h3>
<p>정적 파일들이 등록된 api를 확인한 후에 proxy가 적용됩니다.</p>
<pre><code class="language-javascript">rewrites: async ()  =&gt; ({
  afterFiles: [{
    source: &#39;/test/:path*&#39;,
    destination: &#39;/proxy/:path*&#39;,
  }],
})</code></pre>
<p>동일하게 <code>/test</code>는 정적 컴포넌트가 있는 경로고,<code>${url}/test</code>에 요청하면, <code>/proxy</code>로 요청이 proxy되지 않고, <code>/test</code>에 등록된 정적 컴포넌트가 응답됩니다.</p>
<p>여기서 <strong><code>/test/[id]</code>와 같이 동적 경로가 등록되어 있다면, <code>/test/1</code>에 대한 요청은 <code>/proxy/1</code>로 proxy됩니다.</strong></p>
<h3 id="fallback">fallback</h3>
<p>fallback은 정적 파일과 동적 경로가 등록된 api를 확인한 후에 proxy가 적용됩니다.</p>
<pre><code class="language-javascript">rewrites: async ()  =&gt; ({
  fallback: [{
    source: &#39;/test/:path*&#39;,
    destination: &#39;/proxy/:path*&#39;,
  }],
})</code></pre>
<p>위와 동일한 가정하에, <code>{url}/test</code>, <code>{url}/test/[id]</code>로 요청하면, proxy되지 않습니다.</p>
<p>오로지, <strong>아무것도 등록되지 않은 api에 접근했을 떄만, fallback에 등록한 proxy가 적용됩니다.</strong></p>
<h1 id="rewrite-활용">rewrite 활용</h1>
<p>Next.js 공식 홈페이지에서 여러가지 활용 방안을 소개하고 있습니다.</p>
<ul>
<li>CORS 해결</li>
<li>점진적인 페이지 마이그레이션</li>
<li><a href="https://nextjs.org/docs/pages/building-your-application/deploying/multi-zones">Multi zone</a>이라는 이름으로 Micro frontend를 지원하네요.
이것 또한 rewrite를 응용한 것 입니다.</li>
</ul>
<h1 id="ref">Ref</h1>
<ul>
<li><p><a href="https://nextjs.org/docs/app/api-reference/next-config-js/redirects">https://nextjs.org/docs/app/api-reference/next-config-js/redirects</a></p>
</li>
<li><p><a href="https://nextjs.org/docs/pages/api-reference/next-config-js/rewrites">https://nextjs.org/docs/pages/api-reference/next-config-js/rewrites</a></p>
</li>
<li><p><a href="https://www.cloudflare.com/ko-kr/learning/cdn/glossary/reverse-proxy/">https://www.cloudflare.com/ko-kr/learning/cdn/glossary/reverse-proxy/</a></p>
</li>
<li><p><a href="https://inpa.tistory.com/entry/NETWORK-%F0%9F%93%A1-Reverse-Proxy-Forward-Proxy-%EC%A0%95%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EC%A0%95%EB%A6%AC">https://inpa.tistory.com/entry/NETWORK-%F0%9F%93%A1-Reverse-Proxy-Forward-Proxy-%EC%A0%95%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EC%A0%95%EB%A6%AC</a></p>
</li>
<li><p><a href="https://vercel.com/templates/next.js/microfrontends">https://vercel.com/templates/next.js/microfrontends</a></p>
</li>
<li><p><a href="https://nextjs.org/docs/pages/building-your-application/deploying/multi-zones">https://nextjs.org/docs/pages/building-your-application/deploying/multi-zones</a></p>
</li>
<li><p><a href="https://kotlinworld.com/329">https://kotlinworld.com/329</a></p>
</li>
<li><p><a href="https://jjjayyy.tistory.com/15">https://jjjayyy.tistory.com/15</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[너와 나와 우리를 위한 클린코드 (feat. NextStep, TDD 클린코드 with React)]]></title>
            <link>https://velog.io/@lucky-jun/%EB%84%88%EC%99%80-%EB%82%98%EC%99%80-%EC%9A%B0%EB%A6%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-feat.-NextStep-TDD-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-with-React</link>
            <guid>https://velog.io/@lucky-jun/%EB%84%88%EC%99%80-%EB%82%98%EC%99%80-%EC%9A%B0%EB%A6%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-feat.-NextStep-TDD-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-with-React</guid>
            <pubDate>Sun, 23 Apr 2023 11:41:48 GMT</pubDate>
            <description><![CDATA[<h1 id="nextstep-tdd-클린코드-with-react">NextStep, TDD 클린코드 with React</h1>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/4fa319ea-2d72-4050-a2b7-b1e53dd30261/image.jpeg" alt="cleanCodeReact"></p>
<p>2월 ~ 4월까지 약 2달 동안 <a href="https://edu.nextstep.camp/">NextStep</a>의 <a href="https://edu.nextstep.camp/c/QoTvUh4y/">TDD 클린코드 with React</a> 과정을 진행했습니다.</p>
<p>과정을 진행하면서 여러모로 배운 점들이 정말 많지만, 이번엔 리액트 클린코드 과정 진행과 클린코드에 대해 배우고 느꼈던 점들에 대해 나누어보려고 합니다. 😁</p>
<p>글이 좀 길어져, 목차를 추가했습니다! 📜</p>
<ul>
<li>TDD 클린코드 with React 과정 소개</li>
<li>미션 요구사항에 따른 첫 설계</li>
<li>리뷰를 통한 코드 수정, 기존 코드를 클린하게 수정하기.</li>
<li>클린코드에 대해서</li>
</ul>
<h1 id="tdd-클린코드-with-react-과정-소개">TDD 클린코드 with React 과정 소개</h1>
<p>간단하게 제가 진행했던 TDD 클린코드 with React 2기 과정에 대해 설명하겠습니다.</p>
<p>미션은 크게 2가지로 구성되어있습니다.</p>
<h2 id="페이먼츠-미션">페이먼츠 미션</h2>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/0d700124-8353-4b1a-97eb-bc1bbc6803d1/image.gif" alt="payments_demo"></p>
<p>다수의 input이 있는 복잡한 form을 다루는 미션입니다.</p>
<p><strong>CDD 방법론</strong>을 적용해 <strong>Bottom-up 방식으로 컴포넌트를 개발</strong>하고 <strong>Storybook으로 단위 컴포넌트를 테스트</strong>하는 것을 중점으로 합니다.</p>
<p>(이 미션에선 상태 관리 라이브러리를 사용할 수 없습니다. ㅎㅎ 😯)</p>
<p>페이먼츠 과제 결과물 Repo와 PR -&gt; <a href="https://github.com/Jay-WKJun/react-payments">Payments Github Repo</a>, <a href="https://github.com/next-step/react-payments/pull/98">Pull Request</a></p>
<h2 id="장바구니-페이지">장바구니 페이지</h2>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/8d1dbcf2-defb-4219-bafe-33f9c3f14628/image.gif" alt="cart_demo"></p>
<p>Api가 없는 상황에서도 안정적으로 개발환경을 구축하고 복수의 페이지에서도 복잡한 state를 안정적으로 관리하는 방법을 중점으로 진행합니다.</p>
<p>전 <strong>MSW</strong>와 <strong>react-query</strong>를 적용해 개발했습니다.</p>
<p>장바구니 PR -&gt; <a href="https://github.com/next-step/react-shopping-cart/pull/13">장바구니 1차</a> <a href="https://github.com/next-step/react-shopping-cart/pull/28">장바구니 2차</a></p>
<h2 id="이번-포스트는-페이먼츠-미션을-중심으로-진행됩니다">이번 포스트는 페이먼츠 미션을 중심으로 진행됩니다.</h2>
<p>두가지 미션 중에선 개인적으로 페이먼츠가 더 어려웠고 그만큼 배운 점도 더 많았다고 생각합니다.</p>
<p>따라서, 페이먼츠 미션을 위주로 이번 포스트를 진행하도록 하겠습니다!</p>
<h1 id="페이먼츠-설계">페이먼츠 설계</h1>
<p>페이먼츠 미션을 보고 처음에 생각했던 설계입니다.</p>
<p>이번 미션에선 상태 관리 라이브러리를 사용할 수 없어 전역 상태 공유는 모두 Context Api를 활용했습니다.</p>
<p>크게 2가지를 생각했습니다.</p>
<ul>
<li><strong>어디에서든 손쉽게 사용할 수 있도록 input Element와 동기화 된 객체를 state로 가져보자.</strong></li>
<li><strong>여러겹의 추상화 계층으로 재사용성을 높여보자.</strong></li>
</ul>
<h2 id="어디에서든-손쉽게-사용할-수-있도록-input-element와-동기화-된-객체를-state로-가져보자">어디에서든 손쉽게 사용할 수 있도록 input Element와 동기화 된 객체를 state로 가져보자.</h2>
<p>각 input Element에 대응하는 State 객체를 만들어 사용하고 메소드로 필요한 데이터를 가져올 수 있도록 했습니다.</p>
<p>페이먼츠 과제의 요구사항 중에 다음과 같은 사항이 있었고,</p>
<ul>
<li><strong>각 input들의 조건에 따라 validation 체크</strong></li>
<li><strong>유효한 값 입력시 다음 필드로 Input Focusing</strong></li>
</ul>
<p>페이먼츠 미션의 요구사항을 보고 생각했을 때, 각 input에 대응하는 state를 객체의 형태로 다룬 이유는 다음과 같습니다.</p>
<ul>
<li><p><strong>스스로 validation하는 state 객체</strong></p>
<p>각 Input마다 각자의 validation 로직이 필요합니다.</p>
<p>따라서 객체로 함께 관리하면 컴포넌트에서 별도의 실행로직 없이 새롭게 값이 갱신될 때마다, 자동으로 validation 할 수 있어 <strong>컴포넌트의 로직을 한층 깔끔하게 할 수 있을 것이라고 생각</strong>했습니다.</p>
</li>
<li><p><strong>invalid에 곧바로 대응해 focus할 수 있는 state 객체</strong></p>
<p>Auto focus를 설계할 당시 invalid한 input을 찾아, focus하는 방법을 생각했습니다.</p>
<p>따라서, <strong>invalid한 state 객체를 찾으면 해당 input Element에 곧바로 focus할 수 있도록</strong> HTMLInputElement를 포함시켜두면 좋을 것 이라고 생각했습니다.</p>
</li>
<li><p><strong>컴포넌트의 로직을 깔끔하게 할 수 있을 것으로 기대했습니다.</strong></p>
</li>
</ul>
<p>input의 UI들은 비슷했기에, 구체적인 비즈니스 로직을 state 객체에 넣어두면 <strong>공통 컴포넌트에 UI를 정의하고 객체를 주입해 간편하게 재사용할 수 있을 것</strong>으로 기대했습니다.</p>
<h3 id="state-객체-구현">state 객체 구현</h3>
<p>직접 구현한 input에 대응하는 state 객체와 이를 컴포넌트에 제공하고 적용하는 로직입니다.</p>
<p>(원래 코드를 조금 수정했습니다.. 추상화단계가 더 있지만, 글의 가독성을 위해 로직을 좀 더 직관적으로 수정했습니다. ㅎㅎ)</p>
<pre><code class="language-typescript">// input Element에 대응하는 객체 생성
type TCardNumber = string;

export class CardNumberInputElement {
  value?: TCardNumber;
  errorMessage?: string;
  ref?: HTMLInputElement | null;

  setRef(ref?: HTMLInputElement | null) {
    this.ref = ref;
  }

  // validate 로직, invalidate case에 따른 errorMessage와 함께 관리합니다.
  validateValue(value?: string) {
    if (isNil(value)) return;
    if (!value || value.length !== 4) {
      return &#39;카드 번호 4자리를 입력해주세요.&#39;;
    }
  }

  isAllowToFocusNext() {
    return this.value?.length === 4;
  }

  checkWhetherThisUserInputCanBeSet(inputValue: TCardNumber) {
    return !inputValue || inputValue.length &lt;= 4;
  }

  constructor({ value, ref }: Partial&lt;CardNumberInputElement&gt;) {
    this.value = value;
    this.errorMessage = this.validateValue(value);
    this.ref = ref;
  }
}

// cardStore에 합쳐서 Context로 제공합니다.
const cardStore = {
  cardNumbers: createArray(4, () =&gt; new CardNumberInputElement({})),
  cardOwners: createArray(1, () =&gt; new CardOwnerInputElement({})),
};</code></pre>
<p>컴포넌트에서 아래와같이 적용됩니다.</p>
<pre><code class="language-tsx">interface CardNumbersInputListProps {
  cardNumbers?: TCardStore[&#39;cardNumbers&#39;];
}

export const CardNumbersInputList = memo(function CardNumbersInputList({ cardNumbers }: CardNumbersInputListProps) {
  const errorMessage = cardNumbers?.find((cardNumber) =&gt; !!cardNumber.errorMessage)?.errorMessage;

  return (
    &lt;CardInputWrapper header=&quot;카드 번호&quot; errorMessage={errorMessage}&gt;
      &lt;div className=&quot;input-box&quot;&gt;
        {cardNumbers?.map((cardNumber, i) =&gt; {
          const isLast = checkIsArrayLast(cardNumbers, i);
          const isPasswordType = i &gt;= cardNumbers.length - 2;
          return (
            &lt;CardNumberInput
              key={`cardNumber-input-${i}`}
              type={isPasswordType ? &#39;password&#39; : &#39;text&#39;}
              needDividerRender={!isLast}
              cardNumber={cardNumber}
              index={i}
            /&gt;
          );
        })}
      &lt;/div&gt;
    &lt;/CardInputWrapper&gt;
  );
});

interface CardNumberProps {
  type?: HTMLInputTypeAttribute;
  cardNumber: CardNumberInputElement;
  index: number;
  needDividerRender: boolean;
}

export const CardNumberInput = memo(function CardNumberInput({
  type = &#39;text&#39;,
  cardNumber,
  index,
  needDividerRender,
}: CardNumberProps) {
  const { checkWhetherThisUserInputCanBeSet, value, setRef, errorMessage } = cardNumber;
  const isError = !!errorMessage;

  const cardContextApis = useCardContextApis();

  const onInputChange = {
    if (checkWhetherThisUserInputCanBeSet(e.currentTarget.value)) {
      const newValue = filterNumber(e.currentTarget.value);
      cardContextApis?.dispatch({ type: &#39;cardNumbers&#39;, payload: { index, value: newValue } });
    }
  };

  // CardInfoInputElement는 input Element를 한번 추상화한 컴포넌트입니다.
  // 내부적으로 Error를 판단해 UI로 표현해줍니다.
  return (
    &lt;CardInfoInputElement
      type={type}
      value={value ?? &#39;&#39;}
      className=&quot;input-basic&quot;
      ref={setRef.bind(cardNumber)}
      error={{ isError }}
      onChange={onInputChange}
    /&gt;
  );
});</code></pre>
<h3 id="auto-focusing-기능-구현">Auto focusing 기능 구현</h3>
<p>아래는 AutoFocus를 위해 만든 custom Hook입니다.</p>
<p>cardStore가 갱신될 때마다 실행됩니다.</p>
<pre><code class="language-typescript">// auto focusing 기능을 책임지는 custom hook
// 현재 active인 ref의 state가 isValid인 것을 확인하고 처음부터 끝까지 valid를 확인하고 처음 invalid한 곳을 바라보게한다.
export function useSequentialAutoFocus(cardStore?: TCardState[][] | null) {
  useEffect(() =&gt; {
    if (!cardStore) return;

    const activeState = findActiveState(cardStore);
    if (activeState &amp;&amp; !activeState.errorMessage &amp;&amp; activeState.isAllowToFocusNext()) {
      findInvalidStoreAndFocus(cardStore);
    }
  }, [cardStore]);
}

// 현재 active한 Element를 찾는다.
export function findActiveState(cardStore: TCardState[][]): TCardState | null {
  let activeState: TCardState | null = null;

  cardStore.some((cardStateList) =&gt;
    cardStateList.some((cardState) =&gt; {
      const isActiveElement = cardState.ref === document.activeElement;
      if (isActiveElement) activeState = cardState;
      return isActiveElement;
    })
  );

  return activeState;
}

// value가 없거나 errorMessage가 있는 state를 focus한다.
export function findInvalidStoreAndFocus(cardStore: TCardState[][]): TCardState | null {
  let invalidState: TCardState | null = null;

  cardStore.some((cardStateList) =&gt;
    cardStateList.some((cardState) =&gt; {
      const isInvalid = !cardState.value || !!cardState.errorMessage;
      if (isInvalid) {
        invalidState = cardState;
        // Focus를 이곳에서 실행합니다!!
        cardState.ref?.focus();
      }
      return isInvalid;
    })
  );

  return invalidState;
}</code></pre>
<h2 id="여러겹의-추상화-계층으로-재사용성을-높여보자">여러겹의 추상화 계층으로 재사용성을 높여보자.</h2>
<p>OOP에선 여러번의 상속을 통한 상속 계층을 통해 객체의 재사용성을 높일 수 있습니다.</p>
<p>함수의 세계에선 추상함수를 구체화 함수에 포함하여 사용하는 방법으로 여러 layer를 두어 재사용성을 높일 수 있습니다.</p>
<p>그 예시로 외부 storage와 소통을 위한 useFetch와 service 객체가 있습니다.</p>
<h3 id="usefetch와-service-객체-구현">useFetch와 service 객체 구현</h3>
<p>아래는 저의 구현입니다. 단계별로 구현됩니다.</p>
<p>1단계: General Type을 통해 그 어떤 Service든 받아들일 수 있는 useFetch hook</p>
<pre><code class="language-typescript">import { useCallback, useEffect, useState } from &#39;react&#39;;

export interface Service&lt;T&gt; {
  get(): Promise&lt;T | null&gt;;
  post(newStore?: T | null): Promise&lt;T | null&gt;;
}

// Generic Type을 2가지 경우의 수로 setting할 수 있도록 제공
// 스스로 externalStore를 GET하고 POST해줄 수 있는 객체 넣고, 그 결과를 React lifecycle에 맞춰서 제공해줄 수 있는 hook
export function useFetch&lt;T&gt;(service?: Service&lt;T&gt;): {
  fetchedData?: T | null;
  fetch: (key: keyof Service&lt;T&gt;, newStore?: T | null) =&gt; void;
};
export function useFetch&lt;T, K extends { [method: string | number | symbol]: Promise&lt;T | null&gt; }&gt;(
  service?: Service&lt;T&gt; &amp; K
): { fetchedData?: T | null; fetch: (key: keyof Service&lt;T&gt; &amp; keyof K, newStore?: T | null) =&gt; void } {
  const [fetchedData, setFetchedData] = useState&lt;T | null | undefined&gt;();

  useEffect(() =&gt; {
    service?.get().then((res) =&gt; {
      setFetchedData(res);
    });
  }, [service]);

  const fetch = useCallback(
    (key: keyof Service&lt;T&gt; &amp; keyof K, newStore?: T | null) =&gt; {
      service?.[key](newStore).then((res) =&gt; {
        setFetchedData(res);
      });
    },
    [service]
  );

  return { fetchedData, fetch };
}</code></pre>
<p>2단계: 외부 storage와 연결해주는 service 객체 생성</p>
<pre><code class="language-typescript">const LOCAL_STORAGE_CARD_LIST_KEY = &#39;cardList&#39;;

const get = async () =&gt; {
  const item = window.localStorage.getItem(LOCAL_STORAGE_CARD_LIST_KEY);
  return item ? (JSON.parse(item) as TCardList) : null;
};

export const localStorageService: TCardListService = {
  get,
  post: async (cardList: TCardList) =&gt; {
    window.localStorage.setItem(LOCAL_STORAGE_CARD_LIST_KEY, JSON.stringify(cardList));
    return get();
  },
};</code></pre>
<p>3단계: useFetch를 이용해 cardStore만을 다루는 useFetchCardList hook 생성</p>
<pre><code class="language-typescript">export function useFetchCardList() {
  const appContext = useApplicationContext();
  const { fetch, fetchedData } = useFetch&lt;TCardList | null&gt;(localStorageService);

  const postCard = useCallback(
    (card: TCard, givenCardId?: string) =&gt; {
      const cardId = givenCardId || new Date().getTime();

      if (!fetchedData) {
        fetch(&#39;post&#39;, { [cardId]: card });
        return;
      }

      fetchedData[cardId] = card;
      fetch(&#39;post&#39;, fetchedData);
    },
    [fetch, fetchedData]
  );

  const deleteCard = useCallback(
    (cardId: string) =&gt; {
      if (!fetchedData) return;

      delete fetchedData[cardId];
      fetch(&#39;post&#39;, fetchedData);
    },
    [fetch, fetchedData]
  );

  return { cardList: fetchedData, postCard, deleteCard };
}</code></pre>
<h1 id="페이먼츠-pr-리뷰">페이먼츠 PR 리뷰</h1>
<p>구현을 마치고 PR을 올리면 리뷰어님이 제 구현 코드를 보고 리뷰를 주십니다.</p>
<p>현재 코드에서 좀 더 확장성 있고 읽기 쉬운 코드가 되기 위해 놓친 부분들을 많이 지적해 주셨고,</p>
<p>참고하여 더 깨끗하고 좋은 코드로 수정할 수 있었습니다. 😁</p>
<p><del>(수십개가 넘는 Comment로 두들겨 맞은건 비밀 😇)</del></p>
<h2 id="input-element와-동기화-된-state-객체의-문제점---dom-의존성-문제-로직-책임-문제">input Element와 동기화 된 state 객체의 문제점 -&gt; DOM 의존성 문제, 로직 책임 문제</h2>
<p>로버트 C. 마틴이 쓴 클린코드 책에 따르면, 객체의 재사용성을 위해선 객체끼리의 낮은 결합도와 한가지의 역할만을 맡을 것을 강조했습니다.</p>
<p>하지만, 제가 설계한 객체는</p>
<ul>
<li><strong>HTMLElement와 강하게 결합되어 있고,</strong></li>
<li><strong>component의 onChange 이벤트를 조작하는 로직 또한 가지고 있어, 여러 역할을 맡고 있었습니다.</strong></li>
</ul>
<p>좀 더 자세히 알아보도록 하겠습니다.</p>
<h3 id="dom에-직접적인-의존성을-가지고-있음">DOM에 직접적인 의존성을 가지고 있음</h3>
<p>위에 구현한 코드를 보시면 HTMLInputElement를 그대로 들고있는 것을 알 수 있습니다.</p>
<pre><code class="language-typescript">// 간략화한 코드
export class CardNumberInputElement {
  ref?: HTMLInputElement | null;

  setRef(ref?: HTMLInputElement | null) {
    this.ref = ref;
  }
}</code></pre>
<p><strong>state가 DOM을 직접 가지고 있으면, DOM이 없는 환경에서 바로 문제가 발생할 수 밖에 없습니다.</strong></p>
<p>당장 Server 환경에선 문제가 발생할 것입니다.</p>
<p><strong>코드 변경 또한, 직접 class를 바꿔주고, ref를 사용하는 모든 코드를 바꿔야하므로 굉장한 자원이 소모 됩니다. 😇</strong></p>
<h3 id="로직의-사용처와-선언부가-너무-떨어져-있다-즉-책임이-잘못-배정돼있음">로직의 사용처와 선언부가 너무 떨어져 있다. 즉, 책임이 잘못 배정돼있음</h3>
<p>두번째로 문제가 되는 것은 <strong>state 객체에 책임이 너무 많다는 것</strong>이었습니다.</p>
<p>UI 이벤트에 관여하는 로직을, state 객체가 가지고 있어 사용처와 선언부가 매우 멀어 가독성도 떨어질 뿐더러 객체의 책임 소재가 조금 잘못되었다고 생각했습니다.</p>
<p>이렇게 되면 <strong>컴포넌트를 이용하기 위해 매번 복잡한 객체를 만들어 제공</strong>해줘야합니다. (state 객체가 구체적인 로직을 가지고 있기 때문)</p>
<p>또한, 컴포넌트는 로직이 자주 변경되는 부분 중 하나인데, <strong>만약 새로운 요구사항으로 추상화 컴포넌트를 수정하게 되면, state 객체까지 수정해야합니다.</strong></p>
<p>유지보수가 굉장히 번거로워집니다.</p>
<p>아래는 문제가 되는 부분들만 모아놨습니다.</p>
<pre><code class="language-tsx">// 간략화한 state 객체 코드
export class CardNumberInputElement {
  value?: TCardNumber;

  isAllowToFocusNext() {
    return this.value?.length === 4;
  }

  checkWhetherThisUserInputCanBeSet(inputValue: TCardNumber) {
    return !inputValue || inputValue.length &lt;= 4;
  }
}</code></pre>
<pre><code class="language-tsx">// 간략화한 컴포넌트 코드
export const CardNumberInput = memo(function CardNumberInput({
  cardNumber,
}: CardNumberProps) {
  const { checkWhetherThisUserInputCanBeSet, value, setRef, errorMessage } = cardNumber;
  const isError = !!errorMessage;

  const cardContextApis = useCardContextApis();

  const onInputChange = {
    // !!: state 객체가 onChange 로직에 관여!
    if (checkWhetherThisUserInputCanBeSet(e.currentTarget.value)) {
      const newValue = filterNumber(e.currentTarget.value);
      cardContextApis?.dispatch({ type: &#39;cardNumbers&#39;, payload: { index, value: newValue } });
    }
  };

  return (
    &lt;CardInfoInputElement
      type={type}
      value={value ?? &#39;&#39;}
      ref={setRef.bind(cardNumber)}
      error={{ isError }}
      onChange={onInputChange}
    /&gt;
  );
});</code></pre>
<pre><code class="language-typescript">// auto focus custom hook 로직의 일부분입니다.
const isInvalid = !cardState.value || !!cardState.errorMessage;
if (isInvalid) {
  invalidState = cardState;
  // !!: state 객체가 custom hook 로직에 관여합니다!
  cardState.ref?.focus();
}</code></pre>
<p>-&gt; <a href="https://jbee.io/etc/what-is-good-code/">Jbee님의 클린코드 포스팅</a>과 <a href="https://so-so.dev/essay/2020-my-good-code/#%EC%A0%81%EB%8B%B9%ED%95%9C-%EC%9E%90%EB%A3%8C%ED%98%95">soso님의 클린코드 포스팅</a>을 참고했습니다.</p>
<h3 id="이렇게-수정">이렇게 수정!</h3>
<p>따라서, state에 모든 메소드를 분리하고 DOM의 의존하는 property를 없애 최대한 단순화 하였고, 컴포넌트에서 비즈니스 로직이 수행되도록 수정했습니다.</p>
<pre><code class="language-typescript">// ?!?! 엄청나게 가벼워졌습니다.
export interface CardNumberState {
  value?: CardNumber;
  errorMessage?: string;
}</code></pre>
<pre><code class="language-typescript">// validator라는 Directory를 새로 만들어 검증 로직을 별도로 관리했습니다.
export function validateCardNumber(cardNumber?: CardNumber) {
  if (!cardNumber || cardNumber.length !== 4) {
    return &#39;카드 번호 4자리를 입력해주세요.&#39;;
  }
}

export function checkIsCardNumberFulfilled(cardNumberState: CardNumberState) {
  const { errorMessage, value } = cardNumberState;
  return !errorMessage &amp;&amp; value?.length === 4;
}</code></pre>
<p>아래는 컴포넌트 입니다.</p>
<pre><code class="language-tsx">import {
  CardNumberState,
  useCardContextApis,
} from &#39;@/contexts/CardContext&#39;;
import {
  validateCardNumber,
} from &#39;@/contexts/CardContext/validator&#39;;

interface CardNumberProps {
  type?: HTMLInputTypeAttribute;
  cardNumber: CardNumberState;
  index: number;
}

export const CardNumberInput = memo(function CardNumberInput({
  type = &#39;text&#39;,
  cardNumber,
  index,
}: CardNumberProps) {
  const { value, errorMessage } = cardNumber;
  const isError = !!errorMessage;

  const cardContextApis = useCardContextApis();

  const onInputChange = (e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const filteredNumber = filterNumber(e.currentTarget.value);

    // 메소드가 아니라 직접 컴포넌트에서 로직을 수행합니다.
    if (!filteredNumber || filteredNumber.length &lt;= 4) {
      const newValue = filteredNumber;
      // 객체가 아닌 validator에서 가져온 로직을 사용합니다.
      const errorMessage = validateCardNumber(newValue);

      cardContextApis?.setOneCardState({ type: &#39;cardNumbers&#39;, index, newState: { value: newValue, errorMessage } });
    }
  };

  return (
    &lt;CardInfoInputElement
      type={type}
      value={value ?? &#39;&#39;}
      ref={(el) =&gt; {
        if (el) setElement(el, &#39;cardNumbers&#39;, index);
      }}
      onChange={onInputChange}
      error={{ isError }}
    /&gt;
  );
});</code></pre>
<h2 id="여러겹의-추상화---굳이-이렇게까지-가독성이-크게-떨어지고-코드-수정에-불리">여러겹의 추상화 -&gt; 굳이 이렇게까지? 가독성이 크게 떨어지고 코드 수정에 불리</h2>
<blockquote>
<p>YAGNI, You aren&#39;t gonna need it - 익스트림 프로그래밍, 론 제프리스</p>
</blockquote>
<p>기능에 비해 추상화 레이어가 너무 많아 이해하기 복잡했습니다.</p>
<p>기능이 많이 않다보니 재사용되지 않는 추상화 레이어가 많아졌고 코드의 의도가 희석되어 코드의 이해가 어려워졌던 것입니다.</p>
<p>-&gt; 알고보니 Dan Abramov도 같은 실수를 했었군요,,, ㅎㅎ <a href="https://overreacted.io/goodbye-clean-code/">Goodbye, Clean Code</a></p>
<h3 id="의존성-다이어그램">의존성 다이어그램</h3>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/3b0f8533-79ac-4271-a1b1-3985929be7c8/image.png" alt="review_diagram"></p>
<p>의존성 다이어그램을 그려보면 복잡한 구조가 한 눈에 보인다는 리뷰어님의 조언에 따라 다이어그램을 그려보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/lucky-jun/post/8cf8b7d4-6114-4538-ba97-91aef85b1345/image.png" alt="diagram"></p>
<p>다이어그램을 그려보니 복잡함이 한 눈에 들어왔고 조금씩 구조를 개선하기 위해 노력했습니다.</p>
<h3 id="이렇게-수정-1">이렇게 수정!</h3>
<p>다이어그램의 내용은 1차 수정이고, 최종적으론 <strong>service 객체를 받는 것이 아닌 사용자 스스로 state를 관리하고 그 state만 넘겨받는 것으로 변경했습니다.</strong></p>
<p>대신, 특정 event에 관여할 수 있는 callback을 interface로 제공해 자신의 state를 원활히 관리할 수 있도록 했습니다.</p>
<ul>
<li><strong>과거 service 객체를 받아오는 형태</strong></li>
</ul>
<p>service 객체를 받아 매소드를 통해 직접 CardList를 받아오는 형태입니다.</p>
<p>사용자는 service 객체의 interface와 로직을 지켜 전달해주어야합니다. 매우 번거롭습니다. 😰</p>
<pre><code class="language-typescript">// service 객체를 받아 매소드를 통해 직접 CardList를 받아오는 형태입니다.
export interface Service&lt;CardList&gt; {
  get(): Promise&lt;CardList | null&gt;;
  post(newStore?: CardList | null): Promise&lt;CardList | null&gt;;
}

export function useFetch&lt;CardList&gt;(service?: Service&lt;CardList&gt;) {}</code></pre>
<ul>
<li><strong>state와 callback을 받는 형태</strong></li>
</ul>
<p>custom hook이 아닌 Context Api를 감싼 component에서 그대로 받습니다.</p>
<p>사용자는 cardList를 state로 직접 다루면서 내부 이벤트에 관여할 수 있는 콜백을 넣어줄 수 있습니다.</p>
<p>훨씬 직관적이고 사용자에게 cardList를 다룰 권한이 넘어가 훨씬 자유롭게 사용할 수 있게 됐습니다. 🙌</p>
<pre><code class="language-typescript">// custom hook이 아닌 Context Api를 감싼 component에서 그대로 받습니다.
function ApplicationProvider({
  cardList = {},
  onCardConfirm = (card, cardId) =&gt; {
    console.log(`cardId : ${cardId}, card: ${card}`);
  },
  onCardDelete = (card, cardId) =&gt; {
    console.log(`cardId : ${cardId}, card: ${card}`);
  },
  onCardUpdate = (card, cardId) =&gt; {
    console.log(`cardId : ${cardId}, card: ${card}`);
  },
  onCardSubmit = (card, cardId) =&gt; {
    console.log(`cardId : ${cardId}, card: ${card}`);
  },
  children,
}) {
  const AppContextValue = { cardList, onCardConfirm, onCardUpdate, onCardDelete, onCardSubmit };

  return &lt;ApplicationContext.Provider value={AppContextValue}&gt;{children}&lt;/ApplicationContext.Provider&gt;;
}</code></pre>
<h1 id="너와-나와-우리를-위한-클린코드">너와 나와 우리를 위한 클린코드</h1>
<blockquote>
<p>지금 이 코드를 리뷰해야하는 <strong>지금의 너</strong>와 내 코드를 보고 놀랄 <strong>미래의 나</strong>와 오랫동안 함께 이 코드를 관리해야 할 <strong>우리 팀</strong>을 위한 <strong>클린코드</strong>.</p>
</blockquote>
<p>앞서 제목으로 너와 나와 우리를 위한 클린코드라는 제목으로 이 포스트를 시작했습니다.</p>
<p>코드를 계속해서 수정해보면서 느꼈던 클린코드의 중요성을 한줄의 제목으로 표현해봤습니다. 😁</p>
<p>페이먼츠 과제 리뷰어님과의 열띤 리뷰가 있은 후에 클린코드에 대한 post들과 클린코드 책(written by Robert C. Martin)을 읽으면서 클린코드에 대해 다시 생각해보았습니다.</p>
<h2 id="클린코드와-클린-아키텍쳐는-코드-변경을-쉽고-빠르게-하기-위한-노력이다">클린코드와 클린 아키텍쳐는 코드 변경을 쉽고 빠르게 하기 위한 노력이다.</h2>
<p>클린코드 책에선 <strong>소프트웨어은 살아 숨쉬는 것</strong>이며, 이를 건축과 비교하여 프로그래밍을 설명합니다.</p>
<p>건축과 프로그래밍의 큰 차이는 <strong>재설계가 가능하다!</strong> 라는 것 입니다.</p>
<p>코드와 설계를 조금씩 고쳐가며 소프트웨어가 성장해나가는데, 이 과정을 잘해내기 위한 것이 클린코드입니다.</p>
<p>클린하게 코드를 정리해두면 기존 코드를 누구든 금방 이해할 수 있고, 최소한의 코드만 수정해도 됩니다.</p>
<ul>
<li><p><strong>코드 변경의 파급을 최소화합니다. 정말 변경이 필요한 부분만 살짝 바꿀 수 있도록 합니다.</strong></p>
</li>
<li><p><strong>코드를 읽기 쉽고 직관적으로 이해하기 쉽도록 하여 코드를 읽는 비용을 최소화합니다.</strong></p>
</li>
</ul>
<p>클린코드의 목적을 두가지로 간단하게 정리해봤습니다.</p>
<h2 id="코드는-함께-보는-것이다-명확한-원칙으로-코드의-의도를-뚜렷히-드러내야한다">코드는 함께 보는 것이다. 명확한 원칙으로 코드의 의도를 뚜렷히 드러내야한다.</h2>
<p>복잡한 코드를 해결하기 위한 기법들이 필요 이상으로 적용되면, 오히려 제 코드의 의도를 뚜렷하게 전달하기 힘들게 됩니다.</p>
<p>네이밍과 프로젝트 구조 마찬가지입니다. 명확한 기준으로 나누어 적용하지 않으면, 혼란스러워지고 코드의 의도를 흐리게 만듭니다.</p>
<p><strong>코드의 의도가 흐려지면, 리뷰어들은 코드를 읽는데 더 어려움</strong>을 겪게 되겠죠,,,😱</p>
<p>기술적으로 놀라운 설계 기법과 디자인 패턴이 적용되었다고 해도, 상황에 적절하지 않다면 오히려 독이되는 것 같습니다.</p>
<p>&quot;어려운 것을 쉽고 단순해보이게 하는 것이 진짜 실력자다&quot; 라는 말이 생각나는데, 코드의 세계에서도 동일한 것 같습니다.</p>
<h1 id="마무리">마무리</h1>
<p>어마어마한 삽질의 시간이었지만 😅, 프로그래밍의 가장 기본이 되는 클린코드에 대해 고찰하고 공부할 수 있는 소중한 기회였다고 생각합니다.</p>
<p>정말 유익한 시간이었습니다. 😁</p>
<p>제 허접한 코드를 읽는데 고생하시고 많은 도움주신 리뷰어님께 무한한 감사의 말씀을 드립니다! 🙌</p>
<p>감사합니다.</p>
<h2 id="ref">ref</h2>
<p>클린코드 (written by Robert C. Martin)</p>
<p><a href="https://so-so.dev/essay/2020-my-good-code/#%EC%A0%81%EB%8B%B9%ED%95%9C-%EC%9E%90%EB%A3%8C%ED%98%95">JBee, 좋은코드란 무엇일까?</a></p>
<p><a href="https://so-so.dev/essay/2020-my-good-code/#%EC%A0%81%EB%8B%B9%ED%95%9C-%EC%9E%90%EB%A3%8C%ED%98%95">sosoLog, 내가 생각한 좋은코드</a></p>
<p><a href="https://overreacted.io/goodbye-clean-code/">Overreacted, Goodbye, Clean code</a></p>
<p><a href="https://medium.com/naver-cloud-platform/%EB%84%A4%EC%9D%B4%EB%B2%84%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%A2%8B%EC%9D%80-%EC%BD%94%EB%93%9C%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-c7811f73a46b">Naver cloud platform, 좋은 코드란 무엇일까?🤔 #클린코드 이야기</a></p>
<p><a href="https://www.youtube.com/watch?v=ssDMIcPBqUE">우아한 테크 세미나, 지속가능한 SW 개발을 위한 코드리뷰 유튜브</a></p>
]]></description>
        </item>
    </channel>
</rss>