<?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>Sat, 30 May 2026 18:27:42 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[[당근/회고] 인턴 출근 8주차 회고]]></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-8%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</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-8%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 30 May 2026 18:27:42 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<h1 id="🌱-8주차-회고-20260531">🌱 8주차 회고 (2026.05.31)</h1>
<h2 id="돌아보기">돌아보기</h2>
<h3 id="담당-기능-마무리와-끈질긴-버그-추적">담당 기능 마무리와 끈질긴 버그 추적</h3>
<p>맡은 기능의 QA를 반영하며 마무리에 들어갔고, 진입 흐름을 상황에 따라 더 매끄럽게 정리했다. 한편 월요일에 제보받은 까다로운 버그는 화요일엔 재현조차 못 해 애를 먹다가 수요일에 결국 원인을 잡아 핫픽스를 배포했다. 다만 그 과정에서 마주한 구조적 복잡도가 여전히 근본적으로 해소되지 않았다는 것도 다시 확인했다. 이 부분은 숙제로 남겼다.</p>
<hr>
<h3 id="다음-작업-착수와-일정-산정">다음 작업 착수와 일정 산정</h3>
<p>다음 마일스톤 작업에도 착수했다. API 스펙을 미리 스케치해 가서 백엔드 엔지니어와 논의를 매끄럽게 확정했고, 미리 만들 수 있는 부분을 만들어뒀다. 남은 작업들의 일정을 역산해 정리하기도 했다. 논의에 필요한 걸 미리 준비해 간 게 이번 주엔 잘 맞아떨어졌다. 이전보다 좁은 범위의 작업이긴 했어도, 지난 번보다는 조금 익숙해졌다는 생각이 들었다.</p>
<hr>
<h3 id="주도적으로-제안하고-맥락을-넓힌-한-주">주도적으로 제안하고, 맥락을 넓힌 한 주</h3>
<p>담당 팀원 분이 자리를 비운 날 PM 님이 이벤트 로깅 검토를 내게 맡기셨는데 뭔가 엔지니어로서 신뢰받은 것 같아 꽤 뿌듯했다(누락된 로깅도 찾아 추가했다). 그리고 PM 님과의 원온원에선 회사부터 우리 팀까지 이어지는 조직의 맥락과 커뮤니케이션 피드백을 들을 수 있어 좋았다. </p>
<p>또 백엔드 엔지니어분에게 기능 전후의 성능을 비교할 모니터링 지표를 알면 좋겠다는 피드백을 받고, 프론트 성능 모니터링과 백엔드 모니터링 도구도 배우고 싶다는 생각이 들었다.</p>
<p>코드 품질을 지키기 위한 컨벤션 개선도 주도적으로 제안드렸다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 주에 주로 한 일은 맡은 기능의 QA 반영과 마무리, 까다로운 버그 핫픽스, 그리고 다음 작업 착수였다.</p>
<h3 id="잘한-점">잘한 점</h3>
<ul>
<li>진입 흐름을 상황에 맞게 매끄럽게 정리하고, 연동 방식을 백엔드와 합의해 정돈했다.</li>
<li>지난주부터 따라다니던 버그의 원인을 결국 규명하고 핫픽스를 배포했다.</li>
<li>다음 작업의 스펙을 미리 스케치해 가서 논의를 매끄럽게 확정하고, 일정까지 역산해 정리했다.</li>
<li>코드 품질 컨벤션 개선을 주도적으로 제안하고, 위임받은 로깅 검토에서 누락까지 찾아냈다.</li>
</ul>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ul>
<li>한 주 내내 컨디션이 좋지 않아 업무 효율이 잘 나지 않았다.</li>
<li>구조적 복잡도는 이번 주에도 손대지 못하고 부채로 쌓였다.</li>
<li>&quot;하루종일 한 게 없는 느낌&quot;이 드는 날이 있었다. 진척 체감이 안 되는 날의 패턴을 들여다볼 필요가 있다.</li>
</ul>
<h3 id="다음-주-개선-방향">다음 주 개선 방향</h3>
<ul>
<li>미뤄둔 구조적 복잡도를 별도 리팩토링 아젠다로 떼어내 한 번에 정리하기.</li>
<li>프론트 성능 모니터링 지표와 백엔드 모니터링 도구 보는 법을 학습하기.</li>
<li>컨디션과 업무 시간을 지킬 수 있게 야근에 기대지 않는 하루 단위 계획 세우기.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당근/회고] 인턴 출근 7주차 회고]]></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-7%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</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-7%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 30 May 2026 18:18:39 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<h1 id="🌱-7주차-회고-20260524">🌱 7주차 회고 (2026.05.24)</h1>
<h2 id="돌아보기">돌아보기</h2>
<h3 id="담당-기능-마무리와-디테일-채우기">담당 기능 마무리와 디테일 채우기</h3>
<p>맡은 기능의 남은 디테일을 채운 한 주였다. 가장 골치였던 건 외부 리소스를 다루는 과정에서 생긴 제약이었는데, 여러 우회책을 시도하다 결국 원인을 정확히 짚고 다른 접근으로 풀었다.</p>
<hr>
<h3 id="연쇄-트러블슈팅-주간">연쇄 트러블슈팅 주간</h3>
<p>솔직히 이번 주는 트러블슈팅으로 점철된 주였다. 가장 기억에 남는 건 플랫폼 동작 차이에서 비롯된 까다로운 버그였다. 내 도메인에서 시작된 이슈라 혼자 붙들고 있었으면 한참을 헤맸을 텐데, 한 팀원 분이 함께 파고들어 원인을 규명해주고 결국 해결까지 끌고 가주셨다. 시간을 많이 빌리게 돼 죄송한 마음이 컸지만, 끝까지 같이 봐주신 게 정말 감사했다. 덕분에 우리 도메인의 복잡도와 동작에 대해 더 이해하게 됐다.</p>
<p>그 밖에도 상태가 중복으로 처리되며 생기던 에러를 추적해 배포까지 마쳤고, 한편 사내 도구가 준 잘못된 정보(환각)에 한 번 낚여 시간을 날린 것도 있어서 AI든 사내 도구든 그대로 믿지 말고 한 번 검증하자는 교훈을 다시 한 번 새겼다.</p>
<hr>
<h3 id="원온원">원온원</h3>
<p>화요일엔 리드와 원온원을 했다. 정말 좋은 인사이트를 많이 주셔서 따로 정리해두고 싶을 정도였다.</p>
<p>금요일엔 버디와 원온원을 했는데, 받은 피드백이 너무 좋아서 따로 정리해 다음 주 액션으로 옮겨보려 한다.</p>
<p>먼저 일정 관리. 미팅과 회식이 많아 그 사이에 일을 소화하기 어렵다는 고민을 나눴는데, 고정 일정을 빼고 실제 가용 시간 기준으로 계획을 잡을 것, 업무 시간을 블락으로 확보해둘 것, 빡빡한 일정은 &#39;워킹 데이가 많지 않았다&#39;는 걸 투명하게 공유할 것, 그리고 일정을 못 맞춰도 자책하기보다 다음 산정에 반영하라는 조언을 들었다. 결국 경험을 쌓으며 나를 알아가는 시간이 필요하다는 말이 오래 남았다.</p>
<p>AI 활용에 대한 고민도 깊게 나눴다. 설계에서 계획, 구현으로 이어지는 흐름은 빨라졌지만 중간에 정책·설계가 바뀌면 은근히 부채가 쌓이고, 그게 버그가 되면 디버깅을 하는 게 오히려 더 오래 걸린다는 게 내 고민이었다. 핵심은 &#39;양이 아니라 맥락&#39;이었다. </p>
<p>위임할 것과 직접 볼 것을 구분하고, 
계획 단계에서 &#39;여긴 건들지 말라&#39;는 가드를 더해 방향을 명확히 주고, 
변경 사항이 한꺼번에 몰려 인지부채가 쌓이지 않도록 피드백 루프를 짧게
 — 코드를 들여다보는 주기를 자주 갖고, 중간중간 AI에게 리뷰를 시키는 식으로 —</p>
