<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>라쿤의 끄적끄적</title>
    <link>https://goforit.tistory.com/</link>
    <description>Go for IT ! 라쿤코드의 끄적끄적</description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 22:51:18 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>라쿤</managingEditor>
    <image>
      <title>라쿤의 끄적끄적</title>
      <url>https://tistory1.daumcdn.net/tistory/4412786/attach/4211a711b36641b7a8eef4a921d74d65</url>
      <link>https://goforit.tistory.com</link>
    </image>
    <item>
      <title>개발자의 마지막 보루: 네트워크 지옥에서 빛나는 로깅 이야기</title>
      <link>https://goforit.tistory.com/254</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p7aqT/dJMcaipodna/MB9L9YTUwDm54M70uulR8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p7aqT/dJMcaipodna/MB9L9YTUwDm54M70uulR8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p7aqT/dJMcaipodna/MB9L9YTUwDm54M70uulR8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp7aqT%2FdJMcaipodna%2FMB9L9YTUwDm54M70uulR8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;420&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;자율주행 로봇이 활약하는 물류 현장. 이곳은 겉으로는 첨단을 달리는 듯 보이지만, 개발자에게는 예측 불가능한 네트워크 환경과 싸워야 하는 전쟁터입니다. &amp;quot;로봇은 잘 움직였다고 하는데, 왜 우리는 문제를 재현할 수 없을까?&amp;quot; 이 질문은 현장에서 흔히 마주하는 답답함이자, &lt;strong&gt;&amp;#39;재현 안 되는 버그&amp;#39;&lt;/strong&gt;의 시작점입니다. 로그는 부족하고, 고객의 이야기를 들어도 핵심 맥락을 놓치기 일쑤죠. 이 글은 이런 현장 속에서 저희가 어떻게 문제의 실마리를 찾아내고, 운영의 안정성을 확보했는지에 대한 이야기입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 답답한 현실: &amp;quot;무엇이 문제인지 아무도 모른다&amp;quot;&lt;/h2&gt;
&lt;p&gt;서비스 초반, 몇 대 안 되는 로봇을 운영할 때도 문제가 생기면 그야말로 &amp;#39;맨땅에 헤딩&amp;#39;이었습니다. 현장에서 &amp;quot;로봇이 이상했어요&amp;quot;라는 제보가 들어와도, 그게 시스템 문제인지, 로봇이 의도대로 움직인 것인지, 아니면 사람이 실수한 것인지 파악하기가 너무 어려웠습니다.&lt;/p&gt;
&lt;p&gt;가장 흔한 시나리오는 이런 식입니다.&lt;br&gt;&amp;quot;로봇이 작업을 끝냈는데 엉뚱한 곳으로 갔어요!&amp;quot;&lt;/p&gt;
&lt;p&gt;이런 제보를 받으면, 개발자는 고객에게 끝없이 질문을 던질 수밖에 없습니다. &amp;quot;어떤 작업을 했나요? 로봇은 어디에 있었죠? 어디로 가야 했는데 어디로 갔다는 건가요? 언제 그랬나요?&amp;quot; 질문은 꼬리에 꼬리를 물고, 고객은 반복되는 질문에 지쳐갑니다. 이 과정 자체가 서비스 신뢰도를 갉아먹는다는 사실을 깨달았습니다.&lt;/p&gt;
&lt;p&gt;그래서 저희는 생각했습니다. &lt;strong&gt;&amp;quot;우리에게 정말 필요한 정보는 우리가 직접 챙기자.&amp;quot;&lt;/strong&gt; 그리고 로봇 클라이언트에 로그 시스템을 직접 구축하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;물론 Sentry 같은 훌륭한 에러 수집 서비스들이 있습니다. 하지만 특정 서비스에 얽매이지 않고, 우리가 원하는 모든 데이터를 자유롭게 수집하고 싶었습니다. 특히, 데이터 전송량이 많아질 때 발생하는 비용 문제도 무시할 수 없었기에, 처음부터 우리 환경에 최적화된 시스템을 만드는 것이 중요하다고 판단했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 해결책: 네트워크 걱정 없이 &amp;#39;모든 것&amp;#39;을 기록하다&lt;/h2&gt;
&lt;p&gt;가장 큰 난관은 불안정한 네트워크에서도 로그를 놓치지 않고 수집하는 것이었습니다. 웹 환경에서 로그는 양이 방대할 수 있기에, 단순한 &lt;code&gt;localStorage&lt;/code&gt;로는 한계가 명확했습니다. 저희의 선택은 &lt;strong&gt;&lt;code&gt;IndexedDB&lt;/code&gt;&lt;/strong&gt;였습니다. 브라우저 내에서 대용량 데이터를 저장할 수 있는 강력한 기능이죠. 복잡한 &lt;code&gt;IndexedDB&lt;/code&gt;를 좀 더 쉽게 다루기 위해 &lt;strong&gt;&lt;code&gt;localForage&lt;/code&gt;&lt;/strong&gt; 라이브러리를 사용했습니다. &lt;code&gt;localStorage&lt;/code&gt;처럼 쓰기 쉽지만, 비동기 방식으로 동작해 앱 성능에 영향을 주지 않는다는 점이 아주 매력적이었습니다.&lt;/p&gt;
&lt;h3&gt;① 로그를 &amp;#39;똑똑하게&amp;#39; 담는 그릇 만들기&lt;/h3&gt;
&lt;p&gt;로그 데이터를 어떻게 저장할지가 첫 번째 고민이었습니다. 저희는 &lt;strong&gt;UTC 기준으로 하루에 하나씩 테이블을 만들고&lt;/strong&gt;, 해당 날짜의 모든 로그를 여기에 쌓는 방식을 택했습니다. 각 로그는 발생 시각(&lt;code&gt;timestamp&lt;/code&gt;)을 키로 삼아 저장했고, 어떤 로봇이, 어떤 작업을, 언제, 어디서 했는지 등 현장 상황을 파악하는 데 필요한 모든 맥락 정보를 담았습니다.&lt;/p&gt;
&lt;h3&gt;② 디스크 공간은 소중하니까: 로그 수명 주기 관리&lt;/h3&gt;
&lt;p&gt;로봇의 디스크 공간은 무한하지 않습니다. 오래된 로그를 효과적으로 관리하는 정책이 필요했죠. &lt;code&gt;IndexedDB&lt;/code&gt;는 꽤 많은 공간을 사용할 수 있지만(Chrome 기준 디스크의 최대 80%), 무한정 쌓아둘 수는 없습니다. 저희는 테스트를 통해 하루에 생성되는 로그량을 추정했고, &lt;strong&gt;최대 3일치 로그를 보관&lt;/strong&gt;하기로 결정했습니다. 3일이 지난 로그 테이블은 자동으로 삭제되고, 만약 3일치 로그의 총량이 특정 기준을 넘어서면 가장 오래된 로그부터 지워지도록 하여 디스크 공간을 효율적으로 사용했습니다.&lt;/p&gt;
&lt;h3&gt;③ 현장으로 가지 않고도 로그를 확인하다: 원격 모니터링&lt;/h3&gt;
&lt;p&gt;로그는 잘 쌓였지만, 문제가 생길 때마다 현장에 가서 로그를 직접 뽑아올 수는 없는 노릇이었습니다. 그래서 원격에서 로그를 확인하고 관리할 방법을 고안했습니다. 초기에는 &lt;strong&gt;Slack을 통해 사용자가 로그 보내기 버튼을 누르면 로봇이 스스로 로그를 전송&lt;/strong&gt;해주는 기능을 구현했습니다. 현장 담당자가 문제가 발생했을 때 즉시 로그를 받아볼 수 있게 된 거죠. 이후 서버팀과 협력하여, &lt;strong&gt;OpenSearch를 통해 모든 로봇의 로그 데이터를 주기적으로 백업&lt;/strong&gt;하고 중앙에서 모니터링하고 분석할 수 있는 시스템을 구축했습니다. 이제는 사무실에서도 현장의 상황을 상세히 파악할 수 있게 되었습니다.&lt;/p&gt;
&lt;h3&gt;④ 무엇을 기록할 것인가: 로그 수집 전략의 진화&lt;/h3&gt;
&lt;p&gt;어떤 로그를 수집할지도 중요한 부분이었습니다. 처음에는 가장 문제가 많았던 &lt;strong&gt;네트워크 API 호출과 관련된 데이터&lt;/strong&gt;(&lt;code&gt;request&lt;/code&gt;, &lt;code&gt;response&lt;/code&gt;)를 중점적으로 수집했습니다. API 통신 과정에서 어떤 문제가 발생했는지 파악하는 것이 중요했기 때문입니다. 이 부분만 잘 봐도 사용자의 행동 흐름과 서버와의 통신 문제를 상당 부분 파악할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;점차적으로 앱에서 발생하는 &lt;strong&gt;에러 로그&lt;/strong&gt;, 사용자가 어떤 순서로 작업을 진행했는지 알려주는 &lt;strong&gt;맥락적 이벤트 로그&lt;/strong&gt;, 그리고 다양한 통신부의 &lt;strong&gt;상태 정보&lt;/strong&gt; 등으로 수집 범위를 확장했습니다. 이렇게 함으로써 현장의 상황을 더욱 포괄적이고 입체적으로 이해할 수 있게 되었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 얻게 된 인사이트: &amp;quot;이젠 문제가 두렵지 않다&amp;quot;&lt;/h2&gt;
&lt;p&gt;이러한 로컬 로깅 시스템을 구축하고 나니, 현장 운영에 있어 놀라운 변화가 찾아왔습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;문제 해결의 속도 향상&lt;/strong&gt;: 고객에게 일일이 물어볼 필요 없이, OpenSearch에서 로그만 확인해도 문제의 원인을 파악할 수 있게 되었습니다. &amp;quot;아, 사용자가 이렇게 사용하셨구나!&amp;quot; 혹은 &amp;quot;이 통신 부분에서 잘못된 데이터가 들어왔네!&amp;quot;와 같이 명확한 분석이 가능해졌습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;휴먼 에러의 명확한 증명&lt;/strong&gt;: 때로는 시스템 문제가 아닌, 사용자의 조작 미숙으로 발생하는 문제들도 있습니다. 로그는 이런 경우에도 객관적인 증거를 제공하여, 불필요한 논쟁 없이 문제 해결에 집중할 수 있게 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영 안정성의 기반 마련&lt;/strong&gt;: 네트워크 환경에 관계없이 중요한 데이터를 놓치지 않고 기록할 수 있게 되면서, 서비스 운영 전반의 안정성이 크게 향상되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;운영 환경에서 로그는 단순한 기록을 넘어, 문제를 진단하고 해결하는 데 필수적인 &amp;#39;생명줄&amp;#39;과 같습니다. 앞으로는 이렇게 쌓인 방대한 로그 데이터를 &lt;strong&gt;AI에게 분석을 맡겨보면 어떨까&lt;/strong&gt; 하는 새로운 도전을 고민하고 있습니다. AI의 도움을 받아 문제 상황을 더욱 빠르게 파악하고 대응하여, 로봇과 사람이 함께하는 현장이 더욱 매끄럽게 돌아갈 수 있도록 기여하고 싶습니다.&lt;/p&gt;</description>
      <category>Dev/기타</category>
      <category>frontend</category>
      <category>IndexedDB</category>
      <category>localForage</category>
      <category>logging</category>
      <category>OpenSearch</category>
      <category>로깅</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/254</guid>
      <comments>https://goforit.tistory.com/254#entry254comment</comments>
      <pubDate>Thu, 9 Apr 2026 21:47:07 +0900</pubDate>
    </item>
    <item>
      <title>PC 1대로 로봇 20대를 움직이다: 지속 가능한 E2E 테스트 인프라 구축기</title>
      <link>https://goforit.tistory.com/253</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v7xRK/dJMcaax6meE/o8oTx67u6ZSwjYkA1dSsEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v7xRK/dJMcaax6meE/o8oTx67u6ZSwjYkA1dSsEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v7xRK/dJMcaax6meE/o8oTx67u6ZSwjYkA1dSsEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv7xRK%2FdJMcaax6meE%2Fo8oTx67u6ZSwjYkA1dSsEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;400&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;물류 로봇 서비스를 개발하다 보면 가장 큰 벽에 부딪히는 지점이 있습니다. 바로 &amp;#39;실제로 로봇 수십 대가 동시에 움직일 때도 잘 작동할까?&amp;#39;라는 의문입니다.&lt;/p&gt;
&lt;p&gt;로봇 한두 대를 테스트하는 건 어렵지 않습니다. 하지만 현장 환경, 로봇 설정, 시나리오가 제각각인 상황에서 수십 대의 로봇이 얽히며 발생하는 변수들을 사람이 일일이 확인하기란 불가능에 가깝습니다. 실제 로봇 수십 대를 매번 나열해두고 테스트할 수도 없는 노릇이고요.&lt;/p&gt;
&lt;p&gt;이 고민 끝에 탄생한 &lt;strong&gt;&amp;#39;통합 시스템 연동 검증 플랫폼&amp;#39;&lt;/strong&gt; 구축 과정을 나누어보려 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 왜 굳이 E2E 테스트였을까?&lt;/h2&gt;
&lt;p&gt;유닛 테스트나 통합 테스트는 각 부품이 고장 나지 않았음을 증명합니다. 하지만 로봇 서비스는 &lt;strong&gt;로봇 클라이언트 - 관제 서버 - 물류 서비스 서버 - 관리자 UI&lt;/strong&gt;가 하나의 유기체처럼 움직여야 합니다.&lt;/p&gt;
&lt;p&gt;API 하나가 정상이라도, 전체 흐름에서 타이밍이 어긋나면 로봇은 멈춥니다. 그래서 우리는 실제 사용자의 끝과 끝(End-to-End)을 연결해 &amp;#39;진짜로 동작하는지&amp;#39;를 확인해야만 했습니다. 연결 부위에서 발생하는 예상치 못한 균열을 찾는 것이 우리에겐 가장 중요한 숙제였기 때문입니다.&lt;/p&gt;
&lt;h2&gt;2. Playwright: 가장 빠르고 가벼운 선택&lt;/h2&gt;
&lt;p&gt;도구를 고를 때 가장 중요하게 본 것은 &lt;strong&gt;&amp;#39;병렬성&amp;#39;&lt;/strong&gt;과 &lt;strong&gt;&amp;#39;격리성&amp;#39;&lt;/strong&gt;이었습니다.&lt;/p&gt;
&lt;p&gt;우리가 선택한 &lt;strong&gt;Playwright&lt;/strong&gt;는 브라우저 컨텍스트를 활용해 세션을 매우 가볍게 분리해줍니다. 덕분에 10코어 남짓한 PC 한 대에서도 20개 이상의 로봇 세션을 안정적으로 띄울 수 있었습니다. 특별한 유료 서비스 없이도 워커(Worker)를 통해 병렬 처리를 극대화할 수 있다는 점은, 적은 자원으로 큰 효과를 내야 하는 우리 팀에게 최적의 선택지였습니다.&lt;/p&gt;
&lt;h2&gt;3. 시나리오가 아닌 &amp;#39;상태&amp;#39;에 반응하기&lt;/h2&gt;
&lt;p&gt;처음에는 고정된 순서의 스크립트를 짰지만, 금방 한계가 왔습니다. 네트워크 지연이나 예상치 못한 팝업 하나에 테스트가 깨지기 일쑤였죠.&lt;/p&gt;
&lt;p&gt;그래서 우리는 &lt;strong&gt;&amp;#39;현재 화면 상태&amp;#39;를 보고 대응하는 동적 핸들러 방식&lt;/strong&gt;을 택했습니다. 로봇 세션이 스스로 화면을 인식하고, &amp;quot;지금은 피킹 대기 중이니 피킹 작업을 하자&amp;quot;, &amp;quot;지금은 이동 중이니 도착할 때 까지 기다리자&amp;quot;라고 판단하게 만든 것입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[테스트 실행: 로봇 세션 생성] --&amp;gt; B{화면 상태 체크 Loop}
    B --&amp;gt;|피킹 대기 화면| C[피킹 작업 핸들러 실행]
    B --&amp;gt;|이동 화면| D[이동 중 핸들러 실행]
    B --&amp;gt;|에러 화면| E[에러 데이터 수집 핸들러 실행]
    B --&amp;gt;|기타 상태| F[조건 대기 및 재시도]
    C --&amp;gt; B
    D --&amp;gt; B
    E --&amp;gt; B
    F --&amp;gt; B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 테스트를 훨씬 유연하게 만들었습니다. 시나리오 순서가 조금 뒤섞여도 테스트는 멈추지 않고 계속됩니다. 덕분에 단순한 기능 확인을 넘어, 실제 로봇이 현장에서 겪을 법한 불규칙한 상황들을 미리 시뮬레이션해볼 수 있었습니다.&lt;/p&gt;
&lt;h2&gt;4. 테스트실을 넘어 &amp;#39;실제 현장&amp;#39;으로&lt;/h2&gt;
&lt;p&gt;이 도구의 진정한 가치는 예상치 못한 곳에서 발견되었습니다. 바로 &lt;strong&gt;신규 고객사 현장 구축 단계&lt;/strong&gt;였습니다.&lt;/p&gt;
&lt;p&gt;실제 로봇 수십 대를 현장에 풀기 전, 서버 설정이 올바른지 혹은 현장 네트워크 환경에서 다수의 연결이 원활한지 확인해야 하는 순간들이 있습니다. 이때 우리는 이 플랫폼을 활용해 수십 개의 세션을 실제 현장 서버에 한꺼번에 접속시켰습니다.&lt;/p&gt;
&lt;p&gt;물리적인 로봇을 일일이 옮기거나 사람이 개입하지 않고도, &lt;strong&gt;현장 환경 기반의 다수 로봇 주행 테스트와 동작 테스트&lt;/strong&gt;를 앉은 자리에서 수행할 수 있게 된 것입니다. 이 과정을 통해 현장 투입 초기의 시행착오를 크게 줄일 수 있었고, 이는 곧 팀의 운영 효율로 이어졌습니다.&lt;/p&gt;
&lt;h2&gt;5. 병렬 테스트의 난제: 동시성 제어 서버 구축&lt;/h2&gt;
&lt;p&gt;테스트 규모가 커지고, &lt;strong&gt;여러 대의 PC를 넘나들며 로봇을 제어해야 하는 상황&lt;/strong&gt;이 오면서 새로운 기술적 난관에 부딪혔습니다. 수십 대의 로봇이 각기 다른 PC 환경에서 동시에 작업을 수행하다 보니, 한정된 자원(예: 바코드)을 중복으로 점유하려는 &lt;strong&gt;동시성 문제&lt;/strong&gt;가 발생한 것입니다.&lt;/p&gt;
&lt;p&gt;처음에는 로봇별로 사용할 데이터를 사전에 수동으로 나누어 할당했지만, 여러 PC에서 병렬로 로봇을 돌리며 동일한 바코드 풀(Pool)을 공유해야 하는 상황에서는 이 방식이 더 이상 통하지 않았습니다. 이를 해결하기 위해 우리는 &lt;strong&gt;NestJS와 PostgreSQL, TypeORM&lt;/strong&gt; 기반의 전용 관리 서버를 구축했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;중앙 데이터 스케줄링&lt;/strong&gt;: 각 로봇 세션이 필요한 데이터를 서버에 실시간으로 요청하면, 서버가 가용한 자원을 할당해주는 구조로 전환했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;트랜잭션과 락(Lock)&lt;/strong&gt;: 여러 로봇이 동시에 요청을 보낼 때 데이터가 겹치지 않도록, TypeORM의 &lt;strong&gt;트랜잭션 격리 수준&lt;/strong&gt;과 &lt;strong&gt;DB 락&lt;/strong&gt;을 활용해 동시성 이슈를 해결했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단순히 &amp;#39;바코드 할당&amp;#39;에서 시작한 이 서버는 점차 확장되어, 사람이 일일이 만들기 어려운 대규모 시뮬레이션용 목데이터(엑셀 작업 파일 등)를 자동으로 생성해주는 유틸리티 서버 역할까지 수행하게 되었습니다.&lt;/p&gt;
&lt;h2&gt;6. 데이터가 알려주는 시스템의 건강 상태&lt;/h2&gt;
&lt;p&gt;동시성 제어를 위해 구축한 서버는 점차 우리 팀의 &lt;strong&gt;통합 데이터 분석 플랫폼&lt;/strong&gt;으로 진화했습니다. 각 로봇 세션이 서버와 통신하며 작업을 할당받는 구조가 갖춰지자, 이를 활용해 테스트 과정에서 발생하는 수많은 지표를 실시간으로 수집할 수 있게 된 것입니다.&lt;/p&gt;
&lt;p&gt;테스트가 끝난 뒤 단순히 &amp;#39;성공&amp;#39; 메시지만 보고 넘어가는 것이 아니라, 서버에 쌓인 데이터를 통해 시스템의 미세한 징후들을 기록하기 시작했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;에러 UI 포착&lt;/strong&gt;: Playwright 세션이 화면을 탐색하다가 클라이언트가 정의한 에러 문구를 발견하면, 이를 서버로 전송해 즉시 데이터로 기록했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;네트워크 복기&lt;/strong&gt;: 문제가 생기면 당시의 HAR 로그와 통신 내역을 서버에 연동하여 무엇이 꼬였는지 추적했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;지표 대시보드&lt;/strong&gt;: 수집된 데이터를 바탕으로 기간별 에러 발생 추이를 시각화했습니다. 이를 통해 우리가 놓치고 있던 고질적인 문제들을 객관적인 데이터로 마주하게 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;마치며: 품질은 &amp;#39;자유로움&amp;#39;에서 온다&lt;/h2&gt;
&lt;p&gt;이번 프로젝트를 통해 얻은 가장 큰 인사이트는 &lt;strong&gt;&amp;quot;테스트 인프라가 견고할수록 개발자는 더 자유롭게 도전할 수 있다&amp;quot;&lt;/strong&gt;는 점입니다.&lt;/p&gt;
&lt;p&gt;하드웨어가 없어도, 현장에 가지 않아도 내가 짠 코드가 수십 대의 로봇 사이에서 어떻게 작동할지 미리 알 수 있다는 확신. 그 확신이 팀의 개발 속도를 높이고 서비스의 품질을 단단하게 만들었습니다. PC 1대로 시작한 이 작은 플랫폼은 이제 우리 팀이 품질을 지키는 가장 든든한 동료가 되었습니다.&lt;/p&gt;</description>
      <category>Dev/기타</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/253</guid>
      <comments>https://goforit.tistory.com/253#entry253comment</comments>
      <pubDate>Tue, 7 Apr 2026 22:11:18 +0900</pubDate>
    </item>
    <item>
      <title>정말 그 기능이 필요할까요? : 요구사항 너머의 진짜 고민</title>
      <link>https://goforit.tistory.com/252</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuBZdm/dJMcah42YSU/CQjLhf3h0CuTXo8bWCpa41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuBZdm/dJMcah42YSU/CQjLhf3h0CuTXo8bWCpa41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuBZdm/dJMcah42YSU/CQjLhf3h0CuTXo8bWCpa41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuBZdm%2FdJMcah42YSU%2FCQjLhf3h0CuTXo8bWCpa41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B2B 서비스를 만들다 보면, 고객사의 요구사항(VoC)은 단순한 '의견'을 넘어 프로젝트 전체를 뒤흔드는 거대한 파도로 다가올 때가 많습니다. 특히 대규모 고객사의 한 마디는 비즈니스 임팩트가 크기에, 우리 개발자들은 본능적으로 &quot;어떻게 구현할까?&quot;부터 고민하며 설계도를 펼치곤 하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 가끔은 잠시 멈춰서 물어야 합니다. &lt;b&gt;&quot;정말 그 기능이 최선일까?&quot;&lt;/b&gt; 하고요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. &quot;수개월이 걸릴 대형 프로젝트의 예감&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 현장 운영팀으로부터 긴급한 요구사항이 전달되었습니다. &lt;b&gt;&quot;고객사에서 워크스페이스 1개로 로봇을 여러 층(다층)에서 운영하고 싶어 합니다. 지금 구조로는 안 되니 신규 개발이 필요할 것 같아요.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;층을 넘나들려면, 엘리베이터 제어 연동 작업에 작업 관리 및 명령에 관한 부분도 모두 추가 설계를 발생시키는 큰 작업으로 예상될 만큼 수개월이 걸릴 대형 프로젝트 느낌이였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기술 이전에 '맥락'을 묻다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 무작정 개발을 시작하기 전에, 고객이 로봇을 사용하는 &lt;b&gt;실제 시나리오&lt;/b&gt;를 집요하게 파헤쳐 보기로 했습니다. 과연 이 문제가 로봇들이 실시간으로 층을 넘나들며 작업해야 하는 복잡한 문제인가? 아니면 각 층이라는 독립된 공간에서 제 할 일을 잘하면 되는 문제인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 확인해 본 결과는 생각보다 단순했습니다. 로봇들이 여러 층을 동시에 쓰긴 하지만, 마치 '싱글 스레드'처럼 한 층에서 작업을 완전히 끝내고 다음 층으로 이동해 다시 시작하는 방식이었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 층간 데이터를 고려한 작업 관리 및 명령에 대한 거창한 시스템은 필요 없었습니다. 단순히 층별로 공간을 분리해 설정하는 가이드만 잘 제공한다면 신규 개발 없이도 요구사항을 완벽히 충족할 수 있었습니다. 수개월의 개발 리소스가 단 한 장의 '운영 가이드'로 대체된 순간이었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 요구사항은 '구현'이 아닌 '조율'의 대상입니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객의 요청에 밤을 새워 '검수 과정' 기능을 만들었지만, 정작 현장에서는 &quot;작업 속도가 너무 느려진다&quot;며 기능을 꺼두는 사례를 본 적이 있습니다. 의욕적으로 개발에 뛰어들었지만, 결과적으로는 누구에게도 도움이 되지 않는 기능을 만드느라 소중한 리소스를 낭비한 셈이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발은 단순히 코드를 짜는 행위가 아니라, 한정된 리소스를 투입해 어떤 가치를 만들어낼지 결정하는 &lt;b&gt;트레이드 오프(Trade-off)&lt;/b&gt;의 과정입니다. 새로운 기능을 하나 더할 때마다 시스템의 복잡도는 올라가고, 우리는 그만큼의 유지보수 비용과 잠재적인 안정성 리스크를 감수해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 엔지니어의 진짜 역할은 고객이 &quot;A를 해달라&quot;고 할 때 그 이면의 &quot;왜?&quot;를 묻고, 그 선택이 가져올 파급효과를 미리 짚어주는 것입니다. 고객사가 정말 원하는 가치가 무엇인지 함께 고민하며, 현재의 안정적인 서비스를 해치지 않는 선에서 최선의 해결책을 &lt;b&gt;이끌어내고 조율하는 것&lt;/b&gt;. 그것이 진정으로 비즈니스 문제를 해결해 나가는 성숙한 엔지니어링 마인드가 아닐까 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 화려한 신규 기능을 배포하는 것보다, 기존의 구조를 영리하게 활용해 시스템의 무게를 늘리지 않으면서도 고객의 가려운 곳을 긁어주었을 때 더 큰 성취감을 느낍니다. 기술적인 욕심보다는 서비스의 본질적인 안정성을 지키며 최적의 지점을 찾아가는 것, 그것이 제가 지향하는 가장 강력한 개발 전략입니다.&lt;/p&gt;</description>
      <category>Dev/기타</category>
      <category>요구사항</category>
      <category>조율</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/252</guid>
      <comments>https://goforit.tistory.com/252#entry252comment</comments>
      <pubDate>Mon, 6 Apr 2026 21:37:27 +0900</pubDate>
    </item>
    <item>
      <title>AI가 팀의 언어를 바꾸기까지: Claude Code 도입기</title>
      <link>https://goforit.tistory.com/251</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UQtc1/dJMcahYeWQ9/14LYD3q6kSbEHpUQEGCOX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UQtc1/dJMcahYeWQ9/14LYD3q6kSbEHpUQEGCOX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UQtc1/dJMcahYeWQ9/14LYD3q6kSbEHpUQEGCOX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUQtc1%2FdJMcahYeWQ9%2F14LYD3q6kSbEHpUQEGCOX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;1. 검색으로는 답이 나오지 않는 지점들&lt;/h2&gt;
&lt;p&gt;개발자로 살면서 마주하는 진짜 어려운 문제들은 의외로 구글이나 스택 오버플로우에 답이 없습니다. 우리 팀만의 복잡한 비즈니스 로직, 꼬여버린 레거시 코드, 그리고 특정 산업 도메인에서만 발생하는 기괴한 이슈들. 이런 것들은 결국 혼자 끙끙대며 코드를 파헤치고 수많은 시행착오를 겪어야만 풀리는 숙제들이었습니다.&lt;/p&gt;
&lt;p&gt;저 역시 그 과정에서 참 많이 지치기도 했습니다. &amp;quot;다들 이렇게 고생하면서 배우는 게 맞나?&amp;quot; 싶기도 하고, 누군가에게 물어보고 싶어도 내 맥락을 다 설명하는 데만 한나절이 걸릴 것 같아 포기하곤 했죠. 그렇게 에너지를 쏟아붓는 게 당연한 줄 알았던 시절이 있었습니다.&lt;/p&gt;
&lt;h2&gt;2. 가볍게 시작해서 깊숙이 이식하기까지&lt;/h2&gt;
&lt;p&gt;처음엔 그저 신기해서 써본 ChatGPT와 Gemini였습니다. 그러다 &amp;#39;이걸 내 터미널 안으로 가져오면 어떨까?&amp;#39;라는 생각에 Gemini CLI를 만져보고, 개인적인 메모 도구인 Obsidian과 엮어보면서 조금씩 재미를 붙였습니다.&lt;/p&gt;
&lt;p&gt;그러다 회사에서 Claude Code를 정식으로 업무에 써보게 되었는데, 이게 생각보다 제 작업 방식을 많이 바꿔놓았습니다. 가장 좋았던 건 &lt;strong&gt;&amp;#39;실체를 빨리 볼 수 있다는 것&amp;#39;&lt;/strong&gt;이었습니다. 예전에는 기획자나 디자이너와 모호한 개념을 두고 말로만 씨름했다면, 이제는 Claude Code로 후다닥 만든 초안을 띄워놓고 이야기합니다. &amp;quot;이런 느낌 맞나요?&amp;quot;라고 물으면 대화가 훨씬 명확해지더군요.&lt;/p&gt;
&lt;h2&gt;3. PR 리뷰라는 병목을 마주하며&lt;/h2&gt;
&lt;p&gt;도구를 쓰니 확실히 코드가 만들어지는 속도는 빨라졌습니다. 그런데 이번엔 다른 곳에서 문제가 생겼습니다. 제가 쏟아내는 수많은 PR을 동료들이 검토하는 데 시간이 너무 많이 걸리기 시작한 거죠. &amp;quot;코드는 금방 짜는데, 리뷰는 왜 이렇게 밀릴까?&amp;quot; 고민이 깊어졌습니다.&lt;/p&gt;
&lt;p&gt;그렇다고 AI한테 리뷰를 다 맡기자니, 아직은 그 친구의 할루시네이션(거짓말)을 100% 믿을 순 없었습니다. 실제로 리팩토링 과정에서 미세한 로직이 누락되는 걸 보고 식겁했던 적도 있었거든요.&lt;/p&gt;
&lt;p&gt;그래서 저는 AI에게 &amp;#39;리뷰&amp;#39; 대신 &lt;strong&gt;&amp;#39;설명&amp;#39;&lt;/strong&gt;을 시켰습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PR Description 스킬&lt;/strong&gt;: 제가 PR 본문을 직접 채우는 대신, AI가 작업 내용을 읽고 Mermaid 구조도로 그려주게 했습니다. 리뷰어는 복잡한 코드를 읽기 전에 그림부터 봅니다. &amp;quot;아, 이 로직이 이렇게 흘러가는구나&amp;quot;를 먼저 이해하고 코드를 보니 소통 비용이 확 줄어들었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;테스트와 문서화&lt;/strong&gt;: 귀찮지만 꼭 해야 하는 유닛 테스트, 컴포넌트 목(Mock) 데이터 만들기, 그리고 금방 낡아버리는 문서들을 관리하는 데 AI를 적극 활용했습니다. 예전엔 &amp;quot;나중에 해야지&amp;quot; 하며 미뤄두던 일들을 그때그때 처리할 수 있게 된 게 가장 큰 수확입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 결국은 문제를 해결하는 사람으로&lt;/h2&gt;
&lt;p&gt;가끔은 그런 생각도 듭니다. 예전엔 며칠씩 밤새워 고민하던 일들을 이제는 몇 초 만에 척척 해내는 걸 보며 묘한 허탈감이 들기도 하죠. &amp;quot;내가 쌓아온 숙련도는 이제 의미가 없나?&amp;quot;라는 고민도 잠시 했습니다.&lt;/p&gt;
&lt;p&gt;하지만 결국 우리가 하는 일의 본질은 코드를 치는 행위가 아니라 &lt;strong&gt;&amp;#39;문제를 푸는 것&amp;#39;&lt;/strong&gt;이라고 생각합니다. 반복적인 문서화나 단순한 보일러플레이트 코드 작성에 쓰던 에너지를 아껴서, 더 높은 수준의 아키텍처를 고민하고 팀원들과 더 질 높은 대화를 나누는 것. 그게 진짜 엔지니어링 아닐까요.&lt;/p&gt;
&lt;p&gt;요즘은 여기서 조금 더 나아가, AI가 만든 결과물을 우리 시스템에 더 견고하게 안착시키는 &amp;#39;하네스 엔지니어링(Harness Engineering)&amp;#39; 같은 개념들에도 슬쩍 눈길이 갑니다. 단순히 빨리 만드는 걸 넘어, 어떻게 하면 더 정확하고 효과적으로 공정을 통제할 수 있을지 가볍게 고민해보는 거죠. &lt;/p&gt;
&lt;p&gt;결국 AI는 제 일을 뺏어가는 존재가 아니라, 제가 더 중요한 고민을 할 수 있도록 곁을 지켜주는 든든한 조력자 같은 느낌입니다. 이제는 이 도구를 어떻게 더 체계적으로 다룰지, 어떻게 하면 팀 전체의 DX(개발자 경험)를 더 높일 수 있을지를 조금 더 깊게 파보려 합니다.&lt;/p&gt;</description>
      <category>Dev/기타</category>
      <category>Ai</category>
      <category>claude code</category>
      <category>하네스 엔지니어링</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/251</guid>
      <comments>https://goforit.tistory.com/251#entry251comment</comments>
      <pubDate>Sat, 4 Apr 2026 16:31:38 +0900</pubDate>
    </item>
    <item>
      <title>아키텍처는 트레이드오프의 산물: 물류 로봇 서비스의 FSD 도입과 화이트 라벨링 운영 회고</title>
      <link>https://goforit.tistory.com/250</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_zfgydpzfgydpzfgy.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQ1ARW/dJMcadOZL6U/j1ks8adb7F3hFxttbvRrA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQ1ARW/dJMcadOZL6U/j1ks8adb7F3hFxttbvRrA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQ1ARW/dJMcadOZL6U/j1ks8adb7F3hFxttbvRrA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQ1ARW%2FdJMcadOZL6U%2Fj1ks8adb7F3hFxttbvRrA1%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;1376&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_zfgydpzfgydpzfgy.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;엔지니어링에서 완벽한 &amp;#39;은탄환&amp;#39;은 존재하지 않습니다. 모든 기술적 의사결정은 결국 얻는 것과 잃는 것 사이의 &lt;strong&gt;트레이드오프(Trade-off)&lt;/strong&gt;를 결정하는 과정입니다. &lt;/p&gt;
&lt;p&gt;회사 서비스를 다양한 고객사의 화이트 라벨링 요구사항에 대응하기 위해 시도했던 &lt;strong&gt;FSD(Feature-Sliced Design) 아키텍처 도입&lt;/strong&gt;과 &lt;strong&gt;인프라 기반 설정 주입 전략&lt;/strong&gt; 역시 그 여정의 연속이었습니다. 단순히 무엇을 구축했는지를 넘어, 어떤 고민을 했고 운영 과정에서 무엇을 느꼈는지에 대한 회고를 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 직면한 문제: 기능의 파편화와 예측 불가능한 영향 범위&lt;/h2&gt;
&lt;p&gt;서비스가 초기 단계를 넘어 고도화될수록 프론트엔드 코드는 필연적으로 비대해졌습니다. 로봇 운영 시나리오가 복잡해지고 고객사가 늘어남에 따라 다음과 같은 문제들이 발생했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비대한 비즈니스 로직&lt;/strong&gt;: UI 컴포넌트 내부에 비즈니스 로직, API 호출, 상태 관리가 뒤섞여 재사용이 불가능해짐.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예측 불가능한 영향 범위&lt;/strong&gt;: 도메인 간의 의존성이 스파게티처럼 엉켜, 특정 기능 수정이 예상치 못한 페이지의 오류로 이어짐.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;화이트 라벨링 대응의 한계&lt;/strong&gt;: 고객사별 브랜드 컬러나 특정 기능 활성화 여부를 관리하기 위해 코드 곳곳에 산재한 조건부 렌더링(&lt;code&gt;if-else&lt;/code&gt;)의 한계.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 해결 전략 1: 우리에게 맞는 아키텍처를 찾는 여정&lt;/h2&gt;
&lt;p&gt;무너진 질서를 바로잡기 위해 아토믹 디자인, 레이어드 아키텍처 등 여러 방법론을 검토했습니다. 특히 &lt;strong&gt;&amp;quot;도메인을 외부 인프라로부터 격리한다&amp;quot;&lt;/strong&gt;는 &lt;strong&gt;헥사고날 아키텍처&lt;/strong&gt;의 철학에 가장 큰 매력을 느꼈습니다. 하지만 현대적인 프론트엔드 프레임워크에 이를 어떻게 실천적으로 녹여낼지에 대한 가이드가 부족했습니다.&lt;/p&gt;
&lt;p&gt;우리는 &lt;strong&gt;FSD가 헥사고날의 추상화 철학을 프론트엔드적인 폴더 구조로 실현하는 데 가장 최적화된 그릇&lt;/strong&gt;이라고 판단했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;  잠깐, &lt;a href=&quot;https://feature-sliced.design/&quot;&gt;FSD(Feature-Sliced Design)&lt;/a&gt;란?&lt;/h3&gt;
&lt;p&gt;FSD는 프론트엔드 애플리케이션의 복잡성을 제어하기 위해 코드를 비즈니스 가치와 도메인의 추상화 수준에 따라 7개의 계층(Layers)으로 나누는 아키텍처 방법론입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    app[app: 설정, 스타일, 프로바이더 초기화]
    pages[pages: 전체 페이지 구성 및 라우팅]
    widgets[widgets: 독립적으로 사용 가능한 UI 유닛]
    features[features: 사용자 가치 중심 기능]
    entities[entities: 비즈니스 핵심 도메인 모델 및 로직]
    shared[shared: 재사용 가능한 공용 컴포넌트 및 유틸리티]

    app --&amp;gt; pages
    pages --&amp;gt; widgets
    widgets --&amp;gt; features
    features --&amp;gt; entities
    entities --&amp;gt; shared

    subgraph &amp;quot;의존성 방향 (Dependency Direction)&amp;quot;
        direction[▼ 상위 레이어은 하위 레이어만 참조 가능]
    end
    style direction fill:#f9f9f9,stroke-dasharray: 5 5&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;왜 FSD(Feature-Sliced Design)인가? (명과 암)&lt;/h3&gt;
&lt;p&gt;FSD 도입은 우리 팀에게 강력한 해결책이 되었지만, 명확한 트레이드오프가 존재했습니다.&lt;/p&gt;
&lt;h4&gt;✅ 우리가 얻은 명확한 장점&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;코로케이션(Collocation)을 통한 생산성&lt;/strong&gt;: 기능 슬라이스 안에 UI, State, Type을 모아 응집도를 높임으로써 파일 탐색 시간을 획기적으로 줄였습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도메인 로직 격리 (Port-Adapter)&lt;/strong&gt;: &lt;code&gt;entities&lt;/code&gt;에서 인터페이스(Port)를 정의하고 &lt;code&gt;api&lt;/code&gt;에서 구현(Adapter)하는 구조를 통해 실제 로봇이나 서버 규격 변화에 강한 도메인을 구축했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;⚠️ 운영하며 겪은 현실적인 단점&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;높은 초기 온보딩 비용&lt;/strong&gt;: 폴더 구조가 직관적이지 않아 새로운 팀원이 합류했을 때 코드를 어디에 위치시킬지 결정하는 데 꽤 높은 학습 곡선이 필요했습니다. 이를 위해 &amp;#39;레이어 역할 정의서&amp;#39;라는 별도의 가이드가 필수적이었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Widget 레이어의 블랙홀화&lt;/strong&gt;: 책임이 모호한 결합 로직들이 &lt;code&gt;widgets&lt;/code&gt;로 몰려 거대해지는 현상이 발생했습니다. 이를 방지하기 위해 위젯을 더 작은 피처나 엔티티 단위로 쪼개는 지속적인 리팩토링이 동반되어야 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파일 파편화&lt;/strong&gt;: 코로케이션을 위해 파일을 잘게 쪼개다 보니 파일 개수가 급격히 늘어났습니다. 비록 IDE 검색에 더 의존하게 되었지만, 특정 기능의 맥락을 파악하는 데는 긍정적인 면도 있었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;3. 해결 전략 2: 인프라와 런타임을 아우르는 화이트 라벨링 설계&lt;/h2&gt;
&lt;p&gt;단순히 코드 내부의 설계를 넘어, &lt;strong&gt;AWS S3와 CloudFront를 활용한 &amp;#39;Single-Build, Multi-Config&amp;#39; 전략&lt;/strong&gt;을 수립했습니다.&lt;/p&gt;
&lt;h3&gt;① 배포 및 실행 워크플로우&lt;/h3&gt;
&lt;p&gt;사용자의 호스트 도메인을 식별하여 해당 고객사의 전용 설정을 런타임에 동적으로 주입합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    subgraph Storage [인프라 레이어]
        S3App[(AWS S3&amp;lt;br/&amp;gt;공통 앱)]
        S3Config[(AWS S3&amp;lt;br/&amp;gt;고객사 설정)]
    end

    Storage --&amp;gt; CF[AWS CloudFront&amp;lt;br/&amp;gt;배포망]

    subgraph ClientLayer [클라이언트 실행 환경]
        CF --&amp;gt; Browser[사용자 브라우저]

        subgraph AppRuntime [앱 실행 런타임]
            Browser --&amp;gt; Identify[호스트 도메인 식별]
            Identify --&amp;gt; FetchConfig[고객사 전용&amp;lt;br/&amp;gt;설정 주입]
            FetchConfig --&amp;gt; Render[맞춤형 서비스&amp;lt;br/&amp;gt;렌더링 완료]
        end
    end

    style AppRuntime fill:#f0f7ff,stroke:#0052cc
    style Render fill:#fff7e6,stroke:#ff8c00&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;② 관리 전략에 대한 고찰: JSON 주입 vs 서버 관리&lt;/h3&gt;
&lt;p&gt;S3를 통한 JSON 설정 주입 방식은 빠르고 효율적이지만, 완벽한 솔루션은 아니었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JSON 주입의 한계&lt;/strong&gt;: 고객사가 늘어날수록 클라이언트 스키마 변경 시 과거 설정값들에 대한 마이그레이션 공수가 기하급수적으로 증가합니다. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;적정 기술의 선택&lt;/strong&gt;: UI 테마, 로고 등 자주 변하지 않는 시각적 요소들은 S3 관리 방식이 효과적입니다. 하지만 비즈니스 로직을 빈번하게 제어해야 하는 &lt;strong&gt;Feature Flag&lt;/strong&gt; 성격의 요구사항은 운영상 서버(DB 기반 관리)를 통해 제어하는 것이 더 안전하고 유연하다는 결론을 얻었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. 결과 및 회고: 은탄환은 없다&lt;/h2&gt;
&lt;p&gt;아키텍처 개선을 통해 &lt;strong&gt;유지보수 효율 향상&lt;/strong&gt;과 &lt;strong&gt;S3 설정 파일 추가만으로 신규 고객사 대응&lt;/strong&gt;이 가능한 구조를 만들었습니다. &lt;/p&gt;
&lt;p&gt;가장 큰 소득은 아키텍처가 단순히 코드를 예쁘게 만드는 것을 넘어, &lt;strong&gt;비즈니스의 확장성과 운영 효율 사이에서 끊임없이 트레이드오프를 결정하는 과정&lt;/strong&gt;임을 깊이 체감한 것입니다. 복잡한 로봇 물류 도메인에서도 유연함을 잃지 않는 시스템을 구축한 경험은 앞으로 더 큰 성장을 뒷받침하는 든든한 기반이 될 것입니다.&lt;/p&gt;</description>
      <category>Dev/기타</category>
      <category>FSD</category>
      <category>아키텍처</category>
      <category>프론트엔드</category>
      <category>화이트라벨링</category>
      <category>회고</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/250</guid>
      <comments>https://goforit.tistory.com/250#entry250comment</comments>
      <pubDate>Fri, 3 Apr 2026 19:08:29 +0900</pubDate>
    </item>
    <item>
      <title>로봇 물류 현장의 네트워크 단절을 극복하는 프론트엔드 전략</title>
      <link>https://goforit.tistory.com/249</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_kdzegtkdzegtkdze.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR7TB4/dJMcaax2GVS/jXcpqKku0ZEeEbvWkw7mM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR7TB4/dJMcaax2GVS/jXcpqKku0ZEeEbvWkw7mM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR7TB4/dJMcaax2GVS/jXcpqKku0ZEeEbvWkw7mM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR7TB4%2FdJMcaax2GVS%2FjXcpqKku0ZEeEbvWkw7mM0%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;1376&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_kdzegtkdzegtkdze.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;물류 센터는 프론트엔드 엔지니어에게 가혹한 환경입니다. 수천 평의 창고 부지에는 필연적으로 Wi-Fi 음영 구역이 존재하며, 이동 중인 로봇에 탑재된 클라이언트는 시시각각 네트워크 단절과 지연을 경험합니다. &lt;/p&gt;
&lt;p&gt;이러한 환경에서 &lt;strong&gt;&amp;#39;데이터 정합성&amp;#39;&lt;/strong&gt;과 &lt;strong&gt;&amp;#39;끊김 없는 사용자 경험(UX)&amp;#39;&lt;/strong&gt;이라는 두 마리 토끼를 잡기 위해 고민했던 기술적 해결책들을 공유합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 직면한 문제: &amp;quot;로봇은 움직이는데, 데이터는 멈췄다&amp;quot;&lt;/h2&gt;
&lt;p&gt;자율주행 물류 피킹 서비스를 운영하며 가장 빈번하게 발생한 문제는 네트워크 불안정으로 인한 서비스 이탈이었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UI 프리징&lt;/strong&gt;: API 응답을 기다리는 동안 화면이 멈추거나 로딩 스피너가 무한히 도는 현상.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 오염&lt;/strong&gt;: 네트워크 지연 중 사용자의 중복 클릭으로 인해 동일한 작업 명령이 여러 번 전송되는 문제.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가시성 저하&lt;/strong&gt;: 관리자 페이지와 현장 로봇 간의 작업 상태가 일치하지 않아 운영 효율 저하.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 해결 전략: 신뢰할 수 있는 클라이언트 구축&lt;/h2&gt;
&lt;p&gt;단순히 &amp;quot;네트워크를 좋게 만드는 것&amp;quot;은 엔지니어의 영역 밖이었기에, &lt;strong&gt;&amp;quot;네트워크가 나빠도 신뢰할 수 있는 앱&amp;quot;&lt;/strong&gt;을 만드는 데 집중했습니다.&lt;/p&gt;
&lt;h3&gt;① Service Worker를 통한 오프라인 가용성 확보&lt;/h3&gt;
&lt;p&gt;네트워크가 완전히 끊긴 상황에서도 앱이 &amp;#39;깨지지 않는 것&amp;#39;이 최우선이었습니다. Service Worker를 도입하여 정적 자산을 캐싱하고, 오프라인 감지 시 사용자에게 명확한 안내 UI를 제공했습니다. 이를 통해 사용자는 앱이 고장 난 것이 아니라 네트워크 환경의 문제임을 즉각 인지하고 불필요한 재시도를 멈출 수 있었습니다.&lt;/p&gt;
&lt;h3&gt;② GraphQL Apollo Subscription 헬스 체크 최적화&lt;/h3&gt;
&lt;p&gt;실시간 로봇 상태를 수신하기 위해 WebSocket(Subscription)을 사용했지만, 소켓 연결은 유지되어도 내부 데이터 전송이 지연되는 &amp;#39;좀비 커넥션&amp;#39; 문제가 발생했습니다. &lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 커스텀 헬스 체크 로직을 추가했습니다. 서버와 Ping-Pong 메시지를 주고받으며 지연을 확인하고, 응답 시간이 임계치를 넘어서면 기존 연결을 강제로 끊고 재연결을 시도합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant S as 서버
    participant C as 클라이언트

    Note over S, C: WebSocket 연결 수립 (구독)
    S-&amp;gt;&amp;gt;C: 핑 (Ping)
    C-&amp;gt;&amp;gt;S: 퐁 (Pong, 정상)

    Note right of C: 네트워크 지연 발생
    S-&amp;gt;&amp;gt;C: 핑 (Ping)
    Note over C: 헬스 체크 임계치 초과

    C-&amp;gt;&amp;gt;C: 연결 강제 종료
    C-&amp;gt;&amp;gt;S: 새로운 연결 요청
    S--&amp;gt;&amp;gt;C: 데이터 동기화 및 재연결 완료&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;③ 멱등성 기반의 비동기 동기화 전략&lt;/h3&gt;
&lt;p&gt;가장 핵심적인 변화는 &lt;strong&gt;&amp;#39;선 조작 후 최종 단계 동기화&amp;#39;&lt;/strong&gt; 구조의 도입입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;상태 기반 업데이트(State-based)&lt;/strong&gt;: &amp;quot;물품 1개 추가&amp;quot;와 같은 증분(Incremental) 명령 대신, 클라이언트가 이미 계산을 끝낸 최종 결과값(예: &amp;quot;현재 물품 수량은 2개&amp;quot;)만을 동기화하도록 설계했습니다. &lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    subgraph &amp;quot;증분 방식 (불안정)&amp;quot;
        A1[액션: +1] --&amp;gt;|네트워크 지연| B1(재시도: +1)
        B1 --&amp;gt; C1{서버}
        A1 --&amp;gt; C1
        C1 --&amp;gt;|결과| D1[최종 결과: 2]
    end

    subgraph &amp;quot;상태 기반 (안정 / 멱등)&amp;quot;
        A2[액션: 수량=1] --&amp;gt;|네트워크 지연| B2(재시도: 수량=1)
        B2 --&amp;gt; C2{서버}
        A2 --&amp;gt; C2
        C2 --&amp;gt;|결과| D2[최종 결과: 1]
    end&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비차단 로컬 우선 UX (Non-blocking Local-First)&lt;/strong&gt;: 중간 과정의 모든 액션은 서버 응답을 기다리지 않는 &lt;strong&gt;비동기 명령 전파(Asynchronous Command)&lt;/strong&gt; 방식으로 처리했습니다. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;체크포인트 기반 2단계 정합성 보장&lt;/strong&gt;: 작업을 마무리하는 핵심 시점(예: 피킹 완료 버튼 클릭)을 &lt;strong&gt;체크포인트&lt;/strong&gt;로 설정합니다. 여기서 단순히 완료 요청을 보내는 것이 아니라, 1) 그동안 전파된 모든 벌크 데이터의 최종 상태가 서버와 일치하는지 먼저 확인(Reconciliation)하고, 2) 정합성이 확인된 상태에서만 최종 완료 명령을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant U as 사용자 (작업자)
    participant C as 클라이언트 (앱)
    participant S as 서버

    Note over U, C: [1단계: 비차단 작업 진행]
    U-&amp;gt;&amp;gt;C: 물품 1 피킹 (수량: 1)
    C-&amp;gt;&amp;gt;U: UI 업데이트 (즉시 반영)
    C--&amp;gt;&amp;gt;S: 비동기 명령 전파 (최종 수량: 1)

    U-&amp;gt;&amp;gt;C: 물품 2 피킹 (수량: 2)
    C-&amp;gt;&amp;gt;U: UI 업데이트 (즉시 반영)
    C--&amp;gt;&amp;gt;S: 비동기 명령 전파 (최종 수량: 2)

    Note over U, S: 네트워크 지연으로 일부 명령 전달 지연 가능성

    Note over U, C: [2단계: 체크포인트 정합성 확정]
    U-&amp;gt;&amp;gt;C: &amp;quot;피킹 완료&amp;quot; 클릭
    C-&amp;gt;&amp;gt;S: 벌크 데이터 최종 정합성 확인 요청
    S--&amp;gt;&amp;gt;C: 모든 데이터 동기화 확인 (Sync OK)

    Note over C, S: 데이터가 일치할 때만 최종 완료 단계 진입
    C-&amp;gt;&amp;gt;S: 작업 최종 완료(Finalize) 요청
    S--&amp;gt;&amp;gt;C: 작업 종료 및 다음 태스크 할당 (200 OK)
    C-&amp;gt;&amp;gt;U: 최종 성공 알림 및 화면 전환&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;3. 결과 및 회고&lt;/h2&gt;
&lt;p&gt;이러한 개선 과정을 통해 다음과 같은 성과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;데이터 정합성 100%&lt;/strong&gt;: 네트워크 장애 상황에서도 중복 작업이나 데이터 누락 없이 안정적인 운영이 가능해졌습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;작업 생산성 향상&lt;/strong&gt;: 로딩 대기 시간이 사라짐에 따라 현장 작업자의 심리적 불안감이 해소되었고, 이는 곧 작업 속도 향상으로 이어졌습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영 공수 절감&lt;/strong&gt;: 관리자와 현장의 데이터 불일치로 인해 발생하던 확인 절차가 획기적으로 줄어들었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;프론트엔드 엔지니어링은 단순히 화면을 예쁘게 만드는 것을 넘어, &lt;strong&gt;시스템의 제약 사항을 기술적으로 극복하여 비즈니스의 연속성을 확보하는 일&lt;/strong&gt;임을 다시 한번 체감한 프로젝트였습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;다음 편 예고:&lt;/strong&gt;&lt;br&gt;&lt;em&gt;2편: 1대의 PC로 40대의 로봇을 검증하기: Playwright 기반 병렬 테스트 자동화&lt;/em&gt;&lt;/p&gt;</description>
      <category>Dev/기타</category>
      <category>graphql</category>
      <category>UI</category>
      <category>UX</category>
      <category>네트워크</category>
      <category>논블로킹</category>
      <category>프론트엔드</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/249</guid>
      <comments>https://goforit.tistory.com/249#entry249comment</comments>
      <pubDate>Fri, 3 Apr 2026 00:39:40 +0900</pubDate>
    </item>
    <item>
      <title>제품 하나를 운영하기 위해 온 팀이 필요한 이유</title>
      <link>https://goforit.tistory.com/248</link>
      <description>&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;안녕하세요. 자율 주행 로봇 회사 트위니에서 물류 로봇 서비스 FE 개발을 하고 있는 라쿤입니다.&lt;/p&gt;
&lt;p&gt;돌이켜보면 2022년을 기점으로 제품을 본격적으로 만들기 시작하면서, 시간은 정말 눈 깜짝할 사이에 흘러간 것 같아요. 정신없이 달려오다 보니 어느새 꽤 많은 이야기들이 쌓여 있더라고요.&lt;/p&gt;
&lt;p&gt;그동안은 기술적인 내용을 중심으로 글을 써왔는데, 요즘 들어서는 단순한 기술 이야기보다는 &lt;strong&gt;제품을 만들어 가는 과정&lt;/strong&gt;, 그리고 그 안에서 제가 느꼈던 생각들을 한 번쯤은 정리해보고 싶다는 생각이 들었습니다. 그래서 오늘은 조금은 편한 마음으로 이 글을 써보려고 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;하나의 제품 뒤에는 생각보다 많은 것들이 있다&lt;/h2&gt;
&lt;p&gt;물류 로봇 서비스 하나를 운영한다고 하면, 흔히 로봇만 떠올리기 쉬운데요. 실제로는 그 뒤에 정말 많은 서비스들이 함께 움직이고 있습니다.&lt;/p&gt;
&lt;p&gt;로봇과 물류 작업에 대한 정보를 사용자에게 보여주고 상호작용하는 서비스도 있고, 로봇들이 수행하는 작업을 관리자가 지시하고 확인할 수 있는 관리자 서비스도 있습니다. 또 로봇 주행을 전문적으로 관제하는 백오피스 플랫폼도 빠질 수 없죠.&lt;/p&gt;
&lt;p&gt;여기에 실제 로봇을 움직이게 하는 하드웨어와 펌웨어까지 더해지면, 하나의 제품이 동작하기 위해 얼마나 많은 요소들이 맞물려 돌아가는지 실감하게 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;제품을 키운다는 말의 의미&lt;/h2&gt;
&lt;p&gt;예전에 _아이 하나를 키우는 데는 온 마을이 필요하다_는 말을 들은 적이 있는데요. 일을 하다 보니 이 말이 제품에도 그대로 적용된다는 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;제품 하나가 제대로 동작하기 위해서는 여러 팀과 파트가 각자의 역할을 잘 해내는 것도 중요하지만, 그보다 더 중요하게 느껴졌던 건 &lt;strong&gt;서로 얼마나 잘 소통하느냐&lt;/strong&gt;였던 것 같네요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;초반에는 잘 몰라서 더 어려웠다&lt;/h2&gt;
&lt;p&gt;프로젝트 초반을 떠올려보면, 서로에 대해 잘 알지 못하다 보니 자연스럽게 대화가 줄어들었던 시기가 있었습니다. 각자 맡은 일에 집중하다 보니, 굳이 이야기하지 않아도 되겠지 하고 넘어간 부분들도 있었고요.&lt;/p&gt;
&lt;p&gt;그러다 보니 어떤 이슈는 충분히 공유되지 않은 채 각자의 생각대로만 진행되기도 했습니다. 지금 생각해보면, 그때 조금만 더 이야기했더라면 훨씬 수월했을 일들도 많았던 것 같아요.&lt;/p&gt;
&lt;p&gt;그래서 한동안은 모든 구성원이 조금 더 편하게 이야기할 수 있는 분위기를 만들어보자는 목표로, 의무적으로 함께 식사를 하는 시간도 가져봤던 기억이 납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;라포가 만들어준 변화&lt;/h2&gt;
&lt;p&gt;지금 와서 생각해보면, 그 과정에서 가장 크게 느꼈던 건 구성원들 사이에 &lt;strong&gt;라포(Rapport)&lt;/strong&gt;가 생겼다는 점이었습니다.&lt;/p&gt;
&lt;p&gt;사람들이 자주 얼굴을 보고 이야기할 수 있는 환경이 만들어지다 보니, 그날그날의 이슈나 잘 풀리지 않던 문제, 개인적인 생각이나 의견까지도 자연스럽게 오가기 시작했습니다. 딱딱한 회의 자리보다 훨씬 많은 이야기들이 오갔던 것 같아요.&lt;/p&gt;
&lt;p&gt;그때 이렇게 만들어주셨던, 문** 실장님 감사합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;조직도 하나의 시스템 같다는 생각&lt;/h2&gt;
&lt;p&gt;이런 경험들이 쌓이다 보니, 어느 순간부터 조직도 하나의 시스템처럼 느껴지기 시작했습니다. 각자 맡은 역할이 있고, 그 역할들 사이에는 보이지 않는 경계와 연결 방식이 존재하더라고요.&lt;/p&gt;
&lt;p&gt;그러다 문득, 예전에 들었던 &lt;strong&gt;DDD의 Context Mapping&lt;/strong&gt;이 떠올랐습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Context Mapping을 처음 들었을 때&lt;/h2&gt;
&lt;p&gt;예전에 함께 일하던 백엔드 동료가 서비스를 설계하면서 DDD Context Mapping 이야기를 해준 적이 있습니다. 당시에는 ‘아, 이런 관점도 있구나’ 하면서 꽤 인상 깊게 들었던 기억이 나요.&lt;/p&gt;
&lt;p&gt;Context Mapping은 팀, 도메인, 시스템들이 어떤 힘의 관계와 규칙으로 연결되어 있는지를 정의하는 과정이라고 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;소통이 부족하면 생기는 일들&lt;/h2&gt;
&lt;p&gt;실제로 같은 목표를 바라보고 일하고 있음에도 불구하고, 서로 다른 그림을 그린 채 작업을 진행했던 경험이 있었습니다. 충분한 소통 없이 각자 작업을 하다 보니, 결과적으로는 서로의 작업이 맞지 않아 다시 손을 대야 했던 경우도 있었고요.&lt;/p&gt;
&lt;p&gt;그때 느꼈습니다. &lt;strong&gt;이야기하지 않으면, 결국 돌아가게 된다&lt;/strong&gt;는 걸요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;소통 자체가 Context Mapping이다&lt;/h2&gt;
&lt;p&gt;이런 경험들을 돌아보면서 든 생각은, 어쩌면 조직에서의 소통 그 자체가 하나의 Context Mapping 과정이라는 점이었습니다.&lt;/p&gt;
&lt;p&gt;서로 어떤 생각을 하고 있는지, 어디까지를 책임지고 있는지, 어떤 방향을 바라보고 있는지를 계속해서 맞춰가는 과정이 쌓일수록 조직이라는 시스템은 더 안정적이고 견고해지는 것 같았습니다.&lt;/p&gt;
&lt;p&gt;제품 하나를 키우기 위해 온 팀이 필요한 이유도 결국 여기에 있지 않을까 합니다. 단순히 사람이 많아서가 아니라, 각자의 역할과 경계를 이해하고 연결하는 과정이 필요하기 때문이죠.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;앞으로는 기술적인 이야기뿐만 아니라, 이렇게 제품과 조직, 그리고 그 안에서의 경험들도 조금씩 기록해보려고 합니다.&lt;/p&gt;
&lt;p&gt;이 글이 비슷한 환경에서 일하고 있는 분들께 작은 공감이나, 한 번쯤 고개를 끄덕이게 만드는 계기가 되었으면 좋겠습니다.&lt;/p&gt;</description>
      <category>Dev/기타</category>
      <category>Context Mapping</category>
      <category>개발</category>
      <category>제품</category>
      <category>조직</category>
      <category>팀</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/248</guid>
      <comments>https://goforit.tistory.com/248#entry248comment</comments>
      <pubDate>Wed, 11 Feb 2026 00:41:21 +0900</pubDate>
    </item>
    <item>
      <title>[카페] 대전 보문산 근처 대형 베이커리 카페 &amp;lt;멜뷰&amp;gt;</title>
      <link>https://goforit.tistory.com/247</link>
      <description>&lt;h1&gt;대전 보문산 근처 대형 베이커리 카페 &amp;lt;멜뷰&amp;gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갑작스럽게 추워지면서, 낙엽이 보이기 시작했어요~ 정말 가을이 다가온 것 같아요!&lt;br /&gt;가을 풍경의 보문산 주변 뷰를 통창으로 볼 수 있는, 보문산 근처 대형 베이커리 카페 &lt;b&gt;&lt;a href=&quot;https://naver.me/GRo4dKP7&quot;&gt;&amp;lt;멜뷰&amp;gt;&lt;/a&gt;&lt;/b&gt; 에 다녀왔어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;발레파킹..!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대형 카페 특성상, 외진 곳에 있어서 자차로 가야하는 경우가 많은데요. 역시나 보문산 깊은 곳에 있어서 자차로 가시는 것을 추천드립니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 카페 주차장이 있어서, 주차가 가능하고 발레파킹을 해주셔서 차키를 차에 두고 내리면 주차해주셨어요.&lt;br /&gt;주차장이 그렇게 크진 않아서, 최대한 주차하실 수 있도록 서비스를 제공하시는 것 같았어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카페는 지하 2층 ~ 지상 4층까지 있고, 각 층마다 자리 구성이나 그런 부분이 달랐어요.&lt;br /&gt;애견 동반도 가능하니 참고하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특색있는 소금빵&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지상 1층에서 빵, 커피 주문이 가능한데 소금빵 위주로 많이 있고 다른 빵도 조금씩 있더라구요!&lt;br /&gt;밥도 안먹고 빵먹으로 왔기 때문에, 마들렌, 소시지 소금빵, 몽블랑 주문했고, 날씨가 쌀쌀해져서 따아시켰습니다. 헤헤&lt;br /&gt;마들렌 식감도 좋고, 커피도 맛있었요~&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp7aZz/dJMb99SdfEp/Xtcm3W3Y9gUKx5Fj1kHMI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp7aZz/dJMb99SdfEp/Xtcm3W3Y9gUKx5Fj1kHMI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp7aZz/dJMb99SdfEp/Xtcm3W3Y9gUKx5Fj1kHMI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp7aZz%2FdJMb99SdfEp%2FXtcm3W3Y9gUKx5Fj1kHMI0%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;448&quot; height=&quot;796&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통창을 바라보고, 편한 의자에 앉아서 휴식할 수 있어요!&lt;br /&gt;내부 공간 구성이나 의자는 찍지 못했네요. 먹을 생각에..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가을 느낌 루프탑&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4층 루프탑에서 바라본 초가을 느낌을 받을 수 있었어요!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4000&quot; data-origin-height=&quot;2252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vNxNP/dJMcafrmjYe/lR5QQOl4JpZ9Ok8xXjkGs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vNxNP/dJMcafrmjYe/lR5QQOl4JpZ9Ok8xXjkGs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vNxNP/dJMcafrmjYe/lR5QQOl4JpZ9Ok8xXjkGs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvNxNP%2FdJMcafrmjYe%2FlR5QQOl4JpZ9Ok8xXjkGs0%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;4000&quot; height=&quot;2252&quot; data-origin-width=&quot;4000&quot; data-origin-height=&quot;2252&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;2252&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4GORo/dJMcadNQuZk/d1aWNUHIy0SKxTlU1cE24k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4GORo/dJMcadNQuZk/d1aWNUHIy0SKxTlU1cE24k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4GORo/dJMcadNQuZk/d1aWNUHIy0SKxTlU1cE24k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4GORo%2FdJMcadNQuZk%2Fd1aWNUHIy0SKxTlU1cE24k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;799&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&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;4000&quot; data-origin-height=&quot;2252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eotY9M/dJMcai2GMl4/rWKaBb8fWfJASyM9AUfTkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eotY9M/dJMcai2GMl4/rWKaBb8fWfJASyM9AUfTkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eotY9M/dJMcai2GMl4/rWKaBb8fWfJASyM9AUfTkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeotY9M%2FdJMcai2GMl4%2FrWKaBb8fWfJASyM9AUfTkk%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;4000&quot; height=&quot;2252&quot; data-origin-width=&quot;4000&quot; data-origin-height=&quot;2252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>취미/카페</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/247</guid>
      <comments>https://goforit.tistory.com/247#entry247comment</comments>
      <pubDate>Mon, 3 Nov 2025 01:10:50 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/11/01 - 8.00km (49:09, 6'08, 165)</title>
      <link>https://goforit.tistory.com/246</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvpGR5/dJMcajUPxco/WPGXKtC8JuMrrEQKEHxezK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvpGR5/dJMcajUPxco/WPGXKtC8JuMrrEQKEHxezK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvpGR5/dJMcajUPxco/WPGXKtC8JuMrrEQKEHxezK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvpGR5%2FdJMcajUPxco%2FWPGXKtC8JuMrrEQKEHxezK%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025/11/01 - 8.00km (49:09, 6'08, 165)&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8km 달성!&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초창기에도 8km 뛰었는데, 확실히 케이던스, 미드풋 신경써서 달려서 그런지 다리 발에 크게 부담이 없었음~&lt;/p&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/246</guid>
      <comments>https://goforit.tistory.com/246#entry246comment</comments>
      <pubDate>Mon, 3 Nov 2025 00:24:05 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/30 - 5.20km (31:32, 6'03, 170)</title>
      <link>https://goforit.tistory.com/245</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kHJgQ/dJMcagqgEmQ/2R7ODDkn5kHzyT424j1TR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kHJgQ/dJMcagqgEmQ/2R7ODDkn5kHzyT424j1TR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kHJgQ/dJMcagqgEmQ/2R7ODDkn5kHzyT424j1TR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkHJgQ%2FdJMcagqgEmQ%2F2R7ODDkn5kHzyT424j1TR0%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025/10/30 - 5.20km (31:32, 6'03, 170)&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;케이던스 170 달성&lt;/h3&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/245</guid>
      <comments>https://goforit.tistory.com/245#entry245comment</comments>
      <pubDate>Mon, 3 Nov 2025 00:19:04 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/28 - 5.10km (31:05, 6'05, 165)</title>
      <link>https://goforit.tistory.com/244</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eeF7GC/dJMcahvWuRw/9f51TTdkk7OpN3TuetKoC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eeF7GC/dJMcahvWuRw/9f51TTdkk7OpN3TuetKoC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eeF7GC/dJMcahvWuRw/9f51TTdkk7OpN3TuetKoC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeeF7GC%2FdJMcahvWuRw%2F9f51TTdkk7OpN3TuetKoC1%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025/10/28&amp;nbsp;-&amp;nbsp;5.10km&amp;nbsp;(31:05,&amp;nbsp;6'05,&amp;nbsp;165)&lt;/h3&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/244</guid>
      <comments>https://goforit.tistory.com/244#entry244comment</comments>
      <pubDate>Mon, 3 Nov 2025 00:16:08 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/23 - 5.16km (31:18, 6'04, 165)</title>
      <link>https://goforit.tistory.com/243</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnhbpp/dJMcagRkXGD/peNlI9HGmtTuJvVcAKsc9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnhbpp/dJMcagRkXGD/peNlI9HGmtTuJvVcAKsc9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnhbpp/dJMcagRkXGD/peNlI9HGmtTuJvVcAKsc9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnhbpp%2FdJMcagRkXGD%2FpeNlI9HGmtTuJvVcAKsc9K%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025/10/23&amp;nbsp;-&amp;nbsp;5.16km&amp;nbsp;(31:18,&amp;nbsp;6'04,&amp;nbsp;165)&lt;/h3&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/243</guid>
      <comments>https://goforit.tistory.com/243#entry243comment</comments>
      <pubDate>Mon, 3 Nov 2025 00:13:51 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/22 - 5.00km (31:19, 6'15, 165)</title>
      <link>https://goforit.tistory.com/242</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/474LB/dJMcahQfhXV/W6obgAXNWkhAAczkGdAOd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/474LB/dJMcahQfhXV/W6obgAXNWkhAAczkGdAOd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/474LB/dJMcahQfhXV/W6obgAXNWkhAAczkGdAOd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F474LB%2FdJMcahQfhXV%2FW6obgAXNWkhAAczkGdAOd1%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025/10/22 - 5.00km (31:19, 6'15, 165)&lt;/h3&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/242</guid>
      <comments>https://goforit.tistory.com/242#entry242comment</comments>
      <pubDate>Mon, 3 Nov 2025 00:11:43 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/16 - 5.24km (32:36, 6'13, 162)</title>
      <link>https://goforit.tistory.com/241</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHF5Jv/dJMcabJgh67/2Fcr7ArIebGvCJo6lXgwQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHF5Jv/dJMcabJgh67/2Fcr7ArIebGvCJo6lXgwQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHF5Jv/dJMcabJgh67/2Fcr7ArIebGvCJo6lXgwQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHF5Jv%2FdJMcabJgh67%2F2Fcr7ArIebGvCJo6lXgwQk%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025/10/16 - 5.24km (32:36, 6'13, 162)&lt;/h3&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/241</guid>
      <comments>https://goforit.tistory.com/241#entry241comment</comments>
      <pubDate>Mon, 3 Nov 2025 00:09:38 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/14 - 5.61km (37:42, 6'42)</title>
      <link>https://goforit.tistory.com/240</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lj81i/dJMcafx7M9J/yr1Be2jelNqXUdk3SuDlv1/tfile.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lj81i/dJMcafx7M9J/yr1Be2jelNqXUdk3SuDlv1/tfile.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lj81i/dJMcafx7M9J/yr1Be2jelNqXUdk3SuDlv1/tfile.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flj81i%2FdJMcafx7M9J%2Fyr1Be2jelNqXUdk3SuDlv1%2Ftfile.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;2025/10/14 - 5.61km (37:42, 6'42)&lt;/h3&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/240</guid>
      <comments>https://goforit.tistory.com/240#entry240comment</comments>
      <pubDate>Sun, 2 Nov 2025 11:50:30 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/10 - 5.36km (35:39, 6'38, 160)</title>
      <link>https://goforit.tistory.com/239</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPGPoQ/dJMcake74Eo/6H7cjDDJldI6WRnTYE6EmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPGPoQ/dJMcake74Eo/6H7cjDDJldI6WRnTYE6EmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPGPoQ/dJMcake74Eo/6H7cjDDJldI6WRnTYE6EmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPGPoQ%2FdJMcake74Eo%2F6H7cjDDJldI6WRnTYE6EmK%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025/10/10&amp;nbsp;-&amp;nbsp;5.36km&amp;nbsp;(35:39,&amp;nbsp;6'38,&amp;nbsp;160)&lt;/h3&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/239</guid>
      <comments>https://goforit.tistory.com/239#entry239comment</comments>
      <pubDate>Sun, 2 Nov 2025 06:10:10 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/09 - 4.05km (27:03, 6`40, 165)</title>
      <link>https://goforit.tistory.com/238</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkBHgQ/dJMcagqgpRu/3rnkvKOPJS9d8E2zVUyev0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkBHgQ/dJMcagqgpRu/3rnkvKOPJS9d8E2zVUyev0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkBHgQ/dJMcagqgpRu/3rnkvKOPJS9d8E2zVUyev0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkBHgQ%2FdJMcagqgpRu%2F3rnkvKOPJS9d8E2zVUyev0%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튭 뮤직에서 케이던스 음악이 있는데 맞춰가니 조금 좋아진 느낌??&lt;/p&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/238</guid>
      <comments>https://goforit.tistory.com/238#entry238comment</comments>
      <pubDate>Sun, 2 Nov 2025 06:07:11 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/07 - 2.12km (19:26, 8`45, 163)</title>
      <link>https://goforit.tistory.com/237</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ehq0IU/dJMcae0hL7v/Jhd3oW4UPmxX7Y9LpNfTb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ehq0IU/dJMcae0hL7v/Jhd3oW4UPmxX7Y9LpNfTb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ehq0IU/dJMcae0hL7v/Jhd3oW4UPmxX7Y9LpNfTb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fehq0IU%2FdJMcae0hL7v%2FJhd3oW4UPmxX7Y9LpNfTb1%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2025/10/07&amp;nbsp;-&amp;nbsp;2.12km&amp;nbsp;(19:26,&amp;nbsp;8`45,&amp;nbsp;163)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비가 오늘 날이라, 단지에서 뺑뺑이~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;케이던스를 늘리고자 집중&lt;/p&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/237</guid>
      <comments>https://goforit.tistory.com/237#entry237comment</comments>
      <pubDate>Sun, 2 Nov 2025 06:04:06 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/05 - 4.01km (29:28, 7`21, 158)</title>
      <link>https://goforit.tistory.com/236</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpuKCa/dJMcac2s7tk/B1Qqq4okFCGVKYPK5VqlsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpuKCa/dJMcac2s7tk/B1Qqq4okFCGVKYPK5VqlsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpuKCa/dJMcac2s7tk/B1Qqq4okFCGVKYPK5VqlsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpuKCa%2FdJMcac2s7tk%2FB1Qqq4okFCGVKYPK5VqlsK%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2025/10/05&amp;nbsp;-&amp;nbsp;4.01km&amp;nbsp;(29:28,&amp;nbsp;7`21,&amp;nbsp;158)&lt;/h2&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/236</guid>
      <comments>https://goforit.tistory.com/236#entry236comment</comments>
      <pubDate>Sun, 2 Nov 2025 05:59:52 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/10/04 - 3.03km (19:10, 620)</title>
      <link>https://goforit.tistory.com/235</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yJ4qh/dJMb99Sc01N/824PKkDORh5iMewjNbQRR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yJ4qh/dJMb99Sc01N/824PKkDORh5iMewjNbQRR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yJ4qh/dJMb99Sc01N/824PKkDORh5iMewjNbQRR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyJ4qh%2FdJMb99Sc01N%2F824PKkDORh5iMewjNbQRR1%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025/10/04&amp;nbsp;-&amp;nbsp;3.03km&amp;nbsp;(19:10,&amp;nbsp;620)&lt;/h3&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/235</guid>
      <comments>https://goforit.tistory.com/235#entry235comment</comments>
      <pubDate>Sun, 2 Nov 2025 05:56:05 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/09/20 - 5.18km (38:32, 725)</title>
      <link>https://goforit.tistory.com/234</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V89YL/dJMcaaXSVQD/ihIgyoZd5KbrIg6DIHRLu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V89YL/dJMcaaXSVQD/ihIgyoZd5KbrIg6DIHRLu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V89YL/dJMcaaXSVQD/ihIgyoZd5KbrIg6DIHRLu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV89YL%2FdJMcaaXSVQD%2FihIgyoZd5KbrIg6DIHRLu1%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2025/09/20 - 5.18km (38:32, 725)&lt;/h2&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/234</guid>
      <comments>https://goforit.tistory.com/234#entry234comment</comments>
      <pubDate>Sun, 2 Nov 2025 05:52:13 +0900</pubDate>
    </item>
    <item>
      <title>[러닝] 2025/09/18 - 3.36km (22:56, 821)</title>
      <link>https://goforit.tistory.com/233</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdZoBy/dJMcahW0E67/EtuWdc9OMIWuktHltVBNOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdZoBy/dJMcahW0E67/EtuWdc9OMIWuktHltVBNOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdZoBy/dJMcahW0E67/EtuWdc9OMIWuktHltVBNOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdZoBy%2FdJMcahW0E67%2FEtuWdc9OMIWuktHltVBNOK%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;903&quot; height=&quot;903&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025/09/18&amp;nbsp;-&amp;nbsp;3.36km&amp;nbsp;(22:56,&amp;nbsp;821)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 러닝 시작!&lt;/p&gt;</description>
      <category>취미/러닝</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/233</guid>
      <comments>https://goforit.tistory.com/233#entry233comment</comments>
      <pubDate>Sun, 2 Nov 2025 05:44:12 +0900</pubDate>
    </item>
    <item>
      <title>ETF 알아보기</title>
      <link>https://goforit.tistory.com/232</link>
      <description>&lt;h1&gt;ETF 알아보기&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 그냥 시간을 흘려보내지 말고 흐르는 시간을 최대한 활용하여 자산 형성을 위한 전략이 필요한 시점이네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자산 형성을 위해서 금융 상품이 다양하게 있지만 대표적으로 주식이 현재 인기가 많은 것 같더군요. 주식도 직접 투자하는 방법도 있지만 직접 투자를 위해서 공부하고 시간을 보내는 것이 어려워 보통은 ETF를 이용하시는 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIpWfp/dJMcabvIMTh/0KlWlrvfCLC6uUp3mVbwqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIpWfp/dJMcabvIMTh/0KlWlrvfCLC6uUp3mVbwqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIpWfp/dJMcabvIMTh/0KlWlrvfCLC6uUp3mVbwqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIpWfp%2FdJMcabvIMTh%2F0KlWlrvfCLC6uUp3mVbwqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&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;실제로 안정적인 ETF 적립식 장기 투자의 경우, 높은 수익률을 꾸준히 기록하고 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;매달 50만 원씩 10년(120개월) 동안 총 6000만원을 투자하는데 S&amp;amp;P500 ETF와 예적금을 비교한&lt;br /&gt;시뮬레이션 결과&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;ETF (연 12%)&lt;/th&gt;
&lt;th&gt;예적금 (연 2.5%)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;총 투자금&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6,000만 원&lt;/td&gt;
&lt;td&gt;6,000만 원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;10년 후 총액&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 &lt;b&gt;1억 1,808만 원&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 &lt;b&gt;6,754만 원&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;순수익&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 &lt;b&gt;5,808만 원&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 &lt;b&gt;754만 원&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;연평균 수익률&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;위험 수준&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;높음 (시장 변동성)&lt;/td&gt;
&lt;td&gt;낮음 (원금 보장)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대략 총액은 2배 정도 차이가 발생하며 순수익은 7.7배 정도로 차이가 많이 나네요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 원금 위험이 존재하지만 안정적인 ETF 상품을 적립식으로 장기 투자한 경우 위험 부담이 분산되는 것을 생각하면 높음 수익률이 더욱 매력적으로 다가오네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 ETF는 뭘까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ETF란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;여러 종목을 묶은 인덱스펀드를 주식처럼 거래할 수 있는 상품&quot;&lt;/b&gt; 으로 Exchange Traded Fund (ETF, 상장지수펀드)라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&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;친구가 좋아하는 과일을 구매하기 위한 방법은 아래 2가지가 될 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인 판단으로 예측하여 사과 하나(삼성전자), 바나나 하나(SK하이닉스) 이런 식으로 구매한다면, 한 종류의 과일(개별 종목 주식 구매)만 구매하는 방식이 있지요. (일반적인 주식 구매)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과일 가게에서 잘나가는 과일들 사과3개, 바나나2개, 포도1개, 오렌지1개 등 조금씩 잘 분배된 &lt;b&gt;&quot;과일 바구니&quot;&lt;/b&gt; 를 구매하는 방식이 있지요. (ETF)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과일 가게 주인은 어떻게 과일 바구니를 구성할까요?&lt;br /&gt;요즘 잘나가는 과일의 &lt;b&gt;&quot;통계지수&quot;&lt;/b&gt; 를 확인하면서 일반적인 사람이 좋아할 만한 과일을 고를 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 아무리 잘나가는 과일이라고 할지라도 시간에 따라서 달라지기 때문에, 과일 가게 주인은 사과바구니가 아니라, 1등(사과), 2등(바나나), 3등(포도) .. 이런 식으로 잘 몇 개씩 살지&lt;b&gt;&quot;분배&quot;&lt;/b&gt; 하여 섞은 과일 바구니로 만듭니다. 이렇게 담은 과일들은 누가 사더라도 받는 사람이 좋아하는 과일이 있을 확률이 높을 수 밖에 없죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 끝내지 않고 과일 바구니는 마트, 동네 과일 가게 등 시장에 나와서 편하게 살 수 있고 산 이후에도 사실 그 친구가 좋아하지 않는 과일이었거나, 지금 당장 그 과일 바구니가 더 필요한 사람이 나타났다면 언제든지 &lt;b&gt;&quot;거래&quot;&lt;/b&gt; 하여 다시 팔 수도 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 정리할 수 있을 것 같아요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ETF = 과일 바구니
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지수 추종 (KOSPI200, S&amp;amp;P500) = 잘 나가는 과일 트렌드 통계&lt;/li&gt;
&lt;li&gt;분산 투자 (삼성전자 10주, SK하이닉스 15주) = 좋아할 만한 과일들로 나누기&lt;/li&gt;
&lt;li&gt;주식처럼 거래 = 다른 사람에게 팔기 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ETF 장점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 과일 바구니에 비유한 것과 같이 생각해보면 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;좋아하는 과일이 하나쯤은 있을 확률이 높아서 모든 과일을 싫어하진 않을 것이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;=&amp;gt; 모두 다 망할 확률이 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이미 좋아할 과일들로만 알맞은 개수로 구성되어 딱히 고민할 필요가 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;=&amp;gt; 고민할 필요 없이 잘 나가는 기업만 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;어디든지 과일바구니를 팔고 있어서 실시간으로 사기 편하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;=&amp;gt; 주식시장에서 ETF를 바로 사고 팔 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ETF 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETF는 기초 자산과 운용 방식에 따라서 종류가 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과일이 아닌 다른 물품의 바구니 일 수도 있고, 해당 물품의 종류와 비율, 물품의 선호도에 대해서 적극적, 소극적 대응 등에 따라서 다양하게 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기초 자산별 ETF
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주식형 : 기업 주식&lt;/li&gt;
&lt;li&gt;채권형 : 국채, 회사채&lt;/li&gt;
&lt;li&gt;원자재 : 금, 은, 원유 등의 실물 자산 가격&lt;/li&gt;
&lt;li&gt;통화 : 달러, 유로, 엔화 등 환율&lt;/li&gt;
&lt;li&gt;부동산(리츠) : 부동산 투자회사 주식&lt;/li&gt;
&lt;li&gt;혼합 : 주식 + 채권 + 금 + 리츠 등 혼합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;운용 방식별 ETF
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패시브 : 지수 그대로 추종&lt;/li&gt;
&lt;li&gt;액티브 : 운용사에서 시장 수익 초과를 위해 적극 운용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;수익 구조/투자 전략별 ETF
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배당형 : 배당 수익을 주기적으로 지급&lt;/li&gt;
&lt;li&gt;레버리지/인버스 : 지수 수익률2배/반대로 추종&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETF에 대해서 간략하게 알아봤어요~&lt;br /&gt;ETF투자를 진행하면 이자소득이 발생하고 결국 이자소득세로 15.4 세율이 적용되는데요.&lt;br /&gt;다음에는 이러한 세금을 줄일 수 있는 절세 계좌에 대해서 알아보겠습니다!&lt;/p&gt;</description>
      <category>금융</category>
      <category>ETF</category>
      <category>S&amp;amp;P500</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/232</guid>
      <comments>https://goforit.tistory.com/232#entry232comment</comments>
      <pubDate>Sun, 2 Nov 2025 02:17:26 +0900</pubDate>
    </item>
    <item>
      <title>요즘 Nodejs 개발 환경 구성하기</title>
      <link>https://goforit.tistory.com/231</link>
      <description>&lt;h1&gt;요즘 Nodejs 개발 환경 구성하기&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;1152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cm7fmj/dJMcabJezdu/HPq0SKonwXJ9Ikd5X2yIzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cm7fmj/dJMcabJezdu/HPq0SKonwXJ9Ikd5X2yIzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cm7fmj/dJMcabJezdu/HPq0SKonwXJ9Ikd5X2yIzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcm7fmj%2FdJMcabJezdu%2FHPq0SKonwXJ9Ikd5X2yIzK%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;1270&quot; height=&quot;1152&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;1152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nodejs는 프론트엔드 개발자가 가장 먼저 설치해야 할 javascript 런타임 프로그램 입니다. 옛날에는 &lt;a href=&quot;https://nodejs.org/ko&quot;&gt;공식 홈페이지&lt;/a&gt;에 들어가서 설치하면 설치 파일을 다운로드 받아서 설치하였습니다. 하지만 요즘에는 Nodejs 버전을 관리하는 커맨드 라인 툴을 통해서 받는 형식이 보편화되고 있습니다.&lt;br /&gt;이번 시간에는 예전과 달리 간단하게 Nodejs를 설치하고, 버전을 관리하는 방법에 대해서 이야기 해보려고 합니다.&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;CLI로 설치하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2539&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coLwxQ/dJMcagw0vYI/NYkizW6yu8PqQTiw7cTd51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coLwxQ/dJMcagw0vYI/NYkizW6yu8PqQTiw7cTd51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coLwxQ/dJMcagw0vYI/NYkizW6yu8PqQTiw7cTd51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoLwxQ%2FdJMcagw0vYI%2FNYkizW6yu8PqQTiw7cTd51%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;2539&quot; height=&quot;1034&quot; data-origin-width=&quot;2539&quot; data-origin-height=&quot;1034&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;Nodejs 공식 다운로드 페이지를 확인하면, 설치하려는 Nodejs 버전, OS, Nodejs 버전 매니저 및 패키지 매니저를 설정하여 다운로드 할 수 있는 방법을 CLI로 아주 친절하게 보여주고 있습니다.&lt;br /&gt;초창기에는 정말 단순하게 설치 파일 다운로드 받아서 설치하는 방식이었습니다. 정말 편해졌어요~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVamAL/dJMcabJezdB/cUBKTJBtap9wr82z2zULiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVamAL/dJMcabJezdB/cUBKTJBtap9wr82z2zULiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVamAL/dJMcabJezdB/cUBKTJBtap9wr82z2zULiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVamAL%2FdJMcabJezdB%2FcUBKTJBtap9wr82z2zULiK%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;1280&quot; height=&quot;547&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 아래와 같이 CLI를 구성해서 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# fnm 다운로드 및 설치:
curl -o- https://fnm.vercel.app/install | bash

# Node.js 다운로드 및 설치:
fnm install 22

# Verify the Node.js version:
node -v # Should print &quot;v22.21.0&quot;.

# pnpm 다운로드 및 설치:
corepack enable pnpm

# pnpm 버전 확인:
pnpm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 다운로드 문서를 자세히 보면 Nodejs에 버전에 LTS(&lt;a href=&quot;https://nodejs.org/en/about/previous-releases#nodejs-releases&quot;&gt;Long term support&lt;/a&gt;)가 보입니다. 그리고 옆에 OS 선택에는 Windows, Mac, Linux 등이 있네요. 저는 WSL 환경을 활용하기 때문에 Linux로 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 옆에는 Nodejs 버전 매니저들이 아래와 같이 나열되어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Official : nvm, fnm, Docker&lt;/li&gt;
&lt;li&gt;Unofficial : Brew, Chocolately&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초창기에는 개발 환경이 Windows라서 &lt;a href=&quot;https://chocolatey.org/&quot;&gt;Chocolatey&lt;/a&gt;를 활용하다가 초창기 nodejs 전용 버전 매니저인 &lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;nvm&lt;/a&gt;을 활용하려고 했으나, windows는 지원하지 않아서 &lt;a href=&quot;https://github.com/coreybutler/nvm-windows&quot;&gt;nvm-windows&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;요즘에는 WSL에서 작업을 하다보니 편하게 nvm을 쓰다가 최근에는 Rust로 만들어서 더 빠르다고 하는&lt;a href=&quot;https://github.com/Schniz/fnm&quot;&gt;fnm&lt;/a&gt;을 쓰고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 패키지 매니저로 &lt;a href=&quot;https://www.npmjs.com/&quot;&gt;npm&lt;/a&gt;을 활용했지만, 요즘엔 &lt;a href=&quot;https://yarnpkg.com/&quot;&gt;yarn&lt;/a&gt;, yarn berrry를 거쳐서 &lt;a href=&quot;https://pnpm.io/ko/&quot;&gt;pnpm&lt;/a&gt;을 사용하고 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;fnm&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Schniz/fnm&quot;&gt;fnm&lt;/a&gt;은 Rust로 만들어진 빠르고 간편한 Nodejs 버전 매니저입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;429&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enJdVj/dJMcagw0vYJ/cqlLME0Nx3hkl2l84peD8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enJdVj/dJMcagw0vYJ/cqlLME0Nx3hkl2l84peD8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enJdVj/dJMcagw0vYJ/cqlLME0Nx3hkl2l84peD8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FenJdVj%2FdJMcagw0vYJ%2FcqlLME0Nx3hkl2l84peD8k%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;740&quot; height=&quot;429&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;429&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;아래 주로 사용하는 명령어를 통해서 node를 설치하거나, 편하게 버전을 변경할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Schniz/fnm/blob/master/docs/commands.md#fnm-list-remote&quot;&gt;fnm list-remote&lt;/a&gt; : 설치 가능한 node 버전 확인&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Schniz/fnm/blob/master/docs/commands.md#fnm-list&quot;&gt;fnm list&lt;/a&gt; : 현재 설치된 node 리스트 확인&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Schniz/fnm/blob/master/docs/commands.md#fnm-list&quot;&gt;fnm install&lt;/a&gt;: 특정 버전의 node를 설치&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Schniz/fnm/blob/master/docs/commands.md#fnm-use&quot;&gt;fnm use&lt;/a&gt;: 특정 버전의 node를 사용&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Schniz/fnm/blob/master/docs/commands.md#fnm-use&quot;&gt;fnm default&lt;/a&gt;: 특정 버전을 기본 node 버전으로 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fnm 설치 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서의 안내와 같이 fnm Node 버전 매니저를 설치하는 과정에서 &lt;code&gt;Not installing fnm due to missing dependencies.&lt;/code&gt;라는 에러가 표시되면서 정상적으로 설치되지 않는 현상이 있습니다. 아래와 같은 에러가 발생할 때는 unzip을 설치해주시기 바랍니다!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Schniz/fnm/issues/1385&quot;&gt;Github fnm issue : Not installing fnm due to missing dependencies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;
$ sudo curl -fsSL https://fnm.vercel.app/install | bash

Checking dependencies for the installation script...
Checking availability of curl... OK!
Checking availability of unzip... Missing!
Not installing fnm due to missing dependencies.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;
sudo apt-get install unzip
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;corepack&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 CLI를 확인하면, &lt;a href=&quot;https://github.com/nodejs/corepack&quot;&gt;corepack&lt;/a&gt;으로 설치된다는 것을 확인할 수 있습니다.&lt;br /&gt;Corepack은 Nodejs와 개발 중에 사용할 패키지 매니저 사이의 브릿지 역할을 하는 런타임 종속성 없는 Nodejs 스크립트라고 이야기하고 있어요. yarn, npm, pnpm 설치 없이 사용 가능하게 만들어줍니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 보통 우리 프로젝트에서는 패키지 매니저로 어떤 것 사용하니 이거 설치하세요. 이런 식이였습니다. 프로젝트마다 다르고 프로젝트로 이동 할 때마다 새로 설치하거나 하는 번거러움이 있었습니다. 그런데 프로젝트에 사용하는 어떤 버전의 패키지 매니저인지 넣어서 특정 패키지 매니저를 자동으로 설치하여 활용할 수 있도록 편하게 개발 환경 통합에 기여하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 예시) package.json
{
  &quot;packageManager&quot;: &quot;yarn@3.2.3+sha224.953c8233f7a92884eee2de69a1b92d1f2ec1655e66d08071ba9a02fa&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;corepack enable&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/nodejs/corepack?tab=readme-ov-file#default-installs&quot;&gt;요즘 사용하는 Nodejs는 보통 corepack이 기본적으로 같이 있어서&lt;/a&gt; 바로 사용할 수 있어요. 이전에 전역으로 설치한 패키지 매니저가 있다면 삭제하시고 진행하시면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;마치며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 흐를수록 파편화 되어 있던 개발 환경 설정들이 통합되고 간단하게 변경되고 있는 것을 느꼈습니다. 초창기 기술 스택은 자유롭게 빠르게 발전해 온 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 빠르게 발전해 오면서 열정 넘치는 춘추전국 시대 기술 생태계가 점점 표준화되고 정돈되면서 성숙한 기술 생태계로 합의점을 이루어 가는 것 같습니다. 이로서 개발자들은 더욱 안정적인 개발 환경 및 기술을 가져가 정말 중요한 비즈니스 서비스에 집중 할 수 있게 된 것 같습니다.&lt;/p&gt;</description>
      <category>Dev/Node.js</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/231</guid>
      <comments>https://goforit.tistory.com/231#entry231comment</comments>
      <pubDate>Tue, 28 Oct 2025 01:22:55 +0900</pubDate>
    </item>
    <item>
      <title>WSL 환경 구성하기</title>
      <link>https://goforit.tistory.com/230</link>
      <description>&lt;h1&gt;WSL 환경 구성하기&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윈도우 환경에서 개발을 진행하거나, 시스템을 돌리는 경우 호환성 및 리소스 효율성이 떨어지는 등의 문제가 발생하고 있고 Docker를 돌리거나 쉘 파일을 돌려야 하는 경우 WSL 설치가 필수적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 대부분의 클라우드 서버 등의 서비스는 Linux 기반의 OS에서 돌리는 경우가 많습니다.&lt;br /&gt;그리고, Github Action을 작성할 때도 bash 스크립트를 작성하는 경우가 많기 때문에 해당 환경과 친해질 필요가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 Mac쓰면 됩니다. 그러기엔 Mac에 적응하기에 너무나 늦어버려서 저는 Windows를 씁니다.&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;WSL ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL(Linux용 Windows 하위 시스템)은 Windows 운영 체제 내에서 Linux 환경을 실행할 수 있도록 해주는 호환성 계층입니다. 쉽게 말해, Windows를 사용하면서 별도의 가상 머신(VM)이나 듀얼 부팅 없이 리눅스 명령줄 도구, 애플리케이션, 그리고 다양한 리눅스 배포판(예: Ubuntu, Debian 등)을 사용할 수 있게 해주는 기능입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/windows/wsl/&quot;&gt;MS : Linux용 Windows 하위 시스템 설명서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 Powershell에 입력하면 됩니다.&lt;br /&gt;아래 명령어는 &lt;code&gt;Ubuntu-22.04&lt;/code&gt;를 사용하는 것을 전제로 입력되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/windows/wsl/install&quot;&gt;MS: WSL을 사용하여 Windows에 Linux를 설치하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;
# wsl 설치
wsl --install Ubuntu-22.04

# wsl: 레거시 배포 등록을 사용하고 있습니다. tar 기반 배포를 대신 사용하는 것이 좋습니다.
# 다운로드 중: Ubuntu 22.04 LTS
# Ubuntu 22.04 LTS이(가) 다운로드되었습니다.
# 배포가 설치되었습니다. 'wsl.exe -d Ubuntu 22.04 LTS'을(를) 통해 시작할 수 있습니다.
# Ubuntu 22.04 LTS 시작하는 중...
# Installing, this may take a few minutes...
# Please create a default UNIX user account. The username does not need to match your Windows username.
# For more information visit: https://aka.ms/wslusers
# Enter new UNIX username: user1
# wsl: Failed to start the systemd user session for 'root'. See journalctl for more details.
# New password:
# Retype new password:
# passwd: password updated successfully
# Installation successful!
# To run a command as administrator (user &quot;root&quot;), use &quot;sudo &amp;lt;command&amp;gt;&quot;.
# See &quot;man sudo_root&quot; for details.
# Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.6.87.2-microsoft-standard-WSL2 x86_64)
#  * Documentation:  https://help.ubuntu.com
#  * Management:     https://landscape.canonical.com
#  * Support:        https://ubuntu.com/pro
#  System information as of Wed Oct 22 16:15:33 KST 2025
#   System load:  0.07                Processes:             30
#   Usage of /:   0.1% of 1006.85GB   Users logged in:       0
#   Memory usage: 1%                  IPv4 address for eth0: 172.31.149.119
#   Swap usage:   0%
# This message is shown once a day. To disable it please create the
# /home/user1/.hushlogin file.



# 설치 가능한 Linux 배포판 조회
wsl.exe --list --online

# 다음은 설치할 수 있는 유효한 배포 목록입니다.
# 'wsl.exe --install &amp;lt;Distro&amp;gt;'을 사용하여 설치합니다.

# NAME                            FRIENDLY NAME
# AlmaLinux-8                     AlmaLinux OS 8
# AlmaLinux-9                     AlmaLinux OS 9
# AlmaLinux-Kitten-10             AlmaLinux OS Kitten 10
# AlmaLinux-10                    AlmaLinux OS 10
# Debian                          Debian GNU/Linux
# FedoraLinux-42                  Fedora Linux 42
# SUSE-Linux-Enterprise-15-SP6    SUSE Linux Enterprise 15 SP6
# SUSE-Linux-Enterprise-15-SP7    SUSE Linux Enterprise 15 SP7
# Ubuntu                          Ubuntu
# Ubuntu-24.04                    Ubuntu 24.04 LTS
# archlinux                       Arch Linux
# kali-linux                      Kali Linux Rolling
# openSUSE-Tumbleweed             openSUSE Tumbleweed
# openSUSE-Leap-16.0              openSUSE Leap 16.0
# Ubuntu-20.04                    Ubuntu 20.04 LTS
# Ubuntu-22.04                    Ubuntu 22.04 LTS
# OracleLinux_7_9                 Oracle Linux 7.9
# OracleLinux_8_10                Oracle Linux 8.10
# OracleLinux_9_5                 Oracle Linux 9.5
# openSUSE-Leap-15.6              openSUSE Leap 15.6
&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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QL7nN/dJMb9WrT8HW/2WVpQpRuDjC1xP7My5kZV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QL7nN/dJMb9WrT8HW/2WVpQpRuDjC1xP7My5kZV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QL7nN/dJMb9WrT8HW/2WVpQpRuDjC1xP7My5kZV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQL7nN%2FdJMb9WrT8HW%2F2WVpQpRuDjC1xP7My5kZV0%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;1630&quot; height=&quot;1036&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL 관련해서 일부 네트워크, 파일 시스템, 리소스에 관한 설정을 진행해야 하는 경우가 있을 수 있습니다.&lt;br /&gt;.wslconfig 파일 또는 wsl.conf 파일의 값을 변경하여 설정할 수 있습니다.&lt;br /&gt;그런데 최근에는 GUI형식으로 제공하고 있더라구요. 편하게 설정할 수 있게 되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/windows/wsl/wsl-config#main-wsl-settings&quot;&gt;MS : WSL의 고급 설정 구성&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bucKGr/dJMb9OAIFxb/KCCzdZt0BOfEl5LfUTDbdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bucKGr/dJMb9OAIFxb/KCCzdZt0BOfEl5LfUTDbdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bucKGr/dJMb9OAIFxb/KCCzdZt0BOfEl5LfUTDbdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbucKGr%2FdJMb9OAIFxb%2FKCCzdZt0BOfEl5LfUTDbdk%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;1284&quot; height=&quot;789&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;789&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;769&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vf5IA/dJMb9Xdhjsr/Y7u7ydKgUkq6FZlnPQQCr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vf5IA/dJMb9Xdhjsr/Y7u7ydKgUkq6FZlnPQQCr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vf5IA/dJMb9Xdhjsr/Y7u7ydKgUkq6FZlnPQQCr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVf5IA%2FdJMb9Xdhjsr%2FY7u7ydKgUkq6FZlnPQQCr1%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;769&quot; height=&quot;720&quot; data-origin-width=&quot;769&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 시스템 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL에서는 기본적으로 windows의 파일 시스템, linux의 파일 시스템 모두 경로로 확인하여 사용할 수 있습니다. 하지만 성능이 떨어지기 때문에 wsl을 활용하신다면, linux의 파일 시스템을 활용하는 것 좋다고 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/windows/wsl/filesystems&quot;&gt;Windows 및 Linux 파일 시스템에서 작업&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wsl 터미널에서 아래와 같은 경로로 파일 시스템에 접근할 수 있습니다. 성능을 위해서 home 디렉토리 기준의 사용자 디렉토리에서 프로젝트를 만들고 작업해야겠죠!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;windows 파일 시스템 : &lt;code&gt;/mnt/c/Users/UserName/Something&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;linux 파일 시스템 : /home/UserName/Something&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탐색기 GUI로 이해하면 아래와 같이 Linux라는 폴더가 따로 생겨있고 내부에 Ubuntu &amp;gt; home이 존재하는 것을 볼 수 있습니다. 모든 작업들은 해당 경로에서 진행하면 되겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mA2Ln/dJMb9OAIFx3/DDslpFocOnl3lA55gtFs2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mA2Ln/dJMb9OAIFx3/DDslpFocOnl3lA55gtFs2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mA2Ln/dJMb9OAIFx3/DDslpFocOnl3lA55gtFs2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmA2Ln%2FdJMb9OAIFx3%2FDDslpFocOnl3lA55gtFs2K%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;610&quot; height=&quot;396&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팁을 드리자면, 터미널에서 주로 개발하다가 탐색기와 같은 GUI로 바로 내부 내용을 확인하고 옮기는 작업을 진행하고 싶은 경우가 있는데요. 이럴 때 터미널에서 아래 명령을 작성하면 해당 경로의 파일 탐색기 창이 열립니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;
explorer.exe .
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;터미널 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 환경의 터미널을 Windows에서 접속하는 방법은 두 가지 방법이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/windows/wsl/tutorials/linux&quot;&gt;MS : Linux 및 Bash 시작&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;powershell에서 wsl 명령어로 진입하기&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;```

# 윈도우 디렉토리 기준으로 실행
wsl

pwd
# /mnt/c/Users/user1

# 사용자 디렉토리 기준으로 실행
wsl ~

pwd
# /home/user1

```&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;windows terminal에서 더 보기 버튼에서 Ubuntu를 선택할 수 있습니다.&lt;/li&gt;
&lt;/ol&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;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6TIKl/dJMb9XdhjsU/aefY0fdByMQXLxCyxPKzZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6TIKl/dJMb9XdhjsU/aefY0fdByMQXLxCyxPKzZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6TIKl/dJMb9XdhjsU/aefY0fdByMQXLxCyxPKzZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6TIKl%2FdJMb9XdhjsU%2FaefY0fdByMQXLxCyxPKzZK%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;338&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에디터 사용하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vim&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 환경에는 기본적으로 vim이 깔려있습니다. 따라서 vim 명령어를 통해서 에디터를 활용하셔도 됩니다. ssh로 다른 컴퓨터에 원격으로 접속하여 간단하게 작업하는 경우에는 vim을 활용하고 있어요!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vscode&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하는 경우 대부분의 영역에서 사용하고 계시는 인기있고 기본 에디터 중 하나로 vscode를 빼놓을 수 없는데요. wsl 환경에서는 windows에 vscode가 깔려있다면 바로 활용할 수 있습니다.&lt;br /&gt;방법은 아래와 같이 두 가지 입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;터미널에서 필요한 프로젝트 경로를 대상으로 &lt;code&gt;code&lt;/code&gt; 명령어를 실행하시면 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;```
code .
```&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;windows 환경에서 그냥 vscode를 실행하시고 좌측 하단의 초록색 원격 열기를 눌러 wsl 연결하시면 됩니다. 파일 &amp;gt; 폴더 열기 &amp;gt; linux 경로의 프로젝트를 선택하시면 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bW9E5A/dJMb9OAIFye/26Ljd7TxWcHDNBbtnB6zn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bW9E5A/dJMb9OAIFye/26Ljd7TxWcHDNBbtnB6zn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bW9E5A/dJMb9OAIFye/26Ljd7TxWcHDNBbtnB6zn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbW9E5A%2FdJMb9OAIFye%2F26Ljd7TxWcHDNBbtnB6zn1%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;395&quot; height=&quot;137&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;137&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;318&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNewsf/dJMb9OAIFyf/K7mLm0YE7ZLEXcWWIUK3L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNewsf/dJMb9OAIFyf/K7mLm0YE7ZLEXcWWIUK3L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNewsf/dJMb9OAIFyf/K7mLm0YE7ZLEXcWWIUK3L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNewsf%2FdJMb9OAIFyf%2FK7mLm0YE7ZLEXcWWIUK3L1%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;318&quot; height=&quot;153&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;마치며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 wsl 환경 설정에 대해서 알아 보았습니다. 그 외에 개발에 필요한 프로그램 설치 등은 모두 Ubuntu 기준의 명령어로 진행하셔서 활용하시면 되겠습니다.&lt;br /&gt;이제 grep 과 같은 리눅스 명령어를 쉽게 사용할 수 있고, bash 스크립트도 더 이상 git bash 터미널로 이용할 필요도 없어졌습니다. 또한 Docker를 활용해 볼 수 있는 사전 단계가 완성되었네요.&lt;br /&gt;다음 시간에는 Docker가 뭔지 간단하게 알아보면서 어떻게 활용하고 있는지에 대해서 공유해보겠습니다.&lt;/p&gt;</description>
      <category>Dev/기타</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/230</guid>
      <comments>https://goforit.tistory.com/230#entry230comment</comments>
      <pubDate>Fri, 24 Oct 2025 00:17:36 +0900</pubDate>
    </item>
    <item>
      <title>웹 서비스 안드로이드 환경에 제공하기 (WebAPK, TWA)</title>
      <link>https://goforit.tistory.com/229</link>
      <description>&lt;h1&gt;웹 서비스 안드로이드 환경에서 제공하기 (WebAPK, TWA)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;23년도에 웹 서비스가 키오스크와 같은 모습으로 특정 안드로이드 기기에서 설치되어 제공되어야 하는 상황이 발생했습니다. 웹 서비스를 안드로이드 환경에서 앱 처럼 사용자에게 제공할 수 있는 방법을 찾기 위해서 23년도 당시 탐구하고 고민하여 서비스를 제공했던 경험을 공유합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목표&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 태블릿에서 키오스크와 같은 형태로 사용자에게 서비스 제공하기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제한 사항&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프론트엔드 인력만 활용 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 개발자분이 계시긴 하지만, 서비스 자체를 개발하기에는 부족하여 현재 여유가 되는 프론트엔드 개발자 위주로 개발 진행 가능했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모바일 환경에 대한 전문성 부족
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통 채용 현황이 React 기반의 웹 프론트엔드 개발자로만 구성되어 있는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 서비스할 기기를 변경할 수 없음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 이유인지는 모르겠으나, 이미 의사결정이 완료되어 서비스할 기기 자체 변경은 불가능한 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구 사항&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;오프라인에서도 서비스 표시&lt;/li&gt;
&lt;li&gt;키오스크처럼 전체 화면 표시&lt;/li&gt;
&lt;li&gt;키오스크처럼 기기 부팅 후 앱 자동 시작&lt;/li&gt;
&lt;li&gt;코드 업데이트의 신속 및 유연&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안드로이드 환경에서 서비스를 제공하는 방법 (WebAPK, TWA)&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://reactnative.dev/&quot;&gt;React Native&lt;/a&gt;, &lt;a href=&quot;https://expo.dev/&quot;&gt;Expo&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React와 비슷한 형식을 사용하여 관리할 수 있지만, 스타일 시스템 등이 많이 다릅니다.&lt;/li&gt;
&lt;li&gt;더 많은 네이티브 기능 및 권한을 사용하여 개발할 수 있습니다.&lt;/li&gt;
&lt;li&gt;직접 요소들을 작성하지 않고, 웹뷰 방식의 래퍼 형식으로 만들 수도 있습니다.&lt;/li&gt;
&lt;li&gt;하지만, 웹이 아닌 기술이기 때문에 개발을 위한 학습 비용 및 자동화 테스트 설계를 위한 기술 탐구, 적응하는 비용 등이 너무나 컸기 때문에 React Native는 선택지에서 제외하였습니다.&lt;/li&gt;
&lt;li&gt;물론, React Native는 Hermes engine이 탑재되면서 더욱 최적화된 앱 성능을 자랑하고 있고 해당 생태계는 모바일, 데스크탑, 웹 모든 환경을 크로스 플랫폼 앱을 목적으로 성장하고 있어 흥미롭고 기대되는 기술 영역입니다. 최근에는 Skia 그래픽 엔진이 추가되면서 복잡한 그래픽 처리도 가능한 걸로 알고 있어 더욱 흥미롭네요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/webapks?hl=ko&quot;&gt;WebAPK&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/learn/pwa/installation?hl=ko#webapks&quot;&gt;WebAPK : PWA Android 설치&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Android Chrome에서 웹 서비스에 접속하여 홈 바로가기 설치(앱 설치)를 진행하면 Google WebAPK 생성 서버가 APK를 생성하여 안드로이드 환경에 앱 형태로 설치되어 앱처럼 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;트위터, 스타벅스 등의 사이트에서도 제공하고 있는 방식입니다.&lt;/li&gt;
&lt;li&gt;설치가 가능하기 때문에 안드로이드 기기에서 부팅 자동 시작 대상으로 설정할 수 있었습니다.&lt;/li&gt;
&lt;li&gt;원래 웹 서비스 개발 방식을 그대로 유지할 수 있고, 안드로이드에서 제공하기 위한 설치 파일도 자체적으로 관리해주기 때문에 큰 비용이 발생하지 않았습니다.&lt;/li&gt;
&lt;li&gt;최초에 해당 환경으로 서비스를 제공하였으나, 24년 4월 쯤 APK 파일을 만들어 주는 Google 서버에 문제가 있어서 이후 설치된 앱들은 모두 실행 후 바로 꺼지는 &lt;a href=&quot;https://developer.android.com/topic/performance/vitals/crash?hl=ko&amp;amp;_gl=1*1dux6m4*_up*MQ..*_ga*NTk3NzcxOTY2LjE3NjAzNzY3NDI.*_ga_6HH9YJMN9M*czE3NjAzNzY3NDIkbzEkZzAkdDE3NjAzNzY3NjUkajM3JGwwJGgxOTk0Nzc4NjM1&quot;&gt;비정상 종료(Crash)&lt;/a&gt; 현상이 발생하면서 해당 방식에서 APK 빌드도 관리할 수 있는 TWA 방식으로 이전했습니다.&lt;/li&gt;
&lt;li&gt;또한, &lt;a href=&quot;https://www.wins21.com/kor/promotion/information.html?bmain=view&amp;amp;uid=4122&quot;&gt;WebAPK 형태의 앱을 통한 설치 방식에 관한 범죄&lt;/a&gt;가 발생하면서 Chrome에서 WebAPK의 지속적인 지원이 어려울 것이라고 판단되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/docs/android/trusted-web-activity?hl=ko&quot;&gt;TWA&lt;/a&gt; (Trusted Web Activities)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맞춤 탭 (&lt;a href=&quot;https://developer.chrome.com/docs/android/custom-tabs?hl=ko#what_can_custom_tabs_do&quot;&gt;Android Custom Tabs&lt;/a&gt;)은 Webview와 다르게 브라우저와 상태를 공유하면서 웹 플랫폼의 모든 기능을 사용할 수 있습니다. 또한 사용자는 브라우저를 실행하는 것이 아니라 앱을 사용하는 느낌의 경험을 제공합니다. 서비스 내부에서 브라우징 경험을 보존하는 방식을 말하는 것 같습니다.&lt;/li&gt;
&lt;li&gt;신뢰할 수 있는 웹 활동은 &lt;a href=&quot;https://developer.chrome.com/docs/android/custom-tabs?hl=ko#when_should_i_use_custom_tabs&quot;&gt;맞춤 탭&lt;/a&gt; 기반으로 하는 프로토콜을 사용하여 Android앱에서 PWA와 같은 웹 앱 콘텐츠를 안드로이드 앱과 같은 전체화면으로 사용할 수 있는 경험을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안드로이드에 설치될 수 있고, 웹 서비스를 성능 저하 없이 온전히 웹 API를 모두 활용할 수 있고 전체화면을 통해서 충분히 앱과 같은 사용자 경험을 제공할 수 있어 아주 좋은 선택지였습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;또한, PWA 기반이기 때문에 Service Worker 처리를 통해서 오프라인에서도 웹앱이 깨지지 않아 충분히 사용자에게 안내를 해줄 수 있었고 소스를 재사용 할 수 있기 때문에 더욱 매력적이었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;&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;1509&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lgJxj/btsQ8kncCLH/JRtPkL6MeV5sVNluGeXpUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lgJxj/btsQ8kncCLH/JRtPkL6MeV5sVNluGeXpUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lgJxj/btsQ8kncCLH/JRtPkL6MeV5sVNluGeXpUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlgJxj%2FbtsQ8kncCLH%2FJRtPkL6MeV5sVNluGeXpUk%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;1509&quot; height=&quot;736&quot; data-origin-width=&quot;1509&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;&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;1471&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kSZim/btsQ953mlMY/MYuBDejbCqn3yyT1vJVLq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kSZim/btsQ953mlMY/MYuBDejbCqn3yyT1vJVLq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kSZim/btsQ953mlMY/MYuBDejbCqn3yyT1vJVLq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkSZim%2FbtsQ953mlMY%2FMYuBDejbCqn3yyT1vJVLq0%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;1471&quot; height=&quot;961&quot; data-origin-width=&quot;1471&quot; data-origin-height=&quot;961&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 개발을 진행하면서, React Native로 이전해야 하나라는 걱정도 많았습니다. 하지만 생각보다 가지고 있는 기술을 최대한 활용하여 최소 비용으로 빠르게 서비스 고객에게 제공할 수 있었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, E2E 기반의 자동화 테스트를 작성하거나 시뮬레이션이 필요한 경우에도 빠르고 익숙하게 진행할 수 있었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 결국에는 생소한 모바일 환경이기 때문에 안드로이드 단에서 발생하는 문제 대응이나, 현장 디버깅 등의 대응에는 매우 어려웠던 것 같습니다. 덕분에 잘 모르지만 안드로이드 에뮬레이션 환경을 만들어보기도 하고 adb로 실기기에 연결해서 디버깅도 해보고, logcat으로 crash도 확인하는 등 여러가지 경험을 해본 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 시간에는 WebAPK, TWA 방식의 구체적인 예시를 소개하면서 경험을 공유해보는 시간을 가져보려고 합니다. WebAPK, TWA 방식은 모두 PWA가 전제 조건이기 때문에 PWA에 대해서 먼저 알아보긴 해야겠네요!&lt;/p&gt;</description>
      <category>Dev/web</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/229</guid>
      <comments>https://goforit.tistory.com/229#entry229comment</comments>
      <pubDate>Wed, 15 Oct 2025 00:18:43 +0900</pubDate>
    </item>
    <item>
      <title>Github 여러 계정 사용하기</title>
      <link>https://goforit.tistory.com/228</link>
      <description>&lt;h1&gt;Github 여러 계정 사용하기&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 또는 다른 조직에서 활동을 하는 경우 보통 새로운 Github 계정들을 활용하여 작업을 진행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 작업을 진행하다 보면 하나의 디바이스에서 여러 계정들에 접근해서 작업을 진행하는 경우 인증 문제가 발생하는 경우가 많습니다. &lt;a href=&quot;https://github.blog/changelog/2021-08-12-git-password-authentication-is-shutting-down/&quot;&gt;이전에는 Git에서 Github 아이디, 패스워드 방식의 인증을 통해서 진행했지만 2021-08-13일부터 Git에서 Github 패스워드 인증 방식을 제공하지 않기 시작하면서 토큰(Personal Access, OAuth, SSH Key) 방식의 인증 방식을 사용하도록 하고 있습니다.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;553&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YzhBZ/btsQ5ktKIWO/9B6vlgsuALkPkKgZeN76y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YzhBZ/btsQ5ktKIWO/9B6vlgsuALkPkKgZeN76y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YzhBZ/btsQ5ktKIWO/9B6vlgsuALkPkKgZeN76y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYzhBZ%2FbtsQ5ktKIWO%2F9B6vlgsuALkPkKgZeN76y0%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;1072&quot; height=&quot;553&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;553&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 디바이스에서 여러 계정들에 손쉽게 접근하여, 인증되고 작업을 진행할 수 있는 설정에 대해서 설명합니다. 설명하고 있는 내용은 wsl 환경을 기준으로 진행하고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 공식 문서 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/authentication&quot;&gt;Github 공식 문서의 인증에 관련된 문서&lt;/a&gt;를 확인해봅니다.&lt;br /&gt;&lt;a href=&quot;https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&quot;&gt;기본적으로 SSH Key를 생성하고 ssh-agent에 추가하여 SSH 인증을 통해서 사용하는 방법에 대해서 소개하고 있습니다.&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. SSH란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.ssh.com/academy/ssh&quot;&gt;SSH(Secure Shell)는 안전하지 않은 네트워크에서 안전한 시스템 관리와 파일 전송을 가능하게 하는 소프트웨어 패키지입니다.&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;SSH 프로토콜(22 Port)을 통해서 보안 원격 로그인 및 파일 전송을 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 클라이언트로 구성되어 연결을 맺어 통신하는 구조이며 현재 다양한 무료 SSH 및 상업용 SSH 구현도 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Windows 환경의 대표적인 오픈 소스 &lt;a href=&quot;https://www.putty.org/&quot;&gt;PuTTY&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Linux 환경의 대표적인 오픈 소스 &lt;a href=&quot;https://www.ssh.com/academy/ssh/openssh&quot;&gt;OpenSSH&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenSSH Client = ssh&lt;/li&gt;
&lt;li&gt;OpenSSH Sever = sshd&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH는 공개 키 인증(Public Key Authentication)방식의 암호화된 키를 기반의 인증을 제공합니다.&lt;br /&gt;개인 키를 통해서 서버의 공개 키를 인증합니다. 개인키 및 공개키 모두 사용자 홈 &amp;gt; &lt;code&gt;.ssh&lt;/code&gt; 디렉토리에 저장됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9Edg1/btsQ7Svt7n7/toMapXKe2Ke9eTR7ejPZjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9Edg1/btsQ7Svt7n7/toMapXKe2Ke9eTR7ejPZjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9Edg1/btsQ7Svt7n7/toMapXKe2Ke9eTR7ejPZjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9Edg1%2FbtsQ7Svt7n7%2FtoMapXKe2Ke9eTR7ejPZjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;556&quot; height=&quot;191&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH에서는 설정 파일을 통해서 지정한 패턴 형식을 간단하게 적용하여 사용할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;~/.ssh/config&lt;/code&gt; 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 설정하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH 키 생성하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Github 메일 주소 형식으로 수정하여 Key 생성하기&lt;/li&gt;
&lt;li&gt;생성하면 공개키와 개인키가 생성된 것을 확인 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;기기에서 사용할 Github 계정 수만큼 파일이름을 구분하여 해당 과정을 반복하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;cd ~/.ssh

ssh-keygen -t ed25519 -C &quot;example@example.com&quot;


# &amp;gt; Generating public/private ed25519 key pair.
# &amp;gt; Enter file in which to save the key (/home/user/.ssh/id_ed25519): id_ed25519_github_user1
# &amp;gt; Enter passphrase (empty for no passphrase):
# &amp;gt; Enter same passphrase again:
# &amp;gt; Your identification has been saved in id_ed25519_github_user1
# &amp;gt; Your public key has been saved in id_ed25519_github_user1.pub
# &amp;gt; The key fingerprint is:
# &amp;gt; SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA example@example.com
# &amp;gt; The key's randomart image is:
# &amp;gt; +--[ED25519 256]--+
# &amp;gt; +----[SHA256]-----+

ls

# &amp;gt; config id_ed25519_github_user1.pub id_ed25519_github_user1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ssh-agent에 SSH 키 추가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ssh-agent를 실행 후 agent에 생성된 개인 키를 등록합니다.&lt;/li&gt;
&lt;li&gt;기기에서 사용할 Github 계정 수만큼 생성된 개인 키 등록 과정을 진행해야 합니다,&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;eval &quot;$(ssh-agent -s)&quot;

# &amp;gt; Agent pid 59566

ssh-add ~/.ssh/id_ed25519_github_user1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ssh config 설정하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ssh config를 설정하면 손쉽게 여러 계정 설정으로 접근할 수 있기 때문에 해당 설정을 진행하면 좋습니다.&lt;/li&gt;
&lt;li&gt;vi, vim, nano, notepad 등의 에디터 명령어를 활용하여 &lt;code&gt;config&lt;/code&gt; 파일을 작성하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;vim ~/.ssh/config


#Gitub User1 계정에 대한 SSH 설정
Host github.com-User1
    HostName github.com
    User User1
    IdentityFile ~/.ssh/id_ed25519_github_user1

#Github User2 계정에 대한 SSH 설정
Host github.com-User2
    HostName github.com
    User User2
    IdentityFile ~/.ssh/id_ed25519_github_user2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH pub Key 등록하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬에서 생성한 SSH 공개키를 Github에 등록합니다.&lt;/li&gt;
&lt;li&gt;Github &amp;gt; 프로필 &amp;gt; Settings &amp;gt; SSH and GPG Keys &amp;gt; New SSH Key
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Title : 어떤 기기의 SSH Key인지 구분할 수 있는 이름&lt;/li&gt;
&lt;li&gt;Key type : Authentication Key&lt;/li&gt;
&lt;li&gt;Key
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공개키 파일의 값을 복사하여 붙여 넣기합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat ~/.ssh/id_ed25519_github_user1.pub&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Xjc7/btsQ321mvot/Oi5kf2GckM9ov0fHMtMWs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Xjc7/btsQ321mvot/Oi5kf2GckM9ov0fHMtMWs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Xjc7/btsQ321mvot/Oi5kf2GckM9ov0fHMtMWs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Xjc7%2FbtsQ321mvot%2FOi5kf2GckM9ov0fHMtMWs0%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;1055&quot; height=&quot;652&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 필요한 개인 레포를 클론해봅니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;git clone git@github.com-User1:User1/myRepo.git

# Cloning into 'myRepo'...
# remote: Enumerating objects: 161841, done.
# remote: Counting objects: 100% (15/15), done.
# remote: Compressing objects: 100% (11/11), done.
# remote: Total 161841 (delta 4), reused 11 (delta 4), pack-reused 161826 (from 1)
# Receiving objects: 100% (161841/161841), 164.39 MiB | 8.70 MiB/s, done.
# Resolving deltas: 100% (130261/130261), done.&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 계정 인증을 기기에서 진행할 수 있도록 설정해보았습니다.&lt;br /&gt;Github에서는 SSH 방법 말고 Personal Access 토큰 방식의 인증도 지원하고 있습니다.&lt;br /&gt;해당 토큰을 사용하면, 간단하게 누군가 또는 기기에 특정 권한 범위 및 유효 기간 등을 디테일하게 지정하여 Github에 접근하게 허용할 수 있습니다.&lt;br /&gt;다음 시간에는 해당 방법을 통해서 인증하는 방법을 알아보겠습니다.&lt;/p&gt;</description>
      <category>Dev/Git</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/228</guid>
      <comments>https://goforit.tistory.com/228#entry228comment</comments>
      <pubDate>Sat, 11 Oct 2025 01:04:15 +0900</pubDate>
    </item>
    <item>
      <title>[Design Pattern] MVC 패턴, 어떻게 패턴 공부할까?</title>
      <link>https://goforit.tistory.com/225</link>
      <description>&lt;h1&gt;  Design Pattern&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;면접을 갔다가, 코드 리팩토링에 대해서 이야기 하던 중 디자인 패턴을 알면 도움이 많이 된다는 이야기를 나눠서 공부해 보려고 합니다. 공부한 내용과 생각을 적고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;정확한 자료는 아닐수 있으니 맹신하지 마시길 바랍니다. 항상 다른 자료와 교차 검증을 통해 참고해 주세요.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;디자인 패턴&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;건축의 공법에서 착안하여 목적에 따른 효율적인 코드 작성법을 위한 소프트웨어의 개발 방법(규칙, 패턴)을 공식화 하면서 시작되었습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;MVC 패턴 (Model View Controller)&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;유지보수가 편해지는 코드 구성 방식&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=DUg2SWWK18I&quot;&gt;4 분 안에 MVC 설명 by Web Dev Simplified&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;661&quot; data-filename=&quot;mvc01.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BAnkW/btrgjzf5PAS/Ni5G4tsQ5SFlpgFQdmKGN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BAnkW/btrgjzf5PAS/Ni5G4tsQ5SFlpgFQdmKGN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BAnkW/btrgjzf5PAS/Ni5G4tsQ5SFlpgFQdmKGN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBAnkW%2Fbtrgjzf5PAS%2FNi5G4tsQ5SFlpgFQdmKGN1%2Fimg.png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;661&quot; data-filename=&quot;mvc01.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;Controller&lt;/h2&gt;
&lt;p&gt;컨트롤러는 사용자의 요청에 따라서 Model에 데이터를 의뢰하고, 데이터를 View에 반영해서 사용자에게 알려줍니다. 즉, 사용자 요청에 따른 Model, view를 유기적으로 연결하는 부분이라고 볼수 있겠습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;Model&lt;/h2&gt;
&lt;p&gt;데이터베이스에 데이터를 가져오거나 변경 또는 저장하는 부분입니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;View&lt;/h2&gt;
&lt;p&gt;사용자에게 보여지는 부분을 담당하는 부분입니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;5가지 규칙&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Model은 Controller와 View에 의존하지 않아야 한다.&lt;/li&gt;
&lt;li&gt;View는 Model에만 의존해야 하고, Controller에는 의존하면 안된다.&lt;/li&gt;
&lt;li&gt;View가 Model로 부터 데이터를 받을 때는, 사용자마다 다르게 보여주어야 하는 데이터에 대해서만 받아야 한다. (UI레이아웃 + Model -&amp;gt; View)&lt;/li&gt;
&lt;li&gt;Controller는 Model과 View에 의존해도 된다.&lt;/li&gt;
&lt;li&gt;View가 Model로 부터 데이터를 받을 때, 반드시 Controller에서 받아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;디렉토리 구조 및 사용방식으로 확인해보기 (MVC)&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;추상적으로는 알겠지만, 정확히 코드로 보면서 느끼는것이 좋은 것 같아서 express에서 어떻게 사용하는지 잘알려주는 유튜브 영상을 시청하였습니다.&lt;/p&gt;
&lt;p&gt;영상 시청전에는 Express의 generator를 통한 디렉 구조에서는 확실하게 Model, view, controller에 대한 폴더가 생성되는 구조가 아니기 때문에 MVC가 어떻게 표현되는지 와닿지는 않았습니다. 또한 express의 경우 view로 jade나 pug를 사용하지만 저는 보통 View를 React를 사용하여 client server를 운영하는 방식에 익숙 했기 때문에 더욱 코드 예시를 통해 이해하는게 좋을 듯 했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=Cgvopu9zg8Y&quot;&gt;ExpressJS 및 NodeJS로 MVC 패턴 배우기-튜토리얼 초급 by PedroTech&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=bQuBlR0T5cc&quot;&gt;MVC Pattern Explained Easy | MVC Tutorial (Example in NodeJS) by PedroTech&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;Routes, Route&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;사용자에게 요청을 받고 요청에 적절한 Controller를 반환하는 역할을 담당하는 것으로 생각됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { express } from &amp;quot;express&amp;quot;;
const router = express.Router();

import { GetAllUsers } from &amp;quot;../controllers/User&amp;quot;;
router.get(&amp;quot;/all&amp;quot;, GetAllUsers);

export { router as UserRoute };&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;Controller&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Front, Back server로 CSR을 구성하는 경우에는 Back에서는 단지 변경될 데이터만 가져오게 됨으로 Controller에서 반환하는 것은 view가 아닌 Controller가 가져온 Model의 데이터가 되는 것 같습니다.&lt;ul&gt;
&lt;li&gt;이후, front 에서 데이터를 받아 화면을 구성하겠지요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { QueryListOfUsers } from &amp;quot;../service/UserTable&amp;quot;;

export const GetAllUsers = (req, res) =&amp;gt; {
  const userList = QueryListOfUsers();
  return res.json(userList);
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Service라는 폴더를 구성한 사람도 있는데 이때 Controller에서 Service의 메서드를 활용하여 값을 가져와 연결 시키는 것을 확인했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import User from &amp;quot;../models/User&amp;quot;;

export const QueryListOfUsers = (condition) =&amp;gt; {
  if (condition) {
    return User.findAll(condition).exec();
  }

  return User.findAll().exec();
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Back server로 SSR을 구성하는 경우에는 view를 반환해야하므로 Controller에서는 Model에서 가져온 데이터를 view에 주입시켜 해당 view를 render하도록 하는 것 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;Model (Model &amp;amp; Service)&lt;/h3&gt;
&lt;p&gt;Model의 경우 아직 DB에 대해서 제대로 배우질 못해서 잘 알지는 못하지만, DB에서의 데이터를 가져올때의 로직 및 스키마가 포함된 부분인 것 같습니다.&lt;/p&gt;
&lt;p&gt;어떤 사람들은 Model과 Service 폴더를 나누어 사용하는데 Service가 무엇을 하는 용도인지 궁금했습니다.&lt;/p&gt;
&lt;p&gt;확인해보니 Model의 경우에는 단순 가져올 데이터의 Schema(스키마)를 구성하는 역할을 하고, Service에서 직접적인 데이터를 가져오는 로직을 작성하여 사용하는듯 했습니다.&lt;/p&gt;
&lt;p&gt;이렇게 사용하는 하는 것은 데이터의 안정성을 지키기 위함이라고 합니다. 보통, 많이 복잡한 프로그램을 짜게 되는 경우 여러 데이터 구조가 긴밀히 연결되어 있어 하나를 변경하면, 다른 것도 변경해줘야하는 상황이 발생합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예전에 인스타그램 프로젝트시 발생했던, 게시글 지우기시 해당 댓글 모두 지우기 같은 2가지의 변경을 하게 로직을 짰던 기억이 납니다.&lt;/li&gt;
&lt;li&gt;위와 같이 긴밀하게 연결된 데이터의 경우, 하나만 잘못 처리되어도 다른 데이터에도 영향을 주어 구조를 해치게 되므로 이를 방지하기 위해 이러한 작업사항들을 모두 성공했을 때만, 실행하고 그렇지 않으면 다시 되돌리는 이러한 안전장치라고 합니다. (DB 조작에 대한 에러 처리)&lt;/li&gt;
&lt;li&gt;물론, 다양한 변경 로직들을 재사용할 수도 있게 만들기 때문에 효율적인것 같기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;잠시 드는 생각&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이런 생각이 잠시 들게 됩니다. 전에는 firebase를 기준으로 프로젝트를 만들다 보니 패턴 분리 없이 db조작에 관한 코드들도 redux에 한번에 기록하여 작성했었습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;그때 비동기 요청 작업 코드들 자체를 backend의 service 로직에 가깝게 작성했던것 같습니다. 그러면서 클라이언트 서버에서 redux 비동기 요청자체에서 db에 대한 에러처리까지 같이 받게 되는 그러한 구조를 작성했던것 같습니다. 위에서 말한 MVC의 5규칙에 어긋나는 것은 아닌가 생각이 듭니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이러한 작업들을 했던것을 상기하면서, 어디까지가 과연 프론트고 백엔드 일까라는 생각이 듭니다. 과거 전통적인 SSR이였으면 프론트는 단지 백엔드의 View를 구성하는 사람에 지나지 않지 않을 까? 라는 생각이 듭니다. 그치만 현재는 백엔드에 적절한 양의 데이터를 어느정도 요청하고, 화면에 분배하는 과정을 구현하다 보니 모든 과정이 어떻게 보면 캐싱의 한부분이 아닐까라는 생각이 듭니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;어떻게 공부를 진행할까?&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;디자인 패턴에 대해서 몇가지 조사를 해보았습니다. 유튜브를 통해 디자인패턴을 검색해보면, 여러가지 자료들이 나오고 또한 구글도 잘 나옵니다. 하지만, 보통 JAVA 등의 언어로 설명되어 있어 이해하기가 조금 난해합니다. 그래서, nodejs로 된 예시를 가진 디자인 패턴을 통해 공부해보려고 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;깃허브에 nodejs-design-pattern을 검색해보면, Mario Casciaro의 Nodejs. Design 패턴 책에 대한 깃허브 글을 발견했고 해당 책이 잘 정리되어 있는듯 합니다. 그래서 찾아보니 한글로 번역한 책도 있어 구매해볼 생각입니다.&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.yes24.com/Product/Goods/101686866&quot;&gt;NodeJs 디자인 패턴 바이블&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일단은, 대략적으로 알기 위해서 깃허브에서 star 23개 짜리 repo가 있어 해당 글을 보고 익혀보려고 합니다.&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/adoi/node-design-patterns&quot;&gt;node-design-patterns by adoi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Design Pattern</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/225</guid>
      <comments>https://goforit.tistory.com/225#entry225comment</comments>
      <pubDate>Thu, 30 Sep 2021 01:23:52 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js] 백준 - 기본 수학 문제 1 : 달팽이는 올라가고 싶다, ACM호텔, 부녀회장이 될테야</title>
      <link>https://goforit.tistory.com/224</link>
      <description>&lt;h1&gt;  Node.js를 이용해 백준 문제 풀기&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by1o7Y/btrfFFs7K7r/2NYQnKjqVrmhx3Kd6fU4A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by1o7Y/btrfFFs7K7r/2NYQnKjqVrmhx3Kd6fU4A0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by1o7Y/btrfFFs7K7r/2NYQnKjqVrmhx3Kd6fU4A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby1o7Y%2FbtrfFFs7K7r%2F2NYQnKjqVrmhx3Kd6fU4A0%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;Node.js를 이용해서 백준 문제를 풀고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;기본 수학 문제 1 (3개)&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;최대한 반복문 없이 수학을 활용하도록 노력했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(4) 달팽이는 올라가고 싶다.&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;정상 까지 도착하면 무조건 올라가는 것으로 끝난다고 볼 수 있습니다. &lt;br/&gt;&lt;br&gt;무조건 u - d + u - d + ... + u로 끝납니다. &lt;br/&gt;&lt;br&gt;day(u-d) + d = h &lt;br/&gt;&lt;br&gt;day = (h-d)/(u-d) &lt;br/&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (4) 달팽이는 올라가고 싶다
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
input = input[0].split(&amp;quot; &amp;quot;);
const u = Number(input[0]);
const d = Number(input[1]);
const h = Number(input[2]);
console.log(Math.ceil((h - d) / (u - d)));&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(5) ACM 호텔&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;몇번 째 줄의 몇 층인 지가 중요점 입니다.&lt;/p&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (5) ACM 호텔
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
const testCount = Number(input[0]);
for (let i = 1; i &amp;lt;= testCount; i++) {
  const row = input[i].split(&amp;quot; &amp;quot;);
  const H = Number(row[0]);
  const N = Number(row[2]);

  const line = Math.ceil(N / H);
  const floor = N - H * (line - 1);
  console.log(floor * 100 + line);
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(6) 부녀회장이 될테야&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;층은 0 ~ 14 까지, 호수는 1 ~ 14까지 이므로 층과 호수의 개수는 다릅니다.&lt;br/&gt;&lt;br&gt;문제의 테스트케이스 크기 만큼의 층과 호수의 2차원 배열을 만들었습니다. &lt;br/&gt;&lt;br&gt;수학적으로 하나의 식으로 유도하기는 힘들어서, 내부적인 규칙을 활용해서 해당 호수의 인원수를 채워 넣고, 필요한 호수의 값을 가져오는 방식을 사용하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Array.from()&lt;/code&gt;을 활용하면, 손쉽게 의도적으로 초기화 된 배열을 만들 수 있습니다.&lt;ul&gt;
&lt;li&gt;Array.from에 length 객체와 Callback을 넣으면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const rooms = Array.from({ length: 15 }, () =&amp;gt; Array(14).fill(0));&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;위의 경우 2차 배열로, 14개의 0을 갖는 아이템을 갖는 배열을 아이템으로 15개를 가지는 외부 배열의 구조를 갖습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (6) 부녀회장이 될테야
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
const testCount = Number(input[0]);
const rooms = Array.from({ length: 15 }, () =&amp;gt; Array(14).fill(0));
let answer = &amp;quot;&amp;quot;;
for (let i = 0; i &amp;lt; 15; i++) {
  rooms[i][0] = 1;
  if (i &amp;lt; 14) {
    rooms[0][i] = i + 1;
  }
}
for (let i = 1; i &amp;lt; 14; i++) {
  for (let j = 1; j &amp;lt; 15; j++) {
    rooms[j][i] = rooms[j - 1][i] + rooms[j][i - 1];
  }
}

for (let i = 0; i &amp;lt; testCount; i++) {
  const floor = Number(input[2 * i + 1]);
  const line = Number(input[2 * i + 2]) - 1;
  answer += `${rooms[floor][line]}\n`;
}

console.log(answer);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Dev/백준</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/224</guid>
      <comments>https://goforit.tistory.com/224#entry224comment</comments>
      <pubDate>Tue, 21 Sep 2021 14:34:46 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js] 백준 - 기본 수학 문제 1 : 손익분기점, 벌집O(1), 분수찾기O(1)</title>
      <link>https://goforit.tistory.com/223</link>
      <description>&lt;h1&gt;  Node.js를 이용해 백준 문제 풀기&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OK1hG/btrfFFGETMi/PV6iTBfhXKOVtyP1jhgnik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OK1hG/btrfFFGETMi/PV6iTBfhXKOVtyP1jhgnik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OK1hG/btrfFFGETMi/PV6iTBfhXKOVtyP1jhgnik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOK1hG%2FbtrfFFGETMi%2FPV6iTBfhXKOVtyP1jhgnik%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;Node.js를 이용해서 백준 문제를 풀고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;기본 수학 문제 1 (3개)&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;최대한 반복문 없이 수학을 활용하도록 노력했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(1) 셀프 넘버&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (1) 손익분기점
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
input = input[0].split(&amp;quot; &amp;quot;).map((num) =&amp;gt; Number(num));
const dCost = input[0];
const vCost = input[1];
const income = input[2];
let q = 0;

if (vCost &amp;gt;= income) {
  q = -1;
} else {
  q = Number.isInteger(dCost / (income - vCost))
    ? dCost / (income - vCost) + 1
    : Math.ceil(dCost / (income - vCost));
}

console.log(q);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 벌집 : O(1)&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (2) 벌집
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
const target = Number(input[0]);
console.log(Math.ceil((3 + Math.sqrt(12 * target - 3)) / 6));&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;
벌집의 경우 중심 부로 부터 깊이를 생각하여 1, 2, 3 ... 으로 벌집 방의 늘어 나는 개수에 규칙이 존재 합니다.
&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;깊이&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;1&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;2&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;3&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;4&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;5&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;개수&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;6 + 6*0&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;6 + 6*1&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;6 + 6*2&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;6+ 6*3&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;
깊이 별 방의 개수가 일정하게 규칙성을 가지고 늘어남을 확인하였습니다.
깊이를 d 로 생각하여 d 에 해당 하는 끝 값을 식으로 나타낼 수 있습니다.
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1 &lt;br/&gt;&lt;br&gt;1 + 6(1 + 1&lt;em&gt;0) &lt;br/&gt;&lt;br&gt;1 + 6(1 + 1&lt;/em&gt;0) + 6(1 + 1&lt;em&gt;1) &lt;br/&gt;&lt;br&gt;... &lt;br/&gt;&lt;br&gt;1 + 6(1 + 1&lt;/em&gt;0) + 6(1 + 1*1) + ... + 6(1 + n) &lt;br/&gt;&lt;br&gt;1 다음 부터는 등차 수열의 합을 이용한 공식을 활용하여 나타낼 수 있습니다. &lt;br/&gt;&lt;br&gt;1 + 2 + 3 + ... (n-1) + n -&amp;gt; n(n+1)/2&lt;br/&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1 + Σ6(d-2)+6 &lt;br/&gt;&lt;br&gt;1 + Σ6(d-1)&lt;br/&gt;&lt;br&gt;1 + 3d(d-1)&lt;br/&gt;&lt;br&gt;3d^2 -3d + 1&lt;/p&gt;
&lt;p&gt;
해당 식이 가르키는 값에 target이 들어올 때, target에 따른 Depth 깊이를 해로 구할 수 있습니다.

&lt;br/&gt;

&lt;p&gt;예를 들어, depth가 3인 경우는 끝 값은 19입니다. 그러면 target이 18인 경우 depth는 3보다 조금 작은 숫자 입니다.&lt;br&gt;depth가 2 인경우 끝값은 7 이므로 target이 7&amp;lt; t &amp;lt;=19 이면, depth는 2&amp;lt; d &amp;lt;=3 입니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;target에 대한 depth가 소수점의 숫자 올림처리를 하면 해당 depth가 나오게 됩니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;그래서 d를 구하는 근의 공식을 통해 식을 산출합니다. &lt;br/&gt;&lt;br&gt;3d^2 -3d + 1 = t &lt;br/&gt;&lt;br&gt;d = (3 + Math.sqrt(12 * t - 3)) / 6) 이며 이를 ceil를 통해 올림 처리 합니다.&lt;/p&gt;
&lt;/p&gt;

&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;(3) 분수 찾기: O(1)&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;위 벌집 문제와 비슷한 방식으로 풀어 낼 수 있습니다. 중요점은 가르키는 input이 몇 번째 줄인지, 그 줄의 몇 번째에 있는지 알아야 합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (3) 분수 찾기
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
const target = Number(input[0]);
// 등차수열의 합에 대한 근 찾기
const sol = (-1 + Math.sqrt(8 * target + 1)) / 2;
// 근을 통한 줄 찾기
const rowNum = Math.ceil(sol);
// 줄을 통해서 몇 번째인지 찾기
const colNum = target - (rowNum * (rowNum - 1)) / 2;

// 짝수 번째는 분모가 크고, 홀수 번째 줄은 분자가 크고
if (rowNum % 2 === 0) {
  console.log(`${colNum}/${rowNum - colNum + 1}`);
} else {
  console.log(`${rowNum - colNum + 1}/${colNum}`);
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Dev/백준</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/223</guid>
      <comments>https://goforit.tistory.com/223#entry223comment</comments>
      <pubDate>Tue, 21 Sep 2021 14:33:04 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js] 백준 - 함수&amp;amp;문자열 문제 : 셀프 넘버, 한수, 아스키 코드, 숫자의 합, 알파벳 찾기, 문자열 반복, 단어 공부, 단어의 개수, 상수, 다이얼, 크로아티아 알파벳, 그룹 단어 체커</title>
      <link>https://goforit.tistory.com/222</link>
      <description>&lt;h1&gt;  Node.js를 이용해 백준 문제 풀기&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWURuN/btrftn1SMIn/XVkSWMmH6n44ugsh4xq1yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWURuN/btrftn1SMIn/XVkSWMmH6n44ugsh4xq1yK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWURuN/btrftn1SMIn/XVkSWMmH6n44ugsh4xq1yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWURuN%2Fbtrftn1SMIn%2FXVkSWMmH6n44ugsh4xq1yK%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;Node.js를 이용해서 백준 문제를 풀고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;함수 문제 (2개)&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h3&gt;(1) 셀프 넘버&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (1) 셀프 넘버
const range = new Set([]);
for (let i = 1; i &amp;lt;= 10000; i++) {
  range.add(i);
}

for (let i = 1; i &amp;lt;= 10000; i++) {
  checkSelf(i, range);
}

function checkSelf(n, range) {
  let sum = 0;
  for (let i = 0; i &amp;lt; String(n).length; i++) {
    sum += Number(String(n)[i]);
  }
  if (sum + n &amp;lt;= 10000) {
    range.delete(sum + n);
  }
}

console.log([...range].join(&amp;quot;\n&amp;quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 한수&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (2) 한수
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

const num = Number(input[0]);

function check(num) {
  let count = 0;
  for (let i = 1; i &amp;lt;= num; i++) {
    if (String(i).length === 1) {
      count = i;
    } else {
      const size = Number(String(i)[0]) - Number(String(i)[1]);
      const allSize = [];
      for (let j = 0; j &amp;lt; String(i).length - 1; j++) {
        const cur = Number(String(i)[j]);
        const next = Number(String(i)[j + 1]);

        if (cur - next === size) {
          allSize.push(true);
        } else {
          allSize.push(false);
        }
      }
      if (allSize.every((value) =&amp;gt; value)) {
        count += 1;
      }
    }
  }
  console.log(count);
}

check(num);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;문자열 문제 (10개)&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h3&gt;(1) 아스키 코드&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (1) 아스키 코드
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

console.log(input[0].charCodeAt(0));&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 숫자의 합&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (2) 숫자의 합
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

const sum = input[1]
  .split(&amp;quot;&amp;quot;)
  .reduce((prev, cur) =&amp;gt; Number(prev) + Number(cur), 0);
console.log(sum);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(3) 알파벳 찾기&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (3) 알파벳 찾기
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
const str = input[0];
const alphabets = Array(26).fill(0);

for (let i = 0; i &amp;lt; 26; i++) {
  alphabets[i] = str.indexOf(String.fromCharCode(97 + i));
}

console.log(alphabets.join(&amp;quot; &amp;quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(4) 문자열 반복&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (4) 문자열 반복

const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
let answer = &amp;quot;&amp;quot;;
for (let i = 0; i &amp;lt; input.length; i++) {
  const cur = input[i].split(&amp;quot; &amp;quot;);
  const num = cur[0];
  const str = cur[1];
  if (!num || !str) {
    continue;
  }
  answer +=
    str
      .split(&amp;quot;&amp;quot;)
      .map((item) =&amp;gt; item.repeat(num))
      .join(&amp;quot;&amp;quot;) + &amp;quot;\n&amp;quot;;
}

console.log(answer);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(5) 단어 공부&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (5) 단어 공부
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
input = input[0].toUpperCase();
const alphabets = Array(26).fill(0);
input.split(&amp;quot;&amp;quot;).map((al) =&amp;gt; {
  alphabets[al.charCodeAt(0) - 65] += 1;
});

const maxCount = Math.max(...alphabets);

if (alphabets.filter((i) =&amp;gt; i === maxCount).length &amp;gt; 1) {
  console.log(&amp;quot;?&amp;quot;);
} else {
  console.log(String.fromCharCode(alphabets.indexOf(maxCount) + 65));
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(6) 단어의 개수&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (6) 단어의 개수
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

if (input[0].split(&amp;quot; &amp;quot;)[0] === &amp;quot;&amp;quot;) {
  console.log(0);
} else {
  console.log(input[0].split(&amp;quot; &amp;quot;).length);
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(7) 상수&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (7) 상수
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

const nums = input[0]
  .split(&amp;quot; &amp;quot;)
  .map((item) =&amp;gt; Number(item.split(&amp;quot;&amp;quot;).reverse().join(&amp;quot;&amp;quot;)));

console.log(nums[0] &amp;gt; nums[1] ? nums[0] : nums[1]);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(8) 다이얼&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (8) 다이얼

const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
// case1 (직접 객체 만들기)
const sec = {
  A: 3,
  B: 3,
  C: 3,
  D: 4,
  E: 4,
  F: 4,
  G: 5,
  H: 5,
  I: 5,
  J: 6,
  K: 6,
  L: 6,
  M: 7,
  N: 7,
  O: 7,
  P: 8,
  Q: 8,
  R: 8,
  S: 8,
  T: 9,
  U: 9,
  V: 9,
  W: 10,
  X: 10,
  Y: 10,
  Z: 10,
};

// case 2 (반복문으로 객체 만들기)
const newSec = {};
let num = 3;
for (let i = 0; i &amp;lt; 26; i++) {
  const curAlphabet = String.fromCharCode(65 + i);
  if (i &amp;lt; 18) {
    newSec[curAlphabet] = num;
    if (i % 3 === 2 &amp;amp;&amp;amp; i !== 17) {
      num += 1;
    }
  } else if (i === 18) {
    newSec[curAlphabet] = num;
    num += 1;
  } else if (i &amp;gt; 18) {
    newSec[curAlphabet] = num;
    if (i % 3 === 0 &amp;amp;&amp;amp; i !== 24) {
      num += 1;
    }
  }
}

const res = input[0].split(&amp;quot;&amp;quot;).map((item) =&amp;gt; newSec[item]);
// case2 일때 sec을 newSec으로 변경
console.log(res.reduce((prev, cur) =&amp;gt; prev + cur, 0));&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(9) 크로아티아 알파벳&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;백준 모범 답안이 정말 인상 깊었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (9) 크로아티아 알파벳
// 실패 case01 (재귀법, tastcase가 많은 경우 콜스택 부족으로 실패)
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
input = input[0];
let count = 0;
const ctAlphabet = [&amp;quot;c=&amp;quot;, &amp;quot;c-&amp;quot;, &amp;quot;d-&amp;quot;, &amp;quot;lj&amp;quot;, &amp;quot;nj&amp;quot;, &amp;quot;s=&amp;quot;, &amp;quot;z=&amp;quot;];

function check2(input) {
  if (!input) {
    console.log(count);
    return;
  }
  const cur = input.slice(0, 2);
  if (ctAlphabet.indexOf(cur) !== -1) {
    count += 1;
    return check2(input.slice(2));
  } else if (cur === &amp;quot;dz&amp;quot;) {
    return check3(input.slice(0));
  } else {
    count += 1;
    return check2(input.slice(1));
  }
}

function check3(input) {
  if (input.slice(0, 3) === &amp;quot;dz=&amp;quot;) {
    count += 1;
    return check2(input.slice(3));
  } else {
    return check2(input);
  }
}

check2(input);

// case 백준 모범 답안 (replace 방식)
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
input = input[0];
const ctAlphabets = [&amp;quot;c=&amp;quot;, &amp;quot;c-&amp;quot;, &amp;quot;dz=&amp;quot;, &amp;quot;d-&amp;quot;, &amp;quot;lj&amp;quot;, &amp;quot;nj&amp;quot;, &amp;quot;s=&amp;quot;, &amp;quot;z=&amp;quot;];
for (let alphabet of ctAlphabets) {
  input = input.split(alphabet).join(&amp;quot;Q&amp;quot;);
}
console.log(input.length);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(10) 그룹 단어 체커&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (10) 그룹 단어 체커
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
let count = 0;

for (let i = 1; i &amp;lt; input.length; i++) {
  const rowSet = new Set([]);
  const row = input[i].trim().split(&amp;quot;&amp;quot;);
  let isGroup = true;

  for (let i = 0; i &amp;lt; row.length; i++) {
    const cur = row[i];
    const prev = i === 0 ? 0 : row[i - 1];
    if (cur !== prev &amp;amp;&amp;amp; rowSet.has(cur)) {
      isGroup = false;
      break;
    }
    rowSet.add(cur);
  }

  if (isGroup) {
    count += 1;
  }
}

console.log(count);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Dev/백준</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/222</guid>
      <comments>https://goforit.tistory.com/222#entry222comment</comments>
      <pubDate>Tue, 21 Sep 2021 14:32:20 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js] 백준 - 1차원 배열 문제 : 최소&amp;amp;최대, 최댓값, 숫자의 개수, 나머지, 평균, OX퀴즈, 평균은 넘겠지</title>
      <link>https://goforit.tistory.com/221</link>
      <description>&lt;h1&gt;  Node.js를 이용해 백준 문제 풀기&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DdiNZ/btrfw4HuESD/bWYwMqR38RCxPCfv4Hb3KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DdiNZ/btrfw4HuESD/bWYwMqR38RCxPCfv4Hb3KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DdiNZ/btrfw4HuESD/bWYwMqR38RCxPCfv4Hb3KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDdiNZ%2Fbtrfw4HuESD%2FbWYwMqR38RCxPCfv4Hb3KK%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;Node.js를 이용해서 백준 문제를 풀고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;배움&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;console.log를 통해서 하나 하나 출력하면, 느리므로 시간제한이 있는 경우 출력 값들을 변수에 계속 추가하는 식으로 하여 &lt;strong&gt;최종 마지막에 console.log 한번만 불러서 모두 출력하도록 하자.&lt;/strong&gt; (출력이 줄 단위로 표기해야 하는 경우 추가하는 값의 마지막에 개행 문자를 추가하는 식으로 하면 된다.)&lt;/li&gt;
&lt;li&gt;input을 받아오는 경우, 가끔 개행 문자중에 &lt;code&gt;\n&lt;/code&gt; 만이 아니라 &lt;code&gt;\r\n&lt;/code&gt;인 경우도 있다. 입력값을 받아올 때는 trim하여 확인해 보자 어떤 개행 문자가 들어 있지는 않은지&lt;ul&gt;
&lt;li&gt;윈도우 : &lt;code&gt;\r\n&lt;/code&gt;, 유닉스 &lt;code&gt;\n&lt;/code&gt;, 맥 &lt;code&gt;\r&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;너무 최신의 ES 문법의 경우 백준에서 인식하지 않을 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;1차원 배열 문제 (7개)&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h3&gt;(1) 최소, 최대 문제&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (1) 최소, 최대 문제
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

const nums = input[1].split(&amp;quot; &amp;quot;);
let max = Number(nums[0]);
let min = Number(nums[0]);

for (let i = 0; i &amp;lt; nums.length; i++) {
  const cur = Number(nums[i]);
  if (max &amp;lt; cur) {
    max = cur;
  } else if (min &amp;gt; cur) {
    min = cur;
  }
}

console.log(min, max);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 최댓값&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (2) 최댓값
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let max = Number(input[0]);
let index = 0;
for (let i = 0; i &amp;lt; input.length; i++) {
  const cur = Number(input[i]);
  if (cur &amp;gt; max) {
    max = cur;
    index = i;
  }
}

console.log(max);
console.log(index + 1);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(3) 숫자의 개수&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (3) 숫자의 개수
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

let multiply = 1;
input.map((item) =&amp;gt; {
  multiply = multiply * Number(item);
  // multiply *= Number(item) &amp;lt;- 의 경우 지원하지 않는 듯함
});

const counts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

const strMulti = multiply.toString();

for (let i = 0; i &amp;lt; strMulti.length; i++) {
  const value = Number(strMulti[i]);
  counts[value] += 1;
}

console.log(counts.join(&amp;quot;\n&amp;quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(4) 나머지&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (4) 나머지
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

const remainder = new Set([]);

for (let i = 0; i &amp;lt; input.length; i++) {
  remainder.add(Number(input[i]) % 42);
}

console.log(remainder.size);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(5) 평균&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (5) 평균
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

const count = Number(input[0]);
const scores = input[1].split(&amp;quot; &amp;quot;);
let max = Number(scores[0]);

for (let i = 0; i &amp;lt; scores.length; i++) {
  const cur = Number(scores[i]);
  if (cur &amp;gt; max) {
    max = cur;
  }
}

const newScores = scores.map((item) =&amp;gt; (Number(item) / max) * 100);
const average = newScores.reduce((prev, cur) =&amp;gt; prev + cur, 0) / count;

console.log(average);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(6) OX퀴즈&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (6) OX퀴즈
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
const count = Number(input[0]);
let answer = &amp;quot;&amp;quot;;

for (let i = 1; i &amp;lt;= count; i++) {
  const row = input[i].split(&amp;quot;&amp;quot;);
  let circleValue = 0;
  let rowSum = 0;

  for (let i = 0; i &amp;lt; row.length; i++) {
    const cur = row[i];
    const prev = row[i - 1];

    if (prev !== cur &amp;amp;&amp;amp; cur === &amp;quot;O&amp;quot;) {
      circleValue = 1;
      rowSum += circleValue;
    } else if (prev === cur &amp;amp;&amp;amp; cur === &amp;quot;O&amp;quot;) {
      circleValue += 1;
      rowSum += circleValue;
    }
  }

  answer += `${rowSum}\n`;
}

console.log(answer);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(7) 평균은 넘겠지&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (7) 평균은 넘겠지
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);
let answer = &amp;quot;&amp;quot;;

for (let i = 0; i &amp;lt; input.length; i++) {
  const row = input[i].trim().split(&amp;quot; &amp;quot;);
  const students = Number(row[0]);
  // 입력 유효성 검사
  if (!students || students &amp;gt; row.length) {
    continue;
  }
  // 평균 구하기
  let sum = 0;
  let pass = 0;
  for (let i = 1; i &amp;lt; row.length; i++) {
    sum += Number(row[i]);
  }
  const average = sum / students;

  // 평균을 넘는 사람의 수 구하기
  for (let i = 1; i &amp;lt; row.length; i++) {
    if (Number(row[i]) &amp;gt; average) {
      pass += 1;
    }
  }
  // 평균을 넘는 사람 비율 구하기(소수 3째 자리 까지 모두 표기)
  answer += `${parseFloat(
    Math.round((pass / students) * 100 * 1000) / 1000
  ).toFixed(3)}%\n`;
}
console.log(answer);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Dev/백준</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/221</guid>
      <comments>https://goforit.tistory.com/221#entry221comment</comments>
      <pubDate>Tue, 21 Sep 2021 14:30:58 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js] 백준 문제 풀기(BaeckJoon) - 기본 입출력 문제, if 조건문 문제, for &amp;amp; while 반복문 문제</title>
      <link>https://goforit.tistory.com/220</link>
      <description>&lt;h1&gt;  Node.js를 이용해 백준 문제 풀기&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uRBZh/btrfn4aeISe/SW21XHxKfQ1A40bUvcFhdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uRBZh/btrfn4aeISe/SW21XHxKfQ1A40bUvcFhdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uRBZh/btrfn4aeISe/SW21XHxKfQ1A40bUvcFhdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuRBZh%2Fbtrfn4aeISe%2FSW21XHxKfQ1A40bUvcFhdk%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;node.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;Node.js를 이용해서 백준 문제를 풀고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;Node.js로 문제 푸는 방식&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;
Node.js로 백준 문제를 푸는 경우 다른 언어에 비해서 입출력을 따로 다루어 주어야 합니다. 백준 문제의 경우 성능도 다루기 때문에 많은 사람들이 Node.js의 fs 파일 시스템 모듈을 활용하여 입력을 받고 출력하여 문제를 제출하게 됩니다. fs로 간혹 안되는 부분은 readline을 사용해야 합니다.
&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// fs 방식 입력 받기
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

// input은 각 txt 파일의 row를 string형태의 값을 Array로 가짐
// row가 1개 또는 여러개 인 경우 input을 신경써야 합니다.

// 출력 결과는 항상 console.log를 통해서 전달합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vscode에서 input.txt 파일을 생성하여, vscode에서 테스트용 코드를 작성할 때는 &lt;code&gt;&amp;quot;./input.txt&amp;quot;&lt;/code&gt;의 data input을 받아와 테스트를 진행하고 제출 하는 용도인 경우 &lt;code&gt;&amp;quot;/dev/stdin&amp;quot;&lt;/code&gt;으로 받아오게 됩니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;기본 입출력 &amp;amp; 산술 문제&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// (1) 2557번 Hello World
console.log(&amp;quot;Hello World!&amp;quot;);

// (2) 10718번 We love kriii
console.log(&amp;quot;강한친구 대한육군&amp;quot;);
console.log(&amp;quot;강한친구 대한육군&amp;quot;);

// (3) 10171번 고양이
console.log(&amp;quot;\\    /\\&amp;quot;);
console.log(&amp;quot; )  ( &amp;#39;)&amp;quot;);
console.log(&amp;quot;(  /  )&amp;quot;);
console.log(&amp;quot; \\(__)|&amp;quot;);

// (4) 10172번 개
console.log(&amp;quot;|\\_/|&amp;quot;);
console.log(&amp;quot;|q p|   /}&amp;quot;);
console.log(&amp;#39;( 0 )&amp;quot;&amp;quot;&amp;quot;\\&amp;#39;);
console.log(&amp;#39;|&amp;quot;^&amp;quot;`    |&amp;#39;);
console.log(&amp;quot;||_/=\\\\__|&amp;quot;);

// (5) 1000번 A+B
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

input = input[0];
input = input.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input[0], input[1]);

function solution(A, B) {
  console.log(A + B);
}

// (6) 1001번 A-B

const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

input = input[0];
input = input.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input[0], input[1]);

function solution(A, B) {
  console.log(A - B);
}

// (7) 10998번 AxB
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0], input1[1]);

function solution(A, B) {
  console.log(A * B);
}

// (8) 1008번 A/B
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0], input1[1]);

function solution(A, B) {
  console.log(A / B);
}

// (9) 10869번 사칙연산
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0], input1[1]);

function solution(A, B) {
  console.log(A + B);
  console.log(A - B);
  console.log(A * B);
  console.log(parseInt(A / B));
  console.log(A % B);
}

// (10) 10430번 나머지
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0], input1[1], input1[2]);

function solution(A, B, C) {
  console.log((A + B) % C);
  console.log(((A % C) + (B % C)) % C);
  console.log((A * B) % C);
  console.log(((A % C) * (B % C)) % C);
}

// (11) 2588번 곱셈
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];
let input2 = input[1];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);
input2 = input2.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1, input2);

function solution(A, B) {
  String(B)
    .split(&amp;quot;&amp;quot;)
    .reverse()
    .forEach((i) =&amp;gt; console.log(parseInt(i) * A));
  console.log(A * B);
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;if 조건문 문제&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// if 문 -------------------------------------------------------------
// (1) 1330번 두 수 비교하기
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0], input1[1]);

function solution(A, B) {
  if (A &amp;gt; B) {
    console.log(&amp;quot;&amp;gt;&amp;quot;);
  } else if (A &amp;lt; B) {
    console.log(&amp;quot;&amp;lt;&amp;quot;);
  } else {
    console.log(&amp;quot;==&amp;quot;);
  }
}

// (2) 9498번 시험 성적
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0]);

function solution(A) {
  if (90 &amp;lt;= A) {
    console.log(&amp;quot;A&amp;quot;);
  } else if (80 &amp;lt;= A) {
    console.log(&amp;quot;B&amp;quot;);
  } else if (70 &amp;lt;= A) {
    console.log(&amp;quot;C&amp;quot;);
  } else if (60 &amp;lt;= A) {
    console.log(&amp;quot;D&amp;quot;);
  } else {
    console.log(&amp;quot;F&amp;quot;);
  }
}

// (3) 2753번 윤년
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0]);

function solution(A) {
  if (A % 4 === 0 &amp;amp;&amp;amp; (A % 100 !== 0 || A % 400 === 0)) {
    console.log(1);
  } else {
    console.log(0);
  }
}

// (4) 14681번 사분면 고르기
// fs 모듈 반응 하지 않음
const readline = require(&amp;quot;readline&amp;quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

let input = [];

function solution(A, B) {
  if (A &amp;gt; 0 &amp;amp;&amp;amp; B &amp;gt; 0) {
    console.log(1);
  } else if (A &amp;lt; 0 &amp;amp;&amp;amp; B &amp;gt; 0) {
    console.log(2);
  } else if (A &amp;lt; 0 &amp;amp;&amp;amp; B &amp;lt; 0) {
    console.log(3);
  } else if (A &amp;gt; 0 &amp;amp;&amp;amp; B &amp;lt; 0) {
    console.log(4);
  }
}

rl.on(&amp;quot;line&amp;quot;, function (line) {
  input.push(line);
}).on(&amp;quot;close&amp;quot;, function () {
  solution(input[0], input[1]);
  process.exit();
});

// (5) 2884번 알람시계
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0], input1[1]);

function solution(A, B) {
  if (B &amp;gt;= 45) {
    console.log(A, B - 45);
  } else {
    console.log(A === 0 ? 23 : A - 1, 60 - Math.abs(B - 45));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h2&gt;for &amp;amp; while 문제&lt;/h2&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// For 문 -----------------------------------------
// (1) 구구단
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().trim().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];

input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0]);

function solution(A) {
  for (let i = 1; i &amp;lt;= 9; i++) {
    console.log(`${A} * ${i} = ${A * i}`);
  }
}

// (2) A+B - 3
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

for (let i = 0; i &amp;lt;= input.length - 1; i++) {
  let input1 = input[i];
  input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);
  if (input1.length &amp;lt;= 1) {
    continue;
  }
  solution(input1[0], input1[1]);
}

function solution(A, B) {
  console.log(A + B);
}

// (3) 합
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let input1 = input[0];
input1 = input1.split(&amp;quot; &amp;quot;).map((item) =&amp;gt; +item);

solution(input1[0]);

function solution(A) {
  let sum = 0;
  for (let i = 1; i &amp;lt;= A; i++) {
    sum += i;
  }
  console.log(sum);
}

// (4) 빠른 A + B
// 함수를 호출하는 횟수가 많아지면, 느려지므로 최대한 자료를 조합하여 한번에 출력할 수 있도록 함
// 1348ms
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
for (let i = 0; i &amp;lt;= input.length - 1; i++) {
  let input1 = input[i];
  input1 = input1.split(&amp;quot; &amp;quot;);
  if (input1.length &amp;lt;= 1) {
    continue;
  }
  answer += Number(input1[0]) + Number(input1[1]) + &amp;quot;\n&amp;quot;;
}

console.log(answer);

// (5) N 찍기
// 천만개 (10,000,000) 기준 5179ms
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
for (let i = 1; i &amp;lt;= input[0]; i++) {
  answer += i + &amp;quot;\n&amp;quot;;
}

console.log(answer);

// (6) 기찍 N
// 천만개 (10,000,000) 기준 5545ms
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
for (let i = 0; i &amp;lt; input[0]; i++) {
  answer += input[0] - i + &amp;quot;\n&amp;quot;;
}

console.log(answer);

// (7) A+B - 7
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
let count = 0;

for (let i = 0; i &amp;lt;= input.length - 1; i++) {
  let row = input[i].split(&amp;quot; &amp;quot;);
  if (row.length &amp;lt;= 1) {
    continue;
  } else {
    count += 1;
  }
  answer += `Case #${count}: ${Number(row[0]) + Number(row[1])}\n`;
}

console.log(answer);

// (8) A + B - 8
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
let count = 0;

for (let i = 0; i &amp;lt;= input.length - 1; i++) {
  let row = input[i].split(&amp;quot; &amp;quot;);
  if (row.length &amp;lt;= 1) {
    continue;
  } else {
    count += 1;
  }
  answer += `Case #${count}: ${row[0]} + ${row[1]} = ${
    Number(row[0]) + Number(row[1])
  }\n`;
}

console.log(answer);

// (9) 별 찍기 - 1
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
const input1 = Number(input[0]);
for (let i = 1; i &amp;lt;= input1; i++) {
  answer += &amp;quot;*&amp;quot;.repeat(i) + &amp;quot;\n&amp;quot;;
}

console.log(answer);

// (10) 별 찍기 - 2
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
const input1 = Number(input[0]);
for (let i = 1; i &amp;lt;= input1; i++) {
  answer += &amp;quot; &amp;quot;.repeat(input1 - i) + &amp;quot;*&amp;quot;.repeat(i) + &amp;quot;\n&amp;quot;;
}

console.log(answer);

// (11) X보다 작은 수
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;

const input1 = input[0].split(&amp;quot; &amp;quot;);
const input2 = input[1].split(&amp;quot; &amp;quot;);

for (let i = 0; i &amp;lt; Number(input1[0]); i++) {
  if (Number(input2[i]) &amp;lt; input1[1]) {
    answer += `${input2[i]} `;
  }
}

console.log(answer);

// while ----------------------------------------------
// (1) A + B - 5
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
let count = 0;
while (count &amp;lt; input.length) {
  const row = input[count];
  const items = row.split(&amp;quot; &amp;quot;);
  if (!(Number(items[0]) + Number(items[1]))) {
    count++;
    continue;
  }
  answer += `${Number(items[0]) + Number(items[1])}\n`;
  count++;
}

console.log(answer);

// (2) A + B - 4
// 정수인 입력값만 연산하여 출력해야 합니다.
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let answer = &amp;quot;&amp;quot;;
let count = 0;
while (count &amp;lt; input.length) {
  const row = input[count];
  const items = row.split(&amp;quot; &amp;quot;);
  if (Number(items[0]) % 1 !== 0 || Number(items[1]) % 1 !== 0) {
    count++;
    continue;
  }

  answer += `${parseInt(items[0]) + parseInt(items[1])}\n`;

  count++;
}

console.log(answer);

// (3) 더하기 사이클
// 숫자 앞에 0 붙이는 것은 05, 08 이런식을 이야기 함
const fs = require(&amp;quot;fs&amp;quot;);
const filePath = process.platform === &amp;quot;linux&amp;quot; ? &amp;quot;/dev/stdin&amp;quot; : &amp;quot;./input.txt&amp;quot;;
let input = fs.readFileSync(filePath).toString().split(&amp;quot;\n&amp;quot;);

let count = 0;
let prevNum = Number(input[0]);
let num = Number(input[0]);

while (true) {
  const ten = parseInt(num / 10);
  const one = num % 10;
  const afterOne = (ten + one) % 10;
  num = one * 10 + afterOne;
  count++;
  if (prevNum === num) {
    break;
  }
}

console.log(count);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Dev/백준</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/220</guid>
      <comments>https://goforit.tistory.com/220#entry220comment</comments>
      <pubDate>Fri, 17 Sep 2021 23:00:52 +0900</pubDate>
    </item>
    <item>
      <title>VSCode 스타일의 나만의 포트폴리오 만들기, 목차 기능, 부분 검색 기능, 생략된 글 더 보기 기능, 클립보드 기능</title>
      <link>https://goforit.tistory.com/219</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;  나만의 포트폴리오 만들기&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;252&quot; data-filename=&quot;visual_raccoon_code.gif&quot; width=&quot;609&quot; height=&quot;393&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uDlxt/btrePV6jSKP/iVZQoXrSwZrL35whts6ziK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uDlxt/btrePV6jSKP/iVZQoXrSwZrL35whts6ziK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uDlxt/btrePV6jSKP/iVZQoXrSwZrL35whts6ziK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/uDlxt/btrePV6jSKP/iVZQoXrSwZrL35whts6ziK/img.gif&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;252&quot; data-filename=&quot;visual_raccoon_code.gif&quot; width=&quot;609&quot; height=&quot;393&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;br/&gt;

&lt;P&gt;
요즘에는 취업 준비를 위해서 포트폴리오를 만들고, 이력서를 작성하는데 시간을 보내고 있습니다. 예전에 문제 해결을 위해서 여러 블로그 글을 돌아다니다가, 맥 OS 스타일의 개발 블로그 페이지를 보았습니다.
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://c17an.netlify.app/&quot;&gt;찬미니즘&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정말 잘 만들었더라구요, 그래서 저도 저만의 스타일로 포트폴리오를 만들고 싶었습니다. 그래서 생각한 것이 개발자가 항상 보는 에디터인 vscode의 스타일로 포트폴리오를 만들면 재미있겠다라는 생각이 들어서 만들게 되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/RaccoonCode96/vrcode&quot;&gt;VRCode 포트폴리오 github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;  기획&lt;/h2&gt;
&lt;p&gt;일단, 여러 개발자들이 만든 포트폴리오를 구글에서 찾아보았습니다. 대부분의 경우에는 1개의 page로 구성된 스크롤 스타일로 만들어져 있었습니다.&lt;/p&gt;
&lt;p&gt;그 중에서 해당 목차를 눌러서 해당 제목(title) 위치로 스크롤이 이동되는 형식이 맘에 들어 포트폴리오 프로젝트에서 구현해보기로 목표를 제일 처음 잡았습니다.&lt;/p&gt;
&lt;p&gt;Visual Studio Code를 모티브 하므로, Visual Raccoon Code로 프로젝트 제목을 정하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;VSCode 스타일 확인하기&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;VSCode의 경우 최상단 title bar, 좌측 Navigation의 이동 bar, Main 으로 크게 나뉘며, page에 따라서 좌측 Navigation bar에 대한 side bar?(tab)가 변하는 형태였습니다. 그리고 title bar 바로 아래에는 작업 창을 나타내는 tabs 가 존재했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title bar&lt;/code&gt; : vscode 아이콘, (파일, 편집, 선택영역 ... 등)의 버튼, title, 창 최소화, 최대화, 닫기 버튼&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tabs&lt;/code&gt; : 열려있는 작업 창들을 표현하는 tab들이 들어있는 tabs&lt;ul&gt;
&lt;li&gt;Tab: 각 탭 마다, 파일 아이콘 + 파일 이름 + x 버튼&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;side Navigation&lt;/code&gt; : 탐색기, 검색, git, 디버그, 확장 아이콘&lt;/li&gt;
&lt;li&gt;&lt;code&gt;side bar&lt;/code&gt; : Navigation을 클릭한 경로에 따른 side 영역&lt;ul&gt;
&lt;li&gt;탐색기: 파일 및 폴더 등의 디렉 구조를 표현함&lt;/li&gt;
&lt;li&gt;검색 : 검색, 바꾸기 input이 있으며 검색시, 해당 결과가 나타남&lt;/li&gt;
&lt;li&gt;git : git을 통해서 변경이 발생한 파일들의 목록이 나타남&lt;/li&gt;
&lt;li&gt;디버그: 디버그 실행 버튼들이 있음&lt;/li&gt;
&lt;li&gt;확장: 확장 서비스들의 아이콘, 제목, 설명등의 row가 보여짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt;: 해당 Navigation 경로에 따른 활성화된 tab의 content를 표현하는 영역&lt;/li&gt;
&lt;li&gt;&lt;code&gt;footer&lt;/code&gt; : 브렌치 표시, 여러 확장 프로그램에 대한 상태 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위 처럼 VSCode의 UI는 굉장히 많고, 그에 따른 표현되는 요소가 많다 보니, 단일 페이지 속성의 스크롤 만으로는 표현 되는 UI 요소가 너무 낭비인 것 같다는 생각이 들었습니다. 그래서 최대한 그런 요소들을 잘 활용해 보고 재미있게 만들기로 계획 했습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;VRCode 표현 계획&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title bar&lt;/code&gt;: header로 이름을 잡고, vscode icon과 편집 버튼들, 최대화, 최소화 등의 버튼들은 기능 없이 표현만 하고 title을 프로젝트 이름을 사용하도록 하였습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tabs&lt;/code&gt; : Tabs로 이름을 잡고, 내부적으로 Tab들이 존재하고, 현재 페이지에 대한 간접적인 목차 nav 형태로 만들고자 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Side Navigation&lt;/code&gt;: Navigation으로 이름을 잡고, Intro(탐색기), Contact(검색), github(git), study(디버그), project(확장) 해당 페이지(Route)와 연결 될 수 있도록 구상하였습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Side bar&lt;/code&gt;: Side bar로 이름을 잡고, Navigation 상황에 맞는 이스터 에그 스러운 표현들을 해보도록 계획 했습니다.&lt;ul&gt;
&lt;li&gt;Intro의 side bar는 Component 폴더에서 tabs와 같은 목차 제공&lt;/li&gt;
&lt;li&gt;Contact의 side bar는 회사와 동료들을 찾고 있다는 표현&lt;/li&gt;
&lt;li&gt;study의 side bar는 study 영역에 맞는 버튼 기능을 표현&lt;/li&gt;
&lt;li&gt;Project의 side bar는 각 프로젝트를 extension 처럼 표현&lt;/li&gt;
&lt;li&gt;github의 경우 해당 github 페이지 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mains&lt;/code&gt; : 각 Navigation에 맞는 main들을 표현&lt;ul&gt;
&lt;li&gt;intro의 main 경우 start, about, stacks에 대한 main들을 표현&lt;/li&gt;
&lt;li&gt;contact의 경우 contact에 대한 main 만을 표현&lt;/li&gt;
&lt;li&gt;study의 경우 study에 대한 main 만을 표현&lt;/li&gt;
&lt;li&gt;project의 경우 project에 대한 main만을 표현&lt;ul&gt;
&lt;li&gt;각 프로젝트를 클릭하는 경우 main을 project detail로 바꾸어 표현될 수 있게 하고자 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;github의 경우 route가 없고 단지 github를 새탭으로 띄워주는 역할만 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;footer&lt;/code&gt; : 말 그대로 footer로 master branch임을 보여주고, copy right를 작성하고자 함&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;  구현 사항&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;계획한 대로 화면을 구현하였고, 넣고 싶은 기능들이 구현하는 과정에서 몇가지 생겨 넣었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;목차 기능&lt;/code&gt; : tabs의 tab을 누르면, 해당 title의 위치로 스크롤이 이동&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;생각보다 간단한 기능으로, 단지 원하는 titile 요소에 id를 주어서 a 태그에 해당 id를 href로 주면 a 태그 클릭시 해당 요소로 스크롤이 이동하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;검색 기능&lt;/code&gt;: project route의 경우 side bar로 특정 project들을 extension 스타일로 결과를 볼수 있도록 검색 input을 만들었습니다.&lt;ul&gt;
&lt;li&gt;원했던 검색은 자동 완성 검색과 비슷하게 중간 중간 input에 넣은 값을 부분적으로 만족하는 경우도 검색 결과에 타나 날수 있도록 하고자 하였습니다.&lt;/li&gt;
&lt;li&gt;그래서, input과 검색 target들의 문자열을 하나하나 비교하는 로직으로 구성하였습니다. 원하는 검색은 input의 해당 문자열과, 순서가 맞으면 검색이 되게 하는 것으로 db의 많은 데이터가 아니라 몇개 안됨으로 아래와 같이 코드를 작성하여 구현하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 부분 일치 검색 함수
// (대소문자 상관 없이 입력된 값과 위치가 일치시 true 반환)
const check = useCallback((userInput, title) =&amp;gt; {
  const input = userInput.trim().toLowerCase();
  const target = title.trim().toLowerCase();
  let res = true;
  // input이 없는 경우 모든 결과가 보이게 함
  if (!input) {
    return res;
  }
  // input이 있는 경우 같은 index의 input의 문자열과 target의 문자열 비교, 한번이라도 다른 경우 false 반환
  for (let i = 0; i &amp;lt; input.length; i++) {
    if (input[i] !== target[i]) {
      res = false;
      break;
    }
  }
  return res;
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;생략된 글 더 보기 기능&lt;/code&gt; : instagram에서 구현해보았던 기능으로, 애초에 들온 값을 slice를 통해 제한하여 원래 값과 변화한 값을 두개를 가지고 표시 상태를 변경하여 보여주는 형식으로 구현하였습니다.&lt;ul&gt;
&lt;li&gt;저는 깔끔하게 하기 위해 더 보기가 ON인 경우 더보기 버튼이 사라지게 하였습니다. 상황에 맞게 더보기/ 더 안보기로 만들어도 상관은 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const TestComponent = ({ desc }) =&amp;gt; {
  const [isMore, setIsMore] = useState(false);
  const smallDesc = useMemo(() =&amp;gt; desc &amp;amp;&amp;amp; desc.slice(0, 19), [desc]);
  return (
    &amp;lt;p&amp;gt;
      {isMore ? desc : smallDesc}
      {!isMore &amp;amp;&amp;amp; (
        &amp;lt;span
          className=&amp;quot;study_desc_more&amp;quot;
          onClick={() =&amp;gt; {
            setIsMore(!isMore);
          }}
        &amp;gt;
          ...더 보기
        &amp;lt;/span&amp;gt;
      )}
    &amp;lt;/p&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;클립보드 복사 기능&lt;/code&gt; : 이메일, 전화번호 클릭시 이메일 또는 전화번호가 클립보드로 바로 복사되게 하는 기능을 구현했습니다. (Contact Route에서 유용)&lt;ul&gt;
&lt;li&gt;현재 브라우저의 기능들이 강해지면서 제공하고 있는 API들이 늘어나고 있습니다. 저는 그 중 &lt;code&gt;navigator.clipboard API&lt;/code&gt;를 사용했습니다.&lt;/li&gt;
&lt;li&gt;clipboard API의 &lt;code&gt;writeText()&lt;/code&gt;를 사용하면 손쉽게 사용자가 복사할 수 있게 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;또한, Antdesign의 nofication component를 사용하여 정상적으로 복사가 되었다는 알림을 보일 수 있게 하면 더욱 효과적입니다.&lt;/li&gt;
&lt;li&gt;환경에 따라서 clipboard를 제공하지 않는 경우도 있어서, 에러 방지 코드를 추가했습니다. (locallhost의 주소로 safari 실행시 문제가 되어 추가해습니다. 하지만, 배포 후 safari에서도 잘 동작함을 확인했습니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { notification } from &amp;quot;antd&amp;quot;;

const TestComponent = () =&amp;gt; {
  const getCopy = (type, value) =&amp;gt; {
    if (navigator.clipboard === undefined) {
      //(locallhost의 경우 모바일에서 undefined로 clipboard가 나타나서 작성하게 되었습니다.)
      openNotification(&amp;quot;&amp;quot;, &amp;quot;모바일 환경은 아직 개발중입니다.&amp;quot;);
    } else {
      navigator.clipboard
        .writeText(value)
        .then(() =&amp;gt; {
          openNotification(type, value);
        })
        // 혹시 모를 error 방지
        .catch(() =&amp;gt; {
          openNotification(&amp;quot;&amp;quot;, &amp;quot;모바일 환경은 아직 개발중입니다.&amp;quot;);
        });
    }
  };

  // antd의 notification 컴포넌트 활용
  const openNotification = (type, value) =&amp;gt; {
    const args = {
      message: type ? `  ${type} 복사되었습니다.` : `❌ 복사에 실패 했습니다.`,
      description: `${value}`,
      duration: 2,
    };
    notification.open(args);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div
        onClick={() =&amp;gt; {
          getCopy(&amp;quot;이메일이&amp;quot;, &amp;quot;어쩌구저쩌구@naver.com&amp;quot;);
        }}
      &amp;gt;
        이메일 복사하기
      &amp;lt;/div&amp;gt;
      &amp;lt;div
        onClick={() =&amp;gt; {
          getCopy(&amp;quot;전화번호가&amp;quot;, &amp;quot;010-1111-2222&amp;quot;);
        }}
      &amp;gt;
        전화번호 복사하기
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;배포&lt;/h2&gt;
&lt;p&gt;배포의 경우 CRA로 build 시키고 gh-pages를 활용하여 간단하게 하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;기타&lt;/h2&gt;
&lt;h3&gt;gif에 대한 생각&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;이번 프로젝트의 경우 사용되는 img가 굉장히 많았습니다, gif도 존재하고 생각보다 gif가 mp4 파일 보다 오히려 더 높은 파일 사이즈를 가지게 되는 경우가 많은 것 같습니다. 나름 optimize를 해도 효과적이지는 않았습니다. 나중에는 mp4를 사용해야 될 것 같습니다.&lt;/p&gt;</description>
      <category>Dev/포트폴리오(VRCode)</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/219</guid>
      <comments>https://goforit.tistory.com/219#entry219comment</comments>
      <pubDate>Mon, 13 Sep 2021 18:07:28 +0900</pubDate>
    </item>
    <item>
      <title>20210907 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit39 :CRA + gh-pages를 통한 프로젝트 배포, Firebase 보안 설정, 프로젝트를 마무리 하며</title>
      <link>https://goforit.tistory.com/218</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit39&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diqpZn/btreEJY5QJM/pryKW3HZIpB8lkZLSZnvK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diqpZn/btreEJY5QJM/pryKW3HZIpB8lkZLSZnvK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diqpZn/btreEJY5QJM/pryKW3HZIpB8lkZLSZnvK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiqpZn%2FbtreEJY5QJM%2FpryKW3HZIpB8lkZLSZnvK0%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 안내&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;해당 프로젝트에 관한 자세한 화면 개요 및 스타일, 상태 관리, 코드에 관한 사항은 &lt;a href=&quot;https://github.com/RaccoonCode96/redux_racstagram&quot;&gt;Github : RaccoonCode96/redux_racstagram &lt;/a&gt;을 확인해 주세요.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.09.07 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;CRA + gh-pages 배포&lt;/h2&gt;
&lt;p&gt;드디어 인스타그램 클론을 배포하였습니다.&lt;/p&gt;
&lt;p&gt;배포는 gh-pages를 통해서 배포하였습니다. (gh-pages가 제일 간단하고, 그래도 연관성 있는 도메인과 path를 가질수 있기 때문입니다.)&lt;/p&gt;
&lt;p&gt;gh-pages의 경우 배포하면, repo 이름이 들어간 도메인, Path로 배포가 됩니다. 하지만, React-router-dom에서 다른 설정을 하지 않으면 &lt;code&gt;사용자.github.io/&lt;/code&gt;로 path를 나타내기 때문에 주소에 repo 이름이 기본적으로 깔리게 &lt;code&gt;basename&lt;/code&gt;을 설정하였습니다.&lt;/p&gt;
&lt;p&gt;그럼, gh-pages를 npm으로 설치하고 pakage.json에 script를 작성합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm i gh-pages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;build 명령어로 CRA의 build를 지정하고, predeploy를 지정하여 항상 deploy 전에 build를 할 수 있도록 합니다. 그리고 deploy 명령어에 &lt;code&gt;gh-pages -d 빌드폴더명&lt;/code&gt; 으로 지정합니다. (어떤 빌더를 쓰느냐에 따라 build 폴더가 다르기 때문이죠, parcel의 경우 dist 입니다.)&lt;/li&gt;
&lt;li&gt;hompage 속성에 배포할 주소를 넣어줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// pakage.json
{
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;start&amp;quot;: &amp;quot;react-scripts start&amp;quot;,
    &amp;quot;eject&amp;quot;: &amp;quot;react-scripts eject&amp;quot;,
    &amp;quot;build&amp;quot;: &amp;quot;react-scripts build&amp;quot;,
    &amp;quot;predeploy&amp;quot;: &amp;quot;npm run build&amp;quot;,
    &amp;quot;deploy&amp;quot;: &amp;quot;gh-pages -d build&amp;quot;
  },
  &amp;quot;homepage&amp;quot;: &amp;quot;https://RaccoonCode96.github.io/redux_racstagram&amp;quot; // 배포 주소
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;빌드가 완료되면, build 폴더가 생성되며 staic폴더와 함께 필요한 파일들이 자동으로 build 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;meta 태그 수정&lt;/code&gt; : 빌드전에 public 폴더의 index.html의 meta tag와 manifest.json을 앱에 맞게 수정해 주세요. (더 멋진 앱을 만들고 싶다면, 당연히 title, favicon은 기본으로 넣어주어야 겠죠.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;이미지 관리&lt;/code&gt; : 앱에 사용되는 이미지들은 src의 images 폴더에 넣어 관리하고 build를 하면 build 폴더의 static 폴더의 media 폴더명으로 들어가게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCSS&lt;/code&gt; : SCSS를 사용하면, build를 위해 sass를 설치해야 합니다. (node-sass의 경우 곧 dart-sass 위주로 변경된다고 하니 기본적으로 제공하는 sass를 설치하여 사용합시다. sass-loader의 경우 CRA에서 기본적으로 제공하기 때문에 eject할 필요가 없습니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;npm run deploy 명령어를 통해 build 부터 gh-pages의 publish까지 모두 해결 할 수 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;firebase 보안 규칙 수정&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;firebase의 경우 강력한 보안 규칙을 제공합니다. firebase 콘솔에 들어가 Auth, firestore, storage 서비스의 접근에 대한 규칙을 설정 할수 있습니다. 데이터 베이스의 CRUD에 대한 조건을 걸을수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auth의 경우 domain에 사용할 배포된 도메인을 추가시켜서 사용할 수 있게 합니다.&lt;/li&gt;
&lt;li&gt;firestore 설정에서는 기본적으로 CRUD의 경우 로그인 사용자만 할 수있도록 하였고, 예외적으로 회원가입시 이름 중복체크 때문에 이름만 Read 할 수 있도록 설정 했습니다.&lt;/li&gt;
&lt;li&gt;storage 서비스의 경우 로그인 사용자만 CRUD 할수 있도록 하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;API 키(Browser key) 제한&lt;/h3&gt;
&lt;p&gt;구글에서는 API를 사용할 수 있는 도메인을 제한하는 기능을 제공합니다. 이를 통해서 API 접근 key를 알더라도 다른 사이트에서 사용할 수 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://console.cloud.google.com/apis&quot;&gt;https://console.cloud.google.com/apis&lt;/a&gt; 에 접속하여 Credentials (사용자 인증 정보) 탭을 클릭하여 API 키의 해당 프로젝트의 API 키를 클릭하여 들어갑니다.&lt;/p&gt;
&lt;p&gt;애플리케이션 제한사항으로 적절한 것을 선택합니다(프로젝트에 따라 다릅니다.) 저는 웹사이트 이름으로 제한을 걸었고, 아래 API를 사용할 수 있는 도메인을 넣어 설정해 줍니다. &lt;code&gt;배포할 사이트, localhost, firebase 프로젝트 서버 주소&lt;/code&gt; (주소 도메인 뒤에 path로 애스터리스크를 넣어 유연하게 적용시킬 수 있습니다.)&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;프로젝트를 마치며&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;완벽히 인스타그램의 모든 기능을 클론하지는 못하였지만, 부분적으로 클론을 실시 하였습니다.&lt;/p&gt;
&lt;p&gt;일반적인 클론과는 다르게 모바일 환경에서도 사용할 수 있도록 반응형 스타일을 적용하였고, 최대한 모바일 환경의 UI를 참고하여 만들었습니다.&lt;/p&gt;
&lt;p&gt;개발을 진행하면서, 그래도 끝을 내야되진 않을 까? 라는 생각이 많이 들었습니다. 물론, 계속해서 기능들을 구현하는 것도 좋지만 끝을 맺는 것도 중요하다고 생각합니다. 끝을 내야 결과물이 나오기 때문이죠.&lt;/p&gt;
&lt;p&gt;일단은 현재 프로젝트에서 주요적으로 다룬 게시글CRUD, 프로필 수정, 무한스크롤, 좋아요, 댓글으로 마무리를 하고자 합니다.&lt;/p&gt;
&lt;p&gt;프로젝트를 진행 중 생각하는 시간을 가지고, 기능을 구현하면서 정말 많이 배웠습니다. Redux를 상태관리로 하는 앱의 전체적인 동작 방식에 친숙해졌습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;아쉬운점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;테스트 코드를 작성하지 않아서, 지속적인 테스트 환경이 없다는 게 아쉽습니다.&lt;/li&gt;
&lt;li&gt;Type을 아직 propType이나 TypeScript를 통해 제한을 두지도 않았기 때문에 아쉽습니다.&lt;/li&gt;
&lt;li&gt;인스타그램의 여러가지 요소들을 더 클론하고 싶었지만, 너무 이 프로젝트 자체에 매달릴수는 없기에 너무 아쉬웠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;9월 1일 ~ 9월 6일 까지 포트폴리오 웹사이트를 제작하고 있었습니다. &lt;br/&gt; 다음 시간에 완성된 포트폴리오에 대해서 포스팅 해보려고 합니다.&lt;/h3&gt;
&lt;/blockquote&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/218</guid>
      <comments>https://goforit.tistory.com/218#entry218comment</comments>
      <pubDate>Fri, 10 Sep 2021 15:49:48 +0900</pubDate>
    </item>
    <item>
      <title>20210831 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit38 : 좋아요 기능 보완을 위한 대대적인 코드 리팩토링, 좋아요 기능 보완</title>
      <link>https://goforit.tistory.com/217</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit38&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9fDCb/btreB8ZaGdB/JYDUnaBbTFq3ciRBlQYRkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9fDCb/btreB8ZaGdB/JYDUnaBbTFq3ciRBlQYRkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9fDCb/btreB8ZaGdB/JYDUnaBbTFq3ciRBlQYRkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9fDCb%2FbtreB8ZaGdB%2FJYDUnaBbTFq3ciRBlQYRkk%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 안내&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;해당 프로젝트에 관한 자세한 화면 개요 및 스타일, 상태 관리, 코드에 관한 사항은 &lt;a href=&quot;https://github.com/RaccoonCode96/redux_racstagram&quot;&gt;Github : RaccoonCode96/redux_racstagram &lt;/a&gt;을 확인해 주세요.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.31 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;좋아요 기능 보완을 위한 Post관련 리팩토링&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;이전글의 &amp;#39;좋아요 기능 구현의 딜레마&amp;#39;에서 다루었던 문제를 해결하고자 Post와 관련된 데이터 요청 함수 및 redux state를 대대적으로 리팩토링하였습니다. 이를 통해 Post에 상응하여 구현해야하는 Like 관련 데이터 요청 함수 및 redux state를 그나마 깔끔하게 보완하여 구현할 수 있었습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;먼저 Like를 구현하기 앞서 Like와 Post는 관계가 강하게 만들어져 있기 떄문에, Post를 조금더 간단하게 코드를 만들어야 Like 구현시에도 편할 것을 생각하였습니다. 그래서 Post의 코드를 줄이는 시도를 해보았습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;과거 구현 방식&lt;/h3&gt;
&lt;p&gt;Post 관련한 서버에 데이터를 요청 하는 함수(redux dispatch)는 4가지로 구성되어 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dispatch&lt;ul&gt;
&lt;li&gt;getAllPosts : 모든 글 요청하는 함수로 받아온 일정 개수의 데이터는 Home에서 활용됩니다.&lt;/li&gt;
&lt;li&gt;getCurrentUserPosts : 현재 유저가 작성한 글만을 요청하는 함수로 받아온 일정 개수의 데이터는 Profile 페이지에서 활용됩니다.&lt;/li&gt;
&lt;li&gt;getUserPosts : 특정 유저가 작성한 글만을 요청하는 함수로 받아온 일정 개수의 데이터는 User 페이지에서 활용됩니다.&lt;/li&gt;
&lt;li&gt;getMorePosts : 무한스크롤 observer가 작동하여 getMorePosts를 트리거 하면 &lt;strong&gt;각 페이지 특성에 맞는 데이터를 추가로 가져와 allPosts, currentUserPosts, userPosts State에 추가합니다.&lt;/strong&gt; (즉, 공유되어 재사용됩니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;State&lt;ul&gt;
&lt;li&gt;allPosts : getAllPosts로 받아온 데이터를 저장하는 redux state (Home과 연결)&lt;/li&gt;
&lt;li&gt;currentUserPosts : getCurrentUserPosts로 받아온 데이터를 저장하는 redux state (Profile과 연결)&lt;/li&gt;
&lt;li&gt;userPosts : getUserPosts로 받아온 데이터를 저장하는 redux state (User와 연결)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;시도1) getMorePosts 다른 함수에 통합하여 줄이기&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;단순히 초기 데이터 몇개만 가져와 무한스크롤 동작 준비를 초기화 시키는 getAllPosts, getCurrentUserPosts, getUserPosts 함수에 getMore을 통합 시키는 시도를 해보았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;getMorePosts의 경우 getAllPosts, getCurrentUserPosts, getUserPosts를 통해 초기 데이터가 들어오고 나서 무한스크롤 observer가 동작하고 스크롤이 특정 하단 부분에 도착하면 getMorePosts가 트리거 되는 방식입니다.&lt;/li&gt;
&lt;li&gt;위를 변형하여 getAllPosts, getCurrentUserPosts, getUserPosts에 기존 역할 수행하는 Init 상태, getMorePosts 기능을 수행할 More, None 상태를 구분하여 동작하도록 설계 하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// getMorePosts 기능이 흡수된 getAllPostsThunk
export const getAllPostsThunk = createAsyncThunk(
    &amp;#39;redux-racstagram/post/getAllPostsThunk&amp;#39;,
    async (_, thunkAPI) =&amp;gt; {
        try {
     const {post: {allPosts}} = thunkAPI.getState()

    // 데이터 끝값의 postDate
    const { postDate } = allPosts[allPosts.length - 1]

      let type // 각 상황에 맞게 reducer에서 처리되도록 하기 위한 type
            let qeury = dbService
                .collection(&amp;#39;posts&amp;#39;)
                .orderBy(&amp;#39;postDate&amp;#39;, &amp;#39;desc&amp;#39;)

      // 더불러올 기준 데이터 값인 postDate가 있는 경우 -&amp;gt; more
      if (postDate) {
        qeury = .startAfter(postDate)
        type = &amp;#39;more&amp;#39;
      } else {
        // 처음에 가져온 데이터가 없는 경우 -&amp;gt; init (데이터 가져오기)
        type = &amp;#39;init&amp;#39;
      }

     const {docs}  = await qeury.limit(6).get();

      // 더이상 가져올 데이터가 없는 경우
      if (!docs) {
        return {type: &amp;#39;none&amp;#39;}
      }

            const posts = docs.map((doc) =&amp;gt; ({
                postId: doc.id,
                ...doc.data(),
            }));

            return {type, posts};

    } catch ({ code, message }) {
            return thunkAPI.rejectWithValue({ code, message });
        }
    }
);

// ------- ExtraReducers of redux-toolkit slice -----
const post = createSlice({
  name: &amp;#39;post&amp;#39;
  initialState,
  reducers: {},
  extraReducer: {
  [getAllPostsThunk.pending]: (state) =&amp;gt; ({
            ...state,
            getAllPosts: { ...state.getAllPosts, loading: true },
        }),
        [getAllPostsThunk.fulfilled]: (state, { payload }) =&amp;gt; {
      switch (payload.type) {
        case &amp;#39;init&amp;#39;:
          return {
        ...state,
        allPosts: payload.posts,
        getAllPosts: { ...state.getAllPosts, loading: false, isGet: true, getState: payload.type },
        }
        case &amp;#39;more&amp;#39;:
          return {
            ...state,
        allPosts: [...state.allPosts, ...payload.posts],
        getAllPosts: { ...state.getAllPosts, loading: false, isGet: true, getState: payload.type },
          }
        }
        case &amp;#39;none&amp;#39;:
          return {
            ...state,
        getAllPosts: { ...state.getAllPosts, loading: false, isNone: true, getState: payload.type },
          }
        default:
          return {...state}
        }
    },
        [getAllPostsThunk.rejected]: (state, { payload }) =&amp;gt; ({
            ...state,
            getAllPosts: {
                ...state.getAllPosts,
                loading: false,
                getError: payload,
            },
        })
  }
})&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;통합방식의 문제점&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;위처럼 init, more, none type을 통해서 getMore 기능을 통합 시켰습니다.&lt;br&gt;하지만, 나중에 isNone 상태를 observer에 달아야 하는데 currentUser Profile, user Profile의 경우 하나의 컴포넌트를 재사용하여 공유하는데 isNone 상태를 각각 고려해서 달아주어야 하기때문에 기존의 isNone 부착 방식보다 까다로워 졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;차라리 getMorePost 함수의 경우 처음에 구현한 방식이 더 맞는 방식인 것을 깨달았습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;통합하는 과정에서 여러 기능을 같이 가지고 있다보니 더 복잡해짐을 느꼈으며 하나의 기능은 하나의 함수가 가지는게 옳바름을 다시 한번 느꼈습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;다른 방식 생각하기&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;과연, &amp;#39;조건을 복잡하게 만드는게 무엇일까?&amp;#39;를 생각해 보았습니다. 재사용성을 높이고자 한 컴포넌트에서 계속 서로 다른 것을 맞추려고 조건을 넣어 통합하려다 보니 더 복잡하게 만드는 것 같다고 생각이 되어졌습니다.&lt;/p&gt;
&lt;p&gt;기존에 만들어진 재사용가능한 컴포넌트들을 다시한번 바라보며, pathname에 대한 조건식이 많다는 것을 인지하였고 path 통합 또는 상위 컴포넌트에서 페이지 상황에 맞는 function 및 state를 props로 전달하는 방식으로 개선해야 겠다고 생각했습니다.&lt;/p&gt;
&lt;p&gt;애초에 처음부터 &lt;strong&gt;현재 유저과 특정 유저의 Profile, Posts의 경우 공통점이 많아 통합했어야 했는데,&lt;/strong&gt; route pathname을 &amp;#39;/profile&amp;#39;, &amp;#39;/user/:userId&amp;#39;로 나누고 시작하다 보니 이에 대한 데이터 요청 함수도 나누어서 구현하였던게 문제인것 같았습니다.&lt;/p&gt;
&lt;p&gt;그래서 대대적으로 &lt;strong&gt;현재 유저와 특정 유저에 관련된 항목은 모두 같은 route pathname으로 시작하여 같은 요청 함수를 사용하도록 리팩토링을 진행하였습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;시도2) 현재 유저와 특정 유저 관련 코드 통합&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;currentUser, User 데이터 요청을 따로 나누는게 아닌 User로 통합하여 currentUser에 대한 코드를 없앴습니다. 단순히 글, 댓글, 프로필 수정에 대한 접근만 uid 비교를 통해 허용 하고 똑같이 user 로직을 사용하도록 변경하였습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;시도3) 재사용하는 컴포넌트 내부 조건식 제거&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;재사용하는 컴포넌트 내부의 조건식을 제거하고, &lt;strong&gt;조건이 필요한 함수 및 state의 경우 컴포넌트 외부에서 prop으로 전달하여 동적으로 상황에 맞게 처리하도록 변경하였습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;결과&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;결과적으로, 기존의 post 관련 dispatch 4개에서 3개로 줄여 졌습니다.&lt;/p&gt;
&lt;p&gt;또한, getMorePosts는 내부적으로 All, CurrentUser, User 3개와 관련된 로직을 작성하였었는데 2개로 관련 로직이 줄여졌습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;변경 전 : getAllPosts, getCurrentUserPosts, getUserPosts, getMorePosts(all, currentUser, user)&lt;/li&gt;
&lt;li&gt;변경 후 : getAllPosts, getUserPosts, getMorePosts(all, user)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;좋아요 기능 보완하기&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;Post 관련 로직이 줄었기 때문에, getAllLikes, getUserLikes, getMoreLikes 3개의 distpatch를 구현하였습니다.&lt;br&gt;동작 시기는 Post와 똑같기 때문에 Post관련 dispatch 내부에서 병렬적으로 요청하는 방식으로 구현하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;기존에 Like 상태를 변경하는 setLikeOnThunk, setLikeOffThunk 동작의 경우 db만 변경하고 다시 getLikes로 like 정보를 요청하는 방식이였습니다.&lt;/p&gt;
&lt;p&gt;이 방식에 무한스크롤를 반영하게 되면 set하고 다시 get할 때 기존의 무한스크롤로 불려진 다수의 post 데이터들과 다시 불러온 like 데이터가 맞지 않게 됩니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;그래서, 다시 db에서 데이터를 불러오는 것이 아니라 &lt;strong&gt;like 상태를 set하면 db에 반영하고 바로 redux state 값에도 반영하여 기존에 불러온 like 데이터들이 초기화 되지않고 화면에서도 값이 반영되게 하였습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;또한, allLikes에 반영할지 userLikes에 반영하지 사전에 set요청시 pathName을 조건으로 type을 정해 type과 더불러올 like 기준 값을 같이 dispatch 인자로 넣어 실행시키도록 하였습니다.&lt;/p&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/217</guid>
      <comments>https://goforit.tistory.com/217#entry217comment</comments>
      <pubDate>Fri, 10 Sep 2021 13:34:20 +0900</pubDate>
    </item>
    <item>
      <title>20210825-30 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit37 : 현재 프로젝트에서의 좋아요 기능 구현의 딜레마</title>
      <link>https://goforit.tistory.com/216</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit37&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nvZc1/btrdDxlqQ9p/lWFIh1Yq752KdW0wHOTVB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nvZc1/btrdDxlqQ9p/lWFIh1Yq752KdW0wHOTVB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nvZc1/btrdDxlqQ9p/lWFIh1Yq752KdW0wHOTVB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnvZc1%2FbtrdDxlqQ9p%2FlWFIh1Yq752KdW0wHOTVB0%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 안내&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;해당 프로젝트에 관한 자세한 화면 개요 및 스타일, 상태 관리, 코드에 관한 사항은 &lt;a href=&quot;https://github.com/RaccoonCode96/redux_racstagram&quot;&gt;Github : RaccoonCode96/redux_racstagram &lt;/a&gt;을 확인해 주세요.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;자주 올라오던 포스팅이 최근에 몇 일간 올라오지 않았는데요. 25일 ~ 30일 까지 아버지 사과 과수원 수확을 도와드리느라 한동안 포스팅을 못했습니다.&lt;/p&gt;
&lt;p&gt;당연히, 프로젝트 작업은 못했고 간간히 프로젝트 이슈들만 생각해보는 정도만 한 것 같습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjYcFZ/btrdJ3bODgN/Xc3Kw5vZ8NyUP0rNlDZ8f0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjYcFZ/btrdJ3bODgN/Xc3Kw5vZ8NyUP0rNlDZ8f0/img.jpg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot; data-filename=&quot;apple3.jpg&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjYcFZ/btrdJ3bODgN/Xc3Kw5vZ8NyUP0rNlDZ8f0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjYcFZ%2FbtrdJ3bODgN%2FXc3Kw5vZ8NyUP0rNlDZ8f0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYNpIm/btrdJjsp1b8/dY4kmW9Yfoug6zpeu2ZZG0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYNpIm/btrdJjsp1b8/dY4kmW9Yfoug6zpeu2ZZG0/img.jpg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot; data-filename=&quot;apple1.jpg&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYNpIm/btrdJjsp1b8/dY4kmW9Yfoug6zpeu2ZZG0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYNpIm%2FbtrdJjsp1b8%2FdY4kmW9Yfoug6zpeu2ZZG0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ6xhP/btrdK6lOZET/S66js0JN7HsI9gkYw9kKa0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ6xhP/btrdK6lOZET/S66js0JN7HsI9gkYw9kKa0/img.jpg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot; data-filename=&quot;apple2.jpg&quot; style=&quot;width: 32.5581%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ6xhP/btrdK6lOZET/S66js0JN7HsI9gkYw9kKa0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ6xhP%2FbtrdK6lOZET%2FS66js0JN7HsI9gkYw9kKa0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.25 ~ 30 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;좋아요 기능 구현에 있어서 딜레마&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;저번에 좋아요 기능을 구현했었습니다. 그런데 다른 작업을 하면서 좋아요 기능을 모두다 구현한게 아니라는 것을 깨달았습니다.&lt;/p&gt;
&lt;p&gt;예전에 구현한 좋아요의 경우 db에서 post 내부가 아니라 따로 like를 관리합니다. 그때 좋아요를 구현할때 해당 post와 like 정보가 짝을 맞추어 화면에 표시하는게 중요했었습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;문제인식&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;프로젝트 특성상 post는 각 페이지에 필요한 posts를 가져와야 합니다. posts를 해당 페이지 특성에 맞게 getAllPosts, getCurrentUserPosts, getUserPosts 3가지 형식으로 요청함수를 제작하고 redux state도 3개로 구분하여 관리하기 때문에 like 요청도 이를 맞추어 제작해야한다는 것입니다.&lt;br&gt;또한 무한 스크롤 기능이 추가되었기 때문에 getMorePosts처럼 getMoreLikes도 구현해야합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;post 관련 요청 함수&lt;ul&gt;
&lt;li&gt;getAllPosts : 시간순으로 모든 유저의 글을 요청함 (6개) -&amp;gt; Home page&lt;/li&gt;
&lt;li&gt;getCurrentUserPosts : 현재 유저의 글만을 요청함 (6개) -&amp;gt; Profile page&lt;/li&gt;
&lt;li&gt;getUserPosts : 특정 유저의 글만을 요청함 (6개) -&amp;gt; userProfile page&lt;/li&gt;
&lt;li&gt;getMorePosts : 무한스크롤 기능으로 각 post 형식에 따라 마지막 데이터 다음에 있는 데이터를 가져와 해당 redux state에 추가함 (6개)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;post 관련 redux state&lt;ul&gt;
&lt;li&gt;allPosts, currentUserPosts, userPosts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예전에 구현한 like&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;getLikes : getAllPosts에 해당하는 likes로 무한스크롤이 반영되지 않아 특정 개수 없이 모든 자료를 가져오게 됩니다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;이전에는 getAllPosts에 대한 likes만 구현했고 무한스크롤도 반영하지 않아서 한번에 전체 likes를 가져오기 떄문에 돌아는 가지만 효율적이지 못했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;위와 같은 상황때문에 post와 맞게 like를 구현하려면, getAllLikes, getCurrentUserLikes, getUserLikes, getMoreLikes 이렇게 4개를 구현해야 합니다. 4개씩이나 거이 비슷하게 구현해야 한다고 생각하니 뭔가 보일러 플레이트 코드같고, 좀더 효율적인 코드를 작성하고 싶다는 생각을 했습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;동시에, db에서 likes와 post를 따로 관리하도록 판단한 것이 과연 옳았을 까라는 생각이 들기도 하였으며 분리 하지 말껄이라는 후회도 있었습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;고찰&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h3&gt;생각 1) like관련 데이터를 post db 내부에서 관리하기 (통합 관리)&lt;/h3&gt;
&lt;p&gt;통합 관리로 하면, post 데이터와 한꺼번에 가져올 수 있어 굳이 like 요청 함수를 만들 필요가 없게 됩니다.&lt;/p&gt;
&lt;p&gt;하지만, like 기능상으로 좋아요를 누르사람의 uid가 모두 들어가게 됨으로서 like의 자료가 엄청나게 커질 수 있기 때문에 post의 데이터 크기에 부담을 주는 것은 변하지 않는다고 생각했습니다.&lt;/p&gt;
&lt;p&gt;또한, like의 변화를 반영하게 되면, 나머지 like와 관련없는 post의 데이터도 재 렌더링 될것으로 예상 됩니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;생각 2) 각 postId에 맞는 like 데이터를 각각의 post 마다 server에 요청하기&lt;/h3&gt;
&lt;p&gt;db의 데이터 요청은 redux의 state로 받기 때문에 각 post에 맞는 like데이터를 담을 state 하나 하나를 만들어야 함으로 너무 많은 state를 만들게 될 것임으로 비효율적인 방법인 것 같습니다.&lt;/p&gt;
&lt;p&gt;그러므로, like 데이터는 배열 형태의 1개의 state로 담아와야 할 것 같습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;생각 3) getMorePosts 요청 함수의 통합&lt;/h3&gt;
&lt;p&gt;위의 생각들을 조합해 보면, db분리 관리와 1개의 state 사용은 불가피 한것으로 판단됩니다.&lt;/p&gt;
&lt;p&gt;그러면, 기존의 getMorePosts 함수를 getAllLikes, getCurrentUserLikes, getUserLikes에 각각 포함되도록 하여 getMorePosts 함수를 제거하여 해보는 것은 어떤가 생각해 보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4개의 함수 구현에서 1개만 줄여 3개로 만들어도 총 6개이므로 그나마 효율적으로 함수를 만들수 있다고 생각되어 시도해볼 생각입니다.&lt;/strong&gt;&lt;/p&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/216</guid>
      <comments>https://goforit.tistory.com/216#entry216comment</comments>
      <pubDate>Tue, 31 Aug 2021 23:08:25 +0900</pubDate>
    </item>
    <item>
      <title>20210823,24 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit36 : 유효성 검사 및 피드백, Material UI TextField 커스텀 스타일 적용하기, 유효성 검사에 따른 submit 버튼 disabled 처리</title>
      <link>https://goforit.tistory.com/215</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit36&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lIXrG/btrdJ33Vjv0/ri7tcskdQ23VxGJjpuSfXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lIXrG/btrdJ33Vjv0/ri7tcskdQ23VxGJjpuSfXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lIXrG/btrdJ33Vjv0/ri7tcskdQ23VxGJjpuSfXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlIXrG%2FbtrdJ33Vjv0%2Fri7tcskdQ23VxGJjpuSfXk%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 안내&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;해당 프로젝트에 관한 자세한 화면 개요 및 스타일, 상태 관리, 코드에 관한 사항은 &lt;a href=&quot;https://github.com/RaccoonCode96/redux_racstagram&quot;&gt;Github : RaccoonCode96/redux_racstagram &lt;/a&gt;을 확인해 주세요.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.23 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;Material UI TextField 컴포넌트 도입을 통한 유효성 검사(Validation Check) 및 피드백&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;오늘은 사용자의 input을 받는 경우 유효성 검사를 실시하여 submit을 제한하고, 형식에 맞는 값을 넣도록 안내(피드백)하도록 알림을 보여 줄수 있도록 하고자 하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;과거 구현 방식&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;기존에 input의 경우에는 input을 submit을 누르는 경우 confirm 창을 띄워서 사용자에게 안내했습니다. 이렇게 confirm 창으로 알려주게 되면 사용자가 input을 입력 후에 submit을 계속 눌러서 현재 상황에 대한 안내를 받게 됨으로 사용하는데 불편하다고 생각했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자는 submit을 누르고 confirm 모달 창 안내를 확인한 후 확인을 눌러야 함으로서 계속 2번의 클릭이 필요하게 됩니다.&lt;/li&gt;
&lt;li&gt;사용자가 input을 작성하면서 input 값에 대한 안내를 바로 화면에서 볼 수 있도록 하고자 합니다.&lt;/li&gt;
&lt;li&gt;예전에 구현했던 자동 input 중복 체크를 통한 안내(피드백) 구현과 비슷합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;이렇게 화면에 바로 안내메세지를 보여주는 방식은 상태 메세지를 input 하단에 나타나게 구현할 수 있습니다. 기존에 submit에 대한 반응 처럼(예.로그인 실패) 하나의 영역에 모든 안내(피드백) 메세지를 보이도록 구현했었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예시) 기존: 동일한 영역에서 피드백 제공 형식&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;626&quot; data-filename=&quot;auto_check.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bolyEy/btrdIlYBVYS/3cmSfev59eC4f8S65wi4K0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bolyEy/btrdIlYBVYS/3cmSfev59eC4f8S65wi4K0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bolyEy/btrdIlYBVYS/3cmSfev59eC4f8S65wi4K0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bolyEy/btrdIlYBVYS/3cmSfev59eC4f8S65wi4K0/img.gif&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;626&quot; data-filename=&quot;auto_check.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;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;현재 구현 방식&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;기존의 동일한 영역에서 피드백을 제공하는게 아닌 각 input에 해당하는 부분에서 해당 피드백을 제공하는게 더 좋을 것 같다고 생각했고, 해당 형식에 맞지 않으면 submit 자체를 disabled 처리하여 접근하지 못하게 막음으로서 안전성을 높이고자 하였습니다.&lt;/p&gt;
&lt;p&gt;위에서 말한 것과 같이 각 input 영역에 피드백을 제공하기 위해서, 저는 Material UI TextField의 helperText를 활용하는 것으로 접근하였습니다.&lt;/p&gt;
&lt;p&gt;Material UI의 TextField는 자체적으로 Validation에 대한 error 표시를 쉽게 구현할 수 있도록 만들어져 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;하지만, 현재 상황에서는 단지 error만 표시하는게 아닌 여러가지 상태의 안내를 구현하고자 했습니다. 그렇기 때문에 TextField 컴포넌트의 스타일을 커스텀 해줄 필요가 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;오리지날 TextField 컴포넌트는 단순히 error에 대한 피드백은 빨간색으로만 나타냅니다.&lt;/li&gt;
&lt;li&gt;값이 비거나, 형식에 안맞거나, 형식에 맞는 등의 피드백을 보여주기 위해 다양한 색깔을 TextField 컴포넌트에 주기 위해서는 커스텀이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;TextField 커스텀 스타일 지정하기&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;Material UI의 TextField의 경우 다양한 방식으로 커스텀 스타일을 지정할 수 있습니다. 정말 많은 방식이 존재하기 때문에 어떤 것을 해야할지 난감했습니다.&lt;br&gt;TextField 컴포넌트도 완전히 input 요소 기반이 아닌 내부적으로 다른 요소들도 포함 되어 있어서 내부 요소로 접근하여 스타일을 변경해야 하는 등 복잡하였습니다. (해당 방법을 찾는데 많은 시간이 걸렸습니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;412&quot; data-origin-height=&quot;504&quot; data-filename=&quot;custom_textfield1.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kI2cK/btrdJ2DUE8i/pEMxqXk2fJTdFEuhqST2Dk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kI2cK/btrdJ2DUE8i/pEMxqXk2fJTdFEuhqST2Dk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kI2cK/btrdJ2DUE8i/pEMxqXk2fJTdFEuhqST2Dk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/kI2cK/btrdJ2DUE8i/pEMxqXk2fJTdFEuhqST2Dk/img.gif&quot; data-origin-width=&quot;412&quot; data-origin-height=&quot;504&quot; data-filename=&quot;custom_textfield1.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&gt;공식 사이트에 나와있는 방식 중에서, TextField의 InputProps, FormHelperTextProps로 접근하여 makeStyles hook (Material UI에서 제공하는 hook)을 활용하여 className에 값을 지정해주는 방식을 채택하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://material-ui.com/components/text-fields/#customized-inputs&quot;&gt;material-ui : customized-inputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;현재 구현하려고 하는 스타일은 동적으로 스타일을 변경해 주어야 하는 작업이며, 재사용성을 높이고자 해당 방식을 채택하였습니다.&lt;/li&gt;
&lt;li&gt;다른 방식의 경우에는, 기존 TextField 컴포넌트에 지정된 스타일을 덮어 새로운 컴포넌트를 만드는 방식인 HOC 형태 입니다. (물론, 더 생각해 보면 이방식으로도 만들수 있을 것 같긴 합니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// useStyles에 color를 props로 넘겨서 설정된 input, helperText 스타일 객체를 얻을 수 있습니다.
import { makeStyles } from &amp;quot;@material-ui/core&amp;quot;;
const useStyles = makeStyles({
  input: {
    &amp;quot;&amp;amp; input + fieldset&amp;quot;: {
      borderColor: (props) =&amp;gt; props.color,
    },
    &amp;quot;&amp;amp; input:valid:focus + fieldset&amp;quot;: {
      borderColor: (props) =&amp;gt; props.color,
    },
    &amp;quot;&amp;amp; input:valid:hover + fieldset&amp;quot;: {
      borderColor: (props) =&amp;gt; props.color,
    },
  },
  helperText: {
    color: (props) =&amp;gt; props.color,
  },
});

// 스타일 객체의 해당하는 프로퍼티가 가진 스타일 값 객체를 InputProps, FormHelperTextProps에 연결시켜 적용시킬 수 있습니다.
&amp;lt;TextField
  type=&amp;quot;text&amp;quot;
  variant=&amp;quot;outlined&amp;quot;
  InputProps={{
    className: useStyle({ color: &amp;quot;green&amp;quot; }).input,
  }}
  FormHelperTextProps={{
    className: useStyle({ color: &amp;quot;green&amp;quot; }).helperText,
  }}
/&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;유효성 체크 상태에 따른 스타일 생성 hooks (useChecks)&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이를 효율적으로 사용하기 위해서 validation에 따른 code를 통해 적절한 color, message가 연결 될 수 있도록 구현하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// useChecks js파일의 hooks 구현
import { useSelector } from &amp;quot;react-redux&amp;quot;;
import { makeStyles } from &amp;quot;@material-ui/core&amp;quot;;

// material UI Color TextField 스타일 객체 반환기
const useStyles = makeStyles({
  input: {
    &amp;quot;&amp;amp; input + fieldset&amp;quot;: {
      borderColor: (props) =&amp;gt; props.color,
    },
    &amp;quot;&amp;amp; input:valid:focus + fieldset&amp;quot;: {
      borderColor: (props) =&amp;gt; props.color,
    },
    &amp;quot;&amp;amp; input:valid:hover + fieldset&amp;quot;: {
      borderColor: (props) =&amp;gt; props.color,
    },
  },
  helperText: {
    color: (props) =&amp;gt; props.color,
  },
});

// 패스워드 validation 확인
export const checkPassword = (password) =&amp;gt; {
  let res = false;

  if (!password) {
    res = false;
  } else {
    const check = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/.test(password);
    check ? (res = true) : (res = false);
  }

  return res;
};

// Email validation 확인
export const checkEmail = (email) =&amp;gt; {
  let res = false;

  if (!email) {
    res = false;
  } else {
    const check =
      /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/.test(
        email
      );
    check ? (res = true) : (res = false);
  }

  return res;
};

// website 형식에 대한 피드백 message, color 반환기
export const useCheckWebsite = (website) =&amp;gt; {
  let color = &amp;quot;&amp;quot;;
  let message = &amp;quot;&amp;quot;;
  let code = &amp;quot;&amp;quot;;
  const res =
    /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&amp;amp;//=]*)/.test(
      website
    );
  if (!website) {
    color = &amp;quot;rgba(0, 0, 0, 0.3)&amp;quot;;
    message = &amp;quot;&amp;quot;;
    code = &amp;quot;empty&amp;quot;;
  } else {
    if (res) {
      color = &amp;quot;green&amp;quot;;
      message = &amp;quot;URL 형식에 맞습니다.&amp;quot;;
      code = &amp;quot;success&amp;quot;;
    } else {
      color = &amp;quot;red&amp;quot;;
      message =
        &amp;quot;URL 형식에 맞지 않습니다. (http:// 또는 https://를 포함시켜주세요.)&amp;quot;;
      code = &amp;quot;error&amp;quot;;
    }
  }

  return {
    input: useStyles({ color }).input,
    helperText: useStyles({ color }).helperText,
    helperTextMessage: message,
    code,
  };
};

// userDisplayName 중복 검사 결과에 대한 피드백 message, color 반환기
export const useCheckDisplayName = (prevDisplayName, displayName) =&amp;gt; {
  const exist = useSelector((state) =&amp;gt; state.users.checkDisplayName.exist);

  let code = &amp;quot;&amp;quot;;
  let color = &amp;quot;&amp;quot;;
  let message = &amp;quot;&amp;quot;;

  // 유효성 검사에 따른 code 할당부
  if (prevDisplayName === displayName) {
    code = &amp;quot;default&amp;quot;;
  } else {
    if (!displayName) {
      code = &amp;quot;empty&amp;quot;;
    } else if (!exist[1] || displayName !== exist[1]) {
      //이름이 확인된 적이 없는 경우 또는 이전에 확인된 이름과 input이 같지 않은 경우
      code = &amp;quot;warning&amp;quot;;
    } else {
      // 확인된 이름이 존재하는 경우
      if (exist[0]) {
        code = &amp;quot;error&amp;quot;;
      } else {
        // 확인된 이름이 존재하지 않는 경우
        code = &amp;quot;success&amp;quot;;
      }
    }
  }

  // 할당된 코드에 따른 color, message 할당부
  switch (code) {
    case &amp;quot;empty&amp;quot;:
      color = &amp;quot;orange&amp;quot;;
      message = &amp;quot;이름을 입력해 주세요&amp;quot;;
      break;
    case &amp;quot;warning&amp;quot;:
      color = &amp;quot;orange&amp;quot;;
      message = &amp;quot;중복 확인이 필요합니다.&amp;quot;;
      break;
    case &amp;quot;success&amp;quot;:
      color = &amp;quot;green&amp;quot;;
      message = `${exist[1]}는 사용가능 합니다.`;
      break;
    case &amp;quot;error&amp;quot;:
      color = &amp;quot;red&amp;quot;;
      message = `${exist[1]}는 이미 존재하는 이름입니다.`;
      break;
    default:
      color = &amp;quot;rgba(0, 0, 0, 0.3)&amp;quot;;
      message = ``;
  }
  return {
    input: useStyles({ color }).input,
    helperText: useStyles({ color }).helperText,
    helperTextMessage: message,
    code,
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;이름 중복 체크 피드백, 웹사이트 형식 체크 피드백&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;610&quot; data-filename=&quot;custom_textfield2.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SxByu/btrdJkEPCCz/TPpmxkFoWI3Z5gZCi6SeYk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SxByu/btrdJkEPCCz/TPpmxkFoWI3Z5gZCi6SeYk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SxByu/btrdJkEPCCz/TPpmxkFoWI3Z5gZCi6SeYk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/SxByu/btrdJkEPCCz/TPpmxkFoWI3Z5gZCi6SeYk/img.gif&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;610&quot; data-filename=&quot;custom_textfield2.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;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.24 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;유효성 체크에 따른 submit 버튼 disabled 처리&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;오늘은 validation 충족시 submit 버튼이 누를수 있는 상태로 변경될 수 있게 하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;기존에는 validation 확인도 안한 상태로 언제든지 사용자가 submit을 누를수 있었습니다. 그래서 validation 자체를 submit이 실행될 때 체크하여 알림창을 띄우는 형태 였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자 입장에서는 매번 submit을 눌러서 피드백을 받아야 하는 입장이므로 불편합니다.&lt;/li&gt;
&lt;li&gt;개발자 입장에서는 submit이 validation 관한 로직으로 가득차서 복잡해 집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;실제 인스타그램의 유효성 처리&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이러한 문제점을 생각하고, 실제 인스타그램에서는 어떻게 로그인 등의 Validation이 어떻게 처리되는지 확인했습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;인스타그램에서는 자체적으로 submit 버튼을 형식에 맞지 않으면 diabled로 누르지 못하게 사용자의 접근을 막아 사용자에게 validation 피드백을 주고있었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;실제 인스타그램의 submit 버튼 disabled&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;438&quot; data-filename=&quot;instagram_disabled.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clHucV/btrdJkLBLqH/WoWF6XUpHT9umj8Pc1qHHK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clHucV/btrdJkLBLqH/WoWF6XUpHT9umj8Pc1qHHK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clHucV/btrdJkLBLqH/WoWF6XUpHT9umj8Pc1qHHK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/clHucV/btrdJkLBLqH/WoWF6XUpHT9umj8Pc1qHHK/img.gif&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;438&quot; data-filename=&quot;instagram_disabled.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;br/&gt;

&lt;p&gt;예전에 댓글 submit 버튼을 구현할 때 이런식으로 구현했었는데 이번에도 그러한 형식으로 여러 submit 버튼을 validation을 넣어 diabled 구현을 하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;disabled 로직 작성하기&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;submit을 할 유효성 검사 조건이 모두 충족되는 경우 submit 버튼 disabled를 false로 처리해 주고 나머지는 true로 하여 사용자의 접근을 막습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;346&quot; data-origin-height=&quot;412&quot; data-filename=&quot;racstagram_disabled.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ndDXd/btrdDxMn3ay/wdb8C7kN6qkajnEk4gNLZ1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ndDXd/btrdDxMn3ay/wdb8C7kN6qkajnEk4gNLZ1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ndDXd/btrdDxMn3ay/wdb8C7kN6qkajnEk4gNLZ1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ndDXd/btrdDxMn3ay/wdb8C7kN6qkajnEk4gNLZ1/img.gif&quot; data-origin-width=&quot;346&quot; data-origin-height=&quot;412&quot; data-filename=&quot;racstagram_disabled.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&gt;이를 위해서는 disabled 값을 판단 해줘야 하는 함수가 필요합니다. 저는 checkDisabled 함수를 지정하여 해당 함수를 submit 버튼의 disabled 프로퍼티에 바로 연결시켜 반영된 값을 계속해서 받을 수 있도록 하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;그리고, CSS로 disabled 상태일 경우의 스타일을 추가해 줍니다. (저는 버튼 색상을 gray로 하고 pointer가 안보이게 스타일을 지정하였습니다.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const ExampleComponent = () =&amp;gt; {
  // checkDisplayName 객체, checkWebsiteRef 객체 생략

  // 상태 code 값으로 disable로 지정할 값(true or false) 반환해줌
  const checkDisable = () =&amp;gt; {
    if (
      !(
        checkDisplayName.code === &amp;quot;success&amp;quot; ||
        checkDisplayName.code === &amp;quot;default&amp;quot;
      )
    ) {
      // (dispalyName이 이전과 같거나, 중복 체크에서 성공한 경우)가 아닌 경우 -&amp;gt; disable true (버튼 비활성화)
      return true;
    } else if (checkDisplayName.code === &amp;quot;empty&amp;quot;) {
      // dispalyName이 빈 값인 경우 -&amp;gt; -&amp;gt; disable true (버튼 비활성화)
      return true;
    } else if (
      // (website가 빈 값이거나, 형식검사에서 성공한 경우)가 아닌 경우 -&amp;gt; disable true (버튼 비활성화)
      !(checkWebsiteRef.code === &amp;quot;empty&amp;quot; || checkWebsiteRef.code === &amp;quot;success&amp;quot;)
    ) {
      return true;
    } else {
      return false;
    }
  };

  return (
    &amp;lt;Button
      variant=&amp;quot;contained&amp;quot;
      className=&amp;quot;update_btn&amp;quot;
      color=&amp;quot;primary&amp;quot;
      type=&amp;quot;submit&amp;quot;
      disableElevation
      disabled={checkDisable()}
    &amp;gt;
      수정
    &amp;lt;/Button&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/215</guid>
      <comments>https://goforit.tistory.com/215#entry215comment</comments>
      <pubDate>Tue, 31 Aug 2021 21:40:42 +0900</pubDate>
    </item>
    <item>
      <title>20210820 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit35 : 좋아요 기능 구현, 좋아요 db 설계</title>
      <link>https://goforit.tistory.com/214</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit35&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVVSul/btrcOf5E3yo/ZeoIJh6nat5MA6g6xABToK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVVSul/btrcOf5E3yo/ZeoIJh6nat5MA6g6xABToK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVVSul/btrcOf5E3yo/ZeoIJh6nat5MA6g6xABToK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVVSul%2FbtrcOf5E3yo%2FZeoIJh6nat5MA6g6xABToK%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 안내&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;해당 프로젝트에 관한 자세한 화면 개요 및 스타일, 상태 관리, 코드에 관한 사항은 &lt;a href=&quot;https://github.com/RaccoonCode96/redux_racstagram&quot;&gt;Github : RaccoonCode96/redux_racstagram &lt;/a&gt;을 확인해 주세요.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.20 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;이전에 댓글 보기 버튼과 하트 모양의 좋아요 버튼을 구성하고 있는 PostControl이라는 컴포넌트를 만들었고, 기능적으로는 댓글 기능까지 추가가 완료 되었습니다.&lt;/p&gt;
&lt;p&gt;오늘은 좋아요 버튼을 기능적으로 추가하였습니다. 처음에는 좋아요 기능이 단순하게 구현할 수 있을 거라 생각했지만, 그렇게 간단한 작업은 아니였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;1. 좋아요 기능 구현&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;464&quot; data-filename=&quot;likes_res.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwhqOc/btrcSsJWc4k/UUeVzkNVgPoujQsHLm7Uyk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwhqOc/btrcSsJWc4k/UUeVzkNVgPoujQsHLm7Uyk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwhqOc/btrcSsJWc4k/UUeVzkNVgPoujQsHLm7Uyk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cwhqOc/btrcSsJWc4k/UUeVzkNVgPoujQsHLm7Uyk/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;464&quot; data-filename=&quot;likes_res.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;br/&gt;

&lt;p&gt;좋아요 기능의 경우 단순하게 만든다면 쉽겠지만, 인스타그램을 클론하는 입장에서는 조금 다릅니다.&lt;/p&gt;
&lt;p&gt;인스타그램의 경우 유저가 좋아요를 누르면 좋아요가 활성화(active) 되고, 다른 페이지를 갔다가 와도 계속 좋아요가 활성화(active) 상태여야 하기 때문입니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;사용자 입장에서 화면 유지 고려&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;사용자 입장에서는 사용자가 직접 변경을 가한 상태가 유지된 상태로 사용하길 원합니다. 그래서 이전에 다른 탭으로 이동해도 이전 스크롤 위치를 기억하여 유지하도록 구현하였고, 이번 좋아요 기능도 사용자가 좋아요 버튼을 누른것을 기억해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이전 스크롤 기억 구현시 최초에 글(db 진입점)이 생성되면, 새로고침 또는 로고를 클릭하지 않는 이상 진입점을 새로 갱신하도록 하지 않게 하여 사용자의 지속적인 사용을 도울수 있도록 설계 하였습니다. (너무 최신을 보여주는 것도 불편할 수 있다는 생각이 들었습니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 지속적 사용성을 위해서 이미 가져온 post 데이터는 해당 화면에서 나가서 작업하지 않는 이상 최신화 되지 않고, 다른 페이지에서의 수정 및 삭제 시에만 새로 최신화 시켰습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;댓글 작성, 글 작성 등의 경우 모두 다른 페이지에서 요청을 하고, 이후 진입점 갱신을 시행하여 post가 변경된것을 화면에 update 하였습니다.&lt;/li&gt;
&lt;li&gt;이렇게 하면 다른 사용자도 사용하면서 갑자기 댓글이 변하고, 글이 변하는 현상을 직접 눈으로 보는 것을 막을 수 있기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;판단&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;물론, 화면적으로만 변하게 하고 페이지를 나갔을 때 최종적으로 사용자의 좋아요가 변경된 상태를 db에 요청 처리하는 것도 생각해 볼수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만, 좋아요가 활성화된 상태에서 무한 스크롤로 다른 데이터를 추가하여 다시 렌더링 하게 되면 기존의 활성화된 좋아요는 해제가 되게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이러한 이유로 인스타그램의 좋아요 기능의 경우에는 사용자가 변경을 가하는 경우 db에 빠르게 반영되고 다시 화면에도 반영되어야 한다고 판단되었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;좋아요 DB 설계&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;좋아요 기능은 현재 유저에 따라 각 post에 좋아요를 했는지 안했는지를 구분하여 화면에 나타내야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;구분 방법 : like 데이터에 좋아요를 한 유저의 id를 가지게하여 로그인한 유저ID와 렌더링된 글의 좋아요 데이터에 있는 userId와 비교하여 상태를 나타냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 해당 postId의 like 데이터 예시
const like = {
  likeCount: 3,
  likeUsers: [&amp;quot;고유ID01&amp;quot;, &amp;quot;고유ID02&amp;quot;, &amp;quot;고유ID03&amp;quot;],
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;좋아요 DB 관리 방식 생각하기&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;좋아요 기능의 DB를 설계를 하면서 두 가지 경우를 생각했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;경우1 ) post 데이터 안에서 관리할 것인가?&lt;/li&gt;
&lt;li&gt;경우2 ) postId를 doc 이름으로 하여 따로 likes collection에서 관리 할 것인가?&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;1) Post 데이터 안에서 관리&lt;/h3&gt;
&lt;p&gt;post 데이터 안에서 관리하는 경우에는 post에 이미 사용자의 이전 상태가 기록되어 있기 때문에 이전 상태값을 post 마다 연결하기 매우 수월하여 좋습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// post에서 관리하는 db 구조
const post = {
  postText,
  postDate,
  userId,
  userPhotoUrl,
  userDisplayName,
  postImageUrl,
  commentArray: [],
  // 좋아요
  likes: {
    likeUsers: [], // 사용자 고유 id
    likeCount: 0,
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;하지만, 위에서 말한 것과 같이 db에 반영하면 반영한 결과를 바로 화면에 반영해야 합니다. 그렇다 보니 화면에 반영하려면 데이터를 새롭게 db에서 불러와야 합니다.&lt;/p&gt;
&lt;p&gt;post에서 관리하는 데이터를 단지 좋아요 상태값 하나 때문에 다시 불러오게 되면 &lt;strong&gt;큰 사이즈의 데이터를 다시 렌더링 시켜야 하고, 굳이 다시 렌더링 될 필요가 없는 부분도 다시 렌더링 되어야 함으로서 사용자의 사용성에 영향을 미치게 될것이라고 생각했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;2) 새로운 collection으로 따로 관리 (postId 값으로 연결)&lt;/h3&gt;
&lt;p&gt;따로 관리 하게 되면, post 데이터는 그대로 있는 상태에서 좋아요 데이터만 요청하고 화면에 반영하여 렌더링 할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;대신, 각 post 마다 해당하는 like 정보를 맞춰서 주어야 함으로 정보를 맞추어 주는 작업에 시간을 소비해야하는 단점이 발생하긴 합니다. 그럼에도 다른 데이터와 독립적으로 빠르게 화면에 반영할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// like를 따로 관리하는 DB 구조 (Firestore)
// likes collection - like doc(docName: postId)
const like = {
  likeCount: 0,
  likeUsers: [],
};

// 화면에 필요한 like 데이터를 가져올 때 (Redux State)
const likes = docs.map((doc) =&amp;gt; ({
  postId: doc.id,
  likeCount: doc.data().likeCount,
  isLike: doc.data().likeUsers.includes(uid),
}));

// [{postId, likeCount, isLike}, {postId, likeCount, isLike}, {postId, likeCount, isLike}, ...]&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;like 데이터 지정 또는 가져오기 요청 함수&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;on, off 요청 함수는 통합하여 하나로 재구성 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Likes 가져오기
export const getLikesThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/post/getLikesThunk&amp;quot;,
  async (_, thunkAPI) =&amp;gt; {
    try {
      const {
        profile: {
          currentUser: { uid },
        },
      } = thunkAPI.getState();
      const { docs } = await dbService.collection(&amp;quot;likes&amp;quot;).get();
      const likes = docs.map((doc) =&amp;gt; ({
        postId: doc.id,
        likeCount: doc.data().likeCount,
        isLike: doc.data().likeUsers.includes(uid),
      }));
      return likes;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);

// Like On 상태 지정하기
export const setLikeOnThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/post/setLikeOnThunk&amp;quot;,
  async (postId, thunkAPI) =&amp;gt; {
    try {
      const {
        profile: {
          currentUser: { uid },
        },
      } = thunkAPI.getState();
      const doc = dbService.collection(&amp;quot;likes&amp;quot;).doc(postId);
      const prevDoc = await doc.get();

      await doc.set(
        {
          likeCount: prevDoc.data().likeCount + 1,
          likeUsers: [...prevDoc.data().likeUsers, uid],
        },
        { merge: true }
      );
      thunkAPI.dispatch(getLikesThunk());
      return true;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);

// Like Off 상태 지정하기
export const setLikeOffThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/post/setLikeOffThunk&amp;quot;,
  async (postId, thunkAPI) =&amp;gt; {
    try {
      const {
        profile: {
          currentUser: { uid },
        },
      } = thunkAPI.getState();
      const doc = dbService.collection(&amp;quot;likes&amp;quot;).doc(postId);
      const prevDoc = await doc.get();

      await doc.set(
        {
          likeCount: prevDoc.data().likeCount - 1,
          likeUsers: prevDoc
            .data()
            .likeUsers.filter((userId) =&amp;gt; userId !== uid),
        },
        { merge: true }
      );
      thunkAPI.dispatch(getLikesThunk());
      return true;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;like 요청 함수 연결하기&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;likes 데이터를 가져온 후, post에 맞는 like 정보를 선택하기 위해서 findeLike 함수를 만들었고 이를 실행하여 like의 초기 상태 값으로 설정할 수 있게 하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;물론, 코드 자체는 복잡하지만 구현 해놓고 다시 리팩토링을 진행해야하는 부분입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// post 외부에서 getLikeThunk 요청 후 likes 데이터 가져와 사용
const likes = useSelector((state) =&amp;gt; state.like.likes);

// likes에서 post에 맞는 like 찾기
const findLike = useCallback(() =&amp;gt; {
  const like = likes.find((like) =&amp;gt; like.postId === post.postId);
  return like;
}, [likes, post]);

// like 상태 초기값 설정 하기
const initIsLike = useMemo(() =&amp;gt; findLike()?.isLike, [findLike]);
const [isLike, setLike] = useState(initIsLike);

// like 설정 요청 하기
const toggleDebounce = useMemo(
  () =&amp;gt;
    debounce((checked) =&amp;gt; {
      if (initIsLike !== checked) {
        if (checked) {
          dispatch(setLikeOnThunk(post.postId));
          console.log(&amp;quot;setLike : On&amp;quot;);
        } else {
          dispatch(setLikeOffThunk(post.postId));
          console.log(&amp;quot;setLike : Off&amp;quot;);
        }
      }
    }, 900),
  [initIsLike, dispatch, post]
);

const onChange = useCallback(
  (event) =&amp;gt; {
    const {
      target: { checked },
    } = event;
    setLike(checked);
    toggleDebounce(checked);
  },
  [toggleDebounce]
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;완성 화면과 firestore&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;하트를 누르면 db에 반영됩니다. (혹시 모를 사용자의 많은 수의 클릭 제어를 위해 debounce를 적용했습니다. db 반영은 900ms 뒤에 요청 합니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;464&quot; data-filename=&quot;likes_res.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rLhZh/btrcNLX0JQX/3lEVF24cAkKYTbGIelzQV0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rLhZh/btrcNLX0JQX/3lEVF24cAkKYTbGIelzQV0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rLhZh/btrcNLX0JQX/3lEVF24cAkKYTbGIelzQV0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/rLhZh/btrcNLX0JQX/3lEVF24cAkKYTbGIelzQV0/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;464&quot; data-filename=&quot;likes_res.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;br/&gt;

&lt;ul&gt;
&lt;li&gt;db 반영 모습&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;378&quot; data-filename=&quot;likes_firestore.gif&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wSZCa/btrcLBhQ0Ey/Ocd76wGrTt5xkmjuEOkrz1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wSZCa/btrcLBhQ0Ey/Ocd76wGrTt5xkmjuEOkrz1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wSZCa/btrcLBhQ0Ey/Ocd76wGrTt5xkmjuEOkrz1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/wSZCa/btrcLBhQ0Ey/Ocd76wGrTt5xkmjuEOkrz1/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;378&quot; data-filename=&quot;likes_firestore.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;/figure&gt;
&lt;/p&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/214</guid>
      <comments>https://goforit.tistory.com/214#entry214comment</comments>
      <pubDate>Sat, 21 Aug 2021 18:54:15 +0900</pubDate>
    </item>
    <item>
      <title>20210819 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit34 : 무한 스크롤 개선, Intersection Observer API 활용, 무한 스크롤 더 이상 데이터가 없는 경우 처리(버그 해결)</title>
      <link>https://goforit.tistory.com/213</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit34&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZLTOa/btrcAcbkzkI/AsZMLEupeWXgkhtKu18e50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZLTOa/btrcAcbkzkI/AsZMLEupeWXgkhtKu18e50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZLTOa/btrcAcbkzkI/AsZMLEupeWXgkhtKu18e50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZLTOa%2FbtrcAcbkzkI%2FAsZMLEupeWXgkhtKu18e50%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 안내&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;해당 프로젝트에 관한 자세한 화면 개요 및 스타일, 상태 관리, 코드에 관한 사항은 &lt;a href=&quot;https://github.com/RaccoonCode96/redux_racstagram&quot;&gt;Github : RaccoonCode96/redux_racstagram &lt;/a&gt;을 확인해 주세요.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.19 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;전에 발생했던 무한 스크롤 데이터 끝 처리 버그를 해결하기 위한 접근과 제가 해결한 과정을 나누어 보고자 합니다. Intersection Observer에 대해서 잘 모르시면, 잘 정리된 &lt;a href=&quot;https://heropy.blog/2019/10/27/intersection-observer/&quot;&gt;intersection-observer by HEROPY TECH&lt;/a&gt; 글을 봐주시면 좋겠습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;1. 무한 스크롤 개선&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;586&quot; data-filename=&quot;improve_infinite.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boP8m5/btrcAb4xE1t/pw1osHNdOgGYQabZNkQLkK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boP8m5/btrcAb4xE1t/pw1osHNdOgGYQabZNkQLkK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boP8m5/btrcAb4xE1t/pw1osHNdOgGYQabZNkQLkK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/boP8m5/btrcAb4xE1t/pw1osHNdOgGYQabZNkQLkK/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;586&quot; data-filename=&quot;improve_infinite.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;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;기존 무한 스크롤의 무한 스크롤 데이터 끝 처리 버그의 발생 이유&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;무한 스크롤 데이터 끝 처리 버그를 설명하자면, 데이터 요청 후 더이상 불러올 데이터가 없음에도 불구하고 데이터를 요청을 하여 마지막에 불러온 데이터를 또 불러와 기존에 있던 Array에 넣어 붙임으로서 글이 복사되어 보이는 버그 입니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이는 기존에 Intersection Observer을 활용한 무한 스크롤을 깊은 test 없이 구현하여 발생한 문제입니다. Intersecting이 되어 데이터를 요청 후 데이터가 없는 경우 이에 대한 피드백으로 Observer를 막거나, 어떤 조치를 취하지 않을 뿐더러 근본적으로 lifecycle을 제대로 설정해 두지 않아서 발생한 문제 였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;변경 전 UseInfinteScroll&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;예전에 구현한 UseInfinteScroll에 대해서 간단히 설명하자면, UseInfiniteScroll을 컴포넌트화 시켜 Observer가 감지하는 target인 마지막 element를 자체적으로 가지고 있게 하였습니다.&lt;/p&gt;
&lt;p&gt;외부적으로 UseInfinteScroll을 표시하는 상위 컴포넌트에서 여러 조건을 걸어서 UseInfiniteScroll을 강제로 unmount 시켜 disconnect 시킬 수 있습니다. (부모 상황에 따라서 observe를 제어할 수 있습니다.)&lt;/p&gt;
&lt;p&gt;또한, 상위 컴포넌트에서 observer가 실행시킬 함수를 UseInfiniteScroll 컴포넌트의 excute prop으로 전달하여 유동적으로 실행할 함수를 설정 할수 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;p&gt;하지만, 현재 상황에서는 execute에 들어올 데이터를 더 요청하는 함수(getMorePosts)는 내부적으로 변경될 여지가 있습니다. &lt;strong&gt;예전에 구현한 UseInfinteScroll의 observer는 변경되는 함수를 반영하지 못합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기본적으로 IntersectionObserver의 객체는 생성할 때 부터 실행할 함수를 받아 생성되어 집니다. 그러므로 실행할 함수를 변경하는 것은 불가능 하여 실행 함수를 변경하려면, 새로운 IntersectionObserver 객체를 생성하여 다시 지정해야 합니다.&lt;/p&gt;
&lt;p&gt;만약, 함수가 변경될 필요가 없는 상황이라면 아래와 같이 observer를 컴포넌트 단에서 지정하고 useEffect를 통해서 observer의 상태를 변경해주면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;저의 경우에는 유동적으로 계속해서 execute에 들어오는 함수가 내부적으로 변경되어 반영되어야할 필요가 있어 아래와 같은 상황은 적절하지 않았습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 현재 상황에서 잘못된 구현 방식
import { debounce } from &amp;quot;lodash&amp;quot;;
import React, { useRef, useEffect, useMemo } from &amp;quot;react&amp;quot;;

const UseInfiniteScroll = ({ execute }) =&amp;gt; {
  const debounceExecute = useMemo(() =&amp;gt; debounce(execute, 300), [execute]);
  const lastElRef = useRef(null);
  const observer = new IntersectionObserver(
    ([{ isIntersecting }]) =&amp;gt; {
      if (isIntersecting) {
        console.log(&amp;quot;function run&amp;quot;);
        debounceExecute();
      }
    },
    { threshold: 0.5 }
  );

  useEffect(() =&amp;gt; {
    observer.observe(lastElRef.current);
    console.log(&amp;quot;observe : is watching&amp;quot;);
  }, []);

  useEffect(() =&amp;gt; {
    return () =&amp;gt; {
      observer.disconnect();
      console.log(&amp;quot;observe : disconnected&amp;quot;);
    };
  }, []);

  return &amp;lt;div ref={lastElRef} style={{ height: &amp;quot;20px&amp;quot; }}&amp;gt;&amp;lt;/div&amp;gt;;
};

export default UseInfiniteScroll;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;변경 후 UseInfinteScroll&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;예전과 다르게, observer가 계속해서 새로 만들어져야 한다는 것을 깨닫고 컴포넌트 내부에서는 useRef를 활용해서 미리 observer 식별자를 지정해 두고 prop으로 들어오는 excute의 내부적 변화를 감지하여 observer.current에 변경된 excute 함수를 가진 새로운 IntersectionObaserver 객체를 할당 하여 반영하도록 하였다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;여기저 주의할 점은 observe가 된상태에서 같은 식별자에 새로운 observer 객체를 할당한다고 해도 observe된 객체는 가비지 컬렉터에 의해서 사라지지 않는듯 했습니다.&lt;/strong&gt; 그래서 새로운 객체를 할당하기 전에 excute 함수에 변화가 감지되면 기존에 할당시킨 식별자에 observer를 disconnect해주어야 합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { debounce } from &amp;quot;lodash&amp;quot;;
import React, { useRef, useEffect, useMemo } from &amp;quot;react&amp;quot;;

const UseInfiniteScroll = ({ execute }) =&amp;gt; {
  const debounceExecute = useMemo(() =&amp;gt; debounce(execute, 300), [execute]);
  const lastElRef = useRef(null);
  const observer = useRef(null);

  useEffect(() =&amp;gt; {
    // 식별자에 있는 기존 observe를 제거하여 초기화합니다.
    if (observer.current) {
      observer.current.disconnect();
      console.log(&amp;quot;observe: init&amp;quot;);
    }

    // debounceExecute 함수가 변경될 때마다 변경된 debouceExecute를 반영한 IntersectionObserver 객체를 재할당 하고, observe를 실행합니다.
    observer.current = new IntersectionObserver(
      ([{ isIntersecting }]) =&amp;gt; {
        if (isIntersecting) {
          debounceExecute();
          console.log(&amp;quot;function run&amp;quot;);
        }
      },
      { threshold: 0.5 }
    );

    observer.current.observe(lastElRef.current);
    console.log(&amp;quot;observe : is watching&amp;quot;);
  }, [debounceExecute]);

  // 외부 컴포넌트에 의해 unmount 되면, observe를 disconnect 하여 제거합니다.
  useEffect(() =&amp;gt; {
    return () =&amp;gt; {
      observer.current.disconnect();
      console.log(&amp;quot;observe : disconnected&amp;quot;);
    };
  }, []);

  return &amp;lt;div ref={lastElRef} style={{ height: &amp;quot;20px&amp;quot; }}&amp;gt;&amp;lt;/div&amp;gt;;
};

export default UseInfiniteScroll;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;데이터 끝 처리&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;데이터를 요청하고 데이터가 없는 경우 데이터가 없음을 표시하는 redux state를 만들고 이를 부모 컴포넌트에서 UseInfinteScroll의 unmount 조건으로 걸어 두어 데이터가 끝나면 unmount 시켜 더 이상 observer가 실행되지 않게 합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;return (
  &amp;lt;&amp;gt;
    {!isNone &amp;amp;&amp;amp; &amp;lt;UseInfiniteScroll execute={getMorePosts} /&amp;gt;}
    {isNone &amp;amp;&amp;amp; &amp;quot;더 이상 글이 없습니다.&amp;quot;}
  &amp;lt;/&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/213</guid>
      <comments>https://goforit.tistory.com/213#entry213comment</comments>
      <pubDate>Thu, 19 Aug 2021 19:37:58 +0900</pubDate>
    </item>
    <item>
      <title>20210818 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit33 : 자동 중복 체크 구현(debounce), React 에서 debounce 사용시 주의점, 무한스크롤 데이터 끝 처리 버그 발생</title>
      <link>https://goforit.tistory.com/212</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit33&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b99pWG/btrcB4wNM1o/RuBS5Y1v3FH62kKhuURv71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b99pWG/btrcB4wNM1o/RuBS5Y1v3FH62kKhuURv71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b99pWG/btrcB4wNM1o/RuBS5Y1v3FH62kKhuURv71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb99pWG%2FbtrcB4wNM1o%2FRuBS5Y1v3FH62kKhuURv71%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 구현 하고 싶습니다.)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;무한 스크롤&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 스크롤 위치 기억 (뒤로가기가 아닌 페이지 변해도 기억 합니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;랜덤 유저 추천&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;댓글 기능&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글의 text 더보기 버튼&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.18 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;좋은 개발자의 시작 : 블로그 글 작성 개선&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;회고록을 작성하다 보니 이전의 글의 어체가 모두 명사형 어미로 끝나게 글을 작성하였습니다. 이제 부터는 &amp;#39;하십시오체&amp;#39;를 활용하여 글을 작성함으로써 좀 더 읽기 편한 글을 작성하고자 합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;또한, 전에 작성 했던 글의 무분별한 list 표시를 지우고 필요한 부분만 list 표시를 활용하여 가독성을 높이고자 합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;모두가 알아보기 쉽게 코드를 작성하는 개발자가 좋은 개발자라고 항상 생각 했지만, 코드가 아닌 가장 친숙한 글 작성은 소홀 했던 것 같습니다. 더 좋은 코드, 더 좋은 글을 작성하기 위해 노력하고자 합니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;1. 자동 중복체크 기능 구현 (lodash debounce 활용)&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;기존에 있던 버튼식 displayName 중복 체크 부분을 자동 중복 체크로 변경 하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;626&quot; data-filename=&quot;auto_check.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7JpaT/btrcuJgjPbB/S8cefypYCrksFwZm5x7iuk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7JpaT/btrcuJgjPbB/S8cefypYCrksFwZm5x7iuk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7JpaT/btrcuJgjPbB/S8cefypYCrksFwZm5x7iuk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/7JpaT/btrcuJgjPbB/S8cefypYCrksFwZm5x7iuk/img.gif&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;626&quot; data-filename=&quot;auto_check.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&gt;자동으로 input의 내용을 가지고 중복을 체크 요청을 하기 위해서는 input의 onChange event를 받아서 처리 해야 하는데, onChange는 input의 value가 변할 때 마다 요청을 하기 때문에 너무 많은 event를 발생시킵니다. 불필요한 event를 제어하여 적절한 요청을 다루는 것이 필요 하였습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;스로틀과 디바운스&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;불필요한 event를 제어하는데는 스로틀(Throttle)과 디바운스(Debounce)를 주로 사용하게 됩니다. 제가 이해한 스로틀과 디바운스를 간단하게 말하자면, 스로틀은 많은 수의 호출을 시간 단위로 그룹화 하고 디바운스는 다수의 호출의 연속성을 체크하여 연속된 호출을 하나로 그룹화 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디바운스 : 연속된 호출 중 마지막 호출만 처리 (연속성)&lt;ul&gt;
&lt;li&gt;시간 설정을 통해 지연 호출 시간을 설정 할수 있습니다. 해당 설정 시간 동안에 호출이 없음을 감지하여 마지막 호출을 구분하는 것 같습니다. 시간 설정이 연속성을 판단하는 단위라고 이해하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스로틀 : 다수의 호출 중 일정 시간안에 들어온 호출의 마지막 호출만 처리 (시간성)&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://webclub.tistory.com/607&quot;&gt;디바운스와 스로틀 그리고 차이점 by WEBCLUB KimJaeHee&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa&quot;&gt;쓰로틀링과 디바운싱 by zerocho&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;디바운스와 스로틀을 직접 구현할 수도 있겠지만, 좋은 성능의 디바운스와 스로틀을 제공하는 대표적인 JS 라이브러리인 Lodash를 활용할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lodash.com/docs/4.17.15#debounce&quot;&gt;Lodash : _.debounce&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lodash.com/docs/4.17.15#throttle&quot;&gt;Lodash : _.throttle&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;자동 중복체크에서는 사용자의 입력이 끝남을 판단하여 요청해야하므로 디바운스를 활용하였습니다. 다른 글에서는 input 입력 중 일부 검색 결과를 보여주는 기능을 구현할 때는 스로틀을 활용한다고 합니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;React 함수형 컴포넌트에서 debounce 활용시 주의점&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;저는 onChange Handler 함수를 이미 만들어 놨었고, 기존에 check 요청을 보내는 check 함수를 debounce를 붙여서 onChange Handler 함수에 적용하였습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;리액트에서 debounce를 사용시 주의해야 할 점이 있습니다. &lt;strong&gt;리액트의 함수형 컴포넌트 방식을 사용하는 경우 특별한 처리 없이 선언된 debounce 함수는 재렌더링 시 계속 다시 선언되어 제대로 작동하지 않게 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그러므로 함수형 컴포넌트를 사용한다면, useCallback를 활용하여 debounce 함수가 계속 유지 될수 있게 해야합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;또한, debounce 함수 내부의 일반적인 callback이 아닌 유동적으로 인자를 받는 callback을 구현하는 경우에는 callback을 한번 씌워주어야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const Test = () =&amp;gt; {
  const dispatch = useDispatch();
  const [input, setInput] = useState(&amp;quot;&amp;quot;);

  // 중복 체크 요청 함수
  const check = useCallback(
    (displayName) =&amp;gt; {
      dispatch(checkDisplayNameThunk(displayName));
    },
    [dispatch]
  );

  // 디바운스화 시킨 중복 체크 요청 함수
  // useCallback 사용 주의 (lint에 의해 warning 발생 가능성 있음)
  const debounceCheck = useCallback(
    debounce((displayName) =&amp;gt; {
      check(displayName);
    }, 900),
    []
  );

  // onCangeHandler 함수
  const onChange = useCallback(
    (event) =&amp;gt; {
      const { value } = event.target;
      setInput(value);
      debounceCheck(value);
      // debounceCheck(input); &amp;lt;- 변경한 input이 반영 안되므로 사용 불가
    },
    [inputs, debounceCheck]
  );

  return &amp;lt;input type=&amp;quot;text&amp;quot; value={input} onChange={onChange} /&amp;gt;;
};

export default Test;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;unknown function waring 해결하기&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;대신 이렇게 useCallback 안에서 외부 라이브러리의 함수를 직접적으로 callback으로 사용하는 경우 lint에서 unknown function이니까 inline function을 사용하라고 complie warning을 일으키는데 사용에 대한 문제는 없지만 이러한 경고가 거슬리는 경우 useMemo를 사용하면 해결 가능하다.&lt;/p&gt;
&lt;p&gt;React 공식 문서에서는 &lt;code&gt;useCallback(fn, deps) is equivalent to useMemo(() =&amp;gt; fn, deps)&lt;/code&gt; 라고 되어 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kyleshevlin.com/debounce-and-throttle-callbacks-with-react-hooks&quot;&gt;Debounce and Throttle Callbacks with React Hooks by Kyle Shevlin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://reactjs.org/docs/hooks-reference.html#usecallback&quot;&gt;Reac 공식 Docs useCallback&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// useMemo 방식 (함수를 return 하게 만들면 warning 발생 없음)
const debounceCheck = useMemo(
  () =&amp;gt;
    debounce((displayName) =&amp;gt; {
      check(displayName);
    }, 900),
  [check]
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;2. Intersection Observer를 이용한 무한 스크롤 끝 처리 버그&lt;/h2&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h3&gt;문제 의식&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p&gt;이미 전에 Intersection Observer를 이용한 무한스크롤을 구현하였습니다.&lt;/p&gt;
&lt;p&gt;그때는 Intersection Observer에 대해 깊게 테스트 하지 않고 블로그의 있는 글을 그냥 따라서 치며 제 상황에 맞게 구현하였습니다. 하지만, 블로그의 글을 너무 믿었던 탓인지 &lt;strong&gt;무한 스크롤이 더 이상 불러올 데이터가 없는 경우에 이전에 불러왔던 글을 다시 불러오는 버그가 발생했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;또한, 전에 구현해 놓은 무한 스크롤의 경우 너무 복잡하고 재사용하기도 불편하여, hook 또는 component화를 시켜 재사용하려고 합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;글을 작성하는 시점에서는 그래도 이렇게 왜 이런 현상이 발생하는지 몇가지 테스트를 통해서 짐작이 가기 시작하지만, 처음에는 어디서 발생하는 문제인지도 몰라 접근 조차도 힘들었습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;문제 해결을 위해서 무분별하게 써놓은 코드를 Component식으로 옮겨 정리하고 Intersection Observer API에 대해 더 자료를 찾아 보았습니다. Intersection Observer에 대한 좋은 자료를 찾았고 많은 도움이 되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://heropy.blog/2019/10/27/intersection-observer/&quot;&gt;Intersection Observer - 요소의 가시성 관찰 by HEROPY Tech&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이를 바탕으로 Component에서 test하여 어떤 부분이 에러를 발생시키는지 알아냈습니다. 그러면, 무한 스크롤의 불러올 데이터가 없는 경우 어떻게 처리하는지와 해당 버그에 대한 접근은 다음 시간에 작성하겠습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;다음에 필요한 것들&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; resize throttle 적용하기 또는 resizeObserver API 사용해보기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 무한 스크롤 데이터 끝처리 버그 해결 하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 새 게시글 보기 버튼 또는 로고 클릭시 데이터 진입점 갱신 기능 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profile의 웹사이트 정규표현식 match 정교화&lt;ul&gt;
&lt;li&gt;사용자는 http를 안넣을 수도 있음, 그리고 그외에도 예외 사항을 더 생각해 보자&lt;/li&gt;
&lt;li&gt;아니면, 사용자가 올바른 형식을 넣을 수 있도록 알림 만들기, 결국엔 validation 임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스켈레톤 UI 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; propType으로 type 지정 또는 typeScript 도입&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; sementic tag 적절한 태그로 수정하기 (검토)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; route &amp;#39;/profile&amp;#39; pathName을 &amp;#39;/user/:userName&amp;#39; pathName 사용하게 통합하여 pathname에 대한 조건을 줄여 보자&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 좋아요 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/212</guid>
      <comments>https://goforit.tistory.com/212#entry212comment</comments>
      <pubDate>Thu, 19 Aug 2021 16:54:03 +0900</pubDate>
    </item>
    <item>
      <title>20210817 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit32 : 댓글 작성 요청 개선, 댓글 지우기 요청 기능 구현, 축약된 PostText 더 보기 구현</title>
      <link>https://goforit.tistory.com/210</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit32&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b302Hu/btrcojvmFuy/fBGsEos4lzB64CPrLfPKjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b302Hu/btrcojvmFuy/fBGsEos4lzB64CPrLfPKjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b302Hu/btrcojvmFuy/fBGsEos4lzB64CPrLfPKjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb302Hu%2FbtrcojvmFuy%2FfBGsEos4lzB64CPrLfPKjK%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 가능하게 하고 싶음)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;무한 스크롤&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 스크롤 위치 기억 (뒤로가기가 아닌 페이지 변해도 기억 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;랜덤 유저 추천&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;댓글 기능&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글의 text 더보기 버튼&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.17 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;1. 댓글 작성 요청 개선 (post의 commetArray 반영 개선)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;기존의 댓글 작성의 경우에는 내부적으로 이전 데이터를 가져와서 큐 구조로 넣고 빼는 구조로 설계 했지만, 생각해보니 시간 순서대로 이미 db 색인 정렬이 되어 있기 때문에 comments db에서 해당 post에 대한 최근 comment 2개를 가져와서 다시 붙여 update 하는 식으로 변경하였다.&lt;/li&gt;
&lt;li&gt;즉, post에 있는 commetArray에 반영하는 작업을 comments에 있는 최근 2개 글을 가져와 계속 update 하게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 변경 전 Post의 commentArray에 반영하는 작업 코드
// 큐 방식의 구조

const commentId = doc.id;
const postDoc = dbService.collection(&amp;quot;posts&amp;quot;).doc(postId);
const postDocSnap = await postDoc.get();
const { commentArray } = { ...postDocSnap.data() };
const commentEl = {
  commentId,
  commentDisplayName: commentObj.userDisplayName,
  commentDate: commentObj.commentDate,
  comment,
  count: commentObj.count,
};
if (commentArray.length === 2) {
  commentArray.unshift(commentEl);
  commentArray.pop();
} else {
  commentArray.unshift(commentEl);
}
await postDoc.set(
  {
    commentArray,
  },
  { merge: true }
);&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const setCommentThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/comment/setCommentThunk&amp;quot;,
  async ({ comment, postId }, thunkAPI) =&amp;gt; {
    try {
      const {
        users: { currentUserInfo },
        comment: { comments },
        profile: {
          currentUser: { uid },
        },
      } = thunkAPI.getState();

      const commentObj = {
        userId: uid,
        postId,
        userDisplayName: currentUserInfo.displayName,
        userPhotoUrl: currentUserInfo.userPhotoUrl,
        commentDate: Date.now(),
        comment,
        count: comments[0] ? comments[0].count + 1 : 1,
      };
      // 먼저, comments에 작성한 댓글 db에 기록
      await dbService.collection(&amp;quot;comments&amp;quot;).doc().set(commentObj);

      // 최근 코멘트 2개를 db에서 가져와서 해당 post의 commentArray에 update
      const { docs } = await dbService
        .collection(&amp;quot;comments&amp;quot;)
        .where(&amp;quot;postId&amp;quot;, &amp;quot;==&amp;quot;, postId)
        .orderBy(&amp;quot;commentDate&amp;quot;, &amp;quot;desc&amp;quot;)
        .limit(2)
        .get();
      const commentArray = docs.map((doc) =&amp;gt; ({
        comment: doc.data().comment,
        commentDate: doc.data().commentDate,
        commentDisplayName: doc.data().userDisplayName,
        commentId: doc.id,
        count: doc.data().count,
      }));
      await dbService
        .collection(&amp;quot;posts&amp;quot;)
        .doc(postId)
        .set({ commentArray }, { merge: true });

      thunkAPI.dispatch(getCommentsThunk(postId));
      thunkAPI.dispatch(getAllPostsThunk());
      return true;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;2. 댓글 지우기 요청 기능 구현&lt;/h2&gt;
&lt;br/&gt;


&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;524&quot; data-filename=&quot;delete_comment.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3wRw5/btrcnIVI7FA/G1n0PaAh9ttKlGxr7xxGkk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3wRw5/btrcnIVI7FA/G1n0PaAh9ttKlGxr7xxGkk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3wRw5/btrcnIVI7FA/G1n0PaAh9ttKlGxr7xxGkk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b3wRw5/btrcnIVI7FA/G1n0PaAh9ttKlGxr7xxGkk/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;524&quot; data-filename=&quot;delete_comment.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;br/&gt;

&lt;ul&gt;
&lt;li&gt;사용자의 댓글은 수정이 불가능하게 하고, 단지 지울수만 있게 하였음&lt;/li&gt;
&lt;li&gt;comment를 다루는 것은 이전 글에서 이야기 한것과 같이 post에 들어있는 정보도 update 해주는 것을 주의해야한다.&lt;/li&gt;
&lt;li&gt;comment 제거 이후 위에서 commentArray에 반영하는 코드를 재사용하여 반영시킴&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const deleteCommentThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/comment/deleteCommentThunk&amp;quot;,
  async ({ commentId, postId }, thunkAPI) =&amp;gt; {
    try {
      // comments의 comment delete
      await dbService.collection(&amp;quot;comments&amp;quot;).doc(commentId).delete();

      // delete 이후 최신 comment 가져와서 다시 post의 commentArray 반영
      const { docs } = await dbService
        .collection(&amp;quot;comments&amp;quot;)
        .where(&amp;quot;postId&amp;quot;, &amp;quot;==&amp;quot;, postId)
        .orderBy(&amp;quot;commentDate&amp;quot;, &amp;quot;desc&amp;quot;)
        .limit(2)
        .get();
      const commentArray = docs.map((doc) =&amp;gt; ({
        comment: doc.data().comment,
        commentDate: doc.data().commentDate,
        commentDisplayName: doc.data().userDisplayName,
        commentId: doc.id,
        count: doc.data().count,
      }));
      await dbService
        .collection(&amp;quot;posts&amp;quot;)
        .doc(postId)
        .set({ commentArray }, { merge: true });

      thunkAPI.dispatch(getCommentsThunk(postId));
      thunkAPI.dispatch(getAllPostsThunk());
      return true;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;생략 버튼 연결과 Comfirm 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const Comment = ({ commentObj, postId }) =&amp;gt; {
  // ... 생략
  const deleteComment = useCallback(() =&amp;gt; {
    dispatch(deleteCommentThunk({ commentId, postId }));
  }, [dispatch, commentId, postId]);

  return (
    &amp;lt;li className=&amp;quot;comment_container&amp;quot; key={commentId}&amp;gt;
      {currentUserId === userId &amp;amp;&amp;amp; (
        &amp;lt;button onClick={confirmToggle} className=&amp;quot;delete_comment_btn&amp;quot;&amp;gt;
          &amp;lt;DeleteForeverIcon className=&amp;quot;icon&amp;quot; /&amp;gt;
        &amp;lt;/button&amp;gt;
      )}
      &amp;lt;Confirm
        isOn={confirmIsOn}
        toggle={confirmToggle}
        message={&amp;quot;정말 삭제하시겠습니까?&amp;quot;}
      &amp;gt;
        &amp;lt;button className=&amp;quot;confirm_item&amp;quot; onClick={deleteComment}&amp;gt;
          예
        &amp;lt;/button&amp;gt;
        &amp;lt;button className=&amp;quot;confirm_item&amp;quot; onClick={confirmToggle}&amp;gt;
          아니오
        &amp;lt;/button&amp;gt;
      &amp;lt;/Confirm&amp;gt;
    &amp;lt;/li&amp;gt;
  );
};

export default Comment;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;3. 댓글 페이지에 해당 Post의 contents 표시&lt;/h2&gt;
&lt;br/&gt;


&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;576&quot; data-filename=&quot;post_text.png&quot; width=&quot;616&quot; height=&quot;319&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1qpTG/btrcs8mkPF0/F6kgzS6CvvG8RCwcz6bwo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1qpTG/btrcs8mkPF0/F6kgzS6CvvG8RCwcz6bwo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1qpTG/btrcs8mkPF0/F6kgzS6CvvG8RCwcz6bwo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1qpTG%2Fbtrcs8mkPF0%2FF6kgzS6CvvG8RCwcz6bwo1%2Fimg.png&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;576&quot; data-filename=&quot;post_text.png&quot; width=&quot;616&quot; height=&quot;319&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;br/&gt;

&lt;ul&gt;
&lt;li&gt;인스타그램의 경우 사용자가 작성한 글의 text를 댓글 페이지의 댓글들 최상단에 표시하고 있다.&lt;/li&gt;
&lt;li&gt;Comment 컴포넌트들 위에 .post_text_container 요소를 통해서 해당 Post의 내용(글 작성자 이름, 작성자 이미지, 작성한 글 내용)을 표시함&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;4. Post Text 더 보기 구현 (글 축약 후 버튼 클릭으로 모두 보기, 개행 다루기)&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;535&quot; data-filename=&quot;more_post_text.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rAr79/btrcnIuDQcH/HJ8ku6FCAdqrO4yMzZ4zMK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rAr79/btrcnIuDQcH/HJ8ku6FCAdqrO4yMzZ4zMK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rAr79/btrcnIuDQcH/HJ8ku6FCAdqrO4yMzZ4zMK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/rAr79/btrcnIuDQcH/HJ8ku6FCAdqrO4yMzZ4zMK/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;535&quot; data-filename=&quot;more_post_text.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;br/&gt;

&lt;ul&gt;
&lt;li&gt;shortText : post에서 postText를 가져와서 일단 35글자로 제한하고 35글자 안에서 개행이 있는 부분을 나누어 앞부분만 text로 사용함&lt;/li&gt;
&lt;li&gt;moreBtnCheck 함수 : 개행이 없고 35글자 보다 짧은 text의 경우 더보기 버튼이 화면에 표시될 필요가 없기에 이를 check해서 boolean 값을 반환 함&lt;ul&gt;
&lt;li&gt;return : true는 버튼 표시가 필요한 경우, false는 표시가 필요 없는 경우&lt;/li&gt;
&lt;li&gt;개행은 OS 별로 달라서 모두 체크해 주어야 함&lt;ul&gt;
&lt;li&gt;윈도우 : \r\n (CRLF)&lt;/li&gt;
&lt;li&gt;유닉스 : \n (LF)&lt;/li&gt;
&lt;li&gt;맥 : \r (CR)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const shortText = post.postText.slice(0, 35).split(/(\r\n|\n|\r)/gm)[0];
const moreBtnCheck = () =&amp;gt; {
  // 35자 넘거나, 개행이 있는 경우 더보기 버튼 표시
  return post.postText &amp;gt; 35 || /(\r\n|\n|\r)/gm.test(post.postText);
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const component = () =&amp;gt; {
  // 생략
  return (
    &amp;lt;span className=&amp;quot;post_text&amp;quot;&amp;gt;
            {isMore ? post.postText : shortText}
          &amp;lt;/span&amp;gt;
          {moreBtnCheck() &amp;amp;&amp;amp; !isMore &amp;amp;&amp;amp; (
            &amp;lt;button
              className=&amp;quot;more_text_btn&amp;quot;
              onClick={(e) =&amp;gt; {
                setMore(true);
              }}
            &amp;gt;
              ...더 보기
            &amp;lt;/button&amp;gt;
          )}
  )

}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;문제 발생&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;user 또는 currentUser의 프로필 화면 이동시 가끔 최초 사용자의 글 불러올 때 그 글이 두 개씩 되는 현상이 발생하였다.&lt;/li&gt;
&lt;li&gt;짐작으로는 profile path와 user path가 혼란을 일으켜서 그런것 같기도 하다. 통일이 필요한것 같다.&lt;/li&gt;
&lt;li&gt;아니면, 무한스크롤의 더 불러오기 요청에서 문제를 일으키는 것 같기도 하다.&lt;ul&gt;
&lt;li&gt;최대한 작업 이후에 고쳐야 할 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;다음에 필요한 것들&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 좋아요 기능 -&amp;gt; 진행중&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 기본적인 comments page, comment, commentForm 스타일링&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; comment read, create 요청 함수 컴포넌트에 연결 하기&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post의 comments view 구현과 이에 맞게 comment create, read, delete 수정 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; comments delete가 아닌, 개별적인 comment delete, Update 구현 필요&lt;ul&gt;
&lt;li&gt;comment 수정, 제거에 따라 PostComment도 반영하도록 해주어야 함(Post에 있는 commentArray의 commentEl과 일치하는지 여부를 조건으로 반영 로직 짜기)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Post text 더보기 버튼&lt;ul&gt;
&lt;li&gt;Post Text가 일정 길이가 넘어가는 경우 버튼이 보이게 하고, 버튼 클릭시 css overflow hidden을 풀어주는 식으로 구현 하자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 자동 input 체크 (이름 중복 확인시)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 새 게시글 보기 버튼 또는 로고 클릭시 데이터 진입점 갱신 기능 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post 관련한 input의 check 대략적인 (PostUpdateContainer, postFormContainer)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; auth 관련한 input의 check 대략적인 조건 구현&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profile의 웹사이트 정규표현식 match 정교화&lt;ul&gt;
&lt;li&gt;사용자는 http를 안넣을 수도 있음, 그리고 그외에도 예외 사항을 더 생각해 보자&lt;/li&gt;
&lt;li&gt;아니면, 사용자가 올바른 형식을 넣을 수 있도록 알림 만들기, 결국엔 validation 임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스켈레톤 UI 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; propType으로 type 지정 또는 typeScript 도입&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; sementic tag 적절한 태그로 수정하기 (검토)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; route &amp;#39;/profile&amp;#39; pathName을 &amp;#39;/user/:userName&amp;#39; pathName 사용하게 통합하여 pathname에 대한 조건을 줄여 보자&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 좋아요 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/210</guid>
      <comments>https://goforit.tistory.com/210#entry210comment</comments>
      <pubDate>Wed, 18 Aug 2021 14:39:24 +0900</pubDate>
    </item>
    <item>
      <title>20210816 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit31 : 최신 댓글 최대 2개 보이기 구현, 댓글 총 개수 보이기 구현, useRandom hook 유지 보수, 신규 유저 가입시 count 넘버링 버그 해결, 로직 수정..</title>
      <link>https://goforit.tistory.com/209</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit31&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYAIs/btrcc5XnmX7/o507VJrQvWbPsXWZcaohM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYAIs/btrcc5XnmX7/o507VJrQvWbPsXWZcaohM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYAIs/btrcc5XnmX7/o507VJrQvWbPsXWZcaohM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlYAIs%2Fbtrcc5XnmX7%2Fo507VJrQvWbPsXWZcaohM0%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 가능하게 하고 싶음)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;무한 스크롤&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 스크롤 위치 기억 (뒤로가기가 아닌 페이지 변해도 기억 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;랜덤 유저 추천&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;댓글 기능&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.16 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;1. Post 컴포넌트에 최신 댓글 최대 2개 보이기 구현&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;434&quot; data-filename=&quot;post_comments.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1znON/btrb7vCGZLS/g31PFG3kDuiU8di8TWMkiK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1znON/btrb7vCGZLS/g31PFG3kDuiU8di8TWMkiK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1znON/btrb7vCGZLS/g31PFG3kDuiU8di8TWMkiK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/1znON/btrb7vCGZLS/g31PFG3kDuiU8di8TWMkiK/img.gif&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;434&quot; data-filename=&quot;post_comments.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;h3&gt;PostComment 컴포넌트 구현 및 스타일링&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;PostComment 컴포넌트는 특정 post에 대한 최신 댓글 최대 2개를 Post 컴포넌트에 1개씩 반복하여 표현하기 위한 Presentational Component이다.&lt;ul&gt;
&lt;li&gt;즉, Post 컴포넌트에서 최대 2개의 최신 comment를 요청하여 가져와서 2개를 감싼 배열을 map으로 풀어서 PostComment에 연결하여 댓글 1개에 대한 정보를 표현해 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스타일링은 scss로 적절하게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { Link } from &amp;quot;react-router-dom&amp;quot;;
import &amp;quot;./PostComment.scss&amp;quot;;

const PostComment = ({ commentItem }) =&amp;gt; {
  const { commentDisplayName, comment } = commentItem;
  return (
    &amp;lt;div className=&amp;quot;post_comment_container&amp;quot;&amp;gt;
      &amp;lt;Link className=&amp;quot;user_name&amp;quot; to={`/user/${commentDisplayName}`}&amp;gt;
        {commentDisplayName}
      &amp;lt;/Link&amp;gt;
      &amp;lt;span className=&amp;quot;comment_text&amp;quot;&amp;gt;{comment}&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default PostComment;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;DB post의 commentArray와 comment 일치 시키기&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;DB의 collection에서 posts와 comments를 따로 관리하고 있기 때문에 최근 작성된 comment가 post에도 반영되고 기록, 일치시키는 작업이 중요하다.&lt;/li&gt;
&lt;li&gt;즉, 댓글 작성시(CREATE) posts collection의 post, comments collection의 comment를 모두 기록해주어야 한다.&lt;ul&gt;
&lt;li&gt;CREATE 뿐만아니라, 최근 데이터에 대한 UPDATE, DELETE도 같이 수정 될 수 있게 해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;사용자가 글을 작성하는 경우 DB 기록될 post 데이터 구조에 commentArray라는 필드가 포함되어 올라가게 하였다.&lt;ul&gt;
&lt;li&gt;commentArray는 글이 처음으로 작성되는 때에는 &lt;code&gt;[ ]&lt;/code&gt; 빈 배열의 형태를 값으로 갖는다.&lt;/li&gt;
&lt;li&gt;나중에, 사용자가 해당 글에 댓글을 작성할 때는 comments collection에도 기록되고, 해당 글인 post에 commentArray에도 추가되어 동일성을 유지하도록 설계 하였다.&lt;/li&gt;
&lt;li&gt;commentArray는 comments라는 식별자 명이 많이 쓰이기 때문에 구분하기 위해서 사용하였다.&lt;/li&gt;
&lt;li&gt;commenArray에는 항상 최신의 댓글인 commentEl가 들어가고 최대 2개가 들어가게 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 댓글 작성시 comments collection에서 관리하는 댓글(comment) 데이터 구조 (단위)
const commentObj = {
  postId,
  userDisplayName: currentUserInfo.displayName,
  userPhotoUrl: currentUserInfo.userPhotoUrl,
  commentDate: Date.now(),
  comment,
  count: comments[0] ? comments[0].count + 1 : 1,
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 글 작성시 posts collection에서 관리하는 post 데이터 구조 (단위)
const post = {
  postText: text,
  postDate: Date.now(),
  userId: currentUser.uid,
  userPhotoUrl: currentUserInfo.userPhotoUrl,
  userDisplayName: currentUserInfo.displayName,
  postImageUrl: imageUrl,
  commentArray: [],
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 댓글 작성시 post 데이터 구조의 commentArray 필드에 들어오는 commentEl 데이터 구조
const commentEl = {
  commentId,
  commentDisplayName: commentObj.userDisplayName,
  commentDate: commentObj.commentDate,
  comment,
  count: commentObj.count,
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;CREATE Comment 요청 함수 수정 (setCommentThunk)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;comment db 기록 요청, post db에 최신 최대 2개 comment 기록 요청&lt;/li&gt;
&lt;li&gt;개선 필요 사항) 두 작업에 대한 비동기 처리를 통해 더 빠른 작업이 가능할 듯함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// async
export const setCommentThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/comment/setCommentThunk&amp;quot;,
  async ({ comment, postId }, thunkAPI) =&amp;gt; {
    try {
      // comment DB 반영 요청
      const {
        users: { currentUserInfo },
        comment: { comments },
      } = thunkAPI.getState();

      const commentObj = {
        postId,
        userDisplayName: currentUserInfo.displayName,
        userPhotoUrl: currentUserInfo.userPhotoUrl,
        commentDate: Date.now(),
        comment,
        count: comments[0] ? comments[0].count + 1 : 1,
      };
      const doc = dbService.collection(&amp;quot;comments&amp;quot;).doc();
      const commentId = doc.id;
      await doc.set(commentObj);

      // comment를 post DB의 commentArray 필드에 반영 요청
      const postDoc = dbService.collection(&amp;quot;posts&amp;quot;).doc(postId);
      const postDocSnap = await postDoc.get();
      const { commentArray } = { ...postDocSnap.data() };
      const commentEl = {
        commentId,
        commentDisplayName: commentObj.userDisplayName,
        commentDate: commentObj.commentDate,
        comment,
        count: commentObj.count,
      };
      // 최대 2개 이면서 최신 데이터 유지를 위해 큐를 이용한 작업
      if (commentArray.length === 2) {
        commentArray.unshift(commentEl);
        commentArray.pop();
      } else {
        commentArray.unshift(commentEl);
      }
      await postDoc.set(
        {
          commentArray,
        },
        { merge: true }
      );
      // update된 comment를 화면에 반영하기 위한 dispatch
      thunkAPI.dispatch(getCommentsThunk(postId));
      thunkAPI.dispatch(getAllPostsThunk());
      return true;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;최종 Post 컴포넌트에 표현하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;댓글 개수 보이기&lt;ul&gt;
&lt;li&gt;comment에도 count 필드 값을 넣었기 때문에, 최신 comment의 count 값을 보면 해당 post에 있는 댓글의 개수를 가져 올 수 있음&lt;/li&gt;
&lt;li&gt;댓글이 존재하지 않으면 보이지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;최신 댓글 최대 2개 보이기&lt;ul&gt;
&lt;li&gt;PostComment 컴포넌트를 연결함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const Post = ({ post, deletePost, updatePost, currentUserId }) =&amp;gt; {
  // ... 생략 ...

  return (
    // ... 생략

    &amp;lt;div className=&amp;quot;post_comments&amp;quot;&amp;gt;
      {post.commentArray[0] &amp;amp;&amp;amp; (
        &amp;lt;button className=&amp;quot;comments_count&amp;quot; onClick={goComments}&amp;gt;
          댓글 {post.commentArray[0].count}개 모두 보기
        &amp;lt;/button&amp;gt;
      )}
      {post.commentArray.map((commentEl) =&amp;gt; (
        &amp;lt;PostComment commentEl={commentEl} key={commentEl.commentId} /&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;

    // 생략 ...
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;2. useRandom hook 유지 보수&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;기존에 있던 useRandom hook의 경우 이미 사용자가 적어도 3명 정도인 것으로 만든 hook이라서 useRandom에서 사용되는 count, range 에 대한 잘못된 사용 제한이 필요하다.&lt;/li&gt;
&lt;li&gt;뽑는 숫자가 range 보다 클 수 있는 경우를 제한 해주어야 한다.&lt;/li&gt;
&lt;li&gt;그리고, db에 있는 기존의 zero based의 count 필드 값을 모두 one based 값으로 변경 했기 때문에 useRandom에도 변경을 주었음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const useRandom = (count, range, except) =&amp;gt; {
  // except을 고려하여 count가 range보다 큰 경우
  if (count &amp;gt; range - 1) {
    return [];
  }
  const randomSet = new Set();
  while (randomSet.size &amp;lt; count) {
    const temp = Math.floor(Math.random() * range) + 1;
    if (temp === except) {
      continue;
    }
    randomSet.add(temp);
  }
  return [...randomSet];
};

export default useRandom;

/* 
count : 뽑을 개수
range : 정수 숫자 범위 (range가 1이면 1 / 2이면 1~2 / 3이면 1~3)
except : 뽑는 숫자중에 제외할 정수
return : 일정 범위(0이상 ~ range 이하) 숫자에서 일정한 개수의 랜덤 숫자가 들어간 배열을 반환 (except 고려)
*/&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;3. 신규 유저 가입시 count 넘버링 버그 해결&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;기존의 신규 유저 가입의 경우 count 넘버링이 zero based Index라서 중간에 로직을 다루는 중에 0이 falsy해서 생기는 문제가 발생함&lt;/li&gt;
&lt;li&gt;count 넘버링을 one based로 시작하게 변경하여 해결 함&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;4. 로직 수정에 따른 React devServer 반영 문제&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;디버깅 과정에서 redux의 요청 함수 같은 것을 수정하는 경우, jsx 및 component를 고치는 것과 다르게 바로 dev서버에 반영이 되지 않는 듯 했다. 그래서 이를 모르고 안되는 건가 하고 맞게 잘한 로직도 반영이 안되어 시간을 엄청 날려 먹었다.&lt;/li&gt;
&lt;li&gt;항상 redux 관련 부분을 다루는 경우 또는 문제가 계속 잘 해결이 안되는 것 같다면 server를 한번 내리고 재시작 해야겠다. 물론, 이런 부분까지 바로 서버로 반영해주는 라이브러리가 존재하긴 할 것이다.&lt;/li&gt;
&lt;li&gt;예전에 nodeJS, express를 잠시 조금 공부 했을 때 서버에서 수정한 것은 서버를 내려야 반영된다는 것을 이미 알고 있었고, 이를 편하게 하기 위해서 서버 재실행 없이 watch해서 반영시키는 특정 라이브러리가 있었던 것이 얼핏 기억난다. 그런 느낌으로 react도 존재하지 않을까 생각한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;다음에 필요한 사항&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 좋아요 기능 -&amp;gt; 진행중&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 기본적인 comments page, comment, commentForm 스타일링&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; comment read, create 요청 함수 컴포넌트에 연결 하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post의 comments view 구현과 이에 맞게 comment create, read, delete 수정하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; comment update 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; comments delete가 아닌, 개별적인 comment delete, Update 구현 필요&lt;ul&gt;
&lt;li&gt;comment 수정, 제거에 따라 PostComment도 반영하도록 해주어야 함(Post에 있는 commentArray의 commentEl과 일치하는지 여부를 조건으로 반영 로직 짜기)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Post text 더보기 버튼&lt;ul&gt;
&lt;li&gt;Post Text가 일정 길이가 넘어가는 경우 버튼이 보이게 하고, 버튼 클릭시 css overflow hidden을 풀어주는 식으로 구현 하자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 자동 input 체크 (이름 중복 확인시)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 새 게시글 보기 버튼 또는 로고 클릭시 데이터 진입점 갱신 기능 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post 관련한 input의 check 대략적인 (PostUpdateContainer, postFormContainer)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; auth 관련한 input의 check 대략적인 조건 구현&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profile의 웹사이트 정규표현식 match 정교화&lt;ul&gt;
&lt;li&gt;사용자는 http를 안넣을 수도 있음, 그리고 그외에도 예외 사항을 더 생각해 보자&lt;/li&gt;
&lt;li&gt;아니면, 사용자가 올바른 형식을 넣을 수 있도록 알림 만들기, 결국엔 validation 임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스켈레톤 UI 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; propType으로 type 지정 또는 typeScript 도입&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; sementic tag 적절한 태그로 수정하기 (검토)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; route &amp;#39;/profile&amp;#39; pathName을 &amp;#39;/user/:userName&amp;#39; pathName 사용하게 통합하여 pathname에 대한 조건을 줄여 보자&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 기능 -&amp;gt; 구현중&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 좋아요 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/209</guid>
      <comments>https://goforit.tistory.com/209#entry209comment</comments>
      <pubDate>Tue, 17 Aug 2021 13:16:38 +0900</pubDate>
    </item>
    <item>
      <title>20210815 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit30 : 댓글 관련 컴포넌트 스타일링, 컴포넌트에 Comment Read, Create 연결 하기, Comment delete 구현하기</title>
      <link>https://goforit.tistory.com/208</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit30&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tu5KM/btrcecBToRe/V25mdYAShqBCjtExK5PlR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tu5KM/btrcecBToRe/V25mdYAShqBCjtExK5PlR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tu5KM/btrcecBToRe/V25mdYAShqBCjtExK5PlR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftu5KM%2FbtrcecBToRe%2FV25mdYAShqBCjtExK5PlR0%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 가능하게 하고 싶음)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;무한 스크롤&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 스크롤 위치 기억 (뒤로가기가 아닌 페이지 변해도 기억 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;랜덤 유저 추천&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;댓글 기능&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.15 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;1. 댓글 관련 컴포넌트 스타일링&lt;/h2&gt;
&lt;br/&gt;

&lt;h3&gt;PostControl 컴포넌트 스타일링&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;PostText와 PostImage 사이에 PostControl을 위치시킴&lt;/li&gt;
&lt;li&gt;좋아요의 하트 버튼, 댓글의 말풍선 버튼을 구성 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;798&quot; data-filename=&quot;post_control.png&quot; width=&quot;639&quot; height=&quot;591&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HTflO/btrb6uqjAwY/QCWu9S8nLccNK2274K7Kn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HTflO/btrb6uqjAwY/QCWu9S8nLccNK2274K7Kn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HTflO/btrb6uqjAwY/QCWu9S8nLccNK2274K7Kn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHTflO%2Fbtrb6uqjAwY%2FQCWu9S8nLccNK2274K7Kn0%2Fimg.png&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;798&quot; data-filename=&quot;post_control.png&quot; width=&quot;639&quot; height=&quot;591&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;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;댓글 페이지 스타일링&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;410&quot; data-filename=&quot;comments_style.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v1tDF/btrcea49gW9/ULKSggKdpSDlt5yWUK57hK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v1tDF/btrcea49gW9/ULKSggKdpSDlt5yWUK57hK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v1tDF/btrcea49gW9/ULKSggKdpSDlt5yWUK57hK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/v1tDF/btrcea49gW9/ULKSggKdpSDlt5yWUK57hK/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;410&quot; data-filename=&quot;comments_style.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;ul&gt;
&lt;li&gt;댓글 페이지 스타일링&lt;ul&gt;
&lt;li&gt;댓글 페이지 배치 스타일링 : Navigation 아래 inner 안에 main, side가 양옆으로 나란히 있는 구성으로 배치함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;CommentForm 컴포넌트 스타일링&lt;ul&gt;
&lt;li&gt;fixed 형태가 아닌 flex를 걸어서, main 안에 CommentForm, comments_container가 나란히 존재하고 conmments_container 안에서 여러 comment가 overflow scroll 형태로 움직이게 함&lt;/li&gt;
&lt;li&gt;submit 버튼 색&lt;ul&gt;
&lt;li&gt;disabled 때는 회색&lt;/li&gt;
&lt;li&gt;disabled가 제거된는 때 하늘색 (input 값이 있는 경우)&lt;/li&gt;
&lt;li&gt;hover 시에는 파란색&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;Comment 스타일링&lt;ul&gt;
&lt;li&gt;각 Comment는 댓글 작성자 이미지, 작성자 이름, 댓글 내용이 들어가 있는 형태이다.&lt;/li&gt;
&lt;li&gt;작성자 이미지는 동그라미가 되게하고 작성자 이름은 a tag를 사용하여 굵게 처리함 (작성자 이름 클릭시 해당 작성자 profile로 이동할 수 있게)&lt;/li&gt;
&lt;li&gt;댓글을 span tag를 사용하고 mixins 로 지정한 기본적인 text 컬러로 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;2. 컴포넌트에 Comment Read, Create 연결 하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;402&quot; data-filename=&quot;comments_cr.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNOenR/btrci95IdtA/JqDDM5s6tsrm9mRDXVScy0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNOenR/btrci95IdtA/JqDDM5s6tsrm9mRDXVScy0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNOenR/btrci95IdtA/JqDDM5s6tsrm9mRDXVScy0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bNOenR/btrci95IdtA/JqDDM5s6tsrm9mRDXVScy0/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;402&quot; data-filename=&quot;comments_cr.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;h3&gt;PostControlContainer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;PostControlContainer에서 PostControl 컴포넌트로 goComments라는 함수를 생성하여 넘김&lt;/li&gt;
&lt;li&gt;goComments 함수에서 &lt;strong&gt;getCommentsThunk는 postId를 인자로 받아 Read 요청&lt;/strong&gt;하고, postId에 맞는 &amp;#39;/postId/comments&amp;#39; path로 이동 시킴&lt;/li&gt;
&lt;li&gt;goComments 함수를 말풍선 아이콘을 클릭시 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// ... 생략

const goComments = useCallback(async () =&amp;gt; {
  await dispatch(getCommentsThunk(post.postId));
  history.push({ pathname: `/${post.postId}/comments`, state: { post } });
}, []);

// ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CommentFormContainer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CommentFormContainer에서 CommentForm 컴포넌트로 onSubmit 함수 생성하여 넘기&lt;/li&gt;
&lt;li&gt;onSubmit 함수에서 setCommentThunk는 postId와 comment를 인자로 받아 Create 요청&lt;ul&gt;
&lt;li&gt;그리고, input에 연결된 comment useState를 빈 문자열 &amp;#39;&amp;#39; 상태로 초기화 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// ... 생략

const onSubmit = useCallback(
  (event) =&amp;gt; {
    event.preventDefault();
    dispatch(setCommentThunk({ postId, comment }));
    setComment(&amp;quot;&amp;quot;);
  },
  [dispatch, postId, comment]
);

// ...&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;3. Comment delete 구현하기&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;delete 구현시 고려할 점&lt;ul&gt;
&lt;li&gt;Posts와 Comments를 따로 관리하기 때문에 Post, Comments가 상황에 일치 할 수 있도록 모두 신경 써야 함&lt;/li&gt;
&lt;li&gt;경우1) post가 글 작성자에 의해서 지워지는 경우, 모든 댓글도 같이 지워야 됨&lt;/li&gt;
&lt;li&gt;경우2) 댓글 작성자가 자신의 댓글만 지우는 경우&lt;ul&gt;
&lt;li&gt;이때, 만약 Post에도 2개의 comments view가 있는 경우 지우려는 댓글과 comments view에 있는 댓글과 일치하면, 반영해주어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;현재는 간단하게 경우1 만 구현&lt;/strong&gt;하여 해당 글의 댓글을 모두 지우는 요청으로 만들고, post delete 요청 함수안에 같이 요청하게 넣었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 해당 글에 대한 댓글 모두 지우기 요청 함수
export const deleteCommentsThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/comment/deleteCommentsThunk&amp;quot;,
  async (postId, thunkAPI) =&amp;gt; {
    try {
      const { docs } = await dbService
        .collection(&amp;quot;comments&amp;quot;)
        .where(&amp;quot;postId&amp;quot;, &amp;quot;==&amp;quot;, postId)
        .get();
      const promises = docs.forEach((doc) =&amp;gt; {
        dbService.collection(&amp;quot;comments&amp;quot;).doc(doc.id).delete();
      });
      await Promise.all(promises);
      return true;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 해당 글 지우기 요청 함수
export const deletePostThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/post/deletePostThunk&amp;quot;,
  async (post, thunkAPI) =&amp;gt; {
    try {
      const {
        profile: { currentUser },
      } = await thunkAPI.getState();
      const { postId, postImageUrl, userId } = post;
      // 유저 방어 코드
      if (userId === currentUser.uid) {
        // 글 지우기 요청
        await dbService.collection(&amp;quot;posts&amp;quot;).doc(postId).delete();
        // 해당글의 모든 댓글 지우기 요청
        thunkAPI.dispatch(deleteCommentsThunk(postId));
        if (postImageUrl !== &amp;quot;&amp;quot;) {
          await thunkAPI.dispatch(deleteImageUrlThunk(postImageUrl));
        }
      } else {
        throw new Error(&amp;quot;Invalid user access!&amp;quot;);
      }
      await thunkAPI.dispatch(resetImage());
      return true;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;다음에 필요한 사항&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 좋아요 기능 -&amp;gt; 진행중&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 기본적인 comments page, comment, commentForm 스타일링&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; comment read, create 요청 함수 컴포넌트에 연결 하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post의 comments view 구현과 이에 맞게 comment create, read, delete 수정하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; comment update 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 자동 input 체크 (이름 중복 확인시)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 새 게시글 보기 버튼 또는 로고 클릭시 데이터 진입점 갱신 기능 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post 관련한 input의 check 대략적인 (PostUpdateContainer, postFormContainer)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; auth 관련한 input의 check 대략적인 조건 구현&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스켈레톤 UI 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; propType으로 type 지정 또는 typeScript 도입&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; sementic tag 적절한 태그로 수정하기 (검토)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; route &amp;#39;/profile&amp;#39; pathName을 &amp;#39;/user/:userName&amp;#39; pathName 사용하게 통합하여 pathname에 대한 조건을 줄여 보자&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 좋아요 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/208</guid>
      <comments>https://goforit.tistory.com/208#entry208comment</comments>
      <pubDate>Mon, 16 Aug 2021 18:17:33 +0900</pubDate>
    </item>
    <item>
      <title>20210814 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit29 : 댓글 기능을 위한 Component 및 페이지 Component 설계,  Comments DB 설계, Redux Comment Slice 설계 및 Read, Create 구현</title>
      <link>https://goforit.tistory.com/207</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit29&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq2nZR/btrb7zLSe2u/o4g4u0e1pAoeIaDyXBCGVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq2nZR/btrb7zLSe2u/o4g4u0e1pAoeIaDyXBCGVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq2nZR/btrb7zLSe2u/o4g4u0e1pAoeIaDyXBCGVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq2nZR%2Fbtrb7zLSe2u%2Fo4g4u0e1pAoeIaDyXBCGVK%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 가능하게 하고 싶음)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;무한 스크롤&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 스크롤 위치 기억 (뒤로가기가 아닌 페이지 변해도 기억 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;랜덤 유저 추천&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.14 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;1. 댓글 기능을 위한 Component 및 페이지 Component 설계&lt;/h2&gt;
&lt;h3&gt;개요&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;각 글을 박스(Post Component)안에서 댓글 보기로 이동하는 버튼을 클릭시 해당 글에 대한 댓글 페이지로 이동&lt;/li&gt;
&lt;li&gt;댓글 페이지는 댓글을 작성하는 Form 과 해당 글에 작성된 댓글을 보여줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;라우터 설정&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;기존의 라우터에 Comments 페이지를 연결하는 라우트를 추가 함&lt;/li&gt;
&lt;li&gt;각 글마다 존재하는 postId를 동적으로 path 이름의 부분으로 사용함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&amp;lt;Route path=&amp;quot;/:postId/comments&amp;quot; exact component={Comments} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;컴포넌트&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;PostControl : 모든 글 마다 해당 글의 댓글을 보도록 이동시키는 버튼 및 좋아요 버튼을 다룸&lt;ul&gt;
&lt;li&gt;PostControl 컴포넌트의 경우 Post 컴포넌트에서 사용 됨&lt;/li&gt;
&lt;li&gt;PostControlContainer 컴포넌트와 짝을 이룸 (goComments 함수를 전달 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;Comments : 사용자가 확인하려는 글에 대한 댓글들과 댓글을 입력하는 Form을 보여주는 페이지&lt;ul&gt;
&lt;li&gt;CommentForm : 댓글을 작성하는 Form을 표시하는 컴포넌트 (CommentFormContainer와 짝을 이룸)&lt;/li&gt;
&lt;li&gt;Comment : 댓글 하나의 모양을 표시하는 컴포넌트 (Comments 페이지에서 Comment Array를 받아 map 함수를 통해 각각의 댓글이 Comment 컴포넌트로 전달 됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// improt ... 생략

const Comments = () =&amp;gt; {
  const {
    state: { post },
  } = useLocation();
  const comments = useSelector((state) =&amp;gt; state.comment.comments);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Navigation /&amp;gt;
      &amp;lt;div className=&amp;quot;page&amp;quot;&amp;gt;
        &amp;lt;div className=&amp;quot;inner&amp;quot;&amp;gt;
          &amp;lt;main className=&amp;quot;main comments_main&amp;quot;&amp;gt;
            &amp;lt;CommentFormContainer postId={post.postId} /&amp;gt;
            &amp;lt;ul className=&amp;quot;comments&amp;quot;&amp;gt;
              {comments.map((commentObj) =&amp;gt; (
                &amp;lt;Comment commentObj={commentObj} key={commentObj.commentId} /&amp;gt;
              ))}
            &amp;lt;/ul&amp;gt;
          &amp;lt;/main&amp;gt;
          &amp;lt;Side /&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div className=&amp;quot;modal_root&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

export default Comments;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;2. Comments DB 설계&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;Comments DB 설계는 두 가지 방식을 생각해 보았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;방식 1) Post 데이터 안에서 Comments 데이터 관리하는 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;댓글의 경우에는 사실 각 글에 존재하는 데이터로 Posts Collection에 각 Post 안에 Comments라는 필드 형식으로 만들어도 된다.&lt;/li&gt;
&lt;li&gt;단, Comments 필드는 Comment를 요소로 갖는 Array 형태이어야 한다.&lt;/li&gt;
&lt;li&gt;이렇게 중첩적으로 데이터를 구성하면, Comment가 몇개 안되면 상관 없겠지만 많아질 수록 Post 하나가 가지는 데이터의 크기가 너무 커지게 되어 &lt;strong&gt;단순히 Post와 관련된 정보로만 빠르게 Home 화면을 구성하기에 정보를 가져오는 속도가 느려지게 된다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;방식 2) Posts와 Comments 데이터 분리하여 관리하는 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;방식 1의 대안으로 Posts와 Comments를 분리하여 관리하는 것이다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;대신 Comments를 분리하여 관리하려면 Comments의 각 Comment들은 PostId를 가져야 한다. 이를 통해서, 어떤 Post에 작성된 Comment인지 식별할 수 있다.&lt;/li&gt;
&lt;li&gt;또한 이렇게 분리하면, Comment가 많아져도 Post에서 정보를 가져오는데 부담을 가지지 않게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;comment의 데이터 구조 (방식 2 채택)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;기본적으로 각 comment의 고유 ID는 firebase doc 단위에서 자동으로 doc 이름을 만들게 함&lt;ul&gt;
&lt;li&gt;나중에 data를 가져오게 되면, doc.id를 가져와서 반복되어 화면에 표시되는 요소의 key로 활용할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;postId : 작성되어진 글의 고유 id&lt;/li&gt;
&lt;li&gt;userDisplayName : 댓글을 작성한 유저 이름&lt;/li&gt;
&lt;li&gt;userPhotoUrl : 댓글을 작성한 유저 Profile image&lt;/li&gt;
&lt;li&gt;commentDate : 댓글을 작성한 시간&lt;/li&gt;
&lt;li&gt;comment : 댓글 내용&lt;/li&gt;
&lt;li&gt;count : 해당 post에서 작성된 현재 댓글의 순번 (댓글 개수를 빠르게 파악하기 위함)&lt;ul&gt;
&lt;li&gt;count 필드는 동시에 같은 글에서 댓글을 작성하는 경우 어떻게 될지 우려가 되긴 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const commentObj = {
  postId,
  userDisplayName: currentUserInfo.displayName,
  userPhotoUrl: currentUserInfo.userPhotoUrl,
  commentDate: Date.now(),
  comment,
  count: comments[0] ? comments[0].count + 1 : 0,
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;3. Redux Comment Slice 설계 및 Read, Create 구현&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;Comment와 관련된 서버 요청 및 전역 state를 사용하기 위해서 Redux-toolkit의 Slice 방식으로 Comment module을 만듦&lt;/li&gt;
&lt;li&gt;기본적으로, Comment의 CRUD를 생각하고 있고 무한 스크롤 형식의 getMore도 구현할 예정&lt;/li&gt;
&lt;li&gt;주의 해야 할점은 post를 뿌려 주는 화면에도 대표적인 댓글 2개 정도는 보여주어야 함으로 post에도 comment 데이터가 필요하다.&lt;ul&gt;
&lt;li&gt;comment를 작성 요청하는 경우, 댓글이 post 데이터에도 제일 최근 comment 데이터 2개 정도는 입력되고 계속 update 되게 해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일단, post에 대한 로직을 생각하지 않은체 기본적인 Comment의 Read와 Create를 구현하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;Initail State&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Initial State
const initialState = {
  comments: [],
  setComment: {
    loading: false,
    isSet: false,
    setError: &amp;quot;&amp;quot;,
  },
  getComments: {
    loading: false,
    isGet: false,
    getError: &amp;quot;&amp;quot;,
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;Create 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const setCommentThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/comment/setCommentThunk&amp;quot;,
  async ({ comment, postId }, thunkAPI) =&amp;gt; {
    // 댓글 내용과 postId를 인수로 받아옴
    try {
      // 작성자 정보와 이미 최근에 작성된 댓글의 count 가져 옴
      const {
        users: { currentUserInfo },
        comment: { comments },
      } = thunkAPI.getState();

      // firestore에 작성될 Comment 데이터 구조
      const commentObj = {
        postId,
        userDisplayName: currentUserInfo.displayName,
        userPhotoUrl: currentUserInfo.userPhotoUrl,
        commentDate: Date.now(),
        comment,
        count: comments[0] ? comments[0].count + 1 : 0, // 데이터를 받아올때 commentDate의 내림차순으로 가져오기 때문에 0번 인덱스가 최근 Comment임
      };

      // firestore에 해당 comment 데이터를 작성하게 요청
      await dbService.collection(&amp;quot;comments&amp;quot;).doc().set(commentObj);
      thunkAPI.dispatch(getCommentsThunk(postId)); // 보고 있던 화면을 다시 렌더링 해주기 위해서 요청함
      // comment 작성 요청에 대한 결과 true를 isGet에 update 함
      return true;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;Read 요청&lt;ul&gt;
&lt;li&gt;firestore에서 자동적으로 만드는 doc.id를 가져와서 commentId로 활용함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const getCommentsThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/comment/getCommentsThunk&amp;quot;,
  async (postId, thunkAPI) =&amp;gt; {
    try {
      const { docs } = await dbService
        .collection(&amp;quot;comments&amp;quot;)
        .where(&amp;quot;postId&amp;quot;, &amp;quot;==&amp;quot;, postId)
        .orderBy(&amp;quot;commentDate&amp;quot;, &amp;quot;desc&amp;quot;)
        .get();
      const comments = docs.map((doc) =&amp;gt; ({
        commentId: doc.id,
        ...doc.data(),
      }));
      return comments;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;다음에 필요한 사항&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 좋아요 기능 -&amp;gt; 진행중&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 기본적인 comments page, comment, commentForm 스타일링&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; comment read, create 요청 함수 컴포넌트에 연결 하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 자동 input 체크 (이름 중복 확인시)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 새 게시글 보기 버튼 또는 로고 클릭시 데이터 진입점 갱신 기능 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post 관련한 input의 check 대략적인 (PostUpdateContainer, postFormContainer)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; auth 관련한 input의 check 대략적인 조건 구현&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스켈레톤 UI 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; propType으로 type 지정 또는 typeScript 도입&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; sementic tag 적절한 태그로 수정하기 (검토)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; route &amp;#39;/profile&amp;#39; pathName을 &amp;#39;/user/:userName&amp;#39; pathName 사용하게 통합하여 pathname에 대한 조건을 줄여 보자&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 좋아요 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/207</guid>
      <comments>https://goforit.tistory.com/207#entry207comment</comments>
      <pubDate>Mon, 16 Aug 2021 16:01:22 +0900</pubDate>
    </item>
    <item>
      <title>20210813 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit28 : 랜덤 유저 추천 개선(useRandom 구현), 스크롤 위치 기억(useScroll 구현), useLayoutEffect</title>
      <link>https://goforit.tistory.com/206</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit28&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpPejR/btrcecntuyM/pK9IUFmjEydyXTvuXOjpI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpPejR/btrcecntuyM/pK9IUFmjEydyXTvuXOjpI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpPejR/btrcecntuyM/pK9IUFmjEydyXTvuXOjpI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpPejR%2FbtrcecntuyM%2FpK9IUFmjEydyXTvuXOjpI0%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 가능하게 하고 싶음)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;무한 스크롤&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 스크롤 위치 기억 (뒤로가기가 아닌 페이지 변해도 기억 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;랜덤 유저 추천&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.13 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;1. 랜덤 유저 추천 개선&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;352&quot; data-filename=&quot;random_user.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBrswe/btrb1qn4hPu/8vkxJrevPCJctHwNopHY1K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBrswe/btrb1qn4hPu/8vkxJrevPCJctHwNopHY1K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBrswe/btrb1qn4hPu/8vkxJrevPCJctHwNopHY1K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bBrswe/btrb1qn4hPu/8vkxJrevPCJctHwNopHY1K/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;352&quot; data-filename=&quot;random_user.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;h3&gt;기존의 랜덤 유저 추천 기능&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;기존에 랜덤 유저 추천 방식은 일정한 색인 내에서 자신의 프로필을 제외한 유저 프로필를 2개 가져오는 방식이었다. 그래서 유저가 추가되어 색인이 조금 변동이 생기지 않는이상 다양한 유저를 보여주지 못하는 한계가 있었다.&lt;/li&gt;
&lt;li&gt;원래도 useRandom이라는 hook을 만들어 활용하고 싶었다. Javascript의 set 자료구조를 이용해서 랜덤한 숫자의 중복을 제거한 배열을 반환하도록 만들었었다.&lt;/li&gt;
&lt;li&gt;하지만, 랜덤 유저 추천을 하려면 자신을 제외해야하는 로직이 필요했었다. 그 당시에는 firebase에서 두가지 필드를 조건으로 해서 데이터를 요청하고자 했는데 firebase는 두가지 다른 필드를 조건으로 해서 데이터 요청이 불가능 했었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;접근&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;그래서 임시적으로 자신을 제외한 2명의 유저 정보를 가져오는 것으로 사용하고 있었고, 어느 순간 useRandom에서 자신을 제외한 값을 가지는 radom 숫자 배열을 반환하는 형태로 변경하면 될 것 같다는 생각이 들었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;useRandom hook (useState, useEffect 등의 hook이 사용되진 않음)&lt;/h3&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const useRandom = (count, range, except) =&amp;gt; {
  if (count &amp;gt; range - 1) {
    return &amp;quot;범위 크기 보다 뽑으려는 개수가 많습니다.&amp;quot;;
  }
  const randomSet = new Set();
  while (randomSet.size &amp;lt; count) {
    const temp = Math.floor(Math.random() * (range + 1));
    if (temp === except) {
      continue;
    }
    randomSet.add(temp);
  }
  return [...randomSet];
};

export default useRandom;

/* 
count : 뽑을 개수
range : 정수 숫자 범위
except : 뽑는 숫자중에 제외할 정수
return : 일정 범위(0이상 ~ range 이하) 숫자에서 일정한 개수의 랜덤 숫자가 들어간 배열을 반환 (except 고려)
*/&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;랜덤 유저 정보 요청 함수 (getRandomUserInfoThunk)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;기본적으로 모든 유저는 회원 가입시에 count라는 필드에 가입된 순서대로 숫자를 가짐 (나중에 회원 수를 구하고 싶을때 가장 최근 유저 count 값을 통해 회원수를 가져올 때 활용 가능, 모든 유저를 가져올 필요 없이)&lt;/li&gt;
&lt;li&gt;count는 각 회원의 고유한 숫자이기 때문에, random 한 숫자를 가지고 해당 숫자에 맞는 회원을 가져올 수 있음&lt;/li&gt;
&lt;li&gt;getUserMaxCountThunk를 통해서 가장 최근 가입 유저의 정보에서 count 값을 가져옴 으로서 회원 range를 가져와서 활용함&lt;/li&gt;
&lt;li&gt;useRandom(count, range, except)를 활용하여 랜덤 유저 숫자 배열 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const getRandomUserInfoThunk = createAsyncThunk(
  &amp;quot;redux-racstagram/users/getRandomUserInfoThunk&amp;quot;,
  async (_, thunkAPI) =&amp;gt; {
    try {
      // 회원 수 요청
      await thunkAPI.dispatch(getUserMaxCountThunk());
      const {
        users: {
          currentUserInfo: { count },
          userMaxCount,
        },
      } = await thunkAPI.getState();

      // 현재 유저 count(제외 수)와 회원 수(range)를 가져와 useRandom hook을 돌림
      const random = useRandom(2, userMaxCount, count);

      // firestore에 해당 숫자에 맞는 유저 정보 요청
      const { docs } = await dbService
        .collection(&amp;quot;users&amp;quot;)
        .where(&amp;quot;count&amp;quot;, &amp;quot;in&amp;quot;, random)
        .get();

      // 각 유저의 displayName, userPhotoUrl 정보만 가진 데이터로 가공 -&amp;gt; array
      const res = docs.map((doc) =&amp;gt; {
        const { displayName, userPhotoUrl } = doc.data();
        return { displayName, userPhotoUrl };
      });

      return res;
    } catch ({ code, message }) {
      return thunkAPI.rejectWithValue({ code, message });
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;2. 스크롤 위치 기억 (useScroll 구현)&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;뒤로가기 버튼이 아닌, 페이지 컴포넌트가 unmount 되고 다시 mount가 되는 경우 unmount 되었을 때의 스크롤 위치를 기억하는 기능 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;443&quot; data-filename=&quot;memo_scroll.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kZnqj/btrcb8ljIPt/FRMKP1aPosKbJujOY4eLr0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kZnqj/btrcb8ljIPt/FRMKP1aPosKbJujOY4eLr0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZnqj/btrcb8ljIPt/FRMKP1aPosKbJujOY4eLr0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/kZnqj/btrcb8ljIPt/FRMKP1aPosKbJujOY4eLr0/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;443&quot; data-filename=&quot;memo_scroll.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;br/&gt;

&lt;h3&gt;접근&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;window.scrollY 값을 활용하여 redux에 prevScrollY state에 저장하게 하고 다시 mount 되는 경우 prevScrollY를 참조하여 window.scrollTo 함수를 통해서 해당 스크롤 위치로 이동시키게 하고자 함&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;문제 발생&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;unmount 되는 때에 해당 컴포넌트의 window.scrollY 값을 받고 싶지만 useEffect에서 unmount를 관리하는 return 부분은 컴포넌트가 unmount 되고나서 실행되기 때문에 다른 컴포넌트가 mount 되어 다른 컴포넌트의 window.scrollY 값을 참조하게 됨&lt;/li&gt;
&lt;li&gt;라이프 사이클을 디테일하게 다룰수 있는 방법을 찾아 보았지만, Before Unmount 에 관한 이야기는 없었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;시도1 : Scroll Event 리스너 부착하여 useEffect return 부분에 useState 참조 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;useEffect를 가지고 unmount 문제가 아닌 것인가 하여 scroll event 리스너를 부착하는 방식을 시도 해보았다.&lt;ul&gt;
&lt;li&gt;scroll event 리스너를 부착하는 방식은 불필요하게 너무 많은 event를 발생시키기 때문에 사용하고 싶지 않았다. 딱, unmount 때를 감지하여 값을 참조하고 싶었다. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리스너가 scroll을 감지해 계속 컴포넌트 내부 useState에 값을 반영하도록 하였다. 그리하여 useEffect return 부분에 state를 참조 하려고 시도 하였다.&lt;ul&gt;
&lt;li&gt;하지만 결과는 최초 mount 되었을 때 useState의 초기값 만을 참조되고, 스크롤 event로 인해 변경된 값은 참조되지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이렇다고 해서, unmount를 감지 하지 않고 mount 된 상태에서 계속 Scroll Event를 받으면 dispatch를 요청하는 것을 정말 아닌 것 같았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;시도2 : 리스너 없이 useEffect return 부분에서 setState(window.scrollY)를 시도하고 state 참조 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;기존에 있던 리스너를 빼고, useEffect return 부분에 setState로 값을 바로 변경하고, state를 참조하여 dispatch 하는 방식을 시도해 보았다.&lt;/li&gt;
&lt;li&gt;하지만, 시도1과 같이 state의 초기값만 참조하고, setState에 의해 변경된 값은 반영되어 지지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;차선책 : 해당 페이지를 unmount 시키는 버튼들에 모두 window.scrollY값을 dispatch하게 하는 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;엄청 돌고 돌아서, 간접적으로 원하는 페이지에서 unmount 시키는 즉, 다른 page로 이동시키는 nav 버튼 등에 onClick시 모두 window.scrollY 값을 dispatch 요청하는 것&lt;/li&gt;
&lt;li&gt;가능할것 같지만, 원하는 페이지가 아닌 다른 페이지에서 이런 버튼들을 누르게 되면 쓸데 없는 요청을 하게 됨&lt;/li&gt;
&lt;li&gt;진짜 진짜, 할 수 없으면 최종 책으로 사용하고자 함&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;&lt;strong&gt;최종 해결책 : useLayoutEffect&lt;/strong&gt;&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;하루 종일 google을 돌아다니다가, 어떻게 하다보니 React에서 useLayoutEffect hooks를 지원한다는 것을 알게 되었고 useLayoutEffect는 좀더 디테일한 라이프 사이클을 활용할 수 있게 도와주는 hook임을 알게 되었다. useLayoutEffect를 통해서 내가 원하는 Before Unmount인 라이프 사이클을 활용할 수 있게 되었다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://merrily-code.tistory.com/46&quot;&gt;useLayoutEffect 소개 by 찬민&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;969&quot; data-filename=&quot;hook_flow.png&quot; width=&quot;682&quot; height=&quot;868&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8PkVK/btrb5ndupTc/Su715Mu9BrwsRRKjp47GN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8PkVK/btrb5ndupTc/Su715Mu9BrwsRRKjp47GN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8PkVK/btrb5ndupTc/Su715Mu9BrwsRRKjp47GN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8PkVK%2Fbtrb5ndupTc%2FSu715Mu9BrwsRRKjp47GN1%2Fimg.png&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;969&quot; data-filename=&quot;hook_flow.png&quot; width=&quot;682&quot; height=&quot;868&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;br/&gt;

&lt;ul&gt;
&lt;li&gt;useScroll 구현 (해당 프로젝트에서만 재사용 가능함)&lt;ul&gt;
&lt;li&gt;redux state 값을 참조하고 update 함&lt;/li&gt;
&lt;li&gt;unmount 시의 사용자 window.scrollY 값을 redux의 prevScrollY에 올림&lt;/li&gt;
&lt;li&gt;그리고 mount 되고 prevScrollY 값을 참조받아 scrollTo 메서드로 스크롤을 해당 위치로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { useLayoutEffect } from &amp;quot;react&amp;quot;;
import { useDispatch, useSelector } from &amp;quot;react-redux&amp;quot;;
import { setPrevScrollY } from &amp;quot;../redux/modules/post&amp;quot;;

const useScroll = () =&amp;gt; {
  const dispatch = useDispatch();
  const prevScrollY = useSelector((state) =&amp;gt; state.post.prevScrollY);

  useLayoutEffect(() =&amp;gt; {
    if (prevScrollY) {
      window.scrollTo(0, prevScrollY);
    }
    return () =&amp;gt; {
      dispatch(setPrevScrollY(window.scrollY));
    };
  }, []);
};

export default useScroll;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;다음에 필요한 사항&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 좋아요 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 자동 input 체크 (이름 중복 확인시)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 새 게시글 보기 버튼 또는 로고 클릭시 데이터 진입점 갱신 기능 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 랜덤 유저 개선하기 : useRandom 제외 값 지정하게 변경하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post 관련한 input의 check 대략적인 (PostUpdateContainer, postFormContainer)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; auth 관련한 input의 check 대략적인 조건 구현&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스켈레톤 UI 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; propType으로 type 지정 또는 typeScript 도입&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; sementic tag 적절한 태그로 수정하기 (검토)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; route &amp;#39;/profile&amp;#39; pathName을 &amp;#39;/user/:userName&amp;#39; pathName 사용하게 통합하여 pathname에 대한 조건을 줄여 보자&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; side 바에 유저 랜덤 추천 및 푸터 정보&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 무한 스크롤&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 댓글 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 좋아요 기능&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <category>useLayoutEffect</category>
      <category>랜덤 유저 추천</category>
      <category>스크롤 위치 기억</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/206</guid>
      <comments>https://goforit.tistory.com/206#entry206comment</comments>
      <pubDate>Sat, 14 Aug 2021 18:53:30 +0900</pubDate>
    </item>
    <item>
      <title>20210811 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit27 : 무한스크롤 구현, IntersectionObserver, 불필요한 데이터 요청 제거 및 데이터 요청 시기 조정, 코드 중복 제거를 위한 통합에 대한 고찰</title>
      <link>https://goforit.tistory.com/205</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit27&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q5f8e/btrb2aX0bBA/p0AncZeFCJBbx1ATVKfsAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q5f8e/btrb2aX0bBA/p0AncZeFCJBbx1ATVKfsAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q5f8e/btrb2aX0bBA/p0AncZeFCJBbx1ATVKfsAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq5f8e%2Fbtrb2aX0bBA%2Fp0AncZeFCJBbx1ATVKfsAK%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 가능하게 하고 싶음)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.11 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;작업개요 및 고찰&lt;/h2&gt;
&lt;br/&gt;

&lt;h3&gt;1. 무한 스크롤 구현&lt;/h3&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;327&quot; data-filename=&quot;infinite_scroll.gif&quot; width=&quot;660&quot; height=&quot;509&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxWPeQ/btrbUYrGTQJ/5Qi7BKhKgrC0UrwZpZ5ESk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxWPeQ/btrbUYrGTQJ/5Qi7BKhKgrC0UrwZpZ5ESk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxWPeQ/btrbUYrGTQJ/5Qi7BKhKgrC0UrwZpZ5ESk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dxWPeQ/btrbUYrGTQJ/5Qi7BKhKgrC0UrwZpZ5ESk/img.gif&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;327&quot; data-filename=&quot;infinite_scroll.gif&quot; width=&quot;660&quot; height=&quot;509&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;br/&gt;

&lt;ul&gt;
&lt;li&gt;무한 스크롤 구현 방식 2가지&lt;ul&gt;
&lt;li&gt;방식1) clientHeight + scrollTop = scrollHeight를 이용하는 방식&lt;ul&gt;
&lt;li&gt;하지만, 이런 방법을 사용하면 계속해서 DOM에 접근하기 때문에 성능 이슈가 발생함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;방식2) IntersectionObserver 방식&lt;ul&gt;
&lt;li&gt;Observer를 설정하여 target Element를 설정하여, target이 viewPort에 보이는 경우를 감지하여 필요한 함수를 실행시킴&lt;/li&gt;
&lt;li&gt;성능적 낭비가 없으며, 현재 MDN에 실험적인 기능으로 올라와 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;방식2 : IntersectionObserver 방식 선택&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;전체적인 동작 방식&lt;ul&gt;
&lt;li&gt;(1) 기본적으로 일정 개수의 데이터를 요청하여 가져와 화면에 나타냄&lt;ul&gt;
&lt;li&gt;이때, target도 이미 JSX로 render 되고 ref로 target을 가져올 수 있게함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;(2) render 이후 targetRef가 들어온 이후 IntersectionObserver의 target에 Observer의 observe를 실행시킴&lt;/li&gt;
&lt;li&gt;(3) target이 화면에 보여 감지되는 경우 현재 state에 있는 데이터의 마지막 데이터 이후 부터의 데이터를 일정 개수 요청하도록 하는 함수 실행&lt;/li&gt;
&lt;li&gt;(4) 요청 반복&lt;/li&gt;
&lt;li&gt;(5) 요청 반복후 더이상 가져올 데이터가 없는 경우, error message 값을 state로 올림&lt;/li&gt;
&lt;li&gt;(6) error message가 발생하면, target에 설정된 intersectionObserver의 Observe를 unobserve 또는 disconnect 메서드를 실행시켜 Observe를 끔&lt;ul&gt;
&lt;li&gt;이때, 더 이상 데이터가 없음을 표시함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const component = () =&amp;gt; {
  const target = useRef(null);
  const _onIntersect = useCallback(
    ([{ isIntersecting }]) =&amp;gt; {
      if (getError.message) {
        return;
      }
      if (isIntersecting) {
        getMorePosts();
      }
    },
    [getMorePosts, getError]
  );

  useEffect(() =&amp;gt; {
    let observer;
    // 더이상 불러올 자료가 없는 경우
    if (getError.message) {
      observer &amp;amp;&amp;amp; observer.disconnect();

      // target이 있고, 처음에 불러온 데이터가 있는 경우 observe
    } else if (target &amp;amp;&amp;amp; posts.length) {
      observer = new IntersectionObserver(_onIntersect, {
        rootMargin: `1px`,
        threshold: 0.5,
      });
      observer.observe(target.current);
    }
    return () =&amp;gt; {
      observer &amp;amp;&amp;amp; observer.disconnect();
    };
  }, [getError, _onIntersect, posts]);

  return (
    // 여러 가지 posts 코드 생략
    &amp;lt;div ref={target}&amp;gt;&amp;lt;/div&amp;gt;
  );
};
// 아직, 개선이 필요한 코드임&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;신경쓴 부분&lt;ul&gt;
&lt;li&gt;최대한 모바일 환경과 비슷한 경험을 주도록 신경씀&lt;/li&gt;
&lt;li&gt;사용자가 다른 페이지(탭)로 이동하여도 뒤로가기 버튼시에는 기존에 보던 &lt;strong&gt;스크롤 위치를 유지&lt;/strong&gt;하고자 함&lt;/li&gt;
&lt;li&gt;유저의 스크롤에 의해 계속 요청되어 보여진 데이터도 따로 reset 또는 다시 불러들이지 않도록 하여 &lt;strong&gt;server 요청 낭비를 제어&lt;/strong&gt;하고자 함&lt;/li&gt;
&lt;li&gt;그리하여, 로그인 이후 처음에만 가장 최근 데이터를 가져오게 하여 무한 스크롤 진입점을 제시하고 무한 스크롤을 통한 데이터 요청하도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;문제점&lt;ul&gt;
&lt;li&gt;하지만 위처럼 하게 되면 다른 유저에 의해서 update된 데이터를 새로 갱신할 시점을 정해야함 모바일 환경에서는 보통 Pull down refresh 기능 또는 새로운 게시물 UI 버튼을 표시하여 사용자가 직접 refresh를 유도하게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;아이디어&lt;ul&gt;
&lt;li&gt;데이터 진입점 갱신을 &lt;strong&gt;racstagram 로고 클릭&lt;/strong&gt;을 통해 하거나, 사용자 로그인 이후 일정 시간 마다 진입점 이후 update된 게시물이 있는지 확인하여 &lt;strong&gt;새 게시물 보기 버튼&lt;/strong&gt;을 화면에 띄워 사용자가 진입점 갱신을 선택하도록 개선 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h3&gt;2. 불필요한 데이터 요청 최적화&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;로그인 이후 기본적으로 필요한 데이터를 미리 요청함&lt;ul&gt;
&lt;li&gt;기존에는 render이후 useEffect로 요청하는 형태 였음 그렇기에 처음 부터 바로 표시하지 않고 빈 값을 화면에 뿌리고, 그 이후 요청에 의한 데이터가 들어와 다시 render 시키는 형태 였음 (render만 2번 일으킴)&lt;/li&gt;
&lt;li&gt;로그인 이후 보여질 화면에서 요청하는게 아닌 그 전에 요청을 하기 위해서 Router 부분에서 로그인 true가 되는 경우 요청하도록 함&lt;/li&gt;
&lt;li&gt;randomUserInfo, currentUserInfo, currentUserPosts 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기존에 페이지에서 요청하는 불필요한 데이터 요청 코드 제거&lt;/li&gt;
&lt;li&gt;글을 수정, 작성, 삭제 등 또는 프로필 변경이 있을 경우에만 필요한 데이터를 재요청하도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;재사용성과 중복 제거에 대한 고찰&lt;/h2&gt;
&lt;br/&gt;

&lt;p&gt;코드를 짜면서 개발자는 항상 개발 입장에서 코드를 재사용할 수 있게 하는 것을 목표로 한다. 그래서 내가 코드를 짤때는 어느정도 중복(보일러 플레이트 코드)가 발생하는 경우 이것들을 합칠까 생각하고, 합치기도 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;과연 중복 제거를 위해서 합치는게 과연 옳은 판단인지 생각하게 된다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;예를 들어, A 컴포넌트 와 B컴포넌트가 있다고 가정하면 나는 코드를 작성하였고 확인해 보니 A와 B가 내용적으로 많이 겹치는 부분이 있지만 조금씩 다른 부분도 존재하였다. 그러면, A와 B를 합칠 것을 고려하게 된다.&lt;/p&gt;
&lt;p&gt;하지만, A와 B가 조금 다른 것을 고려하여 C라는 컴포넌트로 합치는 경우 A와 B를 고려한 코드 작성에 의해서 A, B에 대한 조건을 C에 많이 작성하게 되는 것 같다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;물론, 내가 이 프로젝트를 하면서 큰 계획없이 대략적인 계획만 세워놓고 만들었기 때문에 그럴 수도 있다고 생각은 한다.&lt;/p&gt;
&lt;p&gt;(&lt;em&gt;동시에 계획이 얼마나 중요하지 새삼 느끼게 되었다. 또, 실무에서의 제품 개발에 계획을 어떻게 구성하는지 궁금하고 좋은 선임 개발자에게 배우거나 동료와 토의하여 경험해 보고 싶다는 생각이 들었다.&lt;/em&gt;)&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;strong&gt;그럼에도 불구하고 기능 추가와 유지 보수는 필연적으로 발생하는 상황이라고 본다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이러한 상황 때문에 A, B의 차이를 C에서 다루기 위해 C에서의 &lt;strong&gt;로직이 너무 복잡해 지고 다른 개발자가 보았을 때 이해하기가 힘들어지며 본인도 개발 과정에서도 추적하기가 어려워 지는 것 같다.&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이를 위해서, 컴포넌트를 더 잘게 잘게 잘라서 관리 해야 하는 것일 까?&lt;br&gt;어쨌거나 중복제거를 위한 통합은 신중하게 결정해야 하는 것 같다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이런 고찰 중에 개발 방법론의 애자일 방식(필연적인 기능 추가와 유지 보수 발생)과 처음부터 엄청난 설계를 통한 개발 방식인 워터폴 방식이 떠올랐다. 둘 중에 정답은 없으며, 요즘 트렌드는 애자일이라고는 하지만 장단점이 있는것 같다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h2&gt;다음에 필요한 사항&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 랜덤 유저 개선하기 : useRandom 제외 값 지정하게 변경하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profile 정보 요청 시기 조정&lt;ul&gt;
&lt;li&gt;더 빠른 연산을 위해서, 화면이 render 되고 profile에 관련된 정보를 가져오지 말고 profile 보기위해 버튼을 눌렀을 때 부터 미리 profile 정보를 요청하게 하자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post 관련한 input의 check 대략적인 (PostUpdateContainer, postFormContainer)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; auth 관련한 input의 check 대략적인 조건 구현&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; route &amp;#39;/profile&amp;#39; pathName을 &amp;#39;/user/:userName&amp;#39; pathName 사용하게 통합하여 pathname에 대한 조건을 줄여 보자&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;input 자동 체크(중복 검사 기능에 추가)&lt;/li&gt;
&lt;li&gt;side 바에 유저 랜덤 추천 및 푸터 정보&lt;/li&gt;
&lt;li&gt;유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;무한 스크롤&lt;/li&gt;
&lt;li&gt;게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/205</guid>
      <comments>https://goforit.tistory.com/205#entry205comment</comments>
      <pubDate>Fri, 13 Aug 2021 01:22:38 +0900</pubDate>
    </item>
    <item>
      <title>20210809 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit26 : 본인을 제외한 랜덤 유저 추천 기능 구현, side 컴포넌트 스타일링 (회원 추천 + 푸터)</title>
      <link>https://goforit.tistory.com/204</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit26&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eK2ojB/btrbVrfOhLh/8MuYa5OqXeBKnhCE6GUCEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eK2ojB/btrbVrfOhLh/8MuYa5OqXeBKnhCE6GUCEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eK2ojB/btrbVrfOhLh/8MuYa5OqXeBKnhCE6GUCEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeK2ojB%2FbtrbVrfOhLh%2F8MuYa5OqXeBKnhCE6GUCEk%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 가능하게 하고 싶음)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.09 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;작업개요 및 고찰&lt;/h2&gt;
&lt;br/&gt;

&lt;h3&gt;1. 유저 추천 기능 구현 (제한적으로 구현함)&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;유저 추천 기능 : 자신을 제외한 랜덤 유저 몇명의 정보를 띄워 추천 함&lt;ul&gt;
&lt;li&gt;접근01) 랜덤 유저 정보를 가져오기 위해서 각 사용자 마다 고유의 숫자를 주고 랜덤한 숫자에 맞는 유저를 가져옴&lt;ul&gt;
&lt;li&gt;랜덤한 숫자의 array를 만들수 있게 useRandom hook을 만듦&lt;/li&gt;
&lt;li&gt;useRandom은 count 라는 몇개를 뽑을지에 대한 매개변수와 range라는 몇의 범위에서 뽑을지에 대한 매개변수 이다.&lt;/li&gt;
&lt;li&gt;while문을 통해서 set 집합에 있는 요소 개수와 뽑을 개수가 같아지는 순간 뽑는것을 그만 둔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const useRandom = (count, range) =&amp;gt; {
  const randomSet = new Set();
  while (randomSet.size &amp;lt; count) {
    randomSet.add(Math.floor(Math.random() * range));
  }
  return [...randomSet];
};

export default useRandom;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;만들어진 랜덤 숫자 리스트를 가지고, firestore에서 해당 숫자를 가진 유저의 정보를 가져오게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;문제 발생&lt;ul&gt;
&lt;li&gt;firebase 특성상 2중 필드 조건을 허용하지 않는다. (dispalyName이 자신이 아닌 것 중에서 count가 해당 숫자인 것을 가져오게 하고 싶었지만 불가능 했다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;임시 방편&lt;ul&gt;
&lt;li&gt;그래서 임시적으로 displayName이 자신이 아닌 것을 기준으로 색인에서 몇개를 가져오게 하는 firestore의 limit() 함수를 사용하였다.&lt;/li&gt;
&lt;li&gt;하지만 이렇게 하면, 가입유저에 대한 변동이 없는 이상 색인이 변하지 않아서 계속 주변 동일한 사용자를 가져오게 되는 문제가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const getRandomUserInfo = async() =&amp;gt; {

  /*
  // 동일 필드가 아닌 2중 복합 쿼리 불가
  const { docs } = await dbService
          .collection(&amp;#39;users&amp;#39;)
      .where(&amp;#39;displayName&amp;#39;, &amp;#39;!=&amp;#39;, displayName)
      .where(&amp;#39;count&amp;#39;, &amp;#39;in&amp;#39;, useRandom(2, userMaxCount))
      .get();
  */

  // 임시방편
  const { docs } = await dbService
                .collection(&amp;#39;users&amp;#39;)
                .where(&amp;#39;displayName&amp;#39;, &amp;#39;!=&amp;#39;, displayName)
                .limit(2)
                .get();

  const res = docs.map((doc) =&amp;gt; {
    const { displayName, userPhotoUrl } = doc.data();
    return { displayName, userPhotoUrl };
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;나중에 시도해 볼 아이디어&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;현재 유저의 user info의 count 값을 가져와서 해당 값을 제외한 랜덤 숫자 리스트 만들기로 useRandom을 수정해 보자&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;유저 추천 표시가 들어 있는 side 영역의 컴포넌트 스타일링 작업 실시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;461&quot; data-filename=&quot;recommend_user.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDPvF8/btrb23dfAWt/8bW9v2sa0PGDbFy2RM9r21/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDPvF8/btrb23dfAWt/8bW9v2sa0PGDbFy2RM9r21/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDPvF8/btrb23dfAWt/8bW9v2sa0PGDbFy2RM9r21/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dDPvF8/btrb23dfAWt/8bW9v2sa0PGDbFy2RM9r21/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;461&quot; data-filename=&quot;recommend_user.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;br/&gt;

&lt;h2&gt;다음에 필요한 사항&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 랜덤 유저 개선하기 : useRandom 제외 값 지정하게 변경하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profile 정보 요청 시기 조정&lt;ul&gt;
&lt;li&gt;더 빠른 연산을 위해서, 화면이 render 되고 profile에 관련된 정보를 가져오지 말고 profile 보기위해 버튼을 눌렀을 때 부터 미리 profile 정보를 요청하게 하자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post 관련한 input의 check 대략적인 (PostUpdateContainer, postFormContainer)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; auth 관련한 input의 check 대략적인 조건 구현&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;input 자동 체크(중복 검사 기능에 추가)&lt;/li&gt;
&lt;li&gt;side 바에 유저 랜덤 추천 및 푸터 정보&lt;/li&gt;
&lt;li&gt;유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;무한 스크롤&lt;/li&gt;
&lt;li&gt;게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/204</guid>
      <comments>https://goforit.tistory.com/204#entry204comment</comments>
      <pubDate>Fri, 13 Aug 2021 01:14:11 +0900</pubDate>
    </item>
    <item>
      <title>20210808 리팩토링 Instagram 클론 프로젝트 by Redux-toolkit25 : Post Detail View로 이동시 해당 글의 scrollTop 위치로 이동 구현하기</title>
      <link>https://goforit.tistory.com/203</link>
      <description>&lt;blockquote&gt;
&lt;h1&gt;리팩토링 Instagram 클론 프로젝트 by Redux-toolkit25&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkkMSN/btrb2b3FALt/IL6FBvklgp9en09ygux3c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkkMSN/btrb2b3FALt/IL6FBvklgp9en09ygux3c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkkMSN/btrb2b3FALt/IL6FBvklgp9en09ygux3c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkkMSN%2Fbtrb2b3FALt%2FIL6FBvklgp9en09ygux3c0%2Fimg.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot; data-filename=&quot;instagram_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  프로젝트 설명&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;p&gt;이 프로젝트는 기존에 React &amp;amp; firebase를 통해서 만든 인스타그램 클론 프로젝트 리팩토링 프로젝트 입니다. (해당 프로젝트는 프로젝트 카테고리에서 확인 가능합니다.)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;상태 관리&lt;/h2&gt;
&lt;p&gt;해당 프로젝트에서는 &lt;code&gt;redux-toolkit(Slice 모델)&lt;/code&gt;을 사용하여 상태관리를 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;스타일&lt;/h2&gt;
&lt;p&gt;현재 SCSS를 채택하여 css 작업을 진행중에 있으며, 부분적으로 Material UI를 사용하고 있습니다.&lt;br&gt;대부분의 경우에는, Material UI와 React 호환성 문제로 대부분은 SCSS로 직접 구현하고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;  화면 개요&lt;/h1&gt;
&lt;br/&gt;

&lt;p&gt;체크는 현재 기능적으로 구현된 상황을 의미합니다.&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로딩 화면 또는 Component&lt;/code&gt; : 앱 실행 초기화 작업시 로딩 또는 다른 작업시 사용할 로딩 화면 및 Component&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 스타일링 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 화면&lt;/code&gt; : 기본 Email 로그인, Social 로그인, 로그인 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 로그인&lt;/code&gt; : Email, Password input, 로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Social 로그인&lt;/code&gt; : google로그인 버튼, github로그인 버튼&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그인 에러&lt;/code&gt; : Email로그인, google로그인, github 로그인 에러 발생시 사용자에게 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;회원가입 화면&lt;/code&gt; : Email 로그인을 위한 계정을 만드는 화면, 회원가입 에러&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;Email 형식 가입&lt;/code&gt; : Email, Password input, 회원가입 버튼&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 가입시 사용자 Nickname 지정 input (추가 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;피드 화면&lt;/code&gt; : 사용 유저의 모든 게시글을 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;게시글 박스&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;타이틀 영역&lt;/code&gt; : 최상단의 작성자 사진 + 이름, 게시글 수정 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;편집버튼&lt;/code&gt; : 글 수정하기, 삭제하기 모달 -&amp;gt; 해당 버튼 누르면 삭제 또는 수정 페이지로 이동(아니면 모달이 수정하는 모달로 변경)&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;삭제하기&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;수정하기&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;사진 영역&lt;/code&gt; : 기존에는 1개만 가능했음 (욕심내면, 여러개 슬라이드 형식으로 가능하게 하고 싶음)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;내용 영역&lt;/code&gt; : 게시글 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;글 작성 화면&lt;/code&gt; : 글을 작성하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;이미지 리사이징&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;현재 유저 프로필 화면&lt;/code&gt; : 로그인한 현재 유저의 게시물과 대략적인 프로필를 표시하는 화면&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;유저 프로필 수정하기&lt;/code&gt; : 유저 프로필을 수정하는 화면 (userImage, userDisplayname, userIntro)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;로그아웃&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;다른 유저 프로필 화면&lt;/code&gt; : 다른 유저가 작성한 글의 유저 이름을 클릭하여 해당 유저의 프로필 화면 구현&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;프로필 보기&lt;/code&gt; : userImage, userDisplayname, userIntro&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글&lt;/code&gt; : 유저가 작성한 작성 글의 image 표 -&amp;gt; 클릭시 post detail&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;작성 글 detail view&lt;/code&gt; : image 표에서 해당 이미지 클릭시 해당 글 detail view 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;네비게이션 바&lt;/code&gt; : 앱로고 - 피드(Home)탭 - 글 작성탭 - 현재 유저 프로필(프로필 수정, 프로필 이동, 로그아웃) 탭&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Navigation-profile 눌렀을 때 로그아웃, 프로필 수정, 프로필 이동 드롭 다운 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  TIL (Today I Learned, 오늘 깨달은 것들)&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h1&gt;2021.08.08 사항&lt;/h1&gt;
&lt;br/&gt;

&lt;h2&gt;작업개요 및 고찰&lt;/h2&gt;
&lt;br/&gt;

&lt;h3&gt;1. Post Detail View로 이동시 해당 글의 scrollTop 위치로 이동하게 구현하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;다른 유저 및 자신의 프로필에 있는 post Image table에서 특정 이미지 클릭시 해당 글위치로 이동 함&lt;/li&gt;
&lt;li&gt;ProfilePostImages 컴포넌트에서 유저의 이미지 테이블을 보여주는데 해당 이미지를 클릭하는 경우 해당 이미지가 몇번째 글인지 postNum울 Posts라는 페이지로 전달하여 Posts 페이지에서 보여줄 해당글에 스크롤이 위치하도록 함&lt;ul&gt;
&lt;li&gt;Posts 페이지는 자신 또는 특정 유저가 작성한 글만을 Home 페이지 (Feed) 처럼 글을 보여줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;527&quot; data-filename=&quot;move_to_post.gif&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHHX0y/btrbW183pky/tVKDUEGHvoknU6TC98p4qK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHHX0y/btrbW183pky/tVKDUEGHvoknU6TC98p4qK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHHX0y/btrbW183pky/tVKDUEGHvoknU6TC98p4qK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cHHX0y/btrbW183pky/tVKDUEGHvoknU6TC98p4qK/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;527&quot; data-filename=&quot;move_to_post.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;ul&gt;
&lt;li&gt;Posts 페이지로 이동 시키는 함수&lt;ul&gt;
&lt;li&gt;현재 유저의 Profile 컴포넌트와 특정 유저의 User 컴포넌트의 경우에는 거의 비슷하여 Route는 다르지만 Profile 컴포넌트로 통합하여 사용하기 때문에, 각 함수를 구현할 때는 pathname을 조건으로 다른 라우트로 갈수 있게 해야함&lt;ul&gt;
&lt;li&gt;여기서 드는 생각은 현재 profile과 user pathname을 분리하여 놓았기 때문에 오히려 복잡한 로직 함수를 사용하게 되므로 pathname을 현재 유저도 userName을 현재 유저 이름으로 하여 &lt;code&gt;/user/userName&lt;/code&gt;를 같이 사용하는게 좋을 듯 해 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Posts 페이지로 이동 시키는 함수는 선택한 글이 몇번째 글인지를 같이해서 보냄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;    const goPosts = useCallback(
        (postNum) =&amp;gt; {
            if (pathname === &amp;#39;/profile&amp;#39;) {
                history.push({ pathname: &amp;#39;/profile/posts&amp;#39;, state: { postNum } });
            } else if (pathname === `/user/${userName}`) {
                history.push({
                    pathname: `/user/${userName}/posts`,
                    state: { postNum },
                });
            } else {
                console.log(&amp;#39;invalid location request&amp;#39;);
            }
        },
        [hist&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;선택한 이미지가 몇번째 글의 이미지인지 계산하기 (postNum)&lt;ul&gt;
&lt;li&gt;매개변수 postNum은 이미지 테이블 내에서 몇번째 row인지, row안에서 몇번째 인지를 계산해서 전달&lt;/li&gt;
&lt;li&gt;ProfilePostImages 컴포넌트의 이미지 테이블은 1 row당 3개의 이미지가 들어가있음&lt;/li&gt;
&lt;li&gt;예를 들어, 5번째 이미지의 경우 row_index 1번 row의 index_in_row 1번에 해당하므로 &lt;code&gt;3 * row_index + index_in_row&lt;/code&gt; 로 계산하여 전체 posts array에서 계산된 인덱스를 사용하여 해당 post를 가져올 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 생략 ...
return (
  &amp;lt;div className=&amp;quot;post_table&amp;quot;&amp;gt;
    {devidePosts(posts).map((row, index) =&amp;gt; (
      &amp;lt;div className=&amp;quot;posts_row&amp;quot; key={index.toString()}&amp;gt;
        {[0, 1, 2].map((i) =&amp;gt;
          row[i] ? (
            &amp;lt;div
              className=&amp;quot;post_image_container&amp;quot;
              onClick={() =&amp;gt; {
                goPosts(3 * index + i);
              }}
              key={`postTable/${row[i].postId.toString()}`}
            &amp;gt;
              &amp;lt;img
                src={row[i].postImageUrl}
                alt={&amp;quot;postImageUrl&amp;quot;}
                className=&amp;quot;post_image&amp;quot;
              /&amp;gt;
            &amp;lt;/div&amp;gt;
          ) : (
            &amp;lt;div className=&amp;quot;none_image&amp;quot; key={i.toString()}&amp;gt;&amp;lt;/div&amp;gt;
          )
        )}
      &amp;lt;/div&amp;gt;
    ))}
  &amp;lt;/div&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;들어온 postNum을 사용하여 해당 글로 스크롤 움직이기&lt;ul&gt;
&lt;li&gt;글들을 감싸는 글(post)의 부모 요소 ref로 접근&lt;/li&gt;
&lt;li&gt;부모 요소 ref의 children 접근하여 해당 순서의 글을 인덱싱&lt;/li&gt;
&lt;li&gt;인덱싱한 요소의 offsetTop 값을 가져옴&lt;/li&gt;
&lt;li&gt;offsetTop에서 navigatino height 만큼 뺀 값만큼 &lt;code&gt;window.scrollTo()&lt;/code&gt; 함수로 이동&lt;/li&gt;
&lt;li&gt;&lt;code&gt;window.scrollBy()&lt;/code&gt; 함수 이용시, transition을 줄 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const scrollToPost = (postNum) =&amp;gt; {
  if (!postNum) {
    return;
  }
  const targetRef = useRef();
  const top = targetRef.current.children[num].offsetTop;
  // window.scrollBy({
  //     top: top - 54,
  //     left: 0,
  //     behavior: &amp;#39;smooth&amp;#39;,
  // });
  window.scrollTo(0, top - 54);
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;다음에 필요한 사항&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profile 정보 요청 시기 조정&lt;ul&gt;
&lt;li&gt;더 빠른 연산을 위해서, 화면이 render 되고 profile에 관련된 정보를 가져오지 말고 profile 보기위해 버튼을 눌렀을 때 부터 미리 profile 정보를 요청하게 하자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Post Detail View로 이동시 해당 글의 scrollX 위치로 이동하게 구현하기 (스타일링 이후에 scroll 위치 계산이 필요한 작업임)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; validation 구현 필요함&lt;ul&gt;
&lt;li&gt;input 같은 경우, display none 적용시 browser에서 제공하는 validation 말풍선이 뜨지 않기 때문에 따로 구현 필요함&lt;/li&gt;
&lt;li&gt;required를 사용하지 말고, submit 함수 단에서 input값이 들어 왔는지 체크하여 validation error 구현 필요&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; post 관련한 input의 check 대략적인 (PostUpdateContainer, postFormContainer)&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; auth 관련한 input의 check 대략적인 조건 구현&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 input 별로 데이터 형태에 따른 구체적인 조건 설정이 필요함&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 이메일, 패스워드, 유저 네임, 글 내용의 형식(조건, 제한) 지정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 글 작성 시간 (클라이언트 단에서 뿌리는 경우 로컬 시간 변경으로 조작 가능한지 테스트 필요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; profileUpdateContainer과 postFormContainer 통합 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 효과적인 렌더링 제한을 위해서 container에 있는 함수들을 hook으로 만들어 구현하기&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;h1&gt;나중에 구현하고 싶은 기술&lt;/h1&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;input 자동 체크(중복 검사 기능에 추가)&lt;/li&gt;
&lt;li&gt;side 바에 유저 랜덤 추천 및 푸터 정보&lt;/li&gt;
&lt;li&gt;유저 이름 검색을 통한 프로필 보기 (이름 검색)&lt;/li&gt;
&lt;li&gt;무한 스크롤&lt;/li&gt;
&lt;li&gt;게시글 장소 태그로 장소 지도 보기 (지도 API)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Racstagram_V2</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/203</guid>
      <comments>https://goforit.tistory.com/203#entry203comment</comments>
      <pubDate>Fri, 13 Aug 2021 01:12:23 +0900</pubDate>
    </item>
    <item>
      <title>20210715 JavaSciprt DeepDive 13 : Set, Map, 브라우저 렌더링 과정</title>
      <link>https://goforit.tistory.com/202</link>
      <description>&lt;h1&gt;JavaScript Deep Dive 13&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;853&quot; data-filename=&quot;JS_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJdr2y/btrbqwh5wKz/PMM85nzTIE0dK26PQDnwE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJdr2y/btrbqwh5wKz/PMM85nzTIE0dK26PQDnwE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJdr2y/btrbqwh5wKz/PMM85nzTIE0dK26PQDnwE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJdr2y%2Fbtrbqwh5wKz%2FPMM85nzTIE0dK26PQDnwE0%2Fimg.png&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;853&quot; data-filename=&quot;JS_logo.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;  용어 및 중요사항 정리&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;Set&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h2&gt;Set&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;중복되지 않는 유일한 값들의 집합 객체&lt;/li&gt;
&lt;li&gt;수학적 집합을 구현하기 위한 자료구조 임&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JS의 모든 값을 요소로 지정할 수 있음&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;set 객체는 이터러블 임&lt;ul&gt;
&lt;li&gt;for...of, 스프레드 문법, 배열 디스트럭처링 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;배열 vs Set 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;배열&lt;/th&gt;
&lt;th&gt;Set 객체&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;동일한 값을 중복&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요소 순서 의미&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱스로 요소 접근&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Set 객체 생성&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new Set(이터러블)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set 생성자 함수로 생성&lt;/li&gt;
&lt;li&gt;이터러블을 인수로 전달받아 Set 객체 생성&lt;ul&gt;
&lt;li&gt;인수가 없으면, 빈 Set 객체 생성됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이터러블의 중복된 값은 Set 객체에 요소로 저장 되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;Set 프로퍼티, 메서드&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Set.prototype.size 프로퍼티&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;Set 객체의 요소 개수 확인&lt;/li&gt;
&lt;li&gt;setter 함수 없이, getter만 존재하는 접근자 프로퍼티&lt;ul&gt;
&lt;li&gt;size 프로퍼티에 값을 할당하여 변경 불가함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Set.prototype.add(요소)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;새로운 요소가 추가된 Set 객체 반환&lt;/li&gt;
&lt;li&gt;메서드 체이닝으로 연속해서 호출 가능함&lt;/li&gt;
&lt;li&gt;중복된 요소 추가의 경우에는 무시되고 에러는 발생하지 않음&lt;/li&gt;
&lt;li&gt;NaN의 중복도 평가 함&lt;ul&gt;
&lt;li&gt;일반 일치 비교 연산자에서는 NaN은 다르다고 평가함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const set = new Set();
set.add(1).add(&amp;quot;a&amp;quot;).add(null);
console.log(set); // Set(3) {1, &amp;#39;a&amp;#39;, null}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Set.prototype.has(요소값)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;특정 요소의 존재 여부를 나타내는 불리언 값을 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Set.prototype.delete(요소값)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;특정 요소를 삭제하고, 삭제 성공 여부를 나타내는 불리언 값 반환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;연속적으로 호출 불가함&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Set.prototype.clear()&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;모든 요소를 일괄 삭제하고 언제나 undefined를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Set.prototype.forEach(callback, this)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;callback(value, value, set)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;첫번째 인수, 두번째 인수: 순회중 인 요소로 같음(단지 같은 이유에 의미는 없음)&lt;/li&gt;
&lt;li&gt;세번째 인수 : Set 객체 자체&lt;/li&gt;
&lt;li&gt;set 객체는 인덱스가 없기 때문에, 두번째 인수에 인덱스는 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;순회 순서는 요소가 추가된 순서를 따름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;집합 연산 프로토타입 메서드 구현&lt;/h2&gt;
&lt;br/&gt;

&lt;h3&gt;교집합&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;집합 두개의 교집합을 집합으로 만듦&lt;ul&gt;
&lt;li&gt;for ... of로 구현한 교집합 메서드&lt;/li&gt;
&lt;li&gt;filter로 구현한 교집합 메서드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// for...of 을 통한 비교
Set.prototype.intersection = function (set) {
  // 교집합을 담을 Set
  const result = new Set();

  // 비교할 Set 객체를 순회하면서, 기존 Set에 값이 있는지 확인하고 교집합 Set에 넣음
  for (const value of set) {
    if (this.has(value)) result.add(value);
  }
  // 교집합 Set 반환
  return result;
};

// filter 을 통한 비교
Set.prototype.intersection = function (set) {
  return new Set([...this].filter((v) =&amp;gt; set.has(v)));
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;합집합&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;집합 두 개를 합침&lt;ul&gt;
&lt;li&gt;for...of 방식&lt;/li&gt;
&lt;li&gt;스프레드 문법 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// for ... of 방식
Set.prototype.union = function (set) {
  const result = new Set(this);

  for (const value of set) {
    result.add(value);
  }
  return result;
};

// 스프레드 문법 방식
Set.prototype.union = function (set) {
  return new Set([...this, ...set]);
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;차집합&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;A, B중 A에만 있는 요소들의 집합&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;Set.prototype.difference = function (set) {
  const result = new Set(this);

  for (const value of set) {
    result.delete(value);
  }
  return result;
};

Set.prototype.difference = function (set) {
  return new Set([...this].filter((v) =&amp;gt; !set.has(v)));
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;부분 집합과 상위 집합&lt;/h3&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;상위 집합과 부분 집합 관계 체크&lt;ul&gt;
&lt;li&gt;for...of 방식 비교&lt;/li&gt;
&lt;li&gt;every 방식 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// for...of 방식의 this 상위 집합 확인
Set.prototype.isSuperset = function (subset) {
  for (const value of subset) {
    if (!this.has(value)) return false;
  }
  return true;
};

// every 방식의 this 상위 집합 확인
Set.prototype.isSuperset = function (subset) {
  const supersetArr = [...this];
  return [...subset].every((v) =&amp;gt; supersetArr.includes(v));
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;Map&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;h2&gt;Map&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;키와 값의 쌍으로 이루어진 컬렉션(자료구조)&lt;/li&gt;
&lt;li&gt;객체와 유사하지만 다름&lt;ul&gt;
&lt;li&gt;객체는 문자열, 심벌만 키값으로 사용가능 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Map은 JS의 모든 값을 키로 활용 할수 있음&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Map 객체는 이터러블 임&lt;ul&gt;
&lt;li&gt;for...of, 스프레드 문법, 디스트럭처링 할당 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;순서에 의미는 없지만 순회하는 순서는 요소가 추가된 순서를 따름&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;Map vs Object&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;객체&lt;/th&gt;
&lt;th&gt;Map 객체&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;키로 사용할 수 있는 값&lt;/td&gt;
&lt;td&gt;문자열 또는 심벌 값&lt;/td&gt;
&lt;td&gt;객체를 포함한 모든 값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이터러블&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요소 개수 확인&lt;/td&gt;
&lt;td&gt;Object.keys(obj).length&lt;/td&gt;
&lt;td&gt;map.size&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br/&gt;

&lt;h2&gt;Map 객체 생성&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new Map(이터러블)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;Map 생성자 함수로 생성&lt;/li&gt;
&lt;li&gt;인수를 전달하지 않으면 빈 Map 객체 생성&lt;/li&gt;
&lt;li&gt;이터러블 -&amp;gt; key-value 쌍으로 이루어진 요소로 구성되어야 함&lt;ul&gt;
&lt;li&gt;이터러블에 중복된 키를 갖는 요소 존재시 값을 덮어 씀 (중복키 존재X)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const map = new Map([
  [&amp;quot;key1&amp;quot;, &amp;quot;value1&amp;quot;],
  [&amp;quot;key2&amp;quot;, &amp;quot;value2&amp;quot;],
  [&amp;quot;key2&amp;quot;, &amp;quot;value3&amp;quot;],
]);
console.log(map); // Map(2) {&amp;quot;key1&amp;quot; =&amp;gt; &amp;quot;value1&amp;quot;, &amp;quot;key2&amp;quot; =&amp;gt; &amp;quot;value3&amp;quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;Map 프로토타입 메서드, 프로퍼티&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.size 프로퍼티&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;Map 객체의 &lt;strong&gt;요소 개수 확인&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;setter는 없어서, 프로퍼티에 값 할당하여 변경 못함&lt;ul&gt;
&lt;li&gt;Set 과 마찬가지로 값 할당시 에러없이 무시됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.set(key, value)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;Map 객체에 &lt;strong&gt;요소를 추가&lt;/strong&gt;할 때 사용&lt;/li&gt;
&lt;li&gt;메서드 체이닝으로 연속 호출 가능&lt;/li&gt;
&lt;li&gt;중복 키를 갖는 요소 추가시 기존값 덮어 씀&lt;/li&gt;
&lt;li&gt;NaN도 중복으로 평가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.get(key)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;요소를 취득&lt;/strong&gt;할 때 사용&lt;/li&gt;
&lt;li&gt;인수로 전달한 key에 맞는 값(value)를 반환&lt;ul&gt;
&lt;li&gt;key가 존재하지 않는 경우 undefined 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.has(key)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;요소 존재 여부 확인&lt;/strong&gt;시 사용&lt;/li&gt;
&lt;li&gt;요소의 존재를 나타내는 불리언 값 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.delete(key)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;요소를 삭제&lt;/strong&gt;할 때 사용&lt;/li&gt;
&lt;li&gt;삭제 성공 여부에 따른 불리언 값을 반환&lt;/li&gt;
&lt;li&gt;존재하지 않는 키로 Map객체 요소 삭제시 에러없이 무시됨&lt;/li&gt;
&lt;li&gt;연속적으로 호출 불가함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.clear()&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;요소 일괄 삭제&lt;/strong&gt;시 사용&lt;/li&gt;
&lt;li&gt;언제나 undefined 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Map.prototype.forEach(Callback, this)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;callback(value, key, Map)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;요소 순회시 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;이터러블이면서 동시에 이터레이터인 객체를 반환하는 메서드&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.keys()&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;요소키를 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.values()&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;요소값을 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Map.prototype.entries()&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;요소키와 요소값을 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const park = { name: &amp;quot;Park&amp;quot; };
const back = { name: &amp;quot;Back&amp;quot; };

const map = new Map([
  [park, &amp;quot;developer&amp;quot;],
  [back, &amp;quot;designer&amp;quot;],
]);
console.log(map.keys());
console.log(map.values());
console.log(map.entries());&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;

&lt;blockquote&gt;
&lt;h1&gt;브라우저 렌더링 과정&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;파싱 (parsing)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;프로그래밍 언어의 문법에 맞게 작성된 텍스트 문서를 읽어 들여 실행하기 위한 분석 과정&lt;ul&gt;
&lt;li&gt;텍스트 문서의 문자열을 토큰으로 분해&lt;/li&gt;
&lt;li&gt;토큰에 문법적 의미와 구조를 반영하여 트리구조의 자료구조인 파스 트리를 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파싱 완료 -&amp;gt; 파스 트리 기반 중간 언어인 바이트코드 생성 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;렌더링 (rendering)&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;HTNL, CSS, JS로 작성된 문서를 파싱하여 브라우저에서 시각적으로 출력하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;브라우저 렌더링 과정 개요&lt;/h2&gt;
&lt;br/&gt;

&lt;ol&gt;
&lt;li&gt;브라우저가 HTML, CSS, JS, 이미지, 폰트 파일 등 렌더링에 필요한 요청 -&amp;gt; 서버로 응답 받음&lt;/li&gt;
&lt;li&gt;브라우저의 렌더링 엔진이 서버로 부터 응답된 HTML, CSS를 파싱 -&amp;gt; DOM, CSSOM을 생성 -&amp;gt; 결합하여 렌더 트리 생성&lt;/li&gt;
&lt;li&gt;브라우저의 JS 엔진이 서버로 부터 응답된 JS를 파싱 -&amp;gt; AST(Abstract Syntax Tree) 생성 -&amp;gt; 바이트코드로 변환 -&amp;gt; 실행 (JS가 DOM API로 DOM, CSSOM 변경) -&amp;gt; 변경된 DOM, CSSOM이 렌더 트리로 결합&lt;/li&gt;
&lt;li&gt;렌더 트리 기반으로 HTML 요소의 레이아웃(위치, 크기) 계산하여 브라우저 화면에 HTML 요소 페인팅&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;

&lt;h2&gt;요청과 응답&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;브라우저의 주소창 : 서버에 요청 전송용&lt;ul&gt;
&lt;li&gt;주소창에 URL을 입력 엔터 -&amp;gt; URL의 호스트 이름 -&amp;gt; (DNS가 IP주소로 변환함) -&amp;gt; IP주소를 갖는 서버에게 요청 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;루트 요청&lt;/code&gt; : 스킴(scheme)과 호스트(host)만으로 구성된 URI에 의한 요청이 서버로 전송&lt;ul&gt;
&lt;li&gt;요청 내용은 없지만 암묵적으로 서버는 응답으로 index.html을 보내도록 되어 있음&lt;/li&gt;
&lt;li&gt;index.html에 있는 link태그(CSS파일), img태그(이미지 파일), script태그(JS파일) 등을 만나면 HTML 파싱을 일시 중단하고 해당 리소스 파일을 서버로 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;정적 파일 요청&lt;/code&gt; : 다른 정적 파일 필요시 URI + Path를 기술하여 서버에 요청&lt;/li&gt;
&lt;li&gt;&lt;code&gt;동적 파일 요청&lt;/code&gt; : JS를 통해 동적으로 서버에 정적/동적 데이터 요청 가능&lt;/li&gt;
&lt;li&gt;요청과 응답은 개발자 도구 Network 패널에서 확인 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;HTTP 1.1과 HTTP 2.0&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HTTP&lt;/code&gt; : 웹에서 브라우저와 서버가 통신하기 위한 프로토콜(규약)&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HTTP/1.1&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;커넥션당 하나의 요청과 응답만 처리 (여러개 요청, 응답을 한번에 전송 불가)&lt;ul&gt;
&lt;li&gt;index.html 응답 후 CSS, 이미지, JS파일 차례대로 요청, 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리소스의 동시 전송이 불가능한 구조 -&amp;gt; 요청 개수에 비례한 응답 시간 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTTP/2.0&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;커넥션당 여러개의 요청과 응답 가능 (다중 요청, 응답이 가능)&lt;ul&gt;
&lt;li&gt;index.html 응답 후 CSS, 이미지, JS 파일 한번에 동시 요청, 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HTTP/1.1에 비해 50% 정도 빠름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;HTLM 파싱과 DOM 생성&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;DOM은 HTML 문서를 파싱한 결과물&lt;ol&gt;
&lt;li&gt;HTML 요청을 받은 서버가 해당 HTML 파일을 읽어 바이트(2진수)로 메모리에 저장하고, 해당 바이트를 Network로 전송하여 응답함&lt;/li&gt;
&lt;li&gt;브라우저는 서버가 응답한 HTML 문서를 바이트 형태로 응답 받음&lt;/li&gt;
&lt;li&gt;바이트 형태의 HTML 문서를 읽고, 응답 헤더에 있는 meta태그 charset 어트리뷰트 인코딩 방식으로 바이트를 문자열로 변환함&lt;/li&gt;
&lt;li&gt;문자열을 읽어 문법적 의미를 갖는 코드의 최소 단위인 토큰들로 분해&lt;/li&gt;
&lt;li&gt;각 토큰들을 객체로 변환하여 노드을 생성 (노드는 DOM을 구성하는 기본 요소)&lt;/li&gt;
&lt;li&gt;HTML 요소들의 중첩 관계를 반영하여 모든 노드들을 트리 자료구조(DOM)로 구성함&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;CSS 파싱과 CSSOM 생성&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;렌더링 엔진이 순차적으로 HTML을 파싱하면서 DOM 생성중 link, style 태그를 만나면 DOM 생성을 일시 중단함&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;link 태그의 href 어트리뷰트에 지정된 css 파일을 서버에 요청&lt;/li&gt;
&lt;li&gt;브라우저는 바이트로 받고 -&amp;gt; 문자열 -&amp;gt; 토큰 -&amp;gt; 노드 -&amp;gt; (상속 관계 반영) -&amp;gt; CSSOM 을 거쳐 CSSOM을 생성함&lt;/li&gt;
&lt;li&gt;HTML 파싱중 중단된 지점부터 다시 파싱하면서 DOM 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;렌더링 엔진&lt;/code&gt; : 서버로 부터 응답된 HTML, CSS를 파싱하여 DOM, CSSOM를 생성함&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DOM, CSSOM은 렌더 트리로 결합됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;렌더 트리&lt;/code&gt; : 렌더링을 위한 트리 구조의 자료 구조&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;비표시 노드들은 포함되지 않기 때문에 렌더링 되는 노드만으로 구성됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;레이아웃&lt;/code&gt; : 렌더 트리를 가지고 HTML 요소의 위치 크기를 계산하는 작업&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;페인팅&lt;/code&gt; : 계산된 레이아웃 대로 브라우저 화면에 픽셀을 렌더링 하는 작업&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;리렌더링&lt;/code&gt; : 레이아웃 계산과 페인팅을 다시 실행하는 것&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;리렌더링은 성능에 악영향을 줌, 리렌더링 제어가 필요함&lt;/li&gt;
&lt;li&gt;다시 실행 되는 경우&lt;ul&gt;
&lt;li&gt;자바스크립트에 의한 노드 추가 또는 삭제&lt;/li&gt;
&lt;li&gt;브라우저 창의 리사이징에 의한 뷰포트 크기 변경&lt;/li&gt;
&lt;li&gt;HTML 요소의 레이아웃에 변경을 발생시키는 width/height, margin, padding, border, display, position, top/right/bottom/left 등의 스타일 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;JS 파싱과 실행&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;JS 코드에서 DOM API 사용시 이미 생성된 DOM을 동적으로 조작 가능&lt;/li&gt;
&lt;li&gt;렌더링 엔진이 HTML 파싱하며 DOM 생성 중에 script 태그를 만나면 DOM 생성 일시 중단&lt;/li&gt;
&lt;li&gt;script src 어트리뷰트에 정의된 자바스크립트 파일을 서버에 요청&lt;ul&gt;
&lt;li&gt;자바스크립트 엔진이 문자열인 JS 소스코드 -(토크나이징)-&amp;gt; 토큰 -(파싱)&amp;gt; AST -(바이트 코드 생성)-&amp;gt; 바이트 코드 -&amp;gt; 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;토크나이징(tokenizing)&lt;/code&gt; : 단순한 문자열인 자바스크립트 소스 코드를 어위 분석하여 문법적 의미의 토큰들로 분해&lt;ul&gt;
&lt;li&gt;문자열 JS 코드 -&amp;gt; 토큰&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;파싱&lt;/code&gt; : 토큰들의 집합을 구문 분석하여 AST를 생성&lt;ul&gt;
&lt;li&gt;토큰 -&amp;gt; AST&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AST&lt;/code&gt; : 인터프리터나 컴파일러가 사용 또는 TypeScript, Babel, Prettier 같은 트랜스파일러를 구현할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;바이트 코드 생성&lt;/code&gt; :&lt;ul&gt;
&lt;li&gt;AST는 인터프리터가 실행할수 있는 중간 코드 바이트코드로 변환되고 인터프리터로 실행됨&lt;ul&gt;
&lt;li&gt;AST -&amp;gt; 바이트 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;리플로우, 리페인트&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;JS 코드에서 DOM API로 인해 DOM, CSSOM이 변경 -&amp;gt; 렌더 트리 결합 -&amp;gt; 레이아웃 -&amp;gt; 페인트&lt;ul&gt;
&lt;li&gt;&lt;code&gt;리플로우&lt;/code&gt; : 레이아웃 계산을 다시 하는 것을 말함&lt;ul&gt;
&lt;li&gt;노드추가/삭제, 요소크기/위치 변경, 윈도우 리사이징 등의 변경 -&amp;gt; 리플로우 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;리페인트&lt;/code&gt; : 재결합된 렌더 트리를 기반으로 다시 페이트 하는 것&lt;/li&gt;
&lt;li&gt;리플로우, 리페인트 순차적 동시 실행이 아니라, 레이아웃에 영향 없으면 리페인트만 실행됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;직렬적, 병렬적 파싱&lt;/h2&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;직렬적 파싱&lt;/code&gt; : 렌더링엔진, JS엔진이 직렬적(동기적)으로 위에서 아래로 파싱하는 것&lt;ul&gt;
&lt;li&gt;script 태그의 위치를 주의 해야함 -&amp;gt; DOM API를 사용하는데, DOM의 생성이 없으면 문제 발생함&lt;/li&gt;
&lt;li&gt;블록킹 : HTML 파싱중 중단 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;블록킹 해결 방식&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;방법01 : &lt;strong&gt;body 요소의 가장 아래에 script 태그를 위치&lt;/strong&gt;시키는 방법&lt;ul&gt;
&lt;li&gt;DOM 조작시 에러 방지, HTML 먼저 파싱 실행되어 페이지 로딩시간 단축&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;방법02 : async, defer 어트리뷰트 사용 (인라인 JS는 불가, 외부 파일 scr로 연결시만 가능) -&amp;gt; HTNL, JS파일 로드가 비동기적으로 병렬 진행&lt;ul&gt;
&lt;li&gt;&lt;code&gt;async 어트리뷰트&lt;/code&gt; : HTML 파싱과 JS파일 로드를 병렬적(비동기적)으로 진행&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JS 로드 완료시 JS 파일 실행&lt;/strong&gt; -&amp;gt; 로드만 되면 순서 없이 실행 됨 &lt;strong&gt;(순서 보장X)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defer 어트리뷰트&lt;/code&gt; : HTML 파싱과 JS파일 로드를 병렬적(비동기적)으로 진행&lt;ul&gt;
&lt;li&gt;HTML 파싱 완료된 직후 JS 파일 파싱과 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/JavaScript</category>
      <author>라쿤</author>
      <guid isPermaLink="true">https://goforit.tistory.com/202</guid>
      <comments>https://goforit.tistory.com/202#entry202comment</comments>
      <pubDate>Sun, 8 Aug 2021 23:19:07 +0900</pubDate>
    </item>
  </channel>
</rss>