<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>간단한 블로그</title>
    <link>https://partnerjun.tistory.com/</link>
    <description>웹 개발자의 짧은 글</description>
    <language>ko</language>
    <pubDate>Tue, 28 Jul 2026 02:41:25 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>partner_jun</managingEditor>
    <item>
      <title>2025년을 보내며</title>
      <link>https://partnerjun.tistory.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;25년은 굉장히 밀도 있는 해였다. 먼저 회사 업무 측면에서 보면 작년 말부터 이어진 회원 통합 프로젝트에 이어, 숙소 상세 사이트 개편과 공통 미들웨어 라이브러리 개발이 함께 진행되었고, 이어서 전배와 동시에 신규 프로젝트들을 진행하고 있다.&lt;br&gt;회사 업무 외에도 꽤나 도전적이었다. 번지점프를 하러 마카오 여행을 다녀왔고 일본 여행도 3번이나 다녀왔지만, 대학원 진학을 도전한 것이 가장 큰 변화를 만들고 있다. 새로 진행하는 개인 프로젝트도 꽤 재미있는 작업이었는데, 대학원 진학을 위해 시작했지만 정작 나 자신은 사용하지 않게 된 것이 아이러니하다. 이런 일들에 대해 정리할 겸 적어본다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 회사 업무&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;회원 통합 프로젝트&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://partnerjun.tistory.com/113&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;이전 글에 적은 것&lt;/span&gt;&lt;/a&gt;과 같이, 24년 말부터 25년 초까지 야/인/트 3개 서비스를 통합하는 회원 사이트를 개발했다. 초기 손발이 맞지 않아 개발에 지연이 발생했고, 프로젝트 자체의 난이도가 어렵다보니 업무 배분에도 이런저런 문제가 있었다. 무엇보다 일반 로그인에 더해 소셜 로그인의 도메인 지식이 필요했던, 꽤나 어려운 프로젝트였다. 특히 소셜 로그인은 지금 생각하면 아쉬운 부분이 많다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5t10z/dJMcagKWWVW/J5urm00Jt3WRfrgOsle0f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5t10z/dJMcagKWWVW/J5urm00Jt3WRfrgOsle0f1/img.png&quot; data-alt=&quot;대규모 회원 프로젝트를 진행해본 경험은 귀하다. 특히 FE라면.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5t10z/dJMcagKWWVW/J5urm00Jt3WRfrgOsle0f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5t10z%2FdJMcagKWWVW%2FJ5urm00Jt3WRfrgOsle0f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;380&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대규모 회원 프로젝트를 진행해본 경험은 귀하다. 특히 FE라면.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금 생각해보면 재미있던 점도 아쉬운 점도 가득한 프로젝트였다. 이 프로젝트를 통해 다양한 것을 배울 수 있었다. 특히 개발과 관련된 것 외에도 다른 사람을 이끌어 줄 수 있는 사람이 되기 위해 부족했던 점들을 배울 수 있었다. 이 경험은 개발자로서도 사람으로서도 큰 도움이 되리라 믿는다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;숙소 상세 사이트 개편&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;재직 중인 회사는 PC 웹에 약하다. 한동안 모바일 중심으로만 운영해왔기 때문에 PC 유저에겐 억지로 늘린 화면을 제공했다. 그러다 보니 너무 어색하고 완성도가 떨어져 보인다는 의견이 많았고, 드디어 PC 화면을 지원하고, 공통 디자인 요소를 기반으로 하는 프로젝트를 진행했다. 한동안 숙소 상세 사이트를 해왔지만, 다른 작업을 진행하고 있어 내가 오너십을 가지지 않고, 화면 구성에 필요한 컴포넌트 몇 가지를 개발하는 정도로 진행했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1617&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfmgXK/dJMcagEcaiR/Ev4L8ncMvEzybU4q7Yszz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfmgXK/dJMcagEcaiR/Ev4L8ncMvEzybU4q7Yszz0/img.png&quot; data-alt=&quot;그래도 하나씩 짚어보면 그래도 꽤 많이 만들긴 했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfmgXK/dJMcagEcaiR/Ev4L8ncMvEzybU4q7Yszz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfmgXK%2FdJMcagEcaiR%2FEv4L8ncMvEzybU4q7Yszz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;640&quot; height=&quot;351&quot; data-origin-width=&quot;1617&quot; data-origin-height=&quot;886&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그래도 하나씩 짚어보면 그래도 꽤 많이 만들긴 했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;결과적으로 깔끔하게 진행되지 못한 아쉬운 프로젝트였다. 문제가 되었던 것은 대부분 QA 단계에서 누락된 요소들이다. 간단하지만 어처구니 없던 대실 예약 문제가 기억에 남는다. 대실은 숙박이 아니다보니, 더 짧은 시간&lt;i&gt;(4시간 내외)&lt;/i&gt;만 제공하는 형태다. 주문 페이지로 이동하기 위해 대실 시간을 전달하는데, 하루가 더해져 전달되었음에도 그 여러 과정 중 어디에서도 검증되지 않고 있었다. 물론 그렇게 전달한 것 자체도 큰 실수지만 주문 페이지에 &lt;u&gt;다음 날까지로&lt;/u&gt; 노출되는 것을 아무도 발견하지 못했고, 어느 API에서도 오류가 발생하지 않았다는 점이 경악스럽다. 다행히 하루 이상 대실할 수는 없으니 DB 레벨에서 수정할 수 있었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHoKfE/dJMcahpy0PM/CM3Yw8rEVLtJWj2v7kEjkK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHoKfE/dJMcahpy0PM/CM3Yw8rEVLtJWj2v7kEjkK/img.jpg&quot; data-alt=&quot;문제가 하나뿐이었으면 좋았겠지만...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHoKfE/dJMcahpy0PM/CM3Yw8rEVLtJWj2v7kEjkK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHoKfE%2FdJMcahpy0PM%2FCM3Yw8rEVLtJWj2v7kEjkK%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;140&quot; height=&quot;118&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;문제가 하나뿐이었으면 좋았겠지만...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;두번째 문제는 심각했다. 야간 시간대의 예약 문제였다. 모텔은 그 특성상 0시부터 2시는 전날로 판단하는 비즈니스 로직이 있다. 개발 단계에서 이 비즈니스 로직을 테스트하기 위해 야간 시간대로 판단하는 테스트용 플래그를 만들어 사용한다. 하지만 단순 노출 뿐 아니라 예약이나 관리자와 연관된 기능이 많아, 실제로 야간에 테스트 해보지 않으면 작동을 장담할 수 없다. 심지어 이 시간대가 가장 중요한&lt;i&gt;(강성 CS도 잦은)&lt;/i&gt; 피크 타임이다. 바로 이 부분에서 문제가 발생했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cc73ur/dJMcaiBWnp0/iXr7ELp3VWLoNTZ6jKKFx1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cc73ur/dJMcaiBWnp0/iXr7ELp3VWLoNTZ6jKKFx1/img.jpg&quot; data-alt=&quot;나도 이전 개편 작업엔 새벽에 개발했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cc73ur/dJMcaiBWnp0/iXr7ELp3VWLoNTZ6jKKFx1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcc73ur%2FdJMcaiBWnp0%2FiXr7ELp3VWLoNTZ6jKKFx1%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;500&quot; height=&quot;360&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;나도 이전 개편 작업엔 새벽에 개발했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전에는 이 위험한 부분을 테스트하기 위해 신규 기능이 추가되면 새벽에 테스트하는 것을 최종 단계로 하곤 했다. 하지만 이번에는 그렇게 하지 않았던 것 같다. 꼼꼼한 테스트 없이 라이브 배포까지 이루어졌고, 기기에 따라서는 예약 자체가 불가능해지는 문제까지 발생했다. 난 이 즈음 팀을 이동해 자세한 상황에 대해 알지 못했지만, 좋지 않은 일들이 있었다고 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qkoCC/dJMcafrJRqB/UtYkJSvRs1AwEXUG5HAXaK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qkoCC/dJMcafrJRqB/UtYkJSvRs1AwEXUG5HAXaK/img.jpg&quot; data-alt=&quot;난 이 즈음 조직을 이동했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qkoCC/dJMcafrJRqB/UtYkJSvRs1AwEXUG5HAXaK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqkoCC%2FdJMcafrJRqB%2FUtYkJSvRs1AwEXUG5HAXaK%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;140&quot; height=&quot;109&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;난 이 즈음 조직을 이동했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;QA팀도 대규모 조직 개편으로 혼란스러웠던 것을 이해한다. 하지만 발생한 문제들은 실무 담당자들에게 굉장히 큰 스트레스로 돌아오게 된다. 스트레스 내성이 어지간히 높은 사람이 아니라면 정말 힘든 일이 되었을 것이다. 실제로 이 프로젝트 도중에 입사한 PM은 프로젝트가 끝나자마자 곧바로 퇴사했다. 몇 년 전 내가 개편을 진행했을 때에도 아주 똑같은 일이 있었기에 웃을 수 밖에 없었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdLdYv/dJMcahb0rbB/cevxLL2fZKPkJNHDC9CsR0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdLdYv/dJMcahb0rbB/cevxLL2fZKPkJNHDC9CsR0/img.jpg&quot; data-alt=&quot;이게 야생의 맛이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdLdYv/dJMcahb0rbB/cevxLL2fZKPkJNHDC9CsR0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdLdYv%2FdJMcahb0rbB%2FcevxLL2fZKPkJNHDC9CsR0%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;140&quot; height=&quot;140&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이게 야생의 맛이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;공용 미들웨어 프로젝트&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;회원 통합 프로젝트로 로그인은 단순 세션에서 JWT 기반의 토큰으로 전환되었다. 자연스럽게 서비스를 구성하는 사이트들은 토큰의 유효성을 검사하고 갱신하는 로직이 필요해졌다. 하지만 이 로직을 각각의 사이트가 모두 직접 구현하고 유지보수 하기에는 어려움이 있다. 명확한 담당자가 없으면 작업이 누락될 확률이 높고, 발생한 문제가 세션과 관련된 것이라면 전면 장애까지 이어질 수 있기 때문이다. 이 상황에서 몇 년 전 만든 게이트웨이 어플리케이션이 다시 주목받기 시작했다. 게이트웨이 어플리케이션의 목표는 사용자가 모두 같은 도메인으로 접근하고, 루트 도메인에서 하위 path에 따라 다른 웹 어플리케이션으로 프록시한다. &lt;span style=&quot;color: #333333;&quot;&gt;사용자들이 같은 도메인으로 세부 어플리케이션에 접근하게 되면 브라우저 입장에서는 같은 도메인의 사이트로 판단하므로 권한이나 스토리지 등 다양한 부분에서 유리해진다. &lt;/span&gt;특히 공통으로 사용되는 헤더나 쿠키 처리 로직이 있어 이곳에 토큰 로직을 담는다면 쉽게 해결될 법 했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H8T20/dJMcab3WawK/08kU9jVrtL8mI3s2M0KSj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H8T20/dJMcab3WawK/08kU9jVrtL8mI3s2M0KSj1/img.png&quot; data-alt=&quot;이런 식으로 서비스를 구성하는 어플리케이션으로 프록시되는 어플리케이션이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H8T20/dJMcab3WawK/08kU9jVrtL8mI3s2M0KSj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH8T20%2FdJMcab3WawK%2F08kU9jVrtL8mI3s2M0KSj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;580&quot; height=&quot;239&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이런 식으로 서비스를 구성하는 어플리케이션으로 프록시되는 어플리케이션이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;하지만 이 어플리케이션을 확장해 사용할 수는 없었다. 구성한 방식이나 아이디어는 나쁘지 않았지만, 선택한 언어와 프레임워크가 문제였다. Node Express.js로 구현했기 때문이다. 그럭저럭 잘 운영 되는가 싶었지만, 결국 대규모 트래픽에 따른 성능 측면에서 결점을 드러냈다. Node의 이벤트 루프 방식은 작은 규모에서 아주 강력하지만, 대규모 인입에 대해서는 성능적 한계를 가진다. 수많은 사용자의 요청을 받아내야 하는데, 프록시되는 서비스 어플리케이션에서 지연이 발생하면 이벤트 루프에 프록시 처리를 위해 만들었던 콜백이 쌓이게 되어 점점 더 느려지기 시작한다. 더군다나 위 그림처럼 헤더나 쿠키를 설정하기 위한 로직이 포함되어 있다 보니, 많은 연산이 필요해 그 문제는 더욱 심각하다(요청과 관련된 문제라 work thread를 사용하기도 난감하다). &lt;span style=&quot;color: #333333;&quot;&gt;거기에 웹 특성상 응답이 지연되면 일정 임계점을 넘는 순간부터 사용자의 새로고침 폭격이 시작된다. 이 폭격을 맞는 순간부터는 정상적인 운영이 완전히 불가능해진다. &lt;/span&gt;이런 고질적인 문제를 가지고 있어 숙소 도메인보다 더 큰 트래픽이 몰릴 티켓 도메인은 합류가 불가능했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lw46Q/dJMcaaRuQH0/FLTxmd13bBmsNkIS6jHmzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lw46Q/dJMcaaRuQH0/FLTxmd13bBmsNkIS6jHmzK/img.jpg&quot; data-alt=&quot;Node는 안된다고 했잖아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lw46Q/dJMcaaRuQH0/FLTxmd13bBmsNkIS6jHmzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLw46Q%2FdJMcaaRuQH0%2FFLTxmd13bBmsNkIS6jHmzK%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;140&quot; height=&quot;136&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Node는 안된다고 했잖아&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;어찌되었건, 한계에 봉착해 변화를 주어야 하는 이 게이트웨이 어플리케이션에서 토큰 갱신을 처리하는 것은 불가능하다고 판단했다. 당장 이 공통 요소가 필요한 티켓 도메인은 게이트웨이 어플리케이션을 거치지 않기 때문이다. 대안으로 &lt;a href=&quot;https://learn.microsoft.com/ko-kr/azure/architecture/patterns/sidecar&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;사이드카 스타일의 아키텍처&lt;/span&gt;&lt;/a&gt;를 선택했고, 가장 간단하고 강력한 방법으로 먼저&amp;nbsp;&lt;a href=&quot;https://nextjs-ko.org/docs/app/building-your-application/routing/middleware&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Next.js 미들웨어&lt;/span&gt;&lt;/a&gt;에서 사용할 수 있는 라이브러리를 만들기로 했다. 토큰 갱신과 더불어 기존 게이트웨이 어플리케이션에서 수행하던 실험군 선정이나 UA 파싱, 유저 핑거프린트 생성 등의 기능도 포함하기로 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이렇게 거창하게 라이브러리화 할 것 없이 그냥 API를 호출하면 토큰이 갱신되는 것 아닐까 생각할 수 있다. 하지만 생각보다 오래된 서비스이다 보니 그리 호락호락한 문제가 아니었다. 여러 웹 어플리케이션에서 공통된 세션을 사용하기 위해 Shared Redis Session을 사용하는데, 이 곳에 토큰을 저장해 두다 보니 각 어플리케이션이 이 세션에 연결될 수 있어야 했다. 여기서 문제가 발생한다. Next.js의 미들웨어는 기본적으로 Redis Client를 사용할 수 없는 &lt;a href=&quot;https://nextjs-ko.org/docs/pages/building-your-application/rendering/edge-and-nodejs-runtimes&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Edge Runtime&lt;/span&gt;&lt;/a&gt;이라는 것이다. Next.js 15.5부터 다시 Node Runtime을 선택할 수 있게 되었지만, 이미 개발이 완료되어 서비스중인 어플리케이션을 위해서 Edge/Node Runtime 양 쪽 환경을 모두 지원해야 했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;228&quot; data-origin-height=&quot;221&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XyKi5/dJMcagEb8Y5/YstnkkDPo6ukPdwaNkFRdk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XyKi5/dJMcagEb8Y5/YstnkkDPo6ukPdwaNkFRdk/img.jpg&quot; data-alt=&quot;Vercel 안쓴다고 안쓸거라고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XyKi5/dJMcagEb8Y5/YstnkkDPo6ukPdwaNkFRdk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXyKi5%2FdJMcagEb8Y5%2FYstnkkDPo6ukPdwaNkFRdk%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;140&quot; height=&quot;136&quot; data-origin-width=&quot;228&quot; data-origin-height=&quot;221&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Vercel 안쓴다고 안쓸거라고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Edge Runtime은 특성상 Redis 커넥션을 직접 만들 수 없다. Redis와 직접 통신이 불가능하니, 미들웨어에서 직접 Redis Session에 연결할 수 없게 된다. Page Router라면 페이지 레벨(getServerSideProps)에서 고차 함수를 사용한다던지 하는 방법으로 해소가 가능하지만, App Router라면 쉽지 않다. 공통된 방법으로 요청을 처리하는 방법이 미들웨어 뿐이기 때문이다&lt;i&gt;(요청마다 처리한다는 끔찍한 아이디어는 꺼내면 안된다)&lt;/i&gt;.&lt;br&gt;&amp;nbsp;&lt;br&gt;이 문제를 해결하기 위해, Edge Runtime에서 사용할 Internal API endpoint를 만들기로 했다. 어플리케이션의 API에서 Redis에 연결하고, Edge Runtime의 미들웨어에서는 이 API에 요청을 보내도록 하는 것이다. 그리 깔끔한 방법이 아닌 것처럼 보이지만, 이런 식으로 처리되는 라이브러리를 서비스를 하고 있는 곳도 있어 의외로 괜찮은 방법이라고 생각된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmvQZT/dJMcaaKJeoZ/DotU685HQKSiIm0l6sTfK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmvQZT/dJMcaaKJeoZ/DotU685HQKSiIm0l6sTfK0/img.png&quot; data-alt=&quot;미들웨어에서 세션이 필요하면 어플리케이션의 API를 호출한다 (일종의 재귀 호출처럼 보인다)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmvQZT/dJMcaaKJeoZ/DotU685HQKSiIm0l6sTfK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmvQZT%2FdJMcaaKJeoZ%2FDotU685HQKSiIm0l6sTfK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;550&quot; height=&quot;259&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;259&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;미들웨어에서 세션이 필요하면 어플리케이션의 API를 호출한다 (일종의 재귀 호출처럼 보인다)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이렇게 Redis를 조회할 수 있는 내부 API를 제공하여 Redis에 연결할 수 있게 되었다. 하지만 또 다른 문제가 있었다. 이 회사의 어플리케이션은 오래 전부터 &lt;a href=&quot;https://github.com/expressjs/session#readme&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Express-session&lt;/span&gt;&lt;/a&gt;를 이용해 세션을 연결하고 있다. 이 라이브러리는 전통적인 세션 구현을 위한 미들웨어 라이브러리로, 세션 키를 쿠키로 설정해주는 역할도 겸한다. 이 라이브러리를 그대로 사용하면 좋았겠지만, 플러그인으로 &lt;a href=&quot;https://github.com/tj/connect-redis#readme&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;connect-redis&lt;/span&gt;&lt;/a&gt;를 사용해 Shared Redis Session을 사용하고 있는 것이 문제다. 앞서 이야기한 것처럼, Edge Runtime에서는 Redis에 직접 커넥션을 맺을 수 없으니 이 라이브러리를 사용할 수 없었다. 심지어 운영 중인 서비스와 세션을 유지할 수 있어야 하니 이 라이브러리를 분석해 동일하게 동작하도록 만들어야만 했다. 많은 시도 끝에 암호화되는 쿠키 키를 다룰 수 있게 되었고, Redis에 저장되는 세션도 동일하게 만들어냈다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x9PYS/dJMcadN9YpE/su6URYakKCzP8W8Axq7x6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x9PYS/dJMcadN9YpE/su6URYakKCzP8W8Axq7x6K/img.png&quot; data-alt=&quot;이런 이상한 함수들을 만들기 시작했다. 하면 안되는걸 알지만 방법이 없다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x9PYS/dJMcadN9YpE/su6URYakKCzP8W8Axq7x6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx9PYS%2FdJMcadN9YpE%2Fsu6URYakKCzP8W8Axq7x6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;339&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이런 이상한 함수들을 만들기 시작했다. 하면 안되는걸 알지만 방법이 없다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이 외에도 꽤나 많은 기능을 추가하며 공통 미들웨어 패키지를 확장할 수 있도록 구조를 설계했는데, 위에서 설명한 세션같은 것들 때문에 난이도가 너무 높아져 버렸다. 결국 수정할 수 있는 인원이 한정적인 상황이 되어 버렸다. 이전 &lt;a href=&quot;https://partnerjun.tistory.com/107&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;protobuf 로그 생성기&lt;/span&gt;&lt;/a&gt;도 그렇고, 묘하게 나만 유지보수할 수 있는 썩은 프로젝트가 생겨나고 있다. 좋지 않은 상황이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;팀 전환배치&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;사실 회원 통합 프로젝트를 진행할 때부터, 이번에 CTO가 되신 분이 새로운 팀을 만들려 한다는 이야기를 들었다. 굳이 내가 발벗고 나서 이동할 이유도 없고 의욕도 딱히 없었기에 관심을 가지지 않았었다. 하지만 여러가지 정치적(?) 이슈와 심경의 변화로 새로운 팀에 합류하게 되었다. 심지어 팀장을 맡게 될 예정이다. 이전에 &lt;a href=&quot;https://partnerjun.tistory.com/110&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;팀장이 되면 어떻게 할 지에 대해 생각해 보고&lt;/span&gt;&lt;/a&gt; 글을 적었는데 이렇게나 빨리 실전 경험을 하게 될 줄은 몰랐다. 자신은 없지만 해야 할 일은 알고 있으니 그에 맞춰 잘 해보려 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddiygJ/dJMcafFgO6O/ZAzXKYxbzKQ7t6q7dkJslk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddiygJ/dJMcafFgO6O/ZAzXKYxbzKQ7t6q7dkJslk/img.jpg&quot; data-alt=&quot;할 일이 늘었다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddiygJ/dJMcafFgO6O/ZAzXKYxbzKQ7t6q7dkJslk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddiygJ%2FdJMcafFgO6O%2FZAzXKYxbzKQ7t6q7dkJslk%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;140&quot; height=&quot;136&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;할 일이 늘었다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;함께 넘어온 팀원들은 이미 너무나도 잘 하고 계시는 분들이기에, 내가 방해되지 않는다면 어려운 일은 없으리라 생각한다. 특히 CTO가 되신 리더분이 원하는 것은 빠르고 주도적인 개발이다. 나 역시도 그런 스타일로 개발하기를 원한다는 점에서 방향성이 같다. 그래서 나는 훌륭한 플레이어이기에 앞서 길에 있는 방해물을 치워주는 역할을 해 볼 생각이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;CID(AI 채팅 탐색) 프로젝트&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;유행하는(?) AI 채팅 프로젝트를 개발해 런칭했다. 현재로썬 사용률이 매우 낮지만 새로 결성된 팀이 빠르게 개발하고 런칭했다는 점에서 의미가 있었던 프로젝트다. 1개월도 안되는 매우 짧은 개발 기간을 가졌지만 완성도 있는 결과물을 내보여 팀의 역량을 보여줄 수 있었다. 앞으로 다양한 응답 모델과 형식을 추가해 사용성을 강화한다고 하는데, AI 관련 사이트를 꽤나 개발해본 입장에서 사용자의 입력을 받는 대화형 서비스는 히트하기 기대하기 어렵다고 본다. 사실 팀을 위한 성과로 생각하고 있었기에 별 애정이 없는 프로젝트다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daBE67/dJMcaiva8m0/rrtgxhtnp3aofNiBtfbDb1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daBE67/dJMcaiva8m0/rrtgxhtnp3aofNiBtfbDb1/img.jpg&quot; data-alt=&quot;냉정할 필요도 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daBE67/dJMcaiva8m0/rrtgxhtnp3aofNiBtfbDb1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaBE67%2FdJMcaiva8m0%2Frrtgxhtnp3aofNiBtfbDb1%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;300&quot; height=&quot;214&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;냉정할 필요도 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색 사이트 이관과 Golang을 이용한 BFF 개발&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이 프로젝트가 정말 큰 Breaking point가 될 프로젝트라고 판단하고 있다. 다른 팀에서 담당하던 BFF API 서버와 그 서버를 이용하는 검색 및 탐색 페이지를 이관하는 프로젝트다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두가지 측면에서 큰 변화를 줄 것이라고 생각하는데, 첫번째는 팀의 정체성을 보여주는 것에 있다. 팀이 만들어지긴 했지만 아직 담당하는 부분이 명확하지 않다. 앞서 CID 프로젝트가 팀의 역량을 보여주었다고 적었지만, 팀의 필요성에 대해서는 어필이 부족한 상황이다. 하지만 이 프로젝트가 진행되면 &lt;span style=&quot;color: #333333;&quot;&gt;팀이 맡는 주요 서비스가 있기에 업무를 명확하게 이야기할 수 있게 된다&lt;/span&gt;.&lt;br&gt;두번째는 기술적 챌린지다. 사내의 다른 FE 팀과 달리 코어 백엔드 서비스와 맞닿아 연결되는 BFF 서버를 직접 맡게 된다. 이건 팀 구조와도 연관이 있는데, 완전한 기능 조직이었던 이전 팀과 다르게 새로운 팀은 목적 조직 스타일로 구성되어 있기 때문에 가능한 부분이다. 특히 팀원 각각의 역량이 뛰어나기 때문에 가능하다. FE는 걱정할 필요가 없을 정도로 역량을 가지고 있으니 그 원천 데이터에 관심을 가질 수 있게 된다. 이렇게 FE팀이 BFF 서버를 직접 개발하면 FE 코드와 일관성을 가질 수 있고&lt;i&gt;(특히 enum류가 많다)&lt;/i&gt;, 외부에서도 작업 요청을 위한 과정이 줄어든다는 점이 큰 변화를 줄 것이다. 특히 그 BFF 서버를 &lt;a href=&quot;https://go.dev/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Go로 만든&lt;/span&gt;&lt;/a&gt;다는 것이 엄청난 차이점이고 도전이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 Golang을 선택했나&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다른 팀에서 관리하던 BFF 서버는 &lt;a href=&quot;https://nestjs.com/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Nest.js&lt;/span&gt;&lt;/a&gt; + &lt;a href=&quot;https://gcanti.github.io/fp-ts/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;fp-ts&lt;/span&gt;&lt;/a&gt;로 만든 Node 어플리케이션였다. 앞서 게이트웨이 어플리케이션에서도 언급했지만, Node 자체가 가지는 한계가 있다. Node API 개발자라면 반발할 수 있겠다. 이전 BFF 서버를 담당하던 팀은 Node API 개발자로 이루어진 팀으로 다양한 방법으로 성능 개선에 도전하고 있다. 하지만 명쾌한 해답을 내놓지 못했고, 인스턴스 수로 트래픽을 받아내고 있는 실정이다. 엄청난&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 노하우를 가진 개발자라면 그 한계를 넘어설 수도 있을지 모른다. 하지만 그런 개발자가 없으니 현실적으로 불가능하다.&amp;nbsp;&lt;/span&gt;또, 그렇게 튜닝에 성공한다고 해도 그 상태 그대로 유지보수가 가능한지는 또 다른 의문이다. &lt;span style=&quot;color: #333333;&quot;&gt;이런 성능적 한계가 새로운 방식을 택해야 했던 첫번째 이유다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEi8cA/dJMcagKXcMM/jbQtLRL8kFbUu0NAYb4zVK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEi8cA/dJMcagKXcMM/jbQtLRL8kFbUu0NAYb4zVK/img.jpg&quot; data-alt=&quot;입사했을 때 인스턴스 수를 보고 깜짝 놀랐다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEi8cA/dJMcagKXcMM/jbQtLRL8kFbUu0NAYb4zVK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEi8cA%2FdJMcagKXcMM%2FjbQtLRL8kFbUu0NAYb4zVK%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;140&quot; height=&quot;136&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;입사했을 때 인스턴스 수를 보고 깜짝 놀랐다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;두번째는 Protobuf 편의성이다. &lt;/span&gt;이전에 로그 전송 함수와 관련해서 사내의 정의&lt;i&gt;(enum같은 것들)&lt;/i&gt;를 &lt;a href=&quot;https://protobuf.dev/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Protocol Buffer(protobuf)&lt;/span&gt;&lt;/a&gt;로 전환하고 있다는 글을 썼다. 야/인/트 서비스의 병합과 동시에 파편화 되어 있는 정의를 통일해야 한다. 이미 사용하고 있고, 어느정도 성공적인 결과를 만든 Protobuf가 있으니 공통적인 Protobuf를 정의하고 사용하리라는 것을 충분히 예상할 수 있다. 거기에 더해 Protobuf로 모두 정의되면 자연스럽게 &lt;a href=&quot;https://grpc.io/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;gRPC&lt;/span&gt;&lt;/a&gt;를 사용할 가능성도 높다고 판단했다&lt;i&gt;(벌써 현실이 되어가고 있다)&lt;/i&gt;. 결과적으로 Protobuf, gRPC와 호환성이 좋은 방법을 찾아야 했다. &lt;br&gt;&amp;nbsp;&lt;br&gt;이 두가지 상황에 대해 팀원들에게 공유하고 후보를 추렸다. 내가 생각한 후보는 두가지였다. 첫번째는 JVM 언어(주로 Java Spring), 두번째는 Go였다. 그리고 일정이 틀어졌을 때 선택할 마지막 카드로 Node 유지가 있었다. 재미있는 것은 Spring 이야기를 했을 때 모두가 크게 반발했다는 것이다. 진입 장벽은 있으면서, Protobuf를 고려했을 때 큰 장점을 얻을 수 없다는 것이다. 개인적으로는 여차할 때 다른 팀에서 도움을 줄 수 있다는 점도 좋은 장점이라고 생각했는데, 결사 반대하는 모습에 포기했다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw4mGF/dJMcagjR9JO/R6kWkGkQxrNB3lJY1XwgmK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw4mGF/dJMcagjR9JO/R6kWkGkQxrNB3lJY1XwgmK/img.jpg&quot; data-alt=&quot;뭔가 트라우마가 있어 보였다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw4mGF/dJMcagjR9JO/R6kWkGkQxrNB3lJY1XwgmK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw4mGF%2FdJMcagjR9JO%2FR6kWkGkQxrNB3lJY1XwgmK%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;140&quot; height=&quot;109&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뭔가 트라우마가 있어 보였다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;남은 후보인 Go를 선택하기에 앞서 조금 고민해보았다. 과연 팀 운영에 도움이 될까. 러닝 커브가 있음은 명확하다. 특히 FE 개발자에게 Go로 개발하라고 하는 것은 가혹한 일이다. 하지만 LLM 시대는 언어의 장벽이 무너졌다. 잘 모르면 물어가며 개발하면 된다. 그렇기 때문에 Go를 선택하고 개발을 시작했다(사실 기존 코드를 그대로 바꿔달라고 하면 될 줄 알았는데 그건 불가능했다...)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;개발 방식&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;사실 참고할만한 Go API 프로젝트를 찾지 못했다. 간단한 Hello World 수준은 많지만 실무에서, 특히 FE 개발자가 운용할 BFF API에 어울리는 스타일을 찾지 못했다. 거기에 앞서 말한 것처럼 Protobuf를 사용해야 하며, gRPC도 지원할 수 있어야 했다. 결국 내가 고민할 수 밖에 없었다. 지금까지 얻어온 경험과 나름의 노하우를 종합해 아래 방식으로 개발을 진행하고 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;1. API Endpoint 및 외부 API와의 연동은 Protobuf로 정의한다&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 반드시 지켜야 하는 가장 강력한 룰이다. 프로젝트 내에서 처리하는 객체는 구조체로 정의하곤 하지만, 프로젝트 외부에서 접근할 수 있는 타입은 모두 Protobuf로 정의한다. 외부 프로젝트에서도 정의해둔 Protobuf 파일을 빌드해 API의 세부 Path와 요청, 응답을 타입을 그대로 사용할 수 있게 한다.&lt;br&gt;&amp;nbsp;&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/dE3ZV5/dJMcagKXdhj/7QdMthF4kNKKNHpJtt9C9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dE3ZV5/dJMcagKXdhj/7QdMthF4kNKKNHpJtt9C9k/img.png&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;596&quot; style=&quot;width: 58.8612%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dE3ZV5/dJMcagKXdhj/7QdMthF4kNKKNHpJtt9C9k/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdE3ZV5%2FdJMcagKXdhj%2F7QdMthF4kNKKNHpJtt9C9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1618&quot; height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgAKBm/dJMcabQoOSw/KEgiR6BVTsoeKnXciUIcWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgAKBm/dJMcabQoOSw/KEgiR6BVTsoeKnXciUIcWK/img.png&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;640&quot; style=&quot;width: 39.976%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgAKBm/dJMcabQoOSw/KEgiR6BVTsoeKnXciUIcWK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgAKBm%2FdJMcabQoOSw%2FKEgiR6BVTsoeKnXciUIcWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1180&quot; height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼쪽처럼 API Endpoint를 정의하고, 오른쪽 정의대로 요청을 받는다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;2. 싱글톤 패턴을 준수한다&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Node로 만든 API는 의외로 테스트에 약한 편이다. 많은 이유가 있겠지만, 개인적으로 export default로 대표되는 전역 객체 선언 방식이 문제라고 생각한다. 전후 연관 관계를 가진 객체를 주입할 수 없으니 유닛 테스트가 어려워진다고 생각한다. 이런 점을 처음부터 막기 위해 &lt;a href=&quot;https://github.com/uber-go/fx&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;uber의 fx&lt;/span&gt;&lt;/a&gt;를 이용해 로직을 수행하는 서비스를 싱글톤으로 생성하게 유도했다. 이와 동시에 API의 품질을 보증할 수 있는 테스트 코드를 작성하는 것을 가이드했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이외에도 gRPC와 http를 모두 지원하기 위해 gin-gateway로 구현했다던지, facade-service 레이어를 가진다던 하는 것들이 있지만 프로젝트를 마무리한 후 적어보려 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;성과&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 API 작업이 완료되어 간단하게 성능을 테스트 해볼 수 있었다. 로컬 및 배포 환경 기준으로 &lt;b&gt;대부분의 API의 응답 속도가 40~50%로 감소&lt;/b&gt;했다. 기존 API보다는 성능 측면에서 더 나을 것이라 기대하고는 있었지만 이렇게까지 큰 성능 차이를 보여줄 것이라고는 생각하지 않았다(나중에 확인해보니 Node의 JIT&amp;nbsp;&amp;nbsp;영향이긴 했다)&lt;br&gt;&amp;nbsp;&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/loFlo/dJMcajt4Zn3/O5mIY1tL5IGU0XkwQ0xYHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/loFlo/dJMcajt4Zn3/O5mIY1tL5IGU0XkwQ0xYHK/img.png&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;842&quot; style=&quot;width: 52.867%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/loFlo/dJMcajt4Zn3/O5mIY1tL5IGU0XkwQ0xYHK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FloFlo%2FdJMcajt4Zn3%2FO5mIY1tL5IGU0XkwQ0xYHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;635&quot; height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVk5FZ/dJMcahb0IXx/J0GTWwyt2T7MAeh39xljK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVk5FZ/dJMcahb0IXx/J0GTWwyt2T7MAeh39xljK1/img.png&quot; data-origin-width=&quot;301&quot; data-origin-height=&quot;459&quot; style=&quot;width: 45.9702%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVk5FZ/dJMcahb0IXx/J0GTWwyt2T7MAeh39xljK1/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVk5FZ%2FdJMcahb0IXx%2FJ0GTWwyt2T7MAeh39xljK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;301&quot; height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;제일 차이가 적은 API가 이정도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;추측하기로는 기존 API가 &lt;a href=&quot;https://gcanti.github.io/fp-ts/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;fp-ts&lt;/span&gt;&lt;/a&gt;로 만들어져 있어 많은 객체 생성이 이루어지니, 객체 처리에 부하가 큰 Node와 역시너지가 발생하고 있지 않을까 한다. 하지만 그렇다고 해도 너무 큰 차이라 혹시나 놓친 로직이나 API가 있나 싶어 겁이 나는 상황이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YNrDe/dJMcafec5TZ/6peh3zWsO1BoHWLE1RRLSk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YNrDe/dJMcafec5TZ/6peh3zWsO1BoHWLE1RRLSk/img.webp&quot; data-alt=&quot;그냥 두배 빨라진다고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YNrDe/dJMcafec5TZ/6peh3zWsO1BoHWLE1RRLSk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYNrDe%2FdJMcafec5TZ%2F6peh3zWsO1BoHWLE1RRLSk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;140&quot; height=&quot;141&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그냥 두배 빨라진다고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;대학원 진학과 개인 프로젝트&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;내년 기술 경영 대학원에 입학한다. 이제 주어진 업무를 수행하는 개발자를 넘어 리더를 목표로 하고 있기에, 이 목표에 도움을 줄 수 있는 과정이라고 생각한다. 이 석사 과정이 스펙 측면뿐 아니라 실제로 더 나은 리더가 될 수 있는 도움을 줄 수 있을지는 아직 모르겠다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cck7gL/dJMcac9zhQp/5jJ5ItTRhZcXKHyQtQRVEK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cck7gL/dJMcac9zhQp/5jJ5ItTRhZcXKHyQtQRVEK/img.jpg&quot; data-alt=&quot;직장인 + 학생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cck7gL/dJMcac9zhQp/5jJ5ItTRhZcXKHyQtQRVEK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcck7gL%2FdJMcac9zhQp%2F5jJ5ItTRhZcXKHyQtQRVEK%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;140&quot; height=&quot;131&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;직장인 + 학생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;대학원 지원과 동시에 개인 프로젝트도 시작하게 되었다. 석사 과정을 모집하는 학교들을 보니 어학 점수를 요구하는 경우가 꽤 있었다. 그래서 손놓았던 어학 공부를 해볼까 하여 관련 앱들을 보았는데, 구독료가 너무 비싸게 느껴졌다. 그래서 개인 용도로 사용하기 위해 만들기 시작했다. 대충 만들어 보니 꽤나 괜찮아 보여 프로젝트를 확대해 개발했고, 지금은 서비스를 런칭 했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KIPmG/dJMcadm50hb/P0c5mCTzjxpAEp56nykt91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KIPmG/dJMcadm50hb/P0c5mCTzjxpAEp56nykt91/img.png&quot; data-alt=&quot;여러가지로 확장하고 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KIPmG/dJMcadm50hb/P0c5mCTzjxpAEp56nykt91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKIPmG%2FdJMcadm50hb%2FP0c5mCTzjxpAEp56nykt91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;260&quot; height=&quot;543&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2255&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;여러가지로 확장하고 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;서비스 개발자가 대개 그렇듯, 직접 만든 서비스는 사용하지 않게 되어 결국 어학 점수를 요구하지 않는 대학원에 진학하게 되었다. 미국에서 유학 중인 동생이 차후 투자를 위해 많은 사람들과 이야기를 나눠보니 굉장히 긍정적으로 평가했다고 한다. 추세를 보며 기능을 확장해 준비한다면 조만간 제대로 시작할 수 있을 것 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;매년 블로그를 쓰다 보니 의도하지 않아도 무엇인가를 도전하게 되는 것 같다. 내가 했던 일과 생각을 정리하는 것만으로도 큰 도움이 되지만, 최소한 작년보다 더 나은 일년을 보내기 위해 목표를 설정하고 또 해내려고 하는 것이 도움을 준다. 아직까지 아쉬운건 글 쓰는 주기가 너무 길다는 것이다. 글을 쓸 때마다 매번 더 열심히 써야지, 자주 써야지 하는데 쉽지 않다. 형식 없이 간단하게 써두고 싶어도 마음이 허락하지 않는게 크다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이런저런 일이 좀 있다보니 올해보다 내년에 더 해야 할 일이 많다. 회사 업무도 있지만 대학원으로 생활 패턴 자체가 바뀌는 것이 큰 영향을 주지 않을까 생각한다. 이전에는 이렇게 해야 하는 일들이 줄지어 있을 때 걱정이 컸는데, 이젠 걱정보다 기대가 앞선다. 잘 해낼 수 있을지는 모르지만 잘 못한다고 해도 별 문제 없다는 것을 알게 되어서가 아닐까.&lt;/p&gt;</description>
      <category>ETC</category>
      <category>똥글</category>
      <category>회고</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/115</guid>
      <comments>https://partnerjun.tistory.com/115#entry115comment</comments>
      <pubDate>Sun, 4 Jan 2026 19:10:11 +0900</pubDate>
    </item>
    <item>
      <title>소셜 로그인의 구조화</title>
      <link>https://partnerjun.tistory.com/114</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;글을 쓰기에 앞서 네이버/카카오/구글 등 업체는 소셜 프로바이더로, 이 업체의 서비스를 이용해 로그인하는 방식은 소셜 로그인이라고 칭한다. 또, 카카오 기준으로 '&lt;i&gt;인카 코드&lt;/i&gt;'는 code로, '&lt;i&gt;코드&lt;/i&gt;'는 access token으로 칭한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인을 지원하지 않는 서비스는 이제 없다고 봐도 무방하다. 그 압도적인 편의성은 사용자를 끌어모으는데 큰 역할을 한다. 서비스를 만드는 입장에서도 상당히 편리하다. 복잡한 회원 서비스를 훨씬 간단하게 만들어준다. 하지만 항상 간단한 것은 아니다. 사용자에게 제공해야 하는 서비스 수준에서 구현해보면 특유의 진입점 컨트롤이 생각보다 어렵다는 것을 금새 깨닫게 된다. 특이하게도 소셜 로그인은 예제가 많지만 실제 서비스 수준에서 쓰기에는 매우 부적절한 '토이 프로젝트' 수준의 예제가 대부분이다. 심지어 네이버나 카카오는 국내 위주의 서비스이기에 더욱이 예제를 찾기 어렵다. 물론 LLM으로는 도움 받을 수 있지만 실제 서비스에서는 훨씬 더 많은 엣지 케이스가 펼쳐지니 LLM에 의존하기 힘들어진다. 소셜 로그인 진입점과 비즈니스 로직을 처리하는 것도 어렵다. 예를 들어, &lt;a href=&quot;https://partnerjun.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 글에서 소개했던 이전 프로젝트&lt;/a&gt;에서는 회원 가입/로그인, 간편 로그인 연결/해제, 회원 탈퇴 등 5개 이상의 소셜 로그인 진입점이 있었다. 진입점별로 처리해야 하는 비즈니스 로직이 달라지니 엔드포인트의 수가 너무 많아지게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;364&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCx47r/btsNLo6x3lL/vZyk5uYGUkevyOlP1DkoV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCx47r/btsNLo6x3lL/vZyk5uYGUkevyOlP1DkoV1/img.png&quot; data-alt=&quot;제공하는 소셜 프로바이더가 더 많다면?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCx47r/btsNLo6x3lL/vZyk5uYGUkevyOlP1DkoV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCx47r%2FbtsNLo6x3lL%2FvZyk5uYGUkevyOlP1DkoV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;350&quot; height=&quot;339&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;364&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제공하는 소셜 프로바이더가 더 많다면?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 서비스의 개발자 센터에서 등록할 수 있는 URL의 수는 생각보다 적다. 서비스마다 다르지만, 네이버는 5개, 카카오는 10개 수준이었다. 직접 문의하면 더 늘려주거나 직접 등록해주기도 하지만 그게 모든 회사에서, 모든 서비스에서 가능할지는 모른다. 이런 상황에서 어떤 고민을 하고 소셜 로그인을 개발하였는지 공유하고자 이 글을 적었다.&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;498&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZKbTL/btsNKb7T8F9/lqkBTrWIWh03YQkAMaUqD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZKbTL/btsNKb7T8F9/lqkBTrWIWh03YQkAMaUqD0/img.png&quot; data-alt=&quot;검색해서 훔쳐쓰자&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZKbTL/btsNKb7T8F9/lqkBTrWIWh03YQkAMaUqD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZKbTL%2FbtsNKb7T8F9%2FlqkBTrWIWh03YQkAMaUqD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;200&quot; height=&quot;200&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;검색해서 훔쳐쓰자&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소셜 로그인 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 소셜 로그인의 순서를 명확하게 할 필요가 있다. 토이 프로젝트로 진행해본 경우가 많아 대부분 알고 있지만 다시 한번 짚고 넘어가는 것이 크게 도움된다. 더 나은 방법을 찾기 전에 기본은 해야 한다. 네이티브 환경에서의 SDK 로그인도 별반 다르지 않기에 한번 기억해두면 큰 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹에서의 소&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;셜 로그인은 아래 순서로 진행된다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;JS SDK 설정 및 초기화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS로 구성된 SDK를 설치한다. 생각보다 더 긴 시간이 소요되므로 정상적으로 모두 준비되었는지 확인이 필요하다. &lt;br /&gt;자체적인 Initialize 함수를 사용하는 경우가 많아 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;onLoad 등 스크립트 이벤트로 후킹이 불가능하다고 봐야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;서비스에 따라 아래 2번에서 설정할 상태값을 JS SDK의 Initalize 함수에서 미리 설정하기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버튼 클릭 -&amp;gt; 소셜 프로바이더 사이트로 이동(혹은 팝업)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 동작(로그인 버튼 클릭)에 이어 시작되는 과정이다.&lt;/li&gt;
