<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>모르는건 한번으로 족해</title>
    <link>https://hayjo.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 29 Jun 2026 05:49:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hayjo</managingEditor>
    <image>
      <title>모르는건 한번으로 족해</title>
      <url>https://tistory1.daumcdn.net/tistory/3917623/attach/c01885335d4c4ed4abba80fef7f0e32b</url>
      <link>https://hayjo.tistory.com</link>
    </image>
    <item>
      <title>python not found after macOS update to Monterey</title>
      <link>https://hayjo.tistory.com/122</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;(혹시 1.65.0 이전 버전을 쓰고 있다면) VSCode를 업데이트하면 대부분의 오류가 해결된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VSCode를 설치하자마자 추가하는 커맨드가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 디렉토리에서 VSCode를 바로 실행할 수 있어서 자주 쓰는 커맨드.&lt;/p&gt;
&lt;pre id=&quot;code_1650805620586&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;code .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ps. 혹시 컴퓨터를 재시작할 때마다 code command를 다시 설정해줘야 했다면 이쪽이 답이다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/42051&quot;&gt;https://github.com/microsoft/vscode/issues/42051&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VSCode가 Downloads에서 돌아가고 있다면 PATH를 잡아줘도 소용이 없어서 이런 일이 발생한다.&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;그런데 macOS를 Monterey로 업데이트하고나니 이런 에러가 출력되고 동작하지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1650805738554&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/usr/local/bin/code: line 6: python: command not found
/usr/local/bin/code: line 10: ./MacOS/Electron: No such file or directory&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vscode ./MacOS/Electron: No such file or directory monterey 라고 검색하니 정확히 똑같은 이슈를 문의한 사람이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/141738&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/microsoft/vscode/issues/141738&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근본적인 이유는 macOS Monterey에서 내장되어있던 python2를 삭제하면서, VSCode 내부에서 python을 사용하던 기능들이 작동하지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곧 &lt;a href=&quot;https://github.com/microsoft/vscode/pull/138582&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;fix PR&lt;/a&gt;이 올라왔고 3월 초에 1.65.0 버전이 릴리즈되면서 해결되었다. 이후 python이 사용되고 있는 부분이 발견될때마다 패치하고 있는 듯하다.&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;초기에 사용하다보면 문제를 직접 해결해야한다는 것이 진입장벽으로 작용하기도 하지만, 같은 이슈에 따봉을 찍어줄 때는 참 보람차다.&lt;/p&gt;</description>
      <category>FrontEnd</category>
      <category>MacOS</category>
      <category>Monterey</category>
      <category>python</category>
      <category>맥업데이트</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/122</guid>
      <comments>https://hayjo.tistory.com/122#entry122comment</comments>
      <pubDate>Sun, 24 Apr 2022 22:52:06 +0900</pubDate>
    </item>
    <item>
      <title>기존 프로젝트 react v17 -&amp;gt; v18 업그레이드 로그</title>
      <link>https://hayjo.tistory.com/121</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#installing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;React v18이 나왔다는 기쁜 소식&lt;/a&gt;에 기존 프로젝트의 react와 react-dom 버전을 최신으로 올리려고 봤더니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Conflicting peer dependency 에러가 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqQj0T/btry0dqXBmz/Orfdg9i1SuAHEdjLMkKKfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqQj0T/btry0dqXBmz/Orfdg9i1SuAHEdjLMkKKfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqQj0T/btry0dqXBmz/Orfdg9i1SuAHEdjLMkKKfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqQj0T%2Fbtry0dqXBmz%2FOrfdg9i1SuAHEdjLMkKKfK%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;502&quot; height=&quot;267&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@reduxjs/toolkit에서 react@&quot;^16.14.0 || ^17.0.0&quot;만 지원하도록 설정해두었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 버전으로 올리면 React v18을 지원하는지 살펴보니 최근에 업데이트가 되었다.&lt;/p&gt;
&lt;figure id=&quot;og_1649569050778&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Release v1.8.1 &amp;middot; reduxjs/redux-toolkit&quot; data-og-description=&quot;This release updates RTK's peer dependencies to accept React 18 as a valid version. This should fix installation errors caused by NPM's &amp;quot;install all the peer deps and error if they don't match&amp;quot; be...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reduxjs/redux-toolkit/releases/tag/v1.8.1&quot; data-og-url=&quot;https://github.com/reduxjs/redux-toolkit/releases/tag/v1.8.1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/JywZr/hyN0cXFBcd/wO5s2AY2K39bz7t8vcimV1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/reduxjs/redux-toolkit/releases/tag/v1.8.1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/reduxjs/redux-toolkit/releases/tag/v1.8.1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/JywZr/hyN0cXFBcd/wO5s2AY2K39bz7t8vcimV1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Release v1.8.1 &amp;middot; reduxjs/redux-toolkit&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This release updates RTK's peer dependencies to accept React 18 as a valid version. This should fix installation errors caused by NPM's &quot;install all the peer deps and error if they don't match&quot; be...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 toolkit 버전을 올리면 되겠지?&lt;/p&gt;
&lt;pre id=&quot;code_1649569101216&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install @reduxjs/toolkit@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;335&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nILsG/btryUgvXhFZ/r76gfar1hq1gcTPRsMZ8dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nILsG/btryUgvXhFZ/r76gfar1hq1gcTPRsMZ8dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nILsG/btryUgvXhFZ/r76gfar1hq1gcTPRsMZ8dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnILsG%2FbtryUgvXhFZ%2Fr76gfar1hq1gcTPRsMZ8dK%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;510&quot; height=&quot;335&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;335&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러려면 먼저 react-redux 버전을 올려줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1649569202880&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install react-redux@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그랬더니 redux-toolkit 버전이 낮아서 안 된다는 에러가 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;닭이 먼저냐 달걀이 먼저냐를 해결하려면 둘을 동시에 업데이트해줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1649569267549&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install react-redux@latest @reduxjs/toolkit@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리덕스가 해결되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잊지말고 @testing-library/react 버전도 올려줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올리지 않고 테스트를 돌리면 이런 워닝이 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;57&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baseZA/btryV2jGFRo/qBgzpdO2euZoGtPX1n9oGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baseZA/btryV2jGFRo/qBgzpdO2euZoGtPX1n9oGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baseZA/btryV2jGFRo/qBgzpdO2euZoGtPX1n9oGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaseZA%2FbtryV2jGFRo%2FqBgzpdO2euZoGtPX1n9oGK%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;831&quot; height=&quot;57&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;57&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1649569554962&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install @testing-library/react@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트를 마치고 프로젝트를 확인해보는데 라우터가 작동하지 않는다. 살펴보니 이런 이슈가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/facebook/react/issues/21674&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/facebook/react/issues/21674&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1649569738375&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;React 18: react-router@v5 is breaking in the Strict Mode (strict effects) &amp;middot; Issue #21674 &amp;middot; facebook/react&quot; data-og-description=&quot;remix-run/react-router#7870 I do not have permission to post https://github.com/reactwg/react-18/discussions. Please open and pin a new issue in that repo to list all widely-used library that does ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/facebook/react/issues/21674&quot; data-og-url=&quot;https://github.com/facebook/react/issues/21674&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cgPx1O/hyNYOqzRd4/QnCEwO4jSF3CAVixCcwCok/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/facebook/react/issues/21674&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/facebook/react/issues/21674&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cgPx1O/hyNYOqzRd4/QnCEwO4jSF3CAVixCcwCok/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;React 18: react-router@v5 is breaking in the Strict Mode (strict effects) &amp;middot; Issue #21674 &amp;middot; facebook/react&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;remix-run/react-router#7870 I do not have permission to post https://github.com/reactwg/react-18/discussions. Please open and pin a new issue in that repo to list all widely-used library that does ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-router-dom@v5에서 동작하지 않는다고 한다. 이것도 버전을 올려준다.&lt;/p&gt;
&lt;pre id=&quot;code_1649570098786&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install react-router-dom@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-router-dom은 API 변화가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트하고 &lt;a href=&quot;https://reactrouter.com/docs/en/v6/upgrading/v5#upgrade-to-react-router-v6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서의 변경사항&lt;/a&gt;을 참고해서 바뀐 부분을 수정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;lt;Switch /&amp;gt; -&amp;gt; &amp;lt;Routes /&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;Redirect&amp;gt;s inside &amp;lt;Switch&amp;gt; -&amp;gt; &amp;lt;Route path=&quot;/sth&quot; element={&amp;lt;Navigate to=&quot;path&quot;} /&amp;gt;} /&amp;gt;&lt;/li&gt;
&lt;li&gt;useHistory -&amp;gt; useNavigate와 관련 api 변경사항&lt;/li&gt;
&lt;li&gt;그리고 regex 지원 범위 축소 대응&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 지원되던 ? 옵션이 빠져서 약간 당황했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/:parent/:child? 를 /:parent와 /:parent/:child로 각각 쪼개서 넣어주면 정상적으로 작동하는 걸 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useParams를 쓰면 /:parent일 때 child는 undefined로 들어오기 때문에 문제 없다.&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;내 경우 child가 없는 경우를 default로 처리하려했던 거라서, 이제는 /parent에서 따로 redirect를 해주는 게 더 나을 것 같다.&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;react-router-dom까지 버전 업데이트를 하고 나니 드디어 React v18에서 제대로 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 React18의 기능을 직접 써본 건 없지만 일단 한 걸음 내딛었다는데 의의를 두기로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;덧붙임&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 18을 지원하지 않는 dependency 처리 이야기&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;프로젝트 기존 dependency 중에 &lt;a href=&quot;https://www.npmjs.com/package/react-dnd-multi-backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-dnd-multi-backend&lt;/a&gt;가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 도중 드래그 앤 드롭 부분에서 react-dnd의 도움을 받았는데, react-dnd는 마우스와 터치 동시 지원이 안 되는 아쉬움이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 &lt;a href=&quot;https://react-dnd.github.io/react-dnd/docs/backends/touch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-dnd-touch-backend&lt;/a&gt;를 이용하고 enableMouseEvents 옵션을 주면 사용가능하지만, 문서에서 언급하고 있듯이 버그가 좀 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 도입한 것이 react-dnd-multi-backend.&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;그런데 React 버전을 올리려니까 Conflicting peer dependency 이슈가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;react-dnd-multi-backend에서 react18을 지원해줄 때까지 버전 업데이트를 미루거나&lt;/li&gt;
&lt;li&gt;react-dnd-multi-backend를 걷어내고 대체재를 물색하거나&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정을 해야했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 react-dnd-multi-backend에 직접 PR을 올리는 게 가장 이상적인 해결책이겠지만, 쉽지 않을 것 같고 그렇게까지 리소스를 투입하기는 어려워서 다른 방법을 찾기로 했다.&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;생각해보니 JS단에서 touch device를 감지하는 방법이 분명 있을 것 같다.&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;그럼 redux에 backend를 세팅하는 시점에 디바이스 종류를 파악해서 걸맞은 라이브러리를 넣어주면 되지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 생각에 도달, 검색해보니 무려 11년 전에 올라온 답변이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;whats-the-best-way-to-detect-a-touch-screen-device-using-javascript&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isTouchDevice 헬퍼를 이용해서 backend를 조건부로 넣는 것에 성공.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 react-dnd-multi-backend 의존성이 사라졌으니 해당 라이브러리를 들어내도 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1650804875657&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { HTML5Backend } from &quot;react-dnd-html5-backend&quot;;
import { TouchBackend } from &quot;react-dnd-touch-backend&quot;;
import App from &quot;./App&quot;;
import store from &quot;./app/store&quot;;
import isTouchDevice from &quot;./helpers/device&quot;;

const backend = isTouchDevice() ? TouchBackend : HTML5Backend;
const container = document.getElementById(&quot;root&quot;);
const root = createRoot(container);

root.render(
  &amp;lt;React.StrictMode&amp;gt;
    &amp;lt;Provider store={store}&amp;gt;
      &amp;lt;DndProvider backend={backend}&amp;gt;
        &amp;lt;Router&amp;gt;
          &amp;lt;App /&amp;gt;
        &amp;lt;/Router&amp;gt;
      &amp;lt;/DndProvider&amp;gt;
    &amp;lt;/Provider&amp;gt;
  &amp;lt;/React.StrictMode&amp;gt;,
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고보니 처음부터 이렇게 했어도 됐을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 임시방편이고, 마우스와 터치를 둘다 지원하는 디바이스가 있다면 한쪽만 먹힐 테니 최종적으로는 멀티를 처리하는 방향으로 가야하겠지만.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 이 방법은 dynamic import를 할 수 있으니 속도가 조금 향상될 수 있을 것 같기도 하고.&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;이런 부분을 고려하면, 라이브러리를 선택할 때 peer dependency 여부/볼륨도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로젝트가 제때 업데이트되는 건 아니니까, 특정 라이브러리에 의존하게 되면 전체 업데이트 사이클이 종속돼버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;no dependency 라이브러리들이 인기를 끌고 있는 이유를 직접 체감할 수 있었던 좋은 경험이었다.&lt;/p&gt;</description>
      <category>FrontEnd</category>
      <category>React</category>
      <category>react18</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/121</guid>
      <comments>https://hayjo.tistory.com/121#entry121comment</comments>
      <pubDate>Sun, 24 Apr 2022 22:00:18 +0900</pubDate>
    </item>
    <item>
      <title>React in JS Bin</title>
      <link>https://hayjo.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;마크업을 간단히 테스트해볼 때 &lt;a href=&quot;https://jsbin.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JS Bin&lt;/a&gt;을 자주 사용하는데, React를 사용하려면 boilerplate가 약간 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 세팅은 &lt;a href=&quot;https://ko.reactjs.org/docs/add-react-to-a-website.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;리액트 홈페이지에서 제공하는 방식&lt;/a&gt;을 따르면 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://raw.githubusercontent.com/reactjs/reactjs.org/main/static/html/single-file-example.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://raw.githubusercontent.com/reactjs/reactjs.org/main/static/html/single-file-example.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 경로의 HTML 텍스트를 JS Bin의 HTML 부분에 복사 붙여넣기하면 바로 편집이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfD2dS/btruVX68Hu6/X0sIkYTKfxcw9HhYB526g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfD2dS/btruVX68Hu6/X0sIkYTKfxcw9HhYB526g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfD2dS/btruVX68Hu6/X0sIkYTKfxcw9HhYB526g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfD2dS%2FbtruVX68Hu6%2FX0sIkYTKfxcw9HhYB526g0%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;910&quot; height=&quot;467&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;467&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;script 태그에서 바로 작성하는 것이 불편하다면 JavaScript 탭을 ES6 / Babel로 변경하고, script 태그 내부의 내용을 옮겨오면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;399&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GAog6/btruWkOSQFW/JkWSvAb3f53d6qr3eLJKQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GAog6/btruWkOSQFW/JkWSvAb3f53d6qr3eLJKQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GAog6/btruWkOSQFW/JkWSvAb3f53d6qr3eLJKQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGAog6%2FbtruWkOSQFW%2FJkWSvAb3f53d6qr3eLJKQK%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;399&quot; height=&quot;263&quot; data-origin-width=&quot;399&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mw7MM/btruXUPHqtC/rZeDLW3CUNwvfUblKgbKtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mw7MM/btruXUPHqtC/rZeDLW3CUNwvfUblKgbKtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mw7MM/btruXUPHqtC/rZeDLW3CUNwvfUblKgbKtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMw7MM%2FbtruXUPHqtC%2FrZeDLW3CUNwvfUblKgbKtk%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;676&quot; height=&quot;316&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전이 크게 중요하지 않다면 JS Bin에서 자체적으로 추가하는 방법도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상단 Add library 버튼에서 적당한 버전을 클릭하면 되고, 혹시 버전이 중요하다면 &lt;a href=&quot;https://www.codeblocq.com/2016/09/Setup-the-latest-React-in-JSBin/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이쪽&lt;/a&gt;을 참고해서 cdn에서 latest를 가져오면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtAx8h/btruW9l034v/yETHX3Nb06AwibAwcqXR00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtAx8h/btruW9l034v/yETHX3Nb06AwibAwcqXR00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtAx8h/btruW9l034v/yETHX3Nb06AwibAwcqXR00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtAx8h%2FbtruW9l034v%2FyETHX3Nb06AwibAwcqXR00%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;572&quot; height=&quot;479&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리를 선택하면 HTML에 스크립트 태그가 추가된다.&lt;/p&gt;
&lt;pre id=&quot;code_1646221989826&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;https://fb.me/react-15.1.0.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;https://fb.me/react-dom-15.1.0.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 위에서처럼 ES6 / Babel을 선택한 후 탬플릿을 입력하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1646223504186&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const App = () =&amp;gt; {
  return &amp;lt;h1&amp;gt;Hello, world!&amp;lt;/h1&amp;gt;;
};

