<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>vitnal 아카이브</title>
    <link>https://jcon.tistory.com/</link>
    <description>4년차 프론트엔드 개발자 입니다. 이 블로그는 기록하고 싶은 내용을 저장하기 위해 사용하고 있습니다. 정제되지 않은 내용이 있을 수 있는 점 양해 부탁드립니다.</description>
    <language>ko</language>
    <pubDate>Mon, 6 Jul 2026 23:50:21 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>vitnal</managingEditor>
    <image>
      <title>vitnal 아카이브</title>
      <url>https://tistory1.daumcdn.net/tistory/2909963/attach/89bb1f4aee4e4261b9136090104a25b9</url>
      <link>https://jcon.tistory.com</link>
    </image>
    <item>
      <title>실무에서 바로 쓰는 Frontend Clean Code</title>
      <link>https://jcon.tistory.com/211</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://youtu.be/edWbHp_k_9Y?si=TeKCnZaFEhmjq0tk&quot;&gt;https://youtu.be/edWbHp_k_9Y?si=TeKCnZaFEhmjq0tk&lt;/a&gt;&lt;/p&gt;
&lt;iframe src=&quot;https://www.youtube.com/embed/edWbHp_k_9Y&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;4년전 영상이지만 프론트엔드에서 클린코드를 작성하는 방법에 대해서 예제와 함께 친절하고 상세하게 설명하고 있습니다.&lt;/p&gt;
&lt;p&gt;영상의 내용 중 지속적으로 생각하고 적용해야 하는 것을 정리합니다.&lt;/p&gt;
&lt;h2&gt;1. 함수의 단일 책임&lt;/h2&gt;
&lt;p&gt;이 개념은 가장 널리 알려져 있지만, 기능 추가를 하게 되었을 때 핸들러에는 적용을 잘 안시켰던 것 같습니다.&lt;/p&gt;
&lt;p&gt;핸들러 또한 단일 책임을 가지도록 분리하여 필요한 곳에 필요한 함수만 사용될 수 있도록 합니다.&lt;/p&gt;
&lt;h2&gt;2. 응집도를 높이는 것과 추상화 레벨&lt;/h2&gt;
&lt;p&gt;코드의 가독성과 유지보수를 높이기 위해서는 응집도를 높여햐 합니다.&lt;/p&gt;
&lt;p&gt;응집도를 높인다는 뜻은 컴포넌트 안에 상태, 핸들러, jsx가 서로 떨어져 있는 것을 &lt;/p&gt;
&lt;p&gt;하나의 컴포넌트 또는 커스텀훅으로 묶어서 어떤 로직인지 쉽게 파악하도록 하는 것 입니다.&lt;/p&gt;
&lt;p&gt;여기서 주의 할 점은 뭉치기만 하는 것은 클린 코드와는 다르다는 것 입니다.&lt;/p&gt;
&lt;p&gt;영상에서도 클린 코드는 짧은 코드가 아닌 읽기 쉬운 코드라고 전달하고 있습니다.&lt;/p&gt;
&lt;p&gt;따라서 뭉치는 작업을 할 때에 세부 구현과 중요 개념을 불리하여 세부구현만 추상화를 하고,&lt;/p&gt;
&lt;p&gt;중요 개념은 props로 받도록 하여 로직을 파악하기 쉽도록 해야 합니다.&lt;/p&gt;
&lt;h2&gt;배운 점&lt;/h2&gt;
&lt;p&gt;응집도를 높이기 위해 커스텀 훅이나 컴포넌트를 사용할 때 명확한 기준을 잡을 수 있는 내용이었습니다.&lt;/p&gt;
&lt;p&gt;깔끔하게만 보이는 코드가 아니라 읽기 쉽고 유지보수가 쉬운 코드를 작성하는 좀 더 명확한 기준이 생기게 되었습니다.&lt;/p&gt;</description>
      <category>What I Read</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/211</guid>
      <comments>https://jcon.tistory.com/211#entry211comment</comments>
      <pubDate>Wed, 3 Dec 2025 16:45:40 +0900</pubDate>
    </item>
    <item>
      <title>Antigravity(반중력)에 TaskMaster MCP 서버 연결 삽질기</title>
      <link>https://jcon.tistory.com/210</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 구글에서 에이전트 중심 IDE인 Antigravity를 출시했습니다. 마침 cursor 에이전트가 limit이 걸려있던 참이라 바로 설치해보았습니다. AI는 MCP가 있어야 성능을 최대로 끌어낼 수 있다고 생각하기 때문에 처음으로 한 일은 MCP를 설정하는 것 이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Antigravity는 신기하게도 내부에 MCP Store가 있어서 Smithery처럼 원클릭으로 간편하게 설치가 가능했습니다. 하지만 아직 베타버전이라 그런지 종류는 많이 없어서 제가 사용하고 있는 MCP 대부분은 직접 설정을 해주어야 했습니다. 그 과정에서 겪은 두가지 오류와 해결과정을 정리합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 1: Error: exec: &quot;npx&quot;: executable file not found in $PATH.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 설정 파일에 직접 json으로 추가하고 나서 첫 번째 에러가 발생하였습니다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Error: exec: &quot;npx&quot;: executable file not found in $PATH&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 추정: asdf 삭제의 나비효과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npx 경로를 찾지 못하는 에러 였는데, 원인은 기존에 사용하던 Node 버전 관리 도구인 asdf를 삭제하고 fnm으로 갈아타면서 PATH가 꼬인 것으로 추정되었습니다. 터미널에서 npx는 잘 동작했지만 IDE는 Node가 어디에 있는지 모르는 상태가 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번 째 시도: 절대경로로 설정하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 시도한 방법은 절대 경로를 넣어 MCP가 실행이 되는지 확인 하는 것 이었습니다. 실행이 된다면 MCP 설정 방법은 문제가 없다는 뜻이기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;which npx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 which 명령어를 사용하여 npx의 위치는 찾아내고&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;task-master-ai&quot;: {
      &quot;command&quot;: &quot;/User/.../npx&quot;, // which로 찾은 절대경로 입력
      &quot;args&quot;: [&quot;-y&quot;, &quot;task-master-ai&quot;]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;command에 절대 경로를 입력한 후 확인 하니 PATH 오류는 수정되었습니다. 하지만 절대 경로를 사용하면 Node버전이 변경 될 때마다 설정 파일을 수정해야 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번 째 시도: 시스템 심볼릭 링크 연결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;command에 npx만 적어두고 싶은 마음이 생겼습니다. 이를 위해서 IDE가 보통 기본적으로 탐색하는 표준 경로(&lt;code&gt;/opt/homebrew/bin&lt;/code&gt;)에 fnm Node위치를 연결했습니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# 출력: /Users/me/.local/share/fnm/.../bin/node
which node

# 시스템 표준 경로에 심볼릭 링크 생성
sudo ln -s $(which node) /opt/homebrew/bin/node&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 심볼릭 링크 연결을 통해서 command에 다시 &lt;code&gt;npx&lt;/code&gt;만 작성하여 MCP를 실행되게 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 PATH문제는 해결했지만...&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 2: Error: calling &quot;initialize&quot;: EOF&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 난해한 에러가 나타나게 되었습니다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Error: calling &quot;initialize&quot;: EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EOF에러가 나면서 연결이 맺어지기도 전에 MCP가 픽 죽어버렸습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 추정: 터미널에서 직접 실행해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE 로그만으로는 원인을 알 수 없어, 터미널에서 직접 서버를 실행해 보았습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 터미널에서 실행을 위해 글로벌 설치
npm install -g task-master-ai

# 터미널에서 직접 실행 (API 키를 붙여서 실행)
export GOOGLE_API_KEY=&quot;AIz...&quot;
npx -y task-master-ai&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과, 정상적으로 동작하며 터미널에 아래와 같은 로그가 출력되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서는 정상 동작함으로써 유추할 수 있는 원인이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 프로토콜은 클라이언트(IDE)와 서버가 오직 JSON 포맷으로만 대화해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 taskmaster는 실행되자마자 [INFO]... 같은 일반 텍스트 로그(Plain Text)를 표준 출력(stdout)에 노출하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람 눈에는 친절한 정보였지만, JSON만 기다리던 기계(IDE)에게는 해석할 수 없는 데이터였고, 결국 프로토콜 위반으로 연결을 끊어버린 것(EOF)으로 추정하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방안: 로그 필터링 미들웨어 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 텍스트 로그는 버리고 JSON만 통과시켜보기로 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, Node.js의 spawn을 이용해 실제 서버를 실행하되, stdout을 가로채서 필터링하는 스크립트를 생성해야합니다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;// mute_logs.js
const { spawn } = require('child_process');
// 실제 실행할 패키지 경로 (which task-master-ai 로 확인)
const EXECUTABLE_PATH = '/.../bin/task-master-ai';

const proc = spawn(process.execPath, [EXECUTABLE_PATH], {
  env: process.env,
  stdio: ['inherit', 'pipe', 'inherit'], // stdout 가로채기
});

proc.stdout.on('data', (data) =&amp;gt; {
  const output = data.toString();

  output.split('\n').forEach((line) =&amp;gt; {
    if (!line.trim()) return;

    // JSON 형식('{')인 경우에만 진짜 출력으로 내보냄 (MCP 통신용)
    if (line.trim().startsWith('{')) {
      process.stdout.write(line + '\n');
    } else {
      // [INFO] 같은 텍스트 로그는 에러 채널(stderr)로 우회 (연결 유지)
      console.error(`[Filtered]: ${line}`);
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Antigravity MCP 설정에서 task-master-ai를 직접 실행하는 게 아니라, mute_logs.js를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;&quot;task-master-ai&quot;: {
  &quot;command&quot;: &quot;node&quot;,
  &quot;args&quot;: [&quot;/Users/me/scripts/mute_logs.js&quot;], // 래퍼 스크립트 실행
  &quot;env&quot;: { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 MCP 연결 상태를 보니 정상적으로 동작하는 것을 확인 할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cursor 등 다른 에디터에서는 문제 없는 것을 보면 Antigravity에서 처리가 안되어 있는것 같은데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애매한건 검색을 해보면 저와 같은 증상을 찾기가 힘듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 컴퓨터 설정 자체로 생긴 문제 일 수도 있지만 해결하는 과정에서 PATH를 심볼릭 링크로 잡는 방법과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 설정 방법에 대해서 좀 더 이해할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 삽질기가 npx not found나 EOF 에러로 고통받는 분들에게 도움이 되기를 바랍니다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>MCP</category>
      <category>taskmaster</category>
      <category>반중력</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/210</guid>
      <comments>https://jcon.tistory.com/210#entry210comment</comments>
      <pubDate>Wed, 26 Nov 2025 22:10:08 +0900</pubDate>
    </item>
    <item>
      <title>  Antigravity Editor에서 MCP 설정 및 활용하기</title>
      <link>https://jcon.tistory.com/209</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서: &lt;a href=&quot;https://antigravity.google/docs/mcp&quot;&gt;https://antigravity.google/docs/mcp&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  MCP란 무엇이며 왜 필요한가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 Antigravity Editor와 개발 환경 사이의 &lt;b&gt;다리(Bridge)&lt;/b&gt; 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 데이터베이스 스키마, 로그 파일 등의 컨텍스트를 AI에게 제공하려면 내용을 복사하여 붙여넣어야 했습니다. 하지만 MCP를 사용하면 Antigravity가 연결된 MCP 서버에서 이 정보를 필요할 때 &lt;b&gt;직접 가져와서&lt;/b&gt; AI 제안의 정확도와 관련성을 획기적으로 높여줍니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✨ MCP의 핵심 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 통합을 통해 안티그래비티(Antigravity Editor)는 두 가지 주요 기능을 수행하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 컨텍스트 리소스 (Context Resources)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 연결된 MCP 서버의 데이터를 읽어 코드 제안 및 디버깅 정보를 제공합니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;활용 분야&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;작동 방식&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;코드 작성&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;데이터베이스 스키마 검사&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;SQL 쿼리 작성 시, &lt;b&gt;Neon&lt;/b&gt;이나 &lt;b&gt;Supabase&lt;/b&gt; 스키마를 확인하여 올바른 테이블/컬럼 이름 제안&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;디버깅&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;실시간 로그 데이터 접근&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;디버깅 중 &lt;b&gt;Netlify&lt;/b&gt;나 &lt;b&gt;Heroku&lt;/b&gt;에서 최근 빌드 로그를 가져와 문제 해결에 도움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 커스텀 도구 (Custom Tools)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP를 통해 연결된 서버가 정의한 &lt;b&gt;특정 안전 작업&lt;/b&gt;을 Antigravity가 실행할 수 있게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;이 &lt;b&gt;TODO&lt;/b&gt; 항목에 대해 &lt;b&gt;Linear&lt;/b&gt; 이슈 생성해 줘.&quot;&lt;/li&gt;
&lt;li&gt;&quot;인증 패턴을 찾기 위해 &lt;b&gt;Notion&lt;/b&gt;이나 &lt;b&gt;GitHub&lt;/b&gt;을 검색해 줘.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚙️ MCP 연결 가이드: 내장 서버 설치 및 사용자 정의 서버 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Antigravity Editor에서 MCP 서버를 연결하는 방법은 매우 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아직 몇개 없어서 아쉽습니다. 그래도 커스텀 설정도 다른 에이전트와 비슷합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 지원되는 서버 연결 (MCP Store 이용)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 인기 있는 서비스는 내장된 &lt;b&gt;MCP Store&lt;/b&gt;를 통해 쉽게 연결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 스토어 접근:&lt;/b&gt; 에디터 사이드 패널 상단의 &lt;b&gt;&quot;...&quot; 드롭다운&lt;/b&gt; 메뉴를 열고 &lt;b&gt;MCP Store&lt;/b&gt; 패널을 엽니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wwyxn/dJMcabbxPTH/V92USskwU1oD8ZaWzC8ykk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wwyxn/dJMcabbxPTH/V92USskwU1oD8ZaWzC8ykk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wwyxn/dJMcabbxPTH/V92USskwU1oD8ZaWzC8ykk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwwyxn%2FdJMcabbxPTH%2FV92USskwU1oD8ZaWzC8ykk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;703&quot; height=&quot;590&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 설치:&lt;/b&gt; 목록에서 원하는 서버(예: GitHub, Notion, Linear 등)를 선택하고 &lt;b&gt;Install&lt;/b&gt;을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dUcCI1/dJMcahW8JbR/3GEdLX1nchYFBwp7eUKAmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dUcCI1/dJMcahW8JbR/3GEdLX1nchYFBwp7eUKAmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dUcCI1/dJMcahW8JbR/3GEdLX1nchYFBwp7eUKAmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdUcCI1%2FdJMcahW8JbR%2F3GEdLX1nchYFBwp7eUKAmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;446&quot; height=&quot;537&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 인증:&lt;/b&gt; 화면의 안내에 따라 계정을 안전하게 연결(로그인 및 권한 부여)합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 해당 서버의 리소스와 도구는 에디터에서 자동으로 사용 가능해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 사용자 정의 MCP 서버 연결 (Custom MCP Servers)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 시스템이나 직접 구축한 MCP 서버에 연결해야 하는 경우, 설정을 수동으로 수정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. MCP 스토어 열기:&lt;/b&gt; 에디터의 에이전트 패널 상단 &lt;b&gt;&quot;...&quot; 드롭다운&lt;/b&gt;을 통해 MCP Store를 엽니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wwyxn/dJMcabbxPTH/V92USskwU1oD8ZaWzC8ykk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wwyxn/dJMcabbxPTH/V92USskwU1oD8ZaWzC8ykk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wwyxn/dJMcabbxPTH/V92USskwU1oD8ZaWzC8ykk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwwyxn%2FdJMcabbxPTH%2FV92USskwU1oD8ZaWzC8ykk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;703&quot; height=&quot;590&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 서버 관리:&lt;/b&gt; &lt;b&gt;&quot;Manage MCP Servers&quot;&lt;/b&gt;를 클릭합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;설정 보기:&lt;/b&gt; &lt;b&gt;&quot;View raw config&quot;&lt;/b&gt;를 클릭합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정:&lt;/b&gt; &lt;code&gt;mcp_config.json&lt;/code&gt; 파일을 열고 사용자 정의 MCP 서버의 &lt;b&gt;구성 정보&lt;/b&gt;를 추가하여 저장합니다.&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BhMWC/dJMcagKGV2M/dokQbUKFk2BOLf27v5YYF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BhMWC/dJMcagKGV2M/dokQbUKFk2BOLf27v5YYF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BhMWC/dJMcagKGV2M/dokQbUKFk2BOLf27v5YYF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBhMWC%2FdJMcagKGV2M%2FdokQbUKFk2BOLf27v5YYF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;471&quot; height=&quot;194&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;참고:&lt;/b&gt; 사용자 정의 설정은 &lt;code&gt;mcp_config.json&lt;/code&gt; 파일을 수정하여 이루어집니다.&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  현재 지원되는 주요 MCP 서버 목록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP Store는 현재 아래와 같은 다양한 개발 및 협업 도구와의 통합을 지원합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Airweave&lt;/li&gt;
&lt;li&gt;Atlassian&lt;/li&gt;
&lt;li&gt;CodeMind&lt;/li&gt;
&lt;li&gt;Dart&lt;/li&gt;
&lt;li&gt;Figma Dev Mode MCP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Harness&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Heroku&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Linear&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Locofy&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MongoDB&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Neon&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Netlify&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Notion&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;PayPal&lt;/li&gt;
&lt;li&gt;Perplexity Ask&lt;/li&gt;
&lt;li&gt;Prisma&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Sequential Thinking&lt;/li&gt;
&lt;li&gt;SonarQube&lt;/li&gt;
&lt;li&gt;Stripe&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Supabase&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직까지 지원되는게 많지 않지만 Smithery 등 mcp 스토어 방문 필요 없이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간편하게 연동이 되는 점은 매우 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종류 수는 점점 많이 질 것으로 예상 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI</category>
      <category>MCP</category>
      <category>반중력</category>
      <category>안티그래비티</category>
      <category>커스텀</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/209</guid>
      <comments>https://jcon.tistory.com/209#entry209comment</comments>
      <pubDate>Sun, 23 Nov 2025 06:17:37 +0900</pubDate>
    </item>
    <item>
      <title>[일티클] 최고 개발자들의 성공 공식 한가지</title>
      <link>https://jcon.tistory.com/208</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://www.youtube.com/watch?v=eQ604ruizNI&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=eQ604ruizNI&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루에 개발 관련 컨텐츠 하나씩 정리해보는 챌린지를 하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 본 내용은 &quot;&lt;span style=&quot;color: #000000; letter-spacing: -1px; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;최고의 개발자들에게는 이런 '성공 공식'이 있다&quot; 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최고 개발자들이 가지고 있던 성공 공식은 &quot;개발에 대한 열정과 흥미&quot; 였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 개발을 좋아하라는 것이 아닌, 내가 어떤 개발을 좋아하는지 찾는 것 또한 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임, 안드로이드, ios, 프론트엔드, 백엔드 등등 어떤 개발 분야에 흥미를 느끼는지 스스로 알아야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상에서는 흥미를 찾는 가장 쉬운 방법은 &quot;직접 만들어 보는 것&quot;이라고 소개하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 지금은 웹개발을 하고 있더라도, 내가 정말 흥미가 있는지 확신이 안선다면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 개발 등 다른 분야의 개발을 해보면서 내가 흥미가 생기는지 알아가는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순하지만 확실한 방법입니다. 프론트엔드도 흥미를 가지고 있지만 부트캠프에서 공부할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 다양하게 시도해보고 선택했으면 어땠을까 하는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 RN을 공부해 볼까 고민 중이었는데&amp;nbsp;지금이라도 꼭 어플 하나를 만들어 보아야겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;비교군이 생겼을 때 지금 하는 개발에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;더 흥미를 느끼게 될 수도 있으니깐요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 본인들이 흥미있는 개발을 하면서 자연스럽게 최고 개발자가 되시길 응원합니다.&lt;/p&gt;</description>
      <category>What I Read</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/208</guid>
      <comments>https://jcon.tistory.com/208#entry208comment</comments>
      <pubDate>Sat, 22 Nov 2025 22:56:08 +0900</pubDate>
    </item>
    <item>
      <title>taskmaster + cursor 실행 가이드</title>
      <link>https://jcon.tistory.com/207</link>
      <description>&lt;h1&gt;taskmaster 설정 방법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서 튜토리얼: &lt;a href=&quot;https://github.com/eyaltoledano/claude-task-master/blob/main/docs/tutorial.md&quot;&gt;https://github.com/eyaltoledano/claude-task-master/blob/main/docs/tutorial.md&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 및 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 글로벌 설치
npm install -g task-master-ai

# 프로젝트 내부 설치
npm install task-master-ai&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;npx task-master init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정:&lt;br /&gt;.env 파일 또는 .cursor/mcp.json 파일에 사용할 모델의 api key를 입력&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;계획 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구 사항 생성 후 ai를 통해 &lt;code&gt;.taskmaster/templates/example_prd.txt&lt;/code&gt;을 참고 하여 &lt;code&gt;.taskmaster/docs/prd.txt&lt;/code&gt; 파일을 생성해달라고 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 계획 확인 후&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;task-master parse-prd .taskmaster/docs/prd.txt&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;복잡한 작업 분해(선택사항)&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 복잡도 계산
analyze-complexity 

# 작업 분리
task-master expand --all

#또는

task-master expand --id=5 --num=3&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;명령어&lt;/h2&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# 계획 목록 조회
task-master list --with-subtasks

# 계획 실행
task-master next&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모델 설정 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 설정:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;task-master models --setup&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 모델 확인:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;task-master models&lt;/code&gt;&lt;/pre&gt;</description>
      <category>AI</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/207</guid>
      <comments>https://jcon.tistory.com/207#entry207comment</comments>
      <pubDate>Sat, 22 Nov 2025 12:15:45 +0900</pubDate>
    </item>
    <item>
      <title>중학생도 이해하는 CSRF</title>
      <link>https://jcon.tistory.com/206</link>
      <description>&lt;h1&gt; ️ CSRF (Cross-Site Request Forgery) 쉽게 이해하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSRF&lt;/b&gt;는 해커가 &lt;b&gt;'나인 척'&lt;/b&gt; 가장해서 웹사이트에 이상한 요청을 보내는 공격 방법입니다.&lt;br /&gt;쉽게 말해, 내가 로그인되어 있다는 점을 악용해 &lt;b&gt;내가 원하지 않은 행동(비밀번호 변경, 결제, 글 작성 등)을 나도 모르게 하게 만드는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  1. 쉬운 비유: 놀이공원 자유이용권 사건&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황을 &lt;b&gt;'놀이공원'&lt;/b&gt;에 비유하면 아주 쉽게 이해할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;① 로그인 (자유이용권 발급)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분이 놀이공원에서 손목에 &lt;b&gt;'자유이용권 밴드'&lt;/b&gt;를 찼습니다.&lt;/p&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;b&gt;(인터넷 상황)&lt;/b&gt;: 웹사이트에 로그인해서 &lt;b&gt;'쿠키/세션'&lt;/b&gt;을 받은 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② 함정 (사기꾼의 등장)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벤치에서 쉬고 있는데, 낯선 사람이 다가와 &lt;b&gt;&quot;이 상자 안에 뭐가 있는지 구경만 해볼래?&quot;&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;(인터넷 상황)&lt;/b&gt;: 해커가 보낸 &lt;b&gt;이메일이나 링크&lt;/b&gt;를 클릭&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;③ 공격 (가짜 주문)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상자를 여는 순간, 상자 바닥에 숨겨진 무전기가 매점에 주문을 넣습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;&quot;지금 이 밴드를 찬 학생 앞으로 햄버거 100개 주문! 결제는 밴드로!&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;④ 피해 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매점 직원은 무전기 신호와 함께 여러분의 &lt;b&gt;'자유이용권 밴드'&lt;/b&gt;가 확인되니, 여러분이 직접 주문한 줄 알고 결제해 버립니다. 이것이 바로 &lt;b&gt;CSRF&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  2. 실제 인터넷에서는 어떻게 일어날까요?&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;로그인 상태&lt;/b&gt;: 여러분이 은행 사이트에 &lt;b&gt;로그인&lt;/b&gt;을 해둡니다. (브라우저가 로그인 증표를 기억함)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;미끼 클릭&lt;/b&gt;: 해커가 보낸 &quot;경품 당첨&quot; 링크를 클릭합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;몰래 전송&lt;/b&gt;: 해커의 사이트에는 눈에 보이지 않는 명령어가 숨겨져 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*&quot;은행아, 지금 접속한 사람 계좌에서 내 계좌로 100만 원 보내줘.&quot;*&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;자동 인증&lt;/b&gt;: 브라우저는 은행으로 명령을 보낼 때, &lt;b&gt;여러분이 이미 가지고 있던 로그인 증표(쿠키)를 자동으로 같이 보냅니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공격 성공&lt;/b&gt;: 은행은 로그인된 사용자의 요청이라 믿고 돈을 송금합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 3. 어떻게 막을까요? (방어법)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자들은 이를 막기 위해 &lt;b&gt;'CSRF 토큰'&lt;/b&gt;이라는 &lt;b&gt;일회용 비밀 암호&lt;/b&gt;를 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기존 방식 (위험)&lt;/b&gt;: &quot;자유이용권 밴드만 보여주면 통과!&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방어 방식 (안전)&lt;/b&gt;: &quot;자유이용권 밴드와 함께, &lt;b&gt;지금 화면에 떠 있는 비밀 암호('바나나')&lt;/b&gt;를 같이 말해야 통과!&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해커는 여러분의 &lt;b&gt;로그인 상태(밴드)&lt;/b&gt;는 몰래 이용할 수 있어도, 여러분 화면에만 떠 있는 &lt;b&gt;순간의 비밀 암호(토큰)&lt;/b&gt;는 알 수 없어서 공격에 실패하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;b&gt;CSRF란?&lt;/b&gt; 내 로그인 권한을 도용해 해커가 몰래 명령을 내리는 공격.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인은?&lt;/b&gt; 브라우저가 로그인 증표(쿠키)를 자동으로 첨부하는 특성을 악용함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책?&lt;/b&gt; &lt;b&gt;CSRF 토큰&lt;/b&gt; 같은 추가 인증 수단을 사용해 방어함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 내용과 이어지는 &lt;b&gt;실전 적용 편&lt;/b&gt;입니다. 개념을 이해했다면, 실제 프로젝트 코드에 바로 적용할 수 있도록 정리했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt; ️ 개발자를 위한 CSRF 방어 가이드 (React/Next.js)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 개념을 실제 코드로 구현하는 3단계 방어 전략입니다. **1차 방어선(쿠키 설정)**만 잘해도 대부분의 공격을 막을 수 있으며, **2차 방어선(토큰)**까지 구축하면 금융권 수준의 보안을 갖출 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  1. [필수] 1차 방어선: SameSite 쿠키 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 가성비가 좋고 강력한 방법입니다. 브라우저에게 **&quot;이 쿠키는 우리 사이트에서만 써!&quot;**라고 강제하는 옵션입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Backend 설정 예시 (Node.js / Next.js API)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 시 쿠키를 발급하는 코드에 &lt;code&gt;sameSite&lt;/code&gt; 옵션을 추가하세요.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// res.setHeader 또는 쿠키 설정 라이브러리 사용 시
res.setHeader('Set-Cookie', serialize('auth_token', token, {
  httpOnly: true, // 자바스크립트로 탈취 불가 (XSS 방어)
  secure: process.env.NODE_ENV === 'production', // HTTPS에서만 전송
  sameSite: 'lax', // ✅ 핵심! (Lax 또는 Strict 사용)
  path: '/',
}));&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Strict&lt;/code&gt;&lt;/b&gt;: 오직 같은 도메인 내에서만 쿠키 전송 (가장 안전하지만, 외부 링크 타고 들어올 때 로그인 풀릴 수 있음)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Lax&lt;/code&gt;&lt;/b&gt;: 안전한 이동(링크 클릭 등)은 허용하되, 공격 가능성이 있는 요청(POST, Form 전송 등)은 차단. &lt;b&gt;(권장)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  2. [권장] 2차 방어선: CSRF Token (Double Submit Cookie)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 데이터 변경(계정 수정, 결제)이 일어나는 서비스라면 토큰 검증을 추가해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Frontend 구현 예시 (React + Axios)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 수동으로 토큰을 넣지 않고, &lt;b&gt;Axios Interceptor&lt;/b&gt;를 사용해 모든 요청 헤더에 토큰을 자동으로 심어줍니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import axios from 'axios';

// 1. 쿠키에서 CSRF 토큰 값 추출 함수
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}

const api = axios.create({
  baseURL: '/api',
  withCredentials: true, // ✅ 쿠키(토큰)를 주고받으려면 필수 설정
});

// 2. 요청 가로채기 (Interceptor)
api.interceptors.request.use((config) =&amp;gt; {
  // 보통 서버가 'XSRF-TOKEN'이라는 이름으로 쿠키를 내려줍니다.
  const csrfToken = getCookie('XSRF-TOKEN'); 

  if (csrfToken) {
    // 헤더에 토큰을 실어 보냅니다. (백엔드 설정에 따라 키 이름 확인 필요)
    config.headers['X-XSRF-TOKEN'] = csrfToken; 
  }
  return config;
});

export default api;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  3. [최신] Next.js 사용자라면? (Server Actions)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Next.js App Router&lt;/b&gt;를 사용 중이라면 훨씬 편합니다. &lt;b&gt;Server Actions&lt;/b&gt;는 기본적으로 CSRF 공격을 방어하도록 설계되어 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Server Action 예시 (자동 방어)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 토큰 설정 없이, 폼 태그의 &lt;code&gt;action&lt;/code&gt;에 서버 함수를 바로 연결하면 됩니다. Next.js가 내부적으로 해시 ID와 출처(Origin)를 검증합니다.&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;// app/actions.ts (Server)
'use server'

export async function updateProfile(formData: FormData) {
  //  ️ Next.js가 이미 CSRF 검증을 마친 상태입니다.
  const name = formData.get('name');
  await db.user.update({ where: { id: 1 }, data: { name } });
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// app/page.tsx (Client)
'use client'
import { updateProfile } from './actions';

export default function Page() {
  return (
    &amp;lt;form action={updateProfile}&amp;gt;
      &amp;lt;input name=&quot;name&quot; placeholder=&quot;새 이름 입력&quot; /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  요약 체크리스트&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;b&gt;백엔드&lt;/b&gt;: 쿠키 옵션에 &lt;code&gt;SameSite: 'Lax'&lt;/code&gt;가 설정되어 있는가?&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;b&gt;프론트엔드&lt;/b&gt;: &lt;code&gt;withCredentials: true&lt;/code&gt; 설정이 되어 있는가?&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;b&gt;프론트엔드&lt;/b&gt;: (필요시) 헤더에 &lt;code&gt;X-CSRF-TOKEN&lt;/code&gt;을 실어 보내는가?&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>WEB</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/206</guid>
      <comments>https://jcon.tistory.com/206#entry206comment</comments>
      <pubDate>Thu, 20 Nov 2025 18:15:59 +0900</pubDate>
    </item>
    <item>
      <title>[일티클] AI 코딩 도구가 효과가 없나요? 당신의 코드 아키텍처가 문제일 수 있습니다</title>
      <link>https://jcon.tistory.com/205</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@surim014/ai-coding-tools-not-paying-off-your-code-architecture-might-be-to-blame?utm_source=substack&amp;amp;utm_medium=email&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@surim014/ai-coding-tools-not-paying-off-your-code-architecture-might-be-to-blame?utm_source=substack&amp;amp;utm_medium=email&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763468946847&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[번역] AI 코딩 도구가 효과가 없나요? 당신의 코드 아키텍처가 문제일 수 있습니다&quot; data-og-description=&quot;원문: https://jellyfish.co/blog/ai-coding-tools-not-paying-off-your-code-architecture-might-be-to-blame/&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@surim014/ai-coding-tools-not-paying-off-your-code-architecture-might-be-to-blame?utm_source=substack&amp;amp;utm_medium=email&quot; data-og-url=&quot;https://velog.io/@surim014/ai-coding-tools-not-paying-off-your-code-architecture-might-be-to-blame&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HD21a/hyZN6Szt8B/lBsxBDBawiya0YhrA6QvQK/img.jpg?width=1536&amp;amp;height=842&amp;amp;face=0_0_1536_842,https://scrap.kakaocdn.net/dn/cywKSn/hyZNShjFMh/BcJlPOR6I1jX6k2bKr7Lc0/img.jpg?width=1536&amp;amp;height=842&amp;amp;face=0_0_1536_842,https://scrap.kakaocdn.net/dn/bFvl52/hyZNCR7Hpx/fiS9hMMjjuWY4Ljd93vahk/img.png?width=2338&amp;amp;height=846&amp;amp;face=0_0_2338_846&quot;&gt;&lt;a href=&quot;https://velog.io/@surim014/ai-coding-tools-not-paying-off-your-code-architecture-might-be-to-blame?utm_source=substack&amp;amp;utm_medium=email&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@surim014/ai-coding-tools-not-paying-off-your-code-architecture-might-be-to-blame?utm_source=substack&amp;amp;utm_medium=email&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HD21a/hyZN6Szt8B/lBsxBDBawiya0YhrA6QvQK/img.jpg?width=1536&amp;amp;height=842&amp;amp;face=0_0_1536_842,https://scrap.kakaocdn.net/dn/cywKSn/hyZNShjFMh/BcJlPOR6I1jX6k2bKr7Lc0/img.jpg?width=1536&amp;amp;height=842&amp;amp;face=0_0_1536_842,https://scrap.kakaocdn.net/dn/bFvl52/hyZNCR7Hpx/fiS9hMMjjuWY4Ljd93vahk/img.png?width=2338&amp;amp;height=846&amp;amp;face=0_0_2338_846');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[번역] AI 코딩 도구가 효과가 없나요? 당신의 코드 아키텍처가 문제일 수 있습니다&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;원문: https://jellyfish.co/blog/ai-coding-tools-not-paying-off-your-code-architecture-might-be-to-blame/&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 관련 아티클을 읽으면서 정리해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &quot;AI 코딩 도구가 효과가 없나요? 당신의 코드 아키텍처가 문제일 수 있습니다&quot;라는 글을 읽었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용을 요약해서 말해보자면 '마이크로 서비스처럼 분산되어 있는 아키텍처보다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중앙 집중식 아키텍처에서 ai가 더 효율이 좋다' 라고 이해했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저자께서 pr량과 그래프 등을 보여주며 설명을 하였지만 사실 무슨 연관관계가 있는지는 잘 이해는 가지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목만 보고 들어가기 전에는 ai를 활용을 잘하기 위한 아키텍처를 기대하였는데, 너무 큰 범위의 이야기여서 살짝 아쉬움이 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 현재의 AI는 중앙집중식 아키텍처같이 맥락이 모여있는 환경에서 더 잘 작동한다는 것을 알게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>What I Read</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/205</guid>
      <comments>https://jcon.tistory.com/205#entry205comment</comments>
      <pubDate>Tue, 18 Nov 2025 21:37:02 +0900</pubDate>
    </item>
    <item>
      <title>CommonJS와 ESM의 차이점</title>
      <link>https://jcon.tistory.com/204</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;과제 할 때마다 프로젝트를 세로 세팅하는게 번거로워서 react 프로젝트를 스캐폴딩해주는 cli를 만들고 있었습니다.&lt;br /&gt;노드니까 모듈 시스템을 cjs를 사용해야한다고 생각하고 있었는데, 정말 그런가 의구심이 들어 조사를 하였습니다.&lt;br /&gt;결론적으로는 노드에서도 ESM을 사용할 수 있는데 어떤 차이점이 있는지 요약합니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CommonJS와 ESM의 차이점&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;트리쉐이킹&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요즘 현대에는 vite 등 번들러들이 기본적으로 트리쉐이킹을 지원하여 사용되지 않는 코드를 없애 번들 크기를 줄여줍니다.&lt;br /&gt;하지만 CommonJs는 모듈을 사용하고 있다는 것을 런타임이 되어서야 알 수 있어서 트리쉐이킹을 어렵게합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ESM 호환성&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CommonJs 모듈시스템 프로젝트에서는 외부 ESM 모듈 라이브러리를 사용할 수 없지만, ESM 모듈시스템 프로젝트에서는 외부 CommonJS 모듈 라이브러리를 사용할 수 있습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CJS 라이브러리 가져오기 시 주의사항: 명명된 내보내기 제약&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ESM(ECMAScript Modules) 파일에서 CJS(CommonJS) 라이브러리를 가져올 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;명명된 내보내기(Named Export)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식에는 중요한 제약이 따르며 의도대로 작동하지 않을 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;문제의 원인: 모듈 시스템의 근본적인 차이
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CJS:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;module.exports = {...}를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;단일 객체&lt;/b&gt;를 내보냅니다. 이는 Node.js 환경에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;require()를 통해 그 객체 전체를 동적으로 가져옵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ESM:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;export const func = ...를 통해 모듈의 특정 부분을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정적으로&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;내보내며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;import { func } from 'module'&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식으로 가져옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CJS 라이브러리는 명시적으로 명명된 내보내기 (exports.func = ...)를 사용하더라도, Node.js는 최종적으로 이를 하나의&lt;span&gt;&amp;nbsp;&lt;/span&gt;module.exports&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체로 통합하여 처리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ESM 파일에서 CJS 라이브러리의 특정 함수나 변수를 명명된 내보내기 형태로 가져오려고 시도하면(예:&lt;span&gt;&amp;nbsp;&lt;/span&gt;import { func } from 'cjs-lib';), Node.js 환경에서 기본적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;작동하지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시:&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;// CJS 라이브러리 코드 (cjs-lib.js)
module.exports = {
  add: (a, b) =&amp;gt; a + b,
  version: '1.0.0'
};

// ESM 애플리케이션 코드 (app.mjs)
import cjsLib from './cjs-lib.js'; // cjsLib 변수에 { add: ..., version: ... } 객체 전체가 할당됨

console.log(cjsLib.add(1, 2));   // 3 (정상 작동)
// console.log(cjsLib.version); // '1.0.0'
// import { add } from './cjs-lib.js'; // ❌ 이렇게는 사용 불가&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발을 공부하던 당시(17-18년도 즈음) 노드 서버를 만들때 모두 require를 사용하여서 노드는 지금까지 ESM을 지원하지 않는 줄 알고있엇습니다.&lt;br /&gt;그래서 cli 프로젝트를 노드로 돌리기 때문에 모듈시스템을 cjs를 사용했습니다.&lt;br /&gt;하지만 노드에서는 ESM은 19년도부터 지원하고 있었다는 사실을 깨닫고 새삼 고정관념이 무섭다는 것을 느끼게 되었습니다.&lt;br /&gt;ai를 사용해서 빠르게 만든 프로젝트였지만 앞으로 계속 초심을 찾고 이런 포인트들을 알아보면서 성장을 느껴보려합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://toss.tech/article/commonjs-esm-exports-field&quot;&gt;https://toss.tech/article/commonjs-esm-exports-field&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>WEB</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/204</guid>
      <comments>https://jcon.tistory.com/204#entry204comment</comments>
      <pubDate>Sat, 15 Nov 2025 19:01:56 +0900</pubDate>
    </item>
    <item>
      <title>Spec Kit , shrimp taskmanager, Task Master AI 차이점</title>
      <link>https://jcon.tistory.com/203</link>
      <description>&lt;p&gt;Spec Kit , shrimp taskmanager, Task Master AI 차이점을 비교합니다.&lt;/p&gt;
&lt;h2&gt;1.   Spec Kit: &amp;quot;AI와 함께 &amp;#39;설계도&amp;#39;부터 만들자&amp;quot;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;언제 사용하나요?&lt;/strong&gt; 아이디어는 있지만, 이걸 어떻게 구체적인 &amp;#39;기능 명세서&amp;#39;로 만들지 막막할 때 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt;&lt;ol&gt;
&lt;li&gt;사용자가 /specify 명령어로 &amp;quot;SNS 앱 만들고 싶어&amp;quot;라고 아이디어를 던집니다.&lt;/li&gt;
&lt;li&gt;AI가 &amp;quot;그럼 사용자 인증, 피드, 팔로우 기능이 필요하겠네요&amp;quot;라며 spec.md라는 &lt;strong&gt;명세서 파일을 함께 작성&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;이 명확한 명세서(설계도)를 기반으로 코딩을 시작합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심:&lt;/strong&gt; &amp;#39;감(Vibe)&amp;#39;으로 코딩하는 것을 막고, **AI와 합의된 &amp;#39;명세서&amp;#39;**를 만드는 것이 목적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.   Shrimp Task Manager: &amp;quot;AI, 네가 &amp;#39;알아서&amp;#39; 다 해줘&amp;quot;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;언제 사용하나요?&lt;/strong&gt; 목표만 명확하고, 그 과정(기술 스택, 작업 순서 등)은 AI에게 &lt;strong&gt;모두 위임&lt;/strong&gt;하고 싶을 때 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt;&lt;ol&gt;
&lt;li&gt;사용자가 &amp;quot;MBTI 테스트 웹사이트 만들어줘&amp;quot;라고 목표를 제시합니다.&lt;/li&gt;
&lt;li&gt;AI가 &lt;strong&gt;시니어 개발자&lt;/strong&gt;처럼 &lt;strong&gt;스스로&lt;/strong&gt; 기술 스택을 조사하고, 작업 계획을 세우고, 코드를 작성합니다.&lt;/li&gt;
&lt;li&gt;continuous mode를 켜면 사람이 개입하지 않아도 AI가 작업을 끝까지 &lt;strong&gt;자율적으로 실행&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심:&lt;/strong&gt; AI의 &lt;strong&gt;자율성&lt;/strong&gt;을 극대화하여 사용자 개입을 최소화하는 것이 목적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.   Task Master AI: &amp;quot;AI, 내가 준 &amp;#39;기획서&amp;#39;대로 실행해&amp;quot;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;언제 사용하나요?&lt;/strong&gt; &lt;strong&gt;사용자가 이미 명확한 &amp;#39;PRD(제품 요구 사항 문서)&amp;#39;를 가지고 있을 때&lt;/strong&gt; 사용합니다. AI의 자율성보다는 &lt;strong&gt;통제권&lt;/strong&gt;을 원할 때 적합합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt;&lt;ol&gt;
&lt;li&gt;사용자가 상세한 prd.txt 파일을 작성하여 docs 폴더에 넣습니다. (가장 중요!)&lt;/li&gt;
&lt;li&gt;tm parse prd 명령을 내리면, AI가 이 &lt;strong&gt;기획서를 읽고&lt;/strong&gt; 필요한 &amp;#39;할 일 목록(Task List)&amp;#39;을 생성합니다.&lt;/li&gt;
&lt;li&gt;사용자는 &amp;quot;1번 작업 시작해&amp;quot;, &amp;quot;3번 작업 시작해&amp;quot;처럼 &lt;strong&gt;AI에게 작업을 하나씩 지시&lt;/strong&gt;하며 프로젝트를 관리합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심:&lt;/strong&gt; 복잡한 기획서를 AI가 소화할 수 있는 &lt;strong&gt;작업 단위로 분해&lt;/strong&gt;하고, 사용자가 &lt;strong&gt;프로젝트를 완전히 통제&lt;/strong&gt;하는 것이 목적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spec Kit:&lt;/strong&gt; 아이디어를 &lt;strong&gt;설계도&lt;/strong&gt;로 만들 때 (AI와 &lt;strong&gt;기획&lt;/strong&gt;을 함께 함)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shrimp Task Manager:&lt;/strong&gt; 아이디어만 주고 &lt;strong&gt;AI가 다 해주길&lt;/strong&gt; 바랄 때 (AI에게 &lt;strong&gt;위임&lt;/strong&gt;함)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task Master AI:&lt;/strong&gt; 내 기획서를 &lt;strong&gt;AI가 실행&lt;/strong&gt;해주길 바랄 때 (AI를 &lt;strong&gt;관리&lt;/strong&gt;함)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI</category>
      <category>shrimp taskmanager</category>
      <category>Spec Kit</category>
      <category>Task Master AI</category>
      <category>비교</category>
      <category>차이점</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/203</guid>
      <comments>https://jcon.tistory.com/203#entry203comment</comments>
      <pubDate>Fri, 14 Nov 2025 22:28:21 +0900</pubDate>
    </item>
    <item>
      <title>JavaScript 패키지 매니저 아키텍처 분석(feat. npm, yarn, pnpm)</title>
      <link>https://jcon.tistory.com/202</link>
      <description>&lt;h2&gt;JavaScript 패키지 관리 아키텍처 심층 분석&lt;/h2&gt;
&lt;h3&gt;npm, Yarn, pnpm의 진화와 메커니즘&lt;/h3&gt;
&lt;hr&gt;
&lt;h2&gt;들어가며: 단순한 “패키지 설치 도구”가 아닌, 프로젝트의 인프라&lt;/h2&gt;
&lt;p&gt;개발을 하다 보면 누구나 한 번쯤 &lt;code&gt;npm install&lt;/code&gt;을 입력해봤을 것입니다.&lt;br&gt;하지만 단 한 줄의 명령어 뒤에 숨은 복잡한 메커니즘과 아키텍처적 선택에 대해 진지하게 생각해본 적은 많지 않습니다.&lt;/p&gt;
&lt;p&gt;사실, JavaScript 생태계에서 &lt;strong&gt;패키지 매니저의 선택&lt;/strong&gt;은 단순한 도구 결정이 아닙니다.&lt;br&gt;그건 팀의 개발 문화와 철학을 반영하는 &lt;strong&gt;아키텍처적 결정&lt;/strong&gt;입니다.&lt;br&gt;빌드 속도, 캐시 전략, CI/CD 효율성, 그리고 팀 전체의 생산성까지 이 한 선택에 영향을 받습니다.&lt;/p&gt;
&lt;p&gt;JavaScript 패키지 매니저의 역사는 말 그대로 ‘문제 해결의 역사’입니다.&lt;br&gt;npm으로 시작해 Yarn으로 이어지고, 이제는 pnpm과 Yarn Berry가 새로운 패러다임을 제시하고 있습니다.&lt;br&gt;문제는 단순했습니다.&lt;br&gt;“더 빠르게, 더 안정적으로, 더 정확하게.”&lt;/p&gt;
&lt;p&gt;하지만 그 단순한 문장은 지난 10년간 수많은 시행착오와 아키텍처적 실험을 낳았습니다.&lt;br&gt;이 글에서는 &lt;strong&gt;npm → Yarn → pnpm → Yarn Berry&lt;/strong&gt;로 이어지는 진화의 궤적을 따라가며,&lt;br&gt;각 패키지 매니저가 어떤 문제를 해결했고, 어떤 대가를 치렀는지를 살펴보려 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;I. 패키지 매니저가 풀어야 할 세 가지 과제&lt;/h2&gt;
&lt;p&gt;JavaScript 생태계의 패키지 매니저가 맞닥뜨린 핵심 과제는 다음 세 가지로 요약됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;효율성 (Efficiency)&lt;/strong&gt; – 얼마나 빠르게 설치할 수 있고, 얼마나 적은 디스크 공간을 쓰는가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신뢰성 (Reliability)&lt;/strong&gt; – 동일한 코드베이스에서 항상 같은 결과를 재현할 수 있는가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정확성 (Correctness)&lt;/strong&gt; – 의존성이 올바르게 격리되어 있는가&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 세 가지를 모두 만족시키는 도구는 지금도 없습니다.&lt;br&gt;대부분의 패키지 매니저는 &lt;strong&gt;셋 중 둘을 선택&lt;/strong&gt;해야만 했습니다.&lt;br&gt;npm은 ‘호환성’과 ‘단순함’을, Yarn은 ‘속도’와 ‘결정성’을, pnpm은 ‘정확성’과 ‘효율성’을 택했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;II. node_modules의 탄생과 진화&lt;/h2&gt;
&lt;h3&gt;1. npm v1/v2 – 완벽했지만 비효율적인 구조&lt;/h3&gt;
&lt;p&gt;초기의 npm(v1~v2)은 &lt;strong&gt;Node.js의 require() 규칙을 가장 순수하게 구현&lt;/strong&gt;했습니다.&lt;br&gt;각 패키지가 자기 자신의 &lt;code&gt;node_modules&lt;/code&gt;를 가지며, 그 안에 필요한 의존성을 중첩 설치했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A/node_modules/B/node_modules/C&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 구조는 ‘의존성 격리’ 측면에서는 완벽했습니다.&lt;br&gt;하지만 두 가지 문제가 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;패키지 중복:&lt;/strong&gt;&lt;br&gt;A와 B가 둘 다 C를 사용한다면, 디스크에는 C가 두 번 설치됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows의 파일 경로 한계:&lt;/strong&gt;&lt;br&gt;의존성이 깊어질수록 경로가 너무 길어져, 삭제 명령조차 실패하는 경우가 많았습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;한마디로, 완벽했지만 현실적으로 쓸 수 없는 구조였습니다.&lt;br&gt;이때부터 “효율성 vs. 정확성”의 전쟁이 시작되었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. npm v3 / Yarn Classic – 호이스팅으로 인한 부작용&lt;/h3&gt;
&lt;p&gt;npm v3는 이 문제를 해결하기 위해 &lt;strong&gt;호이스팅(Hoisting)&lt;/strong&gt;이라는 전략을 도입합니다.&lt;br&gt;가능한 모든 의존성을 루트 &lt;code&gt;node_modules&lt;/code&gt;로 끌어올려 구조를 평탄화하는 방식이죠.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node_modules/
├── express
├── react
├── accepts (express의 내부 의존성이지만 루트로 올라옴)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 방식은 경로 문제를 해결했지만, 새로운 악몽을 만들었습니다.&lt;/p&gt;
&lt;h4&gt;① 팬텀 의존성 (Phantom Dependency)&lt;/h4&gt;
&lt;p&gt;package.json에 명시하지 않았더라도,&lt;br&gt;호이스팅 덕분에 &lt;strong&gt;“우연히”&lt;/strong&gt; require가 성공하는 경우가 생깁니다.&lt;br&gt;이건 정말 교묘한 문제입니다.&lt;br&gt;당장 눈앞에서는 잘 작동하지만, 나중에 pnpm이나 Yarn Berry로 이전하면&lt;br&gt;“Cannot find module” 오류가 터집니다.&lt;br&gt;왜냐하면 그 의존성은 사실상 &lt;strong&gt;존재하지 않는 의존성&lt;/strong&gt;이었기 때문입니다.&lt;/p&gt;
&lt;h4&gt;② 비결정적 설치 (Non-Deterministic Install)&lt;/h4&gt;
&lt;p&gt;npm v3는 설치 순서에 따라 결과가 달랐습니다.&lt;br&gt;동일한 package.json이라도, 누가 먼저 설치되느냐에 따라 node_modules의 구조가 달라졌습니다.&lt;br&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;“It works on my machine.”&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;호이스팅은 구조적으로 불안정했고,&lt;br&gt;결과적으로 개발자들은 “node_modules 폴더가 신뢰할 수 없다”는 걸 깨닫게 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. Yarn의 등장과 ‘결정론적 설치’&lt;/h3&gt;
&lt;p&gt;이 문제를 해결하기 위해 &lt;strong&gt;Yarn&lt;/strong&gt;이 등장했습니다.&lt;br&gt;Yarn은 &lt;code&gt;yarn.lock&lt;/code&gt; 파일을 통해 &lt;strong&gt;결정론적 설치(Deterministic Install)&lt;/strong&gt;를 구현했습니다.&lt;br&gt;즉, 동일한 코드베이스라면 항상 동일한 버전이 설치됩니다.&lt;/p&gt;
&lt;p&gt;이후 npm도 &lt;code&gt;package-lock.json&lt;/code&gt;을 도입하며 이 개념을 받아들였습니다.&lt;br&gt;하지만 이 시스템에도 한계가 있었습니다.&lt;br&gt;&lt;strong&gt;“어디에 설치되는가?”&lt;/strong&gt;는 여전히 결정되지 않았기 때문입니다.&lt;br&gt;즉, 버전은 같아도 구조는 달라질 수 있었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;III. pnpm이 가져온 혁신: 링크를 통한 정확성&lt;/h2&gt;
&lt;p&gt;이제 pnpm의 차례입니다.&lt;br&gt;pnpm은 한마디로 말해 “npm의 이상을 현대적으로 복원한 시스템”입니다.&lt;br&gt;이 아키텍처의 핵심은 &lt;strong&gt;링크(Link)&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;1. 하드 링크 vs. 심볼릭 링크&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;하드 링크(Hard Link):&lt;/strong&gt;&lt;br&gt;파일 데이터를 직접 공유합니다.&lt;br&gt;동일한 파일이 여러 곳에 존재하더라도 실제 디스크 용량은 하나만 차지합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;심볼릭 링크(Symbolic Link):&lt;/strong&gt;&lt;br&gt;원본 파일의 경로를 가리키는 포인터 역할을 합니다.&lt;br&gt;유연하지만, 원본이 삭제되면 링크가 깨집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;pnpm은 이 두 가지를 조합합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;전역 저장소(&lt;code&gt;~/.pnpm-store&lt;/code&gt;)에 모든 패키지를 &lt;strong&gt;하드 링크&lt;/strong&gt;로 저장&lt;/li&gt;
&lt;li&gt;프로젝트 내 &lt;code&gt;.pnpm&lt;/code&gt; 폴더에 하드 링크 복제&lt;/li&gt;
&lt;li&gt;루트 &lt;code&gt;node_modules&lt;/code&gt;에는 직접 의존성만 &lt;strong&gt;심볼릭 링크&lt;/strong&gt;로 노출&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 구조는 디스크 공간을 거의 쓰지 않으면서도,&lt;br&gt;npm v2의 격리성을 완벽히 복원했습니다.&lt;/p&gt;
&lt;p&gt;이제 &lt;code&gt;express&lt;/code&gt;가 의존하는 &lt;code&gt;accepts&lt;/code&gt; 패키지는&lt;br&gt;루트가 아닌 &lt;code&gt;node_modules/.pnpm/express@버전/node_modules/&lt;/code&gt;에 위치합니다.&lt;br&gt;즉, 팬텀 의존성은 원천적으로 차단됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;IV. Yarn Berry의 급진적 실험: “node_modules를 없애자”&lt;/h2&gt;
&lt;p&gt;Yarn Berry(v2+)는 완전히 다른 길을 택했습니다.&lt;br&gt;&lt;strong&gt;node_modules 자체를 없애버린 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;Yarn Berry를 설치해보면 &lt;code&gt;node_modules&lt;/code&gt; 폴더가 보이지 않습니다.&lt;br&gt;대신 &lt;code&gt;.pnp.cjs&lt;/code&gt;라는 파일 하나가 생깁니다.&lt;br&gt;이 파일은 Node.js의 require를 가로채서,&lt;br&gt;“express는 .yarn/cache/express-v1.zip 안에 있다”라고 직접 알려줍니다.&lt;/p&gt;
&lt;p&gt;즉, 파일 시스템 탐색을 하지 않습니다.&lt;br&gt;그 결과 설치 속도는 &lt;strong&gt;거의 0초에 가까워집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 방식은 ‘Zero-Install’이라 불리며,&lt;br&gt;CI/CD 환경에서 특히 압도적인 속도 차이를 보여줍니다.&lt;br&gt;게다가 팬텀 의존성도 완전히 제거됩니다.&lt;/p&gt;
&lt;p&gt;단점은 호환성입니다.&lt;br&gt;Node.js의 표준 해석 방식을 완전히 대체하기 때문에,&lt;br&gt;IDE나 Linter, TypeScript 컴파일러 등이 이 시스템을 인식해야 합니다.&lt;br&gt;즉, “가장 빠르지만 가장 불편한 방식”입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;V. 종합 비교: 어떤 매니저를 선택해야 할까?&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기준&lt;/th&gt;
&lt;th&gt;npm / Yarn Classic&lt;/th&gt;
&lt;th&gt;pnpm&lt;/th&gt;
&lt;th&gt;Yarn Berry (PnP)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;구조&lt;/td&gt;
&lt;td&gt;평탄화 / 호이스팅&lt;/td&gt;
&lt;td&gt;링크 기반 가상 저장소&lt;/td&gt;
&lt;td&gt;Zip 캐시 + PnP 로더&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;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;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;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;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;td&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h3&gt;  추천 시나리오별 선택 가이드&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;레거시 프로젝트 유지 중이라면 → npm / Yarn Classic&lt;/strong&gt;&lt;br&gt;이미 팬텀 의존성에 의존하고 있을 가능성이 큽니다.&lt;br&gt;새로운 매니저로 전환하면 의외의 오류가 발생할 수 있으니, 점진적인 개선이 바람직합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대규모 모노레포 / CI 최적화가 필요하다면 → pnpm&lt;/strong&gt;&lt;br&gt;전역 저장소 공유로 디스크 낭비를 최소화하고,&lt;br&gt;캐시를 활용해 설치 속도를 극적으로 줄일 수 있습니다.&lt;br&gt;정확성과 실용성의 균형이 가장 뛰어납니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;완전한 의존성 통제가 목표라면 → Yarn Berry (PnP)&lt;/strong&gt;&lt;br&gt;node_modules 없는 세상을 꿈꾼다면 최고의 선택입니다.&lt;br&gt;다만 초기 설정 난이도와 툴 호환성은 직접 감수해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;VI. 마무리하며: 팬텀 의존성을 넘어 “정확성”의 시대로&lt;/h2&gt;
&lt;p&gt;JavaScript 패키지 매니저의 진화는 단순한 속도 경쟁이 아니었습니다.&lt;br&gt;npm v3의 호이스팅은 문제 해결을 위한 ‘필요악’이었고,&lt;br&gt;그로 인해 “팬텀 의존성”이라는 부채가 생겼습니다.&lt;/p&gt;
&lt;p&gt;이제는 &lt;strong&gt;정확성(Dependency Correctness)&lt;/strong&gt;이 새로운 기준이 되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pnpm&lt;/strong&gt;은 하드 링크와 심볼릭 링크를 정교하게 결합해&lt;br&gt;Node.js 표준을 유지하면서도 완벽한 격리성을 구현했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Yarn Berry&lt;/strong&gt;는 아예 기존 패러다임을 버리고&lt;br&gt;새로운 의존성 로더 체계를 만들어냈습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국, 패키지 매니저의 선택은 “무엇이 더 좋냐”의 문제가 아닙니다.&lt;br&gt;&lt;strong&gt;우리 팀이 ‘생태계 호환성’과 ‘아키텍처 정확성’ 중 어느 쪽에 더 가치를 두는가&lt;/strong&gt;의 문제입니다.&lt;/p&gt;
&lt;p&gt;잠시 눈을 감고 생각해보세요.&lt;br&gt;여러분의 프로젝트는 어떤 철학 위에 서 있나요?&lt;br&gt;“모든 게 잘 돌아가는 시스템”을 원하시나요,&lt;br&gt;아니면 “모든 게 명확한 시스템”을 원하시나요?&lt;/p&gt;
&lt;p&gt;그 대답이 바로,&lt;br&gt;당신의 패키지 매니저를 결정짓는 진짜 기준이 될 것입니다.&lt;/p&gt;
&lt;hr&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;strong&gt;요약&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;npm: 호환성 최고, 하지만 팬텀 의존성 존재&lt;/li&gt;
&lt;li&gt;pnpm: 정확성과 효율성의 균형&lt;/li&gt;
&lt;li&gt;Yarn Berry: 급진적 혁신, 속도는 최고지만 호환성 주의&lt;/li&gt;
&lt;/ul&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>Dev Tools</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/202</guid>
      <comments>https://jcon.tistory.com/202#entry202comment</comments>
      <pubDate>Wed, 5 Nov 2025 22:34:48 +0900</pubDate>
    </item>
    <item>
      <title>유용하게 사용중인 MCP</title>
      <link>https://jcon.tistory.com/201</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Cursor에서 유용하게 사용중인 MCP&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론: &amp;lsquo;MCP&amp;rsquo;란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;Vibe-Driven Development&amp;rsquo;라 불리는 최신 AI 코딩 워크플로우는 &lt;b&gt;Cursor&lt;/b&gt; 같은 IDE를 통해 현실이 되었습니다.&lt;br /&gt;이제 개발자는 AI와 대화하며 코드를 작성하지만, 곧 한계에 부딪힙니다.&lt;br /&gt;AI가 &lt;b&gt;외부 세계와 단절되어 있고&lt;/b&gt;, &lt;b&gt;로컬 파일 시스템이나 최신 라이브러리 정보에 접근할 수 없기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 등장한 것이 바로 &lt;b&gt;MCP(Model Context Protocol)&lt;/b&gt;입니다.&lt;br /&gt;MCP는 AI 모델(MCP 클라이언트)이 외부 도구 및 데이터 소스(MCP 서버)와 &lt;b&gt;안전하고 표준화된 방식으로 상호작용&lt;/b&gt;할 수 있게 하는 프로토콜입니다. 쉽게 말해, AI를 위한 &lt;b&gt;USB-C 포트&lt;/b&gt; 또는 &lt;b&gt;범용 API 어댑터&lt;/b&gt;라 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 아키텍처는 AI가 IDE를 벗어나지 않고도 GitHub API를 호출해 이슈를 생성하고, AWS 리소스를 관리하며, 데이터베이스를 쿼리할 수 있도록 합니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구성 요소&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;대응 MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;지식&lt;/td&gt;
&lt;td&gt;최신 코드 문서와 예제 제공&lt;/td&gt;
&lt;td&gt;Context7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인식&lt;/td&gt;
&lt;td&gt;실시간 웹 정보 접근&lt;/td&gt;
&lt;td&gt;Tavily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추론&lt;/td&gt;
&lt;td&gt;단계적 사고 구조화&lt;/td&gt;
&lt;td&gt;Sequential Thinking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기억&lt;/td&gt;
&lt;td&gt;지속적 작업 관리와 반성&lt;/td&gt;
&lt;td&gt;Shrimp Task Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1장. Context7 &amp;mdash; AI의 환각을 치료하는 실시간 코드 명의&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 문제: AI가 가짜 API를 말하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 방대한 코드로 학습했지만, 그 지식은 &lt;b&gt;&amp;lsquo;학습 시점&amp;rsquo;에 동결&lt;/b&gt;되어 있습니다.&lt;br /&gt;그 결과 AI는 오래된 API를 제안하거나, 존재하지 않는 기능을 만들어내는 &lt;b&gt;환각(Hallucination)&lt;/b&gt;을 일으킵니다.&lt;br /&gt;개발자는 잘못된 코드를 디버깅하느라 시간을 낭비합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 해결책: &amp;lsquo;RAG for Code&amp;rsquo; &amp;mdash; Context7의 접근법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Context7&lt;/b&gt;은 AI 코드 에디터를 위한 &amp;lsquo;실시간 코드 문서 서버&amp;rsquo;입니다.&lt;br /&gt;npm, GitHub 등에서 &lt;b&gt;버전별 문서와 코드 예제&lt;/b&gt;를 즉시 가져와 LLM의 컨텍스트에 주입합니다.&lt;br /&gt;이는 코드 전용 &lt;b&gt;검색 증강 생성(RAG)&lt;/b&gt; 시스템으로,&lt;br /&gt;AI가 &amp;ldquo;기억&amp;rdquo; 대신 &amp;ldquo;최신 문서&amp;rdquo;를 참고하도록 만들어 줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3. 사용 예시&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Before: Create a Next.js middleware that checks for a valid JWT...
&amp;rarr; 오래된 _middleware.js 문법을 반환할 수 있음

After: Create a Next.js middleware that checks for a valid JWT... use context7
&amp;rarr; 최신 middleware.ts 구문과 NextRequest 객체를 사용한 정확한 코드 반환&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2장. Tavily &amp;mdash; AI의 눈과 귀, 실시간 웹 인식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 문제: 코드 밖 세상과의 단절&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context7이 코드 라이브러리를 다룬다면,&lt;br /&gt;AI는 여전히 &lt;b&gt;실시간 웹 정보&lt;/b&gt;(기술 블로그, 보안 취약점, 신제품 출시)에 접근하지 못합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. 해결책: Tavily, AI 전용 검색 엔진&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tavily&lt;/b&gt;는 인간용이 아닌 &lt;b&gt;AI 에이전트 전용 웹 검색 엔진&lt;/b&gt;입니다.&lt;br /&gt;일반 검색이 링크를 제공한다면, Tavily는 &lt;b&gt;구조화된 데이터(JSON)&lt;/b&gt;로 정보를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 도구&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;tavily-search&lt;/code&gt;: 실시간 검색&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tavily-extract&lt;/code&gt;: 광고 제거 및 핵심 데이터 추출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tavily-map&lt;/code&gt;: 사이트 구조 맵 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tavily-crawl&lt;/code&gt;: 사이트 전체 크롤링&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. 활용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;오늘 출시된 React 19의 주요 기능은?&amp;rdquo; &amp;rarr; &lt;code&gt;search&lt;/code&gt;&lt;br /&gt;&amp;ldquo;이 GitHub 이슈 관련 블로그 포스트를 모두 요약해 줘.&amp;rdquo; &amp;rarr; &lt;code&gt;crawl + extract&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 탭을 넘나드는 불필요한 작업이 크게 줄어듭니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3장. Sequential Thinking &amp;mdash; AI에게 &amp;lsquo;생각하는 법&amp;rsquo;을 가르치다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 문제: AI의 성급한 결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 문제를 던지면 AI는 사고 과정을 생략하고, 즉시 결과를 내놓습니다.&lt;br /&gt;이 때문에 답은 틀리거나 불완전합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 해결책: 단계별 사고(Chain-of-Thought) 강제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Sequential Thinking MCP&lt;/b&gt;는 AI에게 사고 단계를 강제합니다.&lt;br /&gt;AI는 &amp;ldquo;생각 &amp;rarr; 다음 단계 &amp;rarr; 검증 &amp;rarr; 수정&amp;rdquo;의 순서를 따릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;오류 메시지 분석&lt;/li&gt;
&lt;li&gt;인증 헤더 점검&lt;/li&gt;
&lt;li&gt;CORS 정책 확인&lt;/li&gt;
&lt;li&gt;수정안 제시&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 AI는 &lt;b&gt;성찰(reflection)&lt;/b&gt;과 &lt;b&gt;대안 탐색&lt;/b&gt;을 수행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. 활용 예시&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;gt; &amp;ldquo;내 API 호출이 실패하는 이유를 4단계로 분석해 줘.&amp;rdquo;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 자동으로 사고를 구조화하고 각 단계별 근거를 제시합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4장. Task master AI&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Task Master AI는 이미 작성된 &lt;b&gt;'제품 요구 사항 문서(PRD)'&lt;/b&gt;를 AI가 &lt;b&gt;'실행 가능한 작업(Task)'&lt;/b&gt;으로 분해하고 관리해주는 &lt;b&gt;작업 관리 시스템(Task Management System)&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;shrimp taskmanger보다 task master를 선호하는데 그 이유는 하단 글 참고
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://jcon.tistory.com/203&quot;&gt;https://jcon.tistory.com/203&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
[Spec Kit , shrimp taskmanager, Task Master AI 차이점&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.   Spec Kit: &quot;AI와 함께 '설계도'부터 만들자&quot;언제 사용하나요? 아이디어는 있지만, 이걸 어떻게 구체적인 '기능 명세서'로 만들지 막막할 때 사용합니다.작동 방식:사용자가 /specify 명령어로 &quot;SNS&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jcon.tistory.com](&lt;a href=&quot;https://jcon.tistory.com/203&quot;&gt;https://jcon.tistory.com/203&lt;/a&gt;)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5장. TestSprite&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 특징은 개발자가 테스트 코드를 단 한 줄도 작성하지 않게 하는 것을 목표로 한다는 점입니다. 기존의 AI 코딩 도구들이 애플리케이션 코드를 생성하는 데 집중했다면, TestSprite는 생성된 코드를 검증하고 버그를 찾는 '테스트' 과정 전체를 자동화하는 데 특화되어 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TestSprite의 핵심 기능&lt;/h3&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;검색된 정보(1.1, 2.1, 2.7)에 따르면 TestSprite는 다음과 같은 테스트의 전체 주기를 자동화합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트 계획 자동 생성:&lt;/b&gt; 개발자가 작성한 코드나 자연어 문서(PRD 등)를 AI가 분석하여 테스트 계획을 스스로 수립합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 코드 자동 생성:&lt;/b&gt; 수립된 계획에 따라 AI가 실제 실행할 테스트 코드를(예: 유닛 테스트, 통합 테스트) 자동으로 작성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 자동 실행:&lt;/b&gt; 생성된 테스트를 클라우드 환경 등에서 자동으로 실행합니다. 프론트엔드(UI)와 백엔드(API) 테스트를 모두 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과 분석 및 디버깅:&lt;/b&gt; 테스트 실패 시, AI가 원인을 진단하고(Root Cause Analysis), 버그 리포트를 생성하며 심지어 &lt;b&gt;수정안(Fix)까지 제안&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI/CD 파이프라인 통합:&lt;/b&gt; 기존 개발 워크플로우(CI/CD)에 원활하게 통합되어 코드 변경 시마다 자동으로 테스트를 수행할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  어떻게 작동하나요? (2.6)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;사용자 리뷰에 따르면 일반적인 사용 방식은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;앱 정보 제공:&lt;/b&gt; 테스트할 웹사이트 URL과 로그인 정보 등을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 탐색:&lt;/b&gt; TestSprite의 AI 에이전트가 실제 사용자처럼 앱을 탐색(크롤링)하며 기능을 이해합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 테스트 생성 및 실행:&lt;/b&gt; 발견된 기능들에 대해 AI가 자동으로 테스트 케이스를 만들고, 실제 버튼을 클릭하고 폼을 채우는 등의 테스트를 실행합니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>AI</category>
      <category>Cursor</category>
      <category>MCP</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/201</guid>
      <comments>https://jcon.tistory.com/201#entry201comment</comments>
      <pubDate>Wed, 5 Nov 2025 02:24:30 +0900</pubDate>
    </item>
    <item>
      <title>REST와 GraphQL 비교</title>
      <link>https://jcon.tistory.com/199</link>
      <description>&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;h3&gt;REST vs GraphQL&lt;/h3&gt;
&lt;p&gt;이전 직장에서 REST API로 구축된 프로젝트를 GraphQL로 전환하는 과정을 경험했습니다. 이 경험을 통해 오랫동안 웹 API의 표준처럼 여겨져 온 REST의 자원(Resource) 중심 접근 방식과, 최신 애플리케이션의 복잡한 데이터 요구에 맞춰 등장한 GraphQL의 클라이언트 중심 접근 방식이 어떻게 다른지 체감할 수 있었습니다. 좀 더 명확한 차이점을 이해하기 위해 각 기술이 어떻게 탄생했고, 어떤 핵심 원칙을 가지고 있는지, 그리고 실제 어떤 상황에 사용하면 좋은지, 나아가 캐싱이나 보안 방식의 차이점까지 조사했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;REST란? — 웹을 위해 만들어진 아키텍처 스타일&lt;/h2&gt;
&lt;h3&gt;REST의 탄생&lt;/h3&gt;
&lt;h4&gt;등장 배경과 동기&lt;/h4&gt;
&lt;p&gt;REST는 2000년 &lt;a href=&quot;https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_7&quot;&gt;로이 필딩의 박사 논문&lt;/a&gt;에서 처음으로 소개되었습니다. 그의 논문에서 정의한 REST는 특정 프로토콜이나 표준이 아닌, 분산 하이퍼미디어 시스템을 위한 아키텍처 스타일입니다. 이 스타일은 월드 와이드 웹의 기존 아키텍처(HTTP 1.0/1.1, URI)를 분석하여, 웹을 확장 가능하고 견고하게 만든 핵심 원칙을 분석하는 과정에서 REST가 탄생했습니다. 로이 필딩은 웹이 스스로의 복잡성 때문에 무너지지 않도록, 지켜야 할 아키텍처 원칙들의 집합으로 REST를 제안했습니다.&lt;/p&gt;
&lt;h3&gt;6가지 핵심 원칙&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;클라이언트-서버 (Client-Server):&lt;/strong&gt; 이 원칙은 역할과 책임의 분리를 명확히 합니다. 클라이언트는 사용자 인터페이스(UI)와 사용자 경험(UX)에 집중하고, 서버는 데이터 저장, 비즈니스 로직, 보안을 책임집니다. 이렇게 역할을 나누면 클라이언트와 서버가 서로에게 영향을 주지 않고 독립적으로 개발하고 발전할 수 있어, 전체 시스템의 유연성과 확장성을 높여줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;무상태 (Stateless):&lt;/strong&gt; 서버는 클라이언트의 상태(세션 정보 등)를 저장하지 않습니다. 클라이언트가 서버로 보내는 각 요청에는 서버가 그 요청을 이해하고 처리하는 데 필요한 모든 정보가 담겨 있어야 합니다. 이 원칙 덕분에 서버 설계가 단순해지고, 각 요청을 독립적으로 처리할 수 있어 가시성, 신뢰성, 확장성을 크게 높여줍니다. 서버가 이전 요청을 기억할 필요가 없으니 부하 분산이 쉬워지고, 장애가 생겨도 복구하기가 더 수월해집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐시 가능 (Cacheable):&lt;/strong&gt; 서버가 보내는 응답에는 이 응답을 캐시(임시 저장)해도 되는지 여부를 명시해야 합니다. 클라이언트나 중간 서버는 캐시 가능한 응답을 재사용해서, 똑같은 요청이 또 들어왔을 때 서버까지 가지 않고 캐시에서 바로 응답을 보내줄 수 있습니다. 이는 서버 부하를 줄이고 응답 속도를 높여줍니다. 네트워크 효율성을 높여 사용자가 느끼는 성능을 크게 개선하는 핵심 요소입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;계층화 시스템 (Layered System):&lt;/strong&gt; 클라이언트는 자신이 통신하는 서버가 최종 목적지인지, 아니면 중간에 로드 밸런서나 캐시 같은 다른 계층을 거치는지 알 필요가 없습니다. 각 계층은 보안, 부하 분산 등 특정 기능을 담당하며 다른 계층과 독립적으로 작동합니다. 이 구조는 시스템의 복잡도를 관리하고 기존 시스템을 쉽게 통합하는 데 도움을 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인터페이스 일관성 (Uniform Interface):&lt;/strong&gt; REST를 다른 아키텍처와 가장 뚜렷하게 구분 짓는 핵심 원칙입니다. 시스템 전체의 아키텍처를 단순화하고, 클라이언트와 서버가 독립적으로 발전할 수 있도록 해줍니다. 이 원칙은 다시 네 가지 세부 원칙으로 나뉘며, 다음 섹션에서 자세히 다루겠습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.woodruff.dev/rest-constraint-6-code-on-demand-when-why-and-how-to-use-it/&quot;&gt;코드 온디맨드 (Code-On-Demand, 선택 사항)&lt;/a&gt;:&lt;/strong&gt; 서버가 클라이언트로 실행 가능한 코드(예: JavaScript)를 보내 클라이언트의 기능을 일시적으로 확장할 수 있게 해주는 원칙입니다. 필수는 아니지만, 클라이언트가 미리 모든 기능을 구현할 필요가 없게 만들어 시스템을 더 단순하게 만들 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;이상과 현실의 차이: &amp;#39;진정한 REST&amp;#39;, HATEOAS, 리처드슨 성숙도 모델&lt;/h3&gt;
&lt;h4&gt;인터페이스 일관성의 네 가지 세부 원칙&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;자원의 식별 (Identification of Resources):&lt;/strong&gt; 시스템의 모든 자원은 URI(Uniform Resource Identifier)라는 고유한 주소를 통해 식별됩니다. 예를 들어, 특정 사용자는 &lt;code&gt;/users/123&lt;/code&gt;과 같은 주소로 표현됩니다. URI를 설계할 때는 보통 동사 대신 명사를 사용하고, 여러 자원의 모음을 나타낼 때는 복수형을 씁니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;표현을 통한 자원의 조작 (Manipulation of Resources Through Representations):&lt;/strong&gt; 클라이언트는 자원의 상태를 직접 바꾸는 게 아니라, 자원의 표현(예: JSON 데이터)을 통해 상호작용합니다. 클라이언트는 이 표현과 함께 표준 HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용해 서버에 무엇을 하고 싶은지 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자기 서술적 메시지 (Self-Descriptive Messages):&lt;/strong&gt; 각 메시지는 메시지를 받는 쪽에서 그 메시지를 어떻게 처리해야 하는지 충분한 정보를 스스로 담고 있어야 합니다. 예를 들어, &lt;code&gt;Content-Type&lt;/code&gt; 헤더에 &lt;code&gt;application/json&lt;/code&gt;이라고 명시해서 &amp;#39;이 메시지 본문은 JSON 형식이니 JSON 파서로 처리하세요&amp;#39;라고 알려주는 식입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;애플리케이션 상태의 엔진으로서 하이퍼미디어 (Hypermedia as the Engine of Application State, HATEOAS):&lt;/strong&gt; 서버의 응답에는 현재 자원과 관련하여 다음에 할 수 있는 행동이나 이동할 수 있는 상태에 대한 링크(하이퍼미디어)가 포함되어야 합니다. 예를 들어, 특정 주문(&lt;code&gt;order&lt;/code&gt;) 정보를 조회했을 때, 응답 데이터 안에 그 주문을 &amp;#39;취소&amp;#39;하거나 &amp;#39;결제&amp;#39;할 수 있는 URL 링크가 함께 제공되는 것입니다. REST 아키텍처에서 가장 중요하지만, 잘 지켜지지 않는 원칙입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;현실과의 거리: HATEOAS는 어디에?&lt;/h4&gt;
&lt;p&gt;HATEOAS를 구현한 API는 응답되는 링크를 통해서 비지니스 규칙(예: 구매가 가능할 때에는 구매에 대한 링크를 제공하지만 불가능할 경우 링크를 제공하지 않음)을 서버에 이관하고, 클라이언트에서는 제공받은 링크를 기반으로 개발함으로써 URI변경에 대한 서버와 클라이언트의 관계의 유연성을 제공하지만, 이를 위해 응답되는 링크의 의미와 관계에 대한 인지적 부하와 학습 곡선과 백엔드 개발 비용 증가 등의 단점이 있습니다.&lt;/p&gt;
&lt;p&gt;이처럼 서버(동적 링크 생성)와 클라이언트(하드코딩된 URL 대신 링크 파싱) 양쪽에 구현 복잡성을 더하기 때문에 오늘날 우리가 &amp;quot;REST API&amp;quot;라고 부르는 대부분의 API는 인터페이스 일관성의 첫 세 가지 원칙은 잘 따르지만, HATEOAS는 거의 지키지 않습니다. 로이 필딩은 이런 API는 진정한 REST가 아니라 그냥 HTTP 기반 API일 뿐이라고 말하였습니다.(&lt;a href=&quot;https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven&quot;&gt;출처&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;그러나 로이 필딩의 생각과 달리, 일부 원칙만 지키면서도 RESTful API라 부르는 사례가 많아지자 리처드슨은 레벨을 나누어 어떤 정도의 RESTful 수준인지 표시하는 모델을 제안했습니다.&lt;/p&gt;
&lt;h4&gt;리처드슨 성숙도 모델&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Level 0:&lt;/strong&gt; HTTP를 그냥 원격 함수 호출(RPC)을 위한 전송 수단으로만 사용합니다. 모든 요청이 하나의 엔드포인트로 가고, HTTP 메서드는 무시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 1:&lt;/strong&gt; &amp;#39;자원&amp;#39;이라는 개념을 도입합니다. 각 자원은 고유한 URI를 갖지만, 여전히 모든 작업에 POST 메서드를 쓰는 등 HTTP를 제대로 활용하지는 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 2:&lt;/strong&gt; HTTP 메서드를 의미에 맞게 사용합니다. GET은 조회, POST는 생성, PUT은 수정, DELETE는 삭제에 사용합니다. &lt;strong&gt;오늘날 우리가 &amp;quot;REST API&amp;quot;라고 부르는 대부분의 API가 이 수준에 해당합니다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 3:&lt;/strong&gt; HATEOAS를 도입하여 진정한 REST의 모든 원칙을 만족시킵니다. 클라이언트는 서버가 제공하는 링크를 통해 다음 행동을 결정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;GraphQL의 등장 — 최신 애플리케이션을 위한 쿼리 언어&lt;/h2&gt;
&lt;h3&gt;등장 배경 - 페이스북의 모바일 우선 전략과 그에 따른 과제&lt;/h3&gt;
&lt;h4&gt;문제의 시작&lt;/h4&gt;
&lt;p&gt;2010년대 초, 페이스북은 모바일 앱을 웹 기반(HTML5)에서 네이티브 앱으로 전환하면서 심각한 성능 문제에 부딪혔습니다. 페이스북의 뉴스 피드는 게시물, 작성자 정보, 댓글, 댓글 작성자 정보, 각 항목의 &amp;#39;좋아요&amp;#39; 수 등 여러 데이터가 복잡하게 얽혀 있어 한 번에 가져와야 했습니다.&lt;/p&gt;
&lt;h4&gt;오버페칭과 언더페칭 문제&lt;/h4&gt;
&lt;p&gt;기존의 REST API 방식으로는 이런 데이터를 효율적으로 가져오기 어려웠고, 두 가지 주요 문제가 발생했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;언더페칭 (Under-fetching):&lt;/strong&gt; 클라이언트가 화면을 구성하는 데 필요한 모든 데이터를 모으기 위해 서버에 여러 번 요청을 보내야 하는 문제입니다. 예를 들어, &lt;code&gt;GET /posts/123&lt;/code&gt;으로 게시물 정보를 가져오고, 그 응답에 담긴 작성자 ID로 다시 &lt;code&gt;GET /users/456&lt;/code&gt;을 호출하고, 또 &lt;code&gt;GET /posts/123/comments&lt;/code&gt;를 호출하는 식이죠. 이렇게 여러 번의 요청이 오가는 것은 특히 네트워크 지연 시간이 긴 모바일 환경에서 성능에 치명적인 영향을 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;오버페칭 (Over-fetching):&lt;/strong&gt; 여러 번 요청하는 것을 피하기 위해, 서버에서 &lt;code&gt;/feed&lt;/code&gt;처럼 필요한 모든 정보를 한 번에 내려주는 전용 엔드포인트를 만들 수도 있습니다. 하지만 이 경우, 클라이언트는 당장 화면에 필요하지 않은 데이터까지 모두 포함된 거대한 데이터를 받아야 합니다. 이는 네트워크 대역폭을 낭비하고 클라이언트의 처리 부담을 늘립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GraphQL은 2012년 페이스북 내부에서 바로 이 문제를 해결하기 위해 개발되었고, 2015년에 오픈소스로 공개되었습니다. GraphQL의 핵심 아이디어는 간단합니다. 데이터의 형태를 서버가 아닌 클라이언트가 결정하도록 권한을 넘겨주는 것입니다. 클라이언트가 필요한 데이터의 구조를 정확히 명시해서 요청하면, 서버는 그 구조에 딱 맞춰서 응답을 제공합니다.&lt;/p&gt;
&lt;h3&gt;핵심 구성 요소&lt;/h3&gt;
&lt;p&gt;GraphQL의 아키텍처는 REST 모델과는 정반대의 접근 방식을 취합니다. REST가 자원별로 여러 개의 단순한 엔드포인트를 제공하는 반면, GraphQL은 복잡한 쿼리를 이해할 수 있는 하나의 똑똑한 엔드포인트를 제공합니다. 이 구조적 차이가 데이터 요청 효율성부터 캐싱의 복잡성까지, 두 방식의 거의 모든 주요 차이점을 만들어냅니다. REST에서는 API 호출의 의미가 URI와 HTTP 메서드에 담겨 있지만(엔드포인트 자체가 작업을 정의), GraphQL에서는 POST 요청의 본문에 담긴 쿼리 자체에 그 의미가 담겨 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;스키마 정의 언어 (Schema Definition Language, SDL):&lt;/strong&gt; 모든 GraphQL API의 심장은 &amp;#39;스키마&amp;#39;입니다. 스키마는 이 API가 어떤 데이터를 제공할 수 있고, 어떤 작업을 할 수 있는지 모든 것을 정의하는, 강력한 타입 시스템 기반의 계약서와 같습니다. 이 스키마는 클라이언트와 서버 사이의 &amp;quot;단일 진실 공급원(single source of truth)&amp;quot; 역할을 하며, 특정 프로그래밍 언어에 얽매이지 않고 객체 타입, 필드, 그리고 그 관계를 명확하게 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;쿼리, 뮤테이션, 구독:&lt;/strong&gt; GraphQL에는 세 가지 주요 작업 유형이 있습니다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;쿼리 (Query):&lt;/strong&gt; 데이터를 읽을 때 사용합니다. 클라이언트는 자신이 받고 싶은 JSON 응답의 모양과 똑같은 형태로 쿼리를 작성해서 보내기 때문에, 오버페칭과 언더페칭 문제를 근본적으로 해결할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;뮤테이션 (Mutation):&lt;/strong&gt; 데이터를 생성, 수정, 삭제할 때 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;구독 (Subscription):&lt;/strong&gt; 웹소켓 등을 통해 실시간으로 데이터 업데이트를 받아보기 위한 지속적인 연결을 만들 때 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리졸버 (Resolver):&lt;/strong&gt; 서버에서는 스키마의 각 필드마다 &amp;#39;리졸버&amp;#39;라는 함수가 연결됩니다. 이 함수는 해당 필드에 대한 데이터를 실제로 어떻게 가져올지(데이터베이스에서 가져오거나, 다른 API를 호출하는 등)에 대한 로직을 담고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;강력한 타입 시스템을 갖춘 스키마는 기술적 세부사항을 넘어, API 명세서 자체로 기능합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;REST와 GraphQL 비교&lt;/h2&gt;
&lt;h3&gt;REST와 GraphQL의 기능별 비교&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;기능&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;REST API&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;GraphQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;데이터 요청&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;서버가 데이터 구조를 정의. 엔드포인트마다 고정된 데이터를 반환. 오버/언더페칭이 발생하기 쉬움.&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;클라이언트가 데이터 구조를 정의. 쿼리에 따라 유연하게 데이터를 반환. 오버/언더페칭 문제 해결.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;엔드포인트&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;여러 개의 엔드포인트. 각 자원마다 하나씩 (예: &lt;code&gt;/users&lt;/code&gt;, &lt;code&gt;/posts&lt;/code&gt;).&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;일반적으로 단 하나의 엔드포인트 (예: &lt;code&gt;/graphql&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;프로토콜&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;HTTP 메서드(GET, POST, PUT, DELETE)로 작업을 표현.&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;주로 모든 작업(쿼리, 뮤테이션)에 POST 메서드를 사용.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;타입 시스템&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;약한 타입. JSON Schema 등으로 타입을 정의할 수는 있지만, 강제되지는 않음.&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;스키마 정의 언어를 통한 강력한 타입 시스템. 클라이언트와 서버 간의 계약이 강제됨.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;캐싱&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;표준 HTTP 캐싱(헤더 &lt;code&gt;ETag&lt;/code&gt;, &lt;code&gt;Cache-Control&lt;/code&gt; 등)을 통해 쉽게 구현 가능.&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;단일 엔드포인트를 사용하므로 HTTP 캐싱이 복잡함. 클라이언트 라이브러리나 애플리케이션 레벨에서 캐싱 처리.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;버전 관리&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;주로 URI에 버전을 명시 (예: &lt;code&gt;/v1/users&lt;/code&gt;).&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;스키마를 점진적으로 발전시키는 방식을 선호. 필드를 제거하는 대신 &amp;#39;사용 중단(deprecated)&amp;#39;으로 표시.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;에러 처리&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;HTTP 상태 코드로 요청 전체의 성공/실패를 표시 (예: 404, 500).&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;주로 &lt;code&gt;200 OK&lt;/code&gt; 상태 코드와 함께, 응답 본문 내 &lt;code&gt;errors&lt;/code&gt; 배열에 에러 정보를 담아 반환.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;API 명세 탐색&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;내장된 표준이 없어 OpenAPI/Swagger 같은 외부 문서에 의존.&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;내장된 &amp;#39;인트로스펙션&amp;#39; 기능으로 클라이언트가 스키마 자체에 쿼리하여 API 구조를 탐색할 수 있음.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;개발자 경험과 API의 발전&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API 탐색과 문서화:&lt;/strong&gt; REST는 API의 구조와 사용법을 이해하기 위해 Swagger나 OpenAPI 같은 외부 문서 도구가 필요합니다. 반면, GraphQL은 &amp;#39;인트로스펙션&amp;#39;이라는 기능이 내장되어 있어, 클라이언트가 API 스키마 자체에 쿼리를 보내 사용 가능한 모든 타입과 필드를 동적으로 알아낼 수 있습니다. GraphiQL 같은 도구는 이를 시각적으로 보여주어 API를 탐색하고 테스트하는 강력한 개발 환경을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;버전 관리:&lt;/strong&gt; REST API는 기존 API와 호환되지 않는 변경이 생길 때, 보통 URI에 버전을 붙이는 방식(예: &lt;code&gt;/v2/users&lt;/code&gt;)을 사용합니다. 이는 여러 버전의 API를 동시에 유지보수해야 하는 부담을 줍니다. GraphQL은 버전 관리 대신 진화하는 방식을 권장합니다. 새로운 필드는 기존 클라이언트에 영향을 주지 않고 스키마에 추가할 수 있고, 더 이상 사용하지 않는 필드는 바로 삭제하는 대신 &lt;code&gt;@deprecated&lt;/code&gt; 지시어를 붙여 사용 중단으로 표시할 수 있습니다. 이를 통해 API가 점진적으로 발전해 나갈 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;클라이언트 측 복잡성:&lt;/strong&gt; GraphQL이 데이터 요청 로직을 단순화해주는 반면, 상태 관리나 캐싱 같은 부분에서는 클라이언트 측의 복잡성을 높일 수 있습니다. 이 때문에 보통 Apollo Client나 Relay처럼 정교한 기능을 제공하는 라이브러리를 함께 사용하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;캐싱 문제: 두 아키텍처의 근본적인 차이점&lt;/h3&gt;
&lt;p&gt;캐싱 전략의 차이는 두 아키텍처의 근본적인 철학 차이에서 비롯됩니다. REST의 여러 개의 단순한 엔드포인트와 GraphQL의 하나의 똑똑한 엔드포인트라는 핵심 설계가 캐싱 방식의 차이를 만드는 직접적인 원인입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;REST와 HTTP 캐싱:&lt;/strong&gt; REST 아키텍처는 오랜 시간 발전해온 웹 인프라와 잘 어우러집니다. &lt;code&gt;GET /users/123&lt;/code&gt;과 같은 요청은 여러 번 호출해도 결과가 같고(멱등성), 그 응답은 브라우저, CDN, 리버스 프록시 등 모든 표준 HTTP 캐시가 저장하고 재사용할 수 있습니다. &lt;code&gt;Cache-Control&lt;/code&gt;, &lt;code&gt;ETag&lt;/code&gt;, &lt;code&gt;Last-Modified&lt;/code&gt; 같은 표준 HTTP 헤더를 통해 캐시 정책을 세밀하게 제어할 수 있습니다. 이는 추가 개발 비용 없이 엄청난 성능과 확장성 이점을 얻을 수 있는, REST가 가진 강력하면서도 종종 간과되는 장점입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GraphQL의 캐싱 과제:&lt;/strong&gt; 대부분의 GraphQL 쿼리는 단일 엔드포인트(예: &lt;code&gt;/graphql&lt;/code&gt;)에 &lt;code&gt;POST&lt;/code&gt; 메서드로 전송되기 때문에, 표준 HTTP 캐시 입장에서는 요청의 내용을 알 수 없습니다. 캐시는 요청 URL을 기준으로 작동하는데, GraphQL 요청은 URL이 항상 같아서 사용자를 조회하는 쿼리와 제품을 조회하는 쿼리를 구분할 수 없기 때문입니다. 즉, GraphQL의 설계는 가장 일반적이고 강력한 웹 캐싱 방식을 포기하는 대신, 다른 장점을 선택한 것입니다. 이는 설계 실수가 아니라, 클라이언트가 원하는 대로 데이터를 요청하게 한다는 핵심 목표를 달성하기 위한 필연적인 트레이드오프입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GraphQL의 해결책:&lt;/strong&gt; GraphQL 환경에서 캐싱의 책임은 전송 계층(HTTP)에서 애플리케이션 계층으로 넘어갑니다. 캐싱은 주로 Apollo Client나 Relay 같은 클라이언트 라이브러리가 담당합니다. 이 라이브러리들은 데이터 객체를 정규화하여 메모리에 캐시를 만들어두고, 동일한 데이터에 대한 중복 요청을 막고 UI를 즉각적으로 업데이트해 줍니다. 이는 매우 강력하지만, 단순한 HTTP 캐싱에 비해 구현하고 관리하기가 더 복잡합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;보안 방식의 차이&lt;/h2&gt;
&lt;h3&gt;REST API의 보안 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인증 및 인가:&lt;/strong&gt; REST API 보안은 이미 잘 알려진 패턴을 따릅니다. API 키, HTTP 기본 인증(HTTPS와 함께 사용), JWT(JSON Web Tokens), OAuth 2.0 같은 표준적인 방법들이 널리 사용됩니다. 인가(권한 부여)는 보통 엔드포인트 단위로 처리됩니다. 예를 들어, 특정 사용자가 &lt;code&gt;DELETE /users/{id}&lt;/code&gt; 엔드포인트에 접근할 권한이 있는지 확인하는 방식입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;전송 계층 보안:&lt;/strong&gt; 데이터를 주고받는 과정에서 정보를 보호하기 위해 HTTPS(TLS)를 사용하는 것은 선택이 아닌 필수입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;요청 횟수 제한 (Rate Limiting &amp;amp; Throttling):&lt;/strong&gt; 서비스 거부(DoS) 공격이나 시스템 남용을 막기 위한 핵심적인 방어 수단입니다. 보통 API 게이트웨이나 로드 밸런서에서 구현하며, 엔드포인트별, 사용자별, 또는 IP 주소별로 정해진 시간 동안의 요청 횟수를 제한합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;입력값 검증:&lt;/strong&gt; SQL 인젝션 같은 공격을 막기 위해, 클라이언트로부터 들어오는 모든 데이터(쿼리 파라미터, 요청 본문 등)를 철저히 검증해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;GraphQL API의 보안 방식&lt;/h3&gt;
&lt;p&gt;GraphQL의 &amp;quot;하나의 똑똑한 엔드포인트&amp;quot; 구조는 그 자체로 독특한 보안 과제를 만듭니다. REST에서는 &lt;code&gt;/users&lt;/code&gt;와 &lt;code&gt;/admin/reports&lt;/code&gt;처럼 각기 다른 엔드포인트에 대해 요청 횟수 제한이나 권한 규칙을 간단하게 적용할 수 있습니다. 하지만 GraphQL에서는 모든 요청이 &lt;code&gt;/graphql&lt;/code&gt;로 들어오기 때문에, 이런 보안 제어는 요청 본문을 직접 파싱해서 동적으로 결정을 내리는 애플리케이션 로직 안으로 더 깊숙이 들어와야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인증 및 인가:&lt;/strong&gt; 인증은 REST와 비슷하게 처리할 수 있습니다(예: HTTP 헤더에 JWT 전달). 하지만 인가는 훨씬 더 복잡합니다. 단일 엔드포인트 구조 때문에 &amp;#39;라우트&amp;#39; 수준에서 권한을 검사할 수 없습니다. 이는 GraphQL에서 쿼리를 확인하기 전까지는 무엇을 하려는 것인지 알 수 없기 때문입니다. 그에 따라 인가는 스키마의 각 필드나 타입에 대한 리졸버 안에서 구현되어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;복잡한 쿼리를 통한 서비스 거부 (DoS) 공격:&lt;/strong&gt; 이는 GraphQL의 고유한 취약점입니다. 공격자는 의도적으로 아주 깊게 중첩되거나 순환하는 쿼리를 보내 서버의 자원(CPU, 메모리, 데이터베이스 연결)을 고갈시킬 수 있습니다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;방어 전략:&lt;/strong&gt; 이런 공격을 막기 위해서는 &lt;strong&gt;쿼리 깊이 제한&lt;/strong&gt;(쿼리가 중첩될 수 있는 최대 깊이를 제한), &lt;strong&gt;쿼리 복잡도 분석&lt;/strong&gt;(각 필드에 &amp;#39;비용&amp;#39; 점수를 매기고, 총비용이 정해진 값을 넘는 쿼리는 거부), &lt;strong&gt;타임아웃&lt;/strong&gt; 설정 같은 방어 장치를 구현해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인트로스펙션 쿼리:&lt;/strong&gt; GraphQL은 기본적으로 클라이언트가 인트로스펙션 쿼리를 통해 전체 스키마 정보를 조회할 수 있게 허용합니다. 외부에 공개되지 않는 API의 프로덕션 환경에서 이 기능이 켜져 있으면, 공격자에게 API의 내부 구조, 사용 가능한 모든 쿼리와 뮤테이션, 데이터 타입 등 민감한 정보를 그대로 노출할 수 있습니다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;방어 전략:&lt;/strong&gt; 외부에 공개되지 않는 API의 경우, 프로덕션 환경에서는 인트로스펙션 기능을 비활성화하는 것이 강력히 권장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;그래서, 언제 무엇을 써야 할까요?&lt;/h2&gt;
&lt;h3&gt;REST를 선택하면 좋은 경우: 단순성, 표준, 캐싱이 중요할 때&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;공개 API (Public APIs):&lt;/strong&gt; 안정적이고, 문서화가 잘 되어 있으며, 사용하기 쉬운 공개 API를 만들 때는 REST의 표준화된 구조와 예측 가능성이 큰 장점입니다. 개발자 생태계가 이미 REST에 익숙하기 때문에 새로운 사용자들이 쉽게 적응할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자원 중심 시스템:&lt;/strong&gt; 제공하는 서비스가 명확한 자원들로 나뉘고, 클라이언트가 주로 자원의 전체 정보를 필요로 하는 경우(예: 제품 관리를 위한 간단한 CRUD 서비스) REST는 매우 효율적이고 직관적입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐싱이 매우 중요한 서비스:&lt;/strong&gt; 애플리케이션 성능이 CDN이나 중간 캐시에 크게 의존하고, 응답 데이터가 자주 캐시될 수 있는 경우, REST가 HTTP 캐싱과 완벽하게 호환된다는 점은 결정적인 이점입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단순한 클라이언트 또는 제한된 개발 환경:&lt;/strong&gt; 개발팀이 복잡한 클라이언트 라이브러리나 상태 관리의 부담을 피하고 싶을 때, REST의 단순함은 매력적인 선택이 될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;GraphQL을 선택하면 좋은 경우: 유연성, 효율성, 복잡한 시스템&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;복잡하고 중첩된 데이터:&lt;/strong&gt; 데이터 모델이 서로 복잡하게 얽혀 있어 클라이언트가 이 관계를 자주 탐색해야 하는 경우(예: 소셜 네트워크, 콘텐츠 관리 시스템) GraphQL이 좋은 선택이 될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;마이크로서비스 집계 (API 게이트웨이):&lt;/strong&gt; GraphQL은 여러 마이크로서비스(이들 자체는 REST API일 수 있음) 앞에 위치하는 게이트웨이 역할로 매우 효과적입니다. 이 게이트웨이는 클라이언트에게는 하나의 통일된 GraphQL 스키마를 제공하기 때문에 클라이언트에서 여러 마이크로서비스의 URL에 대한 관리포인트가 줄어드는 등의 이점이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;이전 직장에서 GraphQL을 사용하며 프론트엔드 개발자가 필요한 데이터만 정확히 조회하는 것, 그리고 특히 컴포넌트가 필요로 하는 데이터를 프래그먼트(Fragment)로 함께 선언하여 컴포넌트와 데이터 간의 응집도를 높이는 방식은 매력적이었습니다.&lt;/p&gt;
&lt;p&gt;하지만 겪어본 입장에서, 표준적으로 사용되는 RESTful API에 비해 GraphQL의 초기 러닝커브는 확실히 높다고 생각합니다. 특히 Relay 라이브러리를 사용했는데, 방대한 기능에 비해 문서가 다소 불친절하게 느껴졌습니다. 또한 프로젝트가 커지고 스키마에 정의된 &lt;strong&gt;객체 타입(Object Type)&lt;/strong&gt;이 워낙 많아지다 보니, 쿼리 작성 시 어떤 타입이 어떤 필드를 가지고 있고 서로 어떻게 연결되는지 그 관계를 파악하는 것이 헷갈릴 때도 있었습니다.&lt;/p&gt;
&lt;p&gt;GraphQL은 REST API보다 무조건 좋은 기술은 아닙니다. GraphQL과 Relay가 제공하는 &amp;#39;개발 유연성&amp;#39;과 &amp;#39;네트워크 효율성&amp;#39;이라는 강력한 이점은, &amp;#39;초기 높은 학습 비용&amp;#39;, &amp;#39;클라이언트/서버의 복잡성 증가&amp;#39;라는 명확한 비용과 교환하는 것입니다.&lt;/p&gt;
&lt;p&gt;이처럼 기술을 선택함에 있어서 최신 기술이라고 무조건 사용하는 것보다 현재 우리팀의 목표와 해결하고자 하는 문제를 고민하여 선택할 수 있는 눈을 기를 수 있도록 노력하고자 합니다.&lt;/p&gt;</description>
      <category>WEB</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/199</guid>
      <comments>https://jcon.tistory.com/199#entry199comment</comments>
      <pubDate>Tue, 4 Nov 2025 23:56:02 +0900</pubDate>
    </item>
    <item>
      <title>openapi generator를 사용하여 api에 대한 typescript 코드젠</title>
      <link>https://jcon.tistory.com/198</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저희 회사에서는 openapi 스펙으로 api 스펙을 정의하여 백엔드와 프론트엔드간에 api 스펙을 공유하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드에서는 openapi generator에서 제공하는 typescript-axios를 사용하여 타입스크립트로 코드젠을하여 api에 대한 타입 정의를 자동적으로 생성하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;openapi generator에 대한 간단한 소개와 사용하면서 부딪친 버그들을 소개하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;openapi generator란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/OpenAPITools/openapi-generator?tab=readme-ov-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;openapi-generator&lt;/a&gt;는 위에서 설명한 것 처럼 openapi 스펙으로 정의된 문서를 다양한 코드로 코드젠을 해주는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;install&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;설치 방법은 &lt;/span&gt;&lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot; href=&quot;https://github.com/OpenAPITools/openapi-generator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;를 참고 하시는 것이 제일 좋습니다. 예시는 brew를 사용하여 설치하는 방법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721724693877&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew install openapi-generator

brew tap AdoptOpenJDK/openjdk
brew install --cask adoptopenjdk11
export JAVA_HOME=`/usr/libexec/java_home -v 1.11`&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://openapi-generator.tech/docs/installation/#npm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;npm을 사용하여 설치&lt;/a&gt;도 가능합니다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예시:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/jch1223/openapi-generator/tree/main&quot;&gt;https://github.com/jch1223/openapi-generator/tree/main&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/OAI/OpenAPI-Specification?tab=readme-ov-file&quot;&gt;openapi spec&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;에서 문법을 살펴볼 수 있습니다.&lt;span&gt; 아래처럼 &lt;a href=&quot;https://github.com/jch1223/openapi-generator/blob/main/src/schema/openapi.yml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;yml파일&lt;/a&gt;에 스펙을 정의 한 후&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721724996124&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;openapi: 3.0.3
info:
  title: test api
  version: 0.0.1
servers:
- url: https://localhost:3000
  description: 로컬호스트
paths:
  /data:
    get:
      summary: 데이터 조희
      description: |-
        데이터 조희
      parameters:
      - name: exampleQueryStirng
        in: query
        schema:
          type: object
          properties:
            nullableSting:
              type: string
              nullable: true
            nullalbeArray:
              type: array
              items:
                type: string
                nullable: true
      responses:
        '200':
          description: 성공 응답으로 데이터를 반환합니다.
          content:
            application/json:
              schema: 
                type: array
                items:
                  type: string
                  nullable: true
                  description: 데이터&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721725133743&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;openapi-generator-cli generate -g typescript-axios -i ./src/schema/openapi.yml -o ./src/api&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;사용한 명령어는 아래와 같은 의미를 지닙니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;-g: 사용할 제너레이터&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;-i : 스펙 파일 폴더 위치 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;-o: output 폴더&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;쉘에서 실행을 시키면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/jch1223/openapi-generator/tree/main/src/api&quot;&gt;타입스크립트 코드&lt;/a&gt;를 생성해 주어서 이를 기반으로 api 요청 함수에 대한 타입 정의를 자동화 시킬 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버그 수정기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생성된 setFlattenedQueryParams 함수에서 null에 대한 처리가 안되는 버그&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드젠을 하면 &lt;a href=&quot;https://github.com/jch1223/openapi-generator/blob/main/src/api/common.ts#L87-L107&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;setFlattenedQueryParams&lt;/a&gt; 함수가 생성이 되고 이 함수를 통해서 param을 query string으로 변환해 주는 동작을 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 api 함수를 통해서 null이 포함된 param을 넘겨주었을 때 에러가 터지는 문제가 생겼습니다. 그래서 살펴보니&amp;nbsp;저희 회사에서는 &lt;a href=&quot;https://mvnrepository.com/artifact/org.openapitools/openapi-generator/6.2.1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;6.2.1(22년 11월 1일 배포)&lt;/a&gt; 버전을 사용하고 있었는데, null에 대한 처리가 &lt;a href=&quot;https://github.com/OpenAPITools/openapi-generator/pull/14018&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;22년 12월에 수정&lt;/a&gt;이되어서 버전을 올리는 것으로 해결하였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드젠에서&amp;nbsp;Array&amp;lt;string&amp;nbsp;|&amp;nbsp;null&amp;gt;&amp;nbsp;로&amp;nbsp;안뽑히는&amp;nbsp;버그&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;openapi 스펙에서 null에 대한 처리가 되는 것은 3.0.x이상부터 정직적으로 지원되기 시작하였습니다.(&lt;a href=&quot;https://stackoverflow.com/questions/48111459/how-to-define-a-property-that-can-be-string-or-null-in-openapi-swagger&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;관련 내용&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 3.0.x 버전을 사용하고 있었고, 아래와 같이 yml 파일에 스펙을 정의 하였을 때,&lt;/p&gt;
&lt;pre id=&quot;code_1721727939764&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;paths:
  /data:
    get:
      summary: 데이터 조희
      description: |-
        데이터 조희
      parameters:
      - name: exampleQueryStirng
        in: query
        schema:
          type: object
          properties:
            nullableSting:
              type: string
              nullable: true
            nullalbeArray:
              type: array
              items:
                type: string
                nullable: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721729987937&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export interface DataGetExampleQueryStirngParameter {
    /**
     * 
     * @type {string}
     * @memberof DataGetExampleQueryStirngParameter
     */
    'nullableSting'?: string | null;
    /**
     * 
     * @type {Array&amp;lt;string&amp;gt;}
     * @memberof DataGetExampleQueryStirngParameter
     */
    'nullalbeArray'?: Array&amp;lt;string | null&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nullalbeArray의 타입이 Array&amp;lt;string | null&amp;gt; 로 나타나기를 기대하였지만, &lt;a href=&quot;https://github.com/jch1223/openapi-generator/blob/8f9c3ce94a089093a2f6e7c226063f51623ea921/src/api/api.ts#L31-L44&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;결과&lt;/a&gt;는&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721730043149&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export interface DataGetExampleQueryStirngParameter {
    /**
     * 
     * @type {string}
     * @memberof DataGetExampleQueryStirngParameter
     */
    'nullableSting'?: string | null;
    /**
     * 
     * @type {Array&amp;lt;string&amp;gt;}
     * @memberof DataGetExampleQueryStirngParameter
     */
    'nullalbeArray'?: Array&amp;lt;string&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Array&amp;lt;string | null&amp;gt;&lt;span&gt; 이 아닌 Array&amp;lt;stiring&amp;gt;으로 코드젠이 되었습니다. string의 경우에는 nullable하게 타입이 잘 나타났지만 array에서는 나타나지 않아서 조사를 해보니 array에 대한 nullable이 버그가 있다는 것을 알게 되었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;issue:&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/OpenAPITools/openapi-generator/issues/13445&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/OpenAPITools/openapi-generator/issues/13445&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721730272704&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;[BUG] [typescript-axios] Nullable addtional properites are generated as not nullable &amp;middot; Issue #13445 &amp;middot; OpenAPITools/openapi-gen&quot; data-og-description=&quot;Bug Report Checklist Have you provided a full/minimal spec to reproduce the issue? Have you validated the input using an OpenAPI validator (example)? Have you tested with the latest master to confi...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/OpenAPITools/openapi-generator/issues/13445&quot; data-og-url=&quot;https://github.com/OpenAPITools/openapi-generator/issues/13445&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xbe52/hyWCBirfHm/VuF1wo97qZ3Gk97dP0X9c1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/OpenAPITools/openapi-generator/issues/13445&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/OpenAPITools/openapi-generator/issues/13445&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xbe52/hyWCBirfHm/VuF1wo97qZ3Gk97dP0X9c1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[BUG] [typescript-axios] Nullable addtional properites are generated as not nullable &amp;middot; Issue #13445 &amp;middot; OpenAPITools/openapi-gen&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Bug Report Checklist Have you provided a full/minimal spec to reproduce the issue? Have you validated the input using an OpenAPI validator (example)? Have you tested with the latest master to confi...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;array에 대한 nullable 처리를 타입 캐스팅이나 null 대신 &quot;&quot;(빈문자열)을 보내는 것을 고려했었는데, 이런 버그가 있다는 것을 프론트엔드 팀에 공유하니 구성원께서 &lt;a href=&quot;https://github.com/OpenAPITools/openapi-generator/pull/19157&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;버그 픽스 기여&lt;/a&gt;를 해주셔서 nullable 타입을 사용할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 정식 배포는 되지 않아서(정식배포는 24년 8월 예정) openapi-generator를 &lt;a href=&quot;https://github.com/portone-io/openapi-generator?tab=readme-ov-file#14---build-projects&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;클론 받아서 실행 할 수 있는 방법&lt;/a&gt;을 간략히 정리하자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클론을 받은 후에 해당 레포 위치에서 아래 shell 스크립트를 실행합니다&lt;/p&gt;
&lt;pre id=&quot;code_1721730730443&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./mvnw clean install&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;설치까지 꽤나 오랜 시간이 소요됩니다. 그 후 해당 레포 위치에서 아래 shell 스크립트를 실행하면 array도 nullable하게 코드젠이 잘 작동 합니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721730845341&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \
  -i samples/client/openapi.yml \
  -g typescript-axios\
  -o samples/client/typescript&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Dev Tools</category>
      <category>array nullable bug</category>
      <category>Generator</category>
      <category>OpenAPI</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/198</guid>
      <comments>https://jcon.tistory.com/198#entry198comment</comments>
      <pubDate>Tue, 23 Jul 2024 18:49:20 +0900</pubDate>
    </item>
    <item>
      <title>React에서 JWT를 이용하여 로그인 구현하기</title>
      <link>https://jcon.tistory.com/197</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어플리케이션에서 로그인을 구현하는 방법은 인증을 서버의 세션에서 할지 토큰을 통해서 할지에 따라서 달라지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 JWT를 사용하여 로그인을 구현하는 방법에 대해서 작성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dguxUf/btrI0ehnVvd/iLY8hmHwhYoB7CvNvLDLqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dguxUf/btrI0ehnVvd/iLY8hmHwhYoB7CvNvLDLqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dguxUf/btrI0ehnVvd/iLY8hmHwhYoB7CvNvLDLqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdguxUf%2FbtrI0ehnVvd%2FiLY8hmHwhYoB7CvNvLDLqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 방식과 토큰 방식의 차이점은 아래 글에서 확인 할 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1659702230382&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;쿠키, 세션(cookie, session)과 토큰 (token, JWT)의 차이점&quot; data-og-description=&quot;웹 어플리케이션에서 로그인을 구현하는 방법은 크게 쿠키, 세션을 이용하는 방법과 토큰을 이용하는 방법 2가지가 있습니다. 간단히 말하면 쿠키, 세션 방식은 서버에서 세션아이디를 기록하&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/193&quot; data-og-url=&quot;https://jcon.tistory.com/193&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UZPGm/hyPkKGh7tI/bduVc0OpkX3HBSHQGWXcFK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/eWlaT/hyPkAXYhRP/KfmfkNg82CYxpcNfSLc19K/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/n4bui/hyPkCnVFGS/Bj4i2SqBp7CEMpTno5cShK/img.png?width=749&amp;amp;height=505&amp;amp;face=0_0_749_505&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/193&quot; data-source-url=&quot;https://jcon.tistory.com/193&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UZPGm/hyPkKGh7tI/bduVc0OpkX3HBSHQGWXcFK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/eWlaT/hyPkAXYhRP/KfmfkNg82CYxpcNfSLc19K/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/n4bui/hyPkCnVFGS/Bj4i2SqBp7CEMpTno5cShK/img.png?width=749&amp;amp;height=505&amp;amp;face=0_0_749_505');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;쿠키, 세션(cookie, session)과 토큰 (token, JWT)의 차이점&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;웹 어플리케이션에서 로그인을 구현하는 방법은 크게 쿠키, 세션을 이용하는 방법과 토큰을 이용하는 방법 2가지가 있습니다. 간단히 말하면 쿠키, 세션 방식은 서버에서 세션아이디를 기록하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Access token &amp;amp; Refresh token&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 토큰을 사용하여 로그인을 구현할 때에는 보통 access token과 refresh token을 함께 사용합니다. access token과 refresh token에 대해 간단히 알아보자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;access token: 인증에 필요한 토큰&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;refresh token: access token을 다시 발급 받기 위한 토큰&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;access token만 사용하는 것이 아니라 refresh 토큰을 함께 사용하는 이유는 JWT토큰 방식의 특성 상 JWT토큰이 유효할 경우 인증된 유저로 판단하기 때문에 acess token이 탈취 되었을 때의 피해를 최소화 하기 위해서 access token을 다시 발급 받을 수 있는 refresh token과 함께 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성되어 있는 코드는 의사코드로 동작을 보장하지 않습니다. 상황에 맞게 응용하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 로그인 시에 동작하는 flow를 정리하자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 새로고침 시에도 로그인 상태가 유지되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. access token이 만료되면 refresh token을 통해서 재발행 받아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. refresh token이 만료되었다면 로그아웃 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세부 사항을 구현하기 전에 access token은 api를 요청할 때 자주 사용하게 됨으로 여러 컴포넌트에서 공유되는 것이 좋을 것 같아 access token을 관리하는 context api를 통해서 공유되도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1659702230382&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SessionProvider.js
import React from &quot;react&quot;;

const SessionContext = React.createContext&amp;lt;SessionContextValue&amp;gt;();

export default const SessionProvider = ({
  children,
}) =&amp;gt; {
  const [session, setSession] = React.useState();

  return (
    &amp;lt;SessionContext.Provider
      value={{
        session,
        setSession,
      }}
    &amp;gt;
      {children}
    &amp;lt;/SessionContext.Provider&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1659702230383&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//App.js
import SessionProvider from 'SessionProvider.js'

export default const App = () =&amp;gt; {
	return (
    	&amp;lt;SessionProvider&amp;gt;
        	&amp;lt;Component /&amp;gt;
        &amp;lt;/SessionProvider&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후에 새로고침시에도 로그인을 유지시키기 위해서는 로컬스토리지에 access token과 refresh token을 저장해 둘 필요가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1659702447390&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SessionProvider.js
import React from &quot;react&quot;;

const SessionContext = React.createContext&amp;lt;SessionContextValue&amp;gt;();

export default const SessionProvider = ({
  children,
}) =&amp;gt; {
  const [session, setSession] = React.useState();
  
  // 로그인 시에 로컬스토리지에 저장
  React.useEffect(() =&amp;gt; {
    if (session) {
      localStorage.setItem(&quot;authToken&quot;, session.authToken);
      localStorage.setItem(&quot;refreshToken&quot;, session.refreshToken);
    }
  }, [session]);
  
  // 새로고침, 재접속 시 로그인 유지를 담당
  React.useEffect(() =&amp;gt; {
    const localStorageAuthToken = localStorage.getItem(&quot;authToken&quot;);
    const localStorageRefreshToken = localStorage.getItem(&quot;refreshToken&quot;);

    if (localStorageAuthToken &amp;amp;&amp;amp; localStorageRefreshToken) {
      setSession({
        authToken: localStorageAuthToken,
        refreshToken: localStorageRefreshToken,
      });
    }
  }, []);


  return (
    &amp;lt;SessionContext.Provider
      value={{
        session,
        setSession,
      }}
    &amp;gt;
      {children}
    &amp;lt;/SessionContext.Provider&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 시에 access token과 refresh token을 setSession을 통해 session state에 업데이트 하면, useEffect를 통해서 로컬스토리지에 저장됩니다.&amp;nbsp;새로 고침 시에는 로컬 스토리지에서 가져와 session state에 업데이트 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 새로고침 시에도 로그인이 풀리지 않고, 유지 할 수 있게 되었습니다. 다음은 access token이 만료되었을 경우 refresh token을 재발급하고 refresh token이 만료되었다면 로그아웃 시키도록 하겠습니다. access token을 재발급 받는 여러가지 방법이 있겠지만, 지금은 setInterval을 통해 일정 시간 마다 access token이 만료되었는지 체크하고 만료 되었으면 토큰을 재발행하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1659703521492&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SessionProvider.js
import React from &quot;react&quot;;
import { decodeJwt } from &quot;jose&quot;;

const SessionContext = React.createContext&amp;lt;SessionContextValue&amp;gt;();
const DEFAULT_REFRESH_INTERVAL_MS = 1000;

export default const SessionProvider = ({
  children,
}) =&amp;gt; {
  const [session, setSession] = React.useState();
  
  // 로그인 시에 로컬스토리지에 저장
  React.useEffect(() =&amp;gt; {
    if (session) {
      localStorage.setItem(&quot;authToken&quot;, session.authToken);
      localStorage.setItem(&quot;refreshToken&quot;, session.refreshToken);
    }
  }, [session]);
  
  // 새로고침, 재접속 시 로그인 유지를 담당
  React.useEffect(() =&amp;gt; {
    const localStorageAuthToken = localStorage.getItem(&quot;authToken&quot;);
    const localStorageRefreshToken = localStorage.getItem(&quot;refreshToken&quot;);

    if (localStorageAuthToken &amp;amp;&amp;amp; localStorageRefreshToken) {
      setSession({
        authToken: localStorageAuthToken,
        refreshToken: localStorageRefreshToken,
      });
    }
  }, []);
  
  // 토큰 재발급을 담당
  React.useEffect(() =&amp;gt; {
    regenerateExpiredToken();

    const intervalId = setInterval(
      regenerateExpiredToken,
      DEFAULT_REFRESH_INTERVAL_MS
    );

    return () =&amp;gt; clearInterval(intervalId);
  }, [regenerateExpiredToken]);
  
  const regenerateExpiredToken = async () =&amp;gt; {
    try {
      if (!session) return;

      const authTokenExpiredTime = decodeJwt(session.authToken).exp;
      const refreshTokenExpiredTime = decodeJwt(session.refreshToken).exp;
      const nowUnixTime = dayjs().unix();

      // refresh token이 만료되었다면 로그아웃
      if (refreshTokenExpiredTime &amp;lt; nowUnixTime) {
        throw new Error(&quot;Expired refresh token&quot;);
      }

      // access token이 만료되었다면 토큰 재발급 후 업데이트
      if (authTokenExpiredTime &amp;lt; nowUnixTime) {
        const { accessToken, refreshToken } = await getRegeneratedToken({ refreshToken });
        setSession({ accessToken, refreshToken });
    } catch (error) {
      removeSession();
    }
  };
  
  // 로그아웃 로직
  const removeSession = () =&amp;gt; {
    setSession(undefined);
    localStorage.removeItem(&quot;authToken&quot;);
    localStorage.removeItem(&quot;refreshToken&quot;);
  };

  return (
    &amp;lt;SessionContext.Provider
      value={{
        session,
        setSession,
      }}
    &amp;gt;
      {children}
    &amp;lt;/SessionContext.Provider&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getRegeneratedToken은 refresh token을 전달하면 유효한 새로운 access token과 refresh token을 reponse하는 api라고 생각하시면 됩니다. 로그아웃 시에는 로컬스토리지에 있는 토큰들을 삭제하고, session state도 초기화 하여 구현합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;또 다른 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;access token을 로컬스토리지에 저장하는 것은 xss 공격에 탈취당할 위험성이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하는 또 다른 방법은 로그인 시에 로컬스토리지에 refresh token만을 저장하고 access token은 state에만 저장합니다. 그리고 setInterval로 일정시간마다 체크하는 것이 아니라,&amp;nbsp;&lt;span&gt;setTimeout을 이용하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;access token의 유효시간 또는 유효시간 보다 조금 작은 시간에 토큰을 다시 재발급합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침 시에는 저장된 refresh token을 이용해서 access token을 항상 새로 받도록하는 방법으로 구현을 한다면 xss 공격에서 access token을 탈취당할 위험이 줄어듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1685195677844&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;쿠키, 세션(cookie, session)과 토큰 (token, JWT)의 차이점&quot; data-og-description=&quot;웹 어플리케이션에서 로그인을 구현하는 방법은 크게 쿠키, 세션을 이용하는 방법과 토큰을 이용하는 방법 2가지가 있습니다. 간단히 말하면 쿠키, 세션 방식은 서버에서 세션아이디를 기록하&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/193&quot; data-og-url=&quot;https://jcon.tistory.com/193&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UZPGm/hyPkKGh7tI/bduVc0OpkX3HBSHQGWXcFK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/eWlaT/hyPkAXYhRP/KfmfkNg82CYxpcNfSLc19K/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/n4bui/hyPkCnVFGS/Bj4i2SqBp7CEMpTno5cShK/img.png?width=749&amp;amp;height=505&amp;amp;face=0_0_749_505&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/193&quot; data-source-url=&quot;https://jcon.tistory.com/193&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UZPGm/hyPkKGh7tI/bduVc0OpkX3HBSHQGWXcFK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/eWlaT/hyPkAXYhRP/KfmfkNg82CYxpcNfSLc19K/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/n4bui/hyPkCnVFGS/Bj4i2SqBp7CEMpTno5cShK/img.png?width=749&amp;amp;height=505&amp;amp;face=0_0_749_505');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;쿠키, 세션(cookie, session)과 토큰 (token, JWT)의 차이점&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;웹 어플리케이션에서 로그인을 구현하는 방법은 크게 쿠키, 세션을 이용하는 방법과 토큰을 이용하는 방법 2가지가 있습니다. 간단히 말하면 쿠키, 세션 방식은 서버에서 세션아이디를 기록하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>React</category>
      <category>Access</category>
      <category>JWT</category>
      <category>React</category>
      <category>refresh</category>
      <category>token</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/197</guid>
      <comments>https://jcon.tistory.com/197#entry197comment</comments>
      <pubDate>Fri, 5 Aug 2022 22:00:24 +0900</pubDate>
    </item>
    <item>
      <title>React 초기 프로젝트 세팅 (eslint, husky etc..)</title>
      <link>https://jcon.tistory.com/194</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Yw0u/btrzSAsv5Km/JAKnb8efQwtXhwS2QOQuo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Yw0u/btrzSAsv5Km/JAKnb8efQwtXhwS2QOQuo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Yw0u/btrzSAsv5Km/JAKnb8efQwtXhwS2QOQuo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Yw0u%2FbtrzSAsv5Km%2FJAKnb8efQwtXhwS2QOQuo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;husky, commitlint&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;husky는 git의 hook들을 쉽게 관리하게 해주는 툴입니다. husky에 commitlint를 사용하여 git commit시에 prefix를 쉽게 적용하고, commit 메세지에 대한 검사를 진행하기 위해 적용하는 것을 선호합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;npx husky-init &amp;amp;&amp;amp; npm install
npm install --save-dev @commitlint/cli @commitlint/config-conventional commitizen

npx commitizen init cz-conventional-changelog --save-dev --save-exact
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'
npx husky add .husky/prepare-commit-msg 'exec &amp;lt; /dev/tty &amp;amp;&amp;amp; node_modules/.bin/cz --hook || true'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;//commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되면 터미널에 &lt;code&gt;git commit&lt;/code&gt;명령어를 입력시 prefix 선택과 commit의 제목을 입력 할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;test library, jest dom eslint 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cra로 설치 시 기본적인 세팅은 적용되어 있지만, 좀 더 깔끔한 테스트 코드 작성을 위해 설치하는 것이 좋은 것 같습니다.&lt;/p&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;npm install --save-dev eslint-plugin-testing-library
npm install --save-dev eslint-plugin-jest-dom&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// .eslintrc.json
{
  &quot;plugins&quot;: [&quot;testing-library&quot;, &quot;jest-dom&quot;],
  &quot;extends&quot;: [
    &quot;react-app&quot;,
    &quot;react-app/jest&quot;,
    &quot;plugin:testing-library/react&quot;,
    &quot;plugin:jest-dom/recommended&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsqmbl/btrzQHzldpr/m24k5I2YAqDPTMmAwaxKt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsqmbl/btrzQHzldpr/m24k5I2YAqDPTMmAwaxKt0/img.png&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;180&quot; style=&quot;width: 48.6866%; margin-right: 10px;&quot; data-widthpercent=&quot;49.26&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsqmbl/btrzQHzldpr/m24k5I2YAqDPTMmAwaxKt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsqmbl%2FbtrzQHzldpr%2Fm24k5I2YAqDPTMmAwaxKt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;365&quot; height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0pYn9/btrzQIkOWJP/mTvjTuZsXyIBAEI4XCVqY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0pYn9/btrzQIkOWJP/mTvjTuZsXyIBAEI4XCVqY0/img.png&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;169&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.1507%;&quot; data-widthpercent=&quot;50.74&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0pYn9/btrzQIkOWJP/mTvjTuZsXyIBAEI4XCVqY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0pYn9%2FbtrzQIkOWJP%2FmTvjTuZsXyIBAEI4XCVqY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;353&quot; height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;eslint를 설정 하면 왼쪽 코드를 오른쪽 코드로 자동 수정이 가능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;typescript&lt;/h3&gt;
&lt;pre id=&quot;code_1650382674126&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install --save typescript @types/node @types/react @types/react-dom @types/jest&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1650384386364&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es5&quot;,
    &quot;lib&quot;: [&quot;dom&quot;, &quot;dom.iterable&quot;, &quot;esnext&quot;],
    &quot;allowJs&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;allowSyntheticDefaultImports&quot;: true,
    &quot;strict&quot;: true,
    &quot;forceConsistentCasingInFileNames&quot;: true,
    &quot;noFallthroughCasesInSwitch&quot;: true,
    &quot;module&quot;: &quot;esnext&quot;,
    &quot;moduleResolution&quot;: &quot;node&quot;,
    &quot;resolveJsonModule&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;noEmit&quot;: true,
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;strictNullChecks&quot;: false
  },
  &quot;include&quot;: [&quot;src&quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react 18버전이 나오면서 cra 사용시 react와 관련된 type 라이브러리간에 싱크가 안맞는 경우가 있습니다. 이럴 때 위에 적혀있는 라이브러리에 대한 업데이트가 필요할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1650384545565&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Adding TypeScript | Create React App&quot; data-og-description=&quot;Note: this feature is available with react-scripts@2.1.0 and higher.&quot; data-og-host=&quot;create-react-app.dev&quot; data-og-source-url=&quot;https://create-react-app.dev/docs/adding-typescript/&quot; data-og-url=&quot;https://create-react-app.dev/docs/adding-typescript&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2GnHT/hyN6ZjF2or/Qmqp8w8vk1tc9lEBscETZ1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/sY0cd/hyN6W1zECP/KrpKQynFIIKi1K66TCtLOk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://create-react-app.dev/docs/adding-typescript/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://create-react-app.dev/docs/adding-typescript/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2GnHT/hyN6ZjF2or/Qmqp8w8vk1tc9lEBscETZ1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/sY0cd/hyN6W1zECP/KrpKQynFIIKi1K66TCtLOk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Adding TypeScript | Create React App&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Note: this feature is available with react-scripts@2.1.0 and higher.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;create-react-app.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/194</guid>
      <comments>https://jcon.tistory.com/194#entry194comment</comments>
      <pubDate>Wed, 20 Apr 2022 01:10:22 +0900</pubDate>
    </item>
    <item>
      <title>쿠키, 세션(cookie, session)과 토큰 (token, JWT)의 차이점</title>
      <link>https://jcon.tistory.com/193</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹 어플리케이션에서 로그인을 구현하는 방법은 크게 쿠키, 세션을 이용하는 방법과 토큰을 이용하는 방법 2가지가 있습니다. 간단히 말하면 쿠키, 세션 방식은 서버에서 세션아이디를 기록하고 브라우저에서 쿠키를 저장하는 방법이고 토큰 방식은 토큰을 발행하여 토큰의 유효성을 검증하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJXXmF/btrzNAOmy4j/5o8Ls8EsNYkEjhRqXDQhrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJXXmF/btrzNAOmy4j/5o8Ls8EsNYkEjhRqXDQhrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJXXmF/btrzNAOmy4j/5o8Ls8EsNYkEjhRqXDQhrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJXXmF%2FbtrzNAOmy4j%2F5o8Ls8EsNYkEjhRqXDQhrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키, 세션 (cookie, session)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자(클라이언트)가 서버에 자원을 요청 시 서버에서 사용자를 특정할 수 있는 데이터를 기반으로 세션 아이디를 생성하고, 해당 아이디를 키값으로 하여 필요한 값들을 서버의 메모리에 저장하고, HTTP 프로토콜의 header의 setCookie를 통해서 브라우저의 쿠키에 세션 아이디를 기록하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 request 요청을 보낼 시 header에 cookie를 자동으로 포함하여 보내게 되고(cors일 경우 credentials 등 세팅이 필요) 서버에서는 cookie의 세션 아이디를 토대로 사용자의 로그인 등을 판별할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#%EC%9D%B8%EC%A6%9D%EC%A0%95%EB%B3%B4%EB%A5%BC_%ED%8F%AC%ED%95%A8%ED%95%9C_%EC%9A%94%EC%B2%AD&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;인증 정보를 포함하기 위한 cors 설정&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650360138421&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;교차 출처 리소스 공유 (CORS) - HTTP | MDN&quot; data-og-description=&quot;교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가&amp;nbsp;HTTP&amp;nbsp;헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#%EC%9D%B8%EC%A6%9D%EC%A0%95%EB%B3%B4%EB%A5%BC_%ED%8F%AC%ED%95%A8%ED%95%9C_%EC%9A%94%EC%B2%AD&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cqPLmR/hyN65cVmKf/9EGug3WReN26ztbseNr0w1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080,https://scrap.kakaocdn.net/dn/c34RTP/hyN5urR2ya/gNgBjS12QvHQ9EwCKpR4SK/img.png?width=1024&amp;amp;height=1076&amp;amp;face=0_0_1024_1076&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#%EC%9D%B8%EC%A6%9D%EC%A0%95%EB%B3%B4%EB%A5%BC_%ED%8F%AC%ED%95%A8%ED%95%9C_%EC%9A%94%EC%B2%AD&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#%EC%9D%B8%EC%A6%9D%EC%A0%95%EB%B3%B4%EB%A5%BC_%ED%8F%AC%ED%95%A8%ED%95%9C_%EC%9A%94%EC%B2%AD&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cqPLmR/hyN65cVmKf/9EGug3WReN26ztbseNr0w1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080,https://scrap.kakaocdn.net/dn/c34RTP/hyN5urR2ya/gNgBjS12QvHQ9EwCKpR4SK/img.png?width=1024&amp;amp;height=1076&amp;amp;face=0_0_1024_1076');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;교차 출처 리소스 공유 (CORS) - HTTP | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가&amp;nbsp;HTTP&amp;nbsp;헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;사용자의 데이터를 서버에서 관리하기 때문에 사용자의 데이터가 토큰에 담겨있는 것보다 안전합니다.&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;서버의 성능을 늘리는 방식을 scale-out으로 할 경우 각 서버의 세션을 동기화 해야하는 비용이 추가적으로 발생 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;사용자의 데이터를 서버의 메모리에 저장하기 때문에 메모리 용량에 대한 리스크가 있을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;scale-out, scale-up&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scale-out에 대해 간단히 설명하자면 서버의 성능을 물리적으로 올리는 방법 중 하나 입니다. 서버의 성능을 올리는 방법은 크게scale-out과 scale-up으로 나눌 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oSKVz/btrzRW9VwaI/GE9W29WEXWSUFJkeDkvCfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oSKVz/btrzRW9VwaI/GE9W29WEXWSUFJkeDkvCfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oSKVz/btrzRW9VwaI/GE9W29WEXWSUFJkeDkvCfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoSKVz%2FbtrzRW9VwaI%2FGE9W29WEXWSUFJkeDkvCfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;347&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림처럼&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;scale-out은 서버의 갯수를 늘려서 성능을 올리는 방법이고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;scale-up은 서버 자체의 성능(디스크의 용량 등)을 올리는 방법입니다. 따라서 세션 방식의 경우 서버의 메모리에 의존적이기 때문에 서버의 갯수를 늘리는 scale-out방식으로 성능을 올릴 경우 세션에 대한 데이터를 어떻게 공유시킬 건지에 대한 고민도 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;토큰 방식 (token,&amp;nbsp;JWT)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 로그인을 요청 시 서버에서는 어떠한 데이터를 기반으로 인증이 가능한 토큰을 만들어서 클라이언트에 전달하는 방식입니다. 클라이언트는 요청 시 토큰을 서버로 보내고 서버는 토큰에 대한 유효성을 검사하여 사용자가 인증되었는지 판별합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰방식은 토큰의 유효성을 확인 할 수만 있다면 유저의 인증을 별도로 분리할 수 있습니다. 예로 구글 로그인을 통해서 구글에서 발급한 토큰을 구글의 토큰 인증 서버를 통해 유효성을 검증하여 사용자를 인증할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT토큰은 토큰에 대한 인증을 토큰 자체를 통해서도 가능하게 하여 토큰의 유효성을 확인하기 위해 별도의 인증 서버를 통하지 않고도 JWT토큰을 해석하여 인증 여부를 확인 할 수 있게 만들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY1rKQ/btrzNCk0oqC/bwYFLEbkmq8CPXbOKNosI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY1rKQ/btrzNCk0oqC/bwYFLEbkmq8CPXbOKNosI1/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;http://www.opennaru.com/opennaru-blog/jwt-json-web-token/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY1rKQ/btrzNCk0oqC/bwYFLEbkmq8CPXbOKNosI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY1rKQ%2FbtrzNCk0oqC%2FbwYFLEbkmq8CPXbOKNosI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;749&quot; height=&quot;505&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;nbsp;http://www.opennaru.com/opennaru-blog/jwt-json-web-token/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;사용자의 인증을 토큰을 통해서 관리하기 때문에 서버의 scale-out에서도 인증이 자유롭습니다.&lt;/li&gt;
&lt;li&gt;사용자에 대한 인증 방식의 확장이 가능합니다. (facebook, google 등 소셜 로그인)&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;인증된 토큰을 발급을 하면 토큰이 expire되기 전까지 토큰의 유효성을 막을 방법이 없습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자를 block을 시켜야 할 때 세션방식은 세션 아이디를 파기하여 즉시 block할 수 있지만, 토큰방식은 발급한 토큰이 expire 되기 전까지 사용자에 대한 인증이 유효할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>WEB</category>
      <category>Cookie</category>
      <category>JWT</category>
      <category>session</category>
      <category>token</category>
      <category>VS</category>
      <category>세션</category>
      <category>쿠키</category>
      <category>토큰</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/193</guid>
      <comments>https://jcon.tistory.com/193#entry193comment</comments>
      <pubDate>Tue, 19 Apr 2022 18:43:26 +0900</pubDate>
    </item>
    <item>
      <title>React 18 버전으로 개발하면서 만난 문제들</title>
      <link>https://jcon.tistory.com/192</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스팅은 개인적으로 작은 사이트 프로젝트나 과제 등을 React18 버전으로 진행하면서 발생한 에러 등을 정리하기 위한 글입니다. 지속적으로 추가 될 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sample.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caWBxM/btrziSHp7zL/psFzRrQVbFL6qi5bD4Ksik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caWBxM/btrziSHp7zL/psFzRrQVbFL6qi5bD4Ksik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caWBxM/btrziSHp7zL/psFzRrQVbFL6qi5bD4Ksik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaWBxM%2FbtrziSHp7zL%2FpsFzRrQVbFL6qi5bD4Ksik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;sample.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. cra에서 기존에 제공하던 typescript 템플릿 등의 의존성 오류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 cra를 통해 리액트 프로젝트를 생성하면 18버전으로 생성이 됩니다. 하지만 기존에 있던 typescript 등이 적용된 템플릿은 아직 의존성에 대한 부분들이 업데이트 되지 않았습니다. 의존성 문제는 아래 7가지 라이브러리를 최신 버전으로 업데이트 하여 해결하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1649839425350&quot; class=&quot;perl&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;@testing-library/jest-dom&quot;
&quot;@testing-library/react&quot;
&quot;@testing-library/user-event&quot;
&quot;@types/jest&quot;
&quot;@types/node&quot;
&quot;@types/react&quot;
&quot;@types/react-dom&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;figure id=&quot;og_1649839425351&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jch1223/react-typescript-redux&quot; data-og-description=&quot;Contribute to jch1223/react-typescript-redux development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jch1223/react-typescript-redux&quot; data-og-url=&quot;https://github.com/jch1223/react-typescript-redux&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bNXne2/hyN1FGNaDU/Iuo67ZiohvkLWeXcQNMn7K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jch1223/react-typescript-redux&quot; data-source-url=&quot;https://github.com/jch1223/react-typescript-redux&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bNXne2/hyN1FGNaDU/Iuo67ZiohvkLWeXcQNMn7K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jch1223/react-typescript-redux&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to jch1223/react-typescript-redux development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;@types/react 18버전에서 FC 타입의 props 변경&lt;/h2&gt;
&lt;pre id=&quot;code_1649839425351&quot; class=&quot;coffeescript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { FC } from 'react';

const App: FC = () =&amp;gt; {
  ...
  return &amp;lt;div&amp;gt;App&amp;lt;/div&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 컴포넌트의 타입을 지정할 때 FC 타입을 주로 사용하였습니다. 이전 버전에서는 FC 타입에 기본적으로 children props가 있었지만 @types/react 18버전부터 children props가 제거 되었습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1649839425352&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;React 18 TypeScript children FC&quot; data-og-description=&quot;I upgraded to React 18 and things compiled fine. Today it seems every single component that uses children is throwing an error. Property 'children' does not exist on type 'IPageProps'. Before child...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/71788254/react-18-typescript-children-fc&quot; data-og-url=&quot;https://stackoverflow.com/questions/71788254/react-18-typescript-children-fc&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zisbN/hyN1JPWrgn/5Yn00zi4M4fu8auJyuJqGk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/71788254/react-18-typescript-children-fc&quot; data-source-url=&quot;https://stackoverflow.com/questions/71788254/react-18-typescript-children-fc&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zisbN/hyN1JPWrgn/5Yn00zi4M4fu8auJyuJqGk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;React 18 TypeScript children FC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I upgraded to React 18 and things compiled fine. Today it seems every single component that uses children is throwing an error. Property 'children' does not exist on type 'IPageProps'. Before child...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/192</guid>
      <comments>https://jcon.tistory.com/192#entry192comment</comments>
      <pubDate>Wed, 13 Apr 2022 17:44:14 +0900</pubDate>
    </item>
    <item>
      <title>react 18버전에서 달라지는 batching</title>
      <link>https://jcon.tistory.com/191</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sample.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpeyb3/btryqg3ifDJ/OWZWjhTBHagA000C6iSSbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpeyb3/btryqg3ifDJ/OWZWjhTBHagA000C6iSSbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpeyb3/btryqg3ifDJ/OWZWjhTBHagA000C6iSSbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpeyb3%2Fbtryqg3ifDJ%2FOWZWjhTBHagA000C6iSSbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;sample.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react 18버전이 릴리즈 되었습니다. 변경점 중 하나인 batching 프로세스에 대해서 정리해보려고 합니다. 개발을 하시다보면 17버전 이하에서 이벤트 핸들러 안에 있는데 batching이 안되는 경험을 겪어보신 분들이 있을 것이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 batching에 대해서 간단히 소개하고, 17버전 이하에서 batching이 안되는 상황과 18버전에서 어떻게 변경이 되었는지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. batching이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react에서 이벤트 핸들러안에 여러 setState를 하게 될 경우 같은 함수 스코프의 setState를 일괄적으로 처리하여 한번만 리렌더링 되도록 일괄처리(batching)를 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649047871981&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  useEffect(() =&amp;gt; {
    console.log(&quot;render&quot;);  // Next 버튼 클릭 시 batcing으로 인해 한번만 찍힘.
  });
  
  function handleClick() {
    setCount(c =&amp;gt; c + 1); // 아직 리렌더링 되지 않습니다.
    setFlag(f =&amp;gt; !f); // 아직 리렌더링 되지 않습니다.
    // 마지막에 한번만 리렌더링 됩니다.(이를 batching이라 합니다.)
  }
  
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={handleClick}&amp;gt;Next&amp;lt;/button&amp;gt;
      &amp;lt;h1 style={{ color: flag ? &quot;blue&quot; : &quot;black&quot; }}&amp;gt;{count}&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. batching이 되지 않는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://codesandbox.io/s/react-v17-tkh7ds?file=/src/App.js&quot;&gt;https://codesandbox.io/s/react-v17-tkh7ds?file=/src/App.js&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.log가 한번만 찍히는 프로세스는 react의 batching에 대해서 공부해 보신 분이라면 setCount와 setFlag가 한번만 리렌더링 되는 것이 익숙하실 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경험해 보셨을지 모르겠지만 이벤트 헨들러 내에 있어도 batching이 되지 않는 경우가 있습니다. 조금 더 자세하게 설명하자면 17버전에서는 react의 이벤트 핸들러 내에서만 batching프로세스가 동작하고, promise의 .then 콜백 내부&lt;span style=&quot;color: #000000;&quot;&gt;, setTimeout에서는&amp;nbsp; batching이 되지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649050247213&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  useEffect(() =&amp;gt; {
    console.log(&quot;render&quot;);
  });

  function handleSetTimeoutClick() {
    setTimeout(() =&amp;gt; {
      setCount((c) =&amp;gt; c + 1);
      setFlag((f) =&amp;gt; !f);
    });
    // Next (setTimeout)클릭 시 console에 의해 'render'가 두번 찍힘.
  }

  function handlePromiseClick() {
    const promise = Promise.resolve();
    promise.then(() =&amp;gt; {
      setCount((c) =&amp;gt; c + 1);
      setFlag((f) =&amp;gt; !f);
    });
    // Next (promise)클릭 시 console에 의해 'render'가 두번 찍힘.
  }

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={handleSetTimeoutClick}&amp;gt;Next (setTimeout)&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={handlePromiseClick}&amp;gt;Next (promise)&amp;lt;/button&amp;gt;
      &amp;lt;h1 style={{ color: flag ? &quot;blue&quot; : &quot;black&quot; }}&amp;gt;{count}&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 async, await을 사용할 때에는 batching이 어떻게 일어나게 될까요?&lt;/p&gt;
&lt;pre id=&quot;code_1649050577656&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  useEffect(() =&amp;gt; {
    console.log(&quot;render&quot;);
  });

  async function handleAsyncClick() {
    setCount((c) =&amp;gt; c + 1); 
    setFlag((f) =&amp;gt; !f);
    // await을 만나기 전까지는 batching이 동작함.
    await 1;
    // await 이후에는 batching이 동작하지 않음.
    setCount((c) =&amp;gt; c + 1);
    setFlag((f) =&amp;gt; !f);
  }
  
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={handleAsyncClick}&amp;gt;Next (async)&amp;lt;/button&amp;gt;
      &amp;lt;h1 style={{ color: flag ? &quot;blue&quot; : &quot;black&quot; }}&amp;gt;{count}&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;await 이전까지는 정상적으로 batching이 되지만 await 이후에는 batching이 되지 않는 것을 확인 할 수 있습니다. async 함수 내에 await부분을 주석처리 하고 버튼을 클릭하면 렌더가 한번만 되는 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 동작하는 이유는 자바스크립트 내부적으로 아래와 같이 코드가 변경되서 실행되는 것으로 예상하고 있습니다. 틀린 부분이 있다면 답글 부탁드립니다.&lt;/p&gt;
&lt;pre id=&quot;code_1649050869900&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  useEffect(() =&amp;gt; {
    console.log(&quot;render&quot;);
  });

  async function handleAsyncClick() {
    setCount((c) =&amp;gt; c + 1); 
    setFlag((f) =&amp;gt; !f);
    
    Promise
      .resolve(1)
      .then(() =&amp;gt; {
        setCount((c) =&amp;gt; c + 1);
        setFlag((f) =&amp;gt; !f);
      });
  }
  
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={handleAsyncClick}&amp;gt;Next (async)&amp;lt;/button&amp;gt;
      &amp;lt;h1 style={{ color: flag ? &quot;blue&quot; : &quot;black&quot; }}&amp;gt;{count}&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 이유들로 17버전에서는 batching 프로세스가 동작하지 않는 경우가 있었습니다. 18버전에서는 이런 상황들에서도 batching 프로세스가 동작되도록 변경이 되었는데 ReactDOM.render 메소드의 사용 방법이 달라져서 18버전에 맞게 적용을 해주어야합니다. 만약 18버전의 render 방식으로 하지 않으면 17버전으로 동작하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 18버전에서 batching 적용을 하고 싶지 않을 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다보면 이전 상태에 의존성을 가진 상황을 구현해야 할 때가 있습니다. 이 경우에는 batching 프로세스가 동작하지 않도록 막아야 할 필요성이 있습니다. batching 프로세스를 막는 방법은 아래와 같이 flushSync 메소드를 사용해서 막을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1652606187498&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// batching을 막고 싶을 때
import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() =&amp;gt; {
    setCounter(c =&amp;gt; c + 1);
  });
  // React has updated the DOM by now
  flushSync(() =&amp;gt; {
    setFlag(f =&amp;gt; !f);
  });
  // React has updated the DOM by now
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 18버전 적용 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;18 버전에 대한 적용 방법은 react 공식문서에 자세히 나와있어서 링크로 대체하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1649051394335&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;How to Upgrade to React 18 &amp;ndash; React Blog&quot; data-og-description=&quot;As we shared in the release post, React 18 introduces features powered by our new concurrent renderer, with a gradual adoption strategy for existing applications. In this post, we will guide you through the steps for upgrading to React 18. Please report an&quot; data-og-host=&quot;reactjs.org&quot; data-og-source-url=&quot;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis&quot; data-og-url=&quot;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bR6rIW/hyNVLZ1xh8/T5sukKPTNryMjaRJcCazQ0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bR6rIW/hyNVLZ1xh8/T5sukKPTNryMjaRJcCazQ0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to Upgrade to React 18 &amp;ndash; React Blog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;As we shared in the release post, React 18 introduces features powered by our new concurrent renderer, with a gradual adoption strategy for existing applications. In this post, we will guide you through the steps for upgrading to React 18. Please report an&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;reactjs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/21&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/reactwg/react-18/discussions/21&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#automatic-batching&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#automatic-batching&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/21&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/reactwg/react-18/discussions/21&lt;/a&gt;&lt;/p&gt;</description>
      <category>React</category>
      <category>batching</category>
      <category>react 17</category>
      <category>react 18</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/191</guid>
      <comments>https://jcon.tistory.com/191#entry191comment</comments>
      <pubDate>Mon, 4 Apr 2022 14:53:03 +0900</pubDate>
    </item>
    <item>
      <title>react 컴포넌트가 unmount 된 후 setState가 실행되는 문제 해결 방법</title>
      <link>https://jcon.tistory.com/190</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-filename=&quot;sample.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BQkp4/btq9tICFnQ6/UuY2Mcz2EqZwevUeXhTBek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BQkp4/btq9tICFnQ6/UuY2Mcz2EqZwevUeXhTBek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BQkp4/btq9tICFnQ6/UuY2Mcz2EqZwevUeXhTBek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBQkp4%2Fbtq9tICFnQ6%2FUuY2Mcz2EqZwevUeXhTBek%2Fimg.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-filename=&quot;sample.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트로 개발을 하다보면 한번은 아래와 같은 warning메세지를 보게됩니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.&lt;br /&gt;&lt;br /&gt;현재 해당 warning 메세지는 최초 1회만 나타나고, 브라우저를 새로 고침 하기 전에는 다시 나타나지 않으니 코드 수정 후 확인 할 시에 주의가 필요합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 에러는 fetch 등 비동기를 실행하는 실행 컨텍스트가 완료 되기 전에 컴포넌트가 unmount가 된 후 setState가 실행이 될 때 발생하게 됩니다. 에러가 발생하는 코드는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626056228531&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from &quot;react&quot;;
import &quot;./styles.css&quot;;

const fakeFetch = () =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(&quot;fetch done&quot;);
      resolve(Math.random());
    }, 2000);
  });
};

const Div = () =&amp;gt; {
  const [state, setState] = useState(0);

  useEffect(() =&amp;gt; {
    (async function foo() {
      try {
        const val = await fakeFetch();
        setState(val);
      } catch (error) {
        console.log(&quot;error&quot;, error);
        setState(error.message);
      }
    })();
  }, []);

  return &amp;lt;div style={{ backgroundColor: &quot;red&quot; }}&amp;gt;{state}&amp;lt;/div&amp;gt;;
};

export default function App() {
  const [isDivShow, setIsDivShow] = useState(false);

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;h1&amp;gt;Hello CodeSandbox&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;Start editing to see some magic happen!&amp;lt;/h2&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setIsDivShow((prev) =&amp;gt; !prev)}&amp;gt;버튼&amp;lt;/button&amp;gt;
      {isDivShow &amp;amp;&amp;amp; &amp;lt;Div /&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼을 누르면 Div 컴포넌트가 렌더링 되고 fetch 요청이 갑니다. 2초 안에 다시 버튼을 누르면 Div 컴포넌트는 unmount가 됩니다. fetch가 종료 된 후 setState가 실행이됩니다. 하지만 컴포넌트는 이미 unmount 상태이기 때문에 리엑트에서는 warning 메세지가 나타납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/memory-leak-warning-ditmy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://codesandbox.io/s/memory-leak-warning-ditmy&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1626056550504&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;memory leak warning - CodeSandbox&quot; data-og-description=&quot;memory leak warning by 냉정 using react, react-dom, react-scripts&quot; data-og-host=&quot;codesandbox.io&quot; data-og-source-url=&quot;https://codesandbox.io/s/memory-leak-warning-ditmy&quot; data-og-url=&quot;https://codesandbox.io/s/ditmy&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/memory-leak-warning-ditmy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codesandbox.io/s/memory-leak-warning-ditmy&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;memory leak warning - CodeSandbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;memory leak warning by 냉정 using react, react-dom, react-scripts&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;codesandbox.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법 1 : 트리거 변수를 이용하여 setState 방지&lt;/h2&gt;
&lt;pre id=&quot;code_1626064851064&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from &quot;react&quot;;
import &quot;./styles.css&quot;;

const fakeFetch = () =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(&quot;fetch done&quot;);
      resolve(Math.random());
    }, 2000);
  });
};

const Div = () =&amp;gt; {
  const [state, setState] = useState(0);

  useEffect(() =&amp;gt; {
    let isMount = true;

    (async function foo() {
      try {
        const val = await fakeFetch();

        if (isMount) {
          setState(val);
        }
      } catch (error) {
        console.log(&quot;error&quot;, error);
        setState(error.message);
      }
    })();

    return () =&amp;gt; {
      isMount = false;
    };
  }, []);

  return &amp;lt;div style={{ backgroundColor: &quot;red&quot; }}&amp;gt;{state}&amp;lt;/div&amp;gt;;
};

export default function App() {
  const [isDivShow, setIsDivShow] = useState(false);

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;h1&amp;gt;Hello CodeSandbox&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;Start editing to see some magic happen!&amp;lt;/h2&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setIsDivShow((prev) =&amp;gt; !prev)}&amp;gt;버튼&amp;lt;/button&amp;gt;
      {isDivShow &amp;amp;&amp;amp; &amp;lt;Div /&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isMount 변수를 만들어서 clean up 할 때 false로 변경을 하고, setState는 isMount가 true일 때만 실행을 하게 하면, 메모리 누수를 방지 할 수 있습니다. 하지만 fetch를 날리는 로직마다 isMount 변수를 선언하는 것은 어썸하지 못한것 같습니다.&amp;nbsp;이를 해결하기 위해 custom hook을 사용해서 코드를 수정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useIsMount.js&lt;/p&gt;
&lt;pre id=&quot;code_1626065199733&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useRef } from &quot;react&quot;;

export default function useIsMount() {
  let isMount = useRef(false);

  useEffect(() =&amp;gt; {
    isMount.current = true;

    return () =&amp;gt; {
      isMount.current = false;
    };
  }, []);

  return isMount;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app.js&lt;/p&gt;
&lt;pre id=&quot;code_1626065217192&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from &quot;react&quot;;
import useIsMount from &quot;./useIsMount&quot;;
import &quot;./styles.css&quot;;

const fakeFetch = () =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(&quot;fetch done&quot;);
      resolve(Math.random());
    }, 2000);
  });
};

const Div = () =&amp;gt; {
  const [state, setState] = useState(0);
  const isMount = useIsMount();

  useEffect(() =&amp;gt; {
    (async function foo() {
      try {
        const val = await fakeFetch();

        if (isMount.current) {
          setState(val);
        }
      } catch (error) {
        console.log(&quot;error&quot;, error);
        setState(error.message);
      }
    })();
  }, [isMount]);

  return &amp;lt;div style={{ backgroundColor: &quot;red&quot; }}&amp;gt;{state}&amp;lt;/div&amp;gt;;
};

export default function App() {
  const [isDivShow, setIsDivShow] = useState(false);

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;h1&amp;gt;Hello CodeSandbox&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;Start editing to see some magic happen!&amp;lt;/h2&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setIsDivShow((prev) =&amp;gt; !prev)}&amp;gt;버튼&amp;lt;/button&amp;gt;
      {isDivShow &amp;amp;&amp;amp; &amp;lt;Div /&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/trigger-setstate-kyqr8?file=/src/App.js:0-1076&quot;&gt;https://codesandbox.io/s/trigger-setstate-kyqr8?file=/src/App.js:0-1076&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1626065244953&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;trigger setState - CodeSandbox&quot; data-og-description=&quot;trigger setState by 냉정 using react, react-dom, react-scripts&quot; data-og-host=&quot;codesandbox.io&quot; data-og-source-url=&quot;https://codesandbox.io/s/trigger-setstate-kyqr8?file=/src/App.js:0-1076&quot; data-og-url=&quot;https://codesandbox.io/s/kyqr8&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/trigger-setstate-kyqr8?file=/src/App.js:0-1076&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codesandbox.io/s/trigger-setstate-kyqr8?file=/src/App.js:0-1076&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;trigger setState - CodeSandbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;trigger setState by 냉정 using react, react-dom, react-scripts&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;codesandbox.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&amp;nbsp; 2 :&amp;nbsp; cancelable promise&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 개념과 컨셉은 비슷합니다. 위 방법과 다른 점은 fetch가 resolve나 reject 되기 전에 트리거를 하는 것 입니다. 이 로직을 수행하기 위해서는 promise함수를 cancelable 하게 감싸주어야 합니다. 아래 코드에서는 wrappedPromise 함수가 그 역할을 하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626073598256&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from &quot;react&quot;;
import &quot;./styles.css&quot;;

const makeCancelable = (promise) =&amp;gt; {
  let hasCanceled = false;

  const wrappedPromise = new Promise((resolve, reject) =&amp;gt; {
    promise.then(
      (val) =&amp;gt; !hasCanceled &amp;amp;&amp;amp; resolve(val),
      (error) =&amp;gt; reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      console.log(&quot;cancel&quot;);
      hasCanceled = true;
    }
  };
};

const fakeFetch = () =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(&quot;fetch done&quot;);
      resolve(Math.random());
    }, 2000);
  });
};

const Div = () =&amp;gt; {
  const [state, setState] = useState(0);

  useEffect(() =&amp;gt; {
    let cancelable;

    (async function foo() {
      try {
        cancelable = makeCancelable(fakeFetch());
        const val = await cancelable.promise;
        setState(val);
      } catch (error) {
        console.log(&quot;error&quot;, error);
        setState(error.message);
      }
    })();

    return () =&amp;gt; {
      cancelable.cancel();
    };
  }, []);

  return &amp;lt;div style={{ backgroundColor: &quot;red&quot; }}&amp;gt;{state}&amp;lt;/div&amp;gt;;
};

export default function App() {
  const [isDivShow, setIsDivShow] = useState(false);

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;h1&amp;gt;Hello CodeSandbox&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;Start editing to see some magic happen!&amp;lt;/h2&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setIsDivShow((prev) =&amp;gt; !prev)}&amp;gt;버튼&amp;lt;/button&amp;gt;
      {isDivShow &amp;amp;&amp;amp; &amp;lt;Div /&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;promise에 익숙하더라도 약간 혼란스러울 수 있습니다. 중요한 것은 promise가 pandding 상태 일 때, warppedPromise 함수가 return 하는 객체의 cancel 함수를 실행 시키면 초기에 실행한 fakeFetch가 resolve되어도 wrappedPromise함수에서 체이닝한&amp;nbsp; promise로 인해서 resolve와 reject를 아무것도 실행하지 않기 때문에 계속 pandding 상태로 남아있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Div 컴포넌트가 unmount 될 때 cancelable.cancel()을 실행하게 되면, await cancelable.promise 이후의 로직은 실행이 되지 않아서 에러를 해결 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/kaenseulreobeul-dnqmb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://codesandbox.io/s/kaenseulreobeul-dnqmb&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1626075496573&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;캔슬러블 - CodeSandbox&quot; data-og-description=&quot;캔슬러블 by 냉정 using react, react-dom, react-scripts&quot; data-og-host=&quot;codesandbox.io&quot; data-og-source-url=&quot;https://codesandbox.io/s/kaenseulreobeul-dnqmb&quot; data-og-url=&quot;https://codesandbox.io/s/dnqmb&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/kaenseulreobeul-dnqmb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codesandbox.io/s/kaenseulreobeul-dnqmb&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;캔슬러블 - CodeSandbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;캔슬러블 by 냉정 using react, react-dom, react-scripts&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;codesandbox.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 만약에 fetch가 resolve되고, setState가 실행되기 전에 unmount를 할 경우에는 어떨게 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 상황에서는 역시 warning메세지가 나타나게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1626075575768&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from &quot;react&quot;;
import &quot;./styles.css&quot;;

const makeCancelable = (promise) =&amp;gt; {
  let hasCanceled = false;

  const wrappedPromise = new Promise((resolve, reject) =&amp;gt; {
    promise.then(
      (val) =&amp;gt; !hasCanceled &amp;amp;&amp;amp; resolve(val),
      (error) =&amp;gt; reject(error)
    );
  });

  return {
    wrappedPromise,
    cancel() {
      console.log(&quot;cancel&quot;);
      hasCanceled = true;
    }
  };
};

const fakeFetch = () =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(&quot;fetch done&quot;);
      resolve(Math.random());
    }, 0);
  });
};

const delay = () =&amp;gt; {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(&quot;프로미스 이 후, setState 이전&quot;);
      resolve();
    }, 2000);
  });
};

const Div = () =&amp;gt; {
  const [state, setState] = useState(0);

  useEffect(() =&amp;gt; {
    let cancelable;

    (async function foo() {
      try {
        cancelable = makeCancelable(fakeFetch());
        const val = await cancelable.promise;

        await delay();

        setState(() =&amp;gt; {
          console.log(&quot;setState&quot;);
          return val;
        });
      } catch (error) {
        console.log(&quot;error&quot;, error);
        setState(error.message);
      }
    })();

    return () =&amp;gt; {
      cancelable.cancel();
    };
  }, []);

  return &amp;lt;div style={{ backgroundColor: &quot;red&quot; }}&amp;gt;{state}&amp;lt;/div&amp;gt;;
};

export default function App() {
  const [isDivShow, setIsDivShow] = useState(false);

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;h1&amp;gt;Hello CodeSandbox&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;Start editing to see some magic happen!&amp;lt;/h2&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setIsDivShow((prev) =&amp;gt; !prev)}&amp;gt;버튼&amp;lt;/button&amp;gt;
      {isDivShow &amp;amp;&amp;amp; &amp;lt;Div /&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/kaenseulreobeul-esji-keiseu-b5nre?file=/src/App.js:0-1695&quot;&gt;https://codesandbox.io/s/kaenseulreobeul-esji-keiseu-b5nre?file=/src/App.js:0-1695&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1626075634068&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;캔슬러블 엣지 케이스 - CodeSandbox&quot; data-og-description=&quot;캔슬러블 엣지 케이스 by 냉정 using react, react-dom, react-scripts&quot; data-og-host=&quot;codesandbox.io&quot; data-og-source-url=&quot;https://codesandbox.io/s/kaenseulreobeul-esji-keiseu-b5nre?file=/src/App.js:0-1695&quot; data-og-url=&quot;https://codesandbox.io/s/b5nre&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/kaenseulreobeul-esji-keiseu-b5nre?file=/src/App.js:0-1695&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codesandbox.io/s/kaenseulreobeul-esji-keiseu-b5nre?file=/src/App.js:0-1695&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;캔슬러블 엣지 케이스 - CodeSandbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;캔슬러블 엣지 케이스 by 냉정 using react, react-dom, react-scripts&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;codesandbox.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch가 끝난 이후에 setState는 매우 빠른 속도로 실행될 것이기 때문에 위의 엣지케이스 타이밍에 unmount가 된다는 것은 정말 일어나기 힘들 것입니다. 따라서 fetch를 핸들링 하는 것으로도 warning 메세지가 해결 된다고 생각합니다. 위의 테스트로 warning 메세지의 원인이 비동기에 남아있는 setState 때문임을 좀 더 명확하게 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법 3 : abort controller&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;abortcontroller는 fetch 요청을 취소할 수 있는 웹 api입니다. 사용방법은 mdn 문서를 참고하면 금방 적용할 수 있을 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;abort controller를 사용할 때 주의 할 점은 abort를 할 경우 reject로 반환 된다는 것입니다.따라서 에러 핸들링 부분에 setState가 있다면 당연히 warning 메세지가 나타날 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/AbortController&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.mozilla.org/ko/docs/Web/API/AbortController&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1626075758719&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;AbortController - Web API | MDN&quot; data-og-description=&quot;AbortController&amp;nbsp;인터페이스는 하나 이상의 웹 요청을 취소할 수 있게 해준다.&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/AbortController&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/AbortController&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/baXpPR/hyKRHHcehP/KsS1mxgNbYFF7CQUt9vNSk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/AbortController&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/AbortController&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/baXpPR/hyKRHHcehP/KsS1mxgNbYFF7CQUt9vNSk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;AbortController - Web API | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AbortController&amp;nbsp;인터페이스는 하나 이상의 웹 요청을 취소할 수 있게 해준다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/190</guid>
      <comments>https://jcon.tistory.com/190#entry190comment</comments>
      <pubDate>Mon, 12 Jul 2021 16:56:04 +0900</pubDate>
    </item>
    <item>
      <title>callback과 promise의 차이점</title>
      <link>https://jcon.tistory.com/189</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sample.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn2Kr4/btq62XwzIM7/UR1WlBnHtU641aNK6WHe5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn2Kr4/btq62XwzIM7/UR1WlBnHtU641aNK6WHe5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn2Kr4/btq62XwzIM7/UR1WlBnHtU641aNK6WHe5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn2Kr4%2Fbtq62XwzIM7%2FUR1WlBnHtU641aNK6WHe5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;sample.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 비동기를 처리할 때 callback과 promise를 사용하게 되는데 이 둘의 차이점을 그저 callback을 썼을 때는 callback hell 때문에 가독성이 떨어지는 것 말고 더 명확하게 알기 위해서 정리를 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. callback을 사용한 비동기 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 비동기가 처리되는 방식은 아래 영상으로 대체.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/8aGhZQkoFbQ&quot;&gt;https://youtu.be/8aGhZQkoFbQ&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=8aGhZQkoFbQ&amp;amp;feature=youtu.be&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/czo4dp/hyKxBl7Iiw/5wNXWeWQw9KbC1H0PY36o1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=468_180_550_270&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/8aGhZQkoFbQ&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre id=&quot;code_1623398160291&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function async(callback) {
  setTimeout(() =&amp;gt; {
    callback(&quot;1초 후 실행&quot;);
  }, 1000);
}

async(function (msg) {
  console.log(msg);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async 함수는 첫번째 인자로 callback을 받고, callback이 실행 될 때 첫 번째 인자를 console에 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callback이 실행되는 시점은 async함수 안에 있는 setTimeout으로 인해서 1초 후에 실행이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callback이 실행이 되면서 첫 번째 인자로 &quot;1초 후 실행&quot;이 들어가고, 콘솔에 &quot;1초 후 실행&quot;이 나타납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 callback을 통해서 어떠한 비동기 로직이 끝났을 때 callback 함수를 실행시킴으로써 비동기 작업이 완료 되었을 때 callback에서 작성한 어떠한 행동을 실행할 수 있게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. promise를 사용한 비동기 처리&lt;/h2&gt;
&lt;pre id=&quot;code_1623399380837&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function async() {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve(&quot;1초 후 실행&quot;);
    }, 1000);
  })
}

async().then(res =&amp;gt; {
  console.log(res);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;promise를 사용하게 되면 resolve는 성공했을 때, reject는 에러가 발생 했을 때 첫 번째 인자로 어떠한 값을 넘길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resolve는 .then의 첫 번째 인자로, reject는 .catch의 첫 번째 인자로 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async 함수를 실행하면 1초 후 첫 번째 인자에 &quot;1초 후 실행&quot;이 들어간 resolve가 실행이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인자는 .then의 첫번째 인자는 함수이고, 이 함수의 첫 번째 인자로 받아서 콘솔에 찍히게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. callback과 promise의 차이점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callback을 사용하면 비동기 로직의 결과값을 처리하기 위해서는 callback안에서만 처리를 해야하고, 콜백 밖에서는 비동기에서 온 값을 알 수가 없습니다. 하지만 promise를 사용하면 비동기에에서 온 값이 promise 객체에 저장되기 때문에 코드 작성이 용이해집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1623417157234&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function async(callback) {
  var result;
    
  setTimeout(() =&amp;gt; {
    result = callback(&quot;결과값&quot;);
  },1000);
    
  return result;
}

var b = async((res) =&amp;gt; {
  return res;
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 async 함수에서 비동기가 끝난 후의 값을 가지고 싶어서 위처럼 코드를 썼다면, b를 1초 후, 100초 후에 찍어도 undefined만 나오게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1623417962248&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var p = new Promise((res, rej) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      res(&quot;a&quot;)
    }, 1000)
    
})

var result = p.then(res =&amp;gt; {
  return res;
})

console.log(result) // Promise&amp;nbsp;{&amp;lt;fulfilled&amp;gt;: &quot;a&quot;}

result.then(res =&amp;gt; {
    console.log(res); // &quot;a&quot;
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로미스를 사용하게 되면 프로미스 객체에 비동기가 처리된 결과값이 저장됩니다. 콜백의 경우 매번 비동기를 실행해야지 그 값을 사용(?)할 수 있지만 프로미스는 .then 메소드를 통해서 저장되어 있는 값을 원하는 때에 사용할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre id=&quot;code_1623413850759&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function async(result, callback) {
  setTimeout(() =&amp;gt; {
    callback(result, function (result) {
      console.log(result);
    });
  }, 1000);
}

async(0, function (res, callback) {
  callback(res)
  async(res + 1, function (res, callback) {
    callback(res)
    async(res + 1, function (res, callback) {
      callback(res)
    });
  });
});

// 0
// 1
// 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 비동기 로직의 결과를 다음 비동기로 전달해서 실행해야 할 때 callback은 점점 깊어져서 가독성이 매우 안좋아지고, 코드를 작성할 때도 힘들어집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1623415254222&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function async(result) {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(result);
      resolve(result);
    }, 1000)
  });
}

async(0).then(res =&amp;gt; {
  return async(res + 1);
}).then(res =&amp;gt; {
  return async(res + 1);
})

// 0
// 1
// 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로미스를 사용할 경우 .then을 통해 코드의 깊이가 깊어지지 않고, 이해하기도 쉽습니다.&lt;/p&gt;</description>
      <category>JavaScript</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/189</guid>
      <comments>https://jcon.tistory.com/189#entry189comment</comments>
      <pubDate>Fri, 11 Jun 2021 23:02:55 +0900</pubDate>
    </item>
    <item>
      <title>object의 Enumerable 설정하는 방법</title>
      <link>https://jcon.tistory.com/188</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1623302883383&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Object.defineProperty() - JavaScript | MDN&quot; data-og-description=&quot;Object.defineProperty() 정적 메서드는 객체에 직접 새로운 속성을 정의하거나 이미 존재하는 속성을 수정한 후, 그 객체를 반환합니다.&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HHp86/hyKvDffX0U/FLnz618G2Eqai4UeBqKlhk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HHp86/hyKvDffX0U/FLnz618G2Eqai4UeBqKlhk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Object.defineProperty() - JavaScript | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Object.defineProperty() 정적 메서드는 객체에 직접 새로운 속성을 정의하거나 이미 존재하는 속성을 수정한 후, 그 객체를 반환합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object 등의 prototype을 수정하는 것은 왠만하면 피해야하지만 만약 메소드를 추가해야 한다면 해당 메소드가 &lt;span style=&quot;color: #212121;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/for...in&quot;&gt;for...in&lt;/a&gt;&lt;span style=&quot;color: #212121;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;루프나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys&quot;&gt;Object.keys()&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;(en-US)&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #212121;&quot;&gt;에서 노&lt;/span&gt;출 되지 않도록 숨기는 것이 좋은 습관이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;151&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWNDNy/btq6V43TTsM/ft5NrMAgcWGQh1h0JHq5Y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWNDNy/btq6V43TTsM/ft5NrMAgcWGQh1h0JHq5Y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWNDNy/btq6V43TTsM/ft5NrMAgcWGQh1h0JHq5Y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWNDNy%2Fbtq6V43TTsM%2Fft5NrMAgcWGQh1h0JHq5Y1%2Fimg.png&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;151&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제 처럼 String.prototype에 a라는 메소드를 추가하고 String.prototype을 확인하면 a가 진한 색으로 되어 있는 것을 확인 할 수 있다.&amp;nbsp;이를 Object.defineProperty를 사용하여 열거가 되지 않도록 변경한다.&lt;/p&gt;
&lt;pre id=&quot;code_1623303679943&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Object.defineProperty(String.prototype, &quot;isAnagram&quot;, { enumerable: false });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;159&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J7ulg/btq6Z84ZTWK/1qUvTkcqsn2m7vxvcTiW8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J7ulg/btq6Z84ZTWK/1qUvTkcqsn2m7vxvcTiW8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J7ulg/btq6Z84ZTWK/1qUvTkcqsn2m7vxvcTiW8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ7ulg%2Fbtq6Z84ZTWK%2F1qUvTkcqsn2m7vxvcTiW8K%2Fimg.png&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;159&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>JavaScript</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/188</guid>
      <comments>https://jcon.tistory.com/188#entry188comment</comments>
      <pubDate>Thu, 10 Jun 2021 14:43:15 +0900</pubDate>
    </item>
    <item>
      <title>폴더 안의 파일을 모두 import 하는 방법</title>
      <link>https://jcon.tistory.com/187</link>
      <description>&lt;p&gt;개발 요구사항 중에 config 파일을 입력하면, 메뉴가 자동으로 생성되는 기능이 있었습니다.&lt;/p&gt;
&lt;p&gt;react router dom의 Router 안에 Link 메뉴가 생성되는 것은 map 메서드를 통해 생성이 가능하지만 Switch 컴포넌트는 수작업을 해주어야 해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 컴포넌트 파일이 없을 경우에는 404페이지가 자동으로 뜨게하고 싶어서 찾아보았습니다. 이런 특수한 사항이 아니라면 역시 권장하는 방법은 아닐것 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1613466536207&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Pages = importAll(require.context(&quot;./pages&quot;));

function importAll(r) {
  let files = {};
  r.keys().map((item, index) =&amp;gt; {
    files[item.replace(&quot;./&quot;, &quot;&quot;)] = r(item);
  });
  return files;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 함수를 통해 pages폴더 안에있는 모든 파일을 Pages라는 변수에 담습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1613466696066&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Switch&amp;gt;
	&amp;lt;Route exact path=&quot;/&quot;&amp;gt;
		{Pages[&quot;Home&quot;] ? Pages[&quot;Home&quot;].default : Pages[&quot;NotFound&quot;].default }
	&amp;lt;/Route&amp;gt;
	&amp;lt;Route&amp;gt;
		{Pages[&quot;NotFound&quot;].default}
	&amp;lt;/Route&amp;gt;
 &amp;lt;/Switch&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이 Pages 변수안에 파일 이름으로 module이 담기게 되고,&amp;nbsp; 모듈안의 default 값을 넣어주면 됩니다.&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/187</guid>
      <comments>https://jcon.tistory.com/187#entry187comment</comments>
      <pubDate>Tue, 16 Feb 2021 18:13:37 +0900</pubDate>
    </item>
    <item>
      <title>react 컴포넌트에 websocket 적용하기</title>
      <link>https://jcon.tistory.com/186</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;sample.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nI58t/btqWx7bKy4s/EYKrIqjsC1qqEIkz5uGeY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nI58t/btqWx7bKy4s/EYKrIqjsC1qqEIkz5uGeY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nI58t/btqWx7bKy4s/EYKrIqjsC1qqEIkz5uGeY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnI58t%2FbtqWx7bKy4s%2FEYKrIqjsC1qqEIkz5uGeY1%2Fimg.png&quot; data-filename=&quot;sample.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;코드 ver 0.2&lt;/p&gt;
&lt;pre id=&quot;code_1612850949975&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect, useState, useRef } from &quot;react&quot;;

const SocketTest = () =&amp;gt; {
  const [socketConnected, setSocketConnected] = useState(false);
  const [sendMsg, setSendMsg] = useState(false);
  const [items, setItems] = useState([]);

  const webSocketUrl = `ws://websocket.com`;
  let ws = useRef(null);

  // 소켓 객체 생성
  useEffect(() =&amp;gt; {
    if (!ws.current) {
      ws.current = new WebSocket(webSocketUrl);
      ws.current.onopen = () =&amp;gt; {
        console.log(&quot;connected to &quot; + webSocketUrl);
        setSocketConnected(true);
      };
      ws.current.onclose = (error) =&amp;gt; {
        console.log(&quot;disconnect from &quot; + webSocketUrl);
        console.log(error);
      };
      ws.current.onerror = (error) =&amp;gt; {
        console.log(&quot;connection error &quot; + webSocketUrl);
        console.log(error);
      };
      ws.current.onmessage = (evt) =&amp;gt; {
        const data = JSON.parse(evt.data);
        console.log(data);
        setItems((prevItems) =&amp;gt; [...prevItems, data]);
      };
    }

    return () =&amp;gt; {
      console.log(&quot;clean up&quot;);
      ws.current.close();
    };
  }, []);

  // 소켓이 연결되었을 시에 send 메소드
  useEffect(() =&amp;gt; {
    if (socketConnected) {
      ws.current.send(
        JSON.stringify({
          message: sendMessage,
        })
      );

      setSendMsg(true);
    }
  }, [socketConnected]);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div&amp;gt;socket&amp;lt;/div&amp;gt;
      &amp;lt;div&amp;gt;socket connected : {`${socketConnected}`}&amp;lt;/div&amp;gt;
      &amp;lt;div&amp;gt;res : &amp;lt;/div&amp;gt;
      &amp;lt;div&amp;gt;
        {items.map((item) =&amp;gt; {
          return &amp;lt;div&amp;gt;{JSON.stringify(item)}&amp;lt;/div&amp;gt;;
        })}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default SocketTest;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;onmessage 메서드는 send 후 데이터가 올 때 트리거가 되므로 웹소켓 객체 정의 안으로 이동했습니다. send 메소드는 따로 빼지 않고 onopen 함수 안에 넣어도 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmGrfW/btqWsIDHUp5/KRLgmAScvTSVLZFpk7QWb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmGrfW/btqWsIDHUp5/KRLgmAScvTSVLZFpk7QWb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmGrfW/btqWsIDHUp5/KRLgmAScvTSVLZFpk7QWb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmGrfW%2FbtqWsIDHUp5%2FKRLgmAScvTSVLZFpk7QWb1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;코드 ver 0.1&lt;/p&gt;
&lt;pre id=&quot;code_1612847977499&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect, useState, useRef } from &quot;react&quot;;

const SocketTest = () =&amp;gt; {
  const [socketConnected, setSocketConnected] = useState(false);
  const [sendMsg, setSendMsg] = useState(false);
  const [items, setItems] = useState([]);

  const webSocketUrl = `ws://websocket.com`;
  let ws = useRef(null);

  // 소켓 객체 생성
  useEffect(() =&amp;gt; {
    if (!ws.current) {
      ws.current = new WebSocket(webSocketUrl);
      ws.current.onopen = () =&amp;gt; {
        console.log(&quot;connected to &quot; + webSocketUrl);
        setSocketConnected(true);
      };
      ws.current.onclose = (error) =&amp;gt; {
        console.log(&quot;disconnect from &quot; + webSocketUrl);
        console.log(error);
      };
      ws.current.onerror = (error) =&amp;gt; {
        console.log(&quot;connection error &quot; + webSocketUrl);
        console.log(error);
      };
    }

    return () =&amp;gt; {
      console.log(&quot;clean up&quot;);
      ws.current.close();
    };
  }, []);

  // 소켓이 연결되었을 시에 send 메소드
  useEffect(() =&amp;gt; {
    if (socketConnected) {
      ws.current.send(
        JSON.stringify({
          message: sendMessage,
        })
      );

      setSendMsg(true);
    }
  }, [socketConnected]);

  // send 후에 onmessage로 데이터 가져오기
  useEffect(() =&amp;gt; {
    if (sendMsg) {
      ws.current.onmessage = (evt) =&amp;gt; {
        const data = JSON.parse(evt.data);
        console.log(data);
        setItems((prevItems) =&amp;gt; [...prevItems, data]);
      };
    }
  }, [sendMsg]);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div&amp;gt;socket&amp;lt;/div&amp;gt;
      &amp;lt;div&amp;gt;socket connected : {`${socketConnected}`}&amp;lt;/div&amp;gt;
      &amp;lt;div&amp;gt;res : &amp;lt;/div&amp;gt;
      &amp;lt;div&amp;gt;
        {items.map((item) =&amp;gt; {
          return &amp;lt;div&amp;gt;{JSON.stringify(item)}&amp;lt;/div&amp;gt;;
        })}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default SocketTest;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코드를 짜면서 고민했던 점은 컴포넌트가 unmount 되었을 때 socket 연결이 끊어지기를 바랬습니다. 여러 시행 착오 끝에 소켓 객체를 생성하는 useEffect에 clean up을 하는 것이 원하는대로 동작하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;소켓 객체를 컴포넌트 밖에 전역으로 생성하기 보다는 컴포넌트에 ref를 통해 저장이 되도록 하고, 소켓 객체의 send 메소드와 onmessage 메소드가 순차적으로 실행되기를 바래서 위와 같이 작성했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/186</guid>
      <comments>https://jcon.tistory.com/186#entry186comment</comments>
      <pubDate>Tue, 9 Feb 2021 14:28:29 +0900</pubDate>
    </item>
    <item>
      <title>한 컴퓨터에서 github 계정 두개 이상 연동하기</title>
      <link>https://jcon.tistory.com/185</link>
      <description>&lt;pre id=&quot;code_1610527186493&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Personal account-userA
Host github.com-userA
	HostName github.com
	User git
   	IdentityFile ~/.ssh/id_rsa_userA
    
# Personal account-userB
Host github.com-userB
	HostName github.com
	User git
   	IdentityFile ~/.ssh/id_rsa_userB​&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1610527212477&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	# Personal account-userA 
    Host github.com-userA
		HostName github.com
		User git
       	IdentityFile ~/.ssh/id_rsa_userA
    
    # Personal account-userB
    Host github.com-userB
		HostName github.com
		User git
      	IdentityFile ~/.ssh/id_rsa_userB&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1610526953878&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; ~/.ssh/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vscode를 사용하여 github를 연동할 경우 window의 자격 증명에 하나의 github계정만 연동이 됩니다. 이러다 보니 a계정을 연동을 시켜놓았을 때, b계정의 repo에서 작업을 한 후 commit하면 a계정으로 commit이 된 것으로 나타납니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 b계정의 repo가 private로 되어 있다면 b계정으로 다시 연동을 하지 않으면 push도 되지 않습니다. 이러한 번거로운 작업을 해결 하기 위해서는 ssh를 설정하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. ssh-key 생성하기&lt;/h2&gt;
&lt;p&gt;터미널에 아래 명령어를 입력하여 ~/.ssh/ 폴더로 이동한 후 ssh-key를 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1610526960525&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; ~/.ssh/&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1610527000374&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ssh-keygen -t rsa -C &quot;userA@my_email.com&quot; -f &quot;id_rsa_userA&quot;
$ ssh-keygen -t rsa -C &quot;userB@my_email.com&quot; -f &quot;id_rsa_userB&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞에 &quot;userA@my_email.com&quot;와 &quot;userB@my_email.com&quot;는 사용할 github 계정을 입력합니다. &quot;&quot;id_rsa_userA&quot;와 &quot;id_rsa_userB&quot;는 ssh의 파일 이름입니다. 본인에게 편한 이름을 입력하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ssh-key 적용하기&lt;/h2&gt;
&lt;p&gt;~/.ssh/config에 아래와 같이 입력합니다. config파일이 없다면 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1610527219175&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Personal account-userA 
Host github.com-userA
	HostName github.com
	User git
   	IdentityFile ~/.ssh/id_rsa_userA
    
# Personal account-userB
Host github.com-userB
	HostName github.com
	User git
   	IdentityFile ~/.ssh/id_rsa_userB&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Host에 적혀있는 github.com 뒤에 -userA -userB는 추후에 github repo에 연결할 url이름입니다. 원하시는 이름을 적으시면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ssh-key의 config 설정이 완료된 후에 github를 clone이나 ssh를 연결할 폴더에 .gitconfig 파일 생성한 후 아래와 같이 &lt;span style=&quot;color: #333333;&quot;&gt;폴더에 연결할 github계정을 적습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1610527475636&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[user]
	email = userA@my_email.com&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. ssh-key 깃허브에 적용하기&lt;/h2&gt;
&lt;p&gt;~/.ssh/ 폴더에 가면 .pub 확장자 파일 안에 ssh-key가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1610527793220&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh-rsa AAAAB3NzaC1yc ... = userA@my_email.com&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이것을 모두 복사 한 후 github setting으로 갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/onAFL/btqTvQwGpIu/IMS3mRdkHKt2Y7jWSpd4sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/onAFL/btqTvQwGpIu/IMS3mRdkHKt2Y7jWSpd4sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/onAFL/btqTvQwGpIu/IMS3mRdkHKt2Y7jWSpd4sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FonAFL%2FbtqTvQwGpIu%2FIMS3mRdkHKt2Y7jWSpd4sk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;아래 탭에서 new ssh key를 눌러 ssh key를 등록합니다. title은 편하신 이름으로 설정한 후 key부분에 복사한 ssh-key를 입력하면 됩니다. 완료가 되면 아래와 같이 ssh key가 연동됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crfSZj/btqTwV5AUQR/HGc2BuhrmXZPGhFTHuTqJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crfSZj/btqTwV5AUQR/HGc2BuhrmXZPGhFTHuTqJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crfSZj/btqTwV5AUQR/HGc2BuhrmXZPGhFTHuTqJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrfSZj%2FbtqTwV5AUQR%2FHGc2BuhrmXZPGhFTHuTqJ0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이 후 로컬의 폴더로 가서 아래 명령어를 입력합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1610528128429&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ssh-add ~/.ssh/id_rsa_userA&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 &lt;b&gt;Could not open a connection to your authentication agent&amp;nbsp;&lt;/b&gt;에러가 뜬다면 아래 명령어를 입력 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1610528167541&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ eval $(ssh-agent)
$ ssh-add ~/.ssh/id_rsa_userA&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. clone 또는 계정 연동하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/df3bnz/btqTrRXc9Nn/FTnGOIjaBpk3o7RH6Mz8UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/df3bnz/btqTrRXc9Nn/FTnGOIjaBpk3o7RH6Mz8UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/df3bnz/btqTrRXc9Nn/FTnGOIjaBpk3o7RH6Mz8UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdf3bnz%2FbtqTrRXc9Nn%2FFTnGOIjaBpk3o7RH6Mz8UK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;ssh clone 주소는 git@github.com:github계정/repo이름.git로 되어 있을 것 입니다. 여기에 ~/.ssh/config에서 작성했던 Host를 추가해야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1610528413452&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ~/.ssh/config
Host github.com-userA
...

// ssh 주소
git@github.com-userA:github계정/repo이름.git &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 ssh 주소를 clone할 때 사용하면 됩니다. 만약 이미 clone한 repo라면 아래 명령어를 입력하여 업데이트 하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1610528633883&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git remote set-url origin git@github.com-userA:github계정/repo이름.git &lt;/code&gt;&lt;/pre&gt;</description>
      <category>Git</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/185</guid>
      <comments>https://jcon.tistory.com/185#entry185comment</comments>
      <pubDate>Wed, 13 Jan 2021 18:04:31 +0900</pubDate>
    </item>
    <item>
      <title>redux 강의 기본 핵심 튜토리얼 5탄 - redux data 사용하기</title>
      <link>https://jcon.tistory.com/184</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;sample (28).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oh1tf/btqNCfu8YnD/Gf7gwn9waG61k6z5ZwQcWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oh1tf/btqNCfu8YnD/Gf7gwn9waG61k6z5ZwQcWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oh1tf/btqNCfu8YnD/Gf7gwn9waG61k6z5ZwQcWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foh1tf%2FbtqNCfu8YnD%2FGf7gwn9waG61k6z5ZwQcWK%2Fimg.png&quot; data-filename=&quot;sample (28).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단일 게시물 나타내기&lt;/h2&gt;
&lt;p&gt;Redux 스토어에 새 게시물을 추가 할 수있는 기능이 있으므로 게시물 데이터를 다양한 방식으로 사용하는 몇 가지 기능을 추가 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;현재 게시물 항목이 기본 피드 페이지에 표시되고 있지만 텍스트가 너무 길 경우 콘텐츠의 일부만 표시됩니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;자체 페이지에서 단일 게시물 항목을 볼 수있는 기능이 있으면 도움이 될 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일 게시물 페이지 만들기&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;먼저 게시물 기능 폴더에 새 SinglePostPage 구성 요소를 추가해야합니다. 페이지 URL이 /posts/123과 같은 경우 React Router를 사용하여이 구성 요소를 표시합니다. 여기서 123 부분은 표시하려는 게시물의 ID 여야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1606376245851&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/posts/SinglePostPage.js
import React from 'react'
import { useSelector } from 'react-redux'

export const SinglePostPage = ({ match }) =&amp;gt; {
  const { postId } = match.params

  const post = useSelector(state =&amp;gt;
    state.posts.find(post =&amp;gt; post.id === postId)
  )

  if (!post) {
    return (
      &amp;lt;section&amp;gt;
        &amp;lt;h2&amp;gt;Post not found!&amp;lt;/h2&amp;gt;
      &amp;lt;/section&amp;gt;
    )
  }

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;article className=&quot;post&quot;&amp;gt;
        &amp;lt;h2&amp;gt;{post.title}&amp;lt;/h2&amp;gt;
        &amp;lt;p className=&quot;post-content&quot;&amp;gt;{post.content}&amp;lt;/p&amp;gt;
      &amp;lt;/article&amp;gt;
    &amp;lt;/section&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;React Router는 &lt;span style=&quot;color: #333333;&quot;&gt;prop으로&lt;/span&gt; URL 정보가 있는 match 객체를 전달할 것입니다. 이 컴포넌트를 렌더링하는 경로를 설정할 때 URL의 두 번째 부분을 &lt;span style=&quot;color: #333333;&quot;&gt;match.params에서 &lt;/span&gt;postId라는 변수로 해당 값을 읽을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;postId 값이 있으면 selector 함수를 사용하여 Redux 저장소에서 게시물 개체를 찾을 수 있습니다. state.posts는 모든 포스트 객체의 배열이어야한다는 것을 알고 있으므로 Array.find() 함수를 사용하여 해당하는 ID를 가진 게시물을 반환 할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;useSelector에서 반환 된 값이 변경 될 때마다 컴포넌트가 다시 렌더링된다는 점에 유의해야합니다. 컴포턴트는 항상 store에서 필요한 데이터를 선택해야합니다. 이렇게하면 실제로 필요할 때만 렌더링하는 데 도움이됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;스토어에 일치하는 게시물 항목이 없을 수 있습니다. 사용자가 URL을 직접 입력하려고했거나 올바른 데이터를 로드하지 않았을 수 있습니다. 이 경우 find() 함수는 실제 포스트 객체 대신 undefined를 반환합니다. 컴포넌트는 이를 확인하고 &quot;포스트를 찾을 수 없습니다!&quot;를 표시하여 처리해야합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;저장소에 올바른 게시물 개체가 있다고 가정하면 useSelector가 반환하고 이를 사용하여 페이지에서 게시물의 제목과 콘텐츠를 렌더링 할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;이것이 &amp;lt;PostsList&amp;gt; 컴포넌트의 본문에 있는 로직과 상당히 유사하다는 것을 알 수 있을 것입니다. 여기에서 전체 posts 배열을 반복하여 기본 피드의 게시물을 표시합니다. 두 곳 모두에서 사용할 수있는 Post 컴포넌트를 추출해 볼 수 있지만, 포스트에서 필요한 부분을 표시하는 것과 전체 포스트를 표시하는 방법에는 약간의 차이가 있습니다. 일반적으로 약간의 중복이 있더라도 우선 별도로 작성하는 것이 좋습니다. 그런 다음 나중에 코드의 다른 섹션이 재사용 가능한 컴포넌트를 추출 할 수 있을지 나중에 결정할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;단일 게시물 route 추가&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;이제 &amp;lt;SinglePostPage&amp;gt; 컴포넌트가 있으므로 이를 표시 할 경로를 정의하고 첫 페이지 피드의 각 게시물에 대한 링크를 추가 할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;App.js에서 SinglePostPage를 가져오고 경로를 추가합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1606381331636&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.js
import { PostsList } from './features/posts/PostsList'
import { AddPostForm } from './features/posts/AddPostForm'
import { SinglePostPage } from './features/posts/SinglePostPage'

function App() {
  return (
    &amp;lt;Router&amp;gt;
      &amp;lt;Navbar /&amp;gt;
      &amp;lt;div className=&quot;App&quot;&amp;gt;
        &amp;lt;Switch&amp;gt;
          &amp;lt;Route
            exact
            path=&quot;/&quot;
            render={() =&amp;gt; (
              &amp;lt;React.Fragment&amp;gt;
                &amp;lt;AddPostForm /&amp;gt;
                &amp;lt;PostsList /&amp;gt;
              &amp;lt;/React.Fragment&amp;gt;
            )}
          /&amp;gt;
          &amp;lt;Route exact path=&quot;/posts/:postId&quot; component={SinglePostPage} /&amp;gt;
          &amp;lt;Redirect to=&quot;/&quot; /&amp;gt;
        &amp;lt;/Switch&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/Router&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;그런 다음 &amp;lt;PostsList&amp;gt;에서 해당 특정 게시물로 라우팅되는 &amp;lt;Link&amp;gt;를 포함하도록 수정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1606381623295&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/posts/PostsList.js
import React from 'react'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'

export const PostsList = () =&amp;gt; {
  const posts = useSelector(state =&amp;gt; state.posts)

  const renderedPosts = posts.map(post =&amp;gt; (
    &amp;lt;article className=&quot;post-excerpt&quot; key={post.id}&amp;gt;
      &amp;lt;h3&amp;gt;{post.title}&amp;lt;/h3&amp;gt;
      &amp;lt;p className=&quot;post-content&quot;&amp;gt;{post.content.substring(0, 100)}&amp;lt;/p&amp;gt;
      &amp;lt;Link to={`/posts/${post.id}`} className=&quot;button muted-button&quot;&amp;gt;
        View Post
      &amp;lt;/Link&amp;gt;
    &amp;lt;/article&amp;gt;
  ))

  return (
    &amp;lt;section className=&quot;posts-list&quot;&amp;gt;
      &amp;lt;h2&amp;gt;Posts&amp;lt;/h2&amp;gt;
      {renderedPosts}
    &amp;lt;/section&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;이제 클릭을 통해 다른 페이지로 이동할 수 있으므로 &amp;lt;Navbar&amp;gt; 컴포넌트에 기본 게시물 페이지로 돌아가는 링크도 만들어 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1606381801511&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/Navbar.js
import React from 'react'

import { Link } from 'react-router-dom'

export const Navbar = () =&amp;gt; {
  return (
    &amp;lt;nav&amp;gt;
      &amp;lt;section&amp;gt;
        &amp;lt;h1&amp;gt;Redux Essentials Example&amp;lt;/h1&amp;gt;

        &amp;lt;div className=&quot;navContent&quot;&amp;gt;
          &amp;lt;div className=&quot;navLinks&quot;&amp;gt;
            &amp;lt;Link to=&quot;/&quot;&amp;gt;Posts&amp;lt;/Link&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/section&amp;gt;
    &amp;lt;/nav&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;게시물 편집&lt;/h2&gt;
&lt;p&gt;기존 게시물의 id를 가져와서 store에서 해당 게시물을 읽은 후 제목과 내용을 수정하여 업데이트 할 수 있는 &amp;lt;EditPostForm&amp;gt; 컴포넌트를 만들어 보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;게시물 업데이트&lt;/h3&gt;
&lt;p&gt;우선 postsSlice store가 실제로 게시물을 업데이트하는 방법을 알 수 있도록 새로운 reducer 함수와 액션을 추가해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;createSlice() 내에서 reducers 객체에 새 함수를 추가해야 합니다. 이 reducer의 이름은 어떠한 이벤트가 일어나는지 알 수 있도록 작성하는것이 중요합니다. reducer 이름은 이 액션이 전달 될 때마다 redux devtools에서 액션 타입의 일부로 표시되기 때문입니다. 첫번째 reducer는 postAdded라고 했기 때문에 이번에 새로 만들 함수는 postUpdated라고 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;게시물을 업데이트하려면 다음 사항을 알아야합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;state에서 올바른 게시물 객체를 찾을 수 있도록 업데이트할 게시물의 ID&lt;/li&gt;
&lt;li&gt;사용자가 입력한 새로운 제목과 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;redux 액션 객체에는 type 필드가 있어야하며, 발생한 이벤트에 대한 세부 정보를 얻기 위해 또 다른 필드가 포함될 수 있습니다. 일반적으로 action.payload에 추가정보를 넣고, payload 필드에 &lt;span style=&quot;color: #333333;&quot;&gt;문자열, 숫자, 객체, 배열 등을 사용해여 추가 할 수 있습니다. 게시물 업데이트의 경우 세가지 정보가 더 필요하프로 payload 필드 내부에 넣어서 액션 객체는 &lt;span&gt;{type: 'posts/postUpdated', payload: {id, title, content}} 와 같이 생성하겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;기본적으로 createSlice에 의해 생성 된 액션 생성자는 하나의 인수를 받을 수 있습니다. 액션 생성자에 인자를 넣게 되면 해당 인자는 action.payload 필드로 액션 개체에 추가됩니다. 따라서 id, title, content의 값이 포함된 객체를 postUpdated 액션 생성자에게 인수로 전달할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;또한 reducer는 액션이 전달 될 때 state가 어떻게 업데이트되어야 하는지 결정해야합니다. 이를 생각해서 reducer가 ID를 기반으로 올바른 게시물 객체를 찾고 해당 게시물의 text 및 content 필드를 업데이트 해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;마지막으로 사용자가 게시물을 저장할 때 UI가 새로운 postUpdated 액션을 전달할 수 있도록 createSlice가 생성 한 액션 생성기 함수를 내보내야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;코드는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1607501629464&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/posts/postsSlice.js
const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postAdded(state, action) {
      state.push(action.payload)
    },
    postUpdated(state, action) {
      const { id, title, content } = action.payload
      const existingPost = state.find(post =&amp;gt; post.id === id)
      if (existingPost) {
        existingPost.title = title
        existingPost.content = content
      }
    }
  }
})

export const { postAdded, postUpdated } = postsSlice.actions

export default postsSlice.reducer&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이어서 게시물 수정 양식 생성하기를 배우겠습니다&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/184</guid>
      <comments>https://jcon.tistory.com/184#entry184comment</comments>
      <pubDate>Mon, 16 Nov 2020 14:53:35 +0900</pubDate>
    </item>
    <item>
      <title>redux 강의 기본 핵심 튜토리얼 4탄 - redux data flow</title>
      <link>https://jcon.tistory.com/182</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;sample (25).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yQVeA/btqMBvOm1GI/AbdYWWQPlQnCZQ8kgOFL4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yQVeA/btqMBvOm1GI/AbdYWWQPlQnCZQ8kgOFL4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yQVeA/btqMBvOm1GI/AbdYWWQPlQnCZQ8kgOFL4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyQVeA%2FbtqMBvOm1GI%2FAbdYWWQPlQnCZQ8kgOFL4K%2Fimg.png&quot; data-filename=&quot;sample (25).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이번 파트에서는 createSlice를 사용해서 reducer로직의 slice를 store에 추가하는 방법과 useSelectore, UseDispatch 사용밥법에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 설정&lt;/h2&gt;
&lt;p&gt;react와 redux가 설정되어 있고, 기본적인 css와 API 요청을 가능하게하는 fake REST API가 있는 프로젝트 예제입니다. 이를 토대로 배워보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코드는 CodeSandbox를 통해 확인할 수 있고, fork를 해서 진행 할 수 있습니다. &lt;span style=&quot;color: #333333;&quot;&gt;또는 github 저장소에서 clone을 해도 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605150532925&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;reduxjs/redux-essentials-example-app - CodeSandbox&quot; data-og-description=&quot;reduxjs/redux-essentials-example-app using @reduxjs/toolkit, @testing-library/jest-dom, @testing-library/react, @testing-library/user-event, classnames, date-fns, faker, miragejs, react&quot; data-og-host=&quot;codesandbox.io&quot; data-og-source-url=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed&quot; data-og-url=&quot;https://codesandbox.io/s/qklud&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HVHXZ/hyIdhqM2t1/AtzO0D98INYJXF5bqYk67k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HVHXZ/hyIdhqM2t1/AtzO0D98INYJXF5bqYk67k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;reduxjs/redux-essentials-example-app - CodeSandbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;reduxjs/redux-essentials-example-app using @reduxjs/toolkit, @testing-library/jest-dom, @testing-library/react, @testing-library/user-event, classnames, date-fns, faker, miragejs, react&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;codesandbox.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/reduxjs/redux-essentials-example-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github.com/reduxjs/redux-essentials-example-app&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605150562948&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;reduxjs/redux-essentials-example-app&quot; data-og-description=&quot;Example app for the Redux Essentials tutorial. Contribute to reduxjs/redux-essentials-example-app development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reduxjs/redux-essentials-example-app&quot; data-og-url=&quot;https://github.com/reduxjs/redux-essentials-example-app&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dWIf9Z/hyIdjPG3xI/TCUkitm80pOeGw9y1tZZi1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://github.com/reduxjs/redux-essentials-example-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/reduxjs/redux-essentials-example-app&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dWIf9Z/hyIdjPG3xI/TCUkitm80pOeGw9y1tZZi1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;reduxjs/redux-essentials-example-app&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Example app for the Redux Essentials tutorial. Contribute to reduxjs/redux-essentials-example-app development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;완성된 모습은 아래 링크를 통해 확인 할 수 있습니다. github의 경우 tutorial-steps 브랜치에서 확인 할 수 있습니다&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605150767552&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;reduxjs/redux-essentials-example-app - CodeSandbox&quot; data-og-description=&quot;reduxjs/redux-essentials-example-app using @reduxjs/toolkit, @testing-library/jest-dom, @testing-library/react, @testing-library/user-event, classnames, date-fns, faker, miragejs, react&quot; data-og-host=&quot;codesandbox.io&quot; data-og-source-url=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps&quot; data-og-url=&quot;https://codesandbox.io/s/gvopn&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gpvbR/hyIdbjNlSg/xSJ6jJT8k3tyK7YZTykMT0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gpvbR/hyIdbjNlSg/xSJ6jJT8k3tyK7YZTykMT0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;reduxjs/redux-essentials-example-app - CodeSandbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;reduxjs/redux-essentials-example-app using @reduxjs/toolkit, @testing-library/jest-dom, @testing-library/react, @testing-library/user-event, classnames, date-fns, faker, miragejs, react&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;codesandbox.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로운 Redux + React 프로젝트를 만들고 싶다면?&lt;/h3&gt;
&lt;p&gt;이번 튜토리얼을 마치면 자신의 프로젝트를 생성하고 싶을 것 입니다. 새로운 Redux + React 프로젝트를 생성하는 가장 빠른 방법은 &lt;a href=&quot;https://github.com/reduxjs/cra-template-redux&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Create-React-App에서 제공하는 Redux 템플릿&lt;/a&gt;을 사용하는 것이 좋습니다. 이전 장에서 배웠던 카운터 예제가 작성되어 있는 코드입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 확인&lt;/h3&gt;
&lt;p&gt;첫부분에 말했던 프로젝트의 구조를 간단히 살펴 보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/src
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;index.js: 애플리케이션의 진입점 파일입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;React-Redux&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;Provider&amp;gt;컴포넌트와 메인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;App&amp;gt;컴포넌트를&lt;span&gt;&amp;nbsp;&lt;/span&gt;렌더링합니다.&lt;/li&gt;
&lt;li&gt;App.js: 주요 응용 프로그램 구성 요소입니다.&lt;span&gt; top navbar를&lt;/span&gt; 렌더링하고 다른 콘텐츠에 대한 client-side 라우팅을 처리합니다.&lt;/li&gt;
&lt;li&gt;index.css: 애플리케이션에 사용되는 css 파일입니다.&lt;/li&gt;
&lt;li&gt;/api
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;client.js: GET 및 POST 요청을 할 수있게 하는 AJAX 요청 파일 입니다.&lt;/li&gt;
&lt;li&gt;server.js: 데이터에 대한 fake REST API를 제공합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;우리 앱은 나중에 fake 엔드 포인트에서 데이터를 가져옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;/app
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;Navbar.js: 상단 헤더 및 nav 콘텐츠를 렌더링합니다.&lt;/li&gt;
&lt;li&gt;store.js: Redux 스토어 인스턴스 생성합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지금 앱을 보면 헤더와 welcome메세지를 볼 수 있습니다. Redux DevTools Extension에서 초기 Redux state를 보면 비어있는 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메인 게시물 피드&lt;/h2&gt;
&lt;p&gt;이 앱에서 만들 주요기능은 게시물 목록입니다. 첫번째 목표는 화면에 게시물 목록을 표시하는 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;게시물 slice 만들기&lt;/h3&gt;
&lt;p&gt;첫번째 단계는 게시물에 대한 데이터를 가지고 있는 redux slice를 만드는 것 입니다. redux store에 해당 데이터가 있으면 페이지에 해당 데이터를 표시하는 react 컴포넌트를 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;src 폴더 안에 features 폴더를 생성하고, 그 안에 post 폴더를 생성한 후 postsSlice.js 파일을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;redux toolkit의 createSlice함수를 사용하여 게시물 데이터를 처리하는 방법을 아는 reducer를 만들겠습니다. reducer에는 앱이 시작될 때 redux store에 해당 값이 로드 되도록 초기 데이터가 있어야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금은 화면에 UI가 생길 수 있도록 내부에 fake 데이터로 게시물 객체가 있는 배열을 만들겠습니다. createSlice를 import하고, 초기 posts 배열을 정의한 후 createSlice에 인자로 전달합니다. 그 후 createSlice로 생성된 posts reducer를 export 합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1605157241510&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features / posts / postsSlice.js

import { createSlice } from '@reduxjs/toolkit'

const initialState = [
  { id: '1', title: 'First Post!', content: 'Hello!' },
  { id: '2', title: 'Second Post', content: 'More text' }
]

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {}
})

export default postsSlice.reducer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;새로운 slice를 만들 때마다 redux store에 reducer를 추가 해야 합니다. 현재 redux store가 있지만 내부 데이터가 없습니다. app/store.js 파일에서 postsReducer를 import한 다음 configureStore를 가져와서 posts라는 이름의 postsReducer를 업데이트 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1605158478966&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/store.js

import { configureStore } from '@reduxjs/toolkit'

import postsReducer from '../features/posts/postsSlice'

export default configureStore({
  reducer: {
    posts: postsReducer
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 코드는 redux의 최상위 state에 posts라는 필드가 있고, state.posts의 모든 데이터는 action이 전달 될 때 postsReducer에 의해서 업데이트 되는 것을 뜻합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;여기까지 한 후 Redux DevTools Extension에서 현재 state를 확인 해 봅시다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0S0VR/btqNcFXkqBb/rURAyUjKhkOV5ndIA2m8BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0S0VR/btqNcFXkqBb/rURAyUjKhkOV5ndIA2m8BK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0S0VR/btqNcFXkqBb/rURAyUjKhkOV5ndIA2m8BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0S0VR%2FbtqNcFXkqBb%2FrURAyUjKhkOV5ndIA2m8BK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;게시물 목록 노출&lt;/h3&gt;
&lt;p&gt;이제 store에 몇 가지 게시물 데이터가 있으므로 게시물 목록을 표시하는 react 콤포넌트를 만들겠습니다. 피드 게시물 기능과 관련된 모든 코드는 posts 폴더에 있어야하므로 여기에 PostsList.js 파일을 만드세요.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;게시물 목록을 렌더링하려면 데이터를 가져와야합니다. react 컴포넌트는 react-redux 라이브러리의 hooks 중 useSelector를 사용하여 redux store에서 데이터를 읽을 수 있습니다. &quot;selector function&quot;은 전체 redux state를 매개 변수로 사용하여 호출되며 컴포넌트가 필요한 특정 데이터를 store에서 반환해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;초기 PostList 컴포넌트는 Redux store에서 state.posts 값을 읽은 다음 게시물 배열을 반복하여 화면에 각각 표시합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1605165383288&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/posts/PostsList.js

import React from 'react'
import { useSelector } from 'react-redux'

export const PostsList = () =&amp;gt; {
  const posts = useSelector(state =&amp;gt; state.posts)

  const renderedPosts = posts.map(post =&amp;gt; (
    &amp;lt;article className=&quot;post-excerpt&quot; key={post.id}&amp;gt;
      &amp;lt;h3&amp;gt;{post.title}&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;{post.content.substring(0, 100)}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  ))

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h2&amp;gt;Posts&amp;lt;/h2&amp;gt;
      {renderedPosts}
    &amp;lt;/section&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 &quot;welcome&quot;메시지 대신 PostList 컴포넌트가 나타나도록 App.js 컴포넌트의 라우팅을 수정합니다. App.js 파일에 PostsList파일을 import하고, welcome 메시지를 &amp;lt;PostsList /&amp;gt;로 변경합니다. 다른 컴포넌트도 추가할 예정이므로 React Fragment로 감싸줍니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1605166185226&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.js

import React from 'react'
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect
} from 'react-router-dom'

import { Navbar } from './app/Navbar'

import { PostsList } from './features/posts/PostsList'

function App() {
  return (
    &amp;lt;Router&amp;gt;
      &amp;lt;Navbar /&amp;gt;
      &amp;lt;div className=&quot;App&quot;&amp;gt;
        &amp;lt;Switch&amp;gt;
          &amp;lt;Route
            exact
            path=&quot;/&quot;
            render={() =&amp;gt; (
              &amp;lt;React.Fragment&amp;gt;
                &amp;lt;PostsList /&amp;gt;
              &amp;lt;/React.Fragment&amp;gt;
            )}
          /&amp;gt;
          &amp;lt;Redirect to=&quot;/&quot; /&amp;gt;
        &amp;lt;/Switch&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/Router&amp;gt;
  )
}

export default App&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 작성하면 아래와 같은 화면을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FeKXF/btqNeH1JWtb/s4QMkF6eHAgQTgCxgP2JX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FeKXF/btqNeH1JWtb/s4QMkF6eHAgQTgCxgP2JX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FeKXF/btqNeH1JWtb/s4QMkF6eHAgQTgCxgP2JX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFeKXF%2FbtqNeH1JWtb%2Fs4QMkF6eHAgQTgCxgP2JX1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로운 포스트 추가하기&lt;/h3&gt;
&lt;p&gt;게시물을 작성하고 저장할 수 있는 새로운 게시물 추가 form을 만들어 보겠습니다. 먼저 빈 form을 만들어서 페이지에 추가합니다. 그 후 form을 redux store에 연결하여 &quot;게시물 저장&quot;버튼을 클릭 할 때 새로운 게시물이 추가되도록합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;새 게시물 양식 추가&lt;/h4&gt;
&lt;p&gt;posts폴더에 AddPostForm.js 파일을 생성합니다. 게시물 제목에 대한 text input과 게시물 본문에 대한 text area를 추가합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1605167666819&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/posts/AddPostForm.js

import React, { useState } from 'react'

export const AddPostForm = () =&amp;gt; {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')

  const onTitleChanged = e =&amp;gt; setTitle(e.target.value)
  const onContentChanged = e =&amp;gt; setContent(e.target.value)

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h2&amp;gt;Add a New Post&amp;lt;/h2&amp;gt;
      &amp;lt;form&amp;gt;
        &amp;lt;label htmlFor=&quot;postTitle&quot;&amp;gt;Post Title:&amp;lt;/label&amp;gt;
        &amp;lt;input
          type=&quot;text&quot;
          id=&quot;postTitle&quot;
          name=&quot;postTitle&quot;
          value={title}
          onChange={onTitleChanged}
        /&amp;gt;
        &amp;lt;label htmlFor=&quot;postContent&quot;&amp;gt;Content:&amp;lt;/label&amp;gt;
        &amp;lt;textarea
          id=&quot;postContent&quot;
          name=&quot;postContent&quot;
          value={content}
          onChange={onContentChanged}
        /&amp;gt;
        &amp;lt;button type=&quot;button&quot;&amp;gt;Save Post&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/section&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;App.js파일에 컴포넌트를 import 한 후 &amp;lt;PostsList /&amp;gt; 컴포넌트 위에 작성해서 헤더 아래에 form이 나타나게 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1605167951619&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//App.js

&amp;lt;Route
  exact
  path=&quot;/&quot;
  render={() =&amp;gt; (
    &amp;lt;React.Fragment&amp;gt;
      &amp;lt;AddPostForm /&amp;gt;
      &amp;lt;PostsList /&amp;gt;
    &amp;lt;/React.Fragment&amp;gt;
  )}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;게시물 저장하기&lt;/h4&gt;
&lt;p&gt;Redux store에 새로운 게시물을 추가하기 위해 posts slice를 수정합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;posts slice는 게시물 데이터에 대한 모든 업데이트를 처리합니다. createSlice 내부에는 reducers라는 객체가 있습니다. 지금은 비어있기 때문에 게시물을 추가시킬 수 있는 reducer function을 추가해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;reducer 안에 postAdded라는 function을 추가하면 현재 state 값과 전달 되는 action을 인수로 받습니다. posts slice는 자신이 담당하는 데이터에 대해서만 알고있기 때문에 state는 전체 Redux state가 아니라 posts state이어야합니다. 여기에서는 posts 배열이 들어갑니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;action 객체의 action.payload 필드를 통해 새로운 post 객체를 전달할 것이고, 이 객체를 post 배열에 추가할 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;postAdded reducer를 작성할 때 createSlice는 동일한 이름의 action 생성기를 자동으로 만들어 줍니다. action 생성기를 export하고 UI 컴포넌트에서 사용하여 사용자가 &quot;게시물 저장&quot; 버튼을 클릭하면 action을 전달할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1605168844590&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/posts/postsSlice.js

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postAdded(state, action) {
      state.push(action.payload)
    }
  }
})

export const { postAdded } = postsSlice.actions

export default postsSlice.reducer&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&quot;Post Added&quot; action 전달하기&lt;/h4&gt;
&lt;p&gt;AddPostForm에는 text input과 &quot;게시물 저장&quot;버튼이 있지만 이벤트를 등록하지 않았습니다. postAdded action 생성자를 dispatch해서 사용자가 작성한 제목과 콘텐츠가 포함 된 객체를 전달할 수 있게 해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;게시물 객체는 id 필드를 가지고 있어야합니다. 현재 초기 테스트 게시물은 fake 데이터로 임의로 id를 지정해 주었습니다. 다음에 들어갈 id가 무엇인지 알아내는 코드를 작성할 수 있지만, 고유의 id를 생성하는 것이 더 좋습니다. Redux toolkit에는 이런 기능을 하는 nanoid 함수가 있습니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ID 생성과 action dispatch에 대한 자세히 설명은 다음장에서 하겠습니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;컴포넌트에서 action을 전달하려면 store의 dispatch 함수를 가지고 와야합니다. react-redux에서 useDispatch hook를 통해서 가지고 올 수 있습니다. 또한 postAdded action 생성자를 이 파일에 import해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;컴포넌트에서 dispatch 함수를 사용할 수 있게되면 dispatch( postAdded() )를 호출 할 수 있습니다. React 컴포넌트 useState hook에서 제목 및 콘텐츠를 가져온 후 새 id를 생성해서 새로운 게시물 객체를 생성한 후 이를 postAdded()에 전달합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1605171053016&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/posts/AddPostForm

import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { nanoid } from '@reduxjs/toolkit'

import { postAdded } from './postsSlice'

export const AddPostForm = () =&amp;gt; {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')

  const dispatch = useDispatch()

  const onTitleChanged = e =&amp;gt; setTitle(e.target.value)
  const onContentChanged = e =&amp;gt; setContent(e.target.value)

  const onSavePostClicked = () =&amp;gt; {
    if (title &amp;amp;&amp;amp; content) {
      dispatch(
        postAdded({
          id: nanoid(),
          title,
          content
        })
      )

      setTitle('')
      setContent('')
    }
  }

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h2&amp;gt;Add a New Post&amp;lt;/h2&amp;gt;
      &amp;lt;form&amp;gt;
        {/* omit form inputs */}
        &amp;lt;button type=&quot;button&quot; onClick={onSavePostClicked}&amp;gt;
          Save Post
        &amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/section&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 제목과 텍스트를 입력하고 &quot;게시물 저장&quot;을 클릭하세요. 해당 게시물이 게시물 목록에 표시되어야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;첫 번째로 작동하는 react + redux 앱을 만들었습니다&lt;/h4&gt;
&lt;p&gt;이번에 배운 내용을 통해 redux의 데이터가 어떻게 움직이는지 알 수 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시물 목록은 useSelector를 사용하여 store의 초기 게시물 목록을 읽고 초기 UI를 렌더링 합니다&lt;/li&gt;
&lt;li&gt;새로운 게시물 데이터 객체를 postAdded action을 통해 dispatch 합니다.&lt;/li&gt;
&lt;li&gt;posts reducer는 postAdded action을 확인하고, 새로운 게시물 데이터를 가지고 posts배열을 업데이트합니다.&lt;/li&gt;
&lt;li&gt;redux store는 UI에 일부 데이터가 변경 되었다고 알립니다.&lt;/li&gt;
&lt;li&gt;게시물 목록은 업데이트 된 게시물 배열 데이터를 읽고 새로운 게시물을 표시하기 위해 다시 렌더링합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&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;redux state는 reduxer에 의해서 업데이트됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;reducers는 기존의 state값을 복사하고 새 데이터로 복사본을 수정한 후 새로운 state로 반환합니다&lt;/li&gt;
&lt;li&gt;redu toolkit에서 createSlice는 slice reducer를 생성하고 immer 라이브러리를 통해 불변성을 지키면서 업데이트 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;slice reducer는 configureStore 안에 reducer 필드로 추가되고, redux store 안에 데이터 및 state 필드 이름을 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;react 컴포넌트는 useSelector hook을 사용하여 store의 데이터를 읽습니다
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;selector 함수는 전체 state를 받고 값을 반환해야합니다&lt;/li&gt;
&lt;li&gt;selecorts는 redux store가 업데이트 될 때마다 다시 실행되며, 반환하는 데어터가 변경되면 컴포넌트가 다시 렌더링 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;react 컴포넌트는 useDispatch hook을 사용해서 actions을 전달하고 store를 업데이트 합니다
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;createSlice는 reducer에 있는 action들에 대한 action 생성자 함수를 생성합니다&lt;/li&gt;
&lt;li&gt;컴포넌트에서 action을 전달할 때는 dispatch(someActionCreator())를 호출합니다&lt;/li&gt;
&lt;li&gt;reducer가 실행 될 때 해당하는 action이 있는지 확인하고, 있을 경우 새로운 state를 반환합니다&lt;/li&gt;
&lt;li&gt;form input 값과 같은 임시 데이터는 react 컴포넌트 안에 state로 생성하는 것이 좋습니다. 사용자가 form을 모두 작성한 후에 store를 업데이트하는 action을 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 장에서는 몇가지 기능을 더 추가하고 이미 store에 있는 데이터가 어떻게 동작하는지에 대해 알아보겠습니다&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/182</guid>
      <comments>https://jcon.tistory.com/182#entry182comment</comments>
      <pubDate>Wed, 11 Nov 2020 17:55:23 +0900</pubDate>
    </item>
    <item>
      <title>redux 강의 기본 핵심 튜토리얼 3탄 - redux app 구조</title>
      <link>https://jcon.tistory.com/181</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;sample (24).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FeV2L/btqMr42IzN3/KPvsK4jeep7s5zJjXzm5VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FeV2L/btqMr42IzN3/KPvsK4jeep7s5zJjXzm5VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FeV2L/btqMr42IzN3/KPvsK4jeep7s5zJjXzm5VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFeV2L%2FbtqMr42IzN3%2FKPvsK4jeep7s5zJjXzm5VK%2Fimg.png&quot; data-filename=&quot;sample (24).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이전 포스트에서 redux의 용어와 개념에 대해서 설명했습니다. 이번 포스트에서는 실제 코드 예제를 살펴보고 어떻게 동작이 되는지 살펴 보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;카운터 앱 예제&lt;/h2&gt;
&lt;p&gt;살펴볼 샘플 프로젝트는 버튼을 클릭 할 때 숫자를 더하거나 뺄 수 있는 작은 카운터 앱입니다. 간단하지만 react + redux 애플리케이션이 어떻게 동작하는지 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 샘플 프로젝트는 cra의 공식 redux템플릿(&lt;a href=&quot;https://github.com/reduxjs/cra-template-redux&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github.com/reduxjs/cra-template-redux&lt;/a&gt;)을 사용했습니다. redux-toolkit을 사용하여 redux store 및 로직을 만들고 react-redux(&lt;a href=&quot;https://react-redux.js.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-redux.js.org/&lt;/a&gt;)를 사용하여 redux store와 react 컴포넌트를 함께 연결 하는 표준 redux 구조로 구성되었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예제 코드 : &lt;a href=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-counter-example/tree/master/?from-embed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;codesandbox.io/s/github/reduxjs/redux-essentials-counter-example/tree/master/?from-embed&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1604377019726&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;reduxjs/redux-essentials-counter-example - CodeSandbox&quot; data-og-description=&quot;reduxjs/redux-essentials-counter-example using @reduxjs/toolkit, @testing-library/jest-dom, @testing-library/react, @testing-library/user-event, react, react-dom, react-redux, react-scripts&quot; data-og-host=&quot;codesandbox.io&quot; data-og-source-url=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-counter-example/tree/master/?from-embed&quot; data-og-url=&quot;https://codesandbox.io/s/un8my&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vM6il/hyH7jO4uRt/lFKB1Y3k8Owp8utf7fP6vk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-counter-example/tree/master/?from-embed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codesandbox.io/s/github/reduxjs/redux-essentials-counter-example/tree/master/?from-embed&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vM6il/hyH7jO4uRt/lFKB1Y3k8Owp8utf7fP6vk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;reduxjs/redux-essentials-counter-example - CodeSandbox&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;reduxjs/redux-essentials-counter-example using @reduxjs/toolkit, @testing-library/jest-dom, @testing-library/react, @testing-library/user-event, react, react-dom, react-redux, react-scripts&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;codesandbox.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;로컬에서 프로젝트를 만들고 싶다면 아래와 같이 만들수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1604377414716&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx create-react-app redux-essentials-example --template redux&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카운터 앱 기능 및 DevTools&lt;/h3&gt;
&lt;p&gt;예제로 사용한 카운터 앱은 내부에서 일어나는 일을 볼 수 있도록 설정되어 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;브라우저에서 DevTools를 연 후 redux 탭을 선택하고 오른쪽 상단에서 State를 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/whkIj/btqMqeFcIz1/UCXOPSTzp0isFA22GZOcik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/whkIj/btqMqeFcIz1/UCXOPSTzp0isFA22GZOcik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/whkIj/btqMqeFcIz1/UCXOPSTzp0isFA22GZOcik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwhkIj%2FbtqMqeFcIz1%2FUCXOPSTzp0isFA22GZOcik%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;오른쪽 화면에서 redux store가 다음과 같은 상태 값으로 시작하는 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1604378719219&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  counter: {
    value: 0
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DevTools는 앱을 사용 할 때 store안에 state가 어떻게 변경되는지 보여 줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;앱을 실행 시킨 상태에서 &quot;+&quot;버튼을 클릭한 다음 redux devTools에서 Diff 탭을 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddP6vl/btqMqB72xxg/9kNKOzkm1STrhkfAKzaGh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddP6vl/btqMqB72xxg/9kNKOzkm1STrhkfAKzaGh0/img.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; style=&quot;width: 34.8163%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddP6vl/btqMqB72xxg/9kNKOzkm1STrhkfAKzaGh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddP6vl%2FbtqMqB72xxg%2F9kNKOzkm1STrhkfAKzaGh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;0&quot; height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LaAgh/btqMpMoIiRr/YTB1e4LzkXo5qIEY7OBJDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LaAgh/btqMpMoIiRr/YTB1e4LzkXo5qIEY7OBJDK/img.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; style=&quot;width: 64.0209%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LaAgh/btqMpMoIiRr/YTB1e4LzkXo5qIEY7OBJDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLaAgh%2FbtqMpMoIiRr%2FYTB1e4LzkXo5qIEY7OBJDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;0&quot; height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;여기서 중요한 두 가지를 확인 할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;+&quot;버튼을 클릭했을 때 &quot;counter/increment&quot; type을 가진 action 객체가 store로 전달(dispatch)됩니다.&lt;/li&gt;
&lt;li&gt;해당 action이 전달되었을 때 state.counter.value가 0에서 1로 변경되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음을 실행해 보세요&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;+&quot;버튼을 다시 클릭하세요. 표시되는 값은 2가 됩니다&lt;/li&gt;
&lt;li&gt;&quot;-&quot;버튼을 클릭해보세요. 표시되는 값은 1이 됩니다&lt;/li&gt;
&lt;li&gt;&quot;add mount&quot;를 클릭해보세요 표시되는 값은 3이 됩니다&lt;/li&gt;
&lt;li&gt;input 박스의 숫자를 2에서 3으로 변경해보세요&lt;/li&gt;
&lt;li&gt;&quot;add async&quot;버튼을 클릭해보세요. 진행률 표시가 완료되면 값이 6으로 변경됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;react DevTools를 살펴보겠습니다. 버튼을 클릭할 때마다 action이 전달이 되서 5개의 action을 확인 할 수 있습니다.(좌)&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&quot;counter/incrementByAmount&quot;의 왼쪽 목록에서 action탭을 선택합니다.(우)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zzXHb/btqMM4BxuLV/RE53zHCWR7dvg3H5EfM0w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zzXHb/btqMM4BxuLV/RE53zHCWR7dvg3H5EfM0w0/img.png&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;273&quot; data-filename=&quot;blob&quot; style=&quot;width: 48.4447%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zzXHb/btqMM4BxuLV/RE53zHCWR7dvg3H5EfM0w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzzXHb%2FbtqMM4BxuLV%2FRE53zHCWR7dvg3H5EfM0w0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WLOev/btqMIto1l0G/Wifn8ZqUiFZeNSsfApGJV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WLOev/btqMIto1l0G/Wifn8ZqUiFZeNSsfApGJV0/img.png&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;265&quot; data-filename=&quot;blob&quot; style=&quot;width: 50.3925%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WLOev/btqMIto1l0G/Wifn8ZqUiFZeNSsfApGJV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWLOev%2FbtqMIto1l0G%2FWifn8ZqUiFZeNSsfApGJV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;action객체가 다음과 같은 것을 볼 수 있습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604381147655&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  type: 'counter/incrementByAmount',
  payload: 3
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Diff 탭을 클릭하면 해당 action에 대한 응답으로 state.counter.value가 3에서 6으로 변경된 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;앱 내부에서 어떤 이벤트가 어떤 순서로 발생했는지 확인 할 수 있는 것은 디버그 할 때 매우 유용합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 외에도 DevTools에는 디버그 하는데 도움이 되는 몇 가지 추가 명령과 옵션이 있습니다. 오른쪽 상단의 'Trace'탭을 클릭해 보세요. action이 store에 도달 했을 때 어떤 코드들이 실행되었는지 스택으로 나타납니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxdzX4/btqMmo24G6I/HbTpPU6BBvZ3tQZK77iaTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxdzX4/btqMmo24G6I/HbTpPU6BBvZ3tQZK77iaTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxdzX4/btqMmo24G6I/HbTpPU6BBvZ3tQZK77iaTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxdzX4%2FbtqMmo24G6I%2FHbTpPU6BBvZ3tQZK77iaTk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이렇게 코드의 어떤 부분이 어떤 action을 전달했는지 쉽게 추적 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애플리케이션 작동 방식&lt;/h2&gt;
&lt;p&gt;이번에는 앱이 어떻게 작동하는지 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;이 애플리케이션을 구성하는 주요 파일은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/src
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;index.js: 앱의 시작점&lt;/li&gt;
&lt;li&gt;App.js: 최상위 React 컴포넌트&lt;/li&gt;
&lt;li&gt;/app
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;store.js: Redux 스토어 인스턴스 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;/features
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;/counter
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;Counter.js: 카운터 기능에 대한 UI를 보여주는 React 컴포넌트&lt;/li&gt;
&lt;li&gt;counterSlice.js: 카운터 기능에 대한 Redux 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;redux store 생성&lt;/h3&gt;
&lt;p&gt;app/store.js는 다음과 같습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604381985442&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;redux store는 redux toolkit의 configureStore를 사용하여 생성됩니다. configureStore에 reducer를 전달합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;애플리케이션은 다양한 기능이 있을 수 있으며 각 기능 별로 reducer가 있을 수 있습니다. 이때 모든 reducer를 configureStore에 넣어 호출 할 수 있습니다. reducer객체 안의 key이름은 최종 state 값의 키값을 정의합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;features/counter/counterSlice.js 파일에는 카운터 로직에 대한 reducer를 내보내는 코드가 있습니다. 여기에서 counterReducer를 가지고 와서 store를 만들 때 포함 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;{ counter: counterReducer } 객체를 전달하면 redux state에 state.counter가 만들어지고, action이 dispatch 될 때마다 state.counter가 어떻게 업데이트 되는지 counterReducer에서 결정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;redux를 사용할 때 다양한 플러그인(미들웨어 등)으로 store를 설정할 수 있습니다. configureStore는 여러 미들웨어가 설정되어 있고, 그 중 redux DevTools Extension를 통해 디버깅 할 수 있도록 설정되어 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redux Slice&lt;/h4&gt;
&lt;p&gt;slice는 일반적으로 앱에서 하나의 기능을 수행하는 action들과 redux reducer의 로직에 대해 모아놓은 하나의 파일입니다. root redux state를 여러 state로 분리하기 때문에 slice라고 부릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 블로깅 앱이 있다면 다음과 같이 할 수 있습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604386710424&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'

export default configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    comments: commentsReducer
  }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;state.users, state.posts, state.comments는 redux state의 slice 입니다. usersReducer는 state.users slice의 업데이트를 담당합니다. 이를 slice reducer 함수라고 합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;reducer와 state 구조&lt;/h4&gt;
&lt;p&gt;redux store는 생성 될 때 root reducer를 전달해야만 합니다. 그러면 서로 다른 slice reducer 함수가 여러 개 있는 경우 어떻게 하나의 root reducer를 전달하고, &lt;span style=&quot;color: #333333;&quot;&gt;redux store state를 정의&lt;/span&gt;&amp;nbsp;할 수 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;모든 slice reducer를 호출하면 다음과 같습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604388713530&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function rootReducer(state = {}, action) {
  return {
    users: usersReducer(state.users, action),
    posts: postsReducer(state.posts, action),
    comments: commentsReducer(state.comments, action)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;slice reducer(usersReducer, postsReducer, commentsReducer)에 맞는 redux state(state.users, state.posts, state.comments)를 &lt;span style=&quot;color: #333333;&quot;&gt;전달합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;redux는 combineReducers함수를 사용하면 위와 같은 작업을 자동으로 해줍니다. slice reducer가 들어가있는 객체를 combineReducers에 인자로 넣으면 action이 전달될 때마다 각 slice reducer를 호출하는 함수를 반환합니다. 각 slice reducer의 결과는 최종적으로 하나의 객체로 결합 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위의 코드를 conbineReducers를 사용하면 아래와 같습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604390240409&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer,
  comments: commentsReducer
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;combineReducers를 사용하여 생성한 root reducer를 configureStore에 직접 전달 할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1604390582184&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const store = configureStore({
  reducer: rootReducer
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Slice Reducers와 Actions 생성하기&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;features/counter/counterSlice.js 파일에는 있는 &lt;span style=&quot;color: #333333;&quot;&gt;counteReducer와 해당 파일을 알아보겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604391458171&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state =&amp;gt; {
      // Redux Toolkit은 내무적으로 immer 라이브러리를 사용하고 있습니다.
      // 따라서 아래와 같이 state.value를 직접 변경하여도
      // immer로 인해 불변성을 지킬 수 있습니다
      state.value += 1
    },
    decrement: state =&amp;gt; {
      state.value -= 1
    },
    incrementByAmount: (state, action) =&amp;gt; {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;앞서 UI에서 다른 버튼을 클릭하면 세 가지 redux action type이 전달 되는 것을 확인 했습니다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;{ type: &quot;counter/increment&quot; }&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;{ type: &quot;counter/decrement&quot; }&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;{ type: &quot;counter/incrementByAmount&quot; }&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;action은 type이라는 키,밸류가 있는 객체이고, type의 값은 항상 문자열이며, action 객체를 생성하는 action 생성자 함수가 있다는 것을 배웠습니다. &lt;span style=&quot;color: #333333;&quot;&gt;매번 손으로 작성을 해도 되지만 그것은 너무 비효율적입니다. &lt;/span&gt;그렇다면 이런 action 객체, type 및 action 생성자는 어디에 정의 되어 있을까요?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;redux toolkit에는 createSlice라는 action type, action 생성자 및 action 객체를 생성하는 작업을 하는 함수가 있습니다. slice의 이름을 정의하고, 그 안에 reducer 기능이 있는 객체를 작성하면 자동으로 생성됩니다. name 옵션은 각 action type의 첫번째 부분으로 사용되며 각 reducer의 기능을 담당하는 함수의 이름은 두번째 부분으로 사용됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예를들어 name을 &quot;counter&quot;로 설정하고, reducer의 기능 함수 이름을 &quot;increment&quot;설정하면 둘이 합쳐져서 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;{ type: &quot;counter/increment&quot; } 이라는 action이 생성 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;name 필드 외에도 createSlice에 initialState를 전달하여 초기 state가 호출 될 수 있도록 해야합니다. 예제에서는 value가 0으로 시작하는 초기 state를 전달합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예제에는 세가지의 reducer 함수가 있으며 각각의 버튼을 클릭하여 전달되는 세가지의 action type 확인 했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;createSlice는 reducer 함수와 동일한 이름을 가진 action 생성자를 자동으로 생성합니다. 이를 확인 하고 싶으면 아래와 같이 확인 할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604393104421&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log(counterSlice.actions.increment())
// {type: &quot;counter/increment&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;또한 생성된 action type에 대해 응답할 수 있는 slice reducer도 생성됩니다&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604393309893&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const newState = counterSlice.reducer(
  { value: 10 }, //state
  counterSlice.actions.increment() // dispatch
)
console.log(newState)
// {value: 11}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Reducers의 규칙&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;앞에서 reducer는 항상 지켜야하는 규칙이 있다고 배웠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기존 state와&amp;nbsp; action을 기반으로 새로운 state를 계산해야합니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;state를 직접 수정하면 안됩니다. 대신 기존 state를 복사하고 복사된 state를 변경하여 불변성을 지켜야합니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;비동기 로직이나 다른 side effect가 있으면 안됩니다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이런 규칙을 지켜야하는 이유는 다음과 같습니다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;redux의 목표 중 하나는 코드가 어떻게 동작하는지 예측 가능하게 하는 것 입니다. 함수의 return이 인수에 의해서만 계산되는 경우 해당 코드의 작동 방식을 이해하기 쉽고 테스트도 용이해 집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;reducers가 외부 변수를 사용하거나 랜덤으로 동작하는 경우 함수가 실행 될 때 어떤 일이 발생하는지 정확히 알 수 없습니다.&lt;/li&gt;
&lt;li&gt;reducers가 인수(state)를 포함해서 다른 값을 수정하면 애플리케이션의 작동 방식이 변경될 수 있습니다. 이 경우 state를 업데이트를 했지만 UI가 업데이트 되지 않는 버그의 원인이 될 수 있습니다.&lt;/li&gt;
&lt;li&gt;redux DevTools 기능 중 일부는 reducer에 이런 규칙을 올바르게 적용해야 작동합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;불변성 업데이트는 정말 중요하므로 더 자세하게 알아보겠습니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reducer 및 불변성(immutable) 업데이트&lt;/h3&gt;
&lt;p&gt;앞에서 자바스크립트의 배열/객체 spread 연산자와 원본 배열/객체의 복사본을 생성해서 반환하는 함수를 사용하여 불변성을 지키면서 업데이트하는 방법을 배웠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;불변성을 지키며 업데이트하는 로직을 직접 작성하는 것은 손이 많이가고, redux를 쓸때 reducer에서 state를 변경하는 실수는 자주 일어납니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 redux toolkit의 createSlice를 사용하여 쉽게 작성할 수 있습니다. createSlice는 immer 라이브러리가 내장되어 있습니다. immer는 불변성을 지키며 업데이트를 도와주는 라이브러리입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들면 아래와 같습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604467970808&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function handwrittenReducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;불변성을 지키기 위해 객체의 깊이가 깊어지면 위와 같이 복잡한 코드가 되지만 immer를 사용하면 아래와 같이 코드가 깔끔해집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1604468592693&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function reducerWithImmer(state, action) {
  state.first.second[action.someId].fourth = action.someValue
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기에서 기억해야 할 중요한 사항이 있습니다.&lt;/p&gt;
&lt;p&gt;redux toolkit의 createSlice와 createReducer는 immer가 내장되어 있기 때문에 불변성을 지키는 것을 신경쓰지 않고 객체를 변경하는 것처럼 코드를 작성하지만, immer가 없이 이렇게 작성한다면 버그를 유발합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;counter slice에서 어떻게 작성을 하는지 알아보겠습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604469104680&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/counter/counterSlice.js
export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state =&amp;gt; {
      // 이렇게 작성해도 immer가 불변성을 유지해줍니다
      state.value += 1
    },
    decrement: state =&amp;gt; {
      state.value -= 1
    },
    incrementByAmount: (state, action) =&amp;gt; {
      state.value += action.payload
    }
  }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;increment 함수는 state.value에 1을 더하는 코드입니다. immer는 초기 state 객체를 변경한다고 알고 있기 때문에 return을 할 필요가 없습니다. decrement도 동일합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;increment와 decrement는 action 객체를 받을 필요가 없습니다. dispatch는 되지만 꼭 필요하지는 않기 때문에 action을 매개변수로 받지 않아도 됩니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;반면에 incrementByAmount는 카운터 값에 얼마나 추가해야 하는지를 알아야합니다. 따라서 reducer에서 state와 action을 모두 인자로 받는 함수를 작성합니다. 이 경우 텍스트 input에 입력 한 값이 action.payload로 전달 되므로 state.value에 더할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;불변성에 더 알고 싶다면 참고하세요 : &lt;a href=&quot;https://daveceddia.com/react-redux-immutability-guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;daveceddia.com/react-redux-immutability-guide/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Thunks로 비동기 작성&lt;/h3&gt;
&lt;p&gt;지금까지의 애플리케이션 로직은 동기식으로 작동했습니다. action이 전달되고 store가 reducer를 실행하고 새로운 state를 계산해서 완료됩니다. 그러나 자바스크립트는 비동기 코드를 작성해야하는 여러 가지 방법이 있으며 일반적으로 API를 통해 데이터를 가져오는 작업이 있습니다. redux에서도 비동기 로직을 처리 할 때가 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;thunk는 비동기 로직을 처리하는 라이브러리 입니다. thunk는 두가지 함수를 사용하여 작성됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dispatch와 getState를 인자로 받는 함수&lt;/li&gt;
&lt;li&gt;thunk를 생성하고 반환하는 외부 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 코드는 thunk action 생성자의 예시입니다&lt;/p&gt;
&lt;pre id=&quot;code_1604473828378&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/counter/counterSlice.js
export const incrementAsync = amount =&amp;gt; dispatch =&amp;gt; {
  setTimeout(() =&amp;gt; {
    dispatch(incrementByAmount(amount))
  }, 1000)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;일반적인 redux action생성자를 사용 할 수 있습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604475121014&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;store.dispatch(incrementAsync(5))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러나 thunk를 사용하려면 redux store가 생성될 때 redux-thunk 미들웨어를 추가해야합니다. redux toolkit에는 configureStore에는 이미 추가되어 있어서 thunk를 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;서버에서 데이터를 가져오기 위해 AJAX 호출을 해야하는 경우 이를 thunk에 넣을 수 있습니다. 다음 예제에서 확인해보세요&lt;/p&gt;
&lt;pre id=&quot;code_1604475288216&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/counter/counterSlice.js
// the outside &quot;thunk creator&quot; function
const fetchUserById = userId =&amp;gt; {
  // the inside &quot;thunk function&quot;
  return async (dispatch, getState) =&amp;gt; {
    try {
      // make an async call in the thunk
      const user = await userAPI.fetchById(userId)
      // dispatch an action when we get the response back
      dispatch(userLoaded(user))
    } catch (err) {
      // If something went wrong, handle it here
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음으로 counter 컴포넌트에 대해 살펴보겠습니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Counter 컴포넌트&lt;/h3&gt;
&lt;p&gt;다음은 counter 컴포넌트의 코드입니다&lt;/p&gt;
&lt;pre id=&quot;code_1604479593923&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/counter/Counter.js
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount
} from './counterSlice'
import styles from './Counter.module.css'

export function Counter() {
  const count = useSelector(selectCount)
  const dispatch = useDispatch()
  const [incrementAmount, setIncrementAmount] = useState('2')

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div className={styles.row}&amp;gt;
        &amp;lt;button
          className={styles.button}
          aria-label=&quot;Increment value&quot;
          onClick={() =&amp;gt; dispatch(increment())}
        &amp;gt;
          +
        &amp;lt;/button&amp;gt;
        &amp;lt;span className={styles.value}&amp;gt;{count}&amp;lt;/span&amp;gt;
        &amp;lt;button
          className={styles.button}
          aria-label=&quot;Decrement value&quot;
          onClick={() =&amp;gt; dispatch(decrement())}
        &amp;gt;
          -
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
      {/* omit additional rendering output here */}
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;counter 컴포넌트에는 useState를 사용해서 만든 데이터가 있습니다.&amp;nbsp;그러나 useState에는 count에 대한 값은 없습니다.&lt;/p&gt;
&lt;p&gt;count는 useSelector를 통해 다른 곳에서 값을 가져오고 있습니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;react에는 useState와 useEffect같은 내장 hooks가 있지만, react의 hooks를 사용하여 custom hooks를 만들 수 있습니다. react-redux 라이브러리에는 react 컴포넌트가 redux store와 상호 작용할 수 있도록 만든 custom hooks이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;useSelector를 사용해서 데이터 읽기&lt;/h4&gt;
&lt;p&gt;첫번째로 useSelector hook을 통해 컴포넌트가 redux store에서 필요한 데이터를 가져올 수 있습니다.&amp;nbsp;앞에서 state를 인수로 받아서 state 안에 일부분을 반환하는 selecor 함수를 만들 수 있는것을 배웠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;counterSlice 파일의 하단에 아래와 같은 selector 함수가 있는것을 확인할 수 있습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604480261364&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/counter/counterSlice.js
// 이를 selecotr라고 하며 state에서 값을 선택할 수 있습니다
// selecor는 컴포넌트에서 useSelector안에 넣어도 됩니다
// ex) useSelector ((state) =&amp;gt; state.counter.value)
export const selectCount = state =&amp;gt; state.counter.value&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;redux sotre 접근할 수 있는 경우 아래와 같이 현재 counter의 값을 가져올 수 있습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604480565664&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const count = selectCount(store.getState())
console.log(count)
// 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;컴포넌트에서는 redux store를 직접 부를 수 없기 때문에 useSelector를 사용합니다. selector함수를 전달하면 &lt;span&gt;someSelector(store.getState()) 를 호출하고 그에 대한 결과를 반환합니다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 아래와 같은 코드로 store의 count값을 가져올 수 있습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604480920713&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const count = useSelector(selectCount)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;export한 selector함수만 사용할 필요는 없습니다. 예를 들어 selector 함수를 useSelector에 인자로 넣어도 됩니다&lt;/p&gt;
&lt;pre id=&quot;code_1604481046000&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const countPlusTwo = useSelector(state =&amp;gt; state.counter.value + 2)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;action이 전달되고 redux store가 업데이트 될 때마다 useSelector는 selector함수를 다시 실행합니다. selector가 이전 state와 다른 값을 반환하면 useselector는 새로운 state로 컴포넌트를 리렌더링합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;useDispatch를 사용해서 action 전달하기&lt;/h4&gt;
&lt;p&gt;redux store에 접근 할 수 있다면 store.dispatch(increment())와 같이 action 생성자를 사용하여 전달할 수 있습니다. store에 접근 할 수 없다면 store의 dispatch 메소드만 접근 할 수 있는 방법이 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;useDispatch hook이 redux store의 dispatch 메소드를 제공합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1604543601583&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const dispatch = useDispatch()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;어떤 버튼을 클릭할 때 다음과 같이 action을 전달 할 수 있습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604543696500&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/counter/Counter.js
&amp;lt;button
  className={styles.button}
  aria-label=&quot;Increment value&quot;
  onClick={() =&amp;gt; dispatch(increment())}
&amp;gt;
  +
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트 state와 Forms&lt;/h3&gt;
&lt;p&gt;앱 내의 모든 state를 redux store에 넣어야 할까요?&lt;/p&gt;
&lt;p&gt;답은 아니오입니다. 앱 전체에 필요한 전역 state는 redux store에 있어야합니다. 한곳에서만 필요한 state는 컴포넌트에 있어도 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사용자가 카운터에 추가 될 숫자를 입력하는 텍스트 input이 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1604554139055&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/counter/Counter.js
const [incrementAmount, setIncrementAmount] = useState('2')

// later
return (
  &amp;lt;div className={styles.row}&amp;gt;
    &amp;lt;input
      className={styles.textbox}
      aria-label=&quot;Set increment amount&quot;
      value={incrementAmount}
      onChange={e =&amp;gt; setIncrementAmount(e.target.value)}
    /&amp;gt;
    &amp;lt;button
      className={styles.button}
      onClick={() =&amp;gt; dispatch(incrementByAmount(Number(incrementAmount) || 0))}
    &amp;gt;
      Add Amount
    &amp;lt;/button&amp;gt;
    &amp;lt;button
      className={styles.asyncButton}
      onClick={() =&amp;gt; dispatch(incrementAsync(Number(incrementAmount) || 0))}
    &amp;gt;
      Add Async
    &amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;input의 onChange 핸들러에서 &lt;span style=&quot;color: #333333;&quot;&gt;reducer에 &lt;/span&gt;action을 dispatch함으로써 state를 변경 할 수도 있지만 굳이 이렇게 할 필요는 없습니다. incrementAmount가 사용되는 컴포넌트는 counter 컴포넌트가 유일하기 때문입니다.&amp;nbsp;따라서 incrementAmount는 useState를 사용하는 것이 좋습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마찬가지로 isDropdownOpen 같은 boolean state가 필요한 경우이면서 다른 컴포넌트에서 사용하지 않는다면 컴포넌트안에 있는 것이 좋습니다.&amp;nbsp;&lt;span&gt;React + Redux 앱에서 전역 state는 Redux store에 있고, 로컬 state는 React 컴포넌트에 있어야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;state를 어디에 넣어야 할지 모르겠다면, redux에 어떤 데이터를 넣어야하는지 결정하는 몇가지 방법이 있습니다&lt;/p&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;state가 어떻게 변경되는지 확인 할 필요가 있습니까?&lt;/li&gt;
&lt;li&gt;axios 등을 통해 가져온 데이터를 다시 요청을 보내지 않고 가져왔던 데이터를 그대로 사용하는 경우가 있습니까?&lt;/li&gt;
&lt;li&gt;UI 컴포넌트를 리로드 하는 동안 데이터를 유지해야 합니까?&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이것은 일반적으로 redux에 forms를 어떻게 넣을지 생각하는 방법에 대한 좋은 예 입니다. 대부분의 forms에서 사용하는 state는 redux에 보관하면 안됩니다. 다만 컴포넌트에서 &lt;span style=&quot;color: #333333;&quot;&gt;forms의 값을 사용자가 변경을 완료하면 redux에 전달하여 store를 업데이트합니다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;다음으로 넘어가기 전에 주목해야 할 다른 사항:&amp;nbsp; counterSlice.js의 incrementAsync thunk를 이 컴포넌트에서 사용하고 있습니다. 다른 일반 action 생성자를 전달하는 것과 같은 방식으로 사용합니다. 이 컴포넌트에서는 일반적인 action을 dispatch 하는지 아니면 비동기 로직을 시작하는지 신경쓰지 않습니다. 버튼을 클릭하면 무언가를 전달한다는 것만 알고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Providing the Store&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;컴포넌트가 useSelector 및 useDispatch를 사용하여 Redux store와 통신 할 수 있음을 확인했습니다. 그런데 store를 import 하지 않았습니다. 그렇다면 어떻게 어떤 Redux store와 통신해야하는지 알 수 있을까요?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;애플리케이션의 다른 부분을 모두 살펴보았으니 이제는 시작점으로 가서 어떻게 이 퍼즐들이 맞춰지는지 살펴봅시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1604566054367&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
import * as serviceWorker from './serviceWorker'

ReactDOM.render(
  &amp;lt;Provider store={store}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/Provider&amp;gt;,
  document.getElementById('root')
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;항상 ReactDOM.render(&amp;lt;App /&amp;gt;)을 사용하여 react에게 root 컴포넌트인 App을 렌더링 하도록 했습니다. useSelector와 같은 hook이 제대로 작동하려면 &amp;lt;Provider&amp;gt; 컴포넌트를 사용하여 redux store를 연결시켜야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;store를 app/store.js 에서 만들어 놓은 store를 import합니다. 그 후 &amp;lt;App /&amp;gt;을 &amp;lt;Provider&amp;gt; 컴포넌트로 감싸고 store를 전달 합니다: &amp;lt;Provider&amp;nbsp;store={store}&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;모든 react 컴포넌트에서 &lt;span style=&quot;color: #333333;&quot;&gt;이제 useSelector 또는 usedispatch를 통해 &lt;/span&gt;&amp;lt;Provider&amp;gt;에서 넘겨준 redux store와 연결 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음 장은 redux의 데이터 흐름에 대해 알아보겠습니다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;a href=&quot;https://jcon.tistory.com/182&quot;&gt;4탄 - redux data flow&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605249755905&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;redux 강의 기본 핵심 튜토리얼 4탄 - redux data flow&quot; data-og-description=&quot;이번 파트에서는 createSlice를 사용해서 reducer로직의 slice를 store에 추가하는 방법과 useSelectore, UseDispatch 사용밥법에 대해서 알아보겠습니다. 프로젝트 설정 react와 redux가 설정되어 있고, 기본적..&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/182&quot; data-og-url=&quot;https://jcon.tistory.com/182&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dXSuH6/hyIehDQNKu/Rnr6ZV5JIbeMZ0MRzk5pLk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/lxYkg/hyIc7CPM0p/hlerzVFjdKrkFqAQ3zvoyk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/Rj0gT/hyIemZrEeg/vtqbwvU8T8auojdFxyUkEk/img.png?width=856&amp;amp;height=462&amp;amp;face=0_0_856_462&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/182&quot; data-source-url=&quot;https://jcon.tistory.com/182&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dXSuH6/hyIehDQNKu/Rnr6ZV5JIbeMZ0MRzk5pLk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/lxYkg/hyIc7CPM0p/hlerzVFjdKrkFqAQ3zvoyk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/Rj0gT/hyIemZrEeg/vtqbwvU8T8auojdFxyUkEk/img.png?width=856&amp;amp;height=462&amp;amp;face=0_0_856_462');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;redux 강의 기본 핵심 튜토리얼 4탄 - redux data flow&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이번 파트에서는 createSlice를 사용해서 reducer로직의 slice를 store에 추가하는 방법과 useSelectore, UseDispatch 사용밥법에 대해서 알아보겠습니다. 프로젝트 설정 react와 redux가 설정되어 있고, 기본적..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/181</guid>
      <comments>https://jcon.tistory.com/181#entry181comment</comments>
      <pubDate>Tue, 3 Nov 2020 14:44:50 +0900</pubDate>
    </item>
    <item>
      <title>VS CODE 확장 기능 2탄 (VS CODE Extensions)</title>
      <link>https://jcon.tistory.com/180</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;sample (23).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuoT8k/btqMkm4CLcJ/jrkO69buPoEBasY2DGNlD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuoT8k/btqMkm4CLcJ/jrkO69buPoEBasY2DGNlD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuoT8k/btqMkm4CLcJ/jrkO69buPoEBasY2DGNlD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuoT8k%2FbtqMkm4CLcJ%2FjrkO69buPoEBasY2DGNlD1%2Fimg.png&quot; data-filename=&quot;sample (23).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;vs code extentions에 대해서 작성한 글에 이어서 현재 제가 사용하고 있는 확장기능에 대해서 작성합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. HTML Snippets&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;VOhBvHb.gif&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;610&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4Rb3z/btqMiZWaX8R/qlfvLMbepdb6IegECHlI8k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4Rb3z/btqMiZWaX8R/qlfvLMbepdb6IegECHlI8k/img.gif&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://marketplace.visualstudio.com/items?itemName=abusaidm.html-snippets&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4Rb3z/btqMiZWaX8R/qlfvLMbepdb6IegECHlI8k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/c4Rb3z/btqMiZWaX8R/qlfvLMbepdb6IegECHlI8k/img.gif&quot; data-filename=&quot;VOhBvHb.gif&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;610&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://marketplace.visualstudio.com/items?itemName=abusaidm.html-snippets&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;확장기능 이름처럼 html snippet으로 html이나 jsx 파일에서 태그들을 쉽게 작성할 수 있게 해줍니다. 그림처럼 태그를 입력한 후 tab키를 누르면 코드가 작성됩니다. 만약 실수로 창을 껐다면 ctrl + space를 누르면 창이 다시 나타납니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &lt;span style=&quot;color: #000000;&quot;&gt;Reactjs code snippets&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;component.gif&quot; data-origin-width=&quot;1185&quot; data-origin-height=&quot;688&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FRA1c/btqMiZaNFwy/bMTF3YlDScNb71EdugMk2k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FRA1c/btqMiZaNFwy/bMTF3YlDScNb71EdugMk2k/img.gif&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://marketplace.visualstudio.com/items?itemName=xabikos.ReactSnippets&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FRA1c/btqMiZaNFwy/bMTF3YlDScNb71EdugMk2k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/FRA1c/btqMiZaNFwy/bMTF3YlDScNb71EdugMk2k/img.gif&quot; data-filename=&quot;component.gif&quot; data-origin-width=&quot;1185&quot; data-origin-height=&quot;688&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://marketplace.visualstudio.com/items?itemName=xabikos.ReactSnippets&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDoIq/btqMriTXfDe/QKM1uxjavGRpF5eGiIGEo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDoIq/btqMriTXfDe/QKM1uxjavGRpF5eGiIGEo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDoIq/btqMriTXfDe/QKM1uxjavGRpF5eGiIGEo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDoIq%2FbtqMriTXfDe%2FQKM1uxjavGRpF5eGiIGEo1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;react를 사용할 때 컴포넌트 등을 쉽게 만들 수 있는 스니펫입니다. 저는 주로 rsc 단축어를 많이 사용합니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. &lt;span style=&quot;color: #000000;&quot;&gt;Path Intellisense&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;iaHeUiDeTUZuo.gif&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;270&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beOY69/btqMmpN1guW/J2BSBYk2bGx35rQMnXJrQ1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beOY69/btqMmpN1guW/J2BSBYk2bGx35rQMnXJrQ1/img.gif&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beOY69/btqMmpN1guW/J2BSBYk2bGx35rQMnXJrQ1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/beOY69/btqMmpN1guW/J2BSBYk2bGx35rQMnXJrQ1/img.gif&quot; data-filename=&quot;iaHeUiDeTUZuo.gif&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;270&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;nbsp;https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Path Intellisense는 path를 가이드해주는 확장기능입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;4. &lt;span style=&quot;color: #000000;&quot;&gt;Prettier - Code formatter&lt;/span&gt; &lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;prettier는 가장 유명한 코드 포멧터입니다. 저장시에 자동으로 코드 정렬이 되지 않는다면 ctrl + , 를 누른 다음 on save를 검색한 후 Editor: Format On Save 를 체크하시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;.prettierignore 파일을 생성해서 prettier 적용을 제외할 수 있습니다&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604368119213&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Ignore artifacts:
build
coverage

# Ignore all HTML files:
*.html&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. &lt;span style=&quot;color: #000000;&quot;&gt;vscode-styled-components&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;demo.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;268&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5JOX2/btqMmpUN3fU/JBiWKq1druKG9sdG1Y7UI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5JOX2/btqMmpUN3fU/JBiWKq1druKG9sdG1Y7UI0/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5JOX2/btqMmpUN3fU/JBiWKq1druKG9sdG1Y7UI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5JOX2%2FbtqMmpUN3fU%2FJBiWKq1druKG9sdG1Y7UI0%2Fimg.png&quot; data-filename=&quot;demo.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;268&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;nbsp;https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;vscode-styled-components는 styled component 사용시에 작성한 css 부분에 가독성을 높여줍니다. 해당 확장기능을 사용하면 css 자동완성 및 오탈자도 체크가 되어 코딩할 때 편리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. &lt;span style=&quot;color: #000000;&quot;&gt;Bracket Pair Colorizer&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;example.png&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;119&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJceNZ/btqMtzuBfwz/F62tOoOvEx1eO5B9kzd6I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJceNZ/btqMtzuBfwz/F62tOoOvEx1eO5B9kzd6I1/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJceNZ/btqMtzuBfwz/F62tOoOvEx1eO5B9kzd6I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJceNZ%2FbtqMtzuBfwz%2FF62tOoOvEx1eO5B9kzd6I1%2Fimg.png&quot; data-filename=&quot;example.png&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;119&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;nbsp;https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Bracket Pair Colorizer는 브라켓을 시각화 해줍니다. 여러 확장 기능이 있지만 개인적으로 제일 나은것 같습니다. 버전 2도 나왔습니다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. &lt;span style=&quot;color: #000000;&quot;&gt;Remote - SSH&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;ssh-readme.gif&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;1080&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mQ4me/btqMr5gesZz/vPHYItHVwSvvrREF29MN41/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mQ4me/btqMr5gesZz/vPHYItHVwSvvrREF29MN41/img.gif&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mQ4me/btqMr5gesZz/vPHYItHVwSvvrREF29MN41/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/mQ4me/btqMr5gesZz/vPHYItHVwSvvrREF29MN41/img.gif&quot; data-filename=&quot;ssh-readme.gif&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;1080&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;nbsp;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;원격으로 서버를 접속하는 확장기능으로 &lt;/span&gt;제일 유용한 확장기능이라고 생각합니다. ec2 등 외부 서버에 접속을 할 때 사용하고 있고, 외부 서버의 폴더 트리 및 코드 파일을 vs code에서 확인이 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;단점은 vs code가 무거운 감이 있어서 원격 접속시 원격 서버의 사양이 낮다면 자주 접속이 끊길 수 있습니다. 그리고 초기 세팅이 처음하는 분이라면 복잡할 수 있습니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;접속 설정 방법은 &lt;a href=&quot;https://code.visualstudio.com/docs/remote/ssh-tutorial&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;code.visualstudio.com/docs/remote/ssh-tutorial&lt;/a&gt;를 참고 해보세요.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;pem키 설정은&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://jcon.tistory.com/103&quot;&gt;jcon.tistory.com/103&lt;/a&gt;을 참고하세요&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Dev Tools</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/180</guid>
      <comments>https://jcon.tistory.com/180#entry180comment</comments>
      <pubDate>Mon, 2 Nov 2020 18:02:12 +0900</pubDate>
    </item>
    <item>
      <title>redux 강의 기본 핵심 튜토리얼 2탄 - redux를 사용하기 위해 알아야하는 용어 및 개념</title>
      <link>https://jcon.tistory.com/179</link>
      <description>&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번 장에서는 리덕스를 사용하기 위해 알아야하는 용어와 개념에 대해 알아보겠습니다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;sample (22).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caJRaZ/btqL3vnk6yZ/vRaxTYlk5npjya6CYn4Lj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caJRaZ/btqL3vnk6yZ/vRaxTYlk5npjya6CYn4Lj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caJRaZ/btqL3vnk6yZ/vRaxTYlk5npjya6CYn4Lj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaJRaZ%2FbtqL3vnk6yZ%2FvRaxTYlk5npjya6CYn4Lj0%2Fimg.png&quot; data-filename=&quot;sample (22).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;state 관리&lt;/h2&gt;
&lt;p&gt;간단한 react counter 컴포넌트를 살벼보겠습니다. 버튼을 누르면 컴포넌트의 state가 증가합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1603960994700&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function Counter() {
  // State: a counter value
  const [counter, setCounter] = useState(0)

  // Action: code that causes an update to the state when something happens
  const increment = () =&amp;gt; {
    setCounter(prevCounter =&amp;gt; prevCounter + 1)
  }

  // View: the UI definition
  return (
    &amp;lt;div&amp;gt;
      Value: {counter} &amp;lt;button onClick={increment}&amp;gt;Increment&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;코드에&lt;span&gt; &lt;/span&gt;&lt;/span&gt;state view action이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- state는 앱을 작동 시키는 핵심입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- view는 UI를 정의하고 현재 state를 보여줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- action은 state를 사용자의 입력을 기반으로 발생하는 이벤트 및 트리거로 업데이트 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음은 단방향 데이터 흐름에 대한 설명입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- state는 특정 시점에 앱의 상태를 나타냅니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- UI는 state를 렌더합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- 사용자가 버튼을 클릭하는 등 어떠한 일이 발생하면 state가 업데이트 됩니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- UI는 새로운 state(업데이트 된 state)로 리렌더링 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T1ikM/btqL8WyvJK2/Nsc3TUTUtWOFOCJcqkOke0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T1ikM/btqL8WyvJK2/Nsc3TUTUtWOFOCJcqkOke0/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://ko.redux.js.org/tutorials/essentials/part-1-overview-concepts/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T1ikM/btqL8WyvJK2/Nsc3TUTUtWOFOCJcqkOke0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT1ikM%2FbtqL8WyvJK2%2FNsc3TUTUtWOFOCJcqkOke0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;nbsp;https://ko.redux.js.org/tutorials/essentials/part-1-overview-concepts/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그러나 여러 컴포넌트가 같은 state를 공유해야 할 필요가 있을 때 이런 단방향 데이터 흐름을 관리하는 것은 힘들 수 있습니다. 특히 여러 컴포넌트가 애플리케이션의 다른 부분에 있을 경우에 더욱 힘들어 집니다. &lt;a href=&quot;https://reactjs.org/docs/lifting-state-up.html&quot;&gt;&quot;lifting state up&quot;&lt;/a&gt; 을 사용해서 해결할 수도 있지만 항상 도움이 되는 것은 아닙니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 해결하기 위한 한가지 방법은 컴포넌트에서 state를 추출하여 컴포넌트 트리 외부에 있는 중심 위치에 넣으면 됩니다. 이렇게 하면 콤포넌트 트리는 큰 view가 되고, 모든 컴포넌트는 트리의 어디에 위치하든 state를 가지고 오거나 변경할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;state를 분리하고 &lt;span style=&quot;color: #333333;&quot;&gt;view와 state의 독립성을 유지하는&lt;span&gt; 규칙을 적용함으로써 코드에 더 많은 구조를 사용해도 유지관리를 할 수 있습니다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이것이 redux의 기본 아이디어입니다. 애플리케이션 안에 있는 하나의 중앙 장소에 어디에서든&lt;span style=&quot;color: #333333;&quot;&gt; 사용할 수 있는 state가 있고, state가 업데이트 될 때 어떻게 업데이트가 되는지 알 수 있는 코드를 만들기 위한 특정 패턴입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;불변성&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;자바스크립트 오브젝트와 배열은 기본적으로 모두 변경이 가능합니다. 배열과 오브젝트를 생성하면 안에 내용을 바꿀 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604284038089&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = { a: 1, b: 2 }
// 외부는 여전히 동일한 객체지만 내부는 변경됩니다
obj.b = 3

const arr = ['a', 'b']
// 배열도 마찬가지 입니다
arr.push('c')
arr[1] = 'd'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 중요한 것은 내부의 값이 변경되어도 같은 메모리 값을 참조하고 있기 때문에 내용은 바뀌었지만 외부에서 볼때는 내용이 바뀌기 전과 후가 같다고 인식을 합니다. 이를 오브젝트와 배열의 mutating이라고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 immutably(불변성)을 지키기 위해서 업데이트 할 때 기존의 배열/오브젝트를 복사한 후 복사된 사본을 수정해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자바스크립트의 배열/오브젝트 spread 연산자를 사용하거나, 기존 배열은 변하지 않고, 복사된 새로운 배열을 리턴하는 자바스크립트 배열의 내장 메소드를 사용해서 불변성을 지킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1604284746489&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = {
  a: {
    // 안전하게 obj.a.c를 업데이트 하려면 각각의 부분을 복사해야 합니다
    c: 3
  },
  b: 2
}

const obj2 = {
  // copy obj
  ...obj,
  // overwrite a
  a: {
    // copy obj.a
    ...obj.a,
    // overwrite c
    c: 42
  }
}

const arr = ['a', 'b']
// 끝에 'c'가 추가 된 새로운 배열을 만듭니다
const arr2 = arr.concat('c')

// 또는 기존의 배열 복사본을 만든 다음
const arr3 = arr.slice()
// 복사본을 변경합니다
arr3.push('c')&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;redux는 모든 state가 immutably(불변성, 변경되지 않을)할 것 이라고 예측합니다. 나중에 불변성을 지키는 것이 어디에서 어떻게 중요한지 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;더 자세히 알고 싶다면 아래 링크를 참조하세요&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://daveceddia.com/javascript-references/&quot;&gt;A Visual Guide to References in JavaScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://daveceddia.com/react-redux-immutability-guide/&quot;&gt;Immutability in React and Redux: The Complete Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;용어&lt;/h2&gt;
&lt;p&gt;계속하기전에 리덕스에서는 알아야 할 몇가지 중요한 단어가 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Actions&lt;/h3&gt;
&lt;p&gt;action은 type 이라는 키 값을 가지고 있는 자바스크립트 오브젝트입니다. 애플리케이션에서 어떠한 이벤트가 일어날지에 대한 설명이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들어 &quot;todos/todoAdded&quot;처럼 type은 action에 대한 설명을 하는 문자열이어야 합니다. &quot;domain/eventName&quot;처럼 보통 앞에는 기능 또는 카테고리가 들어가고 뒤에는 어떤 일이 발생 할 지에 대해 적습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;action은 발생하는 이벤트에 대해서 state를 업데이트 할 때 추가적으로 필요한 정보가 있을 수 있습니다. 이런 정보를 payload라고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;일반적인 action은 다음과 같습니다&lt;/p&gt;
&lt;pre id=&quot;code_1604291300394&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Action Creators&lt;/h3&gt;
&lt;p&gt;action 생성자는 action 객체를 리턴하는 함수입니다. action 생성자를 사용하게 되면 코드를 작성할 때마다 일일이 action을 적지 않아도 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1604292366423&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const addTodo = text =&amp;gt; {
  return {
    type: 'todos/todoAdded',
    payload: text
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reducers&lt;/h3&gt;
&lt;p&gt;reducer는 현재 state와 action 객체를 받는 함수입니다. state를 어떻게 업데이트할지 정하고, 정해진 방법대로 코드를 적용한 새로운 state를 반환하기 위해 사용합니다 :&amp;nbsp;(state, action) =&amp;gt; newState&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;reducer는 받은 action(이벤트) type을 기반으로 이벤트를 처리하는 이벤트 리스너라고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;reducer는 항상 몇 가지 특정 규칙을 지켜야합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- 기존 state 및 action을 사용해서 새로운 state값을 계산해서 만들어야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- state를 직접 수정 할 수 없습니다. 기존 state를 deep copy사하고 복사 된 값을 변경하는 방법으로 업데이트 해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- 비동기 작업을 수행하거나 임의의 값을 생성하는 등 기타 side effect를 일으키지 않아야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;reducer의 규칙이 중요한 이유와 올바른 방법은 추후에 더 자세히 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;reducer 함수 내부는 일반적으로 동일한 단계를 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;reducer 안에 &lt;span style=&quot;color: #333333;&quot;&gt;action에 대한 처리를 하는 로직이 있는지 확인합니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;해당하는 action이 있으면 state의 복사본을 만들고, 복사본에 새로운 값을 업데이트 한 다음 리턴하면 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;action이 없다면 기존 state를 변경하지 않고 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;reducer가 따라야하는 규칙을 보여주는 작은 reducer의 예시입니다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604298582659&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // reducer가 action에 대한 처리를 하는지 확인
  if (action.type === 'counter/increment') {
    // 있다면 state의 복사본을 만듭니다
    return {
      ...state,
      // 새로운 값으로 사본을 업데이트 합니다
      value: state.value + 1
    }
  }
  // 없다면 기존 state를 변경하지 않고 반환합니다.
  return state
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;reducer안에 action에 대한 처리는 if/else, switch 등등 모든 종류를 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Store&lt;/h3&gt;
&lt;p&gt;애플리케이션의 &lt;span style=&quot;color: #333333;&quot;&gt;현재 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;state는&lt;span&gt; &lt;span style=&quot;color: #333333;&quot;&gt;redux의&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt; store라는 객체에 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;store는 redux toolkit의 configureStore 메소드를 사용해서 생성되며 인자로 reducer를 넣어줍니다. getState 매서드를 통해 현재 state 값을 가져올 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604299703454&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Dispatch&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;redux store에는 dispatch라는 메소드가 있습니다. state를 업데이트하는 유일한 방법은 store.dispatch()를 사용해서 action객체를 전달하는 것 입니다. store는 reducer 함수를 통해서&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;전달하는 action에 따라 &lt;/span&gt;실행하고 state를 업데이트 합니다. 업데이트 된 후 getState()를 실행하면 업데이트 된 state를 확인 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1604300035017&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;store.dispatch({ type: 'counter/increment' })

console.log(store.getState())
// {value: 1}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;action을 전달하는 것은 애플리케이션에서 이벤트 트리거를 하는 것이라고 볼 수 있습니다. 어떠한 이벤트가 발생했을 때 store는 action을 전달받고, state를 업데이트합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;action을 전달할 때에는 action생성자를 호출하여 전달합니다&lt;/p&gt;
&lt;pre id=&quot;code_1604300645748&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const increment = () =&amp;gt; {
  return {
    type: 'counter/increment'
  }
}

store.dispatch(increment())

console.log(store.getState())
// {value: 2}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Selectors&lt;/h3&gt;
&lt;p&gt;selector는 store state에서 특정 값을 추출하는 방식의 함수입니다. 애플리케이션이 커지면서 앱의 다른 컴포넌트가 같은 데이터를 사용할 때 반복되는 로직을 방지 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1604301438695&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const selectCounterValue = state =&amp;gt; state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redux 애플리케이션 Data Flow&lt;/h3&gt;
&lt;p&gt;앞부분에 앱을 업데이트하는 단방향 데이터 흐름에 대해 설명했습니다. 간단하게 다시 알아보면&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- state는 특정 시점의 앱 상태를 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- UI는 해당 state에 따라 렌더링 됩니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- 사용자가 버튼을 클릭하는 등 어떤 이벤트가 발생하면 발생한 이벤트에 따라 state가 업데이트 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- 새로운 state에 따라 UI가 다시 렌더링 됩니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Redux는 다음 단계로 더 자세히 나눌 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 설정
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;redux store는 root reducer를 사용하여 생성합니다.&lt;/li&gt;
&lt;li&gt;store는 root reducer를 한번 호출해서 초기 state 값을 저장합니다.&lt;/li&gt;
&lt;li&gt;UI가 처음 렌더링 될 때 UI 컴포넌트는 redux store의 현재 state에 접근하고 해당 데이터를 사용하여 렌더링 할 항목을 결정합니다. 또한 state가 변경되었는지 알 수 있도록 store 구독합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;업데이트
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;사용자가 버튼을 클릭하는 등 앱에서 이벤트가 발생합니다.&lt;/li&gt;
&lt;li&gt;redux store에 action을 dispatch하는 코드는 &lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;dispatch({ type: 'counter/increment' })&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;와 같이 쓸 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;store는 이전 state와 전달 받은 action으로 reducer 함수를 실행하고, 반환 값을 새로운 state로 저장합니다.&lt;/li&gt;
&lt;li&gt;store를 구독하고 있는 UI 컴포넌트에 store가 업데이트 되었음을 알립니다.&lt;/li&gt;
&lt;li&gt;store의 data를 사용해야하는 UI 컴포넌트는 사용하고 있는 state가 변경되었는지 확인합니다.&lt;/li&gt;
&lt;li&gt;data가 변경된 컴포넌트는 새로운 data로 리렌더링 되어 화면에 표시되는 내용을 업데이트 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;시각적으로 보면 다음과 같습니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1080&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAfQYa/btqMril23DA/kTNqQE8igOcKtKJtdSzp3k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAfQYa/btqMril23DA/kTNqQE8igOcKtKJtdSzp3k/img.gif&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://ko.redux.js.org/tutorials/essentials/part-1-overview-concepts/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAfQYa/btqMril23DA/kTNqQE8igOcKtKJtdSzp3k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bAfQYa/btqMril23DA/kTNqQE8igOcKtKJtdSzp3k/img.gif&quot; data-filename=&quot;ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1080&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;nbsp;https://ko.redux.js.org/tutorials/essentials/part-1-overview-concepts/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금까지의 내용 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;redux는 애플리케이션의&amp;nbsp; state를 전역에서 관리하기 위한 라이브러리 입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;redux는 일반적으로 redux와 react를 함께 사용하기 위해 react-redux 라이브러리를 사용합니다&lt;/li&gt;
&lt;li&gt;redux tookit은 redux로직을 작성하는데 권장되는 방법입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;redux는 단방향 데이터 흐름 구조를 사용합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;state는 특정 시점의 앱 상태를 나타내고 UI는 해당 상태를 기반으로 렌더링 됩니다.&lt;/li&gt;
&lt;li&gt;앱에서 이벤트가 발생할 때
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;UI가 액션을 전달합니다.&lt;/li&gt;
&lt;li&gt;store는 reducer를 실행하고 발생한 이벤트에 따라 state가 업데이트 됩니다.&lt;/li&gt;
&lt;li&gt;store는 state가 변경되었음을 UI에 알립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;새로운 state에 따라 UI가 다시 렌더링 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;redux는 여러 유형의 코드를 사용합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;action은 type이라는 값을 가지고 있는 일반 객체이며, 앱에서 어떤 이벤트가 발생하는지를 설명합니다.&lt;/li&gt;
&lt;li&gt;reducer는 이전 state + action을 기반으로 새로운 state의 값을 계산하는 함수입니다.&lt;/li&gt;
&lt;li&gt;redux store는 action이 dispatch될 때마다 root reducer를 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음 파트는 redux 앱 구조에 대해 알아보겠습니다&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center -256px / 200px 420px repeat-x; height: 2px; padding: 21px 0px;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;a href=&quot;https://jcon.tistory.com/181&quot;&gt;3탄 - redux app 구조&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605249695994&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;redux 강의 기본 핵심 튜토리얼 3탄 - redux app 구조&quot; data-og-description=&quot;이전 포스트에서 redux의 용어와 개념에 대해서 설명했습니다. 이번 포스트에서는 실제 코드 예제를 살펴보고 어떻게 동작이 되는지 살펴 보겠습니다. 카운터 앱 예제 살펴볼 샘플 프로젝트는 버&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/181&quot; data-og-url=&quot;https://jcon.tistory.com/181&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iWLiG/hyH9AiVQbN/70DNKH4QKldIEkg7mnKbZk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/jzvRp/hyH9t5bfw7/I22ERAXZsSnrXYcnCbhqIk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bKHeXK/hyH791yF5Y/lhxmhPkxbk3HpdzvocSFMK/img.png?width=771&amp;amp;height=710&amp;amp;face=0_0_771_710&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/181&quot; data-source-url=&quot;https://jcon.tistory.com/181&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iWLiG/hyH9AiVQbN/70DNKH4QKldIEkg7mnKbZk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/jzvRp/hyH9t5bfw7/I22ERAXZsSnrXYcnCbhqIk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bKHeXK/hyH791yF5Y/lhxmhPkxbk3HpdzvocSFMK/img.png?width=771&amp;amp;height=710&amp;amp;face=0_0_771_710');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;redux 강의 기본 핵심 튜토리얼 3탄 - redux app 구조&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이전 포스트에서 redux의 용어와 개념에 대해서 설명했습니다. 이번 포스트에서는 실제 코드 예제를 살펴보고 어떻게 동작이 되는지 살펴 보겠습니다. 카운터 앱 예제 살펴볼 샘플 프로젝트는 버&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/179</guid>
      <comments>https://jcon.tistory.com/179#entry179comment</comments>
      <pubDate>Thu, 29 Oct 2020 18:03:34 +0900</pubDate>
    </item>
    <item>
      <title>redux 강의 기본 핵심 튜토리얼 1탄 - redux란 무엇일까?</title>
      <link>https://jcon.tistory.com/178</link>
      <description>&lt;p&gt;이 글은 리덕스 docs를 해석, 요약 한 글입니다(&lt;a href=&quot;https://ko.redux.js.org/tutorials/essentials/part-1-overview-concepts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ko.redux.js.org/tutorials/essentials/part-1-overview-concepts&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;리덕스 docs에서는 redux essentials(리덕스 핵심)을 살펴본 후 튜토리얼로 진행하는 것을 권장합니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;sample (21).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6jHgF/btqL3vnjNXt/W1ZCtYz17yfpScfNs6hSJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6jHgF/btqL3vnjNXt/W1ZCtYz17yfpScfNs6hSJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6jHgF/btqL3vnjNXt/W1ZCtYz17yfpScfNs6hSJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6jHgF%2FbtqL3vnjNXt%2FW1ZCtYz17yfpScfNs6hSJ0%2Fimg.png&quot; data-filename=&quot;sample (21).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://jcon.tistory.com/179&quot;&gt;2탄 - 리덕스를 사용하기 위해 알아야하는 용어 및 개념&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605249816027&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;redux 기본 핵심 튜토리얼 2탄 - redux 용어 및 개념&quot; data-og-description=&quot;이번 장에서는 리덕스를 사용하기 위해 알아야하는 용어와 개념에 대해 알아보겠습니다 state 관리 간단한 react counter 컴포넌트를 살벼보겠습니다. 버튼을 누르면 컴포넌트의 state가 증가합니다 f&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/179&quot; data-og-url=&quot;https://jcon.tistory.com/179&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jN1ga/hyH52NZZU6/fM7m0h1cZF7Ge5a2n7kyzK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bjNLPj/hyH579CyHB/n9kBnwi1zjdzXmoFGrG3P0/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/wGWAH/hyH6bqDQop/GgVfBKWslEjSmeHuqxocc1/img.png?width=738&amp;amp;height=577&amp;amp;face=0_0_738_577&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/179&quot; data-source-url=&quot;https://jcon.tistory.com/179&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jN1ga/hyH52NZZU6/fM7m0h1cZF7Ge5a2n7kyzK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bjNLPj/hyH579CyHB/n9kBnwi1zjdzXmoFGrG3P0/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/wGWAH/hyH6bqDQop/GgVfBKWslEjSmeHuqxocc1/img.png?width=738&amp;amp;height=577&amp;amp;face=0_0_738_577');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;redux 기본 핵심 튜토리얼 2탄 - redux 용어 및 개념&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이번 장에서는 리덕스를 사용하기 위해 알아야하는 용어와 개념에 대해 알아보겠습니다 state 관리 간단한 react counter 컴포넌트를 살벼보겠습니다. 버튼을 누르면 컴포넌트의 state가 증가합니다 f&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://jcon.tistory.com/181&quot;&gt;3탄 - redux app 구조&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605249816028&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;redux 강의 기본 핵심 튜토리얼 3탄 - redux app 구조&quot; data-og-description=&quot;이전 포스트에서 redux의 용어와 개념에 대해서 설명했습니다. 이번 포스트에서는 실제 코드 예제를 살펴보고 어떻게 동작이 되는지 살펴 보겠습니다. 카운터 앱 예제 살펴볼 샘플 프로젝트는 버&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/181&quot; data-og-url=&quot;https://jcon.tistory.com/181&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iWLiG/hyH9AiVQbN/70DNKH4QKldIEkg7mnKbZk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/jzvRp/hyH9t5bfw7/I22ERAXZsSnrXYcnCbhqIk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bKHeXK/hyH791yF5Y/lhxmhPkxbk3HpdzvocSFMK/img.png?width=771&amp;amp;height=710&amp;amp;face=0_0_771_710&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/181&quot; data-source-url=&quot;https://jcon.tistory.com/181&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iWLiG/hyH9AiVQbN/70DNKH4QKldIEkg7mnKbZk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/jzvRp/hyH9t5bfw7/I22ERAXZsSnrXYcnCbhqIk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bKHeXK/hyH791yF5Y/lhxmhPkxbk3HpdzvocSFMK/img.png?width=771&amp;amp;height=710&amp;amp;face=0_0_771_710');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;redux 강의 기본 핵심 튜토리얼 3탄 - redux app 구조&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이전 포스트에서 redux의 용어와 개념에 대해서 설명했습니다. 이번 포스트에서는 실제 코드 예제를 살펴보고 어떻게 동작이 되는지 살펴 보겠습니다. 카운터 앱 예제 살펴볼 샘플 프로젝트는 버&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://jcon.tistory.com/182&quot;&gt;4탄 - redux data flow&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605249816029&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;redux 강의 기본 핵심 튜토리얼 4탄 - redux data flow&quot; data-og-description=&quot;이번 파트에서는 createSlice를 사용해서 reducer로직의 slice를 store에 추가하는 방법과 useSelectore, UseDispatch 사용밥법에 대해서 알아보겠습니다. 프로젝트 설정 react와 redux가 설정되어 있고, 기본적..&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/182&quot; data-og-url=&quot;https://jcon.tistory.com/182&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dXSuH6/hyIehDQNKu/Rnr6ZV5JIbeMZ0MRzk5pLk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/lxYkg/hyIc7CPM0p/hlerzVFjdKrkFqAQ3zvoyk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/Rj0gT/hyIemZrEeg/vtqbwvU8T8auojdFxyUkEk/img.png?width=856&amp;amp;height=462&amp;amp;face=0_0_856_462&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/182&quot; data-source-url=&quot;https://jcon.tistory.com/182&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dXSuH6/hyIehDQNKu/Rnr6ZV5JIbeMZ0MRzk5pLk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/lxYkg/hyIc7CPM0p/hlerzVFjdKrkFqAQ3zvoyk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/Rj0gT/hyIemZrEeg/vtqbwvU8T8auojdFxyUkEk/img.png?width=856&amp;amp;height=462&amp;amp;face=0_0_856_462');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;redux 강의 기본 핵심 튜토리얼 4탄 - redux data flow&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이번 파트에서는 createSlice를 사용해서 reducer로직의 slice를 store에 추가하는 방법과 useSelectore, UseDispatch 사용밥법에 대해서 알아보겠습니다. 프로젝트 설정 react와 redux가 설정되어 있고, 기본적..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center -256px / 200px 420px repeat-x; height: 2px; padding: 21px 0px; color: #333333; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1탄에서는 redux를 사용하기 위해 알아야 할 주요 개념과 용어에 대해 알아보겠습니다.&amp;nbsp;우선, react devtools와 redux devtools extension을 설치합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1603957704779&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;React Developer Tools&quot; data-og-description=&quot;Adds React debugging tools to the Chrome Developer Tools. Created from revision 75726fadfd on 10/19/2020.&quot; data-og-host=&quot;chrome.google.com&quot; data-og-source-url=&quot;https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en&quot; data-og-url=&quot;https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bMKogY/hyH10ji47k/7gfK0zeipryywO9qgALygk/img.jpg?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128&quot;&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bMKogY/hyH10ji47k/7gfK0zeipryywO9qgALygk/img.jpg?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;React Developer Tools&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Adds React debugging tools to the Chrome Developer Tools. Created from revision 75726fadfd on 10/19/2020.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;chrome.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1603957706199&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Redux DevTools&quot; data-og-description=&quot;Redux DevTools for debugging application's state changes.&quot; data-og-host=&quot;chrome.google.com&quot; data-og-source-url=&quot;https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en&quot; data-og-url=&quot;https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bVXrvS/hyH3HJaGLX/5ijQZoZK3K3jb9hcaJHPX1/img.jpg?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128&quot;&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bVXrvS/hyH3HJaGLX/5ijQZoZK3K3jb9hcaJHPX1/img.jpg?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Redux DevTools&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Redux DevTools for debugging application's state changes.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;chrome.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redux는 무엇일까?&lt;/h2&gt;
&lt;p&gt;리덕스는 무엇일까요? 어떤 문제를 해결해 줄 수 있을까요? 왜 사용할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;리덕스는 &lt;b&gt;actions&lt;/b&gt;를 사용해서 state를 관리하고 업데이트 하기 위한 패턴 및 라이브러리입니다. 애플리케이션의 전체 state를 관리하는 중앙 저장소 역할을 하고, state가 actions을 통해서만 업데이트 될 수 있도록하는 규칙을 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redux를 사용해야하는 이유는?&lt;/h2&gt;
&lt;p&gt;애플리케이션의 여러 부분에서 필요한 state를 global에서 관리하는 데에 도움을 줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;리덕스에서 제공하는 패턴과 도구를 사용하면 애플리케이션의 state가 언제, 어디서, 왜, 어떻게 업데이트 되는지, 이러한 변경이 발생했을 때 애플리케이션 로직이 어떻게 작동하는지 쉽게 이해 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redux는 언제 사용해야할까?&lt;/h2&gt;
&lt;p&gt;리덕스는 state를 전역에서 관리하는데에 도움이 되지만 단점도 있습니다. 리덕스를 사용하기 위해서는 알아야하는 개념도 많아지고 코드도 더 많이 작성해야 할 수도 있습니다. 라이브러리이기 때문에 정해진 방식대로 사용해야 하는 제한 사항이 있을 수도 있습니다. 이런 이유로 생상선이 늦춰질 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;리덕스를 사용해야 할 때&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- 많은 state가 있고, 여러 컴포넌트에서 사용해야 할 때&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- state가 자주 업데이트 될 때&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- state의 업데이트 로직이 복잡 할 때&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- 크기가 큰 프로젝트에서 여러 사람들이 작업을 해야 할 때&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redux 라이브러리 및 도구&lt;/h2&gt;
&lt;p&gt;리덕스는 react에 종속 되어있는 라이브러리가 아닌 독립적인 js 라이브러리입니다. 그러나 보통 react와 많이 사용하고, 다른 여러 패키지와 함께 사용하기도 합니다&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React-Redux&lt;/h3&gt;
&lt;p&gt;Redux는 모든 UI 프레임 워크와 통합 할 수 있으며 React와 함께 가장 자주 사용됩니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://react-redux.js.org/&quot;&gt;&lt;b&gt;React-Redux&lt;/b&gt;&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 React 구성 요소가 상태 조각을 읽고 저장소를 업데이트하는 작업을 전달하여 Redux 저장소와 상호 작용할 수 있도록하는 공식 패키지입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redux 툴킷&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://redux-toolkit.js.org/&quot;&gt;&lt;b&gt;Redux Toolkit&lt;/b&gt;&lt;/a&gt;&lt;span&gt; 를 사용하는 것을 추천합니다&lt;/span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;여기에는 Redux 앱을 빌드하는 데 필수적인 패키지와 기능들이 포함되어 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;Redux Toolkit은 redux에서 제시하는 방법들을 기초로 해서 만들어졌고, Redux를 사용할 때 필요한 대부분의&amp;nbsp; 작업을 단순화시켜서 애플리케이션에 redux를 쉽게 적용할 수 있고 redux를 사용할 때 흔히 하는 실수를 방지 할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redux DevTools Extension&lt;/h3&gt;
&lt;p&gt;&lt;b&gt;&lt;/b&gt;시간에 따른 저장소의 state 변화를 보여줍니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;이를 통해 효과적으로 디버깅 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://jcon.tistory.com/179&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2탄 - 리덕스를 사용하기 위해 알아야하는 용어 및 개념&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1604305657479&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;redux 기본 핵심 튜토리얼 2탄 - redux 용어 및 개념&quot; data-og-description=&quot;이번 장에서는 리덕스를 사용하기 위해 알아야하는 용어와 개념에 대해 알아보겠습니다 state 관리 간단한 react counter 컴포넌트를 살벼보겠습니다. 버튼을 누르면 컴포넌트의 state가 증가합니다 f&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/179&quot; data-og-url=&quot;https://jcon.tistory.com/179&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jN1ga/hyH52NZZU6/fM7m0h1cZF7Ge5a2n7kyzK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bjNLPj/hyH579CyHB/n9kBnwi1zjdzXmoFGrG3P0/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/wGWAH/hyH6bqDQop/GgVfBKWslEjSmeHuqxocc1/img.png?width=738&amp;amp;height=577&amp;amp;face=0_0_738_577&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/179&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jcon.tistory.com/179&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jN1ga/hyH52NZZU6/fM7m0h1cZF7Ge5a2n7kyzK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bjNLPj/hyH579CyHB/n9kBnwi1zjdzXmoFGrG3P0/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/wGWAH/hyH6bqDQop/GgVfBKWslEjSmeHuqxocc1/img.png?width=738&amp;amp;height=577&amp;amp;face=0_0_738_577');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;redux 기본 핵심 튜토리얼 2탄 - redux 용어 및 개념&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이번 장에서는 리덕스를 사용하기 위해 알아야하는 용어와 개념에 대해 알아보겠습니다 state 관리 간단한 react counter 컴포넌트를 살벼보겠습니다. 버튼을 누르면 컴포넌트의 state가 증가합니다 f&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://jcon.tistory.com/181&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3탄 - redux app 구조&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1604646905928&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;redux 강의 기본 핵심 튜토리얼 3탄 - redux app 구조&quot; data-og-description=&quot;이전 포스트에서 redux의 용어와 개념에 대해서 설명했습니다. 이번 포스트에서는 실제 코드 예제를 살펴보고 어떻게 동작이 되는지 살펴 보겠습니다. 카운터 앱 예제 살펴볼 샘플 프로젝트는 버&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/181&quot; data-og-url=&quot;https://jcon.tistory.com/181&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iWLiG/hyH9AiVQbN/70DNKH4QKldIEkg7mnKbZk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/jzvRp/hyH9t5bfw7/I22ERAXZsSnrXYcnCbhqIk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bKHeXK/hyH791yF5Y/lhxmhPkxbk3HpdzvocSFMK/img.png?width=771&amp;amp;height=710&amp;amp;face=0_0_771_710&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/181&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jcon.tistory.com/181&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iWLiG/hyH9AiVQbN/70DNKH4QKldIEkg7mnKbZk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/jzvRp/hyH9t5bfw7/I22ERAXZsSnrXYcnCbhqIk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/bKHeXK/hyH791yF5Y/lhxmhPkxbk3HpdzvocSFMK/img.png?width=771&amp;amp;height=710&amp;amp;face=0_0_771_710');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;redux 강의 기본 핵심 튜토리얼 3탄 - redux app 구조&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이전 포스트에서 redux의 용어와 개념에 대해서 설명했습니다. 이번 포스트에서는 실제 코드 예제를 살펴보고 어떻게 동작이 되는지 살펴 보겠습니다. 카운터 앱 예제 살펴볼 샘플 프로젝트는 버&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://jcon.tistory.com/182&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;4탄 - redux data flow&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1605249597446&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;redux 강의 기본 핵심 튜토리얼 4탄 - redux data flow&quot; data-og-description=&quot;이번 파트에서는 createSlice를 사용해서 reducer로직의 slice를 store에 추가하는 방법과 useSelectore, UseDispatch 사용밥법에 대해서 알아보겠습니다. 프로젝트 설정 react와 redux가 설정되어 있고, 기본적..&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/182&quot; data-og-url=&quot;https://jcon.tistory.com/182&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dXSuH6/hyIehDQNKu/Rnr6ZV5JIbeMZ0MRzk5pLk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/lxYkg/hyIc7CPM0p/hlerzVFjdKrkFqAQ3zvoyk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/Rj0gT/hyIemZrEeg/vtqbwvU8T8auojdFxyUkEk/img.png?width=856&amp;amp;height=462&amp;amp;face=0_0_856_462&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/182&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jcon.tistory.com/182&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dXSuH6/hyIehDQNKu/Rnr6ZV5JIbeMZ0MRzk5pLk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/lxYkg/hyIc7CPM0p/hlerzVFjdKrkFqAQ3zvoyk/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/Rj0gT/hyIemZrEeg/vtqbwvU8T8auojdFxyUkEk/img.png?width=856&amp;amp;height=462&amp;amp;face=0_0_856_462');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;redux 강의 기본 핵심 튜토리얼 4탄 - redux data flow&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;이번 파트에서는 createSlice를 사용해서 reducer로직의 slice를 store에 추가하는 방법과 useSelectore, UseDispatch 사용밥법에 대해서 알아보겠습니다. 프로젝트 설정 react와 redux가 설정되어 있고, 기본적..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/178</guid>
      <comments>https://jcon.tistory.com/178#entry178comment</comments>
      <pubDate>Thu, 29 Oct 2020 17:32:27 +0900</pubDate>
    </item>
    <item>
      <title>react hooks에서 useContext(context API), useReducer로 상태관리 하기</title>
      <link>https://jcon.tistory.com/176</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;sample (19).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clkUWE/btqL3we5UFS/1kJ0QYntzHdvRbYT4kSP01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clkUWE/btqL3we5UFS/1kJ0QYntzHdvRbYT4kSP01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clkUWE/btqL3we5UFS/1kJ0QYntzHdvRbYT4kSP01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclkUWE%2FbtqL3we5UFS%2F1kJ0QYntzHdvRbYT4kSP01%2Fimg.png&quot; data-filename=&quot;sample (19).png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;react는 state를 상위 컴포넌트에서 관리하고 해당 state를 하위 컴포넌트 props로 내려주는데 하위 컴포넌트가 많아질 수록 props를 계속 전달하기 때문에 불편해 질 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;context API를 사용하면 &lt;/span&gt;이 문제를 해결할 수 있다. context API를 사용하면 모든 컴포넌트가 context에 있는 state를 받을 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스트에서는 useContext와 useReducer를 사용한 예제를 통해 context API 사용 방법을 익혀보자. &lt;span style=&quot;color: #333333;&quot;&gt;개인적으로 이렇게 context와 component는 폴더로 구분해서 사용하는 것을 좋아한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1603946997729&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;├─components
|  └─Component.jsx
├─context
|  └─ContextProvider.jsx
└─app.js&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;context/ContextProvider.js&lt;/h2&gt;
&lt;p&gt;먼저 context provider를 설정해보자. context provider는 &lt;span style=&quot;color: #000000;&quot;&gt;context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 한다. 구독중인 컴포넌트는 value props가 변경될 때 리렌더링 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1603947778667&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useReducer } from &quot;react&quot;;

// context를 생성한 후 export 한다
export const Context = React.createContext();

// state의 초기 값을 설정한다
const initialState = {
  count: 0;
};


// reducer는 action에서 받은 type에 따라서 state를 변경한다.
const reducer = (state, action) =&amp;gt; {
  switch (action.type) {
    case &quot;INCREASE&quot; :
      return {
        ...state, //현재 state가 하나뿐이라 생략해도 된다. 두개 이상일 경우 변경하지 않은 state를 유지하기 위해 사용한다
        count: action.value, // action.value에서 value는 추후 component에서 dispatch 할 때 payload로 보내주는 값의 이름이다.
      };
    
    default:
      throw new Error();
  }
};

const ContextProvider = ({ children }) =&amp;gt; {
  // useReducer를 사용해서 state와 dispatch를 생성한다.
  const [state, contextDispatch] = useReducer(reducer, initialState);
  
  return (
    &amp;lt;Context.Provider
      //provider에 value props로 state와 dispatch를 내려준다.
      value={{ count: state.count, contextDispatch }}
    &amp;gt;
      {children}
    &amp;lt;/Context.Provider&amp;gt;
  );
};

export default ContextProvider;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;app.js&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성한 provider를 app.js 또는 index.js에 import 하여 모든 하위 컴포넌트가 포함되도록 감싼다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1603948483901&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;
import Component from &quot;./component/Component&quot;;
import ContextProvider from &quot;./context/ContextProvider&quot;;

import &quot;./App.css&quot;;

const App = () =&amp;gt; {
  return (
    &amp;lt;ContextProvider&amp;gt;
      &amp;lt;Component /&amp;gt;
    &amp;lt;/ContextProvider&amp;gt;
  );
};

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;component/Component.js&lt;/h2&gt;
&lt;pre id=&quot;code_1603948927638&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useContext } from &quot;react&quot;;
// ContextProvider를 가지고 오는 것이 아닌 생성한 Context를 가지고 온다.
import { Context } from &quot;../context/ContextProvider&quot;;

const Component = () =&amp;gt; {
 // useContext를 이용하여 import한 Context안의 provider value를 가지고 온다.
  const { count, contextDispatch } = useContext(Context);

  return (
    &amp;lt;div&amp;gt;
    	&amp;lt;p&amp;gt; count: {count} &amp;lt;/p&amp;gt;
        &amp;lt;button 
          // dispatch의 파라미터는 action에 담겨서 provider의 reducer로 전달되면서,
          // provider에서 정의한 reducer에서 action.type과 action.value로 읽을 수 있다.
          onClick={() =&amp;gt; contextDispatch({ type: &quot;INCREASE&quot;, value: count + 1 })
        &amp;gt; 
          증가 
        &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;적용되어 있는 코드가 궁금하다면 아래 repo 확인&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- basic branch : react hooks만 사용&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- contextAPI branch : basic 코드를 context API로 구현&lt;/p&gt;
&lt;figure id=&quot;og_1603946460037&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;jch1223/thumbnail_creater&quot; data-og-description=&quot;Contribute to jch1223/thumbnail_creater development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jch1223/thumbnail_creater&quot; data-og-url=&quot;https://github.com/jch1223/thumbnail_creater&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dEIQfi/hyH14ME812/x9cREk0IV0XGruOQIaSawk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=109_36_308_254&quot;&gt;&lt;a href=&quot;https://github.com/jch1223/thumbnail_creater&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jch1223/thumbnail_creater&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dEIQfi/hyH14ME812/x9cREk0IV0XGruOQIaSawk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=109_36_308_254');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;jch1223/thumbnail_creater&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Contribute to jch1223/thumbnail_creater development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/176</guid>
      <comments>https://jcon.tistory.com/176#entry176comment</comments>
      <pubDate>Thu, 29 Oct 2020 14:34:38 +0900</pubDate>
    </item>
    <item>
      <title>react-nodejs 프로젝트 heroku에 배포하기</title>
      <link>https://jcon.tistory.com/175</link>
      <description>&lt;p&gt;react와 node js로 만든 프로젝트를 heroku cli를 이용하여 배포하는 방법에 대해 알아보자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. heroku cli를 다운로드 받는다&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;devcenter.heroku.com/articles/heroku-cli&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1600066639517&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;The Heroku CLI | Heroku Dev Center&quot; data-og-description=&quot;The Heroku CLI Last updated 01 September 2020 The Heroku Command Line Interface (CLI) makes it easy to create and manage your Heroku apps directly from the terminal. It&amp;rsquo;s an essential part of using Heroku. Download and install The Heroku CLI requires Git&quot; data-og-host=&quot;devcenter.heroku.com&quot; data-og-source-url=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot; data-og-url=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cNLzgp/hyHuDotQdx/Dr8askhD3lqudBb9Gu4pJk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cNLzgp/hyHuDotQdx/Dr8askhD3lqudBb9Gu4pJk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;The Heroku CLI | Heroku Dev Center&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;The Heroku CLI Last updated 01 September 2020 The Heroku Command Line Interface (CLI) makes it easy to create and manage your Heroku apps directly from the terminal. It&amp;rsquo;s an essential part of using Heroku. Download and install The Heroku CLI requires Git&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;devcenter.heroku.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;위 링크에서 자신의 운영체제에 맞는 방법으로 cli를 다운로드 한다. 다운로드 후 텍스트 에디터로 배포할 프로젝트를 연후 터미널 창에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;heroku -v&lt;/span&gt; 명령어를 이용하여 설치가 잘 되었는지 확인한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;윈도우에서 git bash를 사용 중 일 때 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;bash:&amp;nbsp;heroku:&amp;nbsp;command&amp;nbsp;not&amp;nbsp;found&lt;span style=&quot;background-color: #ffffff;&quot;&gt; 에러가 나타날 경우 아래의 글을 참고.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1600066783046&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;window(윈도우)에서 heroku cli가 git bash에서 실행이 안될 때&quot; data-og-description=&quot;heroku는 배포를 쉽게 할 수 있게 만들어 주는 서비스이다. heroku를 이용한 배포 방법 중 heroku cli를 이용해 배포를 하기위해 heroku cli를 다운 받았다. The Heroku CLI | Heroku Dev Center The Heroku CLI L..&quot; data-og-host=&quot;jcon.tistory.com&quot; data-og-source-url=&quot;https://jcon.tistory.com/174&quot; data-og-url=&quot;https://jcon.tistory.com/174&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bVf386/hyHuLz5lAq/6xJ4GzGS7VWo2N8m4NmJq0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/CXOE3/hyHuCiQLSL/OQjk8Kvw8aipUkGeENuUM1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://jcon.tistory.com/174&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jcon.tistory.com/174&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bVf386/hyHuLz5lAq/6xJ4GzGS7VWo2N8m4NmJq0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/CXOE3/hyHuCiQLSL/OQjk8Kvw8aipUkGeENuUM1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;window(윈도우)에서 heroku cli가 git bash에서 실행이 안될 때&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;heroku는 배포를 쉽게 할 수 있게 만들어 주는 서비스이다. heroku를 이용한 배포 방법 중 heroku cli를 이용해 배포를 하기위해 heroku cli를 다운 받았다. The Heroku CLI | Heroku Dev Center The Heroku CLI L..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jcon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;2. heroku 내에 프로젝트 폴더 생성&lt;/p&gt;
&lt;p&gt;heroku 홈페이지에서 직접 생성할 수도 있고, 아래의 명령어를 통해서 생성할 수 도 있다. 홈페이지에서 이미 생성을 했다면 3번으로 넘어간다.&lt;/p&gt;
&lt;pre id=&quot;code_1600067081011&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;heroku login

heroku create&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3-1. 프로젝트 내의 heroku 배포 설정&lt;/p&gt;
&lt;p&gt;heroku cli를 통해 배포하기 위해서는 package.json에서 설정을 해주어야 하는 것이 있다. 우선 engine의 값을 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1600066908408&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//package.json
...
 &quot;engine&quot;: {
    &quot;node&quot;: &quot;12.16.3&quot;,
    &quot;npm&quot;: &quot;6.14.8&quot;
  },
 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;scripts 안에 &lt;span&gt;heroku-postbuild를 설정한다. 이 명령어를 통해 heroku에 배포를 하게 되면 react의 모듈을 설치하고 빌드를 할 수 있게 한다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;clinet&lt;/span&gt;는 react 프로젝트의 폴더 이름이다. 자신의 폴더 이름에 맞게 수정하면 된다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;NPM_CONFIG_PRODUCTION=false&lt;span style=&quot;background-color: #ffffff;&quot;&gt;는 package.json에서&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;devDependencies&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;안의 모듈도 설치하게 하는 명령어이다. 배포 할 때&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;devDependencies안에 모듈이 필요가 없다면 생략해도 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1600067652749&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; ...
 &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node server/index.js&quot;,
    &quot;backend&quot;: &quot;nodemon server/index.js&quot;,
    &quot;frontend&quot;: &quot;npm run start --prefix client&quot;,
    &quot;dev&quot;: &quot;concurrently \&quot;npm run backend\&quot; \&quot;npm run start --prefix client\&quot;&quot;,
    &quot;heroku-postbuild&quot;: &quot;NPM_CONFIG_PRODUCTION=false npm install --prefix client &amp;amp;&amp;amp; npm run build --prefix client&quot;
  },
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;express의 index.js에서 react의 빌드 파일을 보여줄 수 있게 설정해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1600067468079&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//express의 index.js
...
if (process.env.NODE_ENV === &quot;production&quot;) {
//&quot;client/build&quot;는 react의 build파일 경로이다
  app.use(express.static(&quot;client/build&quot;));

//&quot;..client&quot;는 react 프로젝트의 파일 경로, &quot;build&quot;는 react프로젝트 내의 build폴더이다
  app.get(&quot;*&quot;, (req, res) =&amp;gt; {
    res.sendFile(path.resolve(__dirname, &quot;../client&quot;, &quot;build&quot;, &quot;index.html&quot;));
  });
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3-2. heroku 홈페이지 설정&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;tempsnip.png&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;630&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMhvOx/btqIAODUxX5/6kvFtr9y38JMI9vG03k581/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMhvOx/btqIAODUxX5/6kvFtr9y38JMI9vG03k581/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMhvOx/btqIAODUxX5/6kvFtr9y38JMI9vG03k581/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMhvOx%2FbtqIAODUxX5%2F6kvFtr9y38JMI9vG03k581%2Fimg.png&quot; data-filename=&quot;tempsnip.png&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;630&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;heroku 홈페이지에서 2번에 생성한 폴더로 들어간 후 setings 탭으로 간다. app name에서 이름을 변경하면 도메인 네임도 변경된다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;process.env.NODE_ENV&amp;nbsp;===&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;'production'&lt;/span&gt; 설정을 사용하여 배포 모드의 변수를 설정한 것이 있다면 &lt;/span&gt;Config Vars에서 설정이 가능하다. Receal Config Vars 버튼을 눌러&amp;nbsp;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env에서 사용한 키값과 밸류 값을 등록하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;이번 포스팅은 heroku 배포에 관한 포스팅임으로 .env에 대해 다루지는 않겠다. 궁금하신 분은 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.env&lt;/span&gt;에 대해 찾아보면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;4. 배포하기&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;프로젝트 터미널에서 heroku 로그인을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600068734395&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;heroku login&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;git을 적용 중이라면 다음 스텝으로 넘어간다. 적용되어 있지 않다면 아래 명령어를 통해 적용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1600068855235&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git init&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;remote에 heroku git repo를 등록한다&lt;/p&gt;
&lt;pre id=&quot;code_1600068909155&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;heroku git:remote -a heroku폴더이름&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;heroku git에 push를 한다&lt;/p&gt;
&lt;pre id=&quot;code_1600068972555&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git add .
git commit -m '커밋 메세지'
git push heroku master&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 내용은 heroku 홈페이지에서 2번에 생선한 폴더에 들어간 다음 deploy탭을 누른 후 하단에서 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;git에 push를 하면 heroku에서 3번에서 설정한 대로 모듈을 다운받고 빌드를 한 후 배포가 완료된다. 시간이 오래 걸리니 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;remote: Verifying deploy... done.&lt;/span&gt; 메세지가 나올 때 까지 기다리도록 하자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;도메인은 2번에서 생성한 폴더 - settings 하단에 domain 쪽에서 확인 할 수 있다. 위의 과정을 진행하면 heroku에 배포가 완료 된다.&lt;/p&gt;</description>
      <category>Deploy</category>
      <category>deploy</category>
      <category>heroku</category>
      <category>배포</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/175</guid>
      <comments>https://jcon.tistory.com/175#entry175comment</comments>
      <pubDate>Mon, 14 Sep 2020 16:39:56 +0900</pubDate>
    </item>
    <item>
      <title>window(윈도우)에서 heroku cli가 git bash에서 실행이 안될 때</title>
      <link>https://jcon.tistory.com/174</link>
      <description>&lt;p&gt;heroku는 배포를 쉽게 할 수 있게 만들어 주는 서비스이다. heroku를 이용한 배포 방법 중 heroku cli를 이용해 배포를 하기위해 heroku cli를 다운 받았다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1600063815571&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;The Heroku CLI | Heroku Dev Center&quot; data-og-description=&quot;The Heroku CLI Last updated 01 September 2020 The Heroku Command Line Interface (CLI) makes it easy to create and manage your Heroku apps directly from the terminal. It&amp;rsquo;s an essential part of using Heroku. Download and install The Heroku CLI requires Git&quot; data-og-host=&quot;devcenter.heroku.com&quot; data-og-source-url=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot; data-og-url=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cNLzgp/hyHuDotQdx/Dr8askhD3lqudBb9Gu4pJk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cNLzgp/hyHuDotQdx/Dr8askhD3lqudBb9Gu4pJk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;The Heroku CLI | Heroku Dev Center&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;The Heroku CLI Last updated 01 September 2020 The Heroku Command Line Interface (CLI) makes it easy to create and manage your Heroku apps directly from the terminal. It&amp;rsquo;s an essential part of using Heroku. Download and install The Heroku CLI requires Git&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;devcenter.heroku.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 git bash에서&amp;nbsp; &lt;span style=&quot;background-color: #dddddd;&quot;&gt;heroku -v &lt;span style=&quot;background-color: #ffffff;&quot;&gt;명령어를 입력하면 &lt;span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;bash : heroku : 명령을 찾을 수 없습니다&lt;/span&gt; 라는 에러가 나타난다. 설치가 안된 것인지 확인을 위해 cmd에서 같은 명령어를 쳤더니 정상적으로 heroku의 버전이 나타났다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span&gt;이는 git bash에 path 구성이 등록이 되어 있지 않기 때문에 나타난 오류이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span&gt;git bash에서 아래의 명령어를 치면 path를 확인 할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1600063420476&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo $PATH&lt;/code&gt;&lt;/pre&gt;
&lt;figure id=&quot;og_1600064062776&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Fast Web Service Build Up&quot; data-og-description=&quot;COUPANG&quot; data-og-host=&quot;www.coupang.com&quot; data-og-source-url=&quot;https://coupa.ng/bJMx4f&quot; data-og-url=&quot;https://www.coupang.com/vp/products/205676397&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/FfZWg/hyHwBWTvqK/8sErzu1JJo8SEAV85SNOrk/img.jpg?width=230&amp;amp;height=230&amp;amp;face=0_0_230_230,https://scrap.kakaocdn.net/dn/Gc4tV/hyHu3lGO6B/JZTe6aBKgP8RnThJfr6Qe1/img.jpg?width=230&amp;amp;height=230&amp;amp;face=0_0_230_230&quot;&gt;&lt;a href=&quot;https://coupa.ng/bJMx4f&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://coupa.ng/bJMx4f&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/FfZWg/hyHwBWTvqK/8sErzu1JJo8SEAV85SNOrk/img.jpg?width=230&amp;amp;height=230&amp;amp;face=0_0_230_230,https://scrap.kakaocdn.net/dn/Gc4tV/hyHu3lGO6B/JZTe6aBKgP8RnThJfr6Qe1/img.jpg?width=230&amp;amp;height=230&amp;amp;face=0_0_230_230');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Fast Web Service Build Up&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;웹 서비스, 개발에서 멈추지 말고 클라우드에 서비스까지!
그리고 Twitter bootstrap, Django, jQuery, Heroku를 하나로~&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;www.coupang.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;heroku cli를 설치 할 때 경로를 바꾸지 않았다면 git bash에서 아래의 명령어를 통해 실행이 가능할 것이다&lt;/p&gt;
&lt;pre id=&quot;code_1600063501564&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/c/Program\ Files/Heroku/bin/heroku login&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위의 명령어를 입력 후 정상적으로 heroku 로그인이 진행되는 것을 확인 했다면 아래와 같이 입력하여 PATH를 추가하자&lt;/p&gt;
&lt;pre id=&quot;code_1600063747772&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PATH=&quot;$PATH:/c/Program Files/Heroku/bin&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;PATH를 추가 후 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;heroku -v&lt;/span&gt; 명령어를 입력하니 정상적으로 작동하는 것을 확인 했다.&lt;/p&gt;</description>
      <category>Deploy</category>
      <category>CLI</category>
      <category>Git Bash</category>
      <category>heroku</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/174</guid>
      <comments>https://jcon.tistory.com/174#entry174comment</comments>
      <pubDate>Mon, 14 Sep 2020 15:16:07 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 폴더 내부에 2개 이상의 서버를 한번에 켜고 싶을 때</title>
      <link>https://jcon.tistory.com/173</link>
      <description>&lt;p&gt;&lt;span&gt;프로젝트 폴더 내에 클라이언트와 api서버가 함께 있을 때 서버를 키기 위해서는 각각 packge.json이 있는 폴더로 이동하여 npm 명령어를 실행해야 하는 번거로움이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;이런 불편함을 해소하고 하나의 명령어로 두개의 서버를 킬 수 있는 &lt;/span&gt;&lt;span&gt;concurrently라는 npm 패키지가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1598324270897&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install concurrently --save&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 후에 아래의 dev명령어와 같이 입력하면 동시에 두개의 서버가 실행된다&lt;/p&gt;
&lt;p&gt;--prefix 명령어는 client폴더 안에서 &quot;npm run start&quot;를 실행한다는 뜻이다&lt;/p&gt;
&lt;pre id=&quot;code_1598324331439&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;scripts&quot;: {
    &quot;start&quot;: &quot;nodemon server/index.js&quot;,
    &quot;dev&quot;: &quot;concurrently \&quot;npm run start\&quot; \&quot;npm run start --prefix client\&quot;&quot;,
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
  },&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Tech Memo</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/173</guid>
      <comments>https://jcon.tistory.com/173#entry173comment</comments>
      <pubDate>Tue, 25 Aug 2020 12:00:56 +0900</pubDate>
    </item>
    <item>
      <title>next js에서 pwa 다운로드 버튼 생성하기</title>
      <link>https://jcon.tistory.com/172</link>
      <description>&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;데스크탑에서 크롬이나 모바일&amp;nbsp;&lt;/span&gt;안드로이드 같은 경우 beforeinstallpropmt 이벤트를 사용해서 다운로드 버튼을 만들 수 있지만 ios는 작성일 기준까지도 불가능하다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. beforeinstallpropmt 이벤트 등록하기&lt;/h3&gt;
&lt;pre id=&quot;code_1596774306066&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect, useRef } from &quot;react&quot;;

...

let deferredPrompt = useRef(null);

useEffect(() =&amp;gt; {
    console.log(&quot;Listening for Install prompt&quot;);
    window.addEventListener(&quot;beforeinstallprompt&quot;, (e) =&amp;gt; {
      e.preventDefault();
      deferredPrompt.current = e;
    });
    
    //설치가 되어있다면 버튼은 숨긴다
    if (!deferredPrompt.current) {
      return dispatch({
        type: &quot;HIDE_BUTTON&quot;,
      });
    }

//버튼을 보여줌
    dispatch({
      type: &quot;SHOW_BUTTON&quot;,
    });
  }, []);
  
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;useRef를 사용하여 defferdpropmt변수에 beforeinstallpropmt 이벤트를 넣는다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 설치 함수 만들기&lt;/h2&gt;
&lt;pre id=&quot;code_1596774621382&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const installApp = () =&amp;gt; {
    if (!deferredPrompt.current) return false;
    
    //홈화면의 추가를 실행시킨다
    deferredPrompt.current.prompt();
    
    //실행 후 유저가 설치를 했는지 안했는지를 알 수 있다
    deferredPrompt.current.userChoice.then((choiceResult) =&amp;gt; {
      //설치 했을 때
      if (choiceResult.outcome === &quot;accepted&quot;) {
        console.log(&quot;User accepted the A2HS prompt&quot;);
        dispatch({
          type: &quot;HIDE_BUTTON&quot;,
        });
      } else {
      //설치 하지 않았을 때
        console.log(&quot;User dismissed the A2HS prompt&quot;);
      }
    });
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.버튼에 이벤트 등록&lt;/h3&gt;
&lt;pre id=&quot;code_1596774694118&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

return (
  &amp;lt;&amp;gt;
    &amp;lt;button onClick={installApp}&amp;gt;다운로드&amp;lt;/button&amp;gt;
  &amp;lt;/&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;next js에 pwa 적용하기 : &lt;a href=&quot;https://jcon.tistory.com/171&quot;&gt;https://jcon.tistory.com/171&lt;/a&gt;&lt;/p&gt;</description>
      <category>Nextjs</category>
      <category>button</category>
      <category>install</category>
      <category>JS</category>
      <category>NEXT</category>
      <category>PWA</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/172</guid>
      <comments>https://jcon.tistory.com/172#entry172comment</comments>
      <pubDate>Fri, 7 Aug 2020 13:36:02 +0900</pubDate>
    </item>
    <item>
      <title>next js에 pwa 적용하기</title>
      <link>https://jcon.tistory.com/171</link>
      <description>&lt;p&gt;next js에서 pwa를 적용하여 모바일에서 다운로드가 가능하게 만드는 튜토리얼 입니다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. next js 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1596766983264&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx create-next-app&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;next js를 설치한다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. next-pwa 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1596767032259&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install next-pwa&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;next js 프로젝트 안에 next-pwa 패키지를 설치한다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. next.config.js 파일을 생성한다&lt;/h3&gt;
&lt;p&gt;해당 파일 안에 아래와 같이 작성한다&lt;/p&gt;
&lt;pre id=&quot;code_1596767089652&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const withPWA = require('next-pwa')
 
module.exports = withPWA({
    pwa: {
        dest: 'public'
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다른 웹팩에 관련된 패키지를 적용하려면 아래와 같이 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1596767257602&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const withPlugins = require(&quot;next-compose-plugins&quot;);
const withPWA = require(&quot;next-pwa&quot;);
const withOptimizedImages = require(&quot;next-optimized-images&quot;);
const withFonts = require(&quot;next-fonts&quot;);
const FilterWarningsPlugin = require(&quot;webpack-filter-warnings-plugin&quot;);

const nextConfig = {
  generateEtags: false,
  webpack: (config, { isServer }) =&amp;gt; {
    config.plugins.push(
      new FilterWarningsPlugin({
        exclude: /mini-css-extract-plugin[^]*Conflicting order between:/,
      })
    );
    config.resolve.modules.push(__dirname);

    return config;
  },
};

module.exports = withPlugins(
  [
    [
      withPWA,
      {
        pwa: {
          dest: &quot;public&quot;,
        },
      },
    ],
    [
      withOptimizedImages,
      {
        mozjpeg: {
          quality: 90,
        },
        webp: {
          preset: &quot;default&quot;,
          quality: 90,
        },
      },
    ],
    withFonts,
  ],
  nextConfig
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. manifest.json 파일을 만들어야 한다&lt;/h3&gt;
&lt;figure id=&quot;og_1596767324169&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;App Manifest Generator&quot; data-og-description=&quot;manifest.json Generate Icons The Web App Manifest allows for specifying icons of varying sizes. Upload a 512x512 image for the icon and we'll generate the remaining sizes.&quot; data-og-host=&quot;app-manifest.firebaseapp.com&quot; data-og-source-url=&quot;https://app-manifest.firebaseapp.com/&quot; data-og-url=&quot;https://app-manifest.firebaseapp.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://app-manifest.firebaseapp.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://app-manifest.firebaseapp.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;App Manifest Generator&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;manifest.json Generate Icons The Web App Manifest allows for specifying icons of varying sizes. Upload a 512x512 image for the icon and we'll generate the remaining sizes.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;app-manifest.firebaseapp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;위 사이트를 이용하면 icon파일과 manifest.json 파일을 다운로드 받을 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;public폴더를 생성한 후 m&lt;/span&gt;anifest.json파일과 images폴더안에 icons폴더를 넣는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjhh0I/btqGpyuRwV4/WGx9LHrZihaCTWHoLCPNM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjhh0I/btqGpyuRwV4/WGx9LHrZihaCTWHoLCPNM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjhh0I/btqGpyuRwV4/WGx9LHrZihaCTWHoLCPNM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjhh0I%2FbtqGpyuRwV4%2FWGx9LHrZihaCTWHoLCPNM1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. meta tags를 적용한다&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gokulkrishh/awesome-meta-and-manifest&quot;&gt;https://github.com/gokulkrishh/awesome-meta-and-manifest&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1596767702999&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;gokulkrishh/awesome-meta-and-manifest&quot; data-og-description=&quot;⚡ Awesome collection of meta tags &amp;amp; manifest properties. - gokulkrishh/awesome-meta-and-manifest&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/gokulkrishh/awesome-meta-and-manifest&quot; data-og-url=&quot;https://github.com/gokulkrishh/awesome-meta-and-manifest&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/SOyiw/hyG2vky5xZ/VqAXynpvoiOUQM3tADK2l1/img.jpg?width=400&amp;amp;height=400&amp;amp;face=166_87_230_157&quot;&gt;&lt;a href=&quot;https://github.com/gokulkrishh/awesome-meta-and-manifest&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/gokulkrishh/awesome-meta-and-manifest&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/SOyiw/hyG2vky5xZ/VqAXynpvoiOUQM3tADK2l1/img.jpg?width=400&amp;amp;height=400&amp;amp;face=166_87_230_157');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;gokulkrishh/awesome-meta-and-manifest&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;⚡ Awesome collection of meta tags &amp;amp; manifest properties. - gokulkrishh/awesome-meta-and-manifest&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;기본적인 메타태그는 아래와 같다&lt;/p&gt;
&lt;pre id=&quot;code_1596767722053&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;meta charset='utf-8' /&amp;gt;
&amp;lt;meta http-equiv='X-UA-Compatible' content='IE=edge' /&amp;gt;
&amp;lt;meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' /&amp;gt;
&amp;lt;meta name='description' content='Description' /&amp;gt;
&amp;lt;meta name='keywords' content='Keywords' /&amp;gt;
&amp;lt;title&amp;gt;Next.js PWA Example&amp;lt;/title&amp;gt;

&amp;lt;link rel=&quot;manifest&quot; href=&quot;/manifest.json&quot; /&amp;gt;
&amp;lt;link href='/favicon-16x16.png' rel='icon' type='image/png' sizes='16x16' /&amp;gt;
&amp;lt;link href='/favicon-32x32.png' rel='icon' type='image/png' sizes='32x32' /&amp;gt;
&amp;lt;link rel=&quot;apple-touch-icon&quot; href=&quot;/apple-icon.png&quot;&amp;gt;&amp;lt;/link&amp;gt;
&amp;lt;meta name=&quot;theme-color&quot; content=&quot;#317EFB&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;메타태그는 _document.js를 생성해서 적용한다. 꼭 _document.js일 필요는 없다&lt;/p&gt;
&lt;pre id=&quot;code_1596767908714&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Document, { Html, Head, Main, NextScript } from &quot;next/document&quot;;
import { ServerStyleSheet } from &quot;styled-components&quot;;

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

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

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          &amp;lt;&amp;gt;
            {initialProps.styles}
            {sheet.getStyleElement()}
          &amp;lt;/&amp;gt;
        ),
      };
    } finally {
      sheet.seal();
    }
  }
  render() {
    return (
      &amp;lt;Html&amp;gt;
        &amp;lt;Head&amp;gt;
            &amp;lt;meta charset='utf-8' /&amp;gt;
            &amp;lt;meta http-equiv='X-UA-Compatible' content='IE=edge' /&amp;gt;
            &amp;lt;meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' /&amp;gt;
   		    &amp;lt;meta name='description' content='Description' /&amp;gt;
  		    &amp;lt;meta name='keywords' content='Keywords' /&amp;gt;
 		    &amp;lt;title&amp;gt;Next.js PWA Example&amp;lt;/title&amp;gt;

   			&amp;lt;link rel='manifest' href='/manifest.json' /&amp;gt;
      		&amp;lt;link href='/favicon-16x16.png' rel='icon' type='image/png' sizes='16x16' /&amp;gt;
      		&amp;lt;link href='/favicon-32x32.png' rel='icon' type='image/png' sizes='32x32' /&amp;gt;
      		&amp;lt;link rel='apple-touch-icon' href='/apple-icon.png'&amp;gt;&amp;lt;/link&amp;gt;
      		&amp;lt;meta name='theme-color' content='#317EFB' /&amp;gt;
        &amp;lt;/Head&amp;gt;
        &amp;lt;body&amp;gt;
          &amp;lt;Main /&amp;gt;
          &amp;lt;NextScript /&amp;gt;
        &amp;lt;/body&amp;gt;
      &amp;lt;/Html&amp;gt;
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;favicons는 아래의 사이트를 사용하여 생성한다&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.favicon-generator.org/&quot;&gt;https://www.favicon-generator.org/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1596768087935&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Favicon &amp;amp; App Icon Generator&quot; data-og-description=&quot;Upload an image (PNG to ICO, JPG to ICO, GIF to ICO) and convert it to a Windows favicon (.ico) and App Icons. Learn more about favicons.&quot; data-og-host=&quot;www.favicon-generator.org&quot; data-og-source-url=&quot;https://www.favicon-generator.org/&quot; data-og-url=&quot;https://www.favicon-generator.org/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.favicon-generator.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.favicon-generator.org/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Favicon &amp;amp; App Icon Generator&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Upload an image (PNG to ICO, JPG to ICO, GIF to ICO) and convert it to a Windows favicon (.ico) and App Icons. Learn more about favicons.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;www.favicon-generator.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;생성된 아이콘 파일들은 public폴더에 넣는다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. pwa가 적용되었는지 확인한다&lt;/h3&gt;
&lt;pre id=&quot;code_1596768179666&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm run build
npm run start&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 명령어를 통해 빌드 후 시작한다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;크롬에서 f12를 누른 후 lighthouse탭으로 이동하여 generate report를 클릭한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/97xqK/btqGiKYAyvO/rtoDOVnOkvW1Ubk1A2Gkk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/97xqK/btqGiKYAyvO/rtoDOVnOkvW1Ubk1A2Gkk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/97xqK/btqGiKYAyvO/rtoDOVnOkvW1Ubk1A2Gkk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F97xqK%2FbtqGiKYAyvO%2FrtoDOVnOkvW1Ubk1A2Gkk0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE3w0f/btqGiTNQH3V/954nfR1I8iwFKpbjlcYkvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE3w0f/btqGiTNQH3V/954nfR1I8iwFKpbjlcYkvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE3w0f/btqGiTNQH3V/954nfR1I8iwFKpbjlcYkvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE3w0f%2FbtqGiTNQH3V%2F954nfR1I8iwFKpbjlcYkvk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음 글에서는 다운로드 버튼을 만들어서 pwa를 간편하게 다운로드 받을 수 있는 방법에 대해 알아보겠습니다&lt;/p&gt;
&lt;p&gt;pwa 다운로드 버튼 생성하기 :&amp;nbsp; &lt;a href=&quot;https://jcon.tistory.com/172&quot;&gt;https://jcon.tistory.com/172&lt;/a&gt;&lt;/p&gt;</description>
      <category>Nextjs</category>
      <category>JS</category>
      <category>NEXT</category>
      <category>next js</category>
      <category>PWA</category>
      <category>적용</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/171</guid>
      <comments>https://jcon.tistory.com/171#entry171comment</comments>
      <pubDate>Fri, 7 Aug 2020 11:48:59 +0900</pubDate>
    </item>
    <item>
      <title>ios에서 pwa로 접속시 css 변경 / pwa로 시작되는지 감지 하는 방법</title>
      <link>https://jcon.tistory.com/170</link>
      <description>&lt;p&gt;pwa 설치가 가능하다는 것은 명시적으로 보여주기 위해 배너에 pwa 다운로드 버튼을 넣어서 노출시켰다. 이 배너는 pwa를 다운로드 받았을 시에 노출이 되지 않기는 원했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;안드로이드에서는 beforeinstallpropmt 이벤트를 통해 pwa로 시작되는지 웹페이지로 시작되는지 감지가 가능하지만 ios에서는 불가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1596697836579&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;window.addEventListener(&quot;beforeinstallprompt&quot;, (e) =&amp;gt; {
  e.preventDefault();
  deferredPrompt.current = e;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ios는 media쿼리를 사용하여 감지할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1596697992221&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@media all and (display-mode: standalone) {
  body {
    background-color: yellow;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;standalone은 manifest.json에서 설정한 값이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Tech Memo</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/170</guid>
      <comments>https://jcon.tistory.com/170#entry170comment</comments>
      <pubDate>Thu, 6 Aug 2020 16:19:07 +0900</pubDate>
    </item>
    <item>
      <title>github ssh 설정</title>
      <link>https://jcon.tistory.com/168</link>
      <description>&lt;p&gt;github에서 push나 pull 등을 할 때마다 비밀번호를 입력하는 것은 생각보다 번거로운 일이다. 그렇다고 글로벌로 user설정을 하기에도 애매한 경우가 있다. 이럴때에는 ssh설정을 통해서 해결 할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. ssh 키를 생성한다&lt;/p&gt;
&lt;pre id=&quot;code_1593577517601&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096 -C &quot;your_email@gmail.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;passphrase를 입력해야 키 파일이 생성된다.&lt;/p&gt;
&lt;p&gt;키 파일은 ~/.ssh 폴더에 생성된다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 생성된 키 값을 github - setting 페이지의 SSH keys에 붙여넣기 한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v5VPq/btqFfXxwyQE/oFgsdoHszdDEcfBCwrN6l1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v5VPq/btqFfXxwyQE/oFgsdoHszdDEcfBCwrN6l1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v5VPq/btqFfXxwyQE/oFgsdoHszdDEcfBCwrN6l1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv5VPq%2FbtqFfXxwyQE%2FoFgsdoHszdDEcfBCwrN6l1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3. 그 후 로컬에서 ssh 키를 등록한다&lt;/p&gt;
&lt;pre id=&quot;code_1593583950392&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#ssh-agent를 시작
$ eval $(ssh-agent)
#개인키를 등록
$ ssh-add ~/.ssh/id_rsa&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Git</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/168</guid>
      <comments>https://jcon.tistory.com/168#entry168comment</comments>
      <pubDate>Wed, 1 Jul 2020 15:15:02 +0900</pubDate>
    </item>
    <item>
      <title>다른 계정의 pripvate repo 작업하기</title>
      <link>https://jcon.tistory.com/167</link>
      <description>&lt;p&gt;협업을 위해 github에서 private repo를 만든 후 초대가 되었다.&lt;/p&gt;
&lt;p&gt;그후 클론을 하려 했으나 fatal: repository not found' error가 발생했다.&lt;/p&gt;
&lt;p&gt;이를 해결하려면 git 주소에 내 아이디를 입력해주어야 repo를 찾을 수 있다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1592890387898&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git clone https://git계정이름@github.com/cameronmcnz/private-github-repo.gi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@뒤의 부분은 github repo의 주소이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고 : &lt;a href=&quot;https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Five-ways-to-fix-Gits-fatal-repository-not-found-error&quot;&gt;https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Five-ways-to-fix-Gits-fatal-repository-not-found-error&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1592890361332&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Five ways to fix Git's 'fatal: repository not found' error - Coffee Talk: Java, News, Stories and Opinions&quot; data-og-description=&quot;There&amp;rsquo;s nothing worse than joining a new development team and eagerly cloning the existing source code repo only to run head first into Git&amp;rsquo;s &amp;lsquo;fatal: repository not found&amp;rsquo; error. For those who struggle with that problem, here are five potential fix&quot; data-og-host=&quot;www.theserverside.com&quot; data-og-source-url=&quot;https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Five-ways-to-fix-Gits-fatal-repository-not-found-error&quot; data-og-url=&quot;https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Five-ways-to-fix-Gits-fatal-repository-not-found-error&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFtPle/hyGvttYxx2/pF0kmNAxrvZKIDfSkJvk7K/img.jpg?width=1765&amp;amp;height=1178&amp;amp;face=0_0_1765_1178&quot;&gt;&lt;a href=&quot;https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Five-ways-to-fix-Gits-fatal-repository-not-found-error&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/Five-ways-to-fix-Gits-fatal-repository-not-found-error&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFtPle/hyGvttYxx2/pF0kmNAxrvZKIDfSkJvk7K/img.jpg?width=1765&amp;amp;height=1178&amp;amp;face=0_0_1765_1178');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Five ways to fix Git's 'fatal: repository not found' error - Coffee Talk: Java, News, Stories and Opinions&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;There&amp;rsquo;s nothing worse than joining a new development team and eagerly cloning the existing source code repo only to run head first into Git&amp;rsquo;s &amp;lsquo;fatal: repository not found&amp;rsquo; error. For those who struggle with that problem, here are five potential fix&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;www.theserverside.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Git</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/167</guid>
      <comments>https://jcon.tistory.com/167#entry167comment</comments>
      <pubDate>Tue, 23 Jun 2020 14:34:33 +0900</pubDate>
    </item>
    <item>
      <title>apache를 통해 두개 이상의 ssl 설정</title>
      <link>https://jcon.tistory.com/166</link>
      <description>&lt;p&gt;기존에 아파치를 통해서 ssl인증을 통해 https설정을 하였다.&lt;/p&gt;
&lt;p&gt;api서버를 띄운 후에 테스트를 하는 도중 크롬에서 api서버가 https가 아니기 때문에 보안상의 문제가 있다고 하며 값을 가져오지 못하는 이슈가 생겼다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 해결하는 방법은 크롬 보안 설정을 하거나 api서버에도 ssl인증을 하는 방법이 있다.&lt;/p&gt;
&lt;p&gt;실제 서비스를 생각한다면 당연히 후자의 방법으로 해야 문제가 없을 것으로 생각하여 삽질을 시작하였다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아파치 virtualHost 세팅을 생각해 보았을 때 virtualHost 포트를 하나 생성해서 기존처럼 proxypass를 통해 api서버로 보내면 될 것으로 생각하고 virtualHost *:3001, proxypass localhost:3001 설정을 해보았으나 작동하지 않았다. 아파치의 에러메세지를 확인해보니 이미 사용중인 포트라고 나와있었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아파치 포트와 node의 포트는 별개의 포트로 생각을 하였는데 둘이 충돌하는 것으로 보아 같은 포트라는 것을 알게 되었고, virtualHost의 포트를 변경함으로써 해결하게 되었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1590980837115&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Listen 80
Listen 444


NameVirtualHost *:80
&amp;lt;VirtualHost *:80&amp;gt;
    ServerName 서버아이피
    // http -&amp;gt; https로 리다이렉트
    Redirect / https://서버아이피
&amp;lt;/VirtualHost&amp;gt;

# SSL Virtual host add
&amp;lt;VirtualHost *:443&amp;gt;
    ServerName 서버아이피
    # ErrorLog /www/home/logs/error_log
    SSLEngine on
    SSLCertificateFile /etc/ssl/certificate.crt
    SSLCertificateKeyFile /etc/ssl/private/private.key
    SSLCertificateChainFile /etc/ssl/ca_bundle.crt
    
    ProxyPass / http://localhost:3000/
&amp;lt;/VirtualHost&amp;gt;

&amp;lt;VirtualHost *:444&amp;gt;
    ServerName 서버아이피 
    # ErrorLog /www/home/logs/error_log
    SSLEngine on
    SSLCertificateFile /etc/ssl/certificate.crt
    SSLCertificateKeyFile /etc/ssl/private/private.key
    SSLCertificateChainFile /etc/ssl/ca_bundle.crt

    ProxyPass / http://localhost:3001/
&amp;lt;/VirtualHost&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Deploy</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/166</guid>
      <comments>https://jcon.tistory.com/166#entry166comment</comments>
      <pubDate>Mon, 1 Jun 2020 12:08:18 +0900</pubDate>
    </item>
    <item>
      <title>아파치 http https로 리다이렉트</title>
      <link>https://jcon.tistory.com/165</link>
      <description>&lt;pre id=&quot;code_1590656443654&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NameVirtualHost *:80
&amp;lt;VirtualHost *:80&amp;gt;
    ServerName 서버주소
    Redirect / https://서버주소
&amp;lt;/VirtualHost&amp;gt;

# SSL Virtual host add
&amp;lt;VirtualHost *:443&amp;gt;
    ServerName 서버주소
    # ErrorLog /www/home/logs/error_log
    SSLEngine on
    SSLCertificateFile certificate.crt파일경로
    SSLCertificateKeyFile private.key파일경로
    SSLCertificateChainFile ca_bundle.crt파일경로
    DocumentRoot 루트디렉토리경로

    ProxyPass / http://localhost:3000/
&amp;lt;/VirtualHost&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ssl 설정 후 proxyPass를 통해 node서버가 돌아가고 있는 localhost:3000으로 리다이렉트 시킴&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Deploy</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/165</guid>
      <comments>https://jcon.tistory.com/165#entry165comment</comments>
      <pubDate>Thu, 28 May 2020 18:02:05 +0900</pubDate>
    </item>
    <item>
      <title>next + pwa 홈화면에 추가 버튼 생성</title>
      <link>https://jcon.tistory.com/164</link>
      <description>&lt;p&gt;beforinstallpropt라는 wepAPI를 사용하여 클릭시에 홈화면에 추가 할 수 있도록 했다. 다만 ios에서는 불가능하고 안드로이드만 가능하다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;useEffect에서 이벤트를 생성한다&lt;/p&gt;
&lt;pre id=&quot;code_1590653526367&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  let installPrompt = null;
  
  useEffect(() =&amp;gt; {
    console.log('Listening for Install prompt');
    window.addEventListener('beforeinstallprompt', (e) =&amp;gt; {
      // For older browsers
      e.preventDefault();
      console.log('Install Prompt fired');
      installPrompt = e;
      // See if the app is already installed, in that case, do nothing
      if (
        (window.matchMedia &amp;amp;&amp;amp; window.matchMedia('(display-mode: standalone)').matches) ||
        window.navigator.standalone === true
      ) {
        return false;
      }
    });
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;클릭시에 실행할 함수&lt;/p&gt;
&lt;pre id=&quot;code_1590653534450&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const installApp = async () =&amp;gt; {
    if (!installPrompt) {
      alert('이미 다운로드 했습니다')
      return false;
    }

    installPrompt.prompt();

    let outcome = await installPrompt.userChoice;

    if (outcome.outcome == 'accepted') {
      console.log('App Installed');

    } else {
      console.log('App not installed');
    }
    // Remove the event reference
    installPrompt = null;
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;참고 : &lt;a href=&quot;https://blog.anam.co/progressive-web-apps-with-create-react-app/&quot;&gt;https://blog.anam.co/progressive-web-apps-with-create-react-app/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Nextjs</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/164</guid>
      <comments>https://jcon.tistory.com/164#entry164comment</comments>
      <pubDate>Thu, 28 May 2020 17:14:24 +0900</pubDate>
    </item>
    <item>
      <title>next + pwa 설정</title>
      <link>https://jcon.tistory.com/163</link>
      <description>&lt;p&gt;next-pwa 설치&lt;/p&gt;
&lt;pre id=&quot;code_1590653267948&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install next-pwa&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;`next.config.js` 파일 생성&lt;/p&gt;
&lt;pre id=&quot;code_1590653358892&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const withPWA = require('next-pwa')
 
module.exports = withPWA({
    pwa: {
        dest: 'public'
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;meta tag 생성&lt;/p&gt;
&lt;p&gt;예제:&lt;/p&gt;
&lt;pre id=&quot;code_1590653401179&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;meta charset='utf-8' /&amp;gt;
&amp;lt;meta http-equiv='X-UA-Compatible' content='IE=edge' /&amp;gt;
&amp;lt;meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' /&amp;gt;
&amp;lt;meta name='description' content='Description' /&amp;gt;
&amp;lt;meta name='keywords' content='Keywords' /&amp;gt;
&amp;lt;title&amp;gt;Next.js PWA Example&amp;lt;/title&amp;gt;

&amp;lt;link rel=&quot;manifest&quot; href=&quot;/manifest.json&quot; /&amp;gt;
&amp;lt;link href='/favicon-16x16.png' rel='icon' type='image/png' sizes='16x16' /&amp;gt;
&amp;lt;link href='/favicon-32x32.png' rel='icon' type='image/png' sizes='32x32' /&amp;gt;
&amp;lt;link rel=&quot;apple-touch-icon&quot; href=&quot;/apple-icon.png&quot;&amp;gt;&amp;lt;/link&amp;gt;
&amp;lt;meta name=&quot;theme-color&quot; content=&quot;#317EFB&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고 : &lt;a href=&quot;https://itnext.io/pwa-with-next-js-create-next-app-in-2020-%EF%B8%8F-9ee0e1a6313d&quot;&gt;https://itnext.io/pwa-with-next-js-create-next-app-in-2020-%EF%B8%8F-9ee0e1a6313d&lt;/a&gt;&lt;/p&gt;</description>
      <category>Nextjs</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/163</guid>
      <comments>https://jcon.tistory.com/163#entry163comment</comments>
      <pubDate>Thu, 28 May 2020 17:10:31 +0900</pubDate>
    </item>
    <item>
      <title>apache + node 설정</title>
      <link>https://jcon.tistory.com/162</link>
      <description>&lt;p&gt;&lt;span&gt;브라우저에서 서버로 접속시에 아파치를 거쳐서 node의 포트로 넘겨주기위해서 아래의 설정파일에 명령어 추가&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;/etc/httpd/conf/httpd.conf&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1590640169724&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ProxyPass       /       http://localhost:3000/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Deploy</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/162</guid>
      <comments>https://jcon.tistory.com/162#entry162comment</comments>
      <pubDate>Thu, 28 May 2020 17:06:37 +0900</pubDate>
    </item>
    <item>
      <title>centos 8 vscode ssh 접속 설정</title>
      <link>https://jcon.tistory.com/161</link>
      <description>&lt;p&gt;vscode의 ssh 익스텐션을 설치하면 vscode를 통해 원격 접속이 가능하고 폴더 구조와 파일 수정을 vi를 사용하지 않고 vscode를 통해 가능하여 애용하는 익스텐션이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;새로운 서버에 개발환경을 구축해야하는 상황이 되어 기본적인 node mysql apache설치를 끝낸 후 vscode를 통해 ssh 접속을 하려 했으나 아래와 같은 에러가 나타났다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;vscode&amp;nbsp;remote&amp;nbsp;ssh&amp;nbsp;the&amp;nbsp;vs&amp;nbsp;code&amp;nbsp;server&amp;nbsp;failed&amp;nbsp;to&amp;nbsp;start&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해당 에러를 검색하고 해결하기 위해 시도를 해보았지만 실행이 되지 않았다. 찬찬히 처음부터 진행을 해보기로 하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N3J9G/btqErKZJBDD/fe0DIFFc3GoMvnKJyLEkQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N3J9G/btqErKZJBDD/fe0DIFFc3GoMvnKJyLEkQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N3J9G/btqErKZJBDD/fe0DIFFc3GoMvnKJyLEkQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN3J9G%2FbtqErKZJBDD%2Ffe0DIFFc3GoMvnKJyLEkQ0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://code.visualstudio.com/docs/remote/troubleshooting#_installing-a-supported-ssh-server&quot;&gt;https://code.visualstudio.com/docs/remote/troubleshooting#_installing-a-supported-ssh-server&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;우선 위의 안내대로 centos에 ssh를 설치 후에 재시도 해보았으나 역시 접속이 되지 않았다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pSXgr/btqEtFbAVYR/GpyWusHMlxGXnKEJcXvEHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pSXgr/btqEtFbAVYR/GpyWusHMlxGXnKEJcXvEHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pSXgr/btqEtFbAVYR/GpyWusHMlxGXnKEJcXvEHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpSXgr%2FbtqEtFbAVYR%2FGpyWusHMlxGXnKEJcXvEHK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;출처 : &lt;a href=&quot;https://code.visualstudio.com/docs/remote/linux&quot;&gt;https://code.visualstudio.com/docs/remote/linux&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;vscode ssh를 사용하기 위해 안내하는 내용이다. centos 8을 사용하기 때문에 &lt;span&gt;libc6 libstdc++6 python-minimal ca-certificates tar의 버전과 설치 유무를 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;tar가 설치가 되어 있지 않았고 설치 후에 정상적으로 접속이 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;span&gt;glibc 버전 확인 방법&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1590558004810&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# getconf -a | grep libc
GNU_LIBC_VERSION                   glibc 2.3.4&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;libstdc++6&lt;span&gt; 버전 확인 방법&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1590558090678&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; # /sbin/ldconfig -p | grep stdc++
    libstdc++.so.6 (libc6) =&amp;gt; /usr/lib/libstdc++.so.6&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;tar 설치 방법&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a href=&quot;https://www.cyberciti.biz/faq/install-tar-on-rhel-centos-6-7-8-linux-yum-command/&quot;&gt;https://www.cyberciti.biz/faq/install-tar-on-rhel-centos-6-7-8-linux-yum-command/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Dev Tools</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/161</guid>
      <comments>https://jcon.tistory.com/161#entry161comment</comments>
      <pubDate>Wed, 27 May 2020 14:43:24 +0900</pubDate>
    </item>
    <item>
      <title>react native window 개발 환경 세팅</title>
      <link>https://jcon.tistory.com/159</link>
      <description>&lt;p&gt;개발환경 세팅에 대해 상세하게 설명이 되어있다. cmd를 사용하여 설치한 후 git bash에서 실행 시에 오류가 나타났으나 이는 vscode를 재실행 하지 않아서 나타난 오류였다. wsl은 expo 사용시에 충돌이 일어나는 일이 잦아서 세팅을 보류하였다&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://dev-yakuza.github.io/ko/react-native/install-on-windows/&quot;&gt;https://dev-yakuza.github.io/ko/react-native/install-on-windows/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1590471364675&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;윈도우(Windows)에 react native 개발 환경 구축하기&quot; data-og-description=&quot;react-native로 앱을 개발하기 위해 윈도우(Windows)에 개발 환경을 구축해 보고, react-native로 생성한 프로젝트가 잘 동작하는지 확인해 봅니다.&quot; data-og-host=&quot;dev-yakuza.github.io&quot; data-og-source-url=&quot;https://dev-yakuza.github.io/ko/react-native/install-on-windows/&quot; data-og-url=&quot;https://dev-yakuza.github.io/ko/react-native/install-on-windows/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/FEtOq/hyGbkwU1FW/YMf0cKkM2kGAbhFpzYkGDK/img.jpg?width=640&amp;amp;height=426&amp;amp;face=0_0_640_426,https://scrap.kakaocdn.net/dn/cDEme3/hyGbr3Rzvx/yCMlak52kHstn7DQzV88g0/img.png?width=536&amp;amp;height=428&amp;amp;face=0_0_536_428,https://scrap.kakaocdn.net/dn/mxrtK/hyGbjLxMsi/VyNmJPkh4MYcAOZAXIvKoK/img.png?width=522&amp;amp;height=418&amp;amp;face=0_0_522_418&quot;&gt;&lt;a href=&quot;https://dev-yakuza.github.io/ko/react-native/install-on-windows/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev-yakuza.github.io/ko/react-native/install-on-windows/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/FEtOq/hyGbkwU1FW/YMf0cKkM2kGAbhFpzYkGDK/img.jpg?width=640&amp;amp;height=426&amp;amp;face=0_0_640_426,https://scrap.kakaocdn.net/dn/cDEme3/hyGbr3Rzvx/yCMlak52kHstn7DQzV88g0/img.png?width=536&amp;amp;height=428&amp;amp;face=0_0_536_428,https://scrap.kakaocdn.net/dn/mxrtK/hyGbjLxMsi/VyNmJPkh4MYcAOZAXIvKoK/img.png?width=522&amp;amp;height=418&amp;amp;face=0_0_522_418');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;윈도우(Windows)에 react native 개발 환경 구축하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;react-native로 앱을 개발하기 위해 윈도우(Windows)에 개발 환경을 구축해 보고, react-native로 생성한 프로젝트가 잘 동작하는지 확인해 봅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;dev-yakuza.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React Native</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/159</guid>
      <comments>https://jcon.tistory.com/159#entry159comment</comments>
      <pubDate>Tue, 26 May 2020 14:48:04 +0900</pubDate>
    </item>
    <item>
      <title>webpack 간단 설명</title>
      <link>https://jcon.tistory.com/157</link>
      <description>&lt;pre id=&quot;code_1586260145221&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const path = require(&quot;path&quot;);

module.exports = {
  name: &quot;wordrelay-setting&quot;,
  mode: &quot;development&quot;, // 실서비스 : production
  devtool: &quot;eval&quot;,
  resolve: {
    extensions: [&quot;.js&quot;, &quot;.jsx&quot;],
  },

  entry: {
    app: [&quot;./client&quot;],
  }, //입력

  module: {
    rules: [
      {
        test: /\.jsx?/,
        loader: &quot;babel-loader&quot;,
        options: {
          presets: [&quot;@babel/preset-env&quot;, &quot;@babel/preset-react&quot;],
          plugins: [&quot;@babel/plugin-proposal-class-properties&quot;],
        },
      },
    ],
  }, //entry에 적용할 것들
  output: {
    path: path.join(__dirname, &quot;dist&quot;),
    filename: &quot;app.js&quot;,
  }, //출력
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;name :&amp;nbsp; 해당 설정의 이름을 나타냄&lt;/p&gt;
&lt;p&gt;mode : 개발 모드와 프로덕트 모드가 있다&lt;/p&gt;
&lt;p&gt;devtool : 빠르게라는 뜻&lt;/p&gt;
&lt;p&gt;resolve : 어떤 확장자를 바꿀지 정한다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;entry : 해당 파일 내에 import 또는 require 되어있는 파일을 웹팩이 자동으로 추적하기 때문에 최상위 파일만 넣어도 된다&lt;/p&gt;
&lt;p&gt;module : entry에 적용할 바벨을 설정&lt;/p&gt;
&lt;p&gt;&amp;nbsp; - babel-loader : 웹팩에서 바벨을 적용하기 위해 필요&lt;/p&gt;
&lt;p&gt;&amp;nbsp; - &lt;span&gt;@babel/preset-env : 브라우저의 환경에 맞게 변경해주는 바벨&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp; - &lt;/span&gt;&lt;span&gt;@babel/preset-react : jsx문법을 변경하기 위한 바벨&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp; -&amp;nbsp;&lt;/span&gt;&lt;span&gt;@babel/plugin-proposal-class-properties : 클래스 컴포넌트에서 constructor없이 state를 사용할 때 필요한 바벨&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;output : 어느 곳에 파일을 생성할지 정한다. node의 path 명령어를 사용하여 경로를 정하는 것이 좋다&lt;/p&gt;</description>
      <category>Tech Memo</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/157</guid>
      <comments>https://jcon.tistory.com/157#entry157comment</comments>
      <pubDate>Tue, 7 Apr 2020 20:52:44 +0900</pubDate>
    </item>
    <item>
      <title>함수형 프로그래밍(functional programming)이란?</title>
      <link>https://jcon.tistory.com/155</link>
      <description>&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;면접을 보러 다니면서 함수형 프로그래밍이란 무엇인지에 대해 질문을 많이 받았다.&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;느낌적인 느낌이 아닌 좀 더 정확하게 알고 싶어 알아 보았다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;함수형 프로그래밍은 무엇일까?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;프로그램 패러다임의 한 종류이며, 코딩 스타일이다.&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;가장 중요한 것은 인풋에 대한 아웃풋이 있으면 이에 대한 데이터 흐름을 정해야 한다는 것이다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;not functional&lt;/p&gt;
&lt;pre id=&quot;code_1585643347221&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let name = 'Aairon'
let greeting = 'Hi, I'm '

console.log(greeting + name)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;인사를 하는 문구와 이름을 넣어 콘솔에 나타나게 하는 코드이다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;일종의 명령형 프로그래밍이라고 볼수있다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;functional&lt;/p&gt;
&lt;pre id=&quot;code_1585643512465&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function greet(name) {
  return 'Hi, I'm ' + name
}

greet('Aairon')&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드를 함수형으로 표현하면 이와 같다. name을 인풋으로 받아서 아웃풋으로 나타낸다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;함수형 프로그래밍의 특징 중 하나는 사이드이펙트를 피하기 위해 pure fucntion이어야한다는 것이다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;pure function이란?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;함수가 인풋만을 받아서 아웃풋을 계산하고 return 해야 한다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;Not pure&lt;/p&gt;
&lt;pre id=&quot;code_1585643866172&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let name = 'Aairon'

function greet() {
  console.log('Hi, I'm ' + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 인풋을 받지 않고 함수 밖에 있는 변수 또는 전역 변수를 사용하는 것은 pure하지 않다. 또한 return이 없는 것 또한 pure function이라고 보기 힘들다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;pure&lt;/p&gt;
&lt;pre id=&quot;code_1585644064531&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function greet(name) {
  return 'Hi, I'm ' + name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;이와 같이 아웃풋을 return하고, 아웃풋에 영향을 주는 인풋이 있어야 한다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;또 다른 특징으로 higer-order function이 있다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;font-size: 1.25em;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;higher-order function이란?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;다른 함수를 인풋으로 받거나 함수를 아웃풋으로 return하는 것이다. 함수형 프로그래밍에서 많이 사용한다&lt;/p&gt;
&lt;pre id=&quot;code_1585644402159&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function makeAdjectifier(adjective) {
  return function (string) {
    return adjective + ' ' + string;
  }
}

let coolifier = makeAdjectifier('cool')
coolifier('conference'); // 'cool conference'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;coolifier에 makeAdjectifier의 반환되는 함수를 할당함으로써 coolifier를 실행 시 'cool conference'가 반환 되도록 하였다&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;출처 :&amp;nbsp;&lt;a href=&quot;https://youtu.be/e-5obm1G_FY&quot;&gt;https://youtu.be/e-5obm1G_FY&lt;/a&gt;&lt;/p&gt;</description>
      <category>Tech Memo</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/155</guid>
      <comments>https://jcon.tistory.com/155#entry155comment</comments>
      <pubDate>Tue, 31 Mar 2020 18:05:21 +0900</pubDate>
    </item>
    <item>
      <title>expo 배포하기</title>
      <link>https://jcon.tistory.com/151</link>
      <description>&lt;pre id=&quot;code_1584454967346&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;expo fetch:android:keystore&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앱스토어 계정은 매년 비용을 지불해야 해서 플레이스토어만 개발자 계정을 만들었다&lt;/p&gt;
&lt;p&gt;그에 따라 배포는 안드로이드만 진행을 하고 ios는 설정만 해두었다&lt;/p&gt;
&lt;pre id=&quot;code_1584454097817&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;expo&quot;: {
    &quot;name&quot;: &quot;TODO LIST&quot;, //어플의 이름
    &quot;slug&quot;: &quot;MyfirstApp&quot;,
    &quot;privacy&quot;: &quot;public&quot;,
    &quot;sdkVersion&quot;: &quot;36.0.0&quot;,
    &quot;platforms&quot;: [
      &quot;ios&quot;,
      &quot;android&quot;,
      &quot;web&quot;
    ],
    &quot;version&quot;: &quot;1.0.1&quot;,
    &quot;orientation&quot;: &quot;portrait&quot;,
    &quot;splash&quot;: {
      &quot;image&quot;: &quot;./assets/splash.png&quot;,
      &quot;resizeMode&quot;: &quot;contain&quot;,
      &quot;backgroundColor&quot;: &quot;#6830cf&quot;
    },
    &quot;updates&quot;: {
      &quot;fallbackToCacheTimeout&quot;: 0
    },
    &quot;assetBundlePatterns&quot;: [
      &quot;**/*&quot;
    ],
    &quot;ios&quot;: {
      &quot;bundleIdentifier&quot;: &quot;me.jch.todo&quot;, //도메인이라고 생각하면 된다.
      &quot;icon&quot;: &quot;./assets/ios-icon.png&quot;,
      &quot;supportsTablet&quot;: true
    },
    &quot;android&quot;: {
      &quot;versionCode&quot;: 2,
      &quot;package&quot;: &quot;me.jch.todo&quot;, //도메인이라고 생각하면 된다.
      &quot;icon&quot;: &quot;./assets/android-icon.png&quot;,
      &quot;adaptiveIcon&quot;: {
        &quot;backgroundColor&quot;: &quot;#6830cf&quot;,
        &quot;foregroundImage&quot;: &quot;./assets/android-foreground.png&quot;
      }
    },
    &quot;description&quot;: &quot;&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DfoNS/btqCLNqft1C/HvYY0PRn1u8vdMkzUTQYM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DfoNS/btqCLNqft1C/HvYY0PRn1u8vdMkzUTQYM1/img.png&quot; data-filename=&quot;ios-icon.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot; style=&quot;width: 38.9164%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DfoNS/btqCLNqft1C/HvYY0PRn1u8vdMkzUTQYM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDfoNS%2FbtqCLNqft1C%2FHvYY0PRn1u8vdMkzUTQYM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PBWcb/btqCOzqI75d/CRyFK1vKW4tEaKkL4u4RAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PBWcb/btqCOzqI75d/CRyFK1vKW4tEaKkL4u4RAK/img.png&quot; data-filename=&quot;android-icon.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; style=&quot;width: 38.9164%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PBWcb/btqCOzqI75d/CRyFK1vKW4tEaKkL4u4RAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPBWcb%2FbtqCOzqI75d%2FCRyFK1vKW4tEaKkL4u4RAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw8bg6/btqCKf1Zg5k/IvLkvK82p958Hhf1A5qqEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw8bg6/btqCKf1Zg5k/IvLkvK82p958Hhf1A5qqEk/img.png&quot; data-filename=&quot;splash.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2436&quot; style=&quot;width: 19.8416%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw8bg6/btqCKf1Zg5k/IvLkvK82p958Hhf1A5qqEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw8bg6%2FbtqCKf1Zg5k%2FIvLkvK82p958Hhf1A5qqEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1242&quot; height=&quot;2436&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;ios-icon /&amp;nbsp; android-icon /&amp;nbsp; splash&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;ios-icon의 경우 등록하면 애플에서 라운드 처리를 해준다&lt;/p&gt;
&lt;p&gt;android-icon은 운영체제에 따라서 동그란 아이콘과 네모난 아이콘으로 나타나게 되는데&amp;nbsp;android-foreground라는 파일을 통해 설정 할 수 있다. 이 그림 파일은 가운데의 체크 모양 표시가 있고 나머지 부분은 투명하게 설정 후 배경색을 지정해 줌으로써 설정이 가능하다&lt;/p&gt;
&lt;p&gt;splash는 어플의 로딩화면 페이지를 나타낸다. 기기마다 크기가 다르기 때문에 배경색을 설정해 주는 것이 좋다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇게 설정을 마친 후 아래 명령어를 치면 안드로이드 빌드 파일이 생성된다&lt;/p&gt;
&lt;pre id=&quot;code_1584454689785&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;expo build:android&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 빌드를 시도하면 expo 아이디와 비밀번호를 입력하라는 메세지가 나타난다&lt;/p&gt;
&lt;p&gt;로그인 후에 ` 1) Let Expo handle the process! ` 를 선택하면 빌드가 진행 된다.&lt;/p&gt;
&lt;p&gt;빌드가 완료되면 안드로이드 앱을 받을 수 있는 링크가 생성된다. 해당 링크로 이동하여 파일을 받는다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;앱 배포 관련 정보는 빌드 후가 아니면 받을 수 없기 때문에 꼭 받아 두도록 한다. 명령어는 아래와 같다&lt;/p&gt;
&lt;pre id=&quot;code_1584454987856&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;expo fetch:android:keystore&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;안드로이드 파일을 다운 받은 후 구글 플레이 콘솔로 이동하여 `어플리케이션 만들기`를 선택한다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. 왼쪽 메뉴에서 `앱 버전`을 선택한 후 프로덕션 트렉에서 프로덕션 관리로 들어간다&lt;/p&gt;
&lt;p&gt;다운로드 받았던 apk 파일을 업로드 한다.&lt;/p&gt;
&lt;p&gt;하단에 출시 노트(이번 버전의 새로운 기능)에 적당한 내용을 입력한다&lt;/p&gt;
&lt;p&gt;저장 후 검토 버튼을 입력한다. expo를 이용하면 최적화가 되지 않은 apk라는 경고 문구가 나오는데 업로드 하는 데에는 문제가 없다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 제목과 설명을 입력하고, 아이콘 이미지, 어플 화면 이미지, 그래픽 이미지를 등록한다&lt;/p&gt;
&lt;p&gt;개인정보처리방침도 등록한 후 임시저장한다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3. 콘텐츠 등급으로 이동한 후 이메일을 입력하고 카테고리를 선택한다.&lt;/p&gt;
&lt;p&gt;그 후 설문을 작성한다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. `앱 버전`에서 프로덕션 관리로 들어가 하단에 검토 버튼을 누른 후 출시 버튼을 누르면 끝&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React Native</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/151</guid>
      <comments>https://jcon.tistory.com/151#entry151comment</comments>
      <pubDate>Wed, 18 Mar 2020 00:34:48 +0900</pubDate>
    </item>
    <item>
      <title>GIT refusing to merge unrelated histories 오류 해결</title>
      <link>https://jcon.tistory.com/150</link>
      <description>&lt;p&gt;해당 오류는 이미 생성되어 있는 git repo에 clone하지 않은 작업물을 덮어 쓸 때 나타나는 오류이다&lt;/p&gt;
&lt;p&gt;일반적인 git 사용으로는 보기 힘든 에러이지만 사이드 프로젝트를 진행 중&lt;/p&gt;
&lt;p&gt;expo에 관한 설정 때문에 node_modules와 초기 세팅을 다시 해야하는 상황이 발생했고&lt;/p&gt;
&lt;p&gt;해당 repo를 clone하지 않고 새로운 expo init을 하면서 발생한 오류였다&lt;/p&gt;
&lt;p&gt;git push --force 등으로 밀어 버릴수도 있지만 기존에 심은 잔디가 날라가게 두기 싫어서 해결 방법을 찾아보았다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1581267509985&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git pull --allow-unrelated-histories&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;터미널에 위의 명령어를 입력하게 되면 관련이 없는 커밋이 push할 수 있게 변경된다&lt;/p&gt;</description>
      <category>Tech Memo</category>
      <author>vitnal</author>
      <guid isPermaLink="true">https://jcon.tistory.com/150</guid>
      <comments>https://jcon.tistory.com/150#entry150comment</comments>
      <pubDate>Mon, 10 Feb 2020 01:59:21 +0900</pubDate>
    </item>
  </channel>
</rss>