<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>nayoung.log</title>
        <link>https://velog.io/</link>
        <description>문제의 근본적인 원인을 탐구하고 해결하는 것을 좋아하는 프론트엔드 개발자, 진나영입니다!</description>
        <lastBuildDate>Tue, 24 Mar 2026 09:33:26 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>nayoung.log</title>
            <url>https://velog.velcdn.com/images/dev-dino22/profile/cac4c230-9f08-4c3a-bfbf-c1d07666a8ed/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. nayoung.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev-dino22" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[비정상 API 호출 패턴 감지 및 차단을 위한 클라이언트 측 Rate Limiting 레이어 구축]]></title>
            <link>https://velog.io/@dev-dino22/%EB%B9%84%EC%A0%95%EC%83%81-API-%ED%98%B8%EC%B6%9C-%ED%8C%A8%ED%84%B4-%EA%B0%90%EC%A7%80-%EB%B0%8F-%EC%B0%A8%EB%8B%A8%EC%9D%84-%EC%9C%84%ED%95%9C-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%B8%A1-Rate-Limiting-%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@dev-dino22/%EB%B9%84%EC%A0%95%EC%83%81-API-%ED%98%B8%EC%B6%9C-%ED%8C%A8%ED%84%B4-%EA%B0%90%EC%A7%80-%EB%B0%8F-%EC%B0%A8%EB%8B%A8%EC%9D%84-%EC%9C%84%ED%95%9C-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%B8%A1-Rate-Limiting-%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Tue, 24 Mar 2026 09:33:26 GMT</pubDate>
            <description><![CDATA[<h1 id="1-문제-정의">1. 문제 정의</h1>
<h2 id="발단-픽잇의-무한-루프로-인한-api-무한-호출-사건">발단: 픽잇의 무한 루프로 인한 API 무한 호출 사건</h2>
<p>픽잇은 react-router v7 의 비긴급 업데이트와 Suspense 의 <code>startTransition 시 fallback 표시 안함</code> 의 조합으로 인해 백그라운드에서 조용히 API 를 무한 호출한 사건이 있었어요.</p>
<p>(자세한 트러블 슈팅은 <a href="https://medium.com/tecoble/suspense%EC%99%80-%EB%B9%84%EC%BA%90%EC%8B%9C-promise%EA%B0%80-%EC%9C%A0%EB%B0%9C%ED%95%98%EB%8A%94-%EB%AC%B4%ED%95%9C-retry-%EB%A3%A8%ED%94%84-%EC%8B%AC%EC%B8%B5-%EB%B6%84%EC%84%9D%EC%9C%BC%EB%A1%9C-%EB%AF%B8%EB%9E%98%EC%A7%80%ED%96%A5-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%95%99%EC%8A%B5%ED%95%98%EA%B8%B0-09ba799fb944"><strong>여기</strong></a>에서)</p>
<p>해당 사건은 해결이 됐지만, 백그라운드에서 사용자 몰래 조용히 API 가 초에 수십번 요청됐던 버그는 저희의
<strong>서버 비용 폭탄</strong>!으로 이어질 수 있던 심각한 버그였죠.</p>
<p>그래서 이런 비정상적인 서버 요청에 대해 서버 측 뿐만 아니라 프론트엔드에서도 방어 조치가 있으면 좋을 것 같다는 생각이 들었어요.</p>
<hr>
<h2 id="또-언제-문제가-될-수-있을까">또 언제 문제가 될 수 있을까?</h2>
<p>당장은 해당 문제를 Suspense retry 버그로만 겪었지만, 상태 업데이트 리렌더링이 잦은 리액트 프로젝트를 하면서 예기치 못한 무한 루프는 앞으로도 언제든지 벌어질 수 있다고 생각해요. 안티 패턴을 다시 한 번 리마인드 할 겸, 실수하기 쉬운 무한 루프의 두 가지 예시를 나열해볼게요.</p>
<h3 id="☝️-usestate-useeffect-와-같은-훅의-잘못된-사용">☝️ useState, useEffect 와 같은 훅의 잘못된 사용</h3>
<p>deps 의 누락이나 서로 호출하는 등의 실수로 무한 루프라는 끔찍한 버그가 다시 터질 수 있어요. 물론 이는 개발자의 명백한 실수이지만, 코드가 방대해지고 맥락이 길어질 수록 결국 언젠가 실수할 수 있다고 생각해요. 실제로 이런 무한 루프를 겪은 프론트엔드 개발자들의 경험을 푸는 스레드가 있네요.</p>
<p><strong>reference</strong>: <a href="https://www.reddit.com/r/react/comments/1mloqcw/ever_accidentally_create_an_infinite_loop_in_react/">https://www.reddit.com/r/react/comments/1mloqcw/ever_accidentally_create_an_infinite_loop_in_react/</a></p>
<p>게다가 cloudflare 라는 대형 서비스에서조차도 25년에 useEffect의 잘못된 참조로 인한 무한루프를 겪었어요.</p>
<p><strong>reference</strong>: <a href="https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/">https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/</a></p>
<h3 id="✌️-부모-자식-상태-변경-무한루프">✌️ 부모-자식 상태 변경 무한루프</h3>
<p>자식이 부모의 상태를 변경하는 구조가 있다면, useEffect 에 해당 setter를 의존성 배열에 등록하는 순간 무한 루프가 벌어져요. setter로 값을 변경하면, setter자체도 변경되기 때문이에요.</p>
<p>이 또한 애초에 이렇게 작성하면 좋았을 안티 패턴이긴 하겠지만, 이처럼 별 생각 없이 사용했던 코드들이 은밀히 이런 심각한 문제를 발생시킬 수 있어요.</p>
<p>이런 휴먼 에러 말고도 저희가 겪었던 Suspense 의 동작과 라이브러리의 내부적인 업데이트로 인한 문제처럼, 의존성에 의해서도 알지 못하는 새 무한 루프는 언제든 재발할 수 있는 일이라고 판단했어요.</p>
<p>그리고 이러한 무한 루프 속에 API 요청이 섞여있다면 실제 서버 요금 폭탄으로 이어질거에요😱</p>
<hr>
<h2 id="목표-클라이언트의-무한-루프에-따른-api-무한-요청-방어---rate-limit">목표: 클라이언트의 무한 루프에 따른 API 무한 요청 방어 - rate limit</h2>
<p>그래서 핵심 문제 해결의 목표는 위와 같이 정의했어요. </p>
<p><strong>DoS 공격이나 서버의 과부하에 초점을 맞춘 해결 과정은 아닙니다!</strong> 
앞으로 예기치 못한 클라이언트의 무한 루프가 발생했을 때 적절한 대처를 즉시할 수 있도록 고안하는 글이에요.</p>
<hr>
<h2 id="프론트엔드에서-rate-limit-도입의-이점">프론트엔드에서 rate limit 도입의 이점</h2>
<p>제가 구상한 rate limit 방법은 <strong>‘수 초내 수십번의 요청이 갈 경우 비정상적인 API 요청이라고 판단, 에러 토스트 안내 후 에러 페이지로 이동’</strong>이에요.</p>
<p>그런데 백엔드에서도 429 Too Many Request 로 방어해주시기로 했는데요, 서버에서 이미 막아주고 있는데 프론트에서도 막으면 좋을 이유는 뭘까요?</p>
<h3 id="☝️-사용자-경험-개선-및-서버-부담-감소">☝️ 사용자 경험 개선 및 서버 부담 감소</h3>
<p>제가 생각했을 때 가장 좋은 점은, 예상치 못한 버그 상황 시에도 사용자 경험을 해치지 않게해주는 점이에요.</p>
<p>서버에서 429 에러를 주었다는 건 <strong>해당 클라이언트의 요청이 서버가 설정한 시간만큼 블락</strong>되는 것을 의미하는데요, 429 에러가 항상 사용자의 악의적인 테러에만 발생하는 것이 아니라 저희 개발자들의 실수로 벌어진 에러일 경우에도 <strong>사용자들은 해당 시간을 기다려야해요</strong>. 이는 곧 사용자 탈주 및 서비스 불신으로 이어질 수 있어요. 그래서 서버에서 사용자를 차단해버리기 전에 클라이언트에서 최대한 서버에 무리한 요청이 가지 않도록 방어해주는 게 좋다고 판단했어요.</p>
<h3 id="✌️-api-요청-제한에-유연한-대응-가능">✌️ API 요청 제한에 유연한 대응 가능</h3>
<p>서버에서 만약 같은 사용자가 같은 API 요청에 제한을 10분에 100회할 경우에 블락을 하기로 했다고 가정해볼까요? 그럼 서버에서 결정한 정책은 개인의 무리한 사용에 대한 제재가 아닌 서버 부담 완화에 대한 목적일 수 있어요. 또한 서버는 이미 99회 불필요한 요청을 받고 나서야 블락을 하게 돼요.</p>
<p>그렇다고 10분에 100회 라는 기준을 마음대로 줄일 순 없어요. 보통 사용자의 IP 를 기반으로 블락을 하기 때문에 같은 공용 네트워크를 사용 중인 사용자들은 같은 사용자의 요청으로 카운트되거든요.</p>
<p>하지만 프론트에서 rate limit 를 도입하게 된다면 주 목적은 프론트엔드에서의 무한 루프로 인한 수 초 내 수십번의 요청이 가는 것에 대한 차단이므로 서비스 정책 결정에 영향을 주지 않고 개인에 대해 유연하게, 더 엄격한 기준으로 방어할 수 있어요.</p>
<hr>
<h2 id="프론트엔드에서-rate-limit-도입의-단점">프론트엔드에서 rate limit 도입의 단점</h2>
<h3 id="☝️-반복-요청을-허용해야하는-예외-상황-→-관리-포인트-증가">☝️ 반복 요청을 허용해야하는 예외 상황 → 관리 포인트 증가</h3>
<p>모든 API 요청에 일괄적인 기준을 적용할 경우, 정상적인 서비스 이용 시나리오에서도 차단이 발생할 수 있는 오탐 가능성이 존재해요.</p>
<ul>
<li><strong>대량의 데이터 초기화/동기화:</strong> 대시보드 진입 시 여러 개의 위젯 데이터를 동시에 호출하거나, 사용자의 액션 한 번에 수십 개의 독립적인 리소스를 패치해야 하는 경우.</li>
<li><strong>실시간 성격의 폴링(Polling):</strong> 특정 작업의 완료 상태를 확인하기 위해 짧은 주기로 반복 요청을 보내는 로직이 포함된 경우.</li>
<li><strong>사용자의 의도적인 광클:</strong> 검색 필터를 빠르게 여러 번 변경하거나, 페이지네이션을 극도로 빠르게 넘기는 등 예측하기 어려운 사용자 행동 패턴.</li>
</ul>
<p>따라서 <strong>비정상적인 동작이라고 의심할 수 있는 충분한 기준</strong>(현재로는 5초 내 20번 이상의 같은 API 요청)을 세우고, 추후 기능이 확장될 때에도 우리 서비스는 client rate limit 가 동작하고 있음을 의식하고 있어야해요.</p>
<p>결과적으로, 이러한 특수 사례들을 &#39;예외 처리&#39;하기 위해 API별로 rate limit 옵션을 세분화해야 할 수 있으며, 이는 프로젝트 규모가 커질수록 유지보수해야 할 <strong>화이트리스트 관리 포인트</strong>가 늘어남을 의미합니다.</p>
<h3 id="✌️-보안책은-아니다">✌️ 보안책은 아니다</h3>
<p>프론트엔드의 이러한 방어코드는 개발자 도구로 쉽게 우회할 수 있어요. 따라서 이 코드 반영은 DoS 공격 등에 대한 방어코드는 아닌 점을 명심해야해요. </p>
<hr>
<h2 id="결단">결단</h2>
<p>그럼에도 불구하고, 픽잇의 현재 서비스 규모와 데이터 처리 특성을 고려했을 때 Client Rate Limit 도입의 실익이 더 크다고 판단했어요.</p>
<p>특히 &#39;5초 내 20회&#39;라는 임계치는 다음의 실측 지표를 바탕으로 설정되었습니다:</p>
<ul>
<li><strong>정상 시나리오:</strong> 페이지 진입 시 동일 API 호출은 평균 1회이며, 사용자 인터랙션이 집중되는 상황(빠른 클릭 등)에서도 초당 동일 API 요청은 3회를 넘지 않음을 확인했습니다. (또한 빠른 클릭 등에 대한 대응은 쓰로틀링 등의 대응이 더 적절하다고 생각해요.)</li>
<li><strong>이상 시나리오:</strong> 실제 무한 루프 재현 시, 초당 약 20회 이상의 폭발적인 API 요청이 관찰되었습니다.</li>
</ul>
<p>따라서 정상적인 사용 범주에 충분한 간격을 두면서도, 비정상적인 루프를 즉각 감지할 수 있는 최적의 지점으로 5초 내 20회라는 기준을 도출했습니다. 추후 대량 데이터 처리가 필요한 기능 확장 시에는 API별로 임계치를 세분화하여 대응할 계획이에요.</p>
<hr>
<h1 id="2-행동">2. 행동</h1>
<p>위의 문제 정의에 따라 저는 사용자의 API 요청에 대해 제한을 두도록 결정했어요. 구체화를 해볼게요.</p>
<hr>
<h2 id="문제-해결-방법">문제 해결 방법</h2>
<p>우선 비정상적인 API 요청이라는 기준은 ‘수 초 내 수십번의 요청이 갈 경우’ 라고 세워두겠습니다.</p>
<p><strong>일단 어떤 이유에서 API가 비정상적으로 빠르게 반복 요청된 것인지 확신할 수 없으므로 설정한 {Retry After}초 까지 대기 후, 그럼에도 같은 문제가 N 회 이상 반복되면 해당 페이지에서 계속 비정상적인 상황이 나아지지 않을 것이라고 판단하고 에러 페이지로 보내는 방법</strong>을 생각했어요.</p>
<p>해당 에러 페이지에는 저희 픽잇에 에러 보고서를 보낼 수 있는 Sentry 기능과 메인화면으로 돌아가기 버튼을 제공할거에요.</p>
<p>실제로 구글의 SRE(Site Reliability Engineering, 사이트 신뢰성 공학. 구글에서 제안한 코딩과 자동화 기술로 시스템의 신뢰성을 높이는 철학과 실무를 정리한 문서) 의 과부하 처리(Handling Overload) 챕터에서도 이러한 클라이언트의 제한 방법에 대해 다루고 있어요.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/ae5434bd-b0e0-4fbf-83ae-e74a8f69ccb9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/6c62759d-ab2d-4694-92fe-2b198f08be98/image.png" alt=""></p>
<p><strong>reference:</strong> <a href="https://sre.google/sre-book/handling-overload/">https://sre.google/sre-book/handling-overload/</a></p>
<p>위의 글에서 클라이언트는 서버의 지속된 작업 과부하 시 3회 재시도 후 재시도를 그만 두고 호출자에게 알리고 있어요.</p>
<p>(백엔드에서 CPU 실제 리소스 사용량에 따른 고객별 오류 응답과 그 후 클라이언트가 적응형 스로틀링으로 부하를 조절하는 아이디어 등을 다루고 있어요. 최적화를 위한 계산식 등 꽤나 흥미로운 내용이니 읽어봐도 좋을 것 같아요ㅎㅎ)</p>
<p>요청이 이미 3번 실패한 경우 다시 시도해도 해결될 가능성이 낮다는 판단에 따른 것인데요, 이 판단에 공감이 되어 Retry After 초 간격으로 N회 재시도 후에도 같은 현상(비정상적으로 반복되는 API 요청)이라면 해당 페이지에서 계속 비정상적인 API 요청이 일어나 서버에 부담이 갈 것이라고 판단, 사용자에게 피드백 메세지 후 오류 페이지로 이동시키는 결정입니다.</p>
<hr>
<h2 id="정책">정책</h2>
<ul>
<li><strong>5초에 20회 이상 동일한 API 요청이 발생할 경우</strong> 해당 API 요청을 중단하고 사용자 토스트 안내</li>
<li>해당 속성은 빌트인으로 on/off 가능</li>
<li>주된 무한 루프 대상 API 인 GET 메서드에 한 해 전역 적용(이미지 대량 업로드 등 API 가 많이 요청될 수 있는 이외 메서드들은 기본 설정 off</li>
</ul>
<hr>
<h2 id="의사-결정-사항">의사 결정 사항</h2>
<p><strong>apiClient 에서 rate limit 가 발생할 시 window.location.replace() 를 이용해 <code>error/too-many-requests</code> 페이지로 직접 이동</strong></p>
<ul>
<li>무한 루프가 의심되는 상황임을 가정하고, navigate 이동이 아닌 window.location.replace 의 새로고침+url 이동을 통해 SPA 의 상태 초기화 및 안정적인 페이지 이동</li>
</ul>
<p><strong>rate limit 에 대한 관리는 전역적으로 하나만 하면되므로 싱글톤처럼 작성 (하나의 store)</strong></p>
<h3 id="고려한-다른-대안은-없나요---쓰로틀링디바운스-요청-큐잉">고려한 다른 대안은 없나요? - 쓰로틀링/디바운스, 요청 큐잉</h3>
<p><strong>☝️ 쓰로틀링(Throttling) / 디바운스(Debounce)</strong></p>
<ul>
<li><strong>한계:</strong> 특정 UI 이벤트(버튼 클릭, 검색 입력)에는 효과적이지만, 프론트엔드의 비즈니스 로직이나 라이브러리 간의 의존성 꼬임으로 발생하는 &#39;코드 레벨의 무한 루프&#39;를 근본적으로 차단하기엔 부족해요.</li>
<li><strong>결정:</strong> 쓰로틀링은 &#39;정상적인 사용자의 과도한 액션&#39;을 제어하는 용도로 개별 컴포넌트에서 유지하고, 이번 <code>Rate Limit</code>은 &#39;비정상적인 시스템 동작&#39;을 감지하는 시스템 전체의 안전장치(Fail-safe)로 이원화하여 운영하기로 했습니다.</li>
</ul>
<p><strong>✌️ 요청 큐잉(Request Queueing) 및 중복 제거</strong></p>
<p>아래는 요청 큐잉을 관리하는 방향으로 예방한 개발자 분이 작성한 아티클이에요.</p>
<p><strong>reference</strong>: <a href="https://kasterra.github.io/preventing-useEffect-infinite-loop/">https://kasterra.github.io/preventing-useEffect-infinite-loop/</a></p>
<ul>
<li><strong>아이디어:</strong> 동일한 API 요청이 짧은 시간에 몰릴 경우 큐(Queue)에 쌓아두고 하나만 실행하거나 순차적으로 처리하는 방식이에요.</li>
<li><strong>픽잇에서의 판단:</strong> 픽잇은 현재 복잡한 데이터 동기화보다는 실시간 응답성이 중요한 서비스입니다. 만약 무한 루프 상황에서 요청을 큐에 쌓기만 한다면, 브라우저의 메모리 점유율이 급격히 상승하여 결국 탭이 먹통이 되는 현상을 막을 수 없습니다.</li>
<li><strong>결정:</strong> 요청을 &#39;지연&#39;시키는 것보다, 비정상 상황임을 감지하는 즉시 호출을 중단하고 상태를 초기화하는 것이 시스템 안정성과 비용 방어 측면에서 가장 확실한 해법이라고 판단했습니다.</li>
</ul>
<hr>
<h2 id="에러-모니터링">에러 모니터링</h2>
<p>이제 백엔드의 429 인지 무한루프인지에 따라 상황을 기록해 Report 를 Sentry 로 전송해요.</p>
<p>사용자가 정확히 어떤 페이지에서 어떤 에러를 겪었는지, 어떤 API 의 문제였는지 등을 보고 받아 빠른 버그 추적이 가능하도록 마련했어요.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/58fd3fa0-e72d-4ebc-9668-fede4ae9fdfd/image.png" alt=""></p>
<h3 id="보고-항목"><strong>보고 항목</strong></h3>
<p><strong>[공통]</strong></p>
<p><strong><code>rate_limit_source</code></strong> : rate limit이 <strong>어디서</strong> 걸렸는지 구분. <code>client</code> = 클라이언트(우리 코드)에서 막음, <code>server</code> = 서버가 429로 막음.</p>
<p><strong><code>page</code></strong> : <strong>어느 페이지</strong>에서 발생했는지. <code>window.location.pathname + window.location.search</code> </p>
<p><strong><code>api_method</code></strong>: <strong>어떤 HTTP 메서드</strong>로 요청했는지</p>
<p> <strong><code>api_endpoint</code></strong> : <strong>어떤 API 경로</strong>로 요청했는지. 예: <code>/v1/rooms/123</code>, <code>/v1/rooms/123/menus</code></p>
<p><strong>[클라이언트 무한루프 의심 시]</strong></p>
<ul>
<li><strong><code>rate_limit_timestamps</code></strong>: 해당 <code>api_method</code> + <code>api_endpoint</code> 조합으로 <strong>언제 몇 번</strong> 호출됐는지. <code>number[]</code> — 각 요청 시점의 <code>Date.now()</code>(ms) 배열. 윈도우 내 호출 이력 스냅샷.</li>
<li><strong><code>rate_limit_request_count</code></strong> : 위 타임스탬프 배열의 <strong>길이</strong> = 해당 API로 <strong>윈도우 내에 기록된 요청 횟수</strong>. 이 값이 한도(예: 20)에 도달해서 막힌 상황.</li>
</ul>
<p><strong>[서버 429 시]</strong></p>
<p><strong><code>server_message</code></strong> : 서버가 429 응답 body에 넣어 준 <strong>메시지</strong> (있는 경우만)</p>
<hr>
<h2 id="백엔드의-429-error-대응">백엔드의 429 Error 대응</h2>
<p>백엔드에서 Too Many Request 에 대한 대응을 처리해주셨는데요, 이에 따라 저희의 <strong><code>getErrorMessageByCode</code></strong> 와 <strong><code>ERROR_CODE</code></strong> 에러 메세지 객체에도 429 에러 상황을 추가해주었습니다.</p>
<hr>
<h1 id="3-결과">3. 결과</h1>
<h2 id="도입-전-후-성능-비교">도입 전 후 성능 비교</h2>
<h3 id="☝️-성능-벤치마크">☝️ <strong>성능 벤치마크</strong></h3>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/a7cd1e64-e08f-4443-8f42-e2eee54dcc3a/image.png" alt=""></p>
<p>fetch 에 mock 을 적용해 rate limit 가 적용된 apiClient와 적용되지 않은 baseline 의 Rate Limit 로직 추가로 인한 오버헤드는 요청당 약 <strong>0.0019ms</strong>로 확인했어요. 이는 실제 네트워크 환경의 평균 응답 속도(약 50ms) 대비 <strong>0.004% 수준</strong>의 연산량으로, 사용자 체감 성능에 미치는 영향은 사실상 제로에 가까워요.</p>
<p>오히려 1000회 연속 호출 시에도 총 소요 시간이 2ms 내외로 관리되는 점을 보아, 비정상적인 상황(무한 루프 등) 발생 시 시스템을 안정적으로 방어할 수 있는 저비용·고효율 안전장치라고 생각해요.</p>
<p>또한 단순 연산 오버헤드 확인을 넘어, <strong>실제 무한 루프 상황 재현 테스트</strong>를 진행했는데요, 초당 수십 번의 요청이 발생하는 환경에서, 로직은 임계치 도달 즉시 비정상 패턴을 감지하고 차단에 성공했습니다. 즉, 서버에 유의미한 부하가 가기 전 프론트엔드 최전방에서 방어 기능을 수행함을 검증했어요.</p>
<h3 id="✌️-cpu-점유율">✌️ <strong>CPU 점유율</strong></h3>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/d5cc8174-1a47-4cd5-981a-7e1ecd247f88/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/67372f18-5066-4d67-a6c7-86ae7a591447/image.png" alt=""></p>
<p>Bottom-Up 프로파일링 결과, 전체 API 요청 프로세스에서 Rate Limit 핵심 로직(<code>appendTimestamp</code>)이 차지하는 <strong>CPU 점유율은 단 5.4%(0.2ms)에 불과</strong>해요. </p>
<p>이는 브라우저의 기본 <code>fetch</code> 동작(56.7%) 대비 약 10분의 1 수준으로, 시스템 자원을 거의 소모하지 않는 안전한 설계임을 확인했어요.</p>
<hr>
<h2 id="결론">결론</h2>
<h3 id="1-정량적-오버헤드-검증">1. 정량적 오버헤드 검증</h3>
<ul>
<li>Rate Limit 로직 도입 시 발생하는 연산 오버헤드를 요청당 약 0.0019ms(네트워크 지연 시간 대비 0.004% 수준)로 억제하여, 서비스 성능 저하 없이 시스템 안정성 확보</li>
</ul>
<h3 id="2-비즈니스-비용-보호">2. 비즈니스 비용 보호</h3>
<ul>
<li>클라이언트 단 선제적 차단 로직(5초 내 20회 초과 시)을 통해, 프론트엔드 버그로 인한 불필요한 API 호출을 차단하여 클라우드 <strong>인프라 비용 낭비 리스크 방어</strong></li>
</ul>
<h3 id="3-사용자-경험-유지">3. 사용자 경험 유지</h3>
<ul>
<li>서버 측 IP 차단(429 Too Many Requests) 전 단계에서 비정상 요청을 감지하고 전용 에러 페이지 및 피드백 루프를 제공</li>
<li>최악의 상황에서도 사용자 이탈을 방지하고 서비스 신뢰도 유지</li>
</ul>
<h3 id="4-모니터링-및-운영-체계-구축">4. 모니터링 및 운영 체계 구축</h3>
<ul>
<li>Sentry 커스텀 태그 및 User Feedback 연동을 통해 무한 루프 발생 시 실시간 상황 스냅샷 수집 체계 구축</li>
<li>장애 대응 시간(MTTR) 단축</li>
</ul>
<hr>
<h1 id="4-남은-기술-부채">4. 남은 기술 부채</h1>
<p>프론트엔드에서 최악이지만 흔하다면 흔할 수 있는 무한 루프에 대한 대응을 해보았는데요,</p>
<p>적절한 방법을 찾기위해 공부하다보니 , 특히 구글 SRE 에서 고안한 처리 과부하 방법이 꽤 인상적이었어요. 클라이언트와 백엔드가 협력해서 과부하 상황을 원활하게 처리하는 아이디어가 꽤 흥미로웠고, 실제 저희 서비스가 대규모 트래픽을 갖게된다면 꼭 도입하면 좋은 방식이라고 생각했어요. 또, 데이터센터의 대표적인 로드 밸런싱 기법 중 하나인 클라이언트 쓰로틀링도 전역적인 API 요청에 도입할지 팀과 함께 논의해보고 싶네요.</p>
<p>이 방어 코드가 도입됨으로써 이제 우리 픽잇 팀은 무한 루프로 인한 &#39;비용 폭탄&#39; 걱정 없이 더 과감하게 리팩터링하고 기능을 확장할 수 있게 되었어요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당근/회고] 인턴 출근 전 준비]]></title>
            <link>https://velog.io/@dev-dino22/%EB%8B%B9%EA%B7%BC%ED%9A%8C%EA%B3%A0-%EC%9D%B8%ED%84%B4-%EC%B6%9C%EA%B7%BC-%EC%A0%84</link>
            <guid>https://velog.io/@dev-dino22/%EB%8B%B9%EA%B7%BC%ED%9A%8C%EA%B3%A0-%EC%9D%B8%ED%84%B4-%EC%B6%9C%EA%B7%BC-%EC%A0%84</guid>
            <pubDate>Tue, 24 Mar 2026 08:04:17 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<h1 id="합격">합격</h1>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/0af03673-d271-49a7-b1b7-fd7861eec6f4/image.png" alt="당근 인턴 합격 사진"></p>
<p>당근 로컬비즈니스 팀에 인턴으로 합류하게 되었다.
면접 경험은 너무 좋았지만 내 장황한 설명이나 라이브 코테에서의 실수 등 아쉬운 부분이 있었기 때문에 기대를 접고 다시 취준 활동을 하려고 했는데 다음 날 빠르게 최종 합격 결과를 받을 수 있었다.</p>
<p>너무 기대를 안했던 탓인지 메일을 확인하고 멍해져서 글자를 잘못 보고있는 것은 아닌가 엄마에게 다시 읽어달라고 했다. 그러고도 실감이 안나서 한동안 아무 감정이 들지 않았다.</p>
<p>그러다 입사 준비 링크를 받고나서야 실감이 된 건지 갑자기 엄청 벅차오르고 기분이 좋았다. 집 안을 춤 추고 돌아다니며 친한 친구들에게 소식을 알리고 약속을 마구 잡았다. 원래 이번 주에 지원해보려던 다른 공고는 고민 끝에 지원하지 않기로 했다. 3개월의 체험형 인턴이지만 당근의 그로스 팀에서 기대하고있는 경험이 많았기 때문이다.</p>
<p>이러한 결정으로 취준 시작 후 오랜만에 아무 것도 안해도 마음 편히 놀고 푹 쉬는 일주일을 보냈고 이제 출근 전까지 무엇을 준비하면 좋을지 고민이 시작되었다.</p>
<hr>
<h1 id="준비">준비</h1>
<p>내가 합류해서 하게될 일은 아직 서비스의 초기 단계에 있는 &#39;동네걷기&#39; 라는 혜택 미션 서비스의 그로스 액션을 기획하고 실행하는 일이다.</p>
<p>MAU 2000만이라는 국민 서비스 당근 앱 내에서 운영하는 그로스 서비스라니, 프로덕트가 빠르게 커가는 과정을 경험할 생각에 설렌다.</p>
<h2 id="내-필요는-무엇일까">내 필요는 무엇일까?</h2>
<p>먼저 당근에서 &#39;동네걷기&#39; 그로스팀 인턴에게 요구하는 역량을 생각해보면 인턴을 뽑은 이유를 알 수 있을 것 같아, 캡처해두었던 내가 지원한 JD 를 다시 읽어보고 면접 때의 컬쳐핏 질문들을 복기해보았다.</p>
<p>중요해보이는 핵심 역량을 추려보니, 
<strong>[ 빠른 속도, 선택지 정리 및 공유, 방향 제안 ]</strong> 이었다.</p>
<p>JD에도 써있듯 서비스를 키워가는 단계이기 때문에 속도, 일정, 리스크를 고려해 빠르게 변화하는 요구사항에 맞춰 빠르게 선택지를 제안하고 실행해야한다.</p>
<p>그래서 높은 생산성과 프로덕션 감각으로 태스크를 수행해내는 게 내 필요일 것이라고 예상해보고 있다.</p>
<p>그렇다면 여기서 제일 중요한 것은 <strong>시간 관리</strong> 일 것이다. 내가 어떤 Feature 를 구현하고 어떤 이슈를 Fix 하든 언제까지 할 수 있다는 시간의 감이 있어야하고 일정을 공유할 수 있어야 한다.</p>
<h3 id="나만의-업무-레포트-양식-준비">나만의 업무 레포트 양식 준비</h3>
<p>아직 처음이니 분명 태스크의 시간을 예측하기도 시간 분배에도 어려움을 겪을 것이 예상되었다. 그래서 일단 간단하게 작성할 수 있는 노션 업무 레포트 DB 를 만들고 시간 측정 앱을 설치해두었다. </p>
<p>매일 일과 시작 전 할 일을 정리하고 각 태스크에 예상 소요 시간을 적어 시간을 분배한 뒤 실제로 걸린 시간을 함께 기록하면 자동으로 오차(분,%)가 계산된다. 주로 어떤 태스크에서 병목과 오차가 있었는지 한눈에 보기 위해 만들어두었다.</p>
<p>업무 일지 양식도 템플릿을 구해 만들어두었는데, 자세한 양식은 온보딩하면서 커스텀해갈 생각이다.</p>
<h3 id="당근의-기술-아티클-읽기">당근의 기술 아티클 읽기</h3>
<p>일단 당근 프론트엔드 인턴으로서 작성해주신 다양한 회고글들을 염탐해보며 어떻게 생산성을 끌어올렸는지 참고도 해보고 있다. 당근의 기술 아티클들도 읽어보고 있는데, 읽을 수록 빠른 생산성의 템포가 간접적으로 느껴져온다.</p>
<p>또, <strong>요즘 당근 AI 개발</strong>이라는 최근 당근 팀에서 출간한 도서를 읽었다. 이 도서는 비개발직군이 바이브코딩으로 생산성을 끌어올린 사례부터, 프로덕트 관점에서의 AI 도입기, 개발자의 AI 에이전트 플랫폼 개발까지 당근이 어떻게 AI 라는 파도 위에서 서핑을 하고 가치를 창출했는지 소개한다.</p>
<p>막연한 상상보다 더 적극적으로 적용되는 자동화가 신기하면서도 내가 어떤 기여를 할 수 있을지 걱정이 많이 되었다...실무에서 AI로 업무의 자동화를 한다는 건 단순히 코드 에이전트 모델을 md 파일로 가르치고 통제하는 것을 뜻하는 게 아니었다. 프로덕트의 더 다양한 컨텍스트를 읽고 요구사항을 정의하고 분석해 비효율과 불가능을 효율과 가능으로 바꾸는 것...</p>
<p>일단 지금은 책에서 잠깐씩 나온 개념들과 프로그램들을 공부하고 직접 써보면서 AI 라는 거대한 바다에 발을 조금씩 담궈보고 있다.</p>
<p>위에서 만든 노션 업무 일지 DB에 노션 MCP 와 n8n 을 이용해 오차 데이터에 따른 일간/주간 업무 피드백 AI 도 붙여볼까싶다.</p>
<h3 id="동네걷기-실사용자가-되어보기">동네걷기 실사용자가 되어보기</h3>
<table>
<thead>
<tr>
<th align="left"><img src="https://velog.velcdn.com/images/dev-dino22/post/35e630e8-d20a-4bcb-b304-fc4edb22b19e/image.PNG" alt=""></th>
<th align="center"><img src="https://velog.velcdn.com/images/dev-dino22/post/e7db18c2-7c22-4747-950a-32fda4de280f/image.jpg" alt=""></th>
</tr>
</thead>
</table>
<p>프로덕트 관점에서 생각하는 힘을 기르기 위해 일주일 간 실사용자가 되어보기로 했다.</p>
<p>매일 랑이와의 산책 시간에 주로 동네걷기 서비스를 이용했다. 근처 보물상자들을 열어 돈을 버는 게 포켓O고 같기도하고 머니도 쏠쏠하니 꽤 재미있다.</p>
<p>써보면서 사용자로서 아쉬운 점이나 바라는 기능 아이디어는 브레인스토밍처럼 가볍게 노션에 적어보았다.</p>
<hr>
<h1 id="목표">목표</h1>
<p>3개월 뒤 나는 다시 취준생으로 돌아온다. 다시 3개월을 몰입하고 불태워 확실한 무언가를 얻어와야한다.</p>
<h2 id="나는-무엇을-얻고-싶은가">나는 무엇을 얻고 싶은가?</h2>
<p><strong>압도적인 성장 경험</strong>이다.</p>
<p>나는 우테코를 시작하기 전엔 리액트를 쓰면서도 상태라는 게 뭔지도 잘 몰랐다. 디자이너로 활동하면서 요구사항을 어떻게든 돌아가게 하는 코드를 AI 로 될 때까지 만들었을 뿐이었다. 하지만 우테코에서의 레벨1<del>2 에 열심히 미션을 하고 리뷰를 받고 스터디를 하는 등 밀도있는 시간을 보내고, 이론을 채우는 것을 넘어 이유있는 코드를 작성하며 협업할 수 있게 됐다. 레벨 1</del>2 도 방학을 제외하면 3개월이 좀 넘는 짧은 시간이었다.</p>
<p>당근 인턴에서의 3개월도 이런 밀도 높은 성장 경험을 기대하고 있다. 이제 학습자가 아닌 서비스에 직접 기여하는 엔지니어가 되는 성장 경험을 쌓고 싶다.</p>
<p>내가 기대하는 실무자로서의 압도적인 성장 경험이란 당근의 그로스 액션을 경험하며 빠르게 프로덕션을 키우고 운영하는 것을 스펀지처럼 쏙쏙 빨아들여 체화하게 되는 것이다.</p>
<h3 id="그리고">그리고...</h3>
<p>개발자로서 나의 확신을 얻고 싶다.
내가 리액트 코드를 처음 만지게 된 시작은 프로덕트의 중요 기능을 기술 때문에 포기할 수 없었던 책임감이었다.
그러다 개발에 흥미가 생겨 취미로 공부하게 됐고 우테코에 들어가게 되면서는 그저 재미 때문에 학구열을 불태웠다.</p>
<p>이제는 디자이너가 아닌 프론트엔드 개발자가 되어 다시 프로덕트에서 작업자로 일하게 된다. 기술적인 재미가 아닌 유저 가치를 좇으며 서비스 경험을 만들어야한다.</p>
<p>학생 신분을 벗어나 취준생이 된 지금 내가 엔지니어로서 어떤 가치를 만들 수 있는 사람인지, 나는 그에 충실한 사람인지 스스로 증명할 수 있는 시간이 되었으면 좋겠다. 꼭 그런 시간을 보내야겠다.</p>
<p>이를 위해 매일 업무 레포트를 기록하고 매주 한 주 회고글을 작성하고자 한다.</p>
<p>3개월 뒤 다시 취준생으로 돌아왔을 땐 내가 어떤 가치를 창출할 수 있는 개발자인지 스스로 확신할 수 있도록 기록을 꼼꼼히 해두고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS/React] 비동기와 동시성에 대한 고찰 - Intro]]></title>
            <link>https://velog.io/@dev-dino22/JSReact-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%99%80-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0-Intro</link>
            <guid>https://velog.io/@dev-dino22/JSReact-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%99%80-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0-Intro</guid>
            <pubDate>Sun, 23 Nov 2025 08:51:55 GMT</pubDate>
            <description><![CDATA[<h3 id="시작하며">시작하며…</h3>
<p>웹은 지속적으로 진화해왔다. 과거에는 PHP와 같이 서버에서 완성된 HTML을 동적으로 생성해 클라이언트에 전달하는 방식의 서버 템플릿 언어가 주류였다.</p>
<p>그러나 SNS와 같은 웹 애플리케이션에서 사용자와의 다양한 상호작용이 늘어나고, 즉각적인 데이터 요청과 응답이 빈번해지면서 클라이언트 측에서 렌더링을 수행하는 React와 같은 클라이언트 사이드 렌더링(CSR) 방식이 주목받기 시작했다. CSR은 동적이고 풍부한 사용자 경험을 제공하지만, 초기 로딩 속도 지연, SEO 최적화 한계라는 문제점들이 존재했다.</p>
<p>이러한 한계를 극복하기 위해 SSR과 CSR의 장점을 결합한 하이브리드 렌더링 방식이 등장했고, 대표적으로 Next.js가 부상했다. 그러나 Next.js 역시 클라이언트에서 동기적이고 블로킹되는 하이드레이션 과정 문제, 그리고 서버에서 완성된 HTML을 내려주는 동적 구조의 한계를 완전히 해소하지는 못했다.</p>
<p>이 문제들을 한 단계 더 진화시킨 것이 React Server Components(RSC)다. RSC는 Node.js와 같은 서버 환경에서 비동기적으로 작성되고 실행되는 렌더링 모델과, &quot;동시성&quot; 개념을 도입한 React 클라이언트 환경의 렌더링을 접목해 보다 향상된 사용자 경험을 제공할 토대를 마련했다.</p>
<p>이처럼 웹은 사용자에게 더욱 편리해지는 동시에 개발자에게는 점점 더 복잡하지만 강력한 표현과 제어를 가능하게 하는 흥미로운 궤적을 그리고 있다.</p>
<p>이번 글에서 중점적으로 다룰 내용은 ‘비동기, 동시성’ 에 대한 탐구이다.</p>
<p>웹의 발전 과정을 보다시피, 프론트엔드의 개발 생태계는 사용자에게 ‘보다 더 나은 경험’을 제공하기 위해 끝없이 변화하고 진화하고 있다. 그리고 지금 변화의 핵심은 싱글 스레드 언어인 자바스크립트로 비동기를 구현하고 동시성을 보장하는 것에 있다고 생각한다.</p>
<p>잠시 동시성 모드를 발표했던 리액트의 공식 아티클 문서를 보자.</p>
<hr>
<aside>
📢

<h2 id="what-is-concurrent-react">What is Concurrent React?</h2>
<p>The most important addition in React 18 is something we hope you never have to think about: concurrency.</p>
<p>…
React uses sophisticated techniques in its internal implementation, like priority queues and multiple buffering. But you won’t see those concepts anywhere in our public APIs.</p>
<p>When we design APIs, we try to hide implementation details from developers. As a React developer, you focus on <em>what</em> you want the user experience to look like, and React handles <em>how</em> to deliver that experience. So we don’t expect React developers to know how concurrency works under the hood.</p>
</aside>

<hr>
<p>리액트는 동시성 모드가 사용자 경험을 크게 향상시키는 마법 같은 API 로 소개하면서도 이게 “어떻게” 동작하는지는 리액트 개발자들이 알 필요 없다고 설명한다. 공감하는 바이다.</p>
<p>그래서 이번 글에서는 내부적인 구현 방법보다는 개념적인 설명과 이 것이 실제로 SSR이든 CSR이든 리액트 개발에 무슨 의미가 있는지를 소개하고자한다.</p>
<h2 id="비동기-렌더링의-의의">비동기 렌더링의 의의</h2>
<p>18버전 이전의 리액트는 모든 렌더링 작업을 한 번에 처리하는 동기 방식이어서, 렌더링 중 긴 작업이 발생하면 UI가 멈추고 입력에 즉각 반응하지 못하는 문제가 있었다. 또한, 우선순위 구분 없이 업데이트를 순차적으로 처리해 중요한 사용자 인터랙션이 지연되는 현상이 빈번했다. 이로 인해 대규모 UI 상태 변화나 복잡한 상호작용에서 부드러운 사용자 경험 제공에 한계가 있었다.</p>
<p>더 와닿도록 사용자 input 입력 시마다 DOM 에 셀이 4000개씩 늘어나는 코드로 
<code>사용자 이벤트와 같은 긴급한 UI 업데이트가 앞선 UI 처리에 의해 밀린다</code> 라는 상황을 재현해보겠다.</p>
<table>
<thead>
<tr>
<th>기존의 동기적이었던 UI 업데이트</th>
<th>비동기적인 UI 업데이트</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/dev-dino22/post/213214dd-73cf-4bf8-998c-be3a3e938218/image.gif" alt=""></td>
<td><img src="https://velog.velcdn.com/images/dev-dino22/post/059ba0fd-a717-4fb4-bef6-37aa63732a35/image.gif" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>기존의 동기적으로 처리되었던 렌더링의 경우 먼저 예약된 UI 업데이트를 모두 처리한 뒤에야 다음 이벤트 UI 업데이트를 처리할 수 있었다. 그로 인해 위에서 보다시피 사용자 타이핑에 따른 input UI 업데이트에 렉이 걸리는 현상을 확인할 수 있다. 보다 실무적인 상황을 생각해보면 데이터 요청과 응답을 기다렸다가 UI를 업데이트해야 하는 경우가 ‘UI 업데이트를 막는 긴 작업’에 흔히 해당할 것이다.</p>
<p>하지만 input 의 value UI 업데이트는 긴급 업데이트(기존의 일반 setter)로 하고, 나머지 UI 업데이트는 startTransition 으로 감싸 비긴급 업데이트로 처리하도록 하니, input UI 업데이트는 이전 UI 업데이트 렌더링을 기다리지 않고 동작하는 것을 확인할 수 있다.</p>
<p>이러한 리액트의 동시성 모델은 결국 자바스크립트의 비동기 실행 모델 위에 세워져있다. 따라서 자바스크립트의 비동기와 동시성 구현에 대해 이해한다면, 앞으로의 프론트엔드 웹 개발 기술의 변화에 쉽게 적응하고 보다 더 좋아진 사용자 경험을 구현할 수 있을 것이다.</p>
<p>리액트의 동시성 모드에 관심이 없더라도, 거의 모든 작업이 논블로킹으로 작성돼야하는 서버 런타임에서의 자바스크립트와 대부분이 블로킹으로 이루어지는 클라이언트의 렌더링 작업 간의 차이를 탐구하는 것은 항상 더 나은 유저 경험을 만들기 위해 노력하는 프론트엔드에게 유의미한 지식이 될 것이라고 믿는다.</p>
<blockquote>
<p><strong>정리</strong>
웹 렌더링의 발전 과정: 서버 중심 → 클라이언트 중심 → 다시 양쪽 융합</p>
<p>하지만 이 진화의 본질은 “HTML이 어디서 그려지느냐”가 아니라 “비동기를 어디서, 어떻게 기다리느냐”에 있다.
SSR의 Promise, CSR의 Suspense, RSC의 Streaming 모두 이 문제의 다른 해법이다.</p>
</blockquote>
<hr>
<h1 id="비동기">비동기</h1>
<p>비동기는 작업을 순차적으로 기다리지 않고 다음 작업을 바로 이어서 진행하는 방식을 뜻한다. 그리고 싱글스레드 언어인 자바스크립트에서 이러한 비동기 프로그래밍을 어떻게 처리해오고 진화해왔는지를 탐구하는 것은 정말 즐거운 일이다. 자바스크립트가 비동기를 처리하는 [콜백 - Promise - async/await] 과 같은 방식들에 대해 이미 알고있다는 전제로 작성하겠다.</p>
<h2 id="1-서버nodejs에서의-비동기-처리">1. 서버(Node.js)에서의 비동기 처리</h2>
<p>Node.js는 기본적으로 싱글 스레드 기반으로 동작하며, 이벤트 루프와 콜백, 프로미스, async/await를 활용하여 비동기·논블로킹 I/O 처리를 구현한다. </p>
<p><strong>서버가 논블로킹으로 작동해야 하는</strong> <strong>이유</strong>는, 이 방식이 CPU 자원을 효율적으로 활용하여 다수의 클라이언트 요청을 동시에 처리하며 전통적인 멀티스레드 서버가 갖는 스레드 생성 및 관리 오버헤드, 블로킹에 따른 CPU 낭비 문제를 피할 수 있게 해주기 때문이다. </p>
<p>결과적으로 Node.js의 싱글 스레드 + 논블로킹 모델은 적은 자원으로 높은 확장성과 응답성을 구현하는 데 핵심적인 역할을 한다. </p>
<p>그리고 이는 CPU 작업과 I/O 작업을 효율적으로 분리해, 블로킹 없이 다수의 작업을 시간 분할로 관리하는 <strong>동시성 모델</strong>이 핵심이다. 비동기 API를 통해 긴 시간이 소요되는 작업을 이벤트 루프에 위임하고, 작업 완료 시점에 콜백을 받아 처리한다. 이러한 구조는 단일 스레드임에도 불구하고 높은 처리량과 확장성을 가능하게 한다.</p>
<p>참고로 진정한 병렬 처리 기능은 Node.js 10버전 이후부터 도입된 워커 스레드(worker threads)를 통해 지원되기 시작했다. 워커 스레드는 별도의 스레드를 생성해 CPU 집중적인 작업(예: 이미지 처리, 해싱 등)을 병렬로 수행함으로써, 싱글 스레드로 인한 CPU 바운드 작업 병목을 해소한다. 그러나 일상적인 네트워크 I/O나 파일 시스템 접근과 같은 비동기 작업 대부분은 여전히 이벤트 루프와 libuv 스레드 풀을 이용한 비동기 논블로킹 처리에 의존한다.</p>
<p>즉, Node.js 기반 서버 사이드 렌더링(SSR) 환경에서 비동기 코드는 HTML 생성 과정의 중단점 역할을 하면서, 이벤트 루프를 통한 효율적인 동시성 처리와 필요시 워커 스레드를 통한 병렬 처리가 적절히 조합되어 서버 리소스를 효과적으로 활용한다. 이러한 비동기·동시성 모델이 Node.js 서버 환경에서 빠르고 확장성 높은 응답 처리의 핵심 비결이다.</p>
<h2 id="2-클라이언트브라우저에서의-비동기-처리">2. 클라이언트(브라우저)에서의 비동기 처리</h2>
<p>브라우저 역시 자바스크립트 엔진을 기반으로 한 <strong>싱글 스레드 환경</strong>에서 동작하지만, 그 구조적 목표는 서버와 다르다. Node.js가 I/O 중심의 논블로킹 동시성을 최적화하는 것이라면,</p>
<p>브라우저는 사용자 경험을 방해하지 않으면서 <strong>부드럽게 렌더링되는 화면</strong>을 유지하는 데 초점이 맞춰져 있다.</p>
<hr>
<h3 id="21-브라우저의-이벤트-루프-구조">2.1. 브라우저의 이벤트 루프 구조</h3>
<p>브라우저는 <strong>JavaScript 실행 스레드</strong> 외에도 렌더링 엔진(Renderer), 네트워킹 스레드, Web API 스레드 등 다수의 백그라운드 스레드가 함께 작동한다. 하지만 자바스크립트 코드 자체는 여전히 단일 실행 컨텍스트 위에서 실행되며, 이 이벤트 루프(Event Loop)가 모든 <strong>비동기 작업의 흐름을 조율</strong>한다.</p>
<ol>
<li><strong>Micro Task Queue</strong><ul>
<li><code>Promise.then</code>, <code>async/await</code> 이후 처리 등이 이 큐에 등록된다.</li>
</ul>
</li>
<li><strong>Macro Task Queue</strong><ul>
<li><code>setTimeout</code>, <code>setInterval</code>, <code>I/O callbacks</code> 등 일반적인 작업이 이 큐에 쌓인다.</li>
</ul>
</li>
<li><strong>렌더링 단계 (Repaint/Reflow)</strong><ul>
<li>DOM 변경 사항을 반영해 화면을 다시 그린다.</li>
</ul>
</li>
</ol>
<p>즉, 브라우저의 이벤트 루프는 <strong>자바스크립트 실행 → 태스크큐 처리 → 렌더링</strong>이라는 세 가지 단계를 엄격히 순환하면서 동작한다.</p>
<p>이 구조 때문에 자바스크립트는 한 번에 하나의 작업만 수행할 수 있고, 긴 작업이 실행되면 <strong>렌더링이 블로킹되어 UI가 멈춘 듯한 현상</strong>이 발생한다.</p>
<hr>
<h3 id="22-브라우저-비동기의-본질--렌더링을-지연시키지-않는-것">2.2. 브라우저 비동기의 본질 — “렌더링을 지연시키지 않는 것”</h3>
<p>브라우저에서의 비동기는 단순히 병렬로 실행되는 것이 아니라, <strong>렌더링 타이밍을 방해하지 않도록 작업을 나누어 실행하는 방식</strong>으로 구현된다.</p>
<hr>
<h2 id="3-서버-블로킹과-클라이언트-블로킹의-차이">3. 서버 블로킹과 클라이언트 블로킹의 차이</h2>
<p>이제 서버와 클라이언트의 비동기 모델을 비교해보자. 둘 다 “싱글 스레드 + 이벤트 루프”라는 같은 언어적 토대 위에 있지만, ‘무엇이 블로킹으로 작동하느냐’는 완전히 다르다.</p>
<h3 id="31-서버-사이드에서의-블로킹">3.1. 서버 사이드에서의 블로킹</h3>
<p>서버에서의 비동기는 대부분 I/O 작업에 집중되어 있다. 즉, 데이터베이스 조회, API 호출, 파일 접근 같은 작업이 대표적이다</p>
<pre><code class="language-jsx">// 서버사이드 예시 (Node.js, SSR 중)
async function renderPage() {
  const data = await fetchData(); // 여기서 멈춤
  return renderToString(&lt;App data={data} /&gt;);
}</code></pre>
<p>여기서 <code>await fetchData()</code>가 실행되면, <strong>해당 Promise가 resolve되기 전까지 HTML 생성이 중단된다.</strong></p>
<p>이게 바로 SSR의 “렌더링 블로킹 지점”이다. 즉, SSR 환경에서 하나의 비동기 대기는 “전체 페이지 HTML 생성이 멈춘다”는 것을 의미한다.</p>
<p>Node.js는 논블로킹 I/O 기반이지만, “렌더링 함수 실행 컨텍스트 내의 <code>await</code>”은 결국 단일 요청 단위에서는 블로킹처럼 작동한다.</p>
<p>(이 순간에는 같은 요청의 HTML 렌더링이 멈춰 있기 때문이다.)</p>
<hr>
<h3 id="32-클라이언트-사이드에서의-블로킹">3.2. 클라이언트 사이드에서의 블로킹</h3>
<p>반면 클라이언트(브라우저)에서는 <code>await</code>나 <code>Promise</code>가 <strong>전체 페이지의 실행을 멈추게 하지 않는다.</strong></p>
<pre><code class="language-jsx">function Profile() {
  const [data, setData] = useState(null);

  useEffect(() =&gt; {
    fetchUser().then(setData);
  }, []);

  if (!data) return &lt;Loading /&gt;;
  return &lt;UserProfile data={data} /&gt;;
}
</code></pre>
<p>이 코드는 서버처럼 전체 렌더링이 멈추지 않는다. 대신 React는 <strong>fallback UI(<code>Loading</code>)를 먼저 렌더링</strong>한 뒤, 비동기 데이터가 도착하면 컴포넌트를 다시 그린다.</p>
<p>즉, <strong>클라이언트의 블로킹은 “렌더링 단위(UI 컴포넌트)”에 국한된다.</strong> SSR처럼 전체 앱이 멈추는 것이 아니라, 비동기 상태를 가진 일부 UI만 대체(fallback)되어 표시된다.</p>
<hr>
<h3 id="33-이-차이가-중요한-이유">3.3. 이 차이가 중요한 이유</h3>
<p>이 차이는 React Suspense와 SSR Streaming, 그리고 RSC 모델로 직접 이어진다. 서버는 한 번의 HTML 렌더링 중 <code>await</code>이 생기면 그 시점 이후의 HTML은 <strong>생성 자체가 지연</strong>된다.</p>
<p>즉, “HTML을 전송하지 못한 채 기다리는 상태”가 되는 것이다. 이를 해결하기 위한 전략이 바로 <strong>Suspense</strong>이다.</p>
<p>서버가 Promise를 만나면 해당 부분을 “빈 자리(placeholder)”로 남겨둔 채 나머지 HTML을 먼저 스트리밍으로 내려보내고, 비동기 작업이 완료되는 즉시 해당 부분을 채워넣는 방식이다.</p>
<p>이제 서버에서 데이터를 기다리는 동안에도 다른 HTML을 먼저 전송할 수 있다. 즉, 서버에서의 블로킹을 <strong>“부분적 비동기”로 바꾼 것</strong>이다.</p>
<hr>
<h3 id="34--정리하면">3.4.  정리하면</h3>
<ul>
<li>서버에서의 <code>Promise</code>는 <strong>전체 렌더링을 멈추게 한다.</strong><ul>
<li>해결책 → Streaming, Suspense</li>
</ul>
</li>
<li>클라이언트에서의 <code>Promise</code>는 <strong>UI의 일부만 잠시 멈춘다.</strong><ul>
<li>해결책 → Suspense, Transition</li>
</ul>
</li>
</ul>
<p>따라서 SSR과 CSR의 핵심 차이는 “비동기를 어디서 기다리느냐”, “그 기다림 동안 무엇을 보여주느냐”에 있다.</p>
<hr>
<h2 id="4-react가-비동기를-동시성으로-끌어올린-이유">4. React가 비동기를 “동시성”으로 끌어올린 이유</h2>
<p>이제 React가 왜 굳이 “동시성”이라는 개념을 끌어들였는지를 보자. 단순히 Promise를 기다리는 것만으로는 사용자 경험을 충분히 제어할 수 없었기 때문이다.</p>
<h3 id="41-react의-렌더링-스케줄링-모델">4.1. React의 렌더링 스케줄링 모델</h3>
<p>React 18부터 도입된 Concurrent Rendering(동시성 렌더링)은 기존의 <strong>동기적 렌더링 모델을 비동기로 분할 가능한 모델로 확장</strong>했다. 즉, 렌더링 전체를 한 번에 처리하지 않고, <strong>여러 조각으로 나누어 “언제 다시 이어붙일지”를 스케줄링할 수 있게 된 것</strong>이다.</p>
<pre><code class="language-jsx">startTransition(() =&gt; {
  setState(expensiveUpdate());
});</code></pre>
<p>이 코드는 “우선순위가 낮은 상태 업데이트”로 예약되며, React는 이벤트 루프의 여유 시간을 활용해 백그라운드에서 렌더링을 진행한다. </p>
<p>이때 브라우저의 메인 스레드는 여전히 사용자 입력에 즉시 반응할 수 있다. 즉, React의 동시성은 “렌더링 중에도 UI를 멈추지 않는 것”에 초점을 맞춘다.</p>
<hr>
<p>자바스크립트는 싱글 스레드라는 한계에도 환경과 동시성을 활용하여 범용 언어로 자리 잡았다. 이러한 자바스크립트의 동작을 이해하고 리액트가 그리는 동시적인 UI 렌더 방식을 이해한다면 느리고 무거운 화면일지라도 최대한 부드러운 UX를 제공할 수 있을 거라고 기대한다.</p>
<hr>
<h1 id="레퍼런스">레퍼런스</h1>
<p><a href="https://tech.remember.co.kr/%EC%BD%94%EB%93%9C-%ED%95%9C-%EC%A4%84%EB%A1%9C-%EA%B2%BD%ED%97%98%ED%95%98%EB%8A%94-react-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%98-%EB%A7%88%EB%B2%95-5ff18aee148d">https://tech.remember.co.kr/%EC%BD%94%EB%93%9C-%ED%95%9C-%EC%A4%84%EB%A1%9C-%EA%B2%BD%ED%97%98%ED%95%98%EB%8A%94-react-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%98-%EB%A7%88%EB%B2%95-5ff18aee148d</a></p>
<p>도서 &lt;자바스크립트 완벽 가이드&gt;</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Suspense와 비캐시 Promise가 유발하는 무한 Retry 루프 심층 분석으로 미래지향 리액트 학습하기]]></title>
            <link>https://velog.io/@dev-dino22/Suspense%EC%99%80-%EB%B9%84%EC%BA%90%EC%8B%9C-Promise%EA%B0%80-%EC%9C%A0%EB%B0%9C%ED%95%98%EB%8A%94-%EB%AC%B4%ED%95%9C-Retry-%EB%A3%A8%ED%94%84-%EC%8B%AC%EC%B8%B5-%EB%B6%84%EC%84%9D%EC%9C%BC%EB%A1%9C-%EB%AF%B8%EB%9E%98%EC%A7%80%ED%96%A5-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%95%99%EC%8A%B5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev-dino22/Suspense%EC%99%80-%EB%B9%84%EC%BA%90%EC%8B%9C-Promise%EA%B0%80-%EC%9C%A0%EB%B0%9C%ED%95%98%EB%8A%94-%EB%AC%B4%ED%95%9C-Retry-%EB%A3%A8%ED%94%84-%EC%8B%AC%EC%B8%B5-%EB%B6%84%EC%84%9D%EC%9C%BC%EB%A1%9C-%EB%AF%B8%EB%9E%98%EC%A7%80%ED%96%A5-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%95%99%EC%8A%B5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 04 Nov 2025 11:56:38 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며...</h1>
<p>본 문서는 React Client Component 환경에서 Suspense와 use() 훅을 사용하여 비동기 데이터를 처리할 때, react-router의 라우트 이동(navigate)과 관련된 <strong>비캐시(uncached) Promise</strong>가 무한 retry 현상을 유발했던 상황을 심층 분석하여, 그 근본적인 원인이었던 React 동시성 모드(Concurrent Mode)의 startTransition 과 <strong>렌더링 멱등성</strong> 원칙에 대해 고찰한다.</p>
<p>탐구한 내용이 꽤 많고 깊어지는 부분이 있어, 성향에 따라 재미없고 궁금하지도 않은 그저 난해한 글처럼 느낄 독자가 꽤 있을 것 같다. 그래서 추천 대상과 여기서 다루는 학습 키워드들에 대해 간략하게 소개하고 글을 시작하고자 한다.</p>
<p><strong>추천 독자</strong></p>
<ul>
<li>React 18 이후의 동시성 렌더링(Concurrent Rendering) 메커니즘을 깊이 이해하고 싶은 개발자</li>
<li>Suspense와 use() 훅을 클라이언트 컴포넌트에서 활용하려다 예기치 못한 리렌더링 문제를 경험한 개발자</li>
<li>React 내부 재조정(Reconciliation) 원리와 Suspense의 Retry 메커니즘을 근본적으로 알고 싶은 개발자</li>
<li>Promise 가 클라이언트 컴포넌트 렌더주기 내에서 생성되면 안 되는 이유를 알고 싶은 개발자</li>
<li>추가된 동시성 렌더링 매커니즘에 따라 React 라이브러리들의 점진적인 변화로 인해 겪을 수 있는 문제를 방지하고싶은 개발자</li>
</ul>
<p><strong>주요 학습 키워드</strong></p>
<ul>
<li>React 18 Concurrent Mode (startTransition, TransitionLanes, RetryLanes, 긴급/비긴급 업데이트)</li>
<li>Suspense 메커니즘 (Promise throw, Ping 신호, resolveRetryWakeable)</li>
<li>렌더링 멱등성과 Promise 캐싱 전략</li>
<li>react-router navigate()의 비긴급 업데이트</li>
<li>비캐시 Promise로 인한 무한 retry 루프 흐름 분석</li>
</ul>
<hr>
<h1 id="1-문제-발생-배경과-트러블슈팅-시작"><strong>1. 문제 발생 배경과 트러블슈팅 시작</strong></h1>
<h2 id="11-구체적인-문제-상황-소개">1.1. <strong>구체적인 문제 상황 소개</strong></h2>
<p>클라이언트 컴포넌트내에서 React 18의 Suspense와 데이터를 읽는 use() 훅을 사용하던 중 특이한 문제 상황이 발생했다. </p>
<p>자기 자신으로의 라우트 이동 시에 Suspense 가 무한 retry 를 하며 <strong>API 요청을 무한으로 보내는 심각한 버그</strong>였다.</p>
<p>문제 코드는 use 훅을 사용하는 자식 컴포넌트에 부모 컴포넌트가 API 요청을 담당하는 함수(api.get())의 반환값인 <strong>새로운 Promise 객체</strong>를 prop으로 넘겨주는 구조였다. 이러한 컴포넌트에 ‘현재 페이지로 라우트 이동’을 하는 기능이 있던 Header 가 있었다.</p>
<p>해당 프로젝트 코드 구조를 단순하게 묘사해보면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/af8be4ff-f0b7-411d-b79d-833f23eab7bf/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/3231d422-6b74-47d2-857d-1c84a688cd99/image.png" alt=""></p>
<p>해당 코드는 문제가 많은 코드긴 하다. 문제점들에 대해서는 후술하며 문제가 되는 이유를 하나하나 뜯어볼 것이니, 먼저 단순히 이 코드에서 navigate 버튼을 클릭했을 때 어떤 문제가 일어나는지 확인해보자.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/acae5c74-ae40-466c-b093-839fd7471b4a/image.gif" alt=""></p>
<p>자기 자신으로의 이동을 했을 뿐인데 API 무한 요청 버그가 일어났다. 일단 해당 버그를 해결하는 방법은 크게 세 가지가 있었다.</p>
<p><strong>[해결 방법]</strong></p>
<ol>
<li>props 로 넘기고 있는 Promise 객체를 <strong>메모이제이션</strong> 해주거나</li>
<li><strong>Suspense</strong> 에 매 렌더링마다 값이 달라지는 <strong>key</strong> 를 할당해주거나</li>
<li>UsePage 컴포넌트에 호출되어있는 <strong>navigate 훅을 지우는 것</strong></li>
</ol>
<p>위 세 가지 방법 중 하나만 수행해도 현상은 해결됐다. 1,2 번 방법은 그렇다치는데, 3번 방법은 꽤 의아하지 않은가?</p>
<p>🔽 <span style="color: gray">useNavigate 호출 부분만 주석처리했다.</span></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/d99d25af-4f5c-4c65-8a64-56452402ae4f/image.png" alt=""></p>
<p><strong>Header 컴포넌트 안의 navigate 훅과 버튼은 여전히 존재하고, promise 를 캐싱해주지도 않았는데 호출되어있던 navigate 훅을 지우는 것으로도 문제가 해결된다 !</strong></p>
<p>🔽 <span style="color: gray">더이상 API 요청이 무한 발생하지 않는다.</span></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/5cd562b7-f6a2-4749-a85a-4e20882f8d1a/image.gif" alt="navigate 훅 호출을 지우면 해결"></p>
<p>컴포넌트의 리렌더링은 일어났는데 Suspense 의 fallback UI 가 다시 렌더링되지도 않고, 무한 API 요청이 발생하지 않는 점도 신기하다.</p>
<p>문제를 발견했던 당시에는 프로젝트의 스프린트 마감이 다가오고 있었으므로 cached promise로 문제를 해결하고 넘어갔었지만, 3번의 해결법이 꽤 호기심을 자극했던지라 이런 현상이 ‘왜’ 발생하는지 정확한 이유와 흐름을 찾고 싶었던 나는 해당 스프린트 종료 후 탐구를 시작하게 되었다. </p>
<p>해결법도 다 알았으니 원인을 찾는 일은 간단하게 마무리될 줄 알았던 나는 이 문제를 생각보다 꽤 오래 붙잡고 있어야했고, 그 과정에서 정말 많은 학습을 할 수 있었다.</p>
<p>따라서, 이 간단해보이는 트러블 슈팅이 점점 깊어졌던 과정을 함께하면 리액트의 흐름과 동작을 이해하고 리액트가 추구하는 앞으로의 점진적 변화까지 탐구하는 것에 도움이 될 것이라고 생각한다.</p>
<h2 id="12-발견과-가설-수립">1.2. 발견과 가설 수립</h2>
<p>먼저 문제 상황을 명확히 정리해보았다.</p>
<h3 id="121-문제-발생-시점과-증상">1.2.1. 문제 발생 시점과 증상</h3>
<p><strong>[시점]</strong></p>
<ul>
<li>navigate 함수를 통해 <strong>현재 페이지로 다시 이동했을 때</strong>,</li>
</ul>
<p><strong>[문제]</strong></p>
<ul>
<li><strong>API 가 무한 요청된다</strong>.</li>
</ul>
<p><strong>[추가 특이점]</strong></p>
<ul>
<li>리렌더링임에도 <strong>Suspense 의 fallback UI 가 표시되지 않는다</strong>.<ul>
<li>일반적인 <strong>state 업데이트로 인한 리렌더링에서는 fallback UI 가 정상적으로 표시된다</strong>.</li>
</ul>
</li>
<li>개발자 도구 <strong>profiler 탭에서 리렌더링 커밋이 잡히지 않는다</strong>.</li>
</ul>
<p><strong>[문제가 해결되는 상황]</strong></p>
<ul>
<li>props 로 넘기고 있는 Promise 객체를 <strong>메모이제이션</strong> 해준다</li>
<li><strong>Suspense</strong> 에 매 렌더링마다 값이 달라지는 <strong>key</strong> 를 할당해준다</li>
<li>UsePage 컴포넌트에 호출되어있는 <strong>navigate 훅을 지운다</strong></li>
</ul>
<p>navigate 함수를 통해 현재 페이지로 다시 이동했을 때, API 호출 함수인 api.get()이 <strong>무한으로 반복 실행</strong>되는 현상이 정확한 문제 상황이었다. 또 눈여겨볼만한 증상은 일반적인 리렌더링과는 다르게 Suspense의 <strong>fallback UI가 전혀 표시되지도, 컴포넌트 리렌더링 커밋이 잡히지도 않고</strong> 조용히 API 요청만 무한 반복되었다는 점이다. (이 때 네트워크 탭을 열지 않고 무한 요청이 가고있는지도 모른 채 계속 작업했더라면…아찔하다.)</p>
<h3 id="122-가설-수립">1.2.2. 가설 수립</h3>
<p><span style="background-color: #ffe349"><strong>가설 1. state 변화로 인한 업데이트는 리렌더링이고 자기 자신으로 이동하는  navigate의 라우터 업데이트는 &#39;재마운트&#39;일 것이다</strong></span></p>
<p>초기에는 state 변화로 인한 리렌더링과 navigate로 인한 리렌더링의 차이가 &#39;재마운트(remount)&#39; 여부에 있을 것이라고 가설을 세웠다.</p>
<p>하지만, useEffect 실험 결과 이는 <strong>사실이 아니었다</strong>. 자기 자신으로 navigate를 하더라도 컴포넌트가 완전히 언마운트 후 재마운트 되는 것이 아니라 <strong>그냥 리렌더링</strong>되는 것으로 확인됐다.</p>
<blockquote>
<p>💡 <strong>검증된 point.</strong>
현재 페이지로의 라우터 이동에 따른 UI 업데이트는 &#39;리렌더링&#39; 으로 처리된다.</p>
</blockquote>
<p>그런데, 같은 리렌더링이라면 남는 의문점이 있다.</p>
<p>useState 의 state 업데이트로 인한 리렌더링은 Suspense의 fallback UI 도 표시되고, 컴포넌트의 리렌더링 커밋이 추적되었는데, 왜 navigate의 자기자신으로 라우트 이동은 fallback UI 도 표시되지 않고 컴포넌트 리렌더링 커밋도 추적되지 않는 것일까?</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/0cf84833-226b-489a-aad3-33c3ee889325/image.gif" alt="state업데이트와navigate차이"></p>
<p><span style="background-color: #ffe349"><strong>가설 2. state 업데이트로 인한 리렌더링과 라우터 이동이 일으키는 리렌더링은 다른 방식의 리렌더링이다</strong></span></p>
<p>위 이미지에서 확인할 수 있다시피 state 변화는 문제가 없는데 navigate로 인한 리렌더링만 문제이므로, 라우터 관련 동작에 특이점이 있을 것이라고 추론했다.</p>
<p>그렇다면 정확히 state 리렌더링과 router 리렌더링 시 다른 현상을 보이고 있는 주체는 누구일까? 바로 <strong>Suspense</strong> 이다. </p>
<p>같은 리렌더링임에도 두 동작 간에 Fallback UI 의 표시 유무에 차이가 있었다.</p>
<p><span style="background-color: #ffe349"><strong>가설 3. 무한 렌더링을 시도하는 실질적인 주체는 Suspense이며, Promise가 resolve될 때마다 재시도 하는 메커니즘이 비정상적으로 반복되고 있다</strong></span></p>
<p>클라이언트 컴포넌트에서 Suspense와 use를 사용하여 정상적으로 렌더링을 하는 흐름을 간략하게 표현해보면 다음과 같다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/d99d25af-4f5c-4c65-8a64-56452402ae4f/image.png" alt=""></p>
<ol>
<li><p>최초 렌더링 시, <code>Suspense</code> 는 자식(<code>Header, UseComponent, button</code>)을 렌더링하지 않고 <code>fallback UI</code> 를 렌더링하고 있는다.</p>
</li>
<li><p>그동안 <code>Suspense</code> 는 자식들을 백그라운드에서 렌더링 시도하며, <code>UseComponent</code>가 렌더링됨에 따라 Props 로 자식에게 promise 를 넘긴다.</p>
</li>
<li><p>promise props(<code>api.get()</code>)를 받은 자식은 <code>use(promise)</code> 로 pending 상태의 promise 를 <code>throw</code> 한다. 여기까지의 모든 일은 실제 컴포넌트로 마운트되며 렌더링되고 있지않고, 백그라운드에서 일어나고 있는 일이다.</p>
<blockquote>
<p>📝 Note.
 <code>use()</code> 훅은 인자로 context 나 Promise 를 받고, promise가 pending 상태일 경우 <code>throw</code>, <code>fullfilled</code> 상태일 경우 값을 읽어 꺼내주고, <code>error</code> 상태일 경우 error 를 throw 해주는 훅이다. <a href="https://ko.react.dev/reference/react/use">https://ko.react.dev/reference/react/use</a></p>
</blockquote>
</li>
<li><p><code>UseComponent</code> 를 감싸고 있던 Suspense 가 던져진(throw) Promise 를 잡아 해당 promise가 resolve 되면 fallback UI 를 치우고 다시 컴포넌트 렌더링을 시도한다(retry)</p>
</li>
</ol>
<hr>
<p>위의 정상적인 흐름과 현재의 문제 상황의 차이는 Fallback UI 를 표시하지 않는 것과 Promise 생성과 해결이 무한으로 반복되고 있다는 것이다.</p>
<p>use() 훅은 Promise 를 받았을 때 resolve 된 상태가 아니라면 위로 throw 하는 역할 밖에 하지 않는다.</p>
<p>따라서 트리거가 router 이동일지언정, 실제로 문제를 일으키는 범인은 retry 함수를 가진 Suspense 로 좁혀볼 수 있을 것이다.</p>
<blockquote>
<p>💡 <strong>검증된 point.</strong></p>
</blockquote>
<ul>
<li>Suspense 컴포넌트에 <strong>key Prop</strong>을 추가해주거나, api.get() 호출을 useMemo로 감싸 캐싱하면 문제가 해결된다.</li>
<li>Suspense에 key를 추가하는 행위는 fallback UI가 표시되지 않는 문제를 해결하기 위한 리액트 공식 권장 사항이었다.
<a href="https://ko.react.dev/reference/react/Suspense#showing-stale-content-while-fresh-content-is-loading">https://ko.react.dev/reference/react/Suspense#showing-stale-content-while-fresh-content-is-loading</a></li>
</ul>
<p>하지만 아직 내 의문이 제대로 해소된 점은 아무 것도 없었다.</p>
<p><em>- ‘state 업데이트로 인한 리렌더링과 자기자신으로의 라우터 이동에 의한 리렌더링은 무엇이 다르지?’</em>
<em>- ‘Promise는 왜 메모이제이션해야하지?’</em>
<em>- ‘Suspense의 retry의 트리거가 무엇이길래?’ …</em></p>
<p>오히려 의문만 꼬리에 꼬리를 물고 이어졌다. 그래도 첫 번째 의문인 두 리렌더링의 차이를 정확히 알게된다면, 나머지 의문들도 해소될 수 있을 것 같았다. </p>
<p>결국 가설 2번과 3번을 실험하고 검증하기 위해 react-router 라이브러리의 내부 구현 코드를 살펴보았다.</p>
<h3 id="123-가설-탐구">1.2.3. 가설 탐구</h3>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/e093f68c-d93e-4e92-8612-84bf7b2cf081/image.png" alt="라우터 내부 구현"></p>
<p>이 과정에서 알게된 건, 라우터는 자기자신으로의 이동이든 다른 라우터로의 이동이든 동일하게 해당 매커니즘을 사용하고 있었다. 그런데 여기서 처음보는 훅이 있었다. 바로 state의 setter를 감싼 <strong>React.startTransition</strong>이다. 아마 이게 일반적인 state의 업데이트와 차이점을 만들어내는 단서인 것 같았다.</p>
<p>해당 <strong>startTransition</strong> 훅은 React 18 버전에 등장하여, 기존의 동기적이고 멈출 수 없었던 리액트의 렌더 과정을 중단 가능하게 만들고 긴급하지 않은 업데이트를 낮은 우선순위로 스케쥴링할 수 있도록 하는 “동시성 렌더링” 구현 방식의 핵심 API 이다.</p>
<p>이러한 동시성 렌더링의 도입으로 리액트는 이제 중요한 사용자 입력 반응을 유지하면서, 데이터 로딩이나 페이지 전환과 같은 긴 작업을 백그라운드에서 처리할 수 있게 되었다.</p>
<p>그리고 일반적인 state의 업데이트의 경우 긴급 업데이트로 분류되고 startTransition 의 경우 비긴급 업데이트로 분류된다는 사실을 알았다.</p>
<p>🔽 <span style="color: gray">우선순위가 높은 DefaultLane 을 사용하는 state 업데이트 setter와 더 낮은 TransitionLane 을 사용하는 startTransition</span></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/380e30a0-0271-4660-bdc9-a2219b17476d/image.png" alt="Lane우선순위"></p>
<p>그렇다면, 첫 번째 의문인 일반적인 state 업데이트와 startTransition 으로 감싼 state의 업데이트에는 “우선순위”에 있어 차이가 있다는 것을 알 수 있다.</p>
<p>따라서 react-router가 라우트 변경을 처리할 때 <strong>React.startTransition API</strong>를 사용하여 비긴급 업데이트를 수행하는 것과, 컴포넌트 렌더링 주기 내에서 <strong>캐싱되지 않은 Promise</strong>를 생성하는 것이 충돌의 핵심 원인일 것으로 의심되었다.</p>
<p>하지만 아직 의심이고 추론일 뿐이지, Suspense의 retry 와 정확히 어떤 충돌을 일으켜 이런 버그를 발생시키는지는 아직 설명할 수 없다. 여기까지 알고싶다면 이제 Suspense의 동작 원리까지 탐구해봐야할 단계였다.</p>
<hr>
<h1 id="2-react-suspense의-동작-원리-정리"><strong>2. React Suspense의 동작 원리 정리</strong></h1>
<h2 id="21-suspense의-기본-동작-방식">2.1. <strong>Suspense의 기본 동작 방식</strong></h2>
<p>Suspense는 비동기 작업이 완료될 때까지 불필요한 상태 없이 <strong>fallback UI</strong>를 보여줌으로써 사용자 경험을 개선하는 React의 메커니즘이다.</p>
<p>살펴본 Suspense의 비동기 처리 흐름은 다음과 같았다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/ae54c472-fb53-45c4-8fba-a9cd9d70bda7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/cdd7a897-a3fb-411a-b0c2-3e8ab6b2eaf6/image.png" alt=""></p>
<ol>
<li><strong>최초 렌더링 시작:</strong> Suspense는 바로 fallback UI를 대신 보여준다.</li>
<li><strong>자식 컴포넌트 렌더링 시도:</strong> 내부 컴포넌트 렌더링을 시도한다. 내부 컴포넌트가 모두 렌더링 완료될 때까지 리액트의 렌더 트리에는 fallback UI 가 대신 들어가있기 때문에 이 작업은 백그라운드에서 실행되고 있다고 생각해도 좋다.</li>
<li><strong>Promise Throw:</strong> 자식 컴포넌트 내에서 use() 훅이 Promise를 인자로 받아 아직 resolve되지 않은 상태라면, React의 렌더링 스택을 끊기 위해 <strong>Promise를 던진다 (throw)</strong>. 약간 흥미로운 사실은 자바스크립트는 Error 뿐만 아니라 무엇이든 던질 수 있고(throw), 무엇이든 잡을 수 있다(catch)는 것이다.</li>
<li><strong>Promise 추적:</strong> Suspense Boundary는 던져진 Promise를 잡고 상태를 추적 관찰한다. React 내부 플래그(DidCapture 등)를 통해 현재 상태(대기 중인지 여부)를 관리한다.</li>
<li><strong>Retry 신호(Ping):</strong> Promise가 resolve되면, React는 내부적으로 <strong>Ping 신호</strong>를 보내 해당 컴포넌트를 다시 렌더링하라는 명령을 예약한다. 이 Ping 신호는 resolveRetryWakeable 함수를 통해 최종적으로 처리된다.</li>
<li><strong>정상 렌더링 완료:</strong> 재렌더링 시도가 무사히 완료되면, Suspense는 fallback UI 상태를 정상 컴포넌트 렌더링 상태로 전환하여 fallback UI를 화면에서 지운다. 참고할 점은, Suspense는 자신의 모든 자식 컴포넌트들이 렌더링될 준비가 완료됐을 때 fallback UI 를 지우고 한 번에 화면에 나타나는 것을 보장한다.</li>
</ol>
<h3 id="suspense가-비동기-작업을-감지하고-처리하는-기본-메커니즘"><strong>Suspense가 비동기 작업을 감지하고 처리하는 기본 메커니즘</strong></h3>
<p>비동기 작업의 감지 및 처리는 <strong>Fiber 아키텍처</strong>와 <strong>thenable 추적 시스템</strong>을 통해 이루어진다. React는 각 렌더링 시도마다 사용된 Promise들을 <strong>인덱스 기반으로 추적</strong>하는 trackUsedThenable 함수를 사용한다. <strong>이 메커니즘은 컴포넌트가 멱등성을 가진다는 React의 기본 가정에 기반한다.</strong></p>
<p>Promise가 resolve되면, attachSuspenseRetryListeners 함수가 해당 wakeable(Promise)에 대해 retry 리스너를 등록하고, Promise가 resolve되거나 reject될 때 resolveRetryWakeable이 호출되어 boundary를 다시 렌더링하도록 예약한다.</p>
<hr>
<h3 id="starttransition이-suspense-fallback-ui-처리에-미치는-영향"><strong>startTransition이 Suspense fallback UI 처리에 미치는 영향</strong></h3>
<p>startTransition을 사용하여 업데이트를 수행할 때 Suspend가 발생하면, <strong>Suspense</strong>는 일반 업데이트와 다르게 동작한다.</p>
<ul>
<li><strong>일반 업데이트:</strong> Suspend 발생 시 <strong>즉시 fallback UI를 표시</strong>한다. Fallback이 커밋되면 원래 컴포넌트는 렌더링되지 않는다. 위에서 언급했듯, 렌더 트리에 포함되지 않는다는 말이다.</li>
<li><strong>Transition 업데이트:</strong> Suspend 발생 시 <strong>fallback을 즉시 보여주지 않고</strong> 이전 UI를 유지하면서 백그라운드에서 계속 렌더링을 시도한다.</li>
</ul>
<p>Transition 업데이트는 내부적으로 shouldRemainOnPreviousScreen() 함수를 통해 fallback 표시를 건너뛴다. 이로 인해 <strong>매 retry마다</strong> Promise를 던진 컴포넌트가 계속 렌더링되는 환경이 조성된다. 그런데 위에서 설명했듯, <strong>Suspense</strong>는 Promise가 resolve 될 때 retry 를 예약한다.</p>
<p>자. 문제를 찾은 것 같다. 정확히 무한 루프가 되는 흐름을 설명해보면 다음과 같다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/2d702e2d-c24d-49f7-ae60-6b7497b91833/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/50c2dac7-3123-4371-bfe5-b0f998748d8e/image.png" alt=""></p>
<p><strong>무한 retry 루프 시나리오:</strong></p>
<p>1️⃣ <strong>Transition 트리거</strong>
navigate (startTransition으로 래핑된 업데이트)가 호출된다.</p>
<p>2️⃣ <strong>Fallback 미노출</strong>
Transition 업데이트이므로 shouldRemainOnPreviousScreen()이 true를 반환하여 Suspense는 fallback UI를 커밋하지 않고 이전 UI를 유지한다.</p>
<p>3️⃣ <strong>새 Promise 생성</strong>
컴포넌트가 리렌더링되며 api.get()이 호출되어 새로운 Promise A가 생성되고, use()에 의해 throw된다.</p>
<p>이 부분이 위의 정상적인 Suspense 동작과의 핵심 차이인데, 바로 fallback UI 가 표시되지 않아, 백그라운드가 아닌 렌더 트리에 UseComponent 가 포함되었다는 점이다. 그리고 해당 컴포넌트는 마운트되지 않은 상태이다.</p>
<p>4️⃣ <strong>Ping 리스너 등록</strong>
Suspense는 Promise A를 추적하고 resolve 시 retry를 위한 Ping 리스너를 등록한다.</p>
<p>5️⃣ <strong>Promise A resolve 및 Retry</strong>
Promise A가 resolve되면 Ping이 발생하고 prepareFreshStack이 호출되어 다시 렌더링된다. 문제는 이 시점에 fallback UI 에서 “완성된&quot; resolve 컴포넌트를 대체해주는 것이 아닌 Suspense 하위 자식들을 다시 렌더링한다는 것이다.</p>
<p>그렇다면, UseComponent 의 인라인 Props 로 넘겨주는 api.get() 함수도 다시 실행될 것이고, 그럼 새로운 Promise B 가 Props 로 넘어가게된다.</p>
<p><strong>6️⃣ 루프 재시작</strong>
React는 trackUsedThenable에서 Promise A를 재사용하려 하지만, 지금 새로 생성된 Promise B 역시 use(PromiseB)로 trhow 되므로 Ping 리스너를 등록한다. 결국 컴포넌트가 마운트되기 전에 미해결된 Promise가 또 생성되었고, 이 때문에 계속 컴포넌트가 마운트 완료되지 않아 여전히 UI에는 어떤 업데이트도 없는 것이다.</p>
<p><strong>7️⃣ Promise B resolve 및 Retry…</strong>
Promise B가 resolve되면 또 다른 Ping이 발생하고 Root부터 다시 렌더링된다. 이렇게 UI는 아무 변화도 일어나지 않은 채 뒤에서 조용히 이 과정이 무한히 반복되는 것이다.</p>
<hr>
<p>아. 이제 명확해졌다. 어디서부터 꼬여서 어떻게 그런 끔찍한 버그를 낸 건지 흐름을 찾아냈다.</p>
<p>개운해진 마음으로 리액트의 테스트 코드에서도 내가 겪은 문제를 재현한 코드를 발견할 수 있었다. transition 중에 캐시되지 않은 promise를 생성하면 무한 retry 가 된다는 테스트 코드였다. 이미 리액트도 해당 문제를 인지하고 있었다. 아래는 그 코드에서 가져온 경고 문구이다.</p>
<hr>
<p>&quot;<em>A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.</em>&quot;.</p>
<p>“컴포넌트가 캐시되지 않은(uncached) 프로미스에 의해 서스펜드(suspend)되었습니다. 클라이언트 컴포넌트나 훅 내부에서 프로미스를 직접 생성하는 것은 아직 Suspense 호환 라이브러리나 프레임워크를 통해서만 지원됩니다.”</p>
<hr>
<p>번외로, 그렇다면 이제 처음에 언급한 문제를 해결하는 세 가지 방법 중 한 가지인 Promise 를 메모이제이션(캐싱)하는 것도 왜 하나의 해결책이 되는지 설명할 수 있을 것이다. 비긴급 업데이트로 인해 Suspense의 fallback UI 가 커밋되지 않아, 마운트되지 않은 채 다음 UI를 백그라운드에서 준비하더라도 useMemo 로 고정되어 같은 참조값의 Promise가 생성되므로, trackUsedThenable에서 Promise 를 정상적으로 재사용할 수 있는 것이다.</p>
<p><strong>prop으로 넘기는 promise를 캐싱해줬을 때</strong></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/3c014dad-6b4b-4bf8-a6c2-18f4fb82c03c/image.gif" alt="promise를메모이제이션해줬을때"></p>
<p>그래서 위 이미지처럼, promise를 캐싱해줬을 때는 fallback UI 는 뜨지 않고 리렌더링이 일어남을 확인할 수 있다.</p>
<p>그렇다면 마지막 하나의 방법인, Suspense에 렌더링마다 새로운 key 를 부여하는 것으로도 왜 해결이 될까? fallback UI 의 커밋이 일어나지 않아 마운트되기 전에 새 Promise가 계속 생성된다면 새로운 Suspense인 건 상관이 없을텐데 말이다. 해답은 리액트의 테스트 코드 주석에서 찾을 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/14a14434-1ed9-43ea-b946-1f62220f5179/image.png" alt="리액트 테스트 코드"></p>
<p>‘초기 렌더링이 transition(비긴급 업데이트)이었음에도 불구하고, fallback UI가 표시된다’</p>
<p>그렇다. transition 중이더라도 새로운 Suspense라면 fallback UI 를 그냥 보여줘버린다.</p>
<p><strong>Suspense에 새로운 key 값을 줬을 때</strong></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/a14fdaa5-2da6-4a4d-939c-8e007e99419a/image.gif" alt="suspensekey값을줬을때"></p>
<p>마지막에 더 나은 방법이 아직 생각나지 않는다는 주석에서 왠지 개발자의 표정이 괜히 상상된다.</p>
<hr>
<h1 id="3-글-마무리"><strong>3. 글 마무리</strong></h1>
<p>마지막으로 위의 내용이 맞다면, navigate 훅을 사용하지 않아도 리렌더링을 일으키는 원인이 transition 일 때 같은 버그를 확인할 수 있을 것이다.</p>
<p>!codesandbox[dtqcqv?view=editor+%2B+preview&amp;module=%2Fsrc%2FApp.tsx]</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/64ac134c-ae84-4eae-9e6e-14362183b5dd/image.gif" alt="start로래핑"></p>
<p>이 문제 해결 과정 자체가 <strong>React Concurrent Mode</strong>와 관련된 내부 메커니즘을 이해하는 것이 중요하다는 점을 보여준다고 생각한다. 물론 최신 리액트 버전을 사용하고, Suspense와 use 등의 비교적 최신 훅을 사용했기 때문에 해당 트러블 슈팅을 겪은 것도 맞다.</p>
<p>하지만 React Reconciler는 <strong>Concurrent Features</strong>를 지원하기 위해 지속적으로 개선되고 있다. react-router와 같은 주요 라이브러리에 startTransition 과 같이 점진적으로 적용하기로했던 동시성 렌더링의 핵심 훅을 기본으로 사용한 것이 리액트가 앞으로 꾸준히 동시성 모드를 미래지향점으로 잡고 개발해나갈 것을 시사한다고 생각한다.</p>
<p>실제로, 이번에 배포된 React v19.2.0 버전에서는 크롬의 성능 탭에서 스케쥴러를 확인할 수 있는 탭도 추가되었는데, 이 덕분에 긴급/비긴급 업데이트의 흐름을 더 쉽게 추적할 수 있게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/3d0db0b9-1a2c-405f-967b-396ac9eb5f6b/image.png" alt="성능탭스케쥴러"></p>
<p>또한, 리액트의 업데이트는 분명 필요에 의한 업데이트일 것이다. 실제로 나는 프로젝트를 진행하면서 (캐싱과 최적화때문에 이제 곧 도입할 예정이긴 하지만) tanstack-query 도 사용하지 않고 불필요한 로딩 및 에러 상태를 만들지 않으면서 Errorboundary 와 Suspense를 이용해 선언적인 경계를 만드는 것에 좋은 UX/DX 개선을 느꼈다.</p>
<p>최근 리액트는 10년 간의 노력이 담겼다며 react compiler 를 정식 릴리즈하였는데, 이렇듯 더 나은 DX로 더 좋은 UX를 쉽게 구현하기 위해 노력하는 리액트의 업데이트를 놓치지 않고 잘 따라간다면 선진적으로 개발자와 사용자 모두에게 좋은 경험을 제공하는 개발자가 될 수 있을 것이라고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Context API 에서 action 과 state 를 분리해 리렌더링 범위 좁히기(ToastProvider)]]></title>
            <link>https://velog.io/@dev-dino22/Context-API-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-Toast-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dev-dino22/Context-API-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-Toast-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 17 Sep 2025 17:48:31 GMT</pubDate>
            <description><![CDATA[<p>사용자에게 간단한 피드백을 제공하는 UI 요소 중 하나로 <strong>Toast 메시지</strong>가 있다. 네트워크 요청이 성공하거나 실패했을 때 토스트 메세지로 빠르게 알림을 보여주면 UX가 훨씬 좋아진다.</p>
<p>하지만 토스트를 무심코 구현하다 보면 불필요한 상태 관리, 과도한 리렌더링, 여러 개의 토스트를 띄우지 못하는 등의 문제가 생길 수 있다.</p>
<p>이번 글에서는 <strong>가장 단순한 토스트 구현</strong>부터 시작해, 점차 개선하여 <strong>Context API로 토스트</strong>를 구현한 과정을 공유하려 한다.</p>
<p><del>Context API 에 대해 우테코 레벨 2때 공부하면서 이 글을 계속 문서화하려고 했으나 이래저래 레벨 4인 지금에서야 작성하게 되었다...</del></p>
<p>참고) 이 글에서 사용한 스타일 라이브러리는 emotion/styled 이고,예제는 우테코 레벨2 react-shopping-products 미션에서 구현한 코드이다.
이 구현 코드를 바탕으로 현재 픽잇 프로젝트에도 효율적인 토스트를 구현하였다.</p>
<hr>
<h2 id="1-가장-단순한-토스트-구현">1. 가장 단순한 토스트 구현</h2>
<p>가장 쉽게 토스트를 구현하는 방법은 아래처럼 상태를 컴포넌트 안에 두고 조건부 렌더링을 통해 토스트 메시지를 보여주는 것이다.</p>
<pre><code class="language-jsx">function ComponentWithToast() {
  const [message, setMessage] = useState(&#39;&#39;);

  const handleClick = () =&gt; {
    setMessage(&#39;저장되었습니다!&#39;);
    setTimeout(() =&gt; setMessage(&#39;&#39;), 3000);
  };

  return (
    &lt;div&gt;
      &lt;button onClick={handleClick}&gt;저장하기&lt;/button&gt;
      {message &amp;&amp; &lt;Toast&gt;{message}&lt;/Toast&gt;}
    &lt;/div&gt;
  );
}</code></pre>
<p>이 방식은 간단하지만 다음과 같은 문제가 있다</p>
<h3 id="11-문제-발견">1.1. 문제 발견</h3>
<p><strong>1. 상태가 사용하는 쪽 컴포넌트에 있기 때문에 다른 곳에서 재사용하기 어렵다.</strong></p>
<p><strong>2. 토스트의 관리가 특정 컴포넌트에 강하게 결합되어 있다.</strong></p>
<p><strong>3. 하나의 상태만 관리하므로 여러 개의 토스트를 동시에 띄울 수 없다.</strong></p>
<p><strong>4. 토스트 상태 (<code>const [message, setMessage] = useState(&#39;&#39;);</code>) 를 만든 컴포넌트 하위 자식이 모두 리렌더링된다.</strong></p>
<p>이러한 문제점을 해결할 수 있도록 하나씩 차근차근 아이디어를 내보자.</p>
<hr>
<h3 id="12-문제-해결-가설-수립">1.2. 문제 해결 가설 수립</h3>
<p><strong>1. 상태가 사용하는 쪽 컴포넌트에 있기 때문에 다른 곳에서 재사용하기 어렵다.</strong></p>
<p>→ 사용하는 쪽에서 <code>함수 호출</code> 처럼 토스트를 띄우고 내릴 수 있다면, 재사용도 가능해지고 try catch 블록에서 에러 처리도 쉽게 할 수 있지 않을까?</p>
<p><strong>2. 토스트의 관리가 특정 컴포넌트에 강하게 결합되어 있다.</strong></p>
<p>→ 토스트가 뜨는 곳은 고정되어있고, 이를 한 곳에서 관리할 수 있으면 좋을 것 같다.</p>
<p><strong>3. 하나의 상태만 관리하므로 여러 개의 토스트를 동시에 띄울 수 없다.</strong></p>
<p>→ 토스트의 정보를 배열로 관리한다면 해소할 수 있을 것 같다.</p>
<p><strong>4. 토스트 상태 (<code>const [message, setMessage] = useState(&#39;&#39;);</code>) 를 만든 컴포넌트 하위 자식이 모두 리렌더링된다.</strong></p>
<p>→ Context API 로 context 를 구독하는 컴포넌트만 리렌더링되도록 영향 범위를 쪼개보자</p>
<p>→ 또한 value 값을 고정시킬 수 있다면, 구독하고있는 컴포넌트들조차 리렌더링을 발생시키지 않을 수 있을 것이다.</p>
<hr>
<h2 id="2-context-api-로-문제-해소해보기">2. Context API 로 문제 해소해보기</h2>
<p>Context API 를 활용하여 위 아이디어를 적용해보면 해당 문제점들을 해소할 수 있다.</p>
<p><strong>👇 ToastMessage 컴포넌트 구현 코드</strong></p>
<pre><code class="language-jsx">
interface ToastMeesageProps {
  message: string;
  type: MessageType;
  onClose: () =&gt; void;
}

function ToastMessage({ message, type, onClose }: ToastMeesageProps) {
// ToastMessage 컴포넌트는 onClose 함수를 받아 3초 뒤 실행시킨다.
  setTimeout(() =&gt; {
    if (onClose) {
      onClose();
    }
  }, 3000);
  return (
    &lt;S.Container&gt;
      &lt;S.Wrapper type={type}&gt;
        &lt;S.ErrorText&gt;{message}&lt;/S.ErrorText&gt;
      &lt;/S.Wrapper&gt;
    &lt;/S.Container&gt;
  );
}

export default ToastMessage;
</code></pre>
<p><strong>👇 ToastProvider 구현 코드</strong></p>
<pre><code class="language-tsx">export interface ToastItem {
  id: string;
  type: ToastType;
  message: string;
}

interface ToastContextType {
  showToast: (message: string, type: ToastType) =&gt; void;
  removeToast: (id: string) =&gt; void;
}

export const ToastContext = createContext&lt;ToastContextType | undefined&gt;(
  undefined
);

export function ToastProvider({ children }: { children: ReactNode }) {
// ToastItem 객체를 배열로 담고있는 상태를 만들어 여러 개의 토스트를 동시에 순차적으로 띄울 수 있도록 한다.
  const [toasts, setToasts] = useState&lt;ToastItem[]&gt;([]);

  const showToast = useCallback((message: string, type: ToastType) =&gt; {
    const id = Math.random().toString();
    setToasts((prev) =&gt; [...prev, { id, message, type }]);
  }, []);

  const removeToast = useCallback((id: string) =&gt; {
    setToasts((prev) =&gt; prev.filter((toast) =&gt; toast.id !== id));
  }, []);

// children 과 toast 상태 영향 범위를 분리한다.
  return (
    &lt;ToastContext.Provider value={{ showToast, removeToast }}&gt;
      {children}
      &lt;S.ToastContainer&gt;
        {toasts.map((toast) =&gt; (
          &lt;ToastMessage
            key={toast.id}
            message={toast.message}
            type={toast.type}
            onClose={() =&gt; removeToast(toast.id)}
          /&gt;
        ))}
      &lt;/S.ToastContainer&gt;
    &lt;/ToastContext.Provider&gt;
  );
}

export function useToastContext() {
  const context = useContext(ToastContext);
  if (!context) {
    throw new Error(&#39;컨텍스트는 Provider 내부에서만 사용할 수 있습니다.&#39;);
  }
  return context;
}
</code></pre>
<p>위 코드를 보며 앞에서 언급했던 세 가지 문제점을 하나씩 다시 짚어보고, 지금 구조에서 어떻게 해결했는지 정리해보자.</p>
<hr>
<h3 id="21-상태가-사용하는-쪽-컴포넌트에-있기-때문에-재사용성이-떨어진다">2.1. 상태가 사용하는 쪽 컴포넌트에 있기 때문에 재사용성이 떨어진다</h3>
<p>초기의 단순한 구현에서는 <code>useState</code>로 관리하는 <code>message</code> 상태가 개별 컴포넌트 안에 존재했다.</p>
<p>따라서 다른 컴포넌트에서 토스트를 띄우고 싶으면 매번 상태를 따로 만들고, 조건부 렌더링 로직도 반복해서 작성해야 했다.</p>
<p><strong>해결 방법</strong></p>
<ul>
<li><code>ToastProvider</code> 내부에서 <code>toasts</code> 배열을 상태로 관리한다.</li>
<li><code>showToast</code>, <code>removeToast</code> 함수를 Context를 통해 하위 컴포넌트로 내려주어, <strong>어디서든 함수 호출 한 줄만으로 토스트를 띄우고 내릴 수 있도록 한다.</strong>.</li>
</ul>
<p>즉, 사용하는 쪽에서는 상태 관리 코드를 몰라도 되고, 단순히:</p>
<pre><code class="language-jsx">function Example() {
  const { showToast } = useToastContext();

  const handleAsync = async () =&gt; {
    try {
      // 비동기 요청 블록
    } catch (e) {
      showToast(&#39;비동기 요청에 실패하였습니다.&#39;, &#39;error&#39;);
    }
  };</code></pre>
<p>이처럼 호출만 하면 된다. 재사용성이 크게 증가했다.</p>
<hr>
<h3 id="22-토스트-관리가-특정-컴포넌트에-강하게-결합되어-있다">2.2. 토스트 관리가 특정 컴포넌트에 강하게 결합되어 있다.</h3>
<p>특정 UI 안에서 토스트를 직접 렌더링할 경우, 토스트는 그 컴포넌트의 라이프사이클에 묶여버린다. 토스트는 사실 <code>전역 UI 알림</code>에 가깝기 때문에, 특정한 페이지·뷰보다 <strong>앱 전역의 고정된 위치에서 출력되는 게 자연스럽다고 생각했다</strong>.</p>
<pre><code class="language-tsx">return (
  &lt;ToastContext.Provider value={{ showToast, removeToast }}&gt;
        {children}
        &lt;S.ToastContainer&gt;
          {toasts.map((toast) =&gt; (
            &lt;ToastMessage
              key={toast.id}
              message={toast.message}
              type={toast.type}
              onClose={() =&gt; removeToast(toast.id)}
            /&gt;
          ))}
        &lt;/S.ToastContainer&gt;
    &lt;/ToastContext.Provider&gt;
)</code></pre>
<p><strong>해결 방법</strong></p>
<ul>
<li><p><code>ToastProvider</code> 하단에 <code>&lt;S.ToastContainer&gt;</code>를 두고, 여기서만 토스트를 실제 렌더링한다.</p>
</li>
<li><p><code>children</code>은 본래의 화면이고, 토스트가 렌더링되는 <code>&lt;S.ToastContainer&gt;</code> 와 형제 관계에 둠으로써 영향을 분리시킨다.</p>
</li>
<li><p><code>ToastMessage</code> 는 <code>onClose</code> 를 prop 으로 받고 3초 후 스스로 <code>onClose()</code>를 호출해 배열에서 제거된다.  </p>
</li>
</ul>
<p>이렇게 하면 토스트 메시지는 항상 동일한 위치에 고정적으로 렌더링되고, 특정 컴포넌트와 결합되지 않는다.
토스트 관리의 관심사를 전역 컨텍스트로 위임하여 컴포넌트 로직과 UI 로직을 분리한 것이다.</p>
<hr>
<h3 id="23-하나의-상태만-관리해서-여러-개의-토스트를-동시에-띄울-수-없다">2.3. 하나의 상태만 관리해서 여러 개의 토스트를 동시에 띄울 수 없다.</h3>
<p>초기 구현에서는 <code>message</code>라는 단일 문자열만 상태로 관리했을 뿐이라, 토스트를 하나 더 띄우면 기존 토스트가 덮어쓰기 되었다.  이런 방식은 연속적으로 네트워크 요청이 발생하는 경우 UX가 좋지 않다.</p>
<p><strong>해결 방법</strong></p>
<pre><code class="language-tsx">export interface ToastItem {
  id: string;
  type: ToastType;
  message: string;
}

...

export function ToastProvider({ children }: { children: ReactNode }) {
  const [toasts, setToasts] = useState&lt;ToastItem[]&gt;([]);

...

const removeToast = useCallback((id: string) =&gt; {
    setToasts((prev) =&gt; prev.filter((toast) =&gt; toast.id !== id));
  }, []);
</code></pre>
<ul>
<li>토스트 상태를 배열(<code>ToastItem[]</code>)로 관리한다.  </li>
<li><code>showToast</code>는 새로운 토스트 객체를 만들어 배열에 push하고, <code>removeToast</code>는 특정 id를 가진 토스트를 제거한다.  </li>
<li>따라서 여러 개의 토스트 메시지를 동시에 띄우거나, 순차적으로 표시하는 것이 가능하다.  </li>
</ul>
<hr>
<h3 id="24-토스트-상태-변경-시-자식-컴포넌트까지-리렌더링되는-문제">2.4. 토스트 상태 변경 시 자식 컴포넌트까지 리렌더링되는 문제</h3>
<p>앞에서 말한 것처럼 상태를 한 컴포넌트 안에 두면, 토스트가 뜰 때 해당 컴포넌트 하위 트리가 모두 다시 그려진다.</p>
<p>예를 들어 <code>ComponentWithToast</code> 아래에 무겁거나 복잡한 UI가 있으면, 단순히 토스트만 띄워도 매번 같은 UI들이 리렌더링된다.</p>
<p><strong>해결 방법</strong></p>
<ul>
<li><code>ToastProvider</code> 내부에서 토스트 전용 상태(<code>toasts</code>)를 관리하고, <code>children</code>과 토스트 렌더링 영역을 <strong>형제 관계</strong>로 둔다.</li>
<li>이 구조에서 children은 토스트와 독립적으로 렌더링되므로, 토스트가 추가되거나 제거될 때 children은 영향받지 않는다.</li>
</ul>
<p>즉, 토스트 렌더링 범위를 <code>ToastProvider</code> 내부의 별도 <code>&lt;S.ToastContainer&gt;</code>로만 제한함으로써, <strong>토스트 상태 변경이 전체 UI 리렌더링으로 번지지 않도록 최적화</strong>한 것이다.</p>
<hr>
<p>이제 구조상 깔끔해졌지만, 아직 문제가 하나 있다.</p>
<p>바로 <strong>&quot;토스트가 뜰 때마다 <code>showToast</code> 함수를 사용하는 모든 컴포넌트가 리렌더링되는 것&quot;</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/03276999-489c-4dab-897b-75037aad201a/image.gif" alt=""></p>
<p>왜 이런 일이 일어날까? 분명 <code>showToast</code>, <code>removeToast</code>를 <code>useCallback</code>으로 감쌌고, 토스트 영역과 <code>children</code>을 분리했는데도 리렌더링이 발생한다.</p>
<p>그 이유는 Provider의 value 때문이다.</p>
<p><strong>ContextAPI 는 value 변경 시 해당 Context를 구독(<code>useContext</code>)하고 있는 모든 하위 컴포넌트를 리렌더링한다.</strong></p>
<p>showToast와 removeToast 의 참조값은 고정해주었지만,</p>
<p><code>&lt;ToastContext.Provider value={{ showToast, removeToast }}&gt;</code></p>
<p>위처럼 작성하면 매번 <code>{ showToast, removeToast }</code>라는 <strong>새로운 객체를 넘겨주는 것</strong>이기 때문에 Context를 구독하는 모든 컴포넌트가 리렌더링되는 것이다.</p>
<p>이 문제를 해결하기 위해 value를 <code>useMemo</code>로 감싸 참조값을 고정했다.</p>
<pre><code class="language-tsx">const valueToast = useMemo(() =&gt; {
    return { showToast, removeToast };
  }, [removeToast, showToast]);

  return (
    &lt;ToastContext.Provider value={valueToast}&gt;
      {children}</code></pre>
<hr>
<h2 id="3--최종-toastprovider--usememo로-참조값-고정">3.  최종 ToastProvider — useMemo로 참조값 고정</h2>
<pre><code class="language-jsx">import {
  createContext,
  useState,
  ReactNode,
  useCallback,
  useContext,
  useMemo,
} from &#39;react&#39;;
import ToastMessage from &#39;../components/common/ToastMessage&#39;;
import styled from &#39;@emotion/styled&#39;;

type ToastType = &#39;error&#39; | &#39;info&#39;;

export interface ToastItem {
  id: string;
  type: ToastType;
  message: string;
}

interface ToastContextType {
  showToast: (message: string, type: ToastType) =&gt; void;
  removeToast: (id: string) =&gt; void;
}

export const ToastContext = createContext&lt;ToastContextType | undefined&gt;(
  undefined
);

export function ToastProvider({ children }: { children: ReactNode }) {
  const [toasts, setToasts] = useState&lt;ToastItem[]&gt;([]);

  const showToast = useCallback((message: string, type: ToastType) =&gt; {
    const id = crypto.randomUUID();
    setToasts((prev) =&gt; [...prev, { id, message, type }]);
  }, []);

  const removeToast = useCallback((id: string) =&gt; {
    setToasts((prev) =&gt; prev.filter((toast) =&gt; toast.id !== id));
  }, []);

  const valueToast = useMemo(() =&gt; {
    return { showToast, removeToast };
  }, [removeToast, showToast]);

  return (
    &lt;ToastContext.Provider value={valueToast}&gt;
      {children}
      &lt;S.ToastContainer&gt;
        {toasts.map((toast) =&gt; (
          &lt;ToastMessage
            key={toast.id}
            message={toast.message}
            type={toast.type}
            onClose={() =&gt; removeToast(toast.id)}
          /&gt;
        ))}
      &lt;/S.ToastContainer&gt;
    &lt;/ToastContext.Provider&gt;
  );
}

export function useToastContext() {
  const context = useContext(ToastContext);
  if (!context) {
    throw new Error(&#39;컨텍스트는 Provider 내부에서만 사용할 수 있습니다.&#39;);
  }
  return context;
}

const S = {
  ToastContainer: styled.div`
    position: fixed;
    bottom: 40px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1000;
    display: flex;
    gap: 8px;
    flex-direction: column;
  `,
};
</code></pre>
<p>이제 최종 <code>ToastProvider</code>는 다음과 같은 장점을 갖는다:</p>
<ul>
<li><strong>불필요한 상태 최소화</strong>: 전역 provider가 토스트 메시지를 통합 관리한다.</li>
<li><strong>손쉬운 API 제공</strong>: <code>showToast(&quot;저장 성공&quot;, &quot;info&quot;)</code> 호출만으로 어디서든 토스트 사용이 가능하다.</li>
<li><strong>리렌더링 최적화</strong>: <code>useMemo</code>로 Context value의 참조값을 고정하여 불필요한 리렌더링을 차단했다.</li>
<li><strong>다중 토스트 처리</strong>: 배열 형태로 여러 개의 토스트 메시지를 동시에 관리할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/1cec9542-22cf-4d59-b0fc-1eec42217548/image.gif" alt=""></p>
<hr>
<p>단순해 보이는 토스트 구현에서도 고민하고 배울 점이 있었다.</p>
<p>리액트가 제공하는 생명 주기 안에서 어떻게 최대한 선언적이고 최적화된 코드를 작성할 수 있는지 고민하는 것은 늘 새롭고 즐거운 일인 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 장바구니 미션 주간 회고 05/26~06/02]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EB%AF%B8%EC%85%98-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-05260602</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EB%AF%B8%EC%85%98-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-05260602</guid>
            <pubDate>Mon, 02 Jun 2025 14:39:16 GMT</pubDate>
            <description><![CDATA[<p>이번 주간은 지난 상품 목록 리팩토링과 장바구니 1단계 반영 및 2단계 기능 구현 시작, 각종 스터디와 일정 등...
맞물린 스케쥴이 너무 많아서 회고는 간단하게 작성해보려 한다. ㅠㅠ</p>
<hr>
<h1 id="1-장바구니-미션">1. 장바구니 미션</h1>
<hr>
<blockquote>
<p>1단계 PR 링크: <a href="https://github.com/woowacourse/react-shopping-cart/pull/344">https://github.com/woowacourse/react-shopping-cart/pull/344</a></p>
</blockquote>
<h2 id="11-페어-미션">1.1. 페어 미션</h2>
<h3 id="111-상태-설계-의도">1.1.1. 상태 설계 의도</h3>
<p><strong>원본 상태 : <code>cartListData</code>와 <code>SelectionMap</code></strong>
cartListData는 서버에서 받아온 전체 장바구니 데이터를 의미하므로, 변경되지 않는 원본 상태로 관리했다.
selectionMap은 각 장바구니 아이템의 체크 여부를 관리하는 상태인데 이 상태는 cartListData에 의존적이긴 하지만, 아래와 같은 이유로 파생 상태가 아닌 독립적인 원본 상태로 관리했다.</p>
<ul>
<li><code>cartListData</code>가 갱신되더라도 리렌더링 전의 체크 상태를 유지하기 위해 기존에 존재하던 <code>id</code>의 선택 여부(<code>boolean</code>)는 유지되어야 한다.</li>
<li>체크박스 클릭과 같은 <strong>사용자 인터랙션에 따라 동적으로 변경되는 상태이다.</strong></li>
<li>선택 여부는 컴포넌트 리렌더링에도 영향을 주기 때문에, <code>setState</code>에 의해 직접 관리할 수 있어야 한다고 생각하였다.</li>
</ul>
<hr>
<p>*<em>파생 상태 : 주문 금액, 배송비, 총 결제 금액, 전체 선택 여부(isSelectAll) 등 *</em></p>
<p>이 값들은 cartListData와 selectionMap만으로 계산 가능한 값들이므로, 별도로 상태로 저장하지 않고 파생 상태로 관리하였다.</p>
<hr>
<p><strong>useOrderListContext() 작성</strong>
cartListData와 selectionMap 이 페이지에 있는 모든 컴포넌트에서 사용하고 있다.
또, cartListData와 selectionMap은 장바구니 페이지뿐만 아니라 주문 확인 페이지 등 라우터 간에도 공유되어야 하는 상태다.</p>
<p>따라서 Context API를 사용해 cartListData와 selectionMap을 전역 상태로 사용할 수 있게 하는 useOrderListContext를 구현하였다.</p>
<hr>
<p>*<em>selectionMap 상태 구조 *</em></p>
<p>우리는 Record&lt;id, boolean&gt; 형태의 selectionMap 구조를 선택하였다.</p>
<p>이 방식은 각 항목의 선택 여부를 개별적으로 명시할 수 있어 체크/해제 상태를 더 명확하게 추적할 수 있고, 
배열을 사용해 포함 여부를 일일이 탐색하는 것보다 성능 면에서도 효율적이라고 판단했다. </p>
<p>또한 이후 상태를 부분적으로 병합하면서 유지해야 하는 상황에서 boolean 맵 구조가 더 적합하다고 생각했다.</p>
<p>예를 들어, 서버로부터 cartListData가 새롭게 갱신되었을 때, 기존에 존재하던 항목의 체크 상태는 유지하고,
새로 추가된 항목에 대해서만 기본값(true)으로 초기화해야한다.</p>
<p>이러한 요구를 반영하기 위해 useEffect 내부에서 함수형 업데이트 방식을 사용하였다.</p>
<pre><code class="language-tsx">
export const useOrderListContext = () =&gt; {
  const { selectionMap, setSelectionMap } =
    useContext(OrderListContext);
  if (!selectionMap) {
    throw new Error(
      &quot;useOrderListContext must be used within an OrderListProvider&quot;
    );
  }

  useEffect(() =&gt; {
    if (!cartListData) return;

    setSelectionMap((prev) =&gt; {
      const nextMap: Record&lt;string, boolean&gt; = {};
      for (const cart of cartListData) {
        nextMap[cart.id] = prev[cart.id] ?? true;
      }
      return nextMap;
    });
  }, [cartListData, setSelectionMap]);

  return {
    selectionMap,
    setSelectionMap,
  };
};</code></pre>
<p>기존의 selectionMap을 기준으로 새로운 cartListData를 순회하면서, 기존 상태가 존재하는 항목은 그대로 유지하고, 
존재하지 않는 항목에 대해서만 true를 설정하도록 하는 코드이다.</p>
<p>이렇게하면 useEffect는 렌더링 이후 실행을 보장하므로 초기 데이터 <code>undefined</code> 문제도 피할 수 있으면서,
컴포넌트가 리렌더링되어도 이전 선택값을 유지할 수 있다.</p>
<p>이를 통해 사용자의 선택 상태를 안정적으로 보존하면서도, 불필요한 리렌더링을 최소화할 수 있었다.</p>
<hr>
<h2 id="12-장바구니-주간-수업">1.2. 장바구니 주간 수업</h2>
<h3 id="121-ui--fstate--상태를-잘-설계해보는-활동">1.2.1. UI = f(state) : 상태를 잘 설계해보는 활동</h3>
<table>
<thead>
<tr>
<th><strong>변하는 UI 요소</strong></th>
<th><strong>이 UI가 언제 변하나요?</strong></th>
<th><strong>UI 반영을 위해서 어떤 데이터가 필요한가요? (ex. 장바구니 상품 목록의 개수, 배송비)</strong></th>
</tr>
</thead>
<tbody><tr>
<td>장바구니 리스트</td>
<td>페이지를 처음 열었을 때</td>
<td>장바구니 상품 목록(get)</td>
</tr>
<tr>
<td>장바구니 상품 개수</td>
<td>장바구니에 아이템을 담았을 때<br>장바구니의 아이템을 삭제할 때<br>수량이 1개일 때 수량 감소 버튼을 누를 때</td>
<td>장바구니 상품 목록(get, post, delete)</td>
</tr>
<tr>
<td>현재 장바구니 상품 개수 안내 문구</td>
<td>장바구니 상품 개수가 변경될 때</td>
<td>‘장바구니 상품 목록’의 파생 상태(length)</td>
</tr>
<tr>
<td>장바구니 체크 리스트</td>
<td>최초 렌더링 시<br>전체 선택을 눌렀을 때,<br>개별 체크를 선택/해제할 때</td>
<td>장바구니 목록(상태는 [{cartItem, isChecked}])<br>(품절 상태의 아이템은 disabled)</td>
</tr>
<tr>
<td>주문 금액</td>
<td>장바구니 상품의 개수가 변할 때<br>장바구니 상품의 수량이 변할 때</td>
<td>장바구니 목록(cartitem.quantity, cartitem.product.price 데이터)</td>
</tr>
<tr>
<td>배송비</td>
<td>주문 금액이 100,000원 미만/이상일 때</td>
<td>주문 금액</td>
</tr>
<tr>
<td>총 결제 금액</td>
<td>주문 금액이 변경될 때<br>배송비가 변경될 때</td>
<td>주문 금액 + 배송비</td>
</tr>
<tr>
<td>주문 확인 버튼</td>
<td>장바구니 체크 리스트가 1개 이상일 때<br>장바구니 체크 리스트가 0개 일 때</td>
<td>장바구니 체크 리스트 데이터</td>
</tr>
</tbody></table>
<p>각 UI가 언제 변하고 어떤 데이터가 필요하며 각 데이터는 상태/파생 상태/의존 상태 중 어떻게 관리되어야할지 생각해보는 활동을 하였다.</p>
<h3 id="122-setstate-는-비동기-함수다">1.2.2. setState() 는 비동기 함수다</h3>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/0466424c-5901-4415-b6b0-bcaefd93a19d/image.png" alt=""></p>
<p><code>setState()</code> 자체는 코드 실행 줄에 push 되고 pop 되지만,
setState() 의 내부 콜백은 리액트 스케쥴링상 이벤트 루프의 다음 사이클(혹은 React의 내부 큐)에 예약한다.</p>
<p>이러한 관점에서 <code>setState()</code> 는 비동기 함수다. 라고 말할 수 있다.
하지만 이는 리액트의 스케쥴링상 비동기적으로 작동하는 것이기 때문에, 자바스크립트에서 비동기를 처리하는 방식인 <code>Promise</code> 와는 다르다.</p>
<p>동기적으로 실행되는 다른 코드들이 모두 끝난 후, 리액트가 예약해둔 state 변경과 리렌더링이 실행되는 것이고,
이 과정은 자바스크립트의 마이크로태스크/매크로태스크 큐와는 별개로, 리액트가 자체적으로 관리하는 업데이트 큐에서 처리된다.</p>
<h3 id="123-개발자-도구-학습-debugger">1.2.3. 개발자 도구 학습 (debugger)</h3>
<p>개발자 도구 탭에서 Profiler 탭에서 컴포넌트의 렌더 시간과 순서들을 확인할 수 있다.
debugger를 통해 내 프로그램의 실제 흐름을 정확히 파악하는 것도 좋은 것 같다.</p>
<h3 id="124-추천-학습-키워드">1.2.4. 추천 학습 키워드</h3>
<table>
<thead>
<tr>
<th>JS</th>
<th>실행 컨텍스트, 클로저, call stack, heap</th>
</tr>
</thead>
<tbody><tr>
<td>브라우저</td>
<td>브라우저 렌더링, 이벤트 루프</td>
</tr>
<tr>
<td>React</td>
<td>렌더링(render/commit), 불변성</td>
</tr>
</tbody></table>
<hr>
<h1 id="2-스터디">2. 스터디</h1>
<hr>
<h2 id="21-pr-리뷰-스터디">2.1. PR 리뷰 스터디</h2>
<p>이번 주차는 페이먼츠 모듈 미션 2단계를 리뷰했다.
리뷰어의 타입스크립트 심화 피드백을 보고 타입스크립트를 공부해볼 수 있었다.</p>
<h3 id="objectentries의-타입-추론-한계와-유틸-함수-개선">Object.entries의 타입 추론 한계와 유틸 함수 개선</h3>
<p>리뷰 과정에서 가장 흥미로웠던 부분은 <strong>Object.entries</strong>의 타입 추론 한계와, 이 피드백을 받고 개선하기 위해 크루가 만든 <code>objectEntries</code> 유틸 함수였다.</p>
<h4 id="왜-objectentries는-key-타입을-string으로만-추론할까">왜 Object.entries는 key 타입을 string으로만 추론할까?</h4>
<p>타입스크립트에서 <code>Object.entries(obj)</code>는 항상 <code>[string, any][]</code> 타입을 반환한다.<br>이 때문에 아래와 같은 코드에서 타입 안정성이 떨어진다.</p>
<pre><code class="language-ts">const obj = { a: 1, b: 2 };
Object.entries(obj).forEach(([key, value]) =&gt; {
// key의 타입이 string이므로 obj[key]에 타입 에러가 발생할 수 있다.
});</code></pre>
<p>이런 현상은 타입스크립트가 <strong>구조적 타이핑</strong>(structural typing)이라는 특징을 갖고 있기 때문이다.<br>즉, 타입스크립트는 객체 타입이 &quot;열려&quot; 있다고 가정한다.</p>
<p>런타임에는 타입에 명시되지 않은 속성(잉여 속성)이 추가될 수 있기 때문에,<br><code>Object.keys</code>나 <code>Object.entries</code>는 항상 string 기반의 넓은 타입으로 반환한다.</p>
<p>예를 들어,</p>
<pre><code class="language-ts">interface AB { a: string; b: string; }
const obj: AB = { a: &#39;x&#39;, b: &#39;y&#39;, c: &#39;z&#39; }; // c는 타입에는 없지만, 런타임에는 존재 가능</code></pre>
<p>만약 타입스크립트가 <code>Object.keys(obj)</code>의 반환 타입을 <code>&#39;a&#39; | &#39;b&#39;</code>로 좁혀버리면,<br>런타임에 존재하는 &#39;c&#39;를 놓치게 되고, 타입과 실제 값이 불일치하는 문제가 생길 수 있다.</p>
<h4 id="타입을-좁히는-유틸-함수의-필요성">타입을 좁히는 유틸 함수의 필요성</h4>
<p>하지만, <strong>&quot;이 객체는 닫힌 구조(타입에 정의된 키만 존재)&quot;</strong>임을 개발자가 확신할 수 있는 경우도 있다.<br>예를 들어, 카드 번호 입력 필드처럼 구조가 초기화 함수에서 100% 결정되고,<br>이후에 동적으로 키가 추가/삭제되지 않는 경우다.</p>
<p>이런 상황에서는 타입을 좁혀서 더 안전하고 명확한 코드를 작성할 수 있다.
실제로, 크루는 제네릭을 활용해 객체의 키와 값 타입을 정확하게 추론하는 유틸 함수를 만들어 적용했다.
이 유틸 함수는 객체의 타입 정보를 최대한 보존하면서,
Object.entries와 유사하게 동작하도록 설계된 것이 인상적이었다.</p>
<p>이 함수는 제네릭 T를 받아,</p>
<ul>
<li>key는 <code>keyof T</code> (즉, 타입에 정의된 키만)</li>
<li>value는 <code>T[keyof T]</code> (타입에 정의된 값들의 유니언)</li>
</ul>
<p>으로 반환 타입을 좁혀주는 함수였다.</p>
<h4 id="왜-타입스크립트는-기본적으로-이렇게-좁히지-않을까">왜 타입스크립트는 기본적으로 이렇게 좁히지 않을까?</h4>
<p>타입스크립트는 <strong>런타임 안전성</strong>을 최우선으로 한다.<br>자바스크립트 객체는 언제든 동적으로 속성이 추가/삭제될 수 있기 때문에,<br>기본적으로는 넓은 타입(string)을 반환해 잠재적 런타임 오류를 방지한다.</p>
<p>개발자가 &quot;이 객체는 닫힌 구조임을 보증할 수 있다&quot;고 판단하는 경우에만,<br>이런 유틸 함수를 사용해 타입을 좁히는 것이 올바른 설계다.</p>
<h4 id="결론">결론</h4>
<ul>
<li>타입스크립트의 구조적(공변적) 타입 시스템은 안전을 위해 타입을 넓게 잡는다.</li>
<li>하지만, <strong>&quot;이 객체는 닫힌 구조&quot;</strong>임을 개발자가 확신할 수 있다면,<br>유틸 함수를 통해 타입을 좁혀 더 명확하고 안전한 코드를 작성할 수 있다.</li>
</ul>
<hr>
<h4 id="참고-자료">참고 자료</h4>
<ul>
<li><a href="https://medium.com/@yujso66/%EB%B2%88%EC%97%AD-%EC%99%9C-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-object-keys%EC%9D%98-%ED%83%80%EC%9E%85%EC%9D%84-%EC%A0%81%EC%A0%88%ED%95%98%EA%B2%8C-%EC%B6%94%EB%A1%A0%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%A0%EA%B9%8C%EC%9A%94-477253b1aafa">왜 타입스크립트는 Object.keys의 타입을 적절하게 추론하지 못할까요?</a></li>
<li><a href="https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208">관련 타입스크립트 PR 논의</a></li>
<li><a href="https://www.slash.page/libraries/common/utils/src/object/object-entries.i18n">Slash 라이브러리 object-entries 유틸</a></li>
</ul>
<hr>
<p>타입스크립트의 타입 시스템은 쉽지 않지만,<br>이런 심화 내용을 이해하고 직접 적용해보는 경험이 정말 큰 도움이 되는 것 같다.<br>다음 미션에서는 더 깊이 있게 활용해보고 싶다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 상품 목록 미션 주간 회고 05/19~05/25]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%83%81%ED%92%88-%EB%AA%A9%EB%A1%9D-%EB%AF%B8%EC%85%98-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-05190525</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%83%81%ED%92%88-%EB%AA%A9%EB%A1%9D-%EB%AF%B8%EC%85%98-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-05190525</guid>
            <pubDate>Mon, 26 May 2025 08:31:18 GMT</pubDate>
            <description><![CDATA[<h1 id="1-상품-목록-미션">1. 상품 목록 미션</h1>
<blockquote>
<p>1단계 PR 링크: <a href="https://github.com/woowacourse/react-shopping-products/pull/109">https://github.com/woowacourse/react-shopping-products/pull/109</a>
2단계 PR 링크: <a href="https://github.com/woowacourse/react-shopping-products/pull/130">https://github.com/woowacourse/react-shopping-products/pull/130</a></p>
</blockquote>
<h2 id="11-1단계-리팩토링">1.1. 1단계 리팩토링</h2>
<hr>
<h3 id="에러-핸들링---errorboundary와-전역-에러-관리의-한계"><strong>에러 핸들링</strong> - ErrorBoundary와 전역 에러 관리의 한계</h3>
<p>이번 미션에서는 페어와 ErrorBoundary를 사용하지 않고 에러를 처리해보자는 목표로 시작했다. &#39;리액트에서의 에러 처리는 무조건 ErrorBoundary 를 써야지!&#39; 하는 것보단 왜 에러 바운더리를 많이 쓰는 건지 에러를 직접 핸들링해보고 불편함과 필요성을 몸소 느껴보기 위함이었다.</p>
<p>처음에는 ErrorProvider를 만들어 context로 에러 상태를 전역에서 관리해보았지만, 실제로는 렌더링 에러는 ErrorBoundary로 처리하는 것이 훨씬 선언적이고 관리가 쉽다는 것을 깨달았다.</p>
<p>리렌더링 문제와 여러 컴포넌트에서 던지는 에러를 하나의 Provider에서 관리하는 것에 불편함을 느꼈기 때문이다.</p>
<p>그렇게 에러바운더리를 사용하여 리팩토링을 진행하였는데, 이 과정에서</p>
<ul>
<li>ErrorBoundary는 컴포넌트 렌더링 시 발생하는 에러만 잡을 수 있다는 점</li>
<li>try-catch에서 발생하는 실행 중 에러는 잡지 못한다는 점</li>
<li>전역 에러 상태를 context로 관리하면, 에러 상태가 바뀔 때마다 하위 컴포넌트가 모두 리렌더링되는 비효율이 있다는 점</li>
</ul>
<p>을 직접 경험할 수 있었다.</p>
<p>그래서 렌더링 에러는 ErrorBoundary로,
함수 실행 중 에러(try-catch)는 별도의 함수형 토스트(showToast)로 처리하는 이원화된 구조가 가장 현실적이라는 결론을 내렸다.</p>
<p>(참고로, showToast는 현재 여러 개의 토스트를 동시에 쌓아 띄우는 기능이 없어 개선이 필요하다고 느꼈다. 매우매우 간소화시켜 직접 DOM 조작을 하는 리액트스럽지 않은 단순함수로 구현하였기 때문에...)</p>
<hr>
<h3 id="context-api---context-작성의-활용"><strong>Context API</strong> - Context 작성의 활용</h3>
<p>그동안 Context API를 사용할 때는 Provider만 신경 썼는데,
이번 미션을 통해 Context 객체 자체의 설계와 관리로 고유한 훅을 만들 수 있음을 학습했다.</p>
<p>특히,</p>
<ul>
<li>APIDataProvider로 Cart뿐 아니라 다양한 API 데이터를 키로 관리할 수 있게 리팩토링한 경험</li>
<li>Context의 value 구조와 확장성을 고민한 경험</li>
</ul>
<p>이 가장 좋은 학습 경험이었던 것 같다.</p>
<p>시지프의 API Provider 구조를 참고하며,
Context를 어떻게 설계하면 확장성과 유지보수성이 좋아지는지 더 깊게 고민하였고,</p>
<p>이번 미션의 프로그래밍 요구사항이었던</p>
<blockquote>
<ul>
<li>서버 API 통신 결과를 Single Source of Truth (SSOT) 원칙에 따라 관리할 수 있도록, 커스텀 훅을 직접 개발한다.</li>
</ul>
</blockquote>
<ul>
<li>GET method 를 사용하는 모든 API 에 이 커스텀 훅을 적용한다.
GET /cart-items , GET /products API 를 통일된 인터페이스로 data fetching 할 수 있어야 한다.
ex) useData, useResource 등의 이름으로 선언할 수 있다.</li>
<li>반환값에는 데이터, 로딩 여부, 에러 정보 등이 포함되어야 한다.
Context API 를 활용한다. 단, API 마다 Provider 를 따로 만들지 않고, 하나의 Context 에서 관리할 수 있어야 한다.</li>
<li>Context API 사용으로 인한 렌더링 문제는 해결하지 않아도 된다. 문제점은 학습하여 인지하도록 한다.</li>
</ul>
<p>를 만족시킬 수 있었다.
최종적으로 완성된 API Provider는 이러했다.</p>
<pre><code class="language-js">...
const APIContext = createContext&lt;{
  state: APIStateMap;
  setState: React.Dispatch&lt;React.SetStateAction&lt;APIStateMap&gt;&gt;;
}&gt;({
  state: {},
  setState: () =&gt; {},
});

export function APIDataProvider({ children }: PropsWithChildren) {
  const [state, setState] = useState&lt;APIStateMap&gt;({});

  return (
    &lt;APIContext.Provider value={{ state, setState }}&gt;
      {children}
    &lt;/APIContext.Provider&gt;
  );
}
export function useAPIDataContext&lt;T&gt;({
  fetcher,
  name,
}: {
  fetcher: () =&gt; Promise&lt;T&gt;;
  name: string;
}) {
  const { state, setState } = useContext(APIContext);

  const request = useCallback(async () =&gt; {
    setState((prev) =&gt; ({
      ...prev,
      [name]: { data: null, loading: true, error: null },
    }));

    try {
      const result = await fetcher();
      setState((prev) =&gt; ({
        ...prev,
        [name]: { data: result, loading: false, error: null },
      }));
    } catch (e) {
      setState((prev) =&gt; ({
        ...prev,
        [name]: { data: null, loading: false, error: e },
      }));
      showToast(&#39;데이터 요청에 실패하였습니다.&#39;, &#39;error&#39;);
    }
  }, [name, setState]);

  useEffect(() =&gt; {
    if (!state[name]) request();
  }, [request, state, name]);

  const resource = state[name] || {
    data: null,
    loading: false,
    error: null,
  };

  return {
    data: resource.data as T | undefined,
    loading: resource.loading,
    error: resource.error,
    refetch: request,
  };
}
</code></pre>
<p>loading 상태와 error 상태도 반환해야한다는 요구 사항에 맞게 각 state에 error와 loading 값을 갖게되었다.</p>
<p>그리고 data의 name 을 키로 받아 해당하는 키에 data 객체를 저장하고 이를 Provider의 하나의 state에서 관리를 함으로써 여러 데이터를 하나의 Provider에서 각각 관리할 수 있도록 context를 추상화하였다.</p>
<hr>
<h3 id="비동기-데이터-suspense-그리고-wrappromise">비동기 데이터, Suspense, 그리고 wrapPromise</h3>
<p>리액트에서의 비동기 데이터 처리를 useEffect에서 하는 이유에 대해 먼저 고민하고 학습해보면서 Promise 에 이미 status 상태가 있는데 굳이 또 상태를 만들어줘야하는가에 대한 관점, 이러한 관점에서 강점을 갖는 suspense, use() 에 대해 학습했다.</p>
<p>또, react 버전이 18이었기 때문에 use()가 없어서 대신 넣어줬던 헬퍼함수 wrapPromise 에 대해서도 다시 공부했다.</p>
<p><strong>useEffect와 비동기 데이터</strong>
처음에는 &quot;컴포넌트 함수에 async를 붙일 수 없으니 useEffect에서 비동기 요청을 처리한다&quot; 정도로만 이해하고 있었다.</p>
<p>그런데 실제로 then으로 데이터를 받아 props로 넘기는 식으로 해보면,
Promise가 pending 상태로 한 번 넘겨지고, 값이 resolve된 후에는 setState로 갱신해야 한다는 점, 그렇게 setState가 반복되면 무한 루프에 빠질 수 있다는 것 등의 문제를 직접 겪으며, React 생명주기와 useEffect의 역할을 더 깊이 이해하게 되었다.</p>
<p>그리고 Promise 는 이미 pending, fullfilled, error 등의 status 를 반환하고있는데 굳이 다시한 번 error, loading 상태를 만들어주어야하는가에 대해 의구심이 들었고 그로인해 promise의 status 에 따라 fallback UI 를 보여주는 suspense라는 것이 나왔음을 알게되었다.</p>
<p><strong>Suspense와 wrapPromise</strong>
Suspense를 이용하면 로딩 상태를 별도로 관리하지 않아도 선언적으로 비동기 UI 를 처리할 수 있다.</p>
<p>Suspense는 컴포넌트가 Promise를 throw하면 pending 상태를 감지해 fallback UI를 보여주면서 기다렸다가, Promise가 resolve되면 다시 컴포넌트를 렌더링한다.</p>
<p>이때 원래는 promise를 던져주는 역할이 use() 지만, use()는 리액트 19버전부터 나온 훅이기 때문에 use()를 대체할 수 있는 <code>wrapPromise</code> 헬퍼 함수를 <del>훔쳐왔</del> 작성해주었다.</p>
<p><code>wrapPromise</code>는 Promise의 상태를 추적해서, pending이면 Promise를 throw error면 에러를 throw, success면 데이터를 반환해주는 read() 함수를 반환해준다.</p>
<pre><code class="language-js">export function wrapPromise&lt;T&gt;(promise: Promise&lt;T&gt;) {
  let status = &#39;pending&#39;;
  let response: T;
  const suspender = promise.then(
    (res) =&gt; {
      status = &#39;success&#39;;
      response = res;
    },
    (err) =&gt; {
      status = &#39;error&#39;;
      response = err;
    }
  );

  const read = () =&gt; {
    switch (status) {
      case &#39;pending&#39;:
        throw suspender;
      case &#39;error&#39;:
        throw response;
      default:
        return response;
    }
  };
  return { read };
}</code></pre>
<p>suspender에 promise를 두고 만약 resolve 되지 않은 상태(then이 실행되기 전)에서 read함수를 실행하면 기본 status인 pending 케이스가 실행되어 suspender(pending 상태인 Promise)를 throw 해주는 것이다. </p>
<p>그리고 resolve 된 시점에 다시 read()가 호출되면 then()에서 status가 success나 error로 바뀐 후이므로 response(promise 가 resolve된 반환값)이 return 된다.</p>
<hr>
<h2 id="12-2단계-기능-구현">1.2. 2단계 기능 구현</h2>
<h3 id="신경-쓴-점">신경 쓴 점</h3>
<p>1. <strong>RTL 테스트 아이디 삭제 및 Role/aria-label 추가</strong></p>
<p>1단계 피드백에서 리뷰어가 &#39;testid 를 꼭 사용해야할까요?&#39; 라는 질문을 남겨주신만큼 testid 없이 findByRole ...등을 이용해 테스트를 작성해보았다. </p>
<p>처음에는 &quot;테스트를 위해 aria-label이나 role을 이렇게까지 붙여야 하나?&quot; 싶었지만,
생각해보니 스크린 리더나 크롤링 로봇 입장에서도 내 코드가 읽기 어려웠겠다는 생각이 들었다.</p>
<p>aria-label 이나 role, 시맨틱 태그 같은 것들을 신경쓰지 않고 막 작성하는 버릇이 있었는데 덕분에 웹 표준과 접근성에 대해 다시 한 번 고민해볼 수 있었다.</p>
<p>다만, role과 aria-label을 남용하는 건 오히려 웹 표준에 어긋날 수 있다는 점도 알게 되어 시맨틱 태그로 표현할 수 없는 경우에만 보완적으로 사용하도록 신경 썼다.</p>
<hr>
<p>2. <strong>범용적인 APIDataProvider(Context) 구현</strong></p>
<p>위에서 언급한 API Provider 코드 작성에도 신경썼다.
하지만 아직은 Cart만 이 컨텍스트를 사용하고 있다.</p>
<p>Product 데이터는 현재 구조상 ProductList에서만 알고 있는 게 더 맞다고 판단했기 때문이었다.</p>
<hr>
<p>3. <strong>빈 상태 UI 및 UX 개선</strong></p>
<p>이미지가 없을 때, 장바구니가 비어 있을 때 등 다양한 빈 상태(empty state) UI를 추가했다. 상품명이 한 줄을 넘어갈 때는 ...으로 말줄임 처리하여 가독성과 UI/UX도 신경 썼다.</p>
<hr>
<p>4. 범용성 있는 <strong>Counter 컴포넌트 구현</strong></p>
<ul>
<li>Counter에서 canBeZero 옵션을 활성화하면 수량이 1일 때도 - 버튼(휴지통 아이콘)이 활성화되어 클릭 시 해당 아이템이 삭제되도록 하였다.</li>
</ul>
<hr>
<p>5. <strong>Dropdown 키보드 접근성 및 Tab 인덱스</strong></p>
<p>드롭다운 옵션을 키보드로도 선택할 수 있도록 개선했고, 
autoFocus로 Tab 인덱스를 맞춰 사이트 전체를 키보드로도 이용할 수 있게 했습니다.</p>
<hr>
<h2 id="고민한-점">고민한 점</h2>
<p>1. <strong>Context 사용 시 리렌더링 이슈</strong></p>
<p>현재 API 구조상 Context로 데이터를 관리하다 보니 불필요한 리렌더링이 발생한다는 점이 고민되었다.</p>
<p>컴포넌트 전체를 React.memo로 감싸는 방법도 생각해봤지만, 메모이제이션 자체도 비용이라는 점이 떠올라 최적화 방향에 대해 고민이 많았습니다.</p>
<p>이런 문제는 TanStack Query(useQuery)와 같은 라이브러리를 사용하면
내부적으로 캐싱과 구독 범위 관리가 잘 되어 있어서 해소할 수 있다고 얼핏 들어보았지만...
(사용해보진 않았다!)</p>
<p>TanStack Query는 내부 구현이 복잡해 보여,
혹시 모든 컴포넌트를 메모이제이션하지 않고, 너무 복잡한 로직과 패턴을 도입하지 않으면서도 Context 기반 구조에서 리렌더링을 줄일 수 있는 다른 최적화 아이디어가 있을지 고민해보고 있다.</p>
<hr>
<h1 id="2-스터디">2. 스터디</h1>
<h2 id="21-threejs-스터디">2.1. three.js 스터디</h2>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/41f207fd-9e7e-4b53-8926-8d23d2a6e0c4/image.gif" alt=""></p>
<p>일단 키보드로 이동하고 마우스 드래그로 주변을 둘러볼 수 있는 기능을 구현했다!
6월 초까지 짬짬이 기능을 구현해서 하나의 씬을 완성하기로 했는데,</p>
<p>나는 <code>카멜 행성이를 찾아라</code> 사이트를 만들고 싶다.</p>
<p>미션과 다른 스터디를 병행하며 짬내서 부담없이 만들어보는 토이 사이트인만큼 그냥 내가 좋아하는 오브젝트들을 띄워두고, 해당 물체에 가까이 가면 동물의 숲처럼 대화할 수 있는 사이트를 만들고 싶다.</p>
<p>말은 이렇게해도, 점점 미션 일정도 빡빡해지고 하고있는 스터디도 많아서 2주동안 해당 목표를 달성할 수 있을지 걱정이긴 하지만... 그래도 재밌으니까 ㅎㅎ</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/90a6f4f1-1811-4c9f-aa6a-cb3d1ad8e17b/image.gif" alt=""></p>
<p>게더에서 함께 춤추는 three.js 크루들 ㅎㅎ</p>
<hr>
<h2 id="22-유연성-강화-스터디">2.2. 유연성 강화 스터디</h2>
<p>투두메이트를 시작했다!
<img src="https://velog.velcdn.com/images/dev-dino22/post/2dc46a43-463c-466e-a8ba-386761f70049/image.png" alt=""></p>
<p>일정 관리와 나의 막연한 불안감을 해소하기 위해 크루들과 함께하는 투두 메이트이다 ㅎㅎ
확실히 집중력 흐려질 때 쯤 크루들이 일과를 완료했다고 중간중간 알림이 뜨니까 다시 자극받고 집중하게되는 등, 벌써 효과를 느끼고 있다!👍</p>
<h1 id="3--메모">3. +) 메모</h1>
<h2 id="31-학습-키워드">3.1. 학습 키워드</h2>
<hr>
<p>[x] contextAPI
[x] fetching hook
[x] suspense
[x] ErrorBoundary
[] useQuery
[] cache
[] memoization
[x] SSoT
[x] MSW
[] 커스텀 에러 객체
[] 비동기 (callback, Promise, async/await)</p>
<hr>
<p>여기에 적은 키워드들은 내가 개념을 온전히 이해해서 타인에게 완벽히 설명할 수 있을 때 체크를 표시하도록 한다.</p>
<h2 id="32-남은-궁금증">3.2. 남은 궁금증</h2>
<p><strong>궁금증 발단</strong>
then 은 Promise가 풀리기 전까지 await 처럼 기다려주는 게 아니라, Promise가 풀렸을 때 then 콜백함수를 실행하도록 하는 문법인데,</p>
<p>then 이 실행되기 전에 Promise를 할당한 변수에 접근하면 Promise 객체가 뜬다.
그리고 then 에서 풀린 데이터를 return 하면 해당 데이터가 Promise로 할당되어있던 변수에 할당된다.</p>
<pre><code class="language-js">const cartProductData = getCartData().then((cartData) =&gt; return cartData.product)
// cartProductData는 프로미스 객체이다가 then 이 실행되면 cartData의 product를 저장하게된다.</code></pre>
<p>그럼 궁금해지는게, const 는 재할당을 금지하고 있다는 점이다.
Promise &#39;객체&#39;를 저장하고 있다가 새로운 데이터 배열을 새로 저장한다는 게 갑자기 어색하게 느껴진다.</p>
<p><strong>탐구해보기</strong></p>
<pre><code class="language-js">function getNumberAsync() {
  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      resolve([1, 2, 3]);
    }, 2000);
  });
}

let insideValue;
const promiseNumber = getNumberAsync().then((res) =&gt; {
  console.log(&quot;[then 콜백 내부] res객체 :&quot;, res);
  insideValue = res[0];
  return res[0];
});

console.log(&quot;[즉시] promiseNumber:&quot;, promiseNumber);
console.log(&quot;[즉시] promiseNumber 의 type:&quot;, typeof promiseNumber);
setTimeout(() =&gt; {
  console.log(&quot;[3초 후] promiseNumber:&quot;, promiseNumber);
  console.log(&quot;[3초 후] insideValue&quot;, insideValue);
}, 3000);

async function testAwait() {
  const value = await getNumberAsync();
  console.log(&quot;[await] value:&quot;, value);
}
testAwait();</code></pre>
<p>이게 어찌된 일인지 테스트 코드를 작성해보았다.
그리고 아래는 해당 코드를 실행시킨 후 결과이다.</p>
<pre><code class="language-terminal">[즉시] promiseNumber: Promise { &lt;pending&gt; }
[즉시] promiseNumber 의 type: object
[then 콜백 내부] res객체 : [ 1, 2, 3 ]
[await] value: [ 1, 2, 3 ]
[3초 후] promiseNumber: Promise { 1 }
[3초 후] insideValue 1</code></pre>
<p>일단, 내가
<code>그리고 then 에서 풀린 데이터를 return 하면 해당 데이터가 Promise로 할당되어있던 변수에 할당된다.</code> 라고 말했던 것 자체가 틀린 말이었다.</p>
<p>promise 를 반환하는 함수를 await 없이 실행하면 바로 Promise 객체가 할당된다.
그리고 then 으로 resolve된 Promise 를 받아 통째로 반환해도 기존에 할당됐던 Promise 객체의 [[PromiseResult]] 필드에 할당이 되는 것이지, 객체는 여전히 Promise이다.</p>
<p>즉, resolve된 값을 직접 접근할 수 있는 것은 then 내부 뿐이다.
그래서 then에서 resolve 된 데이터를 밖으로 꺼내주고싶다면, Promise가 할당되지 않은 변수에 할당해주어야한다.</p>
<p>그렇지만 await은 Promise가 풀리기 전까지 반환을 하지 않고 기다려서인지 Promise 객체가 할당되지 않고 resolve 된 데이터가 바로 할당된 모습을 볼 수 있다.</p>
<p><strong>이어지는 궁금증</strong></p>
<p>그럼 Promise 객체의 [[PromiseResult]]에 then 과 같은 메서드 없이 접근할 수 있는 방법은 없는 걸까?</p>
<p>async await 과 같은 기다리는 제너레이터 함수는 어떻게 구현하고 작동하는 걸까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 상품 목록 미션 주간 회고 05/12~05/16]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%83%81%ED%92%88-%EB%AA%A9%EB%A1%9D-%EB%AF%B8%EC%85%98-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-05120516</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%83%81%ED%92%88-%EB%AA%A9%EB%A1%9D-%EB%AF%B8%EC%85%98-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-05120516</guid>
            <pubDate>Thu, 15 May 2025 15:54:49 GMT</pubDate>
            <description><![CDATA[<h1 id="1-상품-목록-미션">1. 상품 목록 미션</h1>
<h2 id="11-pr-링크">1.1. PR 링크</h2>
<blockquote>
<p>상품 목록 미션 1단계 PR 링크: <a href="https://github.com/woowacourse/react-shopping-products/pull/109">https://github.com/woowacourse/react-shopping-products/pull/109</a></p>
</blockquote>
<h2 id="12-회고">1.2. 회고</h2>
<p>이번 페어는 전, 현 데일리 미팅조이자 연극조인 크루와 함께 했다. 함께 리액트 딥다이브, PR 리뷰 스터디를 하는 크루이기도 해서 함께 코드를 작성하며 미션을 해보는 게 재밌고 흥미로웠던 주간이다.</p>
<p>미션 시작 날, 오전 수업이 끝나고 점심을 먹고 난 뒤 공원의 <strong>&lt;페어프로그래밍 20분 아이스브레이킹 같이 해보기&gt;</strong> 수업이 무중력 광장에서 진행됐다.
30분 정도 페어와 함께 설문을 작성하며 스타일을 먼저 파악하고 맞춰보는 시간이었다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/f830de74-6bd3-4e47-a87f-49d89266198f/image.JPG" alt=""></p>
<p>작성한 설문이다. 스타일 궁합이 완전히 정반대가 나오는 게 시작도 전에 좀 웃겼다 ㅋㅋ
그렇게 아이스 브레이킹 수업이 끝난 뒤 바로 노션에 페어 프로그래밍 계획표를 정리했다. 그 중에 일부를 가져와봤다.</p>
<hr>
<h3 id="페어-룰">페어 룰</h3>
<h4 id="우선-순위">우선 순위</h4>
<p><strong>학습</strong></p>
<ul>
<li><strong>공통</strong>: 리액트에서 API 비동기 요청 처리</li>
<li><strong>재오</strong>: 타입스크립트로 최대한 타입을 빈틈없이 좁히기</li>
<li><strong>카멜</strong>: 상태 책임 분리에 대한 스스로의 기준을 정립</li>
</ul>
<p><strong>협업</strong></p>
<ul>
<li><strong>공통</strong>: 비판적으로 받아들이기</li>
<li><strong>재오</strong>: 중간중간 의논한 내용 문서화</li>
</ul>
<h3 id="코드-작성-사이클-일단-기능-구현-후-리팩토링">코드 작성 사이클: 일단 기능 구현 후 리팩토링</h3>
<h3 id="고민-최대-시간-1시간">고민 최대 시간: 1시간</h3>
<h3 id="고민-최대-시간-초과-시-결단-방법-가위바위보">고민 최대 시간 초과 시 결단 방법: 가위바위보</h3>
<hr>
<h2 id="논의점-정리">논의점 정리</h2>
<h3 id="논점-1-cartitems-의-상태를-어떻게-관리할까">논점 1. cartItems 의 상태를 어떻게 관리할까?</h3>
<p>사고 흐름: cartItems의 상태를 어떻게 관리할까? → 어디서나 유지되고 조회되는 값이 있다 → context로 관리! → cartId가 POST된 시점에 알 수 없다 → refetch 함수 작성</p>
<p>야그니 결단: context 로 관리하고 refetch 함수 작성</p>
<h3 id="논점-2-에러의-상태를-어떻게-관리할까">논점 2. 에러의 상태를 어떻게 관리할까?</h3>
<p>사고 흐름: 한 페이지 내에서 여러 컴포넌트가 비동기적으로 에러를 던질 수 있다 → 에러 토스트 메세지는 ProductList 내에 띄워져야한다 → 어디서든 조회하고 에러를 던질 수 있게 전역으로 관리해야할 것 같다 → 에러 바운더리도 생각을 했으나 개짜침 → 그래서 선택하지 않고, 차선책으로 loading 상태를 만들어 렌더링을 일으켜볼 생각 → ErrorToast 는 내부에서 setTimeout과 컴포넌트 or null을 반환하는 함수로 구성되어 있다 → 마운트된 시점에 컴포넌트를 렌더링하고 3초 뒤 null 을 반환함 → </p>
<p>의심되는 점:</p>
<ul>
<li>전역 상태의 에러가 최선일까?</li>
<li>에러가 사라지지 않았는데 ErrorToastMessage 컴포넌트가 사라질 때 Error 객체의 isError 상태를 false로 바꿔주는 것이 괜찮은가?</li>
</ul>
<p>try:</p>
<ul>
<li>loading 상태를 만들기</li>
<li>에러 상태 만들기</li>
</ul>
<p>야그니 결단: ErrorToastMessage 컴포넌트가 닫힐 때 Error 객체의 isError 상태를 false로 바꿔준다</p>
<hr>
<h2 id="학습-키워드">학습 키워드</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> emotion - theme</li>
<li><input checked="" disabled="" type="checkbox"> 리액트 비동기 API 요청</li>
<li><input checked="" disabled="" type="checkbox"> <strong><code>basicAuth</code> (http, Basic). API 토큰 요청</strong></li>
<li><input checked="" disabled="" type="checkbox"> contextAPI</li>
<li><input checked="" disabled="" type="checkbox"> createContext 의 초기값을 null 로 줘야한다.</li>
</ul>
<h2 id="13-수업-메모">1.3. 수업 메모</h2>
<p>첫 번째 수업 메모 링크: <a href="https://fantasy-pirate-bd2.notion.site/0513-1f291b23402b80e9884ff62051ec5777?pvs=4">https://fantasy-pirate-bd2.notion.site/0513-1f291b23402b80e9884ff62051ec5777?pvs=4</a></p>
<p>두 번째 수업 메모 링크: <a href="https://fantasy-pirate-bd2.notion.site/SSoT-1f591b23402b80acbe3cc2bd09a6550d?pvs=4">https://fantasy-pirate-bd2.notion.site/SSoT-1f591b23402b80acbe3cc2bd09a6550d?pvs=4</a></p>
<p>대략 이렇게 문서화를 하면서 페어를 진행했더니 확실히 짚고 넘어갈 수 있고 학습 포인트를 놓치지 않을 수 있으면서, 컨벤션과 약속을 의식적으로 유지할 수 있어서 좋았다.</p>
<h2 id="14-ktp-회고">1.4. KTP 회고</h2>
<p>이번 미션은 참 재밌게 진행할 수 있었지만, 한 가지 아쉬웠던 점은 학습 우선 순위였던 &#39;비동기 API&#39; 에 대해 생각보다 깊게 학습할 시간이 없었다는 것이다.</p>
<p>UI를 만들고 기능을 구현하고 비동기 요청 테스트 코드를 작성하는데만 해도 시간이 꽤 걸려서 학습 부채를 많이 남기고 &#39;야그니!&#39; 를 많이 외쳤던 것 같다.</p>
<p>그래서 느꼈던 점을 KTP로 짧게 회고해보자면 아래와 같다.</p>
<h3 id="keep">Keep</h3>
<ul>
<li>페어와 매일 회고하고 논의점과 사고흐름을 그때그때 정리하며 문서화를 잘 해두었던 점이 좋았다.</li>
<li>전역 css의 관리와 스타일 컴포넌트의 컨벤션을 세워가며 작성했던 점</li>
<li>드라이버와 네비게이터의 역할을 충실히 수행했다. 논의는 코드 작성 전에 충분히 하고, 코드 작성에 들어가면 드라이버의 자아를 없애는 것으로 정해두었고 잘 지켜졌다.</li>
</ul>
<h3 id="try">Try</h3>
<ul>
<li><p>비동기 API 학습 시간을 일정에 명확히 반영하여, 기능 구현 시간뿐만 아니라 학습 시간도 확보할 수 있도록 계획을 개선해보자.</p>
</li>
<li><p>Todo List 작성 시, 기능 구현뿐만 아니라 학습할 내용과 예상 학습 시간도 함께 기록하여, 학습 시간도 확보해보도록 시도해보자.</p>
</li>
<li><p>테스트 코드 작성 시간을 좀 더 널널하게 확보해야겠다.</p>
</li>
</ul>
<h3 id="problem">Problem</h3>
<ul>
<li>Todo List 로 할 일을 작성하고 타임라인을 구성하는 데에 있어서, 실제 걸리는 시간을 현실적으로 잡지 못했던 것 같다.</li>
<li>학습 목표로 세웠던 것들이 여전히 학습 부채로 남아있게 되었다.</li>
</ul>
<hr>
<h1 id="2-스터디">2. 스터디</h1>
<h2 id="21-threejs-스터디-512">2.1. three.js 스터디 (5/12)</h2>
<p>이번 three.js 스터디의 학습 과제는 라이팅이었다.
• Light 종류 이해하기 (Ambient, Directional, Point Light)
• 빛과 그림자의 관계
• &quot;Directional Light를 사용해 입체감을 준 씬 만들기&quot;</p>
<p>가 학습 키워드였는데, 라이팅을 적용하기 전 제대로된 모델을 띄워놓고 학습해보고 싶었다. 그래야 light의 실제 적용과 사용의 감각을 익힐 수 있을 것 같았기 때문이다.</p>
<p>그렇게 찾아보고 사용하게된 vrin. image to 3d modeling을 해주는 AI 인데, 재작년? 작년? 쯤에 써봤을 때와 달리 엄청 발전한 것 같다.</p>
<p>1차 gpt 생성 이미지</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/78b04e30-c7e8-4d04-8e79-68f3c6d7523b/image.png" alt=""></p>
<p>2차 포토샵 가공 이미지</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/4dacab13-40f2-4362-ab70-d1948f9a41c7/image.png" alt=""></p>
<p>결과물
👇</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/b28c6b29-9024-40bf-9b93-59b132e83ba8/image.png" alt=""></p>
<blockquote>
<p>three.js 렌더링 영상: <a href="https://youtu.be/7d6yhFUCzKI">https://youtu.be/7d6yhFUCzKI</a></p>
</blockquote>
<blockquote>
<p>추천 AI 3D 모델링 사이트 : <a href="https://vrin.co.kr/">https://vrin.co.kr/</a></p>
</blockquote>
<p>이렇게 만족스러운 모델을 띄워놓고 <code>DirectionalLight</code> 를 중점적으로 공부하니 c4d 공부할 때 생각도 나고 재밌었다. </p>
<h2 id="22-pr-리뷰-스터디512">2.2. PR 리뷰 스터디(5/12)</h2>
<p>리액트 첫 미션인 페이먼츠의 PR 리뷰 스터디를 진행했다.
크루들의 리뷰어 피드백에서 공통적으로 나왔던 내용들을 추출해보면 막상 Javascript 미션 때와 비슷했던 것 같다.</p>
<p><strong>공통 피드백</strong></p>
<ul>
<li>input, label, for, aria-label,role 등 잘 활용하기</li>
<li>시맨틱 태그 적절하게 잘 활용하기 (form, button)</li>
<li>라우터 종류에 대해 학습하기</li>
<li>styled component 의 구분 (ex. S. 네임스페이스 사용)</li>
<li>마르코의 colocation 방식의 디렉터리 구조 제안</li>
<li>파생 상태는 상태가 아닌 반환값으로 처리</li>
<li>useEffect 사용 최소화</li>
</ul>
<p><strong>참고 문서</strong></p>
<ul>
<li><p>리뷰어 - 리액트 라우터 문서 공유</p>
<p>  BrowserRouter 외에도 react-router에서 제공하는 다른 라우터 방식이 있는데, 각 차이점과 선택 기준에 대해서 정리해보시면 좋을 것 같아요.</p>
<p>  <a href="https://reactrouter.com/api/declarative-routers/Router">https://reactrouter.com/api/declarative-routers/Router</a></p>
</li>
<li><p>다른 크루의 퍼널 구현을 보고 따로 찾아본 퍼널 자료</p>
<p>  토스 SLASH23 의 useFunnel 영상: <a href="https://youtu.be/NwLWX2RNVcw">https://youtu.be/NwLWX2RNVcw</a></p>
<p>  npm 주소 : <a href="https://www.npmjs.com/package/@use-funnel/react-router">https://www.npmjs.com/package/@use-funnel/react-router</a></p>
</li>
</ul>
<h2 id="21-모던-리액트-딥다이브-스터디517">2.1. 모던 리액트 딥다이브 스터디(5/17)</h2>
<p>(작성 시점 기준 아직 스터디를 안함...스터디 끝나고 추가 예정)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 페이먼츠 모듈 1단계 주간 회고 - 4/29~5/7]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0-%EB%AA%A8%EB%93%88-1%EB%8B%A8%EA%B3%84-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-42957</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0-%EB%AA%A8%EB%93%88-1%EB%8B%A8%EA%B3%84-%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-42957</guid>
            <pubDate>Wed, 07 May 2025 09:47:15 GMT</pubDate>
            <description><![CDATA[<h1 id="1-페이먼츠-모듈-미션">1. 페이먼츠 모듈 미션</h1>
<p>이번 미션은 지난 미션인 페이먼츠에서 구현해보았던 카드 정보 입력 및 검사 훅들과 모달 컴포넌트를 npm 으로 배포하는 것이었다. </p>
<h2 id="11-pr-링크">1.1 PR 링크</h2>
<blockquote>
<p>페이먼츠 모듈 1단계 PR 링크: <a href="https://github.com/woowacourse/react-modules/pull/83">https://github.com/woowacourse/react-modules/pull/83</a>
(시범 운영 중인 코드 리뷰 AI, 래빗이 리뷰를 너무 많이 달아서 코멘트 수가 98개나 되었다)</p>
</blockquote>
<h2 id="12-회고">1.2. 회고</h2>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/c01b54e8-011a-4f95-a09b-e5e8359ee4a7/image.png" alt=""></p>
<p>이번 페어 미션은 구현 3일 중 하루가 공휴일이었다. 그리고 5/1에 내가 약속이 있었기 때문에 페어에게 양해를 구하고 이틀 안에 미션을 구현하는 것을 목표로 삼았다.</p>
<p>그러나 미션 첫 날 우리는 코딩 없이 설계만 하게되었다.</p>
<p>서로 훅과 컴포넌트에 대한 생각과 분리, 설계 철학에 대한 이야기를 나누며 같은 방향을 보며 페어를 진행하고 싶었던 우리는 서로의 생각을 이해하고 설득하고 아이디어를 내는 데에 집중했다. 그러고나니 하루가 끝나있었다. (물론 오전 중엔 수업도 있었고 오후엔 서로 스터디 일정도 있었던 터라 시간이 부족하기도 했다)</p>
<p>우리는 이틀 안에 구현하기로 했으니 사실 그럼 코딩을 할 수 있는 시간은 하루남은 꼴이었다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/83a8345c-cff1-48f3-b36d-7fcafd13de34/image.png" alt=""></p>
<p>오늘 안에 다 끝내버리자! 각오하고 등교한 둘째날. 타임어택을 한다고 생각하니 오히려 도파민이 싹 도는 게 집중력이 극대화되는 기분이었다.</p>
<p>첫 날을 통으로 함께 생각을 맞추고 설계를 해보며 대화를 많이 하는 데에 쓴 만큼 막상 코딩에 들어가서는 페어와 혼란이나 갈등없이 빠르게 코드만을 구현할 수 있었다.</p>
<p>확실히 서로 미션을 이해하는 것도, 코드 스타일도, 철학도 다르기 때문에 페어를 할 땐 충분한 대화로 처음에 생각과 방향성을 맞추는 게 중요한 것 같다.</p>
<p>...</p>
<p>그러나 우리는 모달 컴포넌트 모듈이 모달 열림과 닫힘 핸들러도 지원하고자 했는데, 여기서 예상치 못한 난관을 겪었다.</p>
<p>우리의 목표는 사용자가 아래와 같이 사용할 수 있게되는 것이었다.</p>
<pre><code class="language-js">import { ModalComponent, useModal } from &#39;laireca-modal-components&#39;;
import &#39;./App.css&#39;;

function App() {
  const { openModalHandler } = useModal();

  const onClickHandler = () =&gt; {
    openModalHandler();
  };

  return (
    &lt;&gt;
      &lt;ModalComponent modalType=&quot;center&quot; closeType=&quot;top&quot; titleText=&quot;카드사 선택&quot; {...optionalProps}&gt;
        {children}
      &lt;/ModalComponent&gt;

      &lt;div className=&quot;button-container&quot;&gt;
        &lt;button className=&quot;click-me-button&quot; onClick={onClickHandler}&gt;
          click me!!
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
}

export default App;</code></pre>
<p>이를 위해 모달 컴포넌트 모듈에 openModalHandler함수와 closeModalHandler, isModalOpened 상태를 반환하는 useModal 이라는 훅도 존재하게 되었는데, 사용자는 간단하게 <code>ModalComponent</code> 하나만 작성하면 이 훅을 어디서 써도 모달이 열리도록 구현하였다. </p>
<p>사용자가 ModalComponent에 isModalOpened 상태를 넘기거나 따로 관리해주지 않아도 해당 훅을 쓰면 모달의 열고 닫음을 할 수 있게 구현하고 싶었던 우리는 상태를 어떻게 관리하고 전달하고 동일한 인스턴스로 유지해야하는지에 대해 고민하게 되었다.</p>
<p>그렇게 시도해보게된 방법은 contextAPI에 싱글톤(?) 패턴을 입혀보는 것이었는데, props drilling 도 없애고 모달 컴포넌트를 useModal 훅의 openModalHandler, closeModalHandler만 꺼내서 조작할 수 있게 되었다.</p>
<p>여기까지 작성했을 때 벌써 시간은 저녁 8시가 넘어있었다. 퇴실 시간은 11시...아직 훅 모듈은 구현 시작도 못했는데.........</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/51d0504a-efe6-478f-99d1-72a0a2941a48/image.png" alt=""></p>
<p>하지만 우리는 할 수 있다!</p>
<p>점점 올라가는 도파민과 심박수를 느끼며 페어와 타임어택 구현에 돌입했고 결국 11시에 딱 맞춰서 배포할 수 있었다😊</p>
<p>npm 배포 시에 tsconfig 설정 때문에 우여곡절을 겪었던 크루들이 해결법을 공유해줘서 배포도 문제 없이 빠르게 할 수 있었다.😊😊</p>
<p>11시에 딱 완성하고 배포까지 성공한 뒤 페어와 후련하게 퇴근하는 그 기분이 잊히지 않는다 ㅎㅎ</p>
<p>config파일들이나 package.json 파일들은 평소에 꼼꼼히 들여다볼 생각을 못했던 파일들인데 학습해봐야할 것 같다. </p>
<p>.env 파일도 그렇고, 내 프로젝트에 존재하는 코드, 파일들은 모두 빠짐없이 설명할 수 있는 사람이 되어야겠다고 생각했다.</p>
<hr>
<p>그리고 제출 후...</p>
<p>이런 식의 모달 컴포넌트를 작성하면서 많은 고민을 하고 학습해볼 수 있었기 때문에 좋은 시도였다고 생각하지만, 리팩토링 시점에서 다시 생각해보니 현재 구조의 명확한 한계점이 느껴졌다.</p>
<p>트레이드 오프 관점에서 리뷰어가 정리해준 현재 코드 구조의 문제점은</p>
<p><strong>이점</strong></p>
<ul>
<li><ModalComponent /> 하나만 작성하면, useModal 훅을 통해 손쉽게 모달의 열림과 닫힘을 제어할 수 있음</li>
</ul>
<p><strong>리스크</strong></p>
<ul>
<li>React의 코드 구현 방식과 맞지 않음</li>
<li>모달을 여러 개 띄우는 UX 구현 어려움</li>
</ul>
<p>인데, 확실히 얻는 이점에 비해 리스크가 크고 사용에 제한적이라는 생각이 들었다.</p>
<p>그래서 일단 1단계 리팩토링에선 isModalOpened 상태를 내보내고 이 상태 기반 조건부 렌더링을 사용자가 처리해주는 방향으로 수정하였지만 2단계 미션을 진행하면서 좀 더 개선점을 찾아볼 생각이다.</p>
<h2 id="13-학습한-점">1.3. 학습한 점</h2>
<ul>
<li>tsconfig 파일들<ul>
<li>dist 폴더에 index.d.ts가 안 생기는 문제</li>
</ul>
</li>
</ul>
<h1 id="2-스터디">2. 스터디</h1>
<h2 id="21-모던-리액트-딥다이브-스터디-52">2.1. 모던 리액트 딥다이브 스터디 (5/2)</h2>
<p>스터디의 기존 교재였던 <code>모던 리액트 딥다이브</code>는 리액트의 내부 구현, 이론적인 내용을 주로 다루는데, 현재 당장 미션을 진행하며 리액트 감각을 익혀야하는 우리의 상황과 맞지 않고 방향이 흐트러진다는 의견이 많아 우선 리액트 공식 문서를 챕터 별로 읽어오면 모여서 토론하고 궁금점을 해소하며 공부하는 방향으로 학습 방법을 재설정하게 되었다.</p>
<p>그리고 읽어오기로 했던 페이지는 리액트 공식 문서의 UI 표현하기 챕터였다.</p>
<blockquote>
<p>리액트 공식문서 UI 챕터 : <a href="https://ko.react.dev/learn/describing-the-ui">https://ko.react.dev/learn/describing-the-ui</a></p>
</blockquote>
<p>리액트의 map return jsx의 key는 꼭 지정되어야하며 index나 즉석 랜덤 생성 값 등을 지정하면 안된다는 것을 알게 되었다.</p>
<p>왜 Tree구조로 dom을 만들었을지도 생각해보았다.</p>
<p>그리고 렌더링 트리에 html 이 포함되지 않는다는 공식문서 문장에 대해서도 내가 오해를 했었다는 것을 알게되었다. 스터디원들 덕분에 스터디 시간에 이야기해보면서 의문점을 해소할 수 있었다.</p>
<p>return 문에 html 태그를 써도 어쨌든 바벨에서 자바스크립트 객체로 변환을 할 때는 createElement로 바뀔 수 있도록 type: “p” 이런식으로 작성된다는 것이었다.</p>
<p>그리고<code>왜 하필 &#39;트리&#39;여야 하는가? UI 구조를 표현하는 데 트리 구조가 가장 적합한 모델이라고 단정할 수 있는가</code> 에 대한 논제에 대해서도 토론해보았는데 나는 이에 대해서
DOM의 표현 방식에 따라 부모-자식 관계를 나타내기 쉬운 자료 구조를 선택했던 게 아닐까 생각했다.</p>
<p>HTML 문법도 부모 태그 안에 자식 태그 안에 자식 태그... 이런 식이고... 
여러 태그들이 있다한들 우리가 보는 페이지는 하나의 DOM, 하나의 문서 객체이기 때문이다.</p>
<p>다양한 의견과 관점들이 나왔는데, 이 스터디에서는 이런 사소한 궁금증부터 리액트의 딥한 고민까지 크루들과 함께 의견을 나누고 공부할 수 있어서 즐겁다.</p>
<h2 id="22-블로그-회고-스터디-52">2.2. 블로그 회고 스터디 (5/2)</h2>
<p>블로그 회고 스터디가 2시로 이번 주만 변경된 공지를 당일에서야 봐서 불참하게 되었다.
리액트 스터디가 매주 금요일 오후 2시에 해서 겹치게 되었기 때문이다...</p>
<p>저번 주 스터디 시간에도 말해주셨었다는데 당시에 들으면서 기억이 안 났나보다... 요즘 정신을 어디다 두고 사는 걸까?ㅋㅋ ㅠㅠ 역시 잠을 꾸준히 잘 자야한다고 느꼈다... 맨날 3-4시간 자다가 하루 10시간 잔다고 피로가 풀리진 않는 것 같다.</p>
<p>죄송합니다...</p>
<h2 id="23-threejs-57">2.3. three.js (5/7)</h2>
<p>2번째 three.js 스터디였다.</p>
<p>• Mesh란 무엇인가
• Geometry 종류 살펴보기 (Box, Sphere 등)
• Material 종류 기본 (MeshBasicMaterial, MeshStandardMaterial)</p>
<p>에 대해 공식 문서를 보고 학습하며 개인 과제를 해오고 스터디날 서로의 코드를 공유하며 설명해주는 방식으로 스터디를 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/3a9d76f3-017b-41ba-8f28-0e02e0e7d085/image.png" alt=""></p>
<p>내가 해간 간단한 과제이다. 이 코드를 구현해보면서 Mesh 와 hdr 렌더의 다양한 속성과 비동기 함수 호출 규칙, Material 종류들을 학습해볼 수 있었다.</p>
<p>나는 hdr 위주의 학습을 공유했는데, 역시 이번에도 다들 열심히 준비해오셔서 Mesh 와 카메라, 조명, Geometry에 대해 더 확실하게 공부할 수 있었다. 거의 강의였다...👍</p>
<p>이제 겨우 2주차인데도 다들 기본으로 해오자고한 과제보다 뭔가 더 덧붙여서 반짝반짝한 작업물들을 가져오는데, 의욕도 학습도 같이 주고 받을 수 있어서 너무 좋은 스터디인 것 같다.</p>
<p>무엇보다 너무 재밌다☺️ 이대로면 역시 우리 학습 끝에 같이 미니 프로젝트도...<code>춘식이의 관찰일기</code> 처럼........ㅎㅎㅎㅎㅎㅎㅎ</p>
<h2 id="24-코딩-테스트-스터디">2.4. 코딩 테스트 스터디</h2>
<p>이번 주는 별다른 공지가 없었어서 알아서 문제 두 문제만 풀고 깃허브에 올려놨다.</p>
<h1 id="3-학습-키워드-정리">3. 학습 키워드 정리</h1>
<ul>
<li>tsconfig</li>
<li>package.json</li>
<li>npm 모듈 배포</li>
<li>contextAPI</li>
<li>three.js
  • Mesh
  • Geometry 종류
  • Material 종류 기본</li>
<li>리액트 key</li>
</ul>
<h1 id="-메모">+. 메모</h1>
<blockquote>
<p>크루가 공유해준 모던 자바스크립트 딥다이브 내용 정리 사이트
<a href="https://poiemaweb.com/">https://poiemaweb.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 페이먼츠 미션]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0-%EB%AF%B8%EC%85%98-ced96i2f</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0-%EB%AF%B8%EC%85%98-ced96i2f</guid>
            <pubDate>Fri, 02 May 2025 00:54:35 GMT</pubDate>
            <description><![CDATA[<p>점점 미션은 바빠지는데 레벨 1 초반 때 썼던 회고글의 분량을 유지하려고하니 회고 글을 안 쓰게되는 것 같아 분량이 적고 완벽하지 않은 글이라도 일기 형식으로 꾸준히 쓰려한다.</p>
<h1 id="1-페이먼츠-미션">1. 페이먼츠 미션</h1>
<h2 id="11-pr-링크">1.1 PR 링크</h2>
<blockquote>
<p><a href="https://github.com/woowacourse/react-payments/pull/435">페이먼츠 1단계 PR 링크</a>
<a href="https://github.com/woowacourse/react-payments/pull/477">페이먼츠 2단계 PR 링크</a></p>
</blockquote>
<h2 id="12-미션-회고">1.2. 미션 회고</h2>
<p>이번 미션은 페이먼츠 미션이었다.
리액트로 &#39;카드 정보를 입력하면 실시간으로 유효성을 검사하며 카드 프리뷰 UI가 업데이트&#39;되는 기능을 구현하는 미션이다.</p>
<p>이제 자바스크립트에서 벗어나 리액트를 배우는 레벨 2의 첫 미션이었기 때문에 리액트의 감각을 익혀가는 것이 가장 유의미했던 미션이지 않을까 싶다.</p>
<p>이번 미션을 진행하면서 훅과 상태 관리에 대해 고민을 많이 하고 다양하게 활용하는 노력을 해보았다.</p>
<p>페어와도 미션 기간 3일 중 하루를 거의 통으로 설계에 대해 대화하고 함께 고민해보는 시간을 가질만큼 자바스크립트의 상태 관리 관성에서 벗어나 리액트스럽게 작성하기 위해 신경썼다.</p>
<p>특히 이번 미션에서는 컴포넌트의 유효성 상태 관리 방식에 대해 깊이 고민했다. <code>isValid</code> 상태를 컴포넌트 외부에서 props로 주고받는 방식이 전체 폼 유효성 판단과 외부 제어에 더 유리하다는 점을 체감했고, 유효성 피드백 메시지 자체를 기준 삼아 상태를 판별하는 방향으로 개선하기도 했다.</p>
<p>구조가 유사한 <code>CardNumberInput</code>, <code>CardExpirationDateInput</code>, <code>CardCVCInput</code> 등을 하나의 컴포넌트로 추상화할지 여부도 깊게 고민해보았지만, 서로 다른 형태와 유효성을 가지고 실시간으로 상태에 따라 업데이트되는 로직이므로 다른 컴포넌트로 관리를 하는 것이 합리적이라는 판단을 내렸다. (추후 2단계에서 확장될 상태도 고려해 리팩토링을 염두에 두었다.)</p>
<p>Storybook을 활용해 정상/오류/빈 값 등 다양한 컴포넌트 상태를 시각적으로 테스트해보았고, 훅이 필요한 경우에는 <code>render</code>를, 단순 UI 컴포넌트는 <code>args</code>를 활용하는 방식으로 구성했다.</p>
<p>또한, Git 커밋을 나누는 기준에 대해서도 고민해보았는데, 특히 다른 컴포넌트들이 의존하는 핵심 컴포넌트를 수정할 때 커밋 범위가 넓어지는 문제에 대해 생각해보았다. 프로그램이 깨지지 않는 단위로 작업을 쪼개는 것이 좋다는 것을 어디선가 보았었는데 여전히 이를 기준으로 삼는 게 맞다는 결론이 났다.</p>
<p>스타일링은 <code>styled-components</code> 나 <code>emotion</code> 대신 Module CSS를 채택했는데, 코드와 스타일의 분리가 명확하고 간단한 상태 기반 스타일링에 적합하다고 판단했기 때문이다.</p>
<p>마지막으로, 페어와의 설계 논의를 통해 전체 흐름, 상태 관리 구조, 컴포넌트 분리 기준을 정리하며 실제 DOM 트리 구조에 기반해 디렉토리 구조를 나눴고, <code>InputForm</code>이 <code>Input</code>을 children으로 받아 재사용성과 확장성을 높이는 방식으로 리팩토링도 진행했다.</p>
<h2 id="13-학습-키워드">1.3. 학습 키워드</h2>
<p>첫 미션의 나만의 학습 목표는 리액트 감각 키우기였기 때문에 개인적으로 리액트의 꽃이라고 생각하는 훅에 대해 다양하게 활용하고자 노력했다.</p>
<ul>
<li>useState</li>
<li>useMemo</li>
<li>useCallback</li>
<li>useEffect</li>
</ul>
<h1 id="2-스터디">2. 스터디</h1>
<h2 id="21-모던-리액트-딥다이브-스터디">2.1. 모던 리액트 딥다이브 스터디</h2>
<p>예정대로 리액트 딥다이브 스터디를 시작했다. 모던 리액트 딥다이브 교재를 보는 스터디였는데, 2주동안 진행한 분량은 JSX와 훅의 내용이었다. 스터디 방식은 한 주동안 정해진 교재 분량을 읽어오면 읽으면서 궁금했던 점이나 논의해볼만한 점을 각자 생각해서 스터디 시간에 말하는 것이었다.</p>
<p>그리고 스터디를 준비하면서 리액트 가상DOM, 파이버, 훅의 내부 구현 등에 대해 추가적으로 궁금한 점이 생겨서 스터디장인 써밋의 도움으로 다양한 테스트와 훅의 내부 구현을 탐구해보며 리액트의 동작 원리에 대해 정말 딥다이브로 공부해볼 수 있었다.</p>
<p>특히 <code>useCallback</code> 의 개념을 학습하면서 클로저의 원리를 이용한다면 일으킬 수 있는 메모리 누수가 있을 것 같았고 이 부분에 대해서도 하루종일 써밋과 다양한 시도를 해보며 useCallback이 연쇄적으로 존재하는 상태에서 두 useCallback을 참조하면서 다른 변수를 참조하는 클로저 함수가 있을 때, 해당 변수가 컴포넌트 리렌더링마다 새로운 메모리에 할당되고 이전 메모리도 지워지지 않고 누적되는 문제가 있다는 것을 알게되었다.</p>
<p>일반적인 경우에는 문제가 되지 않을 정도의 소소한 누수일 수 있지만, 저 쌓이는 변수가 만약 큰 용량의 데이터라면 꽤 부담이 가는 이슈일 것이다.</p>
<h3 id="학습-키워드">학습 키워드</h3>
<ul>
<li>리액트 가상DOM</li>
<li>리액트 파이버</li>
<li>use-* 훅들</li>
</ul>
<h2 id="22-threejs-스터디">2.2. three.js 스터디</h2>
<ul>
<li>three.js 스터디를 새로 들었다. 회고글 작성 시점 기준으로 스터디 첫 주차를 진행한 상태인데, 원래부터 배우고싶고 관심있었던 라이브러리라 간단한 상자를 렌더링해보는 과제도 재밌게 했다. 스터디는 한 주 동안 간단한 실습 목표를 정해두고 각자 구현해와서 자신의 코드를 설명하는 방식이었는데, 스터디원들이 엄청 열심히 각자 궁금한 부분들을 탐구하고 정리해와서 간단한 실습이었는데도 다양한 포인트에서 공부해볼 수 있었다. 너무 즐거운 시간이었다!</li>
</ul>
<h1 id="3-마무리">3. 마무리</h1>
<p>방학 중에 결심했던 내용 중에 매일 조각조각이라도 있었던 일, 느겼던 것을 메모해두고 회고글을 써야겠다는 말이 있었는데 정신 없이 지나가는 하루하루에 하나도 지키지 못했다. 늦게 퇴근하고 랑이 산책 다녀오고나면 더이상 아무 것도 못하고 잠에 곯아떨어지기만 반복했다. 슬슬 피로 누적과 체력의 한계가 느껴진다...</p>
<p>그래서 매일 하루의 끝에 짧게라도 적어놓자는 목표에서 그냥 느낀 순간 바로바로 메모장 켜서 적어놓는 습관을 들일까싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[토이 프로젝트] 행바타 간단한 개발 회고]]></title>
            <link>https://velog.io/@dev-dino22/%ED%96%89%EB%B0%94%ED%83%80-%ED%96%89%EB%B0%94%ED%83%80-%EA%B0%9C%EB%B0%9C-%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@dev-dino22/%ED%96%89%EB%B0%94%ED%83%80-%ED%96%89%EB%B0%94%ED%83%80-%EA%B0%9C%EB%B0%9C-%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Sat, 12 Apr 2025 18:03:46 GMT</pubDate>
            <description><![CDATA[<p>캉골과 1주일의 방학 중 토이 프로젝트를 시작했다. 이름하야 [행바타]! ㅎㅎ
우테코 마스코트 캐릭터인 행성이를 꾸미고 png로 파일을 다운 받을 수 있는 사이트이다.</p>
<p><strong>4일</strong> 만에 완성한 간단한 프로젝트였다.
순수 Javascript로 구현하였다.</p>
<blockquote>
<p>행바타 깃허브 주소: <a href="https://github.com/Woowa-Toy-Lab/hangbata/tree/develop">https://github.com/Woowa-Toy-Lab/hangbata/tree/develop</a></p>
</blockquote>
<blockquote>
<p>행바타 사이트: <a href="https://woowa-toy-lab.github.io/hangbata/">https://woowa-toy-lab.github.io/hangbata/</a></p>
</blockquote>
<h1 id="1-소스-준비">1. 소스 준비</h1>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/50e7d126-c565-448b-9e81-913dc71c9c67/image.png" alt=""></p>
<p>크루들 사이에서 왼손이 샤라웃해준 나만의 행성이 만들기가 유행한 적이 있다. 우테코 선릉캠 칠판엔 항상 행성이 그림들이 가득했고 크루들의 슬랙 프사도 하나 둘 행성이가 되어갔다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/f3cdddd0-4301-4256-9156-5c11c192597a/image.png" alt=""></p>
<p>공유해주신 파일에 들어가보니 귀여운 행성이 사진 파일들이 가득했다.</p>
<p>그렇게 시작하게된 행바타 프로젝트~</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/5526e7de-0a39-4ac1-9cca-e896f0f2e158/image.png" alt=""></p>
<p>초안으로 그려본 피그마 행바타 와이어프레임이다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/79966b99-80a9-4138-8cfb-19ea169e8b63/image.png" alt=""></p>
<p>그리고 나온 행바타 UI 디자인.</p>
<p>대충 [몸, 표정, 소품, 특수효과, 배경]으로 나누어서 저 캔버스 안에 아바타를 꾸미고 이미지를 다운받을 수 있는 걸로 구상했다.</p>
<p>이를 위해 우리는 행성이 svg 파일이 필요했는데... 왼손이 공유해주신 행성이 그림들은 누끼도 따져있지않은 행성이 jpg 사진이었다.
물론 나는 일러스트레이터를 다룰 줄 아니까 벡터로 선을 따는 방법도 있었지만...주어진 시간은 적고 이것도 상당히 귀찮은 작업이었다. 그래서 일러스트레이터의 &quot;이미지 추적 후 확장&quot; 기능을 활용해 소스 작업 시간을 단측시키고 싶었다. 하지만 저 손그림 이미지를 이미지 추적시키면 패스들이 너무 여러갈래로 쪼개져서 쓸 수 없는 수준으로 추출이 된다. 그래서 생각해낸 것이~</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/9040d15d-1a21-4f64-a940-74be9197afca/image.png" alt=""></p>
<p>이렇게 깔끔하게 다시 그려달라고 GPT한테 요구하는 것이다 ㅎㅎ
<img src="https://velog.velcdn.com/images/dev-dino22/post/d7a43ea8-ea04-44fd-a07c-451db9826b99/image.png" alt=""></p>
<p>되게 잘 따준다. 하지만 이렇게 깔끔하게 그려줘봤자 여전히 흰색 배경이 섞인 픽셀 파일의 JPG 사진일 뿐이다. 이것을 이제 어도비 일러스트레이터를 켜서 갖고와보자. 이렇게 이미지를 선택한 상태에서</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/1909d310-739f-4741-8be8-12cf26ea8e8b/image.png" alt=""></p>
<p>오브젝트 - 이미지 추적 - 만든 후 확장
을 누르면 ~</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/a09470c1-e4c7-4106-8cea-6037fc5f39d5/image.png" alt=""></p>
<p>짜잔 깔끔한 svg 상태가 되었다!
<img src="https://velog.velcdn.com/images/dev-dino22/post/d44e5f18-6656-4b15-9149-391ed82344eb/image.png" alt=""></p>
<p>이 툴로 간단하게
<img src="https://velog.velcdn.com/images/dev-dino22/post/96a7f0c0-bd44-400b-bca3-7ddd502ab447/image.png" alt=""></p>
<p>면도 채워줄 수 있고</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/6b72f7b6-b7c2-43dd-b3cd-98dbe02627cb/image.png" alt=""></p>
<p>패스가 이렇게나 깔쌈하게 나뉜 것을 확인할 수 있다.</p>
<p>이제 이 것을 svg로 내보내기만 하면 코드에서 화질 깨짐 없이 동적으로 웹에 그려낼 수 있는 것이다. 굳굳~</p>
<h1 id="2-페어-프로그래밍">2. 페어 프로그래밍</h1>
<p>캉골과 나는 레벨 1 동안 한 번도 같이 페어로 매칭된 적이 없었다. 그래서 토이프로젝트를 시작할 때만 해도 대화를 많이 한 것과는 별개로 서로의 코드 스타일이나 컨벤션은 전혀 모르는 상태였다. 그래서 처음 대략적인 아바타 UI를 구성하고, 상태를 어떻게 관리할 거다하는 식의 큰 틀 로직은 함께 페어 프로그래밍으로 구현하기로 해보았다. 나중에 세부 기능을 구현하면서 분업을 하더라도 서로의 스타일을 맞춰보고 우리만의 컨벤션이 생긴 상태에서 분업을 하는 게 효율적일 것 같았다.</p>
<p>우리의 토이 프로젝트의 가장 큰 목적은 레벨 1 바닐라 자바스크립트에 대한 복습에 있었기 때문에 외부 라이브러리를 쓰지 않고 순수 우리만의 코드로 작성하기로 하였다.</p>
<p>그리고 캉골과 코드를 작성하면서 정말 많은 인사이트를 얻고 또 한번 성장 및 복습이 견고해질 수 있었다!</p>
<p>캉골은 레벨 1 동안 엘레강트 오브젝트 스터디를 하며 객체지향, 객체란 무엇인가에 대한 공부를 많이 한 것 같았다. 그리고 나는 리뷰어 해먼드와 하리를 거쳐, 프론트엔드 크루원들의 코드들을 보는 PR 리뷰 스터디도 진행하면서 선언적인 프로그래밍이란 무엇일까에 대해 고민을 많이 해보았었다.</p>
<p>캉골과 나는 서로 다른 부분에 집중해서 자신만의 기준을 세우고 넘어갔던만큼 서로의 다른 인사이트를 공유할 수 있었고 자칫 고민해보고 넘어가지 못할 뻔 했던 것들을 함께 고민해보며 더욱 깔끔하게 레벨 1을 회고할 기회가 되었던 것 같다.</p>
<p>...</p>
<p>(5/19) 이어서 작성...</p>
<p>레벨 2가 시작되고 너무 바빠져서 행바타 회고를 여태 마무리하지 못했었다. 이미 프로젝트를 끝낸지 오래 됐기도하고 코드가 잘 기억나지 않는다... 그래서 그냥 프로젝트를 진행하며 만들었던 부산물인, <code>createElement()</code> 함수 코드를 가져와봤는데,</p>
<pre><code class="language-js">function toElement&lt;K extends keyof HTMLElementTagNameMap&gt;(
  template: string,
  tag: K
): HTMLElementTagNameMap[K] {
  const container = document.createElement(&quot;div&quot;);
  container.innerHTML = template;

  const el = container.firstElementChild;
  if (!el) {
    throw new Error(&quot;toElement 유틸 에러: element가 없습니다.&quot;);
  }

  return el as HTMLElementTagNameMap[K];
}

interface IArguments {
  id?: string;
  class?: string;
  type?: string;
  name?: string;
  value?: string;
  src?: string;
  alt?: string;
  style?: string;
  for?: string;
}

export function createElement&lt;K extends keyof HTMLElementTagNameMap&gt;(
  tag: K,
  args: IArguments,
  ...children: string[] | Element[]
): HTMLElementTagNameMap[K] {
  const attribute = Object.entries(args)
    .map(([key, value]) =&gt; `${key}=&quot;${value}&quot;`)
    .join(&quot; &quot;);

  const template = `&lt;${tag} ${attribute}&gt;&lt;/${tag}&gt;`;
  const element = toElement(template, tag);

  children.forEach((child) =&gt; {
    if (typeof child === &quot;string&quot;) element.textContent = child;
    else element.appendChild(child);
  });

  return element;
}
</code></pre>
<p>이 코드이다. 캉골의 toElement와 나의 기존 createElement 함수를 합쳐서 만들게된 함수인데, 자바스크립트에서 시멘틱 태그와 agrs, children 을 받아 자식을 가진 엘리먼트를 만들어낼 수 있다.</p>
<pre><code class="language-js">const logoBox = createElement(
  &quot;div&quot;,
  { class: &quot;logo-box&quot; },
  createElement(&quot;img&quot;, {
    src: &quot;./img/logo-alpha.svg&quot;,
    alt: &quot;hangbata logo image&quot;,
  })
);

const logoContainer = createElement(
  &quot;div&quot;,
  { class: &quot;logo-container&quot; },
  logoBox,
  titleBox
);</code></pre>
<p>사용처는 약간 이런 느낌.</p>
<p>이게 되돌아보니 더 마음에 드는 이유가, 레벨 2에서 리액트 딥다이브 스터디를 하며 알게된 건데, 이게 사용처의 모습과 내부 구현 로직이 리액트의 실제 <code>createElement()</code>와 유사했다.</p>
<p>뭔가 리액트에 createElement라는 게 있는지도 몰랐고 내부 구현은 더더욱 몰랐는데 함께 고민해서 정리하고 만든 함수가 리액트의 코드와 비슷했다니까 신기하고 뿌듯한 느낌...ㅎㅎ</p>
<p>이 외에 기능 구현을 하면서, 바닐라 자바스크립트로 아바타 사이트를 구현하는 동안 고민했던 지점과 새로 쌓은 인사이트가 굉장히 많았는데...시간이 너무 지나버렸다...</p>
<p>간단하게 돌아보자면, 드래그로 아바타의 요소를 꾸미고 수정하는 기능과 그렇게 꾸민 파일을 svg/png 파일로 저장하는 로직을 구현할 때 많은 고민을 하기도 하고 애를 먹었던 것 같다.</p>
<p>드래그로 아바타의 요소 위치를 수정하고 꾸밀 수 있는 기능의 경우
<code>&lt;canvas&gt;</code> 태그를 사용하기 vs DOM을 순수 조작하게 하기
사이에서 고민하다가 후자의 방법을 선택한 뒤, 파일을 저장할 때는 png 파일 다운로드 시에 svg를 canvas에 모아 렌더링하는 방식을 선택하였다.</p>
<h1 id="3-배포">3. 배포</h1>
<blockquote>
<p>행바타 사이트: <a href="https://woowa-toy-lab.github.io/hangbata/">https://woowa-toy-lab.github.io/hangbata/</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/c68c0854-07bc-4044-8d3d-83e8a042a429/image.png" alt=""></p>
<p>그렇게 완성된 사이트와 우테코 채널 홍보글 캡쳐이다.
방학이 끝나고도 1달이 넘게 지나 기억이 많이 휘발된 상태에서 마무리하는 회고글이다보니 뭔가 급마무리되는 느낌인데...ㅠㅠ 다음 프로젝트를 할 때는 정말 매일 회고를 남겨야겠다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 레벨 1을 마치며]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EB%A0%88%EB%B2%A8-1%EC%9D%84-%EB%A7%88%EC%B9%98%EB%A9%B0</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EB%A0%88%EB%B2%A8-1%EC%9D%84-%EB%A7%88%EC%B9%98%EB%A9%B0</guid>
            <pubDate>Thu, 03 Apr 2025 05:59:33 GMT</pubDate>
            <description><![CDATA[<h1 id="1-레벨-1-미션-목록-📄">1. 레벨 1 미션 목록 📄</h1>
<p>레벨 1에서는 바닐라 자바스크립트로 웹 UI를 구현하였다. 처음부터 뭔가 거창하게 배우겠다는 마음보다는, 하나하나 만들어가면서 내 코드를 좀 더 읽기 좋게, 다루기 쉽게 만드는 데 집중했던 것 같다. 그러다 보니 자연스럽게 ‘이건 왜 이렇게 짰지?’, ‘이 부분은 굳이 객체로 만들어야 했나?’ 같은 고민을 하게 되고, 그러면서 내가 작성한 코드에 대해 책임을 져가게 되었다.</p>
<p>이전엔 그냥 작동하면 됐지 싶었는데, 지금은 작동하는 것보다 “어떻게 작동하게 했는지”가 더 신경 쓰이기 시작했다. 물론 아직도 모르는 것도 많고, 헷갈릴 때도 많지만, 그래도 전보다는 확실히 단단해졌다는 느낌이 든다.</p>
<p>오늘은 레벨 1을 마치며 느끼고 배운 하드 스킬과 소프트 스킬에 대해 짧게 회고해보고자한다.</p>
<h2 id="11-pr-링크-🔗">1.1. PR 링크 🔗</h2>
<blockquote>
<h3 id="1주차-자동차-경주">1주차: 자동차 경주</h3>
<ul>
<li>1단계: <a href="https://github.com/woowacourse/javascript-racingcar/pull/338">https://github.com/woowacourse/javascript-racingcar/pull/338</a></li>
</ul>
<h3 id="2-3주차-로또">2-3주차: 로또</h3>
<ul>
<li>1단계: <a href="https://github.com/woowacourse/javascript-lotto/pull/352">https://github.com/woowacourse/javascript-lotto/pull/352</a></li>
<li>2단계: <a href="https://dev-dino22.github.io/javascript-lotto/">https://dev-dino22.github.io/javascript-lotto/</a></li>
</ul>
<h3 id="4-5주차-점심-뭐-먹지">4-5주차: 점심 뭐 먹지</h3>
<ul>
<li>1단계: <a href="https://github.com/woowacourse/javascript-lunch/pull/206">https://github.com/woowacourse/javascript-lunch/pull/206</a> &gt; <a href="https://github.com/woowacourse/javascript-lunch/pull/225">https://github.com/woowacourse/javascript-lunch/pull/225</a> (리뷰어의 merge 실수로 인해 2개로 나뉘어진 PR)</li>
<li>2단계: <a href="https://github.com/woowacourse/javascript-lunch/pull/255">https://github.com/woowacourse/javascript-lunch/pull/255</a></li>
</ul>
<h3 id="6-7주차-영화-리뷰">6-7주차: 영화 리뷰</h3>
<ul>
<li>1단계: <a href="https://github.com/woowacourse/javascript-movie-review/pull/197">https://github.com/woowacourse/javascript-movie-review/pull/197</a></li>
<li>2단계: <a href="https://github.com/woowacourse/javascript-movie-review/pull/246">https://github.com/woowacourse/javascript-movie-review/pull/246</a></li>
</ul>
</blockquote>
<h2 id="12-배포한-미션-웹-링크-🔗">1.2. 배포한 미션 웹 링크 🔗</h2>
<blockquote>
<h3 id="로또">로또</h3>
<ul>
<li><a href="https://dev-dino22.github.io/javascript-lotto/">https://dev-dino22.github.io/javascript-lotto/</a></li>
</ul>
<h3 id="점심-뭐-먹지">점심 뭐 먹지</h3>
<ul>
<li><a href="https://dev-dino22.github.io/javascript-lunch/">https://dev-dino22.github.io/javascript-lunch/</a></li>
</ul>
<h3 id="영화-리뷰">영화 리뷰</h3>
<ul>
<li><a href="https://dev-dino22.github.io/javascript-movie-review/">https://dev-dino22.github.io/javascript-movie-review/</a></li>
</ul>
</blockquote>
<hr>
<h1 id="2-기술-회고-✏️">2. 기술 회고 ✏️</h1>
<p>자동차 경주, 로또 미션에서는 객체와 class, 도메인과 UI의 관심사 분리 등에 대해 깊게 고민해볼 수 있었다. 그리고 점심 뭐 먹지, 영화 리뷰 미션에서는 컴포넌트와 함수, API 요청 및 비동기와 이벤트 루프에 대해 공부해볼 수 있었다.</p>
<p>매주 미션을 진행하며 리뷰어에게 리뷰를 받고 크루들과 소통하며 너무 많은 것들을 배웠기에 이 회고에 학습한 모든 것을 정리할 순 없겠지만,</p>
<p>기억에 남는 핵심적인 학습을 키워드 중심으로 짧게 돌아보고자 한다.</p>
<h2 id="21-객체와-class">2.1. 객체와 class</h2>
<h3 id="211-class의-오남용">2.1.1. class의 오남용</h3>
<p>프리코스 때부터 별 고민 없이 관습적으로 써왔던 class의 사용에 대해 다시 돌아보게 되었다.</p>
<p>객체 !== class 이며, class !== 객체지향 프로그래밍 이다. 자바스크립트는 프로토타입 기반의 언어이다. 자바스크립트에 class가 등장하게 된 배경은 자바와 같은 객체지향 언어를 하다가 온 개발자가 익숙한 문법으로 작성할 수 있게 하기위해서였다. 일종의 syntax sugar 인 셈인데, 자바스크립트의 class는 기존 프로토타입 기반 상속 매커니즘을 추상화한 문법이기 때문에 자바의 class와 상당히 다르다고 한다.</p>
<p>(관련 링크: <a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain">https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain</a>)</p>
<p>이처럼 자바스크립트는 본래 프로토타입 기반의 언어이고 class 문법 또한 이를 추상화한 것에 지나지 않기 때문에, class를 사용하지 않고도 충분히 객체지향 프로그래밍이 가능하다.
무분별한 class 사용은 의미없이 가독성 떨어지고 비효율적인 코드를 만들 수 있다.</p>
<p>이러한 개념에 대해서 리뷰어와 소통하고 개인적으로 학습하면서, 기존의 내 코드는 별 고민없이 프리코스 때 남들이 그렇게 썼으니까, class를 남발하며 작성하였음을 알게되었다. 그리고 앞으로 개발할 때 어떤 상황에서 class를 써야하는지에 대해 나만의 기준을 세우게 되었다.</p>
<ul>
<li>복잡한 상태를 가지는지?</li>
<li>인스턴스 만들어 여기저기 재사용을 해야하는지?</li>
<li>상속/조합 이 필요한 상황인지?</li>
</ul>
<h3 id="212-class의-상속과-조합">2.1.2. class의 상속과 조합</h3>
<p>class의 상속과 조합 활용에 대해서도 학습하게 되었다. 상속은 코드 재사용 측면에서 강력한 문법이지만, 잘못 사용하면 오히려 코드의 유연성을 떨어뜨릴 수 있다는 점을 배웠다. 상속보다 조합(Composition)이 더 적합한 경우도 많다는 것을 느꼈고, 로또 2단계 리팩토링에서 조합의 개념을 적용해보았다.</p>
<h3 id="213-모델의-역할과-중간-계층-객체">2.1.3. 모델의 역할과 중간 계층 객체</h3>
<p>로또 미션까지 MVC 패턴을 적용하면서 Model과 View 사이의 역할을 Controller가 어떻게 잘 분리하고 중재할 수 있을지 고민해보게 되었다.</p>
<p>기존의 나의 코드에서는 모델을 마치 상태를 가진 데이터 저장소처럼 작성하고 사용하였다.
하지만 모델은 자신의 역할을 책임지고 메세지를 던지는 주체여야한다고 생각하게 되었다.</p>
<p>또한 이렇게 작성하니 이전보다는 컨트롤러의 과중한 부담이 덜어지긴 했지만, 그럼에도 컨트롤러가 여전히 지닌 책임이 너무 많다는 생각이 들었다.</p>
<p>그래서 컨트롤러와 모델의 역할에 대해 계속 고민하고 여러 피드백들을 보면서 중간 레이어 객체를 만들게 되었다.</p>
<p>모델들의 의존성 주입을 받아 컨트롤러와 소통하는 모델이며 이를 구현하기 위해 배웠던 조합(Composition)을 로또 미션에 적용해보았다.
의존성 주입으로 결합도를 낮추는 시도와 캡슐화로 응집도를 높이려는 노력이었다.</p>
<p>LottoMachine이라는 중간 계층 모델을 만들어 Lotto 모델을 의존성으로 주입해 주면서 추후 미국 로또 추가와 같은 로또의 형식 자체가 변경될 경우에도 LottoMachine은 계속 활용할 수 있도록 확장성을 고려하여 설계하였다. 이렇게 나의 모델은 결합도를 낮출 수 있었다.
또한 도메인 모델들과 컨트롤러의 책임을 분리하기 위해 모델의 캡슐화에 대해서도 고민하였다. </p>
<p>그렇게 나의 모델은 자신의 데이터는 자신이 책임지고 관리하며, 필요한 데이터만 가공해서 내보내므로 프라이빗 필드를 get해야 하는 상황을 피할 수 있게 되었다.</p>
<h2 id="22-테스트-코드-작성">2.2. 테스트 코드 작성</h2>
<h3 id="221-tdd">2.2.1. TDD</h3>
<p>기능을 구현하기 전에 테스트부터 먼저 작성하는 TDD 방식은 처음에는 낯설고 버거웠다. 그래도 점차 구현보다 먼저 요구사항을 정리하고, 작은 단위로 명확하게 기능을 나누는 사고 방식이 길러졌던 것 같다. 테스트가 잘 통과되면 마치 게임 퀘스트에 통과한 느낌이라 묘한 즐거움도 있었다. 리팩토링할 때도 불안감 없이 코드를 고칠 수 있어 구현과 테스트를 번갈아가며 점진적으로 완성해가는 흐름이 생각보다 생산적이었다.</p>
<p>하지만 테스트를 먼저 작성해야 한다는 압박감 때문에 구현보다 테스트를 위한 고민에 시간을 더 많이 쓰는 느낌이 들기도 했다. 특히 처음 접하는 문제나 연결이 복잡한 흐름의 경우, 테스트 코드를 먼저 어떻게 짜야 할지 막막해서 손이 멈추는 순간도 많았다. 또, 테스트 코드 작성 자체가 익숙하지 않다 보니 오히려 테스트가 리팩토링을 방해하거나, 코드의 유연함을 해치는 것처럼 느껴질 때도 있었다. </p>
<p>TDD는 분명 강력한 도구지만, &#39;무조건 TDD를 해야 한다&#39;는 생각보다는 상황에 따라 적절하게 활용하는 유연함이 필요할 것 같다는 생각이 들었다. 특히 로직이 복잡하고 안정성이 중요한 부분에 집중적으로 적용하고, 그렇지 않은 부분은 기능 구현에 먼저 집중하는 식의 균형 잡힌 접근이 더 현실적이라는 걸 느꼈다.</p>
<h3 id="222-단위-테스트--e2e-테스트">2.2.2. 단위 테스트 / E2E 테스트</h3>
<p>초기에는 단위 테스트 중심으로 jset를 이용하여 작은 함수의 동작만 테스트했다. 테스트가 실패할 경우 무엇이 잘못되었는지 쉽게 알 수 있었기에 기능 구현과 리팩토링을 하는 때 모두 유용하게 사용할 수 있었다. 추가로, utils나 validator에 대한 테스트 코드를 작성하는 것도 개인적으로 괜찮았던 것 같다. 도메인 로직에 해당 함수들을 사용할 때 오류가 난다면, 도메인의 문제이고 유틸 함수들은 문제 없다고 믿고 작성할 수 있다는 관점에서 말이다. 그런데 반대로 꼭 작성해야하냐는 리뷰어의 의견도 있었던 걸로 기억한다. util 에 문제가 있는 거라면 어차피 모든 도메인 로직에서 일괄적으로 오류가 터질텐데, 오히려 범용적으로 사용하는 util이야 말로 간접적으로 항상 테스트가 되고있는 것 아니냐는 말이었다. 일리가 있다고 생각하지만, 그래도 도메인 로직이 모두 완성되기 전, 구현 단계에서는 util이나 validator 코드를 &#39;믿고&#39; 쓸 수 있다는 것만으로 시간 단축이 꽤 된다고 생각하기 때문에 나는 뭐...작성해도 괜찮은 것 같다.</p>
<p>웹 UI 미션으로 넘어오면서는 사용자 흐름을 검증하는 E2E 테스트도 cypress로 작성하게 되었다. 특히 영화 리뷰 미션에서는 실제 사용자가 페이지를 어떻게 탐색하고, 어떤 액션을 하는지를 기준으로 테스트를 구성하면서 기능이 제대로 연결되는지를 점검할 수 있었다. 또한 서버의 응답도 테스트하고, fixture로 
다양한 테스트 방식을 경험하면서 테스트는 단순한 보조 수단이 아니라 설계와 구현의 기준점이 될 수 있다는 걸 배웠다.</p>
<h2 id="23-컴포넌트">2.3. 컴포넌트</h2>
<h3 id="231-컴포넌트를-나누는-기준">2.3.1. 컴포넌트를 나누는 기준</h3>
<p>점심 뭐 먹지와 영화 리뷰 미션을 통해 컴포넌트를 어떤 기준으로 나눌 것인지 깊게 고민해보고 나만의 기준을 세울 수 있었다. 초반에는 단순히 UI 요소 하나하나를 컴포넌트로 분리하거나, 화면상에서 나뉘는 영역을 기준으로 컴포넌트를 쪼갰다. 하지만 점차 기능의 응집도와 관심사를 기준으로 나누는 것이 유지보수와 확장성에 더 유리하다는 것을 체감하게 되었다. </p>
<p>컴포넌트는 자신의 역할을 명확하게 가지고 있어야 하고, 내부 상태와 외부에서 주입받는 데이터 간의 경계를 분명히 해야 재사용성이 높아진다는 것을 알게 되었다. 이를 기반으로 <code>MovieList</code>, <code>ScrollObserver</code>, <code>StarRatingForm</code> 등의 컴포넌트를 각각의 목적에 따라 나누고, 각자 이벤트 등록부터 렌더링까지 자신이 책임지도록 구현했다. 이러한 코드를 짜기 위해 리액트의 props 전달을 모방한 인자전달에 따른 구조 생성, 상태 변화에 따른 리렌더링을 위한 콜백 함수의 이해 등등 나 혼자 했다면 이해하기 너무 힘들었을 개념과 시도들이 많았는데, 마침 이런 방식을 해보고싶던 차에 이런 프로그래밍에 능한 페어와 만나게되어 도전해볼 수 있었다. 사실 당시의 나에겐 어려운 도전이긴 했어서 바로 체득하진 못했었지만 계속 이 때 배웠던 개념을 기반으로 리팩토링을 하고 차근차근 활용도 해보면서 이젠 드디어 이해도 되고 나만의 방식으로 작성도 할 수 있게된 것 같다! 우테코는 정말 코치분들 뿐만 아니라 크루들에게도 많이 배워서 혼자 학습할 땐 엄두도 못낼 시도들을 해볼 수 있다는 점이 너무 감사하고 좋은 것 같다.</p>
<h3 id="232-컴포넌트의-관심사---기능-조직-vs-목적-조직-응집도">2.3.2. 컴포넌트의 관심사 - 기능 조직 v.s. 목적 조직 (+응집도)</h3>
<p>기능 조직과 목적 조직. 점심 뭐 먹지 리뷰어 해먼드에게 처음 들었던 생소한 개념이다.
<img src="https://velog.velcdn.com/images/dev-dino22/post/0b2bbe96-89a0-4541-849a-eaec09e02de2/image.png" alt=""></p>
<p>컴포넌트 고민을 시작했던 점심 뭐 먹지 미션에서는 리뷰어 덕분에 정말 생소하고 낯선 관점에서부터 코드를 고민해볼 수 있었다.</p>
<h2 id="24-비동기와-웹-api-이벤트-루프">2.4. 비동기와 웹 API, 이벤트 루프</h2>
<h3 id="241-비동기">2.4.1. 비동기</h3>
<p>비동기 처리 흐름에서는 <code>fetch</code>, <code>async/await</code>, <code>then/catch</code>를 활용해 API와 통신하고, 오류를 잡는 흐름을 처음으로 경험해볼 수 있었다. 특히 영화 리뷰 미션에서는 서버 요청 타이밍과 로딩 처리, 에러 핸들링까지 하나의 흐름으로 설계해야 했기 때문에 자바스크립트의 이벤트 루프와 비동기 큐, 콜스택에 대한 이해가 필요했다. </p>
<p>자바스크립트가 단일 스레드 환경에서도 브라우저의 멀티 스레드를 활용해 비동기를 효과적으로 처리하는 구조라는 것을 알게 되었고, 그 원리에 기반한 UI 구성은 단순히 작동하는 코드 이상으로 사용성과 안정성에 직접적으로 연결된다는 것을 느꼈다.</p>
<h2 id="25-서버-api">2.5. 서버 API</h2>
<h3 id="251-도메인-객체">2.5.1. 도메인 객체</h3>
<p>서버에서 받은 데이터를 그대로 사용하는 것이 아니라, 클라이언트 관점에 맞게 변환하고 추상화하는 중간 계층의 필요성을 체감하게 되었다. 서버 응답 구조가 프론트엔드에서 필요한 형태와 맞지 않거나 네이밍이 일치하지 않는 경우, 서버 API를 변경하게될 경우 등등의 케이스를 대비해 이를 적절히 가공하는 도메인 객체를 별도로 두어 응집력 있는 데이터 구조를 구성하는 것이 핵심 포인트였다.</p>
<p>추가로, 하리의 피드백에서 &#39;서버 네이밍에서는 스네이크 케이스를 많이 쓰고 클라이언트 네이밍은 카멜케이스를 쓰는 경우가 많아, 네이밍을 변환해주는 유틸함수를 만들어 쓰기도 한다&#39;라는 흥미로운 의견을 받을 수 있었다. 이번 미션에서는 시간에 쫓겨 제출하느라 해당 유틸 함수를 만들진 못했지만...방학동안의 리팩토링이나 다음 미션에서 도전해볼만한 재밌는 코드인 것 같다.</p>
<h2 id="26-이벤트">2.6. 이벤트</h2>
<h3 id="261-이벤트-위임">2.6.1. 이벤트 위임</h3>
<p>반복적인 querySelector와 addEventListener 와 같은 DOM 조작은 비용이 많이 드는 작업임을 알게 되었다. 그래서 자바스크립트의 이벤트 버블링을 통해 적절한 부모 요소에서 이벤트를 위임하는 방식으로 DOM 조작을 최소화하는 것이 성능상 좋다. 이를 내 코드에 점진적으로 반영해가며 최적화된 이벤트 핸들링에 대해서도 고민을 많이 하게 되었다.</p>
<h3 id="262-이벤트-관리">2.6.2. 이벤트 관리</h3>
<p>원래 나는 html 친화적으로(?) 최대한 활용하며 작성해보고 싶어서 dataset을 이용해 이벤트 메서드를 붙여주고 전체 DOM에 click 이벤트를 걸어 하나의 ClickEvent.js 파일에서 모든 이벤트 메서드들을 관리했었다. 도전적인 측면에서 나쁘진 않았다고 생각하지만, XSS 공격에 취약하다던가 프로그램 품이 커질 수록 유지보수성이 떨어진다던가 하는 등등의 이유로 이러한 관리방식은 결국 버리게 되었다.</p>
<p>그리고 대신 목적 조직에 대한 키워드를 학습하며 컴포넌트가 마치 객체가 자신의 역할을 자신이 책임지듯, 자신의 이벤트를 붙이고 핸들링하는 식으로 변경하였다.
개인적으로 우리가 로또 미션까지는 도메인 로직과 UI 로직의 분리, 관심사 분리에 학습 목표를 세우고 공부했었기 때문에 이러한 방식이 처음엔 거부감이 들었던 것 같다. 하지만 해먼드의 목적 조직 피드백도 있었고 이런 방식으로 작성했던 써밋에게 의견을 물어봤을 때 돌아온 답변에 설득되어서 코드 구조를 바꾸게 되었다.</p>
<p>내 기존 코드처럼 이벤트 등록/이벤트 관리/컴포넌트 등을 따로 관리하다보면 해당 컴포넌트가 삭제될 경우 최소 3개 이상의 파일을 수정하고 건드려야하는 번거로움이 있다. 하지만 컴포넌트가 자신의 역할을 다 책임진다면 컴포넌트를 삭제할 때도 그 컴포넌트 파일 하나만 딸깍 지워주면 될 것이다. 그래서 유지보수 측면에서도 장점이 있다고 말했다. 음, 설득됐음.</p>
<h3 id="263-addeventlistener--removeeventlistener">2.6.3. addEventListener &amp; removeEventListener</h3>
<p>하리의 피드백 덕분에 자바스크립트에서 addEventListener로 붙여준 이벤트는 계속 중복 등록될 수 있고 해당 요소가 DOM에서 사라지더라도 이벤트가 사라지진 않기에 removeEventListener를 제때 해주지 않으면 메모리 누수로 이어질 수 있다는 것을 알게 되었다. 또한, 이벤트를 등록한 대상의 이벤트를 정확히 지워주려면 이벤트 리스너 두 번째 인자로 들어가는 콜백 함수가 익명 함수가 아닌 기명 함수로 전달되어야 제대로 집힌다는 것을 알았다. 해당 개념을 학습하면서 클래스 내에서 이벤트를 붙여줄 때 클래스 내의 메서드를 쓰겠다고 this를 붙여주면 나중에 this 바인딩이 깨져 의도치 않은 동작이 될 수 있다는 것도 학습할 수 있었다.</p>
<hr>
<h1 id="3-감정-회고-💭">3. 감정 회고 💭</h1>
<p>우테코에서는 단순히 하드 스킬 학습 뿐만 아니라, 매주 페어 프로그래밍, 유연성 강화 워크숍과 같은 다양한 활동을 하기 때문에 소프트 스킬의 역량도 키울 수 있다. 우테코 환경자체가 크루원들끼리의 커뮤니케이션이 매우 활발하고 친화적이며 모든 크루가 열정을 갖고 몰입된 우테코 생활에 임하기 때문에 나도 자연스럽게 자극받고, 혼자였다면 시도하지 않았을 성장의 기회를 얻게 된다.</p>
<p>이런 배경으로 인해 정말 바쁘게 지나가버린 레벨1에서 연극조 크루들과 자주하는 이야기가 있다.
&#39;개발 공부 뿐만 아니라 인생 공부를 하게 해주는 우테코! 사람을 만들어준다!ㅋㅋ&#39;</p>
<p>이런 우스갯소리를 진담처럼 하는만큼, 우테코 생활을 하며 느끼고 내면적으로 성장한 바가 크기 때문에 감정 회고도 짧게 해보려한다.</p>
<h2 id="31-레벨-1에서의-의미있는-변화">3.1. 레벨 1에서의 의미있는 변화</h2>
<p>일단 어떻게든 혼자 해결하며 남에게 물어보기는 절벽 끝에 매달려 외치는 최후의 SOS였던 프리랜서의 오랜 습관이 고쳐졌다. 이건 사실 프리랜서여서라기보다 이런 성향 때문에 프리랜서를 택한 것도 크기 때문에 정말 나의 오랜 성향에 변화가 생긴 셈이다.</p>
<p>구체적으로 말하자면 지금은 모르는 게 생기면 크루들에게도 바로바로 물어보고 고민도 해보고 리뷰어와도 활발한 소통을 하는 모습으로 바뀌었다.
다른 사람의 시간을 쓸데없이 뺏는 것 같아서, 모르는 것을 물어보기 머쓱해서 등등의 이유로 눈치를 보며 혼자 끙끙댔던 과거와 비교해보면, 지금의 습관이 훨씬 긍정적인 것 같다.</p>
<p>더 짧은 시간 내에 빨리 해결하고 많이 배우며 성장할 수 있었기 때문이다.</p>
<h3 id="311-의미있는-경험을-할-수-있었던-이유">3.1.1. 의미있는 경험을 할 수 있었던 이유</h3>
<p>크루들이 있었기 때문이다! 개발을 시작한지 얼마 되지 않아 헛소리도 많이하고 아주 기초적인 질문들도 자주 하였는데 단 한 명도, 단 한 번도 짜증내거나 무시한 사람은 없었다. 모두가 친절하게 자신의 고민처럼 깊게 생각하고 알려주었고 덕분에 기죽지 않고 모르는 건 공부하면 된다는 마인드를 가질 수 있었다.</p>
<h3 id="32-유연성-강화를-위해-시도했던-것">3.2. 유연성 강화를 위해 시도했던 것</h3>
<p>쭈뼛쭈뼛 크루들에게 모르는 걸 물어보러가던 그 때 기분이 생각난다. <del>...그거 사실 엄청난 용기였어... 정말 혼자 도저히 모르겠어서 물어보고싶은데 물어봐도 될까 고민을 1시간을 하고 있었거든, 바로 옆자리에서...ㅋㅋ</del></p>
<p>하지만 지금은 모두가 나의 gpt. 감사합니다 크루들...특히 레벨 1 내내 언제나 선뜻 도움을 주고 힘을 주었던 우리 연극조, 데일리미팅조에게 압도적 감사를 . . .🥹
나도 얼른 도움줄 수 있는 크루원이 되어야겠다.</p>
<p>그리고 첫주차에는 리뷰어와 소통도 제대로 하지 않았었는데
지금은 저번 미션도, 이번 미션도 1단계 피드백에서 코멘트 70개가 넘어가는 매우 큰 변화가 생겼다.</p>
<p>코딩이라는 게 이렇게 코드리뷰를 활발하게 주고받을 때 많이 배우고 성장할 수 있는 것 같아서, 개발자들의 활발한 커뮤니티가 이해되었다.</p>
<hr>
<h1 id="4-앞으로의-계획-및-다짐🫡">4. 앞으로의 계획 및 다짐🫡</h1>
<h2 id="41-방학-때">4.1. 방학 때</h2>
<h3 id="411-공부-목록">4.1.1. 공부 목록</h3>
<ul>
<li>미션 구현하느라 급급해 쌓여만 갔던 학습 부채 해소하기...복습하자.</li>
<li>주렁과 코어 자바스크립트 완독 목표</li>
</ul>
<h3 id="412-프로젝트">4.1.2. 프로젝트</h3>
<ul>
<li>캉골과 토이 프로젝트 (대략 4일 정도?)</li>
</ul>
<h3 id="413-테코톡-발표-준비">4.1.3. 테코톡 발표 준비</h3>
<ul>
<li>아직 레벨2에 할지 3에 할지 안 정했지만...슬슬 준비 시작해야될 듯. 레벨1도 미션에 허덕였는데...미리미리 준비해둬서 나쁠 것 없을 것 같다.</li>
<li>지금 아이디어로는 프론트엔드이고 모바일안드로이드도 같이 있는 선릉캠이니만큼 UI/UX와 연관된 주제는 어떨까싶다. UIUX도 내 학습 목록에 있기도 하고.</li>
</ul>
<h2 id="42-레벨-2-때">4.2. 레벨 2 때</h2>
<h3 id="421-새롭게-설정하는-레벨2-일상-루틴">4.2.1. 새롭게 설정하는 레벨2 일상 루틴</h3>
<ul>
<li>매일 짧게라도 오늘 뭘 했는지, 어땠는지 메모해놓기. 특히 감정. 시간이 많이 지나서야 회고를 쓰게될 때 참고하기 좋을 것 같다.</li>
</ul>
<h3 id="422-예정된-스터디">4.2.2 예정된 스터디</h3>
<p>(유지)</p>
<ul>
<li>코딩테스트 스터디</li>
<li>PR 리뷰 스터디</li>
<li>블로그 회고 스터디</li>
</ul>
<p>(New!)</p>
<ul>
<li>모던 리액트 딥다이브 스터디</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 3주차 회고 - 로또 웹 UI]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EB%A1%9C%EB%98%90-%EC%9B%B9-UI-%EC%9E%91%EC%84%B1-%EC%A4%91</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EB%A1%9C%EB%98%90-%EC%9B%B9-UI-%EC%9E%91%EC%84%B1-%EC%A4%91</guid>
            <pubDate>Mon, 03 Mar 2025 19:17:31 GMT</pubDate>
            <description><![CDATA[<h1 id="1-☝️-1단계--로또">1. ☝️ 1단계 : 로또</h1>
<blockquote>
<p>제출한 웹 로또 링크
<a href="https://dev-dino22.github.io/javascript-lotto/">https://dev-dino22.github.io/javascript-lotto/</a></p>
</blockquote>
<h2 id="11-구현한-uxui-설명">1.1 구현한 UX/UI 설명</h2>
<h3 id="ui-설명">UI 설명</h3>
<h4 id="1-유효성-검증-ui">1. 유효성 검증 UI</h4>
<p><strong>1차 시도</strong>
실시간 input Event로 유효성 검사로 유효한 값을 입력할 때까지 통과되지 않은 로직을 빨간색 에러로 표시</p>
<img width="444" alt="스크린샷 2025-03-03 오전 1 47 16" src="https://github.com/user-attachments/assets/9db37f93-7bcc-4b75-97ee-8b3ea462d60b" />

<p>제공된 기본 로또 UI 시안의 경우, 사용자가 구매금액을 얼마를 입력해야하는지, 로또 당첨번호의 숫자 범위는 어디까지인지 등, 유효값을 직관적으로 알기 힘든 한계가 있다고 생각.</p>
<p>-&gt; 그래서 사용자의 input 이벤트를 실시간 감지하여 유효값을 검사해주는 로직이 있으면 좋겠다.</p>
<p>-&gt; 그래서 처음엔 빨간글씨로 유효하지 않은 이유를 렌더링하고 유효한 값이 모두 입력될 시, 검증 문구가 사라지도록 설계</p>
<p>-&gt; 그런데, 크루들과 부모님께 사용자 경험을 물었더니 &quot;아직 다 치지도 않았는데 빨간 글씨 뜨는 게 기분 나쁘다.&quot;, &quot;검증 문구가 생겼다 사라졌다 하면서 박스가 움직이는 게 거슬린다&quot; 등의 피드백을 받음</p>
<p>-&gt; 그래서 아래와 같이 다시 UI를 재구성하였음</p>
<p><strong>2차 시도</strong>
<img width="442" alt="스크린샷 2025-03-03 오후 4 03 40" src="https://github.com/user-attachments/assets/1f545c68-b030-4a2c-b0ac-d3fd2c9e1f74" /></p>
<img width="324" alt="스크린샷 2025-03-03 오후 4 29 38" src="https://github.com/user-attachments/assets/92875a6c-ecf1-464a-91c5-95c66d5151da" />


<p>회색 글씨로 유효값 안내 문구를 기본으로 띄우고 해당 유효값을 충족할 시 X 표시가 V 표시가 되도록 변경</p>
<img width="398" alt="스크린샷 2025-03-03 오후 4 31 41" src="https://github.com/user-attachments/assets/8a4140dd-0eb2-48ae-a39f-4091e72b86b2" />
<img width="401" alt="스크린샷 2025-03-03 오후 4 32 15" src="https://github.com/user-attachments/assets/9b205665-4eab-486f-af2c-d2580cd6f2f4" />

<p>또한 구입 입력 금액 폼의 경우, 유효한 값을 입력할 때까지 disabled 처리를 해놓았으며, 구입이 진행된 후 추가구매를 할 수 없도록 이 역시 disabled 처리</p>
<img width="474" alt="스크린샷 2025-03-03 오후 4 32 43" src="https://github.com/user-attachments/assets/807248dc-c301-40b4-aba2-607b34257ee6" />

<p>결과확인 버튼의 경우에는 따로 disabled 처리를 하지 않고 유효하지 않은 채로 버튼 클릭 시 안내 문구를 다시 확인해달라는 alert을 띄움.</p>
<p>왜냐하면, 구입 금액 버튼도 디세이블 처리된 상태인데 복잡한 유효값을 만족할 때까지 결과 확인 버튼도 disabled 되어있는 것이 사용자에게 답답함을 야기시킬 것 같았다.</p>
<h4 id="2-편의성---복사-붙여넣기-기능-추가-구현">2. 편의성 - 복사 붙여넣기 기능 추가 구현</h4>
<img width="483" alt="스크린샷 2025-03-03 오후 5 02 17" src="https://github.com/user-attachments/assets/b4b5e7ad-56fa-45bd-9d8e-6085b8a18fda" />

<p>발행된 로또 리스트를 클릭하면, 클릭한 요소의 로또 번호가 복사됨</p>
<img width="395" alt="스크린샷 2025-03-03 오후 5 02 36" src="https://github.com/user-attachments/assets/fce91242-9eda-4771-ad07-1077c586790f" />

<p>그리고 당첨번호 필드에 붙여넣으면 자동으로 스플릿되어 필드값이 채워짐</p>
<p>이러한 기능을 위해 &#39;,&#39;과 공백을 받아 처리를 해주어야해서 input 태그는 number 타입이 아닌 text 타입으로 해두고 0-9 숫자와 &#39;,&#39;.&#39; &#39; 이 아닌 입력 시 입력값이 지워지도록 따로 구현함</p>
<h4 id="3-모달-ui">3. 모달 UI</h4>
<img width="500" alt="스크린샷 2025-03-03 오후 5 11 27" src="https://github.com/user-attachments/assets/e8da0d48-310a-41d4-be2b-39b723606e62" />

<p>모달의 경우 엑스 버튼과 더불어 &#39;esc 키 입력&#39;,&#39;모달창 밖 배경 클릭&#39; 시에도 모달이 닫히도록 구현</p>
<ul>
<li>memo. 이벤트 위임을 이용해 구현하는 과정에서 모달박스를 눌러도 닫히는 버그 발생. fix 예정</li>
</ul>
<hr>
<h3 id="도메인-설명">도메인 설명</h3>
<h4 id="1-이벤트-위임을-통한-addeventlistener-사용-최소화">1. 이벤트 위임을 통한 addEventListener() 사용 최소화</h4>
<pre><code class="language-js">class ClickEvent {
  constructor(elem) {
    elem.addEventListener(&quot;click&quot;, this.onClick.bind(this));
  }

  reload() {
    location.reload();
  }

  showModal(element) {
    if (!element) return;
    document.getElementById(&quot;modalBackground&quot;)?.classList.add(&quot;show&quot;);
  }

  removeModal(element) {
    if (element.id === &quot;modalBackground&quot; || element.id === &quot;closeModalBtn&quot;) {
      document.getElementById(&quot;modalBackground&quot;)?.classList.remove(&quot;show&quot;);
      return;
    }
  }
...
  onClick(event) {
    let target = event.target.closest(&quot;[data-action]&quot;);

    if (!target) return;

    if (
      target.dataset.action &amp;&amp;
      typeof this[target.dataset.action] === &quot;function&quot;
    )
      this[target.dataset.action](target);
  }
}

new ClickEvent(document);</code></pre>
<img width="221" alt="스크린샷 2025-03-03 오후 5 22 45" src="https://github.com/user-attachments/assets/f7ad11a7-6491-4f44-b012-08f8249b8b9e" />

<p>저의 로또페이지의 경우, [&#39;click&#39;, &#39;input&#39;, &#39;submit&#39;] 의 이벤트리스너 등록이 많은 영역에서 필요했습니다. 이러한 이벤트리스너를 모든 요소마다 새로 등록하는 것이 비용이 너무 많이 드는 로직인 것 같아 이벤트 위임의 방식을 이용해 위 코드처럼 document 단위에서 최초 한 번 이벤트 리스너를 등록해두고 이벤트 종류 별로 클래스를 만들어 한 곳에서 관리하고 사용할 요소에 <code>data-*</code> 속성을 추가해주었습니다.</p>
<h4 id="2-모달-ui">2. 모달 UI</h4>
<p>구현에 <template> 과 cloneNode() 를 이용해 innerHTML 사용 지양</p>
<pre><code class="language-html"> &lt;template id=&quot;modalTemplate&quot;&gt;
          &lt;div class=&quot;modal-box&quot; id=&quot;modalBox&quot;&gt;
            &lt;div class=&quot;modal-content&quot;&gt;
            &lt;div class=&quot;close-modal-btn-box&quot;&gt;
              &lt;div class=&quot;close-modal-btn&quot; id=&quot;closeModalBtn&quot; data-action=&quot;removeModal&quot;&gt;
                &lt;svg width=&quot;14&quot; height=&quot;14&quot; viewBox=&quot;0 0 14 14&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
                  &lt;path d=&quot;M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z&quot; fill=&quot;black&quot;/&gt;
                &lt;/svg&gt;
              &lt;/div&gt;
            &lt;/div&gt;
              &lt;h2&gt;🏆 당첨 통계 🏆&lt;/h2&gt;
              &lt;table&gt;</code></pre>
<pre><code class="language-js">function openModal() {
  const modalBackground = getHTML(&quot;modalBackground&quot;);
  if (modalBackground.querySelector(&quot;.modal-box&quot;)) {
    modalBackground.classList.add(&quot;show&quot;);
    return;
  }
  const modalClone = getHTML(&quot;modalTemplate&quot;).content.cloneNode(true);
  getHTML(&quot;modalBackground&quot;).appendChild(modalClone);
  getHTML(&quot;modalBackground&quot;).classList.add(&quot;show&quot;);
}</code></pre>
<p>기존에는 innerHTML을 사용해 모달을 추가하는 방식이 많았지만, 보안 문제(XSS) 및 성능 문제로 인해 innerHTML을 지양하는 것이 좋다고 판단했습니다.</p>
<p>대신 <template>을 활용하여 cloneNode()를 사용하면, 보다 안전하고 효율적으로 동적으로 요소를 추가할 수 있습니다.
createElement 대신 template 태그를 활용한 이유는, 동적으로 변경되는 부분이 크지 않고 단순 결과를 출력하는 모달의 정적 폼이 외부에 아예 노출되면 안되는 부분은 아니라고 생각해서 createElement보다 비용이 적은 template 방식이 더 적합하다고 생각했습니다.</p>
<p>또한 기존에 모달이 열려 있는 경우 새로 추가하는 것이 아니라, 기존 모달을 다시 활성화하는 방식으로 수정하여 불필요한 DOM 추가를 방지했습니다.</p>
<h3 id="1-이번-단계에서-가장-많이-고민했던-문제와-해결-과정에서-배운-점">1) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점</h3>
<!-- 구현 과정에서 가장 어려웠던 점이나 많이 고민한 점은 무엇인가요?
이를 해결하기 위해 어떤 방법들을 검토하고 시도했으며, 그 과정에서 새롭게 배운 점이 있나요? --> 

<p>이제 노드환경에서 브라우저로 넘어와 UI를 그리게된만큼, 가장 많이 고민한 점은 사용자 경험이었습니다.
이 로또 페이지를 실제로 서비스하게된다면, 사용자로서 어떤 부분이 불편하고 어떤 기능이 추가되었으면 좋겠는지를 생각하면서 요구 기능 이상의 편의성 기능들을 추가했습니다. 그로 인해, 프로그램이 다소 무거워지고 아직 역량부족으로 코드가 지저분해진 부분이 있습니다. 또한, 이러한 고민과 시도들을 적용하느라, 정작 도메인 로직 리팩토링에는 부실했던 것 같아 아쉬웠습니다.</p>
<p>또한 이러한 UI 로직에 개발 시간 기회비용을 많이 소모하느라 디렉터리 구조나 상수화도 신경써 진행하지 못하였습니다🥲</p>
<h3 id="2-이번-리뷰를-통해-논의하고-싶은-부분">2) 이번 리뷰를 통해 논의하고 싶은 부분</h3>
<!-- 구현한 코드와 학습 목표와 관련해 피드백을 받고 싶은 부분이나, 함께 논의해보고 싶은 점 -->
<p>이번 구현 과정에서 고민했던 사항과 더 나은 방향을 찾고 싶은 부분들이 있습니다.</p>
<p>1️⃣ UI 개선과 코드 구조화의 균형
UI/UX를 개선하면서 사용자 경험을 향상시키는 데 많은 시간을 투자했지만, 도메인 로직 리팩토링과 코드 구조화에는 상대적으로 부족함이 있었다고 느꼈습니다.
특히,</p>
<p>이벤트 리스너 최적화(이벤트 위임 방식 도입)
모달, 입력값 검증 로직 분리
불필요한 DOM 탐색 제거
등의 개선을 했지만, 더 효과적인 방식이 있을지 궁금합니다.
👉 질문:
현재 UI 중심으로 작성된 코드가 지나치게 무거워졌다고 느끼는데, UX를 유지하면서도 코드 구조를 더 정리할 방법이 있을까요?
UX 를 위해 추가 기능이 늘어날 수록 코드가 무거워지는데 여기서 UX냐 코드 최적화냐를 판단하는 기준이 있는지 궁금합니다.</p>
<p>2️⃣ 유효성 검사 로직 개선 방향
유효성 검증을 개선하면서 input 이벤트를 실시간 감지하여 사용자가 유효한 값을 입력하도록 유도하는 방식을 도입했습니다.
하지만,</p>
<p>검증 로직이 점점 복잡해지는 문제가 있음
실시간으로 유효성을 검사하는 것이 성능적으로 부담이 될 가능성</p>
<p>👉 질문:
현재는 X/✔ 아이콘으로 즉각적인 피드백을 주고 있는데, 이러한 실시간 피드백보다 blur() 처리됐을 때만 하는 게 비용과 타협했을 때 더 적절한 선택인지, 과도한 처리였는지 리뷰어님의 생각이 궁금합니다!</p>
<p>3️⃣ 최적화할 수 있는 부분 (렌더링 성능, 이벤트 핸들링 등)
현재 UI 이벤트가 많아지면서,</p>
<p>불필요한 리렌더링 발생 가능성
모달, 입력 필드 등에서 불필요한 DOM 조작이 있을 수 있음
각종 이벤트 리스너에서 더 최적화할 부분이 있는지 고민</p>
<p>👉 질문:</p>
<p>현재 코드에서 불필요한 렌더링이나 이벤트 리스너 호출을 더 줄일 방법이 있을까요?
입력값 자동 채우기, 모달 관리 방식 등에서 성능 최적화할 부분이 있다면 어떤 방식이 효과적일까요?</p>
<hr>
<h2 id="12-공부해-볼-키워드">1.2 공부해 볼 키워드</h2>
<ul>
<li>클로저 함수</li>
<li>이벤트 위임</li>
<li>img v.s. background-image</li>
<li>event.preventdefault()</li>
<li><code>&lt;template&gt;</code></li>
<li>innerHTML XSS</li>
<li>event.tartget.closet()</li>
<li><code>&lt;script&gt;</code> : difer, async, module</li>
<li>HTML data-* 속성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 2주차 회고 - 로또]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EB%A1%9C%EB%98%90</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EB%A1%9C%EB%98%90</guid>
            <pubDate>Mon, 03 Mar 2025 19:10:53 GMT</pubDate>
            <description><![CDATA[<h1 id="1-👆-1단계--로또---페어-프로그래밍">1. 👆 1단계 : 로또 - 페어 프로그래밍</h1>
<p>기간: 2/18~2/25</p>
<h2 id="11-제출한-코드">1.1 제출한 코드</h2>
<blockquote>
<p><a href="https://github.com/woowacourse/javascript-lotto/pull/352">https://github.com/woowacourse/javascript-lotto/pull/352</a></p>
</blockquote>
<h3 id="1-이번-단계에서-가장-많이-고민했던-문제와-해결-과정에서-배운-점">1) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점</h3>
<!-- 구현 과정에서 가장 어려웠던 점이나 많이 고민한 점은 무엇인가요?
이를 해결하기 위해 어떤 방법들을 검토하고 시도했으며, 그 과정에서 새롭게 배운 점이 있나요? --> 
<p>크게 두 가지를 중점으로 신경써 작업하였습니다.</p>
<ul>
<li>InputHandler로 반복되는 UI 로직 및 검증을 템플릿화 하고 유연한 에러핸들링 작성</li>
<li>중간 레이어 객체 LottoMachine의 설계</li>
<li>중복 코드 제거 및 적절한 의존성 주입/조합으로 결합도를 낮추고 응집도를 높여 확장성과 유지보수성 높이기</li>
</ul>
<h3 id="2-이번-리뷰를-통해-논의하고-싶은-부분">2) 이번 리뷰를 통해 논의하고 싶은 부분</h3>
<!-- 구현한 코드와 학습 목표와 관련해 피드백을 받고 싶은 부분이나, 함께 논의해보고 싶은 점 -->
<ul>
<li><strong><em>LottoMachine 이 현재 중간 레이어 객체의 역할을 잘 하고 있는지?</em></strong>
LottoMachine은 Lotto, Winnings 클래스와 Main(Controller) 클래스 사이에서 중간 계층 역할을 수행하도록 설계되었습니다. 저는 컨트롤러를 &quot;사람&quot;, 모델을 &quot;로또머신과 로또 머신에 필요한 부품들&quot;, 뷰를 로또머신의 스크린으로 생각하며 리팩토링을 해보았습니다.</li>
</ul>
<p>여기서 컨트롤러는 로또 판매점 사장입니다. 사장은 로또가 어떤 로직으로 발행되고 관리되고 계산되는지 알 필요 없이 구매자가 구매를 원하는 금액을 입력하면 발행된 복권을 확인할 수 있고 당첨번호를 넣으면 당첨 통계 및 수익률을 스크린으로 확인할 수 있으면 된다고 생각했습니다.
그래서 저희의 컨트롤러인 Main은 딱 저 정도의 역할 수행을 합니다.</p>
<p>로또 머신은 여러 기능들을 캡슐화되어 갖고 있는 중간 계층 모델입니다. 예를 들어 사장이 로또 머신에 구입금액만 입력해도 로또 머신은 알아서 순차적으로 메소드들을 실행해 계산된 반환값을 사장에게 보여줍니다.</p>
<p>그 외 Lotto, Winnings 등의 모델은, 로또 머신이 로직을 수행하기 위해 필요한 로또 발행 규칙과 우승자 판별 규칙을 갖고 있는 객체입니다.</p>
<p>여기서 Lotto, Winnings, LottoMachine을 작성하면서 신경쓴 점은,</p>
<ul>
<li>Lotto는 나중에 AmericanLotto등, 다른 구성을 가진 로또가 왔을 때도 대응할 수 있도록 로또 머신의 로또를 조합으로 생성하였습니다.</li>
<li>winnings와 같은 당첨 로직은 추후 변경사항이 생길 수도 있는 부분이라고 생각해서 컨트롤러에서 winnings 인스턴스를 생성해 로또 머신에 인스턴스를 주입하는 식으로 작성하였습니다.</li>
</ul>
<ul>
<li><strong><em>재귀함수에 대한 처리를 이렇게 해도 되는지?</em></strong></li>
</ul>
<pre><code class="language-js">// Main.js - play()
    const isRestart = await Input.restartLotto();
    if (isRestart) await this.play();</code></pre>
<p>저는 원래 while문으로 반복에 대한 처리를 해왔었는데, 이번에 let 사용이 금지되면서 이러한 방법을 써보았습니다.
이런 방식에 문제가 있을까요? 재귀함수라는 게 while이나 try..,catch로 처리해야만하는 로직인지 궁금합니다!</p>
<ul>
<li><p>TDD가 제대로 되었는가?
TDD를 적용하기 위해, 의존성이 가장 낮은 Lotto 클래스부터 단위 테스트를 작성하며 진행했습니다. TDD를 처음 접하는 입장에서 이러한 접근 방식이 맞는지 계속 고민하며 구현을 이어나갔습니다. 단위 테스트가 가능한 모든 로직에 대해 TDD를 적용하려고 했는데, 마지막에 로또 머신에 컨트롤러의 역할이 분담되면서 로또 머신에 대한 테스트는 작성하지 못했습니다 ㅠㅠ 아쉽지만 그 외의 테스트는 단위 테스트가 잘 진행되었는지 궁금합니다!</p>
</li>
<li><p>추가로 아래 설명드리는 InputHandler와 Validator에 대한 케빈의 생각도 궁금합니다🥹</p>
</li>
</ul>
<h3 id="3-설계-설명">+3) 설계 설명</h3>
<p>우선, 가장 난해하게 작성된 부분이 InputHandler와 Validator라 생각해서 두 부분에 대해서 설명을 드리려고합니다...ㅠㅠ</p>
<h4 id="a-inputhandler와-validator의-존재-이유">a. InputHandler와 Validator의 존재 이유</h4>
<blockquote>
<p>&#39;꼭 Validator와 input 함수가 에러로 소통해야 할까?&#39;라는 생각이 들었습니다!
잘못된 입력을 알려주는 것도 View 영역이라면, 판단은 Validator가 하고, 어떤 문자열을 보내줄지는 input 함수에서 책임지는 방법은 어떤가요?</p>
</blockquote>
<p>이게 제가 지난 미션에서 받았던 코드 리뷰입니다. 이 부분에 대해서 확실히, 검증은 검증만 하고 view는 입출력만 하면되지 않나? 그렇다면, 에러 메세지에 따른 결정은 view가 해야하지 않을까? 하는 생각이 들었습니다.</p>
<p>그리고, 이번 미션에서 추가된 요구사항이 throw Error를 발생시켜야한다는 것이었는데 이 에러를 발생시키는 일을 Validator나 View가 하지 말고 유틸함수를 호출해 컨트롤러에서 발생해야하지 않을까 생각했습니다.</p>
<p>그래서 throwError라는 유틸함수가 추가로 만들어졌습니다.</p>
<h4 id="b-inputhandler와-validator의-로직-설명">b. InputHandler와 Validator의 로직 설명</h4>
<p>먼저, 인풋핸들러와 벨리데이터는 객체 에러 코드로 소통합니다. 검증하는 로직은 계속 반복되기 때문에 각 검증 로직 키에 boolean 값을 받아, 어떤 키에 true값이 켜졌는지에 따라서 해당 인풋에 따라 미리 정의되어있는 해당 인풋메서드 전용 상수 메세지에 접근해 해당 에러메세지를 결정하는 식입니다. 이를 위해 키값의 조회가 잦게 일어났습니다.</p>
<ul>
<li><strong>*Validator</strong>
먼저 Validator는 불리언을 반환하는 검사 로직의 모음집인 validationUtils 라는 객체가 정의되어있습니다.
그리고, Validator는 이 유틸에서 로직을 재활용하며 각 인풋필드에 맞는 메서드를 갖고 있습니다.
이렇게 한 이유는 입력필드마다 다른 유효성 검사를 진행해야하기 때문에, view나 controller에서 검증을 진행할 때 그에 맞는 메서드 하나만 호출하는 것이 좋을 것 같았기 때문입니다.</li>
</ul>
<pre><code class="language-js"> bonusNumber: (number) =&gt; {
    const errorResults = {
      IS_NUMBER_RANGE_OVER: ValidationUtils.isNumberRangeOver(number, 1, 45),
      IS_NOT_NATURAL_NUMBER: ValidationUtils.isNotNaturalNumber(number),
    };

    return errorResults;
  },</code></pre>
<p>Validator 안의 메서드는 이렇게 errorResult를 반환하는데, 이는 에러 코드를 객체로 반환하는 방식입니다. 해당 값에 true가 하나라도 켜지면 에러가 있는 것으로 간주하고 에러를 발생시키는 식으로 에러 핸들링을 하였고,</p>
<p>저 에러리절트의 키와 동일한 키를 갖는 메세지 객체를 상수파일에 저장해, 해당하는 메세지를 출력하도록 하였습니다.
아래는 ERROR 메세지들을 저장한 객체입니다.</p>
<pre><code class="language-js">const ERROR = {
  USER_INPUT: {
    IS_EMPTY: &quot;[ERROR] 빈 값은 입력할 수 없습니다.&quot;,
  },
  WINNING_NUMBERS: {
    IS_WRONG_ARRAY_LENGTH: &quot;[ERROR] 로또는 6개의 숫자로 이루어져야합니다.&quot;,
    IS_DUPLICATED_NUMBER: &quot;[ERROR] 중복된 숫자는 입력하실 수 없습니다.&quot;,
    IS_ARRAY_NUMBER_RANGE_OVER: &quot;[ERROR] 1~45 사이의 숫자를 입력해야합니다.&quot;,
    IS_NOT_NATURAL_NUMBER_IN_ARRAY: &quot;[ERROR] 숫자는 자연수여야 합니다.&quot;,
  },
  BONUS_NUMBER: {
    IS_NUMBER_RANGE_OVER: &quot;[ERROR] 1~45 사이의 숫자를 입력해야합니다.&quot;,
...

export default ERROR;</code></pre>
<p>그리고, Output이 Validator에서 에러 객체를 받아, ERROR 메세지에 접근해 출력 처리하는 로직 부분입니다.</p>
<pre><code class="language-js">  printErrorResults(errorResults, errorName) {
    Object.entries(errorResults).forEach(([key, value]) =&gt; {
      if (value) this.print(`${ERROR[errorName][key]}`);
    });
  },</code></pre>
<ul>
<li><strong>*InputHandler</strong>
inputHandler는 try...catch를 이용한 재귀함수 방식과, 사용자의 input 값을 빈값 검증-&gt;파싱-&gt;유효성 검증-&gt;return 하는 방식이 모든 input마다 반복되어 템플릿화하기 위해 만들어진 함수입니다.</li>
</ul>
<pre><code class="language-js">export async function inputHandler({
  promptMessage,
  parser,
  validatorMethod,
  errorName,
}) {
  try {
    const userInput = await userInputEmptyHandler(promptMessage);
    const parsedUserInput = parser ? Parser[parser](userInput) : userInput;
    const parsedUserInputError = Validator[validatorMethod](parsedUserInput);
    Output.printErrorResults(parsedUserInputError, errorName);
    throwError(parsedUserInputError);
    return parsedUserInput;
  } catch (error) {
    return inputHandler({
      promptMessage,
      parser,
      validatorMethod,
      errorName,
    });
  }
}</code></pre>
<p>인풋핸들러의 로직은 이러합니다.
인자로 실행할 input의 promptMessage와, 해당 인풋의 파서를 받고 어떤 유효성 검증을 해야하는지 validatorMethod 명을 받습니다. 그리고 ERROR 메세지를 출력할 때, 해당 인풋을 위한 에러메세지를 모아둔 상수에 키로 접근하기 위해 errorName을 받습니다.</p>
<p>그러면 순차적으로 빈값 검증-&gt;파싱-&gt;유효성 검증-&gt;return 이 실행됩니다.</p>
<p>이렇게 하면 장점이</p>
<ol>
<li>유효성 검증이 어긋난 모든 에러의 메세지를 출력할 수 있습니다.</li>
<li>같은 검증 오류(보너스 넘버 입력과 당첨번호 입력을 할 때 둘다 1~45 사이의 숫자인지를 검증하는 등)를 반환해도 다른 메세지 처리를 할 수 있습니다.</li>
<li>반복되는 input 로직을 템플릿화 할 수 있었습니다.</li>
</ol>
<p>이러한 장점을 노리고 작성하였으나... 아직은 리팩토링이 부족한 것 같습니다.
제가 생각한 이 방식의 단점은</p>
<ol>
<li>확장성과 유연성을 위해 이렇게 유틸화를 하였으나 정작 이 핸들러를 사용하기 위해 개발자가 사용법과 구조를 익혀야하는 문제</li>
<li>도메인과 UI 로직 간 분리가 애매해진 것 같다.</li>
<li>높은 결합도</li>
<li>과연 인풋과 에러 핸들링을 위해 이렇게까지 작성되어야할 가치가 있을까?</li>
<li>호출해야할 메서드를 문자열로 감싸 적기 때문에 오타가 날 가능성이 높고, 오타로 인한 오류를 디버깅하기 힘들다</li>
</ol>
<p>이 부분은 좀 더 고민해보고 있습니다. 리뷰어님의 의견이 궁금합니다!🥹</p>
<h3 id="4-아쉬운-점">4. 아쉬운 점</h3>
<ul>
<li>리팩토링을 진행하며 기존 TDD방식으로 작성하던 로직과 컨트롤러 로직이 크게 달라진 부분이 있어서 단위테스트가 제대로 작성되지 못했습니다</li>
<li>시간이 부족해 상수화가 완전히 진행되지 못했습니다</li>
</ul>
<p>긴 글 읽어주셔서 감사합니다🥹 좋은 하루되세요 케빈!</p>
<h2 id="12-피드백">1.2 피드백</h2>
<h3 id="121-리뷰어-피드백">1.2.1 리뷰어 피드백</h3>
<h4 id="코드-리뷰-요약">코드 리뷰 요약</h4>
<ul>
<li>네이밍 컨벤션 통일 필요 (CamelCase, snake_case, kebab-case 혼합 문제)</li>
<li><code>LottoMachine</code>의 역할이 모호하여 중간 계층 역할을 부여하는 방향으로 리팩토링 필요</li>
<li><code>LottoMachine</code>이 단순 로또 생성기에 불과하여 비즈니스 로직 추가 검토 필요</li>
<li><code>TDD</code> 적용 방식 점검 → <code>LottoMachine</code> 관련 테스트 부족 (당첨 번호 비교 및 수익률 계산 테스트 필요)</li>
<li><code>InputHandler</code>와 <code>Validator</code> 설계 논리적이지만 사용성이 어려움 → 재사용성 및 가독성 개선 필요</li>
<li>문자열을 통한 메서드 호출 방식 대신 객체 기반 직접 함수 전달 방식 고려</li>
<li><code>Validator</code>에서 boolean 객체 대신 배열 반환 방식 검토</li>
</ul>
<h4 id="q--a-요약">Q &amp; A 요약</h4>
<h4 id="1-lottomachine의-역할이-적절한가">1. LottoMachine의 역할이 적절한가?</h4>
<ul>
<li>현재 <code>LottoMachine</code>의 역할이 중간 계층으로 설계되었으나, <code>Main(Controller)</code> 클래스가 일부 역할을 수행하여 모호한 상태</li>
<li><code>LottoMachine</code>을 로또 구매 및 당첨 결과까지 처리하는 방향으로 리팩토링 검토</li>
<li>만약 필요하지 않다면 단순 로또 생성 함수로 대체 가능</li>
</ul>
<h4 id="2-tdd-적용-방식">2. TDD 적용 방식</h4>
<ul>
<li>의존성이 낮은 <code>Lotto → LottoMachine → Main</code> 순으로 단위 테스트 작성된 점은 긍정적</li>
<li><code>LottoMachine</code> 관련 테스트가 부족하여 추가 필요 (당첨 번호 비교 및 수익률 계산 테스트 등)</li>
<li><code>Main.js</code>의 <code>play()</code> 흐름을 테스트할 수 있는 코드 추가 검토</li>
</ul>
<h4 id="3-inputhandler와-validator의-역할-분리">3. InputHandler와 Validator의 역할 분리</h4>
<ul>
<li>현재 구조가 재사용성을 높이려는 의도는 있으나 사용성이 어려움</li>
<li>문자열을 통한 메서드 호출 방식은 오타 및 디버깅 어려움이 존재 → 객체 기반 직접 함수 전달 방식 고려</li>
<li><code>Validator</code>에서 boolean 객체 대신 배열 반환으로 가독성 및 유지보수성 개선 검토</li>
</ul>
<h4 id="4-컨트롤러의-역할-정의">4. 컨트롤러의 역할 정의</h4>
<ul>
<li><code>MVC</code> 패턴에서 컨트롤러는 사용자의 입력을 처리하고 모델과 뷰를 연결하는 역할</li>
<li><code>Main.js</code>가 <code>LottoMachine</code>의 버튼을 누르는 역할을 수행하도록 설계됨</li>
<li><code>LottoMachine</code>이 너무 많은 책임을 가지지 않도록 분리 필요 (예: 당첨 계산을 별도 클래스로 분리)</li>
</ul>
<h4 id="5-재귀-호출-vs-while-문">5. 재귀 호출 vs while 문</h4>
<ul>
<li><p><code>let</code> 사용 금지로 인해 <code>while</code> 문을 활용한 루프 구조 적용이 어려웠음</p>
</li>
<li><p>현재 재귀 호출 방식은 무한 루프 가능성이 있으므로 <code>while</code> 문 내부에서 값을 갱신하는 방식 고려</p>
</li>
<li><p>아래 방식으로 변경 가능성 검토</p>
<pre><code class="language-js">export async function inputHandler(inputMethod, parser, validator) {
return (async function askUserInput() {
  const userInput = await inputMethod();
  const parsedUserInput = parser ? parser(userInput) : userInput;

  const errors = validator(parsedUserInput);
  if (Object.values(errors).some(Boolean)) {
    Output.printErrorResults(errors);
    return askUserInput(); 
  }

  return parsedUserInput;
})();
}</code></pre>
</li>
</ul>
<h3 id="122-수업-피드백">1.2.2 수업 피드백</h3>
<h3 id="📝-프론트엔드-개발에서-객체-지향을-배우는-이유">📝 프론트엔드 개발에서 객체 지향을 배우는 이유</h3>
<h3 id="🔹-객체-지향을-배우는-목적">🔹 객체 지향을 배우는 목적</h3>
<ul>
<li>단순히 OOP 개념을 배우는 것이 아니라 <strong>유지보수하기 좋은 코드</strong>를 작성하는 것이 목표</li>
<li><strong>예측 가능하고 파악하기 쉬운 코드</strong> 작성<ul>
<li>객체를 사용할 때 어떤 기능을 써야 하는지 명확해야 함</li>
</ul>
</li>
<li><strong>수정 및 확장이 쉬운 코드</strong> 유지<ul>
<li>새로운 기능 추가/변경이 용이해야 함</li>
</ul>
</li>
</ul>
<h3 id="1️⃣-객체-나누기-모듈화">1️⃣ 객체 나누기 (모듈화)</h3>
<h4 id="🎯-역할-책임-협력-원칙">🎯 역할, 책임, 협력 원칙</h4>
<ul>
<li><strong>자율적</strong>이고 <strong>협력적인 객체</strong> 설계</li>
<li><strong>객체의 자율성</strong> = 내부 구현을 숨기고(캡슐화), 필요한 기능만 공개</li>
<li><strong>객체의 협력성</strong> = 다른 객체와의 상호작용을 고려</li>
</ul>
<h4 id="📌-설계-원칙">📌 설계 원칙</h4>
<ul>
<li><strong>객체는 하나의 책임만 가지도록</strong> 한다.</li>
<li>하나의 기능을 변경할 때 <strong>하나의 객체만 변경</strong>하도록 설계.</li>
<li>데이터는 객체 내부에 <strong>캡슐화</strong>하고, 객체가 직접 처리하도록 한다.</li>
<li><strong>getter 대신 객체에게 메시지를 보내서 데이터 조작</strong>을 유도.</li>
</ul>
<pre><code class="language-js">// ❌ getter 사용 예시
if (lotto.numbers.includes(bonusNumber)) { 
    throw new Error(&quot;보너스 번호는 당첨 번호와 중복될 수 없습니다.&quot;);
}

// ✅ 메시지를 보내는 방식
if (lotto.has(bonusNumber)) { 
    throw new Error(&quot;보너스 번호는 당첨 번호와 중복될 수 없습니다.&quot;);
}</code></pre>
<h3 id="2️⃣-코드-재사용-중복-제거">2️⃣ 코드 재사용 (중복 제거)</h3>
<h4 id="🎯-상속inheritance-vs-조합composition">🎯 상속(Inheritance) vs 조합(Composition)</h4>
<h4 id="🔹-상속-inheritance">🔹 상속 (Inheritance)</h4>
<p>부모 클래스의 기능을 그대로 가져와 확장하는 방식
단점: 부모 클래스의 변경이 하위 클래스에 영향을 줄 수 있음 (강한 결합)</p>
<pre><code class="language-js">class WinningLotto extends Lotto {
  constructor(numbers, bonusNumber) {
    super(numbers);

    if (numbers.includes(bonusNumber)) {
      throw new Error(&quot;보너스 번호는 당첨 번호와 중복될 수 없습니다.&quot;);
    }

    this.bonusNumber = bonusNumber;
  }
}</code></pre>
<h4 id="🔹-조합-composition">🔹 조합 (Composition)</h4>
<p>기존 클래스를 객체로 포함하여 재사용하는 방식
장점: 각 객체가 독립적으로 존재하며, 유연한 구조를 가짐 (느슨한 결합)</p>
<pre><code class="language-js">class WinningLotto {
  constructor(winningLotto, bonusNumber) {
    if (winningLotto.has(bonusNumber)) {
      throw new Error(&quot;보너스 번호는 당첨 번호와 중복될 수 없습니다.&quot;);
    }

    this.winningLotto = winningLotto;
    this.bonusNumber = bonusNumber;
  }
}</code></pre>
<h4 id="📌-정리">📌 정리</h4>
<p>단순한 코드 재사용을 위해 상속을 남용하지 않는다.
상속보다는 조합(Composition)을 우선적으로 고려.
계층 구조가 필요하거나, 상속이 유리한 경우에는 상속을 사용.</p>
<h3 id="3️⃣-객체-지향을-활용한-코드-유지보수성-향상">3️⃣ 객체 지향을 활용한 코드 유지보수성 향상</h3>
<p>객체의 역할을 명확히 정의하고 하나의 책임만 부여
관련된 기능을 한 곳에 모아 변경 범위를 최소화
내부 동작을 숨기고, 필요한 기능만 노출하여 유지보수성 향상
상속보다는 조합을 우선적으로 고려하여 유연한 구조 설계</p>
<h3 id="✅-결론">✅ 결론</h3>
<p>프론트엔드 개발에서도 객체 지향 개념을 배울 필요가 있음!
하지만 목적은 개념 자체를 배우는 것이 아니라 유지보수성과 확장성이 좋은 코드를 작성하는 것! 🚀</p>
<h2 id="13-리팩토링-진행할-점">1.3 리팩토링 진행할 점</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> 인풋 핸들러가 메서드를 넘길 때 문자열로 넘겨서 휴먼에러를 발생시킬 수 있는 점 리팩토링</li>
<li><input checked="" disabled="" type="checkbox"> 로또 머신에게 중간 객체 레이어 역할 부여 &amp;&amp; 메인 컨트롤러의 역할 완화</li>
<li><input checked="" disabled="" type="checkbox"> 배운 조합의 응용으로 의존성을 주입하고 결합도를 낮추며 응집도를 높여보기</li>
</ul>
<h2 id="14-공부해볼-것들">1.4 공부해볼 것들</h2>
<ul>
<li><input disabled="" type="checkbox"> TDD 문법</li>
<li><input checked="" disabled="" type="checkbox"> 객체의 조합</li>
</ul>
<hr>
<h1 id="2-👆-1단계--로또---리팩토링">2. 👆 1단계 : 로또 - 리팩토링</h1>
<h2 id="21-주요-변경점">2.1 주요 변경점</h2>
<hr>
<p><strong>변경점 브리핑</strong></p>
<pre><code class="language-js">class LottoMachine {
  #lottos;

  constructor(LottoClass) {
    this.LottoClass = LottoClass;
    this.winnings;
  }

  publishLottos(money) {
    const count = Math.floor(money / PRICE.LOTTO);
    this.#lottos = Array.from({ length: count }).map(
      () =&gt; new this.LottoClass()
    );
    ...</code></pre>
<pre><code class="language-js">export default class Main {
  async play() {
    const lottoMachine = new LottoMachine(Lotto);
    const purchasePrice = await Input.purchasePrice();
    const publishedLottos = lottoMachine.publishLottos(purchasePrice);
    Output.printLottos(publishedLottos);
    await this.defineWinningRules(lottoMachine);
    await this.printLottoResult(lottoMachine, purchasePrice);
    const isRestart = await Input.restartLotto();
    if (isRestart) await this.play();
  }

  async defineWinningRules(lottoMachine) {
    const winningNumbers = await Input.winningNumbers();
    const bonusNumber = await Input.bonusNumber(winningNumbers);
    const winnings = new Winnings(winningNumbers, bonusNumber);
    lottoMachine.defineRule(winnings);
    return winnings;
  }

  async printLottoResult(lottoMachine, purchasePrice) {
    const { countStatistics, winningRate } =
      lottoMachine.drawWinning(purchasePrice);

    Object.entries(countStatistics).forEach(([rank, amount]) =&gt;
      Output.matchResult(rank, amount)
    );
    Output.winningRate(winningRate);
    Output.newLine();
  }
}</code></pre>
<ul>
<li><input checked="" disabled="" type="checkbox"> 로또 머신에게 중간 객체 레이어 역할 부여 &amp;&amp; 메인 컨트롤러의 역할 완화</li>
<li><input checked="" disabled="" type="checkbox"> 배운 조합의 응용으로 의존성을 주입하고 결합도를 낮추며 응집도를 높여보기</li>
</ul>
<p>메인 컨트롤러는 전체적인 흐름을 관리하는 역할을 하고 로또 머신이 좀 더 도메인 로직 역할을 분담받도록 리팩토링 진행하였습니다.</p>
<p>여기서 컨트롤러는 로또 판매점 사장입니다. 사장은 로또가 어떤 로직으로 발행되고 관리되고 계산되는지 알 필요 없이 구매자가 구매를 원하는 금액을 입력하면 발행된 복권을 확인할 수 있고 당첨번호를 넣으면 당첨 통계 및 수익률을 스크린으로 확인할 수 있으면 된다고 생각했습니다.</p>
<p>라고 했던 저의 의도대로 메인컨트롤러는 단순히 로또 머신의 버튼을 흐름대로 누른다, 머신 내부의 일을 사장이 알 필요는 없다는 관점에 집중해서 리팩토링하였습니다.
(위의 코멘트는 케빈이 못 보셨을 수도 있습니다! 리뷰 작성해주시고 계신 줄 모르고 PR 본문 메세지를 수정했어서요...!🥹)</p>
<p>또한, Lotto가 추후 AmericanLotto같은 게 생길 수도 있으니 Lotto 객체 자체를 컨트롤러에서 LottoMachine에 주입하는 식으로 조합을 의도해서 작성하였습니다.</p>
<p>마지막으로, Winnings.js 같이 당첨룰을 정의하는 방식도 추후 달라질 수 있다고 생각해, 메인컨트롤러에서 Winnings 인스턴스를 만들고 로또 머신에 defineRules(winning)으로 주입하여 주입받은 로직에 맞게 당첨자를 가려내는 식으로 리팩토링하였습니다!</p>
<hr>
<p><strong>리뷰어 답변</strong>
-&gt; 우선 지금 컨트롤러를 로또 판매점 사장에 비유하면서, 로직의 세부 사항을 몰라도 흐름을 관리하는 역할로 정리한 점이 매우 직관적이고 좋은 접근 방식이라고 생각해요 특히, 컨트롤러가 LottoMachine의 버튼을 누르는 역할에 집중하고, 내부 로직에 관여하지 않도록 한 점이 설계적으로 적절해 보입니다.</p>
<p>몇가지만 더 고려해보면 LottoMachine이 너무 많은 책임을 가지게 되지는 않았을까?를 고민해보면 좋을거 같아요 LottoMachine이 로또 발행뿐만 아니라, 당첨 확인까지 담당하게 되면서 &quot;로또 발행기&quot;라기보다는 &quot;로또 게임 관리자&quot;에 가까운 역할이 된거 같아요. 만약 역할을 더 명확히 분리하려면 당첨 계산을 별도의 클래스로 분리할 수도 있을거 같아요</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 1주차 회고 - 자동차 경주]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 14 Feb 2025 14:41:20 GMT</pubDate>
            <description><![CDATA[<p>오늘 우테코에서의 첫 주차 마지막 수업이 끝났다. 이제 겨우 4일 다녔는데 벌써 많은 일이 있었던 한 주였다. 각 날짜별 일기를 짧게 쓰고 이번주 과제였던 자동차 경주 코드 회고를 작성해보려한다. 일기는 당일마다 쓴 게 아니라 그냥 지금(14일 금요일) 지난 날들을 돌아보며 몰아쓰는 것이다 ㅋㅋㅋ</p>
<h1 id="1-주간-일기">1. 주간 일기</h1>
<p><strong><em>2/11 화요일</em></strong>
OT 날이다. 이 날 등교할 때 정말 두근두근했는데 ㅎㅎ 기대돼서 설레기도하고 걱정돼서 불안하기도 한 마음으로 등교했다. 한 편으로 심심하게 점심 혼자 먹을까봐도 걱정했었는데, 다행히 바로 <strong><em>연극</em></strong> 조원이 편성돼서 그럴 일은 없었다🥹 *&quot;연극&quot;*이 뭐냐고요? 
. . . . ㅎ</p>
<p>OT 날엔 출석과 우테코 생활규칙 안내부터 코치와 크루원들의 자기소개, 앞으로의 커리큘럼 계획에 대해 말씀해주셨다. 이제 우테코 안에서는 닉네임(&quot;님&quot; 자 금지. 코치에게조차...! 유교걸은 지금 적는데도 님을 붙이고싶ㄷㅏ)</p>
<p><strong><em>2/12 수요일</em></strong>
연극 조원들과 연극 주제를 정하고 첫 페어프로그래밍을 했던 날이다. 냅다 연극이라니, 너무 막막하고 주제가 생각나지도 않을 것 같았는데 조원들이 아이디어를 많이 내줘서 제출할 수 있었다. 포비의 말벌아저씨 레퍼런스 탓인지 불타오르는 개그 욕심😂</p>
<p>페어와는 처음부터 공통점이 너무 많아서 신기했다! 코드 스타일도 비슷하고 그 이외 살아왔던 흔적(?)에서 공통점이 많아서 금방 즐겁게 같이 코딩했다.(같이 즐거웠던 거 맞나?)</p>
<p>우리는 TDD 개발을 시도해보았는데, 프라이빗 필드에서 getter와 setter를 만들지 않고 테스트 코드를 작성하려고 하니 여러 고민점이 생겨 머리에 쥐가 나는 줄 알았다.</p>
<p>점점 하면 할 수록 그냥 console 찍고 싶은 마음만 가득해져갔다 . . . ㅋㅋㅋㅋ 그래도 내일의 우리를 믿고 퇴근ㅋ</p>
<p>우테코에서의 첫 수업은 혼자 고민해볼 수 있는 관점과 시간을 많이 주는 것이 신선하고 좋았다. 그리고 수업 중 자유롭게 스레드에 사담하거나 질문할 수 있고, 수업 중간중간 계속 올라오는 코치들의 재치있는 농담들까지 뭐랄까 신선한 충격이다. 완전 자유롭고...짱 재밌다. 분명 아침 수업인데 시간가는 줄 모르고 재밌게 듣게되는 매직✨</p>
<p><strong><em>2/13 목요일</em></strong>
아침부터 6시까지 페어프로그래밍을 마무리하다가 6시부턴 또 연극 회의를 해서 결국 연극조원들이랑 10시 넘어서 선릉캠 마감까지하게된 날이다 ㅋㅋㅋ</p>
<p>페어 프로그래밍은 우테코 합격 전, 최종 코테를 준비하면서 스터디원과 원격으로 해본 경험이 있었지만 역시 그때나 이번이나 참 좋은 스터디 방식인 것 같다. 서로의 생각을 이해시키고 설득하고 설득당하면서 코딩을 해야하기 때문에 개발 속도 자체는 당연히 더 느리지만, 이 과정에서 얻어가는 것들이 너무 많아 재밌기 때문이다.</p>
<p>페어와 나는 코드 스타일도 성향도 비슷한 편인 거 같아서 개발 중에 갈등이나 서로 이해안되는 지점은 없었다. 그런만큼 고민에 갇히게 되는 지점들도 비슷해서 같이 프라이빗 필드와 테스트에 대해 고민이 많았는데, 아직도 명확한 답은 모르겠다.</p>
<p>이 과정에서 프라이빗 필드를 참조하는 메서드를 mocking하고 spy 붙이는 코드가 어려워서 헤매고 있었는데 캉O이 도와줬다! 비슷한 문제를 겪었다며 강력한 힌트를 주셔서 덕분에 jest의 모킹과 스파이 개념도 제대로 복습이 되었다 ㅎㅎ</p>
<p><strong><em>2/14 금요일</em></strong>
오늘의 데일리마스터는 나였다. 컨텐츠로 일단 약간의 스트레칭을 함께하는 명상을 준비해갔는데, 고개 스트레칭을 시작하자마자 나지막히 여기저기서 들리는 뚜두둑 소리가 웃펐다😂</p>
<p>오늘은 12시 반까진 오전 수업이 있었다. 자동차 경주 과제를 공원과 함께 풀어보았는데, 역시나 생각해볼 지점들이 많은 수업이었다!</p>
<p>도메인과 UI의 분리부터, validate는 차에서 검증을 해야할까 util일까, 랜덤값 생성같은 기능에 테스트가 꼭 필요한가?/단위 테스트에 꼭 mocking이나 spy가 필요할까 같이 말이다.</p>
<p>랜덤값 생성의 경우엔 테스트 커버리지에 대해 생각해보면 우리가 원하는 범위 내에서 움직이는지에 대한 검증만 확실히 보증하면 되지, 반환값이 꼭 랜덤인가를 테스트로 검증해야하는지는 다시 생각해볼만 하다고 하셨다.</p>
<p>수업이 끝나고 나선 연극 조원들과 리뷰어에게 받은 피드백도 공유하고 각자의 생각도 나눴다. 우리 조는 두 번 입력하면 프로그램이 에러 종료되는 이슈가 있었는데 왜 그러는지 이유를 모르겠어서 연극 조원들에게 말해봤더니 O건이 해결해줬다! 내가 이해를 빨리 잘 못했는데 그림으로 차근차근 알려주셔서 이해할 수 있었다 ㅠ 엄청 중요한 개념을 알아서 좋았다. 밑에 정리해서 적어야지 ㅎㅎ 감사합니다 O건!👍</p>
<p>그리고 다른 조원들이 받은 다양한 리뷰들을 들으니 많이 고민해볼 지점과 깨닫는 지점들이 많아서 좋았다. 코드 리뷰의 강력한 순기능 . . ..</p>
<p>그나저나 모킹하느라 좀 시간이 걸렸었는데, 오늘 수업에서 예고하길, 다음 주차 로또 과제 때는 모킹을 쓰지 말라고 한다. 왜냐하면 모킹을 써야하는 테스트가 과연 단위 테스트일지 생각해보면, 그렇지 않기 때문이다. 프리코스에서 봤던 테스트 코드에 모킹 함수가 있었던 것은 기능 테스트를 목적으로 E2E 테스트 코드를 짠 것이기 때문인데, 우리의 과제는 먼저 단위 테스트를 해보는 것이 목표이기 때문이다.</p>
<p>단위 테스트... 이것도 아직 경계가 모호하게 와닿아서 더 고민하고 공부해봐야 할 것 같다.</p>
<h1 id="2-👆-1단계--자동차-경주---페어-프로그래밍">2. 👆 1단계 : 자동차 경주 - 페어 프로그래밍</h1>
<h2 id="21-제출한-코드">2.1 제출한 코드</h2>
<p><a href="https://github.com/woowacourse/javascript-racingcar/pull/338">https://github.com/woowacourse/javascript-racingcar/pull/338</a></p>
<h2 id="22-피드백">2.2 피드백</h2>
<h3 id="221-리뷰어-피드백">2.2.1 리뷰어 피드백</h3>
<h4 id="카멜-리뷰어-">카멜 리뷰어 :</h4>
<ul>
<li>예외 케이스를 꼼꼼히 잡아줘서 좋았다.</li>
<li><strong>잘못된 입력을 두 번 하면 프로그램이 종료돼버리는 치명적인 오류를 발견했다.</strong></li>
<li>가독성이 좋아서 이해가 잘 되고 좋았다.</li>
<li>필드가 없거나 필드가 아니어도 되는 상황에서도 class를 활용해야했을까?</li>
<li>여러 코드들을 하나의 단위로 묶을 수록, 코드가 안전해지는 동시에 추후에 수정하는 것이 조심스러워 진다. 그래서 꼭 묶을 필요가 없다면 분리하는 것도 가독성과 수정하기 용이함에 큰 장점이 있다고 생각한다.</li>
<li>아직 타입스크립트가 아니기 때문에 각 입력값에 대해 기대하는 타입 검사도 진행해주는 것이 좋을 것 같다.</li>
</ul>
<p><strong>코드 리뷰</strong></p>
<pre><code class="language-js">const winnerNames = [];
    let maxPosition = 0;
    this.#carList.forEach(car =&gt; {
      if (car.position &gt; maxPosition) {
        maxPosition = car.position;
      }
    });

    this.#carList.forEach(car =&gt; {
      if (car.position === maxPosition) {
        winnerNames.push(car.name);
      }
    });
    return winnerNames;</code></pre>
<p>maxPosition은 Math.max 함수를 활용하고, winnerNames는 map 메서드를 활용하면 코드가 더 간결해질 수 있습니다!</p>
<pre><code class="language-js">static async inputTryNumber() {
    try {
      const inputTryNumber = await InputView.readLineAsync(MESSAGE.INPUT.TRY_NUMBER);
      Validator.isEmpty(inputTryNumber);
      console.log(inputTryNumber);
      const parsedNumber = Number(inputTryNumber.trim());
      Validator.isNumber(parsedNumber);
      Validator.isRangeOver(parsedNumber, DEFINITION.MIN_GAME, DEFINITION.MAX_GAME);
      Validator.isDecimal(parsedNumber);
      return inputTryNumber;
    } catch (error) {
      console.log(error.message);
      await this.inputTryNumber();
    }
  }</code></pre>
<p>&#39;꼭 Validator와 input 함수가 에러로 소통해야 할까?&#39;라는 생각이 들었습니다!
잘못된 입력을 알려주는 것도 View 영역이라면, 판단은 Validator가 하고, 어떤 문자열을 보내줄지는 input 함수에서 책임지는 방법은 어떤가요?</p>
<p><strong>Q &amp; A</strong></p>
<blockquote>
<p>TDD로 개발을 할 때 모든 객체와 기능에 대해 테스트 코드를 명세서처럼 작성해두고 기능 개발을 하는 게 나을지, 하나의 기능or모델에 대한 테스트코드를 작성하고 그 기능에 대한 개발을 하고 이렇게 순차적으로 가는 게 나을지?</p>
</blockquote>
<p>-&gt; 좋은 질문입니다~!
문서화의 관점에선 다양하게,
버그 예방의 관점에선 &#39;중요한 것부터 꼼꼼하게&#39;인 것 같습니다!</p>
<blockquote>
<p>기능 개발 단계에서 테스트 코드 작성 vs 기능 개발에선 console 찍고 나중에 품질 검증 목적으로 테스트 코드 작성
리뷰어님의 생각은?! ...저는 그냥 콘솔이 빠른 거 같아요 ㅠㅋㅋㅋㅋ 테스트 코드 어려워요오...</p>
</blockquote>
<p>-&gt; 좋은 질문입니다! 저도 고민될 것 같아요.
좋은 상황은 아니겠지만 정말 너무 급하다면 기능 구현을 우선 해야할 수도 있죠.
하지만 엣지케이스를 정리하는 것 자체는 구현이 들어가기 전에 하는 것이 큰 도움이 된다고 생각해요.
코드로 남겨지면 제일 좋겠지만, 여유가 없다면 테스트해볼만한 것들을 메모나 문서로 미리 정리하는 것도 추천합니다~!</p>
<blockquote>
<p>프라이빗 필드를 변경하고 내용을 참조해 메세지를 던지는 메서드를 테스트할 때, 사실 정확한 검증을 위해서는 해당 프라이빗 필드 값을 보면 좋은데 getter를 지양하면 그럴 수 없으니...그래서 해당 필드를 이용한 기능이 정상적으로 동작하는지 간접적으로 테스트하는 걸로 충분할 수 있는지? 쵸파님 생각이 궁금합니다!</p>
</blockquote>
<p>-&gt; 요건 깊게 생각해본 적이 없네요.. 현재로는 간접적 테스트로 충분할 것 같습니다!</p>
<blockquote>
<p>프론트엔드에도 MVC 패턴 적용이 좋을까요? 실제 프론트엔드 개발자가 자주 보게될 디자인 패턴도 궁금합니다...!</p>
</blockquote>
<p>-&gt; MVC 패턴 자체보다는, 데이터를 다루는 코드와 UI를 다루는 코드를 분리한다는 관점은 자주 사용하고 있습니다.
Flux 아키텍처라는 키워드가 있습니다! 이후 미션 진행하시면서 자연스럽게 알아가실 것 같습니다<del>!
다음 스텝도 파이팅입니다</del>! 💪 💪</p>
<h4 id="페어-리뷰어-">페어 리뷰어 :</h4>
<ul>
<li><p>혼자서 경주가 가능</p>
</li>
<li><p>0회면 경주 x</p>
</li>
<li><p>어떤 기조로 코드를 작성했는지, 구조는 어떻게 설계했는지, 어떤 부분이 어려웠고 더 신경써서 작업했는지를 조금 더 던져주셨다면 좋았을 것 같아요.
리뷰이의 정보가 부족하면 리뷰어는 순수 100% 코드만으로 의도까지 유추해야 한답니다.
앞으로 미션은 많으니 차차 개선해보시죠 ㅎㅎ
(참고로 회사에서는 동료에게 PR을 남기게 될텐데, 동료는 항상 절대적으로 시간이 부족하답니다... 😅 )</p>
</li>
<li><p>index.js 파일에서 바로 Game 인스턴스 생성 및 시작을 해도 되지 않을까요?
index도 class로 만드신 이유가 궁금해요 ㅎㅎ</p>
</li>
<li><p>오타가 있어요 winnerMess&#39;a&#39;ge !
요 익스텐션을 강추 드립니다! 앵간한 오타를 다 잡아줘용
<img src="https://velog.velcdn.com/images/dev-dino22/post/52054e2f-ab65-49ad-ade3-8c1b55af249a/image.png" alt=""></p>
</li>
<li><p>class가 static 메서드만 가지고 있네요. 그렇다면 꼭 class가 아니라 객체였어도 될것 같아요</p>
</li>
<li><p>moveForward &lt; 4 상수화</p>
</li>
<li><p>보통 is~ prefix는 boolean 값을 return하는 변수들에 붙입니다.
boolean을 return하는 함수들로 분리해보면 어떨까요?
(분리하고 나면 겸사겸사 Validator의 함수들 이름도 바꿔야 할텐데, 한번 고민해보세요 ㅎㅎ)</p>
</li>
<li><p>maxPosition을 찾는 과정과
maxPosition만큼 이동한 자동차를 찾는 과정을 각각 다른 메서드로 분리해보면 좋겠어요.
21 ~ 26, 28 ~ 32 line를 기준으로 생각해보셔요!</p>
</li>
<li><p>배열에 직접 값을 집어넣기 보다는
map 메서드를 활용해 Car 인스턴스로 구성된 배열을 한번에 반환해 할당할 수 있을 것 같아요. 이렇게 하면 불변성을 유지하면서 데이터(여기선 carList 필드)를 변경할 수 있답니다<del>! 불변성을 왜 유지해야하는지는 여유있을 때 추가적으로 학습해보시면 좋겠네요</del>!
(천천히 체화될 내용이라 여유를 가지셔요~ ㅎㅎ)</p>
</li>
<li><blockquote>
<p>상태값을 어디 저장했다가 한 번에 넣는게, 바로바로 push하는 거보다 메모리 비용이 적나...? -&gt; 내가 생각했던 것과 달리 둘다 성능면에서는 큰 차이가 없고, 불변성을 지키고 추적성을 용이하게 하게 하기위해 map()으로 한 번에 넣는 게 좋구나!</p>
</blockquote>
</li>
<li><p>start()메서드 하나에 많은 동작이 포함되어 있어요.
인풋을 받고
자동차 리스트를 만들고
경주를 하고 중간 결과값을 출력하고
최종 승자를 계산해서 출력합니다.
1, 2번 - 경주준비 / 3, 4번 - 경주시작 요렇게 2개의 맥락으로 나누어 분리하는걸 목표로 두시면 좋을 것 같아요.
너무 많이 생각하진 않으셨으면 해요😄</p>
</li>
</ul>
<h3 id="222-수업-피드백">2.2.2 수업 피드백</h3>
<h3 id="223-다른-크루원-리뷰어들의-피드백에서-고민해볼만한-점-긁어오기">2.2.3 다른 크루원 리뷰어들의 피드백에서 고민해볼만한 점 긁어오기</h3>
<ul>
<li>class는 본질적으로 생성자 함수 + prototype 으로 동작하는 문법적 설탕이다.
static method 는 클래스 자체에 속하며, prototype method 와는 다르게 동작한다.
ES5로 변환해보며 class가 어떻게 동작하는지 이해해야 한다.
JavaScript의 핵심 개념인 prototype, 생성자 함수, 객체, ES6 &amp; ES5 차이점을 깊이 공부해야 한다.</li>
<li>(심화)
GameController 은 관련 domain, UI 를 불러와서 통제하는 역할로 보입니다.
다만 여기에서 Input, Output 등 모든 모듈이 결합되어 있는데요 (결합되어 있다가 어떤 의미인지 공부해보시면 좋습니다)
이 패턴의 장점과 단점은 무엇이고 언제 써야할까요?
더하여, 결합시키지 않으려면 어떤 대안이 있는지 있을까요?</li>
<li>중간 레이어 객체가 뭘 의미하는 걸까...?</li>
</ul>
<h2 id="23-다른-크루원들과-대화하며-인상깊었던-것">2.3 다른 크루원들과 대화하며 인상깊었던 것</h2>
<ul>
<li>자동차가 움직인 흔적을 history = [] 에 저장해둔 코드</li>
<li><blockquote>
<p>이 코드에 리뷰어가 “car에서 history를 담당한다는 자체가 어색하게 느껴지는 것 같아요. Car를 떠올렸을 때 이름부터가 자동차의 핵심 개념이라기보다는 ‘기록’이라는 부가적인 책임을 암시하는 것 같아요. 이는 자동차라는 도메인의 본질적인 속성으로 다가오지 않는 것 같습니다. 자동차의 핵심적인 기능에 집중을 하는 것은 어떨까요?”라고 달았다고 한다. 리팩토링 때 생각해서 다시 해보자.</p>
</blockquote>
</li>
<li>validate가 input에서만? model에서만? 아님 둘다?</li>
<li><blockquote>
<p>원래 내 의견은 input 에서 검증해서 model에 무결한 인자를 넘기는 게 쓸데없는 코드 호출을 줄이고 mvc 패턴에 더 부합하지 않나 싶었는데, &#39;실제 프론트와 백엔드를 생각해보면 프론트에서 막지 못한 유효하지 않은 데이터 입력이 서버로 넘어갔을 때 서버에서도, 즉 도메인 로직에서도 방어를 할 수 있어야하지 않나 하는 시각에서 둘 다에서 검사해도 좋을 것 같다.&#39;라고 의견을 말해주셔서 설득됐다! 인상 깊은 논리적 시각</p>
</blockquote>
</li>
<li>컨트롤러나 뷰에 우승자 판단을 넣어둔 크루원이 리뷰어에게 &quot;중간 레이어 객체&quot;가 필요할 것 같다는 피드백을 받았따...이게 무슨 말일까? 고민해보자.</li>
<li>어...더 많았는데...; 메모해둘걸..기억력...</li>
</ul>
<h2 id="23-리팩토링-진행할-점">2.3 리팩토링 진행할 점</h2>
<pre><code class="language-js">const DEFINITION = {
  MAX_CAR_RACERS: 40,
  MAX_NAME_LENGTH: 5,
  MIN_GAME: 0,
  MAX_GAME: 100,
};</code></pre>
<ul>
<li>✅ 일단 실수가 있었는데, 이 상수가 사용된 Validator에서 에러를 일으키지 않을 조건이 min 이상 max 이하이기 때문에 0이 아니라 1이어야한다.</li>
</ul>
<pre><code class="language-js">console.log(inputTryNumber);</code></pre>
<ul>
<li>✅ 쓸데없는 테스트용 콘솔 안지웠뜸ㅋㅋㅋ</li>
</ul>
<pre><code class="language-js">class InputController {
  static async inputName() {
    try {
      const inputName = await InputView.readLineAsync(MESSAGE.INPUT.NAME);
      Validator.isEmpty(inputName);
      const splitedName = Parser.splitName(inputName);
      Validator.isArrayLengthOver(splitedName, DEFINITION.MAX_NAME_LENGTH);
      Validator.isDuplicate(splitedName);
      splitedName.forEach(name =&gt; {
        Validator.isStringLengthOver(name, DEFINITION.MAX_NAME_LENGTH );
      });
      return splitedName;
    } catch (error) {
      console.log(error.message);
      await this.inputName();
    }
  }</code></pre>
<ul>
<li>✅ 그리고 짱 중요했던 치명적 오류...<pre><code class="language-js">console.log(error.message);
return this.inputName();</code></pre>
✅ 캐치 블록 안의 재귀함수를 return으로 바꿔줘야한다. 이 InputController를 호출하는 Game.js 에서는<pre><code class="language-js">const inputName = await InputController.inputName();</code></pre>
✅ 이렇게 작성되어있는데, 저 캐치가 작동될 때 그냥 return 없이 냅다 inputName();를 해버리면 inputName()이 재실행되든 말든 이미 inputName에 undefined 가 저장되는 문제가 있다. 이건 내가 트라이 캐치문을 잘 못 이해했던 것이었는데, 캐치 막줄에 재귀함수로 호출한다해서 inputName에 값이 저장되지 않고 다시 try를 무한정 시도한 뒤 반환값이 있을 때 저장되는 줄 알았는데, 그게 아니었따. 그래서 수정! 차근차근 알려준 O건에게 압도적 감사를 , , ., ., .ㅎㅎ</li>
</ul>
<p>-&gt; 🔴 그런데 try...catch 로 재귀를 하는 것에 대해 비판적인 생각을 해보았다. catch에서 반환되는 재귀함수를 결국 상위 호출에 저장해서 다시 실행하는 것이라면 오히려 불필요한 스택을 쌓아서 메모리 효율이 더 나빠지는 게 아닐까?</p>
<p>-&gt; 🔴 이렇게 생각하다보니 try catch의 사용법에 대해 직관적으로도 &quot;유효한 값을 입력할 때까지 이 입력함수를 시도하라&quot;는 명령이 catch에 들어가는 게 맞는지 모르겠다는 생각이 들었음</p>
<p>-&gt; 🔴 catch에는 예외를 기록하거나 해결하는 로직을 써야하지 않을까?</p>
<p>-&gt; 🔴 예를 들어 while문으로 재귀함수를 실행하던 중에 예기치 못한 오류가 발생되거나 메모리 누수가 일어나 메모리를 다 써버려서 프로그램이 다운될뻔 한다거나 할 때 catch를 실행하도록 catch를 약간 최후의 수단처럼 이용하는게 더 합리적이지 않을까 하는 생각이 들었음.</p>
<p>-&gt; ✅ 그래서 유효한 값을 입력할 때까지 입력받을 시도를 계속 하라는 명령을 시행할 inputHandler를 util 함수로 뺐고, 이 함수는 try catch가 아닌 while로 명령을 해당 조건까지 무한 반복하도록 리팩토링 했음!</p>
<ul>
<li><p>✅ is...prefix 검증이 boolean 을 return 하지 않으면서 is 네이밍한 것에 대해 로직을 변경할지 네이밍을 변경할지 -&gt; boolean 값을 반환하는 validationUtils 객체 메서로 변경</p>
</li>
<li><p>✅ 입력 유효성 문제가 한 개가 아니라 여러 개일 때 모든 문제를 다 표시하게하려면?</p>
</li>
</ul>
<pre><code class="language-js">const isDuplicated = (arr) =&gt; new Set(arr).size !== arr.length;</code></pre>
<ul>
<li>✅ 이런 식으로 이게 비었는지, 중복됐는지를 확인하고 boolean 값을 리턴하는 간단한 코드 모음을 객체로 만들고, 이것의 나열들을 각 인풋 유효성에 맞게 순차적으로 실행하는 validator를 메서드로 만드는 게 어떨까? 이름 입력을 위한 validateUserName() 에는 isEmpty, isDuplicated ...가 실행되는 식으로...근데, 이렇게하면 에러 메세지는 각 에러 이유에 맞게 어떻게 출력하지?!?!?</li>
</ul>
<pre><code class="language-js">DEFINITION = {
    MIN: {
        GAME_TRY: 1,
          GAME_ ...
    }
}</code></pre>
<ul>
<li><p>이렇게 MIN 정의와 MAX 정의로 나누면 코드 작성할 때 편하다는 다른 크루원의 아이디어가 있었음. 좋은 듯!</p>
</li>
<li><p>인풋에 대한 검증을 ui에서도 하고 domain 에서도 해볼까?</p>
</li>
<li><p>상수 분리 놓친 거 마저 상수 분리</p>
</li>
<li><p>/view, /domain 으로 디렉터리 수정하는건가?</p>
</li>
<li><p>✅ index라던가 뭐 쓸데없이 class로 만들어버린 거 리팩토링하자</p>
</li>
<li><p>✅ 각 사용자 입력마다 validator 호출 순서가 다르니까 각 입력을 위한 validate 함수를 만들자 -&gt; 공통적으로 자주 쓰일 validation 판단 로직들은 validator 위에 객체메서드로 모아놓음</p>
</li>
<li><p>✅ inputController에 대한 비판적 생각....</p>
</li>
<li><p>✅ &#39;꼭 Validator와 input 함수가 에러로 소통해야 할까?&#39;라는 생각이 들었습니다!
잘못된 입력을 알려주는 것도 View 영역이라면, 판단은 Validator가 하고, 어떤 문자열을 보내줄지는 input 함수에서 책임지는 방법은 어떤가요? 에 대한 해결책</p>
</li>
<li><p>✅ 자동차 &quot;경주&quot; 이므로 혼자 경주하는 것은 의미가 없다 판단, 2명 이상만 참여 가능하게 함</p>
</li>
<li><p>✅ 메모리 과부하를 방지하기 위해 참여인원수 최대 40명 제한</p>
</li>
<li><p>✅ Validator</p>
</li>
<li><p>maxPosition은 Math.max 함수를 활용하고, winnerNames는 map 메서드를 활용</p>
</li>
<li><p>❌ history 상태관리 나도 만들어볼까 -&gt; 시간 없음...ㅎㅎ</p>
</li>
<li><p>모델의 역할에 대해 더 생각해보자...</p>
</li>
<li><p>결합도...불필요한 임포트문이 너무 많진 않은지, 의존성 주입도 적절하게 섞어보자</p>
</li>
<li><p>2차 리뷰 요청 보낼 때, 메세지 잘 적기(어떤 기조로 코드를 작성했는지, 구조는 어떻게 설계했는지, 어떤 부분이 어려웠고 더 신경써서 작업했는지, 어떤 것을 리팩토링 했는지...)</p>
</li>
<li><p>test 코드 jest.fn()을 사용하지 않는다. (마찬가지로, mock/spy 도 사용하지 않는다)</p>
</li>
</ul>
<h2 id="24-느낀-점">2.4 느낀 점</h2>
<h2 id="25-공부해볼-것들-일단-키워드만">2.5 공부해볼 것들 일단 키워드만...</h2>
<ul>
<li>공동 커밋</li>
<li>domain &lt;-&gt; UI</li>
<li>prototype, 생성자 함수, 객체, ES6 &amp; ES5 차이점</li>
<li>중간 레이어 객체....</li>
<li>(+) jest 사용법</li>
</ul>
<h1 id="3-✌️-2단계--자동차-경주-리팩토링">3. ✌️ 2단계 : 자동차 경주 리팩토링</h1>
<p>여기부터는 pr 보낼 때 쓸 메세지이므로 존댓말로 작성합니당</p>
<blockquote>
<p>리팩토링 PR 링크
<a href="https://github.com/woowacourse/javascript-racingcar/pull/410">https://github.com/woowacourse/javascript-racingcar/pull/410</a></p>
</blockquote>
<h2 id="31-리팩토링---디렉터리-구조">3.1 리팩토링 - 디렉터리 구조</h2>
<h3 id="311-domain-view-폴더로-수정">3.1.1 domain/, view/ 폴더로 수정</h3>
<p>📂 리팩토링 전</p>
<pre><code class="language-js">/src
 ├── constants/
 ├── models/  &lt;-- Car 모델이 여기에 위치
 ├── utils/
 ├── views/
 ├── controllers/
</code></pre>
<p>📂 리팩토링 후</p>
<pre><code class="language-js">/src
 ├── constants/
 ├── domain/       &lt;-- 도메인 로직을 한 곳에서 관리
 │   ├── models/   &lt;-- Car, WinnerManager 등 도메인 관련 클래스들
 ├── utils/
 ├── views/
</code></pre>
<h2 id="32-리팩토링---로직-수정">3.2 리팩토링 - 로직 수정</h2>
<h3 id="321-validation-로직-수정">3.2.1 Validation 로직 수정</h3>
<blockquote>
<p>&#39;꼭 Validator와 input 함수가 에러로 소통해야 할까?&#39;라는 생각이 들었습니다!
잘못된 입력을 알려주는 것도 View 영역이라면, 판단은 Validator가 하고, 어떤 문자열을 보내줄지는 input 함수에서 책임지는 방법은 어떤가요?</p>
</blockquote>
<p>💭 이 말씀에 대해서도 많은 고민을 해보았습니다. 확실히 에러 &quot;메세지&quot;를 결정하고 던지는 것은 view의 역할인 것 같고, validator가 에러 메세지까지 정해서 보내는 방식은 같은 검증 로직이지만 다른 메세지를 출력해야할 때 재활용성도 떨어질 것이었습니다.</p>
<pre><code class="language-js">const ValidationUtils = {
  isEmpty: string =&gt; string.trim().length === 0,
  isDuplicated: array =&gt; new Set(array).size !== array.length,
  isArrayLengthOver: (array, max) =&gt; array.length &gt; max,
  isStringLengthOver: (string, max) =&gt; [...string].length &gt; max,
  isArrayLengthRangeOver: (array, min, max) =&gt; array.length &gt; max || array.length &lt; min,
  isRangeOver: (number, min, max) =&gt; number &lt; min || number &gt; max,
  isDecimal: number =&gt; number % 1 !== 0,
  isNotNumber: number =&gt; number === null || typeof number !== &#39;number&#39; || Number.isNaN(number),
  isNotString: string =&gt; typeof string !== &#39;string&#39;,
};</code></pre>
<p>✅ 그래서 우선 Validator.js 에 단순 if문(검증로직)을 boolean 형태로 반환하는 ValidationUtils 객체를 만들었고, 이 객체를 활용하여 각 입력값마다 유효한 검증 로직 순서에 맞게 검사하는 메서드를 가진 Validator로 수정하였습니다. 이렇게 하는 편이 isEmpty와 같은 is네이밍에도 잘 맞고 재활용성 및 유지보수성에도 도움이 되는 방향이라고 생각하였습니다.</p>
<p>💭 그리고 그렇다면 에러메세지 관리 및 처리를 어떻게 해야할까도 고민하였는데, 처음엔 validator가 에러코드를 반환하면 그 에러코드들을 배열에 담아 input이 받아 각 입력필드에 맞게 에러메세지를 출력하는 식을 고민했습니다.</p>
<p>💭 이렇게 한다면 에러코드로 통신하는 서버 API 환경과 통신하는 연습도 되지 않을까 싶었지만, 각 입력필드마다 유효값이 다르고 변경이 빈번할 프론트엔드 환경에는 더 좋은 방법이 있을 것 같았습니다.</p>
<pre><code class="language-js">const Validator = {
  validateInputNames: arrayNames =&gt; {
    const result = {
      IS_ARRAY_LENGTH_RANGE_OVER: ValidationUtils.isArrayLengthRangeOver(arrayNames, DEFINITION.MIN_CAR_RACERS, DEFINITION.MAX_CAR_RACERS),
      IS_DUPLICATED: ValidationUtils.isDuplicated(arrayNames),
      IS_EMPTY: false,
      IS_STRING_LENGTH_OVER: false,
      IS_NOT_STRING: false,
    };

    arrayNames.forEach(name =&gt; {
      if (ValidationUtils.isEmpty(name)) result.IS_EMPTY = true;
      if (ValidationUtils.isStringLengthOver(name, DEFINITION.MAX_NAME_LENGTH)) result.IS_STRING_LENGTH_OVER = true;
      if (ValidationUtils.isNotString(name)) result.IS_NOT_STRING = true;
    });
    return result;
  },</code></pre>
<p>✅ 그래서 validationUtils의 모음집으로서 Validator를 쓰고, 이 validationUtils의 값을 키값쌍으로 가진 객체를 내보냈습니다. 그리고 그 객체의 키값쌍에 따라 다른 Message를 출력하도록 Error.js 객체파일을 만들고, OutputView 파일도 수정하였습니다.</p>
<p>✅ 이렇게 리팩토링하면서, 한 번에 여러 가지의 유효성을 어겼을 때도 모두 안내해줄 수 있도록 하였습니다.</p>
<h3 id="322-inputcontroller❌---inputview-inputhandler-로직-수정">3.2.2 InputController❌ -&gt; InputView, InputHandler 로직 수정</h3>
<blockquote>
<p>다만 한가지 치명적인 버그가 있는데, 한번 입력을 잘못하면 2번째 입력부터 들어가지 않네요.</p>
</blockquote>
<p>💭 말씀해주신 이 부분이 try...catch 의 catch 블록에서 this.inputName() 재귀 입력 함수에 return을 하지 않은 문제였다는 것을 알게 되었습니다. return 을 하지 않으면 단순히 재귀함수를 실행만 하고 이 함수를 호출한 상위 함수의 반환값은 undefined가 되어, 2번째 실행 때 제대로 값이 반환되어도 스택이 꼬이는 문제였습니다.</p>
<p>💭 그래서 해당 부분에 return 을 추가하고나니 제대로 동작하였지만, 문득 이러한 동작에 try catch 를 쓰는 것이 맞는지 의문이 들었습니다.</p>
<p>💭 catch에서 반환되는 재귀함수를 결국 상위 호출에 저장해서 다시 실행하는 것이라면 사용자의 유효하지 않은 입력은 굉장히 빈번하게 일어날 일일텐데, 오히려 불필요한 스택을 쌓아서 메모리 효율이 더 나빠지는 게 아닐까?</p>
<p>💭 이런 생각이 드니 try catch의 직관적인 느낌과도 해당 로직이 맞지 않는다는 생각이 들었습니다. &quot;유효한 값을 입력받을 때까지 이 동작을 반복하라&quot;는 명령이 과연 예외를 기록하거나 해결하는 로직이 들어가야하는 catch에 맞는가? 하는 부분이었습니다.</p>
<p>💭 예를 들어, 재귀함수를 실행하던 중에 예기치 못한 오류가 발생되거나 메모리 누수가 일어나 메모리를 다 써버려서 프로그램이 다운될 뻔 한다거나 할 때 catch를 실행하도록 catch를 마치 최후의 수단처럼 이용하는 게 더 합리적이고 안전하지 않을까 하는 생각이 들었습니다.</p>
<pre><code class="language-js">const InputHandler = {
  async getValidInput(promptMessage, parser, validator, errorCategory) {
    let isNotValid = true;
    let parsedInput;

    while (isNotValid) {
      const input = await InputView.readLineAsync(promptMessage);
      parsedInput = parser ? parser(input) : input;

      const validationResults = validator(parsedInput);
      OutputView.printValidationResults(validationResults, errorCategory);
      isNotValid = Object.values(validationResults).some(value =&gt; value);
    }
    return parsedInput;
  },
};</code></pre>
<p>✅ 그래서 이번 리팩토링에서는 input 재귀함수를 util/inputHandler로 빼서 재활용성과 직관성을 챙겼고, try catch가 아닌 while로 함수를 실행하도록 수정하였습니다.</p>
<p>✅ 그리고 controller란 model과 view 사이를 오가며 마치 매니저 역할을 한다고 생각하는데 input만 담당하는 inputController가 굳이 필요할까? 라는 의문에 inputView에 객체 메서드를 추가하는 방향으로 리팩토링하였습니다.</p>
<h3 id="323-불필요한-class---객체-메서드로-수정">3.2.3 불필요한 class -&gt; 객체 메서드로 수정</h3>
<blockquote>
<p>필드가 없거나 필드가 아니어도 되는 상황에서도 class를 적극 활용해주신 부분을 코멘트 드리고 싶습니다~!</p>
</blockquote>
<p>✅ 단순 함수의 집합을 위해 작성하여 필드가 없는데 불필요하게 class로 작성했던 부분을 수정하였습니다.</p>
<h3 id="324-definitionmin_game-실수-수정">3.2.4 DEFINITION.MIN_GAME 실수 수정</h3>
<pre><code class="language-js">const DEFINITION = {
  MAX_CAR_RACERS: 40,
  MAX_NAME_LENGTH: 5,
  MIN_GAME: 0,
  MAX_GAME: 100,
};</code></pre>
<p>✅ MIN_GAME이 1이었어야하는데 0으로 적어서 게임 시도 0회에도 오류를 반환하지 않는 점을 수정하였습니다.</p>
<h3 id="325-car-의-name-유효성-검사-로직-추가">3.2.5 Car() 의 name 유효성 검사 로직 추가</h3>
<pre><code class="language-js">class Car {
  constructor(name) {
    this.validateName(name);
    this.name = name;
    this.position = 0;
  }

  validateName(name) {
    const validationResults = Validator.validateCarName(name);
    if (Object.values(validationResults).some(isError =&gt; isError)) {
      throw new Error(ERROR.MESSAGE.INVALID_CAR_NAME);
    }
  }</code></pre>
<p>✅ Car가 생성될 때 name이 string인지, 5글자 이하인지를 검사하는 로직을 추가하였습니다.</p>
<p>💭 이미 입력값을 받을 때 name의 유효성을 검사하고 인자를 보내는데 Car 클래스 내부에도 검증이 다시 필요할까? 하는 의문이 들었지만 크루원과 대화하면서 &#39;실제 프론트와 백엔드를 생각해보면 프론트에서 막지 못한 유효하지 않은 데이터 입력이 서버로 넘어갔을 때 서버에서도, 즉 도메인 로직에서도 방어를 할 수 있어야하지 않나 하는 시각에서 둘 다에서 검사해도 좋을 것 같다.&#39;라는 의견을 듣고, 제가 생각하지 못했던 부분이라 설득되었습니다!</p>
<h3 id="326-models-폴더-위치-변경">3.2.6 models 폴더 위치 변경</h3>
<p>✅ models/ 폴더를 domain/ 폴더 하위에 위치 시켰습니다.</p>
<h3 id="327-maxposition--mathmax-리팩토링">3.2.7 maxPosition =&gt; Math.max() 리팩토링</h3>
<blockquote>
<p>maxPosition은 Math.max 함수를 활용하고, winnerNames는 map 메서드를 활용하면 코드가 더 간결해질 수 있습니다!</p>
</blockquote>
<pre><code class="language-js">createCarList(names) {
  this.#carList = names.map(name =&gt; new Car(name));
}
...
const maxPosition = Math.max(...this.#carList.map(car =&gt; car.position));</code></pre>
<p>✅ 반영하였습니다!</p>
<h3 id="328-winnermanagerjs-분리">3.2.8 WinnerManager.js 분리</h3>
<pre><code class="language-js">class WinnerManager {
  #carList;
  #maxPosition;

  constructor(carList) {
    this.#carList = carList;
    this.#maxPosition = this.#getMaxPosition();
  }

  #getMaxPosition() {
    return Math.max(...this.#carList.map(car =&gt; car.position));
  }

  getWinners() {
    return this.#carList.filter(car =&gt; car.position === this.#maxPosition).map(car =&gt; car.name);
  }
}

export default WinnerManager;</code></pre>
<p>✅ 기존에 gmae.js 에서 judgeWinner()로 우승자를 판별하는 대신 WinnerManager.js 를 만들었습니다. </p>
<p>💭 우승자가 누구인지는 game의 필드값에서 쉽게 뽑아낼 수 있는 값인데 winner 객체가 굳이 필요할까 싶어서 따로 만들지 않을 생각이었습니다. 그런데 고민해보니 나중에 2,3등도 뽑아야한다던지 승률을 계산해야한다던지 하는 식의 로직 확장을 생각하면 확실히 분리하는 게 나을 것 같아 분리하였습니다.</p>
<h3 id="329-race-메서드-분리">3.2.9 race() 메서드 분리</h3>
<pre><code class="language-js"> #raceRound() {
    this.#carList.forEach(car =&gt; {
      const randomValue = createRandom();
      car.moveForward(randomValue);
      OutputView.roundResult(car.name, car.position);
    });
  }

  #raceGame(inputTryNumber) {
    for (let i = 0; i &lt; inputTryNumber; i++) {
      this.#raceRound();
      OutputView.break();
    }
  }</code></pre>
<p>✅ 회차별 레이스와 전체 레이스를 관리하는 로직을 분리하였습니다.</p>
<h2 id="33-리팩토링-의도">3.3 리팩토링 의도</h2>
<p>이번 리팩토링의 목표는 가독성을 높이고, 코드의 역할을 명확히 분리하여 유지보수성과 재활용성을 향상시키는 것이었습니다. 또한 쓸데없이 무겁지 않은 프로그램을 만드는 연습을 하기 위해 메모리 비용 측면에서도 고민을 해보았습니다.</p>
<p>크게 네 가지 방향에서 개선을 진행했습니다.</p>
<p>1️⃣ 불필요한 class → 객체 메서드로 변경
필드 없이 단순히 함수만 가지고 있는 클래스들은 객체 메서드로 변경하여 불필요한 인스턴스 생성을 줄이고, 직관적인 코드로 리팩토링했습니다.
예: Validator, InputHandler 등</p>
<p>2️⃣ 입력 검증 및 예외 처리 방식 개선
기존 try...catch 문을 이용한 재귀 호출을 제거하고, while 문을 사용하여 메모리 효율성을 높였습니다.
예외 처리는 정확한 예외 상황에서만 동작하도록 개선하여, 불필요한 호출과 메모리 낭비를 방지했습니다.
입력값 검증은 Validator에서 판단하고, 실제 에러 메시지를 출력하는 역할은 View가 담당하도록 책임을 분리하였습니다.</p>
<p>3️⃣ 우승자 판별 로직을 WinnerManager로 분리
기존 Game 클래스 내부에서 처리하던 우승자 판별 로직을 별도의 WinnerManager 클래스로 분리하여 확장성을 고려했습니다.</p>
<p>4️⃣ 메서드 단위 책임 분리 및 가독성 향상
start() 메서드가 너무 많은 역할을 하고 있었기 때문에 경주 준비(createCarList), 경기 진행(raceGame), <strong>결과 출력(judgeWinner)</strong>을 분리하여 코드 가독성을 향상시켰습니다.
또한, Math.max()를 활용하여 최대값을 구하고, map()을 이용해 우승자를 추출하는 등 더 간결한 코드로 리팩토링을 진행했습니다.</p>
<p>✅ 리팩토링 후 기대 효과</p>
<p>역할이 명확한 구조: 도메인(domain/)과 뷰(views/)를 분리하여 코드의 책임이 명확해짐
유지보수성 향상: 향후 기능 추가 시 변경 영향도를 최소화
메모리 효율 개선: 불필요한 try...catch 재귀 호출 제거
가독성 증가: 더 직관적인 코드 작성 (ex. Math.max() 활용, map()으로 데이터 변환)</p>
<p>이제 다음 미션에서도 보다 명확한 구조와 효율적인 코드 작성을 이어나갈 수 있도록 이번 리팩토링의 경험을 기억하고 더 좋은 코드를 고민하겠습니다! 🚀✨</p>
<p>긴 글 읽어주셔서 감사합니다. 좋은 하루되세요!😃</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 최종 코테 회고 및 최종 합격 후기]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%B5%9C%EC%A2%85-%EC%BD%94%ED%85%8C-%ED%9A%8C%EA%B3%A0-%EB%B0%8F-%EC%B5%9C%EC%A2%85-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%B5%9C%EC%A2%85-%EC%BD%94%ED%85%8C-%ED%9A%8C%EA%B3%A0-%EB%B0%8F-%EC%B5%9C%EC%A2%85-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 31 Dec 2024 14:18:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev-dino22/post/225accee-6f16-4e5b-b6d5-298216d2209f/image.png" alt="">
와아... 12/14에 봤던 우테코 최종 코테의 결과가 이번 27일 발표됐다. 믿기지 않게도 결과는 합격...! 프리코스부터 두 달간 우테코에 매우 몰입했기 때문에 막상 코테를 끝내고 나온 직후엔 후련한 마음이 들어 계속 웃음이 났던 기억이 난다. 엄마는 밝은 내 표정을 보고 잘본 것 같냐 기대하셨지만 내 대답은 &quot;아니 망했어 ㅎ&quot; 였다. 망했다면서 후련한 표정으로 계속 비식비식 웃는 나를 보며 의아해하셨지만 기능 구현도 다 못하고 나온 것치고 이상할 정도로 기분이 너무 후련해서 나도 신기했다. 그만큼 최선을 다했다는 생각이 들어 그랬던 것 같다.</p>
<p>그러나 코테가 끝나고 며칠이 지나자 조금 우울해졌었다. 이제 새해 계획을 세워야하는데 코테를 분명 망쳤다고 생각했기에 우테코를 제외한 일정을 세워야했지만 실낱같이 남은 기대감이 우테코 빠진 새해 계획 세우기를 주춤거리게 했다. 결국 최종 심사 결과 메일을 받을 때까지 신년 계획은 세우지 못한 채 한 구석 답답한 마음을 갖고 일상을 살았다.</p>
<p>그리고 코테를 본지 2주째 되던 27일, 최종 합격 메일을 받을 수 있었다! 합격 글자를 보자마자 심장이 쿵 떨어져 소리 먼저 꺅 지른 뒤 텍스트를 몇 번을 확인했는지 모르겠다. 혹시 간절한 마음이 만들어낸 환상일까 싶어서...(?)🤣</p>
<p>그렇게 너무 얼떨떨해 기쁜지도 모르겠는 하루가 지나자 매우 지독한 감기에 앓아누워버렸다. 병원에 가니 다행히 독감은 아니라고 했다. 긴장이 풀린 탓인지 요즘 감기가 유행이어서인지는 모르겠지만 정말 오랜만에 걸린 감기다. 열이 펄펄 끓어올라 합격의 에너지로 뭐라도 막 하고싶었던 마음과 달리 여태 약 먹고 자기만 반복했다. 기침을 너무 많이 했더니 진짜 온몸이 다 아프다...🥲</p>
<p>아무튼 감사하게도 나의 신년계획은 우테코로 가득차게 되었다. 이제 천천히 블로그 회고글을 마무리하고 우테코 시작 전까지 정보처리기사 필기를 취득해보려한다.</p>
<h1 id="1-최종-코테-준비">1. 최종 코테 준비</h1>
<h2 id="11-풀어본-문제들">1.1 풀어본 문제들</h2>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/befd10e3-592a-4094-8277-74b6504d5101/image.png" alt=""></p>
<p>우선 스터디원 분과 함께 이번년도 7기 프리코스 1<del>4주차 과제를 다시 풀어보았다. 2주차 과제까지는 각자 코테 환경과 같이 gpt 없이 5시간을 재고 풀어서 깃허브에 공유하고 코드리뷰를 남기는 식으로 진행했다. 그리고 3</del>4주차는 페어 프로그래밍을 하며 같이 풀었는데 이 경험이 정말 도움이 많이 되었다. 페어 프로그래밍이라는 걸 처음해본 나는 너무 민폐만 끼칠까봐 걱정했는데, 음... 역시 내가 더 너무 많이 얻어갔던 스터디같지만... 스터디원 분이 감사하게도 정말 친절하게 이끌어주시고 가르쳐주셔서 많이 배울 수 있었던 좋은 경험이었다. 그리고 스터디원 분이 로직을 짜실 때 느꼈던 게, 나는 뭔가 한치 앞만 보며 우다다 구현하느라 뒤에 가서 꼬이는 경우가 많았는데, 스터디원 분은 기능 단위 구현을 할 때도 몇 수 앞을 보면서 로직을 짜셔서 신기했다. 그리고 메소드 체이닝도 잘 활용하셔서 간편하게 코드가 완성되는 것도 신기했다.</p>
<p>페어 프로그래밍은 평소에 갇혀있던 내 개발 습관과 지식에서 벗어나 상대의 지식과 프로그래밍 접근 방식을 이해하고 배워볼 수 있어서 꼭 해보면 좋은 스터디 방식인 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/00139004-ca1a-40c5-8b51-30dbe2adedc7/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev-dino22/post/94b60509-637d-4b8d-a222-21873d98c6ce/image.png" alt=""></p>
<p>그리고 개인적으로는 5기와 6기 최종코테 문제도 실제 코테와 같은 조건으로 풀어보았다. 두 문제 다 생각보다 5시간 안에 기능 구현 후 리팩토링까지 하기에 넉넉했기 때문에 이 수준으로 나온다면 해볼만하다고 생각했었다.</p>
<p>하지만 역시나 걱정은 4주차 과제인 편의점 수준으로 나왔을 때였는데, 기능 구현 자체가 어려운 것보다 구현 시간이 너무 오래걸릴 것 같았기 때문이었다.
그래서 편의점 문제를 시간 재고 다시 풀어보았지만...이미 세번 째 푼 문제인데도 6시간이 넘게 걸렸다 ㅠ</p>
<h2 id="12-준비해간-나만의-템플릿">1.2 준비해간 나만의 템플릿(?)</h2>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/2c6f695d-2cd2-49f5-a82c-2964d5141c78/image.png" alt=""></p>
<p>예고된 최종 코테의 주의사항은 기본적으로 오픈북이지만 AI 도구 사용은 금한다는 것이었다.</p>
<p>그래서 최종 코딩테스트를 보기 전, 나는 모든 프리코스에 공통적으로 쓰였던 InputVeiw.js, OutputView.js, Parser.js, Validator.js, Message.js 파일들을 재사용성 좋게 템플릿화 시켜 준비해갔다. 최종 코테가 어떤 식으로 나올지는 모르겠지만 기능 구현을 최대한 빨리하는 게 중요할 것 같았기 때문이다. 그래서 규칙을 위반하지 않는 한 빠르게 개발을 할 수 있는 환경을 최대한 준비해가고 싶었다.</p>
<p>내가 준비한 파일들 중 Validator.js 파일을 예시로 적어두자면 아래와 같이 자주 사용해야하는 검증 로직들을 쉽게 재활용할 수 있게 작성해갔다. Message.js는 에러 출력 메세지나 안내 메세지 등의 상수를 저장해둘 상수객체 파일이고 Definition.js는 구분자 상수 같은 정의 상수(?)들을 저장해둔 상수객체 파일이다.
인자를 string, array 와 같이 겹칠 위험이 큰 흔한 네이밍을 하는 게 맞는지는 모르겠으나, 일단 코테를 빠르게 완성시키기 위한 Validator이므로 받아야하는 인자의 타입으로 네이밍해두었다.</p>
<p>isStringLengthInRange 처럼 범위(숫자 상수) 안에 있는 값인지를 판단해야하는 경우 원래는 최솟값과 최댓값을 Definition.js에서 설정하게 했었지만 5기, 6기 문제를 풀 때 실용성을 따져보니 이렇게 인자로 받는 게 재활용하기 더 좋은 것 같아서 리팩토링하였다.</p>
<pre><code class="language-js">import { DEFINITION } from &#39;../constants/Definition.js&#39;;
import { MESSAGE } from &#39;../constants/Message.js&#39;;

export const Validator = {
  isEmpty: (string) =&gt; {
    if (string === null || string.trim().length === 0 || !string) {
      throw new Error(MESSAGE.ERROR.IS_EMPTY);
    }
  },
  isMaxStringLength: (string, max) =&gt; {
    if (string.length &gt; max) {
      throw new Error(MESSAGE.ERROR.IS_MAX_STRING_LENGTH);
    }
  },
  isStringLengthInRange: (string, min, max) =&gt; {
    if (string.length &gt; max || string.length &lt; min) {
      throw new Error(MESSAGE.ERROR.IS_NOT_STRING_LENGTH_RANGE);
    }
  },
  isMaxArrayLength: (array, max) =&gt; {
    if (array.length &gt; max) {
      throw new Error(MESSAGE.ERROR.IS_MAX_ARRAY_LENGTH);
    }
  },
  isArrayLengthInRange: (array, min, max) =&gt; {
    if (array.length &gt; max || array.length &lt; min) {
      throw new Error(MESSAGE.ERROR.IS_NOT_ARRAY_LENGTH_RANGE);
    }
  },
  isCorrectArrayLength: (array, number) =&gt; {
    if (array.length !== number) {
      throw new Error(&#39;[ERROR] 입력 요소의 수가 알맞지 않습니다.&#39;);
    }
  },
  isSameInArray: (array) =&gt; {
    const arrayCopy = [...new Set([...array])];
    if (array.length !== arrayCopy.length) {
      throw new Error(MESSAGE.ERROR.IS_SAME_IN_ARRAY);
    }
  },
  isNumber: (number) =&gt; {
    if (typeof number !== &#39;number&#39; || Number.isNaN(number)) {
      throw new Error(MESSAGE.ERROR.NOT_NUMBER);
    }
  },
  isMaxNumber: (number) =&gt; {
    if (number &gt;= DEFINITION.MAX.NUMBER) {
      throw new Error(MESSAGE.ERROR.IS_MAX_NUMBER);
    }
  },
  isNumberInMinAndMax: (number, min, max) =&gt; {
    if (number &lt; min || number &gt; max) {
      throw new Error(MESSAGE.ERROR.IS_NOT_NUMBER_RANGE);
    }
  },
  isNaturalNumber: (number) =&gt; {
    if (
      number &lt;= 0 ||
      !Number.isInteger(number) ||
      Number.isNaN(Number(number))
    ) {
      throw new Error(MESSAGE.ERROR.NOT_NATURAL_NUMBER);
    }
  },
  isInputInArray: (input, inputArray) =&gt; {
    if (inputArray.includes(input)) {
      throw new Error(MESSAGE.ERROR.IS_INPUT_IN_ARRAY);
    }
  },
  notInputInArray: (input, inputArray) =&gt; {
    if (!inputArray.includes(input)) {
      throw new Error(MESSAGE.ERROR.IS_INPUT_IN_ARRAY);
    }
  },
  isValidBooleanInput: (input) =&gt; {
    // {Y or N}의 입력값이 왔는지 확인하는 로직(Definition 에서 Y나 N대신 다른 값으로 쉽게 바꿀 수 있게 리팩토링했음)
    // 대문자 소문자도 정확히 같아야함
    if (!DEFINITION.VALID_TRUE_OR_FALSE.includes(input.trim())) {
      throw new Error(
        MESSAGE.ERROR.NOT_VALID_TRUE_OR_FALSE(
          DEFINITION.VALID_TRUE_INPUT,
          DEFINITION.VALID_FALSE_INPUT,
        ),
      );
    }
  },
  isValidEdge: (string) =&gt; {
    if (
      string[0] !== DEFINITION.VALID_EDGE_START ||
      string[string.length - 1] !== DEFINITION.VALID_EDGE_END
    ) {
      throw new Error(
        MESSAGE.ERROR.NOT_VALID_EDGE(
          DEFINITION.VALID_EDGE_START,
          DEFINITION.VALID_EDGE_END,
        ),
      );
    }
  },
  isFalseThrowError: (boolean, message) =&gt; {
    if (!boolean) {
      throw new Error(message);
    }
  },
};</code></pre>
<h2 id="13-돌아보니-아쉬웠던-점">1.3 돌아보니 아쉬웠던 점</h2>
<p>Validator의 에러 메세지를 상수로 빼지말 걸 그랬다... 에러 메세지 안에 [ERROR]만 앞에 포함되어 있으면 구체적인 에러 메세지를 정해주지 않았던 프리코스와 달리 최종 코딩테스트에서는 에러 메세지의 문구까지 지정해주었기 때문이다. 뭐, 이거 조금 수정한다고 오래 걸리는 건 아니지만...좀 과한 준비였던 것 같다.</p>
<p>(아, 참고로 내 코드처럼 Validator를 작성해 테스트코드에 통과하려면, validator를 호출하는 상위 파일인 main.js나 app.js에서 추가 작업이 필요하다. throw new Error를 검사하지 않고 에러메세지를 출력하는지를 검사하는 테스트코드라면 7기 기준 MissionUtils.Console.print에 스파이를 붙여 에러메세지를 출력하는지를 확인하기 때문에, 상위 파일에서 <code>try...catch</code> 문을 써서 <code>...catch (error) MissionUtils.Console.print(error.message)</code>와 같이 명시적으로 Console.print가 에러 &quot;메세지&quot;를 출력하도록 처리해주어야한다. 이렇게하면 Validator에서 던진 에러의 메세지를 프린트할 수 있다.)</p>
<p>그리고 프리코스 종료 후 받았던 안내 메일에 아래와 같은 안내가 있었는데,
<img src="https://velog.velcdn.com/images/dev-dino22/post/6ed4eff2-d59e-44c1-8a94-56ad7808860f/image.png" alt=""></p>
<p>물론 이 안내에 따라 1~4주차를 다시 구현해보고 4주차는 3번이나 풀어보긴했지만 그래도 내가 끝끝내 시간 내에 못 풀었던 4주차 미션을 이 부분을 되새기며 더 깊게 풀고 분석해볼걸 하는 후회가 남았다. 지난 기수 최종 코테 문제들을 풀 시간에 말이다. ㅠㅠ 스포하자면 최종 코테는 결국 4주차 편의점과 매우 유사하게 출제되었다.</p>
<p>코딩테스트도 결국 시험이기에 출제자의 의도와 일종의 시험 범위인 프리코스 4주차들 문제의 핵심을 파악해 공부하는 것이 좋았던 것 같다.</p>
<p>나름 준비하면서 내가 파악했던 7기 프리코스의 핵심은
<strong>[ 구분자 및 split 메서드 활용, 재고관리와 같은 상태관리 시스템, md로 작성된 줄글파일을 데이터로 파싱, new Date와 DateTimes(우테코 API)의 활용 ]</strong>이었다.</p>
<p>나머지들은 파악한 대로 잘 준비해갔지만...Date 객체는 공부하지 않고 갔다. 이유는... 그냥 까먹었다...블로그에 포스팅하면서 공부해야지 미뤄두다가...
그리고 결국 내가 생각했던 7기 프리코스의 핵심대로 문제는 출제되었다. 그런데 생각보다 기본 기능을 구현하기 위해 자바스크립트의 Date 객체를 활용해야하는 문제가 나왔고 자바스크립트가 Date 객체에 어떤 메서드들을 갖고 있고 각 메소드들은 어떻게 사용되는지를 전혀 몰랐던 나는 급하게 구글링하며 메서드 사용법을 현장에서 익혔다. ㅠㅠ 기능 구현 과정은 아래에 자세히 후술하겠지만, 아무튼 여기서 꽤 시간을 잡아먹은 이슈로... 기능 구현을 조금 남기고 마무리하지 못하게되었었다.</p>
<p>만약 내년도 우테코에 도전할 사람이 내 포스팅을 읽게된다면 준비하는데에 참고가 되길 바라며 아쉬웠던 점을 아래 요약해보겠다.</p>
<blockquote>
</blockquote>
<ul>
<li>상수 파일은 딱히 미리 준비 안해도 될 것 같다.</li>
<li>지난 기수 코테 문제들을 풀어보는 것보다 프리코스 과제들을 최종 코테와 같은 환경에서 완벽하게 풀어내는 것에 더 집중하는 게 좋은 것 같다. 지난 기수 코테 문제들은 완전 여유 남을 때 긴장풀 겸 가볍게 풀어보는 걸로 추천...!</li>
<li>자주 사용하는 Validator와 같은 파일들은 템플릿화 시켜서 준비해가면 시간 단축에 꽤 용이하다!</li>
<li>프리코스 과제들을 회고하며 여기서 어떤 로직이 핵심이었는지를 생각해보고 활용한 자바스크립트 내장 api에 대해 완벽히 숙지하고 가면 좋을 것 같다!</li>
</ul>
<h1 id="2-최종-코테">2. 최종 코테</h1>
<p>7기 최종 코딩테스트는 &quot;출석부&quot; 문제였다. csv로 작성된 출석 기록들을 데이터로 파싱해서 요구하는 출석부 기능을 구현하면 됐다.
<img src="https://velog.velcdn.com/images/dev-dino22/post/3ef48bd7-dad9-4116-9859-bbbac36ebd7b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/8414005a-a247-4fe1-84ac-9d453252b997/image.png" alt=""></p>
<p>그리고 내가 코딩테스트를 망쳤다고 생각한 이유이다. 기본 기능 테스트도 다 통과를 못한 채 내게 됐다. Date 객체 메소드 활용처럼 미리 공부해왔다면 버리지않았을 시간을 잡아먹느라 시간 분배에 실패했고, 1~20분만 더 있었어도 기능을 완성해낼 수 있었을 것 같아 너무나 아쉬웠던 결과였다. </p>
<p>그런데 &#39;돌아가는 쓰레기&#39;라도 만들어서 내라고 할만큼 기능 구현을 강조하는 우테코 코테에 어떻게 합격할 수 있었는지는 아직도 잘 모르겠다.
생각보다 코테 비중보다 자소서와 소감문, 프리코스 점수, 블로그 같은 게 더 비중이 높았던 걸까? 아니면 활발한 코드리뷰의 흔적?</p>
<h1 id="3-소감">3. 소감</h1>
<p>아무튼 감사하게도 우테코에 합격하게 되었다. 이 회고글을 마치는 지금, 새해를 1시간 앞두고 있다. 정말 많은 일이 있었던 한 해였는데, 우테코 합격으로 마무리할 수 있게되어 정말 기쁘다. 평소 디자이너로서 참여한 프로젝트에 조금씩 자바스크립트를 적용해보는 등 개발 업무에 관심은 있었지만 비전공자인 내가 갑자기 진로를 바꿔 개발자로 일할 수 있을지는 모르겠어서 사실 개발자로 취업을 하고 싶다는 생각은 하지 않았었다. 그저 자바스크립트/리액트를 조금 아는 디자이너라고 말할 수 있게 되는 것을 목표로 삼고 있을 뿐이었는데, 문득 찾아온 이 기회가 나도 개발자가 될 수 있는지에 대한 답을 줄 것 같아서 마음이 싱숭생숭 설레는 밤이다.</p>
<p>내년 이맘쯤의 나는 어떤 경험을 하고 어떤 성장을 했을까? 기대를 담아, 신년 계획을 세우러 가야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Javascript API] padStart(),padEnd() - 문자열 메서드]]></title>
            <link>https://velog.io/@dev-dino22/Javascript-API-padStartpadEnd-%EB%AC%B8%EC%9E%90%EC%97%B4-%EB%A9%94%EC%84%9C%EB%93%9C</link>
            <guid>https://velog.io/@dev-dino22/Javascript-API-padStartpadEnd-%EB%AC%B8%EC%9E%90%EC%97%B4-%EB%A9%94%EC%84%9C%EB%93%9C</guid>
            <pubDate>Wed, 11 Dec 2024 16:24:52 GMT</pubDate>
            <description><![CDATA[<h1 id="1-자바스크립트--문자열-메서드-padstart-padend">1. 자바스크립트  문자열 메서드 padStart(), padEnd()</h1>
<p><code>padStart()</code>와  <code>padEnd()</code>메서드는 자바스크립트 문자열 메서드 중 하나로, 결과 문자열이 주어진 길이에 도달할 때까지 지정된 문자열로 채운다. <code>padStart()</code>는 문자열 시작 부분에 패딩이 적용되며 <code>padEnd()</code>는 끝 부분에 패딩이 적용된다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/8e33b5b1-e073-4a1b-81c8-9ef5be033efd/image.png" alt=""></p>
<p>우아한테크코스 프리코스 4주차 편의점 과제를 풀 때 이렇게 영수증을 정렬해서 출력하기 위해 사용했던 메서드이다.</p>
<h2 id="1-1-padstart와-padend의-기본-문법">1-1. padStart()와 padEnd()의 기본 문법</h2>
<p><code>padStart()</code> 메서드의 기본 문법은 다음과 같다:</p>
<pre><code class="language-js">padStart(targetLength, padString)
padEnd(targetLength, padString)</code></pre>
<p>이 메서드는 순서대로 <code>대상 문자열의 원하는 길이값(필수 인자)</code>와 <code>패딩으로 채울 값(선택 사항 인자)</code>를 받는다.</p>
<p>대상 문자열의 길이가 <code>targetLength</code>가 될 때까지 <code>padString</code>을 반복해 길이를 채운다. <code>padString</code>이 생략된다면 공백(space)로 채운다.</p>
<h2 id="1-2-padstart-padend-작성-예시">1-2. padStart(), padEnd() 작성 예시</h2>
<p>다음은 padStart(), padEnd()의 작성 예시이다.</p>
<pre><code class="language-js">// padStart() 예제
const str1 = &quot;123&quot;;
console.log(str1.padStart(5, &quot;0&quot;)); // 출력: &quot;00123&quot;

const str2 = &quot;AB&quot;;
console.log(str2.padStart(6, &quot;-&quot;)); // 출력: &quot;----AB&quot;

// padEnd() 예제
const str3 = &quot;123&quot;;
console.log(str3.padEnd(5, &quot;0&quot;)); // 출력: &quot;12300&quot;

const str4 = &quot;XY&quot;;
console.log(str4.padEnd(6, &quot;*&quot;)); // 출력: &quot;XY****&quot;
</code></pre>
<h2 id="1-3-padstart-padend의-특징과-주의할-점">1-3. padStart(), padEnd()의 특징과 주의할 점</h2>
<ul>
<li><p>padString의 길이가 targetLength를 초과해도 padStart 또는 padEnd는 필요한 만큼만 padString을 잘라서 사용한다.</p>
<pre><code class="language-js">const str = &quot;12&quot;;
console.log(str.padStart(10, &quot;123&quot;)); // 출력: &quot;1231231232&quot;</code></pre>
</li>
<li><p>정렬 및 출력 서식 지정에 유용한 메서드이다. 글 첫 부분 영수증 예제처럼 padStart(), padEnd()를 마치 우측정렬 좌측정렬 기능처럼 이용하면 깔끔하게 정렬된다.</p>
<pre><code class="language-js">const items = [
  { name: &quot;Apple&quot;, price: 100 },
  { name: &quot;Banana&quot;, price: 150 },
  { name: &quot;Cherry&quot;, price: 200 },
];
</code></pre>
</li>
</ul>
<p>items.forEach(item =&gt; {
    console.log(
        item.name.padEnd(10) + // 이름을 좌측 정렬
        item.price.toString().padStart(6) + &quot;원&quot; // 가격을 우측 정렬
    );
});
// 출력:
// Apple     100원
// Banana    150원
// Cherry    200원</p>
<pre><code>
## 1-4. 활용
```js
 products.forEach((product) =&gt; {
      const nameLength = 5 - product.name.length;
      const nameSpace = nameLength + 12;
      const name = product.name.padEnd(nameSpace);
      const quantity = product.quantity.padEnd(6);
      const price = product.price.padStart(10);
      MissionUtils.Console.print(`${name}${quantity}${price}`);
    });</code></pre><p>위 코드는 우테코 4주차 프리코스 영수증 출력에서 상품명이 한글이고 각 길이가 다른 점에 따라, 상품명 길이가 달라도 정렬이 바르게 될 수 있도록 리팩토링 때 추가했던 코드이다.</p>
<p>nameLength 에 적은 5는 상품명의 최대 길이를 적은 것이다.
nameSpace는 기본 12번에 추가로 (가장 긴 상품명의 길이) - (현재 상품명의 길이) 번 더 추가된다.
이렇게하면, </p>
<p>글자길이가 2일 때, nameSpace는 3번 더
글자길이가 3일 때, nameSpace는 2번 더
글자길이가 5일 때, nameSpace는 0번 더 추가됨으로써</p>
<p>모든 name칸이 (가장 긴 상품명의 길이)에 맞춰진 상태에서 우측 패딩값을 주는 것과 같은 효과가 되는 것이다.
<img src="https://velog.velcdn.com/images/dev-dino22/post/7159638a-c034-489c-acea-a371afc5361b/image.png" alt="">
이렇게 잘 정렬돼서 출력되는 걸 확인할 수 있따.</p>
<hr>
<p>글 작성 참고 사이트: <a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd</a>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/padStart">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/padStart</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우아한테크코스] 1차 심사 결과 안내 - 합격!]]></title>
            <link>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-1%EC%B0%A8-%EC%8B%AC%EC%82%AC-%EA%B2%B0%EA%B3%BC-%EC%95%88%EB%82%B4-%ED%95%A9%EA%B2%A9</link>
            <guid>https://velog.io/@dev-dino22/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-1%EC%B0%A8-%EC%8B%AC%EC%82%AC-%EA%B2%B0%EA%B3%BC-%EC%95%88%EB%82%B4-%ED%95%A9%EA%B2%A9</guid>
            <pubDate>Mon, 09 Dec 2024 08:31:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev-dino22/post/d74830c6-88c6-48a3-9c5b-79c432bf2688/image.png" alt=""></p>
<p>와우.......................
우테코에 1차 합격을 했다....!!!!!</p>
<p>개발공부에 기회가 없었던 비전공자 독학러인 나에게 우테코는 4주간의 프리코스만으로 너무나 충분히 많은 공부가 됐던 경험이었기 때문에 1차합격을 간절히 바라면서도 기대는 하지 않았었는데 덜컥 붙다니 확인하자마자 소리를 빽 질러버렸다. 가장 먼저 엄마한테 자랑하고 친구한테 자랑하고 동네방네 떠들기...ㅎㅎ</p>
<p>사실 너무 들뜨면서도 현실감각이 없어서 어안이 벙벙한 느낌이긴 하다. 프리코스 종료 후 오늘 결과발표까지 지난 과제들 회고도 하고 처음부터 다시 시간재고 작성도 해보면서 복습하며 보내고 있었는데, 이제 일주일 간 최종코테 준비에 몰입을 해야겠다.</p>
<p>개발 공부를 제대로 시작한지 얼마 안된 내가 코딩 테스트를 치르러 갈 수 있다는 것만으로 너무 들뜨고 기뻐서 이제 최종 합격 여부가 어떻게 되든 미련없이 받아들일 수 있을 것 같다 ㅎㅎ</p>
<p>원래 지금쯤 지난 회고글들 작성이 완료되었어야하는데 아직도 작성중에 머물고있지만...
이런 행복한 코테기회로 회고글 완성은 좀 더 뒷전으로... ㅠ_ㅠㅋㅋㅋㅋㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[4주차 프리코스] 편의점 개발 회고 - 작성 중...]]></title>
            <link>https://velog.io/@dev-dino22/4%EC%A3%BC%EC%B0%A8-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-%ED%8E%B8%EC%9D%98%EC%A0%90-%EA%B0%9C%EB%B0%9C-%ED%9A%8C%EA%B3%A0-%EC%9E%91%EC%84%B1-%EC%A4%91</link>
            <guid>https://velog.io/@dev-dino22/4%EC%A3%BC%EC%B0%A8-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-%ED%8E%B8%EC%9D%98%EC%A0%90-%EA%B0%9C%EB%B0%9C-%ED%9A%8C%EA%B3%A0-%EC%9E%91%EC%84%B1-%EC%A4%91</guid>
            <pubDate>Thu, 28 Nov 2024 14:30:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev-dino22/post/001b36dd-bf7b-4a6b-9273-143715702b3e/image.png" alt=""></p>
<p>4주차 과제가 끝난지 3주 가까이 지나서야 작성하기 시작하는 4주차 개발 회고글이다. 일과 병행하면서 남는 시간에 잠 줄여가며 참여했던 프리코스인지라 끝난 뒤 휴식도 필요했고 밀린 일을 처리하기도 바빴었다. 아무튼...나에게 멘붕을 선사해주었던 4주차 편의점 과제...
슬프게도 4주차 과제는 테스트 통과만 겨우하고 기능만 겨우 구현해서 제출했다. 일이 겹쳐서 절대적인 시간이 부족하기도 했고, 기능요구사항이 많은 과제에 아직 익숙치않은 TDD, 객체지향 프로그래밍을 적용하며 풀어가는 과정에서 시간 분배에 완전히 실패했기 때문이었다.
기능을 개발해가는 과정에서 계속 혼자 해석이 달라지고 헷갈려서 기능 개발이 끝난 줄 알았는데 계속 또 수정해가며 시간을 빼앗긴 문제도 있었다.
결국 핵심 메서드 찢기에도 실패해서 함수 길이가 100줄이 되는 것에 더해 단일 책임 원칙도 지키지 못했고, 문제가 되는 저 메서드의 들여쓰기도 2를 초과하고 말았다... 프로그래밍 요구사항을 전혀 지키지 못한 것이다...ㅠㅠ 제출 직전까지 해서 내느라 기본적인 예외 케이스 검증 로직도 처리하지 못했던 것은 덤이다... 정말 돌아가는 쓰레기를 제출해버리고 말아서 제출하고나서 허무하고 우울해했던 기억이 난다.</p>
<p>하지만 많은 것을 배울 수 있었던 과제이고 일단 4/4로 제출할 수 있었음에 위안을 삼아본다🥹</p>
<p>해당 레포를 공개로 돌려놓기 상당히 부끄럽지만...</p>
<blockquote>
<p>절망의 내 4주차 프리코스 제출 링크: <a href="https://github.com/dev-dino22/javascript-convenience-store-7-dev-dino22">https://github.com/dev-dino22/javascript-convenience-store-7-dev-dino22</a></p>
</blockquote>
<p>그나저나, 4주차 과제를 진행하는 1주일동안 새로 배웠던 것과 깨닫고 느낀 점이 너무 많았는데, 이것들을 이 포스팅에 글로 정리하려면 정말 오래 걸릴 것 같다...일단 아직 2주차 회고도 작성 중인 상태고 그 와중에 최종 코테 준비로 5시간 시간제한과 AI 사용제한을 두고 1주차부터 과제를 다시 풀어보고 있어서 이 것에 대한 회고 작성도 병행해야하므로... 과제사항만 일단 백업해두고 조금씩 글을 작성해나가야겠다.</p>
<h1 id="📢-1-프리코스-4주차-과제-요구사항">📢 1. 프리코스 4주차 과제 요구사항</h1>
<p>4주차 프리코스 과제의 요구사항은 다음과 같았다.</p>
<blockquote>
<h2 id="편의점">편의점</h2>
</blockquote>
<hr>
<blockquote>
<h3 id="📌-과제-진행-요구-사항">📌 과제 진행 요구 사항</h3>
</blockquote>
<ul>
<li>미션은 편의점 저장소를 생성하는 것으로 시작한다.</li>
<li>기능을 구현하기 전 <code>README.md</code>에 구현할 기능 목록을 정리해 추가한다.</li>
<li>Git의 커밋 단위는 앞 단계에서 <code>README.md</code>에 정리한 기능 목록 단위로 추가한다.</li>
<li><a href="https://gist.github.com/stephenparish/9941e89d80e2bc58a153">AngularJS Git Commit Message Conventions</a>을 참고해 커밋 메시지를 작성한다.</li>
<li>자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다.</li>
</ul>
<hr>
<blockquote>
<h3 id="🔨-기능-요구-사항">🔨 기능 요구 사항</h3>
<p>구매자의 할인 혜택과 재고 상황을 고려하여 최종 결제 금액을 계산하고 안내하는 결제 시스템을 구현한다.</p>
</blockquote>
<ul>
<li>사용자가 입력한 상품의 가격과 수량을 기반으로 최종 결제 금액을 계산한다.<ul>
<li>총구매액은 상품별 가격과 수량을 곱하여 계산하며, 프로모션 및 멤버십 할인 정책을 반영하여 최종 결제 금액을 산출한다.</li>
</ul>
</li>
<li>구매 내역과 산출한 금액 정보를 영수증으로 출력한다.</li>
<li>영수증 출력 후 추가 구매를 진행할지 또는 종료할지를 선택할 수 있다.</li>
<li>사용자가 잘못된 값을 입력할 경우 &quot;[ERROR]&quot;로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.<h4 id="재고-관리">재고 관리</h4>
</li>
<li>각 상품의 재고 수량을 고려하여 결제 가능 여부를 확인한다.</li>
<li>고객이 상품을 구매할 때마다, 결제된 수량만큼 해당 상품의 재고에서 차감하여 수량을 관리한다.</li>
<li>재고를 차감함으로써 시스템은 최신 재고 상태를 유지하며, 다음 고객이 구매할 때 정확한 재고 정보를 제공한다.<h4 id="프로모션-할인">프로모션 할인</h4>
</li>
<li>오늘 날짜가 프로모션 기간 내에 포함된 경우에만 할인을 적용한다.</li>
<li>프로모션은 N개 구매 시 1개 무료 증정(Buy N Get 1 Free)의 형태로 진행된다.</li>
<li>1+1 또는 2+1 프로모션이 각각 지정된 상품에 적용되며, 동일 상품에 여러 프로모션이 적용되지 않는다.</li>
<li>프로모션 혜택은 프로모션 재고 내에서만 적용할 수 있다.</li>
<li>프로모션 기간 중이라면 프로모션 재고를 우선적으로 차감하며, 프로모션 재고가 부족할 경우에는 일반 재고를 사용한다.</li>
<li>프로모션 적용이 가능한 상품에 대해 고객이 해당 수량보다 적게 가져온 경우, 필요한 수량을 추가로 가져오면 혜택을 받을 수 있음을 안내한다.</li>
<li>프로모션 재고가 부족하여 일부 수량을 프로모션 혜택 없이 결제해야 하는 경우, 일부 수량에 대해 정가로 결제하게 됨을 안내한다.<h4 id="멤버십-할인">멤버십 할인</h4>
</li>
<li>멤버십 회원은 프로모션 미적용 금액의 30%를 할인받는다.</li>
<li>프로모션 적용 후 남은 금액에 대해 멤버십 할인을 적용한다.</li>
<li>멤버십 할인의 최대 한도는 8,000원이다.<h4 id="영수증-출력">영수증 출력</h4>
</li>
<li>영수증은 고객의 구매 내역과 할인을 요약하여 출력한다.</li>
<li>영수증 항목은 아래와 같다.
  <strong>구매 상품 내역</strong>: 구매한 상품명, 수량, 가격
  <strong>증정 상품 내역</strong>: 프로모션에 따라 무료로 제공된 증정 상품의 목록
  <strong>금액 정보</strong><pre><code>  - 총구매액: 구매한 상품의 총 수량과 총 금액
  - 행사할인: 프로모션에 의해 할인된 금액
  - 멤버십할인: 멤버십에 의해 추가로 할인된 금액
  - 내실돈: 최종 결제 금액</code></pre></li>
<li>영수증의 구성 요소를 보기 좋게 정렬하여 고객이 쉽게 금액과 수량을 확인할 수 있게 한다.</li>
</ul>
<h3 id="🔴-입출력-요구-사항">🔴 입출력 요구 사항</h3>
<h4 id="입력">입력</h4>
<ul>
<li>구현에 필요한 상품 목록과 행사 목록을 파일 입출력을 통해 불러온다.
<code>products.md</code>과 <code>promotions.md</code> 파일을 이용한다.</li>
<li>두 파일 모두 내용의 형식을 유지한다면 값은 수정할 수 있다.</li>
<li>구매할 상품과 수량을 입력 받는다. 상품명, 수량은 하이픈(-)으로, 개별 상품은 대괄호([])로 묶어 쉼표(,)로 구분한다.<pre><code class="language-js">[콜라-10],[사이다-3]</code></pre>
</li>
<li>프로모션 적용이 가능한 상품에 대해 고객이 해당 수량보다 적게 가져온 경우, 그 수량만큼 추가 여부를 입력받는다.<ul>
<li>Y: 증정 받을 수 있는 상품을 추가한다.</li>
<li>N: 증정 받을 수 있는 상품을 추가하지 않는다.<pre><code class="language-js">Y</code></pre>
</li>
</ul>
</li>
<li>프로모션 재고가 부족하여 일부 수량을 프로모션 혜택 없이 결제해야 하는 경우, 일부 수량에 대해 정가로 결제할지 여부를 입력받는다.<ul>
<li>Y: 일부 수량에 대해 정가로 결제한다.</li>
<li>N: 정가로 결제해야하는 수량만큼 제외한 후 결제를 진행한다.<pre><code class="language-js">Y</code></pre>
</li>
</ul>
</li>
<li>멤버십 할인 적용 여부를 입력 받는다.<ul>
<li>Y: 멤버십 할인을 적용한다.</li>
<li>N: 멤버십 할인을 적용하지 않는다.<pre><code class="language-js">Y</code></pre>
</li>
</ul>
</li>
<li>추가 구매 여부를 입력 받는다.<ul>
<li>Y: 재고가 업데이트된 상품 목록을 확인 후 추가로 구매를 진행한다.</li>
<li>N: 구매를 종료한다.<pre><code class="language-js">Y</code></pre>
</li>
</ul>
</li>
</ul>
<h4 id="출력">출력</h4>
<ul>
<li>환영 인사와 함께 상품명, 가격, 프로모션 이름, 재고를 안내한다. 만약 재고가 0개라면 <code>재고 없음</code>을 출력한다.</li>
</ul>
<pre><code class="language-js">안녕하세요. W편의점입니다.
현재 보유하고 있는 상품입니다.

- 콜라 1,000원 10개 탄산2+1
- 콜라 1,000원 10개
- 사이다 1,000원 8개 탄산2+1
- 사이다 1,000원 7개
- 오렌지주스 1,800원 9개 MD추천상품
- 오렌지주스 1,800원 재고 없음
- 탄산수 1,200원 5개 탄산2+1
- 탄산수 1,200원 재고 없음
- 물 500원 10개
- 비타민워터 1,500원 6개
- 감자칩 1,500원 5개 반짝할인
- 감자칩 1,500원 5개
- 초코바 1,200원 5개 MD추천상품
- 초코바 1,200원 5개
- 에너지바 2,000원 5개
- 정식도시락 6,400원 8개
- 컵라면 1,700원 1개 MD추천상품
- 컵라면 1,700원 10개

구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])</code></pre>
<ul>
<li><p>프로모션 적용이 가능한 상품에 대해 고객이 해당 수량만큼 가져오지 않았을 경우, 혜택에 대한 안내 메시지를 출력한다.</p>
<pre><code class="language-js">현재 {상품명}은(는) 1개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)</code></pre>
</li>
<li><p>프로모션 재고가 부족하여 일부 수량을 프로모션 혜택 없이 결제해야 하는 경우, 일부 수량에 대해 정가로 결제할지 여부에 대한 안내 메시지를 출력한다.</p>
<pre><code class="language-js">현재 {상품명} {수량}개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)</code></pre>
</li>
<li><p>멤버십 할인 적용 여부를 확인하기 위해 안내 문구를 출력한다.</p>
<pre><code class="language-js">멤버십 할인을 받으시겠습니까? (Y/N)</code></pre>
</li>
<li><p>구매 상품 내역, 증정 상품 내역, 금액 정보를 출력한다.</p>
<pre><code class="language-js">===========W 편의점=============
상품명        수량    금액
콜라        3     3,000
에너지바         5     10,000
===========증    정=============
콜라        1
==============================
총구매액        8    13,000
행사할인            -1,000
멤버십할인            -3,000
내실돈             9,000</code></pre>
</li>
<li><p>추가 구매 여부를 확인하기 위해 안내 문구를 출력한다.</p>
<pre><code class="language-js">감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)</code></pre>
</li>
<li><p>사용자가 잘못된 값을 입력했을 때, &quot;[ERROR]&quot;로 시작하는 오류 메시지와 함께 상황에 맞는 안내를 출력한다.</p>
<ul>
<li>구매할 상품과 수량 형식이 올바르지 않은 경우: [ERROR] 올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요.</li>
<li>존재하지 않는 상품을 입력한 경우: [ERROR] 존재하지 않는 상품입니다. 다시 입력해 주세요.</li>
<li>구매 수량이 재고 수량을 초과한 경우: [ERROR] 재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요.</li>
<li>기타 잘못된 입력의 경우: [ERROR] 잘못된 입력입니다. 다시 입력해 주세요.</li>
</ul>
</li>
</ul>
<h4 id="실행-결과-예시">실행 결과 예시</h4>
<pre><code class="language-js">안녕하세요. W편의점입니다.
현재 보유하고 있는 상품입니다.

- 콜라 1,000원 10개 탄산2+1
- 콜라 1,000원 10개
- 사이다 1,000원 8개 탄산2+1
- 사이다 1,000원 7개
- 오렌지주스 1,800원 9개 MD추천상품
- 오렌지주스 1,800원 재고 없음
- 탄산수 1,200원 5개 탄산2+1
- 탄산수 1,200원 재고 없음
- 물 500원 10개
- 비타민워터 1,500원 6개
- 감자칩 1,500원 5개 반짝할인
- 감자칩 1,500원 5개
- 초코바 1,200원 5개 MD추천상품
- 초코바 1,200원 5개
- 에너지바 2,000원 5개
- 정식도시락 6,400원 8개
- 컵라면 1,700원 1개 MD추천상품
- 컵라면 1,700원 10개

구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])
[콜라-3],[에너지바-5]

멤버십 할인을 받으시겠습니까? (Y/N)
Y

===========W 편의점=============
상품명        수량    금액
콜라        3     3,000
에너지바         5     10,000
===========증    정=============
콜라        1
==============================
총구매액        8    13,000
행사할인            -1,000
멤버십할인            -3,000
내실돈             9,000

감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)
Y

안녕하세요. W편의점입니다.
현재 보유하고 있는 상품입니다.

- 콜라 1,000원 7개 탄산2+1
- 콜라 1,000원 10개
- 사이다 1,000원 8개 탄산2+1
- 사이다 1,000원 7개
- 오렌지주스 1,800원 9개 MD추천상품
- 오렌지주스 1,800원 재고 없음
- 탄산수 1,200원 5개 탄산2+1
- 탄산수 1,200원 재고 없음
- 물 500원 10개
- 비타민워터 1,500원 6개
- 감자칩 1,500원 5개 반짝할인
- 감자칩 1,500원 5개
- 초코바 1,200원 5개 MD추천상품
- 초코바 1,200원 5개
- 에너지바 2,000원 재고 없음
- 정식도시락 6,400원 8개
- 컵라면 1,700원 1개 MD추천상품
- 컵라면 1,700원 10개

구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])
[콜라-10]

현재 콜라 4개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)
Y

멤버십 할인을 받으시겠습니까? (Y/N)
N

===========W 편의점=============
상품명        수량    금액
콜라        10     10,000
===========증    정=============
콜라        2
==============================
총구매액        10    10,000
행사할인            -2,000
멤버십할인            -0
내실돈             8,000

감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)
Y

안녕하세요. W편의점입니다.
현재 보유하고 있는 상품입니다.

- 콜라 1,000원 재고 없음 탄산2+1
- 콜라 1,000원 7개
- 사이다 1,000원 8개 탄산2+1
- 사이다 1,000원 7개
- 오렌지주스 1,800원 9개 MD추천상품
- 오렌지주스 1,800원 재고 없음
- 탄산수 1,200원 5개 탄산2+1
- 탄산수 1,200원 재고 없음
- 물 500원 10개
- 비타민워터 1,500원 6개
- 감자칩 1,500원 5개 반짝할인
- 감자칩 1,500원 5개
- 초코바 1,200원 5개 MD추천상품
- 초코바 1,200원 5개
- 에너지바 2,000원 재고 없음
- 정식도시락 6,400원 8개
- 컵라면 1,700원 1개 MD추천상품
- 컵라면 1,700원 10개

구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])
[오렌지주스-1]

현재 오렌지주스은(는) 1개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)
Y

멤버십 할인을 받으시겠습니까? (Y/N)
Y

===========W 편의점=============
상품명        수량    금액
오렌지주스        2     3,600
===========증    정=============
오렌지주스        1
==============================
총구매액        2    3,600
행사할인            -1,800
멤버십할인            -0
내실돈             1,800

감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)
N</code></pre>
<hr>
<blockquote>
<h3 id="🛠️-프로그래밍-요구-사항-1">🛠️ 프로그래밍 요구 사항 1</h3>
</blockquote>
<ul>
<li>Node.js 20.17.0 버전에서 실행 가능해야 한다.</li>
<li>프로그램 실행의 시작점은 App.js의 run()이다.</li>
<li>package.json 파일은 변경할 수 없으며, 제공된 라이브러리와 스타일 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.</li>
<li>프로그램 종료 시 process.exit()를 호출하지 않는다.</li>
<li>프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.</li>
<li>자바스크립트 코드 컨벤션을 지키면서 프로그래밍한다.</li>
<li>기본적으로 JavaScript Style Guide를 원칙으로 한다.</li>
</ul>
<hr>
<blockquote>
<h3 id="🛠️-프로그래밍-요구-사항-2">🛠️ 프로그래밍 요구 사항 2</h3>
</blockquote>
<ul>
<li><strong>indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.</strong><ul>
<li>예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.</li>
<li>힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.</li>
</ul>
</li>
<li><strong>3항 연산자를 쓰지 않는다.</strong></li>
<li><strong>함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.</strong></li>
<li><strong>Jest를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다.</strong><ul>
<li>테스트 도구 사용법이 익숙하지 않다면 아래 문서를 참고하여 학습한 후 테스트를 구현한다.</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="🛠️-프로그래밍-요구-사항-3">🛠️ 프로그래밍 요구 사항 3</h3>
</blockquote>
<ul>
<li>함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.</li>
<li>함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.</li>
<li>else 예약어를 쓰지 않는다.
힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.</li>
<li>구현한 기능에 대한 단위 테스트를 작성한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다.
단위 테스트 작성이 익숙하지 않다면 LottoTest를 참고하여 학습한 후 테스트를 작성한다.</li>
</ul>
<hr>
<blockquote>
<h3 id="🛠️-프로그래밍-요구-사항-4">🛠️ 프로그래밍 요구 사항 4</h3>
</blockquote>
<ul>
<li>함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.</li>
<li>함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.</li>
<li>입출력을 담당하는 클래스를 별도로 구현한다.<ul>
<li>아래 InputView, OutputView 클래스를 참고하여 입출력 클래스를 구현한다.</li>
<li>클래스 이름, 메소드 반환 유형, 시그니처 등은 자유롭게 수정할 수 있다.<pre><code class="language-js">class InputView {
fun readItem(): String {
 println(&quot;구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])&quot;)
 val input = Console.readLine()
 // ...
}
// ...
}
class OutputView {
fun printProducts() {
 println(&quot;- 콜라 1,000원 10개 탄산2+1&quot;)
 // ...
}
// ...
}</code></pre>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="라이브러리">라이브러리</h3>
<ul>
<li><code>Missionutils</code>에서 제공하는 <code>DateTimes</code> 및 <code>Console</code> API를 사용하여 구현해야 한다.<ul>
<li>현재 날짜와 시간을 가져오려면 <code>DateTimes</code>의 <code>now()</code>를 활용한다.<br>

</li>
</ul>
</li>
</ul>
<hr>
<h1 id="👩🏻💻-2-과제-풀이">👩🏻‍💻 2. 과제 풀이</h1>
<h2 id="21-기능-목록">2.1. 기능 목록</h2>
<blockquote>
<h3 id="maincontroller-controllers">MainController (controllers/)</h3>
</blockquote>
<ul>
<li>✅ 현재 재고 출력</li>
<li>✅ 구매할 상품 장바구니 추가</li>
<li>✅ 프로모션 확인 및 처리</li>
<li>✅ 재고 차감</li>
<li>✅ 멤버십 할인 적용 여부 확인 후 영수증 출력</li>
<li>✅ 추가 구매 여부 확인<h3 id="inputview-views">InputView (views/)</h3>
</li>
<li>✅ 구매할 상품명과 수량 입력 받기</li>
<li>✅ 추가 구매 입력 선택 (Y/N) 받기</li>
<li>✅ 멤버십 할인 적용 여부 (Y/N) 받기</li>
<li>✅ 프로모션 무료 상품 추가 여부 (Y/N) 받기</li>
<li>✅ 프로모션 포기 여부 (Y/N) 받기<h3 id="outputview-views">OutputView (views/)</h3>
</li>
<li>✅ 현재 상품 목록과 프로모션 정보 출력</li>
<li>✅ 구매 내역, 할인 내역, 최종 결제 금액이 포함된 영수증 정렬 출력</li>
<li>✅ 오류 발생 시 적절한 메시지 출력<h3 id="productmanager-models">ProductManager (models/)</h3>
</li>
<li>✅ 초기 상품 목록 필드에 세팅</li>
<li>✅ 구매하려는 상품의 재고가 충분한지 확인</li>
<li>✅ 구매 후 재고 차감</li>
<li>✅ 현재 상품 목록 출력용 메세지 생성<h3 id="cart-models">Cart (models/)</h3>
</li>
<li>✅ 장바구니에 상품과 수량 추가하고 할인금액 누적</li>
<li>✅ 상품별 가격과 수량을 바탕으로 프로모션 적용 총 구매액 계산</li>
<li>✅ 멤버십 할인을 적용해 최종 금액 계산</li>
<li>✅ 프로모션 재고가 부족하여 일부 수량을 프로모션 혜택 없이 결제해야 하는 경우, 일부 수량에 대해 정가로 결제할지 여부에 대한 안내 메시지를 출력</li>
<li>✅ 프로모션 적용이 가능한 상품에 대해 고객이 해당 수량만큼 가져오지 않았을 경우, 입력 (Y/N)에 따라 추가증정<h3 id="promotionmanager-models">PromotionManager (models/)</h3>
</li>
<li>✅ N+1 프로모션(예: 1+1, 2+1) 혜택 적용</li>
<li>✅ 프로모션에 따른 할인 금액 계산<h3 id="membershipmanager-models">MembershipManager (models/)</h3>
</li>
<li>✅ 멤버십 회원 할인 적용 (최대 8,000원까지)<h3 id="loadproductdata-utils">loadProductData() (utils/)</h3>
</li>
<li>✅ 초기 상품 목록 객체 데이터로 가져오기<h3 id="loadpromotiondata-utils">loadPromotionData() (utils/)</h3>
</li>
<li>✅ 초기 프로모션 목록 객체 데이터로 가져오기</li>
</ul>
<h2 id="22-요구사항-체크리스트">2.2. 요구사항 체크리스트</h2>
<h3 id="프로그래밍-요구사항">프로그래밍 요구사항</h3>
<blockquote>
<h4 id="✅-nodejs-20170-버전에서-실행-가능">✅ Node.js 20.17.0 버전에서 실행 가능</h4>
</blockquote>
<h4 id="✅-프로그램-실행의-시작점">✅ 프로그램 실행의 시작점</h4>
<ul>
<li>App.js의 run()<h4 id="✅-packagejson-파일-변경-금지">✅ package.json 파일 변경 금지</h4>
</li>
<li>eslint 포맷팅 안잡히도록 gitignore 등록하고 개발 시작<h4 id="✅-프로그램-종료-시-processexit를-호출-금지">✅ 프로그램 종료 시 process.exit()를 호출 금지</h4>
<h4 id="✅-파일-패키지-등의-이름을-바꾸거나-이동-금지">✅ 파일, 패키지 등의 이름을 바꾸거나 이동 금지</h4>
<h4 id="✅-자바스크립트-코드-컨벤션-airbnb-스타일-가이드-준수">✅ 자바스크립트 코드 컨벤션 Airbnb 스타일 가이드 준수</h4>
<h4 id="들여쓰기">들여쓰기</h4>
</li>
<li>최대 depth는 2까지만 허용. (e.g., while문 안에 if문 포함)<h4 id="✅-삼항-연산자-사용-금지">✅ 삼항 연산자 사용 금지</h4>
<h4 id="함수-길이-10줄-이내">함수 길이 10줄 이내</h4>
</li>
<li>함수(또는 메서드) 길이는 최대 10줄을 넘지 않아야 함.<h4 id="else-사용을-지양하고-if-조건절에서-바로-return하는-방식으로-작성">else 사용을 지양하고, if 조건절에서 바로 return하는 방식으로 작성</h4>
<h4 id="함수는-한-가지-역할만-수행">함수는 한 가지 역할만 수행</h4>
</li>
<li>함수는 단일 책임 원칙에 따라 한 가지 역할만 수행해야 함.<h4 id="✅-ui-관련-로직을-제외한-모든-기능에-대해-단위-테스트-작성">✅ UI 관련 로직을 제외한 모든 기능에 대해 단위 테스트 작성</h4>
</li>
<li>Jest로 모든 기능의 테스트를 작성하고, 테스트가 통과되는지 확인.<h4 id="✅-woowacoursemission-utils-라이브러리-사용">✅ @woowacourse/mission-utils 라이브러리 사용</h4>
</li>
<li>Console 및 DateTimes API 활용:
Console.readLineAsync(): 사용자 입력을 비동기로 받을 때 사용.
Console.print(): 출력을 담당.
DateTimes.now(): 현재 날짜와 시간을 가져오는 데 사용.<h4 id="✅-입출력을-담당하는-클래스를-별도로-작성-inputview-outputview">✅ 입출력을 담당하는 클래스를 별도로 작성 (InputView, OutputView).</h4>
<h4 id="하드코딩-사용-지양">하드코딩 사용 지양</h4>
</li>
<li>문자열 같은 데이터를 하드코딩하지 말고 상수로 모아서 관리하기</li>
</ul>
<h2 id="23-예외사항">2.3. 예외사항</h2>
<blockquote>
<h3 id="🛠️-error-를-반환해야-하는-경우">🛠️ [ERROR] 를 반환해야 하는 경우</h3>
</blockquote>
<h4 id="공통">공통</h4>
<p>📍 ✅ 입력값이 비었을 경우 (trim() 적용 후)<br>
📍 ✅ 형식이 잘못된 경우 (예: [사이다-3] 대신 [사이다3] 입력)<br>
📍 ✅ Y/N Input은 Y, N 외의 다른 값을 입력했을 경우<br>
📍 ✅ 숫자가 아닌 값을 입력했을 경우<br>
📍 음수나 0과 같은 자연수(양의 정수)가 아닌 수를 입력했을 경우<br></p>
<h4 id="상품명과-수량-입력">상품명과 수량 입력</h4>
<p>📍 상품명과 수량 형식이 올바르지 않을 경우 (예: [사이다-3] 형식을 사용하지 않았을 때)<br>
📍 상품명이 존재하지 않는 경우 (재고에 없는 상품 입력)<br>
📍 재고보다 많은 수량을 입력했을 경우<br>
📍 수량에 숫자가 아닌 값을 입력했을 경우<br>
📍 수량이 음수이거나 0일 경우<br></p>
<h4 id="프로모션-적용-기간-확인">프로모션 적용 기간 확인</h4>
<p>📍 유효하지 않은 프로모션 상품을 입력했을 경우 (예: 프로모션 기간이 지난 상품)<br></p>
<h3 id="유효한-입력">유효한 입력</h3>
<h4 id="공통-1">공통</h4>
<p>📍 Y/N 입력에서 y/n 소문자로 입력했을 경우<br></p>
<h2 id="24-프로모션-n1-로직-정리">2.4. 프로모션 N+1 로직 정리</h2>
<h3 id="241-프로모션-기간-및-재고-확인">2.4.1. 프로모션 기간 및 재고 확인</h3>
<blockquote>
<ul>
<li>프로모션 정보가 존재하고 프로모션 기간 내일 때 프로모션 조건을 적용</li>
</ul>
</blockquote>
<ul>
<li>상품의 프로모션 재고가 존재하는지 확인</li>
</ul>
<h3 id="242-11-프로모션-처리">2.4.2. 1+1 프로모션 처리</h3>
<blockquote>
<p>buy와 get이 각각 1인 경우</p>
</blockquote>
<h4 id="최대-증정-가능한-수량maxbonusquantity과-실제-가능한-증정-수량actualbonusquantity으로-충분한-프로모션-재고-여부-처리">최대 증정 가능한 수량(maxBonusQuantity)과 실제 가능한 증정 수량(actualBonusQuantity)으로 충분한 프로모션 재고 여부 처리:</h4>
<ul>
<li><h4 id="프로모션-재고가-부족한-경우-actualbonusquantity--maxbonusquantity">프로모션 재고가 부족한 경우 (actualBonusQuantity &lt; maxBonusQuantity)</h4>
추가 구매 수량을 정가로 결제할지 사용자에게 확인
정가 결제를 원하지 않으면 추가 구매 수량을 제거
증정 가능한 수량과 할인 금액을 계산하여 최종 결제 처리</li>
<li><h4 id="프로모션-재고가-충분한-경우-actualbonusquantity--maxbonusquantity">프로모션 재고가 충분한 경우 (actualBonusQuantity === maxBonusQuantity)</h4>
구매 수량이 짝수일 경우:
증정 수량을 구매 수량의 절반으로 설정하고, 할인 금액을 계산
구매 수량이 홀수일 경우:
추가 증정 여부를 사용자에게 묻기
추가 증정에 동의한 경우 구매 수량을 1 증가시키고 증정 수량을 전체 가능 수량으로 설정
추가 증정에 동의하지 않으면 1개의 상품만 구매 처리되며 할인 금액을 계산</li>
</ul>
<h3 id="243-n1-프로모션-처리">2.4.3. N+1 프로모션 처리</h3>
<blockquote>
<p>buy와 get이 다른 값인 경우</p>
</blockquote>
<h3 id="최대-증정-가능한-수량maxbonusquantity과-실제-가능한-증정-수량actualbonusquantity으로-충분한-프로모션-재고-여부-처리-1">최대 증정 가능한 수량(maxBonusQuantity)과 실제 가능한 증정 수량(actualBonusQuantity)으로 충분한 프로모션 재고 여부 처리:</h3>
<ul>
<li><h4 id="프로모션-재고가-부족한-경우-actualbonusquantity--maxbonusquantity-1">프로모션 재고가 부족한 경우 (actualBonusQuantity &lt; maxBonusQuantity)</h4>
정가 결제를 할지 사용자에게 확인하고, 정가 결제를 원하지 않으면 추가 구매 수량을 제외
정가 결제 시, 추가 구매 수량을 포함하여 결제를 진행
증정 가능한 수량과 할인 금액을 최종 결제 금액에 반영</li>
<li><h4 id="프로모션-재고가-충분한-경우-actualbonusquantity--maxbonusquantity-1">프로모션 재고가 충분한 경우 (actualBonusQuantity &gt;= maxBonusQuantity)</h4>
구매 수량이 프로모션 조건(buy)을 정확히 맞추지 않을 때는 프로모션 증정 여부를 사용자에게 확인
추가 증정 여부에 따라 구매 수량을 증가시키거나 증정 수량과 할인 금액을 최종 결제에 반영</li>
</ul>
<h3 id="244-결제-처리">2.4.4. 결제 처리</h3>
<blockquote>
<ul>
<li>총 구매 수량과 증정 수량을 합산하여 결제에 반영</li>
</ul>
</blockquote>
<ul>
<li>할인 금액을 적용하고, 최종 결제 금액을 계산하여 장바구니에 추가</li>
</ul>
<hr>
<h1 id="💭-3-개발-회고">💭 3. 개발 회고</h1>
<h2 id="31-제출할-때-작성해야했던-문답-회고-백업">3.1. 제출할 때 작성해야했던 문답 회고 백업</h2>
<p>안녕하세요! 4주차까지 과제를 마치게 되어 매우 기쁩니다!😄
...사실 뿌듯한 한 편 많이 아쉽기도 합니다. 왜냐하면 마지막 과제를 진행하면서 많은 문제와 부딪히고 벽을 느끼면서도 밤을 새 최선을 다하여 구현해보았지만 결국 프로그래밍 요구사항을 준수하지 못한 채 제출하게 되었기 때문입니다.</p>
<p>4주동안 프리코스 과제를 진행하면서 기대보다도 훨씬 더 많은 것을 배웠습니다. 다양한 문제에 부딪히며 스스로의 부족함을 크게 느끼기도 하였지만 이를 끈기있게 극복해나가는 과정에서 희열을 느끼기도 했습니다.</p>
<p>4주차 프리코스 과제는 독학으로 취미개발을 하던 저의 실력에는 상당히 어려운 과제였습니다.
나름 3주차까지는 MVC패턴, 단일 책임 원칙, 프로그래밍 요구사항, 객체지향프로그래밍 등등의 규칙들과 피드백 사항을 지키며 개발하려고 노력할 수 있었지만 이번 마지막 과제는 기능만 구현하기에도 굉장히 어려웠고, 결국 프로그래밍 요구사항까지 어겨가며 스파게티 코드로 기능만을 겨우 완성할 수 있었습니다.
그 결과, 말그대로 돌아가는 쓰레기를 만들어내게 되었습니다...🥹 너무도 아쉬움과 미련이 많이 남는 마지막 과제였지만 그만큼 많은 것을 배울 수 있었던 과제였기 때문에 느낀 점도 많습니다. 이를 돌아보며 회고를 작성해보려고 합니다.</p>
<ol>
<li>지원서나 중간 회고에서 현실적인 목표를 설정하고 이를 달성했다고 생각하나요? 그 이유는 무엇인가요?
네! 저는 사실 지원할 때부터 거창하거나 구체적인 목표가 있었다기보단, 개발 공부의 기회가 없었던 저에게 좋은 공부가 될 수 있을 것 같아 참여한 프리코스였습니다. 하지만 막상 프리코스를 시작하고 사람들과 피드백을 주고받고 개발자 커뮤니티를 체험해보면서 점점 더 열정이 생겨 지난 한달 간 최선을 다해 공부하고 코딩을 했던 것 같습니다. 그 과정에서 생각조차 못했을만큼 프로그래밍적인 배움이나 고민을 많이 할 수 있었고 무엇보다 독학의 길에서 항상 이렇게 공부하는 게 맞는지 회의가 들었던 저에게 하나의 이정표가 되어준 좋은 경험이었습니다. </li>
</ol>
<p>물론, 마지막 과제를 완전히 준수하지 못한 아쉬움은 남아 있지만, 한 단계씩 기능을 구현하고 피드백을 반영하며 기초를 단단히 하는 과정에서 제 실력이 눈에 띄게 향상되는 것을 느낄 수 있었습니다. 특히, MVC 패턴, 단일 책임 원칙 같은 개념들을 이해하고, 직접 코드에 반영하면서도 한층 성장할 수 있었고, 더 나아가 생소한 객체지향적 사고를 적용하려는 노력이 스스로 매우 뿌듯했습니다.</p>
<ol start="2">
<li>중간 회고에서 조정한 목표가 실제 목표 달성에 도움이 되었나요? 목표를 달성하는 데 어떤 점이 효과적이었다고 생각하나요?
단순한 마음으로 시작했던 지원서 목표와 달리 중간 회고에서 조정한 목표는 “효율적이고 클린한 코드를 작성하고 소통하는 프론트엔드 개발자가 되겠다”입니다. 특히 TDD를 적극적으로 도입하며 테스트 주도 개발을 실천해 보고, 캡슐화를 유지하면서 각 객체가 할 일을 독립적으로 수행하도록 노력해보는 것 역시 목표의 일환이었습니다.</li>
</ol>
<p>하지만 결과적으로 이 중간 목표는 아직 이루지는 못한 것 같습니다. 오히려 어렵고 복잡한 기능을 구현하면서 기능 구현에 급급해 요구사항을 어겨가며 스파게티 코드를 만들어냈기 때문에 이 때의 목표를 돌아보며 반성도 됩니다.</p>
<p>하지만 비록 실제로는 돌아가는 쓰레기에 걸맞는 코드를 제출하게 됐지만, 중간회고의 목표를 저렇게 설정했던 것은 바람직했던 것 같습니다! 중간 목표에 다가가기 위해 TDD로 코드를 작성하는 도전을 해보았는데, 디버깅과 테스트가 훨씬 수월해지고 코드의 안정성이 높아지는 경험을 할 수 있었습니다. 테스트 코드 작성이 완전히 처음이라, 처음엔 어려웠지만, TDD 개발 방식을 습관처럼 따라가면서 객체의 역할과 책임을 고민하게 되었고, 전체적인 코드의 품질이 향상되는 것을 경험할 수 있었습니다.</p>
<ol start="3">
<li><p>각 미션의 목표를 달성하기 위해 세운 계획을 잘 이행했나요? 그 과정에서 어떤 전략이 효과가 있었나요?
미션을 수행하며 세운 계획은, 각 기능을 단계적으로 구현하고 주어진 요구사항을 최대한 유지하며 개발하는 것이었습니다. 이를 위해 TDD 방식을 도입해 테스트 단위부터 각 기능을 세밀하게 검증하는 전략을 택했습니다. 이렇게 코드를 짜다 보니 자연스럽게 디버깅 시간이 줄어들고, 오류가 발생하더라도 빠르게 찾아낼 수 있었습니다. 특히 프로모션 증정 기능을 구현할 때, 다양한 예외 상황을 고려해야 했는데, 이때 if문이 꼬리에 꼬리를 물며 늘어나는 형태로 작성되어 프로그래밍 요구사항을 위반하게 되었습니다... 비록 최종 코드는 스파게티처럼 꼬여버렸지만, 이 경험 덕분에 알고리즘과 코딩 로직에 대한 더 깊은 고민을 하게 되었습니다. 개발자들이 알고리즘을 공부해야하는 이유를 실감하게 된 순간이기도 했습니다.</p>
</li>
<li><p>몰입하고 함께 성장하는 과정을 통해 인상 깊었던 경험이나 변화가 있었나요?
마지막 과제에서 어려움이 컸지만, 그만큼 새로운 점을 배운 것들이 많아 뿌듯합니다. 특히 TDD 방식으로 개발할 때 얻는 안정감은 큰 변화였고, 코드 작성 전에 검증하는 습관이 디버깅에도 큰 도움이 되었습니다. 또한 평소에는 생각지 못했던 터미널 출력에서의 정렬 기능을 활용하기 위해 padStart() 같은 메소드를 찾으면서 터미널에서도 정렬을 유지하는 방법을 알게 된 점이 흥미로웠습니다. 예전에는 단순히 기능 구현에 집중했다면, 이제는 코드의 정돈된 출력을 고민하게 되었고, 이를 통해 조금씩 더 견고하고 가독성 좋은 코드 작성으로 나아가는 변화가 있었던 것 같습니다.</p>
</li>
</ol>
<p>한 편, 인상깊으면서도 슬펐던 경험은 객체지향적 사고와 TDD 개발에 익숙치 않은 상태에서 무턱대고 테스트코드를 작성하며 각 객체의 독립적인 작은 기능부터 구현하다보니 기능 하나하나는 굉장히 빨리 만들어졌는데, 이를 서로 유기적으로 동작하게 하는 부분에서 예상치 못하게 너무 많은 시간을 소모했던 것입니다.</p>
<p>각 객체의 기능은 독립적으로 완벽하게 작동했고 그래서 각 객체가 모두 완성되었을 때 모든 테스트코드를 합격하는 모습을 보며 이제 서로 협력시켜 돌아가게하는 것 쯤이야 금방 할 수 있을 줄 알았습니다.</p>
<p>하지만 객체의 캡슐화를 깨지 않도록 프라이빗 필드의 정보를 밖으로 꺼내는 getter 사용을 지양하려고하면서부터 부딪힌 수많은 문제들이 단순 기능 구현에 시간을 많이 할애하게 하였습니다...</p>
<p>결국 점점 나름대로 깔끔하게 작성 중이었던 객체 내부 메소드들도 하나씩 스파게티가 되어가기 시작했고 그럼에도 막상 객체끼리 협력을 시키려니 끊임없이 튀어나오는 크고작은 버그들과 사투하게 되었습니다.</p>
<p>getter의 개념도 프리코스를 시작하고 객체에 대해 공부하며 처음 알았던 저는, 방금 배운 getter를 쓰지말라는 피드백에 많이 절망스러웠습니다 ㅠ_ㅠ
하지만 왜 getter 나 setter를 쓰면 안되는지, 객체끼리 데이터를 꺼내(get)지 말고 &quot;메세지&quot;를 던지라는 의미는 무엇인지 피드백을 꼼꼼히 읽고 참고 자료들을 찾아보면서 객체의 캡슐화에 대해 깊게 고민하고 공부할 수 있었습니다.</p>
<p>이 또한 혼자 공부할 땐 알지도, 고민해보지도 못했던 핵심적인 개념이라 어렵고 이해가 안돼서 식은 땀을 흘리면서도 즐겁고 설레서 두근거리는 값진 경험을 할 수 있었습니다😊</p>
<p>마지막 과제에서 그동안 잘 지켜왔던 프로그래밍 요구사항 마저도 지키지 못했다니 끝까지 많은 아쉬움이 남지만,
그 과정에서 기대하고 목표했던 바보다 정말 많이 배웠고, 더 큰 성장을 향해 나아갈 수 있는 이정표를 본 느낌이라 너무 즐거웠습니다! 더 배울 기회가 있었으면 좋겠습니다...!</p>
<p>감사합니다!</p>
<h2 id="32-개인적으로-쓰는-개발-회고">3.2. 개인적으로 쓰는 개발 회고</h2>
<p>(작성 중 . . .)</p>
<hr>
<h1 id="📝-4-배운-내용-정리">📝 4. 배운 내용 정리</h1>
<p>(작성 중 . . .)</p>
<hr>
<h1 id="🤔-5-고칠-점">🤔 5. 고칠 점</h1>
<h2 id="51-우테코-공통-피드백-메모">5.1. 우테코 공통 피드백 메모</h2>
<ul>
<li><p><strong>함수(메서드) 라인에 대한 기준도 적용한다</strong></p>
<ul>
<li>프로그래밍 요구사항에는 함수의 길이를 15라인으로 제한하는 규칙이 포함되어 있다. 이 규칙은 main() 함수도 동일하게 적용되며, 공백 라인도 한 라인으로 간주한다. 만약 함수가 15라인을 초과한다면, 역할을 더 명확하게 나누고, 코드의 가독성과 유지보수성을 높일 수 있는 신호로 인식하고 함수 분리 또는 클래스 분리를 고려해야 한다.</li>
</ul>
</li>
<li><p><strong>예외 상황에 대한 고민한다</strong></p>
<ul>
<li>정상적인 상황을 구현하는 것보다 예외 상황을 모두 고려하여 프로그래밍하는 것이 훨씬 어렵다. 하지만, 이러한 예외 상황을 처리하는 습관을 들이는 것이 중요하다. 코드를 작성할 때는 예상되는 예외를 미리 고려하여, 프로그램이 비정상적으로 종료되거나 잘못된 결과를 내지 않도록 한다.
예를 들어, 로또 미션에서 고려할 수 있는 예외 상황은 다음과 같다.<blockquote>
<p>로또 구입 금액에 1000 이하의 숫자를 입력
당첨 번호에 중복된 숫자를 입력
당첨 번호에 1~45 범위를 벗어나는 숫자를 입력
당첨 번호와 중복된 보너스 번호를 입력</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>비즈니스 로직과 UI 로직의 분리한다</strong></p>
<ul>
<li><p>비즈니스 로직과 UI 로직을 한 클래스에서 처리하는 것은 단일 책임 원칙(SRP)에 위배된다. 비즈니스 로직은 데이터 처리 및 도메인 규칙을 담당하고, UI 로직은 화면에 데이터를 표시하거나 입력을 받는 역할로 분리한다. 아래는 비즈니스 로직과 UI 로직이 혼재되어 있다. 비즈니스 로직은 그대로 유지하고, UI 관련 코드는 별도 View 클래스로 분리하는 것이 좋다. 현재 객체의 상태를 보기 위한 로그 메시지 성격이 강하다면, toString() 메서드를 통해 상태를 표현한다. 만약 UI에서 사용할 데이터가 필요하다면 getter 메서드를 통해 View 계층으로 데이터를 전달한다.</p>
<pre><code class="language-js">class Lotto {
#numbers

// 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
contains(numbers) {
 ...
}

// UI 로직
print() {
 ...
}      
}</code></pre>
</li>
</ul>
</li>
<li><p><strong>객체의 상태 접근을 제한한다</strong></p>
<ul>
<li>필드는 private class 필드로 구현한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">class WinningLotto {
   #lotto
   #bonusNumber</code></pre>
<ul>
<li><p><strong>객체는 객체답게 사용한다</strong></p>
<ul>
<li>Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다. 이처럼 Lotto 객체에서 데이터를 꺼내(get) 사용하기보다는, <strong>데이터가 가지고 있는 객체가 스스로 처리할 수 있도록 구조를 변경</strong>해야 한다. 아래와 같이 데이터를 외부에서 가져와(get) 처리하지 말고, 객체가 자신의 데이터를 스스로 처리하도록 메시지를 던지게 한다.
참고자료: <a href="https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/">getter를 사용하는 대신 객체에 메시지를 보내자</a></li>
</ul>
</li>
<li><p><strong>필드(인스턴스 변수)의 수를 줄이기 위해 노력한다</strong></p>
<ul>
<li>필드의 수가 많아지면 객체의 복잡도가 증가하고, 관리가 어려워지며, 버그가 발생할 가능성도 높아진다. 따라서 필드에 중복이 있거나 불필요한 필드가 없는지 확인하고, 이를 최소화한다.</li>
</ul>
</li>
<li><p><strong>성공하는 케이스 뿐만 아니라 예외 케이스도 테스트한다</strong></p>
</li>
<li><p><strong>테스트 코드도 코드다</strong></p>
<ul>
<li>테스트 코드 역시 코드의 일환이므로, 리팩터링을 통해 지속적으로 개선해 나가는 것이 중요하다. 특히, 반복적으로 수행하는 부분이 있다면 중복을 제거하여 유지보수성을 높이고 가독성을 향상시켜야 한다. 단순히 파라미터 값만 바뀌는 경우라면, 파라미터화된 테스트를 통해 중복을 줄일 수 있다.</li>
</ul>
</li>
<li><p><strong>단위 테스트하기 어려운 코드를 단위 테스트하기</strong></p>
</li>
</ul>
<h2 id="52-스터디-피드백-메모">5.2. 스터디 피드백 메모</h2>
<ul>
<li>사용자가 올바르지 않은 값을 입력 시 재입력을 받는 부분에서 재귀 호출로 인해 비동기 처리가 연속적으로 이루어지기 때문에 await를 써서 리턴해주는 것이 안전할 것 같다<pre><code class="language-js">class InputController {
static async getValidPurchaseAmount() {
  try {
    const input = await InputView.inputPurchaseAmount();
    const purchaseAmount = Parser.parseNumber(input);
    LottoValidator.validatePurchaseAmount(purchaseAmount);
    return purchaseAmount;
  } catch (error) {
    Console.print(error.message);
    return InputController.getValidPurchaseAmount(); // 이 부분을 await 으로</code></pre>
</li>
</ul>
<hr>
<h1 id="5-소감">5. 소감</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[[3주차 프리코스] 로또 개발 회고 - 작성 중...]]></title>
            <link>https://velog.io/@dev-dino22/3%EC%A3%BC%EC%B0%A8-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-%EB%A1%9C%EB%98%90-%EA%B0%9C%EB%B0%9C-%ED%9A%8C%EA%B3%A0-%EC%9E%91%EC%84%B1-%EC%A4%91</link>
            <guid>https://velog.io/@dev-dino22/3%EC%A3%BC%EC%B0%A8-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-%EB%A1%9C%EB%98%90-%EA%B0%9C%EB%B0%9C-%ED%9A%8C%EA%B3%A0-%EC%9E%91%EC%84%B1-%EC%A4%91</guid>
            <pubDate>Thu, 28 Nov 2024 13:02:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev-dino22/post/bcac4ef7-ce12-4b3f-8772-1ce667a57bd7/image.png" alt=""></p>
<blockquote>
<p>나의 3주차 과제 PR 링크 : <a href="https://github.com/woowacourse-precourse/javascript-lotto-7/pull/14">https://github.com/woowacourse-precourse/javascript-lotto-7/pull/14</a></p>
</blockquote>
<p>4주차 과제를 제출한지 한참이 지나서야 작성하는 3주차 회고... 3주차와 4주차 과제는 기능을 구현하고 코드를 다듬기에도 너무 바빴어서 계획과 달리 회고를 제 때 작성하지 못했었다. 그리고 4주차가 끝난 뒤에는 프리코스를 하느라 밀렸던 일을 처리하느라 바빴고...</p>
<h1 id="📢-1-프리코스-3주차-과제-요구사항">📢 1. 프리코스 3주차 과제 요구사항</h1>
<p><a href="https://github.com/woowacourse-precourse/javascript-lotto-7">https://github.com/woowacourse-precourse/javascript-lotto-7</a>
3주차 프리코스 과제의 요구사항은 다음과 같았다.</p>
<blockquote>
<h2 id="로또">로또</h2>
</blockquote>
<hr>
<blockquote>
<h3 id="📌-과제-진행-요구-사항">📌 과제 진행 요구 사항</h3>
</blockquote>
<ul>
<li>미션은 문자열 덧셈 계산기 저장소를 포크하고 클론하는 것으로 시작한다.</li>
<li>기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다.</li>
<li>Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다.</li>
<li>AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다.</li>
<li>자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다.</li>
</ul>
<hr>
<blockquote>
<h3 id="🔨-기능-요구-사항">🔨 기능 요구 사항</h3>
<p>간단한 로또 발매기를 구현한다.</p>
</blockquote>
<ul>
<li>로또 번호의 숫자 범위는 1~45까지이다.</li>
<li>1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.</li>
<li>당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.</li>
<li>당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
1등: 6개 번호 일치 / 2,000,000,000원
2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
3등: 5개 번호 일치 / 1,500,000원
4등: 4개 번호 일치 / 50,000원
5등: 3개 번호 일치 / 5,000원</li>
<li>로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.</li>
<li>로또 1장의 가격은 1,000원이다.</li>
<li>당첨 번호와 보너스 번호를 입력받는다.</li>
<li>사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.</li>
<li>사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시키고, &quot;[ERROR]&quot;로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. Exception이 아닌 IllegalArgumentException, IllegalStateException 등과 같은 명확한 유형을 처리한다.</li>
</ul>
<hr>
<blockquote>
<h3 id="🛠️-프로그래밍-요구-사항-1">🛠️ 프로그래밍 요구 사항 1</h3>
</blockquote>
<ul>
<li>Node.js 20.17.0 버전에서 실행 가능해야 한다.</li>
<li>프로그램 실행의 시작점은 App.js의 run()이다.</li>
<li>package.json 파일은 변경할 수 없으며, 제공된 라이브러리와 스타일 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.</li>
<li>프로그램 종료 시 process.exit()를 호출하지 않는다.</li>
<li>프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.</li>
<li>자바스크립트 코드 컨벤션을 지키면서 프로그래밍한다.</li>
<li>기본적으로 JavaScript Style Guide를 원칙으로 한다.</li>
</ul>
<hr>
<blockquote>
<h3 id="🛠️-프로그래밍-요구-사항-2">🛠️ 프로그래밍 요구 사항 2</h3>
</blockquote>
<ul>
<li><strong>indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.</strong><ul>
<li>예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.</li>
<li>힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.</li>
</ul>
</li>
<li><strong>3항 연산자를 쓰지 않는다.</strong></li>
<li><strong>함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.</strong></li>
<li><strong>Jest를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다.</strong><ul>
<li>테스트 도구 사용법이 익숙하지 않다면 아래 문서를 참고하여 학습한 후 테스트를 구현한다.</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="🛠️-프로그래밍-요구-사항-3">🛠️ 프로그래밍 요구 사항 3</h3>
</blockquote>
<ul>
<li>함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.</li>
<li>함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.</li>
<li>else 예약어를 쓰지 않는다.
힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.</li>
<li>구현한 기능에 대한 단위 테스트를 작성한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다.
단위 테스트 작성이 익숙하지 않다면 LottoTest를 참고하여 학습한 후 테스트를 작성한다.<br>

</li>
</ul>
<hr>
<br>

<h1 id="👩🏻💻-2-과제-풀이">👩🏻‍💻 2. 과제 풀이</h1>
<h2 id="21-프로젝트-목표">2.1. 프로젝트 목표</h2>
<ul>
<li>객체 지향 프로그래밍(OOP): 관련된 기능을 클래스로 나누어 객체 간의 협력으로 로또 발매기를 구현한다.</li>
<li>단위 테스트 작성: 기능별로 단위 테스트를 작성해 코드가 의도한 대로 작동하는지 검증한다.</li>
<li>프로그래밍 요구 사항 준수: 제한된 인덴트와 함수 길이를 준수하며, 간결하고 가독성 좋은 코드를 작성한다.</li>
</ul>
<h2 id="22-프로젝트-소개">2.2. 프로젝트 소개</h2>
<p>이 프로젝트는 간단한 로또 발매기로서, 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 결과와 수익률을 계산하고, 이를 출력하는 프로그램이다.</p>
<h2 id="23-기능-목록">2.3. 기능 목록</h2>
<ol>
<li>구입 금액, 당첨 번호, 보너스 번호 입력 기능</li>
<li>로또 구매 개수 계산 및 구매 개수 출력 기능</li>
<li>로또 번호 생성 및 저장 후 출력 기능</li>
<li>로또 구매 개수만큼 로또 발행 기능</li>
<li>로또 번호와 당첨 번호를 비교해 당첨 내역 출력 기능</li>
<li>총 수익률 계산 후 수익률 출력 기능</li>
<li>잘못된 값을 입력한 경우 에러를 반환하고 다시 입력받기 기능<br>


</li>
</ol>
<h2 id="24-개발-중-체크리스트">2.4. 개발 중 체크리스트</h2>
<h3 id="241-클래스-및-함수-구조">2.4.1. 클래스 및 함수 구조</h3>
<ul>
<li>✅ 관련된 함수들을 하나의 클래스로 묶어서 작성했는가?</li>
<li>✅ 클래스는 필드, 생성자, 메서드 순서로 작성했는가?</li>
<li>✅ 한 클래스 또는 함수가 한 가지 기능만 수행하도록 분리했는가?</li>
<li>✅ 중복되는 코드가 있으면 별도의 함수로 분리했는가?</li>
<li>✅ 함수 길이 15줄 이하 규칙을 준수했는가?</li>
<li>✅ else문 대신 조건절에서 값을 반환하는 방식으로 설계했는가?</li>
</ul>
<h3 id="242-요구-사항-검토">2.4.2. 요구 사항 검토</h3>
<ul>
<li>✅ 구입 금액, 당첨 번호, 보너스 번호 입력과 예외 처리를 구현했는가?</li>
<li>✅ 로또 번호를 1~45 범위에서 중복 없이 생성하는지 확인했는가?</li>
<li>✅ 오름차순 정렬된 로또 번호를 출력하는 기능을 구현했는가?</li>
<li>✅ 당첨 결과를 1등~5등까지 분류하여 계산하는 로직을 정확히 구현했는가?</li>
<li>✅ 총 수익률 계산과 출력 형식(소수점 둘째 자리 반올림)을 준수했는가?</li>
</ul>
<h3 id="243-입출력-및-에러-처리">2.4.3. 입출력 및 에러 처리</h3>
<ul>
<li>✅ 사용자의 입력값이 유효하지 않을 경우 &quot;[ERROR]&quot;로 시작하는 메시지를 출력하고, 다시 입력을 받도록 했는가?</li>
<li>✅ 예외 상황별로 에러 메시지를 출력하도록 구현했는가?</li>
<li>✅ 출력 형식을 요구 사항에 맞게 작성했는가?</li>
</ul>
<h3 id="244-테스트-코드-작성">2.4.4. 테스트 코드 작성</h3>
<ul>
<li>✅ 단위 테스트를 각 기능 단위로 작성했는가?</li>
<li>✅ UI제외 로직 부분만 단위 테스트했는가?</li>
<li>✅ 예외 상황에 대한 테스트 케이스를 충분히 작성했는가?</li>
<li>✅ 작은 단위의 테스트부터 작성하여 핵심 기능을 검증했는가?</li>
</ul>
<h3 id="245-코드-스타일-및-컨벤션">2.4.5. 코드 스타일 및 컨벤션</h3>
<ul>
<li>✅ Node.js 20.17.0 버전인지 확인했는가?</li>
<li>✅ const 상수를 정의하여 의미 있는 이름을 부여하고 하드 코딩된 값 사용을 지양했는가?</li>
<li>✅ 3항 연산자를 사용하지 않았는가?</li>
<li>✅ indent depth가 2를 초과하지 않았는가?</li>
</ul>
<h3 id="246-추가-피드백-사항">2.4.6. 추가 피드백 사항</h3>
<ul>
<li>✅ 기능 목록을 정기적으로 업데이트하여 최신 상태로 유지했는가?</li>
<li>✅ README.md 파일을 통해 프로젝트의 개요와 기능을 간결하게 설명했는가?</li>
<li>✅ 중복된 코드가 있다면 리팩토링하여 줄였는가?</li>
</ul>
<h2 id="25-테스트-케이스-작성">2.5. 테스트 케이스 작성</h2>
<h3 id="251-🛠️-error-를-반환해야하는-경우">2.5.1 🛠️ [ERROR] 를 반환해야하는 경우</h3>
<h4 id="공통">공통</h4>
<p>📍 입력값이 비었을 경우(trim() 적용 후)<br>
📍 숫자가 아닌 입력을 했을 경우<br>
📍 숫자 사이에 띄어쓰기를 했을 경우<br>
📍 자연수(양의 정수)가 아닌 경우<br></p>
<h4 id="구매-금액-입력">구매 금액 입력</h4>
<p>📍 입력값이 비었을 경우(trim() 적용 후)<br>
📍 1000의 배수가 아닌 경우<br>
📍 너무 많은 금액일 경우(10억 초과)<br></p>
<h4 id="당첨-번호-입력">당첨 번호 입력</h4>
<p>📍 입력한 숫자열이 6개가 아닌 경우<br>
📍 1~45까지의 자연수를 입력하지 않은 경우<br>
📍 입력한 숫자열에 중복이 있을 경우<br>
📍 구분자로 &#39;,&#39;이 아닌 다른 것을 쓴 경우<br></p>
<h4 id="보너스-번호-입력">보너스 번호 입력</h4>
<p>📍 1~45까지의 자연수를 입력하지 않은 경우<br>
📍 당첨 번호와 중복된 숫자를 입력했을 경우<br>
📍 숫자를 여러개 입력했을 경우<br></p>
<hr>
<h1 id="💭-3-개발-회고">💭 3. 개발 회고</h1>
<hr>
<h1 id="📝-4-배운-내용-정리">📝 4. 배운 내용 정리</h1>
<hr>
<h1 id="🤔-5-고칠-점">🤔 5. 고칠 점</h1>
<h2 id="51-우테코-공통-피드백-메모">5.1. 우테코 공통 피드백 메모</h2>
<ul>
<li><p><strong>함수(메서드) 라인에 대한 기준도 적용한다</strong></p>
<ul>
<li>프로그래밍 요구사항에는 함수의 길이를 15라인으로 제한하는 규칙이 포함되어 있다. 이 규칙은 main() 함수도 동일하게 적용되며, 공백 라인도 한 라인으로 간주한다. 만약 함수가 15라인을 초과한다면, 역할을 더 명확하게 나누고, 코드의 가독성과 유지보수성을 높일 수 있는 신호로 인식하고 함수 분리 또는 클래스 분리를 고려해야 한다.</li>
</ul>
</li>
<li><p><strong>예외 상황에 대한 고민한다</strong></p>
<ul>
<li>정상적인 상황을 구현하는 것보다 예외 상황을 모두 고려하여 프로그래밍하는 것이 훨씬 어렵다. 하지만, 이러한 예외 상황을 처리하는 습관을 들이는 것이 중요하다. 코드를 작성할 때는 예상되는 예외를 미리 고려하여, 프로그램이 비정상적으로 종료되거나 잘못된 결과를 내지 않도록 한다.
예를 들어, 로또 미션에서 고려할 수 있는 예외 상황은 다음과 같다.<blockquote>
<p>로또 구입 금액에 1000 이하의 숫자를 입력
당첨 번호에 중복된 숫자를 입력
당첨 번호에 1~45 범위를 벗어나는 숫자를 입력
당첨 번호와 중복된 보너스 번호를 입력</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>비즈니스 로직과 UI 로직의 분리한다</strong></p>
<ul>
<li><p>비즈니스 로직과 UI 로직을 한 클래스에서 처리하는 것은 단일 책임 원칙(SRP)에 위배된다. 비즈니스 로직은 데이터 처리 및 도메인 규칙을 담당하고, UI 로직은 화면에 데이터를 표시하거나 입력을 받는 역할로 분리한다. 아래는 비즈니스 로직과 UI 로직이 혼재되어 있다. 비즈니스 로직은 그대로 유지하고, UI 관련 코드는 별도 View 클래스로 분리하는 것이 좋다. 현재 객체의 상태를 보기 위한 로그 메시지 성격이 강하다면, toString() 메서드를 통해 상태를 표현한다. 만약 UI에서 사용할 데이터가 필요하다면 getter 메서드를 통해 View 계층으로 데이터를 전달한다.</p>
<pre><code class="language-js">class Lotto {
#numbers

// 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
contains(numbers) {
 ...
}

// UI 로직
print() {
 ...
}      
}</code></pre>
</li>
</ul>
</li>
<li><p><strong>객체의 상태 접근을 제한한다</strong></p>
<ul>
<li>필드는 private class 필드로 구현한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">class WinningLotto {
   #lotto
   #bonusNumber</code></pre>
<ul>
<li><p><strong>객체는 객체답게 사용한다</strong></p>
<ul>
<li>Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다. 이처럼 Lotto 객체에서 데이터를 꺼내(get) 사용하기보다는, <strong>데이터가 가지고 있는 객체가 스스로 처리할 수 있도록 구조를 변경</strong>해야 한다. 아래와 같이 데이터를 외부에서 가져와(get) 처리하지 말고, 객체가 자신의 데이터를 스스로 처리하도록 메시지를 던지게 한다.
참고자료: <a href="https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/">getter를 사용하는 대신 객체에 메시지를 보내자</a></li>
</ul>
</li>
<li><p><strong>필드(인스턴스 변수)의 수를 줄이기 위해 노력한다</strong></p>
<ul>
<li>필드의 수가 많아지면 객체의 복잡도가 증가하고, 관리가 어려워지며, 버그가 발생할 가능성도 높아진다. 따라서 필드에 중복이 있거나 불필요한 필드가 없는지 확인하고, 이를 최소화한다.</li>
</ul>
</li>
<li><p><strong>성공하는 케이스 뿐만 아니라 예외 케이스도 테스트한다</strong></p>
</li>
<li><p><strong>테스트 코드도 코드다</strong></p>
<ul>
<li>테스트 코드 역시 코드의 일환이므로, 리팩터링을 통해 지속적으로 개선해 나가는 것이 중요하다. 특히, 반복적으로 수행하는 부분이 있다면 중복을 제거하여 유지보수성을 높이고 가독성을 향상시켜야 한다. 단순히 파라미터 값만 바뀌는 경우라면, 파라미터화된 테스트를 통해 중복을 줄일 수 있다.</li>
</ul>
</li>
<li><p><strong>단위 테스트하기 어려운 코드를 단위 테스트하기</strong></p>
</li>
</ul>
<h2 id="52-스터디-피드백-메모">5.2. 스터디 피드백 메모</h2>
<ul>
<li>사용자가 올바르지 않은 값을 입력 시 재입력을 받는 부분에서 재귀 호출로 인해 비동기 처리가 연속적으로 이루어지기 때문에 await를 써서 리턴해주는 것이 안전할 것 같다<pre><code class="language-js">class InputController {
static async getValidPurchaseAmount() {
  try {
    const input = await InputView.inputPurchaseAmount();
    const purchaseAmount = Parser.parseNumber(input);
    LottoValidator.validatePurchaseAmount(purchaseAmount);
    return purchaseAmount;
  } catch (error) {
    Console.print(error.message);
    return InputController.getValidPurchaseAmount(); // 이 부분을 await 으로</code></pre>
</li>
</ul>
<hr>
<h1 id="5-소감">5. 소감</h1>
]]></description>
        </item>
    </channel>
</rss>