<p>가져가라는 조언이었다. 피드백 루프를 더 짧게 가져가서 인지 부채가 쌓이지 않게하라는 조언이 가장 와닿으면서, AI 가 주는 생산 속도에 매혹돼 인지를 게을리했구나 반성하게 되었다.</p>
<p>내게 좋게 봐주신 점은 방향을 먼저 싱크하고 물어보며 스스로 피드백 루프를 돌리는 점을 들어주셨다.</p>
<p>이번 주간엔 로컬비즈니스실 FE 회식도 있었는데, 회식 자리에선 다른 팀의 개성 있는 일대기를 들을 수 있어 신기하고 재밌었다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 주에 주로 한 일은 맡은 기능의 디테일 마무리와, 그 과정에서 쏟아진 여러 트러블슈팅이었다.</p>
<h3 id="잘한-점">잘한 점</h3>
<ul>
<li>까다로운 제약을 끝까지 추적해 원인을 정확히 짚고 다른 접근으로 해결했다. 과정을 문서화해 버디와 공유했다.</li>
<li>플랫폼 동작 차이 버그와 중복 상태 에러까지 원인을 규명하고 일부는 배포까지 마쳤다.</li>
<li>원온원 일정을 잡은 덕분에 많은 인사이트를 얻고 막막함을 해소할 수 있었다.</li>
</ul>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ul>
<li>트러블슈팅과 장애 대응에 매몰돼 목표한 메인 작업을 제대로 끝내지 못한 날이 있었다. 작다고 생각한 부분에서 병목이 길어졌다.</li>
<li>반복적인 수작업에 업무 시간이 많이 날아갔고, 코드 리뷰 준비를 계속 다음으로 미루게 됐다.</li>
<li>미팅이 많은 날은 사실상 업무를 거의 못 한다는 걸 다시 체감했다.</li>
<li>작업이 백엔드 API에 의존하는 구간이 많아 영향이 간 경험이 있었다.</li>
<li>구조적으로 찜찜하게 남아 있던 부분을 해소하지 못한 채 부채로 남겼다.</li>
</ul>
<h3 id="다음-주-개선-방향">다음 주 개선 방향</h3>
<ul>
<li>고정 일정을 뺀 실제 가용 시간으로 계획하고 업무 시간을 블락으로 확보하기. 빡빡한 일정은 투명하게 공유하고, 못 맞춰도 자책보다 다음 산정에 반영하기(스스로 잡은 일정의 두 배로 보기).</li>
<li>AI는 위임할 것과 직접 볼 것을 구분하고 계획에 가드를 더해 방향을 명확히 주기. 변경이 한꺼번에 몰려 인지부채가 쌓이지 않게 피드백 루프를 짧게(코드 점검 주기·중간 AI 리뷰) 가져가고, 크면 단계로 쪼개기.</li>
<li>반복적인 수작업은 자동화하거나 미리 일괄 처리하기.</li>
<li>백엔드 API 의존이 큰 구간은 미리 MSW 를 활용해 내 작업이 함께 막히지 않게 하기.</li>
<li>미뤄둔 구조적 부채를 별도 아젠다로 떼어내 정리하기.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당근/회고] 인턴 출근 6주차 회고]]></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-6%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</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-6%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 30 May 2026 17:39:32 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<h1 id="🌱-6주차-회고-20260517">🌱 6주차 회고 (2026.05.17)</h1>
<h2 id="돌아보기">돌아보기</h2>
<h3 id="배포와-모니터링-그리고-습관이-된-점검">배포와 모니터링, 그리고 습관이 된 점검</h3>
<p>이번 주엔 지난주 내내 붙들었던 리팩토링 작업을 드디어 배포했다. 배포 자체보다 더 신경 쓴 건 그 뒤의 모니터링이었다. 여러 경로로 동시에 지켜보면서, 배포 전후의 사용자 지표가 의도대로 움직이는지까지 확인했다. 예전에 정책값 이슈로 마음 졸였던 경험이 있어서, 이제는 에러 모니터링뿐 아니라 사용자 동작이 의도대로 집계되는지까지 챙기는 습관이 생긴 것 같다.</p>
<hr>
<h3 id="설계의-고삐를-내가-잡았다는-감각">설계의 고삐를 내가 잡았다는 감각</h3>
<p>이번 주의 메인은 새로 맡은 기능의 설계와 구현이었다. 요구사항을 받아 API와 화면 흐름, 상태 관리 정책을 먼저 정리하고 그 위에서 구현으로 넘어갔다. 특히 만족스러웠던 건 책임을 어디까지 서버가 갖고 어디부터 클라이언트가 가질지 깔끔하게 나눈 의사결정이었다. 설계 문서를 쓰는 데는 오래 걸렸지만, 그 위에서 계획을 세우고 구현하니 확실히 빨랐다. 이번엔 설계의 고삐를 내가 확실히 잡았다는 감각이 있었고, 오랜만에 인터랙션을 직접 고민해보니 재밌었다.</p>
<hr>
<h3 id="새-도구와-워크플로우-실험">새 도구와 워크플로우 실험</h3>
<p>병렬 작업이 많아지면서 도구를 좀 바꿔봤다.컨덕터는 끝내 손에 안 익어서 포기했고, 대신 수퍼셋으로 병렬 작업을 하니 훨씬 잘 맞았다. AI를 작업에 연결하는 새로운 방식도 처음 시도해봤다. 지난주에 &quot;AI를 더 실험적으로 써보자&quot;고 다짐했던 걸 조금씩 실행에 옮긴 한 주였다. 매일 아침 버디에게 작업 현황을 문서로 정리해 공유하는 루틴도 자리를 잡았는데, 어차피 모든 업무를 메모해두니 정리에 오래 걸리지 않으면서도 병렬 작업이 많을 때 특히 효과가 좋았다.</p>
<hr>
<h3 id="공유의-중요성을-다시-느낀-날">공유의 중요성을 다시 느낀 날</h3>
<p>한 주제에서 학습이 병목이 됐을 때, 혼자 끌어안기보다 솔직하게 막힌 지점을 버디에게 공유드렸더니 바로 직접 설명을 들으며 풀 수 있었다. 혼자 끙끙대며 헤매기보다 빨리 공유해 해소하고 넘어가는 게 결국 더 많이, 더 빨리 일하는 방법이라는 걸 또 느꼈다. 혼자였으면 몰랐을 유의점까지 덤으로 알게 됐다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 주에 주로 한 일은 리팩토링 작업 배포와 모니터링, 그리고 새로 맡은 기능의 설계부터 구현·자체 QA까지였다.</p>
<h3 id="잘한-점">잘한 점</h3>
<ul>
<li>배포 후 여러 경로로 모니터링하며 안정성과 사용자 지표를 함께 확인했다. 지난 이슈 이후 다짐했던 &#39;사용자 동작 집계까지 챙기기&#39;를 실천했다.</li>
<li>서버와 클라이언트의 책임 경계를 깔끔하게 나누는 설계 의사결정을 내렸다. 설계의 고삐를 내가 잡았다는 감각이 있었다.</li>
<li>손에 맞지 않는 도구는 빠르게 정리하고 더 맞는 방식으로 전환하는 등, 도구와 워크플로우를 실험적으로 써봤다.</li>
<li>아침 싱크 공유 루틴을 정착시켜 병렬 작업 중에도 버디와의 싱크가 끊기지 않았다.</li>
</ul>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ul>
<li>한쪽에 신경 쓰다 함께 처리했어야 할 로직을 놓칠 뻔했다. 버디가 미리 발견해주셔서 다행이었지만 아찔했다.</li>
<li>설계를 논의할 때 필요한 부분의 감을 빠르게 잡지 못했다. 경험 부족이겠지만 더 빨리 설계 감을 잡는 법을 찾고 싶다.</li>
<li>일정이 많은 날엔 업무 시간에 일을 못 끝내고 늦게까지 작업하는 패턴이 반복됐다. 일찍 출근하려던 목표도 자주 놓쳤다.</li>
<li>프로젝트의 모든 빌드·CI 파이프라인이 어떻게 도는지 학습이 부족하다는 걸 느꼈다.</li>
</ul>
<h3 id="다음-주-개선-방향">다음 주 개선 방향</h3>
<ul>
<li>논의 자리에 들어가기 전에 필요한 설계 지점을 미리 스케치해두기</li>
<li>설계를 할 때 에러·엣지 케이스까지 처음부터 범위에 넣고 시작하기</li>
<li>프로젝트의 CI 파이프라인을 직접 확인하고 학습하기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당근/회고] 인턴 출근 5주차 회고]]></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-5%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</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-5%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 30 May 2026 16:14:12 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<h1 id="🌱-5주차-회고-20260510">🌱 5주차 회고 (2026.05.10)</h1>
<h2 id="돌아보기">돌아보기</h2>
<h3 id="비동기-오버레이-큐잉-시스템-마이그레이션-완료">비동기 오버레이 큐잉 시스템 마이그레이션 완료!</h3>
<p>이번 주의 메인 이벤트는 오버레이 큐잉 추상화 PR 실제 반영과 동네걷기 홈화면 전체 마이그레이션 완료였다. 월요일에 PR 리뷰를 반영하고 구현 단계에서 설계에 있는 허점들을 수습했다.구현을 시작하고 나서야 보이기 시작한 문제들이 있었다. </p>
<p>비동기 오버레이 큐잉을 설계하게된 이유가 웹뷰에서 알 수 없는 네이티브 맥락과 여러 의존성 팝업들이 동시에 트리거될 수 있고 이 순서를 순차적으로 제어해야하는 복잡도가 있었기 때문인데,
이런 복잡도 안에서 priority 안에서의 정렬 순서 세분화나 분기처리에서 엣지케이스가 생겼다.</p>
<p>또, 버디의 리뷰로 언마운트 시 클린업 상황을 고려할 수 있었다.
동네걷기 도메인 관련 모든 오버레이는 반드시 이 비동기 오버레이 큐잉 시스템을 쓰도록 skills 설정까지 명시해두었다.</p>
<hr>
<h3 id="하네스-엔지니어링에-대한-관심">하네스 엔지니어링에 대한 관심</h3>
<p>이번 주에는 AI 활용 방식에 대해서도 생각이 많아졌다. 설계를 기반으로 구현 Plan을 세우고 스프린트를 한 번에 auto mode로 시켰을 때, 설계의 헛점과 구현 전 단계의 통제, 그리고 빠른 생산이 꽤 잘 되고 있다는 감각이 있었다. 조금은 하네스 엔지니어링의 관점대로 하고 있는걸까? 싶은 생각이 조금 들었다.</p>
<p>그러면서도 아직 팀이 만들어둔 클로드 커맨드를 써본 적이 없었는데, 하네스 엔지니어링에 관심이 많고 선진적으로 도입하고 있는 팀에서 배워갈 수 있는 지점들을 놓치고 있었던 것 같고 반성되었다. AI를 더 실험적으로 다양하게 써보는 게 좋겠다고 다짐한 주였다. </p>
<p>확실히 테크스펙을 자세하고 명확하게 쓰고 나면 Plan도 에이전트 모델이 세워주고 Plan을 검토해서 수정을 거치고나면 구현은 한 번에 꽤 잘해줬다. 그래도 단순 위임을 넘어 diff에 대한 공세한 리뷰는 여전히 사람의 영역이라는 걸 손으로 부딪히면서 느꼈다.</p>
<hr>
<h3 id="출석-지면-추가와-다음-일정">출석 지면 추가와 다음 일정</h3>
<p>목요일부터는 출석을 동네걷기 하위 전 지면에서도 찍히도록 하는 작업을 시작했다. 스트릭을 어떻게 추상화해 처리하는 게 좋을지 다양한 방법을 고민해보고 트레이드오프를 따져보며 근거를 메모해둔 덕분에 빠르게 공유하고 적용할 수 있었다.</p>
<p>아쉬웠던 건 목요일 오후에 다음 과제를 위해 일정을 논의하러 들어갔는데 필요한 API를 너무 짧게 생각하고 갔던 것이다. 생각해야하는 부분이 많았는데 당연한 부분들을 놓친 게 조금 민망했다.</p>
<p>금요일엔 이어서 스트릭 적용과 QA, PR 리뷰까지 마무리했고, 다음 주 일을 수월하게 하기 위한 부채 작업들을 청산하는 느낌이었다.</p>
<p>이제 동네걷기의 출석은 동네걷기에 관련된 모든 지면을 직진입해도 찍힐 수 있게 되었다~</p>
<hr>
<h3 id="주도성에-대한-고민">주도성에 대한 고민</h3>
<p>주의 후반으로 갈수록 &quot;너무 주어진 일만 수동적으로 하고 있는 건 아닌가&quot;라는 생각이 들었다. 다른 인턴들을 보면 더 넓은 영역에서 에러도 빨리빨리 알고 대응하고 자동화도 적극적으로 도입하고 있는데... 인턴 기간 안에서 내가 얻을 수 있는 것에 어떻게 집중해야할지에 대해 다시 고민하고 있다.</p>
<p>그래도 버디와 원온원을 하면서 이 부분을 나눠서 조바심을 많이 덜 수 있었다. 내가 너무 오래 붙든 건 아닐까 걱정스러웠던 오버레이 리팩토링에 관련해 잘하고있다고 격려도 해주셨고, 다른 언어나 직무에도 관심을 갖고 공부하는 것도 좋을 거 같다고 해주셨다. 그래도 &quot;문제를 주도적으로 찾고 정의하고 해결해가는 것&quot;은 중요하고 좋은 경험이라고 해주셔서 좀 더 주도적으로 문제를 정의할 수 있도록 의식해보려고 한다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 주에 주로 한 일은 오버레이 추상화 마이그레이션 완료, 그리고 다음 일을 위한 설계와 부채 청산이었다.</p>
<h3 id="잘한-점">잘한 점</h3>
<ul>
<li>올린 PR의 리뷰들을 하나씩 반영하며 동네걷기 홈화면 전체 오버레이를 새 추상화로 마이그레이션했다.</li>
<li>동네걷기 하위 오버레이는 오버레이 시스템을 사용해 구현될 수 있도록 skills에도 명시해뒀다. 후속 작업자가 실수할 여지를 줄이는 장치어서 만족스러웠다.</li>
<li>출석 지면 추가 적용 방식을 서둘러 정하지 않고 트레이드오프를 메모해가며 고민했는데, 그 덕분에 구현 방향을 맥락 포함해 공유드릴 수 있었다.</li>
<li>공기계에 테스트앱을 등록하고 깔아둔 덕분에 iOS/안드로이드 둘 다 실기기 테스트 환경을 마련했다. 이제 QA 를 더 크로스로 꼼꼼히 할 수 있게 되었다.</li>
</ul>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ul>
<li>의사결정에 시간이 적지 않게 들었다. 트레이드오프를 꼼꼼히 따진 건 좋았지만, 결국 고민 자체가 길어졌다.</li>
<li>일정 논의에 필요한 API를 너무 짧게 생각하고 들어갔다.</li>
<li>하네스 엔지니어링에 관심이 많은 팀에 있으면서도 AI 활용 방식에 대해 좀 더 실험적이지 않았었다. 배움 계기를 놓치고 있었던 느낌.</li>
<li>주어진 일에 너무 수동적으로 임하고 있는 게 아닌가 싶은 의문이 계속 든다. 문제를 주도적으로 발굴해서 제안하는 개수가 적었다.</li>
</ul>
<h3 id="다음-주-개선-방향">다음 주 개선 방향</h3>
<ul>
<li>팀의 클로드 커멘드를 이번 주에는 꼭 써보고, AI 활용을 더 실험적으로 시도해보기.</li>
<li>트레이드오프 분석은 제한된 시간 안에서 끝내고, 액션을 타이트하게 가져가기. 간단해 보이는 요구사항이라도 정책·설계·구현 범위를 빨리 촘촘히 잡고 시작하자.</li>
<li>특히 다가올 일정은 미리 필요한 API나 설계 지점을 머릿속으로 스케치해두기.</li>
<li>주도적으로 문제를 발굴해 한 번에도 제안해보기.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당근/회고] 인턴 출근 4주차 회고]]></title>
            <link>https://velog.io/@dev-dino22/0518-%EC%9E%84%EC%8B%9C%EC%A0%80%EC%9E%A5</link>
            <guid>https://velog.io/@dev-dino22/0518-%EC%9E%84%EC%8B%9C%EC%A0%80%EC%9E%A5</guid>
            <pubDate>Sat, 30 May 2026 15:36:35 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<hr>
