<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발로그</title>
    <link>https://soobakba.tistory.com/</link>
    <description>개발을 하자. 글도 쓰자.</description>
    <language>ko</language>
    <pubDate>Fri, 21 Aug 2026 01:41:45 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>수박소바</managingEditor>
    <image>
      <title>개발로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3103774/attach/27cf1204949942b08bd5b460d387aa21</url>
      <link>https://soobakba.tistory.com</link>
    </image>
    <item>
      <title>[번역] 프론트엔드 개발자가 알아야 할 숫자들 (Latency Numbers)</title>
      <link>https://soobakba.tistory.com/59</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;원본: &lt;a href=&quot;https://vercel.com/blog/latency-numbers-every-web-developer-should-know&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://vercel.com/blog/latency-numbers-every-web-developer-should-know&lt;/span&gt;&lt;/a&gt;&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-filename=&quot;latency.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/bXDeJV/btsHRlUyT7B/KlkbiqgaHFKAbYo2Krklh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXDeJV/btsHRlUyT7B/KlkbiqgaHFKAbYo2Krklh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXDeJV/btsHRlUyT7B/KlkbiqgaHFKAbYo2Krklh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXDeJV%2FbtsHRlUyT7B%2FKlkbiqgaHFKAbYo2Krklh1%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;latency.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 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;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;웹 앱에서 웹 페이지 로드 시간과 사용자 액션에 대한 반응성은 사용자 만족도의 주요 요인이며, 두 가지 모두 네트워크 대기 시간에 의해 지배되는 경우가 많습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;지연 시간 자체는 사용자가 인터넷에 연결(와이파이, LTE, 5G)하고, 사용자가 연결하고 있는 서버의 거리와 그 사이의 네트워크 품질의 함수입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;지연 시간 숫자는 그 자체로 낮게 보일 수 있지만 빠르게 결합됩니다. 예를 들어, 300ms 링크의 3 depth 네트워크 폭포는 총 지연 시간 900ms로 이어집니다. 리액트 서버 컴포넌트와 같은 기술은 네트워크 폭포를 동일한 요청 패턴이 100배 더 빠를 수 있는 서버로 이동시킬 수 있습니다.&lt;/span&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;table style=&quot;border-collapse: collapse; width: 100%; height: 170px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #171717; text-align: start;&quot;&gt;Metric (&lt;/span&gt;측정 기준)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;b&gt;추정 시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #171717; text-align: start;&quot;&gt;Metric Impact (&lt;/span&gt;측정 영향)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;인터넷 와이파이 지연 시간&lt;/b&gt; &lt;br /&gt;와이파이는 연결에 최소한의 지연 시간을 추가합니다. 이것은 신호가 약하거나 오래된 하드웨어로 인해 증가할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;1-4ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;인터넷으로의 5G 고대역(밀리미터파) 대기 시간&lt;/b&gt; &lt;br /&gt;밀리미터파는 가장 빠르게 전개되는 모바일 기술입니다. 그러나 전파탑까지 시야가 확보된 밀집된 도시 지역에서만 사용하기에 실용적입니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;1-5ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;초당 60프레임에 대한 프레임당 사용자 공간 예산&lt;/b&gt; &lt;br /&gt;60fps 디바이스에서는 프레임이 16ms마다 그려집니다. 그러나 실제로 프레임을 처리하는 데 시간이 조금 필요합니다. 여기서 시간은 코드가 무엇을 칠해야 하는지 계산하는 데 사용할 수 있는 시간입니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;5-10ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;매끄러운 프레임 속도&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;5G 중대역 인터넷 대기시간&lt;/b&gt; &lt;br /&gt;정규 5G 대기시간입니다. 신호가 나쁘거나 타워에 과부하가 걸린 경우 경험치가 달라질 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;10-30ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;동일한 클라우드 영역에 있는 서비스 또는 데이터베이스에 대한 왕복 대기 시간&lt;/b&gt;&lt;br /&gt;인터넷에 연결하지 않고 사용자와 가까운 곳에 배치된 다른 서비스에 대한 대기 시간입니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;10ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;LTE &lt;b&gt;인터넷&lt;/b&gt; 지연 시간&lt;/b&gt; &lt;br /&gt;4G 셀룰러 네트워크인 LTE를 위한 일반적인 지연 시간&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;15-50ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;초당 60프레임의 프레임 지속 시간(frame duration)&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;초당 60프레임&lt;/span&gt;은 디스플레이 장치에서 가장 일반적인 프레임 속도입니다. 그러나 일부 최신 장치는 90 또는 120fps와 같은 더 높은 프레임 속도를 지원합니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;16ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;매끄러운&lt;/span&gt;&amp;nbsp;프레임 속도&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;같은 대륙의 다른 도시로의 왕복 대기 시간&lt;/b&gt; &lt;br /&gt;사용자와 같은 대륙의 지역에 배포할 경우 예상할 수 있는 대기 시간입니다. 5000km 거리에 대해 계산되므로 실제 대기 시간은 약간 더 높거나 낮을 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;33ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 56.124%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;시간이 경과함에 따라 인간이 지각하는 가장 짧은 시간&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;사용자의 입력에 응답할 때 이 시간 이하로 머무르는 것은 사용자가 응답을 즉각적으로 인식한다는 것을 의미합니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;40-80ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;INP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 56.124%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;1MB의 CSS를 파싱하는 시간&lt;/b&gt; &lt;br /&gt;CSS를 파싱하는 것은 브라우저가 웹페이지를 렌더링하기 위해 수행해야 하는 작업의 일부입니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;100ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 56.124%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;1MB의 HTML을 파싱하는 시간&lt;/b&gt;&lt;br /&gt;파싱 HTML은 브라우저가 웹 페이지를 렌더링하기 위해 수행해야 하는 작업의 일부입니다. 짧은 웹 페이지에서는 무시할 수 있는 경우가 많지만, 매우 긴 기사의 주요 요소가 될 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;120ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 56.124%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;3G &lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;인터넷&lt;span&gt; 지연 시간&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;3G 지연 시간은 오늘날 일반적으로 가장 느린 셀룰러 표준입니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;150ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 56.124%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;고품질 네트워크를 통해 지구 반대편까지 왕복 대기 시간(cold-potato-routing)&lt;/b&gt; &lt;br /&gt;단일 지역에 서비스를 배포할 경우 확인해야 할 최악의 대기 시간입니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;150ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 56.124%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;1MB의 JS를 파싱하는 시간&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;자바스크립트 파싱&lt;/span&gt;은 CSS와 JS보다 더 빠르게 성장하는 경우가 많기 때문에 페이지 로드 시간에 큰 영향을 미칠 수 있습니다. 코드 분할은 JS 크기를 최소화하는 주요 기법입니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;150ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;FCP, LCP, INP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 56.124%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;인간이 부진하다고 인식하는 시간&lt;/b&gt; &lt;br /&gt;사용자의 입력에 반응할 때 이 값보다 느린 응답은 기다려야 하는 것으로 인식됩니다. 200ms는 INP에서 &quot;개선이 필요하다&quot;는 기준이기도 합니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;200ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;INP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 56.124%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;b&gt;임대 광섬유 없이 지구 반대편까지 왕복 대기 시간( hot-potato-routing)&lt;/b&gt; &lt;br /&gt;사용자가 직접 멀리 떨어진 서버에 연결하거나 저렴한 CDN을 사용할 경우 바이트를 가장 저렴한 경로로 통과할 때부터 사용자의 대기 시간이 두 배로 증가할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.2868%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;300ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5891%;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;TTFB, FCP, LCP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;방법론&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;기기에 의존하는 숫자는 2023년형 하이엔드 안드로이드 폰으로 측정됩니다. 대부분의 노트북은 더 빨라질 것이고, 아이폰은 더 빨라질 것이며, 다른 많은 기기들은 더 느려질 것입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;크레딧&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd; color: #000000; text-align: start;&quot;&gt;제프 딘(Jeff Dean)의 &lt;a href=&quot;https://brenocon.com/dean_perf.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;모든 프로그래머가 알아야 할 최신 번호&quot;&lt;/a&gt;에서 영감을 얻었습니다. 이와 같은 더 많은 숫자는 &lt;a href=&quot;https://github.com/sirupsen/napkin-math&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;냅킨 수학(Napkin Math)&lt;/a&gt;을 참조하십시오.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>FRONT-END</category>
      <category>latency</category>
      <category>network</category>
      <category>Numbers</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/59</guid>
      <comments>https://soobakba.tistory.com/59#entry59comment</comments>
      <pubDate>Thu, 6 Jun 2024 17:20:29 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드 E2E 테스트 &amp;quot;잘&amp;quot; 적용하기 (컨벤션)</title>
      <link>https://soobakba.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 팀 내에서 e2e 테스트를 도입하기 위해 초기 컨벤션을 적용하는 작업을 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 테스트 도구에서 제시하는 문법이나 구조를 따르지만 이 외에 팀원들과 논의하여 적용한 부분들도 있어서 소개해보고자 한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&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;1200&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tdFTT/btsDkN3zFjk/Ta9AaFKXnkEOuhgEEm6GP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tdFTT/btsDkN3zFjk/Ta9AaFKXnkEOuhgEEm6GP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tdFTT/btsDkN3zFjk/Ta9AaFKXnkEOuhgEEm6GP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtdFTT%2FbtsDkN3zFjk%2FTa9AaFKXnkEOuhgEEm6GP1%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;1200&quot; height=&quot;730&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Cypress vs Playwright&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 기존에 cypress로 작성된 e2e 코드가 있었기 때문에 이것을 정상화시키는 동시에 커버리지를 올린다는 생각으로 그대로 진행했었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 생각보다 Cypress 실행속도가 느렸고, 다른 팀에서도 Playwright를 대부분 도입했다고 하여 재검토 요청을 받아 Playwright를 처음으로 사용해보았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;솔직히 여기저기서 많이 언급하는 실행속도의 경우는 개인적으로 엄청 큰 차이는 못느꼈다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 여러 브라우저 지원의 경우도 우리는 기존에도 크로미움만 주로 지원해서 엄청난 메리트로 느끼지는 못했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 Playwright를 직접 사용해보니 사용성 측면에서 DX가 굉장히 좋았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설치부터 테스트 실행까지 Cypress에 비해 편리하다는 느낌을 받았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;계속 테스트 코드를 작성하고 실행시켜보고 싶은 느낌이랄까..?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;IDE 내에서 각 TC마다 간편하게 실행시켜볼 수 있는것도 그렇고, 익스텐션에서 제공하는 셀렉터를 자동으로 추천해주는 Pick locator 기능도 굉장히 좋았다. 그리고 아직 사용해보지 않았지만 인터렉션에 따라 자동으로 테스트 코드를 작성해주는 Codegen 기능도 있다고 하여 바로 Playwright로 갈아타게 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 기존에 작성한 Cypress 기반의 컨벤션 내용도 다양한 레퍼런스를 통해 정리한 것이라서 (참고로 인강까지 결제함...)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;팀에서는 버려진 내용이지만^^ 누군가에게는 도움이 되길 바라며 같이 올려본다..!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;목표&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;s&gt;&lt;i&gt;(Cypress, Playwright 동일 내용이라 한번만 작성)&lt;/i&gt;&lt;/s&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;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨벤션 (Cypress)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 경우 Cypress와 Cypress Testing Library 문법과 컨벤션을 따르기 때문에 공식문서를 참고하시면 좋습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Description은 가급적 한글로 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 봐도 이해할 수 있는 기능명세서 역할을 하도록 빠른 파악을 위해 한글로 작성하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, Selector나 Assertion은 한글이 아닐 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다국어 테스트의 경우에도 description은 한글로 작성하고자 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DOM 구조 혹은 특정 기술에 의존하지 않는 구문으로 작성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://testing-library.com/docs/cypress-testing-library/intro/&quot;&gt;Cypress Testing Library&lt;/a&gt; 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cypress의 get API를 사용해도 DOM 접근이 가능하지만 CSS 셀렉터와 비슷한 구문을 사용하기 때문에 구현에 의존적인 선택자를 작성하게 되는 단점이 있습니다.&lt;/li&gt;