&lt;li&gt;소셜 로그인을 위한 옵션을 설정하고 로그인 폼이 있는 사이트로 이동한다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;옵션으로 팝업 혹은 리다이렉션같은 로그인 방식부터, 로그인 서비스를 이용하기 위한 Client ID, 그리고 로그인 이후 이동할 Redirect URI와 CSRF 공격을 막기 위한 state 등 상태 값이 포함된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;[소셜 프로바이더 사이트]&lt;/span&gt;&amp;nbsp; 소셜 로그인 진행&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 폼을 제공하는 소셜 프로바이더의 사이트로 이동하여 로그인한다.&lt;/li&gt;
&lt;li&gt;소셜 프로바이더의 개발자 센터에서 등록한 약관 등이 노출된다. 1번에서 설정한 조회 범위(scope)에 따라 제공 여부를 선택하기도 한다.&lt;/li&gt;
&lt;li&gt;UA 같은 접속 정보에 따라 '앱으로 로그인' 버튼을 제공하거나 아예 로그인이 막히기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;[소셜 프로바이더 사이트]&lt;/span&gt; 얻어낸 code, 입력했던 state와 함께 Redirect URI로 이동&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;code는 1회용으로 이 값만으로는 아무것도 할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redirect URI: code로 access token 획득&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소셜 로그인을 진행하여 받은&amp;nbsp; 코드와 1번에서 설정했던 Client ID, Redirect URI 등을 이용해 토큰 조회 API에 요청해 Access Token을 획득한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;access token으로 회원 정보 조회&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access Token을 이용해 소셜 프로바이더의 프로필 조회 API에서 회원 정보를 조회한다.&lt;/li&gt;
&lt;li&gt;1번에서 설정한 조회 범위(scope) 옵션에 따라 응답 값이 달라진다. &lt;br /&gt;예를 들어 1번 단계에서 scope에 성별을 입력해두어야만 응답으로 성별을 받을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직 수행&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로필 조회 API로 얻은 회원 정보에서 id(혹은 사용자 구분 토큰)와 사용자 정보를 이용해 회원 가입/로그인, 탈퇴 등 동작을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 순서에서 주의해야 할 것은 먼저 토큰이 &lt;span style=&quot;color: #ee2323;&quot;&gt;code와 access token으로 분리되어 있다는 것&lt;/span&gt;이다. 각 서비스마다 부르는 방식은 다르지만 이렇게 두가지로 분리되어 있는 것은 대부분 동일하다. 다만 서비스에 따라 code가 아닌 access token을 직접 전달하는 경우도 있다. 네이티브 로그인을 이용하는 애플이 대표적이다. 애플은 JS SDK 설정에 Redirect 방식을 명시하더라도 &lt;u&gt;IOS라면 네이티브 로그인을 시도&lt;/u&gt;한다. 네이티브로 로그인되면 Redirect 없이 JS 코드 상에서 access token과 state를 획득하게 된다. 또, IOS가 아니라 Redirect URI로 이동되더라도 &lt;a href=&quot;https://developer.apple.com/documentation/signinwithapple/configuring-your-webpage-for-sign-in-with-apple#Handle-the-Authorization-Response&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;POST로 code와 state를 전송&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;970&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbpK5I/btsNL0jHH1u/bat31jbQxeJtn0KyFaOra0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbpK5I/btsNL0jHH1u/bat31jbQxeJtn0KyFaOra0/img.png&quot; data-alt=&quot;미친놈들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbpK5I/btsNL0jHH1u/bat31jbQxeJtn0KyFaOra0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbpK5I%2FbtsNL0jHH1u%2Fbat31jbQxeJtn0KyFaOra0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;325&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;미친놈들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 만약 Redirect URI을 Next.js의 페이지로 구성했다면 특별한 로직이 필요해진다. context.request 객체에서 body를 뽑아내는 로직이다. &lt;a href=&quot;https://www.npmjs.com/package/formidable&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;formdiable 같은 라이브러리&lt;/a&gt;를 이용하면 아주 간단하게 해결할 수 있다. 다만 애플일 경우에만 이렇게 body에서 얻어낸 값을 사용한다는 점을 코드에 남겨야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼핏 보면 앞서 설명한 과정들을 모두 하나의 URL, 그러니까 Redirect URI로 설정한 페이지에 구현해도 무리가 없어 보인다. 하지만 한가지를 놓쳤다. 팝업 로그인이다. 팝업의 경우에도 Redirect URI로 이동하게 되는데, 로그인 폼이 노출되었던 자식 창이 이동한다는 점을 주의해야 한다. 일반적인 경우라면 자식 창을 닫고 부모 창에서 이동해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xhff0/btsNL9OprIY/g5A5FqKek9wGVHu8R7a6WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xhff0/btsNL9OprIY/g5A5FqKek9wGVHu8R7a6WK/img.png&quot; data-alt=&quot;팝업은 팝업으로 뜬 자식 창이 이동한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xhff0/btsNL9OprIY/g5A5FqKek9wGVHu8R7a6WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxhff0%2FbtsNL9OprIY%2Fg5A5FqKek9wGVHu8R7a6WK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;240&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;팝업은 팝업으로 뜬 자식 창이 이동한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 간단하게 해결할 수 있다. 부모 창에 메시지를 보내는 함수인 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;postmessage&lt;/a&gt;를 이용하면 된다. 하지만 이렇게 로직이 난해해지면 코드를 어떻게 관리해야 할지 고민되기 시작한다. 앞서 계속 언급한 것처럼, 회원 가입/로그인만이 아니라 회원 탈퇴, 연동/해제 등 많은 액션이 필요하므로 이 로직들을 모든 곳에 삽입할 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 나은 방법을 찾아서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 무엇이 문제인지, 혹은 앞으로 문제가 될지 파악했다. 그러면 각 요소들을 어떻게 해야 더 나은 방향으로 변경할 수 있을지 고민해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Redirect URI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인은 보안을 위해 로그인 된 이후 이동할 URL을 미리 등록해놓는다. 문제는 회원 가입/로그인, 탈퇴 등 사용자가 할 수 있는 &lt;span style=&quot;color: #ee2323;&quot;&gt;모든 액션에 대해 Redirect URI를 등록하는 것이 맞느냐는 것&lt;/span&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5yzVS/btsNL3tYf7a/1kxG7qpc3wlxsD0nE60r81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5yzVS/btsNL3tYf7a/1kxG7qpc3wlxsD0nE60r81/img.png&quot; data-alt=&quot;소셜 로그인은 로그인 후 정해둔 URL로만 이동할 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5yzVS/btsNL3tYf7a/1kxG7qpc3wlxsD0nE60r81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5yzVS%2FbtsNL3tYf7a%2F1kxG7qpc3wlxsD0nE60r81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;240&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;소셜 로그인은 로그인 후 정해둔 URL로만 이동할 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 액션에 대해 등록하려고 하면 너무 많은 URL을 등록하고 관리해야 한다. 테스트를 위해 만든 다른 환경들(QA, STAGE 등)을 생각한다면 URL이 많아도 너무 많아진다. 심지어 액션이 추가될 때마다 다시 등록해야 한다. 이미 등록 가능한 개수를 초과했다면 등록 할 때마다 직접 연락해야 할지도 모른다. 그래서 나는 하나의 Redirect URI를 등록하고자 했다. 소셜 로그인을 진행하면 정해진 하나의 URL로만 이동하고, 그 URL에서 상태에 따라 다시 분기하는 형식이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 상태 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번에서 언급한대로 하나의 Redirect URI만을 등록한다면 액션에 따라 어떻게 다시 분기해야 할지 고민하게 된다. 자연스럽게 쿠키를 이용해보게 되었다. 클라이언트에서 설정할 수 있고 서버에도 전달된다. 아주 완벽해 보였다.&lt;/p&gt;
&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;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YElO2/btsNKpLz0Yo/4l1gqDxjzWJiVAxfsRwPak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YElO2/btsNKpLz0Yo/4l1gqDxjzWJiVAxfsRwPak/img.png&quot; data-alt=&quot;그거 쿠키 쓰면 되는거 아니냐?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YElO2/btsNKpLz0Yo/4l1gqDxjzWJiVAxfsRwPak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYElO2%2FbtsNKpLz0Yo%2F4l1gqDxjzWJiVAxfsRwPak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;332&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그거 쿠키 쓰면 되는거 아니냐?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 쿠키는 치명적인 문제가 있었다. &lt;span style=&quot;color: #ee2323;&quot;&gt;브라우저에 남는다는 것&lt;/span&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;350&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOyjwc/btsNK05PMQJ/ZqBymxQUM0t9MOklUzyDBk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOyjwc/btsNK05PMQJ/ZqBymxQUM0t9MOklUzyDBk/img.jpg&quot; data-alt=&quot;쿠키?삭제? 그게 뭔데&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOyjwc/btsNK05PMQJ/ZqBymxQUM0t9MOklUzyDBk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOyjwc%2FbtsNK05PMQJ%2FZqBymxQUM0t9MOklUzyDBk%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;200&quot; height=&quot;200&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿠키?삭제? 그게 뭔데&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 원래 쿠키는 &lt;i&gt;set-cookie&lt;/i&gt; 헤더를 이용해 '문서 단위'로 설정하는 값이기에 동적으로 움직이는 웹 어플리케이션에는 어울리지 않는다. 실제로 테스트 중 가끔 원하는 타이밍에 쿠키가 설정되지 않던 것도 확인했는데, 이건 나중에 재현하고 더 깊게 확인해보려 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 쿠키는 각하되었다. 다음으로 사용할 방법에 대해 고민하던 중 state를 활용하는 방법이 떠올랐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTlWDz/btsNJQgIqNV/nogCyMtPcWXeLE0NV5QSlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTlWDz/btsNJQgIqNV/nogCyMtPcWXeLE0NV5QSlK/img.png&quot; data-alt=&quot;소셜 로그인은 로그인후 입력했던 상태 토큰을 그대로 반환한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTlWDz/btsNJQgIqNV/nogCyMtPcWXeLE0NV5QSlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTlWDz%2FbtsNJQgIqNV%2FnogCyMtPcWXeLE0NV5QSlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;266&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;소셜 로그인은 로그인후 입력했던 상태 토큰을 그대로 반환한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발해야 하는 소셜 프로바이더인 구글, 애플, 카카오, 네이버 4개 서비스 모두 소셜 로그인에 상태 토큰인 state를 포함할 수 있다. state는 주로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CSRF 공격을 막기 위해 사용되지만, 그 대신&lt;/span&gt;&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;state에 직렬화된 JSON 객체를 사용하면 되겠다는 생각&lt;/span&gt;이 들었다. state의 본래 목적인 CSRF 토큰과 로그인 이후 처리할 동작 등 다양한 것들을 모아 JSON 객체로 만들고, 이것을 직렬화-암호화하여 문자열로 만든다. 그리고 로그인할 때 함께 전송한다. 이렇게 전송한 state 문자열은 로그인 이후 되돌려 받게 된다. 로그인 이전의 상태를 이어갈 수 있는 것이다. 이렇게 state를 사용하는 방법이 없던 것은 아닌 것 같지만 서비스 수준에서 사용해도 괜찮은지 확신이 없었다. 하지만 여러 방면에서 테스트해보니 다른 방법들보다 훨씬 더 안정적이라는 것을 알게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgyEBw/btsNLz7OwZo/EmhyBvKtScTHaKeF84kGd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgyEBw/btsNLz7OwZo/EmhyBvKtScTHaKeF84kGd0/img.png&quot; data-alt=&quot;state 직렬화/역직렬화 코드 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgyEBw/btsNLz7OwZo/EmhyBvKtScTHaKeF84kGd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgyEBw%2FbtsNLz7OwZo%2FEmhyBvKtScTHaKeF84kGd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;388&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;state 직렬화/역직렬화 코드 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인이 완료되어 이동한 Redirect URI에서 이 state 문자열을 복호화-역직렬화하면 로그인을 시도했을 때의 상태를 알게 되므로 더 명확하게, 심지어 여러 윈도우를 유지하고 있더라도 상태에 대해 명확하게 파악하고 처리할 수 있게 된다. 복호화 과정이 있으니 state에 임의의 값을 넣는 공격에서도 자유로워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 역할 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명한 단계에 따르면, 소셜 로그인 5~7번 과정은 명확하게 다른 로직이다. 하나의 Redirect URI을 이용하게 되면 이 로직들이 같은 페이지에서 분기 처리되어야 하므로 코드를 관리에 어려움이 생긴다. 그래서 나는 위에서 설명한 state에 다음에 진행해야 할 액션의 구분 값을 넣도록 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9LtK/btsNLb7deaW/1QutCeom6Hc0deChfQIXN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9LtK/btsNLb7deaW/1QutCeom6Hc0deChfQIXN0/img.png&quot; data-alt=&quot;state에 다음에 할 동작을 기록해둔다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9LtK/btsNLb7deaW/1QutCeom6Hc0deChfQIXN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9LtK%2FbtsNLb7deaW%2F1QutCeom6Hc0deChfQIXN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;79&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;139&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;state에 다음에 할 동작을 기록해둔다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인이 완료된 후 이동한 Redirect URI에서는 state에서 확인한 액션의 구분 값을 통해 비즈니스 로직을 수행하는 페이지로 이동하도록 했다. 각 페이지에서는 주어진 역할만을 수행하고 다음 로직을 수행할 페이지로 리다이렉션 한다.&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;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tdy09/btsNMz662sH/F0vRY2sek2j5D38mbJoC71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tdy09/btsNMz662sH/F0vRY2sek2j5D38mbJoC71/img.png&quot; data-alt=&quot;정해진 페이지(URL)에서 비즈니스 로직을 수행한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tdy09/btsNMz662sH/F0vRY2sek2j5D38mbJoC71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftdy09%2FbtsNMz662sH%2FF0vRY2sek2j5D38mbJoC71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정해진 페이지(URL)에서 비즈니스 로직을 수행한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 페이지 자체가 하나의 모듈이 되어 재활용도 가능해지고, &lt;span style=&quot;color: #ee2323;&quot;&gt;비즈니스 로직 수정이 다른 페이지에 영향을 주지 않기 때문에 사이드 이펙트에서 안전&lt;/span&gt;해진다. 302 리다이렉션은 브라우저 히스토리 스택에 쌓이지 않으므로 유저의 뒤로가기에도 영향을 주지 않는다. 이 방법을 통해 코드 관리에 이점을 더할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. 팝업 핸들링&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 말했듯 데스크탑의 경우 사용자 편의를 위해 팝업 로그인을 하곤 한다. 팝업 로그인을 설정하면 새 창으로 소셜 로그인을 진행하게 되는데, 팝업으로 열린 윈도우에서 로그인하고 등록한 Redirect URI로 이동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xhff0/btsNL9OprIY/g5A5FqKek9wGVHu8R7a6WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xhff0/btsNL9OprIY/g5A5FqKek9wGVHu8R7a6WK/img.png&quot; data-alt=&quot;팝업 윈도우(자식 창)만 이동한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xhff0/btsNL9OprIY/g5A5FqKek9wGVHu8R7a6WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxhff0%2FbtsNL9OprIY%2Fg5A5FqKek9wGVHu8R7a6WK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;240&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;팝업 윈도우(자식 창)만 이동한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3번 역할 분리에서 URL을 분리하니 이 문제도 손쉽게 해결할 수 있게 된다. Redirect URI은 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/opener&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;window.opener&lt;/a&gt;가 있으면(자식 창이라면) &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;postMessage&lt;/a&gt;를 이용해 code나 state 등을 부모 창에 전달하고 창을 닫는다. 그리고 메시지를 받은 부모 창에서 3번에서 정의한 세부 URL로 이동하면 된다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dN5THy/btsNLSzmpYh/UKiGI8ueirxCvc4Kl8fJsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dN5THy/btsNLSzmpYh/UKiGI8ueirxCvc4Kl8fJsK/img.png&quot; data-alt=&quot;Parent Window에 event listener를 등록해야 한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dN5THy/btsNLSzmpYh/UKiGI8ueirxCvc4Kl8fJsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdN5THy%2FbtsNLSzmpYh%2FUKiGI8ueirxCvc4Kl8fJsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;246&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;363&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Parent Window에 event listener를 등록해야 한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 그림처럼 소셜 로그인과 관련된 로직은 전혀 수정할 필요가 없다. 덕분에 소셜 로그인의 각 페이지가 명확하게 모듈화되고, 비즈니스 로직 변경에 따른 사이드 이펙트에서 안전해진다.&lt;/p&gt;
&lt;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;앞서 말한 내용들을 정리하면 아래와 같다. 먼저 소셜 로그인이 필요한 진입점들을 구성한다. 그리고 진입점에서 소셜 로그인을 하기 전, state에 로그인 후 시도할 동작을 작성하여 직렬화한다. 소셜 프로바이더 사이트에서 소셜 로그인이 완료되면 일종의 '게이트웨이' 역할을 하는 Redirect URI 페이지로 이동한다. 이 '게이트웨이' 페이지에서는 state를 역직렬화하여 다음으로 실행해야 할 동작에 대해 알아낸다. 이어서 동작을 수행하는 페이지로 리다이렉션 시킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjQX2J/btsNKhUDR2B/0WGvYZGsI9hCcT2c1jpZP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjQX2J/btsNKhUDR2B/0WGvYZGsI9hCcT2c1jpZP0/img.png&quot; data-alt=&quot;전체 플로우는 의외로 간단하다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjQX2J/btsNKhUDR2B/0WGvYZGsI9hCcT2c1jpZP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjQX2J%2FbtsNKhUDR2B%2F0WGvYZGsI9hCcT2c1jpZP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1266&quot; height=&quot;544&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전체 플로우는 의외로 간단하다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 요소를 설명할 때에는 꽤 복잡한 것 같지만 이렇게 전체 플로우로 보면 납득 가능할 것이다. 이 구조로 구성했을 때 가장 큰 장점은 URL, 그러니까 페이지별로 역할이 확실히 분리되어 있다는 점이다. 위 그림을 예로 들면 /social/withdraw 페이지는 정말 회원 탈퇴 비즈니스 로직만을 수행하면 된다. 이 페이지를 수정하기 위해 회원 가입 로직을 파악해야 한다거나, 잘못 수정해서 영향이 가거나 하는 일은 없다. 그렇기 때문에 조금 더 마음 편하게 수정이 가능해진다. 추가 작업에도 장점을 가진다. 다른 액션이 추가되더라도 /social 하위 URL에 페이지를 추가하면 되기 때문이다.&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;그래서 나는 누구나 한눈에 알아볼 수 있고 또 마음놓고 수정할 수 있어야 한다고 생각했고, 최대한 노력해 이런 구조를 생각하고 적용했다. 하지만 정말 이 구조를 누구나 한눈에 알아볼 수 있을지 장담할 수 없다. 이전 글에 적었듯 페이지 단위의 로직에 익숙하지 않은 개발자가 많고, 클라이언트에서 동작하는 것을 선호하기 때문에 이 구조에 대해 가이드해주어야 하기 때문이다. 그래서 조만간 팀 내에 이 구조에 대해 공유하고 의견을 물어 더 개선할 점이 있을지 논의해볼 생각이다. 이미 런칭하여 수정하기는 어렵겠지만, 좋은 구조를 고민해둔다면 분명 다음 기회에 더 나은 결과물이 나올 것이다. 다만 회원 시스템을 다시 만들 일이 있을지 의문스럽다. 다른 회사에 가도 있던걸 쓰지 새로 만들 것 같지는 않기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XQify/btsNLoem4Zb/5ZtekKuOKqtg0KQu7WEHwk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XQify/btsNLoem4Zb/5ZtekKuOKqtg0KQu7WEHwk/img.jpg&quot; data-alt=&quot;회원 시스템을 또 만들 일이 있을까?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XQify/btsNLoem4Zb/5ZtekKuOKqtg0KQu7WEHwk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXQify%2FbtsNLoem4Zb%2F5ZtekKuOKqtg0KQu7WEHwk%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;200&quot; height=&quot;194&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원 시스템을 또 만들 일이 있을까?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>ECMAScript | TypeScript</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/114</guid>
      <comments>https://partnerjun.tistory.com/114#entry114comment</comments>
      <pubDate>Mon, 5 May 2025 13:06:04 +0900</pubDate>
    </item>
    <item>
      <title>회원통합 프로젝트에 대한 회고</title>
      <link>https://partnerjun.tistory.com/113</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 10월 말부터 신규 프로젝트에 참여하게 되었다. 야놀자, 인터파크, 트리플 3개 회사를 통합하면서 회원 체계, 그리고 로그인 시스템도 통합한다는 장대한 꿈의 프로젝트였다. 요즘 추세에 맞춰 간단한 UI와 과정으로 회원의 상태를 전환할 수 있게 하기로 하였기에 혼자서 진행하기로 했다. 하지만 내용이 점점 더 많아지고, 많은 경우의 수를 대응하자는 요청이 있었다. 결국 팀원 한명이 더 투입되어 FE는 2명이 개발하게 되었다.&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;929&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dA2a6z/btsNs5zkPhv/PAl69MLdcXHkNWZkfj1Iu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dA2a6z/btsNs5zkPhv/PAl69MLdcXHkNWZkfj1Iu0/img.png&quot; data-alt=&quot;전체 플로우 중 '일부'&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dA2a6z/btsNs5zkPhv/PAl69MLdcXHkNWZkfj1Iu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdA2a6z%2FbtsNs5zkPhv%2FPAl69MLdcXHkNWZkfj1Iu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;532&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전체 플로우 중 '일부'&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무 방식에서도 차이가 있었다. 기획에서 모든 플로우를 정하는 것이 아니라, 디자이너가 유저 플로우를 담당하는 방식으로 진행되었다. 유저 친화적이라는 장점도 있지만, 개발 측면에서 고려되지 않은 내용들이 다수 추가됨에 따라 FE에 업무적 압박감이 더욱 커졌다. 가장 문제가 되었던 것은 보존하지 않기로 하지 않았던 상태 데이터를 이용해 UI를 구성했던 것이다. 기획서를 보고 데이터를 구조화하고 준비하던 API와 달리, FE는 디자인 가이드를 보고 준비하여 서로 어긋나기 시작한 것이다. 결국 쿼리 파라미터를 이용해 지속적으로 유저의 상태를 이어가게 하였다. 그러다보니 코드 리뷰나 구조화 등은 오히려 더 족쇄가 되었고, 얼기설기 이어진 API와 FE의 로직들은 변화에 취약해졌다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 불만족스러운 결과물이 만들어졌지만, 어떻게든 일정 내에 런칭하는데는 성공했다. 후회보다는 어떤 점을 잘 했는지, 어떤 점이 부족했었는지, 그리고 어떤 점을 배웠는지 고민하여 적어둔다. 앞으로 진행할 프로젝트에서는 훨씬 더 나은 결과물을 내리라 다짐한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;잘한 점과 못한 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 업무&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;잘한점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BCJGq/btsNHQuOJ0X/FafRKlxv2DaKPfgRbbSn3k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BCJGq/btsNHQuOJ0X/FafRKlxv2DaKPfgRbbSn3k/img.jpg&quot; data-alt=&quot;새로운 이름으로 런칭한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BCJGq/btsNHQuOJ0X/FafRKlxv2DaKPfgRbbSn3k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBCJGq%2FbtsNHQuOJ0X%2FFafRKlxv2DaKPfgRbbSn3k%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;400&quot; height=&quot;267&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;새로운 이름으로 런칭한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;size18&quot;&gt;&lt;b&gt;PoC를 통한 선행개발&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 개발에 앞서 어려움이 예상되는 부분을 개발해봄으로써 전체적인 구조를 잡고 난제를 해결했다. 이 부분이 가장 이질적이며 어려웠던 부분이다. 문제가 발생한 원인은 단순하다. 소셜 로그인 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buw1HF/btsNGH0iXw1/vdrrQnRVx9eAcfiKs8NPAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buw1HF/btsNGH0iXw1/vdrrQnRVx9eAcfiKs8NPAk/img.png&quot; data-alt=&quot;카카오/네이버/구글 등 소셜 로그인은 필수 기능이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buw1HF/btsNGH0iXw1/vdrrQnRVx9eAcfiKs8NPAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuw1HF%2FbtsNGH0iXw1%2FvdrrQnRVx9eAcfiKs8NPAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;155&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;카카오/네이버/구글 등 소셜 로그인은 필수 기능이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황은 회원 통합 서비스에서 오히려 더 큰 어려움으로 다가온다. 각각의 앱에 등록된 서비스와 클라이언트 아이디가 다르기 때문에 단순한 방법으로는 로그인을 시도한 유저가 같은 회원임을 알 수 없다. 특히 이 프로젝트는 합병 예정이지만 법적으로는 아직 구분된 회사라는 특이한 상황에서 개발을 시작하게 되었다. 그렇기 때문에 소셜 서비스에서 지원하는 &lt;a href=&quot;https://help.naver.com/service/23029/contents/20552?lang=ko&amp;amp;osType=COMMONOS&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;서비스 그루핑&lt;/a&gt;을 할 수 없다는 문제가 생겼다(&lt;i&gt;구글/애플은 그런 기능을 지원하는지조차 알 수 없었다&lt;/i&gt;). 또, 기존 로그인 기능을 제거하는 것이 아니라 유지하면서 신규 서비스의 로그인을 지원해야 한다는 점도 중요한 점이었다. 이미 기기에 등록된 소셜 서비스의 클라이언트 아이디가 있다면 이것을 상황에 따라 동적으로 교체할 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제들을 해결하기 위해 네이티브 SDK(Android/IOS)가 아니라 웹뷰(JS SDK)로 로그인하고자 했다. 별 문제 없어 보였지만 테스트 도중 또 다른 문제를 발견하게 되었다. 보안 이슈였다. 웹뷰는 조작이 가능한 브라우저이기 때문에 소셜 로그인을 허용하지 않는 경우가 있던 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RehZu/btsNGVjJjdC/HenhK5Xf1bdEvKPjAh4lFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RehZu/btsNGVjJjdC/HenhK5Xf1bdEvKPjAh4lFk/img.png&quot; data-alt=&quot;웹뷰에서는 구글 로그인이 불가능하다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RehZu/btsNGVjJjdC/HenhK5Xf1bdEvKPjAh4lFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRehZu%2FbtsNGVjJjdC%2FHenhK5Xf1bdEvKPjAh4lFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;257&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;웹뷰에서는 구글 로그인이 불가능하다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;1106&quot; data-origin-height=&quot;605&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cI3UJu/btsNGhm64QQ/iCFzyNPA0YpHp6JU6Bhr0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cI3UJu/btsNGhm64QQ/iCFzyNPA0YpHp6JU6Bhr0K/img.png&quot; data-alt=&quot;앱에서 완전히 분리된 인 앱 브라우저로 이동한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cI3UJu/btsNGhm64QQ/iCFzyNPA0YpHp6JU6Bhr0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcI3UJu%2FbtsNGhm64QQ%2FiCFzyNPA0YpHp6JU6Bhr0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1106&quot; height=&quot;605&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;605&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앱에서 완전히 분리된 인 앱 브라우저로 이동한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 PoC를 진행하지 않았다면 이런 복잡하고 잘 사용되지 않는 기술에 대해 미리 검토하지 못했을 것이다. 특히 미리 개발하여 TF 인원들에게 예시 화면을 공유함으로써 필요한 기능에 대해 다시 한번 확인할 수 있었고, 실행 결과를 영상으로 남겨둠으로써 당시 상황을 재현할 수 있었다. 특히 PoC를 진행하면서 어떻게 문서 작업을 해야 하며 어떤 것들에 우선순위를 두어야 하는지 알게 되었다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 프로젝트에 있어 아주 중요한 과정이었다고 생각한다. &lt;/span&gt;이 경험은 앞으로도 큰 도움이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;못한점&lt;/h4&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;사실 리더가 아닌 실무자라는 입장상 다른 팀에 일정을 강요할 수 없다. 하지만 그럼에도 강요했어야 했다는 생각이 든다. 개발 기간에 들어갔음에도 최상위 결정권자의 의사와 엣지 케이스의 발견 때문에 기획과 디자인이 계속해서 바뀌게 되었다. FE에서 사용할 수 있을법한 API가 나오기까지는 너무나 오랜 시간이 걸렸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wz0HN/btsNIBYbUyT/e5GlklMLyKyrajakUIkVW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wz0HN/btsNIBYbUyT/e5GlklMLyKyrajakUIkVW0/img.png&quot; data-alt=&quot;심지어 QA가 끝나가는 상황에 API가 개발되고 있었다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wz0HN/btsNIBYbUyT/e5GlklMLyKyrajakUIkVW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwz0HN%2FbtsNIBYbUyT%2Fe5GlklMLyKyrajakUIkVW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;200&quot; height=&quot;195&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;심지어 QA가 끝나가는 상황에 API가 개발되고 있었다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느지막히 나온 API는 BFF라고 보기 어려운 수준이었다. 하나의 액션을 수행하기 위해 여러 API를 연속으로 불러야 하는 문제가 가장 두드러졌는데, 트랜잭션을 관리할 수 없는 FE에서 이런 상황이 발생하면 말 그대로 아무 것도 할 수 없는 상황에 처하게 되었다. 또, 화면 구성에도 큰 문제가 생겼다. 늦어지는 API를 기다릴 수 없으니 먼저 화면을 개발해두었는데, 개발이 끝난 화면이 불필요해지기도 했고 중간중간 필요한 화면이 추가되기도 했다. 정상적인 작업이 불가능한 상황이 이어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;늘 그렇듯 일정이 끝나갈수록 이슈에 대한 모든 초점은 FE로 집중 되었다. 일정에 대한 압박과 불완전한 API를 기반으로 만들어낸 결과물은 '단단하지' 않았고, QA 중 발견한 엣지 케이스에 무너져 내리기 시작했다. 몰아치는 이슈를 해결하고자 야근도 계속 하게 되었다. 완전한 일정 관리 실패였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발 리딩&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;믿고 맡기는 것은 오만함일 수 있다는 점을 배웠다. '당연히' 잘 하리라 생각한 부분에서도 많은 결점이 있었고 이것은 전체적인 완성도를 떨어뜨려 '단단하지 않은' 프로젝트가 되었다. 예를 들어, 앞서 설명했던 PoC에서 진행한 인 앱 브라우저를 이용한 로그인도 코드 자체의 난이도에 어려움은 없었기에 간단히 설명만 해주었지만(&lt;i&gt;당연히 관련된 문서는 공유했다&lt;/i&gt;), 웹뷰와 인 앱 브라우저의 쿠키가 공유되리라 생각한다거나, 세션에 무엇인가를 저장해두려는 등 정상적인 동작이 불가능한 코드가 작성되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 이런 문제를 막기 위해 PR을 제시한다. 동감하지만 현실적으로 가능한지에 대해 의문이 생긴다. 너무나 많은 회의(&lt;i&gt;정말 하루 종일 회의를 했다&lt;/i&gt;)로 시간이 없고 지쳐있는 상황에서 코드를 보니 너무 많은 부분을 놓쳤다. 큰 문제 중 하나는 타입 선언이었는데, 인터페이스를 이중 상속받는 등 개발 초기에 절대로 해서는 안되는 개발을 했다는 사실을 개발 마지막 주에 발견했다. 이미 엎질러진 물이기에 더이상 수정은 불가능했기에 어쩔 수 없이 그대로 두고 개발하였지만 다양한 엣지 케이스를 대응하지 못해 엉망 진창의 타입 아닌 타입 선언으로 변하고 말았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 내가 더 강하게 API 우선순위에 대해 의견을 제시하고 리딩했다면 이런 문제들이 없지 않았을까 하는 고민을 하게 되었다. PR을 조금 더 신경 썼더라면 어땠을까. 미리 개발 방향에 대해 제시하고 큰 그림을 그려줬다면 어땠을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 진행되고 있는 다음 작업은 아주 철저하게 준비를 하고 있다. 가장 먼저 한 일은 각 페이지가 어떤 동작을 해야 하는지 서버와 클라이언트 레벨에서 철저하게 작성하기 시작했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzKYJr/btsNIRVa419/wQtYYPmWeaCeA37djsskjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzKYJr/btsNIRVa419/wQtYYPmWeaCeA37djsskjK/img.png&quot; data-alt=&quot;페이지(URL) 단위의 로드맵이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzKYJr/btsNIRVa419/wQtYYPmWeaCeA37djsskjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzKYJr%2FbtsNIRVa419%2FwQtYYPmWeaCeA37djsskjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;168&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;페이지(URL) 단위의 로드맵이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 진행하며 알게 되었는데, PHP/JSP처럼 서버와 클라이언트에서의 동작을 명확하게 구분하여 개발하는 것을 '신세대' FE 개발자는 잘 하지 못하는 것처럼 보였다. XHR을 이용한 클라이언트 사이드에서의 요청을 선호하고 자주 사용해왔기에 회원 가입과 같은 페이지 단위의 플로우 개발에 약한 모습을 보이는 것 같다. 다음에 비슷한 일을 하거나 학습에 대해 조언을 구한다면 이렇게 작성한 페이지 단위에서 어떤 로직을 어떻게 수행해야 하는지 작성하여 가이드할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 기술&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;못한점&lt;/h4&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;다른 프로젝트에서 사용하지 않던&lt;span&gt; 기술을 도입함으로써 이슈 대응에 너무 긴 시간이 소요되었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7ahHN/btsNHOX04Dz/ZJZtvoAiKgi91qpUfFpgGK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7ahHN/btsNHOX04Dz/ZJZtvoAiKgi91qpUfFpgGK/img.webp&quot; data-alt=&quot;물론 난 쓰기 싫었다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7ahHN/btsNHOX04Dz/ZJZtvoAiKgi91qpUfFpgGK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7ahHN%2FbtsNHOX04Dz%2FZJZtvoAiKgi91qpUfFpgGK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;물론 난 쓰기 싫었다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올드함의 끝을 달리는 Beanstalk를 사용하던 다른 프로젝트와 달리, 사내 EKS 시스템을 사용함에 따라 전체적인 시스템 구성이 변하게 되었다. 생각치도 못한 로깅 시스템도 걸림돌이었다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Datadog을 사용하기 위해서는 &lt;/span&gt;사내에서 제공하는 라이브러리를 이용해야 했는데 어처구니없게도 Express.js 용으로만 만들어져 있어 Trace가 유실되는 문제를 발견했다. 나는 이를 해결하고자 꽤나 긴 시간을 들여 직접 Datadog과 연결을 설정하였는데, 커스터마이징해서 쓰지 말라는 경고를 받아 그냥 되던 말던 모르겠다는 식으로 마무리할 수 밖에 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 시간에도 문제가 있었다. 사내의 FE 프로젝트는 모노레포로 구성됨에 따라 빌드에 큰 리소스를 사용하고 있다. 모노레포가 문제가 아니라 정책상 빌드 캐시를 적용하지 않는 점이 문제다. 캐시를 사용하지 않으니 매번 모든 패키지를 다시 빌드하는데, 이 때문에 너무 긴 시간을 소모한다. 게다가 다른 패키지에서 사용하는 라이브러리도 도커 이미지에 포함되어 엄청나게 큰 용량을 가지게 된다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;배포 이후 dockerfile을 수정하고 &lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://nextjs.org/docs/pages/api-reference/config/next-config-js/output#automatically-copying-traced-files&quot;&gt;next의 standalone 모드를 사용하는 등&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 아주 간단한 튜닝으로 빌드 시간을 크게 줄여냈지만 &lt;/span&gt;일정상 개발 도중에는 이런 문제를 자세하게 파악하지 못했고, 전체적인 개발 생산성을 크게 떨어뜨렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next v15 버전으로 버전을 올린 것도 문제였다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;14버전으로 시작했지만 개발 도중 15버전으로 버전을 올렸는데, 기존에 사용하던 기능들이 정상 작동되지 않는 것을 확인하지 못해 시간 낭비로 이어졌다. 하나 예를 들자면 instrumentation 파일 이슈가 있었다. Next config의 &lt;a href=&quot;https://nextjs.org/docs/pages/api-reference/config/next-config-js/pageExtensions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pageExtension&lt;/a&gt;을 설정하여 사용하고 있는데,&lt;span&gt;&amp;nbsp;15버전부터 &lt;a href=&quot;https://nextjs.org/docs/app/guides/instrumentation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;instrumentation&lt;/a&gt;이 정식 도입되면서 페이지 파일로 구분되었다. 14버전에서는 pageExtension을 사용하더라도 instrumentation.ts 파일로 사용하는 것과 다른 부분이다. 개발하던 도중 버전을 올려&amp;nbsp; instrumentation의 코드가 제대로 작동되지 않고 있다는 사실을 파악하지 못했고, 전혀 다른 부분에 수정을 가해 시간을 낭비했을뿐 아니라 코드의 질도 떨어뜨리게 되었다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발 리딩 실패&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로도 개발 리딩에 실패했다고 생각한다. 자신 있다고, 할 수 있다고 말한다고 해서 정말 믿고 맡기는 것이 정말 옳은 것일지 의문이 든다. 내가 직접 하면 1시간으로 끝날 작업이 2~3일 소모될 수 있다는 것이 문제다. 대답을 믿지 않았어야 했던 것일까.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;925&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8MUEy/btsNGCkpwwo/JV0njKeoTBNxVAtzCdKhxk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8MUEy/btsNGCkpwwo/JV0njKeoTBNxVAtzCdKhxk/img.jpg&quot; data-alt=&quot;신뢰란 무엇일까&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8MUEy/btsNGCkpwwo/JV0njKeoTBNxVAtzCdKhxk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8MUEy%2FbtsNGCkpwwo%2FJV0njKeoTBNxVAtzCdKhxk%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;200&quot; height=&quot;218&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;925&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;신뢰란 무엇일까&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들자면, IOS에서 발생했던 '간헐적으로 로컬 스토리지에 접근 되지 않는' 문제가 있었다. 새 창 혹은 새 웹뷰를 열고, 새로 열린 웹뷰에서 결과를 로컬 스토리지에 저장한 후 창을 닫아 원본 페이지로 돌아가는 로직이 있다. 돌아온 원본 페이지에서는 저장된 값을 꺼내기 위해 setInterval로 로컬 스토리지를 polling하는 로직이 구현되어 있었다. 그런데 간헐적으로 로컬 스토리지의 함수들이 모두 먹통이 되는 문제가 있다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DQSIz/btsNHs8U5Er/cRCrRH53OWEOGFbB1nHQc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DQSIz/btsNHs8U5Er/cRCrRH53OWEOGFbB1nHQc0/img.png&quot; data-alt=&quot;인증 유도 화면에서 polling한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DQSIz/btsNHs8U5Er/cRCrRH53OWEOGFbB1nHQc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDQSIz%2FbtsNHs8U5Er%2FcRCrRH53OWEOGFbB1nHQc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;291&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인증 유도 화면에서 polling한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원은 이 문제를 해결하기 위해 며칠간 앓고 있었다. 이야기를 듣고 잠시 후 나는 IOS가 보안 이슈로 기능을 차단한다고 판단했다. 로컬 스토리지에 접근하는 것은 엄연히 Disk IO에 해당하는 기능인데 이걸 지속적으로 부르고 있다면 이건 아주 이상한 일이다. 팀원은 이 문제를 해결하기 위해 로컬 스토리지를 사용하던 로직을 쿠키로 변경했고, 쿠키 특성상 실시간 업데이트가 잘 되지 않으니 방어 로직이나 설정 시점을 변경하는 등 엉망이 되고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈치챘겠지만 심지어 이 구현 자체가 잘못 되기도 했다. 새 창에서 데이터를 변경했을 때 처리하기 위해 polling을 시도한 것인데, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;visibility change&lt;/a&gt; 이벤트와 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;postmessage&lt;/a&gt;를 사용하면 간단하게 해결된다. 내가 신경쓰지 않는 동안 다른 길로 두발자국 걸어가 고민하고 있었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 더 빠르다고, 잘 안다고 일을 빼앗아 하는 것은 분명 잘못되었다. 동료의 성장에도 도움이 되지 않는다. 하지만 그렇다고 해서 믿고 맡기는 것만이 옳은 것일까? 일정 내에 산출물을 내야 하는 상황이라면 성장할 수 있도록 맡기고 기다리는 것이 성공할 수 있는 길일까? 나는 아직 답을 내지 못하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;잘한점&lt;/h4&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;같은 웹 페이지를 3사 서비스 앱에서 사용하게 됨으로써 공통적으로 사용할 수 있는 신규 웹뷰 브릿지 라이브러리를 개발해야 했다. 기존에 사용하던 라이브러리와 크게 다르지 않지만 몇가지 신경써서 개발한 부분이 있다. 첫번째는 커맨드 형식의 웹뷰 브릿지다. 웹뷰가 시작될 때 앱은 웹뷰의 window 객체에 하나의 함수를 등록하고, 함수 내에서 문자열 작성된 커맨드에 따라 다른 기능을 실행하는 것이다. 별거 아닌 것처럼 보이지만 버전이 다를 때에도 오류가 발생하지 않는다는 큰 장점이 있다. 두번째는 응답 처리다. 웹뷰 브릿지의 경우 실제로는 함수 실행에 대한 응답을 받을 수 없다. 네이티브 시스템에서 호출을 받는 것 뿐이지 실제로 연동된 함수가 아니기 때문이다. 이를 위해 네이티브에서 함수를 실행한 후, 실행할 웹의 콜백 함수의 이름도 함께 전송하도록 했다. 기존에도 있던 기능이지만 나는 여기에 비동기 대기 로직을 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZcij/btsNIgusu5R/p2USdcw08orccPMtMwPzBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZcij/btsNIgusu5R/p2USdcw08orccPMtMwPzBk/img.png&quot; data-alt=&quot;모든 동작은 콜백을 받기에 await 구문이 사용 가능해진다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZcij/btsNIgusu5R/p2USdcw08orccPMtMwPzBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZcij%2FbtsNIgusu5R%2Fp2USdcw08orccPMtMwPzBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;114&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모든 동작은 콜백을 받기에 await 구문이 사용 가능해진다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹뷰 브릿지 함수 자체를 async로 만들고, 네이티브에서 콜백 함수를 실행했을 때 웹뷰 브릿지 함수를 resolve 하도록 한 것이다. 이런 간단한 방법으로 웹뷰 브릿지 함수가 항상 응답을 보장하는 함수가 되었다.&lt;/p&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;state를 활용한 소셜 로그인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인은 CSRF 토큰 확인을 위해 state라는 값을 전송할 수 있다. 이것이 표준 스펙화 되어있다는 점을 발견했는데, PoC 도중 이 state를 조금 다르게 사용하는 아이디어를 얻었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTlWDz/btsNJQgIqNV/nogCyMtPcWXeLE0NV5QSlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTlWDz/btsNJQgIqNV/nogCyMtPcWXeLE0NV5QSlK/img.png&quot; data-alt=&quot;소셜 로그인은 로그인후 입력했던 상태 토큰을 그대로 반환한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTlWDz/btsNJQgIqNV/nogCyMtPcWXeLE0NV5QSlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTlWDz%2FbtsNJQgIqNV%2FnogCyMtPcWXeLE0NV5QSlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;266&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;소셜 로그인은 로그인후 입력했던 상태 토큰을 그대로 반환한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상태, 혹은 이동해야 하는 URL 등을 포함하여 직렬화-암호화하고 state로 전송하면 항상 동일한 URL로 이동해야 하는 소셜 로그인에서(로그인 완료 URL을 등록하므로) 유저 상태에 따라 액션을 분기할 수 있겠다고 생각한 것이다. 이 아이디어는 위에서 설명했던 인앱 브라우저 로그인에서 활용할 수 있었다. 유저가 단순 로그인하려고 했던 것인지, 계정 통합을 위해 무엇인가 인증받던 상황인지 등을 state에 담는 것이다. 세션을 유지할 수 없는 인앱 브라우저에서도 쿼리로 전달받은 state로 다양한 정보를 이어받을 수 있게 되었다. 이것과 관련된 내용은 &lt;a href=&quot;https://partnerjun.tistory.com/114&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 글&lt;/a&gt;에서 조금 더 풀어 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레거시 시스템과의 융합&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 회원 서비스에 맞춰 모든 서비스의 회원 로그인 방식을 바꾸기는 불가능에 가깝다. 그래서 기존 시스템과 신규 시스템을 함께 사용할 수 있도록 개발하는 것도 중요한 포인트였다. 나는 이런 레거시 시스템과 함께 사용할 수 있도록 기존 로그인 정보를 확인하는 세션과 신규 로그인을 위한 토큰 시스템을 동시에 업데이트 하도록 조정했다. 특히 기존 방식 그대로 회원 정보를 사용하면서, 신규 토큰 정보를 갱신할 수 있도록 gateway 어플리케이션에 토큰 갱신 로직을 포함한 것이 유효했다고 생각한다. 다른 서비스는 회원 시스템 변경에 영향을 받지 않고 자연스럽게 신규 회원 서비스를 이용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 배운점&lt;/h3&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;개발 작업을 어떻게 관리할 것인가에 대해 조금 더 배울 수 있었다.&amp;nbsp;특히 진행해야 하는 작업을&amp;nbsp;&lt;i&gt;어떻게&lt;/i&gt; 공유해야 하는지에 대해 많은 고민을 하게 되었다. 이런저런 이야기를 나누다 보니 FE에서 필요한 API 명세를 작성해두는 것이 낫겠다는 생각을 하게 되었다. 어떤 API가 필요하고 응답이 어떤지 적엄으로써 각 페이지와 컴포넌트가 어떻게 구성될 것인지를 미리 적어두는 것인데, API 개발자가 이 내용을 보면 그대로 만들어주지 않더라도 필요한 값에 대해서 인지할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tEv4T/btsNIYGE6sP/anNB0cHq8bsSagcNEAXx6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tEv4T/btsNIYGE6sP/anNB0cHq8bsSagcNEAXx6k/img.png&quot; data-alt=&quot;FE 기준에서 먼저 API 명세를 작성한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tEv4T/btsNIYGE6sP/anNB0cHq8bsSagcNEAXx6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtEv4T%2FbtsNIYGE6sP%2FanNB0cHq8bsSagcNEAXx6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;223&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;437&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FE 기준에서 먼저 API 명세를 작성한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 이것이 더 나은 방법인지 장담할 수 없다. 하지만 최소한 끌려다니는 개발에서 벗어날 수 있을 것이라고 기대하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할 분담과 코드리뷰&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랫동안 합을 맞춰왔거나 경험이 많은 개발자가 아닌 경우 명확한 지시가 필요함을 알게 되었다. 사람마다, 역량에 따라, 그리고 상황에 따라 다르겠지만, 알아서 잘 하는 것을 기대하는 것은 과한 기대일지도 모르겠다. 또, 어떻게 코드 리뷰를 해야 할지에 대해 더 고민하는 기회를 가지게 되었다. 지금까지 나는 개인의 '개성'이 될 수 있는 코드에 대해서는 별달리 코멘트를 하지 않는 편이었다. 하지만 그것이 그 사람의 '개성'이 아니라 잘못된 방향으로 가고 있는 전조일 수 있다는 것을 알게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYE6K1/btsNJHjO9Ph/nxgZsdCM3hKk0fPWo4NXK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYE6K1/btsNJHjO9Ph/nxgZsdCM3hKk0fPWo4NXK1/img.png&quot; data-alt=&quot;Cursor같은 툴을 사용하면서 경향이 심해졌다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYE6K1/btsNJHjO9Ph/nxgZsdCM3hKk0fPWo4NXK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYE6K1%2FbtsNJHjO9Ph%2FnxgZsdCM3hKk0fPWo4NXK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;303&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Cursor같은 툴을 사용하면서 경향이 심해졌다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해하고 의도한 것이 아니라, 단순히 이렇게 하면 된다고 추천받은 코드일 수 있기 때문이다. 이렇게 작성된 코드는 직면한 문제만을 해결하게 된다. 나는 이제 의도한 작성한 것인지, 아니면 다른 방법을 찾지 못해 차선책을 선택한 것인지 물어보고 더 나은 방향을 제시할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;후임자를 어떻게 가이드할 것인가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 적었던 것처럼, 오히려 올드한 개발 방식에 대해 가이드 해주어야 하는 상황이 있었다. 정말 간단해서 당연히 알고 있을 것이라 생각하고 있거나, 혹은 알고 있다고 말하더라도 자주 사용하지 않아 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;익숙하지 않은 개발 방식인 경우가 있을 수 있다. 이런 경우도 앞서 계속 말해온 문서를 작성하며 조금 더 자세하게 개요를 적는 식으로 가이드하면 어떨까 한다. 물론 가이드해도 이해하지 못하거나 읽지 않을 수 있다. 하지만 그것은 개인의 역량이니 어쩔 수 없겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리다이렉션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직의 분리를 위해 '무의미한' 리다이렉션 전용 페이지를 만드는 것이 더 나은 상황을 발견했다. 예를 들자면, 소셜 로그인으로 얻은 토큰으로 엑세스 토큰을 얻어내는 것과 엑세스 토큰으로 회원 정보를 조회하는 것은 다른 로직이니 다른 페이지로 분리하는 것이 더 나을수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKXhWJ/btsNHy2nsoJ/JTlhscZrW7arH9V40v7FZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKXhWJ/btsNHy2nsoJ/JTlhscZrW7arH9V40v7FZK/img.png&quot; data-alt=&quot;302 리다이렉션이라면 뒤로가기도 문제없다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKXhWJ/btsNHy2nsoJ/JTlhscZrW7arH9V40v7FZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKXhWJ%2FbtsNHy2nsoJ%2FJTlhscZrW7arH9V40v7FZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;196&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;302 리다이렉션이라면 뒤로가기도 문제없다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP나 JSP같은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;오래된 개발 방식처럼 '페이지' 단위로 로직을 수행하도록 하는 것이다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;책임 분리 원칙을 페이지 단위에 녹여냈다고 볼 수 있다.&lt;span&gt; 각 URL에서 기대하는 로직이 명확하고, API를 연속으로 호출하지 않도록 구성할 수 있다. 수정이 필요할 때 페이지를 '끼워넣는' 식으로 처리할 수 있다는 것도 장점이다. 하지만 관리해야 하는 URL이 많아지고 사용자 경험에 악영향을 줄 수 있다는점, 그리고 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;이런 방식으로 개발하는 것에 익숙하지 않은 경우가 많다는 점이 단점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프록시와 헤더&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어처구니없게도 클라이언트 요청을 API에 프록시 처리하다가 이슈를 발견했다. 이유는 굉장히 단순한데, Content Length나 Content Type 등 헤더가 요청 온 그대로 전송되었기 때문이다. 실제로 전송하는 body의 length와 헤더로 전송된 Content Length가 달라 API에서 오류가 발생하는데, 이 오류는 로그에도 남지 않는다. 프레임워크 레벨에서 잘못된 요청으로 감지하고 예외 처리하기 때문이다. 간단하지만 경험이 없으면 찾기 힘든 이슈다. 이전에 크롤링하고 프록시하는 토이 프로젝트들을 해보지 않았다면 굉장히 고생했을 것 같았다. 유사한 실수를 막기 위해 헤더에서 불필요한 값을 제거하도록 상위 레벨에서 제어하는 것이 나아보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KOST8/btsNIQ3eo99/nplST1Hl0i4Nnn7U3KnmFk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KOST8/btsNIQ3eo99/nplST1Hl0i4Nnn7U3KnmFk/img.jpg&quot; data-alt=&quot;망가진 프로젝트를 살리는 것 또한 중요한 능력이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KOST8/btsNIQ3eo99/nplST1Hl0i4Nnn7U3KnmFk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKOST8%2FbtsNIQ3eo99%2FnplST1Hl0i4Nnn7U3KnmFk%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;200&quot; height=&quot;171&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;망가진 프로젝트를 살리는 것 또한 중요한 능력이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;</description>
      <category>ECMAScript | TypeScript</category>
      <category>똥글</category>
      <category>회고</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/113</guid>
      <comments>https://partnerjun.tistory.com/113#entry113comment</comments>
      <pubDate>Fri, 2 May 2025 14:57:37 +0900</pubDate>
    </item>
    <item>
      <title>배려하는 코드란 무엇일까</title>
      <link>https://partnerjun.tistory.com/112</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 꽤 큰 프로젝트를 진행하고 있다. 3개 서비스의 회원을 통합해서 로그인하는 SSO 프로젝트다. &lt;b&gt;&lt;i&gt;회원 통합&lt;/i&gt;&lt;/b&gt;이라는 이름으로 진행하는 이 프로젝트는 너무나 많은 기획 변경과 구조 변경으로 런칭도 전에 레거시화 되는 최악의 결과물을 만들어내고 말았다. 이 프로젝트를 진행하며 기술 내/외적으로 많은 것을 생각했고 얻었다. 그 중 하나를 적어두려 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rLYhk/btsNlZtGlh5/qdlUxJGfeikhJTeT6vxF9k/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rLYhk/btsNlZtGlh5/qdlUxJGfeikhJTeT6vxF9k/img.webp&quot; data-alt=&quot;정말 끔찍하게 힘든 프로젝트였다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rLYhk/btsNlZtGlh5/qdlUxJGfeikhJTeT6vxF9k/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrLYhk%2FbtsNlZtGlh5%2FqdlUxJGfeikhJTeT6vxF9k%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;185&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정말 끔찍하게 힘든 프로젝트였다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전부터 계속해서 적어왔듯, 나는 프로젝트를 진행할 다음 개발자를 위해 코드를 작성해야 한다고 생각한다. 그런데 그게 어떤 코드인지에 대해 더 깊게 생각해볼 필요가 있다고 느꼈다. &lt;i&gt;다른 사람에게 이렇게 작성해 &lt;/i&gt;라는 가이드를 줄 수 없기 때문이다. 그래서 다음 FE 웹 개발자를 위해 어떻게 작성해야 할지 몇가지 고민했다. 거창한 내용은 아니지만 다음에 찾아볼 수 있게 적어둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배려하는 코드란&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;맥락/호흡이 짧아야 한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째로 호흡이 짧아야 한다. 가장 중요하게 생각하는 요소다. 유지보수 단계로 넘어갔을 때, 전체 맥락을 파악하라고 하는 것은 너무 무리한 요구다. 중간에 투입될 수 있는 다른 개발자를 위해서도 마찬가지다. 전체 코드를 작성한 개발자조차도 모든 것을 기억하지 못하는데, 그런 상황에서 &lt;i&gt;이 부분을 고쳤을 때 어디에 어떤 영향이 갈지 몰라 &lt;/i&gt;라는 것은 정말 끔찍하다고 밖에 표현할 수가 없다. 심지어 최근 유행하는 LLM을 이용하려면 더더욱 호흡이 길어서는 안된다. 특정 코드 영역을 교체하는 것으로 단순하게 원하는 바를 이루어낼 수 있어야 한다. 한동안 유행했던 TDD 개발 역시 이 내용과 일맥상통한다. 적절한(이게 가장 어렵지만) 부분으로 코드 뭉치를 나눈다면 다른 부분에 영향이 없을 것이다. 그렇다면 FE 중심의 웹 개발에서는 어떻게 적용되어야 할까? 나는 두가지 부분으로 나누어 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 페이지를 분리한다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직을 처리하기 위해 302로 페이지를 이동시키는 방법을 고려한다. 굉장히 오래된(php 시절의) 처리 방식이기도 한데, Next.js와 같이 페이지 URL을 운용하는 경우가 많아 첫번째로 꼽았다. App Router든 Page Router이든 결국 클라이언트의 요청을 받아 처리하는 부분이 필요한데, 이 부분의 호흡을 짧게 해야 한다. 단순하게 함수로 분리하는 것도 있지만, 필요에 따라 페이지 자체를 나누는 것도 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 작업했던 프로젝트의 소셜 로그인이 그렇다. 소셜 로그인은 그 특성상 Redirect URL을 등록해놓고 로그인하면 소셜 코드를 가지고 URL로 이동하도록 되어있다(네이버/카카오/구글/애플 모두 동일한 정책이다). 그러다보니 하나의 페이지에서 여러 동작을 수행하고자 했고,&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 코드의 맥락을 잃고 각 파라미터의 의미나 로직 분기가 어려워졌다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 복잡한 로직을 코드로 풀어낼 수도 있겠지만 더 간단한 방법을 선택했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTYjzf/btsNmIYtWJg/teFWjfkbJP3xcyzAdDyt5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTYjzf/btsNmIYtWJg/teFWjfkbJP3xcyzAdDyt5K/img.png&quot; data-alt=&quot;소셜 로그인 관련 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTYjzf/btsNmIYtWJg/teFWjfkbJP3xcyzAdDyt5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTYjzf%2FbtsNmIYtWJg%2FteFWjfkbJP3xcyzAdDyt5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1106&quot; height=&quot;471&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;소셜 로그인 관련 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할의 분리를 위해 페이지 URL 자체를 분리한 것이다. 서버에서 페이지의 역할에 해당하는 로직만을 수행하고, state에 맞는 다음 스탭의 URL을 302로 응답한다. 이렇게 분리하면 사용자 경험 측면에 불편을 주지 않으면서 URL 별로 모듈화된 로직만을 구현할 수 있게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 최대한 컴포넌트를 나누지 않아야 한다&lt;/h4&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;1129&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m58iN/btsNiZtzfpH/OlYYjX9pwRDg79dWSAKyEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m58iN/btsNiZtzfpH/OlYYjX9pwRDg79dWSAKyEK/img.png&quot; data-alt=&quot;같은 컴포넌트지만 상태에 따라 보여질 컴포넌트가 다르다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m58iN/btsNiZtzfpH/OlYYjX9pwRDg79dWSAKyEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm58iN%2FbtsNiZtzfpH%2FOlYYjX9pwRDg79dWSAKyEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1129&quot; height=&quot;556&quot; data-origin-width=&quot;1129&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;같은 컴포넌트지만 상태에 따라 보여질 컴포넌트가 다르다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 QA 도중 발견한 문제를 수정할 때 생겼다. 그 유사한 컴포넌트가 어떤 상황에 노출되는지, 또 어떤 차이점을 가지고 있는지 파악하기 어려웠고, 심지어 진입한 페이지와 컴포넌트가 다대다 관계가 되어버림으로써 사이드 이펙트에 아주 취약한 상태가 되었다. 문구나 기능을 커스텀 훅을 이용해 페이지별로 모았다면 진입 경로에 따라 수정 여부를 결정할 수 있으니 더 나은 상태가 되었을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1051&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/txoov/btsNmjLYRDn/pgkeYZbadRIcLqF1yyoIn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/txoov/btsNmjLYRDn/pgkeYZbadRIcLqF1yyoIn0/img.png&quot; data-alt=&quot;페이지 진입시 보여지는 컴포넌트를 금새 파악할 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/txoov/btsNmjLYRDn/pgkeYZbadRIcLqF1yyoIn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftxoov%2FbtsNmjLYRDn%2FpgkeYZbadRIcLqF1yyoIn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1051&quot; height=&quot;454&quot; data-origin-width=&quot;1051&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;페이지 진입시 보여지는 컴포넌트를 금새 파악할 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 구조가 되면서 컴포넌트 트리 구조를 버리게 되었지만(구조화가 깨져버렸다), 페이지에 대한 맥락은 파악하기 유리해졌다. PageA에서 보여지는 C2 컴포넌트의 타이틀을 변경하기 위해 컴포넌트를 타고 돌아다닐 필요가 없다. 만약 상태에 따라 PageA에서 B1, C2 컴포넌트가 보여질 수 있다고 하더라도 usePageFooter에 넣는 파라미터를 보면 금새 찾을 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;불필요하게 단단한 코드를 지양해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 단단한 코드란 변화에 유연하지 않은 코드다. 타입 가드와 상속을 이용하는 인터페이스 정의를 예로 들 수 있다. 물론 이것들은 훌륭하고 유용한 기술이다. 하지만 개발 단계에서 정말 필요할지 고민해볼 필요가 있다. 예를 들어 타입 가드를 이용해 아주 명확하게 나누어진 타입이 있다고 하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNwzK5/btsNm0xMxET/NV0IIt3zdiiMau3Hz2tXf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNwzK5/btsNm0xMxET/NV0IIt3zdiiMau3Hz2tXf1/img.png&quot; data-alt=&quot;얼핏 보기에 적당해 보인다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNwzK5/btsNm0xMxET/NV0IIt3zdiiMau3Hz2tXf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNwzK5%2FbtsNm0xMxET%2FNV0IIt3zdiiMau3Hz2tXf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;614&quot; height=&quot;536&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;얼핏 보기에 적당해 보인다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 타입에 대한 응답에 필드를 추가하는 것은 쉽다. 하지만 그 타입을 상속했기 때문에 문제가 복잡해진다. 특히 다중 상속이 들어간 순간 특정 타입에 대해서만 변경하기는 아주 어려워진다. 위 예시에서 &lt;i&gt;ICommonResponse&lt;/i&gt;의 job이 필수 값이 되었다면 어떨까? 단순히 &lt;i&gt;ICommonResponse&lt;/i&gt; 필드에서만 변경해도 될까? 하지만 이것이 외부로부터 받는 값이라 job을 제공받지 못해서 실패한다면? JoinFailure에서 상속받는 &lt;i&gt;ICommonResponse&lt;/i&gt;에서 job필드를 Optional로 바꿔도 될까? 아니, job을 제공받지 못해서 실패했으니 type도 추가될 것이다. 그럼 그 type에 대응하기 위한 새로운 interface를 정의해야 할지도 모른다. 잠시 생각해보면 답은 찾을 수 있다. 그런데 그 답을 코드로 작성한 결과물이 다른 개발자가 보기에 잘 만든 코드일지는 장담할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 이런 문제를 피하기 위해 초기 개발, 그러니까 QA에 들어가기 전까지는 바보같은 타입 나열이 낫다고 생각한다. TS답지도 않고 무의미한 선언이 많아진다. 하지만 변화에는 유연해진다. 이렇게 개발이 진행 되고 자리잡은 이후 리팩토링하면 되지 않을까? 개발 도중에 JS가 자리잡을 수 있었던 그 유연성을 포기할 필요는 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bME0Jj/btsNiYVG7lW/c2wO5jSxwb5aBJBVFVy890/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bME0Jj/btsNiYVG7lW/c2wO5jSxwb5aBJBVFVy890/img.png&quot; data-alt=&quot;type에 string도 들어간게 정말 이상해보인다. 하지만 유연하다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bME0Jj/btsNiYVG7lW/c2wO5jSxwb5aBJBVFVy890/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbME0Jj%2FbtsNiYVG7lW%2Fc2wO5jSxwb5aBJBVFVy890%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1026&quot; height=&quot;250&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;type에 string도 들어간게 정말 이상해보인다. 하지만 유연하다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조직원의 평균 레벨에 맞는 코드를 작성해야 한다&lt;/h3&gt;
