<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>today-is-first.log</title>
        <link>https://velog.io/</link>
        <description>이찬</description>
        <lastBuildDate>Thu, 16 Oct 2025 11:09:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>today-is-first.log</title>
            <url>https://velog.velcdn.com/images/today-is-first/profile/755d983e-244a-4a52-beb4-ca437ac441d8/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. today-is-first.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/today-is-first" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[비전공자 개발자가 취업 성공까지(feat. 네부캠 9기, 싸피 13기)]]></title>
            <link>https://velog.io/@today-is-first/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%B7%A8%EC%97%85-%EC%84%B1%EA%B3%B5%EA%B9%8C%EC%A7%80feat.-%EB%84%A4%EB%B6%80%EC%BA%A0-9%EA%B8%B0-%EC%8B%B8%ED%94%BC-13%EA%B8%B0</link>
            <guid>https://velog.io/@today-is-first/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%B7%A8%EC%97%85-%EC%84%B1%EA%B3%B5%EA%B9%8C%EC%A7%80feat.-%EB%84%A4%EB%B6%80%EC%BA%A0-9%EA%B8%B0-%EC%8B%B8%ED%94%BC-13%EA%B8%B0</guid>
            <pubDate>Thu, 16 Oct 2025 11:09:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>어느새 개발자로 취업하겠다고 마음 먹은지 1년 반이 넘게 흘렀다. 처음 시작했을 때 계획과 달리 취업이라는 목표에 도달하기까지 꽤나 오랜 시간이 걸렸다. 그동안의 시간을 정리해보려고 한다.</p>
</blockquote>
<h2 id="왜-개발자가-되고-싶었나">왜 개발자가 되고 싶었나</h2>
<p>제약회사의 인사총무팀에서 인턴하던 도중 회사 내 지적 장애를 가진 동료분께 업무를 인수인계 해야했다. 처음에는 업무 매뉴얼을 만들어서 교육을 시도해봤지만, 어려운 절차로 인해 실패했다.</p>
<p>다른 방법을 모색하던 중 사내 도서관을 정리하다가 <strong>파이썬으로 업무 자동화하기</strong>라는 주제의 책을 발견하였고, 독학으로 프로그램을 제작하여 <strong>클릭 한번으로 업무가 완성되게 만들었다.</strong> <strong>동료분이 만족해하는 모습을 보며 뿌듯함을 느꼈고 개발자가 되기로 결심했다.</strong></p>
<h2 id="비전공자의-고민">비전공자의 고민</h2>
<p>나는 대학교에서 <strong>어문 계열을 단일 전공한 순수 혈통의 비전공자</strong>이다. 따라서, 개발에 관련된 강의를 일체 수강한 적이 없었다. 개발자가 되기 위해 교육을 받아야 했고, <strong>가장 경쟁률이 치열한 교육기관에 가서 좋은 교육을 듣고 좋은 동료들과 같이 성장하기로 결심했다.</strong></p>
<p>내가 목표로 한 교육 기관은 <strong>네이버 부스트캠프, 싸피, 우아한 테크코스</strong> 총 3군데였다. 하지만, 경쟁이 치열한 만큼 나의 강점을 확실히 어필해야하는데, 나의 경우 <strong>학과, 학점, 동아리 등 어필할 수 있는 게 아무것도 없어서 고민</strong>이었다.</p>
<h2 id="네이버-부스트캠프-9기-네부캠">네이버 부스트캠프 9기 (네부캠)</h2>
<p>다행히 네이버 부스트캠프의 경우 <strong>베이직, 챌린지 코스동안 3번의 테스트와 과제, 피어 활동을 종합적으로 평가</strong>하기 때문에 나의 성장 의지와 열정을 보여주기 최적이었다.</p>
<p>챌린지 코스는 한달동안 진행되는데 매일 git 구현하기, 가상 메모리 구현하기와 같은 과제들이 쏟아지고 피어리뷰 및 동료 피드백까지 거치는 활동이다. </p>
<p>매일 오전 10시쯤에 과제가 공개되면 새벽 3시까지 코드를 완성하고 자는 일이 반복되었다. <strong>CS지식, 코드 컨벤션, 클린 코드 등을 배우며 성장할 수 있었고, 다행히도 멤버십에 합류할 수 있었다.</strong></p>
<p>네부캠 멤버십에서는 <strong>어떻게 하면 좋은 개발자로 성장할 수 있는지와 문제 해결력을 기르는 방법에 대해서 배웠다.</strong> 이 때의 가르침 덕분에 문제를 만나도 해결할 수 있는 능력을 키울 수 있었다.</p>
<p>네부캠 멤버십에서 프로젝트를 진행했는데 프론트엔드 3명, 백엔드 1명과 팀이 되었고, <strong>백엔드 비중이 높은 프로젝트를 진행해서 누군가 백엔드로 전향해야 하는 상황</strong>이었다.</p>
<p>나는 <strong>원래 프론트를 희망했지만, 인프라와 백엔드를 맡게 되었다.</strong> 프로젝트를 진행하는 과정에서 백엔드가 없으면 사용자에게 실질적인 가치를 제공할 수 없다는 걸 깨닫게 되었고, 백엔드에 대한 관심이 생겼다.</p>
<h2 id="삼성-청년-ai-아카데미-13기싸피">삼성 청년 AI 아카데미 13기(싸피)</h2>
<p>네부캠 멤버십 코스가 끝나며 <strong>네부캠 리팩토링 코스에 참여할지, 싸피를 갈지, 취준을 할지 선택</strong>을 했어야 했다. 당시 나의 선택은 <strong>매달 100만원의 생활비와 맛있는 점심 및 여러 취업 준비를 지원해주는 싸피</strong>였다. 지금 돌이켜보면 <strong>개발자 취업이 어려운 이 시기에 최고의 선택을 했다고 생각한다.</strong></p>
<p><strong>싸피만큼 교육생에게 지원해주는 교육기관은 없다.</strong></p>
<ul>
<li>각 반에 프로, 강사, 컨설턴트 등이 배치</li>
<li>각 기업에서 채용설명회를 하러 옴</li>
<li>특정 기업에 지원할 때 가산점</li>
<li>면접이 잡히면 채용 컨설턴트님과 상담</li>
<li>모의 면접도 진행</li>
<li>각 기업에 맞는 맞춤 컨설팅도 지원</li>
<li>포트폴리오 컨설팅</li>
<li>프로젝트 비용 지원</li>
<li>각종 기업 연계 프로젝트
등등</li>
</ul>
<p>매달 100만원의 생활비, 무료 점심, 각종 취업 지원이라는 혜택을 받으며 취업 준비와 프로젝트를 병행할 수 있기 때문에 <strong>요즘같이 취업 시장이 어려운 상황에서는 최고의 교육기관이다.</strong></p>
<p>다만, 네부캠은 정글에서 맨 몸에 돌 하나 들고 직접 원숭이 같은 야생동물과 맞서 싸워 잡아 먹어야했다면 싸피는 레스토랑 같았다.</p>
<p>가만히 앉아있으면 <strong>웨이터가 알아서 먹을 것을 가져다 주고, 먹는 법을 알려준다.</strong> 하지만, 이게 마냥 좋은 게 아니다. 실제 우리가 개발을 하게 되면 <strong>예상하지 못한 문제를 맞막뜨리게 되고 이를 직접 해결해야 하는데, 혼자서 해결하는 법을 배우지 못한 사람들은 어려움을 겪게 된다.</strong></p>
<p>만약 싸피를 다니거나 지원할 사람이 이 글을 읽게 된다면, <a href="https://velog.io/@today-is-first/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C-%EC%84%B1%EC%9E%A5%ED%95%98%EB%A0%A4%EB%A9%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C">개발자로 성장하려면 어떻게 해야할까</a> 이 글을 읽어봤으면 좋겠다!</p>
<h2 id="취업-준비">취업 준비</h2>
<p>개발자는 취업할 때 너무나 많은 것을 준비해야한다.</p>
<ul>
<li>CS</li>
<li>이력서</li>
<li>포트폴리오</li>
<li>코딩 테스트</li>
<li>기술 면접</li>
<li>인성 면접</li>
<li>기술 블로그</li>
</ul>
<p>그래서, 하나씩 채워나가려고 하면 안되고 계속 꾸준히 모든 걸 신경써줘야 한다.</p>
<h3 id="cs">CS</h3>
<p>나는 네부캠 멤버십을 할 때 CS스터디를 만들었다. <strong>매주 하나의 주제를 가지고 각자 15분간 발표하고 서로 Q&amp;A하는 방식이었다.</strong> 면접에서 많이 나오는 질문과 답변을 외우는 형식이 아니라, <strong>하나의 주제를 탄생 배경부터 특징까지 정리하니 훨씬 이해가 잘됐고, 기억에 잘남았다.</strong></p>
<p>내가 첫 번째로 준비한 주제는 <a href="https://velog.io/@today-is-first/HTTP%EB%8A%94-%EC%99%9C-0.9%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%A0%EA%B9%8C">HTTP는 왜 0.9부터 시작할까</a>였는데, 이 발표를 스터디뿐만 아니라 네부캠의 다른 피어들에게도 설명하느라 <strong>하루에 3번 발표하기도 했다.</strong></p>
<p>근데, 이 주제를 지금까지 수십번도 넘게 발표했는데 싸피 면접을 볼 때도 활용했고, 싸피에서 프로젝트 팀원이 정해지면 해당 인원들에게도 했다.</p>
<p>또한 <strong>이번에 취업하게 된 회사 면접에서도 스피치했는데, 하나의 주제를 잘 정리해놓으니 나만의 강점을 보여주는 무기로 사용할 수 있었다.</strong></p>
<p>만약 CS를 공부하게 된다면, 답을 외우는 게 아니라 꼭 해당 개념을 기술 블로그로 정리하고 남에게 설명하는 방식으로 공부했으면 좋겠다.</p>
<hr>
<h3 id="이력서와-포트폴리오">이력서와 포트폴리오</h3>
<p>주변에 있는 대부분의 사람들이 이력서와 포트폴리오를 작성하는데 어려움을 겪는 것 같다. 물론 나도 그렇다.</p>
<p>내가 가장 크게 도움받았던 블로그가 있다.</p>
<p>기획자분이 작성하긴 했지만, 충분히 개발자 포폴에도 적용 가능하다고 생각한다.</p>
<p>경험 정리 / 마인드맵 정리 파트는 꼭 봤으면 좋겠다.
(경험 정리, 조직 생활 정리 노션은 해당 블로그에 댓글을 작성하면 받을 수 있다.)</p>
<p><a href="https://brunch.co.kr/@new-una/18">합격만 바라며 만들었던 포트폴리오</a>
<a href="https://brunch.co.kr/@new-una/22">합격한 포트폴리오 제작기(1탄)</a>
<a href="https://brunch.co.kr/@new-una/23">합격한 포트폴리오 제작기(2탄)</a>
<a href="https://brunch.co.kr/@new-una/24">합격한 포트폴리오 제작기(3탄) + 실제 예시 공유</a>
<a href="https://brunch.co.kr/@new-una/26">포트폴리오 제작기를 마무리하며</a></p>
<p>위 글을 보고 경험 정리를 했고, 이를 바탕으로 이력서, 자소서, 면접 질문 준비 등에 활용했다.</p>
<p>처음 작성한 이력서가 맘에 들어서 위풍당당하게 카카오 재직 중인 프론트엔드분께 대면 피드백을 받으러 갔다.</p>
<p>물론, 탈탈 털렸다.</p>
<p>이후 피드백을 바탕으로 수정하였고, 싸피에서 지원해주는 이력서 컨설턴트분께도 피드백을 받고 수정하는 과정을 반복했다.</p>
<p>공통적인 피드백을 정리하자면, 이력서는 면접을 위한 재료와 같다. 그리고 그 재료로 <code>내가 지원자 중 가장 [이것]이 뛰어난 사람이다.</code> 라는 것을 어필해야한다.</p>
<p>따라서, 여러분이 다른 참가자와 비교했을 때 차별점이 될만한 것을 찾아야 한다.</p>
<p>즉, 여러분만의 무기를 만들고 그것에 대한 스토리텔링을 이력서에 녹여내야 한다.</p>
<p><strong>반드시 이력서를 작성하면 면접관으로 들어가는 사람들에게 피드백을 받았으면 좋겠다.</strong></p>
<hr>
<h3 id="기술-블로그">기술 블로그</h3>
<p>나는 사실 벨로그 말고도 네이버 일상 블로그를 3년전부터 운영 중이다. 따라서 기록하는 게 습관이 되어서 글을 작성하는데 큰 어려움은 없었는데, 내 주변의 사람들이 작성에 어려움을 느끼는 것 같아 내가 중요하게 생각하는 포인트 두 가지를 적어봤다.</p>
<h4 id="면접관이-읽고-싶은-글쓰기">면접관이 읽고 싶은 글쓰기</h4>
<p>의무감에 정보만 나열된 글을 작성하는 사람들이 있다. 면접관은 이미 해당 정보를 알고 있는데 굳이 읽으려 하지 않을 것이다.</p>
<p>내가 어떤 생각을 갖고 있는지, 문제를 맞닥뜨리고 어떻게 해결하려고 했는지를 위주로 작성하면 좋다.</p>
<p><a href="https://www.youtube.com/watch?v=BGZaUpUtY6k&amp;t=620s">토스 모닥불 EP 8</a>을 참고하면 어떤 걸 바라는지 알 수 있을 것이다.</p>
<h4 id="적절한-시각-자료-제공">적절한 시각 자료 제공</h4>
<p>논리의 구조와 함께 코드 레벨을 제공하거나 로직이 어렵다면 시각화해서 제공하면 좋다. <a href="https://velog.io/@today-is-first/%EC%96%B8%EB%A7%88%EC%9A%B4%ED%8A%B8%EB%A6%AC%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%8B%9C-%EB%AF%B8%EB%94%94%EC%96%B4-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EB%81%8A%EA%B9%80-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0">참고 - 언마운트/리마운트 시 미디어 스트림 끊김 현상 해결</a></p>
<p>나의 경우 CS스터디에서 자료를 정리하던 습관 때문에 기술 블로그도 발표의 형식으로 자료를 정리하고 기술하는 것을 선호하는데, 해당 방법은 시간이 오래걸리기 때문에 클로드를 활용해서 시각화하는 것도 좋아 보인다.</p>
<p>물론 다른 개발자 선배분께 이야기를 들었을 때 기술 블로그가 서류 합격과 크게 관련이 없다고 하셨다.</p>
<p>하지만 미리 내 경험을 정리해두면 면접 때 따로 준비하지 않아도 쉽게 대답할 수 있고, 나의 경우 내 이력서에 최대한 블로그 링크를 첨부하여 근거를 제시하는데 활용했다.</p>
<h3 id="꾸준히-노력하기-가장-중요">꾸준히 노력하기 (가장 중요)</h3>
<p>할 게 굉장히 많기 때문에 꾸준히 조금씩 하는 방법밖에 없다.</p>
<p>나는 아래와 같은 계획을 세웠지만, 물론 실패한 날도 많다</p>
<ul>
<li>매 주 1회 기술 블로그 작성</li>
<li>매일 코테 1문제 풀기</li>
<li>매일 딥다이브 2시간 <a href="https://velog.io/@today-is-first/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C-%EC%84%B1%EC%9E%A5%ED%95%98%EB%A0%A4%EB%A9%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C#%EB%94%A5%EB%8B%A4%EC%9D%B4%EB%B8%8C">참고</a></li>
</ul>
<p>하지만 꾸준히 계속 노력한다면 결국 성과가 나올 수 밖에 없다.</p>
<hr>
<h2 id="취업-성공">취업 성공</h2>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/e8aee692-936b-47ae-b03b-87fdfed1784d/image.png" alt=""></p>
<p>위의 노력을 꾸준히 쌓아서 취업까지 도달할 수 있었다. 물론 해당 회사는 신입은 무조건 인턴 3개월을 거친 후 채용 전환 심사를 보기 때문에 아직 하나의 벽을 더 넘어야 한다.</p>
<p><strong>지금 해왔던 것처럼 최선을 다하면 넘을 수 있다고 생각하고 회사 생활을 즐겨보려고 한다.</strong></p>
<h2 id="맺음말">맺음말</h2>
<p>물론 나도 아직 갈 길이 멀지만, 내 주변의 취준생들에게 조금이나마 도움이 될까 싶어서 적어봤다.</p>
<p>모두가 원하는 것을 이룰 때까지 힘냈으면 좋겠다 화이팅!
나도 화이팅!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[취준 중간 회고와 연휴 때 성장 계획]]></title>
            <link>https://velog.io/@today-is-first/%EC%B7%A8%EC%A4%80-%EC%A4%91%EA%B0%84-%ED%9A%8C%EA%B3%A0%EC%99%80-%EC%97%B0%ED%9C%B4-%EB%95%8C-%EC%84%B1%EC%9E%A5-%EA%B3%84%ED%9A%8D</link>
            <guid>https://velog.io/@today-is-first/%EC%B7%A8%EC%A4%80-%EC%A4%91%EA%B0%84-%ED%9A%8C%EA%B3%A0%EC%99%80-%EC%97%B0%ED%9C%B4-%EB%95%8C-%EC%84%B1%EC%9E%A5-%EA%B3%84%ED%9A%8D</guid>
            <pubDate>Sat, 04 Oct 2025 07:18:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>금요일(10.10)을 빼면 거의 10일이 이어지는 연휴를 맞이하게 됐다. 이번 연휴를 어떻게 알차게 보낼 수 있을까 생각을 하다가 취준 중간 회고 겸 계획을 작성해보고자 한다.</p>
