<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yunbh_0401.log</title>
        <link>https://velog.io/</link>
        <description>프론드엔드 개발자</description>
        <lastBuildDate>Mon, 04 May 2026 12:08:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yunbh_0401.log</title>
            <url>https://velog.velcdn.com/images/yunbh_0401/profile/af46ae72-b27b-4ad3-b23a-7b974dce1721/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yunbh_0401.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yunbh_0401" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Build with AI Seoul 2026 with Google DeepMind 후기]]></title>
            <link>https://velog.io/@yunbh_0401/Build-with-AI-Seoul-2026-with-Google-DeepMind-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@yunbh_0401/Build-with-AI-Seoul-2026-with-Google-DeepMind-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 04 May 2026 12:08:03 GMT</pubDate>
            <description><![CDATA[<h1 id="첫번째-고민">첫번째 고민</h1>
<p>개발자가 기획을 하고, 디자이너가 코드를 작성하고, 이제는 비개발자도 서비스를 만들어내는 시대가 되었다.
이런 변화를 보면서 자연스럽게 한 가지 고민으로 이어졌다.</p>
<p>“그렇다면 개발자인 나는 어떤 역량을 더 키워야 할까?”
그리고 더 나아가,
“내가 가져가야 할 역량의 범위는 어디까지 확장되어야 하는 걸까?”</p>
<p>단순히 기술을 더 잘하는 것만으로 충분한 시대인지,
아니면 문제를 정의하고 제품을 만들어내는 전반적인 능력까지 가져가야 하는지,
기준이 명확하지 않은 상태에서 방향을 잡기가 쉽지 않았다.</p>
<p>한편으로는 또 다른 고민도 있었다.
AI와 관련된 정보들이 하루에도 수없이 쏟아지는 환경 속에서,
각자 다른 방식으로 성과를 내고 있는 사람들을 보며
<strong>“과연 나는 어떤 정보를 선택해서 따라가야 할까?”</strong>라는 질문을 계속하게 됐다.</p>
<p>모든 정보가 다 맞는 것처럼 보이지만, 동시에 누구에게나 정답이 될 수는 없는 상황.
결국 나에게 맞는 기준을 스스로 만들어야 한다는 생각이 들었고,
그 과정에서 들었던 몇 가지 발표들이 이 고민을 정리하는 데 큰 도움이 됐다.</p>
<p>첫 번째로 인상 깊었던 발표는
David McLaughlin의 세션이었다.</p>
<p>이 발표에서는 “좋은 소프트웨어란 무엇인가”라는 질문을 AI 시대의 관점에서 다시 짚어주고 있었다.
단순히 코드를 잘 작성하는 것을 넘어, 제품(Product), 디자인, 비즈니스, 그리고 데이터까지
여러 요소가 유기적으로 결합되어야 비로소 좋은 소프트웨어가 만들어진다는 이야기였다.</p>
<p>특히 인상 깊었던 부분은,
AI 시대가 되면서 개발 방식 자체가 바뀌고 있다는 점이었다.</p>
<p>과거에는 코드 작성 능력 자체가 핵심 경쟁력이었다면,
이제는 AI를 통해 코드 생산 비용이 크게 낮아지면서
“무엇을 만들 것인가”, “어떤 문제를 풀 것인가”, “어떤 맥락에서 동작하게 할 것인가”가
더 중요한 요소로 바뀌고 있었다.</p>
<p>즉, 개발자의 역할이 단순히 구현하는 사람에서
문제를 정의하고, 적절한 맥락과 데이터를 설계하는 사람으로 확장되고 있는 것이다.</p>
<p>또 하나 강조되었던 것은 데이터와 맥락(Context)의 중요성이었다.
아무리 뛰어난 모델을 사용하더라도 입력되는 데이터가 부정확하거나
문제 상황에 대한 맥락이 충분히 전달되지 않으면 결과 역시 한계가 있을 수밖에 없다.</p>
<p>결국 좋은 결과를 만들어내는 것은 모델 자체가 아니라,
그 모델을 어떻게 활용하고 어떤 환경 위에서 동작하게 하느냐에 달려 있다는 점이었다.</p>
<p>또 하나 인상 깊었던 발표는
업스테이지에서 오신 백수영님의 세션이었다.</p>
<p>“왜 조직은 작아지고 점점 더 빨라지고 있는가”라는 주제로,
AI 시대에 변화하고 있는 업무 방식에 대해 이야기해주셨다.</p>
<p>기존의 개발 방식은 기획, 디자인, 개발, QA처럼
각 역할이 명확히 나뉘어 있고 단계적으로 진행되는 구조였다.
하지만 이 방식은 AI 시대에 들어서면서 한계를 드러내고 있다고 지적했다.</p>
<p>기능을 만드는 속도는 빨라졌지만,
사람 간의 전달과 조율 과정에서 오히려 병목이 발생하고,
전체 속도는 기대만큼 빨라지지 않는다는 것이다.</p>
<p>이 문제를 해결하기 위해 업스테이지에서는
한 사람이 하나의 영역을 처음부터 끝까지 책임지는 방식으로
업무 구조를 변화시키고 있다고 한다.</p>
<p>단순히 개발만 하는 것이 아니라,
기획과 설계, 구현까지 하나의 흐름 안에서 직접 담당하는 구조다.</p>
<p>이 발표들을 통해 느낀 것은,
AI 시대의 개발자는 단순히 기술을 잘 다루는 것을 넘어
전체 흐름을 이해하고 설계할 수 있는 시야를 가져야 한다는 점이었다.</p>
<p>자연스럽게 한 가지 질문으로 이어졌다.
“그렇다면 이런 시야를 어떻게 키울 수 있을까?”</p>
<p>생각해보니 단순히 글을 읽거나 강의를 듣는 것만으로는 한계가 있을 것 같았다.
결국 직접 부딪혀보지 않으면 알 수 없는 영역이라는 생각이 들었다.</p>
<p>마침 나는 새로운 서비스를 하나 만들어볼 계획을 가지고 있었다.
기획부터 디자인, 개발까지 모든 과정을 직접 경험하며 서비스를 출시해보는 것.</p>
<p>이 과정을 통해 단순히 기능을 구현하는 것을 넘어,
문제를 정의하고, 방향을 설정하고, 실제 사용자에게 전달되는 전체 흐름을 고민해볼 수 있겠다는 
생각이 들었다.</p>
<p>또한 개발자로서 실제 고객을 대상으로 서비스를 운영해보는 경험은,
나만의 기준과 철학을 만들어가고
그동안 쌓아온 역량을 더 깊이 있게 키우고 증명할 수 있는 기회가 될 것 같다는 생각이 들었다.</p>
<p>결국 이런 경험들이 쌓이면서
단순히 기능을 구현하는 개발자가 아니라,
문제 해결과 서비스 전체를 고민할 수 있는 개발자로 성장해 나갈 수 있을 것이라는 기대가 생겼다.</p>
<h1 id="두번째-고민">두번째 고민</h1>
<p>현재 나는 퀘스티란 서비스를 맡아
신규 기능 개발과 유지보수를 함께 담당하고 있으며,
인력 부족으로 프론트엔드 영역을 거의 혼자 책임지고 있다.</p>
<p>우리 팀은 기능을 빠르게 개발해 고객 반응을 보고,
그에 맞춰 지속적으로 수정해 나가는 방식으로 서비스를 발전시키고 있다.</p>
<p>하지만 이 과정에서 기획과 정책이 자주 변경되면서
문서화가 제대로 이루어지지 않았고,
관련 히스토리 또한 여러 곳에 흩어져 있는 상태가 되었다.</p>
<p>그 결과, 코드에는 의도를 파악하기 어려운 부분과
불필요한 로직들이 점점 쌓여가고 있었다.</p>
<p>문제는 이런 상태에서 장애나 이슈가 발생했을 때다.
현재 구조에서는 원인을 파악하는 데부터 많은 시간이 소요되며,
빠르게 대응하기보다는 오히려 해결이 지연될 가능성이 크다.</p>
<p>더욱이 나 역시 모든 코드의 맥락을 완벽히 이해하고 있는 상태가 아니기 때문에,
이 상황은 단순한 불편함이 아니라 구조적인 리스크라고 느껴졌다.</p>
<p>그래서 이 문제를 어떻게 해결할 수 있을지에 대해
생각하는 시간이 점점 많아졌다.</p>
<p>처음에는 AI를 활용해 개인적으로 정리하거나 자동화하는 방법을 떠올리기도 했다.
하지만 곧 한 가지 생각에 도달했다.</p>
<p>나는 단순히 개발자가 아니라,
이 팀에 속한 한 명의 동료라는 점이었다.</p>
<p>이 문제를 혼자 해결한 뒤
“이렇게 만들어놨으니 이렇게 사용해주세요”라고 전달하는 방식보다는,</p>
<p>“이런 문제가 있어서 이런 방식으로 정리해보려고 하는데 어떻게 생각하시나요?”
라는 질문을 계속 던지며
팀원들과 함께 우리에게 맞는 업무 프로세스를 만들어가는 것이 더 중요하다고 느꼈다.</p>
<p>그렇다면 결국 남는 질문은 하나였다.
“그래서 어떻게 해야 할까?”</p>
<p>요즘은 다양한 회사에서 각자의 방식으로
효율적인 개발 방법론이나 프로세스를 공유하고 있다.</p>
<p>하지만 단순히 “이게 좋다”는 이유만으로 가져오는 것이 아니라,
그 방식이 우리 팀의 상황과 구조에서도 실제로 잘 동작할 수 있는지에 대해
한 번 더 고민해볼 필요가 있다고 생각했다.</p>
<p>이런 고민을 가지고 발표를 들으러 갔고,
그 자리에서 이경록 CEO의 세션을 들을 수 있었다.</p>
<p>요즘 가장 많이 언급되는 주제 중 하나인 하네스 엔지니어링(Harness Engineering)에 대한 내용이었는데,
이 개념을 단순한 트렌드가 아니라 실제 개발 방식의 변화로 설명해주셨던 점이 인상 깊었다.</p>
<p>발표 내용을 간단히 정리해보면,
기존에는 우리가 AI를 사용할 때 프롬프트를 잘 작성하는 것에 집중했다면,
그 이후에는 RAG나 메모리, 툴을 활용하는 컨텍스트 엔지니어링으로 발전해왔다.</p>
<p>하지만 이 방식의 한계는 분명했다.
아무리 컨텍스트를 잘 구성하더라도 구조 자체가 정적이기 때문에
결과가 지속적으로 개선되기 어렵고, 결국 사람의 개입에 의존할 수밖에 없다는 점이었다.</p>
<p>그래서 등장한 개념이 하네스 엔지니어링이다.</p>
<p>핵심은 단순히 입력을 잘 넣는 것이 아니라,
결과를 기반으로 시스템 자체를 계속 수정하고 발전시키는 구조를 만드는 것이다.</p>
<p>예를 들어 기존에는 결과가 잘못 나오면
프롬프트를 다시 수정하는 방식이었다면,</p>
<p>하네스에서는 어떤 결과가 잘못되었는지 판단하고
그 원인을 룰이나 검증 로직에서 찾아내고 
그 로직 자체를 수정해서 다음에는 같은 문제가 발생하지 않도록 만드는 구조를 만든다</p>
<p>즉, 결과를 고치는 것이 아니라
결과를 만들어내는 시스템을 개선하는 방식이라고 볼 수 있었다.</p>
<p>이 구조는 특히 여러 단계로 이루어진 작업이나
장기적으로 반복되는 작업에서 점점 더 큰 효과를 낼 수 있다고 설명해주셨다.</p>
<p>그리고 더 인상 깊었던 부분은,
이 하네스 엔지니어링을 실제 팀의 업무 프로세스에 적용해
어떻게 개선을 시도하고 있는지에 대한 이야기였다.</p>
<p>다만 여기서 강조했던 한 가지가 있다.</p>
<p>“정답은 없다”는 것.</p>
<p>회사마다 상황이 다르고, 팀마다 구조가 다르기 때문에
어떤 방식이 항상 옳다고 말할 수는 없으며,
지금도 계속 다양한 실험을 통해 더 나은 방향을 찾아가고 있는 단계라는 점이었다.</p>
<p>이 이야기를 들으면서 자연스럽게 이런 생각이 들었다.
“그렇다면 우리 팀에는 어떤 방식의 하네스가 맞을까?”</p>
<p>하지만 솔직히 말하면,
이 글을 쓰고 있는 지금도 그 답을 명확하게 내리지는 못한 상태다.</p>
<p>그럼에도 불구하고 한 가지는 확실해졌다.</p>
<p>이런 불확실한 상황일수록
혼자 고민하고 끝내는 것이 아니라 팀원들과 계속 공유하고,
여러 가지 시도를 통해 우리에게 맞는 방식을 찾아가는 과정 자체가 중요하다는 점이다.</p>
<p>완벽한 방법을 찾는 것보다,
작은 실험을 반복하면서 점점 더 나은 방향으로 개선해 나가는 것.</p>
<p>그 방향성을 다시 한 번 확신하게 된, 의미 있는 시간이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025년 하반기 회고]]></title>
            <link>https://velog.io/@yunbh_0401/2025%EB%85%84-%ED%9B%84%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@yunbh_0401/2025%EB%85%84-%ED%9B%84%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 25 Jan 2026 14:48:35 GMT</pubDate>
            <description><![CDATA[<h1 id="2025---2">2025 - 2</h1>
<p>어느덧 1월의 끝자락입니다. 정신없는 일상에 밀려 이제야 책상 앞에 앉았네요. 지난 2025년 상반기 회고록을 썼을 때만 해도 &#39;참 치열하다&#39;고 생각했는데, 하반기는 그보다 훨씬 더 가파른 가속도가 붙은 시간이었습니다. 마치 빠르게 감기를 한 것처럼 지나가 버린 시간들입니다.</p>
<p>그 속도에 휩쓸려 놓칠 뻔했던 기억들을 뒤늦게나마 하나씩 붙잡아보려 합니다. 2025년 하반기, 나에게는 어떤 일들이 있었고 나는 무엇을 배우며 성장했는지, 흩어진 생각들을 정리하며 다음 걸음을 위한 매듭을 지어봅니다.</p>
<h2 id="toss-frontend-assistant">Toss Frontend Assistant</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/439efbe5-293a-46ae-a802-f8e14e04c392/image.jpeg" alt="">
토스에서의 3개월은 개발자로서 제 방향성을 치열하게 고민하게 만든 시간이었습니다. 
실력 있는 동료들과 함께하며 때로는 한계에 부딪히는 &#39;벽&#39;을 느끼기도 했지만, 오히려 그 경험 덕분에 제가 나아가야 할 더 넓은 길을 발견할 수 있었습니다. 2025년을 통틀어 저에게 가장 강렬한 성장의 변곡점이 된 이 소중한 기록들을 아래 링크에 담아보았습니다.</p>
<p><a href="https://velog.io/@yunbh_0401/Toss-Frontend-Assistant-%ED%9A%8C%EA%B3%A0%EB%A1%9D">토스 어시 회고록</a></p>
<h2 id="마지막-큐시즘">마지막 큐시즘</h2>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://velog.velcdn.com/images/yunbh_0401/post/5d58a330-f926-4f2e-a2a9-33ca5902bdd7/image.png" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/yunbh_0401/post/c80c30ea-d128-4e7d-b810-d1645f57b2a7/image.jpeg" alt=""></td>
</tr>
<tr>
<td align="center"><img src="https://velog.velcdn.com/images/yunbh_0401/post/fe5a3e2b-9e3f-4540-85c1-6bfe5542bbe4/image.jpeg" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/yunbh_0401/post/04646545-09d0-40ac-a0ca-9b44902a10bd/image.jpeg" alt=""></td>
</tr>
</tbody></table>
<p>일을 병행하며 큐시즘 활동을 이어가는 것이 쉽지는 않았지만, 올해가 마지막이라는 생각에 지난번보다 더 치열하게 임했습니다. 특히 이전 프로젝트에서 느꼈던 아쉬움을 반복하지 않기 위해, <strong>&#39;프론트엔드 개발자로서 기술로 서비스에 기계적으로 기여하는 것을 넘어, 어떻게 하면 사용자와 개발자 경험을 실질적으로 개선할 수 있을까&#39;</strong>를 끊임없이 고민했던 시간이였습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/e92fae1b-bce7-4098-95ed-1adfae4556ba/image.png" alt=""></p>
<p><a href="https://minion.toss.im/9tDCNj4x">온서베이 링크</a></p>
<p>온서베이는 스크리닝 퀴즈와 관심사 매칭을 통해, 대학원생 등 전문 연구자가 필요로 하는 &#39;검증된 타겟&#39;의 고품질 응답을 10배 빠르게 수집하는 데이터 솔루션입니다.</p>
<p>&#39;큐시즘&#39;에서 시작된 이 프로젝트는 현재 &#39;앱인토스&#39;를 통해 정식 런칭되어 운영 중입니다. 출시 약 한 달이 지난 지금, 온서베이는 놀라운 성장세를 보이고 있습니다.</p>
<p>누적 가입자 수: 7,000명 돌파</p>
<p>일일 활성 사용자(DAU): 평균 1,000명 유지</p>
<p>프론트엔드 개발자로서 이 폭발적인 트래픽을 직접 마주하며, 사용자의 니즈가 기술로 실현되는 과정을 생생하게 경험하고 있습니다.</p>
<p>초기 서비스 운영 과정에서 겪었던 크고 작은 이슈들과, 이를 해결하며 서비스의 완성도를 높여갔던 트러블슈팅 과정들을 조만간 하나씩 정리해 보려 합니다. &#39;돌아가는 서비스&#39;를 넘어 &#39;잘 굴러가는 서비스&#39;로 만들기 위한 고민의 흔적들을 기대해 주세요.</p>
<h2 id="하이컨시-입사">하이컨시 입사</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/8467bf7d-16e7-4a38-8038-3d08141696c3/image.png" alt=""></p>
<p>토스 어시스턴트가 끝나고 다시 취준을 하게 되었습니다 학교와 큐시즘 동아리를 병행하면서 꾸준히 이력서를 넣으면 하루하루를 보냈는데
정말 이번 학기에는 다양한 곳에서 면접을 진행했었습니다 최종적으로는 하이컨시란 곳에 프론트엔드 개발자로 입사를 하게 되었습니다.</p>
<p>하이컨시에서 제가 소속된 팀은 주로 학생과 강사분들을 타겟으로 한 밀착형 서비스를 만들어가고 있습니다. 이전에는 미처 몰랐던 교육 도메인 지식과 에듀테크(Edu-Tech) 산업의 기술 생태계를 하나씩 배워가는 중입니다.
단순히 화면을 그리는 것을 넘어, 교육 현장의 사용자들에게 실질적인 가치를 전달하기 위해 환경이 어떻게 구성되어 있는지, 어떤 기술적 고민이 필요한지 깊이 있게 파고들며 도메인 지식을 확장하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/825deeaf-2a3a-4be9-8305-ea69b705ce1a/image.png" alt=""></p>
<p>최근에는 코엑스에서 열린 <strong>‘제23회 대한민국 교육박람회’</strong>에 저희 팀이 부스 운영팀으로 참가하게 되었습니다. 모니터 앞에서 코드를 작성하던 시간에서 벗어나, 우리가 만든 서비스를 오시는 분들에게 직접 설명하고 소통할 수 있는 뜻깊은 자리였습니다.</p>
<p>단순한 기능 설명을 넘어, 현장의 사용자분들이 주시는 피드백을 통해 <strong>‘우리 서비스가 앞으로 나아가야 할 방향’</strong>과 <strong>‘실제 사용자가 느끼는 개선점’</strong>이 무엇인지 생생하게 체감할 수 있었습니다. 개발자로서 내가 짠 코드가 실제 교육 현장에서 어떻게 쓰이고, 어떤 가치를 전달해야 하는지 다시금 고민하게 된 계기가 되었습니다.</p>
<p>또한, 수많은 교육 관련 기업의 부스를 돌아보며 다른 회사들의 기술력과 기획 의도를 한자리에서 살펴볼 수 있었습니다. &quot;어떤 기술 스택으로 이 문제를 풀었을까?&quot;, &quot;사용자 경험을 위해 어떤 장치를 두었을까?&quot;를 고민하며 관람하다 보니, 에듀테크 도메인에 대한 이해도가 한층 더 깊어지는 것을 느꼈습니다.</p>
<h2 id="새해-목표">새해 목표</h2>
<p>요즘은 본업과 사이드 프로젝트를 병행하며, 그 어느 때보다 밀도 높은 하루하루를 보내고 있습니다. 두 가지 일을 모두 챙기다 보니 몸은 바쁘지만, 성장을 위한 기분 좋은 압박감을 즐기는 중입니다.</p>
<p>물론 하이컨시의 구성원으로서, 그리고 온서베이의 운영자이자 개발자로서 스스로 부족함을 느낄 때도 많습니다. 서비스 운영부터 기술적인 완성도까지 채워야 할 빈틈이 여전히 보이기 때문입니다. 하지만 반대로 생각하면, 이 부족한 점들을 하나하나 해결해 나가는 과정 자체가 곧 저의 성장판이 될 것이라 믿습니다. 문제를 마주하고 답을 찾아가는 이 시간이 쌓인다면, 올해 연말에는 지금보다 훨씬 단단한 개발자가 되어 있지 않을까요?</p>
<p>작년에 그랬던 것처럼, 올해도 제가 마주한 기술적인 도전과 소중한 경험들을 이곳 블로그에 꾸준히 기록해 보려 합니다. &#39;고민의 흔적&#39;이 담긴 저의 기록들에 앞으로도 많은 관심과 응원 부탁드립니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우리 팀의 성장을 이끈 협업 문화와 프로세스 개선 후기]]></title>
            <link>https://velog.io/@yunbh_0401/%EC%9A%B0%EB%A6%AC-%ED%8C%80%EC%9D%98-%EC%84%B1%EC%9E%A5%EC%9D%84-%EC%9D%B4%EB%81%88-%EC%97%85%EB%AC%B4-%EB%AC%B8%ED%99%94%EC%99%80-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B0%9C%EC%84%A0-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@yunbh_0401/%EC%9A%B0%EB%A6%AC-%ED%8C%80%EC%9D%98-%EC%84%B1%EC%9E%A5%EC%9D%84-%EC%9D%B4%EB%81%88-%EC%97%85%EB%AC%B4-%EB%AC%B8%ED%99%94%EC%99%80-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B0%9C%EC%84%A0-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 26 Oct 2025 17:47:16 GMT</pubDate>
            <description><![CDATA[<h1 id="🚨-우리가-해결하고-싶었던-문제점들">🚨 우리가 해결하고 싶었던 문제점들</h1>
<p>업무 프로세스를 개선하기 전, 저희 팀은 몇 가지 고질적인 문제들로 인해 불필요한 시간과 에너지를 소모하고 있었습니다.</p>
<h2 id="1-모든-대화가-뒤섞여-히스토리-파악이-불가능했습니다">1. 모든 대화가 뒤섞여 히스토리 파악이 불가능했습니다.</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/74a4a903-5d12-4255-bc28-c0bbcda88e89/image.png" alt=""></p>
<ul>
<li><p>원인: 모든 소통을 Discord의 단일 채널에서, 쓰레드(Thread) 구분 없이 진행했습니다.</p>
</li>
<li><p>문제점:
  a. 특정 기능이나 작업에 대한 논의를 다시 찾아보려면 끝없이 스크롤을 올려야 했습니다.
  b. 과거에 어떤 논의를 통해 현재의 결론에 도달했는지 그 맥락을 파악하기가 거의 불가능했습니다.</p>
</li>
</ul>
<h2 id="2-정보가-여러-곳에-흩어져-있어-단일-진실-공급원ssot이-없었습니다">2. 정보가 여러 곳에 흩어져 있어 &#39;단일 진실 공급원(SSOT)&#39;이 없었습니다.</h2>
<ul>
<li><p>원인: 논의는 Discord, 이슈 기록은 Notion, 프로젝트 일정(WBS)은 Google Sheets 등 정보가 제각각의 툴에 흩어져 있었습니다.</p>
</li>
<li><p>문제점:
a. 하나의 작업을 이해하기 위해 여러 툴을 넘나들며 정보를 조각모음 해야 했습니다.
b. 이 작업의 최종 결정 사항이 뭐였죠?&quot;와 같이 상태를 파악하기 위한 불필요한 질문과 커뮤니케이션 비용이 계속해서 발생했습니다.</p>
</li>
</ul>
<h2 id="3-팀원들-간에-지금-무슨-일을-하는지-업무-공유가-제대로-되지-않았습니다">3. 팀원들 간에 지금 무슨 일을 하는지 업무 공유가 제대로 되지 않았습니다.</h2>
<ul>
<li><p>원인: 각자 어떤 작업을 진행하고 있는지 파악할 수 있는 중앙화된 공간이 없었고, 구두로 공유하거나 아예 공유되지 않는 경우가 많았습니다.</p>
</li>
<li><p>문제점:
a. 누가 어떤 일에 집중하고 있고, 어떤 부분에서 어려움을 겪는지 알기 어려워 적시에 도움을 주거나 받기 힘들었습니다.
b. 업무가 중복되거나 누락되는 경우가 발생하여 전체적인 프로젝트 진행 상황 파악이 어려웠습니다.</p>
</li>
</ul>
<hr>
<h1 id="💡-그래서-우리는-이렇게-해결했습니다">💡 그래서, 우리는 이렇게 해결했습니다.</h1>
<p>앞서 정의한 고질적인 문제들을 해결하기 위해, 저희는 단순히 새로운 툴을 도입하는 것을 넘어 &#39;소통의 규칙&#39;을 세우고, &#39;반복적인 과정을 자동화&#39;하며, &#39;정보를 한곳으로 모으는&#39; 세 가지 핵심 전략을 실행에 옮겼습니다.</p>
<h2 id="📌-step-1---소통의-질서를-잡다-모든-논의는-쓰레드에서">📌 Step 1 - 소통의 질서를 잡다: &#39;모든 논의는 쓰레드에서&#39;</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/a8d88569-83c6-4629-9630-e11d047f07b7/image.png" alt=""></p>
<p>가장 먼저 도입한 규칙은 간단하지만 강력했습니다. 바로 <strong>&#39;모든 작업 관련 논의는 반드시 Discord 쓰레드(Thread)를 만들어 시작한다&#39;</strong>는 것이었습니다.</p>
<p>이 간단한 규칙 하나가 뒤죽박죽 섞여 있던 대화의 물꼬를 터주었습니다. 더 이상 특정 주제를 찾기 위해 무한 스크롤을 할 필요가 없어졌고, 각 논의의 맥락이 명확하게 보존되기 시작했습니다. 이제 누구든 특정 기능이나 버그에 대한 쓰레드만 찾아 들어가면, 그 논의의 시작부터 끝까지 모든 흐름을 쉽게 파악할 수 있게 되었습니다.
그리고 대화가 주제별로 분리되어 더 이상 히스토리 파악을 위해 시간을 낭비하지 않게 되었습니다.</p>
<h2 id="📌-step-2---논의를-행동으로-discord-이슈-생성-봇-개발">📌 Step 2 - 논의를 행동으로: &#39;Discord 이슈 생성 봇&#39; 개발</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/70f8cfa7-32d9-494a-8945-4de54a9dcc41/image.png" alt=""></p>
<p>쓰레드로 소통이 정리되었지만, Discord에서 나눈 대화가 실제 &#39;일감&#39;으로 이어지기까지의 과정은 여전히 번거로웠습니다. 이 허들을 넘기 위해, 저희는 Discord 이슈 생성 봇을 직접 개발했습니다.</p>
<p>이제 팀원들은 Discord 채널에서 간단한 명령어(!이슈생성)만으로 Linear에는 새로운 이슈를, Notion 데이터베이스에는 기록을 동시에 생성할 수 있게 되었습니다. 논의의 흐름이 끊기지 않고 자연스럽게 공식적인 작업으로 등록되는 파이프라인이 만들어진 것입니다.</p>
<h2 id="📌-step-3---모든-역사를-한-곳에-linear를-단일-진실-공급원ssot으로">📌 Step 3 - 모든 역사를 한 곳에: &#39;Linear를 단일 진실 공급원(SSOT)으로&#39;</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/5e3ea75e-6b5d-4ed7-bb24-dc30dabc0ae3/image.png" alt=""></p>
<p>마지막으로, 저희는 흩어져 있던 모든 정보를 한곳으로 모으고 작업의 공식적인 역사를 기록할 최종 목적지를 정했습니다. 바로 Linear입니다. 저희는 다음과 같은 중요한 규칙을 세웠습니다.</p>
<p>&quot;모든 중요한 논의 과정과 최종 결정 사항은 반드시 해당 Linear 이슈의 코멘트로 요약하여 남긴다.&quot;</p>
<p>단순히 &#39;완료&#39;라고 적는 것이 아니라, 왜 이 방향으로 결정되었는지, 어떤 기술적 논의를 거쳤는지, 디자인이 왜 변경되었는지와 같은 핵심적인 히스토리를 상세히 기록했습니다. Discord 쓰레드에서는 자유롭게 의견을 나누되, 그 결과는 반드시 Linear에 응축하여 남기는 것입니다.</p>
<p>이 규칙을 통해 Linear는 명실상부한 우리 팀의 <strong>&#39;단일 진실 공급원(Single Source of Truth)&#39;</strong>이 되었습니다.</p>
<h3 id="잠깐-linear가-무엇인가요">잠깐, Linear가 무엇인가요?</h3>
<p>혹시 리니어(Linear)가 생소한 분들을 위해 간단히 짚고 넘어가겠습니다. 리니어는 특히 소프트웨어 개발팀을 위해 설계된, 현대적인 이슈 트래커 및 프로젝트 관리 도구입니다.</p>
<p>기존의 무겁고 복잡한 도구들과 달리, 매우 빠르고 직관적인 사용자 경험(UX)에 초점을 맞추어 개발자가 불필요한 과정 없이 오롯이 작업 자체에만 집중할 수 있도록 돕습니다. 저희 팀은 바로 이 Linear를 모든 작업의 진행 상황을 공유하고, 논의의 역사를 기록하는 <strong>&#39;중앙 허브&#39;</strong>로 사용하기로 결정했습니다.</p>
<p>🔗 Linear 공식 홈페이지: <a href="https://linear.app">https://linear.app</a></p>
<hr>
<h1 id="✨-우리팀-프로세스-한-번에-보기">✨ 우리팀 프로세스 한 번에 보기</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/1a910bdb-2ca3-4bbe-9469-a9197ff42a41/image.png" alt=""></p>
<h2 id="1단계-모든-것의-시작-discord">1단계: 모든 것의 시작, Discord</h2>
<p>쓰레드 생성: 특정 주제에 대한 논의를 위해 Discord 채널에 쓰레드(Thread)를 생성합니다.</p>
<p>이슈 생성: 논의 결과, 공식적인 작업으로 등록할 필요가 생기면 해당 쓰레드에 간단한 명령어(!이슈생성 [이슈 내용]-[담당자])를 입력합니다.</p>
<h2 id="2단계-이슈-생성-봇의-활약-자동화">2단계: 이슈 생성 봇의 활약 (자동화)</h2>
<p>Linear 이슈 자동 생성: Discord 봇이 명령어를 감지하고, 입력된 내용으로 Linear에 새로운 이슈를 즉시 생성합니다.</p>
<p>Notion DB 자동 추가: 동시에, 해당 이슈 내용을 Notion 데이터베이스에도 새로운 항목으로 자동 추가합니다.</p>
<h2 id="3단계-프로젝트-관리까지-한번에-자동화">3단계: 프로젝트 관리까지 한번에 (자동화)</h2>
<p>Google Apps Script 실행: Notion 데이터베이스에 새로운 항목이 추가된 것을 트리거(Trigger)로, 미리 작성해 둔 Google Apps Script가 자동으로 실행됩니다.</p>
<p>Google Sheets WBS 자동 생성: 실행된 스크립트는 Notion에 추가된 정보를 바탕으로 Google Sheets의 WBS(작업분류체계)에 새로운 행을 추가하고 내용을 자동으로 채워 넣습니다.</p>
<h2 id="4단계-히스토리-기록의-중심-linear">4단계: 히스토리 기록의 중심, Linear</h2>
<p>논의 및 결정사항 기록: 생성된 Linear 이슈 카드 내에서, 관련된 모든 논의 과정과 최종 결정 사항을 코멘트로 상세하게 기록합니다. 이로써 Linear는 모든 작업의 공식적인 <strong>&#39;단일 진실 공급원(Single Source of Truth)&#39;</strong>이 됩니다.</p>
<p>이처럼, Discord에서 시작된 간단한 명령어 하나가 Linear, Notion, Google Sheets까지 연쇄적으로 업데이트하여, 팀원들은 반복적인 기록 작업에서 해방되고 오직 중요한 업무에만 집중할 수 있게 되었습니다.</p>
<hr>
<h1 id="🚀-배운점">🚀 배운점</h1>
<p>지금까지 저희 팀이 어떻게 업무 프로세스를 개선했는지 그 과정과 결과를 쭉 공유해 드렸습니다. 하지만 이 글을 마무리하며 정말로 강조하고 싶은 것은, 멋진 자동화 시스템을 구축하는 기술 그 자체가 아니었습니다. 정작 가장 중요했던 것은 기술 외적인 부분에 있었습니다.</p>
<h2 id="1-무엇을-만들지보다-왜-만드는지가-훨씬-중요합니다">1. &#39;무엇을&#39; 만들지보다 &#39;왜&#39; 만드는지가 훨씬 중요합니다.</h2>
<p>처음 이 문제를 해결해야겠다고 마음먹었을 때, 저는 곧바로 &#39;어떤 봇을 만들까?&#39;, &#39;어떤 기술을 쓸까?&#39;를 고민했습니다. 하지만 잠시 멈춰 생각해보니 그건 순서가 아니었습니다. 가장 먼저 했어야 하는 일은 팀원 모두가 &#39;우리의 이 문제가 정말 해결이 필요한 심각한 문제&#39;라고 공감대를 형성하는 것이었습니다.</p>
<p>&quot;다들 특정 대화 찾느라 시간 낭비한 적 많으시죠?&quot;, &quot;지난번에 나왔던 그 아이디어, 기록이 안 돼서 놓친 거 너무 아깝지 않나요?&quot; 와 같이 문제점을 함께 공유하고 나서야, 비로소 제가 만들 솔루션이 &#39;개인의 욕심&#39;이 아닌 &#39;팀을 위한 도구&#39;가 될 수 있었습니다. 기술은 목적이 아니라, 공유된 문제를 해결하기 위한 수단일 뿐이라는 것을 깊게 깨달았습니다.</p>
<h2 id="2-설명이-아닌-설득의-핵심은-그들의-언어로-말하는-것입니다">2. 설명이 아닌 &#39;설득&#39;의 핵심은 &#39;그들의 언어&#39;로 말하는 것입니다.</h2>
<p>단순히 &quot;이 시스템을 도입하면 업무 효율이 올라갑니다&quot;라고 말하는 것은 아무런 힘이 없습니다. 팀원들의 마음을 움직인 것은 &#39;그래서 이걸 쓰면 내 소중한 주말 시간이 어떻게 절약되는데?&#39; 라는 질문에 대한 명확한 답변이었습니다.</p>
<ul>
<li><p>&#39;리니어 작성하고, 노션 작성하고, 구글 시트 작성하고... 어휴, 작업 시작하기도 전에 되게 뭔가를 많이 해야 하네.&#39; 이런 생각 이제 안 하셔도 돼요. 그냥 디스코드에서 명령어 한 번이면 이 모든 게 한 번에 끝납니다.&quot;</p>
</li>
<li><p>&quot;&#39;어? 그때 이 내용 논의했던 것 같은데 뭐였지? 분명 대화 기록이 있을 텐데...&#39; 하면서 한참 동안 디스코드 채널 스크롤 올리며 과거를 헤맬 필요 없어요. 이제 관련 리니어 티켓 하나만 열어보면 모든 논의의 역사가 정리되어 있을 거예요.&quot;</p>
</li>
</ul>
<p>이처럼 팀원 각자의 입장에서 겪는 불편함(Pain Point)을 정확히 짚어주고, 그것이 어떻게 해결될 수 있는지 구체적인 &#39;이점(Benefit)&#39;을 보여주었을 때 비로소 모두가 기꺼이 새로운 변화에 동참하기 시작했습니다.</p>
<h2 id="3-가장-강력한-무기는-함께-만들어간다는-동료-의식입니다">3. 가장 강력한 무기는 &#39;함께 만들어간다&#39;는 동료 의식입니다.</h2>
<p>제가 만약 모든 것을 혼자 완벽하게 만들어 &quot;자, 이제부터 이렇게 쓰세요!&quot;라고 일방적으로 발표했다면 분명 큰 저항에 부딪혔을 겁니다.</p>
<p>대신 저는 초기 아이디어 단계부터 동료들에게 의견을 구했습니다. &quot;명령어는 어떤 게 편할 것 같으세요?&quot;, &quot;리니어 티켓에 어떤 정보가 자동으로 들어가면 가장 유용할까요?&quot; 와 같이 계속해서 질문하며 그들을 과정에 참여시켰습니다. 덕분에 이 개선 프로젝트는 저 혼자만의 것이 아닌 <strong>&#39;우리 팀 모두의 프로젝트&#39;</strong>가 될 수 있었습니다.</p>
<p>결론적으로, 성공적인 프로세스 개선은 훌륭한 자동화 도구를 만드는 기술적인 능력을 넘어, <strong>팀원들과 함께 문제를 공감하고, 더 나은 방향을 제시하며, 모두가 기꺼이 참여하도록 이끄는 &#39;설득과 소통의 기술&#39;</strong>에 달려있다는 것을 배울 수 있었던 값진 경험이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Toss Frontend Assistant 회고록]]></title>
            <link>https://velog.io/@yunbh_0401/Toss-Frontend-Assistant-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@yunbh_0401/Toss-Frontend-Assistant-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Tue, 21 Oct 2025 17:11:24 GMT</pubDate>
            <description><![CDATA[<p>이번에 Toss에서 Frontend Assistant로 일할 수 있는 기회를 얻었습니다.
3개월 동안 DEUS 팀에 소속되어 일하며 정말 많은 것을 보고 배우는 소중한 시간을 보냈습니다.
이 경험을 통해 느끼고 배운 것들을 정리해두고자 이렇게 기록을 남기려 합니다.</p>
<h1 id="🚀-빠르게-성장하는-팀-속에서">🚀 빠르게 성장하는 팀 속에서</h1>
<p>제가 DEUS 팀에 합류했을 때는 약 8명 정도의 팀원이 있었습니다.
그중 두 분은 곧 휴직을 하시면서 실제로 함께 일하는 인원은 더 줄어든 상태였죠.</p>
<p>하지만 시간이 지나 퇴사할 즈음엔 팀원이 15명으로 늘어나면서,
불과 3개월 사이에 팀 규모가 거의 두 배로 성장했습니다.
그 과정에서 조직이 커질 때 자연스럽게 생기는 여러 문제들을 가까이서 볼 수 있었습니다.</p>
<hr>
<h2 id="🧭-온보딩-체계의-부재와-공통-언어의-필요성">🧭 온보딩 체계의 부재와 공통 언어의 필요성</h2>
<p>가장 먼저 느꼈던 문제는 온보딩 체계의 부재였습니다.
당시 팀은 빠르게 인원이 늘어나고 있었지만, 새로 합류한 구성원이 팀의 서비스나 코드 구조를 이해할 수 있는 명확한 가이드가 없었습니다.</p>
<p>문서를 찾으려 해도 최신화되지 않았거나, 사람마다 다른 설명을 들을 때도 있었죠.
결국 같은 개념을 두고도 팀원마다 다르게 표현하거나, 용어 해석이 달라 의사소통에 혼선이 생기곤 했습니다.</p>
<p>이 문제를 해결하기 위해 팀에서는 자연스럽게 논의가 시작되었습니다.
“우리가 쓰는 용어를 한 번 정리해보자”, “처음 들어오는 사람도 빠르게 이해할 수 있는 온보딩 문서를 만들자” 같은 의견들이 오갔고,
이를 바탕으로 실제로 온보딩 문서와 도메인 언어 정리 문서가 만들어졌습니다.</p>
<p>문서를 단순히 작성하는 데서 끝나지 않고,
팀 내에서 “이 용어는 이렇게 쓰자”, “이 프로세스는 이렇게 이해하면 된다”는 식으로 피드백을 주고받으며
점차 팀의 공통 언어와 기준이 자리 잡기 시작했습니다.</p>
<p>이 과정을 지켜보면서, 빠르게 성장하는 조직일수록
<strong>‘문서를 만드는 것’보다 ‘팀이 같은 언어로 말할 수 있게 만드는 것’</strong>이 더 중요하다는 걸 배웠습니다.
온보딩 문서는 단순한 정보의 나열이 아니라,
새로운 구성원이 “이 팀의 사고방식과 맥락”을 이해할 수 있게 해주는 첫 접점이라는 점도 느꼈습니다.</p>
<hr>
<h2 id="🧩-작업의-맥락을-잃지-않기-위한-노력">🧩 작업의 맥락을 잃지 않기 위한 노력</h2>
<p>또 하나 기억에 남는 부분은 개발 프로세스를 정립해 나가는 과정이었습니다.
팀 규모가 빠르게 커지면서 작업량이 늘어나자,
품질과 일관성도 중요했지만 무엇보다 <strong>“작업의 맥락을 잃지 않는 것”</strong>이 큰 과제가 되었습니다.</p>
<p>시간이 지나면 “이 작업은 왜 시작된 거지?”, “누가 어떤 논의 끝에 이렇게 결정한 거였지?” 같은 질문이
쉽게 생기곤 했거든요.</p>
<p>우리는 이런 문제를 해결하기 위해 <strong>Linear</strong>을 적극적으로 활용했습니다.
단순히 할 일을 나열하기보다 ‘왜 이 일을 하게 되었는지’,
그리고 <strong>‘어떤 대화 속에서 결정되었는지’</strong>를 남기려는 시도를 했습니다.</p>
<p>모든 이슈를 기록할 때 배경과 목적, 완료 조건을 함께 적고,
논의가 오갔던 맥락을 코멘트로 남기며
나중에 돌아보더라도 팀의 사고 흐름을 따라갈 수 있도록 정리했습니다.</p>
<p>이런 방식이 자리 잡으면서 팀 내 소통도 훨씬 투명해졌습니다.
서로가 어떤 생각으로 일하고 있는지 이해하기 쉬워졌고,
새로 합류한 사람도 과거의 의사결정 과정을 자연스럽게 학습할 수 있었습니다.</p>
<p>결국 우리는 단순히 일을 관리하는 체계를 만든 것이 아니라,
<strong>“생각의 흐름까지 공유되는 개발 문화”</strong>를 만들어가고 있었다고 느꼈습니다.</p>
<hr>
<h2 id="🛠-함께-성장하는-개발-문화-함께하는-deus-engineering">🛠 함께 성장하는 개발 문화, 함께하는 DEUS Engineering</h2>
<p>또 한 가지 인상 깊었던 점은 팀이 스스로 문제를 발견하고 개선해나가는 문화였습니다.
개발자들 사이에서 공유할 내용이나 맞춰봐야 할 부분이 생기면,
그때그때 채팅으로 이야기하고 끝내는 대신,
매주 월요일마다 한 시간씩 <strong>‘함께하는 DEUS Engineering’</strong>라는 시간을 따로 마련했습니다.</p>
<p>이 시간에는 기능 개발을 잠시 멈추고,
개발자들이 한자리에 모여 최근 겪은 불편함이나 개선 아이디어를 자유롭게 이야기했습니다.
예를 들어, 코드 리뷰 방식이나 코드 컨벤션, 배포 프로세스 같은
팀 전체에 영향을 주는 주제들을 다루었고, 논의가 끝나면 바로 실험적으로 적용해보기도 했습니다.</p>
<p>흥미로웠던 점은 이 시간이 ‘회의’라기보다는 함께 일하는 방식을 만들어가는 워크숍에 가까웠다는 것입니다.
누군가의 지시로 정해지는 게 아니라,
모두가 동등하게 의견을 내고 결정하는 과정을 통해
팀이 점점 하나의 방향성을 공유하게 되었죠.</p>
<hr>
<h1 id="🎯-deus에서-프론트엔드로-뛰어든-나의-여정">🎯 Deus에서 프론트엔드로 뛰어든 나의 여정</h1>
<p>토스 전사 디자이너가 사용하는 사내 디자인 툴 DEUS의 프론트엔드 개발과 사용자 경험 개선을 담당하며,
프로덕트의 안정성, 사용성, 일관성을 높이는 다양한 기능 개선, 버그 해결, 디자인 시스템 리팩토링을 진행했습니다.</p>
<h2 id="🧩-ddsdeus-design-system작업-경험과-배움">🧩 DDS(Deus Design System)작업 경험과 배움</h2>
<p>Deus 플랫폼의 디자인 시스템은 DDS 1.0 버전을 기반으로 운영되고 있었지만,
커스터마이징 요구 증가와 하드코딩된 스타일 덮어쓰기로 인해
컴포넌트 구조가 파편화되고 유지보수가 어려운 상태였습니다.</p>
<p>또한 명확한 관리 주체와 체계적인 유지 프로세스가 부재하여,
디자인 변경 시 각 팀이 개별적으로 수정하는 비효율이 발생하기도 했습니다.</p>
<p>이 문제를 해결하기 위해, 저는 다음과 같은 작업을 수행했습니다.
    •    기존 UI 구조를 전면 분석하고 14개 주요 DDS 컴포넌트를 개선 및 신규 설계
    •    DDS 2.0 버전과 커스텀 디자인 요소 통합을 통해 중복 스타일과 하드코딩 요소 제거
    •    Storybook 기반 미리보기 환경 구축으로 UI를 시각적으로 확인·테스트 가능하게 함
    •    각 컴포넌트별 Props, 예제 코드, 가이드라인 문서화로 누구나 쉽게 재사용할 수 있는 환경 조성</p>
<p>이 과정을 통해 디자인 자산의 파편화를 해소하고, 디자인 시스템 유지·관리 프로세스를 체계화하여 일관된 UI 개발 환경을 마련할 수 있었습니다.</p>
<p>그러나 되돌아보면, DDS 작업이 너무 어려웠고 다른 개발자들과 충분히 합의하지 않고 혼자 진행한 부분이 아쉬움으로 남았습니다. 이에 대해 커피챗에서 조언을 받았는데,</p>
<blockquote>
<p>DDS 작업은 단순히 컴포넌트를 만드는 게 아니라 팀 전체의 ‘언어’와 ‘약속’을 만드는 일이므로 원래 어려운 작업입니다.
합의가 부족해 문제가 생긴다는 점을 스스로 포착했다는 것 자체가 긍정적인 신호라고 볼 수 있습니다.</p>
</blockquote>
<p>하지만 이 경험 덕분에 팀과 협업하는 방식과 개선 포인트를 직접 체감하며 배울 수 있는 소중한 기회가 되었고,
작업의 난이도와 어려움은 자연스러운 것이며, 혼자 시도해보는 과정조차 성장의 중요한 발판이 될 수 있음을 깨달았습니다.</p>
<hr>
<h2 id="🐛-수많은-버그를-해결하면서-얻은-배움">🐛 수많은 버그를 해결하면서 얻은 배움</h2>
<p>Deus 플랫폼에서 프론트엔드 개발을 하면서, 도메인과 코드 구조가 낯설어 작은 UI 버그부터 구조적인 상태 관리 문제까지 다양한 버그를 겪었습니다.</p>
<p>처음에는 단순히 화면이 깨지거나 기능이 동작하지 않는 문제를 빠르게 해결하는 데만 집중했습니다.
예를 들어, 로딩 화면에서 다크모드가 제대로 적용되지 않는 문제를 처리할 때, 처음엔 “화면 색만 바꿔주면 되겠지”라고 생각하며 단기적으로 패치했습니다.
하지만 곧 다른 화면이나 상태 관리 로직에서 문제가 재발하면서, 근본적인 구조와 상호작용을 이해하지 못하면 비슷한 버그가 반복될 수밖에 없다는 것을 깨달았습니다.</p>
<p>또한, 상태 관리 구조나 컴포넌트 간 의존 관계를 제대로 파악하지 못한 채 고치다 보니,
한 버그를 고치는 과정에서 다른 화면에서 새로운 버그가 발생하기도 했습니다.
이런 경험은 “단기적으로 고치고 끝내는 방식”이 장기적으로는 문제를 키울 수 있다는 사실을 직접 체감하게 만들었습니다.</p>
<p>커피챗을 통해 조언을 받으면서, 문제를 바라보는 깊이를 세 단계로 나누어 생각하는 방법을 배우게 되었습니다.
    •    레벨 1️⃣: 결과 중심 – 눈에 보이는 변화만 확인하며 문제를 빠르게 처리하는 단계
    •    레벨 2️⃣: 구조 이해 – 코드의 역할과 상호작용을 이해하며 근본 원인을 찾는 단계
    •    레벨 3️⃣: 근본 원인 고민 – 코드 구조 자체가 최선인지, 아키텍처 개선 가능성을 검토하는 단계</p>
<p>실제로 ‘다크모드 분리 작업’이 좋은 예시였습니다. 처음에는 단순히 로딩 화면 색상을 바꾸는 요구였지만,
상태 관리 구조 자체의 문제를 발견하고 근본적으로 개선함으로써, 같은 버그가 반복되지 않도록 조치할 수 있었습니다.</p>
<p>이 경험을 통해, 버그를 단순히 고치는 데 그치지 않고 근본 원인을 이해하고 구조를 점검하는 사고가 얼마나 중요한지 배우게 되었고,
커피챗을 통해 얻은 조언 덕분에 앞으로는 문제를 좀 더 깊이 있게 분석하고 구조적으로 해결할 수 있는 시야를 갖추게 되었습니다.</p>
<hr>
<h2 id="💬-팀-내-협업과-소통에서-배운-것">💬 팀 내 협업과 소통에서 배운 것</h2>
<p>Deus 팀에서 일하면서 가장 많이 부딪힌 과제 중 하나는 디자이너 등 비개발자와의 소통이었습니다.
초반에는 기술적 용어와 개발자 중심의 사고 방식 때문에, 상대방이 이해하기 어렵다는 피드백을 종종 받았습니다.
“너무 개발자 중심으로만 얘기해서 이해하기 어렵고, 어떻게 해야 할지 모르겠다”는 말이 기억에 남습니다.</p>
<p>커피챗을 통해 조언을 받으면서 깨달은 중요한 포인트는, 좋은 개발자란 기술만 잘하는 사람이 아니라, 상대가 이해할 수 있게 전달하는 사람이라는 것입니다.</p>
<p>1️⃣ 맥락 중심으로 쉽게 설명하기</p>
<p>상대방을 무시하는 게 아니라, 상대가 지금 어떤 상황인지 ‘맥락’만 이해하면 된다는 생각으로 설명하는 방법입니다.
예를 들어, “DB가 꼬여서 API가 어쩌고…“라고 말하는 대신,
“지금 약간 교통사고가 난 상황이에요”처럼 쉽고 극단적인 비유를 사용하면 핵심 상황을 바로 전달할 수 있습니다.</p>
<p>2️⃣ 선택지를 제공하기</p>
<p>문제를 길게 설명하기보다, 상대방이 결정할 수 있는 선택지를 제시하는 것이 대화를 훨씬 생산적으로 만듭니다.
    •    나쁜 예: “지금 의존성 문제가 있고 리팩토링 범위가 어쩌고…” (그래서 어떻게 하길 원하는 건가?)
    •    좋은 예: “대략 이런 상황입니다. A 방법은 3일, B 방법은 하루지만 일부 기능을 포기해야 합니다. 어떻게 할까요?”</p>
<p>이처럼 선택지를 주면 상대방은 판단만 하면 되므로, 이야기가 빠르게 다음 단계로 진행됩니다.</p>
<p>추가로, 커피챗에서 배운 중요한 교훈 중 하나는 ‘상황에 맞는 소통 방식’을 구분하는 것입니다.
    •    개발자끼리 모여 일할 때는 전문 용어를 써도 소통이 가능하지만,
    •    디자이너, PO 등과 협업할 때는 누구나 이해할 수 있는 언어로 설명하는 능력이 훨씬 중요합니다.</p>
<p>결국, 현업에서 잘하는 개발자를 가르는 기준은 기술이 아니라 커뮤니케이션 능력이라는 사실을 몸소 느낄 수 있었습니다.
이 경험 덕분에, 기술적 지식과 더불어 누구에게나 명확하게 의사 전달하는 습관을 갖추는 것이 얼마나 중요한지 배웠습니다.</p>
<hr>
<h1 id="✨-돌아보며-앞으로의-다짐">✨ 돌아보며, 앞으로의 다짐</h1>
<p>커리어를 본격적으로 시작하기에 앞서, Deus 팀에서의 3개월은 제게 큰 충격과 배움을 준 시간이었습니다.
단순히 개발 실력뿐만 아니라, 팀이 성장하는 과정에서 무엇이 중요한지, 효과적인 소통 방식이 무엇인지 직접 경험하며 배울 수 있었습니다.</p>
<p>특히 커피챗에서 동료분께 “앞으로 어떤 개발자가 되고 싶냐”는 질문을 받았을 때, 선뜻 답하지 못했던 순간이 기억납니다.
그때 비로소, 제가 앞으로 어떤 방향으로 성장해야 할지, 어떤 개발자가 되고 싶은지 진지하게 고민해봐야겠다는 생각이 들었습니다.
앞으로 다시 취업 준비를 하면서는, 다른 분들의 이야기와 조언도 들어보고, 다양한 경험을 쌓아보면서 조금씩 저만의 방향을 구체화해가고 싶습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/697159fe-0dc5-4007-ae71-8831ebbbf506/image.jpeg" alt=""></p>
<p>마지막으로, 처음부터 끝까지 함께 해주신 Deus 팀원 분들께 깊은 감사의 마음을 전하고 싶습니다.
덕분에 많은 것을 보고 배우며 성장할 수 있었고, 이 경험은 앞으로 제 커리어에 큰 밑거름이 될 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI를 활용한 프론트엔드 자동화로 협업 효율 높이기]]></title>
            <link>https://velog.io/@yunbh_0401/AI%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%9E%90%EB%8F%99%ED%99%94%EB%A1%9C-%ED%98%91%EC%97%85-%ED%9A%A8%EC%9C%A8-%EB%86%92%EC%9D%B4%EA%B8%B0</link>
            <guid>https://velog.io/@yunbh_0401/AI%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%9E%90%EB%8F%99%ED%99%94%EB%A1%9C-%ED%98%91%EC%97%85-%ED%9A%A8%EC%9C%A8-%EB%86%92%EC%9D%B4%EA%B8%B0</guid>
            <pubDate>Sat, 04 Oct 2025 14:41:05 GMT</pubDate>
            <description><![CDATA[<h1 id="소개">소개</h1>
<p>이번 프로젝트에서 저는 프론트엔드 리드로 참여하며, 직행 플랫폼의 개선을 위한 여러 아이디어를 구현했습니다. 그 과정에서 단순히 기능을 구현하는 것에 그치지 않고, 팀 전체의 협업 효율과 개발 안정성을 높이는 자동화 시스템 구축에 주력했습니다. 오늘은 제가 프로젝트에서 어떻게 자동화를 적용했고, 어떤 효과를 얻었는지 공유하려 합니다.</p>
<hr>
<h1 id="🚨-반복-작업과-협업-비효율-문제">🚨 반복 작업과 협업 비효율 문제</h1>
<p>프로젝트 초기, 팀원들과 함께 기능 아이디어를 기획하며 느낀 점은 반복적인 작업이 많고, 협업 과정에서 발생하는 비효율이 적지 않다는 것이었습니다. 예를 들어:</p>
<ul>
<li>WBS 작성과 관리가 매번 수작업으로 이루어짐</li>
<li>디자인 변경 시 CSS 코드까지 수동으로 반영해야 함</li>
<li>배포 및 빌드 과정에서 실수로 오류가 발생할 가능성 존재</li>
<li>API 스펙 변경 시 타입 불일치로 개발 중 오류 발생</li>
</ul>
<p>이러한 문제들은 프로젝트가 커질수록 팀 전체의 생산성을 떨어뜨릴 수 있는 리스크였습니다.</p>
<hr>
<h1 id="✨-적용한-자동화-사례">✨ 적용한 자동화 사례</h1>
<h2 id="1-wbs-작성-자동화">1. WBS 작성 자동화</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/96f5fb3b-81e7-4165-b0ca-5a48dbfe1586/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/fd67cbe4-d6f4-4438-bbe0-33758c047968/image.png" alt=""></p>
<p>WBS는 프로젝트 관리에서 핵심이지만, 매번 업데이트하는 것은 번거롭습니다. 이를 해결하기 위해 노션과 Google Worksheet를 연결했습니다. 팀원들이 노션에 자신의 이슈를 입력하면, 자동으로 워크시트에 반영되고 최종적으로 WBS가 생성되도록 설계했습니다. 덕분에 WBS 작성 시간을 줄이고, 누락 없이 최신 상태를 유지할 수 있었습니다.</p>
<p>자세한 방법은 아래 링크에서 확인해주시면 됩니다.(제가 알려준 방법임)
<a href="https://velog.io/@cotn963/%EA%B5%AC%EA%B8%80-%EC%8A%A4%ED%94%84%EB%A0%88%EB%93%9C-%EC%8B%9C%ED%8A%B8-%EB%85%B8%EC%85%98-WBS-%EC%9E%90%EB%8F%99%ED%99%94-%EC%B2%98%EB%A6%AC">WBS 작성 자동화 방법</a></p>
<h2 id="2-디자인-시스템-자동화">2. 디자인 시스템 자동화</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/922309bc-6ea1-48c5-aaf0-c7c94fdc7c87/image.png" alt=""></p>
<p>디자인 토큰 변경 시 CSS 코드를 일일이 수정하는 것은 비효율적이었습니다. Figma Token Studio로 JSON 파일을 생성하고, AI 스크립트를 이용해 CSS 코드로 변환하도록 자동화했습니다. 덕분에 디자이너가 토큰을 바꾸면 코드까지 자동 반영되어, 디자인 변경 속도가 크게 향상되었습니다.</p>
<p>자세한 방법은 아래 링크에서 확인해주시면 됩니다.
<a href="https://velog.io/@iberis/Tokens-Studio-Tailwind-CSS-%ED%94%BC%EA%B7%B8%EB%A7%88-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9E%90%EB%8F%99-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0">디자인 시스템 자동화</a></p>
<h2 id="3-cicd-자동화">3. CI/CD 자동화</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/c8522c80-e236-4f9e-878a-08db982a583e/image.png" alt="">
<img src="https://velog.velcdn.com/images/yunbh_0401/post/0a41b030-c43f-4942-a089-ef4c1ded859b/image.png" alt=""></p>
<p>GitHub Actions를 활용해 메인 브랜치에 푸시될 때마다 서비스와 디자인 시스템 스토리북을 자동 배포하도록 설정했습니다.</p>
<h2 id="4-api-타입-통일화">4. API 타입 통일화</h2>
<p>백엔드에서 Swagger(OpenAPI) 문서를 제공했지만, 스펙 변경 시 프론트엔드 타입이 불일치하는 문제가 있었습니다. 이를 해결하기 위해 Swagger 문서를 읽어 프론트엔드 타입을 자동으로 생성하도록 했습니다. 덕분에 API 일관성이 유지되고, 개발 속도가 향상되었습니다.</p>
<h2 id="5-husky-기반-개발-프로세스-자동화">5. Husky 기반 개발 프로세스 자동화</h2>
<p>코드 컨벤션과 빌드 오류 문제를 방지하기 위해 Husky를 활용했습니다.
    •    커밋 전 린트 검사 → 코드 스타일 자동 확인
    •    푸시 전 빌드 검사 → 빌드 오류 방지
    •    체크아웃 시 패키지 정보 변경 감지 → 자동 설치</p>
<p>이 덕분에 팀원들은 환경 설정이나 빌드 문제를 걱정하지 않고 개발에 집중할 수 있었습니다.</p>
<hr>
<h1 id="⭐️-배운-점과-효과">⭐️ 배운 점과 효과</h1>
<p>이번 프로젝트를 통해 배운 가장 큰 점은 개별 기능 구현을 넘어, 팀 전체가 효율적으로 움직일 수 있는 환경을 만드는 것이 중요하다는 것입니다. 자동화를 통해 반복적인 수작업을 줄이고, 빌드·배포 과정의 안정성을 높이며, 디자인과 API의 일관성을 유지할 수 있었습니다.</p>
<p>또한, 팀원들이 보다 빠르고 안전하게 협업할 수 있도록 환경을 마련하는 과정에서 기술적 리더십과 시스템 설계 능력도 함께 향상되었습니다. 단순히 코드를 작성하는 것을 넘어, 프로젝트 전체를 바라보고 개선점을 찾아 자동화하는 경험은 제게 큰 성장의 기회가 되었습니다.</p>
<hr>
<h1 id="📌-앞으로-도전해보고-싶은-자동화">📌 앞으로 도전해보고 싶은 자동화</h1>
<p>이번 프로젝트에서 다양한 자동화 경험을 쌓으면서, 아직 더 시도해보고 싶은 영역이 있습니다. 앞으로는 단순 반복 작업을 줄이는 것을 넘어, 실시간 모니터링과 협업 효율을 극대화하는 자동화를 구현하고자 합니다. 구체적으로는 다음과 같습니다.</p>
<h2 id="1-sentry-기반-에러-로깅-및-알림-자동화">1. Sentry 기반 에러 로깅 및 알림 자동화</h2>
<p>서비스 운영 중 발생하는 에러를 실시간으로 감지하고, 팀원에게 즉시 알림을 전달하는 시스템을 구축하고 싶습니다.
Sentry를 활용하여 프론트엔드와 백엔드 오류를 통합 모니터링
AI 기반 분류로 에러 우선순위 결정
Discord 등으로 자동 알림 전송 → 빠른 대응 가능
이를 통해 서비스 안정성을 더욱 높이고, 문제가 발생했을 때 팀이 즉시 대응할 수 있는 환경을 만들고자 합니다.</p>
<h2 id="2-discord-기반-협업-알림-및-이슈-생성">2. Discord 기반 협업 알림 및 이슈 생성</h2>
<p>팀원 간 커뮤니케이션을 자동화하고, 작업 상태를 실시간으로 공유하는 시스템을 구현해보고 싶습니다.
Discord 채팅에서 특정 명령어 입력 시 자동으로 Notion에 이슈 생성
생성된 이슈가 팀원에게 자동 알림 → 상태 공유와 투명성 향상
반복적인 회의나 보고 과정 없이, 실시간으로 프로젝트 진행 상황 파악 가능
이 과정을 통해 협업 과정의 효율을 높이고, 정보 누락이나 중복 작업을 최소화할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/7ba1b2c2-772a-4122-ab6a-0610444eba49/image.png" alt=""></p>
<p>사실 이미 이슈 생성 봇은 이번 프로젝트에서 도입해보려고 했습니다. AI에게 동작 기능 코드를 맡겨 Discord와 Notion 연동 이슈 생성 봇을 만드려고 했으나, 기능이 점점 복잡해지고 코드 흐름이 뒤얽혀서 결국 제가 직접 손을 대기 어려울 정도가 되었습니다. 시간이 제한된 상황에서 중간에 포기할 수밖에 없었지만, 이 경험 자체가 큰 배움이 되었습니다.</p>
<h2 id="3-figma-mcp-연동을-통한-디자인-자동화">3. Figma MCP 연동을 통한 디자인 자동화</h2>
<p>디자이너와 개발자 간 작업 흐름을 더욱 긴밀하게 만들기 위해, Figma MCP를 활용한 디자인 자동화도 도전해보고 싶습니다.
Figma에서 디자인 변경 시, 개발 코드와 자동 동기화
AI 스크립트를 통해 컴포넌트 상태별 CSS 및 Tailwind 클래스 자동 생성
디자인과 코드의 일관성 유지 → 디자이너와 개발자의 피드백 루프 단축
이로써 디자인 변경이 발생해도 빠르고 안전하게 서비스에 반영될 수 있도록 만들고자 합니다.</p>
<hr>
<p>이번 프로젝트에서 경험한 자동화와 더불어, 이러한 실시간 모니터링, 협업, 디자인 동기화 자동화를 시도해본다면, 팀 전체의 생산성과 안정성을 한층 더 높일 수 있을 것이라 기대하고 있습니다. 앞으로도 저는 단순한 기능 구현을 넘어, 팀과 서비스 전체를 바라보며 효율과 안정성을 개선하는 개발에 도전해나갈 계획입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 큐시즘 X 직행 기업 프로젝트 회고]]></title>
            <link>https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-X-%EC%A7%81%ED%96%89-%EA%B8%B0%EC%97%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-X-%EC%A7%81%ED%96%89-%EA%B8%B0%EC%97%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 03 Oct 2025 14:12:55 GMT</pubDate>
            <description><![CDATA[<h1 id="💙-큐시즘이란">💙 큐시즘이란?</h1>
<p>💡한국대학생 IT 경영학회로, 매 기수 70-80명의 대학생들이 모여 각각 기획, 디자인, 개발(프론트/백엔드) 파트를 맡아 IT 프로덕트를 만드는 학회</p>
<p>기업과 연계하여 진행되는 기업 프로젝트, 자체적으로 서비스를 만드는 밋업 프로젝트 등 실무형 커리큘럼과 각종 네트워킹까지 다양한 활동을 할 수 있는 학회입니다.</p>
<p>이 중 이번에는 직행과 함께 유저 참여도를 높이기 위한 개선 과제를 진행했습니다.</p>
<hr>
<h1 id="🤔-직행이-가지고-있던-고민">🤔 직행이 가지고 있던 고민</h1>
<p>기존 온보딩 프로세스는 진입장벽이 높고 사용자가 얻을 수 있는 가치를 즉시 보여주지 못해, 낮은 전환율과 높은 이탈률을 보였습니다. 특히, 공고 탐색이 주목적인 신규 유저 입장에서는 “가입부터 요구하는 서비스”라는 인식이 쉽게 형성될 수 있었습니다.</p>
<p>따라서 단순히 가입을 늘리는 것이 아니라, 가입 자체가 곧바로 사용자에게 의미 있는 경험으로 이어지도록 설계하는 것이 중요했습니다. 신규 유저가 처음 접속했을 때 빠르게 “이 플랫폼은 나에게 꼭 필요한 곳”이라는 확신을 가질 수 있어야 했고, 이를 위해 온보딩 단계에서 즉각적으로 체감 가능한 맞춤형 추천과 개인화된 탐색 경험을 제공할 필요가 있었습니다.</p>
<hr>
<h1 id="🧩-직행의-문제를-해결하기-위해-우리가-제안한-3가지-기능">🧩 직행의 문제를 해결하기 위해 우리가 제안한 3가지 기능</h1>
<p>채용 플랫폼 <strong>직행</strong>은 빠르게 성장하면서 여러 문제를 겪고 있었습니다.</p>
<ul>
<li>공고는 많아졌지만, 나에게 맞는 것을 찾기 어려움</li>
<li>회원가입을 강제하지 않다 보니 개인화 추천이 불가능</li>
<li>취업 이후에는 다시 들어올 이유가 없어 낮은 리텐션 발생</li>
</ul>
<p>우리는 이 세 가지 문제를 풀기 위해, <strong>온보딩 → 관심 기업 뉴스레터 → 오늘의 지원</strong> 세 가지 기능을 제안했습니다.</p>
<hr>
<h2 id="1-온보딩-맞춤형-추천을-가능하게-만들다">1. 온보딩: 맞춤형 추천을 가능하게 만들다</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/e6a55e96-c8fd-4894-b9dc-2de43e8ac2b1/image.png" alt=""></p>
<p>기존 온보딩 프로세스는 복잡하고, 신규 유저가 얻을 수 있는 가치를 바로 보여주지 못했습니다. 이 때문에 <strong>전환율은 낮고 이탈률은 높았죠.</strong></p>
<p>우리는 이 과정을 단순화했습니다.</p>
<ul>
<li>처음엔 신입/경력, 경력 기간, 희망 직무·지역 등 <strong>핵심 정보 3~4개만 입력</strong></li>
<li>이후 서비스 사용 과정에서 자연스럽게 <strong>추가 정보를 보완하는 점진적 온보딩</strong></li>
</ul>
<p>이렇게 설계한 덕분에, <strong>사용자는 빠르게 맞춤형 공고를 추천받으며 “가입의 즉각적인 가치”를 경험</strong>할 수 있습니다.</p>
<hr>
<h2 id="2-관심-기업-뉴스레터-리텐션의-문제를-풀다">2. 관심 기업 뉴스레터: 리텐션의 문제를 풀다</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/0cd96392-890b-4c65-9b3f-422f641af2c4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/c1b82b2d-0d05-4685-8d70-5d78fe4da799/image.png" alt=""></p>
<p>구직자들은 기업 분석을 위해 하루 2시간 이상을 소비하는 경우가 많습니다. 하지만 직행은 공고 중심이라, 기업 소식이나 업계 트렌드를 제공하기엔 한계가 있었습니다.</p>
<p>우리는 <strong>‘관심 기업 뉴스레터’ 기능</strong>을 제안했습니다.</p>
<ul>
<li>기업 상세 페이지에서 [소식 받기] 버튼 클릭</li>
<li>관심 기업의 뉴스·이슈를 주 1회 메일로 제공</li>
<li>유사 기업 동향까지 함께 큐레이션</li>
</ul>
<p>이를 통해 구직자는 <strong>기업 탐색 시간을 줄이고</strong>, 취업 이후에도 업계 동향을 확인하기 위해 직행에 <strong>계속 방문할 이유</strong>를 갖게 됩니다.</p>
<hr>
<h2 id="3-오늘의-지원-실행력을-높이다">3. 오늘의 지원: 실행력을 높이다</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/c366cb6d-c20c-4b56-ae63-8a100f1ccb8f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/bc308a3e-f526-44a1-bc4d-2af01d1f12f7/image.png" alt=""></p>
<p>많은 구직자들이 수십 개의 공고를 둘러보다가 결국 <strong>지원으로 이어지지 않는 문제</strong>를 겪습니다.</p>
<p>우리는 이 문제를 해결하기 위해 <strong>‘오늘의 지원’ 기능</strong>을 설계했습니다.</p>
<ul>
<li>하루 목표 지원 개수를 세우고, 맞춤형 공고를 리스트업</li>
<li>지원 여부를 체크하고, 결과를 기록</li>
<li>합불 데이터를 기반으로 주간 리포트 제공 (지원 패턴, 강점·약점 분석)</li>
</ul>
<p>결과적으로 사용자는 <strong>단순 탐색을 넘어 실제 지원으로 이어지는 루틴을 형성</strong>할 수 있고, 플랫폼은 자연스럽게 <strong>전환율을 끌어올릴 수 있습니다.</strong></p>
<hr>
<h2 id="마무리">마무리</h2>
<p>직행이 가진 문제는 사실 대부분의 채용 플랫폼이 마주하는 공통적인 과제입니다.</p>
<ul>
<li><strong>온보딩으로 개인화 추천 문제 해결</strong></li>
<li><strong>관심 기업 뉴스레터로 리텐션 강화</strong></li>
<li><strong>오늘의 지원으로 전환율 개선</strong></li>
</ul>
<p>우리는 이 세 가지 기능을 통해, 직행이 단순히 채용공고를 모아두는 플랫폼이 아니라 <strong>구직자의 여정을 끝까지 함께하는 서비스</strong>로 발전할 수 있도록 도왔습니다.</p>
<p>이번 직행 프로젝트는 단순히 채용 플랫폼의 기능 개선을 넘어, 구직자 여정을 어떻게 더 쉽고 의미 있게 만들 수 있을까라는 질문을 함께 고민한 과정이었습니다.
이 과정에서 우리는 숫자 중심의 개선(전환율, 리텐션)과 사용자 경험 중심의 개선이 결코 분리될 수 없다는 중요한 사실을 배웠습니다.</p>
<h1 id="to일행">To.일행</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/edeb3ac3-f6d6-4e44-a88e-9d594f3c1a36/image.png" alt=""><img src="https://velog.velcdn.com/images/yunbh_0401/post/a6cbecce-6cc1-4cd1-bf88-6412df255b5f/image.jpeg" alt=""></p>
<p>한 달이라는 길지 않은 시간 동안 정말 모두 고생 많았어. 덕분에 좋은 결과까지 얻어낼 수 있어서 너무 기쁘고, 함께했던 시간이 오래 기억에 남을 것 같아.</p>
<p>지금도 충분히 잘 지내고 있지만, 앞으로도 이번 프로젝트를 떠올리면서 서로 아껴주고 존중하면서, 지금처럼 서로의 든든한 동료이자 개그맨이 되어주자.</p>
<p>다시 한 번, 다들 고생 많았어!</p>
<h2 id="예고">예고</h2>
<p>다음 글에서는 기업 프로젝트를 진행하며, 우리가 어떻게 여러 자동화 시스템을 구축해 프로젝트를 더 효율적으로 이끌어갈 수 있었는지에 대해 공유하려 합니다.(많관부)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025년 상반기 회고]]></title>
            <link>https://velog.io/@yunbh_0401/2025%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@yunbh_0401/2025%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 13 Jul 2025 14:06:07 GMT</pubDate>
            <description><![CDATA[<h1 id="2025---1">2025 - 1</h1>
<p>2024년 회고를 쓴 지 벌써 반년이 지났지만, 당시에는 어떤 일이 있었는지 떠올리는 것조차 쉽지 않았다.
올해는 그때보다 훨씬 더 많은 일들을 경험했고, 성장할 수 있는 기회도 많았기에 이번에는 상반기와 하반기로 나누어 회고를 남겨보려 한다.</p>
<h2 id="⚡️-엘리스-스파크-캠프">⚡️ 엘리스 스파크 캠프</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/5d4a6410-9dc3-46f9-8f19-318835629ee8/image.jpeg" alt=""></p>
<p>2월에는 엘리스에서 진행한 스파크 캠프에 참가했다.
전혀 모르는 사람들과 한 팀을 이뤄, 2주간 엘리스에서 제공하는 AI 관련 API를 활용해 서비스를 기획하고 개발하는 해커톤 형태의 행사였다.</p>
<p>우리 팀은 구성부터 꽤나 이상적이었다. 디자이너 1명, 프론트엔드 2명, 백엔드 2명으로 역할이 잘 분배되어 있었고, 엘리스 관계자들도 팀 구성이 좋다고 이야기할 정도였다.</p>
<p>하지만 아이디어 기획부터 쉽지 않았다. 개발 기간이 짧았기 때문에 MVP 모델을 최대한 작게 가져가야 했고, 이로 인해 백엔드 개발자 분들이 할 수 있는 작업이 제한적이었다.
몇 가지 아이디어를 더 해보자는 제안도 있었지만, 결국 시간이 부족해 계획대로 진행하지는 못했다.</p>
<p>우리 프론트엔드 팀 역시 둘 다 처음으로 React Native를 사용해 개발을 진행하다 보니, 아주 간단한 컴포넌트를 만드는 데도 시간이 많이 걸렸다. 코드도 꽤 복잡해졌던 걸로 기억한다.</p>
<p>아쉬운 점도 있었지만, 처음 만난 사람들과 함께 고민하고 협업해 하나의 서비스를 완성했다는 경험은 정말 값졌다.
무엇보다 이번 경험을 통해 내 커뮤니케이션 방식에 대해 돌아볼 수 있는 기회도 있었다.
나는 평소에 답답함을 느끼면 말이 빨라지고 목소리 톤도 높아지는 편인데,
같은 프론트엔드 팀원으로부터 <strong>“조금 정신없고, 죄책감이 들었다”</strong>는 피드백을 받고 나서부터는 가능한 한 차분하게 대화하려고 노력하고 있다.</p>
<h2 id="🎓-25학번으로-다시-대학-생활-시작">🎓 25학번으로 다시 대학 생활 시작</h2>
<p>2024년에 취업 준비를 하면서 아쉬웠던 점 중 하나는, 지원하고 싶은 대기업이나 중견기업의 상당수가 4년제 대학 졸업자를 요구했다는 점이었다.
그때 이 조건이 벽처럼 느껴졌고, 결국 올해 전공심화 과정을 통해 다시 대학에 입학하게 되었다.</p>
<p>처음에는 단순히 컴퓨터공학과로 진학할 생각이었지만, 마침 이 학교에서 인공지능소프트웨어학과 전공심화 과정도 신설되었다. 평소 AI 개발에 대한 호기심이 많았던 나는 고민 끝에 인공지능소프트웨어학과를 선택하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/9f0e5172-2c41-41c5-afbb-3c531e415000/image.jpeg" alt=""></p>
<p>수업은 야간에 진행되었고, 자연스럽게 오후 7시까지의 시간이 비게 되었다. 이 시간을 어떻게 썼냐면…
오전부터 오후까지는 알바를 하고, 수업까지 2~3시간이 남는 시간엔 도서관에 가서 공부하거나 이력서를 정리하고, 큐시즘 프로젝트도 진행하는 등 꽤 알차게 보냈다.</p>
<p>이 학교는 예전 학교에 비해 시설이 훨씬 좋고 쾌적해서 도서관도 자주 이용하게 되었고, 덕분에 집중도 잘 됐던 것 같다.</p>
<p>1학기 수업은 대부분 AI 기획과 관련된 내용이었다. 그래서 나처럼 &#39;뼛속까지 개발자&#39;인 사람에게는 꽤 낯설고 어려운 수업들이었다. 열심히 참여하긴 했지만, 아쉽게도 성적은 썩 만족스럽지 못했다.</p>
<p>2학기에는 챗봇도 만들고, 졸업작품도 진행해야 하다 보니 이번보다 훨씬 더 많은 노력이 필요할 것 같다.
그래서 미리 대비하려고, AI 개발 관련 강의 하나를 구매해서 듣기 시작했다. 이번엔 단순히 수업을 따라가는 데 그치지 않고, 실제로 내가 구현할 수 있는 수준까지 올려보는 게 목표다.</p>
<h2 id="💙-동아리-병행">💙 동아리 병행</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/11c32756-0ac4-4902-8202-07ee67d7e575/image.jpeg" alt="">
2024년에 취업 준비를 하면서 가장 아쉬웠던 점 중 하나는, 학생 때 개발 동아리 활동을 많이 해보지 못했다는 것이었다.
그래서 학교에 다시 다니게 된다면, 꼭 동아리 활동을 하겠다는 목표를 세웠고, 그 결과 선택한 곳이 바로 <strong>큐시즘(KUSITMS)</strong>이었다.
큐시즘에서 어떤 활동을 했는지에 대해서는 이전 글에서 자세히 다뤄두었으니, 궁금하다면 아래 링크를 통해 확인해보면 좋을 것 같다. 👇</p>
<h3 id="큐시즘-활동">큐시즘 활동</h3>
<ol>
<li><a href="https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-X-%EC%84%9C%EC%9A%B8-%EC%9A%B0%EC%9C%A0-%EA%B8%B0%EC%97%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0">큐시즘 기업 프로젝트</a></li>
<li><a href="https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-%EB%B0%8B%EC%97%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0">큐시즘 밋업 프로젝트</a></li>
<li><a href="https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-%EA%B3%B5%EC%8B%9D-%ED%99%88%ED%8E%98%EC%9D%B4%EC%A7%80-Next.js%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%BA%90%EC%8B%B1%EC%9D%84-%EA%B3%81%EB%93%A4%EC%9D%B8">큐시즘 공식 홈페이지 마이그레션</a></li>
</ol>
  <img src="https://velog.velcdn.com/images/yunbh_0401/post/b58ddd6a-6cd5-421a-a47c-cec057416feb/image.jpeg" width="100%" />
  <img src="https://velog.velcdn.com/images/yunbh_0401/post/3fca7229-1770-4683-9cfe-ccdab31f801c/image.jpeg" width="100%" />
  <img src="https://velog.velcdn.com/images/yunbh_0401/post/821fff10-5853-4b5c-880f-92d6288305b3/image.jpeg" width="100%" />


<p>큐시즘에서는 면접 스터디를 직접 만들고 운영해보기도 했고,
큐시즘 공식 홈페이지 개선 프로젝트에도 참여해 의미 있는 성과를 냈다.
동아리 내 프론트엔드 친구들과도 많은 이야기를 나누며 서로의 고민과 경험을 공유했고, 그 덕분에 실력뿐만 아니라 사람 간의 연결도 깊어질 수 있었던 시간이었다.</p>
<p>만약 내가 과거로 돌아간다고 해도, 다시 큐시즘을 선택할 거다.정말 후회 없이, 값진 경험을 많이 할 수 있었던 동아리였다.</p>
<h2 id="🌊-취업이란-서핑과-같다-좋은-파도를-기다리자">🌊 취업이란 서핑과 같다. 좋은 파도를 기다리자</h2>
<p>이번 상반기에는 작년보다 운이 좀 더 좋았다. 지금까지 총 5곳에서 면접을 봤는데 한 곳에서만 최종합격을 하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/e0f64c0a-cefc-44c8-88db-99cedbf99a35/image.png" alt=""></p>
<p>지원했던 5곳 중 정말 유독 아쉬웠던 곳이 하나 있었다.
이곳은 서류 - 과제 테스트 - 기술 면접 - 컬처핏 면접 - 대표 면접까지 총 5단계의 채용 프로세스를 가지고 있었다.</p>
<p>그중에서도 이번에 컬처핏 면접을 처음 경험해봤는데,
처음이라 그런지 정말 많은 준비를 해갔다.
하지만 결과는 아쉽게도 탈락이었다. 😢</p>
<p>이 회사는 서비스 도메인도 내가 정말 관심 있는 분야였고,
합류하게 된다면 많은 걸 배우고 성장할 수 있을 거란 기대감도 컸기에,
여러 지원처 중 가장 미련이 남는 곳이었다.</p>
<p>그래도 언젠가 다시 채용 공고가 올라올 거라 믿고,
그때는 꼭 다시 지원해보려고 한다.
(이 글 혹시 보고 계시다면… 제발 뽑아주세요 ㅠㅠ)</p>
<h3 id="🚨-레전드-상황-발생---토스-개발자들이-꿈꾸는-회사로-간다">🚨 레전드 상황 발생 - &quot;토스&quot; 개발자들이 꿈꾸는 회사로 간다.</h3>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/f416020b-0851-4d22-8210-e8286eebb766/image.png" alt=""></p>
<p>탈락 메일을 받고 아쉬움에 잠겨 있던 것도 잠시,
모르는 번호로 한 통의 전화가 걸려왔다.</p>
<p>전화를 받자마자 들려온 목소리는... 토스 채용팀이었다.
&quot;이번에 토스 프론트엔드 개발자로 최종 합격하셨습니다.&quot;
첫 출근 날짜를 조율하고, 전화를 마치고 나니 손이 덜덜 떨릴 정도였다.</p>
<p>정말 꿈만 같았다.</p>
<p>토스에는 이력서를 10번 넘게 넣었고, 그때마다 탈락했었다.
그러다 어느 날, 하나의 서류가 붙었고, 라이브 코딩 테스트와 면접까지 진행하게 되었다.
그리고... 믿기 어렵게도 그게 붙어버린 것이다.</p>
<p>이 글을 쓰고 있는 지금은, 합격 연락을 받은 지 이틀째 되는 날이다. 아직도 토스라는 단어만 들어도 저절로 웃음이 난다.</p>
<p>7월 21일, 내 커리어가 본격적으로 시작된다.</p>
<p>토스 개발자 분들이 어떤 방식으로 일하고, 어떤 생각을 하며 코드를 짜는지 항상 궁금했는데, 이제는 그걸 바로 옆에서 지켜볼 수 있는 위치에 내가 서 있게 되었다.</p>
<p>비록 어시스턴트 포지션이라 짧은 기간일 수는 있지만,
그 안에서 최대한 많이 배우고 성장해서 나가고 싶다. 요즘은 토스에서 어떻게 일하면 좋을지, 그리고 무엇을 배워 나가야 할지 매일 고민하고 기대하고 있다.</p>
<h1 id="마무리">마무리</h1>
<p>취업 준비를 하면서 항상 미래에 대한 불안감을 지울 수 없었다. 불안이 커질수록 더 많이 노력했고, 주변 사람들과 이야기하며 그 불안감을 조금씩 덜어낼 수 있었다.</p>
<p>가끔 이런 생각이 든다. “작년처럼 혼자 준비했더라면, 지금의 결과를 만들 수 있었을까?” 그 질문엔 자신 있게 “절대 아니다”라고 말할 수 있을 정도로, 정말 많은 사람들의 도움을 받았고 나 자신도 최선을 다해 달려왔다.</p>
<p>그래서 이번엔 주변 사람들에게 진심으로 감사의 인사를 전하고 싶고, 무엇보다 내 자신에게도 큰 칭찬을 해주고 싶다.</p>
<p>하반기엔 어떤 일이 있을지 모르지만, 지금처럼 나를 돌아보고 성장하는 시간을 꾸준히 가져가고 싶다. 이 글을 읽고 계신 분들이 댓글로 짧게나마 응원의 한마디씩 남겨주신다면 정말 큰 힘이 될 것 같다☺️</p>
<p>긴 글 읽어주셔서 감사합니다. 다음에도 좋은 이야기로 돌아오겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[공식 홈페이지 Next.js로 마이그레이션 ]]></title>
            <link>https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-%EA%B3%B5%EC%8B%9D-%ED%99%88%ED%8E%98%EC%9D%B4%EC%A7%80-Next.js%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%BA%90%EC%8B%B1%EC%9D%84-%EA%B3%81%EB%93%A4%EC%9D%B8</link>
            <guid>https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-%EA%B3%B5%EC%8B%9D-%ED%99%88%ED%8E%98%EC%9D%B4%EC%A7%80-Next.js%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%BA%90%EC%8B%B1%EC%9D%84-%EA%B3%81%EB%93%A4%EC%9D%B8</guid>
            <pubDate>Tue, 10 Jun 2025 16:27:03 GMT</pubDate>
            <description><![CDATA[<h1 id="🔥-기존-큐시즘-홈페이지의-문제점">🔥 기존 큐시즘 홈페이지의 문제점</h1>
<p>큐시즘 공식 홈페이지는 내부 활동과 프로젝트를 외부에 효과적으로 알리는 중요한 창구로, 구성원 모집 및 대외 홍보에 핵심적인 역할을 합니다. 그러나 기존 홈페이지는 클라이언트 사이드 렌더링(CSR) 방식으로 구현되어 있어 몇 가지 중요한 문제점이 있었습니다.</p>
<ol>
<li><p>초기 로딩 속도가 느려 사용자 경험에 부정적인 영향을 주었습니다. 콘텐츠가 자바스크립트 로딩 이후에야 렌더링되어, 네트워크 상태가 좋지 않거나 저사양 기기를 사용하는 방문자의 경우 <strong>페이지가 표시되기까지 상당한 지연</strong>이 발생했습니다. 실제로 Lighthouse 기준 <strong>LCP(Largest Contentful Paint) 점수가 약 7초</strong>로 측정되어, 웹 성능 측면에서 개선이 시급한 상황이었습니다.</p>
</li>
<li><p>검색엔진 최적화(SEO)가 부족해 정보 노출에 제한이 있었습니다. 모든 콘텐츠가 클라이언트에서 동적으로 생성되다 보니 크롤러가 주요 정보를 제대로 인식하지 못했고, 이는 <strong>검색 결과에 큐시즘 활동이나 프로젝트가 노출되지 않는 문제</strong>로 이어졌습니다.</p>
</li>
<li><p>그동안 큐시즘 프로젝트 전시는 별도의 페이지에서 분리 관리되고 있어, 사용자가 큐시즘 전체 활동을 한눈에 파악하기 어려운 구조였습니다. 이로 인해 전시 콘텐츠의 접근성과 일관성이 떨어졌고, 내부적으로도 유지보수에 불편함이 존재했습니다.</p>
</li>
<li><p>반응형 작업이 되어 있지 않아 태블릿이나 모바일에서 접속 시 UI가 깨지는 문제가 있었습니다.</p>
</li>
</ol>
<h2 id="🤔-왜-nextjs를-사용하였는가">🤔 왜 Next.js를 사용하였는가?</h2>
<p>이번 리뉴얼에서는 Next.js 기반으로 전체 구조를 마이그레이션하고, 동시에 프로젝트 전시 탭도 새로 구축할 예정입니다.
Next.js를 선택한 이유는 다음과 같습니다:</p>
<p><strong>서버 사이드 렌더링(SSR) 및 정적 생성(ISR)</strong>을 통해 초기 렌더 속도와 SEO를 자연스럽게 개선할 수 있어, 성능과 검색 노출 문제를 동시에 해결할 수 있기 때문입니다.</p>
<p>무엇보다 개인적으로 React보다 최적화에 신경 쓸 부분이 적어 <strong>개발 생산성을 높일 수 있었고</strong>, Next.js 경험이 많아 짧은 시간 안에 안정적인 마이그레이션과 신규 전시 탭 개발을 병행할 수 있다는 점이 결정적인 이유였습니다.</p>
<p>이번 개편을 통해 큐시즘의 활동과 프로젝트를 더 많은 사람에게 빠르고 정확하게 전달하고, 다양한 디바이스 환경에서도 일관된 사용자 경험을 제공할 수 있도록 할 예정입니다.</p>
<hr>
<h1 id="📀-캐싱을-이용하여-홈페이지-개선">📀 캐싱을 이용하여 홈페이지 개선</h1>
<p>제가 맡은 부분은 <strong>프로젝트를 한눈에 볼 수 있는 프로젝트 페이지</strong>와 <strong>큐밀리(학회원)들이 남긴 후기를 모아볼 수 있는 후기 페이지</strong>였습니다.</p>
<h2 id="💙-프로젝트-페이지">💙 프로젝트 페이지</h2>
<p>🔗 <a href="https://github.com/kusitms-com/kusitms.com/blob/dev/src/pages/Projects/ProjectsPage.tsx">기존 코드 링크</a>
🔗 <a href="https://github.com/kusitms-com/kusitms.com/blob/main/src/app/projects/meetup/page.tsx">개선 코드 링크</a>
🔗 <a href="https://www.kusitms.com/projects/meetup">프로젝트 페이지 링크</a></p>
<p>CSR 구조로 코드가 작성되어 있어 초기 로딩 속도가 매우 느렸습니다. 콘텐츠가 자바스크립트 로딩 이후에야 렌더링되어, 네트워크 상태가 좋지 않거나 저사양 기기를 사용하는 방문자의 경우 <strong>페이지가 표시되기까지 상당한 지연</strong>이 발생했습니다. 실제로 Lighthouse 기준 <strong>LCP(Largest Contentful Paint) 점수가 약 7초</strong>로 측정되어, 웹 성능 측면에서 개선이 시급한 상황이었습니다.</p>
<p>그리고 해당 링크의 코드를 보면, 페이지 파일임에도 불구하고 <strong>컴포넌트 분리가 제대로 되어 있지 않고</strong>,  
<strong>반응형 작업을 시도하다가 마무리되지 않아 주석이 많이 남아 있는 코드</strong>임을 확인할 수 있습니다.</p>
<p>이 코드를 어떻게 활용해 개선할 수 있을지 많이 고민했지만,<br>결국 <strong>스타일 코드 외에는 가져올 수 있는 부분이 없었습니다</strong>.  
기존 스타일 코드를 <strong>TailwindCSS</strong>로 변환하는 작업은 오래 걸리지 않을 것으로 판단되어 스타일만 참고하였고,<br><strong>HTML 구조나 컴포넌트 구조는 너무 복잡하게 되어 있어 처음부터 새로 만드는 것이 더 효율적이라고 판단</strong>했습니다.</p>
<p>따라서, <strong>피그마 디자인을 참고하며 처음부터 구조를 다시 잡고 캐싱을 이용하여 페이지 마이그레이션 작업을 진행</strong>하였습니다.</p>
<h2 id="💙-페이지의-isr-적용">💙 페이지의 ISR 적용</h2>
<table>
  <tr>
    <td><img src="https://velog.velcdn.com/images/yunbh_0401/post/6af95070-6fc2-461e-a608-7621dcf77e82/image.png" width="500"/></td>
    <td><img src="https://velog.velcdn.com/images/yunbh_0401/post/cbc6adb9-9fdf-4fc3-97b5-9abb4bf1da24/image.png" width="500"/></td>
  </tr>
</table>


<p>프로젝트 페이지에 들어가면, 밋업 프로젝트와 기업 프로젝트를 각각 확인할 수 있도록 탭이 나뉘어 있는 것을 볼 수 있습니다. 
저희 큐시즘 웹사이트는 실시간으로 데이터를 불러오는 구조가 아니기 때문에, SSR(Server Side Rendering)이 아닌 ISR(Incremental Static Regeneration) 방식으로 정적 페이지를 제공하는 것을 목표로 했습니다.</p>
<p>SSR은 사용자가 페이지에 접속할 때마다 서버에서 실시간으로 페이지를 생성해주는 방식입니다. 반면 ISR은 한 번 정적으로 페이지를 생성해두고, 일정 주기마다(예: 1주일) 백그라운드에서 새로운 데이터로 페이지를 갱신하는 방식입니다.
큐시즘 홈페이지의 특성을 보면, 가장 많은 접속이 이루어지는 시기는 2월과 7월입니다. 이 시기는 신규 모집 인원을 뽑기 시작하면서 지원자 분들이 대거 큐시즘 사이트에 방문하는 시기입니다. 
하지만 저희 사이트는 데이터가 자주 바뀌지 않는 구조이기 때문에, SSR 방식을 사용하면 2월과 7월에 갑자기 많은 사용자가 몰릴 때마다 서버가 실시간으로 페이지를 계속 생성해야합니다. 이로 인해 서버 부하가 급격히 증가할 수 있고, 심한 경우 사이트 접속이 원활하지 않을 수 있다는 문제점이 있습니다.</p>
<p>이런 상황을 고려해, 저희는 ISR 방식을 선택했습니다. ISR을 적용하면 페이지가 미리 정적으로 생성되어 있기 때문에, 많은 사용자가 동시에 접속하더라도 서버에 큰 부담이 가지 않는다. 또한, 데이터가 변경될 필요가 있을 때만 주기적으로(예: 1주일마다) 페이지가 갱신되기 때문에, 최신 정보도 적절히 반영할 수 있습니다.</p>
<blockquote>
<p>그렇다면 왜 SSG(Static Site Generation) 방식으로 하지 않고 굳이 ISR을 선택했는가?</p>
</blockquote>
<p>사실 저희도 처음에는 SSG 방식으로 구현하려고 했습니다. SSG는 빌드 시점에 모든 페이지를 미리 생성해두는 방식이기 때문에, 데이터가 거의 변하지 않는 사이트에는 매우 적합합니다. 하지만 큐시즘 프로젝트의 경우, 모집 일정이나 프로젝트 정보가 가끔씩 변경될 수 있고, 새로운 프로젝트가 추가될 수도 있습니다. 만약 SSG로만 구현한다면, 이런 변경 사항을 반영하려면 사이트 전체를 다시 빌드하고 배포해야 한다는 번거로움이 있었습니다.
반면 ISR을 사용하면, 데이터가 변경되더라도 설정한 주기(예: 1주일)에 맞춰 자동으로 페이지가 갱신됩니다. 만약 긴급하게 정보를 업데이트해야 할 경우, Next.js의 태그 기능을 활용해 특정 페이지만 빠르게 재생성할 수도 있습니다. 이런 유연함 덕분에, 저희는 SSG 대신 ISR 방식을 선택하게 되었습니다.</p>
<p>실제로 밋업 프로젝트 데이터를 패치하는 코드는 아래와 같이 작성했습니다.</p>
<h3 id="data-fetch-code">Data Fetch Code</h3>
<pre><code class="language-ts">export const getMeetupProjects = async (
  cardinal: string,
  batch?: string
): Promise&lt;MeetupResponse&gt; =&gt; {
  try {
    const url = `${baseUrl}/api/projects/meetup?batch=${batch}&amp;cardinal=${cardinal}&amp;order=desc`;

    const res = await fetch(url, {
      method: &quot;GET&quot;,
      headers: {
        &quot;Content-Type&quot;: &quot;application/json&quot;,
      },
      cache: &quot;force-cache&quot;,
      next: { revalidate: 604800, tags: [&quot;meetupProjects&quot;] }, // 1주일(604800초)마다 재검증
    });

    if (!res.ok) {
      throw new Error(`HTTP error! status: ${res.status}`);
    }

    const data = await res.json();

    return data;
  } catch (err) {
    console.error(&quot;Failed to fetch meetup projects:&quot;, err);
    throw err;
  }
};</code></pre>
<h3 id="page-code">Page Code</h3>
<pre><code class="language-ts">async function MeetupProjectPage() {
  const meetupProjectList = await getMeetupProjects(&quot;&quot;);

  return (
    &lt;&gt;
      &lt;section className=&quot;w-full max-w-6xl mx-auto py-8 text-center&quot;&gt;
        &lt;h1 className=&quot;text-5xl font-black mb-4 mt-[180px] leading-[130%]&quot;&gt;
          KUSITMS의 다양한 프로젝트를 &lt;br /&gt;
          구경해보세요!
        &lt;/h1&gt;
        &lt;p className=&quot;font-normal text-[20px]&quot;&gt;
          &lt;ProjectTotalCount pathname=&quot;meetup&quot; /&gt;의 프로젝트를 볼 수 있어요.
        &lt;/p&gt;
        &lt;NavButtons /&gt;
      &lt;/section&gt;
      &lt;section className=&quot;mx-auto w-full max-w-[1180px]&quot;&gt;
        &lt;div className=&quot;flex w-full gap-[90px] mt-[100px] justify-center items-center&quot;&gt;
          &lt;ImageCard&gt;
            &lt;ImageCard.Sticker&gt;
              &lt;Image
                src=&quot;/projects/sticker/MeetupSticker.svg&quot;
                alt=&quot;스티커&quot;
                width={70}
                height={100}
                priority
                style={{ width: 70, height: 100 }}
              /&gt;
            &lt;/ImageCard.Sticker&gt;
            &lt;ImageCard.Image src=&quot;/projects/tmp/meetup_tmpImg.jpeg&quot; /&gt;
          &lt;/ImageCard&gt;
          &lt;EventIntro type=&quot;meetup&quot; /&gt;
        &lt;/div&gt;
        &lt;ProjectContainer data={meetupProjectList.data} /&gt;
      &lt;/section&gt;
    &lt;/&gt;
  );
}

export default MeetupProjectPage;</code></pre>
<h3 id="🧩-data-cache">🧩 Data Cache</h3>
<p>우리가 일반적으로 생각할 수 있는 API 캐싱입니다.</p>
<pre><code class="language-ts">// Revalidate at most every hour
fetch(&#39;https://...&#39;, { next: { revalidate: 3600 } })</code></pre>
<p>Next.js가 확장해놓은 fetch 함수에 next.revalidate 옵션을 넘기면 <a href="https://nextjs.org/docs/app/deep-dive/caching">Data Cache</a>가 동작합니다. 성공적으로 데이터를 가져왔다면 그 응답값을 저장해두었다가 동일한 경로로 fetch 함수를 실행할 때 실제 API 호출은 건너뛰고 저장해놓은 응답값을 반환합니다.</p>
<p>하나의 요청 동안만 유효한 Request Memoization과 다르게 Data Cache는 일정 시간 동안에 웹 서버로 들어오는 모든 요청에 대해 동작합니다. 만약 next.revalidate를 1초로 설정했다면, 1초에 1000명의 사용자가 접속해도 실제 API 요청은 1회 전송됩니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/a9de7750-bf33-4731-b111-f91f7ffdce87/image.png" alt=""></p>
<p>이 내용을 활용하여 Next.js의 fetch 옵션에 캐시 유효 기간을 일주일(604800초)로 설정했습니다. 이 설정 덕분에, 페이지는 최초 빌드 시 정적으로 생성되고, 7일마다 백그라운드에서 자동으로 최신 데이터로 갱신됩니다. 덕분에 실시간성이 필요 없는 프로젝트 페이지를 효율적으로 관리할 수 있고, 사용자는 항상 빠른 속도로 페이지를 이용할 수 있었습니다.
이처럼 SSR의 서버 부하 문제와 SSG의 불편함을 모두 피하면서, ISR을 통해 안정적이고 효율적으로 프로젝트 페이지를 운영할 수 있게 되었습니다.</p>
<h4 id="✨tip">✨Tip</h4>
<blockquote>
<p>fetch 문에 캐싱 주기(revalidate)만 설정하면 페이지 파일에서 별도로 <code>revalidate</code>를 따로 지정할 필요 없이 ISR이 자동으로 적용됩니다.</p>
</blockquote>
<hr>
<h2 id="💜-프로젝트-상세-페이지">💜 프로젝트 상세 페이지</h2>
<p>🔗 <a href="https://github.com/kusitms-com/kusitms.com/blob/dev/src/pages/ProjectDetail/index.tsx">기존 코드 링크</a>
🔗 <a href="https://github.com/kusitms-com/kusitms.com/blob/main/src/app/projects/meetup/%5BprojectNumber%5D/page.tsx">개선된 코드 링크</a>
🔗 <a href="https://www.kusitms.com/projects/meetup/45">프로젝트 상세 페이지 링크</a></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/7e676f4b-8a75-4a7a-9911-89a7b9cfcd4a/image.gif" alt=""></p>
<p>위 사진을 보면 알 수 있듯이, 각 프로젝트 요소를 클릭하면 상세 페이지로 이동하는 플로우로 구현되어 있다. URL도 /project/meetup에서 /project/meetup/프로젝트ID와 같이 동적 라우팅이 적용되도록 구성했다.</p>
<p>이렇게 동적 라우팅이 적용된 상세 페이지에서는 각 프로젝트의 이미지를 불러오게 되는데, 만약 이 페이지를 SSR(Server Side Rendering) 방식으로 렌더링하면 이미지가 처음에 천천히, 서서히 나타나는 현상이 발생할 수 있다. 이는 서버에서 페이지를 렌더링할 때 이미지 리소스가 완전히 준비되지 않아, 사용자가 페이지에 진입했을 때 이미지가 점진적으로 로드되기 때문이다.</p>
<p>이 문제를 해결하기 위해서 저는 3가지 방법을 이용하여 해결해보았습니다.</p>
<h2 id="💜-페이지-isr-적용">💜 페이지 ISR 적용</h2>
<p>첫 번째로, 해당 상세 페이지를 ISR(Incremental Static Regeneration)로 전환해 정적으로 미리 생성해두었습니다.
이렇게 하면 서버가 페이지와 이미지를 미리 렌더링해두기 때문에, 사용자가 상세 페이지에 접속했을 때 이미지가 지연 없이 빠르게 표시될 수 있습니다.</p>
<p>상세 페이지를 ISR(Incremental Static Regeneration)로 전환하기 위해 Next.js의 generateStaticParams 함수를 사용하여 빌드 시점에 미리 정적 경로를 생성해주고, 이후에는 설정한 주기마다 백그라운드에서 페이지를 재생성되게 설정을 해주었습니다.</p>
<h3 id="page-code-1">Page Code</h3>
<pre><code class="language-ts">import Image from &quot;next/image&quot;;
import { getMeetupProjectDetail, getMeetupProjects } from &quot;@/service/projects&quot;;

export async function generateStaticParams() {
  const meetupProjectList = await getMeetupProjects(&quot;&quot;);
  return meetupProjectList.data.meetup_list.map((project) =&gt; ({
    projectNumber: project.meetup_id.toString(),
  }));
}

async function ProjectDetailPage({ params }) {
  const { projectNumber } = await params;
  const { data: project } = await getMeetupProjectDetail(projectNumber);

  return (
    &lt;div&gt;
      {/* ...생략... */}
      &lt;Image
        src={project.poster_url ?? &quot;/footerLogo.svg&quot;}
        alt=&quot;포스터&quot;
        width={580}
        height={820}
        style={{ width: &quot;580px&quot;, height: &quot;820px&quot; }}
        priority
        unoptimized
      /&gt;
      {/* ...생략... */}
    &lt;/div&gt;
  );
}

export default ProjectDetailPage;
</code></pre>
<h2 id="💜-이미지-태그에-priority-옵션적용">💜 이미지 태그에 priority 옵션적용</h2>
<p>또한 이미지 로딩 성능 개선을 위해 Next.js의 Image 컴포넌트를 적극 활용했습니다. 내부 정적 이미지의 경우 빌드 시점에 최적화 및 캐싱이 자동으로 적용되며, 외부 이미지의 경우 첫 접속 시 서버에서 최적화 및 캐싱을 수행합니다. 단, 외부 이미지의 경우 <strong>배포 시마다 캐시가 초기화될 수 있으므로 주의</strong>가 필요합니다.</p>
<p>특히 주요 콘텐츠(예: 프로젝트 포스터 등)에 대해서는 <code>&lt;Image priority /&gt;</code> 옵션을 적용해 지연 로딩을 비활성화하고, 초기 렌더링 시 바로 로드되도록 처리했습니다. 이를 통해 LCP 성능을 더욱 개선하고 사용자 경험을 높였습니다.</p>
<pre><code class="language-ts">&lt;Image
  src={project.poster_url}
  alt=&quot;포스터&quot;
  width={580}
  height={820}
  priority // 이 옵션이 핵심!
/&gt;</code></pre>
<h2 id="💜-link-태그-프리페칭-적용">💜 Link 태그 프리페칭 적용</h2>
<p>Next.js의 Link 컴포넌트는 기본적으로 prefetch가 활성화되어 있어, 뷰포트에 들어오거나 마우스 호버 시 해당 페이지의 리소스를 미리 받아옵니다. 이 기능 덕분에 사용자가 링크를 클릭하기 전에 이미 필요한 데이터를 미리 받아와서, 실제로 페이지를 이동할 때 훨씬 빠른 전환이 가능합니다.</p>
<p>하지만 프로젝트 리스트처럼 한 번에 많은 링크가 렌더링되는 경우,모든 링크에 대해 자동으로 prefetch가 일어나면 네트워크 리소스가 불필요하게 낭비될 수 있다고 생각했습니다. 특히 사용자가 실제로 클릭하지 않을 링크까지 모두 미리 받아오게 되면, 브라우저가 불필요한 데이터를 많이 다운로드하게 되고, 이로 인해 오히려 성능이 저하될 수도 있다고 판단하였습니다.
이런 상황을 방지하기 위해,</p>
<p>그래서 <code>prefetch={false}</code>로 자동 프리페치를 꺼주고, <code>onMouseEnter</code> 이벤트에서 <code>router.prefetch()</code>를 직접호출해 &quot;정말 사용자가 관심을 보인(마우스를 올린) 링크&quot;에 대해서만 프리페치가 일어나도록 설정했습니다.
이렇게 하면 네트워크 리소스를 효율적으로 사용할 수 있고, 실제로 사용자가 클릭할 가능성이 높은 링크만 미리 데이터를 받아오게 되어 성능과 UX를 모두 챙길 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/cfef1118-7577-4ff8-a948-ce1149ec178c/image.gif" alt=""></p>
<pre><code class="language-ts">import Link from &quot;next/link&quot;;
import { useRouter } from &quot;next/navigation&quot;;

function MeetupProjectLink({ project }) {
  const router = useRouter();

  return (
    &lt;Link
      href={`/projects/meetup/${project.meetup_id}`}
      prefetch={false}
      onMouseEnter={() =&gt; router.prefetch(`/projects/meetup/${project.meetup_id}`)}
    &gt;
      {/* 카드 내용 */}
    &lt;/Link&gt;
  );
}</code></pre>
<hr>
<h1 id="🔎-검색엔진-노출을-위한-설정">🔎 검색엔진 노출을 위한 설정</h1>
<h2 id="🔖-meta-data-설정">🔖 Meta Data 설정</h2>
<p>큐시즘 웹사이트는 과거에 모든 콘텐츠를 클라이언트에서 동적으로 생성하는 구조였기 때문에, 검색엔진 최적화(SEO)가 부족해 정보 노출에 한계가 있었습니다. 실제로 크롤러가 주요 정보를 제대로 인식하지 못해 큐시즘의 활동이나 프로젝트가 검색 결과에 잘 노출되지 않는 문제가 있었습니다. </p>
<p>이 문제를 해결하기 위해, 최근에는 각 페이지와 상세 페이지 레이아웃에 메타데이터(metadata) 설정을 꼼꼼하게 적용해보았습니다.</p>
<p>예를 들어, 프로젝트 전체 목록 페이지에서는 아래와 같이 metadata 객체를 선언해 title, description, keywords, openGraph, twitter 카드 등 다양한 정보를 명시적으로 지정했다.</p>
<pre><code class="language-ts">// src/app/projects/layout.tsx
export const metadata: Metadata = {
  title: &quot;KUSITMS | Projects&quot;,
  description: &quot;큐시즘에서 진행한 다양한 프로젝트들을 한눈에 확인해보세요.&quot;,
  keywords: [&quot;큐시즘&quot;, &quot;KUSITMS&quot;, &quot;프로젝트&quot;, &quot;밋업&quot;, &quot;기업&quot;, &quot;대학생 IT 학회&quot;],
  openGraph: { ... },
  twitter: { ... },
};</code></pre>
<p>또한, 프로젝트 상세 페이지의 경우에는 각 프로젝트별로 고유한 메타데이터가 동적으로 생성되도록 아래와 같이 구현했다.
이렇게 하면 각 프로젝트마다 검색 노출이 잘 되도록 할 수 있습니다.</p>
<pre><code class="language-ts">// src/app/projects/meetup/[projectNumber]/layout.tsx
export async function generateMetadata({ params }: Props) {
  const { projectNumber } = await params;
  const { data: project } = await getMeetupProjectDetail(projectNumber);

  return {
    title: `KUSITMS | ${project.name}`,
    description: `${project.one_line_intro}`,
    keywords: [project.name, &quot;KUSITMS&quot;, &quot;큐시즘&quot;, &quot;밋업&quot;, &quot;프로젝트&quot;],
    openGraph: { ... },
    twitter: { ... },
  };
}</code></pre>
<p>이런 메타데이터 설정 덕분에 검색엔진이 각 프로젝트의 정보를 정확하게 인식할 수 있게 되었고, 큐시즘 활동과 프로젝트가 검색 결과에 더 잘 노출되는 효과를 기대할 수 있게 됐다.</p>
<h2 id="🔖-robots--sitemap-설정">🔖 robots &amp; sitemap 설정</h2>
<p>여기에 더해, 검색엔진이 사이트 전체를 자유롭게 탐색할 수 있도록 robots.txt 파일도 설정했습니다.
아래와 같이 모든 검색엔진이 사이트 전체를 크롤링할 수 있도록 허용하고, sitemap의 위치도 명시했습니다.</p>
<pre><code class="language-ts">export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: &quot;*&quot;,
      allow: &quot;/&quot;,
    },
    sitemap: &quot;https://www.kusitms.com/sitemap.xml&quot;,
  };
}</code></pre>
<p>또한, 사이트의 전체 구조와 각 페이지의 URL, 갱신 주기, 우선순위 등을 검색엔진에 알려주기 위해 sitemap.xml도 동적으로 생성했습니다.
특히 동적으로 생성되는 프로젝트 상세 페이지까지 모두 sitemap에 포함시켜, 검색엔진이 모든 프로젝트를 빠짐없이 인덱싱할 수 있도록 해주었습니다.</p>
<pre><code class="language-ts">export default async function sitemap(): Promise&lt;MetadataRoute.Sitemap&gt; {
  const meetupProjectList = await getMeetupProjects(&quot;&quot;);
  const dynamicRoutes = meetupProjectList.data.meetup_list.map((project) =&gt; ({
    url: `https://www.kusitms.com/projects/meetup/${project.meetup_id}`,
    lastModified: new Date(),
    changeFrequency: &quot;monthly&quot;,
    priority: 1,
  }));

  return [
    { url: &quot;https://www.kusitms.com&quot;, ... },
    { url: &quot;https://www.kusitms.com/projects/meetup&quot;, ... },
    ...dynamicRoutes,
  ];
}</code></pre>
<p>이렇게 메타데이터, robots.txt, sitemap.xml을 체계적으로 설정함으로써, 검색엔진이 큐시즘의 모든 활동과 프로젝트 정보를 정확하게 인식하고, 검색 결과에 더 잘 노출될 수 있도록 개선했습니다.</p>
<p>이로 인해 큐시즘의 온라인 가시성과 접근성이 크게 향상되었습니다.</p>
<hr>
<h1 id="🤩-성과">🤩 성과</h1>
<table>
  <tr>
    <td><img src="https://velog.velcdn.com/images/yunbh_0401/post/f69351bf-857c-409a-bf3c-10d8c901c4d1/image.png" width="400"/></td>
    <td><img src="https://velog.velcdn.com/images/yunbh_0401/post/c2812854-2ede-46ce-91d0-16bf095873bf/image.png" width="350"/></td>
  </tr>
  <tr>
    <td><img src="https://velog.velcdn.com/images/yunbh_0401/post/3b0f9e68-ed3a-4b25-9189-284b41f48cf3/image.png" width="400"/></td>
    <td><img src="https://velog.velcdn.com/images/yunbh_0401/post/7de3a9ad-70a8-4f37-8003-3004384bbc43/image.png" width="400"/></td>
  </tr>
</table>


<p>이번 마이그레이션의 핵심 목표 중 하나는 <strong>느린 초기 로딩 속도로 인한 사용자 경험 저하</strong>를 해결하고, <strong>웹 성능 지표를 개선</strong>하는 것이었습니다. 이를 위해 Next.js App Router 기반 SSR 구조를 도입하고, 성능 병목 요소들을 전반적으로 분석 및 개선했습니다.</p>
<h2 id="✅-1-isr-도입을-통한-초기-렌더링-및-이미지-표시-속도-개선">✅ 1. ISR 도입을 통한 초기 렌더링 및 이미지 표시 속도 개선</h2>
<p>기존 홈페이지는 클라이언트 사이드 렌더링(CSR) 방식이어서, 사용자가 페이지를 요청하면 빈 HTML만 전달되고 브라우저가 자바스크립트를 모두 다운로드·실행한 뒤에야 실제 콘텐츠가 보였다. 이로 인해 저사양 기기나 느린 네트워크 환경에서는 렌더링 지연이 심각했습니다.</p>
<h3 id="개선-방식">개선 방식</h3>
<p>Next.js의 App Router 기반 ISR(Incremental Static Regeneration) 기능을 도입해, 주요 페이지와 상세 페이지를 서버에서 미리 정적으로 생성하도록 했다.
특히 상세 페이지의 경우, generateStaticParams를 활용해 각 프로젝트별로 정적 파일을 미리 만들어두고, 이미지도 서버에서 미리 준비되도록 했습니다.</p>
<h3 id="성과">성과</h3>
<p>사용자는 페이지에 진입하자마자 콘텐츠와 이미지를 지연 없이 바로 확인할 수 있게 되었고, LCP(Largest Contentful Paint) 지표가 약 7초에서 1초 이내로 크게 단축되었습니다.</p>
<h2 id="✅-2-이미지-priority-옵션-및-link-프리페치-최적화">✅ 2. 이미지 priority 옵션 및 Link 프리페치 최적화</h2>
<p>대형 썸네일 이미지나 프로젝트 포스터 등 주요 이미지는 Next.js의 <code>Image</code> 태그에 priority 옵션을 적용해, 지연 로드(lazy loading)를 비활성화하고 서버에서 미리 이미지를 준비하도록 했습니다.
또한, 프로젝트 리스트에서 상세 페이지로 이동할 때는 Link 컴포넌트의 prefetch 동작을 커스터마이즈하여, 마우스 호버 시에만 prefetch가 일어나도록 최적화했습니다.
이렇게 함으로써 네트워크 리소스를 효율적으로 사용하면서도, 실제로 사용자가 클릭할 가능성이 높은 링크에 대해서만 빠른 전환 경험을 제공할 수 있었습니다</p>
<h3 id="성과-1">성과</h3>
<p>초기 렌더링에 필요한 이미지만 빠르게 로딩되고, 불필요한 네트워크 사용이 줄어들어 LCP 및 CLS(Cumulative Layout Shift) 지표가 안정화되었다.</p>
<h2 id="✅-3-서버-컴포넌트-기반-번들-크기-및-seo-최적화">✅ 3. 서버 컴포넌트 기반 번들 크기 및 SEO 최적화</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/c4a4787d-e1ef-4604-b05d-59da3f56371e/image.png" alt=""></p>
<p>Next.js App Router 구조를 적극 활용해, UI의 대부분을 서버 컴포넌트(Server Component)로 구성하고, 클라이언트 컴포넌트는 필요한 부분에만 선언적으로 분리했다.
또한, 각 페이지와 상세 페이지 레이아웃에 title, description, og:image 등 메타데이터를 동적으로 생성해 SEO를 강화했다.
robots.txt와 sitemap.xml도 직접 구현하여, 검색엔진이 사이트 전체와 동적 상세 페이지까지 빠짐없이 인덱싱할 수 있도록 했다.</p>
<h3 id="성과-2">성과</h3>
<p>번들 크기와 자바스크립트 파싱/실행 시간이 줄어들어 TTI(Time to Interactive)와 FCP(First Contentful Paint)가 개선되었고, 검색엔진에서의 정보 노출과 페이지 품질 평가도 크게 향상되었다.</p>
<p>이러한 성능 및 SEO 개선 노력의 결과, Lighthouse 기준 주요 웹 성능 지표는 전반적으로 ‘Good’ 등급을 획득했으며, 특히 LCP는 약 7초에서 1초 내외로 크게 개선되었다.
실제 사용자 경험의 체감 속도가 높아졌을 뿐만 아니라, 검색엔진에서 큐시즘의 활동과 프로젝트가 더 잘 노출되는 효과도 얻을 수 있었다.</p>
<hr>
<h1 id="소감">소감</h1>
<p>항상 Next.js를 이용하여 프로젝트를 진행하며 아쉬운 점이 있었습니다.</p>
<p>아 이거 이렇게 하면 더 좋게 고칠 수 있을 거 같은데...
내가 짠 구조가 과연 좋은 구조일까?..</p>
<p>등등 다양한 생각들을 하면서 프로젝트를 끝냈어서 아쉬운 점이 많이 남았었는데,
이번 큐시즘 홈페이지를 마이그레이션하면서는 정말 후회 없이 진행했던 거 같습니다.</p>
<p>이 글에서는 전시 탭 구축과 반응형 적용에 대해서는 따로 언급하지 않았지만,
전시 탭은 이미 구축을 끝낸 상태이고, 반응형 작업만 남았습니다.</p>
<p>반응형 작업은 7월에 진행할 예정이라, 나중에 이것도 따로 글로 한 번 정리해보려 합니다.</p>
<p>마지막으로 제 깃허브 남기며 글을 끝내보겠습니다. 긴 글 읽어주셔서 감사합니다.</p>
<p>피드백은 언제나 환영이니 많은 관심과 조언 부탁드리겠습니다.</p>
<p><a href="https://github.com/78-artilleryman">깃허브 주소</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🪖 [큐시즘] 밋업 프로젝트 회고]]></title>
            <link>https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-%EB%B0%8B%EC%97%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-%EB%B0%8B%EC%97%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 03 Jun 2025 14:27:32 GMT</pubDate>
            <description><![CDATA[<h1 id="💙-큐시즘이란">💙 큐시즘이란?</h1>
<p>💡한국대학생 IT 경영학회로, 매 기수 70-80명의 대학생들이 모여 각각 기획, 디자인, 개발(프론트/백엔드) 파트를 맡아 IT 프로덕트를 만드는 학회</p>
<p>기업과 연계하여 진행되는 기업 프로젝트, 자체적으로 서비스를 만드는 밋업 프로젝트 등 실무형 커리큘럼과 각종 네트워킹까지 다양한 활동을 할 수 있는 학회입니다.</p>
<p>이 중 이번에는 큐시즘 인원들끼리 팀을 꾸려 기획자 분들이 아이디어를 발제하여 그것에 대한 mvp를 만드는 밋업 프로젝트에 대해서 간단하게 설명을 해드리고 회고를 해보려고 합니다.
이번 밋업프로젝트는 총 두달간 진행되었는데요 그 과정을 함께 보시죠</p>
<hr>
<h1 id="💡-아이디어-발제-및-커피챗">💡 아이디어 발제 및 커피챗</h1>
<blockquote>
<p>아이디어 발제</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/df3dc53b-c884-4017-ab30-530d4b218be2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/986067b2-0ce3-4d4b-894b-323ddcdc382c/image.png" alt=""></p>
<blockquote>
<p>커피챗</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/851dbd58-cfeb-4a54-a4e4-9f4b05eed248/image.png" alt=""></p>
<p>큐시즘의 메인 프로젝트인 밋업프로젝트의 시작을 알리는 아이디어 발제 및 커피챗 세션을 진행하였습니다. 사전 아이디어톤을 통해 선발된 8개 아이디어에 대해서 발표하고, 이야기를 나누는 시간을 가졌습니다.</p>
<p>8개 아이디어 전부 너무 훌륭한 아이디어여서 선택을 하는 과정에서 정말 많은 고민을 했었습니다.</p>
<h2 id="아이디어-선택-기준">아이디어 선택 기준</h2>
<p>아이디어를 선정할 때 저는 다음 세 가지 기준을 중심으로 고민했습니다:</p>
<ol>
<li><p>프론트엔드 개발자로서 새로운 기술이나 도전을 경험할 수 있는가?</p>
</li>
<li><p>제한된 기간 내에 MVP를 완성할 수 있는 적정 규모인가?</p>
</li>
<li><p>실제 사용자 확보가 비교적 수월한 주제인가?</p>
</li>
</ol>
<hr>
<h1 id="🪖-사랑꾼">🪖 사랑꾼</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/bd499cff-1da6-4ab7-8695-97f7d281a676/image.png" alt=""></p>
<p>이런 기준을 세우고 아이디어를 고른 결과 <strong>사랑꾼</strong>이란 곰신 커플들을 위한 서비스를 선택하게 되었습니다.</p>
<p>기존의 군인/곰신 커플은 군대라는 장소와 연락의 제약으로 인해 많은 문제를 겪어왔습니다. 상호 단절이 자연스럽고, 이에 불화가 생길 시에 풀기 어려운 문제가 발생합니다. 이러한 문제점이 더욱 가중될 수 있다는 것은 군인/곰신 커플이 가지는 필연적인 어려움입니다.</p>
<p>사랑꾼은 상호 이해(감정, 일정)를 바탕으로 표현을 도와주어 군인과 곰신의 관계 유지를 돕는 커플 <strong>공유 캘린더 서비스</strong>입니다.</p>
<h2 id="선택-이유">선택 이유</h2>
<p>이번 프로젝트에서는 단순한 웹 서비스가 아닌, 앱에 가까운 경험을 제공할 수 있는 PWA로 개발해야 했습니다. 여기에 푸시 알림 기능까지 더해져 사용자에게 감정과 일정을 효과적으로 전달하는 것이 중요한 과제였죠.</p>
<p>특히 핵심 기능 중 하나인 캘린더는 단순한 날짜 표시 이상의 역할을 해야 했기 때문에, 기존 라이브러리를 사용하는 대신 직접 캘린더를 구현할 수도 있겠다는 부담과 기대가 공존했습니다.
이런 기술적인 도전이 오히려 제게는 큰 동기부여가 되었고, 프론트엔드 개발자로서 한 단계 성장할 수 있는 좋은 기회라고 판단했습니다.</p>
<p>기능적으로는 일정 공유, 감정 상태 전달, 푸시 알림 등 주요 기능들이 명확했고, 두 달이라는 기간 안에 충분히 구현 가능한 규모라 생각했습니다.
무엇보다 팀의 기획자분이 곰신 사용자들의 니즈를 직접 조사하고, 유사 서비스와의 차별점을 명확하게 제시해준 덕분에 사용자 확보에 대한 확신도 들었습니다. 이 점이 최종 선택에 큰 영향을 주었습니다.</p>
<h2 id="기술스택">기술스택</h2>
<table>
<thead>
<tr>
<th>기술</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>TypeScript</strong></td>
<td>타입 안정성을 통해 유지보수성과 리팩터링 효율 향상</td>
</tr>
<tr>
<td><strong>React + Vite</strong></td>
<td>컴포넌트 기반 구조 + 빠른 번들링과 HMR 제공</td>
</tr>
<tr>
<td><strong>PWA</strong></td>
<td>모바일 설치 가능, 웹 푸시 알림 등 기능 제공</td>
</tr>
<tr>
<td><strong>Tailwind CSS</strong></td>
<td>유틸리티 기반 CSS로 빠르고 일관된 스타일링</td>
</tr>
<tr>
<td><strong>shadcn/ui</strong></td>
<td>접근성과 커스터마이징이 강력한 UI 컴포넌트 라이브러리</td>
</tr>
<tr>
<td><strong>Zustand</strong></td>
<td>간단하고 가벼운 전역 상태 관리</td>
</tr>
<tr>
<td><strong>React Query</strong> (<code>@tanstack/react-query</code>)</td>
<td>서버 상태 관리, 캐싱, 리페칭 자동화</td>
</tr>
<tr>
<td><strong>Axios</strong></td>
<td>HTTP 요청/응답 처리 및 인터셉터 설정</td>
</tr>
<tr>
<td><strong>MSW</strong></td>
<td>API mocking으로 프론트 개발 병렬화 지원</td>
</tr>
<tr>
<td><strong>Vitest</strong></td>
<td>Vite와 통합된 빠른 테스트 실행 환경</td>
</tr>
</tbody></table>
<p>이번 프로젝트에서는 PWA 적용이 필수 조건 중 하나였습니다. 이전 프로젝트에서 간단히 PWA를 도입해본 경험이 있었는데, 그때 가장 까다로웠던 부분은 메니페스트와 서비스 워커 등록 과정이었습니다.</p>
<h3 id="⚡️-vite--pwa">⚡️ Vite + PWA</h3>
<p>공식문서 링크</p>
<p>이번에는 그 어려움을 줄이기 위해 많은 리서치를 했고, 다행히도 Vite에서 PWA를 쉽게 적용할 수 있도록 지원해주는 플러그인을 발견했습니다.
해당 플러그인을 통해 PWA 설정을 간편하게 구성할 수 있었고, 공식 문서에 따라 메니페스트와 서비스 워커 등록까지 수월하게 진행할 수 있었습니다.
이로 인해 PWA 도입에 대한 기술적 장벽을 크게 낮출 수 있었고, 앱처럼 동작하는 사용자 경험을 안정적으로 제공할 수 있었습니다.</p>
<h3 id="🧩-msw-mock-service-worker">🧩 MSW (Mock Service Worker)</h3>
<p>프론트엔드 개발을 하다 보면 종종 백엔드 API가 준비되지 않은 상태에서 UI 개발을 먼저 진행해야 할 상황이 생깁니다.
이번 프로젝트 초반에는 MSW를 활용해 API 요청/응답을 가짜로 처리하고, 백엔드와 동시에 개발을 진행하려는 계획이 있었습니다. MSW를 활용하면 실제 서버가 없어도 UI와 데이터 흐름을 테스트할 수 있어 개발 효율을 높일 수 있기 때문입니다.</p>
<p>하지만 예상보다 프로젝트 초기 세팅과 UI 개발에 시간이 많이 소요되었고, 일정이 타이트했던 탓에 MSW를 실제로 적용할 시간은 확보하지 못했습니다.
그럼에도 불구하고 백엔드와는 요청/응답 타입을 명확히 정의하고 협의하면서 개발을 진행했기에, 향후 리팩토링 단계에서 MSW를 활용한 예외 케이스 테스트나 에러 시나리오 처리를 추가해볼 계획입니다.</p>
<h3 id="🧪-vitest">🧪 Vitest</h3>
<p>이번 프로젝트에서는 날짜 기반의 일정 관리 기능이 핵심 중 하나였습니다.
백엔드에서 전달받는 날짜 데이터는 DATE 형식이었고, 이를 사용자가 보기 쉽게 가공하거나, 사용자의 선택 값을 다시 백엔드에서 처리 가능한 형식으로 변환하는 작업이 필요했습니다.
이 과정에서 여러 개의 날짜 변환 함수들이 작성되었고, 공통 유틸로 분리해 재사용성을 높였습니다. 하지만 다양한 입력값에 따라 예외가 발생하기 쉬운 구조였기에 신뢰성 있는 단위 테스트가 꼭 필요했습니다.</p>
<p>그래서 해당 함수들에 대해 Vitest를 활용한 테스트 코드를 작성해, 입력값이 원하는 결과로 정확히 변환되는지 검증했습니다. 이 덕분에 코드의 안정성과 유지보수성이 높아졌습니다.</p>
<p>향후에는 주요 컴포넌트 간 상호작용과 사용자 플로우가 잘 작동하는지 확인하기 위해, 통합 테스트 및 E2E 테스트까지 확장해 나갈 계획입니다.</p>
<p>✅ 테스트 코드 (formatDate.test.ts)</p>
<pre><code class="language-ts">import { describe, it, expect } from &#39;vitest&#39;
import { formatDate } from &#39;./formatDate&#39;

describe(&#39;formatDate 함수&#39;, () =&gt; {
  it(&#39;Date 객체를 &quot;YYYY-MM-DD&quot; 형식의 문자열로 변환한다&#39;, () =&gt; {
    const input = new Date(&#39;2025-12-12T00:00:00Z&#39;)
    const result = formatDate(input)
    expect(result).toBe(&#39;2025-12-12&#39;)
  })

  it(&#39;한 자리 수 월과 일을 0으로 채워서 반환한다&#39;, () =&gt; {
    const input = new Date(&#39;2025-01-05T00:00:00Z&#39;)
    const result = formatDate(input)
    expect(result).toBe(&#39;2025-01-05&#39;)
  })
})</code></pre>
<hr>
<h1 id="🔥-개발-과정">🔥 개발 과정</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/c76c26ac-be71-4836-802b-4972be4be786/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/b45ee843-b7d1-4405-a67d-d0021ee1d053/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/10c5c719-80b4-49db-91dd-d72846d7c586/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/4c6bb6e5-c9b0-4940-9401-50cf1898178e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/6f146a5a-04b9-4d4f-bff8-bdf039bb49a4/image.png" alt="">
<img src="https://velog.velcdn.com/images/yunbh_0401/post/5124c250-4c50-4074-9b42-1a047c96748f/image.jpeg" width="500"/></p>
<p>팀이 꾸려진 이후, 매주 토요일마다 총 5번의 집중 협업 세션이 진행되었습니다. 각 팀은 정해진 시간 동안 함께 모여 프로젝트 개발에 힘을 쏟았고, 매 세션마다 점점 더 탄탄해지는 결과물을 만들어갔습니다.</p>
<p>하지만 개발에만 집중하다 보면 지치기 마련이죠. 그래서 중간중간에는 잠시 쉬어가는 시간도 마련되었습니다.
각 파트별로 모여, 익명으로 제출된 고민들을 주제로 자유롭게 이야기 나누는 토크 세션이 열렸습니다.
준비된 고민 카드 중에서 하나를 뽑아도 되고, 하고 싶은 이야기를 꺼내도 되는 방식으로 편안한 분위기 속에서 서로의 이야기에 공감하고, 함께 고민을 나누며 유대감을 키울 수 있었습니다. 다른 팀의 사람들과도 이야기하며 새로운 관점과 조언을 얻는 뜻깊은 시간이었습니다.</p>
<p>그리고 세션이 시작되기 전, 특별한 시간이 찾아왔습니다. 바로 ‘큐밀리 라디오’!
기프팀, MT, 세션 외 회의나 회식, 소모임, 스터디 등에서 고마웠던 순간들을 되새기며, 따뜻한 마음을 담은 익명 사연이 소개되는 시간이었습니다.
기프팀 전체를 향한 메시지부터, 큐밀리 개개인을 향한 고마운 이야기들까지. 웃음과 감동이 가득했던 큐밀리 라디오는 모두의 마음에 잔잔한 여운을 남겼습니다.</p>
<hr>
<h1 id="🚀-밋업-데이">🚀 밋업 데이</h1>
<blockquote>
<p>발표 </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/20a4078a-f5de-4973-892c-da6ce547e5f7/image.jpg" alt="">
<img src="https://velog.velcdn.com/images/yunbh_0401/post/40ff0fa1-bf98-4269-91e6-772598cac445/image.jpg" alt="">
<img src="https://velog.velcdn.com/images/yunbh_0401/post/41c8dc75-0849-47a0-ae3b-0b8e0aa6327b/image.JPG" alt=""></p>
<p>큐시즘 활동의 피날레라 할 수 있는 밋업데이가 열렸습니다.
각 팀은 발제한 아이디어를 바탕으로 직접 구현한 MVP를 발표하고, 시연하는 시간을 가졌습니다.
이번 밋업데이는 큐시즘 외부인도 신청을 통해 참여할 수 있었기에, 다양한 관람객이 자리를 함께해 더욱 활기찬 분위기 속에서 진행되었습니다.</p>
<p>또한, 유튜브 라이브로 행사 전체가 실시간 송출되며 녹화되었기 때문에 큐시즘에서 탄생한 멋진 서비스들을 더 널리 알릴 수 있는 소중한 기회가 되었습니다.</p>
<hr>
<blockquote>
<p>부스 운영</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/324617d1-c532-44eb-8e6e-098a9d3324be/image.jpeg" alt="">
<img src="https://velog.velcdn.com/images/yunbh_0401/post/632a8a62-0c9a-4a77-8070-186217ee1d3f/image.jpeg" alt=""></p>
<p>발표 이후에는 각 팀이 부스를 열고, 자신들이 만든 서비스를 직접 시연하는 시간을 가졌습니다.
부스를 찾은 참가자들은 실제 서비스를 체험해보고, 피드백을 주고받으며 활발한 소통이 이루어졌습니다.</p>
<p>저희 팀 부스에도 많은 분들이 찾아주셨는데요.
그중에는 현직 기획자, 디자이너, 개발자분들도 계셔서 전문적인 조언을 들을 수 있었고, 실제 곰신 커플 분들도 방문해 주셔서 사용자 입장에서의 소중한 피드백을 들을 수 있었습니다.
서비스에 대한 다양한 의견을 직접 들을 수 있는 뜻깊고 감사한 시간이었습니다.</p>
<hr>
<h1 id="🏃🏿-끝이-아니라-시작">🏃🏿 끝이 아니라 시작</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/35de8b9f-b13f-4222-acff-05d096e83f72/image.JPG" alt="">
<img src="https://velog.velcdn.com/images/yunbh_0401/post/b5b0ec18-e5f4-406b-a6d7-7df0ec05dc77/image.jpg" alt=""></p>
<p>2월에 시작된 큐시즘 여정이 어느덧 막을 내렸습니다.
하지만 우리 팀에게 이 끝은 진짜 시작에 불과합니다.</p>
<p>팀원 모두가 우리가 만든 서비스를 실제로 세상에 선보이고 싶은 열정으로 가득 차 있습니다.
그래서 큐시즘 활동이 종료된 이후에도 지속적으로 만나며, 서비스를 더욱 발전시켜 나가기로 했습니다.</p>
<p>아직 해결해야 할 문제들이 많지만, 하나씩 풀어나가며 앱스토어와 플레이스토어에 정식 출시하고, 실제 사용자들과 함께 서비스 운영을 경험해보려 합니다.</p>
<p>앞으로의 여정도 함께 지켜봐 주신다면 정말 감사하겠습니다.</p>
<p>그리고 방산소년단 다들 힘들겠지만 항상 웃으면서 지금처럼 쭉 계속해보자 화이팅 🫡</p>
<blockquote>
<p>깃허브 주소
<a href="https://github.com/Gomushim/frontend">https://github.com/Gomushim/frontend</a></p>
</blockquote>
<blockquote>
<p>서비스 주소 (아마 잘 안될거임 빨리 고쳐볼게유 ㅠ)
<a href="https://www.sarangkkun.site/">https://www.sarangkkun.site/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🥛[큐시즘] X [서울 우유] 기업 프로젝트 회고]]></title>
            <link>https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-X-%EC%84%9C%EC%9A%B8-%EC%9A%B0%EC%9C%A0-%EA%B8%B0%EC%97%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@yunbh_0401/%ED%81%90%EC%8B%9C%EC%A6%98-X-%EC%84%9C%EC%9A%B8-%EC%9A%B0%EC%9C%A0-%EA%B8%B0%EC%97%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 24 Mar 2025 07:23:02 GMT</pubDate>
            <description><![CDATA[<h1 id="💙-큐시즘이란">💙 큐시즘이란?</h1>
<p>💡한국대학생 IT 경영학회로, 매 기수 70-80명의 대학생들이 모여 각각 기획, 디자인, 개발(프론트/백엔드) 파트를 맡아 IT 프로덕트를 만드는 학회</p>
<p>기업과 연계하여 진행되는 기업 프로젝트, 자체적으로 서비스를 만드는 밋업 프로젝트 등 실무형 커리큘럼과 각종 네트워킹까지 다양한 활동을 할 수 있는 학회입니다.</p>
<p>이 중 이번에는 서울 우유와 함께한 기업 프로젝트에 대한 회고를 해보도록 하겠습니다.</p>
<hr>
<h1 id="🥛-프로젝트-소개-및-문제점-파악">🥛 프로젝트 소개 및 문제점 파악</h1>
<p>현재 서울우유에서는 전자세금계산서의 진위 여부를 담당 직원이 수작업으로 검증하고 있으며, 이 과정에서 휴먼 에러 발생 가능성과 반복적인 업무로 인한 비효율성이 문제가 되고 있습니다.</p>
<p>이를 해결하기 위해 저희는 OCR 기술과 진위 여부 검증 API를 활용하여 전자세금계산서 검증을 자동화하고 업무 효율성을 높이며 휴먼 에러를 최소화하는 백오피스 서비스 프로젝트를 개발하였습니다.</p>
<h3 id="⭐-솔루션-바로보기">⭐ 솔루션 바로보기</h3>
<p><a href="https://www.notion.so/yunbh0401/1b8f7fdfbdbb8077bc89fd28869457c7?pvs=4#1b8f7fdfbdbb8063a8defd6aed2e6602">https://www.notion.so/yunbh0401/1b8f7fdfbdbb8077bc89fd28869457c7?pvs=4#1b8f7fdfbdbb8063a8defd6aed2e6602</a></p>
<h3 id="⭐-자세한-내용">⭐ 자세한 내용</h3>
<p>노션
<a href="https://yunbh0401.notion.site/1b8f7fdfbdbb8077bc89fd28869457c7?pvs=4">https://yunbh0401.notion.site/1b8f7fdfbdbb8077bc89fd28869457c7?pvs=4</a></p>
<p>깃허브
<a href="https://github.com/Seoul-Milk-Team5/SM-Frontend">https://github.com/Seoul-Milk-Team5/SM-Frontend</a></p>
<hr>
<h1 id="💬-개발-과정">💬 개발 과정</h1>
<p>2025년 2월 18일부터 3월 12일까지 약 한 달간 빠르게 진행된 프로젝트였습니다. 기획, 디자인, 프론트엔드, 백엔드 개발이 동시에 이루어졌으며, 예상대로 최종 마무리는 프론트엔드 개발 단계에서 이루어졌습니다.</p>
<p>실제 개발 기간은 약 2주 정도였던 것 같습니다. 이번 프로젝트에서의 기술 스택 선정 과정과 협업 방식을 돌아보며, 다음 프로젝트에서는 어떤 점을 보완하면 좋을지 이야기해 보겠습니다.</p>
<h2 id="🤔-기술-스택-고민과-결정">🤔 기술 스택 고민과 결정</h2>
<p>이번 프로젝트는 서울유우 직원들이 사용하는 어드민 페이지로, <strong>SEO 최적화가 필요하지 않으며</strong>, <strong>약 4주 내에 빠르게 개발해야 하는 요구사항</strong>이 있었습니다.</p>
<h3 id="✅-필수-과제-지능형-세금계산서-검증-시스템-구축">✅ 필수 과제: 지능형 세금계산서 검증 시스템 구축</h3>
<p>단순 반복적인 업무를 자동화하여 각 부서의 업무 소요 시간을 단축하고자 합니다.</p>
<p>전달받은 전자(세금)계산서 정보를 홈택스 자료와 비교하여 실제 발급된 계산서가 맞는지 검증하고, 자동으로 지급 결의서까지 작성되는 프로그램을 개발해주세요.</p>
<h3 id="🔹-요구-사항">🔹 요구 사항</h3>
<ol>
<li><p>세금계산서는 사진 촬영 후 첨부하거나 이미지 업로드 방식으로 등록할 수 있어야 한다.</p>
</li>
<li><p>업로드된 이미지는 저장 후 조회할 수 있어야 한다.</p>
</li>
<li><p>세금계산서 이미지에서 추출된 데이터를 국세청 홈택스 자료와 비교하여 진위 여부를 판단한다.</p>
</li>
<li><p>검증된 계산서 데이터를 저장하여 자동으로 지급 결의서를 작성한다.</p>
</li>
<li><p>검증된 자료는 DB에 저장되며, 사용자는 본인이 저장한 자료를 조회할 수 있어야 한다.</p>
</li>
<li><p>관리자는 모든 사용자의 검증 결과를 조회할 수 있어야 하며, 조회 조건에는 계산서의 공급자, 공급받는 자, 기간 등이 포함되어야 한다.</p>
</li>
</ol>
<p>필수 과제 내용을 바탕으로, 클라이언트 측에서는 복잡한 UI 구현이나 대규모 API 연결이 필요하지 않을 것으로 판단하였습니다. 이에 따라, TypeScript와 React를 기술 스택으로 선정하여 개발을 진행했습니다.</p>
<hr>
<h3 id="📌-typescript와-react">📌 TypeScript와 React</h3>
<table>
<thead>
<tr>
<th><strong>기술</strong></th>
<th><strong>선택 이유 및 근거</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>TypeScript</strong></td>
<td>- <strong>코드의 안정성</strong>을 높임  <br> - 코드 유지보수 개선</td>
</tr>
<tr>
<td><strong>React</strong></td>
<td>- <strong>컴포넌트 기반 구조</strong>로 UI를 효율적으로 재사용 및 유지보수 가능  <br> - <strong>React의 기본 훅 (useState, useEffect, useContext)만 활용하여 상태 관리</strong>  <br> - Redux, Zustand 등의 라이브러리를 도입하지 않아 <strong>번들 사이즈를 줄이고 성능 최적화</strong>  <br> - <strong>SEO가 필요 없는 어드민 페이지</strong>이므로 CSR(클라이언트 사이드 렌더링) 방식으로 구현</td>
</tr>
</tbody></table>
<p>또한, 번들 파일 크기를 줄이고 성능을 최적화하기 위해 불필요한 라이브러리 사용을 지양했습니다.</p>
<p>상태 관리는 React의 기본 훅을 활용하여 구현하였으며, 이는 프로젝트 규모와 요구 사항을 고려할 때 충분히 효율적인 방식이라 판단했습니다.</p>
<p>API 통신은 별도의 라이브러리를 사용하지 않고, 기본 fetch 함수를 활용하여 처리하였습니다.</p>
<h3 id="🎯-내가-느낀점">🎯 내가 느낀점</h3>
<p>React의 기본 훅을 최대한 활용하여 추가적인 라이브러리를 학습할 시간을 줄였습니다. 덕분에 시간 내에 필수 과제뿐만 아니라 선택 과제까지 구현할 수 있었고, 좋은 성과를 낼 수 있었습니다.</p>
<p>하지만 한 가지 아쉬운 점이 있었습니다. 기획 의도대로 개발을 진행하다 보니,</p>
<h4 id="실제-대화-내용-예시">실제 대화 내용 예시</h4>
<p>기획자: &quot;페이지 이동 후에도 이 파일이 그대로 유지되면 좋겠습니다.&quot;
나: &quot;그럼 useContext를 사용하여 상태를 유지해야겠네요.&quot;</p>
<p>이렇게 해서 최종적으로 프로젝트에서 useContext를 5개 정도 사용하였는데, 이로 인해 불필요한 렌더링이 발생하였습니다. 시간이 부족하여 최적화까지 신경 쓰지는 못했지만, 성능에 치명적인 영향을 주는 것은 아니었기에 그대로 진행하였습니다. 다만, 개인적으로는 다소 아쉬움이 남았습니다.</p>
<hr>
<h3 id="📌-tailwind와-shadcn-ui">📌 Tailwind와 Shadcn UI</h3>
<table>
<thead>
<tr>
<th><strong>기술</strong></th>
<th><strong>선택 이유 및 근거</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Tailwind</strong></td>
<td>- <strong>유틸리티 퍼스트 방식</strong>으로 빠르게 스타일 적용 가능  <br> - <strong>불필요한 CSS 중복 제거</strong>로 유지보수 편리  <br> - <strong>반응형 디자인 최적화</strong>로 추가적인 미디어 쿼리 작업 최소화</td>
</tr>
<tr>
<td><strong>Shadcn</strong></td>
<td>- <strong>Tailwind와 호환되는 UI 컴포넌트 제공</strong> → 빠르고 일관된 디자인 적용 가능  <br> - <strong>기본 UI 컴포넌트 활용</strong> → 버튼, 모달 등을 직접 개발할 필요 없이 즉시 사용 가능  <br> - <strong>손쉬운 커스터마이징</strong>으로 프로젝트 요구사항에 맞게 유연한 적용 가능</td>
</tr>
</tbody></table>
 <br>

<p>기획과 디자인이 완료되면 UI를 구현하고, 백엔드 개발자가 API를 완성하면 이를 연결해야 했다. 하지만 약 4주 동안 진행되는 프로젝트였기 때문에, 프론트엔드 개발자가 UI까지 직접 구현하면 개발 시간이 부족할 것이라 판단했습니다.</p>
<p>그래서 이번 프로젝트에서는 처음으로 Shadcn UI를 도입했습니다. Shadcn은 Tailwind 기반의 UI 라이브러리로, 디자인 시스템이 적용된 컴포넌트들을 제공하기 때문에 빠르게 UI를 구성할 수 있었습니다. 또한, 필요한 경우 손쉽게 커스터마이징할 수 있다는 점도 큰 장점이었습니다.</p>
<p>Tailwind CSS를 사용한 이유도 비슷했습니다. 스타일을 유틸리티 기반으로 적용할 수 있어 별도의 CSS 파일 없이 빠르게 UI를 구현할 수 있었고, 반응형 디자인도 미디어 쿼리를 직접 작성할 필요 없이 Tailwind의 클래스만으로 쉽게 적용할 수 있어 개발 속도를 높이는 데 도움이 됐습니다. 
특히, Shadcn의 모든 스타일이 Tailwind로 구성돼 있어 프로젝트 전반에서 일관된 디자인을 유지할 수 있었습니다.</p>
<h3 id="🎯-느낀-점">🎯 느낀 점</h3>
<p>이번 프로젝트에서 Shadcn과 Tailwind를 활용한 선택은 매우 효과적이었습니다. 덕분에 UI 개발 속도를 크게 단축할 수 있었고, 그 시간을 기능 개발과 API 연동에 더 집중할 수 있었습니다.</p>
<p>다만, Shadcn을 처음 사용하다 보니 라이브러리 구조를 이해하고 적용하는 데 초반에 약간의 학습 시간이 필요했다. 또한, 기본적으로 제공되는 컴포넌트 스타일이 프로젝트의 디자인 시스템과 완전히 일치하지 않는 경우, 추가적인 커스터마이징이 필요했다. 하지만 Tailwind와 함께 사용하니 스타일을 조정하는 과정도 어렵지 않았습니다.</p>
<p>결과적으로, 빠르게 진행되는 프로젝트에서는 Shadcn과 Tailwind의 조합이 매우 유용했고, 앞으로도 비슷한 환경에서는 적극 활용할 계획입니다.</p>
<hr>
<h2 id="🔗-ocr-검증-프로세스-개선">🔗 OCR 검증 프로세스 개선</h2>
<h3 id="문제-인식">문제 인식</h3>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/e6aafdcd-323d-46b8-abf8-d170fb089496/image.png" alt=""></p>
<p>세금계산서 OCR 데이터를 추출한 후, 홈택스를 통해 진위 여부를 검증하는 과정에서 몇 가지 문제가 발생했었습니다. 먼저, 백엔드에서 OCR 데이터를 추출한 후 프론트엔드로 전달되지 않아, 이후의 검증 절차를 진행할 수 없었습니다. 이는 백엔드에서 OCR 데이터를 응답 값에 포함하지 않는 구조였기 때문이었습니다.</p>
<p>또한, OCR을 통해 추출된 데이터의 각 속성이 명확히 문서화되지 않아, 어떤 항목이 어떤 의미를 가지는지 파악하기 어려웠습니다. 이로 인해 프론트엔드에서 데이터를 처리하는 과정에서도 혼선이 발생했습니다.</p>
<h3 id="개선-방법">개선 방법</h3>
<p>우선, 백엔드 개발자에게 OCR 데이터를 프론트엔드로 전달해 줄 것을 요청하여, 프론트엔드에서 검증 프로세스를 원활하게 진행할 수 있도록 했습니다. 이를 통해 검증 절차로 넘어가지 못하는 문제를 해결했습니다.</p>
<p>또한, OCR 응답 데이터의 각 속성들이 어떤 의미를 가지는지 백엔드 개발자와 협의하여 하나하나 명확하게 정의하고,이를 기반으로 프론트엔드에서 데이터를 처리하는 로직을 개선하여, 불필요한 혼선을 방지할 수 있었습니다.</p>
<p>마지막으로, 원래 백엔드에서 처리할 예정이었던 데이터 필터링 로직을 프론트엔드에서 대신 구현했습니다. OCR 데이터 중에서도 성공적으로 추출된 데이터만 검증 요청을 보낼 수 있도록 하여, 불필요한 요청을 줄이고 전체 검증 프로세스의 속도를 최적화했습니다. 이를 통해 검사 시간을 줄이는 동시에 시스템의 효율성을 높일 수 있었습니다.</p>
<h3 id="🎯-느낀-점-1">🎯 느낀 점</h3>
<p>메인 기능을 개발하면서, 하나의 문제를 해결하면 또 다른 문제가 발생하는 일이 반복되었습니다. 마치 개복치처럼 한 가지를 고치면 새로운 오류가 터지는 상황이 계속되었고, 그만큼 많은 시간을 투자해야 했습니다.</p>
<p>이번 경험을 통해, 다음번에는 테스트 코드를 먼저 작성하여 보다 안정적으로 동작하도록 개선해야겠다는 생각을 하였습니다. 또한, 예외 처리를 뒤늦게 추가하다 보니 일부 오류를 빠르게 대응하지 못한 점도 아쉬움으로 남았습니다. 만약 백엔드와 더 긴밀하게 협의하여 오류 코드에 대한 소통을 원활히 했다면, 보다 깊이 있는 예외 처리를 빠르게 적용할 수 있었을 것이라 생각합니다.</p>
<hr>
<h2 id="ut-진행">UT 진행</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/7ae7c324-b470-4fc1-86f8-0627283547af/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/14448d70-7678-464e-ad16-58cf82132eb4/image.png" alt=""></p>
<p>이번 UT(Usability Test) 세션에서는 다른 팀들이 우리 프로토타입을 테스트하고 피드백을 주는 시간이었습니다. 여러 팀의 다양한 시각과 관점에서 우리의 프로덕트를 점검할 수 있었던 기회였죠. 각 팀은 우리의 프로토타입을 실제 사용자처럼 다루며, 사용성에 대한 인사이트를 제공해주었습니다.</p>
<h3 id="과정">과정</h3>
<p>UT 세션은 주로 두 가지 주요 단계로 진행되었습니다. 첫 번째는 다른 팀들이 우리 프로토타입을 실제로 사용해보며, 그 사용성에 대한 피드백을 제공하는 시간이었고, 두 번째는 각 팀의 피드백을 바탕으로 개선점을 도출해내는 시간이었습니다. 팀원들은 프로토타입을 테스트하며 어려운 부분이나 직관적이지 않은 점들에 대해 의견을 나누었고, 그 의견을 기획, 디자인, 개발팀과 공유하여 프로젝트의 완성도를 높일 수 있었습니다.</p>
<h3 id="🎯-느낀점">🎯 느낀점</h3>
<p>다른 팀들의 피드백을 듣는 과정에서 내가 미처 생각하지 못한 부분들을 발견할 수 있었고, 사용자 관점에서의 중요한 인사이트를 얻을 수 있었습니다. 특히, 테스트를 통해 사용자가 실제로 느끼는 불편함을 직접 들을 수 있었기 때문에, 그 피드백을 바탕으로 우리 프로덕트를 더 직관적이고 사용하기 편리한 방향으로 개선할 수 있는 기회를 얻었습니다. 이 경험을 통해, 앞으로의 프로젝트에서는 피드백을 적극적으로 반영하고 더 나은 프로덕트를 만드는 데 중점을 두어야겠다는 생각을 하게 되었습니다.</p>
<h3 id="🔗-ut-리뷰-링크">🔗 UT 리뷰 링크</h3>
<p><a href="https://handy-hope-df5.notion.site/UT-1b0c15ac394e802b8f50df36cc86385f?pvs=4">https://handy-hope-df5.notion.site/UT-1b0c15ac394e802b8f50df36cc86385f?pvs=4</a></p>
<hr>
<h2 id="✅-qa-진행">✅ QA 진행</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/46a27485-34c9-4d8c-86a0-b353b9123eb3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/2ec09821-e049-45ba-95a8-265a27394611/image.png" alt=""></p>
<p>기획자, 디자이너, 백엔드, 프론트 개발자 모두가 모여 QA를 진행했습니다. 이 과정에서는 테스트 계획서를 먼저 작성하여, 각 기능별 테스트 항목과 예상 결과를 명확하게 정의한 후 진행하였습니다. 테스트 계획서에 맞춰 테스트를 진행하며 각 파트의 담당자들이 해당 영역에서 발생할 수 있는 문제를 미리 파악하고 해결 방안을 모색할 수 있었습니다.</p>
<p>특히, 테스트 계획서를 통해 기능 테스트뿐만 아니라 UI/UX 테스트, API 통합 테스트, 예외 처리 검증 등 모든 측면을 꼼꼼히 점검할 수 있었고, 이는 프로젝트의 전반적인 품질을 높이는 데 큰 도움이 되었습니다. 각 팀은 각자의 영역에서 발생할 수 있는 문제를 사전에 예측하고, 이를 미리 해결하려는 노력 덕분에 QA 과정이 원활하게 진행되었습니다.</p>
<h3 id="🎯-느낀-점-2">🎯 느낀 점</h3>
<p>QA 진행을 통해 협업의 중요성을 다시 한 번 느꼈습니다. 기획자, 디자이너, 백엔드, 프론트 개발자 모두가 함께 모여 테스트를 진행하면서, 각자의 관점에서 발생할 수 있는 문제를 공유하고 개선할 수 있었습니다. 특히 테스트 계획서를 작성한 덕분에 테스트가 체계적이고 효율적으로 진행되었으며, QA 후 발생한 버그들도 빠르게 수정할 수 있었습니다.</p>
<p>다만, QA 과정에서 더 개선할 점은 테스트 케이스를 작성할 때 좀 더 다양한 예외 상황을 고려하지 못한 부분이 있었습니다. 예를 들어, 극단적인 데이터나 예상치 못한 사용자의 행동에 대한 테스트가 부족해 일부 문제가 발견되었습니다. 다음에는 보다 더 꼼꼼하게 다양한 시나리오를 테스트할 수 있도록 준비하는 것이 중요하다는 교훈을 얻었습니다.</p>
<h1 id="🎉-성과">🎉 성과</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/462905aa-1ebf-4376-8633-948912e6c828/image.png" alt=""></p>
<ul>
<li><h4 id="업무-효율성-98-증가">업무 효율성 98% 증가</h4>
<h4 id="기존-50개-문서-검증-시간-250분-→-개선-후-5분">기존 50개 문서 검증 시간: 250분 → 개선 후: 5분</h4>
<p>UT 결과: 반려 46건, 승인 1건, 검증 실패 3건으로 평균 업무 시간이 5분으로 감소하여 효율성 98% 향상</p>
</li>
<li><h4 id="에러율-감소로-지급결의서-수정-프로세스-축소">에러율 감소로 지급결의서 수정 프로세스 축소</h4>
<h4 id="기존-에러율-067-→-개선-후-0">기존 에러율: 0.67% → 개선 후: 0%</h4>
<p>검증 정확도 테스트 6회 진행 결과, 입력값과 출력값이 모두 동일하게 측정되어 에러율 0% 달성</p>
</li>
<li><h4 id="사내-업무-생산성-향상">사내 업무 생산성 향상</h4>
<p>권한별로 효율적인 업무 관리 가능</p>
<p>일반 사용자: 내 업무 관리, 관리자: 업무 관리 및 사용자 권한 관리</p>
</li>
</ul>
<h1 id="to-돈까스클럽-🍽️">To. 돈까스클럽 🍽️</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/1ae68fec-a8d9-46aa-915d-ed91827ec990/image.jpeg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/44b52332-d7e9-4873-9432-6f938a76ea9f/image.jpeg" alt=""></p>
<p>우리가 함께한 시간은 정말 소중하고, 그만큼 즐거운 시간이었어. 훌륭하고 재능 있는 너희들과 함께 코딩을 하며 배운 점들이 많았고, 무엇보다 함께 이룬 성과가 너무나 값지다고 느껴. 비록 이제는 각자의 길을 가게 되었지만, 내 마음속에서 &#39;돈클&#39;은 언제나 함께할 거야. 우리의 추억과 성과는 영원히 기억될 거고, 그 소중한 시간들을 잊지 않을 거야.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회고 2024]]></title>
            <link>https://velog.io/@yunbh_0401/%ED%9A%8C%EA%B3%A0-2024</link>
            <guid>https://velog.io/@yunbh_0401/%ED%9A%8C%EA%B3%A0-2024</guid>
            <pubDate>Tue, 31 Dec 2024 14:28:10 GMT</pubDate>
            <description><![CDATA[<h1 id="2024">2024</h1>
<p>이제 곧 있으면 2024년이 끝난다. 정말 살면서 가장 이슈가 많았던 해였다.
전부 이야기할 수는 없지만 조금이나마 있었던 일을 이야기 하며 회고를 진행해보겠다.</p>
<h1 id="🗓️-12월--6월">🗓️ 12월 ~ 6월</h1>
<h2 id="우물안의-개구리">우물안의 개구리</h2>
<p>12월 처음으로 프론트엔드 개발자로 회사 면접을 진행하게 되었었다. 그때 면접장 분위기, 습도, 날씨 등등 아직도 기억이 생생하다. 
왜냐하면 나는 학교 생활을 하며 나 자신이 개발자로 일을 잘할 거라고 한 치 의심도 없었는데, 면접 이후 그 생각은 산산조각 나버렸다.
면접을 국밥 먹는 거보다 더 시원하게 말아먹었다. 면접 때 정말 프론트엔드 개발자가 가져야 할 기본 지식과 소프트 스킬 위주로 질문을 주셨는데, 진짜 80% 이상은 모르거나 이상하게 대답하였다. 
어느 정도였냐면, 면접관이 &quot;오늘 면접 처음 보신 거예요?&quot;라고 물어볼 정도였으니 더 이상은 설명하지 않겠다...</p>
<h2 id="2024-최고의-선택">2024 최고의 선택</h2>
<p>아무튼 그때부터 위기감을 정말 많이 느꼈던 것 같다. 학교도 졸업해서 앞으로 취업을 나 혼자만의 힘으로 해야 한다는 사실이 내 멘탈을 정말 많이 흔들었다.
그래서 어떻게 해야 할까 생각하다가 친구가 &quot;부트캠프 들어가서 공부해보는 거 어때?&quot; 하며 하나의 링크를 던져주었다. 
커리큘럼을 보면서 기초부터 자세히 알려줄거 같아 다시 초심을 찾아보자라는 마인드로 6개월 동안 진행되는 코드잇 부트캠프에 합류하게 되었다.</p>
<h2 id="잊지-못-할-경험">잊지 못 할 경험</h2>
<p>부트캠프를 진행하며 정말 후회한적이 없었던거 같다. 다양한 사람들과 대화하고, 같이 프로젝트를 진행하고, 자신들의 생각을 펼치며 토론하는 등 현업에서 일하는거 같은 경험을 했었다.
부트캠프를 오기전까지만 해도 내 자존감은 바닥을 기어다녔는데 부트캠프 덕분에 많이 회복되었다. 그렇게 좋은 경험을 하며 6월에 부트캠프는 막을 내렸다. </p>
<hr>
<h1 id="🗓️-6월--12월">🗓️ 6월 ~ 12월</h1>
<h2 id="새로운-출발">새로운 출발</h2>
<p>부트캠프 수료 후 나는 오직 취업만을 목표로 삼았었다. 취업을 위해 이력서와 포트폴리오를 수십번 작성하며 여러 회사에 지원을 하였다.
결과는 처참했다. 서류 탈락 이메일이 끊이질 않았다. 주위에서는 경기가 안 좋다... 부트캠프 출신은 안 뽑는다.. 등 안 좋은 이야기만 들리기 시작했고, 결과 또한 그 말을 뒷받침 해주듯 안 좋은 결과만이 계속 나에게 돌아왔다.</p>
<h2 id="한줄기-빛">한줄기 빛</h2>
<p>서류 탈락 이메일 공격을 계속 받는 상황에서 햇빛이 뜨겁게 비추는 날에 서류 합격 이메일이 최초로 날라왔다.
정말 기분이 좋아서 그때 이미 회사 다니는 상상을 할 정도였다.</p>
<p>아무튼 처음으로 서류가 붙은 회사는 디xx였는데 다음 채용과정이 라이브코딩이였다. 라이브코딩? 코딩테스트는 많이 들어봤어도 라이브코딩은 과정은 정말 처음 들었었다.
그래서 어떤식으로 준비해야할지 몰라 이곳 저곳 물어보며 준비했던 기억이 있다.
결과는 아쉽게 실패... 중간에 좀 절었는데 아마 그거 때문에 떨어진거 같다. 그게 아니였으면 과연 입사했을까?
실망도 잠시 라이브코딩이 끝나고 이메일을 확인해보니 하나의 서류가 또 합격을 받았다.</p>
<p>이번에는 면접보러 가야해서 바쁘게 면접 준비를 했었다. 면접을 가 면접관님과 이야기를 하는 과정에서 난 또 실망을 할 수밖에 없었다. 왜냐하면 내가 생각한 직무와 전혀 다른 일을 해야했기 때문이다.(거의 노가다하러 갔어야함)
그래서 집으로 돌아와 다시 이력서를 고치며 다른 회사에 지원을 하였다....</p>
<h2 id="인턴-레츠고">인턴 레츠고</h2>
<p>9월 서류와 면접을 합격하며 나에게 첫 인턴경험이 생겼다. 내 첫 회사는 SI 개발 회사였다. 주변에서 SI가면 뒤지게 힘들다, 갈 곳이 아니다, 가면 배울게 없다, 라는 말이 정말 많았다. 
말이 무섭게 내가 업무를 받아서 처리하기전에 기존에 개발되어있던 코드들을 보면서 적응하는 시간이 있었다. 그 시간은 정말 충격적이였다. 이 코드가 어떻게 돌아가지? 라는 생각이 들 정도로 스파게티 코드들이 정말 많았고, 거의 다 하드코딩되어있었다.
그래도 슈퍼 신입답게 내가 이걸 어떻게하면 모두가 보기 쉽게 코드를 리팩토링할 수 있을지 열심히 고민했던거 같다. 하지만 이제 막 들어온 인턴에게 그럴 기회는 없었다...
업무를 진행하며 정말 실수를 많이 했었는데 이 실수들이 내 성격과 버릇을 다 알려주는거 같아 많이 쪽팔렸다. 이래서 개발자 잘도 하겠다라는 생각까지하며 내 잘못된 버릇과 성격을 고치기위해 노력을 많이 했던 시간이였다.</p>
<h2 id="기대를-한-죄">기대를 한 죄</h2>
<p>계약기간 1주일이 남은 시점 이제 슬슬 정규직 전환 이야기가 나올 타이밍인데 나오지않고 있었다. 이때만해도 정규직이 될거란 생각에 편안하게 평소처럼 업무를 보고 있었다.
그러던 순간 팀장님이 나를 호출하여 회의실로 데려가셨다. 기다리던 이야기를 하는데 &quot;원래 병현님을 000 프로젝트 계약이 되면 그 프로젝트에 투입시켜서 진행하려고 했는데 프로젝트가 계속 계약이 연기되면서 아직도 언제 계약이 될지 모른다&quot;라는 말과 함께 1~2개월 더 계약직으로 일 할거냐 아니면 퇴사하시겠냐고 물어보셨다.</p>
<p>나는 결국 퇴사를 선택하였다</p>
<p>이유는 더 좋은 회사를 가고 싶다는 마음과 뭔가 취준할 때 최선을 다하지 않았다라는 느낌이 들었고, 인턴 생활을 하며 내가 아직 많이 부족하다는 생각을 정말 많이 했었다
그래서 다시 취업 시장에 뛰어들어 나의 부족함을 보안하고, 목표을 이루겠다는 마음을 먹었다.</p>
<hr>
<h1 id="2025">2025</h1>
<p>2024년에 다양한 개발 행사를 참여하고, 컨퍼런스 영상도 많이 찾아보고, 회사에서 일도 하며 느낀건 하나였다. 기본기만 잘 되어있어도 신입 개발자로써 더 가질게 없다라는 생각이 들었다.
그리고 내가 비록 프론트엔드 개발자로 취업을 할거지만 프론트엔드 지식뿐만 아니라 다양한 분야에 지식도 어느정도는 가지고 있어야겠다라는 생각이 들었다.
그래서 2025 목표는 이렇게 세워봤다.</p>
<h2 id="1-react-내부-동작-원리-구현-및-글-작성">1. React 내부 동작 원리 구현 및 글 작성</h2>
<p>이거 하나만 제대로 공부해서 내껄로 만들 수 있다면 프론트엔드 개발자로 또 한걸음 성장할 수 있게다라는 걸 많이 느끼는 해였다. 그래서 관련된 스터디로 운영된다고 하여 들어가서 확실하게 해볼 생각이다.</p>
<h2 id="2-코딩테스트">2. 코딩테스트</h2>
<p>하 이거는 뭐 말이 필요없다. 좋은 곳 가려면 무조건 해야한다. 이거 못해서 떨어진 좋은 회사들이 꽤 있다... 이건 시간을 내서라도 꾸준히 할 생각이다.</p>
<h2 id="3-ai를-곁들인-서비스-기획-및-제작">3. AI를 곁들인 서비스 기획 및 제작</h2>
<p>요즘 서비스에 AI가 없는 서비스를 찾기가 힘들다. 그래서 회사들도 AI가 결합된 서비스를 개발할 수 있는 사람들 많이 뽑고 있는 추세이다. 그리고 요즘은 또 쉽게 AI 모델을 학습시키고 API까지 만들 수 있는 프로그램이나 서비스들이 많아져서 크게 어렵지 않다. 이걸 이용해서 새로운 서비스를 만들 생각이다.
이미 기획과 디자인은 어느정도한 상태인데 아직 갈 길이 멀다....</p>
<h2 id="4-학사-학위-취득-및-정보처리기사-취득">4. 학사 학위 취득 및 정보처리기사 취득</h2>
<p>이번에 취업준비를 하면서 대기업 채용도 꽤 있었지만 난 하나도 넣지 못 했다. 전문 학사 학위로는 자격이 안되어 지원이 불가능했다. 그래서 이번에 동양미래대 전공심화를 지원하여 야간에 학교를 다닐 생각이다.
인공지능소프트웨어학과를 넣어서 가면 AI와 웹을 융합하여 어떻게 개발하면 되는지 배울 수 있어 매우 기대가 된다. 이 기술을 바탕으로 3번에서 말한 프로젝트를 잘 만들어볼 생각이다.
학교 다니면서 정처기도 공부하여 꼭 2025년에 따보도록 하겠다.</p>
<hr>
<h1 id="마무리">마무리</h1>
<p>할 말은 많지만 글 쏨씨가 부족하여 많이 적지는 못했는데 이렇게 과거를 떠올리며 글로 정리하다보니 정말 2024년 힘들긴 했지만 좋은 경험들을 많이한거 같다.
2025년에는 지금 세운 목표들이 전부 성공할 수 있도록 많은 응원과 관심 부탁드립니다. 물론 저도 많이 노력할겁니다 ㅎ</p>
<p>긴 글 읽어주셔서 감사드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[EC2 HTTPS 적용 및 로드밸런서 설정]]></title>
            <link>https://velog.io/@yunbh_0401/EC2-https-%EB%B0%8F-%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%84%9C-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@yunbh_0401/EC2-https-%EB%B0%8F-%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%84%9C-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Tue, 31 Dec 2024 06:06:35 GMT</pubDate>
            <description><![CDATA[<p>지난 글에서 AWS 서비스를 이용하여 Nest 프로젝트를 EC2에 배포하는 과정을 정리해보았습니다.
이번에는 EC2에 있는 Nest와 통신을 하여 데이터를 주고 받아야하기 때문에 https 적용 및 로드밸런서 설정을 같이 해보려고 합니다.</p>
<h1 id="💳-도메인-구매">💳 도메인 구매</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/527fafb5-4cb8-4c54-943f-85b8d4dae8ab/image.png" alt=""></p>
<p>우선 도메인을 적용시키기 위해 도메인을 구매해야 합니다. 
가비아가 첫 구매시 도메인 가격이 싸기 때문에 저는 가비아에서 구매했습니다.</p>
<h1 id="route-53-도메인-적용">Route 53 도메인 적용</h1>
<p>이제는 위에서 구입한 도메인을 AWS Route 53에 적용해야합니다.
저희가 Route 53에서 도메인을 구매한게 아니라, 이 과정에서 가비아의 도메인과 Route 53을 서로 연동시키는 추가적인 과정이 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/b8b6c09e-14bd-44ef-bbb3-4b4b80d9cb9b/image.png" alt=""></p>
<ol>
<li>우선 AWS Route 53의 호스팅 영역에 들어가서 &#39;호스팅 영역 생성&#39;을 누릅니다.</li>
<li>구매한 도메인 입력</li>
<li>퍼블릭 호스트 영역 선택</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/141f858b-a05d-4f1f-8396-74be6305eeab/image.png" alt=""></p>
<p>이렇게 호스팅 영역을 생성하고, 호스팅의 세부정보 레코드를 보면 다음과 같이 레코드들이 있습니다.
(현재 A와 CNAME은 나중에 추가 예정이고, NS와 SOA 2개만 만들어짐)</p>
<p>여기서 각 레코드들의 유형을 보면 A,NS,SOA,CNAME가 있는데 각 레코드들의 개념은 다음과같습니다.</p>
<p><strong>A 레코드</strong>
-저희가 호스팅한 도메인 주소와 서버의 IP주소를 매핑시키는 레코드입니다.
즉 해당 레코드를 만듦으로써, 도메인 주소 클릭시 서버의 IP주소로 바꿔서 서버에 들어올 수 있게 하는 레코드입니다.</p>
<p><strong>NS 레코드 (Name Server)</strong>
-네임 서버 레코드로, 해당 도메인에 대한 네임서버 의 권한을 누가 관리하고 있는지 알려주는 레코드입니다.
즉, 해당 레코드를 클릭했을때, DNS 처리과정에서 해당 도메인 DNS 쿼리를 처리할 서버가 지정되어있는 레코드입니다.
이때, DNS 시스템의 안정성과 고가용성을 위해 1개가 아닌 4개의 값이 존재합니다.</p>
<p><strong>SOA 레코드 (Start of Authority)</strong>
-NS레코드의 네임서버가 해당 도메인에 관하여 핵심 정보를 가지고 있음을 증명하는 레코드입니다.
즉, SOA 레코드의 값은 NS 레코드 값중 하나이며, 도메인 이름, 도메인 관리아 메일, 도메인 일련번호 등의 필수적인 정보를 가지고 있습니다.</p>
<p><strong>CNAME 레코드 (Canonical Name Record)</strong>
-도메인 별명 레코드며, 다른 도메인 주소를 A 레코드의 도메인 주소로 이중매핑 시키는 레코드입니다.
즉, 고정IP가 아닌 유동IP인 상황에서 CNAME 레코드를 활용하여 도메인과 IP 매핑 수정을 최소화 할 수 있는 장점을 가진 레코드입니다.</p>
<p>이제 여기에 있는 NS 레코드를 가비아의 도메인에 적용시킬 것입니다. NS레코드에 존재하는 4개의 값들을 가비아의 도메인 정보에 입력하면됩니다.
가비아 페이지의 &#39;내 서비스 관리 -&gt; 도메인 통합 관리툴 -&gt; 도메인 정보 변경 -&gt; 내도메인 클릭 -&gt; 네임서버 설정&#39;에 들어가서 방금 4개의 값들을 차례대로 1차~4차까지 등록을 하면됩니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/7d758d85-789a-4289-a698-a5c404c78aaa/image.png" alt=""></p>
<h1 id="certificate-manager-인증서-발급-및-도메인-적용">Certificate Manager 인증서 발급 및 도메인 적용</h1>
<p>이제 HTTPS를 적용하기 위해 Certification Manager를 통해 SSL/TLS 인증서를 발급해야 됩니다.</p>
<blockquote>
<p>💡 SSL/TLS 인증서는 웹 서버와 클라이언트 간 통신을 암호화하여 데이터를 안전하게 보호하고, 서버의 신원을 인증하는 디지털 인증서입니다. 이를 통해 데이터 도청과 변조를 방지하며, HTTPS를 활성화해 사용자에게 신뢰를 제공합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/c5c9050e-7604-422c-9915-37558e135596/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/48711447-a97a-4af5-85ba-998a9501d2c8/image.png" alt=""></p>
<ol>
<li>퍼블릭 인증서 요청을 선택</li>
<li>도메인이름은 전에 사두었던 도메인의 이름으로 설정</li>
<li>검증 방법 및 키 알고리즘은 기본 권장값으로 설정</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/9aca8772-263e-4a2f-8028-e177f948232b/image.png" alt=""></p>
<p>이렇게 설정 완료를 안후, 인증서 리스트를 확인하면 다음과 같은 창이 나올겁니다.
지금 저는 전에 만들어둔 인증서기 떄문에 &#39;발급됨&#39;이 뜨는데 발급까지 시간이 좀 소요가 됩니다.</p>
<p>이제 인증서 ID를 클릭하고 Route 53에 레코드 생성을 하면됩니다.
그러면 아까 위에 CNAME 레코드가 만들어질겁니다.</p>
<hr>
<h1 id="로드밸런서-설정">로드밸런서 설정</h1>
<p>한 대의 EC2만 사용할 예정이어도, https를 적용하려면 로드밸런서가 필요합니다.</p>
<blockquote>
<p>ELB(Elastic Load Balancer)란 애플리케이션 트래픽을 여러 대상에 자동으로 분산시켜 안정적인 AWS서버 환경을 운용하는데에 도움을 주는 서비스다. EC2뿐만 아니라 컨테이너(ECS), AWS Lambda 등으로 다양한 서비스와 연계하여 부하를 분배할수 있다.</p>
</blockquote>
<h2 id="1-타겟-그룹-설정">1. 타겟 그룹 설정</h2>
<p>로드밸런서 설정전 타겟 그룹을 설정을 먼저 해줘야합니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/39635602-7f4f-47c8-ba29-563225ded117/image.png" alt=""></p>
<ol>
<li>대상유형을 인스턴스로 선택</li>
<li>그룸 이름 작성</li>
<li>포트는 80번으로 선택</li>
<li>vpc 선택(EC2와 똑같은 vpc로 선택해야함)</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/ab3f3b24-661a-4503-92e4-e9fec76182ee/image.png" alt="">
Health check는 타겟 그룹의 EC2 인스턴스가 건강한지 확인하는 방법을 말합니다.</p>
<p>Unhealthy하다면, 로드 밸런서가 해당 타겟 그룹으로 요청(부하)을 보내지 않습니다.</p>
<p>디폴트로 하고 다음 페이지로 넘어가 사용할 인스턴스를 체크하고, 포트 번호를 확인하고 추가해줍니다.
<img src="https://velog.velcdn.com/images/yunbh_0401/post/c0a9fb76-9cdb-49e3-a6d8-147a8426603d/image.png" alt=""></p>
<hr>
<h2 id="2-로드밸런서-설정">2. 로드밸런서 설정</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/a7a9fdd9-2dec-4a5c-b5ab-574fb63959cd/image.png" alt="">
저희는 단순하게 HTTPS를 연결할거니깐 맨 왼쪽에 있는 걸로 선택하여 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/c83e023c-e186-430e-a07c-e787587f213d/image.png" alt=""></p>
<ol>
<li>로드밸런서 이름 이력
나머지는 안 건드려도 됩니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/7a7b434b-4f1b-45cb-b795-4d2666b4a774/image.png" alt="">
2. VPC를 연결합니다
근데 여기서 VPC는 EC2와 연결된 것과 똑같은 것으로 연결해줘야합니다. 그리고 서브넷은 퍼블릭 서브넷으로 선택해줘야합니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/8186c3b2-cca5-4f43-9587-fed9dc56ddb0/image.png" alt="">
3. 필요한 보안그룹을 연결해줍니다</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/d8a4768c-062b-4fee-9c6d-af355def6aca/image.png" alt="">
4. HTTP 80, HTTPS 443 에 대한 리스너를 생성합니다.
5. Forward to를 위에서 생성한 타겟 그룹으로 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/19bdfbe3-534b-477b-a4d7-2884ef032345/image.png" alt="">
6. 아까 만들어둔 인증서 선택</p>
<hr>
<h1 id="도메인-레코드-생성">도메인 레코드 생성</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/141f858b-a05d-4f1f-8396-74be6305eeab/image.png" alt="">
아까전에 봤던 사진에서 유형 A 레코드를 추가해줄겁니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/4ac6e829-70f9-4d2d-8ac7-61d112400767/image.png" alt=""></p>
<p>레코드 이름(서브 도메인)은 사용해도 되고, 안해도 됩니다. 저는 안하고 넘어가겠습니다.
트래픽 라우팅 대상을 위 그림처럼 설정해서, 위에서 생성한 LB를 지정해주어야 합니다.
그리고 레코드를 생성합니다.</p>
<p>자, 이제 호스팅 영역의 레코드 4개가 다 생성되었습니다!</p>
<hr>
<h1 id="로드-밸런서-리스너-규칙-추가">로드 밸런서 리스너 규칙 추가</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/90bdff1a-149f-48ac-8e41-afddd72948d0/image.png" alt="">
만약 HTTP로 접근을 하게되면 HTTPS로 리다이렉트 되게 규칙을 변경해줍니다.</p>
<hr>
<h1 id="nginx-역방향-프록시-설정">nginx 역방향 프록시 설정</h1>
<p>80번 포트에서 들어오는 요청을 Docker 컨테이너의 3001번 포트로 전달하기 위해서는 nginx를 이용하여 역방향 프록시를 설정해줘야합니다.
nginx를 EC2에 접속하여 설치부터 해주겠습니다.</p>
<ol>
<li><strong>nginx 설정 파일 수정</strong></li>
</ol>
<p>nginx 설정 파일을 수정하여, 80번 포트에서 들어오는 요청을 Docker 컨테이너의 3001번 포트로 전달하도록 설정합니다</p>
<pre><code class="language-bash">sudo nano /etc/nginx/sites-available/default</code></pre>
<pre><code>server {
    listen 80;

    server_name lily-server.shop;  # 도메인 이름

    location / {
        proxy_pass http://localhost:3001;  # Docker 컨테이너의 3001번 포트로 요청 전달
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
</code></pre><ol start="2">
<li>재시작</li>
</ol>
<pre><code class="language-bash">sudo systemctl restart nginx</code></pre>
<hr>
<h1 id="🎉-적용-완료">🎉 적용 완료</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/5545a7bf-8af9-4016-b110-36fd3828e18c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/ba859daf-c83e-465d-bb56-63f0afc711ab/image.png" alt=""></p>
<p>힘겹게 배포는 성공했지만 배포를 하는 과정에서 AWS 비용이 꽤나 나올 거 같고 프리티어도 곧 끝나서 이 방법 보다는 Vercel로 배포를 다시하려고 한다.
서버리스로 배포를 하면 내가 서버쪽은 신경 안 쓰고 개발에만 집중할 수 있을 거 같고, 초기에는 사용자가 거의 없을거라 비용적으로도 이게 더 적합할 거 같아 지금까지 만든건 없애고 다시 배포를 하려고 합니다.</p>
<p>그래도 이런 과정에서 AWS 서비스들을 한 번씩 써보는 좋은 경험이 되어 나중에 다시 배포하게 되면 좀 더 쉽게 할 수 있을 거 같다</p>
<p>참고
<a href="https://woojin.tistory.com/94">EC2 HTTPS로 연결하기 (2) - 로드밸런서로 리다이렉트 설정하고 Health check 통과하기</a>
<a href="https://inpa.tistory.com/entry/AWS-%F0%9F%93%9A-ELB-Elastic-Load-Balancer-%EA%B0%9C%EB%85%90-%EC%9B%90%EB%A6%AC-%EA%B5%AC%EC%B6%95-%EC%84%B8%ED%8C%85-CLB-ALB-NLB-GLB">ELB(Elastic Load Balancer) 구성 &amp; 사용법 가이드</a>
<a href="https://www.youtube.com/watch?v=dCZKSMO_ebg&amp;t=2155s">vpc 생성 참고</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS를 이용해서 Nest 배포 (Docker를 곁들인)]]></title>
            <link>https://velog.io/@yunbh_0401/AWS%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-Nest-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@yunbh_0401/AWS%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-Nest-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Tue, 24 Dec 2024 11:20:46 GMT</pubDate>
            <description><![CDATA[<p>예전부터 API 서버는 어떻게 만드는건지 궁금하여 이번에 Lily라는 프로젝트를 진행하면서 만들어보면 좋을 거 같아 API 서버를 만들어보려고 합니다.</p>
<h1 id="🛠️-언어-및-프레임워크-선택">🛠️ 언어 및 프레임워크 선택</h1>
<p>익숙한 TypeScript와 이를 효과적으로 활용할 수 있는 Nest.js 프레임워크를 선택하여 프로젝트를 진행하려 합니다. 특히, Nest.js는 ORM을 통해 데이터베이스와의 상호작용을 지원하며, 제가 이전에 Prisma를 활용해 API를 개발했던 경험이 있어, 이 방법을 선택하면 보다 효율적으로 개발을 진행할 수 있을 것이라 판단했습니다.</p>
<h1 id="🚀-배포">🚀 배포</h1>
<p>작업을 무작정 진행하기보다는, 먼저 애플리케이션을 배포 환경에 설정해두고 시작하려고 합니다. 이렇게 하면 배포 과정에서 발생할 수 있는 문제들을 미리 인지하고, 이를 빠르게 해결할 수 있을 것 같아 배포를 한 후 작업을 하려고 합니다.</p>
<h2 id="🧩-배포-아키텍쳐">🧩 배포 아키텍쳐</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/ce56dd68-49d1-4709-881e-746505aa31b0/image.png" alt=""></p>
<p>앞으로 할 작업들의 대한 아키텍쳐이다. 순서를 간단하게 설명드리면</p>
<ol>
<li>먼저 Nest.js 프로젝트를 GitHub와 연동하여 코드 변경 사항을 GitHub에 푸시할 수 있도록 설정합니다.</li>
<li>코드가 푸시되면, GitHub Actions를 통해 다음 작업이 자동으로 이루어집니다:<ul>
<li>Docker 이미지를 생성하고, 배포에 필요한 스크립트 파일들을 압축하여 S3 버킷에 업로드합니다.</li>
<li>생성된 Docker 이미지를 AWS의 컨테이너 이미지 저장소인 ECR(Amazon Elastic Container Registry)에 전달합니다.</li>
</ul>
</li>
<li>이후, AWS CodeDeploy를 통해 배포 명령을 실행합니다.<ul>
<li>CodeDeploy는 EC2 인스턴스에서 Docker 이미지와 S3에 저장된 스크립트 파일들을 각각 받아와 최종적으로 애플리케이션을 배포합니다.</li>
</ul>
</li>
</ol>
<p>겉으로 보기엔 간단해 보이는 작업일 수 있지만, 네트워크 지식이 부족하고 AWS를 활용한 배포는 처음이라 개인적으로는 쉽지 않은 도전이었습니다. 이번 기회를 통해 네트워크 지식을 쌓고자 했고, 끝까지 포기하지 않고 배포 과정을 완수하기 위해 노력했습니다.(결국 2주만에 성공)</p>
<p>제가 이번에 배포하면서 알게된 내용을 조금이나마 공유하고자 글을 써보는 것이니 혹시라도 틀린 부분이나 개선점이 있으면 댓글 부탁드리겠습니다. 🙏</p>
<h2 id="🐳-docker-image-생성을-위한-docker-작성">🐳 Docker Image 생성을 위한 Docker 작성</h2>
<pre><code class="language-Docker"># Node.js 기반 이미지 사용 (Node.js 18 버전)
FROM node:18

# 컨테이너 내부의 작업 디렉터리를 /app으로 설정
WORKDIR /app

# package.json과 package-lock.json 파일을 컨테이너로 복사
COPY package*.json ./

# 프로젝트 의존성 패키지 설치
RUN npm install

# NestJS CLI 전역 설치
RUN npm install -g @nestjs/cli

# 현재 디렉터리의 모든 소스 파일을 컨테이너로 복사
COPY . .

# 환경 설정 파일 복사
COPY .env .env

# Prisma 데이터베이스 마이그레이션 실행
RUN npx prisma migrate deploy

# 프로덕션용 빌드 실행
RUN npm run build

# 애플리케이션 실행 명령어 설정 (프로덕션 모드)
CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start:prod&quot;]
</code></pre>
<p>컨테이너를 만들기 위해서는 Docker 이미지가 필요합니다. 이 이미지는 애플리케이션과 그 의존성들이 미리 설정된 템플릿으로, 이를 기반으로 실행 가능한 컨테이너가 생성됩니다. 따라서, &quot;이 코드는 도커 이미지를 만들기 위한 코드&quot;라고 생각하면 쉽게 이해할 수 있습니다.</p>
<h3 id="docker란">Docker란?</h3>
<blockquote>
<p>Docker는 애플리케이션과 그에 필요한 모든 의존성, 라이브러리, 설정 파일 등을 하나의 컨테이너라는 단위로 패키징하여 어디서나 일관되게 실행할 수 있게 해주는 플랫폼입니다. 이를 통해 개발, 테스트, 배포 환경에 관계없이 애플리케이션이 동일한 방식으로 실행될 수 있도록 보장합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/a9d09609-a2af-4f9d-ab42-6998ac7e900f/image.png" alt=""></p>
<p>Dockerfile을 작성한 후 프로젝트를 빌드하면 Docker 이미지가 생성됩니다. 이 이미지만 있으면, EC2와 같은 가상 환경에서 Docker 컨테이너를 생성하여 프로젝트가 실행될 수 있게 됩니다. 즉, 이미지를 통해 애플리케이션을 가상 환경에서도 동일하게 실행할 수 있게 되는 것입니다.</p>
<p>이걸 이제 AWS ECR로 전달하여 EC2에서 가져올 수 있도록 할 것입니다.</p>
<h2 id="⚙️-scripts-파일-작성">⚙️ Scripts 파일 작성</h2>
<p>배포 자동화를 하기 위해서는 EC2가 저희가 원하는대로 동작해줘야합니다. 이걸 어떻게 하는걸까요?? 바로 codedeploy와 appspec.yml 파일을 이용해서 할 수 있습니다.</p>
<blockquote>
<p>appspec.yml 파일은 AWS CodeDeploy에서 사용하는 설정 파일로, 애플리케이션 배포 프로세스를 정의합니다. 이 파일은 배포 단계와 각 단계를 어떻게 처리할지에 대한 지침을 포함합니다. CodeDeploy는 이 파일을 읽고, 지정된 순서대로 배포 작업을 실행합니다.</p>
</blockquote>
<h3 id="appspecyml">appspec.yml</h3>
<pre><code class="language-yml">version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/app

permissions:
  - object: /home/ubuntu/app/scripts
    pattern: &#39;**&#39;
    owner: ubuntu
    group: ubuntu
    mode: 755

hooks:
  ApplicationStop:
    - location: scripts/stop.sh
      timeout: 300
      runas: ubuntu
  BeforeInstall:
    - location: scripts/before_install.sh
      timeout: 300
      runas: ubuntu
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 300
      runas: ubuntu
</code></pre>
<ol>
<li><p>version: 0.0
버전 정보: appspec.yml 파일의 버전을 지정합니다. 0.0은 기본 버전입니다.</p>
</li>
<li><p>os: linux
운영 체제: 배포 대상이 되는 운영 체제를 지정합니다. 이 예제에서는 linux로 설정되어 있습니다.</p>
</li>
<li><p>files
파일 복사: 배포할 파일들의 소스와 목적지 경로를 설정합니다.</p>
<p>source: 배포할 파일들이 위치한 소스 디렉토리입니다. 여기서는 루트 디렉토리(/) 전체를 의미합니다.
destination: EC2 인스턴스 내의 대상 디렉토리입니다. /home/ubuntu/app 경로로 복사됩니다.</p>
</li>
<li><p>permissions
파일 권한 설정: 배포 후 특정 디렉토리나 파일에 대한 권한을 설정합니다.</p>
<p>object: 권한을 설정할 대상 파일이나 디렉토리입니다.
pattern: 대상 파일의 패턴을 지정합니다. **는 하위 디렉토리까지 모두 포함합니다.
owner, group: 파일의 소유자와 그룹을 설정합니다. ubuntu 사용자와 그룹으로 설정되어 있습니다.
mode: 파일의 권한을 설정합니다. 755는 읽기, 쓰기, 실행 권한을 소유자에게 부여하고, 그룹과 다른 사용자에게 읽기 및 실행 권한을 부여합니다.</p>
</li>
<li><p>hooks
배포 후 작업: 배포 프로세스 동안 실행할 스크립트 및 명령을 설정합니다. 각 단계는 ApplicationStop, BeforeInstall, ApplicationStart 등으로 정의되어 있습니다.</p>
<p>ApplicationStop
배포 전에 애플리케이션을 중지하는 스크립트를 실행합니다.</p>
<p>BeforeInstall
애플리케이션을 설치하기 전에 실행할 스크립트를 지정합니다.</p>
<p>ApplicationStart
애플리케이션이 설치되고, 중지된 후에 실행할 시작 스크립트를 설정합니다.</p>
</li>
</ol>
<h3 id="1-stopsh">1. stop.sh</h3>
<pre><code class="language-bash">#!/bin/bash

# 실행 중인 도커 컨테이너 중지
docker stop $(docker ps -a -q) 2&gt;/dev/null || true

# 중지된 컨테이너 삭제
docker rm $(docker ps -a -q) 2&gt;/dev/null || true

# 사용하지 않는 이미지 삭제 (선택사항)
docker image prune -af 2&gt;/dev/null || true

exit 0 </code></pre>
<p>위 스크립트는 새로운 버전의 애플리케이션을 배포할 때, 기존 Docker 컨테이너와 이미지를 정리하여 충돌 없이 배포가 이루어지도록 준비하는 작업을 수행합니다.</p>
<h3 id="2-before_installsh">2. before_install.sh</h3>
<pre><code class="language-bash">#!/bin/bash

# 패키지 매니저 업데이트
sudo apt-get update

# Docker 설치를 위한 필수 패키지 설치
sudo apt-get install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

# Docker의 공식 GPG 키 추가
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# Docker 레포지토리 설정
echo \
  &quot;deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

# Docker 설치
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io

# Docker 서비스 시작
sudo systemctl start docker
sudo systemctl enable docker

# 현재 사용자를 docker 그룹에 추가
sudo usermod -aG docker ubuntu

# Docker Compose 설치
sudo curl -L &quot;https://github.com/docker/compose/releases/download/v2.17.0/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# AWS CLI 설치
sudo apt-get install -y awscli

# 스크립트 실행 권한 설정
sudo chmod +x /home/ubuntu/app/scripts/*.sh

if [ -d /home/ubuntu/app ]; then
    rm -rf /home/ubuntu/app/*
fi

mkdir -p /home/ubuntu/app
mkdir -p /home/ubuntu/app/scripts</code></pre>
<p>위 스크립트는 애플리케이션을 설치하기 전에 서버에 Docker, Docker Compose, AWS CLI 설치 및 환경 설정을 수행하며, 애플리케이션 배포를 위한 디렉토리와 스크립트 실행 환경을 준비하는 스크립트입니다.</p>
<h3 id="3startsh">3.start.sh</h3>
<pre><code class="language-bash">#!/bin/bash
cd /home/ubuntu/app

# .env 파일 로드
export $(cat /home/ubuntu/app/codedeploy.env | xargs)

# 도커 이미지 풀
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_REGISTRY
docker pull $ECR_REGISTRY:$IMAGE_TAG

# 새 컨테이너 실행
docker run -d -p 3001:3001 --name app $ECR_REGISTRY:$IMAGE_TAG </code></pre>
<p>위 스크립트는 애플리케이션이 설치되고, 중지된 후 새로운 도커 이미지를 ECR에서 가져와, 기존 컨테이너 정리 없이 새로운 컨테이너를 실행하는 스크립트입니다.</p>
<p>이렇게 만든 스크립트 파일들을 압축하여 S3 저장소에 올려줘 EC2가 이 파일들을 가져와서 실행할 수 있게 해주어야합니다.
저희가 매번 배포할 때마다 명령어를 입력하여 저장소에 도커 이미지 올리고 S3에 파일을 업로드할 수 없으니깐 이 작업을 깃허브 액션을 이용하여 자동화를 시켜줄겁니다.</p>
<h2 id="🐈-github-action을-이용한-ci--cd">🐈 Github Action을 이용한 CI | CD</h2>
<h3 id="전체-코드">전체 코드</h3>
<pre><code class="language-yaml">name: Dockerizing to Amazon ECR # 워크플로우의 이름을 &#39;Dockerizing to Amazon ECR&#39;로 설정합니다.

on:
  push: # 특정 브랜치에 코드가 푸시될 때 워크플로우가 트리거됩니다.
    branches: [&#39;main&#39;] # &#39;main&#39; 브랜치에 푸시될 때만 워크플로우 실행
  pull_request: # 풀 리퀘스트가 열리거나 업데이트될 때 트리거됩니다.
    branches: [&#39;main&#39;] # &#39;main&#39; 브랜치에 대한 풀 리퀘스트일 때만 실행

jobs: # 실행할 작업을 정의합니다.
  deploy: # &#39;deploy&#39;라는 작업을 정의합니다.
    name: Deploy # 작업의 이름을 &#39;Deploy&#39;로 설정
    runs-on: ubuntu-latest # 최신 Ubuntu 버전에서 실행되도록 설정
    environment: production # 이 작업이 &#39;production&#39; 환경에서 실행됨을 정의

    steps: # 이 작업에서 실행될 단계들을 정의합니다.
      - name: Checkout # 소스 코드를 체크아웃하는 단계
        uses: actions/checkout@v3 # GitHub 제공 체크아웃 액션 사용

      - name: Create .env file
        run: |
          echo &quot;DATABASE_URL=${{ secrets.DATABASE_URL }}&quot; &gt; .env
          echo &quot;DIRECT_URL=${{ secrets.DIRECT_URL }}&quot; &gt;&gt; .env

      - name: Create codedeploy .env file
        run: |
          echo &quot;ECR_REGISTRY=${{ secrets.ECR_REGISTRY }}&quot; &gt; codedeploy.env
          echo &quot;IMAGE_TAG=${{ github.sha }}&quot; &gt;&gt; codedeploy.env

      - name: Config AWS credentials # AWS 자격 증명을 구성하는 단계
        uses: aws-actions/configure-aws-credentials@v2 # AWS 자격 증명 구성 액션 사용
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }} # 사용 중인 리전

      - name: Login To Amazon ECR # Amazon ECR에 로그인하는 단계
        id: login-ecr # 이 단계의 ID를 &#39;login-ecr&#39;로 설정 (후속 단계에서 참조 가능)
        uses: aws-actions/amazon-ecr-login@v1 # Amazon ECR 로그인 액션 사용

      - name: Build, tag, and push image to Amazon ECR # Docker 이미지를 빌드, 태그, ECR에 푸시하는 단계
        id: build-image # 이 단계의 ID를 &#39;build-image&#39;로 설정
        env:
          ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} # ECR 레지스트리 URL 설정
          IMAGE_TAG: ${{ github.sha }} # GitHub의 커밋 SHA 값을 이미지 태그로 사용
        run: |
          docker build -t $ECR_REGISTRY:$IMAGE_TAG .
          docker push $ECR_REGISTRY:$IMAGE_TAG
          echo &quot;::set-output name=image::$ECR_REGISTRY:$IMAGE_TAG&quot;  # 빌드된 이미지 URL을 워크플로우 출력으로 설정

      - name: Create deployment package
        run: |
          mkdir -p deploy
          cp -r scripts deploy/
          cp appspec.yml deploy/
          cp codedeploy.env deploy/
          cd deploy &amp;&amp; zip -r ../deploy.zip .

      - name: Upload to S3
        run: |
          aws s3 cp deploy.zip s3://${{ secrets.S3_BUCKET }}/deploy.zip

      - name: Create CodeDeploy Deployment
        run: |
          aws deploy create-deployment \
            --application-name lily-server-deploy \
            --deployment-group-name lily-server-deploy-group \
            --s3-location bucket=${{ secrets.S3_BUCKET }},bundleType=zip,key=deploy.zip \
            --region ${{ secrets.AWS_REGION }}
</code></pre>
<p>아래에서 코드를 나눠서 설명드리겠습니다.</p>
<h3 id="docker-image-ecr에-업로드">Docker Image ECR에 업로드</h3>
<pre><code class="language-yaml">      - name: Login To Amazon ECR # Amazon ECR에 로그인하는 단계
        id: login-ecr # 이 단계의 ID를 &#39;login-ecr&#39;로 설정 (후속 단계에서 참조 가능)
        uses: aws-actions/amazon-ecr-login@v1 # Amazon ECR 로그인 액션 사용

      - name: Build, tag, and push image to Amazon ECR # Docker 이미지를 빌드, 태그, ECR에 푸시하는 단계
        id: build-image # 이 단계의 ID를 &#39;build-image&#39;로 설정
        env:
          ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} # ECR 레지스트리 URL 설정
          IMAGE_TAG: ${{ github.sha }} # GitHub의 커밋 SHA 값을 이미지 태그로 사용
        run: |
          docker build -t $ECR_REGISTRY:$IMAGE_TAG .
          docker push $ECR_REGISTRY:$IMAGE_TAG
          echo &quot;::set-output name=image::$ECR_REGISTRY:$IMAGE_TAG&quot;  # 빌드된 이미지 URL을 워크플로우 출력으로 

</code></pre>
<p>이 단계에서는 빌드된 도커 이미지를 ECR에 올리는 작업을 진행하게 됩니다.
여기서 보이는 <code>secrets.ECR_REGISTRY</code>는 깃허브에서 환경변수로 한 후 위 코드처럼 값을 불러와 사용할 수 있습니다.</p>
<h3 id="codedeploy가-실행할-스크립트-압축-후-s3-업로드">Codedeploy가 실행할 스크립트 압축 후 S3 업로드</h3>
<pre><code class="language-yaml">  - name: Create deployment package
        run: |
          mkdir -p deploy
          cp -r scripts deploy/
          cp appspec.yml deploy/
          cp codedeploy.env deploy/
          cd deploy &amp;&amp; zip -r ../deploy.zip .
   - name: Upload to S3
        run: |
          aws s3 cp deploy.zip s3://${{ secrets.S3_BUCKET }}/deploy.zip</code></pre>
<p>이 단계에서는 Codedeploy가 EC2에게 작업 명령을 내일 스크립트 파일들을 압축하여 S3에 올리는 작업을 거치게 됩니다.</p>
<h3 id="codedeploy-실행">Codedeploy 실행</h3>
<pre><code class="language-yaml">      - name: Create CodeDeploy Deployment
        run: |
          aws deploy create-deployment \
            --application-name lily-server-deploy \
            --deployment-group-name lily-server-deploy-group \
            --s3-location bucket=${{ secrets.S3_BUCKET }},bundleType=zip,key=deploy.zip \
            --region ${{ secrets.AWS_REGION }}
</code></pre>
<p>이 단계에서는 AWS에서 미리 만들어둔 Codedeploy에게 배포를 진행하라는 명령을 하게됩니다</p>
<h3 id="codedeploy-오류">Codedeploy 오류</h3>
<p>만약 위와 같은 방법으로 진행하였는데 배포 실패가 뜨는 경우가 있을겁니다.(글에는 안 썼지만 보안그룹, IAM 설정 등은 다 했다고 가정했을 때)</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/a8a67d08-bf2c-40ae-be7f-52a49391ac0b/image.png" alt=""></p>
<p>인스턴스에 기존 AWS 자격 증명 파일이 저장되어있어 IAM 정보를 제대로 못 가져오는 현상이 있을 수 있다고 합니다.</p>
<pre><code class="language-bash"># AWS 자격증명 파일 삭제
$ sudo rm -rf /root/.aws/credentials

# codedeploy-agent 재시작
$ sudo systemctl restart codedeploy-agent</code></pre>
<p>이 문제는 지우고 다시 시작한 후, 로그를 확인해보시면 해결이 될겁니다.</p>
<p><a href="https://velog.io/@gingaminga/AwsCodeDeployCommandErrorsAccessDeniedException">참고</a></p>
<h2 id="🖥️-ec2-설정">🖥️ EC2 설정</h2>
<p>codedeloy를 통해서 스크립트 파일들이 정상적으로 동작되게 하기 위해서는 EC2에서 필요한 프로그램들을 깔아줘야한다.</p>
<ol>
<li><p>Docker 설치:
EC2 인스턴스에 SSH로 접속합니다.
Docker를 설치합니다:</p>
<pre><code class="language-bash">sudo apt-get update
udo apt-get install -y docker.io</code></pre>
</li>
<li><p>Docker 서비스 시작 및 자동 시작 설정:</p>
<pre><code class="language-bash">sudo systemctl start docker
sudo systemctl enable docker</code></pre>
</li>
<li><p>Docker 그룹에 사용자 추가:
현재 사용자를 Docker 그룹에 추가하여 sudo 없이 Docker 명령어를 실행할 수 있도록 합니다:</p>
<pre><code class="language-bash">sudo usermod -aG docker $USER</code></pre>
</li>
<li><p>변경 사항 적용:
로그아웃 후 다시 로그인하거나, 다음 명령어로 현재 셸을 다시 시작합니다:</p>
<pre><code class="language-bash">sudo apt-get install -y awscli</code></pre>
</li>
<li><p>AWS CLI 설치:
AWS CLI를 설치하여 S3 및 ECR과 상호작용할 수 있도록 합니다:</p>
<pre><code class="language-bash">sudo apt-get install -y awscli</code></pre>
</li>
<li><p>CodeDeploy Agent 설치:
CodeDeploy Agent를 설치하여 EC2 인스턴스에서 배포를 받을 수 있도록 합니다:</p>
<pre><code class="language-bash">sudo apt-get install -y ruby
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto</code></pre>
</li>
<li><p>CodeDeploy Agent 시작 및 상태 확인:</p>
<pre><code class="language-bash">sudo service codedeploy-agent start
sudo service codedeploy-agent status</code></pre>
</li>
<li><p>CodeDeploy Agent 실시간 로고 확인 명령어</p>
<pre><code class="language-bash">tail -f /var/log/aws/codedeploy-agent/codedeploy-agent.log</code></pre>
</li>
</ol>
<h2 id="마무리">마무리</h2>
<p>이번에 AWS를 이용해서 처음으로 배포를 해보는 것이라 많은 것이 낯설고 정말 오류가 계속 터져나왔습니다.
그래도 포기하지않고 이곳 저곳 알아보며 결국 배포를 성공하니 정말 기쁘더군요
부족한 글이지만 다른 분들도 이 글을 읽고 조금이나마 도움이 되시길 바랍니다. </p>
<p>다음에는 도메인을 하나 사서 도메인 연결 및 ssl 설정 주제로 글을 써보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FeedB 간단한 유닛 테스트 작성]]></title>
            <link>https://velog.io/@yunbh_0401/FeedB-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9C%A0%EB%8B%9B-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1</link>
            <guid>https://velog.io/@yunbh_0401/FeedB-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9C%A0%EB%8B%9B-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1</guid>
            <pubDate>Mon, 02 Dec 2024 02:34:04 GMT</pubDate>
            <description><![CDATA[<h1 id="🗃️-스토리북으로-문서화">🗃️ 스토리북으로 문서화</h1>
<p>프로젝트를 진행하면서 공용 컴포넌트를 많이 도입했지만, 팀원들이 이를 사용하는 방법을 잘 이해하지 못해 만든 사람에게 질문이 계속 이어졌습니다. 이로 인해 실제 개발에 할애할 시간이 줄어들었습니다. 
또한 공용 컴포넌트의 기능이 자주 변경되면서 버그가 자주 발생하는 문제도 겪었습니다.</p>
<p>그래서 스토리북으로 문서화를 진행하여 조금이나마 공통 컴포넌트를 사용하기 쉽게 바꿔보도록 하였습니다.
그 중 제가 모달과 토스트 컴포넌트를 맡아서 진행하기로 했습니다.</p>
<h2 id="스토리-작성-모달">스토리 작성 (모달)</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/b94c0b2d-73e1-44a1-9fe4-c2c8a574aa6a/image.png" alt=""></p>
<p>일단 먼저 스토리북을 세팅한 후 버튼 UI 컴포넌트를 가져와 버튼을 누르면 모달이 열리게끔 설정을 하려고 했습니다.
근데 여기서 문제가 생겼습니다.
저희 모달 같은 경우에는 React-Potal을 사용했기 때문에 id-modal을 가진 div를 따로 만들어줘야했습니다.</p>
<pre><code class="language-ts">import React, { useEffect } from &quot;react&quot;;
import type { Preview } from &quot;@storybook/react&quot;;
import &quot;../app/_styles/globals.css&quot;;

// 전역 decorator 추가
const WithModalRoot = (Story: React.ComponentType) =&gt; {
  useEffect(() =&gt; {
    // &#39;modal-root&#39;가 존재하지 않으면 생성
    if (!document.getElementById(&quot;modal&quot;)) {
      const modalRoot = document.createElement(&quot;div&quot;);
      modalRoot.setAttribute(&quot;id&quot;, &quot;modal&quot;);
      document.body.appendChild(modalRoot);
    }
  }, []);

  return &lt;Story /&gt;;
};

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
  decorators: [WithModalRoot], // Decorator를 추가하여 모든 스토리에 적용
};

export default preview;
</code></pre>
<p>그거에 대한 작업은 <strong><em>preview</em></strong> 파일에 위와 같이 코드를 작성하여 modal이란 아이디를 가진 div를 최상단에 만들어줘 모달이 정상적으로 열리게 해결해주었습니다.</p>
<p>#</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/001d68b2-ac67-44b6-8efc-f4667fe7bbab/image.png" alt=""></p>
<p>그렇게 로그인, 회원가입, 회원정보 수정 모달을 전부 스토리를 연결하고 스토리 파일이 있는 위치에 .mdx 파일을 만들어줘 문서화를 진행하였습니다.</p>
<h2 id="유닛테스트">유닛테스트</h2>
<pre><code class="language-ts">import { composeStories } from &quot;@storybook/react&quot;;
import React from &quot;react&quot;;
import { render, screen, userEvent } from &quot;../../util/test-util&quot;;
import * as stories from &quot;./Modal.stories&quot;;

const { Login } = composeStories(stories);

jest.mock(&quot;next/image&quot;, () =&gt; ({
  __esModule: true,
  default: (props: any) =&gt; {
    const { src, alt } = props;
    return &lt;img src={src} alt={alt} /&gt;;
  },
}));

describe(&quot;Login Modal 컴포넌트 테스트&quot;, () =&gt; {
  it(&quot;버튼을 클릭하면 모달이 열려야 한다&quot;, async () =&gt; {
    // GIVEN
    render(&lt;Login /&gt;);

    // 초기 상태: 모달이 없는지 확인
    expect(screen.queryByRole(&quot;dialog&quot;)).not.toBeInTheDocument();

    // WHEN
    const button = screen.getByRole(&quot;button&quot;, { name: /로그인/i });
    await userEvent.click(button);

    // 모달이 열렸는지 확인
    expect(screen.getByRole(&quot;dialog&quot;)).toBeInTheDocument();
  });

  it(&quot;x 버튼을 클릭하면 모달이 닫혀야 한다&quot;, async () =&gt; {
    // GIVEN
    render(&lt;Login /&gt;);

    // WHEN
    const loginButton = screen.getByRole(&quot;button&quot;, { name: /로그인/i });
    await userEvent.click(loginButton);

    // 모달 열림 확인
    const modal = screen.getByRole(&quot;dialog&quot;);
    expect(modal).toBeInTheDocument();

    // x 버튼 클릭
    const closeButton = screen.getByRole(&quot;button&quot;, { name: /닫기/i });
    await userEvent.click(closeButton);

    // THEN
    // 모달이 닫혔는지 확인
    expect(screen.queryByRole(&quot;dialog&quot;)).not.toBeInTheDocument();
  });
});
</code></pre>
<p>테스트 목표는 간단하게 정하였습니다</p>
<p>테스트 목표</p>
<ol>
<li>로그인 버튼을 클릭하면 모달이 열려야 한다.</li>
<li>모달의 x 버튼을 클릭하면 모달이 닫혀야 한다.</li>
</ol>
<p>이 테스트를 작성하는 도중 모달이 띄워지는걸 어떻게 테스트 도구가 확인하는지 잘 모르는 상태였는데 </p>
<pre><code class="language-ts">&lt;Modal role=&quot;dialog&quot;&gt;&lt;/Modal&gt;</code></pre>
<p>이런식으로 role을 이용해서 인식을 할 수 있도록 바꿔주면 되더라구요.</p>
<p>그렇게 목표에 맞게 테스트 코드를 작성해보았습니다.</p>
<h2 id="스토리-작성-토스트">스토리 작성 (토스트)</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/b7b3152b-4518-48fb-9d38-f8cd9851a3b6/image.png" alt=""></p>
<p>이번에도 별 다를 거 없이 버튼 UI 컴포넌트를 가져와 버튼을 누르면 토스트 메시지가 보이게 스토리를 작성하여 다른 사람들이 UI를 확인할 수 있게끔 해놓았습니다.</p>
<h2 id="문서화">문서화</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/667b0f29-11e4-4eae-9f4e-04a3502252aa/image.png" alt=""></p>
<p>이것도 똑같이 mdx 파일을 만들어 제가 설정해놓은 스토리들을 가져와서 문서화를 시켜주었습니다.</p>
<h2 id="유닛테스트-1">유닛테스트</h2>
<pre><code class="language-ts">import { composeStories } from &quot;@storybook/react&quot;;
import React from &quot;react&quot;;
import { render, screen, userEvent, waitFor } from &quot;../../util/test-util&quot;;
import * as stories from &quot;./Toast.stories&quot;;
const { Success } = composeStories(stories);

describe(&quot;Toast 컴포넌트 테스트&quot;, () =&gt; {
  it(&quot;Toast 클릭 시 Toast가 잘 뜨는지 확인&quot;, async () =&gt; {
    // GIVEN
    render(&lt;Success /&gt;);

    // WHEN
    const button = screen.getByRole(&quot;button&quot;);
    await userEvent.click(button);

    //THEN;

    await waitFor(() =&gt; {
      expect(screen.getByText(&quot;성공 성공&quot;)).toBeInTheDocument();
    });
    expect(screen.getByText(&quot;성공 성공&quot;)).toBeInTheDocument();
  });

  it(&quot;Toast 클릭 후 3초 뒤 Toast가 사라지는지 확인&quot;, async () =&gt; {
    // GIVEN
    const { container } = render(&lt;Success /&gt;);

    // WHEN
    const button = screen.getByRole(&quot;button&quot;);
    await userEvent.click(button);

    const toastContainer = container.querySelector(&quot;#toast-container&quot;);

    // THEN
    jest.advanceTimersByTime(3901);
    await waitFor(() =&gt; {
      expect(toastContainer?.hasChildNodes()).toBeFalsy();
    });
    expect(toastContainer?.hasChildNodes()).toBeFalsy();
  });
});
</code></pre>
<p>테스트 목표</p>
<ol>
<li>Toast 클릭 시 정상적으로 표시되는지 확인</li>
<li>Toast 클릭 후 3초 뒤에 자동으로 사라지는지 확인</li>
</ol>
<p>버튼을 클릭하여 Toast가 잘 뜨는지 확인하고 3초간 기다렸다가 잘 사라지는지 확인하기 위한 테스트 코드를 작성하여 정상적으로 동작하는지 확인하였습니다.</p>
<h1 id="느낀점">느낀점</h1>
<p>이번에 간단하게 UI들을 유닛 테스트를 진행해보았습니다. 정말 많이 느낀 것은 예전 멘토님들께서 UI 컴포넌트는 헤드리스 컴포넌트로 작성하는 것이 중요하다고 말씀하셨던 내용이 실감이 나기 시작했습니다.</p>
<p>헤드리스 컴포넌트는 UI의 동작을 논리적인 부분과 시각적인 부분을 분리하여, 실제 화면 구성에 영향을 미치지 않도록 설계된 컴포넌트입니다. 이 접근 방식은 다음과 같은 장점이 있습니다:</p>
<p>테스트 용이성: UI 컴포넌트를 로직과 스타일로 나누어 작성하면, 로직을 독립적으로 테스트할 수 있어 테스트가 쉬워집니다. 예를 들어, 버튼이나 모달 컴포넌트의 동작을 별도로 테스트하고, UI는 나중에 스타일링을 통해 추가할 수 있습니다.
재사용성: 헤드리스 컴포넌트는 시각적인 부분에 의존하지 않으므로, 다른 프로젝트나 페이지에서도 쉽게 재사용할 수 있습니다.
유연성: 시각적인 스타일을 별도로 처리함으로써, 디자인을 변경할 때 컴포넌트의 로직을 변경할 필요 없이 스타일만 수정하면 되기 때문에 더 많은 유연성을 제공합니다.
UI 컴포넌트를 테스트하면서 헤드리스로 구성된 로직을 먼저 작성하고, 그 후에 실제 UI를 붙이는 방식으로 작업을 진행하니 코드의 품질이 높아지고, 테스트 코드도 더욱 명확해진다는 것을 느꼈습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[디자인 시스템 & 스토리북 배포]]></title>
            <link>https://velog.io/@yunbh_0401/%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%8A%A4%ED%86%A0%EB%A6%AC%EB%B6%81-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@yunbh_0401/%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%8A%A4%ED%86%A0%EB%A6%AC%EB%B6%81-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Fri, 29 Nov 2024 13:58:46 GMT</pubDate>
            <description><![CDATA[<h1 id="👀-지난-이야기">👀 지난 이야기....</h1>
<p>지난 글에서 Turborepo에 대해서 이야기를 했었습니다.
그리고 마지막에 디자인 시스템을 어떻게 배포할건지 이번 글에서 설명한다고 그랬었는데요 바로 알아보도록 하죠</p>
<h1 id="🎨-디자인-시스템을-왜-npm으로-배포하는가">🎨 디자인 시스템을 왜 npm으로 배포하는가?</h1>
<p>이전 글에서 Turborepo로 생성한 프로젝트를 통째로 Vercel에 업로드하면 Verce이 알아서 app 폴더안에 있는 서비스들을 각각 도메인을 부여해 배포한다는 말을 했었는데요 그럼 여기서 사용하는 ui 폴더 컴포넌트들은 어떻게 되는걸까요? 
모노레포로 관리하고 있기 때문에 큰 문제 없이 사용할 수 있습니다.</p>
<p>하지만 저희는 </p>
<h2 id="배포-계획">배포 계획</h2>
<p>메인 서비스 - AWS RCS로 배포
관리자 서비스 - Vecel로 배포
디자인 시스템 - npm에 배포
스토리북 - Vecel로 배포</p>
<p>요런식으로 배포하기로 했었습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/2b470825-6277-4210-aa71-6bddd154eb7f/image.png" alt=""></p>
<p>그러면 app안에 있는 특정 서비스들만 따로 Vercel로 선택하여 배포를 진행하여야합니다. 물론 Vercel에서 아주 친절하게 따로 배포할 수 있게 서비스를 제공해주고 있습니다.</p>
<p>저기서 스토리북을 선택하고 배포를 시작하게 되면 아래와 같은 에러가 뜰겁니다.</p>
<ul>
<li><strong>* Error: No Output Directory named &quot;dist&quot; found after the Build completed. You can configure the Output Directory in your Project Settings. *</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/55e954d8-8725-4b20-9444-872357d8564a/image.png" alt=""></p>
<p>자 이건 왜 뜨는걸까요?? 저도 몰라서 삽질 좀 했습니다.
생각을 곰곰히 해봤습니다. 여태 저의 프론트엔드 개발 인생을 되돌아보며....
보통 dist 폴더는 빌드를 하면 나오는 것인데.... 라고 생각했더니 번뜩 떠올랐습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/ab20b3d9-6568-41c5-a470-b4112e860c04/image.png" alt=""></p>
<p>위와 같이 빌드한 후에 dist 폴더를 만들 수 있도록 명령어를 수정했습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/5d216c39-e025-4cce-be0b-2422b78e6496/image.png" alt=""></p>
<p>바로 성공해버리는 결과가 나와버렸습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/3468e6d4-4505-4ff4-9160-140f8d2309c0/image.png" alt="">
배포된 주소로 접속을 해보니 문제가 좀 있었습니다
저건 왜저럴까요?</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/311114d8-6b4b-4c49-b216-55430ea3453d/image.png" alt=""></p>
<p>사진에 26번째 줄을 보시면 스토리북에서 지금 packages/ui를 가져와서 사용하려고 합니다.
근데 저희는 배포를 어떻게 했었죠??
루트 디렉토리를 app/storybook으로 해주었습니다. 그러면 로컬 주소로 되어있는 저 폴더 위치를 따라가 ui를 절대 가져올 수가 없겠죠?
이럴땐 방법이 2가지가 있습니다.</p>
<h3 id="첫번째---빌드할-때-ui-포함시키기">첫번째 - 빌드할 때 ui 포함시키기</h3>
<pre><code class="language-js">async viteFinal(config) {
  return {
    ...config,
    define: { &quot;process.env&quot;: {} },
    resolve: {
      alias: [
        {
          find: &quot;ui&quot;,
          replacement: resolve(__dirname, &quot;../../../packages/ui/&quot;),
        },
      ],
    },
    optimizeDeps: {
      include: [&quot;../../../packages/ui&quot;], // 빌드 시 포함
    },
  };
}
</code></pre>
<p>ui 패키지 파일을 Storybook 빌드에 포함시켜 배포 환경에서도 문제없이 사용할 수 있게 합니다.</p>
<h3 id="두번째---별도-npm-패키지로-배포">두번째 - 별도 NPM 패키지로 배포</h3>
<p>사실 첫 번째 방법이 편하고 좋습니다. 그럼에도 제가 이 방법을 선택한 이유는 지금은 스토리북에서 빌드때 포함시키는 방법은 알았지만 다른 프로젝트에서 어떻게 빌드할 때 포함시키는지 모르기 때문에 두 번째 방법을 선택했습니다.(그리고 npm에 배포 한 번 해보고 싶었음)</p>
<p>이런 이유들로 npm에 디자인 시스템을 배포하려고 하는데 어떤걸 해줘야할까요?</p>
<h3 id="빌드-방법">빌드 방법</h3>
<ol>
<li>번들러 선택</li>
<li>디자인 시스템(ui) 빌드 폴더(dist) 생성</li>
<li>npm 배포</li>
</ol>
<p>생각보다 간단하죠?? 
일단 번들러를 골라야하는데 이번에 저는 &quot;tsup&quot;이라는 번들러를 선택해서 진행해보았습니다.</p>
<p>설명을 드리자면
tsup은 TypeScript로 작성된 코드를 빌드하기 위한 번들러입니다. 빠르고 간단하며, Zero-config(별도의 설정 없이 기본 설정으로 작동)로 유명합니다. 내부적으로 ESBuild를 사용하여 번들링 속도가 매우 빠릅니다.</p>
<h3 id="tsup의-주요-특징">tsup의 주요 특징</h3>
<ol>
<li><p>빠른 빌드 속도:
ESBuild를 사용하여 다른 번들러보다 훨씬 빠른 속도를 제공합니다.</p>
</li>
<li><p>Zero-config:
기본적으로 TypeScript 파일을 읽어들여 JavaScript로 컴파일하고, ESM, CommonJS 등 다양한 출력 형식을 지원합니다.
간단한 설정만으로도 충분히 동작합니다.</p>
</li>
<li><p>다양한 출력 형식:
CommonJS (cjs), ECMAScript Module (esm), UMD (umd), IIFE 등의 출력 형식을 지원합니다.</p>
</li>
<li><p>Tree Shaking:
사용하지 않는 코드를 제거하여 더 작은 번들 크기를 만듭니다.</p>
</li>
<li><p>간단한 사용법:
CLI를 통해 쉽게 실행할 수 있으며, 설정 파일을 사용하지 않아도 동작합니다.</p>
</li>
<li><p>TypeScript 지원:
TypeScript 컴파일을 기본적으로 지원하며, 타입 선언 파일(.d.ts)도 함께 생성할 수 있습니다.</p>
</li>
</ol>
<p>저번에 pluma 디자인 시스템 만들땐 esbuild를 사용해서 했었는데 이것도 내부적으로 ESBuild를 사용해서 호감이 가더라구요.
그리고 검색하다가 안건데 Turborepo에서도 디자인 시스템 레포를 만들 수 있도록 지원을 해줍니다. 클릭 몇번으로 레포지토리 생성 및 배포까지 해주더라구요.
근데 거기서도 기본으로 설정해주는 번들러가 tsup입니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/d1bef7dc-d66f-48f7-a278-0e3319018283/image.png" alt=""></p>
<p>그래서 저는 tsup를 사용해 빌드를 해주었고 dist 폴더가 아주 잘 생성되었습니다.</p>
<h3 id="npm-배포-명령어">npm 배포 명령어</h3>
<pre><code>npm publish --access public</code></pre><p><img src="https://velog.velcdn.com/images/yunbh_0401/post/caebbeb8-9ec5-4b2f-bd71-435530c84a78/image.png" alt=""></p>
<p>이거 해주면 바로 배포됩니다. 정말 쉽더라구요</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/f5a0de17-b403-4d5a-bb66-9ce3b748dc6f/image.png" alt=""></p>
<p>참고로 패키지 이름은 여기서 정해주면 되는데 앞에 @ 붙히면 npm이 그룹으로 인식을 하여 배포가 안됩니다. 저는 지금 제 유저명으로 해둬서 @을 붙힌 상태로 배포된 상태입니다.
그리고 배포하기전에 버전도 바꿔서 올려주면 좋을 거 같습니다.</p>
<h2 id="스토리북-적용">스토리북 적용</h2>
<p>이건 뭐 다들 자주 하시는 작업입니다. 저희가 프로젝트 진행하다가 필요한 라이브러리나 패키지가 있으면 어떻게 합니까?</p>
<pre><code>pnpm i @yunbh/design-system</code></pre><p>명령어 실행해주고 다시 실행해보면 </p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/b812207c-7e69-46a8-8cda-ad72e9d37924/image.png" alt=""></p>
<p>기가맥히게 잘 뜨는 걸 볼 수 있습니다.</p>
<p>네 오늘도 긴 글 읽어주셔서 감사합니다. 다음에 더 좋은 글로 오겠습니다.
혹시 코드 보고싶은 분은 언제든지 와서 보십쇼
<a href="https://github.com/78-artilleryman/Lily">깃허브 주소</a>
질문도 환영입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[터보레포 구조 겉핥기]]></title>
            <link>https://velog.io/@yunbh_0401/%ED%84%B0%EB%B3%B4%EB%A0%88%ED%8F%AC-%EA%B5%AC%EC%A1%B0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@yunbh_0401/%ED%84%B0%EB%B3%B4%EB%A0%88%ED%8F%AC-%EA%B5%AC%EC%A1%B0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 29 Nov 2024 12:06:56 GMT</pubDate>
            <description><![CDATA[<h1 id="🤔-왜-모노레포를-사용하는가">🤔 왜 모노레포를 사용하는가?</h1>
<p>이번에 진행할 프로젝트는 메인 서비스(Lily), 관리자 서비스, 디자인 시스템 총 3개를 만들 계획이라 모노레포로 프로젝를 진행하기로 결정했습니다.
모노레포에 종류가 되게 많지만 그 중 Turborepo를 선택하여 진행해보려고 합니다.</p>
<p>Turborepo는 Vercel에서 개발한 오픈소스 모노레포 관리 툴입니다. Turborepo는 모노레포 환경에서의 빌드, 테스트, 배포를 최적화하고 관리하는 데 도움을 주기 위해 만들어졌습니다.
Vercel에서 만들었기 때문에 Vercel로 서비스를 배포하기가 아주 간단합니다. 저는 이번에 디자인 시스템 스토리북과, 관리자 서비스를 Vercel로 배포할 계획이여서 Turborepo 사용하는 것은 적합한 선택이라고 생각합니다.</p>
<p>그리고 모노레포를 사용하면 장점은 통합된 코드베이스 관리, 의존성 관리 용이, 중복 코드 감소 등이 있어서 지금 상황처럼 프로젝트를 원활하게 진행하는데 큰 도움을 주죠</p>
<h1 id="🛠️-turborepo-설치-및-설정">🛠️ Turborepo 설치 및 설정</h1>
<p>이번 프로젝트에서는 pnpm을 사용해서 진행해보려고 합니다.
pnpm은 모노레포 환경에서 여러 패키지를 효율적으로 관리할 수 있도록 지원하는데 이는 Turborepo와 같은 모노레포 관리 툴과 잘 결합됩니다.</p>
<h2 id="설치">설치</h2>
<pre><code>pnpm install turbo --global</code></pre><p><img src="https://velog.velcdn.com/images/yunbh_0401/post/02c9f166-562b-4c3f-b418-9bcb08b78543/image.png" alt=""></p>
<p>설치 후 프로젝트를 열어보면 위 사진과 같이 구성이 되어있습니다.(스토리북, 허스키, 프리티어는 추가로 설정해줘야함)
프로젝트를 설치하면 해줘야할 국룰 작업이 있죠?</p>
<h2 id="eslint--prettier-설정">Eslint + Prettier 설정</h2>
<p>아까 모노레포를 사용하면 통합된 코드베이스 관리를 할 수 있다고 말했습니다.
모노레포를 사용하지 않으면 프로젝트마다 Eslint + Prettier를 전부 하나씩 다 설정을 해줘야합니다
하지만 저희는 그러기 귀찮으니깐 모노레포를 사용해서 모든 프로젝트에 똑같은 코드베이스 설정을 해줄 수 있습니다.</p>
<p>위에 사진을 보시면 packages라는 폴더가 있는데 그 폴더에는 </p>
<ol>
<li>eslint-config</li>
<li>typescript-config</li>
</ol>
<p>두 가지 폴더들이 있습니다.
<img src="https://velog.velcdn.com/images/yunbh_0401/post/45aeacb1-cbe4-4fb5-9236-7625e32909a3/image.png" alt=""></p>
<p>그 중 eslint-config에 있는 base.js를 열어보면 저희가 흔히 봤던 코드 구조가 나오는데요.
이 파일에서 저희가 적용해주고 싶은 Eslint 규칙을 추가해주시면 프로젝트에 적용이 아주 잘 된답니다.
근데 바로 밑에 있는 next.js는 무엇일까요??</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/cd4a12d9-d759-43d6-88f1-acd5a2258f8a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/6a332bfb-a6a7-4da1-a689-e0244e713afd/image.png" alt=""></p>
<p>결론만 말씀드리면 지금 web과 docs라는 폴더안에는 Next 프로젝트가 구성되어 있습니다. base.js에 넣은 Eslint 규칙은 모든 프로젝트에 적용이 되는 공통 규칙이고 next.js에 설정한 규칙은 web과 docs 프로젝트에만 적용되는 규칙입니다.</p>
<p>사진을 보면 알 수 있듯이 eslint.config.js에 next.js 파일을 가져와 적용하는 코드를 볼 수 있죠
참고로 이건 eslint 9버전으로 설정한 코드입니다. 8버전은 코드가 다르니 그건 알아서 찾아서 잘 해보시길 👍</p>
<p>이제 남은 건 프리티어 설정인데 이건 뭐 너무 쉽습니다. 프로젝트 루트에 .prettierrc 파일 하나 만들어주고 규칙 설정해주고 vscode 폴더 있을텐데 거기에 아래 코드 넣어주면 끝나더라구요. 그럼 이제 넘어갈게요</p>
<pre><code class="language-json">{
  &quot;eslint.workingDirectories&quot;: [
    {
      &quot;mode&quot;: &quot;auto&quot;
    }
  ],
  &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;,
  &quot;editor.formatOnSave&quot;: true,
  &quot;[javascript]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;
  },
  &quot;[typescript]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;
  },
  &quot;[json]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;
  }
}</code></pre>
<h2 id="허스키-설정">허스키 설정</h2>
<p>허스키 설정도 해줬는데 이건 예전 글에서 말했던 내용이랑 별 다를 거 없어서 넘어갈게요
<a href="https://velog.io/@yunbh_0401/%ED%97%88%EC%8A%A4%ED%82%A4-%EC%A1%B0%EB%A0%A8%EC%82%AC">허스키 설정 글 링크</a></p>
<h1 id="🧩-packages--ui">🧩 packages / ui</h1>
<p>여태 참고했던 사진을 보면 알 수 있듯이 packages/ui 위치에 폴더 하나가 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/019f9274-3a34-4c55-9178-cf4c5856c52c/image.png" alt=""></p>
<p>구조상 여기에 UI 컴포넌트를 만들어서 web과 docs에서 사용할 수 있게 해주는 역할을 하는 것 같죠?</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/39ddb750-f5f3-4f2c-9708-e2bcf37d67d9/image.png" alt=""></p>
<p>web 프로젝트에 있는 package.json을 보니 잘 가져와서 사용하고 있습니다. 
이건 어떤 원리로 이렇게 사용할 수 있는 것일까요??</p>
<h2 id="원리">원리</h2>
<p>Turborepo는 ui 폴더를 로컬 패키지로 간주하고, 이를 모노레포의 다른 프로젝트(예: apps/project1, apps/project2)에서 의존성으로 추가하여 사용할 수 있게 합니다.</p>
<p>ui 폴더는 npm 패키지처럼 작동합니다.
보통 ui 폴더 안에 package.json이 있으며, 이 파일을 통해 의존성, 빌드 설정, 이름 등을 정의합니다.
예: @repo/ui라는 이름의 패키지를 정의한다면, 다른 프로젝트에서 이를 의존성으로 추가할 수 있습니다.</p>
<p>말이 어렵긴한데 실제로 설정하는건 어렵지않으니 나중에 한 번 해보시면 좋을 거 같습니다.</p>
<p>그리고 저기 나오는 workwpace는 무엇일까요??</p>
<p>워크스페이스(Workspace)는 모노레포(Monorepo) 환경에서 여러 프로젝트(패키지)를 한 곳에서 관리할 수 있게 하는 구조입니다.
이를 통해 각각의 프로젝트(패키지)가 서로 독립적으로 작동하면서도 공유되는 의존성이나 코드를 쉽게 재사용할 수 있습니다.</p>
<pre><code>packages:
  - &quot;apps/*&quot;
  - &quot;packages/*&quot;</code></pre><p>pnpm-workspace.yaml이라는 파일이 있는데 여기서 워크스페이스를 설정해줄 수 있습니다.
apps/<em>: apps 폴더 아래의 모든 디렉토리를 워크스페이스로 간주.
packages/</em>: packages 폴더 아래의 모든 디렉토리를 워크스페이스로 간주.</p>
<p>요약을 해보자면</p>
<p>1.워크스페이스(Workspace)는 모노레포에서 여러 프로젝트(패키지)를 한곳에서 관리하며, 코드와 의존성을 쉽게 재사용할 수 있는 구조입니다.
2. pnpm-workspace.yaml 파일에서 apps/<em>와 packages/</em>로 워크스페이스 경로를 설정해 각 디렉토리를 관리 대상으로 지정합니다.
3. ui 폴더는 package.json을 통해 로컬 npm 패키지처럼 정의되며, 다른 프로젝트에서 이를 의존성으로 추가해 사용할 수 있습니다.</p>
<p>이런 이유들로 ui 패키지에 있는 것들을 쉽게 가져와 사용할 수 있는 것입니다.</p>
<p>하지만 저희는 이렇게 하면 안됩니다.</p>
<p>왜냐하면 서비스마다 다른 서비스를 이용하여 배포해야하기 때문이죠</p>
<h3 id="배포-계획">배포 계획</h3>
<ol>
<li>메인 서비스 - AWS RCS로 배포</li>
<li>관리자 서비스 - Vecel로 배포</li>
<li>디자인 시스템 - npm에 배포</li>
<li>스토리북 - Vecel로 배포</li>
</ol>
<p>지금처럼 아무 설정 안하고 Vercel에 이 모노레포를 통째로 배포를 하게 되면 Vercel에서 app 폴더안에 있는 서비스들을 각각의 도메인으로 배포를 해줍니다.
Vercel를 이용하여 전부 배포할 생각이면 진짜 이거만큼 개꿀 기능이 없는데 저는 위와 같은 배포 계획을 세웠기 때문에 ui 패키지를 바꿔줘야합니다.</p>
<p>어떻게 바꿀지와 왜 바꿔야하는지는 다음 글에서 다루겠습니다. </p>
<p>긴 글 읽어주셔서 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[다시 공부하는 Next.js...]]></title>
            <link>https://velog.io/@yunbh_0401/%EB%8B%A4%EC%8B%9C-%EA%B3%B5%EB%B6%80%ED%95%98%EB%8A%94-Next.js</link>
            <guid>https://velog.io/@yunbh_0401/%EB%8B%A4%EC%8B%9C-%EA%B3%B5%EB%B6%80%ED%95%98%EB%8A%94-Next.js</guid>
            <pubDate>Tue, 26 Nov 2024 02:11:27 GMT</pubDate>
            <description><![CDATA[<h1 id="잘-못-알고-있던-정보">잘 못 알고 있던 정보</h1>
<p>이전 글에서 Next 캐싱 관련해서 문제를 해결한 글을 작성한 적이 있습니다.
<a href="https://velog.io/@yunbh_0401/Next.js-13-App-Router%EC%97%90%EC%84%9C%EC%9D%98-%EC%BA%90%EC%8B%B1">수치플 링크</a></p>
<p>과거에 잘못된 학습으로 인해 문제를 해결하는 방식이 비효율적이었음을 뒤늦게 알게 되었고, 이번 글을 통해 그 과정을 반성하며 새로운 방향을 제안하고자 합니다. 특히 Next.js 15 버전 출시와 관련해 변화된 점도 함께 다뤄보겠습니다.</p>
<p>이전 글에서 다룬 문제는 다음과 같습니다:
서버에서 가져온 데이터가 Next.js의 캐싱 메커니즘에 의해 초기에는 이전 게시물이 나타나고, 몇 초 후에야 최신 게시물이 반영되며 화면이 깜빡거리는 현상이 발생했습니다.</p>
<p>이를 해결하기 위해 <strong>revalidatePath</strong>와 <strong>revalidateTag</strong>를 페이지가 렌더링될 때마다 실행하도록 설정해, 매번 새로운 데이터를 가져오도록 구현했습니다.</p>
<p>하지만 이 접근법에는 심각한 비효율이 있었습니다.</p>
<ol>
<li>서버는 이미 데이터를 캐싱하고 있음에도 불구하고,</li>
<li>페이지 렌더링 시마다 데이터를 다시 가져오는 반복적인 요청이 발생했습니다.</li>
</ol>
<p>이로 인해 불필요한 데이터 요청과 서버 자원 낭비라는 문제가 발생했습니다.</p>
<p>그럼 올바른 해결 방법은 무엇일까요?</p>
<h1 id="옳바른-해결-방법">옳바른 해결 방법</h1>
<p><a href="https://nextjs.org/docs/14/app/api-reference/functions/fetch">Next 14 버전 공식문서 링크</a></p>
<pre><code class="language-ts">fetch(`https://...`, { cache: &#39;force-cache&#39; | &#39;no-store&#39; })

fetch(`https://...`, { next: { revalidate: false | 0 | number } })</code></pre>
<p>Next에서 사용하는 fetch 함수에 옵션을 넣어 문제를 해결할 수 있습니다
fetch 함수 옵션을 어떻게 설정하는지 아주아주아주 잘 나와있더라구요</p>
<h2 id="캐시-확인-여부">캐시 확인 여부</h2>
<pre><code class="language-ts">fetch(`https://...`, { cache: &#39;force-cache&#39; | &#39;no-store&#39; })
</code></pre>
<p><strong><em>force-cache(기본값)</em></strong> - Next.js는 데이터 캐시에서 일치하는 요청을 찾습니다.
일치하는 항목이 있고 최신이면 캐시에서 반환됩니다.
일치하는 항목이 없거나 오래된 항목인 경우 Next.js는 원격 서버에서 리소스를 가져와 다운로드한 리소스로 캐시를 업데이트합니다</p>
<p><strong><em>no-store</em></strong> - Next.js는 캐시를 확인하지 않고 모든 요청에서 원격 서버에서 리소스를 가져오고, 다운로드한 리소스로 캐시를 업데이트하지 않습니다.</p>
<h2 id="캐시-조정">캐시 조정</h2>
<pre><code class="language-ts">fetch(`https://...`, { next: { revalidate: false | 0 | number } })</code></pre>
<p><strong>* false(기본값) *</strong> - 리소스를 무기한 캐시합니다. 의미적으로 .와 동일합니다 revalidate: Infinity. HTTP 캐시는 시간이 지남에 따라 이전 리소스를 제거할 수 있습니다.
<strong><em>0</em></strong> - 리소스가 캐시되는 것을 방지합니다.
<strong>* number *</strong> - (초) 리소스의 캐시 수명이 최대 n초여야 함을 지정합니다.</p>
<h3 id="feedb-코드-일부">FeedB 코드 일부</h3>
<pre><code class="language-ts">function httpClient() {
  async function get&lt;R&gt;(url: string, params?: Record&lt;string, any&gt;, headers?: HeadersInit, tags = [&quot;&quot;]) {
    const urlParams = new URLSearchParams(params).toString();
    const response = await fetch(`${BASE_URL}${url}?${urlParams}`, {
      headers,
      next: {
        tags: [...tags],
      },
    });
    const result: R = await response.json();
    return result;
  }</code></pre>
<p>피드비에서는 데이터를 가져오는 데 fetch 함수를 사용하고 있었고, 이것이 문제의 원인 중 하나였습니다.</p>
<p>fetch 함수의 cache 옵션은 기본값으로 데이터를 무기한으로 캐싱합니다.
이로 인해 다음과 같은 문제가 발생했습니다:</p>
<ol>
<li>서버에서 이전 데이터를 받아와 캐싱합니다.</li>
<li>캐싱된 데이터를 기반으로 화면이 렌더링되고,</li>
<li>이후 React Query로 새로운 데이터를 받아와 화면을 갱신하면서, 이전 데이터가 잠깐 나타났다 사라지는 깜빡거림이 발생했습니다.
즉, fetch를 아무런 설정 없이 사용하면 Next.js가 자동으로 데이터를 캐싱하고, 이를 가져다 사용하게 됩니다.</li>
</ol>
<p>이러한 동작은 처음에는 편리해 보일 수 있지만, 실시간 데이터가 필요한 경우 캐싱된 오래된 데이터를 계속 가져오는 문제가 됩니다.</p>
<p>이렇게 fetch 함수에 아무것도 안 쓰면 Next가 알아서 캐싱도 해주고 캐싱된 데이터도 가져와주니 참 고마울만도한데 이런건 직접 설정할 수 있게 해줘야죠;;
저처럼 똑같은 문제와 생각을 한 사람들이 많았는지 15버전에서는 새롭게 바뀝니다. 그 설명은 마지막에 하죠</p>
<h3 id="다른-프로젝트-코드">다른 프로젝트 코드</h3>
<pre><code class="language-ts">  async function get&lt;R&gt;(url: string, headers: HeadersInit, params?: Record&lt;string, any&gt;): Promise&lt;R&gt; {
    const urlParams = new URLSearchParams(params).toString();

    try {
      const response = await fetch(`${BASE_URL}/api/${url}?${urlParams}`, {
        method: &quot;GET&quot;,
        headers: headers,
        credentials: &quot;include&quot;,
        cache: &quot;no-store&quot;,
      });
      ...
    }
  }</code></pre>
<p>이런식으로 fetch 함수에 <code>cache: &quot;no-store&quot;,</code> 옵션을 넣어줘 항상 새로운 데이터를 가져올 수 있도록 할 수 있습니다</p>
<p>revalidatePath, revalidateTag 함수는 데이터를 가져와 캐싱을 한 상태일 때 새로운 값을 가져오고 캐싱된 데이터를 초기화 시켜주기 위해 사용하는 것입니다.
여러분들은 저처럼 이상하게 사용하지 마십시오</p>
<h1 id="nextjs-15에서-fetch-함수의-캐싱-동작-변경">Next.js 15에서 fetch 함수의 캐싱 동작 변경</h1>
<p>[Next 15 버전 공식문서 링크]
(<a href="https://nextjs.org/docs/app/api-reference/functions/fetch">https://nextjs.org/docs/app/api-reference/functions/fetch</a>)</p>
<p>Next.js 15버전에서는 fetch 함수의 cache 옵션 기본값이 변경되었습니다.
이를 통해 개발자들이 실시간 데이터를 더 쉽게 다룰 수 있게 되었으며, 새로운 기본 동작은 다음과 같습니다:</p>
<pre><code class="language-ts">fetch(`https://...`, { cache: &#39;force-cache&#39; | &#39;no-store&#39; })</code></pre>
<p>캐시를 확인하지 않고 모든 요청에서 항상 원격 서버에서 리소스를 가져옵니다.
다운로드한 리소스로 캐시를 업데이트하지 않습니다.</p>
<ul>
<li><p><strong><em>no-store (기본값)</em></strong> </p>
<ul>
<li>Next.js는 캐시를 확인하지 않고 모든 요청에서 원격 서버에서 리소스를 가져오고, 다운로드한 리소스로 캐시를 업데이트하지 않습니다.</li>
</ul>
</li>
<li><p><strong><em>force-cache</em></strong> </p>
<ul>
<li>Next.js는 데이터 캐시에서 일치하는 요청을 찾습니다.</li>
<li>일치하는 항목이 있고 최신이면 캐시에서 반환됩니다.</li>
<li>일치하는 항목이 없거나 오래된 항목인 경우 Next.js는 원격 서버에서 리소스를 가져와 다운로드한 리소스로 캐시를 업데이트합니다</li>
</ul>
</li>
</ul>
<p>이전에는 fetch 함수에서 cache 옵션을 별도로 설정하지 않으면 기본값으로 데이터를 무기한 캐싱했습니다.
하지만 15버전에서는 항상 새로운 데이터를 불러오도록 기본 동작이 변경되었습니다.</p>
<p>이 업데이트 덕분에, 실시간 데이터를 다뤄야 하는 상황에서 추가적인 설정 없이도 깜빡거림 없이 최신 데이터를 가져올 수 있습니다.
또한, 캐싱이 필요한 경우에는 force-cache와 같은 명시적인 설정을 통해 세밀하게 제어할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[API 요청 시 인증 토큰 문제 해결]]></title>
            <link>https://velog.io/@yunbh_0401/API-%EC%9A%94%EC%B2%AD-%EC%8B%9C-%EC%9D%B8%EC%A6%9D-%ED%86%A0%ED%81%B0-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@yunbh_0401/API-%EC%9A%94%EC%B2%AD-%EC%8B%9C-%EC%9D%B8%EC%A6%9D-%ED%86%A0%ED%81%B0-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sat, 17 Aug 2024 09:07:23 GMT</pubDate>
            <description><![CDATA[<h1 id="🚨-이슈-발생">🚨 이슈 발생</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/d1e6000a-ebd2-4ebe-908f-0405fcba9ac8/image.png" alt=""></p>
<ol>
<li><p>로그인을 하고 인증 토큰이 필요한 API 요청에서 오류가 계속 나는 걸 볼 수 있습니다.
로그인 후 토큰은 저희가 의도한 대로 로컬 스토리지에 잘 저장되어 있었지만 계속 오류가 발생하였고, 새로고침을 해야 오류가 발생하지 않고 서버와 통신할 수 있었습니다.</p>
</li>
<li><p>메인 페이지에서 프리패칭하여 프로젝트 리스트 데이터를 가져오고 있는데 isLiked 속성이 계속 false로 오는 문제가 발생하였습니다.
그래서 메인 페이지에서는 자신이 누른 게시물에 좋아요를 확인할 수 없는 문제가 있었습니다.</p>
</li>
</ol>
<h1 id="🤔-원인-분석">🤔 원인 분석</h1>
<h2 id="첫-번째-문제">첫 번째 문제</h2>
<pre><code class="language-ts">import { HEADER } from &quot;../_constants/HeaderToken&quot;;

  getCurrentUserId: async () =&gt; {
    return await httpClient().get&lt;CurrentUserIdType&gt;(&quot;/profile&quot;, { &quot;&quot;: &quot;&quot; }, HEADER.headers);
  },</code></pre>
<pre><code class="language-ts">import { getToken } from &quot;../_utils/handleToken&quot;;

export const HEADER = {
  headers: {
    Authorization: &quot;Bearer &quot; + getToken()?.accessToken,
  },
  multipartHeaders: {
    Authorization: &quot;Bearer &quot; + getToken()?.accessToken,
    &quot;Content-Type&quot;: &quot;multipart/form-data&quot;,
  },
  applicationHeaders: {
    Authorization: &quot;Bearer &quot; + getToken()?.accessToken,
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
};</code></pre>
<p>첫 번째 문제부터 분석을 해보면 위와 같은 코드에서 <code>getToken</code> 함수를 이용해 로컬 스토리지에서 토큰을 가져와 <code>HEADER</code> 객체를 만든 후 API 로직에 import로 가져와 사용하고 있다.</p>
<p>이렇게 보면 문제가 없어 보여서 문제를 찾는데 좀 까다로웠다.</p>
<p>이 상황에서 <code>getToken</code>함수가 로그인 후에 토큰을 제대로 가져오지 못한다.
이유는 <code>HEADER</code> 객체가 정의될 때 이미 <code>getToken</code> 함수가 실행되기 때문입니다. 이 경우, HEADER 객체가 처음 정의될 때는 로컬 스토리지에 토큰이 없을 수 있으며, 이후에 로그인 후에 토큰이 추가되어도 초기 HEADER 객체는 갱신되지 않습니다.</p>
<p>두번 째 문제도 위와 같은 문제로 일어난 줄 알고 이것만 해결하면 둘 다 해결될 줄 알았지만 그건 내 착각이었다.</p>
<h2 id="두번-째-문제">두번 째 문제</h2>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/0b76df61-0d7a-4ccf-8e5d-d14b4a5c35b0/image.png" alt=""></p>
<p>isLiked = 자신이 게시물에 좋아요을 눌렀는지 확인하는 속성</p>
<p>스웨거로 테스트를 해본 결과 프로젝트 리스트 데이터를 요청할 때 헤더에 토큰이 없으면 isLiked 속성 값은 무조건 다 <code>false</code>로 오고, 토큰을 포함해서 요청을 하면 isLiked <code>true</code>로 잘 온다.</p>
<pre><code class="language-ts">export const projectApi = {
  getProjectList: async ({
    page = 1,
    size = 12,
    limit = 0,
    searchString = &quot;&quot;,
    projectTechStacks = [],
    sortCondition = &quot;RECENT&quot;,
  }: ProjectListParams) =&gt; {
    return await httpClient().get&lt;ProjectResponseType&gt;(
      &quot;/projects&quot;,
      {
        sortCondition,
        projectTechStacks,
        searchString,
        page,
        size,
        limit,
      },
      HEADER.applicationHeaders,
      [&quot;pojectList&quot;]
    );
  },</code></pre>
<p>두 번째 문제도 비슷하게 <code>HEADER</code> 객체를 전달하여 헤더에 인증 토큰을 담아 서버에 데이터 요청을 하는 방식으로 구성되어 있습니다.
앞서 말한 첫 번째 문제로 로그인 직후에는 토큰이 포함되지 않는 오류가 있기 때문에 isLiked <code>false</code>로 오는 게 이해가 되었지만 새로고침 후에도 isLiked는 계속 <code>false</code> 값으로 돌아왔다.</p>
<pre><code class="language-ts">async function MainPage() {
  revalidateTagAction(&quot;pojectList&quot;);
  const queryClient = getQueryClient();

  const projectListQuery = projectQueryKeys.list({ page: 1, size: 16 });

  await queryClient.prefetchInfiniteQuery({
    queryKey: projectListQuery.queryKey,
    queryFn: projectListQuery.queryFn,
    initialPageParam: 1 as never,
    getNextPageParam: (lastPage: any) =&gt; {
      const { customPageable } = lastPage;
      if (customPageable.hasNext) {
        return customPageable.page + 1; // 다음 페이지 번호 반환
      }
      return undefined; // 더 이상 페이지가 없으면 undefined 반환
    },
  });

  const dehydratedState = dehydrate(queryClient);

  return (
    &lt;HydrationBoundary state={dehydratedState}&gt;
      &lt;main className=&quot;mx-auto my-16 grid w-[1200px] grid-cols-[230px_minmax(976px,_1fr)] grid-rows-[100px_minmax(800px,_1fr)]&quot;&gt;
        &lt;SelectStack /&gt;
      &lt;/main&gt;
    &lt;/HydrationBoundary&gt;
  );
}</code></pre>
<p>문제의 원인은 프리패칭을 하는 과정에서 발생했던 것이었다. 위 코드처럼 프리패칭을 하게 되면 서버에서 데이터 패칭을 하게 된다.
서버에서 데이터 패칭이 실행되기 때문에 클라이언트에 있는 코컬스토리지에는 접근이 불가능했던 것이다.</p>
<p>그럼 프리패칭을 안하면 되는거 아닌가? 라고 생각할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/a75564af-a49b-4a6e-a40e-cb9d1c650071/image.png" alt=""></p>
<p>프리패칭을 안 하고 클라이언트에서 데이터 요청을 하게 되면 사진을 보면 알 수 있듯이 LCP 지표가 3초로 측정되는 것을 볼 수 있을 것이다.
앞으로 메인 페이지에서는 더 많은 사진 데이터를 가져와 화면에 보여줄 것이기 때문에 적어도 프로젝트 리스트를 서버에서 가져오는 요청은 프리패칭이 되야한다고 생각했습니다.</p>
<h1 id="🔆-해결-방법">🔆 해결 방법</h1>
<h2 id="첫-번째-문제-해결-방법">첫 번째 문제 해결 방법</h2>
<pre><code class="language-ts">  getCurrentUserId: async () =&gt; {
    const HEADER = getHeaders();
    return await httpClient().get&lt;CurrentUserIdType&gt;(&quot;/profile&quot;, {}, HEADER.headers);
  },</code></pre>
<pre><code class="language-ts">import { getToken } from &quot;../_utils/handleToken&quot;;

export const getHeaders = () =&gt; {
  const token = getToken()?.accessToken;

  return {
    headers: {
      Authorization: &quot;Bearer &quot; + token,
    },
    multipartHeaders: {
      Authorization: &quot;Bearer &quot; + token,
      &quot;Content-Type&quot;: &quot;multipart/form-data&quot;,
    },
    applicationHeaders: {
      Authorization: &quot;Bearer &quot; + token,
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
  };
};
</code></pre>
<p>첫 번째 문제는 데이터 요청마다 <code>getHeaders</code> 함수를 호출하여 로컬 스토리지에 있는 토큰을 가져올 수 있도록 로직을 개선해 보았습니다.</p>
<h2 id="두-번째-문제-해결-방법">두 번째 문제 해결 방법</h2>
<pre><code class="language-ts">
export const projectApi = {
  getProjectList: async (
    {
      ...
    }: ProjectListParams,
    token?: string
  ) =&gt; {
    const applicationHeaders = {
      Authorization: &quot;Bearer &quot; + token,
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    };
    return await httpClient().get&lt;ProjectResponseType&gt;(
      &quot;/projects&quot;,
      {
        sortCondition,
        projectTechStacks,
        searchString,
        page,
        size,
        limit,
      },
      applicationHeaders,
      [&quot;pojectList&quot;]
    );
  },</code></pre>
<pre><code class="language-ts">
async function MainPage() {

  const queryClient = getQueryClient();
  const cookieStore = cookies();
  const ACCESS_TOKEN = cookieStore.get(&quot;ACCESS_TOKEN&quot;);

  const projectListQuery = projectQueryKeys.list({ page: 1, size: 16 }, ACCESS_TOKEN?.value);

  await queryClient.prefetchInfiniteQuery({
    queryKey: projectListQuery.queryKey,
    queryFn: projectListQuery.queryFn,
    initialPageParam: 1 as never,
    getNextPageParam: (lastPage: any) =&gt; {
      const { customPageable } = lastPage;
      if (customPageable.hasNext) {
        return customPageable.page + 1; // 다음 페이지 번호 반환
      }
      return undefined; // 더 이상 페이지가 없으면 undefined 반환
    },
  });

  const dehydratedState = dehydrate(queryClient);

  return (
    &lt;HydrationBoundary state={dehydratedState}&gt;
        ...
    &lt;/HydrationBoundary&gt;
  );
}

export default MainPage;</code></pre>
<p>두 번째 문제는 로그인 후 인증 토큰을 쿠키에 저장할 수 있도록 로직을 변경시키고, 메인 페이지에서 쿠키에 접근하여 저장된 토큰을 가져와 프로젝트 리스트 API 로직에 매개변수로 전달하여 통신이 이루어지도록 로직을 개선해 보았습니다.</p>
<p>알아보니깐 쿠키는 서버와 클라이언트 모두가 접근이 가능하기 때문에 위와 같은 방법으로 문제를 해결해 보았습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 13 App Router에서의 캐싱]]></title>
            <link>https://velog.io/@yunbh_0401/Next.js-13-App-Router%EC%97%90%EC%84%9C%EC%9D%98-%EC%BA%90%EC%8B%B1</link>
            <guid>https://velog.io/@yunbh_0401/Next.js-13-App-Router%EC%97%90%EC%84%9C%EC%9D%98-%EC%BA%90%EC%8B%B1</guid>
            <pubDate>Wed, 24 Jul 2024 14:39:23 GMT</pubDate>
            <description><![CDATA[<h1 id="캐싱이란">캐싱이란?</h1>
<blockquote>
<p>캐싱(Caching)은 자주 요청되는 데이터나 연산 결과를 임시 저장해 두고, 이후에 동일한 요청이 있을 때 저장된 데이터를 재사용하여 응답 속도를 높이고 서버 자원을 절약하는 기술입니다. 이는 웹 개발에서 매우 중요한 성능 최적화 전략 중 하나입니다.</p>
</blockquote>
<p>피드비 프로젝트는 Next.js 14버전 App Router 방식을 사용해 진행하고 있습니다.</p>
<p>Next.js App Router에 캐싱 특징은 아래와 같습니다.</p>
<h2 id="1-자동-캐싱">1. 자동 캐싱</h2>
<p>App Router는 서버 컴포넌트의 데이터를 자동으로 캐싱할 수 있습니다. 이 기능은 자주 변경되지 않는 데이터를 캐시하여 응답 속도를 높입니다. 예를 들어, 사용자가 자주 조회하는 블로그 게시글이나 제품 정보 등을 캐싱할 수 있습니다.</p>
<h2 id="2-캐싱-옵션">2. 캐싱 옵션</h2>
<p>개발자는 각 컴포넌트나 데이터 페칭 로직에 대해 캐싱 옵션을 설정할 수 있습니다. 넥스트.js는 revalidate 설정을 통해 캐시된 데이터를 주기적으로 갱신할 수 있습니다. 이는 데이터가 일정 시간 동안 유효하도록 설정하고, 그 이후에는 새로운 데이터를 가져오도록 하는 방식입니다.</p>
<h2 id="3-브라우저-캐싱">3. 브라우저 캐싱</h2>
<p>Next.js는 정적 자산(이미지, CSS, JavaScript 파일 등)을 브라우저 캐시에 저장하도록 최적화할 수 있습니다. 이를 통해 사용자가 웹사이트를 재방문할 때 로딩 속도가 빨라집니다.</p>
<p>위와 같은 내용을 학습하고 피드비 프로젝트에서 발생한 이슈를 해결해보았습니다.</p>
<h1 id="🚨-이슈-발생">🚨 이슈 발생</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/f3fb1613-2ee2-4030-a15d-d65701f342e0/image.gif" alt=""></p>
<p>사진을 보시면 새로고침을 했을 경우 프로젝트 리스트 데이터를 새로 불러와 컴포넌트가 생성되고 있습니다.
근데 맨 첫번 째 게시물이 처음에는 없다가 뒤늦게 나타나는 문제를 보실 수 있을겁니다.</p>
<h1 id="🤔-원인-분석">🤔 원인 분석</h1>
<p>위 문제는 Next에서 제공해주는 자동 캐싱 기능 때문에 일어난 문제입니다.</p>
<pre><code class="language-ts"> const projectListQuery = projectQueryKeys.list({ page: 1, size: 16 });

  const { data, fetchNextPage } = useInfiniteQuery({
    queryKey: projectListQuery.queryKey,
    queryFn: ({ pageParam = 1 }) =&gt; projectApi.getProjectList({ ...projectState, page: pageParam }),
    initialPageParam: 1,
    getNextPageParam: lastPage =&gt; {
      const { customPageable } = lastPage;
      if (customPageable.hasNext) {
        return customPageable.page + 1; // 다음 페이지 번호 반환
      }
      return undefined; // 더 이상 페이지가 없으면 undefined 반환
    },
  });</code></pre>
<p>현제 프로젝트 리스트 데이터는 React-Query로 불러오고 데이터를 캐싱하고 있습니다.</p>
<p>근데 Next.js와 React Query는 데이터 캐싱이 서로 다른 방식으로 이루어지기 때문에 캐싱된 데이터를 각각 업데이트 해줘야합니다.</p>
<h2 id="넥스트js-app-router의-캐싱">넥스트.js App Router의 캐싱</h2>
<p>서버 캐싱
서버 컴포넌트에서 데이터를 가져올 때 서버 측에서 캐싱을 할 수 있습니다. 예를 들어, 빌드 시점에 데이터를 페칭하고 이를 정적으로 캐싱하거나, revalidate 속성을 사용해 주기적으로 데이터를 갱신할 수 있습니다.</p>
<h2 id="react-query의-캐싱">React Query의 캐싱</h2>
<p>클라이언트 메모리 캐싱
React Query는 클라이언트 측에서 데이터 요청 결과를 캐시합니다. 기본적으로 브라우저 메모리에 저장되며, 특정 키에 따라 데이터를 관리합니다.</p>
<p>현재 클라이언트 메모리 캐싱된 데이터는 React Query에서 제공해주는 <code>queryClient.invalidateQueries</code> 메서드를 이용해서 프로젝트 리스트 데이터에 변경 사항이 생기면 캐싱된 데이터를 무효화 시켜주는 작업은 되어있는 상태입니다.</p>
<p>그럼 이제 Next.js가 자동으로 해준 서버 캐싱을 무효화만 시켜주면 해결되는 문제입니다.</p>
<h1 id="🔆-해결-방법">🔆 해결 방법</h1>
<p>Next.js에는 서버 캐싱을 무효화 시켜줄 방법이 두 가지가 있습니다.</p>
<h2 id="1-revalidatepath">1. revalidatePath</h2>
<pre><code class="language-ts">revalidatePath(path: string, type?: &#39;page&#39; | &#39;layout&#39;): void;</code></pre>
<p><code>revalidatePath</code> 함수는 두개의 인자를 넣어줘야합니다. 첫 번째 인자는 revalidatePath로 어떤 URL 즉 어떤 경로에 있는 캐싱된 데이터를 무효화할 것인지 사용자가 정하여 넣어주면 됩니다.
두번째는 정해진 경로에서 page 범위까지 데이터를 무효화할 것인지 아니면 layout까지 데이터를 무효화할 것인지 사용자가 결정하여 넣어주면 됩니다.</p>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-ts">&quot;use server&quot;;

import { revalidatePath } from &quot;next/cache&quot;;

export async function revalidatePathAction(url: string, type: &quot;page&quot; | &quot;layout&quot; = &quot;page&quot;) {
  revalidatePath(url, type);
}</code></pre>
<p>저는 따로 util 함수로 빼어 다른 곳에서도 사용할 수 있도록 했습니다. 
<code>revalidatePathAction</code> 함수를 선언해 매개변수로 URL과 type을 받을 수 있게하였습니다.</p>
<p>이제 URL에 &quot;/main&quot;을 전달하고 type에는 &quot;page&quot;를 전달하여 메인 페이지에 캐싱된 데이터를 초기화하여 발생했던 이슈를 해결할 수 있습니다.</p>
<p>그렇지만 이 방법에 대한 단점이 하나 있습니다.</p>
<h3 id="단점">단점</h3>
<p>프로젝트 리스트 데이터 무효화 시키면 되는데 <code>revalidatePath</code> 함수를 사용하면 메인 페이지에 캐싱된 데이터 모두가 무효화처리 되어 갱신이 필요하지 않는 데이터까지 초기화가 되면서 이런 데이터까지 다시 캐싱되는 비효율적인 작업이 이루워지게 됩니다.</p>
<p>그래서 저는 두번째 방법으로 이슈를 해결했습니다.</p>
<h2 id="2-revalidatetag">2. revalidateTag</h2>
<pre><code class="language-ts">revalidateTag(tag: string): void;</code></pre>
<p><code>revalidateTag</code>가 해주는 역할은 <code>revalidatePath</code>와 비슷합니다. 차이점은 <code>revalidatePath</code>은 페이지나 레이아웃 단위로 캐싱된 데이터를 무효화 시켜주지만 <code>revalidateTag</code>는 캐싱된 데이터에서 사용자가 무효화 싶은 데이터 태그를 찾아 무효화 시켜주는 것입니다.</p>
<p>이 방법은 React-query에서 데이터를 Key로 관리하는 것과 비슷하다고 봅니다.</p>
<p>그러면 API 통신으로 가져온 데이터에 태그를 붙여줘야하는데 어떻게 해줄까요??</p>
<h3 id="태그-지정">태그 지정</h3>
<pre><code class="language-ts">fetch(url, { next: { tags: [...] } });</code></pre>
<p><code>fetch</code>를 이용해서 데이터를 불러올 때 태그를 지정할 수 있습니다.</p>
<h3 id="사용-예시-1">사용 예시</h3>
<pre><code class="language-ts">&quot;use server&quot;;

import { revalidatePath, revalidateTag } from &quot;next/cache&quot;;

export async function revalidatePathAction(url: string, type: &quot;page&quot; | &quot;layout&quot; = &quot;page&quot;) {
  revalidatePath(url, type);
}

export async function revalidateTagAction(tag: string) {
  revalidateTag(tag);
}</code></pre>
<p>일단 먼저 <code>revalidateTagAction</code>를 선언하고 인자값으로 사용자가 원하는 태그를 받아 <code>revalidateTag</code>함수를 사용할 수 있도록 해주었습니다. </p>
<pre><code class="language-ts">  async function get&lt;R&gt;(url: string, params?: Record&lt;string, any&gt;, headers?: HeadersInit, tags = [&quot;&quot;]) {
    const urlParams = new URLSearchParams(params).toString();
    const response = await fetch(`${BASE_URL}${url}?${urlParams}`, {
      headers,
      next: {
        tags: [...tags],
      },
    });
    const result: R = await response.json();
    return result;
  }
</code></pre>
<p>다른 사람들도 사용할 수 있게 지정할 태그를 매개변수로 받아서 지정할 수 있도록 해주었습니다.</p>
<pre><code class="language-ts">getProjectList: async ({
    page = 1,
    size = 12,
    limit = 0,
    searchString = &quot;&quot;,
    projectTechStacks = [],
    sortCondition = &quot;RECENT&quot;,
  }: ProjectListParams) =&gt; {
    return await httpClient().get&lt;ProjectResponseType&gt;(
      &quot;/projects&quot;,
      {
        sortCondition,
        projectTechStacks,
        searchString,
        page,
        size,
        limit,
      },
      HEADER.applicationHeaders,
      [&quot;pojectList&quot;]
    );
  },</code></pre>
<p>그런 다음에 프로젝트 리스트를 불러오는 로직에 <code>pojectList</code>라는 태그 이름을 지정하여 넘겨주었습니다.</p>
<pre><code class="language-ts">import { revalidateTagAction } from &quot;../_utils/revalidationAction&quot;;

async function MainPage() {
  revalidateTagAction`(&quot;pojectList&quot;);

  {&#39;&#39;&#39;}

  return (
    &lt;HydrationBoundary state={dehydratedState}&gt;
      &lt;main className=&quot;mx-auto my-16 grid w-[1200px] grid-cols-[230px_minmax(976px,_1fr)] grid-rows-[100px_minmax(800px,_1fr)]&quot;&gt;
        &lt;SelectStack /&gt;
      &lt;/main&gt;
    &lt;/HydrationBoundary&gt;
  );
}

export default MainPage;</code></pre>
<p>마지막으로 메인페이지가 랜더링이 될 때 <code>revalidateTagAction</code> 함수를 실행시켜 사용자가 항상 최신 데이터만 볼 수 있도록 해주었습니다.</p>
<p>그리고 이 내용을 팀원들과 공유하여 프로젝트 게시물이 생성될 때 캐싱된 데이터를 무효화 시킬 수 있도록 할 예정입니다.</p>
<h1 id="해결">해결</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/e51b13e2-6beb-4f3a-9a35-42927ad1b013/image.gif" alt=""></p>
<blockquote>
<p>참고 - Next 공식 문서
<a href="https://nextjs.org/docs/app/api-reference/functions/revalidatePath">https://nextjs.org/docs/app/api-reference/functions/revalidatePath</a>
<a href="https://nextjs.org/docs/app/api-reference/functions/revalidateTag">https://nextjs.org/docs/app/api-reference/functions/revalidateTag</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Toast 메시지 구현]]></title>
            <link>https://velog.io/@yunbh_0401/Toast-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@yunbh_0401/Toast-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Thu, 18 Jul 2024 10:40:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>FeedB는 개발자들이 자신이 만든 토이 프로젝트나 사이드 프로젝트를 공유하고, 다른 사람들이 피드백을 남길 수 있는 서비스입니다.</p>
</blockquote>
<p>Toast 메시지 기능은 사용자 인터페이스에서 중요한 역할을 합니다. 예를 들어 사용자에게 즉각적인 피드백을 제공하여, 작업이 성공적으로 완료되었는지, 오류가 발생했는지 등을 실시간으로 알려줄 수 있습니다. </p>
<p>이는 사용자의 행동에 대한 명확한 결과를 전달하여 사용자 경험을 향상시킬 수 있습니다.</p>
<p>그래서 이번에는 Toast UI를 어떻게 구현했는지 설명해드리곘습니다.</p>
<h1 id="step-1---toastcontext">Step 1 - ToastContext</h1>
<pre><code class="language-ts">&quot;use client&quot;;

import { createContext, useContext, useState, ReactNode } from &quot;react&quot;;

interface Toast {
  id: number;
  message: string;
  type: &quot;success&quot; | &quot;error&quot;;
}

interface ToastContextType {
  toasts: Toast[];
  addToast: (message: string, type: &quot;success&quot; | &quot;error&quot;) =&gt; void;
  removeToast: (id: number) =&gt; void;
}

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

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

  const addToast = (message: string, type: &quot;success&quot; | &quot;error&quot;) =&gt; {
    setToasts([...toasts, { id: Date.now(), message, type }]);
  };

  const removeToast = (id: number) =&gt; {
    setToasts(toasts.filter(toast =&gt; toast.id !== id));
  };

  return &lt;ToastContext.Provider value={{ toasts, addToast, removeToast }}&gt;{children}&lt;/ToastContext.Provider&gt;;
};

export const useToast = (): ToastContextType =&gt; {
  const context = useContext(ToastContext);
  if (!context) {
    throw new Error(&quot;useToast는 ToastProvider안에서 사용해주세요&quot;);
  }
  return context;
};
</code></pre>
<p>왜 Toast 메시지 기능을 context로 구현했는가?</p>
<h3 id="1-상태-관리의-일관성-유지">1. 상태 관리의 일관성 유지</h3>
<p>콘텍스트를 사용하면, 애플리케이션의 어떤 부분에서도 손쉽게 Toast 메시지를 추가하거나 삭제할 수 있습니다. 전역 상태를 관리하기 때문에, 상태의 일관성을 유지하고, 다양한 컴포넌트 간에 데이터 전달을 효율적으로 할 수 있습니다.</p>
<h3 id="2-페이지-이동과-무관한-상태-유지">2. 페이지 이동과 무관한 상태 유지</h3>
<p>Next.js와 같은 프레임워크에서는 페이지 간 이동이 발생할 때 상태가 초기화되는 경우가 많습니다. 하지만 콘텍스트를 사용하면, Toast 메시지와 같은 상태를 글로벌하게 관리할 수 있어, 페이지 이동 후에도 메시지 상태가 유지됩니다. 이는 사용자 경험을 향상시키는 중요한 요소입니다.</p>
<h3 id="3-컴포넌트-간의-의존성-감소">3. 컴포넌트 간의 의존성 감소</h3>
<p>Toast 메시지를 콘텍스트로 분리함으로써, 개별 컴포넌트는 Toast 메시지를 직접 관리할 필요가 없어집니다. 대신, useToast 훅을 사용하여 필요한 곳에서 손쉽게 Toast 기능을 사용할 수 있습니다. 이는 코드의 가독성을 높이고, 컴포넌트 간의 의존성을 줄여 유지보수를 용이하게 합니다</p>
<h3 id="4-유연한-메시지-관리">4. 유연한 메시지 관리</h3>
<p>addToast 함수와 removeToast 함수를 통해, 동적으로 Toast 메시지를 관리할 수 있습니다. 메시지의 추가 및 삭제가 직관적이고, 필요에 따라 메시지의 타입(성공, 오류 등)을 쉽게 설정할 수 있어, 다양한 상황에 맞는 메시지를 제공할 수 있습니다.</p>
<hr>
<h1 id="step-2---layout-적용">Step 2 - layout 적용</h1>
<pre><code class="language-ts">export const metadata: Metadata = {
...
};

export default function RootLayout({
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;) {
  return (
    &lt;html lang=&quot;ko&quot;&gt;
      &lt;head&gt;
        &lt;Script src=&quot;https://developers.kakao.com/sdk/js/kakao.js&quot; strategy=&quot;beforeInteractive&quot; /&gt;
      &lt;/head&gt;
      &lt;body&gt;
        &lt;div id=&quot;modal&quot; /&gt;
        &lt;Providers&gt;
          &lt;LoginProvider&gt;
            &lt;ToastProvider&gt;
              &lt;Header /&gt;
              &lt;LoadingWrapper&gt;
                {children}
                &lt;ToastContainer /&gt;
              &lt;/LoadingWrapper&gt;
            &lt;/ToastProvider&gt;
          &lt;/LoginProvider&gt;
        &lt;/Providers&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<p>위에서 만든 <code>ToastProvider</code>를 전역으로 사용할 수 있게 최상단 layout.ts 파일에 넣어주었습니다.</p>
<p>다음은 <code>ToastContainer</code> 컴포넌트에 대해서 설명드리겠습니다.</p>
<hr>
<h1 id="step-3---toastcontainer">Step 3 - ToastContainer</h1>
<pre><code class="language-ts">&quot;use client&quot;;

import React from &quot;react&quot;;
import { useToast } from &quot;@/app/_context/ToastContext&quot;;
import ModalPortal from &quot;@/app/_utils/ModalPortal&quot;;
import Toast from &quot;./Toast&quot;;

function ToastContainer() {
  const { toasts, removeToast } = useToast();

  return (
    &lt;ModalPortal&gt;
      &lt;div className=&quot;fixed bottom-5 left-1/2 z-50 w-[384px] -translate-x-1/2 transform&quot;&gt;
        {toasts.map(toast =&gt; (
          &lt;Toast key={toast.id} message={toast.message} type={toast.type} onClose={() =&gt; removeToast(toast.id)} /&gt;
        ))}
      &lt;/div&gt;
    &lt;/ModalPortal&gt;
  );
}

export default ToastContainer;
</code></pre>
<p><code>ToastContainer</code>는 이름으로 예상할 수 있듯이 Toast 메시지를 감싸주는 컴포넌트입니다.</p>
<p>사용자가 여러 작업을 빠르게 하다 보면 자연스럽게 Toast 메시지는 여러개가 쌓이게 될 것입니다. 그걸 원하는 위치에 띄워줄 수 있도록 <code>ModalPortal</code>로 감싸고 그 안에서 맵핑을 돌려 여러개에 Toast 메시지가 랜더링되게 해주었습니다.</p>
<p><code>ModalPortal</code>은 React에서 제공해주는 createPortal를 사용해서 따로 구현해둔 컴포넌트입니다. 이걸 사용해 감싸준 이유는 Toast 메시지가 부모 컴포넌트에 스타일 영향을 받지 않도록 하기 위해서 넣어주었습니다.</p>
<h1 id="step-4---toast">Step 4 - Toast</h1>
<pre><code class="language-ts">&quot;use client&quot;;

interface ToastProps {
  message: string;
  type: &quot;success&quot; | &quot;error&quot;;
  onClose: () =&gt; void;
}

const Toast = ({ message, type, onClose }: ToastProps) =&gt; {
  const [show, setShow] = useState(false);
  const [isExiting, setIsExiting] = useState(false);

  useEffect(() =&gt; {
    // 컴포넌트가 마운트되면 표시 애니메이션을 시작합니다.
    setShow(true);

    // 3초 후에 토스트를 사라지기 시작하게 설정합니다.
    const hideTimeout = setTimeout(() =&gt; {
      setIsExiting(true); // 애니메이션 시작
    }, 3000);

    // 애니메이션이 끝난 후에 onClose 호출
    const onCloseTimeout = setTimeout(() =&gt; {
      if (isExiting) {
        onClose();
      }
    }, 3500); // 애니메이션이 끝난 후 추가로 0.5초 후 호출

    return () =&gt; {
      clearTimeout(hideTimeout);
      clearTimeout(onCloseTimeout);
    };
  }, [isExiting, onClose]);

  const backgroundColor = type === &quot;success&quot; ? &quot;border-blue-500 bg-blue-100&quot; : &quot;border-red-500 bg-red-100&quot;;

  return (
    &lt;div
      className={`transform transition-all duration-500 ease-in-out ${show ? (isExiting ? &quot;-translate-y-10 scale-95 opacity-0&quot; : &quot;translate-y-0 scale-100 opacity-100&quot;) : &quot;translate-y-5 scale-95 opacity-0&quot;}  ${backgroundColor} mb-2 flex items-center gap-2 rounded-xl border border-solid p-4`}&gt;
      {type === &quot;success&quot; ? (
        &lt;Image src={checkCircle} alt=&quot;성공&quot; width={24} priority /&gt;
      ) : (
        &lt;Image src={errorCircle} alt=&quot;실패&quot; width={24} priority /&gt;
      )}
      &lt;p className=&quot;text-gray-900&quot;&gt;{message}&lt;/p&gt;
      &lt;Image
        src={closeIcon}
        alt=&quot;닫기 아이콘&quot;
        width={24}
        height={24}
        priority
        className=&quot;absolute right-4 cursor-pointer&quot;
        onClick={onClose}
      /&gt;
    &lt;/div&gt;
  );
};

export default Toast;
</code></pre>
<p>마지막으로 Toast 컴포넌트 설명해드리겠습니다.</p>
<p>다른 곳에 비해 상태를 많이 관리하고 있는데 
<code>const [show, setShow] = useState(false)</code> 상태는 Toast 메시지가 화면에 보이는 여부를 관리하는 상태값 입니다.</p>
<p>useEffect를 이용해서 컴포넌트가 랜더링 되면서 <code>setShow(true)</code> 훅이 실행되고 그 다음에 Toast 메시지가 화면에 보여지게 됩니다.</p>
<p><code>show</code>값을 처음부터 <code>true</code>로 주어 화면에 바로 보여지게 할 수도 있지만 그렇게 한다면 처음 등장하는 애니메이션이 제대로 작동하지 않아 위와 같은 방법으로 구현을 한겁니다.</p>
<p>그리고 3초뒤에는 사라지는 애니매이션이 실행되어 화면에서 사라지고, 3.5초 뒤에는 쌓여있던 Toast 메시지를 삭제하게 되면서 완전히 사라지게 됩니다.</p>
<h1 id="step-5---사용방법">Step 5 - 사용방법</h1>
<pre><code class="language-ts">const { addToast } = useToast();

const putReflyCommentmutation = useMutation({
    mutationFn: (comment: string) =&gt; {
      return commentApi.putReflyComment(commentId, comment);
    },
    onSuccess: () =&gt; {
      queryClient.invalidateQueries({
        queryKey: [&quot;comment&quot;, &quot;reflyList&quot;, &quot;reflyCommentList&quot;],
      });
      setTextValue(&quot;&quot;);
      addToast(&quot;댓글이 수정되었습니다&quot;, &quot;success&quot;);// 작업 성공적으로 수행됐을때
    },
    onError: error =&gt; {
      console.error(&quot;Error:&quot;, error);
      addToast(&quot;댓글이 수정 오류가 발생했습니다&quot;, &quot;error&quot;);// 작업 에러 발생했을때
    },
  });</code></pre>
<p>아까 전역으로 설정해둔 context를 임포트 한 후 원하시는 곳에서 함수 호출하여 매개변수로 원하는 Toast 메시지 내용과 타입을 정해서 사용할 수 있습니다.</p>
<h1 id="완성">완성</h1>
<p><img src="https://velog.velcdn.com/images/yunbh_0401/post/a6802aee-cbf5-48c1-9128-dae4af4fcaaf/image.gif" alt=""></p>
<hr>
<h3 id="깃허브">깃허브</h3>
<blockquote>
<p>📌 <a href="https://github.com/Feed-B/frontend">https://github.com/Feed-B/frontend</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>