&lt;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;421&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mZb2j/btsNnoroivZ/XYEctu2G9jqhfWKWA4U03K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mZb2j/btsNnoroivZ/XYEctu2G9jqhfWKWA4U03K/img.png&quot; data-alt=&quot;스카우터가 터질 것 같은 개발력&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mZb2j/btsNnoroivZ/XYEctu2G9jqhfWKWA4U03K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmZb2j%2FbtsNnoroivZ%2FXYEctu2G9jqhfWKWA4U03K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;421&quot; height=&quot;344&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스카우터가 터질 것 같은 개발력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분의 가장 대표적인 것은 함수형 프로그래밍일 것이다. &lt;a href=&quot;https://ramdajs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ramda&lt;/a&gt;같은 라이브러리를 이용하면 짧은 코드로 멋지게 해결해낼 수도 있지만 유지보수에 있어서 훨씬 더 긴 시간을 소요하게 할 수도 있다. 그런 코드에 익숙하지 않다면 앞서 말했던 맥락 파악에 어려움이 생기기 때문이다. 특히나 연차가 낮거나 전공자 출신이 아닌 개발자가 많을 수 있는 FE 개발은 더욱 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중복에 대해 알려야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복된 코드를 만드는 것에 두려움을 가져서는 안된다. 하지만 중복되는 내용이 있다는 것을 알려야 한다. 특히 &lt;i&gt;dirty and paste&lt;/i&gt;를 지향할수록 생산성이 좋아지는 개발 단계에서는 더 중요하다. 결점을 찾아 어느 한 부분만 고쳐놓았지만,&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;단순히 붙여넣기 해놓은 코드가 너무나 적절하고 완벽해서 릴리즈 직전까지 아무도 문제를 찾지 못할 수 있다. 이런 경우 다른 개발자는 찾기가 더더욱 어려워진다. 이미 결점을 해결해놓은 부분을 가지고 열심히 고민하고 있을지도 모른다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEPaLY/btsNmm22Ep0/KC8YCpjCK71doyNtsKqVBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEPaLY/btsNmm22Ep0/KC8YCpjCK71doyNtsKqVBk/img.png&quot; data-alt=&quot;이렇게 해두면 바로 인지할 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEPaLY/btsNmm22Ep0/KC8YCpjCK71doyNtsKqVBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEPaLY%2FbtsNmm22Ep0%2FKC8YCpjCK71doyNtsKqVBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;194&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이렇게 해두면 바로 인지할 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황을 방지하기 위해 미리 유사하거나 같은 코드가 어느 부분에 있는지 적어둘 필요가 있다. 코드를 고치는 것만이 중요한게 아니다. 코드를 고칠 수 있게 유도하는 것도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이유를 알 수 있어야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 요구에 따라 바보가 되어야 할 수도 있다. 이렇게 바보가 되었을 때, 왜 그렇게 했는지에 대해 반드시 적어야 한다. 불의를 참지 못하는 개발자들은 이런 코드를 고치고 싶어하는데, 고치는 순간 장애가 발생한다. 간단한 주석이 앞으로의 장애를 막는 코드가 될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 웹뷰를 닫을 때 불필요한 다이얼로그를 호출하도록 한 코드가 있다. 대상이 되는 앱은 웹뷰가 닫힐 때 약 300ms 정도의 애니메이션이 있고, 애니메이션이 종료 되었을 때 웹뷰가 완전히 닫힌 상태가 된다. 평소에는 아무런 문제가 없는 동작이지만 웹뷰가 여러개 쌓여있을 때 문제가 발생했다. 상위 웹뷰가 닫히면 아래에 있던 웹뷰가 특정 응답을 수신하고 곧바로 닫히는 구현이 있었다. 포인트는 상위 웹뷰가 닫히는 애니메이션이 동작하고 있을 때, 하위 웹뷰가 활동 가능한 상태가 된다는 점에 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vva5J/btsNnAyGL0X/1CFj6oCKkm5K6wI8vhhc7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vva5J/btsNnAyGL0X/1CFj6oCKkm5K6wI8vhhc7K/img.png&quot; data-alt=&quot;앱이 현재 보이는 창을 닫도록 개발된 모양이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vva5J/btsNnAyGL0X/1CFj6oCKkm5K6wI8vhhc7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVva5J%2FbtsNnAyGL0X%2F1CFj6oCKkm5K6wI8vhhc7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;438&quot; height=&quot;438&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;438&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앱이 현재 보이는 창을 닫도록 개발된 모양이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 웹뷰에서는 필요한 로직을 수행하고 웹뷰를 닫는데 상위 웹뷰가 아직 닫히지 않았으므로 상위 웹뷰를 닫고자 하는 것이다. 닫히는 애니메이션이 동작하고 있던 웹뷰에 다시한번 닫는 요청이 들어가고, 하위 웹뷰는 닫히지 않아 더 이상 동작이 이어지지 않게 되어버린다. 근본적인 문제를 해결하기에는 시간이 부족하여 하위 웹뷰에서 다이얼로그를 띄운 후 버튼을 눌렀을 때 웹뷰를 닫도록 수정했다. 얼핏 보기에 아무 의미 없는 메시지 다이얼로그이지만 꼭 필요한 요소가 되어버린 것이다. 이런 이상한 히스토리가 있는 코드를 설명 없이 알 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;</description>
      <category>ECMAScript | TypeScript</category>
      <category>똥글</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/112</guid>
      <comments>https://partnerjun.tistory.com/112#entry112comment</comments>
      <pubDate>Wed, 16 Apr 2025 09:34:50 +0900</pubDate>
    </item>
    <item>
      <title>네이티브 앱에서 하이브리드 앱으로</title>
      <link>https://partnerjun.tistory.com/111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 기기의 성능이 압도적으로 좋아지면서 하이브리드 앱의 한계를 느끼기 어려워졌다. 웹 개발이라는 특성상 빠른 배포로 다양한 실험을 진행함으로써 지표를 확인할 수 있고, 특히 앱 심사라는 골치아픈 문제를 피해갈 수도 있다는 점도 큰 장점이다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;토스를 필두로 실제 서비스에 적용하는 사례가 많아지면서 다니고 있는 회사에서도 적용하겠다는 목표가 세워졌다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실제 개발에 들어가는 것은 몇 달 뒤가 될 것 같지만 어떤 점을 고려해야 할지 미리 생각해두었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고려해야 할 것들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Android&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;뒤로가기(Back Key)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드는 물리적 버튼(요새는 제스처)으로 뒤로 가기가 가능하다는 점을 고려해야 한다. 브라우저인 웹뷰 자체에서 '뒤로 가기'를 해 버리므로 의도하지 않은 동작을 할 가능성이 높다. 특히 앱에 노출되는 화면이기 때문에 모달(팝업이나 바텀 시트 등)이 노출되었을 때 자연스럽게 '뒤로 가기' 버튼을 누르게 된다. 이 동작을 적절하게 핸들링하지 않았다면 페이지 자체가 이동되어 끔찍한 UX를 선사하게 된다. 이런 문제를 해결하기 위해 윈도우 객체에 이벤트 핸들러를 추가할 수 도 있겠지만, search param이나 hashbang 등 URL을 이용한 동작으로 페이지 이동을 시뮬레이팅하는 것이 이상적이다. Next.js에서는 이런 구현을 위해 &lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/parallel-routes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Parallel Routes&lt;/a&gt;를 지원해주고 있으며, router 등을 이용해 직접 구현하는 것도 크게 어렵지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IOS&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Swipe Back&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IOS는 기본적으로 열려 있는 뷰를 스와이프하는 특별한 동작이 가능하다. 이 '스와이프 백'은 안드로이드의 '뒤로 가기' 버튼과 마찬가지로 이전 페이지로 이동할 수도 있고, 뷰 자체를 닫아 버릴 수도 있다. 먼저 '뒤로 가기' 동작을 허용했다고 생각하면 '캐시된' 이전 페이지가 스와이프하는 현재 뷰의 아래에 깔려 보이게 된다(BF 캐시를 이용한 이전 화면). 만약 이전 화면이 모달(팝업/바텀시트)이며 위에서 언급한 URL 이동으로 구현되어 있다면 모달이 없는 화면을 모달이 있는 화면이 덮고 있는 상황이 될 것이며, 이는 스와이프 할 때 매우 어색한 문제를 일으킨다. 두번째로 뷰 자체를 닫아 버리는 경우, 새 웹뷰를 통한 인증 등 이탈을 막아야 하는 흐름에서 추가적인 예외 처리를 해야 한다. 그렇기 때문에 앱 자체에서 웹뷰와 브릿지를 이용해 특정 상황에서 스와이프 백이 동작되지 않도록 설정하는 것이 무난하다. 이 작업은 &lt;a href=&quot;https://partnerjun.tistory.com/97&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;22년 하반기 회고&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;648&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGzqcw/btsJLQl4Ufv/aRe09JgRe8JMkWGx3XCJsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGzqcw/btsJLQl4Ufv/aRe09JgRe8JMkWGx3XCJsK/img.png&quot; data-alt=&quot;요거.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGzqcw/btsJLQl4Ufv/aRe09JgRe8JMkWGx3XCJsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGzqcw%2FbtsJLQl4Ufv%2FaRe09JgRe8JMkWGx3XCJsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;576&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1244&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요거.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;페이지 이탈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IOS는 물리적으로 페이지를 이탈할 수 있는 방법이 없다(위에서 언급한 SwipeBack이 있긴 하지만). 그렇기 때문에 주로 페이지 왼쪽 상단의 '뒤로가기' 버튼을 사용하게 되는데, JS가 로드 되지 않는 경우 이 버튼이 동작하지 않을 수 있다. 주로 버튼에 클릭 핸들러를 추가하는데 이 클릭 핸들러 함수 자체가 번들링되기 때문이다. 매우 느린 인터넷 환경, 혹은 네트워크 오류로 인해 JS 파일이 제대로 실행되지 않고 있다면 페이지를 닫을 방법이 없어지고, 이 말은 곧 앱을 강제 종료해야 한다는 뜻이다. 그렇기 때문에 이런 최악의 경우에도 페이지를 이탈할 수 있는 방법을 강구해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공통&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;브릿지 라이브러리 개발&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱과 기능을 통신하기 위한 브릿지 함수를 개발하고 관리해야 한다. 이 부분은 아주 기초적인 것으로 대부분의 회사, 도메인에서 이미 준비되어 있다. 특이한 것은 Jsonp과 유사하게 콜백 함수를 실행하도록 한다는 것인데, 이게 제대로 동작하지 않을 때 Sentry와 같은 클라이언트 로그 시스템에 오류가 남는다는 것이다. 가능한대로 Inbound Filter 처리해두긴 했지만 꽤나 신경쓰이는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;성능&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저라는 샌드박스에서 기동되는 웹 페이지는 당연히 네이티브 앱보다 느리다. 모바일 기기의 성능이 좋아졌지만 네이티브 앱과 비교하면 상대적으로 느린데, FE 특성상 철저히 관리하지 않으면 더 느려질 가능성이 높다. 느려진 페이지는 앱의 품질 자체를 떨어뜨리므로 항상 신경써야 한다. 특히 IOS는 위에 적은 것처럼 페이지 이탈 자체가 불가능해지는 이슈도 발생할 수 있다는 점을 주의해야 한다. 번들 분석 같은 기본적인 작업을 모두 끝냈는데도 느리다면, 성능을 끌어올리기 위해 도메인을 분리하는 방법도 있다. 마이크로 프론트엔드에 입각해 기능별로 도메인을 분리하여 번들의 물리적인 크기 자체를 줄여버리는 것이다. h2 이후로 다운로드 시간은 신경 쓰이지도 않는 문제가 되었기에 번들 파일의 크기가 줄어들면 페이지의 실행 속도는 눈에 띄게 빨라진다. 하지만 도메인을 분리하면 각 도메인별로 권한(GPS나 알람 등)을 허용해주어야 하고, local(session) storage 공유가 불가능한 문제가 있다. 그래서 최근 gateway proxy를 이용해 하나의 도메인으로 사용자에게 서비스를 제공하는 곳이 많아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sleep-wake, onPageShow update&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이라는 특성상 최소화-활성화를 핸들링해야 한다. 간단하게는 페이지를 새로고침 할 수 도 있고, API에 데이터를 재요청하는 방법도 있다. 특히 웹뷰를 여러개 띄웠을 때, 다른 웹뷰에서 갱신된 데이터를 새로 가져와 보여주어야 하는 경우가 많다. 이 내용은 &lt;a href=&quot;https://partnerjun.tistory.com/94&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BF캐시와 관련된 문서&lt;/a&gt;에 작성했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Long touch Selection&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트나 이미지를 길게 눌렀을 때 동작을 제어할지 판단해야 한다. 텍스트를 선택할 수 있거나(파란 색 셀렉션) 이미지를 저장하거나 하는 기능 비활성화를 고려해야 한다는 뜻이다. 이전 회사에서 함께 일했던 앱 개발자는 이것이 완성도의 차이라고 했다. 그때까지는 별 생각이 없었지만 그 이야기를 듣고부턴 계속 신경쓰인다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;File Selection&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹뷰에서 파일을 선택할 수 있다면 앱에서 어떤 식으로 허용해줄지 핸들러를 설정해야 한다. 특히 안드로이드의 경우 확장자나 다중 파일 선택 등을 앱에서 제어하므로, 관련된 문제가 발생했을 때 HTML과 안드로이드 양 쪽 확인이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Client Version, Client log&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 버전을 체크하고 로깅하거나 분기 처리할 수 있는 방법을 강구해야 한다. 경우에 따라서는 API까지 앱의 버전을 전달해야 하는데, 웹뷰를 열 때 요청되는 헤더나 쿠키 등을 이용하는 방법이 일반적이다. 한번 잘 정해두면 크롤러 등 귀찮은 문제를 걸러내는 데에도 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;웹뷰 이동 방식 결정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹뷰 내에서 페이지를 이동해야 할 때 웹 페이지의 리다이렉션을 이동할지 새로운 웹뷰를 띄울지 판단해야 한다. 새로운 웹뷰로 이동하는 경우 기본적으로 더 느리지만, 사용자의 '뒤로 가기'에 무조건 창을 닫는 식의 일관된 동작을 구현할 수 있어 UX 측면에서는 더 좋은 선택일 수 있다. 또 약관을 확인하거나 로그인을 하는 등 이전 동작의 흐름으로 되돌아와야 하는 경우는 이동보다 새로운 웹뷰를 띄우는 것이 더 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 측면에서의 문제를 제외하고 생각나는대로 적어두었다. 성능 문제를 제외한다면 크게 이슈가 될 것은 보이지 않는다. 다만 이 성능이라는 것이 네이티브와 상대적으로 비교하게 되니 곤란하다. 잘 될때는 아무 말 없지만 어쩌다 한번 잘 되지 않을 때(특히 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;네트워크 문제가 있을 때)&lt;/span&gt; 하이브리드 웹으로 전환된 것을 느끼게 되고, &quot;왜 전환했냐&quot;는 비난의 대상이 되기 때문이다. 나야 이미 몇 번 경험해서 또 욕하는구나 하고 넘어가지만 함께 작업할 동료들은 부담스러울지도 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;95e98552f814417513028870c6dc80590525072d322db86ea0e5b5f072fa8f3c.png&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnNqUR/btsJLtLx57H/D7Dbf4N2LjVEQHo3GeQRh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnNqUR/btsJLtLx57H/D7Dbf4N2LjVEQHo3GeQRh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnNqUR/btsJLtLx57H/D7Dbf4N2LjVEQHo3GeQRh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnNqUR%2FbtsJLtLx57H%2FD7Dbf4N2LjVEQHo3GeQRh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-filename=&quot;95e98552f814417513028870c6dc80590525072d322db86ea0e5b5f072fa8f3c.png&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>하이브리드 웹</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/111</guid>
      <comments>https://partnerjun.tistory.com/111#entry111comment</comments>
      <pubDate>Wed, 25 Sep 2024 22:09:09 +0900</pubDate>
    </item>
    <item>
      <title>팀장이 되면 어떻게 해야 할까</title>
      <link>https://partnerjun.tistory.com/110</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;실장에 이어 팀장도 퇴사한다는 소식을 전해왔다. 전 CTO가 이직한 회사로 간다고 한다. 팀장뿐 아니라 팀원도 한 명 같이 가기에 더 많은 퇴사자가 있을 가능성이 크다. 흔히 그렇듯 대규모 이동이다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqGhsn/btsJD7Bi6wn/t3Xr4h9z3kaCw3NhMHksL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqGhsn/btsJD7Bi6wn/t3Xr4h9z3kaCw3NhMHksL1/img.png&quot; data-alt=&quot;그들이 '굴러간' 돌이 될지도 모른다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqGhsn/btsJD7Bi6wn/t3Xr4h9z3kaCw3NhMHksL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqGhsn%2FbtsJD7Bi6wn%2Ft3Xr4h9z3kaCw3NhMHksL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그들이 '굴러간' 돌이 될지도 모른다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이 와중에 상위 조직인 유닛의 유닛장이 나에게 팀장 의사가 있는지 물어왔다. 팀장 아래 레벨 중 지원자가 있는지 확인하는 것이었다. 최근 회사가 비즈니스적인 이슈로 여러가지 이야기들이 있는 불안정한 시기이기에 우선 &lt;i&gt;&quot;손 들고 하겠다고 하지는 않겠다&quot;&lt;/i&gt; 정도로 의사를 전했다. 곰곰히 생각해보면 조만간, 혹은 적합한 사람이 없다면 당장 팀장이 되어야 하는 상황인 것을 깨달았다. 지금이 아니더라도 팀장이 되어야 할 것이며, 미리 준비하지 않는다면 내가 욕하던 무책임한 팀장이 되리라는 생각이 들었다. 내가 팀장이 된다면 어떻게, 무엇을 해야 할지 몇가지 생각해보았고 이를 적어둔다.&lt;br&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;이직했을 때와 마찬가지다. 이미 있던 것을 부수고 바꾸는 것은 쉽다. 이전의 상황을 분석하고 이해할 필요가 없기 때문이다. 이해는 어렵다. 사람은 가지각색이고 왜 그런 선택을 했는지 본인이 아니면 이해하기 어렵다. 하지만 어렵다고 해도 포기해서는 안 된다. 아무리 가치없어 보이고 멍청한 결과물로 보여 다시 시작해도 나 역시 같은 전철을 밟아 누군가가 부술 무엇인가를 만들어낼 확률이 높기 때문이다. 나는 이게 소위 말하는 신사업, 차세대 프로젝트가 실패하는 이유라고 생각한다. 2~3주 정도 한발치 떨어져 바라보는 겉핥기가 아니라, 직접 업무를 직면하여 면밀하게 파악하고 재구성해야 한다. 그것이 아무리 쓸모없는 회의나 문서라 할지라도.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5NWoC/btsJD5Kfjl6/CHlYQxeHD0WMcNmkn6PSDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5NWoC/btsJD5Kfjl6/CHlYQxeHD0WMcNmkn6PSDk/img.png&quot; data-alt=&quot;&amp;quot;갈아엎자&amp;quot;는 직책자가 해서는 안되는 말이 아닐까.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5NWoC/btsJD5Kfjl6/CHlYQxeHD0WMcNmkn6PSDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5NWoC%2FbtsJD5Kfjl6%2FCHlYQxeHD0WMcNmkn6PSDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&quot;갈아엎자&quot;는 직책자가 해서는 안되는 말이 아닐까.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;작은 것부터&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;항상 목표는 높다. 심지어 목표를 높게 하라고 모두가 말한다. 하지만 목표 그 자체를 이루어내거나 목표를 이루어 내기 위한 과정을 유지해나가는 경우는 극히 드물다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MXx2v/btsJD8UxD2q/uqRAmkmZyytxoKXTKaali1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MXx2v/btsJD8UxD2q/uqRAmkmZyytxoKXTKaali1/img.png&quot; data-alt=&quot;나도 명확한 목표를 선정해서 이룬 적은... 없는 것 같다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MXx2v/btsJD8UxD2q/uqRAmkmZyytxoKXTKaali1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMXx2v%2FbtsJD8UxD2q%2FuqRAmkmZyytxoKXTKaali1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;나도 명확한 목표를 선정해서 이룬 적은... 없는 것 같다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;목표 선정과 달성에 대해 가장 깊게 분석한 분야는 게임이라고 생각한다. 최근의 게임들은 계단식 성장과 &quot;분재화&quot;를 추구한다. 하루 하루 숙제만 하다보면 어느새 성장하는 것이다. 회사 생활도 마찬가지다. 계단식 성장과 분재화를 통해 점점 성장하도록 유도하는 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;팀장이 이러한 &quot;분재화&quot;된 사이클을 유도하면 팀과 개인 모두 성장할 수 있을 것이라 기대한다. 그러기 위해&amp;nbsp;한번에 많은 일들을 시작하기보다 아주 간단한 것부터 시작해야 한다. 특히 조심해야 하는 것은&amp;nbsp;회의나 평가에 이어지는 것들이다. &quot;팀을 위해&quot; 많은 회의를 잡고 &quot;공정한 평가를 위해&quot; 다양한 잣대를 세우기보다 아주 간단한 것부터 시작해야 할 것이다. 이를테면 무엇에 관심있는지, 무엇을 하고 있는지 물어보는 티타임이다.&lt;br&gt;&amp;nbsp;&lt;br&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;i&gt;&quot;이 팀은 잘 하고 있으니 건드릴 필요가 없어&quot;&lt;/i&gt;라는 인식을 심어주어야 한다. 이런 정도로 팀의 가치를 증명하기 위해서 어떤 방법이 있을지 고민해보았다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;주어진 업무 수행&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;팀에 기대하는 기본적이고 단순한 것으로, 정확하게 수행한다 해도 팀의 가치가 높아지지 않는다. 심지어 팀이 팀으로 존속할 수 있을 가능성도 매우 낮다. 제대로 해내지 못했을 때 감점이 되는 요소일 뿐이다. 하지만 업무 수행도 가치가 생길 수 있다. 수행한 업무를 모아 가치를 만들어내면 된다. &lt;i&gt;'업무 수행함'&lt;/i&gt; 으로 끝나는 것이 아니라, 흔히 말하는 &lt;i&gt;액션 아이템&lt;/i&gt;을 발굴해내야 한다. 어떠한 업무를 통해 우리는 서비스를 더 뛰어나게 하기 위해 이러한 것을 발견했다는 식이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;일례로, 제작년 수행했던 액션 아이템이 있다. 클라이언트 로그(통계) 전송 라이브러리 개발이다. 사내의 페이지를 수정/개발할 때마다 클라이언트 로그를 변경해야 했는데, 이 전송 로직이 프로젝트별로 흩어져 있었다. 심지어 오래된 버전의 라이브러리를 사용하고 있어 번들링 되었을 때 용량도 너무 컸다. 로직을 한데 모아 개선하고 라이브러리를 최신화한 것을 라이브러리로 만들었고 이는 꽤나 좋은 평가를 받았다. 다른 팀에서도 사용하는 일종의 공통 라이브러리가 되었을 뿐 아니라, 이를 기반으로 다양한 업무로도 이어졌다. 단순한 로그 작업에서 다른 팀에까지 영향을 미치는 프로젝트로 이어진 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;능동적인 이미지&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;FE는 그 특성상 수동적인 팀으로 보일 가능성이 높다. &lt;i&gt;디자인이 나와야 한다, API가 나와야 한다&lt;/i&gt; 등 어쩔 수 없는 현실적인 제약 때문이다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enTvrf/btsJESDvInp/i5k3TJFYkFmVyYQJD85UlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enTvrf/btsJESDvInp/i5k3TJFYkFmVyYQJD85UlK/img.png&quot; data-alt=&quot;뭘 알려줘야 하지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enTvrf/btsJESDvInp/i5k3TJFYkFmVyYQJD85UlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FenTvrf%2FbtsJESDvInp%2Fi5k3TJFYkFmVyYQJD85UlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뭘 알려줘야 하지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;너무 불리하다. 한걸음 떨어져서 본다면 팀 자체가 수동적인, 곧 알아서 일하지 않는 집단으로 보일 확률이 높다. 한번 생긴 인식은 떨쳐내기 어렵다. 문제가 있을 때 정리를 위한 첫번째 타깃이 될 확률이 높다. 이 현실적인 제약을 비틀어 능동적인 이미지를 만들어 두어야 한다. 개인적으로 프로토타이핑을 통한 진행 상황 공유가 그 해답이라고 생각한다. 웹 개발, 특히 웹 FE는 다른 개발에 비해 프로토타이핑에 강하다. 컴포넌트화 되면서 변화에도 그리 민감하지 않다. 이런 이점을 살려 디자인이 나오지 않더라도, 혹은 API가 나오지 않더라도 미리 작업하고 기획이나 디자이너에게 공유하는 것이다. &lt;i&gt;&quot;이런 분위기로 노출될텐데 한번 확인 부탁드립니다&quot;&lt;/i&gt;라는 말 한마디로 충분하다. 덤으로 담당자들에게 다시 한번 생각할 시간을 주기에 프로젝트의 완성도도 높아진다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술적 증명&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;목적 조직이건 기능 조직이건 기술적 가치를 가질 수 있는 집단이라면 팀 외부에 영감을 줄 수 있어야 한다. 단순히 팀 내에서 잘 만들었고 훌륭한 기술을 사용하고 있다고 하더라도 어필하지 않으면 아무런 가치가 없다. 심지어 실제로 그 기술을 사용하지 않더라도 다른 팀, 다른 조직에게 이러한 것이 있고 우리에게 어떠한 가치가 있을 수 있다는 것을 보여준다면 그것만으로도 팀의 기술적 역량이 증명된다. 더 나아가서 팀, 조직뿐 아니라 더 넓은 필드를 목표로 해야 한다. 작게는 개인 블로그나 팀 블로그, 크게는 사내/외부 발표(컨퍼런스)나 도서 출판 등이 있다. 이런 외부 활동을 활발하게 하고 있는 배민이나 토스는 모두가 '테크 기업'이라는 인식을 가지게 되었다. 회사에 대해 좋은 인식을 심어주는 팀은 회사에서 손놓고 싶지 않을 것이다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dszFIz/btsJFv8VD3T/NO2UXkijGXjINgqQeouqAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dszFIz/btsJFv8VD3T/NO2UXkijGXjINgqQeouqAK/img.png&quot; data-alt=&quot;천룡인에 한걸음 다가서게 될지도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dszFIz/btsJFv8VD3T/NO2UXkijGXjINgqQeouqAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdszFIz%2FbtsJFv8VD3T%2FNO2UXkijGXjINgqQeouqAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;천룡인에 한걸음 다가서게 될지도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;물론 발표는 쉬운 일이 아니다. 심지어 공유할 내용이 없을 지도 모른다. 그래서 내가 제안할 목표는 도서 출판이다. 업무를 수행하다 보면 많은 문제에 직면한다. 이렇게 직면했던 문제와 해결 방안들을 모아 문서화하면 그것만으로도 중-고급자에게 도움되는 엄청난 자산이 된다. 심지어 시장에 초급-중급자만을 위한 도서만이 있다는 것을 생각해보면 경쟁력도 있다. 실제 도서로 출판하지 못해도 개인/팀 블로그에 올릴 수 있다는 점도 장점이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;비즈니스적 증명&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;기술자는 기술을 주력으로 생각한다. 하지만 회사는 돈을 벌기 위해 존재한다. 돈을 벌어주지 못하는 팀은 필요 없다. 그렇기 때문에 &quot;기술&quot;이 매출 증대 혹은 감소 방지에 영향을 주고 있음을 명확한 지표로 나타내야 한다. 여기에서 흔히 말하는 &quot;정량적 지표&quot;가 필요하다. 웹 개발자가 만들어낼 수 있는, 매출에 영향을 주는 지표는 크게 두가지로 생각된다. 첫째로 서버 비용 증감이다. 클라우드 서비스를 이용하기 시작하면서 서버 비용은 큰 부담이 되었다. 특히 프로모션에 직면하는 FE는 서버의 수가 가장 많을지도 모른다(SSR 여부에 따라 차이가 크다). 개발자가 목메는 성능을 잡아 이러한 서버의 수를 줄인다면 그것만으로도 상당한 액수를 줄여낼 수 있다. 단순히 &lt;i&gt;&quot;퍼포먼스가 늘었다&quot;&lt;/i&gt;에서 끝나는 것이 아니라 &lt;i&gt;&quot;비용이 몇 퍼센트 감소했다&quot;&lt;/i&gt;까지 이어져야 한다는 것이다. 두번째는 SEO를 통한 외부 진입 성과다. 한국의 검색 엔진은 거의 죽었다고 볼 수 있다. 대부분이 구글에 의존하고 있는 상황이기에 구글의 검색 순위에 따라 매출은 크게 증가한다. 몇 달간 어떤 작업을 통해 어떻게 변화했는지를 우리가 전송하는 클라이언트 로그를 기반으로 문서화해서 공유해야 한다. 장기적으로 어떤 작업을 진행해서 어떤 증감이 있었는지 가시적으로 보여준다면 관심을 가지게 될 것이다.&lt;br&gt;&amp;nbsp;&lt;br&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;i&gt;&quot;항상 가슴속에 사표를 지녀라&quot;&lt;/i&gt;는 말은 이전 회사에서 들었던 가장 인상깊은 말이었다. 회사가 전부가 아니라는 말이기도 하지만, 언제든 회사에서 나갈 수 있도록 준비해야 한다는 말이기도 하다. 특히나 요즘처럼 높은 인건비로 신경이 곤두서있는 시기라면 평소의 준비가 빛을 발할 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;나만의 '무기' 준비&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;기술자는 가진 기술로 빛난다. 하지만 모두가 같은 빛을 발한다면 비교될 수 밖에 없다. 우리는 비교당하는 사회에서 살아남기 어려움을 안다. 이른바 '제네럴리스트'로서는 살아남기 쉽지 않다는 뜻이다. 그래서 나는 본인만의 특별한 빛을 보여줄 수 있는 '무기'를 준비하는 것이 좋다고 생각한다. 하지만 정말 엄청나게 깊게 팔 필요는 없다. 그런 지식은 검증할 수 있는 사람도 없고, 한정적인 상황에서 빛을 발해 오히려 인정받기 어렵다. 동료가 찾아와 답을 얻어갈 정도의 '스페셜리스트'를 목표로 해야 한다. 당연히 막연하게 준비하는 것은 어렵다. 지금까지 해 온 업무들, 그리고 내 생각에도 '꽤나 스마트하게' 해결했던 문제를 모아보자. 그 문제를 어떻게 해결할 수 있었는지, 다른 대안은 어떤 것이 있었는지 생각하고 찾아보는 것으로 첫 발걸음을 뗄 수 있지 않을까.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;신기술에 대한 비판적 사고&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;신기술에 대해 무조건 긍정하여 도입하고자 하는 사람이 있는 반면, 무조건 반대하는 사람이 있다. 나는 두가지 형태 모두 일리있다고 생각한다. 새로운 기술에 대해 반대한다면 항상 같은 것만을 사용하게 되어 학습하지 못하게 된다. 우리는 학습하지 못하면 도태된다. 하지만 무조건 긍정하여 도입한다면 돈을 벌기 위한 프로젝트가 기술의 시험장이 되어버린다. 제대로 알지 못하는 상황에서 생각치 못한 결점을 맞이하게 되면 문제가 심각해진다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w8mYG/btsJDVA5TBZ/hLRhmjcuW34Ke01bKXkR5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w8mYG/btsJDVA5TBZ/hLRhmjcuW34Ke01bKXkR5k/img.png&quot; data-alt=&quot;그래서 그거 왜 씀&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w8mYG/btsJDVA5TBZ/hLRhmjcuW34Ke01bKXkR5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw8mYG%2FbtsJDVA5TBZ%2FhLRhmjcuW34Ke01bKXkR5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그래서 그거 왜 씀&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그렇기 때문에 나는 두가지의 중도, 하지만 부정적인 측면에 약간 더 기울어, 기술에 대해 비판하는 상태를 지향해야 한다고 생각한다. 비판하기 위해서는 자연스레 기술에 대해 파악해야 하기 때문이다. 장단점을 파악하고 이 서비스에 있어 기술을 사용했을 때 어떠한 가치를 가지는지에 대해 분석하고 사고할 수 있어야 한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;가치 증명&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;내가 가진 특징과 기술을 증명할 수 있어야 한다. 해온 것들을 앞에서 말로 설명할 수 있으면 좋겠지만 그럴 기회조차도 주지 않는 것이 현실이다. 최소한 문서에서만큼은 완벽하게 포트폴리오나 블로그 등을 주기적으로 기록하고 갱신해야 한다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vo0MJ/btsJFKLH2Sc/KSj7U7bGf4JarIGeJW9TS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vo0MJ/btsJFKLH2Sc/KSj7U7bGf4JarIGeJW9TS0/img.png&quot; data-alt=&quot;나도 제대로 지키지 못하고 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vo0MJ/btsJFKLH2Sc/KSj7U7bGf4JarIGeJW9TS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVo0MJ%2FbtsJFKLH2Sc%2FKSj7U7bGf4JarIGeJW9TS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;나도 제대로 지키지 못하고 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그러한 '페이퍼' 스타일을 싫어하는 사람도 많다.&amp;nbsp;나도 링크드인이나 이력서 등 문서로 떠드는 사람을 좋아하지 않는다. 흔히 친구들에게 &lt;i&gt;&quot;이 사람은 핵미사일을 만들어 본 것 같다&quot;&lt;/i&gt;고 비아냥거리기도 한다. 하지만 지금 그것이 유행이고 현실이다. 내가 결정권자라면 그것 말고는 볼 수 있는 것이 없기 때문이다. 살아남기 위해서는 준비하는 수밖에 없다.&lt;br&gt;&amp;nbsp;&lt;br&gt;가장 주요한 것은 팀과 개인의 가치 증명, 이 두가지를 동시에 해내는 것이다. 위에서 언급한 팀 블로그나 도서 발매 등이 대표적이다. 물론 굉장히 어려운 일이다. 하지만 내가 이직하기 위해 준비한다고 생각하면 조금이나마 의욕이 생기지 않을까.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이런저런 이야기를 써두었지만 내 생각에 팀장으로써 중요한 것은 두가지다. 팀의 가치를 증명할 수 있는 일들을 주도하고, 팀원 개개인의 능력을 연마하고 증명할 수 있는 판을 짜는 것이다. 어떤 팀장에게 각자도생이라는 말을 들었던 적이 있다. 맞는 말이다. 그 누가 책임지고 도움을 줄 수 있을까. 맞지만 옳지는 못한 말이다. 팀장이라면 각자도생하라는 말로 끝나는 것이 아니라, 각자도생 하는 방법을 알려주어야 한다고 생각한다. 그것이 직위의 무게니까. 난 적어도 지금은 그렇게 생각한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>똥글</category>
      <category>팀장</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/110</guid>
      <comments>https://partnerjun.tistory.com/110#entry110comment</comments>
      <pubDate>Tue, 17 Sep 2024 21:27:31 +0900</pubDate>
    </item>
    <item>
      <title>atob와 encodeURIComponent. 짝이 되는 변환 함수들</title>
      <link>https://partnerjun.tistory.com/109</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두개 함수는 모두 파라미터와 반환 값이 문자열이다. 그렇기 때문에 &lt;i&gt;boolean&lt;/i&gt;이나 &lt;i&gt;undefined&lt;/i&gt;를 입력했을 때 아무런 오류 없이 문자열 &quot;&lt;i&gt;true&quot;&lt;/i&gt; 나 &quot;&lt;i&gt;undefined&quot;&lt;/i&gt;를 입력한 결과가 반환된다. 이 특징은 encode(decode)URIComponent 함수에서 특히 두드러진다. 웹 개발을 하다보면 쿠키에 저장된 값을 읽어 처리해야 하는 경우가 많다. 함수의 입력과 반환 값에 대해 고민하지 않는다면 무심결에 아래와 같은 코드를 작성하기 쉽다.&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;721&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIK5aI/btsJe6hNWF8/PKyZSKIzrudL2FWSh031jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIK5aI/btsJe6hNWF8/PKyZSKIzrudL2FWSh031jK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIK5aI/btsJe6hNWF8/PKyZSKIzrudL2FWSh031jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIK5aI%2FbtsJe6hNWF8%2FPKyZSKIzrudL2FWSh031jK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;62&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;위 로직은 꺼내 사용하는 &lt;i&gt;key&lt;/i&gt;라는 쿠키가 정의되어있지 않을 때 문제가 발생한다. &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;269&quot; data-origin-height=&quot;71&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djJYse/btsJd3l9RUG/pfJK6bodkkyIsE9P1GKsW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djJYse/btsJd3l9RUG/pfJK6bodkkyIsE9P1GKsW1/img.png&quot; data-alt=&quot;decodeURI의 반환값은 &amp;quot;문자열&amp;quot;이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djJYse/btsJd3l9RUG/pfJK6bodkkyIsE9P1GKsW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjJYse%2FbtsJd3l9RUG%2FpfJK6bodkkyIsE9P1GKsW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;269&quot; height=&quot;71&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;269&quot; data-origin-height=&quot;71&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;decodeURI의 반환값은 &quot;문자열&quot;이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시처럼 &lt;i&gt;undefined&lt;/i&gt;를 파라미터로 입력하면 문자열 &lt;i&gt;&quot;undefined&quot;&lt;/i&gt;가 반환되기 때문이다. 이 값을 다시 쿠키에 쓰지 않았다면 한 숨 돌릴 수 있지겠만, 쿠키에 사용했다면 한동안 &lt;i&gt;&quot;undefined&quot;&lt;/i&gt;로 등록된 값을 처리해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. atob와 btoa&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 함수는 문자열을 base64로 인코딩/디코딩 하는 함수다. 최근들어 다양한 곳에 base64가 사용되고 있는데, 이미지/비디오 문자열화뿐 아니라 간단한 난독화에도 사용하기 좋기 때문이다. 아무튼 이 두 함수의 특이한, 그리고 신경써야 할 점은 바로 이름이다. a to b라는 단어를 보았을 때, 우리는 흔히 a를 ascii 혹은 AB테스트의 A(비교군) 등으로 생각한다. 하지만 이 함수는 반대로 동작한다. a가 base64이며, b가 non-base64로 디코딩 된 문자열이다. 직관적이지 않아 헷갈릴 수 있지만 문제는 위에서 언급했듯 이 함수의 입력 - 출력은 모두 문자열이라는 것이다.&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;240&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyEU0/btsJdIoXNVb/KaJhcuCQUkKZYKIgZCgiP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyEU0/btsJdIoXNVb/KaJhcuCQUkKZYKIgZCgiP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyEU0/btsJdIoXNVb/KaJhcuCQUkKZYKIgZCgiP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkyEU0%2FbtsJdIoXNVb%2FKaJhcuCQUkKZYKIgZCgiP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;240&quot; height=&quot;98&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에, 위 이미지처럼 반대로 사용하더라도 아무런 오류를 발생시키지 않을 수 있다. 이는 잠재적인 위험으로 남을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. encode(decode)URI와 encode(decode)URIComponent의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가이드에서 무조건적으로 &lt;i&gt;encode(decode)URIComponent&lt;/i&gt;를 사용하도록 하고 있지만, 두개 함수의 차이를 알아두는 것이 좋다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NxU6u/btsJeayu0WI/Wug7rnxnmoaiC5XKVW48b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NxU6u/btsJeayu0WI/Wug7rnxnmoaiC5XKVW48b0/img.png&quot; data-alt=&quot;말로 설명할 필요가 없다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NxU6u/btsJeayu0WI/Wug7rnxnmoaiC5XKVW48b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNxU6u%2FbtsJeayu0WI%2FWug7rnxnmoaiC5XKVW48b0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;716&quot; height=&quot;89&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;말로 설명할 필요가 없다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;encode(decode)URI는 URL의 쿼리 파라미터를 위해 인코딩한다. 그렇기 때문에 :// 등의 URL 구문과 알파벳은 인코딩하지 않는다. 실무에서 URL의 쿼리 부분만을 인코딩하는 경우는 드물기 때문에 거의 사용되지 않는 것이다. 하지만 전체 URL 중 쿼리 파라미터만을 인코딩해야 한다면(특히 한글이 포함된 경우) 쿼리 파라미터를 분리하여 직접 처리하는 것보다 더 편하게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 사이트를 둘러보던 중, 쿠키에 undefined가 있어 살펴보니 개요에서 언급한 문제가 있었다. 큰 이슈로 이어지지는 않았지만, 쿠키 만료가 1년이었다는 것이 문제였다. 불편한 &quot; &lt;i&gt;=== 'undefined'&lt;/i&gt; &quot; 구문이 1년은 유지되어야 한다는 것이다.&amp;nbsp;&lt;/p&gt;</description>
      <category>ECMAScript | TypeScript</category>
      <category>encodeURIComponent</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/109</guid>
      <comments>https://partnerjun.tistory.com/109#entry109comment</comments>
      <pubDate>Wed, 31 Jul 2024 21:03:04 +0900</pubDate>
    </item>
    <item>
      <title>2024년 상반기를 보내며</title>
      <link>https://partnerjun.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;벌써 상반기가 지나갔다. 일이 많아서인지 루틴화된 생활 때문인지 시간이 너무 빠르게 흘러가는 것 같다. 반기치고는 꽤 굵직한 일들을 많이 했다. 연말 회고는 평가 자료를 수집할 겸 작성하지만 지금은 평가 기간이 아니다보니 지표나 결과보다는 기억에 남은 것들에 대해 적어두려고 한다. 어떤 작업을 했었고, 아쉬웠던 점이 무엇인지 정도다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 업무&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;문서작업&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;코딩 스타일 가이드&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 방침에 따라 팀 별로 언어나 플랫폼에 대해 코딩 스타일 가이드를 작성하라는 지시가 있었다. 웹 개발이니 그에 맞춰 TypeScript, HTML의 코딩 스타일 가이드를 작성했다. 당연히 처음부터 작성한 것은 아니고, 유명 기업들의 가이드를 참고하여 현재 프로젝트에서 사용하고 있는 것들을 문서화하고자 했다. 실제 프로젝트와 비교하면서 적다 알게 되었는데 프로젝트간의 룰 차이도 크고 오래된 프로젝트는 프로젝트 내에서도 컴포넌트마다 제각각의 스타일로 개발되어 있는 경우가 많았다. 이유는 크게 두가지라고 생각된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;324&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kE5ZR/btsHTH4dZXB/m2qss2MPBr49ovgMDEKRPk/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kE5ZR/btsHTH4dZXB/m2qss2MPBr49ovgMDEKRPk/tfile.svg&quot; data-alt=&quot;lint rule을 고정하는게 중요하다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kE5ZR/btsHTH4dZXB/m2qss2MPBr49ovgMDEKRPk/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkE5ZR%2FbtsHTH4dZXB%2Fm2qss2MPBr49ovgMDEKRPk%2Ftfile.svg&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;180&quot; height=&quot;158&quot; data-origin-width=&quot;324&quot; data-origin-height=&quot;285&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;lint rule을 고정하는게 중요하다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;첫번째는 lint의 적용 시기다. 오래된 프로젝트들은 개발 이후에 lint가 적용되었고, 이미 작성된 코드를 모두 수정할 수는 없으니 lint의 룰을 느슨하게 잡아 그대로 유지되었을 가능성이 높아 보인다. 두번째는 담당자의 변경이다. 당연하게도 많은 손을 탄 프로젝트일수록 각자의 개성, 습관이 남아있게 된다. 이런 것들은 lint에 걸리지 않지만(때로는 이 때문에 룰을 제외한다) 코드를 보는 사람에게 혼란을 준다.&lt;br&gt;코딩 스타일 가이드 문서를 작성하다 보니 문서보다는 lint나 주석이 우선이 아닐까 하는 생각이 더 강해졌다. 문서는 유실되기 쉽지만 프로젝트의 코드는 그대로 유지될테니까.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;서스테이닝&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;후기 요약(GPT) 위치 변경&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;AB 테스트 끝에 숙소 상세 영역 최상단에 넣었던 후기 요약의 위치를 변경했다.(&lt;span style=&quot;color: #333333;&quot;&gt;나 개인의 결정은 아니지만) &lt;/span&gt;말도 많고 탈도 많았던 그 영역을 치웠다는 점에서 굉장히 만족스럽다. 재미있는 점은 LLM를 이용해서 만든 이 &lt;b&gt;요약 컨텐츠&lt;/b&gt;는 긍정적인 지표가 딱히 없었다는 것이다. 기능 런칭 극초기를 제외하면 매출이나 pv, 체류 시간 등에 긍정적인 영향이 거의 없었다. 내가 생각한 가장 큰 이유는 많은 사람들은 글을 잘 읽지 못하게 되어 버렸다는 것이다. LLM의 특성상 글이 길수록 효과적이지만 길면 읽지 않는다(못한다). 짧게 요약하면 의미가 없고, 길게 요약하면 읽지 않는다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq9sJF/btsHRLgngRt/js0MfbLNVywbZjkjXOrjjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq9sJF/btsHRLgngRt/js0MfbLNVywbZjkjXOrjjk/img.png&quot; data-alt=&quot;주기적으로 욕먹어서 수명이 좀 늘었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq9sJF/btsHRLgngRt/js0MfbLNVywbZjkjXOrjjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq9sJF%2FbtsHRLgngRt%2Fjs0MfbLNVywbZjkjXOrjjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;439&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주기적으로 욕먹어서 수명이 좀 늘었다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;대신 나쁜 점은 아주 명확하다. 좋지 않은 요약이 메인에 뜬다면 업주는 굉장한 스트레스를 받는다. 10명 중 1명만이 나쁜 경험에 대한 후기를 남겼다 하더라도(심지어 그게 사실인지 아닌지도 모른다) LLM은 그 후기를 중점으로 요약 할 수 도 있다(&lt;i&gt;할 수 도&lt;/i&gt; 라는 점이 더 큰 문제다). 업주는 영업 담당자에게 불만을 토로할 수 밖에 없고 영업 담당자는 &lt;i&gt;&quot;이러이러한 일이 있었다&quot;&lt;/i&gt; 정도의 의견을 전달하는 방법 밖엔 없다. (ONOFF 기능이 있었지만 모두가 설정해서 제외했다) 심지어 그 의견을 들어도 정치적으로 얽힌 것이 많아 위치를 바꾸는데도 이렇게 긴 시간이 걸렸다. 조만간 아예 다른 지면으로 치워버릴 것 같은데 없어지면 또 아쉬울 것 같다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;해외 숙소 sitemap.xml 추가&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;숙소 수가 너무 많아 데이터 인덱싱에 문제로 뒤늦게 추가했다. sitemap 파일 내에 정의할 수 있는 URL의 개수 제한이 있어 조건을 정하고 파일로 생성, 압축하여 업로드하는 식으로 해결했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rOTJB/btsHSr2AFra/3ZeX6IkZhkbaEqmwzyV5vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rOTJB/btsHSr2AFra/3ZeX6IkZhkbaEqmwzyV5vK/img.png&quot; data-alt=&quot;페이지가 너무 많아 파일로 나누었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rOTJB/btsHSr2AFra/3ZeX6IkZhkbaEqmwzyV5vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrOTJB%2FbtsHSr2AFra%2F3ZeX6IkZhkbaEqmwzyV5vK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;350&quot; height=&quot;349&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;페이지가 너무 많아 파일로 나누었다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;최종적으로는 인덱싱된 페이지가 40만개에서 100만개 이상으로 늘어났다. 그에 따라 &lt;span style=&quot;color: #EE2323;&quot;&gt;검색 결과 노출이 20만에서 25만 정도로 약 25%정도 상승&lt;/span&gt;했다. 구글에서 이정도이니 네이버에서도 비슷한 정도로 상승하지 않았을까 예상하고 있다. 다만 여전히 아쉬운 것은 평균 검색 결과 순위가 10위대로 너무 낮다는 것이다. 현재로써는 API 응답 속도나 각종 기능으로 인한 코드 자체의 무거움으로 순위를 끌어올리는데 한계가 있다.(물론 압도적인 페이지 진입이 있다면 오르겠지만)&amp;nbsp;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/rendering/server-components&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Server Component&lt;/span&gt;&lt;/a&gt;를 사용한다던지 SEO 툴을 이용한 점검 등이 필요한 시점이다. 그나마 지금 당장 손쉽게 가능한 것은 google structured data를 추가하는 것이다. 해외쪽은 다른 작업에 밀려 아직까지도 추가하지 못했는데 이 작업이 된다면 순위가 조금 오르지 않을까 기대하고 있다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;불필요한 코드 제거&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;별거 아닌 것처럼 보이지만 이건 생각보다 어려운 일이다. 전통적인(이라기엔 얼마 안 됐지만) 컴포넌트 아키텍처링인 도메인/용도에 따라 묶는 방식이라면 간단하게 삭제해 나가면 된다. 하지만 atomic 패턴은 문제가 커진다. 사용하지 않는 컴포넌트의 확인이 어렵다. 그리고 사용한다고 하더라도 &lt;b&gt;논리적으로&lt;/b&gt;&amp;nbsp;노출되지 않는 경우도 많다. 예를 들면 페이지 이관이 그렇다. 실제로 페이지는 남아있지만 라우팅 단계에서 다른 페이지/사이트로 리다이렉션 시키는 경우다. &lt;span style=&quot;color: #8A3DB6;&quot;&gt;참조를 하긴 하는 코드이기에 트리쉐이킹 단계에서 제거되지 않아 성능에도 악영향&lt;/span&gt;을 미친다. &lt;a href=&quot;https://partnerjun.tistory.com/104&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;이전 회고에&lt;/span&gt;&lt;/a&gt; 적은 것처럼, 이런 경우 코드 그래프 생성 툴을 이용하거나 IDE의 inspection을 이용해 사용되지 않는 코드를 제거한다. 귀찮아서 기록으로 남겨두지는 않았지만 이 작업으로 청크 파일의 크기가 꽤나 줄었고 JS 실행 속도에도 영향을 미쳤다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;회원 등급제 적용&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;난 이 작업을 진행하며 놀랐다. &lt;i&gt;아직도 회원 등급이 없었다고?&lt;/i&gt; 내 입장에서는 특별히 할 일이 많지는 않은 작업이었다. 단순히 회원 특가라는 배지를 노출하는 정도였다. 이게 생각보다 효과가 좋았는지 연계된 작업들이 계속 논의되고 있다. 이번 작업보다는 다음 작업들이 많을 것이라 생각된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1CFr3/btsHRoTjcdh/skih6jCZSPsicrnKxXRof0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1CFr3/btsHRoTjcdh/skih6jCZSPsicrnKxXRof0/img.png&quot; data-alt=&quot;사원 특가도 좀...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1CFr3/btsHRoTjcdh/skih6jCZSPsicrnKxXRof0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1CFr3%2FbtsHRoTjcdh%2Fskih6jCZSPsicrnKxXRof0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사원 특가도 좀...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;모노레포 이관&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;유행에 따르기 위해서인지 &quot;실 단위&quot;의 모노레포로 이관하는 작업이 있었다. 각자 담당하던 프로젝트를 pnpm + turbo로 관리하는 프로젝트로 이관했다. 배포와 관련된 몇가지 사소한 문제가 있었지만 큰 어려움은 없었다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z45Ip/btsHSqCCyBJ/oXK0cFidRanjKyK64BQuwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z45Ip/btsHSqCCyBJ/oXK0cFidRanjKyK64BQuwK/img.png&quot; data-alt=&quot;프로젝트는 pnpm으로 관리된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z45Ip/btsHSqCCyBJ/oXK0cFidRanjKyK64BQuwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz45Ip%2FbtsHSqCCyBJ%2FoXK0cFidRanjKyK64BQuwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;225&quot; height=&quot;160&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트는 pnpm으로 관리된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;개인적으로는 FE는 모노레포가(TBD가) 어울리지 않는다고 생각한다. 이유가 몇가지 있는데, 첫번째로는 package-lock 파일이다. FE는 다양한 이유(특히 개발 단계에서)로 패키지 참조가 변하게 된다. 참조된 패키지가 변할 때마다 lock 파일이 변경되고 충돌이 일어난다. 브랜치마다 lock 파일이 다른 경우가 많고, 배포조차도 ci 명령어를 이용하기 때문에 lock 파일이 제대로 생성된 파일이 아니라면 배포도 불가능하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두번째는 배포 시기의 조정이다. 모노레포는 TBD를 기본으로 하는데, 이는 &lt;u&gt;항상 배포되어도 되는 API 백엔드에서 유리&lt;/u&gt;한 방식이다. UI/UX의 변경은 대부분 그렇지 않다. AB 테스트 혹은 플래그로 노출 여부를 결정한다면 그나마 낫지만(이것도 코드 용량이 커져 비효율적이다) 테스트 없이 QA 이후 바로 배포해야 하는 경우 브랜치를 따로 관리해야 한다. &lt;i&gt;QA 서버에 배포하는 브랜치와 &quot;지금&quot; 배포를 위한 브랜치&lt;/i&gt;. 브랜치 관리 포인트가 늘어났을 뿐더러 메인 브랜치가 변경될 때마다 QA 브랜치의 lock 파일과 충돌이 발생한다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;세번째로는 공용 패키지다. UI 구성을 위한 패키지는 npm이나 nexus 등을 이용해 배포하고 버전 관리하는 것이 기본이다. 하지만 공용 패키지를 모노레포 안에 옮긴다면 버전 관리에 문제가 발생한다.&lt;br&gt;예를 들어 아이콘 컴포넌트가 있고 파라미터로 아이콘의 이름을 입력하는 컴포넌트가 있다고 치자. 아이콘들이 추가/변경되어 호출하는 이름도 변경해야 할 때, 공용 패키지가 작업되었다고 곧바로 병합한다면 작업되지 않은 프로젝트에는 아이콘이 없거나 일부만 변경되는 등 문제가 발생한다. 기존 아이콘을 남겨도 문제고, 새로운 아이콘으로 한번에 바꿔버려도 크기나 색상 등 문제가 생길 확률이 높아 전수 조사가 필요해진다. 이런 경우는 공용 패키지 작업자가 한번에 모두 처리해야 하겠지만, 여러 지면, 혹은 여러 사이트에서 참조하는 것을 변경하는 작업에는 큰 리소스가 들어간다. 심지어 일정이 정해져 있다면 메인 브랜치로의 병합 자체를 미루는 수 밖에 없다.&lt;br&gt;&amp;nbsp;&lt;br&gt;네번째로는 용량 측면에서의 문제다. 실 단위 모노레포에 많은 프로젝트를 넣어버리니 용량이 어마어마하게 커진다. 개발 기기의 성능이 향상되었다곤 하지만 코드를 검색하기 위해 매번 범위를 지정해야 한다. 지정하지 않으면 모든 프로젝트에서 코드를 찾기 시작하니까. 이를 해결하려고 sparse checkout을 하곤 한다. 하지만 브랜치가 분리되거나 정작 필요할 때 또다시 checkout 받아야 하는 등 한계가 있다. 심지어 배포할 때도 마찬가지다. 흔히 젠킨스 서버에서 checkout을 받는데 이러면 너무 많은 파일을 받게 된다. 더군다나 pnpm-turbo로 빌드하면 필요 없는 패키지도 빌드하여 시간이 더 소요된다(캐시 설정은 문제 발생 여지가 많아 더 골치아프다)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EccXZ/btsHSxnW3LZ/1JDYkbiY1iQMwv42nFFA8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EccXZ/btsHSxnW3LZ/1JDYkbiY1iQMwv42nFFA8k/img.png&quot; data-alt=&quot;일정적 여유가 있는 회사에서나 가능하지 않을까&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EccXZ/btsHSxnW3LZ/1JDYkbiY1iQMwv42nFFA8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEccXZ%2FbtsHSxnW3LZ%2F1JDYkbiY1iQMwv42nFFA8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;일정적 여유가 있는 회사에서나 가능하지 않을까&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;주저리 주저리 적었지만 물론 해결할 수 없는 문제는 없다. 하지만 FE에 어울리는지 고민했었는지 의문스럽기도 하고, 의도 자체가 불순해보인다. 실제로 커밋 횟수가 어쩌고 하고 있기도 하고...&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로모션 - 놀먹보&lt;/h3&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y8fYc/btsHTmTxChk/zJQVNAtaKyeYUOydwKami0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y8fYc/btsHTmTxChk/zJQVNAtaKyeYUOydwKami0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y8fYc/btsHTmTxChk/zJQVNAtaKyeYUOydwKami0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY8fYc%2FbtsHTmTxChk%2FzJQVNAtaKyeYUOydwKami0%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;600&quot; height=&quot;598&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇가지 업체들과 연계하여 서로의 사이트에서 쿠폰을 발급받는(주고받는?) 프로모션 페이지와 그 페이지에서 요청하는 BFF 서버(API aggregation)를 개발했다. 생각보다 어렵지 않겠거니 했지만 기술 외적인 문제가 꽤 많았다. 그 중 가장 고통스러웠던 에셋 관리다. 50여개 이상의 이미지가 들어가다 보니 구분할 수 있게 파일명도 바꾸고 디렉토리로 정리해서 버킷에 업로드했다. 여기까진 괜찮았지만... 개발 기간 중 이미지가 계속 변경되어 몇번이나 업로드하려니 분노가 머리 끝까지 치밀어 올랐다. 같은 이미지를 몇 번이나 다시 올린건지...&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AslJQ/btsHSuETtvE/kImvjhPjWyA0QN48tui39K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AslJQ/btsHSuETtvE/kImvjhPjWyA0QN48tui39K/img.png&quot; data-alt=&quot;그만바꿔&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AslJQ/btsHSuETtvE/kImvjhPjWyA0QN48tui39K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAslJQ%2FbtsHSuETtvE%2FkImvjhPjWyA0QN48tui39K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그만바꿔&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;API 사용에도 아쉬움이 있었다. 기존 사용하던 API를 어떻게든 재사용하자는 의견이 모였고, 새로운 기능을 제공하려다 보니 내가 직접 BFF 서버에 이벤트 참여 시간, 날짜 구분이나 에러 메시지 처리 등 별 잡다한 기능들을 추가해야만 했다. 여러 업체들과 연계하는 프로젝트라는 것도 꽤 큰 관리 포인트였다. 업체별 이벤트 페이지가 당일에 정상적으로 작동하지 않는 것은 물론이고, 쿠폰 발급에 문제가 있어 담당자들이 진땀을 빼기도 했다. 업체 문제로 이미 광고비를 집행했던 푸시를 못 보내기도 했는데 어떻게 해결했는지는 모르겠다.&lt;br&gt;URL 인코딩과 관련되어 꽤 골치아픈 문제도 있었다. 앱의 딥링크는 특정 URI 파라미터를 추가 입력했을 때 웹뷰로 보여주는 기능이 있는데, 아래 같은 경우에 문제가 발생했다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 기본 URL. 