<p>회사 노션에 매일 업무 일지와 회고를 적고있는데 블로그에 올려둘 회고글이 많이 밀렸다.
노션의 개인 회고글을 블로그 용으로 붙이면서 내용을 다시 읽으니 새록새록 다시 배움도 정리할 수 있고 나쁘지 않은 것 같다.</p>
<hr>
<h1 id="🌱-4주차-회고-20260503">🌱 4주차 회고 (2026.05.03)</h1>
<h2 id="돌아보기">돌아보기</h2>
<h3 id="테크스펙-마무리와-칭찬">테크스펙 마무리와 칭찬</h3>
<p>이번 주는 오버레이 비동기 큐잉 시스템 테크스펙을 마무리짓고 공유한 주였다. 월요일에 버디의 피드백을 받아 기존 설계와 비교해 테크스펙을 보강하고 팀에 공유드렸는데 칭찬받아따! 팀원 분이 &quot;설계 너무 좋다&quot;고 해주셨고, 버디는 준비되면 위클리에서도 발표해보라고 격려해주셨다! 너무 기뻤다🥕</p>
<p>특히 단순한 결과물 칭찬뿐 아니라 이런 설계가 나오기까지의 사고 과정 자체에 팀원 분이 관심을 보여주셨는데, 그게 제일 좋았다. 추상화 단위를 끌어올린 결정이 단순히 구현 선택이 아니라 문제 재정의의 결과로 잘 전달됐구나 싶었다. 기존 설계를 &quot;다른 방식&quot;이 아닌 &quot;기반 위에 확장하는 형태&quot;로 포지셔닝한 것도 다행이었다. 내가 설계한 이 오버레이 비동기 큐잉 시스템이 구체적으로 우리 도메인의 어떤 복잡성을 풀었는지 추후 블로그에 기술적으로 더 풀어볼 예정이다.</p>
<hr>
<h3 id="promise와-이벤트-루프의-재발견">Promise와 이벤트 루프의 재발견</h3>
<p>구현 단계로 넘어가면서 JS의 비동기 동시성 메커니즘을 다시 깊게 파본 주이기도 했다. 오버레이 비동기 큐잉은 결국 Promise의 pending/settled 상태머신을 바깥으로 빼내어 그대로 활용하는 구조라, Promise 와 오버레이의 active 상태를 정확히 매칭하려면 이벤트 루프와 실행 컨텍스트의 세세한 동작과 정확한 타이밍을 제대로 알고 있어야했다.</p>
<p>원래 대충 이렇게만 알고 있었다. 동기 코드가 먼저 실행되고, await 을 만나면 async 함수가 콜스택에서 빠졌다가 Promise가 settled되면 다시 돌아와서 나머지가 실행된다 정도. 근데 구현하다보니 세부에서 모호한 지점이 등장했다.</p>
<ul>
<li>&quot;콜스택이 비워진다&quot;는 기준이 정확히 뭐지? 전역 실행 컨텍스트까지 제거되는 순간이라면, 그럼 함수의 환경정보도 사라지는 채 어떻게 변수를 유지해서 다시 이어서 실행되는 거지?</li>
<li>await 이후 라인의 코드는 정확히 언제, 어느 사이클에 다시 실행되는 거지? 마이크로태스크 큐와 매크로태스크 큐가 각각 한 번씩 비워지고 나서 다음 사이클에?</li>
</ul>
<p>우선 이 질문들을 분해해가며 ECMAScript 명세와 실제 엔진 동작까지 파보면서 개념을 확실히 정리했다.</p>
<ul>
<li><strong>콜스택 &quot;empty&quot;의 명세적 기준</strong>: 전역 동기 스크립트의 평가가 끝나면 스크립트 컨텍스트도 스택에서 명시적으로 제거된다. 명세상 &quot;콜스택이 비었다&quot;는 건 전역 실행 컨텍스트까지 빠진 상태를 뜻한다.</li>
<li><strong>데이터 유지의 정체 — 렉시컬 환경</strong>: 실행 컨텍스트가 콜스택에서 모두 사라져도 누군가 참조하고 있는 이상 메모리 힙의 렉시컬 환경은 살아 있고, 이게 클로저의 정체다. 이전에는 &quot;실행 컨텍스트 = 렉시컬 환경&quot;으로 생각하고 있었는데, 둘이 다른 레이어에 존재한다는 걸 명확히 구분하게 됐다.</li>
<li><strong>Job은 &quot;빈 스택&quot;에서 시작한다</strong>: await 이후 코드는 원래 함수 컨텍스트가 그대로 스택에 남아 있어서 이어 실행되는 게 아니고, await에서 suspend됐던 바로 그 동일한 실행 컨텍스트가 resume되며 다시 push된다. 전역 컨텍스트가 다시 들어오는 게 아니다. 이 지점에서 직관과 제일 상충하는데, &quot;모든 함수 컨텍스트는 전역 컨텍스트 위에서 실행된다&quot;는 통념은 <strong>동기 코드 한정</strong>으로만 맞다는 것을 알게 됐다.</li>
<li><strong>await 이후 코드의 실행 시점</strong>: 마이크로태스크 단계에서 실행된다. 다음 매크로태스크 사이클을 기다리지 않고 현재 사이클의 동기 코드가 끝난 직후 마이크로태스크 큐를 전부 비우는 시점에 끌려든다. 매크로태스크는 그 뒤에야 차례가 온다.</li>
</ul>
<p>이 마지막 포인트가 큐잉 시스템 구현에서 <code>queueMicrotask(process)</code>를 쓴 이유와 직결되는 게 재밌었다. 매크로태스크로 미루면 그 사이에 다른 태스크가 끼어들 수 있지만, 마이크로태스크로 미루면 같은 동기 태스크에 들어온 모든 reserve 호출을 한 번에 정렬해 처리하는 게 보장된다. 설계와 JS 엔진 동작이 그대로 맞닿는 느낌이었다.</p>
<hr>
<h3 id="구현-시작과-ai-활용">구현 시작과 AI 활용</h3>
<p>프로미스와 async/await을 머릿속으로 정리했다고 구현이 쉬워지는 건 아니었다. 이벤트 루프가 우아하게 처리해주는 영역이 있지만, 우선순위대로 정렬하고 선점과 임계 권한을 넘겨주는 스케줄링은 직접 구현해야 했다. 동시성 프로그래밍 이론적으로 더 정교하게 해보고 싶어서 법카로 동시성 프로그래밍 책까지 샀는데, 학습한 개념과 JS 코드로의 구현이 아직 시원하게 연결되지는 않는 느낌이다.</p>
<p>대신 이번 주 수요일 구현을 시작하면서 좋은 러닝이 하나 있었다. 테크스펙을 자세히 쓰고 구현 Plan을 마크다운으로 촘촘히 세워서 에이전트 AI에게 맡겼더니 한 번에 꽤 괜찮은 출력이 일관되게 나왔다는 것이다. </p>
<p>목요일에는 PR 올리고 리뷰까지 받았다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 주에 주로 한 일은 오버레이 큐잉 시스템 테크스펙을 마무리짓고, JS 비동기 동작을 명세 레벨까지 파고든 뒤 실제 구현과 PR까지 올린 일이었다.</p>
<h3 id="잘한-점">잘한 점</h3>
<ul>
<li>기존 오버레이 설계를 참고해 테크스펙을 보강하고 공유해 칭찬받았다. 단순 결과물뿐 아니라 문제 재정의에 이르기까지의 사고 흐름 자체에 팀원이 관심을 보여주셨다는 점이 특히 기분 좋았다.</li>
<li>구현 중에 await 이후 실행 시점, 콜스택과 렉시컬 환경의 관계, Job이 빈 스택에서 시작한다는 명세적 구조까지 정리했다. 설계의 근거와 흐름을 스스로 확신하게 되는 과정이었다.</li>
<li>테크스펙과 Plan을 촘촘히 쓴 다음 에이전트에게 구현을 맡겨보면서, 설계에 시간을 많이 투자하면 구현은 정말 빨리 끝난다는 걸 체감했다.</li>
<li>기후캠페인 코드를 사이드이펙트 없이 깔끔하게 걷어냈다. 구현 당시에 세웠던 가장 강한 목표였던 &#39;한번에 깔끔히 걷어질 수 있는 코드&#39;가 달성된 셈이다.</li>
</ul>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ul>
<li>구현을 시작하고 보니 설계 단계에서 잡았다고 생각한 것보다 직접 스케줄링해야하는 부분이 많았다. Promise의 이벤트 루프만 활용하면 로직이 아주 심플해질 줄 알았는데 그렇진 않았다.</li>
<li>동시성 프로그래밍 책을 샀지만 학습한 개념과 JS 구현이 아직 시원하게 연결되지는 않는다. 괜히 복잡도만 키우는 건 아닌지 찜찜하다.</li>
<li>테크스펙을 아이디어 단계까지 단단히 잡아둔 것은 좋았는데, 실제 코드베이스의 복잡도 (세부 플래그, 여러 트리거 훅 등)을 설계 단계에서 더 깊게 인지했으면 좋았을 것 같다.</li>
<li>문화의 날 등 고정 일정 때문에 실제 업무 시간이 적은 날에 진척이 잘 안 나갔다.</li>
</ul>
<h3 id="다음-주-개선-방향">다음 주 개선 방향</h3>
<ul>
<li>설계 단계에서 코드베이스의 실제 복잡도(트리거 훅의 세부 status flag, 관련 프로세스의 연관 흐름 등)까지 더 꼼꼼히 인지하고 설계하기.</li>
<li>구현하는 동안 동시성 프로그래밍 이론은 따로 시간을 빼서 읽기보다 이번에 마주한 구현 이슈들(선점, 임계, 제개)과 엮어서 필요한 만큼만 참고하는 방향으로 가자. 책은 제대로 읽으려면 오히려 복잡도를 키울 수 있다.</li>
<li>고정 일정이 많은 날에도 할당치를 다해낼 수 있는 일 배분 방식을 고안해보기. 야근을 보완으로 쓰기보다 업무 시간 내의 밀도를 올리는 쪽으로 노력하는 것이 예측 가능한 인력이 되는 가장 중요한 방법이라고 생각이 든다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당근/회고] 인턴 출근 3주차 회고]]></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-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</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-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 30 Apr 2026 10:53:23 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<h1 id="🌱-3주차-회고-20260426">🌱 3주차 회고 (2026.04.26)</h1>
<h2 id="돌아보기">돌아보기</h2>
<h3 id="기후부-캠페인-성황리에-종료">기후부 캠페인 성황리에 종료!</h3>
<p>월요일에 배포한 동네걷기 기후부 캠페인이 성황리에 잘 마무리되었다!</p>
<table>
<thead>
<tr>
<th align="left">캠페인 마지막 날 모달</th>
<th align="center">캠페인 종료 후 UI 전환 성공</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><img src="https://velog.velcdn.com/images/dev-dino22/post/c997f7d0-ae9b-48fa-8a88-5e9a100dff1a/image.gif" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/dev-dino22/post/45a1b0a9-dd3f-4d48-b7ff-9bd9c56cd410/image.gif" alt=""></td>
</tr>
</tbody></table>
<p>마지막 날의 보상은 스탬프 모달 후 종료 안내 바텀시트가 올라온 뒤 마일스톤 위의 나무아이콘이 사라져야했고</p>
<p>마지막 날의 자정이 지나 홈화면에 들어오면 기후부 캠페인 관련 코드가 싹 걷어지고 기존의 기본 UI 로 돌아와야했는데 다행히 둘 다 무사히 성공했다.</p>
<p>위 영상은 혹시라도 코드가 걷어질 때 기존 화면에 문제가 생길까봐 바로 써보면서 찍어둔 영상이다. 무사히 코드가 걷어진 걸 확인하고 나서도 불안해서 1시간 정도는 계속 이벤트 로깅을 관찰하고 온콜대기를 했다. 하지만 정말 다행히도 그런 불행은 일어나지 않았다...ㅎㅎ</p>
<p>첫 과제로 진행하고 배포한 기후부 캠페인의 반응이 굉장히 좋았다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/4d2c816e-426b-478e-a935-956f4f6de2dd/image.png" alt=""></p>
<blockquote>
<p>당근은 기후에너지환경부, 한국환경보전원과 &#39;탄소중립·녹색성장 대국민 인식 제고 및 정보 제공 협력을 위한 업무협약&#39;을 30일 체결한다고 밝혔다.
...
이는 세 기관이 지난 20~26일 &#39;지구를 위한 동네걷기&#39; 캠페인을 진행해 총 47만4844명이 참여한 성과를 기반으로 진행한다.</p>
</blockquote>
<p>출처: <a href="https://www.etnews.com/20260430000061">https://www.etnews.com/20260430000061</a></p>
<p>동네걷기 캠페인의 성과를 기반으로 4월 30일에 당근이 기후에너지환경부, 한국환경보전원과 &#39;탄소중립·녹색성장 대국민 인식 제고 및 정보 제공 협력을 위한 업무협약&#39;을 체결한다는 내용의 보도자료까지 발표되었다.</p>
<p>이렇게 좋은 성과가 나온 프로모션을 개발해서 배포할 수 있었던 게 뿌듯하고 기쁜 걸 넘어서 엄청 신기한 기분이다. 코드 리뷰 꼼꼼히 봐주시고 배포 마지막까지 함께 엣지케이스 대응해주시며 믿고 맡겨주신 버디께 정말 감사한 마음이다.</p>
<hr>
<h3 id="이슈-대응과-재발방지책-제안">이슈 대응과 재발방지책 제안</h3>
<p>기후부 캠페인이 진행되는 동안 모니터링을 지속했다. 그리고 중간에 한 번 이슈를 겪어 죄책감과 책임감을 많이 느낀 일이 있었다.</p>
<p>캠페인 둘째 날, 진행 날짜 정책값이 잘못 설정되어 일부 유저들에게 이벤트 종료 바텀시트가 미리 노출되는 이슈가 있었다. 캠페인 진행 날짜를 API로 받아오는 구조이다 보니 그 값이 어디서 관리되는지 충분히 확인하지 못했던 것이 원인이었다.</p>
<p>해당 이슈의 확인이 늦어 당일 연차 휴가 중이셨던 버디가 발견하시고 수습해주시고 말았다. 확인이 늦은 것, 버디의 온콜을 늦게 확인해 결국 휴가 중이신 버디가 수습하게된 점이 너무 죄송스럽고 반성되어, 이 사건을 계기로 개인으로서는 에러 모니터링 뿐만 아니라 의도한 대로 사용자 이벤트가 집계되고 있는지 사용자 모니터링도 함께 해야겠다고 다짐했고, 팀에는 재발방지책을 제안드렸다. </p>
<p>PRD 단계에서 정책성 값의 관리 주체를 명시하자는 것이었다. 이런 값들은 운영 중에 변경될 수 있는데, 어디서 제어되는지가 PRD에 명시되어 있다면 엔지니어가 검토 단계와 배포 단계에서 한 번 더 의식해서 확인할 수 있겠다고 생각했다.</p>
<p>서버에서 받아오는 값이라고 해서 모두 백엔드 DB에서 관리되는 게 아니라는 것과 다른 경로로도 조작될 수 있다는 것을 알게 된 좋은 러닝 포인트였다. 그리고 이슈를 추적하면서 빅쿼리로 이벤트 로깅 집계를 확인하는 방법도 새로 학습했다.</p>
<hr>
<h3 id="리팩토링-방향성-잡기">리팩토링 방향성 잡기</h3>
<p>이번 주 동안은 동네걷기 도메인의 리팩토링 포인트도 함께 정리했다. 아무리 다양한 프로모션이 진행되더라도 바뀌지 않을 비즈니스 로직의 뼈대를 발라내고, 도메인의 본질적인 복잡도가 어디에 있는지 짚어보는 작업이었다. 기후부 캠페인을 구현하면서 느껴지는 복잡도들을 중간중간에 메모해두었었는데 리팩토링 방향성에 도움이 됐다.</p>
<p>만보기 대시보드의 흐름을 그림으로 그려가며 정리해봤는데, 어디에서 강한 결합이 생겨있고 어떤 로직들이 서로 다른 관심사를 가진 채 섞여있는지 명확하게 보였다. 역시 전체 플로우를 파악할 때 그림으로 그려보는 게 가장 좋은 방법인 것 같다.</p>
<p>이렇게 정리한 리팩토링 포인트들을 버디에게 공유드렸고, 공감대가 있었던 부분을 내가 맡아 리팩토링하기로 했다. 매일 아침 버디께 싱크 공유를 드리며 날카로운 피드백을 받고 추상화를 거듭하다 보니 본질적인 문제로 추상화해 재정의할 수 있었다.</p>
<p>테크스펙 초안을 쓰는 데 생각보다 오래 걸렸지만, 작성하면서 스스로 의문이 생기고 설계의 빈틈이 보여서 계속 보완해나간 시간이었다.</p>
<p>이 때 기분이 좋았던 일이 하나 있었는데, 테크스펙을 작성하다가 이전에 비슷한 고민으로 팀원분이 작성하셨던 문서를 발견했다. 풀이 방법은 달랐지만 문제 정의와 아이디어 시작까지 비슷하게 도출되었다는 게 신기하고 뿌듯했다. 내가 문제 정의를 제대로 하고 있었구나 하고 확인받는 느낌이었달까!</p>
<hr>
<h3 id="첫-온콜-대응">첫 온콜 대응</h3>
<p>이번 주에는 첫 온콜 대응도 해봤다(?) 내가 맡은 동네걷기 도메인은 아니었고, 다른 프론트엔드 분들이 출근하시기 전에 백엔드 분이 발견한 이슈를 보고 원인을 파악해 출근 중이신 팀분들께 원인을 공유드렸다. 칭찬 받았따.</p>
<p>내 도메인이 아니어도 가장 의심가는 원인을 생각해 코드 흐름을 따라가서 원인을 짚어낼 수 있었다는 게 뿌듯했다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 주에 주로 한 일은 기후캠페인 모니터링과 함께 기존 코드의 복잡도가 뭉쳐있는 부분을 리팩토링하기 위해 설계하는 일이었다.</p>
<h3 id="잘한-점">잘한 점</h3>
<ul>
<li><p>캠페인 둘째 날 발생한 정책값 이슈에 대해 단순 수습에 그치지 않고, PRD 단계에서 정책성 값의 관리 주체를 명시하자는 팀 차원의 재발방지책을 제안드렸다.</p>
</li>
<li><p>동네걷기 도메인의 플로우를 그림으로 그려가며 정리해서, 결합과 관심사 분리 지점을 명확히 짚어내고 리팩토링 포인트를 도출했다.</p>
</li>
<li><p>매일 아침 버디에게 싱크 공유를 드리며 더 나은 풀이방법을 위해 마인드맵을 그려가며 추상화를 거듭한 끝에, 더 본질적인 문제로 재정의할 수 있었다. 꽤 뿌듯한 과정이었어서 작성하고있는 테크스펙을 나중에 따로 포스팅할 수 있으면 좋겠다.</p>
</li>
<li><p>이전에 팀원분이 비슷한 고민으로 작성하신 테크스펙을 발견했는데, 문제 정의와 핵심 아이디어까지 비슷하게 도출되어서 내 문제 정의 방향성에 확신을 얻었다.</p>
</li>
<li><p>내 도메인이 아닌 영역의 온콜 이슈를 출근 시간 전에 원인 파악해서 팀에 공유드렸다. 칭찬도 받아서 뿌듯했다.</p>
</li>
</ul>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ul>
<li><p>캠페인 진행 날짜를 API로 받아오는 구조였는데, 그 값이 어디서 관리되는지 충분히 확인하지 않고 넘어갔던 것이 이슈의 직접적인 원인이었다.</p>
</li>
<li><p>테크스펙 공유가 늦어졌다. 점심 직후에 드리려고 했는데, 작성하다 보니 스스로 의문점과 설계의 빈틈이 계속 보여 보완하다가 퇴근 시간이 거의 다되어서야 공유드릴 수 있었다.</p>
</li>
<li><p>설계와 아이디어를 다듬는 단계에서 시간이 너무 오래 걸린다는 느낌을 받았다.</p>
</li>
<li><p>고정 일정이 많은 날은 회의 사이사이에 생각의 맥락이 끊겨, 깊이 몰입해야 하는 작업의 진척이 더뎠다.</p>
</li>
</ul>
<h3 id="다음-주-개선-방향">다음 주 개선 방향</h3>
<ul>
<li><p>테크스펙이나 설계 문서를 공유할 때, 100% 완성도를 추구하기보다 이른 단계에서 초안을 공유하고 피드백을 받아 발전시키는 방향으로 일해보기</p>
</li>
<li><p>회의가 많은 날은 짧은 호흡의 작업을, 연속 시간이 확보된 날은 깊은 사고가 필요한 설계 작업을 배치하는 식으로 요일별 작업 성격을 맞춰 배분해보기</p>
</li>
<li><p>설계를 하고 리팩토링을 하는 업무에는 뽀모도로나 타이트한 시간 관리가 없어도 될 것 같다. 오히려 시간 측정이 몰입도를 방해한다는 생각이 들었다. 태스크를 잘게 쪼개 구현을 빠르게 쳐내야하는 환경일 때만 다시 시행하고 리팩토링 주간에는 뽀모도로를 통한 시간 추적 및 관리를 폐기한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당근/회고] 인턴 출근 2주차 회고]]></title>
            <link>https://velog.io/@dev-dino22/%EB%8B%B9%EA%B7%BC%ED%9A%8C%EA%B3%A0-%EC%B6%9C%EA%B7%BC-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev-dino22/%EB%8B%B9%EA%B7%BC%ED%9A%8C%EA%B3%A0-%EC%B6%9C%EA%B7%BC-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 19 Apr 2026 14:55:40 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<h1 id="🌱-2주차-회고-20260420">🌱 2주차 회고 (2026.04.20)</h1>
