<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>camille_protocol.log</title>
        <link>https://velog.io/</link>
        <description>앞으로 꾸준히 나아가기</description>
        <lastBuildDate>Thu, 13 Nov 2025 21:22:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>camille_protocol.log</title>
            <url>https://velog.velcdn.com/images/camille_protocol/profile/e28768be-0748-437a-b1b3-156ffa0d3354/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. camille_protocol.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/camille_protocol" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[다녀왔습니다, DAN25]]></title>
            <link>https://velog.io/@camille_protocol/%EB%8B%A4%EB%85%80%EC%99%94%EC%8A%B5%EB%8B%88%EB%8B%A4-DAN25</link>
            <guid>https://velog.io/@camille_protocol/%EB%8B%A4%EB%85%80%EC%99%94%EC%8A%B5%EB%8B%88%EB%8B%A4-DAN25</guid>
            <pubDate>Thu, 13 Nov 2025 21:22:01 GMT</pubDate>
            <description><![CDATA[<h2 id="dan25-후기">DAN25 후기</h2>
<p>부스트캠프 멤버십에 합류한지도 벌써 3개월이라는 시간이 넘었는데, 어느날 갑자기 뜻밖의 소식을 들었습니다.</p>
<blockquote>
<p>오는 11월 6 - 7일, 팀네이버 컨퍼런스 DAN25가 개최됩니다. 네이버 측에서 부스트캠프 웹·모바일 캠퍼 여러분들이 기술적인 전문성과 인사이트를 가진 개발자로 성장하는 데 도움이 될 수 있도록 DAN25 티켓을 제공해주셨어요. 👏
DAN25에 참가하여 영감과 인사이트를 얻고 싶은 캠퍼라면 아래 내용을 확인 후 신청해주세요.</p>
</blockquote>
<p>네이버의 컨퍼런스인 <a href="https://dan.naver.com/25">DAN25</a>에 참석할 캠퍼를 모집한다는 소식...!</p>
<p>늘 가고 싶었던 컨퍼런스였는데, 이번 기회에 다녀올 수 있을 것 같아서 메시지를 확인하자마자 바로 신청했고 당첨되어서 다녀오게 됐습니다!</p>
<h2 id="부스">부스</h2>
<p>DAN25는 세션 외에도 네이버의 선행 기술과 서비스가 집약된 다양한 체험 부스로 풍성했습니다. 특히, 단순히 굿즈를 제공하는 차원을 넘어 <strong>기술을 직접 체험</strong>할 수 있는 부스들이 깊은 인상을 남겼습니다. 글에는 다 담지 못할 정도로 정말 많은 부스들이 있었고, 굿즈도 정말 다양해서 가방을 안 들고 갔다면 후회할 정도였어요.</p>
<p>가장 눈에 띄었던 곳은 <strong>XR 기반 콘텐츠 체험 부스</strong>와 <strong>버추얼 프로덕션(Virtual Production)을 활용한 이머시브 미디어 부스인 &#39;비전 스테이지&#39;</strong> 였습니다. 비전 스테이지에서는 가상과 현실이 매끄럽게 결합된 몰입형 콘텐츠를 눈으로 확인해볼 수 있었는데, 사진에 보이는 것처럼 다양한 배경을 제공하는 세트가 준비되어 있어서 촬영감독님의 큐 사인에 맞춰서 영상을 찍어 볼 수 있었습니다. 물론 긴장한 탓에 뚝딱거린 건 덤입니다 😆</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/e2d5775d-db17-4061-9690-302f0d5847b5/image.jpg" alt=""></p>
<p>부스 체험을 마친 후에는 시간표대로 원하는 세션을 골라서 들었습니다. 전체 세션은 <a href="https://dan.naver.com/25/sessions#DAY2">DAN25 세션 홈페이지</a>를 참고해 주세요. 세션별 자료는 세션 제목 링크를 클릭하면 확인할 수 있습니다.</p>
<h2 id="익숙함을-새로움으로-네이버-카페-경험-설계"><a href="https://dan.naver.com/25/sessions/718">익숙함을 새로움으로! 네이버 카페 경험 설계</a></h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/dde2a387-7b51-475d-a56f-9eda7e165a6b/image.jpg" alt=""></p>
<blockquote>
<p>국내 1등 커뮤니티 네이버 카페는 매니저와 카페 멤버들과 함께 성장하는 서비스입니다. 오래된 것은 늘 나쁜 사용성일까요? 새로운 것이라고 다 좋은 사용성일까요? 사용자들의 애정 어린 공간을 바꾼다는 것은 어려운 것은 물론, 책임감이 필요합니다. 카페 서비스의 UX/UI 개선 과정을 통하여 익숙함과 새로움을 잘 담는 좋은 변화는 과연 무엇일까를 찾아가는 프로덕트 디자이너들의 경험을 공유합니다.</p>
</blockquote>
<h3 id="20년-역사의-서비스-익숙함-속에-새로움을-담다">20년 역사의 서비스, &#39;익숙함&#39; 속에 &#39;새로움&#39;을 담다</h3>
<p>20년이 넘는 세월 동안 월 3천만 명의 사용자가 이용하고 1천만 개 이상의 카페가 존재하는 네이버 카페. 이 세션은 이처럼 거대한 <strong>&#39;익숙함&#39;</strong>을 가진 서비스를 어떻게 <strong>&#39;새로움&#39;</strong>으로 혁신했는지에 대한 프로덕트 디자이너들의 치열한 고민을 엿볼 수 있었습니다. 단순히 최신 트렌드를 따르는 것이 아닌, 사용자들의 애정 어린 공간을 바꾸는 것에 따르는 <strong>책임감</strong>이 세션 전반에 걸쳐 느껴졌습니다.</p>
<p>개선의 출발점은 <strong>&#39;기본기&#39; 다지기</strong>였습니다. 비효율적인 수동 작업을 줄이고자 <strong>디자인 시스템</strong>을 구축하여 컴포넌트와 UX/UI를 체계화한 부분이 인상깊었습니다. 거대한 레거시 서비스라도 안정적인 성장을 위해서는 반드시 거쳐야 할 기초 다지기의 과정이었던 것입니다.</p>
<h3 id="데이터와-네이버-생태계를-활용한-성장기">데이터와 네이버 생태계를 활용한 &#39;성장기&#39;</h3>
<p>2023년부터 본격화된 <strong>&#39;성장기&#39;</strong>에서는 방대한 <strong>카페 콘텐츠</strong>에 주목했습니다. 이전에는 단순히 각 카페로 향하는 길목 역할만 하던 카페 홈을, 많은 사람들이 자주 이야기하는 내용을 <strong>이슈 및 트렌드 키워드</strong>로 과감하게 시각화하여 전면에 배치했습니다. 관심사 기반 커뮤니티의 특성을 살려 콘텐츠 노출과 명확한 인터랙션을 통해 사용자의 탐색 경험을 개선하려는 노력이 돋보였습니다. 또한, 가입 및 등업 과정의 불편함을 <a href="https://brunch.co.kr/@joojun/136"><strong>UX 라이팅</strong></a>으로 해소한 점은 작은 변화가 큰 사용자 경험 개선을 가져온 좋은 사례였다고 생각합니다.</p>
<p>특히 <strong>&#39;쓰는 경험(거래글 에디터 개선)&#39;</strong>에 대한 부분이 가장 인상깊었는데요, 사용자가 키워드 입력 후 상품을 검색하고 선택하면 <strong>상품 정보가 자동으로 입력</strong>되도록 설계하여 거래글 작성의 복잡성을 획기적으로 낮추거나, 나의 구매 내역을 연동하여 판매글을 쉽게 작성하게 하는 플로우는 네이버 생태계의 강점을 극대화한 설계라고 생각했습니다.</p>
<p>거래 에디터 설계 시, <strong>Depth가 많아질수록 사용자가 겪는 불편함이 커진다</strong>는 점을 인지하고 간략한 플로우를 유지하려 노력했다는 점 역시, 서비스 설계자로서 <strong>사용자 여정을 최소화</strong>하려는 섬세한 고민이 돋보이는 부분이었다고 느꼈습니다.</p>
<p>마지막으로 <strong>AI 기술</strong>을 활용하여 <strong>보기 좋은 판매 이미지 생성</strong>이나 <strong>사진 분석 기반의 상품 판매글 등록</strong>을 쉽게 하겠다는 NEXT 스텝을 통해, 앞으로의 네이버 카페가 더더욱 기대할 수 있었습니다.</p>
<h2 id="mobile-한계를-넘어서---mobile-game--mobile-vtuber-live-streaming"><a href="https://dan.naver.com/25/sessions/716">Mobile 한계를 넘어서 - Mobile Game &amp; Mobile VTuber Live Streaming</a></h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/9e772a87-9d44-4a4c-86f8-a9774e83f547/image.jpg" alt=""></p>
<blockquote>
<p>PRISM Live Studio는 PC와 Mobile 환경 모두에서 Live Streaming을 지원하는 도구형 애플리케이션입니다. 이 중에서도 이번 세션에서는 PRISM Live Studio Mobile, 즉 모바일 Live Streaming 도구에 대해 이야기하려고 합니다.</p>
<p>모바일 환경은 본질적으로 PC 환경보다 제약이 많을 수밖에 없습니다. 그러나 사용자들은 점점 더 모바일에서도 PC 수준의 기능과 성능을 기대하고 있습니다. 이에 따라 PRISM Live Studio Mobile은 모바일의 한계를 넘어서는 기능과 기술을 끊임없이 고민하고 있습니다.</p>
<p>특히, 모바일 환경과는 어울리지 않는다고 여겨질 수 있는 두 가지 영역—Game Live Streaming과 VTuber Live Streaming—에서 PRISM은 도전적인 시도를 이어가고 있습니다. 이번 세션에서는 이 두 가지 기능을 모바일에서 구현하기 위해 적용된 기술과 방법들을 공유하고자 합니다.</p>
</blockquote>
<h3 id="안드로이드-개발자의-시선으로-바라본-모바일-스트리밍-서비스">안드로이드 개발자의 시선으로 바라본 모바일 스트리밍 서비스</h3>
<p>평소 스트리밍 서비스에 관심이 많고 안드로이드 개발을 공부하는 입장에서, 세션의 제목부터 흥미가 생겨서 듣지 않으면 정말 후회할 것 같다는 생각에 누구보다 빠르게 자리에 앉았습니다.</p>
<p>스트리밍이 이루어지는 과정에 대해서 간단하게 정리하자면, <strong>&#39;영상 프레임 획득 → 오버레이(후원 이펙트, 채팅 등) 합성 → 인코딩 → 네트워크 전송&#39;</strong>이라는 과정을 거치게 되고, 이 과정에서 많은 리소스를 소모합니다. 특히 스트리밍 환경이 점점 발전함에 따라서, 사용자들은 PC 수준의 4K 화질과 다양한 오버레이를 요구하게 됐고, 모바일의 기술 수준을 뛰어넘는 부하가 발생하는 경우가 잦아졌습니다.</p>
<h3 id="게임-스트리밍-floating-ui와-화면-캡처-후처리">게임 스트리밍: Floating UI와 화면 캡처 후처리</h3>
<p>가장 중요한 사용자 니즈는 <strong>&quot;게임 화면을 가리지 마라&quot;</strong> 였습니다. PC 환경처럼 게임 화면을 캡처한 후, 그 위에 오버레이를 합성하는 방식을 모바일에도 적용하는 것이 핵심이었습니다. 이러한 문제를 처리하는 방법을 크게 Android와 iOS에서 나누어서 설명했는데, 안드로이드에서는 다음과 같은 방법으로 문제를 해결할 수 있었습니다.</p>
<ol>
<li><strong>화면 밖 렌더링:</strong> 일반적인 안드로이드 UI(View와 Compose)는 화면에 붙어 있어야 렌더링되지만, 라이브 방송 중 내 앱이 백그라운드에 있어도 오버레이를 렌더링해야 했습니다.</li>
<li><strong>Floating UI 활용:</strong> 이를 위해 <strong>백그라운드 서비스</strong>를 이용해 <strong>다른 앱 위에 그리기</strong> 권한으로 View를 부착하고, 해당 오버레이의 <strong>알파 값을 0</strong>으로 주어 화면에 보이지 않게 처리했습니다.</li>
<li><strong>텍스처 전환:</strong> 이 보이지 않는 오버레이를 텍스처로 전환하여 스트리밍 프레임에 합성함으로써, 사용자의 게임 시야를 가리지 않으면서도 오버레이 기능을 구현할 수 있었습니다.</li>
</ol>
<h3 id="버튜버-스트리밍-성능-한계와-범용성-확보">버튜버 스트리밍: 성능 한계와 범용성 확보</h3>
<p>고성능 PC의 전유물이었던 버튜버 스트리밍을 모바일로 가져오는 도전도 인상깊었습니다. 특히 안드로이드 환경에서는 얼굴을 스캔하는 <strong>페이셜 트래킹</strong>이 큰 문제였습니다. 구글의 <a href="https://github.com/google-ai-edge/mediapipe"><strong>Mediapipe</strong></a>가 있지만, 플래그십 기기에서도 15fps라는 낮은 프레임으로 프로덕트 레벨에 부적합했습니다.</p>
<p>PRISM은 <strong>제페토의 얼굴 인식 솔루션</strong> 등을 도입하여 성능 문제를 해결하고, 2D 이미지 기반의 <a href="https://tips.clip-studio.com/ko-kr/articles/10101"><strong>PNGtuber</strong></a>와 <strong>3D (<a href="https://www.navercorp.com/media/pressReleasesDetail?seq=32162">VRM</a> 포맷)</strong>를 동시에 지원하여 모바일 유저의 접근성을 높였습니다.</p>
<p>하나의 목표를 달성하기 위해서 정말 많은 문제 상황들을 마주했고, 이러한 문제 상황을 다양한 방법으로 해결해 나가는 과정이 인상깊었던 세션이었습니다.</p>
<h2 id="작은-도전들이-만든-큰-의미-모두를-위한-네이버-뉴스"><a href="https://dan.naver.com/25/sessions/695">작은 도전들이 만든 큰 의미, 모두를 위한 네이버 뉴스</a></h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/ae9f482e-5af6-400b-8dc9-1c40b3045829/image.jpg" alt=""></p>
<blockquote>
<p>네이버 뉴스는 지난 20여 년 동안 국민 대다수가 이용하는 서비스로 자리 잡으며, 사회적 역할에 집중해왔습니다. 그리고 최근 사회적 재난과 급격한 뉴스 소비 패턴의 변화를 겪으며, 작지만 의미있는 변화를 시도하고 있습니다. 이번 세션에서는 모두를 위한 퍼블릭 미디어 플랫폼으로서 뉴스 서비스가 만든 의미있는 변화와 실험들을 소개합니다.</p>
</blockquote>
<h3 id="신뢰를-넘어-사회적-역할에-집중하는-뉴스-플랫폼">신뢰를 넘어, 사회적 역할에 집중하는 뉴스 플랫폼</h3>
<p>이번 세션에서는 25년 역사의 네이버 뉴스가 신뢰성과 공정함을 지키는 <strong>&#39;신중함&#39;</strong>의 단계를 넘어, <strong>&#39;변화&#39;</strong>를 통해 뉴스를 <strong>더 쉽고 편하게</strong> 전달하기 위해 고심하는 과정을 엿볼 수 있었습니다. 단순히 뉴스를 제공하는 것을 넘어, <strong>퍼블릭 미디어 플랫폼</strong>으로서 사회적 역할을 강화하려는 노력이 인상 깊었습니다.</p>
<h3 id="긴급-상황-1초라도-더-빠른-정보-전달">긴급 상황: 1초라도 더 빠른 정보 전달</h3>
<p>네이버는 <strong>재난 상황</strong>에서 뉴스 특보에 대한 사용자의 높은 니즈(후속 보도, 실시간 정보)에 대응하기 위해 <strong>&#39;뉴스특보 라이브&#39;</strong>를 10월부터 도입했습니다. 실시간 라이브 영상, 관련 기사/영상 클립을 상단에 집중적으로 모아 전달하는 등 <strong>재난 상황 특화 UX</strong>를 실험하고 있었습니다. 특히 사용자가 현장 사진이나 질문을 공유하는 <strong>실시간 제보 기능</strong>은, 재난 시 정보의 일방적인 전달이 아닌 <strong>양방향 소통</strong>을 통해 신속성을 높이려는 의미 있는 시도였다고 느꼈습니다.</p>
<h3 id="모두를-위한-뉴스-접근성-혁신">모두를 위한 뉴스: 접근성 혁신</h3>
<p>개인적으로 가장 흥미로웠던 부분은 <strong>접근성</strong>을 높이기 위한 시도였습니다. 복잡하고 텍스트 양이 많은 뉴스 환경에서, ATM의 큰 글씨 보기 모드에서 영감을 받아 <strong>&#39;쉬운 뉴스판&#39;</strong>을 도입한 점이 흥미로웠습니다.</p>
<p>네이버는 시각 정보 습득에 어려움을 겪는 사용자들을 위해 명도 대비, 텍스트 크기, 구조 복잡성, 시각 정보(아이콘)의 네 가지 핵심 요소를 개선했습니다.</p>
<ul>
<li><strong>쉬운 뉴스판/기사 뷰:</strong> 한 화면에 하나의 기사, 커진 글자 크기, 강한 명도 대비를 적용하여 가독성을 극대화했습니다.</li>
<li><strong>기능 버튼 개선:</strong> 기존에 상호작용 과정에서 화면에서 사라질 수 있는 버튼들을 <strong>바텀 시트</strong>에 고정 배치하고, <strong>아이콘과 텍스트를 병행</strong> 제공하여 의미를 명확히 함으로써, 접근성이 필요한 사용자들이 핵심 기능에 언제든 쉽게 접근할 수 있도록 설계했습니다.</li>
</ul>
<h3 id="새로운-실험-소비-동선-단축과-ai-요약">새로운 실험: 소비 동선 단축과 AI 요약</h3>
<p>접근성을 넘어 뉴스 소비의 효율성을 높이는 실험들도 진행 중이었습니다.</p>
<ol>
<li><strong>멀티 뷰어:</strong> 텍스트, 영상, 클립 등 다양한 포맷의 뉴스를 <strong>이동 없이 이슈 중심으로</strong> 한 공간에서 소비하게 하여 복잡한 동선을 정리했습니다.</li>
<li><strong>요약 뷰어:</strong> 숏폼 콘텐츠에 익숙한 사용자를 위해 <strong>AI를 활용하여</strong> 기사의 핵심을 빠르게 파악할 수 있는 <strong>요약 뷰</strong>를 제공합니다.</li>
<li><strong>브리핑 뷰어 (실험 중):</strong> 쏟아지는 기사 속에서 사용자의 관심사에 맞는 5~10개의 기사를 <strong>음성 브리핑</strong>으로 간추려 주는 기능은 오늘날 정보 과부하 시대에 <strong>큐레이션의 가치</strong>를 극대화하는 매력적인 해결책으로 다가왔습니다.</li>
</ol>
<p>네이버 뉴스는 사용자의 <strong>&#39;필요&#39;</strong>와 <strong>&#39;편의&#39;</strong>를 중심에 두고 끊임없이 변화하며, 디지털 시대의 공정한 정보 전달자로서 역할을 다하려는 <strong>작지만 의미있는 도전</strong>들을 이어가고 있었습니다.</p>
<h2 id="후기">후기</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/0416a630-f0a1-414b-bf72-5cdfa69daf55/image.jpg" alt=""></p>
<p>이번 DAN25 컨퍼런스에 참여하며 네이버가 단순히 기존의 거대 서비스를 유지하는 것에 만족하지 않고, AI, XR, 접근성 혁신 등 미래를 위한 다양하고 깊이 있는 시도를 끊임없이 이어가고 있음을 체감했습니다. 익숙함 속의 혁신을 고민하는 카페의 UX 설계부터, 모바일 환경의 제약을 창의적으로 극복하는 라이브 스트리밍 기술, 그리고 사회적 책임을 다하는 뉴스 접근성 개선까지, 모든 세션에서 기술적 전문성과 사용자 중심의 철학을 동시에 엿볼 수 있었습니다. 개발자로서 큰 영감을 얻었으며, 벌써부터 내년 DAN26에서 어떤 새로운 기술과 인사이트가 공개될지 기대됩니다. 이처럼 소중한 배움의 기회를 제공해주신 부스트캠프 측에 다시 한번 깊이 감사드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[부스트캠프 웹・모바일 10기 챌린지 후기]]></title>
            <link>https://velog.io/@camille_protocol/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9-%EB%AA%A8%EB%B0%94%EC%9D%BC-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@camille_protocol/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9-%EB%AA%A8%EB%B0%94%EC%9D%BC-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 17 Aug 2025 07:39:50 GMT</pubDate>
            <description><![CDATA[<h2 id="프롤로그-무엇을-위해-4주를-불태웠을까">프롤로그: 무엇을 위해 4주를 불태웠을까?</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/fa082178-3a05-4a5b-9a40-986e6a218aac/image.png" alt="부스트캠프 챌린지"></p>
<p>부스트캠프 웹・모바일 10기의 챌린지 과정과 함께한 4주라는 시간이 끝났습니다. 많은 밤을 새우고, 때로는 끼니를 거르며, 에너지 드링크로 버텼던 치열했던 시간이 막을 내렸습니다. 지난 경험이 기억에서 사라지기 전, 스스로에게 묻지 않을 수 없었습니다. &#39;나는 무엇을 위해 4주를 불태웠는가?&#39; 라고 말입니다.</p>
<p>챌린지 이전의 저는 정제된 커리큘럼에만 익숙했던 수동적인 학습자였습니다. 성장에 대한 열망은 있었지만 그 방향은 막연했고, 마음 한편에는 늘 ‘내가 여기에 맞는 사람일까?’ 하는 불안감이 자리 잡고 있었습니다. 저에게 학습은 성장을 위한 과정이라기보다, 결과와 증명에 대한 압박감과 불안을 잠재우기 위한 수단에 가까웠습니다.</p>
<p>그래서 이 글은 4주 전의 저에게 보내는 하나의 답변입니다. 지난 4주간의 챌린지는 저에게 무엇을 남겼을까요? 이 글을 통해 &#39;문제 해결력&#39;이라는 단어를 어떻게 재정의하게 되었는지, AI와 동료라는 두 개의 거울을 통해 무엇을 보았는지, 그리고 이 모든 과정 끝에 어떤 새로운 다짐을 하게 되었는지 이야기하려 합니다. 이번 회고는 단순히 새로운 지식을 얻은 기록이 아니라, 어떤 문제 앞에서도 주도적으로 길을 찾아갈 수 있는 ‘나만의 성장 나침반’을 얻게 된 과정에 대한 이야기입니다.</p>
<h2 id="1-나의-성장-엔진-문제-해결력을-재정의하기">1. 나의 성장 엔진, 문제 해결력을 재정의하기</h2>
<p><img src="https://images.unsplash.com/photo-1539627831859-a911cf04d3cd?q=80&w=1471&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="Olav Ahrens Røtne Cube"></p>
<p>부스트캠프는 끊임없이 ‘문제 해결력’에 대해 질문을 던졌습니다. <strong>챌린지 이전의 저에게 문제 해결력이란 그저 ‘주어진 과제를 코드로 구현해내는 기술’</strong> 이었습니다. 하지만 4주간의 챌린지는 그 정의를 송두리째 바꾸어 놓았습니다. 이제 저에게 문제 해결력이란, 단순히 코드를 작성하는 능력이 아니라 <strong>불확실한 상황 속에서 스스로 문제를 정의하고, 전략적으로 학습하며, 작은 성공을 쌓아나가는 과정을 통해 끝내 답을 찾아가는 종합적인 힘</strong>을 의미합니다. 이 문제 해결력이 바로 챌린지를 통해 얻은 저의 가장 중요한 성장 엔진입니다.</p>
<h3 id="1-1-결과에서-과정으로-완벽한-결과라는-강박에서-벗어나기">1-1. 결과에서 과정으로: 완벽한 결과라는 강박에서 벗어나기</h3>
<p>챌린지 초반, 저를 가장 괴롭혔던 것은 ‘결과’에 대한 강박이었습니다. ‘구현을 모두 마치지 못하면 다음 과정에 가지 못할 거야’라는 <strong>막연한 불안감</strong>, &#39;처음부터 완벽하게 설계해야 한다&#39;라는 <strong>강박</strong>은 저를 <strong>완벽한 설계라는 함정</strong>에 빠뜨렸습니다. 머릿속으로 이상적인 구조를 그리는 데 대부분의 시간을 쏟았고, 그 결과 정작 구현에 들어갈 시간이 부족해 핵심 기능을 완성하지 못하는 실패를 반복했습니다. 당시의 저는 결과물을 제출하는 것에만 급급했고, 그 과정에서 진정한 학습의 본질을 놓치고 있었습니다.</p>
<p>이 악순환을 끊어낸 것은 3주차에 경험한 하나의 전환점이었습니다. 해야만 한다는 강박으로 지새우던 이전의 밤들과 달리, <strong>막혔던 문제를 해결하는 과정 자체가 즐거워서 시간 가는 줄 모르고 몰입했던 경험</strong>이었습니다. 그제서야 저는 깨달았습니다. 저를 앞으로 나아가게 하는 가장 강력한 원동력은 결과에 대한 압박감이 아니라, <strong>과정 자체를 즐기는 마음</strong>에 있다는 것을 말입니다. <a href="https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0">1주차 회고</a>에서 ‘결과’와 ‘증명’에서 ‘과정’과 ‘학습’으로 목표를 바꾸겠다고 다짐했던 것이 비로소 몸으로 체감되는 순간이었습니다.</p>
<h3 id="1-2-거대한-벽-앞에서-작은-성공을-쌓아-올리는-상향식-접근법">1-2. 거대한 벽 앞에서: 작은 성공을 쌓아 올리는 상향식 접근법</h3>
<p>과정에 집중하기로 마음먹었지만, 여전히 모호하고 거대한 미션은 그 자체로 높은 벽이었습니다. 완벽주의라는 함정에서 벗어나 이 벽을 넘기 위해, 저는 <strong>거대한 미션을 작은 성공의 합으로 나누는</strong> 상향식 전략을 점진적으로 발전시켜 나갔습니다.</p>
<p>초기에는 미션을 기능 단위로 쪼개 짧은 ‘학습-구현’ 사이클을 반복하는 실험을 했습니다. 이는 챌린지 과정에서 마스터 JK님이 남겨주신 조언에서 큰 영감을 받았습니다. JK님은 <strong>“덩어리가 크기 때문에 작은 부분을 찾아내서 작게작게 구현해보는 게 필요합니다. 그래서 작은 목표를 자주 실행하면서 성취감도 쌓아야 합니다.”</strong> 라고 말씀하셨고, 저는 이 조언을 바탕으로 문제 해결 전략을 새롭게 세울 수 있었습니다.</p>
<p>이 전략은 4주차에 이르러 <strong>미니 튜토리얼</strong> 방식으로 구체화되었습니다. 하나의 큰 기능을 마치 튜토리얼을 따라 하듯, <strong>지금 당장 실행하고 결과를 볼 수 있는 가장 작은 단위</strong>로 계속해서 쪼갰습니다. 예를 들어 ‘네트워크 통신 구현’이라는 막연한 목표 대신, ‘1. 소켓 객체 생성하기’, ‘2. 서버에 연결 요청 보내기’처럼 <strong>즉각적인 피드백이 가능한 단위로 체크포인트를 세분화</strong>했습니다. 이 방법을 통해 저는 거대한 문제를 마주했을 때 막막함 대신 즉각적인 성취감을 얻으며 다음 단계로 나아갈 동력을 얻을 수 있었습니다.</p>
<h3 id="1-3-가장-중요한-것-회고를-통한-어제의-나를-넘어서는-법">1-3. 가장 중요한 것: 회고를 통한 어제의 나를 넘어서는 법</h3>
<p>이 모든 생각과 전략의 변화는 결코 우연히 일어나지 않았습니다. 그 중심에는 매주 진행했던 <strong>주간 학습 피드백</strong>과 개인적으로 블로그에 발행했었던 <strong>개인적인 주간 회고</strong>가 있었습니다. 저에게 주간 학습 피드백은 단순한 기록을 넘어, 모든 실행과 협업의 과정을 의미 있는 성장으로 완성시켜준 <strong>성장의 나침반</strong>이었습니다.</p>
<p>1주차에 ‘이게 챌린지에서 원하는 성장 방향인가?’라는 근본적인 질문을 던진 순간부터, <a href="https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0">3주차에 KPT 방법론을 적용해 체계적으로 자신을 돌아본 경험</a>까지, 회고는 제 성장의 모든 변곡점과 함께했습니다. 매주 회고를 작성하며 저는 실패의 원인을 분석하고, 동료에게서 얻은 교훈을 되새겼으며, 다음 주를 위한 구체적인 전략을 세울 수 있었습니다.</p>
<p>챌린지 과정에서 <strong>경쟁해야 할 상대는 다른 캠퍼들이 아니라 오직 어제의 나</strong> 뿐이었습니다. 그리고 회고는 어제의 나를 객관적으로 돌아보고, 오늘의 나를 점검하여, 더 나은 내일의 나로 나아갈 수 있게 해주는 가장 강력하고 중요한 도구였습니다.</p>
<h2 id="2-성장을-비추는-두-개의-거울-ai와-동료">2. 성장을 비추는 두 개의 거울: AI와 동료</h2>
<p><img src="https://images.unsplash.com/photo-1521737604893-d14cc237f11d?q=80&w=1484&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="Annie Spratt Colleague"></p>
<p>챌린지에서의 성장은 결코 혼자만의 힘으로 이루어지지 않았습니다. 그 과정에는 저의 상태를 비추고 더 나은 방향으로 이끌어준 두 개의 거울이 있었습니다. 바로 AI와 동료입니다. 처음에는 그저 답을 구하는 도구, 혹은 단순한 경쟁자로만 여겼던 이 두 존재는, 4주간의 상호작용을 통해 저의 가장 든든한 성장 파트너가 되어주었습니다.</p>
<h3 id="2-1-ai-답안-생성기에서-사고-확장-파트너로">2-1. AI: 답안 생성기에서 사고 확장 파트너로</h3>
<p>챌린지 초반, AI는 저에게 가장 손쉬운 탈출구이자 가장 위험한 함정이었습니다. 구현에 쫓기다 보니 충분한 고민 없이 AI에 의존했고, 이는 결국 1주차 미션 해결 과정에서 제 <strong>생각 없는 질문에 AI가 덩달아 바보가 되는 악순환</strong>으로 이어졌습니다. 이 실패를 통해 AI의 가치는 코드 생성이 아닌, 저의 사고 과정을 강화하는 데 있다는 것을 깨달았고, 이후 AI의 역할을 의식적으로 바꾸며 학습과 성장의 파트너로 활용했습니다.</p>
<p>AI 활용법은 세 단계를 거쳐 진화했습니다. 첫째, AI를 <strong>학습 내비게이터</strong>로 활용했습니다. 챌린지의 방대한 학습량 앞에서 길을 잃지 않기 위해, AI에게 &quot;이번 미션을 해결하기 위해 학습해야 할 필수 지식과 그 깊이는 어느 정도인가?&quot;라고 질문했습니다. 이를 통해 무한정 깊게 파고드는 이른바 ‘야크 털 깎기’를 방지하고, 지금 단계에서 가장 중요한 핵심 지식에 집중하며 학습 효율을 높일 수 있었습니다.</p>
<p>둘째, 생각의 근육을 키우는 <strong>설계 스파링 파트너</strong>로 삼았습니다. 중요한 원칙은, <strong>반드시 제가 먼저 충분히 고민하여 설계 초안을 만든 후에 AI를 활용</strong>한다는 것이었습니다. 저의 설계안을 AI에게 제시하며 “이 요구사항을 구현하기 위해서 <code>List</code>와 <code>LinkedHashMap</code> 중 어떤 자료구조가 좋을지 장단점과 함께 설명해 줘”, “이러한 기능을 추가한다면 어떤 트레이드오프가 발생할까?”와 같이 구체적인 질문을 던졌습니다. 이러한 <strong>설계 랠리</strong>를 통해 하나의 정답에 갇히지 않고 더 견고하고 유연한 설계를 고민하는 훈련을 할 수 있었습니다.</p>
<p>마지막으로 챌린지 후반부에는 AI의 역할을 한 단계 더 발전시켜, 제 문제 해결 과정 자체를 진단하는 <strong>메타인지 코치</strong>로 활용했습니다. 제가 작성한 코드를 리뷰받는 것을 넘어, “이번 미션을 해결한 저의 접근 방식에서 보완할 점은 없을까?”와 같이 저의 사고 습관 자체에 대한 피드백을 구했습니다. 그 결과 AI는 저의 약점인 ‘초기 단계의 과도한 설계’ 문제를 정확히 지적해주었고, 이는 저의 문제 해결 습관과 메타인지를 개선하는 결정적인 계기가 되었습니다.</p>
<p>결론적으로 저는 AI를 정답을 주는 해결사가 아닌, 더 나은 질문을 하도록 유도하는 파트너로 활용하는 법을 배웠습니다. AI는 제 생각을 대신해주는 존재가 아니라, 오히려 더 깊고, 넓고, 날카롭게 생각하도록 돕는 최고의 훈련 도구였습니다.</p>
<h3 id="2-2-동료-보이지-않는-경쟁자에서-함께-걷는-동반자로">2-2. 동료: 보이지 않는 경쟁자에서 함께 걷는 동반자로</h3>
<p><strong>챌린지 초반의 동료는 저에게 불안감을 증폭시키는 존재</strong>였습니다. 슬랙에서 먼저 미션을 끝내고 토론하는 동료들을 보며 조급함에 휩싸였고, 저를 제외한 피어 그룹원 모두가 전공자라는 사실에 ‘나만 뒤처지는 건 아닐까’ 하는 불안감에 시달리기도 했습니다.</p>
<p>하지만 여러 결정적인 경험들이 동료를 경쟁자가 아닌 동반자로 다시 보게 만들었습니다. 2주차에 한 동료가 “미션을 다 끝내지 못하더라도 밤 10시에는 무조건 자겠다”고 선언하고 실천하는 모습은, 비효율적인 밤샘 대신 <strong>‘용기 있는 휴식’</strong> 의 중요성을 깨닫게 해주었습니다. 그리고 모두가 어려워했던 미션에서 저뿐만 아니라 대다수의 동료가 힘들어했다는 사실을 알게 되었을 때, 불필요한 비교에서 벗어나 불안감으로부터 해방될 수 있었습니다.</p>
<p>이러한 인식의 전환은 챌린지 과정에 마련된 다양한 소통 채널을 통해 더욱 단단해졌습니다. 매일 미션이 공개될 때마다 줌 라운지에 모여, 카메라를 켜고 미션을 해결하던 <strong>모각코</strong> 시간은 지칠 때 다시 나아갈 힘을 주는 중요한 원동력이 되었습니다. 또한, 매주 금요일 진행된 <strong>릴레이 프로젝트</strong>에서는 다른 동료들이 AI를 어떻게 활용하여 문제를 해결하는지 엿보며, 저 혼자서는 생각하지 못했을 새로운 접근법을 배울 수 있었습니다.</p>
<p>동료라는 거울을 통해 성장하는 기회는 매일 진행되는 ‘피어 피드백’ 시간에도 있었습니다. 다만 제한된 시간 안에 여러 동료의 코드를 깊이 있게 들여다보는 것은 또 다른 도전이었습니다. 저는 이 현실적인 제약을 극복하고자 저만의 기준을 세웠습니다. 모든 것을 완벽하게 분석하려는 대신, <strong>‘동료 한 명에게서 배울 점 하나는 반드시 찾기’</strong> 라는 작은 목표를 세워 피드백에 임했습니다. 이러한 접근법은 시간적 한계 속에서도 동료의 장점을 꾸준히 흡수하고, 저 또한 의미 있는 기여를 할 수 있게 만드는 효과적인 전략이 되었습니다.</p>
<p>가장 큰 변화는 3주차부터 추가된 짝 설계 미션에서 일어났습니다. 첫 짝 설계 미션에서 저는 스스로를 ‘슬로우 스타터’라 여기며 저보다 잘 하는 짝에게 민폐를 끼치지 않아야 한다는 부담감에 짓눌려 있었습니다. 그 결과, “제가 이 부분에 대해 잘 몰라서…”, “00님은 어떻게 생각하시는지…”와 같이 스스로를 낮추고 회피하는 소극적인 태도를 보였습니다. 해결책을 제시하지 못하고 자책만 하는 수동적인 모습은 결국 세 번의 미팅 중 마지막이 되어서야 겨우 짝의 말을 이해하고 원활하게 설계에 참여하는, 만족스럽지 못한 결과로 이어졌습니다.</p>
<p>이를 통해 저는 진정한 협업의 의미에 대해 깊이 반성하게 되었습니다. 그리고 4주차 짝 설계에서는 의식적으로 다른 태도를 취했습니다. 먼저 제 의견을 적극적으로 공유했을 뿐만 아니라, 현재 학습이 부족한 부분에 대한 이해도를 솔직하게 드러내며 모르는 것을 더 이상 숨기지 않았습니다. 놀랍게도, 이러한 <strong>솔직함은 약점이 아닌 신뢰의 기반이 되었습니다</strong>. 오히려 더 편안한 분위기에서 다양한 아이디어를 나눌 수 있었고, 3주차보다 훨씬 만족스러운 설계를 함께 완성할 수 있었습니다. 다양한 동료와의 경험을 통해, 동료는 단순히 지식을 교환하는 대상을 넘어 저의 태도를 비추고 성장을 이끄는 거울이라는 것을 깨닫게 되었습니다.</p>
<h2 id="3-챌린지의-마지막-챕터-지속-가능한-성장에-대하여">3. 챌린지의 마지막 챕터: 지속 가능한 성장에 대하여</h2>
<p><img src="https://images.unsplash.com/photo-1565049981953-379c9c2a5d48?q=80&w=1469&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="Clint Patterson Charging"></p>
<p>지난 4주간, 저는 ‘어제의 나’를 뛰어넘기 위해 스스로를 끊임없이 몰아붙였습니다. ‘멤버십에 꼭 합격하고 싶다’는 열망과 ‘주어진 모든 것을 활용해 최선을 다해보자’는 열정이 합쳐져, 제 모든 것을 챌린지에 쏟아부었습니다. 문제 해결의 의미를 깨닫고, 과정의 즐거움을 발견하며 개발자로서 단단한 토대를 쌓았다고 생각했습니다.</p>
<p>그 과정에서 저의 몸은 계속해서 경고 신호를 보내고 있었습니다. 밤을 새우는 날이 잦아졌고, 끼니는 거르기 일쑤였으며, 부족한 에너지는 에너지 드링크로 채워 넣었습니다. 극심한 스트레스와 불규칙한 생활 속에서 몸이 지속적인 경고를 보내고 있었지만, 저는 애써 그 신호를 무시했습니다. 지금은 달려야 할 때라고, 이 시간만 버티면 된다고 스스로를 다독였습니다.</p>
<p>결국 몸은 가장 극적인 순간에 무너져 내렸습니다. 멤버십 합격이라는 꿈에 그리던 결과를 확인하고, 기쁜 마음으로 점심을 먹고 집으로 돌아오던 길이었습니다. 긴장이 풀린 순간, 식은땀과 함께 눈앞이 흐려지기 시작했고, 저는 그대로 길에 쓰러지듯 주저앉아 간신히 몸을 이끌고 집으로 돌아와야 했습니다. 4주간의 성취가 정점에 달한 바로 그날, 저의 건강은 바닥을 드러낸 것입니다. 다음날 병원에 가서야 한달만에 몸무게가 5kg이나 빠졌다는 사실을 알게 됐습니다.</p>
<p>쓰러지고 나서야 비로소 깨달았습니다. 챌린지가 저에게 가르쳐준 마지막 교훈은 코드나 CS 지식이 아닌, 바로 <strong>지속 가능한 성장</strong>의 중요성이었습니다. 최고의 퍼포먼스는 카페인과 잠을 맞바꾸어 단기간 쥐어짜 내는 것이 아니었습니다. 잘 먹고, 잘 자고, 잘 쉬면서 꾸준히 나아가는 마라톤과 같은 것이었습니다.</p>
<p>우리의 몸은 성장을 위한 가장 중요한 하드웨어입니다. 열정과 의지는 그 위에서 동작하는 소프트웨어일 뿐, 하드웨어가 멈추면 아무 소용이 없습니다. 저는 챌린지를 통해 얻은 ‘전략적으로 사고하고 회고하는 능력’을 이제 저의 건강 관리에 적용하려 합니다. 앞으로 개발자로서의 긴 여정을 걸어갈 저에게, 기술적 성장만큼이나 스스로를 돌보는 것의 중요성을 일깨워준 가장 값진 교훈이 되었습니다.</p>
<h2 id="에필로그-나만의-성장-나침반을-갖게-되다">에필로그: 나만의 성장 나침반을 갖게 되다</h2>
<p><img src="https://images.unsplash.com/photo-1682685797769-481b48222adf?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="NEOM Unsplash"></p>
<p>4주라는 시간은 저에게 개발자로서의 평생을 살아갈 태도를 배우게 한 소중한 시간이었습니다. 챌린지를 통해 얻은 가장 큰 자산은 특정 기술이나 지식이 아닌, 어떤 문제 앞에서도 주도적으로 학습하고, 동료와 협력하며, 즐겁게 성장해나갈 수 있다는 ‘태도’와 ‘자신감’입니다. 그리고 이 모든 것을 가능하게 할 저만의 ‘성장 나침반’을 손에 쥐게 되었습니다.</p>
<h3 id="챌린지를-통해-얻은-나만의-성장-공식">챌린지를 통해 얻은 나만의 성장 공식</h3>
<p>저의 성장 나침반이 가리키는 방향은 ‘즐거움’입니다. ‘해야만 한다’는 강박이 아니라 막혔던 문제를 해결하는 과정 자체가 ‘즐거워서’ 몰입했던 경험은, 즐거움이야말로 저를 지치지 않게 하는 가장 강력한 동기임을 깨닫게 해주었습니다. 그리고 이 즐거움을 바탕으로, 저는 지식을 온전히 제 것으로 만드는 세 단계의 성장 공식을 정립할 수 있었습니다.</p>
<ol>
<li><strong>내재화:</strong> 추상적인 개념을 저만의 언어와 비유로 소화하는 단계입니다.</li>
<li><strong>설명:</strong> 내재화한 이해를 타인에게 설명하며 지식을 더욱 단단하게 만드는 단계입니다.</li>
<li><strong>구현:</strong> 마지막으로, 다져진 이해를 실제 코드로 구현하며 그 효용성을 몸으로 증명하는 단계입니다.</li>
</ol>
<p>이 세 단계를 거칠 때 비로소 지식은 휘발되지 않고 저의 것이 되었습니다.</p>
<h3 id="멤버십-그리고-그-이후를-위한-next-step">멤버십, 그리고 그 이후를 위한 Next Step</h3>
<p>이제 이 성장 나침반을 바탕으로, 멤버십 과정과 그 이후를 위한 구체적인 다짐을 세우고자 합니다. 먼저, 챌린지에서 체득한 <strong>자기주도적 학습 방식</strong>을 이어갈 계획입니다. 제가 작성한 모든 코드에 대해 근거를 가지고 자신 있게 설명하고, AI를 답안지가 아닌 사고 확장 파트너로 활용하는 훈련을 계속할 것입니다. 또한, 소극적인 태도에서 벗어나 동료에게 먼저 인사이트를 공유하고 문제 해결에 도움을 주는 <strong>성숙한 협업</strong>을 실천하고자 합니다.</p>
<p>무엇보다 가장 중요한 목표는 <strong>지속 가능한 개발자</strong>가 되는 것입니다. 챌린지 과정에서 스스로를 몰아붙인 끝에 건강을 잃었던 값비싼 경험을 통해, 진정한 성장은 꾸준함에서 온다는 것을 깨달았습니다. 이제 하루 코어타임이 끝나면 과감히 코드에서 손을 떼고, 남는 시간에는 운동과 회고로 내일을 준비하고자 합니다. <strong>앞으로만 나아가는 것이 아니라 꾸준히 뒤를 돌아보며 저만의 속도를 지키는 것</strong>, 그것이 진정한 성장임을 이제는 확신하기 때문입니다. 이 모든 다짐은 결국 ‘지속 가능한 개발자로 성장하기’라는 하나의 목표를 향하고 있습니다.</p>
<p>이제 저는 저만의 나침반과 함께, 다음 모험을 시작할 준비를 마쳤습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 부스트캠프 10기 챌린지 3주차 회고]]></title>
            <link>https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 01 Aug 2025 16:20:51 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가면서-어느새-챌린지의-끝을-향해">들어가면서: 어느새, 챌린지의 끝을 향해</h2>
<p>벌써 네이버 부스트캠프 웹/모바일 10기의 챌린지 과정도 절반이 넘게 지나갔습니다.</p>
<p><a href="https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0">2주차 회고</a>에서 저는 <strong>&#39;내가 설명할 수 있는 CS 지식&#39;</strong> 을 학습하고, <strong>&#39;의식적인 행동과 휴식&#39;</strong> 을 통해 자기 주도적으로 성장하며, <strong>&#39;동료와 함께&#39;</strong> 나아가겠다는 다짐을 했었습니다. 이제 챌린지 마지막 주를 앞두고, 이 다짐들이 3주차에서 어떻게 지켜졌는지, 어떤 시련을 겪었고 무엇을 배웠는지 돌아보려 합니다. 이번 회고는 <a href="https://youtu.be/VJxppgsHjF8?si=49dq8OzEFgm3jkX3">노래</a>를 들으면서 읽어보시는 걸 권장합니다.</p>
<h2 id="3주차에서-맞이한-새로운-시련들">3주차에서 맞이한 새로운 시련들</h2>
<p>3주차에 접어들며, 미션의 형식과 난이도 모두에 변화가 찾아왔습니다. 하루에 하나의 미션을 해결하던 방식에서 이틀에 걸쳐 구현과 개선을 반복하는 방식으로 바뀌었으며, 짝과 함께 하는 설계나 AI 피어 세션과 같은 새로운 활동이 추가되었습니다. 이러한 변화는 저에게 기대와 동시에 부담감으로 다가왔습니다.</p>
<p>저는 두 가지 큰 시련에 마주했습니다. 하나는 미션 자체의 복잡성과 모호함이었고, 다른 하나는 2주차에 세웠던 &#39;용기 있는 휴식&#39;이라는 다짐을 뒤로 한 채 또다시 늦게까지 미션에 매달려야 했던 저 자신과의 싸움이었습니다.</p>
<h3 id="모호한-요구사항과-완벽한-설계라는-함정">모호한 요구사항과 완벽한 설계라는 함정</h3>
<p>3주차의 미션들은 지난 2주간 학습했던 다양한 CS 지식을 활용해서 해결해야 하는 복잡한 문제들이었습니다. 특히, 익숙하다고 생각했던 특정 CS 개념의 내부 동작 원리를 깊이 파고들어야 했던 두 번째 미션은 저에게 낯설고 어렵게 다가왔습니다. 무엇보다 요구사항이 역대급으로 모호하게 느껴져, &#39;어디서부터 어디까지, 어떻게 설계하고 구현할 것인지&#39;를 스스로 정의해야 하는 상황에 직면했습니다.</p>
<p>이러한 막막함 속에서 저는 또다시 &#39;완벽한 설계&#39;라는 함정에 빠졌습니다. 3주차 첫 미션에서 머릿속에 이상적인 시스템 아키텍처를 그리는 데 대부분의 시간을 쏟았고, 그 결과 정작 구현에 들어갈 시간이 바닥나 핵심 기능 중 하나조차 제대로 완성하지 못했습니다. 이 고리를 끊어내야 한다는 것을 알았지만, &#39;구현을 제대로 마치지 못하면 다음 과정에 가지 못할 거야&#39;라는 막연한 불안감이 또 다시 저를 짓누르기 시작했습니다.</p>
<h3 id="지키지-못한-용기-있는-휴식">지키지 못한 &#39;용기 있는 휴식&#39;</h3>
<p>2주차 회고에서 비효율적인 밤샘 대신 &#39;용기 있는 휴식&#39;을 취하겠다고 다짐했지만, 3주차 두 번째 미션에서는 이 다짐을 어기고 또다시 잠에 늦게 들고 말았습니다.</p>
<p>하지만 신기하게도 이번 밤샘은 지난번처럼 찝찝하거나 불안하지 않았습니다. &#39;해야만 한다&#39;는 강박이 아니라, 막혔던 부분을 해결해나가는 과정이 재미있어 시간 가는 줄 몰랐기 때문입니다.</p>
<h2 id="kpt로-돌아보는-3주차">KPT로 돌아보는 3주차</h2>
<p>지난 회고까지는 생각이 떠오르는대로 글을 써내려갔는데, 이번에는 회고의 다양한 방법론 중 가장 간단하지만 효과적인 <a href="https://zzsza.github.io/diary/2023/06/05/how-to-retrospect/">KPT 회고</a>를 통해 3주차를 돌아보고자 합니다. 어떤 것들을 지켜나가고, 어떤 문제들이 있었고, 이를 돌아보며 4주차에는 어떤 시도를 할 지 하나씩 천천히 살펴보겠습니다.</p>
<h3 id="keep---즐거움을-나침반-삼아-나아가기">Keep - 즐거움을 나침반 삼아 나아가기</h3>
<p>3주차의 어려움 속에서도 제가 계속 이어나가고 싶은 긍정적인 경험과 태도들이 있었습니다. 특히 <strong>&#39;해야만 해서&#39;가 아닌 &#39;즐거워서&#39;</strong> 밤을 새웠던 경험은 저의 가장 큰 자산이 되었습니다. 이 경험을 통해 저는 무작정 선을 긋는 것이나 스스로를 몰아붙이는 것 모두 불필요한 제약이라는 것을 깨달았습니다. 이제는 &#39;즐기는 마음&#39;을 저의 나침반으로 삼고, 이 나침반이 가리키는 방향을 믿고 나아가려 합니다.</p>
<p>또한, AI를 활용하는 방식도 큰 발전을 이루었습니다. 챌린지 과정 전에는 단순히 답을 찾는 도구로 사용했다면, 이제는 AI에게 <strong>동료 리뷰어</strong> 역할을 부여해 저의 문제 해결 과정을 객관적으로 진단받았습니다. AI는 저의 &#39;초기 단계의 과도한 설계&#39;라는 약점을 정확히 지적해주었고, 덕분에 저는 좋은 설계와 과도한 설계의 경계에서 길을 잃고 있었음을 깨달을 수 있었습니다. 이는 AI를 &#39;스파링 파트너&#39;로 활용하려 했던 2주차의 다짐보다 한 단계 더 나아간 구체적인 시도였습니다.</p>
<p>마지막으로, <strong>추상적인 개념을 제 언어로 소화하고 구현을 통해 효용성을 체감하는 저만의 학습법</strong>은 여전히 유효했습니다. 3주차 미션에서 사전적 정의만으로는 이해하기 어려웠던 개념들을 비유를 통해 설명할 수 있게 되었을 때 비로소 &#39;아하 모먼트&#39;를 경험할 수 있었고, 이를 코드로 구현하며 이해를 한층 더 다질 수 있었습니다. 이러한 긍정적인 경험들을 통해 성장했지만, 동시에 저는 아직 해결하지 못한 문제점들과 마주해야 했습니다.</p>
<h3 id="problem---여전히-방황했던-나에게">Problem - 여전히 방황했던 나에게</h3>
<p>그럼에도 불구하고 3주차에는 <strong>여전히 길을 잃고 방황하던 순간들</strong>이 있었습니다. 가장 큰 문제는 2주차 회고에서 다짐했던 &#39;용기 있는 휴식&#39;을 지키지 못하고, &#39;완벽한 설계&#39;라는 함정에 빠졌던 것입니다. 미션의 요구사항이 모호해지자 저는 다시 불안감에 휩싸였고, 결국 구현에 앞서 이상적인 시스템 아키텍처를 그리는 데 대부분의 시간을 쏟아버렸습니다. 불확실성을 줄여보고자 한 시도라고 당시에는 생각했지만, 막상 구현에 들어가니 설계에서 바뀌어야 하는 부분이 눈에 띄게 있었습니다.</p>
<p>짝 설계 미션에서는 &#39;슬로우 스타터&#39;라는 생각에 갇혀 소극적인 태도를 보였습니다. 빠른 학습 후에 설계 초안을 가져가야 한다는 부담감에 짓눌렸고, 스스로를 낮추며 해결책을 제시하지 못하는 수동적인 모습을 보였습니다. <strong>&#39;스스로가 부족하다는 것을 인정하는&#39; 태도는 좋지만, 거기서 한 발자국 더 나아가야 했습니다.</strong></p>
<h3 id="try---더-나은-내일을-위해">Try - 더 나은 내일을 위해</h3>
<p>3주차의 경험을 바탕으로 4주차에는 다음과 같은 것들을 시도해보려 합니다.</p>
<ul>
<li><p><strong>짝 설계 미션에서 더 적극적인 태도 취하기</strong>: 다음 짝 설계 미션에서는 요구사항이 공개되자마자 분석하고, 학습해야 할 키워드를 정리하여 짝에게 먼저 공유하고자 합니다. &#39;슬로우 스타터&#39;라는 생각에 갇히지 않고, 적극적으로 소통하며 함께 목표와 체크포인트를 설정하여 미션에 접근하며 함께 성장할 계획입니다.</p>
</li>
<li><p><strong>복습과 체화에 집중하기</strong>: 미션을 진행하며 알게 된 CS 지식을 학습정리 노트에 남기는 것 만으로 그것이 100% 저의 지식이 되는 것은 아닙니다. 사실 진작에 했어야 하는 일이지만, 이번 주말 동안 1~3주차에 작성한 학습 정리 자료를 모두 돌아보며 새롭게 업데이트하고, 이를 말로 설명하는 연습을 통해 복습하며 지식에 대한 이해도를 다시 한 번 짚고 넘어가고자 합니다.</p>
</li>
<li><p><strong>즐기는 개발자가 되기</strong>: 비효율적인 밤샘은 피하되, 막혔던 부분을 해결하는 즐거움이 느껴질 때에는 그 몰입을 끊지 않고 끝까지 이어가고자 합니다. 그로 인한 체력 저하는 2일차 개선 과정에만 전념하고 일찍 잠자리에 드는 방식으로 유연하게 조절해 봐야겠습니다.</p>
</li>
</ul>
<h3 id="3주간의-변화-한눈에-보는-나의-성장">3주간의 변화: 한눈에 보는 나의 성장</h3>
<p>지난 3주간의 챌린지 과정을 돌아보며, 저에게 나타난 가장 큰 변화는 &#39;불안감에 쫓기는 캠퍼&#39;에서 &#39;성장을 즐기는 개발자&#39;로 나아가고 있다는 점입니다. 3주간의 회고를 정리해서 AI에게 분석을 맡긴 결과, 다음과 같이 주차별로 저의 성장 동기, 학습 방식, AI 활용법, 동료 관계, 태도 변화를 한눈에 정리할 수 있었습니다.</p>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">1주차</th>
<th align="left">2주차</th>
<th align="left">3주차</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>성장 동기</strong></td>
<td align="left">&#39;다음 과정에 가지 못할까&#39;라는 불안감과 조급함</td>
<td align="left">&#39;어제의 나&#39;와 비교하며 자기주도적 성장에 집중</td>
<td align="left">&#39;즐거워서&#39; 몰입하는 경험을 통해 내적 동기를 강화</td>
</tr>
<tr>
<td align="left"><strong>학습 방식</strong></td>
<td align="left">AI에 의존, 학습과 구현의 균형 상실</td>
<td align="left">&#39;학습-설계-구현&#39; 사이클 반복, &#39;야크 털 깎기&#39; 방지</td>
<td align="left">&#39;즐거움&#39;을 나침반 삼아 학습, &#39;나만의 언어&#39;와 &#39;구현&#39;을 통한 체화</td>
</tr>
<tr>
<td align="left"><strong>AI 활용</strong></td>
<td align="left">AI에 과도하게 의존</td>
<td align="left">AI를 &#39;스파링 파트너&#39;로 활용하며 사고 확장 시도</td>
<td align="left">AI를 &#39;동료 리뷰어&#39;로 활용해 객관적 진단 및 구체적인 협업 시나리오 수립</td>
</tr>
<tr>
<td align="left"><strong>동료 관계</strong></td>
<td align="left">피어 피드백에 소극적, 동료들의 학습법을 배우려 다짐</td>
<td align="left">동료의 실천을 보고 휴식의 중요성을 깨닫고, 비교의 대상을 &#39;나&#39;로 한정</td>
<td align="left">짝 설계 미션에서 &#39;수동적 태도&#39;의 문제점을 깨닫고 적극적으로 소통하는 방식으로 변화</td>
</tr>
<tr>
<td align="left"><strong>태도 변화</strong></td>
<td align="left">&#39;완벽해 보이기&#39;에 급급한 비효율적인 모습</td>
<td align="left">자기 객관화를 통해 자신의 약점을 인식하고 개선하려 노력</td>
<td align="left">&#39;못하는 것을 인정하는&#39; 것을 넘어 &#39;더 잘하기 위해 노력하는&#39; 능동적 태도 확립</td>
</tr>
</tbody></table>
<h2 id="오직-즐기는-자만이-살아남는다">오직 즐기는 자만이 살아남는다</h2>
<p>3주차의 긴 고민과 성찰의 끝에서, 저는 &#39;즐거움&#39;이라는 나침반을 발견했습니다. 과거 현업 개발자인 지인에게 &#39;해야 해서&#39; 밤을 새우는 것과 &#39;하다 보니&#39; 밤을 새우는 것의 차이를 들었을 때, 그 의미를 온전히 이해하지 못했습니다. 하지만 3주차 미션에서 &#39;해야만 한다&#39;는 강박이 아닌 &#39;학습하는 과정이 재미있다&#39;는 마음으로 밤을 새웠던 경험을 통해, 비로소 그 차이를 깨달을 수 있었습니다. <strong>즐기는 마음</strong>이 바로 저를 앞으로 나아가게 하는 가장 강력한 원동력이었던 것입니다.</p>
<p>이러한 깨달음은 최근 제가 본 F1 영화 속 주인공의 태도와 맞닿아 있었습니다. 한때 전설이었지만 은퇴했던 베테랑 드라이버가 돈이나 명성이 아닌, 그저 &quot;레이스가 재미있으니까&quot;라는 순수한 이유 하나로 다시 F1에 돌아온 모습에서 저는 깊은 감명을 받았습니다. 그의 태도처럼, 저 또한 남들의 평가나 미래에 대한 불안감에 휘둘리지 않고, 오직 <strong>성장하는 과정 자체를 즐기는 개발자</strong>가 되고 싶다는 생각이 들었습니다.</p>
<p>어차피 개발자로 살아간다는 것은 <strong>평생 도전과 마주하는 일의 연속</strong>일 것입니다. 이 길을 즐기지 못한다면 결코 오래갈 수 없다고 생각했습니다. 막막한 미션 앞에서 좌절하고, 다른 사람과 비교하며 불안해하고, 잠을 줄이며 스스로를 몰아붙이는 대신, 그 과정 자체에서 즐거움을 찾는 사람이 되고 싶다는 다짐을 하게 되었습니다. 이제는 저를 옭아맸던 모든 불필요한 제약들을 내려놓고, &#39;즐거움&#39;이라는 나침반이 가리키는 방향을 따라 묵묵히 나아가야 할 시간입니다.</p>
<blockquote>
<p>... (중략) ... </p>
<p>아니 벌써 3주차라니. 점검 연결고리가 많아지고 복잡해지고 더 봐야하는 자료도 많고 스스로 결정해야 하는 것도 많아지고 있습니다.</p>
<p>무더운 여름철이라 지치기도 하고 할 일이 줄어들지 않는 것 같아 머리가 아프기도 하죠.</p>
<p>이 순간이 학습 과정에서 고점을 지나는 결정적 순간일 겁니다. <strong>챌린지는 CS를 학습하고, 학습하는 지식을 무언가 직접 만들면서 경험하고 연결하는 순간들이 이어져있습니다.</strong></p>
<p>다음주 마지막 주를 만나기 전에
주간 학습 피드백을 작성하기 전에
3주차 결정적 순간을 잘 보내고 있는 스스로를 위로하고 격려도 해주세요</p>
<p>어떤 주제나 미션이든 <strong>모든 학습을 끝내는 것은 불가능하다는 것을 인정하고 딱 내가 한 만큼은 인정해줍시다.</strong></p>
<p>- 3주차를 마무리하면서 슬랙에 올라온 JK님의 메시지</p>
</blockquote>
<h2 id="나오면서-three-laps-is-a-lifetime">나오면서: Three laps is a lifetime</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/8fc994d2-17fe-4ed9-9622-52e8b25b49c5/image.jpg" alt="F1 the movie"></p>
<p>이처럼 &#39;과정 자체를 즐기는&#39; 마음가짐은, 챌린지 마지막 주를 앞두고 F1 영화 속 대사를 떠올리게 했습니다. JK님의 메시지처럼, 이제는 모든 학습을 끝낼 수 없다는 것을 인정하고 제가 한 만큼을 인정해주려 합니다. 그리고 이 모든 경험을 바탕으로 남은 챌린지 과정을 온전히 즐겨보려 합니다.</p>
<p>F1 영화 속 주인공 소니 헤이스가 자신의 동료인 조슈아 피어스에게 던진 <strong>&quot;Three laps is a lifetime&quot;</strong> 이라는 대사가 영화를 보고 온 지 2주가 넘었음에도 계속 귓가에 맴돌고 있습니다. F1 레이스에서 단 3바퀴는 평생과도 같은 시간이며, 그 안에는 어떤 일이든 일어날 수 있다는 뜻입니다. 챌린지 과정의 마지막 4주차 역시 그렇습니다. 불안함과 조급함은 내려놓고, 오직 &#39;즐거움&#39;이라는 나침반을 따라 저만의 레이스를 펼치는 4주차를 만들어가고자 합니다.</p>
<blockquote>
<p><em><strong>&quot;Three laps is a lifetime.&quot;</strong></em></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 부스트캠프 10기 챌린지 2주차 회고]]></title>
            <link>https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 25 Jul 2025 07:58:17 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가면서-어느새-절반">들어가면서: 어느새 절반</h2>
<p><a href="https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0">1주차 회고</a>에서는 챌린지의 시작과 함께 했던 다짐들이 얼마나 지켜졌는지 돌아보는 시간을 가졌습니다. 그리고 2주차를 위한 새로운 다짐을 하기도 했고요. 오늘은 1주차 회고에서 세웠던 목표가 얼마나 지켜졌는지, 얼마나 힘들었고 왜 힘들었는지, 앞으로 나아가야 할 방향은 어디인지, 어떤 방법들을 시도해 봤는지 돌아보고 3주차를 위한 목표를 정해보고자 합니다.</p>
<h2 id="1주차-목표-얼마나-달성했나요">1주차 목표, 얼마나 달성했나요?</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/358ef63a-adec-4145-96d0-3d9ebb194c15/image.png" alt=""></p>
<blockquote>
<p>놀랍게도 그렇습니다.</p>
</blockquote>
<p>1주차 주간 회고에서 저는 다음과 같은 4가지 목표를 세웠습니다:</p>
<ol>
<li><strong>내가 설명할 수 있는 CS 지식</strong>을 학습하기</li>
<li>최선의 결과를 낼 수 있는 <strong>의식적인 행동과 휴식</strong></li>
<li><strong>근거를 바탕으로 자기 주도적인</strong> 캠퍼 되기</li>
<li>동료와 <strong>함께 성장하기 위해 준비</strong>하기</li>
</ol>
<p>결론부터 말하자면, 1주차에 세웠던 네 가지 목표를 완벽하게 달성하지는 못했습니다. 2주차는 이 목표들을 지키려 애쓰는 과정에서 <strong>수없이 넘어지고, 헤매고, 다시 일어서기를 반복했던 시간</strong>이었습니다. 사실 일일 회고를 쓰고 있었는데, 1주차 주간 회고와 비교해보니 해결해야겠다고 다짐했던 고민들이 계속 이어지고 있었다는 것을 알 수 있었습니다. 그렇다면 저는 2주차에서 전혀 성장하지 않았던 걸까요?</p>
<h2 id="2주차-미로에서-길-잃어버리기">2주차: 미로에서 길 잃어버리기</h2>
<p>1주차의 다짐과 함께 들어선 2주차는, 생각보다 더 복잡하고 높은 벽으로 둘러싸인 미로와 같았습니다. 돌이켜보면, 모든 어려움은 제 출발점이 남들과 조금 달랐다는 현실에서 시작된 것 같아요.</p>
<p>2주차의 중간쯤에 다다라서 멘탈이 무너져버렸고, 메타인지를 위해 스스로에게 질문을 던져보았습니다.</p>
<ul>
<li>컴퓨터공학 전공자인가? → 아니다.</li>
<li>챌린지 이전에 전공자만큼 CS 공부를 해왔는가? → 아니다.</li>
<li>코틀린에 익숙한가? → 아니다. 베이직 과정에서 처음 배웠다.</li>
<li>요구사항 분석 및 설계 경험이 많은가? → 아니다.</li>
</ul>
<p>이처럼 CS 지식, 코틀린, TDD 등 챌린지에서 마주하는 모든 것이 저에게는 낯설었습니다. <strong>익숙한 개념과 생소한 개념을 이해하는 데 걸리는 시간의 간극</strong>은 당연했지만, 그 차이를 마주할 때마다 스스로가 작아지는 것을 느꼈습니다. </p>
<p>이 낯섦은 &#39;불안과 조급함&#39;이라는 더 높은 벽을 만들었고, 결국 악순환이 시작되고야 말았습니다.</p>
<p>미션이 어려워질수록 강해지는 <strong>&#39;구현을 모두 마치지 못하면 다음 과정에 가지 못할 거야&#39;라는 근거없는 막연한 불안감</strong>은, 어떻게든 기능을 완성해야 한다는 강박으로 이어졌습니다.</p>
<p>여기에 &quot;내가 지금 헤매는 이 부분은 다른 동료들은 이미 해결했을 거야&quot;라는 조급함이 더해지자, 저는 늦은 밤까지 잠들지 못하고 책상 앞을 지켰습니다. 하지만 이는 결국 수면 부족과 체력 저하, 그리고 다음 날의 비효율로 이어질 뿐이었습니다.</p>
<p>특히 익숙한 개념을 기반으로 구현해야 했던 미션의 실패는 이 악순환에 쐐기를 박았습니다. <strong>새로운 지식을 배우는 것보다, 이미 알고 있던 지식을 복잡하게 응용해야 하는 상황</strong>에서 저의 약점이 명확히 드러났던 것입니다.</p>
<p>머릿속에 어렴풋이 그려진 로직을 명확하게 정리하지 못한 채 성급하게 구현을 시작했고, 결국 AI의 도움으로도 실마리를 풀지 못한 채 미션을 완수하지 못했습니다. 그렇게 2주차의 미로 속에서 저는 방향을 잃은 채 불안감과 씨름하며 헤매고 있었습니다.</p>
<h2 id="출구를-찾기-위한-다양한-시도들">출구를 찾기 위한 다양한 시도들</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/68b79956-3847-48ff-851a-88751ff10817/image.png" alt=""></p>
<blockquote>
<p>플랜 A가 실패하면 플랜 B, 플랜 B가 실패하면 플랜 C ...</p>
</blockquote>
<p>끝이 보이지 않는 미로 속에서 마냥 주저앉아 있을 수만은 없었습니다. 벽을 더듬으며 출구를 찾기 위해, 저는 이런저런 시도를 시작했습니다. 그 과정에서 동료들과의 대화를 통해 한 걸음 더 나아갈 수 있었고, 마침내 지식이 온전히 제 것이 되는 기쁨도 맛볼 수 있었습니다.</p>
<h3 id="나에게-맞는-학습법을-찾기-위한-실험">나에게 맞는 학습법을 찾기 위한 실험</h3>
<p>가장 먼저 저에게 맞는 옷을 찾듯, 새로운 학습 사이클을 실험해보기 시작했습니다. 처음에는 미션에 필요한 모든 지식을 한 번에 학습한 뒤 구현을 시작했습니다. 하지만 이 방식은 학습 단계에서 너무 많은 시간을 소모해, 정작 중요한 구현 단계까지 가지 못하는 문제가 있었습니다.</p>
<p>이런 시행착오 끝에, <strong>각 미션을 독립적인 단위로 쪼개어 <code>학습-구현</code>의 작은 사이클을 여러 번 반복하는 것</strong>이 저에게 가장 잘 맞는다는 결론을 내렸습니다. 또한 학습 과정에서 길을 잃지 않도록 LLM을 활용해 가드레일을 설치했습니다.</p>
<p>지난주에는 단순히 키워드를 기반으로 검색하며 무엇이 중요한지 모른 채 헤맸다면, 이번 주에는 요구사항을 분석하면서 처음 보는 키워드를 정리하고, AI에게 &quot;이번 미션을 해결하기 위해 학습해야 할 <strong>필수적인 지식과 그 깊이</strong>를 정해달라&quot;고 요청했습니다. 덕분에 무한히 파고드는 &#39;야크 털 깎기&#39;를 방지하고 핵심에 집중할 수 있었습니다.</p>
<h3 id="동료들과-함께-성장하기">동료들과 함께 성장하기</h3>
<p>기술적인 시도만큼이나 중요했던 것은 마음가짐의 변화였고, 그 실마리는 고맙게도 동료들에게서 찾아왔습니다.</p>
<p>비슷한 고민을 하던 동료 캠퍼가 &quot;이번 주 미션을 다 끝내지 못하더라도 밤 10시에는 무조건 자겠다&quot;고 말하고 실제로 그 약속을 지키는 모습을 보며 머리를 한 대 맞은 듯한 느낌을 받았습니다. <strong>비효율적인 밤샘 대신 택한 다음 날을 위한 용기 있는 휴식</strong>. 저도 다음 주에는 그 용기를 꼭 실천해 봐야겠습니다.</p>
<p>또한, 저만 뒤처지고 있다고 생각했던 미션에서 다른 동료들 역시 어려움을 겪고 있다는 사실을 알게 되었을 때, 안도감과 함께 &#39;나만 그런 게 아니었구나&#39;하는 동질감을 느꼈습니다. 이 경험 덕분에 타인과 나를 비교하는 것이 얼마나 불필요한 불안을 만드는지 절실히 깨닫고, 묵묵히 저만의 길을 가야겠다는 확신을 얻었습니다.</p>
<h3 id="지식이-온전히-내-것이-되는-순간">지식이 온전히 내 것이 되는 순간</h3>
<p>1주차에 비해 저에게 맞는 학습법을 찾고 마음의 안정을 얻자, 비로소 지식이 온전히 제 것이 되는 순간을 경험할 수 있었습니다. 이런 과정을 겪으며, 지식이 온전히 제 것이 되려면 두 가지 단계가 필요하다는 걸 깨달았습니다.</p>
<p>첫 번째는 <strong>추상적인 개념을 제 언어로 소화해내는 과정</strong>이었습니다. 예를 들어, 함수형 프로그래밍의 &#39;모나드&#39; 개념을 어린 왕자의 &#39;양이 들어있는 상자&#39;에 비유하여 저만의 이야기로 정리해보니, 막막했던 개념이 훨씬 명확하고 친숙하게 다가왔습니다.</p>
<p>두 번째는 이렇게 <strong>이해한 개념을 실제 코드로 구현하며 효용성을 체감하는 과정</strong>이었습니다. 이론을 직접 코드로 작성하며 문제를 해결해 나갈 때, 비로소 &#39;내가 학습한 CS 지식이 어떻게 코드로 구현되는지&#39;를 명확히 깨달을 수 있었습니다. 결국 진정한 이해란, 저만의 언어로 개념을 내재화하는 과정과 그 지식을 활용해 실제 문제를 해결하는 경험이 합쳐질 때 완성되는 것 같았습니다.</p>
<h2 id="아하-모먼트와-3주차의-계획">아하 모먼트와 3주차의 계획</h2>
<p>2주차의 치열했던 미로 탐험 끝에, 저는 앞으로 나아갈 길을 밝혀줄 든든한 지도와 나침반을 손에 쥐게 되었습니다. 어지러웠던 생각의 종착점에서 마주한 마스터의 메시지는 제가 찾은 길이 틀리지 않았다는 확신을 주었고, 이를 바탕으로 3주차를 위한 구체적인 계획을 세울 수 있었습니다.</p>
<h3 id="마스터의-메시지">마스터의 메시지</h3>
<blockquote>
<p>&quot;학습과 구현 : 이론과 실습 : WHAT-&gt;HOW 과정을 어느 하나만 선택하거나 그렇다고 둘 다 완벽해야 한다고 강박을 갖기 보다는 <strong>작은 단위로 나누고 반복하고 점차 연결시키는 게</strong> 가장 좋기는 합니다. 미션의 요구사항은 학습하고 연습해보기 좋은 내용들이지만 덩어리가 크기 때문에 작은 부분을 찾아내서 작게작게 구현해보는 게 필요합니다. 목표가 그 자체로 크면 다 못하면 실망감도 크기 마련이죠. <strong>그래서 작은 목표를 자주 실행하면서 성취감도 쌓아야 합니다.</strong> 그래야 전체 중에서는 못한 게 있더라도 작은 목표를 어느정도 쌓고나면 쉴 때도 만족하고 쉴 수 있습니다. 그렇지 않으면 매번 찝찝하거든요.&quot;</p>
</blockquote>
<p>어지럽던 제 고민에 대한 해답이 마치 이 메시지 안에 다 들어있는 것만 같았습니다. 체크포인트를 작성해왔지만 거대한 미션을 하나의 덩어리로 보고 있었기에 실망감이 컸고, 작은 성공에 만족하지 못했기에 매번 찝찝함 속에서 잠들었습니다. 마스터의 메시지는 제가 경험으로 찾아낸 &#39;작게 쪼개는 전략&#39;이 저에게 있어 올바른 방향이었음을 확인시켜 주는 나침반이기도 했습니다.</p>
<h3 id="3주차를-위한-액션-플랜">3주차를 위한 액션 플랜</h3>
<p>간단하게 정리해 본 3주차의 실천 사항은 이렇습니다:</p>
<ul>
<li><p><strong>학습:</strong></p>
<ul>
<li>미션을 거대한 덩어리가 아닌, <strong>&#39;작은 목표의 합&#39;</strong>으로 바라보기.</li>
<li>체크포인트를 통과할 때마다 성취감 느끼기.</li>
</ul>
</li>
<li><p><strong>생활:</strong></p>
<ul>
<li>회복에 필수적인 수면 시간을 꼭 지키기.</li>
<li>몸과 마음의 건강을 위해, 짧게라도 운동하는 시간 갖기.</li>
</ul>
</li>
<li><p><strong>주말:</strong></p>
<ul>
<li>온전한 <strong>재충전과 다음 주를 위한 예열의 시간</strong>으로 삼기.</li>
<li>이번 주에 어떤 문제를 해결하는 경험을 했는지 돌아보기.</li>
</ul>
</li>
</ul>
<h2 id="나오면서-너만의-플레이를-해">나오면서: 너만의 플레이를 해!</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/40e66bb4-fc42-42c1-8625-4437e699d569/image.jpg" alt="백화점 서점의 서고에 붙어있던 팻말, &quot;어제보다 나은 나&quot;라고 적혀 있음"></p>
<blockquote>
<p>회고 초안을 작성하고 저녁을 먹으러 나왔다가 우연히 마주친 서점의 팻말. 가끔은 우연이 우연이 아닌 것 같기도 합니다.</p>
</blockquote>
<p>회고를 마무리하며 2주차를 돌아보니, 지난 주차 회고와 대부분 동일한 고민을 하고 있었다는 걸 깨달았습니다. 하지만 마냥 도돌이표로만 가득 찬 시간은 또 아니었구나라는 생각도 듭니다.</p>
<p>2주차는 평소 자기주도적인 학습에 익숙지 않았기에, 이에 적응하며 나만의 방법을 찾아가는 소중한 로그 수집 과정이었습니다. 다음 주에는 불필요한 고민은 내려놓고, &#39;어제의 나&#39;와만 비교하며 저만의 속도로 나아가야겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 부스트캠프 10기 챌린지 1주차 회고]]></title>
            <link>https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 18 Jul 2025 12:30:05 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가면서-누구나-다-계획은-있다">들어가면서: 누구나 다 계획은 있다</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/a7256e10-808e-41f4-a9d0-97435a2f7fec/image.png" alt="챌린지 입과 환영 메일"></p>
<p><a href="https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EB%B2%A0%EC%9D%B4%EC%A7%81-%ED%9B%84%EA%B8%B0">지난 글</a>에서 언급했듯, 베이직 과정을 마치고 네이버 부스트캠프 10기 웹/모바일 챌린지 과정에 합류하게 됐습니다. 벌써 일주일이라는 시간이 지났습니다.</p>
<h3 id="마음만큼은-개발-장인">마음만큼은 개발 장인</h3>
<p>챌린지 시작 전, 저는 몇 가지 목표를 세웠습니다. 베이직 과정에서 확인했던 저의 단점과 아쉬운 점들을 바탕으로, &#39;챌린지 과정에서는 이런 부분들을 고쳐야겠다&#39;는 다짐을 했습니다.</p>
<ul>
<li>AI 의존도 최소화하기</li>
<li>매일 주어지는 모든 미션을 구현해서 제출하기</li>
<li>시간 관리 잘 하기</li>
<li>스펀지처럼 성장하기</li>
</ul>
<p>이 외에도 많은 다짐을 하고 챌린지 과정을 시작했습니다.</p>
<h3 id="세상에-계획대로-되는-일은-하나도-없다">세상에 계획대로 되는 일은 하나도 없다</h3>
<p>그렇게 일주일이 지난 지금, 제대로 지켜진 목표는 하나도 없었습니다. 1주차가 지나고 난 뒤 느끼는 챌린지 과정은 정글에 가까웠습니다. 챌린지에서 주어지는 미션의 <strong>요구사항은 더 모호</strong>했습니다. <strong>학습해야 할 지식은 생소하고 어려웠고</strong>, 미션을 해결하기까지 걸리는 <strong>시간은 더 길어졌습니다</strong>.</p>
<p>그리고 챌린지부터는 슬랙에서 다른 캠퍼들과 소통을 할 수 있었는데, 먼저 빠르게 미션을 완료하고 궁금했던 점에 대해서 토론을 하는 캠퍼들을 보면서 &#39;나는 왜 아직 구현을 시작도 못 했지?&#39;, &#39;내가 챌린지에 맞는 사람인가?&#39;와 같은 조급함과 불안감이 들었습니다.</p>
<p>결국 지켜야 했던 다짐과 목표는 잊어버린 채, 어떻게든 시간 안에 미션을 구현해 제출하기 위해 AI에게 이거 구현해 줘, 저거 만들어 줘라며 매일 나오는 미션을 쳐내기에 급급한 저만 남았습니다.</p>
<h2 id="챌린지-3일-차-기깔나게-무너지기">챌린지 3일 차: 기깔나게 무너지기</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/7709b755-4d1b-49aa-96fc-0453bb7b61c4/image.jpg" alt="고민은 했나요"></p>
<p>그렇게 어영부영, 애매한 학습과 애매한 구현을 마치면서 3일차 미션을 맞이했습니다. 3일차 미션은 이미 예전에 한 번 봤던 개념이었고, 그래서 크게 어렵지 않을 거라는 생각에 제대로 된 학습을 진행하지 않고 바로 AI와 함께 설계와 구현으로 넘어갔습니다.</p>
<h3 id="바보에게-바보가">바보에게 바보가</h3>
<p>그렇게 저는 새벽 3시가 넘도록 출력되는 결과 코드를 보면서 &#39;다시 만들어 줘&#39;, &#39;아니야 이 부분이 잘못됐잖아&#39;라며 AI와 열심히 싸웠습니다. 스스로 고민하지 않고 <strong>바보같이 질문을 하니, AI도 덩달아 바보가 되는 대참사</strong>가 일어났고, 4시가 다 되어서야 구현된 결과 코드를 만들어낼 수 있었습니다. 학습 저장소에 대충 업로드한 뒤 그래도 고비는 넘겼다고 생각하고 잠에 들었습니다. 당연히 구현된 코드를 보고 누가 저에게 해당 코드에 대한 설명을 부탁하면 아무 말도 못 할 수 밖에 없었습니다. 제 생각이 절반도 채 반영되지 않았고, 스스로도 이해할 수 없는 코드였기 때문입니다.</p>
<h3 id="피어-피드백에서-리액션-봇-되기">피어 피드백에서 리액션 봇 되기</h3>
<p>그렇게 아침에 찾아온 피어 피드백 시간에서, 저는 아무 말도 할 수 없었습니다. 같은 그룹의 캠퍼들이 서로의 결과물에 대해 다양한 질문을 던지고 의견을 주고받는 동안, 저는 &#39;이런 부분이 인상깊었다&#39;, &#39;미션이 어려웠을 텐데 고생 많으셨다&#39; 등 형식적인 대답만을 할 수밖에 없는 리액션 봇이 되어 있었습니다. </p>
<h3 id="공허하게-깨어-있기">공허하게 깨어 있기</h3>
<p>챌린지를 준비하면서 이전 기수 캠퍼들이 블로그에 작성했던 다양한 후기를 읽었을 때, 공통적으로 <strong>챌린지 기간동안 잠을 많이 못 잔 것 같다</strong>라는 이야기를 찾아 볼 수 있었습니다. 슬랙에서도 다른 캠퍼들이 늦게까지 미션을 수행하면서 토론하는 모습도 찾아볼 수 있었습니다.</p>
<p>저 역시도 1주차를 진행하면서 평균적으로 4~5시간 정도 잠을 잔 것 같습니다. 하지만 제가 이렇게 적게 잤던 이유는 미션을 진행하면서 아직 남아 있는 부분이 있고, 그런 부분을 어떻게 해결할지 알고 있기 때문이 아니라, <strong>&#39;다른 사람들도 다 늦게까지 깨어 있으면서 공부하는데, 내가 뭐라고 지금 자?&#39;</strong>라는 생각 때문이었습니다.</p>
<p>당연히 시간이 지날수록 효율은 오르지 않았고, 그저 깨어 있기만 한 상태가 될 뿐이었습니다. 마치 한강에서 진행하는 <strong>멍 때리기 대회</strong>에 참가한 것 같았습니다. 코드를 보고 다음 단계로 나아갈 생각을 하는 게 아니라, &#39;아 줄바꿈 예쁘게 잘 되어있네&#39; 따위의 생각을 하며 스스로에게 무의미한 채찍질을 할 뿐이었습니다. 3일차 피어 피드백이 끝나고, 머릿속에는 단 하나의 의문만이 남아 있었습니다. <strong>&quot;이게 챌린지에서 원하는 성장 방향인가?&quot;</strong></p>
<h3 id="전환점-마스터의-메시지">전환점, 마스터의 메시지</h3>
<p>4일차 미션을 맞이하고 슬랙을 살펴보던 중, 다른 캠퍼의 고민에 캠프 마스터가 남긴 답변을 발견했습니다. 머리를 한 대 얻어맞은 느낌이었습니다.</p>
<blockquote>
<p>&quot;... 지식을 알고 있다고 구현이 항상 쉽고 평탄한 것도 아니고 구현을 했다고 지식을 이해하고 설명할 수 있는 것도 아니고 결국 <strong>우리는 우리가 이해한 만큼 설명하고 경험한 만큼 구현할 수 있을 겁니다</strong>(AI가 대신 하는건 대신할 뿐이죠). 완벽할 수 없고 내가 무엇을 모르는 지 모르는 상황에서는 전략적으로 의도적으로 다양한 실험과 반복 검증을 해 볼 수 밖에 없을 것 같아요 ... (중략) ... 앞으로 만나는 모든 문제들이 그렇다고 생각하면 학습 전략도 의도적인 연습이 필요한 겁니다 ... &quot;</p>
</blockquote>
<p>&#39;우리는 우리가 이해한 만큼 설명하고 경험한 만큼 구현할 수 있다&#39;는 문장이 가장 와닿았습니다. 3일동안 미션을 수행하면서 <strong>학습했던 지식을 타인에게 설명할 수 있는가? 제출한 코드들은 내가 이해한 만큼 구현됐는가?</strong>라는 질문을 스스로에게 던졌고, 아니라는 결론을 내렸습니다.</p>
<p>마스터의 이야기처럼, 완벽할 수 없고 내가 무엇을 모르는 지 모르는 상황에서 <strong>의도적인 학습 전략에 대한 연습</strong>이 필요했습니다.</p>
<h2 id="re-제로부터-시작하는-챌린지-생활">RE: 제로부터 시작하는 챌린지 생활</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/3ee95176-1952-4eed-8518-a294c5bf4157/image.png" alt="챌린지 과정 설명"></p>
<p>지금과는 완전히 다른 성장 전략을 세워야겠다는 생각이 들어서, 부스트캠프 챌린지 과정의 소개 문구를 다시 한 번 읽고 목표를 정리해 봤습니다.</p>
<ol>
<li>CS 지식을 학습한다.</li>
<li>한정된 시간 내 최선의 결과를 낸다.</li>
<li>자기 주도적으로 판단하고 행동한다.</li>
<li>동료와 함께 성장한다.</li>
</ol>
<p>이 목표는 부스트캠프에서 제공하는 <strong>&#39;무엇을(What)&#39;</strong> 입니다. 그리고 각 목표를 <strong>&#39;어떻게(How)&#39;</strong> 달성할 지는 캠퍼 각자의 몫입니다. 이 &#39;어떻게&#39;를 모르고 1주차를 진행했기에 목표를 달성할 수 없이 사서 고생하는 챌린지 1주차를 보냈다는 결론을 내렸습니다. 목표를 위한 방법을 정해야 했습니다.</p>
<h3 id="1-내가-설명할-수-있는-cs-지식을-학습하기">1. 내가 설명할 수 있는 CS 지식을 학습하기</h3>
<p>마스터의 메시지를 보고, 4일차 미션은 애매한 학습과 애매한 구현이 아니라 우선순위를 정해야겠다는 생각이 들었습니다. <strong>CS 지식을 학습한다</strong>는 목표와, <strong>내가 이해한 만큼만 설명할 수 있다</strong>는 마음가짐으로 학습에 우선순위를 두고 진행했습니다. 그 결과 요구사항을 완벽하게 구현하는 데에는 실패했지만, 4일차 미션에서 학습한 지식은 스스로에게, 나아가 타인에게 설명할 수 있게 되었습니다.</p>
<p>그렇다고 구현 역시 소홀히 할 수는 없으니, <strong>미션 요구사항을 충족할 수 있는 결과물을 내기 위해 필요한 지식</strong>이라는 목표를 추가적으로 세워 지킬 수 있도록 하고자 합니다. 햄버거를 만들기 위해서 각 재료가 무엇인지, 어떻게 요리해야 하는지는 알아야 하지만, 각 재료가 어떤 분자구조를 가지고 있는지 까지는 지금 당장은 알 필요가 없으니까요.</p>
<h3 id="2-최선의-결과를-낼-수-있는-의식적인-행동과-휴식">2. 최선의 결과를 낼 수 있는 의식적인 행동과 휴식</h3>
<p>이 목표를 달성하기 위해, <strong>깨어 있는 시간을 허비하지 않기</strong>와 <strong>충분한 수면을 취하기</strong>라는 전제조건을 설정했습니다. 아무것도 하지 못하는 채로 새벽까지 깨어 있는 것 보다 충분한 수면을 취하고 깨어 있는 시간에 최대한 집중하는 것이 당연히 훨씬 더 낫습니다.</p>
<p>물론 이러한 사실을 몰랐던 것은 아닙니다. 정말 많은 TV 프로그램이나 유튜브, 기사에서 <a href="https://www.bbc.com/korean/features-41775870">충분한 수면 시간이 중요한 이유</a>에 대해 다뤄왔지만, 남들보다 못한 자신을 자책하며 커진 불안감을 애써 밤을 새는 행위로 달래고 있었을 뿐입니다. 하지만 챌린지 과정에서 경쟁해야 할 상대는 다른 캠퍼들이 아니며, 오로지 &#39;어제의 나&#39;일 뿐입니다.</p>
<p>2주차에는 수면시간을 7시간으로 확보하고, 챌린지 일과를 진행해야 하는 9시부터 19시까지는 식사 시간을 제외하면 최대한 집중하며 학습과 구현의 우선순위를 정한다는 목표를 세웠습니다.</p>
<h3 id="3-근거를-바탕으로-자기-주도적인-캠퍼-되기">3. 근거를 바탕으로 자기 주도적인 캠퍼 되기</h3>
<p>이전까지는 AI를 사용하며 충분한 생각을 하지 않고 <strong>답을 찾기</strong>에 급급했습니다. 하지만 AI가 내린 답은 저의 답이 아니고, 그렇기에 AI를 &#39;활용&#39;하는 것이 아닌 &#39;사용&#39;하거나 소모하는 것에 그쳤습니다. 이는 우선순위를 정하지 않은 채로, 의식적으로 행동하지 않은 채로 시간과 불안에 쫓기다 보니 발생한 문제라고 생각되어서 앞선 두 목표를 설정하고 실천하는 것으로 해결할 수 있다는 생각이 들었습니다.</p>
<p>또한 동시에 AI를 &#39;잘 활용하는&#39; 방법에 대해서도 고민해 봤습니다. 단순히 AI를 활용해 답을 찾는 것이 아니라, 더 나은 문제 해결력을 기르기 위한 사고의 확장 역할을 도와줄 수 있는 방향을 고민하게 됐습니다. 자세한 내용은 잠시 뒤에 이야기해 보고자 합니다.</p>
<h3 id="4-동료와-함께-성장하기-위해-준비하기">4. 동료와 함께 성장하기 위해 준비하기</h3>
<p>목표를 정하지 않고 요구사항과 관련 없는 지식까지 학습하기, 늦게까지 깨어 있고 충분한 수면을 취하지 않기, 고민 없이 AI에게 전적으로 의존하기. 이런 행동들이 결국 연쇄적으로 작용해 피어 피드백 세션을 잘 준비하지 못했습니다. 분명 피어 피드백 세션은 캠퍼들이 서로의 성장에 도움이 될 수 있는 방향으로 토론하는 시간이지만, 이를 잘 준비할 수 없었습니다. 무엇보다 충분한 수면을 취하지 않고 반쯤 졸린 상태로 캠퍼의 코드를 아침에 보게 되니 내가 무엇을 어떻게 분석해야 하는지도 감이 안 잡혔습니다.</p>
<p>2주차부터는 더 맑은 정신으로 피어 피드백 그룹에 있는 동료들의 코드를 분석하고, 다양한 질문을 준비해서 피드백 세션에서 저와 다른 캠퍼들이 함께 성장할 수 있는 방향을 찾아야겠다는 목표를 세웠습니다. 동일한 미션에 대해 어떤 방법으로 학습하고 구현했는지를 물어보고, 또 저의 방법을 공유하면서 보다 건강한 챌린지 과정을 보내고자 합니다.</p>
<h2 id="ai를-사용해-성장하기">AI를 사용해 &#39;성장하기&#39;</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/484b16ed-541c-468e-a94b-19540ffff6c3/image.jpg" alt="나와 AI의 퓨전"></p>
<p>베이직에 이어 챌린지 과정을 진행하면서, AI에 대한 의존도가 점점 높아지는 스스로를 발견할 수 있었습니다. 3일차 미션에서는 <strong>AI를 활용하는 것을 넘어 자아를 의탁하는 수준</strong>으로 사용하고 있다는 것을 깨닫게 됐고, 아직 2077년은 오지도 않았는데 사이버펑크 세계관에 도착해버린 저를 돌아보며 많은 반성을 했습니다. 요구사항을 구현해냈다는 사실에만 집착할 뿐, 그 답을 얻는 과정, 즉 &#39;사고의 흐름&#39;은 전혀 제 것이 아니었기 때문입니다.</p>
<p>이 고민에 대한 답을 찾기 위해 현업에 계신 지인분들에게 조언을 구했고, 대화 끝에 다음과 같은 두 가지 방법으로 AI를 활용한 성장 계획을 세울 수 있었습니다.</p>
<h3 id="ai는-내-스파링-파트너">AI는 내 스파링 파트너</h3>
<p>부스트캠프에 합류할 때 세웠던 <strong>문제 해결력 키우기</strong>라는 목표를 달성하기 위해서, 2주차부터는 AI를 답안지가 아닌, 생각의 근육을 키우는 <strong>&#39;스파링 파트너&#39;</strong> 로 활용하고자 합니다.</p>
<p>과거에는 저의 부족한 경험을 AI의 빠른 답변으로 메우려 했습니다. 제가 AI를 활용하는 방식은 이런 형태에 가까웠습니다. 설계라고 생각했던 것은 설계가 아닌 단어 나열에 불과했습니다. 가장 크게 깨달음을 얻었던 3일차 미션은 다음과 같은 방식으로 AI를 사용했습니다.</p>
<blockquote>
<p>😵‍💫 <strong>(과거의 나)</strong>: 이번 미션 요구사항이야. 이런 클래스를 만들고, 저런 로직이 필요해. 대충 이런 메서드들이 있으면 될 것 같아. (메서드 목록만 대강 나열) 이걸로 구현을 시작해보자.</p>
<p>🤖 <strong>(AI)</strong>: 알겠습니다. 요청하신 내용을 바탕으로 어쩌구 클래스를 구현했습니다. (코드 블록을 제공)</p>
<p>😵‍💫 <strong>(과거의 나)</strong>: 음... 그렇구나. 근데 왜 저쩌구 로직을 이렇게 구현한 거야? 더 좋은 방법은 없을까? 괜찮다면 다음 기능 구현으로 넘어가자.</p>
<p>🤖 <strong>(답변 생성기)</strong>: 다음 기능 구현을 진행해 보겠습니다...</p>
</blockquote>
<p>하지만 이제는 미션의 요구사항과 배경지식을 충분히 학습한 뒤, 먼저 제 힘으로 요구사항의 동작 방식을 고민하고 설계의 큰 그림을 그리고자 합니다. 그리고 AI에게 제가 구상한 설계를 설명하며 검토하고 수정하는 &#39;설계 랠리&#39;를 진행할 계획입니다. 아래 대화는 활용 방안의 예시로, 네이버 부스트캠프의 콘텐츠 이용 및 보호 수칙을 지키기 위해 생성형 AI로 만든 <strong>임의의 대화 예시</strong>입니다.</p>
<blockquote>
<p><strong>😎 (미래의 나):</strong> 이번에 간단한 칸반 보드 시스템을 만들어보려고 해. 핵심은 <code>KanbanBoard</code> 클래스를 설계하고, <code>addTask</code>와 <code>moveTask</code> 실행 시 태스크 목록을 조작하는 것 같아. 내 생각엔 &#39;To Do&#39;, &#39;In Progress&#39;, &#39;Done&#39; 같은 각 상태(column)의 태스크들을 관리할 자료구조가 중요할 것 같은데, <strong>어떤 자료구조를 사용하는 게 좋을지 장단점과 함께 여러 대안을 제시해 줘.</strong></p>
<p><strong>🤖 (설계 파트너):</strong> 좋은 접근입니다. 각 상태별로 태스크 목록을 관리하는 데에는 두 가지 방식을 제안할 수 있습니다.</p>
<p>1.  <strong><code>List&lt;Task&gt;</code> 기반 관리:</strong> 구현이 매우 간단하고 직관적입니다. 하지만 특정 태스크를 ID로 찾거나 삭제할 때 리스트 전체를 탐색해야 해서 최악의 경우 O(N)의 시간이 걸릴 수 있습니다.</p>
<p>2.  <strong><code>LinkedHashMap&lt;String, Task&gt;</code> 기반 관리:</strong> 태스크 ID를 Key로, 태스크 객체를 Value로 저장하는 방식입니다. 태스크의 추가 순서를 보장하면서도, 특정 태스크를 ID로 찾거나 삭제할 때 O(1)의 시간 복잡도를 가집니다. 다만, <code>List</code>에 비해 구조가 약간 더 복잡합니다.</p>
<p>어떤 방식에 더 관심이 있으신가요?</p>
<p><strong>😎 (미래의 나):</strong> 흥미로운 제안이야. 2번, <code>LinkedHashMap</code> 방식이 확장성을 고려했을 때 더 나아 보이네. 그런데 만약 여기서 <strong>&#39;특정 담당자(assignee)에게 할당된 모든 태스크를 빠르게 찾는&#39;</strong> 기능이 추가적으로 필요하다면, 시간 복잡도는 어떻게 되지? 모든 태스크를 순회해야 하니 비효율적일 것 같은데, 이걸 최적화할 다른 방법은 없을까?</p>
<p><strong>🤖 (설계 파트너):</strong> 훌륭한 질문입니다. 맞습니다. 현재 방식으로는 모든 상태의 <code>Map</code>들을 순회해야 하므로 매우 비효율적입니다. 만약 이를 개선하고 싶다면, 기존 자료구조와 별개로 <strong>담당자별 인덱스, 즉 <code>Map&lt;String, List&lt;String&gt;&gt;</code> (담당자 이름, 태스크 ID 목록)을 추가로 유지</strong>하는 방법을 사용할 수 있습니다. 이 경우 특정 담당자의 태스크 목록을 O(1)에 가깝게 조회할 수 있지만, 태스크를 추가하거나 담당자를 변경할 때 이 인덱스도 함께 업데이트해야 하는 비용이 발생합니다.</p>
<p><strong>😎 (미래의 나):</strong> 좋아. 명확하게 이해됐어. 읽기(조회) 성능을 우선하고 싶으니, &#39;담당자별 인덱스&#39; 아이디어를 적용하는 게 좋겠어. 그럼 이 아이디어를 적용해서 <code>KanbanBoard</code> 클래스의 기본 구조와 태스크 관리 부분의 <strong>설계 초안 코드</strong>를 작성해 줘. 내가 그 코드를 보고 리뷰하면서 구체화해 나갈게.</p>
<p><strong>🤖 (설계 파트너):</strong> 알겠습니다. 논의된 내용을 바탕으로 설계 초안을 작성하겠습니다. (초안 코드 제공) 이 설계에서 더 궁금하거나 개선하고 싶은 부분이 있다면 언제든지 말씀해 주세요.</p>
</blockquote>
<p>사람이 먼저 모든 코드를 작성하면 고민이 많아져 결과물이 늦어질 수 있습니다. 대신 AI가 제 설계를 기반으로 빠르게 결과물의 초안을 만들면, 저는 &#39;리뷰어&#39;가 되어 그 코드를 검토하고, 질문을 던지며 제 고민을 주입할 수 있습니다. <strong>이 설계의 트레이드오프는 무엇인지, 더 나은 방법은 없는지를 끊임없이 따져 묻는</strong> 것입니다. 만약 AI와 대화가 막힌다면, &quot;너랑 소통이 잘 되려면 내가 어떤 정보를 더 줘야 할까?&quot;라고 물어보며 더 나은 협업을 유도하고자 합니다. 이것이 제가 AI를 단순하게 &#39;사용&#39;하는 것을 넘어 &#39;활용&#39;하기 위한 계획입니다.</p>
<h3 id="챌린지-밖에서-ai를-활용하는-방법">챌린지 밖에서 AI를 활용하는 방법</h3>
<p>챌린지에서 어려움을 겪고 있는 근본적인 이유 중 하나는 코틀린이라는 <strong>새로운 언어에 대한 숙련도 부족</strong>과, 설계를 실제 코드로 옮기는 <strong>구현 경험 자체가 적었기 때문</strong>입니다. 복잡한 미션의 설계에 집중해야 할 시간에, 기본적인 문법이나 간단한 로직 구현에 막혀 시간을 허비하는 경우가 많았습니다. 앞서 언급했던 마스터의 메시지 중 <strong>&#39;경험한 만큼만 구현할 수 있다&#39;</strong>는 말이 와닿았습니다.</p>
<p>이러한 기초 체력을 기르기 위해, 챌린지 코어타임 전후로 <strong>&#39;AI 없이 훈련하는 시간&#39;</strong> 을 가질 계획입니다.</p>
<p>매일 아침 워밍업 시간을 정했습니다. 코틀린 책을 읽고, AI의 도움 없이 저 혼자만의 힘으로 간단한 코딩 테스트 문제를 풀며 로직 구현을 연습할 계획입니다. 이는 AI라는 답안지를 보지 않고 스스로 사고하는 훈련이자, 언어의 문법과 라이브러리에 대한 유창성을 확보하는 과정이기도 합니다. 이 훈련을 통해 쌓인 경험은 챌린지 미션이라는 더 큰 문제를 해결할 때, 저를 더 단단하게 받쳐주는 버팀목이 될 것이라 믿습니다.</p>
<h2 id="나오면서-2주차를-앞둔-나의-다짐">나오면서: 2주차를 앞둔 나의 다짐</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/cd5032a3-dcd9-4a49-b188-1395b457fadd/image.jpeg" alt="난 이렇게 살아왔는데"></p>
<p>길고 복잡했던 1주차의 고민은 결국 하나의 질문으로 모아졌습니다.</p>
<blockquote>
<p><strong>&#39;나는 무엇을 위해 이 챌린지에 참여하고 있는가?&#39;</strong></p>
</blockquote>
<p>1주차의 저는 그 답을 &#39;결과&#39;와 &#39;증명&#39;에서 찾으려 했습니다. 하지만 수많은 시행착오 끝에, 제가 찾아야 할 답은 <strong>&#39;과정&#39;</strong>과 <strong>&#39;학습&#39;</strong>에 있다는 것을 깨달았습니다.</p>
<h3 id="결과가-아닌-학습이라는-가장-중요한-목표-되새기기">결과가 아닌 학습이라는 가장 중요한 목표 되새기기</h3>
<p>&#39;이 실력으로 다음 과정에 갈 수 있을까?&#39;와 같은 <strong>미래에 대한 불안감</strong>은 저를 조급하게 만들었고, 결국 학습의 본질을 놓치게 했습니다. 3일 차의 커다란 실패와 피어 세션에서의 부끄러움은, 어쩌면 이 가장 중요한 사실을 깨닫기 위해 지불해야 했던 비싼 수업료였을지도 모릅니다.</p>
<p>이제 저는 더 이상 막연한 결과에 흔들리지 않으려 합니다. 2주차부터 저의 우선순위가 될 목표는 &#39;학습&#39; 그 자체입니다. <strong>어제의 내가 이해하지 못했던 CS 지식을 오늘의 내가 이해하고, 오늘의 내가 설명하지 못했던 것을 내일의 내가 설명할 수 있다면</strong>, 그것이 바로 챌린지 과정에서 저의 성공입니다. 챌린지 과정은 다음 단계를 위한 평가 무대가 아니라, 성장을 위한 훈련장이라는 마음가짐으로 조금씩 나아가야겠다는 생각이 듭니다.</p>
<h3 id="조급함-내려놓기-나의-속도를-믿고-나아가기">조급함 내려놓기: 나의 속도를 믿고 나아가기</h3>
<p>슬랙 채널에서 다른 캠퍼들의 빛나는 성과를 볼 때마다 느껴졌던 조급함과 불안감. 이제 그 감정들과 건강하게 이별하려 합니다. <strong>경쟁 상대는 다른 캠퍼가 아닌 &#39;어제의 나&#39; 뿐이라는 사실</strong>을 받아들이기로 했습니다.</p>
<p>사람마다 학습의 속도와 방식은 다릅니다. 중요한 것은 나만의 속도를 존중하며, 꾸준히 나아가는 것이라 믿습니다. 1주차에 기깔나게 무너졌던 경험은, 역설적으로 2주차를 더 단단하게 시작할 수 있는 최고의 자산이 되었습니다. 이제 조급함은 내려놓고, 자신에 대한 믿음을 가지고 2주차의 미션을 마주할 준비를 해야겠습니다.</p>
<blockquote>
<p>이제는 1주차를 돌아보며 재정의하고 구체화시킨 목표와 함께 합니다. 2주차에는 또 어떤 새로운 미션들이 저를 기다리고 있을까요?</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 부스트캠프 10기 베이직 과정 후기]]></title>
            <link>https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EB%B2%A0%EC%9D%B4%EC%A7%81-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@camille_protocol/%EB%84%A4%EC%9D%B4%EB%B2%84-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-10%EA%B8%B0-%EB%B2%A0%EC%9D%B4%EC%A7%81-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 07 Jul 2025 12:30:03 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가면서">들어가면서</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/26e847b2-5638-43be-aa0e-c8aa09a0fce9/image.png" alt="베이직 안내"></p>
<p>2주 전, <a href="https://boostcamp.connect.or.kr/main_wm.html">네이버 부스트캠프 웹/모바일 10기</a>에 모바일(Android) 과정으로 참여했고, 지난 주 금요일에 베이직 과정을 끝마쳤습니다. 이번 회고에서는 부스트캠프의 베이직 과정에 대한 소감과 함께 2주간의 여정이 저에게 어떤 영향을 미쳤는지에 대해 이야기해보고자 합니다.</p>
<h2 id="베이직-입과-전">베이직 입과 전</h2>
<p>베이직 입과 전 기존의 제 개발 공부 방식을 돌아보면, 저는 온라인 강좌 수강이나 커리큘럼이 세세하게 정해진 부트캠프 참여를 통해 개발 지식을 쌓아왔습니다. 주어진 강의를 듣고 따라 하며 &quot;이러한 기술은 그냥 이렇게 쓰는구나&quot; 하고 고개를 끄덕이는 <strong>수동적인 학습 방식</strong>에 익숙했습니다. 문제를 해결할 때도 근거 있는 결정보다는 &quot;이거 많이 쓰던데&quot;, &quot;저거 좋다던데&quot;와 같이 <strong>타인의 의견에 의존하는 경향</strong>이 컸습니다. AI를 활용할 때도 제 사고를 확장하는 도구라기보다는, 막히는 부분에서 빠르게 정답을 찾기 위한 수단으로 의존하는 경우가 많았습니다.</p>
<p>여행에 비유하자면 마치 패키지 여행같은 학습 방법을 지속한 셈입니다. 가이드의 안내에 맞춰 &quot;이거 하세요&quot;, &quot;저기 가세요&quot;와 같은 방식이었습니다. 베이직 과정의 OT에 참석했을 때, &quot;Learning by Doing&quot;이라는 키워드를 강조했지만, 아무리 그래도 처음부터 시작하는 사람들한테 기본적인 건 알려 주겠지, 기본적인 문법은 알려주지 않을까?라는 생각으로 버티고 있었던 것 같습니다.</p>
<h2 id="베이직의-방식-this-is-the-way">베이직의 방식: THIS IS THE WAY</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/27137b9b-3ba5-45f8-b0a8-28d8fc6400dd/image.jpg" alt="야생"></p>
<p>그러나 베이직 과정은 첫날부터 저를 <strong>야생</strong>으로 내던졌습니다. 기본 문법 대신 바로 요구사항을 분석해 결과를 구현하는 미션을 마주했습니다. 그제서야 부스트캠프의 모집 안내에 있던 FAQ 중 하나가 떠올랐습니다.</p>
<blockquote>
<p>&quot;베이직에서 프로그래밍 언어에 대해 연습할 수 있는 기회가 있습니다. 따라서 비전공자도 참여 가능하며 앱이나 웹 개발 경험이 없어도 지원할 수 있습니다. 다만, <strong>적어도 하나의 프로그래밍 언어로 논리적인 코드를 작성해 본 경험</strong>이 있어야 합니다.&quot;</p>
</blockquote>
<p>이전에 프론트엔드를 공부하면서 자바스크립트를 다뤄 본 적은 있었지만, 코틀린은 처음이기에 빠르게 기초적인 문법들을 공부하며 익숙해져야 했습니다. 그리고 문법에 적응할 틈도 없이, 매일같이 새로운 미션들이 쏟아져 나왔습니다.</p>
<h3 id="쏟아지는-미션들">쏟아지는 미션들</h3>
<blockquote>
<p><strong>⛓️‍💥 작은 문제 해결 경험을 반복하며, 직접 부딪히고 배우기</strong></p>
</blockquote>
<p>미션의 종류는 여러가지였습니다. 요구사항을 읽고 코드로 구현하거나, 설계하는 미션, 특정 개념에 대해서 깊게 조사하는 미션 등 종류를 가리지 않고 다양한 영역에서 <strong>문제 해결력</strong>을 기를 수 있는 미션들이었습니다. 하-중-상 난이도로 구성되어 있었는데, 상 난이도의 미션을 처음 본 날에는 &#39;이걸 하루만에 어떻게 다 하지?&#39;라는 말을 뇌까렸던 적도 있었습니다.</p>
<p>그래도 어떻게든 당일 미션은 당일 내에 해결하자는 목표를 세우고, 수단과 방법을 가리지 않고(그렇다고 부스트캠프의 생활 수칙에 위배되는 방법을 사용한 건 당연히 아닙니다) 문제를 해결해냈습니다.</p>
<h3 id="미션-제출-끝-피어-피드백-시작">미션 제출 끝, 피어 피드백 시작</h3>
<blockquote>
<p><strong>🔭 시야를 확장하고 한발 뒤에서 돌아보기</strong></p>
</blockquote>
<p>미션 제출이 끝나면, 피어 피드백을 진행해야 했습니다. 같은 그룹으로 묶인 캠퍼들의 제출 결과물을 살펴보면서 댓글을 통해 피드백을 하는 세션이었습니다. 사실 모든 캠퍼들의 결과물을 피드백하기는 어려웠습니다. 미션을 당일에 해결하는 것부터가 버거웠으니까요. 그래도 시간이 나면 최대한 많은 캠퍼들의 코드를 보면서, <strong>같은 문제에 대해 다른 방식으로 접근한 부분이 있는지</strong>를 찾아보려고 노력했습니다. 그리고 인상깊었던 접근법이 있었다면 이를 다음 미션의 문제 접근법에 녹여내기도 했습니다.</p>
<h3 id="데일리-회고">데일리 회고</h3>
<p>피어 피드백까지 완료하게 되면, 마지막으로 하루를 돌아볼 수 있는 데일리 회고 섹션이 남아 있었습니다. 매일 미션 수행 과정을 돌아보며, 성장했던 점과 아쉬웠던 점을 다시 한번 생각해 볼 수 있는 시간이었습니다.</p>
<h2 id="베이직에서-잘-성장하려면">베이직에서 잘 성장하려면?</h2>
<p>베이직 과정을 시작할 때 처음으로 정했던 목표는 다음과 같았습니다:</p>
<blockquote>
<p><strong>스스로 문제를 정의하고, 다양한 관점에서 분석해 주도적으로 해결하는 능력을 갖춘 개발자가 되기</strong></p>
</blockquote>
<p>베이직 과정이 완전히 종료된 지금, 여태까지의 회고를 돌아보며 생각해 봤을 때 이 목표가 100% 달성된 것은 아니라고 생각합니다. 그러나 베이직을 통해 얻은 경험과 깨달음은 목표를 향해 나아갈 수 있는 단단한 발판이 되어주었습니다. 특히, 문제 해결력 테스트를 준비하며 프로그래머스에서 <a href="https://school.programmers.co.kr/learn/courses/30/lessons/64061">크레인 인형뽑기 게임</a>이라는 문제를 풀어 봤는데, 베이직 이전의 저라면 &#39;이게 어떤 문제지?&#39; 하고 한참을 고민하다 포기했을 문제였습니다. 그러나 베이직 이후의 저는 베이직에서 배웠던 분석-설계-구현 방법을 되새기며 문제를 풀 수 있게 되었습니다.</p>
<p>예를 들어, 크레인 문제를 마주했을 때, 이전의 저라면 막막함에 바로 검색부터 하거나 AI에게 문제를 풀어달라고 요청했을 겁니다. 하지만 이제는 이렇게 사고하게 되었습니다.</p>
<ol>
<li><p><strong>(분석)</strong> &quot;요구사항이 뭐지? 크레인이 인형을 뽑고, 바구니에 담는다. 같은 인형 두 개가 연속으로 쌓이면 터진다. 최종적으로 터진 인형의 총 개수를 구해야 하네.&quot; -&gt; &quot;이걸 데이터로 표현해볼까? 게임 보드는 2차원 배열(<code>Array&lt;IntArray&gt;</code>), 크레인 작동 위치는 1차원 배열(<code>IntArray</code>), 인형을 담는 바구니는 위에서 넣고 빼야 하니 <code>Stack</code>이 적합하겠다.&quot;</p>
</li>
<li><p><strong>(설계)</strong> &quot;크레인 작동 위치 배열을 순회하면서 각 위치에 해당하는 보드의 열을 탐색해야지. 맨 위부터 0이 아닌 인형을 찾아서 뽑고, 그 자리는 0으로 만들어야겠다. 뽑은 인형을 바구니(스택)에 넣기 전에, 바구니의 맨 위 인형과 같은지 비교해야겠다. 같으면 바구니에서 인형을 빼고(pop) 사라진 인형 개수(+2)를 기록, 다르면 그냥 바구니에 넣자(push).&quot;</p>
</li>
<li><p><strong>(구현)</strong> 이 설계에 따라 코드를 작성하니, 이전보다 훨씬 자신감 있고 논리적으로 문제를 해결할 수 있었습니다.</p>
</li>
</ol>
<p>이 경험을 통해 &#39;어떻게 성장해야 하는가&#39;에 대한 저만의 답을 어렴풋이 찾게 되었습니다.</p>
<h3 id="의식적인-문제-분석-설계-구현">의식적인 문제 분석-설계-구현</h3>
<p>문제의 요구사항을 수동적으로 읽고 바로 코딩에 돌입하는 것이 아니라, <strong>&#39;컴퓨터의 관점&#39;에서 문제를 재정의</strong>하는 과정이 핵심입니다. &quot;이 문제를 해결하기 위해 어떤 데이터가 필요하고, 그 데이터는 어떤 상태를 가지며, 어떻게 변해야 하는가?&quot;를 끊임없이 질문해야 한다고 생각합니다.</p>
<h3 id="사고의-틀-확장하기">사고의 틀 확장하기</h3>
<p>&#39;정답은 하나&#39;라는 생각에서 벗어나야 합니다. 내가 작성한 코드가 동작하더라도, 그게 유일하거나 최선의 방법은 아닐 수 있습니다. 베이직 과정의 피어 피드백은 바로 이 지점에서 가장 강력한 학습 도구가 되었다고 생각합니다. 다른 캠퍼의 코드에서 <code>when</code>을 활용한 깔끔한 분기 처리, 혹은 <code>scope function</code>을 이용해 코드를 간결하게 만든 것을 보며 &quot;아, 이렇게도 생각할 수 있구나!&quot;하고 감탄했던 순간들이 있었습니다. 저는 이러한 과정을 그냥 넘어가지 않고 기억해 두었다가 다음 미션에 의식적으로 적용해 보곤 했습니다. <strong>적극적으로 동료의 코드를 분석하고, 궁금한 점은 질문하며 다양한 접근법을 흡수</strong>하려는 노력이 시야를 넓혀줄 수 있었습니다.</p>
<h3 id="ai-잘-활용하기">AI 잘 활용하기</h3>
<p>베이직 과정을 시작하기 전 제가 세웠던 목표 중 하나는 AI에 의존하지 않는 개발자가 되는 것이었습니다. 베이직 과정을 진행하면서 해당 목표를 최대한 지키기 위해, 미션을 진행하다 막혔을 때 &quot;이 미션을 풀어줘&quot;가 아닌 &quot;이 문제를 해결하기 위해 Stack을 사용하려고 하는데, 시간 복잡도 측면에서 괜찮은 선택일까?&quot; 와 같이 <strong>문제 분석과 설계를 통해 내 생각을 먼저 제시하고 AI의 의견을 구하는 방식</strong>으로 활용했습니다.</p>
<h3 id="꾸준하게-회고하고-성장하기">꾸준하게 회고하고 성장하기</h3>
<p>매일 작성했던 데일리 회고는 그 성장을 위한 체크포인트와 같은 역할을 해 주었습니다. &quot;오늘은 시간 분배에 실패해서 상 난이도 미션을 해결하기까지 시간이 오래 걸렸다&quot; 와 같은 아쉬웠던 점을 기록했기에, 다음 날 &quot;중요한 기능부터 구현하고, 리드미 작성은 나중에 하자&quot;는 구체적인 개선 전략을 세울 수 있었습니다. <strong>실수를 부끄러워하지 말고, 오늘의 실패를 내일의 성공을 위한 데이터로 삼는 태도</strong>가 꾸준한 성장의 원동력입니다.</p>
<h2 id="돌아보니-아쉬웠던-점">돌아보니 아쉬웠던 점</h2>
<p>치열했던 만큼, 당연히 아쉬움도 많이 남았습니다.</p>
<ul>
<li><p><strong>복습과 추가 학습의 부재</strong>: 매일 쏟아지는 미션을 해결하는 데 급급해, 한번 사용했던 개념이나 API를 깊이 있게 파고들지 못했습니다. <code>associateBy</code> 같은 편리한 함수를 &quot;어디선가 봤는데...&quot; 하고 넘어간 뒤 다시 <code>for</code>문으로 구현하는 비효율을 겪기도 했습니다. 남는 시간을 최대한 활용해 배웠던 내용들을 완전히 내 것으로 만드는 시간을 가졌다면, &#39;아는 것&#39;과 &#39;쓸 수 있는 것&#39;의 간극을 더 효과적으로 줄일 수 있었을 겁니다.</p>
</li>
<li><p><strong>체력적 아쉬움</strong>: &quot;어떻게든 당일 미션은 당일 내에 해결하자&quot;는 목표는 좋았지만, 종종 수면 시간을 줄여가며 목표를 달성했습니다. 당연히 다음 날 집중이 잘 될 리가 없었고, 특히 후반부로 갈수록 체력의 중요성을 절감했습니다.</p>
</li>
<li><p><strong>조금 더 많은 시간을 리드미가 아닌 문제 해결에 투자했다면</strong>: 베이직 과정 내내 문제 해결 과정과 고민을 상세히 기록하는 것에 많은 시간을 쏟았습니다. 덕분에 언제나 리드미가 상세하게 작성되어 있어서 많은 도움을 받았다는 캠퍼분들의 피드백도 종종 볼 수 있었습니다. 물론 기록은 중요하지만, 때로는 핵심 로직 구현에 더 집중해야 할 시간에 문서 작업에 매몰되기도 했습니다. 문제 해결력 테스트에서 시간 압박을 크게 느꼈던 이유도, 정해진 시간 내에 핵심을 꿰뚫고 빠르게 구현하는 훈련이 상대적으로 부족했기 때문일지도 모릅니다. &#39;잘 설명하는 것&#39;과 &#39;잘 푸는 것&#39; 사이의 균형을 찾는 것이 중요했습니다.</p>
</li>
</ul>
<h2 id="베이직-과정에서-알게-된-강점과-약점">베이직 과정에서 알게 된 강점과 약점</h2>
<p>2주간의 베이직 과정은 단순히 코딩 기술을 연마하는 시간을 넘어, 개발자로서의 저 자신을 정밀하게 진단하고 이해하는 계기가 되었습니다. 매일의 미션과 회고를 통해 저는 몇 가지 뚜렷한 강점과 개선이 필요한 약점을 발견할 수 있었습니다.</p>
<h3 id="강점">강점</h3>
<p>가장 큰 강점은 <strong>문제를 데이터의 관점에서 바라보고 체계적으로 해결책을 설계하는 &#39;데이터 중심 사고&#39;</strong>였습니다. 복잡한 요구사항을 마주했을 때, 저는 곧바로 구현에 뛰어들기보다 어떤 데이터 구조가 문제의 본질을 가장 잘 표현할 수 있을지 먼저 고민했습니다. 이러한 습관 덕분에 복잡한 문제 앞에서도 쉽게 길을 잃지 않을 수 있었습니다.</p>
<p>더불어, 한번 작성한 코드에 만족하지 않고 <strong>끊임없이 더 나은 코드를 추구하는 &#39;리팩토링 의지&#39;</strong>와 <strong>실수를 성장의 발판으로 삼는 &#39;성장형 마인드셋&#39;</strong>은 저를 앞으로 나아가게 한 원동력이었습니다. 단순히 &#39;동작&#39;하는 코드를 넘어 품질, 가독성, 유지보수성을 높이기 위해 노력했고, 동료의 코드나 저의 과거 실수에서 배운 점을 다음 미션에 꼭 적용하려 했습니다. 오류 로그 하나하나를 학습 기회로 삼아 근본 원인을 파고드는 과정은 지식을 더욱 단단하게 만들었습니다.</p>
<h3 id="약점">약점</h3>
<p>하지만 이러한 강점의 이면에는 명확한 약점들도 존재했습니다. 가장 먼저, 새로운 언어(Kotlin)에 대한 유연성과 적응성의 부족이 발목을 잡았습니다. Kotlin이 제공하는 강력하고 간결한 API를 충분히 활용하지 못하고, 이전에 익숙했던 방식으로 <strong>비효율적인 &#39;돌아가는 길&#39;</strong>을 택하는 경향이 있었습니다. 이는 언어와 API에 대한 피상적인 이해로 이어졌습니다. 함수의 이름만 보고 기능을 추측하다가 예상치 못한 결과를 마주하고 디버깅에 시간을 쏟는 일이 잦았는데, 공식 문서를 통해 반환 값과 부수 효과를 꼼꼼히 확인하는 습관의 중요성을 절감했습니다.</p>
<p>이러한 비효율은 <strong>&#39;시간 관리&#39;의 어려움</strong>으로 이어졌습니다. 깊이 있는 탐구를 선호하는 성향과 언어 숙련도 부족이 맞물려 미션 해결 시간이 예상보다 길어졌고, 조급함으로 이어져 AI에 대한 의존도를 높이는 악순환을 만들었습니다. 학습의 깊이와 속도, 그리고 AI의 &#39;도움&#39;과 &#39;의존&#39; 사이에서 건강한 균형점을 찾는 것이 챌린지 과정을 시작하기 전까지 해결해야 하는 과제로 남게 됐습니다.</p>
<p>마지막으로, 문제의 &#39;정상 경로&#39;를 구현하는 데에는 익숙했지만, 예상치 못한 &#39;엣지 케이스&#39;나 예외 상황에 대한 <strong>&#39;방어적 사고&#39;</strong>가 부족했습니다. 사용자의 잘못된 입력, 데이터의 경계 값 등 비정상적인 경로에 대한 예측과 방어 로직 설계에 소홀했던 점은 실제 안정적인 서비스를 만들어야 할 때 치명적인 버그로 이어질 수 있는 부분이라고 생각합니다.</p>
<p>결론적으로 베이직 과정은 저에게 <strong>&#39;데이터 중심적이고 끈기 있는 학습자&#39;라는 강점</strong>을 확인시켜 주었지만, 동시에 <strong>&#39;새로운 환경에 대한 적응, 시간 관리, 방어적 코드 작성&#39;이라는 명확한 성장 과제</strong>도 함께 안겨주었습니다. 다른 무엇보다도 이러한 강점과 약점에 대한 명확한 인지가 베이직 과정의 가장 큰 수확이며, 앞으로 시작될 챌린지 과정을 헤쳐나갈 가장 중요한 지도가 될 것이라고 생각합니다.</p>
<h2 id="문제-해결력-테스트와-챌린지-합격">문제 해결력 테스트와 챌린지 합격</h2>
<p>10일차 미션이 끝난 뒤 바로 다음 날인 토요일에 문제 해결력 테스트를 진행했습니다. 지난 기수와 다르게 10기부터는 <strong>베이직 과정에서의 활동</strong>과 <strong>문제 해결력 테스트 결과</strong>, 그리고 <strong>지원서</strong>를 종합해 챌린지 입과 여부를 판단한다고 했는데, 그래도 문제 해결력 테스트를 너무 못 보면 챌린지 과정에 입과할 수 없을까봐 어떻게든 지난 베이직 미션들을 돌아보며 문제 해결력 테스트를 준비했습니다.</p>
<p>문제 해결력 테스트는 구현 문제, CS 문제, 서술형 문제 세 가지였습니다. 문제의 난이도가 어려웠다고 생각하지는 않습니다. 베이직 과정을 충실하게 진행했다면 대부분의 문제는 수월하게 풀 수 있는 정도의 난이도라고 느꼈습니다. 하지만 문제를 풀면서 정말 확실하게 와닿았던 것은 <strong>스스로가 아직 시간적 압박에 잘 대처하지 못한다</strong>는 점이었습니다. 구현 문제도 3문제 중 1문제밖에 풀지 못했을 정도로 시간에 쫓기며 문제를 풀었고, 그래서 좋은 결과를 기대하지는 않고 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/80e086d4-b605-4e45-833d-aed13339beec/image.png" alt="챌린지 입과 메일"></p>
<p>결과를 확인하기 전까지 마음을 졸였지만, 다행히 챌린지 과정에 합격할 수 있었습니다. 베이직 과정 동안 매일 치열하게 고민하고, 꾸준히 회고하며 성장하려 했던 노력을 좋게 봐주신 것이 아닐까 생각합니다. 만약 다음 기수에 지원하시는 분들이라면 미션을 빨리 해결하고 문제 해결력 테스트 대비에 집중하는 것 보다, 베이직 과정을 성실하게 수행하면서 더 많은 것들을 얻어가겠다는 마음으로 2주간의 여정을 시작하면 좋을 것 같습니다.</p>
<h2 id="나오면서-앞으로의-길">나오면서: 앞으로의 길</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/f16ebbe8-c22e-4379-85b5-1b88e02840cf/image.png" alt="챌린지 안내"></p>
<p>다음 주 월요일부터는 드디어 챌린지 과정이 시작됩니다. 베이직과는 다르게 <strong>코어 타임</strong>이 정해져 있고, <strong>동료들과 함께 피어 피드백</strong>을 통해 서로의 문제 해결 방식에 대해 토론하며 사고를 확장해나가는 경험을 한달동안 하게 됩니다. 이전 수료생 분들의 후기를 찾아보면 베이직과는 차원이 다른 미션들을 수행하게 되고, 한달 내내 제대로 잠을 자본 적이 거의 없다고 할 정도로 강도높은 과정이 될 것 같습니다.</p>
<p>베이직 과정이 정해진 코스 없이 스스로 길을 찾아야 하는 <strong>&#39;탐험&#39;</strong>이었다면, 챌린지 과정은 동료들과 함께 더 험난한 산을 오르는 <strong>&#39;등반&#39;</strong>과 같을 것이라고 생각합니다. 베이직에서 얻은 &#39;스스로 생각하는 근육&#39;과 &#39;데이터 중심 사고&#39;라는 나침반, 그리고 &#39;회고를 통한 성장&#39;이라는 비상식량을 잘 챙겨서, 앞으로의 챌린지 여정도 무사히, 그리고 즐겁게 헤쳐나가고 싶습니다. 그럼 다음에는 챌린지 과정의 회고로 찾아오겠습니다! 🧑‍💻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[다녀왔습니다, DroidKnights 2025]]></title>
            <link>https://velog.io/@camille_protocol/%EB%8B%A4%EB%85%80%EC%99%94%EC%8A%B5%EB%8B%88%EB%8B%A4-DroidKnights-2025</link>
            <guid>https://velog.io/@camille_protocol/%EB%8B%A4%EB%85%80%EC%99%94%EC%8A%B5%EB%8B%88%EB%8B%A4-DroidKnights-2025</guid>
            <pubDate>Wed, 18 Jun 2025 09:42:52 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가면서-어쩌다보니">들어가면서: 어쩌다보니</h2>
<p>저는 프론트엔드 개발자를 꿈꾸던 취준생이었습니다. 그러던 중 문득 안드로이드 개발에 흥미를 느끼기 시작했고, 새로운 길을 모색하던 참이었죠. 낯선 분야에 발을 들이기 전, 막연하게나마 개발자 커뮤니티의 분위기를 느껴보고 싶다는 생각을 하던 그때, 제가 참여하고 있던 슬랙 채널에 올라온 한 줄기 빛과 같은 메시지를 발견했습니다.</p>
<blockquote>
<p>&quot;드로이드나이츠 2025 행사가 6월 17일 화요일로 가까이 다가왔습니다. 이번 행사에 RevenueCat이 플레티넘 스폰서로 참가하게 되면서 10장(한 장에 약 6만원)의 티켓을 커뮤니티에 기여하게 되었습니다. 드로이드나이츠 티켓 구매를 놓쳐 아쉬우셨던 분께는 좋은 기회입니다.&quot;</p>
</blockquote>
<p>무려 국내 최대 안드로이드 개발자 컨퍼런스인 드로이드나이츠의 티켓을 무료로 얻을 기회라니, 놓칠 수 없었습니다. 블로그 포스팅이라는 작은 조건이 있었지만, 이는 저에게 개발 커뮤니티에 첫발을 내딛는 소중한 기회가 될 것이라 생각했습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/dbf3f07e-bdb4-40fb-8121-35a3b1dd75f3/image.png" alt=""></p>
<p>두근거리는 마음으로 구글 폼에 신청서를 제출했고, 며칠 뒤 꿈같은 메일을 받게 되었습니다. RevenueCat의 엄재웅님께서 보내주신 당첨 축하 메일이었죠. &quot;드로이드나이츠 2025 &lt;&gt; RevenueCat 티켓 당첨자에 선정되신 것을 축하드립니다!&quot;. 메일을 통해 RevenueCat이 인앱 결제, 구독 및 서버 드리븐 페이월 SDK를 제공하며 개발자들이 수익을 창출하는 데 도움을 주는 회사라는 것도 알게 되었습니다. 부스에서 굿즈를 나눠줄 예정이니 작은 가방을 지참하라는 친절한 안내까지!</p>
<p>그렇게 저는 안드로이드 개발에 대한 막연한 흥미와 함께 행운처럼 찾아온 기회를 통해, 국내 최대 안드로이드 개발자 컨퍼런스인 &#39;드로이드나이츠 2025&#39;에 참여하게 되었습니다.</p>
<h2 id="드로이드나이츠-2025">드로이드나이츠 2025</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/5f0f61ae-72d7-4832-8a8e-2318d58d65d6/image.jpg" alt=""></p>
<p>드로이드나이츠는 국내 최대 규모의 안드로이드 개발자 컨퍼런스로, 올해 열린 행사는 &#39;드로이드나이츠(DroidKnights) 2025&#39;였습니다. &quot;주니어부터 시니어까지 모두가 공감하고 즐길 수 있는 지식의 장으로 만들고자 합니다&quot;라는 슬로건처럼, 안드로이드 개발의 최신 트렌드와 깊이 있는 기술 논의가 활발히 이루어지는 자리였습니다.</p>
<p>행사 시간표와 더 자세한 정보는 <a href="https://www.droidknights.dev/">공식 페이지</a> 링크에서 확인할 수 있습니다.</p>
<h2 id="revenuecat과-부스-탐방">RevenueCat과 부스 탐방</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/8cae8170-5b18-4894-a152-b38f0561973f/image.png" alt=""></p>
<p><strong><a href="https://www.revenuecat.com/">RevenueCat(레비뉴캣)</a></strong>은 인앱 결제, 구독 및 서버 드리븐 기반의 페이월 SDK를 제공하며, 구글 플레이 및 애플 스토어와 연동하여 결제 트래킹, LTV 분석 및 그로스 전략을 통해 서비스 매출을 증대시키는 것을 도와주는 기업입니다. <strong>&quot;Help Developers Make More Money&quot;</strong>라는 사명처럼, OpenAI(ChatGPT), Notion 등 큰 기업들뿐만 아니라 개인 앱에 RevenueCat을 통합하여 높은 수익을 창출하는 1인 개발자들을 위한 그로스 지원에도 많은 노력을 하고 있다고 합니다.</p>
<p>이번 드로이드나이츠에서 플래티넘 스폰서로 참여한 RevenueCat 부스에도 방문했습니다. 티켓 당첨 메일에서부터 작은 가방을 지참하라는 안내가 있었는데, 부스에서 다양한 굿즈를 나눠주셔서 그 이유를 알 수 있었습니다. 아쉽게도 담당자분과 깊은 대화를 나누지는 못했지만, 활기찬 부스 분위기와 개발자들을 위한 세심한 배려를 느낄 수 있었습니다. RevenueCat이 단순히 결제 솔루션을 제공하는 것을 넘어, 개발자들의 수익 창출과 성장에 진심으로 기여하고자 한다는 인상을 받았습니다. 언젠가 제가 만들고 출시하게 될 앱에서도 레비뉴캣의 인앱 결제 서비스를 사용하게 된다면 높은 만족도를 얻을 수 있을 것 같다는 생각이 들었습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/a00f3cf4-e942-4819-ad58-9eaf15f5380a/image.png" alt=""></p>
<p>드로이드나이츠 현장에서는 <strong><a href="https://www.jetbrains.com/">JetBrains(젯브레인스)</a></strong>의 부스도 만날 수 있었습니다. 젯브레인스는 개발자들에게 익숙한 강력한 IDE와 다양한 개발 도구를 제공하는 기업인만큼, 부스에도 많은 개발자들이 모여 있었습니다.</p>
<p>부스에서는 간단한 퀴즈쇼가 진행되어 젯브레인스의 최신 서비스와 도구들에 대한 정보를 알아볼 수 있었습니다. 또한 질문과 설명을 통해 젯브레인스가 개발 생산성 향상을 위해 어떤 노력을 하고 있는지 엿볼 수 있었던 유익한 시간이었습니다.</p>
<h2 id="세션-후기">세션 후기</h2>
<p>이번 드로이드나이츠 2025에서 다양한 세션을 접하면서 안드로이드 개발의 넓은 스펙트럼과 개발자들의 깊이 있는 고민을 엿볼 수 있었습니다. 비록 모든 기술적인 내용을 완벽하게 이해하지는 못했지만, 각 세션이 제시하는 문제 해결 방식과 새로운 시도들은 개발자로서의 시야를 넓혀주는 데 큰 도움이 되었습니다.</p>
<h3 id="readmoretextview-텍스트-더보기-기능-구현하기">ReadMoreTextView: 텍스트 &#39;더보기&#39; 기능 구현하기</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/ee9d2019-c022-4e58-8cb7-bb26764f4d1b/image.jpg" alt=""></p>
<p>평소 모바일 앱을 사용하면서 길게 늘어진 텍스트를 깔끔하게 보여주는 &#39;더보기&#39; 기능을 자주 접했지만, 그 안에 이렇게 복잡하고 섬세한 구현 원리가 숨겨져 있을 줄은 몰랐습니다. 세션은 단순히 라이브러리를 가져다 쓰는 것을 넘어, 직접 기능을 구현할 때 기능성, 유지보수, 호환성, 안정성을 어떻게 고려해야 하는지 강조했습니다. 특히 안드로이드에서 텍스트를 다룰 때 생기는 <strong>&#39;깜빡임 처리&#39;</strong>나 &#39;이모지 깨짐(lowsurrogate 문제)&#39; 같은 예상치 못한 문제점들, 그리고 StaticLayout이나 TextMeasurer를 활용해 텍스트와 &#39;더보기&#39; 버튼의 너비를 정확히 계산하는 과정이 정말 흥미로웠습니다.</p>
<p>특히 기억에 남는 건, <code>ComposeView</code>로 모든 것을 구현하려다가 <code>Compose Multiplatform</code>에서 발생할 수 있는 <strong>RTL(Right-to-Left)</strong> 이슈 같은 예상치 못한 난관에 부딪혔다는 경험 공유였습니다. 이는 제가 앞으로 개발을 할 때 단순히 기술을 익히는 것뿐만 아니라, 다양한 환경과 잠재적 문제점을 미리 파악하고 설계하는 통찰력이 중요하다는 것을 일깨워줬습니다. 이 세션은 <strong>요구사항 분석부터 레퍼런스 및 라이브러리 탐색, 직접 구현, 그리고 테스트</strong>에 이르는 개발 프로세스의 중요성을 다시 한번 강조했습니다. &#39;더보기&#39; 버튼 하나에도 이렇게 깊은 고민과 노력이 필요하다는 것을 알게 되면서, UI/UX 구현의 복잡성과 개발자의 섬세함에 깊은 인상을 받았습니다.</p>
<h3 id="kmp로-figma-아이콘-동기화-클릭-한-번이면-끝">KMP로 Figma 아이콘 동기화, 클릭 한 번이면 끝!</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/b300f219-3a32-42ff-bc15-5463f0cd4426/image.jpg" alt=""></p>
<p>이 세션은 디자이너와 개발자 간의 협업에서 발생하는 비효율적인 부분을 어떻게 기술로 해결할 수 있는지 보여준 인상적인 강연이었습니다. Figma에서 아이콘을 다운로드하고, 변환하고, 프로젝트에 적용하는 일련의 과정이 얼마나 번거로운지 공감하며 들었습니다. 세션에서는 Figma Rest API를 활용하여 이러한 반복적인 수작업을 자동화하는 솔루션을 제시했는데, 특히 <strong>KMP(Kotlin Multiplatform)</strong>를 활용해 다양한 플랫폼에서 아이콘을 동기화하는 핵심 로직을 만든다는 점이 놀라웠습니다.</p>
<p>저는 안드로이드 개발 경험이 전무하지만, <strong>&#39;귀찮은 일은 자동화한다&#39;</strong>는 개발자의 지혜와 문제를 해결하기 위해 다양한 기술(Figma API, KMP)을 조합하는 방식이 매우 흥미로웠습니다. 비효율을 줄여 생산성을 높이는 이러한 접근 방식은 어떤 분야의 개발자가 되든 반드시 갖춰야 할 역량이라는 것을 깨달았습니다.</p>
<h3 id="제미나이와-함께-안드로이드-공부하기">제미나이와 함께 안드로이드 공부하기</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/6b38ba5a-4d27-4a23-bbcc-f29b98554304/image.jpg" alt=""></p>
<p>이 세션은 구글 코리아 DevRel(Developer Relations) 연사님의 강연으로, 인공지능 시대에 우리가 어떻게 학습하고 성장해야 할지에 대한 깊이 있는 통찰을 제공했습니다. 특히 &#39;나는 AI와 공부한다&#39;는 추천 도서를 언급하며, AI가 단순한 도우미가 아닌 강력한 학습 파트너가 될 수 있음을 강조했습니다.</p>
<p>저는 제미나이를 활용한 <strong>학습 사이클 (구체적 경험 → 성찰적 관찰 → 추상적 개념화 → 능동적 실험)</strong>에 가장 큰 공감을 했습니다. 직접 코드를 실행하고 분석한 후, NotebookLM으로 소스를 긁어모아 노트를 만들고, AI 개발 툴에 코드를 통째로 읽어와 설명을 부탁하거나 오류를 만들어 수정해보는 과정은 제가 앞으로 안드로이드를 비롯한 새로운 기술을 학습할 때 적용해보고 싶은 효과적인 방법론이었습니다. AI를 활용하여 학습 효율을 극대화하고, 미지의 영역을 탐구하는 개발자의 적극적인 자세를 배울 수 있었던 유익한 시간이었습니다.</p>
<p>현재 제미나이 2.5 모델에는 LearnLM이라는 학습용 파인튜닝 모델이 통합되었는데, 이를 활용할 수 있는 방법은 구글에서 제공하는 <a href="https://services.google.com/fh/files/misc/learnlm_prompt_guide.pdf">공식 문서</a>를 참고하시면 좋을 것 같습니다.</p>
<h3 id="jetpack-xr-살펴보기-xr-개발은-어떻게-이루어지는가">Jetpack XR 살펴보기: XR 개발은 어떻게 이루어지는가?</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/51ccbfc2-5390-44bc-89ca-6d6137ae7b03/image.jpg" alt=""></p>
<p>이 세션은 개인적으로 가장 이해하기 어려웠지만, 동시에 가장 미래 지향적이고 흥미로웠던 세션 중 하나였습니다. 삼성, 구글, 퀄컴이 협력하여 개발 중인 프로젝트 무한(XR 디바이스)과 함께 공개된 <strong>Jetpack XR SDK</strong>에 대한 내용이 다뤄졌습니다. &#39;Home Space 모드&#39;와 &#39;Full Space 모드&#39;와 같은 XR 환경의 기본 개념, 그리고 extensions 구조를 통해 안드로이드 SDK가 어떻게 확장되어 XR 개발을 지원하는지 설명했습니다.</p>
<p>솔직히 &#39;Entity Component System&#39;이나 내부적인 복잡한 구조는 온전히 이해하기 어려웠지만, 안드로이드 개발이 단순히 스마트폰을 넘어 XR(확장 현실)과 같은 새로운 폼팩터로 확장되고 있다는 사실이 저에게는 신선한 충격이자 새로운 기회로 다가왔습니다. 미래의 안드로이드 개발이 어떤 모습일지 어렴풋이 그려볼 수 있었고, 새로운 기술 분야에 대한 막연한 호기심과 도전 의식을 갖게 된 계기가 되었습니다.</p>
<h3 id="컴포즈-스냅샷-내부원리-찍어먹기">컴포즈 스냅샷 내부원리 찍어먹기</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/001badd0-97eb-4c78-b95d-71c8f7217e0b/image.jpg" alt=""></p>
<p><strong>Jetpack Compose</strong>가 UI를 효율적으로 업데이트하는 핵심 원리인 &#39;스냅샷&#39;에 대해 다룬 세션이었습니다. 기존 View 모델에서는 데이터가 바뀌어도 UI를 직접 갱신해야 했던 반면, Compose는 상태를 선언하고 추적하여 필요한 부분만 자동으로 리컴포지션(재구성)한다는 점이 매우 인상 깊었습니다. 연사님께서는 스냅샷의 내부 원리를 Git의 브랜치와 머지에 비유하여 설명해주셨는데, 이 비유 덕분에 <strong>&#39;동시성 제어 시스템&#39;과 &#39;MVCC(Multi-Version Concurrency Control)&#39;</strong> 같은 어려운 개념을 어느 정도 이해할 수 있었습니다.</p>
<p>Composable 함수가 병렬적으로 실행될 수 있다는 가능성, 그리고 이를 위해 상태의 격리가 필수적이라는 설명은 Compose의 효율성과 안정성을 보장하는 핵심 메커니즘을 알려주었습니다. 제가 Compose를 깊이 알지는 못하지만, UI 프레임워크가 어떻게 내부적으로 상태를 관리하고 변경을 감지하는지에 대한 고차원적인 이해를 돕는 세션이었습니다.</p>
<h3 id="2025-드로이드나이츠-앱-개발기">2025 드로이드나이츠 앱 개발기</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/8bc48c68-9f94-4316-b368-d0272861af13/image.png" alt=""></p>
<p>이 세션은 실제로 &#39;2025 드로이드나이츠&#39; 공식 앱을 개발하며 겪었던 과정과 아키텍처에 대한 이야기를 공유해주었습니다. Clean Architecture 기반의 유연한 아키텍처 제안, api 모듈과 구현 분리를 통한 빠른 빌드 시간 확보 등 실제 앱 개발에서 맞닥뜨리는 실질적인 문제와 그 해결책을 엿볼 수 있었습니다.</p>
<p>특히 <strong>Compose Multiplatform</strong>을 활용하여 기존 앱 코드를 멀티플랫폼으로 마이그레이션한 경험은 인상 깊었습니다. KMP Wizard나 build-logic plugin을 통한 모듈화, CI 구성을 통한 점검 등 실제 개발 과정의 노하우를 배울 수 있었습니다. retrofit 대신 ktor를 사용하는 등 라이브러리 선택에 대한 고민도 함께 공유해주어, 단순히 기술을 아는 것을 넘어 프로젝트의 특성과 유지보수를 고려한 설계가 얼마나 중요한지 체감할 수 있었습니다.</p>
<h3 id="레드팀-화이트해커의-offensive-관점-안드로이드-앱-보안feat-cve-2025-">레드팀 화이트해커의 Offensive 관점 안드로이드 앱 보안(feat. CVE-2025-****)</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/fe33fa02-bb64-4d63-839a-2dd9a3a478de/image.jpg" alt=""></p>
<p>이번 컨퍼런스에서 가장 강렬한 경각심을 일깨워준 세션이었습니다. <strong>레드팀(공격자 역할)</strong>의 관점에서 안드로이드 앱이 어떤 방식으로 공격받을 수 있는지, 그리고 개발 단계에서 어떤 보안 취약점들을 주의해야 하는지 생생하게 들을 수 있었습니다. 웹뷰 하이재킹, XSS(Cross Site Script), Intent Redirection, Insecure Authentication, Insecure Logging, API 설정 오류 등 다양한 취약점들이 실제 사례와 함께 소개될 때는 등골이 오싹해지기도 했습니다.</p>
<p>특히 <strong>&quot;공격자 입장에서는 앱 코드를 전부 뜯어서 볼 수 있기 때문에 주의해야 한다&quot;</strong>는 말은 제게 큰 충격으로 다가왔습니다. 보안은 선택이 아닌 필수라는 것을 다시 한번 깨달았고, 앞으로 어떤 개발자가 되든 개발 초기 단계부터 보안을 고려하고, 중요한 로직은 외부에 두거나 전문적인 솔루션을 사용하는 등 방어적인 설계를 해야 한다는 것을 깊이 새기게 되었습니다. 기술적인 깊이도 중요하지만, 사용자의 데이터를 보호하는 책임감을 가져야 함을 배운 소중한 시간이었습니다.</p>
<h2 id="나오면서-내년에-또-만나요">나오면서: 내년에 또 만나요!</h2>
<p>드로이드나이츠 2025는 이러한 저의 새로운 개발 여정의 시작점에서 만난 소중한 이정표였습니다. 안드로이드 개발에 대한 깊이 있는 지식뿐만 아니라, AI 시대의 학습 전략, 미래 기술 동향, 그리고 개발자로서 반드시 갖춰야 할 보안 의식까지, 다각적인 시야를 얻을 수 있었습니다. 비록 모든 세션 내용을 완벽히 소화하지는 못했지만, 개발자 커뮤니티의 활기찬 에너지와 끊임없이 배우고 공유하는 문화를 직접 경험하며 큰 동기 부여를 받았습니다.</p>
<p>이처럼 소중한 기회를 제공해주신 <strong>RevenueCat</strong>에 다시 한번 진심으로 감사드립니다. 덕분에 국내 최대 규모의 안드로이드 개발자 컨퍼런스에 직접 참여하여 많은 것을 보고 배울 수 있었습니다. 내년 드로이드나이츠에서도 더욱 성장한 개발자의 모습으로 다시 참여하여, 그때는 지금보다 훨씬 깊이 있는 이해와 통찰력을 가지고 세션을 즐기고 싶습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[hoonie-blog: 테마를 위한 지속 쿠키 도입기]]></title>
            <link>https://velog.io/@camille_protocol/hoonie-blog-%ED%85%8C%EB%A7%88%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%A7%80%EC%86%8D-%EC%BF%A0%ED%82%A4-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@camille_protocol/hoonie-blog-%ED%85%8C%EB%A7%88%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%A7%80%EC%86%8D-%EC%BF%A0%ED%82%A4-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Mon, 16 Jun 2025 14:51:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 이전에 운영하던 블로그에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2025년 4월 1일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서">들어가면서</h2>
<p>최근 블로그에 관한 피드백을 한 지인으로부터 받게 되었습니다. 다음과 같은 질문이었습니다.</p>
<blockquote>
<p>“블로그의 테마 쿠키를 보니 세션 쿠키로 설정되어 있던데, 혹시 지속 쿠키를 사용하지 않으신 특별한 이유가 있으신가요? 세션 쿠키는 브라우저를 종료하면 삭제되어서 여쭤봅니다.”</p>
</blockquote>
<p>이 질문을 받고, 무심코 넘겼던 테마 설정 방식에 대해 다시 생각해보게 되었습니다. 브라우저를 닫았다가 다시 열면 테마가 초기화되는 현상은 분명 사용자 입장에서 불편하게 느껴질 수 있는 부분입니다. 실제로 지금까지는 세션 쿠키로도 충분하다고 판단했기에 별다른 의문 없이 사용해왔지만, 이 피드백을 계기로 테마 상태를 어떤 방식으로 저장하고 유지하는 것이 적절할지 고민하게 되었습니다.</p>
<hr>
<h2 id="쿠키-다시-보기">쿠키, 다시 보기</h2>
<p>회고를 진행하기에 앞서, 먼저 질문을 하나 던져 봅시다.</p>
<blockquote>
<p>&quot;쿠키란 무엇인가요?&quot;</p>
</blockquote>
<p><strong>쿠키</strong>(cookie)는 사용자의 웹 브라우저에 저장되는 <strong>작은 데이터 조각</strong>으로, 웹사이트가 사용자의 상태나 정보를 기억할 수 있도록 도와주는 수단입니다. 서버 또는 클라이언트에서 설정할 수 있으며, 이후 사용자가 해당 사이트를 다시 방문할 때 요청과 함께 자동으로 전송됩니다.</p>
<p>이렇게 전송된 쿠키는 로그인 상태 유지, 테마와 같은 사용자 선호 설정 저장, 방문 이력 기록 등에 사용될 수 있습니다.</p>
<h3 id="세션-쿠키-vs-지속-쿠키">세션 쿠키 vs 지속 쿠키</h3>
<p>쿠키는 <strong>유효 기간이 명시되었는지</strong>에 따라 두 가지로 나뉩니다:</p>
<ul>
<li><p><strong>세션 쿠키(Session Cookie)</strong><br>만료 시간이 명시되지 않은 쿠키입니다. 브라우저를 종료하면 자동으로 삭제됩니다.<br>예: 로그인한 상태를 유지하되, 브라우저를 닫으면 로그아웃되도록 처리하는 경우</p>
</li>
<li><p><strong>지속 쿠키(Persistent Cookie)</strong><br>명시적인 <code>Expires</code> 또는 <code>Max-Age</code> 속성이 설정된 쿠키로, <strong>브라우저를 닫더라도 일정 기간 동안 유지</strong>됩니다.<br>예: 다크모드 설정처럼 사용자의 선호를 장기간 기억할 필요가 있는 경우</p>
</li>
</ul>
<h3 id="클라이언트-측에서의-설정">클라이언트 측에서의 설정</h3>
<p>JavaScript에서 쿠키를 설정할 때는 <code>document.cookie</code>를 사용합니다.</p>
<pre><code class="language-js">document.cookie = &#39;theme=dark; max-age=604800; path=/; SameSite=Lax&#39;;</code></pre>
<p>각 속성은 다음과 같은 의미를 가집니다:</p>
<ul>
<li><code>theme=dark</code>: 저장할 쿠키 값</li>
<li><code>max-age=604800</code>: 7일 동안 유지</li>
<li><code>path=/</code>: 사이트 전체에서 유효</li>
<li><code>SameSite=Lax</code>: 보안 속성</li>
</ul>
<p>이처럼 쿠키는 <strong>짧고 간단한 문자열 조각</strong>이지만, 사용자 경험의 연속성과 품질을 좌우할 수 있는 중요한 요소입니다. 특히 테마 설정처럼 사용자의 &#39;선호&#39;가 반영된 값은 가능한 한 지속적으로 유지되어야 하며, 이를 위해서는 세션 쿠키보다는 지속 쿠키가 더 적합한 선택일 수 있습니다.</p>
<h2 id="세션에서-지속-쿠키로-전환하기">세션에서 지속 쿠키로 전환하기</h2>
<p>쿠키의 개념과 세션, 지속 쿠키의 차이점에 대한 이해를 바탕으로 기존의 <code>ThemeProvider</code>를 다음과 같이 개선하게 되었습니다.</p>
<h3 id="변경-전-세션-쿠키">변경 전: 세션 쿠키</h3>
<p>기존에는 <code>toggleTheme</code> 함수 안에서 <code>document.cookie</code>를 통해 쿠키를 설정하되, <code>max-age</code>나 <code>expires</code> 값을 지정하지 않았습니다. 따라서 기본적으로 브라우저 세션이 끝나면 해당 쿠키는 삭제되었습니다.</p>
<pre><code class="language-tsx">document.cookie = `theme=${newTheme}; path=/`;</code></pre>
<h3 id="변경-후-지속-쿠키">변경 후: 지속 쿠키</h3>
<p>쿠키가 브라우저 종료 후에도 유지되도록 하기 위해, <code>max-age</code> 값을 명시한 형태로 변경하였습니다. 또한 쿠키 옵션을 상수화하여 재사용성과 가독성을 높였습니다.</p>
<pre><code class="language-tsx">document.cookie = `${THEME.COOKIE.NAME}=${newTheme}; max-age=${THEME.COOKIE.MAX_AGE}; ${THEME.COOKIE.OPTIONS}`;</code></pre>
<p>또한 테마 초기화 시점에서도 동일한 방식으로 쿠키를 설정하여, 테마 설정이 브라우저 재접속 이후에도 유지되도록 하였습니다.</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
    document.cookie = `${THEME.COOKIE.NAME}=${initialTheme}; max-age=${THEME.COOKIE.MAX_AGE}; ${THEME.COOKIE.OPTIONS}`;
}, [initialTheme]);</code></pre>
<p>이러한 수정은 사용자가 다크모드 혹은 라이트모드로 설정한 테마가 <strong>브라우저를 닫았다 다시 열어도 유지되는 효과</strong>를 가져오며, 결과적으로 더 일관된 사용자 경험을 제공할 수 있게 되었습니다.</p>
<h2 id="쿠키를-어떻게-갱신할-것인가">쿠키를 어떻게 갱신할 것인가</h2>
<p>지속 쿠키를 도입하면서 가장 고민했던 부분은 <strong>언제 어떤 방식으로 쿠키를 갱신할 것인가</strong>였습니다. 사용자가 테마를 설정하고 나서 일정 기간 동안 그 상태를 유지하도록 하는 것이 목표였기 때문에, 쿠키를 무조건 매번 설정하는 방식과 조건부로 설정하는 방식 사이에서 선택이 필요했습니다.</p>
<h3 id="7일의-기적">7일의 기적</h3>
<p>지속 쿠키를 사용할 때 중요한 요소 중 하나는 쿠키의 <strong>유효 기간 설정</strong>입니다. 쿠키의 수명은 <code>max-age</code> 혹은 <code>expires</code> 속성으로 설정되며, 너무 짧으면 사용자 경험이 단절되고, 너무 길면 보안상 문제가 될 수 있습니다.</p>
<p>이를 설정함에 있어, 저는 <a href="https://www.cookiestatus.com/">cookiestatus.com</a>에서 정리한 주요 브라우저의 퍼스트 파티 쿠키 수명 정책을 참고했습니다:</p>
<table>
<thead>
<tr>
<th>브라우저</th>
<th>수명 제한</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Chrome</strong></td>
<td>400일</td>
<td>—</td>
</tr>
<tr>
<td><strong>Safari</strong></td>
<td>7일</td>
<td>Intelligent Tracking Prevention (ITP) 적용</td>
</tr>
<tr>
<td><strong>Firefox</strong></td>
<td>기본적으로 없음, 단 조건부 삭제 가능</td>
<td>45일간 사용자 상호작용 없는 추적 도메인은 삭제 대상</td>
</tr>
<tr>
<td><strong>Edge</strong></td>
<td>Chrome 기반과 동일</td>
<td>—</td>
</tr>
<tr>
<td><strong>Brave</strong></td>
<td>기본적으로 추적 차단, 7일 제한 가능성 있음</td>
<td>상황에 따라 달라짐</td>
</tr>
</tbody></table>
<p>이 중 Safari는 <strong>ITP(Intelligent Tracking Prevention)</strong> 정책에 따라 퍼스트 파티 쿠키라도 <strong>7일 후 만료</strong>되는 제약이 있으며, Firefox 역시 <strong>특정 조건 하에서 쿠키를 자동으로 삭제</strong>합니다. 예를 들어, 추적자로 간주되는 도메인 중 <strong>45일 이내 사용자 상호작용</strong>이 없는 경우, 저장소가 주기적으로 초기화됩니다.</p>
<p>따라서 모든 브라우저 환경에서 테마 상태가 예상대로 유지되도록 하기 위해, 저는 <code>max-age</code> 값을 <strong>7일</strong>로 설정하였습니다. 이는 Safari의 제한에 대응하면서, Firefox의 조건부 정책에도 어느 정도 대비할 수 있는 실용적인 기준이라고 판단했습니다.</p>
<h3 id="매번-설정하는-현재-방식">매번 설정하는 현재 방식</h3>
<p>현재 블로그에서는 페이지가 로드될 때마다 <code>document.cookie</code>를 통해 쿠키를 설정하는 방식을 사용하고 있습니다. 코드가 단순하고 직관적이며, 클라이언트 환경에서 빠르게 구현할 수 있다는 장점이 있습니다.</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
    document.cookie = `${THEME.COOKIE.NAME}=${initialTheme}; max-age=${THEME.COOKIE.MAX_AGE}; ${THEME.COOKIE.OPTIONS}`;
}, [initialTheme]);</code></pre>
<p>또한 테마를 전환할 때도 동일한 방식으로 쿠키를 재설정합니다.</p>
<pre><code class="language-tsx">const toggleTheme = () =&gt; {
    const newTheme = theme === &#39;light&#39; ? &#39;dark&#39; : &#39;light&#39;;
    setTheme(newTheme);
    document.cookie = `${THEME.COOKIE.NAME}=${newTheme}; max-age=${THEME.COOKIE.MAX_AGE}; ${THEME.COOKIE.OPTIONS}`;
    document.documentElement.classList.toggle(&#39;dark&#39;);
};</code></pre>
<h3 id="조건부-갱신-방식과의-비교">조건부 갱신 방식과의 비교</h3>
<p>한편으로는, 쿠키에 생성 시각을 저장하고 일정 시간이 지났을 때만 쿠키를 갱신하는 방식도 고려해보았습니다. 예를 들어, 쿠키에 타임스탬프를 포함하여 6일 이상이 경과했을 때만 새로운 쿠키를 설정하는 방식입니다. 이 방법은 불필요한 쿠키 갱신을 줄일 수 있고, 구조적인 확장에도 유리합니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>매번 설정 (현재 방식)</th>
<th>조건부 갱신 (타임스탬프)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>쿠키 갱신 빈도</strong></td>
<td>페이지 로드 시마다 항상 갱신</td>
<td>일정 시간 경과 시에만 갱신</td>
</tr>
<tr>
<td><strong>성능 부하</strong></td>
<td>쓰기 연산이 매번 발생</td>
<td>쓰기 연산이 줄어듦</td>
</tr>
<tr>
<td><strong>코드 복잡도</strong></td>
<td>단순하고 직관적</td>
<td>유틸리티 함수 및 시간 계산 로직 필요</td>
</tr>
<tr>
<td><strong>구현 난이도</strong></td>
<td>쉽고 빠르게 적용 가능</td>
<td>상대적으로 구현 복잡</td>
</tr>
<tr>
<td><strong>유지보수성</strong></td>
<td>구조화는 어려우나 간단한 유지보수 가능</td>
<td>구조화되어 유지보수와 확장에 유리</td>
</tr>
<tr>
<td><strong>사용자 경험 차이</strong></td>
<td>없음</td>
<td>없음</td>
</tr>
</tbody></table>
<hr>
<h2 id="나오면서">나오면서</h2>
<p>블로그의 규모나 페이지 사용 패턴을 고려했을 때, 현재 방식은 성능에 거의 영향을 주지 않으며 구현과 유지보수가 용이하다는 장점이 있습니다. 더 구조적인 조건부 갱신 방식도 고려해보았지만, 현재로서는 매번 쿠키를 설정하는 방식이 개인 블로그 수준에서는 충분히 합리적인 선택이라고 판단했습니다. 추후 필요에 따라 개선할 수 있도록 가능성은 열어두되, 지금의 단순한 구조가 오히려 유지보수 측면에서는 유리하다고 생각합니다.</p>
<p>이번 작업을 통해 <strong>기술을 올바르게 이해하고 사용하는 방법</strong>에 대해 다시 한번 생각해보게 되었습니다. 처음에는 단순히 쿠키의 수명만 연장하면 된다고 생각했지만, 실제로는 각 브라우저의 정책, 사용자의 경험, 코드의 구조 등 다양한 요소들이 얽혀 있다는 사실을 알게 되었습니다. 앞으로도 기술을 사용할 때는 기능만이 아니라 <strong>맥락과 목적에 맞게 적용하는 태도</strong>가 중요하다는 점을 잊지 않으려 합니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://www.cookiestatus.com/">cookiestatus.com</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Guides/Cookies">HTTP 쿠키 - MDN</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025년 1분기 회고]]></title>
            <link>https://velog.io/@camille_protocol/2025%EB%85%84-1%EB%B6%84%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@camille_protocol/2025%EB%85%84-1%EB%B6%84%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 16 Jun 2025 14:50:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 이전에 운영하던 블로그에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2025년 3월 30일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서">들어가면서</h2>
<p>마지막으로 작성했던 회고가 <a href="https://hoonieblog.xyz/blog/retro-2023">2023년</a>이었습니다. 그동안 회고를 써야지 써야지 했다가 그렇게 2025년의 1분기가 다 지나갔습니다. 시간은 참 빠르게 흘러가는 것 같습니다. 그동안은 회고를 작성하면서 다양한 방식을 사용해 봤습니다. KPT와 4L 방식의 회고를 사용해 봤었는데, 모든 회고에 같은 방식을 사용하기보다는 상황에 맞게 각자 다른 회고 방식을 적용해보는 것이 좋을 것 같다는 생각이 들었습니다. 이번에 회고 방법에 대해서 조사하면서 <code>STAR</code>라는 회고 방식에 대해 알게 되었습니다. 해당 회고 방식은 다음과 같이 이루어져 있습니다.</p>
<ul>
<li><strong>Situation</strong>: 상황</li>
<li><strong>Task</strong>: 목표</li>
<li><strong>Action</strong>: 행동</li>
<li><strong>Result</strong>: 결과</li>
</ul>
<p>이러한 방식을 사용하면 각 회고 항목에 대해 체계적으로 정리할 수 있다는 생각이 들었습니다. 이번 회고에서는 <code>STAR</code> 회고 방식을 적용하여, 2025년의 1분기를 주니어 프론트엔드 개발자로 취업을 준비하며 했던 다양한 활동을 중점으로 되돌아보고자 합니다.</p>
<hr>
<h2 id="hoonie-blog-개인-기술-블로그-개발">hoonie-blog: 개인 기술 블로그 개발</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/91011f2d-a0c2-479f-9346-cc669f17fd4a/image.webp" alt="hoonie-blog"></p>
<h3 id="situation-상황">Situation (상황)</h3>
<p>블로그 플랫폼을 옮겨다니는 &quot;블로그 유목민&quot; 생활에 지쳐, 글쓰기에 집중할 수 있는 나만의 블로그가 필요했습니다. 또한 블로그를 직접 만들어보고 꾸준히 가꿔나가는 과정을 통해 성장하고 싶었습니다.</p>
<h3 id="task-목표">Task (목표)</h3>
<ul>
<li>불필요한 의존성을 최소화한 블로그 구축</li>
<li>글쓰기 경험을 해치지 않는 미니멀한 디자인 적용</li>
<li>SEO 최적화를 통한 검색 노출 향상</li>
<li>기초부터 시작해 개선해나가는 과정을 통한 성장</li>
</ul>
<h3 id="action-행동">Action (행동)</h3>
<ul>
<li>Next.js와 MDX를 기반으로 한 블로그 시스템 구축</li>
<li>다크모드 기능 구현 및 FOUC(Flash of Unstyled Content) 문제 해결을 위한 쿠키 기반 접근법 적용</li>
<li>사이트맵과 robots.txt 생성하여 SEO 최적화</li>
<li>Vercel Web Analytics와 Speed Insights 도입</li>
</ul>
<h3 id="result-결과">Result (결과)</h3>
<ul>
<li>의존성 최소화로 유지보수가 용이한 블로그 구축 완료</li>
<li>다크모드 전환 시 깜빡임 없는 UX 구현</li>
<li>검색엔진을 위한 기본적인 SEO 최적화 완료</li>
<li>꾸준한 업데이트를 통한 개발 역량 성장</li>
</ul>
<hr>
<h2 id="토이-프로젝트-moty-개발-및-연사-참여">토이 프로젝트 MOTY 개발 및 연사 참여</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/1507bc4b-8cd4-4d8e-9844-f2d8569cc7e0/image.webp" alt="MOTY"></p>
<h3 id="situation-상황-1">Situation (상황)</h3>
<p>크리스마스 이후 친구와 대화를 하던 중, 친구의 토이 프로젝트에 대해서 알게 됐습니다. 연말을 맞아 비슷한 컨셉의 토이 프로젝트를 만들어보고 싶었고, 새해까지 얼마 남지 않은 상황에서 빠르게 프로젝트를 개발하고 배포해야 했습니다.</p>
<h3 id="task-목표-1">Task (목표)</h3>
<ul>
<li>사용자가 올해의 영화를 선택하고 공유할 수 있는 웹 서비스 개발</li>
<li>닉네임 입력, 영화 선택, 한줄 평 남기기, 결과 페이지 공유라는 주요 기능 구현</li>
<li>기능 구현을 넘어 웹 접근성, UX 등을 고려한 서비스 개발</li>
</ul>
<h3 id="action-행동-1">Action (행동)</h3>
<ul>
<li>빠른 개발을 위해 피그마로 간단한 프로토타이핑 진행</li>
<li>불필요한 기술 스택 사용을 최소화</li>
<li>TMDB API를 활용한 영화 검색 기능 구현</li>
<li>페이지별 핵심 기능 개발</li>
<li>웹 접근성 개선 및 UX 개선</li>
</ul>
<h3 id="result-결과-1">Result (결과)</h3>
<ul>
<li>2일 만에 &#39;올해의 영화&#39;를 공유할 수 있는 웹 서비스 완성</li>
<li>웹 접근성 지침을 준수하여 모두가 사용할 수 있는 서비스 개발</li>
<li>이후 세미나에서 토이 프로젝트 개발 과정 및 성장을 공유할 수 있는 기회 확보</li>
</ul>
<h3 id="더-알아보기">더 알아보기</h3>
<ul>
<li><a href="https://velog.io/@camille_protocol/MOTY-%EC%98%AC%ED%95%B4%EC%9D%98-%EC%98%81%ED%99%94-%EA%B3%B5%EC%9C%A0-%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8">MOTY: 올해의 영화 공유 토이 프로젝트</a></li>
<li><a href="https://cafe.naver.com/winterlood/1520">02.19 세미나 및 연사 소개 - 하룻밤 사이에 뚝딱 만든 토이프로젝트가 더 나은 개발자로 성장하는 여정이 되기까지</a></li>
</ul>
<hr>
<h2 id="한입-챌린지-4기-typescript">한입 챌린지 4기: TypeScript</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/2fcce494-8051-4548-8fdf-c974a8ebb6f9/image.webp" alt=""></p>
<h3 id="situation-상황-2">Situation (상황)</h3>
<p>TypeScript는 이제 프론트엔드 개발 생태계에서 필수적인 기술이 됐습니다. JavaScript에 정적 타입을 추가한 TypeScript는 코드의 안정성을 높이고, 개발 경험을 개선해줍니다. 따라서 개발자로서 성장하기 위해 TypeScript에 대한 깊이 있는 이해가 필요했습니다.</p>
<h3 id="task-목표-2">Task (목표)</h3>
<ul>
<li>이정환님의 &#39;한 입 크기로 잘라먹는 타입스크립트&#39; 챌린지 참여</li>
<li>2주간 매일 강의를 수강하며 미션 완료하기</li>
<li>TypeScript에 익숙해지기</li>
</ul>
<h3 id="action-행동-2">Action (행동)</h3>
<ul>
<li>정해진 날짜별 강의를 듣고 학습한 내용 기반으로 미션 수행</li>
<li>하루 약 1시간 투자하여 강의 수강 및 미션 완료</li>
<li>학습한 개념을 실습을 통해 직접 적용해보는 경험</li>
<li>타입스크립트의 기본 문법부터 인터페이스, 제네릭, 타입 가드 등 핵심 개념 학습</li>
</ul>
<h3 id="result-결과-2">Result (결과)</h3>
<ul>
<li>타입스크립트의 기초를 체계적으로 학습</li>
<li>타입 추론 메커니즘과 유틸리티 타입을 활용하는 방법 이해</li>
<li>실제 프로젝트에 타입스크립트를 적용해볼 예정</li>
<li>아쉬운 점으로 학습 내용의 기록 부재를 인식하고 향후 개선점으로 삼음</li>
</ul>
<h3 id="더-알아보기-1">더 알아보기</h3>
<ul>
<li><a href="https://velog.io/@camille_protocol/%ED%95%9C%EC%9E%85-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9A%8C%EA%B3%A0">한입 타입스크립트 챌린지 회고</a></li>
<li><a href="https://inf.run/EvrS5">한 입 크기로 잘라먹는 타입스크립트(TypeScript)</a></li>
</ul>
<hr>
<h2 id="가상-dom의-오해와-진실-이해하기">가상 DOM의 오해와 진실 이해하기</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/06ce6d5b-7dcd-44a6-8f84-f945e2ca0aec/image.png" alt="VDOM"></p>
<h3 id="situation-상황-3">Situation (상황)</h3>
<p>리액트의 가상 DOM에 대해 &quot;실제 DOM보다 빠르다&quot;라는 오해가 있었고, 이 주장이 절반만 맞는 말이라는 인식에서 출발해 가상 DOM의 실체를 탐구하게 되었습니다. 특히 글의 설득력을 위해 보다 객관적인 근거가 될 수 있는 자료를 찾아보고 이를 바탕으로 가상 DOM의 성능을 분석하고자 했습니다.</p>
<h3 id="task-과제">Task (과제)</h3>
<ul>
<li>가상 DOM의 실제 성능과 역할을 정확히 이해하고 분석하기</li>
<li>가상 DOM의 등장 배경과 작동 원리 설명하기</li>
<li>리액트 파이버와 렌더링 프로세스, Diffing 알고리즘 탐구하기</li>
<li>가상 DOM에 대한 비판과 대안 기술 조사하기</li>
<li>실제 벤치마크 결과를 바탕으로 가상 DOM의 성능 분석하기</li>
</ul>
<h3 id="action-행동-3">Action (행동)</h3>
<ul>
<li>가상 DOM의 등장 배경과 브라우저 렌더링 과정을 분석</li>
<li>리액트 파이버 아키텍처와 작동 방식 연구</li>
<li>리액트의 렌더링 프로세스를 단계별로 분석</li>
<li>Diffing 알고리즘의 원리와 최적화 케이스 정리</li>
<li>Svelte와 Million.js와 같은 가상 DOM의 대안 기술 조사</li>
<li>2020년 발표된 논문의 벤치마크 결과를 통해 다양한 프레임워크의 성능 비교 분석</li>
</ul>
<h3 id="result-결과-3">Result (결과)</h3>
<ul>
<li>가상 DOM이 모든 상황에서 빠른 것은 아니라는 결론 도출</li>
<li>가상 DOM은 성능 최적화 도구를 넘어 선언적이고 상태 중심적인 UI 개발을 가능하게 하는 리액트의 핵심 개념임을 확인</li>
<li>가상 DOM을 무조건 기피할 필요는 없으며, 프로젝트의 특성과 요구사항에 맞는 기술을 선택하는 것이 중요하다는 결론 도출</li>
</ul>
<h3 id="더-알아보기-2">더 알아보기</h3>
<ul>
<li><a href="https://velog.io/@camille_protocol/%EA%B0%80%EC%83%81-DOM%EC%9D%98-%EC%98%A4%ED%95%B4%EC%99%80-%EC%A7%84%EC%8B%A4">가상 DOM의 오해와 진실</a></li>
<li><a href="https://old.million.dev/blog/virtual-dom">Virtual DOM: Back in Block</a></li>
<li><a href="https://svelte.dev/blog/virtual-dom-is-pure-overhead">Virtual DOM is pure overhead</a></li>
</ul>
<hr>
<h2 id="함수형-프로그래밍-스터디">함수형 프로그래밍 스터디</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/a8741a54-9c40-4d62-8151-87ce196ad2cd/image.webp" alt=""></p>
<h3 id="situation-상황-4">Situation (상황)</h3>
<p>선언적이고 읽기 쉬운 코드 작성을 위해 함수형 프로그래밍 패러다임에 대한 이해가 필요했습니다. 또한 앞으로 진행될 객체지향 스터디와, 그 이후의 멀티 패러다임 프로그래밍에 대한 학습을 위해 함수형 프로그래밍에 대한 이해가 필수적이라고 생각했습니다.</p>
<h3 id="task-목표-3">Task (목표)</h3>
<ul>
<li>함수형 프로그래밍의 핵심 개념 이해 및 적용</li>
<li>JavaScript/TypeScript에서 함수형 접근법 적용 방법 학습</li>
</ul>
<h3 id="action-행동-4">Action (행동)</h3>
<ul>
<li>온라인 스터디 그룹 결성 및 주 1회 스터디 진행</li>
<li>함수형 프로그래밍 관련 도서 및 자료 학습</li>
<li>순수 함수, 불변성, 고차 함수, 구현 패턴 등 개념 실습</li>
</ul>
<h3 id="result-결과-4">Result (결과)</h3>
<ul>
<li>함수형 프로그래밍의 기본 원칙과 패턴에 대한 이해 확립</li>
<li>스터디 그룹 내에서 지식 공유 및 네트워킹</li>
</ul>
<h3 id="더-알아보기-3">더 알아보기</h3>
<ul>
<li><a href="https://product.kyobobook.co.kr/detail/S000001952246">쏙쏙 들어오는 함수형 코딩</a></li>
</ul>
<hr>
<h2 id="containerpresentational-react-디자인-패턴-학습">Container/Presentational React 디자인 패턴 학습</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/490bac9d-12ac-433c-bb96-49f7766e568f/image.webp" alt=""></p>
<h3 id="situation-상황-5">Situation (상황)</h3>
<p>이전 토마토들 프로젝트에서 폴더 구조에 대한 고민을 했고, 회고에서 UI와 로직을 분리하기 위해 컨테이너 패턴을 적용했다고 언급했습니다. 그러나 이력서 피드백을 통해 실제로 적용한 패턴이 제대로 된 컨테이너 패턴이 아니라는 것을 깨달았고, 해당 패턴에 대해 학습해보고자 했습니다.</p>
<h3 id="task-목표-4">Task (목표)</h3>
<ul>
<li>React의 Container/Presentational 패턴에 대해 학습하기</li>
<li>토마토들 프로젝트에서 사용한 패턴이 무엇인지 정확히 파악하기</li>
<li>디자인 패턴의 올바른 적용과 이해에 대해 정리하기</li>
</ul>
<h3 id="action-행동-5">Action (행동)</h3>
<ul>
<li>Container/Presentational 패턴에 대한 학습</li>
<li>전통적인 Container/Presentational 패턴과, Hooks 시대의 패턴 변화 탐구</li>
<li>토마토들 프로젝트에서 사용한 패턴의 특징 분석 및 파악</li>
<li>패턴 사용의 이유와 문제점 분석</li>
</ul>
<h3 id="result-결과-5">Result (결과)</h3>
<ul>
<li>토마토들 프로젝트에서 사용한 패턴이 제대로 된 컨테이너 패턴이 아니라는 것을 학습</li>
<li>기술적 개념을 정확히 이해하고 올바르게 적용하는 것의 중요성을 깨달음</li>
</ul>
<h3 id="더-알아보기-4">더 알아보기</h3>
<ul>
<li><a href="https://velog.io/@camille_protocol/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-ContainerPresentational">React 디자인 패턴: Container/Presentational</a></li>
<li><a href="https://velog.io/@camille_protocol/%ED%86%A0%EB%A7%88%ED%86%A0%EB%93%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%BA%EC%9C%BC%EB%A9%B4%EC%84%9C">토마토들 프로젝트를 맺으면서</a></li>
</ul>
<hr>
<h2 id="오픈소스-기여-zed-에디터-이슈-제보">오픈소스 기여: Zed 에디터 이슈 제보</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/b96711b4-b9c2-4c7b-a2d0-0e98f1a887c1/image.webp" alt=""></p>
<h3 id="situation-상황-6">Situation (상황)</h3>
<p>개발에 사용하는 Zed 에디터를 사용하면서 한글 관련 치명적인 문제들을 발견했습니다. JSX 파일에서 한글 입력 시 에디터가 충돌하는 현상과 내장 터미널에서 한글 파일명이 제대로 표시되지 않는 유니코드 정규화 이슈가 있었습니다. 이러한 문제들은 한국어 사용자로서 개발 경험을 크게 저해하는 요소였고, 오픈소스 프로젝트에 기여할 수 있는 기회이기도 했습니다.</p>
<h3 id="task-목표-5">Task (목표)</h3>
<ul>
<li>Zed 에디터의 한글 관련 버그 상세 분석 및 문서화</li>
<li>재현 가능한 형태로 이슈 정리</li>
<li>개발자가 이해하기 쉽게 기술적으로 정확한 이슈 리포트 작성</li>
<li>가능한 해결책 제안</li>
</ul>
<h3 id="action-행동-6">Action (행동)</h3>
<ul>
<li>JSX 파일에서 발생하는 UTF-8 문자 경계 오류를 재현 가능한 코드 예제와 함께 정리</li>
<li>내장 터미널의 유니코드 정규화(NFC/NFD) 이슈에 대한 상세 분석</li>
<li>다양한 환경 설정을 통한 문제 해결 시도 (locale 설정 등)</li>
<li>문제를 명확히 보여주는 예제 코드 작성</li>
<li>상세한 오류 로그와 시스템 정보 수집</li>
<li>GitHub 이슈를 통해 체계적으로 문제 상황 보고</li>
</ul>
<h3 id="result-결과-6">Result (결과)</h3>
<ul>
<li>Zed 개발팀으로부터 상세한 이슈 제보에 대한 긍정적인 피드백 획득</li>
<li>이슈가 개발팀에 의해 우선순위가 지정되고 국제화(internationalization) 태그가 추가됨</li>
<li>개발자들이 직접 이슈를 담당하고 해결 과정에 착수</li>
<li>오픈소스 프로젝트에 기여하는 과정에서 기술적 커뮤니케이션 능력 향상</li>
</ul>
<h3 id="더-알아보기-5">더 알아보기</h3>
<ul>
<li><a href="https://github.com/zed-industries/zed/issues/26036">Unicode Normalization Issue in Zed&#39;s Built-in Terminal on macOS #26036</a></li>
<li><a href="https://github.com/zed-industries/zed/issues/27599">Zed crashes when editing JSX/React code with Korean characters due to UTF-8 character boundary error #27599</a></li>
</ul>
<hr>
<h2 id="이번-분기의-수확">이번 분기의 수확</h2>
<p>이번 분기는 프론트엔드 개발자로서 기술적 깊이와 폭을 모두 확장하는 시간이었습니다. 특히 블로그 개발 과정에서 실제 사용자 경험과 성능 최적화, SEO까지 고려하는 종합적인 시각을 갖게 된 점이 가장 큰 성장이었습니다.</p>
<h3 id="성장한-점">성장한 점</h3>
<p>이번 분기 동안 <strong>이론적 지식을 실제 프로젝트에 적용</strong>하는 과정에서 예상치 못한 문제들을 해결하는 능력을 키울 수 있었습니다. 특히 MOTY 프로젝트를 통해 <strong>기술적 내용을 다른 개발자들에게 효과적으로 전달</strong>하는 커뮤니케이션 스킬을 기를 수 있었고, 이는 세미나 발표 경험으로 이어졌습니다.</p>
<p>한입 챌린지와 함수형 프로그래밍 스터디를 통해 <strong>타입스크립트</strong>와 <strong>함수형 프로그래밍</strong>이 코드의 품질과 유지보수성을 높이는 데 얼마나 중요한지 체감했습니다. 또한 가상 DOM에 관한 조사를 통해 모든 상황에서 최적의 기술은 없으며, <strong>상황에 맞는 적절한 기술 선택</strong>의 중요성을 깨달았고, 리액트 디자인 패턴 학습을 통해 <strong>기술적 개념의 정확한 이해와 적용</strong>이 얼마나 중요한지 알게 되었습니다.</p>
<h3 id="개선할-점">개선할 점</h3>
<p><strong>학습 내용의 기록과 공유 부족</strong>이 가장 큰 아쉬움으로 남습니다. 한입 타입스크립트 챌린지를 수강하며 매일 배운 개념들을 정리하지 않았고, 결과적으로 핵심 개념들을 다시 찾아보는 시간이 필요했습니다. 다음 학습에서는 옵시디언과 같은 기록 앱에 당일 학습 내용을 바로 정리하는 습관을 들여야겠습니다.</p>
<p><strong>이력서의 프로젝트 성과 계량화</strong> 부분이 미흡했습니다. 지인들로부터 &quot;프로젝트에서 달성한 성과를 수치로 표현하라&quot;는 피드백을 받았지만, 토이 프로젝트의 특성상 사용자 수나 성능 개선 지표를 제시하기 어려웠습니다. 다음 프로젝트에서는 시작 단계부터 측정 가능한 지표를 설정하고 추적할 필요가 있다고 느꼈습니다.</p>
<hr>
<h2 id="나오면서-2분기-계획">나오면서: 2분기 계획</h2>
<p>다음 분기에는 개인 블로그에 <strong>페이지네이션, 태그 기능</strong> 등을 추가 개발하여 사용자 경험을 개선하고, <strong>프론트엔드 성능 최적화</strong>에 관한 학습을 진행할 계획입니다. 특히 <strong>월 2회 이상 기술 블로그 포스팅</strong> 작성을 습관화하고, 최소 <strong>1개 이상의 오픈 소스 프로젝트에 기여</strong>하는 것을 목표로 삼고 있습니다. 이번 분기의 경험을 토대로 더 체계적이고 깊이 있는 학습을 진행하며 주니어 프론트엔드 개발자로서의 역량을 한층 더 강화하고자 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 디자인 패턴: Container/Presentational]]></title>
            <link>https://velog.io/@camille_protocol/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-ContainerPresentational</link>
            <guid>https://velog.io/@camille_protocol/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-ContainerPresentational</guid>
            <pubDate>Mon, 16 Jun 2025 14:39:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 이전에 운영하던 블로그에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2025년 3월 23일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서">들어가면서</h2>
<p>토마토들 회고를 진행하면서 <a href="https://hoonieblog.xyz/blog/retro-tomatoes#%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0">폴더 구조</a>에 대한 이야기를 한 적이 있습니다. 당시 프로젝트에서는 UI와 로직을 분리하기 위해 컴포넌트를 어떻게 구조화할지 고민했었고, 블로그 글을 참고해서 <strong>컨테이터 패턴</strong>을 적용했다고 언급했습니다.</p>
<p>그러나 최근 이력서 피드백을 받으면서 이것이 제대로 된 컨테이너 패턴이 아니라는 것을 깨닫게 되었습니다. 이번 글에서는 그 고민과 연결되는 React의 대표적인 디자인 패턴인 Container/Presentational 패턴에 대해 자세히 알아보겠습니다.</p>
<h2 id="containerpresentational-패턴이란">Container/Presentational 패턴이란?</h2>
<p>이 패턴은 Dan Abramov가 <a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0">Presentational and Container Components</a>라는 제목의 글에서 처음 소개한 패턴으로, 애플리케이션 로직과 UI를 명확하게 분리하는 방법을 제시합니다. 이 패턴을 사용하면 다음과 같은 두 가지 타입의 컴포넌트로 관심사를 분리할 수 있습니다:</p>
<ol>
<li><strong>Presentational 컴포넌트</strong>: 어떻게 보여지는지(how things look)에 집중</li>
<li><strong>Container 컴포넌트</strong>: 어떻게 동작하는지(how things work)에 집중</li>
</ol>
<p>예시를 통해 Container/Presentational 패턴을 살펴볼까요? 사용자의 정보를 불러와 표시하는 컴포넌트를 생각해봅시다.</p>
<h2 id="presentational-컴포넌트">Presentational 컴포넌트</h2>
<p>Presentational 컴포넌트는 다음과 같은 특징을 가집니다:</p>
<ul>
<li><strong>데이터를 props로만 받음</strong>: 외부로부터 데이터를 전달받아 표시합니다.</li>
<li><strong>UI에만 집중</strong>: 어떻게 보여질지에만 관심을 둡니다.</li>
<li><strong>일반적으로 상태를 갖지 않음</strong>: 대부분 순수 함수형 컴포넌트로 구현됩니다(UI 관련 상태 제외).</li>
<li><strong>데이터 변경 방법을 모름</strong>: 데이터 변경은 props로 전달받은 콜백 함수를 통해서만 수행합니다.</li>
<li><strong>재사용성이 높음</strong>: 다양한 Container와 함께 사용될 수 있습니다.</li>
</ul>
<pre><code class="language-jsx">// UserProfileView.jsx - Presentational 컴포넌트
import &#39;./UserProfile.css&#39;;

const UserProfileView = ({ user, onEditClick }) =&gt; (
    &lt;div className=&#39;user-profile&#39;&gt;
        &lt;img src={user.avatar} alt={user.name} className=&#39;user-profile__avatar&#39; /&gt;
        &lt;div className=&#39;user-profile__info&#39;&gt;
            &lt;h2 className=&#39;user-profile__name&#39;&gt;{user.name}&lt;/h2&gt;
            &lt;p className=&#39;user-profile__email&#39;&gt;이메일: {user.email}&lt;/p&gt;
            &lt;p className=&#39;user-profile__date&#39;&gt;
                가입일: {new Date(user.createdAt).toLocaleDateString()}
            &lt;/p&gt;
            &lt;button className=&#39;user-profile__edit-button&#39; onClick={onEditClick}&gt;
                프로필 수정
            &lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
);

export default UserProfileView;</code></pre>
<p><code>UesrProfileView</code> 컴포넌트는 사용자 프로필을 어떻게 표시할지에만 집중하고 있습니다. 데이터를 어디서 가져오는지, 어떻게 처리하는지에 대한 로직은 전혀 포함하지 않습니다.</p>
<h2 id="container-컴포넌트">Container 컴포넌트</h2>
<p>Container 컴포넌트는 다음과 같은 특징을 가집니다:</p>
<ul>
<li><strong>데이터 제공에 집중</strong>: 데이터를 가져오고 처리하는 로직을 담당합니다.</li>
<li><strong>상태 관리</strong>: 데이터와 관련된 상태를 관리합니다.</li>
<li><strong>API 호출 및 데이터 변환</strong>: 외부 API와 통신하고 데이터를 가공합니다.</li>
<li><strong>직접적인 UI 렌더링 없음</strong>: 대신 Presentational 컴포넌트에 데이터를 전달합니다.</li>
<li><strong>라이프사이클 메소드/효과 활용</strong>: 컴포넌트 생명주기에 따른 데이터 처리를 관리합니다.</li>
</ul>
<pre><code class="language-jsx">import { Component } from &#39;react&#39;;
import UserProfileView from &#39;./UserProfileView&#39;;

class UserProfileContainer extends Component {
    state = {
        user: null,
        loading: true,
        error: null,
    };

    componentDidMount() {
        // 데이터 페칭 로직
        fetch(`https://api.example.com/users/${this.props.userId}`)
            .then((response) =&gt; response.json())
            .then((data) =&gt; {
                this.setState({ user: data, loading: false });
            })
            .catch((error) =&gt; {
                this.setState({ error, loading: false });
            });
    }

    handleEditClick = () =&gt; {
        // 프로필 편집 페이지로 이동 로직
        const { user } = this.state;
        window.location.href = `/edit-profile/${user.id}`;
    };

    render() {
        const { user, loading, error } = this.state;

        if (loading) return &lt;div&gt;로딩 중...&lt;/div&gt;;
        if (error) return &lt;div&gt;에러 발생: {error.message}&lt;/div&gt;;
        if (!user) return &lt;div&gt;사용자를 찾을 수 없습니다&lt;/div&gt;;

        // Presentational 컴포넌트에 데이터와 이벤트 핸들러 전달
        return &lt;UserProfileView user={user} onEditClick={this.handleEditClick} /&gt;;
    }
}

export default UserProfileContainer;</code></pre>
<p><code>UserProfileContainer</code> 컴포넌트는 사용자 데이터를 가져오고, 상태를 관리하며, 이벤트 핸들러를 정의하는 등의 로직을 담당합니다. 실제 UI 렌더링은 <code>UserProfileView</code> 컴포넌트에 위임하고 있습니다.</p>
<p>지금까지 이야기한 두 컴포넌트를 다이어그램으로 표시하면 다음과 같습니다:</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/1aba7870-a6ba-45c7-9642-0286ce37015e/image.webp" alt="conatiner-presentational-diagram"></p>
<h2 id="hooks-시대의-containerpresentational-패턴">Hooks 시대의 Container/Presentational 패턴</h2>
<p>React 16.8에서 <strong>Hooks</strong>가 도입되면서 Container/Presentational 패턴에도 변화가 생겼습니다. 이전에는 클래스 컴포넌트로 Container를 구현하고 함수형 컴포넌트로 Presentational을 구현하는 경우가 많았지만, Hooks를 사용하면 함수형 컴포넌트만으로도 상태 관리와 사이드 이펙트 처리가 가능해졌습니다.</p>
<h3 id="커스텀-hook을-활용한-로직-분리">커스텀 Hook을 활용한 로직 분리</h3>
<pre><code class="language-jsx">import { useState, useEffect } from &#39;react&#39;;

export const useUser = (userId) =&gt; {
    const [state, setState] = useState({
        user: null,
        loading: true,
        error: null,
    });

    useEffect(() =&gt; {
        const fetchUser = async () =&gt; {
            try {
                const response = await fetch(`https://api.example.com/users/${userId}`);
                const data = await response.json();
                setState({ user: data, loading: false, error: null });
            } catch (error) {
                setState({ user: null, loading: false, error });
            }
        };

        fetchUser();
    }, [userId]);

    return {
        ...state,
        refreshUser: () =&gt; {
            setState((prev) =&gt; ({ ...prev, loading: true }));
        },
    };
};</code></pre>
<h3 id="hooks로-인한-변화-단일-컴포넌트-접근법">Hooks로 인한 변화: 단일 컴포넌트 접근법</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/b25f5436-c9e6-4f81-991b-3531dd3590a9/image.webp" alt="hooks-diagram"></p>
<p>Hooks를 사용하면 로직을 커스텀 훅으로 추출하고, 컴포넌트 자체는 UI 표현에 집중할 수 있습니다.</p>
<pre><code class="language-jsx">import { useUser } from &#39;./useUser&#39;;

const UserProfile = ({ userId = 1 }) =&gt; {
    const { user, loading, error } = useUser(userId);

    if (loading) return &lt;div&gt;로딩 중...&lt;/div&gt;;
    if (error) return &lt;div&gt;에러 발생: {error.message}&lt;/div&gt;;
    if (!user) return &lt;div&gt;사용자를 찾을 수 없습니다&lt;/div&gt;;

    const handleEditClick = () =&gt; {
        window.location.href = `/edit/${user.id}`;
    };

    return (
        &lt;div className=&#39;user-profile&#39;&gt;
            &lt;img src={user.avatar} alt={user.name} /&gt;
            &lt;h2&gt;{user.name}&lt;/h2&gt;
            &lt;p&gt;이메일: {user.email}&lt;/p&gt;
            &lt;p&gt;가입일: {new Date(user.createdAt).toLocaleDateString()}&lt;/p&gt;
            &lt;button onClick={handleEditClick}&gt;프로필 수정&lt;/button&gt;
        &lt;/div&gt;
    );
};

export default UserProfile;</code></pre>
<p>그렇다면 <strong>Container/Presentational 패턴</strong>은 더 이상 필요 없을까요? 그렇지 않습니다. Hooks가 많은 경우에 이 패턴을 대체할 수 있지만, 다음과 같은 상황에서는 여전히 Container/Presentational 패턴이 유용합니다:</p>
<ul>
<li><strong>큰 규모의 애플리케이션</strong>: 팀 규모가 크고 역할이 명확히 구분된 경우</li>
<li><strong>높은 재사용성이 필요한 컴포넌트</strong>: 다양한 데이터 소스와 함께 사용될 UI 컴포넌트</li>
<li><strong>복잡한 로직과 UI를 포함하는 컴포넌트</strong>: 분리가 코드 가독성을 높이는 경우</li>
</ul>
<h2 id="토마토들에서-사용했던-패턴은-containerpresentational-패턴인가">토마토들에서 사용했던 패턴은 Container/Presentational 패턴인가?</h2>
<p>그럼 지금까지 다뤄본 내용을 바탕으로, 이전에 작성했던 토마토들 회고에서 언급한 구조가 Container/Presentational 패턴이 맞는지 살펴보겠습니다.</p>
<h3 id="패턴-분석">패턴 분석</h3>
<p>회고에서 설명한 <code>containers</code> 폴더의 사용 방식을 살펴보면:</p>
<pre><code class="language-tsx">import { fetchActivityContestAbstractWith } from &#39;@/lib/fetchActivityAbstractWith&#39;;
import Activity from &#39;@/containers/activity/Activity&#39;;

export default async function Page({ searchParams }: PageProps) {
    const filters = searchParams.filters?.split(&#39;,&#39;).filter(Boolean) || [];
    const sort = searchParams.sort || &#39;관련도순&#39;;

    const { data: activitiesContests, error } =
        await fetchActivityContestAbstractWith({
            filters,
            sort,
            mainCategory: &#39;대외활동&#39;,
        });

    if (error) {
        // 에러 핸들링 로직
    }

    return &lt;Activity activitiesContests={activitiesContests || []} /&gt;;
}</code></pre>
<p>위와 같은 구조를 가지고 있는 것을 확인할 수 있습니다. 이는 전통적인 Container/Presentational 패턴과는 미묘하게 다릅니다. 정확히 말하자면, <strong>Next.js의 페이지 컴포넌트와 컨테이너 컴포넌트의 하이브리드 패턴</strong>이라고 볼 수 있습니다.</p>
<h3 id="차이점">차이점</h3>
<ol>
<li><p><strong>역할 분배의 차이</strong>:</p>
<ul>
<li><strong>전통적인 패턴</strong>: Container 컴포넌트가 데이터 페칭, 상태 관리, 이벤트 핸들링을 모두 담당</li>
<li><strong>토마토들</strong>: <code>Page</code> 컴포넌트(Next.js 라우팅 컴포넌트)가 데이터 페칭을 담당하고, <code>Activity</code> 컴포넌트는 UI 렌더링과 상태 관리를 담당</li>
</ul>
</li>
<li><p><strong>데이터 흐름</strong>:</p>
<ul>
<li><strong>전통적인 패턴</strong>: Container가 데이터를 가져오고 Presentational에 전달</li>
<li><strong>토마토들</strong>: <code>Page</code>가 데이터를 가져와 <code>Activity</code>에 전달 (Container가 Presentational 역할만 하는 것이 아님)</li>
</ul>
</li>
</ol>
<h3 id="토마토들에-적용된-패턴의-정체">토마토들에 적용된 패턴의 정체</h3>
<p>여러 가지 자료를 찾아본 결과, 제가 사용한 패턴은 <strong>페이지 기반 데이터 페칭 패턴(Page-based Data Fetching Pattern)</strong> 또는 <strong>라우트 중심 구성 패턴(Route-centric Organization Pattern)</strong> 이라고 볼 수 있습니다.</p>
<ol>
<li><strong>라우트 컴포넌트(Page)</strong>: 데이터 페칭, URL 파라미터 처리, 에러 핸들링 등 라우팅 관련 로직 담당</li>
<li><strong>컨테이너 컴포넌트(Activity)</strong>: UI 구조화, 상태 관리, 사용자 인터랙션 처리 담당</li>
<li><strong>프레젠테이셔널 컴포넌트</strong>: UI 렌더링만 담당</li>
</ol>
<h3 id="패턴-사용에-대한-분석">패턴 사용에 대한 분석</h3>
<p>토마토들 프로젝트에서 이 패턴을 사용한 이유는 크게 두 가지였습니다.</p>
<p><strong>Next.js의 서버 컴포넌트 활용</strong>이 첫 번째 이유였습니다. 서버에서 데이터를 가져오는 것이 성능상 유리하다고 판단했고, 클라이언트-서버 컴포넌트 경계를 명확히 하고 싶었기 때문입니다.</p>
<p><strong>코드 구조화의 명확성</strong>이 두 번째 이유였습니다. 페이지 레벨의 로직과 컴포넌트 레벨의 로직을 분리하고 싶었고, 폴더 구조를 통해 관심사를 명확히 구분하고 싶었습니다.</p>
<p>하지만 이 패턴이 정말 필요한 선택이었는지 다시 생각해보면 몇 가지 문제점이 있었습니다.</p>
<p><strong>과도한 분리</strong>가 첫 번째 문제였습니다. 프로젝트 규모가 작았음에도 불구하고 불필요하게 복잡한 구조를 만들었고, 실제로는 대부분의 컴포넌트가 단순한 렌더링 작업만 수행했습니다.</p>
<p><strong>Next.js의 기본 패턴과의 불일치</strong>가 두 번째 문제였습니다. Next.js는 이미 페이지 기반 라우팅을 제공하며 서버 컴포넌트를 통한 데이터 페칭을 권장하는데, 추가적인 컨테이너 계층이 오히려 코드의 복잡성을 증가시켰습니다.</p>
<p><strong>유지보수 비용</strong>이 세 번째 문제였습니다. 컴포넌트 간 데이터 흐름을 추적하기 어려웠고, 새로운 기능 추가 시 여러 파일을 수정해야 하는 번거로움이 있었습니다.</p>
<p>당시에는 &quot;좋은 구조&quot;를 만들고 싶은 욕심에 불필요한 복잡성을 추가했던 것 같습니다. 프로젝트의 규모와 복잡성을 고려했을 때, 더 단순한 구조로 시작하고 필요에 따라 점진적으로 개선하는 것이 더 나은 접근 방식이었을 것 같습니다.</p>
<h3 id="결론">결론</h3>
<p>지금까지 React의 Container/Presentational 패턴에 대해 자세히 알아보았습니다. 이 패턴은 Dan Abramov가 제안한 것으로, 애플리케이션의 로직과 UI를 명확하게 분리하는 방법을 제시합니다. Presentational 컴포넌트는 UI 표현에만 집중하고, Container 컴포넌트는 데이터 처리와 상태 관리에 집중하는 것이 핵심입니다.</p>
<p>하지만 제가 이전에 토마토들 프로젝트에서 사용했던 패턴은 이와는 달랐습니다. Next.js의 특성을 활용한 <strong>라우트-컨테이너 패턴</strong>이었는데, 이를 Container/Presentational 패턴이라고 잘못 이해하고 있었습니다.</p>
<p>이번 회고를 통해 기술적 개념을 정확히 이해하고 올바르게 적용하는 것이 얼마나 중요한지 깨달았습니다. 특히 디자인 패턴과 같은 핵심 개념을 제대로 파악하지 못한 채 사용하면, 팀원들에게 잘못된 정보를 전달할 뿐만 아니라 코드의 진짜 의도도 왜곡될 수 있다는 점을 알게 되었습니다. 앞으로는 기술적 용어와 개념을 더 꼼꼼히 학습하고, 정확한 표현을 사용하는 데 더 신경 쓰려고 합니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://www.patterns.dev/react/presentational-container-pattern/">Container/Presentational Pattern
</a></li>
<li><a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0">Presentational and Container Components
</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(이제는 모두가 알고 있는) 가상 DOM의 오해와 진실]]></title>
            <link>https://velog.io/@camille_protocol/%EA%B0%80%EC%83%81-DOM%EC%9D%98-%EC%98%A4%ED%95%B4%EC%99%80-%EC%A7%84%EC%8B%A4</link>
            <guid>https://velog.io/@camille_protocol/%EA%B0%80%EC%83%81-DOM%EC%9D%98-%EC%98%A4%ED%95%B4%EC%99%80-%EC%A7%84%EC%8B%A4</guid>
            <pubDate>Mon, 16 Jun 2025 14:34:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 이전에 운영하던 블로그에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2025년 3월 16일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서-가상-dom에-대한-흔한-오해">들어가면서: 가상 DOM에 대한 흔한 오해</h2>
<blockquote>
<p>&quot;리액트의 가상 DOM은 실제 DOM보다 빠르다.&quot;</p>
</blockquote>
<p>리액트를 학습하는 과정에서 자주 접하게 되는 주장입니다. 이 문장은 많은 개발자들에게 사실로 받아들여지고 있습니다. 그러나 이는 절반만 맞는 말입니다. 가상 DOM이 특정 상황에서 성능상 이점을 제공하는 것은 사실이지만, 모든 상황에서 항상 빠른 것은 아닙니다.</p>
<h2 id="리액트와-가상-dom">리액트와 가상 DOM</h2>
<h3 id="가상-dom의-등장-배경">가상 DOM의 등장 배경</h3>
<p>먼저 가상 DOM의 등장 배경에 대해서 알아보기 전에, 기존의 웹 페이지 렌더링 과정에 대해서 살펴봅시다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>HTML 파싱</td>
<td>HTML 문서를 파싱하여 DOM 트리를 생성합니다.</td>
</tr>
<tr>
<td>CSS 파싱</td>
<td>CSS를 파싱하여 CSSOM 트리를 생성합니다.</td>
</tr>
<tr>
<td>렌더 트리 구성</td>
<td>DOM 트리와 CSSOM 트리를 결합해 렌더 트리를 생성합니다.</td>
</tr>
<tr>
<td>레이아웃</td>
<td>각 요소의 정확한 위치와 크기를 계산합니다</td>
</tr>
<tr>
<td>페인트</td>
<td>계산된 위치와 스타일에 따라 각 요소를 화면에 픽셀로 그립니다.</td>
</tr>
</tbody></table>
<p>이것이 브라우저가 웹페이지를 렌더링하는 일반적인 과정이며, 레이아웃이 발생하면 페인팅 역시 필연적으로 발생하게 됩니다. 브라우저는 이러한 과정을 거쳐 사용자에게 완성된 웹페이지를 제공해 왔습니다.</p>
<p>만약 특정 요소의 위치와 크기가 변경될 경우, 앞서 언급한 것처럼 <strong>레이아웃-페인팅</strong> 작업이 발생하게 됩니다. 또한 DOM 변경이 일어나는 요소가 많은 자식 요소를 가지고 있다면, 변경이 발생할 때 하위 자식 요소도 같이 렌더링됩니다. 이러한 추가 작업은 SPA가 본격적으로 부상하면서 점차 늘어나기 시작했습니다. <strong>하나의 페이지에서 변경되는 요소를 계속해서 계산</strong>하는 경우가 증가하기 시작한 것입니다.</p>
<p><code>가상 DOM</code>은 이러한 문제를 해결하기 위해서 등장했습니다. 가상 DOM은 실제 브라우저 DOM의 가벼운 사본으로, 메모리상에서 변경 사항을 먼저 계산한 후 최종 결과만 실제 DOM에 반영합니다. 이 접근법은 여러 변경 사항을 일괄적으로 처리하여 불필요한 렌더링을 최소화하는 데 도움을 줍니다.</p>
<h3 id="리액트-파이버-가상-dom을-지탱하는-힘">리액트 파이버: 가상 DOM을 지탱하는 힘</h3>
<p>리액트는 가상 DOM을 <code>리액트 파이버(React fiber)</code>라는 아키텍처-이자 객체-를 통해 관리합니다. 리액트 파이버는 작업 단위로 구성되어 있으며 파이버 재조정자(fiber reconciler)에 의해 관리됩니다.</p>
<p>리액트 파이버의 핵심은 렌더링 작업을 작은 단위로 쪼개고 우선순위를 부여하는 능력입니다. 이를 통해 작업을 임시 중단하고 나중에 재개할 수 있으며, 자바스크립트의 싱글 스레드 환경에서도 비동기적인 렌더링을 가능하게 합니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/5cd35bf7-f2c7-4519-ae4a-b425ee8befa3/image.webp" alt="React Fiber"></p>
<p>작동 방식은 이중 트리 구조를 기반으로 합니다. <code>Current</code> 트리는 현재 화면에 표시된 UI를, <code>WorkInProgress</code> 트리는 변경 작업이 진행 중인 상태를 나타냅니다. 모든 변경 작업이 완료되면 리액트는 단순히 포인터를 변경하여 <code>WorkInProgress</code> 트리를 새로운 <code>Current</code> 트리로 만듭니다.</p>
<p>효율성 측면에서 리액트 파이버는 최초 렌더링 시에만 새 파이버를 생성하고, 이후 업데이트에서는 변경점을 기반으로 가급적 기존 파이버를 재사용합니다. 이러한 구조를 통해 리액트는 복잡한 UI 업데이트를 예측 가능하고 효율적인 방식으로 처리할 수 있습니다.</p>
<h3 id="리액트의-렌더링-프로세스">리액트의 렌더링 프로세스</h3>
<p>리액트 파이버를 통해, 리액트는 최초 렌더링 시 다음과 같은 과정을 진행합니다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1. 초기화</td>
<td>React 엘리먼트 생성</td>
</tr>
<tr>
<td>2. 가상 DOM 생성</td>
<td>컴포넌트 트리에 대한 최초 가상 DOM 구조 생성</td>
</tr>
<tr>
<td>3. 파이버 트리 구축</td>
<td>각 컴포넌트에 대한 파이버 노드 생성 및 트리 구조화</td>
</tr>
<tr>
<td>4. 렌더 단계</td>
<td>파이버 트리를 순회하며 모든 컴포넌트의 렌더 함수 실행</td>
</tr>
<tr>
<td>5. 커밋 단계</td>
<td>파이버 트리의 변경사항을 실제 DOM에 반영</td>
</tr>
<tr>
<td>6. 레이아웃 계산</td>
<td>브라우저가 요소의 크기와 위치를 계산</td>
</tr>
<tr>
<td>7. 페인팅</td>
<td>브라우저가 모든 시각적 요소를 화면에 그림</td>
</tr>
</tbody></table>
<p>이후 업데이트가 발생해 리렌더링이 진행될 경우 렌더 단계에서 재조정 작업을 실행합니다. 이 재조정 단계에서 Diffing 알고리즘을 적용해 변경점을 식별하는데, 이 알고리즘이 바로 오늘 하고자 하는 이야기의 주인공입니다.</p>
<h3 id="diffing-알고리즘">Diffing 알고리즘</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/ccb557ad-8144-490f-923d-fad3ce979a6e/image.webp" alt="Diffing Algorithm"></p>
<p><code>Diffing 알고리즘</code>은 가상 DOM의 이전 버전과 새 버전을 비교해 실제 DOM에서 변경해야 할 최소한의 요소를 식별하는 과정입니다. 리액트는 <strong>다른 타입의 엘리먼트는 다른 트리를 생성한다</strong>는 가정 하에, 업데이트가 발생했을 때 두 루트 엘리먼트부터 시작해 트리를 동시에 순회하며 비교 작업을 진행합니다.</p>
<p>이때 <strong>엘리먼트 타입</strong>을 비교하여 타입이 같으면 속성만 업데이트하고 자식 요소로 재귀적으로 진행하지만, 타입이 다르면 이전 트리를 완전히 해체하고 새 트리를 생성합니다. <strong>DOM 노드</strong>의 경우 같은 타입이면 속성만 업데이트하고 내부 변경을 진행하지만, 다른 타입이면 전체 서브트리를 교체합니다. <strong>컴포넌트</strong>는 같은 타입이면 인스턴스를 유지하며 props만 업데이트하지만, 다른 타입이면 이전 컴포넌트를 언마운트하고 새 컴포넌트를 마운트합니다. 마지막으로 <strong>자식 리스트</strong>는 key가 없으면 순서대로 비교하고, key가 있으면 이를 기준으로 일치항목을 찾아 효율적으로 DOM을 조작합니다.</p>
<p>이를 기반으로 세울 수 있는 리액트의 최적화 케이스는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>결과</th>
<th>효율성</th>
</tr>
</thead>
<tbody><tr>
<td>같은 위치, 같은 타입</td>
<td>속성만 변경</td>
<td>가장 효율적</td>
</tr>
<tr>
<td>같은 타입, 다른 위치, key 있음</td>
<td>실제 DOM 이동으로 해결</td>
<td>효율적</td>
</tr>
<tr>
<td>다른 타입</td>
<td>전체 서브트리 재생성</td>
<td>비효율적</td>
</tr>
</tbody></table>
<h2 id="가상-dom에-대한-말말말">가상 DOM에 대한 말말말</h2>
<p>얼핏 보면 이러한 Diffing 알고리즘이 굉장히 효율적인 것처럼 보입니다. 그러나 최근 몇 년간 가상 DOM의 성능에 대한 의문은 끊임없이 제기되어 왔습니다. 특히 Svelte(스벨트)의 창시자인 리치 해리스가 작성한 <a href="https://svelte.dev/blog/virtual-dom-is-pure-overhead">Virtual DOM is pure overhead</a>라는 도발적인 제목의 글이 이 논쟁에 불을 지폈습니다.</p>
<p>리치 해리스는 그의 글에서 &quot;가상 DOM이 빠르다&quot;는 문구가 일종의 밈이 되었다고 지적하며, 가상 DOM 자체가 <strong>기능이 아닌 수단</strong>이라고 주장했습니다.</p>
<blockquote>
<p>&quot;가상 DOM은 기능이 아닙니다. 그것은 목적을 달성하기 위한 수단이며, 그 목적은 선언적이고 상태 주도적인 UI 개발입니다. 가상 DOM은 상태 전환에 대해 생각하지 않고도 일반적으로 충분히 좋은 성능으로 앱을 구축할 수 있게 해주기 때문에 가치가 있습니다.&quot; <a href="https://svelte.dev/blog/virtual-dom-is-pure-overhead#Why-do-frameworks-use-the-virtual-DOM-then">(원문 번역)</a></p>
</blockquote>
<p>그의 주장에 따르면, Diffing 작업 자체가 불필요한 오버헤드처럼 느껴집니다. 앞서 이야기한 것처럼, 가상 DOM을 사용할 때 실제 DOM 업데이트에 앞서 새로운 가상 DOM과 이전 가상 DOM의 스냅샷을 비교하는 Diffing 작업이 항상 수반됩니다. 그러나 대부분의 업데이트에서는 앱의 기본 구조가 크게 변경되지 않기 때문에, 이 작업이 불필요한 경우가 많다는 것입니다.</p>
<h2 id="가상-dom의-대안-피하거나-개선하거나">가상 DOM의 대안: 피하거나, 개선하거나</h2>
<p>이러한 단점을 가진 Diffing 알고리즘을 사용하는 가상 DOM에 대한 여러가지 시도가 있었습니다. 그 중 두 가지 사례만 소개하고자 합니다.</p>
<h3 id="svelte">Svelte</h3>
<blockquote>
<p>&quot;런타임에서 조정하지 말고, 컴파일 타임에 최적화하라.&quot;</p>
</blockquote>
<p>Svelte는 가상 DOM을 사용하지 않는 대신, <strong>컴파일 타임에 DOM을 업데이트</strong>하는 코드를 생성합니다. 간단한 카운터 예시를 하나 살펴봅시다.</p>
<pre><code class="language-svelte">&lt;script&gt;
    let count = $state(0);

    function increment() {
        count += 1;
    }
&lt;/script&gt;

&lt;button onclick={increment}&gt;
    클릭 수: {count}
&lt;/button&gt;</code></pre>
<p>리액트에서는 버튼을 클릭할 때마다 컴포넌트 함수가 재실행되고, 새로운 가상 DOM 트리가 생성된 후 이전 트리와 비교(diffing)하는 과정을 거칩니다. 반면 스벨트는 컴파일 시점에 count 변수가 변경될 때 어떤 DOM 요소를 어떻게 업데이트해야 하는지 미리 파악하고, 런타임에는 해당 요소만 직접 업데이트합니다.</p>
<h3 id="millionjs">Million.js</h3>
<blockquote>
<p>DOM을 비교하지 말고, 데이터를 비교하기</p>
</blockquote>
<p>Million.js는 기존의 가상 DOM을 개선한 <code>블록 가상 DOM(Block Virtual DOM)</code>이라는 새로운 접근법을 제시합니다. 이 방식은 <strong>정적 분석</strong>과 <strong>더티 체킹</strong>이라는 특징을 가지고 있습니다.</p>
<p><strong>정적 분석</strong>(Static Analysis)은 가상 DOM을 분석하여 동적 부분을 편집 맵(Edit Map)으로 추출합니다. 이는 가상 DOM의 동적 부분과 상태 간의 매핑 목록입니다.
<strong>더티 체킹</strong>(Dirty Checking)은 가상 DOM 트리가 아닌 상태만 비교하여 변경 사항을 결정합니다. 상태가 변경되면 편집 맵을 통해 DOM을 직접 업데이트합니다.</p>
<pre><code class="language-jsx">import { useState } from &#39;react&#39;;
import { block } from &#39;million/react&#39;;

function Count() {
    const [count, setCount] = useState(0);

    const node1 = count + 1;
    const node2 = count + 2;

    return (
        &lt;div&gt;
            &lt;ul&gt;
                &lt;li&gt;{node1}&lt;/li&gt;
                &lt;li&gt;{node2}&lt;/li&gt;
            &lt;/ul&gt;
            &lt;button
                onClick={() =&gt; {
                    setCount(count + 1);
                }}
            &gt;
                Increment Count
            &lt;/button&gt;
        &lt;/div&gt;
    );
}
const CountBlock = block(Count);</code></pre>
<p>위 예시에서 Million.js는 정적 분석 단계에서 동적 부분(node1과 node2)을 추출하고, 상태(count)가 변경될 때 이 부분만 업데이트합니다.</p>
<h2 id="그렇다면-가상-dom을-기피해야-하는가">그렇다면 가상 DOM을 기피해야 하는가?</h2>
<p>그렇다면 가상 DOM은 기피해야 할 대상일까요? 그렇지는 않습니다. <a href="https://old.million.dev/blog/virtual-dom#lots-of-static-content-with-little-dynamic-content">Million.js에 대한 글</a>에서 언급한 것처럼, <strong>동적 콘텐츠가 많은 경우</strong>에는 여전히 전통적인 가상 DOM 방식이 효율적입니다.</p>
<p>이론적인 논의를 넘어, 실제 성능 비교 결과에서도 가상 DOM을 기피하지는 않아야 한다는 것을 확인할 수 있습니다.</p>
<p>2020년에 발표된 논문 <a href="https://www.doria.fi/handle/10024/177433">&quot;DOM benchmark comparison of the front-end JavaScript frameworks React, Angular, Vue, and Svelte&quot;</a>에서는 다양한 자바스크립트 프레임워크에서의 DOM 조작 성능 벤치마크 지표를 확인할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>실험 유형</th>
<th>React v16.12.0</th>
<th>Vue v2.6.11</th>
<th>Angular v8.2.14</th>
<th>Svelte v3.20.0</th>
</tr>
</thead>
<tbody><tr>
<td>DOM 요소 10,000개 추가 (ms)</td>
<td>30.96</td>
<td><strong>25.36</strong></td>
<td>52.75</td>
<td>31.26</td>
</tr>
<tr>
<td>단일 요소 수정 (ms)</td>
<td>16.58</td>
<td>22.23</td>
<td>6.08</td>
<td><strong>0.11</strong></td>
</tr>
<tr>
<td>10,000개 요소 수정 (ms)</td>
<td><strong>17.86</strong></td>
<td>20.64</td>
<td>896.76</td>
<td>885.03</td>
</tr>
<tr>
<td>단일 요소 제거 (ms)</td>
<td>16.54</td>
<td>24.51</td>
<td><strong>0.09</strong></td>
<td>0.53</td>
</tr>
<tr>
<td>10,000개 요소 제거 (ms)</td>
<td><strong>7.39</strong></td>
<td>33.33</td>
<td>23.83</td>
<td>22.97</td>
</tr>
<tr>
<td>컴파일 속도 (s)</td>
<td>3.96</td>
<td>3.07</td>
<td>8.70</td>
<td><strong>1.61</strong></td>
</tr>
</tbody></table>
<p>벤치마크 결과에서 확인할 수 있듯, <strong>단일 요소 조작</strong>은 직접 DOM을 조작하는 <strong>스벨트</strong>가 압도적으로 빠른 반면, <strong>대량 요소를 조작</strong>하는 경우에는 가상 DOM을 사용하는 <strong>리액트</strong>가 압도적인 성능을 보입니다. 가상 DOM이 모든 상황에서 빠른 것은 아니지만, 대규모 DOM 업데이트가 필요한 복잡한 애플리케이션에서는 여전히 매우 효율적임을 알 수 있습니다.</p>
<h2 id="나오면서">나오면서</h2>
<blockquote>
<p>&quot;리액트는 가상 DOM이 대부분의 상황에서 웬만한 애플리케이션을 개발할 수 있을 만큼 합리적으로 빠르기 때문에 이를 채용했다.&quot;</p>
</blockquote>
<p>가상 DOM은 단순히 성능 최적화를 위한 도구가 아닌, 리액트의 근간을 이루는 핵심 개념입니다. 리액트가 가상 DOM을 도입한 주된 목적은 개발자들이 선언적이고 상태 중심적인 UI 개발 방식을 채택할 수 있도록 하는 데 있습니다. 이는 복잡한 상태 전환과 DOM 조작 로직을 직접 관리하지 않고도 예측 가능하고 일관된 방식으로 애플리케이션을 구축할 수 있게 해줍니다.</p>
<p>앞선 벤치마크 결과에서 확인했듯이, 가상 DOM은 모든 상황에서 최고의 성능을 보장하지는 않습니다. 단일 요소의 조작에서는 Svelte와 같이 컴파일 타임 최적화를 사용하는 접근법이 더 효율적일 수 있습니다. 그러나 대규모 DOM 업데이트나 복잡한 UI 구조에서는 가상 DOM의 일괄 처리 방식이 여전히 강력한 이점을 제공합니다.</p>
<p>따라서 &quot;가상 DOM이 항상 실제 DOM보다 빠르다&quot;는 주장은 정확하지 않지만, 동시에 가상 DOM을 무조건 기피해야 한다는 주장 역시 지나친 단순화입니다. 리액트의 창시자들이 언급했듯이, 가상 DOM은 대부분의 웹 애플리케이션에서 충분히 빠른 성능을 제공하면서도 개발자 경험을 크게 향상시키는 균형 잡힌 접근법입니다.</p>
<p>오늘날 웹 개발의 다양한 요구사항을 고려할 때, 프로젝트의 특성과 요구사항에 맞는 기술을 선택하는 것이 중요합니다. 작은 규모의 인터랙티브 컴포넌트에는 스벨트와 같은 컴파일 타임 최적화 방식이 유리할 수 있으며, 대규모 엔터프라이즈 애플리케이션에서는 리액트와 같은 가상 DOM 기반 프레임워크가 더 적합할 수 있습니다. Million.js와 같은 하이브리드 접근법도 특정 상황에서 좋은 대안이 될 수 있습니다.</p>
<p>결론적으로, 가상 DOM에 대한 미신에서 벗어나 각 기술의 장단점을 정확히 이해하고, 상황에 맞게 적절한 도구를 선택하는 것이 현명한 개발자의 자세일 것입니다. 웹과 그 기반 기술은 계속해서 진화하고 있으며, 우리는 이러한 발전을 열린 마음으로 받아들이고 배워나가야 합니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://wikibook.co.kr/react-deep-dive/">모던 리액트 Deep Dive</a></li>
<li><a href="https://yozm.wishket.com/magazine/detail/2493/">리액트 배칭(Batching)의 모든 것</a></li>
<li><a href="https://svelte.dev/blog/virtual-dom-is-pure-overhead">Virtual DOM is pure overhead</a></li>
<li><a href="https://old.million.dev/blog/virtual-dom">Virtual DOM: Back in Block</a></li>
<li><a href="https://www.velotio.com/engineering-blog/react-fiber-algorithm">An Introduction to React Fiber - The Algorithm Behind React</a></li>
<li><a href="https://www.doria.fi/handle/10024/177433">DOM benchmark comparison of the front-end JavaScript frameworks React, Angular, Vue, and Svelte (2020)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[한입 타입스크립트 챌린지 회고]]></title>
            <link>https://velog.io/@camille_protocol/%ED%95%9C%EC%9E%85-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@camille_protocol/%ED%95%9C%EC%9E%85-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%B1%8C%EB%A6%B0%EC%A7%80-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 16 Jun 2025 14:24:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 이전에 운영하던 블로그에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2025년 3월 7일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서">들어가면서</h2>
<p>타입스크립트는 최근 프론트엔드 개발 생태계에서 필수적인 기술이 되었습니다. 자바스크립트에 정적 타입 시스템을 더해 코드의 안정성을 높이고, 개발 경험을 개선해주는 타입스크립트를 제대로 배워보고자 이정환님의 <a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8/dashboard">&#39;한 입 크기로 잘라먹는 타입스크립트&#39;</a> 챌린지에 참여하게 되었습니다.</p>
<p>개발자로서 성장하기 위해서는 새로운 기술을 꾸준히 학습하는 것이 중요하다고 생각합니다. 특히 타입스크립트는 자바스크립트 프로젝트에 점진적으로 도입할 수 있고, 코드 품질을 향상시키는 데 도움이 되기 때문에 이번 기회를 통해 다시 한 번 정리해보고 싶었습니다.</p>
<h2 id="챌린지-진행-방식">챌린지 진행 방식</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/4ef0cc6b-5408-40ec-ba8d-a3bf19ece6a3/image.webp" alt="Daily Mission"></p>
<p>챌린지는 2주간 매일 강의를 수강하며 미션을 완료하는 식으로 진행됐습니다. 각 날짜별로 정해진 강의를 듣고, 학습한 내용을 바탕으로 그날 배운 내용을 적용해볼 수 있는 미션을 수행하는 방식이었습니다.</p>
<h2 id="좋았던-점">좋았던 점</h2>
<h3 id="매일-조금씩-꾸준히-할-수-있는-분량">매일 조금씩 꾸준히 할 수 있는 분량</h3>
<p>챌린지의 가장 큰 장점은 적절한 학습 분량이었습니다. 하루에 1시간 정도 투자하면 강의를 듣고 미션까지 완료할 수 있어서 부담이 없었습니다. 학습을 꾸준히 이어갈 수 있어 타입스크립트에 대한 이해도를 점진적으로 높일 수 있었습니다.</p>
<h3 id="매일-학습내용에-기반한-미션">매일 학습내용에 기반한 미션</h3>
<p>그날 배운 내용을 바로 실습해볼 수 있는 미션이 제공되어 학습 효과가 높았습니다. 이론으로만 끝나는 것이 아니라 직접 코드를 작성해보면서 개념을 확실히 이해할 수 있었고, 실수를 통해 더 많은 것을 배울 수 있었습니다.</p>
<h3 id="언어를-막-학습하는-개발자를-위한-강의">언어를 막 학습하는 개발자를 위한 강의</h3>
<p>타입스크립트를 처음 접하는 개발자를 위해 쉽고 명확하게 설명해주는 강의였습니다. 복잡한 개념도 이해하기 쉽게 풀어서 설명해주었고, 실제 개발 현장에서 많이 쓰이는 패턴과 사례를 중심으로 학습할 수 있어 매우 유익했습니다.</p>
<h2 id="아쉬웠던-점">아쉬웠던 점</h2>
<h3 id="미션의-난이도와-범위">미션의 난이도와 범위</h3>
<p>미션으로 주어지는 문제가 때로는 너무 쉽거나, 그날 강의에서 배운 모든 내용을 다루고 있지 않은 경우가 있었습니다. 조금 더 도전적인 미션이 있었다면 학습 내용을 더 깊이 이해하고 응용하는 데 도움이 되었을 것 같습니다.</p>
<h3 id="마지막-미션의-구성">마지막 미션의 구성</h3>
<p>마지막 미션의 경우 강의 내용이 아닌 후기 작성이 과제로 주어졌습니다. 물론 그날 배운 내용은 이전 시간에 이미 다루긴 했지만, 마지막까지 타입스크립트 실습을 통해 배운 내용을 종합해보는 기회가 있었으면 더 좋았을 것 같습니다.</p>
<h3 id="내가-아쉬웠던-점---학습-기록의-부재">내가 아쉬웠던 점 - 학습 기록의 부재</h3>
<p>개인적으로는 학습하면서 바로바로 기록을 남기지 않은 점이 아쉽습니다. 미리 공부한 내용을 정리하고 블로그에 포스팅했다면, 나중에 참고할 수 있는 자료가 되었을 텐데 그러지 못한 점이 아쉽습니다. 다음 챌린지에서는 매일 학습한 내용을 간단하게라도 기록하는 습관을 들여야겠다고 생각했습니다.</p>
<h2 id="나오면서">나오면서</h2>
<h3 id="한입-챌린지를-마치고-얻은-지식">한입 챌린지를 마치고 얻은 지식</h3>
<p>이번 챌린지를 통해 타입스크립트의 기본 문법부터 인터페이스, 제네릭, 타입 가드 등 핵심 개념까지 체계적으로 배울 수 있었습니다. 특히 타입 추론 메커니즘과 유틸리티 타입을 활용하는 방법을 이해하게 된 것이 가장 큰 수확이었습니다.</p>
<h3 id="앞으로의-학습-계획">앞으로의 학습 계획</h3>
<p>이번 챌린지는 타입스크립트의 기초를 다지는 좋은 기회였습니다. 앞으로는 공식 문서나 책을 통해 타입스크립트의 심화 학습을 진행할 예정입니다. 특히 고급 타입 시스템과 타입스크립트 컴파일러에 대한 이해를 높이고, 리액트와 타입스크립트를 함께 사용하는 방법에 대해 더 깊이 공부하려고 합니다. 또한 실제 프로젝트에 타입스크립트를 적극적으로 도입하여 이론으로 배운 내용을 실전에서 적용해볼 계획입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[hoonie-blog: 블로그는 제발 사드세요]]></title>
            <link>https://velog.io/@camille_protocol/hoonie-blog-%EB%B8%94%EB%A1%9C%EA%B7%B8%EB%8A%94-%EC%A0%9C%EB%B0%9C-%EC%82%AC%EB%93%9C%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@camille_protocol/hoonie-blog-%EB%B8%94%EB%A1%9C%EA%B7%B8%EB%8A%94-%EC%A0%9C%EB%B0%9C-%EC%82%AC%EB%93%9C%EC%84%B8%EC%9A%94</guid>
            <pubDate>Mon, 16 Jun 2025 14:17:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 이전에 운영하던 블로그에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2025년 2월 15일에 작성됐습니다.</p>
</blockquote>
<h2 id="블로그-유목민의-자기소개">블로그 유목민의 자기소개</h2>
<p>처음에는 벨로그, 그다음에는 노션 기반의 개인 블로그 템플릿, 그리고 이후에는 직접 만든 Supabase 기반 블로그까지—저는 블로그 플랫폼을 끊임없이 옮겨 다니는 유목민이었습니다.</p>
<p>기존의 서비스를 사용하는 대신 직접 만든 블로그를 운영한다는 점이 언제나 매력적으로 다가왔고, 그 결과 이곳저곳을 기웃거리며 더 나은 블로그 서비스를 찾아 헤맸습니다. 결국 Supabase를 활용해 블로그를 직접 만들었지만, 시간이 지나면서 점점 방치하게 되었습니다. 사용 경험이 만족스럽지 않았고, 자연스럽게 글을 쓰고자 하는 동기도 줄어들었기 때문입니다.</p>
<p>BaaS와 다양한 라이브러리를 활용해 직접 구축한 점은 흥미로웠지만, 지나치게 많은 의존성 덕분에 지속적인 관리가 어렵다는 문제가 있었습니다. 만약 사용한 라이브러리 중 하나라도 지원이 중단된다면, 블로그를 유지보수하는 데 또 많은 시간을 들여야 했습니다. 결국 오랜만에 블로그를 다시 살펴보다가 화면 가득한 의존성 업데이트 목록을 보고는 결론을 내렸습니다.</p>
<blockquote>
<p>새로운 블로그를 만드는 게 더 빠르겠다.</p>
</blockquote>
<h2 id="기술-스택과-폴더-구조">기술 스택과 폴더 구조</h2>
<p>블로그를 구현하기 전에, 이전과 같은 실수를 반복하지 않기 위해서 두 가지의 대원칙을 세웠습니다:</p>
<ol>
<li><strong>불필요한 의존성</strong>을 줄일 것</li>
<li>글쓰기 경험을 해치지 않는 <strong>미니멀한 블로그</strong>를 만들 것</li>
</ol>
<p>이 두 가지 원칙을 바탕으로 Next.js와 MDX를 활용한 블로그 구현을 하기로 결정했습니다.</p>
<h3 id="nextjs를-선택한-이유">Next.js를 선택한 이유</h3>
<p>블로그 개발에 너무 많은 시간을 들이기보다는, 정해진 방식대로 개발할 수 있는 프레임워크를 선택하는 것이 더 효율적이라고 판단했습니다. 다행히 <a href="https://nextjs.org/docs/pages/building-your-application/configuring/mdx">공식문서</a>에서 확인할 수 있듯이, Next.js는 <strong>자체적으로 MDX를 지원</strong>하고 있습니다.</p>
<ul>
<li>정적 사이트 생성(SSG)과 서버 사이드 렌더링(SSR)지원</li>
<li>폴더 기반 라우팅</li>
</ul>
<h3 id="mdx를-선택한-이유">MDX를 선택한 이유</h3>
<p>블로그의 핵심은 결국 글쓰기 경험이라고 생각합니다. MDX를 사용하면 마크다운의 간결함과 JSX의 확장성을 동시에 활용할 수 있어서, 단순한 글 작성 뿐만 아니라 컴포넌트와 같은 요소도 쉽게 추가할 수 있습니다.</p>
<ul>
<li>Markdown의 간결함</li>
<li>JSX와의 결합</li>
<li>불필요한 데이터베이스 제거</li>
</ul>
<p>잠시 뒤에 설명하겠지만, MDX를 사용하면 아래와 같이 파일 내부에 컴포넌트를 쉽게 삽입할 수 있습니다. 또한 Supabase와 같은 데이터베이스를 따로 사용하지 않아도 되었기에, 블로그의 유지보수가 훨씬 더 간편해졌습니다.</p>
<pre><code class="language-mdx">export const metadata = {
    title: &#39;Next.js로 블로그 만들기&#39;,
    description:
        &#39;Next.js와 MDX를 사용하여 개인 블로그를 만드는 과정을 공유합니다.&#39;,
    date: &#39;2025-02-07&#39;,
};

&lt;PostTitle {...metadata} /&gt;

## 그리고 이렇게 마크다운 문법을 그대로 사용할 수 있습니다!</code></pre>
<p>결론적으로, Next.js와 MDX의 조합을 통해 최소한의 설정으로도 깔끔한 블로그를 만들 수 있기에 이러한 조합을 선택했습니다.</p>
<h3 id="폴더-구조">폴더 구조</h3>
<pre><code class="language-markdown">hoonie-blog
├─ public/
├─ src/
│ ├─ app/
│ │ ├─ blog/
│ │ ├─ layout.tsx
│ │ ├─ sitemap.ts
│ │ ├─ robots.ts
│ ├─ components/
│ │ ├─ post/
│ │ └─ ui/
│ ├─ fonts/
│ ├─ lib/
│ ├─ types/
└─ 기타 프로젝트 설정 파일 (Prettier, ESLint, Next.js 등)</code></pre>
<p>구조적으로 크게 복잡한 부분은 없으며, 블로그 글은 <code>/app/blog/</code> 내부에서 MDX 파일로 관리하는 방식을 선택했습니다.</p>
<h2 id="톺아보는-hoonie-blog">톺아보는 hoonie-blog</h2>
<h3 id="mdx의-스타일과-컴포넌트-적용">MDX의 스타일과 컴포넌트 적용</h3>
<p>Next.js는 <code>@next/mdx</code>패키지를 사용하여 MDX를 처리합니다. 이 과정에서 <strong>remark</strong>와 <strong>rehype</strong>라는 두 가지 주요 라이브러리를 활용하는데, 이 둘은 <a href="https://www.daleseo.com/unified-remark-rehype/">Markdown을 HTML로 변환</a>하는 데 핵심적인 역할을 합니다. Next.js에서는 <code>next.config.mjs</code>에서 remark/rehype 플러그인을 설정할 수 있으며, 저는 GitHub Markdown을 지원하는 <a href="https://github.com/remarkjs/remark-gfm">remark-gfm</a>을 사용했습니다.</p>
<p>또한 Next.js에서 MDX 렌더링을 커스터마이징하기 위해서, <code>@next/mdx</code>는 mdx-components라는 특수한 컴포넌트를 사용합니다. 기본적인 wrapper와 함께, <a href="https://www.npmjs.com/package/sugar-high">sugar-high</a> 라이브러리를 사용해 코드 블럭에 문법 강조를 적용했습니다. 이제 MDX에서 작성한 콘텐츠는 자동으로 스타일이 적용된 HTML로 렌더링됩니다.</p>
<pre><code class="language-tsx">// ...import

export function useMDXComponents(components: MDXComponents): MDXComponents {
    return {
        wrapper: ({ children }) =&gt; (
            &lt;section className=&#39;prose prose-sm md:prose-base dark:prose-invert max-w-none break-keep&#39;&gt;
                {children}
            &lt;/section&gt;
        ),
        code: ({ children, className, ...props }) =&gt; {
            if (typeof children === &#39;string&#39; &amp;&amp; !className) {
                return &lt;code {...props}&gt;{children}&lt;/code&gt;;
            }
            const codeHTML = highlight(children as string);
            return &lt;code dangerouslySetInnerHTML={{ __html: codeHTML }} {...props} /&gt;;
        },
        img: (props) =&gt; {
            return (
                &lt;Image
                    src={`/images/posts/${props.src}`}
                    alt={props.alt || &#39;&#39;}
                    width={600}
                    height={600}
                    quality={75}
                    loading=&#39;lazy&#39;
                    style={{ width: &#39;100%&#39;, height: &#39;auto&#39; }}
                /&gt;
            );
        },
        a: ({ href = &#39;&#39;, ...props }) =&gt; (
            &lt;Link href={href} target=&#39;blank&#39; {...props} /&gt;
        ),
        PostTitle,
        ...components,
    };
}</code></pre>
<h3 id="게시글-불러오기---mdx-파일에서-메타데이터-추출하기">게시글 불러오기 - MDX 파일에서 메타데이터 추출하기</h3>
<p>블로그 게시글을 불러올 때에는 Node의 fs(파일 시스템) 모듈을 사용했습니다.
이 방법을 사용하면 각 MDX 파일의 메타데이터(제목, 날짜 등)를 쉽게 추출할 수 있으며, 블로그 글 목록 페이지에 사용할 데이터를 간편하게 추출할 수 있습니다.</p>
<pre><code class="language-typescript">export async function getAllPosts(): Promise&lt;Post[]&gt; {
    const dirs = fs
        .readdirSync(POSTS_DIR, { withFileTypes: true })
        .filter((dirent) =&gt; dirent.isDirectory())
        .map((dirent) =&gt; dirent.name);

    const posts = await Promise.all(
        dirs.map(async (dir) =&gt; {
            const { metadata } = await import(`@/app/blog/(posts)/${dir}/page.mdx`);
            return {
                ...metadata,
                slug: dir,
                date: new Date(metadata.date).toISOString().split(&#39;T&#39;)[0],
            };
        }),
    );

    return posts.sort(
        (a, b) =&gt; new Date(b.date).getTime() - new Date(a.date).getTime(),
    );
}</code></pre>
<p>처음에는 MDX 기반으로 블로그를 구현하는 것이 매우 간단해 보였습니다.
단순히 MDX 파일을 렌더링하고, 메타데이터를 추출하여 정렬하면 끝이었으니까요. 하지만 문제는 예상하지 못한 곳에서 발생했습니다.</p>
<h2 id="좌충우돌-트러블슈팅-다크모드-구현하기">좌충우돌 트러블슈팅: 다크모드 구현하기</h2>
<p>대부분의 웹사이트에서 다크모드를 지원하는 요즘, 제 블로그에도 그러한 기능을 빼 놓을 수는 없었습니다. 해당 기능을 지원하는 브라우저도 많고, 사용자의 선호도도 점차 증가하는 추세이기 때문입니다. 하지만 해당 기능을 구현하면서 예상하지 못한 문제가 발생했습니다. 바로 <strong>FOUC(Flash of Unstyled Content)</strong> 문제였습니다.</p>
<h3 id="다크모드에서-발생한-fouc-문제">다크모드에서 발생한 FOUC 문제</h3>
<p>최초로 시도한 다크모드 구현 방식은 <strong>로컬 스토리지와 <code>useEffect</code>를 활용</strong>하여,
브라우저에 저장된 다크모드 설정을 불러오는 것이었습니다.</p>
<p>그러나 Next.js에서는 초기 HTML을 <strong>서버에서 렌더링(SSR)</strong> 하고,
이를 클라이언트가 전달받아 <strong>하이드레이션</strong> 을 수행하는 구조이기 때문에,
<strong>웹 페이지가 로드될 때 스타일이 적용되기 전의 기본 스타일이 잠시 보이는 FOUC 현상이 발생</strong>할 수밖에 없었습니다. 인라인 스크립트를 사용해 문제를 해결할 수도 있었지만, 보안 문제나 가독성 저하 등의 한계가 명확했습니다.</p>
<h3 id="쿠키-한-입에-문제-해결">쿠키 한 입에 문제 해결!</h3>
<p>해당 문제에 대해 지인에게 의견을 구했고, 로컬 스토리지를 사용하는 대신 <a href="https://nextjs.org/docs/app/api-reference/functions/cookies">쿠키를 활용해 보는 것</a>이 어떻겠냐는 조언을 얻었습니다. 쿠키를 활용하면 서버에서 초기 상태를 결정할 수 있어서, 하이드레이션을 거치기 전에 테마를 적용할 수 있기 때문입니다. 즉, <strong>SSR 과정에서 쿠키를 읽어, HTML이 브라우저에 도착하기 전에 다크모드가 적용된 상태로 렌더링할 수 있다는 것</strong>이었습니다.</p>
<pre><code class="language-tsx">export default async function RootLayout({
    children,
}: Readonly&lt;{ children: React.ReactNode }&gt;) {
    // 쿠키 값을 기반으로 테마 결정하기
    const cookieStore = await cookies();
    const theme = cookieStore.get(&#39;theme&#39;);
    const initialTheme = theme?.value === &#39;dark&#39; ? &#39;dark&#39; : &#39;light&#39;;

    // ...
}</code></pre>
<p>이렇게 쿠키 값을 기반으로 렌더링 시 다크모드가 적용된 HTML을 가져오기 때문에, 브라우저가 페이지를 로드할 때 FOUC 문제가 더 이상 발생하지 않았습니다. 또한 사용자가 버튼을 통해 테마를 토글할 경우에도 쿠키를 기반으로 적용되도록 했습니다.</p>
<pre><code class="language-typescript">const toggleTheme = () =&gt; {
    const newTheme = theme === &#39;light&#39; ? &#39;dark&#39; : &#39;light&#39;;
    setTheme(newTheme);
    document.cookie = `theme=${newTheme}; path=/`;
    document.documentElement.classList.toggle(&#39;dark&#39;);
};</code></pre>
<h2 id="이제-seo를-고려할-차례">이제 SEO를 고려할 차례</h2>
<p>블로그를 운영하면서 검색 유입을 늘리는 것은 중요한 목표 중 하나였습니다. 그래서 Next.js 블로그에 SEO 최적화를 적용하는 방법을 고민했고, 다양한 전략을 시도해 보았습니다.</p>
<h3 id="sitemap사이트맵-생성하기">Sitemap(사이트맵) 생성하기</h3>
<p>검색 엔진이 사이트의 구조를 쉽게 파악할 수 있도록 사이트맵을 추가했습니다. 이를 위해 sitemap.ts 파일을 생성하여 자동으로 Sitemap을 동적으로 생성하도록 설정했습니다. 이전에 만든 <code>getAllPosts()</code> 함수를 활용해, 블로그 게시글 목록을 가져와 사이트맵에 포함시켰습니다.</p>
<p>제 블로그의 게시글은 next/mdx 기반으로 작성되며, 새로운 게시글이 추가될 때마다 다시 빌드하고 배포하는 방식으로 운영됩니다. 따라서 동적 API 방식 대신, 빌드 시 자동으로 Sitemap을 생성하는 방식을 선택했습니다. 이 방식 덕분에 추가적인 API 호출 없이도 SEO 최적화가 가능해졌습니다.</p>
<pre><code class="language-ts">export default async function sitemap(): Promise&lt;MetadataRoute.Sitemap&gt; {
    const posts = await getAllPosts();

    const blogPosts = posts.map((post) =&gt; ({
        url: `https://hoonie-blog.vercel.app/blog/${post.slug}`,
        lastModified: post.date,
        changeFrequency: &#39;weekly&#39; as const,
        priority: 0.6,
    }));

    return [
        {
            url: &#39;https://hoonie-blog.vercel.app&#39;,
            lastModified: new Date(),
            // 검색 엔진이 페이지 변경 주기를 파악할 수 있도록 changeFrequency 설정
            changeFrequency: &#39;daily&#39; as const,
            priority: 1.0,
        },
        {
            url: &#39;https://hoonie-blog.vercel.app/blog&#39;,
            lastModified: new Date(),
            changeFrequency: &#39;weekly&#39; as const,
            // 검색 엔진이 페이지의 상대적 중요도를 판단할 수 있도록 priority 설정
            priority: 0.8,
        },
        ...blogPosts,
    ];
}</code></pre>
<h3 id="robotstxt-생성하기">robots.txt 생성하기</h3>
<p><strong>robots.txt</strong>는 검색 엔진 크롤러(예: Googlebot, Bingbot 등)에게 웹사이트 접근 가능 여부를 안내하는 역할을 하는 파일입니다. Next.js에서는 robots.txt를 동적으로 생성할 수 있도록 robots.ts 파일을 설정할 수 있습니다.</p>
<pre><code class="language-ts">import { BASE_URL } from &#39;./sitemap&#39;;
import type { MetadataRoute } from &#39;next&#39;;

export default function robots(): MetadataRoute.Robots {
    return {
        rules: {
            userAgent: &#39;*&#39;,
            allow: &#39;/&#39;,
        },
        sitemap: `${BASE_URL}/sitemap.xml`,
    };
}</code></pre>
<p>제 블로그에는 따로 어드민 페이지가 존재하지 않기 때문에, 굳이 비공개 및 관리 페이지 차단을 위한 <code>disallow</code> 속성을 사용하지는 않았습니다. 또한 <code>sitemap</code> 속성을 추가해 자동으로 생성된 sitemap을 robots.ts 파일에 명시해 검색 엔진이 이를 쉽게 찾도록 유도했습니다.</p>
<h3 id="vercel-web-analytics와-speed-insights">Vercel Web Analytics와 Speed Insights</h3>
<p>추가적으로 블로그의 방문자 데이터와 속도 측정을 위해서, Vercel에서 제공하는 Web Analytics와 Speed Insights를 추가했습니다.</p>
<p>Vercel Web Analytics는 웹사이트 방문자 데이터를 실시간으로 분석하는 도구로, 이를 통해 어떤 경로로 방문자가 유입되었는지, 어떤 페이지가 인기 있는지, 트래픽이 어떤 패턴을 보이는지 등을 파악할 수 있습니다. Speed Insights는 실제 사용자 데이터를 기반으로 사이트 성능을 분석하는 도구입니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/c5da2373-67f4-4c39-9cd2-c9fa10087559/image.webp" alt="Vecel Analytics"></p>
<h2 id="블로그는-제발-사-드세요-하지만">블로그는 제발 사 드세요, 하지만…</h2>
<p>Next.js로 블로그를 만들면서 느낀 점을 한마디로 정리하자면, 제목처럼 “블로그는 제발 사 드세요.”였습니다. 처음에는 단순히 MDX를 변환해 페이지에 렌더링하기만 하면 되는 작업이라고 생각했습니다. 하지만 SEO 최적화, 다크모드 FOUC 문제 해결, 사이트맵 설정, 성능 개선 등 다양한 요소들을 고려하며 시행착오를 겪어야 했습니다. 아직 미흡한 부분도 여전히 많다고 느껴집니다.</p>
<p>그럼에도 불구하고, 직접 블로그를 만드는 것은 가치 있는 경험이었습니다.</p>
<h3 id="나만의-블로그를-만든다는-것의-의미">나만의 블로그를 만든다는 것의 의미</h3>
<p>이미 수많은 블로그 플랫폼이 존재하고, 기본적으로 제공되는 기능들만 활용해도 충분히 좋은 블로그를 운영할 수 있습니다. 하지만 직접 만든 블로그에는 ‘나만의 색깔’을 담을 수 있습니다.</p>
<ul>
<li>내가 원하는 방식으로 디자인을 커스터마이징할 수 있고,</li>
<li>SEO, 페이지 속도, 데이터 구조 등을 내가 직접 최적화할 수 있으며,</li>
<li>새로운 기능을 추가하면서 개발자로서 성장할 수도 있습니다.</li>
</ul>
<p>단순히 글을 작성하는 공간이 아니라, 내가 원하는 방식으로 발전시킬 수 있는 무한한 가능성을 가진 플랫폼이 된다는 뜻이기도 합니다.</p>
<h3 id="앞으로의-개선-계획">앞으로의 개선 계획</h3>
<p>지금의 블로그는 기본적인 기능만 갖춘 상태여서, 앞으로 더 많은 기능을 추가하면서 지속적으로 발전시켜 나아갈 예정입니다. 현재 우선순위에 둔 기능은 다음과 같습니다.</p>
<ul>
<li>게시글 목록 페이지네이션 추가</li>
<li>태그 및 검색 기능 도입</li>
</ul>
<p>이외에도 다양한 피드백을 기반으로 조금씩 블로그를 개선해 나아 갈 계획입니다.</p>
<p>다른 모든 서비스나 프로젝트가 그렇겠지만, 특히 블로그는 한 번 만들고 끝나는 것이 아니라, 계속해서 개선해 나아가는 프로젝트라고 생각합니다. 앞으로도 꾸준히 블로그를 발전시키면서, 더 좋은 글과 더 나은 사용자 경험을 제공할 수 있도록 노력하고자 합니다.</p>
<blockquote>
<p>블로그는 사 드시는 게 편합니다. 하지만, 그럼에도 불구하고 직접 만드는 경험은 가치가 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[MOTY: 올해의 영화 공유 토이 프로젝트]]></title>
            <link>https://velog.io/@camille_protocol/MOTY-%EC%98%AC%ED%95%B4%EC%9D%98-%EC%98%81%ED%99%94-%EA%B3%B5%EC%9C%A0-%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@camille_protocol/MOTY-%EC%98%AC%ED%95%B4%EC%9D%98-%EC%98%81%ED%99%94-%EA%B3%B5%EC%9C%A0-%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Mon, 16 Jun 2025 14:10:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 이전에 운영하던 블로그에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2025년 1월 20일에 작성됐습니다.</p>
</blockquote>
<h2 id="tldr">TL;DR</h2>
<blockquote>
<p>MOTY는 연말을 맞아 &#39;올해의 영화&#39;를 친구들과 공유할 수 있는 웹 서비스입니다. 단순한 기능 구현을 넘어 웹 접근성, UX등을 고려하며 &quot;누구나 쉽게 이용할 수 있는 서비스&quot;를 만들고자 했습니다.</p>
</blockquote>
<h2 id="어쩌다-시작한-토이-프로젝트">어쩌다 시작한 토이 프로젝트</h2>
<p>크리스마스가 지나고 친구와 함께 이야기를 나누던 중 친구가 진행 중인 토이 프로젝트에 대해서 알게 되었습니다. 올해의 음악을 골라 공유하는 서비스였어요. 친구 이야기를 듣다가 저도 연말을 장식할 수 있는 토이 프로젝트를 하면 어떨까라는 생각이 들었고, 고민하다가 올해의 영화를 골라 공유할 수 있는 서비스를 만들기로 결심했어요.</p>
<h2 id="할-건-많은데-시간은-없네">할 건 많은데, 시간은 없네</h2>
<p>제가 구현하고자 하는 토이 프로젝트의 주요 기능은 대략 다음과 같았습니다:</p>
<ul>
<li>닉네임 입력하기</li>
<li>영화 선택하기</li>
<li>한줄 평 남기기</li>
<li>결과 페이지 친구들에게 공유하기(카카오톡, 링크 복사, 이미지로 저장)</li>
</ul>
<p>문제는 새해까지 시간이 얼마 남지 않았기에, 기획부터 첫 버전의 배포까지 해커톤처럼 빠르게 진행해야 했어요. 밤을 샐 각오를 하고, 노트에 초안을 끄적인 다음, 피그마를 통해 간단한 프로토타이핑을 완성했습니다.</p>
<p>최대한 빠른 시간 안에 개발을 진행하고 배포해야 했기 때문에, 불필요한 기술 스택은 배제 했습니다. 특히 기술 블로그를 개발한 뒤 의존성 문제로 인해 유지보수에 어려움을 겪었던 경험이 있었기에, 사용하지 않는 의존성은 아예 거들떠보지도 않았어요.</p>
<p>라이브러리는 React와 Tailwind CSS, 개발 도구와 언어로는 Vite와 TypeScript를, 코드 품질 도구로는 ESLint와 Prettier를 사용하기로 했습니다. 특히 Vite의 템플릿에 제공되는 ESLint의 경우 9버전이기 때문에 Prettier와의 포맷팅 룰 충돌이 일어나지 않는다는 점에서 추가적인 플러그인을 설치할 필요가 없어져서 초기 환경 설정이 한결 편했습니다.</p>
<p>API는 <a href="https://developer.themoviedb.org/reference/intro/getting-started">TMDB에서 제공하는 Open API</a>를 사용했습니다. 기본적인 검색 기능도 제공하고 있어서, 관련된 로직에 크게 힘을 쏟지 않아도 됐어요. 이후 간단하게 폴더 구조를 만들고, 바로 개발에 들어갔습니다.</p>
<h2 id="페이지별로-톺아보는-moty-구현기">페이지별로 톺아보는 MOTY 구현기</h2>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/4b6f07da-119a-417e-86d4-20481a8daa9a/image.webp" alt="MOTY Preview"></p>
<h3 id="0-moty의-플로우-생각해보기">0. MOTY의 플로우 생각해보기</h3>
<p>사용자는 MOTY를 다음과 같은 플로우로 이용할 수 있습니다:</p>
<ol>
<li>사용자 이름 입력</li>
<li>영화 검색</li>
<li>한줄평 작성</li>
<li>결과 페이지를 확인 후 공유</li>
</ol>
<p>각 단계에서 입력된 정보는 쿼리 스트링을 통해 다음 단계로 전달돼요. 이는 CSR 환경의 일회성 토이 프로젝트라는 특성을 고려했을 때, 별도의 상태관리나 서버 구현 없이도 필요한 정보를 효율적으로 전달할 수 있는 방식이라고 생각했기 때문입니다.</p>
<h3 id="1-사용자-이름-입력-페이지">1. 사용자 이름 입력 페이지</h3>
<blockquote>
<p>MOTY를 사용하는 사용자의 이름을 입력받아요</p>
</blockquote>
<p>이 페이지에서는 사용자 이름에 대한 유효성 검사를 진행이 핵심 기능이었습니다. 사용자의 이름으로 어떤 문자열을 허용할지 등의 기준을 생각한 뒤, 다음과 같은 기준을 세웠어요:</p>
<ul>
<li>빈 문자열인가?</li>
<li>불필요한 공백이 존재하는가?</li>
<li>이모지가 포함되어 있는가?</li>
<li>허용된 문자만 사용하고 있는가? (한글, 영어, 숫자)</li>
<li>최대 길이를 초과했는가?</li>
</ul>
<pre><code class="language-ts">// validation.ts
export const NAME_VALIDATION: ValidationRules = {
    patterns: {
        emoji: /[\u{1F300}-\u{1F9FF}]/u,
        validChars: /^[ㄱ-ㅎ가-힣a-zA-Z0-9]+$/,
    },
    messages: {
        emoji: &#39;이모지는 사용할 수 없어요.&#39;,
        invalidChars: &#39;한글, 영문, 숫자만 사용할 수 있어요&#39;,
        empty: &#39;이름을 입력해 주세요.&#39;,
        length: &#39;10글자 이하로 입력해 주세요.&#39;,
        space: &#39;공백은 사용할 수 없어요.&#39;,
    },

    // 그 외 규칙들...
};</code></pre>
<p>문자열에 대한 패턴은 정규식으로 처리하고, 나아가 사용자에게 유효성 검사가 통과하지 못한 이유를 자세하게 설명해줄 수 있는 메시지도 포함해 객체 형태의 상수로 관리했습니다.</p>
<pre><code class="language-ts">// validation.d.ts
declare type ValidationRules = {
    maxLength: number;
    patterns: Record&lt;ValidationPatternKey, RegExp&gt;;
    messages: Record&lt;ValidationMessageKey, string&gt;;
    rules: Record&lt;ValidationRulesKey, boolean&gt;;
};</code></pre>
<p>또한 잘못된 키 사용을 방지하고, 타입 안정성을 확보하기 위해 <strong>Record</strong> 유틸리티 타입을 사용해 객체의 키-값 쌍을 정의했어요.</p>
<h3 id="2-영화-검색-페이지">2. 영화 검색 페이지</h3>
<blockquote>
<p>올해의 영화로 선정할 영화를 검색하고, 한줄평을 입력해요</p>
</blockquote>
<p>검색 페이지는 4개의 페이지 중 가장 많은 기능을 구현해야 했던 페이지였습니다. 사용자는 검색 페이지에서 다음과 같은 플로우로 페이지를 사용하게 됩니다:</p>
<ul>
<li>찾고싶은 영화에 대한 키워드 검색</li>
<li>키워드를 기반으로 검색된 영화 목록을 탐색</li>
<li>원하는 영화를 선택하고 한 줄 평을 작성</li>
</ul>
<p>먼저 검색 기능은 TMDB에서 제공하는 Open API를 사용했습니다. 키워드를 기반으로 요청을 보내고, 해당하는 정보를 응답으로 제공해 줄 수 있는 검색 API예요. JSON 형태로 제공되는 응답에는 현재 페이지와 함께 영화의 정보에는 id, 장르, 제목, 줄거리 등이 포함되어 있습니다. 다만 이 검색 API에는 <strong>아쉬운 점</strong>이 하나 있었는데, 이후 리팩토링 섹션에서 살펴보도록 할게요.</p>
<pre><code class="language-ts">const { searchTerm, setSearchTerm, fetchState, handleSearch, loadMore } =
    useMovieSearch();</code></pre>
<p>영화 목록을 탐색하는 기능은 <code>IntersectionObserver</code>를 활용한 무한 스크롤을 통해 구현했습니다. 사용자가 무한 스크롤을 사용할 때, 영화를 다 불러왔는지의 여부를 알려주기 위해 observerRef에 해당하는 요소에 메시지를 표시할 수 있도록 했어요. TMDB의 검색 API가 페이지네이션까지 지원하고 있었기 때문에, 간단하게 현재 페이지 수를 인자로 넘겨주는 것 만으로도 원하는 응답을 제공받을 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/a634ef8a-c9b7-4775-8a6e-9ead45211dec/image.webp" alt="MOTY Infinite Scroll"></p>
<p>그리고 영화를 선택하면, 모달 창에서 한 줄 평을 입력하고 결과 페이지로 이동하게 됩니다. 한 줄 평이 너무 길어지는 것을 방지하기 위해서 이름과 마찬가지로 글자 수 제한을 뒀는데, ‘몇 자 까지가 한줄평인가’라는 생각이 문득 들기도 했습니다. 정답은 없지만요.</p>
<h3 id="3-결과-페이지">3. 결과 페이지</h3>
<blockquote>
<p>선정된 영화를 티켓 형식으로 보여주고, 이미지로 저장하거나 카카오톡 또는 URL을 통해 공유해요.</p>
</blockquote>
<p>결과 페이지는 사용자의 이름, 영화 정보 및 한줄평을 쿼리 스트링으로 받아 렌더링하고, 이를 공유할 수 있는 기능을 가지고 있어요. 공유 방식은 카카오톡, URL 복사, 이미지 저장 3가지로 결정했습니다. 카카오톡을 통한 공유는 <a href="https://developers.kakao.com/docs/latest/ko/message/js-link">kakao developers의 공식 문서</a>에서 상세하게 정보를 제공해 주고 있었기 때문에 수월하게 구현했습니다. 채팅방에 공유했을 때 보여지는 메시지 템플릿도 쉽게 설정할 수 있었어요.</p>
<pre><code class="language-js">window.Kakao.Share.sendCustom({
    // 내가 설정한 템플릿 id로 간편하게 적용
    templateId: 115794,
    templateArgs: {
        USERNAME: username,
        TITLE: movieTitle,
        THUMB: imageUrl,
        QUOTE: quote,
    },
});</code></pre>
<p>이미지 저장 기능은 캡처 기능을 구현할 때 자주 사용되는 라이브러리인 <a href="https://html2canvas.hertzen.com/">html2canvas</a>를 사용했습니다. DOM을 파싱하고 CSS를 계산한 뒤, Canvas API를 사용해 화면을 그리고 이를 이미지 데이터로 변환해 저장하는 과정을 통해 캡처를 가능하게 하는 라이브러리입니다. 다만 API를 통해 불러온 이미지를 사용했기 때문에 CORS 오류가 발생했는데, 다양한 방법을 통해 해결해 보려 했음에도 불구하고 여전히 해결하지 못한 상황입니다. 이미지 저장 자체는 정상적으로 작동하고 있습니다.</p>
<h2 id="리팩토링-이런-것들에-집중했어요">리팩토링, 이런 것들에 집중했어요</h2>
<p>그렇게 이틀 만에 완성된 MOTY였지만, 보다 나은 서비스를 위해서는 가야 할 길이 멀었어요. 성능 최적화, UX, 접근성, SEO 등 다양한 부분에서 개선이 필요한 부분이 보였습니다. 실사용자들로부터 피드백을 받고, 이를 분석한 후 하나씩 적용해 나갔어요.</p>
<h3 id="ux--ui">UX &amp; UI</h3>
<p>아까 검색 페이지에서 검색 API에 문제가 하나 있었다고 했던 것, 기억하시나요? 바로 TMDB의 검색 API가 시맨틱 검색(Semantic Search)을 지원하지 않는다는 것이었습니다.</p>
<blockquote>
<p>시맨틱 검색이란 단순한 키워드 매칭이 아닌, 콘텐츠의 의미와 맥락을 이해하여 사용자의 검색 의도에 맞는 결과를 제공하는 검색 방식이에요.</p>
</blockquote>
<p>예를 들어서, 사용자가 ‘인사이드 아웃’이라는 영화를 검색하고 싶을 때, ‘인사이드아웃’이라고 붙여 쓴 키워드를 통해 검색을 진행해도 키워드를 바탕으로 의미있는 검색 결과를 제공하는 것이 바로 시맨틱 검색입니다. 하지만 사용중인 TMDB API가 시맨틱 검색을 지원하지 않아서, 이로 인해 발생하는 문제를 처리해야 했어요.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/4c9a1126-80cc-4d65-94e1-a5e56e2852d2/image.webp" alt="MOTY Search Refactor"></p>
<p>그래서 별도의 검색 알고리즘을 구현하는 대신, 검색 결과 없음 컴포넌트에 검색 팁을 표시하고 이를 페이지 진입 시점에 노출하여 사용자가 올바른 검색어를 입력할 수 있도록 안내했습니다. 이를 통해 추가 개발 리소스 없이도 더 나은 사용자 경험을 제공할 수 있었어요.</p>
<h3 id="접근성">접근성</h3>
<p>다음으로 신경썼던 부분은 웹 접근성입니다. 서비스를 개발하며 접근성을 고려해야 하는 이유는 너무나도 많고, 또 중요하지만, 웹이 가진 힘을 생각해본다면 한 문장으로 귀결될 수 있을 것 같습니다.</p>
<blockquote>
<p>&quot;웹의 힘은 그것의 보편성에 있다. 장애에 구애없이 모든 사람이 접근할 수 있는 것이 필수적인 요소이다.&quot; - Tim Berners-Lee</p>
</blockquote>
<p>물론 모범적인 웹 접근성의 모든 기준을 개인이 고려하기에는 너무나도 그 항목이 방대합니다. 당장 <a href="https://www.wa.or.kr/board/view.asp?sn=22592&amp;page=1&amp;search=&amp;SearchString=&amp;BoardID=0004&amp;cate=">한국형 웹 콘텐트 접근성 지침(KWCAG)</a>만 살펴보더라도, 54페이지에 달하는 지침을 가지고 있어 이걸 어디서부터 어떻게 적용해야 하나 싶기도 했습니다. 그럼에도 가능한 많은 접근성을 적용해 보고자 했고, 따라서 Claude에 해당 지침을 업로드하고 코드를 살펴보는 식으로 접근성을 고려한 리팩토링을 진행했습니다. 예시를 몇 가지 살펴볼까요?</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/8229ddaa-faa0-45f1-9932-017e80b4f59b/image.webp" alt="MOTY 404"></p>
<p>위 이미지는 404 오류가 발생했을 때 리다이렉션 되는 페이지의 모습입니다. 이 페이지에서 시각 장애가 없는 사용자라면 <em>&#39;아, 404 오류가 발생했고, 이로 인해 이 페이지에 접근했구나. 버튼을 눌러 메인 페이지로 돌아가야지&#39;</em>라는 판단을 충분히 할 수 있겠지만, 시각 장애를 가진 사용자라면 이러한 정보를 스크린리더에 의존해야 합니다. 또한 tab 키를 사용해 탐색할 경우, 버튼에만 포커스가 가기 때문에 h1과 p 태그의 내용은 자동으로 읽히지 않습니다.</p>
<pre><code class="language-html">&lt;h1 className=&quot;text-4xl font-bold&quot; id=&quot;not-found-title&quot;&gt;404 오류&lt;/h1&gt;
&lt;p className=&quot;mt-2 text-center text-xl&quot; id=&quot;not-found-description&quot;&gt;
    죄송합니다.
    &lt;br aria-hidden=&quot;true&quot; /&gt;
    요청하신 페이지를 찾을 수 없습니다.
&lt;/p&gt;

// h1과 p에 대한 label을 button에 연결
&lt;button aria-describedby=&quot;not-found-title not-found-description&quot;&gt;
    메인 페이지로 돌아가기
&lt;/button&gt;</code></pre>
<p>이러한 상황을 고려하여 button 요소에 aria-describedby 속성을 부여하고, h1과 p 태그의 id를 연결했습니다. 이를 통해 스크린리더 사용자가 tab 키로 버튼에 도달했을 때 &#39;메인 페이지로 돌아가기 버튼&#39; 뿐만 아니라 &#39;404 오류&#39;와 &#39;죄송합니다. 요청하신 페이지를 찾을 수 없습니다.&#39;라는 설명도 함께 읽어주어, 현재 상황과 버튼의 용도를 충분히 이해할 수 있도록 했습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/8a939849-2fb8-4629-bb6f-d2b39e18c465/image.webp" alt="MOTY Contrast"></p>
<p>또한 저시력이나 색각 장애가 있는 사용자도 콘텐츠를 불편 없이 인식할 수 있도록 텍스트와 배경색 간의 적절한 명도 대비를 제공하는 것이 중요합니다. WCAG 지침은 텍스트와 배경색 간의 명도 대비가 4.5:1 이상이 되어야 한다고 규정하고 있는데, <a href="https://sitero.co.kr/contrast">이를 확인할 수 있는 도구</a>를 사용해 해당 지침을 만족할 수 있도록 UI를 개선했습니다.</p>
<p>마지막으로 접근성 개선 중 가장 고민했던 부분 중 하나는 영화 선택 모달이었습니다. 초기 버전에서는 한줄평 작성 모달이 열려도 키보드 포커스가 모달 뒤의 배경 콘텐츠(영화 목록)에 그대로 남아있어, 키보드 사용자는 모달의 내용과 상호작용할 수 없는 문제가 있었습니다.</p>
<p>KWCAG 지침에서는 이러한 상황에 대해 다음과 같이 명시하고 있습니다.</p>
<blockquote>
<p>&quot;모달이 실행되면 초점은 모달로 이동하고, 모달이 활성화된 상태에서 키보드는 모달 내부에서만 이동해야 한다.&quot;</p>
</blockquote>
<p>이 지침을 준수하기 위해 <strong>포커스 트랩(Focus Trap)</strong>을 구현했습니다. 말 그대로 사용자의 키보드 포커스를 특정 영역(모달) 안에 &#39;가두는&#39; 기술입니다. 이를 커스텀 훅(useFocusTrap)으로 만들어 재사용성을 높였습니다.</p>
<pre><code class="language-tsx">import { useEffect } from &quot;react&quot;;
import { getFocusableElements } from &quot;../utils/dom&quot;;

interface UseFocusTrapProps {
  containerRef: React.RefObject&lt;HTMLElement&gt;;
  onClose: () =&gt; void;
}

export const useFocusTrap = ({ containerRef, onClose }: UseFocusTrapProps) =&gt; {
  useEffect(() =&gt; {
    const setInitialFocus = () =&gt; {
      const container = containerRef.current;
      if (!container) return;

      const focusableElements = getFocusableElements(container);
      focusableElements[0]?.focus();
    };

    const trapFocus = (event: KeyboardEvent) =&gt; {
      if (!containerRef.current) return;

      const focusableElements = getFocusableElements(containerRef.current);
      if (focusableElements.length === 0) return;

      const firstElement = focusableElements[0] as HTMLElement;
      const lastElement = focusableElements.at(-1) as HTMLElement;

      if (event.shiftKey &amp;&amp; document.activeElement === firstElement) {
        event.preventDefault();
        lastElement.focus();
      } else if (!event.shiftKey &amp;&amp; document.activeElement === lastElement) {
        event.preventDefault();
        firstElement.focus();
      }
    };

    const handleKeyDown = (event: KeyboardEvent) =&gt; {
      if (event.key === &quot;Escape&quot;) {
        onClose();
        return;
      }

      if (event.key === &quot;Tab&quot;) {
        trapFocus(event);
      }
    };

    document.addEventListener(&quot;keydown&quot;, handleKeyDown);
    setInitialFocus();

    return () =&gt; {
      document.removeEventListener(&quot;keydown&quot;, handleKeyDown);
    };
  }, [containerRef, onClose]);
};</code></pre>
<h3 id="seo">SEO</h3>
<p>결과 페이지의 링크를 공유했을 때 opengraph 이미지와 함께 쿼리 스트링으로부터 받아온 메타 정보를 표시하고자 했습니다. 그러나 문제가 있었습니다. CSR 환경에서는 검색엔진 크롤러가 서버로부터 받는 초기 HTML만을 읽기 때문에 동적으로 생성되는 메타 태그를 제대로 인식하지 못합니다. 이를 해결하기 위해서는 react-helmet-async와 같은 라이브러리와 함께 프리렌더링 도구(React-Snap)를 사용할 수 있지만, 서버가 없는 순수 CSR 환경에서는 이러한 방식도 완벽한 해결책이 되지 못합니다. 또한 react-helmet-async는 최근 유지보수가 거의 이루어지지 않고 있어 안정성에도 우려가 있었습니다. 따라서 이 프로젝트에서는 불완전한 동적 SEO 대신, index.html에 필수적인 메타 정보만을 정적으로 명시하는 방식을 선택했습니다.</p>
<h2 id="나오면서-좋아하는-마음으로-개발을-한다는-것">나오면서: 좋아하는 마음으로 개발을 한다는 것</h2>
<p>MOTY는 2024년 연말을 맞이하여 &#39;올해의 영화&#39;를 공유할 수 있는 웹 서비스입니다. 사용자의 이름을 입력받고, 영화를 검색하여 한줄평과 함께 공유할 수 있는 기능을 제공하고 있어요.</p>
<p>서비스를 개발하며 가장 신경 썼던 부분은 &quot;좋아하는 마음으로 만들되, 제대로 된 서비스를 만들자&quot;였습니다. 단순히 영화를 검색하고 공유하는 기능을 넘어서, 장애가 있는 사용자도 불편함 없이 서비스를 이용할 수 있도록 웹 접근성 지침을 준수하려고 했고, 시맨틱 검색이 지원되지 않는 상황에서도 사용자가 원하는 영화를 쉽게 찾을 수 있도록 UX를 개선했으며, CSR 환경에서 SEO를 최적화하기 위해 고민했습니다.</p>
<p>이러한 고민과 노력 덕분이었을까요? 제가 좋아하는 영화라는 주제로 누구나 불편함 없이 이용할 수 있는 서비스를 만들 수 있었다는 점에서도 의미가 깊지만, 감사하게도 이 과정을 좋게 봐주신 분 덕분에 <a href="https://cafe.naver.com/winterlood/1520">한입 커뮤니티 온라인 세미나</a>에서 발표자로 서는 소중한 경험까지 할 수 있었습니다.</p>
<p>하나의 작은 프로젝트를 완성하고 공유하는 경험이 단순히 기능을 구현하는 것을 넘어, 더 나은 웹을 향한 여정이자 또 다른 성장의 기회가 될 수 있다는 것을 다시 한번 느꼈습니다. 🙂</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[다녀왔습니다, TEOConf 2024]]></title>
            <link>https://velog.io/@camille_protocol/%EB%8B%A4%EB%85%80%EC%99%94%EC%8A%B5%EB%8B%88%EB%8B%A4-TEOConf-2024</link>
            <guid>https://velog.io/@camille_protocol/%EB%8B%A4%EB%85%80%EC%99%94%EC%8A%B5%EB%8B%88%EB%8B%A4-TEOConf-2024</guid>
            <pubDate>Mon, 16 Jun 2025 13:54:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 이전에 운영하던 블로그에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2024년 12월 7일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서">들어가면서</h2>
<p>개발자라면 누구나 한번쯤은 가 보고 싶은 곳, 바로 컨퍼런스입니다. 다양한 개발자들이 모인 곳에서 함께 세션을 듣고, 네트워킹하며 인사이트를 얻어갈 수 있는 시간을 가지는 행사예요. 저는 늘 이런 컨퍼런스에 한번쯤 가 보고 싶었습니다. 뭔가 멋있어 보이기도 하고, 나와 같은 직군에 종사하는 분들을 만나면 어떤 이야기들을 들을 수 있을까 궁금하기도 했기 때문입니다. 그래서 올해 초 <a href="https://2024.feconf.kr/">FEConf 2024</a>에 다녀오기도 했습니다. 아쉽게도 첫 컨퍼런스 경험이었지만, 얻어간 것들이 적다는 생각이 많이 들었습니다. 직접 사용해보지 못한 기술들에 대한 이야기와 어려운 개념 때문에 인사이트보다 물음표를 더 많이 떠안고 집에 돌아와야 했어요. 배움이 아직 적은 저에게, 첫 컨퍼런스는 마치 ‘그들만의 리그’와 같은 경험이었습니다.</p>
<p>그러던 중 지인에게 <a href="https://www.teoconf.com/">TEOConf 2024(이하 테오콘)</a>가 열린다는 소식을 듣게 되었습니다. 이번 컨퍼런스도 저번과 같이 그냥 빈손으로 돌아오면 어떡하지라는 생각을 하던 차에, 소개 페이지에서 본 문구가 관심을 끌었습니다.</p>
<blockquote>
<p>TEOConf 2024는 모두에게 열려있는 컨퍼런스라는 컨셉으로, 누구나 자신의 경험과 지식을 공유할 수 있는 즐거운 네트워킹의 장을 마련합니다.</p>
</blockquote>
<p>‘모두에게 열려있는 컨퍼런스’라는 문구가 흥미를 자극했고, 뒤이어 소개된 세션들의 제목도 저에게는 더 친숙하게 다가와 신청하게 됐습니다. 뽑는 인원수가 그렇게 많지는 않아 혹시 떨어지면 어쩌나 했지만, 정말 운이 좋게도 당첨되어서 다녀오게 됐습니다. 테오콘은 네트워킹 세션이 존재하는데, 이를 위해 사전에 5인 정도의 규모로 조가 편성되어서 어떤 분들을 만날까, 이야기는 잘 해볼 수 있을까 두근거리는 마음을 안고 발걸음을 옮겼습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/c6d6a888-7cb5-48c4-9789-b9eb6c4c2fe0/image.webp" alt="TeoConf 2024 Draw"></p>
<h2 id="teoconf-2024-후기">TEOConf 2024 후기</h2>
<p>대략적인 진행 순서는 아래와 같았습니다. 테오콘의 공간 대여를 진행한 ‘항해 플러스’에 대한 소개와, 팀빌딩과 팀네트워킹을 마친 뒤 세션을 듣고 각 세션별로 팀 네트워킹을 진행하는 순서로 진행됐어요.</p>
<h3 id="시간표">시간표</h3>
<ul>
<li>입장, 오프닝과 스폰서 소개 - 항해 플러스!</li>
<li>팀빌딩과 팀네트워킹</li>
<li>세션1</li>
<li>세션2</li>
<li>세션3</li>
<li>세션4</li>
<li>테오의 Q&amp;A</li>
<li>선물교환식과 마무리</li>
</ul>
<h3 id="팀빌딩과-팀네트워킹">팀빌딩과 팀네트워킹</h3>
<p>항해 플러스에 대한 소개를 듣고 난 뒤에는 팀빌딩과 팀네트워킹 세션이 진행됐습니다. 각자 서로에 대해서 소개하고 짧은 이야기를 나누면서 아이스브레이킹을 진행할 수 있는 세션이었어요. 일정이 끝날 때까지 서로 어색한 상태로 있으면 어떡하지라는 걱정이 들었지만, 다들 능동적으로 참여해 주시고 특히 저희 팀의 MC를 자처해서 맡아주신 분(고마워요 요마!) 덕분에 금새 화기애애한 분위기가 됐습니다. 초성퀴즈나 음악 1초 듣고 맞추기 같은 간단한 레크리에이션도 진행했는데, 이후 세션이 끝날 때마다 해당 세션에 대해 서로의 의견을 교환하는 세션 네트워킹 시간에 더 자유롭게 이야기를 꺼낼 수 있게 됐습니다. 테오콘의 배려가 돋보이는 진행이었어요. 그럼 본격적으로 세션에 대한 이야기를 하나씩 해 볼까요?</p>
<h3 id="세션1-30인의-심리-싸움-오프라인-주식게임-제작기">세션1: 30인의 심리 싸움, 오프라인 주식게임 제작기</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/ec0a5b03-1af9-4320-a83d-61b0a791593e/image.webp" alt="TEOConf 2024 S1"></p>
<ul>
<li>어느 날 찾아온 개발 권태기를 극복하기 위한 사이드 프로젝트</li>
<li>예능 프로그램에서 봤던 게임을 소셜 게임 형태로 구현하기</li>
<li>개발할 때 신경썼던 두 가지: 한정된 시간 내에 빠르게 개발하기, 고가용성</li>
<li>모임 서비스 MUNTO에 주식게임을 런칭한 후기</li>
<li>소셜 게임 프로젝트를 진행하며 얻은 것과 앞으로의 미래</li>
</ul>
<p>테오콘의 시작을 연 첫 번째 세션은 하이안의 <a href="https://drive.google.com/file/d/1dOI2bsc_q2Dj8Wq_nUwQ-6-sbA8jrSDy/view">‘30명의 심리 싸움, 오프라인 주식게임 제작기’</a>였습니다. 하이안은 4년차 프론트엔드 개발자로, 부업과 재태크에 관심이 많은 호기심 부자라고 스스로를 소개했어요. 어느 날 갑자기 번아웃이 찾아온 그는 회사 밖의 삶을 찾고 동기부여를 얻기 위해 사이드 프로젝트를 시작했습니다. 예능 프로그램 더 지니어스의 게임 중 하나였던 <a href="https://www.youtube.com/watch?v=5DbY6_j6pOE">‘폭풍의 증권시장’</a>을 소셜 게임으로 만들고 이를 직접 운영하며 수익을 얻는 과정을 진행하면서, 어떻게 번아웃을 극복하고 성장할 수 있었는지를 알아볼 수 있었습니다. 특히 개발자로 기획, 디자인, 개발, 마케팅이라는 과정을 전부 스스로 진행하는 과정이 인상깊었습니다. 개발 과정에서는 한정된 시간 내에 개발을 완료하기 위해 불필요한 기술 스택을 배제하고, 고가용성을 확보하기 위해 익숙한 기술을 사용하며 신뢰 가능한 AWS 클라우드 등을 선택하는 등 어떤 부분에 중점을 두고 개발을 진행했는지가 잘 드러나서 좋았어요.</p>
<p>또한 번아웃을 느꼈을 때 이를 극복하기 위해 작은 아이디어라도 직접 실행에 옮기게 된 모습이 인상깊었습니다. 단순히 아이디어로만 남기지 않고, 이를 실제 프로덕트로 구현해 수익까지 창출해낸 실행력이 저에게도 큰 동기부여가 되어 주었습니다.</p>
<h3 id="세션2-trunk-code-quality로-일관성-있는-코드를-쓰는-개발자-되기">세션2: Trunk Code Quality로 일관성 있는 코드를 쓰는 개발자 되기</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/f4249f7f-e1a6-415d-b22c-2e81c7672621/image.webp" alt="TEOConf 2024 S2"></p>
<ul>
<li>일관된 코드 작성의 중요성과 어려움</li>
<li>린터 및 포맷터 관리의 중요성</li>
<li>Trunk에 대한 소개와, React Native Step Counter를 개발하면서 적용해 본 후기</li>
</ul>
<p>두 번째 세션은 앤드류의 <a href="https://drive.google.com/file/d/1gCJ48ZMUU64d9YkcczwgRqWlkevh4pu6/view">‘Trunk Code Quality&#39;로 일관성 있는 코드를 쓰는 개발자 되기’</a> 였습니다. 글을 읽고 계신 여러분 모두 <strong>린터</strong>(Linter)와 <strong>포맷터</strong>(Formatter)에 대해서 잘 알고 계실 거라고 생각합니다. 전자는 소스 코드를 분석해 문법, 잠재적 버그, 스타일, 컨벤션과 관련된 문제를 찾아내는 도구입니다. 후자는 소스 코드를 자동으로 지정된 스타일 규칙에 맞게 정렬하고 재구성하는 도구이고요. 저는 린터로는 ESLint, 포맷터로는 Prettier를 주로 사용합니다. 이러한 린터와 포맷터는 전략없이 사용하게 된다면 런타임 버전과 설정 충돌 등으로 개발자의 골머리를 앓게 하는 원인이 되기도 합니다.</p>
<p>앤드류는 평소 일관된 코드 작성의 중요성과 어려움에 대해 고민해오던 중, 회사에서 진행한 React Native 기반의 Step Counter를 자체적으로 개발하면서 <a href="https://trunk.io/code-quality">Trunk</a>라는 DevEx 툴킷을 접하게 됐습니다. 공식 문서에서, Trunk는 자신의 역할을 이렇게 정의하고 있습니다.</p>
<blockquote>
<p>&quot;Trunk is a developer experience(DevEx) toolkit that enables you to ship code quickly while maintaining the necessary guardrails for excellent engineer teams.&quot;</p>
</blockquote>
<p>코드를 빠르게 배포하면서, 필수적인 가드레일(코드 스타일)은 유지할 수 있도록 도와주는 도구인 셈이지요. Trunk에 대해서 조금 더 소개하자면, 기존의 오픈소스 린터, 포맷터, 정적 분석 도구를 설치, 관리, 실행하고 일관된 출력을 제공할 수 있습니다. C++로 작성되어 있어, 단순히 기존의 린터들을 husky등을 사용해 차례대로 실행하는 것 보다 더 빠르기도 하고요. 또한 코드 베이스 전체를 검사하는 대신 새로운 변경 사항에 대해서만 검사를 실행하는 등 더 나은 생산성을 확보하기 위한 차별점도 가지고 있습니다. 앤드류가 개발해야 했던 Step Counter 기능은 다양한 언어를 사용하고 있었고, 각 언어에 따른 코드의 스타일도 구체적으로 지정되어 있었기에 Trunk를 적용하면서 더 나은 개발 경험을 확보할 수 있었습니다. 또한 <a href="https://www.npmjs.com/package/@dongminyu/react-native-step-counter">오픈소스 라이브러리로 만들어 배포</a>하기도 했습니다.</p>
<p>앤드류의 세션은 개발자가 코드 품질에 대해서 지녀야 할 책임감과, 지속 가능한 소프트웨어 개발을 위한 방법에 대해서 한번 더 생각해 볼 수 있는 기회가 되었습니다. 다음 프로젝트에는 Trunk를 꼭 사용해 봐야겠어요.</p>
<h3 id="세션3-출근해서-바로-써-먹을-수-있는-커뮤니케이션-팁">세션3: 출근해서 바로 써 먹을 수 있는 커뮤니케이션 팁</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/1e59ea60-4bc5-4888-9e66-c8e15aa9910a/image.webp" alt="TEOConf 2024 S3"></p>
<ul>
<li>보다 나은 생산성을 위해 생각해볼 수 있는 것: 커뮤니케이션</li>
<li>팁 1. 팀원들과 디테일에 대한 생각 맞추기</li>
<li>팁 2. 대화할 때 배경지식 even하게 맞추기</li>
<li>팁 3. 궁금해 할 점 예측하고 미리 대답하기</li>
<li>팁 4. 대안이 있다면 같이 제시하기</li>
<li>팁 5. 전달할 내용 한번에 전달하기</li>
<li>팁 6. 예측가능하게 작업상황 자주 공유하기</li>
<li>팁 7. 이슈 가능성 미리 공유하기</li>
<li>팁 8. 마감일과 중요도를 파악하기</li>
</ul>
<p>세 번째 세션은 동훈의 <a href="https://speakerdeck.com/donghoon759/culgeunhaeseo-baro-sseo-meogeul-su-issneun-keomyunikeisyeon-tib?slide=2">‘출근해서 바로 써 먹을 수 있는 커뮤니케이션 팁’</a>이었습니다. 교육 스타트업에 재직중인 그는 보다 나은 생산성을 위한 방향에 대해서 고민하던 중, 커뮤니케이션을 개선할 수 있는 방법에 대해 고민하고 8개의 팁을 공유했습니다. 세션의 목표는 제목처럼 내일 당장이라도 출근해서 써먹을 수 있는 커뮤니케이션 팁에 대해서 소개해주는 것이었습니다. 동훈은 업무상 비동기 커뮤니케이션(채팅, 메일 등 실시간 상호작용을 기반으로 하지 않는)을 하게 되는 경우가 더 많다고 했습니다. 이러한 비동기 커뮤니케이션 상황에서는 상대의 답장이 언제 올지 모르기 때문에, 자신의 커뮤니케이션 스킬에 집중하는 방향을 선택했습니다.</p>
<p>저는 평소 교육 프로그램에 참여해 팀 프로젝트를 진행하면서 소프트스킬에 대한 긍정적인 피드백을 많이 받았습니다. 그럼에도 불구하고 스스로는 아쉬운 부분이 있다고 느낀 경우들이 종종 있었습니다. ‘처음부터 이렇게 이야기했다면 대화 자원을 덜 소모해도 됐을텐데’, ‘보다 나은 답변을 받기 위해서 내 질문 방법을 개선해야 하는 것이 아닐까?’라는 고민을 하곤 했어요. 그래서 동훈의 세션이 더 도움이 됐습니다.</p>
<p>소개된 모든 팁들에 공감했지만, 그중 가장 와닿았던 팁은 <strong>이슈 가능성 미리 공유하기</strong>였습니다. 프로젝트를 진행하면서 처음 구현해보거나 익숙하지 않은 작업을 진행해야 할 때, 다음과 같이 이야기해볼까요?</p>
<blockquote>
<p>&quot;페이지네이션 컴포넌트는 익숙하지 않아서 구현을 완료하기까지 시간이 더 걸릴 것 같습니다. 예정대로라면 7일에 PR을 올릴게요.&quot;</p>
</blockquote>
<p>팀원들은 7일까지 제가 작업을 최대한 진행할 것이라는 사실과 함께, 익숙하지 않은 작업을 하다 보니 시간이 더 걸릴수도 있겠다는 점을 인지할 수 있습니다. 이를 통해 미리 이슈 가능성에 대해 공유하고, 하염없이 기다리기만 하는 상황을 방지할 수 있어요. 이렇게 커뮤니케이션 방법을 개선한다면 프로젝트를 진행하면서 불필요한 자원을 소모하는 경우가 줄어들게 되고, 원활하게 작업을 이어갈 수 있을 거예요.</p>
<h3 id="세션4-주니어-개발자의-200일간-혼자만의-짧은-글쓰기-챌린지로-성장하기">세션4: 주니어 개발자의, 200일간 혼자만의 짧은 글쓰기 챌린지로 성장하기</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/76905e9c-5ada-4488-afcd-c1441f5004cc/image.webp" alt="TEOConf 2024 S4"></p>
<ul>
<li>글쓰기가 개발자 성장에 도움되는 이유</li>
<li>글을 쓰기 시작한 계기</li>
<li>실패기</li>
<li>짧은 글쓰기 챌린지 시작</li>
<li>성공할 수 있었던 이유</li>
<li>마치며</li>
</ul>
<p>마지막 세션은 병스커의 <a href="https://drive.google.com/file/d/1i2GMA9CuxSKmFoLDe6Tvwzosci4NOuNM/view">‘주니어 개발자의, 200일간 혼자만의 짧은 글쓰기 챌린지로 성장하기’</a>였습니다. 에듀테크, 블록체인 도메인에서 FE 개발자로 근무하고 있는 그는 프로덕트 디자이너로 첫 커리어를 시작했고, 이후 개발자로 전환했어요. 정보를 얻고자 알게된 아티클 문화로부터 디자이너들도 글을 써서 공유한다는 것을 알게 되었고, 개발자로 전향하며 그들이 글을 공유하는 문화는 훨씬 더 거대했다는 것을 깨닫게 되었습니다. 직업을 갖기 전에도 어떤 소재이든 글을 쓰는 사람이 되고 싶었던 그는 이후 기슬 블로그를 직접 생산하는 사람이 되는 것을 목표로 정했어요. 이를 통해 기술적 성장을 이룰 수 있기 때문이었습니다.</p>
<p>물론 첫걸음부터 성공한 것은 아니었습니다. 가장 어려웠던 것은 <strong>꾸준하게 글을 작성하기</strong>였는데, 글쓰기를 위한 소재가 없고, 시간내기가 어렵다는 나름의 변명이 있었습니다. 이를 해결할 수 있는 방법으로, 병스커는 ‘30일간의 매일 짧은 글쓰기 챌린지’를 시작했어요. 그리고 글쓰기에 대한 부담감을 덜기 위해서 글의 주제를 조금 더 자유롭게 작성했습니다. 소제에 제한을 두지 않고, 그날 겪은 에피소드나 풀리지 않는 고민등을 짧게 작성해 나갔습니다. 또한 꾸준하게 글을 쓰기 위해서, 스마트폰을 통해 언제 어디서든 글을 써내려갈 수 있는 환경을 만들었어요. 그렇게 짧은 글쓰기 챌린지는 이제 어느덧 300일을 바라보고 있게 됐습니다.</p>
<p>병스커의 세션은 저에게 자기반성의 시간이기도 했습니다. ‘꾸준한 회고를 통해 성장한다’는 모토로 기술 블로그를 운영하는 저였지만, 정작 꾸준하게 글을 작성하지 않고 있었어요. 그리고 과거의 병스커처럼, 저 역시도 소재가 부족하다거나 시간내기가 어렵다 등의 핑계를 대면서 글쓰기를 이어가지 못했습니다. 막상 글을 써야겠다는 생각이 들었을 때에도, ‘기술 블로그인데, 그래도 좀 퀄리티 있는 글을 적어야 하지 않을까? 어떤 주제를 잡아야 하지?’등의 고민을 하며 초안조차도 쉽게 작성하지 못하고 다시 뒤로가기를 누르는 저를 볼 수 있었습니다. 그러다보니 글쓰기 습관이 잡히지 않았고, 회고 스터디를 진행하면서도 스터디 전날 부랴부랴 글을 짜내곤 했어요. 이제는 저도 병스커처럼, 짧은 글이라도 노션이나 SNS에 남기는 것을 시작으로 글쓰기 습관을 꾸준히 다져야겠다는 생각이 들었습니다.</p>
<h3 id="테오의-qa">테오의 Q&amp;A</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/337575ea-ad5c-4b1f-943f-e618a9741958/image.webp" alt="TEOConf 2024 QnA"></p>
<p>모든 세션이 끝나고 난 뒤, 테오의 Q&amp;A가 진행됐습니다. Q&amp;A 세션에서는 테오콘 신청 당시 받았던 사전 질문을 살펴보며 이에 대한 답변을 해 주는 시간이었어요. 정말 다양한 질문이 있었지만, 질문의 주된 골자는 다음과 같이 요약할 수 있을 것 같습니다.</p>
<blockquote>
<p>&quot;어떻게 해야 개발자로서 성장할 수 있을까요? ‘잘 하는’ 개발자가 되려면 어떻게 해야 할까요?&quot;</p>
</blockquote>
<p>아마 글을 읽고 계신 여러분도 마음 한켠에 품어두고 있는 질문이라고 생각합니다. 몇년 사이 AI가 급부상하고, 어려워지는 상황 속에서 개발자가 어떤 역할을 해야 하는지에 대해서 많은 이야기들이 오갔습니다. 그래서 다들 불안함을 느끼기도 하는 것 같고요. 지금부터 써내려가는 이야기는 세션을 들으며 테오의 의견을 요약한 것입니다.</p>
<p>테오는 앞선 질문에 대한 해답을 주기 전에 질문을 하나 던졌습니다.</p>
<blockquote>
<p>&quot;우리는 왜 불안함을 느낄까요?&quot;</p>
</blockquote>
<p>불안함이라는 감정은 현재에 안주하고 싶지 않고, 성장하고 싶다고 생각할 때 느껴지는 감정입니다. 이러한 불안함을 떨쳐내는 방법은 신뢰와 자기확신을 기반으로 앞으로 나아가야 하고요. 그러나 믿음에는 근거가 있어야 합니다. 그리고 테오는 그러한 근거는 <strong>성장, 기여, 기록</strong>의 세 가지 방법으로부터 빚어질 수 있다고 말했습니다.</p>
<p>첫 번째로, 성장하기 위해서는 내가 회사에서 무엇을 하고 있는지, 그리고 무엇이 중요한지를 돌아봐야 합니다. 일을 잘 한다는 것은 곧 자신의 쓸모를 회사에 증명하는 과정입니다. 입사 초기 주니어 개발자의 경우 개발의 기초 체력을 키워야 합니다. 흔히들 하는 공식 문서 톺아보기처럼 심화 개념에 대해 공부하며 개발자로서 기반을 다지는 과정이 여기에 속합니다. 이후에는 성과를 낼 수 있고, 누군가를 도울 수 있는 방향으로 학습을 진행해야 합니다. 덧붙여서, 무엇이 중요한지 모른 채 열심히만 하는 것은 올바른 성장의 방향이 아닙니다. 또한 회사의 업무가 너무 단조로워서 내가 정체되어 성장할 수 없을 것 같아 두렵다는 생각이 들 때에는, 스스로 필요를 만들어 학습하는 것을 권장합니다. 이 역시 성장의 일종이며, 우리의 뇌는 필요한 것을 학습할 때 활성화되기 때문입니다.</p>
<p>두 번째로, 기여를 한다는 것은 남의 일을 돕는다는 뜻이기도 합니다. 이때 오해하지 말아야 할 것은, 이는 남의 일을 빼앗거나 단순히 대신 해주기만 하는 호구가 되는 방향이 아니라는 점입니다. 물론 이러한 이야기가 오갔을때 흔히 사람들은 이렇게 이야기하곤 합니다.</p>
<blockquote>
<p>&quot;하지만 내 일도 벅차지 않나요?&quot;</p>
</blockquote>
<p>그럼에도 불구하고 그걸 어떻게 해낼 수 있을지 고민하고 해결하는 것이 주니어 이후 개발 실력을 향상시킬 수 있는 방법입니다. 테오는 주니어부터 시니어 개발자까지의 단계를 이렇게 정의합니다.</p>
<ul>
<li>도움이 필요한 사람(주니어)</li>
<li>누구의 도움이 없이도 문제를 해결할 수 있는 사람</li>
<li>주어진 업무 외에도 무언가를 더 해낼 수 있는 사람</li>
<li>다른 사람들을 도울 수 있는 사람(시니어)</li>
</ul>
<p>이렇게 성장하기 위해서는, 내가 도움을 줄 수 있는/받을 수 있는 사람의 범위를 한 명씩 늘려가 보는 것이 중요합니다.</p>
<p>마지막으로, 성장하고 기여하기 위해서는 <strong>기록</strong>의 힘을 무시할 수 없습니다. 나의 성과를 보여줄 수 있는 부분을 조금씩 기록해 나아가, 이렇게 쌓인 기록은 곧 성장의 증거가 되기 때문입니다.</p>
<h2 id="나오면서-성장하는-개발자를-꿈꾼다는-것">나오면서: 성장하는 개발자를 꿈꾼다는 것</h2>
<p>이렇게 저의 두 번째 컨퍼런스 경험은 끝났습니다. 기대했던 것 보다 더 많은 것들을 얻어갈 수 있었던 시간이었어요. 기회가 된다면 다음 번에도 다시 참여하고 싶습니다. 그럼 테오콘은 저에게 어떤 도움이 됐을까요? 개발자로서의 제 모토는 <strong>코드 그 너머의 변화를 추구하는 개발자</strong>입니다. 조금 더 풀어서 설명하자면 이렇습니다. 매일 수많은 사람들이 사용하는 웹 서비스에서, 일상적인 경험을 더 나은 방향으로 변화시키는 프론트엔드 개발자가 되는 것입니다.</p>
<p>그러나 이런 모토를 가지고 있음에도 불구하고, 구체적으로 어떤 방법을 통해 제가 꿈꾸는 개발자가 될 수 있는지는 아직까지 물음표였습니다. 학습의 방향을 올바르게 설정하지 못하고, <strong>무엇이 중요한지 모른 채 열심히만 하는</strong> 상태였어요. 얼마 전 교육 프로그램에 지원하면서 자소서와 포트폴리오를 만들기도 했는데, 만들다 보니 제가 설정한 모토에 대한 근거가 부족하다는 것을 깨닫기도 했습니다. 불안함을 느끼고 성장하려고 했지만, 성장의 방향이 잘못 설정되어 있었습니다.</p>
<p>하지만 테오콘 이후로 학습의 방향성을 잡을 수 있게 됐습니다. 테오가 이야기했던 ‘성장, 기여, 기록’이라는 측면에서 살펴볼까요?</p>
<h3 id="성장">성장</h3>
<ul>
<li>개발 기초 체력을 다지되, 단순 기술 습득을 넘어 나의 모토(사용자 경험 개선)에 초점 맞추기</li>
<li>하이안처럼 번아웃 상황에서도 새로운 도전을 통해 성장 기회를 만들기</li>
<li>앤드류의 사례처럼 코드 품질과 개발 생산성을 높이는 도구들을 학습하고 실제로 적용해보기</li>
<li>동훈이 제시한 것처럼 기술적 스킬 뿐만 아니라 커뮤니케이션 능력도 함께 발전시키기</li>
</ul>
<h3 id="기여">기여</h3>
<ul>
<li>사용자 경험을 개선할 수 있는 프로젝트에 참여하거나 기존 프로젝트 리팩토링하기</li>
<li>다른 개발자들과 지식을 공유하고 서로 성장할 수 있는 네트워크 형성하기</li>
<li>오픈소스 프로젝트에 참여해 기여해보기</li>
</ul>
<h3 id="기록">기록</h3>
<ul>
<li>병스커의 사례처럼 꾸준한 글쓰기 습관 만들기</li>
<li>핑계대지 않고, 짧은 글이라도 매일 작성하는 습관 들이기</li>
<li>성장 과정과 기여 활동을 기록해 나의 성장 증거로 삼기</li>
</ul>
<p>이러한 방향성을 가지고 꾸준히 실천한다면, 모토가 모토로만 남지 않고 실현될 수 있겠죠?</p>
<p>사족이지만, 예전에 제가 개발자로서 가지고 있는 모토는 ‘완벽한 개발자’였습니다. 무엇이든 마스터하려고 했었어요. 예를 들면, JS 마스터 같은 것들 말입니다. 그리고 라이브러리와 프레임워크를 자유자제로 다루고, 어떤 알고리즘 문제를 봐도 척척 풀어내며 신들린 성능 최적화와 함께 접근성까지 고려하는 정말 궁극의 개발자가 되고 싶었습니다. 물론 지금 생각해보면 거의 공상에 가까운 비현실적인 모토였지만요. 이제는 명확한 방향성을 가지고 앞으로 나아갈 때입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🍅 토마토들 프로젝트를 맺으면서]]></title>
            <link>https://velog.io/@camille_protocol/%ED%86%A0%EB%A7%88%ED%86%A0%EB%93%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%BA%EC%9C%BC%EB%A9%B4%EC%84%9C</link>
            <guid>https://velog.io/@camille_protocol/%ED%86%A0%EB%A7%88%ED%86%A0%EB%93%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%BA%EC%9C%BC%EB%A9%B4%EC%84%9C</guid>
            <pubDate>Mon, 16 Jun 2025 13:46:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 <a href="https://hoonieblog.xyz/post/retro-tomatoes">이전에 운영하던 블로그</a>에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2024년 11월 3일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서">들어가면서</h2>
<p>지난 9월 중순부터 10월까지, <a href="https://udemy.wjtb.co.kr/event/id/277">프로젝트 캠프: Next.js 3기</a>라는 교육 프로그램에 참여하게 됐습니다. 2주간의 사전 교육 기간 이후 4주동안의 팀 프로젝트를 진행했고, 이에 대한 회고를 풀어보고자 합니다.</p>
<ul>
<li><a href="https://tomatoes-lemon.vercel.app/">토마토들 배포 링크</a></li>
<li><a href="https://github.com/hminah0215/tomatoes">토마토들 GitHub</a></li>
</ul>
<h3 id="토마토들이란">토마토들이란</h3>
<p><code>토마토들</code>은 대학생들이 참여할 수 있는 대외활동과 공모전 정보를 손쉽게 찾을 수 있도록 제공하고, 취업과 대학생활에 도움이 되는 다양한 정보를 안내하는 웹 서비스입니다. 프로젝트는 기업과의 연계 과정으로 진행되었고, 필요한 디자인 파일과 서비스에 대한 소개는 미리 기업측으로부터 제공받았습니다.</p>
<p>토마토들의 주요 기능은 크게 홈, 매거진, 대외활동 및 공모전 페이지와 검색 페이지로 나뉩니다.</p>
<ul>
<li><strong>홈</strong>: 매거진과 공모전 및 대외활동 정보 모아보기</li>
<li><strong>공모전/대외활동</strong>: 공모전과 대외활동에 대한 정보를 필터와 정렬 기능을 통해 확인하기</li>
<li><strong>매거진</strong>: 대학생활 및 대외활동에 관련된 다양한 팁 모아보기</li>
<li><strong>검색</strong>: 공모전과 대외활동 및 매거진에 대한 정보 검색하기</li>
</ul>
<p>저는 이 중에서 공모전 및 대외활동 페이지와, 헤더와 페이지네이션 공통 컴포넌트를 구현했습니다. 그리고 대외활동 및 공모전 페이지에서 사용하기 위한 데이터를 크롤링하는 작업도 담당했습니다. 프로젝트를 진행하면서 어떤 기능들을 어떻게 구현했고, 또 어떤 고민들을 했는지 알아보기에 앞서 프로젝트의 개발 환경에 대한 이야기를 해보고자 합니다.</p>
<h2 id="개발환경">개발환경</h2>
<p>개발 환경은 크게 프론트/백엔드의 기술 스택과 폴더 구조, 그리고 데이터로 나누어 보았습니다. 먼저 기술 스택부터 살펴볼까요?</p>
<h3 id="프론트엔드">프론트엔드</h3>
<p>프론트엔드에서는 <code>Next.js</code>, <code>TypeScript</code>, <code>TailwindCSS</code>를 사용했습니다.</p>
<p><code>Next.js</code>의 SSR을 사용하면 다양한 공모전 및 대외활동 정보의 페이지를 빠르게 불러올 수 있고, 파일 구조에 기반 라우팅을 사용하기 때문에 프로젝트의 확장성과 유지보수가 용이하다는 장점이 있어서 선택했습니다.</p>
<p><code>TypeScript</code>는 이제는 사실상 Next.js를 사용하는 프로젝트에서는 디폴트가 된 스택이라고 생각하지만, 이번 프로젝트에서 사용한 그 이유를 고르자면 단연 <strong>타입</strong> 때문입니다. 다양한 컴포넌트에서 다양한 데이터 타입을 사용해야 했는데, 타입스크립트의 정적 타입 검사 기능을 통해 어디에 어떤 값이 들어가야 하는지 보다 명확하게 알 수 있었습니다.</p>
<p><code>Tailwind CSS</code>는 쉽고 빠른 적용을 위해 사용했습니다. 사전에 정의된 유틸리티 클래스 이름을 사용하는 것만으로 간편하게 스타일을 적용할 수 있다는 장점이 있었고, Next.js와의 기본 통합이 간편했어요. 추후에 이야기 할 내용지만, <strong>사전에 정의된 유틸리티 클래스</strong>라는 것이 때로는 마냥 좋은 것만은 아니기도 했습니다.</p>
<p>그 외에도 다양한 이유가 있겠지만, 추가적으로 2주간의 사전 직무 교육에서 위 세 가지 기술 스택을 학습했다는 점도 있었어요. 프로젝트 기간동안 새로운 기술 스택을 배우기보다는 모두가 어느정도 익숙해진 기술 스택을 사용하는 것이 더 빠른 생산성을 보장할 수 있다고 생각했기 때문입니다.</p>
<h3 id="백엔드">백엔드</h3>
<p>백엔드에서는 <code>Supabase</code>, <code>Python</code> 등을 사용했습니다.</p>
<p>저희 팀은 프로젝트에서 활용할 수 있는 데이터가 따로 주어지지 않았기 때문에 데이터를 직접 크롤링한 뒤 가공해야 했는데, 가공된 데이터를 저장할 수 있는 DB와 이를 사용자에게 전달해줄 수 있는 서버가 필요했습니다. 지금 사용하고 있는 블로그를 만들면서 <code>Supabase</code>를 사용해 본 적이 있었는데, 마침 다른 팀원도 사용해 본 적이 있다고 해서 빠르게 습득하고 활용 가능한 해당 기술을 선택했습니다.</p>
<p><code>Python</code>의 경우 프로젝트를 진행하면서 정말 오랜만에 사용해 본 언어였습니다. 기초적인 문법을 제외하면 아예 모르고 있다고 말해도 과언이 아닐 정도로 부족한 이해도를 가지고 있어서, 크롤링 관련 부분에 대해서는 LLM과 팀원의 도움을 많이 받았습니다.</p>
<h3 id="폴더-구조">폴더 구조</h3>
<p>Next.js는 기본적으로 정해진 폴더 구조가 있어서, 특히 app 폴더의 경우 해당 컨벤션을 잘 따르기만 하는 것으로도 충분했습니다. 그러나 보다 좋은 폴더 구조를 사용하고 싶다는 생각이 들어서, 관련 자료를 찾아보던 중 <code>containers</code> 폴더를 활용해 UI 및 레이아웃과 라우터 관련 로직을 분리하면 컴포넌트의 가독성을 높일 수 있다는 글을 읽게 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/a5d35887-9c5f-420a-9bd3-4411dcf08e7b/image.webp" alt="Containers Structure"></p>
<p>위와 같이 api를 제외하면 app과 완전히 동일한 형태의 구조를 containers에서 갖춘 것을 확인할 수 있습니다.</p>
<pre><code class="language-tsx">import { fetchActivityContestAbstractWith } from &#39;@/lib/fetchActivityAbstractWith&#39;;
import Activity from &#39;@/containers/activity/Activity&#39;;

// 메타데이터와 같은 페이지 컴포넌트에 필요한 로직

export default async function Page({ searchParams }: PageProps) {
    const filters = searchParams.filters?.split(&#39;,&#39;).filter(Boolean) || [];
    const sort = searchParams.sort || &#39;관련도순&#39;;

    const { data: activitiesContests, error } =
        await fetchActivityContestAbstractWith({
            filters,
            sort,
            mainCategory: &#39;대외활동&#39;,
        });

    if (error) {
        // 에러 핸들링 로직
    }

    return &lt;Activity activitiesContests={activitiesContests || []} /&gt;;
}</code></pre>
<p>그 결과 페이지 컴포넌트에서 이렇게 UI와 페이지의 로직을 분리해, 보다 가독성 높은 컴포넌트를 작성할 수 있게 되었어요. 나머지 다른 폴더는 일반적인 폴더 구조와 그 역할이 동일했습니다. 묘하게 이질적인 폴더가 하나 있는데, 바로 <code>scripts</code> 폴더입니다. <strong>앞서 데이터가 주어지지 않았다</strong>고 했는데, 이제 데이터와 크롤링에 대한 이야기를 해 보려고 합니다.</p>
<h3 id="데이터">데이터</h3>
<p>토마토들에는 크게 두 가지의 데이터가 필요했습니다. 대학생활 꿀팁과 관련된 매거진 페이지에서 보여주는 콘텐츠에 필요한 데이터와, 공모전 및 대외활동 페이지에서 보여주는 콘텐츠에 필요한 데이터였어요. 저는 둘 중 후자를 구해야 했습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/f46647d9-4afd-4696-ab6a-0e7e3dc2944d/image.webp" alt="Tomatoes Data"></p>
<p>위와 같은 페이지에 필요한 데이터를 구해야 했는데, 다행히 대학생을 위한 대외활동과 공모전을 소개하는 기존의 서비스는 많았기에 그중 한 곳인 &#39;캠퍼스픽&#39;에서 데이터를 크롤링해 오기로 했습니다. 크롤링한 데이터를 무단으로 상업적인 용도로 활용하면 당연히 법적인 문제를 야기할 수 있기 때문에, 설명을 이어가기에 앞서 저희가 크롤링 한 데이터는 비상업적인 용도로, 어떠한 수익도 창출하지 않음을 미리 밝힙니다.</p>
<p>결국 LLM과 함께 열심히 고민한 뒤, 우여곡절 끝에 데이터를 크롤링하긴 했습니다. 하지만 파이썬에 대한 지식이 부족했기 때문에 새로운 데이터 구조를 추가할 때마다 스크립트를 매번 실행시켜야 했고, 덕분에 정제된 데이터를 얻는 데까지 꽤나 오랜 시간이 들었습니다. 팀원의 도움이 없었다면 더 긴 시간을 크롤링에 소모해야 했을 것입니다. 하지만 문제는 이것 뿐만이 아니었습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/74335e1a-5a42-4e09-a7e4-2294f96219c2/image.webp" alt="Activity Category Filter"></p>
<p>토마토들은 대외활동과 공모전에서 적용 가능한 다양한 필터를 제공합니다. 그리고 그 종류가 총 84가지나 됐어요. 크롤링된 데이터에는 필터가 없었기에 그 필터를 직접 적용해 줘야 했는데, 수많은 데이터를 하나씩 읽어보면서 직접 종류에 맞는 필터를 삽입해 주는 것 말고는 다른 방법이 떠오르지 않았습니다.</p>
<p>결국 임시방편으로 데이터를 크롤링한 뒤 정제하는 과정에서 무작위로 필터를 적용해 주었습니다. 그래서 해외 봉사활동인데 국내 필터가 적용되어 있다거나 하는 안타까운 일이 벌어지고 말았지만, 딱히 더 좋은 방법이 떠오르지 않았고 기한 내에 프로젝트를 완수해야 했기 때문에 어쩔 수 없이 이러한 결정을 내리게 되었습니다. 이렇게 정제된 데이터를 Supabase에 저장하고, 필요에 따라 요청을 보내 불러오는 식으로 활용했습니다.</p>
<h2 id="구현과-고민">구현과 고민</h2>
<h3 id="헤더와-내비게이션">헤더와 내비게이션</h3>
<p>제가 담당했던 헤더와 내비게이션 바 컴포넌트는 웹과 모바일에서 레이아웃이 상이했습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/57ec5e97-268a-40ce-b799-64db2bc74c07/image.webp" alt="Navigation Bar Mobile"></p>
<p>모바일에서는 이렇게 내비게이션 바가 분리되어 스크린의 하단에 위치하는 형태였습니다. 이걸 구현할 수 있는 두 가지 방법을 떠올려 봤고, 저는 두 번째 방법을 선택했습니다.</p>
<ol>
<li>웹과 모바일에서 사용하는 내비게이션 컴포넌트를 두 개 만들고, 조건부로 렌더링하기</li>
<li>CSS를 활용해 하니의 내비게이션 컴포넌트를 스크린 width에 따라 다르게 렌더링하기 ✅</li>
</ol>
<pre><code class="language-ts">const containerClasses =
    &#39;flex flex-1 items-center fixed md:static bottom-[30px] left-1/2 transform -translate-x-1/2 md:transform-none z-50&#39;;

const listClasses =
    &#39;flex gap-6 lg:gap-10 bg-white h-full px-8 md:px-0 py-4 md:py-0 whitespace-nowrap shadow md:shadow-none border md:border-none border-sub-gray-100 rounded-full md:rounded-none items-center&#39;;

const linkItemClasses =
    &#39;whitespace-nowrap text-base lg:text-xl font-medium lg:font-semibold rounded-full py-2 px-3&#39;;

const activeLinkClasses = &#39;bg-sub-yellow-500 text-sub-gray-500&#39;;

const inactiveLinkClasses = &#39;bg-sub-yellow-100 text-sub-gray-400&#39;;</code></pre>
<p>이렇게 커스텀 클래스를 만들고 반응형으로 적용될 수 있도록 구현했습니다.</p>
<h3 id="페이지네이션과-그리드-뷰">페이지네이션과 그리드 뷰</h3>
<p>헤더와 내비게이션이 맛보기였다면, 페이지네이션과 그리드 뷰가 저에게는 메인 디쉬였습니다. 요즘 말로는 <strong>킥</strong>이라고 할 수도 있겠네요. 흔히 많은 양의 데이터를 한 번에 표시하지 않고 나눠서 표시하는 기능을 <strong>페이지네이션</strong>(Pagination)이라고 부릅니다. 토마토들에서 사용되는 페이지네이션 컴포넌트를 살펴볼까요?</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/69490f20-59d2-4925-b652-16a539447d23/image.webp" alt="Tomatoes Pagination"></p>
<p>저는 페이지네이션 컴포넌트를 이렇게 분리했어요. 페이지 컨트롤까지 포함하는 컴포넌트는 <strong>Pagination</strong>, 내부 컨텐츠를 그리드 형태로 렌더링하는 컴포넌트는 <strong>GridView</strong> 였습니다. 문제는 이 두 컴포넌트가 정말 다양한 곳에서, 정말 다양한 형태로 사용되고 있었어요. 먼저 GridView 컴포넌트부터 살펴볼까요?</p>
<h4 id="gridview">GridView</h4>
<ul>
<li>매거진<ul>
<li>토마토 Pick (웹 4 * 2 / 모바일 2 * 3)</li>
<li>토마토 Tip (웹 3 * 5 / 모바일 1 * 12)</li>
</ul>
</li>
<li>대외활동 및 공모전<ul>
<li>웹 4 * 4 / 모바일 2 * 5</li>
<li>홈<ul>
<li>BEST PICK (웹 2 * 4 / 모바일 2 * 3)</li>
<li>토마토들 추천활동 (웹 2 * 4 / 모바일 그리드 뷰 미사용)</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>이렇게 GridView 컴포넌트는 서비스 내의 많은 곳에서 다양한 형태로 사용되고 있었습니다. 내부에 렌더링되는 콘텐츠의 양도 페이지별로 달랐고요. 그래서 공용 컴포넌트를 하나 만들어 둔다면 재사용성을 높일 수 있을 거라고 생각했고, 고민 끝에 다음과 같은 구조로 GridView 컴포넌트를 구현했습니다.</p>
<pre><code class="language-tsx">export default function GridView&lt;T&gt;({
    items,
    GridItem,
    columnStyle,
    gapStyle,
}: GridViewProps&lt;T&gt;) {
    const columnStyles = {
        web4mobile2: &#39;grid-cols-2 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-4&#39;,
        web3mobile1: &#39;grid-cols-1 sm:grid-cols-2 md:grid-cols-3&#39;,
    };

    const gapStyles = {
        gapStyle1: &#39;gap-x-4 gap-y-8 md:gap-x-8 md:gap-y-12&#39;,
        gapStyle2: &#39;gap-x-6 gap-y-10 md:gap-x-10 md:gap-y-12&#39;,
        gapStyle3: &#39;gap-x-2 gap-y-6 md:gap-x-8 md:gap-y-10&#39;,
    };

    const gridClass =
        `grid ${columnStyles[columnStyle]} ${gapStyles[gapStyle]}`.trim();

    return (
        &lt;div className={gridClass}&gt;
            {items.map((item, index) =&gt; (
                &lt;GridItem key={index} item={item} /&gt;
            ))}
        &lt;/div&gt;
    );
}</code></pre>
<p>GridView 컴포넌트는 다음과 같은 props를 받습니다:</p>
<ul>
<li><strong>items</strong>: 렌더링되어야 하는 콘텐츠의 데이터</li>
<li><strong>GridItem</strong>: 렌더링되어야 하는 콘텐츠 레이아웃 컴포넌트</li>
<li><strong>columnStyle</strong>: 열의 형태</li>
<li><strong>gapStyle</strong>: 갭의 형태</li>
</ul>
<p>사용자가 해당 컴포넌트에 필요한 props를 전달하면, 이를 내부에서 사용할 수 있도록 구현했습니다. 보다 유연한 컴포넌트의 사용을 위해서, 제네릭 타입을 활용했어요.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/aa16eb72-c285-4b2d-bf38-efcff6746e02/image.webp" alt="GridView JSDoc"></p>
<p>그리고 JSDoc을 사용해 컴포넌트를 처음 사용하더라도 빠르게 활용할 수 있도록 매개변수와 사용 방법을 명시해 주었습니다.</p>
<h4 id="pagination">Pagination</h4>
<p>페이지네이션 컴포넌트도 GridView 만큼이나 여러 페이지에서 다양하게 사용되고 있었습니다.</p>
<ul>
<li>매거진<ul>
<li>토마토 Pick (웹 8개 / 모바일 6개)</li>
<li>토마토 Tip (웹 15개 / 모바일 12개)</li>
</ul>
</li>
<li>대외활동 및 공모전<ul>
<li>웹 16개 / 모바일 10개</li>
</ul>
</li>
<li>홈<ul>
<li>BEST PICK (웹 8개 / 모바일 6개)</li>
<li>토마토들 추천활동 (웹 8개 / 모바일 그리드 뷰 미사용)</li>
</ul>
</li>
</ul>
<p>보다시피, 페이지네이션은 한 페이지당 표시되는 콘텐츠의 개수가 페이지마다, 웹과 모바일 환겸마다 상이했습니다. 개인적으로는 페이지당 표시되는 컨텐츠가 웹과 모바일 환경에서 동일하게 유지되는 것이 사용자에게 더 나은 경험을 제공한다고 생각하지만(구현하기도 더 쉽고요), 최대한 디자인을 따르기로 했습니다. 그 전에, 우선 이렇게 페이지당 표시되는 컨텐츠가 웹/모바일에서 달라질 경우 발생하는 문제점에 대해서 살펴볼까요?</p>
<p>먼저 60개의 컨텐츠에 대한 페이지네이션이 이루어진다고 가정해봅시다. 웹은 페이지당 16개의 컨텐츠를, 모바일은 페이지당 10개의 컨텐츠를 렌더링합니다. 이 경우, 생성되는 페이지의 수는 각각 다음과 같습니다.</p>
<ul>
<li><strong>웹</strong>: ( 16 × 3 + 12 × 1 페이지) = 4 페이지</li>
<li><strong>모바일</strong>: (10 × 6 페이지) = 6 페이지</li>
</ul>
<p>처음부터 다른 환경에서 웹과 모바일 페이지를 확인하고 있다면 큰 문제가 되지는 않겠지만, 만약 사용자가 브라우저의 가로 크기를 모바일만큼 줄여서 보다가 웹의 가로 크기만큼 늘린다고 가정해봅시다. 이때 사용자가 모바일에서 6페이지를 보고 있었다면, 웹에서는 존재하지 않는 페이지를 보게 됩니다. 당연히 문제가 발생할 수 밖에 없습니다.</p>
<pre><code class="language-ts">export default function useResponsiveItemsPerPage(
    webItemsPerPage: number,
    mobileItemsPerPage: number,
) {
    const [itemsPerPage, setItemsPerPage] = useState(webItemsPerPage);

    useEffect(() =&gt; {
        function handleResize() {
            if (window.innerWidth &gt;= 768) {
                setItemsPerPage(webItemsPerPage);
            } else {
                setItemsPerPage(mobileItemsPerPage);
            }
        }

        handleResize();
        window.addEventListener(&#39;resize&#39;, handleResize);

        return () =&gt; {
            window.removeEventListener(&#39;resize&#39;, handleResize);
        };
    }, [webItemsPerPage, mobileItemsPerPage]);

    return itemsPerPage;
}</code></pre>
<p>그래서 해당 문제를 해결할 수 있도록 커스텀 훅을 만들었습니다. 이 훅은 웹과 모바일 환경에서 필요한 페이지 당 컨텐츠의 개수를 훅으로 받아, 화면의 가로 크기를 감지하고 적절한 컨텐츠 개수인 <code>itermsPerPage</code>를 반환합니다. 이를 Pagination 컴포넌트에 적용시켜 스크린 크기에 따라 적절하게 페이지를 조절할 수 있도록 했어요.</p>
<pre><code class="language-ts">const itemsPerPage = useResponsiveItemsPerPage(
    webItemPerPage,
    mobileItemPerPage,
);

const totalItems = contents.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);

if (currentPage &lt; 1 || currentPage &gt; totalPages) {
    notFound();
}</code></pre>
<p>또한, 앞서 말했던 <strong>존재하지 않는 페이지에 접근하게 되는 문제</strong>에 대한 해결책은 사용자가 존재하지 않는 페이지에 접근할 경우 <code>notFound()</code>를 호출해 적어도 사용자에게 잘못된 접근임을 알려줄 수 있는 방식으로 처리했습니다. 최종적으로는 웹 → 모바일, 모바일 → 웹 전환 시 이를 감지하고 사용자가 바뀐 뷰에서 위치하는 페이지를 어림잡아 계산해 보여주는 방식을 사용하고 싶었지만, 해당 부분은 구현하지 못했습니다.</p>
<h3 id="필터와-정렬-처리하기">필터와 정렬 처리하기</h3>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/4e855b3c-8af0-4396-8c63-f98d86d7e224/image.webp" alt="Tomatoes Filter and Sort"></p>
<p>먼저 공모전/대외활동의 메인 페이지를 살펴보면, 이런 식으로 필터를 토글하거나, 정렬 옵션을 선택할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/433a8f99-7713-45b5-a63a-93e3b5b88205/image.webp" alt="Tomatoes Detail Page"></p>
<p>그리고 사용자가 선택했던 필터는 이렇게 상세 페이지에서도 동일하게 적용되어 있어야 했어요. 또한 이 필터를 바탕으로 카테고리별 메인 페이지에서 필터에 맞는 컨텐츠만을 불러오도록 해야 했습니다. 어떤 방법으로 이 기능을 구현할 수 있을까 고민하다가, 쿼리 스트링이 떠올랐습니다.</p>
<pre><code class="language-ts">const updateURL = (newTab: string, newFilters: string[], newSort: string) =&gt; {
    const params = new URLSearchParams();

    if (newTab) params.set(&#39;tab&#39;, newTab);
    if (newFilters.length &gt; 0) params.set(&#39;filters&#39;, newFilters.join(&#39;,&#39;));
    if (newSort) params.set(&#39;sort&#39;, newSort);

    router.push(`${pathname}?${params.toString()}`);
};</code></pre>
<p>필터를 토글할 때마다 호출되는 <code>updateURL</code>은 선택된 필터와 정렬 옵션을 기반으로 URL을 업데이트하는 함수입니다. Next.js의 <code>router.push</code> 덕분에, 페이지를 새로고침하지 않고도 URL을 업데이트할 수 있었어요. 이제 페이지에서 URL을 기반으로 Supabase에 쿼리를 보낸 뒤 데이터를 가져와 처리해야 했는데, 이는 <code>searchParams</code>를 사용해 구현했습니다. 아래와 같이 페이지에서 searchParams를 받아오고, 필터와 정렬 옵션을 추출해 데이터 페칭 함수에 전달하는 방식입니다.</p>
<pre><code class="language-tsx">export default async function Page({ searchParams }: PageProps) {
    const filters = searchParams.filters?.split(&#39;,&#39;).filter(Boolean) || [];
    const sort = searchParams.sort || &#39;관련도순&#39;;

    const { data: activitiesContests, error } =
        await fetchActivityContestAbstractWith({
            filters,
            sort,
            mainCategory: &#39;대외활동&#39;,
        });

    if (error) {
        // 에러 핸들링 로직
    }

    return &lt;Activity activitiesContests={activitiesContests || []} /&gt;;
}</code></pre>
<h2 id="아쉬웠던-부분들">아쉬웠던 부분들</h2>
<p>토마토들은 그렇게 마무리됐지만, 아쉬운 부분들이 남아있었습니다.</p>
<h3 id="부실한-디자인">부실한 디자인</h3>
<p>기업으로부터 디자인을 제공받았을 때, 디자인 시스템이 준비되어있다는 초기 프로젝트 설명회의 말과는 달리 폰트와 공통 컴포넌트 몇 개 정도가 끝이었습니다. 페이지 와이어프레임도 아예 제공되지 않은 부분들이 많았고(개발자의 상상력을 믿고 있었는지), 디자인에 일관적인 부분이 적었어요.</p>
<h3 id="없는-기능명세와-데이터">없는 기능명세와 데이터</h3>
<p>기능명세도 아예 제공되지 않았습니다. 물론 주어진 와이어프레임을 보고 소위 말하는 &#39;알잘딱깔센&#39; 을 발휘해서 만들수도 있었겠지만, 제가 지원한 과정은 ‘교육 프로그램’ 과정이었고, 그래서 최소한의 융통성을 기대했어요. 데이터도 전혀 제공되지 않아 직접 크롤링을 진행했습니다. 더욱이 같은 기업의 다른 프로젝트에는 제공된 디자인 시스템과 유저 플로우 등이 저희 프로젝트에는 존재하지 않아서, 그 부재가 더욱 크게 느껴졌던 것 같습니다.</p>
<h3 id="원활하지-못했던-소통-환경">원활하지 못했던 소통 환경</h3>
<p>기업과의 소통도 그렇게 원할하지는 않았습니다. 각 팀별 멘토는 뒤늦게 배정됐고, 피드백도 빠르게 받아볼 수 없었어요. 물론 연락을 하자마자 바로 답변을 준다거나, 꼭두새벽부터 답변을 바랐던 것은 아니었습니다. 그러나 통상 걸리는 피드백 시간보다도 조금은 더 길었던 것 같아요. 디자인과 관련해서도, 디자이너와 직접적인 소통을 할 수 있었다면 퍼블리싱 과정이 더 원활했을 것이라고 생각합니다.</p>
<h3 id="완벽한-상황은-없다">완벽한 상황은 없다</h3>
<p>그렇다고 일방적으로 한쪽 탓만 할 수는 없었습니다. 실무에서는 이런 경우가 비일비재하다고들 하기도 하고(놀랍게도 이보다 더한 경우도 있다고도 하고요), 저희 팀도 어쨌든 주어진 시간 안에 프로젝트를 어떻게든 완성해 결과물을 구현해야 했기 때문입니다. 그래서 개발 단계에서 아쉬운 부분들도 있었습니다.</p>
<p>어떤 상황에서도 유연하게 대처하는 개발을 위해서, 프로젝트 초기에 시간을 좀 더 들여 보다 체계적인 계획과 설계를 만들었다면 프로젝트 중간에 고통받는 일이 없었을 것 같다는 생각이 들었습니다. 하지만 일단 어떻게든 만들고 보자는 마음에 제대로 준비를 하지 못한 채 개발을 시작했고, 그러다보니 계속해서 의견이 바뀌는 바람에 같은 컴포넌트나 페이지를 몇번이고 다시 수정해야 했던 순간들이 있었어요. <strong>최소한 기능 명세라도 만들고 시작했어야 했다</strong>는 생각이 머리를 스쳤습니다.</p>
<h3 id="제대로-활용하지-못했던-기술스택">제대로 활용하지 못했던 기술스택</h3>
<p>그래서 이런저런 상황에 임기응변식으로 대처하다 보니, 일정에 차질이 생기기 시작했습니다. 결국 호기롭게 시작했던 프로젝트는 어느새 <strong>이것만이라도 제대로 구현해보자</strong>는 상태로 진행됐고, 결국 프로젝트에서 100% 활용해 보고 싶었던 것들을 활용하지 못했습니다.</p>
<ul>
<li>제대로 활용하지 못한 Next.js의 장점</li>
<li>다소 지저분한 코드</li>
<li>임시방편으로 만들었던 데이터 구조와 쿼리</li>
<li>접근성</li>
<li>성능 최적화</li>
</ul>
<p>토마토들 프로젝트와 연금복권의 공통점이라면 ‘뭔가 계속 나온다’라는 겁니다.</p>
<h2 id="나오면서-그렇게-또-성장한다">나오면서: 그렇게 또 성장한다</h2>
<p>그래도 3주라는 기간동안 아무것도 얻어가지 못한 것은 아닙니다. 우선 Next.js를 활용해 본 두 번째 프로젝트이기도 했습니다. 블로그를 만들면서 적용해 본 것과, 토마토들을 만들면서 적용해본 부분들이 서로 달랐기에 상호보완적으로 리팩토링을 진행할 수 있을 것 같아요. 또한 다양한 상황에 대처하기 위해서 좋은 초석을 쌓아야 하는 이유를 몸소 느꼈습니다. 그리고 페이지네이션과 같이 더 재사용성이 높은 컴포넌트를 만들어보는 경험도 가져갈 수 있었고요. 처음 사용해보는 폴더 구조를 적용해 본 것도 있고, 돌아보면 배웠던 점들이 꽤 많을 것 같습니다.</p>
<p>그러나 무엇보다도, 다시 한번 내가 무엇을 모르는지 알게 됐다는 점이 가장 큰 것 같습니다. 스스로 어떤 부분이 부족하고, 어떤 부분을 어떻게 채워나가야 하는지를 조금 더 명확하게 알 수 있었던 3주였어요. 프로그램 기간은 끝났지만 아직 토마토들은 끝나지 않았고, 다음주부터 또 팀원들과 리팩토링 계획에 대해서 이야기해 볼 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA["UX/UI의 10가지 심리학 법칙"을 읽고]]></title>
            <link>https://velog.io/@camille_protocol/UXUI%EC%9D%98-10%EA%B0%80%EC%A7%80-%EC%8B%AC%EB%A6%AC%ED%95%99-%EB%B2%95%EC%B9%99%EC%9D%84-%EC%9D%BD%EA%B3%A0</link>
            <guid>https://velog.io/@camille_protocol/UXUI%EC%9D%98-10%EA%B0%80%EC%A7%80-%EC%8B%AC%EB%A6%AC%ED%95%99-%EB%B2%95%EC%B9%99%EC%9D%84-%EC%9D%BD%EA%B3%A0</guid>
            <pubDate>Mon, 16 Jun 2025 13:35:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 <a href="https://hoonieblog.xyz/post/book-laws-of-ux">이전에 운영하던 블로그</a>에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2024년 1월 6일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서">들어가면서</h2>
<p>저는 프론트엔드 개발자를 꿈꾸지만, 놀랍게도 디자인에 대해서는 전혀 조예가 없습니다. 더 엄밀히 이야기하자면, 디자인에 대한 조예가 아니라 예술적 감각이 전혀 없다고 봐도 무방합니다. 저는 아직도 어떤 색들이 서로 보색 관계를 이루고 있는지 모를 뿐더러(사실 이건 미술시간에 졸아서 그런 것 같습니다), 클론 코딩 강의의 랜딩 페이지를 제가 생각하기에 예쁜 색과 디자인으로 변경한 뒤 지인분에게 ‘눈이 썩을 것 같다’는 평가를 들었습니다.</p>
<p>물론 디자이너와 개발자라는 직군이 엄연히 나누어져 있는 만큼 제가 전문적인 디자이너 수준으로 디자인을 하게 되진 않겠지만(그러나 풍문으로 듣기에는 겸업을 하는 경우도 많은 모양입니다), 적어도 디자인을 보고 ‘이런 의도로 디자인했구나’같은 생각은 들 수 있게끔 어느 정도의 지식을 가지고 싶었습니다. 또한 작은 프로젝트를 위해서 UI 디자인을 결정해야 할 때 뛰어나지는 않더라도 최악은 아닌 UI를 골라낼 수 있는 안목을 키우고 싶었습니다.</p>
<p>그래서 예전에 구매하고 책장에서 먼지가 쌓여가는 책을 하나 꺼냈습니다. 입문하기에 이만큼 좋은 책은 없는 것 같았거든요. <strong>절대로 책 두께가 얇아서</strong> 선택한 것은 아닙니다. 바로,</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/1c9033ed-4426-4dfd-a2fa-7917e93dfdfa/image.webp" alt="Laws Of UX Cover"></p>
<p>이 책입니다. 200쪽도 채 되지 않는 책에, 저를 놀라게 할 만한 내용이 매 쪽마다 담겨 있었습니다. 너무 가슴이 떨려서 글을 쓰는 지금도 청심환을 먹는 중입니다. 아, 이건 에너지 드링크를 너무 마셔서 그렇군요. 아무튼, 이 책이 탄생한 계기는 이렇다고 합니다.</p>
<blockquote>
<p>저자인 존 야블론스키(Jon Yablonski)씨는 디자이너로 일하던 중 가장 힘든 시기를 보내던 중이었습니다. 한 브랜드의 클라이언트 프로젝트를 진행하고 있었는데, 다른 프로젝트와 사뭇 다른 점이 있었답니다.</p>
<p>바로 지지할 데이터가 아직 마련되지 않은 상황에서 이해관계자들에게 디자인 결정의 근거를 제시해야 한다는 것이었습니다. 정량적, 정성적 데이터가 있었다면 쉬웠겠지만, 그렇지 않은 상황에서 이러한 근거를 제시하기란 어려운 일이었습니다.</p>
<p>그래서 야블론스키 씨는 심리학을 선택했습니다. 행동심리학부터 인지심리학까지, 자신의 근거를 뒷받침할 자료들을 모으기 위해 많은 공부를 했고 그 끝에 이해관계자들을 설득할 수 있었다고 합니다.</p>
</blockquote>
<p>그런데 온라인상에서 디자이너에게 도움을 줄 수 있는 양질의 자료를 찾기 어려웠기 때문에, 야블론스키 씨는 본인이 이러한 자료를 만들기로 했습니다. 그렇게 탄생한 웹사이트가 바로 <a href="https://lawsofux.com/">Laws of UX</a>입니다. 글을 쓰면서 확인해 보니 웹사이트에는 21개의 규칙이 있었는데, 이 책에서는 그 중 10가지의 법칙을 다루고 있습니다. 그럼 하나씩 살펴보고, 책을 읽으면서 느낀 점을 이야기해 보고자 합니다.</p>
<h2 id="10가지-심리학-법칙들">10가지 심리학 법칙들</h2>
<h3 id="제이콥의-법칙">제이콥의 법칙</h3>
<p><code>제이콥의 법칙</code>이란, 사용자는 <strong>다른 웹사이트를 통해 축적된 경험을 바탕으로 디자인 관례에 대한 기대치를 형성하는 경향을 보인다</strong>는 내용의 법칙입니다. 이렇게 축적된 경험은 곧 사용자의 멘탈 모델이 되고, 사용자가 새롭게 접하는 웹사이트가 기존에 사용했던 웹사이트와 유사하다면 기대치와 멘탈 모델이 조화를 일으켜 서비스를 원할하게 이용할 수 있습니다. 결국 저희는 이런 익숙함을 공략해야 한다는 내용입니다. 굳이 예시를 들지 않아도, 저희가 늘 사용하는 웹사이트를 살펴보면 비슷한 디자인을 가진 것들이 참 많습니다. 동영상 스트리밍 서비스나 OTT 플랫폼 등을 떠올려 보세요. 무언가 공통적인 디자인이 머리속에 그려지지 않나요?</p>
<p>그렇다고 저자가 모든 웹사이트와 애플리케이션의 동작 방식을 반드시 동일하게 만들어야 한다고 주장하는 것은 아닙니다. 다만 무조건 완전히 새로운 것을 만들기 보다는, 사용자가 어떤 서비스를 사용할 때 그 사용법을 매번 공부하지 않고도 원할하게 사용할 수 있어야 하기 때문에, 기존의 멘탈 모델을 반영한 일반적인 관례도 생각해 볼 필요가 있다는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/74fe5473-e5e7-4fba-8bb2-d9d0821df1b1/image.webp" alt=""></p>
<blockquote>
<p>사진: 사용자의 멘탈 모델(좌)와 기대치(우)가 조화를 일으키는 모습이다(아닙니다)</p>
</blockquote>
<h3 id="피츠의-법칙">피츠의 법칙</h3>
<p><code>피츠의 법칙</code>은 사용자가 <strong>대상을 사용하기까지 걸리는 시간이 대상의 크기 및 대상까지의 거리와 연관이 있다</strong>는 내용의 법칙입니다. 이게 무슨 이야기냐구요? 버튼을 예시로 들어볼게요. 만약 저희가 어떤 버튼을 클릭(혹은 터치)해야 한다고 했을 때,</p>
<ul>
<li>버튼(대상)의 크기</li>
<li>버튼(대상)까지의 거리</li>
</ul>
<p>이 두 가지가 우리가 버튼을 사용하기까지 걸리는 시간과 연관이 있다는 내용입니다. 이때 만약 버튼이 너무 작거나, 화면의 이전 동작을 수행한 위치에서 다음 동작을 수행하기 위한 버튼이 너무 멀리 떨어져 있다면 시간이 증가할 것입니다. 혹은 버튼이 독립적으로 구분되지 않을 정도로 가까이 붙어 있다던가 하는 경우에도 비슷한 문제가 발생할 수 있겠네요. 따라서 상호작용 가능한 요소의 크기를 적절하게 설정하고, 요소 사이의 간격을 잘 조절해야 한다는 것을 알 수 있었습니다.</p>
<p>그런데 이 ‘적절하게’, 그리고 ‘잘’이라는 표현이 조금 애매하지 않나요? 다행히, 이미 정해진 기준을 저희는 쉽게 찾아볼 수 있습니다(석박사들이 머리를 맞대고 만들었어요). 대표적으로 애플의 <a href="https://developer.apple.com/design/human-interface-guidelines">HIG(Human Interface Guidelines)</a>와 구글의 <a href="https://m3.material.io/">Material Design</a>이 이를 잘 설명하고 있답니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/120d280c-eda1-418d-a0d8-50613b7fcf90/image.webp" alt=""></p>
<blockquote>
<p>만약 x를 누르려다가 팔로우를 눌렀는데 그게 전 애인 계정이라면? 유감입니다.</p>
</blockquote>
<h3 id="힉의-법칙">힉의 법칙</h3>
<p><code>힉의 법칙</code>은 <strong>의사결정에 걸리는 시간이 선택지의 개수와 복잡성에 비례해서 늘어난다</strong>는 법칙입니다. 서비스의 인터페이스가 너무 많아지면 사용자의 목표 달성에 방해가 된다는 것인데요, 사실 저는 이걸 ‘점심 메뉴 선택’의 법칙이라고 부르고 싶네요. 식당에 들어갔을 때 점심 메뉴가 너무 많다면 오히려 결정에 방해가 되는 것과 같은 현상입니다. 이래서 뷔페를 가야 하나 봅니다. 뷔페는 입에 들어가는 만큼 먹을 수 있거든요.
아무튼, 이러한 힉의 법칙과 같이 등장하는 개념이 <strong>인지 부하</strong>(Cognitive Load)인데, 쉽게 말해 정신적 자원의 양입니다. 인터페이스에 요소가 너무 많거나 취해야 하는 동작이 명확하지 않을수록 많은 인지 부하가 걸리게 됩니다. 결국 이러한 인지 부하는 복잡한 선택지를 단순화 하는 방법을 통해서 줄일 수 있습니다. 하지만 너무 추상화하지 않도록 하세요. 사용자가 의미를 이해하지 못할 수도 있습니다!</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/d24b3033-2ce4-4158-8540-984f450ce2b3/image.webp" alt=""></p>
<blockquote>
<p>극한의 인지 부하가 단시간에 걸리면 어떻게 되는지 궁금하신 분들은 &#39;인디아나 존스: 크리스탈 해골의 왕국&#39;을 시청하세요.</p>
</blockquote>
<h3 id="밀러의-법칙">밀러의 법칙</h3>
<p><code>밀러의 법칙</code>은 <strong>기억 범위에 더 큰 영향을 미치는 것은 정보의 기본 단위인 비트(bit)의 양이 아니라, 정보 덩어리(chunk)의 개수</strong>라는 것입니다. 책에서는 이 법칙의 대표적인 예시로 전화번호를 들고 있습니다. 가령, 01012345678과 같은 번호보다는 010-1234-5678과 같이 구분된 번호가 더 기억에 잘 남는다는 것입니다. 마찬가지로, 웹사이트의 카테고리나 텍스트 등도 이러한 덩어리화를 잘 활용하면 사용자들이 정보를 더 쉽게 처리하고 이해할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/27ae598f-de6d-4013-853b-c246832dbd77/image.webp" alt=""></p>
<blockquote>
<p>하지만 이 번호는 너무 길군요.</p>
</blockquote>
<h3 id="포스텔의-법칙">포스텔의 법칙</h3>
<p><code>포스텔의 법칙</code>은 <strong>자신이 행하는 일은 엄격하게, 남의 것을 받아들일 때는 너그럽게</strong>라는 말 아래, 서비스 제공자는 누구에게나 정상 작동하는 인터페이스를 제공하며, 동시에 사용자의 입력은 다양하게 고려해야 한다는 것을 강조하고 있습니다.
5장의 내용은 한마디로 요약하자면 ‘예측할 수 없는 결과에 대비하라’라고 할 수 있겠네요. 저만 해도 작은 프로젝트를 만들면서 몸소 느꼈지만, 사용자들은 정말 예측할 수 없는 방향으로 서비스를 사용합니다. ‘비밀번호는 영소문자, 숫자 그리고 특수 기호를 사용해야 합니다’라고 적어 놔도, 이모지를 입력하는 사용자는 분명히 있을 겁니다. 그렇기 때문에 사용자를 더욱 너그럽게 고려해야 한다는 것이지요.</p>
<p>또 사용자가 서비스를 다양한 환경에서 사용한다면 어떨까요? 예전에는 사용자가 PC로만 웹 서비스를 사용했다면, 지금은 PC, 스마트폰, 태블릿 등(심지어 콘솔 게임기에서도!) 다양한 환경에서 웹 서비스를 사용합니다. 이러한 배경 속에 반응형 디자인과 같은 새로운 개념들이 탄생하기도 했어요. 그렇기 때문에 포스텔의 법칙을 잘 적용하면, 사용자의 부담이 줄어들어 향상된 UX를 가진 서비스를 완성할 수 있을 겁니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/192eeb90-6ff2-456b-b9b2-51db7caa57b9/image.webp" alt=""></p>
<blockquote>
<p>사용자를 고려하는 것은 마치 두더지 게임을 하는 것과 같습니다. 차이점이 있다면 사용자는 때려도 점수가 올라가지 않습니다. 대신 때린 사람 형량이 올라가겠지요.</p>
</blockquote>
<h3 id="피크-엔드-법칙">피크 엔드 법칙</h3>
<p><code>피크 엔드 법칙</code>은 <strong>인간은 과거 사건을 떠올릴 때, 경험 전반을 고려하기보다 감정적으로 절정에 이른 순간과 마지막 순간에 집중하는 경향이 있다</strong>는 현상을 다룹니다. 저자는 이때 사람의 판단에 관여하는 사고나 이성에 발생하는 계통 오차인 인지 편향을 이해한다면 도움이 될 것이라고 말합니다. 이 인지 편향의 종류 중 하나인 기억 편향(memory bias)이 바로 피크 엔드 법칙입니다. 결국 사용자가 서비스에 대해서 전반적으로 좋은 경험을 가져가게 하려면, 감정적 절정의 순간을 잘 공략해야 한다는 것이지요.
책에서는 우버를 그 예시로 들고 있는데, 비슷한 서비스인 카카오 T를 생각해 볼까요? 택시를 호출하고 택시가 도착할 때까지 아무런 피드백이 없다면 사용자는 해당 서비스를 사용하는 과정을 굉장히 지루하게 느끼게 될 것입니다. 그래서 택시의 도착 과정을 애니메이션으로 표현했어요. 이를 통해서 사용자는 택시에 탄다는 목표를 향해 조금씩 다가간다는 느낌을 받게 될 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/c2a17d4d-77b5-437e-ae62-bfaa781dd84a/image.webp" alt=""></p>
<blockquote>
<p>그런데 지금은 택시 말고 다른 게 오고 있는 것 같습니다.</p>
</blockquote>
<h3 id="심미적-사용성-효과">심미적 사용성 효과</h3>
<p><code>심미적 사용성 효과</code>는 <strong>디자인이 아름다우면 긍정적인 감정 반응이 일어나고, 인지 능력이 향상되며 사용하기 편하다는 인식과 함께 제품(서비스)에 대한 신뢰도도 높아지는 현상</strong>을 다루고 있습니다. 책에서 소개된 연구 결과에 따르면, 사용자가 웹사이트에 가지는 첫 인상을 판단하는 데에는 0.05초 정도가 걸리며, 이렇게 결정된 첫 인상은 사용자 경험 전반에 영향을 미친다고 합니다. 역시 첫인상이 중요하네요.</p>
<p>저는 현 시대에 이 분야의 끝판왕은 개인적으로 애플이라고 생각합니다. 산업 디자인과 UI 디자인 모두 미려한 매력을 가지고 있어 그 팬층도 더 두터운 것 같아요. 저자가 이야기하는 바와 같이 물론 이러한 애플의 디자인에 문제가 전혀 없진 않지만, 미학적으로 아름다운 디자인은 사용자들이 이러한 사용성 문제를 어느 정도 눈감아 주도록 합니다(사실 여전히 불편한 것들이 몇 가지 있긴 합니다).
그렇다고 예쁘다고 다 덮어놓고 가는 것은 또 지양해야 합니다. 저자는 이러한 점에 대해서, 문제 식별이 중요한 사용성 테스트에서마저 심미적 사용성 효과가 발생하면 큰 문제가 될 수 있다고 경고합니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/708c8231-4d67-4e6c-b125-4be1170aa0d8/image.webp" alt=""></p>
<blockquote>
<p>심미적 사용성 효과의 맹점을 잘 파악한 친구구나!</p>
</blockquote>
<h3 id="폰-레스토프-효과">폰 레스토프 효과</h3>
<p><code>폰 레스토프 효과</code>는 인간이 <strong>다른 요소와 시각적으로나 개념적으로 분리된 항목을 더 잘 기억한다</strong>는 것에 대해서 설명합니다. 밀러의 법칙에서 이야기 한 기억 용량에 대해서 기억하고 계신가요? 이와 마찬가지로 집중력 역시 용량과 시간에 대해 제한을 가지고 있습니다.</p>
<p>이러한 폰 레스토프 효과의 예시는 대부분의 서비스에서 찾아볼 수 있는데, 이는 서비스의 인터페이스가 사용자가 집중할 곳을 안내하거나, 당황하거나 산만해지는 일을 방지하는 등의 역할을 수행하기 때문입니다. 예를 들어 소셜 서비스에서 댓글을 삭제할 때 나타나는 모달(Modal) 등이 대표적인 예시입니다. 물론 가끔가다 엉뚱한 버튼을 강조해서 사용자들의 혼을 쏙 빼놓는 경우도 있지만요.</p>
<p>나아가 저자는 이러한 폰 레스토프 효과의 주의점도 이야기하고 있는데, 너무 많은 것을 강조하거나 장애를 가진 사람들 및 그러한 환경을 고려하지 않고 인터페이스를 디자인하는 것은 지양해야 한다고 말합니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/07b78a8b-c5a0-454f-8b9a-0223b10191e8/image.webp" alt=""></p>
<blockquote>
<p>경고: 제발 버튼 가지고 월리를 찾아라 하지 마세요</p>
</blockquote>
<h3 id="테슬러의-법칙">테슬러의 법칙</h3>
<p><code>테슬러의 법칙</code>은 <strong>모든 시스템에는 더 이상 줄일 수 없는 복잡성이 존재한다</strong>고 이야기합니다. 결국 이렇게 남겨진 최소한의 복잡성은 개발(디자인)과정이나 사용자 인터렉션 단계 중 어느 쪽에서든 처리해야 합니다. 9장에서는 이에 대한 예시로 Gmail의 스마트 편지쓰기와 스마트 답장 기능을 소개합니다. 두 기능 모두 사용자의 수고를 덜어주고 시간을 절약하게 해서 서비스의 복잡성을 처리하는데 도움을 주고 있다고 볼 수 있겠네요.</p>
<p>하지만 반면에, 저자는 이렇게 단순성을 추구할 때 과도하게 추상화하지 않는 것 역시 중요하다고 이야기합니다. 너무 단순화되어 그 의미를 알아볼 수 없는 아이콘과 같이 말이지요. 저는 아직도 메시지 전송 버튼이 종이 비행기로 표현된 것이 적절한지에 대한 의문이 가끔 듭니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/7903e648-1ae2-4afa-a899-0bb5182ed957/image.webp" alt=""></p>
<blockquote>
<p>사용자의 수고를 덜어 원시인으로 만들어 주는 자동완성 기능도 존재하는 것 같군요</p>
</blockquote>
<h3 id="도허티-임계">도허티 임계</h3>
<p><code>도허티 임계</code>는 <strong>컴퓨터의 반응 시간이 생산성에 불균형을 미친다</strong>는 도허티의 발견을 토대로 세워진 임계값입니다. 기존에는 PC가 작업을 수행하는 반응 시간의 임계값이 2초라고 여겨졌으나, 도허티의 논문에 따라 생산성 반응 시간 감소의 정비례로 증가할 때 이것이 성립하는 시간의 임계값인 0.4초가 되었다고 합니다.</p>
<p>이에 맞춰서 서비스의 처리 시간을 0.4초까지 줄일 수 있다면 좋겠지만, 그 이상이어도 개선의 방법이 없다면 다른 방법을 사용해야 합니다. 예를 들면 미리 컨텐츠의 뼈대를 표시하는 뼈대 화면(skeleton screen)이나, 미리 이미지를 작게 로딩하고 로딩이 완료되면 크게 확대해 원본을 표시하는 블러 업(blur up)효과 등이 있습니다. 이런 효과를 적용할 경우 사용자는 요청한 작업이 진행되고 있다는 사실을 알 수 있고, 사용자가 기다리는 동안 볼거리를 제공받을 수 있다는 장점이 있습니다. 결국 서비스의 성능은 기술적인 문제일 뿐만 아니라 본질적인 디자인 요소이기도 합니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/e6ff2331-3d06-4f79-94d2-3b98a74e3ad1/image.webp" alt=""></p>
<blockquote>
<p>Q. 안경을 쓰고 마스크를 착용하면 김이 서립니다. 이것도 블러 업 효과인가요?</p>
<p>A. 헛소리 그만 하고 안경을 닦으세요.</p>
</blockquote>
<h2 id="나오면서">나오면서</h2>
<p>이렇게 10가지의 심리학 법칙을 간단하게 알아봤습니다. 사실 책을 읽으면서 두 번 놀랐는데, 첫 번째는 책에서 예시로 든 UI/UX가 제가 이미 평소에 웹 서비스를 사용하면서 봐 왔거나 경험한 것이라는 사실입니다. 오히려 책을 읽기 전에는 예시로 어려운 내용이 나오지 않을까 걱정했었는데, 이러한 예시 덕분에 책에서 소개하는 심리학 법칙들을 보다 쉽게 이해할 수 있었습니다.</p>
<p>두 번째는 제가 평소에 사용하는 웹 서비스에 이렇게 인간의 심리에 대한 깊은 탐구가 녹여져 있다는 점이었습니다. 좋은 서비스와 나쁜 서비스를 사용하면서 느꼈던 편안함과 불편함이 모두 이러한 인간의 사고와 심리에 기반을 두고 있다는 사실이 놀라울 따름입니다.</p>
<p>이후 11장에서, 저자는 지금까지 소개한 심리학 법칙을 바탕으로 보다 책임감있는 서비스를 만드는 것이 왜 중요한지, 그리고 그렇게 하기 위해서는 어떤 자세를 가져야 하는지를 또 한번 심리학 이론과 함께 설명합니다. 사실 지금까지의 내용만큼 11장의 내용도 저에게는 꽤나 인상깊게 다가왔습니다. 저자는 인간의 행동과 사고를 고찰한 심리학 이론이 역으로 인간에게 어떤 영향을 미칠 수 있는지와 함께, 기술이 비록 그 의도가 선하더라도 의도치 않은 부작용을 낳을 수 있다는 점을 설명하며 디지털 윤리를 강조합니다. 단적인 예로 소셜 서비스의 부작용을 언급하기도 합니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/a18bfa37-8d37-49b3-bc5d-f8ace5147f1c/image.webp" alt=""></p>
<p>11장을 읽으면서 넷플릭스 다큐멘터리 소셜 딜레마(the social dilemma, 2020)가 생각나기도 했습니다. 소셜 미디어가 인류에게 미친 영향과, 무분별한 소셜 미디어 사용을 비판하는 전 소셜 서비스 직원들의 인터뷰를 담은 내용입니다. 오랜만에 한번 다시 봐야겠다는 생각이 들었습니다.</p>
<blockquote>
<p>&quot;윤리적인 디자인 결정을 내리는 첫 단계는 인간의 마음이 부당하게 이용될 수도 있다는 사실을 인정하는 것에서 시작한다. 우리는 자신이 만드는 기술에 책임을 지고 그러한 기술이 사람들의 시간이나 관심, 그리고 디지털 생활 전반의 균형에 해가 되지 않게 해야 한다.</p>
<p>- UX/UI의 10가지 심리학 법칙, p.150&quot;</p>
</blockquote>
<p>책을 마무리하는 12장에서는 이러한 심리학 법칙을 디자인 프로세스에 도입하기 위한 방법을 소개하고 있습니다. 사실 이 부분은 디자이너에게 더 중요한 내용이 될 것 같습니다. 물론 개발자가 알아서는 안될 내용이란 건 아닙니다.</p>
<p>이렇게 UX/UI에 대한 짧은 여정을 마쳤습니다. 여전히 더 나은 UX/UI를 만들기 위해 고민해야 할 부분은 많지만, 그래도 책을 통해 조금이나마 알게 되어 기쁩니다. 이 여정을 통해 배운 지식이 언젠가 잘 쓰일 날이 오기를 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2023년 연간 회고]]></title>
            <link>https://velog.io/@camille_protocol/2023%EB%85%84-%EC%97%B0%EA%B0%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@camille_protocol/2023%EB%85%84-%EC%97%B0%EA%B0%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 16 Jun 2025 13:24:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 <a href="https://hoonieblog.xyz/post/retro-2023">이전에 운영하던 블로그</a>에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2023년 11월 11일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>한동안 블로그 글이 뜸했습니다. 그동안 여러 일들이 있었고, 바쁘다는 핑계로 회고를 거의 기록하지 못했습니다. 연말이 다가오면서 지금까지의 제 모습을 솔직하게 돌아보고자 합니다. 개발자를 꿈꿨던 그 시절부터 지금까지 어떤 삶을 살아왔고, 무엇이 잘못됐으며, 앞으로 어떻게 해 나가야 할지를 돌아보는 시간을 갖고자 합니다.</p>
<h2 id="개발자가-되고-싶다고-생각했던-그-순간부터">개발자가 되고 싶다고 생각했던 그 순간부터</h2>
<p>어릴 때부터 기술에 관심이 많았습니다. 정확히 말하자면, 기술에 관심<strong>만</strong> 많았다고 이야기하는 것이 맞을 것 같습니다. 새로운 전자기기나 소프트웨어를 알게 되면 친구들에게 자랑하기 일쑤였고, 잡지나 TV를 통해서 정보들을 조금씩 찾아나갔습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/bbb196c2-3911-42c7-b656-b6713f1bb5d5/image.webp" alt="Uplink Screenshot"></p>
<p>개발과의 첫 인연은 &#39;업링크&#39;라는 게임이었습니다. 해커가 되어 은행의 돈을 훔치거나, 주요 국가 시설을 사보타주하는 게임이었는데, 그때는 해커가 되고 싶다고 생각했습니다. 짧은 지식으로 어떻게 해커가 될 수 있는지 찾아보고, 간단하게 OS를 설치해 보거나 다양한 툴들을 만져 보곤 했습니다. &quot;해커가 주인공인 게임을 하는 것&quot;과 &quot;해커가 되어서 활동하는 것&quot; 사이에는 큰 간극이 존재하지만, 당시에는 컴퓨터를 잘 하면 이런 멋진 사람이 될 수 있다고 생각했습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/8b2e3b79-5212-483e-a642-ed801822162f/image.webp" alt="Kali Linux"></p>
<p>처음 설치했던 운영체제는 데비안 계열의 &#39;칼리 리눅스&#39;였습니다. 설치만 할 줄 알았고 사용 방법은 몰랐기 때문에, 이런 운영체제들을 설치할 때마다 롤백하는 것은 늘 컴퓨터 기사님의 몫이었습니다. 우분투나 CentOS 같은 운영체제도 설치해 봤지만, 설치만 하고 깊게 공부하지는 않았습니다. 솔직히 말하면, &quot;남들이 잘 모르는 걸 알면서 그걸 멋있어하는 자신&quot;의 모습에 취해 있었습니다.</p>
<p>본격적으로 컴퓨터에 대해 공부하기 시작한 것은 대학교 1학년 때였습니다. C언어를 배우게 되었고, 이때의 경험을 기반으로 개발자가 되어야겠다는 생각이 들었습니다. 돌이켜보면, 제가 가진 개발에 대한 재능은 미미했습니다. 포인터 개념에서 좌절했고, 결국 재수강을 통해서야 C언어의 기본을 이해했습니다.</p>
<p>이후에도 명확한 목표 없이 파이썬, 웹 개발(HTML/CSS), 데이터 분석을 위한 R, 앱 개발을 위한 스위프트 등 다양한 기술을 맛보기식으로 접했습니다. 그렇게 이것저것 간만 보다가 4년이라는 시간이 흘렀고, 2023년을 맞이하게 되었습니다.</p>
<h2 id="2023년의-여정">2023년의 여정</h2>
<p>그렇게 뭐 하나 제대로 할 줄 모르는 상태로 2023년을 맞이했습니다. 가진 것이라고는 어떻게든 따라가면서 얻어낸 학사 학위뿐이었습니다. 이대로 있다가는 백수 신세를 면치 못할 것이 뻔했고, 정말 흘러가는 대로 살다가 죽을 것 같았기에 다시 한번 열정을 불태워 보려 했습니다. 그렇게 부트캠프에 들어가게 되었습니다.</p>
<h3 id="멋사와-함께한-상반기">멋사와 함께한 상반기</h3>
<p>부트캠프를 한 번도 경험해 본 적이 없었기에 부트캠프에 대한 환상도 있었고, 천성이 스스로 무언가를 하기보다는 다른 누군가와 함께 하는 것을 좋아했기 때문에 부트캠프를 선택했습니다. 처음으로 자기소개서도 작성하고, 아이패드로 자기소개 영상도 찍어보았습니다. 자기소개서에 대한 확신도 없었고, 기초적인 개념 테스트조차도 수월하게 해내지 못했지만, 다행히 합격 메일을 받게 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/1f8f2b22-8ed0-46e1-ba91-5be74cd52aa1/image.webp" alt="Likelion Mail"></p>
<p>4개월간의 프론트엔드 부트캠프였는데, 시작은 순조로웠습니다. 연초의 새로운 마음가짐과 함께 열심히 하겠다는 의지가 있었고, 함께 하는 동료들이 모두 열정에 가득 차 있어서 즐겁게 그리고 열심히 했습니다. 스터디에도 참여하고, 필수가 아니었던 특강도 꼬박꼬박 챙겨 들으며 열심히 달렸습니다.</p>
<p>하지만 시간이 지날수록 스스로 나태해졌다는 사실을 깨달았습니다. 열심히 공부하고자 들어갔던 스터디에서는 공부에 집중하기보다 사람들과 만나서 시간을 보내는 자신을 발견했고, 필수 과제 외의 선택 과제는 점점 등한시하기 시작했습니다. 최종 팀 프로젝트를 시작하기 전에는 나태함이 극에 달해, 수업 시간에 줌만 켜 놓고 유튜브를 보거나 다른 짓을 하기 시작했습니다. &#39;어차피 노션에 있는 교안 보고 복습하면 되겠지&#39;, &#39;내가 그렇게 못하는 것도 아닌데&#39;라는 생각들이 점점 머릿속을 채워갔습니다.</p>
<p>최종 팀 프로젝트가 시작하기 직전에 겨우 정신을 차렸습니다. 물론 이것마저도 잠깐의 각성에 불과했습니다. 정신을 차린 이유는 단순히 팀 프로젝트에서 빌런이 되고 싶지 않았기 때문이었습니다.</p>
<p>한 달 동안 열심히 프로젝트를 진행했습니다. 주어진 디자인과 서버 API를 기반으로 서비스를 구현하는 프로젝트였는데, 이 한 달 동안은 다시 어떻게든 해내야겠다는 생각만으로 부딪히며 울며 겨자 먹기로 프로젝트를 완수했습니다. 거의 3주 정도는 평소에 자던 시간의 두 배 가까이 줄여가며 프로젝트를 했지만, 그런 모습이 자랑스럽거나 떳떳하지는 않았습니다. 열심히 한다, 잘 한다의 척도가 밤을 새는 것이 아닐뿐더러, 그렇게 밤을 새운 것조차도 프로젝트의 규모가 크거나 어려워서가 아니라 그동안 쌓아뒀던 부채를 해결하느라 그랬기 때문입니다.</p>
<p>프로젝트는 무사히 끝마쳤습니다. 팀원들 모두가 소신이 있고 똑 부러지는 분들이라, 혹시 직설적인 피드백이 오더라도 겸허하게 받아들일 준비가 되어 있었지만, 다행히 다들 긍정적인 평가를 해주었습니다. 4개월간의 부트캠프 여정은 그렇게 끝났습니다. 성공 혹은 실패의 관점에서 이야기하자면, 개인적으로는 실패였습니다. 부트캠프의 질이 아니라, 제 태도가 문제였다고 생각합니다.</p>
<h3 id="붕-떠버린-하반기와-우테코-프리코스">붕 떠버린 하반기와 우테코 프리코스</h3>
<p>부트캠프가 끝나고 최종 팀 프로젝트를 진행했던 팀원들과 리팩토링을 시작했습니다. 코드를 정리하고, 프로젝트 시작 시 도입하지 못했던 기술 스택들을 도입해 보자는 것이 목적이었습니다. 리코일과 리액트 쿼리를 도입하기로 했었는데, 결국 최종적으로 도입한 것은 리액트 쿼리였습니다. 그러나 팀원들 중 유일하게 리액트 쿼리에 대해 제대로 이해하지 못하고 손이 가는 대로 코드를 적용한 것은 저뿐이었습니다. 돌이켜보면, 제가 그만큼 간절하지 않았기 때문이라고 생각합니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/07d7eaee-ee47-48c6-bf2c-dcc93e854800/image.png" alt="Jeju WEB Conference 2023 Poster"><img src="https://velog.velcdn.com/images/camille_protocol/post/6e3d6239-7ab3-439b-b370-d093a4deda04/image.webp" alt=""></p>
<p>새로운 경험을 찾다가 제주도에서 열리는 웹 컨퍼런스에 연사로 참여하게 되었습니다. 웹 접근성을 주제로 발표했는데, 한 달 동안 웹 접근성에 대해 공부하면서 많은 것을 배웠습니다. 하지만 그 이후에도 프론트엔드 개발자가 되겠다는 제 목표는 점점 흐릿해졌습니다.</p>
<p>이후 멋사에서 만난 동료들과 위치 기반 소셜 서비스 사이드 프로젝트를 시작했지만, 이 역시 몰입하지 못했습니다. 회의 때마다 완료하기로 한 계획은 계속 뒤로 미뤄졌고, 프로젝트 시작 한 달 후에 동료에게 이와 관련해 피드백을 받았습니다. 원인은 역시 &quot;간절하지 않았다&quot;입니다.</p>
<p>그렇게 이곳저곳을 기웃거리다가 우아한 테크코스의 프리코스 과정에 참여하게 되었습니다. 우아한 테크코스 프리코스는 4주 동안 주어지는 미션들을 매주 해결하는 과정입니다. 무언가에 진정하게 몰입하고 싶었고, 스스로의 문제 해결 능력을 확인해 보고 싶어서 신청했습니다. 1주 차와 2주 차 미션은 무난하게 진행했지만, 3주 차 미션은 어떻게 해야 할지 감이 잡히지 않았고, 제출 이후 코드 리뷰에서 많은 피드백을 받아 &quot;내가 정말 개발자가 될 수 있을까?&quot;라는 의문이 들었습니다. 이제 4주 차 미션을 앞두고 있는데, 설레는 마음보다는 걱정이 더 앞서고 있습니다.</p>
<h2 id="실속-없는-2023년을-돌아보며">실속 없는 2023년을 돌아보며</h2>
<p>2023년을 이렇게 보내면서, 이러다가는 정말 답이 없겠다는 생각이 들었습니다. 스스로를 돌아보고 무엇이 문제인지 파악하고자 이 회고를 작성하게 되었습니다. 지금이라도 정신을 차리지 않으면 무기력하게 2024년을 맞이할 것 같아서입니다.</p>
<h3 id="돌아보니-발견한-문제점">돌아보니 발견한 문제점</h3>
<p>문제가 정말 많았습니다. 그럼에도 그 문제를 돌아보기보다는 회피하는 데만 급급했습니다. 이번 회고를 통해 돌아본 후, 제 문제를 정리해 보았습니다.</p>
<h4 id="1-나태함">1. 나태함</h4>
<p>그때는 무언가를 열심히 하고 있고, 하루가 눈코 뜰 새 없이 바쁘게 지나간다고 생각했지만, 사실 그렇지 않았습니다. &quot;갓생&quot;을 사시는 분들이 새삼 존경스럽다는 생각이 듭니다. 저는 그렇지 못했고, 의미 없이 보내는 날들이 많았습니다.</p>
<h4 id="2-명확한-목표의-부재">2. 명확한 목표의 부재</h4>
<p>돌이켜보면, 제가 개발자가 되고 싶었던 이유는 단순히 &quot;멋있어 보여서&quot;였습니다. 그 외의 다른 측면에 대해서는 거의 생각해 본 적이 없었습니다. 그러다 보니 공부 중에도 &#39;내가 이걸 왜 하고 있지?&#39;라는 생각이 자주 들었습니다. 목표가 없었기에, 어떤 일을 해도 쉽게 길을 잃고 좌절했습니다.</p>
<h4 id="3-잘못된-공부-방법">3. 잘못된 공부 방법</h4>
<p>개발은 둘째치고, 단순한 기술 블로그 내용을 작성하는 것만으로도 제 공부 방법이 얼마나 잘못됐는지 알 수 있었습니다. 회고를 작성하기 위해 검색하다가 &quot;개발자 공부, 어떻게 해야될까요?&quot;라는 글을 보게 되었는데, 최악의 공부 방법 예시에 제 방식이 그대로 적혀 있었습니다.</p>
<h3 id="앞으로의-개선-방향">앞으로의 개선 방향</h3>
<p>문제를 파악했다면 고쳐야 합니다. 그래서 나름 고민해 보았습니다.</p>
<h4 id="1-기록하기">1. 기록하기</h4>
<p>사실 회고를 쓴 첫 번째 이유이기도 합니다. 스스로를 돌아보는 과정을 오늘 처음 겪는 것은 아닙니다. 다만 돌아본 후 해결책을 찾지 않고 그냥 내버려 두었습니다. 자연스레 해결될 줄 알았지만, 잊히기만 했을 뿐 해결되지는 않았습니다. 그래서 기록하기 시작했습니다. 더 자주, 더 꼼꼼하게 기록할수록 더 많이, 더 깊게 돌아볼 수 있을 것입니다.</p>
<h4 id="2-명확한-목표-설정하기">2. 명확한 목표 설정하기</h4>
<p>제가 프론트엔드 개발자가 되고 싶은 이유는 무엇일까요? 예전에는 단순히 &quot;멋있어 보여서&quot;였지만, 이제는 그 &quot;멋있음&quot;을 다르게 해석하고자 합니다.</p>
<p>&quot;프론트엔드 개발자는 여러 기술 역량을 바탕으로 서비스를 개발하고 유지보수하기 위해 지속적으로 공부하며 현실 세계의 문제를 해결합니다. 그리고 저는 현실 세계의 문제를 웹 서비스를 통해 해결하여, 웹을 사용하는 사람들에게 더 나은 경험과 순간들을 제공할 수 있는 멋있는 프론트엔드 개발자가 되고 싶습니다.&quot;</p>
<p>처음에는 거창하다고 생각했습니다. &quot;현실 세계의 문제&quot;라고 하면, 세계 평화나 지구 온난화 같은 거대한 문제를 해결해야 할 것 같았기 때문입니다. 하지만 더 작고 단순한 것들도 현실 세계의 문제가 될 수 있습니다. 간편하게 은행에 가지 않고도 송금하거나, 영화를 예매하는 것, 취미가 같은 사람들이 모여 이야기할 수 있는 공간을 만드는 것, 외출 전 날씨를 확인하는 것 등이 모두 현실 세계의 문제를 웹에서 해결하는 예입니다. 이런 문제를 해결하는 멋있는 프론트엔드 개발자가 되는 것이 제 목표입니다.</p>
<h4 id="3-효과적인-공부-방법-도입하기">3. 효과적인 공부 방법 도입하기</h4>
<p>이를 위해서는 많은 시간을 공부에 투자해야 합니다. 그리고 저는 이 공부를 잘못된 방법으로 하고 있었습니다. 소위 말하는 &#39;학문적 탐구&#39;에만 몰두하고 있었습니다. 그러나 제가 원하는 프론트엔드 개발자가 되기 위해서는 이론을 실전에 적용해야 합니다. 결국 이론과 실전 모두 중요하며, 두 가지를 균형 있게 발전시켜야 합니다. 그래서 제가 가진 지식을 활용할 수 있는 개인 프로젝트를 시작하고자 합니다.</p>
<h2 id="남은-올해의-계획">남은 올해의 계획</h2>
<p>벌써 연말입니다. 날씨는 추워졌고 크리스마스 준비가 한창입니다. 올해의 제 행보는 우테코 프리코스의 결과에 따라 달라질 것 같습니다. 지원서에도 있었던 질문처럼, 우테코와 함께하지 못한다면 어떻게 공부할 것인지 고민해보았습니다.</p>
<p>평소에 주도적으로 공부하는 스타일이 아니었기 때문에, 우테코 합격 여부와 관계없이 스스로 공부하는 방법을 찾아야 합니다. 상식적으로 생각해 볼 때, 합격보다는 불합격 확률이 높기 때문에 그 이후 계획을 세우는 것이 현실적입니다. 또한 언제까지나 외부에 의존해서 공부할 수도 없습니다.</p>
<h3 id="우테코-불합격-시-계획">우테코 불합격 시 계획</h3>
<h4 id="1-진행-중인-사이드-프로젝트-마무리하기">1. 진행 중인 사이드 프로젝트 마무리하기</h4>
<p>먼저 진행 중인 사이드 프로젝트를 마무리하겠습니다. 이는 우테코 합격 여부와 관계없이 완료해야 할 과제입니다. 이 프로젝트에 대한 회고는 이번 달 말부터 기록할 예정입니다.</p>
<h4 id="2-이력서-작성하기">2. 이력서 작성하기</h4>
<p>이력서는 진작 작성했어야 했지만, 계속 미루어왔습니다. 그 이유는 스스로의 역량이 부족하다고 느껴 더 준비해야겠다는 생각 때문이었습니다. 그러나 최근 읽은 책의 한 구절이 마음에 와닿았습니다.</p>
<blockquote>
<p>&quot;시간도 활력도 동기도 충만한 미래의 꿈같은 시간을 기다리다가는 평생 기다리기만 해야 한다. 백번 양보해도, 결국 막판에 해치워야 하는 스트레스의 굴레에서 벗어나지 못할 것이다. 그러니 이제 완벽한 타이밍이 없다는 사실을 인정하고, 완전히 받아들이자.&quot;</p>
<p><strong>- 게으른 완벽주의자를 위한 심리학</strong></p>
</blockquote>
<p>역량 부족을 이유로 이력서 작성을 미룬다면, 결국 &#39;완벽한 타이밍&#39;을 기다리다 기회를 놓치게 될 것입니다. 또한 이력서를 작성하지 않는다면 제가 목표를 향해 얼마나 왔는지, 앞으로 어디로 가야 하는지 확인할 수 없습니다. 면접 준비도 함께 진행해야겠습니다.</p>
<h4 id="3-개인-프로젝트-진행하기">3. 개인 프로젝트 진행하기</h4>
<p>앞서 말한 대로, 학문적 공부와 실전을 균형 있게 발전시키기 위해 제 역량을 실전에 적용할 수 있는 개인 프로젝트를 진행하려 합니다. 프로젝트를 통해 어떤 부분이 부족한지 파악할 수 있을 것입니다. 다만 무엇을 만들지는 아직 고민 중입니다.</p>
<h2 id="마무리-회고를-회고하기">마무리: 회고를 회고하기</h2>
<p>처음으로 작성해 본 연간 회고입니다. 다시 읽어보니 미흡한 부분들이 많습니다. 하지만 글쓰기에 재능이 없다면 노력이라도 해야겠지요. 연간 회고를 작성했다는 것 자체가 의미가 있습니다. 예전의 저였다면 올해도 그냥 흘려보냈을 것입니다.</p>
<p>벌써 겨울이 코앞까지 다가왔는데, 이번 겨울은 제법 오래갈 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TravelUs 회고]]></title>
            <link>https://velog.io/@camille_protocol/TravelUs-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@camille_protocol/TravelUs-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 16 Jun 2025 13:02:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✍️ 블로그 이전 안내
이 글은 <a href="https://hoonieblog.xyz/post/retro-travelus">이전에 운영하던 블로그</a>에 작성된 글입니다. 블로그를 이전하며 모든 기록을 이곳으로 옮겨왔습니다. 글에 포함된 정보는 최초 작성 시점을 기준으로 하고 있으니, 참고해 주세요. 본 게시글은 2023년 9월 30일에 작성됐습니다.</p>
</blockquote>
<h2 id="들어가면서">들어가면서</h2>
<p>멋쟁이사자처럼 프론트엔드 스쿨 5기가 종료된 지 벌써 3개월이라는 시간이 흘렀습니다. 조금 늦었다는 생각도 들었지만, 기록을 남기지 않는 것 보다는 늦게나마 남기는 것이 낫기에 프로젝트의 여정을 담은 글을 작성해 보려고 합니다. 지금부터의 이야기는 이미 옅게 흩어진 제 기억과, 프로젝트를 진행하면서 노션에 남긴 메모를 최대한 참고해 정리한 결과물입니다.</p>
<p><strong>⚠️현재 <a href="https://github.com/FRONTENDSCHOOL5/final-04-fearless4">TravelUs</a> 프로젝트는 유지보수가 되고 있지 않습니다.</strong></p>
<h2 id="프로젝트를-시작하기-전">프로젝트를 시작하기 전</h2>
<p>팀 프로젝트를 제외한 정규 과정이 모두 종료된 후, 스스로 가장 먼저 던졌던 질문은 하나였습니다.</p>
<blockquote>
<p>“지금의 내 실력으로 팀 프로젝트를 무사히 끝마칠 수 있을까?”</p>
</blockquote>
<p>개발자가 되어야겠다는 생각으로 시작했던 부트캠프 초기와는 다르게, 시간이 지날수록 점점 네트워킹에 집중하고 본질을 잊어버리고 있었습니다. 당연히 학습은 미진할 수 밖에 없었고, 매일 찾아오는 새로운 배움에 물음표를 던지기 보다는 이른 마침표를 찍어내곤 했습니다. 결국 프로젝트를 함께 진행할 동료들에게 짐이 되는 것이 두려워지고 말았습니다.</p>
<p>그 두려움이 저를 프로젝트로 이끌었습니다. 오히려 두렵기 때문에 무엇이라도 더 해야겠다는 생각이 들었습니다. 그래서 프로젝트 팀이 정해졌을 때 먼저 자처해서 팀장이 되겠다고 했어요. 책임져야 할 것들이 많아질수록 더 프로젝트에 집중할 수 있다고 생각했고, 또 프로젝트의 전반적인 관리를 해 보고 싶다는 생각에서였습니다. 프로젝트의 일정을 관리하고, 팀원의 원활한 소통을 도우며 동시에 나에게 주어진 기능 구현을 해야 하기도 했습니다.</p>
<h2 id="시작부터-끝까지">시작부터 끝까지</h2>
<h3 id="1주차-프로젝트-계획-설립--디자인">1주차: 프로젝트 계획 설립 &amp; 디자인</h3>
<p>1주차는 프로젝트 계획을 설립하는 시간이었습니다. 함께하게 된 팀원들은 기존의 부트캠프 기간동안 한 번도 이야기를 나눠보지 않은 인원이라 걱정이 앞섰지만, 다행히 빌런은 없었습니다. 모두가 서로의 의견을 존중하며, 때로는 비판적인 의견을 제시할 수 있는 역량을 가지고 있었어요. 덕분에 프로젝트 시작 전에 불필요한 감정 소모를 진행하는 일은 없었습니다. 먼저 각자의 역량에 대해서 솔직하게 공유하기로 했습니다. 팀원을 속이고 프로젝트를 진행하다가 예기치 못한 문제를 마주했을 때 잃는 것이 당장의 부끄러움보다 더 크기 때문이었습니다.</p>
<p>이후에는 팀 규칙을 정했습니다. 정말 세부적인 부분까지는 정해두지 않았지만, 최소한의 틀은 잡아두고자 했습니다. 예를 들면 <strong>회의에 참여하지 못할 경우에는 최소 12시간 전에는 알려달라</strong>나, <strong>이것도 모르냐</strong>와 같은 말은 하지 않는다던가 등이었습니다. 어떻게 보면 당연할 수 있는 내용들이지만, 문서화의 유무에 따라 팀이 어떻게 받아들이는지가 다르기 때문입니다.</p>
<p>프로젝트의 주제도 결정해야 했습니다. 이미 멋사 측에서 백엔드와 기본적인 디자인을 제공했기에 큰 고민을 할 필요는 없었지만, 그래도 조금이나마 우리 팀만의 아이디어로 이루어진 프로젝트를 구현하고 싶었습니다. 소셜 서비스 형태의 오픈 마켓이라는 점을 고려해서, 여행 동행을 구하거나 여행 상품을 판매하는 사람들이 사용하는 서비스라면 어떨까라는 생각이 들었습니다. 그렇게 TravelUS라는 프로젝트가 탄생했어요.</p>
<p>또한 통일성 있고 유지보수가 쉬운 코드를 위한 코드 컨벤션과, 팀의 보다 나은 소통과 협업에 도움이 될 수 있는 커밋 컨벤션도 결정했습니다. 브랜치 전략도 결정했는데, 처음에는 <strong>Git Flow</strong>와 <strong>GitHub Flow</strong> 중에서 여러 이야기가 오갔습니다.</p>
<p><img src="https://velog.velcdn.com/images/camille_protocol/post/e082d201-e0e6-44c9-bce8-4510564939fa/image.webp" alt="GitHub Flow 모식도"></p>
<p>프로젝트의 규모와 개발부터 배포까지의 흐름이 비교적 단순하다는 점을 고려했을 때, 저희에게는 후자가 보다 나은 브랜치 전략이라고 판단했기에 최종적으로 <strong>GitHub Flow</strong>를 선택했습니다.</p>
<p>다음으로는 프로젝트에 어떤 기술 스택을 사용할지 결정해야 했습니다. 사용하기로 결정된 주요한 기술 스택은 React, Styled-components, Axios였습니다.</p>
<p><code>React</code>는 SPA를 구현하고자 하는 팀의 의도에 부합했기 때문에 선택했습니다. 페이지의 전환 없이 필요한 데이터만을 서버로부터 받아와 렌더링할 수 있고, 컴포넌트 단위의 개발이 가능하기 때문에 비슷한 컨텐츠 요소를 여러 페이지에서 재활용해야 하는 저희 프로젝트에 잘 맞을 것이라고 판단했어요.</p>
<p><code>Styled-components</code>는 다양한 CSS 라이브러리 선택지 중 하나였습니다. 저희 팀은 프로젝트의 규모가 작아 번들의 큰 크기가 크게 체감되지 않을 것이며, 스타일을 컴포넌트에 직접 연결해 가독성과 유지보수성을 높일 수 있다고 판단했기 때문에 해당 라이브러리를 사용하기로 결정했습니다.</p>
<p><code>Axios</code>는 기존의 Fetch API에 비해서 요청을 보내는 코드가 간결하기 때문에 가독성 측면에서 좋고, 응답 데이터를 활용하기 편리하게 자동으로 JSON 형식으로 변경해 주며, 에러 처리 방식 및 메시지 형식이 자세하여 개발 중 오류를 처리하기 용이하기 때문에 선택했습니다.</p>
<p>이후 기능 및 API 명세를 확인하면서 공통으로 구현할 컴포넌트와 그렇지 않은 컴포넌트를 분리하고, 2주차부터는 본격적으로 공통 컴포넌트 개발을 시작했습니다.</p>
<h3 id="2주차-공통-컴포넌트-개발">2주차: 공통 컴포넌트 개발</h3>
<p>이 기간 동안은 로그인 및 회원가입 페이지를 함께 작업했는데, 저는 회원가입 이후 초기 프로필을 설정하는 페이지를 맡게 됐습니다. 프로젝트 바로 직전 들었던 특강에서 마크업에 관련된 스포일러는 어느 정도 당했기 때문에 이를 구현하는 것은 어렵지 않았으나, 유효성 검사가 발목을 잡았습니다.</p>
<p>고민을 했던 부분은 이미지에 대한 유효성 검사였는데, 주어진 API에서는 jpg, gif, png, jpeg, bmp, tif, heic 확장자를 제외한 파일이 업로드 되거나 파일의 크기가 10MB를 초과할 경우 사용자에게 이를 알려주어야 했습니다.</p>
<pre><code class="language-js">const allowedExtensionsRegex = /\.(jpg|gif|png|jpeg|bmp|tif|heic)$/i;</code></pre>
<p>따라서 파일 확장자에 대한 정규식을 만들고, 파일의 이름으로부터 확장자를 분리한 뒤 소문자로 변환하여 정규식의 패턴과 일치하는지 확인했습니다. 파일의 크기가 10MB를 초과하는지 역시 분기 처리를 통해 추가해 주었습니다.</p>
<h3 id="3주차-역할분담에-따른-기능-구현">3주차: 역할분담에 따른 기능 구현</h3>
<p>3주차에는 역할분담에 따른 페이지 및 기능을 구현하는 기간이었습니다. 제가 맡은 기능은 크게 두 가지였습니다.</p>
<h4 id="게시물-crud">게시물 CRUD</h4>
<p>게시물의 작성, 읽기, 수정, 삭제 기능과 이를 포함하는 페이지를 만들어야 했습니다. 읽기와 삭제는 서버에 GET요청을 보낸 응답을 렌더링하거나 DELETE 요청을 보내는 것이라 작성과 수정에 비하면 상대적으로 수월했는데, 역시 문제는 작성과 수정이었습니다.</p>
<p><strong>게시물 작성</strong>을 구현하면서 무엇보다도 <code>textarea</code> 요소의 크기 변화에 대해서 많이 고민해야 했습니다. 텍스트를 작성할수록 textarea의 높이가 그 크기에 맞춰 자동으로 늘어나거나 줄어들도록 해야 했는데, 제가 선택한 방법은 textarea의 scrollHeight 속성을 조절하는 방법이었습니다.</p>
<pre><code class="language-js">const handleResizeHeight = () =&gt; {
    textarea.current.style.height = &#39;0&#39;;
    textarea.current.style.height = `${textarea.current.scrollHeight}px`;
};</code></pre>
<p>useRef 훅을 사용해 textarea를 참조하고, <code>handleResizeHeight</code> 를 사용해 사용자의 입력에 따라 그 크기를 조절했습니다. textarea에 작성되는 콘텐츠는 내용이 바뀔 때마다 상태로 저장해 서버에 요청을 보낼 때 용이하도록 했습니다.</p>
<p><strong>게시물 수정</strong>의 경우 미리 작성된 게시글로부터 수정하기 위한 정보를 어떻게 불러오는지가 관건이었는데, 이 부분은 React Router의 <code>useLocation</code>과 <code>useNavigate</code> 훅을 사용해 게시물 상세 페이지로부터 텍스트와 이미지를 전달받아 수정한 뒤 서버에 PUT 요청을 보내는 방식으로 구현했습니다.</p>
<h4 id="댓글">댓글</h4>
<p>댓글 기능은 크게 댓글 작성과 삭제로 나뉘어져 있었습니다. 사용자가 작성하지 않은 댓글은 삭제 대신 신고 처리가 가능하도록 구현해야 했습니다. 이 부분은 현재 로그인한 사용자의 ID가 클릭한 댓글의 작성자와 일치한다면 삭제 모달을, 아니라면 신고 모달을 띄우는 것으로 분기 처리를 해 주었습니다.</p>
<p>또한 댓글의 작성 시간은 게시물과는 다르게 x분 전, y시간 전 과 같이 표시되어야 했습니다. 어떤 방법을 사용하면 좋을 지 고민하다가 시간 관련 라이브러리인 moment.js를 선택했습니다.</p>
<pre><code class="language-js">moment.locale(&#39;ko&#39;);
const fromNow = moment(createdAt).fromNow();</code></pre>
<p>이렇게 두 줄만 작성하면 현재 시간(fromNow)으로부터 댓글이 작성된 시간(createdAt)의 차이를 확인할 수 있었습니다. 심지어 지역 설정까지 가능했기 때문에 복잡한 로컬라이제이션 과정 없이 ‘5분 전’등으로 작성 시간을 표시할 수 있었습니다.</p>
<h3 id="4주차-버그-수정-및-배포">4주차: 버그 수정 및 배포</h3>
<p>4주차는 그동안 발견하지 못했던 버그를 해결하고 최종적으로 완성된 프로젝트를 배포했습니다.</p>
<p>배포를 위한 툴로 Vercel, Netlify, GitHub Page가 고려되었는데, 배포할 프로젝트의 규모가 크지 않고, 최대한 간편하게 배포하기를 원했기 때문에 Netlify를 사용하여 배포했어요.</p>
<h2 id="돌아보기">돌아보기</h2>
<h3 id="liked">Liked</h3>
<ul>
<li>처음으로 진행하는 팀 프로젝트였는데, 개발부터 배포까지의 과정을 전체적으로 경험해 볼 수 있어서 좋았습니다.</li>
<li>팀원들과 프로젝트를 함께 진행하면서 같은 부분을 구현하더라도 서로 다른 의견을 교환하고 생각해 볼 수 있었습니다.</li>
<li>구현 중 예측하지 못했던 문제들이 발생하는 경우들이 종종 있었고, 이를 함께 해결해 나가며 성장할 수 있었습니다.</li>
<li>백엔드 API를 활용한 프로젝트를 만드는 경험을 할 수 있었습니다.</li>
</ul>
<h3 id="lacked">Lacked</h3>
<ul>
<li>회의를 꾸준히 기록하려고 했지만, 프로젝트를 진행하다 보니 당장 구현해야 하는 기능에만 급급해서 제대로된 기록을 남기지 못했습니다.</li>
<li>코드 리뷰가 올바르게 이루어지지 않고 그냥 넘어가는 경우가 있었습니다.</li>
<li>코드 컨벤션을 잘 지키지 못했던 것 같습니다.</li>
<li>리팩토링 과정에서 TanStack Query를 충분히 이해하고 사용하기 보다는 돌아가는 코드를 구현하는 데 그쳤습니다.</li>
<li>리팩토링을 했음에도 불구하고 여전히 놓치거나 해결하지 못한 개선점이나 버그들이 있었습니다.</li>
</ul>
<h3 id="learned">Learned</h3>
<ul>
<li>동료들과 함께 협업을 어떻게 진행해나가야 하는지 배울 수 있었습니다.</li>
<li>건강을 생각하지 않고 프로젝트를 진행한 날이 있었는데, 다음날 프로젝트에까지 영향을 미쳤습니다.</li>
<li>열심히 하는 것도 중요하지만 꾸준히 할 수 있는 것이 더 중요합니다.</li>
<li>부족하지만 공식문서를 통해 TanStack Query를 활용하는 방법에 대해서 공부할 수 있었습니다.</li>
</ul>
<h3 id="longed-for">Longed for</h3>
<ul>
<li>‘나중에 기록으로 남기겠지’라고 생각하면 늦습니다. 작업이 하나 끝날 때 마다 꾸준히 기록으로 남겨봅시다.</li>
<li>단순히 라이브러리가 있으니까 사용하기 보다 내가 왜 이 라이브러리/프레임워크를 사용했는지를 의식적으로 생각하도록 합시다.</li>
<li>개발자가 보기에 단순해 보이는 오류도 사용자에게 명시하지 않는다면 불편을 겪습니다. 항상 사용자의 입장에서 생각해 봅시다.</li>
</ul>
<h2 id="나오면서">나오면서</h2>
<p>첫 팀 프로젝트였던만큼 두렵기도 했고 설레이는 마음도 있었는데, 딱 정확히 반반이었던 것 같습니다. 처음 기능구현 사항을 확인했을 때는 이걸 언제 다 구현하나 싶었지만, 막상 닥치고 나니 어떻게든 구현하는 저를 볼 수 있었습니다. 물론 <strong>주어진 요구사항을 구현하는 것</strong>과 <strong>주어진 요구사항을 분석하면서 더 나은 서비스를 만들기 위해 노력하는 것</strong> 사이에는 큰 간극이 있지만, 전자도 해냈으니 점점 발전하면서 후자도 해낼 것이라고 믿습니다.</p>
<p>그리고 동료평가 받기 전까지만 해도 팀원으로서 너무 별로였다는 평가가 있을까봐 걱정을 많이 했는데, 다행히 모든 피드백들이 내가 프로젝트에 열심히 기여했다고 이야기해 주고 있어서 조금은 뿌듯했습니다. 하지만 공통적으로 너무 무리하지 말고 건강을 꼭 챙기라는 이야기도 있어서 다음에는 꼭 건강도 관리하면서 프로젝트를 진행해야겠습니다.</p>
]]></description>
        </item>
    </channel>
</rss>