// IOS는 google.com 이후의 &quot;?name=&quot; 부터 인식하지 못한다.
testApp://webview?title=테스트&amp;amp;url=https://www.google.com?name=google

// URL 부분 인코딩
testApp://webview?title=테스트&amp;amp;url=https%3A%2F%2Fwww.google.com%3Fname%3Dgoogle&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;u&gt;딥링크 자체가 쿼리 파라미터 룰을 따르고 있기 때문에&lt;/u&gt; 쿼리 파라미터로 전달되는 URL에도 쿼리 파라미터가 있는 경우 구문이 모호해진다. 이를 해결하기 위해 URL만 인코딩하여 전달해야 했는데 외부 링크 담당자가 이 구문을 인코딩하지 않고 삽입한다던가, 업체에서 이미 인코딩된 URL을 사용하는 경우 링크에 문제가 발생했다. 특히 모바일에서 다른 URL을 사용하는 경우는 파악하기 어렵다보니 찾는데도 꽤나 긴 시간이 걸렸다.&lt;br&gt;이런저런 이슈가 있긴 했지만 다 지나고 나니 생각보다 재미있었던 프로젝트였다. 일단 여기저기 마케팅 자료를 뿌려 검색하면 내가 만든 페이지가 뜨는 것도 재미있고, 제공하는 상품이 꽤 많다보니 커뮤니티 사이트에서 반응을 보는 것도 재미있었다. 물론 난 출석도 한두번 하고 안해서 받은게 없다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brEpzl/btsHRMGkeHj/Bud98Q52GiXQsL1S4HHzPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brEpzl/btsHRMGkeHj/Bud98Q52GiXQsL1S4HHzPK/img.png&quot; data-alt=&quot;누르면 고장날까봐 안누름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brEpzl/btsHRMGkeHj/Bud98Q52GiXQsL1S4HHzPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrEpzl%2FbtsHRMGkeHj%2FBud98Q52GiXQsL1S4HHzPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;누르면 고장날까봐 안누름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;DAU나 신규, 이탈 회원 등 성과 지표는 목표대비 90% 남짓으로 큰 성공을 거두었다고 보기는 어려울 수 있지만, 업체들로부터 좋은 반응을 얻어 아마 지속적으로 진행할 것 같다고 공유받았다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Protobuf 로그 자동화&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이건 이전에 &lt;a href=&quot;https://partnerjun.tistory.com/107&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;글을 썼던 내용&lt;/span&gt;&lt;/a&gt;이다. protobuf로 작성된 통계(클라이언트 로그) 명세를 각 언어별 전송 함수로 생성되도록 일종의 툴을 만들었다. 생각보다 결과물이 만족스러웠다. 의도한대로 타입 체크도 명확하게 이루어지고 실제로 입력해야 하는 값도 많이 줄어들었다. 기존에는 고정된 문자열을 계속 입력하면서 이게 뭐하는 짓인가 싶기도 했는데 그런 면에서 모두가 편해졌다고 믿는다.&lt;br&gt;평가 측면에서는 다른 팀의 구성원들과 이슈를 제기하고 회의하며 맞췄던 과정이 꽤 좋게 평가받고 있는 것 같다. 개인적으로 이런 평가 측면의 중요성보다 다른 플랫폼의 코드를 만들어내는 일종의 &lt;b&gt;크로스 플랫폼 툴 개발 경험&lt;/b&gt;이 유의미하다고 생각된다. 폴리글랏의 유행은 지난 것 같지만 그래도 여러 언어에 대한 친숙함은 큰 강점이 될 것이다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFRhqC/btsHSsUF5tI/VUqiWf83Xqu0y5xRgfM2M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFRhqC/btsHSsUF5tI/VUqiWf83Xqu0y5xRgfM2M1/img.png&quot; data-alt=&quot;물론 아직도 왜 protobuf를 썼는지는 모름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFRhqC/btsHSsUF5tI/VUqiWf83Xqu0y5xRgfM2M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFRhqC%2FbtsHSsUF5tI%2FVUqiWf83Xqu0y5xRgfM2M1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;200&quot; height=&quot;200&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;물론 아직도 왜 protobuf를 썼는지는 모름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&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;상반기에는 팀 외부의 인원과 논의하고 맞춰나가는 작업이 많았다. 여러 측면에서 큰 도움이 되는 것을 느끼지만 그만큼 기술 측면에 관심이 줄어들어 가는 것을 느낀다. 기술은, 특히 변화가 빠른 FE는 계속 관심을 가지고 학습해야 하지만... 쉽지 않다.&lt;br&gt;여러 취미를 가져보려고 노력하고 있다. 경량비행기 자격증을 목표로 삼고 운전 연수를 했고, 얼마 전 조종 체험을 했다. 상당히 재미있었지만 자격증을 따기까지 최소 600만원이 들어간다고 하여 고민중이다. 이 돈이면 여행을 가도 몇 번을 간다는 생각이 든다. 이어서 공 3개 저글링에 도전해보려 한다. 10여년 전 한발 자전거는 결국 실패했었지만 이건 꽤 가능성이 보인다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>회고</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/108</guid>
      <comments>https://partnerjun.tistory.com/108#entry108comment</comments>
      <pubDate>Sun, 9 Jun 2024 12:57:09 +0900</pubDate>
    </item>
    <item>
      <title>protobuf를 이용한 &amp;quot;자동 함수 생성&amp;quot; 프로젝트</title>
      <link>https://partnerjun.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전부터 강조했듯 FE는 필연적으로 통계(클라이언트 로그)와 친숙해질 수 밖에 없다. 이번 상반기의 주요 프로젝트는 이 클라이언트 로그와 관련된 프로젝트로 내가 소속된 웹 팀 뿐 아니라 앱 팀, 그리고 다른 부서의 웹 팀까지 사용하는 것을 목표로 하는 프로젝트를 진행했다. 간단하게 말하자면 protobuf 정의를 이용해 Java, Kotlin, Swift, Typescript 4개 언어의 소스 파일을 생성하는 프로젝트였다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxaIZ1/btsHLV27jL9/ybmhsUCDWxzkk3autiA1UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxaIZ1/btsHLV27jL9/ybmhsUCDWxzkk3autiA1UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxaIZ1/btsHLV27jL9/ybmhsUCDWxzkk3autiA1UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxaIZ1%2FbtsHLV27jL9%2FybmhsUCDWxzkk3autiA1UK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;200&quot; height=&quot;200&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;다니고 있는 회사에는 통계와 관련된 작업을 하는 DIA팀이 있다. DIA팀은 상반기 프로젝트로 &lt;b&gt;엑셀로 작성하던 클라이언트 로그 명세서&lt;/b&gt;를 protobuf로 작성하는 프로젝트를 진행했다. 많은 팀이 참조하는 클라이언트 로그 정의가 변하면 업무 프로세스도 변할 수 밖에 없다. 왜 하필 상속 정의가 지원되는, 명세의 한계가 명확한 protobuf를 선택했는지는 모르겠지만(서버간 통신을 하지도 않고 심지어 유효성 검사는 JAVA로 작성했다!)... 자세한 내막은 모른다. 다만 앞으로 귀찮아 지겠거니 싶었다. 예상대로 얼마 지나지 않아 귀찮은 작업이 할당됐다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t2Xpm/btsHERt5XzG/UZKlRHlqNmlYOBkIFRmKH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t2Xpm/btsHERt5XzG/UZKlRHlqNmlYOBkIFRmKH0/img.png&quot; data-alt=&quot;또냐&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t2Xpm/btsHERt5XzG/UZKlRHlqNmlYOBkIFRmKH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft2Xpm%2FbtsHERt5XzG%2FUZKlRHlqNmlYOBkIFRmKH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;또냐&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;protobuf로 정의된 파일에는 보내야 하는 &lt;b&gt;값(로그)의 타입 정의가 되어&lt;/b&gt; 있으니 이를 각 플랫폼의 함수로 생성하고, 그 함수를 호출하는 것만으로 클라이언트 로그를 전송할 수 있게 하자는 것이다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;html&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;/* 