ReactDOM.render(
  &amp;lt;App /&amp;gt;,
  document.getElementById('root')
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l0kMy/btruUnei0Yc/yd6nbJU38wsxtdeW4MVcW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l0kMy/btruUnei0Yc/yd6nbJU38wsxtdeW4MVcW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l0kMy/btruUnei0Yc/yd6nbJU38wsxtdeW4MVcW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl0kMy%2FbtruUnei0Yc%2Fyd6nbJU38wsxtdeW4MVcW1%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;1029&quot; height=&quot;406&quot; data-origin-width=&quot;1029&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FrontEnd/JavsScript</category>
      <category>JSBin</category>
      <category>React</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/120</guid>
      <comments>https://hayjo.tistory.com/120#entry120comment</comments>
      <pubDate>Wed, 2 Mar 2022 21:21:41 +0900</pubDate>
    </item>
    <item>
      <title>Git 원격 저장소 URL 변경하기(token에서 ssh로 변경한 후 동기화)</title>
      <link>https://hayjo.tistory.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 github 계정 인증시 기존 authentication token 방식을 사용하다가 기한이 만료되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;push를 하려고 하니 아래처럼 아이디/비밀번호를 입력하라는 창이 뜬다.&lt;/p&gt;
&lt;pre id=&quot;code_1643724657052&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Username for 'https://github.com/somethingsomething.git':
Password for 'https://github.com/somethingsomething.git':&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;authentication token을 다시 발급받으려고 하는데 홈페이지가 리뉴얼되어 어디있는지 못 찾겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이참에 ssh를 사용하기로 하고 홈페이지를 참조해 public key 등록까지 마쳤는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reference: &lt;a href=&quot;https://docs.github.com/en/authentication/connecting-to-github-with-ssh&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.github.com/en/authentication/connecting-to-github-with-ssh&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;https로 클론 받았던 기존의 로컬 브랜치 인증까지 자동 업데이트되지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssh 방식으로 새로 클론 받으면 해결되지만, 로컬 브랜치에 작업 내역이 있다면 인증만 변경하고 싶을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 때 아래와 같이 입력해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1643724540386&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git remote set-url origin git@github.com:something&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨왼쪽은 아래처럼 clone시 나오는 ssh 부분을 입력하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bW0BUb/btrr9fYfHEz/mEhkzFyhARbc6tNzJbiKTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bW0BUb/btrr9fYfHEz/mEhkzFyhARbc6tNzJbiKTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bW0BUb/btrr9fYfHEz/mEhkzFyhARbc6tNzJbiKTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbW0BUb%2Fbtrr9fYfHEz%2FmEhkzFyhARbc6tNzJbiKTk%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;366&quot; height=&quot;138&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증문제 해결!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;References:&lt;br /&gt;&lt;a href=&quot;https://minsone.github.io/git/github-managing-remotes-changing-a-remotes-url&quot;&gt;https://minsone.github.io/git/github-managing-remotes-changing-a-remotes-url&lt;/a&gt;&lt;/p&gt;</description>
      <category>git</category>
      <category>git remote set-url</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/119</guid>
      <comments>https://hayjo.tistory.com/119#entry119comment</comments>
      <pubDate>Tue, 1 Feb 2022 23:18:23 +0900</pubDate>
    </item>
    <item>
      <title>바닐라코딩 10기 프렙, 부트캠프, 취직 후기</title>
      <link>https://hayjo.tistory.com/118</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://soodev20.tistory.com/19&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;친애하는 동기분의 멋진 후기&lt;/span&gt;&lt;/a&gt;를 보니 작년 이맘때쯤 프렙을 신청하면서 마음 졸였던 기억이 난다.&lt;br&gt;나 역시 바닐라코딩을 선택하기까지 고민을 꽤 했고 그때 최근 후기가 큰 도움이 됐기에, 내 경험도 누군가에게는 도움이 되지 않을까 싶어 적어본다.&lt;br&gt; &lt;br&gt; 내가 가장 궁금했던 것은 다들 힘들다는데 대체 얼마나 힘든지, 고비는 언제 오는지, 시간은 얼마나 투자해야 하는지, 어느 정도 수준이어야 진도를 따라갈 수 있는지, 정말 코스 중간에 떨어지는지 등 전체 플로우에 관한 것이 대부분이었다. 그래서 내가 그동안 어떤 과정을 거쳤는지를 시간 순으로 나열해보았다. 프렙 신청부터 부트캠프 시작까지의 기록은 개인 캘린더와 메일을 참조해서 복원했다. 부트캠프와 취업 과정은 상세하게 기록을 남겨두지 않아서(도저히 그럴 정신이 없었다..) 다소 부정확할 수 있다.&lt;br&gt; &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 01. 11. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 비전공자로서 독학 6개월차, 마음만큼 안 나가는 진도에 막막해져 교육프로그램을 조사하다가 바닐라코딩을 처음 알게 되었다.&lt;br&gt; 마침 프렙코스가 1월 29일까지 모집 중이었고 마감까지는 2주 정도의 시간이 있었다. 그동안 바닐라코딩의 커리큘럼과 후기와 아웃풋을 조사하고, 수료 가능성과 수료 후 취업 가능성을 따져보았다. 당시 취업률이 거의 100%에 육박했기 때문에 중도 탈락만 면하면 취업에는 문제 없을 것 같았다. 문제는 수료 가능성이었는데, 예상 시나리오는 이랬다.&lt;br&gt; &lt;br&gt;1. 프렙 합격 -&amp;gt; 프렙 수료 -&amp;gt; 부트캠프 합격 -&amp;gt; 부트캠프 수료 -&amp;gt; &lt;span style=&quot;color: #006dd7;&quot;&gt;취업 성공&lt;/span&gt; (이상적인 시나리오)&lt;br&gt;2. 프렙 합격 -&amp;gt; 프렙 수료 -&amp;gt; 부트캠프 합격 -&amp;gt; 부트캠프 수료 -&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;취업 실패&lt;/span&gt; (가능성 매우 낮음)&lt;br&gt;3. 프렙 합격 -&amp;gt; 프렙 수료 -&amp;gt; 부트캠프 합격 -&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;부트캠프 중도 탈락&lt;/span&gt; (가장 걱정됐던 시나리오. 시간과 비용을 상당히 지출한 이후 다른 교육프로그램을 찾아봐야 한다.)&lt;br&gt;4. 프렙 합격 -&amp;gt; 프렙 수료 -&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;부트캠프 탈락&lt;/span&gt; (다른 교육프로그램을 찾아봐야 하지만, 프렙 수강료는 상대적으로 비싸지 않기 때문에 리스크가 적다.)&lt;br&gt;5. 프렙 합격 -&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;프렙 중도 탈락&lt;/span&gt; (가능성 높지 않음)&lt;br&gt;6. 프렙 탈락 (다른 교육프로그램을 찾아봐야 하지만, 심사 기간도 비교적 짧고 비용도 지출되지 않으니 리스크가 적다.)&lt;br&gt; &lt;br&gt; 최악의 경우는 3번이었는데, 프렙을 신청할 당시에 고민할 부분은 아니라고 생각해서 일단 신청해보기로 했다. 수료한 지금 드는 생각이지만, 바로 이 탈락할지도 모른다는 압박감이 사람을 절박하게 만들어서 잠재력을 끝까지 끌어내는 것 같다. &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 01. 26. 화요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 프렙 신청서를 작성했다. 합격하고 싶은 마음이 커서 꽤 길게 작성하고 검토도 했다. 이후 &lt;a href=&quot;https://medium.com/vanilla-coding/%EB%8B%AC%EC%BD%A4%EC%82%B4%EB%B2%8C%ED%95%9C-%ED%94%84%EB%A0%99-10%EA%B8%B0-%EC%84%9C%EB%A5%98%EC%8B%AC%EC%82%AC-%EA%B7%B8-%EB%92%B7-%EC%9D%B4%EC%95%BC%EA%B8%B0-659adc51a0a6&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;우리 기수의 평균 자기소개 글자수가 굉장히 긴 편이었다는 사실&lt;/span&gt;&lt;/a&gt;을 알고 꽤 놀랐던 기억이 있다. 나름대로 노력을 했으나 평균이라니. 기대도 되고 긴장도 됐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 02. 01. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 프렙 어드미션 결과가 나왔다. 다행히도 합격이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 02. 04. 목요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 결제를 했다. 돈을 내고 나니 실감이 났다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 02. 06. 토요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 사전학습 가이드와 과제물이 날아왔다. 이때 나는 생활코딩 WEB3를 따라가고 있었고 조금만 더하면 AJAX까지 끝날 것 같은 상태였다. 사전학습 가이드 분량을 보았을 때 남은 기간 3주가 풀로 소요되지는 않을 것 같았기 때문에 사전학습을 조금 미루고 생활코딩을 계속했다. 지금 생각해보면 나같은 사람들이 많았던 것 같기도 하다. 메일에 사전학습가이드를 우선으로 진행해달라는 말도 있었으나 성실하게 지키진 못했다.  (그리고 곧 후회하게 된다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 02. 16. 화요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 금방 끝날 것 같았던 WEB3는 10일이 지나도록 끝나지 않았다. 더 이상 미룰 수 없어 사전학습 가이드를 시작했다. CSS가 생각보다 어려웠고 진도가 더뎌서 늦게 시작한 것을 엄청 후회했다. 입학 과제 제출이 26일이었으니 이때 시간이 10일 정도 있었던 셈이다.&lt;br&gt; 처음 며칠간은 하루에 3시간 정도씩 투자했는데 도무지 가망이 안 보여 제출 직전 일주일 동안 하루에 14시간씩 꼬박 작업해서 간신히 완료했다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pCwAe/btrp6InqhsY/qgSCgwK0LYXrbacD6Cg4EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pCwAe/btrp6InqhsY/qgSCgwK0LYXrbacD6Cg4EK/img.png&quot; data-alt=&quot; 어쩌다보니 굉장히 벼락치기가 된 학습시간.. &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pCwAe/btrp6InqhsY/qgSCgwK0LYXrbacD6Cg4EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpCwAe%2Fbtrp6InqhsY%2FqgSCgwK0LYXrbacD6Cg4EK%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;685&quot; height=&quot;430&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 어쩌다보니 굉장히 벼락치기가 된 학습시간.. &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 02. 26. 금요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 입학 과제를 마감 1시간 30분 전에 제출하고, 그날 오후에 개강 관련 정보를 전달 받았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 03. 01. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 프렙이 개강했다. 이때부터 매주 평일에는 컨텐츠를 학습한 뒤 과제를 구현하고, 주말에는 코드리뷰를 받는 패턴을 반복했다.&lt;br&gt; 코드리뷰가 무엇인지 굉장히 궁금했는데 말 그대로 내가 작성한 코드를 리뷰 받는 단계다. 글쓰기에서의 첨삭과 비슷하지만 토론의 성격도 있다. 내가 작성한 코드가 곧 결과물이기 때문에 거기에 오류가 없는지 점검하고, 더 나은 개선 방법이 있는지 살펴보고, 서로가 생각하는 바가 일치하는지 점검하는 과정이 필요한데, 코드리뷰가 이 부분을 도와준다. (그러다가 이슈를 발견하기도 하고, 여기서 시작해서 회의를 하기도 한다.) 대략 이런 식으로 댓글로 달린다. 예시는 리액트 레포지토리: &lt;a href=&quot;https://github.com/facebook/react/pull/22806#discussion_r754679914&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://github.com/facebook/react/pull/22806#discussion_r754679914&lt;/span&gt;&lt;/a&gt;&lt;br&gt; 코드리뷰는 개인적으로 프렙 과정 중에서 가장 좋아했던 부분이기도 했다. 초심자 입장에서는 챙긴다고 챙겨도 놓치는 부분이 많은데 그 부분들도 다시 보게 되고, 미처 몰랐던 사실을 알게 되기도 하고, 같은 로직을 여러 방식으로(대부분은 더 간결하고 심플한 방식을 제안 받았는데) 작성할 수 있다는 사실에 깜짝 놀라기도 했다.&lt;br&gt; 과제 수행에 필요했던 시간은 과제마다 달랐지만(기수별로 과제도 달라지는 것 같다) 보통은 10시간 ~ 20시간 이내였다. 내가 CSS에 그렇게 시간을 많이 쏟지 않아서인 것도 있다. (그리고 이후 꾸준히 후회를 하게 된다.) 나는 과제 자체보다는 다른 코드리뷰를 살펴보는 데 시간을 많이 썼다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 04. 09. 금요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 부트캠프 신청서를 작성했다. 오피스아워에서 챙겨주시기 때문에 일정을 까먹을 일은 없다. 전반적으로 프렙 신청서 작성과 비슷하다.&lt;br&gt; 오피스아워는 체크인 세션이라고 생각하면 될 것 같다. 학습 자료가 주어지고, 각자 자료를 자체적으로 학습한 뒤 어려움이 있는 부분은 질문을 하거나 추가로 설명해주시는 방식으로 피드백이 이루어지는데, 이를 위한 시간이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 04. 26. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 부트캠프 서류에 통과하고 코딩테스트 일정을 안내 받았다. 안내된 기간 안에만 응시하면 된다. 불안한 마음에 3일 정도 프로그래머스를 열심히 들여다보았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 04. 30. 금요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 프렙 코스 오피스아워 마지막 날이었다. 오피스아워가 끝나고 곧바로 부트캠프 어드미션 코딩테스트에 응시했다. 이날 오피스아워에서 모두 한마음으로 테스트 걱정을 했던 기억이 난다.&lt;br&gt; 난이도는 시간 내에 풀고 코드스타일 점검을 할 여유가 있을 만큼이었던 것 같다. 점검을 마치고 나니 시간이 별로 남지 않았다. 코드스타일이 중요한 요소고 &lt;s&gt;이후 부트캠프에서 깜짝 코드 대공개 시간을 가질 수도 있기 때문에 &lt;/s&gt; 가능한 깔끔하게 작성하는 게 좋다. 다른 사람이 읽을 코드라고 생각하면 아무래도 결과물이 더 잘 나오는 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 05. 03. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 부트캠프 어드미션 결과가 나왔고 다행히 합격이었다. (금액도 컸고 사무실에 가보고 싶은 마음도 있어서) 방문결제 예약을 잡았다. 나머지 기간 동안에는 그동안 학습했던 자료와 피드백들을 정리했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 05. 07. 금요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 방문결제를 했다. 친절하게 리마인더 메일을 주셔서 좋았다. 결제를 마치고 궁금했던 것들, 일과는 어떻게 진행되는지, 프렙 때와 같이 사전학습 가이드나 과제가 나오는지 같은 것들을 물어봤다. 사전 가이드가 있다는 말을 듣고 내심 안심했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 05. 10. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 사전 가이드가 날아왔다. 프렙 가이드와는 성격이 좀 달랐는데, 프렙은 사전학습이 신청 필수조건이었다면 부트캠프의 사전 가이드는 커리큘럼 예습에 가까웠다. 하지만 프렙 사전학습을 미뤘다가 크게 데인 경험이 있는 나는 이걸 무조건 완료하고 가야한다고 생각했다. 물론 이번에도 바로 공부를 시작했던 건 아니었고 , 기존 프렙 내용 정리도 좀 하고, 알고리즘도 풀고, 토이도 하다보니 시간이 가서 OT 이후에나 본격적으로 시작했다. 이 시점에 푼 알고리즘 문제가 어느 정도인지는 잘 모르겠지만 현재 기준 프로그래머스 기록은 이렇다. 부트캠프 어드미션 테스트를 준비하면서도 어느 정도 풀었는데 그때 정확히 몇 문제를 풀었는지는 잘 모르겠다. 부트캠프를 시작하고 따로 푼 문제는 10문제가 안 된다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rtlSR/btrp3JHp6CZ/LFw61FeUePkW1lN9zO0Qj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rtlSR/btrp3JHp6CZ/LFw61FeUePkW1lN9zO0Qj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rtlSR/btrp3JHp6CZ/LFw61FeUePkW1lN9zO0Qj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrtlSR%2Fbtrp3JHp6CZ%2FLFw61FeUePkW1lN9zO0Qj0%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;377&quot; height=&quot;157&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;157&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 05. 24. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 부트캠프 OT에 참석했다. 온라인 옵션을 같이 제공해주는 점이 인상적이었다. 온라인 신청 마감 직전까지 온라인으로 들을까 고민했다. 지금 생각하면 큰 차이는 없는듯.&lt;br&gt; 온라인으로만 보던 사람들을 실제로 만나니까 내가 정말 부트캠프를 하는구나 싶기도 하고, 미뤄온 사전 가이드가 걱정되기도 해서 이 이후부터는 준비를 시작했다. 초반에는 의욕이 앞서서 열심히 했지만 이후 또 시간이 모자라게 됐고 , 결국 컴퓨터 이론:프론트:백엔드 3파트를 65:30:5 정도의 비율로 학습하고 말았다. 어느 정도로 했냐고 하면, 컴퓨터 이론은 주어진 자료를 다 살펴보고 코드를 직접 작성해봤고, 튜토리얼을 제공하는 리액트는 튜토리얼을 구현해봤다. 백엔드는 자료 링크 클릭만 해본 수준. 백엔드는 어쩌나 걱정이 됐으나 생활코딩에서 구경은 해봤고 또 중간에 브레이크 기간이 있으니까 어떻게든 되겠지 싶었다.&lt;br&gt; 뒤늦게 알았지만 내가 준비를 적게 한 편은 아니었던 것 같다. 투자한 시간과 노력 자체는 괜찮았지만 밸런싱에 아쉬움이 남는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 06. 07. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 부트캠프 개강. 첫 번째 주제 컴퓨터 이론.&lt;br&gt; 어떻게 적응하지 걱정을 했으나 다들 열심히 하는 분위기여서 과제에만 집중할 수 있었다. 이 시기는 개강 전 벼락치기로 준비한 사전 공부 중 65%에 해당하는 파트여서 굉장한 어려움은 없었다. (굉장한 어려움은 이후에 겪게 된다.) 프렙 과제도 신기한 주제가 많았는데 부트캠프 과제는 정말 기대 이상의 퀄리티여서 구현하면서 몹시 재미있었다. 물론 도전하고 성취하는 재미가 있었다는 거지 쉬웠다는 뜻은 아니다. &lt;br&gt; 그리고 바닐라코딩 컴퓨터를 쓰면서 강제로 git에 익숙해지게 됐다. 실수로 다른 사람 이름으로 커밋하거나 브랜치 없이 작업해서 작업 내용을 날리거나, 가기 전에 푸시를 안 하는 실수를 몇 번 하다보니 git이 뭔지 조금씩 이해됐다. 실수할 때마다 아찔했지만 돌이켜보면 덕분에 git 이해도가 높아진 것 같다. 어차피 할 실수라면 파급효과가 적을 때 미리 해서 바로잡고 가는 편이 차라리 나은듯.&lt;br&gt; 부트캠프의 코드리뷰는 프렙 코드리뷰와 비슷했지만 더 섬세했고 그래서 더 무섭기도 했다. 구현 분량이 많아지니까 배울 것도 많지만, 동시에 실수할 부분도 많아진다. 과제 제출 전이면 항상 세미콜론이나 띄어쓰기 실수 없나 살펴보느라 아찔한 시간을 보내곤 했다. 신경을 쓴다고 썼지만 실수가 나왔고 덕분에 대비를 위해 각종 익스텐션과 툴에 익숙해질 수 있었다. &lt;br&gt; 자동완성을 믿고 무지성으로 코드를 작성하다가 리액트의 propTypes를 prototype으로 쓴 실수는 너무 충격적이어서 못 잊을 것 같다. 어쩐지 프롭 에러가 하나도 안 날 때 이상함을 느꼈어야 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 06. 28. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 첫 번째 테스트. 썩 효율적인 방법은 아니었지만 어떻게 해결하긴 했다. 이후 동기분들의 놀라운 코드를 보고 리팩토링을 했다. 나머지 브레이크 기간에는 3주차 과제를 리액트를 이용해 구현해보았다. 3주차 과제가 까다롭기도 했고 리액트도 걱정됐기에 시도해보았는데 나름대로 괜찮았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 07. 05. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 두 번째 주제 프론트엔드. 이때부터 예습 약발이 떨어져서 자료를 살펴보는 시간이 길어지기 시작했고, 과제를 화요일 오후에 간신히 시작하는 경우가 많아졌다. 과제가 해결되지 않아서 하루 온종일 그 생각만 하고있다가 멘토링 때도 그 얘기만 했던 기억이 난다.&lt;br&gt; 내 기수에는 퍼스널멘토링이 있었는데, 현업으로 뛰고 계신 선배분과 소프트한 이야기를 할 기회가 주어진다. 내가 궁금한 부분을 질문할 때도 있었지만 주로 현재 상태와 멘탈에 대한 이야기를 많이 했던 것 같다. 어떤 이야기를 나눌지 미리 더 잘 준비해갔다면 더 유익한 시간을 보낼 수 있었을 것 같아서 아쉬움이 남는다. 하지만 아쉬운 것과는 별개로 퍼스널멘토링이 있어서 굉장히 좋았는데, 이때쯤 내가 잘하고 있는 걸까? 하는 의문이 계속 들면서 정말 취업할 수 있을까에 대한 걱정이 스물스물 올라왔는데 멘토님께서 응원해주시고 자신감을 북돋아주셨기 때문이다. 무엇보다 생생한 수료 후 취업 사례를 계속 접하니까 불안함에 지치지 않고 계속할 수 있었던 것 같다. 바쁜 와중에 시간 내주신 멘토님께 새삼 감사한 마음이 든다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 07. 26. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 두 번째 테스트. 나름대로 최선을 다했다고 생각했지만 아쉬웠다. 현재 내 수준이 여기까지구나 싶어 더 열심히 해야겠다 마음을 다잡은 계기가 됐다. 생각해보면 그래도 이때는 쪽잠이라도 잤고 어쨌든 구현은 다 했으니 다행이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 07. 29. 목요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 동기분들과 테스트 리뷰를 했다. 나와 같은 시간을 투자했다고는 믿기지 않는 굉장한 결과물들을 보면서 자극도 받고 공부도 많이 됐다. 여기서 자극을 받아 나머지 브레이크 동안 백엔드 예습을 포기하고 리액트 과제를 복습했다. 막상 백엔드주차에 어려움을 겪어서 잘한 선택인지는 미지수지만, 생각해보면 이때 예습을 했어도 똑같았을 것 같기도 하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 08. 02. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 대망의 백엔드 시작. 개인적으로 백엔드 과제가 가장 어려웠는데, 로직이나 코드 작성 자체가 어렵다기보다는 환경설정과 각종 라이브러리 적용이 유독 어렵게 느껴졌다. 부트캠프 내내 지금은 공부하는 단계니까 최대한 라이브러리 없이 직접 해보자 스탠스를 취했는데, 그러다보니 정작 라이브러리를 도입해야 하는 시점에 난처해졌다. 다시 돌아간다면 이 부분은 좀 개선해보고 싶다.&lt;br&gt; 이때 유독 밤을 많이 샜다. 돌아보면 전체적으로 밤을 많이 새운 편은 아니었지만 이때가 가장 시간적으로 빠듯했다. 매일은 아니었고, 두번째, 세번째 과제하면서 하루이틀씩은 지새운 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 08. 23. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 마지막 테스트. 정말 어려웠던 기억이 난다. 처음으로 요구사항을 다 구현하지 못한 과제여서 자괴감도 많이 들었다. 과제를 받고 제출하기까지 잠도 거의 못 잤다. 이때 받은 스트레스가 굉장했는데, 지금 생각해보면 제한시간이 길지 않았던 게 차라리 다행이었다. 수료 후 인터뷰 때 포기하고 싶었던 순간은 없었냐고 질문 받았을 때 잠깐 이 시간이 떠올랐지만, 당시에는 포기를 고민할 시간조차 없었어서 결국은 없다고 대답했던 기억이 난다. 마지막에 부족한 결과물과 함께 (혹시 모를 중도 탈락 사태에 대비한) 계좌번호를 제출하면서 온갖 시나리오를 다 생각했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 08. 26. 목요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 동기분들과 테스트 리뷰를 했다. 나한테는 불가능한 작업처럼 보였는데, 매우 훌륭하게 완성된 동기분들의 결과물을 보면서 느끼는 바가 많다. 스스로를 너무 과대평가하지 말고, 혼자서 모든 걸 다 하려고 하지도 말아야겠다는 생각이 이때 들었다. 시간이 없거나 어려움이 있다면 인정하고 도움(테스트는 혼자 해결해야하니까 이때는 라이브러리 적용 정도가 되겠지만)을 구하면 처음에는 불가능해보여도 완성할 수 있구나를 절실하게 깨달았던 것 같다.&lt;br&gt; 그러고 보면 사실 프로그래밍 자체가 언제나 거인의 어깨 위에서 이루어지는 작업이기도 하다. 내가 기계어나 어셈블리로 코딩하지 않고 자바스크립트로 코딩하는 것부터 이미 완성된 다른 사람들의 작업을 이용하는 것. 앞으로도 내가 실제로 하게 될 작업도 무언가를 밑바닥에서부터 만드는 것이 아니라, 이미 어느 정도 완성되어 있는 부분들을 어떻게 효율적으로 조립할지에 가깝다는 생각이 들었다. 처음 프로그래밍을 배우기 시작했을 때의 마음으로 돌아간 기분이었다. 어떻게 만드는지보다는 일단 만드는 게 중요하다. 잘 만드는 건 일단 만들 수 있게 되고 나서 생각해볼 일. 가장 못한 테스트였지만 동시에 가장 배운 바가 많아서 개인적으로 기억에 남는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 08. 27. 금요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 마지막 테스트 통과 메일을 받았다. 심지어 잘했다..? 메일이 잘못 온 걸까 잠시 의심했지만 켄님은 빈말을 하는 분이 아니기 때문에 믿기로 했다. 완성도보다는 다른 부분을 좋게 봐주신 것 같은데, 어쨌든 운좋게 살아남았다.&lt;br&gt; 테스트에서 정말 떨어지는가 하면 정말로 떨어지기도 한다... 평가 기준은 정확히는 모르겠지만 테스트 결과를 비롯해 복합적인 요인이 고려되는 것 같다. 적어도 요구사항을 완벽하게 구현하지 못한 내가 통과했으니 꼭 완성을 해야만 통과하는 게 아닌 건 확실하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 08. 30. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 팀 프로젝트 시작일. 기획부터 개발 후 배포까지 팀으로 진행했다. 팀이라는 생각에 든든했고 덕분에 혼자 했으면 못 했을 규모로 프로젝트를 진행할 수 있었다. 매일매일 스크럼 미팅을 했고 쌓여있는 작업들을 처리하다보니 시간이 정신없이 지나갔다. 내가 오늘 작업을 제때 완성해줘야 내일 동료의 작업에 지장이 없다고 생각하니 책임감이 막대해져서 스스로를 몰아붙이기도 했다. 다들 그랬던 것 같지만 못 먹고 못 자서 이때 체중이 많이 줄었다. 나도 모르게 예민해지려는 시점에 붙잡아준 동료가 고마웠고, 때때로는 내가 지지해줄 수 있어서 다행이었다.&lt;br&gt; 또 개발 도중 기획 당시에는 생각 못 했던 이슈를 발견해 중간에 추가 및 수정 작업이 대거 추가됐고 일정이 빠듯해지기도 했다. 하지만 더 나은 해결책을 찾기 위해 머리를 맞대고 토론하면서 얻었던 에너지가 굉장해서 무사히(?) 완성할 수 있었다. 큰 산을 넘고 동료들과 노을이 질 무렵에 산책하면서 프로젝트를 회고했던 순간은 오래오래 기억에 남을 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 09. 18. 토요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 1차 프로젝트 발표날이었다. 다들 대단했다. 다음주가 추석이었고 추석을 쇠고 나면 개인 프로젝트였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 09. 27. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 개인 프로젝트 시작일. 팀 프로젝트 때 규모가 커서 고생했던 경험을 살려 이번에는 최소한의 기능만 가지고 시작했다. 가볍게 출발하니 기획도 셋업도 훨씬 수월했다. 초반 진도가 예상보다 빨리 나가서 좋았으나 어느 정도 완성하고보니 기획이 가벼워도 너무 가벼워서 문제가 생겼다. 게임을 구상했는데 이걸 만든 나만 플레이할 수 있는 그런 상황이 온 것 . 결국 작업 도중에 기획을 변경하고 기능과 튜토리얼을 추가했다. 맨처음 기획과 비교하면 작업량이 2배 정도 늘어난 것 같다. 덕분에 일정은 타이트해졌지만 보다 나은 결과물을 뽑아낼 수 있어서 다행이라고 생각한다.&lt;br&gt; 코로나 상황이라 개인 프로젝트는 거의 재택으로 진행했는데, 나는 집이 멀어 통근 시간을 아껴서 좋았다. 이후에 이야기를 나눠보니 고정 스케쥴이 없으니 스트레스나 피로에 취약해질 수 있다는 단점도 있는 것 같다. 개인 프로젝트라서 혼자 모든 걸 책임져야 하는데, 집에 있다보니까 고립된 느낌을 받기도 쉽다. 장기전일 때는 언제나 그렇지만, 재택근무 시에는 특히 컨디션 조절이 중요한 듯. 켄님이 봐주시는 데일리 체크인이 있어 그나마 중심을 잡지 않았나 싶다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 10. 23. 토요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 개인 프로젝트 스탠드업. 원래는 외부인들도 오실 예정이었으나 코로나 때문에 간소화되었다. 발표해야 하는 입장에서는 긴장이 덜 돼서 다행이었다. &lt;br&gt; 다들 작업물 퀄리티가 대단해서 감동적이었다. 부트캠프 마지막 시간에 어드미션 코딩테스트 코드를 다시 보면서 발전사를 이야기하는 시간을 잠깐 가졌었는데, 그때도 찡했지만 프로젝트 발표는 정말 감회가 새로웠다. 모두 이만큼 성장했다는 게 실감이 나서인 것 같다. Promise 이해하느라 골머리 앓은 게 불과 몇 달 전인데 이런 걸 만들 수 있게 되다니.&lt;br&gt; 가까스로 완성한 내 프로젝트에는 아직 부족한 게 많지만, 프렙을 시작할 당시의 나는 상상도 못했던 결과물이라는 생각을 하니 감개무량했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 11. 01. 월요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 본격적인 구직 시작. 프로젝트 발표를 마무리하고, 일주일 간의 준비기간을 갖고 구직에 돌입했다. 구직 첫주에 첫 지원을 하고, 2주차부터 면접을 보기 시작했다. 프로세스가 짧은 곳은 면접 한 번으로 결과가 나오기도 하고, 길게 이어지는 곳은 몇 단계를 거치느라 5주 이상 소요되는 곳도 있었다.&lt;br&gt; 개인적으로 이때가 제일 힘들었다. 나는 좋게 말하면 신중하고 겸손하고, 나쁘게 말하면 의심이 많고 스스로를 잘 안 믿는 편이다. 이 특성이 부트캠프 동안에는 실수를 (그나마) 덜 하고 더 나은 코드를 고민하는 긍정적인 방향으로 작용했는데, 구직 활동이란 스스로를 믿고 자신감 있게 팔아야하는(?) 과정이다보니 정말 쉽지 않았다. 스스로를 의심하는 습관 덕에 자신감은 바닥까지 떨어지고 서류 지원부터 계속 망설이게 되고 만 것이다. 이때 멘토님께 들었던 조언 중에 기억에 남는 조언이 있다.&lt;br&gt; 결국 면접이란 같이 일하고 싶은 사람을 뽑는 과정이다. 이 사람의 실력이 절대적인 것도 아니고, 질문에 대답을 하고 못하고가 절대적으로 중요한 것도 아니다. 결국은 같이 일하고 싶은 느낌을 주면 될 뿐이다.&lt;br&gt; 이 조언을 듣고 이런저런 생각이 많이 들었다. 나는 이상적인 지원자란 게 존재한다고 생각했는데, 사실은 이상형처럼 막연한 어떤 개념에 불과할 수도 있었다. 꼭 모든 조건을 갖춰야만 채용하는 게 아닌 것이다. 아하 모먼트였다. 하루 아침에 성향을 바꾸는 건 어려웠지만 이후 마음만은 훨씬 편안해졌다. 또 중요한 건 면접도 보다보면 는다는 것. 실제로 후반부에 진행한 면접이 결과가 더 좋기도 했다. 그동안 켄님과 멘토님의 피드백을 받고 개선된 것도 있고, 스스로 경험이 쌓인 것도 있고, 좋은 결과를 얻은 동기분들에게 자극을 받은 것도 있고, 복합적으로 성장했다고 생각한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2021. 12. 17. 금요일.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 구직 7주차 금요일에 가고 싶던 기업에서 오퍼를 받고 구직 활동을 종료했다. 쉽지 않았지만 어쨌든 성공했다. 야호. 오퍼를 받고 소리를 지른 건 공공연한 비밀이 되었다. &lt;br&gt; 프렙 신청서를 작성하던 당시 코스 수료 후 계획을 묻는 질문이 있었다. 나는 이렇게 대답했다.&lt;br&gt; 부트캠프 코스에 도전할 계획입니다. 계획대로 무사히 수료해서 올해 안에 개발자로 취업에 성공하면 더없이 기쁠 것 같네요.&lt;br&gt; 그리고 실제로 더없이 기쁘게 되었다.&lt;br&gt; &lt;br&gt; &lt;br&gt; 사실 취업은 시작일 뿐이고 이제부터 새로운 여정을 또 시작해야한다. 그래도 이제는 함께 이 과정을 수료하고 서로 힘이 돼주는 동기분들과의 네트워크가 있고, 어려운 상황이라면 조언을 구할 수 있는 든든한 켄님과 멘토님들이 계시니까 이전처럼 막막하지는 않을 것 같다. 나를 믿어준 새로운 회사에서 잘해낼 수 있도록 또 최선을 다해봐야겠다. 감사했습니다, 그리고 앞으로도 잘 부탁드립니다 &lt;/p&gt;</description>
      <category>FrontEnd/Bootcamp</category>
      <category>frontend</category>
      <category>VanillaCoding</category>
      <category>바닐라코딩</category>
      <category>부트캠프</category>
      <category>프론트엔드</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/118</guid>
      <comments>https://hayjo.tistory.com/118#entry118comment</comments>
      <pubDate>Sun, 9 Jan 2022 22:45:40 +0900</pubDate>
    </item>
    <item>
      <title>GET google api access token (with gapi, no firebase)</title>
      <link>https://hayjo.tistory.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;*** 이 포스팅은 작성자의 경험을 바탕으로 작성되었으며, 사실과 다른 부분이 있을 수 있습니다 ***&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 React, 서버 Node.js(Express)를 사용합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google은 현재 인증 방법 업데이트 중.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인은 &lt;span&gt;새로운 라이브러리 &lt;a href=&quot;https://accounts.google.com/gsi/client&quot;&gt;https://accounts.google.com/gsi/client&lt;/a&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;(문서: &lt;a href=&quot;https://developers.google.com/identity/gsi/web/guides/client-library&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developers.google.com/identity/gsi/web/guides/client-library&lt;/a&gt;)&lt;/span&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google api 접근을 위한 OAuth 2.0은 기존 라이브러리 &lt;span&gt;&lt;a href=&quot;https://apis.google.com/js/api.js&quot;&gt;https://apis.google.com/js/api.js&lt;/a&gt;&lt;/span&gt; 를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(문서: &lt;a href=&quot;https://developers.google.com/identity/protocols/oauth2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developers.google.com/identity/protocols/oauth2&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;로그인 방식이 업데이트되면서 scope 추가 부분이 없어져 권한 허용 요청을 별도로 처리해야 하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0 부분은 아직 업데이트가 되지 않아서 기존 라이브러리를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 라이브러리를 사용하면서 access token을 재발급받고 싶은 경우 아래처럼 접근할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1632419984934&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;await window.gapi.get.getAuthInstance().currentUser.get().reloadAuthResponse();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react를 사용하면서 기존 라이브러리를 사용해서 OAuth를 처리한다면 npm의 &lt;a href=&quot;https://www.npmjs.com/package/react-google-login&quot;&gt;react-google-login&lt;/a&gt; 패키지를 사용하면 간편하다. 다만 access token 재발급 가능 유무를 확인하지 못해 실제로 사용해보지는 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firebase를 사용하면 간단하게 google api accessToken을 획득할 수 있으나 유효기간이 1시간이라는 단점이 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저의 수면 및 운동 정보를 분석해주는 웹 애플리케이션 프로젝트를 진행하면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google 로그인과 google fitness api 접근 권한을 처리할 필요가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;firebase로는 한계가 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 firebase 로그인 + addScope 조합을 시도해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/auth/web/google-signin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;firebase를 이용해서 google 로그인을 처리&lt;/a&gt;하면 addScope만으로 accessToken을 획득할 수 있기 때문.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(firebase@8.9.1 기준) 먼저 provider 설정 시에 해당 부분을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1632387585440&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const provider = new firebase.auth.GoogleAuthProvider();

provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
firebase.auth().signInWithRedirect(provider);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 getRedirectResult에서 아래와 같은 형태로 credential을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1632390145318&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { user, credential } = await firebase.auth().getRedirectResult();

console.log(credential);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 firebase에서 제공하는 google api accessToken은 유효기간이 1시간이며, 별도의 refreshToken을 제공하지 않는다는 것.&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;firebase에서 제공하는 토큰에는 3종류가 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;idToken&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저의 기본적인 정보(이름, 이메일, 프로필 사진 등)를 얻을 수 있는 토큰&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;refreshToken&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 idToken을 갱신하기 위한 토큰. firebase를 사용 중이고 유저가 로그인된 상태라면, refreshToken을 이용할 필요 없이 getIdToken(true)로 처리하면 갱신된 idToken을 획득할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;accessToken&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google api 접근을 위한 토큰. 유효기간은 1시간. 로그인 직후에만 얻을 수 있다. 갱신 불가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*토큰 관련해서는 이쪽 스택오버플로우 답변에서 보다 상세한 정보를 확인할 수 있다: &lt;a href=&quot;https://stackoverflow.com/questions/40838154/retrieve-google-access-token-after-authenticated-using-firebase-authentication&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;retrieve-google-access-token-after-authenticated-using-firebase-authentication&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;처음에는 이름을 보고 access-refresh가 짝일 것이라고 생각해서 해당 엔드포인트를 찾아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔드포인트: &lt;a href=&quot;https://developers.google.com/identity/toolkit/reference/securetoken/rest/v1/token&quot;&gt;https://developers.google.com/identity/toolkit/reference/securetoken/rest/v1/token&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;access_token이라는 말에 희망을 가졌으나, 실제로 접근을 시도해보면 반환해주는 access_token는 firebase의 idToken이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;769&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lTDhQ/btrfDhgNfC8/t8IoSb9735Xhe3viQZFJW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lTDhQ/btrfDhgNfC8/t8IoSb9735Xhe3viQZFJW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lTDhQ/btrfDhgNfC8/t8IoSb9735Xhe3viQZFJW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlTDhQ%2FbtrfDhgNfC8%2Ft8IoSb9735Xhe3viQZFJW0%2Fimg.png&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;769&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #232629;&quot;&gt;또 idToken을 이용해 signInWithCredential으로 재로그인하는 아래와 같은 방법도 있었지만, 내 경우에는 이렇게 했을 때 새로운 accessToken이 나오지 않았다. 해당 필드가 생략된 채 반환된다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1632391761013&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;How to Refresh Google AccessToken in Firebase? #AskFirebase&quot; data-og-description=&quot;I am attempting to build a web app that will be integrated with and installed into Google Drive. The user will be able to create and share my app's files in their drive. I am trying to write it us...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/41387535/how-to-refresh-google-accesstoken-in-firebase-askfirebase&quot; data-og-url=&quot;https://stackoverflow.com/questions/41387535/how-to-refresh-google-accesstoken-in-firebase-askfirebase&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bXJdvv/hyLI7D4ayH/Sk8ZnVgJVrHLOyXnd25zw1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/41387535/how-to-refresh-google-accesstoken-in-firebase-askfirebase&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/41387535/how-to-refresh-google-accesstoken-in-firebase-askfirebase&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bXJdvv/hyLI7D4ayH/Sk8ZnVgJVrHLOyXnd25zw1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to Refresh Google AccessToken in Firebase? #AskFirebase&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I am attempting to build a web app that will be integrated with and installed into Google Drive. The user will be able to create and share my app's files in their drive. I am trying to write it us...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한참을 헤맨 끝에 아래의 답변을 확인한다.&lt;/p&gt;
&lt;figure id=&quot;og_1632387722880&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;How to get providers access tokens from Firebase authenticated user?&quot; data-og-description=&quot;I am using Firebase to authenticate with GitHub, Twitter and Facebook and I know I can get the provider access token upon authenticating like so firebase.auth().signInWithPopup(provider).then(func...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/45001120/how-to-get-providers-access-tokens-from-firebase-authenticated-user&quot; data-og-url=&quot;https://stackoverflow.com/questions/45001120/how-to-get-providers-access-tokens-from-firebase-authenticated-user&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yiAUu/hyLHClldWS/kErAntJK8i97ozt9GgdOLk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/45001120/how-to-get-providers-access-tokens-from-firebase-authenticated-user&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/45001120/how-to-get-providers-access-tokens-from-firebase-authenticated-user&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yiAUu/hyLHClldWS/kErAntJK8i97ozt9GgdOLk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to get providers access tokens from Firebase authenticated user?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I am using Firebase to authenticate with GitHub, Twitter and Facebook and I know I can get the provider access token upon authenticating like so firebase.auth().signInWithPopup(provider).then(func...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;Unfortunately, Firebase Auth does not manage OAuth access tokens for users. After OAuth sign in via popup/redirect, the access token is returned but no new access tokens will be returned afterwards.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;안타깝게도 Firebase Auth는 유저의 OAuth access token을 관리하지 않는다. 팝업이나 리다이렉트를 통해 OAuth 로그인이 완료된 직후에만 access token이 반환되고, 이후에는 새로운 access token이 반환되지 않는다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #232629;&quot;&gt;수동동기화 기능을 구현하려면 refresh token이나 new access token이 필수적이기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #232629;&quot;&gt;firebase를 포기하고 google OAuth를 직접 연결하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sign in With Google&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #232629;&quot;&gt;google identity 페이지에 가보니 기존 방식인 google Sign-in은 더 이상 업데이트되지 않을 것이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #232629;&quot;&gt;대신 새로 도입된 Sign In With Google을 사용하라고 되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;265&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o1AIf/btrfFFoaOFt/ErPzx62KbT3H06Naz1CWCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o1AIf/btrfFFoaOFt/ErPzx62KbT3H06Naz1CWCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o1AIf/btrfFFoaOFt/ErPzx62KbT3H06Naz1CWCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo1AIf%2FbtrfFFoaOFt%2FErPzx62KbT3H06Naz1CWCK%2Fimg.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;265&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 api 라이브러리도 아래 라이브러리로 대체되었음이 명시되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;274&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mwNpC/btrfPirs4bW/bVcCmPYrmkyR5TErtpbYcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mwNpC/btrfPirs4bW/bVcCmPYrmkyR5TErtpbYcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mwNpC/btrfPirs4bW/bVcCmPYrmkyR5TErtpbYcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmwNpC%2FbtrfPirs4bW%2FbVcCmPYrmkyR5TErtpbYcK%2Fimg.png&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;274&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리를 위해서 &lt;a href=&quot;https://developers.google.com/identity/gsi/web/guides/display-button#javascript&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;구글에서 제공한 예시 코드&lt;/a&gt;와 &lt;a href=&quot;https://stackoverflow.com/questions/34424845/adding-script-tag-to-react-jsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;리액트에서 sciprt 태그를 처리하는 방법&lt;/a&gt;을 참고해서 아래와 같이 함수를 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1632389196746&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function initGoogle() {
  window.google.accounts.id.initialize({
    client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
    callback,
  });

  window.google.accounts.id.renderButton(
    document.getElementById(&quot;google-login-button&quot;),
    { theme: &quot;outline&quot;, size: &quot;large&quot; },
  );

  window.google.accounts.id.prompt();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 스크립트 로드가 완료되면 처리하도록 useEffect에서 핸들러로 달아주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1632389211943&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const script = document.createElement(&quot;script&quot;);

  script.src = &quot;https://accounts.google.com/gsi/client&quot;;
  script.async = true;
  script.addEventListener(&quot;load&quot;, initGoogle);

  document.body.appendChild(script);

  return () =&amp;gt; {
    script.removeEventListener(&quot;load&quot;, initGoogle);
    document.body.removeChild(script);
  };
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #232629;&quot;&gt;문제는 새로 도입된 Sign In With Google에는 addScope에 해당하는 기능이 없다는 것.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1632388384162&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Google API 클라이언트 ID 가져오기 &amp;nbsp;|&amp;nbsp; Sign In With Google &amp;nbsp;|&amp;nbsp; Google Developers&quot; data-og-description=&quot;경고 : 이 데이터는 Google 사용자 데이터 정책에 따라 제공됩니다. 정책을 검토하고 준수하십시오. 그렇게하지 않으면 프로젝트 또는 계정이 정지 될 수 있습니다. 이 페이지는 Cloud Translation API를&quot; data-og-host=&quot;developers.google.com&quot; data-og-source-url=&quot;https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid&quot; data-og-url=&quot;https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cyJN9J/hyLHJY47Cx/DfS9HWoydTR8nDuTDQT850/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cyJN9J/hyLHJY47Cx/DfS9HWoydTR8nDuTDQT850/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Google API 클라이언트 ID 가져오기 &amp;nbsp;|&amp;nbsp; Sign In With Google &amp;nbsp;|&amp;nbsp; Google Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;경고 : 이 데이터는 Google 사용자 데이터 정책에 따라 제공됩니다. 정책을 검토하고 준수하십시오. 그렇게하지 않으면 프로젝트 또는 계정이 정지 될 수 있습니다. 이 페이지는 Cloud Translation API를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;137&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRWcnf/btrfQmtpOUX/dwAxbmhBgdB6k5RWgZOCr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRWcnf/btrfQmtpOUX/dwAxbmhBgdB6k5RWgZOCr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRWcnf/btrfQmtpOUX/dwAxbmhBgdB6k5RWgZOCr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRWcnf%2FbtrfQmtpOUX%2FdwAxbmhBgdB6k5RWgZOCr0%2Fimg.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;137&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보 보호차원에서 바람직한 방향이긴 하다. 권한을 추가하기 위해서 문서를 더 읽어보니, &lt;a href=&quot;https://developers.google.com/identity/gsi/web/guides/migration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Migrating from Google Sign-In&lt;/a&gt; 필드가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 사용하던 access token과 scope가 모두 ID token으로 대체되었다고.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;436&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lIJ1s/btrfO5eTcWu/Swt4721QRoKCBChF0bGFm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lIJ1s/btrfO5eTcWu/Swt4721QRoKCBChF0bGFm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lIJ1s/btrfO5eTcWu/Swt4721QRoKCBChF0bGFm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlIJ1s%2FbtrfO5eTcWu%2FSwt4721QRoKCBChF0bGFm1%2Fimg.png&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;436&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 사용했던 window.google.accounts 객체에는 id와 oauth2라는 속성이 있다. oauth2를 이용해서 ID token에 권한 옵션을 추가하려고 시도해보니&lt;/p&gt;
&lt;pre id=&quot;code_1632392901970&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;window.google.accounts.oauth2.initTokenClient({
  client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 실험적인 기능이라는 에러가 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;34&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s60TA/btrfG1YTDM1/KKrnGsRThcKUNehwgz1MQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s60TA/btrfG1YTDM1/KKrnGsRThcKUNehwgz1MQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s60TA/btrfG1YTDM1/KKrnGsRThcKUNehwgz1MQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs60TA%2FbtrfG1YTDM1%2FKKrnGsRThcKUNehwgz1MQK%2Fimg.png&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;34&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 방법은 기존 OAuth 2.0으로 인증하는 것 뿐. 확인을 위해 왼쪽의 &lt;a href=&quot;https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OAuth 2.0&lt;/a&gt;으로 가보니&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;84&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buxwbq/btrfG3CklJj/Ni2DkysHXteKkOGCDXeep0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buxwbq/btrfG3CklJj/Ni2DkysHXteKkOGCDXeep0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buxwbq/btrfG3CklJj/Ni2DkysHXteKkOGCDXeep0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuxwbq%2FbtrfG3CklJj%2FNi2DkysHXteKkOGCDXeep0%2Fimg.png&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;84&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 스크립트 태그로 라이브러리를 호출하는 방식이 설명되어 있고, 아직 업데이트가 되지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;174&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq1koG/btrfKBFnoCK/uTmGXDHPklcNWbgJvmU6Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq1koG/btrfKBFnoCK/uTmGXDHPklcNWbgJvmU6Sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq1koG/btrfKBFnoCK/uTmGXDHPklcNWbgJvmU6Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq1koG%2FbtrfKBFnoCK%2FuTmGXDHPklcNWbgJvmU6Sk%2Fimg.png&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;174&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 처리를 위해 기존의 gapi를 그대로 써야한다면 로그인에만 새로운 방식을 도입할 이유도 없어보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Google OAuth 2.0&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 플로우에는 옵션이 여러 가지가 있는데, 크게는 Server Side(HTTP/REST)와 Client Side로 나눌 수 있다.&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;h4 data-ke-size=&quot;size20&quot;&gt;1. Server Side&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/identity/protocols/oauth2/web-server#httprest_1&quot;&gt;https://developers.google.com/identity/protocols/oauth2/web-server#httprest_1&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- access_type을 offline으로 지정하면 이후 refresh token을 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- offline으로 지정시 반환되는 authorization_code를 token으로 바꾸기 위해서는 client_secret이 필요하다. (이 특성 때문에 환경변수가 노출되는 클라이언트 단에서는 사용할 수 없다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- redirect_uri가 구글 콘솔에 등록되어 있어야 하고, token 교환 요청을 보내는 uri는 처음 인증과정에서 지정했던 것과 같은 redirect_uri여야 한다. 그렇지 않은 경우, 둘다 구글 콘솔에 등록된 uri더라도 mismatch_error가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Client Side&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 구글에서 제공하는 라이브러리(script tag 형태)가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 리액트에서 사용할 경우 public/index.html에 추가하거나 별도의 로드 작업이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 스크립트 로드가 완료되면 window.&lt;span&gt;gapi로 라이브러리 사용이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- 로그인이 완료되면 &lt;span style=&quot;color: #006dd7;&quot;&gt;gapi&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;.auth2.getAuthInstance&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;().currentUser.get().reloadAuthResponse()&lt;/span&gt;로 전역에서 토큰을 포함한 유저 정보에 접근할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;span&gt;reloadAuthResponse&lt;/span&gt; 메소드를 확인하지 못해서, access token 갱신을 위해서는 무조건 refresh token을 발급받아야 한다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위에서 언급한 redirect_uri와 client_secret 문제로 클라이언트 단에서 직접 인증을 처리할 수 없어, 고민 끝에 다음 두 가지 방법을 떠올렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 로그인만 서버사이드 렌더링을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 유저가 로그인 요청을 보내면 클라이언트에서 redirect_uri를 서버단으로 지정, 서버에서 인증 및 토큰 작업을 모두 처리(refresh token은 별도의 저장공간에 저장)한 후 클라이언트로 리다이렉트한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘다 탐탁지 않아 혹시 다른 방법이 있을까 검색하다가 이런 이슈를 발견한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stackoverflow: &lt;a href=&quot;https://stackoverflow.com/questions/32150845/how-to-refresh-expired-google-sign-in-logins/37896285#37896285&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;how-to-refresh-expired-google-sign-in-logins&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1632421713382&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;No way of obtaining refresh _token from gapi.auth2.authorize api from client side &amp;middot; Issue #643 &amp;middot; google/google-api-javascript-&quot; data-og-description=&quot;Hi, Am using gapi.auth2.authorize api to allow multiple user accounts being signed in. but it's callback function doesn't provide a refresh_token. Requirement is to keep the users until the...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/google/google-api-javascript-client/issues/643#issuecomment-642350903%EF%BB%BF&quot; data-og-url=&quot;https://github.com/google/google-api-javascript-client/issues/643&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/biDM4o/hyLI4OkvTk/3sKcHIkn99WDYzKJOuFXqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=995_142_1040_190&quot;&gt;&lt;a href=&quot;https://github.com/google/google-api-javascript-client/issues/643#issuecomment-642350903%EF%BB%BF&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/google/google-api-javascript-client/issues/643#issuecomment-642350903%EF%BB%BF&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/biDM4o/hyLI4OkvTk/3sKcHIkn99WDYzKJOuFXqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=995_142_1040_190');&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;No way of obtaining refresh _token from gapi.auth2.authorize api from client side &amp;middot; Issue #643 &amp;middot; google/google-api-javascript-&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hi, Am using gapi.auth2.authorize api to allow multiple user accounts being signed in. but it's callback function doesn't provide a refresh_token. Requirement is to keep the users until the...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰을 재발급 받고 싶다면 아래 메소드를 이용하면 된다고.&lt;/p&gt;
&lt;pre id=&quot;code_1632421782381&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;await window.gapi.get.getAuthInstance().currentUser.get().reloadAuthResponse();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 목적이었던 수동동기화 기능은 유저가 로그인 되어있는 상황에만 필요하므로, 이 메소드면 충분하다는 결론을 내리고 이 방법대로 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gapi를 사용한다면 서버단에서 idToken을 verify 할 때도 google-auth-libraray를 사용해야 한다. 사용법은 firebase-admin과 거의 유사하다.&lt;/p&gt;
&lt;pre id=&quot;code_1632422007815&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const { OAuth2Client } = require(&quot;google-auth-library&quot;);
 
 const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
 
 const loginTicket = await googleClient.verifyIdToken({
   idToken, // 클라이언트에서 getAuthResponse, reloadAuthResponse 등으로 얻을 수 있다.
   audience: process.env.GOOGLE_CLIENT_ID,
 });&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방법은 현재 작업중인 것으로 보이는 window.google.accounts.oauth2가 정식으로 출범하기 전까지만 유효할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 방법이 나온다면 다시 마이그레이션을 거쳐야할 듯.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그전에 firebase에서 access token을 refresh 하는 방법을 제공해주지 않을까 하는 기대를 해본다!&lt;/p&gt;</description>
      <category>FrontEnd/Bootcamp</category>
      <category>firebaseaccesstoken</category>
      <category>firebasegoogle</category>
      <category>gapi</category>
      <category>googleaccesstoken</category>
      <category>GoogleAPI</category>
      <category>googlelogin</category>
      <category>googleOauth</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/117</guid>
      <comments>https://hayjo.tistory.com/117#entry117comment</comments>
      <pubDate>Fri, 24 Sep 2021 03:40:17 +0900</pubDate>
    </item>
    <item>
      <title>Base N(n진법)으로 변환하기</title>
      <link>https://hayjo.tistory.com/116</link>
      <description>&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://hayjo.github.io/Visualization-for-BaseN/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hayjo.github.io/Visualization-for-BaseN/&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Intro&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 풀이를 하다보면 n진법을 다뤄야 할 때가 있는데, 가끔씩 필요하다보니 그새 원리를 잊어버릴 때가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 찾아보는 것도 일이라서 이참에 변환해주는 원리를 직접 표현해두기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기본 로직&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스로 삼은 것은 이쪽 자료. 자세한 설명은 &lt;a href=&quot;https://terms.naver.com/entry.naver?docId=3572374&amp;amp;cid=58944&amp;amp;categoryId=58970&quot;&gt;네이버 지식백과 진법 변환 페이지&lt;/a&gt;에 잘 나와있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;138&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lv4yF/btq4ZgYAvOz/JQAwORiynDWAeWqukhk5M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lv4yF/btq4ZgYAvOz/JQAwORiynDWAeWqukhk5M0/img.png&quot; data-alt=&quot;출처: 소수의 진법 변환(https://terms.naver.com/entry.naver?docId=3572374&amp;amp;amp;amp;cid=58944&amp;amp;amp;amp;categoryId=58970)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lv4yF/btq4ZgYAvOz/JQAwORiynDWAeWqukhk5M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLv4yF%2Fbtq4ZgYAvOz%2FJQAwORiynDWAeWqukhk5M0%2Fimg.png&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;138&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 소수의 진법 변환(https://terms.naver.com/entry.naver?docId=3572374&amp;amp;cid=58944&amp;amp;categoryId=58970)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽의 2 * (2&amp;nbsp; * (2 * (2 * 0 + 1) + 1) + 0) + 1 수식을 단계별로 표현해주기로 한다.&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;(12) + 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2 * (6) + 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2 * (2 * 3) + 0) + 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2 * (2 * ((2) + 1) + 0) + 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2 * (2 * (2 * (2 * (0) + 1) + 1) + 0) + 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지가 있으면 다른 항으로 분리하고, 분리 후 남은 몫을 2로 나눈다. 이 작업을 몫이 0이 될 때까지 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 함수로 표현하면 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1621074311912&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function getNumberByBase(base, number) {
  let remainder = 0;
  let quotient = number;
  let result = '';

  while (quotient &amp;gt; 0) {
    remainder = quotient % base;
    quotient = (quotient - remainder) / base;
    result = String(remainder) + result;
    
    // 추가 작업
  }

  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;숫자 나누기를 HTML 요소로 표현하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위의 while 루프 안에서 HTML 요소를 만들어주면 되는데, 필요한 작업은 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 처음 숫자를 입력 받기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 나머지를 분리해서 숫자를 &lt;span style=&quot;color: #006dd7;&quot;&gt;(숫자 - 나머지) +&lt;b&gt; 나머지&lt;/b&gt;&lt;/span&gt; 형태로 교체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. (숫자 - 나머지) 값을 BASE로 나눠서 &lt;span style=&quot;color: #006dd7;&quot;&gt;(BASE * 새로운 몫)&lt;/span&gt;으로 교체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 새로운 몫을 대상으로 반복&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*요소를 계속 교체해야 하므로(Node.replaceChild) depth를 맞춰줄 것&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;이걸 HTML에서 생각해보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 처음 숫자를 입력 받기&lt;/p&gt;
&lt;pre id=&quot;code_1621653522327&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;holder&quot;&amp;gt;
  &amp;lt;span class=&quot;quotient&quot;&amp;gt;최초 숫자&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 나머지를 분리해서 숫자를 &lt;span style=&quot;color: #006dd7;&quot;&gt;(숫자 - 나머지) +&lt;b&gt; 나머지&lt;/b&gt;&lt;/span&gt; 형태로 교체&lt;/p&gt;
&lt;pre id=&quot;code_1621653540014&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;holder&quot;&amp;gt;
  (
  &amp;lt;span class=&quot;quotient&quot;&amp;gt;숫자 - 나머지&amp;lt;/span&amp;gt;
  ) + 
  &amp;lt;span class=&quot;remainder&quot;&amp;gt;나머지&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. (숫자 - 나머지) 값을 BASE로 나눠서 &lt;span style=&quot;color: #006dd7;&quot;&gt;(BASE * 새로운 몫)&lt;/span&gt;으로 교체&lt;/p&gt;
&lt;pre id=&quot;code_1621653644212&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;holder&quot;&amp;gt;
  (BASE * 
  &amp;lt;span class=&quot;quotient&quot;&amp;gt;새로운 몫&amp;lt;/span&amp;gt;
  ) + 
  &amp;lt;span class=&quot;bold&quot;&amp;gt;나머지&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 새로운 몫을 대상으로 반복&lt;/p&gt;
&lt;pre id=&quot;code_1621653732827&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;holder&quot;&amp;gt;
  (BASE * (
  &amp;lt;span class=&quot;quotient&quot;&amp;gt;숫자 - 나머지&amp;lt;/span&amp;gt;
  ) + 
  &amp;lt;span class=&quot;remainder&quot;&amp;gt;나머지&amp;lt;/span&amp;gt;
  ) + 
  &amp;lt;span class=&quot;remainder&quot;&amp;gt;나머지&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 변경을 어떻게 처리할까 고민하다가,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.quotient 요소의 변경하고, 매번 추가되는 괄호나 + 기호는 createTextNode로 만들어서 quotient 앞뒤로 삽입해주는 방식을 택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 + 기호 뒤에 .remainder 요소를 삽입해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞뒤에 삽입하는 방법으로는 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Node/insertBefore&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Node.insertBefore&lt;/a&gt;을 이용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤에 삽입하는 경우에 필요한 insertAfter 메소드는 없지만, 대신에 참조노드로 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Node/nextSibling&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Node.nextSibling&lt;/a&gt;을 지정해서 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.nextSibling은 대상 노드가 마지막 노드면 null을 반환해주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.insertBefore은 참조노드로 null 값이 들어오는 경우 .appendChild처럼 마지막 노드로 추가하기 때문에 의도한 대로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621675292026&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;holder&quot;&amp;gt;
  &amp;lt;span class=&quot;quotient&quot;&amp;gt;최초 숫자&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 HTML 뼈대를 만들어주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621675441429&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const $holder = document.querySelector('.holder');
const $quotient = document.querySelector('.quotient');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 요소를 잡아온 다음,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621740527050&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function getNumberByBase(base, number) {
  let remainder = 0;
  let quotient = number;
  let result = '';

  while (quotient &amp;gt; 0) {
    remainder = quotient % base;
    const withoutRemainder = quotient - remainder;
    quotient = withoutRemainder / base;
    result = String(remainder) + result;

    separateRemainderElement({
      remainder,
      withoutRemainder,
    });
    await delay(1000);
    replaceQuotientElementWithNewQuotient({
      base,
      quotient,
    });
    await delay(1000);
  }

  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 추가작업으로 표시해둔 부분에 들어갈 작업을 함수로 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간차를 두고 보여주어야 해서 delay 함수를 추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1621740571714&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function delay(waitingTime) {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve();
    }, waitingTime);
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 표시해둔 작업을 함수로 구현하고&lt;/p&gt;
&lt;pre id=&quot;code_1621740481979&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function separateRemainderElement({ remainder, withoutRemainder }) {
  const $prefix = document.createTextNode('(');
  const $postfix = document.createTextNode(') + ');
  $holder.insertBefore($prefix, $quotient);
  $holder.insertBefore($postfix, $quotient.nextSibling);
  // $postfix는 $quotient 뒤에 추가해야 해서 $quotient의 다음 노드 앞에 삽입하도록 했다.
  
  $quotient.textContent = String(withoutRemainder);
  const $remainder = document.createElement('span');
  $remainder.textContent = String(remainder);
  $remainder.classList.add('remainder');
  $holder.insertBefore($remainder, $postfix.nextSibling);
  // $remainder도 $postfix 뒤에 추가해야 해서 마찬가지로 다음 노드 앞에 삽입
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1621740494346&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function replaceQuotientElementWithNewQuotient({ base, quotient }) {
  $quotient.textContent = String(quotient);
  const $multipliedByBase = document.createTextNode(`${base} * `);
  $holder.insertBefore($multipliedByBase, $quotient);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 작동하나 테스트를 해본다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;288&quot; data-origin-height=&quot;50&quot; data-filename=&quot;May-23-2021 12-26-08.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T3sJs/btq5t21h4rf/YVUog4o1bMCurPdTBud7J1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T3sJs/btq5t21h4rf/YVUog4o1bMCurPdTBud7J1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T3sJs/btq5t21h4rf/YVUog4o1bMCurPdTBud7J1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/T3sJs/btq5t21h4rf/YVUog4o1bMCurPdTBud7J1/img.gif&quot; data-origin-width=&quot;288&quot; data-origin-height=&quot;50&quot; data-filename=&quot;May-23-2021 12-26-08.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인풋 / n진법 케이스 추가하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 유저로부터 인풋을 받을 수 있도록 input form을 만들고, 진법을 직접 선택할 수 있도록 콤보박스를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 처리가능한 최대숫자는 Number.Max_SAFE_INTEGER로 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 &amp;lt;= user input &amp;lt;= Number.Max_SAFE_INTEGER로 받아서 Number로 변환해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621741395588&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;user-input__form-box&quot;&amp;gt;
  &amp;lt;form class=&quot;user-input__form js__user-input__form&quot;&amp;gt;
    &amp;lt;input class=&quot;user-input__input-box js__user-input__input-box&quot; type=&quot;text&quot; /&amp;gt;
  &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 먼저 HTML 뼈대를 만들어주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621741488670&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const $inputFormBody = document.querySelector('.js__user-input__form');
const $userInput = document.querySelector('js__user-input__input-box');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소를 잡아온 다음에&lt;/p&gt;
&lt;pre id=&quot;code_1621742052353&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$inputFormBody.addEventListener('submit', handleUserInput);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너를 달아주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621742066050&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function handleUserInput(event) {
  event.preventDefault();
  const inputNumber = Number($userInput.value);

  if (Number.isSafeInteger(inputNumber) &amp;amp;&amp;amp; inputNumber &amp;gt; 0) {
    await getNumberByBase(2, inputNumber);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너함수에서 입력 받은 숫자를 처리해서 넘기게 한다. 그리고 테스트를 해보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;144&quot; data-filename=&quot;May-23-2021 12-57-45.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NpnAI/btq5AQYKUtB/qnkQLdrKKZi7q18PDx6tTk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NpnAI/btq5AQYKUtB/qnkQLdrKKZi7q18PDx6tTk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NpnAI/btq5AQYKUtB/qnkQLdrKKZi7q18PDx6tTk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/NpnAI/btq5AQYKUtB/qnkQLdrKKZi7q18PDx6tTk/img.gif&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;144&quot; data-filename=&quot;May-23-2021 12-57-45.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도대로 잘 작동한다. 이제 n진법을 선택할 수 있도록 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션은 주로 사용할 법한 진법들인 2~9진법, 16진법(0~9, A~F), 36(0~9, A~Z)진법으로 하려고 하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10진법이 넘어가면 나머지가 10 이상으로 표시되는 문제가 있으니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;separateRemainderElement 함수에 값을 넘길 때 remainder를 toString(base)처리를 해서 넘겨주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1621743484030&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;separateRemainderElement({
  remainder: remainder.toString(base),
  withoutRemainder,
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;36진법이 잘 표시되는지 테스트를 해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621743805709&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;display-base-number&quot;&amp;gt;
  현재
  &amp;lt;span class=&quot;base-number js__base-number&quot;&amp;gt;36&amp;lt;/span&amp;gt;
  진법을 사용 중입니다.
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에 안내 표시를 넣어주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621743835548&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const $baseNumber = document.querySelector('.js__base-number');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소를 잡아와서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621743845693&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function handleUserInput(event) {
  event.preventDefault();
  const inputNumber = Number($userInput.value);

  if (Number.isSafeInteger(inputNumber) &amp;amp;&amp;amp; inputNumber &amp;gt; 0) {
    await getNumberByBase(+$baseNumber.textContent, inputNumber);  // 여기
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;base를 넘길 때 값을 가져와서 넘기도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 테스트를 해보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;172&quot; data-filename=&quot;May-23-2021 13-25-49.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U1u9X/btq5ue7THWc/PrThK0QIvl6s3HmrOpg20K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U1u9X/btq5ue7THWc/PrThK0QIvl6s3HmrOpg20K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U1u9X/btq5ue7THWc/PrThK0QIvl6s3HmrOpg20K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/U1u9X/btq5ue7THWc/PrThK0QIvl6s3HmrOpg20K/img.gif&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;172&quot; data-filename=&quot;May-23-2021 13-25-49.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 나오고 있다. 이제 콤보박스를 만들고 리스너를 달아주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621744148613&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;base-number__select-box__holder&quot;&amp;gt;
  &amp;lt;span&amp;gt;현재&amp;lt;/span&amp;gt;
  &amp;lt;select class=&quot;js__base-number__select-box&quot; name=&quot;base-number&quot;&amp;gt;
    &amp;lt;option value=&quot;2&quot; selected&amp;gt;2&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;3&quot;&amp;gt;3&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;4&quot;&amp;gt;4&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;5&quot;&amp;gt;5&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;6&quot;&amp;gt;6&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;7&quot;&amp;gt;7&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;8&quot;&amp;gt;8&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;9&quot;&amp;gt;9&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;10&quot;&amp;gt;10&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;16&quot;&amp;gt;16&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;36&quot;&amp;gt;36&amp;lt;/option&amp;gt;
  &amp;lt;/select&amp;gt;
  &amp;lt;span&amp;gt;진법을 사용 중입니다.&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콤보박스에 값을 주고,&lt;/p&gt;
&lt;pre id=&quot;code_1621746461973&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const $selectBoxBody = document.querySelector('.js__base-number__select-box');
let BASE_NUMBER = 2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 여기서 값을 받아와야 하니 $baseNumber는 없애고 BASE_NUMBER 변수를 새로 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621746474990&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function handleBaseNumberSelecting({ target }) {
  const selectedBase = target.children[target.selectedIndex].value;
  BASE_NUMBER = Number(selectedBase);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콤보박스 값이 변경되면 BASE_NUMBER 값이 바뀌게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 테스트를 해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;348&quot; data-filename=&quot;May-23-2021 14-11-38.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLYIne/btq5uUaI3IL/41zwMSjceykqGWUFyWvlN1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLYIne/btq5uUaI3IL/41zwMSjceykqGWUFyWvlN1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLYIne/btq5uUaI3IL/41zwMSjceykqGWUFyWvlN1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cLYIne/btq5uUaI3IL/41zwMSjceykqGWUFyWvlN1/img.gif&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;348&quot; data-filename=&quot;May-23-2021 14-11-38.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도대로 잘 작동한다. 이제 여러 번 사용할 수 있게 초기화 부분을 추가해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;초기화 추가하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1621748634430&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function handleUserInput(event) {
  event.preventDefault();
  const inputNumber = Number($userInput.value);

  if (Number.isSafeInteger(inputNumber) &amp;amp;&amp;amp; inputNumber &amp;gt; 0) {
    resetHolder();
    await getNumberByBase(BASE_NUMBER, inputNumber);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 새로 숫자가 들어오면 초기화를 해줘야하니 값을 넘기기 전에 resetHolder 함수를 실행해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1621748621780&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function resetHolder() {
  const temporaryParent = new DocumentFragment();
  temporaryParent.appendChild($quotient);
  $holder.textContent = '';
  $holder.appendChild(temporaryParent);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$holder 빼고 나머지 자식을 전부 초기화해야하는데 마땅한 방법이 생각나지 않아서 Fragment에 옮겨두는 방법을 썼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화가 되기는 하는데 HTML 요소의 값이 초기화될 뿐, getNumberByBase 함수가 중단되는 것은 아니라서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 getNumberByBase 함수가 실행 중일 때 새로 숫자가 들어오면 이렇게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;172&quot; data-filename=&quot;May-23-2021 14-46-24.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0WaUi/btq5BmpQysS/2UFdEET5dNNjf1m3mLART1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0WaUi/btq5BmpQysS/2UFdEET5dNNjf1m3mLART1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0WaUi/btq5BmpQysS/2UFdEET5dNNjf1m3mLART1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b0WaUi/btq5BmpQysS/2UFdEET5dNNjf1m3mLART1/img.gif&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;172&quot; data-filename=&quot;May-23-2021 14-46-24.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우를 막아야 하니 변환이 진행되고 있는 도중에는 새로 값이 들어오지 않도록 차단해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flag 변수를 만들어서 처리하면 될 것 같아서 &lt;a href=&quot;https://hayjo.tistory.com/114&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;기존에 작성했던 throttle 함수&lt;/a&gt;를 재활용해서 클로저 형태로 만들어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621749963974&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function modifiedThrottle(func) {
  let isWaiting = false;

  return async (...args) =&amp;gt; {
    if (isWaiting) {
      return;
    }

    isWaiting = true;
    await func(...args);
    isWaiting = false;
  };
}

const modifiedGetNumberByBase = modifiedThrottle(getNumberByBase);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isWaiting 함수를 만들어주고 실행 앞뒤로 토글을 추가해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 함수가 실행 중일 때 새로 숫자를 넣어보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;482&quot; data-origin-height=&quot;178&quot; data-filename=&quot;May-23-2021 15-07-50.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clcT7B/btq5t1VMjWx/iKknscNnB1fgdiDAaqlcck/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clcT7B/btq5t1VMjWx/iKknscNnB1fgdiDAaqlcck/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clcT7B/btq5t1VMjWx/iKknscNnB1fgdiDAaqlcck/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/clcT7B/btq5t1VMjWx/iKknscNnB1fgdiDAaqlcck/img.gif&quot; data-origin-width=&quot;482&quot; data-origin-height=&quot;178&quot; data-filename=&quot;May-23-2021 15-07-50.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행은 안 끊기고 잘 된다. resetHolder 실행위치만 getNumberByBase 내부로 바꿔주면 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;자릿수 테이블로 보여주기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 값을 자릿수에 맞춰서 정렬해서 보여주는 부분을 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11의 변환값이라면 이런 형태로, 위의 계산이 진행될 때마다 추가되면 좋겠다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 26.8606%; height: 120px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2^3&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2^2&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2^1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2^0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getNumberByBase의 while loop마다 추가되어야 하니 우선 함수 위치를 정하고, 지수 값이 될 index를 추가해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1621750840233&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function getNumberByBase(base, number) {
  resetHolder();

  let remainder = 0;
  let quotient = number;
  let result = '';
  let index = 0;

  while (quotient &amp;gt; 0) {
    remainder = quotient % base;
    const withoutRemainder = quotient - remainder;
    quotient = withoutRemainder / base;
    result = String(remainder) + result;

    // 숫자 나누기 보여주는 부분
    updatePolynomialTable(base, index, remainder); // 여기 추가
    index++;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 테이블이 될 HTML 요소를 만들어주고,&lt;/p&gt;
&lt;pre id=&quot;code_1621750676413&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;mark-polynomial-table__container&quot;&amp;gt;
  &amp;lt;table class=&quot;mark-polynomial-table js__mark-polynomial-table&quot;&amp;gt;
    &amp;lt;thead class=&quot;js__mark-polynomial-thead&quot;&amp;gt;
      &amp;lt;tr class=&quot;mark-polynomial-thead__exponent js__mark-polynomial-thead__exponent&quot;&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;/thead&amp;gt;
    &amp;lt;tbody class=&quot;js__mark-polynomial-tbody&quot;&amp;gt;
      &amp;lt;tr class=&quot;mark-polynomial-tbody__remainder js__mark-polynomial-tbody__remainder&quot;&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;/tbody&amp;gt;
  &amp;lt;/table&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thead 부분이 base부분, tbody 부분이 나머지 부분이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행은 하나씩만 쓸 거라서 tr을 만들고 거기에 바로 클래스를 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621751107403&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const $polynomialTableHead = document.querySelector('.js__mark-polynomial-thead__exponent');
const $polynomialTableBody = document.querySelector('.js__mark-polynomial-tbody__remainder');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 요소를 잡아와서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621751654691&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function updatePolynomialTable(base, index, remainder) {
  const $multiplierBox = document.createElement('td');
  const $remainderBox = document.createElement('td');

  $multiplierBox.textContent = `${base} ** ${index}`;
  $remainderBox.textContent = remainder;

  $polynomialTableHead.prepend($multiplierBox);
  $polynomialTableBody.prepend($remainderBox);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 들어오면 테이블에 td로 만들어서 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서부터 추가해줘야해서 append 대신에 prepend를 썼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;220&quot; data-filename=&quot;May-23-2021 15-37-03.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0bwHC/btq5vEkwAVK/JRJ5VVIcgX3L2xOizJh93k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0bwHC/btq5vEkwAVK/JRJ5VVIcgX3L2xOizJh93k/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0bwHC/btq5vEkwAVK/JRJ5VVIcgX3L2xOizJh93k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/0bwHC/btq5vEkwAVK/JRJ5VVIcgX3L2xOizJh93k/img.gif&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;220&quot; data-filename=&quot;May-23-2021 15-37-03.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 보여주는 elements가 늘었으니 초기화 범위도 늘려줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1621751584891&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function resetElements() {
  const temporaryParent = new DocumentFragment();
  temporaryParent.appendChild($quotient);
  $holder.textContent = '';
  $holder.appendChild(temporaryParent);

  $polynomialTableHead.textContent = '';
  $polynomialTableBody.textContent = '';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 만들어준 resetHolder 함수를 확장해서 테이블도 초기화해주게 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 2 ** 4 표기가 보기 좋지 않으니 보기 좋게 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;number-box class를 만들고, exponent도 따로 class를 부여해서 작은 숫자로 표기되게 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1621754675615&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.number-box {
  vertical-align: middle;
  box-sizing: border-box;
  display: inline-block;
  min-width: 30px;
  font-size: 45px;
}

.exponent-box {
  font-size: 20px;
  vertical-align: top;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1621754637328&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function updatePolynomialTable(base, index, remainder) {
  const $multiplierCell = document.createElement('td');
  const $remainderCell = document.createElement('td');

  const $multiplier = document.createElement('span');
  const $base = getElementBox(base, ['number-box']);
  const $exponent = getElementBox(base, ['exponent-box']);
  const $remainder = getElementBox(remainder, ['number-box']);

  $multiplier.appendChild($base);
  $multiplier.appendChild($exponent);

  $multiplierCell.appendChild($multiplier);
  $remainderCell.appendChild($remainder);

  $polynomialTableHead.prepend($multiplierCell);
  $polynomialTableBody.prepend($remainderCell);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 페이지 데모:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;668&quot; data-filename=&quot;May-23-2021 18-59-45.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BqAgL/btq5vFcKXP3/Fqpk2Ti9r5t2byxtk1rxBK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BqAgL/btq5vFcKXP3/Fqpk2Ti9r5t2byxtk1rxBK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BqAgL/btq5vFcKXP3/Fqpk2Ti9r5t2byxtk1rxBK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/BqAgL/btq5vFcKXP3/Fqpk2Ti9r5t2byxtk1rxBK/img.gif&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;668&quot; data-filename=&quot;May-23-2021 18-59-45.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FrontEnd/알고리즘</category>
      <category>n진법</category>
      <category>n진법변환</category>
      <category>n진수</category>
      <category>이진법</category>
      <category>이진수</category>
      <category>이진수변환</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/116</guid>
      <comments>https://hayjo.tistory.com/116#entry116comment</comments>
      <pubDate>Sun, 23 May 2021 15:44:48 +0900</pubDate>
    </item>
    <item>
      <title>비동기 흐름 정리 feat. Element.animate()</title>
      <link>https://hayjo.tistory.com/115</link>
      <description>&lt;p&gt;자바스크립트의 Async Flow에는 여러 가지가 있다.&lt;/p&gt;
&lt;p&gt;각 flow는 주어진 작업을 병렬로 처리할 것이냐, 직렬로 처리할 것이냐, 인자는 언제 어떻게 넘길 것이냐 등에서 차이를 보이는데,&lt;/p&gt;
&lt;p&gt;추상적인 개념이다보니 바로 와닿지 않아서 직접 시각화해보기로 했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;flow 설명은 &lt;a href=&quot;https://medium.com/velotio-perspectives/understanding-node-js-async-flows-parallel-serial-waterfall-and-queues-6f9c4badbc17&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;understanding-node-js-async-flows-parallel-serial-waterfall-and-queues&lt;/a&gt;를 참고했고,&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://caolan.github.io/async/v3/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;node의 async 모듈&lt;/a&gt;을 이용했다.&lt;/p&gt;
&lt;p&gt;(큐와 우선순위 큐는 아직 이해가 부족해서 이번 포스팅에서는 생략한다.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Parallel&lt;/h3&gt;
&lt;p&gt;독립적인 task 여러 개를 병렬적으로 수행하고, 모든 task가 완료되면 최종 callback을 호출한다.&lt;/p&gt;
&lt;p&gt;최종 callback에는 각 task의 결과가 전달된다. 이때 최종 callback에 넘겨주는 결과는 실행한 task의 순서와 동일하게 보장된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;212&quot; data-filename=&quot;parallel.gif&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ck3Y3j/btq3PCCxac3/Mzyz4av5v7VjjRRSaedZH0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ck3Y3j/btq3PCCxac3/Mzyz4av5v7VjjRRSaedZH0/img.gif&quot; data-alt=&quot;Parallel&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck3Y3j/btq3PCCxac3/Mzyz4av5v7VjjRRSaedZH0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ck3Y3j/btq3PCCxac3/Mzyz4av5v7VjjRRSaedZH0/img.gif&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;212&quot; data-filename=&quot;parallel.gif&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;Parallel&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Series&lt;/h3&gt;
&lt;p&gt;이전 작업이 끝나야 실행할 수 있는 task 여러 개를 수행하고, 모든 task가 완료되면 최종 callback을 호출한다.&lt;/p&gt;
&lt;p&gt;최종 callback에는 모든 task의 결과가 전달된다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;series.gif&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;218&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ImULv/btq3O6cF4bn/tLH8tmzZkvZ0FeOexQm7V0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ImULv/btq3O6cF4bn/tLH8tmzZkvZ0FeOexQm7V0/img.gif&quot; data-alt=&quot;Series&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ImULv/btq3O6cF4bn/tLH8tmzZkvZ0FeOexQm7V0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ImULv/btq3O6cF4bn/tLH8tmzZkvZ0FeOexQm7V0/img.gif&quot; data-filename=&quot;series.gif&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;218&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;Series&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Waterfall&lt;/h3&gt;
&lt;p&gt;이전 작업이 끝나야 실행할 수 있는 task 여러 개를 수행하고, 모든 task가 완료되면 최종 callback을 호출한다.&lt;/p&gt;
&lt;p&gt;각 task는 자기 작업이 끝나면 다음 task에 결과를 넘긴다. 최종 callback은 마지막 task의 결과를 받게 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;*바로 위의 Series와는 결과 전달 방법에서 차이가 난다. Series의 최종 callback은 각 task의 결과가 담긴 array를 인자로 받지만, Waterfall의 최종 callback은 마지막 task의 결과만을 받는다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;waterfall_done.gif&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;376&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/catkLy/btq36SRR8Ts/npI5CKSXNUnnaniCeXKHHK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/catkLy/btq36SRR8Ts/npI5CKSXNUnnaniCeXKHHK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/catkLy/btq36SRR8Ts/npI5CKSXNUnnaniCeXKHHK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/catkLy/btq36SRR8Ts/npI5CKSXNUnnaniCeXKHHK/img.gif&quot; data-filename=&quot;waterfall_done.gif&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;376&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Race&lt;/h3&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;각 task는 병렬(parallel)로 처리되지만, 어떤 task 하나가 끝나거나 에러가 나는 즉시 최종 callback이 호출된다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;216&quot; data-filename=&quot;race.gif&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXfBEj/btq3N5rFxU1/B4PwX2JyvxLvjfuh4cPKbk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXfBEj/btq3N5rFxU1/B4PwX2JyvxLvjfuh4cPKbk/img.gif&quot; data-alt=&quot;Race&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXfBEj/btq3N5rFxU1/B4PwX2JyvxLvjfuh4cPKbk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bXfBEj/btq3N5rFxU1/B4PwX2JyvxLvjfuh4cPKbk/img.gif&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;216&quot; data-filename=&quot;race.gif&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;Race&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;Race 개념을 이해하는 과정에서 1등 task가 끝나면 나머지 진행 중인 task들은 어떻게 되는지에 대한 의문이 들었다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;완료되지 않았더라도 더 이상 진행하지 않고 멈추는 로직을 추가해야 하나? 싶어 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/race&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN&lt;/a&gt;을 찾아보니 아래와 같은 예제 코드가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1619761863491&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var p1 = new Promise(function(resolve, reject) {
    setTimeout(() =&amp;gt; resolve('하나'), 500);
});
var p2 = new Promise(function(resolve, reject) {
    setTimeout(() =&amp;gt; resolve('둘'), 100);
});

Promise.race([p1, p2])
.then(function(value) {
  console.log(value); // &quot;둘&quot;
  // 둘 다 이행하지만 p2가 더 빠르므로
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;둘다 이행하지만 이라고 되어있으니 resolve 이전의 작업은 수행하는 것으로 추측되었다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;resolve 앞쪽에 각각 console.log 구문을 끼워넣고 출력을 해보니 둘다 진행되는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;raceTest.gif&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctluo3/btq3RD8HBPv/MDQKc6DsB9bRqu2QgJvQDk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctluo3/btq3RD8HBPv/MDQKc6DsB9bRqu2QgJvQDk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctluo3/btq3RD8HBPv/MDQKc6DsB9bRqu2QgJvQDk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ctluo3/btq3RD8HBPv/MDQKc6DsB9bRqu2QgJvQDk/img.gif&quot; data-filename=&quot;raceTest.gif&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;resolve 이전의 작업은 race 결과와 관계없이 모두 완료하는 것으로 보여서, 나머지 task들도 자기 작업은 완료하도록 처리했다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&lt;a href=&quot;http://gist.github.com/Rich-Harris/11010768#file-promise-js-L118&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;polyfill&lt;/a&gt;과 &lt;a href=&quot;https://caolan.github.io/async/v3/race.js.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;node async 소스코드&lt;/a&gt;를 참고했다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시각화 과정&lt;/h2&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;먼저 task와 finalCallback에 해당하는 DOM 요소를 만들고, 색 변화 애니메이션 효과로 작업 진행을 표현했다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;그리고 작업이 완료되고 결과를 넘겨줄 때, 결과값이 될 DOM 요소를 콜백 task 오른쪽으로 옮기고 holder로 감싸주었다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;holder가 있는 경우, 작업 진행을 표현할 때 holder도 색이 변하도록 해서 인자와 함께 처리된다는 의미를 더했다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;각 flow는 함수로 호출하고, 함수의 인자로 위에서 만든 DOM 요소(또는 요소를 처리하는 함수)를 주었다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;element: task&lt;/h3&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;제일 먼저 task 색 변화 애니메이션을 구현했다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;Web API의 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/animate&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Element.animate()&lt;/a&gt;를 이용하면 인자로 넘긴 애니메이션을 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Animation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Animation&lt;/a&gt; 객체로 만들어준다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;애니메이션이 완료되었는지를 알고 싶다면 Animation 객체의 finished 프로퍼티를 확인하면 된다. 완료 상황이 Promise로 들어간다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;색이 변하는 애니메이션이 끝나면 실제 요소 배경색을 바꿔주어야 하기 때문에, finished 이후 색 변화를 추가하고, callback으로 해당 요소를 넘겼다.&lt;/p&gt;
&lt;pre id=&quot;code_1619770886995&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function doTask({ element, color, delay, callback }) {
  const animation = element.animate({
    backgroundColor: color.start,
  }, delay);
    animation.finished.then(() =&amp;gt; {
    element.style.backgroundColor = color.end;
    callback(element);
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;그리고 task에 인자로 들어갈 element를 만드는 함수와&lt;/p&gt;
&lt;pre id=&quot;code_1619771701867&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function makeTask(content) {
  const task = document.createElement('div');
  task.classList.add('task');
  task.textContent = content;

  return task;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;그걸 여러 개 만드는 함수를 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1619771753898&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function makeTasks(n) {
  const taskList = [];
  for (let i = 0; i &amp;lt; n; i++) {
    const task = makeTask(`Task${i+1}`);
    taskList.push(task);
  }

  return taskList;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;task class CSS는 이렇게 줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1619771792169&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.task {
  display: inline-block;
  padding: 1rem 0.5rem;
  background-color: rgb(247, 196, 196);
  margin: 1rem;
  vertical-align: middle;
  border-radius: 4px;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;color는 task, holder, finalCallback에 따라 달라져서 따로 객체로 분리했다.&lt;/p&gt;
&lt;pre id=&quot;code_1619771568863&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const colors = {
  task: {
    start: 'salmon',
    end: '#8cf3a4',
  },
  holder: {
    start: '#8cf3a4',
    end: '#d3fcdc',
  },
  argsHolder: {
    start: '#d39d9d',
    end: '#c0e3c0',
  },
  final: {
    start: '#eaf8ed',
    end: 'rgb(247, 196, 196)',
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;delay에 넣어줄 작업시간이 필요하니 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN&lt;/a&gt;을 참고해서 함수를 추가해주고,&lt;/p&gt;
&lt;pre id=&quot;code_1619771444333&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  function generateRandomInt(min = 400, max = 5000) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;잘 되는지 확인해본다. task를 그냥 만들기만 하기 때문에 만든 다음에 다른 요소에 추가해주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1619772279310&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const tasks = makeTasks(2);
tasks.forEach(task =&amp;gt; {
  document.querySelector('body').appendChild(task);
  doTask({
    element: task,
    color: colors.task,
    delay: generateRandomInt(500, 1500),
    callback: (element) =&amp;gt; {
      element.textContent = `${element.textContent} is Done`;
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;taskIsDone.gif&quot; data-origin-width=&quot;288&quot; data-origin-height=&quot;110&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF9lxV/btq3OdwDho1/AbZGfVgY9Ber3k7UWfafok/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF9lxV/btq3OdwDho1/AbZGfVgY9Ber3k7UWfafok/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF9lxV/btq3OdwDho1/AbZGfVgY9Ber3k7UWfafok/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bF9lxV/btq3OdwDho1/AbZGfVgY9Ber3k7UWfafok/img.gif&quot; data-filename=&quot;taskIsDone.gif&quot; data-origin-width=&quot;288&quot; data-origin-height=&quot;110&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;element: holder&lt;/h3&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;함수가 인자를 받아서 처리하는 경우, 인자와 자기 자신을 외부와 구분해줄 holder가 필요하다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;holder는 element 여러 개를 담을 수도 있고, 하나를 담을 수도 있으니 array와 HTMLElement가 들어오는 경우 각각에 대해 처리를 해줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1619774052647&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function makeArgumentHolder(elements) {
  if (!Array.isArray(elements) &amp;amp;&amp;amp; !(elements instanceof HTMLElement)) {
    throw new Error(&quot;1st parameter of makeArgumentHolder accepts an array or an element&quot;);
  }

  const holder = document.createElement('div');
  holder.classList.add('argument-holder');

  if (Array.isArray(elements)) {
    elements.forEach((element) =&amp;gt; {
      holder.appendChild(element);
    });

    return holder;
  }

  holder.appendChild(elements);
  return holder;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;잘 되나 확인해본다. 마찬가지로 그냥 holder를 만들기만 하기 때문에 만든 다음에 다른 요소에 추가해주어야 한다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;이 경우 taskHolder가 body에 추가되기 때문에, 각 task를 직접 추가할 필요가 없어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1619774242239&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const tasks = makeTasks(2);
const taskHolder = makeArgumentHolder(tasks);
document.querySelector('body').appendChild(taskHolder);
tasks.forEach(task =&amp;gt; {
  doTask({
    element: task,
    color: colors.task,
    delay: generateRandomInt(500, 1500),
    callback: (element) =&amp;gt; {
      element.textContent = `${element.textContent} is Done`;
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;argsHolder.gif&quot; data-origin-width=&quot;346&quot; data-origin-height=&quot;158&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1OVmn/btq3UFR5SKD/SHbHCVh9ZpP229I1y2zma0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1OVmn/btq3UFR5SKD/SHbHCVh9ZpP229I1y2zma0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1OVmn/btq3UFR5SKD/SHbHCVh9ZpP229I1y2zma0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b1OVmn/btq3UFR5SKD/SHbHCVh9ZpP229I1y2zma0/img.gif&quot; data-filename=&quot;argsHolder.gif&quot; data-origin-width=&quot;346&quot; data-origin-height=&quot;158&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;element: final Callback&lt;/h3&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;final Callback은 인자를 받아야하므로 holder에 감싸져 있다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;그리고 최종 호출이 있다는 건 어떤 플로우 안에 있다는 뜻이니까, 바로 parent를 받아서 거기에 추가해주기로 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1619773916620&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function makeFinalCallback(parent) {
  const task = makeTask('Final');
  const holder = makeArgumentHolder(task);
  parent.appendChild(holder);

  return {
    task,
    holder,
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;이제 task1의 작업이 끝나면 final holder의 자식에 추가하고, final을 실행하면 된다. (아직 parallel 적용 전이라서 task를 1개로 줄였다.)&lt;/p&gt;
&lt;pre id=&quot;code_1619774932614&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const tasks = makeTasks(1);
const taskHolder = makeArgumentHolder(tasks);
const $body = document.querySelector('body');
const { task: final, holder: finalHolder } = makeFinalCallback($body);
$body.appendChild(taskHolder);
tasks.forEach(task =&amp;gt; {
  doTask({
    element: task,
    color: colors.task,
    delay: generateRandomInt(500, 1500),
    callback: (element) =&amp;gt; {
      element.textContent = `${element.textContent} is Done`;
      finalHolder.appendChild(taskHolder);
      doTask({
        element: final,
        color: colors.task,
        delay: 1000,
        callback: (element) =&amp;gt; {
          element.textContent = `${element.textContent} is Done`;
        }
      });
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;finalCallback.gif&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;196&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ziuCT/btq3OcSfGZt/W9g0ountiwHOYKbx8yke1k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ziuCT/btq3OcSfGZt/W9g0ountiwHOYKbx8yke1k/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ziuCT/btq3OcSfGZt/W9g0ountiwHOYKbx8yke1k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ziuCT/btq3OcSfGZt/W9g0ountiwHOYKbx8yke1k/img.gif&quot; data-filename=&quot;finalCallback.gif&quot; data-origin-width=&quot;382&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 data-selectable-paragraph=&quot;&quot;&gt;이제 여기에 parallel을 적용해서 인자 개수를 늘려본다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;flow: parallel.gif&lt;/h3&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;parallel은 함수가 담긴 배열인 tasks를 받아서 작업을 돌리고, 모든 task가 완료됐는지 확인해서, 완료되면 finalCallback을 호출하고 결과값을 넘겨준다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;위에서는 tasks에 DOMelement를 담았지만, 이제 parallel에는 함수를 넘겨줘야 하므로 map으로 익명함수들로 변경해줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1620044746746&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const taskElements = makeTasks(5);
const taskHolder = makeArgumentHolder(taskElements);
const $body = document.querySelector('body');
const { task: final, holder: finalHolder } = makeFinalCallback($body);
$body.appendChild(taskHolder);

const tasks = taskElements.map((element) =&amp;gt; {
  return (callback) =&amp;gt; doTask({
    element,
    color: colors.task,
    delay: generateRandomInt(),
    callback,
  });
});

async.parallel(tasks, () =&amp;gt; {
  finalHolder.appendChild(taskHolder);
  doTask({
    element: final,
    color: colors.task,
    delay: 1000,
    callback: (element) =&amp;gt; {
      element.textContent = `${element.textContent} is Done`;
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;async 모듈의 waterfall에서는 callback을 호출할 때 첫번째 인자로 error를 넘겨주는데,&lt;/p&gt;
&lt;p&gt;if (error) 가 참이면 callback에 error를 인자로 넘겨주기 때문에 callback 호출시 첫번째 인자는 null을 주어야 한다.&lt;/p&gt;
&lt;p&gt;내 경우는 doTask에서 이 부분을 처리하기 때문에 맞춰서 바꿔주고&lt;/p&gt;
&lt;pre id=&quot;code_1620117287752&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function doTask({ element, color, delay, callback }) {
  const animation = element.animate({
    backgroundColor: color.start,
  }, delay);
  animation.finished.then(() =&amp;gt; {
    element.style.backgroundColor = color.end;
    callback(null, element);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;parallel_temp.gif&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;194&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PzywW/btq3TWtoSIa/pH6AlmrvRFqeFKOYg8tyeK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PzywW/btq3TWtoSIa/pH6AlmrvRFqeFKOYg8tyeK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PzywW/btq3TWtoSIa/pH6AlmrvRFqeFKOYg8tyeK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/PzywW/btq3TWtoSIa/pH6AlmrvRFqeFKOYg8tyeK/img.gif&quot; data-filename=&quot;parallel_temp.gif&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;194&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;기초적인 parallel 애니메이션이 완성되었다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;하지만 현재 상태로는 인자를 받았다는 표시가 불분명하니, 인자를 받아서 처리한다는 뜻으로 holder에 표시를 해주면 좋겠다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;holder와 element를 둘다 받아서 지정한 색상으로 바꿔주는 함수를 만들고&lt;/p&gt;
&lt;pre id=&quot;code_1619778551678&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function doTaskWithArguments({ element, holder, callback: finalCallback }) {
  doTask({
    element: holder,
    color: colors.final,
    delay: 300,
    callback: () =&amp;gt; {
      doTask({
        element,
        color: colors.task,
        delay: 1000,
        callback: () =&amp;gt; {
          doTask({
            element: holder,
            color: colors.holder,
            delay: 300,
            callback: finalCallback,
          });
        }
      });
    }
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;task가 끝나면 taskHolder 색을 바꿔주도록 추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1620039724179&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const taskElements = makeTasks(5);
const taskHolder = makeArgumentHolder(taskElements);
const $body = document.querySelector('body');
const { task: final, holder: finalHolder } = makeFinalCallback($body);
$body.appendChild(taskHolder);

const tasks = taskElements.map((element) =&amp;gt; {
  return (callback) =&amp;gt; doTask({
    element,
    color: colors.task,
    delay: generateRandomInt(500, 1500),
    callback,
  });
});

parallel(tasks, (element) =&amp;gt; {
  finalHolder.appendChild(taskHolder);
  doTask({                   // taskHolder highlighting
    element: taskHolder,
    color: colors.argsHolder,
    delay: 500,
    callback: () =&amp;gt; {
      finalHolder.appendChild(taskHolder);
      doTaskWithArguments({  // finalCallback 처리
        element: final,
        holder: finalHolder,
        delay: 1000,
        callback: () =&amp;gt; {
          console.log('All is done.');
        }
      });
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;원하는 대로 색깔 지정은 잘 됐는데, tasks를 처리한 이후 holder의 색을 바꾸는 부분에서 callback 지옥이 생기고 있다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;이걸 해결하려면 순차 실행인 waterfall flow가 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;parallel_done.gif&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;188&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uzyby/btq3XbSW0CG/k8jhe8JasTDduCtTGd5grK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uzyby/btq3XbSW0CG/k8jhe8JasTDduCtTGd5grK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uzyby/btq3XbSW0CG/k8jhe8JasTDduCtTGd5grK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/Uzyby/btq3XbSW0CG/k8jhe8JasTDduCtTGd5grK/img.gif&quot; data-filename=&quot;parallel_done.gif&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;188&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;waterfall - 순차적용&lt;/h3&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;waterfall은 함수가 담긴 배열인 tasks를 받아서 순서대로 작업을 돌리고, 한 task가 끝나면 다음 task에 결과를 넘겨주면서 호출한다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;모든 task가 완료되면 finalCallback을 호출한다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;waterfall을 이용하면 tasks 완료 -&amp;gt; argument holder 컬러링 -&amp;gt; final holder로 이동 -&amp;gt; final callback 작업을 순차적으로 처리할 수 있다. waterfall을 이용하면 바로 위의 콜백지옥 코드를 이렇게 표현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1620043059935&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const taskElements = makeTasks(5);
const taskHolder = makeArgumentHolder(taskElements);
const $body = document.querySelector('body');
const { task: final, holder: finalHolder } = makeFinalCallback($body);
$body.appendChild(taskHolder);

const tasks = taskElements.map((element) =&amp;gt; {
  return (callback) =&amp;gt; doTask({
    element,
    color: colors.task,
    delay: generateRandomInt(500, 1500),
    callback,
  });
});

async.waterfall(
  [
    function doTasks(callback) {
      parallel(tasks, callback);
    },
    function highlightTaskHolder(_, callback) {
      doTask({
        element: taskHolder,
        color: colors.argsHolder,
        delay: 500,
        callback,
      });
    },
    function moveTasksToFinalCallback(_, callback) {
      finalHolder.appendChild(taskHolder);
      callback(null); // 인자를 안 넘기면 undefined가 되니까 상관 없을 것 같기도 하다.
    },
    function doFinalTask(callback) { // 위에서 error 외의 인자를 넘기지 않아서 더 받지 않았다
      doTaskWithArguments({
        element: final,
        holder: finalHolder,
        callback,
      });
    }
  ],
  function showFinalMessage() {
    console.log('All is done.');
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;waterfall을 적용하니 순차적으로 실행되는 함수들을 Array에 담을 수 있고,&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;함수 기능을 설명해주는 이름을 붙여줄 수도 있어서 가독성이 향상되었다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;원래는 finalCallback에서 마지막 함수의 실행 결과를 인자를 받지만 나는 더 이상 진행할 작업이 없어서 인자는 받지 않았다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;flow: series.gif&lt;/h3&gt;
&lt;p&gt;parallel 구조를 응용하면 series도 쉽게 만들 수 있다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;series는 이전 작업이 끝나야 실행할 수 있는 task 여러 개를 수행하고, 모든 task가 완료되면 finalCallback을 실행하면 된다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;parallel과 나머지 부분은 같고, task를 처리하는 순서만 다르다. 그러니 doTasks에서 parallel를 series로 교체하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;series_done.gif&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;198&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFa6nc/btq38l0imUm/GVSxkajvMREzKLjpSYZ4Gk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFa6nc/btq38l0imUm/GVSxkajvMREzKLjpSYZ4Gk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFa6nc/btq38l0imUm/GVSxkajvMREzKLjpSYZ4Gk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bFa6nc/btq38l0imUm/GVSxkajvMREzKLjpSYZ4Gk/img.gif&quot; data-filename=&quot;series_done.gif&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;198&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;flow: race.gif&lt;/h3&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;race도 parallel과 거의 흡사하지만, 제일 먼저 완료된 task만 final에 인자로 넘어간다는 점이 다르다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;나머지 task는 자기 자리에서 완료되어야 하므로 기존 taskHolder는 그대로 두고, 새로 elementHolder를 만들어주었다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;elementHolder는 로직 중간에 새로 생성되는 요소라서 인자로 넘겨서 다음 함수가 처리할 수 있도록 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1620046986458&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const taskElements = makeTasks(5);
const taskHolder = makeArgumentHolder(taskElements);
const $body = document.querySelector('body');
const { task: final, holder: finalHolder } = makeFinalCallback($body);
$body.appendChild(taskHolder);

const tasks = taskElements.map((element) =&amp;gt; {
  return (callback) =&amp;gt; doTask({
    element,
    color: colors.task,
    delay: generateRandomInt(500, 1500),
    callback,
  });
});

async.waterfall(
  [
    function doTasks(callback) {
      race(tasks, callback);
    },
    function addElementHolder(element, callback) {
      const elementHolder = makeArgumentHolder(element);
      callback(null, elementHolder);  // 첫번째 인자로 error - null
    },
    function moveTasksToFinalCallback(elementHolder, callback) {
      finalCallbackHolder.appendChild(elementHolder);
      callback(null, elementHolder);  // 첫번째 인자로 error - null
    },
    function highlightTaskHolder(elementHolder, callback) {
      doTask({
        element: elementHolder,
        color: colors.argsHolder,
        delay: 500,
        callback,
      });
    },
    function doFinalTask(_, callback) {
      doTaskWithArguments({
        element: finalCallback,
        holder: finalCallbackHolder,
        callback,
      });
    }
  ],
  function showFinalMessage() {
    console.log('All is done.');
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;race_done.gif&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;178&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AVNee/btq3XbrUXwE/AGxCvjerZ7nKVPKxWoYaWK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AVNee/btq3XbrUXwE/AGxCvjerZ7nKVPKxWoYaWK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AVNee/btq3XbrUXwE/AGxCvjerZ7nKVPKxWoYaWK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/AVNee/btq3XbrUXwE/AGxCvjerZ7nKVPKxWoYaWK/img.gif&quot; data-filename=&quot;race_done.gif&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;178&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;flow: waterfall.gif&lt;/h3&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;parallel, series, race에서는 각 task가 개별적으로 실행되었지만,&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;waterfall에서는 직전 task 결과를 받아서 실행해야 하기 때문에 tasks에 들어가는 익명함수도 parallel과는 달라야 한다.&lt;/p&gt;
&lt;p data-selectable-paragraph=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;매 task마다 인자가 새로 결정되기 때문에, 위 race에서 첫 번째 task가 끝나고 finalCallback 실행전에 했던 작업들,&lt;/p&gt;
&lt;p&gt;addElementHolder, ... 를 매번 수행해야 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다만 완료된 직전 task 입장에서는 자신을 받을 다음 타자가 누구인지 모르니까 그대로 넘기고,&lt;/p&gt;
&lt;p&gt;다음 task에서 작업 전에 그걸 받아서 holder로 감싸고, 위에서 만든 doTaskWithArgument로 작업을 했다.&lt;/p&gt;
&lt;p&gt;색 변화가 많아서 hightlightTaskHolder는 생략했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1620049952792&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const tasks = taskElements.map((element) =&amp;gt; {
  return (args, callback) =&amp;gt; {
    if (typeof args === &quot;function&quot;) {  // 인자 없이 넘긴 경우 ex) 첫번째 실행
      doTask({
        element,
        color: colors.task,
        delay: generateRandomInt(500, 1200),
        callback: args,
      });
      return;
    }
    const elementHolder = makeArgumentHolder(element);
    elementHolder.appendChild(args);
    taskHolder.appendChild(elementHolder);
    doTaskWithArguments({
      element,
      holder: elementHolder,
      callback,
    });
  }
});

async.waterfall(
  [
    function doTasks(callback) {
      waterfall(tasks, callback);
    },
    function highlightTaskHolder(_, callback) {
      doTask({
        element: taskHolder,
        color: colors.argsHolder,
        delay: 500,
        callback,
      });
    },
    function moveTasksToFinalCallback(_, callback) {
      finalHolder.appendChild(taskHolder);
      callback();
    },
    function doFinalTask(callback) {
      doTaskWithArguments({
        element: final,
        holder: finalHolder,
        callback,
      });
    }
  ],
  function showFinalMessage() {
    console.log('All is done.');
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;waterfall_done.gif&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;376&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/catkLy/btq36SRR8Ts/npI5CKSXNUnnaniCeXKHHK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/catkLy/btq36SRR8Ts/npI5CKSXNUnnaniCeXKHHK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/catkLy/btq36SRR8Ts/npI5CKSXNUnnaniCeXKHHK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/catkLy/btq36SRR8Ts/npI5CKSXNUnnaniCeXKHHK/img.gif&quot; data-filename=&quot;waterfall_done.gif&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;376&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/hayjo/async_visualization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github.com/hayjo/async_visualization&lt;/a&gt;&lt;/p&gt;</description>
      <category>FrontEnd/JavsScript</category>
      <category>animate</category>
      <category>AsyncFlow</category>
      <category>parallel</category>
      <category>Race</category>
      <category>Series</category>
      <category>waterfall</category>
      <category>비동기</category>
      <category>비동기흐름</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/115</guid>
      <comments>https://hayjo.tistory.com/115#entry115comment</comments>
      <pubDate>Mon, 3 May 2021 23:03:57 +0900</pubDate>
    </item>
    <item>
      <title>Throttle: 함수가 주기적으로 실행되는지 확인하기</title>
      <link>https://hayjo.tistory.com/114</link>
      <description>&lt;p&gt;명시적인 리턴값이 없는 경우, 함수가 의도대로 작동하는지를 어떻게 확인할 수 있을까 하는 의문이 생겼다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;throttle은 함수와 delay를 인자로 받고, delay 동안에는 실행 요청이 들어오더라도 1회 이상 실행되지 않는 함수를 리턴해준다.&lt;/p&gt;
&lt;p&gt;코드를 작성한 후 인자로 들어온 함수가 정말로 특정 시간 동안 실행되지 않는지 여부를 어떻게 확인할 수 있을까를 고민하다가&lt;/p&gt;
&lt;p&gt;인자 함수가 실행되면 로그를 찍어주면 되겠다는 생각이 들었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;잠깐 짚고 넘어가는 throttle과 debounce의 차이&lt;/h3&gt;
&lt;figure id=&quot;og_1618581086132&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;(JavaScript) 쓰로틀링과 디바운싱&quot; data-og-description=&quot;안녕하세요. 이번 시간에는 쓰로틀링(throttling)과 디바운싱(debouncing)에 대해 알아보겠습니다. 원래 예정에 없던 강좌이지만 요청을 받았기 때문에 써봅니다. 프로그래밍 기법 중 하나입니다(아니&quot; data-og-host=&quot;www.zerocho.com&quot; data-og-source-url=&quot;https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa&quot; data-og-url=&quot;https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/n1nCv/hyJTyZDk0N/DiamcBjRfKXjyPVHpg3knK/img.png?width=462&amp;amp;height=297&amp;amp;face=0_0_462_297,https://scrap.kakaocdn.net/dn/rtbrS/hyJUUUiTxk/dGQiUSKqkihRbtH7ZZbN80/img.png?width=462&amp;amp;height=297&amp;amp;face=0_0_462_297,https://scrap.kakaocdn.net/dn/bSrLHM/hyJTJUqiUs/Ibva3yLp3IDJTsvLDw3md1/img.png?width=462&amp;amp;height=297&amp;amp;face=0_0_462_297&quot;&gt;&lt;a href=&quot;https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/n1nCv/hyJTyZDk0N/DiamcBjRfKXjyPVHpg3knK/img.png?width=462&amp;amp;height=297&amp;amp;face=0_0_462_297,https://scrap.kakaocdn.net/dn/rtbrS/hyJUUUiTxk/dGQiUSKqkihRbtH7ZZbN80/img.png?width=462&amp;amp;height=297&amp;amp;face=0_0_462_297,https://scrap.kakaocdn.net/dn/bSrLHM/hyJTJUqiUs/Ibva3yLp3IDJTsvLDw3md1/img.png?width=462&amp;amp;height=297&amp;amp;face=0_0_462_297');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;(JavaScript) 쓰로틀링과 디바운싱&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;안녕하세요. 이번 시간에는 쓰로틀링(throttling)과 디바운싱(debouncing)에 대해 알아보겠습니다. 원래 예정에 없던 강좌이지만 요청을 받았기 때문에 써봅니다. 프로그래밍 기법 중 하나입니다(아니&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;www.zerocho.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;b&gt;throttle&lt;/b&gt;: 무한스크롤 시에 사용하면 유용하다. 한 번 작동하면 일정 시간 동안은 더 이상 작동하지 못하도록 막는다.&lt;/p&gt;
&lt;p&gt;[&lt;a href=&quot;https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L10897&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;lodash throttle source-code&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;debounce&lt;/b&gt;: 검색어 입력 도중에 결과를 보여주는 경우 사용하면 유용하다. 특정 시간 동안 입력 받은 작업은 하나로 치고, 마지막 작업(혹은 첫번째 작업)만 실행한다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L10304&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[lodash debounce source-code&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;lodash에서는 throttle을 debounce에 &lt;span&gt;'leading'과&lt;/span&gt;&lt;span&gt; &lt;span&gt;'trailing'&lt;/span&gt;&lt;span&gt; 옵션을 true로 주고 실행하는 방식으로 구현했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://lodash.com/docs/#debounce&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;문서&lt;/a&gt;에 보면 leading(선행)과 &lt;span&gt;&lt;span&gt;trailing(후행) 옵션이 true일 때, delay 동안 1번 보다 많이 실행되는 경우, delay 만큼 시간이 지난 다음에 1번만 실행된다고 되어있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 인자로 넘겨줄 함수는 아래처럼 만들었다.&lt;/p&gt;
&lt;p&gt;몇 번째 호출되었는지를 확인해야 하기 때문에 클로저로 nthRun을 넣고, 출력될 때마다 nthRun과 현재 시간을 보여준다.&lt;/p&gt;
&lt;pre id=&quot;code_1618582404103&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function showCallingSequence(name) {
  let nthRun = 0;
  return () =&amp;gt; {
    const currentTime = Date.now();
    console.log(`[${name}] ${nthRun++}th ${currentTime}`);
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 이 함수를 throttle에 넣고, 안 넣고로 나누어 실험군과 대조군을 만들고&lt;/p&gt;
&lt;pre id=&quot;code_1618582259280&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const experimental = showCallingSequence('■Throttle');
const throttled = throttle(experimental, 500); // throttle 기준이 500이므로 0.5초에 1번만 실행된다

const control = showCallingSequence('□Control');&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1초 동안 0.05초 주기로 각각 실행을 시켜본다.&lt;/p&gt;
&lt;pre id=&quot;code_1618582531018&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const timeID = setInterval(() =&amp;gt; {
  control();
  throttled();
}, 50);

setTimeout(() =&amp;gt; clearInterval(timeID), 1000);&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;h4 data-ke-size=&quot;size20&quot;&gt;먼저&lt;b&gt; lodash의 throttle&lt;/b&gt;로 테스트를 해봤다.&lt;/h4&gt;
&lt;p&gt;jsbin에서 lodash를 쓰려면 아래 태그를 html 부분에 추가해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1618585110824&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/lodash/4/lodash.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;jsbin에서는 const, let과 탬플릿리터럴이 잘 안 먹어서 수정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1618585145525&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function showCallingSequence(name) {
  var nthRun = 0;
  return function () {
    var currentTime = Date.now();
    console.log(name + ' ' + nthRun++ + 'thRun ' + currentTime);
  };
}

var experimental = showCallingSequence('■Throttle');
var throttled = _.throttle(experimental, 500);

var control = showCallingSequence('□Control');

var timeID = setInterval(() =&amp;gt; {
  control();
  throttled();
}, 50);

setTimeout(() =&amp;gt; clearInterval(timeID), 1000);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Apr-17-2021 00-01-08.gif&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;754&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnOhAd/btq2MvqQHcr/toej7sBvd3oA8BpAyravkk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnOhAd/btq2MvqQHcr/toej7sBvd3oA8BpAyravkk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnOhAd/btq2MvqQHcr/toej7sBvd3oA8BpAyravkk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dnOhAd/btq2MvqQHcr/toej7sBvd3oA8BpAyravkk/img.gif&quot; data-filename=&quot;Apr-17-2021 00-01-08.gif&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;754&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;의도대로 _.throttle이 500ms 텀을 두고 실행되고 있는 것을 확인할 수 있다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;처음에 작성했던 (오류가 있는) throttle 함수&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1618581361293&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const throttle = function (func, wait) {
  let isWaiting = false;

  return function () {
    if (isWaiting) {
      return;
    }

    setTimeout(() =&amp;gt; {
      isWaiting = true;
    }, wait);

    return func();
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 실행하면 isWaiting이 false이므로 setTimeout이 걸리고 isWaiting이 true로 토글된다.&lt;/p&gt;
&lt;p&gt;이후에 다시 isWaiting을 false로 바꿔주는 부분이 없기 때문에(오류 부분) 현재는 특정 시간 이후로 실행을 막는 형태다.&lt;/p&gt;
&lt;p&gt;문제가 있다는 건 알겠는데, 눈에 보이는 게 없어서 정확히 어떻게 문제가 되는지 확인하기가 어렵다.&lt;/p&gt;
&lt;p&gt;위에서 만든 showCallingSequence를 인자로 주고 출력해보면 이렇게 나온다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Apr-17-2021 00-07-25.gif&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;790&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PRckl/btq2NVa2FKP/8QViFB7KjXsSKFWoSQzkJk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PRckl/btq2NVa2FKP/8QViFB7KjXsSKFWoSQzkJk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PRckl/btq2NVa2FKP/8QViFB7KjXsSKFWoSQzkJk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/PRckl/btq2NVa2FKP/8QViFB7KjXsSKFWoSQzkJk/img.gif&quot; data-filename=&quot;Apr-17-2021 00-07-25.gif&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;790&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;의도대로 특정 시간(wait) 동안만 실행을 막고 싶다면, setTimeout이 끝나면 isWaiting을 다시 false로 돌려줘야 한다.&lt;/p&gt;
&lt;p&gt;그리고 함수가 실행되는 동안에는 추가 실행을 막아야 하니 return 직전에 isWaiting = true를 추가해준다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;(오류를 해결한) throttle 함수&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1618582139927&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const throttle = function (func, wait) {
  let isWaiting = false;

  return function () {
    if (isWaiting) {
      return;
    }

    setTimeout(() =&amp;gt; {
      isWaiting = false;
    }, wait);

    isWaiting = true;
    return func();
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 되면 아래처럼 작동하게 된다.&lt;/p&gt;
&lt;p&gt;setTimeout 시작&lt;/p&gt;
&lt;p&gt;-&amp;gt; 비동기 함수이므로 아래 라인 먼저 실행&lt;/p&gt;
&lt;p&gt;-&amp;gt; isWaiting = true 토글&lt;/p&gt;
&lt;p&gt;-&amp;gt; 인자로 받은 함수 실행&lt;/p&gt;
&lt;p&gt;-&amp;gt; setTiemout이 돌아가는 동안은 isWaiting이 true이므로 추가로 호출되더라도 실행이 차단됨&lt;/p&gt;
&lt;p&gt;-&amp;gt; setTimeout 종료되고, 함수가 끝나서 콜스택이 빔&lt;/p&gt;
&lt;p&gt;-&amp;gt; setTimeout에 인자로 넣어뒀던 함수가 실행되면서 isWaiting = false이 설정됨&lt;/p&gt;
&lt;p&gt;-&amp;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;Apr-17-2021 00-09-33.gif&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;790&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cddphe/btq2L2CIaXu/4sf9i7UGsv2XxGquK3oQjk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cddphe/btq2L2CIaXu/4sf9i7UGsv2XxGquK3oQjk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cddphe/btq2L2CIaXu/4sf9i7UGsv2XxGquK3oQjk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cddphe/btq2L2CIaXu/4sf9i7UGsv2XxGquK3oQjk/img.gif&quot; data-filename=&quot;Apr-17-2021 00-09-33.gif&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;790&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;_.throttle과 비교해보니 다른 점이 눈에 띈다.&lt;/p&gt;
&lt;p&gt;lodash의 throttle은 Control의 9th, 18th 턴에서 실행되었는데, 내가 구현한 함수는 10th에서만 실행되었다.&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;이렇게 함수를 실행해주는 함수를 어떻게 테스트하는가를 고민해보았다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체 코드&lt;/h4&gt;
&lt;p&gt;-파이어폭스 개발자도구 콘솔에서 실행해서 IIFE로 감쌌다&lt;/p&gt;
&lt;pre id=&quot;code_1618586280860&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(function() {
  const throttle = function (func, wait) {
    let isWaiting = false;

    return function () {
      if (isWaiting) {
        return;
      }

      setTimeout(() =&amp;gt; {
        isWaiting = false;
      }, wait);

      isWaiting = true;
      return func();
    };
  };
  
  const showCallingSequence = function (name = '') {
    let nthRun = 0;
    return function () {
      const currentTime = Date.now();
      console.log(`[${name}] ${nthRun++}th ${currentTime}`);
    };
  }
  
  const experimental = showCallingSequence('■Throttle');
  const throttled = throttle(experimental, 500); // throttle 기준이 500이므로 0.5초에 1번만 실행된다

  const control = showCallingSequence('□Control');
  
  const timeID = setInterval(() =&amp;gt; {
    control();
    throttled();
  }, 50);
  setTimeout(() =&amp;gt; clearInterval(timeID), 1000);
})();&lt;/code&gt;&lt;/pre&gt;</description>
      <category>FrontEnd/JavsScript</category>
      <category>Throttle</category>
      <category>주기실행</category>
      <category>함수작동확인</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/114</guid>
      <comments>https://hayjo.tistory.com/114#entry114comment</comments>
      <pubDate>Sat, 17 Apr 2021 00:21:25 +0900</pubDate>
    </item>
    <item>
      <title>SVG로 Progress-bar 만들기 with JS로 width 변경</title>
      <link>https://hayjo.tistory.com/113</link>
      <description>&lt;p&gt;남은 시간을 진행바로 표시해줘야 하는 상황이 생겼다.&lt;/p&gt;
&lt;p&gt;CSS에 -webkit-progress-bar 라는 프로퍼티가 있다고 듣기는 했는데 아직 생소해서 이번에는 최대한 아는 걸로 만들어보기로 했다.&lt;/p&gt;
&lt;p&gt;시간이 주어지면 (ex 10초) 남은 시간만큼 타임바가 짧아지다가 사라지는 형태다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;progress-bar.gif&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;52&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VT7Yf/btq0PcMlrD3/yAtyMSEYMJPmmadcEEZVs1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VT7Yf/btq0PcMlrD3/yAtyMSEYMJPmmadcEEZVs1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VT7Yf/btq0PcMlrD3/yAtyMSEYMJPmmadcEEZVs1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/VT7Yf/btq0PcMlrD3/yAtyMSEYMJPmmadcEEZVs1/img.gif&quot; data-filename=&quot;progress-bar.gif&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;52&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;큰 아이디어는 이쪽에서 얻었다: &lt;a href=&quot;https://mygumi.tistory.com/293&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ProgressBar 구현하는 3가지 방법&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 직선 모양이 있어야하니 svg 태그를 추가한다. 이미지를 잡기 위해서 우선 기본값을 넣어주었다.&lt;/p&gt;
&lt;p&gt;(Move to 좌표(0, 0) -&amp;gt; Horizontal 가로 좌표(변수) -&amp;gt; Vertical 세로 좌표 10으로 이동 -&amp;gt; 거기서 0으로 되돌아와서 마저 채우기)&lt;/p&gt;
&lt;pre id=&quot;code_1616425725318&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;wrapper progress-bar&quot;&amp;gt;
  &amp;lt;svg class=&quot;bar progres-bar&quot;&amp;gt;
    &amp;lt;path d=&quot;M 0 0 H 500 V 10 H 0&quot; /&amp;gt;
  &amp;lt;/svg&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 js를 구성한다. 먼저 상수처리. 셀렉터를 가져오고, 시간 제한을 정해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1616428221006&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const SELECTOR = {
  PROGRESS_BAR: {
    WRAPPER: document.querySelector('.wrapper.progress-bar'),
    BAR: document.querySelector('.bar.progress-bar'),
    PATH: document.querySelector('.bar.progress-bar').firstElementChild,
  },
};

const DATA = {
  TIME_LIMIT: 10000,
  CURRENT_TIMER: null,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;진행바를 처리할 변수를 하나 만들고, 처리할 넓이를 주면 path 값을 리턴하도록 메소드를 만든다.&lt;/p&gt;
&lt;p&gt;진행바 크기는 부모 요소의 크기에 따라 변해야 하기 때문에 offsetWidth를 가져왔다.&lt;/p&gt;
&lt;p&gt;remaining에는 남은 시간을 담을 예정이다.&lt;/p&gt;
&lt;pre id=&quot;code_1616427451552&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const progressBar = {
  getNewPath({ width }) {
    return `M 0 0 H ${width} V 10 H 0`;
  },
  get eachPoint() {
    return SELECTOR.PROGRESS_BAR.WRAPPER.offsetWidth / TIME_LIMIT;
  },
  remaining: 0,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;타이머는 재귀함수로 만들어서, 남은 시간이 있는지 확인해보고 있으면 다시 실행하도록 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1616428525619&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function reduceRemainingTime(delay) {
  if (progressBar.remaining &amp;gt; 0) {
    progressBar.remaining -= delay;
    return true;
  }
  progressBar.remaining = 0;
  // handleTimeOut(); 종료 후 로직은 이쪽에서 처리하면 된다.
  return false;
}

function checkRemainingTime(delay = 10) {
  const hasTime = reduceRemainingTime(delay);
  updateProgressBar();
  if (hasTime) {
    CURRENT_TIMER = setTimeout(checkRemainingTime, delay);
  }
}

function setTimer(delay = 1000) {
  progressBar.remaining = delay;
  CURRENT_TIMER = setTimeout(checkRemainingTime);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;checkRemainingTime에서 실행할 updateProgressBar() 함수는 이렇게 구성되어 있다.&lt;/p&gt;
&lt;p&gt;&amp;lt;path&amp;gt; 태그의 'd' 속성에 위의 progressBar.getNewPath 메소드에서 구한 Width 값을 변경해서 넣어준다.&lt;/p&gt;
&lt;p&gt;Width 값은, 전체 Width에서 남은 시간/전체 시간의 비율을 점유한다.&lt;/p&gt;
&lt;pre id=&quot;code_1616430045706&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function changeSVGPathWidth(newPath) {
  SELECTOR.PROGRESS_BAR.PATH.setAttribute('d', newPath);
}

function convertWidth(width) {
  return progressBar.eachPoint * width;
}

function updateProgressBar() {
  const width = progressBar.remaining;
  const convertedWidth = convertWidth(width);
  const newPath = progressBar.getNewPath({
    width: convertedWidth,
  });
  changeSVGPathWidth(newPath);
}
&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;CSS는 이렇다.&lt;/p&gt;
&lt;p&gt;위에서 사용한 Width가 부모인 div태그(.wrpper) 기준이기 때문에 자식들은 꼭 width: 100%를 가져야 하고,&lt;/p&gt;
&lt;p&gt;.wrapper에는 padding이 있으면 안 된다.&lt;/p&gt;
&lt;p&gt;그렇지 않으면 overflow된 부분만큼은 진행바에 반영이 안 되기 때문에, 진행바가 overflow 영역에 들어가는 동안은 진행바가 갱신되지 않는 것처럼 보인다.&lt;/p&gt;
&lt;pre id=&quot;code_1616429975673&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.wrapper.progress-bar {
  margin: 50px;
}

.bar-box.progress-bar {
  width: 100%;
}

.bar.progress-bar {
  width: 100%;
  fill: rgb(177, 170, 226);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;svg 태그의 너비를 바로 반영하고 싶다면 아래처럼 접근해서 처리할 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1616430382751&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECTOR.PROGRESS_BAR.BAR_BOX.width.animVal.value&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 prograssBar 객체를 정의했던 부분 중에서 offsetWidth 값 대신에 width ~ 를 넣어주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1616430466192&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const progressBar = {
  getNewPath({ width }) {
    return `M 0 0 H ${width} V 10 H 0`;
  },
  get eachPoint() {
    return SELECTOR.PROGRESS_BAR.BAR_BOX.width.animVal.value / TIME_LIMIT;
  },
  remaining: 0,
};&lt;/code&gt;&lt;/pre&gt;</description>
      <category>FrontEnd/JavsScript</category>
      <author>hayjo</author>
      <guid isPermaLink="true">https://hayjo.tistory.com/113</guid>
      <comments>https://hayjo.tistory.com/113#entry113comment</comments>
      <pubDate>Tue, 23 Mar 2021 01:28:39 +0900</pubDate>
    </item>
  </channel>
</rss>