&lt;li&gt;좀 더 사용자 친화적인 구문을 작성할 수 있도록 Testing Library를 사용하여 작성하면 아래와 같이 요소를 탐색할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 구현에 의존적인 선택자로 요소 탐색
cy.get('.list &amp;gt; li')

// 사용자가 앱을 사용하는 방식과 유사하게 요소 탐색
cy.findByLabelText('상품명')
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;사용자 정의 속성 (data-testid)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반복되는 구문 및 Util성 구문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cypress에서 제공하는 &lt;a href=&quot;https://docs.cypress.io/api/cypress-api/custom-commands&quot;&gt;Custom Commands&lt;/a&gt; 를 사용하여 Cypress 구문을 확장합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래는 Url 검증 코드와 로그인 코드를 Custom Commands로 선언하여 사용한 예시입니다.&lt;/li&gt;
&lt;li&gt;이와 같이 여러 테스트파일에 걸쳐 반복적으로 사용되는 구문을 아래와 같이 선언하여 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 선언
Cypress.Commands.add('assertUrl', url =&amp;gt; {
  cy.url().should('eq', `${Cypress.env('baseUrl')}${url}`);
});

// 사용
cy.assertUrl('/account/sign-in');
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 선언
Cypress.Commands.add('signIn', () =&amp;gt; {
  localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN, ACCESS_TOKEN);
	// 중략...
});

// 사용
cy.signIn();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;모든 반복 코드를 Cypress API로 확장할 필요는 없으며, 동일한 페이지 내에서 반복되는 구문은 확장 없이 해당 페이지 내에서 함수를 만들어서 사용해도 무방할 것 같습니다.&lt;/li&gt;
&lt;li&gt;그 외 반복적인 selector나 intercept의 경우 &lt;a href=&quot;https://docs.cypress.io/api/commands/as&quot;&gt;as 구문&lt;/a&gt;을 사용하여 alias를 지정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704974197125&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 선언
cy.findAllByRole('table').eq(1).findAllByRole('row').as('itemList');

// 사용
cy.get('@itemList').findByText('1개').should('exist');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API Mocking&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cypress의 &lt;b&gt;&lt;a href=&quot;https://docs.cypress.io/api/commands/intercept&quot;&gt;intercept&lt;/a&gt; API&lt;/b&gt;를 사용하여 API를 stubbing, spying 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Stubbing: 네트워크 호출에 대해 미리 정해진 응답을 반환하는 것&lt;br /&gt;Spying: 네트워크 호출이 어떻게 이루어졌는지 요청/응답에 대해 기록&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;mocking 응답에 대한 json 객체는 fixtures 폴더 하위에 구성되어 있으며 해당 응답을 가지고 아래와 같이 작성하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;import fixturesUserInfo from '../fixtures/user/info.json';

