<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>이수재 블로그</title>
    <link>https://soojae.tistory.com/</link>
    <description>코드는 효율적으로, 공부는 비효율적으로</description>
    <language>ko</language>
    <pubDate>Thu, 2 Jul 2026 15:55:46 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>SooJae</managingEditor>
    <image>
      <title>이수재 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3114746/attach/b6490e6d0fa544d48d94778205481f90</url>
      <link>https://soojae.tistory.com</link>
    </image>
    <item>
      <title>[Proxmox] 설치 후 추천 설정 작업</title>
      <link>https://soojae.tistory.com/110</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/spoKt/btsKYAvE54h/9GWzJlQ1Z4aH4kvYKeOiJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/spoKt/btsKYAvE54h/9GWzJlQ1Z4aH4kvYKeOiJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/spoKt/btsKYAvE54h/9GWzJlQ1Z4aH4kvYKeOiJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FspoKt%2FbtsKYAvE54h%2F9GWzJlQ1Z4aH4kvYKeOiJ1%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; alt=&quot;proxmox logo&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;484&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. No valid subscription 알람창 제거&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxmox를 실행하면 다음과 같은 알람창이 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qryvz/btsKZvmXRyh/7tRTvRz8PjpuIfKjRnuG4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qryvz/btsKZvmXRyh/7tRTvRz8PjpuIfKjRnuG4K/img.png&quot; data-alt=&quot;You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qryvz/btsKZvmXRyh/7tRTvRz8PjpuIfKjRnuG4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQryvz%2FbtsKZvmXRyh%2F7tRTvRz8PjpuIfKjRnuG4K%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; alt=&quot;sub error&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;219&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.&lt;/figcaption&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. proxmoxlib.js 파일 편집&lt;/p&gt;
&lt;pre id=&quot;code_1732810741923&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;nano /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Ctrl + w (찾기) 를 누른 후 No valid subscription 를 적으면 해당 위치로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;아래 캡처에서 빨간색으로 표시된 부분의 코드를 기존의 `!==`&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에서 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;`===`&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;로 수정해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/niIzO/btsKZNUXCiE/0Y9XC2oMlCsQvszHryL9rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/niIzO/btsKZNUXCiE/0Y9XC2oMlCsQvszHryL9rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/niIzO/btsKZNUXCiE/0Y9XC2oMlCsQvszHryL9rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FniIzO%2FbtsKZNUXCiE%2F0Y9XC2oMlCsQvszHryL9rk%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; alt=&quot;remove sub error&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;544&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 저장하면 더 이상 No valid subscription 알람이 나타나지 않는다.&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;2. 무료 레포지토리 설정 + 패키지 업데이트&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치하자마자 `$ apt-get update`를 하면 에러가 발생한다.&amp;nbsp; Proxmox VE 레포지토리는 기본적으로 엔터프라이즈(유료 구독)로 되어있기 때문이다. 그래서 무료로 사용자용 레포지토리(pve-no-subscription)를 이용해야한다.&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;Proxmox 콘솔로 접속 후, 아래 화면에서 Shell 부분을 클릭한 후에 하나씩 작업해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7LThR/btsKZMars8M/Hl87VVm9aKpDSBHlPge0g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7LThR/btsKZMars8M/Hl87VVm9aKpDSBHlPge0g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7LThR/btsKZMars8M/Hl87VVm9aKpDSBHlPge0g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7LThR%2FbtsKZMars8M%2FHl87VVm9aKpDSBHlPge0g1%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; alt=&quot;console view&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;236&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;313&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;1. sources.list.d 폴더 내의 파일들 제거 (warning 방지)&lt;/p&gt;
&lt;pre id=&quot;code_1732810807378&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rm /etc/apt/sources.list.d/*&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;2.&amp;nbsp; 다음 명령어로 레포지토리 추가&lt;/p&gt;
&lt;pre id=&quot;code_1732810827063&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo &quot;deb [arch=amd64] http://download.proxmox.com/debian/pve bookworm pve-no-subscription&quot; &amp;gt; /etc/apt/sources.list.d/pve-no-subscription.list&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. apt 업데이트&lt;/p&gt;
&lt;pre id=&quot;code_1732810950021&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apt update &amp;amp;&amp;amp; apt full-upgrade&lt;/code&gt;&lt;/pre&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. 네트워크 변경&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유기 변경으로 인해 게이트웨이(공유기 주소)가 변경 되거나, 설치시 네트워크를 잘못 입력했을 경우 네트워크를 변경하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. `$ nano /etc/network/interfaces`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. address와 gateway를 변경 (아래 캡쳐 참고)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVDqUv/btsKYCmKTZ5/s3nA2SMc1od5tnQSH6kO0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVDqUv/btsKYCmKTZ5/s3nA2SMc1od5tnQSH6kO0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVDqUv/btsKYCmKTZ5/s3nA2SMc1od5tnQSH6kO0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVDqUv%2FbtsKYCmKTZ5%2Fs3nA2SMc1od5tnQSH6kO0k%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; alt=&quot;access network&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;379&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;415&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;참고로 필자는 iptime -&amp;gt; asus 공유기로 교체했기 때문에 `address: 192.168.50.100/24`, `gateway: 192.168.50.1` 로 변경했다.&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;4. DNS 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷을 사용하는 통신사를 변경했을때, DNS를 변경해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. dns를 관리하는 resolv.conf 파일에 접근&lt;/p&gt;
&lt;pre id=&quot;code_1732895112462&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nano /etc/resolv.conf&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;2. nameserver 등록&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hi4Fl/btsKZDrEI4n/GW0yql2tcnJsLr1LAHiZSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hi4Fl/btsKZDrEI4n/GW0yql2tcnJsLr1LAHiZSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hi4Fl/btsKZDrEI4n/GW0yql2tcnJsLr1LAHiZSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHi4Fl%2FbtsKZDrEI4n%2FGW0yql2tcnJsLr1LAHiZSk%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; alt=&quot;change dns&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;379&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;415&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;h3 data-ke-size=&quot;size23&quot;&gt;참고 ) 통신사별 네임서버 목록&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;KT&lt;/b&gt;&lt;br /&gt;- 기본 DNS 서버 : 168.126.63.1 &lt;br /&gt;- 보조 DNS 서버 : 168.126.63.2&lt;br /&gt;&lt;br /&gt;&lt;b&gt;SKT&lt;br /&gt;&lt;/b&gt;- 기본 DNS 서버 : 219.250.36.130&amp;nbsp;&lt;br /&gt;- 보조 DNS 서버 : 210.220.163.82&lt;br /&gt;&lt;br /&gt;&lt;b&gt;LG&lt;br /&gt;&lt;/b&gt;- 기본 DNS 서버 : 164.124.101.2 &lt;br /&gt;- 보조 DNS 서버 : 203.248.252.2&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Google&lt;/b&gt;&lt;br /&gt;- 기본 DNS 서버 : 8.8.8.8 &lt;br /&gt;- 보조 DNS 서버 : 8.8.4.4&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 유저 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 구성 시 보안을 강화하기 위해 루트(root) 계정으로 직접 SSH 접속하는 대신, 별도의 일반 사용자를 생성하고 SSH 접속을 설정하는 방식으로 접근하자.&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;proxmox 서버에 접속해서 유저 생성&lt;/p&gt;
&lt;pre id=&quot;code_1732894790528&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ adduser jerry
$ apt install sudo
$ groupadd sudoers
$ usermod -g sudoers jerry
$ EDITOR=nano visudo
$ su jerry&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;로컬에서 ssh 공개키를 서버로 복사&lt;/p&gt;
&lt;pre id=&quot;code_1732894858135&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh-copy-id root@192.168.50.100

/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
root@192.168.0.12's password:

Number of key(s) added:        1

Now try logging into the machine, with:   &quot;ssh 'root@192.168.0.12'&quot;
and check to make sure that only the key(s) you wanted were added.&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;`$ nano /etc/ssh/sshd_config`파일 에서 비밀번호 인증 및 루트 계정 로그인 방식을 차단&lt;/p&gt;
&lt;pre id=&quot;code_1732894882288&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no&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;ssh 서비스 리스타트&lt;/p&gt;
&lt;pre id=&quot;code_1732894897328&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo systemctl restart ssh&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxmox(PVE) 8 설치 후 해야할 일 - &lt;a href=&quot;https://velog.io/@wooadev/ProxmoxPVE-8-%EC%84%A4%EC%B9%98-%ED%9B%84-%ED%95%B4%EC%95%BC-%ED%95%A0-%EC%9D%BC-No-valid-subscription-%ED%8C%9D%EC%97%85-%EC%97%86%EC%95%A0%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@wooadev/ProxmoxPVE-8-%EC%84%A4%EC%B9%98-%ED%9B%84-%ED%95%B4%EC%95%BC-%ED%95%A0-%EC%9D%BC-No-valid-subscription-%ED%8C%9D%EC%97%85-%EC%97%86%EC%95%A0%EA%B8%B0&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HDD Mount -&amp;nbsp;&lt;a href=&quot;https://velog.io/@hong-brother/Proxmox&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@hong-brother/Proxmox&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxmox VE 기초설정 - &lt;a href=&quot;https://annyeong.me/n/proxmox-setup&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://annyeong.me/n/proxmox-setup&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxmox DHCP 설정 - &lt;a href=&quot;https://velog.io/@minse0204/Proxmox-DHCP-%EC%84%A4%EC%A0%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@minse0204/Proxmox-DHCP-%EC%84%A4%EC%A0%95&lt;/a&gt;&lt;/p&gt;</description>
      <category>Infra/Proxmox</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/110</guid>
      <comments>https://soojae.tistory.com/110#entry110comment</comments>
      <pubDate>Fri, 29 Nov 2024 00:44:37 +0900</pubDate>
    </item>
    <item>
      <title>[Proxmox] Proxmox 8.3버전 설치</title>
      <link>https://soojae.tistory.com/109</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;proxmox.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFlPKS/btsKYihIWZk/7CZjSXXxyY0ScOu1oA5b10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFlPKS/btsKYihIWZk/7CZjSXXxyY0ScOu1oA5b10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFlPKS/btsKYihIWZk/7CZjSXXxyY0ScOu1oA5b10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFlPKS%2FbtsKYihIWZk%2F7CZjSXXxyY0ScOu1oA5b10%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; alt=&quot;proxmox 로고&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;484&quot; data-filename=&quot;proxmox.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxmox를&amp;nbsp;설치해보자.&amp;nbsp;USB&amp;nbsp;저장소가&amp;nbsp;필요하다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;컴퓨터 사양&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;CPU&lt;/b&gt;: AMD Ryzen 5 5600&amp;nbsp;&lt;br /&gt;&lt;b&gt;RAM&lt;/b&gt;:&amp;nbsp;64GB&amp;nbsp;&lt;br /&gt;&lt;b&gt;SSD&lt;/b&gt;:&amp;nbsp;1TB&amp;nbsp;(512GB&amp;nbsp;+&amp;nbsp;512GB)&amp;nbsp;&lt;br /&gt;&lt;b&gt;GPU&lt;/b&gt;:&amp;nbsp;NVIDIA&amp;nbsp;GeForce&amp;nbsp;GTX&amp;nbsp;1660&amp;nbsp;Super&lt;/blockquote&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. Proxmox ISO 파일 다운로드&lt;/h2&gt;
&lt;figure id=&quot;og_1732635771218&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;Downloads&quot; data-og-description=&quot;Proxmox VE 8.3 ISO Installer Version 8.3-1 File Size 1.45 GB Last Updated November 21, 2024 SHA256SUM b5c2d10d6492d2d763e648bc8562d0f77a90c39fac3a664e676e795735198b45&quot; data-og-host=&quot;www.proxmox.com&quot; data-og-source-url=&quot;https://www.proxmox.com/en/downloads&quot; data-og-url=&quot;https://www.proxmox.com/en/downloads&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ltJCe/hyXDiPyhQA/vBOC9nHJknCmzfC0rVkN70/img.png?width=1240&amp;amp;height=698&amp;amp;face=0_0_1240_698,https://scrap.kakaocdn.net/dn/Mkmh9/hyXDbQpZxB/aNzTDi0nce7YKUNsDFo0wk/img.png?width=1240&amp;amp;height=698&amp;amp;face=0_0_1240_698&quot;&gt;&lt;a href=&quot;https://www.proxmox.com/en/downloads&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.proxmox.com/en/downloads&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ltJCe/hyXDiPyhQA/vBOC9nHJknCmzfC0rVkN70/img.png?width=1240&amp;amp;height=698&amp;amp;face=0_0_1240_698,https://scrap.kakaocdn.net/dn/Mkmh9/hyXDbQpZxB/aNzTDi0nce7YKUNsDFo0wk/img.png?width=1240&amp;amp;height=698&amp;amp;face=0_0_1240_698');&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;Downloads&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Proxmox VE 8.3 ISO Installer Version 8.3-1 File Size 1.45 GB Last Updated November 21, 2024 SHA256SUM b5c2d10d6492d2d763e648bc8562d0f77a90c39fac3a664e676e795735198b45&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.proxmox.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사이트에 접속한 후에 `Proxmox VE 8.3 ISO Installer`를 다운받는다. (아래 화면 참고)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQlkv0/btsKXz4aXek/AgpzRciJckBm0zwpD1kUHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQlkv0/btsKXz4aXek/AgpzRciJckBm0zwpD1kUHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQlkv0/btsKXz4aXek/AgpzRciJckBm0zwpD1kUHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQlkv0%2FbtsKXz4aXek%2FAgpzRciJckBm0zwpD1kUHK%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; alt=&quot;proxmox website&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;457&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Rufus 다운로드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사이트에서 Rufus를 다운받자.&lt;/p&gt;
&lt;figure id=&quot;og_1732635913730&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;Rufus - 간편한 방법으로 부팅 가능한 USB 드라이브 만들기&quot; data-og-description=&quot;Rufus는 USB 키/펜드라이브, 메모리 스틱 등과 같은 부팅 가능한 USB 플래시 드라이브를 포맷하고 생성하는 데 도움이 되는 유틸리티입니다. Rufus는 작은 크기에도 불구하고 필요한 모든 것을 제공&quot; data-og-host=&quot;rufus.ie&quot; data-og-source-url=&quot;https://rufus.ie/ko/&quot; data-og-url=&quot;https://rufus.ie/ko/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Xwgf7/hyXDbQp1Wx/7NNY9Th9OOUJeMk547s0pk/img.png?width=1018&amp;amp;height=1234&amp;amp;face=0_0_1018_1234,https://scrap.kakaocdn.net/dn/czLvRP/hyXDfkZ2k9/87lzGQxvxraD0Ancskh1X0/img.png?width=983&amp;amp;height=1235&amp;amp;face=0_0_983_1235,https://scrap.kakaocdn.net/dn/dC9dWS/hyXDhiNh5w/auWHNIOFyebAir4SOLblRK/img.png?width=950&amp;amp;height=1266&amp;amp;face=0_0_950_1266&quot;&gt;&lt;a href=&quot;https://rufus.ie/ko/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://rufus.ie/ko/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Xwgf7/hyXDbQp1Wx/7NNY9Th9OOUJeMk547s0pk/img.png?width=1018&amp;amp;height=1234&amp;amp;face=0_0_1018_1234,https://scrap.kakaocdn.net/dn/czLvRP/hyXDfkZ2k9/87lzGQxvxraD0Ancskh1X0/img.png?width=983&amp;amp;height=1235&amp;amp;face=0_0_983_1235,https://scrap.kakaocdn.net/dn/dC9dWS/hyXDhiNh5w/auWHNIOFyebAir4SOLblRK/img.png?width=950&amp;amp;height=1266&amp;amp;face=0_0_950_1266');&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;Rufus - 간편한 방법으로 부팅 가능한 USB 드라이브 만들기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Rufus는 USB 키/펜드라이브, 메모리 스틱 등과 같은 부팅 가능한 USB 플래시 드라이브를 포맷하고 생성하는 데 도움이 되는 유틸리티입니다. Rufus는 작은 크기에도 불구하고 필요한 모든 것을 제공&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;rufus.ie&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;3. Proxmox 부팅용 USB 제작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rufus를 이용해서 준비한 USB를 부팅용 USB를 만들어보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 캡처 2024-11-27 024655.png&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0DGjC/btsKYrYI09J/TsJVlUktKm4MEoqwAAx7Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0DGjC/btsKYrYI09J/TsJVlUktKm4MEoqwAAx7Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0DGjC/btsKYrYI09J/TsJVlUktKm4MEoqwAAx7Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0DGjC%2FbtsKYrYI09J%2FTsJVlUktKm4MEoqwAAx7Z0%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; alt=&quot;rufus&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;528&quot; data-filename=&quot;화면 캡처 2024-11-27 024655.png&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxmox 설치용 USB 제작이 완료되었다.&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;4. 부팅 순서 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;USB를 Proxmox를 설치할 기기에 연결한 후, 부팅을 진행한다.&amp;nbsp; 부팅 시 BIOS 메뉴에 진입하려면 특정 키(주로 Delete 키)를 반복해서 누른다. 부팅 순서 변경은 아래 글을 참고하면 된다.&lt;/p&gt;
&lt;figure id=&quot;og_1732713341658&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;부팅 순서를 변경하는 방법&quot; data-og-description=&quot;PC에서 부팅 순서를 변경하는 방법을 알아보십시오. 다음 단계에 따라 부팅 시퀀스 우선 순위와 하드 드라이브 BBS 우선 순위를 설정하십시오.&quot; data-og-host=&quot;kr.msi.com&quot; data-og-source-url=&quot;https://kr.msi.com/support/technical_details/MB_Boot_Priority&quot; data-og-url=&quot;https://kr.msi.com/support/technical_details/MB_Boot_Priority&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bxKzKn/hyXDeNqMth/dueTqfgOrDVkJJMGX08TvK/img.jpg?width=960&amp;amp;height=540&amp;amp;face=0_0_960_540,https://scrap.kakaocdn.net/dn/cByfZU/hyXGMhoopg/VZWkqZUpZ67rdKqINrN9YK/img.jpg?width=960&amp;amp;height=540&amp;amp;face=0_0_960_540,https://scrap.kakaocdn.net/dn/bLtOHt/hyXDdOtVOg/u062WKPKVwKQ1eh76OtdvK/img.jpg?width=960&amp;amp;height=540&amp;amp;face=0_0_960_540&quot;&gt;&lt;a href=&quot;https://kr.msi.com/support/technical_details/MB_Boot_Priority&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kr.msi.com/support/technical_details/MB_Boot_Priority&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bxKzKn/hyXDeNqMth/dueTqfgOrDVkJJMGX08TvK/img.jpg?width=960&amp;amp;height=540&amp;amp;face=0_0_960_540,https://scrap.kakaocdn.net/dn/cByfZU/hyXGMhoopg/VZWkqZUpZ67rdKqINrN9YK/img.jpg?width=960&amp;amp;height=540&amp;amp;face=0_0_960_540,https://scrap.kakaocdn.net/dn/bLtOHt/hyXDdOtVOg/u062WKPKVwKQ1eh76OtdvK/img.jpg?width=960&amp;amp;height=540&amp;amp;face=0_0_960_540');&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;부팅 순서를 변경하는 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;PC에서 부팅 순서를 변경하는 방법을 알아보십시오. 다음 단계에 따라 부팅 시퀀스 우선 순위와 하드 드라이브 BBS 우선 순위를 설정하십시오.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kr.msi.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;부팅 순서 변경이 완료되면, 기기를 재시작한다.&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;5. Proxmox 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시작하면 아래와 같은 화면이 표시된다. 일반적으로 첫 번째 옵션을 선택하지만, 필자의 경우 검은 화면이 나타나 두 번째 옵션(Terminal UI)을 선택해 설치를 진행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2024-11-27-22-22-07 001.jpeg&quot; data-origin-width=&quot;5712&quot; data-origin-height=&quot;4284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dR6xAo/btsK0iNPfK7/OpyexZJNYvOeNPHfkvRkC0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dR6xAo/btsK0iNPfK7/OpyexZJNYvOeNPHfkvRkC0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dR6xAo/btsK0iNPfK7/OpyexZJNYvOeNPHfkvRkC0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdR6xAo%2FbtsK0iNPfK7%2FOpyexZJNYvOeNPHfkvRkC0%2Fimg.jpg&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; alt=&quot;Proxmox-1&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;540&quot; data-filename=&quot;KakaoTalk_Photo_2024-11-27-22-22-07 001.jpeg&quot; data-origin-width=&quot;5712&quot; data-origin-height=&quot;4284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2024-11-27-22-22-07 002.jpeg&quot; data-origin-width=&quot;5712&quot; data-origin-height=&quot;4284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctk6AJ/btsKZacaCi8/rm4ZxIc9cqMpvVavRO7KvK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctk6AJ/btsKZacaCi8/rm4ZxIc9cqMpvVavRO7KvK/img.jpg&quot; data-alt=&quot;검은 화면이 나타난 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctk6AJ/btsKZacaCi8/rm4ZxIc9cqMpvVavRO7KvK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctk6AJ%2FbtsKZacaCi8%2Frm4ZxIc9cqMpvVavRO7KvK%2Fimg.jpg&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; alt=&quot;Proxmox-2&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;540&quot; data-filename=&quot;KakaoTalk_Photo_2024-11-27-22-22-07 002.jpeg&quot; data-origin-width=&quot;5712&quot; data-origin-height=&quot;4284&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;검은 화면이 나타난 모습&lt;/figcaption&gt;
&lt;/figure&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;blockquote data-ke-style=&quot;style3&quot;&gt;Contry: South Korea&lt;br /&gt;Timezone: Asia/seoul&lt;br /&gt;keyboard layout: U.S English&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7kgJP/btsKZuazXYr/590eoHZPi0l6V2kmKsinOk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7kgJP/btsKZuazXYr/590eoHZPi0l6V2kmKsinOk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7kgJP/btsKZuazXYr/590eoHZPi0l6V2kmKsinOk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7kgJP%2FbtsKZuazXYr%2F590eoHZPi0l6V2kmKsinOk%2Fimg.jpg&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; alt=&quot;proxmox-3&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;570&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&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;h3 data-ke-size=&quot;size23&quot;&gt;패스워드, 이메일 설정&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Root password: 루트 사용자 패스워드&lt;br /&gt;Administrator email: 루트 사용자 이메일&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EBeT1/btsKZdta149/CHtIV6PuPGDUl3Kj5YDxk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EBeT1/btsKZdta149/CHtIV6PuPGDUl3Kj5YDxk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EBeT1/btsKZdta149/CHtIV6PuPGDUl3Kj5YDxk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEBeT1%2FbtsKZdta149%2FCHtIV6PuPGDUl3Kj5YDxk0%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; alt=&quot;proxmox-5&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;540&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&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;h3 data-ke-size=&quot;size23&quot;&gt;네트워크 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 주소들은 기본 값으로 지정했다. hostname만 내 임의로 지정했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Management Interface:Proxmox VE가 사용할 네트워크 인터페이스를 선택하는 항목&lt;br /&gt;Hostname (FQDN):Proxmox 서버의 호스트 이름을 설정 &lt;br /&gt;IP Address (CIDR):Proxmox 서버가 사용할 고정 IP 주소를 설정. CIDR 표기법을 사용하여 서브넷 마스크를 포함해야 한다. &lt;br /&gt;Gateway Address:서버가 외부 네트워크와 통신하기 위해 사용할 게이트웨이 주소를 설정&lt;br /&gt;DNS Server Address:서버가 도메인 이름을 해석할 때 사용할 DNS 서버 주소를 설정.&amp;nbsp;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2024-11-27-22-22-09 007.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YQErQ/btsK0oHeUr1/yG0wmjMbmBQVlnz0cSKP31/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YQErQ/btsK0oHeUr1/yG0wmjMbmBQVlnz0cSKP31/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YQErQ/btsK0oHeUr1/yG0wmjMbmBQVlnz0cSKP31/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYQErQ%2FbtsK0oHeUr1%2FyG0wmjMbmBQVlnz0cSKP31%2Fimg.jpg&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; alt=&quot;proxmox-7&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;540&quot; data-filename=&quot;KakaoTalk_Photo_2024-11-27-22-22-09 007.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 화면에서는 DNS Server에 168.126.63.1를 사용했는데, 이는 내 인터넷 통신사가 KT여서 KT의 DNS서버가 자동으로 설정되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서&amp;nbsp;진행하면&amp;nbsp;설치가&amp;nbsp;완료되며&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;화면이&amp;nbsp;표시된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_3895.JPG&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;2653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kw5qO/btsKY2yDP8S/piqz7vpxFnBbBMkWknlbz0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kw5qO/btsKY2yDP8S/piqz7vpxFnBbBMkWknlbz0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kw5qO/btsKY2yDP8S/piqz7vpxFnBbBMkWknlbz0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKw5qO%2FbtsKY2yDP8S%2Fpiqz7vpxFnBbBMkWknlbz0%2Fimg.jpg&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; alt=&quot;proxmox-8&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;446&quot; data-filename=&quot;IMG_3895.JPG&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;2653&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;h2 data-ke-size=&quot;size26&quot;&gt;6. 콘솔 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxmox서버 설치가 완료됐다! 이제 해당 서버의 공유기와 같은 망에 존재하는 기기로 위의 주소로 접속 해보면 다음과 같은 화면이 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1249&quot; data-origin-height=&quot;962&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzM5tK/btsKYKkK4or/kgCKTin8nKvwrKuniSTzCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzM5tK/btsKYKkK4or/kgCKTin8nKvwrKuniSTzCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzM5tK/btsKYKkK4or/kgCKTin8nKvwrKuniSTzCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzM5tK%2FbtsKYKkK4or%2FkgCKTin8nKvwrKuniSTzCK%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; alt=&quot;proxmox-9&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;555&quot; data-origin-width=&quot;1249&quot; data-origin-height=&quot;962&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;blockquote data-ke-style=&quot;style3&quot;&gt;username: root&lt;br /&gt;password: 설치시 등록했던 비밀번호&lt;/blockquote&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ldgYN/btsKYioth8v/pSSIoZ51F1ZQXfmgsFWQdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ldgYN/btsKYioth8v/pSSIoZ51F1ZQXfmgsFWQdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ldgYN/btsKYioth8v/pSSIoZ51F1ZQXfmgsFWQdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FldgYN%2FbtsKYioth8v%2FpSSIoZ51F1ZQXfmgsFWQdk%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;720&quot; height=&quot;419&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;744&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxmox 설치 - &lt;a href=&quot;https://phum.co.kr/tech-32/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://phum.co.kr/tech-32/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Infra/Proxmox</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/109</guid>
      <comments>https://soojae.tistory.com/109#entry109comment</comments>
      <pubDate>Wed, 27 Nov 2024 22:50:20 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] DeepL API를 이용한 번역 기능 추가하기</title>
      <link>https://soojae.tistory.com/105</link>
      <description>&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-filename=&quot;다운로드 (3) 복사본.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWwnBp/btsJE96emKv/X0kRcb5ivjWmDzqyvhUHyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWwnBp/btsJE96emKv/X0kRcb5ivjWmDzqyvhUHyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWwnBp/btsJE96emKv/X0kRcb5ivjWmDzqyvhUHyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWwnBp%2FbtsJE96emKv%2FX0kRcb5ivjWmDzqyvhUHyk%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; alt=&quot;Spring&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-filename=&quot;다운로드 (3) 복사본.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&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;h2 data-ke-size=&quot;size26&quot;&gt;DeepL 번역&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 번역 API 로는 Google Translate API, Microsoft Azure Translator, 그리고 최근 많이 사용되는 DeepL API가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히, 아직까지는 DeepL이 다른 번역 API들보다 더 우수한 번역 결과를 보여주는 것 같다. 이번 시간에는 DeepL API를 스프링 부트에 적용하여 번역 기능을 추가해보자.&amp;nbsp;&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;DeepL API Key 발급&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 아래 사이트로 접속한다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1726681512742&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;DeepL Pro | 텍스트, Word 및 기타 문서를 안전하게 번역하세요&quot; data-og-description=&quot;빠르고 정확하며 안전한 번역. 개인 및 팀 사용자용 DeepL Pro.&quot; data-og-host=&quot;www.deepl.com&quot; data-og-source-url=&quot;https://www.deepl.com/ko/pro#developer&quot; data-og-url=&quot;https://www.deepl.com/ko/pro&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zna4N/hyW6xeb7rb/985G03IkCePU5f7IbDEPf1/img.png?width=5382&amp;amp;height=2694&amp;amp;face=0_0_5382_2694&quot;&gt;&lt;a href=&quot;https://www.deepl.com/ko/pro#developer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.deepl.com/ko/pro#developer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zna4N/hyW6xeb7rb/985G03IkCePU5f7IbDEPf1/img.png?width=5382&amp;amp;height=2694&amp;amp;face=0_0_5382_2694');&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;DeepL Pro | 텍스트, Word 및 기타 문서를 안전하게 번역하세요&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;빠르고 정확하며 안전한 번역. 개인 및 팀 사용자용 DeepL Pro.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.deepl.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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;1129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wAWAW/btsJFuvv5hQ/pRQmqsSUkVuoy7zen1ThvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wAWAW/btsJFuvv5hQ/pRQmqsSUkVuoy7zen1ThvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wAWAW/btsJFuvv5hQ/pRQmqsSUkVuoy7zen1ThvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwAWAW%2FbtsJFuvv5hQ%2FpRQmqsSUkVuoy7zen1ThvK%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; alt=&quot;DeepL 요금플랜&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;648&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;1129&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입을 한 후에, 위 화면에서 DeepL API Free 플랜을 신청한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bch2na/btsJDFrVSte/8RU529mWmd4BLEHKkgJTTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bch2na/btsJDFrVSte/8RU529mWmd4BLEHKkgJTTK/img.png&quot; data-alt=&quot;모든 필드를 작성하자&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bch2na/btsJDFrVSte/8RU529mWmd4BLEHKkgJTTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbch2na%2FbtsJDFrVSte%2F8RU529mWmd4BLEHKkgJTTK%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; alt=&quot;DeepL API Free 1단계&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;759&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모든 필드를 작성하자&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 화면이 나오는데, 요금이 발생하지는 않는다. 무료 플랜의 경우 500,000자까지 이용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct3Ws7/btsJDQfIi5n/KHV7h4FAXa1vqBCWvY08Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct3Ws7/btsJDQfIi5n/KHV7h4FAXa1vqBCWvY08Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct3Ws7/btsJDQfIi5n/KHV7h4FAXa1vqBCWvY08Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct3Ws7%2FbtsJDQfIi5n%2FKHV7h4FAXa1vqBCWvY08Qk%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; alt=&quot;API키&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;338&quot; data-origin-width=&quot;1255&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;무료 플랜 구독을 완료한 후에, 내 계정에 들어가서 위의 API키를 복사한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Deepl-mock&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deepl-mock은 DeepL API를 모방하여 애플리케이션 테스트를 간소화하기 위한 모의 서버이다. 이를 통해 실제 API 호출을 하지 않고도 DeepL API와의 상호작용을 테스트할 수 있으며, 프록시 서버도 포함되어 있어 프록시 사용 테스트도 가능하게 해준다.&lt;/p&gt;
&lt;figure id=&quot;og_1726751968812&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 - DeepLcom/deepl-mock: deepl-mock is a mock HTTP server that simulates some behavior of the DeepL API to simplify applica&quot; data-og-description=&quot;deepl-mock is a mock HTTP server that simulates some behavior of the DeepL API to simplify application testing. - DeepLcom/deepl-mock&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/DeepLcom/deepl-mock&quot; data-og-url=&quot;https://github.com/DeepLcom/deepl-mock&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5Sruu/hyW6LKoOzw/jiR1m8mlRtSKZGKGAgJU5k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/DeepLcom/deepl-mock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/DeepLcom/deepl-mock&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5Sruu/hyW6LKoOzw/jiR1m8mlRtSKZGKGAgJU5k/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 - DeepLcom/deepl-mock: deepl-mock is a mock HTTP server that simulates some behavior of the DeepL API to simplify applica&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;deepl-mock is a mock HTTP server that simulates some behavior of the DeepL API to simplify application testing. - DeepLcom/deepl-mock&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;위의 리포지토리를 클론 받은 후에 도커로 띄우거나 npm으로 실행하면 된다. 도커로 띄어보자.&lt;/p&gt;
&lt;pre id=&quot;code_1726752247225&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker image build -t deepl/deepl-mock .
$ docker run -d --rm --name deepl-mock -p3000:3000 -p3001:3001 deepl/deepl-mock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 구성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle.kts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle 파일에 deepL라이브러리를 추가해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1726750956647&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;com.deepl.api:deepl-java:1.6.0&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TranslateController.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726751775741&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/v1/translate&quot;)
class TranslateController(
    private val translateService: TranslateService
) {

    @PostMapping
    fun translate(@RequestBody translateRequest: TranslateRequest): String {
        return translateService.translate(translateRequest)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TranslateRequest.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726751811300&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class TranslateRequest(
    val text: String,
    val targetLang: LanguageCode
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TranslateService.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726750898175&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class TranslateService(
    private val translator: Translator = Translator(
        &quot;여기에 발급 받은 DeepL API Key 값을 입력. 절대 외부(깃허브 등)에 노출 금지!!&quot;,
        TranslatorOptions().setServerUrl(&quot;http://localhost:3000&quot;) // deepL-mock을 연결하기 위한 로컬주소. 제거 예정 
        .setSendPlatformInfo(false) // DeepL로 내 어플리케이션 정보를 보내지 않음
    ),

    ) {

    fun translate(text: String, targetLang: LanguageCode): String {
        return translator.translateText(
            text, null, targetLang.code, TextTranslationOptions().setFormality(Formality.PreferMore)
        ).text
    }
}

enum class LanguageCode(val code: String) {
    ENGLISH(&quot;en-US&quot;),
    KOREAN(&quot;ko&quot;),
}&lt;/code&gt;&lt;/pre&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yjDLY/btsJD6XGU6I/vV0z0ndnuxRn5fzv7mGzKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yjDLY/btsJD6XGU6I/vV0z0ndnuxRn5fzv7mGzKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yjDLY/btsJD6XGU6I/vV0z0ndnuxRn5fzv7mGzKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyjDLY%2FbtsJD6XGU6I%2FvV0z0ndnuxRn5fzv7mGzKk%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; alt=&quot;응답&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;361&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeepL API 호출이 정상적으로 동작하는 것을 확인했으므로, 이제 실제 API를 호출할 수 있도록 `TranslateService.kt` 코드에서 `.setServerUrl(&quot;http://localhost:3000&quot;)`를 제거한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U9dJd/btsJEPusamc/MuJy9Yi7Lj07MCkFx5T0Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U9dJd/btsJEPusamc/MuJy9Yi7Lj07MCkFx5T0Tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U9dJd/btsJEPusamc/MuJy9Yi7Lj07MCkFx5T0Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU9dJd%2FbtsJEPusamc%2FMuJy9Yi7Lj07MCkFx5T0Tk%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; alt=&quot;결과값 2&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;384&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역이 잘 된것을 확인 할 수 있다. 그러나, 첫 번째 단어인 `Hooks`는 `후크`로 번역하기 보다는 `Hooks`그대로 두거나, `훅`으로 번역되 기를 원한다면. DeepL의 glossary 기능을 사용하여 특정 단어를 사용자 정의할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Glossary 기능 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Glossary기능을 통해 커스텀 용어집을 만들어보자. 우선 이전 코드를 수정해야한다.&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;TranslateRequest.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726839096785&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class TranslateRequest(
    val text: String,
    val targetLang: Lang,
    val sourceLang: Lang,
    val glossaryId: String?
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;TranslateService.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726837545659&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class TranslateService(
    private val translator: Translator = Translator(
        &quot;여기에 발급 받은 DeepL API Key 값을 입력. 절대 외부(깃허브 등)에 노출 금지!!&quot;,
        TranslatorOptions().setSendPlatformInfo(false) // DeepL로 내 어플리케이션 정보를 보내지 않음
    ),

    ) {

	fun translate(translateRequest: TranslateRequest): String {
        val translationOptions = TextTranslationOptions().setFormality(Formality.PreferMore)
        // glossaryId가 있으면 glossary를 사용하겠다는 의미
        translateRequest.glossaryId?.let { translationOptions.setGlossary(it) }

        val text = translator.translateText(
            translateRequest.text,
            // Glossary 기능을 설정하면 sourceLang을 꼭 지정을 해야한다.
            translateRequest.sourceLang.sourceCode,
            translateRequest.targetLang.targetCode,
            translationOptions
        ).text
        return text
    }

    fun createGlossary(createGlossaryRequest: CreateGlossaryRequest): CreateGlossaryResponse {
        val createGlossary = translator.createGlossary(
            createGlossaryRequest.glossaryName,
            createGlossaryRequest.sourceLang.sourceCode,
            createGlossaryRequest.targetLang.targetCode,
            GlossaryEntries(createGlossaryRequest.entries)
        )
        return CreateGlossaryResponse(
            glossaryId = createGlossary.glossaryId,
            ready = createGlossary.isReady,
            name = createGlossary.name,
            sourceLang = createGlossary.sourceLang,
            targetLang = createGlossary.targetLang,
            creationTime = createGlossary.creationTime,
            entryCount = createGlossary.entryCount
        )
    }

    fun getGlossary(glossaryId: String): GlossaryInfo {
        return translator.getGlossary(glossaryId)
    }
}

// 영어의 경우 번역시 sourceCode일때와 targetCode일때 코드가 다르다. 
// targetCode일때는 EN-US, EN-GB로 구분해서 써야한다.
enum class Lang(val sourceCode: String, val targetCode: String) {
    ENGLISH(&quot;EN&quot;, &quot;EN-US&quot;),
    KOREAN( &quot;KO&quot;,&quot;KO&quot;),
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Glossary 적용 후, 테스트 결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FWa0N/btsJIhiAdgZ/UVf6wglrekMhYDO5ZqZWQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FWa0N/btsJIhiAdgZ/UVf6wglrekMhYDO5ZqZWQK/img.png&quot; data-alt=&quot;위의 glossaryId를 저장해두자.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FWa0N/btsJIhiAdgZ/UVf6wglrekMhYDO5ZqZWQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFWa0N%2FbtsJIhiAdgZ%2FUVf6wglrekMhYDO5ZqZWQK%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; alt=&quot;glossary 1&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;394&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위의 glossaryId를 저장해두자.&lt;/figcaption&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;`Hook : 훅`으로 저장한 후 다시 번역을 시도해보니, 성공적으로 `Hooks`가 `훅`으로 번역되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqVUoI/btsJGndJFZl/YwvvvAo5RnkKuxseqgcQj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqVUoI/btsJGndJFZl/YwvvvAo5RnkKuxseqgcQj0/img.png&quot; data-alt=&quot;성공!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqVUoI/btsJGndJFZl/YwvvvAo5RnkKuxseqgcQj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqVUoI%2FbtsJGndJFZl%2FYwvvvAo5RnkKuxseqgcQj0%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;720&quot; height=&quot;394&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성공!&lt;/figcaption&gt;
&lt;/figure&gt;
&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;그런데...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Glossary를&amp;nbsp;사용할&amp;nbsp;때는&amp;nbsp;한계가&amp;nbsp;있다.&amp;nbsp;Glossary&amp;nbsp;기능을&amp;nbsp;이용하려면 `sourceLang`이 `null`일&amp;nbsp;수&amp;nbsp;없어,&amp;nbsp;언어&amp;nbsp;감지&amp;nbsp;기능을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;없다.&amp;nbsp;&lt;br /&gt;또한, `sourceLang`을 `ENGLISH`로&amp;nbsp;지정한&amp;nbsp;상태에서&amp;nbsp;번역을&amp;nbsp;진행하면,&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;자연스럽지&amp;nbsp;않은&amp;nbsp;번역이&amp;nbsp;나올&amp;nbsp;수&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;1144&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enW8tw/btsJHkUUlMi/hZFemakAiAodUQkYP31W01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enW8tw/btsJHkUUlMi/hZFemakAiAodUQkYP31W01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enW8tw/btsJHkUUlMi/hZFemakAiAodUQkYP31W01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FenW8tw%2FbtsJHkUUlMi%2FhZFemakAiAodUQkYP31W01%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; alt=&quot;그런데 말입니다&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;394&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`text`가 영어, 일본어, 중국어, 프랑스어 등 다양한 언어로 무작위로 입력되는 환경에서, 그 언어에 맞게 `sourceLang`을 자동으로 변경하고 싶지만, 이를 자동화하기가 쉽지 않다. 결국 둘 중에 하나를 포기해야한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;glossary 기능을 포기&lt;/li&gt;
&lt;li&gt;`sourceLang`값 `null` (언어감지 기능)을 포기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;glossary 기능을 포기하는 것이 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeepL API 문서 - &lt;a href=&quot;https://developers.deepl.com/docs/v/ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.deepl.com/docs/v/ko&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>DeepL</category>
      <category>deepl api</category>
      <category>번역</category>
      <category>스프링 번역</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/105</guid>
      <comments>https://soojae.tistory.com/105#entry105comment</comments>
      <pubDate>Fri, 20 Sep 2024 23:21:04 +0900</pubDate>
    </item>
    <item>
      <title>[Spring AI] Vector Store와 RAG를 이용한 할루시네이션 방지</title>
      <link>https://soojae.tistory.com/104</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (3).png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIIQLr/btsJEGJ7iez/Vb1nZ9WKlM2MPpalm0qKpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIIQLr/btsJEGJ7iez/Vb1nZ9WKlM2MPpalm0qKpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIIQLr/btsJEGJ7iez/Vb1nZ9WKlM2MPpalm0qKpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIIQLr%2FbtsJEGJ7iez%2FVb1nZ9WKlM2MPpalm0qKpK%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; alt=&quot;spring ai&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-filename=&quot;다운로드 (3).png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;스프링 AI 시리즈&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://soojae.tistory.com/99&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring AI] 준비 (기본 개념, OpenAI API Key, 크레딧 충전)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://soojae.tistory.com/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring AI] 챗봇 만들기 (Kotlin)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[Spring&amp;nbsp;AI]&amp;nbsp;Vector&amp;nbsp;Store와&amp;nbsp;RAG를&amp;nbsp;이용한&amp;nbsp;할루시네이션&amp;nbsp;방지&lt;/li&gt;
&lt;li&gt;[Spring AI] OpenAI 비용을 절감하는 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주의: 해당 포스팅 진행시, embedding된 데이터에 대한 질문을 OpenAI의 GPT-4 모델을 사용하여 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;처리할 때마다&lt;/b&gt;&lt;/span&gt; 약 0.01달러의 비용이 발생합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 할루시네이션(Hallucination)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서 AI가 `이수재`라는 사람에 대한 정보를 보유하고 있지 않음에도 불구하고, 존재하지 않는 정보를 생성했다. 이는 할루시네이션이라고 불리며, AI가 학습한 데이터 내에서 명확한 답을 찾지 못하거나 관련 정보가 부족할 때, 추론을 통해 그럴듯한 답을 만들어내는 현상이다.&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;실제 프로젝트 진행 중, AI에서 할루시네이션이 발생하는 경우는 주로 AI가 정확한 정보에 접근하지 못하거나 불완전한 데이터를 기반으로 추론할 때 나타난다. 특히, 사내 기술 문서나 회사 내부에서만 사용하는 전용 자료처럼 인터넷에 존재하지 않거나 공개되지 않은 정보의 경우 이러한 문제가 자주 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하는 데 도움을 줄 수 있는 것이 바로 `Vector store`, `embedding`, 그리고 `RAG (Retrieval- Augmented Generation)`이다.&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;`Embedding`은 문서나 데이터를 AI가 이해할 수 있는 벡터 형태로 변환하는 기술로, 이를 통해 데이터를 벡터화하여 AI가 처리할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Vector store`는 이렇게 벡터화된 데이터를 저장하고, 필요한 정보를 빠르게 검색할 수 있도록 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`RAG`는 이 과정에서 중요한 역할을 한다. AI가 질문을 받으면, RAG는 먼저 Vector Store에서 해당 질문과 관련된 정보를 검색하고, 검색된 데이터를 바탕으로 AI가 답변을 생성한다. 이로 인해 AI는 embedding 형태로 저장된 사내 기술 문서나 관련 정보등을 활용하여 더욱 정확하고 관련성 높은 답변을 제공할 수 있다.&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;결국, embedding을 통해 데이터를 벡터화하고, Vector store에서 필요한 정보를 검색하며, RAG로 이를 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;RAG에 대해 좀 더 알아보고 싶다면, 아래 블로그를 참고하면 좋다. 내용이 굉장히 잘 정리되어있다.&lt;/p&gt;
&lt;figure id=&quot;og_1727242471842&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;문서 전처리와 임베딩의 중요성: RAG 프로젝트 성공하기&quot; data-og-description=&quot;최근 저는 회사에서 RAG(Retrieval-Augmented Generation)를 활용한 사내 문서 챗봇 프로젝트를 진행하게 되었습니다. 이 프로젝트를 진행하면서 채팅의 퀄리티를 높이려면, 문서의 전처리와 임베딩이 중&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@minji.sql/%EB%AC%B8%EC%84%9C-%EC%A0%84%EC%B2%98%EB%A6%AC%EC%99%80-%EC%9E%84%EB%B2%A0%EB%94%A9%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1-rag-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B8%B0-97ae34e879b4&quot; data-og-url=&quot;https://medium.com/@minji.sql/%EB%AC%B8%EC%84%9C-%EC%A0%84%EC%B2%98%EB%A6%AC%EC%99%80-%EC%9E%84%EB%B2%A0%EB%94%A9%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1-rag-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B8%B0-97ae34e879b4&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b1p9cL/hyXaAPf9Og/LcJiTrR4Soz6BFmqvl16L1/img.png?width=1200&amp;amp;height=900&amp;amp;face=0_0_1200_900,https://scrap.kakaocdn.net/dn/dKLLcM/hyXaEREPj4/qwYZ1nlEfuO38npbfOy870/img.png?width=1358&amp;amp;height=906&amp;amp;face=0_0_1358_906&quot;&gt;&lt;a href=&quot;https://medium.com/@minji.sql/%EB%AC%B8%EC%84%9C-%EC%A0%84%EC%B2%98%EB%A6%AC%EC%99%80-%EC%9E%84%EB%B2%A0%EB%94%A9%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1-rag-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B8%B0-97ae34e879b4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@minji.sql/%EB%AC%B8%EC%84%9C-%EC%A0%84%EC%B2%98%EB%A6%AC%EC%99%80-%EC%9E%84%EB%B2%A0%EB%94%A9%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1-rag-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B8%B0-97ae34e879b4&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b1p9cL/hyXaAPf9Og/LcJiTrR4Soz6BFmqvl16L1/img.png?width=1200&amp;amp;height=900&amp;amp;face=0_0_1200_900,https://scrap.kakaocdn.net/dn/dKLLcM/hyXaEREPj4/qwYZ1nlEfuO38npbfOy870/img.png?width=1358&amp;amp;height=906&amp;amp;face=0_0_1358_906');&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;문서 전처리와 임베딩의 중요성: RAG 프로젝트 성공하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최근 저는 회사에서 RAG(Retrieval-Augmented Generation)를 활용한 사내 문서 챗봇 프로젝트를 진행하게 되었습니다. 이 프로젝트를 진행하면서 채팅의 퀄리티를 높이려면, 문서의 전처리와 임베딩이 중&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.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;이제 Spring AI 코드를 만들어보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vector Store 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI가 지원하는 다양한 Vector Store가 존재하지만, 이번 프로젝트에서는 `Chroma DB`라는 Vector Store를 설치해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;docker-compose.yaml&lt;/h3&gt;
&lt;pre id=&quot;code_1726649204795&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  chroma:
    image: ghcr.io/chroma-core/chroma:0.5.4
    container_name: chroma-db
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./local-env/volumes/chroma-data:/chroma/chroma
    command: &quot;--workers 1 --host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30&quot;
    environment:
      - IS_PERSISTENT=TRUE
    ports:
      - &quot;8000:8000&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ChromaDB Admin&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChromaDB를&amp;nbsp;위한&amp;nbsp;정식&amp;nbsp;UI&amp;nbsp;도구는&amp;nbsp;아직&amp;nbsp;존재하지&amp;nbsp;않는다.&amp;nbsp;하지만&amp;nbsp;UI&amp;nbsp;환경에서&amp;nbsp;ChromaDB를&amp;nbsp;작업할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;도와주는&amp;nbsp;비공식&amp;nbsp;오픈소스인&amp;nbsp;ChromaDB&amp;nbsp;Admin을&amp;nbsp;활용해보자.&lt;/p&gt;
&lt;figure id=&quot;og_1726650107591&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 - flanker/chromadb-admin: Admin UI for Chroma embedding database built with Next.js&quot; data-og-description=&quot;Admin UI for Chroma embedding database built with Next.js - flanker/chromadb-admin&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/flanker/chromadb-admin&quot; data-og-url=&quot;https://github.com/flanker/chromadb-admin&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/DJmLN/hyW2YYNys5/rftbmDpxkAuFBnbHoEGrHk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/flanker/chromadb-admin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/flanker/chromadb-admin&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/DJmLN/hyW2YYNys5/rftbmDpxkAuFBnbHoEGrHk/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 - flanker/chromadb-admin: Admin UI for Chroma embedding database built with Next.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Admin UI for Chroma embedding database built with Next.js - flanker/chromadb-admin&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;위 리포지토리를 `git clone`한 후에, 실행시켜준다.&lt;/p&gt;
&lt;pre id=&quot;code_1726650561085&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ pnpm install &amp;amp;&amp;amp; pnpm dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, http://localhost:3000에 접속하면 아래와 같은 화면을 확인 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1952&quot; data-origin-height=&quot;1003&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyoUU/btsJEOnMNZa/l4WXkjXWijCWwDrk4Yhzck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyoUU/btsJEOnMNZa/l4WXkjXWijCWwDrk4Yhzck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyoUU/btsJEOnMNZa/l4WXkjXWijCWwDrk4Yhzck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkyoUU%2FbtsJEOnMNZa%2Fl4WXkjXWijCWwDrk4Yhzck%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; alt=&quot;ChromaDB Admin&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;370&quot; data-origin-width=&quot;1952&quot; data-origin-height=&quot;1003&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;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 파일 구조&lt;/h2&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;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ejug07/btsJDssNO6N/8Sb0vZ58jL6VIlvyAXPk4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ejug07/btsJDssNO6N/8Sb0vZ58jL6VIlvyAXPk4k/img.png&quot; data-alt=&quot;이번 포스팅에 필요한 파일들만 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ejug07/btsJDssNO6N/8Sb0vZ58jL6VIlvyAXPk4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fejug07%2FbtsJDssNO6N%2F8Sb0vZ58jL6VIlvyAXPk4k%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;604&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이번 포스팅에 필요한 파일들만 표시&lt;/figcaption&gt;
&lt;/figure&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;build.gradle.kts&lt;/h3&gt;
&lt;pre id=&quot;code_1726652342724&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    kotlin(&quot;jvm&quot;) version &quot;1.9.25&quot;
    kotlin(&quot;plugin.spring&quot;) version &quot;1.9.25&quot;
    id(&quot;org.springframework.boot&quot;) version &quot;3.3.3&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.6&quot;
}

group = &quot;org.example&quot;
version = &quot;0.0.1-SNAPSHOT&quot;

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
    maven { url = uri(&quot;https://repo.spring.io/milestone&quot;) }
}

extra[&quot;springAiVersion&quot;] = &quot;1.0.0-M2&quot;

dependencies {
    implementation(&quot;org.springframework.ai:spring-ai-openai-spring-boot-starter&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-validation&quot;)
	// 아래 라이브러리 추가
    implementation(&quot;org.springframework.ai:spring-ai-chroma-store-spring-boot-starter&quot;)

    
    implementation(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
    testImplementation(&quot;org.jetbrains.kotlin:kotlin-test-junit5&quot;)
    testRuntimeOnly(&quot;org.junit.platform:junit-platform-launcher&quot;)   
}

dependencyManagement {
    imports {
        mavenBom(&quot;org.springframework.ai:spring-ai-bom:${property(&quot;springAiVersion&quot;)}&quot;)
    }
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll(&quot;-Xjsr305=strict&quot;)
    }
}

tasks.withType&amp;lt;Test&amp;gt; {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1726652209348&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  application:
    name: spring-ai
  ai:
    openai:
      api-key: # 여기에 발급 받은 OpenAI API Key 값을 입력. 절대 외부(깃허브 등)에 노출 금지!!
      embedding:
        options:
          model: text-embedding-ada-002
      chat:
        options:
          model: gpt-3.5-turbo
          temperature: 0.7
          max-tokens: 200

    vectorstore:
      chroma:
        client:
          host: http://localhost
          port: 8000
        collection-name: vector_store
        initialize-schema: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AiChatConfig.kt (이전 포스팅에서 만들었던 파일)&lt;/h3&gt;
&lt;pre id=&quot;code_1726651943885&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class AiChatConfig(
    val chatClient: ChatClient.Builder,
) {
    @Bean
    fun chatClient(): ChatClient {
        return chatClient.build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AiDocumentController.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726651905011&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/v1/documents&quot;)
class AiDocumentController(
    private val aiDocumentService: AiDocumentService
) {


    @PostMapping(&quot;/markdown&quot;)
    fun createDocumentFromMarkDown() {
        return aiDocumentService.storeDocumentsFormMarkdown()
    }

    @PostMapping(&quot;/chat&quot;)
    fun chat(@RequestBody chatAiDocumentRequest: AiDocumentChatRequest): AiDocumentChatResponse {
        return aiDocumentService.chat(chatAiDocumentRequest)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AiDocumentService.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726651994140&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class AiDocumentService(
    private val vectorStore: VectorStore,
    private val chatClient: ChatClient,
) {

    @Value(&quot;classpath:/documents/rules-of-hooks.md&quot;)
    var mdResource: Resource? = null

    fun storeDocumentsFormMarkdown() {
        val textReader = TextReader(mdResource)
        val splitDocuments = TokenTextSplitter().split(textReader.read())
        vectorStore.add(splitDocuments)
    }

    fun chat(chatAiDocumentRequest: AiDocumentChatRequest): AiDocumentChatResponse {
        val response = chatClient.prompt()
            .advisors(
                QuestionAnswerAdvisor(
                    vectorStore, SearchRequest.defaults()
                )
            )
            .user(chatAiDocumentRequest.userInput)
            .call()
            .content()

        return AiDocumentChatResponse(response);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AiDocumentChatRequest.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726653372582&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class AiDocumentChatRequest(
    @NotBlank(message = &quot;User input must not be blank&quot;)
    val userInput: String = &quot;&quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;AiDocumentChatResponse.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1726653357755&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class AiDocumentChatResponse(val response: String)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;rules-of-hooks.md&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 공식 문서에서 `rules-of-hooks.md` 파일을 GitHub에서 다운로드하여 프로젝트의 `resources/documents` 경로에 추가했다.&lt;/p&gt;
&lt;figure id=&quot;og_1726652133979&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;react.dev/src/content/reference/rules/rules-of-hooks.md at main &amp;middot; reactjs/react.dev&quot; data-og-description=&quot;The React documentation website. Contribute to reactjs/react.dev development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reactjs/react.dev/blob/main/src/content/reference/rules/rules-of-hooks.md&quot; data-og-url=&quot;https://github.com/reactjs/react.dev/blob/main/src/content/reference/rules/rules-of-hooks.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gbarl/hyW23yZe9y/YAOBUMW7jIEFL7rENWfAEk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/reactjs/react.dev/blob/main/src/content/reference/rules/rules-of-hooks.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/reactjs/react.dev/blob/main/src/content/reference/rules/rules-of-hooks.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gbarl/hyW23yZe9y/YAOBUMW7jIEFL7rENWfAEk/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;react.dev/src/content/reference/rules/rules-of-hooks.md at main &amp;middot; reactjs/react.dev&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The React documentation website. Contribute to reactjs/react.dev 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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 서버를 실행시킨 후, markdown 문서를 chromadb에 적재시켜보자.&lt;/p&gt;
&lt;pre id=&quot;code_1726653485040&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl --location --request POST 'http://localhost:8080/api/v1/documents/markdown'&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;http://localhost:3000에 접속(chromadb-admin)하면, markdown파일의 내용들이 적재가 된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2541&quot; data-origin-height=&quot;1309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tB91z/btsJEkm0yDZ/g8KyGNBp9f1bqeq0I5Ut41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tB91z/btsJEkm0yDZ/g8KyGNBp9f1bqeq0I5Ut41/img.png&quot; data-alt=&quot;chromadb-admin 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tB91z/btsJEkm0yDZ/g8KyGNBp9f1bqeq0I5Ut41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtB91z%2FbtsJEkm0yDZ%2Fg8KyGNBp9f1bqeq0I5Ut41%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; alt=&quot;chroma db 접속&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;371&quot; data-origin-width=&quot;2541&quot; data-origin-height=&quot;1309&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;chromadb-admin 화면&lt;/figcaption&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;이제&amp;nbsp;스프링부트&amp;nbsp;서버에&amp;nbsp;질문을&amp;nbsp;해보자.&amp;nbsp;앞서&amp;nbsp;적재한 `rules-of-hooks.md`&amp;nbsp;파일의&amp;nbsp;내용과&amp;nbsp;유사한&amp;nbsp;질문을&amp;nbsp;하면,&amp;nbsp;해당&amp;nbsp;내용을&amp;nbsp;바탕으로&amp;nbsp;적절한&amp;nbsp;답변을&amp;nbsp;제공할&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;1145&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhJMJQ/btsJE8MWZDs/MGgVSqj23JmGHblF9A1e2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhJMJQ/btsJE8MWZDs/MGgVSqj23JmGHblF9A1e2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhJMJQ/btsJE8MWZDs/MGgVSqj23JmGHblF9A1e2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhJMJQ%2FbtsJE8MWZDs%2FMGgVSqj23JmGHblF9A1e2k%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; alt=&quot;react hook 답변&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;604&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;961&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;이제&amp;nbsp;이수재에&amp;nbsp;대해&amp;nbsp;물어보면,&amp;nbsp;이전&amp;nbsp;포스팅에서&amp;nbsp;이상한&amp;nbsp;답변이&amp;nbsp;나왔던&amp;nbsp;것과&amp;nbsp;달리,&amp;nbsp;chromaDB에&amp;nbsp;저장된&amp;nbsp;내용만을&amp;nbsp;바탕으로&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;1145&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clyQpL/btsJEpaJDxk/E5BzNBNyPgK5GnkPrSCo61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clyQpL/btsJEpaJDxk/E5BzNBNyPgK5GnkPrSCo61/img.png&quot; data-alt=&quot;chromadb에 저장된 내용만을 바탕으로 답변&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clyQpL/btsJEpaJDxk/E5BzNBNyPgK5GnkPrSCo61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclyQpL%2FbtsJEpaJDxk%2FE5BzNBNyPgK5GnkPrSCo61%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; alt=&quot;이수재 답변&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;604&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;961&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;chromadb에 저장된 내용만을 바탕으로 답변&lt;/figcaption&gt;
&lt;/figure&gt;
&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; 그런데...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용이 지나치게 많이 발생하고 있다. 질문 한번에 약 0.01달러가 발생하고있다. 예를 들어, 사내 기술 문서를 위한 챗봇을 만든다면 GPT-4 모델을 사용하는 것은 오버 엔지니어링일 수 있다. 사내 문서 검색이나 관련성 높은 답변 제공에는 고사양 AI 모델이 꼭 필요하지 않을 수 있으므로, 더 경량화된 모델이나 로컬 LLM을 고려하는 것이 적합할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div data-message-id=&quot;d529a7d3-dbe9-4b4d-ad1b-15b4e844959e&quot; data-message-author-role=&quot;assistant&quot;&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;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 전처리와 임베딩의 중요성: RAG 프로젝트 성공하기 - &lt;a href=&quot;https://medium.com/@minji.sql/%EB%AC%B8%EC%84%9C-%EC%A0%84%EC%B2%98%EB%A6%AC%EC%99%80-%EC%9E%84%EB%B2%A0%EB%94%A9%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1-rag-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B8%B0-97ae34e879b4&quot;&gt;https://medium.com/@minji.sql/%EB%AC%B8%EC%84%9C-%EC%A0%84%EC%B2%98%EB%A6%AC%EC%99%80-%EC%9E%84%EB%B2%A0%EB%94%A9%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1-rag-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B8%B0-97ae34e879b4&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vector Store란 - &lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=164964&amp;amp;boardType=techBlog&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devocean.sk.com/blog/techBoardDetail.do?ID=164964&amp;amp;boardType=techBlog&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG = Vector Search + LLM 1편 - &lt;a href=&quot;https://blog.mnc.ai/74&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.mnc.ai/74&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AI - &lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/embeddings.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-ai/reference/api/embeddings.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ai</category>
      <category>chromaDB</category>
      <category>GPT</category>
      <category>Kotlin</category>
      <category>open ai</category>
      <category>Rag</category>
      <category>spring ai</category>
      <category>vector store</category>
      <category>스프링 ai</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/104</guid>
      <comments>https://soojae.tistory.com/104#entry104comment</comments>
      <pubDate>Wed, 18 Sep 2024 20:27:15 +0900</pubDate>
    </item>
    <item>
      <title>hELLO 스킨 4.10버전 백틱 인라인 코드 설정</title>
      <link>https://soojae.tistory.com/103</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ffz9y/btsJCzyyQpW/2b6wCyKQ2izORicnyvS7k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ffz9y/btsJCzyyQpW/2b6wCyKQ2izORicnyvS7k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ffz9y/btsJCzyyQpW/2b6wCyKQ2izORicnyvS7k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFfz9y%2FbtsJCzyyQpW%2F2b6wCyKQ2izORicnyvS7k0%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; alt=&quot;티스토리 로고&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;229&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hELLO스킨은 4.10버전 기준으로 화면 구조가 변경되어서, `document.querySelectorAll()` 코드를 다음과 같이 변경해야한다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;HTML편집 화면 &amp;lt;/body&amp;gt; 앞에 삽입&lt;/h3&gt;
&lt;pre id=&quot;code_1726222705715&quot; class=&quot;xquery&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt; 
  window.onload = function() { 
  let textNodes = document.querySelectorAll(&quot;div.contents_style &amp;gt; *:not(figure):not(pre):not(div)&quot;);
  textNodes.forEach(node =&amp;gt; {
    node.innerHTML = node.innerHTML.replace(/`(.*?)`/g, `&amp;lt;code class='notion-code'&amp;gt;$1&amp;lt;/code&amp;gt;`);
  });
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;CSS편집 화면 코드 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1726222705716&quot; class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;css&quot;&gt;&lt;code&gt;.notion-code {
    font-family: Consolas !important;
    line-height: normal !important;
    background: rgba(135,131,120,0.15) !important;
    color: #EB5757 !important;
    border-radius: 3px !important;
    font-size: 85% !important;
    padding: 0.2em 0.4em !important;
    margin-right: 0.2em !important;
    display: inline-block !important;
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백틱을 이용한 인라인 코드블록 관련 내용은 아래 블로그들을 참고하면 된다.&lt;/p&gt;
&lt;figure id=&quot;og_1726218088278&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;[티스토리]10초만에 가능한 인라인 코드블록 편하게 적용하는 방법&quot; data-og-description=&quot;안녕하세요 무택입니다 :) 오늘은 티스토리에서 인라인코드를 코드블록으로 만드는 방법 + 간편하게 설정할 수 있는 방법에 대해 써보려고 합니다. 개발 관련 블로그를 운영하고 있고 운영할 생&quot; data-og-host=&quot;mu08.tistory.com&quot; data-og-source-url=&quot;https://mu08.tistory.com/112#google_vignette&quot; data-og-url=&quot;https://mu08.tistory.com/112&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OspNP/hyW2T91cP7/J2XyNzdBoAkkPl19aTq9vk/img.jpg?width=720&amp;amp;height=495&amp;amp;face=0_0_720_495,https://scrap.kakaocdn.net/dn/bkUbmk/hyW202mSBO/MVRDofbn3CixEMJSbqOQ90/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cOsmeX/hyW2Ta7wzw/NwZPVt8bjRZpCJNI0QTylk/img.jpg?width=720&amp;amp;height=495&amp;amp;face=0_0_720_495&quot;&gt;&lt;a href=&quot;https://mu08.tistory.com/112#google_vignette&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mu08.tistory.com/112#google_vignette&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OspNP/hyW2T91cP7/J2XyNzdBoAkkPl19aTq9vk/img.jpg?width=720&amp;amp;height=495&amp;amp;face=0_0_720_495,https://scrap.kakaocdn.net/dn/bkUbmk/hyW202mSBO/MVRDofbn3CixEMJSbqOQ90/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cOsmeX/hyW2Ta7wzw/NwZPVt8bjRZpCJNI0QTylk/img.jpg?width=720&amp;amp;height=495&amp;amp;face=0_0_720_495');&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;[티스토리]10초만에 가능한 인라인 코드블록 편하게 적용하는 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요 무택입니다 :) 오늘은 티스토리에서 인라인코드를 코드블록으로 만드는 방법 + 간편하게 설정할 수 있는 방법에 대해 써보려고 합니다. 개발 관련 블로그를 운영하고 있고 운영할 생&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mu08.tistory.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;figure id=&quot;og_1726218353256&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;티스토리에 백틱으로 인라인 코드 블럭 만들기&quot; data-og-description=&quot;들어가며 &amp;#96;Velog&amp;#96;에서 작성했던 글들을 &amp;#96;Tistory&amp;#96;로 이전하면서 &amp;#96;Tistory&amp;#96;에서도 인라인 코드 블럭을 적용할 수 있다는 사실을 이번에 처음 알게 되었다. 하지만 알아보니 굉장히 번거로운 과정을 거쳤&quot; data-og-host=&quot;dev-qhyun.tistory.com&quot; data-og-source-url=&quot;https://dev-qhyun.tistory.com/12&quot; data-og-url=&quot;https://dev-qhyun.tistory.com/12&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bCU9zl/hyW20OQbZD/O40LfNK2eKddHC2uqoFNTk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/AqAkQ/hyW2QSZkq7/W6QE7eDz7XL5Ut9DVvK4a1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://dev-qhyun.tistory.com/12&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev-qhyun.tistory.com/12&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bCU9zl/hyW20OQbZD/O40LfNK2eKddHC2uqoFNTk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/AqAkQ/hyW2QSZkq7/W6QE7eDz7XL5Ut9DVvK4a1/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; data-ke-size=&quot;size16&quot;&gt;티스토리에 백틱으로 인라인 코드 블럭 만들기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며 `Velog`에서 작성했던 글들을 `Tistory`로 이전하면서 `Tistory`에서도 인라인 코드 블럭을 적용할 수 있다는 사실을 이번에 처음 알게 되었다. 하지만 알아보니 굉장히 번거로운 과정을 거쳤&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev-qhyun.tistory.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;</description>
      <category>Tistory</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/103</guid>
      <comments>https://soojae.tistory.com/103#entry103comment</comments>
      <pubDate>Fri, 13 Sep 2024 21:18:44 +0900</pubDate>
    </item>
    <item>
      <title>cannot deserialize from Object value (no delegate- or property-based Creator)]</title>
      <link>https://soojae.tistory.com/101</link>
      <description>&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 id=&quot;code_1726576122911&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;JSON parse error: Cannot construct instance of org.example.springai.dto.AiChatRequest (although at least one Creator exists): 
cannot deserialize from Object value (no delegate- or property-based Creator)]&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;/p&gt;
&lt;pre id=&quot;code_1725899156360&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class AiChatRequest(
    val userInput: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson이 객체를 역직렬화할 때는 두 가지 전략을 사용하여 데이터를 객체로 변환한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Delegating-based&lt;/b&gt;: 하나의 값(예: 문자열, 숫자 등)만을 필드로 갖는 경우 그 값을 해당 클래스에 직접 전달하여 생성자를 호출한다. 이를 통해 단일 값으로 생성할 수 있는 객체에 적합하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Properties-based&lt;/b&gt;: 클래스 내의 여러 필드를 이용해 객체를 생성하는 방식이다. 이 방식은 JSON에서 키-값 쌍을 각각 클래스의 필드에 매핑합니다. 즉, JSON의 각 속성을 클래스의 필드에 직접 대응시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson은 필드가 1개일 때 어느 방식으로 역직렬화할지 알지 못해 오류를 발생시키는 경우가 있다. AiChatRequest 클래스가 필드가 1개인 경우, Jackson은 이를 단일 값으로 취급해야 하는지, 아니면 키-값 쌍으로 취급해야 하는지 결정할 수 없어서 발생하는 문제다.&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;해결법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 값 제공 방법 (이 방법으로 해결)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin에서 제공하는 기본값은 Jackson이 기본 생성자를 호출할 수 있게 하며, 이로 인해 Jackson이 역직렬화를 올바르게 수행할 수 있게 된다. 필드가 없을 경우에는 기본값을 사용하고, 값이 제공될 경우 해당 값을 사용하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1725899323363&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class AiChatRequest(
    @NotBlank(message = &quot;User input must not be blank&quot;)
    val userInput: String = &quot;&quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그 외 방법 - @JsonCreator 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 필드 클래스에서 Jackson에게 명시적으로 생성자를 사용하도록 지시할 수 있습니다. 이를 위해 @JsonCreator와 @JsonProperty 어노테이션을 사용하여 Jackson이 올바르게 역직렬화할 수 있게 도울 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1726575931884&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;data class AiChatRequest(
        @JsonProperty(&quot;userInput&quot;) 
        @NotBlank(message = &quot;User input must not be blank&quot;) 
        val userInput: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;하지만 `@JsonProperty` 어노테이션을 사용하는 것은 다른 개발자가 그 이유를 쉽게 유추하기 힘들기 때문에, 사용하지 않는 것이 더 낫다고 판단했다.&lt;/div&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Spring] Kotlin에서 data class의 필드가 1개일 때 역직렬화가 안 되는 문제 (no delegate- or property-based Creator) -&amp;nbsp;&lt;a href=&quot;https://cl8d.tistory.com/111&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cl8d.tistory.com/111&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Language/Kotlin</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/101</guid>
      <comments>https://soojae.tistory.com/101#entry101comment</comments>
      <pubDate>Wed, 11 Sep 2024 00:32:54 +0900</pubDate>
    </item>
    <item>
      <title>[Spring AI] 챗봇 만들기 (Kotlin)</title>
      <link>https://soojae.tistory.com/100</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (3).png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7zseA/btsJvQs67rq/kmolv9Qyn3mYNbX5hJWL20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7zseA/btsJvQs67rq/kmolv9Qyn3mYNbX5hJWL20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7zseA/btsJvQs67rq/kmolv9Qyn3mYNbX5hJWL20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7zseA%2FbtsJvQs67rq%2Fkmolv9Qyn3mYNbX5hJWL20%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; alt=&quot;Spring AI&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-filename=&quot;다운로드 (3).png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;스프링 AI 시리즈&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://soojae.tistory.com/99&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring AI] 준비 (기본 개념, OpenAI API Key, 크레딧 충전)&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;[Spring AI] 챗봇 만들기 (Kotlin)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://soojae.tistory.com/104&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring AI] Vector Store와 RAG를 이용한 할루시네이션 방지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[Spring AI] OpenAI 비용을 절감하는 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 OpenAI과 Spring AI를 활용한 챗봇을 만들어보자.&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;전체 파일 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;389&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xkerq/btsJwQFXBSH/PEzISsHV0o7dOtksFlmrtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xkerq/btsJwQFXBSH/PEzISsHV0o7dOtksFlmrtk/img.png&quot; data-alt=&quot;이번 포스팅에 필요한 파일들만 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xkerq/btsJwQFXBSH/PEzISsHV0o7dOtksFlmrtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxkerq%2FbtsJwQFXBSH%2FPEzISsHV0o7dOtksFlmrtk%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; alt=&quot;전체 파일 구조&quot; loading=&quot;lazy&quot; width=&quot;389&quot; height=&quot;718&quot; data-origin-width=&quot;389&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이번 포스팅에 필요한 파일들만 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle.kts&lt;/h3&gt;
&lt;pre id=&quot;code_1725893930326&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    kotlin(&quot;jvm&quot;) version &quot;1.9.25&quot;
    kotlin(&quot;plugin.spring&quot;) version &quot;1.9.25&quot;
    id(&quot;org.springframework.boot&quot;) version &quot;3.3.3&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.6&quot;
}

group = &quot;org.example&quot;
version = &quot;0.0.1-SNAPSHOT&quot;

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
    maven { url = uri(&quot;https://repo.spring.io/milestone&quot;) }
}

extra[&quot;springAiVersion&quot;] = &quot;1.0.0-M2&quot;

dependencies {
    implementation(&quot;org.springframework.ai:spring-ai-openai-spring-boot-starter&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-validation&quot;)
    
    implementation(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
    testImplementation(&quot;org.jetbrains.kotlin:kotlin-test-junit5&quot;)
    testRuntimeOnly(&quot;org.junit.platform:junit-platform-launcher&quot;)
}

dependencyManagement {
    imports {
        mavenBom(&quot;org.springframework.ai:spring-ai-bom:${property(&quot;springAiVersion&quot;)}&quot;)
    }
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll(&quot;-Xjsr305=strict&quot;)
    }
}

tasks.withType&amp;lt;Test&amp;gt; {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1725894774997&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  application:
    name: spring-ai
  ai:
    openai:
      api-key: # 여기에 발급 받은 OpenAI API Key 값을 입력. 절대 외부(깃허브 등)에 노출 금지!!
      chat:
        options:
          model: gpt-3.5-turbo
          temperature: 0.7
          max-tokens: 200&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AiChatConfig.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1725955339013&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class AiChatConfig(
    val chatClient: ChatClient.Builder
) {
    @Bean
    fun chatClient(): ChatClient {
        return chatClient.build()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OpenAiEmbeddingConfig.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1725894704457&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableConfigurationProperties(OpenAiEmbeddingProperties::class)
class OpenAiEmbeddingConfig(
    private val openAiEmbeddingProperties: OpenAiEmbeddingProperties
) {
    @Bean
    fun embeddingModel(): EmbeddingModel {
        return OpenAiEmbeddingModel(OpenAiApi(openAiEmbeddingProperties.apiKey))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;DTO(AiChatRequest.kt, AiChatResponse.kt)&lt;/h3&gt;
&lt;pre id=&quot;code_1725895058076&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// AiChatRequest
data class AiChatRequest(
    @NotNull(message = &quot;User input must not be null&quot;)
    val userInput: String = &quot;&quot;
)


// AiChatResponse
data class AiChatResponse(val response: String&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AiChatController.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1725894619427&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/v1/chat&quot;)
class AiChatController(
    private val aiChatService: AiChatService
) {
    @PostMapping
    fun chat(@RequestBody aiChatRequest: AiChatRequest): AiChatResponse {
        return aiChatService.chat(aiChatRequest)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AiChatService.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1725895041090&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class AiChatService(
    private val chatClient: ChatClient,
    private val aiPromptService: AiPromptService
) {

    fun chat(aiChatRequest: AiChatRequest): AiChatResponse {
        val prompt = aiPromptService.createPrompt(aiChatRequest.userInput)
        return AiChatResponse(chatClient.prompt(prompt).call().content())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;system-message.st&lt;/h3&gt;
&lt;pre id=&quot;code_1725895234972&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;You are a helpful AI assistant that helps people find information. Your name is {name}
You should reply to the user's request. and do not repeat it.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;user-message.st&lt;/h3&gt;
&lt;pre id=&quot;code_1725895274084&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Tell me {userInput}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AiPromptService.kt&lt;/h3&gt;
&lt;pre id=&quot;code_1725895103179&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class AiPromptService(
    @Value(&quot;classpath:prompts/chat/system-message.st&quot;)
    private val systemResource: Resource,
    @Value(&quot;classpath:prompts/chat/user-message.st&quot;)
    private val userResource: Resource,
) {
    // 유저 입력을 메시지로 변환
    private fun createUserMessage(userInput: String): Message =
        PromptTemplate(userResource).createMessage(
            mapOf(&quot;userInput&quot; to userInput)
        )

    // 시스템 메시지 생성
    private fun createSystemMessage(): Message =
        SystemPromptTemplate(systemResource).createMessage(
            mapOf(&quot;name&quot; to &quot;Jerry&quot;)
        )

    // 프롬프트 생성
    fun createPrompt(userInput: String): Prompt =
        Prompt(listOf(createUserMessage(userInput), createSystemMessage()))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 PromptService는 조금 자세히 살펴보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;systemResource: 시스템의 역할이나 행동을 정의하는 메시지를 저장한 템플릿 파일인 system-message.st&lt;br /&gt;userResource: 사용자가 입력한 내용을 담는 메시지를 저장한 템플릿 파일인 user-message.st&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 리소스는 각각 특정 메시지 패턴을 따르며, 메시지 내부에서 동적으로 값을 바인딩할 수 있도록 구성되어 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`createUserMessage` 함수의 경우 `userInput`에 사용자에게 입력받은 `userInput`이 바인딩이 된다.&lt;/li&gt;
&lt;li&gt;`createSystemMessage` 함수의 경우 `name`에 `Jerry`가 바인딩 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 시스템의 이름을 물어보면 `Jerry`라고 답을 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;745&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sNyan/btsJvQmpojy/ZGLdKyhsA3KkxhBcd0HBa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sNyan/btsJvQmpojy/ZGLdKyhsA3KkxhBcd0HBa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sNyan/btsJvQmpojy/ZGLdKyhsA3KkxhBcd0HBa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsNyan%2FbtsJvQmpojy%2FZGLdKyhsA3KkxhBcd0HBa1%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;720&quot; height=&quot;485&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;745&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;대화형 AI에서는 사용자의 질문에 자연스럽고 일관된 답변을 제공하기 위해, 시스템의 역할과 사용자 입력을 효과적으로 전달하는 방법이 중요하다.&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;h2 data-ke-size=&quot;size26&quot;&gt;추가&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FtZMG/btsJwyTaoj3/EWnbtrkqBZ8rj1Xe6Ckhrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FtZMG/btsJwyTaoj3/EWnbtrkqBZ8rj1Xe6Ckhrk/img.png&quot; data-alt=&quot;예?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FtZMG/btsJwyTaoj3/EWnbtrkqBZ8rj1Xe6Ckhrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFtZMG%2FbtsJwyTaoj3%2FEWnbtrkqBZ8rj1Xe6Ckhrk%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; alt=&quot;Ai 채팅 결과&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;461&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 배우도 아니고, 연기력이 좋지도 않다. 위처럼 AI가 잘못된 정보를 응답해주는 것을 AI 할루시네이션이라고 한다.&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;AI 할루시네이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 할루시네이션이란 인공지능 모델, 특히 자연어 처리(NLP) 모델이 실제로 존재하지 않거나 잘못된 정보를 마치 사실인 것처럼 생성하거나 제공하는 현상을 의미한다. 이는 인공지능이 주어진 데이터를 바탕으로 패턴을 학습하고 추론을 하지만, 때때로 오류를 범하거나 기존의 정보와 일치하지 않는 새로운 정보를 생성할 때 발생한다.&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;다음 포스팅에서 Spring AI와 벡터 스토어(Vector Store)를 활용하여 AI 할루시네이션을 줄이고, 보다 신뢰성 있는 답변을 제공하는 방법을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 ai prompt - &lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/prompt.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-ai/reference/api/prompt.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ai</category>
      <category>GPT</category>
      <category>Kotlin</category>
      <category>openAI</category>
      <category>spring ai</category>
      <category>스프링 ai</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/100</guid>
      <comments>https://soojae.tistory.com/100#entry100comment</comments>
      <pubDate>Tue, 10 Sep 2024 02:20:47 +0900</pubDate>
    </item>
    <item>
      <title>[Spring AI] 준비 (기본 개념, OpenAI API Key, 크레딧 충전)</title>
      <link>https://soojae.tistory.com/99</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx2NVP/btsJvxAOB0Z/oWjyoIjgKVUDPusZ5QVYJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx2NVP/btsJvxAOB0Z/oWjyoIjgKVUDPusZ5QVYJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx2NVP/btsJvxAOB0Z/oWjyoIjgKVUDPusZ5QVYJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx2NVP%2FbtsJvxAOB0Z%2FoWjyoIjgKVUDPusZ5QVYJ0%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; alt=&quot;Spring AI&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 AI 시리즈&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;[Spring AI] 준비 (기본 개념, OpenAI API Key, 크레딧 충전)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://soojae.tistory.com/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring AI] 챗봇 만들기 (Kotlin)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://soojae.tistory.com/104&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring AI] Vector Store와 RAG를 이용한 할루시네이션 방지&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;[Spring AI] OpenAI 비용을 절감하는 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 AI란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI는 인공지능(AI) 기능을 Spring 애플리케이션에 통합하기 위한 라이브러리이다. 기존에는 거의 파이썬으로만 AI를 활용했지만, 이제 Spring AI를 통해 Java에서도 AI를 활용할 수 있다.&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;기본 개념 (더보기를 눌러주세요)&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 모델은 텍스트, 이미지, 오디오 등 다양한 입력을 처리하여 결과를 생성하는 알고리즘이다. Spring AI는 언어, 이미지, 오디오 입력과 출력을 지원하며, &lt;b&gt;임베딩&lt;/b&gt;(Embeddings)도 지원하여 더 고급 사용 사례를 처리할 수 있게 한다. GPT 같은 모델은 사전 훈련된 모델로, 일반적으로 사용자가 특별한 기계 학습 배경 없이도 활용할 수 있는 장점이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프롬프트(Prompt)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 AI 모델에 원하는 출력을 유도하기 위해 입력하는 텍스트이다. 단순한 문자열 입력처럼 보일 수 있지만, 사실상 시스템의 역할과 사용자 입력 등을 포함하는 복잡한 구조이다. 이를 통해 사용자는 모델의 출력을 원하는 방향으로 유도할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예:&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'If the answer is not in the context, inform the user that you can't answer the question.'등의 프롬프트를 통해서 AI 할루시네이션(Hallucination)을 방지&lt;/li&gt;
&lt;li&gt;'answer only in Korean.' 프롬프트를 통해서 한국어로만 답변하도록 설정&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;Spring AI에서는 &lt;b&gt;StringTemplate&lt;/b&gt; 라이브러리를 이용해 프롬프트를 동적으로 생성할 수 있다. 예를 들어, 특정 사용자 입력에 따라 맞춤형 프롬프트를 만들 수 있으며, 이 프롬프트는 Spring MVC 아키텍처에서의 &quot;View&quot;와 비슷하게 작동한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;임베딩(Embeddings)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베딩은 텍스트, 이미지, 비디오 등의 입력을 벡터로 변환한 숫자 표현이다. 이 벡터는 AI 모델이 의미를 이해하고 서로 다른 입력 간의 유사성을 비교할 수 있게 해준다. 특히 &lt;b&gt;RAG(Retrieval Augmented Generation)&lt;/b&gt; 패턴과 같은 실용적인 응용에서 중요한 역할을 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰(Tokens)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰은 AI 모델에서 입력과 출력을 처리하는 기본 단위이다. 입력된 텍스트는 토큰으로 변환되고, 출력될 때 다시 텍스트로 변환된다. 토큰 수에 따라 비용이 발생하며, API 호출에서 모델이 한 번에 처리할 수 있는 토큰 수에는 제한이 있다. 이를 &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;AI 모델의 출력은 일반적으로 문자열 형태로 제공된다. 하지만 필요할 경우, 특정 형식으로 구조화된 데이터를 얻기 위해 추가적인 프롬프트 구성 및 변환이 필요할 수 있다. Spring AI는 이 과정도 지원한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부 데이터 및 API와의 통합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 모델의 데이터셋이 최신 정보까지 포함하지 않는 경우가 많다. 이를 해결하기 위한 방법으로는 &lt;b&gt;파인튜닝&lt;/b&gt;, &lt;b&gt;프롬프트 스터핑&lt;/b&gt;, 그리고 &lt;b&gt;함수 호출&lt;/b&gt; 같은 기법이 있다. 특히 함수 호출을 통해 AI 모델이 외부 시스템의 실시간 데이터를 활용할 수 있게 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RAG(Retrieval Augmented Generation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG는 AI 모델의 프롬프트에 외부 데이터를 통합하여 더 정확한 응답을 생성하는 기법이다. 이는 벡터 데이터베이스와 결합되어, 유사한 내용을 찾아 프롬프트에 넣고 AI 모델에 전달한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 호출(Function Calling)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI는 대형 언어 모델(LLM)이 외부 API와 통신할 수 있도록 &lt;b&gt;함수 호출&lt;/b&gt; 기능을 지원한다. 이 기능은 모델이 외부 시스템의 API를 호출하여 필요한 데이터를 실시간으로 처리하게 해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI 응답 평가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI는 AI 모델이 생성한 응답을 평가하는 &lt;b&gt;Evaluator API&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;이 개념들을 바탕으로 Spring AI는 Java 개발자가 AI 기술을 손쉽게 애플리케이션에 통합하고 활용할 수 있는 다양한 도구를 제공하고 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OpenAI API Key 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://platform.openai.com/api-keys&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://platform.openai.com/api-keys&lt;/a&gt;에 접속하면 아래와 같은 화면을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;You와 Service account가 있는데 Service account를 선택하는 것이 좋다. 그 이유는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 개인 계정(You)을 사용하면 개인의 인증 정보와 애플리케이션의 인증 정보가 혼재되기 때문에, 여러 서비스에서 동일한 API 키를 사용할 때 보안 리스크가 커진다. 반면 Service Account는 애플리케이션에 필요한 최소한의 권한만 부여하여 권한 분리가 명확하게 이루어진다.&lt;br /&gt;2. 애플리케이션의  Service Account를 통해 발급된 Secret Key는 보다 쉽게 관리하고, 필요 시 교체(회전)할 수 있다. 개인 계정의 키를 사용하게 되면 개인이 계정을 변경하거나 보안 이슈가 발생할 때 모든 애플리케이션의 키를 다시 설정해야 하는 번거로움이 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 Service Account를 사용하자. Account ID는 프로젝트 명으로 하는 것이 좋다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-09 오후 5.39.12.png&quot; data-origin-width=&quot;3826&quot; data-origin-height=&quot;1916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oby7w/btsJxlFeMm2/Z1BUTgeKJVFPnOINSq6wOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oby7w/btsJxlFeMm2/Z1BUTgeKJVFPnOINSq6wOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oby7w/btsJxlFeMm2/Z1BUTgeKJVFPnOINSq6wOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foby7w%2FbtsJxlFeMm2%2FZ1BUTgeKJVFPnOINSq6wOK%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; alt=&quot;시크릿키 생성 1&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;361&quot; data-filename=&quot;스크린샷 2024-09-09 오후 5.39.12.png&quot; data-origin-width=&quot;3826&quot; data-origin-height=&quot;1916&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;span style=&quot;color: #ee2323;&quot;&gt;절대 깃 허브에 올리거나 외부에 노출하지 말아야 한다&lt;/span&gt;&lt;/b&gt;. 저 키가 노출되는 순간 요금 폭탄이 발생한다고 생각하는게 좋다. 혹시나 노출이 되었다면 즉시 해당 키를 폐기하고 재 발급 받자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-09 오후 5.43.41.png&quot; data-origin-width=&quot;3456&quot; data-origin-height=&quot;1946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9uZL1/btsJvpI6LvL/sC8gnyEyP3ZPEoLCDLj8GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9uZL1/btsJvpI6LvL/sC8gnyEyP3ZPEoLCDLj8GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9uZL1/btsJvpI6LvL/sC8gnyEyP3ZPEoLCDLj8GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9uZL1%2FbtsJvpI6LvL%2FsC8gnyEyP3ZPEoLCDLj8GK%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; alt=&quot;시크릿키 생성&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;405&quot; data-filename=&quot;스크린샷 2024-09-09 오후 5.43.41.png&quot; data-origin-width=&quot;3456&quot; data-origin-height=&quot;1946&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;h2 data-ke-size=&quot;size26&quot;&gt;크레딧 충전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 OpenAI의 API호출에는 토큰 수 기준으로 과금이 된다.(가격표: &lt;a href=&quot;https://openai.com/api/pricing/&quot;&gt;https://openai.com/api/pricing/&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;물론 OpenAI를 사용하지 않는 방법도 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 다른 상업용 AI 모델의 API를 사용 (Google의 Gemini, Meta의 LLaMA 등)&lt;br /&gt;2. 허깅페이스에 있는 모델(&lt;a href=&quot;https://huggingface.co/models&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://huggingface.co/models&lt;/a&gt;)을 다운받아 직접 로컬로 실행하는 방법 -&amp;gt; 내 컴퓨터가 GPT역할을 해야하므로 성능이 좋아야한다.&lt;br /&gt;3.직접 모델을 만드는 방법.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번엔 빠른 Spring AI 사용을 위해 OpenAI API를 사용하자.&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;다음 링크로 접속하면 Billing 화면이 보인다. &lt;a href=&quot;https://platform.openai.com/settings/organization/billing/overview&quot;&gt;https://platform.openai.com/settings/organization/billing/overview&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3452&quot; data-origin-height=&quot;1946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyXtBQ/btsJwqgtGzA/hzQ2wxRayignu5s25J7n11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyXtBQ/btsJwqgtGzA/hzQ2wxRayignu5s25J7n11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyXtBQ/btsJwqgtGzA/hzQ2wxRayignu5s25J7n11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyXtBQ%2FbtsJwqgtGzA%2FhzQ2wxRayignu5s25J7n11%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; alt=&quot;빌링 사이트&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;406&quot; data-origin-width=&quot;3452&quot; data-origin-height=&quot;1946&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 화면에서 `Add to credit balance` 버튼을 눌러, 카드 결제를 통해 충전할 수 있다(tax 별도...)&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;앞서 언급했듯이, OpenAI API의 요금은 토큰 수에 따라 부과된다. 참고로, Input 토큰(사용자가 API에 요청할 때 보낸 텍스트의 토큰 수)과 Output 토큰(OpenAI가 해당 요청에 대해 응답으로 생성한 텍스트의 토큰 수) 모두 요금 부과의 대상이다. &lt;br /&gt;토큰에 대한 정확한 기준은 없지만, 영어 텍스트의 경우 대략적으로 4글자가 1토큰으로 계산된다고 알려져있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://platform.openai.com/tokenizer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://platform.openai.com/tokenizer&lt;/a&gt; 사이트에 접속하여 테스트를 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;예: Hello, my name is Lee Sujae, and I want to become a good developer.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3398&quot; data-origin-height=&quot;1780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/orTk7/btsJwayhobB/Dj6TiR0TbUUIgUOal9txKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/orTk7/btsJwayhobB/Dj6TiR0TbUUIgUOal9txKk/img.png&quot; data-alt=&quot;67자에 19토큰&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/orTk7/btsJwayhobB/Dj6TiR0TbUUIgUOal9txKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2ForTk7%2FbtsJwayhobB%2FDj6TiR0TbUUIgUOal9txKk%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; alt=&quot;저렴한 영어&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;377&quot; data-origin-width=&quot;3398&quot; data-origin-height=&quot;1780&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;67자에 19토큰&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어로&amp;nbsp;입력해보면&amp;nbsp;토큰화를&amp;nbsp;비교적&amp;nbsp;잘&amp;nbsp;처리한다.&amp;nbsp;이제&amp;nbsp;한글을&amp;nbsp;사용해보자.&amp;nbsp;한글은&amp;nbsp;영어에&amp;nbsp;비해&amp;nbsp;같은&amp;nbsp;내용을&amp;nbsp;표현할&amp;nbsp;때&amp;nbsp;글자&amp;nbsp;수가&amp;nbsp;적으므로,&amp;nbsp;토큰&amp;nbsp;수가&amp;nbsp;줄어들&amp;nbsp;것으로&amp;nbsp;기대할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;예: 안녕, 내 이름은 이수재라고 해. 나는 좋은 개발자가 되고싶어.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3398&quot; data-origin-height=&quot;1780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw6XWS/btsJvYx3oJU/wIVih9KkuiebvfAYVmwsF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw6XWS/btsJvYx3oJU/wIVih9KkuiebvfAYVmwsF0/img.png&quot; data-alt=&quot;감히 한글을 써? 너는 1:1 교환이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw6XWS/btsJvYx3oJU/wIVih9KkuiebvfAYVmwsF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw6XWS%2FbtsJvYx3oJU%2FwIVih9KkuiebvfAYVmwsF0%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; alt=&quot;비싼 한국어&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;377&quot; data-origin-width=&quot;3398&quot; data-origin-height=&quot;1780&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;감히 한글을 써? 너는 1:1 교환이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 예상과 달리 한글은 더 많은 토큰을 차지한다. 거의 한 글자당 1토큰으로 계산되기 때문에, 비용 효율성을 고려하면 가능하면 영어로 입력하는 것이 좋다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 문장 부호(&amp;lsquo;, . ? 등)도 거의 각각 하나의 토큰으로 계산되므로, 가능하다면 사용하는 것을 줄이는 것이 좋다.&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;Spring AI 사용 준비가 끝났다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI - &lt;a href=&quot;https://spring.io/projects/spring-ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://spring.io/projects/spring-ai&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI API -&lt;a href=&quot;https://openai.com/api&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://openai.com/api&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ai</category>
      <category>GPT</category>
      <category>openAI</category>
      <category>spring ai</category>
      <category>스프링 ai</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/99</guid>
      <comments>https://soojae.tistory.com/99#entry99comment</comments>
      <pubDate>Mon, 9 Sep 2024 23:55:48 +0900</pubDate>
    </item>
    <item>
      <title>OAuth2.0이란?</title>
      <link>https://soojae.tistory.com/98</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (1).jpeg&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6SOTz/btsJvxl5TeN/fzCKZnQedSkeBXIREv3t8k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6SOTz/btsJvxl5TeN/fzCKZnQedSkeBXIREv3t8k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6SOTz/btsJvxl5TeN/fzCKZnQedSkeBXIREv3t8k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6SOTz%2FbtsJvxl5TeN%2FfzCKZnQedSkeBXIREv3t8k%2Fimg.jpg&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; alt=&quot;자물쇠가 있는 문&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;510&quot; data-filename=&quot;다운로드 (1).jpeg&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OAuth 2.0이란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OAuth 2.0&lt;/b&gt;은 인터넷 애플리케이션이 사용자의 자원에 안전하게 접근할 수 있도록 돕는 &lt;b&gt;인증 및 권한 부여 프레임워크&lt;/b&gt;다. 이 프레임워크는 사용자의 민감한 인증 정보를 직접 공유하지 않고, 대신 &lt;b&gt;액세스 토큰&lt;/b&gt;을 통해 애플리케이션이 자원에 접근할 수 있게 한다. 이 방식은 특히 클라우드 서비스나 API 통합에서 자주 사용된다.&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;h4 data-ke-size=&quot;size20&quot;&gt;예:&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 사용자가 구글 드라이브 API를 사용하는 A라는 서비스에 OAuth2를 통해 가입했다. &lt;br /&gt;2. 사용자는 OAuth2 인증을 거쳐 로그인한 후, 권한 요청 팝업이 표시되었고, 사용자는 구글 드라이브의 자원 접근을 허가했다.&amp;nbsp; &lt;br /&gt;3. 그 결과, A 서비스는 구글 드라이브에서 사용자의 사진을 다운로드할 수 있었다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. &lt;b&gt;Resource Owner (자원 소유자)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자원 소유자는 시스템에 저장된 데이터나 리소스의 실제 소유자를 의미한다. 대부분의 경우 자원 소유자는 &lt;b&gt;애플리케이션 사용자&lt;/b&gt;다. 이 사용자는 &lt;b&gt;자신의 자원을 제3자 애플리케이션이 접근할 수 있도록 허용&lt;/b&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;위의 예시 : &lt;b&gt;사용자&lt;/b&gt; (구글 드라이브에 저장된 사진을 소유하고 있는 사용자)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. &lt;b&gt;Client (클라이언트)&lt;/b&gt;&lt;/h4&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 예시 : &lt;b&gt;A 서비스&lt;/b&gt; (사용자의 구글 드라이브에 접근하여 사진을 다운로드하려는 애플리케이션)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. &lt;b&gt;Authorization Server (권한 부여 서버)&lt;/b&gt;&lt;/h4&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;b&gt;구글의 OAuth 2.0 권한 부여 서버&lt;/b&gt; (A 서비스에 액세스 토큰을 발급하는 역할)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. &lt;b&gt;Resource Server (자원 서버)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자원 서버는 클라이언트가 요청하는 자원을 호스팅하는 서버다. 클라이언트가&amp;nbsp;액세스&amp;nbsp;토큰을&amp;nbsp;전달하면,&amp;nbsp;자원&amp;nbsp;서버는&amp;nbsp;해당&amp;nbsp;클라이언트가&amp;nbsp;적절한&amp;nbsp;권한을&amp;nbsp;갖고&amp;nbsp;있는지&amp;nbsp;확인하고&amp;nbsp;자원에&amp;nbsp;대한&amp;nbsp;접근을&amp;nbsp;허용한다. 자원 서버는 보통 API 서버 형태로 존재한다.&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한 부여 방식 (Grant Types)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0에는 여러 가지 권한 부여 방식이 존재하며, 각각 특정한 시나리오에 맞게 설계되어 있다. 이는 클라이언트가 어떻게 자원 소유자에게 권한을 요청하고, 액세스 토큰을 발급받는지를 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. &lt;b&gt;Authorization Code Grant (인가 코드 방식)&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;OAuth 2.0에서 가장 널리 사용되는 인증 방식이다. 서버 기반 애플리케이션에서 주로 사용되며, 사용자가 권한 부여를 완료한 후 &lt;b&gt;인가 코드&lt;/b&gt;를 클라이언트에게 발급하고, 클라이언트는 이를 이용해 &lt;b&gt;액세스 토큰&lt;/b&gt;을 얻는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 예&lt;/b&gt;: 웹 애플리케이션에서 구글, 페이스북과 같은 제3자 인증 서버를 통해 로그인하는 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. &lt;b&gt;Client Credentials Grant (클라이언트 자격 증명 방식)&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;/li&gt;
&lt;li&gt;&lt;b&gt;사용 예&lt;/b&gt;: 백엔드 시스템 간 통신에서 리소스 접근을 제어할 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. &lt;b&gt;Refresh Token Grant (리프레시 토큰 방식)&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;/li&gt;
&lt;li&gt;&lt;b&gt;사용 예&lt;/b&gt;: 세션을 유지하면서 사용자에게 추가 인증을 요구하지 않도록 하는 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. &lt;b&gt;Password Grant (패스워드 방식)&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;li&gt;&lt;b&gt;사용 예&lt;/b&gt;: 레거시 시스템이나 간단한 내부 애플리케이션에서 사용될 수 있지만, 현재는 거의 사용되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. &lt;b&gt;Device Authorization Grant (디바이스 코드 방식)&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;사용자가 UI가 제한된 디바이스에서 인증할 때 사용하는 방식이다. 사용자는 다른 디바이스에서 웹 브라우저를 통해 권한 부여 절차를 완료하고, 디바이스는 &lt;b&gt;디바이스 코드&lt;/b&gt;를 사용해 액세스 토큰을 받는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 예&lt;/b&gt;: 스마트 TV나 게임 콘솔 같은 제한된 디바이스에서 사용자가 인증할 때. 맥북으로 스마트 TV를 미러링 할때 입력하는 4자리 값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;6. &lt;b&gt;Implicit Grant (암시적 방식)&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;을 바로 받는 방식으로, 주로 클라이언트 애플리케이션이 브라우저에서 실행되는 경우 사용됩니다. 보안 문제로 OAuth 2.1에서는 사용이 권장되지 않으며, 대부분의 경우 &lt;b&gt;Authorization Code Grant&lt;/b&gt;로 대체된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 예&lt;/b&gt;: 과거에 주로 사용되었으나 현재는 거의 사용되지 않는 방식.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OAuth 2.0의 장점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. &lt;b&gt;보안 강화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0은 자원 소유자의 &lt;b&gt;민감한 자격 증명&lt;/b&gt;을 클라이언트 애플리케이션과 직접 공유하지 않고, &lt;b&gt;액세스 토큰&lt;/b&gt;을 사용해 자원에 접근할 수 있도록 한다. 이렇게 하면 자격 증명 유출 위험을 줄이고, 토큰의 만료나 리프레시를 통해 보안을 추가적으로 강화할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. &lt;b&gt;유연성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0은 다양한 인증 흐름을 제공하여, 웹 애플리케이션, 모바일 앱, 서버 간 통신 등 여러 상황에 맞게 사용할 수 있다. 각 흐름은 특정한 사용 사례에 맞게 설계되어 있어, 상황에 맞는 최적의 인증 방식을 선택할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. &lt;b&gt;확장성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0은 다양한 클라이언트와 서비스 간의 인증 및 권한 부여 과정을 쉽게 확장할 수 있는 프레임워크다. 예를 들어, 하나의 인증 서버를 통해 여러 애플리케이션이 공통으로 인증 및 권한 부여 절차를 사용할 수 있다. 이를 통해 복잡한 통합 시나리오에서도 일관된 방식으로 사용자 인증을 처리할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. &lt;b&gt;서드파티 애플리케이션 통합&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0은 서드파티 애플리케이션이 사용자의 자원에 안전하게 접근할 수 있도록 하는 표준화된 방식을 제공한다. 이를 통해 사용자는 신뢰하는 서드파티 애플리케이션이 자신을 대신해 자원에 접근하도록 허용할 수 있다. 이 과정에서 사용자는 자신의 자격 증명을 제공하지 않고도, 특정한 자원에 대한 접근 권한만을 안전하게 위임할 수 있다.&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;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;: 사용자들이 페이스북, 구글, 트위터 등 소셜 미디어 계정을 사용해 다른 서비스에 로그인할 수 있는 기능이 OAuth 2.0을 통해 구현된다. 사용자는 별도의 아이디와 비밀번호를 생성하지 않고, 기존의 소셜 계정을 통해 권한을 위임받아 다양한 애플리케이션에 로그인할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 접근 제어&lt;/b&gt;: 많은 서비스가 외부 애플리케이션이나 파트너가 자신들의 API를 통해 데이터를 요청할 수 있도록 한다. 이때 OAuth 2.0을 사용하여 외부 애플리케이션이 API에 접근할 수 있는 권한을 안전하게 부여할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라우드 서비스 통합&lt;/b&gt;: 다양한 클라우드 서비스 간의 데이터 통합이나 상호작용을 가능하게 한다. 예를 들어, 구글 드라이브에 저장된 파일을 다른 서비스에서 접근할 때 OAuth 2.0을 통해 구글 계정 자격 증명을 사용하지 않고도 안전하게 파일에 접근할 수 있다.&lt;/li&gt;
&lt;/ul&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;OAuth 2.0은 다양한 인터넷 서비스 간의 &lt;b&gt;안전한 인증 및 권한 부여&lt;/b&gt;를 제공하는 강력한 프레임워크다. 이 프로토콜을 사용하면 클라이언트 애플리케이션이 사용자의 자원에 안전하게 접근할 수 있으며, 여러 상황에 맞춘 유연한 인증 흐름을 통해 각 서비스 간의 통합을 쉽게 확장할 수 있다. OAuth 2.0을 도입하면 애플리케이션의 보안을 강화하고, 사용자 경험을 개선할 수 있는 중요한 인증 방식이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ChatGPT&lt;/p&gt;</description>
      <category>Knowledge/Security</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/98</guid>
      <comments>https://soojae.tistory.com/98#entry98comment</comments>
      <pubDate>Mon, 9 Sep 2024 00:25:15 +0900</pubDate>
    </item>
    <item>
      <title>해싱(Hashing)과 일반 해시 함수(non-cryptographic hash function)</title>
      <link>https://soojae.tistory.com/90</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (1).jpeg&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKDDNj/btsJiqtvs74/DkKiBFEDdC6geKPjUxb0Ek/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKDDNj/btsJiqtvs74/DkKiBFEDdC6geKPjUxb0Ek/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKDDNj/btsJiqtvs74/DkKiBFEDdC6geKPjUxb0Ek/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKDDNj%2FbtsJiqtvs74%2FDkKiBFEDdC6geKPjUxb0Ek%2Fimg.jpg&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; alt=&quot;자물쇠 그림&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;510&quot; data-filename=&quot;다운로드 (1).jpeg&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;510&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해싱(Hashing)&lt;/b&gt;은 &lt;b&gt;가변 길이의 입력 데이터를 받아서 고정 길이의 출력 값으로&amp;nbsp;변환&lt;/b&gt;하는 과정을 의미한다. 이때 생성된 고정된 크기의 값은 &lt;b&gt;해시 값&lt;/b&gt; 또는 &lt;b&gt;다이제스트(digest)&lt;/b&gt;라고 불리며, 원본 데이터와는 크기나 형식이 다르다. 해싱은 &lt;b&gt;데이터의 무결성 검증&lt;/b&gt;, &lt;b&gt;빠른 데이터 검색&lt;/b&gt;, &lt;b&gt;비밀번호 저장&lt;/b&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;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;: 서로 다른 입력값이 동일한 해시 값을 생성할 확률은 매우 낮다. 이를 &lt;b&gt;충돌 저항성&lt;/b&gt;이라고 하며, 이 특성 덕분에 해싱은 데이터의 무결성을 검증하는 데 유용하다. 이상적으로는 해시 함수는 서로 다른 두 개의 입력값이 동일한 해시 값을 생성하지 않도록 설계된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고정된 출력 크기&lt;/b&gt;: 해시 함수는 입력 데이터의 크기와 상관없이 항상 고정된 크기의 해시 값을 생성한다. 예를 들어, SHA-256 해시 함수는 256비트 크기의 해시 값을 생성한다.&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;다음 해시 함수를 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1724769305572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public int hashCode() {
    int h = hash;
    if (h == 0 &amp;amp;&amp;amp; value.length &amp;gt; 0) {
        char val[] = value;
        for (int i = 0; i &amp;lt; value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Effective Java에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Joshua Bloch는 값 31이 &lt;b&gt;홀수&lt;/b&gt;와 &lt;b&gt;소수&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;짝수를 곱할 때&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자 5(이진수 101)를 짝수 2로 곱하면 10(이진수 1010)이 된다.&lt;/li&gt;
&lt;li&gt;숫자 6(이진수 110)을 짝수 2로 곱하면 12(이진수 1100)이 된다.&lt;/li&gt;
&lt;li&gt;짝수를 곱할 시 마지막 비트가 항상 0이 되므로, 표현 가능한 숫자 범위의 절반 밖에 사용하지 못한다. 그래서 같은 해시 값, 즉 해시 충돌이 일어날 가능성이 높아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;홀수를 곱할 때&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자 5(이진수 101)를 홀수 3으로 곱하면 15(이진수 1111)가 된다.&lt;/li&gt;
&lt;li&gt;숫자 6(이진수 110)을 홀수 3으로 곱하면 18(이진수 10010)이 된다.&lt;/li&gt;
&lt;li&gt;마지막 비트까지 전부 사용할 수 있다. 그러므로 서로 다른 해시 값을 가질 가능성이 높아지고, 충돌이 줄어든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시와 소수(prime number)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소수는 1과 자기 자신 외에는 나눌 수 없는 수. 소수는 약수가 적기 때문에 해시 코드 생성 시 충돌을 줄이는 데 도움이 된다. 즉, 해시 함수에서 소수를 사용하면 해시 값의 분포가 더 고르게 퍼지게 되어, 서로 다른 입력값이 같은 해시 값을 가질 확률이 낮아진다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에 해쉬 곱셈 상수 31을 사용하는 이유 중 하나로 31이 2^5 - 1이어서 쉬프트 연산을 통해 (N &amp;lt;&amp;lt; 5) - N과 같이 속도를 개선할 수 있다고 한다. 그렇다면 31(홀수 + 상수 + 쉬프트 연산)과 19(홀수 + 상수)사이에 실제로 속도 차이가 있을까? 1억번씩 호출하여 비교해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 곱셈 상수 31 vs 19 속도 비교&lt;/h3&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package org.example;

public class HashTestMain {

  // 해시 상수 31을 사용하는 클래스
  static class HashWith31 {
    private String value;

    public HashWith31(String value) {
      this.value = value;
    }

    @Override
    public int hashCode() {
      int result = 1;
      result = 31 * result + (value != null ? value.hashCode() : 0);
      return result;
    }
  }

  // 해시 상수 19를 사용하는 클래스
  static class HashWith19 {
    private String value;

    public HashWith19(String value) {
      this.value = value;
    }

    @Override
    public int hashCode() {
      int result = 1;
      result = 19 * result + (value != null ? value.hashCode() : 0);
      return result;
    }
  }

  public static void main(String[] args) {
    // 테스트할 문자열
    String testString = &quot;This is a test string for hashCode performance measurement.&quot;;

    // 반복 횟수
    int iterations = 100_000_000;

    // HashWith19 클래스의 속도 측정
    HashWith19 hash19 = new HashWith19(testString);
    long startTime19 = System.nanoTime();
    int hash19Result = 0;
    for (int i = 0; i &amp;lt; iterations; i++) {
      hash19Result = hash19.hashCode() + i; // 캐싱 방지를 위해 i를 더함
    }
    long endTime19 = System.nanoTime();
    long totalTime19 = endTime19 - startTime19;

    // HashWith31 클래스의 속도 측정
    HashWith31 hash31 = new HashWith31(testString);
    long startTime31 = System.nanoTime();
    int hash31Result = 0;
    for (int i = 0; i &amp;lt; iterations; i++) {
      hash31Result = hash31.hashCode() + i; // 캐싱 방지를 위해 i를 더함
    }
    long endTime31 = System.nanoTime();
    long totalTime31 = endTime31 - startTime31;

    System.out.println(&quot;Hash 곱셈 상수 19를 사용했을 때 총 시간: &quot; + totalTime19 + &quot; nanoseconds&quot;);
    System.out.println(&quot;Hash 곱셈 상수 31을 사용했을 때 총 시간: &quot; + totalTime31 + &quot; nanoseconds&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 시에는 19와 31 각각 주석처리를 하고 독립적으로 실행하였다. 결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-28 오전 3.15.26.png&quot; data-origin-width=&quot;445&quot; data-origin-height=&quot;33&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cauRhr/btsJhjPKzZP/rPnrAD9Pwh8bVrvckzcC9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cauRhr/btsJhjPKzZP/rPnrAD9Pwh8bVrvckzcC9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cauRhr/btsJhjPKzZP/rPnrAD9Pwh8bVrvckzcC9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcauRhr%2FbtsJhjPKzZP%2FrPnrAD9Pwh8bVrvckzcC9K%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; alt=&quot;Hash 19&quot; loading=&quot;lazy&quot; width=&quot;445&quot; height=&quot;33&quot; data-filename=&quot;스크린샷 2024-08-28 오전 3.15.26.png&quot; data-origin-width=&quot;445&quot; data-origin-height=&quot;33&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-28 오전 3.15.40.png&quot; data-origin-width=&quot;445&quot; data-origin-height=&quot;33&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kF4Dr/btsJiSJZ5AN/nALgTMZO4iDKm1T2LSmByk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kF4Dr/btsJiSJZ5AN/nALgTMZO4iDKm1T2LSmByk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kF4Dr/btsJiSJZ5AN/nALgTMZO4iDKm1T2LSmByk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkF4Dr%2FbtsJiSJZ5AN%2FnALgTMZO4iDKm1T2LSmByk%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; alt=&quot;Hash 31&quot; loading=&quot;lazy&quot; width=&quot;445&quot; height=&quot;33&quot; data-filename=&quot;스크린샷 2024-08-28 오전 3.15.40.png&quot; data-origin-width=&quot;445&quot; data-origin-height=&quot;33&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해쉬 곱셈 상수가 19일 때 오히려 더 빠르다. 여러 번 테스트해본 결과, 31이 더 빠를 때도 있지만 차이는 크지 않다. 컴퓨터의 성능이 향상된 덕분인지, 이제는 31을 사용한다고 속도가 개선된다는 것은 과거의 이야기인 것 같다.&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;HashCode()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hashCode() 메서드의 실제 동작을 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1724772352330&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    String anotherTest = &quot;java&quot;;
    String test = &quot;test&quot;;
    String oneMoreTest = &quot;dev&quot;;
    System.out.println(test.hashCode()); // output: 3556498
    System.out.println(test.hashCode()); // output: 3556498
    System.out.println(anotherTest.hashCode()); // output: 3254818
    System.out.println(oneMoreTest.hashCode()); // output: 99349
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 보면 해시 함수 특징을 알 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hashCode() 호출은 단방향 연산으로, 생성된 해시 값만으로는 원래 문자열을 복구할 수 없다.&lt;/li&gt;
&lt;li&gt;결정론적이다. 동일한 변수를 두 번 호출했을 때, 동일한 해시 값을 반환한다.&lt;/li&gt;
&lt;li&gt;Java의 int는 32비트(4바이트)의 고정된 크기를 가진다. 즉 hashCode()메서드가 반환하는 해시 값(int 형태)도 항상 32비트의 고정된 길이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일반 해시 함수?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 해시 함수(Non-cryptographic hash function)는 보안 목적으로 설계되지 않은 해시 함수로, 주로 데이터 검색, 해시 테이블, 체크섬과 같은 용도에 사용된다. 일반 해시 함수는 빠른 계산 속도와 충돌을 적절히 처리할 수 있는 성능을 중시하지만, 암호화 수준의 보안성을 제공하지는 않는다.&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;&lt;b&gt;빠른 계산&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;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MurmurHash&lt;/b&gt;: 매우 빠르고 충돌이 적은 해시 함수로, 주로 해시 테이블에서 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CityHash&lt;/b&gt;: 구글에서 개발한 해시 함수로, 매우 빠르면서도 높은 성능을 제공한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DJB2&lt;/b&gt;: 단순하고 빠르며, 자주 사용되는 해시 함수 중 하나이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java HashMap은 어떻게 동작하는가? - &lt;a href=&quot;https://d2.naver.com/helloworld/831311&quot;&gt;https://d2.naver.com/helloworld/831311&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deep Dive into Hasing - &lt;a href=&quot;https://www.baeldung.com/cs/hashing&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.baeldung.com/cs/hashing&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Knowledge/Data Structure</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/90</guid>
      <comments>https://soojae.tistory.com/90#entry90comment</comments>
      <pubDate>Wed, 28 Aug 2024 02:00:01 +0900</pubDate>
    </item>
    <item>
      <title>[K8S] labels, selector, matchLabels에 대하여</title>
      <link>https://soojae.tistory.com/89</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPbiFy/btsmFhmSjoh/39bIC1pZa104phHeIZgHr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPbiFy/btsmFhmSjoh/39bIC1pZa104phHeIZgHr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPbiFy/btsmFhmSjoh/39bIC1pZa104phHeIZgHr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPbiFy%2FbtsmFhmSjoh%2F39bIC1pZa104phHeIZgHr1%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; alt=&quot;kubernetes 로고&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개인적으로 공부한 내용을 작성한 게시글입니다. 잘못된 내용이 있을 수 있습니다. &lt;br /&gt;틀린 부분을 알려주시면 바로 고치겠습니다.감사합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 리소스의 Manifest 파일들을 작성하다보면 많은 labels, matchLabels, selectors 필드들을 볼 수 있다. &lt;br /&gt;이번 포스팅을 통해 이들을 정리해보려고 한다.&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Selector &lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Deployment&lt;/span&gt;&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; letter-spacing: 0px;&quot;&gt;우선 Deployment의 Manifest 파일을 살펴보자&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1688278286278&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
  labels:
    app: hello
spec:
  selector:
    matchLabels:
      app: hello
      track: stable
    matchExpressions:
      - {key: tier, operator: In, values: [backend, backend2, backend3]}
  replicas: 3
  template:
    metadata:
      labels:
        app: hello
        tier: backend
        track: stable
    spec:
      containers:
        - name: hello
          image: &quot;gcr.io/google-samples/hello-go-gke:1.0&quot;
          ports:
            - name: http
              containerPort: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;`metadata`필드의 `labels`필드는 Deployment자체의 label 정보를 의미한다.&lt;/li&gt;
&lt;li&gt;`selector`필드의 `matchLabels`와 `matchExpressions`필드는 배포할 Pod의 labels를 가리킨다.&lt;/li&gt;
&lt;li&gt;`matchLabels`와 `matchExpressions`는 각각의 필드에 AND(&amp;amp;&amp;amp;) 연산자가 사용되고, `matchLabels`와 `matchExpressions` 끼리도 AND(&amp;amp;&amp;amp;) 연산자가 사용된다.&lt;/li&gt;
&lt;li&gt;`template`필드부터는 사실 Pod의 metadata라고 생각하면 된다. 그래서 `template`필드의 `labels`필드는 Deployment의 label 정보가 아닌 Pod의 자체의 label 정보이다.&lt;/li&gt;
&lt;li&gt;그래서 꼭 `&lt;span style=&quot;color: #ee2323;&quot;&gt;selector.matchLabels`와 `template.metadata.labels`는 일치&lt;/span&gt;해야 한다!&lt;/li&gt;
&lt;li&gt;라벨을 지정하면 다음 커맨드로 해당 라벨이 존재하는 리소스들을 조회할 수 있다. `$ kubectl get &amp;lt;resource&amp;gt; -l &amp;lt;label&amp;gt;`&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Service&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Service의 Manifest 파일을 살펴보자&lt;/p&gt;
&lt;pre id=&quot;code_1688280050906&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: hello
  labels:
    app: hello
spec:
  selector: # matchLabels가 존재하지 않는다.
    app: hello
    tier: backend
  ports:
  - protocol: TCP
    port: 80
    targetPort: http&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;`metadata`필드의 `labels`필드는 Service의 label 정보를 의미한다.&lt;/li&gt;
&lt;li&gt;`selector`필드는 해당 값의 label을 가진 Pod들을 Service 대상으로 지정한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;`matchLabels`필드와 `matchExpressions`필드가 존재하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 확인할 수 있듯이 `matchLabels`, `matchExpresisons` 필드가 Service에는 존재하지 않는다. 대신 `selector`필드만으로도 `matchLabels`처럼 동작한다. &lt;br /&gt;사실 Deployment뿐만 아니라&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;Job, ReplicaSet, 그리고 DaemonSet에서도 `matchLabels`, `matchExpressions`필드가 있다. &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;text-align: start;&quot;&gt;그렇다면&lt;/span&gt; 왜 Service만 matchLabels, matchExpressions 필드가 없을까?&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;Job, Deployment, ReplicaSet, DaemonSet은 다양한 Pod들을 생성하거나 직접적으로 제어해야 하기 때문에&amp;nbsp; `matchExpressions`를 사용한다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에, Service는 네트워크 요청을 &lt;b&gt;기존에 존재하고,&lt;/b&gt;&amp;nbsp;&lt;b&gt;동일한 역할을 수행하는&lt;/b&gt; Pod들에 라우팅 하는 것이기 때문에, &lt;b&gt;라벨을 명확하게 정하는 것이 좀 더 안정적&lt;/b&gt;일 것이다&lt;b&gt;. &lt;br /&gt;&lt;/b&gt;그래서 `matchExpressions`를 삭제하고, `selector` 만으로 `matchLabels` 역할을 하도록 한 것이 아닐까 생각한다. (괜히 matchLabels가 존재하면, matchExpressions 기능도 있을 것이라 생각하여 작성할 수 있으므로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;ETC&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Equality-based, Set-based Requirement&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 위의 `matchLabels`는 Equality-based requirement(동등 기반 요구)방식, `matchExpression`는 Set-based Requirement(집합 기만 요구)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API는 현재 Equality-based, Set-based이라는 두 가지 유형의 selector를 지원한다. label selector는 쉼표로 여러 label을 선택할 수 있는데, 이때 쉼표는 AND(&amp;amp;&amp;amp;) 연산자 역할을 한다. OR(||) 연산자는 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Equality-based requirement&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Equality-based 또는 InEquality-based requirements를 통해 label의 keys와 values로 필터링할 수 있다.&amp;nbsp; `=, ==, !=` 연산자를 이용하여 필터링할 수 있다. `=`, `==` 은 둘 다 동등함을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Set-based requirement&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Set-based label requirements는 values 집합에 따라 keys를 필터링할 수 있다. in, notin 그리고 exists (키 식별자만) 연산자를 이용하여 필터링할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688283682284&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;environment in (production, qa)
tier notin (frontend, backend)
partition
!partition
partition,environment notin (qa)&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;첫 번째는 key가 `environment`일 경우, value가 `production` 또는 `qa`인 label을 필터링한다.&lt;/li&gt;
&lt;li&gt;두 번째는 key가 `tier`일 경우, value가 `frontend`, `backend`가 &lt;span style=&quot;color: #ee2323;&quot;&gt;아닌&lt;/span&gt; label을 필터링한다&lt;/li&gt;
&lt;li&gt;세 번째는 value를 체크하지 않는다. 오직 `partition`인 key가 있는 label을 필터링한다.&lt;/li&gt;
&lt;li&gt;네 번째도 value를 체크하지 않는다. 오직 `partition`가 &lt;span style=&quot;color: #ee2323;&quot;&gt;아닌&lt;/span&gt; key가 있는 label을 필터링한다.&lt;/li&gt;
&lt;li&gt;다섯 번째는 key가 `partition` 또는 `environment`일 때 value가 `qa`가 &lt;span style=&quot;color: #ee2323;&quot;&gt;아닌&lt;/span&gt; key가 있는 label을 필터링한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 따라서, Service 리소스는 Equality-based selector를 사용하는 반면, 나머지 리소스들은 Equality-based selector와 Set-based selector 모두 사용한다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;labels 작성 예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;release : &quot;stable&quot;,&lt;span&gt; &lt;/span&gt;&quot;canary&quot;&lt;/li&gt;
&lt;li&gt;environment : &quot;dev&quot;,&amp;nbsp;&quot;qa&quot;,&amp;nbsp;&quot;production&quot;&lt;/li&gt;
&lt;li&gt;tier : &quot;frontend&quot;,&lt;span&gt; &lt;/span&gt;&quot;backend&quot;, &quot;cache&quot;&lt;/li&gt;
&lt;li&gt;partition : &quot;customerA&quot;, &quot;customerB&quot;&lt;/li&gt;
&lt;li&gt;track : &quot;daily&quot;, &quot;weekly&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Assigning Pods to Nodes - &lt;a href=&quot;https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Labels and Selectors - &lt;a href=&quot;https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/&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;</description>
      <category>Infra/Kubernetes</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/89</guid>
      <comments>https://soojae.tistory.com/89#entry89comment</comments>
      <pubDate>Thu, 6 Jul 2023 20:47:19 +0900</pubDate>
    </item>
    <item>
      <title>[K8S] 쿠버네티스를 이용하여 Elasticsearch, Kibana, Logstash 배포 (with helm)</title>
      <link>https://soojae.tistory.com/88</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_k8s.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xsa4Z/btsh2GKsVMr/1GImi5LSSIwbNJsm07FUe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xsa4Z/btsh2GKsVMr/1GImi5LSSIwbNJsm07FUe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xsa4Z/btsh2GKsVMr/1GImi5LSSIwbNJsm07FUe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxsa4Z%2Fbtsh2GKsVMr%2F1GImi5LSSIwbNJsm07FUe0%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; alt=&quot;k8s 로고&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;506&quot; data-filename=&quot;edited_k8s.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개인적으로 공부한 내용을 작성한 게시글입니다. 잘못된 내용이 있을 수 있습니다. &lt;br /&gt;틀린 부분을 알려주시면 바로 고치겠습니다. 감사합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 K8S 이용하여 Elasticsearch를 배포해 보자. Helm이라는 패키징 툴을 이용해서 배포할 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Helm 이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스를 통해 애플리케이션을 배포하기 위해서는 많은 리소스들이 필요하다. 대표적으로 Pod, Deployment, Statefulset, DaemonSet, Service, ConfigMap, Secret, PersistentVolume, PersistentVolumeClaim, Ingress 등등&lt;br /&gt;위의 리소스들의 템플릿은 비슷한데, 배포하기 위해서는 애플리케이션마다 모두 yaml파일을 만들어야 하고, 관리를 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 반해 helm을 이용하면 공통적인 템플릿을 만들어 Value 값만 바꿔가며 환경, 프로젝트에 따라 다양하게 설정할 수 있어 관리가 용이해진다.&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;`$ brew install helm`&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;를 통해 helm을 설치할 수 있다.&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;elasticsearch 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bitnami의 elasticsearch에서 제공하는 values.yaml 파일의 설정을 사용하여 배포해 보자. `-f values.yaml` 옵션은  helm 차트를 values.yaml 파일의 값으로 커스터마이징 하라는 뜻이다.&lt;br /&gt;이를 통해 stage환경에서는 `values_stage.yaml`, production 환경에서는 `values_production.yaml` 등의 파일을 따로 만들어서 환경에 따라 쉽게 설정을 적용할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;values.yaml 다운로드&lt;/h4&gt;
&lt;pre id=&quot;code_1685444154307&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ helm repo add bitnami https://charts.bitnami.com/bitnami

$ helm repo update

$ helm search repo -l bitnami/elasticsearch | head

$ wget https://raw.githubusercontent.com/bitnami/charts/master/bitnami/elasticsearch/values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;배포&lt;/h4&gt;
&lt;pre id=&quot;code_1685444315744&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# jerry라는 네임스페이스 생성
$ kubectl create namespace jerry

# values.yaml 파일을 이용하여 my-elasticsearch 라는 이름의 애플리케이션 배포
$ helm install my-elasticsearch -f values.yaml --set sysctlImage.enabled=true -n jerry bitnami/elasticsearch&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;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;아래와 같은 메시지가 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;605&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LsEoR/btshG81Fafh/QpISmDiTdL3GDHkkkeTAF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LsEoR/btshG81Fafh/QpISmDiTdL3GDHkkkeTAF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LsEoR/btshG81Fafh/QpISmDiTdL3GDHkkkeTAF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLsEoR%2FbtshG81Fafh%2FQpISmDiTdL3GDHkkkeTAF1%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; alt=&quot;es 배포&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;605&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;605&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;s&gt;$ kubectl get pods -n jerry&lt;/s&gt; 로 pods이 정상적으로 동작하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CGfR1/btsh33rrw1j/JrOaUWt0zkOMSbATqujeJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CGfR1/btsh33rrw1j/JrOaUWt0zkOMSbATqujeJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CGfR1/btsh33rrw1j/JrOaUWt0zkOMSbATqujeJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCGfR1%2Fbtsh33rrw1j%2FJrOaUWt0zkOMSbATqujeJ1%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; alt=&quot;pods 확인&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;174&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;174&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;`$ &amp;nbsp;kubectl port-forward -n jerry svc/my-elasticsearch 9200:9200 &amp;amp;` 커맨드를 통해 쿠버네티스 클러스터 내부의 서비스를 로컬 머신에서 직접 접근할 수 있도록 port-forwading 해준다.&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;`$ curl localhost:9200` 커맨드를 입력하면 elasticsearch을 정상적으로 호출되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djJDO4/btsh028qPFw/Jzc3Z0VOOirKao1fOqo2a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djJDO4/btsh028qPFw/Jzc3Z0VOOirKao1fOqo2a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djJDO4/btsh028qPFw/Jzc3Z0VOOirKao1fOqo2a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjJDO4%2Fbtsh028qPFw%2FJzc3Z0VOOirKao1fOqo2a0%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; alt=&quot;포워딩 후 호출&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;316&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;316&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;추가로 kibana와 logstash도 배포해 보자. &lt;br /&gt;이번에는 values.yaml 파일없이 bitnami에서 설정한 default 값을 사용하고, elasticsearch에 연결하기 위해 호스트 값과 포트번호만&amp;nbsp; set 옵션을 통해 바꿔보자.&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;kibana 배포&lt;/h3&gt;
&lt;pre id=&quot;code_1685368978382&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm install my-kibana oci://registry-1.docker.io/bitnamicharts/kibana \
  --set &quot;elasticsearch.hosts[0]=elasticsearch.jerry.svc.cluster.local&quot; \
  --set elasticsearch.port=9200 \
-n jerry&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3JEtH/btshZwoqNRr/w15aLp2aGf9F1wbW7MDgik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3JEtH/btshZwoqNRr/w15aLp2aGf9F1wbW7MDgik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3JEtH/btshZwoqNRr/w15aLp2aGf9F1wbW7MDgik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3JEtH%2FbtshZwoqNRr%2Fw15aLp2aGf9F1wbW7MDgik%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; alt=&quot;kibana 확인&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;382&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;382&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;h3 data-ke-size=&quot;size23&quot;&gt;logstash 배포&lt;/h3&gt;
&lt;pre id=&quot;code_1685369148791&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm install my-logstash bitnami/logstash \
  --set elasticsearch.host=my-elasticsearch-master.jerry.svc.cluster.local \
  --set elasticsearch.port=9200 \
-n jerry&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brNy2Y/btsh2QzmjxE/Cb3yGXWqKQbOHYDzpN1jG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brNy2Y/btsh2QzmjxE/Cb3yGXWqKQbOHYDzpN1jG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brNy2Y/btsh2QzmjxE/Cb3yGXWqKQbOHYDzpN1jG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrNy2Y%2Fbtsh2QzmjxE%2FCb3yGXWqKQbOHYDzpN1jG0%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; alt=&quot;logstash 확인&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;423&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;423&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 모든 배포된 애플리케이션의 리소스 들이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zJUgg/btsh2nj8RqI/hLv9mg2ku07gJbT6wP3McK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zJUgg/btsh2nj8RqI/hLv9mg2ku07gJbT6wP3McK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zJUgg/btsh2nj8RqI/hLv9mg2ku07gJbT6wP3McK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzJUgg%2Fbtsh2nj8RqI%2FhLv9mg2ku07gJbT6wP3McK%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; alt=&quot;전체 리소스&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;584&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;584&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ETC&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서는 공식 elasticsearch 이미지가 아닌&amp;nbsp; bitnami/elasticsearch 이미지를 사용했다.&lt;/p&gt;
&lt;figure id=&quot;og_1685338504873&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;Docker&quot; data-og-description=&quot;&quot; data-og-host=&quot;hub.docker.com&quot; data-og-source-url=&quot;https://hub.docker.com/r/bitnami/elasticsearch&quot; data-og-url=&quot;https://hub.docker.com/r/bitnami/elasticsearch&quot; data-og-image=&quot;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://hub.docker.com/r/bitnami/elasticsearch&quot; data-source-url=&quot;https://hub.docker.com/r/bitnami/elasticsearch&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('&amp;quot;&amp;quot;');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Docker&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;hub.docker.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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Bitnami와 Official 이미지의 차이점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 설정 - BItnami는 애플리케이션에 배포와 설정을 단순화하는데 중점을 두고 있다. 실제 서비스에 적용하기 위한 기본 보안 설정 등이 이루어져 있다. 반면 공식 이미지는 사용자가 환경에 맞게 설정을 수정해야 할 수도 있다.&lt;/li&gt;
&lt;li&gt;보안 설정: Bitnami는 보안에 많은 중점을 둔다. 이는 초기 설정에서부터 사용자가 적절한 보안 구성을 가지도록 도와준다.&lt;/li&gt;
&lt;li&gt;유지 보수와 업데이트 -&amp;nbsp; Bitnami와 공식 이미지 모두 자체 업데이트와 유지 보수 메커니즘이 있지만, Bitnami는 더욱 체계적인 프로세스를 가지고 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;커스텀: Bitnami의 경우 공식 이미지에 비해 사용자의 커스텀 설정이 제한적일 수 있다.&lt;/li&gt;
&lt;li&gt;공식 지원 불가: Bitnami 이미지는 공식 개발자가 지원하지 않기 때문에, 문제 발생 시 Bitnami 커뮤니티나 Bitnami 지원 팀에 의존해야 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bitnami elasticsearch 배포 - &lt;a href=&quot;https://itsmetommy.com/2020/06/10/kubernetes-install-bitnami-elasticsearch-kibana-using-helm/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://itsmetommy.com/2020/06/10/kubernetes-install-bitnami-elasticsearch-kibana-using-helm/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bitnami에서 values_procduction.yaml의 지원 중단 - &lt;a href=&quot;https://github.com/bitnami/charts/issues/5095&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/bitnami/charts/issues/5095&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra/Kubernetes</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/88</guid>
      <comments>https://soojae.tistory.com/88#entry88comment</comments>
      <pubDate>Tue, 6 Jun 2023 01:31:42 +0900</pubDate>
    </item>
    <item>
      <title>[K8S] MacOS M2 환경 쿠버네티스 설치 (Docker Desktop)</title>
      <link>https://soojae.tistory.com/87</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_k8s.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5VTBr/btshMV1BTBy/DcAf4IVQdw6JRpWlGZWeCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5VTBr/btshMV1BTBy/DcAf4IVQdw6JRpWlGZWeCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5VTBr/btshMV1BTBy/DcAf4IVQdw6JRpWlGZWeCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5VTBr%2FbtshMV1BTBy%2FDcAf4IVQdw6JRpWlGZWeCK%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; alt=&quot;kubernetes 로고&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-filename=&quot;edited_k8s.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간에는 Kubernetes를 Docker Desktop를 사용하여 로컬환경에 설치하려고 한다. 이는 &lt;b&gt;Lightweight Kubernetes Distributions &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;Lightweight Kubernetes Distributions에는 Docker Desktop 이외에 Minikube, MicroK8S, Kind, K3S등이 있다. 아래 링크에  들의 비교글이 있다.&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://www.baeldung.com/ops/kubernetes-lightweight-distributions#comparison&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/ops/kubernetes-lightweight-distributions#comparison&lt;/a&gt;&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;설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 우선 Docker-desktop을 설치하자. 아래 링크로 들어가 Apple Chip 버전을 다운로드 후 설치하면 된다.&lt;/p&gt;
&lt;figure id=&quot;og_1685286139600&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;Download Docker Desktop | Docker&quot; data-og-description=&quot;Docker Desktop is available to download for free on Mac, Windows, or Linux operating systems. Get started with Docker today!&quot; data-og-host=&quot;www.docker.com&quot; data-og-source-url=&quot;https://www.docker.com/products/docker-desktop&quot; data-og-url=&quot;https://www.docker.com/products/docker-desktop/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bkHqHS/hySLABsi4a/ZGmaHlsTaVT4kgGa8VNvyK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dbpQvL/hySLEKA5SP/QRNBn9OpxoPPo3PkYHk7Vk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.docker.com/products/docker-desktop&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.docker.com/products/docker-desktop&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bkHqHS/hySLABsi4a/ZGmaHlsTaVT4kgGa8VNvyK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dbpQvL/hySLEKA5SP/QRNBn9OpxoPPo3PkYHk7Vk/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;Download Docker Desktop | Docker&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Docker Desktop is available to download for free on Mac, Windows, or Linux operating systems. Get started with Docker today!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.docker.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;2. 설치된 Docker Desktop을 실행 후 메인 화면에서 설정 아이콘을 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_k8s-1.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2m9Hk/btshTvuDrpc/KEcVZ7KXvygviNsP1URK41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2m9Hk/btshTvuDrpc/KEcVZ7KXvygviNsP1URK41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2m9Hk/btshTvuDrpc/KEcVZ7KXvygviNsP1URK41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2m9Hk%2FbtshTvuDrpc%2FKEcVZ7KXvygviNsP1URK41%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; alt=&quot;docker desktop 메인화면&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;448&quot; data-filename=&quot;edited_k8s-1.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;448&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;3. 설정 화면에서 Kubernetes 탭을선택하고 Enable Kubernetes를 체크한 후  재 시작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_k8s-2.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyE9hl/btshAr2suLN/dMKSoY9SB8k4LIPnW9kJyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyE9hl/btshAr2suLN/dMKSoY9SB8k4LIPnW9kJyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyE9hl/btshAr2suLN/dMKSoY9SB8k4LIPnW9kJyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyE9hl%2FbtshAr2suLN%2FdMKSoY9SB8k4LIPnW9kJyK%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; alt=&quot;kubernetes 설정 화면&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;448&quot; data-filename=&quot;edited_k8s-2.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;448&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;4. &lt;s&gt;$ kubectl get all&lt;/s&gt;&amp;nbsp;커맨드를 입력하면 정상적으로 설치된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-05-29 오전 1.08.09.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NRp7D/btshBltIsxf/mgHDNkPfR32aonC8c4JJXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NRp7D/btshBltIsxf/mgHDNkPfR32aonC8c4JJXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NRp7D/btshBltIsxf/mgHDNkPfR32aonC8c4JJXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNRp7D%2FbtshBltIsxf%2FmgHDNkPfR32aonC8c4JJXk%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; alt=&quot;docker desktop k8s 설치 확인&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;447&quot; data-filename=&quot;edited_스크린샷 2023-05-29 오전 1.08.09.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;447&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;&amp;nbsp;&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;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Li&lt;span style=&quot;text-align: start;&quot;&gt;ghtweight Kubernetes Distributions이란 -&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.baeldung.com/ops/kubernetes-lightweight-distributions&quot;&gt;https://www.baeldung.com/ops/kubernetes-lightweight-distributions&lt;/a&gt;&lt;/p&gt;</description>
      <category>Infra/Kubernetes</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/87</guid>
      <comments>https://soojae.tistory.com/87#entry87comment</comments>
      <pubDate>Mon, 29 May 2023 14:38:35 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드 구현 세부 사항 테스트</title>
      <link>https://soojae.tistory.com/84</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_banner.webp&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XoY6Q/btr3gMEOHc1/UTIRDKAY2I3zujm5aIbIp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XoY6Q/btr3gMEOHc1/UTIRDKAY2I3zujm5aIbIp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XoY6Q/btr3gMEOHc1/UTIRDKAY2I3zujm5aIbIp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXoY6Q%2Fbtr3gMEOHc1%2FUTIRDKAY2I3zujm5aIbIp0%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; alt=&quot;프론트엔드 구현 세부 사항 테스트 섬네일&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;557&quot; data-filename=&quot;edited_banner.webp&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;557&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은&amp;nbsp;&lt;a href=&quot;https://kentcdodds.com/&quot;&gt;Kent C. Dodds&lt;/a&gt;의 &lt;a href=&quot;https://kentcdodds.com/blog/testing-implementation-details&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Testing Implementation Details&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;포스트를 번역한 글입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Thank you Kent C. Dodds. Because of your posts, I can continue to grow as an engineer.&lt;/blockquote&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;이전에 enzyme를 사용할 때 (그 시절 모두가 사용했던 것처럼), 나는 enzyme의 특정 API를 신경 써서 사용했다. shallow rendering를 완전히 피하기 위해, `instance()`, `state()`, `find('컴포넌트이름')`와 같은 API를 사용하지 않았다. 그리고 다른 사람들의 풀 리퀘스트 코드를 검토할 때, 왜 이런 API들을 피해야 하는지에 대해 몇 번이고 설명했다. &lt;br /&gt;그 이유는 이런 API들이 컴포넌트의 `구현 세부 사항`을 테스트하도록 만들기 때문이다.&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;h2 data-ke-size=&quot;size26&quot;&gt;`구현 세부 사항` 테스트는 왜 나쁜가?&lt;/h2&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;b&gt;False Negative&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;코드의 로직이 변경되어 정상적으로 동작하지 않음에도 테스트가 성공. &lt;b&gt;False Positive&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;명확하게 말하자면, 테스트는 &quot;소프트웨어가 작동하는지 확인하는 것&quot;을 의미한다. 테스트가 성공하면 테스트는 &quot;positive&quot;(소프트웨어가 작동함) 임을 의미한다. 그렇지 않으면 테스트는 &quot;Negative&quot;(소프트웨어가 작동하지 않음)가 되었다는 것을 의미한다. &quot;False&quot;라는 용어는 테스트에서 잘못된 결과가 나왔다는 것을 의미한다. &lt;br /&gt;즉, 소프트웨어가 실제로는 고장이 났지만 테스트는 성공(False Positive)하거나, 소프트웨어가 실제로는 작동하지만 테스트는 실패한 경우(False Negative)를 의미한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 간단한 Accordion 컴포넌트를 예로 들어 각 컴포넌트들을 살펴보자&lt;/p&gt;
&lt;pre id=&quot;code_1678547993113&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// accordion.js
import * as React from 'react'
import AccordionContents from './accordion-contents'

class Accordion extends React.Component {
  state = {openIndex: 0}
  setOpenIndex = openIndex =&amp;gt; this.setState({openIndex})
  render() {
    const {openIndex} = this.state
    return (
      &amp;lt;div&amp;gt;
        {this.props.items.map((item, index) =&amp;gt; (
          &amp;lt;&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; this.setOpenIndex(index)}&amp;gt;
              {item.title}
            &amp;lt;/button&amp;gt;
            {index === openIndex ? (
              &amp;lt;AccordionContents&amp;gt;{item.contents}&amp;lt;/AccordionContents&amp;gt;
            ) : null}
          &amp;lt;/&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    )
  }
}

export default Accordion&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;위의 예제에서 왜 최신 함수 컴포넌트(with hook)가 아닌 구식 클래스 컴포넌트를 사용하는지 궁금하겠지만, 계속 읽다 보면 흥미로운 사실을 깨달을 것이다. (enzyme를 알면 이미 예상하고 있을 수도 있다.)&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;pre id=&quot;code_1678549013475&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// __tests__/accordion.enzyme.js
import * as React from 'react'
// shadow render를 사용하지 않는 이유 
// https://kcd.im/shallow
import Enzyme, {mount} from 'enzyme'
import EnzymeAdapter from 'enzyme-adapter-react-16'
import Accordion from '../accordion'

// enzyme 리액트 어댑터 설치
Enzyme.configure({adapter: new EnzymeAdapter()})

test('setOpenIndex sets the open index state properly', () =&amp;gt; {
  const wrapper = mount(&amp;lt;Accordion items={[]} /&amp;gt;)
  expect(wrapper.state('openIndex')).toBe(0)
  wrapper.instance().setOpenIndex(1)
  expect(wrapper.state('openIndex')).toBe(1)
})

test('Accordion renders AccordionContents with the item contents', () =&amp;gt; {
  const hats = {title: 'Favorite Hats', contents: 'Fedoras are classy'}
  const footware = {
    title: 'Favorite Footware',
    contents: 'Flipflops are the best',
  }
  const wrapper = mount(&amp;lt;Accordion items={[hats, footware]} /&amp;gt;)
  expect(wrapper.find('AccordionContents').props().children).toBe(hats.contents)
})&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OK. 이제 이 테스트가 어떤 문제를 일으키는지 살펴보자...&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;리팩터링시 False Nagatives&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의외로 많은 사람들이 테스트, 특히 UI 테스트를 싫어한다. 왜 그럴까? 여러 가지 이유가 있지만, 내가 반복해서 듣는 한 가지 큰 이유는 테스트를 돌보는 데 너무 많은 시간을 소비하기 때문이다. &quot;코드를 변경할 때마다 테스트가 실패한다!&quot; 이는 생산성에 큰 걸림돌이 된다.&lt;br /&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;위의 Accordion을 여러 개의 Accordion이 한 번에 열 수 있도록 리팩터링 해보자. 리팩터링은 &lt;b&gt;구현&lt;/b&gt;만 변경하고, 기존 동작은 전혀 변경하지 않는 것을 말한다. 따라서 동작을 변경하지 않도록&amp;nbsp;&lt;b&gt;구현&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;여러 Accordion 요소를 한 번에 열 수 있도록 내부 상태를 `openIndex`에서 `openIndexes`로 변경해 보자.&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;659&quot; data-origin-height=&quot;665&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chi0Ic/btr3dTElsOs/qeDVgMzTFelgvEF2gV1juK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chi0Ic/btr3dTElsOs/qeDVgMzTFelgvEF2gV1juK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chi0Ic/btr3dTElsOs/qeDVgMzTFelgvEF2gV1juK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchi0Ic%2Fbtr3dTElsOs%2FqeDVgMzTFelgvEF2gV1juK%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; alt=&quot;아코디언 클래스 리팩터링&quot; loading=&quot;lazy&quot; width=&quot;659&quot; height=&quot;665&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;665&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;좋아, 앱에서 간단히 확인해 보니 모든 것이 여전히 제대로 작동하는 것으로 나타났다. 이제 나중에 여러 개의 Accordion을 여는 기능을 컴포넌트에 쉽게 추가할 수 있을 것 같다! &lt;br /&gt;이제 테스트를 해보자! &lt;span style=&quot;text-align: start;&quot;&gt; 펑  이런, 테스트가 실패했다. 어느 테스트가 실패한 걸까? `setOpenIndex sets the open index state properly.` 테스트가 실패했다.&lt;br /&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;span style=&quot;text-align: start;&quot;&gt;에러 메시지 내용은?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678557241530&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;expect(received).toBe(expected)

Expected value to be (using ===):
  0
Received:
  undefined&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;이를 False Negative라고 한다. 애플리케이션 코드에 문제가 생긴 것이 아니라, 테스트 코드에 문제가 생겼기 때문에 테스트가 실패한 것을 의미한다. 솔직히 이런 상황이 테스트에서 제일 짜증 난다. 자, 테스트 코드를 수정해 보자.&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;658&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d07jO0/btr3cI4nWnk/IBYonP1xvaslNfZMfovwT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d07jO0/btr3cI4nWnk/IBYonP1xvaslNfZMfovwT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d07jO0/btr3cI4nWnk/IBYonP1xvaslNfZMfovwT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd07jO0%2Fbtr3cI4nWnk%2FIBYonP1xvaslNfZMfovwT0%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; alt=&quot;아코디언 클래스 테스트 수정&quot; loading=&quot;lazy&quot; width=&quot;658&quot; height=&quot;254&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;254&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;결론: `구현 세부 사항`을 테스트하는 이러한 테스트 케이스에는 리팩터링 시 False Negative를 줄 수 있다. 그 결과 유지 관리가 어렵고 성가신 테스트 코드가 많이 생성된다.&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;False Positives&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 동료가 Accordion 컴포넌트 작업을 하다가 다음과 같은 코드를 발견했다고 가정해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1678559375676&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;button onClick={() =&amp;gt; this.setOpenIndex(index)}&amp;gt;{item.title}&amp;lt;/button&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;그는 금방이라도 최적화 하고 싶은 마음에 &quot;그래, render 함수 내부의 인라인 화살표 함수는 &lt;a href=&quot;https://medium.com/@ryanflorence/react-inline-functions-and-performance-bdff784f5578&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;성능이 좋지 않으니&lt;/a&gt;, 리팩터링 해야겠다! 제대로 동작할 것 같은데, 빠르게 고치고 테스트해보자.&quot;라고 생각한다.&lt;/p&gt;
&lt;pre id=&quot;code_1678620383654&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;button onClick={this.setOpenIndex}&amp;gt;{item.title}&amp;lt;/button&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;Cool. 테스트를 실행해보니... ✅✅ 완벽하다! 테스트에 대해 매우 확신하기 때문에 따로 브라우저에서 확인하지 않고 코드를 커밋했다. 이 커밋은 아무런 관련이 없는 PR(Pull Request)과 섞여 있고, 이 PR에는 수천 줄의 코드가 포함되어 있어&amp;nbsp; 눈에 띄지 않게 된다. &lt;br /&gt;결국 운영 환경에서 Accordion 기능이 고장 나게 되고, Nancy는 Salt Lake에서 다가오는 2월의 위키드 공연을 가지 못해 울고 있고, 당신의 팀은 기분이 좋지 않다 (※ 역자 주: 에러 코드를 찾고 수정하느라 공연을 보러 가지 못하고, 팀원들도 마찬가지...)&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;그렇다면 무엇이 잘못된 걸까? `setOpenIndex`가 호출될 때 상태가 변경되고 Accordion의 내용이 적절하게 표시되는지 확인하는 테스트가 없었나? 아니, 해당 기능의 테스트가 있었다! &lt;br /&gt;하지만 문제는 `button`이 `setOpenIndex`에 올바르게 연결되었는지 확인하는 테스트가 없었다는 것이다.&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;이것을 False Positive라고 부른다.&lt;/b&gt; 이는 테스트는 성공했지만, 원래는 실패했어야 한다는 뜻이다! 어떻게 하면 재발을 방지할 수 있을까? 버튼 클릭 시 상태가 올바르게 업데이트되는지 확인하기 위한 또 다른 테스트를 추가해야 한다. 그리고 다시는 이런 실수를 반복하지 않도록 Code Coverage를 항상 100%로 유지해야 한다.&lt;br /&gt;아, 그리고 ESLint 플러그인을 많이 만들어서 &lt;span style=&quot;text-align: start;&quot;&gt;사람들이 구현 세부 사항을&lt;/span&gt;&amp;nbsp;테스트 하는 API를 사용하지 않도록 막아야 한다.&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;... 하지만 너무 귀찮다... 우리는 False Positive와 False Negative에 지쳐버린 상태다. 거의 테스트를 작성하지 않는 것이 더 좋을 것 같다. &lt;b&gt;모든 테스트를 삭제해 버리자!&lt;/b&gt; &lt;br /&gt;만약 더 넓은 &lt;a href=&quot;https://blog.codinghorror.com/falling-into-the-pit-of-success/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;성공&lt;/a&gt; &lt;a href=&quot;https://twitter.com/kentcdodds/status/859994199738900480&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;구덩이&lt;/a&gt;(pit of success) (※역자 주: 구덩이의 절벽은 U자 형태로 되어있으니 올라가는 것(실패)은 어렵고 떨어지는 것(성공)은 쉽다. 즉 잘 만든 시스템일수록 성공하기는 쉽고, 실패하기는 오히려 어렵다는 뜻.)를 가진 도구가 있으면 좋지 않을까? 물론이지! 그런 도구가 있다!&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;h2 data-ke-size=&quot;size26&quot;&gt;`구현 세부 사항`이 없는 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 `구현 세부 사항`을 테스트하지 않도록 API를 제한하면서 Enzyme의 모든 테스트를 다시 작성할 수 있지만, &lt;a href=&quot;https://github.com/testing-library/react-testing-library&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;React Testing Library&lt;/a&gt; 라이브러리를 사용하면 `구현 세부 사항`을 테스트하는 것이 애초에 어렵기 때문에, 이 라이브러리를 사용하기로 한다. 지금 바로 확인해 보자!&lt;/p&gt;
&lt;pre id=&quot;code_1678627433848&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// __tests__/accordion.rtl.js
import '@testing-library/jest-dom/extend-expect'
import * as React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Accordion from '../accordion'

test('can open accordion items to see the contents', () =&amp;gt; {
  const hats = {title: 'Favorite Hats', contents: 'Fedoras are classy'}
  const footware = {
    title: 'Favorite Footware',
    contents: 'Flipflops are the best',
  }
  render(&amp;lt;Accordion items={[hats, footware]} /&amp;gt;)

  expect(screen.getByText(hats.contents)).toBeInTheDocument()
  expect(screen.queryByText(footware.contents)).not.toBeInTheDocument()

  userEvent.click(screen.getByText(footware.title))

  expect(screen.getByText(footware.contents)).toBeInTheDocument()
  expect(screen.queryByText(hats.contents)).not.toBeInTheDocument()
})&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;좋아. 하나의 테스트에서 모든 동작을 성공적으로 검증할 수 있다. 또한 이 테스트는 내부 상태의 이름이&amp;nbsp;`openIndex`, `openIndexes`, `tacosAreTasty`  중 어느 것이든 상관없이 성공한다. Nice! False Negative를 해결했다! &lt;br /&gt;또한 클릭 핸들러가 제대로 연결되지 않은 경우에 테스트가 실패한다. 이러면 False Positive도 해결된다. 그리고 어떤 API를 사용하면 안 되는지를 기억할 필요도 없다. React Testing Library를 정상적으로 사용하기만 하면, Accordion 컴포넌트가 사용자가 원하는 대로 작동하고 있다는 확신을 가질 수 있는 테스트를 얻을 수 있다.&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;그럼... `구현 세부 사항`&lt;span&gt;은 정확히 뭐야?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;간단하게 정의하자면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;구현 세부 사항은 사용자가 사용하거나 보거나 이해하지 못하는 사항이다.&lt;/blockquote&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;br /&gt;브라우저에서 컴포넌트와 상호 작용할 end-user는 분명 사용자다. 그들은 렌더링 된 버튼과 콘텐츠를 보고 상호작용 할 것이다.&lt;br /&gt;또한 주어진 props(위 코드의 경우 아이템의 리스트)로 Accordion을 렌더링 할 개발자도 있다. 그래서 React 컴포넌트에는 일반적으로 end-user와 developer라는 두 명의 사용자가 있다. &lt;br /&gt;&lt;b&gt;End-user와 developer는 애플리케이션 코드를 만들 때 반드시 고려해야하는 두 명의 &quot;사용자&quot;다.&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;br /&gt;End-user은 컴포넌트에서 렌더링 된 화면을 보고 상호작용한다. 반면 developer는 컴포넌트에 전달하는 props를 보고 상호 작용한다. &lt;br /&gt;따라서 테스트는 일반적으로 &quot;전달된 props를 올바르게 사용할 수 있는가&quot; 와 &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;a style=&quot;text-align: start;&quot; href=&quot;https://github.com/testing-library/react-testing-library&quot;&gt;React Testing Library&lt;/a&gt;가 하는일이다. 이 라이브러리는 더미 props를 Accordion 컴포넌트에 전달한 다음 React Element를 쿼리 하는 구문(getByRole, getByText)등을 사용하여 렌더링 된 화면을 표시하고(또는 표시하지 않고), 렌더링 된 버튼을 클릭하여 상호작용한다.&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;Enzyme의 테스트를 생각해 보자. Enzyme는 `state와` `openIndex`를 참조하고 있었다. 위에서 언급한 두 가지 유형의 사용자에게는 어떤 함수가 호출되었는지, 인덱스가 primitive 한 값으로 저장되는지, 배열로 저장되는지는 솔직히 신경 쓰지 않는다. 또한 `setOpenIndex` 메서드에 대해서는 특히 알 필요가 없다.&lt;br /&gt;하지만 Enzyme의 테스트 케이스는 아무도 신경 쓰지 않는 것들을 테스트하고 있다.&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;text-align: start;&quot;&gt;이것이 바로 Enzyme 테스트가 False Negatives를 일으키기 쉬운 이유다. &lt;b&gt;end-user와 developer와는 다른 방식으로 테스트를 작성하기 때문에 애플리케이션 코드가 고려해야 하는 세 번째 사용자, 즉 테스트가 등장한다!&lt;/b&gt; 이 테스트는 솔직히 아무도 신경 쓰지 않는 사용자다. &lt;br /&gt;나는 애플리케이션 코드가 이 세 번째 사용자인 테스트를 고려하지 않았으면 좋겠다. 완전히 시간 낭비다. 테스트 자체를 위한 테스트는 필요하지 않다. &lt;i&gt;자동화된 테스트는 애플리케이션 코드가 운영 환경의 사용자에게 잘 작동하는지 확인해야 한다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&quot;&lt;a href=&quot;https://twitter.com/kentcdodds/status/977018512689455106&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;The more your tests resemble the way your software is used, the more confidence they can give you.&lt;/a&gt;  &amp;mdash; me&quot;&lt;br /&gt;&lt;br /&gt;&quot;테스트가 소프트웨어 사용 방식과 유사할수록, 그것들이 제공하는 신뢰도는 더욱 높아진다.&quot; &amp;mdash; kent&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;이에 대해 좀 더 알고 싶다면 다음을 참고하라. &lt;a href=&quot;https://kentcdodds.com/blog/avoid-the-test-user&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Avoid the Test User&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 hooks는?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글쎄, &lt;b&gt;enzyme는 hooks에서 많은 문제가 있다&lt;/b&gt;. `구현 세부 사항`을 테스트할 때 구현이 변경되면 테스트에 큰 영향을 미친다. 클래스 컴포넌트를 hooks가 있는 함수형 컴포넌트로 마이그레이션 하는 경우 그 과정에서 테스트의 무언가를 망가뜨려도 알 수 없기 때문에 문제가 생긴다.&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;반면 React Test Library는 어떨까? 클래스 컴포넌트와 hooks가 있는 함수형 컴포넌트 둘다 정상적으로 작동한다. 이 글의 마지막에 있는 codesandbox 링크에서 확인해 볼 수 있다. 나는 React Testing Library로 작성하는 테스트를 다음과 같이 부른다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;구현 세부 사항이 없고 리팩터링에 용이하다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;0.webp&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNdpKv/btr3IEeYYwr/zChNMsrZA2oJ70VxfrHXTk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNdpKv/btr3IEeYYwr/zChNMsrZA2oJ70VxfrHXTk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNdpKv/btr3IEeYYwr/zChNMsrZA2oJ70VxfrHXTk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNdpKv%2Fbtr3IEeYYwr%2FzChNMsrZA2oJ70VxfrHXTk%2Fimg.webp&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; alt=&quot;happy goats&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;1200&quot; data-filename=&quot;0.webp&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1200&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;h2 data-ke-size=&quot;size26&quot;&gt;결과&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 `구현 세부 사항`을 테스트 하는 것을 피할 수 있을까? 올바른 테스팅 도구를 사용하는 것이 시작이다. 여기 무엇을 테스트해야 하는지 알기 위한 프로세스가 있다. &lt;br /&gt;이 프로세스를 따르면 테스트할 때 올바른 마인드로 테스트할 수 있으며, 자연스럽게 `구현 세부 사항`을&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;피할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;해당 코드를 한개의 unit 혹은 몇 개의 unit으로 범위를 좁혀라 (예: &quot;결제&quot; 버튼 클릭 시 장바구니의 아이템과 함께&amp;nbsp;&lt;span style=&quot;text-align: left;&quot;&gt;/checkout으로&amp;nbsp;&lt;/span&gt;요청함)&lt;/li&gt;
&lt;li&gt;해당 코드를 살펴보고 '사용자'가 누구인지 생각하라(결제 form을 렌더링하는 developer, 버튼을 클릭하는 end-user)&lt;/li&gt;
&lt;li&gt;코드가 손상되지는 않았는지 사용자가 직접 수동으로&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;테스트하기 위한 지침서를 작성하라. &lt;br /&gt;예:
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;더미 데이터가 들어있는 장바구니를 사용하여 결제 form을 렌더링한다.&lt;/li&gt;
&lt;li&gt;결제 버튼을 클릭한다.&lt;/li&gt;
&lt;li&gt;/checkout API로 보낼 때 더미 데이터가 올바르게 전송되는지 확인한다.&lt;/li&gt;
&lt;li&gt;fake 성공 응답이 잘 내려오는지 확인한다.&lt;/li&gt;
&lt;li&gt;성공 메시지가 잘 보이는지 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;작업한 지침서를 자동화된 테스트로 작성하라.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되었으면 좋겠다! 테스트를 한 단계 더 발전시키고 싶다면 다음 사이트에서 Pro license를 얻는 것을 추천한다. &lt;a href=&quot;https://testingjavascript.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TestingJavaScript.com&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;Good luck!&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;P.S. 위의 코드들을 확인하고 싶다면, &lt;a href=&quot;https://codesandbox.io/s/rlnw1r5nxo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;codesandbox&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;P.S.P.S. 연습 문제로... 만약 `AccordionContents` 컴포넌트의 이름을 바꾸면 두 번째 Enzyme 테스트에서는 어떤 일이 일어날까?&lt;/p&gt;</description>
      <category>Knowledge/Test</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/84</guid>
      <comments>https://soojae.tistory.com/84#entry84comment</comments>
      <pubDate>Tue, 14 Mar 2023 03:31:46 +0900</pubDate>
    </item>
    <item>
      <title>[모던 자바 인 액션] 1장 보충 - 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?</title>
      <link>https://soojae.tistory.com/86</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;899&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcJnnf/btr22JgwZSh/8FpEZkOtcwFE2XrcrJtpCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcJnnf/btr22JgwZSh/8FpEZkOtcwFE2XrcrJtpCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcJnnf/btr22JgwZSh/8FpEZkOtcwFE2XrcrJtpCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcJnnf%2Fbtr22JgwZSh%2F8FpEZkOtcwFE2XrcrJtpCK%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; alt=&quot;모던 자바 인 액션 책&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;899&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;899&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h2 data-ke-size=&quot;size26&quot;&gt;1.1 역사의 흐름은 무엇인가&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바 1.0&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;※ 메모리 모델이란 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://parkcheolu.tistory.com/14&quot;&gt;https://parkcheolu.tistory.com/14&lt;/a&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;자바 5&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;p data-ke-size=&quot;size16&quot;&gt;※ 스레드 풀이란: &lt;a href=&quot;https://jenkov.com/tutorials/java-concurrency/thread-pools.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jenkov.com/tutorials/java-concurrency/thread-pools.html&lt;/a&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;자바 7&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;병렬 실행에 도움을 줄 수 있는 Fork / Join 프레임워크, 그러나 활용이 힘들다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ Fork/Join Framework in Java: &lt;a href=&quot;https://warpgate3.tistory.com/entry/ForkJoin-Framework-in-Java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://warpgate3.tistory.com/entry/ForkJoin-Framework-in-Java&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ Java&amp;nbsp;Fork&amp;nbsp;and&amp;nbsp;Join&amp;nbsp;using&amp;nbsp;ForkJoinPool: &lt;a href=&quot;https://jenkov.com/tutorials/java-util-concurrent/java-fork-and-join-forkjoinpool.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jenkov.com/tutorials/java-util-concurrent/java-fork-and-join-forkjoinpool.html&lt;/a&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;자바 8&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스트림 API, 메서드 참조와 람다, 인터페이스의 디폴트 메서드&lt;/li&gt;
&lt;li&gt;자바 8 기법은 함수형 프로그래밍에서 위력을 발휘한다.&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;1.2 왜 아직도 자바는 변화하는가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2.2 스트림 처리&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스의 스트림: &lt;a href=&quot;https://love-every-moment.tistory.com/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://love-every-moment.tistory.com/52&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 8 에는 java.util.stream 패키지에 스트림 API 가 추가되었다&lt;/li&gt;
&lt;li&gt;우리가 하려는 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리 할 수 있다&lt;/li&gt;
&lt;li&gt;스레드라는 복잡한 작업을 사용하지 않으면서 &lt;b&gt;공짜&lt;/b&gt;로 병렬성을 얻을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2.3 동작 파라미터화로 메서드에 코드 전달하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 8에서는 메서드를 다른 메서드의 인수로 넘겨주는(일급 객체) 기능을 제공하며, 이를 동작 파라미터화(behavior parameterization)라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2.4 병렬성과 공유 가변 데이터&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;/ul&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.3 자바 함수&amp;nbsp;&lt;/h2&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&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;함수&lt;/b&gt;&lt;span&gt;(function)라는 용어는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;메서드&lt;/b&gt;&lt;span&gt;(method) 특히 정적 메서드(Static method)와 같은 의미로 사용된다. 자바의 함수는 이에 더해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;수학적인 함수&lt;/b&gt;처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다&lt;/li&gt;
&lt;li&gt;&lt;span&gt;객체(객체의 참조)도 값이다. new 또는 팩토리 메서드 또는 라이브러리 함수를 이용해서 객체의 값을 얻을 수 있다. 객체 참조는 클래스의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;인스턴스&lt;/b&gt;(Instance)를 가리킨다&lt;/li&gt;
&lt;li&gt;프로그래밍 언어의 핵심은 값을 바꾸는 것이고 이 값을 일급 값 (또는 시민) 이라고 부른다&lt;/li&gt;
&lt;li&gt;자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스 같은)가 값의 구조를 표현하는 데 도움될 수 있다.&lt;/li&gt;
&lt;li&gt;메서드, 클래스 등은 이급 자바 시민에 해당한다.&lt;/li&gt;
&lt;li&gt;만약 메서드와 클래스가 일급 시민으로 만들 수 있다면, 즉 런타임에 메서드를 전달할 수 있다면 프로그래밍에 유용하게 활용할 수 있다.(자바스크립트에서는 가능하지롱)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;131-메서드와-람다를-일급-시민으로&quot; data-ke-size=&quot;size23&quot;&gt;1.3.1 메서드와 람다를 일급 시민으로&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 자바 8 이전
File[] hiddenFiles = new File(&quot;.&quot;).listFiles(new FileFilter() {
	public boolean accept(File file) {
		return file.isHidden(); // 숨겨진 파일 필터링
    }
})

// 자바 8 이후
File[] hiddenFiles = new File(&quot;.&quot;).listFiles(File::isHidden);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;`isHidden`이라는 함수(함수라는 용어를 사용했다는 것에 주목) 는 이미 준비되어 있으므로 자바 8의 메서드 참조 (method reference) :: ('이 메서드를 값으로 사용하라'의 의미)를 이용해서 listFiles 에 직접 전달할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;람다 : 익명함수&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 (int x) -&amp;gt; x + 1, 즉 'x 라는 인수로 호출하면 x + 1을 반환'하는 동작을 수행하도록 코드를 구현할 수 있다&lt;/li&gt;
&lt;li&gt;이용할 수 있는 편리한 클래스나 메서드가 없을 때 람다 문법을 이용해 더 간결하게 코드를 구현 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 8 이후 코드 예시&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class Test {
	public static boolean isGreenApple(Apple apple) {
		return GREEN.equals(apple.getColor());
    }

    public static boolean isHeavyApple(Apple apple) {
        return apple.getWeight() &amp;gt; 150;
    }
	
	public interface Predicate&amp;lt;T&amp;gt; {
		boolean test(T t);
    }
	
	static List&amp;lt;Apple&amp;gt; filterApples(List&amp;lt;Apple&amp;gt; inventory, Predicate&amp;lt;Apple&amp;gt; p) {
		List&amp;lt;Apple&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
		for (Apple apple : inventory) {
			if (p.test(apple)) {
				result.add(apple);
            }
        }
		return result;
    }
}&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;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;filterApples(inventory, Apple::isGreenApple);
filterApples(inventory, Apple::isHeavyApple);&lt;/code&gt;&lt;/pre&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.4 스트림&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의&amp;nbsp;모든&amp;nbsp;자바&amp;nbsp;애플리케이션은&amp;nbsp;컬렉션을&amp;nbsp;만들고&amp;nbsp;활용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&amp;nbsp;리스트에서&amp;nbsp;고가의&amp;nbsp;트랜잭션만&amp;nbsp;필터링할&amp;nbsp;경우&amp;nbsp;다음&amp;nbsp;코드처럼&amp;nbsp;많은&amp;nbsp;기본코드를&amp;nbsp;구현해야한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class LegacyTest {
  private static void groupTransaction() {
    // 그룹화된 트랜잭션을 더할 Map 생성
    Map&amp;lt;Currency, List&amp;lt;Transaction&amp;gt;&amp;gt; transactionsByCurrencies = new HashMap&amp;lt;&amp;gt;();
    // 트랜잭션의 리스트를 반복
    for (Transaction transcation : transactions) {
      // 고가의 트랜잭션을 필터링
      if (transaction.getPrice() &amp;gt; 1000) {
        // 트랜잭션의 통화 추출
        Currency currency = transaction.getCurrency();
        List&amp;lt;Transaction&amp;gt; transactionsForCurrency = transactionsByCurrencies.get(currency);
        // 현재 통화의 그룹화된 맵에 항목이 없으면 새로 만든다.
        if (transactionsForCurrency == null) {
          transactionsForCurrency = new ArrayList&amp;lt;&amp;gt;();
          transactionsByCurrencies.put(currency, transactionsForCurrency);
        }
        // 현재 탐색된 트랜잭션을 같은 통화의 트랜잭션 리스트에 추가한다.
        transactionsForCurrency.add(transaction);
      }
    }
  }
}&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;게다가&amp;nbsp;위&amp;nbsp;예제&amp;nbsp;코드에는&amp;nbsp;중첩된&amp;nbsp;제어&amp;nbsp;흐름&amp;nbsp;문장이&amp;nbsp;많아서&amp;nbsp;코드를&amp;nbsp;한번에&amp;nbsp;이해하기도&amp;nbsp;어렵다.&amp;nbsp;&lt;br /&gt;스트림&amp;nbsp;API를&amp;nbsp;이용하면&amp;nbsp;다음처럼&amp;nbsp;문제를&amp;nbsp;해결할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import static java.util.stream.Collectors.toList; 
class LegacyTest {
  private static void groupTransaction() {
    Map&amp;lt;Currency, List&amp;lt;Transaction&amp;gt;&amp;gt; transactionsByCurrencies = 
        transactions.stream()
            .filter((Transaction t) -&amp;gt; t.getPrice() &amp;gt; 1000) // 고가의 트랜잭션 필터링
            .collection(groupingBy(Transaction::getCurrency)); //통화로 그룹화함
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컬렉션&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;for-each루프를 이용해 반복 과정을 직접 처리함. -&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;외부 반복&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;기본적으로 단일 CPU만 이용하여 순차적으로 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스트림&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라이브러리 내부에서 수행함. -&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내부 반복&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;기본적으로 멀티 CPU를 이용하여 병렬로 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4.1 멀티 스레딩은 어렵다.&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;두 스레드가 적절하게 제어되지 않은 상황에서 공유된 sum 변수에 숫자를 더하면 다음과 같은 문제가 있다. (책의 그림을 보는 것을 추천)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구성 환경&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 1, 스레드 2, 변수 sum (값 100)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드 1이 sum 변수 값을 읽는다.&lt;/li&gt;
&lt;li&gt;스레드 2가 sum 변수 값을 읽는다.&lt;/li&gt;
&lt;li&gt;스레드 1이 sum 변수에 3을 더한다.&lt;/li&gt;
&lt;li&gt;스레드 2가 sum 변수에 5를 더한다.&lt;/li&gt;
&lt;li&gt;스레드 1이 sum 변수에 103을 저장한다.&lt;/li&gt;
&lt;li&gt;스레드 2가 sum 변수에 105를 저장한다. 결과 : 108이 아닌 105가 저장된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;자바 8은&amp;nbsp;컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제&amp;nbsp;그리고&amp;nbsp;멀티코어 활용 어려움을 해결하기 위해 스트림 API를 도입했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자바의 병렬성과 공유되지 않은 가변 상태&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;흔히 자바의 병렬성은 어렵고, synchronized는 쉽게 에러를 일으킨다고 생각한다.&lt;/li&gt;
&lt;li&gt;자바 8은 이를 해결하기 위해 두가지 방식을 사용한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;라이브러리에서 분할 처리를 한다. 큰 스트림을 병렬 처리할 수 있도록 작은 스트림으로 분할한다.&lt;/li&gt;
&lt;li&gt;filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용하지 않는다면 가변 공유 객체를 통해 병렬성을 누릴 수 있다.&lt;br /&gt;-&amp;gt; 여러 스레드가 동시에 가변 객체에 접근하더라도, 메서드가 상호작용하지 않기 때문에 안전하지만, 만약 전달되는 메서드가 공유되는 가변 객체를 변경한다면 문제가 발생한다. 이런 경우에는 스레드 동기화 기법을 사용하여 해결해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;함수형 프로그래밍에서 함수형이란 '함수를 일급 값으로 사용한다.'는 의미를 가지고 있지만 부가적으로 '프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다'&amp;nbsp;라는 의미도 포함한다.&lt;/li&gt;
&lt;/ul&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.5&amp;nbsp; 디폴트 메서드와 자바 모듈&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 &lt;b&gt;모듈&lt;/b&gt;을 정의할 수 있다.&lt;/li&gt;
&lt;li&gt;모듈 덕분에 JAR 같은 컴포넌트 구조를 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;문서화와 모듈 확인 작업이 용이해졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 자바 9 모듈이란: &lt;a href=&quot;https://mslim8803.tistory.com/39&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://mslim8803.tistory.com/39&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.&lt;/li&gt;
&lt;li&gt;디폴트 메서드는 특정 프로그램을 구현하는데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1678357811949&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Apple&amp;gt; heavyApples =
        inventory.stream()
                .filter((Apple a) -&amp;gt; a.getWeight() &amp;gt; 150)
                .collect(toList());

List&amp;lt;Apple&amp;gt; heavyApples =
        inventory
                .parallelStream()
                .filter((Apple a) -&amp;gt; a.getWeight() &amp;gt; 150)
                .collect(toList());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;자바 8 이전에는 Stream()이나 parallelStream() 메서드를 지원하지 않았다.&lt;/li&gt;
&lt;li&gt;이 기능을 추가하려면 Collection 인터페이스에 해당 메서드를 추가하고 ArrayList 등과 같은 구현체에 메서드를 구현해야 했다.&lt;/li&gt;
&lt;li&gt;하지만 이미 컬렉션 API의 인터페이스를 구현하는 많은 컬렉션 프레임워크가 존재했다. 인터페이스에 새로운 메서드를 추가한다면 인터페이스를 구현하는 모든 클래스는 새로 추가된 메서드를 구현해야 했다.&lt;/li&gt;
&lt;li&gt;자바 8은 클래스에서 구현하지 않아도 되도록 메서드를 인터페이스에 추가했다. 이를 디폴트 메서드라고 부른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 인터페이스 vs 추상 클래스:&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://yaboong.github.io/java/2018/09/25/interface-vs-abstract-in-java8/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://yaboong.github.io/java/2018/09/25/interface-vs-abstract-in-java8/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 다이아몬드 상속 문제: &lt;a href=&quot;https://siyoon210.tistory.com/125&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://siyoon210.tistory.com/125&lt;/a&gt;&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.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어함수형 언어도 프로그램을 돕는 여러 장치를 제공한다.&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일례로 명시적으로 서술형의 데이터를 이용해 null을 회피하는 기법이 있다. 자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional&amp;lt;T&amp;gt; 클래스를 제공한다. Optional&amp;lt;T&amp;gt;는 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ null이 왜 나쁜가: &lt;a href=&quot;https://www.mimul.com/blog/why-null-is-bad/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.mimul.com/blog/why-null-is-bad/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;구조적 패턴 매칭 기법도 있다. 자바에서는 if-then-else나 switch문을 이용하지만 다른 언어에서는 패턴 매칭으로 더 정확한 비교를 구현할 수 있다는 사실을 증명했다.&lt;/li&gt;
&lt;li&gt;아쉽게도 자바 8은 패턴 매칭을 완벽하게 지원하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ (구조적) 패턴 매칭: &lt;a href=&quot;https://0391kjy.tistory.com/8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://0391kjy.tistory.com/8&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;&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모던 자바 인 액션 - &lt;a href=&quot;https://search.shopping.naver.com/book/search?bookTabType=ALL&amp;amp;pageIndex=1&amp;amp;pageSize=40&amp;amp;query=%EB%AA%A8%EB%8D%98%20%EC%9E%90%EB%B0%94%20%EC%9D%B8%20%EC%95%A1%EC%85%98&amp;amp;sort=REL&quot;&gt;https://search.shopping.naver.com/book/search?bookTabType=ALL&amp;amp;pageIndex=1&amp;amp;pageSize=40&amp;amp;query=%EB%AA%A8%EB%8D%98%20%EC%9E%90%EB%B0%94%20%EC%9D%B8%20%EC%95%A1%EC%85%98&amp;amp;sort=REL&lt;/a&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;모던 자바 인 액션 소스 코드 - &lt;a href=&quot;https://hanbit.co.kr/support/supplement_survey.html?pcode=B4926602499&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hanbit.co.kr/support/supplement_survey.html?pcode=B4926602499&lt;/a&gt;&lt;/p&gt;</description>
      <category>Language/Java</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/86</guid>
      <comments>https://soojae.tistory.com/86#entry86comment</comments>
      <pubDate>Thu, 9 Mar 2023 20:12:02 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드에서는 어떤 것을 테스트 해야 할까?</title>
      <link>https://soojae.tistory.com/83</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;photo-1494017411273-233a4d225d36.jpg&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;1375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vg2dc/btr2gjbTLZ2/DjK91vqTkWHztV0Hr4KkY0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vg2dc/btr2gjbTLZ2/DjK91vqTkWHztV0Hr4KkY0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vg2dc/btr2gjbTLZ2/DjK91vqTkWHztV0Hr4KkY0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVg2dc%2Fbtr2gjbTLZ2%2FDjK91vqTkWHztV0Hr4KkY0%2Fimg.jpg&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; alt=&quot;프론트엔드에서 어떤 것을 테스트 해야할까&quot; loading=&quot;lazy&quot; width=&quot;1100&quot; height=&quot;1375&quot; data-filename=&quot;photo-1494017411273-233a4d225d36.jpg&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;1375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은&amp;nbsp;&lt;a href=&quot;https://kentcdodds.com/&quot;&gt;Kent C. Dodds&lt;/a&gt;의&amp;nbsp;&lt;span&gt;&lt;a href=&quot;https://kentcdodds.com/blog/how-to-know-what-to-test&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;How to know what to test&lt;/a&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;포스트를 번역한 글입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Thank you Kent C. Dodds. Because of your posts, I can continue to grow as an engineer.&lt;/blockquote&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;많은&amp;nbsp;&lt;/b&gt;콘텐츠를 만들었다. &lt;br /&gt;하지만 테스트를 작성하는 방법을 알아도, 애플리케이션에 대한 신뢰성을 얻기 위해서는 그저 절반일 뿐이다. 무엇을 테스트할지 아는 것은 중요한 또 다른 절반이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kentcdodds.com/workshops&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;워크숍&amp;nbsp;자료&lt;/a&gt;와&amp;nbsp;&lt;a href=&quot;https://testingjavascript.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TestingJavaScript.com&lt;/a&gt;에서 무엇을 테스트해야 하는지 아는 방법에 대해 이야기하고 있지만, 이에 대한 질문을 많이 받아서 블로그 포스팅을 작성하는 것이 좋겠다고 생각했다. 가보자고!&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;테스트하는 이유 기억하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우리는 사용자가 애플리케이션을 사용할 때 제대로 작동할 것이라는 신뢰성을 위해 테스트를 작성한다.&lt;/b&gt; 워크플로우를 개선하기 위해 테스트를 작성하는 사람들도 있지만, 나는 궁극적으로 신뢰성에 관심이 있다. &lt;br /&gt;그렇기 때문에 우리가 테스트하는 것은 신뢰성 향상과 직결되어야 한다. 테스트를 작성할 때 고려해야 할 핵심 사항은 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;테스트 중인 코드에 대한 생각보다는 코드가 지원하는 Use Case에 대해 더 많이 생각하라.&lt;/blockquote&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://kentcdodds.com/blog/testing-implementation-details&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;재앙으로 가는 길&lt;/a&gt;이다.&amp;nbsp;&lt;br /&gt;반대로 Use Case를 생각함으로써, 우리는 사용자가 애플리케이션을 사용하는 방식과 유사한 방식으로 테스트를 작성할 수 있다.&lt;br /&gt;(※ 역자 주: 코드 내부의 동작 방식에 초점을 맞추는 게 아닌, 코드가 제공하는 &lt;b&gt;기능&lt;/b&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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdcTsp/btr1Y7Khaoz/Bk8hTLzr8qaphITu0uUOQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdcTsp/btr1Y7Khaoz/Bk8hTLzr8qaphITu0uUOQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdcTsp/btr1Y7Khaoz/Bk8hTLzr8qaphITu0uUOQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdcTsp%2Fbtr1Y7Khaoz%2FBk8hTLzr8qaphITu0uUOQ0%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; alt=&quot;Kent C. Dodds의 트윗&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;301&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;301&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Code&amp;nbsp;Coverage&amp;nbsp;&amp;lt;&amp;nbsp;Use&amp;nbsp;Case&amp;nbsp;Coverage&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Code Coverage는 실행된 코드 라인의 비율을 나타내는 지표로, 테스트 중에 실행된 코드 라인을 보여준다. 예를 들어:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function arrayify(maybeArray) {
  if (Array.isArray(maybeArray)) {
    return maybeArray
  } else if (!maybeArray) {
    return []
  } else {
    return [maybeArray]
  }
}&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;현재, 이 함수에 대한 테스트가 없으므로 Code Coverage 보고서에는 이 함수에 대한 Coverage가 '&lt;b&gt;0%&lt;/b&gt;'라고 표시된다. &lt;br /&gt;이 경우 Code&amp;nbsp;Coverage 보고서는 테스트가 필요함을 알려주지만, 이 함수의 중요성과 &lt;b&gt;이 함수가 지원하는 Use Case가 무엇인지는 알려주지 않는다&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;사실 전체 애플리케이션을 고려하고 무엇을 테스트해야 할지 고민할 때, Code Coverage 보고서는 대부분의 시간을 어디에 사용해야 할지에 대한 인사이트를 제공하는 데 아주 부족하다. 대신 우리의 코드베이스에서 어떤 코드가 테스트되지 않았는지를 식별하는 데 도움이 된다.&lt;br /&gt;따라서 Code Coverage 보고서를 보고 테스트가 누락된 부분을 확인할 때 if/else 문, loop 문, lifecyles에 대해 생각하지 말고, 자신에게 물어봐라:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&quot;What use cases are these lines of code supporting, and what tests can I add to support those use cases?&quot;&lt;br /&gt;&lt;br /&gt;이 코드 라인들은 어떤 Use Case를 지원하고 있으며, 이 Use Case를 지원하는 어떤 테스트를 추가할 수 있을까?&lt;/blockquote&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;Use Case Coverage&quot;는 우리가 테스트하는 Use Case의 수를 나타내는 지표이다. 불행하게도 자동화된 &quot;Use Case Coverage&quot;는 없다. 우리가 직접 만들어야 한다. &lt;br /&gt;하지만 Code Coverage 보고서는 때때로 우리가 커버하지 않은 Use Case를 식별하는 데 도움이 될 수 있다. 한번 시도해 보자.&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;는&amp;nbsp; Use Case를 파악할 수 있다. &lt;br /&gt;이 Use Case 문장은 실제로 우리의 테스트에 대한 좋은 제목이 될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;test('배열이 주어지면 배열을 리턴한다.', () =&amp;gt; {
  expect(arrayify(['Elephant', 'Giraffe'])).toEqual(['Elephant', 'Giraffe'])
})&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;이 테스트가 완료되면 Coverage 보고서는 다음과 같이 표시된다 (강조 표시된 선이 Coverage):&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o1eX6/btr2gkBUO7S/Qo4b7psu9DQkge1o6vTh2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o1eX6/btr2gkBUO7S/Qo4b7psu9DQkge1o6vTh2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o1eX6/btr2gkBUO7S/Qo4b7psu9DQkge1o6vTh2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo1eX6%2Fbtr2gkBUO7S%2FQo4b7psu9DQkge1o6vTh2K%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; alt=&quot;배열이 주어지면 배열을 리턴한다.&quot; loading=&quot;lazy&quot; width=&quot;667&quot; height=&quot;289&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;289&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;이제 나머지를 살펴보면 아직 테스트하지 않은 Use Case가 두 개 더 있다는 것을 확인할 수 있다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;falsy 값이 주어지면 빈 배열을 리턴한다.&lt;/li&gt;
&lt;li&gt;배열이 아닌 인자가 주어지면 해당 인자를 원소로 가지는 배열을 리턴한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에 이 Use Case들을 추가하고, Code Coverage에 어떤 영향을 미치는지 확인해 보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;test('falsy 값이 주어지면 빈 배열을 리턴한다', () =&amp;gt; {
  expect(arrayify()).toEqual([])
})&lt;/code&gt;&lt;/pre&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;661&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S5eBw/btr2gjQw7TO/LX1AkTN0u6cKfn25za7dNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S5eBw/btr2gjQw7TO/LX1AkTN0u6cKfn25za7dNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S5eBw/btr2gjQw7TO/LX1AkTN0u6cKfn25za7dNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS5eBw%2Fbtr2gjQw7TO%2FLX1AkTN0u6cKfn25za7dNK%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; alt=&quot;만약 주어진 값이 falsy 값이라면, 빈 배열을 리턴한다.&quot; loading=&quot;lazy&quot; width=&quot;661&quot; height=&quot;279&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;279&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;test(`배열이 아니고 falsy 값이 아니라면, 해당 인자를 원소로 가지는 배열을 리턴한다.`, () =&amp;gt; {
  expect(arrayify('Leopard')).toEqual(['Leopard'])
})&lt;/code&gt;&lt;/pre&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;665&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eMCtjY/btr1WmBcQrw/FptsqyXHmICGFcK3YQP9c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eMCtjY/btr1WmBcQrw/FptsqyXHmICGFcK3YQP9c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eMCtjY/btr1WmBcQrw/FptsqyXHmICGFcK3YQP9c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeMCtjY%2Fbtr1WmBcQrw%2FFptsqyXHmICGFcK3YQP9c0%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; alt=&quot;배열이 아니고 falsy 값이 아니라면, 주어진 인자로부터 배열을 리턴한다&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;283&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;283&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;멋지다! 브라보! 이제 이 함수의 Use Case를 변경하지 않는 한 테스트가 계속 정상적으로 통과할 것이라고 신뢰할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Code Coverage가 완벽한 지표는 아니지만, 우리 코드베이스에서 &quot;Use Case Coverage&quot;가 빠진 부분을 식별하는 유용한 도구가 될 수 있다.&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;Code Coverage로 Use Case를 숨길 수 있다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔 Code Coverage 보고서에는 Code Coverage가 100%로 표시되지만, Use Case Coverage가 100%가 아닐 수 있다. &lt;br /&gt;이것이 바로 내가 테스트를 작성하기 전에 모든 Use Case를 생각하려고 노력하는 이유이다.&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;예를 들어 `arrayify` 함수가 다음과 같이 구현되었다고 가정해 보자:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function arrayify(maybeArray) {
  if (Array.isArray(maybeArray)) {
    return maybeArray
  } else {
    return [maybeArray].filter(Boolean)
  }
}&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;아래 두 Use Case를 통해, 우리는 100%의 Coverage를 얻을 수 있다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Use Case Coverage 보고서를 보면 아래 Use Case는 빠져있는 것으로 나올 것이다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;falsy 값이 주어진다면, 빈 배열을 리턴한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 문제가 될 수 있다. 사용자가 직접 `arrayify()` 코드를 사용할 때 이러한 테스트 케이스는 코드에 대한 확신을 주지 못할 것이기 때문이다.&lt;br /&gt;지금은 잘 작동하는 것 같고, 이 케이스에 대한 테스트를 작성하지 않아도 코드가 해당 Use Case를 지원할 수 있지만, 테스트를 작성하는 이유는 코드의 변경이 발생하더라도 우리가 의도한 Use Case를 계속해서 지원할 수 있도록 보장하기 위함이다. &lt;br /&gt;따라서, 우리는 이러한 Use Case를 커버할 수 있도록 테스트를 추가해야 한다.&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;이 테스트가 누락되면 어떻게 잘못될 수 있는지에 대한 예로, 누군가 이 `.filter(Boolean)` 코드를 보고 &quot;음, 이상하네... 저게 정말 필요한 건가?&quot;라고 생각할 수 있다. &lt;br /&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;blockquote data-ke-style=&quot;style3&quot;&gt;Test use cases, not code.&lt;br /&gt;&lt;br /&gt;코드를 테스트하지 말고, Use Case들을 테스트하라.&lt;/blockquote&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;h2 data-ke-size=&quot;size26&quot;&gt;React에 적용하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 작성할 때는 지원해야 하는 사용자가 이미 두 명이라는 것을 기억하라: End 사용자와&amp;nbsp;developer 사용자. 다시 말하지만, Use Case보다 코드에 대해 초점을 맞추면, 코드의 세부사항을 구현하기 시작하게 되는 함정에 빠지기 쉽다. &lt;br /&gt;그렇게 되면 의도치 않은 &lt;a href=&quot;https://kentcdodds.com/blog/avoid-the-test-user&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 React로 개발하는 개발자들이 테스트에 대해 생각하는 몇 가지 측면이 있는데, 결국 세부 사항을 구현하는 테스트를 하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 것에 대해 코드에 대해 생각하기보다는 코드가 End 사용자와, developer 사용자에게 영향을 미치는 요소에 대해 생각해야 하며, 다음과 같은&amp;nbsp; Use Case에 대해 생각하고 이를 테스트하라.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라이프사이클 메서드(Lifecycle methods)&lt;/li&gt;
&lt;li&gt;요소 이벤트 핸들러(Element event handlers)&lt;/li&gt;
&lt;li&gt;내부 컴포넌트 상태(Internal Component State)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 두 사용자와 관련이 있으므로 테스트해야 하는 항목은 다음과 같다. 각각 DOM을 변경하거나, HTTP 요청을 하거나 콜백 프로퍼티를 호출하거나, &lt;i&gt;관찰 가능한&lt;/i&gt; side effect 생성등&amp;nbsp;테스트를 하기 위한 여러 가지 유용한 작업들이 있다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;사용자(User) 상호작용(`@testing-library/user-event`라이브러리에서는 `userEvent`를 사용.): End 사용자가 컴포넌트가 렌더링 하는 요소(element)와 상호작용할 수 있는가?&lt;/li&gt;
&lt;li&gt;프로퍼티(Props) 변경(React Testing Library에서 `rerender` 사용): developer 사용자가 컴포넌트를 새로운 프로퍼티로 re-render 되면 어떻게 되는가?&lt;/li&gt;
&lt;li&gt;컨텍스트(Context) 변경(React Testing Library에서 `rerender` 사용): developer 사용자가 컨텍스트를 변경하여 컴포넌트가 re-reder 되면 어떻게 되는가&lt;/li&gt;
&lt;li&gt;구독(Subscription) 변경: 컴포넌트가 구독하는 이벤트 Emitter가 변경되면 어떻게 되는가? (fierbase, redux store, router, media query, 온라인 상태와 같은 브라우저 기반 구독 같은)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;br /&gt;다소 부담스러울 수 있다. 특히 대규모 애플리케이션에서 테스트를 막 시작한 경우라면 더욱 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 사용자의 관점에서 애플리케이션을 고려하고 다음과 같이 질문하라:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이 애플리케이션이 고장 났을 때 가장 화가 나는 부분은 어디인가?&lt;/blockquote&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;blockquote data-ke-style=&quot;style3&quot;&gt;이 애플리케이션에서 에러가 발생하면 큰일이 나는 곳은 어디인가?&lt;/blockquote&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;br /&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;우선순위가 정해진 목록이 있으면 특정 Use Case에 대해 대부분의 사용자가 거치는 '행복한 경로'를 포괄하는 단일 End To End(E2E) 테스트를 작성하는 것이 좋다. &lt;br /&gt;이 방법으로 목록에 있는 몇 가지 주요 기능들을 테스트할 수 있다. 설정하는데 시간이 조금 걸릴 수 있지만, 투자 대비 &lt;b&gt;큰&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;E2E테스트는 100% Use Case Coverage를 제공하지 않지만(100%를 얻으려는 시도조차 하지 말아야 한다.) 100% Code Coverage를 제공하지 않지만(E2E 테스트에서는 신경 쓰지 말아야 한다.), 좋은 출발점이 될 것이며, 현재 코드에 대한 자신감을 크게 높여줄 것이다.&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;몇 가지 E2E 테스트 케이스가 있으면 E2E 테스트에서 놓치고 있는 일부 특정 케이스에 대한 통합 테스트와, 해당 기능이 사용하는 더 복잡한 비즈니스 로직에 대한 Unit 테스트 작성을 시작할 수 있다. &lt;br /&gt;100% Code Coverage를 목표로 삼지 말아야 한다. 그럴 가치가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;테스트 문화와 합리적인 코드 커버리지 목표를 설정하는 방법에 대해 자세히 알아보려면 &lt;a href=&quot;https://2018.assertjs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AssertJS 2018&lt;/a&gt;에서 발표한&amp;nbsp;&lt;a href=&quot;https://twitter.com/aarondjents&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Aaron Abramov&lt;/a&gt;의 강연을 시청하면 좋다: &lt;a href=&quot;https://www.youtube.com/watch?list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw&amp;amp;v=_pnW-JjmyXE&amp;amp;feature=youtu.be&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;소프트웨어 설계 원칙으로 테스트 패턴 수립하기&lt;/a&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;다양한 테스트 유형의 차이점에 대해 자세히 알아보려면 이 게시물을 보는 것이 좋다: &lt;a href=&quot;https://soojae.tistory.com/82&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프론트엔드에서의 Static, Unit, Integration, E2E 테스트&lt;/a&gt;&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;Good luck.&amp;nbsp;&lt;/p&gt;</description>
      <category>Knowledge/Test</category>
      <category>React</category>
      <category>test</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/83</guid>
      <comments>https://soojae.tistory.com/83#entry83comment</comments>
      <pubDate>Tue, 7 Mar 2023 21:05:45 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드에서의 Static, Unit, Integration, E2E 테스트</title>
      <link>https://soojae.tistory.com/82</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;banner.webp&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boyDTb/btr1URmVMzL/a3DTe4xpLwDJL5ECxzjsV0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boyDTb/btr1URmVMzL/a3DTe4xpLwDJL5ECxzjsV0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boyDTb/btr1URmVMzL/a3DTe4xpLwDJL5ECxzjsV0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboyDTb%2Fbtr1URmVMzL%2Fa3DTe4xpLwDJL5ECxzjsV0%2Fimg.webp&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; alt=&quot;트로피형 테스트&quot; loading=&quot;lazy&quot; width=&quot;1100&quot; height=&quot;501&quot; data-filename=&quot;banner.webp&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;501&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은&amp;nbsp;&lt;a href=&quot;https://kentcdodds.com/&quot;&gt;Kent C. Dodds&lt;/a&gt;의 &lt;a href=&quot;https://kentcdodds.com/blog/static-vs-unit-vs-integration-vs-e2e-tests&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Static vs Unit vs Integration vs E2E Testing for Frontend Apps&lt;/a&gt; 포스트를 번역한 글입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Thank you Kent C. Dodds. Because of your posts, I can continue to grow as an engineer.&lt;/blockquote&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://testingjavascript.com/&quot;&gt;TestingJavaScript.com&lt;/a&gt;&lt;span&gt;에 있는 &quot;Testing Practices with&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://twitter.com/jbrains&quot;&gt;J.B.&amp;nbsp;Rainsberger&lt;/a&gt;&quot; 인터뷰에서 그는 내가 정말 좋아하는 비유를 들려주었다. 그는 다음과 같이 말했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&quot;You can throw paint against the wall and eventually you might get most of the wall, but until you go up to the wall with a brush, you'll never get the corners.  ️&quot;&lt;br /&gt;&lt;br /&gt;&quot;벽에 페인트를 던진다면 결국 대부분의 벽을 페인트칠할 수 있지만, 붓을 들고 벽에 올라가지 않는 한 절대 모서리를 페인트칠할 수 없다. ️&quot;&lt;/blockquote&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;벽 전체에 가느다란 붓을 사용해야 할까? 물론 아니다. 그렇게 하면 시간이 너무 오래 걸리고 최종 결과물이 균일하지 않을 것이기 때문이다. 200년 전 고조할머니가 바다 건너 가져온 가구 주변까지 롤러로 칠할 수 있을까? 불가능하다.&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;a href=&quot;https://twitter.com/kentcdodds/status/960723172591992832&quot;&gt;내가 트로피를 만든 이유다.&lt;/a&gt; 그 후&amp;nbsp;&lt;a href=&quot;https://twitter.com/Mappletons&quot;&gt;Maggie Appleton&lt;/a&gt;&amp;nbsp;(&lt;a href=&quot;https://egghead.io/?af=5236ad&quot;&gt;egghead.io&lt;/a&gt;의 뛰어난 아트/디자인을 만든 사람)이&amp;nbsp; &lt;a href=&quot;https://testingjavascript.com/&quot;&gt;TestingJavaScript.com&lt;/a&gt;을 위해 이 트로피를 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;3277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bREwjX/btr1QxCJ9M0/D13cj8DuqylxtkuF37THf0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bREwjX/btr1QxCJ9M0/D13cj8DuqylxtkuF37THf0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bREwjX/btr1QxCJ9M0/D13cj8DuqylxtkuF37THf0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbREwjX%2Fbtr1QxCJ9M0%2FD13cj8DuqylxtkuF37THf0%2Fimg.webp&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; alt=&quot;트로피형 테스트 (상세)&quot; loading=&quot;lazy&quot; width=&quot;3200&quot; height=&quot;3277&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;3277&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;테스팅 트로피에는 4가지 타입의 테스트가 있다. 위의 설명은 화면 리더기를 사용하는 사용자(그리고 이미지가 로드되지 않는 경우)를 위해 아래 자세하게 설명하려고 한다.&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;End to End&lt;/b&gt;: 사용자처럼 행동하는 도우미 로봇이다. 앱을 클릭하고, 올바르게 작동하는지 확인한다. 'functional testing' 또는 e2e라고도 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Integration&lt;/b&gt;: 여러 장치가 함께 상호 작용하여 잘 동작하는지 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Unit&lt;/b&gt;: 기능들이 각각 독립적으로 잘 동작하는지 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Static&lt;/b&gt;: 코드를 작성할 때 오타와 타입에러를 확인한다.&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;테스트 타입&lt;/h2&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;h3 data-ke-size=&quot;size23&quot;&gt;End to End&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 전체 애플리케이션(프론트엔드 및 백엔드 모두)을 실행하며, 테스트는 실제 사용자가 사용하는 것처럼 앱과 상호 작용한다. 아래 테스트 코드는 &lt;a href=&quot;https://www.cypress.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cypress&lt;/a&gt;로 작성되었다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import {generate} from 'todo-test-utils'

describe('todo app', () =&amp;gt; {
  it('should work for a typical user', () =&amp;gt; {
    const user = generate.user()
    const todo = generate.todo()
    // 여기서는 등록 절차를 진행한다.
    // 일반적으로 이 작업을 수행하는 e2e 테스트는 하나만 존재한다.
    // 나머지 테스트들은 동일한 endpoint를 호출할 것이다.
    // 애플리케이션이 인간과 같은 방식으로 동작하기 때문에, 
    // 우리는 수작업으로 테스트를 수행하지 않아도 된다.
    cy.visitApp()

    cy.findByText(/register/i).click()

    cy.findByLabelText(/username/i).type(user.username)

    cy.findByLabelText(/password/i).type(user.password)

    cy.findByText(/login/i).click()

    cy.findByLabelText(/add todo/i)
      .type(todo.description)
      .type('{enter}')

    cy.findByTestId('todo-0').should('have.value', todo.description)

    cy.findByLabelText('complete').click()

    cy.findByTestId('todo-0').should('have.class', 'complete')
    // etc...
    // 나의 E2E 테스트는 일반적으로 사용자가 하는 것과 비슷하게 동작한다.
    // 때로는 상당히 길어질 수 있다.
  })
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Integration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 테스트는 전체 앱을 렌더링한다. 이는 Integration 테스트의 `필수 조건은 아니며`, 대부분의 Integration 테스트에서는 전체 앱을 렌더링 하지 않는다. 하지만 모듈의 렌더링 메서드에서 사용하는 모든 제공자들과 함께 렌더링 될 것이다. (가상의 `test/app-test-utils` 모듈의 렌더링 메서드가 하는 일이다.) &lt;br /&gt;Integration 테스트의 기본 개념은 가능한 한 적게 모의(mock)을 하는 것이다. 나는 거의 다음 것들만 모의(mock)한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;네트워크 Requests(&lt;a href=&quot;https://mswjs.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MSW&lt;/a&gt; 사용)&lt;/li&gt;
&lt;li&gt;애니메이션을 담당하는 컴포넌트 (테스트 중에 애니메이션을 기다릴 사람은 없을 테니까)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import * as React from 'react'
import {render, screen, waitForElementToBeRemoved} from 'test/app-test-utils'
import userEvent from '@testing-library/user-event'
import {build, fake} from '@jackfranklin/test-data-bot'
import {rest} from 'msw'
import {setupServer} from 'msw/node'
import {handlers} from 'test/server-handlers'
import App from '../app'

const buildLoginForm = build({
  fields: {
    username: fake(f =&amp;gt; f.internet.userName()),
    password: fake(f =&amp;gt; f.internet.password()),
  },
})

// integration 테스트는 일반적으로 MSW를 통한 HTTP Request만 모의(mock) 테스트한다.
const server = setupServer(...handlers)

beforeAll(() =&amp;gt; server.listen())
afterAll(() =&amp;gt; server.close())
afterEach(() =&amp;gt; server.resetHandlers())

test(`logging in displays the user's username`, async () =&amp;gt; {
  // 커스텀 렌더링은 애플리케이션이 로딩을 완료할 때까지 resolve하는 
  // Promise를 리턴한다. (서버 렌더링의 경우 필요하지 않을 수 있다.)
  // 또한 커스텀 렌더링은 초기 route를 지정(특정 페이지를 렌더링)할 수 있도록 해준다.
  await render(&amp;lt;App /&amp;gt;, {route: '/login'})
  const {username, password} = buildLoginForm()

  userEvent.type(screen.getByLabelText(/username/i), username)
  userEvent.type(screen.getByLabelText(/password/i), password)
  userEvent.click(screen.getByRole('button', {name: /submit/i}))

  await waitForElementToBeRemoved(() =&amp;gt; screen.getByLabelText(/loading/i))

  // 사용자가 로그인했는지 확인할 필요가 있는 모든 것을 assert 하라.
  expect(screen.getByText(username)).toBeInTheDocument()
})&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://jestjs.io/docs/configuration#resetmocks-boolean&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;&lt;a href=&quot;https://testing-library.com/docs/react-testing-library/setup/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;React Testing Library&lt;/a&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;Unit&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import '@testing-library/jest-dom/extend-expect'
import * as React from 'react'
// 위의 통합 테스트 예제에서와 같은 Integration 테스트 유틸리티 모듈이 있다면,
// @testing-library/react대신 해당 모듈을 사용해도 된다.
import {render, screen} from '@testing-library/react'
import ItemList from '../item-list'

// 몇몇 사람들은 이것들을 unit 테스트라고 부르지 않는다.
// 왜냐하면 리액트로 DOM에 렌더링을 하기 때문이다.
// 그들은 얕은(shallow) 렌더링을 사용하라고 할 것이다.
// 그들이 당신에게 이런 말들을 한다면, 이 링크를 보여주면 된다. https://kcd.im/shallow 
test('renders &quot;no items&quot; when the item list is empty', () =&amp;gt; {
  render(&amp;lt;ItemList items={[]} /&amp;gt;)
  expect(screen.getByText(/no items/i)).toBeInTheDocument()
})

test('renders the items in a list', () =&amp;gt; {
  render(&amp;lt;ItemList items={['apple', 'orange', 'pear']} /&amp;gt;)
  // note: 이렇게 간단한 경우에는 스냅샷을 사용하는 것도 고려해볼 수 있지만, 다음 조건이
  // 모두 충족될 때만 사용하는 것이 좋다.
  // 1. 스냅샷이 작을 때
  // 2. toMatchInlineSnapshot() 메서드를 사용할 때
  // Read more: https://kcd.im/snapshots
  expect(screen.getByText(/apple/i)).toBeInTheDocument()
  expect(screen.getByText(/orange/i)).toBeInTheDocument()
  expect(screen.getByText(/pear/i)).toBeInTheDocument()
  expect(screen.queryByText(/no items/i)).not.toBeInTheDocument()
})&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;누구나 아래의 코드를 unit 테스트라고 부르고, 그 말이 맞다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 순수 함수(pure functinos)는 unit 테스트에 가장 적합하며, 
// 그들에 대해 jest-in-case 라이브러리를 사용하는 것을 좋아한다.
import cases from 'jest-in-case'
import fizzbuzz from '../fizzbuzz'

cases(
  'fizzbuzz',
  ({input, output}) =&amp;gt; expect(fizzbuzz(input)).toBe(output),
  [
    [1, '1'],
    [2, '2'],
    [3, 'Fizz'],
    [5, 'Buzz'],
    [9, 'Fizz'],
    [15, 'FizzBuzz'],
    [16, '16'],
  ].map(([input, output]) =&amp;gt; ({title: `${input} =&amp;gt; ${output}`, input, output})),
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Static&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot; data-line-numbers=&quot;true&quot; data-lang=&quot;js&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 버그를 발견할 수 있니?
// ESLint의 for-direction 규칙을 사용하면
// 코드 리뷰보다 더 빨리 발견할 수 있을 것이다.  
for (var i = 0; i &amp;lt; 10; i--) {
  console.log(i)
}

const two = '2'
// ok, 이건 좀 인위적이야, 
// 하지만 타입스크립트는 이것이 나쁘다는 것을 알려 줄 것이다:
const result = add(1, two)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;왜 &lt;s&gt;당신은&lt;/s&gt; 테스트를 작성하는가? 내가 당신에게 테스트하라고 말했기 때문인가? PR시 테스트가 포함되지 않으면 reject 될까 봐 그런가? 테스트가 당신의 workflow를 개선해서 그런가?&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;br /&gt;그래서 나는 어떤 일을 하든 작성하는 테스트가 애플리케이션에 대해 최대한 신뢰성을 얻을 수 있도록 하고, 테스트할 때 어떤 trade-off가 있는지 잘 알아 본다.&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://slides.com/kentcdodds/confident-react&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;my sliedes&lt;/a&gt;에서 발췌)에서 강조하고 싶은 테스팅 트로피에는 몇 가지 중요한 요소가 있다:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;2263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NvVhE/btr1140OF2A/WVq3yOI0BzQEmY2MJshU21/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NvVhE/btr1140OF2A/WVq3yOI0BzQEmY2MJshU21/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NvVhE/btr1140OF2A/WVq3yOI0BzQEmY2MJshU21/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNvVhE%2Fbtr1140OF2A%2FWVq3yOI0BzQEmY2MJshU21%2Fimg.webp&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; alt=&quot;트로피의 trade-off&quot; loading=&quot;lazy&quot; width=&quot;3200&quot; height=&quot;2263&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;2263&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;이미지 화살표는 자동화된 테스트를 고려할 때 생각해야 할 세 가지 trade-off를 나타 낸다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;cost--heap--&quot; data-ke-size=&quot;size23&quot;&gt;Cost: ￠ heap ➡  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스팅 트로피가 올라갈수록, 더 많은 비용이 든다. 이는 지속적 통합 환경(Continuous integration environment)에서 테스트를 실행하는 실제 비용뿐만 아니라 엔지니어가 각 개별 테스트를 작성하고 유지 관리하는 데 걸리는 시간도 포함된다.&lt;br /&gt;트로피의 위로 올라갈수록 실패 포인트가 많아지므로 테스트가 중단될 가능성이 높아져 테스트를 분석하고 수정하는데 더 많은 시간이 소요된다. 중요한 #복선이므로 기억하라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;speed---&quot; data-ke-size=&quot;size23&quot;&gt;Speed:   ➡  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 트로피가 위로 올라갈수록 테스트는 일반적으로 느리게 실행된다. 이는 테스트 트로피에서 더 높은 단계로 올라갈수록 테스트가 실행하는 코드가 더 많기 때문이다. &lt;br /&gt;Unit테스트는 일반적으로 의존성이 없거나 해당 의존성을 모킹(mocking)할 수 있는 작은 단위의 기능을&amp;nbsp; 테스트한다.(수천 라인의 코드를 단 몇 줄로 효과적으로 대체). 중요한 #복선이므로 기억하라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;confidence-simple-problems---big-problems-&quot; data-ke-size=&quot;size23&quot;&gt;Confidence: Simple problems   ➡ Big problems  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 사람들이 테스트 피라미드  에 대해 이야기할 때 비용과 속도의 trade-off를 언급한다. &lt;br /&gt;하지만 이것이 유일한 trade-off라면 테스팅 피라미드를 고려할 때 Unit 테스트에만 100% 집중하고, 다른 형태의 테스트를 완전히 무시할 것이다. 물론 이렇게 해서는 안되며, 그 이유는 내가 이전에 말한 적이 있는 매우 중요한 원칙이 있기 때문이다:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://twitter.com/kentcdodds/status/977018512689455106&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;The more your tests resemble the way your software is used, the more confidence they can give you.&quot;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;테스트가 소프트웨어 사용 방식과 유사할수록, 더 많은 신뢰를 줄 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 문장이 무엇을 의미 하는가? 즉, Marie 이모가 세무 소프트웨어를 사용하여 세금을 할 수 있도록 하려면 실제로 마리 이모가 해보는&amp;nbsp; 방법보다 더 좋은 방법이 없다는 뜻이다. &lt;br /&gt;하지만 Marie 이모가 버그를 찾아줄 때까지 기다릴 수는 없지 않은가? 그것은 시간이 너무 오래 걸리고 테스트해야할 일부 기능을 놓칠 수 있다. 여기에 정기적으로 소프트웨어 업데이트 하고 있다는 사실을 고려하면, 아무리 많은 사람들이 있더라도 이를 따라잡을 방법이 없다.&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;우리는 trade-off를 고려해야한다&lt;/b&gt;. 어떻게? 소프트웨어를 테스트하는 소프트웨어를 작성한다. 테스트할 때 항성 trade-off를 고려하는 것은 이제 우리의 테스트가 Marie 이모가 소프트웨어를 테스트할 때만큼 안정적으로 소프트웨어가 사용되는 방식이과 비슷하지 않다는 것이다. &lt;br /&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;테스팅 트로피가 위로 올라갈 수록 &quot;신뢰도 계수&quot;라는 것이 높아진다.&lt;/b&gt; 이는 각 테스트 수준에서 상대적인 신뢰성 수준을 나타낸다. &lt;br /&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;blockquote data-ke-style=&quot;style3&quot;&gt;트로피 위로 올라갈수록 실패 포인트가 많아지므로 테스트가 중단될 가능성이 높아져 테스트를 분석하고 수정하는데 더 많은 시간이 소요된다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Unit테스트는 일반적으로 의존성이 없거나 해당 의존성을 모킹(mocking)할 수 있는 작은 단위의 기능을&amp;nbsp; 테스트한다.(수천 라인의 코드를 단 몇 줄로 효과적으로 대체).&amp;nbsp;&lt;/blockquote&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;br /&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;특히 정적 분석 도구로 비즈니스 로직에 대한 신뢰성을 얻을 수 없다. Unit 테스트는 의존성 호출이, 적절하게 이루어졌는지 보장할 수 없다. (호출 방법에 대한 Assertion은 할 수 있지만 Unit 테스트로 제대로 호출되는지는 보장할 수 없다.). &lt;br /&gt;UI Integration 테스트는 백엔드에 올바른 데이터를 전달하고, 에러를 올바르게 처리하고 파싱하는지 보장할 수 없다. &lt;br /&gt;End to End는 매우 훌륭하지만, 일반적으로 non-Production(운영 환경에 가깝지만 실제 운영 환경이 아닌 환경)에서 실행하여 신뢰성 대비 실용성의 trade-off가 있다.&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;이제 다른 방향으로 가보자. 테스팅 트로피의 맨 위에서 Form과 URL 생성의 edge case에 대해 E2E 테스트를 사용하여 특정 필드를 입력하고 제출 버튼을 클릭하는 것을 확인하기 위해 E2E 테스트를 사용하려고 하면, 전체 애플리케이션(백엔드 포함)을 실행하여 많은 설정 작업을 하게 된다. 이는 Integration 테스트에 좀 더 적합하다.&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;쿠폰 코드 계산기의 edge case를 테스트하기 위해 &lt;span&gt;Integration 테스트를 사용하려고 한다면, 쿠폰 코드 계산기를 사용하는 컴포넌트를 렌더링할 수 있는지 확인하기 위해 설정 함수(setup function)에서 상당의 양을 작업을 할 것이다. &lt;br /&gt;이런 edge case는 Unit 테스트에서 하는 것이 더 낫다.&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;&lt;br /&gt;&lt;/span&gt;&lt;span&gt;만약 숫자 대신 문자열을 사용하여 add 함수를 호출했을 때의 결과를 검증하기 위해 &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Unit 테스트를 사용하려 한다면, TypeScript와 같은 정적 타입 체크 도구를 사용하는 것이 훨씬 더 효과적일 수 있다.&lt;/span&gt;&lt;/span&gt;&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;&lt;span&gt;&lt;span&gt;결과&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;트로피의 각 수준마다 각각의 trade-off를 갖고있다. E2E테스트는 실패 포인트가 더 많아서 어떤 코드가 문제를 일으켰는지 추적하기가 어려울 수 있지만, 이것은 더 많은 신뢰성을 가질 수 있다는 것을 의미한다. &lt;br /&gt;이는 테스트를 작성할 시간이 충분하지 않은 경우 특히 유용하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;애초에 테스트를 통해 문제를 발견하지 못하는 것보다는 자신감을 가지고 실패의 원인을 추적하는 것이 더 낫다.&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;span&gt;&lt;span&gt;결국 &lt;b&gt;나는 그 구분에 별로 신경쓰지 않는다.&amp;nbsp;&lt;br /&gt;&lt;/b&gt;만약 내 Unit 테스트를 Integration 혹은 E2E라고 생각한다면(어떤 사람들은 그렇게 부르기도 한다  &amp;zwj;♂️) 그렇게 생각해라. &lt;br /&gt;나는 내 코드가 비즈니스 요구 사항을 충족하는 것이 더 중요하다. 그 목표를 달성하기 위해 다양한 테스트 전략을 혼합하여 사용할 것이다.&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;Good luck!&lt;/p&gt;</description>
      <category>Knowledge/Test</category>
      <category>React</category>
      <category>test</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/82</guid>
      <comments>https://soojae.tistory.com/82#entry82comment</comments>
      <pubDate>Sun, 5 Mar 2023 19:04:37 +0900</pubDate>
    </item>
    <item>
      <title>[yarn] npm을 막고 yarn으로 강제하는 방법</title>
      <link>https://soojae.tistory.com/80</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;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ziyJG/btr1ILtfOwM/XWqBqWVtGDaEkHpe6Dekq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ziyJG/btr1ILtfOwM/XWqBqWVtGDaEkHpe6Dekq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ziyJG/btr1ILtfOwM/XWqBqWVtGDaEkHpe6Dekq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FziyJG%2Fbtr1ILtfOwM%2FXWqBqWVtGDaEkHpe6Dekq0%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; alt=&quot;yarn 로고&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;250&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;패키지 매니저로 yarn을 사용하는데, 협업하는 과정에서 실수로 `npm` 명령어를 사용해서 `package-lock.json` 파일이 생성되는 경우가 종종 발생한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그때 마다 'npm 사용하지 마시고, yarn을 사용해주세요.'라고 말해야 하는 상황(시간이 지나서 또 package-lock.json 파일이 올라온다면...)을 피하고 싶다면 npm을 막고 yarn으로 강제하자.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;3가지의 강제화 방법이&amp;nbsp;있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;package.json 파일의 scripts 속성의 preinstall 속성을 이용한 강제화 (추천)&lt;/h2&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// package.json
{
	// ... 생략
  &quot;scripts&quot;: {
    // ... 생략
    &quot;preinstall&quot;: &quot;node -e 'if(!/yarn\\.js$/.test(process.env.npm_execpath))throw new Error(\&quot;yarn을 사용해주세요\&quot;)'&quot;
  }	
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;`$ npm install` 실행시 결과&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnrSbE/btr1EeP6Wmq/caA60fi8BS1BT1iBi0h5Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnrSbE/btr1EeP6Wmq/caA60fi8BS1BT1iBi0h5Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnrSbE/btr1EeP6Wmq/caA60fi8BS1BT1iBi0h5Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnrSbE%2Fbtr1EeP6Wmq%2FcaA60fi8BS1BT1iBi0h5Vk%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;800&quot; height=&quot;421&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 후술할 Engines를 사용하는 방식처럼 따로 .npmrc 파일을 만들 필요가 없고, only-allow 방식처럼 라이브러리를 사용할 필요도 없다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Package.json 파일의 Engines 값을 이용한 강제화&lt;/h2&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// package.json

{
  // ... 생략
  &quot;engines&quot;: {
    &quot;npm&quot;: &quot;npm 대신 yarn을 사용해주세요&quot;
  }
}&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;package.json 파일이 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;위치에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;.npmrc&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 생성하여 다음 코드를&lt;span&gt;&amp;nbsp;&lt;/span&gt;추가해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// .npmrc
engine-strict = true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;`$ npm install` 실행 시&amp;nbsp;결과&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nwqct/btr1IK8QxwR/UYQUcIi9J5SFLQL2Ort7J0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nwqct/btr1IK8QxwR/UYQUcIi9J5SFLQL2Ort7J0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nwqct/btr1IK8QxwR/UYQUcIi9J5SFLQL2Ort7J0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnwqct%2Fbtr1IK8QxwR%2FUYQUcIi9J5SFLQL2Ort7J0%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;1256&quot; height=&quot;290&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;only-allow 라이브러리 사용 (개인적으로 비 추천)&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;only-allow 라이브러리를 사용하면 원하는 패키지 매니저로 강제할 수 있다.&lt;br /&gt;npm 주소 -&amp;nbsp;&lt;a href=&quot;https://www.npmjs.com/package/only-allow&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://www.npmjs.com/package/only-allow&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;하지만 굳이 라이브러리를 써야 하나 싶다. 또한 `boxen`과 `which-pm-runs` 라이브러리에 의존되어있기도 하다.&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Force yarn install instead of npm install for node modules - &lt;a href=&quot;https://stackoverflow.com/a/41233367&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://stackoverflow.com/a/41233367&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;How to Force Use Yarn or NPM - &lt;a href=&quot;https://www.freecodecamp.org/news/how-to-force-use-yarn-or-npm/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://www.freecodecamp.org/news/how-to-force-use-yarn-or-npm/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category>Knowledge/Web</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/80</guid>
      <comments>https://soojae.tistory.com/80#entry80comment</comments>
      <pubDate>Thu, 2 Mar 2023 19:11:20 +0900</pubDate>
    </item>
    <item>
      <title>[Security] Access control 종류(ACL, RBAC, ABAC)와 Coarse-grained, Fine-grained</title>
      <link>https://soojae.tistory.com/78</link>
      <description>&lt;p style=&quot;text-align: left;&quot; 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;765&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vcsl4/btr1nEamdaQ/PJkgqvDY0eloPSSNab5udK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vcsl4/btr1nEamdaQ/PJkgqvDY0eloPSSNab5udK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vcsl4/btr1nEamdaQ/PJkgqvDY0eloPSSNab5udK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVcsl4%2Fbtr1nEamdaQ%2FPJkgqvDY0eloPSSNab5udK%2Fimg.jpg&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; alt=&quot;Auth 로고&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;510&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Coarse와 Fine&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Coarse-grained와 Fine-grained의 사전적 뜻은 각각 `&amp;lt;석재.목재 등이&amp;gt;결이 거친`, `결이 고운` 이란 뜻이라고 한다. 감이 잡히지 않는다&lt;br /&gt;Coarse, Fine 단어로만 찾아보면 아래와 같다.&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;Coarse&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTkf2a/btr1lBEXXJ6/dvyVeZkXUauBc1K0IZ4zrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTkf2a/btr1lBEXXJ6/dvyVeZkXUauBc1K0IZ4zrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTkf2a/btr1lBEXXJ6/dvyVeZkXUauBc1K0IZ4zrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTkf2a%2Fbtr1lBEXXJ6%2FdvyVeZkXUauBc1K0IZ4zrK%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; alt=&quot;Coarse의 뜻&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;100&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;Fine&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nIlxB/btr0RJdZVbL/Wy58QhyKBYpyDrPouV2Wn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nIlxB/btr0RJdZVbL/Wy58QhyKBYpyDrPouV2Wn0/img.png&quot; data-alt=&quot;사전:&amp;amp;amp;nbsp; https://www.merriam-webster.com/dictionary/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nIlxB/btr0RJdZVbL/Wy58QhyKBYpyDrPouV2Wn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnIlxB%2Fbtr0RJdZVbL%2FWy58QhyKBYpyDrPouV2Wn0%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; alt=&quot;Fine의 뜻&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;224&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사전:&amp;amp;nbsp; https://www.merriam-webster.com/dictionary/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;즉 하나의 물질이 있다면 이것을 큰 입자들로 나누냐, 작은 입자들로 나누냐의 차이다. 즉 '매우 구체적인' 이라고 생각하면 된다.&lt;br /&gt;사실 Coarse-grained와 Fine-grained는 프로그래밍에서 전반적으로 사용하는 용어다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;예를 들어 구글 시트 API를 호출하려면, 발급받은 ClientId와 SecretKey로 인증을 한 후에 구글 시트를 불러올 수 있다.&lt;br /&gt;이때 Coarse-grained는 `getGoogleSheet` 함수 안에 인증 + 구글 시트 조회 + Rows 값 조회를 전부 넣는 것을 뜻한다.&lt;br /&gt;반면 Fine-grained는 `authGoogleSheet`, `getGoogleSheet`, `getGoogleSheetRows` 각각 함수를 만들어서 호출하는 것을 뜻한다.&lt;br /&gt;비교해 보면 Fine-grained가 좀 더 유연하게(authGoogleSheet를 다른 곳에서 호출하는 등) 사용할 수 있다. 하지만 작업시간이 Coarse-grained보다 오래 걸린다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이제 Access Control 관점에서 살펴보자.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Access Control에서의 Coarse-grained와 Fine-grained&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Coarse: 크루들은 사무실 문을 열 수 있다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Fine: 판교에서 근무하는 크루들은 근무시간 동안 사무실 문을 열고 닫을 수 있다.&lt;br /&gt;&lt;/span&gt;Finer: 판교에서 근무하고 클라우드 파트 소속인 크루들은 A 프로젝트에 배정이 되어있다면, 근무시간 동안 사무실 문을 열고 닫을 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Coarse -&amp;gt; Fine으로 갈수록 점점 세분화(granularity)되어 보안이 강화되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&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 style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Access Control의 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Control의 종류는 크게 3가지로 나눌 수 있다.&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;Access Control List (ACL)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 인증된 후에, 해당 사용자의 IP가 인증된 사용자 리스트(White List) 또는 차단된 사용자 리스트(Black 리스트)에 있는지에 따라 Access를 허용하거나 차단한다. 사용자 IP가 White List에 없을 경우에도 차단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, ACL은 특정 사용자의 시스템 액세스 권한 여부를 결정하는 데 사용된다.&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;Role-based access control(RBAC)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 역할에 따라 액세스 권한을 부여하는 방식이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;RBAC에서 사용자는 다른 권한을 부여받을 수 있는 여러 역할을 가질 수 있다.&amp;nbsp;예를 들어 IT회사에서의 RBAC을 다음과 같이 나눠볼 수 있다.&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;네트워크 관리자 역할: 네트워크 관리, 보안 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;총 관리자는 각 권한을 부여/회수하는 것이 간단하고, 역할에 따라 어떤 권한을 갖고 있는지 쉽게 파악할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 한계가 있다. 다음과 같은 경우를 살펴보자.&lt;br /&gt;시스템 관리자가 데이터베이스의 역할도 부여받고 싶은데, 시스템 관리자의 역할이 주 업무이므로 데이터 베이스 관리 중 삭제, 복구는 불가능하도록 하고 싶다. 이럴 경우 어떻게 해야 할까?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다른 역할을 또 만들어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 &amp;amp; 데이터 관리자 역할: 서버 관리, 시스템 관리, 삭제 기능을 제거한 데이터 베이스관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 또 네트워크 관리자 역할도 부여받는다면... 점점 역할이 많아지고 복잡해진다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Attribute-based access control(ABAC)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 대신 속성을 이용하여 더 세밀하게 권한을 부여할 수 있다. 위의 RBAC의 한계를 개선할 수 있다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자: 해당 부서 직원&lt;/li&gt;
&lt;li&gt;리소스: 파일&lt;/li&gt;
&lt;li&gt;동작: 읽기/쓰기&lt;/li&gt;
&lt;li&gt;환경: 해당 부서의 컴퓨터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 여러 속성을 활용하여 권한에 대해 좀 더 세밀한 작업을 할 수 있다. &lt;br /&gt;만약 읽기만 가능하도록 하고 싶으면 `쓰기` 속성을 제거하고, 재택에서도 되도록 하려면 `재택` 속성만 추가하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;ACL -&amp;gt; RBAC -&amp;gt; ABAC 순으로 점점 Coarse -&amp;gt; Fine으로 되는 것을 확인할 수 있다.&amp;nbsp;&lt;br /&gt;Fine일수록 유연해지고 세밀한 접근제어를 할 수 있지만, 그만큼 구현이 어렵고 관리포인트가 많아진다는 단점이 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;dontcryme님 블로그 - &lt;a href=&quot;https://blog.naver.com/dontcryme/30124306779&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://blog.naver.com/dontcryme/30124306779&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;What is Fine-Grained Access Control? -&amp;nbsp;&lt;a href=&quot;https://www.immuta.com/blog/what-is-fine-grained-access-control-and-why-its-so-important/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.immuta.com/blog/what-is-fine-grained-access-control-and-why-its-so-important/&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;What Is Fine-Grained Access Control? - &lt;a href=&quot;https://www.styra.com/blog/what-is-fine-grained-access-control/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.styra.com/blog/what-is-fine-grained-access-control/&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;Coarse-grained vs. fine-grained access control - &lt;a href=&quot;https://www.webfarmr.eu/2011/05/coarse-grained-vs-fine-grained-access-control-part-i/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.webfarmr.eu/2011/05/coarse-grained-vs-fine-grained-access-control-part-i/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Knowledge/Security</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/78</guid>
      <comments>https://soojae.tistory.com/78#entry78comment</comments>
      <pubDate>Wed, 1 Mar 2023 20:22:59 +0900</pubDate>
    </item>
    <item>
      <title>[React] ChatGPT와 구글 스프레드 시트를 이용한 i18n작업</title>
      <link>https://soojae.tistory.com/70</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MB3Kj/btrZ7aP2MlA/cgq2SS5cT23aFW0PTlCjuk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MB3Kj/btrZ7aP2MlA/cgq2SS5cT23aFW0PTlCjuk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MB3Kj/btrZ7aP2MlA/cgq2SS5cT23aFW0PTlCjuk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMB3Kj%2FbtrZ7aP2MlA%2Fcgq2SS5cT23aFW0PTlCjuk%2Fimg.jpg&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; alt=&quot;ChatGPT 로고&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;900&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;ChatGPT&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;최근 ChatGPT가 핫하다. 필자도 프로그래밍 관련 질문들은 구글링 하기 전에, 먼저 ChatGPT에게 물어본다.&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;643&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bihQtX/btr0jjx4umB/5k6h8eKZ7nifXkfBifuOCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bihQtX/btr0jjx4umB/5k6h8eKZ7nifXkfBifuOCK/img.png&quot; data-alt=&quot;내 무습다.. 와 내 생각을 읽을라 카는데..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bihQtX/btr0jjx4umB/5k6h8eKZ7nifXkfBifuOCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbihQtX%2Fbtr0jjx4umB%2F5k6h8eKZ7nifXkfBifuOCK%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; alt=&quot;ChatGPT와의 대화&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;556&quot; data-origin-width=&quot;643&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내 무습다.. 와 내 생각을 읽을라 카는데..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;놀라운 부분은 필자가 `adopt`라는 scope function이 kotlin에 있지 않냐고 물어봤더니, 없다고 하는 것에 그치지 않고, &lt;br /&gt;`adopt`의 늬앙스가 `apply`, `also`와 유사하여&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;사용자가 헷갈린 것이라 판단해&lt;/span&gt;, `apply`와 `also`에 대해 설명해 준다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이 뛰어난 기능을 어딘가에 활용해볼까 고민하던 중, ChatGPT의 확장 프로그램을 구글 스프레드 시트에 설치하여 활용할 수 있다는 소식을 듣고, 구글 스프레드 시트로 작업한 i18n에 활용하기로 결정했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;구글 스프레드 시트를 이용한 i18n 자동화 방법은 아래 게시물을 보는 것을 추천한다. 이런 게시물을 볼 때마다 정말 대단한 개발자 분들이 많다는 생각이 든다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;국제화(i18n) 자동화 가이드 : NHN Cloud Meetup&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;프런트엔드 개발을 하다 보면 국제화와 번역을 수작업과 막일로 하는 경우가 있습니다.&amp;quot;복붙&amp;quot;이나 반복적인 수작업으로 인해 고통받는 모든 프런트엔드 개발자를 자동화 가이드를 작성하였습&quot; data-og-host=&quot;meetup.nhncloud.com&quot; data-og-source-url=&quot;https://meetup.nhncloud.com/posts/295&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WTiw8/hyRHxexDCO/KbyrKDFMnJLDb6P0IDpFH1/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270&quot; data-og-url=&quot;https://meetup.toast.com/posts/295&quot;&gt;&lt;a href=&quot;https://meetup.toast.com/posts/295&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://meetup.nhncloud.com/posts/295&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WTiw8/hyRHxexDCO/KbyrKDFMnJLDb6P0IDpFH1/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270');&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;국제화(i18n) 자동화 가이드 : NHN Cloud Meetup&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프런트엔드 개발을 하다 보면 국제화와 번역을 수작업과 막일로 하는 경우가 있습니다.&quot;복붙&quot;이나 반복적인 수작업으로 인해 고통받는 모든 프런트엔드 개발자를 자동화 가이드를 작성하였습&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;meetup.nhncloud.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;우선 구글 스프레드 시트 API를 이용하기 위해서는 아래 작업이 필요하다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;[React] 구글 스프레드 시트 API 이용하기(with google-spreadsheet)&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;버전 정보 react: ^18.2.0 react-scripts: ^4.0.2 (5버전은 에러가 너무 많이 나서 다운그레이드) google-spreadsheet: ^3.3.0 구글 스프레드 시트 API 구글 스프레드 시트 API를 이용하여 받아온 데이터를 React에서 &quot; data-og-host=&quot;soojae.tistory.com&quot; data-og-source-url=&quot;https://soojae.tistory.com/67&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c6a8cp/hyRIAhfvWP/a8ON8dacrY1pPchnpqgqW0/img.png?width=800&amp;amp;height=731&amp;amp;face=0_0_800_731,https://scrap.kakaocdn.net/dn/ccSMN3/hyRIxY7Yaz/bbWeLLwpG4yzgL4JsxZ731/img.png?width=800&amp;amp;height=731&amp;amp;face=0_0_800_731,https://scrap.kakaocdn.net/dn/snKtf/hyRIqTe7Ur/7VbYxYbtiv5ABkp9Cs9Ub0/img.png?width=750&amp;amp;height=685&amp;amp;face=0_0_750_685&quot; data-og-url=&quot;https://soojae.tistory.com/67&quot;&gt;&lt;a href=&quot;https://soojae.tistory.com/67&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://soojae.tistory.com/67&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c6a8cp/hyRIAhfvWP/a8ON8dacrY1pPchnpqgqW0/img.png?width=800&amp;amp;height=731&amp;amp;face=0_0_800_731,https://scrap.kakaocdn.net/dn/ccSMN3/hyRIxY7Yaz/bbWeLLwpG4yzgL4JsxZ731/img.png?width=800&amp;amp;height=731&amp;amp;face=0_0_800_731,https://scrap.kakaocdn.net/dn/snKtf/hyRIqTe7Ur/7VbYxYbtiv5ABkp9Cs9Ub0/img.png?width=750&amp;amp;height=685&amp;amp;face=0_0_750_685');&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] 구글 스프레드 시트 API 이용하기(with google-spreadsheet)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;버전 정보 react: ^18.2.0 react-scripts: ^4.0.2 (5버전은 에러가 너무 많이 나서 다운그레이드) google-spreadsheet: ^3.3.0 구글 스프레드 시트 API 구글 스프레드 시트 API를 이용하여 받아온 데이터를 React에서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;soojae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;구글 스프레드 시트 작업이 완료되었다면, 이제 ChatGPT 사용을 위한 작업을 진행하자.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;구글 스프레드 시트에 ChatGPT 확장 프로그램 설치&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;OpenAI API 플랫폼 페이지에 접속한다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;OpenAI API&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;An API for accessing new AI models developed by OpenAI&quot; data-og-host=&quot;platform.openai.com&quot; data-og-source-url=&quot;https://platform.openai.com/overview&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://platform.openai.com&quot;&gt;&lt;a href=&quot;https://platform.openai.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://platform.openai.com/overview&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;OpenAI API&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An API for accessing new AI models developed by OpenAI&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;platform.openai.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;프로필을 눌러 View API keys메뉴 -&amp;gt;&amp;nbsp;&lt;s&gt;Create new secret key&lt;/s&gt;를 눌러 API 키를 발급한 후, 복사한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zuVot/btr0qXBSkSB/JdUmuIRmwRnWnWITXonYb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zuVot/btr0qXBSkSB/JdUmuIRmwRnWnWITXonYb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zuVot/btr0qXBSkSB/JdUmuIRmwRnWnWITXonYb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzuVot%2Fbtr0qXBSkSB%2FJdUmuIRmwRnWnWITXonYb1%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; alt=&quot;ChatGPT API 키 발급&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;417&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;확장 프로그램 -&amp;gt; 부가기능 설치하기 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nenxG/btr0NN0xcR0/Fc5HcoExR2wnhYz2tMQ0v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nenxG/btr0NN0xcR0/Fc5HcoExR2wnhYz2tMQ0v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nenxG/btr0NN0xcR0/Fc5HcoExR2wnhYz2tMQ0v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnenxG%2Fbtr0NN0xcR0%2FFc5HcoExR2wnhYz2tMQ0v0%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; alt=&quot;구글 스프레드 시트 부가기능 설치&quot; loading=&quot;lazy&quot; width=&quot;1198&quot; height=&quot;283&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;마켓플레이스에서 ChatGPT 를 검색하여 GPT for Sheets를 설치한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcpsRP/btr0sA7sBxX/BRQRogQHtr2Yin34TcnPak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcpsRP/btr0sA7sBxX/BRQRogQHtr2Yin34TcnPak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcpsRP/btr0sA7sBxX/BRQRogQHtr2Yin34TcnPak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcpsRP%2Fbtr0sA7sBxX%2FBRQRogQHtr2Yin34TcnPak%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; alt=&quot;구글 스프레드 시트 ChatGPT 플러그인 검색화면&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;518&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아래와 같이`Set API key`-&amp;gt; 발급 받은 API 키 값을 적용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5pzFm/btr0RPDIqc6/IBKCRJTnCzIDgU2ZmoQCOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5pzFm/btr0RPDIqc6/IBKCRJTnCzIDgU2ZmoQCOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5pzFm/btr0RPDIqc6/IBKCRJTnCzIDgU2ZmoQCOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5pzFm%2Fbtr0RPDIqc6%2FIBKCRJTnCzIDgU2ZmoQCOK%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; alt=&quot;구글 스프레드 시트 ChatGPT API 적용&quot; loading=&quot;lazy&quot; width=&quot;1041&quot; height=&quot;711&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;711&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;구글 스프레드 시트의 `GOOGLETRANSLATE` 기능을 사용하면 해당 키 값에 대한 번역을 해준다. (1차로 번역)&lt;br /&gt;`GPT_TRANSLATE()` 함수를 사용하지 않는 이유는, `GOOGLETRANSLATE`과 번역 품질이 비슷하다. GPT는 호출 할때마다 과금이 되니 그냥 `GOOGLETRANSLATE`를 사용했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1167&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6xhUU/btr0WewsWoQ/tA9FljtzMXf0UaENW12Xak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6xhUU/btr0WewsWoQ/tA9FljtzMXf0UaENW12Xak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6xhUU/btr0WewsWoQ/tA9FljtzMXf0UaENW12Xak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6xhUU%2Fbtr0WewsWoQ%2FtA9FljtzMXf0UaENW12Xak%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; alt=&quot;구글 스프레드 시트 번역&quot; loading=&quot;lazy&quot; width=&quot;1167&quot; height=&quot;219&quot; data-origin-width=&quot;1167&quot; data-origin-height=&quot;219&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;`키`값을 기준으로 `GOOGLETRANSLATE` 기능을 사용하여&amp;nbsp;각 국가의 언어로 번역해준다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;하지만 `키`값에 오타나, 영어가 아닌 언어, 잘못된 영어 문장이더라면(파란색 테두리) 확인이 불가능하다. &lt;br /&gt;이때 ChatGPT를 사용하여 이를 체크하고 수정해보자.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위의 `문장 체크 실행셀`을 체크하면 아래와 같이 값을 출력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w0FAq/btr07AMoGY3/jgie4eRN2zQWC96KUMwnM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w0FAq/btr07AMoGY3/jgie4eRN2zQWC96KUMwnM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w0FAq/btr07AMoGY3/jgie4eRN2zQWC96KUMwnM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw0FAq%2Fbtr07AMoGY3%2Fjgie4eRN2zQWC96KUMwnM1%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; alt=&quot;ChatGPT를 이용하여 체크&quot; loading=&quot;lazy&quot; width=&quot;1220&quot; height=&quot;160&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;만약 값이 올바르다면 &lt;s&gt;OK&lt;/s&gt;를 출력하고, 그렇지 않다면 올바른 값으로 수정되도록 만들었다. 그럼 ChatGPT가 고쳐준 값으로 다시 체크해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv1wSD/btr0Oa2umNf/UIHsk6AoqZjn0r9U6qlWGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv1wSD/btr0Oa2umNf/UIHsk6AoqZjn0r9U6qlWGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv1wSD/btr0Oa2umNf/UIHsk6AoqZjn0r9U6qlWGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv1wSD%2Fbtr0Oa2umNf%2FUIHsk6AoqZjn0r9U6qlWGK%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; alt=&quot;구글 스프레드 시트 값 통과&quot; loading=&quot;lazy&quot; width=&quot;1217&quot; height=&quot;258&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;고쳐준 값 전부 통과했다.&lt;br /&gt;GPT 함수의 형식은 다음과 같다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GPT(&quot;GPT를 적용 할 prompt&quot;, &quot;참조할 셀&quot;, &quot;Temperature (0~1값. 0에 가까울 수록 보수적, 1에 가까울수록 창의적인 답변을 리턴한다.)&quot;)&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위에 작성한 `문장 체크 결과`의 GPT 함수는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GPT(&quot;Is this a correct word or sentence with no typos in English? If it is, just show 'OK', if it is wrong, correct it. This word or sentence is: &quot;, A7, 0.1)&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;사용 후기&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;ChatGPT를 활용하여 구글 스프레드 시트 작업을 더욱 효율적으로 할 수 있었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;물론 아직은 번역의 품질이나, 정보의 정확성 등. 부족한 면이 아직 보이지만 이것도 GPT의 버전이 올라가면서 고쳐질 것이라 생각한다.&lt;br /&gt;구글이나 메타 등의 AI 서비스들이 출시되면 이런 서비스들의 발전이 더 가속화 될 것 같다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그렇게 되면 가까운 미래에는 AI가 수 많은 직업을 대체할 것 같다. 개발자, 특히 코드 작성만 할 줄 아는 개발자는 대체될 가능성이 높을 것 같다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;대체 불가능한 개발자가 되도록 더 열심히 공부하고 노력해야겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dX40Nb/btr0KRhQD3L/j3q4OxXfw5KZ9m6fslm6oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dX40Nb/btr0KRhQD3L/j3q4OxXfw5KZ9m6fslm6oK/img.png&quot; data-alt=&quot;창의적인 아이디어, 복잡한 문제 해결 능력 갖췄던데...?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dX40Nb/btr0KRhQD3L/j3q4OxXfw5KZ9m6fslm6oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdX40Nb%2Fbtr0KRhQD3L%2Fj3q4OxXfw5KZ9m6fslm6oK%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; alt=&quot;거짓말하는 ChatGPT&quot; loading=&quot;lazy&quot; width=&quot;802&quot; height=&quot;482&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;창의적인 아이디어, 복잡한 문제 해결 능력 갖췄던데...?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;그럼 이제 구글 스프레드 시트를 이용하여 React에서 i18n을 작업해보자!&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;gpt-for-sheet - &lt;a href=&quot;https://gptforwork.com/gpt-for-sheets/get-started&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://gptforwork.com/gpt-for-sheets/get-started&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <category>ai</category>
      <category>ChatGPT</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/70</guid>
      <comments>https://soojae.tistory.com/70#entry70comment</comments>
      <pubDate>Mon, 27 Feb 2023 20:50:35 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드 테스트 - TDD와 종류(Unit, Integration, E2E)</title>
      <link>https://soojae.tistory.com/74</link>
      <description>&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blg_inline_TDD_as_a_scaffold_diagram_mobile.jpeg&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xaln9/btr1Kv6nJh7/hnA7XB78GR6J17AruZTKX0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xaln9/btr1Kv6nJh7/hnA7XB78GR6J17AruZTKX0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xaln9/btr1Kv6nJh7/hnA7XB78GR6J17AruZTKX0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxaln9%2Fbtr1Kv6nJh7%2FhnA7XB78GR6J17AruZTKX0%2Fimg.jpg&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; alt=&quot;TDD로고&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;800&quot; data-filename=&quot;blg_inline_TDD_as_a_scaffold_diagram_mobile.jpeg&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;테스트 주도 개발(TDD)이란?&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;TDD는 Test-Driven Development의 약자로 실제 코드를 작성하기 전에 테스트 코드를 먼저 작성하는 개발 프로세스다. TDD 프로세스는 다음 단계를 따른다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트 코드 작성: 우선 구현하려는 동작을 설명하는 테스트 코드를 작성한다. 이때, 아직 동작을 구현하는 코드를 작성하지 않았으므로 테스트는 실패 해야한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;테스트 실행: 테스트를 실행한다. 위에 언급했듯이, 코드를 작성하지 않았으므로 실패해야한다.&lt;/li&gt;
&lt;li&gt;코드 작성: 1번에 작성했던 설명에 부합하는 코드를 작성한다. 이때 테스트를 통과하기 위한 목적으로만 작성해야한다.&lt;/li&gt;
&lt;li&gt;즉 아직 존재하지 않은 많은 문제들을 생각하여 코드를 작성하기보다는 테스트 목표에 집중하여 작성하는 것이 중요하다.&lt;/li&gt;
&lt;li&gt;테스트 실행: 테스트를 다시 실행한다.&lt;/li&gt;
&lt;li&gt;리팩터링: 테스트를 통과하면 코드를 리팩터링 한다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;TDD의 이점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트를 먼저 작성함으로써, 개발자는 구현하려는 동작에 대해 먼저 생각하게 되어 특정한 요구사항에 맞는 코드를 작성할 수 있다.&lt;/li&gt;
&lt;li&gt;코드의 유지보수성을 향상시키고, 시간이 지나도 프로젝트의 유지 관리가 쉬워진다.&lt;/li&gt;
&lt;li&gt;코드 수정 시 미처 생각지 못한 부분(기존 코드에 영향을 받는)에 대한 문제점을 미리 파악할 수 있다.&lt;/li&gt;
&lt;li&gt;애플리케이션에 대한 신뢰성이 높아진다.&lt;/li&gt;
&lt;li&gt;테스트 코드는 동작 방식을 설명하니, 테스트 파일 자체를 문서로 활용할 수&amp;nbsp; 있다.&lt;/li&gt;
&lt;li&gt;각종 에러 케이스에 대한 생각을 많이 하게 되므로 두뇌 건강에 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;테스트 종류&lt;/h2&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;480&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUy4cf/btr0BBNKAoh/L13eIc8GuCvWmU4ffZUljk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUy4cf/btr0BBNKAoh/L13eIc8GuCvWmU4ffZUljk/img.gif&quot; data-alt=&quot;잠금(Unit) 테스트는 통과. 잠금과 상호 작용하는 문(Integration) 테스트는 실패&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUy4cf/btr0BBNKAoh/L13eIc8GuCvWmU4ffZUljk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bUy4cf/btr0BBNKAoh/L13eIc8GuCvWmU4ffZUljk/img.gif&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; alt=&quot;Unit vs Integration Test&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;264&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;잠금(Unit) 테스트는 통과. 잠금과 상호 작용하는 문(Integration) 테스트는 실패&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;단위 테스트(Unit Test)&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;툴: Jasmine, Jest, Karma, Mocha&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;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예: 카카오 프렌즈샵에서 상품을 구매하는 경우, 단위 테스트는 `구매` 기능을 테스트할 때 고객의 잔액에서 상품의 금액 만큼 출금하는지, 상품의 재고를 업데이트 하는지, 영수증을 잘 생성하는지 각각 확인한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;코드 예시&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { render } from '@testing-library/react';
import Item from '../components/Item';

describe('Item', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;it('should render item name and price', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const { getByText } = render(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;Item name=&quot;Test Item&quot; price={12000} addToCart={() =&amp;gt; {}} /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(getByText('Test Item')).toBeInTheDocument();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(getByText('Price: 12000')).toBeInTheDocument();
&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;it('should remove item stock', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;// ... 
&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;// ...
});&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;통합 테스트(Integration Test)&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;툴 종류: Jest, Testing Library&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;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예: 카카오 프렌즈샵에서 상품을 구매하는 경우, 통합 테스트는 `구매`시 고객의 잔액, 재고 업데이트, 영수증 생성시 다른 구성 요소 간의 상호 작용이 잘 되어 구매가 완료되는지 확인한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;코드 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { render, screen, fireEvent } from '@testing-library/react';
import Shop from '../pages/shop';

describe('Shop', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;it('should display receipt after checkout', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;render(&amp;lt;Shop /&amp;gt;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const item1Button = screen.getByText('Item 1');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const item2Button = screen.getByText('Item 2');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const cartItems = screen.getAllByTestId('cart-item');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const checkoutButton = screen.getByText('Checkout');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const receiptItems = screen.getAllByTestId('receipt-item');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const total = screen.getByTestId('receipt-total');

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 장바구니에 상품 추가
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fireEvent.click(item1Button);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fireEvent.click(item2Button);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(cartItems).toHaveLength(2);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 장바구니에서 상품 제거
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const removeButton = cartItems[0].querySelector('button');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fireEvent.click(removeButton);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(cartItems).toHaveLength(1);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 상품을 구매하고, 영수증 출력
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fireEvent.click(checkoutButton);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(receiptItems).toHaveLength(1);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(receiptItems[0]).toHaveTextContent('Item 2 - 12000원');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(total).toHaveTextContent('Total: 16000원');
&amp;nbsp;&amp;nbsp;});
});&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;E2E 테스트 (End-To-End Test)&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;툴 종류: Cypress, Puppeteer&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;E2E 테스트는 시스템의 시작부터 끝까지 전체 흐름을 확인하는 테스트 유형이다.&lt;/li&gt;
&lt;li&gt;시스템이 예상대로 작동하고 사용자의 요구 사항을 충족하는지 확인하기 위해 모든 구성 요소와 해당 구성 요소의 상호 작용을 테스트하는 것이 포함된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; 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;상품 구매&lt;/li&gt;
&lt;li&gt;결제 방식 선택&lt;/li&gt;
&lt;li&gt;구매 완료&lt;/li&gt;
&lt;li&gt;구매 영수증을 고객 메일로 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위의 예시처럼 사용자의 입장에서 전체 Flow가 정상적으로 동작을 하는지 확인한다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;코드 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const puppeteer = require('puppeteer');

describe('Shopping', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;let browser;
&amp;nbsp;&amp;nbsp;let page;

&amp;nbsp;&amp;nbsp;beforeAll(async () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 테스트 환경 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;browser = await puppeteer.launch();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;page = await browser.newPage();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.goto('http://localhost:3000');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.type('[data-testid=&quot;login-email&quot;]', 'user@example.com');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.type('[data-testid=&quot;login-password&quot;]', 'password');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.click('[data-testid=&quot;login-button&quot;]');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.waitForNavigation();
&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;afterAll(async () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 브라우저 종료
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await browser.close();
&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;it('should allow a user to buy an item and receive a receipt', async () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 상품을 장바구니에 추가
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.click('[data-testid=&quot;item-123&quot;]');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.click('[data-testid=&quot;add-to-cart-button&quot;]');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.click('[data-testid=&quot;cart-icon&quot;]');

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 장바구니의 추가된 상품 체크
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const cartItem = await page.waitForSelector('[data-testid=&quot;cart-item&quot;]');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(await cartItem.evaluate(el =&amp;gt; el.innerText)).toContain('Item 1');

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 아이템 구매 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.click('[data-testid=&quot;checkout-button&quot;]');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.click('[data-testid=&quot;confirm-purchase-button&quot;]');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.waitForSelector('[data-testid=&quot;receipt&quot;]');

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 영수증 화면 출력
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const receiptItems = await page.$eval('[data-testid=&quot;receipt-items&quot;]', el =&amp;gt; el.innerText);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(receiptItems).toContain('Item 1');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(receiptItems).toContain('12000 원');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const receiptTotal = await page.$eval('[data-testid=&quot;receipt-total&quot;]', el =&amp;gt; el.innerText);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(receiptTotal).toContain('Total: 16000 원');

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 영수증을 고객 메일로 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await page.click('[data-testid=&quot;send-receipt-button&quot;]');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const receiptSent = await page.waitForSelector('[data-testid=&quot;receipt-sent&quot;]');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expect(await receiptSent.evaluate(el =&amp;gt; el.innerText)).toContain('Receipt sent to user@example.com');
&amp;nbsp;&amp;nbsp;});
});&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위에서 사용한 테스팅 라이브러리인 Puppeteer와 Jest에 대해 좀 더 알아보고 싶다면, 아래 포스트들을 읽어보는 것을 추천한다.&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;Puppeteer&lt;/h4&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;[test tool 1] Puppeteer란?&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;소개 puppeteer은 구글에서 만든 노드 라이브러리로 Headless Chrome 또는 Chrominum을 제어할 수 있다. * headless? 백그라운드에서 작동하는 브라우저이다. 때문에 일반 사용자가 브라우저를 사용할 때처럼&quot; data-og-host=&quot;kkangdda.tistory.com&quot; data-og-source-url=&quot;https://kkangdda.tistory.com/112&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/inczg/hyRKVdqkAd/CTLXKdlhxxNmKaXvfEUGz0/img.png?width=290&amp;amp;height=422&amp;amp;face=0_0_290_422,https://scrap.kakaocdn.net/dn/CvC8v/hyRJP0cOnj/oLFI5FWWNJoS6vXh020Sk1/img.png?width=290&amp;amp;height=422&amp;amp;face=0_0_290_422,https://scrap.kakaocdn.net/dn/gheBt/hyRKQiSYsT/KVVSRkpIb6HVkQ4npNz5a1/img.png?width=1622&amp;amp;height=932&amp;amp;face=0_0_1622_932&quot; data-og-url=&quot;https://kkangdda.tistory.com/112&quot;&gt;&lt;a href=&quot;https://kkangdda.tistory.com/112&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kkangdda.tistory.com/112&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/inczg/hyRKVdqkAd/CTLXKdlhxxNmKaXvfEUGz0/img.png?width=290&amp;amp;height=422&amp;amp;face=0_0_290_422,https://scrap.kakaocdn.net/dn/CvC8v/hyRJP0cOnj/oLFI5FWWNJoS6vXh020Sk1/img.png?width=290&amp;amp;height=422&amp;amp;face=0_0_290_422,https://scrap.kakaocdn.net/dn/gheBt/hyRKQiSYsT/KVVSRkpIb6HVkQ4npNz5a1/img.png?width=1622&amp;amp;height=932&amp;amp;face=0_0_1622_932');&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;[test tool 1] Puppeteer란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소개 puppeteer은 구글에서 만든 노드 라이브러리로 Headless Chrome 또는 Chrominum을 제어할 수 있다. * headless? 백그라운드에서 작동하는 브라우저이다. 때문에 일반 사용자가 브라우저를 사용할 때처럼&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kkangdda.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;Jest&lt;/h4&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Jest Tutorial: A Comprehensive Guide With Examples And Best Practices&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;This tutorial dive deep into every aspect of the Jest framework (i.e., installation, configuration, etc.) and test execution on local &amp;amp; cloud grids. The learnings of this Jest tutorial can be utilized in using Jest in an ideal manner in your project&quot; data-og-host=&quot;www.lambdatest.com&quot; data-og-source-url=&quot;https://www.lambdatest.com/learning-hub/jest-tutorial&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bcD03j/hyRKLQUaCh/tXAHaJ1GCEeXFKHoMqRxm1/img.png?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270,https://scrap.kakaocdn.net/dn/2pIe0/hyRKW5XELT/BZMIym75UIQtmHYKDZqFo0/img.png?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270,https://scrap.kakaocdn.net/dn/iFm2Q/hyRKT9eGw6/7j1o0XkmaMRm2FDKGML591/img.png?width=773&amp;amp;height=163&amp;amp;face=0_0_773_163&quot; data-og-url=&quot;https://www.lambdatest.com/learning-hub/jest-tutorial&quot;&gt;&lt;a href=&quot;https://www.lambdatest.com/learning-hub/jest-tutorial&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.lambdatest.com/learning-hub/jest-tutorial&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bcD03j/hyRKLQUaCh/tXAHaJ1GCEeXFKHoMqRxm1/img.png?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270,https://scrap.kakaocdn.net/dn/2pIe0/hyRKW5XELT/BZMIym75UIQtmHYKDZqFo0/img.png?width=480&amp;amp;height=270&amp;amp;face=0_0_480_270,https://scrap.kakaocdn.net/dn/iFm2Q/hyRKT9eGw6/7j1o0XkmaMRm2FDKGML591/img.png?width=773&amp;amp;height=163&amp;amp;face=0_0_773_163');&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;Jest Tutorial: A Comprehensive Guide With Examples And Best Practices&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This tutorial dive deep into every aspect of the Jest framework (i.e., installation, configuration, etc.) and test execution on local &amp;amp; cloud grids. The learnings of this Jest tutorial can be utilized in using Jest in an ideal manner in your project&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.lambdatest.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;그냥 E2E로만 테스트하면 안 될까?&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위의 코드를 보면 어차피 Unit, Integration 테스트 코드를 작성해도, 실제로 사용자는 E2E 방식으로 동작할텐데 굳이 시간 써가면서 각 코드를 써야할까라는 생각이 든다.&amp;nbsp;&lt;br /&gt;사실 Unit, Integration에서 테스트 했던 내용들은 E2E 테스트로 확인할 수 있고, 테스트가 중복되는 경우가 많다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;하지만&lt;/b&gt;, Unit이 E2E보다 테스트 실행 시간이 훨씬 빠르다. 그리고 세분화 할수록 어떤 에러가 발생했는지도 빠르게 체크 할 수 있다. 정리하면 다음과 같다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;E2E만 사용할 경우(granularity가 낮을 수록)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드 작성 시간&amp;nbsp;&amp;uarr;&lt;/li&gt;
&lt;li&gt;유지보수 비용 &amp;uarr;&lt;/li&gt;
&lt;li&gt;이슈 발생 시, 문제 해결 시간 &amp;darr;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;E2E, Integrationm, Unit을 사용할 경우(granularity가 높을 수록)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 작성 시간&amp;nbsp;&amp;darr;&lt;/li&gt;
&lt;li&gt;유지보수 비용 &amp;darr;&lt;/li&gt;
&lt;li&gt;이슈 발생 시, 문제 해결 시간 &amp;uarr;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 방식이 특별히 나쁘다라고 단언할 수는 없다. 각각의 Trade Off가 존재한다. 어느 정도까지 세분화를 할지는 프로젝트 규모와 개발 기한에 따라 달라질 것이다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; 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;필요없는 테스트는 넘어가도 된다: 예를 들어 데이터베이스를 연결이 잘 되는지는 프론트엔드와 관련이 없으므로 테스트할 필요가 없다. 이에 대한 것은 따로 Unit 테스트를 할 필요가 없다.&lt;/li&gt;
&lt;li&gt;E2E 테스트가 가장 우선시 되야한다: 결국은 사용자 관점으로 봐야하므로 E2E 테스트가 가장 우선시 되야한다. 하지만 동작 Flow와 관련 없는 것들은 세분화하자 (예: 프렌즈 샵의 상품의 재고를 표시하는 것은 굳이 E2E 테스트에서 확인할 필요가 없다.)&lt;/li&gt;
&lt;li&gt;함수의 기능이 `단순한 연결`일 경우는 작성하지 않아도 된다: 예를 들어 NextJS의 getStaticProps는 데이터를 갖고와서 props에 넘겨주기만 하는 함수이다. 이 함수는 테스트할 필요가 없다. (함수 내에 데이터를 변환하거나 복잡한 로직이 있으면 모를까)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Knowledge/Test</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/74</guid>
      <comments>https://soojae.tistory.com/74#entry74comment</comments>
      <pubDate>Mon, 27 Feb 2023 00:45:52 +0900</pubDate>
    </item>
    <item>
      <title>[Intellij] 인텔리제이에 ChatGPT 플러그인 적용</title>
      <link>https://soojae.tistory.com/75</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.jpg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcc6U9/btr0VKItHVa/KUeLWFr4O6kc9qT8osV811/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcc6U9/btr0VKItHVa/KUeLWFr4O6kc9qT8osV811/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcc6U9/btr0VKItHVa/KUeLWFr4O6kc9qT8osV811/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcc6U9%2Fbtr0VKItHVa%2FKUeLWFr4O6kc9qT8osV811%2Fimg.jpg&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; alt=&quot;ChatGPT로고&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-filename=&quot;다운로드.jpg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;23년 3월 11일 - ChatGPT 플러그인 2.1.3 버전 기준으로 정보 업데이트&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Intellij에도 ChatGPT 플러그인을 적용할 수 있다.&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;ChatGPT 플러그인 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Preferences... 메뉴( Mac 단축키: &lt;s&gt;Command + ,&lt;/s&gt; ) - &amp;gt; Plugins -&amp;gt; ChatGPT 플러그인 설치&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqJm3Y/btr0GKX4YFO/i75wTAqkO9GtpdQA5v1vUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqJm3Y/btr0GKX4YFO/i75wTAqkO9GtpdQA5v1vUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqJm3Y/btr0GKX4YFO/i75wTAqkO9GtpdQA5v1vUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqJm3Y%2Fbtr0GKX4YFO%2Fi75wTAqkO9GtpdQA5v1vUK%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; alt=&quot;Preference Plugin 화면&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;626&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;626&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;2. &lt;a href=&quot;https://platform.openai.com/account/api-keys&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://platform.openai.com/account/api-keys&lt;/a&gt; 에 로그인하고 API 키 발급&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkH5dU/btr0KSN35bH/X0QrFxXTsf73PFQwr5NIc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkH5dU/btr0KSN35bH/X0QrFxXTsf73PFQwr5NIc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkH5dU/btr0KSN35bH/X0QrFxXTsf73PFQwr5NIc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkH5dU%2Fbtr0KSN35bH%2FX0QrFxXTsf73PFQwr5NIc0%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; alt=&quot;openai 사이트 - api키 발급&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;484&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;484&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;3. 인텔리제이 사이드 바에 ChatGPT 아이콘 클릭 후, 설정 아이콘 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tRZil/btr0GavGL3t/QwB3zugK4ynWdZZmmIHON1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tRZil/btr0GavGL3t/QwB3zugK4ynWdZZmmIHON1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tRZil/btr0GavGL3t/QwB3zugK4ynWdZZmmIHON1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtRZil%2Fbtr0GavGL3t%2FQwB3zugK4ynWdZZmmIHON1%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; alt=&quot;ChatGPT플러그인 아이콘&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;578&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;578&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;4. Official API키를 추가한다. (직접 ChatGPT에 요청을 보내는 API 서버를 만들었다면, Customize에 API 주소를 넣으면 된다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B3o3K/btr20xaF7ak/duawjjpBGGJay0UgQeNtj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B3o3K/btr20xaF7ak/duawjjpBGGJay0UgQeNtj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B3o3K/btr20xaF7ak/duawjjpBGGJay0UgQeNtj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB3o3K%2Fbtr20xaF7ak%2FduawjjpBGGJay0UgQeNtj0%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; alt=&quot;Official API key access token&quot; loading=&quot;lazy&quot; width=&quot;1018&quot; height=&quot;419&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ Email과 Password를 입력하고 로그인하면, 정보 저장은 절대 안하고 자동으로 토큰을 refresh 해준다고는 하는데... 굳이?&amp;nbsp;&lt;br /&gt;Access Token만 넣는게 안전해보여서 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;4-1. GPT 3.5 Turbo도 설정해서 사용할 수 있다. &lt;a href=&quot;https://pub.towardsai.net/building-customized-chatbots-for-the-web-using-gpt-3-5-turbo-190424827493&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GPT-3.5란?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yNTv1/btr292f8btg/zPHXx9kStzceNv8aTuQqW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yNTv1/btr292f8btg/zPHXx9kStzceNv8aTuQqW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yNTv1/btr292f8btg/zPHXx9kStzceNv8aTuQqW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyNTv1%2Fbtr292f8btg%2FzPHXx9kStzceNv8aTuQqW0%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;926&quot; height=&quot;270&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;270&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;4-2. 자신만의 Custom Actions를 지정할 수 있다. (&lt;s&gt;ChatGPT 플러그인 사용법&lt;/s&gt; - &lt;s&gt;코드&amp;nbsp;요청&lt;/s&gt;에서 설명)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnMNDy/btr2ZUK5bOh/4t4rQD6qfiInbydoUcyTkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnMNDy/btr2ZUK5bOh/4t4rQD6qfiInbydoUcyTkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnMNDy/btr2ZUK5bOh/4t4rQD6qfiInbydoUcyTkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnMNDy%2Fbtr2ZUK5bOh%2F4t4rQD6qfiInbydoUcyTkk%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;788&quot; height=&quot;340&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;340&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ChatGPT 플러그인 2.0 버전일 경우 설정&lt;/h4&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Offical 에 API Key를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/67gie/btr0DWLdIg1/wXxq5ueQLVfaXli7bJHre0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/67gie/btr0DWLdIg1/wXxq5ueQLVfaXli7bJHre0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/67gie/btr0DWLdIg1/wXxq5ueQLVfaXli7bJHre0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F67gie%2Fbtr0DWLdIg1%2FwXxq5ueQLVfaXli7bJHre0%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;800&quot; height=&quot;535&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;연결 세팅 값&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read Timeout : GPT가 질문에 답변하기까지 대기할 수 있는 시간. 초과시 연결이 끊어진다. ( 네트워크 상태가 좋지 않으면 시간을 더 길게 변경할 수 있다. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection Timeout : GPT에 연결되기까지 대기할 수 있는 시간. 초과시 연결이 끊어진다. (네트워크 상태가 좋지 않으면 시간을 더 길게 변경할 수 있다.)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32;&quot;&gt;ChatGPT 플러그인 사용법&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #292c32;&quot;&gt;&lt;/span&gt;&lt;/h2&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;span style=&quot;background-color: #ffffff; color: #292c32;&quot;&gt;기본 요청&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 질문을 작성하면 GPT가 답변해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (1).png&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4KgVp/btr0GbH8YPh/pNFwmckdYiOKunmwJFmil1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4KgVp/btr0GbH8YPh/pNFwmckdYiOKunmwJFmil1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4KgVp/btr0GbH8YPh/pNFwmckdYiOKunmwJFmil1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4KgVp%2Fbtr0GbH8YPh%2FpNFwmckdYiOKunmwJFmil1%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;691&quot; height=&quot;589&quot; data-filename=&quot;다운로드 (1).png&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;589&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;아래 Search History를 클릭하면 사용자가 인텔리제이에서 찾았었던 (Mac 단축키: &lt;s&gt;Command + Shift + F&lt;/s&gt;) 값들에 대한 목록이 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (2).png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;887&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buoqWV/btr0KR2KjFr/x9jXLMHa3GIe5nqNayV961/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buoqWV/btr0KR2KjFr/x9jXLMHa3GIe5nqNayV961/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buoqWV/btr0KR2KjFr/x9jXLMHa3GIe5nqNayV961/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuoqWV%2Fbtr0KR2KjFr%2Fx9jXLMHa3GIe5nqNayV961%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;720&quot; height=&quot;887&quot; data-filename=&quot;다운로드 (2).png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;887&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;h3 data-ke-size=&quot;size23&quot;&gt;코드 요청&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcLyRb/btr20xIFBHV/ZY1xEKodU1cpLH8PeLlgf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcLyRb/btr20xIFBHV/ZY1xEKodU1cpLH8PeLlgf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcLyRb/btr20xIFBHV/ZY1xEKodU1cpLH8PeLlgf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcLyRb%2Fbtr20xIFBHV%2FZY1xEKodU1cpLH8PeLlgf1%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; alt=&quot;요청 prefix&quot; loading=&quot;lazy&quot; width=&quot;863&quot; height=&quot;255&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;255&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;코드 블럭을 지정한 후에 우클릭을 하면 preset 들을 이용하여 질문할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lf9Ko/btr21Cwed5a/E2ySLWGkc556M3Idm0eP71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lf9Ko/btr21Cwed5a/E2ySLWGkc556M3Idm0eP71/img.png&quot; data-alt=&quot;코드 선택후 Explain prefix를 사용한 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lf9Ko/btr21Cwed5a/E2ySLWGkc556M3Idm0eP71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flf9Ko%2Fbtr21Cwed5a%2FE2ySLWGkc556M3Idm0eP71%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; alt=&quot;요청 prefix Explain this code&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;778&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;코드 선택후 Explain prefix를 사용한 결과&lt;/figcaption&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;위의 요청 preset 목록 중 &lt;s&gt;Custom Actions&lt;/s&gt;를 누르면 위에 작업 했던 Custom Actions를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GHHze/btr27XsQFt6/ladin1Ruq9it0Jrhxkahw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GHHze/btr27XsQFt6/ladin1Ruq9it0Jrhxkahw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GHHze/btr27XsQFt6/ladin1Ruq9it0Jrhxkahw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGHHze%2Fbtr27XsQFt6%2Fladin1Ruq9it0Jrhxkahw1%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; alt=&quot;요청 prefix Custom Actions&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;254&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;254&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;위 팝업에서 `Add Custom Action`을 누르면 요청을 보내기 쉽게 모달창을 띄워준다. (주로 사용하는 기능)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNSIlt/btr20AlFyhh/jwmDxBbFMrsclYawTfBCEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNSIlt/btr20AlFyhh/jwmDxBbFMrsclYawTfBCEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNSIlt/btr20AlFyhh/jwmDxBbFMrsclYawTfBCEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNSIlt%2Fbtr20AlFyhh%2FjwmDxBbFMrsclYawTfBCEK%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; alt=&quot;Add Custom Action 모달&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;557&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;557&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;위 기능은 주로 사용하는 기능이다보니 단축키를 지정하는 것이 좋다. 제공해주는 단축키가 있긴한데, 기본 단축키가 Mac 기준으로 `Shift + Command + Q`로 되어있다. &lt;br /&gt;그런데 이건 Mac 로그아웃 단축키라 키가 안먹히는데... 다른 단축키로 변경하자.&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;WebStorm -&amp;gt; Preferences... -&amp;gt; Keymap -&amp;gt; Plugins -&amp;gt; ChatGPT -&amp;gt; `Ctrl + Shift + C`로 변경&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bntOir/btr3asF0Acq/0IK9AQFDJBAOLAviO3F8u0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bntOir/btr3asF0Acq/0IK9AQFDJBAOLAviO3F8u0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bntOir/btr3asF0Acq/0IK9AQFDJBAOLAviO3F8u0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbntOir%2Fbtr3asF0Acq%2F0IK9AQFDJBAOLAviO3F8u0%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; alt=&quot;단축키 변경&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;494&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;494&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;이렇게 설정하면 Extension selection( Mac 단축키 : `Option + &amp;uarr;` )와 조합하여 야무지게 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1OaJd/btr24MZRqDp/9rrbQFu4XSEAWAl7cWwm91/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1OaJd/btr24MZRqDp/9rrbQFu4XSEAWAl7cWwm91/img.gif&quot; data-alt=&quot;선택 범위를 지정한 후 Custom Action 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1OaJd/btr24MZRqDp/9rrbQFu4XSEAWAl7cWwm91/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b1OaJd/btr24MZRqDp/9rrbQFu4XSEAWAl7cWwm91/img.gif&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; alt=&quot;선택 범위를 지정한 후 Custom Action 실행&quot; loading=&quot;lazy&quot; width=&quot;990&quot; height=&quot;672&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;선택 범위를 지정한 후 Custom Action 실행&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;`Online Chat` 탭을 누르면 직접&amp;nbsp;&lt;span&gt;ChatGPT 사이트에 접속해서 사용할 수 있지만, 현재는 사용할 수 없다.&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 ChatGPT가 오픈된 지 얼마 안 됐지만, 이미 여러 개발자들이 ChatGPT를 활용한 다양한 플러그인들을 개발하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 ChatGPT 버전이 업데이트 되면서 얼마나 많은 것들이 바뀔지 기대된다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Tools/IntelliJ</category>
      <category>ai</category>
      <category>ChatGPT</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/75</guid>
      <comments>https://soojae.tistory.com/75#entry75comment</comments>
      <pubDate>Sun, 26 Feb 2023 00:11:03 +0900</pubDate>
    </item>
    <item>
      <title>[React] Module not found: Can't resolve 'http2' in ~</title>
      <link>https://soojae.tistory.com/69</link>
      <description>&lt;p style=&quot;text-align: left;&quot; 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;768&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IOqbC/btrZ7bVIMfw/9ql5Jq6tkkrokkDDB49Q4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IOqbC/btrZ7bVIMfw/9ql5Jq6tkkrokkDDB49Q4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IOqbC/btrZ7bVIMfw/9ql5Jq6tkkrokkDDB49Q4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIOqbC%2FbtrZ7bVIMfw%2F9ql5Jq6tkkrokkDDB49Q4k%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;768&quot; height=&quot;500&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;문제 발생&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;React에서 googleapis 라이브러리를 사용해보려고 했더니 다음과 같은 에러가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFVsHe/btrZ6iAKtnm/fIvnk9B6h4JkAFiKN9LpXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFVsHe/btrZ6iAKtnm/fIvnk9B6h4JkAFiKN9LpXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFVsHe/btrZ6iAKtnm/fIvnk9B6h4JkAFiKN9LpXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFVsHe%2FbtrZ6iAKtnm%2FfIvnk9B6h4JkAFiKN9LpXk%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;1119&quot; height=&quot;93&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;원인은 간단했다. googleapis가 Node.js에서만 동작하는 라이브러리기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmoZIi/btrZ4booJhk/skoyV43VoEISLZKpFBe2l1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmoZIi/btrZ4booJhk/skoyV43VoEISLZKpFBe2l1/img.png&quot; data-alt=&quot;npm을 잘 읽자...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmoZIi/btrZ4booJhk/skoyV43VoEISLZKpFBe2l1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmoZIi%2FbtrZ4booJhk%2FskoyV43VoEISLZKpFBe2l1%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;808&quot; height=&quot;412&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;npm을 잘 읽자...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;해당 라이브러리는 Nodejs (express, nestjs 등)에서만 동작하는 라이브러리다. 사용하지 말자.&lt;/p&gt;</description>
      <category>React</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/69</guid>
      <comments>https://soojae.tistory.com/69#entry69comment</comments>
      <pubDate>Fri, 24 Feb 2023 04:32:04 +0900</pubDate>
    </item>
    <item>
      <title>[React] 구글 스프레드 시트 API 이용하기(with google-spreadsheet)</title>
      <link>https://soojae.tistory.com/67</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;731&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcyauC/btrZ2AaK32G/kt2Jyz1BeKbtVaiRAHuvXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcyauC/btrZ2AaK32G/kt2Jyz1BeKbtVaiRAHuvXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcyauC/btrZ2AaK32G/kt2Jyz1BeKbtVaiRAHuvXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcyauC%2FbtrZ2AaK32G%2Fkt2Jyz1BeKbtVaiRAHuvXk%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; alt=&quot;구글 스프레드 로고&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;731&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;731&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;react: ^18.2.0&lt;/li&gt;
&lt;li&gt;react-scripts: ^4.0.2 (5버전은 에러가 너무 많이 나서 다운그레이드)&lt;/li&gt;
&lt;li&gt;google-spreadsheet: ^3.3.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구글 스프레드 시트 API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 스프레드 시트 API를 이용하여 받아온 데이터를 React에서 처리하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 이용하면 자바스크립트나 파이썬등에서 코드를 통해 시트 내용을 조회, 생성, 수정, 삭제를 할 수 있다.&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;예를 들어, 기획자들이 스프레드시트에서 작업한 내용을 DB에 반영해야 하는 경우가 종종 있다. 하지만 스프레드시트의 데이터가 자주 업데이트되면, 이를 관리하는 데 상당한 노력이 필요하게 된다. 반대로, 개발 측에서 데이터를 수정하면, 그 변경 내용을 스프레드시트에도 반영해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서 한쪽에서 변경된 내용이 실수로 전달되지 않으면, 스프레드시트와 개발 쪽의 데이터가 일치하지 않을 위험이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 구글 스프레드시트 API를 사용하면 두 매체 간의 데이터를 자동으로 동기화할 수 있어, 이러한 문제를 효과적으로 해결할 수 있다.&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;구글 스프레드 시트 API 등록 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선&amp;nbsp;&lt;a href=&quot;https://console.cloud.google.com/apis/credentials&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://console.cloud.google.com/apis/credentials&lt;/a&gt;로 이동하여 프로젝트를 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_p0.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cy5DqR/btr0klEvATZ/KwdiLsmEKC49fLlgd1oStk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cy5DqR/btr0klEvATZ/KwdiLsmEKC49fLlgd1oStk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cy5DqR/btr0klEvATZ/KwdiLsmEKC49fLlgd1oStk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcy5DqR%2Fbtr0klEvATZ%2FKwdiLsmEKC49fLlgd1oStk%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; alt=&quot;구글 스프레드 시트 API 사이트&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;756&quot; data-filename=&quot;edited_p0.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMz9Xk/btr0sz8yEJo/KYAuKnP8hACGY1OVj4ftFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMz9Xk/btr0sz8yEJo/KYAuKnP8hACGY1OVj4ftFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMz9Xk/btr0sz8yEJo/KYAuKnP8hACGY1OVj4ftFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMz9Xk%2Fbtr0sz8yEJo%2FKYAuKnP8hACGY1OVj4ftFk%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; alt=&quot;구글 스프레드 시트 프로젝트 추가&quot; loading=&quot;lazy&quot; width=&quot;579&quot; height=&quot;514&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;514&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqSB55/btr0aUd4q2J/HVwcGzuJXnLle6fYsIjEk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqSB55/btr0aUd4q2J/HVwcGzuJXnLle6fYsIjEk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqSB55/btr0aUd4q2J/HVwcGzuJXnLle6fYsIjEk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqSB55%2Fbtr0aUd4q2J%2FHVwcGzuJXnLle6fYsIjEk0%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; alt=&quot;구글 스프레드 시트 사용자 인증 정보&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;374&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0oWtv/btrZ7buGPOT/rgB5ZlmQ4VkcKPg65BkkLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0oWtv/btrZ7buGPOT/rgB5ZlmQ4VkcKPg65BkkLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0oWtv/btrZ7buGPOT/rgB5ZlmQ4VkcKPg65BkkLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0oWtv%2FbtrZ7buGPOT%2FrgB5ZlmQ4VkcKPg65BkkLk%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; alt=&quot;구글 스프레드 시트 서비스 계정&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;518&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;518&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;843&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXPyLv/btrZ67Y6FLP/68kBHDUJhKk2rYDU09Z0o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXPyLv/btrZ67Y6FLP/68kBHDUJhKk2rYDU09Z0o1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXPyLv/btrZ67Y6FLP/68kBHDUJhKk2rYDU09Z0o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXPyLv%2FbtrZ67Y6FLP%2F68kBHDUJhKk2rYDU09Z0o1%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; alt=&quot;구글 스프레드 시트 API 키 발급&quot; loading=&quot;lazy&quot; width=&quot;793&quot; height=&quot;843&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;843&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;키 추가가 완료되면, 해당 키값이 들어있는 json파일이 다운로드된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEVgBx/btrZUCM67Kh/Z8rB3mDYEnDhGl4FqJbBg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEVgBx/btrZUCM67Kh/Z8rB3mDYEnDhGl4FqJbBg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEVgBx/btrZUCM67Kh/Z8rB3mDYEnDhGl4FqJbBg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEVgBx%2FbtrZUCM67Kh%2FZ8rB3mDYEnDhGl4FqJbBg0%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; alt=&quot;구글 스프레드 시트 API 키 파일 다운로드&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;568&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;568&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;h2 data-ke-size=&quot;size26&quot;&gt;구글 스프레드 시트 API 활성화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 구글 스프레드 시트를 만들고, 위의 파일을 적용하면 API가 닫혀있어서 403 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 API를 사용 상태로 수정하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YEy6R/btrZ7bVGhxi/hHBYHRikJTGF9UGayuOAIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YEy6R/btrZ7bVGhxi/hHBYHRikJTGF9UGayuOAIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YEy6R/btrZ7bVGhxi/hHBYHRikJTGF9UGayuOAIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYEy6R%2FbtrZ7bVGhxi%2FhHBYHRikJTGF9UGayuOAIK%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; alt=&quot;구글 스프레드 시트 API 활성화&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;728&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pBG2K/btrZ85HbIQN/BqSv4rPvmak8oBYz2F8UD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pBG2K/btrZ85HbIQN/BqSv4rPvmak8oBYz2F8UD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pBG2K/btrZ85HbIQN/BqSv4rPvmak8oBYz2F8UD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpBG2K%2FbtrZ85HbIQN%2FBqSv4rPvmak8oBYz2F8UD0%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; alt=&quot;구글 스프레드 시트 API 검색&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;340&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzi4PT/btrZ67ThatP/SoaBzryASd3iDizGYKMQKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzi4PT/btrZ67ThatP/SoaBzryASd3iDizGYKMQKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzi4PT/btrZ67ThatP/SoaBzryASd3iDizGYKMQKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzi4PT%2FbtrZ67ThatP%2FSoaBzryASd3iDizGYKMQKk%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; alt=&quot;구글 스프레드 시트 API 활성화 버튼&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;354&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;354&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;h2 data-ke-size=&quot;size26&quot;&gt;권한 부여&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 작업 했더라도, 공유가 안되어 있어서 메시지만 다른 403 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 계정 목록에 있는 계정을 추가하자. 키 생성시 다운로드 받은 json파일에도 client_email이 존재한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EVAdQ/btrZ6jNcLEI/aOcH1xKAVN30XwIKz4tf00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EVAdQ/btrZ6jNcLEI/aOcH1xKAVN30XwIKz4tf00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EVAdQ/btrZ6jNcLEI/aOcH1xKAVN30XwIKz4tf00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEVAdQ%2FbtrZ6jNcLEI%2FaOcH1xKAVN30XwIKz4tf00%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; alt=&quot;구글 스프레드 시트 API 권한 부여&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;584&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;584&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;스프레드 시트를 입맛에 맞게 만든다. 이때 Document ID와 worksheet ID값을 복사한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BBm9e/btrZ7qkJVIV/kabqq7siF1rbXHPYQYqa30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BBm9e/btrZ7qkJVIV/kabqq7siF1rbXHPYQYqa30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BBm9e/btrZ7qkJVIV/kabqq7siF1rbXHPYQYqa30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBBm9e%2FbtrZ7qkJVIV%2Fkabqq7siF1rbXHPYQYqa30%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; alt=&quot;구글 스프레드 시트 API ID 값들&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;297&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;297&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;h2 data-ke-size=&quot;size26&quot;&gt;React 작업&lt;/h2&gt;
&lt;pre id=&quot;code_1676908339109&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$yarn add google-spreadsheet --dev

# 타입 스크립트를 사용할 경우
$yarn add @types/google-spreadsheet --dev&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 다운로드 받은 파일을 적절한 위치에 넣은 후 require하고, 바로 위에서 복사한 Document ID 값을 넣어준다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// src/libs/googlesheet.ts

import {GoogleSpreadsheet, GoogleSpreadsheetRow} from &quot;google-spreadsheet&quot;;
import {useEffect, useState} from &quot;react&quot;;

const credential = require(키 추가한 후 다운로드된 json파일 위치);

// 구글 시트 조회하는 로직
export const getGoogleSheet: () =&amp;gt; Promise&amp;lt;GoogleSpreadsheet&amp;gt; = async () =&amp;gt; {
    const doc = new GoogleSpreadsheet(Document ID 값 넣는 곳);
    // 구글 인증이 필요하다.
    await doc.useServiceAccountAuth(credential);
    await doc.loadInfo();
    return doc;
}

// 구글 시트 조회하는 custom useHook
const useGoogleSheet = (sheetId: string) =&amp;gt; {
    const [googleSheetRows, setGoogleSheetRows] = useState&amp;lt;GoogleSpreadsheetRow[]&amp;gt;([]);

    const fetchGoogleSheetRows = async () =&amp;gt; {
        const googleSheet = await getGoogleSheet();
        const sheetsByIdElement = googleSheet.sheetsById[sheetId];
        const rows = await sheetsByIdElement.getRows();
        setGoogleSheetRows(rows)
    }

    useEffect(() =&amp;gt; {
        fetchGoogleSheetRows();
    },[]);

    return [googleSheetRows];
}

export default useGoogleSheet;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;worksheet ID값도 넣어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1676888976552&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/App.tsx

const App = () =&amp;gt; {

    const [data] = useGoogleSheet(worksheet ID 값 넣는곳);
    const languageList = ['아이디','설명'];

    return (
        &amp;lt;div&amp;gt;
            {data.map((row) =&amp;gt; {
                return (&amp;lt;div&amp;gt;아이디: {row[languageList[0]]}, 설명: {row[languageList[1]]}&amp;lt;/div&amp;gt;);
            })}
        &amp;lt;/div&amp;gt;
    )

}

export default App;&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;React를 실행하면 정상적으로 받아오는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tSbAn/btrZ9MtPvTf/jzQks105r6JtsQCM7znfJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tSbAn/btrZ9MtPvTf/jzQks105r6JtsQCM7znfJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tSbAn/btrZ9MtPvTf/jzQks105r6JtsQCM7znfJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtSbAn%2FbtrZ9MtPvTf%2FjzQks105r6JtsQCM7znfJK%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; alt=&quot;React 결과 값&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;503&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;503&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Refereneces&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thenewstack 사이트 - &lt;a href=&quot;https://thenewstack.io/how-to-use-google-sheets-as-a-database-with-react-and-ssr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://thenewstack.io/how-to-use-google-sheets-as-a-database-with-react-and-ssr&lt;/a&gt;&lt;/p&gt;</description>
      <category>React</category>
      <category>React</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/67</guid>
      <comments>https://soojae.tistory.com/67#entry67comment</comments>
      <pubDate>Wed, 22 Feb 2023 20:56:27 +0900</pubDate>
    </item>
    <item>
      <title>[Next.JS] SSG - getStaticPaths</title>
      <link>https://soojae.tistory.com/65</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;NextJS로고.jpeg&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2KCiw/btr1lvL8Q6s/0Pnwij62oFiGeq1NtNoJeK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2KCiw/btr1lvL8Q6s/0Pnwij62oFiGeq1NtNoJeK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2KCiw/btr1lvL8Q6s/0Pnwij62oFiGeq1NtNoJeK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2KCiw%2Fbtr1lvL8Q6s%2F0Pnwij62oFiGeq1NtNoJeK%2Fimg.jpg&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; alt=&quot;NextJS 로고&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;700&quot; data-filename=&quot;NextJS로고.jpeg&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;getStaticProps에 대해 익숙하지 않다면, 아래 글을 먼저 보는 것을 추천 드립니다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;[Next.JS] SSG - getStaticProps&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;getStaticProps SSG(Static Site Generator)는 페이지를 호출할 때마다 서버에서 pre-render하는 SSR와 달리 빌드시점에 pre-rendering을 한다. // posts는 빌드 시점에 getStaticProps()에 의해 채워진다. function Blog({ posts }&quot; data-og-host=&quot;soojae.tistory.com&quot; data-og-source-url=&quot;https://soojae.tistory.com/64&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zcw0y/hyRHxrbj18/dxhGM6Dtxzukm6YTeYI4eK/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264,https://scrap.kakaocdn.net/dn/frglR/hyRHr5xYen/K4iD9IO7tUowiaakorpPaK/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264,https://scrap.kakaocdn.net/dn/cH5l1n/hyRHsQXI0a/wKvlwo6BsKCZxrx32VQKu0/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264&quot; data-og-url=&quot;https://soojae.tistory.com/64&quot;&gt;&lt;a href=&quot;https://soojae.tistory.com/64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://soojae.tistory.com/64&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zcw0y/hyRHxrbj18/dxhGM6Dtxzukm6YTeYI4eK/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264,https://scrap.kakaocdn.net/dn/frglR/hyRHr5xYen/K4iD9IO7tUowiaakorpPaK/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264,https://scrap.kakaocdn.net/dn/cH5l1n/hyRHsQXI0a/wKvlwo6BsKCZxrx32VQKu0/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264');&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;[Next.JS] SSG - getStaticProps&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;getStaticProps SSG(Static Site Generator)는 페이지를 호출할 때마다 서버에서 pre-render하는 SSR와 달리 빌드시점에 pre-rendering을 한다. // posts는 빌드 시점에 getStaticProps()에 의해 채워진다. function Blog({ posts }&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;soojae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;getStaticPaths&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;동적 경로를 사용하는 페이지에서 getStaticPaths라는 함수를 사용할 때 Next.js는 getStaticPaths에 지정된 모든 경로를 정적으로 미리 렌더링한다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;paths&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;paths값은 pre-rendering할 경로를 결정한다. 예를 들어 pages/posts/[id].js라는 이름의 동적 경로를 사용하는 페이지가 있다고 가정해보자. 이 페이지에서 getStaticPaths 메서드를 통해 다음을 return한다면&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export async function getStaticPaths() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;paths: [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{ params: { id: '1' }},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;params: { id: '2' },
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// i18n을 구성하면 경로에 대한 locale도 return할 수 있다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;locale: &quot;en&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fallback: true, false 또는 &quot;blocking&quot; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 작성하면 /posts/1 와 /posts/2이 `next build`시에 정적으로 페이지가 생성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지 이름이 `pages/posts/[postId]/[commentId]`인 경우 params에는 `postId`와 `commentId`가 포함되어야 한다.&lt;/li&gt;
&lt;li&gt;페이지 이름이 `pages/[...slug]`와 같은 포괄 경로를 사용하는 경우 매개변수에는 배열인 slug가 포함되어야 한다. 이 배열이 `['hello', 'world']`인 경우 Next.js는 /hello/world에 페이지를 정적으로 생성한다.&lt;/li&gt;
&lt;li&gt;페이지에서 &lt;a href=&quot;https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes&quot; target=&quot;_self&quot;&gt;&lt;span&gt;optional catch-all-route&lt;/span&gt;&lt;/a&gt;를 사용 할때, `null, [], undefined, false`를 사용하여 루트 최상위 경로를 렌더링한다. 예를 들어, `pages/[[...slug]]`에 `slug: false`를 제공하면 Next.js는 `/` 페이지를 정적으로 생성한다.&lt;/li&gt;
&lt;li&gt;params는 &lt;b&gt;case-sensitive&lt;/b&gt;하다. 그래서 Posts와 posts는 다른 경로로 인식한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Fallback 옵션&lt;/h2&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;false일 경우&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getStaticPaths에서 return하지 않은 path는 404 페이지를 return한다.&lt;/li&gt;
&lt;li&gt;이 옵션은 생성할 경로 수가 적거나 새 페이지 데이터를 자주 추가하지 않는 경우에 유용하다&lt;/li&gt;
&lt;li&gt;경로를 더 추가해야 하는데 `fallback: false`로 설정한 경우 빌드를 다시 실행해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;true일 경우&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;getStaticProps의 동작은 다음과 같은 방식으로 변경된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getStaticPaths에서 return된 경로는 빌드 시 getStaticProps에 의해 HTML로 렌더링된다.&lt;/li&gt;
&lt;li&gt;빌드 시점에 생성되지 않은 경로는 404 페이지가 생성되지 않는다. 대신 Next.js는 해당 경로에 대한 첫 번째 요청 시 페이지의 &quot;fallback&quot; 버전을 제공한다. 구글과 같은 웹 크롤러는 fallback이 제공되지 않고, `path`는 `fallback: blocking`처럼 동작한다.&lt;/li&gt;
&lt;li&gt;`fallback: true`로 설정된 페이지가 `next/link` 또는 `next/router`(클라이언트 측)를 통해 탐색되는 경우 Next.js는 fallback을 제공하지 않고 대신 페이지가 `fallback: blocking`으로 동작한다.&lt;/li&gt;
&lt;li&gt;백그라운드에서 Next.js는 요청된 경로 HTML과 JSON을 정적으로 생성한다. 이때 getStaticProps도 실행된다.&lt;/li&gt;
&lt;li&gt;완료되면 브라우저는 생성된 경로에 대한 JSON을 수신한다. 이는 필요한 props들이 포함된 페이지를 자동으로 렌더링하는 데 사용된다. 사용자 입장에서는 페이지가 fallback 페이지에서 full 페이지로 바뀐다.&lt;/li&gt;
&lt;li&gt;동시에 Next.js는 이 경로를 미리 렌더링된 페이지 목록에 추가하여 동일한 경로에 요청이 들어오면, 빌드 할 때 pre-rendering된 페이지들과 마찬가지로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;`fallback: true`는 &lt;a href=&quot;https://nextjs.org/docs/advanced-features/static-html-export&quot; target=&quot;_self&quot;&gt;&lt;span&gt;next export&lt;/span&gt;&lt;/a&gt;를 사용할땐 동작하지 않는다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;fallback: true는 언제 사용해야 할까?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱 데이터에 의존하는 정적페이지가 매우 많은 경우(ex: 매우 큰 전자 상거래 사이트)에 유용하다. 모든 제품 페이지를 빌드 시간에 미리 렌더링 하는 것은 매우 오랜 시간이 걸린다.&lt;/li&gt;
&lt;li&gt;대신 페이지의 작은 하위 subset을 정적으로 생성하고 나머지 페이지에 대해서는 `fallback:true`로 한다. 사용자가 아직 생성되지 않은 페이지를 요청하면 loading indicator 또는 skeleton 컴포넌트 페이지가 표시된다.&lt;/li&gt;
&lt;li&gt;getStaticProps가 완료되면 요청된 데이터로 페이지가 렌더링된다.&lt;/li&gt;
&lt;li&gt;`fallback: true`일 경우 생성된 페이지를 업데이트 할 수 없다. &lt;a href=&quot;https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration&quot; target=&quot;_self&quot;&gt;&lt;span&gt;ISR&lt;/span&gt;&lt;/a&gt;을 확인하자&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Blocking일 경우&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;true일 경우와 비슷하게 동작하지만, getStaticPaths가 return하지 않은 새 path는 SSR과 동일하게 HTML이 생성될 때까지 기다린 다음 이후 요청을 위해 캐시되어 `path`당 한 번만 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getStaticPaths에서 return된 `path`는 빌드 시점에 getStaticProps에 의해 HTML로 렌더링된다.&lt;/li&gt;
&lt;li&gt;빌드 시점에 생성되지 않은 경로는 404 페이지가 생성되지 않는다. 대신 Next.js는 첫 번째 요청에 대해 SSR을 수행하고 생성된 HTML을 return한다.&lt;/li&gt;
&lt;li&gt;완료되면 브라우저는 생성된 경로에 대한 HTML을 수신한다. 사용자 입장에서는 &quot;브라우저가 페이지를 요청 중&quot;에서 &quot;전체 페이지가 로드됨&quot;으로 전환된다. &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;로딩/폴백 상태의 플래시가 표시되지 않는다.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;동시에 Next.js는 이 경로를 미리 렌더링된 페이지 목록에 추가한다. 동일한 경로에 대한 후속 요청은 빌드 시 미리 렌더링된 다른 페이지와 마찬가지로 생성된 페이지를 제공한다.&lt;/li&gt;
&lt;li&gt;`fallback: 'blocking'`일 경우 생성된 페이지를 업데이트를 하지 않는다. 업데이트 하려면 &lt;a href=&quot;https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration&quot; target=&quot;_self&quot;&gt;&lt;span&gt;ISR&lt;/span&gt;&lt;/a&gt;도 함께 사용해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;`fallback: true`는 &lt;a href=&quot;https://nextjs.org/docs/advanced-features/static-html-export&quot; target=&quot;_self&quot;&gt;&lt;span&gt;next export&lt;/span&gt;&lt;/a&gt;를 사용할땐 동작하지 않는다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Fallback 페이지&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지의 props가 비어있다.&lt;/li&gt;
&lt;li&gt;`router`를 사용하면 fallback이 렌더링 되고있는지 감지할 수 있고, 렌더링 되는 중이라면 `router.isFallback` 값은 true를 return한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// pages/posts/[id].js
import { useRouter } from 'next/router'

function Post({ post }) {
&amp;nbsp;&amp;nbsp;const router = useRouter()

&amp;nbsp;&amp;nbsp;// 만약 page가 아직 생성되지 않았으면 getStaticProps()가 끝날때까지 
&amp;nbsp;&amp;nbsp;// `&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;`이 보여진다.
&amp;nbsp;&amp;nbsp;if (router.isFallback) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// post관련 코드...
}

// build 시점에 이 메서드가 호출된다.
export async function getStaticPaths() {
&amp;nbsp;&amp;nbsp;return {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// `/posts/1` 과 `/posts/2`에 대한 페이지만 생성된다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// fallback 값이 true이므로 정적으로 추가 페이지 생성할 수 있다.
	// 예: `/posts/3`
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fallback: true,
&amp;nbsp;&amp;nbsp;}
}

// build 시점에 이 메서드가 호출된다.
export async function getStaticProps({ params }) {
&amp;nbsp;&amp;nbsp;// params은 id값을 갖고 있다. `id`.
&amp;nbsp;&amp;nbsp;const res = await fetch(`https://.../posts/${params.id}`)
&amp;nbsp;&amp;nbsp;const post = await res.json()

&amp;nbsp;&amp;nbsp;return {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;props: { post },
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 요청이 들어올 때, 최대 10초마다 한 번 페이지를 재생성한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;revalidate: 10,
&amp;nbsp;&amp;nbsp;}
}

export default Post&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Fallback 테스트&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;※ 테스트 코드는 제 깃허브(&lt;a href=&quot;https://github.com/SooJae/nextjs-ssg-fallback&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://github.com/SooJae/nextjs-ssg-fallback&lt;/span&gt;&lt;/a&gt;)에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getStaticPaths 메서드에서 id가 1~5인 유저 리스트를 조회하여 path를 return한다.&lt;/li&gt;
&lt;li&gt;getStaticProps 메서드에서 해당 페이지에 접근할 경우 해당 id의 사용자를 조회한다. 조회 전 의도적으로 getStaticProps의 실행을 늦추기 위해 sleep 시간을 1초로 지정한다. (Fallback이 true일때와 'blocking'일때의 비교를 하기위해)&lt;/li&gt;
&lt;li&gt;http://localhost:3000/users/{id} 경로로 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이때 각 Fallback값에 따라 결과가 어떻게 바뀌는지 확인해보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;`$ yarn dev`&amp;nbsp;실행시 페이지에 매번 접근할 때마다 getStaticProps가 호출되므로 정확한 테스트를 위해, &lt;br /&gt;`$ yarn build &amp;amp;&amp;amp; yarn start`로 실행하여 배포할 때와 똑같은 환경으로 테스트 하자&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Fallback: false인 경우&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// pages/users/[id].tsx

import {GetStaticPropsContext} from &quot;next&quot;;
import axios from &quot;axios&quot;;

interface User {
    id: number,
    name: string,
    username: string,
    email: string
}

interface UserDetailPageProps {
    user: User
}

export const UserDetailPage = ({user}: UserDetailPageProps) =&amp;gt; {
    return (
        &amp;lt;div&amp;gt;
            {user.id} / {user.name} / {user.email}
        &amp;lt;/div&amp;gt;
    )
}

export const getStaticPaths = async () =&amp;gt; {

    const {data: users}: { data: User[] } = await axios.get('https://jsonplaceholder.typicode.com/users');
    // 유저 리스트에서 5명.
    const partialUserList = users.slice(0, 5);
    const paths = partialUserList.map(user =&amp;gt; ({params: {id: user.id.toString()}}));

    return {
        paths,
        fallback: false,
        // fallback: true,
        // fallback: 'blocking',
    }
}

export const getStaticProps = async (context: GetStaticPropsContext) =&amp;gt; {
    const id = context.params?.id || '';

    await sleep(1000); // 사실 fallback: false일 경우에는 영향이 없다.

    const {data: user}: { data: User } =
        await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);

    return {
        props: {
            user
        }
    }
}

const sleep = (ms: number) =&amp;gt; {
    return new Promise(resolve =&amp;gt; setTimeout(resolve, ms))
}


export default UserDetailPage;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;결과&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GA1Cm/btrZQd6XEGa/vVYNnzbKgXVvaw1nSf8gQK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GA1Cm/btrZQd6XEGa/vVYNnzbKgXVvaw1nSf8gQK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GA1Cm/btrZQd6XEGa/vVYNnzbKgXVvaw1nSf8gQK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/GA1Cm/btrZQd6XEGa/vVYNnzbKgXVvaw1nSf8gQK/img.gif&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; alt=&quot;Next.js getStaticPaths fallback:false 결과&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;514&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;존재하지 않는 path({id: 7}) 페이지에 접근하면 404 에러 페이지가 나온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Fallback: true인 경우&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// pages/users/[id].tsx

import {GetStaticPropsContext} from &quot;next&quot;;
import axios from &quot;axios&quot;;
import {useRouter} from &quot;next/router&quot;;

interface User {
    id: number,
    name: string,
    username: string,
    email: string
}

interface UserDetailPageProps {
    user: User
}

export const UserDetailPage = ({user}: UserDetailPageProps) =&amp;gt; {
    const router = useRouter();
    // fallback version
    if (router.isFallback) {
        return (
            &amp;lt;div&amp;gt;
                Loading...
            &amp;lt;/div&amp;gt;
        )
    }
    return (
        &amp;lt;div&amp;gt;
            {user.id} / {user.name} / {user.email}
        &amp;lt;/div&amp;gt;
    )
}

export const getStaticPaths = async () =&amp;gt; {

    const {data: users}: { data: User[] } = await axios.get('https://jsonplaceholder.typicode.com/users');
    // 유저 리스트에서 5명.
    const partialUserList = users.slice(0, 5);
    const paths = partialUserList.map(user =&amp;gt; ({params: {id: user.id.toString()}}));

    return {
        paths,
        // fallback: false,
        fallback: true,
        // fallback: 'blocking',
    }
}

export const getStaticProps = async (context: GetStaticPropsContext) =&amp;gt; {
    const id = context.params?.id || '';

    await sleep(1000);

    const {data: user}: { data: User } =
        await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);

    return {
        props: {
            user
        }
    }
}

const sleep = (ms: number) =&amp;gt; {
    return new Promise(resolve =&amp;gt; setTimeout(resolve, ms))
}


export default UserDetailPage;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;결과&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxSPyb/btrZ4bUsZhp/qGuMwSVwNUtkCiqK0Jein1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxSPyb/btrZ4bUsZhp/qGuMwSVwNUtkCiqK0Jein1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxSPyb/btrZ4bUsZhp/qGuMwSVwNUtkCiqK0Jein1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cxSPyb/btrZ4bUsZhp/qGuMwSVwNUtkCiqK0Jein1/img.gif&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; alt=&quot;Next.js getStaticPaths fallback:true 결과&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;514&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;존재하지 않는 `path`({id: 7}) 페이지에 접근하면 `Loading...`(sleep 1초)이 보이고 페이지가 생성된다.&lt;/li&gt;
&lt;li&gt;이때 다른 페이지({id:1})로 이동했다가 다시 {id:7} 페이지로 접근하면 로딩없이 바로 나온다. 즉 페이지가 캐싱되었다는 것을 알 수 있다.&lt;/li&gt;
&lt;li&gt;주의할 점은 `fallback:false` 때와 달리 위의 코드에서 19~27번 라인의 코드가 없으면, 아래와 같은 에러가 발생하고, 빌드에 실패한다. (&lt;span style=&quot;color: #777777;&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/messages/prerender-error&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://nextjs.org/docs/messages/prerender-error&lt;/span&gt;&lt;/a&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5RGfk/btrZSTmQSrA/8uardO41KKJ2Pyt5WiyB71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5RGfk/btrZSTmQSrA/8uardO41KKJ2Pyt5WiyB71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5RGfk/btrZSTmQSrA/8uardO41KKJ2Pyt5WiyB71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5RGfk%2FbtrZSTmQSrA%2F8uardO41KKJ2Pyt5WiyB71%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; alt=&quot;Next.js getStaticPaths fallback:true 에러&quot; loading=&quot;lazy&quot; width=&quot;871&quot; height=&quot;159&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Fallback: 'blocking'인 경우&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// pages/users/[id].tsx

import {GetStaticPropsContext} from &quot;next&quot;;
import axios from &quot;axios&quot;;
import {useRouter} from &quot;next/router&quot;;

interface User {
    id: number,
    name: string,
    username: string,
    email: string
}

interface UserDetailPageProps {
    user: User
}

export const UserDetailPage = ({user}: UserDetailPageProps) =&amp;gt; {
    // const router = useRouter();
    // // fallback version
    // if (router.isFallback) {
    //     return (
    //         &amp;lt;div&amp;gt;
    //             Loading...
    //         &amp;lt;/div&amp;gt;
    //     )
    // }
    return (
        &amp;lt;div&amp;gt;
            {user.id} / {user.name} / {user.email}
        &amp;lt;/div&amp;gt;
    )
}

export const getStaticPaths = async () =&amp;gt; {

    const {data: users}: { data: User[] } = await axios.get('https://jsonplaceholder.typicode.com/users');
    // 유저 리스트에서 5명.
    const partialUserList = users.slice(0, 5);
    const paths = partialUserList.map(user =&amp;gt; ({params: {id: user.id.toString()}}));

    return {
        paths,
        // fallback: false,
        // fallback: true,
        fallback: 'blocking',
    }
}

export const getStaticProps = async (context: GetStaticPropsContext) =&amp;gt; {
    const id = context.params?.id || '';

    await sleep(1000);

    const {data: user}: { data: User } =
        await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);

    return {
        props: {
            user
        }
    }
}

const sleep = (ms: number) =&amp;gt; {
    return new Promise(resolve =&amp;gt; setTimeout(resolve, ms))
}


export default UserDetailPage;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;결과&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csPpFF/btrZNvfVAVr/Rg5I8qK5UZhmrKLpROQyek/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csPpFF/btrZNvfVAVr/Rg5I8qK5UZhmrKLpROQyek/img.gif&quot; data-alt=&quot;탭의 로고를 보면 로딩 중인것을 확인할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csPpFF/btrZNvfVAVr/Rg5I8qK5UZhmrKLpROQyek/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/csPpFF/btrZNvfVAVr/Rg5I8qK5UZhmrKLpROQyek/img.gif&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; alt=&quot;Next.js getStaticPaths fallback:'blocking' 결과&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;514&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;탭의 로고를 보면 로딩 중인것을 확인할 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fallback: true일때와 비슷하지만, 페이지 생성 중에 fallback 버전의 페이지(`
&lt;div&gt;Loading...&lt;/div&gt;
`)를 보여주지 않는다.&lt;/li&gt;
&lt;li&gt;loading 상태를 확실히 보여주기 위해 sleep을 1초가 아닌 2초로 두었다. 즉 로딩시간 1초 이내면 사용자가 로딩 중인것도 인지하기 힘들다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Fallback 값 true vs 'blocking'&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간이 짧은 로직일 경우 - true일 경우 화면이 loading 상태 -&amp;gt; 렌더링이 완료된 페이지로 바뀌는 찰나의 flickering(혹은 flashing)때문에 UX가 오히려 안 좋아진다. 이때는 blocking을 사용하는 것이 좋아 보인다.&lt;/li&gt;
&lt;li&gt;시간이 긴 로직일 경우 - bloking일 경우 화면의 변화가 없기 때문에, 사용자 입장에서는 웹사이트에 에러가 발생한 것으로 인식될 수 있다. 그러니 오래걸리는 로직은 true로 두어 loading 상태를 보여주는 것이 좋아보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Next.js 공식 사이트 - &lt;a href=&quot;https://nextjs.org/docs/basic-features/data-fetching/get-static-props#runs-on-every-request-in-development&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://nextjs.org/docs/basic-features/data-fetching/get-static-props#runs-on-every-request-in-development&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;mskwon님 블로그 - &lt;a href=&quot;https://velog.io/@mskwon/next-js-static-generation-fallback&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://velog.io/@mskwon/next-js-static-generation-fallback&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category>Next.JS</category>
      <category>Next.js</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/65</guid>
      <comments>https://soojae.tistory.com/65#entry65comment</comments>
      <pubDate>Tue, 21 Feb 2023 20:28:21 +0900</pubDate>
    </item>
    <item>
      <title>[Next.JS] SSG - getStaticProps</title>
      <link>https://soojae.tistory.com/64</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;NextJS로고.jpeg&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t11rU/btr1ddL6MkP/AP3N2mJZWcFOkmU4kwd3N1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t11rU/btr1ddL6MkP/AP3N2mJZWcFOkmU4kwd3N1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t11rU/btr1ddL6MkP/AP3N2mJZWcFOkmU4kwd3N1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft11rU%2Fbtr1ddL6MkP%2FAP3N2mJZWcFOkmU4kwd3N1%2Fimg.jpg&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; alt=&quot;NextJS 로고&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;700&quot; data-filename=&quot;NextJS로고.jpeg&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getStaticProps&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSG(Static Site Generator)는 페이지를 호출할 때마다 서버에서 pre-render하는 SSR와 달리 &lt;b&gt;빌드&lt;/b&gt;시점에 pre-rendering을 한다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// posts는 빌드 시점에 getStaticProps()에 의해 채워진다.
function Blog({ posts }) {
  return (
    &amp;lt;ul&amp;gt;
      {posts.map((post) =&amp;gt; (
        &amp;lt;li&amp;gt;{post.title}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  )
}

// 이 함수는 Server Side에서 빌드 타임에 호출된다.
// Client Side에서는 호출되지 않으므로 직접 데이터베이스 쿼리를 수행할 수도 있다.
export async function getStaticProps() {

  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // Blog 컴포넌트는 빌드 시점에 'posts'를 프로퍼티로 받는다.
  return {
    props: {
      posts,
    },
  }
}

export default Blog&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;페이지 렌더링에 필요한 데이터는 사용자 request 전에 빌드 시점에서 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;headless CMS에서 데이터를 갖고 온다.&lt;/li&gt;
&lt;li&gt;getStaticProps는 HTML과 JSON 파일을 생성하며, 이 두 파일은 성능을 위해 CDN에 캐싱할 수 있다.&lt;/li&gt;
&lt;li&gt;데이터는 공개적으로 캐시될 수 있다(사용자별이 아님). 특정 상황에서는 미들웨어를 사용하여 경로를 다시 작성함으로써 이 조건을 우회할 수 있다.&lt;/li&gt;
&lt;li&gt;항상 서버에서 실행되며 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;클라이언트에서는 실행되지 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;`next build`가 실행될 때 동작한다.&lt;/li&gt;
&lt;li&gt;페이지에서만 사용가능하다. 즉 _app, _document, _error 파일에선 사용이 불가능하다. React는 페이지가 렌더링되기 전에 필요한 모든 데이터를 가지고 있어야 하기 때문이다.&lt;/li&gt;
&lt;li&gt;`next dev`로 실행하면, getStaticProps는 매 요청마다 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context Object&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context 파라미터의 객체에는 아래의 key값들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;params: 페이지가 dynamic route인 경우, (ex: page 이름이 [id].js라면 {id: ...} 객체 return)&lt;/li&gt;
&lt;li&gt;req - HTTP 요청 객체(서버 전용)&lt;/li&gt;
&lt;li&gt;res - HTTP 응답 객체(서버 전용)&lt;/li&gt;
&lt;li&gt;query - 객체로 파싱된 URL의 Query String (ex: /post?type=news 일 경우 {type:'news'} 객체 return)&lt;/li&gt;
&lt;li&gt;preview: true일 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://nextjs.org/docs/advanced-features/preview-mode&quot;&gt;Preview Mode&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;모드&lt;/li&gt;
&lt;li&gt;previewData: preview에서 사용되는 데이터.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;setPreviewData함수를 이용하여 설정할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;resolvedUrl: 클라이언트 전환을 위해 _next/data 접두사를 제거하고, 원래 쿼리 값을 포함하는 정규화된 요청 URL 버전&lt;br /&gt;(ex /post?type=news 일 경우 '/post?type=news' 문자열 return&lt;/li&gt;
&lt;li&gt;locale: 현재 로케일 값&lt;/li&gt;
&lt;li&gt;locales: 사용 가능한 로케일의 목록 값&lt;/li&gt;
&lt;li&gt;defaultLocale: 기본 로케일 값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Server side 코드 직접 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getStaticProps는 Server side에서만 실행되고, Client Side에서는 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저용 JS번들에도 포함되지 않으므로 API Route를 통해 데이터를 갖고오는 대신 직접 Server side 코드 (데이터베이스 쿼리등)를 직접 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// lib/load-posts.js

export async function loadPosts() {
  const res = await fetch('https://.../posts/')
  const data = await res.json()

  return data
}

// pages/blog.js
import { loadPosts } from '../lib/load-posts'

export async function getStaticProps(context) {

  const posts = await loadPosts()

  return { props: { posts } }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 경로는 CMS에서 일부 데이터를 가져오는 데 사용된다. 그 다음 해당 API 경로를 getStaticProps에서 직접 호출한다. 이렇게 하면 추가 호출이 발생하여 성능이 저하된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능이 저하되지 않는 방법으로 lib폴더를 이용하는 방법이있다. CMS에서 데이터를 가져오는 로직은 `lib/`&amp;nbsp;폴더를 사용하여 공유할 수 있다. 그리고 그 데이터를 getStaticProps와 공유할 수 있다.&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;lib 폴더&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Next.js프로젝트에서 사용되는 JavaScript 모듈을 저장하는 폴더.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lib폴더는 컴포넌트 또는 페이지에 종속되지 않는 util 함수, API 클라이언트, 데이터 로드 함수 등과 같은 코드를 보관하는 데 사용된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;util 함수 : getStaticProps에서 데이터를 가공하거나 페이지에서 사용되는 헬퍼 함수를 lib 폴더에 넣어 재사용성을 높일 수 있다.&lt;/li&gt;
&lt;li&gt;API 클라이언트: getStaticProps에서 API를 호출할 때, 일반적으로 fetch나 axios와 같은 라이브러리를 사용한다. 이러한 API 호출 코드를 lib 폴더에 넣어 재사용성을 높일 수 있다.&lt;/li&gt;
&lt;li&gt;데이터 로드 함수: getStaticProps에서 데이터를 불러올 때, 데이터베이스나 외부 API 등에서 데이터를 가져오는 로직을 lib 폴더에 넣어 재사용성을 높일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Return 값&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;revalidate&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적 페이지를 다시 생성하는 시간을 지정하는 옵션. 해당 시간이 지나면 Nextj.js는 캐시된 페이지를 버리고 새 페이지를 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;revalidate()메서드를 이용해서 on-demand 방식으로 시간을 지정할 수 있다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration&quot;&gt;Incremental Static Regeneration(ISR)&lt;/a&gt;을 활용하는 페이지의 캐시 상태는 x-nextjs-cache 응답 헤더의 값을 읽어서 확인할 수 있다. 가능한 값은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MISS - 경로가 캐시에 없다(첫 번째 방문 시 최대 한 번만 발생).&lt;/li&gt;
&lt;li&gt;STALE - 경로가 캐시에 있지만 revalidate 시간을 초과하여 백그라운드에서 업데이트 된다.&lt;/li&gt;
&lt;li&gt;HIT - 경로가 캐시에 있고 revalidate 시간을 초과하지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;notFound&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;notFound를 사용하면 페이지에서 404 상태와 404 페이지를 return할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`notFound: true`를 사용하면 이전에 성공적으로 생성된 페이지가 있더라도 404페이지를 return한다. 이는 사용자가 생성한 콘텐츠가 작성자에 의해 삭제되는 등의 경우를 위함이다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export async function getStaticProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  if (!data) {
    return {
      notFound: true,
    }
  }

  return {
    props: { data },
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;getStaticPaths의 fallback: false 모드일 땐 notFound가 필요하지 않다. &lt;br /&gt;getStaticPaths에서 pre-rendering할때 미리 path 목록을 생성하는데, 그 목록에 없는 페이지에 접근하면 404페이지를 return하기 때문이다.  &lt;a href=&quot;https://soojae.tistory.com/65#Fallback%3A%20false%EC%9D%B8%20%EA%B2%BD%EC%9A%B0-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테스트 링크&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;redirect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect는 클라이언트를 다른 페이지로 redirection한다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export async function getStaticProps(context) {
  const res = await fetch(`https://...`)
  const data = await res.json()

  if (!data) {
    return {
      redirect: {
        destination: '/',
        permanent: false,
        // statusCode: 301
      },
    }
  }

  return {
    props: { data },
  }
}&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;위 코드의 경우 data가 없으면 `/`로 redirection 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`destination` 속성을 사용하여 redirection할 URL을 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;`permanent` 속성을 사용하여 해당 redirection을 영구 redirection로 설정할 수도 있다.&lt;/li&gt;
&lt;li&gt;일반적으로 `permanent` 속성을 true로 설정하면 redirection을 클라이언트 캐시에 저장하고, 301 HTTP 상태 코드를 return한다.&lt;/li&gt;
&lt;li&gt;false로 설정하면 매번 새로운 redirection을 받고, 302 HTTP 상태 코드를 return한다.&lt;/li&gt;
&lt;li&gt;`redirect`를 사용하여 클라이언트를 다른 페이지로 redirection할 때는, 반드시 올바른 HTTP 상태 코드를 사용하여 redirection해야한다.&lt;/li&gt;
&lt;li&gt;드물지만 구형 HTTP 클라이언트가 올바르게 redirection되도록 custom statusCode를 할당해야 하는 경우가 있다. 이 경우 statusCode 속성을 사용할 수 있지만 permanent 속성과 같이 사용할 수는 없다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTML, JSON 정적 페이지 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 시점에 getStaticProps가 포함된 페이지가 pre-rendering되면 페이지의 HTML파일과 JSON이 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 JSON 파일은 `next/link` 또는 `next/router`의 Client Side 라우팅에 사용된다. getStaticProps를 사용하여 pre-rendering된 페이지로 이동하면 Next.js는 이 JSON 파일을 가져와 페이지 구성요소의 프로퍼티로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증분 정적 생성(ISR) 을 사용하는 경우, 백그라운드에서 Client Side 네비게이션에 필요한 JSON을 생성한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;process.cwd() 메서드로 파일 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getStaticProps를 이용해서 file을 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 코드를 별도의 디렉터리에 컴파일하므로 return하는 경로가 페이지 디렉터리와 다르다. 그래서 `__dirname`을 사용할 수 없다. 대신에 `process.cwd()` 메서드를 사용해서 파일을 갖고 올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { promises as fs } from 'fs'
import path from 'path'

function Blog({ posts }) {
  return (
    &amp;lt;ul&amp;gt;
      {posts.map((post) =&amp;gt; (
        &amp;lt;li&amp;gt;
          &amp;lt;h3&amp;gt;{post.filename}&amp;lt;/h3&amp;gt;
          &amp;lt;p&amp;gt;{post.content}&amp;lt;/p&amp;gt;
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  )
}

export async function getStaticProps() {
  const postsDirectory = path.join(process.cwd(), 'posts')
  const filenames = await fs.readdir(postsDirectory)

  const posts = filenames.map(async (filename) =&amp;gt; {
    const filePath = path.join(postsDirectory, filename)
    const fileContents = await fs.readFile(filePath, 'utf8')

    return {
      filename,
      content: fileContents,
    }
  })
  return {
    props: {
      posts: await Promise.all(posts),
    },
  }
}

export default Blog&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;/p&gt;
&lt;figure id=&quot;og_1677588197925&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;[Next.JS] SSG - getStaticPaths&quot; data-og-description=&quot;getStaticProps에 대해 익숙하지 않다면, 아래 글을 먼저 보는 것을 추천 드립니다. [Next.JS] SSG - getStaticProps getStaticProps SSG(Static Site Generator)는 페이지를 호출할 때마다 서버에서 pre-render하는 SSR와 달&quot; data-og-host=&quot;soojae.tistory.com&quot; data-og-source-url=&quot;https://soojae.tistory.com/65&quot; data-og-url=&quot;https://soojae.tistory.com/65&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pzrNK/hyRMx6j6m8/ZOCtkFH5W1ZXcFFaKKATYk/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264,https://scrap.kakaocdn.net/dn/cLqVGM/hyRMqe2sJu/0khNj5hQec7tp3m6jMepm1/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264,https://scrap.kakaocdn.net/dn/b9D18m/hyRMuuZhkg/diMKDfhGTPTShSANnX7Drk/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264&quot;&gt;&lt;a href=&quot;https://soojae.tistory.com/65&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://soojae.tistory.com/65&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pzrNK/hyRMx6j6m8/ZOCtkFH5W1ZXcFFaKKATYk/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264,https://scrap.kakaocdn.net/dn/cLqVGM/hyRMqe2sJu/0khNj5hQec7tp3m6jMepm1/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264,https://scrap.kakaocdn.net/dn/b9D18m/hyRMuuZhkg/diMKDfhGTPTShSANnX7Drk/img.png?width=440&amp;amp;height=264&amp;amp;face=0_0_440_264');&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;[Next.JS] SSG - getStaticPaths&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;getStaticProps에 대해 익숙하지 않다면, 아래 글을 먼저 보는 것을 추천 드립니다. [Next.JS] SSG - getStaticProps getStaticProps SSG(Static Site Generator)는 페이지를 호출할 때마다 서버에서 pre-render하는 SSR와 달&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;soojae.tistory.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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCES&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 공식 사이트 -&amp;nbsp;&lt;a href=&quot;https://nextjs.org/docs/basic-features/data-fetching/get-static-props#runs-on-every-request-in-development&quot;&gt;https://nextjs.org/docs/basic-features/data-fetching/get-static-props#runs-on-every-request-in-development&lt;/a&gt;&lt;/p&gt;</description>
      <category>Next.JS</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/64</guid>
      <comments>https://soojae.tistory.com/64#entry64comment</comments>
      <pubDate>Mon, 20 Feb 2023 19:51:11 +0900</pubDate>
    </item>
    <item>
      <title>[Next.JS] SSR - getInitialProps, getServerSideProps</title>
      <link>https://soojae.tistory.com/60</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;NextJS로고.jpeg&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1fEVb/btr1nDcdj10/VWrUnI3fbpQfAPWKYfk5Wk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1fEVb/btr1nDcdj10/VWrUnI3fbpQfAPWKYfk5Wk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1fEVb/btr1nDcdj10/VWrUnI3fbpQfAPWKYfk5Wk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1fEVb%2Fbtr1nDcdj10%2FVWrUnI3fbpQfAPWKYfk5Wk%2Fimg.jpg&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; alt=&quot;NextJS 로고&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;700&quot; data-filename=&quot;NextJS로고.jpeg&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getInitialProps, getServerSideProps, getStaticProps 전부 Server Side Rendering(SSR)을 위해 필요한 함수들이다. SSR은 서버에서 미리 데이터를 채운 페이지를 브라우저에 보여준다. &lt;span&gt;이는 특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Search_engine_optimization&quot;&gt;SEO&lt;/a&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구현에 유용하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간은 getInitalProp와 getServerSideProps에 대해 알아보자.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;각각의 렌더링 유형은 props를 페이지 컴포넌트로 전달된다. 초기 HTML가 내려왔을 때, 클라이언트 측에서 이 props 데이터를 확인 할 수 있다. 이는 페이지에서 적절한 hydrate를 위함이다. &lt;br /&gt;그러므로 클라이언트에서 사용하면 안되는 민감한 정보(secret key등)를 props로 전달하면 안 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getInitialProps&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 getInitialProps는 &lt;a href=&quot;https://nextjs.org/docs/advanced-features/automatic-static-optimization&quot;&gt;Automatic Static Optimization&lt;/a&gt;을 사용하지 못하기 때문에 권장하지 않는다. getInitialProps는 비동기로 동작하며 원하는 페이지에 &lt;a href=&quot;https://javascript.info/static-properties-methods&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;static method&lt;/a&gt; 로서 추가할 수 있다&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Page({ stars }) {
  return &amp;lt;div&amp;gt;Next stars: {stars}&amp;lt;/div&amp;gt;
}

Page.getInitialProps = async (ctx) =&amp;gt; {
  const res = await fetch('https://api.github.com/repos/vercel/next.js')
  const json = await res.json()
  return { stars: json.stargazers_count }
}

export default Page&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;getInitialProps에서 return된 데이터는 서버 렌더링 시 JSON.stringify가 수행하는 것과 유사하게 직렬화된다. getInitialProps에서 return된 객체가 Date, Map 또는 Set을 사용하지 않고 일반 Object인지 확인하자.&lt;/li&gt;
&lt;li&gt;getInitialProps는 처음에는 서버에서 호출된다. 그 후 `next/link`또는 `next/router`를 이용하여 페이지 이동할 때는 &lt;b&gt;클라이언트에서 호출된다.&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;만약 커스텀 _app에서 getInitialProps를 사용하고, 이동한 페이지에서 &lt;b&gt;getServerSideProps가 구현되었다면, getInitialProps는 서버에서 실행된다.&lt;/b&gt; &lt;br /&gt;= &amp;gt; getServerSideProps이 없으면 SSR할 필요가 없으므로 렌더링 최적화를 위해 CSR로 사용하지만, getServerSideProps이 있다면 SSR 때문에 어차피 최적화도 안될 것이다. (첫 페이지를 로딩할 때처럼 HTML과 스크립트를 다시 불러와야 하므로).&amp;nbsp;&lt;br /&gt;그래서 약간의 성능을 챙기기위해 서버에서 같이 실행되는 듯하다?(혹시 틀린 정보라면 댓글 부탁드립니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Context Object&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getInitialProps는 context 파라미터를 받는데, 이 파라미터 각체는 다음 프로퍼티를 갖고있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pathname - /pages 폴더 내에 있는 페이지의 현재 경로. (ex: /post?type=news 일 경우 /post)&lt;/li&gt;
&lt;li&gt;query - 객체로 파싱된 URL의 Query String (ex: /post?type=news 일 경우 {type:'news'})&lt;/li&gt;
&lt;li&gt;asPath - 브라우저에 표시되는 실제 경로(쿼리 포함)의 String (ex: /post?type=news 일 경우 &amp;nbsp;/post?type=news)&lt;/li&gt;
&lt;li&gt;req - HTTP 요청 객체(서버 전용)&lt;/li&gt;
&lt;li&gt;res - HTTP 응답 객체(서버 전용)&lt;/li&gt;
&lt;li&gt;err - 렌더링 중 오류가 발생한 경우 오류 객체&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getInitialProps는 자식 컴포넌트에서 사용할 수 없고, 오직 default export가 정의된 페이지에서 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;getInitialProps 내부에서 SSR을 사용한다면, 애플리케이션이 느려지지 않게 &lt;a href=&quot;https://arunoda.me/blog/ssr-and-server-only-modules&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;적절히 import&lt;/a&gt; 해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getServerSideProps&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지에서 getServerSideProps (SSR) 를 호출하면, Next.js는 각 요청에 대해 getServerSIdeProps가 return한 데이터를 사용하여 이 페이지를 pre-rendering한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676781159780&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function Page({ data }) {
  // 렌더링 데이터
}

// 매 요청마다 호출된다.
export async function getServerSideProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  // props 객체를 통해 data를 page로 전달한다.
  return { props: { data } }
}

export default Page&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;getServerSideProps는 서버 측에서만 실행되며 브라우저에서는 실행되지 않는다.&lt;/li&gt;
&lt;li&gt;페이지를 직접 요청하면 요청 시점에 getServerSideProps가 실행되고, 이 페이지가 return된 소품과 함께 미리 렌더링된다.&lt;/li&gt;
&lt;li&gt;`next/link`또는 `next/router`를 이용하여 페이지를 요청하면 Next.js가 &lt;b&gt;서버에 API를 요청하고, 서버는 getServerSIdeProps를 실행한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://next-code-elimination.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://next-code-elimination.vercel.app/&lt;/a&gt; 에 들어가보면 Next.js로 만든 코드들을 Next.js 코드를 제거한 순수 client-side 코드로 변경하는 것을 볼 수 있다.&lt;/li&gt;
&lt;li&gt;getServerSideProps는 페이지에서만 동작한다. 페이지가 아닌 파일(_app.js, _document.js, component등)에서는 동작하지 않는다.&lt;/li&gt;
&lt;li&gt;getServerSideProps내에서 에러가 발생시 기본적으로 pages/500.js 을 보여준다. dev모드에서는 500파일을 보여주진 않고, 에러화면을 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Context Object&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context 파라미터의 객체에는 아래의 key값들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;params: 페이지가 dynamic route인 경우, (ex: page 이름이 [id].js라면 {id: ...} 객체 return)&lt;/li&gt;
&lt;li&gt;req - HTTP 요청 객체(서버 전용)&lt;/li&gt;
&lt;li&gt;res - HTTP 응답 객체(서버 전용)&lt;/li&gt;
&lt;li&gt;query - 객체로 파싱된 URL의 Query String (ex: /post?type=news 일 경우 {type:'news'} 객체 return)&lt;/li&gt;
&lt;li&gt;preview: true일 경우 &lt;a href=&quot;https://nextjs.org/docs/advanced-features/preview-mode&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Preview Mode&lt;/a&gt; 모드&lt;/li&gt;
&lt;li&gt;previewData: preview에서 사용되는 데이터. &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;setPreviewData함수를 이용하여 설정할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;resolvedUrl: 클라이언트 전환을 위해 _next/data 접두사를 제거하고, 원래 쿼리 값을 포함하는 정규화된 요청 URL 버전&lt;br /&gt;(ex /post?type=news 일 경우 '/post?type=news' 문자열 return&lt;/li&gt;
&lt;li&gt;locale: 현재 로케일 값&lt;/li&gt;
&lt;li&gt;locales: 사용 가능한 로케일의 목록 값&lt;/li&gt;
&lt;li&gt;defaultLocale: 기본 로케일 값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;notFount&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`notFound: true`는 Next.js의 getServerSideProps 메서드에서 return되는 객체의 속성 중 하나로, 해당 페이지를 찾을 수 없음을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 데이터베이스에서 요청한 데이터가 존재하지 않는 경우, 혹은 유효하지 않은 URL을 요청한 경우 등이 있다. 이 경우, `notFound: true`를 return하여 클라이언트에게 해당 페이지가 존재하지 않음을 알리고, 사용자를 404 페이지로 redirection할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1676787119065&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export async function getServerSideProps(context) {
  const { params } = context;
  const post = await fetch(`https://api.example.com/posts/${params.id}`).then(res =&amp;gt; res.json());

  if (!post) {
    return {
      notFound: true
    }
  }

  return {
    props: {
      post
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시 코드에서는 getServerSideProps 메서드에서 데이터베이스에서 요청한 데이터가 존재하지 않는 경우 `notFound: true`를 return한다. 이 경우, Next.js는 클라이언트에게 해당 페이지가 존재하지 않음을 알리고, 404 페이지로 redirection한다.&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;redirect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect는 Next.js의 getServerSideProps 메서드에서 return되는 객체의 속성 중 하나로, 클라이언트를 다른 페이지로 redirection한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 사용자가 로그인하지 않은 경우 로그인 페이지로 redirection하거나, 특정 조건이 충족되지 않는 경우 다른 페이지로 redirection하는 등이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1676787172461&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export async function getServerSideProps(context) {
  const { req } = context;

  if (!req.headers.cookie) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
        // statusCode: 301
      }
    }
  }

  return {
    props: {
      // ...
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시 코드에서는 getServerSideProps 메서드에서 클라이언트의 쿠키를 검사하여 로그인하지 않은 경우 `redirect`를 return하여 로그인 페이지로 redirection한다.&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;`destination` 속성을 사용하여 redirection할 URL을 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;`permanent` 속성을 사용하여 해당 redirection을 영구 redirection로 설정할 수도 있다.&lt;/li&gt;
&lt;li&gt;일반적으로, permanent 속성을 true로 설정하면 redirection을 클라이언트 캐시에 저장하고, 301 HTTP 상태 코드를 return한다.&lt;/li&gt;
&lt;li&gt;false로 설정하면 매번 새로운 redirection을 받고, 302 HTTP 상태 코드를 return한다.&lt;/li&gt;
&lt;li&gt;`redirect`를 사용하여 클라이언트를 다른 페이지로 redirection할 때는, 반드시 올바른 HTTP 상태 코드를 사용하여 redirection해야한다.&lt;/li&gt;
&lt;li&gt;드물지만 구형 HTTP 클라이언트가 올바르게 redirection되도록 custom statusCode를 할당해야 하는 경우가 있다. 이 경우 statusCode 속성을 사용할 수 있지만 permanent 속성과 같이 사용할 수는 없다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Edge APi Routes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/api-routes/introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;API Routes란&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getServerSideProps는 서버리스 런타임과 엣지 런타임 둘다 사용할 수 있다. 그러나 현재 Edge Runtime에서는 응답 객체에 접근 할 수 없다. 접근하기 위해서는 기본 런타임인 Node.js 런타임을 사용해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676781070386&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const config = {
  runtime: 'nodejs',
}&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.JS 공식 홈페이지&amp;nbsp;- &lt;a href=&quot;https://nextjs.org/docs/api-reference/data-fetching/get-initial-props&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://nextjs.org/docs/api-reference/data-fetching/get-initial-props&lt;/a&gt;&lt;/p&gt;</description>
      <category>Next.JS</category>
      <category>Next.js</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/60</guid>
      <comments>https://soojae.tistory.com/60#entry60comment</comments>
      <pubDate>Sun, 19 Feb 2023 15:30:37 +0900</pubDate>
    </item>
    <item>
      <title>[Keycloak] keycloakify로 테마 커스텀 - 화면 작업 방식</title>
      <link>https://soojae.tistory.com/62</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Keycloak.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NyhPN/btrZ4cf5Vwr/JlLavz6BHRxtEeykplGc4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NyhPN/btrZ4cf5Vwr/JlLavz6BHRxtEeykplGc4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NyhPN/btrZ4cf5Vwr/JlLavz6BHRxtEeykplGc4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNyhPN%2FbtrZ4cf5Vwr%2FJlLavz6BHRxtEeykplGc4K%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; alt=&quot;Keycloak 로고&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;600&quot; data-filename=&quot;Keycloak.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이전 게시물과 이어집니다. 아직 keycloakify 설치, 세팅하기 전이라면 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;다음 포스팅을 확인해 주세요.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1676611484813&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;[Keycloak] keycloakify로 테마 커스텀 - 초기 설정&quot; data-og-description=&quot;버전 정보 keycloak: v20.0.3 keycloakify: v6 로그인 테마 커스텀 사실 keycloak의 테마는 이쁘지 않다... 이를 극복하기위해 직접 freeMaker(https://freemarker.apache.org/) 즉, .ftl 확장자 파일을 수정해야 한다. freeMa&quot; data-og-host=&quot;soojae.tistory.com&quot; data-og-source-url=&quot;https://soojae.tistory.com/61&quot; data-og-url=&quot;https://soojae.tistory.com/61&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/byv3YH/hyREFo5bTz/VDOPKMe527uKybz8kc8wC1/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/i1ZJS/hyREIeYpOs/GHIk4SuKGV5dZELbNFRDX0/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/bnSuSh/hyREQjMGth/2ouO9zmZaIflU3OxmhtHg1/img.png?width=900&amp;amp;height=600&amp;amp;face=0_0_900_600&quot;&gt;&lt;a href=&quot;https://soojae.tistory.com/61&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://soojae.tistory.com/61&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/byv3YH/hyREFo5bTz/VDOPKMe527uKybz8kc8wC1/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/i1ZJS/hyREIeYpOs/GHIk4SuKGV5dZELbNFRDX0/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/bnSuSh/hyREQjMGth/2ouO9zmZaIflU3OxmhtHg1/img.png?width=900&amp;amp;height=600&amp;amp;face=0_0_900_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;[Keycloak] keycloakify로 테마 커스텀 - 초기 설정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;버전 정보 keycloak: v20.0.3 keycloakify: v6 로그인 테마 커스텀 사실 keycloak의 테마는 이쁘지 않다... 이를 극복하기위해 직접 freeMaker(https://freemarker.apache.org/) 즉, .ftl 확장자 파일을 수정해야 한다. freeMa&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;soojae.tistory.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;h2 style=&quot;text-align: left;&quot; 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;keycloak: v20.0.3&lt;/li&gt;
&lt;li&gt;keycloakify: v6&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;화면 작업 방식&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;화면 작업 방식은 2가지로 보면 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;keycloak에서 기본으로 사용하는 login.ftl, register.ftl 등의 freemarker(.ftl) 파일 작업&lt;/li&gt;
&lt;li&gt;기존 keycloak에는 없는 extra 파일 작업 (ex: 로그인 MFA 화면 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 keycloak에서 기본으로 사용하는 freemarker 파일들은 어디서 확인할 수 있을까? 공식 문서에는 login.ftl, account.ftl, register.ftl 만 언급되고 나머지 ftl들은 찾을 수 없다.&lt;br /&gt;&lt;br /&gt;다행히 keycloakify가 빌드된 폴더에서 이 파일들을 찾을 수 있다.&lt;br /&gt;&lt;br /&gt;이전 시간에 작업했던 프로젝트에서 아래 경로로 들어가 보면 파일 목록들이 나온다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;keycloakify-advanced-starter/build_keycloak/src/main/resources/theme/keycloakify-advanced-starter/login&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;681&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VVTJx/btrZzKR83Am/daU08CoAkKeiBlKn2V1Aa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VVTJx/btrZzKR83Am/daU08CoAkKeiBlKn2V1Aa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VVTJx/btrZzKR83Am/daU08CoAkKeiBlKn2V1Aa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVVTJx%2FbtrZzKR83Am%2FdaU08CoAkKeiBlKn2V1Aa1%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; alt=&quot;Keycloak 빌드 ftl 파일&quot; loading=&quot;lazy&quot; width=&quot;368&quot; height=&quot;681&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;681&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;my-extra-page-1,2 는 extra 파일이므로 무시하자. email 폴더는 이메일 전송 시에 사용되는 email template들이 들어가 있다. 현재는 login 폴더 내의 파일들만 보면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;화면 작업 순서&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1. KcApp 폴더 내에 작업할 파일을 생성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;241&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjCdL6/btrZUBuc1Uu/0ukSpVPK1O4g6EkqSra6NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjCdL6/btrZUBuc1Uu/0ukSpVPK1O4g6EkqSra6NK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjCdL6/btrZUBuc1Uu/0ukSpVPK1O4g6EkqSra6NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjCdL6%2FbtrZUBuc1Uu%2F0ukSpVPK1O4g6EkqSra6NK%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; alt=&quot;Keycloak React 코드&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;241&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;241&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;2. keycloakify 양식에 맞게 이쁘게 파일 작업&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZuVcl/btr0IvTqZ2q/WqB632aVCXQcPVrBfepBr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZuVcl/btr0IvTqZ2q/WqB632aVCXQcPVrBfepBr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZuVcl/btr0IvTqZ2q/WqB632aVCXQcPVrBfepBr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZuVcl%2Fbtr0IvTqZ2q%2FWqB632aVCXQcPVrBfepBr1%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; alt=&quot;Keycloak React 코드에 keycloakify&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;215&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;3. KcContext.ts 파일 작업&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FHmNB/btrZCpeTWWb/n2fCviP41nXfqCocwGTz9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FHmNB/btrZCpeTWWb/n2fCviP41nXfqCocwGTz9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FHmNB/btrZCpeTWWb/n2fCviP41nXfqCocwGTz9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFHmNB%2FbtrZCpeTWWb%2Fn2fCviP41nXfqCocwGTz9K%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; alt=&quot;Keycloak keycloakify 테스트 설정&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;631&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;작업한 파일들을 양식에 맞게 추가해 준다. &lt;br /&gt;사실 이 부분은 keycloak에서 기본으로 제공해 주는 login.ftl, register.ftl 등의 파일들에는 작업하지 않아도 잘 동작한다.&lt;br /&gt;하지만 하는 것이 유지보수 측면에서도 좋고 속이 편해진다. mockData에 sampleData를 넣음으로써 테스트하기도 용이하다.&lt;br /&gt;&lt;br /&gt;4. KcApp.tsx 파일 작업&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b58LXE/btr0JBluuFp/kCmCks404ToChTEORJtVf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b58LXE/btr0JBluuFp/kCmCks404ToChTEORJtVf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b58LXE/btr0JBluuFp/kCmCks404ToChTEORJtVf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb58LXE%2Fbtr0JBluuFp%2FkCmCks404ToChTEORJtVf0%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; alt=&quot;Keycloak keycloakify redirect 설정&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;278&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;작업한 파일들의 경로를 추가해 준다.&lt;br /&gt;&lt;br /&gt;5. KcContext.ts 파일의 mockPageId 작업&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bejLgI/btrZBsb0IBG/gvDFrWY42l49q2ACHbJnck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bejLgI/btrZBsb0IBG/gvDFrWY42l49q2ACHbJnck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bejLgI/btrZBsb0IBG/gvDFrWY42l49q2ACHbJnck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbejLgI%2FbtrZBsb0IBG%2FgvDFrWY42l49q2ACHbJnck%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; alt=&quot;Keycloak keycloakify mockId&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;346&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;작업한 파일을 테스트하기 위해서는 mockPageId를 설정해야 한다. mockPageId는 하나밖에 설정할 수 없다. &lt;br /&gt;그래서 한 화면씩만 테스트를 할 수 있다고 생각하면 된다.&lt;br /&gt;&lt;br /&gt;6. (&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;기존 keycloak에는 없는 extra 파일 작업 경우에만&lt;/span&gt;&lt;/b&gt;) package.json 파일 작업&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eeWlC8/btrZB2KVmQe/3tsOYQ52iWUekvMtYOwI60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eeWlC8/btrZB2KVmQe/3tsOYQ52iWUekvMtYOwI60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eeWlC8/btrZB2KVmQe/3tsOYQ52iWUekvMtYOwI60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeeWlC8%2FbtrZB2KVmQe%2F3tsOYQ52iWUekvMtYOwI60%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; alt=&quot;Keycloak keycloakify package.json 작업&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;317&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;7. `$ yarn start` 실행하여 작업한 화면 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mXeOD/btrZC5glDBm/KNrfTIa7t6rxS2FUrLME9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mXeOD/btrZC5glDBm/KNrfTIa7t6rxS2FUrLME9k/img.png&quot; data-alt=&quot;성공!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mXeOD/btrZC5glDBm/KNrfTIa7t6rxS2FUrLME9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmXeOD%2FbtrZC5glDBm%2FKNrfTIa7t6rxS2FUrLME9k%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; alt=&quot;Keycloak keycloakify yarn start&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;448&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성공!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;앞으로 커스텀 화면을 작업할 때마다 위와 같이 진행하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Keycloak</category>
      <category>Auth</category>
      <category>keycloak</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/62</guid>
      <comments>https://soojae.tistory.com/62#entry62comment</comments>
      <pubDate>Fri, 17 Feb 2023 18:55:27 +0900</pubDate>
    </item>
    <item>
      <title>[Keycloak] keycloakify로 테마 커스텀 - 초기 설정</title>
      <link>https://soojae.tistory.com/61</link>
      <description>&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Keycloak.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgxTPT/btrZ77rdAud/cwamnYmQgnU86w7F6jLBj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgxTPT/btrZ77rdAud/cwamnYmQgnU86w7F6jLBj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgxTPT/btrZ77rdAud/cwamnYmQgnU86w7F6jLBj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgxTPT%2FbtrZ77rdAud%2FcwamnYmQgnU86w7F6jLBj1%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; alt=&quot;Keycloak 로고&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;600&quot; data-filename=&quot;Keycloak.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; 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;keycloak: v20.0.3&lt;/li&gt;
&lt;li&gt;keycloakify: v6&lt;/li&gt;
&lt;/ul&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;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;로그인 테마 커스텀&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사실 keycloak의 테마는 이쁘지 않다...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BbkUU/btrZqWrJs3V/KBQfC3wUmj5lKYHgiSGPoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BbkUU/btrZqWrJs3V/KBQfC3wUmj5lKYHgiSGPoK/img.png&quot; data-alt=&quot;꾸민 느낌 내려고 배경에 그라데이션 도형을 넣은게 더 열 받는다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BbkUU/btrZqWrJs3V/KBQfC3wUmj5lKYHgiSGPoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBbkUU%2FbtrZqWrJs3V%2FKBQfC3wUmj5lKYHgiSGPoK%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; alt=&quot;Keycloak 로그인 화면&quot; loading=&quot;lazy&quot; width=&quot;831&quot; height=&quot;592&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;꾸민 느낌 내려고 배경에 그라데이션 도형을 넣은게 더 열 받는다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이를 극복하기위해 직접 freeMaker.&amp;nbsp;즉, .ftl 확장자 파일을 수정해야 한다.&lt;br /&gt;freeMarker가 어떤 코드인지 확인하기 위해 &lt;a href=&quot;https://github.com/keycloak/keycloak-quickstarts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;keycloak 깃허브&lt;/a&gt;에서 .ftl 확장자를 찾아보면 아래와 같이 되어있다.&lt;br /&gt;&lt;br /&gt;짧은 코드라 읽히긴 하지만, 이 코드 양이 많아지거나 커스텀 로직을 추가해야 한다고 생각하면 쉽지 않다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;492&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIE1sj/btrZuGt7tmP/JqXeKqPbVNlZ2ehKqNW6mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIE1sj/btrZuGt7tmP/JqXeKqPbVNlZ2ehKqNW6mk/img.png&quot; data-alt=&quot;저 위의 빨간 에러는 뭔데... jsp 하던 때로 돌아간 것 같다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIE1sj/btrZuGt7tmP/JqXeKqPbVNlZ2ehKqNW6mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIE1sj%2FbtrZuGt7tmP%2FJqXeKqPbVNlZ2ehKqNW6mk%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; alt=&quot;ftl 코드&quot; loading=&quot;lazy&quot; width=&quot;492&quot; height=&quot;580&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;저 위의 빨간 에러는 뭔데... jsp 하던 때로 돌아간 것 같다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이를 위해 Joseph Garrone라는 개발자분이 React로도 개발이 가능하도록 (React로 개발 후에 빌드하면 ftl파일로 변화된다) keycloakify라는 라이브러리를 만들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Keycloakify 설치&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;keycloakify-advanced-starter(&lt;a href=&quot;https://github.com/garronej/keycloakify-advanced-starter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://github.com/garronej/keycloakify-advanced-starter&lt;/span&gt;&lt;/a&gt;) 에서 프로젝트를 clone 한 후에 Quick Start에 쓰여있는 순서에 맞춰서 작업을 진행한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ yarn
$ yarn keycloak # 빌드를 한번 한다. (몇몇 assets 들은 public/keycloak_static 폴더에 복사되며
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 이는 keycloak 외의 커스텀 페이지를 개발하기 위해 필요하다.)
$ yarn start # Hello World 페이지가 나온다.

# src/KcApp/kcContext.ts 파일의 15번째 줄의 주석을 제거하고, https://localhost:3000에 접속하면
# 이제 로그인 페이지를 개발할 수 있다.

# 테마를 완성하고 $yarn keyclock를 하여 표시되는 콘솔 문구들을 통해 테스트 하면된다.
$ yarn keycloak&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;src/KcApp 폴더에 있는 kcContext.ts 파일의 15번째 라인의 주석을 해제하자&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { getKcContext } from &quot;keycloakify/lib/getKcContext&quot;;

export const { kcContext } = getKcContext&amp;lt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// NOTE: A 'keycloakify' field must be added 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// in the package.json to generate theses pages
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// https://docs.keycloakify.dev/build-options#keycloakify.extrapages
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;| { pageId: &quot;my-extra-page-1.ftl&quot;; } 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;| { pageId: &quot;my-extra-page-2.ftl&quot;; someCustomValue: string; }
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;| { pageId: &quot;register.ftl&quot;; authorizedMailDomains: string[]; }
&amp;gt;({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Uncomment to test the login page for development.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;mockPageId&quot;: &quot;login.ftl&quot;,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // &amp;lt;=============== 이 부분의 주석을 제거한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;mockData&quot;: [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;pageId&quot;: &quot;login.ftl&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;locale&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//When we test the login page we do it in french
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;currentLanguageTag&quot;: &quot;fr&quot;, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`$ yarn start`&lt;i&gt;&amp;nbsp;&lt;/i&gt;를 실행하면 로그인 페이지가 잘 나온다. 이제 `$ yarn keycloak`&lt;i&gt;&amp;nbsp;&lt;/i&gt;을 하면 많은 콘솔이 출력되는데 하나씩 뜯어보자.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;  Building the keycloak theme...⌚
  Let keycloakify do its thang

# 생성된 테마는 도커이미지의 컨테이너의 &quot;/opt/keycloak/providers&quot; 폴더에 위치된다.
✅ Your keycloak theme has been generated and bundled into ./build_keycloak/target/keycloakify-advanced-starter-keycloak-theme-1.0.8.jar  
It is to be placed in &quot;/opt/keycloak/providers&quot; in the container running a quay.io/keycloak/keycloak Docker image.

...
# 테스트를 하기위해서 keycloak docker를 띄우자. 아래 커맨트를 이용하면 20.0.1 keycloak을 설치할 수 있다.
To test your theme locally you can spin up a Keycloak 20.0.1 container image with the theme pre loaded by running:

  $ ./build_keycloak/start_keycloak_testing_container.sh  
# 버전을 다르게 하고 싶으면 start_keycloak_testing_container.sh 파일을 수정하면 된다.
Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags

# 컨테이너가 올라가면 아래 순서대로 적용한다.
Once your container is up and running: 
- Log into the admin console   http://localhost:8080/admin username: admin, password: admin  
- Create a realm named &quot;myrealm&quot;
- Create a client with ID: &quot;myclient&quot;, &quot;Root URL&quot;: &quot;https://www.keycloak.org/app/&quot; and &quot;Valid redirect URIs&quot;: &quot;https://www.keycloak.org/app/*&quot;
- Select Login Theme: keycloakify-advanced-starter (don't forget to save at the bottom of the page)
- Go to   https://www.keycloak.org/app/   Click &quot;Save&quot; then &quot;Sign in&quot;. You should see your login page


# 이 프로세스를 시연한 데모 영상이다. 
Video demoing this process: https://youtu.be/N3wlBoH4hKg&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그럼 순서대로 작업해보자.&lt;br /&gt;우선 현재 기준 최신버전인 20.0.3으로 바꾸려고 한다.&lt;br /&gt;&lt;br /&gt;&lt;s&gt;$ vim ./build_keycloak/start_keycloak_testing_container.sh&lt;/s&gt;&amp;nbsp;를 통해 버전만 변경해 준다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;665&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqZ686/btrZtsXdK8s/kw8TZ66MISQlqxL1TzjxLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqZ686/btrZtsXdK8s/kw8TZ66MISQlqxL1TzjxLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqZ686/btrZtsXdK8s/kw8TZ66MISQlqxL1TzjxLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqZ686%2FbtrZtsXdK8s%2Fkw8TZ66MISQlqxL1TzjxLk%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; alt=&quot;keycloakify 버전 변경&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;337&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;337&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이제 `$ ./build_keycloak/start_keycloak_testing_container.sh` 를 하면 20.0.3 버전으로 keycloak-testing-container 컨테이너 이름을 가진 keycloak 도커가 실행된다.&lt;br /&gt;이제 아래 순서대로 진행하면 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;http://localhost:8080/admin에 접속&lt;/li&gt;
&lt;li&gt;아이디: admin, 비밀번호: admin로 로그인&lt;/li&gt;
&lt;li&gt;'myrealm'라는 이름의 realm 생성&lt;/li&gt;
&lt;li&gt;'myrealm'내에서 'myclient'라는 이름의 client 생성&lt;/li&gt;
&lt;li&gt;&quot;Root URL&quot;: &quot;https://www.keycloak.org/app/&quot; 로 설정 &quot;Valid redirect URIs&quot;: &quot;https://www.keycloak.org/app/*&quot; 로 설정&lt;/li&gt;
&lt;li&gt;Login Theme을 keycloakify-advanced-starter로 설정&lt;/li&gt;
&lt;li&gt;save 버튼 꼭 누르기!&lt;/li&gt;
&lt;/ol&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;674&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPKwej/btrZrxLGGZ2/s7MdRALtlFyLnYiFJpQBx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPKwej/btrZrxLGGZ2/s7MdRALtlFyLnYiFJpQBx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPKwej/btrZrxLGGZ2/s7MdRALtlFyLnYiFJpQBx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPKwej%2FbtrZrxLGGZ2%2Fs7MdRALtlFyLnYiFJpQBx1%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; alt=&quot;Keycloak Client URL 설정&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;384&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;384&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;&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;674&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qfLVM/btrZrdzYaEa/MaOlok4pMl0z6KF2BeVY4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qfLVM/btrZrdzYaEa/MaOlok4pMl0z6KF2BeVY4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qfLVM/btrZrdzYaEa/MaOlok4pMl0z6KF2BeVY4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqfLVM%2FbtrZrdzYaEa%2FMaOlok4pMl0z6KF2BeVY4k%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; alt=&quot;Keycloak Client 테마 설정&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;455&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;455&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&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;여기서 Root URL은 이후 상대 경로들을 간편하게 작성하기 위해 default로 정의한 경로다.&lt;/li&gt;
&lt;li&gt;예를 들어 위의 경우 Valid redirect URIs, Admin URL를 설정할 때 앞에 'https://www.keycloak.org/app/'을 따로 적어주지 않아도 된다.&lt;/li&gt;
&lt;li&gt;Valid redirect URIs는 로그인 성공 후에 redirect 되는 URI를 제한한다.&lt;/li&gt;
&lt;li&gt;위의 뜻을 해석하자면 https://www.keycloak.org/app/ 하위 주소(ex: https://www.keycloak.org/app/a, https://www.keycloak.org/app/b 등)가 아닌 다른 주소로 redirect 된다면 에러가 발생한다는 뜻이다.&lt;/li&gt;
&lt;li&gt;Invalid parameter: redirect_uri 에러가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위와 같이 저장 후에 &lt;a href=&quot;https://www.keycloak.org/app&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://www.keycloak.org/app&lt;/span&gt;&lt;/a&gt; 에 접속하여 그대로 'save' 버튼을 누른 후에 'Sign in' 버튼을 누르면 아래와 같이 커스텀 된(?) 화면이 나온다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;674&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S1Vdh/btrZrZH1HS7/oTDLHtdfImz2CgkCNAJ6k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S1Vdh/btrZrZH1HS7/oTDLHtdfImz2CgkCNAJ6k1/img.png&quot; data-alt=&quot;커스텀 테마 덕분에 로그인 화면이 화사해졌다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S1Vdh/btrZrZH1HS7/oTDLHtdfImz2CgkCNAJ6k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS1Vdh%2FbtrZrZH1HS7%2FoTDLHtdfImz2CgkCNAJ6k1%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; alt=&quot;Keycloak Client 로그인 테마 적용&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;485&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;커스텀 테마 덕분에 로그인 화면이 화사해졌다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아이디: admin, 비밀번호: admin로 로그인을 시도해보자. &lt;br /&gt;안된다. admin은 myRealm내의 가입된 사용자가 아니기 때문이다. &lt;br /&gt;&lt;br /&gt;아래 포스트를 따라서 myRealm에 사용자를 생성하자. (18버전이라 화면이 살짝 다르지만 그대로 따라 하면 된다.)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678766976092&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;[Keycloak] Realm, Client, User 생성 (Quarkus, 17버전 이후)&quot; data-og-description=&quot;이번 시간에는 Realm과 Client, 그리고 User에 대해 알아보겠습니다. 위 그림에서 Application은 Client라고도 불립니다. 혼동을 방지하기위해 앞으로 Client라고 부르겠습니다. Realm Realm은 사용자, 인증, 인&quot; data-og-host=&quot;soojae.tistory.com&quot; data-og-source-url=&quot;https://soojae.tistory.com/47#User-1&quot; data-og-url=&quot;https://soojae.tistory.com/47&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cbPxd4/hyRVbBQMOu/bj8GqXGZd6vwtWKfhMMRsK/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/gXE9f/hyRU6tI73t/hGBOyBpIo3xY3IIHig55f1/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/VtQEw/hyRVctWX5O/Cf0TjanIODtkcHERN8Yck0/img.png?width=2988&amp;amp;height=1202&amp;amp;face=0_0_2988_1202&quot;&gt;&lt;a href=&quot;https://soojae.tistory.com/47#User-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://soojae.tistory.com/47#User-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cbPxd4/hyRVbBQMOu/bj8GqXGZd6vwtWKfhMMRsK/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/gXE9f/hyRU6tI73t/hGBOyBpIo3xY3IIHig55f1/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/VtQEw/hyRVctWX5O/Cf0TjanIODtkcHERN8Yck0/img.png?width=2988&amp;amp;height=1202&amp;amp;face=0_0_2988_1202');&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;[Keycloak] Realm, Client, User 생성 (Quarkus, 17버전 이후)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 시간에는 Realm과 Client, 그리고 User에 대해 알아보겠습니다. 위 그림에서 Application은 Client라고도 불립니다. 혼동을 방지하기위해 앞으로 Client라고 부르겠습니다. Realm Realm은 사용자, 인증, 인&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;soojae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이제 로그인이 가능하다!&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반년 전만 해도 이런 식으로 테스트를 할 수 없었다. (keycloakify 라이브러리의 docs 페이지도 없었다.)&lt;br /&gt;지금 이렇게 실제 적용하는 것과 비슷하게 테스트할 수 있도록 만들어주신 걸 보니, 어떻게 하면 사용자들이 쉽게 이해할 수 있을지 고민한 흔적들이 많이 보인다.&lt;br /&gt;대단한 개발자라고 생각한다. 나도 열심히 실력을 키워서 이번 생에 이런 프로젝트 하나 이상은 꼭 만들어야지.&lt;br /&gt;&lt;br /&gt;기초 작업은 끝났다! 다음 포스팅부터 본격적으로 로그인, 회원가입 화면들을 만들예정이다.&lt;/p&gt;</description>
      <category>Keycloak</category>
      <category>Auth</category>
      <category>keycloak</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/61</guid>
      <comments>https://soojae.tistory.com/61#entry61comment</comments>
      <pubDate>Thu, 16 Feb 2023 05:18:08 +0900</pubDate>
    </item>
    <item>
      <title>[Next.JS] _app.js, _document.js</title>
      <link>https://soojae.tistory.com/59</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;NextJS로고.jpeg&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NE8dq/btr1znsFBne/P8N4CX8zu3VbOtnUhdWaHk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NE8dq/btr1znsFBne/P8N4CX8zu3VbOtnUhdWaHk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NE8dq/btr1znsFBne/P8N4CX8zu3VbOtnUhdWaHk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNE8dq%2Fbtr1znsFBne%2FP8N4CX8zu3VbOtnUhdWaHk%2Fimg.jpg&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; alt=&quot;NextJS 로고&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;700&quot; data-filename=&quot;NextJS로고.jpeg&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;_app.js, _document.js&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일들은 pages폴더에 위치한 Next.JS에서 제공하는 로직을 override하기 위해 필요한 파일들이다. Next.JS에서 제공해 주는 기능만 사용한다면 생성하지 않아도 된다. 하지만 실제로 개발하다 보면 100% 커스텀해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;_app.js&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;_app은 모든 페이지에 레이아웃 형태로 항상 적용되어 있다.&lt;/li&gt;
&lt;li&gt;페이지 이동 시에 상태가 변하지 않는다.&lt;/li&gt;
&lt;li&gt;필요한 초기 데이터를 getInitialProps에서 받아 컴포넌트들에 공유할 수 있다.&lt;/li&gt;
&lt;li&gt;global css를 추가하기 좋은 파일이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// _app.ts
const App = ({Component, pageProps}: AppProps) =&amp;gt; {
  return &amp;lt;Component {...pageProps} /&amp;gt;}
}

export default App;&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;위 코드에서 Component는 활성 중인 page다. 그래서 route간 이동할 때마다 Component는 항상 새 페이지로 교체된다.&lt;/li&gt;
&lt;li&gt;pageProps는 `App.getInitialProps`를 이용하여 prefetching된 데이터를 받을 수 있다. 데이터가 없다면 빈 객체({})가 내려온다.&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;_app 에서는 Next.JS의 getStaticProps, getServerSideProps와 같은 &lt;a style=&quot;color: #ee2323;&quot; href=&quot;https://nextjs.org/docs/basic-features/data-fetching/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Data Fetching methods&lt;/a&gt;가 동작하지 않는다.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;server only file이며, Next.JS Server에서 사용함으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;Client에서 사용하는 로직 (eventListener의 window/ DOM로직)을 사용하면 안 된다.&lt;/span&gt; (window is not defined 에러 발생)&lt;/li&gt;
&lt;li&gt;만약 getInitialProps를 _app에 사용하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://nextjs.org/docs/basic-features/data-fetching/get-static-props&quot;&gt;SSG&lt;/a&gt;의 효과가 없어 모든 페이지에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://nextjs.org/docs/advanced-features/automatic-static-optimization&quot;&gt;Automatic Static Optimization&lt;/a&gt;를 할 수 없다.&lt;/li&gt;
&lt;li&gt;만약 _app에서 getInitialProps를 사용해야 한다면, `next/app`에서 App객체의 InitialProps를 통해 데이터를 가져와야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3u4Os/btrZqz36woT/Wj25WW2DnSztuBAXdLkvi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3u4Os/btrZqz36woT/Wj25WW2DnSztuBAXdLkvi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3u4Os/btrZqz36woT/Wj25WW2DnSztuBAXdLkvi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3u4Os%2FbtrZqz36woT%2FWj25WW2DnSztuBAXdLkvi0%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;700&quot; height=&quot;362&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;362&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;h3 data-ke-size=&quot;size23&quot;&gt;실제 사용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getInitialProps를 이용하여 _app에서 특정 api를 호출 후, index.ts에 그 결과 값을 전달하는 로직을 만들어보자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1676448965926&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// _app.ts

interface DogProps {
    message: string,
    status: string
}

const CustomApp = ({Component, pageProps}: AppProps) =&amp;gt; {
    return &amp;lt;Component {...pageProps} /&amp;gt;
}

CustomApp.getInitialProps = async (appContext: AppContext) =&amp;gt; {
    // 여기서의 App은 next/app 내의 App
    const appProps = await App.getInitialProps(appContext);
    let response = await fetch('https://dog.ceo/api/breeds/image/random');
    let dog: DogProps = await response.json()

    return {pageProps: dog}
}

export default CustomApp;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1676449041453&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.ts

interface DogProps {
    message: string,
    status: string
}

const Home = (dog: DogProps) =&amp;gt; { // _app.ts에서 내려받은 dog값
    return (
        &amp;lt;div&amp;gt;
            {dog.status !== &quot;error&quot; &amp;amp;&amp;amp; &amp;lt;img src={dog.message} alt=&quot;강아지 사진&quot;/&amp;gt;}
            {dog.status === &quot;error&quot; &amp;amp;&amp;amp; &amp;lt;div&amp;gt;에러 발생!&amp;lt;/div&amp;gt;}
        &amp;lt;/div&amp;gt;
    )
}

export default Home;&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GCAAw/btrZrd7ke7X/TPmEKnooExDpq4WyW3CS7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GCAAw/btrZrd7ke7X/TPmEKnooExDpq4WyW3CS7k/img.png&quot; data-alt=&quot;귀여워&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GCAAw/btrZrd7ke7X/TPmEKnooExDpq4WyW3CS7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGCAAw%2FbtrZrd7ke7X%2FTPmEKnooExDpq4WyW3CS7k%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; alt=&quot;NextJS 이미지 API 결과&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;453&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;귀여워&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;h2 data-ke-size=&quot;size26&quot;&gt;_document.js&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;_document는 `html`과 `body` 태그를 커스텀하게 만들 수 있다.&lt;/li&gt;
&lt;li&gt;`,` ``, `&lt;main&gt;`&lt;/main&gt;그리고 ``는 적절한 렌더링을 위해 꼭 필요한 태그들이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Document = () =&amp;gt; {
    return (
        &amp;lt;Html lang=&quot;en&quot;&amp;gt; // html 태그 커스텀
            &amp;lt;Head/&amp;gt;
            &amp;lt;body className=&quot;bg-white&quot;&amp;gt;  // body 태그 커스텀
            &amp;lt;Main/&amp;gt;
            &amp;lt;NextScript/&amp;gt;
            &amp;lt;/body&amp;gt;
        &amp;lt;/Html&amp;gt;
    )
}

export default Document;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;_app 에서는 Next.JS의 getStaticProps, getServerSideProps와 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #ee2323;&quot; href=&quot;https://nextjs.org/docs/basic-features/data-fetching/overview&quot;&gt;Data Fetching methods&lt;/a&gt;가 동작하지 않는다.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;server only file이며, NextJS Server에서 사용함으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Client에서 사용하는 로직 (eventListener의 window/ DOM로직)을&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용하면 안 된다.&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(window is not defined 에러 발생)&lt;/li&gt;
&lt;li&gt;_document의 `` 컴포넌트는 `next/head` 파일 내의 ``컴포넌트와 다르다. `next/document` 파일에서 import 한 것이며, 이는 `` 내의 태그들은 모든 페이지에서 적용될 태그들만 사용해야 한다. 만약 `태그와 같이 페이지마다 다른 값이 보이려면 `next/head`의 ``를 사용해야 한다.&lt;/li&gt;
&lt;li&gt;`&lt;main&gt;`&lt;/main&gt;외에는 브라우저에서 실행되지 않으므로 비즈니스 로직이 있으면 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 renderPage&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 주로 CSS-in-JS (ex: emotion, styled-component)가 SSR에서 적용되도록 하기 위해서 사용한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const originalRenderPage = ctx.renderPage

    // 리액트 렌더링 로직
    ctx.renderPage = () =&amp;gt;
      originalRenderPage({
        // 애플리케이션 전체에 적용하기 위한 로직 (_app)
        enhanceApp: (App) =&amp;gt; App,
        // 각 페이지에 적용하기 위한 로직 
        enhanceComponent: (Component) =&amp;gt; Component,
      })

    // 부모 getInitialProps 실행. 위의 커스텀된 renderPage가 포함되어있다.
    const initialProps = await Document.getInitialProps(ctx)

    return initialProps
  }

  render() {
    return (
      &amp;lt;Html&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;
    )
  }
}

export default MyDocument
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.JS 공식 홈페이지 - &lt;a href=&quot;https://nextjs.org/docs/advanced-features/custom-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://nextjs.org/docs/advanced-features/custom-app&lt;/a&gt;&lt;/p&gt;</description>
      <category>Next.JS</category>
      <category>Next.js</category>
      <category>React</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/59</guid>
      <comments>https://soojae.tistory.com/59#entry59comment</comments>
      <pubDate>Wed, 15 Feb 2023 20:39:49 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 시큐리티] WebSecurityConfigurerAdapter deprecated 대응</title>
      <link>https://soojae.tistory.com/53</link>
      <description>&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-filename=&quot;다운로드.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RswLs/btrX4lNim23/srfl3HVkZEkkcSpdvHQhE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RswLs/btrX4lNim23/srfl3HVkZEkkcSpdvHQhE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RswLs/btrX4lNim23/srfl3HVkZEkkcSpdvHQhE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRswLs%2FbtrX4lNim23%2Fsrfl3HVkZEkkcSpdvHQhE1%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; alt=&quot;스프링 시큐리티 로고&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebSecurityConfigurerAdapter is deprecated&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 6.0 버전 기준으로 WebSecurityConfigurerAdapter를 완전 사용할 수 없게 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp9wvv/btrYeZPp28l/Jc6y3skWSXikYXcfILCRRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp9wvv/btrYeZPp28l/Jc6y3skWSXikYXcfILCRRK/img.png&quot; data-alt=&quot;import를 하지 못한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp9wvv/btrYeZPp28l/Jc6y3skWSXikYXcfILCRRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp9wvv%2FbtrYeZPp28l%2FJc6y3skWSXikYXcfILCRRK%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;811&quot; height=&quot;264&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;import를 하지 못한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 WebSecurityConfigurerAdapter를 상속 후에, configure 메소드를 오버라이딩 하는 방식은 deprecated 됐다. &lt;br /&gt;대신 개발자가 직접 component-based security 설정을 할 수 있도록 변경되었다. 즉 커스텀 할 설정들을 @Bean으로 등록하여 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HttpSecurity 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1675651146540&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변경된 방식 (filterChain을 사용)

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -&amp;gt; authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

}

// 기존 방식 (deprecated)

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -&amp;gt; authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
    }

}&lt;/code&gt;&lt;/pre&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;span style=&quot;background-color: #ffffff; color: #4a4a4a;&quot;&gt;WebSecurity 설정&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1675651417593&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변경된 방식(WebSecurityCustomizer 클래스 사용)

@Configuration
public class SecurityConfiguration {

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
     // antMatchers 부분도 deprecated 되어 requestMatchers로 대체
        return (web) -&amp;gt; web.ignoring().requestMatchers(&quot;/ignore1&quot;, &quot;/ignore2&quot;);
    }

}

// 기존 방식
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers(&quot;/ignore1&quot;, &quot;/ignore2&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JDBC 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1675692956783&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변경된 방식
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username(&quot;user&quot;)
            .password(&quot;password&quot;)
            .roles(&quot;USER&quot;)
            .build();
        auth.jdbcAuthentication()
            .withDefaultSchema()
            .dataSource(dataSource())
            .withUser(user);
    }
}


// 기존 방식
@Configuration
public class SecurityConfiguration {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
            .build();
    }

    @Bean
    public UserDetailsManager users(DataSource dataSource) {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username(&quot;user&quot;)
            .password(&quot;password&quot;)
            .roles(&quot;USER&quot;)
            .build();
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
        users.createUser(user);
        return users;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에 &lt;a href=&quot;https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에서 Ldap이나 In-memory 그리고&amp;nbsp; AuthenticationManager등의 변경된 설정법을 확인할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security without the WebSecurityConfigurerAdapter -&amp;nbsp;&lt;a href=&quot;https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security: Upgrading the Deprecated WebSecurityConfigurerAdapter - &lt;a href=&quot;https://www.baeldung.com/spring-deprecated-websecurityconfigureradapter&quot;&gt;https://www.baeldung.com/spring-deprecated-websecurityconfigureradapter&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Auth</category>
      <category>springboot</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/53</guid>
      <comments>https://soojae.tistory.com/53#entry53comment</comments>
      <pubDate>Mon, 6 Feb 2023 23:21:10 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 시큐리티] AuthenticationManager, AuthenticationProvider</title>
      <link>https://soojae.tistory.com/55</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zj5Fb/btrYjeF6odz/L8cRJ0V2GLNK42jLDaSTF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zj5Fb/btrYjeF6odz/L8cRJ0V2GLNK42jLDaSTF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zj5Fb/btrYjeF6odz/L8cRJ0V2GLNK42jLDaSTF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZj5Fb%2FbtrYjeF6odz%2FL8cRJ0V2GLNK42jLDaSTF1%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;800&quot; height=&quot;450&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이 글은 정수원님의 &lt;a href=&quot;https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&lt;/span&gt;&lt;/a&gt; 강의를 수강하면서 학습한 내용을 정리한 글입니다. 일부 강의 내용을 인용하였으며, 문제가 될 시 인용 부분을 수정 또는 삭제하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;AuthenticationManager&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;인증 처리하는 filter로부터 인증처리를 지시받는 첫번째 클래스.&lt;br /&gt;ID와 Password를 Authentication 인증 객체에 저장하고 이 객체를 AuthenticationManager에게 전달한다. 이 인증에 대해 관리를 하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBHcYJ/btrYiUOAhNq/EzSlHW02hT8KjBwXCdoNp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBHcYJ/btrYiUOAhNq/EzSlHW02hT8KjBwXCdoNp1/img.png&quot; data-alt=&quot;출처 : 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBHcYJ/btrYiUOAhNq/EzSlHW02hT8KjBwXCdoNp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBHcYJ%2FbtrYiUOAhNq%2FEzSlHW02hT8KjBwXCdoNp1%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;738&quot; height=&quot;239&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;AuthenticationManager 인터페이스를 실제로 구현한 것이 ProviderManager. 이 AuthenticationManager 인터페이스를 구현하면 커스텀 ProviderManager도 만들 수 있다.&lt;br /&gt;AuthenticationProvider 목록 중에서 인증 처리 요건에 맞는 AuthenticationProvider를 찾아 인증 처리를 위임한다. 그래서 실제로 ProviderManager가 ID와 Password를 검증하지는 않는다. 단지 각 인증에 맞는 Provider를 찾아 인증처리를 맡긴다.&lt;br /&gt;만약 ProviderManager에 해당 조건에 맞는 Provider가 없다면, 부모 ProviderManager에서 Provider를 탐색하여 찾는다면 부모 ProviderManager에게 인증을 맡긴다. (예: 위의 사진에서 Oauth 인증일 경우) &lt;br /&gt;인증이 완료되면 자신을 호출한 Filter에게 결과 값을 넘겨준다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;AuthenticationProvider&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;ID와 Password가 검증하는 실질적인 클래스다. 위의 ProviderManager가 적절한 Provider 선택후 아래와 같은 로직이 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKc0kM/btrYdjhf81c/rkhsnKSZ66evuetafvOta0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKc0kM/btrYdjhf81c/rkhsnKSZ66evuetafvOta0/img.png&quot; data-alt=&quot;출처 : 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKc0kM/btrYdjhf81c/rkhsnKSZ66evuetafvOta0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKc0kM%2FbtrYdjhf81c%2FrkhsnKSZ66evuetafvOta0%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;738&quot; height=&quot;298&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;AuthenticationProvider는 인터페이스다. 이 인터페이스를 구현하면 커스텀 AuthenticationProvider도 만들 수 있다.&lt;br /&gt;`authenticate` 메서드는 &lt;b&gt;실질적인 인증처리&lt;/b&gt;를 하고, `support` 메소드는 &lt;b&gt;인증 처리의 기준&lt;/b&gt;을 정한다.&lt;br /&gt;authenticate 동작 순서 살펴보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ID검증: UserDetailsService에서 DB에서 사용자를 조회. 사용자가 없을 경우 UserNotFoundException 예외를 발생시킨다. 성공하면 UserDetails 객체를 반환한다.&lt;/li&gt;
&lt;li&gt;Password 검증: 반환 받은 UserDetails 객체에 저장된 Password와 Authentication에 저장된 Password(로그인시 입력한 Password)를 `matches` 메서드를 이용하여 비교. 일치하지 않는다면 BadCredentialException 예외를 발생시킨다.&lt;/li&gt;
&lt;li&gt;추가 검증 까지 완료하면 Authentication(유저 정보, 권한 정보)를 AuthenticationManager에게 전달. AuthenticationManager는 Filter에게 전달하고, Filter는 이 정보를 전역적으로 사용할 수 있게 SecurityContext에 전달한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - &lt;a href=&quot;https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Auth</category>
      <category>springboot</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/55</guid>
      <comments>https://soojae.tistory.com/55#entry55comment</comments>
      <pubDate>Mon, 6 Feb 2023 20:36:22 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 시큐리티] 필터(DelegatingFilterProxy, FilterChainProxy)</title>
      <link>https://soojae.tistory.com/54</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEXfb0/btrX7YYLmkk/MbWTRtb6ZU1MxI1v6JcoGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEXfb0/btrX7YYLmkk/MbWTRtb6ZU1MxI1v6JcoGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEXfb0/btrX7YYLmkk/MbWTRtb6ZU1MxI1v6JcoGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEXfb0%2FbtrX7YYLmkk%2FMbWTRtb6ZU1MxI1v6JcoGk%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; alt=&quot;스프링 시큐리티 로고&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 이 글은 정수원님의 &lt;a href=&quot;https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&lt;/a&gt; 강의를 수강하면서 학습한 내용을 정리한 글입니다. 일부 강의 내용을 인용하였으며, 문제가 될 시 인용 부분을 수정 또는 삭제하겠습니다.&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;필터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS에서 실행된 요청이 오면 이 요청이 서블릿으로 들어오는데, 서블릿에 들어오기 전에 처리를 하는 것이 필터.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필터의 흐름&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HTTP 요청 -&amp;gt; WAS -&amp;gt; 필터 -&amp;gt; 서블릿 -&amp;gt; 컨트롤러&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필터 체인&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HTTP 요청 -&amp;gt; WAS -&amp;gt; 필터 1 -&amp;gt; 필터 2 -&amp;gt; 필터 3 -&amp;gt; 서블릿 -&amp;gt; 컨트롤러&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DelegatingFilterProxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 필터는 Servlet 스펙에 있는 기술이기 때문에 Servlet 컨테이너에서만 생성되고 실행된다. Spring의 Ioc 컨테이너와는 컨테이너가 다르기 때문에 Spring Bean으로 Injection하거나 Spring에서 사용되는 기술을 Servlet에서 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 가능하게 해주는 것이 DelegatingFilterProxy 클래스다.&amp;nbsp;&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;732&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8IF5/btrX8i3zRu0/VxMK6V3qcgyZhgKK3PVn4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8IF5/btrX8i3zRu0/VxMK6V3qcgyZhgKK3PVn4K/img.png&quot; data-alt=&quot;출처 : 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8IF5/btrX8i3zRu0/VxMK6V3qcgyZhgKK3PVn4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8IF5%2FbtrX8i3zRu0%2FVxMK6V3qcgyZhgKK3PVn4K%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; alt=&quot;DelegatingFilterProxy&quot; loading=&quot;lazy&quot; width=&quot;732&quot; height=&quot;162&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DelegatingFilterProxy는 springSecurityFilterChain 이름으로 생성된 빈을 ApplicationContext에서 찾아 요청을 위임한다. 실제로 보안 처리는 하지 않고 위임만 하는 Servlet Filter다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DelegatingFilterProxy는 이 Servlet 컨테이너와 Spring의 Ioc 컨테이너를 연결해주는 필터라고 생각하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FilterChainProxy&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v1PEU/btrYfZClYVC/z9oU8tYoDmH3HbzgaQQ6J0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v1PEU/btrYfZClYVC/z9oU8tYoDmH3HbzgaQQ6J0/img.png&quot; data-alt=&quot;출처 : 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v1PEU/btrYfZClYVC/z9oU8tYoDmH3HbzgaQQ6J0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv1PEU%2FbtrYfZClYVC%2Fz9oU8tYoDmH3HbzgaQQ6J0%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; alt=&quot;FilterChainProxy&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;284&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&lt;/figcaption&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNX4i/btrYg9LGaqW/Iyb5LeygqRpBGi1RukX1i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNX4i/btrYg9LGaqW/Iyb5LeygqRpBGi1RukX1i1/img.png&quot; data-alt=&quot;출처 : 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNX4i/btrYg9LGaqW/Iyb5LeygqRpBGi1RukX1i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNX4i%2FbtrYg9LGaqW%2FIyb5LeygqRpBGi1RukX1i1%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; alt=&quot;Servlet Container - Springboot 동작방식&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;281&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP요청이 들어오면 Servlet 컨테이너에서 요청을 받는다. &lt;br /&gt;Servlet&amp;nbsp;컨테이너 내의 필터들이 동작하는 중간에 DelegatingFilterProxy Filter가 요청을 받으면 &lt;span&gt;springSecurityFilterChain 이름으로 생성된 빈을 AnnotationConfigServletWebServerApplicationContext 객체에서 찾는다. &lt;br /&gt;이 찾은 Filter Bean이 바로 FilterChainProxy다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;br /&gt;그 후에 FilterChainProxy Bean에 요청을 전달한다. 이제 FilterChainProxy에서 필터들을 이용하여 보안처리를 진행한 후 최종적으로 SpringMVC의 DeispatcherServlet에 전달하여 요청에 대한 Servlet 처리를 하게 된다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - &lt;a href=&quot;https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/dashboard&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Overview and Need for DelegatingFilterProxy in Spring - &lt;a href=&quot;https://www.baeldung.com/spring-delegating-filter-proxy&quot;&gt;https://www.baeldung.com/spring-delegating-filter-proxy&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;스프링 공식 문서 -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-filters-review&quot;&gt;https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-filters-review&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Auth</category>
      <category>springboot</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/54</guid>
      <comments>https://soojae.tistory.com/54#entry54comment</comments>
      <pubDate>Mon, 6 Feb 2023 19:12:59 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 시큐리티] HttpSecurity vs WebSecurity</title>
      <link>https://soojae.tistory.com/52</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_security-logo.jpg&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tJuSl/btrX8hWXpxG/2m4uqQ8nhP9fdYqVh122J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tJuSl/btrX8hWXpxG/2m4uqQ8nhP9fdYqVh122J1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tJuSl/btrX8hWXpxG/2m4uqQ8nhP9fdYqVh122J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtJuSl%2FbtrX8hWXpxG%2F2m4uqQ8nhP9fdYqVh122J1%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; alt=&quot;스프링 시큐리티 로고&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-filename=&quot;edited_security-logo.jpg&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HttpSecurity vs WebSecurity&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBuilder는 웹 보안을 구성하는 빌더 클래스로서 웹 보안을 구성하는 빈 객체와 설정 클래스들을 생성하는 역할을 한다.&lt;br /&gt;이 빌더의 종류로는 HttpSecurity와 WebSecurity가 있다. 오늘은 이 둘에 대해서 알아보려고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1675569291489&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class CustomWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
        .antMatchers(&quot;/health&quot;, &quot;/health/**&quot;)
        .antMatchers(&quot;/publics/**&quot;);
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
         http.csrf().disable()
            .authorizeRequests()
            .antMatchers(&quot;/health&quot;, &quot;/health/**&quot;).hasRole('USER') // 무시된다.
            .anyRequest().authenticated();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSecurity는 HttpSecurity의 상위에 있다. WebSecurity의 `ignoring`에 endpoint를 만들면, Security Filter Chain이 적용되지 않는다.&lt;br /&gt;이 경우 Cross-Site Scripting, XSS 공격, content-sniffing에 취약해진다.&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;HttpSecurity의 `permitAll`에 endpoint를 지정하면 인증처리의 결과를 무시하지만 Security Filter Chain이 적용되어 Cross-Site Scripting, XSS 공격, content-sniffing에 대한 검사를 할 수 있다.&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;위의 예제의 경우 WebSecurity에 `ignoring`에 설정된 `/publics/**` 엔드포인트에 대하여 HttpSecurity 부분은 동작하지 않는다.&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;실무에서 HttpSecurity의 `permitAll`이 설정된 endpoint에 잘못된 BearerToken 값이 들어오면 Security Filter Chain를 거치면서 에러를 반환하지만, WebSecurity는 `ignoring`이 설정된 잘못된 BearerToken 값이 들어와도 통과된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 WebSecurity는 보안과 전혀 상관없는 로그인 페이지, 공개 페이지(어플리캐이션 소개 페이지 등), 어플리캐이션의 health 체크를 하기위한 API에 사용하고, 그 이외에는 HttpSecurity를 사용하는 것이 좋다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p id=&quot;9b93&quot; data-ke-size=&quot;size16&quot;&gt;SpringBoot : Security Configuration using HTTPSecurity vs WebSecurity -&amp;nbsp;&lt;a href=&quot;https://ravthiru.medium.com/springboot-security-configuration-using-httpsecurity-vs-websecurity-1a7ec6a23273&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ravthiru.medium.com/springboot-security-configuration-using-httpsecurity-vs-websecurity-1a7ec6a23273&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Auth</category>
      <category>springboot</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/52</guid>
      <comments>https://soojae.tistory.com/52#entry52comment</comments>
      <pubDate>Sun, 5 Feb 2023 12:53:20 +0900</pubDate>
    </item>
    <item>
      <title>[Keycloak] 외부 DB(Mysql)로 변경 (Quarkus, 17버전 이후)</title>
      <link>https://soojae.tistory.com/48</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yPgyC/btrZzoIyFMC/fUy403xuU4UbEKB1jgxmnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yPgyC/btrZzoIyFMC/fUy403xuU4UbEKB1jgxmnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yPgyC/btrZzoIyFMC/fUy403xuU4UbEKB1jgxmnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyPgyC%2FbtrZzoIyFMC%2FfUy403xuU4UbEKB1jgxmnk%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; alt=&quot;Keycloak 로고&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;600&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제껏 DB를 따로 생성하지 않아도 Realm, Client, User를 생성하고 저장할 수 있었던 이유는, Keycloak내의 내부 DB(h2)를 사용하고 있었기 때문입니다. &lt;br /&gt;하지만 보통 운영 단계에서는 외부 DB를 사용하는 경우가 많으므로 Keycloak 내부 DB에서 외부 DB로 변경해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 DB는 Mysql입니다. 저는 Docker Compose로 설치해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mysql 5버전 설치&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# docker-compose.yml

version: &quot;3.8&quot;
services:
  mysql_5:
    image: mysql:5.7.38
    container_name: mysql_5
    environment:
      - MYSQL_DATABASE=keycloak5
      - MYSQL_USER=jerry
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=root_password
    ports:
        - &quot;3305:3306&quot; # 포트는 3305번으로 생성하겠습니다.
    healthcheck:
      test: &quot;mysqladmin ping -u root -p$${MYSQL_ROOT_PASSWORD}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mysql 8버전 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1656469990215&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker-compose.yml

version: &quot;3.8&quot;
services:
  mysql_8:
    image: mysql:8.0.29
    container_name: mysql_8
    environment:
      - MYSQL_DATABASE=keycloak8
      - MYSQL_USER=jerry
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=root_password
    ports:
      - &quot;3308:3306&quot; # 포트는 3308번으로 생성하겠습니다.
    healthcheck:
      test: &quot;mysqladmin ping -u root -p$${MYSQL_ROOT_PASSWORD}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다음 커맨드를 입력하여 실행해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1656470451308&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker-compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Realm 백업&lt;/h3&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-filename=&quot;edited_blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dioJjT/btrZuGnkqlH/pKjNzoBxvDY8DfnTCaZwPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dioJjT/btrZuGnkqlH/pKjNzoBxvDY8DfnTCaZwPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dioJjT/btrZuGnkqlH/pKjNzoBxvDY8DfnTCaZwPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdioJjT%2FbtrZuGnkqlH%2FpKjNzoBxvDY8DfnTCaZwPK%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; alt=&quot;Keycloak Realm 백업&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;204&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Export 메뉴로 들어가 Export를 하면&amp;nbsp; SampleRealm의 백업파일인 realm-export.json 파일이 다운로드 됩니다.&amp;nbsp;&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;외부 DB 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;17버전을 기준으로 변경되었습니다. 복잡했던 16버전에 비해, 17 이후 버전은 설정이 간편해졌습니다.&amp;nbsp;&lt;br /&gt;keycloak이 설치된 폴더안에 conf 폴더 내 keycloak.conf 파일을 수정하겠습니다.&lt;br /&gt;다음 커맨드로 keycloak.conf파일에 접근합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1656477287973&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vi ./conf/keycloak.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;keycloak.conf 파일을 아래와 같이 설정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1656477721729&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Basic settings for running in production. Change accordingly before deploying the server.

# Port
# 기존 8080이었던 keycloak 포트를 9090으로 변경
http-port=9090 

# Database

# The database vendor.
db=mysql

# The username of the database user.
db-username=jerry

# The password of the database user.
db-password=password

# The full database JDBC URL. If not provided, a default URL is set based on the selected database vendor.
db-url=jdbc:mysql://localhost:3308/keycloak8&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 keycloak을 재 실행 시켜줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1656478338721&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sh ./bin/kc.sh start-dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 아래와 같이 keycloak8 데이터베이스에 테이블이 92개가 생성된 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;1236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5Ae9x/btrF0jezK0b/jGHYVu3CXaY3CjEX1d8A5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5Ae9x/btrF0jezK0b/jGHYVu3CXaY3CjEX1d8A5k/img.png&quot; data-alt=&quot;92개의 테이블이 생성됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5Ae9x/btrF0jezK0b/jGHYVu3CXaY3CjEX1d8A5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5Ae9x%2FbtrF0jezK0b%2FjGHYVu3CXaY3CjEX1d8A5k%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; alt=&quot;Keycloak 테이블 생성&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;462&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;1236&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;92개의 테이블이 생성됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따로 테이블 생성이 필요 없는 이유는 keycloak이 실행되면서 &lt;a href=&quot;https://www.liquibase.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;liquibase&lt;/a&gt;가 실행되기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1656478652954&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Keycloak 실행시 아래와 같은 로그가 발생하는데, jpa-changelog-master.xml 정보로 데이터베이스를 업데이트 한다는 것을 확인 할 수 있습니다.
2022-06-29 13:46:21,750 INFO  [org.keycloak.quarkus.runtime.storage.database.liquibase.QuarkusJpaUpdaterProvider] (main) Initializing database schema. Using changelog META-INF/jpa-changelog-master.xml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 http://localhost:9090 에 접속해 보면 아래와 같은 화면이 나옵니다. admin 계정을 생성해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE59z7/btrZuIrT63z/OvuyVQRXyKmDwbLUCWN3B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE59z7/btrZuIrT63z/OvuyVQRXyKmDwbLUCWN3B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE59z7/btrZuIrT63z/OvuyVQRXyKmDwbLUCWN3B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE59z7%2FbtrZuIrT63z%2FOvuyVQRXyKmDwbLUCWN3B1%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; alt=&quot;Keycloak 어드민 로그인&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;287&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Export 했던 Realm 설정을 Import 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pIihi/btrZrYPPJQj/rpRvXVTPCZP8XKSKwzgnb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pIihi/btrZrYPPJQj/rpRvXVTPCZP8XKSKwzgnb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pIihi/btrZrYPPJQj/rpRvXVTPCZP8XKSKwzgnb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpIihi%2FbtrZrYPPJQj%2FrpRvXVTPCZP8XKSKwzgnb0%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; alt=&quot;Keycloak Realm 생성&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;287&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Import 메뉴에서 Import를 하면 에러가 발생합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그래서 위와 같이 Add realm 버튼을 누른 후 Select file을 클릭하여 백업해 두었던 realm-export.json파일을 선택합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cargnJ/btrZqV7pNhJ/1tlnOAIM5RDPC0ISqh2Cy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cargnJ/btrZqV7pNhJ/1tlnOAIM5RDPC0ISqh2Cy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cargnJ/btrZqV7pNhJ/1tlnOAIM5RDPC0ISqh2Cy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcargnJ%2FbtrZqV7pNhJ%2F1tlnOAIM5RDPC0ISqh2Cy1%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; alt=&quot;Keycloak Realm Export&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;287&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 Realm이 생성되고, 외부 DB에서도 Realm이 생성된 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;39&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co6EXM/btrZuCSLYx1/BTldkWPnrjyCmSCMRIxd30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co6EXM/btrZuCSLYx1/BTldkWPnrjyCmSCMRIxd30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co6EXM/btrZuCSLYx1/BTldkWPnrjyCmSCMRIxd30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco6EXM%2FbtrZuCSLYx1%2FBTldkWPnrjyCmSCMRIxd30%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; alt=&quot;Keycloak Realm DB&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;39&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;39&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수고하셨습니다.&lt;/p&gt;</description>
      <category>Keycloak</category>
      <category>Auth</category>
      <category>keycloak</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/48</guid>
      <comments>https://soojae.tistory.com/48#entry48comment</comments>
      <pubDate>Wed, 29 Jun 2022 11:38:39 +0900</pubDate>
    </item>
    <item>
      <title>[Keycloak] Realm, Client, User 생성 (Quarkus, 17버전 이후)</title>
      <link>https://soojae.tistory.com/47</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eitgVM/btrZD1EQAl5/ZutAO2SAAyH8o7vsqV4Wwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eitgVM/btrZD1EQAl5/ZutAO2SAAyH8o7vsqV4Wwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eitgVM/btrZD1EQAl5/ZutAO2SAAyH8o7vsqV4Wwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeitgVM%2FbtrZD1EQAl5%2FZutAO2SAAyH8o7vsqV4Wwk%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; alt=&quot;Keycloak 로고&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;600&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간에는 Realm과 Client, 그리고 User에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_master_realm.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n6O7t/btrZA1S7BQs/Grqj74xurQ9Igu8Czf2zV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n6O7t/btrZA1S7BQs/Grqj74xurQ9Igu8Czf2zV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n6O7t/btrZA1S7BQs/Grqj74xurQ9Igu8Czf2zV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn6O7t%2FbtrZA1S7BQs%2FGrqj74xurQ9Igu8Czf2zV0%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; alt=&quot;Keycloak Realm 설명&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;497&quot; data-filename=&quot;edited_master_realm.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 Application은 Client라고도 불립니다. 혼동을 방지하기위해 앞으로 Client라고 부르겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Realm&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Realm은 사용자, 인증, 인가, 권한, 그룹이 관리하는 범위입니다. &lt;br /&gt;한 Realm내의 Client들(위 그림에서는 Application입니다.)은 서로 SSO를 공유합니다. 각각의 Realm은 독립적입니다.&amp;nbsp;&lt;br /&gt;처음 Keycloak에 접속하면 Master Realm이 있는데, 이 Realm은 다른 Realm을 관리하는데만 사용합니다. (단지 관리를 하기위한 Realm이고, 다른 Realm이 Master Realm을 상속 받는 것은 아닙니다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Client&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client란 인증, 인가 업무를 Keycloak에게 요청할 수 있는 주로 어플리케이션이나 서비스를 뜻합니다. Keycloak은 Client들에게 보안이나, SSO를 제공해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;User&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User란 Client를 이용하는 사용자를 뜻합니다. 사용자는 Realm단위로 되어있습니다. A라는 Realm에 가입된 X라는 사용자는 Client들에서 X의 아이디 하나로 로그인이 가능하지만, B라는 Realm에는 가입이 되어있지 않으므로 로그인이 불가능합니다.&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;Realm과 Client를 아래 예시로 설명해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxiyLn/btrF0Z0vx1b/lLnisV5Wx2xIvdNqNW3yjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxiyLn/btrF0Z0vx1b/lLnisV5Wx2xIvdNqNW3yjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxiyLn/btrF0Z0vx1b/lLnisV5Wx2xIvdNqNW3yjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxiyLn%2FbtrF0Z0vx1b%2FlLnisV5Wx2xIvdNqNW3yjK%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; alt=&quot;Keycloak 카카오톡 화면&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;316&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림에서 카카오톡의 고유기능인 캘린더, 서랍, 공지사항을 제외한 각각의 Service(Client)는 같은 Realm을 사용하는 Client라고 볼 수 있습니다. 따로 가입하는 절차 없이 카카오톡 아이디로 바로 이용할 수 있기 때문입니다.&lt;br /&gt;반면에 카카오 뱅크, 카카오 페이, 티스토리 등을 사용시에는 계정을 새로 가입해야하기 때문에, 같은 Realm이라고 볼 수 없습니다.&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;Realm과 Client 생성&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3082&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXi1dW/btrFZ0e3zfQ/1iO1oUOyvPvHkESBaKJktk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXi1dW/btrFZ0e3zfQ/1iO1oUOyvPvHkESBaKJktk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXi1dW/btrFZ0e3zfQ/1iO1oUOyvPvHkESBaKJktk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXi1dW%2FbtrFZ0e3zfQ%2F1iO1oUOyvPvHkESBaKJktk%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; alt=&quot;Keycloak Realm&amp;amp;#44; Client 생성&quot; loading=&quot;lazy&quot; width=&quot;3082&quot; height=&quot;1216&quot; data-origin-width=&quot;3082&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 사이드 바에서 Add realm 버튼을 눌러 Realm을 생성해줍시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3800&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tF4QJ/btrFZFvsvFF/XcBZPPkf2SfQifPK6RPQJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tF4QJ/btrFZFvsvFF/XcBZPPkf2SfQifPK6RPQJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tF4QJ/btrFZFvsvFF/XcBZPPkf2SfQifPK6RPQJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtF4QJ%2FbtrFZFvsvFF%2FXcBZPPkf2SfQifPK6RPQJ0%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; alt=&quot;Keycloak Realm 생성&quot; loading=&quot;lazy&quot; width=&quot;3800&quot; height=&quot;1192&quot; data-origin-width=&quot;3800&quot; data-origin-height=&quot;1192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Realm 생성이 완료되면, Clients 메뉴로 들어간 후&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Create해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2990&quot; data-origin-height=&quot;1150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9MAMv/btrF0ZUcvzJ/2zrXBkcW5Sk99gUnX9DLhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9MAMv/btrF0ZUcvzJ/2zrXBkcW5Sk99gUnX9DLhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9MAMv/btrF0ZUcvzJ/2zrXBkcW5Sk99gUnX9DLhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9MAMv%2FbtrF0ZUcvzJ%2F2zrXBkcW5Sk99gUnX9DLhK%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; alt=&quot;Keycloak Client 생성&quot; loading=&quot;lazy&quot; width=&quot;2990&quot; height=&quot;1150&quot; data-origin-width=&quot;2990&quot; data-origin-height=&quot;1150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Users 메뉴로 들어가 유저를 생성해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2988&quot; data-origin-height=&quot;1202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buW8ZK/btrF1RBjLl8/5sjpKn30faTcbB1zHsMDA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buW8ZK/btrF1RBjLl8/5sjpKn30faTcbB1zHsMDA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buW8ZK/btrF1RBjLl8/5sjpKn30faTcbB1zHsMDA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuW8ZK%2FbtrF1RBjLl8%2F5sjpKn30faTcbB1zHsMDA1%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; alt=&quot;Keycloak 사용자 생성&quot; loading=&quot;lazy&quot; width=&quot;2988&quot; height=&quot;1202&quot; data-origin-width=&quot;2988&quot; data-origin-height=&quot;1202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Credentials 탭으로 들어가 비밀번호를 생성해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2988&quot; data-origin-height=&quot;1202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8vrYl/btrFWcgawOn/CowrHHJVtxVeuSFnXT0Dfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8vrYl/btrFWcgawOn/CowrHHJVtxVeuSFnXT0Dfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8vrYl/btrFWcgawOn/CowrHHJVtxVeuSFnXT0Dfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8vrYl%2FbtrFWcgawOn%2FCowrHHJVtxVeuSFnXT0Dfk%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; alt=&quot;Keycloak Credentials 설정&quot; loading=&quot;lazy&quot; width=&quot;2988&quot; height=&quot;1202&quot; data-origin-width=&quot;2988&quot; data-origin-height=&quot;1202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clients 메뉴로 들어와 account-console 링크를 눌러줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2988&quot; data-origin-height=&quot;1202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sL5xY/btrFZpy5XdK/2FKxoRA0mPBoLXTqztVPp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sL5xY/btrFZpy5XdK/2FKxoRA0mPBoLXTqztVPp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sL5xY/btrFZpy5XdK/2FKxoRA0mPBoLXTqztVPp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsL5xY%2FbtrFZpy5XdK%2F2FKxoRA0mPBoLXTqztVPp1%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; alt=&quot;Keycloak Clients 생성 완료&quot; loading=&quot;lazy&quot; width=&quot;2988&quot; height=&quot;1202&quot; data-origin-width=&quot;2988&quot; data-origin-height=&quot;1202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sign in 버튼을 눌러줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3772&quot; data-origin-height=&quot;1172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC10KU/btrF3g8Nr2E/UN84NgUswPoctl308XOdX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC10KU/btrF3g8Nr2E/UN84NgUswPoctl308XOdX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC10KU/btrF3g8Nr2E/UN84NgUswPoctl308XOdX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC10KU%2FbtrF3g8Nr2E%2FUN84NgUswPoctl308XOdX0%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; alt=&quot;Keycloak 사용자 로그인&quot; loading=&quot;lazy&quot; width=&quot;3772&quot; height=&quot;1172&quot; data-origin-width=&quot;3772&quot; data-origin-height=&quot;1172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록했던 아이디와 비밀번호를 입력해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u9Mvk/btrFZoGUrZw/LmSgjY5R3KJ4xfqtXaRVj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u9Mvk/btrFZoGUrZw/LmSgjY5R3KJ4xfqtXaRVj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u9Mvk/btrFZoGUrZw/LmSgjY5R3KJ4xfqtXaRVj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu9Mvk%2FbtrFZoGUrZw%2FLmSgjY5R3KJ4xfqtXaRVj1%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; alt=&quot;Keycloak 사용자 로그인 화면&quot; loading=&quot;lazy&quot; width=&quot;1442&quot; height=&quot;1118&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 접속된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3812&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3dKpb/btrFYEKh9JG/zbsa5FUi1UdgURNJoNlUP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3dKpb/btrFYEKh9JG/zbsa5FUi1UdgURNJoNlUP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3dKpb/btrFYEKh9JG/zbsa5FUi1UdgURNJoNlUP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3dKpb%2FbtrFYEKh9JG%2Fzbsa5FUi1UdgURNJoNlUP0%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; alt=&quot;Keycloak 로그인 성공&quot; loading=&quot;lazy&quot; width=&quot;3812&quot; height=&quot;1118&quot; data-origin-width=&quot;3812&quot; data-origin-height=&quot;1118&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;</description>
      <category>Keycloak</category>
      <category>Auth</category>
      <category>keycloak</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/47</guid>
      <comments>https://soojae.tistory.com/47#entry47comment</comments>
      <pubDate>Wed, 29 Jun 2022 10:57:42 +0900</pubDate>
    </item>
    <item>
      <title>[Keycloak] 설치 (Quarkus, 17버전 이후)</title>
      <link>https://soojae.tistory.com/46</link>
      <description>&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-filename=&quot;edited_Keycloak.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqdDzd/btrZCqyeePW/dNdZZGEUnLCfXGH0i8dkIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqdDzd/btrZCqyeePW/dNdZZGEUnLCfXGH0i8dkIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqdDzd/btrZCqyeePW/dNdZZGEUnLCfXGH0i8dkIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqdDzd%2FbtrZCqyeePW%2FdNdZZGEUnLCfXGH0i8dkIK%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; alt=&quot;Keycloak 로고&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;600&quot; data-filename=&quot;edited_Keycloak.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;keycloak은 17버전 이후로 WildFly에서 Quarkus로 변경되었습니다. (&lt;a href=&quot;https://www.keycloak.org/migration/migrating-to-quarkus&quot;&gt;https://www.keycloak.org/migration/migrating-to-quarkus&lt;/a&gt;)&lt;br /&gt;17버전 이전과 설정 방법이 다르고 정보 또한 부족하여 많은 시행 착오를 겪었습니다. Keycloak을 사용하시는 분들께 도움이 되고자 작업했던 내용들을 정리하려고 합니다.&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;Keycloak이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;keycloak은 웹 앱, Restful 웹 서비스를 위한 SSO 솔루션입니다. Keycloak은 로그인, 등록, 관리 및 계정 관리를 위한 사용자 정의 가능한 사용자 인터페이스를 제공합니다.&amp;nbsp; Keycloak을 통합 플랫폼으로 사용하여 기존 LDAP 및 Active Directory 서버에 연결할 수도 있습니다.&amp;nbsp;Facebook 및 Google과 같은 타사 ID 제공업체에 인증을 위임할 수도 있습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저 애플리케이션을 위한 Single Sign On / Single Sign Out&lt;/li&gt;
&lt;li&gt;OpenID / SAML 지원.&lt;/li&gt;
&lt;li&gt;소셜 로그인 - Google, GitHub, Facebook, Twitter 및 기타 소셜 네트워크로 로그인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;테마 지원 - 기본 Keycloak에서 제공해주는 페이지들을 커스텀 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;로그인 흐름 - 선택적 사용자 자체 등록, 비밀번호 복구, 이메일 확인, 비밀번호 업데이트 필요 등&lt;/li&gt;
&lt;li&gt;세션 관리 - 관리자와 사용자가 사용자 세션을 보고 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;토큰 매퍼 - 사용자 속성, 역할 등을 원하는 방식으로 토큰과 명령문에 매핑합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 Keycloak을 직접 다운 받는 방법 (공부 할때 추천) 과, Docker Compose를 이용하는 방법 두가지로 진행하겠습니다.&lt;br /&gt;OpenJDK 11버전 이상이 설치되어 있어야 합니다. 설치 환경은 Mac 기준입니다.&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;1. Keycloak을 직접 다운로드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.keycloak.org/downloads&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.keycloak.org/downloads&lt;/a&gt; 에 접속하여 아래 Zip 파일을 다운 받습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OdZdE/btrF0jLdAwG/WIKYKFek1SqTyBs9aClvg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OdZdE/btrF0jLdAwG/WIKYKFek1SqTyBs9aClvg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OdZdE/btrF0jLdAwG/WIKYKFek1SqTyBs9aClvg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOdZdE%2FbtrF0jLdAwG%2FWIKYKFek1SqTyBs9aClvg0%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; alt=&quot;Keycloak 다운로드&quot; loading=&quot;lazy&quot; width=&quot;2640&quot; height=&quot;1576&quot; data-origin-width=&quot;2640&quot; data-origin-height=&quot;1576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 압축을 풀고 keycloak의 폴더에서 다음 커맨드를 입력합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1656401976561&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sh ./bin/kc.sh start-dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Docker Compose로 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1656400527648&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker-compose.yml

version: &quot;3.8&quot;
services:
  keycloak:
    image: quay.io/keycloak/keycloak:18.0.2
    container_name: keycloak
    environment:
      - KEYCLOAK_ADMIN=admin
      - KEYCLOAK_ADMIN_PASSWORD=admin
    ports:
      - &quot;8080:8080&quot;
    command: start-dev
    healthcheck:
      test: &quot;curl -f http://localhost:8080/admin || exit 1&quot;&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;/p&gt;
&lt;pre id=&quot;code_1656401536787&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker-compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Admin 계정 생성과 로그인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080에 접속합니다.&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;2700&quot; data-origin-height=&quot;1314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chadfk/btrFXY2M5sW/Ke8EtdnIqsR5nvRCxRblak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chadfk/btrFXY2M5sW/Ke8EtdnIqsR5nvRCxRblak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chadfk/btrFXY2M5sW/Ke8EtdnIqsR5nvRCxRblak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchadfk%2FbtrFXY2M5sW%2FKe8EtdnIqsR5nvRCxRblak%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; alt=&quot;Keycloak Admin 계정 생성&quot; loading=&quot;lazy&quot; width=&quot;2700&quot; height=&quot;1314&quot; data-origin-width=&quot;2700&quot; data-origin-height=&quot;1314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 화면에서 최초 admin 계정을 생성합니다. &lt;br /&gt;저는 아이디: admin, 비밀번호: admin으로 설정하겠습니다. (Docker Compose의 경우 이미 admin/admin으로 생성되어 있습니다.)&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;2700&quot; data-origin-height=&quot;1314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qAA5K/btrFY8KBo23/4K57P7barlhXeILcrpkGbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qAA5K/btrFY8KBo23/4K57P7barlhXeILcrpkGbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qAA5K/btrFY8KBo23/4K57P7barlhXeILcrpkGbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqAA5K%2FbtrFY8KBo23%2F4K57P7barlhXeILcrpkGbK%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; alt=&quot;Keycloak Admin 로그인&quot; loading=&quot;lazy&quot; width=&quot;2700&quot; height=&quot;1314&quot; data-origin-width=&quot;2700&quot; data-origin-height=&quot;1314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간색으로 표시된 부분을 클릭하고, 로그인을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3796&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k6Jmn/btrFZ1cTgvU/QlcCcu2YzphKdpfjyKKER1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k6Jmn/btrFZ1cTgvU/QlcCcu2YzphKdpfjyKKER1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k6Jmn/btrFZ1cTgvU/QlcCcu2YzphKdpfjyKKER1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk6Jmn%2FbtrFZ1cTgvU%2FQlcCcu2YzphKdpfjyKKER1%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; alt=&quot;Keycloak 로그인 완료 화면&quot; loading=&quot;lazy&quot; width=&quot;3796&quot; height=&quot;1338&quot; data-origin-width=&quot;3796&quot; data-origin-height=&quot;1338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수고하셨습니다. 다음 포스트에서는 Realm, Client 그리고 User 생성을 해보겠습니다.&lt;/p&gt;</description>
      <category>Keycloak</category>
      <category>Auth</category>
      <category>keycloak</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/46</guid>
      <comments>https://soojae.tistory.com/46#entry46comment</comments>
      <pubDate>Tue, 28 Jun 2022 16:46:52 +0900</pubDate>
    </item>
    <item>
      <title>페이지 성능 최적화</title>
      <link>https://soojae.tistory.com/41</link>
      <description>&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;widthContent&quot; data-filename=&quot;edited_intro-web-browsers.jpg&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUzSYy/btrXVykZKCW/LEir3EinmtIlUvy3cJiHw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUzSYy/btrXVykZKCW/LEir3EinmtIlUvy3cJiHw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUzSYy/btrXVykZKCW/LEir3EinmtIlUvy3cJiHw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUzSYy%2FbtrXVykZKCW%2FLEir3EinmtIlUvy3cJiHw0%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;400&quot; height=&quot;219&quot; data-filename=&quot;edited_intro-web-browsers.jpg&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;219&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;오늘은 페이지 성능 최적화에 대해 알아보겠습니다. &lt;br /&gt;페이지 성능 최적화는 웹 서비스에서 중요한 개념이자, 이후에 포스팅할 CSR(Client Side Rendering)과 SSR(Server Side Rendering), 그리고 SSG(Static Site Generation)를 설명하기 위해 필요한 개념입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;페이지 성능 최적화를 해야 하는 이유?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;사용자가 사이트를 방문한 후, 아무런 요청을 실행하지 않고 떠나는 비율을 &lt;b&gt;이탈률&lt;/b&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-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;edited_edited_bounce.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bevxm1/btrXUSRZiC0/frkuKMfPUmL2MvH43888q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bevxm1/btrXUSRZiC0/frkuKMfPUmL2MvH43888q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bevxm1/btrXUSRZiC0/frkuKMfPUmL2MvH43888q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbevxm1%2FbtrXUSRZiC0%2FfrkuKMfPUmL2MvH43888q1%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;600&quot; height=&quot;418&quot; data-filename=&quot;edited_edited_bounce.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위의 그래프를 보시면 알 수 있듯이, 페이지 로딩 속도에 대한 이탈률을 보면 로딩 시간이 3초가 걸리면 32%, 5초면 90%, 6초면 106%, 10초면 123% 라고 합니다.&lt;br /&gt;2018년도 자료이니, 2020년도인 지금은 로딩 속도에 대한 이탈률이 더 증가했다고 봐도 과언이 아닐 것입니다.&lt;br /&gt;아무리 잘 만든 페이지라도, 사용자가 이용하지 않으면 말짱 도루묵이겠죠? 이런 이탈률을 줄이기 위해 페이지 성능 최적화가 필요합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;페이지 속도 측정에 가장 중요한 4가지 값&lt;/h2&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;widthContent&quot; data-filename=&quot;edited_Page_Speed_Mess-Stationen.jpg&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5jxBy/btrXVrzAa4m/xSgvHgqhcDNQlPF2e1H3z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5jxBy/btrXVrzAa4m/xSgvHgqhcDNQlPF2e1H3z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5jxBy/btrXVrzAa4m/xSgvHgqhcDNQlPF2e1H3z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5jxBy%2FbtrXVrzAa4m%2FxSgvHgqhcDNQlPF2e1H3z1%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;700&quot; height=&quot;225&quot; data-filename=&quot;edited_Page_Speed_Mess-Stationen.jpg&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;br /&gt;TTFB(Time to First Byte)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 웹 사이트를 호출하면 웹 서버에서 수신한 첫 번째 바이트가 도착하는 시간입니다.&lt;br /&gt;즉, HTTP 요청에 걸리는 시간 + 서버의 요청 처리 시간 + 서버에서 클라이언트까지의 응답 시간이라고 보시면 됩니다.&lt;br /&gt;TTFB 속도는 서버의 프로세스와 연관되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;최적화 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 호스팅 업체 변경&lt;br /&gt;2. CDN 사용&lt;br /&gt;3. 서버 코드 경량화 &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FCP(First Contentful Paint)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTFB 이후에 콘텐츠(HTML 코드, 스타일, 이미지 등)가 표시될 때까지의 시간입니다.&lt;br /&gt;이 시점은 사용자가 이 웹사이트가 실제로 동작한다고 인식하도록 해주기 때문에 중요합니다. (사용자 이탈률이 줄어듭니다.)&lt;br /&gt;Pagespeed Insights(페이지 속도를 측정해주는 구글 툴)에서는 TTFB 측정은 건너뛰고 FCP 측정을 첫 번째로 합니다. 이는 개발자를 위한 것이며, Pagespeed Insight는 FCP를 활용하여 OnPage 최적화(제목 태그, 콘텐츠, 내부 링크 및 URL 최적화)를 위한 제안을 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;최적화 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 데이터 압축&lt;br /&gt;2. HTTP/2 사용&lt;br /&gt;3. 리로드할 필요 없는 콘텐츠를 캐싱&lt;br /&gt;4. 코드 경량화 &amp;amp; 스플리팅&lt;br /&gt;5. 라이브러리 정리&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FMP(First Meaningful Paint)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 의미 있는 콘텐츠가 그려지기 까지의 시간입니다. 하지만 이벤트 리스너(화면 스크롤, 클릭 등)는 아직 추가되지 않았습니다.&lt;br /&gt;사용자는 이 시점에서 페이지가 완전히 로드가 되었다고 인식합니다. 그러므로 FMP의 최적화는 좀 더 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;최적화 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 최적화, 사진이 많은 웹사이트일 때는 LazyLoading (예: Twitter, Instagram 등 스크롤을 내리면 추가 페이지 로딩)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TTI(Time To Interactive)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트의 실행이 완료되어. 페이지가 상호작용 가능하게 될 때까지의 시간 (이벤트 발생 등).&lt;br /&gt;대부분의 페이지 속도 테스트는 이 값을 기초로 사용합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최적화 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FCP와 FMP를 최적화&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 화면을 통해 시점들을 살펴봅시다. 밑의 FP(First Paint)는 첫 픽셀이 그려지기 까지의 시간입니다.&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;widthContent&quot; data-filename=&quot;render_terms.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yYCd2/btqLRUF6yYq/hFvJI1re7TZ4Irl6g8vMA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yYCd2/btqLRUF6yYq/hFvJI1re7TZ4Irl6g8vMA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yYCd2/btqLRUF6yYq/hFvJI1re7TZ4Irl6g8vMA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyYCd2%2FbtqLRUF6yYq%2FhFvJI1re7TZ4Irl6g8vMA0%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;800&quot; height=&quot;275&quot; data-filename=&quot;render_terms.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위의 화면들을 보시면 중요한 시점은 FCP와 FMP인 것을 알 수 있습니다. 우리가 웹 사이트를 방문한다고 생각해봅시다.&lt;br /&gt;페이지의 로딩이 끝날 때까지 흰 화면을 보여주기보다는, 화면을 부분적(FCP)으로 보여준 후&amp;nbsp;의미 있는 화면(FMP)을 보여주는 것이 좋겠죠?&lt;br /&gt;그러므로 사용자에게 페이지의 성능이 좋다는 느낌을 주기 위해서는 FCP와 FMP시간을 단축시켜야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;페이지 로딩 최적화 방법과 자세한 설명은 &lt;b&gt;TOAST UI&lt;/b&gt;에서 포스팅한&amp;nbsp;&lt;a href=&quot;https://ui.toast.com/fe-guide/ko_PERFORMANCE/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;성능 최적화&lt;/a&gt;에서 확인해보시길 바랍니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;글에 오류가 있으면 알려주세요 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REFERENCES&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.ryte.com/magazine/measure-page-speed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://en.ryte.com/magazine/measure-page-speed&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://ui.toast.com/fe-guide/ko_PERFORMANCE/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ui.toast.com/fe-guide/ko_PERFORMANCE/&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://marshall-ku.com/web/log/ttfb-%EB%8C%80%ED%8F%AD-%EB%8B%A8%EC%B6%95-%EC%84%B1%EA%B3%B5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://marshall-ku.com/web/log/ttfb-%EB%8C%80%ED%8F%AD-%EB%8B%A8%EC%B6%95-%EC%84%B1%EA%B3%B5&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;!-- 레이어 팝업 --&gt;</description>
      <category>Knowledge/Web</category>
      <category>CSR</category>
      <category>SSG</category>
      <category>SSR</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/41</guid>
      <comments>https://soojae.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 26 Oct 2020 20:40:38 +0900</pubDate>
    </item>
    <item>
      <title>npm과 npx의 차이</title>
      <link>https://soojae.tistory.com/40</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Magdalena Siljanoska의 &lt;a href=&quot;https://medium.com/javascript-in-plain-english/yes-its-npx-not-npm-the-difference-explained-58cbb202ec33&quot;&gt;Yes, it&amp;rsquo;s npx, not npm &amp;mdash; the difference explained&lt;/a&gt; 를 번역한 게시물입니다.&lt;br /&gt;Thank you Magdalena Siljanoska! Thanks to your writing, I can grow further as a developer.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;npm_vs_npx.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;240&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDobAq/btqKrKzjhNE/BUHZGY2YzGW6X26Oi2VA0K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDobAq/btqKrKzjhNE/BUHZGY2YzGW6X26Oi2VA0K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDobAq/btqKrKzjhNE/BUHZGY2YzGW6X26Oi2VA0K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bDobAq/btqKrKzjhNE/BUHZGY2YzGW6X26Oi2VA0K/img.gif&quot; data-filename=&quot;npm_vs_npx.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;240&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 data-ke-size=&quot;size16&quot;&gt;나는 최근에 &lt;a href=&quot;https://medium.com/javascript-in-plain-english/how-to-build-a-simple-task-manager-with-react-8895d4526b2e&quot;&gt;리액트를 배우면서&lt;/a&gt; 내가 잘 알고 있는 &lt;b&gt;npm&lt;/b&gt;이 아닌 &lt;b&gt;npx&lt;/b&gt;를 보고 혼란스러워했고, 많은 사람들이 혼란스러워하는 모습을 봤다.&lt;br /&gt;우리들 중 일부는 그것이 이상하다고 생각했지만 별 생각이 없었고, 또 다른 사람들은 오타라고 생각하고 &lt;b&gt;npm&lt;/b&gt;을 &lt;b&gt;npx&lt;/b&gt;로 &quot;&lt;i&gt;고쳐서&lt;/i&gt;&quot; 어려움을 겪었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇번 더 이런 상황을 겪게 되니 이것에 대해 설명할 가치가 있다고 생각했다. 그래서 이 게시물을 작성하게 되었다. 나와 같이 npx에 대해 오해하고 있었던 모든 사람들을 위해서.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm이 아니라 npx가 맞아! 오타가 아니야! &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NPM&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 알고있듯이, &lt;a href=&quot;https://www.npmjs.com/&quot;&gt;npm&lt;/a&gt;은 &lt;b&gt;의존성(dependency)&lt;/b&gt; 과 &lt;b&gt;패키지 관리(package management)&lt;/b&gt;를 자동화 하기 위한 Node.js의 패키지 매니저이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 프로젝트를 위한 모든 의존성 패키지를 &lt;b&gt;package.json&lt;/b&gt; 파일에 지정할 수 있다. 그리고 언제든지, 그리고 누구라도 그 의존성 패키지들을 설치해야 할때는 단순히 &lt;b&gt;npm install&lt;/b&gt; 만 타이핑하면 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것은 또한 &lt;b&gt;versioning&lt;/b&gt;을 제공한다. &lt;b&gt;versioning&lt;/b&gt;이란 우리 프로젝트에 의존하는 의존성 패키지들의 버전을 지정할 수 있게 해주는 것을 말한다.&lt;br /&gt;그래서 대부분의 경우, 패키지의 버전 업데이트로 인해 우리의 프로젝트가 깨지는 것을 방지하거나, 우리가 선호하는 버전을 이용할 수 있게 해준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NPX&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그와 반대로 &lt;a href=&quot;https://www.npmjs.com/package/npx&quot;&gt;npx&lt;/a&gt;는 &lt;b&gt;Node 패키지들을 실행하는 도구&lt;/b&gt;다. 그리고 npm 5.2버전부터 npm 안에 포함되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;npx&lt;/b&gt;는 다음과 같이 동작한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. npx는 우선 기본적으로, 실행할 패키지가 실행 가능한 경로에 있는지 확인한다. (예를 들면, 우리의 프로젝트내에서 확인한다.)&lt;br /&gt;2. 만약에 있다면, 그것을 실행한다.&lt;br /&gt;3. 아니라면 패키지가 설치가 되지 않았다는 것으로 판단하여, npx가 가장 최신 버전의 패키지를 설치하고 실행한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 행동들은 기본 설정일때의 &lt;b&gt;npx&lt;/b&gt; 동작방식이다. flags를 설정하면 사용자에 맞게 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면, 만약에 &lt;b&gt;npx some-package --no-install&lt;/b&gt; 을 실행시키면, 이는 &lt;b&gt;npx&lt;/b&gt;가 &lt;b&gt;some-package&lt;/b&gt;가 &lt;b&gt;기존에&lt;/b&gt; &lt;b&gt;설치되어있지 않더라도&lt;/b&gt; &lt;b&gt;some-package&lt;/b&gt;를 &lt;b&gt;실행&lt;/b&gt;시키라는 뜻이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npx flags에 대해 자세히 알고 싶다면 &lt;a href=&quot;https://www.npmjs.com/package/npx&quot;&gt;여기로&lt;/a&gt; 가라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Example&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 우리의 패키지가 &lt;b&gt;my-package&lt;/b&gt;고, 이 패키지를 실행시키고 싶다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글쎄, &lt;b&gt;npx&lt;/b&gt;가 없이 패키지를 실행하려면, 우리는 지역 경로(local path)로부터 my-package를다음과 같이 실행해야할 것이다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;$ ./node_modules/.bin/my-package&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 &lt;b&gt;package.json&lt;/b&gt; 파일의 scripts 부분에 작성하는 방법도 있다. 다음과 같이.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;something&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;scripts&quot;: {
  &quot;my-package&quot;: &quot;./node_modules/.bin/my-package&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;npm run my-package&lt;/b&gt; 로 실행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &lt;b&gt;npx&lt;/b&gt;를 이용하면 우리는 단순히 &lt;b&gt;npx my-package&lt;/b&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;b&gt;npm !== npx&lt;/b&gt; 라는 것이고, 이 짧은 글이 당신의 &lt;b&gt;npx&lt;/b&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;해피코딩!  &lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 번역 오류가 있으면 알려주세요 감사합니다.&lt;/p&gt;
&lt;!-- 레이어 팝업 --&gt;</description>
      <category>Knowledge/Web</category>
      <category>NPM</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/40</guid>
      <comments>https://soojae.tistory.com/40#entry40comment</comments>
      <pubDate>Fri, 9 Oct 2020 21:32:27 +0900</pubDate>
    </item>
    <item>
      <title>ESLint와 Prettier 사용법과 차이점</title>
      <link>https://soojae.tistory.com/39</link>
      <description>&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;widthContent&quot; data-filename=&quot;eslint.jpg&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6LgO4/btqKkPnF29W/8jFKHaCcJsnlqJTWK1Y9C0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6LgO4/btqKkPnF29W/8jFKHaCcJsnlqJTWK1Y9C0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6LgO4/btqKkPnF29W/8jFKHaCcJsnlqJTWK1Y9C0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6LgO4%2FbtqKkPnF29W%2F8jFKHaCcJsnlqJTWK1Y9C0%2Fimg.jpg&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;700&quot; height=&quot;400&quot; data-filename=&quot;eslint.jpg&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;400&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;span style=&quot;color: #000000;&quot;&gt;안녕하세요. 오늘은 ESLint와 Prettier의 개념과, 사용법 그리고 차이점에 대해 말해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ESLint&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Linter의 기능&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;느슨한 형식의 언어인 Javascript에서는 코드 에러가 자주 발생합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 JavaScript는 동적 분석(프로그램을 직접 실행해서 코드를 분석 &amp;lt;=&amp;gt; 프로그램을 실행하지 않고 분석하는 정적 분석)을 하기 때문에&amp;nbsp; 에러를 찾기 위해서는 코드를 직접 실행해서 확인을 해봐야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 도와주는 것이 Linter입니다. Linter는 &lt;span style=&quot;color: #000000;&quot;&gt;코드를 &lt;/span&gt;정적으로 분석하기 때문에, 프로그램을 실행하지 않고도 코딩 컨벤션에 위배되는 코드나 안티 패턴을 자동으로 검출해줍니다. &lt;span style=&quot;color: #000000;&quot;&gt;추가적으로 간단한 코드 포맷팅 기능도 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ESLint 설치&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1602049393632&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -D eslint&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;이제 eslint를 실행해봅시다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602124513849&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./node_modules/.bin/eslint -v&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: #000000;&quot;&gt;그런데 &lt;b&gt;./node_modules/.bin/eslint&lt;/b&gt; 를 직접 작성하기 번거로울 뿐만 아니라 보기에도 좋지 않다는 게 느껴질 것입니다. 이를 해결하기 위해 글로벌로도 설치해주세요.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602124657114&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -g eslint&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: #000000;&quot;&gt;이제 eslint 명령어만로도 실행이 가능합니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602124850995&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ eslint -v&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: #000000;&quot;&gt;※ 글로벌로 설치하기 싫거나, 설치를 하지 못하는 상황이고, 로컬에 있는 eslint만 사용하고 싶은데 &lt;b&gt;./node_modules/.bin/eslint&amp;nbsp;&lt;/b&gt;를 작성하기 힘들 때는 npx를 사용하면 됩니다. (&lt;a href=&quot;https://soojae.tistory.com/40&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;npx란?&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602124992235&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npx eslint -v&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: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 ESLint 패키지 설치가 완료되면 초기화를 해주세요.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602049522049&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ eslint --init&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: #000000;&quot;&gt;그럼 다음과 같은 단계별 질문이 나옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602049553755&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;? How would you like to use ESLint? &amp;hellip; 
To check syntax only
❯ To check syntax and find problems
To check syntax, find problems, and enforce code style

// ESLint의 사용 목적은 무엇인가요?

? What type of modules does your project use? &amp;hellip; 
❯ JavaScript modules (import/export)
CommonJS (require/exports)
None of these

// 어떤 타입의 모듈을 사용하나요?

? Which framework does your project use? &amp;hellip; 
React
Vue.js
❯None of these

// 어떤 프레임워크에서 사용할 것인가요? (앵귤러는 왜 없나요...)

? Does your project use TypeScript? &amp;rsaquo; No / Yes

// 타입 스크립트를 사용하나요?

? Where does your code run? &amp;hellip; (Press &amp;lt;space&amp;gt; to select, &amp;lt;a&amp;gt; to toggle all, &amp;lt;i&amp;gt; to invert selection)
✔ Browser
✔ Node

// 어떤 환경에서 코드가 동작하나요?

? What format do you want your config file to be in? &amp;hellip; 
JavaScript
YAML
❯ JSON

// 어떤 포맷으로 ESLint 설정파일을 생성할까요?&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: #000000;&quot;&gt;완료되면 .eslintrc.json 파일이 생성되며, 이 파일에는 ESLint에 대한 설정들이 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.eslintrc.json 파일을 만들기 싫다면, package.json 파일 내의 eslintConfig 부분에서도 설정할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.eslintignore 파일을 생성하여 해당 파일, 폴더들에 대한 ESLint를 무시할 수도 있습니다. (프로젝트 내의 /node_modules/* and /bower_components/* 는 기본적으로 제외되어 있습니다.)&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: #000000;&quot;&gt;그럼 이제 예제를 통해 ESLint를 적용해 봅시다. test.js 파일을 생성해 주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602051415137&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test.js

const abc = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;br /&gt;test.js 파일 생성후, 커맨드 창에 아래와 같이 입력하면 다음과 같은 메시지를 확인할 수 있습니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602051383498&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ eslint test.js


/Users/soojae/IdeaProjects/Blog/Tools/Example/test.js
1:7 error 'abc' is assigned a value but never used no-unused-vars

✖ 1 problem (1 error, 0 warnings)&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: #000000;&quot;&gt;내용을 보니 변수를 선언한 후 사용을 하지 않아서 발생한 에러인 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;console.log(abc)&lt;/b&gt;를 추가해서 abc를 사용해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602051660121&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test.js

const abc = 0

console.log(abc)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다시 eslint test.js를 입력하면 에러가 발생하지 않는 것을 확인할 수 있습니다.&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: #000000;&quot;&gt;이제 간단한 에러를 자동으로 해결해주는 --fix를 사용해봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602051566620&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package.json

[
  rules: {
  &quot;semi&quot;: &quot;error&quot;,
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&quot;semi&quot;는 세미콜론(;)이 필요한 위치에 없을 시 에러를 발생시킵니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602051871157&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ eslint test.js --fix&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;test.js 파일을 확인해보면, 세미콜론(;)이 자동적으로 추가된 것을 확인할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602056181125&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test.js

const abc = 0;

console.log(abc);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 ESLint와 한 세트로 붙어 다니는 Prettier에 대해 알아봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;Prettier.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPZQsu/btqKnqgzDRJ/yQmtkcXlfNMU2p712HXrM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPZQsu/btqKnqgzDRJ/yQmtkcXlfNMU2p712HXrM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPZQsu/btqKnqgzDRJ/yQmtkcXlfNMU2p712HXrM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPZQsu%2FbtqKnqgzDRJ%2FyQmtkcXlfNMU2p712HXrM0%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;700&quot; height=&quot;291&quot; data-filename=&quot;Prettier.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Prettier&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;Prettier는 어느 경우에 사용할까요? 사실 실 생활에서도 Pretter가 적용되어 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;탕수육(Code)을 먹을 때 부먹이냐 찍먹이냐의 싸울 때, 결국 사주는 사람(Pritter)의 취향으로 가게 됩니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드로 본다면 한 개발자는 indent 값을 2로 주고 싶은데, 또 다른 개발자는 4로 주고 싶을 경우가 있을 것입니다. 이를 해결해 주는 것이 Prettier입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Prettier는 2016년에 등장한 코드 포맷터입니다. Prettier를 사용하면 코드를 완전히 다시 작성해줍니다.(변경이 필요한 부분만 바꾸는 것이 아니라, 코드 전체를 새로 작성합니다.)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;새로 작성한다고 해도 코드 내용은 변하지 않고, 구조적 뷰만 변경됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602056132332&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -g -E prettier
$ npm i -D -E prettier&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: #000000;&quot;&gt;-E는 --save-exac의 축약어로서 해당 패키지를 정확한 버전으로 설치해줍니다. Prettier에서는 버전이 달라지면서 스타일이 변할 수 있기 때문에 이 옵션을 붙이는 것을 권장합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 설정 파일인 .prettierrc.json 파일을 생성하고, Prettier 옵션을 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://prettier.io/docs/en/options.html&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;https://prettier.io/docs/en/options.html&lt;/span&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt; 에 들어가서 알맞게 변경해주세요. 아래는 예시입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602052141434&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//.prettierrc.json

{
  &quot;arrowParens&quot;: &quot;avoid&quot;,
  &quot;bracketSpacing&quot;: true,
  &quot;htmlWhitespaceSensitivity&quot;: &quot;css&quot;,
  &quot;insertPragma&quot;: false,
  &quot;jsxBracketSameLine&quot;: false,
  &quot;jsxSingleQuote&quot;: false,
  &quot;printWidth&quot;: 80,
  &quot;proseWrap&quot;: &quot;preserve&quot;,
  &quot;quoteProps&quot;: &quot;as-needed&quot;,
  &quot;requirePragma&quot;: false,
  &quot;semi&quot;: true,
  &quot;singleQuote&quot;: true,
  &quot;tabWidth&quot;: 2,
  &quot;trailingComma&quot;: &quot;none&quot;,
  &quot;useTabs&quot;: false
}&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: #000000;&quot;&gt;이제 test.js 파일을 다음과 같이 수정해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602052451305&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test.js

const abc
= 0
    
console.
log(abc)&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: #000000;&quot;&gt;prettier를 적용하면 다음과 같은 결과가 나옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602052501787&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ prettier test.js --write&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1602052510820&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test.js

const abc = 0;
    
console.log(abc);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;br /&gt;ESLint VS Prettier&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;ESLint는 코드 포맷터의 역할도 하지만, 주로 코드 에러를 잡아내고 코드 문법을 강제하는 등 코드 품질을 개선에 중점을 두었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Prettier는 코드의 최대 길이, 함수에서, 작은따옴표(')를 사용할 것인지 아니면 큰 따옴표(&quot;)를 사용할 것인지 등 코드가 예쁘게 보이도록 하는지에 중점을 두었습니다. 하지만 코드의 에러를 잡아내진 못합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Prettier 공식 홈페이지에는 아래와 같은 문구가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;use Prettier for formatting and linters for catching bugs!&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉 ESLint에도 코드 포맷팅이 있긴 하지만 Prettier의 코드 포맷팅에 비해 특화되어있지 않으므로, &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드 에러와 문법을 정적으로 탐지할때는 ESLint를 사용하고 포맷팅에는 Prettier를 사용하면 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ESLint + Prettier &lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;이제 ESLint와 Prettier를 같이 사용해봅시다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 위에서 말씀드렸듯이, ESLint가 코드 포맷터의 역할도 하기 때문에 해당 Prettier와 충돌할 수도 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 방지하기 위해서 아래의 패키지를 추가합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;eslint-config-prettier&lt;/i&gt; 패키지는 ESLint 규칙에서 Pretter 규칙과 충돌하는 규칙들을 OFF 시키는 역할을 합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602052695899&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -D eslint-config-prettier&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;eslint-plugin-prettier는 Prettier 규칙들을 ESLint 규칙에 추가하는 패키지입니다. 즉, eslint --fix 만 실행해줘도 prettier --write를 사용할 필요 없이 prettier까지 적용시켜줍니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;.eslintrc.json&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;. eslintrc.json&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar12&quot; data-grammar-focus=&quot;false&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602052825759&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -D eslint-plugin-prettier&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: #000000;&quot;&gt;.eslintrc.json 에 다음을 추가해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602052875237&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .eslintrc.json

{
    &quot;plugins&quot;: [
        &quot;prettier&quot;
    ],
    &quot;rules&quot;: {
        &quot;prettier/prettier&quot;: &quot;error&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;br /&gt;이제 ESLint만 실행해도 Prettier의 포매팅 기능을 포함해서 실행합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Git 저장소에 푸시 전 ESLint, Prettier 강제 적용 하기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;매번 ESLint를 실행하는 것은 번거로울 뿐만 아니라, 같은 팀 내 개발자의 실수로 ESLint, Prettier를 적용하지 않은 코드들이 저장소에 푸시될 수도 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇게 되면 깃 로그에 &lt;i&gt;ESLint, Prettier 적용&lt;/i&gt; 같은 생산성 없는 로그들이 쌓이게 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 경우를 방지하기 위해 commit 전에 코드들에게 ESLint와 Prettier이 강제적으로 적용되도록 만들어 봅시다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 위해서는&lt;i&gt;husky와&lt;/i&gt; &lt;i&gt;lint-staged&lt;/i&gt; 패키지가 추가로 필요합니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602053434305&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -D husky lint-staged&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Husky&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;Git Hook을 편리하게 관리해주는 툴입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;git의 특정 이벤트(commit, push, pull 등)가 발생하려고 하면, 그 순간에 Hook을 해서 스크립트가 실행되도록 해주는 것이 Git Hook입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Git Hook에 대한 자세한 설명은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://woowabros.github.io/tools/2017/07/12/git_hook.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;훅으로 Git에 훅 들어가기&lt;/a&gt;&lt;/span&gt; 에서 확인하시면 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간단하게 사용할 용어들만 정리했습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;pre-commit : 커밋 메시지를 작성하기 전에 호출됨&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;prepare-commit-msg : 커밋 메시지 생성 후 편집기 실행 전에 호출됨&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;commit-msg : 커밋 메시지와 관련된 명령을 넣을 때 호출됨&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;post-commit : 커밋이 완료되면 호출됨&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;pre-push : 푸시가 실행되기 전에 호출됨&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Husky도 .huskyrc, .huskyrc.json, .huskyrc.js, husky.config.js와 같은 별도의 설정 파일을 만들 수도 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;package.json 파일에 husky를 추가해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602053654893&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package.json

&quot;husky&quot;: {
    &quot;hooks&quot;: {
      &quot;pre-commit&quot;: &quot;eslint --fix &amp;amp;&amp;amp; prettier --write&quot;,
      &quot;pre-push&quot;: &quot;npm test&quot;
    }
  },&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;lint-staged&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;husky만 사용한다면 프로젝트 내의 모든 코드를 검사하기 때문에 비효율 적입니다. lint-staged는 git에 staged 상태인 파일만 lint 해주는 도구입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602053824676&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package.json

&quot;husky&quot;: {
    &quot;hooks&quot;: {
      &quot;pre-commit&quot;: &quot;lint-staged&quot;,
      &quot;pre-push&quot;: &quot;npm test&quot;
    }
  },
  &quot;lint-staged&quot;: {
    &quot;*.js&quot;: [
      &quot;eslint --fix&quot;
      &quot;prettier --write&quot;
    ]
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;br /&gt;사실 위에서 eslint-plugin-prettier 패키지를 설치했기 때문에 &lt;b&gt;eslint --fix&lt;/b&gt;만 사용해도 &lt;b&gt;prettier --write&lt;/b&gt;까지 적용이 됩니다. 이제 에러를 잘 잡아내는지 확인하기 위해 &lt;b&gt;console.log(abc)&lt;/b&gt;를 제거해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602053863531&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test.js

const abc
= 0&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: #000000;&quot;&gt;이제 commit을 해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602053890630&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ git commit -m &quot;[Tools] eslint husky lint-staged 테스트&quot;

husky &amp;gt; pre-commit (node v12.18.0)
✔ Preparing...
⚠ Running tasks...
❯ Running tasks for *.js
✖ eslint --fix [FAILED]
◼ prettier --write
&amp;darr; Skipped because of errors from tasks. [SKIPPED]
✔ Reverting to original state because of errors...
✔ Cleaning up... 

✖ eslint --fix:

/Users/soojae/IdeaProjects/Blog/Tools/Example/test.js
1:7 error 'abc' is assigned a value but never used no-unused-vars

✖ 1 problem (1 error, 0 warnings)

husky &amp;gt; pre-commit hook failed (add --no-verify to bypass)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;변수 abc를 사용하지 않아 에러가 발생했고, commit이 발생되지 않았습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Prettier까지도 적용이 되지 않는 것을 보아, ESLint 단계에서 에러를 잡아내자마자 실행이 멈췄다는 것을 알 수 있습니다.. 이제 ESLint 규칙에 위배되는 코드들이 Git 저장소에 올라 올 일은 없을 것입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 다시&amp;nbsp;&lt;b&gt;console.log(abc)&lt;/b&gt;를 추가해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602054116180&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test.js

const abc
= 0

console.
log(abc)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;다시 커밋을 하면 아래와 같은 메시지가 나옵니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602054143260&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ git commit -m &quot;[Tools] eslint husky lint-staged 테스트&quot;

husky &amp;gt; pre-commit (node v12.18.0)
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up... 
[master 17e09f6] [Tools] eslint huskey lint-staged 테스트
3 files changed, 17 insertions(+), 15 deletions(-)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;commit이 완료되면 test.js에 prettier가 자동적으로 적용된 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1602054170013&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test.js
// prettier까지 적용된 것을 확인 할 수 있습니다.

const abc = 0;

console.log(abc);&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: #000000;&quot;&gt;현재 회사에서는 TSLint 만 사용하고 있습니다. 하지만 이번 대형 프로젝트에 투입되면서, 제대로 Lint가 제대로 적용이 안된 코드들이 올라오고 코드 스타일도 각각 달라서 혼란스러웠습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;다음 프로젝트에는 ESLint &lt;/span&gt;(2019년 이후로 TSLint 업데이트가 중단되고 ESLint에 추가된다고 합니다.&amp;nbsp; &lt;a href=&quot;https://medium.com/palantir/tslint-in-2019-1a144c2317a9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TSLint in 2019&lt;/a&gt;를 참고해주세요.)&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;와 prettier, husky, 그리고 lint-staged을 사용할 수 있도록 &lt;/span&gt;건의를 드려보려고 합니다.&lt;br /&gt;&lt;br /&gt;그리고 앞으로 진행할 저의 토이 프로젝트에는 &lt;b&gt;무조건&amp;nbsp;&lt;/b&gt;적용시킬 겁니다. ESLint와 Prettier는 그만큼 강력하고 개발자가 코드에만 집중할 수 있도록 도와주는 유용한 도구입니다.&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&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;span style=&quot;color: #000000;&quot;&gt;글에 오류가 있으면 알려주세요. 감사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr style=&quot;border: none; font-size: 0px; line-height: 0; height: 20px; margin: 20px auto; background: url('https://t1.daumcdn.net/keditor/dist/0.4.0/image/divider-line.svg') 0px -120px / 200px 200px; cursor: pointer !important; opacity: 0.2; color: #444444; font-family: 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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REFERENCES&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://jbee.io/web/formatting-code-automatically/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jbee.io/web/formatting-code-automatically/&lt;/a&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/better-programming/ESLint-vs-prettier-57882d0fec1d&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/better-programming/ESLint-vs-prettier-57882d0fec1d&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://subicura.com/2016/07/11/coding-convention.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://subicura.com/2016/07/11/coding-convention.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://feynubrick.github.io/2019/05/20/eslint-prettier.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://feynubrick.github.io/2019/05/20/eslint-prettier.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://woowabros.github.io/tools/2017/07/12/git_hook.html&quot;&gt;https://woowabros.github.io/tools/2017/07/12/git_hook.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/palantir/tslint-in-2019-1a144c2317a9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/palantir/tslint-in-2019-1a144c2317a9&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;&amp;nbsp;&lt;/p&gt;
&lt;!-- 레이어 팝업 --&gt;</description>
      <category>Language/Javascript</category>
      <category>Prettier</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/39</guid>
      <comments>https://soojae.tistory.com/39#entry39comment</comments>
      <pubDate>Wed, 7 Oct 2020 17:33:53 +0900</pubDate>
    </item>
    <item>
      <title>NCA 자격증 취득 후기</title>
      <link>https://soojae.tistory.com/38</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bImwOG/btqHsdESzHo/jPL5cdygaTTo76Xcxc4zN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bImwOG/btqHsdESzHo/jPL5cdygaTTo76Xcxc4zN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bImwOG/btqHsdESzHo/jPL5cdygaTTo76Xcxc4zN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbImwOG%2FbtqHsdESzHo%2FjPL5cdygaTTo76Xcxc4zN1%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;815&quot; height=&quot;269&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사를 다니면서 틈틈이 공부한 끝에 NCA 자격증을 취득했습니다. 자격증을 취득하기 위해 공부했던 방식과, 후기를 공유하려고 합니다.&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;네이버 클라우드 플랫폼 자격증 중에는 NCA, NCP, NCE 총 3가지가 있습니다. NCA 취득하고 NCP, NCP를 취득하고 NCE 시험을 볼 수 있습니다.&lt;br /&gt;NCA 시험은 네이버 클라우드 플랫폼 자격증 중의 가장 기초가 되는 자격증입니다. 이 시험은 Overview, Compute, Storage, Network, Media에서 문제가 출제됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자격증 공부하는 방법은 네이버 클라우드 설명서(&lt;a href=&quot;https://docs.ncloud.com/ko/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.ncloud.com/ko/&lt;/a&gt;) 사이트에 들어가셔서 Compute, Storage, Network, Media를 순서대로 전부 실습을 해보는 것이 좋습니다.&lt;br /&gt;그 후에, 다시 설명서를 꼼꼼히 보셔야 합니다. 각 메뉴의 첫 번째 문단인 '사용하기 전에' 부분들은 유심히 보셔야 합니다. 그리고 설명서를 읽으시다가 숫자가 출몰한다면 전부 외우셔야 합니다 (예: 한 계정당 생성할 수 있는 서버 개수는 50개, 스토리지 용량 1GB 증가당 40 IOPS 증가, CLA 저장 용량은 100GB 등). 요금 가격까지는 외우실 필요 없지만, 요금이 어떻게 책정되는지는 아셔야 합니다. 글에서&amp;nbsp;&lt;b&gt;볼드(Bold)&lt;/b&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;4가지 항목을 전부 공부하신 후, Overview를 봐야 하는데 사실 이 부분은 어떻게 공부해야 하는지 말씀드리기 조심스럽습니다. &lt;br /&gt;모든 메뉴를 전부 공부하는 것이 완벽하나, 시간이 여의치 않다면 네이버 클라우드 플랫폼의 각 메뉴들이 기본적으로 어떤 일들을 하는지를 간략하게라도 공부하시는 것을 추천드립니다.&lt;br /&gt;제가 봤던 시험에는 Overview에 해당하는 부분들이 VMware on Ncloud, AI&amp;middot;NAVER API, AI&amp;middot;Application Service, Analytics, SECURITY에서 출제되었습니다. &lt;br /&gt;그 후에 네이버 클라우드 서비스(&lt;a href=&quot;https://www.ncloud.com/product&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;www.ncloud.com/product&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;br /&gt;예를 들어, 한 계정당 생성할 수 있는 서버 수가 50대이니까 오토 스케일링이나 로드 밸런서에서 사용될 최대 서버 수는 50대가 안 넘겠죠? 오토스케일링당 생성할 수 있는 서버 수는 30대까지고, 하나의 로드밸런서에 바인드 할 수 있는 서버 수는 50대까지입니다.&lt;br /&gt;또, 오토 스케일링을 공부하다 보면 Launch Configuration과 Auto Scaling Group가 나오는데, 전자는&amp;nbsp;&lt;b&gt;서버 스펙을 설정&lt;/b&gt;, 후자는 Auto Scaling Group은&amp;nbsp;&lt;b&gt;서버&lt;/b&gt;&amp;nbsp;&lt;b&gt;생성 / 반납의 행위에 대한 요소들&lt;/b&gt;이라고 생각하시면 됩니다. 그럼 오토 스케일링에 관한 문제에서 해당 문제의 보기들이&amp;nbsp;Launch Configuration과 Auto Scaling Group 중 각각 어디에 해당하는 내용인지를 쉽게 찾으실 수 있으실 겁니다.&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;낮았는데, 이번 NCA를 준비하면서 인프라에 대한 기본적인 지식들을 다시 쌓을 수 있어서 정말 좋았습니다.&lt;br /&gt;오랜만에 자격증 시험을 봐서 힘들었지만, 자격증 취득에 보람을 느꼈습니다. 사실, 코로나 이후 반복되는 집돌이 생활과 끝없는 프런트엔드 공부에 슬럼프가 생겼었습니다. &lt;br /&gt;하지만 NCA 시험을 준비하면서 생활에 활력이 생기고, 슬럼프에서 벗어났습니다. 매일 NCA 공부하고 외우는 것을&amp;nbsp;반복하니, 프런트엔드 공부가 하고 싶더라고요...&lt;br /&gt;좋은 경험이었습니다. 또다시 프런트엔드 공부에 슬럼프가 발생하면 그땐 또 다른 공부를 하며 극복하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그럼 NCA를 준비하시는 모든 분들 파이팅하세요!&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;widthContent&quot; data-filename=&quot;제목 없음.png&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0O5vT/btqHCWPpTOl/FAYeW84n9O2c9iMdxsiOKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0O5vT/btqHCWPpTOl/FAYeW84n9O2c9iMdxsiOKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0O5vT/btqHCWPpTOl/FAYeW84n9O2c9iMdxsiOKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0O5vT%2FbtqHCWPpTOl%2FFAYeW84n9O2c9iMdxsiOKk%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;848&quot; height=&quot;624&quot; data-filename=&quot;제목 없음.png&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;624&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;</description>
      <category>Infra</category>
      <category>NCA</category>
      <category>NCP</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/38</guid>
      <comments>https://soojae.tistory.com/38#entry38comment</comments>
      <pubDate>Sun, 30 Aug 2020 23:27:40 +0900</pubDate>
    </item>
    <item>
      <title>앵귤러 컴포넌트 생명주기(Angular Component Lifecycle)</title>
      <link>https://soojae.tistory.com/35</link>
      <description>&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;widthContent&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd2Clr/btqGevyj0jF/JUBtJ3IUMyfKoDrTKbkot0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd2Clr/btqGevyj0jF/JUBtJ3IUMyfKoDrTKbkot0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd2Clr/btqGevyj0jF/JUBtJ3IUMyfKoDrTKbkot0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd2Clr%2FbtqGevyj0jF%2FJUBtJ3IUMyfKoDrTKbkot0%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;250&quot; height=&quot;250&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Angular Component Lifecycle&lt;/h2&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;앵귤러에는 8단계의 라이프 사이클이 있습니다. (constructor 함수는 타입스크립트에서 동작하므로 제외) 각 단계는 &lt;b&gt;라이프 사이클 훅 이벤트&lt;/b&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-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;auar_0401.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVEfKP/btqGdtuFkIP/PvNKFW8ywkTF1ssymUIkF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVEfKP/btqGdtuFkIP/PvNKFW8ywkTF1ssymUIkF0/img.png&quot; data-alt=&quot;앵귤러 라이프 사이클&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVEfKP/btqGdtuFkIP/PvNKFW8ywkTF1ssymUIkF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVEfKP%2FbtqGdtuFkIP%2FPvNKFW8ywkTF1ssymUIkF0%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;298&quot; height=&quot;768&quot; data-filename=&quot;auar_0401.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앵귤러 라이프 사이클&lt;/figcaption&gt;
&lt;/figure&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;constructor(Typescript)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;constructor &lt;/b&gt;함수는 앵귤러와 별개로 타입스크립트에서 호출하는 메서드입니다. &lt;br /&gt;&lt;b&gt;constructor &lt;/b&gt;함수에는 컴포넌트에 의존성들(예: 서비스)을 주입(inject)하는 것이 가장 좋습니다. &lt;b&gt;constructor&lt;/b&gt; 함수가 실행된 후에 앵귤러의 라이프 사이클이 실행됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;br /&gt;ngOnChanges&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;constructor &lt;/b&gt;함수 이후에 제일 처음 호출되는 라이프 사이클 훅 이벤트입니다.&lt;br /&gt;자식컴포넌트에서 &lt;b&gt;@Input&lt;/b&gt; 데코레이터를 이용하면 부모 컴포넌트에서 프로퍼티를 전달 받을 수 있습니다. 부모 컴포넌트에서 전달해주는 프로퍼티가 변경되면 &lt;b&gt;ngOnChanges&lt;/b&gt;가 &lt;b&gt;재&lt;/b&gt; &lt;b&gt;호출&lt;/b&gt; 됩니다.&lt;br /&gt;부모 컴포넌트에서 &lt;b&gt;동일한 값을 전달 받거나 전달 받은 값이 객체 내의 프로퍼티만 바꾼 값&lt;/b&gt;일 경우 호출되지 않습니다.&lt;br /&gt;&lt;b&gt;ngOnChanges&lt;/b&gt; 훅 함수는 부모 컴포넌트에서 받은 프로퍼티의 정보를 담고 있는 &lt;b&gt;SimpleChanges &lt;/b&gt;파라미터를 하나 더 받습니다. 이 파라미터는 다음과 같은 프로퍼티를 갖고있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;previousValue &lt;/b&gt;: 이전에 받은&amp;nbsp;프로퍼티의 값을 갖고 있습니다.&lt;br /&gt;&lt;b&gt;currentValue &lt;/b&gt;: 방금 받은 프로퍼티의 값을 갖고 있습니다.&lt;br /&gt;&lt;b&gt;isFirstChange &lt;/b&gt;: 방금 받은 프로퍼티가 첫번째 프로퍼티라면 true, 아니라면 false를 반환합니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;ngOnInit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;ngOnChange&lt;/b&gt; 훅 함수 이후에 호출되고 초기에 &lt;b&gt;한번만 호출&lt;/b&gt;됩니다.&lt;br /&gt;클래스가 가지고있는 프로퍼티와 &lt;b&gt;@Input&lt;/b&gt;을 통해 내려받은 프로퍼티가 모두 초기화가 된 이후에 호출됩니다.&lt;br /&gt;주로 프로퍼티 값 설정, 서비스를 이용해 HTTP통신후 결과값을 프로퍼티에 할당하는 등 화면에 데이터가 표시되기 전 필요한 프로퍼티들을 초기화 시키는데 사용됩니다. &lt;br /&gt;&lt;br /&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;ngDoCheck&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;컴포넌트의 상태 변경을 감지한 후에 &lt;b&gt;항상 호출&lt;/b&gt;됩니다. 그만큼 자주 실행되기 때문에 조심히 사용해야 합니다.&lt;br /&gt;주로 변경 감지 로직 구현, 다른 컴포넌트에 대한 알고리즘을 작성할때 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;ngAfterContentInit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;컴포넌트의 템플릿을 컴포넌트 뷰로 준비하거나 뷰 안에 있는 디렉티브를 준비한 이후에 실행됩니다.&lt;br /&gt;이 방법은 구성요소의 모든 바인딩을 처음으로 점검할 필요가 있을 때 실행한다.&lt;br /&gt;&lt;b&gt;ngDoCheck&lt;/b&gt; 훅 함수가 처음 실행된 직후에 &lt;b&gt;한 번만 호출&lt;/b&gt;됩니다. 이 함수는 기본적으로 자식 컴포넌트 초기화와 연결 되어있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;ngAfterContentChecked&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;ngAfterContentInit&lt;/b&gt; 훅 함수가 실행된 후에 호출됩니다.&lt;br /&gt;앵귤러의 변경 감지 메커니즘에 의해 컴포넌트의 내용(content)의 변경을 확인(check)합니다. 또한 &lt;b&gt;ngDoCheck&lt;/b&gt; 함수가 &lt;b&gt;실행될 때마다 뒤 이어서 호출&lt;/b&gt;됩니다.&lt;br /&gt;이 함수 또한 기본적으로 자식 컴포넌트 초기화와 연결되어 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;ngAfterViewInit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;ngAfterContextChecked&lt;/b&gt; 훅 함수가 실행된 후에 호출됩니다.&lt;br /&gt;컴포넌트 뷰가 완전히 초기화 되었을때(자식 컴포넌트 포함) 실행됩니다. 이 라이프 사이클 훅 함수는 오직 컴포넌트에만 적용됩니다.&lt;br /&gt;자식 컴포넌트까지 완전히 초기화 되었을 때 실행되는 특성으로 인해, 부모 컴포넌트에서 자식 컴포넌트에 접근을 하는(@ViewChild 디렉티브) 함수를 사용할때 이 라이프 사이클 함수에 작성을 해줍니다.&lt;br /&gt;화면에 적용 하는 라이브러리(echart, D3 등)또는 @ViewChild 디렉티브를 사용하려면 이 훅 함수에 코드를 넣어야 하기 때문에 자주 사용하는 편입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;ngAfterViewChecked&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;ngAfterViewInit&lt;/b&gt; 훅 함수가 실행된 후에 호출됩니다. &lt;br /&gt;앵귤러의 변경 감지 메커니즘에 컴포넌트 뷰(자식 컴포넌트 포함)를 확인(check)합니다. &lt;b&gt;ngAfterContentChecked&lt;/b&gt; 훅 함수가 &lt;b&gt;실행될 때마다 이어서 호출&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;ngOnDestroy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&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;라이프 사이클에서 초기에 한번만 호출하는 훅 함수와 변경을 감지할 때마다 호출함수를 나눈다면, 다음과 같이 정리할 수 있겠습니다.&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;widthContent&quot; data-filename=&quot;Untitled Diagram (1).png&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn73CO/btqGdEJ9jiQ/5FevrwxOrLMw8eHKnOC2T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn73CO/btqGdEJ9jiQ/5FevrwxOrLMw8eHKnOC2T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn73CO/btqGdEJ9jiQ/5FevrwxOrLMw8eHKnOC2T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn73CO%2FbtqGdEJ9jiQ%2F5FevrwxOrLMw8eHKnOC2T1%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;501&quot; height=&quot;421&quot; data-filename=&quot;Untitled Diagram (1).png&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 오류가 있으면 알려주세요. 감사합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REFERENCE&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://indepth.dev/if-you-think-ngdocheck-means-your-component-is-being-checked-read-this-article/&quot;&gt;https://indepth.dev/if-you-think-ngdocheck-means-your-component-is-being-checked-read-this-article/&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.freecodecamp.org/news/angular-lifecycle-hooks/&quot;&gt;https://www.freecodecamp.org/news/angular-lifecycle-hooks/&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Angular</category>
      <category>angular</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/35</guid>
      <comments>https://soojae.tistory.com/35#entry35comment</comments>
      <pubDate>Fri, 7 Aug 2020 18:29:29 +0900</pubDate>
    </item>
    <item>
      <title>함수형 프로그래밍 전문가 되기 (Part 6)</title>
      <link>https://soojae.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Charles Scalfani의&amp;nbsp;&lt;a href=&quot;https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-6-db502830403&quot;&gt;So You Want to be a Functional Programmer (Part 6)&lt;/a&gt;를 번역한 게시물입니다.&amp;nbsp;&lt;br /&gt;Thank you Charles Scalfani! Thanks to your writing, I can grow into a better developer.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;widthContent&quot; data-filename=&quot;functional_programming_6_1.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu8G4i/btqF1ogbpDr/tVDmbWut9UoRqwvcgunqPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu8G4i/btqF1ogbpDr/tVDmbWut9UoRqwvcgunqPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu8G4i/btqF1ogbpDr/tVDmbWut9UoRqwvcgunqPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu8G4i%2FbtqF1ogbpDr%2FtVDmbWut9UoRqwvcgunqPk%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;1400&quot; height=&quot;621&quot; data-filename=&quot;functional_programming_6_1.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 프로그래밍 컨셉들을 이해하는 첫 단계가 가장 중요하고 때로는 가장 어려운 단계다.&lt;br /&gt;하지만 올바른 관점으로 접근한다면 그렇게 어렵지 않다.&lt;br /&gt;&lt;br /&gt;이전 게시물 : &lt;a href=&quot;https://soojae.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 1&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/30&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 2&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 3&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 4&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/33&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 5&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;이제 어쩌지?&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_6_2.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB4aev/btqF00UqYWs/CVJpyMcs3G6NokPWtCu4Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB4aev/btqF00UqYWs/CVJpyMcs3G6NokPWtCu4Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB4aev/btqF00UqYWs/CVJpyMcs3G6NokPWtCu4Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB4aev%2FbtqF00UqYWs%2FCVJpyMcs3G6NokPWtCu4Gk%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;638&quot; height=&quot;329&quot; data-filename=&quot;functional_programming_6_2.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 훌륭한 것들을 모두 배웠으니, 당신은 이렇게 생각할 것 이다. &quot;이제 어쩌지? 이제까지 배운 것을 내 프로그래밍에 어떻게 적용시키지?&quot;&lt;br /&gt;&lt;br /&gt;사정에 따라 다르다. 만약 당신이 Elm또는 Haskell처럼 순수 함수형 언어를 할줄 안다면, 이 개념들을 잘 활용할 수 있을 것이고 이 언어들이 잘 활용할 수 있도록 도와줄 것이다.&lt;br /&gt;&lt;br /&gt;만약 대부분의 사람들 처럼, Javascript 같은 명령형 언어를 사용한다면, 그동안 배웠던 것들을 많이 활용 할 수는 있지만, 훨씬 더 많은 연습이 필요할 것이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_6_3.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwVtXG/btqF1SVuOxR/b5aFNQ96IoZdrTr0PvMUdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwVtXG/btqF1SVuOxR/b5aFNQ96IoZdrTr0PvMUdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwVtXG/btqF1SVuOxR/b5aFNQ96IoZdrTr0PvMUdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwVtXG%2FbtqF1SVuOxR%2Fb5aFNQ96IoZdrTr0PvMUdk%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;334&quot; data-filename=&quot;functional_programming_6_3.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;자바스크립트는 함수형 프로그래밍을 할 수 있도록 해주는 많은 특징들을 갖고있다. 순수하진 않지만 약간의 불변성을 갖도록 해줄 수 있고, 라이브러리를 이용한다면 좀 더 높은 불변성을 갖도록 해줄 수 있다.&lt;br /&gt;&lt;br /&gt;이상적이지는 않지만, 만약 사용해야 한다면 함수형 프로그래밍의 이점을 얻지 않을 이유가 없다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;불변성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 고려해봐야 할 것은 불변성이다. ES2015(ES6)에서 나타난 새로운 키워드인 &lt;b&gt;const&lt;/b&gt;가 있다. 이것은 일단 변수를 할당하면, 재할당이 불가능 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595691357303&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = 1;
a = 2; // this will throw a TypeError in Chrome, Firefox or Node
       // but not in Safari (circa 10/2016)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;정의된 a는 상수이다. 그리고 한번 할당된 후에는 재할당이 불가능 하다. 이 이유로 &lt;b&gt;a = 2&lt;/b&gt;는 에러가 발생했다.&lt;br /&gt;&lt;br /&gt;문제는 자바스크립트의 const는 한계가 있다는 것이다. 아래 예제를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595691387651&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = {
    x: 1,
    y: 2
};
a.x = 2; // NO EXCEPTION!
a = {}; // this will throw a TypeError&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;a.x = 2&lt;/b&gt;는 왜 에러가 발생하지 않았을까? 그 이유는 단지 변수 a만 &lt;b&gt;const&lt;/b&gt;키워드로 불변성이 되었을 뿐이기 때문이다.. &lt;b&gt;a.*&lt;/b&gt;은 불변하지 않다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;const&lt;/b&gt;도 한계가 있다는 것을 알 수 있다.&lt;br /&gt;&lt;br /&gt;그럼 위의 코드를 완전한 불변성을 만들 수 있을까?&lt;br /&gt;&lt;br /&gt;불행하게도, 우리는 Immutable.js 라이브러리를 사용하는 방법 밖에 없다. 이 라이브러리는 자바스크립트에게&amp;nbsp;좀더 나은 불변성을 갖게 해주지만, 코드를 자바처럼 보이게 만든다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;커링과 합성 함수&lt;/h4&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;br /&gt;아래 좀더 복잡한 예시를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595696293758&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const f = a =&amp;gt; b =&amp;gt; c =&amp;gt; d =&amp;gt; a + b + c + d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;커링 부분을 직접 작성했다는 것에 주목하자.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;f&lt;/b&gt;함수를 호출할때 우리는 다음과 같이 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595727723839&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log(f(1)(2)(3)(4)); // prints 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Lisp&lt;/b&gt;프로그래머가 위의 많은 괄호를 봤다면 울었을 것이다.&lt;br /&gt;&lt;br /&gt;커링 함수를 작성하기 위한 많은 라이브러리가 있지만, 내가 가장 좋아하는 라이브러리는 &lt;a href=&quot;http://ramdajs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ramda&lt;/a&gt;이다&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Ramda&lt;/b&gt;를 사용하면 아래와 같이 변경할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595727788173&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const f = R.curry((a, b, c, d) =&amp;gt; a + b + c + d);
console.log(f(1, 2, 3, 4)); // prints 10
console.log(f(1, 2)(3, 4)); // also prints 10
console.log(f(1)(2)(3, 4)); // also prints 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;함수 정의는 그리 좋지 않지만, 모든 괄호를 넣을 필요는 없어졌다. 우리가 f를 호출할 때마다 원하는 만큼의 파라미터수를 정할 수 있다는 것에 유의해라.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Ramda&lt;/b&gt;를 사용해서 &lt;a href=&quot;https://soojae.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part3&lt;/a&gt;와 &lt;a href=&quot;https://soojae.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part4&lt;/a&gt;에서 작성했던 &lt;b&gt;mult5AfterAdd10&lt;/b&gt;을&amp;nbsp;개선해보자&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595728272907&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const add = R.curry((x, y) =&amp;gt; x + y);
const mult5 = value =&amp;gt; value * 5;
const mult5AfterAdd10 = R.compose(mult5, add(10));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Ramda&lt;/b&gt;는 &lt;b&gt;R.add&lt;/b&gt;, &lt;b&gt;R.multiply&lt;/b&gt;&amp;nbsp;함수도 있다. 위의 코드를 다시한번 개선 해보자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595728310288&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const mult5AfterAdd10 = R.compose(R.multiply(5), R.add(10));&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;Map, Filter 그리고 Reduce&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Ramda&lt;/b&gt; 라이브러리만의 &lt;b&gt;map&lt;/b&gt;, &lt;b&gt;filter&lt;/b&gt;, 그리고 &lt;b&gt;reduce&lt;/b&gt;가 있다. 비록 이 함수들이 바닐라 자바스크립트의 &lt;b&gt;Array.prototype&lt;/b&gt;에도 있지만, &lt;b&gt;Ramda&lt;/b&gt;&amp;nbsp;라이브러리는 &lt;b&gt;커링&lt;/b&gt;이 적용된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595728879330&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const isOdd = R.flip(R.modulo)(2);
const onlyOdd = R.filter(isOdd);
const isEven = R.complement(isOdd);
const onlyEven = R.filter(isEven);
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(onlyEven(numbers)); // prints [2, 4, 6, 8]
console.log(onlyOdd(numbers)); // prints [1, 3, 5, 7]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;R.modulo&lt;/b&gt;는 2개의 파라미터를 받는다. 첫번째 파라미터는 &lt;b&gt;나눠지는 수&lt;/b&gt;이고, 두번째 파라미터는 &lt;b&gt;나누는 수&lt;/b&gt;이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;isOdd &lt;/b&gt;함수는 2로 나눈 나머지 값이다. 나머지가 0이면 &lt;b&gt;falsey&lt;/b&gt;(홀수가 아님), 나머지가 1이면 &lt;b&gt;truthy&lt;/b&gt;(홀수)이다. 우리는 2를 나누는 수로 지정하기 위해 &lt;b&gt;modulo&lt;/b&gt;의 첫번 째와 두번째 파라미터를 뒤바꾼다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;isEven&lt;/b&gt; 함수는 단지 &lt;b&gt;isOdd&lt;/b&gt;&amp;nbsp;함수의 보수(complement)일 뿐이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;onlyOdd&lt;/b&gt; 함수는 &lt;b&gt;isOdd&lt;/b&gt;의 조건자(boolean 값을 반환)가 있는 &lt;b&gt;filter&lt;/b&gt; 함수이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;onlyEven&lt;/b&gt;&amp;nbsp;함수는 &lt;b&gt;isEven&lt;/b&gt;을 조건자로 사용하는 &lt;b&gt;filter&lt;/b&gt;이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;numbers&lt;/b&gt;를 &lt;b&gt;onlyEven&lt;/b&gt;, &lt;b&gt;onlyOdd&lt;/b&gt;에 넘겨줄 때, &lt;b&gt;isEven&lt;/b&gt; 그리고 &lt;b&gt;isOdd&lt;/b&gt;는 그들의 마지막 파라미터를 받고 우리가 기대한 숫자배열을 리턴한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;자바스크립트의 단점들&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_6_4.png&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWcx18/btqF1pfqZti/VWLgT4qyxZMhLnDuw4kkvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWcx18/btqF1pfqZti/VWLgT4qyxZMhLnDuw4kkvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWcx18/btqF1pfqZti/VWLgT4qyxZMhLnDuw4kkvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWcx18%2FbtqF1pfqZti%2FVWLgT4qyxZMhLnDuw4kkvK%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;480&quot; height=&quot;270&quot; data-filename=&quot;functional_programming_6_4.png&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;대부분의 프런트엔드 개발자들은 브라우저에서 자바스크립트만을 사용한다. 왜냐하면 Javascript가 오랫동안 유일한 선택이었기 때문이다. 하지만 개발자들은 이제 자바스크립트를 직접 쓰는 것에서 멀어지고 있다.&lt;br /&gt;&lt;br /&gt;대신에 개발자들은 다른 언어로 작성한 후, 컴파일 하고, 더 정확하게 자바스크립트로 변환한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;CoffeeScript&lt;/b&gt;는 이런 언어들중에 가장 첫번째로 나온 언어이다. 최근에는 Angular 2에서 채택한 타입스크립트가 있다. Babel 또한 자바스크립트로 변환기(transpiler)로 고려할만 하다.&lt;br /&gt;&lt;br /&gt;시간이 지날수록 점점 더 사람들은 이런 방식으로 프로그래밍을 하고 있다.&lt;br /&gt;하지만 이 언어들은 자바스크립트로부터 시작된 언어들이고, 좀더 나아졌을 뿐이다. 순수 함수형 언어에서 자바스크립트로 변환하는 방법은 어떨까?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;Elm&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_6_5.png&quot; data-origin-width=&quot;256&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRifIy/btqF16e8yMC/aJhYH5W2pSOLkeX6Qzry80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRifIy/btqF16e8yMC/aJhYH5W2pSOLkeX6Qzry80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRifIy/btqF16e8yMC/aJhYH5W2pSOLkeX6Qzry80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRifIy%2FbtqF16e8yMC%2FaJhYH5W2pSOLkeX6Qzry80%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;256&quot; height=&quot;256&quot; data-filename=&quot;functional_programming_6_5.png&quot; data-origin-width=&quot;256&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이번 시리즈에서 Elm이 함수형 프로그래밍을 이해하는데 도움을 줄 것이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;근데 Elm이 뭐야? 어떻게 쓰는 건데?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Elm은 순수 함수형 언어이다. TEA(이 아키텍쳐는 Redux 개발자들에게 영감을 주었다.)라고 불리는 Elm 아키텍쳐를 사용하여 웹 응용 프로그램을 만드는데 사용할 수 있다.&lt;br /&gt;&lt;br /&gt;Elm 프로그램은 런타임 에러가 &lt;b&gt;없다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Elm은 창시자인 Evan Czapliki가 일하는 &lt;a href=&quot;https://www.noredink.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NoRedInk&lt;/a&gt; 회사에서 프로덕션으로 사용된다.(그는 &lt;a href=&quot;https://prezi.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Prezi&lt;/a&gt;에서 일하기도 했다.)&lt;br /&gt;&lt;br /&gt;더 자세한 정보는 NoredInk 출신의 Richard Feldman의 &lt;a href=&quot;https://www.youtube.com/watch?v=R2FtMbb-nLs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;6 Months of Elm in Production&lt;/a&gt; 강연을 보아라.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;그럼 나의 모든 자바스크립트 코드를 Elm으로 교체해야 하나요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그럴필요는 없고 점차적으로 변경해가면 된다. 더 공부를 하고싶다면 다음 블로그를 보아라. &lt;a href=&quot;https://elm-lang.org/news/how-to-use-elm-at-work&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;How to use Elm at Work&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;Elm을 배워야 하는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 순수 함수 언어로 프로그래밍을 하는 것은 제한적이기도 하고 자유롭기도 하다. 그것은 당신이 할 수 있는 것을 제한하지만 동시에 버그나, 나쁜 디자인으로부터 자유롭게 해준다. 모든 Elm은 &lt;b&gt;Functionally Reactive Model&lt;/b&gt;인 Elm 아키텍쳐를 따르기 때문이다.&lt;br /&gt;&lt;br /&gt;2. 함수형 언어는 당신이 더 좋은 프로그래머가 될 수 있도록 해준다. 이 글에 있는 내용들은 빙산에 일각이다. 당신은 프로그램을 경량화하고 안정되게 동작시키기 위해선 좀더 공부할 필요가 있다.&lt;br /&gt;&lt;br /&gt;3. 자바스크립트라는 언어를 만드는데 10일밖에 걸리지 않았다. 하지만 20년간 다소 함수적이고, 다소 객체지향 적이며, 완전히 명령형 프로그래밍 언어가 되었다. Elm은 수십년동안 수학과 컴퓨터 과학 분야에서 활동해온 Haskell 커뮤니티에서 30년간 배운 것을 이용해 디자인 되었다.&lt;br /&gt;Elm 아키텍쳐(TEA)는 수십년간 디자인되고 정제되었다. 그리고 Evan의 논문인 Functional Reactive Programming의 결과물이다. 이 디자인의 공식화에 들어간 사고(thinking)의 수준의 진가는 &lt;a href=&quot;https://www.youtube.com/watch?v=Agu6jipKfYw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Controlling Time and Space&lt;/a&gt;를 보면 알 수 있다.&lt;br /&gt;&lt;br /&gt;4. Elm은 개발자들의&amp;nbsp;더 나은 삶을 목표로&amp;nbsp;프런트엔드 개발자에 의해 디자인 되었다. &lt;a href=&quot;https://www.youtube.com/watch?v=oYk8CKH7OhE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Let's Be Mainstream&lt;/a&gt;를 보면 Elm의 목표를 좀 더 이해할 수 있을 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;미래에는?&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_6_6.png&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rzWEH/btqF2R27cHn/Ku9tZT6yKS8hlVlvPznymK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rzWEH/btqF2R27cHn/Ku9tZT6yKS8hlVlvPznymK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rzWEH/btqF2R27cHn/Ku9tZT6yKS8hlVlvPznymK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrzWEH%2FbtqF2R27cHn%2FKu9tZT6yKS8hlVlvPznymK%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;562&quot; data-filename=&quot;functional_programming_6_6.png&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;미래에 대해서 단정짓는 것은 불가능 하다. 하지만 약간의 추측은 할 수 있다. 아래는 내가 추측한 것들이다.&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style3&quot;&gt;자바스크립트로 컴파일 되는 언어들이 주목 받을 것이다.&lt;br /&gt;&lt;br /&gt;40년이상 존재해온 함수형 프로그래밍의 개념들은 소프트웨어의 복잡성 문제를 해결하기 위해 재평가 될 것이다.&lt;br /&gt;&lt;br /&gt;하드웨어의 상태(예: 대량의 값싼 메모리와 빠른 프로세서)는 함수형 기술들을 가능하게 할 것이다.&lt;br /&gt;&lt;br /&gt;CPU는 더 이상 빨라지지 않겠지만 코어 수는 계속 증가할 것이다.&lt;br /&gt;&lt;br /&gt;가변 상태는 복잡한 시스템에서 가장 큰 문제중 하나로 인식될 것이다.&lt;/blockquote&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;br /&gt;&lt;br /&gt;내 목표는 다른 사람들이 이런 것들을 더 쉽게 배우고, 더 나은 프로그래머가 될 수 있도록 도와서 그들이 미래에 더 경쟁력 있는 직업을 가질수 있도록 돕는 것이다.&lt;br /&gt;&lt;br /&gt;Elm이 미래에 거대한 언어가 될 것이라는 나의 예측이 틀렸다고 하더라도, 나는 함수형 프로그래밍과 Elm이 미래에 그 궤도에 오를 것이라고 확실하게 말할 수 있다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이 글들을 읽은 후에 함수형 프로그래밍의 개념에 대한 당신의 능력과 이해력에 대해 더욱 자신감을 갖기를 바란다.&lt;br /&gt;&lt;br /&gt;앞으로의 당신의 노력에 행운을 빈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 번역 오류가 있으면 알려주세요. 감사합니다.&lt;/p&gt;
&lt;!-- 레이어 팝업 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Knowledge/Web</category>
      <category>Functional Programming</category>
      <category>javascript</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/34</guid>
      <comments>https://soojae.tistory.com/34#entry34comment</comments>
      <pubDate>Sun, 26 Jul 2020 00:14:56 +0900</pubDate>
    </item>
    <item>
      <title>함수형 프로그래밍 전문가 되기 (Part 5)</title>
      <link>https://soojae.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 글은 Charles Scalfani의&amp;nbsp;&lt;a href=&quot;https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-5-c70adc9cf56a&quot;&gt;So You Want to be a Functional Programmer (Part 5)&lt;/a&gt;를 번역한 게시물입니다.&amp;nbsp;&lt;br /&gt;Thank you Charles Scalfani! Thanks to your writing, I can grow into a better developer.&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-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_5_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uWX1P/btqFNawoi8Y/M3D2bJtXEyAq1K33XmCmo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uWX1P/btqFNawoi8Y/M3D2bJtXEyAq1K33XmCmo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uWX1P/btqFNawoi8Y/M3D2bJtXEyAq1K33XmCmo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuWX1P%2FbtqFNawoi8Y%2FM3D2bJtXEyAq1K33XmCmo0%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;700&quot; height=&quot;310&quot; data-filename=&quot;functional_programming_5_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이전 게시물 : &lt;a href=&quot;https://soojae.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 1&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/30&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 2&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 3&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 4&lt;/a&gt;&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;참조 투명성&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_5_2.png&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMfYS/btqFNQjqnJo/lQ2K2CI7UAbD9hCqW5IjH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMfYS/btqFNQjqnJo/lQ2K2CI7UAbD9hCqW5IjH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMfYS/btqFNQjqnJo/lQ2K2CI7UAbD9hCqW5IjH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMfYS%2FbtqFNQjqnJo%2FlQ2K2CI7UAbD9hCqW5IjH0%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;444&quot; height=&quot;270&quot; data-filename=&quot;functional_programming_5_2.png&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;270&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;참조 투명성&lt;/b&gt;은 순수함수가 표현식으로 안전하게 대체될 수 있다고 설명하는 멋진 용어이다. 아래 예제가 이해하는데 도움이 될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;대수학에서 다음 공식을 봤을 때,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595070124116&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;y = x + 10&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 x 값이 다음과 같을 때&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595070131471&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;x = 3&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;x값을 방정식에 대입하여 다음과 같은 식을 얻을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595070140888&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;y = 3 + 10&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;방정식이 아직 유효하다는 것에 주목해라. 우리는 순수함수로도 위와 같이 대체를 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기 Elm로 작성한 문자열 주위에 작은 따옴표를 넣는 함수가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595070148964&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;quote str =
    &quot;'&quot; ++ str ++ &quot;'&quot;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 위의 함수를 사용하는 코드를 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595070205710&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;findError key =
    &quot;Unable to find &quot; ++ (quote key)&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;findError&lt;/b&gt;는&amp;nbsp;&lt;b&gt;key &lt;/b&gt;검색을 실패할때 오류 메시지를 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;quote &lt;/b&gt;함수가 순수하기 때문에, 우리는 간단히 함수를&amp;nbsp;&lt;b&gt;quote &lt;/b&gt;함수의 본문 (')으로 대체 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595070727268&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;findError key =
   &quot;Unable to find &quot; ++ (&quot;'&quot; ++ key ++ &quot;'&quot;)&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;나는&amp;nbsp;이것을 &lt;b&gt;Reverse Refactoring&lt;/b&gt;이라고 부른다(그 편이 이해가 잘된다). 이 프로세스는 프로그래머 또는 프로그램이 코드를 추론하기 위해 사용할 수 있다. (예: 컴파일러 또는 테스트 프로그램)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Reverse Refactoring&lt;/b&gt;은특히 재귀함수에 대한 추론을 할때 도움이 된다.&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a id=&quot;user-content-실행-순서&quot; href=&quot;https://github.com/SooJae/Blog/blob/master/Javascript/Functional_Programming/5/%EA%B8%B0%EB%B3%B8%20%ED%95%A8%EC%88%98%ED%98%95%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D5.md#%EC%8B%A4%ED%96%89-%EC%88%9C%EC%84%9C&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;실행 순서&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_5_3.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pxZr1/btqFNPkz1oA/y1mQWSH2okmHemuaJR9Oh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pxZr1/btqFNPkz1oA/y1mQWSH2okmHemuaJR9Oh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pxZr1/btqFNPkz1oA/y1mQWSH2okmHemuaJR9Oh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpxZr1%2FbtqFNPkz1oA%2Fy1mQWSH2okmHemuaJR9Oh1%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; data-filename=&quot;functional_programming_5_3.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;대부분의 프로그램들은 싱글 스레드다. 즉 한 번에 하나의 코드만 실행 된다. 프로그램이 멀티스레드 일지라도 대부분의 스레드는 I/O 작업(파일, 네트워크 기타 등등)이 완료될 때 까지 차단된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이것이 우리가 코드를 작성할 때 자연스럽게 순차적인 순서(ordered steps) 관점으로 생각하는 이유중 하나이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style3&quot;&gt;1. 빵을 꺼내라&amp;nbsp;&lt;br /&gt;2. 빵 2조각을 토스트 기계에 넣어라&amp;nbsp;&lt;br /&gt;3. 굽기를 선택해라&amp;nbsp;&lt;br /&gt;4. 레버를 밑으로 내려라&amp;nbsp;&lt;br /&gt;5. 토스트가 올라올때까지 기다려라&amp;nbsp;&lt;br /&gt;6. 토스트를 꺼내라&amp;nbsp;&lt;br /&gt;7. 버터를 꺼내라&amp;nbsp;&lt;br /&gt;8. 버터 칼을 꺼내라&amp;nbsp;&lt;br /&gt;9. 토스트에 버터를 발라라&lt;/blockquote&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 예제에서 2가지 독립적인 동작이 있다. 버터를 꺼내는 것과, 빵을 굽는 것이다. 이들은 9단계가 되서야 상호의존적이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;우리는 7, 8 단계와 1 ~ 6 단계를 동시에 진행 할 수 있다. 왜냐하면 서로에게 독립적인 행동들이기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;하지만 동시에 진행하게 되면, 우리는 혼란에 빠진다. 아래의 예시를 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style3&quot;&gt;Thread 1&amp;nbsp;&lt;br /&gt;--------&amp;nbsp;&lt;br /&gt;1. 빵을 꺼내라&amp;nbsp;&lt;br /&gt;2. 빵 2조각을 토스트 기계에 넣어라&amp;nbsp;&lt;br /&gt;3. 굽기를 선택해라&amp;nbsp;&lt;br /&gt;4. 레버를 밑으로 내려라&amp;nbsp;&lt;br /&gt;5. 토스트가 올라올때까지 기다려라&amp;nbsp;&lt;br /&gt;6. 토스트를 꺼내라&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Thread 2&amp;nbsp;&lt;br /&gt;--------&amp;nbsp;&lt;br /&gt;1. 버터를 꺼내라&amp;nbsp;&lt;br /&gt;2. 버터 칼을 꺼내라&amp;nbsp;&lt;br /&gt;3. &lt;b&gt;Thread 1이 끝날때까지 기다려라&amp;nbsp;&lt;/b&gt;&lt;br /&gt;4. 토스트에 버터를 발라라&lt;/blockquote&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만약 스레드 1이 실패 하면 스레드 2는 어떤 일이 발생 할까? 두 스레드들을 조율하는 메커니즘은 무엇인가? 누가 토스트의 주인인가? 스레드 1? 스레드 2? 아니면 둘다?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이러한 복잡성에 대해 생각하지 않고, 차라리 우리의 프로그램이 단 하나의 스레드만 사용하도록 하는 것이 더 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러나 프로그램의 효율을 가능한 최대로 짜내야 한다면, 우리는 멀티스레딩 소프트웨어를 만드는 것에 엄청난 노력을 기울여야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러나 멀티 스레딩에는 2가지 주요한 문제점이 있다. 첫번 째, 멀티 스레드 프로그램은 쓰기, 읽기, 추론, 테스트, 디버그가 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;두번째, 자바스크립트 같은 몇몇 언어들은 멀티스레딩을 지원하지 않는다. 지원 하더라도 성능이 좋지 않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러나 순서 상관 없이 모든 것을 병렬로 처리되면 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이상한 소리로 들릴지도 모르지만, 그렇게 나쁜 방법은 아니다. 이를 이해하기 위해 Elm 코드를 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595070928640&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;buildMessage message value =
    let
        upperMessage =
            String.toUpper message
        quotedValue =
            &quot;'&quot; ++ value ++ &quot;'&quot;
    in
        upperMessage ++ &quot;: &quot; ++ quotedValue&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;buildMessage&lt;/b&gt;는&amp;nbsp;&lt;b&gt;message&lt;/b&gt;와&amp;nbsp;&lt;b&gt;value&lt;/b&gt;를 받은 뒤 대문자&amp;nbsp;&lt;b&gt;message&lt;/b&gt;와 콜론(:), 그리고 작은 따옴표(')안의&amp;nbsp;&lt;b&gt;value&lt;/b&gt;를 생산한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;병렬로 처리하기 위해서는&lt;b&gt; upperMessage&lt;/b&gt;와&amp;nbsp;&lt;b&gt;quotedValue&lt;/b&gt;가 독립적이여야 한다. 그렇다면 독립적이기 위해서는 어떤 조건이 필요할까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;독립적이기 위해서는 2가지를 충족시켜야 한다. &lt;br /&gt;첫 번째, 순수 함수여야 한다. 순수함수는 실행될 때 서로의 실행에 영향을 끼치지 않기 때문에 독립적이기 위해서 중요하다. &lt;br /&gt;두 번째, 한 함수의 출력 값이 다른 함수의 입력 값이 되면 안된다. 만약 이와 같은 상황이 일어난다면, 첫번째 스레드가 끝날 때까지 두번째 스레드가 실행되지 못하고 기다려야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 코드에서&amp;nbsp;&lt;b&gt;upperMessage&lt;/b&gt;와&amp;nbsp;&lt;b&gt;quotedValue&lt;/b&gt;는 둘다 순수 함수고, 다른 하나의 출력 값을 필요로 하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러므로 이 두 함수는&amp;nbsp;순서에 상관없이&amp;nbsp;실행이 잘 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;컴파일러는 프로그래머의 도움없이 알아서 결정 할 수 있다. 하지만 이것은 부작용의 영향을 파악하는 것이 어렵기 때문에 순수 함수 언어에서만 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;순수 함수 언어의 실행 순서는 컴파일러에 의해 결정된다.&lt;/span&gt;&lt;/blockquote&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이것은 CPU 속도가 빨라지지 않고 있는 현재 시점에선 굉장한 이점이 있다. CPU 속도가 빨라지지 않는 대신에 코어수가 점점 더 증가하고 있는데, 이는 하드웨어 레벨에서 코드를 병렬로 실행시킬 수 있다는 것을 뜻한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;불행하게도, 명령형 언어에서는 매우 거친 수준을 제외하고 이러한 코어 수 증가의 장점을 완전히 활용할 수 없다. 완전히 활용하려면 프로그램의 구조를 획기적으로 바꿔야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;순수 함수형 언어를 사용하면 코드를 한줄도 변경하지 않고도 미세한 레벨의 CPU 코어를 자동으로 활용할 수 있다.&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a id=&quot;user-content-타입-명시&quot; href=&quot;https://github.com/SooJae/Blog/blob/master/Javascript/Functional_Programming/5/%EA%B8%B0%EB%B3%B8%20%ED%95%A8%EC%88%98%ED%98%95%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D5.md#%ED%83%80%EC%9E%85-%EB%AA%85%EC%8B%9C&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;타입 명시&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_5_4.png&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MNGNe/btqFOLPf9tZ/8Emm3vZkIdnOPn3XKKp2Y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MNGNe/btqFOLPf9tZ/8Emm3vZkIdnOPn3XKKp2Y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MNGNe/btqFOLPf9tZ/8Emm3vZkIdnOPn3XKKp2Y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMNGNe%2FbtqFOLPf9tZ%2F8Emm3vZkIdnOPn3XKKp2Y1%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;480&quot; height=&quot;480&quot; data-filename=&quot;functional_programming_5_4.png&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;480&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;전적 타입 언어에서, 타입은 인라인(inline)으로 정의된다. 이 뜻을 이해 하기 위해 아래의 Java 코드를 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071494390&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static String quote(String str) {
    return &quot;'&quot; + str + &quot;'&quot;;
}&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;함수 정의와 타입이 어떻게 함께 인라인이 되는지 주목해라. 제네릭을 사용한다면 더 구별하기 힘들어 질 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;private final &lt;b&gt;Map&amp;lt;Integer, String&amp;gt;&lt;/b&gt; getPerson(&lt;b&gt;Map&amp;lt;String, String&amp;gt;&lt;/b&gt; people, &lt;b&gt;Integer&lt;/b&gt; personId) {&lt;br /&gt;// ...&lt;br /&gt;}&lt;/span&gt;&lt;/blockquote&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;나는 타입들을 Bold체로 표시해서 구별하기 쉽게 했지만, 그것들은 여전히 함수 정의에 방해가 된다. 변수의 이름을 찾으려면 주의 깊게 읽어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;동적 타입 언어에서 이것은 문제가 되지 않는다. 자바스크립트에서 코드를 다음과 같이 작성 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071700507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var getPerson = function(people, personId) {
    // ...
};&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;타입의 정보가 없는 것이 문제가 되지 않는다면 읽기 더 쉬워진다. &lt;br /&gt;단 하나의 문제는 타입 안전성을 포기한 것이다. 우리는 쉽게 이 파라미터들을 거꾸로 전달할 수도 있다. 즉, &lt;b&gt;people&lt;/b&gt;을 숫자타입으로, &lt;b&gt;personId&lt;/b&gt;를 객체타입으로 전달할 수 있다. 이렇게 변경을 하더라도 에러가 발생하지 않는다. 즉 프로그램상 문제가 있지만, 어디에서 에러가 발생했는지 발견하기 힘들다는 것을 뜻한다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt; 자바에서는 이런 현상이 일어나지 않는다. 컴파일 자체가 되지 않기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만약 우리가 자바스크립트의 유연함과 자바의 안정성을 모두 가질 수 있다면 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그렇게 할 수 있다. 아래 Elm에서의 함수 타입 명시를 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071728464&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;add : Int -&amp;gt; Int -&amp;gt; Int
add x y =
    x + y&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;각 라인에서 타입 정보가 어떻게 작성되었는지 주목해라. 띄어쓰기는 분리를 하기 위함이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;당신은 타입 명시에 오타가 있다고 생각할 수도 있다. 나는 내가 저 코드를 처음봤을때는 첫 -&amp;gt; 가 ,여야 한다고 생각했다. 하지만 오타가 아니었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;괄호 함축(implied parentheses)을 사용하면 좀더 이해하기 쉬울 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071778961&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;add : Int -&amp;gt; (Int -&amp;gt; Int)&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;add&lt;/b&gt;&amp;nbsp;함수는 1개의&amp;nbsp;&lt;b&gt;Int&lt;/b&gt;&amp;nbsp;파라미터를 사용한다. 그리고 1개의&amp;nbsp;&lt;b&gt;Int&lt;/b&gt;&amp;nbsp;파라미터를 받고&amp;nbsp;&lt;b&gt;Int&lt;/b&gt;를 리턴하는 함수를 리턴한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기 괄호 함축(implied parentheses)을 사용한 또 다른 타입 명시 예제를 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071794213&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;doSomething : String -&amp;gt; (Int -&amp;gt; (String -&amp;gt; String))
doSomething prefix value suffix =
    prefix ++ (toString value) ++ suffix&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;doSomething&lt;/b&gt;&amp;nbsp;함수는 1개의&amp;nbsp;&lt;b&gt;String&lt;/b&gt;&amp;nbsp;파라미터를 받아서 1개의&amp;nbsp;&lt;b&gt;Int&lt;/b&gt;&amp;nbsp;파라미터를 받는 함수를 리턴하고, 1개의&amp;nbsp;&lt;b&gt;String&lt;/b&gt;&amp;nbsp;파라미터를 받고&amp;nbsp;&lt;b&gt;String&lt;/b&gt;을 리턴한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;어떻게 모든 함수들은 1개의 파라미터만 받을까? 그 이유는 Elm에서의 모든 함수는 커링 함수이기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;항상 코드는 오른쪽으로 진행되기 때문에 괄호가 필요한 것은 아니다. 그래서 아래와 같이 간단하게 작성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071843187&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;doSomething : String -&amp;gt; Int -&amp;gt; String -&amp;gt; String&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;괄호는 함수를 파라미터로서 전달할때 필수적이다. 괄호가 없다면 타입 명시가 애매모호해진다. 아래 예제를 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071906100&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;takes2Params : Int -&amp;gt; Int -&amp;gt; String
takes2Params num1 num2 =
    -- do something&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위의 코드는 아래의 코드와 매우 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071919262&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;takes1Param : (Int -&amp;gt; Int) -&amp;gt; String
takes1Param f =
    -- do something&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;takes2Param&lt;/b&gt;&amp;nbsp;함수는 2개의 파라미터&amp;nbsp;&lt;b&gt;Int&lt;/b&gt;와 또 다른&amp;nbsp;&lt;b&gt;Int&lt;/b&gt;를 받는다. 반면에&amp;nbsp;&lt;b&gt;takes1Param&lt;/b&gt;&amp;nbsp;함수는 한개의 (Int -&amp;gt; Int) 파라미터를 받는 함수이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기&amp;nbsp;&lt;b&gt;map&lt;/b&gt;을 위한 타입 명시가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595071964490&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;map : (a -&amp;gt; b) -&amp;gt; List a -&amp;gt; List b
map f list =
    // ...&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위 코드의 경우 괄호가 필요하다. 왜냐하면 함수 f의 타입이 &lt;b&gt;(a -&amp;gt; b)&lt;/b&gt;이기 때문이다. 즉, 이 함수는 타입이 &lt;b&gt;a&lt;/b&gt;인 파라미터를 받고, 타입이 &lt;b&gt;b&lt;/b&gt;인 결과값을 리턴한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기&amp;nbsp;&lt;b&gt;a&lt;/b&gt;라는 어떤 타입이 있다. 타입이 대문자라면 그것은 명시적 타입(예 : String)이다. &lt;br /&gt;타입이 소문자라면 그것은 어떤 타입이든 될 수 있다. 여기서 a의 타입이 String일수도 있지만, Int 일수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만약 &lt;b&gt;(a -&amp;gt; a) &lt;/b&gt;라면, 그것은 input 타입과 output 타입이 &lt;b&gt;반드시&lt;/b&gt; 같아야 한다는 것을 뜻한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위 코드의 &lt;b&gt;map&lt;/b&gt;의 경우, &lt;b&gt;(a -&amp;gt; b)&lt;/b&gt;로 되어있는데, 이는 a(input)와 b(output)가 같은 타입 일수도, 다른 타입 일수도 있다는 것을 뜻한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;일단&amp;nbsp;&lt;b&gt;a&lt;/b&gt;의 타입이 결정되면,&amp;nbsp;&lt;b&gt;a&lt;/b&gt;는 전체 구문에서 반드시 동일해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;(Int -&amp;gt; String) -&amp;gt; List Int -&amp;gt; List String&lt;/span&gt;&lt;/blockquote&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위에서 모든 &lt;b&gt;a&lt;/b&gt;가 &lt;b&gt;Int&lt;/b&gt;로 대체되었다. 그리고 &lt;b&gt;b&lt;/b&gt; 또한 &lt;b&gt;String&lt;/b&gt; 으로 대체되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;List Int&lt;/b&gt;&amp;nbsp;리스트에&amp;nbsp;&lt;b&gt;Int&lt;/b&gt;타입 들이 포함되었다는 뜻이고,&amp;nbsp;&lt;b&gt;List String&lt;/b&gt;은 리스트에&amp;nbsp;&lt;b&gt;String&lt;/b&gt;타입들이 포함되어 있다는 것을 뜻한다. 만약 당신이 자바 또는 다른 언어에서 제네릭 타입을 사용한다면 이 개념은 친숙할 것이다.&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;머리 아파! 이제 한계야!&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_5_5.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UQKl2/btqFPrQZfY6/mww4KFKpKw9keYXgn6saZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UQKl2/btqFPrQZfY6/mww4KFKpKw9keYXgn6saZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UQKl2/btqFPrQZfY6/mww4KFKpKw9keYXgn6saZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUQKl2%2FbtqFPrQZfY6%2Fmww4KFKpKw9keYXgn6saZk%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;300&quot; height=&quot;225&quot; data-filename=&quot;functional_programming_5_5.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;오늘은 여기까지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&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;a href=&quot;https://soojae.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 6&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 번역 오류가 있으면 알려주세요. 감사합니다.&lt;/p&gt;
&lt;!-- 레이어 팝업 --&gt;</description>
      <category>Knowledge/Web</category>
      <category>Functional Programming</category>
      <category>javascript</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/33</guid>
      <comments>https://soojae.tistory.com/33#entry33comment</comments>
      <pubDate>Sun, 19 Jul 2020 23:04:03 +0900</pubDate>
    </item>
    <item>
      <title>함수형 프로그래밍 전문가 되기 (Part 4)</title>
      <link>https://soojae.tistory.com/32</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Charles Scalfani의&amp;nbsp;&lt;a href=&quot;https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-4-18fbe3ea9e49#.iddazh5wz&quot;&gt;So You Want to be a Functional Programmer (Part 4)&lt;/a&gt;를 번역한 게시물입니다.&amp;nbsp;&lt;br /&gt;Thank you Charles Scalfani! Thanks to your writing, I can grow into a better developer.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_4_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDiFE2/btqFLtAXoY6/KsdtAFMC56fX2KrKKEvr9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDiFE2/btqFLtAXoY6/KsdtAFMC56fX2KrKKEvr9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDiFE2/btqFLtAXoY6/KsdtAFMC56fX2KrKKEvr9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDiFE2%2FbtqFLtAXoY6%2FKsdtAFMC56fX2KrKKEvr9k%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;700&quot; height=&quot;310&quot; data-filename=&quot;functional_programming_4_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&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;br /&gt;&lt;br /&gt;이전 게시물 : &lt;a href=&quot;https://soojae.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 1&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/30&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 2&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 3&lt;/a&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_4_2.png&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dX8G1x/btqFIGPYMns/FWB0v2RCPqsCd3UOoxGu6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dX8G1x/btqFIGPYMns/FWB0v2RCPqsCd3UOoxGu6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dX8G1x/btqFIGPYMns/FWB0v2RCPqsCd3UOoxGu6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdX8G1x%2FbtqFIGPYMns%2FFWB0v2RCPqsCd3UOoxGu6K%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;338&quot; height=&quot;239&quot; data-filename=&quot;functional_programming_4_2.png&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://soojae.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 3&lt;/a&gt;의 마지막 부분에서 &lt;b&gt;mult5 &lt;/b&gt;함수는 1개의 파라미터를 받았고, &lt;b&gt;add &lt;/b&gt;함수는 2개의 파라미터를 받았기 때문에 &lt;b&gt;mult5 &lt;/b&gt;함수와 &lt;b&gt;add10 &lt;/b&gt;함수를 합성할 때 문제가 발생했었다.&lt;br /&gt;&lt;br /&gt;우리는 모든 함수가 하나의 파라미터만 받도록 제한함으로써 이 문제를 해결할 수 있다.&lt;br /&gt;&lt;br /&gt;나를 믿어라. 이건 그렇게 나쁜 방법이 아니다&lt;br /&gt;&lt;br /&gt;우리는 2개의 파라미터를 사용하지만 한 번에 1개만 사용하는&lt;b&gt; add &lt;/b&gt;함수를 작성한다. &lt;b&gt;커링&lt;/b&gt; 함수는 이것을 가능하게 해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;커링 함수는 한 번에 1개의 파라미터만 받는 함수이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;커링 함수는 우리가 &lt;b&gt;mult5&lt;/b&gt; 함수와 합성하기 전에, 첫 번째 파라미터를 &lt;b&gt;add&lt;/b&gt; 함수에게 준다. 그러고 나서 &lt;b&gt;mult5AfterAdd10 &lt;/b&gt;함수가 호출될 때, &lt;b&gt;add&lt;/b&gt; 함수는 두 번째 파라미터를 받게 된다.&lt;br /&gt;&lt;br /&gt;이제 자바스크립트에서 &lt;b&gt;add&lt;/b&gt; 함수에 커링을 적용한 것을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594912331786&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var add = x =&amp;gt; y =&amp;gt; x + y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 &lt;b&gt;add&lt;/b&gt; 함수는 지금은 하나의 파라미터만 받고 나중에 또 다른 하나의 파라미터를 받는 함수이다. &lt;br /&gt;&lt;br /&gt;자세히 말하면, &lt;b&gt;add&lt;/b&gt; 함수는 1개의 파라미터 &lt;b&gt;x&lt;/b&gt;를 받고, 1개의 파라미터 &lt;b&gt;y&lt;/b&gt;를 받는 함수를 리턴하는데, 이는 결국 &lt;b&gt;x와 y를 합하는 결과&lt;/b&gt;를 리턴하게 된다.&lt;br /&gt;&lt;br /&gt;이제 우리는 이&amp;nbsp;&lt;b&gt;add&lt;/b&gt; 함수를 이용하여 개선된 &lt;b&gt;mult5AfterAdd10 &lt;/b&gt;함수를 만들 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594912998121&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var compose = (f, g) =&amp;gt; x =&amp;gt; f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 합성함수는 2개의 파라미터 &lt;b&gt;f&lt;/b&gt;와 &lt;b&gt;g&lt;/b&gt;를 받는다. 그리고 &lt;b&gt;x&lt;/b&gt;라는 1개의 파라미터를 받는 함수를 리턴한다. 이 함수가 호출될 때 &lt;b&gt;g&lt;/b&gt;함수에 &lt;b&gt;x&lt;/b&gt;를 적용한 후에 그 결과를 &lt;b&gt;f&lt;/b&gt;함수에 적용한다.&lt;br /&gt;&lt;br /&gt;그래서 우리가 한건 정확히 뭘까? 단순한 구식 &lt;b&gt;add&lt;/b&gt; 함수를 커링 방식의 버전으로 바꿨다. 첫 번째 파라미터인 10을 상위 함수에 전달할 수 있고, 마지막 파라미터는 &lt;b&gt;mult5AfterAdd10&lt;/b&gt;이 호출될 때 전달될 것이다. 때문에 &lt;b&gt;add&lt;/b&gt; 함수는 좀 더 유연해졌다. &lt;br /&gt;&lt;br /&gt;이쯤 되면, Elm에서 커링을 이용한&amp;nbsp;&lt;b&gt;add&lt;/b&gt; 함수를 어떻게 만들지 궁금할 것이다.&lt;br /&gt;&lt;br /&gt;궁금할 필요 없다. Elm를 포함한 다른 함수형 언어에서는 모든 함수가 자동으로 컬링 된다.&lt;br /&gt;&lt;br /&gt;그래서 add 함수는 동일하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595044965582&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;add x y =
    x + y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래 코드가 바로 &lt;a href=&quot;https://soojae.tistory.com/31&quot;&gt;Part 3&lt;/a&gt;에서 작성되었어야 할 &lt;b&gt;mult5AfterAdd10&lt;/b&gt; 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595045001623&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mult5AfterAdd10 =
    (mult5 &amp;lt;&amp;lt; add 10)&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;br /&gt;Elm은 자바스크립트 같은 명령형 언어를 이긴다. 왜냐하면 Elm은 커링과 합성 같은 함수적인 것들에 최적화되어있기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;커링과 리팩터링&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_4_3.png&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k9O5i/btqFNsDcGuq/5mZ7YM2fcRIwREfHKkWpbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k9O5i/btqFNsDcGuq/5mZ7YM2fcRIwREfHKkWpbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k9O5i/btqFNsDcGuq/5mZ7YM2fcRIwREfHKkWpbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk9O5i%2FbtqFNsDcGuq%2F5mZ7YM2fcRIwREfHKkWpbk%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;282&quot; height=&quot;350&quot; data-filename=&quot;functional_programming_4_3.png&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;파라미터가 &lt;b&gt;많은&lt;/b&gt; 함수인 일반 버전을 만든 다음, 파라미터가 &lt;b&gt;적은&lt;/b&gt; 함수인 특정 버전을 만들기 위해 리팩터링 할 때 커링은 다시 한번 빛난다.&lt;br /&gt;&lt;br /&gt;예를 들어, 문자열에 단일 괄호 세트와, 이중 괄호 세트를 추가하는 함수가 아래에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595045547912&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bracket str =
    &quot;{&quot; ++ str ++ &quot;}&quot;
doubleBracket str =
    &quot;{{&quot; ++ str ++ &quot;}}&quot;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595045584225&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bracketedJoe =
    bracket &quot;Joe&quot;
doubleBracketedJoe =
    doubleBracket &quot;Joe&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;우리는 이제 bracket과 doubleBracket을 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595045632025&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;generalBracket prefix str suffix =
    prefix ++ str ++ suffix&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 이제 generalBracker을 사용할 때마다 bracket을 넘겨줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595045656606&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bracketedJoe =
    generalBracket &quot;{&quot; &quot;Joe&quot; &quot;}&quot;
doubleBracketedJoe =
    generalBracket &quot;{{&quot; &quot;Joe&quot; &quot;}}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;사실 우리가 정말로 원하는 것은 일거양득(the best of both worlds)이다.&lt;br /&gt;&lt;br /&gt;만약 우리가 &lt;b&gt;generalBracket&lt;/b&gt;의 파라미터의 순서를 바꾼다면, 커링 함수라는 사실을 활용하여 &lt;b&gt;bracket&lt;/b&gt;과 &lt;b&gt;doubleBracket&lt;/b&gt;을 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595046017895&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;generalBracket prefix suffix str =
    prefix ++ str ++ suffix
bracket =
    generalBracket &quot;{&quot; &quot;}&quot;
doubleBracket =
    generalBracket &quot;{{&quot; &quot;}}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;먼저 static으로 고정할 부분(&lt;b&gt;prefix&lt;/b&gt;와 &lt;b&gt;suffix)을&lt;/b&gt; 첫 번째 파라미터에 넣고, 변경될 가능성이 높은 파라미터(&lt;b&gt;str&lt;/b&gt;)를 가장 마지막에&amp;nbsp;배치하면 쉽게 &lt;b&gt;generalBracket&lt;/b&gt;의 특정 버전을 생성할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;파라미터의 순서는 완전한 커링 활용을 위해서 중요하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;또한 &lt;b&gt;bracket&lt;/b&gt;과 &lt;b&gt;doubleBracket&lt;/b&gt;은 &lt;b&gt;Point-Free 표기법&lt;/b&gt; 즉, str 파라미터가 함축되었다는 것에 주목하자. &lt;b&gt;bracket&lt;/b&gt;과 &lt;b&gt;doubleBracket&lt;/b&gt; 모두 마지막 파라미터를 기다리고 있는 함수이다.&lt;br /&gt;&lt;br /&gt;이제 우리는 이전과 동일하게 사용할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595051905147&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bracketedJoe =
    bracket &quot;Joe&quot;
doubleBracketedJoe =
    doubleBracket &quot;Joe&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_4_4.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHnC5e/btqFNQjhltR/7MOneMHD70adF08UA99fRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHnC5e/btqFNQjhltR/7MOneMHD70adF08UA99fRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHnC5e/btqFNQjhltR/7MOneMHD70adF08UA99fRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHnC5e%2FbtqFNQjhltR%2F7MOneMHD70adF08UA99fRk%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;468&quot; height=&quot;178&quot; data-filename=&quot;functional_programming_4_4.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;함수형 언어들에서 사용하는 일반적인 함수 3가지를 알아보자.&lt;br /&gt;&lt;br /&gt;그전에, 아래의 자바스크립트 코드를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595051972647&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (var i = 0; i &amp;lt; something.length; ++i) {
    // do stuff
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;이 코드는 큰 문제가 있다. 버그는 아니다. 그 문제는 바로 boilerplate 코드다. 즉, 준비 코드를 계속 반복해서 작성해야 한다는 것이다. &lt;br /&gt;&lt;br /&gt;만약 당신의 코드가 Java, C#, 자바스크립트, PHP, Python 같은 명령형 언어에서 작성한 코드라면, 당신은 함수형 언어보다 더 많은 boilerplate 코드를 작성하고 있는 자기 자신을 발견할 것이다.&lt;br /&gt;&lt;br /&gt;그러니 boilerplate를 없애보자. boilerplate를 하나 또는 두 함수 안에 넣고 다시는 for-loop를 작성하지 않도록 해보자. 음... 사실 명령형 언어가 아닌 함수형 언어를 사용하기 전까지 불가능하다고 보면 된다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;things&lt;/b&gt;라는 배열을 변경해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595052796856&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var things = [1, 2, 3, 4];
for (var i = 0; i &amp;lt; things.length; ++i) {
    things[i] = things[i] * 10; // MUTATION ALERT !!!!
}
console.log(things); // [10, 20, 30, 40]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;안돼! 불변하지 않잖아!!&lt;br /&gt;&lt;br /&gt;다시 시도해 보자. 이번엔 &lt;b&gt;things&lt;/b&gt;가 불변할 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595052822476&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i &amp;lt; things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;우리는 &lt;b&gt;things&lt;/b&gt;를 불변하게 만들었지만, &lt;b&gt;newThings&lt;/b&gt;는 불변하지 않다.&lt;br /&gt;일단 우리는 이 사실을 무시하고 넘어가자. 위의 코드는 명령형 프로그래밍 언어인 자바스크립트로 작성한 것이니까. 우리가 함수형 언어(예: Elm)를 사용한다면 불변할 것이다.&lt;br /&gt;&lt;br /&gt;여기서의 요점은 이러한 함수들이 어떻게 동작하는지 이해하고, 우리 코드의 소음을 줄일 수 있도록 돕는 것이다.&lt;br /&gt;&lt;br /&gt;이 코드를 가지고 함수에 넣자. 첫 번째, 공통 함수 &lt;b&gt;map&lt;/b&gt;을 호출할 것이다. 왜냐하면 이 함수는 이전 배열의 각 요소를 새 배열의 새 값에 매핑시켜주기 때문이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1595053422159&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var map = (f, array) =&amp;gt; {
    var newArray = [];
    for (var i = 0; i &amp;lt; array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;함수 &lt;b&gt;f&lt;/b&gt;가 전달되어 &lt;b&gt;map&lt;/b&gt;함수가 &lt;b&gt;array&lt;/b&gt;에 있는 각 요소에 원하는 작업을 할 수 있다는 것에 주목하자.&lt;br /&gt;&lt;br /&gt;이제 &lt;b&gt;map&lt;/b&gt; 함수를 이용하기 위해 이전 코드를 다시 작성해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595053751839&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var things = [1, 2, 3, 4];
var newThings = map(v =&amp;gt; v * 10, things);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;for-loop가 없다!. 그리고 훨씬 읽기 쉬워졌고, 추론하기도 쉽다.&lt;br /&gt;&lt;br /&gt;기술적으로 map 함수 안에 for-loop가 있다. 그러나 적어도 더 이상 boilerplate 코드를 작성할 필요가 없어졌다.&lt;br /&gt;&lt;br /&gt;이제 또 다른 공통 함수인 &lt;b&gt;filter&lt;/b&gt;를 작성해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595053780475&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var filter = (pred, array) =&amp;gt; {
    var newArray = [];
for (var i = 0; i &amp;lt; array.length; ++i) {
        if (pred(array[i]))
            newArray[newArray.length] = array[i];
    }
    return newArray;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;어떻게 &lt;b&gt;pred &lt;/b&gt;함수가 배열의 요소를 유지하고 싶으면 &lt;b&gt;True&lt;/b&gt;, 요소를 버리고 싶으면 &lt;b&gt;False&lt;/b&gt;를 리턴하는지 살펴보자.&lt;br /&gt;&lt;br /&gt;아래의&lt;b&gt; filter &lt;/b&gt;함수가 홀수를 필터링하는 동작을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595064810615&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var isOdd = x =&amp;gt; x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;새로운&amp;nbsp;&lt;b&gt;filter&lt;/b&gt;&amp;nbsp;함수를 사용하는 것은 직접 for-loop 반복문을 작성하는 것보다 매우 간단하다.&lt;br /&gt;&lt;br /&gt;마지막 공통 함수형 함수는 &lt;b&gt;reduce&lt;/b&gt;이다. &lt;b&gt;reduce&lt;/b&gt;는 리스트를 갖고 와서 하나의 값으로 줄이기 위해 사용되지만, 실제로는 훨씬 더 많은 것을 할 수 있다.&lt;br /&gt;&lt;br /&gt;아래 함수는 함수형 언어에서 &lt;b&gt;fold라고&lt;/b&gt; 불린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595064860606&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var reduce = (f, start, array) =&amp;gt; {
    var acc = start;
    for (var i = 0; i &amp;lt; array.length; ++i)
        acc = f(array[i], acc); // f() 함수는 파라미터 2개를 받는다.
    return acc;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;reduce&lt;/b&gt;함수는 &lt;b&gt;f&lt;/b&gt;라는 감축 함수와 초기 값, 그리고 배열을 사용한다.&lt;br /&gt;&lt;br /&gt;감축 함수인 &lt;b&gt;f&lt;/b&gt;는 2개의 파라미터,&amp;nbsp;&lt;b&gt;array&lt;/b&gt;의 최근 요소, 그리고 누산기인 &lt;b&gt;acc&lt;/b&gt;를 받는다. f함수는 이 파라미터들을 사용하여 각 반복마다 새로운 누산기를 생성한다. &lt;br /&gt;반복이 끝나면, 최종 누산기의 값이 리턴된다.&lt;br /&gt;&lt;br /&gt;아래는 fold가 어떻게 동작하는지를 이해하기 위한 예제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1595066467673&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var add = (x, y) =&amp;gt; x + y;
var values = [1, 2, 3, 4, 5];
var sumOfValues = reduce(add, 0, values);
console.log(sumOfValues); // 15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;add&lt;/b&gt;함수는 2개의 파라미터를 받아서 합한다.&lt;br /&gt;&lt;b&gt;reduce&lt;/b&gt;&amp;nbsp;함수는 &lt;b&gt;초기값&lt;/b&gt;이 0부터 시작하고, 합치기 위해 배열 &lt;b&gt;values&lt;/b&gt;에 이 값을 전달한다. &lt;b&gt;reduce&lt;/b&gt;함수 안에서, 값이 반복됨에 따라 합이 누적된다. 마지막 총 누적된 결괏값인 &lt;b&gt;sumOfValues&lt;/b&gt;가 리턴된다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;map&lt;/b&gt;, &lt;b&gt;filter&lt;/b&gt;, 그리고 &lt;b&gt;reduce&lt;/b&gt;&amp;nbsp;함수는 boilerplate인 for-loop 반복문을 작성할 필요 없이 배열을 조작할 수 있게 해 준다.&lt;br /&gt;&lt;br /&gt;함수형 언어에서 &lt;b&gt;map&lt;/b&gt;,&amp;nbsp;&lt;b&gt;filter&lt;/b&gt;, &lt;b&gt;reduce&lt;/b&gt;는 훨씬 유용하다. 생성에서의 loop 조차 없고, 재귀로만 이루어져 있기 때문이다. 반복 함수들은 프로그래밍에 그렇게 많은 도움을 주진 못한다. 하지만 필수적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;br /&gt;머리 아파! 이제 한계야!&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_4_5.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tqe9L/btqFNFh8a4b/T9TIU0E5OftW1Q5YIRvId0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tqe9L/btqFNFh8a4b/T9TIU0E5OftW1Q5YIRvId0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tqe9L/btqFNFh8a4b/T9TIU0E5OftW1Q5YIRvId0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTqe9L%2FbtqFNFh8a4b%2FT9TIU0E5OftW1Q5YIRvId0%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;300&quot; height=&quot;225&quot; data-filename=&quot;functional_programming_4_5.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&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;br /&gt;&lt;br /&gt;이후 게시물에서는 참조 투명성, 실행 순서, 타입 등에 대해 이야기하려고 한다.&lt;br /&gt;&lt;br /&gt;다음 게시물 : &lt;a href=&quot;https://soojae.tistory.com/33&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 5&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 번역 오류가 있으면 알려주세요 감사합니다.&lt;/p&gt;
&lt;!-- 레이어 팝업 --&gt;</description>
      <category>Knowledge/Web</category>
      <category>Functional Programming</category>
      <category>javascript</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/32</guid>
      <comments>https://soojae.tistory.com/32#entry32comment</comments>
      <pubDate>Sat, 18 Jul 2020 19:14:25 +0900</pubDate>
    </item>
    <item>
      <title>함수형 프로그래밍 전문가 되기 (Part 3)</title>
      <link>https://soojae.tistory.com/31</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 글은 Charles Scalfani의 &lt;a href=&quot;https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-3-1b0fd14eb1a7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;So You Want to be a Functional Programmer (Part 3)&lt;/a&gt;를 번역한 게시물입니다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Thank you Charles Scalfani! Thanks to your writing, I can grow into a better developer.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_3_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nVlgm/btqFHqrZBHA/fJvk9EJPoE7ShzGndO3iGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nVlgm/btqFHqrZBHA/fJvk9EJPoE7ShzGndO3iGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nVlgm/btqFHqrZBHA/fJvk9EJPoE7ShzGndO3iGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnVlgm%2FbtqFHqrZBHA%2FfJvk9EJPoE7ShzGndO3iGK%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;700&quot; height=&quot;310&quot; data-filename=&quot;functional_programming_3_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;함수형 프로그래밍의 개념을 이해하기 위해 내딛는 첫걸음은 매우 중요하다. 매우 힘든 첫걸음이지만 올바른 관점으로 접근한다면 힘들어할 필요가 없다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이전 게시물 : &lt;a href=&quot;https://soojae.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 1&lt;/a&gt;, &lt;a href=&quot;https://soojae.tistory.com/30&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 2&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;합성 함수&lt;/span&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_3_2.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRNQAL/btqFGxrPOeb/kpGXSyrzHUEHWJO8dBkP71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRNQAL/btqFGxrPOeb/kpGXSyrzHUEHWJO8dBkP71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRNQAL/btqFGxrPOeb/kpGXSyrzHUEHWJO8dBkP71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRNQAL%2FbtqFGxrPOeb%2FkpGXSyrzHUEHWJO8dBkP71%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;600&quot; height=&quot;290&quot; data-filename=&quot;functional_programming_3_2.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;290&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;프로그래머로서 우리는 게으르다(좋은 뜻). 우리는 빌드, 테스트, 배포 코드를 계속해서 작성하는 것을 원하지 않는다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;우리는 항상 일을 한 번만 하는 방법과 그것을 어떻게 재 사용하여 다른 일을 할 수 있는지 알아내려고 노력한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;코드 재사용은 좋다. 하지만 코드 재사용을 실제로 실천하긴 어렵다. 코드를 굉장히 상세하게 만들면, 그것을 재사용할 수 없다. 그렇다고 너무 일반적으로 만들면 애당초 사용하기 어려울 수 있다. 코드를 작고 재사용이 가능한 조각으로 만들면 그 코드는 더 복잡한 기능의 조각이 될 수 있다. 그렇기 때문에 이 둘 사이의 균형을 잡는 것이 중요하다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;함수형 프로그래밍에서, 함수들은 빌딩 블록이다. 우리는 매우 구체적인 작업을 하기 위해 함수들을 작성하고 레고 블록처럼 조립한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이것이 &lt;b&gt;합성함수&lt;/b&gt;이다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서 합성함수는 어떻게 동작할까? 두개의 자바스크립트 함수를 살펴보자.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594827856089&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var add10 = function(value) {
  return value + 10;
};

var mult5 = function(value) {
  return value * 5;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;꽤 복잡하다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;화살표 함수 표현식&lt;/b&gt;을 사용해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594827900276&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var add10 = value =&amp;gt; value + 10;
var mult5 = value =&amp;gt; value * 5;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;훨씬 나아졌다. 이제 value에 10을 더한 값에 5를 곱하는 함수가 필요하다고 생각해보자. 다음과 같이 작성할 수 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594827922230&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var mult5AfterAdd10 = value =&amp;gt; 5 * (value + 10)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;매우 간단한 예제이긴 하지만, 함수를 이렇게 작성하는 것은 좋지 않다. 그 이유는&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;첫째로, 괄호를 잊어버리는 실수를 할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;둘째로, 우리는 이미 10을 더하는 함수와 5를 곱하는 함수를 각각 갖고 있다. 즉, 이미 있는 함수들을 다시 작성하고 있다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러니 &lt;b&gt;add10&lt;/b&gt;과 &lt;b&gt;mult5&lt;/b&gt;를 사용해서 새로운 함수를 만들어보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594828374744&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var mult5AfterAdd10 = value =&amp;gt; mult5(add10(value));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;우리는 단지 기존의 함수를 이용해서 &lt;b&gt;mult5AfterAdd10&lt;/b&gt; 이라는 함수를 만들었다. 당연히 함수를 새로 만드는 것보다 이 방법이 더 좋다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;수학에서, &lt;b&gt;f ∘ g&lt;/b&gt;는 합성 함수이다. 그리고&lt;b&gt; &amp;ldquo;g composed with f&amp;rdquo;&lt;/b&gt; 또는 일반적으로 &lt;b&gt;&amp;ldquo;f after g&amp;rdquo;&lt;/b&gt;라고 읽는다. 그래서&lt;b&gt; (f ∘ g)(x&lt;/b&gt;&lt;b&gt;)&lt;/b&gt; 는 &lt;b&gt;x&lt;/b&gt;값과 함께 함수 &lt;b&gt;g&lt;/b&gt;를 호출한 다음 함수 &lt;b&gt;f&lt;/b&gt;를 호출한 것과 같다. 간단히 표현하자면&lt;b&gt;&amp;nbsp;f(g(x))&lt;/b&gt;와 동일하다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예제에서 우리는 &lt;b&gt;mult5 ∘ add10(&amp;ldquo;mult5 after add10&amp;rdquo;)&lt;/b&gt;를 갖고 있다, 그러므로 이 함수의 이름은 &lt;b&gt;mult5AfterAdd10&lt;/b&gt; 이라고 볼 수 있다. &lt;b&gt;value&lt;/b&gt;과 함께 &lt;b&gt;add10&lt;/b&gt;을 호출하고 후에 &lt;b&gt;mult5&lt;/b&gt;를 호출했다. 간단히 표현하자면 &lt;b&gt;mult5(add10(value))&lt;/b&gt;와 동일하다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;네이티브 자바스크립트는 합성함수가 없기 때문에 Elm을 예로 들어보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594828749704&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 &amp;lt;&amp;lt; add10) value&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Elm에서는 &amp;lt;&amp;lt; 라는 연산자로 함수를 합성한다. 이 연산자는 데이터가 어떻게 흘러가는지 시각적으로 알려준다. 첫 번째로&amp;nbsp;&lt;b&gt;value&lt;/b&gt;가 &lt;b&gt;add10&lt;/b&gt;으로 전달된다. 그 결과는 &lt;b&gt;mult5&lt;/b&gt;로 전달된다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;mult5AfterAdd10&lt;/b&gt;에 있는 괄호,&lt;b&gt;&amp;nbsp;(mult5 &amp;lt;&amp;lt; add10)&lt;/b&gt;를 주시하자. 이것은 &lt;b&gt;value&lt;/b&gt;를 적용하기 이전에 먼저 확정적으로 함수끼리 합성을 시키기 위해 괄호가 존재한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다음과 같은 방법으로 당신이 원하는 만큼 함수들을 합성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594828977156&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;f x =
   (g &amp;lt;&amp;lt; h &amp;lt;&amp;lt; s &amp;lt;&amp;lt; r &amp;lt;&amp;lt; t) x&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;x&lt;/b&gt;는 함수 t에 전달되고, 그 결과 값이 r로 전달된 후, 그 결과 값이 s로 전달된다. 이런 식으로 마지막 g까지 전달된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만약 자바스크립트에서 이와 같은 기능을 구현한 적이 있었다면, 그건 &lt;b&gt;g(h(s(r(t(x))))) &lt;/b&gt;이렇게 보였을 것이다. 괄호가 끔찍하다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Point-Free 표기법&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_3_3.png&quot; data-origin-width=&quot;208&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1kPgk/btqFIF3kMAP/g34dYgiOUUZwd0KnoVbzv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1kPgk/btqFIF3kMAP/g34dYgiOUUZwd0KnoVbzv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1kPgk/btqFIF3kMAP/g34dYgiOUUZwd0KnoVbzv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1kPgk%2FbtqFIF3kMAP%2Fg34dYgiOUUZwd0KnoVbzv1%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;208&quot; height=&quot;208&quot; data-filename=&quot;functional_programming_3_3.png&quot; data-origin-width=&quot;208&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;파라미터를 정의할 필요 없이 함수를 작성하는 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Point-Free 표기법&lt;/b&gt;이라는 코딩 방식이 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;처음 보는 사람들은 이 방식이 이상하게 보일 것이지만, 계속 사용하다 보면 이 방식의 간결함에 감탄할 것이다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;당신은 &lt;b&gt;mult5AfterAdd10&lt;/b&gt;에서 &lt;b&gt;value&lt;/b&gt;가 파라미터 리스트에서 한 번, 사용될 때 한 번, 총 두 번 정의되었다는 것을 알아차렸을 것이다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594829036661&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 이 함수는 파라미터가 1개다. 

mult5AfterAdd10 value =
    (mult5 &amp;lt;&amp;lt; add10) value&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;그러나 이 파라미터는 &lt;b&gt;add10&lt;/b&gt;이 동일한 파라미터를 사용할 것이기 때문에 불 필요하다. 같은 동작을 하는 Point-Free 방식으로 바꾸면 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594829231452&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 이 함수 또한 파라미터가 1개다.

mult5AfterAdd10 =
    (mult5 &amp;lt;&amp;lt; add10)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;Point-Free 방식을 이용한 것이 더 많은 이점이 있다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;첫 번째, 우리는 불필요한 파라미터를 정의하지 않아도 된다. 그로 인해 정의할 파라미터 이름을 생각하느라 머리를 싸매지 않아도 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;두 번째, 덜 복잡해지기 때문에 좀 더 읽기 쉬워진다. 위의 예시는 단순하다. 하지만 함수 하나에 무수한 파라미터들이 있다고 상상해보자.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;파라다이스에서의 문제&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_3_4.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dC351g/btqFHpNrgOA/VTTY54kWA3RUnBOTniSSL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dC351g/btqFHpNrgOA/VTTY54kWA3RUnBOTniSSL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dC351g/btqFHpNrgOA/VTTY54kWA3RUnBOTniSSL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdC351g%2FbtqFHpNrgOA%2FVTTY54kWA3RUnBOTniSSL0%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;600&quot; height=&quot;463&quot; data-filename=&quot;functional_programming_3_4.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;지금까지 어떻게 합성 함수가 동작하는지와 간결성, 명확성, 그리고 유연성을 위해 Point-Free 표기법으로 어떻게 함수를 정의해야 하는지를 살펴보았다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이제 조금 다른 시나리오에서 이 아이디어들을 사용해보고, 그것들이 어떻게 진행되는지 살펴보자. &lt;b&gt;add10&lt;/b&gt; 함수를 &lt;b&gt;add&lt;/b&gt;로 변경한다고 상상해보자.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594829377696&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;add x y =
    x + y
mult5 value =
    value * 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;어떻게 위의 두 함수를 이용해서 &lt;b&gt;mult5AfterAdd10&lt;/b&gt;를 만들까?&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;읽던걸 멈추고 잠시 생각해보자. 생각이 끝났다면, 작성해보자.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;작성이 끝났다면 비교해보자. 당신은 아래와 같이 작성했을 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594829423629&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 이 함수는 잘못됐어!!!

mult5AfterAdd10 =
    (mult5 &amp;lt;&amp;lt; add) 10 &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;하지만 이 함수는 동작하지 않는다. &lt;b&gt;add&lt;/b&gt;의 파라미터가 2개이기 때문이다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만약 Elm으로 이 개념을 이해하기 어렵다면 Javascript로 작성해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594829484404&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var mult5AfterAdd10 = mult5(add(10)); // 이 함수는 동작하지 않는다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;이 코드는 왜 잘못됐을까?&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;add&lt;/b&gt;&amp;nbsp;함수는 2개의 파라미터가 필요한데 1개만 입력받았다. 그리고 그 &lt;i&gt;부정확한 결과&lt;/i&gt;가 &lt;b&gt;mult5&lt;/b&gt;로 전달되었고, 이는 결국 잘못된 결과를 산출할 것이다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;사실 Elm이었으면, 컴파일러 단계에서 에러가 발생할 것이다(Elm의 위대한 장점 중 하나). 다시 작성해보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594829729829&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var mult5AfterAdd10 = y =&amp;gt; mult5(add(10, y)); // not point-free&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;이것은 Point-Free 방식이 아니지만, 이 방식으로도 동작은 한다. 하지만 더 이상 함수를 결합하지 못한다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 함수가 점점 더 복잡해질수록 문제가 생긴다. 만약 &lt;b&gt;mult5AfterAdd10&lt;/b&gt;과 다른 함수를 결합하고 싶을 때가 되면 정말 곤란해질 것이다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;결국, 합성 함수의 유용성은 한계가 있으므로 이 두 개의 함수를 결합할 수 없게 되었다. 너무 아쉽다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 문제를 어떻게 해결할 수 있을까? &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만약 &lt;b&gt;add&lt;/b&gt;함수에 단 하나의 파라미터를 미리 전달하고, 두 번째 파라미터는 &lt;b&gt;mult5AfterAdd10&lt;/b&gt;을 호출할 때 전달하는 방법이 있다면 훌륭할 것 같다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;찾아보니 방법이 있다. &lt;b&gt;커링&lt;/b&gt;이라는 방법이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;머리 아파! 이제 한계야!!&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_3_5.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLDv7D/btqFGyj0pbd/EuCQKoVTHq6cJIhm9GkRR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLDv7D/btqFGyj0pbd/EuCQKoVTHq6cJIhm9GkRR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLDv7D/btqFGyj0pbd/EuCQKoVTHq6cJIhm9GkRR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLDv7D%2FbtqFGyj0pbd%2FEuCQKoVTHq6cJIhm9GkRR0%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;300&quot; height=&quot;225&quot; data-filename=&quot;functional_programming_3_5.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;오늘은 여기까지.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이후 게시물에서는 커링, 함수형 함수(map, filter, fold 등등), 참조 투명성 등에 대해 이야기할 예정이다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다음 게시물: &lt;a href=&quot;https://soojae.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part 4&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 번역 오류가 있으면 알려주세요 감사합니다.&lt;/p&gt;
&lt;!-- 레이어 팝업 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Knowledge/Web</category>
      <category>Functional Programming</category>
      <category>javascript</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/31</guid>
      <comments>https://soojae.tistory.com/31#entry31comment</comments>
      <pubDate>Thu, 16 Jul 2020 01:31:50 +0900</pubDate>
    </item>
    <item>
      <title>함수형 프로그래밍 전문가 되기 (Part 2)</title>
      <link>https://soojae.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Charles Scalfani의 &lt;a href=&quot;https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-2-7005682cec4a&quot;&gt;So You Want to be a Functional Programmer (Part 2)&lt;/a&gt;를 번역한 게시물입니다.&lt;br /&gt;Thank you Charles Scalfani! Thanks to your writing, I can grow into a better developer.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_2_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/47ZgK/btqFGMgJjY8/K9mayi1M6Tdt4P1KpvXYQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/47ZgK/btqFGMgJjY8/K9mayi1M6Tdt4P1KpvXYQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/47ZgK/btqFGMgJjY8/K9mayi1M6Tdt4P1KpvXYQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F47ZgK%2FbtqFGMgJjY8%2FK9mayi1M6Tdt4P1KpvXYQk%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;700&quot; height=&quot;310&quot; data-filename=&quot;functional_programming_2_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 게시물 : &lt;a href=&quot;https://soojae.tistory.com/29&quot;&gt;Part 1&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;친절한 알림&lt;/span&gt;&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_2_2.png&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZjYDk/btqFCp8BiKS/2tPnK2VrGkxmmD8NrjAeTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZjYDk/btqFCp8BiKS/2tPnK2VrGkxmmD8NrjAeTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZjYDk/btqFCp8BiKS/2tPnK2VrGkxmmD8NrjAeTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZjYDk%2FbtqFCp8BiKS%2F2tPnK2VrGkxmmD8NrjAeTk%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;283&quot; height=&quot;314&quot; data-filename=&quot;functional_programming_2_2.png&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 나올 코드를 천천히 읽어라. 코드를 이해한 다음에 다음으로 넘어가라. 각각의 세션은 이전 세션과 이어진다.&lt;br /&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_2_3.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEvv6P/btqFFUfhm5c/jULw6ETIRm6uBjcGiU7xv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEvv6P/btqFFUfhm5c/jULw6ETIRm6uBjcGiU7xv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEvv6P/btqFFUfhm5c/jULw6ETIRm6uBjcGiU7xv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEvv6P%2FbtqFFUfhm5c%2FjULw6ETIRm6uBjcGiU7xv0%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;700&quot; height=&quot;413&quot; data-filename=&quot;functional_programming_2_3.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;413&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;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function validateSsn(ssn) {
    if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn))
        console.log('Valid SSN');
    else
        console.log('Invalid SSN');
}
function validatePhone(phone) {
    if (/^\(\d{3}\)\d{3}-\d{4}$/.exec(phone))
        console.log('Valid Phone Number');
    else
        console.log('Invalid Phone Number');
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;validatePhone &lt;/b&gt;함수와&lt;b&gt; validateSSn &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;b&gt;값&lt;/b&gt;, &lt;b&gt;정규식&lt;/b&gt;, &lt;b&gt;출력 메시지의 마지막 부분인 'SSN', 'Phone Number'&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;function validateValue(value, regex, type) {
    if (regex.exec(value))
        console.log('Invalid ' + type);
    else
        console.log('Valid ' + type);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 코드에서의 &lt;b&gt;ssn&lt;/b&gt;과 &lt;b&gt;phone&lt;/b&gt; 파라미터들은 이제 &lt;b&gt;value&lt;/b&gt;로 표현된다.&lt;br /&gt;정규식인 &lt;code&gt;/^\\d{3}-\\d{2}-\\d{4}$/&lt;/code&gt; 와 &lt;code&gt;/^\\(\\d{3}\\)\\d{3}-\\d{4}$/&lt;/code&gt; 는 &lt;b&gt;regex&lt;/b&gt;로 표현된다.&lt;br /&gt;출력 메시지의 마지막 부분인 '&lt;b&gt;SSN&lt;/b&gt;', '&lt;b&gt;Phone Number&lt;/b&gt;'은 &lt;b&gt;type으로&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;br /&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;하지만 아래와 같은 상황이 발생하면 어떻게 될까?&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function validateAddress(address) {
    if (parseAddress(address))
        console.log('Valid Address');
    else
        console.log('Invalid Address');
}

function validateName(name) {
    if (parseFullName(name))
        console.log('Valid Name');
    else
        console.log('Invalid Name');
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 &lt;b&gt;parseAddress&lt;/b&gt;와 &lt;b&gt;parseFullName&lt;/b&gt; 함수는 문자열을 가져와서 파싱 할 경우 true를 리턴하는 함수다.&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;b&gt;address&lt;/b&gt;와 &lt;b&gt;name&lt;/b&gt;을 &lt;b&gt;value&lt;/b&gt; 파라미터로 바꿀 수 있다. 그리고 &lt;b&gt;'Address'와&lt;/b&gt; &lt;b&gt;'Name'을&lt;/b&gt; &lt;b&gt;type&lt;/b&gt;로 바꿀 수 있다.&lt;br /&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;h3 data-ke-size=&quot;size23&quot;&gt;고차 함수&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_2_4.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IiKKc/btqFDaDnICH/GLszkpz93LqKCSFBs0aFTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IiKKc/btqFDaDnICH/GLszkpz93LqKCSFBs0aFTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IiKKc/btqFDaDnICH/GLszkpz93LqKCSFBs0aFTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIiKKc%2FbtqFDaDnICH%2FGLszkpz93LqKCSFBs0aFTk%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;300&quot; height=&quot;300&quot; data-filename=&quot;functional_programming_2_4.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 언어가 함수를 파라미터로 전달하는 것을 지원하지 않는다. 몇몇 언어에서는 가능하지만 만들기가 쉽지 않다. 함수형&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 프로그래밍에서 함수는 1급 시민이다. 다시 말하면 함수를 단지 '값'으로 취급한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;함수들은 단지 값이기 때문에 우리는 그 함수들을 파라미터로 전달할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 자바스크립트가 순수 함수 언어는 아니지만, 당신은 몇몇 함수형 동작들을 할 수 있다. 아래 코드는 파싱 함수를 parseFunc라는 파라미터로 전달하여 위에서 본 두 함수를 단일 함수로 리팩터링 했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;function validateValueWithFunc(value, parseFunc, type) {
    if (parseFunc(value))
        console.log('Invalid ' + type);
    else
        console.log('Valid ' + type);
}&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;고차 함수는 함수를 파라미터로 취급하거나, 함수를 리턴한다.&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이전 4개 함수에 대해 고차 함수를 호출할 수 있다.(자바스크립트에서 동작하는 Regex.exec는 정규식에 일치하는 부분이 있으면 truthy 한 값을 리턴한다.)&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;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;validateValueWithFunc('123-45-6789', /^\d{3}-\d{2}-\d{4}$/.exec, 'SSN');
validateValueWithFunc('(123)456-7890', /^\(\d{3}\)\d{3}-\d{4}$/.exec, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 동일한 4가지의 함수를 만드는 것보다 훨씬 좋다.&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;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec;
var parsePhone = /^\(\d{3}\)\d{3}-\d{4}$/.exec;

validateValueWithFunc('123-45-6789', parseSsn, 'SSN');
validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');&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;b&gt;parseSsn&lt;/b&gt;과 &lt;b&gt;parsePhone&lt;/b&gt; 외에, 파싱 하기 위한 더 많은 정규식이 있다고 생각해보자. 매번 정규식 파서를 만들 때마다, 우리는. exec를 끝에 붙여줘야 한다.&lt;br /&gt;장담하건대, 이런 것(뒤에. exec를 붙이는 것)은 잊어버리기 쉽다.&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;exec&lt;/b&gt;를 리턴하는 고차 함수를 만들어서 방지할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function makeRegexParser(regex) {
    return regex.exec;
}

var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/);
var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/);

validateValueWithFunc('123-45-6789', parseSsn, 'SSN');
validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;makeRegexParser&lt;/b&gt;는 정규을 취하고 문자열을 실행하는 exec 함수를 리턴한다. &lt;b&gt;validateValueWithFunc&lt;/b&gt;는 &lt;b&gt;string&lt;/b&gt;과 &lt;b&gt;value&lt;/b&gt; 입력 값을 parse 함수로 전달한다.&lt;br /&gt;&lt;b&gt;parseSsn&lt;/b&gt;와 &lt;b&gt;parsePhone&lt;/b&gt;는 사실상&amp;nbsp;이전 코드와 같다. 정규 표현식을 &lt;b&gt;exec&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;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;makeRegexParser&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;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function makeAdder(constantValue) {
    return function adder(value) {
        return constantValue + value;
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터인 &lt;b&gt;constantValue&lt;/b&gt;와 &lt;b&gt;adder&lt;/b&gt;를 리턴하는 &lt;b&gt;makeAddr&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;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;var add10 = makeAdder(10);
console.log(add10(20)); // prints 30
console.log(add10(30)); // prints 40
console.log(add10(40)); // prints 50&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 &lt;b&gt;add10&lt;/b&gt;&amp;nbsp;함수를 만들었다. 상수 &lt;b&gt;10&lt;/b&gt;을 함수를 리턴하는 &lt;b&gt;makeAdder&lt;/b&gt;에 전달해줌으로써 모든 값에 10이 더해질 것이다.&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;adder&lt;/b&gt;는 makeAdder 함수가 리턴된 후에도 &lt;b&gt;constantValue&lt;/b&gt;에 접근 할 수 있다는 점에 주목해라. 접근할 수 있었던 이유는 &lt;b&gt;constantValue&lt;/b&gt;가 &lt;b&gt;adder&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;br /&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_2_5.png&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5bmvv/btqFGNGLMbZ/lGA4aGafL215KUIsxjqZ10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5bmvv/btqFGNGLMbZ/lGA4aGafL215KUIsxjqZ10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5bmvv/btqFGNGLMbZ/lGA4aGafL215KUIsxjqZ10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5bmvv%2FbtqFGNGLMbZ%2FlGA4aGafL215KUIsxjqZ10%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;576&quot; height=&quot;300&quot; data-filename=&quot;functional_programming_2_5.png&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로저를 이용한 함수들을 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function grandParent(g1, g2) {
    var g3 = 3;
    return function parent(p1, p2) {
        var p3 = 33;
        return function child(c1, c2) {
            var c3 = 333;
            return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3;
        };
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서 &lt;b&gt;child&lt;/b&gt;는 &lt;b&gt;child&lt;/b&gt; 자신의 변수들과 &lt;b&gt;parent&lt;/b&gt;의 변수들, 그리고 &lt;b&gt;grandParent&lt;/b&gt;의 변수 들에 접근한다.&lt;br /&gt;&lt;b&gt;parent&lt;/b&gt;는 &lt;b&gt;parent&lt;/b&gt; 자신의 변수들과, &lt;b&gt;grandParent&lt;/b&gt;의 변수에 접근한다.&lt;br /&gt;&lt;b&gt;grandParent&lt;/b&gt;는 오직 &lt;b&gt;grandParent&lt;/b&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;이를 이용한 예제를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;var parentFunc = grandParent(1, 2); // returns parent()
var childFunc = parentFunc(11, 22); // returns child()
console.log(childFunc(111, 222)); // prints 738
// 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;parentFunc&lt;/b&gt;는 &lt;b&gt;grandParent&lt;/b&gt;가 &lt;b&gt;parent&lt;/b&gt;를 리턴할 때, &lt;b&gt;parent&lt;/b&gt;의 스코프가 살아있도록 유지시켜준다.&lt;br /&gt;비슷하게 &lt;b&gt;childFunc&lt;/b&gt;는 &lt;b&gt;parent&lt;/b&gt;가 &lt;b&gt;child&lt;/b&gt;를 리턴할 때, &lt;b&gt;child&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;br /&gt;함수는 참조가 남아있는 한 여전히 존재한다. 예를 들어 &lt;b&gt;child&lt;/b&gt;의 스코프는 &lt;b&gt;childFunc&lt;/b&gt;가 &lt;b&gt;child&lt;/b&gt;의 스코프를 계속 참조하는 한 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;클로저는 함수에 대한 참조에 의해 계속 살아 있는 함수의 스코프이다.&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트 안에서의 클로저들은 변수들이 변할 수(mutable) 있기 때문에 문제가 된다. 즉, 클로저들은 닫힌 시점부터 반환된 함수가 호출될 때까지 값을 변경할 수 있다.&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;고맙게도, 함수형 프로그래밍의 변수들은 불변(Immutable)하여 버그와 혼동이 생기지 않도록 해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;머리 아파! 이제 한계야!&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_2_6.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y8SqS/btqFGL93X3U/vywolMFYamGtEEF2pB7dE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y8SqS/btqFGL93X3U/vywolMFYamGtEEF2pB7dE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y8SqS/btqFGL93X3U/vywolMFYamGtEEF2pB7dE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY8SqS%2FbtqFGL93X3U%2FvywolMFYamGtEEF2pB7dE0%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;300&quot; height=&quot;225&quot; data-filename=&quot;functional_programming_2_6.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이후 게시물에서는 합성 함수, 커링, 일반적인 함수형 함수(map, filter, fold 등등) 등에 대해 이야기 할 예정이다.&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://soojae.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part3&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;글에 번역 오류가 있으면 알려주세요 감사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;!-- 레이어 팝업 --&gt;</description>
      <category>Knowledge/Web</category>
      <category>Functional Programming</category>
      <category>javascript</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/30</guid>
      <comments>https://soojae.tistory.com/30#entry30comment</comments>
      <pubDate>Wed, 15 Jul 2020 21:34:00 +0900</pubDate>
    </item>
    <item>
      <title>함수형 프로그래밍 전문가 되기 (Part 1)</title>
      <link>https://soojae.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Charles Scalfani의 &lt;a href=&quot;https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-1-1f15e387e536&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;So You Want to be a Functional Programmer (Part 1)&lt;/a&gt; 를 번역한 게시물입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thank you Charles Scalfani! Thanks to your writing, I can grow into a better developer.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;widthContent&quot; data-filename=&quot;functional_programming_1_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hu9yo/btqFxzbmjbf/KLg8lKjzehD7ZYNcL7UYg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hu9yo/btqFxzbmjbf/KLg8lKjzehD7ZYNcL7UYg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hu9yo/btqFxzbmjbf/KLg8lKjzehD7ZYNcL7UYg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHu9yo%2FbtqFxzbmjbf%2FKLg8lKjzehD7ZYNcL7UYg0%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;700&quot; height=&quot;310&quot; data-filename=&quot;functional_programming_1_1.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthContent&quot; data-filename=&quot;functional_programming_1_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8x0vl/btqFxzbmmLy/L0c9IneWEMPGjWAkT8CpSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8x0vl/btqFxzbmmLy/L0c9IneWEMPGjWAkT8CpSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8x0vl/btqFxzbmmLy/L0c9IneWEMPGjWAkT8CpSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8x0vl%2FbtqFxzbmmLy%2FL0c9IneWEMPGjWAkT8CpSk%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;230&quot; data-filename=&quot;functional_programming_1_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;230&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;br /&gt;우리는 부모님의 차로 연습을 했고, 동네의 길을 완전히 익힐 때까지는 고속도로를 달리지 않았다.&lt;br /&gt;반복되는 연습과 공포스러운 순간들을 보내면서, 운전을 배우게 되고 마침내 운전면허증을 취득했다.&lt;br /&gt;&lt;br /&gt;운전면허를 취득한 후, 여행할 때마다 우리의 운전실력은 점점 좋아졌고 자신감도 높아졌다.&lt;br /&gt;그러다 다른 사람의 차를 운전해야 하거나, 기존의 차를 떠나보내고 새로운 차를 사야 하는 날이 왔다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;다른 차&lt;/b&gt;의 운전대 뒤에 앉아본 적이&amp;nbsp;없었나? 운전대 뒤에 앉은 게&amp;nbsp;&lt;b&gt;정말 처음이었나?&lt;/b&gt; 사실 처음은 아니었다.&lt;br /&gt;하지만 이전에 차에 탔을 때는 승객일 뿐이었다. 이번에는 모든 것을 제어하는 운전석에 앉아있다.&lt;br /&gt;&lt;br /&gt;다른 차를 운전했을 때, 우리는 자신에게 몇 가지 질문을 던졌다. 열쇠는 어디에 꽂고, 라이트는 어디에 있고, 어떻게 방향 지시등을 사용하고, 어떻게 사이드 미러를 조정하는지.&lt;br /&gt;그 후, 차는 꽤 순조롭게 항해했다. 처음과 비교해서 왜 쉬워진 것일까?&lt;br /&gt;&lt;br /&gt;그 이유는 그 차가 예전 차와 꽤 비슷하기 때문이다. 그것은 차에 필요한 기본적인 것들을 모두 갖고 있고, 그 기본적인 것들은 비슷한 자리에 위치해있다.&lt;br /&gt;&lt;br /&gt;이전 차와 몇 가지가 다르게 구현되었고, 추가된 기능들이 있을 수도 있다. 그 기능들은 처음 운전할 때, 심지어 두 번째 운전할 때도 사용하지 않는다. 하지만 시간이 흘러 우리가 유용하다고 판단되는 차의 새로운 기능들을 자연스럽게 배우게 된다.&lt;br /&gt;&lt;br /&gt;프로그래밍 언어를 배우는 것은 이와 같다. 처음에는 가장 어렵다. 그러나 일단 허리띠를 졸라매면, 그 뒤의 것은 더 쉬워진다.&lt;br /&gt;&lt;br /&gt;제2 외국어를 처음 시작할 때, 당신은 다음과 같은 질문을 묻는다. &quot;모듈을 어떻게 만들지? 배열을 어떻게 탐색하지? substring 함수에서 파라미터는 무엇이지?&quot;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;당신이 이 새로운 언어를 배울 수 있다고 확신한다. 왜냐하면 그 새로운 언어는 당신이 예전에 배웠던 언어들에서 편하게 사용하기 위해 몇 가지 새로운 것들이 추가된 것뿐이기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;당신의 첫 우주선&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_1_3.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HFMAr/btqFwjT6Dnd/ujxG12HiLlLDIdB9Kh5b10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HFMAr/btqFwjT6Dnd/ujxG12HiLlLDIdB9Kh5b10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HFMAr/btqFwjT6Dnd/ujxG12HiLlLDIdB9Kh5b10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHFMAr%2FbtqFwjT6Dnd%2FujxG12HiLlLDIdB9Kh5b10%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;600&quot; height=&quot;303&quot; data-filename=&quot;functional_programming_1_3.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;평생 한 대의 차를 운전해 왔든 수십 대의 차를 운전해 왔든, 당신이 우주선의 운전대 잡게 될 것이라고 생각해 봐라.&lt;br /&gt;&lt;br /&gt;만약 당신이 우주선을 운행할 것이라면, 당신의 운전실력이 우주선을 운행하는 것에 도움을 줄 것이라고 기대하지 않는다. 0부터 다시 배울 것이라고 생각할 것이다.&lt;br /&gt;아마 당신은 그 우주선을 운행하는 것은 지상에서 운전하는 것과 매우 다르다는 생각으로 훈련을 시작할 것이다.&lt;br /&gt;&lt;br /&gt;물리학적으로는 변하지 않았다. 같은 우주에서 운행하는 것뿐이다.&lt;br /&gt;&lt;br /&gt;함수형 프로그래밍을 배우는 것도 마찬가지다. 이제까지 자신이 알고 있던&amp;nbsp; 프로그래밍 방식과 매우 달라질 것이라고 예상해야 한다. 그리고 당신이 알고 있는 기존 프로그래밍에 대한 많은 지식들은 함수형 프로그래밍을 이해하는데 도움이 되지 않을 것이다.&lt;br /&gt;&lt;br /&gt;함수형 프로그래밍은 당신이 생각하는 기존 프로그래밍과 매우 다른 생각을 가지도록 가르쳐줄 것이다. 그만큼 함수형 프로그래밍을 깨닫게 된다면, 당신은 아마 기존 프로그래밍의 사고방식으로 돌아가지 않을 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;당신이 알고 있는 모든 것을 잊어라&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_1_4.png&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uCNEx/btqFvu9ZIzD/v1WW4kVBlMrKKQGkLj2Mg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uCNEx/btqFvu9ZIzD/v1WW4kVBlMrKKQGkLj2Mg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uCNEx/btqFvu9ZIzD/v1WW4kVBlMrKKQGkLj2Mg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuCNEx%2FbtqFvu9ZIzD%2Fv1WW4kVBlMrKKQGkLj2Mg1%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;460&quot; height=&quot;276&quot; data-filename=&quot;functional_programming_1_4.png&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;사람들은 이 구절을 이야기하는 것을 좋아한다. &lt;b&gt;함수형 프로그래밍을 배우는 것은 프로그래밍 공부를 처음부터 시작하는 것과 같다. &lt;/b&gt;기존에 알고 있던 비슷한 많은 개념이 있지만, &lt;b&gt;모든 것을 다시 배워야 한다고 생각하는 것이 가장 좋다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;올바른 관점을 가진다면 올바른 생각을 가질 것이고, 올바른 관점은 개념이 어렵더라도 포기하지 않게 해 줄 것이다.&lt;br /&gt;&lt;br /&gt;당신이 프로그래머로서 작성하던 기존 코드들 중에, 함수형 프로그래밍으로는 대체할 수 없는 부분들이 있다.&lt;br /&gt;&lt;br /&gt;자동차는 후진을 할 수 있다. 하지만 우주선에서는 후진을 할 수 없다. &quot;띠용? 후진을 못한다고?! 후진 없이 어떻게 운전을 하라고?!&quot; 우주선은 3차원 공간에서 조종할 수 있기 때문에 역주행이 필요 없다는 것이 밝혀졌다. 일단 이것을 이해한다면, 당신은 절대 후진을 하지 않을 것이다. 사실 언젠간 당신은 '자동차는 왜 이렇게 제한이 많나?'라고 생각할 날이 올 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;함수형 프로그래밍은 배우는 시간이 좀 걸린다. 그러니 인내심을 가져라.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그러므로 명령형 프로그래밍을 사용하던 냉동 창고에서 벗어나 함수형 프로그래밍이라는 온천에 살짝 들어가 보자.&lt;br /&gt;&lt;br /&gt;앞으로 나올 많은 게시물들은 함수형 프로그래밍의 개념들에 대한 게시물이며, 당신이 함수형 프로그래밍을 배우기 전에 도움이 될 만한 것들이다.&lt;br /&gt;만약 당신이 이미 함수형 프로그래밍에 대해 알고 있다면, 개념을 정리하는데 도움이 될 것이다.&lt;br /&gt;&lt;br /&gt;함수형 프로그래밍을 배우기 위해 서두르지 마라, 지금부터 천천히 읽고 코딩 예제를 이해하는 시간을 가져라.&lt;br /&gt;심지어 당신은 각 섹션이 끝난 후 생각을 정리하기 위해 읽는 것을 멈추고 싶을지도 모른다. 그럼 시간을 충분히 갖고 생각을 다 끝낸 후에 읽으면 된다.&lt;br /&gt;&lt;br /&gt;가장 중요한 것은 당신이 &lt;b&gt;이해하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;순수성&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_1_5.png&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSaoZc/btqFz1ZConk/fT3v6qpgeAqKPq2ZUk1pNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSaoZc/btqFz1ZConk/fT3v6qpgeAqKPq2ZUk1pNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSaoZc/btqFz1ZConk/fT3v6qpgeAqKPq2ZUk1pNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSaoZc%2FbtqFz1ZConk%2FfT3v6qpgeAqKPq2ZUk1pNK%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;450&quot; height=&quot;299&quot; data-filename=&quot;functional_programming_1_5.png&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;함수형 프로그래밍을 사용하는 사람들이 순수성을 말할 때, 순수 함수를 언급한다.&lt;br /&gt;&lt;br /&gt;순수 함수는 매우 간단한 함수이다. 순수 함수는 단지 입력된 파라미터에 따라 동작할 뿐이다.&lt;br /&gt;&lt;br /&gt;아래는 자바스크립트의 순수 함수 예제이다.&lt;/p&gt;
&lt;pre id=&quot;code_1594370247196&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var z = 10;
function add(x, y) {
  return x + y;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 &lt;b&gt;add&lt;/b&gt;함수가 z변수에 접근하지 않는 것에 유의해라. &lt;b&gt;add&lt;/b&gt;함수는 z변수를 읽지도, 쓰지도 않는다. 단지 입력된 파라미 터인 x와 y을 읽고, 두 파라미터를 더해서 결과로 반환할 뿐이다. &lt;br /&gt;&lt;br /&gt;이것이 순수 함수이다. 만약 &lt;b&gt;add&lt;/b&gt;함수가 z에 접근했다면, 더 이상 순수 함수가 아니다.&lt;br /&gt;&lt;br /&gt;여기 또 다른 함수가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1594370251103&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function justTen() {
  return 10;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 &lt;b&gt;justTen&lt;/b&gt;이 순수 함수라면 상수만 반환할 수 있다. 왜?&lt;br /&gt;왜냐하면 파라미터 값을 주지 않았기 때문이다. 순수 함수이기 때문에 입력된 파라미터 외에는 어떠한 것도 접근할 수 없다. 그러므로 이 함수가 반환할 수 있는 것은 상수 밖에 없다.&lt;br /&gt;&lt;br /&gt;순수 함수는 파라미터 값들이 없으면 동작하지 않기 때문에 유용하지 않다. 함수 &lt;b&gt;justTen&lt;/b&gt;이 위와 같이 정의되었다면 차라리 상수 값 10을 정의하여 사용하는 편이 낫다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;가장 &lt;b&gt;유용한&lt;/b&gt;&amp;nbsp;순수 함수는 적어도 하나의 파라미터를 갖고 있어야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 함수를 살펴보자&lt;/p&gt;
&lt;pre id=&quot;code_1594370855625&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function addNoReturn(x, y) {
  var z = x + y
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 아무것도 반환하지 않는다. x와 y를 더한 후 변수 z에 대입하지만, 아무것도 반환은 하지 않는다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;유용한&lt;/b&gt;&amp;nbsp;순수 함수들은 무엇인가를 반환해야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;처음에 봤던 &lt;b&gt;add&lt;/b&gt; 함수를 다시 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1594370935793&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function add(x, y) {
  return x + y;
}
console.log(add(1, 2)); // prints 3
console.log(add(1, 2)); // still prints 3
console.log(add(1, 2)); // WILL ALWAYS print 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;add(1, 2)&lt;/b&gt;은 항상 3을 반환한다는 것을 알 수 있다. 그렇게 놀랄 일이 아니다. 왜냐하면 순수 함수이기 때문이다.&lt;br /&gt;만약 &lt;b&gt;add&lt;/b&gt;&amp;nbsp;함수가 외부 값을 사용했다면, 함수의 동작을 예측할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;순수 함수는 같은 Input이 입력된다면, 항상 같은 Output을 반환한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;순수 함수는 외부 변수를 수정할 수 없다. 아래 함수들은 &lt;b&gt;순수 함수가 아니다&lt;/b&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1594371030038&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;writeFile(fileName);
updateDatabaseTable(sqlCmd);
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 함수들은 모두 &lt;b&gt;부작용(Side Effects)&lt;/b&gt;이 있다. 위 함수들은 호출될 때 파일이나 데이터베이스의 테이블을 조작하고, 데이터를 서버에 보낸다. 그리고 소켓통신을 하기 위해 OS를 호출한다. 위의 함수들은 단순히 입력된 값을 실행하는 것 외에 추가적으로 다른 작업을 한다.&lt;br /&gt;당신은 절대 이 함수들이 어떤 값을 반환할지 예측할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;순수 함수는 부작용(Side Effects)이 없다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Javascript, Java, 그리고 C#과 같은 명령형 프로그래밍 언어에는 &lt;b&gt;어디에나&lt;/b&gt; 부작용이 있다. 이는 프로그램의 &lt;b&gt;어느 곳에서든&lt;/b&gt; 변수가 수정될 수 있기 때문에 디버깅을 매우 어렵게 만든다. 그럼 변수가 잘못된 값으로 바뀌어서 버그가 생겼을 때 어디를 봐야 하나? 모든 코드를 일일이 봐야 하나? 그건 좋지 않은 방법이다.&lt;br /&gt;&lt;br /&gt;이쯤 되면 아마 &quot;순수 함수만으로 어떻게 프로그래밍을 하라는 거야?&quot;라고 생각할 것이다.&lt;br /&gt;&lt;br /&gt;함수형 프로그래밍은 단순히 순수 함수를 작성하는 것만이 아니다.&lt;br /&gt;&lt;br /&gt;함수형 언어들로 코드의 부작용을 완전히 없앨 수는 없다. 단지 부작용을 &lt;b&gt;억제&lt;/b&gt;를 할 뿐이다. 왜냐하면 프로그램은 사용자와 밀접한 관계가 있기 때문에, 순수 함수로만 프로그램을 만들 수 없다.&lt;br /&gt;우리의 목표는 비순수 함수를 최소한으로 하고, 우리의 프로그램의 순수 함수와 분리해서 작성하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;불변성&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_1_6.png&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/exw7r7/btqFAOMimRX/TRZt01MtgQnsKrZA5FlVW1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/exw7r7/btqFAOMimRX/TRZt01MtgQnsKrZA5FlVW1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/exw7r7/btqFAOMimRX/TRZt01MtgQnsKrZA5FlVW1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fexw7r7%2FbtqFAOMimRX%2FTRZt01MtgQnsKrZA5FlVW1%2Fimg.jpg&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;698&quot; height=&quot;390&quot; data-filename=&quot;functional_programming_1_6.png&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;390&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;pre id=&quot;code_1594414805190&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var x = 1;
x = x + 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 이해하려면 수학 시간에 배운 것을 잊어야 한다. 수학에서는 x = x + 1이 성립되지 않는다. 하지만 명령형 프로그래밍에서 위의 식은 x + 1의 결과를 다시 x에 넣으라는 것을 의미한다.&lt;br /&gt;&lt;br /&gt;하지만 함수형 프로그래밍에서 x = x + 1은 불가하다. 그렇기에 수학 시간에 잊었던 것들을 다시 기억해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;함수형 프로그래밍에서는 변수가 &lt;b&gt;없다&lt;/b&gt;.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;저장된 값들은 여전히 변수라고 불린다. 하지만 그것들은 사실 상수이다. 즉, x에 값이 할당되면 계속 그 값이 유지된다.&lt;br /&gt;&lt;br /&gt;걱정하지 마라, x는 지역 변수이기 때문에 생명 주기가 짧다. 그러나 살아있는 동안에는 값이 절대 변할 수 없다.&lt;br /&gt;&lt;br /&gt;아래는 웹 개발자를 위한 순수 함수형 프로그래밍 언어인 Elm를 이용한 상수 변수에 대한 예제이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594415877605&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;addOneToSum y z =
  let
    x = 1
  in
    x + y + z&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ML 스타일의 문법에 익숙하지 않다면 설명해 줄 수 있다. &lt;b&gt;addOneToSum&lt;/b&gt; 함수는 y와 z 파라미터를 사용하는 함수다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;let&lt;/b&gt; 블록 안의 x에는 1이 할당되어 있다. 즉 생명주기 동안 x는 계속 1일 것이다. 이 생명 주기는 함수 실행이 끝날 때 종료될 것이다.&lt;br /&gt;&lt;b&gt;in&lt;/b&gt; 블록 안의 let block, viz.x 에는 정의된 값이 포함될 수 있다. x + y + z의 결과를 반환한다.&lt;br /&gt;&lt;br /&gt;다시 한번 &quot;변수 없이 프로그래밍을 어떻게 하라는 거지?&quot;라는 소리가 나올 것이다.&lt;br /&gt;&lt;br /&gt;언제 변수를 수정하고 싶은지 생각해보자. 떠오르는 2가지의 일반적인 경우가 있다. 바로 다중 값 수정(예: 객체나 레코드의 단일 값 수정)과 단일 값 수정(예: 반복문 카운터)이다.&lt;br /&gt;&lt;br /&gt;함수형 프로그래밍은 값이 수정된 레코드의 복사본을 만들어 레코드의 값을 수정한다. 이를 가능하게 하는 데이터 구조를 이용하여 레코드의 모든 부분을 복사할 필요 없이 효율적으로 이를 수행한다.&lt;br /&gt;&lt;br /&gt;함수형 프로그래밍의 값을 복사하는 방법으로 단일 값도 수정할 수 있다.&lt;br /&gt;&lt;br /&gt;아, 반복문은 사용하지 &lt;b&gt;않는다&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;&quot;뭐? 변수도 없는데 지금 반복문도 없다고 하는 거야? 화가 난다! 화가 나!&quot;&lt;br /&gt;&lt;br /&gt;워 워, 캄다운. 우리가 루프를 할 수 없는 것이 아니라, 단지&lt;b&gt; for, while, do, repeat,&lt;/b&gt; 기타 등등 같은 특정 반복문 구조들을 사용하지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;함수형 프로그래밍은 반복을 하기 위해 재귀 함수를 이용한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기 당신이 자바스크립트에서 반복을 사용할 수 있는 두 가지 방법이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1594416344525&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// simple loop construct
var acc = 0;
for (var i = 1; i &amp;lt;= 10; ++i)
  acc += i;
console.log(acc); // prints 55

// without loop construct or variables (recursion)
function sumRange(start, end, acc) {
  if (start &amp;gt; end)
    return acc;
  return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0)); // prints 55&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;어떻게 재귀 함수가 스스로를 호출하여 for 반복문과 동일한 기능을 할 수 있는지 보자. 재귀 함수는 이전 값들을 수정하지 않는다. 대신에 이전 값들을 계산하고 반환된 새로운 값을 사용한다.&lt;br /&gt;&lt;br /&gt;사실 함수형 프로그래밍을 조금만 공부해보면 자바스크립트에서는 재귀 함수를 찾기 힘들다는 것을 알게 된다. 두 가지 이유가 있다.&lt;br /&gt;첫 번째, 자바스크립트의 문법은 매끄럽지 못하다(Syntax of Javascript is noisy). 두 번째, 당신은 아마 재귀적으로 생각하는 것에 익숙하지 않을 것이다.&lt;br /&gt;&lt;br /&gt;Elm 문법을 보자. 좀 더 읽기 쉽고 이해하기 쉽다.&lt;/p&gt;
&lt;pre id=&quot;code_1594416441829&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sumRange start end acc = 
  if start &amp;gt; end then 
    acc 
  else 
    sumRange (start + 1) end (acc + start) &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행해보면&amp;nbsp;아래와&amp;nbsp;같은&amp;nbsp;결과가&amp;nbsp;나온다.&lt;/p&gt;
&lt;pre id=&quot;code_1594416402896&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sumRange 1 10 0 = -- sumRange (1 + 1) 10 (0 + 1)
sumRange 2 10 1 = -- sumRange (2 + 1) 10 (1 + 2)
sumRange 3 10 3 = -- sumRange (3 + 1) 10 (3 + 3)
sumRange 4 10 6 = -- sumRange (4 + 1) 10 (6 + 4)
sumRange 5 10 10 = -- sumRange (5 + 1) 10 (10 + 5)
sumRange 6 10 15 = -- sumRange (6 + 1) 10 (15 + 6)
sumRange 7 10 21 = -- sumRange (7 + 1) 10 (21 + 7)
sumRange 8 10 28 = -- sumRange (8 + 1) 10 (28 + 8)
sumRange 9 10 36 = -- sumRange (9 + 1) 10 (36 + 9)
sumRange 10 10 45 = -- sumRange (10 + 1) 10 (45 + 10)
sumRange 11 10 55 = -- 11 &amp;gt; 10 =&amp;gt; 55
55&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신은 아마 &lt;b&gt;for&lt;/b&gt; 반복문이 훨씬 이해하기 쉽다고 생각할 것이다. 논쟁의 여지가 있고, 좀 더 &lt;b&gt;친숙함&lt;/b&gt;의 문제가 있지만, &lt;b&gt;비 재귀 함수의 반복문은 불변하지 않기(Mutaility) 때문에 좋지 않다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;여기서 불변성에 대해서 전부 설명하긴 힘들다. 불변성에 좀 더 공부하고 싶다면 &lt;a href=&quot;https://medium.com/@cscalfani/why-programmers-need-limits-3d96e1a0a6db&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Why Programmers Need Limits&lt;/a&gt;의 Global Mutable State 섹션을 보아라.&lt;br /&gt;&lt;br /&gt;불변성의 확실한 장점은 만약 당신의 프로그램에 있는 변수에 접근할 경우, 당신은 오직 &lt;b&gt;읽기&lt;/b&gt;만 할 수 있다. 즉, 아무도 그 값을 바꿀 수 없다는 뜻이다. 그렇기 때문에 어떠한 경우도 값이 바뀔 일이 없다.&lt;br /&gt;&lt;br /&gt;그리고 만약 프로그램이 멀티 스레드라면, A 스레드가 B 스레드의 값을 변경할 수 없다. 값은 변하지 않으며, 만약 A 스레드에서 B스레드의 값을 수정하려면 기존 값으로부터 새로운 값을 만들어야 한다.&lt;br /&gt;&lt;br /&gt;나는 90년대 중반에 &lt;a href=&quot;https://www.youtube.com/watch?v=uIOYSjBRORM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Creature Crunch&lt;/a&gt; 라는 게임 엔진을 개발했었다. 그 엔진의 버그가 발생하는 가장 큰 원인은 멀티 스레드 문제였다. 그때 불변성에 대해 알았으면 좋았을 것이다. 하지만 그 당시 나는 2x와 4x CD-ROM 드라이브에서 실행되는 게임의 성능을 비교하는 것에 열중해 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;불변성은 좀 더 간단하고, 안전한 코드를 만들어준다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;머리 아파! 이제 한계야!&lt;/h3&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;widthContent&quot; data-filename=&quot;functional_programming_1_7.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qJH9d/btqFAfp4rxZ/xvsNuk8Pqnp92BKm1BGxxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qJH9d/btqFAfp4rxZ/xvsNuk8Pqnp92BKm1BGxxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qJH9d/btqFAfp4rxZ/xvsNuk8Pqnp92BKm1BGxxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqJH9d%2FbtqFAfp4rxZ%2FxvsNuk8Pqnp92BKm1BGxxk%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;300&quot; height=&quot;225&quot; data-filename=&quot;functional_programming_1_7.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;오늘은 여기까지.&lt;br /&gt;&lt;br /&gt;이 이후에는 고차 함수, 합성 함수, 커링 등에 대해서 포스팅할 예정이다.&lt;br /&gt;&lt;br /&gt;다음 게시물 : &lt;a href=&quot;https://soojae.tistory.com/30&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Part2&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;글에 번역 오류가 있으면 알려주세요. 감사합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Knowledge/Web</category>
      <category>Functional Programming</category>
      <category>javascript</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/29</guid>
      <comments>https://soojae.tistory.com/29#entry29comment</comments>
      <pubDate>Sat, 11 Jul 2020 15:49:38 +0900</pubDate>
    </item>
    <item>
      <title>[MacOS][Docker] Jenkins - 2. Jenkins 설정</title>
      <link>https://soojae.tistory.com/26</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;jenkins.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VDM8Z/btqCjRtT2mP/AnyQBUNTBTZFyNoB6zwUJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VDM8Z/btqCjRtT2mP/AnyQBUNTBTZFyNoB6zwUJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VDM8Z/btqCjRtT2mP/AnyQBUNTBTZFyNoB6zwUJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVDM8Z%2FbtqCjRtT2mP%2FAnyQBUNTBTZFyNoB6zwUJ0%2Fimg.png&quot; data-filename=&quot;jenkins.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&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;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;docker-logo.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1208&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b14BmM/btqCnZ5CxSQ/psirjhuUCwUc4xF568ccPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b14BmM/btqCnZ5CxSQ/psirjhuUCwUc4xF568ccPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b14BmM/btqCnZ5CxSQ/psirjhuUCwUc4xF568ccPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb14BmM%2FbtqCnZ5CxSQ%2FpsirjhuUCwUc4xF568ccPk%2Fimg.png&quot; data-filename=&quot;docker-logo.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1208&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;h2 data-ke-size=&quot;size26&quot;&gt;1. Plugin&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 플러그인을 설치해봅시다. 저는 빌드할 때 npm 명령어를 사용해야 해서 NodeJS 플러그인을 설치하겠습니다.&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Jenkins 관리 - 플러그인 관리를 클릭합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;788&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wD2IF/btqCrj3aHAm/28R7OlcjEQOj6b46AGlkmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wD2IF/btqCrj3aHAm/28R7OlcjEQOj6b46AGlkmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wD2IF/btqCrj3aHAm/28R7OlcjEQOj6b46AGlkmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwD2IF%2FbtqCrj3aHAm%2F28R7OlcjEQOj6b46AGlkmk%2Fimg.png&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;788&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;그 후에 &lt;b&gt;설치 가능&lt;/b&gt; 탭을 누르고, &lt;b&gt;필터&lt;/b&gt;에 필요한 플러그인을 검색합니다.&lt;br /&gt;그 후에 체크박스를 누르고 &lt;b&gt;재시작 없이 설치하기&lt;/b&gt;를 클릭합니다. (플러그인 설치 실패시&amp;nbsp;&lt;b&gt;지금 다운로드하고 재시작 후 설치하기&lt;/b&gt;로 설치해보시기 바랍니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;541&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blzwBJ/btqCqMR1ttR/Na54JIlrmWrXjoJxzN7qK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blzwBJ/btqCqMR1ttR/Na54JIlrmWrXjoJxzN7qK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blzwBJ/btqCqMR1ttR/Na54JIlrmWrXjoJxzN7qK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblzwBJ%2FbtqCqMR1ttR%2FNa54JIlrmWrXjoJxzN7qK1%2Fimg.png&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;541&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;h2 data-ke-size=&quot;size26&quot;&gt;2. Global Tool Configuration&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젠킨스 전체에서 사용할 도구를 설정하는 페이지입니다.&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Jenkins 관리 - Global Tool Configuration을 클릭합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1141&quot; data-origin-height=&quot;783&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kNuj4/btqCqNQRJGw/W7376NwWwvLnVu5ykwpkF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kNuj4/btqCqNQRJGw/W7376NwWwvLnVu5ykwpkF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kNuj4/btqCqNQRJGw/W7376NwWwvLnVu5ykwpkF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkNuj4%2FbtqCqNQRJGw%2FW7376NwWwvLnVu5ykwpkF0%2Fimg.png&quot; data-origin-width=&quot;1141&quot; data-origin-height=&quot;783&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;다음과 같은 화면이 나옵니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1365&quot; data-origin-height=&quot;875&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rt0Fu/btqCn0Q3C2V/QUUxO9oK7oNO2XgEAKQED0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rt0Fu/btqCn0Q3C2V/QUUxO9oK7oNO2XgEAKQED0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rt0Fu/btqCn0Q3C2V/QUUxO9oK7oNO2XgEAKQED0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frt0Fu%2FbtqCn0Q3C2V%2FQUUxO9oK7oNO2XgEAKQED0%2Fimg.png&quot; data-origin-width=&quot;1365&quot; data-origin-height=&quot;875&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;JDK, Git은 대부분의 프로젝트에서 사용됩니다. 저는 추가로 Maven와 NodeJS를 설치하겠습니다.&lt;br /&gt;Jenkins 도커 컨테이너에는 Git과 JDK는 기본적으로 설치되어 있습니다.&lt;br /&gt;Kinematic에 Settings - General로 가서 JAVA_HOME을 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;909&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHFG0I/btqClyne86y/ETdAofJIMEhkaHgc08zbek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHFG0I/btqClyne86y/ETdAofJIMEhkaHgc08zbek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHFG0I/btqClyne86y/ETdAofJIMEhkaHgc08zbek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHFG0I%2FbtqClyne86y%2FETdAofJIMEhkaHgc08zbek%2Fimg.png&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;909&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;위에서 확인한 JAVA_HOME의 경로를 아래와 같이 써줍시다. Name은 변수로 사용될 이름입니다.&lt;br /&gt;Git은 아래와 같이 설정해주시면 됩니다. Git은 따로 Name을 변경하지 않고 경로만 잡아주면 git 명령어를 사용할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1141&quot; data-origin-height=&quot;783&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xk5bj/btqCpRlNJpP/Sj1MUVrfQtfxkuSBht7j91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xk5bj/btqCpRlNJpP/Sj1MUVrfQtfxkuSBht7j91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xk5bj/btqCpRlNJpP/Sj1MUVrfQtfxkuSBht7j91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxk5bj%2FbtqCpRlNJpP%2FSj1MUVrfQtfxkuSBht7j91%2Fimg.png&quot; data-origin-width=&quot;1141&quot; data-origin-height=&quot;783&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven과 Nodejs는 젠킨스에서 자동으로 설치되도록 설정해줍니다. 그 후에 Save 버튼을 눌러주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1511&quot; data-origin-height=&quot;885&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SnBMf/btqCoRzziWB/77fkPW1JdVJUqDDE6bCHM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SnBMf/btqCoRzziWB/77fkPW1JdVJUqDDE6bCHM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SnBMf/btqCoRzziWB/77fkPW1JdVJUqDDE6bCHM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSnBMf%2FbtqCoRzziWB%2F77fkPW1JdVJUqDDE6bCHM0%2Fimg.png&quot; data-origin-width=&quot;1511&quot; data-origin-height=&quot;885&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;&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins 초기 세팅 (Global Tool Configuration): &lt;a href=&quot;https://blog.jiniworld.me/22&quot;&gt;https://blog.jiniworld.me/22&lt;/a&gt;&lt;/p&gt;</description>
      <category>Infra</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/26</guid>
      <comments>https://soojae.tistory.com/26#entry26comment</comments>
      <pubDate>Mon, 2 Mar 2020 18:32:51 +0900</pubDate>
    </item>
    <item>
      <title>[Ubuntu 18.04] 우분투에 NodeJS 설치</title>
      <link>https://soojae.tistory.com/25</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Selection_006.png&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjfl3O/btqCjR1ENof/A86iKHYrkfKSxAQogTHVDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjfl3O/btqCjR1ENof/A86iKHYrkfKSxAQogTHVDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjfl3O/btqCjR1ENof/A86iKHYrkfKSxAQogTHVDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjfl3O%2FbtqCjR1ENof%2FA86iKHYrkfKSxAQogTHVDK%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;591&quot; height=&quot;365&quot; data-filename=&quot;Selection_006.png&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CURL 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1583070162661&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get install curl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. PPA를 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1583070174395&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. NodeJS 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1583070180820&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get install -y nodejs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. build-essential 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PPA를 통해서 NodeJS를 설치하면 NodeJS 뿐만 아니라 npm도 같이 설치됩니다. 하지만 npm install시 에러가 나는 것을 방지하여 build-essential을 설치해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1583070209743&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get install build-essential&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Ubuntu 16.04] node.js 와 npm 설치: &lt;a href=&quot;https://itstory.tk/entry/Ubuntu-1604-nodejs-%EC%99%80-npm-%EC%84%A4%EC%B9%98&quot;&gt;https://itstory.tk/entry/Ubuntu-1604-nodejs-와-npm-설치&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra</category>
      <author>SooJae</author>
      <guid isPermaLink="true">https://soojae.tistory.com/25</guid>
      <comments>https://soojae.tistory.com/25#entry25comment</comments>
      <pubDate>Sun, 1 Mar 2020 22:41:29 +0900</pubDate>
    </item>
  </channel>
</rss>