* 예시 코드
* 아래 버튼을 클릭하면 { value: 1234, test: 'test' } 와 같은 값이 전송된다. (test 필드는 함수 내에 선언)
*/
&amp;lt;Button onClick={Log.button.click({ value: 1234; })}&amp;gt;
&amp;nbsp;&amp;nbsp;클릭
&amp;lt;/Button&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;생각보다 사용자의 입력에 따라 변하는 값이 적기 때문에 위처럼 호출할 수 있게 된다면 관리가 편해진다는 것은 명백하다. 항상 고정된 값을 보낸다면 함수 내에 static하게 정의해둘 수 있고, 특히 가장 많은 실수를 유발하는 &lt;b&gt;타입 유효성&lt;/b&gt;이 해결된다. 예를 들자면 number 타입으로 로그를 정의했지만 개발자가 신경쓰지 않아 string으로 전달되고 있는 경우다.&amp;nbsp;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&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;간단하게 보자면 proto, textproto(txtpb)로 작성된 클라이언트 로그 정의를 읽어내 각 언어별로(java, swift, kotlin, typescript) 로그 전송 함수가 정의된 파일을 생성하는 것이 목표다. 머릿속으로 생각해 보았을 때 크게 어렵지 않다. 템플릿을 작성해 두고, 전송해야 하는 로그의 각 필드의 이름과 타입을 읽어와 작성된 템플릿으로 파일을 만들면 된다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diGflz/btsHKqcoOE5/umykK78hXPOqoaDE8rIfxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diGflz/btsHKqcoOE5/umykK78hXPOqoaDE8rIfxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diGflz/btsHKqcoOE5/umykK78hXPOqoaDE8rIfxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiGflz%2FbtsHKqcoOE5%2FumykK78hXPOqoaDE8rIfxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;진행과정&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;POC와 언어 선택&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;protobuf의 주요 구성 요소인&amp;nbsp;proto와 txtpb는 go 언어로 작성되어 있다. 그렇다보니 자연스럽게 go로 poc를 진행했다. 하지만 금새 문제를 발견했다. &lt;u&gt;proto3가 상속 정의를 지원하지 않아 일종의 트릭&lt;/u&gt;을 사용했는데, 이 때문이다. 로그의 필드 값을 message로 선언한 것 까지는 좋지만 각 로그별로 message를 정의하고 구조체의 이름을 입력한 것이다. 아래 예시를 보면 뭔가 묘한 것을 느낄 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class=&quot;go&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;// txtpb 파일