</blockquote>
<p>연휴 맞이 집 대청소를 하면서 내가 전역 전 D-101일부터 작성했던 일기를 꺼내보게 됐다.</p>
<p>나는 KCTC 훈련을 진행하는 전문대항군연대에서 복무했다.(특이하게도 훈련 때 북한 군복을 입는 부대) 부대 복귀 없이 산에서 일주일 넘게 땅파고 지내면서 <strong>힘든 시간을 보내면서도 꿈에 대한 열정으로 가득했다.</strong></p>
<p>코로나로 인해 유학을 떠날 수 없게 되면서, 내가 평소 좋아하던 것 중에서 진로를 선택하고자 했다. 당시 나는 주식과 영상 편집에 관심이 있었던 것 같다.</p>
<p>그때 쓴 일기를 공유해보자면</p>
<blockquote>
<p>오늘은 삽으로 땅을 파 참호를 만들었다.
앞으로 여기서 며칠동안 먹고 자야한다.
여기는 온통 벌레, 거미, 도마뱀, 똥파리 천지이다.
주변에는 맷돼지가 음식물 쓰레기를 뒤지고 있고, 고라니가 소리지르며 뛰어다닌다.
(중략)
신병 때 썼던 일기를 읽고 힘을 냈던 것처럼
지금 쓰는 이 일기가 훗날의 나에게 도움이 되길 바란다.
이 글을 읽는 미래의 내가 어떤 것을 하고 있을까 궁금하다.
영상 편집이나 주식 아니면 전혀 다른 것?
그 무엇이 되었든 최선을 다해서 원하는 것을 이루길</p>
</blockquote>
<p>당시에는 전혀 생각지도 못한 코딩을 하게 됐다.</p>
<p>군생활동안 하늘이라는 군대 동기와 탁구를 치면서 우리가 전역하고 어떻게 성장할지 고민하는 가졌다.</p>
<p>그때는 5년 뒤에 성공한 인생을 살고 있을 거라 기대했지만, 나도 하늘이도 기대했던 인생을 살지는 못하고 있다.</p>
<p><strong>하지만, 그래도 한 가지 지켜진 것이 있다.</strong></p>
<p>그때도, 지금도 하늘이와 나는 계속 서로가 어떻게 성장할 수 있을지 계획을 세우고 이행하며 성장하고 있다는 것이다.</p>
<p>어제 하늘이와 통화를 하며 내가 5년 사이 이룬 것을 정리해봤다.</p>
<h3 id="내가-이룬-것">내가 이룬 것</h3>
<p>나의 목표는 <strong>다양한 직무를 경험하고 진로를 정하는 것</strong>이었다.
코로나가 언제 끝날지, 모르는 상황 속에서 한국에서 어떻게 살아갈지 고민했어야 했다.</p>
<p>내가 한 직무 경험</p>
<ul>
<li>학교 도서관 교내 근로 2년 반</li>
<li>제약회사 인사총무팀 인턴 6개월</li>
<li>검찰청 집행과 대학생 알바 2개월</li>
<li>현장실습 지원센터 교내 근로 4개월</li>
</ul>
<p>이 중 제약회사에서 근무하면서 <strong>장애를 가진 동료를 위해 업무 자동화 툴을 만들었는데, 이때의 경험이 나를 개발자로 이끌었다.</strong></p>
<p>이후 개발자가 되기 위해 다양한 노력을 했는데, <strong>경쟁률이 치열한 교육 기관에 들어가서 교육을 받고 좋은 동료들 사이에서 성장하려고 했다.</strong></p>
<p>베이직 코스 - 챌린지 코스를 거치고, 세 번의 테스트 과정을 통과하였고, 멤버십 과정에 선발되어 <strong>네이버 부스트캠프 멤버십 과정</strong>에 합류할 수 있었다.</p>
<p>네부캠에서는 <strong>좋은 개발자가 무엇인지, 그러한 개발자로 성장하려면 어떻게 해야하는지를 배웠다.</strong> 또한, 좋은 동료들을 알게 되어 CS스터디를 운영하게 됐고, 벌써 1년이 넘게 유지되고 있다. 물론 그 사이 다들 좋은 곳에 가게되어, 2명은 네이버에, 한 명은 유망한 스타트업에 합류하였고 이제 나만 취업하면 된다.</p>
<p>네부캠 과정 중 <strong>백엔드에 대한 관심이 생겨서 네부캠 수료 후 삼성에서 운영하는 싸피에 합류하게 되었다.</strong> 매일 9시부터 저녁 6시까지 싸피에서 교육을 받고, 저녁 6시부터 9시까지 까페에서 공부하는 스터디를 운영하며 추가적인 공부를 하고 있다.</p>
<p>또한, 동료 학습을 굉장히 중요하게 생각하는데, 프로젝트를 진행하며 매주 1회 기술 블로그 작성, 매일 2시간 언어에 대해 서로 질문하며 공부하는 딥다이브 문화 등을 정착하며 성장하려고 하고 있다.</p>
<p>요약하자면 5년동안 여러 직무를 경험했고, 진로를 정했으며 높은 경쟁률을 뚫고 좋은 교육기관에서 성장하고 있다. 또한 최근엔 면접도 보게 됐다.</p>
<h3 id="첫-번째-면접">첫 번째 면접</h3>
<p>올해 하반기에 개발자로서 처음으로 취업을 준비하게 됐다. 프론트, 백엔드를 둘다 경험해보면서 프론트가 훨씬 적성에도 잘맞고 흥미가 간다고 느껴져서 프론트엔드만 지원하기로 결심했다. </p>
<p>8월 말 처음으로 이력서도 만들어보고, 네부캠 때 멘토를 해주셨던 프론트엔드분께 연락을 드려서 대면으로 이력서에 대한 조언을 받았다. 또한 싸피에서 지원해주는 컨설팅 프로그램을 신청해서 이스트소프트에 재직 중인 분께 이력서에 대한 조언을 받아 수정하였다.</p>
<p>그렇게 완성된 이력서로 처음 넣었던 곳에서 면접 연락이 왔다. 면접 잡힌 날이 하필이면 프로젝트 마지막 날이라 프로젝트 마무리로 정말 바쁜 와중에 면접 준비도 병행하느라 힘들었다.</p>
<p>개발자로서 직무 면접이나 인성 면접이 처음이라 준비하는 데 어려움이 많았는데, 나름 잘 대답했던 것 같다. 당시 면접에 심장이 쿵쿵 뛰어서 긴장했나 싶었는데, 다시 생각해보니 당시 커피만 세 잔 마신 상태라 과도한 카페인으로 심장이 뛰었던 것 같다.</p>
<p>내가 가진 장점을 잘 어필했다고 생각이 들어서 다른 회사 지원을 중단할까 자만하기도 했는데, 기대와 달리 떨어졌다...</p>
<p>사실 떨어진지도 모르겠다. 원래 빠르면 월요일, 늦어도 목요일까지는 합/불 연락을 준다고 했는데 여태 감감 무소식이다. 좀 서운할지도...</p>
<p>나도 예전에 인사총무팀에서 근무할 때 공채 합격 발표가 예정보다 일주일 뒤로 밀리면서 하루종일 지원자로부터 재촉 연락을 받아 힘들었던 경험이 있었다. 근데 막상 내가 지원자로 합격 연락을 기다려보니 그분들이 왜 그랬는지 이해하게 되었다. 피말리는 경험이다.</p>
<h3 id="연휴-때-계획">연휴 때 계획</h3>
<p>연휴 때 약속도 많이 잡히긴 했지만, 그와중에 최대한 내가 부족한 부분을 채우려고 한다.</p>
<p>우선 면접 때 대답이 부족했던 함수형 프로그래밍, 객체 지향 프로그래밍을 프론트엔드에서 어떻게 잘 활용할 수 있을지 정리하는 글을 작성할 예정이다.</p>
<p>또한, 요즘 Next의 필요성을 많이 느끼고 있다. 리액트에서 라이트하우스를 개선하려고 여러 조치를 해야 하는 것들이 Next에서는 그냥 지원해주는 경우가 많아서 기술에 대한 관심이 생겼다.</p>
<p>이외에도 타입스크립트나 자바스크립트의 핵심 개념을 정리하면서 기술을 활용하는 개발자가 되기 위해 노력할 예정이다.</p>
<p>연휴동안 할 일</p>
<ul>
<li>함수형 프로그래밍, 객체지향 프로그래밍 프론트에서 어떻게 잘 활용할 수 있을까 정리</li>
<li>Next 공부</li>
<li>Typescript, Javascript 핵심 개념 정리</li>
<li>hotjar로 수집한 데이터 기반 개선 경험 정리</li>
<li>코딩 테스트 준비</li>
</ul>
<p>맨날 이런 계획을 세우면서 느끼는 건데, 계획은 거창한 것보다 바로 실행 가능한 작은 단위로 나눠놓는 게 좋은 것 같다.</p>
<p>거창하지는 않지만, 위에 적은 것들을 확실하게 정리하면서 성장하는 시간을 가져보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트에서의 객체 지향과 함수형 프로그래밍?? (1편)]]></title>
            <link>https://velog.io/@today-is-first/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5</link>
            <guid>https://velog.io/@today-is-first/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5</guid>
            <pubDate>Sun, 28 Sep 2025 17:53:29 GMT</pubDate>
            <description><![CDATA[<p>이번에 면접을 보게 되면서 <code>프론트에서 객체지향과 함수형 프로그래밍을 어떻게 활용할 수 있을지 구체적인 예시로 설명해주세요</code>라는 질문을 받게 됐습니다.</p>
<p>평소 테스트 코드를 짜면서 <strong>단일 책임 원칙(SRP)</strong>이 중요하다는 것과 컴포넌트 설계를 하면서 <strong>개방 폐쇄 원칙(OCP)</strong>이 중요하다는 것은 느꼈는데</p>
<p>막상 프론트엔드에서 어떻게 <strong>객체 지향 프로그래밍과 함수형 프로그래밍</strong>을 잘 적용할 수 있을지 고민해본 적이 없어서 한번 고민 과정을 기록해보겠습니다.</p>
<p>당시 제가 질문을 듣고 떠올랐던 의문들은 다음과 같습니다.</p>
<ol>
<li><p>리액트의 컴포넌트를 순수 함수로 볼 수 있을 것인가?</p>
<ul>
<li>리액트의 컴포넌트 내부에서 외부 스토어를 구독하거나 서버 상태를 가질 수도 있는데, 순수 함수라 볼 수 있을까</li>
</ul>
</li>
<li><p>그러면 컴포넌트 내에서 상태를 관리하지 않는 순수 view는 순수 함수인가?</p>
</li>
<li><p>리액트의 컴포넌트가 순수 함수라면 HOC도 함수형 프로그래밍인 것인가?</p>
</li>
<li><p>리액트 16버전 이전에 사용된 class형 컴포넌트 선언을 하면 객체 지향이라 할 수 있는가?</p>
</li>
</ol>
<p>머릿 속이 복잡해지면서 해당 질문에 대한 답을 제대로 하지 못한 것 같아서 당시 떠오른 의문을 하나씩 없애보겠습니다.</p>
<hr>
<h3 id="리액트-컴포넌트를-순수-함수로-볼-수-있을까">리액트 컴포넌트를 순수 함수로 볼 수 있을까?</h3>
<p>리액트 함수형 컴포넌트는 동일한 props이 오면 동일한 UI가 반환되니 순수 함수에 가깝습니다.</p>
<p>다만, 리액트 훅으로 외부 상태를 의존하게 되면 순수 함수가 아니게 됩니다.</p>
<p>상태를 관리하지 않는 View 컴포넌트는 순수 함수라고 볼 수 있을 것 같습니다.</p>
<h3 id="hoc고차-컴포넌트는-함수형-프로그래밍인가">HOC(고차 컴포넌트)는 함수형 프로그래밍인가?</h3>
<p>HOC는 함수(컴포넌트)를 인자로 받고 반환한다는 점에서 함수형 프로그래밍이라 할 수 있습니다.</p>
<h3 id="class형-컴포넌트는-객체지향이라-할-수-있을까">Class형 컴포넌트는 객체지향이라 할 수 있을까?</h3>
<p>리액트 16 이전의 class 컴포넌트는 겉보기엔 OOP처럼 보이지만, 그냥 라이프사이클 메서드를 담기위한 문법이라고 봐야합니다.</p>
<hr>
<p>일단 당시의 의문들이 좀 해소된 것 같습니다.</p>
<p>아직도 프론트엔드에서 어떻게 잘 활용할 수 있는지는 의문입니다.</p>
<p>그래서 가장 먼저 찾아본 자료는 <a href="https://toss.tech/article/firesidechat_frontend_2">토스의 모닥불 - 함수형 프로그래밍, 프론트엔드 개발에 진짜 도움될까?</a>입니다.</p>
<p>영상의 내용을 요약하자면 아래와 같습니다.</p>
<ul>
<li>객체지향은 구조 설계 / 모듈 분리 원칙과 같은 거시적 관점에서 활용하면 좋음</li>
<li>함수형 프로그래밍은 사이드 이팩트를 줄일 수 있고, 레이지 이벨류에이션을 적용하면 좋음</li>
</ul>
<p>토스에서는 어떻게 객체지향과 함수형 프로그래밍을 적용하는지 감은 온 것 같습니다.</p>
<p>제 코드에 실제로 적용해보면서 객체지향과 함수형 프로그래밍의 장점을 느껴보고 싶습니다.</p>
<hr>
<h3 id="맺음말">맺음말</h3>
<p>원래는 이번 편에 프론트엔드에서 객체지향과 함수형 프로그래밍을 어떻게 잘 적용할 수 있을지와 실제 적용을 다룰려고 했는데</p>
<p>프로젝트 최종 발표(이따 7시간 뒤 발표...)와 겹치게 되면서, 아쉽게도 면접 당시의 의문을 정리하는 정도로 끝났습니다</p>
<p>이번 연휴가 굉장히 기니 이때 확실하게 정리해보려고 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 개발 생산성을 높여보자]]></title>
            <link>https://velog.io/@today-is-first/AI%EB%A1%9C-%EA%B0%9C%EB%B0%9C-%EC%83%9D%EC%82%B0%EC%84%B1%EC%9D%84-%EB%86%92%EC%97%AC%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@today-is-first/AI%EB%A1%9C-%EA%B0%9C%EB%B0%9C-%EC%83%9D%EC%82%B0%EC%84%B1%EC%9D%84-%EB%86%92%EC%97%AC%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 28 Sep 2025 07:30:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>안녕하세요, 오늘은 AI툴과 MSW를 활용해 개발 생산성을 높이려고 시도한 과정을 말씀드리겠습니다.</p>
</blockquote>
<p>이번 모의투자 프로젝트를 진행하면서 가장 큰 걸림돌이 되었던 것은 <strong>개발 일정</strong>이었습니다.</p>
<p>개발 기간이 10일 밖에 없는 상황에서 <strong>백엔드/프론트 개발 및 연동, 배포 환경 테스트, 모의투자대회 서비스 오픈</strong>까지 해야했습니다.</p>
<p>따라서 <strong>어떻게 하면 프론트엔드에서 생산성을 높일 수 있을까</strong> 고민했습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/f6a1384a-3755-4e18-8a63-0bbb41b07243/image.PNG" alt=""></p>
<p>제가 경험한 프로젝트는 일반적으로 위와 같이 진행됐습니다.</p>
<p>기획이 나오면 <strong>디자인 및 와이어 프레임 제작과 API 및 스키마 설계</strong>를 합니다.</p>
<p>그 후 백엔드 개발이 완료되면 프론트엔드 개발이 시작이 되죠.</p>
<p>따라서 <strong>프론트는 항상 프로젝트의 마지막 단계에서 병목 지점으로 오해받기도 합니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/6d6550fd-7838-4450-97d4-7cdecc69951a/image.PNG" alt=""></p>
<p>이번 프로젝트에서는 우선 디자인과 API 작성의 시간을 단축하고자 했습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/d4dac916-4ba5-41c9-9813-395d578be974/image.PNG" alt=""></p>
<p>그래서 디자인에서는 <code>Figma MCP</code>를 활용했습니다.
Figma MCP를 사용하면 컨텍스트 기반으로 <strong>AI가 자동으로 화면을 만들어줍니다.</strong></p>
<p>따라서 피그마를 다루지 못하는 사람도 와이어프레임 제작에 참여할 수 있어서
<strong>서로의 생각을 동기화하는데 유용하게 쓰였습니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/25121681-9d4e-47e1-9682-2bf78e902df0/image.PNG" alt=""></p>
<p>그 다음으로는 <code>V0</code>를 사용했습니다.</p>
<p>V0의 경우 <strong>Next, tailwindCSS를 기반으로 코드가 작성되고 바로 인터렉션이 가능한 프로토타입을 생성</strong>하기 때문에 별다른 러닝커브 없이 빠르게 적용 가능해서 선택했습니다.</p>
<p>또한, 회의를 통해 <strong>기획 변경이 발생하더라도 프롬프팅으로 빠르게 수정이 가능합니다.</strong>
<strong>프로토타입을 제작하는데 가장 효과적인 툴</strong>이었습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/a2a6e139-8fd5-40ca-9473-a96cda4c74c4/image.PNG" alt=""></p>
<p>앞서 FigmaMCP와 V0로 만든 프로토 타입을 활용해서 API 명세서를 작성하였습니다.</p>
<p>이전 프로젝트에서는 로우 피델리티(도형과 텍스트로만 구성된 와이어 프레임)으로 API 명세서를 작성했습니다.</p>
<p>이로 인해 <strong>백엔드와 프론트엔드의 생각이 달라 개발을 하면서 계속 수정해야 하는 상황이 발생</strong>했습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/c61e2683-e81f-434d-a423-0fc158702cab/image.PNG" alt=""></p>
<p>하지만, 인터렉션이 가능한 프로토타입을 활용하니 <strong>어떤 컴포넌트에서 어떤 요청을 보내야하는지 명확해졌고, API 명세서 작성 속도가 향상되었습니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/26dff9c6-87b7-4f32-afa3-a934a62e0209/image.PNG" alt=""></p>
<p>따라서 이전 프로젝트에서 3일 걸리던 작업이 <strong>Figma MCP, V0 도입 후 하루만에 끝날 정도로 개발 생산성이 많이 올라가게 됐습니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/df865916-0786-4d90-9fe1-abd7ba8aa264/image.PNG" alt=""></p>
<p>그 다음에는 백엔드가 완성되지 않더라도 <strong>프론트엔드 작업을 병렬적으로 진행</strong>할 수 있는 방법에 대해 모색했습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/d58aa276-e4b2-4fbe-80c5-a8cd9de4728d/image.PNG" alt=""></p>
<p>일반적으로 프론트가 백엔드 서버에 요청을 보내고, 백엔드에서는 비즈니스 로직을 처리한 뒤 프론트로 응답을 보냅니다.</p>
<p>프론트는 받은 응답을 바탕으로 컴포넌트를 렌더링하죠.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/1b1f2f2a-b678-44b4-82bf-ea8af1afc72d/image.png" alt=""></p>
<p>요청을 보내고, 응답이 와야 렌더링 되는 구조로 인해 <strong>백엔드에 대한 의존이 발생</strong>하게 됩니다.</p>
<p>따라서 이 의존을 없애면 프론트와 백엔드가 병렬적으로 작업을 진행할 수 있다고 판단했습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/ea08af06-c8e8-4636-aa13-dad348e5ad60/image.png" alt=""></p>
<p>저희가 이번에 도입한 것은 <code>MSW</code> 입니다.</p>
<p>사용자가 fetch()를 호출하면 브라우저 Web API가 Request를 생성 및 스케쥴링합니다.</p>
<p>그 뒤 현재 페이지가 해당 서비스 워커에 의해 컨트롤되고 있고, 요청 URL이 서비스 워커의 scope 안에 있으면 브라우저가 fetch 이벤트를 서비스 워커로 디스패치합니다.</p>
<p>MSW가 등록되어 있다면, MSW는 등록된 핸들러 목록에서 기준으로 요청을 매칭하고, 매칭되면 목업데이터를 반환하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/4ed4a231-a132-4670-9f95-868d00c6049d/image.PNG" alt=""></p>
<p>따라서 MSW 도입으로 <strong>3가지 효과</strong>를 얻었습니다.</p>
<p>첫 번째로는 백엔드와 프론트가 <strong>병렬 개발</strong>을 할 수 있게 되었습니다.</p>
<p>두 번째로는 <strong>에러 관련 응답을 반환</strong>하게 하거나 <strong>응답에 지연을 걸어서 폴백</strong>을 뜨게하는 등 <strong>비동기 처리에 수월</strong>해졌습니다.</p>
<p>마지막으로 vitest 같은 테스트 라이브러리를 쓰게되면 <strong>MSW로 모킹한 데이터를 받을 수 있어 테스트에도 용이</strong>해졌습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/0a894889-b3ab-48f9-a4b5-a9279965a1ff/image.PNG" alt=""></p>
<p>따라서 10일이라는 짧은 개발 기간임에도 불구하고
백엔드가 개발이 완료되어 서버 인스턴스에 배포된 순간 <strong>프론트에도 별다른 조치없이 바로 연동되어 빠른 서비스 배포가 가능했습니다.</strong></p>
<h2 id="마무리">마무리</h2>
<p>AI가 발전되면서 개발에 이를 어떻게 활용하는지에 따라 개발 생산성을 높일 수 있다는 걸 배웠습니다.</p>
<p>Figma MCP → V0 → MSW로 이어지는 흐름을 구축하면서 기획-디자인-프로토타입-명세-구현 과정에서의 개발 시간을 단축할 수 있었습니다.</p>
<p>특히, 백엔드 개발 전에 에러나 폴백 상황을 미리 개발할 수 있었던 게 특히 시간 단축에 도움이 되었던 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내가 나타나 볼게 얍!]]></title>
            <link>https://velog.io/@today-is-first/%EB%82%B4%EA%B0%80-%EB%82%98%ED%83%80%EB%82%98-%EB%B3%BC%EA%B2%8C-%EC%96%8D</link>
            <guid>https://velog.io/@today-is-first/%EB%82%B4%EA%B0%80-%EB%82%98%ED%83%80%EB%82%98-%EB%B3%BC%EA%B2%8C-%EC%96%8D</guid>
            <pubDate>Sun, 14 Sep 2025 17:12:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>안녕하세요, 오늘은 프론트엔드 비동기 처리 핵심 3가지 중 하나인 폴백 처리에 대한 글입니다. 요즘 프로젝트를 하는데 다양한 fallback 처리 방법을 고민하고, 하나씩 적용해보면서 장단점을 정리해봤습니다.</p>
