<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>memoir.</title>
        <link>https://velog.io/</link>
        <description>회고할 가치가 있는 개발을 하자</description>
        <lastBuildDate>Tue, 25 Nov 2025 10:20:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>memoir.</title>
            <url>https://velog.velcdn.com/images/hys-lee/profile/a9656a7b-97f8-4254-aa55-8300b94fc0b2/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. memoir.. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hys-lee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[배포 관련한 간략 회고]]></title>
            <link>https://velog.io/@hys-lee/%EB%B0%B0%ED%8F%AC-%EA%B4%80%EB%A0%A8%ED%95%9C-%EA%B0%84%EB%9E%B5-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hys-lee/%EB%B0%B0%ED%8F%AC-%EA%B4%80%EB%A0%A8%ED%95%9C-%EA%B0%84%EB%9E%B5-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 25 Nov 2025 10:20:25 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>이번 프로젝트에서 배포 관련한 작업을 모두 도맡아 진행했다.</p>
<p>아무래도 FE팀원분은 경력직이라 해당 경험이 있었지만, 나에게 부족한 지점 중 하나가 이 지점이라 생각했기 때문이다.</p>
<p>직접 마주하며 시행착오를 겪어봐야 알 수 있는 부분이라 생각하여 담당하게 되었고, 덕분에 느낀점이 좀 있었다.</p>
<p>진행 과정에서 있던 일을 많이 적을 순 없으나, 잊어버리기 아쉬운 기억이라 기록한다.</p>
<h1 id="회고">회고</h1>
<h3 id="netlify---스테이징">Netlify - 스테이징</h3>
<p>기존에는 Upstream 레포지토리를 두고, 개별로 fork해서 개발 후 Upstream/main에 바로 배포해버리는 방식을 사용하고 있었다.</p>
<p>배포 단계에 가까워지기 전에는 불필요한 작업을 최소화하자는 의견에 의해서였다.</p>
<br/>

<p>데모 데이가 다가오자, Upstream/dev를 생성했는데 막상 AWS를 사용하기는 아까웠다.</p>
<p>BE 팀에서는 무료 도메인을 얻어 무료에 가깝게 사용하고 있다는 것을 듣고, 무료 배포 방식을 찾아봤다.</p>
<p>물론 Vercel을 사용하는 것이 제일 간편했으나, 안해봤던 서비스를 사용하고 싶었고 Netlify를 사용해보게 되었다.</p>
<br/>

<p>Vercel과 Netlify모두 <strong>Serverless</strong> 방식이었기 때문에, 불편한점이 많지는 않았다.</p>
<p>다만 Vercel에 비해 처리할 작업이 존재했는데, <strong>netlify.toml</strong>를 통해 플러그인을 적용해야 했다.</p>
<p>Vercel과 마찬가지로 Edge Functions를 통해 서버 액션 등을 처리하고, 단순 정적 파일들을 호스팅하는 것이 아니라 SSR에도 대응할 수 있다는 것을 알게 되어 신선했다.</p>
<br/>

<p>시연하면서 느꼈던 단점은, 아무래도 <strong>Cold-Start</strong>로 인해 성능이 좋지 않았다는 것이다.</p>
<p>우리 서비스처럼 동적 라우팅이 많고 사용자 수가 적으며, BE 서버도 좋지 않은 상황에선 크게 느껴지게 되었다.</p>
<p>이에 Production 단계에선 EC2를 사용하기로 했다.</p>
<h3 id="ec2--docker--nginx---production">EC2 + Docker + Nginx - Production</h3>
<p>성능을 조금이라도 끌어올리기 위해 EC2를 사용하게 되었다.</p>
<p>Netlify에서는 도메인 네임을 설정하 수 있었기 때문에 편리했으나, EC2를 사용하기 위해선 무료 도메인을 구해야 했다.</p>
<br/>

<p>또한, 다른 팀들에서 Docker를 사용하는 것을 보고 도입해보자는 생각을 했다.</p>
<p>Docker를 제대로 사용해보는 것은 처음이었기 때문에 좋은 기회라 생각했다.</p>
<p>EC2 배포 자체는 경험이 있었기 때문에, 설정 과정은 어렵지 않았으나 Docker를 같이 사용함으로 인해 얻는 이점이 커보였다.</p>
<br/>

<p>우선 EC2 Free Tier를 사용했기 때문에 컴퓨팅 성능이 매우 제한적이었는데, 때문에 CD 과정에서 종종 에러가 발생하곤 했다.</p>
<p>직접 레포지토리를 사용해 빌드 및 배포를 하는 과정에서 인스턴스가 메모리 이슈로 중단되어버리는 것이다.</p>
<p>그러나 Docker를 통해 빌드한 이미지를 Docker Hub에 업로드하고, EC2 인스턴스에서 이를 다운 받아 실행하는 것으로 메모리 이슈 없이 배포를 해결할 수 있었다.</p>
<p>이러한 CD 과정을 github actions를 통해 자동화 했다.</p>
<br/>

<p>Nginx는 TLS인증이 간단하고, 리버스 프록시가 더 깔끔하기 때문에 사용하게 되었다.</p>
<p>certbot만 따로 운영하여 처리하기엔 비용이 너무 컸다.</p>
<p>이 역시 처음 사용해보았는데, 과정을 따로 외운다기 보다는 그 때 그 때 찾아보는 것이 좋아보였다.</p>
<p>역시 설정 파일을 잘 처리해야 했던 것이다.</p>
<br/>

<p>추후, 고도화 작업에서 HTTPS/2로 변경하였는데, 과정이 매우 간단했다는 점은 인상 깊었다.</p>
<h1 id="마치며">마치며</h1>
<p>배포 관련해서 있었던 일과 느낀점을 간략하게 적어보았다.</p>
<p>이 부분에 있어서 크게 할 말은 없기 때문에, Motimo를 제작하며 그리고 Prography를 수료하며 있던 것들 중 남은 것들을 정리하겠다.</p>
<br/>

<p>사실 주요한 기술적 이슈 및 해결은 포트폴리오에 적었기 때문에 기술적으론 할 말이 많지는 않다. 
정말 버리기 아까운, 내 노력이 많이 들어갔던 것들에 대해 작성하고자 이번 시리즈를 시작하게 되었던 것이다.</p>
<p>그 외에, 진행하면서 느꼈던 점들이 몇 가지 있다.</p>
<p>우선, 어떤 것을 하든 본인이 하는 것에 따라 얻어갈 수 있는 것들이 다르다는 점이다.</p>
<p>예를 들어, 다른 팀들이 어느정도로 했는지는 정확히 모르지만, 디자인 시스템을 도입하는데 꽤 많이 공을 들였고, 이는 포트폴리오에 쓸 좋은 경험이 되었다. 
면접에서도 도움이 될 경험들이 많이 쏟아져 나왔고, 덕분에 채용 자소서를 작성하고 면접을 준비하는데 큰 도움이 되었다.</p>
<p>어떤 것이든 얼만큼 고민하고, 시도하고, 노력하느냐에 따라 가치가 바뀌는 것 같다.</p>
<br/>
또한, 인간관계를 비롯한 소프트 스킬의 중요성도 느꼈다.

<p>우리 팀은 서로 서먹한 쪽에 속했다. 서로 어느정도 농담도 하고 만나면 잘 이야기 했지만, 그 외에는 딱히 더 만나는 등의 활동이 없었다. </p>
<p>다른 팀의 경우 다같이 MT도 참가하고 정규 날짜 이외에 따로 프로젝트 회의 등을 위해 모이는 등의 추가적인 노력으로 끈끈함이 보이는 듯 했지만, 우리는 그정도는 아닌 듯 했다.</p>
<p>프로젝트를 유지하고 수익화를 위해 나아가겠다는 몇몇 팀들과는 분위기가 달랐던 것이다.</p>
<p>물론, 마지막 시간에 보니 우리보다 상황이 좋지 않았던 팀도 있어보이긴 했지만, 이들과 비교하는게 무슨 소용이 있겠는가.</p>
<p>조금 더 잘 소통하는 것이 팀적으로 프로젝트를 진행하는데 있어서 얼마나 중요한지 깨닫게 되었다.</p>
<br/>
마지막으로, 이러한 IT 커뮤니티의 한계를 느끼기도 했다.

<p>솔직하게 처음에 지원할 때는 다른 경력자 분들께 엄청나게 배울게 많다고 생각했으나, 그렇지는 않았다.</p>
<p>다들 프로젝트를 진행하느라 바쁘기도 했고, 여러 프로젝트를 동시에 진행하고 계시는 분들도 많았기 때문에 시간적 여유가 부족했다.</p>
<p>또한, 소위 &#39;네카라&#39;와 같은 고수로부터 사사받는 상황은 망상에 가까웠다. 대부분 IT 커뮤니티를 이용하는 사람들은 빠르게 사이드 프로젝트를 제작하고자 하는 사람인데, 이는 이직을 위함이 대부분이기 때문이다.</p>
<p>고수들이 과연 이직을 위한 포트폴리오를 IT 커뮤니티를 통해 얻을지는 의문이다. 상황상 본인의 프로젝트에선 이직하고자 하는 영역의 경험을 얻기 어려울 경우도 있겠지만, 이러한 사이드프로젝트를 통해 충분한 경험을 쌓기는 어려워보였다.</p>
<p>이러한 구조적 한계로 인해 고수로부터 사사받는 기대는 접는 것이 낫다. 차라리 인맥을 얻는다고 생각하는 편이 마음이 편할 것이다.</p>
<p>시스템을 보려고 노력한다면 보이는 것들이 있는 듯 했다.</p>
<hr>
<p>이상으로 느꼈던 점들을 최대한 정리해보았다.</p>
<p>밤샘 코딩도 해보고, 스트레스도 받고, 이슈들도 해결하면서 결과적으로 좋은 경험과 결과물을 얻었다.</p>
<p>어떤 활동을 하든 스스로 높은 결과물을 얻고 가치를 창출하기 위해 고민하고 나아가야 할 것임을 명심하며 이만 시리즈를 마치겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[둘러보기와 MSW]]></title>
            <link>https://velog.io/@hys-lee/%EB%91%98%EB%9F%AC%EB%B3%B4%EA%B8%B0%EC%99%80-MSW</link>
            <guid>https://velog.io/@hys-lee/%EB%91%98%EB%9F%AC%EB%B3%B4%EA%B8%B0%EC%99%80-MSW</guid>
            <pubDate>Tue, 25 Nov 2025 09:43:37 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>회원 가입 전, 서비스를 둘러보는 &#39;게스트모드&#39;에 대한 아이디어가 나왔다.</p>
<p>전환율을 조금이라도 높이기 위해, 미리 체험시키는 것이다.</p>
<br/>

<p>게스트모드에선 서비스 핵심 기능인 &#39;그룹 챗&#39;을 제외한 다른 기능을 사용할 수 있도록 했다.</p>
<p>즉, TODO를 작성하고 완료 시 Reaction을 할 수 있어야 했는데, 이에 대해 내가 온전히 작업을 담당하게 되었다.</p>
<p>이유는 배포 시기까지 시간이 없었는데, BE 주요 작업이 진행중이었고, FE에서 온전히 처리할 아이디어가 있었기 때문이었다.</p>
<br/>

<p>사실, 나중에 FE 경험 많은 친구에게 물어보니, 애초에 이 방식은 잘못된 접근이었다고 한다. BE에서 맡았어야 할 작업이었다고.</p>
<p>그러나, 어쨌든 아이디어를 내서 동작 시켰기에 이대로 잊어버리기 아쉬워 기록하게 되었다.</p>
<h1 id="회고록">회고록</h1>
<h3 id="배경-상황">배경 상황</h3>
<p>FE 입장에선, 그룹챗을 뺀 기능을 동작하게 하면서 API를 사용하지 않는게 좋아보였다.</p>
<p>우리 서버는 성능면에서 제한이 많았던 것이다. 
(기억상, 사비를 들였던 것으로 기억한다.)</p>
<p>따라서, 로컬 공간인 IndexedDB를 사용하면서, 기존에 개발된 API 관련 동작을 활용해 최대한 비용이 적게 기능을 개발하려 하니, MSW를 사용해 보자는 아이디어를 떠올리게 되었다.</p>
<h3 id="db-처리하기">DB 처리하기</h3>
<p>IndexedDB를 꼭 사용해보고 싶었기 때문에 좋은 기회라 생각했다.</p>
<p>사용방법을 알아보니, IndexedDB는 NOSQL이라 API동작만 잘 짜두면 됐다.</p>
<p>스웨거를 통해 사용되고 있는 스토어를 확인했고, 
CRUD관련 메서드들을 AI 도움을 받아 작성했다.</p>
<h3 id="둘러보기-적용하기">둘러보기 적용하기</h3>
<p>우선, 기존에 사용되던 authStore를 사용하여 guest모드를 체크 후 MSW를 활성화 하는 컴포넌트를 마운트 시켰다.</p>
<p>각 API관련 각종 메서드 내용은 AI를 통해 빠르게 생성할 수 있었다.</p>
<p>msw를 활성화 시키는 메서드를 제작해 브라우저 환경에서 Dynamic Import후 실행될 수 있도록 했다.</p>
<h3 id="회고">회고</h3>
<p>당시에는 괜찮은 방법이라 생각했고, IndexedDB까지 사용해볼 계기가 되어 좋다 여겼으나 지금 생각해보니 그렇지 못한 것 같다.</p>
<p>우선, 친구 말대로 이건 BE에서 담당했어야 하는 일은 분명하다.</p>
<p>API 동작을 FE에서 작성하는 것이 말이 안되기 때문이다.</p>
<br/>
나의 경우는 AI로 작성해서 크게 불편함을 못 느꼈을 수 있었지만, 명백히 책임 범위에서 벗어나는 행위이다.


<p>또한, 만약 BE API 수정이 있었다면 이걸 MSW용 API에 다시 적용해야 했다는 점도 위를 시사한다.</p>
<br/>

<p>애초에 MSW의 목적은 테스트 용이고, 배포에 나타나면 안된다는 것이다.</p>
<h1 id="마치며">마치며</h1>
<p>회고해보니 부끄러운 경험이었다.</p>
<p>당시의 상황도, 나의 판단도, 그리고 이를 쓸만하다 여겼던 생각도 전부 다 말이다.</p>
<br/>

<p>난 창의적인 결과물은, 관습보다 조건과 정의에 집중해서 해결책을 찾는 과정에서 나온다고 생각했었기에 의도적으로 관습을 고려하지 않았었는데, 이것이 문제가 되었던 것 같다.</p>
<p>관습이 생긴 이유가 있을 것이므로 어느정도 고려하는 것은 분명 필요한 것이다.</p>
<br/>

<p>그래도 회고를 통해 부족한 점을 느낄 수 있어 좋았고, 필요한 부분에 대해서는 누구의 책임인지 명확히 하고 이를 요구할 수 있어야 겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SEO 적용기]]></title>
            <link>https://velog.io/@hys-lee/SEO-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@hys-lee/SEO-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Tue, 25 Nov 2025 09:18:17 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>고도화 작업 중 하나로, SEO 작업을 꼭 해보고 싶었다.</p>
<p>인턴 생활 때도 사이트맵이 어쩌구 하며 FE 팀원 분이 작업하실 때 무슨 소린지 못 알아들었던 것과 동아리 홈페이지 유지관리 팀에서 OpenGraph 처리 작업을 하려다 못했던 것이 아쉬웠기 때문이다.</p>
<p>우리 서비스는 딱히 SEO가 중요한 서비스는 아니지만, 어쨌든 완성되어 성공적으로 배포했기에 할 수 있는 한 SEO 최적화를 진행하고자 했다.</p>
<h1 id="적용기">적용기</h1>
<p>Gemini에게 물어보니, 다음의 작업들이 필요했다.</p>
<p>Metadata, OpenGraph, SiteMap, Robots.txt</p>
<h3 id="metadata-처리하기">Metadata 처리하기</h3>
<p>Next.js를 사용하고 있었기 때문에 문서를 보면서 진행했는데, </p>
<p>당시 page.tsx가 모두 RCC였기 때문에 동적으로 처리하기 애매하여 정적으로 처리하게 되었다.
(아직 SSR 전환 전이었다.)</p>
<p>사실상 Next.js에서 기본적으로 지원하는 방식이 있어서 native로 처리하는 것보다 간단했다.</p>
<pre><code class="language-typescript">export const metadata: Metadata = {
  title: &quot;타이틀&quot;,
  description: &quot;설명&quot;,
  keywords: [&quot;키워드1&quot;,&quot;키워드2&quot;,...],
};</code></pre>
<p>타입이 정의되어있기 때문에, 필요한 것들을 채워주면 되었다.</p>
<h3 id="opengraph">OpenGraph</h3>
<p>사실 제일 눈에 띄는 변화라면 OG라고 할 수 있다.</p>
<p>찾아보니, facebook과 twitter 크게 두 가지에 대응되는 방식이 있었다.</p>
<p>각각에 맞춤으로 만들기보다 가성비 좋게 처리하기 위해 공통으로 쓰이는 것들만 처리하기로 했다.</p>
<p>(참고로, Next에서 알아서 각 영역에 특화된 내용들을 만들어 주기도 한다고 함)</p>
<p>항목들은 아래와 같다.</p>
<blockquote>
<ul>
<li>og:title</li>
</ul>
</blockquote>
<ul>
<li>og:description</li>
<li>og:image</li>
<li>og:url</li>
<li>og:type</li>
</ul>
<br/>
<br/>

<p>Next에서는 Metadata와 OG를 한번에 설명하고 있었는데, 위 항목을 보면 알겠지만 Metadata에 작성된 부분과 일치하는 것이 많다. </p>
<p>title, description은 Next가 빌드 시 OG 관련 정보로 처리해준다고 한다.</p>
<p>남은 url과 type은 아래와 같이 Metadata에 입력하면 된다.</p>
<pre><code class="language-typescript">openGraph: {
    url: &quot;페이지 url&quot;,
    type: &quot;website&quot;,
  },
};</code></pre>
<p>마지막 남은 OG image는 <code>opengraph-image.png</code>를 app폴더 바로 아래에 위치시키면 됐다.</p>
<p>빌드 시 아래와 같이 된다.</p>
<pre><code class="language-typescript">&lt;meta property=&quot;og:title&quot; content=&quot;모티모 | 그룹 기반 목표 관리 서비스&quot;/&gt;
&lt;meta property=&quot;og:description&quot; content=&quot;그룹 매칭을 통해 목표를 관리해요!&quot;/&gt;
&lt;meta property=&quot;og:url&quot; content=&quot;https://6d93a4b5a919.ngrok-free.app&quot;/&gt;
&lt;meta property=&quot;og:image:type&quot; content=&quot;image/png&quot;/&gt;
&lt;meta property=&quot;og:image:width&quot; content=&quot;1600&quot;/&gt;
&lt;meta property=&quot;og:image:height&quot; content=&quot;630&quot;/&gt;
&lt;meta property=&quot;og:image&quot; content=&quot;https://6d93a4b5a919.ngrok-free.app/opengraph-image.png?23ecf2e2dc069f9f&quot;/&gt;
&lt;meta property=&quot;og:type&quot; content=&quot;website&quot;/&gt;</code></pre>
<br/>

<p>참고로, 개발환경에서 테스트하기 위해 ngrok을 사용했고, facebook의 og 테스트 사이트에서 확인하며 개발을 진행했다.</p>
<h3 id="sitemap">SiteMap</h3>
<p>사이트맵은, 구글 크롤러가 우리 사이트의 각 페이지들 정보를 가져갈 수 있도록 하는 것이다.</p>
<p>우리 서비스는 웹앱에 가깝기 때문에, 특히 랜딩 페이지나 소개 페이지도 없기 때문에 사이트맵을 적용하지 않으려 했다.</p>
<p>개인 정보만으로 이뤄진 우리 서비스는 외부에 공개되는 정보인 SiteMap을 갖는 것이 안 좋을 수 있는 것이다.</p>
<br/>

<p>그러나, 로그인이 들어가는 Onboarding 페이지는 공개하기로 했다. </p>
<p>사용자가 이를 통해 어떤 서비스인지 알 수 있고 (서비스 소개 문구가 적힌 이미지가 사용되고 있었다), 이 페이지조차 처리하지 않으면 외부로 공개하지 못할 것 같았기 때문이다.</p>
<br/>

<p>역시 Next.js에서 지원해주는 기능을 사용하면 간편했고, 처리할 페이지도 하나뿐이라 아래와 같이 간단한 코드로 해결했다.</p>
<pre><code class="language-typescript">// app/sitemap.ts
import type { MetadataRoute } from &quot;next&quot;;

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: `서비스URL/onboarding`,
      lastModified: new Date(),
      changeFrequency: &quot;always&quot;, // 사실 never로 해도 상관은 없을 듯.
      priority: 1,
    },
  ];
}
</code></pre>
<p>SiteMap은 원래 XML파일을 제공해야 하는데, Next에서 위 코드를 보고 빌드 시 해당 파일을 제작하는 것으로 보였다.</p>
<h3 id="robotstxt">Robots.txt</h3>
<p>이는 SiteMap과 반대로, 크롤러가 들어오지 못하게 하는 작업이다.</p>
<p>사실, 여기서 처리해도 무시하고 들어오는 경우도 있긴 하지만, 최소한의 처리라고 보면 될 것 간다.</p>
<p>이 부분을 대충 처리한다면, 불필요한 요청이 많아져 비용적으로 손해가 클 것이다.</p>
<br/>

<p>우리 서비스에선 onboarding만 허용하기로 했으므로 코드는 아래와 같았다.</p>
<pre><code class="language-typescript">// robots.ts

import type { MetadataRoute } from &quot;next&quot;;

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: &quot;*&quot;,
      allow: &quot;/onboarding&quot;,
      disallow: [
        &quot;/adding-goal/&quot;,
        &quot;/details/&quot;,
        &quot;/feed/&quot;,
        &quot;/group/&quot;,
        &quot;/mypage/&quot;,
        &quot;/notification/&quot;,
        &quot;/api/&quot;,
      ],
    },
    sitemap: `서비스URL/sitemap.xml`,
  };
}</code></pre>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/c0705d09-48fb-4bac-907c-048dcb6a3b47/image.png" alt=""></p>
<h1 id="마치며">마치며</h1>
<p>직접 적용해보기 전에는 어렵게만 느껴졌는데, 막상 해보니 생각보다 작업량도 많지 않고 부담이 없었다.</p>
<p>위 과정 이외에도, SEO최적화에 좋은 것은 Semantic HTML, 사이트 로딩 속도 등이 있는데, 이러한 부분은 다른 영역이라 생각해 담지 않았다.</p>
<br/>

<p>이 경험을 통해 왜 웹앱이 많지 않은지 느꼈다.</p>
<p>결국 검색이 잘 되어 신규 이용자를 늘리는 것이 신생 서비스의 최고 목표인데, 웹앱의 경우 SEO를 통해 사용자를 늘리기는 어려운 것이다.</p>
<p>외부로 드러낼 정보 대신, 사용자 개인 정보를 활용하는 것이 대부분인 웹앱은 시작부터 단점을 안고 가는 것이나 다름 없다.</p>
<br/>

<p>그러나, 이번에 사이트맵 적용 대상이었던 온보딩 페이지라거나, SSR 적용되었다면 고려했을 그룹 채팅 별 동적 OG 및 Metadata 등 SSR을 통해 충분히 최적화 가능한 부분이 있기 때문에 SSR의 장점을 확인할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RN과 웹뷰]]></title>
            <link>https://velog.io/@hys-lee/RN%EA%B3%BC-%EC%9B%B9%EB%B7%B0</link>
            <guid>https://velog.io/@hys-lee/RN%EA%B3%BC-%EC%9B%B9%EB%B7%B0</guid>
            <pubDate>Tue, 25 Nov 2025 08:38:27 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>사실, Motimo 서비스를 앱으로 배포하려고 했다.</p>