events {
  object_name: &quot;TestTarget&quot;
  object_type: OBJECT_TYPE_UNSPECIFIED
  event_type: EVENT_TYPE_CLICK
  custom_json_name: &quot;TestDetail&quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&quot;go&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;// proto 파일

syntax = &quot;proto3&quot;;

message Events {
  string object_name = 1;
  ObjectType object_type = 2;
  EventType event_type = 3;
  optional string custom_json_name = 4;
}

message TestDetail {
  string name = 1;
  int64 age = 2;
  repeated string favorite = 3;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;로그의 기본 형식으로 Events라는 메시지를 정의해 놓고, 로그의 상세 값에 해당하는 message의 이름(&lt;i&gt;custom_json_name&lt;/i&gt;)을 문자열로 정의해둔 것이다. &lt;i&gt;custom_json_name&lt;/i&gt;에 다른 메시지의 이름을 입력하면 다른 필드를 입력받는 &quot;다른 로그&quot;의 정의가 되는 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqQufE/btsHFNLbpp6/8WGNZJfrshXmTc25XZr0k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqQufE/btsHFNLbpp6/8WGNZJfrshXmTc25XZr0k1/img.png&quot; data-alt=&quot;&amp;quot;import&amp;quot; 한 것이 아니라 이름을 입력했다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqQufE/btsHFNLbpp6/8WGNZJfrshXmTc25XZr0k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqQufE%2FbtsHFNLbpp6%2F8WGNZJfrshXmTc25XZr0k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&quot;import&quot; 한 것이 아니라 이름을 입력했다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;i&gt;custom_json_name&lt;/i&gt;으로 정의한 구조체의 이름을 단순 문자열로 입력했으므로 Events와 TestDetail의 &lt;b&gt;프로그래밍적인 연결이 없다&lt;/b&gt;(논리적인 연결만이 있다). 그렇기 때문에 dynamic import가 불가능한 컴파일 언어인 go로 txtpb 파일을 읽으면 &lt;span style=&quot;color: #EE2323;&quot;&gt;TestDetail이 정의된 proto 파일은 읽지 않는 것&lt;/span&gt;이다. 파일 스캔 등 다소 무리한 방법으로 메모리에 올리더라도 go의 리플랙션으로는 원하는 정도로 필드들을 뽑아낼 수 없었다. 특히 치명적인 것은 프로그래밍적인 연결이 없어 &lt;span style=&quot;color: #333333;&quot;&gt;protobuf에서 지원하는 각 언어별 정의로 전환하는 protoc로도 모든 타입을 추출해니지 못한다는 것이다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;결국 이 프로젝트에서 장점을 보이지도 않는 go는 선택지에서 제외되었다(딱히 익숙하지도 않고). dynamic import가 가능하며 proto와 txtpb 파일을 읽어낼 수 있는 언어는 node(ts)와 python이 남는다. 유지보수는 FE 개발팀이 하게 될 것이 명백하고 어떤 타입이 들어올지, 앞으로 어떤 변화가 있을지 장담할 수 없어 타입 정의에서 더 자유롭고 라이브러리가 넘쳐나는 node를 선택했다. 심지어 정 안되겠다 싶으면 라이브러리를 대충 만들어서 써도 되니까.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;개발&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;작성해야 하는 프로그램의 절차는 아주 명확했다.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;proto, txtpb가 정의된 깃 리포지토리를 받아온다.&lt;/li&gt;&lt;li&gt;proto로 작성된 structure, enum 등 정의를 메모리에 올린다(타입으로써의 정의)&lt;/li&gt;&lt;li&gt;txtpb에 작성된 실제 &quot;클라이언트 로그&quot; 명세를 모아 객체화한다.&lt;/li&gt;&lt;li&gt;객체화한 클라이언트 로그 명세를 각 언어별 전송 함수 정의로 변환한다.&lt;/li&gt;&lt;li&gt;함수 정의를 파일로 생성한다.&lt;/li&gt;&lt;li&gt;생성된 파일에 lint를 적용하여 문법적 오류를 확인하고 코드 스타일을 보정한다.&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;1번은 간단하다. 쉘 파일을 만들고 git clone을 실행하면 됐다. 어차피 옵션을 입력 받아야 하니 쉘 파일 작성은 당연했다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdXmCu/btsHKQuQI9Y/cQhKU0zMJR7NlIojVNWa6K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdXmCu/btsHKQuQI9Y/cQhKU0zMJR7NlIojVNWa6K/img.gif&quot; data-alt=&quot;오랜만에 쉘 파일을 작성하자니 매우 고통스러웠다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdXmCu/btsHKQuQI9Y/cQhKU0zMJR7NlIojVNWa6K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bdXmCu/btsHKQuQI9Y/cQhKU0zMJR7NlIojVNWa6K/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;오랜만에 쉘 파일을 작성하자니 매우 고통스러웠다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;2번부터 고민이 시작됐다. protobuf 파일을 읽어올 수 있는 라이브러리가 있을까?&lt;br&gt;당연히 있다. &lt;a href=&quot;https://protobufjs.github.io/protobuf.js/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;protobufjs&lt;/span&gt;&lt;/a&gt; 라는 라이브러리다. txtpb를 지원하지 않는다거나, comment 추출시 오류가 발생한다거나 하는 문제가 있었지만 이런 문제를 해결할 수 있게 node를 플랫폼으로 선정했었다. 하지만 txtpb는 명쾌한 답이 없었다. txtpb를 ts로 읽어내는 케이스가 너무 드물다보니 메인터넌스 되고 있는 라이브러리가 발견되지 않았다.&lt;br&gt;결국 아주 오래된 라이브러리를 사용할 수 밖에 없었다. &lt;a href=&quot;https://github.com/chaosmail/prototxt-parser#readme&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;prototxt-parser&lt;/span&gt;&lt;/a&gt;라는 라이브러리인데, proto2 시절에 작성되었는지 proto3에 추가된 기능들이 지원되지 않았다. 특히 치명적인 것은 인라인 주석으로 작성된 내용이 있으면 파일을 읽어올 때 오류가 발생하는 것이다. 직접 라이브러리를 작성하거나 수정하기엔 시간이 부족했다. 주석을 떼었다 붙이는 식으로 해결했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;249&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdPDIT/btsHLeIYz3R/iQ2NBuA9R2B0BkslTGQLbk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdPDIT/btsHLeIYz3R/iQ2NBuA9R2B0BkslTGQLbk/img.webp&quot; data-alt=&quot;문제는 없다. 문제는.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdPDIT/btsHLeIYz3R/iQ2NBuA9R2B0BkslTGQLbk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdPDIT%2FbtsHLeIYz3R%2FiQ2NBuA9R2B0BkslTGQLbk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;249&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;249&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;문제는 없다. 문제는.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이제 3번이다. 어떻게든 읽어온 txtpb의 정의를 객체화한다. 위에서 본 것과 같이 txtpb 파일은 아래처럼 정의되어 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// txtpb/testTarget.txtpb

# 테스트 로그임
events {
&amp;nbsp;&amp;nbsp;object_name: &quot;TestTarget&quot;
&amp;nbsp;&amp;nbsp;object_type: OBJECT_TYPE_UNSPECIFIED // types/objectType.proto에 정의된 enum 값
&amp;nbsp;&amp;nbsp;event_type: EVENT_TYPE_CLICK // types/eventType.proto에 정의된 enum 값
&amp;nbsp;&amp;nbsp;custom_json_name: &quot;TestDetail&quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이 파일에서 참조하는 message는 아래처럼 정의되어 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// proto/testDetail.proto

syntax = &quot;proto3&quot;;

message TestDetail {
&amp;nbsp;&amp;nbsp;# 작성자 이름
&amp;nbsp;&amp;nbsp;string name = 1;
&amp;nbsp;&amp;nbsp;# 작성자 나이
&amp;nbsp;&amp;nbsp;int64 age = 2;
&amp;nbsp;&amp;nbsp;# 취미
&amp;nbsp;&amp;nbsp;repeated string favorite = 3;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;events로 정의된 로그와 &quot;TestTarget&quot; message 정의를 함수로 만들기 위해 데이터를 모아 객체화한다. 로그에서 참조하고 있는 enum과&lt;i&gt;(위 예시에서의 object_type, event_type)&lt;/i&gt; message, 각 파일의 위치, 그리고 작성된 주석 엄청나게 많은 데이터를 포함한다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;/*
* 템플릿 엔진으로 전달되는 객체의 간단한(?) 예시. 실제로는 훨씬 많은 정보가 포함되었다.
*/

{
&amp;nbsp;&amp;nbsp;&quot;importModules&quot;: [ // 템플릿 파일에서 import 해야 하는 값
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;types/objectType.ts&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;types/eventType.ts&quot;,
&amp;nbsp;&amp;nbsp;],
&amp;nbsp;&amp;nbsp;&quot;fileName&quot;: &quot;test.txtpb&quot;, // 이 객체에 해당하는 실제 txtpb 파일
&amp;nbsp;&amp;nbsp;&quot;filePath&quot;: &quot;test/test.txtpb&quot;,
&amp;nbsp;&amp;nbsp;&quot;events&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;TestTarget&quot;: {
&amp;nbsp;&amp;nbsp; 	&amp;nbsp;&amp;nbsp;&quot;githubLinks&quot;: [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;....&quot;, // clone 받은 파일의 위치를 blob으로 전환하여 github 링크를 JSDOC 스타일로 작성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;objectName&quot;: &quot;OBJECT_TYPE_UNSPECIFIED&quot;, // enum 값으로 import하여 사용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;eventType&quot;: &quot;EVENT_TYPE_CLICK&quot;, // enum 값으로 import하여 사용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;customJsonName&quot;: &quot;TestDetail&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;parameters&quot;: { // 입력받아야 하는 값 (testDetail에 정의된 필드)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;string&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;isOptional&quot;: false,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;isArray&quot;: false,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;age&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;number&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;isOptional&quot;: false,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;isArray&quot;: false,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;favorite&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;string&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;isOptional&quot;: false,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;isArray&quot;: true, // 배열 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;나는 이 객체의 데이터가 많을 수록 유리하다고 생각했다. 각 언어별로 요구되는 데이터가 다르기 때문이다. 하지만 아래에서 설명할 템플릿 엔진이 자동완성을 지원하지 않아 아무런 의미가 없어졌다. 뭐가 있는지를 모르는데 어떻게 쓸 수 있겠는가.&lt;br&gt;&amp;nbsp;&lt;br&gt;이쯤 와서 한숨을 돌렸다. 필요한 protobuf 데이터들을 읽어왔고, 객체로 뽑아냈다. 이제 언어별로 템플릿을 작성하고 템플릿 엔진을 이용해 파일로 생성하면 된다. 함수는 실제로 로그를 전송하는 로직이 담긴 함수를 호출하고, 파라미터로 입력받은 값이나 기본 값들을 모두 전달하는 것이다. 각 플랫폼이 모두 동일하게 map 형태로 담아 보내면 되기에 전송 자체는 어려움이 없었다. 다만 플랫폼별(언어별) 템플릿을 모두 작성해야 하니 이게 꽤나 복잡한 일이었다. 언어별 data class나 function, 그리고 optional이나 array 정의 문법을 모두 알아야 하니까.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BBKhr/btsHMAxsFFQ/Mc7PkVsIMAO7JVxLGkWwiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BBKhr/btsHMAxsFFQ/Mc7PkVsIMAO7JVxLGkWwiK/img.png&quot; data-alt=&quot;결국 4개 언어 템플릿을 모두 내가 작성했다!!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BBKhr/btsHMAxsFFQ/Mc7PkVsIMAO7JVxLGkWwiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBBKhr%2FbtsHMAxsFFQ%2FMc7PkVsIMAO7JVxLGkWwiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결국 4개 언어 템플릿을 모두 내가 작성했다!!!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;여기서 좋지 않았던 선택이 있었다. 템플릿 엔진으로 &lt;a href=&quot;https://github.com/mde/ejs&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;EJS&lt;/span&gt;&lt;/a&gt;를 선택한 것이다. EJS는 템플릿 내에서 JS 문법을 사용한다. 그 말인 즉슨, JS에 친숙하지 않은 개발자에게는 벽이 된다는 것이다. 템플릿에 어떤 값이 필요할지 알 수 없으니 템플릿 엔진에서 데이터를 다루도록 EJS를 선택했지만, 어느정도 안정화 되어 템플릿에 필요한 값이 구체화된 이후에는 의미없는 벽만 남게 되었다. 최근 회사에서 목표하는 &lt;i&gt;다른 팀의 작업, 다른 팀의 서비스에 영향을 주는&lt;/i&gt; 개발 문화에 어긋나는 것이다. 아주 간단한 문법만을 지원하는 mustche나 jade 같은 것을 사용했으면 어땠을까 하는 아쉬움이 남는다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;1410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDwYXK/btsHFcrXJ6f/0sqNhAuexD8LSKwlV1qPhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDwYXK/btsHFcrXJ6f/0sqNhAuexD8LSKwlV1qPhk/img.png&quot; data-alt=&quot;템플릿은 대충 이렇게 생겼다. 끔찍하다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDwYXK/btsHFcrXJ6f/0sqNhAuexD8LSKwlV1qPhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDwYXK%2FbtsHFcrXJ6f%2F0sqNhAuexD8LSKwlV1qPhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;452&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;1410&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;템플릿은 대충 이렇게 생겼다. 끔찍하다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;마지막으로는 쉘에서 생성된 파일들에 대해 lint를 추가했다. autofix를 통해 들여쓰기 문법을 고치는 것도 중요하지만 템플릿 엔진 특성상 실수하기 쉬운 문법 문제를 아주 손쉽게 파악하고 해결할 수 있었다. 그런데 주석 줄바꿈시에 들여쓰기가 망가지는 것은 어째선지 4개 언어 모두에서 발견되었다. 심지어 어떻게 수정하는지조차 알 수 가 없었다. 한참을 고민하다 찾아보니 주석의 들여쓰기는 lint의 기본 요소가 아니라는 말이 있었다. 그럼 지금까지 우리가 봐 온 주석 스타일은 prettier의 기능인걸까? 아니면 IDE? 깊게 생각하지 않기로 했다. 쓰는 쪽에서 고치며 되니까.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dl6k0Q/btsHK9ny8VM/LezRnte0bkqBM5EPinqJh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dl6k0Q/btsHK9ny8VM/LezRnte0bkqBM5EPinqJh1/img.png&quot; data-alt=&quot;언어별 lint를 모두 적용해둔 것으로 내 할 일은 다했다...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dl6k0Q/btsHK9ny8VM/LezRnte0bkqBM5EPinqJh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdl6k0Q%2FbtsHK9ny8VM%2FLezRnte0bkqBM5EPinqJh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;언어별 lint를 모두 적용해둔 것으로 내 할 일은 다했다...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발은 끝났지만, 프로젝트가 끝나지는 않았다&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 한번에 끝나는 작업은 아니다. 꾸준한 유지보수가 필요하다. 앱 팀의 요구에 따라 템플릿 수정도 필요했고, 이 함수 자동 생성 프로그램과 로그 정의 전환이 동시에 이루어졌으니 예외 케이스가 간간히 발견되고 있다. 또 회사에서는 올해 상반기부터 서버 바이패스 로그를 도입하고자 하고 있다. API의 응답을 FE가 조립해서 전송하는 것이 아니라 API에서 &quot;이 값을 로그로 전달해라&quot;는 식으로 묶어서 응답하는 것이다. FE에서는 그 값을 ㅊ마조하지 않고 로그로 전송하도록 하는 것이 목표다. 물론 모든 상황에서 적용이 가능하지는 않다. 사용자의 동작에 따라 결정되는 값은 따로 전송하게 되니 어찌보면 두 벌의 작업이 필요하다. 이러한 API 바이패스 로그 데이터는 어떻게 정의할지, proto 파일을 정의할 때 &lt;i&gt;&quot;이 부분은 API의 데이터에요&quot;&lt;/i&gt;&amp;nbsp;라는 의견을 어떻게 주고 받을지에 대해서 논의하고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 했던 작업과 가장 큰 차이점은 팀 내의 작업이 아닌 실 단위, 심지어 실을 넘는 논의와 질답이 이어지는 범위가 큰 작업이라는 것이다. 결과물도 꽤나 만족스럽다. 엑셀 파일을 보며 어떤 파라미터가 어떤 타입으로 전송되어야 하는지 하나씩 맞춰 보는 것이 아니라, 쉘 파일을 실행시켜 생성된 함수를 호출하는 방식으로 로그를 전송하게 되었다. 로그의 필드가 누락될 확률도 크게 줄어들고 타입의 유효성은 보장되며, 불필요하게 다시 입력해야 하는 고정된 값은 입력할 필요조차 없다.&lt;br&gt;&amp;nbsp;&lt;br&gt;작업 당시에는 너무 귀찮았지만 끝내고 나니 꽤나 재미있고 가치있는 작업이었다고 생각한다. 아쉬운 것도 몇가지 있다. 먼저 코드의 품질이다. 로그 정의를 protobuf로 전환하는 작업과 동시에 개발을 시작했더니 중간중간 어설프게 쫓게 된 부분이 있다. 그런 부분들은 코드의 품질을 떨어뜨렸고, 결과적으로 수정이 쉬운 코드 작성과 거리가 멀어졌다고 생각한다. 또 다른 점은 앞서 말한 템플릿 엔진 선정이다. 이 프로젝트를 누가, 또 어떻게 사용할 것인지에 대해 먼저 고민했다면 다른 엔진을 선택했을지도 모른다. 이미 너무 많은 작업이 있었기에 이제와서 변경하기에도 어려움이 있다. EJS로 전달된 객체의 타입 추론이 불가능한 것을 생각하면 더욱 아쉽다. 여유가 된다면 리팩토링을 진행하는 수 밖에.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>protobuf</category>
      <category>txtpb</category>
      <category>템플릿 엔진</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/107</guid>
      <comments>https://partnerjun.tistory.com/107#entry107comment</comments>
      <pubDate>Sun, 2 Jun 2024 18:19:24 +0900</pubDate>
    </item>
    <item>
      <title>H2에서 청크 개수 제한은 성능에 영향을 줄까? 주는 것 같다.</title>
      <link>https://partnerjun.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;얼마 전 회사에서 공유했던 문서 중 하나를 가져와 블로그에도 적어둔다. 다른 글과 다르게 회사에 공유했던 주요 내용만 요약한 글이지만, 제대로 확인된 내용이 아니기도 하고 평소에 어떤 식으로 이야기를 풀어 공유 해왔는지를 남겨두는데 의미를 둔다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;신규 프로젝트 런칭 이후 성능 개선 작업 도중 문득 이상한 것을 발견했다. HTTP2를 지원하는 스테이지 서버임에도 때때로 굉장히 &quot;느리게&quot; 페이지의 JS가 실행되는 것이었다. &lt;b&gt;혹시나 하여 청크 개수 제한을 걸고 배포해보니 불규칙적인 로드 완료 지연이 크게 줄어든 것을 느꼈다&lt;/b&gt;. 이것을 제대로 확인해보진 않았지만 분명하게 빨라진 것을 느꼈고, 다른 성능 개선 작업에 더해(서비스에 아무런 영향이 없으니) 청크 제한을 건 상태로 라이브 배포를 진행했다. 다른 작업 요소가 포함되어 있었으니 명확하게 &lt;i&gt;&quot;이건 이만큼 영향이 있었다&quot;&lt;/i&gt; 라고 이야기할 수 없었고, 이에 청크 개수 제한이 성능 개선에 도움이 될 수 있는 것인지 리서치, 테스트했던 결과를 팀 내에 공유했다. 팀원들의 다양한 의견을 얻고 싶었지만 다들 바쁜지 딱히 관심이 없어 보였다는 점이 아쉽다. 내용이 내용인지라 의견 내기 어렵긴 했을 것 같다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4h2wu/btsBC4rcmBo/0JziOyXsmyRuygh5TIeJtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4h2wu/btsBC4rcmBo/0JziOyXsmyRuygh5TIeJtK/img.png&quot; data-alt=&quot;HTTP2에서 청크 개수는 성능에 연관이 있을까?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4h2wu/btsBC4rcmBo/0JziOyXsmyRuygh5TIeJtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4h2wu%2FbtsBC4rcmBo%2F0JziOyXsmyRuygh5TIeJtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;341&quot; height=&quot;148&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HTTP2에서 청크 개수는 성능에 연관이 있을까?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;아래 내용부터는 회사에서 공유했던 문서의 내용이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;작업 중 발견한 Chunk 파일 개수 제한이 정말 성능 향상을 가져오는지, 어느정도 성과를 가지는지 확인하기 위한 테스트를 진행하고 결과를 공유하고자 함&lt;/li&gt;&lt;li&gt;정량적 측정을 진행하진 못했지만, &lt;b&gt;청크&amp;nbsp;파일 개수를 제한 했을 때 속도가 더 빨라진 것으로 보여 적용한 상태&lt;/b&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt; &lt;/b&gt;&lt;/li&gt;&lt;li&gt;앱에서 static 파일들을 캐시하지 않는 상황으로(매번 삭제하고 있음. 다른 문서를 통해 공유), 청크 파일 개수 및 HTTP2에 영향을 그대로 받는 상황&lt;/li&gt;&lt;li&gt;해당 내용 확인을 위해 HTTP2가 지원되는 스테이지 환경에 지속적으로 배포하여 확인 진행&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;예상&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;해당 내용 확인을 위해 예상되는 제약 사항이나 배경에 대해 간단하게 고민을 전개함&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;&lt;b&gt;HTTP2와 Chunk 파일 개수가 연관이 있을까?&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;HTTP2의 멀티플렉싱으로 JS 파일들이 동시에 다운로드되니 영향이 없을 것 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;하지만 청크 제한을 걸지 않으면 오히려 전체 파일의 용량이 커지므로(gzip, brotli 관련) 더 느려질 가능성 있음&lt;br&gt;&lt;br&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;b&gt;Hydration과 연관이 있을 수 있을까?&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;논리적으로 생각했을 때 JS 파일이 모두 다운로드, 실행되어야 Hydration이 완료될 것 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;컴포넌트 렌더링을 모두 해서 메모리에 들고 있어야 fiber로 비교를 하든 말든 하니까.&lt;br&gt;&lt;br&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li&gt;그럼 JS 파일이 일부만 다운로드 되어도 Hydration이 가능할까? 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;아래 -&amp;gt; 위 순서로 트리를 구성할 순 없으므로 전체 파일을 다운로드하지 않으면 Hydration 자체는 불가능함 
      &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
       &lt;li&gt;dynamic 등 임의적으로 분리한 컴포넌트는 연관이 없을 것&lt;br&gt;&lt;br&gt;&lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;b&gt;결국&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;HTTP2를 기반으로 각 번들 파일이 더 빠르게 다운로드 하더라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ff6600;&quot;&gt;결국 실행이 가장 늦게 다운로드 된 파일에 블라킹 될 것으로 예상됨&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;단 하나의 파일이라도 더 &lt;i&gt;&quot;느리게&quot;&lt;/i&gt; 다운로드 완료된다면 전체 Scripting이 지연되므로 네트워크 환경에 영향을 받지 않을까?&lt;br&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;대부분이 모바일 사용자인 상황이라면 네트워크 환경이 고르지 않을 확률이 훨씬 높다.&lt;/li&gt; 
     &lt;li&gt;결국 &lt;u&gt;청크 개수 제한은 간접적으로라도 성능 향상에 도움을 줄&lt;/u&gt; 것이다&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;실제 배포를 통한 성능 지표 비교&lt;/h2&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;1. Chunk Limit 4 적용된 상태 (4개 파일로 나누어짐)&lt;/h3&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/ovsFU/btsBBOvkAFA/vRkttMsjwwnN2bRhOy6tfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ovsFU/btsBBOvkAFA/vRkttMsjwwnN2bRhOy6tfK/img.png&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;876&quot; style=&quot;width: 51.6003%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ovsFU/btsBBOvkAFA/vRkttMsjwwnN2bRhOy6tfK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FovsFU%2FbtsBBOvkAFA%2FvRkttMsjwwnN2bRhOy6tfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1516&quot; height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYndH/btsBx73IZuz/Dp1teS0A4te8AekRqJGE81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYndH/btsBx73IZuz/Dp1teS0A4te8AekRqJGE81/img.png&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;1092&quot; style=&quot;width: 47.2369%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYndH/btsBx73IZuz/Dp1teS0A4te8AekRqJGE81/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlYndH%2FbtsBx73IZuz%2FDp1teS0A4te8AekRqJGE81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1730&quot; height=&quot;1092&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Waiting for Server가 약 117ms 발생&lt;/figcaption&gt;
&lt;/figure&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/epslfC/btsBuyVn51u/XPCZzvydzeghTrrRlhy8O1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/epslfC/btsBuyVn51u/XPCZzvydzeghTrrRlhy8O1/img.png&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;444&quot; style=&quot;width: 48.4731%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/epslfC/btsBuyVn51u/XPCZzvydzeghTrrRlhy8O1/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FepslfC%2FbtsBuyVn51u%2FXPCZzvydzeghTrrRlhy8O1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;640&quot; height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbQV6K/btsBykogmVk/cyKZMPAE35hzJI94bFlgOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbQV6K/btsBykogmVk/cyKZMPAE35hzJI94bFlgOk/img.png&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;430&quot; style=&quot;width: 50.3641%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbQV6K/btsBykogmVk/cyKZMPAE35hzJI94bFlgOk/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbQV6K%2FbtsBykogmVk%2FcyKZMPAE35hzJI94bFlgOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;644&quot; height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;스크립팅에 약 650ms가 소요&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;Content download까지의 &lt;span style=&quot;color: #FF0000;&quot;&gt;대기 시간(Waiting for server response)가 100ms 이상 소요&lt;/span&gt;&lt;/li&gt;&lt;li&gt;청크 개수 제한이 없을 때에 비해 파일 다운로드에 상대적으로 더 긴 시간이 소요됨 (340ms 이상)&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;2. Chunk Limit 미적용 상태 (24개 이상의 파일로 나누어짐)&lt;/h3&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/ciOcpx/btsBzxUWR3Z/lb9DshIfA9kp5OKS15Q780/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciOcpx/btsBzxUWR3Z/lb9DshIfA9kp5OKS15Q780/img.png&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;1232&quot; style=&quot;width: 48.5977%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciOcpx/btsBzxUWR3Z/lb9DshIfA9kp5OKS15Q780/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciOcpx%2FbtsBzxUWR3Z%2Flb9DshIfA9kp5OKS15Q780%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1360&quot; height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vifCn/btsBuxPGroQ/PSjD5UlVVR8pvksDyh0GXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vifCn/btsBuxPGroQ/PSjD5UlVVR8pvksDyh0GXk/img.png&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;1544&quot; style=&quot;width: 50.2396%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vifCn/btsBuxPGroQ/PSjD5UlVVR8pvksDyh0GXk/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvifCn%2FbtsBuxPGroQ%2FPSjD5UlVVR8pvksDyh0GXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1762&quot; height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;같은 버전-프로젝트임에도 Waiting for Server에 700ms 소요(약 7배?)&lt;/figcaption&gt;
&lt;/figure&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/kyF9d/btsBy8A5CDu/uOzfnenzpO9pWqMTWbJJpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyF9d/btsBy8A5CDu/uOzfnenzpO9pWqMTWbJJpk/img.png&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;436&quot; style=&quot;width: 49.6054%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyF9d/btsBy8A5CDu/uOzfnenzpO9pWqMTWbJJpk/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkyF9d%2FbtsBy8A5CDu%2FuOzfnenzpO9pWqMTWbJJpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;662&quot; height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPqiX7/btsByrATLVd/RaoPB7fsZJLb57zzZaTmHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPqiX7/btsByrATLVd/RaoPB7fsZJLb57zzZaTmHk/img.png&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;434&quot; style=&quot;width: 49.2318%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPqiX7/btsByrATLVd/RaoPB7fsZJLb57zzZaTmHk/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPqiX7%2FbtsByrATLVd%2FRaoPB7fsZJLb57zzZaTmHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;654&quot; height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Scripting 시간은 별 차이가 없음&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;Content Download까지 &lt;span style=&quot;color: #FF0000;&quot;&gt;대기 시간(Waiting for server response)가 700ms 이상 소요&lt;/span&gt;&lt;/li&gt;&lt;li&gt;각각의 파일 다운로드에는 큰 시간이 소요되지 않음 (20ms 내외)&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;체크포인트&lt;/h2&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;청크 개수 제한 여부와 Scripting 시간 자체는 큰 연관 관계가 없는 것으로 보임&lt;/li&gt; 
 &lt;li&gt;Document가 다운로드 된 이후, JS파일 다운로드가 시작되기까지 지연은 동일하게 있음 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;하지만 Chunk 파일의 개수가 많은 경우에 &lt;span style=&quot;color: #ff0000;&quot;&gt;&lt;span style=&quot;color: #ff6600;&quot;&gt;Waiting for server response 지연 시간이 평균적으로 더 길게&lt;/span&gt; &lt;/span&gt;기록됨&lt;/li&gt; 
   &lt;li&gt;&lt;span&gt;Waiting for server response은 무엇인가?&lt;/span&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://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation&quot;&gt;Chrome 문서의 설명&lt;/a&gt;&lt;span&gt;에 따르면 네트워크가 느리거나 브라우저가 다른 작업을 하고 있을 때 지연될 수 있다고 함&lt;/span&gt; 
      &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
       &lt;li&gt;네트워크 지연의 경우, Node.js(Next.js) 서버 단일 쓰레드 문제로 의심할 수 있음 
        &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
         &lt;li&gt;하지만 S3업로드 후 CDN을 거치는 상태이니 연관 없을 것&lt;/li&gt; 
        &lt;/ul&gt; &lt;/li&gt; 
       &lt;li&gt;브라우저의 다른 작업이라 하면 &lt;span style=&quot;color: #ff0000;&quot;&gt;곧 스크립트 작업, Rendering 및 Parsing &amp;amp; Execution을 뜻할 것으로 예상&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
    &lt;/ul&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;다운로드 시간 지연은 곧 스크립트 실행 지연으로 직결되므로 청크 개수 제한을 걸지 않으면 H2에서 생각보다 빠르지 않다 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;예상했던 모바일 네트워크 상태로 인한 불균형은 차치하고, &lt;br&gt;Waiting for server response 지연에 의해 더 나쁜 성능을 보이고 있었을 지도 모르는 상황&lt;/li&gt; 
     &lt;li&gt;&quot;Waiting for server response&quot;가 무엇인지, 어떻게 감소시킬 수 있는지 확인이 필요함&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;결과 (뇌피셜)&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;명확한 참고 자료, 혹은 정량적 측정이 불가능 하였지만 &lt;span style=&quot;color: #000000;&quot;&gt;&lt;u&gt;HTTP2라 할지라도 매번 다시 다운로드 받는 환경&lt;/u&gt;이라면&lt;/span&gt; 청크 파일의 &quot;적당한&quot; 개수 제한이 필요한 것으로 보임&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;&lt;span style=&quot;color: #1f2328;&quot;&gt;현재 서비스는 모바일 중심의 서비스로 &lt;u&gt;불안정한 모바일 네트워크 특성상&lt;/u&gt; 파일 개수가 많을수록 다운로드가&amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt;크게 지연되는 파일이 생길 가능성이 높음&lt;/span&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;color: #1f2328;&quot;&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;특정 파일의 다운로드 지연 -&amp;gt; 스크립트 실행 지연으로 직결됨&lt;/span&gt;&lt;/span&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li style=&quot;list-style-type: none;&quot;&gt; 
      &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
       &lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;특정 파일의 다운로드 지연 확인 방법&lt;/span&gt; 
        &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
         &lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Chrome Network 탭&lt;/span&gt; 
          &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
           &lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;→ JS 파일(청크 파일) URL block &lt;/span&gt;&lt;/li&gt; 
           &lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;→ 새로고침 (JS 실행 안됨)&lt;/span&gt;&lt;/li&gt; 
           &lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;→ URL blocking 해제 &lt;/span&gt;&lt;/li&gt; 
           &lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;→ console 탭에서 script insert 구문 작성 (JS 실행됨)&lt;/span&gt;&lt;/li&gt; 
          &lt;/ul&gt; &lt;/li&gt; 
         &lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위 순서로 &lt;u&gt;특정 JS 파일이 다운로드 지연되는 상황&lt;/u&gt;을 인위적으로 만들어 확인할 수 있음&lt;/span&gt;&lt;/li&gt; 
        &lt;/ul&gt; &lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGNPKm/btsBxcxpNIZ/WthLcTYHO5dk5hB2UvS6N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGNPKm/btsBxcxpNIZ/WthLcTYHO5dk5hB2UvS6N0/img.png&quot; data-alt=&quot;청크로 나누어진 JS 파일의 URL 블락&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGNPKm/btsBxcxpNIZ/WthLcTYHO5dk5hB2UvS6N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGNPKm%2FbtsBxcxpNIZ%2FWthLcTYHO5dk5hB2UvS6N0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;408&quot; height=&quot;471&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;청크로 나누어진 JS 파일의 URL 블락&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjJvFp/btsBy9HguJS/Kraaz6BBOudItdNWheU5H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjJvFp/btsBy9HguJS/Kraaz6BBOudItdNWheU5H0/img.png&quot; data-alt=&quot;JS 파일이 블라킹되어 다운로드 되지 않는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjJvFp/btsBy9HguJS/Kraaz6BBOudItdNWheU5H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjJvFp%2FbtsBy9HguJS%2FKraaz6BBOudItdNWheU5H0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;834&quot; height=&quot;172&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JS 파일이 블라킹되어 다운로드 되지 않는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;73&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oax7k/btsBypDCcDY/HyD6txHjSdhE1sdRjeWss1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oax7k/btsBypDCcDY/HyD6txHjSdhE1sdRjeWss1/img.png&quot; data-alt=&quot;URL 블라킹 해제 후 콘솔에서 직접 JS 파일 삽입&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oax7k/btsBypDCcDY/HyD6txHjSdhE1sdRjeWss1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foax7k%2FbtsBypDCcDY%2FHyD6txHjSdhE1sdRjeWss1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1089&quot; height=&quot;73&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;URL 블라킹 해제 후 콘솔에서 직접 JS 파일 삽입&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #666666;&quot;&gt;위 순서를 따르면 JS 파일이 삽입된 이후에 JS가 실행됨. Webpack 자체에서 청크 파일이 모두 다운로드 될 때까지 JS 실행을 미루고 있음을 확인할 수 있음.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;&lt;span style=&quot;color: #1f2328;&quot;&gt;프로그래밍 특성상 JS 파일의 &lt;span style=&quot;color: #666666;&quot;&gt;parsing → eval → parsing → ...&lt;/span&gt; 작업의 반복 회수를 줄이는 것이 효율적일 것이라고 예상&lt;/span&gt;&lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;color: #1f2328;&quot;&gt;gzip 혹은 brotli 로 인한 압축으로 번들링하여 파일의 개수가 줄어들면 압축률이 상승, JS 파일의 절대적인 용량 자체도 줄어듦&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;color: #1f2328;&quot;&gt;다운로드 시간 역시 JS실행 시간에 큰 영향을 주므로 파일 용량 감소도 큰 영향을 줄 수 있음&lt;/span&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;&lt;span style=&quot;color: #1f2328;&quot;&gt;참고) webpack의 &lt;a href=&quot;https://github.com/webpack/webpack/tree/main/examples/http2-aggressive-splitting&quot;&gt;http2-aggressive-splitting&lt;/a&gt;과 같은 라이브러리는 첫 로드 속도를 위한 것이 아닌&lt;br&gt;변경되지 않은 파일에 대한 캐시를 전략적으로 하고자 하는 것임.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;span style=&quot;color: #1f2328;&quot;&gt;&lt;b&gt;결론적으로, 청크 개수 제한을 설정하지 않을 이유도 없음&lt;/b&gt;&lt;br&gt;&lt;/span&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;color: #1f2328;&quot;&gt;청크 개수 제한은 Webpack의 기본 플러그인으로, 성능이나 서비스에 악영향을 주거나 운영 환경 변화가 필요하지 않기 때문&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #1F2328;&quot;&gt;그 외&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;추가로 위 내용을 확인하며&amp;nbsp;&lt;span style=&quot;color: #1F2328;&quot;&gt;HTTP2에 대한 이상할 정도의 신뢰가 형성되어 있는 것을 보게 되었음&lt;/span&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;아래 적어둔&amp;nbsp;&lt;a href=&quot;https://stackoverflow.com/questions/33658302/why-http-2-is-slower-than-plain-https&quot;&gt;Why HTTP/2 is slower than plain HTTPS2?&lt;/a&gt;과 같은 문서들을 보면 무조건적인 상위 호환이라는 인식이 있음 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;기본적으로 HTTP 1.1의 상위 호환이지만 이는 부족했던 점을 보완하려고 하는 것일 뿐, 발생할 수 있는 모든 문제가 해결되었다고 장담할 수 없음&lt;br&gt;&lt;br&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;인터넷에서 볼 수 있는 HTTP2 벤치마크 결과는 &lt;u&gt;버킷의 이미지 같은 완전 정적 파일&lt;/u&gt;의 결과 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span style=&quot;color: #ff0000;&quot;&gt;브라우저에서 &quot;실행&quot; 해야하는 JS를 서빙하는 &lt;/span&gt;입장에서의 벤치마크로 갈음 할 수 없음&lt;/li&gt; 
   &lt;li&gt;불안정한 모바일 네트워크와 같은 환경에서의 테스트라고 볼 수도 없음&lt;br&gt;&lt;br&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;HTTP2의 문제가 아니더라도, 인프라나 소프트웨어(Nginx, Chrome, Webpack Chunk) 측면에서 한계가 있을 수 있지 않을까 하는 생각 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;심지어 서비스의 앞단 WAF에서 발생하는 문제일 수도 있음&lt;br&gt;&lt;br&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;늘 그렇듯, &quot;&lt;a href=&quot;https://en.wikipedia.org/wiki/No_Silver_Bullet&quot;&gt;은탄환은 없다&lt;/a&gt;&quot; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;보험정도는 들어둘만 하다. 반대로 보험이 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;리스크를 주는 아이러니한 상황이 아니라면.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;참고 문서&lt;/h2&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/articles/performance-http2?hl=ko&quot; target=&quot;_self&quot;&gt;&lt;span&gt;HTTP/2 소개&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;stack overflow - &lt;a href=&quot;https://stackoverflow.com/questions/53450852/am-i-truly-using-http2&quot; target=&quot;_self&quot;&gt;&lt;span&gt;Am I truly using http2?&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;stack overflow - &lt;a href=&quot;https://stackoverflow.com/questions/33658302/why-http-2-is-slower-than-plain-https&quot; target=&quot;_self&quot;&gt;&lt;span&gt;Why HTTP/2 is slower than plain HTTPS2?&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://medium.com/webpack/webpack-http-2-7083ec3f3ce6&quot; target=&quot;_self&quot;&gt;&lt;span&gt;webpack &amp;amp; HTTP/2&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/webpack/webpack/tree/main/examples/http2-aggressive-splitting&quot; target=&quot;_self&quot;&gt;&lt;span&gt;http2-aggressive-splitting&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://blog.khanacademy.org/forgo-js-packaging-not-so-fast/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;JS packaging&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://blog.khanacademy.org/forgo-js-packaging-not-so-fast/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;Forgo JS packaging? Not so fast&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위 내용이 회사의 팀 문서로 남겨둔 내용이다. 결론이 딱히 없긴 하지만 누군가에겐 도움이 되지 않을까 하는 생각이다. 명쾌하게 답을 얻진 못했지만 의심가는 점들이 있으니 프로젝트에는 청크 개수 제한을 걸어두었다. 위 내용에 적었듯, 네트워크 환경의 문제로 느려질 수도 있지만 HTTP2 멀티플렉싱이 지원되는 상황에서도 JS 실행이 지연되는 것을 확인했고, 청크 파일 개수 제한을 걸지 않을 이유도 없기 때문이다. 센트리 퍼포먼스를 이용하면 사용자의 랜딩에 정말 유효한 영향을 미치는지 확인할 수 있을 것 같지만... 내 궁금증을 해소하기 위해 상용화된 회사 서비스로 실험을 하고 싶지 않았다.&lt;br&gt;청크 개수 제한을 걸고 마무리한 이후에도 주기적으로 이 내용에 대해 검색해보고 있다. 이미 H3로 넘어가는 시점이 와버려서인진 모르겠지만 딱히 명쾌함-혹은 고개가 끄덕여질만한 답을 얻진 못했다. 오히려 고민만 더해졌는데, &lt;a href=&quot;https://stackoverflow.com/questions/36517829/what-does-multiplexing-mean-in-http-2&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;What does multiplexing mean in HTTP/2&lt;/span&gt;&lt;/a&gt; 라는 스택오버플로의 답변을 보고 나서다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekaB3h/btsBzAkmXFF/wO09kEVsEEIrZKk3WDp1uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekaB3h/btsBzAkmXFF/wO09kEVsEEIrZKk3WDp1uK/img.png&quot; data-alt=&quot;H2에서 브라우저는 파일을 조각내어 다운로드 받고 다시 조립한다?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekaB3h/btsBzAkmXFF/wO09kEVsEEIrZKk3WDp1uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekaB3h%2FbtsBzAkmXFF%2FwO09kEVsEEIrZKk3WDp1uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;665&quot; height=&quot;117&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;H2에서 브라우저는 파일을 조각내어 다운로드 받고 다시 조립한다?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;브라우저가 파일을 조각냈다가 다시 조립한다는 내용이다. 웹 브라우저에서 JS를 실행하는 아주 근본적인, 기본적인 룰은 스크립트를 만났을 때 그 즉시 스크립트를 실행한다는 것이다. 그렇다면 이렇게 &lt;i&gt;&quot;다시 조립한&quot;&lt;/i&gt; JS 파일은 즉시 실행되는 것일까? 그럼 그 &lt;i&gt;&quot;무거운&quot;&lt;/i&gt; JS 실행 중에도 다른 파일의 조립을 수행할까? 그럼 멀티플렉싱으로 받은 CSS로 만든 CSSOM은? 이미지나 레이아웃은?... 이건 크로니움의 탭(프로세스)와 관련된 문제이기도 하다. &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/main/docs/threading_and_tasks.md&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;크로니움의 관련된 문서를 살펴보아도&lt;/span&gt;&lt;/a&gt; 명확한 답을 얻을 수는 없었다. 안타깝지만 나는 여기까지만 고민해본 상태다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfD2St/btsBzMSFHJ1/4oxxestzBhrM8iUcFCiXx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfD2St/btsBzMSFHJ1/4oxxestzBhrM8iUcFCiXx1/img.png&quot; data-alt=&quot;몰름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfD2St/btsBzMSFHJ1/4oxxestzBhrM8iUcFCiXx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfD2St%2FbtsBzMSFHJ1%2F4oxxestzBhrM8iUcFCiXx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;100&quot; height=&quot;100&quot; data-origin-width=&quot;100&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;몰름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>h2</category>
      <category>webpack</category>
      <author>partner_jun</author>
      <guid isPermaLink="true">https://partnerjun.tistory.com/106</guid>
      <comments>https://partnerjun.tistory.com/106#entry106comment</comments>
      <pubDate>Thu, 7 Dec 2023 22:54:30 +0900</pubDate>
    </item>
  </channel>
</rss>