</blockquote>
<p>우선 fallback UI를 왜 써야하는지 간단하게 알아보겠습니다.
웹 성능 측정 지표 중 가장 중요한 CWV 중 <code>CLS</code>라는 항목이 있습니다.</p>
<p>Cumulative Layout Shift는 쉽게 설명하면 페이지 렌더링 이후 얼마나 이동하는지에 대한 측정 지표입니다.</p>
<p>만약에 사용자가 A라는 버튼을 누르고 싶었는데, 광고가 갑자기 렌더링되어 B라는 버튼을 누르게 된다면 UX가 크게 떨어지게 됩니다.</p>
<p>즉, CLS가 높으면 사용자가 의도하지 않는 동작을 유발할 가능성이 높아지게 됩니다.</p>
<p>이를 줄이기 위해, 네트워크 지연이 있는 데이터 UI는 바로 비워두지 않고 <strong>Fallback UI</strong>를 먼저 보여준 뒤 실제 데이터를 채워 넣는 방식을 씁니다.</p>
<p>이렇게 하면 화면 구조가 갑자기 변하지 않아 CLS 안정성은 물론이고 LCP와 UX도 개선됩니다.</p>
<h2 id="다양한-fallback-구현-방법들">다양한 Fallback 구현 방법들</h2>
<h3 id="1-isloading-및-useeffect-사용">1. isLoading 및 useEffect 사용</h3>
<p>우선 굉장히 나이브하게 구현하는 방법부터 함께 보겠습니다.</p>
<p><code>isLoading</code>이라는 상태를 만들고, useEffect를 활용해서 데이터를 패칭할 때는 스켈레톤을 보여주면 됩니다.</p>
<pre><code class="language-jsx">export default function UserInfo() {
  const [userInfo, setUserInfo] = useState&lt;User[] | null&gt;(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() =&gt; {
    let alive = true;
    setIsLoading(true);
    fetchUsers()
      .then((u) =&gt; alive &amp;&amp; setUserInfo(u))
      .finally(() =&gt; alive &amp;&amp; setIsLoading(false));
    return () =&gt; {
      alive = false;
    };
  }, []);

  if (isLoading) return &lt;div&gt;로딩 중… (스켈레톤 UI)&lt;/div&gt;;
  ... 에러 처리 ...

  return (
    ... UI 처리 ...
  );
}
</code></pre>
<p>다만 이렇게 되면, 비동기처리를 하는 다양한 컴포넌트에서 중복된 폴백 처리 코드를 작성하게 됩니다.</p>
<h3 id="2-커스텀-훅-및-datastate">2. 커스텀 훅 및 DataState</h3>
<p>데이터를 패칭하는 비즈니스 로직을 커스텀 훅으로 만들어서 분리해줍니다.
View는 View의 역할만 할 수 있게 headless 패턴을 사용합니다.</p>
<p>useUserInfo =&gt; 데이터 패칭 로직
DataState =&gt; 로딩/에러/정상 UI 분기
UserInfoView =&gt; View 역할</p>
<p>이렇게 3가지 컴포넌트를 활용하여 선언적으로 표현할 수 있습니다.</p>
<pre><code class="language-jsx">// 커스텀 훅
export function useUserInfo() {
  const [userInfo, setUserInfo] = useState&lt;User[] | null&gt;(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState&lt;unknown&gt;(null);

  useEffect(() =&gt; {
    let alive = true;
    setIsLoading(true);
    fetchUsers()
      .then((u) =&gt; alive &amp;&amp; setUserInfo(u))
      .catch((e) =&gt; alive &amp;&amp; setError(e))
      .finally(() =&gt; alive &amp;&amp; setIsLoading(false));

    return () =&gt; {
      alive = false;
    };
  }, []);

  return { userInfo, isLoading, error };
}</code></pre>
<pre><code class="language-jsx">// DataState로 로딩/에러/정상 UI 분기 처리를 캡슐화
function DataState({
  isLoading,
  error,
  loadingFallback,
  errorFallback,
  children,
}: DataStateProps) {
  if (isLoading) {
    return &lt;&gt;{loadingFallback}&lt;/&gt;;
  }

  if (error) {
    if (errorFallback) {
      return &lt;&gt;{errorFallback(undefined, error)}&lt;/&gt;;
    }
    return (
      &lt;div
        role=&quot;alert&quot;
        className=&quot;rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700&quot;
      &gt;
        데이터를 불러오는 중 오류가 발생했습니다.
      &lt;/div&gt;
    );
  }
  return &lt;&gt;{children}&lt;/&gt;;
}</code></pre>
<pre><code class="language-jsx">export default function UserInfoView() {
  const { userInfo, isLoading, error } = useUserInfo();

  return (
    &lt;DataState
      isLoading={isLoading}
      error={error}
      loadingFallback={&lt;div&gt;로딩 중… (스켈레톤 UI)&lt;/div&gt;}
      errorFallback={(reset, err) =&gt; (
        ...errorFallback...
      )}
    &gt;
      &lt;ul&gt;
          {userInfo?.map((u) =&gt; (
            &lt;li key={u.id}&gt;{u.name}&lt;/li&gt;
          ))}
        &lt;/ul&gt;
    &lt;/DataState&gt;
  );
}</code></pre>
<h3 id="react-query--suspense-사용">React Query + Suspense 사용</h3>
<p>요즘 대부분 서버 상태 관리하는 도구로 React Query를 사용하는 것 같습니다. 캐싱을 적절히 사용하면 서버의 부하를 줄일 수 있다는 장점도 있지만, 프론트엔드에서의 비동기 처리를 쉽게 관리할 수 있게 해줍니다.</p>
<p>또한, v5부터 도입된 <code>useSuspenseQuery</code>와 React의 <code>Suspense</code>를 사용하면 쉽게 fallback을 구현할 수 있습니다.</p>
<pre><code class="language-jsx">// ReactQuery
export function useUserInfo() {
  const { data } = useSuspenseQuery({
    queryKey: [&quot;userInfo&quot;],
    queryFn: fetchUserInfo,
  });
  return { userInfo: data };
}</code></pre>
<pre><code class="language-jsx">// View
export default function UserInfoView() {
  const { userInfo } = useUserInfo();

  if (userInfo.length === 0) {
    return &lt;div&gt;표시할 사용자 정보가 없습니다.&lt;/div&gt;;
  }

  return (
    &lt;ul&gt;
      {userInfo.map((u) =&gt; (
        &lt;li key={u.id}&gt;{u.name}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// 상위 컴포넌트
import { Suspense } from &quot;react&quot;;
import UserInfoView from &quot;./UserInfoView&quot;;
import { ErrorBoundary } from &quot;./ErrorBoundary&quot;;

export default function App() {
  return (
    &lt;ErrorBoundary&gt;
      &lt;Suspense fallback={스켈레톤 UI}&gt;
        &lt;UserInfoView /&gt;
      &lt;/Suspense&gt;
    &lt;/ErrorBoundary&gt;
  );
}
</code></pre>
<hr>
<h3 id="맺음말">맺음말</h3>
<p>fallback을 어떤 식으로 구현하는 게 좋을까 고민하다가 가장 리액트스러운 코드를 찾으려고 했습니다. 맨 밑의 방법이 가장 선언적으로 작성되었다고 생각이 들어 현재 프로젝트에 채택하였습니다.</p>
<p>사실 그동안 관성적으로 <code>useQuery</code>만 써봤지 <code>useSuspenseQuery</code>가 도입되었는지도 몰랐는데 이번 기회에 알게 되어서 좋습니다.</p>
<p>isLoading이나 error를 받을 필요없이 자동으로 던져주니까 코드가 훨씬 깔끔해지는 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[언마운트/리마운트 시 미디어 스트림 끊김 현상 해결]]></title>
            <link>https://velog.io/@today-is-first/%EC%96%B8%EB%A7%88%EC%9A%B4%ED%8A%B8%EB%A6%AC%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%8B%9C-%EB%AF%B8%EB%94%94%EC%96%B4-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EB%81%8A%EA%B9%80-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@today-is-first/%EC%96%B8%EB%A7%88%EC%9A%B4%ED%8A%B8%EB%A6%AC%EB%A7%88%EC%9A%B4%ED%8A%B8-%EC%8B%9C-%EB%AF%B8%EB%94%94%EC%96%B4-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EB%81%8A%EA%B9%80-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Thu, 04 Sep 2025 03:55:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>안녕하세요, 저번에 <a href="https://velog.io/@today-is-first/%EB%A9%B4%EC%A0%91-%EC%84%B8%EC%85%98-%EC%9E%AC%EC%97%B0%EA%B2%B0-%EC%A0%84%EB%9E%B5">면접 세션 재연결 전략</a>에 대해서 소개해드린 바 있는데, 이 때 발생했던 WebRTC 사용자간 <strong>미디어 스트림 끊김 현상을 해결</strong>한 과정을 소개드리겠습니다.</p>
</blockquote>
<h2 id="문제-상황">문제 상황</h2>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/9bb810bf-8d9f-4adf-936e-fcf2ac5329c4/image.png" alt=""></p>
<p>우선 WebRTC로 서로 간의 면접을 진행하다가 <strong>네트워크 장애가 발생하거나 사용자가 새로고침을 누르는 등의 행위를 하면 자동 재연결 프로세스를 통해 세션을 복구</strong>하게 됩니다.</p>
<p>다만, 세션이 복구되더라도 WebRTC를 통해 P2P로 주고 받던 <strong>미디어 스트림이 끊기는 문제가 발생</strong>했습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/1d5fc6f1-1eac-46e8-8ab0-c53fb3cc8dff/image.png" alt=""></p>
<p>사용자에게 흔히 발생하는 <strong>네트워크 변동과 새로 고침 상황에서 미디어 스트림이 끊기는 원인을 분석</strong>했습니다.</p>
<p>네트워크가 <strong>유선에서 무선 와이파이로 변하는 등의 상황</strong>에서는 <strong>기존의 ICE Candidate이 무효화</strong>되어 연결이 끊기게 됩니다.</p>
<p><strong>새로고침 시 기존 PeerConnection 및 MediaStream 객체가 브라우저 컨텍스트에서 모두 소멸</strong>되며, <strong>재마운트 시 새로운 객체로 생성되므로 기존 P2P 연결이 끊기게 됩니다.</strong></p>
<p>또한, 동시에 <strong>부모 컴포넌트 상태 변경으로 인한 자식 컴포넌트의 리렌더링으로 인해 컴포넌트 리렌더링으로 초기화 로직이 경쟁적으로 실행되며 Race Condition</strong>도 존재하였습니다.</p>
<h2 id="해결-과정">해결 과정</h2>
<p>우선, <strong>미디어 스트림을 관리하는 스토어에 비동기 작업을 공유하는 로직을 추가</strong>했습니다.</p>
<pre><code class="language-typescript">interface MediaStreamState {
    ... 미디어 상태...
  // 준비 상태/에러
  isReady: boolean;
  error: string | null;

  // 내부 대기자
  _readyPromise: Promise&lt;MediaStream&gt; | null;
  _resolveReady?: (s: MediaStream) =&gt; void;
  _rejectReady?: (e: unknown) =&gt; void;
}</code></pre>
<p><strong>deferred promise 패턴</strong>을 사용하여 <strong>프로미스 외부에서 resovle와 reject를 할 수 있도록</strong> <code>createDeferred</code> 함수를 만들었습니다.</p>
<pre><code class="language-typescript">function createDeferred&lt;T&gt;() {
  let resolve!: (v: T) =&gt; void;
  let reject!: (e: unknown) =&gt; void;
  const p = new Promise&lt;T&gt;((res, rej) =&gt; {
    resolve = res;
    reject = rej;
  });
  return { p, resolve, reject };
}</code></pre>
<p>이후 <strong>미디어 스트림을 초기화 할 때 deffered promise 패턴을 사용</strong>하였습니다.</p>
<pre><code class="language-typescript">initMyStream: async () =&gt; {
      const { myStream, _readyPromise } = get();
      // 스트림이 이미 있으면 반환
      if (myStream) return myStream;
      // 스트림을 가져오는 비동기 작업이 진행 중이면 프로미스 반환
      if (_readyPromise) return _readyPromise;
      // deferred promise 패턴을 사용하여 프로미스와 resolve, reject 생성
      const { p, resolve, reject } = createDeferred&lt;MediaStream&gt;();
      set({
        _readyPromise: p,
        _resolveReady: resolve,
        _rejectReady: reject,
        error: null,
      });
      ... const stream = 미디어 가져오는 로직 ...
        // 스트림을 가져오면 resolve하여 대기 중인 호출자에게 스트림 반환
        resolve(stream);
        return p;
      } ... 에러 처리 ...</code></pre>
<p>이로써 언마운트 / 앱 마운트 발생 시 <strong>리렌더링으로 인한 중복 호출 시에도 하나의 공유된 Promise 인스턴스를 상태에 유지</strong>함으로써, <strong>중복 호출 시에도 동일한 비동기 결과를 참조하게 되어 초기화 과정이 일관성을 유지</strong>하게 됐습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/35b230bb-ce54-457d-bc8b-15d142421eb3/image.png" alt=""></p>
<p>이후 사용자 간 <strong>기존 Candidate이 무효화되면 새롭게 수집한 Candidate을 교환하여 연결을 복원</strong>하였습니다.</p>
<h2 id="결과">결과</h2>
<p>새로운 탭으로 재접근 혹은 새로고침같은 <strong>언마운트 / 재마운트 상황에서도 서로의 미디어 스트림을 확인</strong>할 수 있게 되었고, WebRTC 재연결 로직을 추가하여 <strong>안정적인 서비스를 제공</strong>할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/178b3c8e-94d1-4115-b44f-9d05d6c78ff3/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[꼬리 프로젝트 회고]]></title>
            <link>https://velog.io/@today-is-first/%EA%BC%AC%EB%A6%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@today-is-first/%EA%BC%AC%EB%A6%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 01 Sep 2025 01:46:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>싸피 2학기의 첫 번째 공통 프로젝트가 종료되었다. 두 번째 프로젝트의 시작하기 전에 회고를 해보려고 한다.</p>
</blockquote>
<h3 id="프로젝트-멤버-구성">프로젝트 멤버 구성</h3>
<p>공통 프로젝트는 <strong>싸피에서 임의로 정해준 반 내에서 팀을 구해야했다.</strong> 다만, 나는 웹 관련 프로젝트를 진행하고 싶었는데 임베디드를 전공한 사람들과 같은 반에 배정되어 팀 구성에 어려움이 많았다.</p>
<p>싸피에는 팀 구성할 때 비전공자/전공자 비율을 맞춰야 하는 조건이 있다. 그러다보니 <strong>웹을 희망하는 비전공 백엔드와 웹을 희망하지만 처음 해보는 임베디드 전공자</strong>로 팀을 이루게 되었다.</p>
<h3 id="프로젝트-목표">프로젝트 목표</h3>
<p>팀을 리딩하게 되면서 우리가 어떤 걸 목표로 삼아야할까 고민이 많았다.</p>
<p>그래서 정해진 게 <code>기능 목록을 최소화하되, 디테일한 개발을 해보자</code>였다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/609e4dce-cf74-48b8-8089-7e74a6450bcb/image.png" alt=""></p>
<p>내가 생각한 <code>디테일한 개발</code>은 두 가지의 의미였다.</p>
<p><strong>첫 번째로, 돌아가기만 하는 기능을 AI로 찍어내지 않는 것</strong></p>
<p><strong>두 번째로, 사용자에게 제공되는 서비스라고 가정하고 고도화하려고 노력하는 것</strong></p>
<p>디테일한 개발을 하기 위해서는 <strong>전제 조건</strong>이 필요했다.</p>
<p><strong>문제를 발견할 수 있고 이를 해결할 수 있는 실력이다.</strong></p>
<p>다만, 우리 팀은 웹을 처음하는 사람들도 있었고 기초가 부족한 부분이 많아서 이를 극복했어야 했다.</p>
<h3 id="기초-다지기">기초 다지기</h3>
<p>우리 팀이 기초를 다지고 실력을 키우기 위해 노력한 것은 두 가지이다.</p>
<p><strong>첫 번째로, 딥다이브이다.</strong></p>
<p>매일 아침 데일리 스크럼이 끝나고 2시간 정도 언어나 프레임워크 혹은 프로젝트에 쓰이는 기술을 학습하는 시간을 가졌다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/14197247-1f1a-4363-9458-39f65f42ff2f/image.png" alt=""></p>
<p>예를 들어, 클로저를 학습한다고 가정하면 개념에 이어서 추가적으로 연관된 지식을 학습하는 방식으로 진행했다.</p>
<p>프로젝트 기간 동안 프론트 파트는 190+개, 백엔드는 160+개가 넘는 질문에 답을 달며 학습했다.</p>
<p><strong>두 번째로, 기술 블로그 작성이다.</strong></p>
<p>매 주 1회 기술 블로그 작성을 하며, 프로젝트 내에 쓰이는 기술이나 진행하며 겪은 문제/해결 과정을 기록하고 서로 공유하는 문화를 만들었다.</p>
<p>처음에는 시간을 너무 많이 잡아먹나 고민이 있었지만,
오히려 조금씩 기초가 다져지면서 후반부에 속도가 훨씬 빨라지는 걸 느낄 수 있었다.</p>
<p>프로젝트가 끝나고 모두가 공통적으로 딥다이브 시간에 공부하고, 기술 블로그를 작성하면서 성장한 게 많았다고 얘기했고, 나도 그렇게 느꼈다.</p>
<hr>
<blockquote>
<p>프로젝트를 진행하며 있었던 작은 해프닝들을 모아봤다.</p>
</blockquote>
<h3 id="해프닝-1---모래로-집을-짓지-말자">해프닝 1 - 모래로 집을 짓지 말자</h3>
<p>우리 팀은 개발에 들어가기 전 충분한 학습을 권장했다.</p>
<p>OAuth 로그인을 맡은 두 명에게 로그인에 필요한 지식을 학습하도록 하고, 5일정도 지나고 작성한 로직을 팀원에게 공유하는 시간을 가졌다.</p>
<p>여기서 문제가 발생했는데, 소셜 로그인인데 OIDC를 사용하지 않는 로직을 짜왔다. 간단하게 설명하면 <strong>로그인 로직에 인증 과정이 없는 것과 같다.</strong></p>
<p>어쩌다 이렇게 됐을까 디버깅을 해본 결과 <strong>잘못된 정보를 제공하는 블로그와 AI의 합작품이었다.</strong></p>
<p>그래서 팀원 분들에게 <strong>모래로 집을 짓지 말고, 옆집에서 공짜로 나눠주는 벽돌로 집을 지어라</strong>는 주제로 강연을 하게 됐다.</p>
<blockquote>
<p>그 때 했던 내용을 간단하게 작성하겠다.</p>
</blockquote>
<p>우리가 만들어야 하는 서비스를 집을 짓는 거라고 생각해보자.</p>
<p>우리는 집을 지을 것이다.
<strong>다만, 우리는 집을 지어본 경험도 부족하고, 집을 잘 짓는 방법도 모른다.</strong></p>
<p>지금 상황에서 우리가 짓는 집은 <strong>파도 한번에 무너지는 해변가의 모래성과 같다.</strong></p>
<p>이렇게 지은 집을 다른 사람에게 거주하라고 권유할 수 있을까?
그 사람은 마음 놓고 우리가 지은 집을 이용할 수 있을까?</p>
<blockquote>
<p>그렇지 않다.
그러면 우리는 어떻게 집을 잘 지을 수 있을까</p>
</blockquote>
<p>주변을 둘러보자
해변가 옆 가게에 각자의 연장통을 들고 있는 사람들이 줄 서있는 게 보인다.</p>
<p>남여노소를 가리지 않고 줄을 서있고, 심지어 업계에서 유명한 건축가도 거기에 서있다.</p>
<p>가게 입구를 보니, 사람들이 벽돌과 설계도를 받아 나오는 게 보인다. 심지어 가게 앞 푯말에는 <code>무료</code>라고 적혀있다.</p>
<p>이 가게의 이름은 <strong>&quot;공식 문서&quot;</strong>이다.</p>
<p><strong>건축에 대해서 가장 잘 아는 사람들이 벽돌과 설계도를 무료로 나눠주고 있다.</strong> 파도 한번에 무너지는 모래로 집을 필요가 없다.</p>
<p>다만, <strong>AI의 도입으로 사람들은 벽돌을 받으려 줄을 서지 않으려 한다.</strong> 잘 정리되어 있는 다른 사람의 블로그를 보거나 GPT한테 물어보지만 팩트 체크를 하지 않는다. 공식 문서 작성이 잘되어 있는 경우엔 오히려 공식 문서를 읽어보는 게 훨씬 빠를 때도 있다는 걸 간과한다.</p>
<blockquote>
<p>이번 문제도 그러했다.</p>
</blockquote>
<p>카카오 OAuth의 경우 공식문서가 워낙 잘 되어 있어서 공식 문서를 봤다면 소셜 로그인에서 OIDC를 안쓰는 로직을 짜지 않았을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/00b75f01-8542-4764-bbb9-d866f37b72b0/image.png" alt="카카오 OAuth 로그인 로직"></p>
<p>모래로 집 짓지 말고, 공식 문서에서 무료로 나눠주는 벽돌로 집을 짓자</p>
<h2 id="맺음말">맺음말</h2>
<p>배운 것도 많고, 느낀 것도 많은 프로젝트였다.</p>
<p>간단한 프로젝트이다 보니 스키마만 같이 구성하면 CRUD API는 백엔드가 알아서 짤 수 있을 거라 생각했는데, 내 생각과 백엔드의 생각이 완전히 달랐던 경우가 있어서 API 명세서의 중요성을 다시금 느꼈다.</p>
<p>TDD로 인해서 초기에 시간을 많이 잡아먹었지만, 오히려 리팩토링이 진행될수록 시간이 절약되는 걸 느꼈다.</p>
<p>다음 프로젝트에서는 AI를 개발에 활용해서 와이어 프레임이나 E2E테스트, 코드 리뷰 등에 활용해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[면접 세션 재연결 전략]]></title>
            <link>https://velog.io/@today-is-first/%EB%A9%B4%EC%A0%91-%EC%84%B8%EC%85%98-%EC%9E%AC%EC%97%B0%EA%B2%B0-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@today-is-first/%EB%A9%B4%EC%A0%91-%EC%84%B8%EC%85%98-%EC%9E%AC%EC%97%B0%EA%B2%B0-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Wed, 20 Aug 2025 06:25:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>안녕하세요, 이번에는 프로젝트를 진행하며 가장 UX적으로 안좋다고 느껴졌던 면접 세션 중단 상황을 개선한 과정을 소개드리겠습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/b94c13fc-38f0-4775-9c2e-ac3e04c7b27c/image.PNG" alt=""></p>
<p>MVP 모델을 처음 개발하고 <strong>1대1로 면접을 연습하는 기능을 테스트 하는 과정</strong>에서 갑자기 <strong>서로의 연결이 끊기는 상황이 발생</strong>했고, 다시 면접 세션을 만들어야 하는 상황이 발생했습니다.</p>
<p>결국 그동안의 기록은 사라지고 <strong>처음부터 서비스를 이용해야 하는 경험은 불편</strong>했고, 실제 서비스라면 <strong>많은 유저 이탈</strong>이 될 거라 예상했습니다.</p>
<p>이후 백엔드 로그를 보면서 디버깅을 해본 결과 스터디원 분이 까페에서 서비스를 이용하다보니 <strong>와이파이가 불안정해서 웹소켓 연결이 끊겨 발생한 문제</strong>였습니다.</p>
<p>WebRTC를 통해 면접 세션을 제공해야 하는 서비스 입장에서 가장 중요한 것은 <strong>다양한 상황에서도 안정적으로 서비스를 이용</strong>할 수 있게 하는 것이라는 생각에 <strong>재연결 로직을 구현</strong>하게 되었습니다.</p>
<blockquote>
<p>총 <strong>세 가지 상황을 가정</strong>했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/cc685dc5-e411-4649-9880-8a49b0c7878c/image.PNG" alt=""></p>
<p><strong>첫 번째로는 위의 상황과 같은 네트워크 변동입니다.</strong></p>
<p><strong>두 번째로는 유저가 새로고침을 하는 상황입니다.</strong>
웹 사이트가 잘 동작하지 않는다는 생각이 들면 새로고침부터 눌러보는 유저에 대응하였습니다.</p>
<p><strong>세 번째로는 동일 유저가 새 탭으로 재접근 하는 상황입니다.</strong>
웹 사이트가 잘 동작하지 않는다는 생각이 들면 새로고침부터 눌러보는 유저가 있는데 이래도 제대로 동작하지 않으면 새 탭으로 재접근을 시도하게 됩니다. 이를 대비하였습니다.</p>
<blockquote>
<p>기존 로직은 <code>사용자1</code>이 방을 만들고 <code>사용자2</code>가 방에 입장하면 사용자끼리 WebRTC의 오퍼와 엔서를 주고 받으며 1대1 화상 연결을 진행하게 됩니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/cf9b27ad-5116-423d-9312-f5db692e9081/image.PNG" alt=""></p>
<blockquote>
<p>이후 면접 세션 관련 이벤트를 주고 받으며 면접을 진행하게 되는데, 이 때 네트워크 장애가 발생하게 되면 <strong>재연결 로직이 없어서 처음부터 다시 진행</strong>해야 했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/4f347c1d-ff82-46c5-8b10-9ca056be8dc9/image.PNG" alt=""></p>
<blockquote>
<p>이를 개선한 로직은 다음과 같습니다.
면접 세션의 <strong>핵심 페이즈의 상태를 저장</strong>해서 <strong>네트워크 장애가 발생</strong>하게 되면</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/cc50b5f4-d162-4967-8365-9131cd6396e8/image.PNG" alt=""></p>
<blockquote>
<p>사용자는 <strong>방 재참가 시도</strong>를 하게 되고 서버에서 <strong>자동 재연결 프로세스를 처리</strong>하게 됩니다. <strong>중복 접속 감지 및 새 연결 우선 허용한 이후에 상태 복구 메세지를 전송</strong>하여 <strong>클라이언트가 세션을 복구</strong>할 수 있게 했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/5add2e93-84d2-4f91-ab2d-723bcb9a39f4/image.PNG" alt=""></p>
<h2 id="결과">결과</h2>
<blockquote>
<p>영상을 보면 네트워크를 변동하더라도 다음 질문 선택에 대한 세션을 그대로 유지할 수 있는 걸 확인할 수 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/d47b8544-bd2a-4a50-9d98-1f1e718163d8/image.gif" alt=""></p>
<blockquote>
<p>마찬가지로 새로고침을 눌러도 세션이 유지됩니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/66e5bfa8-9f4d-4dc5-a06b-5019e9361e05/image.gif" alt=""></p>
<blockquote>
<p>마지막으로 새로운 탭으로 접근하게 되어도 세션을 그대로 유지하는 걸 확인할 수 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/bfc5bd84-13bb-4c9c-90b4-64df4298475f/image.gif" alt=""></p>
<h2 id="느낀-점">느낀 점</h2>
<p>이번 경험을 통해 프론트엔드 개발에서 기술적인 완성도만큼이나 <strong>사용자 경험이 중요</strong>하다는 점을 크게 깨달았습니다. 단순히 화면을 구현하는 것이 아니라, 서비스 이용 과정에서 <strong>사용자가 불편을 느낄 수 있는 지점을 파악하고 해결</strong>하는 것이 진정한 <strong>프론트엔드 역할</strong>이라는 것을 느꼈습니다.</p>
<p>네트워크 변동·새로고침·재접속 같이 <strong>실제 사용자 경험에 큰 영향을 미치는 상황을 최대한 대비</strong>해야겠다 생각이 들었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Core Web Vital 개선기]]></title>
            <link>https://velog.io/@today-is-first/Core-Web-Vital-%EA%B0%9C%EC%84%A0%EA%B8%B0</link>
            <guid>https://velog.io/@today-is-first/Core-Web-Vital-%EA%B0%9C%EC%84%A0%EA%B8%B0</guid>
            <pubDate>Wed, 20 Aug 2025 04:37:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오늘은 웹 성능의 가장 중요한 지표로 사용되는  Core Web Vital을 개선한 과정에 대해서 설명드리겠습니다.</p>
</blockquote>
<blockquote>
<p>코어 웹 바이탈은 구글이 2020년 이후부터 검색 순위 알고리즘에 반영 중인 SEO 핵심 지표라고 할 수 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/a0cb9c4d-c554-4e8a-9d80-2f9f12a61ad3/image.PNG" alt=""></p>
<blockquote>
<p><strong>코어 웹 바이탈</strong>은 총 3가지로 구성되어 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/3361726b-ed66-49b9-8332-cda62cdf26c4/image.PNG" alt=""></p>
<p><strong>Largest Contentful Paint</strong>는 최대로 의미 있는 페인트가 완료될 때까지의 시간인데, 화면에서 가장 큰 콘텐츠의 페인트가 완료되는 시점이라고 생각하셔도 됩니다.</p>
<p><strong>First Input Delay</strong>는 렌더링 이후 사용자가 처음으로 상호작용할 때까지 걸리는 지연입니다.</p>
<p><strong>Cumulative Layout Shift</strong>는 누적 레이아웃 시프트라고도 부르고 렌더링 이후 레이아웃의 변화를 감지하는 지표입니다. 예를 들어, 로딩 시 스켈레톤 UI를 적용하면 개선할 수 있습니다.</p>
<blockquote>
<p>이 중 <strong>최대로 의미있는 페인트가 완료될 때까지의 시간을 의미하는 LCP를 개선한 과정</strong>을 설명드리겠습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/7533a337-7f7c-4173-bc75-4541fe8a9341/image.PNG" alt=""></p>
<blockquote>
<p>이 페이지에서 <strong>최대 컨텐츠(가장 의미 있는 요소)</strong>는 무엇일까요?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/43614f10-d9a2-42cd-8c11-40f01a02b0c5/image.PNG" alt=""></p>
<blockquote>
<p>오른쪽에 있는 양복을 입은 여우의 이미지입니다.
LCP의 대상인 <strong>오른쪽 이미지를 얼마나 빨리 로딩하느냐</strong>에 따라 <strong>사용자가 사이트 반응성을 체감</strong>하게 됩니다..</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/0f0f857f-e430-4e63-b8bf-bed066dd5153/image.PNG" alt=""></p>
<blockquote>
<p>이거는 개선 전 라이트 하우스 지표입니다.
여기서 주목해야 할 점은 LCP가 2.5초 걸렸다는 겁니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/270f53a8-eddc-45dc-90a9-19a5d5d54807/image.PNG" alt=""></p>
<blockquote>
<p>구글은 <strong>LCP가 2.5초 이상이면 개선이 필요</strong>하다고 밝혔는데요
저희 사이트 메인 페이지의 LCP는 <strong>개선이 필요한 상태</strong>였습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/6e0c08d5-5bc7-447a-a041-d246d7a9a9a7/image.PNG" alt=""></p>
<blockquote>
<p>LCP의 구성요소는 다음과 같습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/feb29d1b-fedb-45af-9018-b3f694fdc11c/image.PNG" alt=""></p>
<blockquote>
<p>핵심만 따져보면 가장 중요한 포인트는 로드 시간을 줄여서 최대한 컨텐츠가 빨리 뜨게 만드는 것입니다. </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/ac5a0949-09ef-4466-8180-6179891a4704/image.PNG" alt=""></p>
<p>다음과 같은 여러 가지의 기법이 사용됩니다.</p>
<ol>
<li><p>이미지를 캐싱해서 빠르게 다운 받을 수 있게 하는 CDN 사용</p>
</li>
<li><p>이미지의 포맷이나 크기를 조정</p>
</li>
<li><p>폰트나 이미지를 사용 전에 백그라운드에서 프리로드</p>
</li>
<li><p>코드 스플리팅을 통해 초기 로드 크기를 줄이고, 필요한 시점에 모듈을 지연 로드</p>
</li>
</ol>
<blockquote>
<p>저희 사이트에는 다음과 같은 3가지의 시도를 했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/2c922901-bebe-4a90-9a4c-559959ae74e8/image.PNG" alt=""></p>
<p>우선 이미지 파일의 포맷을 압축률이 높은 포맷으로 바꾼 후 프리로드를 적용했습니다.</p>
<p>두 번째로 리액트의 레이지와 Vite의 라이브러리 모듈화를 적용했습니다. 다만, 저희 서비스 특성상 초기 렌더링에 큰 이점이 없어 롤백했습니다.</p>
<img src="https://velog.velcdn.com/images/today-is-first/post/a38bcd11-06da-4ba8-a92f-5e32cae2a525/image.png" style="width:400px">


<blockquote>
<p>결과적으로 <strong>퍼포먼스 100점, SEO 100점을 달성</strong>할 수 있었습니다. 또한 <strong>LCP를 2.5초에서 0.6초로 개선</strong>할 수 있었습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/e0ab34d0-1b80-471e-a722-7daea881b6ea/image.PNG" alt=""></p>
<h2 id="결론">결론</h2>
<p><strong>이미지 포맷 최적화 및 프리로드 적용을 통해 가장 큰 컨텐츠인 메인 이미지의 로딩 속도를 개선</strong>했습니다.</p>
<p>또한 지속적인 성능 측정으로 불필요한 시도는 과감히 롤백하며 핵심 개선 포인트에 집중했습니다.</p>
<p>그 결과, <strong>LCP를 2.5초에서 0.6초로 단축</strong>하고, 라이트하우스 지표에서 <strong>퍼포먼스와 SEO 100점</strong>을 달성할 수 있었습니다.</p>
<p>이를 통해 사용자는 사이트에 진입하자마자 빠른 반응성을 체감할 수 있다는 피드백을 받았고, UX를 개선할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[추상화는 한 끗 차이]]></title>
            <link>https://velog.io/@today-is-first/%EC%B6%94%EC%83%81%ED%99%94%EB%8A%94-%ED%95%9C-%EB%81%97-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@today-is-first/%EC%B6%94%EC%83%81%ED%99%94%EB%8A%94-%ED%95%9C-%EB%81%97-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Wed, 06 Aug 2025 15:54:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/today-is-first/post/a05f379d-afe6-4b44-be47-89ad2f710bee/image.png" alt=""></p>