<p>애초에 모바일용으로 디자인이 이뤄졌고, 타겟 사용자를 고려했을 때 모바일 앱일 때 활용도가 높을 것으로 예상했기 때문이다.</p>
<br/>

<p>그러나 팀 구성이 웹 FE들만 있었기 때문에, 우선 웹 개발 이후 앱으로 고도화할 예정이었다.</p>
<p>결국 웹 버전으로 서비스를 배포하게 되었지만, 그 과정에서 있던 경험들은 유의미했기에 회고록을 작성하고자 한다.</p>
<h1 id="회고록">회고록</h1>
<h3 id="상황-설명">상황 설명</h3>
<p>Motimo는 인증 방식으로 OAuth2를 사용하고 있었는데, 이로 인해 생기는 문제점이 있었다.</p>
<p>Google 로그인은 Webview 컴포넌트 안에서 OAuth 방식의 동작이 제한된다는 것이다.</p>
<p>토큰을 ReactNative 안에서 받아서 처리해야 하기 때문에, 로그인 화면을 RN으로 제작할 필요가 있었다.</p>
<p>다행히, 이는 AI를 통해 빠르게 제작했다. 이미 디자인이 나왔기에 빠르게 적당한 코드를 생성시키고, 디테일은 직접 RN코드를 수정하며 처리했다.</p>
<h3 id="webview-적용하기">Webview 적용하기</h3>
<p>인증 관련해서 고려 사항들이 있었다.</p>
<ul>
<li><p>RN안에서 토큰을 발급받고, 이를 웹뷰 안으로 전송해야 함.</p>
</li>
<li><p>웹뷰 안에서의 로그아웃 등의 토큰 처리에 대해 RN 환경과 연동해야 한다.</p>
</li>
</ul>
<br/>

<p>방법을 찾아보면, expo의 WebBrowser의 메서드를 사용하거나 Google.useAuthRequest 등을 사용해야만 한다.</p>
<p>기본적으로 RN에서 인증을 요청하는 과정인데, 예전에는 WebBrowser의 메서드를 사용하면 꽤 편하게 문제를 해결 할 수 있는 것으로 보였다.</p>
<p>RN에서 로그인 관련 처리 후, 웹에서 사용하던 방식대로 AccessToken과 RefreshToken을 발급받게 되고, 이러한 토큰을 SecureStore에 저장 후 웹뷰 안쪽으로 보내주는 것이다.</p>
<p>그러나, 현재는 403 에러가 뜨는 것이 확인 되었다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/a5711caf-be3f-44b3-9976-de9da4c1eab4/image.png" alt=""></p>
<br/>

<p>OS가 제공하는 시스템 브라우저와 같이 신뢰할 수 있는 브라우저 환경에서 403에러가 발생하지 않는다는 것과 함께 다음의 방식을 사용해야 함을 봤다.</p>
<pre><code class="language-typescript">  const [request, response, promptAsync] = Google.useAuthRequest({
    androidClientId: &#39;YOUR_GOOGLE_ANDROID_CLIENT_ID.apps.googleusercontent.com&#39;,
    iosClientId: &#39;YOUR_GOOGLE_IOS_CLIENT_ID.apps.googleusercontent.com&#39;,
    // expoClientId: &#39;...&#39;, // Expo Go에서 테스트 시 웹 클라이언트 ID를 여기에 넣을 수도 있습니다.
  });

  useEffect(() =&gt; {
    if (response?.type === &#39;success&#39;) {
      // Google로부터 인증 성공!
      const { authentication } = response;
      const idToken = authentication?.idToken;</code></pre>
<p>그러나 이 방식은 idToken만을 얻기 때문에, 결국 세션 유지를 위해서는 백엔드에서 OAuth처리 후 토큰들을 보내줘야만 했다.</p>
<br/>
문제는, 고도화 과정은 나 혼자 진행하고 백엔드 분들의 도움을 받기 어려운 상황이었다는 점이다.



<h3 id="꼼수-useragent">꼼수.. userAgent</h3>
<p>어떻게든 웹뷰를 띄워보고 싶어 방법을 찾다, userAgent를 모바일 브라우저로 바꿔 미봉책을 마련할 수 있음을 알게 되었다.</p>
<p>아래의 user agent를 사용하면 되었다.</p>
<pre><code class="language-typescript">const FAKE_USER_AGENT =  &#39;Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Mobile Safari/537.36&#39;;</code></pre>
<br/>


<p>아래는 이를 통해 Webview를 적용해본 테스트 화면이다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/6445e9b1-04f4-4a5f-981c-d32d2805b691/image.gif" alt=""></p>
<h3 id="한계">한계</h3>
<p>결국 위 방법은 언제 막힐지 모르는 방식이라는 점과, 스토어 배포 시 반려의 원인이 될 요소가 될 것임이 단점이었다.</p>
<p>또한, 근본적으로 정석적인 방법을 사용하지 못한 이유는 백엔드와의 소통 부족임을 느꼈다.</p>
<p>애초에 백엔드에서 쿠키 기반의 인증 방식을 사용하지 못했는데, 관련해 인증과정을 함께 담당했떤 동료 FE의 말에 따르자면 해당 팀원 분이 쿠키 처리 방식을 어려워 하는 것으로 보였다고 했다.</p>
<br/>

<p>당시에는 보안이 취약해지긴 하겠지만, 서비스 제작 동기와 규모, 그리고 유지 면에서 생각해보면 내 작업에 크게 영향 없을 것이라 생각했다. 
또한, 해당 팀원 분은 다른 프로젝트도 동시에 진행하는 것으로 알고 있어 시간 관계상 어려웠나보다 생각이 들었다.</p>
<p>그러나 고도화 과정에서 이번 웹뷰 처리도 그렇고 SSR전환에 있어서도 인증 과정이 발목을 잡는 것을 통해 생각보다 인증이 미치는 영향이 크다는 것을 느꼈다.</p>
<p>또한, 확장성과 나 자신을 위해서라도 최소한의 기능 기준은 지켜지도록 팀원에게 확실히 요구할 수 있어야 함을 느꼈다.</p>
<h1 id="마치며">마치며</h1>
<p>이번 고도화 작업은 프로젝트 제작 기간이 종료되고 발표를 마친 이후에 혼자 했던 것이다.</p>
<p>다른 백엔드 한분이 더 계셨지만, 유지보수에 대해 생각이 없으셨기 때문에 경험을 쌓고자 스스로 진행했었다.</p>
<br/>
프로젝트를 진행하면서 조금 더 친분을 쌓고 분위기를 만들었다면 그래도 다같이 고도화를 진행하며 좋은 결과를 얻을 수 있었지 않았을까 아쉽다.

<p>소프트 스킬의 중요성도 느낄 수 있었던 경험이었다.</p>
<br/>

<p>그래도 FE 인원끼리 모여 짧게 진행했떤 RN 스터디 내용을 활용해서 로그인 화면도 만들고, 웹뷰도 띄워봤었기에 뜻 깊었다.</p>
<p>이를 잘 살려 다른 프로젝트에 적용해 꼭 앱을 배포할 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Focus Trap]]></title>
            <link>https://velog.io/@hys-lee/Focus-Trap</link>
            <guid>https://velog.io/@hys-lee/Focus-Trap</guid>
            <pubDate>Tue, 25 Nov 2025 07:57:48 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>이번에는, 이슈 처리 과정 중 있던, Focus Trap 문제인 줄 알았으나 다른 어이없는 것이 원인이었던 사건을 간단하게 이야기 해보려고 한다.</p>
<p>이 후 Focus Trap에 대해서 조금 알아보며 정리했던 것들도 있기 때문에 글을 작성하게 되었다.</p>
<h1 id="이슈-해결기">이슈 해결기</h1>
<h3 id="상황을-설명하자면">상황을 설명하자면..</h3>
<p>서비스에 바텀 탭바가 존재했다.</p>
<p>시연 과정에서 파악한 이슈인데, 특정 페이지에서 바텀 탭바를 클릭해 해당 페이지로 이동하는 과정에서 발생했다.</p>
<p>컨텐츠 화면을 클릭 후 바텀 탭바를 클릭해야 링크로 이동되었던 것이다.</p>
<br/>
마치 포커스를 다른 영역에 주어 이동시키고 바텀 탭바를 클릭할 때 동작하는 것으로 보였기 때문에 포커스 트랩을 원인으로 추측하고 확인해보았다.

<h3 id="결론">결론</h3>
<p>갑작스럽지만, 결론부터 말하겠다.</p>
<p>바텀 탭바 위에 있던 바텀시트에 있는 <code>after</code>가 바텀 탭바를 가려서 클릭되지 않았던 것이었다.</p>
<br/>

<p>바텀 시트 높이를 조금 올린 후 테스트를 진행 시 정상 작동함을 확인했기 때문에 확실해 보인다.</p>
<p>이에, 해당 after영역에 <code>point-events:none</code>처리를 통해 해결했다.</p>
<h3 id="focus-trap">Focus Trap</h3>
<p>직접 코드를 뜯어 after부분을 발견하기 전, Focus Trap이 적용된 방식을 보며 이를 알아보았다.</p>
<br/>

<p>Motimo는 바텀시트를 위해 Vaul 라이브러리를 사용하고 있는데, 이 라이브러리는 Radix를 기반으로 하는지, <code>span</code>의 <code>data-radix-focus-guard</code>라는 속성 명을 갖는 가드를 사용하여 Focus Trap을 적용하고 있다.</p>
<p>이 방식은 Top-FocusGuard와 Bottom-FocusGuard를 두어 tab및 shft+tab으로 포커스 이동 시 해당 이들로 둘러쌓인 범위 안에서 포커스가 이동하도록 만든다고 한다.</p>
<br/>

<p>보다 기본적인 방식으로는, Focus Trap적용 영역에서 tab 등의 동작에 대해, 이동할 컴포넌트를 지정해주는 방식이 있다.</p>
<h1 id="마치며">마치며</h1>
<p>Focus Trap에 대해 자세히 공부하지 않고, 방법론만 알아봤기 때문에 글이 짧다.</p>
<p>사실 이 경험을 통해 접근성 기술들에 대해 처음으로 고민해봤기 때문에 꽤 인상 깊었다.</p>
<br/>
이외에도 많은 접근성 기술들이 있는 것을 알게 되었는데, 사실 이 분야도 깊이 들어가면 한 없이 깊어질 수 있다는 것을 느꼈다. 

<p>접근성 관련 조항 등을 숙지하고, 기술적으로 해결하며 이를 검증하는데는 많은 전문성이 필요한 것이다.</p>
<p>토스도 접근성 기술자를 따로 채용하는 등의 행보를 보인 것으로 보아 위를 확실히 알 수 있었다.</p>
<br/>

<p>사실상 일반적인 웹 개발자의 경우는 Radix 등의 headless 컴포넌트를 사용한다거나, 내부 규칙에 맞게 처리하는 등으로 해결할 것이기에 깊이 공부할 기회가 없기도 하겠지만, 활용도 있어보이는 기술들도 있기 때문에 기회가 된다면 공부하고자 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전역 디바운스 적용기]]></title>
            <link>https://velog.io/@hys-lee/%EC%A0%84%EC%97%AD-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8A%A4-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@hys-lee/%EC%A0%84%EC%97%AD-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8A%A4-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Tue, 25 Nov 2025 07:30:42 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>저번 낙관적 업데이트에 이어서, 이번에는 전역 디바운스 적용기이다.</p>
<p>살짝 언급되었지만, Motimo FE에서 API핸들링 관련 코드는 전역으로 관리되고 있다.</p>
<br/>

<p>따라서 서비스 시연 시간에 동료 FE들에게 Debounce적용은 기본이라는 말을 들었을 때, &#39;맞다!&#39; 이외에도 &#39;어떻게 하지?&#39; 라는 생각이 들었다.</p>
<p>일반적인 디바운스 작업은 이벤트 핸들링 근처에 붙이는 것으로 알고 있기 때문이다.</p>
<br/>