<h2 id="돌아보기">돌아보기</h2>
<p>⬇️ 4/20 배포된 내 첫 배포 기후주간 캠페인 화면 ㅎㅎ</p>
<table>
<thead>
<tr>
<th align="left">배포 전</th>
<th align="center">배포 후</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><img src="https://velog.velcdn.com/images/dev-dino22/post/c54910c7-c9c1-4fa8-859d-c59d759f8e0f/image.png" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/dev-dino22/post/240bfb2a-0be6-40e2-914e-cb4e2e5b8e33/image.png" alt=""></td>
</tr>
</tbody></table>
<p>이번 한 주는 일주일 뒤면 바로 배포가 나갈 [ 동네걷기 X 기후환경부 기후주간 캠페인 ] 을 구현하는 주간이었다. 저번주 동안 익힌 기존 코드베이스 이해를 기반으로 프로모션 기간 1주일 동안만 UI를 변경하다가 기간이 지나면 바로 기존 UI로 돌아가야했다. 그만큼 금방 걷어질 코드기도 하고, 설정한 시간이 지나면 바로 기존 코드로 돌아와 문제없이 동작해야했으므로 다른 코드에 영향을 주지 않게 작업해야했다.</p>
<p>그리고 이 과정에서 간단한 요구사항에 비해 생각 못한 엣지케이스가 구현 중 계속 터져나왔다. 생각보다 동네걷기라는 서비스의 특성상 만보기와 같은 네이티브 기능으로 파생되는 복잡도나 리워드와 같은 다른 외부 맥락과의 강결합으로 인해 고려해야하는 점들이 많았다. 저번 주의 온보딩 주간동안 코드베이스를 간략하게 파악했을 때는 전혀 생각못했던 복잡도에서 많이 헤맸던 것 같다.</p>
<p>그래도 다른 코드에 영향이 안 가게 작업하다 보니 코드베이스의 이해도가 확실히 더 깊어졌고 작업 대상이었던 홈화면의 흐름을 꽤 확실히 숙지할 수 있었던 것 같다.</p>
<p>또, 직접 API 스펙을 yaml 파일로 작성해 제안드리고 PRD 와 피그마를 보면서 헷갈리는 점은 바로바로 질문드리는 등 백엔드, PD, PM 분들과 함께 협업하는 것에 익숙해질 수 있었다. 그리고 구현하다가 설계 방향에 있어서 임의로 결정하고 리뷰받기에는 변경사항이 커질 수도 있는 부분들은 작업 전 버디에게 바로 논의드리고, 매일 작업 현황을 공유하는 등 싱크를 꾸준히 맞추려고 노력했고 버디에게 칭찬받을 수 있었다.</p>
<p>월요일과 화요일은 11시가 넘어 퇴근했는데, 이는 구현의 범위가 절대적으로 많았다기 보다는 기존 코드베이스의 흐름을 완전히 이해해야 다른 코드에 영향을 안 주는 작업이 가능했기 때문에 이해하고 설계를 고민하는데에 시간이 많이 든 탓이었다. 큰 이슈가 없었는데도 예상한 시간보다 훨씬 오래걸려 야근을 하게된 점은 스스로 꽤 아쉽고 주눅이 들었다. 이대로면 체력도 문제지만 계속 야근이 습관이 되고 목표한 시간 내에 일을 쳐내지 못하면 정말 예기치 못한 이슈가 터졌을 때 야근이라는 카드를 쓸 수가 없기 때문이다. 신뢰가고 예측 가능한 인력이 되자는 목표를 위해서는 확실히 개선해야하는 점이었다.</p>
<p>수요일에는 버디와 싱크를 맞추고 코드를 함께 점검하는 시간을 가졌고 목요일, 금요일에는 사내 앱에 직접 배포해 PD, PM 의 QA를 받았다. QA 를 편하게 하실 수 있도록 작성했던 플러그인 코드가 뿌듯했다. 정확히는 뿌듯했었다. 금요일 퇴근 직전 버그를 발견하기 전까지...</p>
<p>QA 마지막 날인 금요일, 퇴근 시간을 앞두고 작은 버그 하나를 발견했다. 발견 당시에는 사소해 보였지만 원인을 추적하다 보니 결국 내가 QA 편의를 위해 작성했던 플러그인 코드에서 출발한 문제였다. QA 가 끝나면 지울 코드라는 생각에 학습 비용을 들이지 않겠다고 판단해 클로드에게 많이 맡겼었는데, 그 과정에서 외부 상태를 오버라이드해서 데이터를 주입하는 구조를 별 거부감 없이 accept 한 것이 화근이었다. </p>
<p>&quot;어차피 지울 코드&quot;라는 이유로 비즈니스 로직과 QA 로직이 한 데 섞여버렸고, 이로 인해 간헐적으로 상태가 꼬이는 문제가 발생하고 있었다. 운이 나빴다면 발견하지 못하고 그대로 배포될 뻔한 버그였다.</p>
<p>이미 6시가 가까워진 시간이었고 PR 리뷰도 끝나서 나는 접근성 개선 같은 소소한 작업만 마저 할 거라고 공유드렸던 상황이었다. 혼자 수습해보고 싶은 마음이 잠시 들었지만, 월요일이 배포라는 점을 생각하면 늦더라도 빨리 공유드리는 게 맞다고 판단했다. 죄송한 마음으로 버디에게 말씀드렸는데 곧장 자리로 오셔서 함께 코드를 봐주셨다.</p>
<p>가장 감사하면서도 죄송했던 건, 내가 당황해서 흐름을 따라가는 속도가 느려졌는데도 버디는 단 한 번도 재촉하지 않으시고 내가 끝까지 스스로 이해하고 정리할 수 있게 기다려주셨다는 점이다. 이미 퇴근 시간이 한참 지난 시간이었는데 옆에서 묵묵히 같이 남아주셨다.</p>
<p>집에 돌아오는 길에 이 사건을 곱씹으면서 한 주 동안 내가 어디까지를 스스로 했고 어디서부터 클로드에게 손을 놓았는지 돌이켜봤다. 초반 코드베이스 학습과 전체 플로우 설계는 직접 했지만, 구현을 시작하면서 러프했던 설계의 구멍을 마주칠 때마다 클로드를 부르기 시작했고, QA 플러그인부터는 거의 위임에 가까운 상태가 되어 있었다. 단순히 클로드를 많이 썼다는 게 문제가 아니라, 그 과정에서 &quot;왜 이 설계인가&quot;에 대한 판단을 스스로 내리지 않은 채 accept를 눌렀던 순간들이 있었다는 게 진짜 문제였다. </p>
<p>입사 전부터 다짐했고 원온원 때 버디께도 말씀드렸던 방향과 어긋나 있었다. 구현 속도라는 핑계로 설계의 고삐를 놓쳐버린 셈이다. 앞으로는 클로드에게 코드 생성을 맡기더라도 그 설계가 왜 그 모양이어야 하는지는 내가 직접 답할 수 있는 상태여야 accept를 누르겠다고, 그리고 테스트용이나 임시 코드라도 비즈니스 로직과 섞이는 순간엔 학습 비용을 들여서라도 스스로 이해하고 짜야겠다고 다짐했다.</p>
<p>일요일에도 배포 전 마지막 점검을 했다. 이 김에 플러그인 제거와 함께 리팩토링과 접근성 개선, 트리거 조건분기를 다시 정리하다가 또 새로운 케이스 하나가 눈에 들어왔다. 시간이 늦어 일요일에는 공유드리지 못하고 월요일 아침에서야 말씀드려 함께 해결했다. 엣지케이스를 배포 직전까지 계속 새로 발견하고 있다는 점은 분명히 반성해야 할 부분이다. 다음 과제부터는 구현 시작 전 설계 단계에서 엣지케이스 정리에 더 많은 시간을 쓰기로 마음먹었다.</p>
<p><img src="https://velog.velcdn.com/images/dev-dino22/post/23316b64-834c-4273-88f5-8284cdf11b87/image.png" alt=""></p>
<p>우당탕탕 정신없었던 첫 배포지만 오전 11시에 배포했는데 3시간 정도 지난 오후 2시에 이미 이벤트 참여 댓글이 1만개 가까이 달릴 정도로 생각보다 뜨거운 관심을 받았다. <a href="https://economist.co.kr/article/view/ecn202604200005">기사</a>까지 났다!</p>
<hr>
<h2 id="정리">정리</h2>
<h3 id="잘한-점">잘한 점</h3>
<ul>
<li>매일 일찍 출근해 하루 할 일을 정리하고 업무 중의 시간 측정 및 퇴근 전 하루 회고 작성 루틴을 계속 유지하고 있다.</li>
<li>API 스펙을 직접 설계하고 닐과 논의하며 GET 없이 POST 하나로 가는 의견을 냈다.</li>
<li>다른 코드에 영향이 가지 않도록 신경 쓰면서 작업한 결과 웹뷰 기반의 코드베이스 이해도가 한층 깊어졌다.</li>
<li>구현 일정이 늦어지지 않게 풀리지않는 문제들은 30분만 혼자 고민해보고 버디에게 도움을 요청하는 시간 관리 룰을 적용했다.</li>
<li>버디 QA에서 프로모션 기간 내 깔끔하게 걷어지도록 작성한 점, QA용 플러그인을 꼼꼼히 만들고 진행상황을 중간중간 공유한 점에 대해 좋은 피드백을 받았다.</li>
<li>퇴근 직전에 발견한 상태 꼬임 이슈를 늦은 시간이라도 망설이지 않고 바로 버디에게 보고드려 함께 해결할 수 있었다.</li>
<li>한 주 동안 어디서부터 클로드에게 의존하기 시작했는지 스스로 흐름을 되짚어보며 점검했다.</li>
<li>사실 첫 과제라서 과제를 주실 때까지만 해도 버디가 함께 구현할 생각이었는데 생각보다 내가 꼼꼼히 구현해나가고 공유도 꾸준히 해서 나에게 맡겨주셨다고 했다. 엄청 뿌듯하고 기쁜 일이었다!</li>
</ul>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ul>
<li>월요일과 화요일 모두 11시 넘어 퇴근했다. 큰 이슈가 없었음에도 예상보다 훨씬 오래 걸렸다는 점에서 자기 능력에 대한 과신이 있었다고 생각한다.</li>
<li>뽀모도로와 시간 트래킹이 무너졌다. 마음이 급해지면 가장 먼저 놓는 도구가 되어버렸다.</li>
<li>수요일 브랜치 머지 실수 여파와 vite 캐시 꼬임 디버깅으로 메인 업무 시간이 너무 밀렸다.</li>
<li>QA용 플러그인을 &quot;어차피 지울 코드&quot;라는 생각으로 클로드에게 완전히 맡겼고, 외부 상태 오버라이딩 제안에 거부감 없이 accept 했다. 그 결과 비즈니스 로직과 QA 로직이 섞이며 간헐적인 상태 꼬임이 발생했다.</li>
<li>원온원에서 온보딩 기간 동안엔 클로드 사용을 지양하고 손코딩을 해보겠다고 말씀드렸는데, 기한이 다가오자 그 다짐과 어긋난 자신을 발견했다.</li>
<li>엣지케이스를 배포 전날까지 계속 새로 발견하고 있었다. 조기에 더 꼼꼼히 정리하지 못한 점이 아쉽다.</li>
</ul>
<h3 id="다음-주-개선-방향">다음 주 개선 방향</h3>
<ul>
<li>가용 시간을 지금까지 잡아온 것의 절반으로 본다. 온보딩 기간임을 인정하고, 일정을 공유할 때도 이 기준으로 보수적으로 잡는다.</li>
<li>QA 도구라도 메인 코드의 상태 관리에 손을 대는 순간 학습 비용을 들여야 하는 영역이다.</li>
<li>클로드에게 코드 생성을 맡기더라도 설계만큼은 확실히 내가 고삐를 쥐고 구현적인 코드 생성만 맡긴다. 어떤 것을 내가 잡고 어떤 것을 맡길지의 감각을 키워나간다.</li>
<li>엣지케이스 정리를 구현 시작 전 단계로 앞당긴다. (PRD 엔지니어 검토 단계에서부터 리뷰해 PM,PD 분들과도 더 적극적으로 소통하자.)</li>
<li>배포 직전에 새로 발견되는 케이스가 줄어들도록 설계 단계에서 시간을 더 쓴다.</li>
</ul>
<hr>
<p><del>2주차 회고글은 기후 캠페인이 배포되고 난 후 올리는 게 좋을 것 같아서 비공개 처리를 해두었다가 3주차가 끝난 지금에서야 공개로 돌리게 되었다</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당근/회고] 인턴 출근 1주차 회고]]></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-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</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-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 12 Apr 2026 15:42:41 GMT</pubDate>
            <description><![CDATA[<p>문제되는 내용이 포함되어 있을 경우 수정/삭제 하겠습니다.</p>
<h1 id="🌱-1주차-회고-20260412">🌱 1주차 회고 (2026.04.12)</h1>
<p>출근 전 결심했던 대로 매주 회고를 작성해보려고 한다. 회고의 목적은 지난 한 주간 어떻게 업무했는지를 객관적으로 돌아보며 더 나은 다음 주를 만들기 위함이다.</p>
<p>매일 회고를 적어두는만큼, 한 주 회고에 너무 긴 시간을 투자하는 부담을 만들고싶진 않아서 간단하게 작성해보려고 한다.</p>
<h2 id="돌아보기">돌아보기</h2>
<p>입사 첫 주였다. 온보딩 세션, 팀 소개, 장비 세팅부터 시작해 팀원들과의 원온원까지 고정 일정이 매우 많은 한 주였다. 그래서 실제 업무 가능 시간을 계산해보면 월요일부터 금요일까지 생각보다 많지 않았고, 그 시간 안에 코드베이스 파악, 웹뷰-네이티브 브릿지 학습, 과제 분석 및 설계를 진행했다. 내가 소속한 로컬비즈니스의 C2C 팀원들과 원온원을 빠르게 진행하며 팀의 문화와 기대치를 파악하는 데도 많은 시간을 썼다.</p>
<h3 id="ai-의-사용">AI 의 사용</h3>
<p>생각보다 사내 AI로 업무의 상당 부분이 정말정말 많이 자동화가 되어있었다. 게다가 내부 슬랙봇을 통해 지난 사내 대화와 모든 문서, PR 등을 쉽게 열람하고 정리할 수 있었기 때문에 빠른 온보딩에 큰 도움이 됐다.</p>
<p>클로드도 활용할 수 있었는데, 혼자 공부할 땐 가격이 너무 비싸 사용해보지 못했던 클로드를 이렇게 마음껏 사용할 수 있다니...실제로 직무 상관 없이 많은 분들이 AI 를 적극적으로 활용해서 업무의 자동화를 끊임없이 만들고 계셨다.</p>
<p>정말 인턴으로서는 당근이 최고의 환경이 아닐까?</p>
<h3 id="팀원들이-나에게-기대하는-점">팀원들이 나에게 기대하는 점</h3>
<p>나는 출근 전 <a href="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">회고글</a> 에서 나를 채용한 이유에 대해 팀의 생산성을 높이기 위해서라고 예상했고 이를 위해 매일 작성할 업무 DB 템플릿을 미리 만들어두었다. 매일 아침에 조금 일찍 출근해서 하루 할 업무를 정리 후 시간을 계획하고 실제 시간은 얼마나 걸렸는지 체크해 점점 업무 시간 측정을 잘하게되기 위함이었다. 이는 팀의 생산성을 높이기 위해서는 스스로 시간관리를 잘하고 예측 시간을 정확히 공유함으로써 &quot;예측 가능한 인력&quot;이 되어야겠다고 목표했기 때문이다.</p>
<p>그리고 실제로 원온원을 진행하면서 나에게 어떤 기대를 하고 계신지 여쭤보았는데, 정리해보면 모두 같은 말씀을 해주신 것 같다. &#39;신뢰할 수 있는 동료&#39;가 되는 것.
일정 관리와 공유를 잘 하고, 맡은 업무를 생산성있게 해내고 이런 일들은 결국 모두 나라면 어떤 동료를 신뢰할 수 있을까 를 생각해보았을 때 당연히 필요할 역량들이었다.</p>
<p>그로스액션을 빠르게 수행하는 팀에 들어오면서 프론트엔드로서 어떤 팀원이 되어야할지 고민이 많이 되었는데, 원온원을 진행하면서 생각보다 빠르게 추구할 방향을 정리할 수 있었던 것 같다.</p>
<h3 id="팀-문화">팀 문화</h3>
<p>예상보다도 훨씬 더 수평적이고 인턴에게도 기회가 많은 환경임을 크게 느꼈다. </p>
<p>나는 출근 전 내가 맡게될 &#39;동네걷기&#39;라는 서비스에 익숙해지기도 할 겸 2주간 매일 사용하면서 사용성에 대한 메모를 했었다. 입사하게되면 당근만 아는 맥락과 의도를 가진 채로 서비스를 바라보는 일종의 오염된(?) 뇌가 될텐데, 좀 더 사용자의 입장에서 어떤 불편함과 감정을 느꼈는지 적어두고 싶었기 때문이다. 그런데 이 것을 레포트 형식으로 다듬어서 스크럼 시간에 간단하게 발표할 기회가 있었다. 그리고 꽤 긍정적인 피드백을 받을 수 있었는데, 프론트엔드 혹은 인턴이라는 직무나 직급에 경계를 두지 않고 프로덕트를 위한 다양한 의견을 낼 수 있는 환경임을 실감했다.</p>
<p>그리고...조금 웃긴? 일화인데, 나 때문에 내 버디가 계속 놀림을 받았다...ㅋㅋㅋㅋ
나는 첫 회사생활이고 원래도 긴장도가 높은 편이라 첫 주는 계속 얼어있는 채로 지냈다. 닉네임으로 부르고 수평적인 문화인 걸 알아도, 프리랜서 때 클라이언트에게 깍듯이 했던 것처럼 누군가 오실 때마다 일어나서 꾸벅 인사하고 잔뜩 긴장해있고 그랬다.</p>
<p>그리고 사건의 발단은 로컬비즈니스실의 FE 위클리 때 였는데, 회의실에 먼저 도착해서 누군가 들어오실 때마다 자리에서 일어나 꾸벅 인사를 했다. 그런데 그게...당근 문화에선 꽤 이질적인 행동이었던 것 같다. 당시에도 다들 웃겨하셨고 다음 날 출근해보니 버디가 얼마나 기강을 잡은 거냐고 막 놀림받고 있었다😳</p>
<p>당연히 심각한 일이 된 건 아니고 웃긴 해프닝인 일이었지만, 정말 감사할 정도로 따뜻하게 하나하나 잘 챙겨주신 버디인데 내 경직된 반응이 너무 계속되면 괜히 오해를 받을까 싶기도하고 수평적인 팀문화를 해치지 않게 적응하는 것도 필요한 것 같아서 금요일부턴 긴장도를 조금 내려놓았다. 그랬더니 원온원도 웃긴 분위기로 한결 더 편하게 할 수 있었고 회사에서의 시간동안 체력이 크게 닳지 않는 느낌이었다.</p>
<p>역시 이런 수평적인 좋은 문화에서 불필요하게 긴장해있는 건 업무적으로도 좋지 않은 영향이 있을 것 같아서 더 긴장을 풀어가려고 한다. (실제로, 긴장이 풀리고나니 주말동안 피곤이 몰려와 거의 몸살이 날 뻔 했다...)</p>
<hr>
<h2 id="정리">정리</h2>
<h3 id="잘한-점">잘한 점</h3>
<ul>
<li>원온원과 매일 회고를 진행하며 스스로 해가야할 방향을 빠르게 정리할 수 있었다.</li>
<li>입사 전 실사용자로서 서비스를 써보고 느낀 유저 경험을 스크럼에서 공유했고, 좋은 반응을 받았다.</li>
<li>생소한 개념(웹뷰-네이티브 브릿지)을 이론적으로 너무 깊게 파고드는 욕심을 중간에 의식적으로 멈추려는 시도를 했다.</li>
<li>매일 더 일찍 출근해 하루 일과를 정리하고 퇴근 전 회고를 빠짐없이 작성했다.</li>
<li>용기 내서 권한 문제를 버디에게 여쭤보고 인프라팀과 소통했다.</li>
<li>사내 AI 툴을 적극적으로 활용해서 온보딩을 잘할 수 있었다.</li>
<li>가용 시간을 명시적으로 계산하고 하루를 계획하는 습관을 만들었다.</li>
</ul>
<hr>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ul>
<li><strong>뽀모도로를 제대로 활용하지 못했다.</strong> 맥락 전환이 잦고, 두루두루 살펴보는 작업과 하나에 집중하는 작업이 섞이다 보니 타이머를 켜는 타이밍을 자꾸 놓쳤다. 업무 시간 트래킹이 흐릿해졌다.</li>
<li>슬랙 확인이 느려서 일정을 누락할 뻔한 순간이 있었다.</li>
<li>학습할 때 &#39;어디까지 이해할지&#39;의 경계를 미리 정하지 않아서 시간이 예상보다 훨씬 오래 걸렸다.</li>
<li>웹뷰 세팅에만 예상의 3배 가까운 시간을 썼다.</li>
<li>과제 분석을 목요일까지 시작조차 못했다. 계획을 세웠어도 실행 순서가 밀리면 계획 자체를 다시 조율하지 못하는 패턴이 있었다.</li>
</ul>
<hr>
<h3 id="다음-주-개선-방향">다음 주 개선 방향</h3>
<ul>
<li><strong>가용 시간 명시는 계속한다.</strong> 고정 일정을 먼저 빼고 실제 쓸 수 있는 시간을 계산하는 건 좋은 습관이었다. 단, 계획할 때 가용 시간의 최대 2/3만 채운다고 가정하고 여유를 둔다.</li>
<li><strong>뽀모도로는 &#39;모든 업무&#39;가 아닌 &#39;집중이 필요한 단일 태스크&#39;에만 적용해본다.</strong> 맥락 파악처럼 흐름이 이어지는 작업에는 억지로 끊지 않고, 코드 구현처럼 명확한 태스크에 집중적으로 쓴다.</li>
<li><strong>학습 전에 목표 맵을 간단히 그린다.</strong> &quot;이 개념에서 나는 ○○만 알면 된다&quot;는 경계를 미리 정하고 시작한다. 찜찜함이 남더라도 주말을 활용하거나 다음으로 미루는 연습을 한다.</li>
<li><strong>아침 루틴에 당일 일정 리마인드를 넣는다.</strong> 전날 저녁에 다음 날 고정 일정을 한 번 훑고, 당일 아침에 한 번 더 확인한다.</li>
</ul>
]]></description>
        </item>
        <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>
    </channel>
</rss>