<blockquote>
<p>오늘은 &quot;적절한 추상화는 무엇일까?&quot; 고민하면서 코드 리팩토링한 과정을 코드와 함께 공유드리겠습니다.</p>
</blockquote>
<h2 id="추상화하여-공통-컴포넌트-만들기">추상화하여 공통 컴포넌트 만들기</h2>
<img src="https://velog.velcdn.com/images/today-is-first/post/3680ac09-6a61-4664-9ddd-6870c5daff8c/image.gif" style="height:700px">

<p>위 영상처럼 채팅을 치면 자동으로 스크롤이 밑으로 내려가는 컴포넌트가 있습니다.
해당 기능이 다른 컴포넌트에서도 사용하게 되어 추상화하여 공통 훅으로 만들어야 했습니다.</p>
<pre><code class="language-tsx">// Before
const useScrollToBottom = () =&gt; {
  const messages = useChattingWindowStore(state =&gt; state.messages);
  useLayoutEffect(() =&gt; {
    const list = document.querySelector(&#39;[aria-label=&quot;scrollable-list&quot;]&#39;);
    list?.scrollTo({ top: list.scrollHeight, behavior: &#39;instant&#39; });
  }, [messages]);
  return messages;
};</code></pre>
<p>다만 현재 코드는 <code>messages</code>라는 상태에 강하게 결합되어 범용적인 스크롤 하단 이동 훅으로 추상화하지 못했습니다.</p>
<pre><code class="language-tsx">// After
const useScrollToBottom = ({
  dependencies,
  scrollRef,
}: useScrollToBottomProps) =&gt; {
  useLayoutEffect(() =&gt; {
    const scroll = scrollRef.current;
    scroll?.scrollTo({ top: scroll.scrollHeight });
  }, dependencies);
};

// 사용 예시
function ScrollableList({ children }: ScrollableListProps) {
  const listRef = useRef&lt;HTMLUListElement&gt;(null);
  useScrollToBottom({
    dependencies: [children],
    scrollRef: listRef,
  });

  return (
    &lt;ul
      className=&quot;h-full w-full overflow-y-auto px-5&quot;
      aria-label=&quot;scrollable-list&quot;
      ref={listRef}
    &gt;
      {children}
    &lt;/ul&gt;
  );
}</code></pre>
<p>우선, <code>useScrollToBottom</code> 훅에서의 <code>messages</code> 의존을 제거하였습니다.
<strong>useLayoutEffect</strong>의 의존성을 주입받고, 리렌더링 되어도 항상 같은 값을 참조하는 useRef를 사용하여 안정성을 높였습니다.</p>
<p>따라서 <code>useScrollToBottom</code> 훅은 컨텐츠가 추가되어도 자동으로 최하단이 보여야 하는 모든 컴포넌트에서 사용 가능한 훅으로 개선되었습니다.</p>
<h2 id="과한-추상화-되돌리기">과한 추상화 되돌리기</h2>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/39a93646-fef7-44fd-bcda-ad5b9532b00f/image.png" alt=""></p>
<p>프로젝트 내 채팅창에는 4가지 형태의 메세지가 보여져야 합니다.</p>
<ul>
<li>면접 질문</li>
<li>면접 답변</li>
<li>내 채팅</li>
<li>상대방 채팅</li>
</ul>
<p>처음 프로젝트 개발 당시에는 해당 메세지가 단순히 보여지는 형태만 다를 거라고 생각했습니다.</p>
<p>그래서 이 네 가지 메시지를 하나의 컴포넌트인 <code>NameTaggedMessage</code>로 구현해 재사용하려 했습니다.</p>
<pre><code class="language-tsx">function NameTaggedMessage({ sender, message, type }: NameTaggedMessageProps) {
  const isRightAligned = type === CHAT_TYPES.ANSWER || type === CHAT_TYPES.USER;

  return (
    &lt;div className={`flex flex-col ${isRightAligned ? &#39;items-end&#39; : &#39;items-start&#39;} mb-3`}&gt;
      &lt;div className=&quot;mb-1 text-xs text-gray-400&quot;&gt;{sender}&lt;/div&gt;
      &lt;div
        role=&quot;listitem&quot;
        aria-label=&quot;name-tagged-message&quot;
        className={`max-w-[75%] rounded-2xl px-4 py-2 text-sm break-words whitespace-pre-wrap shadow-sm ${chatStyleMap[type]} ${
          isRightAligned
            ? &#39;rounded-tl-2xl rounded-tr-md rounded-bl-2xl&#39;
            : &#39;rounded-tl-md rounded-tr-2xl rounded-br-2xl&#39;
        }`}
      &gt;
        {message}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>다만 실제 서버에서는 메시지 타입이 <code>Question, Answer, Chat</code>의 세 가지로만 구분되어 전달되었고, 그 중 <code>Chat</code> 타입은 메시지를 보낸 <strong>sender가 나인지 아닌지를 비교해</strong> 내 채팅인지 상대방 채팅인지 클라이언트에서 직접 판단해야 했습니다.</p>
<p>이로 인해 <code>NameTaggedMessage</code> 컴포넌트는 다음과 같은 이중 분기 구조를 가지게 되었습니다.</p>
<ul>
<li><code>1차 분기</code>: 메시지의 type에 따라 스타일/정렬 결정</li>
<li><code>2차 분기</code>: Chat 타입일 경우, sender가 나인지 비교하여 정렬/색상 분기</li>
</ul>
<p>그래서 이를 다음과 같이 두 개의 역할 중심 컴포넌트로 분리하였습니다:</p>
<ul>
<li><code>NameTaggedMessage</code>: 일반 채팅 메시지 전용 컴포넌트</li>
<li><code>QuestionAnswerMessage</code>: 면접 질문/답변 전용 컴포넌트</li>
</ul>
<p>각 컴포넌트는 자신이 다루는 메시지 유형에만 집중할 수 있게 되어, 내부 분기 로직이 단순해지고 UI 표현도 명확해졌습니다.</p>
<pre><code class="language-tsx">function NameTaggedChatMessage({ message }: NameTaggedChatMessageProps) {
  const { sender, text, isMyMessage = false } = message;

  return (
    ...
  );
}

function QuestionAnswerMessage({ message }: { message: Message }) {
  const { sender, text, type } = message;
  const isQuestion = type === CHAT_TYPES.question;

  return (
    ...
  );
}</code></pre>
<p>UI가 비슷하다는 이유로 하나의 컴포넌트로 묶었지만, 실제로는 표현해야 하는 맥락이 다른 메세지들이었습니다. 실제 컴포넌트의 역할을 기준으로 분리하는 게 중요하다는 걸 배우게 된 것 같습니다.</p>
<h2 id="맺음말">맺음말</h2>
<p>코드의 재사용만 생각하면서 구현을 하다가 오히려 좋지 않은 추상화를 하게 되었습니다.</p>
<p>차라리 가벼운 코드 중복은 눈 감아주고 3군데 이상 공통적으로 쓰이게 될 경우 기능을 기준으로 추상화하고자 마음 먹었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[소잃고 폴더 구조 고치기]]></title>
            <link>https://velog.io/@today-is-first/%EC%86%8C%EC%9E%83%EA%B3%A0-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0-%EA%B3%A0%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@today-is-first/%EC%86%8C%EC%9E%83%EA%B3%A0-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0-%EA%B3%A0%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 Aug 2025 16:27:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오늘 시간을 갈아넣어서 기존의 프론트엔드 폴더 구조를 전부 개선하는 작업을 했습니다.
프로젝트 중간에 폴더 구조를 싹다 바꾸는 게 굉장히 부담스럽고 시간이 많이 드는 작업이지만, 과감히 결정하게 된 과정을 공유드리겠습니다</p>
</blockquote>
<h2 id="기존-폴더-구조">기존 폴더 구조</h2>
<p>기존 프로젝트는 역할 중심으로 폴더를 나누는 방식으로 구성되어 있었습니다. UI 컴포넌트는 <code>components/</code>, 페이지는 <code>pages/</code>, 전역 상태는 <code>stores/</code>, 타입은 <code>customTypes/</code> 등 <strong>관심사의 분리</strong>에 초점을 둔 구조였습니다.</p>
<pre><code>📦 src
├── 📁 api                  // API 호출 함수들
├── 📁 assets               // 정적 리소스 (이미지, 아이콘 등)
├── 📁 components           // 페이지에 공통적으로 사용되는 UI 컴포넌트
│   ├── 📁 common           // Header, Router 등 공용 컴포넌트
│   ├── 📁 homePage         // 홈 페이지 전용 컴포넌트
│   ├── 📁 loginPage        // 로그인 페이지 전용 컴포넌트
│   └── 📁 practicePage     // 실습 페이지 전용 컴포넌트
├── 📁 constants            // 상수 정의
├── 📁 customTypes          // 타입스크립트 타입 정의
├── 📁 hooks                // 커스텀 훅 정의
├── 📁 layouts              // 공용 레이아웃 컴포넌트
├── 📁 mocks                // 테스트용 목 데이터
├── 📁 pages                // 라우팅 단위의 실제 페이지 컴포넌트
│   ├── 📁 homePage
│   ├── 📁 loginPage
│   ├── 📁 myPage
│   ├── 📁 pairPracticePage
│   ├── 📁 practicePage
│   └── 📁 soloPracticePage
├── 📁 stores               // Zustand 기반 전역 상태 관리
├── 📄 App.tsx              // 루트 컴포넌트
├── 📄 main.tsx             // 앱 진입점
└── 📄 index.css            // 글로벌 스타일</code></pre><p>기존 코드는 관심사별로 코드를 분리하는 데에는 좋았지만, 문제는 관심사로 분리하다 보니 하나의 기능에 필요한 코드가 여러 폴더에 흩어지는 문제가 있었습니다.</p>
<p>그래서 리팩토링하거나, 기능을 추가하게 될 경우 너무 많은 폴더를 이동해서 수정해야하는 불편함이 있었고 생산성도 떨어졌습니다.</p>
<p>예를 들어 <code>practicePage</code>에 간단한 버튼 컴포넌트를 수정한다고 가정해보겠습니다.</p>
<pre><code>📦 src
├── 📁 components/practicePage/
│   └── 📄 FeedbackButton.tsx          ← UI 컴포넌트 정의
├── 📁 hooks/practicePage/
│   └── 📄 useFeedbackHandler.ts       ← 버튼 클릭에 사용할 커스텀 훅
├── 📁 customTypes/practicePage/
│   └── 📄 FeedbackButtonProps.ts      ← 버튼의 Props 타입
├── 📁 constants/
│   └── 📄 feedbackConstants.ts        ← 버튼에서 사용할 상수 정의
├── 📁 stores/
│   └── 📄 useFeedbackStore.tsx        ← 상태 저장 및 전역 관리
├── 📁 pages/practicePage/
│   └── 📄 index.tsx                   ← 페이지에서 버튼 컴포넌트 사용</code></pre><p>단 하나의 기능을 구현하기 위해 6개의 폴더를 이동해야 했고, 폴더 내에서 해당 파일을 찾기도 힘들었습니다.</p>
<p>그래서 폴더 구조를 변경해야겠다는 필요성을 느끼고 여러 자료를 공부하던 중 <strong>하나의 키워드</strong>를 찾았습니다</p>
<p><a href="https://toss.tech/article/firesidechat_frontend_10">토스 모닥불 - 디렉토리 관리</a>
<a href="https://frontend-fundamentals.com/code-quality/">토스 - 프론트엔드 지침서</a></p>
<h2 id="응집도">응집도</h2>
<p>토스에서는 <code>수정하기 쉬운 코드</code>가 <strong>좋은 코드</strong>라고 합니다. </p>
<p>여기서 <strong>수정</strong>은 무엇을 의미할까요?</p>
<p>객체지향을 공부하셨던 분들은 <strong>OCP</strong>에서는 <code>확장에는 열려있고, 수정에는 닫힘</code>이라고 했는데, 왜 수정하는 게 좋은 코드이지 의아해 하실 수도 있습니다.</p>
<p><strong>OCP</strong>에서 말하는 <strong>수정</strong>은, 새로운 요구사항이 생겼을 때 기존 코드를 변경하지 않고 새로운 코드를 추가하여 확장할 수 있어야 한다는 의미입니다.</p>
<p>반면 <strong>토스</strong>에서 말하는 <strong>수정하기 쉬운 코드는</strong>, 어떤 기능을 변경하거나 개선할 때 관련된 코드들이 한 곳에 모여 있어 쉽게 파악하고 수정할 수 있는 코드를 말합니다.</p>
<p>그것이 바로 <strong>응집도가 높은 코드</strong>입니다.</p>
<h2 id="응집도가-높아지려면-어떻게-해야할까요">응집도가 높아지려면 어떻게 해야할까요?</h2>
<p>응집도가 높아지려면 역할 기반이 아닌 기능 기반으로 코드를 모으면 됩니다.</p>
<p>예를 들어, 아래처럼 하나의 기능을 하는 코드들이 멀리 떨어져 있으면 수정하기 불편할 겁니다</p>
<h3 id="before">before</h3>
<pre><code>📦 src
├── 📁 components/practicePage/
│   └── 📄 FeedbackButton.tsx          ← UI 컴포넌트 정의
    ...(수 없이 많은 파일들)
    ...
    ...
├── 📁 hooks/practicePage/
│   └── 📄 useFeedbackHandler.ts       ← 버튼 클릭에 사용할 커스텀 훅
    ...
    ...
    ...
├── 📁 customTypes/practicePage/
│   └── 📄 FeedbackButtonProps.ts      ← 버튼의 Props 타입
    ...
    ...
    ...
├── 📁 constants/
│   └── 📄 feedbackConstants.ts        ← 버튼에서 사용할 상수 정의
    ...
    ...
    ...
├── 📁 stores/
│   └── 📄 useFeedbackStore.tsx        ← 상태 저장 및 전역 관리
    ...
    ...
    ...
├── 📁 pages/practicePage/
│   └── 📄 index.tsx                   ← 페이지에서 버튼 컴포넌트 사용
    ...
    ...
    ...</code></pre><h3 id="after">after</h3>
<p>하지만 이렇게 기능 단위로 묶이게 되면, 응집성이 높아 기능 수정이나 추가가 수월해집니다.</p>
<p>한 눈에 봐도 기능으로 묶이는 게 편해보이죠?</p>
<pre><code>
📦 src
├── 📁 features
│   └── 📁 feedbackButton
│       ├── 📄 index.ts               // 컴포넌트 export
│       ├── 📄 FeedbackButton.tsx     // UI 컴포넌트
│       ├── 📄 useFeedback.ts         // 관련 커스텀 훅
│       ├── 📄 feedbackStore.ts       // Zustand 전역 상태
│       ├── 📄 types.ts               // 타입 정의
│       └── 📄 constants.ts           // 상수 정의
├── 📁 pages
│   └── 📁 practicePage
│       ├── 📄 index.tsx              // 피드백 버튼 import 및 사용</code></pre><h2 id="fsd-feature-sliced-design">FSD (Feature-sliced Design)</h2>
<p><a href="https://feature-sliced.design/kr/docs/get-started/overview">FSD 문서</a></p>
<p>FSD는 프론트엔드에서 쓰이는 <strong>기능 기반 폴더 구조</strong>의 대표적인 예시입니다.</p>
<p>FSD를 설명하는 글은 너무나 많으니 이번 글에서는 저희 프로젝트에 <strong>어떤 기준</strong>으로 적용했는지 설명드리겠습니다.</p>
<h3 id="layer">Layer</h3>
<p>우선 FSD에서는 계층적인 구조를 가집니다.</p>
<pre><code>📦 src
├── 📁 app            # 앱의 진입점
├── 📁 pages          # 페이지 단위
├── 📁 widgets        # 페이지를 구성하는 컴포넌트
├── 📁 features       # 사용자 중심의 기능 단위 (ex. 로그인, 채팅)
├── 📁 entities       # 모델 단위 (ex. User, Room 등)
├── 📁 shared         # 프로젝트 전반에서 재사용되는 코드들</code></pre><p>막상 적용하려보면 무엇을 어디에 넣어야 할지 감 잡기가 어렵습니다.</p>
<p>실제 저희 프로젝트의 홈페이지를 보면서 설명드리겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/a4008de3-3170-46ab-8381-8a1d95a4ab68/image.png" alt=""></p>
<pre><code>📦 HomePage
├── 📁 헤더
│   ├── 📄 로고
│   ├── 📄 네비게이션 메뉴 (홈, 마이페이지, 면접 질문)
│   └── 📄 로그인 버튼
├── 📁 좌측
│   ├── 📄 히어로 텍스트
│   │   ├── &quot;꼬리에 꼬리를 무는 면접 질문&quot;
│   │   └── &quot;이제는 꼬리🦊 와 함께 AI로 연습하세요&quot;
│   ├── 📄 혼자 연습하기 버튼
│   └── 📄 같이 연습하기 버튼
└── 📁 우측
    └── 📄 여우 캐릭터 일러스트 (정장+책상)</code></pre><p>우선 헤더의 경우 공통적으로 쓰이는 컴포넌트입니다.</p>
<p>실제로는 <code>MainLayout.tsx</code> 내부에서 관리되고, React Router의 <code>&lt;Outlet /&gt;</code>을 통해 <code>&lt;HomePage /&gt;</code>가 보여지는 구조입니다.</p>
<p>이때 <strong>&quot;공통적으로 사용된다&quot;</strong>는 이유만으로 <strong>shared에 넣을지, 아니면 widgets에 둘지를 고민하게 됩니다.</strong></p>
<p><code>&lt;Header /&gt;</code>의 경우 공통적으로 쓰이긴 하지만, 어떤 도메인과 직접적으로 연결되어 사용되는 재사용 UI가 아니고 그냥 페이지를 구성하는 하나의 기능 블록이라는 점에서 <code>widgets</code>이라고 판단했습니다.</p>
<pre><code class="language-markdown">. 📂 widgets
└── 📂 header/
│  ├── 📄 index.tsx
│  └── 📂 model/
│    ├── 📄 constants.ts
│  └── 📂 test/
│    ├── 📄 Header.test.tsx
│    ├── 📄 LogoLink.test.tsx
│  └── 📂 ui/
│    ├── 📄 LogoLink.tsx</code></pre>
<hr>
<p>다만 <code>&lt;Header /&gt;</code>를 구성하는 요소 중 <strong>로그인 버튼</strong>은 다르게 관점에서 고려해야 합니다.</p>
<p><code>&lt;LoginButton /&gt;</code>은 단순한 UI 요소처럼 보일 수 있지만, <strong>실제로는 인증 로직과 연결된 사용자 중심 기능을 포함하고 있기 때문입니다.</strong></p>
<p>소셜 로그인 API 요청, 로그인 상태 판단, 리디렉션 등 인증 도메인과 밀접하게 연결된 로직이 포함되어 있다면, 이 컴포넌트는 단순한 UI가 아닌 <strong>기능 단위(feature)</strong>로 봐야 합니다.</p>
<p>따라서 저희는 로그인 버튼을 <code>features/auth/ui</code>로 분리하였습니다.</p>
<pre><code class="language-markdown">. 📂 features
└── 📂 auth/
│  └── 📂 model/
│    ├── 📄 constants.ts
│  └── 📂 test/
│    ├── 📄 GoToLoginButton.test.tsx
│  └── 📂 ui/
│    └── 📄 GoToLoginButton.tsx</code></pre>
<hr>
<p>그러면 나머지 좌측, 우측에 해당하는 컴포넌트들은 어디에 넣을 수 있을까요?</p>
<p><code>&lt;HomePage/&gt;</code>에서만 사용되므로, <code>/pages/homePage/ui</code> 에 위치시키는 것이 적절합니다.</p>
<pre><code class="language-markdown">. 📂 homePage
│  ├── 📄 index.tsx
└── 📂 test/
│  ├── 📄 BackgroundShadow.test.tsx
│  ├── 📄 HeroText.test.tsx
│  ├── 📄 LeftSection.test.tsx
│  ├── 📄 PairPracticeButton.test.tsx
│  ├── 📄 PracticeButton.test.tsx
│  ├── 📄 SoloPracticeButton.test.tsx
│  ├── 📄 ThumbnailContainer.test.tsx
│  ├── 📄 index.test.tsx
└── 📂 ui/
│  ├── 📄 BackgroundShadow.tsx
│  ├── 📄 HeroText.tsx
│  ├── 📄 LeftSection.tsx
│  ├── 📄 PairPracticeButton.tsx
│  ├── 📄 PracticeButton.tsx
│  ├── 📄 SoloPracticeButton.tsx
│  └── 📄 ThumbnailContainer.tsx</code></pre>
<p>이렇게 인증 도메인과 관련된 로직은 <code>feature/auth</code> 안에서 응집되고, <code>&lt;Header /&gt;</code>는 여전히 레이아웃 구성 UI로써의 역할만 담당하게 되어 역할이 명확하게 분리됩니다.</p>
<p>또한, 하나의 페이지에서만 사용되는 컴포넌트들은 <code>pages/page/ui</code>에서 응집된 구조를 가지게 됐습니다.</p>
<p>이러한 구조 덕분에 컴포넌트의 역할과 책임이 명확히 분리되었고,
FSD가 지향하는 <strong>응집도 높은 구성과 명확한 책임 분리</strong>를 적용할 수 있었습니다.</p>
<hr>
<h2 id="맺음말">맺음말</h2>
<p>이 글을 읽으시다 의문이 드셨을 수도 있습니다</p>
<blockquote>
<p>처음부터 기능 기반으로 폴더 구조를 잘 짰으면 되는 거 아닌가요?</p>
</blockquote>
<p>맞습니다.</p>
<p>사실 오늘의 리팩토링 과정은 프로젝트 초반부터 예상된 일이기도 했습니다.</p>
<h3 id="역할-기반-폴더-구조-의사결정-과정">역할 기반 폴더 구조 의사결정 과정</h3>
<p>처음 폴더 구조를 설계할 때, 여러 자료를 찾아보면서 <code>FSD</code>가 근 1~2년 사이에 프론트엔드에서 많이 쓰인다는 걸 알게 됐습니다.</p>
<p>다만, 단순히 트렌드라서 적용하기 보다는 실제로 내가 <strong>기능 중심으로 묶어야 할 필요성</strong>을 느끼고, <strong>구조를 바꿨을 때 얻는 장점</strong>을 체감하며 적용하고 싶었습니다.</p>
<p>따라서 역할 기반 설계를 먼저 적용하고, 불편함이 명확해지면 기능 기반 구조로 변경하기로 결정했습니다.</p>
<p>이 의사결정을 하는데 큰 근거가 되었던 건 <strong>TDD</strong>였습니다.</p>
<p>테스트 코드가 존재하니 <strong>구조를 변경해도 기존 기능이 깨지지 않았는지 빠르게 확인할 수 있다는 믿음</strong>이 있었습니다.</p>
<p>그리고 폴더 구조 리팩토링을 겪으면 <strong>TDD의 가장 큰 장점인 안전한 리팩토링</strong>을 실제로 체감할 수 있으니 <strong>일석이조</strong>라는 생각으로 결정했습니다.</p>
<h3 id="실제-적용-후기">실제 적용 후기</h3>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/af56c4dc-dfed-4c59-a48c-fd0739bf8899/image.png" alt=""></p>
<p>폴더 구조를 기능 기반으로 변경하고, 연관된 코드들을 리팩토링하니 <strong>총 105개의 파일 변경이 있었습니다.</strong></p>
<p>컴포넌트를 옮기고 경로를 수정하고, import 경로를 재정리하는 과정에서 <strong>수많은 에러가 발생</strong>했습니다.</p>
<p>처음에는 쌓여가는 에러에 겁이 났지만, <code>테스트 코드 있으니까 괜찮을 거야</code>라고 동료와 대화를 나누며 걱정을 떨쳐낼 수 있었습니다.</p>
<p>실제로 테스트를 실행하면 어떤 컴포넌트에서 문제가 발생했는지 즉시 확인할 수 있었고,
UI나 로직을 수정해도 기능이 여전히 정상 동작하는지 자동으로 검증할 수 있었습니다.</p>
<p>이 과정을 겪으면서 두 가지를 깨달았습니다.</p>
<ol>
<li>응집도가 높은 구조는 유지보수가 쉬워진다.</li>
<li>테스트 코드 함께라면 리팩토링이 즐겁다</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발자로 성장하려면 어떻게 해야할까]]></title>
            <link>https://velog.io/@today-is-first/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C-%EC%84%B1%EC%9E%A5%ED%95%98%EB%A0%A4%EB%A9%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@today-is-first/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C-%EC%84%B1%EC%9E%A5%ED%95%98%EB%A0%A4%EB%A9%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sat, 02 Aug 2025 07:44:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>1~6월보다 최근 프로젝트를 진행한 한 달 동안 더 많이 성장하고 있다고 느낀다.
최근에 내가 <strong>개발자로 성장하려고 어떤 노력을 하고 있는지</strong> 적어보고자 한다.</p>
</blockquote>
<h2 id="개발자란-무엇일까">개발자란 무엇일까?</h2>
<p>내가 생각하는 개발자는 단순히 컴퓨터 언어를 사용해서 코딩을 하는 사람은 아니다.</p>
<p>개발자는 어떠한 <code>문제</code>를 정의하고, 적절한 기술을 사용해서 <strong>해결</strong>하는 사람이라고 생각한다.</p>
<p>즉, 실력이 좋은 개발자로 성장하려면 <strong>문제를 해결하는 능력</strong>을 길러야 한다.</p>
<p>여기서의 <code>문제</code>는 비즈니스 문제 같은 중요한 것들 뿐만 아니라 다양하고 사소한 것들도 포함된다고 생각한다.</p>
<p>예를 들어, <code>A</code>라는 코드 안에서 사용되는 함수를 <code>B</code>에서도 사용 가능하게 분리할 때 어떻게 하면 재사용성 높게 할 지도 하나의 문제로 볼 수 있다.</p>
<p>협업 과정에서 다른 사람들도 쉽게 이해할 수 있게 <strong>추상화하고 선언적으로 코드를 짜고, 이해 가능한 메서드/변수 명을 고민하는 것</strong>도 하나의 문제로 볼 수 있다.</p>
<h2 id="문제-해결력은-어떻게-기를-수-있을까">문제 해결력은 어떻게 기를 수 있을까</h2>
<p>문제 해결력은 단순히 알고리즘 문제를 많이 푼다고 해서 자연스럽게 길러지는 것은 아니다.</p>
<p>문제 해결에는 <strong>CS 지식, 프로그래밍 언어에 대한 깊은 이해, 효율적인 구조와 설계에 대한 감각, 코드를 읽고 고치는 능력, 그리고 협업 과정에서의 커뮤니케이션 능력</strong>까지도 영향을 미친다.</p>
<p><code>즉, 개발자로서 문제를 해결하는 능력은 단순한 구현 능력 그 이상이다.</code></p>
<p>예를 들어, 어떤 기능을 구현할 때도 단순히 동작하게 만드는 것에서 그치지 않고, <strong>어떤 방식이 더 유지보수에 유리할지, 팀원들이 이해하기 쉬운 구조는 무엇인지, 성능과 확장성을 고려한 설계인지를 고민하는 과정</strong>에서 문제 해결력이 길러진다.</p>
<p>또한, AI의 성능이 향상되면서 어떤 기능이 동작하게 만드는 건 너무 쉬운 세상이 되어버렸다. 그래서 난 오히려 하나의 기능을 <strong>고도화 할 수 있는 능력</strong>이 훨씬 중요해지고 있다고 느낀다.</p>
<blockquote>
<p>따라서 이번 프로젝트는 하나의 기능을 디테일하게 만드는 것에 집중했다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/bea5ef90-e241-4be5-b523-43b425ffb176/image.png" alt=""></p>
<h2 id="디테일한-개발을-하려면-어떻게-해야할까">디테일한 개발을 하려면 어떻게 해야할까</h2>
<p>우선 본인이 사용하는 기술에 대해서 자세히 알아야한다.</p>
<p>기술을 사용하는 사람이 아니라 <strong>활용하는 사람</strong>이 되어야하고, 철저한 학습이 선행되어야 한다고 생각한다.</p>
<blockquote>
<p>이번 프로젝트에서 로그인 기능을 구현한 과정을 예시로 들어보겠다</p>
</blockquote>
<p>우선 여러 인증 방식에 대한 지식을 학습하고 기술 블로그를 작성했다.</p>
<p><a href="https://velog.io/@today-is-first/%EB%8B%B9%EC%8B%A0%EC%9D%B4-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%A0-%EB%95%8C-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%98%EB%8A%94-%EA%B2%83%EB%93%A4">참고 - 당신이 로그인을 구현할 때 반드시 알아야하는 것들</a></p>
<p>이후 토큰 방식 인증을 사용하는 <strong>OIDC 소셜 로그인</strong>을 구현하는 것으로 결정을 내렸다.</p>
<p>OAuth를 통해 받은 Access Token은 Opaque 토큰이자 베어러 토큰이므로 이를 인증에 활용하면 안된다는 것을 학습했다.</p>
<p>따라서 우리 서버에서 ID Token 검증 이후 자체적으로 <strong>Access Token, Refresh Token</strong>을 발행하는 방식을 채택하였다.</p>
<p><strong>Access Token</strong>는 어디에 저장해야 할 지 고민을 했다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th><strong>Cookie</strong></th>
<th><strong>LocalStorage</strong></th>
<th><strong>SessionStorage</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>서버 전송 여부</strong></td>
<td>매 요청 시 자동 전송 (<code>Cookie</code> 헤더)</td>
<td>직접 전송 필요</td>
<td>직접 전송 필요</td>
</tr>
<tr>
<td><strong>보안 옵션</strong></td>
<td><code>Secure</code>, <code>HttpOnly</code>, <code>SameSite</code> 설정 가능</td>
<td>없음</td>
<td>없음</td>
</tr>
<tr>
<td><strong>브라우저 접근</strong></td>
<td>JavaScript로 접근 가능 <strong>(단, <code>HttpOnly</code>면 불가)</strong></td>
<td>JavaScript로 접근 가능</td>
<td>JavaScript로 접근 가능</td>
</tr>
</tbody></table>
<p>클라이언트에 토큰을 저장할 수 있는 방법인 <strong>Cookie, LocalStorage, SessionStorage</strong> 중 보안 옵션을 걸 수 있어 그나마 안전한 쿠키를 사용하기로 결정했다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/8f4966c8-46f1-4057-87ef-f90fe3de135d/image.png" alt=""></p>
<hr>
<p>쿠키에 걸 수 있는 다양한 보안 옵션을 공부하면서, 해당 옵션을 적용했을 때 구현 과정에 끼칠 영향을 고려했다.</p>
<p>예를 들어, <strong>httpOnly</strong> 옵션은 <strong>크로스 사이트 스크립팅(XSS)</strong> 공격을 막을 수 있는 옵션이다.</p>
<p>쿠키는 <code>document.cookie</code>를 통해 접근이 가능한데 <strong>httpOnly 옵션</strong>을 쓰면 이를 막고, HTTP 요청에만 담기게 된다.</p>
<p>보안적으로 좋지만 <strong>프론트엔드 개발자도 쿠키에 대한 접근이 불가능</strong>하게 되어, <strong>Authorization 헤더</strong>에 토큰을 담을 수 없게 된다.</p>
<p>따라서 백엔드에서는 <strong>Authorization 헤더</strong>가 아닌 쿠키에 접근해 토큰을 검증해야 하고, 프론트엔드에서는 별도로 <strong>Authorization 헤더</strong>에 담을 필요가 없다는 걸 알았다.</p>
<p><strong>httpOnly</strong> 옵션으로 인해 프론트엔드에서는 Access Token에 직접 접근할 수 없기 때문에,
<code>/api/me</code> 엔드포인트에 요청을 보내 사용자의 인증 상태를 확인하고, 그 결과를 상태로 저장하여 UI에 반영하기로 결정했다.</p>
<p>이처럼 구현 과정에 앞서 <strong>인증 방식, 토큰 저장 방식의 보안성, 쿠키의 동작 방식과 보안 옵션</strong>의 의미를 충분히 학습하고 정리했기 때문에, 실제 구현 단계에서는 <strong>설계 변경없이 빠르게 개발을 진행</strong>할 수 있었다.</p>
<blockquote>
<p>디테일한 개발을 위해선 기초가 튼튼해야 한다고 느꼈고, 우리 팀에서 하는 추가적인 활동도 소개하겠다.</p>
</blockquote>
<h2 id="딥다이브">딥다이브</h2>
<p>우리 프로젝트에는 <strong>딥다이브</strong>라고 불리는 시간이 있다.</p>
<p>매일 아침 데일리 스크럼이 끝나면 하루에 2시간정도 언어나 프레임워크에 대한 지식을 학습한다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/8a0cdad0-8232-41a2-ab14-3a8b51dd1699/image.png" alt=""></p>
<p>예를 들어, <strong>클로저</strong>를 학습한다고 가정하면 개념에 이어서 추가적으로 연관된 지식을 학습하는 방식으로 진행한다.</p>
<p>이 때, AI와 공식문서를 최대한 활용하고자 하는데</p>
<p>mdn이나 리액트 공식 문서를 활용해 나만의 답변을 먼저 작성하고, AI에 질문을 해서 내 답변이 적절한지와 추가적으로 학습할 내용을 물어보는 방식으로 학습하고 있다.</p>
<p>3주 동안 프론트 파트는 160개, 백엔드 파트는 130개가 넘는 질문/답변을 작성했다.</p>
<p>아래는 실제 우리 팀에서 사용하는 프롬포트이니 시도해보려는 사람은 참고하면 좋을 것 같다.</p>
<pre><code>나는 자바/ 스프링 백엔드, 자바스크립트 프론트엔드로 입사하려는 신입 개발자야

너가 웹 개발 20년차 전문 CTO 및 면접관 입장에서

지원자가 대답하면 좋을 것 같은 답변을 작성하는 걸 도와줘야 돼

나는 두괄식패턴인 OREO 방식으로 2~3줄에서 3~5줄 사이로 답변을 작성하고 싶어

그리고 한번에 모든 걸 말하지 않고

묻는 질문에 대한 핵심 내용만 답변한 다음 추가질문을 유도할 거야

따라서 내가 질문을 하면 최대 3~4줄 정도로 답변하고

예상되는 추가 질문을 작성해줘</code></pre><h2 id="기술-블로그">기술 블로그</h2>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/58d57543-4a9a-44d7-8a58-99d2036ca13e/image.png" alt=""></p>
<p>우리 팀은 <strong>주 1회</strong> 기술 블로그 작성을 강제하고 있다.</p>
<p>기술 블로그의 필요성은 아는데 막상 작성하는 시간도 오래 걸리는 작업이라 시작하기가 힘들다.</p>
<p>그래서 우리 팀은 <strong>주 1회</strong>를 작성하지 않으면 작성을 완료할 때까지 개발에 참여하지 못하게 하여 무조건 작성할 수 밖에 없는 환경을 조성하였고 실제로 잘 지켜지고 있다.</p>
<p>또한 MM에 매주 작성된 기술 블로그를 올리고 있는데, 누군가 내 글을 볼 수 있다는 압박이 있으면 글의 논리나 맥락을 신경쓰게 되고 퀄리티를 신경쓸 수 밖에 없어진다.</p>
<h2 id="맺음말">맺음말</h2>
<p>이번 프로젝트가 만족스럽게 진행되고 있고, 진행 방식이 좋은 것 같아 공유하고자 글을 작성해봤다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유용한 에어비엔비 코드 컨벤션 모음]]></title>
            <link>https://velog.io/@today-is-first/%EC%9C%A0%EC%9A%A9%ED%95%9C-%EC%97%90%EC%96%B4%EB%B9%84%EC%97%94%EB%B9%84-%EC%BD%94%EB%93%9C-%EC%BB%A8%EB%B2%A4%EC%85%98-%EB%AA%A8%EC%9D%8C</link>
            <guid>https://velog.io/@today-is-first/%EC%9C%A0%EC%9A%A9%ED%95%9C-%EC%97%90%EC%96%B4%EB%B9%84%EC%97%94%EB%B9%84-%EC%BD%94%EB%93%9C-%EC%BB%A8%EB%B2%A4%EC%85%98-%EB%AA%A8%EC%9D%8C</guid>
            <pubDate>Sun, 27 Jul 2025 17:09:59 GMT</pubDate>
            <description><![CDATA[<h2 id="airbnb-코드컨벤션-주소">Airbnb 코드컨벤션 주소</h2>
<p><a href="https://github.com/parksb/javascript-style-guide">https://github.com/parksb/javascript-style-guide</a></p>
<h2 id="airbnb-코드컨벤션">Airbnb 코드컨벤션</h2>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#3.6">3.6</a> 프로퍼티의 단축구문을 이용해 주십시오.</li>
<li>key와 value가 같으면  단축 구문을 사용해봅시다.</li>
</ul>
<pre><code class="language-jsx">// bad
const obj = {
  lukeSkywalker: lukeSkywalker,
};

// good
const obj = {
  lukeSkywalker,
};</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#4.3">4.3</a> 배열을 복사할때는 배열의 확장연산자 <code>...</code> 를 이용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
const len = items.length;
const itemsCopy = [];
let i;

for (i = 0; i &lt; len; i++) {
  itemsCopy[i] = items[i];
}

// good
const itemsCopy = [...items];</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#5.2">5.2</a> 배열의 구조 분해를 이용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">const arr = [1, 2, 3, 4];

// bad
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#5.3">5.3</a> 복수의 값을 반환하는 경우는 배열의 구조화 대입이 아닌 오브젝트의 구조화 대입을 이용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
function processInput(input) {
  return [left, right, top, bottom];
}

const [left, __, top] = processInput(input);

// good
function processInput(input) {
  return { left, right, top, bottom };
}

const { left, right } = processInput(input);</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#7.7">7.7</a> 함수의 파라메터를 변이시키는 것보다 default 파라메터를 이용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// really bad
function handleThings(opts) {
  opts = opts || {};
  // handleThings() 호출 시 파라미터가 전달되면 opts에 저장, 없으면 빈 객체({})
}

// good
function handleThings(opts = {}) {
  // 함수 선언 시 default 값을 저장
}</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#7.9">7.9</a> 항상 default 파라메터는 뒤쪽에 두십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
function handleThings(opts = {}, name) {
  // ...
}

// good
function handleThings(name, opts = {}) {
  // ...
}</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#8.2">8.2</a> 함수의 본체가 하나의 식으로 구성된 경우에는 중괄호({})를 생략하고 암시적 return을 이용하는것이 가능합니다. 그 외에는 <code>return</code> 문을 이용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// good
[1, 2, 3].map(number =&gt; `A string containing the ${number}.`);

// bad
[1, 2, 3].map(number =&gt; {
  const nextNumber = number + 1;
  `A string containing the ${nextNumber}.`;
});

// good
[1, 2, 3].map(number =&gt; {
  const nextNumber = number + 1;
  return `A string containing the ${nextNumber}.`;
});</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#8.3">8.3</a> 식이 복수행에 걸쳐있을 경우는 가독성을 더욱 좋게하기 위해 소괄호()로 감싸 주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
[1, 2, 3].map(number =&gt; &#39;As time went by, the string containing the &#39; +
  `${number} became much longer. So we needed to break it over multiple ` +
  &#39;lines.&#39;
);

// good
[1, 2, 3].map(number =&gt; (
  `As time went by, the string containing the ${number} became much ` +
  &#39;longer. So we needed to break it over multiple lines.&#39;
));</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#9.1">9.1</a> <code>prototype</code> 을 직접 조작하는것을 피하고 항상 <code>class</code> 를 이용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
function Queue(contents = []) {
  this._queue = [...contents];
}
Queue.prototype.pop = function() {
  const value = this._queue[0];
  this._queue.splice(0, 1);
  return value;
}

// good
class Queue {
  constructor(contents = []) {
    this._queue = [...contents];
  }
  pop() {
    const value = this._queue[0];
    this._queue.splice(0, 1);
    return value;
  }
}</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#9.3">9.3</a> 메소드의 반환값으로 <code>this</code> 를 반환하는 것으로 메소드채이닝을 할 수 있습니다.</li>
</ul>
<pre><code class="language-jsx">// bad
Jedi.prototype.jump = function() {
  this.jumping = true;
  return true;
};

Jedi.prototype.setHeight = function(height) {
  this.height = height;
};

const luke = new Jedi();
luke.jump(); // =&gt; true
luke.setHeight(20); // =&gt; undefined

// good
class Jedi {
  jump() {
    this.jumping = true;
    return this;
  }

  setHeight(height) {
    this.height = height;
    return this;
  }
}

const luke = new Jedi();

luke.jump()
  .setHeight(20);</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#11.1">11.1</a> iterators를 이용하지 마십시오. <code>for-of</code> 루프 대신에 <code>map()</code> 과 <code>reduce()</code> 와 같은 JavaScript 고급함수(higher-order functions)를 이용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">const numbers = [1, 2, 3, 4, 5];

// bad
let sum = 0;
for (let num of numbers) {
  sum += num;
}

sum === 15;

// good
let sum = 0;
numbers.forEach((num) =&gt; sum += num);
sum === 15;

// best (use the functional force)
const sum = numbers.reduce((total, num) =&gt; total + num, 0);
sum === 15;</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#13.1">13.1</a> 변수를 선언 할 때는 항상 <code>const</code> 를 사용해 주십시오. 그렇게 하지 않으면 글로벌 변수로 선언됩니다. 글로벌 namespace 를 오염시키지 않도록 캡틴플래닛도 경고하고 있습니다.</li>
</ul>
<pre><code class="language-jsx">// bad
superPower = new SuperPower();

// good
const superPower = new SuperPower();</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#13.3">13.3</a> 우선 <code>const</code> 를 그룹화하고 다음에 <code>let</code> 을 그룹화 해주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
let i, len, dragonball,
    items = getItems(),
    goSportsTeam = true;

// bad
let i;
const items = getItems();
let dragonball;
const goSportsTeam = true;
let len;

// good
const goSportsTeam = true;
const items = getItems();
let dragonball;
let i;
let length;</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#15.3">15.3</a> 단축형을 사용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
if (name !== &#39;&#39;) {
  // ...stuff...
}

// good
if (name) {
  // ...stuff...
}

// bad
if (collection.length &gt; 0) {
  // ...stuff...
}

// good
if (collection.length) {
  // ...stuff...
}</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#17.1">17.1</a> 복수행의 코멘트는 <code>/** ... */</code> 을 사용해 주십시오. 그 안에는 설명과 모든 파라메터, 반환값에 대해 형이나 값을 기술해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// good
/**
 * make() returns a new element
 * based on the passed in tag name
 *
 * @param {String} tag
 * @return {Element} element
 */
function make(tag) {

  // ...stuff...

  return element;
}</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#18.6">18.6</a> 길게 메소드를 채이닝하는 경우는 인덴트를 이용해 주십시오. 행이 새로운 문이 아닌 메소드 호출인 것을 강조하기 위해서 선두에 점 (.) 을 배치해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
const leds = stage.selectAll(&#39;.led&#39;).data(data).enter().append(&#39;svg:svg&#39;).class(&#39;led&#39;, true)
    .attr(&#39;width&#39;, (radius + margin) * 2).append(&#39;svg:g&#39;)
    .attr(&#39;transform&#39;, &#39;translate(&#39; + (radius + margin) + &#39;,&#39; + (radius + margin) + &#39;)&#39;)
    .call(tron.led);

// good
const leds = stage.selectAll(&#39;.led&#39;)
    .data(data)
  .enter().append(&#39;svg:svg&#39;)
    .classed(&#39;led&#39;, true)
    .attr(&#39;width&#39;, (radius + margin) * 2)
  .append(&#39;svg:g&#39;)
    .attr(&#39;transform&#39;, &#39;translate(&#39; + (radius + margin) + &#39;,&#39; + (radius + margin) + &#39;)&#39;)
    .call(tron.led);</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#21.3">21.3</a> 수치의 경우: <code>Number</code> 로 형변환하는 경우는 <code>parseInt</code> 를 이용하고, 항상 형변환을 위한 기수를 인수로 넘겨 주십시오.</li>
</ul>
<pre><code class="language-jsx">const inputValue = &#39;4&#39;;

// bad
const val = new Number(inputValue);

// bad
const val = +inputValue;

// bad
const val = inputValue &gt;&gt; 0;

// bad
const val = parseInt(inputValue);

// good
const val = Number(inputValue);

// good
const val = parseInt(inputValue, 10);</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#21.6">21.6</a> 부울값의 경우:</li>
</ul>
<pre><code class="language-jsx">const age = 0;

// bad
const hasAge = new Boolean(age);

// good
const hasAge = Boolean(age);

// good
const hasAge = !!age;</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#22.2">22.2</a> 오브젝트, 함수 그리고 인스턴스에는 camelCase를 사용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
const OBJEcttsssss = {};
const this_is_my_object = {};
function c() {}

// good
const thisIsMyObject = {};
function thisIsMyFunction() {}
</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#22.3">22.3</a> 클래스나 constructor에는 PascalCase 를 사용해 주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
function user(options) {
  this.name = options.name;
}

const bad = new user({
  name: &#39;nope&#39;,
});

// good
class User {
  constructor(options) {
    this.name = options.name;
  }
}

const good = new User({
  name: &#39;yup&#39;,
});</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#22.6">22.6</a> 파일을 1개의 클래스로 export 하는 경우, 파일명은 클래스명과 완전히 일치시키지 않으면 안됩니다.</li>
</ul>
<pre><code class="language-jsx">// file contents
class CheckBox {
  // ...
}
export default CheckBox;

// in some other file
// bad
import CheckBox from &#39;./checkBox&#39;;

// bad
import CheckBox from &#39;./check_box&#39;;

// good
import CheckBox from &#39;./CheckBox&#39;;</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#22.7">22.7</a> Default export가 함수일 경우, camelCase를 이용해 주십시오. 파일명은 함수명과 동일해야 합니다.</li>
</ul>
<pre><code class="language-jsx">function makeStyleGuide() {
}

export default makeStyleGuide;</code></pre>
<ul>
<li><a href="https://github.com/tipjs/javascript-style-guide#23.3">23.3</a> 프로퍼티가 <code>boolean</code> 인 경우, <code>isVal()</code> 이나 <code>hasVal()</code> 로 해주십시오.</li>
</ul>
<pre><code class="language-jsx">// bad
if (!dragon.age()) {
  return false;
}

// good
if (!dragon.hasAge()) {
  return false;
}

// good
if (!dragon.isAge()) {
  return false;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트의 꽃, 클로저 ]]></title>
            <link>https://velog.io/@today-is-first/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EA%BD%83-%ED%81%B4%EB%A1%9C%EC%A0%80</link>
            <guid>https://velog.io/@today-is-first/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EA%BD%83-%ED%81%B4%EB%A1%9C%EC%A0%80</guid>
            <pubDate>Sun, 27 Jul 2025 17:08:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>자바스크립트를 공부하다 보면 항상 빠지지 않고 클로저에 대한 얘기가 나온다.
클로저가 무엇이길래 항상 언급되는 걸까</p>
</blockquote>
<h3 id="클로저란">클로저란?</h3>
<p>클로저는 주변 상태에 대한 참조와 함께 묶인 함수의 조합이다. 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공하고, 함수 생성 시마다 클로저는 생성된다. <a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Closures">출처 : mdn - 클로저</a></p>
<p>여러분들이 만약 <code>map, reduce, filter</code>와 같은 배열 메서드를 사용해본 적이 있다면 클로저를 활용해 본 경험이 있는 것이다.</p>
<p>해당 메서드처럼 <strong>함수를 인자로 사용하거나 반환하는</strong> 함수를 고차함수라고 한다.</p>
<p>이번에 공부할 개념인 클로저는 고차함수가 <strong>함수를 반환할 때 생성되는 대표적인 개념</strong>이다.</p>
<blockquote>
<p>이렇게 사전적 정의만 보면 이해하기 어려울 수 있다.
간단한 코드를 살펴보자</p>
</blockquote>
<img src="https://velog.velcdn.com/images/today-is-first/post/3bd79bea-7f1a-4a9b-8c66-63e49ba497b6/image.png" style="width:500px">

<p>위의 코드를 실행하게 되면 아래와 같이 함수 별로 다른 카운터 값이 나오는 것을 확인할 수 있다.
이게 가능한 이유가 <code>클로저</code> 때문이다.</p>
<p><code>counter</code> 라는 함수가 실행되게 되면서 선언 당시 주변 환경을 기억하는 클로저가 생성되게 된다.</p>
<pre><code class="language-js">const counter1 = counter();</code></pre>
<p>따라서 <code>counter1</code>에는 실행 당시의 렉시컬 환경이 저장되게 되고
외부 함수에서 디폴트 파라미터로 선언된 <code>count = 0</code>를 내부적으로 참조하게 된다.</p>
<pre><code class="language-js">function counter(count = 0 // 외부 변수를) {
  return () =&gt; {
    return count++; // 내부 함수에서 참조
  };
}</code></pre>
<p>따라서 <code>counter1, counter2</code>는 생성 당시 서로 다른 클로저가 생기게 되면서 각자 다른 카운트 값을 관리할 수 있게 되는 것이다.</p>
<blockquote>
<p>그러면 내부 함수에서 참조하지 않으면 어떻게 되나요?</p>
</blockquote>
<img src="https://velog.velcdn.com/images/today-is-first/post/d74b47d5-7dc5-454e-b49a-ae21fb931650/image.png" style="width:500px">

<p>당연하지만 함수 생성마다 디폴트 파라미터 값인 0으로 초기화 된 값이 카운터에 저장되게 된다</p>
<p>이러한 클로저는 주로 함수형 프로그래밍에서 활용되게 되는데 <code>커링</code>에 대해서 알아보자</p>
<h3 id="커링">커링</h3>
<p>커링은 <strong>여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수들의 연속으로 변환하는 기법</strong>이다.</p>
<p>이것도 사전적인 정의는 어려우니 코드로 이해해보자</p>
<p>예를 들어, 이벤트가 있는데 <strong>1등에게는 현재 투입금의 2배, 2등에게는 1,5배, 꽝은 0.5배</strong>를 계산해야한다.</p>
<pre><code class="language-javascript">function getPrize(multiplier, amount) {
  return multiplier * amount;
}

console.log(getPrize(2, 1000));    // 1등: 2000
console.log(getPrize(1.5, 1000));  // 2등: 1500
console.log(getPrize(0.5, 1000));  // 꽝: 500</code></pre>
<p>이렇게 작성해도 되긴 한다. 하지만 여러분의 동료가 코드를 읽는다고 가정해보자</p>
<p>처음 본 사람이 <code>getPrize(1.5, 1000)</code>만 보고 <strong>2등에게 주는 금액을 구하는 함수</strong>라는 것을 명확히 알 수 있을까?</p>
<p>이 코드를 커링을 사용해서 개선할 수 있다</p>
<pre><code class="language-javascript">function getPrize(multiplier) {
  return function(amount) {
    return multiplier * amount;
  };
}

const firstPrize = getPrize(2);
const secondPrize = getPrize(1.5);
const noPrize = getPrize(0.5);

console.log(firstPrize(1000));  // 2000
console.log(secondPrize(1000)); // 1500
console.log(noPrize(1000));     // 500</code></pre>
<p>이렇게 되면 해당 함수를 호출할 때 어떤 것을 구하고 싶은지 명확해진다</p>
<p>아래와 같이 코드를 좀 더 간단하게 작성할 수도 있다.</p>
<pre><code class="language-javascript">const getPrize = multiplier =&gt; amount =&gt; multuplier * amount;

const firstPrize = getPrize(2);
const secondPrize = getPrize(1.5);
const noPrize = getPrize(0.5);

console.log(firstPrize(1000));  // 2000
console.log(secondPrize(1000)); // 1500
console.log(noPrize(1000));     // 500</code></pre>
<p>커링은 위의 예시와 같이 <strong>하나의 인자를 고정해두고 싶을 때 사용이 가능</strong>하고, 이 때 함수 실행 당시의 렉시컬 스코프를 기억하는 <strong>클로저를 이용하여 인자에 접근</strong>한다.</p>
<blockquote>
<p>클로저라는 개념을 몰랐어도, JS로 코딩을 했다면 여러 곳에서 사용했을 것이다.
하지만 클로저가 어떻게 함수가 종료되어도 외부 변수를 참조할 수 있게 해줄까?</p>
</blockquote>
<h3 id="클로저가-생기는-흐름">클로저가 생기는 흐름</h3>
<pre><code class="language-javascript">const getPrize = multiplier =&gt; amount =&gt; multuplier * amount;

const firstPrize = getPrize(2);</code></pre>
<p>이렇게 코드가 있다고 가정해보자</p>
<ol>
<li><code>getPrize</code> 함수 객체가 힙에 저장 됨</li>
<li><code>getPrize(2)</code> 호출</li>
<li>실행 컨텍스트에 <code>multiplier = 2</code>가 바인딩 됨</li>
<li>내부 함수 <code>amount =&gt; multiplier * amount</code>가 힙에 함수 객체로 생성됨</li>
<li>이 내부 함수는 <code>multiplier = 2</code>를 클로저로 캡처함</li>
<li>내부 함수는 자신이 선언된 환경을 참조해야 하므로 <code>multiplier = 2</code>도 힙에 저장됨.</li>
<li><strong>firstPrize</strong>는 <code>getPrize(2)</code>의 실행 결과인 내부 함수 객체를 참조하게 되면서 <code>multiplier = 2</code>도 참조함.</li>
<li>참조 중이므로 GC 대상에서 제외되고, 나중에도 클로저로 접근 가능 함</li>
</ol>
<p>위와 같은 흐름으로 클로저가 생기게 된다.</p>
<p>GC 대상에서 제외되면서 메모리 누수가 발생할 수 있고</p>
<p>이것이 클로저의 가장 큰 단점이라고 할 수 있다.</p>
<hr>
<h3 id="iife-immediately-invoked-function-expression">IIFE (Immediately Invoked Function Expression)</h3>
<p>클로저는 다양한 곳에 활용되지만, 캡슐화에도 사용이 가능하다.</p>
<p>IIFE는 즉시 실행 함수로 불리며, 함수를 정의하고 곧바로 호출하는 패턴이다.</p>
<p>예를 들어 카운트 기능이 있다고 가정해보자</p>
<pre><code class="language-javascript">const counter = (function () {
  let count = 0;
  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    getCount() {
      return count;
    }
  };
})();

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount());  // 2
console.log(counter.count); // undefined
</code></pre>
<p>이렇게 선언하자마자 호출을 하게 되면</p>
<p>counter에는 아래의 값이 담기게 된다.</p>
<pre><code class="language-javascript">...
return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    getCount() {
      return count;
    }
  };
...</code></pre>
<p>이후 외부에서 count에 접근은 불가능하게 되고 <code>increment, getCount</code> 같은 메서드를 통해서만 조작이 가능하게 할 수 있다.</p>
<h2 id="맺음말">맺음말</h2>
<p>클로저의 개념은 알고 있었지만, 내가 실제로 클로저를 제대로 알고 활용했나 의문이 들 정도로 중요한 개념이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[당신이 로그인을 구현할 때 반드시 알아야하는 것들]]></title>
            <link>https://velog.io/@today-is-first/%EB%8B%B9%EC%8B%A0%EC%9D%B4-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%A0-%EB%95%8C-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%98%EB%8A%94-%EA%B2%83%EB%93%A4</link>
            <guid>https://velog.io/@today-is-first/%EB%8B%B9%EC%8B%A0%EC%9D%B4-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%A0-%EB%95%8C-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%98%EB%8A%94-%EA%B2%83%EB%93%A4</guid>
            <pubDate>Sat, 19 Jul 2025 14:09:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>로그인 기능이 없는 서비스는 거의 없습니다. 특히 프로젝트를 진행하게 되면 반복적으로 로그인 기능을 구현하게 되는데, 이번 프로젝트를 진행하며 고민한 것들을 정리해봤습니다.</p>
</blockquote>
<h2 id="stateful-vs-stateless">Stateful vs Stateless</h2>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/0fc3f8bc-ca46-47bf-9efb-056af3e55405/image.png" alt=""></p>
<p>대표적인 로그인 방식은 <strong>stateful / stateless</strong> 두 가지로 나뉩니다.</p>
<p>서버에서 사용자의 인증 상태를 직접 기억하고 있으면 <strong>stateful</strong> 하다고 하고 일반적으로 <code>세션</code> 방식이 해당됩니다.</p>
<p>반면, 서버에서 사용자의 인증 상태를 기억하지 않으면 <strong>stateless</strong> 하다고 하고 일반적으로 <code>토큰</code> 기반 인증이 사용됩니다.</p>
<h2 id="세션-방식">세션 방식</h2>
<p>우선 세션 방식이 왜 생기게 됐는지부터 알아봅시다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/4c328c66-4e5b-462a-8480-10f050a5f7bc/image.png" alt=""></p>
<p>이를 이해하기 위해선 HTTP의 가장 중요한 특징인 비연결성과 비상태성을 알아야 합니다.</p>
<p>HTTP를 잘모르시는 분들은 <a href="https://velog.io/@today-is-first/HTTP%EB%8A%94-%EC%99%9C-0.9%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%A0%EA%B9%8C">HTTP는 왜 0.9부터 시작할까</a> 이 글을 읽어보세요.</p>
<p>쉽게 설명하면 <code>요청과 응답이 오고가면 연결은 끊긴다</code>, <code>서버는 이전 요청 정보를 기억하지 못한다</code> 입니다.</p>
<p>예를 들어, 사용자가 <strong>1번 게시물 조회</strong> 요청을 보내고, 서버에서 응답을 했다고 합시다.
이후 사용자가 <strong>&quot;이전 요청 기준 다음 게시물 정보 조회&quot;</strong> 요청을 보내더라도 HTTP는 상태를 저장하지 않기 때문에 서버는 적절한 응답을 할 수 없게 됩니다.</p>
<p>그렇기 때문에 사용자가 <strong>로그인 했는지, 어떤 게시물을 읽었는지</strong> 같은 사용자별 상태를 유지할 수 없습니다.</p>
<p>이를 해결하기 위해 등장한 것이 <strong>세션</strong>입니다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/9104b043-da0b-429d-bc9b-d463caddb136/image.png" alt=""></p>
<p>서버는 사용자가 로그인하는 등의 특정 행동을 했을 때, 해당 사용자에 대한 정보를 <strong>서버에 저장하고</strong>,</p>
<p>이를 구분하기 위한 고유한 식별자(SID, Session ID)를 생성해 클라이언트에게 전달합니다.</p>
<p>이후 클라이언트는 요청마다 이 SID를 함께 보내고, 서버는 이를 통해 <strong>누구인지 식별하고 이전 상태를 기억</strong>할 수 있게 됩니다.</p>
<p>보통 세션은 <code>쿠키</code>에 보관하는데 쿠키가 무엇인지 알아봅시다.</p>
<h2 id="쿠키는-왜-탄생했을까요">쿠키는 왜 탄생했을까요?</h2>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/e14891f4-18c3-446a-9aea-1a79b326c327/image.png" alt=""></p>
<p>쿠키는 넷스케이프의 직원이었던 <strong>루 몬툴리(louis j montulli ii)</strong>에 의해 만들어졌습니다.</p>
<p>당시 MCI라는 미국의 통신 회사와 <strong>전자 상거래 프로그램</strong>을 개발하면서 장바구니 같은 정보를 어딘가에 저장할 필요가 있었습니다.</p>
<p>각 고객의 정보들을 서버에 저장을 하게 되면 서버에 부하 및 비용이 발생하게 되어서 클라이언트에 저장할 수 있는 방법을 고안했고, 그것이 <strong>쿠키</strong>입니다.</p>
<blockquote>
<p>쿠키의 어원이 정확히 밝혀지지는 않았지만, 과거 유닉스 개발자들이 수신 후 그대로 전송하는 데이터 블록을 매직 쿠키로 불렀고 여기서 유래됐다는 게 유력합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/cc8fa1f2-fed8-4622-ab7a-6f3ab77bee8a/image.png" alt=""></p>
<p>탄생 배경을 알면 특징을 알 수 있죠</p>
<p>그래서 쿠키는 <strong>클라이언트에서 관리된다는</strong> 특징이 있습니다.</p>
<p>쿠키는 HTTP 요청 시마다 자동으로 요청 헤더에 포함되어 전송되므로, 서버가 세션 ID(SID)를 쿠키에 설정하면 이후 클라이언트의 모든 요청에 해당 SID가 자동으로 포함됩니다.</p>
<p>쿠키에는 <code>Secure, HttpOnly, SameSite</code>와 같은 보안 옵션을 설정할 수 있어,
클라이언트 측에서 데이터를 저장하는 방식 중에서는 상대적으로 보안성이 높은 편입니다.</p>
<blockquote>
<p>클라이언트에 저장할 수 있는 보관소는 뭐가 있을까요?</p>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th><strong>Cookie</strong></th>
<th><strong>LocalStorage</strong></th>
<th><strong>SessionStorage</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>저장 위치</strong></td>
<td>브라우저 (클라이언트)</td>
<td>브라우저</td>
<td>브라우저</td>
</tr>
<tr>
<td><strong>서버 전송 여부</strong></td>
<td>✅ 매 요청 시 자동 전송 (<code>Cookie</code> 헤더)</td>
<td>❌ 직접 전송 필요</td>
<td>❌ 직접 전송 필요</td>
</tr>
<tr>
<td><strong>만료 시점</strong></td>
<td>설정한 <code>Expires</code>/<code>Max-Age</code> 또는 세션 종료</td>
<td>영구 저장 (삭제 전까지 유지)</td>
<td>탭/창 닫을 때 삭제</td>
</tr>
<tr>
<td><strong>저장 용량</strong></td>
<td>약 4KB</td>
<td>약 5~10MB</td>
<td>약 5~10MB</td>
</tr>
<tr>
<td><strong>보안 옵션</strong></td>
<td>✅ <code>Secure</code>, <code>HttpOnly</code>, <code>SameSite</code> 설정 가능</td>
<td>❌ 없음</td>
<td>❌ 없음</td>
</tr>
<tr>
<td><strong>브라우저 접근</strong></td>
<td>JavaScript로 접근 가능 (단, <code>HttpOnly</code>면 불가)</td>
<td>JavaScript로 접근 가능</td>
<td>JavaScript로 접근 가능</td>
</tr>
<tr>
<td><strong>사용 예시</strong></td>
<td>세션 ID, 로그인 상태 유지</td>
<td>캐시, 비회원 장바구니</td>
<td>단기 상태(탭 단위) 저장</td>
</tr>
</tbody></table>
<p>클라이언트 측 저장 방식에는 대표적으로 <strong>Cookie</strong>, <strong>LocalStorage</strong>, <strong>SessionStorage</strong>가 있습니다.  </p>
<p>이 중에서는 <strong>쿠키가 상대적으로 보안 옵션이 다양하여 보안성이 높은 편</strong>이지만,<br><strong>클라이언트에 저장되는 모든 데이터는 XSS 등의 공격에 노출될 수 있어</strong> 민감한 정보를 저장할 때는 항상 주의가 필요합니다.</p>
<blockquote>
<p><strong>&quot;그러면 클라이언트에 저장할 때 쿠키쓰고 모든 보안 옵션 걸어놓으면 되겠네요?&quot;</strong></p>
</blockquote>
<p>만약 여러분이 이 글을 읽으시고 <strong>&quot;보안 옵션 = 좋은 거&quot;</strong>, 그냥 다 걸어야겠다 생각하시면 큰일이 날 수도 있습니다.</p>
<p>예를 들어 OAuth 로그인(OIDC) 구현을 한다고 가정해보죠</p>
<blockquote>
<p>OAuth를 잘모르시는 분들은 아래 글들을 읽어보세요
<a href="https://velog.io/@today-is-first/OAuth%EB%8A%94-%EC%96%B4%EC%A9%8C%EB%8B%A4-%ED%83%84%EC%83%9D%ED%96%88%EC%9D%84%EA%B9%8C">OAuth는 어쩌다 탄생했을까</a>, <a href="https://velog.io/@today-is-first/OAuth-2.02.1-OIDC">OAuth 2.0~2.1</a>, <a href="https://velog.io/@today-is-first/%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%80-OAuth%EA%B0%80-%EC%95%84%EB%8B%88%EB%8B%A4">소셜 로그인은 OAuth가 아니다?!</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/a037eafb-40b3-4278-9c9d-f75b3b5d8538/image.png" alt=""></p>
<p>OIDC는 위 그림과 같은 방식으로 진행됩니다.</p>
<ol>
<li><p>사용자가 로그인 버튼 클릭 → 팝업 또는 리다이렉트로 Kakao 인증 서버로 이동 (accounts.kakao.com)</p>
</li>
<li><p>인증 성공 → 다시 우리 사이트로 리다이렉트 (예: 우리 서비스.com/oauth/callback)</p>
</li>
<li><p>서버는 전달받은 <strong>Authorization Code</strong>를 이용해 Kakao로 부터 ID Token 발급 및 발리데이션 진행</p>
</li>
<li><p>이후, DB에 등록된 사용자인지 확인 후 엑세스 토큰을 쿠키에 저장함</p>
</li>
<li><p>클라이언트는 로그인 후 메인 페이지로 이동 → 로그인 된 상태로 보여야 함</p>
</li>
</ol>
<p>하지만, 클라이언트가 메인 페이지로 이동을 하게 될 때 쿠키에 <strong>SameSite=Lax</strong>일 경우, 리다이렉트 직후 요청은 크로스 사이트 컨텍스트라고 판단해서 브라우저가 요청에 쿠키를 안붙이게 됩니다.</p>
<p>서버는 엑세스 토큰이 없으니 사용자를 인식하지 못하고 <strong>“로그인 안 됨”</strong> 상태가 되게 됩니다.</p>
<p>만약 여러분이 개발자로 입사한 후,<br><strong>&quot;쿠키 보안 옵션은 좋은 거니까 전부 설정해야지&quot;</strong> 하고 무심코 머지하게 된다면,<br>해당 사이트는 <strong>사용자가 로그인해도 로그인되지 않은 것처럼 보이는 심각한 버그</strong>가 발생할 수 있습니다.</p>
<p>이제 세션 방식을 쓸 때 왜 쿠키를 사용하는지 이해하셨을 거라 생각하고</p>
<p>다시 세션으로 돌아가겠습니다</p>
<hr>
<h2 id="분산-서버에서-세션은-어떻게-관리될-수-있을까">분산 서버에서 세션은 어떻게 관리될 수 있을까</h2>
<p>세션 방식은 서버가 사용자의 인증 상태를 직접 관리한다는 특징이 있었죠.</p>
<p>소규모 서비스는 <strong>단일 서버 메모리</strong>에 세션 정보를 저장해도 충분합니다.</p>
<p>하지만 서비스 규모가 커져서 <strong>서버가 여러 대로 늘어나고 로드밸런싱 환경</strong>이라면 어떨까요?</p>
<p>사용자가 인증을 하더라도, 세션 정보는 <strong>처음 접속한 서버 메모리</strong>에만 세션 정보가 저장되기 때문에</p>
<p>사용자의 다음 요청이 다른 서버로 가게 된다면 로그인 되지 않은 상태로 인식되게 됩니다.</p>
<p>이러한 문제를 해결하기 위한 방법은 대표적으로 두 가지가 있습니다:</p>
<ol>
<li><strong>Redis</strong>와 같은 외부 저장소를 사용하여 <strong>세션을 중앙에서 관리</strong>  </li>
<li><strong>JWT와 같은 토큰 기반 인증</strong>으로 인증 상태를 클라이언트에 위임</li>
</ol>
<p>이 중 두 번째 방식인 토큰 기반 인증을 알아봅시다.</p>
<h2 id="토큰-기반-인증-방식">토큰 기반 인증 방식</h2>
<p>토큰 기반 인증 방식은 세션 방식과 달리, <strong>서버에 사용자 인증 상태를 저장하지 않습니다.</strong></p>
<p>그렇다면 서버는 어떻게 사용자가 로그인한 상태인지 판단할 수 있을까요?</p>
<p>정답은 바로, <strong>토큰 자체에 사용자 정보를 담는 것</strong>입니다.</p>
<p>사용자가 로그인하면, 서버는 <strong>JWT(JSON Web Token)</strong>와 같은 토큰을 생성하고,<br>이 토큰을 클라이언트가 저장한 뒤, 요청마다 함께 전송하게 됩니다.</p>
<p>서버는 이 토큰의 <strong>서명을 검증</strong>하고, 내부에 담긴 사용자 정보를 확인하여<br><strong>사용자를 인증</strong>합니다.</p>
<p>이 방식은 인증 상태를 서버가 별도로 보관하지 않기 때문에,<br><strong>분산된 서버 환경에서도 동일한 JWT만으로 사용자 인증이 가능</strong>하다는 장점이 있습니다.</p>
<blockquote>
<p>여기서 <strong>베어러 토큰(Bearer Token)</strong>의 개념을 아시는 분들은 토큰으로 <strong>인증</strong>을 한다는 게 이상하게 느껴질 수 있습니다.</p>
</blockquote>
<h2 id="bearer-token이란">Bearer Token이란?</h2>
<p><strong>Bearer</strong>는 &#39;소지자&#39;, &#39;보유하는 사람&#39;이라는 의미입니다.</p>
<p>즉, <strong>Bearer Token</strong>은 <strong>해당 리소스에 접근할 수 있는 권한을 가진 사람(소지자)</strong>에게 부여되는 토큰입니다.</p>
<p>여기서 중요한 포인트는, <strong>이 토큰은 권한만 나타낼 뿐, 실제로 인증된 사용자임을 보장하지는 않는다는 점입니다.</strong></p>
<p>Bearer Token을 가지고 있다고 해서, 그것이 <strong>해당 사용자가 실제 로그인하고 인증받은 주체</strong>라는 뜻은 아닙니다.</p>
<p>많은 사람들이 소셜 로그인을 <strong>OAuth</strong>라고 알고 있지만, <a href="https://velog.io/@today-is-first/%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%80-OAuth%EA%B0%80-%EC%95%84%EB%8B%88%EB%8B%A4">OAuth는 &#39;인가(Authorization)&#39;를 위한 프로토콜이지 &#39;인증(Authentication)&#39;을 위한 프로토콜은 아닙니다.</a></p>
<p>즉, OAuth만으로는 로그인(=인증)을 할 수 없으며,<br><strong>OAuth로 발급받은 액세스 토큰(Access Token)을 사용자 인증 수단으로 사용하는 것은 보안적으로 위험합니다.</strong><br>왜냐하면, OAuth의 액세스 토큰은 <strong>단순한 Bearer Token</strong>이기 때문입니다.</p>
<blockquote>
<p>그렇다면, 토큰 기반 방식에서 <strong>어떻게 사용자를 인증할 수 있을까요?</strong></p>
</blockquote>
<p>OIDC를 사용한 소셜 로그인에서는 ID Token으로 사용자를 인증할 수 있습니다.</p>
<p>다만, ID Token이 검증되었다 하더라도 Authorization Code로 OAuth 서버로 부터 받은 엑세스 토큰을 인증용 토큰으로 사용하면 안됩니다.</p>
<p>ID Token 검증 이후 우리 서비스 서버에서 <strong>자체 발급한 토큰을 인증용 토큰</strong>으로 사용해야 하며, <strong>Opaque 토큰</strong>이 아닌 <strong>JWT와 같이 클레임을 담고 있고, 서버가 서명을 검증할 수 있는 토큰</strong>을 사용해야 합니다.</p>
<p>그렇다면, 왜 OAuth 서버에서 발급한 Access Token은 인증 수단으로 쓰면 안 되고,<br>우리 서버에서 발급한 토큰은 인증 수단으로 사용할 수 있을까요?</p>
<p>JWT에 대해서 알아봅시다.</p>
<h2 id="jwt란">JWT란?</h2>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/0a2c5861-fba9-424b-a017-aa00af8907f5/image.png" alt=""></p>
<p><strong>JWT</strong>는 <strong>Header</strong>, <strong>Payload</strong>, <strong>Signature</strong> 세 부분으로 구성된 토큰입니다:</p>
<p>JWT의 가장 큰 장점은 바로 <strong><code>서명(Signature)</code>을 통해 토큰이 변조되지 않았음을 서버가 검증할 수 있다는 점</strong>입니다.</p>
<p>토큰은 클라이언트에 저장되기 때문에 언제든지 <strong>탈취되거나 변조될 위험이 존재</strong>합니다.<br>하지만 JWT는 서명을 통해 위변조 여부를 검증할 수 있기 때문에,<br><strong>서버는 이 토큰을 신뢰하고 사용자 인증에 활용할 수 있습니다.</strong></p>
<p>또한 <strong>Payload(페이로드)</strong>에 <code>userId</code>, <code>role</code>, <code>exp</code>, <code>iss</code> 등의 정보를 담을 수 있어,<br>추가적인 데이터 조회 없이도 다양한 인증/인가 로직에 활용이 가능합니다.</p>
<blockquote>
<p>즉, 서비스 자체적으로 발행하는 JWT 토큰의 경우 <strong>시그니처를 통해 검증을 할 수 있어서 인증에 사용</strong>할 수 있는 것입니다.</p>
</blockquote>
<hr>
<p>하지만 또 떠오르는 질문이 있죠</p>
<blockquote>
<p>위변조는 시그니처로 검증이 가능하지만, 토큰이 탈취되면 위험한 거 아닌가요?</p>
</blockquote>
<p>맞습니다.</p>
<p>토큰은 탈취당하면 어뷰징을 막기 어렵기 때문에 여러 보안적인 요소를 고려해야 합니다.</p>
<p>대표적인 방법을 설명드리겠습니다.</p>
<h3 id="1-엑세스-토큰의-만료-기한을-짧게-한다">1. 엑세스 토큰의 만료 기한을 짧게 한다.</h3>
<p>엑세스 토큰의 만료 기한을 15분 정도로 짧게 설정해두면, 해당 토큰이 탈취되어도 최대 15분밖에 어뷰징을 못하게 됩니다.</p>
<p>다만, 이럴 경우 엑세스 토큰이 만료되면 또 다시 로그인을 해야 하므로 UX에 큰 악영향을 끼칩니다.</p>
<h3 id="2-리프레시-토큰을-도입한다">2. 리프레시 토큰을 도입한다.</h3>
<p>이 문제를 해결하기 위해 일반적으로 <strong>Refresh Token</strong>을 함께 사용합니다.</p>
<ul>
<li><strong>Access Token</strong>: 특정 리소스에 대한 <strong>접근 권한</strong> 또는 <strong>사용자 인증 정보</strong>를 담고 있는 토큰</li>
<li><strong>Refresh Token</strong>: 새로운 Access Token을 <strong>재발급받을 수 있는 권한</strong>을 가진 토큰</li>
</ul>
<p>예를 들어, Access Token은 15분마다 만료되도록 설정하고,<br>Refresh Token은 <strong>1주일 정도의 만료 기한</strong>을 두면,<br>사용자는 로그인 한 번만 하면 <strong>1주일 동안은 추가 로그인 없이 서비스를 계속 이용</strong>할 수 있습니다.</p>
<blockquote>
<p>그러면, 리프레시가 탈취되면 오히려 일주일동안 어뷰징이 가능해 더 위험한 거 아닌가요?</p>
</blockquote>
<p>그래서 리프레시 토큰에 보안적인 요소도 고려해야 합니다.</p>
<h2 id="refresh-token-rotationrtr">Refresh Token Rotation(RTR)</h2>
<p>엑세스 토큰 A를 발급받을 때 엑세스 토큰 B를 발급할 수 있는 리프레시 토큰 1을 발급받는 방식입니다.</p>
<ul>
<li>클라이언트가 로그인 → Access Token A + Refresh Token 1 발급</li>
<li>Access Token A 만료 →</li>
<li>Refresh Token 1 사용 → Access Token B + Refresh Token 2 발급</li>
</ul>
<p>이럴 경우 리프레시 토큰이 탈취되어도 하나의 엑세스 토큰만 발급 받을 수 있기 때문에 과한 어뷰징을 방지할 수 있습니다.</p>
<blockquote>
<p>근데 리프레시 토큰 1으로 Access Token B를 발급받으면,<br>그때 받은 리프레시 토큰 2로 또 Access Token C를 발급받을 수 있잖아요?<br>그럼 결국 탈취자는 계속 어뷰징할 수 있는 거 아닌가요?</p>
</blockquote>
<p>그래서 <strong>Refresh Token Rotation(RTR)</strong>의 핵심은 바로 <strong>리프레시 토큰의 재사용을 감지하고 차단하는 것</strong>에 있습니다.</p>
<p>서버는 리프레시 토큰이 사용될 때마다 해당 정보를 <strong>기록</strong>하고 있다가,
<strong>이미 사용된 토큰이 다시 사용되면</strong> → <strong>탈취 시도로 간주</strong>하여 해당 세션을 무효화하거나
로그아웃 처리 등의 <strong>보안 조치를 수행</strong>합니다.</p>
<p>또한, 보안을 강화하기 위해 <strong>사용자 IP, User-Agent, Device 정보 등을 토큰에 바인딩</strong>하는 <strong>토큰 바인딩</strong> 기법도 함께 사용됩니다.</p>
<p>이를 통해 정당한 사용자와 탈취된 토큰 사용자를 구분하고, <strong>정상 사용자는 보호하면서 악의적인 요청만 차단</strong>할 수 있습니다.</p>
<h2 id="맺음말">맺음말</h2>
<p>처음에는 간단하게 작성하려고 했는데 글을 작성하다 보니 많은 내용을 작성하게 된 것 같습니다. 
로그인을 구현할 때 어떤 고민을 해야하는 지 정리해봤는데 처음 구현하시는 분들에게 많은 도움이 되었으면 좋겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[북마크 하나에도 스키마 설계가 필요하다고요?]]></title>
            <link>https://velog.io/@today-is-first/%EC%9A%B0%EB%A6%AC-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EB%A7%9E%EB%8A%94-%EC%8A%A4%ED%82%A4%EB%A7%88%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@today-is-first/%EC%9A%B0%EB%A6%AC-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EB%A7%9E%EB%8A%94-%EC%8A%A4%ED%82%A4%EB%A7%88%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sat, 19 Jul 2025 07:30:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프로젝트를 진행하며 우리 서비스에 맞는 데이터 스키마는 무엇일지 고민한 과정을 기록해봤다.</p>
</blockquote>
<h2 id="개요">개요</h2>
<p>나는 매일 저녁 런닝을 뛰면서 GPT와 함께 면접 연습을 한다. 프롬포트로 나에 대한 정보와 내 대답에 맞는 꼬리 질문을 생성하게 하여 연습하는데 <strong>이를 웹 프로젝트로 구현하면 어떨까</strong> 라는 생각에서 우리 프로젝트는 시작됐다.</p>
<p>프로젝트의 핵심 기능은 <code>AI와 혼자 연습하기</code>, <code>사람과 1대1로 연습하기</code>, <code>면접 질문 세트 공유</code> 3가지이고, 이 글에서 다룰 것은 <code>면접 질문 세트 공유</code> 기능을 기획하며 나눈 회의 내용이다.</p>
<h2 id="무엇이-문제였는가">무엇이 문제였는가</h2>
<p>예를 들면 누군가 공유해놓은 아래와 같은 면접 질문 세트가 있다고 가정해보자
<img src="https://velog.velcdn.com/images/today-is-first/post/f62989b8-c19c-4c03-9e4a-85f852f95054/image.png" alt=""></p>
<p>이 면접 질문 세트는 DB에 아래와 같은 형식으로 저장될 것이다.</p>
<blockquote>
<p>참고. 예시를 들기 위해 엄청 간소화 한 버전이고
실제는 아래 처럼 구성되어 있다.
<code>[question_sets] --(1:N)--&gt; [questions] --(1:1)--&gt; [answers]</code></p>
</blockquote>
<table>
<thead>
<tr>
<th>질문 세트 ID</th>
<th>질문 세트 제목</th>
<th>질문</th>
<th>답변</th>
<th>작성자</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>면접 질문 세트</td>
<td>토큰과 세션의 차이는 무엇인가요?</td>
<td>토큰은 ... 세션은 ... 입니다.</td>
<td>A</td>
</tr>
</tbody></table>
<p>만약 우리 서비스 <strong>이용자 B</strong>가 해당 질문 세트가 맘에 들어서 북마크를 하고 사용한다고 가정해보자</p>
<p>문제는 <strong>질문 세트 작성자 A</strong>가 해당 질문 세트를 수정할 때 발생한다.</p>
<table>
<thead>
<tr>
<th>질문 세트 ID</th>
<th>질문 세트 제목</th>
<th>질문</th>
<th>답변</th>
<th>작성자</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>면접 질문 세트</td>
<td>HTTP는 무엇인가요?</td>
<td>HTTP는 HyperText Transfer Protocol의 약자로...</td>
<td>A</td>
</tr>
</tbody></table>
<p>이렇게 되면 이용자 B는 본인이 찜을 했던 시점과 전혀 다른 질문 세트를 북마크하게 된다.</p>
<hr>
<p>그러면 여기서 판단을 해야한다.
이용자 B는 다음 중 어떤 방식을 원할까?</p>
<p><strong>1. 질문 세트의 수정 사항이 자동으로 반영</strong>
원하지 않은 질문이나 변경된 답변까지 적용되어 혼란을 줄 수 있음</p>
<p><strong>2. 질문 세트가 수정되더라도 내가 찜한 시점의 내용이 유지</strong>
질문 세트를 복제해서 따로 저장해야 하므로 DB 구조를 고민해야 함</p>
<blockquote>
<p>예를 들어, A 사용자가 아래 질문 세트를 북마크하고 연습했다고 가정해보자.</p>
</blockquote>
<table>
<thead>
<tr>
<th>번호</th>
<th>질문</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>쿠키와 세션의 차이는?</td>
</tr>
<tr>
<td>2</td>
<td>JWT는 왜 사용하는가?</td>
</tr>
</tbody></table>
<p>며칠 뒤 A가 다시 복습하려고 열었는데, 질문 세트가 아래처럼 바뀌어 있다.</p>
<table>
<thead>
<tr>
<th>번호</th>
<th>질문</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>HTTP와 HTTPS의 차이는?</td>
</tr>
<tr>
<td>2</td>
<td>JWT와 OAuth의 차이는?</td>
</tr>
</tbody></table>
<p>이 경우 A는 &quot;내가 연습하던 질문이 아니네?&quot;, &quot;답변도 달라졌고, 복습이 어렵다...&quot; 라는 혼란을 겪게 된다.</p>
<p>따라서 우리는 사용자가 북마크한 시점의 질문 세트가 그대로 유지되어야 한다고 판단했다.</p>
<p>이 방식이 사용자 경험 측면에서 더 신뢰할 수 있고 예측 가능하기 때문이다.</p>
<blockquote>
<p>그러면 북마크 할 때 어떻게 DB에 저장해야 할까?</p>
</blockquote>
<p>우선 나이브하게 생각해보자</p>
<p>사용자가 질문 세트를 북마크하는 시점에 해당 질문 세트를 복제해서 관리하는 경우이다.</p>
<table>
<thead>
<tr>
<th>질문 세트 ID</th>
<th>질문 세트 제목</th>
<th>질문</th>
<th>답변</th>
<th>작성자</th>
<th>구독자</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>면접 질문 세트</td>
<td>토큰과 세션의 차이는 무엇인가요?</td>
<td>토큰은 ... 세션은 ... 입니다.</td>
<td>A</td>
<td></td>
</tr>
<tr>
<td>2</td>
<td>면접 질문 세트</td>
<td>토큰과 세션의 차이는 무엇인가요?</td>
<td>토큰은 ... 세션은 ... 입니다.</td>
<td>A</td>
<td>B</td>
</tr>
</tbody></table>
<p>이제, 이용자 B는 작성자 A가 수정하여도 북마크 시점과 같은 질문세트를 볼 수 있게 되었다. 해피 엔딩..?</p>
<blockquote>
<p>100만 명이 해당 질문 세트를 북마크하게 되면 어떻게 될까</p>
</blockquote>
<table>
<thead>
<tr>
<th>질문 세트 ID</th>
<th>질문 세트 제목</th>
<th>질문</th>
<th>답변</th>
<th>작성자</th>
<th>구독자</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>면접 질문 세트</td>
<td>토큰과 세션의 차이는 무엇인가요?</td>
<td>토큰은 ... 세션은 ... 입니다.</td>
<td>A</td>
<td></td>
</tr>
<tr>
<td>2</td>
<td>면접 질문 세트</td>
<td>토큰과 세션의 차이는 무엇인가요?</td>
<td>토큰은 ... 세션은 ... 입니다.</td>
<td>A</td>
<td>B</td>
</tr>
<tr>
<td>3</td>
<td>면접 질문 세트</td>
<td>토큰과 세션의 차이는 무엇인가요?</td>
<td>토큰은 ... 세션은 ... 입니다.</td>
<td>A</td>
<td>C</td>
</tr>
<tr>
<td>4</td>
<td>면접 질문 세트</td>
<td>토큰과 세션의 차이는 무엇인가요?</td>
<td>토큰은 ... 세션은 ... 입니다.</td>
<td>A</td>
<td>D</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody></table>
<p>딱 봐도 불필요한 데이터가 반복되는 게 느껴진다.</p>
<blockquote>
<p>그러면 우리는 이 데이터를 어떻게 우아하게 관리할 수 있을까?</p>
</blockquote>
<p><code>면접 질문세트</code></p>
<table>
<thead>
<tr>
<th>질문 세트 ID</th>
<th>버전</th>
<th>질문 세트 제목</th>
<th>질문</th>
<th>답변</th>
<th>작성자</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1</td>
<td>면접 질문 세트</td>
<td>토큰과 세션의 차이는 무엇인가요?</td>
<td>토큰은 ... 세션은 ... 입니다.</td>
<td>A</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>면접 질문 세트</td>
<td>HTTP는 무엇인가요?</td>
<td>HTTP는 HyperText Transfer Protocol의 약자로...</td>
<td>A</td>
</tr>
</tbody></table>
<p><code>면접 질문 북마크 테이블</code></p>
<table>
<thead>
<tr>
<th>구독 ID</th>
<th>질문 세트 ID</th>
<th>버전</th>
<th>구독자</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1</td>
<td>1</td>
<td>B</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>1</td>
<td>C</td>
</tr>
<tr>
<td>3</td>
<td>1</td>
<td>2</td>
<td>D</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody></table>
<p>아까와는 확실히 중복된 데이터가 적은 게 느껴진다.</p>
<p>이제 사용자마다 다른 버전 참조도 가능하면서 중복된 데이터를 방지하는 구조를 가지게 됐다.</p>
<p>근데 이 방식대로 라면... </p>
<blockquote>
<p>질문 하나라도 바뀌면 새로운 버전의 전체 질문 세트를 생성해야 하는 거 아냐?</p>
</blockquote>
<p>그래서 세트 / 질문 둘 다 버전으로 관리하기로 결정했다.</p>
<p>질문 하나가 수정되더라도 다른 질문들이 중복적으로 생성될 필요없게 되었다.</p>
<h2 id="맺음말">맺음말</h2>
<p>북마크라는 하나의 작은 기능을 기획하면서도 굉장히 고민할 게 많다고 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TDD를 적용하고 나의 성공시대 시작됐다]]></title>
            <link>https://velog.io/@today-is-first/TDD%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B3%A0-%EB%82%98%EC%9D%98-%EC%84%B1%EA%B3%B5%EC%8B%9C%EB%8C%80-%EC%8B%9C%EC%9E%91%EB%90%90%EB%8B%A4</link>
            <guid>https://velog.io/@today-is-first/TDD%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B3%A0-%EB%82%98%EC%9D%98-%EC%84%B1%EA%B3%B5%EC%8B%9C%EB%8C%80-%EC%8B%9C%EC%9E%91%EB%90%90%EB%8B%A4</guid>
            <pubDate>Sat, 19 Jul 2025 06:21:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번에는 TDD를 프로젝트에 적용해보면서 첫 날에 느낀 장점에 대해 정리를 하는 글을 작성하게 됐습니다. 제가 TDD를 프로젝트에 적용한 이유가 궁금하신 분들은 이전 글 <a href="https://velog.io/@today-is-first/%EC%9D%B4%EB%B2%88-%ED%94%8C%EC%A0%9D%EC%97%90-TDD%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%B4%EB%B3%BC%EA%B9%8C">이번 플젝에 TDD를 도입해볼까?</a> 참고 바랍니다.</p>
</blockquote>
<h2 id="tdd란-무엇인가">TDD란 무엇인가?</h2>
<p>이전 글에도 작성하였지만 TDD는 단순히 기능을 만들기 전에 테스트를 먼저 짜는 것만 의미하는 게 아니다. 테스트를 먼저 짜는 행위가 가져오는 파급 효과를 생각해보자.</p>
<blockquote>
<p><strong>테스트를 먼저 짜기 위해선 무엇을 알아야 할까?</strong></p>
</blockquote>
<p>테스트는 해당 기능이 정상적으로 동작하는지 확인하는 과정이다. 즉, 테스트를 작성하기 위해선 <strong>해당 기능이 어떤 동작을 해야하는지</strong> 알아야 한다. 또한 해당 기능이 <strong>테스트에 용이한 구조를 가지게끔 설계</strong>해야한다.</p>
<blockquote>
<p><strong>테스트하기 쉬운 구조가 왜 좋은 코드일까?</strong></p>
</blockquote>
<p>만약 여러분이 아래와 같은 함수를 테스트 해야한다고 가정해보자.</p>
<pre><code class="language-javascript">function 주문처리(상품목록, 할인율) {
  // 1. 총액 계산
  let 총액 = 0;
  for (let i = 0; i &lt; 상품목록.length; i++) {
    총액 += 상품목록[i].가격 * 상품목록[i].수량;
  }

  // 2. 할인 적용
  const 할인금액 = 총액 * 할인율;
  const 최종금액 = 총액 - 할인금액;

  // 3. 영수증 출력 형식 구성
  const 영수증 = {
    총액: 총액,
    할인금액: 할인금액,
    최종금액: 최종금액,
    항목수: 상품목록.length
  };

  return 영수증;
}</code></pre>
<p>이 함수는 아래와 같은 <strong>3가지 책임</strong>을 갖고 있다.</p>
<ul>
<li>상품들의 총 금액 계산</li>
<li>할인 금액 적용</li>
<li>총액, 할인금액 등 영수증 출력 형식 구성</li>
</ul>
<p>즉, 테스트 시 <code>특정 로직만 검증하기가 어렵고 로직 변경 시 다른 부분까지 영향</code> 받기 쉬운 상태이다.</p>
<p>위 함수를 테스트하기 쉬운 구조로 분리하면 어떻게 될까?</p>
<pre><code class="language-javascript">function 총액계산(상품목록) { ... }
function 할인적용(총액, 할인율) { ... }
function 영수증생성(총액, 할인금액, 항목수) { ... }</code></pre>
<p>각 함수들이 하나의 책임만 갖게 되어 특정 기능만 테스트 할 수 있고, 다른 기능에 영향을 주지 않는 코드가 탄생하게 된다.</p>
<p><code>즉, 단일 책임 원칙(SRP)를 잘 지키는 코드 구조를 갖게 된다는 것이다.</code></p>
<p>그래서 내가 생각하는 <strong>TDD의 장점</strong>은 다음과 같다.</p>
<ul>
<li><strong>테스트 하기 쉬운 코드를 고민하다보니 좋은 구조를 가진 코드를 작성하게 된다.</strong></li>
<li><strong>해당 기능이 어떤 입력을 받고, 어떤 출력을 내야 할지 명확해진다.</strong></li>
<li><strong>리팩토링에 대한 두려움이 줄어든다</strong></li>
</ul>
<p>위의 예시로 앞의 장점 두 개는 충분히 설명된 것 같고, <code>리팩토링에 대한 두려움이 줄어든다</code>는 밑에 서술하겠다.</p>
<hr>
<h2 id="tdd의-신호등">TDD의 신호등</h2>
<p>TDD는 3가지 단계를 거쳐 진행된다.</p>
<ul>
<li>🔴<code>Red</code> : 통과에 실패하는 테스트 코드를 먼저 작성</li>
<li>🟢<code>Green</code> : 테스트 코드를 성공시키기 위해 실제 코드 작성</li>
<li>🔵<code>Blue(Refactor)</code> : 중복 코드 제거, 일반화 등 리팩토링 수행</li>
</ul>
<p>여기서 핵심은 <code>실패하는 테스트 코드</code>를 먼저 작성하는 것이다. 테스트 코드를 작성하다보면 개발자의 실수로 무조건 통과하거나 의미 없는 테스트 코드를 작성하게 된다. 특히, 프론트엔드에서 비동기로 동작하는 메서드를 테스트 할 때 주의해야한다.</p>
<h2 id="프론트엔드에서-tdd를-어떻게-적용할-수-있을까">프론트엔드에서 TDD를 어떻게 적용할 수 있을까?</h2>
<blockquote>
<p>React의 테스트 툴인 React Testing Library에서는 &quot;너의 컴포넌트가 어떻게 구현되었는지가 아니라, 어떻게 동작하는지를 테스트하라&quot;고 말한다.</p>
</blockquote>
<p>div가 몇 겹이고 내부에서 useEffect가 실행되었는지 등 내부 구현에 집착하는 테스트를 하지 말아야 한다.</p>
<p>사용자 입장에서 컴포넌트를 바라보고, 클릭했을 때 어떤 변화가 일어나는지 혹은 텍스트가 정확히 렌더링되는지 테스트 해야한다.</p>
<blockquote>
<p>이 컴포넌트를 테스트해보자!</p>
</blockquote>
<p>아래와 같이 클릭하면 로그인 페이지로 이동하는 버튼 컴포넌트가 있다
이 컴포넌트는 무엇을 테스트 해야할까?</p>
<pre><code class="language-jsx">const LoginButton = () =&gt; {
  return (
    &lt;Link
      to=&quot;/login&quot;
    &gt;
      로그인
    &lt;/Link&gt;
  );
};</code></pre>
<p>사용자 관점에서 바라보면 된다.</p>
<ul>
<li>페이지에 버튼 컴포넌트가 렌더링 되어야 함</li>
<li>클릭했을 때 로그인 페이지로 이동해야 함</li>
</ul>
<pre><code class="language-jsx">describe(&quot;LoginButton 컴포넌트&quot;, () =&gt; {

  it(&quot;버튼이 렌더링되어야 한다&quot;, () =&gt; {
    render(
      &lt;MemoryRouter&gt;
        &lt;LoginButton /&gt;
      &lt;/MemoryRouter&gt;
    );

    const linkElement = screen.getByRole(&quot;link&quot;, { name: &quot;로그인&quot; });
    expect(linkElement).toBeInTheDocument();
  });

  it(&quot;버튼 클릭 시 /login 페이지로 이동해야 한다&quot;, () =&gt; {
    render(
      &lt;MemoryRouter initialEntries={[&quot;/&quot;]}&gt;
        &lt;Routes&gt;
          &lt;Route path=&quot;/&quot; element={&lt;LoginButton /&gt;} /&gt;
          &lt;Route path=&quot;/login&quot; element={&lt;div&gt;로그인 페이지입니다&lt;/div&gt;} /&gt;
        &lt;/Routes&gt;
      &lt;/MemoryRouter&gt;
    );

    const linkElement = screen.getByRole(&quot;link&quot;, { name: &quot;로그인&quot; });

    // 클릭 이벤트 발생
    fireEvent.click(linkElement);

    // 로그인 페이지로 이동했는지 확인
    expect(screen.getByText(&quot;로그인 페이지입니다&quot;)).toBeInTheDocument();
  });
});
</code></pre>
<p>이렇게 두 가지만 테스트하면 되고, 이 로그인 버튼이 어떤 태그 안에 생겼는지 등은 테스트하지 않아도 된다.</p>
<blockquote>
<p>여기서 잠깐 토막 상식!</p>
</blockquote>
<p>React Testing Library에서는 DOM 요소를 가져올 때 <code>getByRole</code>을 통해 가져오는 것을 권장한다. <strong>getById, querySelector</strong>는 많이 알겠지만 <strong>Role</strong>은 생소한 사람들이 많을 것이다. 이것은 WAI 라는 웹 접근성 협회에서 지정한 <strong>ARIA-Role</strong>을 기반으로 요소를 찾는 메서드이다.</p>
<h3 id="aria-role이란">ARIA-Role이란?</h3>
<p><strong>ARIA-Role</strong>은 웹 접근성 향상을 위해 HTML 요소가 어떤 역할을 하는지 명시하는 속성이다. 시각장애인 등 보조기기를 사용하는 사용자들이 웹을 이해하고 탐색할 수 있도록 도와준다.</p>
<p>Accessible Rich Internet Applications의 약자이며, 대표적으로 <code>button, link, textbox, heading</code> 등이 있고 시멘틱 태그를 사용한다면 대부분의 요소에 이 역할이 자동으로 적용되게 된다.</p>
<p>프론트엔드를 처음 배울 때 시멘틱 마크업의 중요성에 대해 배우게 된다. 하지만 구현을 하다보면 태그가 가진 의미대로 HTML 구조를 짜는 게 번거롭다는 이유로 필요성을 간과하곤 한다. 나도 그러했다.</p>
<blockquote>
<p>시각 장애인은 웹을 어떻게 이해할 수 있을까?</p>
</blockquote>
<p>시각장애를 가진 강사님의 웹 접근성 특강을 듣고 내 생각이 바뀌게 됐다. 시각 장애인은 스크린 리더와 같은 보조 도구를 통해 웹을 이해한다. 이 도구는 단순히 텍스트를 읽는 것이 아니라, 웹에 있는 태그들을 기반으로 구조와 의미를 파악해 사용자에게 전달한다.</p>
<p>예를 들어 <code>&lt;h1&gt;</code> 태그는 &quot;여기가 가장 중요한 제목이다&quot;라는 의미로, <code>&lt;button&gt;</code> 태그는 &quot;이건 클릭할 수 있는 버튼이다&quot;라고 안내한다.</p>
<p>하지만 만약에 시맨틱 태그 대신 <code>&lt;div&gt;</code>나 <code>&lt;span&gt;</code>만을 사용해 구성된 페이지라면, 스크린 리더는 그것이 무엇인지 알 수 없어 사용자는 혼란을 겪거나 주요 기능에 접근조차 하지 못할 수 있다.</p>
<p>이러한 이유로 웹 접근성에서는 <strong>시멘틱 마크업과 ARIA-Role</strong>이 매우 중요하다. 개발자가 마크업을 어떻게 작성하느냐에 따라 누군가에게는 아무것도 안보이는 세상이 될 수도 있다. <strong>만약 당신이 프론트엔드 개발자라면 <code>getByRole</code>을 습관화하여 누구에게나 보이는 세상을 만들어보자.</strong></p>
<h2 id="tdd를-적용하고-나의-성공시대-시작됐다">TDD를 적용하고 나의 성공시대 시작됐다.</h2>
<p>TDD를 처음 적용해보면서 많은 시행착오를 겪고 &quot;&quot;이게 실제로 유용한가&quot;&quot;라는 끊임없는 의구심이 들었다. 이 생각이 <strong>180도 바뀌게 된 계기</strong>가 있었는데</p>
<p>나와 프론트엔드 팀원은 테스트 코드 작성에 익숙하지 않았기 때문에 연습하기 위해 서로 드라이버 / 네비게이터가 되어주면서 기본 컴포넌트를 구현하였다.</p>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/dd0d6b34-274e-46a5-85e3-ab65a4c7f2fa/image.png" alt=""></p>
<p>이 사진에 있는 컴포넌트이고, 중복된 코드를 분리하는 리팩토링이 필요했다.</p>
<pre><code class="language-jsx">function SoloPracticeButton() {
  const navigate = useNavigate();
  const handleClick = () =&gt; {
    navigate(&#39;/solo-practice&#39;);
  };
  return &lt;button onClick={handleClick}&gt;혼자 연습하기&lt;/button&gt;;
}

function PairPracticeButton() {
  const navigate = useNavigate();
  const handleClick = () =&gt; {
    navigate(&#39;/pair-practice&#39;);
  };
  return &lt;button onClick={handleClick}&gt;같이 연습하기&lt;/button&gt;;
}</code></pre>
<p>따라서, 아래와 같이 공통 기능을 하는 <code>PracticeButton</code> 컴포넌트를 만들고 각 컴포넌트에 적용하였다.</p>
<pre><code class="language-jsx">
function PracticeButton({ text, path }: PracticeButtonProps) {
  const navigate = useNavigate();
  return &lt;button onClick={() =&gt; navigate(path)}&gt;{text}&lt;/button&gt;;
}

function PairPracticeButton() {
  return &lt;PracticeButton text=&quot;같이 연습하기&quot; path=&quot;/pair-practice&quot; /&gt;;


function SoloPracticeButton() {
    return &lt;PracticeButton text=&quot;혼자 연습하기&quot; path=&quot;/solo-practice&quot; /&gt;;</code></pre>
<p>이 리팩토링 과정에서 혼자/같이 연습하기 버튼 컴포넌트의 변경이 있었지만 이미 TDD로 진행하며 작성된 테스트가 존재하였기 때문에 테스트가 통과된다면 이 기능이 정상적으로 동작한다는 확신을 가질 수 있었다. 무엇보다 놀라웠던 점은, 프론트 서버를 실행하지 않고도 기능을 검증할 수 있었다는 점이다.</p>
<p><a href="https://www.youtube.com/watch?v=L1dtkLeIz-M">[A5] 프론트엔드에서 TDD가 가능하다는 것을 보여드립니다.</a></p>
<p>이 영상을 보며 <strong>테스트 코드 통과 여부만 확인하며 개발</strong>한다는 게 이해가 잘 안갔는데, 실제로 TDD를 적용해보니 우리도 테스트 통과 여부만 확인하고 굳이 동작 테스트를 안하게 되었다.</p>
<p>TDD의 가장 강력한 장점은 <code>리팩토링</code> 할 때 느껴진다.</p>
<h2 id="맺음말">맺음말</h2>
<p>물론 TDD를 적용해본지 얼마 안됐다 보니 경험이 유니크하지는 않을 수 있어도 내 개발 인생에서는 굉장히 유니크한 경험이었다. 앞으로 TDD를 적용해보며 겪은 시행착오를 잘 정리해서 추가적으로 올리겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jest 주로 사용하는 메서드 모음]]></title>
            <link>https://velog.io/@today-is-first/Jest-%EC%A3%BC%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%A9%94%EC%84%9C%EB%93%9C-%EB%AA%A8%EC%9D%8C</link>
            <guid>https://velog.io/@today-is-first/Jest-%EC%A3%BC%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%A9%94%EC%84%9C%EB%93%9C-%EB%AA%A8%EC%9D%8C</guid>
            <pubDate>Sun, 13 Jul 2025 06:44:53 GMT</pubDate>
            <description><![CDATA[<h2 id="객체-테스트">객체 테스트</h2>
<ul>
<li>toStrictEqual</li>
<li>toMatchObject</li>
</ul>
<h2 id="함수-테스트">함수 테스트</h2>
<ul>
<li>toHaveBeenCalledTimes</li>
<li>toHaveBeenCalledWith</li>
<li>메서드를 테스트할 때는 <code>jest.fn()</code></li>
<li>객체 안의 메서드를 테스트할 때는 <code>jest.spyOn(obj, &quot;메서드 이름&quot;)</code></li>
</ul>
<h2 id="모킹">모킹</h2>
<ul>
<li>mockImplementation( ( ) ⇒ { } )<ul>
<li>내가 원하는 동작으로 변경할 수 있음</li>
</ul>
</li>
<li>mockImplementationOnce( ( ) ⇒ { } )<ul>
<li>한번만 내가 원하는 대로 실행되게 할 수 있음</li>
</ul>
</li>
<li>mockReturnValue()<ul>
<li>반환 값만 바꾸고 싶을 때</li>
</ul>
</li>
<li>mockReturnValueOnce()<ul>
<li>반환 값을 내가 원하는 걸로 한번만 반환되게 하는 법</li>
</ul>
</li>
</ul>
<h2 id="비동기-함수-테스트">비동기 함수 테스트</h2>
<ul>
<li><p>mockResolvedValue</p>
</li>
<li><p>mockRejectedValue</p>
</li>
<li><p>웬만하면 <code>expect.assertions(1)</code>로 실제 expect가 실행되는지 확인해주는 것이 좋음</p>
</li>
<li><p>주의해야 하는 사항</p>
<ul>
<li>비동기 테스트할 때 반드시 <code>return</code> 을 넣어줘야 함</li>
<li><code>return</code>이 없으면 테스트가 종료되기 전에 끝나서 정확한 테스트가 불가</li>
<li>대신 테스트 내부 메서드에 <code>async</code>를 붙이게 되면 <code>return</code> 없어도 됨</li>
</ul>
</li>
<li><p>비동기 테스트 하는 방법</p>
<ul>
<li><p>Promise, Async 둘 다 방법은 똑같음</p>
</li>
<li><p><code>resolves / rejects</code> 사용</p>
<pre><code class="language-jsx">test(&#39;Promise 성공 테스트1&#39;, () =&gt; {
expect(successPromise).resolves.toBe(&#39;성공&#39;);
});

test(&#39;Promise 실패 테스트1&#39;, () =&gt; {
expect(failPromise()).rejects.toThrow(&#39;실패&#39;);
});</code></pre>
</li>
<li><p><code>then / catch</code>사용</p>
<pre><code class="language-jsx">test(&#39;Promise 성공 테스트2&#39;, () =&gt; {
const spyOn = jest.fn(successPromise);
return spyOn().then(() =&gt; {
  expect(spyOn).toHaveBeenCalledWith(&#39;성공&#39;);
});
});

test(&#39;Promise 실패 테스트2&#39;, () =&gt; {
return failPromise().catch((e) =&gt; {
  expect(e).toEqual(new Error(&#39;실패&#39;));
});
});</code></pre>
</li>
<li><p><code>async / await</code> 사용</p>
<pre><code class="language-jsx">test(&#39;Promise 성공 테스트3&#39;, async () =&gt; {
const result = await successPromise();
expect(result).toBe(&#39;성공&#39;);
});

test(&#39;Promise 실패 테스트3&#39;, async () =&gt; {
try {
  await failPromise();
} catch (e: any) {
  expect(e).toBeInstanceOf(Error);
  expect(e.message).toBe(&#39;실패&#39;);
}
});</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="에러-테스트">에러 테스트</h2>
<ul>
<li><p>toThrow</p>
</li>
<li><p>에러 테스트를 바로 호출하면 에러가 나고 끝나서 콜백으로 감싸줘야 함</p>
<pre><code class="language-jsx">  test(&#39;에러 테스트&#39;, () =&gt; {
      expect( () =&gt; error()).toThrow(Error);
  })</code></pre>
</li>
</ul>
<h2 id="스파이-없애는-법">스파이 없애는 법</h2>
<ul>
<li>mockClear<ul>
<li>함수가 누구랑 실행되었는지, 몇 번 호출 되었는 지만 초기화</li>
<li>toHaveBeenCalledTimes + With 초기화</li>
</ul>
</li>
<li>mockReset<ul>
<li>mockClear + mockImplementation 초기화</li>
</ul>
</li>
<li>mockRestore<ul>
<li>아예 없애버림</li>
</ul>
</li>
<li>clearAllMocks</li>
<li>resetAllMocks</li>
</ul>
<h2 id="테스트-라이프-사이클">테스트 라이프 사이클</h2>
<ul>
<li>beforeEach, afterEach<ul>
<li>각 테스트 단위</li>
</ul>
</li>
<li>beforeAll, afterAll<ul>
<li>파일 단위</li>
</ul>
</li>
</ul>
<h2 id="시간--날짜-테스트">시간 / 날짜 테스트</h2>
<ul>
<li>useFakeTimers</li>
<li>useRealTimers</li>
<li>jest는 타이머 관련 기능 테스트가 잘되어있어서 공식 문서 보면 좋음</li>
</ul>
<h2 id="모킹-1">모킹</h2>
<ul>
<li>jest.mock<ul>
<li>모듈 자체를 모킹하는 기능</li>
<li>호이스트링 되어서 맨 위에서 호출된 거랑 똑같은 동작을 함</li>
<li>차라리 mock을 쓸거면 맨 위에서 선언하는 게 좋음</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[이번 플젝에 TDD를 도입해볼까?]]></title>
            <link>https://velog.io/@today-is-first/%EC%9D%B4%EB%B2%88-%ED%94%8C%EC%A0%9D%EC%97%90-TDD%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%B4%EB%B3%BC%EA%B9%8C</link>
            <guid>https://velog.io/@today-is-first/%EC%9D%B4%EB%B2%88-%ED%94%8C%EC%A0%9D%EC%97%90-TDD%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%B4%EB%B3%BC%EA%B9%8C</guid>
            <pubDate>Sun, 13 Jul 2025 06:44:18 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>내가 경험한 그동안의 프로젝트들은 <code>기획 - 구현 - 배포</code>의 과정을 거쳐 진행됐다. 대략 6주정도 진행되는 소규모 프로젝트에서 1~2주 정도 기획에 할애하고, 3주동안 개발하고, 남은 시간 발표를 준비했던 것 같다. 그러다보니 당장 눈 앞의 구현을 빠르게 쳐내느라 <code>어떤 코드가 좋은 코드인지, 어떤 게 좋은 설계인지</code> 고민하거나 공부하는 시간이 없었다. 실제로 좋은 코드는 무엇인지, 그게 왜 좋은 것인지 설명할 자신이 없다. 그래서 이번 프로젝트에서는 내 부족한 부분을 채우고자 결심했다.</p>
<h2 id="좋은-코드가-무엇일까">좋은 코드가 무엇일까</h2>
<p>좋은 코드란 무엇일까 공부하다가 많은 사람이 <strong>테스트하기 좋은 코드</strong>를 좋은 코드로 분류하는 경우를 많이 봤다. 예를 들면, Spring에서 의존성을 주입하는 방법이 대표적으로 <code>setter, 필드, 생성자 주입</code> 3가지가 있다. 이 중 <code>테스트에 용이</code>하고, 불변 객체를 보장하는 <strong>생성자 주입</strong>이 권장된다. </p>
<p>그동안은 <code>생성자 주입이 좋다</code>고 하니까 관성적으로 따르기만 했지, <strong>왜 좋은지 직접 체감하거나 설명할 수는 없었다</strong>. 실제로 단위 테스트나 통합 테스트를 제대로 작성해본 적도 없어서, 생성자 주입이 테스트에 어떤 이점을 주는지 경험하지 못했다. 그래서 이번 프로젝트에서는 단순히 구현만 빠르게 끝내는 게 아니라, <strong>테스트 가능한 구조를 고민하고</strong>, 왜 그런 설계가 더 나은지 스스로 납득하며 개발하는 것을 목표로 삼았다.</p>
<h2 id="tdd-≠-테스트-코드-짜기">TDD ≠ 테스트 코드 짜기</h2>
<p>TDD는 단순히 테스트 코드를 짜는 것만 얘기하는 것은 아니다. <code>TDD</code>는 xP 개발론의 일종으로 켄트백이 제시하였는데, 실제 돌아가는 코드를 작성하기 전에 해당 동작을 검증하는 테스트부터 작성하는 것이다. 사실상 <strong>Test First Development</strong>와 같다고 생각해도 된다. 그렇다면 구현 전에 테스트를 작성하는 <code>TDD</code>가 구현 이후 테스트를 작성하는 <code>Test Last Development</code>와 어떤 차별점이 있을까.</p>
<p>구현 전에 테스트를 작성하기 위해선 반드시 고민해야 하는 게 있다.</p>
<ul>
<li>내가 정확히 어떤 기능을 개발하려는가?</li>
<li>그 기능을 어떻게 테스트 할 것인가?</li>
</ul>
<p>위의 사항을 고민하다 보면 어쩔 수 없이 아래와 같은 효과가 생긴다.</p>
<ul>
<li>하나의 기능만을 수행하는 결합도가 낮은 설계를 하게 되고, 테스트하기 수월한 구조를 고민하게 된다.</li>
<li>실패하는 테스트 코드를 먼저 작성하고 이를 통과하는 코드를 후에 작성하게 되면서 내가 어떤 걸 개발해야 하는 지 명확한 상황에서 필요한 부분만 개발하게 된다.</li>
</ul>
<p>TDD는 단순히 테스트의 유/무 혹은 테스트 작성 시점의 차이가 아니라 기능을 구현하기 전에 내가 작성할 코드는 어떤 동작을 해야하고, 해당 기능이 잘 동작하는 지 검증하는 방법을 미리 고민하는 개발 접근법이다.</p>
<h2 id="맺음말">맺음말</h2>
<p>원래 TDD가 무엇인지 설명하는 글을 작성하려고 했는데, 이번 프로젝트에 TDD를 왜 적용하게 됐는지 서술하는 글이 됐다. 실제로 개발해보고 TDD를 어떻게 잘 적용하려고 했는지 작성하는 게 좋을 것 같아 다음 주 기술 블로그에 기술 하겠다.</p>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://www.youtube.com/watch?v=P9ItzDrPlso">TDD로 앞서가는 프론트엔드: 디자인, API 없이도 개발을 시작하는 방법 / if(kakaoAI)2024</a></p>
<p><a href="https://www.youtube.com/watch?v=AT-LzqMbrvo">TDD, 코드 품질에 신경쓰는 회사는 몇 프로나 될까?</a></p>
<p><a href="https://www.youtube.com/watch?v=L1dtkLeIz-M">[A5] 프론트엔드에서 TDD가 가능하다는 것을 보여드립니다.</a></p>
<p><a href="https://tech.kakaopay.com/post/implementing-tdd-in-practical-applications/">실전에서 TDD하기 | 카카오페이 기술 블로그</a></p>
<p><a href="https://tidyfirst.substack.com/p/tdd-isnt-design">TDD Isn&#39;t Design</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지는 어떻게 저장해야 할까?]]></title>
            <link>https://velog.io/@today-is-first/%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C-%EC%9E%91%EC%84%B1-%EC%A4%91</link>
            <guid>https://velog.io/@today-is-first/%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C-%EC%9E%91%EC%84%B1-%EC%A4%91</guid>
            <pubDate>Sun, 04 May 2025 12:26:13 GMT</pubDate>
            <description><![CDATA[<h2 id="s3">S3</h2>
<ul>
<li>Simple Storage Service</li>
<li>글로벌 서비스 / 단, 데이터는 리전에 저장</li>
<li>무제한 용량<ul>
<li>하나의 객체는 0byte에서 5TB의 용량</li>
</ul>
</li>
<li>객체 스토리지 서버<ul>
<li>파일 보관만 가능</li>
<li>어플리케이션 설치 불가능</li>
</ul>
</li>
<li>스토리지 서버의 종료<ul>
<li>객체</li>
<li>파일</li>
<li>블록 (EBS, EFS 등)</li>
</ul>
</li>
<li>버킷<ul>
<li>S3의 저장공간을 구분하는 단위</li>
<li>디렉토리 / 폴더와 같은 개념</li>
<li>버킷 이름은 전 세계에서 고유 값 : 리전에 관계 없이 중복된 이름이 존재할 수 없음</li>
</ul>
</li>
<li>객체<ul>
<li>Owner : 소유자</li>
<li>Key : 파일의 이름</li>
<li>Value : 파일의 데이터</li>
<li>Version Id : 파일의 버전 아이디</li>
<li>Metadata : 파일의 정보를 담은 데이터</li>
<li>0byte가 가능한 경우<ul>
<li>실제 파일의 데이터가 0이지만 메타 데이터만 담아서 쓰는 경우</li>
</ul>
</li>
</ul>
</li>
<li>S3의 내구성<ul>
<li>최소 3개의 가용 영역에 데이터를 분산 저장</li>
<li>99.99…%의 내구성<ul>
<li>파일 잃어버릴 확률이 로또 당첨 확률보다 122배 작음</li>
</ul>
</li>
</ul>
</li>
<li>보안 설정<ul>
<li>S3의 모든 버킷은 새로 생성 시 기본적으로 Private</li>
<li>보안 설정은 객체 단위와 버킷 단위로 구성<ul>
<li>Bucket Policy : 버킷 단위</li>
<li>ACL(Access Control List) : 객체 단위</li>
</ul>
</li>
<li>MFA를 활용해 객체 삭제 방지 가능</li>
<li>버저닝 가능</li>
<li>엑세스 로그 생성 및 전송 가능<ul>
<li>다른 버킷 혹은 다른 계정으로 전송 가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="버킷의-사용-목적별로-분리하자">버킷의 사용 목적별로 분리하자</h3>
<ul>
<li>정적 웹 호스팅 버킷<ul>
<li>만약 백엔드 서버 없이 정적 웹 페이지만 배포하고자 할 때 사용가능</li>
<li>모든 접근을 다 허용 설정</li>
<li>대신 Cloudfront 같은 CDN 서비스를 앞 단에 두고 해당 도메인만 S3에 직접 접근 가능하게 변경</li>
<li>Route 53를 사용해서 CNAME 레코드로 클라우드 프론트 엔드 포인트에 별칭을 붙여줌</li>
<li>Amazon WAF를 클라우드 프론트 앞 단에 사용해서 아이피를 컨트롤 해줌</li>
</ul>
</li>
<li>Presigned URL<ul>
<li>프라이빗 버킷이어도 URL이 있으면 접근 가능</li>
<li>Time To Live 시간이 정해져 있어서 보안적으로 이점이 있음</li>
<li>누군가 버킷 내 파일에 접근해야 할 때 버킷 전체를 퍼블릭으로 전환하지 않아도 되어서 보안적으로 이점이 있음</li>
<li>서버에서 ShortURL을 구현해서 사용자한테 넘겨주는 것도 고려해보면 좋음</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/today-is-first/post/0608841f-645a-41bd-b3bc-9765af6fb7fb/image.png" alt="">
<a href="https://www.youtube.com/watch?v=vgYfAndrpPU">출처 : 우아한테크</a></p>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://www.youtube.com/watch?v=vgYfAndrpPU">[우아한테크세미나] 사례별로 알아보는 안전한 S3 보안 가이드</a></p>
<p><a href="https://www.youtube.com/watch?v=LazOCTfdSeQ">쉽게 설명하는 AWS 기초 강좌 20: Amazon S3 기초</a></p>
]]></description>
        </item>
    </channel>
</rss>