<p>그러나 그러기엔 너무 서비스 복잡도가 높아졌기 때문에, 전역에서 처리하는 방법을 생각하게 되었고 꽤 성공적으로 해결했기에 기록을 남긴다.</p>
<h1 id="적용기">적용기</h1>
<h3 id="전역-api-핸들링">전역 API 핸들링</h3>
<p>우선 배경 상황을 설명해야겠다.</p>
<p>꽤 설명하기 복잡하기 때문에, 우선 코드를 첨부한다.</p>
<pre><code class="language-typescript">const httpClient = new HttpClient({
  baseUrl: (() =&gt; {
    return process.env.API_URL || &quot;&quot;;
  })(),
...

// API 클라이언트 인스턴스 생성
export const api = new Api(httpClient);
</code></pre>
<p>차근 차근 설명하겠다.</p>
<ul>
<li><p>HttpClient와 Api 클래스는 <code>swagger-typescript-api</code>를 통해 제작되었다.</p>
<ul>
<li><p>htteClient는 baseUrl, Authorization Header 등의 처리를 담당한다.</p>
</li>
<li><p>api는 httpClient를 사용하여 동작하는 각 api들의 모음인 object이다.</p>
</li>
</ul>
</li>
</ul>
<br/>

<p>즉, API 동작에 직접 관여하는 디바운스 작업은 HttpClient나 Api 클래스 내부에 추가할 수 없다.</p>
<p>re-generated를 통해 코드가 삭제될 수도 있는 것이다..</p>
<h3 id="debounce-wrapper">Debounce-Wrapper</h3>
<p>이 문제를 해결하기 위해, httpClient.request를 감싸는 Debounce-Wrapper를 만들어 감싼 뒤, httpClient.request에 덮어쓰는 방식을 사용했다.</p>
<p>기존 구조를 건드리지 않으면서 기능을 편입시키는 유일한 방식으로 보였기 때문이다.</p>
<p>타입처리는 아래와 같이 해결했다.</p>
<pre><code class="language-typescript">const debouncer = &lt;T, E&gt;(apiRequest: typeof httpClient.request&lt;T, E&gt;) =&gt; {</code></pre>
<h3 id="전역-디바운스">전역 디바운스</h3>
<p>디바운스를 전역처리하기 위해, 개별 요청에 대해 timer 처리가 필요했다.</p>
<p>이를 위해 Object에 API URL을 key로, timer id값 (혹은 undefined)를 value로 처리했다.</p>
<br/>

<p>핵심 쟁점은 httpClient.request의 반환인 Promise를 그대로 반환하면서도 디바운스 동작을 처리해야 한다는 것이었음.</p>
<p>이로 인해 생기는 고려사항은 아래와 같았다.</p>
<ul>
<li><p>Promise안에 setTimeout 두기</p>
<ul>
<li>일반적인 디바운스는 setTimeout안에 비동기 처리를 하지만, 우리는 Promise를 즉시 return해야 하므로.</li>
</ul>
</li>
<li><p>각 API URL에 대한 타이머 Object 클로저 활용</p>
<ul>
<li>key를 API URL, 타이머 id를 value로 하여 Wrapper가 반환하는 함수 외부에서 공통적으로 접근할 수 있도록.</li>
</ul>
</li>
<li><p>Promise 외부 reject 할당 및 실행</p>
<ul>
<li>동일 url에 대해 timer가 존재하면, Promise를 reject하고 clearTimeout을 해야 하므로.</li>
</ul>
</li>
</ul>
<p>위의 고려사항을 따져 작성한 전역 디바운서는 아래와 같았다.</p>
<pre><code class="language-typescript">// 주의! 아래 구조에 결함이 있음. 추후 설명 예정

const debounceer = &lt;T, E&gt;(apiRequest: typeof httpClient.request&lt;T, E&gt;) =&gt; {
  const timeLimit = 300;
  const timerDictionary: { [apiFullUrl: string]: number | undefined } = {};
  let rejectTimer: (reason?: any) =&gt; void;
  return (
    requestParams: Parameters&lt;typeof httpClient.request&lt;T, E&gt;&gt;[0],
  ): ReturnType&lt;typeof httpClient.request&lt;T&gt;&gt; =&gt; {
    const apiFullUrl = `${requestParams.path}?${requestParams.query}`;
    const timer = timerDictionary[apiFullUrl];

    if (timer) {
      clearTimeout(timer);
      rejectTimer();
      // rejectTimer(&quot;debouncing&quot;);
    }
    const apiRes: Promise&lt;T&gt; = new Promise((resolve, reject) =&gt; {
      rejectTimer = () =&gt; reject(&quot;debouncing&quot;);
      timerDictionary[apiFullUrl] = Number(
        // timer = Number(
        setTimeout(async () =&gt; {
          try {
            // 비동기 동작 이후..
            timerDictionary[apiFullUrl] = undefined; // timer비워주기..
            resolve(res);
          } catch (error) {
              showToast(`API ERROR`, new Date());
          }
        }, timeLimit),
      );
    });</code></pre>
<p>디바운스 처리에 대해 Toast 동작도 추가했기에 꽤 만족스러웠으나...</p>
<h3 id="reject-결함---api-url별-reject">Reject 결함 - API URL별 reject</h3>
<p>이번 회고록을 작성하며 다시 생각해보니, rejectTimer를 timer마다 자신의 것을 갖고 있어야 했다.</p>
<p>언뜻 정상 작동했던 것으로 보이는 이유는, 일반적으로 디바운스가 발생하는 상황에서는 동일 fetching이 연속적으로 발생하므로, rejectTimer함수는 이전 Promise에 대한 것임이 잘 들어맞았던 것임.</p>
<p>이 부분을 수정하면 더 좋은 코드가 될 것이다.</p>
<h3 id="ssr-전환에서의-오류">SSR 전환에서의 오류</h3>
<p>이 디바운싱 처리로 인해 SSR 전환 과정에서 어려움을 겪게 되었는데, 바로 에러 처리 때문이었음.</p>
<p>정확히는 전역 API관리 때문에 어려움을 겪었던 것인데, 디바운스가 꽤 큰 영향이 있었음.</p>
<p>reject등을 통해 에러를 던지게 되는데, 이 때 서버 환경에서 받은 에러를 통해 Streaming SSR 과정에서 RSC Payload 전송 과정에서 문제가 발생하여 아예 서비스가 동작하지 않게 되었던 것.</p>
<br/>

<hr>
<p>채용 시즌에, 토스에 지원했었는데 관련 테스트 문제 중 <strong>Result Pattern</strong> 이라 알려진 구조를 제작하는 것이 있었다.</p>
<p>해당 테스트를 망치고 이를 공부하며 도대체 왜 이런 구조를 사용하는지 이해가 가지 않았는데, 이번 경험을 통해 아예 에러를 발생시키면 안되는 상황을 알게 되어 뜻깊었다.</p>
<hr>
<br/>

<p>아무튼, 서버 환경에서 에러를 발생시키지 않도록, 그리고 showToast와 같이 전역 상태 훅을 사용하지 않도록 <code>typeof window===&#39;window&#39;</code>를 통해 분기 처리를 수행하여 전역 디바운스를 고도화했다.</p>
<h1 id="마치며">마치며</h1>
<p>이번 회고록을 작성하며, Gemini에게 다시 몇가지 물어보고 생각을 정리하게 되었다.</p>
<p>애초에 왜 Promise 안에 setTimeout을 넣어야 했는지, Reject관련해서 구조적 문제가 있는게 맞는지 등등.</p>
<p>이 과정에서 이렇게 Promise제어권을 외부에 전달하여 처리하는 방법을 <strong>Deferred Pattern</strong>이라 하며, Promise를 처리하는 여러 패턴 중 하나라는 것을 알게 되었다.</p>
<br/>

<p>또한, 제한된 상황에서 Wrapping 방식으로 기능을 추가했던 경험은 다른 상황에서도 유용할 것 같았고, 사실 토큰 재발급 처리도 이 방식으로 API 결과를 감싸서 처리했는데 이는 별로 좋지 못한 상황이기에 작성하지 않기로 했다.</p>
<p>간단하게 정리하자면, 401에러에 대해 토큰 재발급 API 호출을 전역으로 처리하기 위한 것이었는데, 애초에 이러한 방식으로 토큰을 운영하는 것 자체가 문제인 것 같았다. 확실히 아예 BE에 맡기던가, FE라면 Middleware나 Proxy (Next.js에서) 를 통해 관리하는 것이 옳기 때문이다.</p>
<br/>

<p>결과적으로 요구사항을 고려하며 머리를 짜내서 생각해낸 방법이 이미 존재하는 방법임을 알게 되어 이러한 패턴들을 공부해야함을 느꼈고, 
조금 더 잘 정리하고 테스트를 작성하는 등의 후처리 과정을 통해 이번과 같은 결점이 없도록 개발해야 함을 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Optimistic Update 적용기]]></title>
            <link>https://velog.io/@hys-lee/Optimistic-Update-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@hys-lee/Optimistic-Update-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Tue, 25 Nov 2025 06:16:34 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>9월달까지 정신없이 Prography 활동을 마치고, 또 11월 중순까지 취업 활동이 한바탕 휩쓸어서 이제서야 정리하게 되었다.</p>
<p>요번년도 초부터 Prography라는 IT 커뮤니티, 흔히 말해 IT 동아리를 하게 되었는데 생각치 못 하게 얻은 점들이 많았다.</p>
<p>이를 자소서 작성과 포트폴리오 업데이트에 사용하면서, 미쳐 넣지 못한 내용들이 많음을 느꼈다.</p>
<p>오히려 마이너스일 수 있는 것들이라거나, 포트폴리오에 넣기엔 내용이 적다든가...</p>
<p>이러한 점들을 정리해서 올리고, 회고하는 시간을 가지려 한다.</p>
<h1 id="적용기">적용기</h1>
<p>사실 이 전에는 낙관적 업데이트가 있다는 것만 알았고, 적용해본 적은 없었다.</p>
<p>마침 서비스 개발 이후 고도화할 시간이 있었고, 또 필요했기에 적용해보게 되었다.</p>
<h3 id="상황은-다음과-같았다">상황은 다음과 같았다.</h3>
<ul>
<li><p>체크 박스 동작에 대해 비동기 fetching이 이뤄졌다.</p>
</li>
<li><p>체크 박스 UI였기에, 동기적으로 동작하는 것처럼 보일 필요가 있었다.</p>
</li>
<li><p>사용자가 빈번하게 체크/해제 하는 상황이 생길 것이기에 빠른 반응 속도가 필요했다.</p>
</li>
</ul>
<p>이러한 상황에서 여기엔 낙관적 업데이트가 있지 않으면 너무 불편할 것이라 생각해 적용해보게 되었다.</p>
<h3 id="useoptimistc">useOptimistc</h3>
<p>처음에 고려했던 것은 React 19에 정식 편입된 useOptimistic이었다.</p>
<p>당시 고도화를 고려하며 React 19의 각종 hooks를 공부하고 있었는데, 여타 라이브라리를 사용할 필요 없이 낙관적 업데이트를 적용할 수 있어 깔끔해 보였다.</p>
<p>Transition을 비롯해 React 19 기능들을 적용하고 싶은 욕심도 있었다.</p>
<p>그러나, 결론만 말하자면 이 방식은 포기하게 되었다.</p>
<p>useOptimistic을 사용한다면, swr을 통해 관리하고 있는 서버상태(클라이언트 캐시)를 보존할 수 있어 재사용성을 높일 수 있다는 장점이 있다.</p>
<p>하지만 useSWR을 통한 GET, 체크 동작과 연결된 fetching의 PATCH, 그리고 useOptimistic의 optimisticValue라는 징검다리들의 연동 과정에 문제가 있었다.</p>
<p>순서대로 설명하자면 아래와 같다.</p>
<ol>
<li><p>체크 동작으로 인해 동기적으로 optimisticValue를 체크박스에서 상태로 사용하고, fetching이 실행된다.</p>
<p> 이는 startTransition내부에 낙관적 업데이트 동작을 먼저 수행하도록 하기에 발생하는 순서이다.</p>
<p> 참고로, useOptimistic의 상태를 변경하는 함수는 action안에서 실행되어야 하기에 startTransition을 사용하게 되었다.</p>
<p> RCC들만 사용하고 있었기 때문에 복잡성을 늘리고 싶지 않았기 때문이었다.</p>
</li>
</ol>
<br/>

<ol start="2">
<li><p>startTransition안에 PATCH 동작이 완료된다면 이 action은 pending 상태에서 벗어나게 되어, 내부에 관련된 useOptimistic에선 초기값을 사용하게 된다.</p>
<p> 여기서 useOptimistic에 주입된 초기값은 useSWR에서의 데이터를 사용할 것이므로, fetching 이후에 관련 mutate를 실행하게 될 것이다.</p>
</li>
</ol>
<br/>

<ol start="3">
<li><p>GET요청이 다시 발생하고, 해당 결과를 받아 리렌더링하여 useOptimistic에 초기값을 전달한다.</p>
<p> 변경된 초기값을 체크박스에서 반영하여 안정된 데이터를 사용하게 된다.</p>
</li>
</ol>
<br/>

<p>중간에 useOptimistic이 사용되면서 과정이 복잡해진 것에 더해, 헛점이 있다.</p>
<p>바로, mutate이후 Transition은 종료되어 useOptimstic에선 초기값을 사용하게 되는데, 이 땐 아직 새로운 GET 요청이 완료되기 전이라는 것이다.</p>
<blockquote>
<p>즉, 낙관적 업데이트 -&gt; 다시 원본 값으로 회귀 -&gt; PATCH 반영된 GET 결과로 업데이트</p>
<p>이 과정이 발생하게 된다.</p>
</blockquote>
<p>오히려 UX를 해치는 <strong>Flickering</strong>이 발생하는 것이다..!</p>
<h3 id="swr의-bound-mutate">swr의 bound mutate</h3>
<p>useOptimistic방식이 제외되면서, 결국 서버 상태에 대해 직접 낙관적 업데이트 하는 방식을 선택하게 되었다.</p>
<p>useSWRMutation를 사용하는 방식과 bound mutate를 사용하는 방식 중 bound mutate를 선택했다.</p>
<br/>

<p>현재 API관련 핸들링, useSWR과의 연결 과정을 전역적으로 처리하고 있었고, 이 과정에서 AI Generated 코드를 사용하고 있었다.</p>
<p>(FE팀원 분이 만들어주셨는데, 편리하기도 하고 좋은 구조를 발견하는 등 장점도 있었지만, 함부로 건드리기 애매한 단점이 있었다. 관련해서 추후 이야기 하게 될 것 같다.)</p>
<p>이 때, key값을 API 그룹과 메서드명 그리고 props를 사용해 배정해주어 useSWR을 만들어주도록 처리하고 있었다.</p>
<p>useSWRMutation은 정확한 key값을 전달해줘야 했기 때문에 우리 방식에는 어려운 점이 많았다.</p>
<br/>

<p>이에, bound mutate를 사용했는데, 간단하게 Optimistic Data를 인자로 넘겨주만 됐다.</p>
<h3 id="체크-표시-ui-에러">체크 표시 UI 에러</h3>
<p>이상이 없어야 할 동작에 이슈가 발생했다.</p>
<p>체크 표시가 씹히는 경우가 있던 것이다.</p>
<p>해당 체크박스 컴포넌트에 전달되는 값을 체크해보니, 정상적으로 데이터는 전달되고 있어서 더 이해가 가지 않았다.</p>
<br/>

<p>문제의 원인은 CSS 적용에 있었다.</p>
<p>기존에는 다음과 같은 방식으로 되어있었다.</p>
<pre><code class="language-javascript">
function CheckBox(checked, ...props){
    return &lt;input
  className=&quot;bg-white checked:bg-blue&quot;
  type=&quot;checkbox&quot; checked={checked}/&gt;
}

</code></pre>
<p>아래와 같이 수정했더니 정상 동작 했다.</p>
<pre><code class="language-javascript">function CheckBox(checked, ...props){
    return &lt;input
  className={checked?&quot;checked:bg-blue&quot;:&quot;bg-white&quot;}
  type=&quot;checkbox&quot; checked={checked}/&gt;
}</code></pre>
<br/>

<p>해결된 이유를 몰라 Gemini와 대화하며 얻은 결론은, <strong>제어권 문제</strong>가 있었다는 것이다.</p>
<h3 id="제어권">제어권</h3>
<p>이는 FE라면 누구나 들어봤던 <strong>제어/비제어 컴포넌트</strong> 이야기를 확장한 것으로 보였다.</p>
<p>결국 제어/비제어 컴포넌트는 제어권을 React에서 가지거나 브라우저가 가지는 이야기인 것이다.</p>
<br/>

<p>이 상황 역시 같다.</p>
<p>기존 방식은 <code>checked:</code>를 통해 브라우저가 컴포넌트의 상태를 파악하고 CSS를 입히도록 하고 있다.</p>
<p>변경 이후, checked 상태 값을 통해 React에서 두 CSS 중 선택하도록 되어있다.</p>
<br/>

<p>이 상황을 자세히 보자면, 애초에 CheckBox 컴포넌트가 checked 상태를 외부로부터 주입받지만 onChange를 처리하는 방식에서 문제가 있었다.</p>
<p>해당 컴포넌트를 사용하는 구조는 아래와 같다.</p>
<pre><code class="language-javascript">&lt;label htmlFor=&quot;id&quot; onClick={handleClick}&gt;
    &lt;CheckBox id=&quot;id&quot; checked={checked} readonly/&gt;  
&lt;/label&gt;</code></pre>
<p>CheckBox를 제어 방식으로 사용하고 있음에도 브라우저에게 제어권을 넘기려고 했던 것이었다..!</p>
<br/>

<blockquote>
<p>참고로, 제어/비제어 방식 모두 사용할 수 있도록 만드려면 value 인자로 들어오는 값의 존재 여부에 따라 처리하면 된다고 한다.</p>
<p>radix 같은 곳에서 자주 사용하는 방식이라고 하니, 직접 만들 때 참고하면 좋을 듯 하다.</p>
</blockquote>
<h3 id="고도화">고도화</h3>
<p>추가적인 고도화 사항으로, 체크 동작으로 인해 리스트에서 순서가 바뀌는 케이스가 많았기에 UX를 개선하고자 애니메이션을 추가했다.
<br/>
motion의 AnimatePresense와 layout을 사용해 위치 변경에 대한 애니메이션을 추가하고, </p>
<p>비동기 동작으로 최종 업데이트 된 GET데이터를 반영하기 전에 opacity를 조절해 레이아웃 애니메이션 동작을 사용자에게 알리는 방식으로 처리했다.</p>
<h1 id="마치며">마치며</h1>
<p>처음으로 Optimistic Update를 적용해봤는데, 적확한 상황에 알맞은 방식으로 사용하지 않는다면 해가 될 수 있음을 느꼈던 경험이었다.</p>
<br/>

<p>동작 메커니즘에 대해 몰랐다면 이슈 원인을 파악할 수 없었을 것이므로, 기술을 적용할 때 배경지식의 중요성도 느끼게 되었다. </p>
<br/>
추가로, 리액트 동시성 처리와 브라우저 동작에 대해 궁금증이 커졌다. 특히, 브라우저가 어떻게 동작하는지가 흥미로웠는데 문제는 이를 공부하기 어렵다는 것이다. 

<p>당장 Next든 React든 FE기술들에 대해서는 공식문서들이 많고, 관련 컨퍼런스 등을 통해서도 많은 것들을 배울 수 있지만, 웹 FE가 종속되어있는 브라우저에 대해서는 제대로 공부할 수 있는 문서가 없어보였다.</p>
<p>누군가 &#39;근본&#39;에 대한 중요성을 설파하던데, 이 근본이라는 것이 정말 어려운 것 같다.</p>
<p>정말 파고들자면 브라우저 동작까지 갈 것 같은데, 이에 대한 자료가 있을까?
AI는 피상적으로 인터넷 환경에 널부러진 각종 자료들을 긁어 모아 정리해서 보여줄 뿐이라는 생각이 들며 기반에 대해 불안해졌다.</p>
<br/>

<p>궁금증과 불안을 낳기도 했지만, 좋은 경험이었던 것은 분명하다.</p>
<p>비록 너무 하찮아보여 포트폴리오에 넣지는 못했지만, 잊어버리기엔 아까워 제일 처음으로 기록으로 남겼다.</p>
<p>다른 하찮은(?) 경험들도 계속 작성하여 되돌아 볼 수 있도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FP와 프론트엔드]]></title>
            <link>https://velog.io/@hys-lee/FP%EC%99%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C</link>
            <guid>https://velog.io/@hys-lee/FP%EC%99%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C</guid>
            <pubDate>Thu, 27 Mar 2025 03:11:31 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하면서">시작하면서</h2>
<p>이전 OOP를 프론트에 적용하는 고민을 개인 프로젝트를 통해 경험하고, 여러 관련 문제들을 해결하려 했다.</p>
<p>제법 복잡한 상황을 맞게 되었는데, OOP적 관점만으로는 개인적으로 이를 정리하기 어려웠다.</p>
<p>이에 FP를 공부하고 이 관점에서 문제를 해결했기에, 이를 기록하고자 한다.</p>
<h1 id="본론">본론</h1>
<h2 id="문제-상황">문제 상황</h2>
<p>차트 라이브러리를 사용하며, 위에 리액트 컴포넌트를 띄워야 했다.</p>
<p>차트는 꺽은선 그래프인데, 각 데이터 지점을 클릭 시 &#39;마크 포인트&#39;가 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/4766e3a3-af83-44da-b4ae-311fa68086a1/image.png" alt=""></p>
<p>위의 &#39;마크 포인트&#39;들에 대응해서 리액트 컴포넌트 Annotation을 띄우게 된다.</p>
<p>또한, 흰색 점선으로 둘러쌓인 마크포인트를 &#39;포커스&#39;라고 하는데,</p>
<p>현재 선택한 지점을 의미한다.</p>
<p>이는 해당 마크포인트나 관련 Annotation를 클릭 시, 혹은 데이터 지점을 처음 클릭할 때 활성화 된다.</p>
<p>상황이 복잡한 것 같으니, 정리하자면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/bdbc3131-1d54-4306-8265-d5ab09c8448d/image.png" alt=""></p>
<p>위를 더 복잡하게 하는 조건은 아래와 같다.</p>
<ol>
<li>마크 포인트 클릭으로는 Annotation에 필요한 데이터를 전부 얻을 수 없다.</li>
<li>포커스 데이터는 외부 사용을 위해 전역 상태로 관리한다.</li>
<li>차트는 Echart를 사용하기에, 적절한 동작을 위해 instance에 대해 필요한 기능을 추가해야 한다. (리액트 친화적이지 않다</li>
</ol>
<p>덕분에 코드는 아래와 같았다.</p>
<pre><code class="language-typescript">/*각종 import*/

/* 기본 그래프 데이터 생성 */

interface MarkData {
  [id: string]: MarkDataContent;
  // focus: MarkDataContent;
}

/* 기본 그래프 옵션 정의 */

const RealFlowEcharts = () =&gt; {
    /*아래는 hooks*/

    /*차트 ref랑 이거로 저장하는 echartinstance 상태 저장*/

    /*markData상태 (빈거초기화)랑, atom으로부터 focus상태*/

    /* focus정보 useEffect안에서 deps없이 접근 위해 ref에 state저장 */

  useEffect(() =&gt; {
    /* &lt;&lt;포커스 정보 변화를 markData에 반영&gt;&gt;
  (외부에서 atom으로 focus수정을 동일 대상의 markpoint의 정보에 반영위해)*/
  }, [focusInfo]);

  useEffect(() =&gt; {
      /*&lt;&lt;그래프 인스턴스 처리&gt;&gt;*/
        /*첫 렌더링에 instance를 state에 저장.*/ 
    }, []);

  useEffect(() =&gt; {
      // 이건 기본. echartinstance있을 때만 동작시키도록.
    if (!echartInstance) return;

        /*렌더링용 마크포인트 데이터 생성*/        
        /*
            렌더링용 마크포인트 데이터 정리
                  markData에 있는 데이터 렌더링용으로 정리
                  그 중 focus에 대응되는 것은 focus용으로 정리
                */
                /*focus에 있는 것(markData에는 없는)도 렌더링용으로 편입*/   
        /* &lt;&lt;마크포인트 그래프에 반영&gt;&gt; */

  }, [echartInstance, markData, focusInfo]);

  useEffect(() =&gt; {
    if (!echartInstance) return;
        /*&lt;&lt;클릭 이벤트 핸들러 등록&gt;&gt;*/
        /*클릭 이벤트에서 markPoint와 series는 제외.*/

        /*공통 데이터 정리하기 - accumulatedValue(그래프에서 정확한 y값), markData저장용 공통 데이터 템플릿*/

        /*series(데이터 포인트)클릭: 포커스에 markData정보랑 같이 추가. (tmpFocus)*/
        /*
            markpoint클릭: 
                - focus라면 
                    - tmpFocus라면: &#39;시리즈-날짜&#39;를 키로, focus에서 대충 걸러서 markData로 추가 / focus 비활성화
                    - 원본있는 focus라면: focus비활성화
                - 아니라면: focus로 데이터 반영 및 활성화 및 original 연결
        */ // 불필요한 코드가 많긴 했네..

    return /*클릭 이벤트 핸들러 제거*/
  }, [echartInstance, defaultDatum]); // defaultDatum위치 정리한번 해야함.

    /* annotation 제한 범위 위해 */
  const constraintsRef = useRef(null);

  return (
    &lt;div style={{ height: &#39;100%&#39;, width: &#39;100%&#39; }} ref={constraintsRef}&gt;
      &lt;ReactECharts
        ref={chartRef}
        style={{ width: &#39;100%&#39;, height: &#39;100%&#39; }}
        option={defaultOption}
      /&gt;
      {Object.entries(markData)
        .map(
            ([key,/*마크데이터들*/,],idx) =&gt; (
            &lt;ChartAnnotation            
              /* 클릭 이벤트에 이거랑 관련된 markData를 focus로 편입 및 활성화 */             
                /*key랑 위치 및 내용 데이터 넣기*/
            /&gt;
          )
        )}
    &lt;/div&gt;
  );
};
export default RealFlowEcharts;
</code></pre>
<p>구조만 표현하기 위해 내부 코드를 날리고 주석으로 대체했지만, 문제가 많다.</p>
<blockquote>
</blockquote>
<ul>
<li>각 기능마다 useEffect가 사용되고 있다.</li>
<li>관리 포인트가 산재되어 디버깅이 어렵다.</li>
<li>추상화가 없어 가독성이 떨어진다.
...
=&gt; 유지보수가 어렵다</li>
</ul>
<h3 id="oop">OOP?</h3>
<p>물론, 충분하게 추상화와 모듈화를 하지 않아보인다.</p>
<p>그러나 변명하자면, 컴포넌트 구현부에 존재하는 코드들 중 마지막의 constraintsRef를 제외하곤 차트에 대한 것이었기에 크게 차이나지 않았을 것이다.
<br></p>
<p>또한, 각 기능들을 커스텀 훅으로 묶어내는 것으로도 문제를 해결할 수 없었을 것이다.</p>
<p>hooks간 동작의 연결이 필요한 부분이 적었기 때문이다.</p>
<p>그저 1겹으로 된 단순한 캡슐화를 했을 것이다.</p>
<h2 id="fp-고민">FP 고민</h2>
<p>FP가 데이터와 동작을 잘 구조화 할 수 있는 힌트가 되지 않을까 싶어 공부하게 되었다.</p>
<p>이 내용은 추후 작성하겠지만, 제일 인상깊었던 부분은 &#39;액션&#39;에 대한 접근이었다.</p>
<p>FP에서 side-effect가 존재하는 함수를 &#39;액션&#39;이라고 부를 수 있다.
액션이 아닌 순수 함수라면 &#39;계산&#39;이라고 부를 수 있다.</p>
<blockquote>
</blockquote>
<ul>
<li>계산은 신뢰할 수 있는 영역에 속하기 때문에, 최대한 액션에서 계산을 뽑아내야 한다.</li>
<li>이러한 액션을 참조하는 함수도 액션이 되기 때문에, 액션을 최대한 상위 계층에 위치시켜야 한다.</li>
</ul>
<p>위는 프론트엔드에서도 적용 가능한 부분이라고 봤고, 이를 고려해 리팩토링을 진행했다.</p>
<h2 id="fp-적용">FP 적용</h2>
<p>우선 계층을 분리했다.</p>
<p>FP에서 배운 어니언 아키텍쳐와 액션을 최상위 계층으로 올린다는 것을 고려해 다음과 같이 분리했다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/757a2ed6-22be-480b-af90-226d1642a495/image.png" alt=""></p>
<p>계산으로 드러낼 수 있던 부분은 기능 로직들 뿐이었으나, 이는 복잡한 로직들을 정리한다는 의도에 맞았기에 적절한 행동이었다.</p>
<p>기능의 동작 추가는 차트 instance에 적용하는 방식이었기에, &#39;기능 로직들&#39;에서 &#39;차트 라이브러리&#39;를 직접 사용하게 할 순 없었다.</p>
<p>또한, 차트 라이브러리를 컴포넌트로 사용했기 때문에 위의 구조가 되었다.</p>
<br>
코드로 보면 아래와 같다.

<pre><code class="language-typescript">// ReusableEchart.tsx - 차트 라이브러리

const ReusableEchart = memo(
  ({ cachedGetInstance, defaultOption }: ReusableEchartProps) =&gt; {
    const chartRef = useRef(null);

    useEffect(() =&gt; {
      // 기본 차트 옵션 적용, 인스턴스 외부로 전달

          chartInstance = echarts.init(chartRef.current);

        chartInstance.setOption(defaultOption);

        cachedGetInstance(chartInstance);

      return () =&gt; {
        if (chartInstance) {
          chartInstance.dispose();
        }
      };
    }, [cachedGetInstance, chartRef]);

    return (
      &lt;div ref={chartRef} style={{ width: &#39;100%&#39;, height: &#39;100%&#39; }}&gt;&lt;/div&gt;
    );
  }
);
export default ReusableEchart;</code></pre>
<pre><code class="language-typescript">// chartHandlers.ts - 기능 로직들

const makeActualFlowEchartOption = (defaultDate, defaultDatum) =&gt; ({
    // date와 datum로 기본 옵션 만들기
});


const addEventHandlerNew = (
  param: echarts.ECElementEvent,
  defaultDatum,
  determineFocus
): {
  type: &#39;mark&#39; | &#39;focus&#39;;
  data?: MarkDataContent | null;
  origin?: string;
}[] =&gt; {
  if (param.componentType !== &#39;markPoint&#39; &amp;&amp; param.componentType !== &#39;series&#39;) {
    return [];
  }

  if (param.componentType === &#39;series&#39;) {
    const accumulatedValue = getYcoord(
      defaultDatum,
      param.seriesIndex as number,
      param.dataIndex
    );
    const markDataTemplate: any = {
      asset: param.seriesName || &#39;이름 없음&#39;,
      date: param.name,
      type: &#39;normal&#39;,
      value: Number.isInteger(Number(param.value)) ? Number(param.value) : 0,
      viewPos: [param.event!.offsetX, param.event!.offsetY],
      dataIndex: param.dataIndex || 0,
      seriesIndex: param.seriesIndex as number,
      accumulatedValue,
    };
    return [{ type: &#39;focus&#39;, data: markDataTemplate }];
  } else {
    const paramData = param.data as ParamData;
    const isFocus = determineFocus(
      paramData.yAxis,
      paramData.xAxis,
      paramData.name
    );
    if (isFocus) {
      return [{ type: &#39;mark&#39; }, { type: &#39;focus&#39;, data: null }];
    } else {
      return [
        { type: &#39;focus&#39;, origin: `${paramData.name}-${paramData.xAxis}` },
      ];
    }
  }
};

const makeMarkPoints = (markData, focusInfo) =&gt; {
    // 차트 상 표시와 차트 위 컴포넌트 렌더링을 위한 데이터 가공
});
</code></pre>
<pre><code class="language-typescript">// FlowCharts.tsx

const ActualFlowEchartNew = () =&gt; {
  const [chartInstance, setChartInstance] = useState(null);
  const [markData, setMarkData] = useState&lt;MarkData&gt;({} as MarkData);
  const [focusInfo, setFocusInfo] = useAtom(ActualFlowFocusInfoAtom);
  const constraintsRef = useRef(null);
  const getInstance = useCallback((instance) =&gt; {
    if (instance) {
      setChartInstance(instance);
    }
  }, []);

  const markPoints = makeMarkPoints(markData, focusInfo);

  const date = makeDefaultDate();
  const datum = makeDefaultDaum();

  const actualFlowDefaultOption = makeActualFlowEchartOption(date, datum);


  useEffect(() =&gt; {
      // 차트 상 표시 적용 
  }, [markPoints, chartInstance]);


  useEffect(() =&gt; {
    // 이벤트 핸들러 부착
    if (!chartInstance) return;
    (chartInstance as EChartsInstance).on(
      &#39;click&#39;,
      (param: echarts.ECElementEvent) =&gt; {
        const determineFocus = (yAxis, xAxis, name) =&gt; {
          // 포커스 구별 결과를 반환
        };

        const evaluations = addEventHandlerNew(param, datum, determineFocus);

        evaluations.forEach((eachEvaluation) =&gt; {
          if (eachEvaluation.type === &#39;mark&#39;) {
            // key값 만들어 markData에 focusInfo를 편입
            // setMarkData와 focusInfo사용

            } else if (eachEvaluation.type === &#39;focus&#39;) {
            if (eachEvaluation.origin) {
              // origin 필드가 있다면 markData[origin]을 focusInfo로 set
              // markData와 setFocusInfo사용

            } else {
              // data 필드 값에 대해 focusInfo로 업데이트
              // setFocusInfo사용
            }
          }
        });
      }
    );

    return () =&gt; (chartInstance as EChartsInstance).off(&#39;click&#39;);
  }, [chartInstance, datum, focusInfo]);

  return (
    &lt;&gt;
      &lt;div style={{ height: &#39;1000px&#39;, width: &#39;100%&#39; }} ref={constraintsRef}&gt;
        &lt;ReusableEchart
          cachedGetInstance={getInstance}
          defaultOption={actualFlowDefaultOption}
        /&gt;
        {/*markData를 통해 차트 위 컴포넌트 렌더링*/}
      &lt;/div&gt;
    &lt;/&gt;
  );
};
</code></pre>
<p><strong>장점</strong>
chartHandler에 존재하는 모든 함수를 &#39;계산&#39;으로 만들었고, 이 부분이 핵심 로직의 대부분을 담당하기 때문에 유지보수에 유리해졌다.</p>
<p>기능을 추가 및 수정한다고 해서, 다른 기능들에 영향을 미치지 않는 다는 것은 명확해지기 때문.</p>
<p><strong>한계</strong>
최상위 계층에서 &#39;어떻게&#39;라는 정보를 담고 있다.
각 useEffect안에, &#39;계산&#39;을 통해 얻은 가공된 데이터를 처리하는 방법을 작성해야 했다.</p>
<p>이를 해결하려면, 더 계층을 나눠야 한다.</p>
<p>하지만 이를 위해선 markData, setMarkData, focusInfo, setFocusInfo를 모두 넘겨받는 함수로 분리해야 하기 때문에, 지저분해서 포기했다.</p>
<h2 id="정리">정리</h2>
<p>기능 관리하는데 있어서 확실피 수월해짐을 느꼈다.
이전에는 setMarkData안에 setFocusInfo가 들어가는 등 매우 복잡한 구조였다.</p>
<p>또한 계층이 분리되어있지 않아, 어디에서 문제가 발생하는지 확인하기 위해서는 모든 문맥을 읽고 하나하나 따라가며 파악해야 했다.</p>
<p>FP관점으로 계층을 분리하고, 로직을 순수함수로 분리하는 과정에서 신뢰할 수 있는 영역을 만들었기 때문에, 필요한 부분에만 신경 쓸 수 있다는 점이 좋았다.</p>
<h2 id="마치면서">마치면서...</h2>
<p>나름 FP의 주요 원칙으로 고려하며 리팩토링을 진행했으나, 솔직히 완벽하지 않다고 생각한다.</p>
<p>react의 scheduler 관련 코드를 보면서 꽤 계층화와 추상화가 잘 이루어져 있다고 생각했는데, 내 결과물에는 여러 한계를 볼 수 있었으므로.</p>
<p>또한, FP를 완벽하게 프론트엔드에 적용하기 어렵다는 점도 한 몫 한다고 본다.</p>
<br>
OOP와 마찬가지로, 어느 부분에 FP의 어떤 것을 적용할지 좀 더 고민해야 할 것 같다.

<p>또한, OOP와 어떻게 조화를 이룰지도 생각하고, 자신만의 스타일을 키워나가야 할 것이다.</p>
<h1 id="출처">출처</h1>
<p><a href="https://product.kyobobook.co.kr/detail/S000001952246">쏙쏙 들어오는 함수형 코딩</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[상태 관리와 유연한 설계]]></title>
            <link>https://velog.io/@hys-lee/%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EC%99%80-%EC%9C%A0%EC%97%B0%ED%95%9C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@hys-lee/%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EC%99%80-%EC%9C%A0%EC%97%B0%ED%95%9C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sat, 21 Dec 2024 08:45:12 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하며">시작하며</h2>
<p>이전 포스트에 이어서, 다양한 상태 관리 라이브러리들에 유연한 구조를 갖게 코드를 작성하는 방법을 고민해봤다.
<br></p>
<p>준비중인 개인 프로젝트는 여러 컴포넌트에서 사용되는 상태들 중 일부를 멀리 떨어진 다른 컴포넌트에서도 사용해야 하기에,</p>
<p>또한 유연성을 증대시키기 위해 Atomic방식의 상태관리 라이브러리를 사용하는 쪽으로 마음이 굳어졌다.
<br></p>
<p>그럼에도, 새로운 개념의 클라이언트 상태 관리 라이브러리가 2~3년마다 나오는 것 같았기에 쉽게 이들을 교체할 수 있는 구조를 고심하고 제작하고자 한다.</p>
<p>따라서, 이번 실습의 목표는 다음과 같다.</p>
<blockquote>
</blockquote>
<p>직접 다양한 상태관리 라이브러리의 변경에 유연한 커스텀 훅과 컴포넌트를 만들어보고, 테스트 코드까지 작성해보자!</p>
<h1 id="실습">실습</h1>
<h2 id="기본-환경">기본 환경</h2>
<p>목표에 집중하기 위해 최대한 간단하게 기본 환경을 구성했다.
<br></p>
<p>main.tsx, App.tsx, Comp1.tsx, Comp2.tsx, Comp3.tsx가 존재한다.</p>
<ul>
<li>main.tsx: root에 대해 App 컴포넌트를 렌더링한다.</li>
<li>App.tsx: Comp1, Comp2, Comp3을 동일한 계층의 자식으로 가진다.<pre><code class="language-typescript">function App() {
return (
  &lt;&gt;
    &lt;Comp1 /&gt;
    &lt;Comp2 /&gt;
    &lt;Comp3 /&gt;
  &lt;/&gt;
);
}</code></pre>
<ul>
<li>Comp1.tsx: authorization 상태가 true일 때만 보여지는 컴포넌트이다.</li>
<li>Comp2.tsx: actHistories 상태를 쓰는 컴포넌트이다.</li>
<li>Comp3.tsx: actHistories 상태를 읽는 컴포넌트이다.</li>
</ul>
</li>
</ul>
<p>Comp1, Comp2, Comp3에서 쓰이는 authorization과 actHistories 는 공유 상태로 설정했다.</p>
<p>또한, Comp2와 Comp3이 멀리 떨어진 컴포넌트라 가정하고, 한쪽에서 쓰기, 다른 쪽에서 읽기를 수행하도록 했다.</p>
<h3 id="공유-상태-관리-준비">공유 상태 관리 준비</h3>
<p>사용되는 상태관리 라이브러리는 다음과 같다.</p>
<ul>
<li>Redux - Reducer를 사용한 중앙 집중형 Top-Down 상태관리</li>
<li>Mobx - Proxy를 직접 이용한 구독형 상태관리</li>
<li>Jotai - Atom기반 Bottom-Up으로 사용하는 상태관리</li>
</ul>
<p>후술 하겠지만, Mobx의 특성으로 인해, Comp1,2,3을 다음과 같이 분리했다.</p>
<pre><code class="language-typescript">export const CoreComp1 = () =&gt; {
  const { authorization, toggleAuthorization } = useAuthorization();
  return &lt;&gt;
    ...
    &lt;/&gt;;

}

const Comp1 = () =&gt; {
  return (
    &lt;&gt;
      &lt;CoreComp1 /&gt;
    &lt;/&gt;
  );
};</code></pre>
<p>위 코드에서, CoreComp1은 상태를 사용해 UI를 반환하는 핵심 컴포넌트가 되고</p>
<p>Comp1은 사용되는 상태관리 라이브러리에 따라 CoreComp1을 사용하는 중간 컴포넌트가 들어올 수 있도록 했다.
<br></p>
<h3 id="공유-상태">공유 상태</h3>
<p>각 라이브러리에서 공용으로 사용하도록  Vanilla로 제작해두었다.</p>
<pre><code class="language-typescript">// src/sharingStates.ts
export interface IActHistory {
  id: number;
  name: string;
}

export interface ISharingStates {
  authorization: boolean;
  actHistories: IActHistory[];
}
const sharingStates: ISharingStates = {
  authorization: false,
  actHistories: [],
};

export default sharingStates;
</code></pre>
<h3 id="공통-커스텀-훅">공통 커스텀 훅</h3>
<p>각 상태관리 라이브러리 별 커스텀 훅을 제작하고,</p>
<p>이를 상태에 따른 공통 커스텀 훅에서 사용되도록 했다.</p>
<pre><code class="language-typescript">// src/commonCustomHooks/useActHistories.ts

const useActHistories = () =&gt; {
  return // 여기에 상태 관리 라이브러리에 맞춘 커스텀 훅이 실행됨.
};
export default useActHistories;


// src/commonCustomHooks/useAuthorization.ts
const useAuthorization = () =&gt; {
  return // 여기에 상태 관리 라이브러리에 맞춘 커스텀 훅이 실행됨.
};
export default useAuthorization;</code></pre>
<p>공통 커스텀 훅 내부에서는 상태 관리 라이브러리 관련 코드를 고려하지 않고 교체할 수 있도록, 내부 로직을 없앴다.
<br></p>
<blockquote>
<p>사실, 로직이 존재하는 각 라이브러리 별 커스텀 훅 내부에서 공유 상태를 불러오지 않고, 인자로 받아오는 방법을 사용한다면</p>
</blockquote>
<p>관심사 분리도 되고 테스트코드 작성도 편리했을 것이다.</p>
<blockquote>
<br>
그러나, 각 라이브러리별 상태 관리 방법이 다르기에 커스텀 훅 내부 코드는 달라 재사용이 어려우므로

</blockquote>
<p>교체 용이성에 초점을 두어 위와 같은 구조를 사용했다.</p>
<h2 id="redux">Redux</h2>
<h3 id="기본-준비-사항">기본 준비 사항</h3>
<p>redux는 store에 reducer를 등록하고, 보통 App을 감싸는 Provider를 통해 이를 내려보내준다.</p>
<p>useSelector와 useDispatch를 통해 상태를 사용하고 action을 dispatch한다.</p>
<p>reducer는 slice를 통해 손쉽게 정의 가능하고, 이 때 initialState를 정의한다.</p>
<p>redux toolkit을 사용하면 mutable하게 상태를 처리할 수 있다.</p>
<p>따라서 _sharingStates.ts_에서 가져와 상태를 정의했다.</p>
<pre><code class="language-typescript">// src/reduxThings/reducers/ActHistoriesReducer.ts
const initialState = sharingStates.actHistories;
const ActHistorySlice = createSlice({
  name: &#39;ActHistory&#39;,
  initialState,
  reducers: {
    add: (state, action: PayloadAction&lt;{ historyName: string }&gt;) =&gt; {
      state.push({ id: state.length, name: action.payload.historyName });
    },

    mutate: (state, action: PayloadAction&lt;{ id: number; newName: string }&gt;) =&gt; {
      const targetHistory = state.find(
        (history) =&gt; history.id === action.payload.id
      );
      if (targetHistory) targetHistory.name = action.payload.newName;
    },
  },
});
export const { add, mutate } = ActHistorySlice.actions;
export default ActHistorySlice.reducer;


// src/reduxThings/reducers/AuthorizationReducer.ts
const AuthorizatoinSlice = createSlice({
  name: &#39;Authorization&#39;,
  initialState: sharingStates.authorization,
  reducers: {
    toggle: (state) =&gt; {
      return !state;
    },
  },
});
export const { toggle } = AuthorizatoinSlice.actions;
export default AuthorizatoinSlice.reducer;

</code></pre>
<p>테스트 코드에서 재사용하기 위해 Provider를 따로 제작해주었고,</p>
<p>useSelector에서 상태 타입을 부여하기 위해 다음과 같이 store를 정의했다.</p>
<pre><code class="language-typescript">// src/reduxThings/ReduxProvider.tsx
const ReduxProvider = ({ children }: { children: ReactNode }) =&gt; {
  return &lt;Provider store={store}&gt;{children}&lt;/Provider&gt;;
};
export default ReduxProvider;


// src/reduxThings/reduxStore.ts
const store = configureStore({
  reducer: {
    ActHistory: ActHistoryReducer,
    Authorization: AuthorizationReducer,
  },
});
export default store;

export type RootState = ReturnType&lt;typeof store.getState&gt;;
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch: UseDispatch&lt;AppDispatch&gt; = useDispatch;
export const useAppSelector: UseSelector&lt;RootState&gt; = useSelector;

</code></pre>
<h3 id="redux-기반-커스텀-훅">Redux 기반 커스텀 훅</h3>
<p>실제 로직은 전부 reducer내부에 존재하기 때문에,</p>
<p>각 커스텀 훅 내부 코드는 간단했다.</p>
<pre><code class="language-typescript">// src/reduxThings/useReduxActHistories.ts
import { add, mutate } from &#39;./reducers/ActHistoriesReducer&#39;;

const useReduxActHistories = () =&gt; {
  const actHistories = useSelector((state: RootState) =&gt; state.ActHistory);
  const mydispatch = useAppDispatch();

  const addActHistories = (historyName: string = &#39;기본&#39;) =&gt;
    mydispatch(add({ historyName }));

  const mutateActHistoryName = (id: number, newName: string) =&gt; {
    mydispatch(mutate({ id, newName }));
  };
  return { actHistories, addActHistories, mutateActHistoryName };
};

export default useReduxActHistories;</code></pre>
<pre><code class="language-typescript">// src/reduxThings/useReduxAuthorization.ts
import { toggle } from &#39;./reducers/AuthorizationReducer&#39;;

const useReduxAuthorization = () =&gt; {
  const authorization = useAppSelector((state) =&gt; state.Authorization);
  const dispatch = useAppDispatch();

  const toggleAuthorization = () =&gt; {
    dispatch(toggle());
  };

  return { authorization, toggleAuthorization };
};
export default useReduxAuthorization;
</code></pre>
<h3 id="커스텀-훅-테스트-코드">커스텀 훅 테스트 코드</h3>
<p>testing-library/react와 testing-library/jest-dom, jest를 사용했다.</p>
<p>renderHook을 사용해 리덕스 베이스 훅의 기능만 테스트했고, ReduxProvider를 wrapper로 사용했다.</p>
<pre><code class="language-typescript">// src/reduxThings/useReduxActHistories.test.ts
describe(&#39;useReduxActHistories 테스트&#39;, () =&gt; {
  beforeEach(() =&gt; {
    sharingStates.actHistories = [];
  });

  test(&#39;addActHistories로 actHistories는 1개가 되어야 함 &#39;, () =&gt; {
    const { result } = renderHook(() =&gt; {
        return useReduxActHistories();
      },{ wrapper: ReduxProvider }
    );

    act(() =&gt; {
      result.current.addActHistories(&#39;하나&#39;);
    });
    expect(result.current.actHistories.length).toBe(1);
    expect(
      result.current.actHistories[result.current.actHistories.length - 1]
    ).toEqual(expect.objectContaining({ id: 0, name: &#39;하나&#39; }));
  });
});</code></pre>
<p>useReduxAuthorization에 대한 테스트도 유사하니, 생략하겠다.</p>
<h3 id="평가">평가</h3>
<p>커스텀 훅이나 테스트 코드 작성이 간편해서 좋았으나,</p>
<p>보일러 플레이트가 좀 많았다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/72be250e-fb9a-43d9-bb5b-82a8cd2e7b31/image.png" alt=""></p>
<p>파일 개수 만이 아니라, 각 파일 내 코드도 꽤 많았다.
<br></p>
<p>또한, 최상단에 Provider를 놓아야 하는 점이 불편했다.</p>
<pre><code class="language-typescript">// src main.tsx
...
  &lt;ReduxProvider&gt;
    &lt;App /&gt;
  &lt;/ReduxProvider&gt;
...</code></pre>
<p>물론, zustand를 사용하면 Provider를 안써도 되긴 하나, 태생적인 한계가 있었다.
<br></p>
<p>Top-Down방식이기에, 여러 reducer에 사용되는 states들을 조합하기 어려웠다.</p>
<h2 id="mobx">Mobx</h2>
<h3 id="기본-준비-사항-1">기본 준비 사항</h3>
<p>mobx는 observable을 observer나 useObserver로 감싼 컴포넌트 내부에서 사용하는 방식이다.</p>
<p>따라서, 커스텀 훅이 쓰이는 컴포넌트를 wrapping하는 ObservingComp를 제작해야했다.</p>
<p>또한, 다른 상태관리 라이브러리들과 일관된 사용성을 만들기 위해</p>
<p>컴포넌트 인자로 observable을 전달받는 방식이 아닌, 
커스텀 훅 내부에서 observable을 사용하는 방식을 선택했다.
<br>
이를 위해선, React의 Context를 사용해야 했으므로, Provider와 함께 다음과 같이 제작했다.</p>
<pre><code class="language-typescript">// src/mobxThings/ObservingContextProvider.tsx
import { useObserver } from &#39;mobx-react-lite&#39;;

const ObservableSharingStatesContext = createContext(observableSharingStates);
const ObservableActHistoriesContext = createContext(observableActHistories);

export { ObservableSharingStatesContext, ObservableActHistoriesContext };

function ObservingContextProvider&lt;T&gt;({context, value, children}: {
  context: Context&lt;T&gt;;
  value: T;
  children: () =&gt; ReactNode;
}) {
  return (
    &lt;context.Provider value={value}&gt;{useObserver(children)}&lt;/context.Provider&gt;
  );
}
export default ObservingContextProvider;</code></pre>
<pre><code class="language-typescript">// src/mobxThings/ObservingComp1.tsx
const ObservingComp1 = () =&gt; {
  // context 사용을 최대한 쪼갰음. 불필요한 렌더링을 최대한 줄이기 위해서.
  return (
    &lt;ObservingContextProvider
      context={ObservableSharingStatesContext}
      value={observableSharingStates}
    &gt;
      {CoreComp1}
    &lt;/ObservingContextProvider&gt;
  );
};

export default ObservingComp1;

// src/mobxThings/ObservingComp2.tsx
const ObservingComp2 = () =&gt; {
  return (
    &lt;ObservingContextProvider
      context={ObservableActHistoriesContext}
      value={observableActHistories}
    &gt;
      {CoreComp2}
    &lt;/ObservingContextProvider&gt;
  );
};

export default ObservingComp2;

// ObservingComp3은 ObservingComp2와 같으므로, 생략한다.</code></pre>
<p><del>지금보니, 그냥 Observable이름에 따라 Provider Wrapper 이름으로  지을걸 그랬다.</del>
<br></p>
<p>이들을 각 Comp에 적용하면 다음과 같은 코드가 된다.</p>
<pre><code class="language-typescript">const Comp1 = () =&gt; {
    return (
    &lt;&gt;
      &lt;ObservingComp1 /&gt;
    &lt;/&gt;
  );
};</code></pre>
<br>

<p>구독 가능한 상태에 대해서는 변화에 감지당하는 기준으로 쪼개 observable로 정의했다.</p>
<pre><code class="language-typescript">
export const observableSharingStates = observable(sharingStates);

observableSharingStates.actHistories = observable(
  observableSharingStates.actHistories
);
export const observableActHistories = observableSharingStates.actHistories;
</code></pre>
<h3 id="mobx-기반-커스텀-훅">Mobx 기반 커스텀 훅</h3>
<p>Redux와는 다르게, 커스텀 훅 내부에 로직이 정의되므로,</p>
<p>커스텀 훅이 반환하는 메서드를 커스텀 훅 함수 외부에 따로 정의했다.</p>
<pre><code class="language-typescript">// src/mobxThings/useMobxActHistory.tsx
const _addActHistories = (actHistories: IActHistory[]) =&gt; {
  return function (newName: string = &#39;기본&#39;) {
    const newHistory: IActHistory = {
      id: actHistories.length,
      name: newName,
    };
    actHistories.push(newHistory);
  };
};

const _mutateActHistoryName = (actHistories: IActHistory[]) =&gt;
  function (targetId: number, newName: string) {
    const targetHistory = actHistories.find(
      (history: IActHistory) =&gt; history.id === targetId
    );
    if (targetHistory) targetHistory.name = newName;
  };


const useMobxActHistory = () =&gt; {
  const actHistories = useContext(ObservableActHistoriesContext);
  const addActHistories = _addActHistories(actHistories);
  const mutateActHistoryName = _mutateActHistoryName(actHistories);

  return { actHistories, addActHistories, mutateActHistoryName };
};

export default useMobxActHistory;


// src/mobxThings/useMobxAuthorization.tsx
const _toggleAuthorization = (observableSharingStates: ISharingStates) =&gt; {
  observableSharingStates.authorization =
    !observableSharingStates.authorization;
};

const useMobxAuthorization = () =&gt; {
  const observableSharingStates: ISharingStates = useContext(
    ObservableSharingStatesContext
  );
  const toggleAuthorization = _toggleAuthorization(observableSharingStates);

  return {
    authorization: observableSharingStates.authorization,
    toggleAuthorization,
  };
};

export default useMobxAuthorization;</code></pre>
<h3 id="커스텀-훅-테스트-코드-1">커스텀 훅 테스트 코드</h3>
<p>사용한 라이브러리는 이전과 동일하다.
<br></p>
<p>기존에 존재하는 Context를 사용하지만, 최대한 테스트 코드는 독립적으로 작동시키고 싶어 </p>
<p>다음과 같이 mocking용 observable을 제작했다.
<br></p>
<p>이상했던 점은, act내부에서 훅을 사용했는데</p>
<p>rerender 함수를 실행해야 result가 변경되었다는 점이다.</p>
<pre><code class="language-typescript">// src/mobxThings/useMobxAuthorization.test.tsx
const initValue = false;
const initStates = observable({ actHistories: [], authorization: initValue });
const MockProvider = ({ children }: { children: ReactNode }) =&gt; (
  &lt;ObservableSharingStatesContext.Provider value={initStates}&gt;
    {children}
  &lt;/ObservableSharingStatesContext.Provider&gt;
);

describe(&#39;useAuthorization 테스트&#39;, () =&gt; {
  test(&#39;toggleAuthorization이 되나?&#39;, () =&gt; {
    const { result, rerender } = renderHook(() =&gt; useMobxAuthorization(), {
      wrapper: MockProvider,
    });
    act(() =&gt; {
      result.current.toggleAuthorization();
    });
    rerender();
    expect(result.current.authorization).toBe(!initValue);
  });
});</code></pre>
<p>useMobxActHistory의 테스트 코드도 위와 비슷하니 생략한다.</p>
<h3 id="평가-1">평가</h3>
<p>기본적으로 mutable하게 사용하는 점이 괜찮았으나, observable을 사용하기 위해 useObserver를 붙여야 하는 점은 불편했다.</p>
<p>이에 의해 깔끔한 코드를 만들기 위해 ObservingComp들을 제작해야 했다.
<br></p>
<p>또한, Context를 통해 observable을 전달한다는 점도 불편했다.</p>
<p>observable이 변하면 Context하위의 컴포넌트들도 전부 리렌더링 될 것이다.</p>
<p>Context의 단점이 명확한 상황에서 이에 의존하는 방식이 좋지 않았다.
<br></p>
<p>이에 각 파일 내부 내용이 많지는 않았지만, 여러 파일을 만들어야 했다.
<img src="https://velog.velcdn.com/images/hys-lee/post/dc479b17-78bc-4ae8-bfd7-e60f7a7092f1/image.png" alt=""></p>
<h2 id="jotai">Jotai</h2>
<h3 id="기본-준비-사항-2">기본 준비 사항</h3>
<p>코드 상으로는 보일러 플레이트가 제일 작다.</p>
<p>적절한 atom만 생성하면 되기 때문이다.</p>
<p>여러 atom을 합쳐 derived atom을 제작하면, atom의 변화가 derived atom에 반영되는 것도 좋았다.</p>
<p>이들을 다음과 같이 정의했따.</p>
<pre><code class="language-typescript">// src/jotaiThings/atoms.ts
const authorizationAtom = atom(false);

const actHistoriesAtom = atom&lt;IActHistory[]&gt;([]);

const sharingStatesAtom = atom&lt;ISharingStates&gt;((get) =&gt; {
  const authorization = get(authorizationAtom);
  const actHistories = get(actHistoriesAtom);
  return { authorization, actHistories };
});

export { authorizationAtom, actHistoriesAtom, sharingStatesAtom };</code></pre>
<p>그러나, 후술하겠지만, 렌더링 최적화를 위해 write-only atom을 제작하면서 살짝 복잡해진다.</p>
<p>코드 상 derived atom인 shringStatesAtom은 read-only atom이다.</p>
<h3 id="jotai-기반-커스텀-훅--기본">Jotai 기반 커스텀 훅 : 기본</h3>
<p>Atomic방식은 useState를 사용하는 것과 매우 유사했기 때문에, 기본 사용법은 러닝커브가 거의 없었다.
<br></p>
<p>또한 Jotai는 RCC에서는 Provider가 필요 없기에 코드가 깔끔했다.</p>
<pre><code class="language-typescript">// src/jotaiThings/useJotaiAuthorization.ts
const useJotaiAuthorization = () =&gt; {
  const [authorization, setAuthorization] = useAtom(authorizationAtom);

  const toggleAuthorization = () =&gt; {
    setAuthorization(!authorization);
  };

  return { authorization, toggleAuthorization };
};
export default useJotaiAuthorization;
</code></pre>
<p>역시 커스텀 훅 내부에 로직이 존재하기에, 커스텀 훅 외부로 분리하는게 맞겠지만,</p>
<p>코드가 간단하여 이대로 두었다.</p>
<h3 id="jotai-기반-커스텀-훅--렌더링-최적화">Jotai 기반 커스텀 훅 : 렌더링 최적화</h3>
<blockquote>
<p>useSetAtom을 사용하면, 굳이 write-only atom을 제작하지 않고도 불필요한 리렌더링을 방지할 수 있다고 한다..!</p>
</blockquote>
<p>사용방법도 생각보다 간단하니, 밑의 방법보다 useSetAtom을 애용하자!</p>
<pre><code class="language-typescript">const efficientSetFunc = useSetAtom(someAtom);</code></pre>
<blockquote>
<p>만약 write-only atom을 사용해야겠다면 아래와 같이 처리할수도 있을 것이다..</p>
</blockquote>
<p>튜토리얼에서, write-only atom을 사용하면 필요한 곳에서만 렌더링을 유발할 수 있다하여 시도해봤다.</p>
<p>커스텀 훅 내부에 임시로 최적화를 위한 atom들을 다음과 같이 정의했다.</p>
<pre><code class="language-typescript">// src/jotaiThings/useAuthorization.ts
const historyValueAtom = atom((get) =&gt; get(actHistoriesAtom));

type updateArg = IActHistory[] | ((past: IActHistory[]) =&gt; IActHistory[]);
const setAtom = atom(null, (get, set, update: updateArg) =&gt; {
  const realUpdate =
    typeof update === &#39;function&#39; ? update(get(actHistoriesAtom)) : update;
  set(actHistoriesAtom, realUpdate);
});</code></pre>
<p>벌써부터 코드가 좀 복잡한데,</p>
<p>historyBalueAtom은 read-only atom이고, setAtom은 write-only atom이다.
<br></p>
<p>atom을 정의할 때, read와 write함수를 넎어 정의할 수 있는데, 첫 인자는 read함수, 두 번째 인자는 write함수이다.
<br></p>
<p>get을 통해 atom의 value를 가져오고, 
set으로 atom의 value를 set하며, 
update를 통해 useAtom으로부터의 setState함수에 들어오는 인자에 접근할 수 있다.
<br></p>
<p>기본 atom 사용으로 얻는 setState처럼, 값과 함수를 모두 받을 수 있도록 정의하기 위해 위와 같이 write함수를 정의했다.
<br></p>
<p>이를 적용한 커스텀 훅 관련 코드는 다음과 같다.</p>
<pre><code class="language-typescript">// src/jotaiThings/useJotaiActHistories.ts

// * 타입스크립트를 사용했기에, 가시성 위해 정의 순서를 아래와 같이 할 수 있었음.
const useJotaiActHistories = () =&gt; {
  const getActHistories = useActHistoriesValue;
  const addActHistories = useAddActHistory();
  const mutateActHistoryName = useMutateActHistoryName();
  return {
    getActHistories,
    addActHistories,
    mutateActHistoryName,
  };
};

export default useJotaiActHistories;


const useActHistoriesValue = () =&gt; {
  const [actHistories] = useAtom(historyValueAtom);

  return actHistories;
};

const useAddActHistory = () =&gt; {
  const [, setatom] = useAtom(setAtom);

  return (newName: string = &#39;기본&#39;) =&gt; {
    setatom((past) =&gt; [...past, { id: past.length, name: newName }]);
  };
};

const useMutateActHistoryName = () =&gt; {
  const [, setatom] = useAtom(setAtom);

  return (id: number, newName: string) =&gt; {
    setatom((prev) =&gt; {
      const newHistories = [...prev];

      const target = newHistories.find((history) =&gt; history.id === id);
      if (target) target.name = newName;

      return newHistories;
    });
  };
};</code></pre>
<p>커스텀 훅에서 state와 이에 대한 기능 함수들을 같이 제공하려 했기에 복잡해졌다.</p>
<p>write-only atom을 제외한 다른 atom들은 useAtom에 사용하면, 변화가 있을 때 마다 리렌더링을 유발한다.
<br></p>
<p>따라서 커스텀 훅 내부에서 
<code>const [actHistories] = useAtom(historyValueAtom);</code> 을 정의하고,
값인 actHistories를 반환할 수 없었다.
<br></p>
<p>따라서 렌더링 최적화를 위해서는 커스텀 훅에서 state자체를 반환하는게 아니라,
get함수의 형태로 반환해야 했다.
<code>const getActHistories = useActHistoriesValue;</code>
<br></p>
<p>또한, 이렇게 get함수의 내용을 정의한 형태를 지키기 위해 
다른 기능 함수들의 정의가 고차함수 형태를 띄게 되었다.
<br></p>
<p>이렇게 write-only atom을 사용한다면, get함수를 사용하지 않는 곳에서는 리렌더링이 일어나지 않게 된다.</p>
<h3 id="커스텀-훅-테스트-코드-2">커스텀 훅 테스트 코드</h3>
<p>독립적인 테스트를 위해 Provider와 Store를 정의해 wrapper로 사용할 수 있었다.</p>
<p>코드는 다음과 같다.</p>
<pre><code class="language-typescript">// src/jotaiThings/useJotaiAuthorization.test.ts
describe(&#39;useJotaiAuthorization 테스트: &#39;, () =&gt; {
  const initValue = false;
  beforeEach(() =&gt; {
    sharingStates.authorization = initValue;
  });

  const mockStore = createStore();
  mockStore.sub(authorizationAtom, () =&gt; {});

  const MockJotaiProvider = ({ children }: { children: ReactNode }) =&gt; (
    &lt;Provider store={mockStore}&gt;{children}&lt;/Provider&gt;
  );

  test(&#39;토글로 true되어야 함.&#39;, () =&gt; {
    const { result } = renderHook(() =&gt; useJotaiAuthorization(), {
      wrapper: MockJotaiProvider,
    });
    act(() =&gt; {
      result.current.toggleAuthorization();
    });

    expect(result.current.authorization).toBe(!initValue);
  });
});</code></pre>
<p>useJotaiActHistories테스트에 대해서도 위와 비슷하게 작성했다.</p>
<p>(리렌더링 여부를 체크할 순 있었지만 생략했다.)</p>
<h3 id="평가-2">평가</h3>
<p>다른 상태관리 라이브러리들보다 간단하게 사용 가능했고,
useState의 사용감을 그대로 느낄 수 있었으며,
bottom-up방식으로 derived Atom을 만들수도 있어서 매우 좋았다.
<br></p>
<p>read-only, write-only방식으로 derived Atom을 만들어 atom의 변화에 반응할수도,
util에서 selectAtom을 사용해 atom의 일부 필드의 변화에만 반응시킬 수도 있어 유연함에서 강점을 보였다.
<br></p>
<p><del>다만, 렌더링 최적화를 위해 기존의 atom과 이를 기반으로하는 write-only atom을 만들어야 한다는 점에서 관리의 어려움을 증대시켰고,</del></p>
<blockquote>
<p>위 내용을 수정하자면, useSetAtom과 useAtomValue를 통해 간단하게 분리해서 처리할 수 있다.</p>
</blockquote>
<p>커스텀 훅에서 readable atom을 같이 전달 하기 위해서는 get함수로 반환해야 한다는 점이 불편한 사용감을 전달했다.</p>
<br>

<p>개인적으로는, atom의 합성이 강력하여 상태관리 라이브러리를 담는 커스텀 훅의 기능을 넘볼 수 있어 책임 분리에 어려움도 있었다.
<br></p>
<p>튜토리얼에서는 createAtoms 패턴을 소개하는데, 팩토리 패턴의 변형 느낌이다.</p>
<pre><code class="language-typescript">const createAtoms = (initValue)={
    const baseAtom = atom(initValue);
    const valueAtom = atom((get)=&gt;get(baseAtom));
    const setAtom = atom(null, (get, set, update)=&gt;{set(update)})

    return [valueAtom, setAtom];
};</code></pre>
<br>

<p>이를 변형하여, read-only atom없이 최적화를 위한 write-only atom만 필요하다면 다음과 같이 제작하는 좋아 보인다.</p>
<pre><code class="language-typescript">const createEfficientAtoms=&lt;T&gt;(initValue)=&gt;{
    const baseAtom = atom&lt;T&gt;(initValue);

      type updateArg = T | ((past: T) =&gt; T);
    const setAtom = atom(null, 
        (get, set, update: updateArg)=&gt; {
              const realUpdate = typeof update === &#39;function&#39; ?
                update(get(actHistoriesAtom)) : update;
              set(actHistoriesAtom, realUpdate);
        }
    );
      return [baseAtom, setAtom];
}</code></pre>
<p>baseAtom을 반환하는 이유는, atom합성에 유연하게 사용할 수 있기 위함이다.
<br></p>
<h2 id="끝내며">끝내며</h2>
<p>세가지 상태관리 라이브러리들을 커스텀 훅 안에 적용할 수 있었다.
<br></p>
<p>다만, 각 상태관리 라이브러리마다 사용방법이 천차만별이라 
모든 상황을 대비하려면 최대한 컴포넌트를 분리하는 것이 중요해보였다.
<br></p>
<p>최적화처럼 특정 상태관리 라이브러리의 활용을 극대화 하기 위해서는 
get함수를 사용해야 하는 등 불편한 DX를 유발하기도 했다.
<br></p>
<p>그래도 일반적으로는 커스텀 훅과 적절한 추상화를 통해
유연하게 이들 간 교체 가능함을 확인했고, 
OOP를 프론트엔드에 적용하는 경험을 얻었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OOP와 프론트엔드]]></title>
            <link>https://velog.io/@hys-lee/OOP%EC%99%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C</link>
            <guid>https://velog.io/@hys-lee/OOP%EC%99%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C</guid>
            <pubDate>Sat, 21 Dec 2024 06:08:29 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하며">시작하며</h2>
<p>이전 OOP 스터디 이후, 개인 프로젝트를 준비했다.</p>
<p>두 가지 난관을 해결하는 것을 중점으로 뒀는데, 이들은 다음과 같다.</p>
<blockquote>
<ol>
<li>복잡한 UI 공유 상태 관리로 인한 로직 분리</li>
</ol>
</blockquote>
<ol start="2">
<li>각종 라이브러리 및 프레임워크 등의 툴에 대한 유연성</li>
</ol>
<p>이들의 해결 방법은 OOP로 보였기에,</p>
<p>이를 프론트엔드에 어떻게 적용할 수 있을지 고민해보았다.</p>
<h1 id="고찰">고찰</h1>
<h2 id="프론트에서-oop가-잘-언급되지-않는-이유">프론트에서 OOP가 잘 언급되지 않는 이유</h2>
<p>OOP의 제일 핵심인 객체, 책임, 소통 이야기부터 해보자.</p>
<p>기본적인 전제 사항으로 객체들은 각자의 책임을 갖고, 상호 소통한다.</p>
<p>일반적으로 소통의 형태는 다음과 같다.</p>
<pre><code class="language-javascript">// SomeClass 내부
const response = insanceOfOtherClass.doThis(ingredient);</code></pre>
<p>소통의 대상이 굳이 class일 필요는 없기에, 형태도 위와 같지 않아도 되지만</p>
<p>프론트엔드에서는 단방향 흐름이 일반적이라는 것이 차이점이다.
<br></p>
<h3 id="컴포넌트의-트리-구조">컴포넌트의 트리 구조</h3>
<p>결국 컴포넌트가 UI의 조각이기 때문에, html의 구조를 따르기 때문에</p>
<p>트리구조를 이룬다는 점이 OOP적용에 어려움을 만들 것이다.
<br></p>
<p>OOP를 적용함에 있어 생성자 주입에 대해 생각해보자.</p>
<p>컴포넌트에 DI를 적용해 상태를 전달한다면 prop drilling이 발생할 것이다.</p>
<p>컴포넌트에 컴포넌트를 전달하는 방법이라면 가시성이 떨어지게 된다.</p>
<h3 id="이벤트에-대한-동작의-복잡성">이벤트에 대한 동작의 복잡성</h3>
<p>객체들의 책임을 분리하고 동작을 설계하는 관점에서 보면 차이가 두드러진다.</p>
<p>이전의 &quot;숫자야구 게임&quot;을 생각해보자.
<br></p>
<p>이 간단한 서비스에도, 하나의 입력에 대한 결과를 도출하려면 협력이 필요하다.</p>
<p>Guard가 validation을, Referee가 Judge를, GameMaster는 게임 진행을 책임진다.</p>
<p>각 객체들이 동적으로 동작하며 협력에 참여하는 것이다.
<br></p>
<p>반면, 일반적인 프론트엔드 서비스에서 이러한 경우는 거의 없다.</p>
<p>대부분은, 주어진 입력 혹은 서버로부터의 데이터와 현재의 상태를 사용한다.</p>
<p>하나의 이벤트를 처리하는 동안, 상태를 동적으로 재계산 하는 일은 없을 것이다.
<br></p>
<p>최대한 복잡한 프론트엔드 협력 동작을 생각해본다면, 애니메이션일 것이다.</p>
<p>그 것도 Interactive Animation말이다.</p>
<p>그러나 이러한 복잡한 작업은 WebGL등을 활용할 것이므로, </p>
<p>컴포넌트들로 복잡한 동적 상호작용이 일어날 상황은 쉽게 만나지 못할 것이다.</p>
<h3 id="공유-상태-관리">공유 상태 관리</h3>
<p>컴포넌트 단위로 작업하기 시작하면서,</p>
<p>문제가 되는 지점은 멀리 떨어진 컴포넌트간 상태를 공유하는 것이었다.
<br></p>
<p>이쯤와서 생각이 드는 점은, 아예 관심사가 다르다는 것이다.</p>
<p>컴포넌트와 상태 관리를 통해 프론트엔드에서는 UI가 상태 변화에 반응하여 &quot;각각&quot; 동작하면 되므로, 상태 관리의 중요성이 더 오르는 것이다.</p>
<br>

<br>

<h2 id="프론트에서의-협력과-상태-관리">프론트에서의 협력과 상태 관리</h2>
<blockquote>
<p>이렇게까지 각각 동작한다면, 어쩌면 협력이 없는 것은 아닐까?</p>
</blockquote>
<p>그러나 협력의 정의를 넓힌다면, 컴포넌트들의 동작에도 협력은 존재하는 것 같다.</p>
<p>일반적으로 생각하는 직접적인 소통을 통한 협력이 아닌,</p>
<p>공유 상태를 통한 간접적인 소통을 통해서.
<br></p>
<p>마치 대자보에 각 객체의 책임에 따라 수행한 동작의 결과를 기록하고, </p>
<p>이들을 각자 알아서 사용하는 방식이랄까.
<br></p>
<p>작업을 추상화하고 분업한다는 점에서 협력이라 말할 수 있을 것이다.</p>
<h3 id="커스텀-훅">커스텀 훅</h3>
<p>협력이 가능하다면 책임도 부여할 수 있고 설계도 가능할 것이다.</p>
<p>그렇다면, 책임과 재사용성이 적용될 인터페이스와 클래스의 관계를</p>
<p>프론트엔드의 어디에서 찾을 수 있을까?
<br></p>
<p>나는 컴포넌트가 구조체며, 커스텀 훅이 인터페이스의 역할을 수행할 수 있다고 생각했다.</p>
<p>공유 상태를 메시지로 본다면, 컴포넌트에게 기능을 제공하며 메시지를 다룰 수 있는 것은 커스텀 훅일 것이다.
<br></p>
<p>커스텀 훅의 기능을 지킨다면, 어떤 컴포넌트에든 연결할 수 있으며</p>
<p>컴포넌트가 어떤 UI를 가질지는 해당 컴포넌트에서 정할 수 있다.
<br></p>
<p>커스텀 훅은 재사용성을, 컴포넌트는 자율성을 갖게 된다.</p>
<h3 id="상태-관리와-커스텀-훅">상태 관리와 커스텀 훅</h3>
<p>프론트엔드의 문제점인 공유 상태 관리 동작을 커스텀 훅 내부에 넣어도 될까?</p>
<p>재사용성을 중시하며 커스텀 훅을 제작한다면, 내부에 추상적이지 않은 동작을 넣는 것은 좋지 못하다.</p>
<p>그러나, 컴포넌트에서 사용되는 상태가 복잡한 경우, 컴포넌트 본문에 이들을 사용한 동작들을 꺼내놓는다면 가시성을 낮출 것이다.</p>
<p>뿐만 아니라, 이러한 동작들이 재사용된다면?
<br></p>
<p>이에 대한 고민을 함께 해준 영상이 있었다.</p>
<p>!youtube[ebKRUxN5otQ]</p>
<blockquote>
<p>커스텀 훅은 재사용성 면에서도 의미가 있지만, </p>
</blockquote>
<p>캡슐화, 추상화에서도 중요한 위치를 갖는다.</p>
<p>정리해보자면 다음과 같을 것이다. 
<br></p>
<ol>
<li><p>공유 상태 및 다양한 상태를 관리하는 경우, 여러 훅들이 쓰이는 경우
커스텀 훅을 제작한다. </p>
<p>이는 추상화 수준을 맞추는 것을 중점으로 두게 될 것이다.</p>
</li>
<li><p>재사용 될 추상 레벨의 복잡한 동작을 커스텀 훅으로 추출한다.</p>
<p>이는 재사용성에 중점을 둔 것이다.</p>
</li>
</ol>
<p>이 2가지를 기준으로 커스텀 훅을 제작하면 될 것 같다.</p>
<h3 id="의의">의의</h3>
<p>커스텀 훅에서 공유 상태 관리를 한다면 OOP의 조건들은 얼추 갖추게 되므로</p>
<p>설계가 가능하다는 점이 중요한 것 같다.
<br></p>
<p>설계에서 중요한 책임을 갖는 대상은 논쟁적일 수 있으나,</p>
<p>개인적으로는 여러 컴포넌트 간의 UI로직에 참여하는 대상은 책임을 갖는 것 같다.</p>
<p>공유 상태를 다루는 컴포넌트는 책임을 가진다는 것을 부정할 수 없을 것이다.
<br></p>
<p>컴포넌트가 독립적으로 간단하게 동작하는 경우에는, 커스텀 훅을 적용하기 애매하다.</p>
<p>책임을 부여하는 방법은 커스텀 훅의 사용이 될 것인데, </p>
<p>그렇지 않고 컴포넌트에 책임을 부여한다면 렌더링 최적화나 추상화 정도에 따라 컴포넌트를 분리할 때 책임소지가 불분명해질 것이다.
<br></p>
<p>따라서, &quot;단순히 UI를 구성하는 컴포넌트&quot;와 &quot;UI로직에 묶여있는 컴포넌트&quot;를 구분하는 것이 좋아보인다.</p>
<h1 id="끝내며">끝내며</h1>
<p>UI로직에 묶인 컴포넌트에는 커스텀 훅을 통해 책임을 부여하는 기준을 세웠다.</p>
<p>이제 다음 문제는 이러한 컴포넌트를 어떻게 구분짓고 관리할 것인가인데,</p>
<p>이에 대한 방안으로는 <strong>Atomic 디자인 시스템</strong>이 될 것 같다.
<br></p>
<p>원자에서 시작해서 bottom-up방식으로 합성해가는 구조인데,</p>
<p>각 Atom &gt; Molecule &gt; Organism &gt; Template &gt; Page로 나눠져 있다.
<br></p>
<p>이 때, </p>
<p>Molecule는 SRP(Single Responsibility Principle)를 갖기에, 재사용성이 두드러진다.</p>
<p>Organism은 구체적인 컨텍스트를 가지기 때문에, Molecule보다 재사용성이 떨어지게 된다.
<br></p>
<blockquote>
<p>즉, <strong>Molecule부터 커스텀 훅</strong>을 적용할 수 있을 것이고,</p>
</blockquote>
<p><strong>Molecule</strong>는 독립적인 동작을 수행하는 &quot;UI를 구성하는 컴포넌트&quot;가,
<strong>Organism</strong>은 공유 상태를 사용하는 &quot;UI로직에 묶인 컴포넌트&quot;가 될 것 같다.</p>
<br>

<p>프론트엔드 작업을 하며, 추상적인 면에서 이렇게 고민을 했던 경험은 처음이어서 뜻 깊었다.</p>
<p>열심히 고민했던 OOP의 관점을 프론트엔드에도 적용할 수 있어보여 기쁘다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OOP와 책임주도설계]]></title>
            <link>https://velog.io/@hys-lee/OOP%EC%99%80-%EC%B1%85%EC%9E%84%EC%A3%BC%EB%8F%84%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@hys-lee/OOP%EC%99%80-%EC%B1%85%EC%9E%84%EC%A3%BC%EB%8F%84%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sat, 21 Dec 2024 04:04:14 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하며">시작하며</h2>
<p>친구 &#39;정&#39;모씨와 9월부터 10월까지 OOP 스터디를 진행했다.</p>
<p>누군가는 취준할 때 SOLID원칙을 외웠었다 그러고,</p>
<p>막연하게 중요한 개념인 것 같으면서 선뜻 손이 안 갔던 개념이라 시작을 망설였다.
<br></p>
<p>정모씨의 &#39;비즈니스-UI 로직이 컴포넌트 안에서 뭉쳐있던 경험을 해보지 않았냐&#39;, </p>
<p>&#39;추상적인 것일수록 한번쯤은 접해봐야 한다&#39;는 말에 넘어가버렸다.
<br></p>
<p>시간이 지난 지금와서 돌이켜보자면,</p>
<p>좋은 코드에 대해 고민할 수 있게 해준 선택이었다.
<br></p>
<p>OOP 공부는 &#39;객체지향의 사실과 오해&#39; 라는 책으로 진행했다.</p>
<p>여기에는 그 내용보다는, 이를 직접 적용해본 경험을 서술하려 한다.</p>
<h1 id="실습">실습</h1>
<h3 id="상황">상황</h3>
<p>숫자 아구 게임을 제작하기로 했다.
<br></p>
<p>생각해보면, 우테코 프리코스에서도 TDD에 대한 영상이 올라오는 등</p>
<p>OOP에 대한 고민이 있었어야 했던 시간이었음을 깨닫게 되었다.
<br></p>
<p>아주 대표적인 프리코스 문제로 이전 코드와 비교해보기도 좋아 선정했다.</p>
<h3 id="책임-주도-설계">책임 주도 설계</h3>
<p>책에 나온대로 유스케이스를 먼저 작성했다.
이후 협력을 생각하고, 도메인 모델과 책임을 다음과 같이 정했다.</p>
<p>각 시도별 자잘한 이름은 다를 수 있으나 큰 틀은 다음과 같다.</p>
<ul>
<li>경호원: validity를 체크한다.</li>
<li>출제자: 게임의 정답을 출제한다.</li>
<li>심판: 사용자의 입력을 정답과 비교한다.</li>
</ul>
<h2 id="첫-번째-시도">첫 번째 시도</h2>
<h3 id="설계">설계</h3>
<p>이 때, 위에 추가해서 안내자를 객체로 설계했다.</p>
<p>인터페이스 제작 자체는 크게 막히는게 없었다.</p>
<p>당시의 주석은 다음과 같다.</p>
<pre><code class="language-typescript">
// interface QuestionerInterface&lt;T&gt; {
//   // 자료구조를 마음대로 정해도 되는가?
//   getAnswer: () =&gt; T[];
// }

// interface AnnouncerInterface&lt;S, T&gt; {
//   getErrorComment: (result: S) =&gt; string;
//   getGameComment: (result: T) =&gt; string;
//   getInitComment: () =&gt; string;
// }

// interface GuardInterface&lt;U&gt; {
//   getValidity: () =&gt; U;
// }
// interface HeadGuardInterface extends GuardInterface&lt;ValidityResultType&gt; {
//   getValidity: () =&gt; ValidityResultType;
// }

// interface ExchangerInterface&lt;S, T&gt; {
//   getArrangedInput: (input: S) =&gt; T;
// }

// interface HeadRefreeInterface&lt;T&gt; {
//   getResult: () =&gt; T;
// }
// interface SubRefreeInterface&lt;T&gt; {
//   getCount: (input: T) =&gt; number;
// }
// ////////////// 애매한게, 서로 주고 받는 메시지 형식을 정해야 하는데, interface로 똑같이 두기에는 애매해서 일단 type로 둠.
// type AnswerType = number[];
// type ValidityResultType = {
//   inputLengthValidity: boolean;
//   inputTypeValidity: boolean;
// };
// type RoundResultType = {
//   ballCount: number;
//   strikeCount: number;
// };
// type SubValidityResultType = {
//   result: boolean;
// };
// type UserInputType = string;
// type InputArrangedType = number[];</code></pre>
<p>당시 class 이외의 구현체가 사용되는 경우를 생각해 설계했으나,</p>
<p>구현하지 못하고 스터디 시간이 되었다.
<br></p>
<p>정모씨와의 작은 토론이후 class를 사용할 수 밖에 없다는 결론을 내렸다.</p>
<p>object를 구현체로 사용하기에는 재사용성에서 불편했기 때문이다.</p>
<p>이후 class로 구현하고 동작을 우선시하여 코드를 작성했다.</p>
<h3 id="구현">구현</h3>
<p>결론적으로, 책에서 하지 말라는 것은 다 했던 것 같다..</p>
<ul>
<li>로직에 사용될 자료구조가 변할 수 있다 생각했기에 제네릭을 남발했다.</li>
<li>클래스 내에 사용될 객체들을 내부에서 직접 생성했다.</li>
<li>로직을 한 곳에서 관리하고자 생각에 로직을 따로 모아두어 import해왔다.</li>
</ul>
<p>작성한 코드는 다음과 같다.</p>
<pre><code class="language-typescript">
class Worker&lt;T&gt; implements WorkerI&lt;LogicResultT&gt; {
  private ingredients: T;
  private logic: (ingre: T) =&gt; LogicResultT;
  constructor(ingredients: T, logic: (ingre: T) =&gt; LogicResultT) {
    this.ingredients = ingredients;
    this.logic = logic;
  }
  get result() {
    // 로직과 요소를 합쳐 동작시킨다
    return this.logic(this.ingredients);
  }
}

class HeadGuard&lt;T extends HeadGuardLogicIngredientsI&gt; implements HeadGuardI {
  private logicGoalMap: {
    [goals in GuardGoalsT]: (ingre: T) =&gt; GuardResultT;
  };
  private guards: Worker&lt;T&gt;[];
  constructor({ ...props }: T) {
    this.logicGoalMap = {
      type: typeGuardLogic,
      len: lenGuradLogic,
      phase: phaseGuardLogic,
    };
    this.guards = Object.values(this.logicGoalMap).map(
      (logic) =&gt; new Worker(props, logic)
    );
  }

  validate() {
    // 외부 import로부터의 로직으로 만들어진 guard들을 실행시켜 결과를 반환한다.
  }
}

class HeadReferee implements WorkerI&lt;RefereeResultT&gt; {
  private logicsAndGoals: {
    logic: (ingre: LogicIngredientsI) =&gt; RefereeResultT;
    goal: RefereeGoalsT;
  }[];
  private referees;
  constructor(input, answer) {
    this.logicsAndGoals = [
      { logic: strikeLogic, goal: &#39;strike&#39; },
      { logic: ballLogic, goal: &#39;ball&#39; },
    ];

    this.referees = this.logicsAndGoals.map(
      ({ logic }) =&gt; new Worker({ input, answer }, logic)
    );
  }

  get result() {
    // 대충 외부에서 import해온 로직으로부터의 referee들을 실행시켜, 그 결과를 반환한다.
  }
}

class Game implements GameI {
  private _input: DataT;
  private answer: DataT;
  private phase: number;
  private isEnd: boolean;
  constructor(answer: DataT = new Questioner(3).answer) {
    this.answer = answer;
    this.phase = 0;
    this.isEnd = false;
  }
  _validate() {
    // new HeadGuard에 입력,정답,페이즈를 넣어 validate시킨다.
    headGuard.validate();
  }
  _judge() {
    // 대충 new HeadReferee에 입력과 정답을 넣어 결과를 반환한다.
    // 결과 중 phase정보를 통해 end여부를 판단한다.
  }
  process() {
    // validate 후 phase++. 이후 judge결과를 반환한다.
  }
  set input(newIinput: DataT) {
    // 입력할 수 있게 한다.
  }
}


class Questioner {
  private len;
  constructor(len: number) {
    this.len = len;
  }
  _makeRandomDigit() {
    return Math.floor(Math.random() * 9);
  }
  get answer() {
    // 대충 _makeRandomDigit으로 정답을 만든다
    return result;
  }
}

export default Game;
</code></pre>
<h3 id="연결">연결</h3>
<p>node환경에서 동작하도록 연결하는 과정에서 오류를 찾기 어려워 헤맸다.</p>
<p>역설적으로 TDD의 중요성을 체감했다.</p>
<h2 id="두-번째-시도--top-down">두 번째 시도 : Top-Down</h2>
<p>정모씨의 코드를 보고 DTO, DI와 같은 개념의 부재로 기준 없는 코드를 작성했음을 느꼈다.</p>
<ul>
<li><p>DTO를 통해 객체간 메시지를 객체로 만들어 익숙한 사용감을 주었다.</p>
</li>
<li><p>생성자 주입을 통해 테스트에 용이하고, 책임이 확실하게 분리된 클래스를 제작할 수 있었다.</p>
</li>
</ul>
<h3 id="설계-1">설계</h3>
<p>무엇보다도, 이전의 시도와 다른 점은 설계였다.</p>
<p>아예 클래스 다이어그램을 그렸는데, 그림을 그리니 훨씬 설계에 용이했다.
<br></p>
<p>최상위 클래스인 Controller와 view와의 연결이 어색하여,</p>
<p>가장 바깥 부분부터 편하게 생각하는 Top-Down방식으로 먼저 설계했다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/3b388151-ceb6-4b0a-9cae-a9d8e27e3e8c/image.png" alt=""></p>
<p>DTO들은 의도적으로 클래스만 제작했는데,</p>
<p>비즈니스 로직에 들어가는 객체들은 잘 변하지 않을 것이라는 점과,</p>
<p>데이터 객체들을 다른 객체들과 구분짓고 싶었기 때문이다.</p>
<h3 id="구현-1">구현</h3>
<p>이전보다는 편했지만</p>
<p>클래스를 구현하고 테스트 코드를 작성하며 수정할 부분이 생겼을 때,</p>
<p>상단부터 하단의 모델까지 많은 부분이 바뀌어야 했다.
<br></p>
<p>사용하는 입장에서 설계하기에 생각하기 편하지만, 단점이 존재함을 느꼈다.
<br></p>
<p>폴더 구조는 다음과 같다.
<img src="https://velog.velcdn.com/images/hys-lee/post/29c13679-b6d4-447f-a59b-98b614f7cdbd/image.png" alt=""></p>
<p>Controller의 index.ts는 정모씨의 코드를 보고 힌트를 얻었다.
<br></p>
<p>결국 생성자 주입을 적용하면,</p>
<p>최상단 클래스 생성자에는 여러 재료가 되는 인스턴스들을 인자로 넣어줘야 했다.</p>
<p>따라서, 각 인스턴스들을 미리 생성해놓고, 조립해주는 함수들을 만들었고,</p>
<p>이렇게 만든 Controller 인스턴스로 사용자에게 기능을 제공하도록 했다.</p>
<p>코드는 다음과 같다.</p>
<pre><code class="language-typescript">// 여기에 Game을 GameController에 넣는 동작을 만들어두자.
// 외부에서 controller사용할 때는, 어떤 게임 넣을지 직접 안하고,
//  메서드만 사용해서 실행시키기 위해.

/*
엄청난 클래스들 import부분
*/

// 게임에 필요한 설정들

const _makeConfig = () =&gt; {
  const DEFAULT_PHASE = 10;
  const DEFAULT_LEN = 3;
  const defaultNumberBaseballGameConfig = new DefaultGameConfigDTO(
    DEFAULT_PHASE,
    DEFAULT_LEN
  );
  return defaultNumberBaseballGameConfig;
};
const _makeGuard = () =&gt; {
  const typeGuard = new TypeGuard();
  const lenGuard = new LenGuard();
  const phaseGuard = new PhaseGuard();
  return new Guard1(typeGuard, lenGuard, phaseGuard);
};
const _makeReferee = () =&gt; {
  const strikeReferee = new StrikeReferee();
  const ballReferee = new BallReferee();
  return new Referee1([strikeReferee, ballReferee]);
};
const _makeAnswer = (answerLen: number) =&gt; {
  const typePitchingDTOguard = new TypePitchingDTOGuard();
  const identityPitchingDTOguard = new IdentityNumberPitchingDTOGuard();
  const answerMaker = new RandomAnswerMaker(
    typePitchingDTOguard,
    identityPitchingDTOguard
  );
  return answerMaker.makeAnswer(answerLen);
};

// 게임 생성

const _makeGame = () =&gt; {
  const config = _makeConfig();
  const guard = _makeGuard();
  const referee = _makeReferee();
  const answer = _makeAnswer(config.dataLen);

  return new NumberBaseballGame(config, guard, referee, answer);
};
const numberBaseballGame = _makeGame();

// 컨트롤러 생성

const defaultGameController = new DefaultGameController(numberBaseballGame);

const CustomConfigGameController = (config: GameConfigDTO) =&gt; {
  const customConfig = new DefaultGameConfigDTO(config.phase, config.dataLen);
  const guard = _makeGuard();
  const referee = _makeReferee();
  const answer = _makeAnswer(config.dataLen);
  const customNumberBaseballGame = new NumberBaseballGame(
    customConfig,
    guard,
    referee,
    answer
  );

  return new DefaultGameController(customNumberBaseballGame);
};

export default defaultGameController;
export { CustomConfigGameController };

</code></pre>
<h2 id="세-번째-시도--bottom-up">세 번째 시도 : Bottom-Up</h2>
<p>이번엔 바텀 업으로 다시 설계를 해봤다.</p>
<p>Bottom의 장점으로는 변화에 유연하다는 것인데,</p>
<p>이는 상위 구조가 하위 구조들로 이뤄지도록 제작하기 때문이다.</p>
<p>또한, 가장 낮은 구조부터 제작하므로, TDD가 자연스럽다는 것도 장점이다.</p>
<h3 id="설계-2">설계</h3>
<p>다이어그램은 다음과 같다.</p>
<p>이전보다 객체간 책임을 명료화 하는데 힘을 쓰니, 구현에 도움이 되었다.</p>
<p>객체가 가져야 할 동작들의 기준이 세워지게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/2cac7969-4398-4dbb-b2e6-4187ecc01f1f/image.png" alt=""></p>
<p>DTO들도 interface를 갖게 해서, 보다 유연하게 만들었다.
<br></p>
<p>Top-Down과 비교하면 달라진 부분들이 몇 있을 것인데,</p>
<p>대부분 책임을 다시 생각하는 과정에서 달라졌다.</p>
<p>예를 들어, PitchingDTO에 대해 factory와 validator를 제작하지 않는 것이 있다.</p>
<p>Bottom-Up 설계를 하면서, answer 데이터를 answerMaker가 만들지 않는다면,</p>
<p>또 다른 사용자로부터 받게 될텐데, 이에 따른 객체를 그 때 따로 만드는게 책임을 적절히 분리하는 것이라 생각하여 간단하게 DTO만 남겼다.
<br></p>
<p>또한, 말 그대로 class를 구현체로 삼다 보니, 네이밍 방식도 변화가 있었다.</p>
<p>구현 방법에 맞춰 구현체 이름을 짓게 되었다.</p>
<h3 id="구현-2">구현</h3>
<p>폴더 구조는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/e81d7c63-735e-45e8-96b1-f27600226885/image.png" alt=""></p>
<p>서비스 내부에서만 사용되는 타입들도 전부 models안에 넣었다.</p>
<p>컨트롤러까지 올라가는 데이터들은 DTO로, 서비스에서만 사용되는 것들은 단순 타입으로 정의했다.</p>
<pre><code class="language-typescript">import JudgeTypes from &#39;../../RuleTerms/Judgement/types/JudgeTypes&#39;;

type JudgeResult = {
  [key in JudgeTypes]?: number;
};
export { JudgeResult };
</code></pre>
<p>이번 컨트롤러 index.ts는 사용자 입장을 더 신경써서 제작했다.</p>
<p>게임 컨트롤러로 필요한 작업 중 컨트롤러 클래스를 직접 사용하는 메서드들과 아닌 메서드가 존재했기에,</p>
<p>클로저를 활용해서 사용자에게 메서드들을 제공했다.</p>
<pre><code class="language-typescript">
function _makeDefaultDistinguishingRefereeMaster() {
  const strikeReferee = new DefaultStrikeReferee();
  const ballReferee = new DefaultBallReferee();
  return new DistinguisingRefereeMaster([strikeReferee], [ballReferee]);
}
function _makeInsensitiveGuardMaster() {
  const typeguard = new DefaultTypeGaurd();
  const lengaurd = new DefaultLenGuard();
  const phaseGuard = new DefaultPhaseGuard();

  return new InsensitiveGuardMaster(typeguard, lengaurd, phaseGuard);
}
function _makeConfig() {
  const DEFAULT_PHASE = 10;
  const DEFAULT_DATALEN = 3;
  return new DefaultGameConfigDTO(DEFAULT_PHASE, DEFAULT_DATALEN);
}
function _makeAnswer(datalen: number) {
  const answerMaker = new RandomAnswerMaker();
  return answerMaker.makeAnswer(datalen);
}
function _makeGameMaster(config: GameConfigDTO = _makeConfig()) {
  const defaultRefereeMaster: RefereeMasters =
    _makeDefaultDistinguishingRefereeMaster();
  const defaultGuardMaster: GuardMaster = _makeInsensitiveGuardMaster();
  const answer = _makeAnswer(config.dataLen);
  const gameMaster = new PitchingGameMaster(
    defaultRefereeMaster,
    defaultGuardMaster,
    answer,
    config
  );
  return gameMaster;
}
const initCustomGame = (gameMaster: GameMaster) =&gt; {
  const gameController = new DefaultGameController(gameMaster);
  return gameController;
};

const initPithicngGame = (config: GameConfigDTO = _makeConfig()) =&gt; {
  const gameMaster: GameMaster = _makeGameMaster(config);
  const gameController = new DefaultGameController(gameMaster);
  const restartGame = (config: GameConfigDTO) =&gt; {
    const newGM = _makeGameMaster(config);
    return new DefaultGameController(newGM);
  };
  /**
   * gameRun 설명
   * @param input number[]
   * @returns { isSuccess: boolean;  detailResult: {strike:number, ball:number};  remainPhase: number;}
   */
  const gameRun = (input: number[]) =&gt; {
    const pitchingInput = new ArrayPitchingDTO(input);
    return gameController.run(pitchingInput);
  };
  return {
    gameRun,
    restartGame,
    end: gameController.end,
    getLastestResult: gameController.getLastestResult,
  };
};

export { initPithicngGame };
</code></pre>
<br>

<h1 id="끝내며">끝내며</h1>
<p>OOP의 최대 장점을 느낄 수 있었다.</p>
<p>바로, &quot;유연한 설계&quot;이다.</p>
<p>애초에 프로그래밍을 하며 제대로 설계해본 경험이 없었는데,</p>
<p>이번 경험을 통해 변화에 유연하고 에러가 적은 구현을 할 수 있었다.
<br></p>
<p>또한, 프론트엔드에서도 반드시 적용되어야 할 개념도 적용해 볼 수 있었는데,</p>
<p>&quot;책임 분리&quot;, &quot;적절한 추상화 레벨&quot;, &quot;테스트 코드 작성&quot;이다.
<br></p>
<p>사실, 이번 실습이후에 고민이 커졌다.</p>
<p>이렇게 장점이 많은데, 프론트엔드에서 적용하기가 어려워보였기 때문이다.</p>
<p>다음 몇 포스트들은 이에 대한 고민을 담은 글일 것이다.</p>
<h4 id="보너스-프리코스-시절-코드">*보너스* 프리코스 시절 코드</h4>
<pre><code class="language-javascript">import { MissionUtils } from &#39;@woowacourse/mission-utils&#39;;

const getCom = function generateComputer(computer) {
  while (computer.length &lt; 3) {
    // 함수 이름에 따라 Number만 선택됨.
    let number = MissionUtils.Random.pickNumberInRange(1, 9);
    if (!computer.includes(number)) {
      computer.push(number);
    }
  }
};

const getBbNum = async function inputTreating() {
  // 인풋 받기

  let rawBaseballNum = await MissionUtils.Console.readLineAsync(
    &#39;숫자를 입력해주세요 : &#39;
  );

  // valid 확인 - 문자열 길이
  if (rawBaseballNum.length != 3) {
    throw new Error(&#39;[ERROR] 숫자가 잘못된 형식입니다.&#39;);
  }

  // valid 확인 -  각 자리 값
  let parsedBaseballNum = rawBaseballNum.split(&#39;&#39;).map((raw) =&gt; {
    let parsed = Number(raw);
    if (parsed === 0 || Number.isNaN(parsed)) {
      throw new Error(&#39;[ERROR] 숫자가 잘못된 형식입니다.&#39;);
    }
    return parsed;
  });

  // valid한 인풋 값.
  return parsedBaseballNum;
};

const match = function matchingComputerNumWithBaseballNum(
  computer,
  baseballNum
) {
  // 비교하기
  let strikes = 0;
  let balls = 0;

  // 비교하기 - 숫자 포함 여부부터.
  baseballNum.forEach((bNum, index) =&gt; {
    if (computer.includes(bNum)) {
      if (bNum === computer[index]) {
        strikes += 1;
        return false;
      }
      balls += 1;
    }
  });

  // 비교하기 - 코멘트 정하기
  let ballComment = balls !== 0 ? `${balls}볼` : &#39;&#39;;
  let strikesComment = strikes !== 0 ? `${strikes}스트라이크` : &#39;&#39;;
  let beteween = balls !== 0 &amp;&amp; strikes !== 0 ? &#39; &#39; : &#39;&#39;;

  let commentFinal =
    balls === 0 &amp;&amp; strikes === 0
      ? &#39;낫싱&#39;
      : `${ballComment}${beteween}${strikesComment}`;

  // 비교하기 = 출력하기
  MissionUtils.Console.print(commentFinal);

  if (strikes === 3) {
    return true;
  }
  return false;
};

class App {
  async play() {
    try {
      MissionUtils.Console.print(&#39;숫자 야구 게임을 시작합니다.&#39;);
      let start = 1;
      while (start === 1) {
        // 컴퓨터 값 생성.
        let computer = [];

        getCom(computer);

        let correct = false;
        // 비교
        while (!correct) {
          let baseballNum = await getBbNum();
          correct = match(computer, baseballNum);
        }

        // 게임 종료 시.
        MissionUtils.Console.print(&#39;3개의 숫자를 모두 맞히셨습니다! 게임 종료&#39;);

        // 계속 및 종료
        start = Number(
          await MissionUtils.Console.readLineAsync(
            &#39;게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.\n&#39;
          )
        );

        // 계속 및 종료의 에러 체크
        if (start !== 1 &amp;&amp; start !== 2) {
          throw new Error(&#39;[ERROR] 숫자가 잘못된 형식입니다.&#39;);
        }
      }
    } catch (e) {
      throw e;
    }
  }
}

export default App;</code></pre>
<p>책임과 객체에 대한 이해가 하나도 없는 모습이다...</p>
<p>그래도 이 때랑 비교하면 성장 체감은 확실히 되는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vanilla JS로 React 구현하기]]></title>
            <link>https://velog.io/@hys-lee/Vanilla-JS%EB%A1%9C-React-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hys-lee/Vanilla-JS%EB%A1%9C-React-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 26 Oct 2024 08:26:42 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하면서">시작하면서...</h2>
<p>최근, 동아리에서 React기초 세미나를 개최하게 됐다.</p>
<p>React를 사용하는데 있어, 여러가지 제약이나 규칙이 존재하는데,
이들이 &#39;왜 필요한가&#39;에 대해서는 알려주지 못했다.</p>
<p>너무나도 자연스럽게 사용하고 있던 React의 비밀을 파헤치기 위해
React의 동작을 Vanilla JS로 구현하며 파헤쳐보자..!</p>
<p>(<a href="https://junilhwang.github.io/TIL/">개발자 황준일</a>블로그를 많이 참조했다.)</p>
<hr>
<h1 id="실습">실습</h1>
<br>

<h2 id="세팅">세팅</h2>
<p>참조 글을 따라, 함수형 컴포넌트를 구현한다.</p>
<pre><code class="language-javascript">import renderDebounce from &#39;./renderDebounce&#39;;

function MyReact() {
  const options = {
    state: [],
    currentStateKey: 0,
    root: null,
    rootComponent: null,
  };

  // _render에서의 renderDebounce는 batch 동작 수행하는 역할.
  // requestAnimationFrame을 통한 debounce.
  const _render = renderDebounce(() =&gt; { 
    if (!options.root || !options.rootComponent) return;
    options.root.innerHTML = options.rootComponent();
    options.currentStateKey = 0; 
  });

  function render(root, rootComponent) {
    options.root = root;
    options.rootComponent = rootComponent;
    _render();
  }
  function setState(newState) {
    options.state[options.currentStateKey] = newState;
    _render();
  }
  function useState(initState) {
    const { state, currentStateKey } = options; 
    if (state.length === currentStateKey) {
      state.push(initState);
    }
    options.currentStateKey += 1; 
    return [state[currentStateKey], setState]; 
  }
  return { useState, render };
}

export const { useState, render } = MyReact();
</code></pre>
<br>

<h2 id="목표">목표</h2>
<p>참조글에는 위의 React라이브러리를 사용해 함수형 컴포넌트를 동작시킨다.</p>
<p>글 말미에도 나와있듯이, eventListener를 사용할 때 전역 변수를 이용한다.</p>
<p>이는 버그 가능성을 높이고 사용에 불편한 방식이므로,
이 부분을 먼저 해결하고자 했다.</p>
<br>

<p>추가적으로 세운 목표는 다음과 같다.</p>
<ul>
<li>Diff알고리즘을 적용</li>
<li>setState실행이 관련 컴포넌트에서 re-render를 트리거</li>
<li>useEffect제작 및 render이후 동작 수행</li>
</ul>
<pre><code class="language-javascript">/*useState가 사용된 컴포넌트부터 리렌더(실행)시키도록.
ㄴ&gt; 구현이 어렵..
diff알고리즘 적용
이벤트 리스너에 달기.
useEffect만들기.
ㄴrender이후에 mounted과정에서 실행.
컴포넌트 반환으로 인한 처리랑 일반 노드 처리랑 동일하게 만들어서, 이벤트 리스너와 props에 대해 동일하게 처리할 수 있도록 함.
render 사이클에 hook을 넣기 어려웠음.. 함수형 컴포넌트에서 이대로 사용한다면.</code></pre>
<br>

<h2 id="과정">과정</h2>
<h4 id="1-event-핸들링">1. Event 핸들링</h4>
<p>jsx 사용경험을 최대한 보존하기 위해, 참조글에서 사용한 innerHTML형식을 그대로 이용하려고 시도했다.</p>
<p>이 방법은 innerHTML이 결국 string이기 때문에, &#39;값&#39;을 넣어야 제대로 동작한다는 한계가 있었다. 문자열 내에서는 함수를 정의할 수 없기 때문.</p>
<p>그래서 참조글에서도 이름을 가진 함수를 전역변수로 정의하고, 이를 참조하도록 한 것이다.</p>
<br>
이러한 한계를 극복하기 위해, 참조 글 이전 포스트의 h함수를 사용하기로 했다.

<blockquote>
<p>여기서 h함수란, Virtual DOM을 Real DOM으로 바꿔주는 함수이다.</p>
<p>사용이 불편하기에 JSX를 사용하게 되었으나,
babel을 통해 이 방식으로 트랜스파일한다 가정하에 진행했다.</p>
</blockquote>
<p>완성된 innerHTML이 아니라,
정보들을 props로 받아들이기 때문에 후처리가 편하다는 이유에서였다.</p>
<p>적용한 코드는 다음과 같다.</p>
<pre><code class="language-javascript">// core/MyJsxConvertion.js

function isEventProp(propKey) {
  // &#39;onClick&#39;처럼 react에서 사용하는 event handling props 사용에 맞춰 처리.
  // &#39;on&#39;으로 시작하고 뒤에 대문자가 오는 경우 event handling props로 인지.
  return (
    propKey.slice(0, 2) === &#39;on&#39; &amp;&amp;
    &#39;A&#39; &lt;= propKey.slice(2, 3) &amp;&amp;
    propKey.slice(2, 3) &lt;= &#39;Z&#39;
  );
}
function setProps(node, props) {
  if (props !== null) {
    Object.keys(props).forEach((propKey) =&gt; {
      if (isEventProp(propKey)) {
        // 이벤트 핸들링
        node.addEventListener(propKey.slice(2).toLowerCase(), props[propKey]);
      } else {
        node.setAttribute(propKey, props[propKey]);
      }
    });
  }
}

function setChildren (node,children){
  children.forEach((child) =&gt; {
    if (typeof child === &#39;string&#39;) {
      node.innerHTML += child;
    } else {
      // child는 MyJsxConvertion함수의 반환인 element
      node.appendChild(child);
    }
  });
}

function MyJsxConvertion(type, props, ...children) {
  const node = document.createElement(type);

  // props붙이기
  setProps(node, props);

  //children관리
  setChildren(node, children);

  return node;
}

export default MyJsxConvertion;
</code></pre>
<br>

<p>이를 사용한 함수형 컴포넌트의 모습은 다음과 같다.</p>
<pre><code class="language-javascript">// Component/Mix.js

import h from &#39;../core/MyJsxConvertion.js&#39;;

const Mix = () =&gt; {
  return (
    h(&#39;div&#39;,null,
        h(&#39;ul&#39;,null,
            h(&#39;li&#39;, { key: 1 }, &#39;리스트1&#39;),
              h(&#39;li&#39;, { key: 2 }, &#39;리스트2&#39;),
              MixChild()
        )
    )
  );
};
export default Mix;

// Component/MixChild.js
import h from &#39;../core/MyJsxConvertion.js&#39;;

export default MixChild(){
    return (
        h(
          &#39;button&#39;,
          {onClick:()=&gt;{console.log(&quot;클릭&quot;)}},
          &#39;버튼&#39;
        );
    );
}
</code></pre>
<p>이처럼 h함수를 사용한다면, 우리의 MyReact라이브러리에도 수정이 필요하다.</p>
<p>반환이 innerHTML이 아닌 element이기 때문!</p>
<br>
렌더링의 핵심인 _render함수만 수정해주었다.

<pre><code class="language-javascript">const _render = renderDebounce(() =&gt; {
    if (!options.root || !options.rootComponent) return;

      // 이 밑의 부분이 수정된 부분이다.
    if (options.root.childNodes.length !== 0) {
      options.root.removeChild(options.root.childNodes[0]);
    }
    options.root.appendChild(options.rootComponent());
      // 이 윗 부분이 수정된 부분이다.

    options.currentStateKey = 0; 
  });</code></pre>
<p>여기서 render함수가 root에서만 사용되므로, childNode[0]만을 고려했다.</p>
<p>렌더마다 새롭게 생성된 element들을 root element에 교체하는 방식이다.
<br>
이제 이벤트 핸들링을 간편하게 할 수 있게 됐다..!</p>
<blockquote>
<p>이 밑의 과정은 추가 목표를 달성하기 위해 삽질했던 경험이다.</p>
<p>나름의 고뇌가 들어갔다 생각하지만, 결과적으로 useEffect제작 이외엔 달성에 실패했기에 참고하여 읽길 바란다.</p>
</blockquote>
<br>

<h4 id="2-diff알고리즘을-적용">2. Diff알고리즘을 적용</h4>
<p>참조 글의 이전 포스트에 Diff알고리즘을 클래스형 컴포넌트에 적용하는 내용이 있었다.</p>
<p>처음엔 이 방법을 함수형 컴포넌트에 이식하고자 했다.</p>
<br>
먼저, 클래스형 컴포넌트에 있던 Diff알고리즘에 대한 코드를 살펴보자.

<pre><code class="language-javascript">// core/Component.js
class Component{
    ...
    render(){
      // 이 부분을 통해 기존의 노드(target)과 새로운 노드(newNode)를 
      // Diff알고리즘 코드(updateElement)에 넣는다.
      const newNode = this.$target.cloneNode(true);
      newNode.innerHTML = this.template(); // 새로운 템플릿 넣기.

      const oldChildNodes = [...this.$target.childNodes];
      const newChildNodes = [...newNode.childNodes];
      const maxIter = Math.max(oldChildNodes.length, newChildNodes.length);
      for (let i = 0; i &lt; maxIter; i++) {
        updateElement(this.$target, newChildNodes[i], oldChildNodes[i]);
      }

      // 밑은 EventListener를 다시 설정하는 과정이다.
      requestAnimationFrame(() =&gt; {
        this.setEvent();
      });

      ...
    }
    ...
}


// core/vdom.js
export function updateElement(parent, newNode, oldNode) {
// parent랑 oldNode가 실제 document에 메달린 노드들이 됨.

  // 1. oldNode만 =&gt; 얘 제거
  if (!newNode &amp;&amp; oldNode) {
    oldNode.remove();
    return;
  }
  // 2. newNode만 =&gt; 얘 추가
  if (newNode &amp;&amp; !oldNode) {
    parent.appendChild(newNode);
    return;
  }
  // 3. 모두 text타입 =&gt; nodeValue보고 판단
  if (newNode instanceof Text &amp;&amp; oldNode instanceof Text) {
    if (oldNode.nodeValue === newNode.nodeValue) return;
    oldNode.nodeValue = newNode.nodeValue;
    return;
  }

  // 4. old와 new 태그이름이 다를 경우
  if (newNode.nodeName !== oldNode.nodeName) {
    const idx = [...parent.childNodes].indexOf(oldNode);
    oldNode.remove();
    parent.insertBefore(newNode, parent.children[idx] || null);
    return;
  }

  // 5. 태그 이름은 같은 경우.=&gt;속성 비교해야 함.
  updateAttributes(oldNode, newNode);

  // 6. 자식들에 대해 1~5과정 반복
  const newChildren = [...newNode.childNodes];
  const oldChildren = [...oldNode.childNodes];
  const maxLength = Math.max(newChildren.length, oldChildren.length);
  for (let i = 0; i &lt; maxLength; i++) {
    updateElement(oldNode, newChildren[i], oldChildren[i]);
  }
}

function updateAttributes(oldNode, newNode) {
  const oldProps = [...oldNode.attributes];
  const newProps = [...newNode.attributes];

  for (const { name, value } of newProps) {
    if (value === oldNode.getAttribute(name)) continue;
    oldNode.setAttribute(name, value);
  }

  for (const { name, value } of oldProps) {
    if (newNode.getAttribute(name) !== undefined) continue;
    oldNode.removeAttribute(name);
  }
}
</code></pre>
<p>좀 길긴 하지만 이해에 무리가 가지는 않을 것이다.</p>
<p>중요한 것은, 이 방법은 기존의 노드와 새로운 노드를 비교하며
변경점을 기존 노드에 적용하는 방식이라는 것이다.</p>
<p>이는 EventListener를 수정할 때 문제가 된다.</p>
<p>DOM API에서는 특정 노드의 EventLisetner들을 조회할 수 없다.
이들을 참조할 유일한 방법은 EventListener에 등록된 함수 자체를 알고 있는 것이다.</p>
<p>즉, 각 노드마다의 정보(EventListener를 포함한)를 알고 있어야 할 것이다.</p>
<p>이는 Component class로 컴포넌트 내 사용된 eventListener정보를 저장하여 해결할 수 있을 것이므로, </p>
<p>&quot;클래스형 컴포넌트 + 함수형 컴포넌트&quot;를 목표로 잡았다.</p>
<h4 id="3-setstate실행이-관련-컴포넌트에서-re-render를-트리거">3. setState실행이 관련 컴포넌트에서 re-render를 트리거</h4>
<p>이를 달성하기 위해서도 &quot;클래스형 컴포넌트 + 함수형 컴포넌트&quot;를 목표로 잡았다.</p>
<p>useState가 실행되는 곳의 컴포넌트를 render시켜야 했기 때문에,
해당 컴포넌트의 정보를 알고 있어야 했다.</p>
<p>가장 간단한 방법은, setState를 통해 해당 컴포넌트의 render를 실행하는 것이라 생각해 위의 목표에 도전했다.</p>
<h4 id="⭐특수목표⭐----render-수정하기">⭐특수목표⭐ -  render 수정하기</h4>
<p>⚒️시도 1</p>
<p>함수형 컴포넌트에서 사용되는 setState가
클래스형 컴포넌트의 메서드인 render를 실행시키기 위해선, </p>
<p>MyReact라이브러리 내부 useState안에 인스턴스 정보를 전달해야 한다.</p>
<p>먼저, 함수형 컴포넌트 내부에 인스턴스 정보를 전달하기 위해 다음의 방법을 사용했다.</p>
<pre><code class="language-javascript">// Component/ParentComponent.js
function ParentComponent(){
    return (
        h(&#39;div&#39;,null,
          new Component(ChildComponent.bind(this))
    );
}

// core/Componenet.js
class Component{
    constructor($target){
        this.$target = $target.bind(this);
    }
}

/*
// core/MyJsxConvertion.js
import Component from &#39;./Component.js&#39;;
function MyJsxConvertion(){
    ...
    //children관리
    if (child instanceof Component){
      child = 
    } else if (typeof child === &#39;string&#39;){
      // string타입 처리
    }else{
      // 다른 기본 node들 대한 처리
    }
}*/
</code></pre>
<p>문제는 함수형 컴포넌트 안에서 useState안으로 this를 전달할 때에 있었다.
최대한 React의 함수형 컴포넌트 사용 경험을 유지하려 했기에,</p>
<p>함수형 컴포넌트 안에서 useState에 this를 직접 바인딩해 사용하진 않으려 했다.</p>
<pre><code class="language-javascript">fuction MyComponent(){
      // babel적용 시 밑의 코드가 된다는 방법은 쫌... 
      const [state,setState] = useState.bind(this)(initState);
    return ;
}</code></pre>
<p>이렇게 가정한다면, 일관성에 문제가 생긴다.</p>
<p>custom Hooks에도 똑같이 바벨 적용 후 <code>.bind(this)</code>가 추가될 것이라 가정해야 하는데, 이는 옳지 못하다.
babel을 통해 &#39;use로 시작하는 함수 뒤에는 <code>.bind(this)</code>를 붙여라&#39;를 수행할 수 있지만, React에서 customHook 이름을 use로 시작하도록 하는 것은 Convention일 뿐이다.</p>
<p>개발에 혼란이 없도록 하는 Naming Convention일 뿐, 메커니즘적 오류를 일으키지 않으므로 이 방법은 제외하기로 했다.</p>
<br>

<p>⚒️시도 2</p>
<p>useState가 Component클래스를 통해 정의된다면 컴포넌트 정보를 얻을 수 있으니, MyReact를 수정하기로 했다. </p>
<p>MyReact안에 Component클래스를 정의하면 가능할 듯 했다.</p>
<p>실제 React라이브러리를 사용할 때도 React.Component로 사용하므로,
가능성이 보였다.</p>
<pre><code class="language-javascript">// core/MyReact.js
function MyReact(){
  const options={
      ...
    root:null,
    rootComponent:null
  };

  const _render=renderDebouncer(()=&gt;{
      ...
  });

  function render(root, rootComponent){
      options.root = root;
    options.rootComponent = rootComponent;
    _render();
  }

  function functionUseState(initState, thisBinded){
      ...
    function setState(newState){
          ...
        state = newState;

          // 넘겨받은 thisBinded를 활용한다.
          render(thisBinded.$parent, thisBinded.$target);

    };
    return [state, setState.bind(thisBinded)];
  }

  class Component{
    $parent;
      constructor($target){
        this.$target = $target;
          ...
    }
    render(){
        this.$parent.appendChild(this.$target());
    }
    setup($parent){
        this.$parent = $parent;
    }

    // 여기가 핵심이다.
    static classUseState=(initState)=&gt;{
        functionUseState(initState, this);
    }
  }

  const useState = Component.classUseState;
  return {useState, render, Component};
}
export default {useState, render, Component} = MyReact();

// core/MyJsxConvertion.js
function MyJsxConvertion(type, props, ...children){
  const node = document.createElement(type);

  ...

  // children관리
  children.forEach(child=&gt;{
      if (child instanceof Component){

      // 부모 노드를 인스턴스에 넘기기
      child.setup(node);
      child.render();
    } else if (typeof child === &#39;string&#39;){
      // string타입 처리
      ...
    }else{
      // 다른 기본 node들 대한 처리
      ...
    }
  });
}</code></pre>
<p>이 방법 역시 실패했다.</p>
<p>핵심은 static 메서드를 화살표 함수로 정의해, this를 미리 바인딩하는 것이었는데,
이는 불가능 했다.</p>
<p>클래스 관련한 객체들의 생성 순서는 다음과 같이 때문이다.</p>
<blockquote>
<h4 id="class-객체---클래스prototype---클래스-인스턴스">Class 객체 -&gt; 클래스.prototype -&gt; 클래스 인스턴스.</h4>
</blockquote>
<p>따라서 아직 만들어지지도 않은 클래스 인스턴스를 참조할 순 없었다.</p>
<p><del>이쯤 되니, 실제로 React는 어떻게 만들어졌는지 궁금해진다...</del></p>
<br>


<h4 id="4-useeffect제작-및-render이후-동작-수행">4. useEffect제작 및 render이후 동작 수행</h4>
<p>useEffect제작은 간단했다. </p>
<p><a href="https://hewonjeong.github.io/deep-dive-how-do-react-hooks-really-work-ko/">[번역] 심층 분석: React Hook은 실제로 어떻게 동작할까?</a> 를 참고하여 다음과 같이 구현했다.</p>
<pre><code class="language-javascript">// core/MyReact.js
function MyReact(){
    ...
    function useEffect(callback, deps) {
      if (options.deps.length === options.currentEffectKey) {
        options.deps.push(deps);
      }
      let depsChanged = false;
      for (let i = 0; i &lt; Math.max(deps.length, options.deps.length); i++) {
        if (deps[i] !== options.deps[options.currentEffectKey][i]) {
          depsChanged = true;
        }
      }
      if (!deps || depsChanged) {
        callback();
        options.deps = deps;
      }
    }
}</code></pre>
<p>사실 주된 목표가 render 이후에 callback을 실행하는 것이었는데,
이를 위해 각 컴포넌트의 render이후에 이에 맞는 useEffect가 실행돼야 했다. </p>
<p>하지만, 컴포넌트 객체를 클래스와 같이 사용할 수 없기에 개별 render 내부에 자신의 useEffect를 실행하기 불가능했고,</p>
<p>MyReact내의 render내에서 useEffect가 적용될 컴포넌트를 특정할 수 없어 이는 포기했다.
<br></p>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/c6b7e909-1a45-4a24-b4df-e48d1b48291b/image.gif" alt=""></p>
<p>click 이벤트에 대해 잘 동작하는 것을 확인할 수 있다!</p>
<p>해당 컴포넌트 코드는 다음과 같다.</p>
<pre><code class="language-javascript">import h from &#39;../core/MyJsxConvertion&#39;;
import { useState, useEffect } from &#39;../core/MyReact&#39;;

export default function MixChild() {
  const [state, setState] = useState(3);

  useEffect(() =&gt; {
    console.log(&#39;클릭함!&#39;);
  }, [state]);

  return h(
    &#39;div&#39;,
    {
      onClick: () =&gt; {
        setState(state + 1);
      },
    },
    `${state}`
  );
}
</code></pre>
<br>

<h2 id="후기">후기</h2>
<p>위의 발전과 삽질 과정에서 알게된 점은 다음과 같다.</p>
<ul>
<li><strong>Hooks을 조건문 등의 내부에서 사용하지 않는 규칙의 이유를 느꼈다.</strong>
<br> MyReact 라이브러리 내부에서 hooks를 관리할 때, 각 hook들에 사용되는 자원을 array의 형태로 저장하고, 실행 순서에 맞는 자원을 참조하도록 했다. <br> 
조건문안에 hooks가 사용된다면 참조 대상이 바뀌게 될 것이다!</li>
</ul>
<br>

<ul>
<li><strong>Fiber 사용 이유</strong>
<br> 컴포넌트 마다 정보를 가지고 있고 각 렌더링 사이클에 접근할 수 있어야 Hooks가 완벽하게 제 역할이 가능한데,
<br>이는 Class형 컴포넌트와 결합하여 이뤄낼 순 없다.<br> 다른 구조가 필요한데, 이는 Fiber가 될 것 같다. 
<br>공부할 명분을 얻었다..!</li>
</ul>
<br>

<p>이외에, 참조 글의 관련 포스팅을 따라오며 알게된 점은 다음과 같다.</p>
<ul>
<li><p><strong>클래스형 컴포넌트에 대한 친숙도</strong><br>
  클래스형 컴포넌트 사용시 필요했던 컴포넌트 생명 주기를 공부할 때 도움이 될 것이다.
  this를 이용하기에 state가 항상 최신값이 된다는 것이 렌더링 사이클과 어긋난다는 한계점에 대해 이해했다.</p>
  <br></li>
<li><p>Virtual DOM에 대한 이해  (핵심은 repaint-reflow방지=&gt;in memory변화
  <br>정체가 단순하게 type, props, children을 갖는 Object임을 알게 됐다. in-memory의 변화로 batch하게 DOM을 변경하여 reflow,repaint를 방지하는 목표로 사용됨을 느꼈다.</p>
<br>  
</li>
<li><p>Observer pattern이해, 다른 상태관리 방법들 이해 단초
  <br> 전역 상태 관리든, Proxy든, Atomic이든, Observer Pattern이 사용될 것임을 알 수 있었다. 이에 대한 공부에 단초가 될 것이다.</p>
</li>
<li><p>requestAnimationFrame 친숙도
  <br> setTimeout처럼 편하게 사용할 수 있게 됐다. 다음 프레임 전에 실행이 확정된다는 점이 매우 편리하게 느껴졌다.</p>
</li>
<li><p>Class에서 prototype, this에 대한 이해
   <br> 인스턴스 메서드, class관련 객체 생성 순서, 클래스 내부에서 this위치에 따른 바인딩에 대해 공부하게 되었다. </p>
</li>
<li><p>Diff알고리즘 구현 경험</p>
</li>
<li><p>Proxy객체 사용법</p>
</li>
</ul>
<br>
<br>

<p>사실은 React에 대한 이해를 최우선으로 했는데, 보다 깊은 이해를 위해서는 클래스형 컴포넌트를 직접 공부하고 Fiber구조에 대해 알아보는게 확실해보였다.</p>
<p>그래도 이들에 대한 단초를 얻고, JS를 보다 이해할 수 있어 좋았다.</p>
<hr>
<h2 id="참조">참조</h2>
<p><a href="https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Make-useSate-hook/">Vanilla Javascript로 React UseState Hook 만들기</a>
<a href="https://hewonjeong.github.io/deep-dive-how-do-react-hooks-really-work-ko/">[번역] 심층 분석: React Hook은 실제로 어떻게 동작할까?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Authentication 찍먹하기(3) - OAuth]]></title>
            <link>https://velog.io/@hys-lee/Authentication-%EC%B0%8D%EB%A8%B9%ED%95%98%EA%B8%B03-OAuth</link>
            <guid>https://velog.io/@hys-lee/Authentication-%EC%B0%8D%EB%A8%B9%ED%95%98%EA%B8%B03-OAuth</guid>
            <pubDate>Mon, 16 Sep 2024 09:12:02 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하면서">시작하면서...</h2>
<p>Authentication 찍먹하기 시리즈의 마지막이 될 OAuth이다.</p>
<p>Google의 OAuth를 사용한 경험을 기록한 것이기 때문에, 
다른 기업의 것들과 세세한 부분에서 차이가 있을 수 있음을 알린다.</p>
<p>또한, OAuth로 얻은 사용자의 정보는 어플리케이션 서버가 갖기 때문에,
이를 이용해서 사용자에게 권한을 주고 로그인 시키기 위해서
JWT를 같이 사용하는 경우가 많다고 한다.</p>
<p>본 실습에서도 JWT를 같이 사용해 클라이언트에서 인증을 확인할 수 있도록 했다.</p>
<p>또한, 보다 현실적인 실습을 위해 엑세스 토큰과 리프레시 토큰을 사용했음을 참고하기 바란다.
권한과 관련해서 위의 토큰들을 db에 저장하지 않기 위해서도 JWT를 사용했다.</p>
<p>마지막으로, 실습에 사용한 OAuth버전은 2.0이다.</p>
<h1 id="실습">실습</h1>
<p>모든 실습은 express를 통해 진행되었다.</p>
<p>이번에는 OAuth를 통해 얻은 사용자 정보를 DB에 저장하고, 
이를 이용해서 토큰을 발행하는 방식으로 진행했기 때문에
MongoDB를 같이 사용하였다.</p>
<p>id token을 통해 유저 정보를 얻고,
access token을 통해 권한에 맞는 api를 사용하는 실습을 진행한다.
(권한을 규정하지는 않고 단순히 access token만을 사용했다.)</p>
<h2 id="oauth">OAuth</h2>
<h3 id="기본-세팅">기본 세팅</h3>
<p>실습에 필요한 패키지들을 미리 설치하자.</p>
<p>node-fetch
dotenv
mongoose
jsonwebtoken
cookie-parser</p>
<p>우선 세상에서 가장 간단한 백엔드 코드부터 시작해보자.</p>
<pre><code class="language-javascript">import express from &#39;express&#39;;
import jwt from &#39;jsonwebtoken&#39;;
import dotenv from &#39;dotenv&#39;;
import cookieParser from &#39;cookie-parser&#39;;

dotenv.config();

const app = express();
app.use(express.json()); // oauth 사용을 위한 미들웨어. 데이터 통신에 사용.
app.use(cookieParser()); // 쿠키 작업을 위한 미들웨어

app.get(&#39;/&#39;, (req, res, next) =&gt; {
  console.log(&#39;와우&#39;);
});</code></pre>
<h3 id="oauth-적용하기">OAuth 적용하기</h3>
<h4 id="⏹️-google-oauth로-넘어가기-전">⏹️ Google oauth로 넘어가기 전</h4>
<p>코드 작성보다, Google API에서 OAuth관련 설정을 하는 것이 우선이다.
참조를 보고 설정을 마치고 client id, client secret를 .env파일에 저장하자.</p>
<p>편하게 사용하기 위해 google oauth url, access token url, token info url역시 .env파일에 저장했다.
이들은 다음과 같다.</p>
<pre><code>GOOGLE_OAUTH_URL=https://accounts.google.com/o/oauth2/v2/auth
GOOGLE_ACCESS_TOKEN_URL=https://oauth2.googleapis.com/token
GOOGLE_TOKEN_INFO_URL=https://oauth2.googleapis.com/tokeninfo</code></pre><p>또한, OAuth 인증을 통해 얻을 수 있는 유저 정보들을 지정해야 한다.</p>
<p>이들을 적용해서 코드를 작성하면 다음과 같다.</p>
<pre><code class="language-javascript">const GOOGLE_OAUTH_URL = process.env.GOOGLE_OAUTH_URL;
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_OAUTH_SCOPES = [
  &#39;https%3A//www.googleapis.com/auth/userinfo.email&#39;,
  &#39;https%3A//www.googleapis.com/auth/userinfo.profile&#39;,
];

...

app.get(&#39;/&#39;, async (req, res, next) =&gt; {
  const state = &#39;some_state&#39;;
  const scope = GOOGLE_OAUTH_SCOPES.join(&#39; &#39;);
  const GOOGLE_OAUTH_CONTENT_SCREEN_URL = `${GOOGLE_OAUTH_URL}?client_id=${GOOGLE_CLIENT_ID}&amp;redirect_uri=${GOOGLE_CALLBACK_URL}&amp;access_type=offline&amp;response_type=code&amp;state=${state}&amp;scope=${scope}`;
  res.redirect(GOOGLE_OAUTH_CONTENT_SCREEN_URL);
});
</code></pre>
<p>GOOGLE_OAUTH_CONTENT_SCREEN_URL을 통해 
google oauth로그인 창이 있는 페이지로 리다이렉트 된다.</p>
<p>참고로, 이 페이지는 google api에서 oauth설정할 때 커스텀할 수 있다.
<br></p>
<h4 id="⏹️-google-oauth에서-넘어온-후">⏹️ Google oauth에서 넘어온 후</h4>
<p>oauth인증을 완료하고 어플리케이션 페이지로 다시 돌아올 때,
유저의 정보를 얻을 api라우터를 다음과 같이 작성한다.</p>
<p>이 라우터를 통해 OAuth에 등록된 어플리케이션의 서버는 유저의 정보를 얻게 된다.
이 정보에는 id token, access token, refresh token등의 데이터가 들어있다.</p>
<pre><code class="language-javascript">const GOOGLE_CALLBACK_URL = &#39;http%3A//localhost:8000/google/callback&#39;;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const GOOGLE_ACCESS_TOKEN_URL = process.env.GOOGLE_ACCESS_TOKEN_URL;

app.get(&#39;/google/callback&#39;, async (req, res) =&gt; {
  const { code, state } = req.query;

  const data = {
    code,
    client_id: GOOGLE_CLIENT_ID,
    client_secret: GOOGLE_CLIENT_SECRET,
    redirect_uri: &#39;http://localhost:8000/google/callback&#39;,
    grant_type: &#39;authorization_code&#39;,
  };

  // 구글에게 access_token관련 데이터를 요청하자.
  const response = await fetch(GOOGLE_ACCESS_TOKEN_URL, {
    method: &#39;POST&#39;,
    body: JSON.stringify(data),
  });

  const access_token_data = await response.json();

  const { id_token, refresh_token, access_token, expires_in } =
    access_token_data;
  console.log(&#39;id token: &#39;, id_token);

  ...
});</code></pre>
<p>이렇게 access token을 얻어 유저에게 권한을 부여해 api를 사용가능하게 허락한다.</p>
<br>

<h4 id="⏹️-id-token을-활용해-유저의-정보를-얻어내기">⏹️ Id token을 활용해 유저의 정보를 얻어내기</h4>
<p>하지만, 애플리케이션에서 유저의 정보가 필요한 경우들이 많다.
이를 위해 id token을 사용해 유저의 정보를 얻어내야 한다.</p>
<p>필요한 정보의 범위를 미리 지정했고, 받을 수 있는 정보들은 OAuth에서도 설정했다.</p>
<p>관련한 코드는 다음과 같다.</p>
<pre><code class="language-javascript">  ...
  // /google/callback 라우터 내부이다. 위 코드의 아랫 부분.
  const token_info_reponse = await fetch(
      `${process.env.GOOGLE_TOKEN_INFO_URL}?id_token=${id_token}`
    );
  const { email, name } = await token_info_reponse.json();

  ...
</code></pre>
<h3 id="oauth-활용하기">OAuth 활용하기</h3>
<h4 id="⏹️-유저-정보-가져오고-db연결하기">⏹️ 유저 정보 가져오고 DB연결하기</h4>
<p>이제 유저의 정보를 통해 인증하거나 회원가입하는 동작을 만들어보자.</p>
<p>이를 위해선 어플리케이션이 DB를 통해 유저의 정보를 가지고 있어야 하므로
DB와의 연결이 필요할 것이다.</p>
<p>회원가입에 있어서는 간단하게 DB에 데이터를 생성하는 것으로 처리했다.</p>
<p>MongoDB관련 설정은 매우 간단하다.
IP허용을 해주고 사용할 곳의 URI를 가져와서 .env에 저장해주자.</p>
<p>코드는 아래와 같다.</p>
<pre><code class="language-javascript">/// MONGO DB 연결 및 스키마 설정
mongoose.connect(process.env.MONGO_URI);
const OAuthUserSchema = new mongoose.Schema({
  name: {
    type: String,
    unique: true,
    trim: true,
    require: [true, &#39;Please provide a name&#39;],
    minlength: 3,
    maxlength: 56,
  },
  email: {
    type: String,
    match: [
      /^(([^&lt;&gt;()[\]\\.,;:\s@&quot;]+(\.[^&lt;&gt;()[\]\\.,;:\s@&quot;]+)*)|(&quot;.+&quot;))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
      &#39;Please provide a valid email.&#39;,
    ],
    unique: true,
  },
  password: {
    type: String,
    minlength: 6,
    required: false,
  },
});

const OAuthUser = mongoose.model(&#39;OAuthUser&#39;, OAuthUserSchema);

...

  ...
  // /google/callback 라우터 내부이다. 이전 코드의 아랫 부분.
  let user = await OAuthUser.findOne({ email }).select(&#39;-password&#39;);
  if (!user) {
    user = await OAuthUser.create({ email, name });
  }
  ...</code></pre>
<br>

<h4 id="⏹️-인증-작업-하기">⏹️ 인증 작업 하기</h4>
<p>각종 토큰들을 얻어 냈으니,
jwt와 cookie를 이용해서 클라이언트에게 토큰을 전달하자.</p>
<p>먼저 로그인, 인증에 관련된 동작에 대해 실습해보자.</p>
<p>인증과 관련된 jwt생성 작업과 인증 라우터 코드는 다음과 같다.</p>
<pre><code class="language-javascript">...
// 유저 id로 토큰 만들기.
// 유저 정보를 사용하는 부분이라 스키마에 method를 연결해보았다.
OAuthUserSchema.methods.generateToken = (userId) =&gt; {
  const token = jwt.sign({ id: userId }, process.env.JWT_SECRET, {
    expiresIn: process.env.JWT_LIFETIME,
  });
  return token;
};
...

  ...
  // /google/callback 라우터 내부이다. 이전 코드의 아랫 부분.
  const token = user.generateToken(user._id);
  ...
  // cookie에 jwt토큰 넣어주기
  // 페이지 로딩 등 authentication에 사용
  res.cookie(&#39;idToken&#39;, token, { httpOnly: true }); 
  ...

// 인증 겸 유저 정보 요청을 위한 라우터
  app.get(&#39;/userLoad&#39;, (req, res, next) =&gt; {
    // 
    try {
      const idToken = req.cookies.idToken;
      if (!idToken) throw new Error(&#39;토큰이 없어유&#39;);
      if (jwt.verify(idToken, process.env.JWT_SECRET)) {
        res.json({ info: &#39;유저정보&#39; });
      } else {
        throw Error(&#39;토큰이 만료됨&#39;);
      }
    } catch (e) {
      res.status(400).json({ message: e.message });
    }
  });
</code></pre>
<br>

<h4 id="⏹️-access-token으로-api-처리하기">⏹️ Access token으로 api 처리하기</h4>
<p>이제 access token을 사용해서 권한이 부여된 유저가
api를 사용하는 상황에 대해 실습해보자.</p>
<pre><code class="language-javascript">...
const convertJwt = (token, expires_in) =&gt; {
  return expires_in
    ? jwt.sign({ token }, process.env.JWT_SECRET, {
        expiresIn: &#39;1m&#39;,
      })
    : jwt.sign({ token }, process.env.JWT_SECRET, {
        expiresIn: &#39;2m&#39;,
      });
};
...
  ...
  // /google/callback 라우터 내부이다. 이전 코드 부근.
  const accessToken = convertJwt(access_token, expires_in);
  ...

  // api사용하는 Authorization 사용 -- db나 서버에 저장하기 싫으니까 JWT로.
  res.cookie(&#39;accessToken&#39;, accessToken, {
    httpOnly: true,
    secure: true,
  }); 
  ...

...
// api처리하는 라우터.
app.get(&#39;/some-api&#39;, (req, res, next) =&gt; {
  try {
    const accessToken = req.cookies.accessToken;
    if (!accessToken) throw new Error(&#39;액세스 토큰이 없어용&#39;);
    if (jwt.verify(accessToken, process.env.JWT_SECRET)) {
      res.json({ data: &#39;api요청 결과&#39; });
    } else {
      throw new Error(&#39;유효하지 않은 토큰&#39;);
    }
  } catch (e) {
    res.status(400).json({ message: e.message });
  }
});
...
</code></pre>
<br>

<h4 id="⏹️-refresh-token-사용해보기">⏹️ Refresh token 사용해보기</h4>
<p>위의 부분만 알아도 충분하겠지만
refresh토큰을 사용하지 않아 찝찝했기에 사용방법을 알아내느라 시간을 조금 보냈다.</p>
<p>google oauth2.0에서는 token관련 fetch를 할 때,
refresh token관련된 속성들을 추가하면 된다고 한다.</p>
<p>밑의 코드는
refresh토큰에 대한 api안에 refresh token을 다시 발급받는 과정과
refresh token을 사용해서 access token을 새로 발급하는 과정을 모두 담고 있다.</p>
<p>사실, refresh token이 valid하면서 access token이 invalid한 경우와 
둘 모두가 invalid한 경우를 나눠
전자에는 전달받은 refresh token을 사용해 access token을 새로 발급받는 작업을,
후저에는 refresh token을 google oauth로부터 새로 발급받고, access token도 새로 발급해주는 작업을 해야 함이 맞다.</p>
<p>실습 당시, refresh token을 갱신하는 방법을 알아내며 다른 일들에 치여 있었기에 
맥락과 의도가 이상하지만, 과정을 연습한다는 측면에서만 봐주길 바란다.</p>
<pre><code class="language-javascript">...
  ...
  // /google/callback 라우터 내부이다. 이전 코드 부근.
  // api사용하는 Authorization 사용-- db나 서버에 저장하기 싫으니까 JWT로.
  res.cookie(&#39;refreshToken&#39;, refreshToken, {
    httpOnly: true,
    secure: true,
  }); 


  // callback page 확인을 위해 메시지만 보냈다.
  res.send(&#39;구글 oauth callback url&#39;);
}
...

app.get(&#39;/refresh&#39;, async (req, res, next) =&gt; {
  // api사용에 쓰이는 Access token만료 시 refresh token으로 갱신.
  try {
    const refreshToken = req.cookies.refreshToken;
    console.log(jwt.decode(refreshToken));
    if (!refreshToken) throw new Error(&#39;리프레시 토큰이 없어용&#39;);
    if (jwt.verify(refreshToken, process.env.JWT_SECRET)) {
      // 밑의 작업은 refreshToken을 갱신하는 작업.
      // verify가 false일 때 하는 것이 원래는 맞다.
      const data = {
        client_id: GOOGLE_CLIENT_ID,
        client_secret: GOOGLE_CLIENT_SECRET,
        refresh_token: jwt.decode(refreshToken).token,
        grant_type: &#39;refresh_token&#39;,
      };
      const response = await fetch(&#39;https://oauth2.googleapis.com/token&#39;, {
        method: &#39;POST&#39;,
        body: JSON.stringify(data),
      });

      // access token을 발급받는 동작.
      const access_token_data = await response.json();
      res.cookie(
        &#39;accessToken&#39;,
        convertJwt(
          access_token_data.access_token,
          access_token_data.expires_in
        ),
        {
          httpOnly: true,
          secure: true,
        }
      );
    } else {
      throw new Error(&#39;유효하지 않은 토큰&#39;);
    }
  } catch (e) {
    res.status(400).json({ message: e.message });
  }
});

export default app;
</code></pre>
<h1 id="후기">후기</h1>
<p>이번 회고는 많이 늦었다.
때문에 생생함이 많이 없다는 점이 아쉽다.</p>
<p>그래도 Authenticaion 시리즈는 마무리할 수 있어서 기쁘다.</p>
<p>다음에는 회고 작성을 뒤로 미루게 해준 여러 원인들 중 하나인
&#39;MERN 스택&#39;강좌 공부의 일부를 작성하고자 한다.</p>
<p>Regular하지는 못해도 Continuous한 회고록이 되도록 해보자.</p>
<h1 id="참조">참조</h1>
<p><a href='https://permify.co/post/oauth-20-implementation-nodejs-expressjs/'>OAuth 2.0 implementation in Node.js</a>
<a href='https://myung-ho.tistory.com/107'>[oAuth2] Node Express로 google oAuth2 사용하기[2. 코드 작성]</a>
<a href="https://inpa.tistory.com/entry/ODM-%F0%9F%93%9A-%EB%AA%BD%EA%B5%AC%EC%8A%A4-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%A0%95%EB%A6%AC">[ORM] 📚 Mongoose 사용법 정리 (Node.js - MongoDB)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Authentication 찍먹하기(2) - JWT]]></title>
            <link>https://velog.io/@hys-lee/Authentication-%EC%B0%8D%EB%A8%B9%ED%95%98%EA%B8%B02-JWT</link>
            <guid>https://velog.io/@hys-lee/Authentication-%EC%B0%8D%EB%A8%B9%ED%95%98%EA%B8%B02-JWT</guid>
            <pubDate>Mon, 12 Aug 2024 13:47:48 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하면서">시작하면서...</h2>
<p>json web token 방식은 사실 MERN 스택 강의를 들으며 적용해보았기에,
찍먹하기 위해 따로 코드를 작성하지는 않았다.</p>
<p>그러나, 깔끔한 시리즈 작성을 위해 즉석으로 글을 작성해보기로 했다.</p>
<p>이래도 될 정도로 express환경에서 jwt를 사용하기 너무 간단했다.</p>
<p><em>(물론 간단한 과정만 강의에서 소개해주었기 때문이겠지만)</em></p>
<hr>
<h1 id="실습">실습</h1>
<p>모든 실습은 express를 통해 진행되었다.</p>
<p>간단한 백엔드 작업만으로, 브라우저에서 확인할 수 있을 정도만 코드를 작성했다.</p>
<h2 id="jwt">JWT</h2>
<h3 id="기본-세팅">기본 세팅</h3>
<p>우선, jwt에 필요한 패키지들은 다음과 같다.</p>
<ul>
<li>jsonwebtoken</li>
<li>dotenv</li>
<li>cookie-parser</li>
</ul>
<p>이전 포스트와 같이 세상에서 가장 간단한 백엔드 코드를 작성해준다.</p>
<pre><code class="language-javascript">import express from &#39;express&#39;;
import jwt from &#39;jsonwebtoken&#39;;
import dotenv from &#39;dotenv&#39;;
import cookieParser from &#39;cookie-parser&#39;;

dotenv.config();

const app = express();
app.use(cookieParser()); // 쿠키 작업을 위한 미들웨어

app.get(&#39;/&#39;, (req, res, next) =&gt; {
  console.log(&#39;와우&#39;);
});

app.listen(3000,()=&gt;{
    console.log(&quot;3000번 켜짐&quot;);
});</code></pre>
<h3 id="jwt-적용하기">JWT 적용하기</h3>
<p>우선 서버쪽에 데이터가 와야하는데,
프론트 단을 만들기보다 url query로 데이터를 넘기는 편이 간단하기에
다음과 같이 <code>&#39;/&#39;</code>라우터를 변경한다..</p>
<pre><code class="language-javascript">app.get(&#39;/&#39;, (req, res, next) =&gt; {
  console.log(&#39;와우&#39;);
  const data = req.query.data;
});
</code></pre>
<p>이제 브라우저 URL에 다음과 같이 입력하면 데이터를 백에서 받을 수 있다
<code>http://localhost:3000?data=123</code></p>
<p>이렇게 받아온 data를 jwt을 통해 토큰을 발급해서 cookie로 넘겨주자.</p>
<pre><code class="language-javascript">app.get(&#39;/&#39;, (req, res, next) =&gt; {
  console.log(&#39;와우&#39;);
  const data = req.query.data;
  const token = jwt.sign(data, process.env.JWT_SECRET,{
      expiresIn:&#39;1m&#39; // 1분의 수명
  });
  res.cookie(&#39;token&#39;,token,{httpOnly:true});
});</code></pre>
<blockquote>
<p>❕httpOnly옵션은 js를 통한 공격을 막기 위해 사용한다. </p>
</blockquote>
<br>

<p>이제 브라우저에 들어가 cookie를 살펴보면 token을 확인할 수 있다!
<br>
<em>이번에는 db연결은 생략하겠다. 
mongoose를 통해 findOne, create등을 사용하면 된다</em></p>
<h2 id="참조">참조</h2>
<p><a href="https://www.inflearn.com/course/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%B8%94%EB%A1%9C%EA%B7%B8-%ED%92%80%EC%8A%A4%ED%83%9D">리액트로 나만의 블로그 만들기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Authentication 찍먹하기(1) - Session]]></title>
            <link>https://velog.io/@hys-lee/Authentication-%EC%B0%8D%EB%A8%B9%ED%95%98%EA%B8%B01-Session</link>
            <guid>https://velog.io/@hys-lee/Authentication-%EC%B0%8D%EB%A8%B9%ED%95%98%EA%B8%B01-Session</guid>
            <pubDate>Mon, 12 Aug 2024 13:44:01 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하면서">시작하면서...</h2>
<br>

<p>인턴하면서 인증과정과 token에 대해 무지했기에,
제휴사 페이지 개발을 시작할 때 사용했던 토큰 이름을 관리자 페이지의 것으로 착각했던 경험이 있었다.</p>
<p>이러한 경험을 방지하기 위해 어떤 방법으로 인증이 활용되는지를 알아보고자 한다.
<br>
가장 먼저 도전했던 것은 &#39;<strong>MERN스택</strong>&#39; 체험하기였는데,
덕분에 회원가입이나 로그인 코드를 직접 만들어보고 JWT를 적용해볼 수 있었다.</p>
<p>이 글에서는 이후, 간단하게 다른 인증 방식을 제작했던 경험을 기술하려 한다.</p>
<br>

<hr>
<h1 id="실습">실습</h1>
<p>모든 실습은 express를 통해 진행되었다.</p>
<p>간단한 백엔드 작업만으로, 브라우저에서 확인할 수 있을 정도만 코드를 작성했다.
<br></p>
<h2 id="session">Session</h2>
<h3 id="기본-세팅">기본 세팅</h3>
<p>우선, 세션에 필요한 패키지들을 우선 설치했다.</p>
<ul>
<li>express-session</li>
<li>dotenv</li>
</ul>
<br>
이후 기본적인 라우터 처리를하고 서버를 열어준다.

<pre><code class="language-javascript">import express from &#39;express&#39;;
import session from &#39;express-session&#39;;
import dotenv from &#39;dotenv&#39;;

dotenv.config();

const app = express();

app.get(&#39;/&#39;, (req, res, next) =&gt; {
  console.log(&#39;와우&#39;);
});

app.listen(3000,()=&gt;{
    console.log(&quot;3000번 켜짐&quot;);
});
</code></pre>
<p><del>세상에서 가장 간단한 백엔드 코드</del></p>
<br>

<h3 id="세션-적용하기">세션 적용하기</h3>
<p>이제 세션을 사용해보자.
다음과 같이 애플리케이션 레벨 미들웨어로 적용한다.</p>
<pre><code class="language-javascript">app.use(
  session({
    secret: &#39;wow this is my secret&#39;,
    resave: false,
    saveUninitialized: true,
    cookie: { maxAge: 10 * 1000 },
  })
);</code></pre>
<blockquote>
<p>resave와 saveUninitailized 옵션은 일반적인 경우 위와 같이 처리해준다.</p>
<ul>
<li>resave: 
매 request마다 기존의 session에 변경사항이 없어도 다시 저장하는 옵션.</li>
<li>saveUninitailized:
초기화 되지 않은 session도 저장하기.</li>
</ul>
</blockquote>
<br>

<p>이제 세션에 저장할 수 있는 정보들에 직접 접근해보자.</p>
<p>이전에 만들어둔 <code>&#39;/&#39;</code> 라우터를 다음과 같이 수정한다.</p>
<pre><code class="language-javascript">app.get(&#39;/&#39;, (req, res, next) =&gt; {
  console.log(&#39;세션 정보들: &#39;, req.session);
  if (req.session.num == undefined) {
    req.session.num = 1;
  } else {
    req.session.num += 1;
  }
  res.send(`curNum: ${req.session.num}`);
});</code></pre>
<p>코드 내용은 다음과 같다.</p>
<blockquote>
<p>📜
<em>session에 num이라는 필드가 없다면 1로 초기화를,
있다면 기존값에 1씩 더하고, 
이를 결과를 문자열로 클라이언트에 전달하세요.</em></p>
</blockquote>
<p>브라우저에서 cookie탭을 보면 세션 id값을 확인할 수 있을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/hys-lee/post/72d4c6d6-7198-48c0-bb6d-d78e576bd8e0/image.png" alt=""></p>
<h3 id="db와-연결하기">DB와 연결하기</h3>
<p>실무에서의 경험을 체험해보기 위해 세션 정보들을 DB에 연결하기로 했다.</p>
<p>간단한 MongoDB를 사용해보았다.</p>
<p>필요한 패키지는 다음과 같다.</p>
<ul>
<li>connect-mongodb-session</li>
</ul>
<p>express-session관련 패키지 중 가장 간단하게 사용할 수 있는 패키지로 보였다.</p>
<p>기본 세팅에 대한 코드는 다음과 같다.</p>
<pre><code class="language-javascript">var MongoStore = require(&#39;connect-mongodb-session&#39;)(session);
const store = new MongoStore(
  {
    uri: process.env.MONGO_URI,
    collection: &#39;Session&#39;,
  },
  (error) =&gt; {
    console.error(error);
  }
);
store.on(&#39;error&#39;, (err) =&gt; console.error(err));
</code></pre>
<p>이제 이렇게 만들어진 <code>store</code>를 세션 동작과 연결하면 되는데, 
이는 session 미들웨어에 다음과 같이 간단하게 처리할 수 있다.</p>
<pre><code class="language-javascript">app.use(
  session({
    secret: &#39;wow this is my secret&#39;,
    resave: false,
    saveUninitialized: true,
    cookie: { maxAge: 10 * 1000 },
    store: store, // 바로 이 부분이다.
  })
);

app.get(&#39;/&#39;, (req, res, next) =&gt; {
  ...</code></pre>
<p>정말 간단하게도 이렇게 하면 mongoDB에 쌓이는 num 데이터를 볼 수 있다!</p>
<h2 id="참고-자료">참고 자료</h2>
<p><a href='https://www.youtube.com/watch?v=uoq3Bp7nKUA&list=PLuHgQVnccGMCHjWIDStjaZA2ZR-jwq-WU&index=3'>생활코딩 Web4 - Express Session &amp; Auth</a></p>
]]></description>
        </item>
    </channel>
</rss>