cy.apiIntercept('GET', '/.api/users', {
  statusCode: 200,
  body: fixturesUserInfo,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 API path에 따른 응답값은 이미 정해져 있기 때문에 호출할 때 마다 해당 json을 불러오는 것은 불편할 것으로 보여 이 부분은 추가적으로 검토한 후 다시 정리하도록 하겠습니다.&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;a href=&quot;https://docs.cypress.io/guides/continuous-integration/github-actions&quot;&gt;cypress에서 github action&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;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 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;컨벤션 (Playwright)&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 경우 Playwright 문법과 컨벤션을 따르기 때문에 공식문서를 참고하시면 좋습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-1-폴더-구조&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/day1snowball/zerobase/wiki/E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A8%EB%B2%A4%EC%85%98#1-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;폴더 구조&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래와 같은 폴더 구조를 가집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704974382901&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;e2e
ㄴ fixture // mocking용 json 파일 묶음 폴더
ㄴ support // 공용 유틸성 파일 묶음 폴더
ㄴ {도메인}
  ㄴ xxx.spec.ts
ㄴ xxx.spec.ts&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 파일은 1depth, 2depth 혼재되는 방식으로, 기본적으로 1depth로 하고, 테스트 파일이 길어지거나 맥락별로 파일을 분리할 필요가 있다면 2depth로 진행합니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-3-description은-가급적-한글로-작성&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/day1snowball/zerobase/wiki/E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A8%EB%B2%A4%EC%85%98#3-description%EC%9D%80-%EA%B0%80%EA%B8%89%EC%A0%81-%ED%95%9C%EA%B8%80%EB%A1%9C-%EC%9E%91%EC%84%B1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Description은 가급적 한글로 작성&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;누가 봐도 이해할 수 있는 기능명세서 역할을 하도록 빠른 파악을 위해 한글로 작성하도록 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론, Selector나 Assertion은 한글이 아닐 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다국어 테스트의 경우에도 description은 한글로 작성하고자 합니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-4-dom-구조-혹은-특정-기술에-의존하지-않는-셀렉터-사용&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/day1snowball/zerobase/wiki/E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A8%EB%B2%A4%EC%85%98#4-dom-%EA%B5%AC%EC%A1%B0-%ED%98%B9%EC%9D%80-%ED%8A%B9%EC%A0%95-%EA%B8%B0%EC%88%A0%EC%97%90-%EC%9D%98%EC%A1%B4%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%85%80%EB%A0%89%ED%84%B0-%EC%82%AC%EC%9A%A9&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;DOM 구조 혹은 특정 기술에 의존하지 않는 셀렉터 사용&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마크업 클래스보다는 더 명시적인 값으로 요소 선택하기 (&lt;a style=&quot;color: #000000;&quot; href=&quot;https://playwright.dev/docs/locators&quot;&gt;Locators 문서&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 친화적인 값&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704974620191&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//  
page.locator('button.buttonIcon.episode-actions-later');

//  
page.getByRole('button', { name: 'submit' });&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 정의 속성 (data-e2e)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704974610385&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Dialog = () =&amp;gt; {
  return (
    &amp;lt;div data-testid=&quot;dialog&quot;&amp;gt;
      // ...
    &amp;lt;/div&amp;gt;
  )
}

page.getByTestId('dialog')&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright&quot;&gt;Playwright Test for VSCode&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;익스텐션을 설치하면 Pick locator 기능을 지원하여 마우스 커서를 올려 손쉽게 요소 선택에 대한 추천을 받을 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VSCode에서 Pick locator 선택&lt;/li&gt;
&lt;/ul&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;508&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sBsw5/btsDiRsajlS/MFBNVm2kLOe24oJl8Jyvx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sBsw5/btsDiRsajlS/MFBNVm2kLOe24oJl8Jyvx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sBsw5/btsDiRsajlS/MFBNVm2kLOe24oJl8Jyvx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsBsw5%2FbtsDiRsajlS%2FMFBNVm2kLOe24oJl8Jyvx1%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;293&quot; height=&quot;217&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;376&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;열린 브라우저에서 특정 요소 위에 마우스를 올리면 아래와 같이 셀렉터 추천&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;464&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7ERZn/btsDkPNPjsd/gOUSGS8Peb1CWJVcDcV4l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7ERZn/btsDkPNPjsd/gOUSGS8Peb1CWJVcDcV4l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7ERZn/btsDkPNPjsd/gOUSGS8Peb1CWJVcDcV4l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7ERZn%2FbtsDkPNPjsd%2FgOUSGS8Peb1CWJVcDcV4l0%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;464&quot; height=&quot;307&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-5-pageevaluate-사용&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/day1snowball/zerobase/wiki/E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A8%EB%B2%A4%EC%85%98#5-pageevaluate-%EC%82%AC%EC%9A%A9&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;page.evaluate() 사용&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Playwright scripts는 브라우저와는 다른 환경에서 실행되기 때문에 Web API 처럼 브라우저 환경에서 접근 가능한 객체에 접근하려면 page.evaluate() API를 사용해야 합니다. (&lt;a style=&quot;color: #000000;&quot; href=&quot;https://playwright.dev/docs/evaluating&quot;&gt;링크&lt;/a&gt;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 로그인 환경을 만들기 위해 localStorage에 접근하는 코드입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1704974949602&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async signin() {
  await this.page.evaluate((token) =&amp;gt; 
		localStorage.setItem('access-token', token), 'foo');
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-6-pompage-object-models-사용&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/day1snowball/zerobase/wiki/E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A8%EB%B2%A4%EC%85%98#6-pompage-object-models-%EC%82%AC%EC%9A%A9&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;POM(Page Object Models) 사용&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Playwright에서 제시하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://playwright.dev/docs/pom&quot;&gt;Page Object Models&lt;/a&gt;을 사용하여 재사용 가능한 코드를 관리합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&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;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 예시는 로그인과 같은 일반적인 작업을 캡슐화하기 위해 Common이라는 helper class를 만들었습니다. (네이밍 변경 가능)&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1704975109895&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// e2e/support/common.ts
export default class Common {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async signin() {
    await this.page.evaluate((token) =&amp;gt; localStorage.setItem('access-token', token), 'foo');

    await this.page.route('/.api/users', async (route) =&amp;gt; {
      await route.fulfill({ json: fixturesUserInfo });
    });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 테스트 코드에서 Common을 사용하여 손쉽게 로그인 로직을 호출할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1704975134895&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// e2e/main.spec.ts
test('로그인시 &quot;장바구니&quot;와 &quot;마이페이지&quot; 버튼이 보인다', async ({ page }) =&amp;gt; {
  const common = new Common(page);
  await common.signin();

  await expect(page.getByLabel('장바구니')).toBeVisible();
  await expect(page.getByLabel('마이페이지')).toBeVisible();
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-7-api-mocking&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/day1snowball/zerobase/wiki/E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A8%EB%B2%A4%EC%85%98#7-api-mocking&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;API Mocking&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;stubbing할 응답은&lt;span&gt;&amp;nbsp;&lt;/span&gt;/tests/e2e/fixtures&lt;span&gt;&amp;nbsp;&lt;/span&gt;경로에 json 파일로 존재합니다. 기존에 cypress용으로 존재하던 파일들을 그대로 옮겨온 것이라 수정이 필요할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1704975182434&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import fixturesUserInfo from '../fixtures/user/info.json';

await this.page.route('/.api/users', async (route) =&amp;gt; {
  await route.fulfill({ json: fixturesUserInfo });
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 API path에 따른 응답값은 이미 정해져 있기 때문에 호출할 때 마다 해당 fixture를 불러오는 것은 불편할 것으로 보여 이 부분은 추가적으로 검토한 후 다시 정리하도록 하겠습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-9-ci&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/day1snowball/zerobase/wiki/E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A8%EB%B2%A4%EC%85%98#9-ci&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;CI&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;playwright 설치 시 자동 추가된&lt;span&gt;&amp;nbsp;&lt;/span&gt;playwright.yml&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 활용하여 테스트 해보고 업데이트 예정입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프론트엔드</category>
      <category>Cypress</category>
      <category>e2e</category>
      <category>playwright</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/58</guid>
      <comments>https://soobakba.tistory.com/58#entry58comment</comments>
      <pubDate>Thu, 11 Jan 2024 21:22:05 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] React Native 폰트 추가 이후 빌드 실패 삽질 기록</title>
      <link>https://soobakba.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;팀원 중 한명이 RN 프로젝트에 새로운 폰트를 추가했고 나는 그걸 pull 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 빌드는 잘 됐는데 unrecognized font family 에러가 떴다. 대충 이렇게 생긴 에러다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-04-21 오전 9.08.33.png&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QG9tJ/btrzZNSWIzt/7LCAozNkkJFFQOsYiWK4Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QG9tJ/btrzZNSWIzt/7LCAozNkkJFFQOsYiWK4Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QG9tJ/btrzZNSWIzt/7LCAozNkkJFFQOsYiWK4Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQG9tJ%2FbtrzZNSWIzt%2F7LCAozNkkJFFQOsYiWK4Bk%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;394&quot; height=&quot;410&quot; data-filename=&quot;스크린샷 2022-04-21 오전 9.08.33.png&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;816&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_1650499651175&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx react-native link --verbose&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거 폰트 관련 에러 검색할 때도 나왔던거 같은데 아직 정확히 react-native link가 뭐하는 앤지는 모르겠음...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 저거 치니까 처음에 다시 리로드 되서 잘 나오나 싶더니 로그인 버튼 누르니까 앱이 죽어버림.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 빌드 시도하니까 실패.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후로 pod install, pod update, pod install --repo-update 등등 다해보고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node_modules 지웠다가 다시 깔고 Podfile 지웠다가 다시해보고 별 짓을 다해도 빌드가 안됨...&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;폭풍검색을 하니까 xcode로 빌드해서 성공한 사람이 있대서 xcode로 시도&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;그치만 xcode는 빌드 실패 원인을 더 자세히 알려주더라..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보니까 ttf 파일 중복으로 인한 에러였음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555;&quot;&gt;error: Multiple commands produce ~~ (새로운폰트).ttf&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;background-color: #ffffff; color: #555555;&quot;&gt;그래서 다시 폭풍검색으로 Copy Bundle Resources 열어서 ttf 파일 지워줌.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555;&quot;&gt;해당 링크 참조.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555;&quot;&gt;&lt;a title=&quot;해당 링크&quot; href=&quot;https://eugenehwang1124.tistory.com/55&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://eugenehwang1124.tistory.com/55&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650501049839&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;[React Native ] react-native-vector-icons ios에서 사용하기(에러 해결)&quot; data-og-description=&quot;https://www.npmjs.com/package/react-native-vector-icons react-native-vector-icons Customizable Icons for React Native with support for NavBar/TabBar, image source and full styling. www.npmjs.com 여..&quot; data-og-host=&quot;eugenehwang1124.tistory.com&quot; data-og-source-url=&quot;https://eugenehwang1124.tistory.com/55&quot; data-og-url=&quot;https://eugenehwang1124.tistory.com/55&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cXhcVp/hyN65L0U0k/BXfnosMbUY32xiKkxXkmQK/img.png?width=800&amp;amp;height=517&amp;amp;face=0_0_800_517,https://scrap.kakaocdn.net/dn/ciTm2H/hyN6Y7blrt/uECm7WUNIlRfa6G8OdTut1/img.png?width=800&amp;amp;height=517&amp;amp;face=0_0_800_517,https://scrap.kakaocdn.net/dn/cAphdu/hyN6ZE1eCe/Y2L7olKrHyRSibPy2yy4b1/img.png?width=2796&amp;amp;height=1810&amp;amp;face=0_0_2796_1810&quot;&gt;&lt;a href=&quot;https://eugenehwang1124.tistory.com/55&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://eugenehwang1124.tistory.com/55&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cXhcVp/hyN65L0U0k/BXfnosMbUY32xiKkxXkmQK/img.png?width=800&amp;amp;height=517&amp;amp;face=0_0_800_517,https://scrap.kakaocdn.net/dn/ciTm2H/hyN6Y7blrt/uECm7WUNIlRfa6G8OdTut1/img.png?width=800&amp;amp;height=517&amp;amp;face=0_0_800_517,https://scrap.kakaocdn.net/dn/cAphdu/hyN6ZE1eCe/Y2L7olKrHyRSibPy2yy4b1/img.png?width=2796&amp;amp;height=1810&amp;amp;face=0_0_2796_1810');&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 Native ] react-native-vector-icons ios에서 사용하기(에러 해결)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://www.npmjs.com/package/react-native-vector-icons react-native-vector-icons Customizable Icons for React Native with support for NavBar/TabBar, image source and full styling. www.npmjs.com 여..&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;eugenehwang1124.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;p data-ke-size=&quot;size16&quot;&gt;지워주고 나니 빌드 잘됨~~!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 때까지만 해도 해결된줄 알았음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 빌드되고 나서 앱이 켜지니까 다시 unrecognized font family 에러 나옴 ㅡㅡ&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;npx react-native link 랑 npx react-native link --verbose 등등 별짓 다해봐도 안됨.&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;그러다가 xcode에서 다시 &lt;span style=&quot;background-color: #ffffff; color: #555555;&quot;&gt;Copy Bundle Resources에 ttf파일들 넣어줬더니 잘 작동함.. 하...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555;&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;background-color: #ffffff; color: #555555;&quot;&gt;아무튼 이렇게 해줬더니 드디어 빌드도 잘되고 앱도 잘 돌아간다..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-04-21 오전 12.27.28.png&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wFXkz/btrzYjZZjOr/dsM7q2PECmbSLakSpRjQt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wFXkz/btrzYjZZjOr/dsM7q2PECmbSLakSpRjQt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wFXkz/btrzYjZZjOr/dsM7q2PECmbSLakSpRjQt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwFXkz%2FbtrzYjZZjOr%2FdsM7q2PECmbSLakSpRjQt0%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;556&quot; height=&quot;393&quot; data-filename=&quot;스크린샷 2022-04-21 오전 12.27.28.png&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;574&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;진짜 xcode 사용법도 모르고 앱 파일 구성 자체를 잘 모르니까 맨땅에 헤딩하는 기분...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부 좀 해야겠다...&lt;/p&gt;</description>
      <category>TIL</category>
      <category>build</category>
      <category>error</category>
      <category>font</category>
      <category>ReactNative</category>
      <category>rn</category>
      <category>ttf</category>
      <category>unrecognized</category>
      <category>Xcode</category>
      <category>리액트네이티브</category>
      <category>빌드실패</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/57</guid>
      <comments>https://soobakba.tistory.com/57#entry57comment</comments>
      <pubDate>Thu, 21 Apr 2022 09:41:11 +0900</pubDate>
    </item>
    <item>
      <title>써드파티 API 호출을 위한 암호화 삽질기</title>
      <link>https://soobakba.tistory.com/56</link>
      <description>&lt;h2&gt;배경&lt;/h2&gt;
&lt;p&gt;우리 서비스에 사용되는 sms 발송 업체를 기존 &lt;a href=&quot;https://www.bizmsg.kr/&quot;&gt;비즈엠&lt;/a&gt;(웹에서 발송하는 카카오톡 및 SMS 기능 제공 서비스 업체)에서 &lt;a href=&quot;https://www.ncloud.com/&quot;&gt;ncloud&lt;/a&gt;(NAVER에서 운영하는 각종 클라우드 서비스)로 변경하기로 했다. 이를 위해 API 사용법을 알아보고 &lt;a href=&quot;https://sens.apigw.ntruss.com/apigw/swagger-ui?productId=plv61henn8&amp;amp;apiId=j5tgfxp2ba&amp;amp;stageId=a0y11xe7vi&amp;amp;region=KR#/v2/post_services__serviceId__messages&quot;&gt;스웨거(Swagger - API 스펙 확인 및 테스트 가능한 환경)&lt;/a&gt;에서 테스트까지 수월하게 마쳤다.&lt;/p&gt;
&lt;p&gt;그런데 실제 코드 단에서 테스트를 해보니 계속 401 Unauthorized 에러가 났다. 이유는 헤더에 전송하는 암호화한 데이터가 잘못되었던 것인데, 이를 해결하기 위해 암호화 알고리즘에 대해 많이 찾아보게 되었고 그 과정에서 알게 된 것들과 에러 해결 과정을 정리해 보려고 한다.&lt;/p&gt;
&lt;h2&gt;암호화 알고리즘?&lt;/h2&gt;
&lt;p&gt;암호화는 사실 깊게 파면 끝도 없을 주제일 것이다. 큰 분류만 보자면 아래와 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;대칭형 암호 (비밀키 암호)&lt;/li&gt;
&lt;li&gt;비대칭형 암호 (공개키 암호)&lt;/li&gt;
&lt;li&gt;단방향 암호 - 복호화가 불가능한 해시값
&lt;ol&gt;
&lt;li&gt;SHA-1 (취약점 발견되어 이제 사용 X)&lt;/li&gt;
&lt;li&gt;SHA-2&lt;/li&gt;
&lt;li&gt;SHA-3&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이러한 대분류에 따라 아래 속하는 종류들도 다양한데, 이 중에서 내가 필요로 하는 것은 단방향 암호인 HmacSHA256 알고리즘이다.&lt;/p&gt;
&lt;h2&gt;HMAC + SHA256&lt;/h2&gt;
&lt;p&gt;HMAC은 Hash-based Message Authentication의 약자로 해싱기법을 이용해서 메시지의 위변조가 있었는지 체크하는 기법이다.&lt;/p&gt;
&lt;p&gt;해싱은 원문 메시지를 일정한 길이의 다른 메시지로 변환하는데, 이 원문을 해싱하여 나온 메시지를 다이제스트라고 하며, 여기서 SHA-2 해시 알고리즘을 사용한 256bit 길이의 다이제스트를 반환하는 것을 가리켜 HmacSHA256 이라고 한다.&lt;/p&gt;
&lt;p&gt;동작 방식은 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;메세지를 보내는 Sender와 메세지를 받는 Receiver는 둘 다 특정 Key와 무슨 알고리즘을 사용할지 알고 있다.&lt;/li&gt;
&lt;li&gt;보내는 쪽에서 보내고자 하는 내용을 특정 Key를 이용해 해시 알고리즘에 적용하여 &lt;b&gt;시그니처 Signature&lt;/b&gt;라는 것을 생성한다.&lt;/li&gt;
&lt;li&gt;받는 쪽에서도 Key와 해시 알고리즘을 사용해서 똑같이 Signature를 만들어서 비교한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;어떻게 적용할까?&lt;/h2&gt;
&lt;p&gt;암호화 알고리즘의 동작원리를 알았으니 이제 실제 코드에 적용해보자.&lt;/p&gt;
&lt;p&gt;ncloud 가이드에서는 아래와 같은 헤더들을 요청시 전송해야 한다고 나와있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;_2021-03-18__2.06.43.png&quot; data-origin-width=&quot;1646&quot; data-origin-height=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YvU5S/btq3LVhwkH4/daDfaSwSfW8K3RK6Jr4cKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YvU5S/btq3LVhwkH4/daDfaSwSfW8K3RK6Jr4cKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YvU5S/btq3LVhwkH4/daDfaSwSfW8K3RK6Jr4cKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYvU5S%2Fbtq3LVhwkH4%2FdaDfaSwSfW8K3RK6Jr4cKk%2Fimg.png&quot; data-filename=&quot;_2021-03-18__2.06.43.png&quot; data-origin-width=&quot;1646&quot; data-origin-height=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;여기서 &lt;b&gt;x-ncp-apigw-signature-v2&lt;/b&gt; 헤더에 앞서 언급한 Signature를 생성해서 보내줘야 했다.&lt;/p&gt;
&lt;p&gt;또한 Signature는 아래와 같은 조건으로 생성되어야 하는데 처음에는 이 생성 조건에 대한 이해가 부족해서 띄어쓰기와 같은 오타로 인해 첫 번째 삽질을 했었다.  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;_2021-03-18__3.24.09.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;264&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4GtK9/btq3JAkVJBB/JrBUXOyyXe4VLnbFyVdsT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4GtK9/btq3JAkVJBB/JrBUXOyyXe4VLnbFyVdsT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4GtK9/btq3JAkVJBB/JrBUXOyyXe4VLnbFyVdsT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4GtK9%2Fbtq3JAkVJBB%2FJrBUXOyyXe4VLnbFyVdsT1%2Fimg.png&quot; data-filename=&quot;_2021-03-18__3.24.09.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;264&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그리고 해당 문서에는 JavaScript 요청 예시가 브라우저 환경 기준으로 나와있었는데 덕분에 crypto-js 모듈의 존재를 알게 되었지만 버전과 문법에 차이가 있어서 이 부분에서 crypto-js 소스를 뒤져보면서 두 번째 삽질을 했다. Base64로 인코딩 하는 부분도 삽질에 한 몫 했다.&lt;/p&gt;
&lt;p&gt;나는 Node 환경에서 진행하기 때문에 crypto-js npm 모듈을 이용해 아래와 같이 작성했다.&lt;/p&gt;
&lt;p&gt;import hmacSHA256 from 'crypto-js/hmac-sha256'; import Base64 from 'crypto-js/enc-base64'; const stringToSign = `POST ${SEND_MESSAGE_URL}\\n${timestamp}\\n${ACCESS_KEY}`; const hmacDigest = Base64.stringify(hmacSHA256(stringToSign, SECRET_KEY));&lt;/p&gt;
&lt;pre id=&quot;code_1619695117159&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import hmacSHA256 from 'crypto-js/hmac-sha256';
import Base64 from 'crypto-js/enc-base64';

const stringToSign = `POST ${SEND_MESSAGE_URL}\n${timestamp}\n${ACCESS_KEY}`;
const hmacDigest = Base64.stringify(hmacSHA256(stringToSign, SECRET_KEY));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 서치를 하다보니 Node에는 Crypto가 내장되어 있어서 별도의 모듈 추가 없이 사용할 수 있다고 한다.&lt;/p&gt;
&lt;p&gt;그래서 다시 아래와 같이 작성을 했다.&lt;/p&gt;
&lt;p&gt;import crypto from 'crypto'; const hmac = crypto.createHmac('sha256', SECRET_KEY); const hmacDigest = hmac .update(`POST ${SEND_MESSAGE_URL}\\n${timestamp}\\n${ACCESS_KEY}`) .digest('base64');&lt;/p&gt;
&lt;pre id=&quot;code_1619695149020&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import crypto from 'crypto';

const hmac = crypto.createHmac('sha256', SECRET_KEY);
const hmacDigest = hmac
	.update(`POST ${SEND_MESSAGE_URL}\n${timestamp}\n${ACCESS_KEY}`)
	.digest('base64');&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 두 코드로 만들어진 다이제스트를 비교해보니 둘 다 같은 값이 나온다. 사용법이 조금 다르지만 무엇을 사용해도 무방할 것 같다.&lt;/p&gt;
&lt;p&gt;그러나 이렇게 해도 계속 401에러가 나왔었는데... 정신없는 코드를 정리하고 문서를 다시 잘 살펴보니 Signature를 만들 때 사용되는 URL을 잘못 입력해두었던 것이다. (세 번째 삽질)&lt;/p&gt;
&lt;p&gt;// 이렇게 하면 안되고 const SEND_MESSAGE_URL = `/services/${SERVICE_ID}/messages`; // 이렇게 해야한다. const SEND_MESSAGE_URL = `/sms/v2/services/${SERVICE_ID}/messages`;&lt;/p&gt;
&lt;pre id=&quot;code_1619695181085&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이렇게 하면 안되고
const SEND_MESSAGE_URL = `/services/${SERVICE_ID}/messages`;
// 이렇게 해야한다.
const SEND_MESSAGE_URL = `/sms/v2/services/${SERVICE_ID}/messages`;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하고 호출하니 정상적으로 200응답을 받아 문자도 제대로 발송이 되었다.&lt;/p&gt;
&lt;h2&gt;느낀 점&lt;/h2&gt;
&lt;p&gt;알고나서 보니 쉬운 것 같지만, 암호화 관련 작업이 처음이었던 나에게는 도대체 무엇이 원인일까 하고 모르는 단어가 나올 때 마다 검색해보고 뒤져보고 하느라 사소한 실수를 발견하는데에도 많은 시간이 걸렸다.&lt;/p&gt;
&lt;p&gt;그래도 이런 삽질 덕분에 많은 자료를 찾아보면서 새롭게 알게된 것들이 많았고, 이번 실수와 같은 일을 반복하지 말자는 점도 배울 수 있었다. 앞으로는 당연하게 생각했던 코드들도 놓치지 말고 항상 의심해봐야겠다!&lt;/p&gt;
&lt;h3&gt;참고자료&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://juneyr.dev/2019-06-10/spring-hmac&quot;&gt;https://juneyr.dev/2019-06-10/spring-hmac&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://jusungpark.tistory.com/34&quot;&gt;https://jusungpark.tistory.com/34&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nodejs.org/docs/latest-v14.x/api/crypto.html#crypto_hmac_digest_encoding&quot;&gt;https://nodejs.org/docs/latest-v14.x/api/crypto.html#crypto_hmac_digest_encoding&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://dalkomit.tistory.com/18&quot;&gt;https://dalkomit.tistory.com/18&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Base64-인코딩이란&quot;&gt;https://effectivesquid.tistory.com/entry/Base64-인코딩이란&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발상식 일반</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/56</guid>
      <comments>https://soobakba.tistory.com/56#entry56comment</comments>
      <pubDate>Thu, 29 Apr 2021 20:19:54 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] AWS 서버 접속 및 에러 해결 정리</title>
      <link>https://soobakba.tistory.com/53</link>
      <description>&lt;p&gt;토이프로젝트를 진행하면서 AWS로 EC2 인스턴스를 만들어보고 서버를 올려보고 있다.&lt;/p&gt;
&lt;p&gt;워낙 많은 자료들이 있어서 찾아보면서 따라하기는 어렵지 않았지만 내가 모르는게 너무 많아서 일일히 찾아봐야 하는게 번거롭긴 했다...&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원격 서버 접속&lt;/h3&gt;
&lt;p&gt;먼저 인스턴스를 만들면서 받은 key로 인스턴스에 접속하려면 터미널을 열어 key가 있는 폴더로 가서 아래와 같이 입력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1611558024232&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;chmod 400 &amp;lt;your pem key name&amp;gt;.pem
ssh -i &quot;&amp;lt;your pem key name&amp;gt;.pem&quot; ubunutu@&amp;lt;your IPv4 Public IP&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;&amp;gt;안에는 자신의 파일명과 IP 주소에 맞게 알아서 입력하면 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기서 &lt;span&gt;&lt;b&gt;chmod&lt;/b&gt;란 리눅스 명령어로 &lt;b&gt;change mod&lt;/b&gt; 라는 뜻으로 파일의 접근 권한을 변경하는 명령어다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1611559943982&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;chmod [레퍼런스] [연산자] [변경할 접근권한] [파일]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;위와 같이 옵션을 지정할 수 있는데 &lt;span&gt;[레퍼런스] [연산자] [변경할 접근권한] 대신 8진수 숫자로 표현할 수도 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;여기서 400은 파일 소유자의 읽기 권한을 나타내고 각 &lt;span&gt;접근 권한에 따라 값을 더하면 된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-01-25 오후 3.50.20.png&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;900&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgX6bM/btqUJYNW1FA/jAMMSsNEhiBgT0JTxzWmsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgX6bM/btqUJYNW1FA/jAMMSsNEhiBgT0JTxzWmsK/img.png&quot; data-alt=&quot;접속 성공!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgX6bM/btqUJYNW1FA/jAMMSsNEhiBgT0JTxzWmsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgX6bM%2FbtqUJYNW1FA%2FjAMMSsNEhiBgT0JTxzWmsK%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-01-25 오후 3.50.20.png&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;900&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;접속 성공!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경 변수 설정&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;접속에 성공하면 아래 명령어로 node와 npm을 깔고 내가 만든 코드를 원하는 디렉토리에서 git clone을 해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1611573142369&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install nodejs
sudo apt install npm&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;그러나 npm install을 하고 실행시키니 다음과 같은 에러가 났다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-01-25 오후 4.02.37.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;1472&quot; width=&quot;648&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQFyKC/btqUBwkmQEq/yOLPE50IcG6B1QAEzZ6lsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQFyKC/btqUBwkmQEq/yOLPE50IcG6B1QAEzZ6lsk/img.png&quot; data-alt=&quot;./dev 파일을 찾을수 없다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQFyKC/btqUBwkmQEq/yOLPE50IcG6B1QAEzZ6lsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQFyKC%2FbtqUBwkmQEq%2FyOLPE50IcG6B1QAEzZ6lsk%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-01-25 오후 4.02.37.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;1472&quot; width=&quot;648&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;./dev 파일을 찾을수 없다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span&gt;몽고 DB를 연결할 때 &lt;/span&gt;&lt;b&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;NODE_ENV&lt;/b&gt; 값에 따라서 production 모드일 땐&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;MONGO_URI&lt;/b&gt; 값을 사용하고 아니면 &lt;b&gt;dev.js&lt;/b&gt; 파일을 사용하도록 했는데 보안을 위해 dev.js 는 git에 올리지 않았기 때문에 이러한 에러가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;따라서&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;NODE_ENV를 production으로 설정해주고 그에 따라 MONGO_URI 값도 설정해줘야 했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;리눅스에서 환경 변수 설정은 아래와 같이 하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1611561899850&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export NODE_ENV=production
export MONGO_URI='...'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 의아한 점이 NODE_ENV는 따옴표 없이 그냥 써도 잘 인식되는데 MONGO_URI는 따옴표 없이 쓰니까 다음과 같이 string이 아니고 &quot;undefined&quot; 를 받았다고 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;mongo_error.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;31&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mXsQZ/btqUGUrCZVV/DvxEpYH81Q3X53109hoC6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mXsQZ/btqUGUrCZVV/DvxEpYH81Q3X53109hoC6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mXsQZ/btqUGUrCZVV/DvxEpYH81Q3X53109hoC6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmXsQZ%2FbtqUGUrCZVV%2FDvxEpYH81Q3X53109hoC6k%2Fimg.png&quot; data-filename=&quot;mongo_error.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;31&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;안그래도 값을 할당할때 부터 헷갈리긴 했는데 따옴표를 쓰면 잘 실행되므로 일단 넘어가고 다음에 찾아보기로...ㅠ_ㅠ&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;그리고 이게 서버를 껐다 켤때마다 리셋되던데 자동화 할 수 있는 방법도 찾아봐야겠다...&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;환경변수를 영구 등록하려면 vi 에디터로&amp;nbsp;&lt;span style=&quot;color: #000000;&quot;&gt;~/.bachrc 파일 마지막 부분에 환경변수 등록 명령어를 추가하면 터미널이 켜질때마다 환경변수 등록이 자동적으로 실행된다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1612180156286&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vi ~/.bachrc&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B88va/btqVkLHOS1s/0WYcZGpsIEOpBJVjpc1aPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B88va/btqVkLHOS1s/0WYcZGpsIEOpBJVjpc1aPK/img.png&quot; data-alt=&quot;bashrc 파일 맨 밑에 환경변수 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B88va/btqVkLHOS1s/0WYcZGpsIEOpBJVjpc1aPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB88va%2FbtqVkLHOS1s%2F0WYcZGpsIEOpBJVjpc1aPK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;bashrc 파일 맨 밑에 환경변수 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미 사용중인 포트 에러 처리&lt;/h3&gt;
&lt;p&gt;환경 변수를 설정하고 켰더니 이번엔 아래 에러가 났다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;alreadyuseport.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;730&quot; width=&quot;682&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn9KDw/btqUGVcYbNa/2UtTCxKwK3v7AH3Z40D5Jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn9KDw/btqUGVcYbNa/2UtTCxKwK3v7AH3Z40D5Jk/img.png&quot; data-alt=&quot;이전에 프로세스가 비정상적으로 종료되어 나타난 에러ㅠㅠ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn9KDw/btqUGVcYbNa/2UtTCxKwK3v7AH3Z40D5Jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn9KDw%2FbtqUGVcYbNa%2F2UtTCxKwK3v7AH3Z40D5Jk%2Fimg.png&quot; data-filename=&quot;alreadyuseport.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;730&quot; width=&quot;682&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이전에 프로세스가 비정상적으로 종료되어 나타난 에러ㅠㅠ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;내가 이전에 서버 연결해서 NodeJS 프로세스를 실행했었는데 오랜 시간이 지나 자동으로 원격 서버와 연결이 끊어진적이 있어서 그것 때문에 발생한 에러인것 같다.&lt;/p&gt;
&lt;p&gt;해결 방법은 간단하다. 맥이나 리눅스에서는 아래 명령어로 해결 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1611572885291&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 5000번 포트를 사용하는 프로세스 리스트 출력
lsof -i TCP:5000
// 특정 프로세스 강제 종료
kill -9 &amp;lt;PID&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-01-25 오후 4.03.49.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;118&quot; width=&quot;680&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PHOHj/btqUM8CoXnx/AF5BlDXHX7kKPAU7gR1Iy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PHOHj/btqUM8CoXnx/AF5BlDXHX7kKPAU7gR1Iy1/img.png&quot; data-alt=&quot;리스트 확인 후 해당 PID 종료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PHOHj/btqUM8CoXnx/AF5BlDXHX7kKPAU7gR1Iy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPHOHj%2FbtqUM8CoXnx%2FAF5BlDXHX7kKPAU7gR1Iy1%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-01-25 오후 4.03.49.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;118&quot; width=&quot;680&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;리스트 확인 후 해당 PID 종료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과 확인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-01-25 오후 4.19.26.png&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;294&quot; width=&quot;464&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zXYHD/btqUzuN003D/XdRN4Cxr4NQ7pYFyVV4qOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zXYHD/btqUzuN003D/XdRN4Cxr4NQ7pYFyVV4qOk/img.png&quot; data-alt=&quot;드디어 만난 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zXYHD/btqUzuN003D/XdRN4Cxr4NQ7pYFyVV4qOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzXYHD%2FbtqUzuN003D%2FXdRN4Cxr4NQ7pYFyVV4qOk%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-01-25 오후 4.19.26.png&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;294&quot; width=&quot;464&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;드디어 만난 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;드디어 정상적으로 실행된거 같아서 브라우저에 서버 IP 주소와 포트번호를 입력하니 드디어 Hello World를 만날 수 있었다!&lt;/p&gt;
&lt;p&gt;&lt;s&gt;그러나 내가 원한건 클라이언트 폴더에 작업해둔 3000번 포트를 사용하는 리액트 프로젝트 화면이었으나 3000번 포트를 입력하면 연결되지 않았다...&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;이건 이제부터 다시 삽질을 해봐야 겠다.&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;나는 클라이언트를 spa로 작업했는데 빌드한 파일을 보여주려면 S3같은 정적리소스 서버를 별도로 파거나, 현재의 EC2에 별도의 웹서버를 띄워줘야 하는거 같다. 그래서 일단은 개발시 로컬로 작업할 때 처럼 실행했는데 해당 리액트 프로젝트가 3000번 포트로 띄워졌기 때문에 웹브라우저로 접속하려면 AWS 해당 인스턴스의 보안그룹에서 3000번 포트를 열어줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-01-31 오후 4.09.44.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;124&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dS193Q/btqVbv6FUki/rOrDrKa9UWkBop6OdS6QA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dS193Q/btqVbv6FUki/rOrDrKa9UWkBop6OdS6QA1/img.png&quot; data-alt=&quot;3000번 포트 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dS193Q/btqVbv6FUki/rOrDrKa9UWkBop6OdS6QA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdS193Q%2FbtqVbv6FUki%2FrOrDrKa9UWkBop6OdS6QA1%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-01-31 오후 4.09.44.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;124&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3000번 포트 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span&gt;보안 그룹에서 인바운드 규칙 편집을 눌러 수정하고 접속하면 잘 접속이 된다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;+추가&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;PM2를 이용해서 서버 무중단 설정&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;서버 연결이 끊어지면 node 웹서버도 같이 끊어진다. 그럴땐 pm2를 이용해서 관리하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1612179868233&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pm2 start npm --name &quot;{app_name}&quot; -- run {script_name}

// 나의 경우는 아래 명령어로 처리
pm2 start npm --name &quot;rsp-ranking&quot; -- run dev&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;출처&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;a href=&quot;https://velog.io/@rheey90/AWS-EC2-Node.js-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;velog.io/@rheey90/AWS-EC2-Node.js-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1611560249051&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;AWS EC2 Node.js 서버 배포&quot; data-og-description=&quot;AWS EC2는 아마존 웹 서비스에서 제공하는 서비스로서 아마존 웹 서비스 클라우드에서 확장 가능한 컴퓨팅 용량을 제공합니다. 과거와는 달리 투자 비용이 상당히 드는 서버를 담당하는 하드웨어&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@rheey90/AWS-EC2-Node.js-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC&quot; data-og-url=&quot;https://velog.io/@rheey90/AWS-EC2-Node.js-서버-배포&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/NiKC3/hyI2vJthRA/PUAYHdajPwksgNhNXIxv71/img.png?width=1440&amp;amp;height=877&amp;amp;face=0_0_1440_877,https://scrap.kakaocdn.net/dn/coAMbQ/hyI4sdgxJS/cIA2T9Kmv60gvKzWxyc30k/img.png?width=1440&amp;amp;height=877&amp;amp;face=0_0_1440_877,https://scrap.kakaocdn.net/dn/bVGcV2/hyI2LyNB6Z/VQttldA3BsyvZVrLwq0wf1/img.png?width=1440&amp;amp;height=877&amp;amp;face=0_0_1440_877&quot;&gt;&lt;a href=&quot;https://velog.io/@rheey90/AWS-EC2-Node.js-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@rheey90/AWS-EC2-Node.js-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/NiKC3/hyI2vJthRA/PUAYHdajPwksgNhNXIxv71/img.png?width=1440&amp;amp;height=877&amp;amp;face=0_0_1440_877,https://scrap.kakaocdn.net/dn/coAMbQ/hyI4sdgxJS/cIA2T9Kmv60gvKzWxyc30k/img.png?width=1440&amp;amp;height=877&amp;amp;face=0_0_1440_877,https://scrap.kakaocdn.net/dn/bVGcV2/hyI2LyNB6Z/VQttldA3BsyvZVrLwq0wf1/img.png?width=1440&amp;amp;height=877&amp;amp;face=0_0_1440_877');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;AWS EC2 Node.js 서버 배포&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;AWS EC2는 아마존 웹 서비스에서 제공하는 서비스로서 아마존 웹 서비스 클라우드에서 확장 가능한 컴퓨팅 용량을 제공합니다. 과거와는 달리 투자 비용이 상당히 드는 서버를 담당하는 하드웨어&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://bimmermac.com/2789&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;bimmermac.com/2789&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1611560245773&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;리눅스 chmod 명령 사용 방법 | BIMMER MAC&quot; data-og-description=&quot;chmod 명령어 소개 chmod(change mod)는 파일의 접근 권한을 변경하는 명령어입니다. chown과 달리 파일의 소유자도 명령을 내릴 수 있습니다. 당연히 소유권이 없는 사용자는 chmod 명령을 사용할&amp;hellip;&quot; data-og-host=&quot;bimmermac.com&quot; data-og-source-url=&quot;https://bimmermac.com/2789&quot; data-og-url=&quot;https://bimmermac.com/2789&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/brO2wr/hyI4o9LJj9/MIE9k2VkErPHK7XMHt4DJ1/img.png?width=250&amp;amp;height=116&amp;amp;face=0_0_250_116,https://scrap.kakaocdn.net/dn/2YZKj/hyI2IB0mqg/9xmSlfKcqKwD6MGNi96SOk/img.png?width=250&amp;amp;height=116&amp;amp;face=0_0_250_116&quot;&gt;&lt;a href=&quot;https://bimmermac.com/2789&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bimmermac.com/2789&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/brO2wr/hyI4o9LJj9/MIE9k2VkErPHK7XMHt4DJ1/img.png?width=250&amp;amp;height=116&amp;amp;face=0_0_250_116,https://scrap.kakaocdn.net/dn/2YZKj/hyI2IB0mqg/9xmSlfKcqKwD6MGNi96SOk/img.png?width=250&amp;amp;height=116&amp;amp;face=0_0_250_116');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;리눅스 chmod 명령 사용 방법 | BIMMER MAC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;chmod 명령어 소개 chmod(change mod)는 파일의 접근 권한을 변경하는 명령어입니다. chown과 달리 파일의 소유자도 명령을 내릴 수 있습니다. 당연히 소유권이 없는 사용자는 chmod 명령을 사용할&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;bimmermac.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://devhyun.com/blog/post/23&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;devhyun.com/blog/post/23&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1611561370735&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;NodeJS 환경 변수 설정 - 데브현&quot; data-og-description=&quot;애플리케이션을 다양한 환경에서 테스트를 하거나 배포를 합니다. DB 커넥션, 외부 API 연동과 같은 정보들은 환경별로 다르기 때문에 매번 변경해줘야 하는 번거로움이 있습니다. NodeJS에서는 시&quot; data-og-host=&quot;devhyun.com&quot; data-og-source-url=&quot;https://devhyun.com/blog/post/23&quot; data-og-url=&quot;https://devhyun.com/blog/post/23&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/56H5m/hyI4uIVyP3/ZTpitLmR3LxSysMX4NLOj0/img.jpg?width=627&amp;amp;height=414&amp;amp;face=0_0_627_414,https://scrap.kakaocdn.net/dn/dliHp7/hyI4wtdcKd/E50a7TzK3rkx0TrhjPJrok/img.jpg?width=627&amp;amp;height=414&amp;amp;face=0_0_627_414,https://scrap.kakaocdn.net/dn/JcXN6/hyI2JHKl72/Otm0vk7V1ngjlxIOoof0c1/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600&quot;&gt;&lt;a href=&quot;https://devhyun.com/blog/post/23&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devhyun.com/blog/post/23&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/56H5m/hyI4uIVyP3/ZTpitLmR3LxSysMX4NLOj0/img.jpg?width=627&amp;amp;height=414&amp;amp;face=0_0_627_414,https://scrap.kakaocdn.net/dn/dliHp7/hyI4wtdcKd/E50a7TzK3rkx0TrhjPJrok/img.jpg?width=627&amp;amp;height=414&amp;amp;face=0_0_627_414,https://scrap.kakaocdn.net/dn/JcXN6/hyI2JHKl72/Otm0vk7V1ngjlxIOoof0c1/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;NodeJS 환경 변수 설정 - 데브현&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;애플리케이션을 다양한 환경에서 테스트를 하거나 배포를 합니다. DB 커넥션, 외부 API 연동과 같은 정보들은 환경별로 다르기 때문에 매번 변경해줘야 하는 번거로움이 있습니다. NodeJS에서는 시&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;devhyun.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://jootc.com/p/201912253249&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jootc.com/p/201912253249&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1611572513287&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;NodeJS address already in use 문제 해결 - JooTC&quot; data-og-description=&quot;NodeJS address already in use 문제 해결 방법 Error: listen EADDRINUSE: address already in use :::5000 현재 다른 프로세스에서 사용 중이라 해당 서비스를 시작할 수 없다는 에러입니다.&quot; data-og-host=&quot;jootc.com&quot; data-og-source-url=&quot;https://jootc.com/p/201912253249&quot; data-og-url=&quot;https://jootc.com/p/201912253249&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cBFHYX/hyI4v80HyF/mOt8DsZiPZlM4mOb2NC4Hk/img.png?width=700&amp;amp;height=350&amp;amp;face=0_0_700_350&quot;&gt;&lt;a href=&quot;https://jootc.com/p/201912253249&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jootc.com/p/201912253249&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cBFHYX/hyI4v80HyF/mOt8DsZiPZlM4mOb2NC4Hk/img.png?width=700&amp;amp;height=350&amp;amp;face=0_0_700_350');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;NodeJS address already in use 문제 해결 - JooTC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;NodeJS address already in use 문제 해결 방법 Error: listen EADDRINUSE: address already in use :::5000 현재 다른 프로세스에서 사용 중이라 해당 서비스를 시작할 수 없다는 에러입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;jootc.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://sosobaba.tistory.com/279&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;sosobaba.tistory.com/279&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1612180064495&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;리눅스 환경변수 등록하기(환경변수 영구 등록)&quot; data-og-description=&quot;안녕하세요 마블랑입니다. 이번 포스팅은 터미널 창에서 리눅스 Ubuntu(우분투)의 환경변수를 등록하는 방법에 대해서 소개하겠습니다. 먼저 현재 리눅스에 등록된 전체 환경변수를 보려면 터미&quot; data-og-host=&quot;sosobaba.tistory.com&quot; data-og-source-url=&quot;https://sosobaba.tistory.com/279&quot; data-og-url=&quot;https://sosobaba.tistory.com/279&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cQOaqM/hyI7bYMtKO/OodC71jgocbbcpHJ8XRzM0/img.jpg?width=800&amp;amp;height=802&amp;amp;face=0_0_800_802,https://scrap.kakaocdn.net/dn/cF19Wg/hyI8ZWIrbQ/MdrfVqqKiAD3rde7qW0Js0/img.jpg?width=800&amp;amp;height=802&amp;amp;face=0_0_800_802,https://scrap.kakaocdn.net/dn/4K3WU/hyI8XxOgNH/hkFK9euovUtzXw6uIoSxBK/img.jpg?width=883&amp;amp;height=886&amp;amp;face=0_0_883_886&quot;&gt;&lt;a href=&quot;https://sosobaba.tistory.com/279&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sosobaba.tistory.com/279&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cQOaqM/hyI7bYMtKO/OodC71jgocbbcpHJ8XRzM0/img.jpg?width=800&amp;amp;height=802&amp;amp;face=0_0_800_802,https://scrap.kakaocdn.net/dn/cF19Wg/hyI8ZWIrbQ/MdrfVqqKiAD3rde7qW0Js0/img.jpg?width=800&amp;amp;height=802&amp;amp;face=0_0_800_802,https://scrap.kakaocdn.net/dn/4K3WU/hyI8XxOgNH/hkFK9euovUtzXw6uIoSxBK/img.jpg?width=883&amp;amp;height=886&amp;amp;face=0_0_883_886');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;리눅스 환경변수 등록하기(환경변수 영구 등록)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;안녕하세요 마블랑입니다. 이번 포스팅은 터미널 창에서 리눅스 Ubuntu(우분투)의 환경변수를 등록하는 방법에 대해서 소개하겠습니다. 먼저 현재 리눅스에 등록된 전체 환경변수를 보려면 터미&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;sosobaba.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>aws</category>
      <category>ec2</category>
      <category>env</category>
      <category>nodejs</category>
      <category>리눅스</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/53</guid>
      <comments>https://soobakba.tistory.com/53#entry53comment</comments>
      <pubDate>Mon, 25 Jan 2021 17:10:09 +0900</pubDate>
    </item>
    <item>
      <title>[JS] 마이크로태스크(Microtasks)와 async/await</title>
      <link>https://soobakba.tistory.com/48</link>
      <description>&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;js.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot; width=&quot;310&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sYdPL/btqM64othTD/2g6BN211mlBir4ZlgCKjGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sYdPL/btqM64othTD/2g6BN211mlBir4ZlgCKjGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sYdPL/btqM64othTD/2g6BN211mlBir4ZlgCKjGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsYdPL%2FbtqM64othTD%2F2g6BN211mlBir4ZlgCKjGk%2Fimg.png&quot; data-filename=&quot;js.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot; width=&quot;310&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Promise 객체가 가진 핸들러들 .then(), .catch(), .finally() 는 모두 비동기적으로 실행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1605016703072&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log('Start!');
Promise.resolve('Promise!').then(res =&amp;gt; console.log(res));
console.log('End!');

// 아래와 같이 출력
// Start!
// End!
// Promise!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드를 보면 Promise를 바로 resolve 해주어도 then이 실행되는 것은 동기 코드보다 나중이다. 왜 그럴까?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마이크로태스크&lt;b&gt;(Microtasks)&lt;/b&gt;와 매크로태스크(Macrotasks)&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;이 때 등장하는 것이 바로 마이크로태스크다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ECMA에선 위와 같은 비동기 처리를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;PromiseJobs&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;라는 내부 큐를 명시하는데 V8 엔진에서 '&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;마이크로태스크 큐&lt;/b&gt;'&lt;span style=&quot;color: #333333;&quot;&gt;(ES8 용어)&lt;/span&gt; 라고 부르기 때문에 이 용어가 좀 더 선호된다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마이크로태스크에 앞서 매크로태스크가 존재하는데 둘 다 비동기 작업을 관리하기 위한 개념이지만 약간의 차이가 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 38px;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 19.3024%; height: 19px;&quot;&gt;Microtasks&lt;/td&gt;
&lt;td style=&quot;width: 80.6976%; height: 19px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Promise callback,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;process.nextTick(Node.js에서 사용하는 비동기 실행 함수), queueMicrotask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 19.3024%; height: 19px;&quot;&gt;(Macro)tasks&lt;/td&gt;
&lt;td style=&quot;width: 80.6976%; height: 19px;&quot;&gt;setTimeout, setInterval, setImmediate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;위 표에서 보듯이 함수마다 다른 태스크가 적용되는데, &lt;span style=&quot;color: #333333;&quot;&gt;Promise는 Microtasks에 속해있는것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Promise가 수행되고&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;.then(), .catch(), .finally() 등이 호출되면 각 함수에 넘겨진 콜백이 &lt;b&gt;Microtask queue&lt;/b&gt;에 추가된다. 이 말인 즉슨, &lt;span style=&quot;color: #333333;&quot;&gt;.then(), .catch(), .finally() 이 실행될 때 콜백이 바로 실행되지 않고 비동기로 진행된다는 뜻이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기서 선행되어야 하는 지식이 있는데 바로 자바스크립트 &lt;a href=&quot;https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이벤트 루프&lt;/a&gt;다. 이벤트 루프를 이해하면 동기 코드와 비동기 코드가 어떤 과정을 거쳐 실행되는지 알 수 있다. &lt;b&gt;마이크로태스크&lt;/b&gt;도 이와 거의 동일하나 &lt;b&gt;(매크로)태스크&lt;/b&gt;보다 우선순위가 높다.&lt;/span&gt;&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;호출 스택에 모든 실행 함수들이 추가되고 값을 반환하면 호출 스택을 빠져나온다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;호출 스택이 비어 있으면 &lt;b&gt;마이크로태스크 큐&lt;/b&gt;에 있던 태스크들이 하나씩 호출 스택에 추가되고 실행된다.&lt;/li&gt;
&lt;li&gt;호출 스택과 &lt;b&gt;마이크로태스크 큐&lt;/b&gt;가 모두 비어있게 되면 이벤트 루프는 &lt;b&gt;(매크로)태스크 큐&lt;/b&gt;를 확인하고 남아있는 작업을 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;해당 내용은 이 &lt;a href=&quot;http://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke?fbclid=IwAR3cfIk3iVpt1EoFOflRVs4VFe6GC2m2nbkP99bWgSduAkxVCIFSXVgKYzE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;를 참고하였고 위 과정을 시각적으로 표현해주어 아주 쉽게 이해가 가능하다.&lt;s&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;async / await&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;async/await는 프로미스 코드를 동기적 코드처럼 다룰수 있게 해주는 편리한 문법이다.&lt;/p&gt;
&lt;pre id=&quot;code_1605020422703&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function f1() {
  return 1
}
f1() // Promise 객체 반환 (state: 'fulfilled', result: 1)
f1().then(console.log) // 1 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;함수 선언시 function 앞에 'aynsc'를 넣어주면 Promise 객체를 반환하는 함수가 된다. 프로미스가 아닌 값을 반환하더라도 이행된 상태의 프로미스로 값을 감싸서 반환한다. 그래서 위 코드처럼 .then()을 호출해줄 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;await는 async가 선언된 함수 내부에서만 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1605022554581&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function f2() {
  const res = await new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; resolve('완료!'), 1000);
  });
  console.log(res);
}
f2() // 1초 뒤에 '완료!' 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자바스크립트 코드가 실행되다가 위와 같이 await 키워드를 만나면 해당 프로미스가 처리될 때 까지 기다렸다가 완료되면 아래 console.log를 실행한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실제로는 await 키워드를 만나면 async 함수의 실행이 일시정지되고 async 함수 전체가 &lt;b&gt;마이크로태스크 큐&lt;/b&gt;로 삽입된다. 그래서 호출 스택이 비어있는 상태가 되고 이 때 엔진은 다른 스크립트를 실행하거나 이벤트 처리 등을 할 수 있기 때문에 CPU 리소스가 낭비되지 않는다.&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;최상위 레벨 코드에서 await 사용 불가능.&lt;/li&gt;
&lt;li&gt;프로미스는 아니지만 프로미스와 호환 가능한 'thenable' 객체도 await와 함께 사용 가능.&lt;/li&gt;
&lt;li&gt;클래스 메소드에도 async 붙여서 사용 가능.&lt;/li&gt;
&lt;li&gt;try...catch 문법으로 async/await 에러 핸들링 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>async/await</category>
      <category>EventLoop</category>
      <category>JavaScript</category>
      <category>macrotask</category>
      <category>microtask</category>
      <category>promise</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/48</guid>
      <comments>https://soobakba.tistory.com/48#entry48comment</comments>
      <pubDate>Wed, 11 Nov 2020 00:00:58 +0900</pubDate>
    </item>
    <item>
      <title>[JS] 프라미스 API 중 Promise.all 직접 구현해보기</title>
      <link>https://soobakba.tistory.com/47</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;js.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot; width=&quot;330&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0vyPN/btqMtywTlOl/z7fdmmCuBjh37K90WCXOMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0vyPN/btqMtywTlOl/z7fdmmCuBjh37K90WCXOMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0vyPN/btqMtywTlOl/z7fdmmCuBjh37K90WCXOMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0vyPN%2FbtqMtywTlOl%2Fz7fdmmCuBjh37K90WCXOMK%2Fimg.png&quot; data-filename=&quot;js.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot; width=&quot;330&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Promise에는 5가지 정적 메서드가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Promise.all&lt;/li&gt;
&lt;li&gt;Promise.allSettled&lt;/li&gt;
&lt;li&gt;Promise.race&lt;/li&gt;
&lt;li&gt;Promise.resolve&lt;/li&gt;
&lt;li&gt;Promise.reject&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 중 가장 유용하고 많이 쓰이는 것은 &lt;b&gt;Promise.all&lt;/b&gt; 일 것이다. 그래서 공부겸 직접 구현해보았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Promise.all 직접 구현해보기&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1604420265878&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function all(iterable = []) {
  if (!iterable || !iterable.length) return Promise.resolve([]);

  return new Promise((resolve, reject) =&amp;gt; {
    const result = iterable.map(() =&amp;gt; ({ state: 'pending', value: undefined }));
    
    iterable.forEach((iter, idx) =&amp;gt; {
      if (iter instanceof Promise === false) {
        result[idx] = { state: 'fulfilled', value: iter };
        const isDone = !result.some(v =&amp;gt; v.state === 'pending');
        if (isDone) resolve(result.map(v =&amp;gt; v.value));
        return;
      }
      
      iter.then(value =&amp;gt; {
        result[idx] = { state: 'fulfilled', value };
        const isDone = !result.some(v =&amp;gt; v.state === 'pending');
        if (isDone) resolve(result.map(v =&amp;gt; v.value));
      }).catch(err =&amp;gt; {
        reject(err)
      });
    });
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;처음에는 감을 못잡고 좀 해맸었는데 mdn 문서를 읽으면서 넘겨주는값과 반환값들을 살펴보면서 한땀한땀 짜기 시작하니 생각보다 금방 짜졌다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그러나 mdn에 주어진 예제들을 가지고 하나씩 실행하다보니 예외가 생각보다 많았고 &lt;/span&gt;몇 번의 리팩토링 끝에 완성(?)하였다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위의 코드에서 (*) 부분은 원래 생각지 못했던 부분인데, all 함수에 순회객체를 넘길 때 Promise 인스턴스가 하나도 없을 경우 resolve가 되지 않아 then으로 값이 넘어오지 않고 계속 pending상태로 남아있는 경우가 발생해서 추가하게 된 부분이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 처음에는 아무생각없이 result.length로 판단하고 resolve를 실행해주었는데 비동기함수 인자를 마지막에 넘기는 바람에 실행이 너무나도 잘되서 그게 끝인줄 알았는데... 다행히 블로그 작성을 완료하기 전에 순서를 바꿔 테스트 해보니 안되는걸 발견하고 이후로 엄청 고민했던거 같다.&lt;/p&gt;
&lt;p&gt;그래서 결국 해결책으로 사용한 방법이 Promise 객체처럼 상태값을 활용하는 것!&lt;/p&gt;
&lt;p&gt;{ state: 'pending', value: undefined } 이 객체를 처음 이터러블 length 만큼 초기화 해두고 상태값을 판단해서 모두 fulfilled되면 resolve하는 것으로 해결해 주었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코드는 &lt;a href=&quot;https://github.com/sunivers/promise-practice/blob/master/index.js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙&lt;/a&gt;에 올려두었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실행해보기&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1604420631613&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const p = new Promise((resolve, reject) =&amp;gt; {
  setTimeout(() =&amp;gt; {
    resolve('foo');
  }, 1000);
});

// 성공
all(['hi', p, Promise.resolve(33)])
  .then(values =&amp;gt; console.log(values))
  .catch(reason =&amp;gt; console.log(reason));
  
// 실패
all(['hi', p, Promise.resolve(33), Promise.reject(555)])
  .then(values =&amp;gt; console.log(values))
  .catch(reason =&amp;gt; console.log(reason));

// 콘솔에는 아래 순서대로 찍힌다.
// 555
// [ 'hi', 'foo', 33 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행 결과&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;스크린샷 2020-11-04 오전 2.40.08.png&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;64&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hbuog/btqMwnnAsqm/rLRmdtyWSU7ciZEraN9oqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hbuog/btqMwnnAsqm/rLRmdtyWSU7ciZEraN9oqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hbuog/btqMwnnAsqm/rLRmdtyWSU7ciZEraN9oqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHbuog%2FbtqMwnnAsqm%2FrLRmdtyWSU7ciZEraN9oqK%2Fimg.png&quot; data-filename=&quot;스크린샷 2020-11-04 오전 2.40.08.png&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;64&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;성공 케이스는 모든 실행이 완료되어야만 나오기 때문에 위 코드의 경우 setTimeout에 걸려서 실패 케이스보다 나중에 나온다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결하지 못한 예외케이스&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;mdn 문서에 나온 예시 대부분은 출력이 일치하는 것을 확인했는데 한 가지 정확하게 구현하지 못한 케이스가 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2020-11-04 오전 1.54.35.png&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;682&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vXGeq/btqMwmPLwWO/C47Wday9ce8PDycdAo3kq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vXGeq/btqMwmPLwWO/C47Wday9ce8PDycdAo3kq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vXGeq/btqMwmPLwWO/C47Wday9ce8PDycdAo3kq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvXGeq%2FbtqMwmPLwWO%2FC47Wday9ce8PDycdAo3kq0%2Fimg.png&quot; data-filename=&quot;스크린샷 2020-11-04 오전 1.54.35.png&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;682&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2020-11-04 오전 2.44.43.png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;438&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M2eJ8/btqMvgJfQUY/GaGIKsNDHxRsvRO93VpPk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M2eJ8/btqMvgJfQUY/GaGIKsNDHxRsvRO93VpPk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M2eJ8/btqMvgJfQUY/GaGIKsNDHxRsvRO93VpPk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM2eJ8%2FbtqMvgJfQUY%2FGaGIKsNDHxRsvRO93VpPk0%2Fimg.png&quot; data-filename=&quot;스크린샷 2020-11-04 오전 2.44.43.png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;438&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;i&gt;the stack is now empty&lt;/i&gt; 가 출력되기 전에는 Promise가 pending 상태여야 하는데 내가 만든 all 함수는 fulfilled 상태로 뱉는다.&lt;/p&gt;
&lt;p&gt;Promise 인스턴스가 아닌 값도 Promise로 한번 감싸서 then 처리를 해주는걸까?&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>promise</category>
      <category>promise.all</category>
      <category>직접구현</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/47</guid>
      <comments>https://soobakba.tistory.com/47#entry47comment</comments>
      <pubDate>Wed, 4 Nov 2020 03:08:24 +0900</pubDate>
    </item>
    <item>
      <title>클린코드 스터디 후기</title>
      <link>https://soobakba.tistory.com/46</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;cover.jpg&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;395&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHdeQI/btqMhtIU15A/16840onhO6AjYK2X9ENdp1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHdeQI/btqMhtIU15A/16840onhO6AjYK2X9ENdp1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHdeQI/btqMhtIU15A/16840onhO6AjYK2X9ENdp1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHdeQI%2FbtqMhtIU15A%2F16840onhO6AjYK2X9ENdp1%2Fimg.jpg&quot; data-filename=&quot;cover.jpg&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;395&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참여 동기&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;우아한 테크러닝에 참여하면서 많은 사람들이 정말 열정적으로 공부하고 있구나를 알게 되었다. 초반에는 사람들 블로그 염탐도 많이 갔었다. 그러다가 몇몇분들이 스터디 모집글을 올리는걸 보고 자극받아서 여기저기 참여하게 되었는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/Road-of-CODEr/clean-code&quot;&gt;클린코드 스터디&lt;/a&gt;도 그 중 하나다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사실 이 책은 여기저기서 추천하는 글들을 많이 보았는데 집에 사놓고 안 읽은 책만 수두룩했기 때문에 도전할 엄두가 안났었다. 그런데 마침 이 책을 가지고 스터디를 한다길래 잘됐다 싶어서 참여했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참여 과정&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;책은 회사 도서구매비로 결재를 올렸는데 팀장님이 자기 집에 이 책 두권이나 있다면서 결재올린건 회사에 두고 한 권을 나에게 공짜로 주셨다ㅎㅎ &lt;s&gt;(팀장님도 다 안읽으셨다고...)&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;나는 사실 스터디 내내 거의 청강 느낌으로 참여했다. &lt;/span&gt;스터디 형식이 챕터별로 발표자를 정해 정리해서 공유하는 형식인데, 자발적 참여로 이루어져서 사실&amp;nbsp;발표자가 거의 고정되어있었다...&amp;nbsp;&lt;s&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;그래도 최대한 꼬박꼬박 참여는 했었고 책도 다 읽으려고 노력했다. &lt;span style=&quot;color: #333333;&quot;&gt;막판에 부진했지만...&lt;/span&gt;&amp;nbsp;결국 다 못읽었지만... 스터디에 참여만 해도 발표자분이 내용을 거의 떠먹여주셔서 얻어가는 정보가 정말 많았다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 좋았던 점은 클린코드 책이 Java 코드 위주여서 자바스크립트만 다뤄본 나에게는 생소하고 공감가지 않는 내용들이 간혹 있었는데 스터디 시간에 그런것들을 짚어주시고 실제 자바스크립트 코드에 클린코드 법칙을 적용하면 어떤식으로 적용할 수 있는지 등을 직접 코드로 보여주고 설명해주셔서 그런 점이 정말 좋았다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 책 발표가 끝나도 네트워킹 시간이라고 하면서 남은 시간동안 여러가지 얘기도 하고 이런 저런 정보를 공유해주셨는데 은근 꿀정보가 많아서 알차게 주워담아두었다.&lt;/p&gt;
&lt;p&gt;&lt;s&gt;나중에 알고보니 스터디 리드하시는 분이 나랑 같은 대학교 심지어 같은과 후배님이셨는데... 개발 실력은 나보다 선배님이었다...&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참여 후 느낀점&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;사실 이번에 클린코드 책을 읽으면서 가장 좋았던 점은 그동안 내가 실무를 하면서 막연하게 느꼈던 가려운 부분들을 시원하게 긁어준 느낌이었다는거다. 매번 코드를 짜면서 리팩토링을 하거나 로직을 어디까지 분리할지, 주석처리, 예외처리 등등 애매하고 정확한 답이 없는 문제들로 고민한 적이 많았는데 이 책을 읽으면서 어느정도 규칙이 세워졌다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;특히 &lt;b&gt;추상화 레벨&lt;/b&gt;이라는 용어가 가장 기억에 남는데, 로우레벨의 코드는 추상화하여 분리하고 그렇게 분리된 코드들의 추상화 레벨을 모두 일정하게 맞춰야 한다는 내용을 읽고선 이 후 회사에 가서 내가 분리할지 말지 고민하던 코드뭉치들을 모두 정리했다.&lt;/p&gt;
&lt;p&gt;모든 코드들이 그렇듯이 어디까지 분리할것인지가 내가 최근에 겪었던 가장 고민하던 부분이었는데 그걸 추상화 레벨이라는 개념하나로 모두 정리해준듯 했다. 물론 사람마다 차이는 있겠지만 이 책이 인기있는 이유는 그만큼 많은 사람들이 인정하고 받아들일 수 있을만한 내용이라는거 아닐까?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;발표자분도 매번 말씀했던게 저자의 잔소리가 잔뜩 적혀있다고 했는데 모든 내용이 수용가능한 것은 아니지만 그래도 다 좋은 말들임에는 틀림없는 것 같았다.&lt;/p&gt;
&lt;p&gt;틈날때 마다 못읽은 부분들은 더 읽어보고 스터디 레파지토리에 정리된거 보면서 중요한 부분들은 항상 되새기면서 코드를 짜도록 노력해야겠다.&lt;/p&gt;</description>
      <category>개발상식 일반</category>
      <category>cleancode</category>
      <category>클린코드</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/46</guid>
      <comments>https://soobakba.tistory.com/46#entry46comment</comments>
      <pubDate>Mon, 2 Nov 2020 00:53:04 +0900</pubDate>
    </item>
    <item>
      <title>[JS] 프라미스 Promise 알아보기</title>
      <link>https://soobakba.tistory.com/45</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;js.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot; width=&quot;356&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FTWWY/btqLU9RTHQ8/HxukcS2eoKmPK3nDoW1QG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FTWWY/btqLU9RTHQ8/HxukcS2eoKmPK3nDoW1QG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FTWWY/btqLU9RTHQ8/HxukcS2eoKmPK3nDoW1QG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFTWWY%2FbtqLU9RTHQ8%2FHxukcS2eoKmPK3nDoW1QG1%2Fimg.png&quot; data-filename=&quot;js.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot; width=&quot;356&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Promise&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;ES6에서 등장한 Promise 객체는 자바스크립트에서의 비동기 처리를 획기적으로 변화시켜주었다. 기존에 '콜백 지옥'이라 불리던 것을 해결하며 더 명료한 코드를 작성할 수 있도록 해준다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Promise 객체 속성&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2020-10-28 오전 12.35.02.png&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;364&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8rVXe/btqLXSiXHwM/KOZ7rCKxaRKDppkLPFCKw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8rVXe/btqLXSiXHwM/KOZ7rCKxaRKDppkLPFCKw1/img.png&quot; data-alt=&quot;프로미스 객체를 콘솔에 찍어보았다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8rVXe/btqLXSiXHwM/KOZ7rCKxaRKDppkLPFCKw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8rVXe%2FbtqLXSiXHwM%2FKOZ7rCKxaRKDppkLPFCKw1%2Fimg.png&quot; data-filename=&quot;스크린샷 2020-10-28 오전 12.35.02.png&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;364&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로미스 객체를 콘솔에 찍어보았다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Promise는 catch, finally, then 등의 메소드를 가지고 있고 &lt;span style=&quot;color: #333333;&quot;&gt;state와 result라는 내부 프로퍼티를 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Promise는 3가지 state를 갖는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;pending - 비동기 처리가 아직 완료되지 않은 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;fulfilled - 비동기 처리가 성공적으로 끝나 resolve가 호출된 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;rejected - 비동기 처리 중 오류가 발생해 reject가 호출된 상태&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;위 상태에 따른 result는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;pending 상태 - undefined&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;fulfilled 상태 - resolve를 호출할 때 넘겨준 값.&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span&gt;resolve(&quot;Done!&quot;) 이렇게 호출하면 result는 &quot;Done!&quot; 이 된다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;rejected 상태 - reject를 호출할 때 넘겨준 에러 객체.&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span&gt;reject(new Error()) 이렇게 호출하면 result는 &lt;/span&gt;&lt;u&gt;&lt;i&gt;Promise {&amp;lt;rejected&amp;gt;: Error... &lt;/i&gt;&lt;/u&gt;로 시작하는 에러 객체가 할당된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;resolve나 reject호출을 안 할 경우는 계속 pending상태를 유지한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문법&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1603813152774&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const promise = new Promise((resolve, reject) =&amp;gt; {
  // ... 비동기 처리
  resolve(result); // 성공시
  reject(new Error()); // 실패시
})
  .then(result =&amp;gt; console.log(result)) // resolve가 실행됐을 경우
  .catch(error =&amp;gt; console.log(error)) // reject가 실행됐을 경우
  .finally(() =&amp;gt; console.log('finally')) // 성공/실패에 상관없이 항상 호출&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;문법 및 사용법은 위와 같다.&lt;/p&gt;
&lt;p&gt;1. 비동기 로직을 Promise 객체로 감싸고 해당 로직이 성공할 경우 resolve를 실패할 경우 reject를 호출하도록 코드를 구성한다.&lt;/p&gt;
&lt;p&gt;2. 비동기 로직이 완료된 후 처리되어야 하는 코드를 then과 catch에 넣는다.&lt;/p&gt;
&lt;p&gt;3. then과 catch는 각각 인자로 resolve와 reject를 호출할 때 넘겨준 인자들을 받는다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;에러는 가급적 catch로 처리&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;.then에서 첫 번째 인수는 프라미스가 이행되었을 때 실행되는 함수, 두 번째 인수는 프라미스가 거부되었을 때 실행되는 함수이기 때문에 에러 처리시 아래처럼 코드를 구성하는것도 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1603813661696&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const promise = new Promise((resolve, reject) =&amp;gt; reject(new Error('hi')));
promise.then(result =&amp;gt; alert(result), error =&amp;gt; alert(error));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러나 프로미스가 이행(resolve)된 후, 첫 번째 인수인 함수에서 에러가 발생했을 경우는 에러 캐치가 제대로 이루어지지 않는다.&lt;/p&gt;
&lt;p&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1603815265014&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const promise = new Promise((resolve, reject) =&amp;gt; resolve('hi'));

// .then 사용
promise.then(result =&amp;gt; {
  console.log(result);
  throw new Error('This is Error 1');
}, (error) =&amp;gt; {
  console.log(error);
})

// .catch 사용
promise.then(result =&amp;gt; {
  console.log(result);
  throw new Error('This is Error 2');
}).catch((error) =&amp;gt; {
  console.log(error);
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드의 실행결과는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2020-10-28 오전 1.14.11.png&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;254&quot; width=&quot;593&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uZQ4b/btqLRVmxhxK/xKq0aaO1HlKHD7GN6PBkIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uZQ4b/btqLRVmxhxK/xKq0aaO1HlKHD7GN6PBkIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uZQ4b/btqLRVmxhxK/xKq0aaO1HlKHD7GN6PBkIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuZQ4b%2FbtqLRVmxhxK%2FxKq0aaO1HlKHD7GN6PBkIk%2Fimg.png&quot; data-filename=&quot;스크린샷 2020-10-28 오전 1.14.11.png&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;254&quot; width=&quot;593&quot; height=&quot;NaN&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;.catch를 사용한 코드에서는 에러를 무사히 잡아서 콘솔에 출력했지만 .then만 사용한 코드에서는 에러를 잡지 못하고 뱉어버렸다.&lt;/p&gt;
&lt;p&gt;그래서 가급적 catch를 사용해서 에러를 처리하는 것이 안전하다. 그리고 코드를 읽는 입장에서도 두번째 인자로 에러를 처리하는것은 catch보다 명시적이지 못하다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고 자료 : &lt;a href=&quot;https://joshua1988.github.io/web-development/javascript/promise-for-beginners/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;joshua1988.github.io/web-development/javascript/promise-for-beginners/&lt;/a&gt;&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>async</category>
      <category>js</category>
      <category>promise</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/45</guid>
      <comments>https://soobakba.tistory.com/45#entry45comment</comments>
      <pubDate>Wed, 28 Oct 2020 00:58:16 +0900</pubDate>
    </item>
    <item>
      <title>[주간회고] 2020년 10월 4주차 회고 기록</title>
      <link>https://soobakba.tistory.com/44</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2020-10-25 오후 10.43.05.png&quot; data-origin-width=&quot;214&quot; data-origin-height=&quot;196&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhq7wM/btqLIG9Tkzz/5UB5wZMBZqm8RaSBZ18xWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhq7wM/btqLIG9Tkzz/5UB5wZMBZqm8RaSBZ18xWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhq7wM/btqLIG9Tkzz/5UB5wZMBZqm8RaSBZ18xWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhq7wM%2FbtqLIG9Tkzz%2F5UB5wZMBZqm8RaSBZ18xWk%2Fimg.png&quot; data-filename=&quot;스크린샷 2020-10-25 오후 10.43.05.png&quot; data-origin-width=&quot;214&quot; data-origin-height=&quot;196&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;작년 연말에 많은 개발자들이 1년 회고록을 올리는 걸 보고는 그 많은 것들을 기억하고 글로 정리한다는게 대단하다고 생각하면서도 나는 귀찮고 까먹어서 절대 못할거라고 생각했었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그런데 이번 &lt;a href=&quot;https://github.com/blackcoffee-dev/blog&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그 스터디&lt;/a&gt;를 하면서 이렇게 짧막하게 주간회고를 기록할 수도 있다는 것을 알게되어서 가벼운 마음으로 작성해보고자 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;회사&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아레나 참가개편&lt;/h3&gt;
&lt;p&gt;지겹도록 길었던 아레나 참가개편 작업이 끝을 보이기 시작하면서 이번주는 그래도 조금 널널했다. 끝없는 큐에이 이슈를 처리하면서 지난 주 하루는 새벽 5시 넘어서 퇴근한 대기록을 세웠었는데 이번주는 그 여파인지 몰라도 좀 게으르게 일했던거 같다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;금요일 팀 회고 시간에 모두가 각자 이번주 스프린트에 작업한 내용에 대해 이야기 하는데 팀장님과 본부장님 모두 이번 작업이 길어지고 힘들었던 것에 대해 &lt;/span&gt;&lt;b&gt;이건 누구의 잘못도 아닌 조직 시스템의 문제다.&amp;nbsp;&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;라고 얘기해주셔서 조금 마음이 놓였다. 이번에 나를 포함한 개편작업에 참여했던 크루원들 모두 지치고 자신감도 많이 떨어졌을텐데 앞으로 전체개편이라는 더 큰 산이 남아있기에... 이번 실패(?)를 발판 삼아서 다음 작업때는 심기일전 해야한다고 하셨다....&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;CDD와 스토리북&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;스토리북은 &lt;/span&gt;예전부터 너무 써보고 싶었는데 이번에 한 팀원이 CDD에 대한 기술공유발표를 하면서 스토리북 사용을 언급, 예시를 보여주어 내친김에 간단한 로딩컴포넌트와 토스트메세지 컴포넌트를 작성해 보았다. 처음에 약간 헤매느라 시간을 까먹었지만 폴더로 정리하고 상태별로 정리하고나니 눈에 딱 보여지는것이 너무 좋았다. 기획자, 디자이너와 협업하기 딱 좋은 툴 같다. &lt;s&gt;아직은 서버에 올리지 않아 개발자끼리만 볼 수 있지만...&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;CDD를 도입하고 스토리북을 쓰기 시작하면 결국은 디자인 시스템이 도입되어야 한다는데 그걸 잘 풀어나갈 수 있을지... 걱정반 기대반&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;스터디&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;지금 여기저기 발을 걸친 스터디가 많이 있는데 사실 말그대로 발만 걸치게 된 스터디도 좀 있다...&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;월 - 블로그 스터디&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;수 - 성공과 실패를 결정하는 1%의 네트워크 원리 책 스터디&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;목 - 코어 자바스크립트 토대로 공부하고 블로그 작성 스터디 (매주 요일 변경 가능, 다음주는 수요일)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;일 - 클린코드 책 스터디 (이번주 종료 - 불참)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;그 외 - &lt;a href=&quot;https://github.com/Road-of-CODEr/stupid-week&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;한 주에 3개 목표를 정해 지키는 스터디&lt;/a&gt; (이번주 첫 참여 - 알고리즘 문제 3개 풀기 도전중)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;요일별로 정리해보면 이정도인데 사실 수요일, 일요일 책스터디는 발표자가 정해져있고 거의 청강식이라서 책을 제대로 안읽은적도 많고 비양심적인 참여가 많았다. 이번주 특히ㅠㅠ 그래도 발표자분이 워낙 준비를 잘해오셔서 듣는것만으로도 도움이 되긴 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;월요일 블로그 스터디와 목요일 코어 자바스크립트 스터디는 그래도 나름 열심히 참여했다고 생각했는데 다른 분들을 보면 정말 열심히 사시고 대단한 분들이 많아서 자극이 많이 된다. 그러나 남들 쫓아가려다가 가랑이가 찢어질거 같아서 나는 내 페이스를 유지하면서 할 수 있는만큼만 최선을 다하자고 다짐했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;운동&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;주말동안 드디어 테니스를 시작했다. 그동안 헬스, 필라테스, 플라잉 요가 등 좋다는 운동들을 다 조금씩 해봤는데 한두달 하다보면 다 재미가 없어졌다. 그래서 게임처럼 할 수 있는 구기종목을 해봐야겠다 생각했고 배드민턴, 탁구, 스쿼시 등 찾아보다가 실내 테니스가 있다길래 바로 등록했다.&lt;/p&gt;
&lt;p&gt;강사님이 되게 푸근하고 털털한 스탈이셔서 내 저질체력을 놀리시면서도 맞춤형으로 잘 알려주셔서 넘 좋았다.&lt;/p&gt;
&lt;p&gt;토요일에는 정말 기초 훈련만 한 느낌이었는데 일요일에는 테니스 용어랑 자세를 어렴풋하게나마 배웠다. 생각보다 진도도 쭉쭉 나가고 실력도 잘 느는거 같아서 지금까진 정말 재밌다. 비록 팔다리에 다 알이 베겼지만 앞으로 꾸준히 할 운동을 찾은 느낌이 들어서 기분이 좋다 :)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;주간회고는 금방 할 수 있을줄 알았는데 이런저런 생각들과 느낀점을 떠올리며 글로 다듬어 정리하려다보니 생각보다 오래걸렸다. 글 쓰는 일은 정말 어려운 것 같다. 글 쓰기는 언제쯤 편하게 느껴질까.&lt;/p&gt;</description>
      <category>회고</category>
      <category>개발자</category>
      <category>스터디</category>
      <category>운동</category>
      <category>주간회고</category>
      <author>수박소바</author>
      <guid isPermaLink="true">https://soobakba.tistory.com/44</guid>
      <comments>https://soobakba.tistory.com/44#entry44comment</comments>
      <pubDate>Sun, 25 Oct 2020 22:44:39 +0900</pubDate>
    </item>
  </channel>
</rss>