<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ubin.log</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 공부중</description>
        <lastBuildDate>Mon, 12 Jan 2026 08:57:19 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ubin.log</title>
            <url>https://velog.velcdn.com/images/ubin_ing/profile/ab1f565d-763c-4ac5-9bfe-8fc182fdd176/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ubin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ubin_ing" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2025 젊음을 브랜딩하다 : 바다로 나간 금붕어들의 삶에 대한 이야기]]></title>
            <link>https://velog.io/@ubin_ing/2025youngfreshmanneverdie</link>
            <guid>https://velog.io/@ubin_ing/2025youngfreshmanneverdie</guid>
            <pubDate>Mon, 12 Jan 2026 08:57:19 GMT</pubDate>
            <description><![CDATA[<p>여러분의 2025년은 어땠나요? 2025년을 끝마치는 시점인 지금, 조금 색다르고 재미있는 회고 글을 준비했습니다.</p>
<p>이십대 초반, 어린 나이에 사회 생활을 시작한 사회 초년생들이 바라보는 인생을 담은 매거진을 공유합니다. 자신의 색깔을 가진 일곱 명의 젊음을 지금부터 소개해드릴게요!</p>
<h3 id="목차">목차</h3>
<h4 id="낭만가-고졸전형-준비중인-취준생입니다---우준성"><a href="#%EB%82%AD%EB%A7%8C%EA%B0%80-%EA%B3%A0%EC%A1%B8%EC%A0%84%ED%98%95-%EC%A4%80%EB%B9%84%EC%A4%91%EC%9D%B8-%EC%B7%A8%EC%A4%80%EC%83%9D%EC%9E%85%EB%8B%88%EB%8B%A4---%EC%9A%B0%EC%A4%80%EC%84%B1-1">“낭만가 고졸전형 준비중인 취준생입니다” - 우준성</a></h4>
<h4 id="더-많은-시도와-경험을-하고싶다---강승훈"><a href="#%EB%8D%94-%EB%A7%8E%EC%9D%80-%EC%8B%9C%EB%8F%84%EC%99%80-%EA%B2%BD%ED%97%98%EC%9D%84-%ED%95%98%EA%B3%A0%EC%8B%B6%EB%8B%A4---%EA%B0%95%EC%8A%B9%ED%9B%88-1">“더 많은 시도와 경험을 하고싶다” - 강승훈</a></h4>
<h4 id="미소-짓게-하는-서비스를-만들겠습니다---김예림"><a href="#%EB%AF%B8%EC%86%8C-%EC%A7%93%EA%B2%8C-%ED%95%98%EB%8A%94-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EA%B2%A0%EC%8A%B5%EB%8B%88%EB%8B%A4---%EA%B9%80%EC%98%88%EB%A6%BC-1">“미소 짓게 하는 서비스를 만들겠습니다” - 김예림</a></h4>
<h4 id="사랑으로-세계를-제패하고-싶다---박시원"><a href="#%EC%82%AC%EB%9E%91%EC%9C%BC%EB%A1%9C-%EC%84%B8%EA%B3%84%EB%A5%BC-%EC%A0%9C%ED%8C%A8%ED%95%98%EA%B3%A0-%EC%8B%B6%EB%8B%A4---%EB%B0%95%EC%8B%9C%EC%9B%90-1">“사랑으로 세계를 제패하고 싶다” - 박시원</a></h4>
<h4 id="지금의-나를-가장-잘-살고-싶어서---조민수"><a href="#%EC%A7%80%EA%B8%88%EC%9D%98-%EB%82%98%EB%A5%BC-%EA%B0%80%EC%9E%A5-%EC%9E%98-%EC%82%B4%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%84%9C---%EC%A1%B0%EB%AF%BC%EC%88%98-1">“지금의 나를 가장 잘 살고 싶어서” - 조민수</a></h4>
<h4 id="행복을-미루지-않고-지금을-살아가고-싶다---한태영"><a href="#%ED%96%89%EB%B3%B5%EC%9D%84-%EB%AF%B8%EB%A3%A8%EC%A7%80-%EC%95%8A%EA%B3%A0-%EC%A7%80%EA%B8%88%EC%9D%84-%EC%82%B4%EC%95%84%EA%B0%80%EA%B3%A0-%EC%8B%B6%EB%8B%A4---%ED%95%9C%ED%83%9C%EC%98%81-1">“행복을 미루지 않고, 지금을 살아가고 싶다” - 한태영</a></h4>
<h4 id="입력-출력-체력---김은빈"><a href="#%EC%9E%85%EB%A0%A5-%EC%B6%9C%EB%A0%A5-%EC%B2%B4%EB%A0%A5---%EA%B9%80%EC%9D%80%EB%B9%88-1">“입력 출력 체력” - 김은빈</a></h4>
<h3 id="낭만가-고졸전형-준비중인-취준생입니다---우준성-1">“낭만가 고졸전형 준비중인 취준생입니다” - 우준성</h3>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/7d8ee40b-922f-4b66-a302-83b6f54b3e50/image.png" alt=""></p>
<blockquote>
<p><strong>안녕하세요, 간단하게 자기소개 부탁드립니다.</strong></p>
</blockquote>
<p>저는 1금융권 은행에서 금고은행 시스템을 개발 및 유지보수 하고 있는 전산직 은행원, 올해 스물둘이 된 곧 군대 들어가는 청년이자 휴직을 앞둔 우준성이라고 합니다.</p>
<blockquote>
<p><strong>40% 확률로 연봉 2배, 60% 확률로 연봉 1/2배가 되는 버튼이 있다면 누르시겠어요?</strong></p>
</blockquote>
<p>네. 안 되면 퇴사하죠 뭐. (웃음)</p>
<blockquote>
<p><strong>본인이 경찰관으로 일한다면 어떨 것 같나요?</strong></p>
</blockquote>
<p>스펙타클하고 재밌을 것 같은데요? (직접 현장에 투입되고 그런 거예요?)</p>
<p>가끔 앉아서 일하는 개발자의 직업으로 살다 보면 그런 생각이 들긴 하거든요, 몸을 쓰는 직업이면 좋긴 하겠다. 더위든 추위든 그대로 피부로 느끼고, 운동도 하고, 어떻게 말하면 육체노동이 필요한 삶도 재밌을 것 같다. 언젠가 한 번쯤은 해보고 싶다는 생각을 해봤었어요.</p>
<blockquote>
<p><strong>여가 시간을 주로 어떻게 보내시나요? 즐기고 있는 취미 또는 최근에 시간을 보냈던 일을 공유해주세요.</strong></p>
</blockquote>
<p>요즘엔 취미랄 건 하나밖에 없어요. <strong>음악</strong>을 하고 있고요, 작년 6월경부터 실제로 작업실을 계약해서 혼자 음악을 만들고 녹음, 믹싱, 마스터링을 하고 있어요. 올해 1~2월경에 그동안 준비했던 정규 앨범이 발매될 예정입니다.</p>
<p>(제목과 함께 간단하게 설명 부탁드려요.) 앨범 제목은 ‘스물하나’에요. 스무 살 때까지는 항상 한 해 회고를 블로그에 적었었는데, 이번에는 이런 기록을 다른 방식으로 남겨보고 싶었어요. 글로써는 한계가 있다고 느껴져서, 제가 좋아하는 음악이라는 수단으로 그걸 기록하고자 했습니다. 실제로(앨범이) 감정도 복잡하고 가사 또한 획일화되진 않은 것 같아요. …뒤죽박죽인데, 그것조차도 스물하나의 감정으로 담은 그런 작품을 만들었습니다.</p>
<blockquote>
<p><strong>준성 님의 2025년을 마음에 드는 세 개의 단어로 표현해주세요.</strong></p>
</blockquote>
<p><strong>‘낭만’</strong>, <strong>‘강박’</strong>, <strong>‘사랑’</strong>.</p>
<p><strong>낭만</strong>은 항상 제 삶의 모토인 것 같아요. 저는 좀 더 긍정적으로 삶을 바라보려고 하고, 더 찰나의 순간을 길게 느끼려고 하고, 세상을 아름답게 바라보고 싶어요.</p>
<p>그런데 오히려 그런 찰나를 길게 느끼려고 하고, 아름답게 살아보려고 하는 <strong>강박</strong>이 저를 잡아먹던 시기가 종종 있었거든요. 좀 쉬어야 할 것 같은데도 주말에 무언가를 안 하면 안 될 것 같고, 시간을 좀 더 써야 할 것 같고, …이런 강박이 많아서, 올해부터는 좀 줄이려고 노력할 것 같아요.</p>
<p>세 번째가 <strong>사랑</strong>이죠? 얼마 전에 제 MBTI가 바뀌었거든요. 2년 전엔 ENTP였는데, INFP로(X프피로… (웃음))바뀌게 됐어요. 전체적으로 보면 누군가한테 다정한 사람이자 섬세한 사람이 되고 싶다, 사랑을 줄 수 있는 사람이 되고 싶다는 생각이 재작년 대비 많이 바뀐 것 같아요.</p>
<blockquote>
<p><strong>만약 2026년을 표현한다면 마음에 들었던 세 개의 단어 중 하나를 빼고, 두 개를 추가해야 해요. 어떻게 바꾸시겠어요?</strong></p>
</blockquote>
<p><strong>‘강박’</strong>을 빼고 싶어요. 그리고 <strong>‘더 낭만’</strong>, <strong>‘좀 더 낭만’</strong> 두 개를 추가하고 싶어요. (웃음)</p>
<p>제 꿈에 대해서 고민할 때마다 계속해서 다른 길에 대한 가능성을 스스로 닫는 나 자신이 보이더라고요. 지금 보수적인 업계에서 일하기도 하고 주변 분들의 영향도 받다 보니 무의식적으로 그렇게 되는데, 미래에는 진짜 낭만을 찾아서 제가 하고 싶은 걸 쫄지 않고 도전을 해보고 싶어요.</p>
<blockquote>
<p><strong>5년 뒤 내 삶을 일·취미·사랑으로 나눈다면, 10을 기준으로 각각 얼마나 두고 싶나요?</strong></p>
</blockquote>
<p>(일이 바뀔 수도 있는 거죠? 음… 어려운데? 그 인터넷에 키 몸무게 분배만큼 어려운데?)</p>
<p><strong>일 5</strong>, <strong>사랑 4</strong>, <strong>취미 일</strong>.</p>
<p>제 취미가 일이 되는 삶을 살고 싶어요. 좋아하는 걸 업으로 삼고 싶고, 그때 취미라고 말할 수 있는 건 소수의 영역일 것 같아요. 저는 정말 좋아하는 일을 할 때 워커홀릭이 되는 것 같아요. 그리고 사랑은 항상 갈망하는 존재이고, 사랑 없이는 삶이 더 풍성해질 수 없다고 생각하기 때문에 추구할 것 같습니다.</p>
<blockquote>
<p><strong>인생에서 평생 하나를 없앨 수 있다면? 일 때문에 힘들기, 친구 때문에 힘들기, 사랑 때문에 힘들기</strong></p>
</blockquote>
<p><strong>사랑 때문에 힘들기</strong>요.</p>
<p>일은 무한한 가능성이 있어요. (조금 철없는 얘기긴 하지만) 어떤 일이든 나한테 맞는 일이 있을 것이고, 일 자체도 정말 무수히 많을 것이고 앞으로도 계속해서 생겨날 거고. 그런 열린 생각으로 걱정이 없어요.</p>
<p>친구 관계는… 솔직히 말하면 제가 친구 관계에 그렇게 많은 애정을 쏟는 사람은 아니거든요. 있으면 있는 대로 없으면 없는 대로, 도움이 될 수 있다면 도움을 주고 받을 수 있으면 받고 하면서 자연스럽게 멀어지고 가까워지는 것 같아서 인위적으로 힘듦을 버리고 싶진 않아요.</p>
<p>사랑에 대해서는 제가 아무리 노력하더라도 이해할 수 없는 이별을 많이 해봤거든요. 캐치볼처럼, 주고받을 때 한 명이라도 한눈을 팔거나 다른 마음이 오면, 혹은 내가 너무 세게 던지면 어긋나기 때문에 제일 어렵고… …그래서 사랑 때문에 힘든 걸 없애고 싶어요.</p>
<blockquote>
<p><strong>22살의 나에게 사랑이란?</strong></p>
</blockquote>
<p>사랑보다 22살에 포커스를 맞춰보면, 어쩌면 20대 초반인 지금은 정신적인 성장기에 있는 거잖아요. 이 성장기에는 계속 생각이 바뀔 거고, 그렇기에 더 헤어질 수밖에 없는 인연이 많은 것 같아요. 그런 측면에서 내가 차였던 찼던 이별을 하게 됐을 때 그런 상대방의 흔적이나 찌꺼기 같은 게 깊게 남더라고요 저는. 그 당시에는 그게 너무 아픈데, 오히려 서서히 슬픈 감정이 배제되면 그런 찌꺼기들이 오롯이 제 양분이 되는 것 같아요. 스며드는 부분도 있고, …그래서 그런 <strong>찌꺼기들을 남기고 성장하는 과정</strong>인 것 같아요, 저한테 사랑이란.</p>
<blockquote>
<p><strong>언젠가 정말 마음 아플 나에게 지금의 내가 메세지를 보낼 수 있다면, 어떤 이야기를 하고 싶으세요?</strong></p>
</blockquote>
<p>“요즘 많이 힘들지? 일단 지금 휴대폰 켜고 배민 들어가서 제일 먹고 싶은 걸 시키렴. 그리고 편의점 뛰어가서 맥주 네 캔 만 원에 사오고, 일단 마시고 생각하자. 항상 엄마가 해주시던 말씀 있잖아. 전화위복(<strong>轉禍爲福</strong>)이라고. 모든 안 좋은 일은 좋은 일이 일어나기 위한 과정일 수 있다. 안 좋은 일이 생겼으니까, …고점 다시 찍을 일만 남았고, … … …일단 먹고 생각하자!”</p>
<blockquote>
<p><strong>오늘 밤이 유성우가 떨어지는 날이고, 딱 한 가지 소원을 빌 수 있다면 어떤 소원을 비시겠어요?</strong></p>
</blockquote>
<p><strong>“평생, 낭만을 잃지 않을 용기를 주세요.”</strong></p>
<p>시간이 지날수록 현실은 더 물밀듯이 밀려오고, 제 주변에 당연하다고 생각했던 사람들과의 이별이나 사별처럼 현실을 받아들일 수밖에 없는 순간들이 존재하겠지만, 그럼에도 지금의 꿈을 현실로 인해서 접는 일이 없었으면 좋겠기에 이런 소원을 빌 것 같아요.</p>
<blockquote>
<p><strong>마지막으로, 좋아하는 노래 한 곡 추천해주세요.</strong></p>
</blockquote>
<p>(찾아봐도 될까요?) 최근에 검정치마 앨범을 많이 돌려봤어요. (조휴일이라는 남자에게 사랑에 빠질 것 같아요. (웃음))</p>
<p><strong>검정치마의 Our own summer.</strong> (제가 제일 좋아하는 가사도 말해도 돼요?)
“그곳에 가면 네가 있을 것 같아. 붉은 참나무 식당에서 기다렸지”
<img src="https://velog.velcdn.com/images/ubin_ing/post/e1afeded-d90e-41ff-b090-2cc9beb8b68b/image.png" width="250px" height="250px" /></p>
<p>저도 최근에 이런 적이 있거든요. 정말 보고 싶은 사람이 있어서 한 시간 동안 그 사람을 기다렸던 적이 있는데, 정말 공감이 세게 가는 이, …단어 하나하나의 선택이 너무 예술적이라고 느꼈어요. 그리고 몽글몽글한 검정치마만의 감성이… …멋진 아티스트인 것 같습니다.</p>
<blockquote>
<p><strong>인터뷰는 어떠셨나요?</strong></p>
</blockquote>
<p>오랜만에 저를 돌아볼 수 있고, 제가 아끼는 후배가 이런 재밌는 일을 한다는 소식을 듣고 바로 달려와서 (웃음) 영감받을 수 있는 시간이 된 것 같습니다. 불러주셔서 감사합니다.</p>
<p><em>instagram: <a href="https://instagram.com/henrywoo0">@henrywoo0</a></em></p>
<p><em>tistory: <a href="https://white-world.tistory.com">https://white-world.tistory.com</a></em></p>
<hr>
<h3 id="더-많은-시도와-경험을-하고싶다---강승훈-1">“더 많은 시도와 경험을 하고싶다” - 강승훈</h3>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/56d84a6a-540d-40fd-b1e8-ac627be4cb8f/image.png" alt=""></p>
<blockquote>
<p><strong>안녕하세요, 간단하게 자기소개 부탁드립니다.</strong></p>
</blockquote>
<p>저는 이제 스물한 살이 된 강승훈이고요, 이커머스 분야 회사에서 백엔드 개발자와 PM 업무를 동시에 하고 있습니다.</p>
<blockquote>
<p><strong>40% 확률로 연봉 2배, 60% 확률로 연봉 1/2배가 되는 버튼이 있다면 누르시겠어요?</strong></p>
</blockquote>
<p>누를 것 같아요. 어차피 사회 초년생인데 뭐 잃어봐야죠.</p>
<p>(그럼 서른 살일 때는 누르실 건가요?) 그땐 안 누르죠. (웃음) 잃을 게 많으니까요. 가정도 있을 거고…</p>
<blockquote>
<p><strong>본인이 변호사로 일한다면 어떨 것 같나요?</strong></p>
</blockquote>
<p>되게 피곤하게 살 것 같아요. 변호사라는 게 사람과 사람 사이를 중재하는 일이나 변호하는 역할을 하잖아요. 저는 책임감이 필요한 일은 진짜 모든 걸 다 제쳐놓고 그 일만 하는 성향이 있단 말이죠. 그걸 또 돈을 받고 하다 보니, 다루는 사건이 있다면 30대 정도 익숙해졌을 때는 몰라도, 20대 중반쯤 변호사가 되었다면… 연애도 안 하고 부모님께 연락도 안 드리고, 끼니도 거르고 일만 파고 있을 것 같아요.</p>
<blockquote>
<p><strong>승훈 님의 2025년을 마음에 드는 세 개의 단어로 표현해주세요.</strong></p>
</blockquote>
<p>추상적인 단어는 없는 것 같아요. <strong>‘행복’</strong>, <strong>‘적응’</strong>, <strong>‘힘듦’</strong> 세 가지예요.</p>
<p>세 단어가 타임라인으로 이어져요. 일단 첫 회사에 취직하면서 직장이 생기고 돈을 버는 사회의 일원이 되었으니까, 초기에는 <strong>행복</strong>했어요.</p>
<p>그리고 회사에 들어오니 이제 <strong>적응</strong>해야죠. 회사 동료분들과 여러 가지 이야기를 나누고 소통하면서 열심히 적응하려고 했던 게 올해는 기억에 남아요. </p>
<p>마지막 <strong>‘힘듦’</strong> 같은 경우 직장 상사의 비위를 맞추거나 사회생활 같은 게, 제가 나이도 어리고 많이 어렵다 보니까(저는 대부분의 인턴들보다 나이가 어리잖아요) 어떻게 대처해야 할지? 같은 사회생활 측면에서 힘이 들었던 것 같아요.</p>
<blockquote>
<p><strong>지금 삶에서 부족하거나 힘들다고 느끼는 것들이 있나요?</strong></p>
</blockquote>
<p>시간이 좀 부족한 것 같긴 해요. 지금은 밤낮 해서 일하고 여가 생활만 해도 하루가 끝나잖아요. 그래서 조금이라도 시간이 있었다면 어떻게 나한테 좀 더 도움이 되는 일을 할 수 있지 않았을까? 라는 생각이 드네요. 최소 주 40시간 근무는 해야 하니까, 근무 시간과 취침 시간을 빼면 몇 시간 남진 않잖아요. 그래서 일하는 시간 대신, 나를 위한 시간이 더 많았다면 나에 대해 투자를 더 할 수 있지 않았을까 하는 후회도 조금 있네요.</p>
<blockquote>
<p><strong>만약 2026년을 표현한다면 마음에 들었던 세 개의 단어 중 하나를 빼고, 두 개를 추가해야 해요. 어떻게 바꾸시겠어요?</strong></p>
</blockquote>
<p><strong>‘힘듦’</strong>을 빼고, <strong>‘자기 계발’</strong>이랑, 그리고… <strong>‘버킷리스트’</strong>.</p>
<p><strong>힘듦</strong>을 빼는 건 당연하다고 생각해요(저는 행복하게 살고 싶거든요). 그리고 <strong>자기 계발</strong>은 슬슬 필요하다고 생각되는 게, 대학생이 아니다 보니 스무 살을 남들처럼 재밌게 놀면서 보내진 못했단 말이죠. 그래서 저는 그냥 아예 스무 살은 열심히 놀아보자는 생각으로 살아봤는데, 또 마냥 논다고 좋지만은 않더라고요. (웃음) 뭔가 마음 한 곳이 텅 빈 느낌이 계속 있어서, …뭐 자기 계발이라고 (크게) 말하긴 했지만, 회사 내에서 소통할 때 쓰는 능력이라든지(이를테면 소프트 스킬 같은) 나에게 도움이 되는 것들을 찾아보고 싶어서 자기 계발이라고 표현한 것 같아요.</p>
<p><strong>버킷리스트</strong> 같은 경우는 자기 계발과 비슷한 맥락인 것 같은데요, 너무 저를 (자기 계발을 위해) 몰아세우기보다는 버킷리스트를 세워서 하나하나씩 채워가며 삶 속에서 좀 특별한 행복? 을 지금부터 계획해 나가고 싶어서 골랐어요.</p>
<blockquote>
<p><strong>5년 뒤 내 삶을 일·취미·사랑으로 나눈다면, 10을 기준으로 각각 얼마나 두고 싶나요?</strong></p>
</blockquote>
<p>저는 <strong>일 3</strong>, <strong>취미 4</strong>, <strong>사랑 3</strong>으로 둘 것 같아요.</p>
<p>5년 뒤니까… 저는… (고민하다가) 음, 아마추어랑 프로 차이를 아시나요? 아마추어가 돈을 주고 배우는 사람이고, 프로가 돈을 받고 일하는 사람이거든요. 어떻게 보면 저는 사실상 프로잖아요. 프로로 남기 위해서는 어느 정도 일에 비중을 둬야 한다고 생각해요. 그래서 3 정도 줬고, 취미 같은 경우는… 일이라는 게 3으로 뒀지만 ‘일’이라는 것 자체에 대한 강도가 취미보다 훨씬 높잖아요. 그래서 그 일을 좀 더 열심히 지치지 않고 하기 위해서는 취미를 좀 더 높여야 한다고 생각해요. 사랑도 비슷해요. 나 자신을 환기하거나, 사랑하는 사람과 시간을 보내는 거니까 3으로 뒀어요. 어떻게 보면 일과 여가생활을 분리해서 <strong>일 3, 여가생활 7</strong>로 나눌 수 있을 것 같아요.</p>
<blockquote>
<p><strong>인생에서 평생 하나를 없앨 수 있다면? 일 때문에 힘들기, 친구 때문에 힘들기, 사랑 때문에 힘들기</strong></p>
</blockquote>
<p>저는 <strong>사랑 때문에 힘들기</strong>요.</p>
<p>여운이 길게 남는 걸 싫어해요. 사랑은 가장 많은 감정을 쏟는 행위기 때문에, 사랑이 있다가 한 번에 박살이 나버리면 마냥 오래갈 것 같지 않아도 엄청나게 오래 가거든요. 그 여운 때문에 일이나 일상생활을 못 하는 게 싫어서 여운이 길게 남는 ‘사랑 때문에 힘들기’를 제거하고 싶어요.</p>
<blockquote>
<p><strong>어떤 삶을 살아가려고 노력하시는지 궁금해요. 자신만의 철학이나 가치관, 또는 고민 중인 게 있다면 자유롭게 말씀해주실 수 있나요?</strong></p>
</blockquote>
<p>저는 가치관을 <strong>‘유쾌하게’</strong>로 정의하거든요(제 깃허브에도 적어뒀어요). 뭐든지 이왕 할 거라면 최대한 긍정적으로 하는 게 맞다고 봐요. 예를 들어 야근을 하더라도, 그다음 날 휴일이 있다면 ‘휴일이 있으니까 열심히 해보자. 야근하는 건 상관없잖아?’ 같이 생각하면서 긍정적으로, 유쾌하게 살려고 합니다. 그래서 제 철학이나 가치관은 ‘유쾌하게’ 네 글자로 정리할 수 있을 것 같아요.</p>
<p>어떻게 보면 포괄적인 개념이라, 이렇게 저렇게 모든 일에 적용하고 살고 있는 것 같아요. 딱히 복잡한 개념은 아닌 것 같아요.</p>
<blockquote>
<p><strong>21살의 나에게 사랑이란?</strong></p>
</blockquote>
<p>제가 T라서, 사랑이란 <strong>투자 가치가 있는 여가 활동</strong>이죠, 사실상. (웃음) 좋아하는 사람이랑 같이 있으면 행복해지고, 그래서 가성비 좋은 여가 활동이라 생각합니다.</p>
<blockquote>
<p><strong>언젠가 정말 마음 아플 나에게 지금의 내가 메세지를 보낼 수 있다면, 어떤 이야기를 하고 싶으세요?</strong></p>
</blockquote>
<p><strong>“승훈아, 초년생 때, 스무 살 때 그 풋풋했던 마음을 가지고 힘들더라도 계속해서 살아갔으면 좋겠어.”</strong></p>
<blockquote>
<p><strong>마지막으로, 좋아하는 노래 한 곡 추천해주세요.</strong></p>
</blockquote>
<p>이찬혁 - 멸종위기사랑.
<img src="https://velog.velcdn.com/images/ubin_ing/post/813ca16d-2874-4cee-9b97-96930f2af6db/image.png" width="250px" height="250px" /></p>
<p><strong>인터뷰는 어떠셨나요?</strong></p>
<p>재밌었어요. 뭔가 연말정산 같은 느낌이라… 재밌었어요.</p>
<p><em>e-mail: <a href="mailto:nicebrian0328@gmail.com">nicebrian0328@gmail.com</a></em></p>
<hr>
<h3 id="미소-짓게-하는-서비스를-만들겠습니다---김예림-1">“미소 짓게 하는 서비스를 만들겠습니다” - 김예림</h3>
<div align="center">
  <img src="https://velog.velcdn.com/images/ubin_ing/post/2b69693c-a761-480e-96ed-c5eb087d75b2/image.jpg" width="350px" height="350px" />
</div>


<blockquote>
<p><strong>안녕하세요, 간단하게 자기소개 부탁드립니다.</strong></p>
</blockquote>
<p>저는 스무 살이고, 현재 실리콘밸리 소재 AI 번역 스타트업에서 프론트엔드 엔지니어로 근무하고 있는 김예림입니다.</p>
<blockquote>
<p><strong>회사에선 제일 막내이시겠어요. 동료 직원분들이 어떻게 바라봐주시나요?</strong></p>
</blockquote>
<p>다니고 있는 회사에서는 나이와 막내라는 위치 상관없이 굉장히 자유롭게 의견을 낼 수 있어요. 이런 부분에 있어서 너무 감사한 동료들을 만났다고 생각해요. 제가 하고 싶었던 거나 기여하고 싶은 것을 자신 있게 얘기하면 대부분 수용을 해주세요. 어떻게 보면 막내스럽지 않게 대해주시는 것 같아서 감사하면서도 책임감을 가지고 일할 수 있게 되는 것 같습니다.</p>
<blockquote>
<p><strong>40% 확률로 연봉 2배, 60% 확률로 연봉 1/2배가 되는 버튼이 있다면 누르시겠어요?</strong></p>
</blockquote>
<p>저는 아마 안 누를 것 같기는 한데… 음… 이유를 생각해보자면, 저는 어린 나이에 돈을 벌고 있다는 것만으로도 굉장히 특별한 길을 가고 있다고 생각하거든요. 시간이 깡패라고 생각하는 사람이라, 돈을 리스키있게 조정하는 것보다는 꾸준한 수입이 있는 상태로 시간을 좀 더 잘 쓰는 데 초점을 맞출 것 같아요.</p>
<blockquote>
<p><strong>본인이 간호사로 일한다면 어떨 것 같나요?</strong></p>
</blockquote>
<p>(간호사? 오… 진짜 생각을 한 번도 해본 적도 없는 직종인데, 간호사라면…)</p>
<p>(성실하게 일은 했을 거 같은데, 그러면서 뭔가 브이로그나 다른 걸 찍으면서 살지 않았을까요..? 20대 간호사 성장 로그를 찍으면서...?)</p>
<p>간호사를 하게 되면 많은 사람과 얘기를 나누려고 할 것 같아요. 병동 안에서 활기차고 따뜻한 대화를 나눠보고 싶고, 여러 사람들의 인생을 많이 들어볼 기회이지 않을까 싶네요. 많이 이야기도 나누어보고 싶고, 그러면서 병동 내에서 되게 활기찬 변화를 줄 수 있지 않을까 싶어요.</p>
<blockquote>
<p><strong>여가 시간을 주로 어떻게 보내시나요? 즐기고 있는 취미 또는 최근에 시간을 보냈던 일을 공유해주세요.</strong></p>
</blockquote>
<p>저는 주로 본가에서 재택을 하다보니 (상경한) 친구들과 멀리 떨어져 있어요. 제가 외향형 인간이다 보니까 친구들과 놀고 싶은데 어떻게 놀아야 하지 싶더라고요. 그래서 2025년에서는 온라인(디스코드)에서 친구들과 게임을 하면서 보냈고, 또 방탈출이나 머리쓰는 걸 좋아해서 그런 것들을 푸는 걸 좋아했어요. 요즘은 글 쓰기에 관심이 많아져서 취미로 가져보고 싶어 책을 읽고 내용을 글로 표현하는 것을 2026년에는 취미로 가져가보고 싶습니다.</p>
<blockquote>
<p><strong>예림 님의 2025년을 마음에 드는 세 개의 단어로 표현해주세요.</strong></p>
</blockquote>
<p>성인과 직장인을 동시에 마주하는 해였다 보니 <strong>‘새로움’</strong>.</p>
<p>그리고 주로 적응하려는 데 많은 초점을 맞췄던 것 같아요, <strong>‘적응’</strong>. </p>
<p>다른 한 가지는 <strong>‘확장’</strong>이라는 키워드를 잡고 싶어요. 세계관이 넓어지는 한 해였던 것 같아요.</p>
<blockquote>
<p><strong>5년 뒤 내 삶을 일·취미·사랑으로 나눈다면, 10을 기준으로 각각 얼마나 두고 싶나요?</strong></p>
</blockquote>
<p>(5년 뒤면 스물다섯 스물여섯이니까… 그쯤이면…) 저는 사실 모든 인간관계에 사랑이 굉장히 중요하다고 생각하는 편이어서 <strong>사랑을 5</strong> 정도 둘 것 같고, <strong>일을 4</strong>, <strong>취미를 1</strong> 정도로 둘 것 같아요. 여기서 일이라는 게 회사나 커리어적으로의 일이 아니라 제가 직접 찾은 하고 싶은 일이자 좋아하는 일일 거라는 생각이 들어서, 취미 비중이 그렇게 높진 않은 것 같아요. 일 자체에 동기를 많이 가지고 있으면 좋겠다는 바가 있네요.</p>
<blockquote>
<p><strong>인생에서 평생 하나를 없앨 수 있다면? 일 때문에 힘들기, 친구 때문에 힘들기, 사랑 때문에 힘들기</strong></p>
</blockquote>
<p>사실 모든 게 힘든 일이 있어야 극복하는 과정에서 성장한다고 생각하거든요. 그래서 완전히 없애는 게 오히려 단점일 것 같긴 하지만, 저는 친구 때문에 힘들면 항상 (친구 관계라는 게) 일대일이 아니라 다양한 사람과 엮여 있던 경우가 많아서, 단 한 명이랑도 사이가 안 좋아지면 머리가 아프기에 <strong>친구 때문에 힘들기</strong>를 선택하겠습니다.</p>
<blockquote>
<p><strong>어떤 삶을 살아가려고 노력하시는지 궁금해요. 자신만의 철학이나 가치관, 또는 고민 중인 게 있다면 자유롭게 말씀해주실 수 있나요?</strong></p>
</blockquote>
<p><strong>‘후회 없이 살자’</strong>인 것 같아요. 젊은 나이에 사회에 입문했기 때문에 그에 따른 베네핏이 굉장히 많다고 생각하는데, 그 상태에서 흘러가는 시간에 대해서 후회하지 않았으면 좋겠어요.</p>
<p>그게 뭐 일이든 노는 거든 항상 하나를 할 때 ‘최선을 다했으니 후회 없다’라는 생각이 들 정도로 살면, 삶의 질과 자존감이 함께 높아지는 것 같아서 그런 삶을 살려고 노력하고 있어요. 고민 중인 건 2025년도에는 하고 싶은 일은 많았으나 실행을 한 일이 많지 않았던 것 같아 후회가 남아서, 2026년에는 한없이 실패해 보고, 최선을 다해보고, 회고를 할 수 있는 해가 된다면 좋지 않을까 하는 생각이 드네요.</p>
<blockquote>
<p><strong>20살의 나에게 사랑이란?</strong></p>
</blockquote>
<p>저는 사랑이라는 키워드가 굉장히 넓은 개념이라 생각해요.</p>
<p>가족 간의 사랑, 우정도 사랑이고. 저는 그런 면에서 사랑이 저를 움직이는 원동력이 되거든요. ‘후회 없이 살자’는 제가 저한테 하고자 하는 말이라면, 남들에게 ‘김예림’이라는 사람이 어떤 사람이었으면 하냐고 질문한다면 제가 <strong>받는 사랑을 두세 배로 돌려주고 싶은 사람</strong>인 것 같아요. </p>
<p>편지에 그런 표현을 많이 쓰기도 하는데요, 어떻게 보면 모든 사랑과 모든 사람, 제가 좋아하는 일을 할 수 있게 된 것에도 수많은 사람의 사랑과 희생이 있었다고 생각해요. 그에 보답하고 싶은 마음이 커서 사랑이 제 삶을 움직이는 원동력이 되는 것 같아요.</p>
<blockquote>
<p><strong>언젠가 정말 마음 아플 나에게 지금의 내가 메세지를 보낼 수 있다면, 어떤 이야기를 하고 싶으세요?</strong></p>
</blockquote>
<p>(아마 그때의 저는 최선을 다했을 거라고 생각하고, 또 그런 거로 마음 아파하고 있을 것 같아서,)</p>
<p><strong>“지금까지 너무 수고 많았고, 지금 힘든 것도 다 추억이 될 거니까 최선을 다해서 이 힘든 과정을 잘 겪어보고, 이후 그만큼 성장해 있을 너를 기대해.”</strong></p>
<p>약간… 어떻게 말해줘야 할지는 모르겠지만 수고했을 것이고 잘하고 있다고 말해주고 싶어요. 잘하고 있다는 욕심 때문에 힘들어하고 있을 것 같아서, 잘하고 있다 수고 많았다 이런 이야기 해주고 싶을 것 같아요.</p>
<blockquote>
<p><strong>오늘 밤이 유성우가 떨어지는 날이고, 딱 한 가지 소원을 빌 수 있다면 어떤 소원을 비시겠어요?</strong></p>
</blockquote>
<p><strong>어느 상황에든 감사할 수 있게 해달라고 소원을 빌 것 같아요.</strong> 제가 생각하는 행복이라는 건 엄청나게 큰 게 아니라서 주변에서도 바로 찾을 수 있다고 생각하거든요.</p>
<p>제가 생각하는 행복에서 가장 중요한 건 번아웃이 오지 않는 상태, 그리고 나 자신이 처한 상황에 대해 만족하고 감사할 수 있는 마음인 것 같아요. 어떻게 보면 그게 행복의 가장 기본적인 베이스라고 생각해요.</p>
<p>물론 “항상 행복하게 해주세요”, “돈 많이 벌게 해주세요” 같은 소원을 빌 수도 있겠지만, 돈을 많이 번다고 해서 그 순간에 정말 만족할 수 있을지는 잘 모르겠거든요. 그래서 어떤 순간에 있든, 어떤 상황에 있든 <strong>감사할 수 있는 사람이 되게 해달라</strong>는 소원을 빌 것 같아요.</p>
<blockquote>
<p><strong>마지막으로, 좋아하는 노래 한 곡 추천해주세요.</strong></p>
</blockquote>
<p>2025년에 가장 많이 들었던 노래는 <strong>이찬혁 - 멸종위기사랑</strong>인 것 같아요. 저도 평소에 하고 싶었던 말들을 노래로 잘 풀어냈다는 느낌을 받았어요. 또 멜로디도 좋고 들을 때마다 신이 나더라고요. 그래서 2025년도에 가장 좋았던 노래인 것 같아요.
<img src="https://velog.velcdn.com/images/ubin_ing/post/813ca16d-2874-4cee-9b97-96930f2af6db/image.png" width="250px" height="250px" /></p>
<blockquote>
<p><strong>인터뷰는 어떠셨나요?</strong></p>
</blockquote>
<p>생각보다 제가 예상했던 질문이 안 나오고 어떻게 보면 딥한 질문이어서, 색달랐고 좋았어요. 다른 분들이 어떻게 답변하셨는지 몰라서, 제가 잘 답변한 지도 모르겠고… (웃음) 다른 분들 인터뷰 내용도 한번 보고 싶네요.</p>
<p><em>instagram: <a href="https://instagram.com/ye._.xim">@ye._.xim</a>
e-mail: <a href="mailto:loveyr0118@gmail.com">loveyr0118@gmail.com</a></em></p>
<hr>
<h3 id="사랑으로-세계를-제패하고-싶다---박시원-1">“사랑으로 세계를 제패하고 싶다” - 박시원</h3>
<div align="center">
  <img src="https://velog.velcdn.com/images/ubin_ing/post/521df009-bcdf-446d-ad3b-7a2e63efa905/image.png" width="250px" height="250px" />
</div>


<blockquote>
<p><strong>안녕하세요, 간단하게 자기소개 부탁드립니다.</strong></p>
</blockquote>
<p>안녕하세요, 저는 스물한 살 박시원이고, 대학생 겸 프리랜서로 일하고 있습니다. UI/UX 디자인, 브랜드 디자인을 겸하고 있고, 이쁜 것과 저를 사랑합니다. (<em>’저’요?</em> 네 저요. <em>아하… 제대로 들은 게 맞군요(웃음)</em>)</p>
<blockquote>
<p><strong>여가 시간을 주로 어떻게 보내시나요? 즐기고 있는 취미 또는 최근에 시간을 보냈던 일을 공유해주세요.</strong></p>
</blockquote>
<p>저는 침대에 누워서 릴스를 보거나, 게임을 한다든가 하면서 시간을 녹이는 걸 되게 좋아하는데, 최근에는 이쁜 걸 만들기를 좋아해서 <strong>방 꾸미기</strong>를 좀 하고 있어요. 예를 들어 방에 붙일 포스터를 만든다던가, 방 구조를 재설계한다던가. 쉬는 시간에는 그렇게 보내는 것 같아요. (<em>최근에는 탁상 달력도 만드신 걸로 알고 있는데.</em>) 네, 그것도 일부죠.</p>
<blockquote>
<p><strong>시원 님의 2025년을 마음에 드는 세 개의 단어로 표현해주세요.</strong></p>
</blockquote>
<p>제가 11월 초에 감명 깊게 들은 노래가 멸종위기사랑이라서, <strong>‘멸종’</strong>, <strong>‘위기’</strong>, <strong>‘사랑’</strong></p>
<p><strong>멸종</strong>같은 경우에는, 제가 주변에 사랑하던 것들이 많이 없어졌어요. 예를 들어 우빈 님도 그렇고(<em>?</em>) 주변에 소중한 사람들이 많이 없어지기도 했고요. 또 제가 너무 사랑했던 고등학교를 떠나와 새 환경에 적응하는 과정에서도 제 감정 속 무언가가 멸종이 된 것 같아요. </p>
<p>그 감정 때문에 위기가 찾아와서 많이 울기도 했고, 버틸 수 있을까 생각했던 기억도 많이 남았던 것 같아요. 그럼에도 불구하고 그 과정에서 사랑을 너무 많이 받았고, 그걸 돌려줄 수 있었던 기회도 많았던 것 같기에, <strong>‘멸종’</strong>, <strong>‘위기’</strong>, <strong>‘사랑’</strong> 이렇게 표현할 수 있는 것 같아요.</p>
<blockquote>
<p><strong>만약 2026년을 표현한다면 마음에 들었던 세 개의 단어 중 하나를 빼고, 두 개를 추가해야 해요. 어떻게 바꾸시겠어요?</strong></p>
</blockquote>
<p>음… 일단 <strong>위기</strong>를 뺄 것 같고요, 그리고… (추가? 음…) <strong>행운</strong>과 <strong>행복</strong>?</p>
<p>작년의 <strong>위기</strong>가 제게 너무 큰 타격이었기에 올해는 위기가 없었으면 좋겠어요. 또 올해는 위험한 시도라고 칭한다면 위험한 시도인 것들을 몇 번 해보고 싶어서 그때마다 <strong>행운</strong>이 따랐으면 좋겠고, 또 그때는 <strong>행복</strong>이 있었으면 하는 마음에 그렇게 추가하고 싶어요.</p>
<blockquote>
<p><strong>5년 뒤 내 삶을 일·취미·사랑으로 나눈다면, 10을 기준으로 각각 얼마나 두고 싶나요?</strong></p>
</blockquote>
<p>저 <strong>사랑 8</strong> <strong>일 1</strong> <strong>취미 1</strong>이요.</p>
<p>(5년 뒤라고 말씀하셨죠? …5년 뒤면 제가 군대를 갔다 오고 취업했을 때인데,) 지금 열심히 일하는 이유가 뭐냐고 물어보면 ‘나중에 일을 덜 하려고’라고 답을 하고 싶어요. 물론 일을 사랑하기야 한다만, 5년 뒤에도 이렇게 열심히 할 생각은 없기 때문에 일은 최대한 배제하고 싶어요. 취미도 고민을 해봤는데 취미를 그렇게 즐기는 사람도 아니고, 취미보다는 사랑이 좀 더 가치가 크다고 생각했기 때문에 사랑은 8을 뒀고, 나머지는… …(웃음) 아이 뭐 뭐 뭐가 더 필요합니까 사랑 있음 됐지 라는 마인드로 살아가지고. 네. (웃음)</p>
<blockquote>
<p><strong>인생에서 평생 하나를 없앨 수 있다면? 일 때문에 힘들기, 친구 때문에 힘들기, 사랑 때문에 힘들기</strong></p>
</blockquote>
<p>(고민 없이) 아 저 <strong>사랑 때문에 힘들기</strong>요.</p>
<p>(이러니까 제가 너무 사랑 중독자 같은데, 아…참…) 일 때문에 힘들기? 어… 그냥 내가 열심히 하면 됨. 친구 때문에 힘들기? 어… 손절하면 됨(<em>?</em>). 근데 사랑 때문에 힘들기? 어… 이건 어떻게 할 수가 없거든요. (물론 다른 것도 여러 종류가 있겠다만) 사랑 때문에 힘든 건 보통 내가 너무 사랑한 나머지 힘들게 되는 거라고 생각해요. 근데 어떻게 사람이 사랑을 포기합니까 (웃음) 그리고 저는 다른 사람들보다 더더욱 사랑에 목매어 사는 사람이기 때문에 그만큼 힘든 리스크가 더 커지는 것 같아요. 그래서 사랑 때문에 힘들지만 않다면 제 인생에 있을 불행의 70% 정도는 없어지지 않을까 싶습니다.</p>
<blockquote>
<p><strong>21살의 나에게 사랑이란?</strong></p>
</blockquote>
<p>저는 작년에 사랑을 많이 받았다고 생각해요. 작년에는 재작년에 비해 조용히 살았음에도 불구하고 많은 사람에게 잊히지 않았다는 느낌을 받았고, 새로운 사람을 만나고 있던 사람들을 떠나보내는 과정에서 사랑을 더 많이 받았다는 생각이 들었어요. 그래서, 스무 살의 박시원의 사랑은 <strong>남들이 나에게 주는 애정</strong>이라고 정의했어요.</p>
<p>그리고 스물한 살의 나에게 물어본다면? (음… 사랑이란 무엇일까?…) 아직은 모르겠어요. 스물한 살의 박시원의 사랑? (<em>제가 이건 내년 이맘때쯤 다시 한번 여쭤볼게요.</em>)</p>
<blockquote>
<p><strong>무언가 때문에 정말 힘들거나 아팠을 때, 어떻게 이겨내셨는지 궁금해요.</strong></p>
</blockquote>
<p>힘들었던 적은 많았어요. 올해 많았던 것 같아요. 사랑 때문에도 힘들었고, 학교 때문에도 힘들었고, 우정 때문에도 힘들었고, …근데 그럴 때마다 해결해 줬던 건 뻔한 말이지만 시간이었던 것 같아요. <strong>시간이 지나면 다 괜찮아지고.</strong></p>
<p>저만의 특별한 요소가 있다고 물어본다면 음악? 그때그때 맞는 음악을 듣는 것 같아요. 저는 가사에 집중하는 편인데, 사랑 때문에 힘들 때는 ‘잔나비 - 주저하는 연인들을 위해’를 틀고, 우정 때문에 힘들면 ‘다 XX라’ 이런 느낌의 노래를 튼다던가, 상황에 맞는 힘이 될만한 노래를 틀어주는 것 같아요.</p>
<p>제 플레이리스트 중에 상사병이라는 플레이리스트가 있거든요? 그 플레이리스트를 올해 1,000분이나 들었더라구요. 그래서 그게 올해 사랑 때문에 힘들었다는 방증이었던 것 같아요.</p>
<blockquote>
<p><strong>언젠가 정말 마음 아플 나에게 지금의 내가 메세지를 보낼 수 있다면, 어떤 이야기를 하고 싶으세요?</strong></p>
</blockquote>
<p>저는 <strong>‘과거에게 부끄럽지 않게, 미래에게 후회하지 않게’</strong>라는 인생의 모토를 가지고 살아요. 그런 면에 있어서 미래의 저에게 메세지를 보내고 싶지는 않은 것 같아요.</p>
<blockquote>
<p><strong>오늘 밤이 유성우가 떨어지는 날이고, 딱 한 가지 소원을 빌 수 있다면 어떤 소원을 비시겠어요?</strong></p>
</blockquote>
<p><strong>평생의 사랑을 찾아달라고 빌고 싶어요.</strong> 저는 검정치마의 3집(TEAM BABY)을 되게 좋아하거든요. 그 노래에서 되게 길게 사랑에 관해 얘기를 해요. 사랑이란 이런 것이고, 이게 사랑이고, …이걸 길게 보여준 앨범인데, 저도 그런 사랑을 찾고 싶어요. 천년만년의 사랑. 언젠가 찾을 수 있겠죠?</p>
<blockquote>
<p><strong>마지막으로, 좋아하는 노래 한 곡 추천해주세요.</strong></p>
</blockquote>
<p>(아… 뭘 추천해드려야 좀… 그걸까?) 이제 스무 살은 아니고 스물한 살이잖아요. 귀여울 나이는 아니잖아요.</p>
<p><strong>아일릿의 NOT CUTE ANYMORE</strong> 추천해드리겠습니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/56238441-599d-4fc1-bc83-14e520018b66/image.png" width="250px" height="250px" /></p>
<blockquote>
<p><strong>인터뷰는 어떠셨나요?</strong></p>
</blockquote>
<p>너무 재밌어요. 저는 이런 얘기 너무 하고 싶었고 너무 좋아해요.</p>
<p><em>e-mail: <a href="mailto:park@siwon.im">park@siwon.im</a></em></p>
<hr>
<h3 id="지금의-나를-가장-잘-살고-싶어서---조민수-1">“지금의 나를 가장 잘 살고 싶어서” - 조민수</h3>
<div align="center">
<img src="https://velog.velcdn.com/images/ubin_ing/post/6920bc83-29f4-4894-9112-1abdefc5cf03/image.png" width="300px" height="300px" />  
</div>


<blockquote>
<p><strong>안녕하세요, 간단하게 자기소개 부탁드립니다.</strong></p>
</blockquote>
<p>안녕하세요, 저는 증권사에서 채권과 펀드, 수익증권, 신탁 등의 상품을 관리하는 업무를 하고 있는 스물한 살 조민수입니다.</p>
<blockquote>
<p><strong>회사에선 제일 막내이시겠어요. 동료 직원분들이 어떻게 바라봐주시나요?</strong></p>
</blockquote>
<p>일단 열심히 하는 태도를 엄청나게 칭찬해 주시고, 실수가 발생하더라도 재발하지 않으면 된다는 마인드를 심어주셔서 감사함을 느껴요. 그리고 막내니까 많이 배려해 주시고, 저를 진짜 자녀 대하듯이 이뻐해 주셔서 회사 다닐 때 동료 간의 스트레스는 없는 것 같습니다.</p>
<blockquote>
<p><strong>40% 확률로 연봉 2배, 60% 확률로 연봉 1/2배가 되는 버튼이 있다면 누르시겠어요?</strong></p>
</blockquote>
<p>(네! 누를 것 같은데요? 당연히 누를 것 같은데… 아 잠시만 60% 절반? 근데 이거 너무 현실적으로 들어가면은…)</p>
<p>전 안 누를 것 같아요. 매달 적금이든 주식이든 정기적으로 나가야 하는 금액이 있는데, 전자만 보고 무모한 도전을 했다가는 1/2배라는 암울한 상황을 맞닥뜨리게 될 것 같아서, 안정적인 게 우선일 것 같아요.</p>
<blockquote>
<p><strong>여가 시간을 주로 어떻게 보내시나요? 즐기고 있는 취미 또는 최근에 시간을 보냈던 일을 공유해주세요.</strong></p>
</blockquote>
<p>저는 사람 만나는 걸 너무 좋아해서, 좋아하는 사람들과 예쁜 카페에 가거나 좋은 식당에 가서 맛있는 음식을 먹으며 시간을 보내는 걸 좋아해요. </p>
<p>그리고 요새는 피아노를 다시 연주해 보고 싶다는 생각이 들어서, 퇴근하고 다닐 피아노 학원을 알아보고 있습니다. (예전에 해본 적이 있으세요?) 초등학교 1학년 때부터 6학년 때까지 6년 정도를 쳤는데, 그때는 정말 제 의지가 아니…(웃음)고 부모님께서 시키셔서 다녔었어요. 요즘은 제가 왈츠 장르에 빠져서 그런 곡들을 직접 자유자재로 쳐 보고 싶어서 피아노를 연주해 보고 싶다는 생각이 들었습니다. 그리고 피아노를 연주하면 스트레스가 풀릴 것 같아요.</p>
<blockquote>
<p><strong>민수 님의 2025년을 마음에 드는 세 개의 단어로 표현해주세요.</strong></p>
</blockquote>
<p>첫 번째는 ‘원동력’인 것 같고, ‘자유’랑 ‘재미’에 초점을 맞추려고 살았던 것 같아요.</p>
<p>2025년도는 3년 동안 취업을 열심히 준비해 온 과정에 대해서 스스로에게 보답하고 싶은 마음이 컸어요. ‘재미’를 찾아서 가고 싶은 나라가 있으면 여행도 가고, 친구들 만나는 게 재밌으니까 친구들도 자주 만나고, 취미생활을 자주 즐기려 했던 해였어요. 그래서 이렇게 재미를 추구하는 게 원동력이 되어서, 직장에서도 일을 열심히 할 수 있게 된 것 같아요.</p>
<blockquote>
<p><strong>5년 뒤 내 삶을 일·취미·사랑으로 나눈다면, 10을 기준으로 각각 얼마나 두고 싶나요?</strong></p>
</blockquote>
<p>일단 <strong>일은 3</strong>이고요, <strong>취미는 4</strong>이고, <strong>사랑이 3</strong>일 것 같아요.</p>
<p>현재 직장은 워라밸이 잘 지켜지는 편이어서, 퇴근 후에도 제가 하고 싶은 취미생활을 즐길 수 있는 게 장점인 것 같아요. 이를 발전시켜서 주말에는 말씀드렸던 피아노를 더 열심히 칠 수도 있을 것 같고, 열심히 일해서 번 돈을 모아 해외여행도 자주 갈 것 같아요. 제가 좋아하는 일을 열심히 한 것에 대한 보상으로, 재미있게 놀고 싶다는 생각이 들어 취미를 4로 뒀어요.</p>
<p>일은 매일 8시부터 5시까지 고정적으로 근무를 해야 하는 상황이기에 3으로 두었고, 사랑은… 제 인생에서 1순위를 사랑으로 두진 않을 것 같아요. 5년 뒤라면 제 미래에 대한 고민이나 생각이 더 가치 있다고 느낄 것 같아요.</p>
<blockquote>
<p><strong>어떤 삶을 살아가려고 노력하시는지 궁금해요. 자신만의 철학이나 가치관이 있다면 자유롭게 말씀해주실 수 있나요?</strong></p>
</blockquote>
<p>전 제가 하고 싶은 걸 다 해야 직성이 풀리는 성격이기 때문에, 솔직히 지금 취업을 한 이유도 경제적인 독립을 얻어서 하고 싶은 해외여행이 있으면 해외여행도 가고, 사고 싶은 물건이 있으면 직접 사고 싶어서인 것 같아요. 막… 그런 것들이요.</p>
<p>나중엔 조향사라는 꿈도 가지고 있어서, 조향사도 되고 싶고 메이크업 샵도 차리고 싶고, 어쨌든 저만의 하고 싶은 꿈들이 있어요. 그래서 제가 하고 싶은 일이 있다면 그 꿈에 매진하기 위해서 최선을 다하면서 살고 있는 것 같고, 결국엔 <strong>제가 하고 싶은 건 다 하고 살자</strong>라는 좌우명을 가지고 살고 있어요.</p>
<p>그리고 저는 인간관계를 되게 중요시해서, 제 주위 사람들이나 가족이랑도 행복한 추억을 많이 쌓고 싶은 생각이 있어요. 저 혼자 하는 것보다는, 제가 좋아하는 사람들이나 가족과 같이 좋은 시간을 보내는 걸 더 좋아하는 것 같아요. 지금 삶에서는 그게 제일 중요한 것 같아요.</p>
<blockquote>
<p><strong>본인이 소설가로 일한다면 어떨 것 같나요?</strong></p>
</blockquote>
<p>완판될 것 같은데요? (웃음) 소설가? 음…</p>
<p>저는 인간관계에 대한 얘기를 엄청 쓸 것 같아요. (소설? 음… 소설가?)</p>
<p>저는 긍정의 힘을 정말 중요시하는 사람으로서, 소설에서 내포하는 주제를 ‘긍정의 힘’으로 잡고 소설을 쓰는 사람이 될 것 같아요.</p>
<blockquote>
<p><strong>21살의 나에게 사랑이란?</strong></p>
</blockquote>
<p>저만의 안식처인 것 같아요. 왜냐면은, 제가 정말 사랑하는 사람이랑 있으면 직장 생활에서 받았던 스트레스가 풀리고, 그리고 어쨌든 제 일상을 매일 공유하는 사람으로서 가장 친하고, 가장 재밌고, 서로를 너무 응원하는 사이일 테니까요. 그래서 제 안식처일 것 같고요.</p>
<p>그리고 저도 그 친구(사랑하는 사람)한테, 조금… 제 미래의 그 친구에게 힘이 되고 싶을 것 같아요. 저한테도 의지를 했으면 좋겠고, 서로 의지가 되면서, 응원에 의해서 둘 다 긍정적인 미래를 같이 그려가는 관계로 발전하고 싶어요.</p>
<blockquote>
<p><strong>무언가 때문에 정말 힘들거나 아팠을 때, 어떻게 이겨내셨는지 궁금해요.</strong></p>
</blockquote>
<p>이겨내진 못했고, 그냥 견뎠던 것 같아요. 시간이 해결해 준다는 말도 맞는 것 같고요. …제가 진짜 힘들 때도 좋아하는 친구들이랑 맛있는 거 먹고, 예쁜 카페 가고, 가족들이랑 시간을 보내다 보니까 힘들었던 부분에 대해서 조언을 얻을 수도 있었고, 그 답변을 조언 삼아서 한 단계 더 성장할 수 있게 된 계기가 된 것 같아요.</p>
<p>그리고 제가 힘들었던 부분이 바로 나아지지 않는 상황이었기에, 혼자 가만히 휴대폰만 무기력하게 보고 있는 것보다는, 오히려 책을 읽으면서 다른 저자의 생각을 가져오거나 에세이 같은 걸 읽으면서 위로를 많이 받았던 것 같아요.</p>
<blockquote>
<p><strong>언젠가 정말 마음 아플 나에게 지금의 내가 메세지를 보낼 수 있다면, 어떤 이야기를 하고 싶으세요?</strong></p>
</blockquote>
<p>저는 고등학교 때 취업을 준비하면서도 힘들었고, 직장 생활에 처음 적응하는 것도 힘들었어요. 그 당시에는 그냥 힘들다고만 느꼈겠지만, 지금 직장 생활에 적응한 입장에서 돌이켜보면 힘들었던 순간마다 진짜 큰 성장을 했고, 그게 다 제 삶에 녹아들어서 저를 더 좋고 긍정적인 사람으로 만들어 준 것 같아요.</p>
<p>그래서 지금 힘든 순간이 5년, 10년 뒤 미래의 나에게 어떤 자산이 될지 오히려 기대된다고 생각할 것 같아요. 그러니까 지금 당장은 힘들더라도, 포기만 하지 말고 계속 견디고 버티다 보면 언젠가는 좋은 결과가 있지 않을까 싶어요.</p>
<p><strong>“항상 바라는 대로 잘 흘러가는 편이었으니까 이번에도 힘들어서 우울함에 갇혀 있기보다는, 민수 네가 바라는 목표랑 미래만 보고 현재를 오히려 즐기면서 살아”</strong>라고 말을 할 것 같습니다.</p>
<blockquote>
<p><strong>오늘 밤이 유성우가 떨어지는 날이고, 딱 한 가지 소원을 빌 수 있다면 어떤 소원을 비시겠어요?</strong></p>
</blockquote>
<p>저는 엄마, 아빠, 언니, 제 가족 구성원들이랑 오래오래 살고 싶다고 빌 것 같아요. 그게 제 소원이에요. 왜냐면은, 음… 어쨌든 고등학교 때도 학교에 계속 있다가 집에 돌아오면 하루가 끝나 있고, 직장 생활을 해도 아침 일찍 나갔다가 저녁에 들어오면 가족들이랑 있을 수 있는 시간이 저녁밖에 없잖아요. 주말에도 약속이 있으면 가족들이랑 보내는 시간이 자연스럽게 줄어들고요.</p>
<p>지금은 저도 아직 완전히 안정기라고 보기는 어렵고, 직장 생활이 주가 되는 시기이긴 하지만, 그래도 진짜 가족들이랑 오래오래 살면서 여행도 자주 다니고 싶고, 맛있는 저녁도 같이 먹고 싶고, 엄마 아빠 언니를 오래 보고 싶어요. 가장 저를 잘 알고, 배려해 주고, 제가 잘 되길 진심으로 바라고 응원해 주는 사람들이고, 되게 돈독한 관계라고 생각해서 같이 오래오래 살고 싶은 마음이 있는 것 같아요.</p>
<p>그리고 저는 제가 어떤 사람이든 저를 좋아해 준다는 저만의 믿음이 있어요. (웃음) 저도 물론 엄마 아빠가 어떤 사람이든 항상 감사하고, 오래오래 보고 싶은 마음이 있고요. 제가 힘들 때나 기쁠 때나 매 순간 같이 있었던 사람들이니까, 저한테 좋은 사람들이랑 오래 보고, 오래 같이 살고 싶은 마음이 있어요.</p>
<blockquote>
<p><strong>마지막으로, 좋아하는 노래 한 곡 추천해주세요.</strong></p>
</blockquote>
<p>(제가 빠진 피아노곡 소개해도 돼요?) 편곡하시는 분인데, 인스타에서 처음 발견했는데 너무 좋더라고요.</p>
<p><strong>Waltz for the Dead Clown - Single</strong>.</p>
<img src="https://velog.velcdn.com/images/ubin_ing/post/86c5277e-e57b-4672-b041-11953e265aa4/image.png" width="250px" height="250px" />

<p>이 곡을 꼭 정복하고 싶어요.</p>
<blockquote>
<p><strong>인터뷰는 어떠셨나요?</strong></p>
</blockquote>
<p>일단 질문의 질이 너무 좋았고, 계속 고뇌할수록 더 좋은 답변이 나올 것 같다는 생각이 들었어요. 다만 즉흥으로 말하다 보니 답변에 대한 아쉬움이 조금 남는 것 같고요.</p>
<p>솔직히 살아가면서 유튜브를 보거나, 영화를 보면서 시간을 보내는 등 시간을 무의미하게 흘려보낼 때가 많은 것 같은데, 이번 인터뷰는 지금까지 살아온 제 인생에 대해서 계속 생각하게 만드는 여운이 남는 시간이었어요. 그래서 제가 생각했던 것보다 훨씬 진중했고, 되게 좋은 인터뷰였던 것 같습니다.</p>
<p><em>e-mail: <a href="mailto:jom963538@gmail.com">jom963538@gmail.com</a></em></p>
<hr>
<h3 id="행복을-미루지-않고-지금을-살아가고-싶다---한태영-1">“행복을 미루지 않고, 지금을 살아가고 싶다” - 한태영</h3>
<div align="center">
  <img src="https://velog.velcdn.com/images/ubin_ing/post/85b52ee7-d583-45cb-ab12-df2c3a48b740/image.png" width="300px" height="300px" />
</div>


<blockquote>
<p><strong>안녕하세요, 간단하게 자기소개 부탁드립니다.</strong></p>
</blockquote>
<p>저는 에듀테크 분야의 IT 기업에서 백엔드 엔지니어로 근무하고 있고, 올해로 갓 스무 살이 된 한태영이라고 합니다.</p>
<blockquote>
<p><strong>40% 확률로 연봉 2배, 60% 확률로 연봉 1/2배가 되는 버튼이 있다면 누르시겠어요?</strong></p>
</blockquote>
<p>우와… 아…(엄청나게 고민하다가) 음… 저는 안 누르겠습니다. 제가 피파 온라인이라는 게임을 즐겨 했었는데, 선수 카드를 4강에서 5강으로 강화 성공할 확률이 50%거든요. 그런데도 10번 시도하면 8번 정도 실패하는 경향이 있어서, 개인적으로 40%라는 확률을 믿긴 힘들 것 같아요. (웃음)</p>
<blockquote>
<p><strong>본인이 제빵사로 일한다면 어떨 것 같나요?</strong></p>
</blockquote>
<img src="https://velog.velcdn.com/images/ubin_ing/post/f44d342c-ec2a-4b75-a820-44dd7282ca91/image.png" width="250px" height="250px" />


<p>실제로 베이킹을 즐겨 했었어요. 이전 취미 중 베이킹이 있었고, 하면서도 많은 호평을 받았던 것 같아서, 진지하게 제빵사로도 진로를 고민해 본 적이 있어요. 제빵사의 한태영은 기존의 제빵 시장에서와 다른 차별점을 가지고, 지금의 전공인 소프트웨어를 결합해서 좀 더 스마트하게 장사를 하는 사람이 될 것 같아요.</p>
<blockquote>
<p><strong>여가 시간을 주로 어떻게 보내시나요? 즐기고 있는 취미 또는 최근에 시간을 보냈던 일을 공유해주세요.</strong></p>
</blockquote>
<p>저는 사람을 만남으로 되게 많은 영감과 활력을 느끼는 것 같아서, 요즘은 여가 시간에 많은 사람들을 만나보려고 노력하고 있어요. 친한 사람뿐만 아니라 어색한 사람과도 만나 배워가고 알아가는 시간이 재밌어요. 다양한 사람들을 만나길 원해서 최근에는 사회인 야구팀에 들어가기도 했고, 전국 학생회장 출신들이 모이는 커뮤니티에서도 활동하고 있어요. 더불어 IT 동아리 한 곳에 지원해서, 그 동아리에서도 다양한 분들과의 만남을 기대하고 있습니다.</p>
<blockquote>
<p><strong>태영 님의 2025년을 마음에 드는 세 개의 단어로 표현해주세요.</strong></p>
</blockquote>
<p>저는 <strong>‘신념’</strong>, <strong>‘증명’</strong>, <strong>‘정리’</strong> 세 가지로 말씀드려보고 싶어요.</p>
<p>2025년을 시작하면서 고치고 싶었던 특징 몇 가지가 있는데, 그중 하나는 ‘나의 주관이 뚜렷하지 않다’ 였어요. 그래서 주관을 가지기 위해서 어떤 걸 할 수 있을까 고민하다가, 내가 어떤 걸 좋아하고 싫어하는지와 같은 것들을 정리했어요. 어떻게 보면 그런 것들이 모여서 신념이 될 수 있을 것 같아요. 그런 신념을 정리하면서 또 내가 가지고 있는 장점은 어떻게 살릴지, 단점은 어떻게 보완할지 확실히 알게 된 것 같아서 <strong>‘신념’</strong>을 꼽았어요.</p>
<p>다음은 <strong>‘증명’</strong>입니다. 글이 올라가는 시점엔 졸업생이겠지만 현재는 고등학교에 재학 중인 상태예요. 학교에서 현장실습이라는 제도로 취업을 준비하는 기간 동안 주변의 여러 기대감을 안고 있었어요. 저에 대한 긍정적인 타이틀이 많았는데, 사실 그때는 저조차도 확신이 없었어요. (나 자신보다 높은 평가를 받고 있다고 생각했거든요) 그래서 실제로 내가 이런 평가를 받는 게 유효한가에 대한 증명을 받고 싶었고, 또 증명받을 여러 기회가 있어서 이를 거듭하며 나에 대한 ‘신념’도 채우는 데 도움이 되었던 것 같아요. 그래서 올해는 나를 <strong>‘증명’</strong>하는 해가 아니었나 싶은 생각이 들었어요.</p>
<p>세 번째는 정리인데, 음… …되게, 올해는 학생이라는 신분에서 사회 초년생으로 바뀌면서 근처의 환경이라든지, 가질 수 있는 생각들이 많이 바뀌었던 것 같아요. 그런 상황들 속에서 2025년의 끝자락에서 많은 것들을 정리했고, 잘 정리했던 해인 것 같아서 <strong>‘정리’</strong>라는 키워드를 마지막으로 뽑아보고 싶었어요.</p>
<blockquote>
<p><strong>만약 2026년을 표현한다면 마음에 들었던 세 개의 단어 중 하나를 빼고, 두 개를 추가해야 해요. 어떻게 바꾸시겠어요?</strong></p>
</blockquote>
<p><strong>‘정리’</strong>라는 키워드를 빼고, 말씀드렸던 것처럼 <strong>‘몰입’</strong>, 그리고 <strong>‘여과’</strong>라는 단어를 추가하고 싶어요.</p>
<p>여러 경험을 통과시켜서 중요한 의미만을 남기는 과정이라는 뜻으로 <strong>‘여과’</strong>를 말씀드려봤어요. 제가 지금까지 다양하고 많은 경험을 겪었다고 생각해서, 그 경험들을 여과해서 진짜 중요한 의미들을 얻고, 그걸로 몰입하는 삶을 살아보고 싶어요.</p>
<p>제가 지금 가진 신념은 꽤 유의미하다고 생각해요. 이 신념은 오래 가지고 싶기 때문에 신념은 (빼고 싶은 단어가) 아니에요. 또 개발자는 항상 증명해야 하는 삶을 살아야 한다고 생각해요. (또다시 보여줘야 돼~) 그래서 뭔가 여과와 정리가 비슷한 키워드일 것 같은데, 정리에서 한 단계 더 나아간 게 여과인 것 같아서, <strong>정리</strong>라는 키워드는 빼보고 싶네요.</p>
<blockquote>
<p><strong>어떤 삶을 살아가려고 노력하시는지 궁금해요. 자신만의 철학이나 가치관, 또는 고민 중인 게 있다면 자유롭게 말씀해주실 수 있나요?</strong></p>
</blockquote>
<p>(저는 이걸 되게 중요하게 생각하고, 항상 이걸 되게 깊이 생각하는 걸 좋아하기 때문에, 이 글을 보고 계신 분 중 이 주제에 대해서 저랑 말씀을 나눌 분이 있다면 커피챗을 신청해주세요.)</p>
<p>제가 지향하는 삶은 한참 뒤인 80살, 그 나이가 되고서 지금의 저를 보았을 때 되게 ‘후회 없이 살았구나’, ‘지금까지 살아왔던 모든 순간이 행복했구나’라는 생각이 들었으면 좋겠어요. 제가 행복할 수 있는 일인 것 같다면 과감하게 시작하는 것 같고, 또 뭔가… 지금 시기에는 되게 많은 걸 경험할 수 있기 때문에 다양한 경험을 해보려고 노력하고 있는 것 같아요. <strong>여러 경험이 많고, 그 경험들에 대해 후회하지 않는 삶</strong>을 살고 싶은 것 같습니다.</p>
<blockquote>
<p><strong>20살의 나에게 사랑이란?</strong></p>
</blockquote>
<p>20대의 사랑은 완성된 감정으로 할 수 없다고 생각해요. 미숙한 감정들로 사랑을 할 수 있을 것 같은데 완성된 감정보단 연습에 가깝고, 확신보다는 확인에 가깝고, 영원을 약속하기보단, 음… 지금의 진심에 더 가까운 사랑인 것 같아요. 그래서 저는 지금의 진심을 표현하기 위해… 음… (한참 고민) 지금의 진심을… <strong>지금을 진심을 다해서 표현하고, 언젠가의 영원까지 약속하고 싶어요.</strong></p>
<blockquote>
<p><strong>언젠가 정말 마음 아플 나에게 지금의 내가 메세지를 보낼 수 있다면, 어떤 이야기를 하고 싶으세요?</strong></p>
</blockquote>
<p><strong>“태영아, 이미… 많은 산을 넘어온 나를 믿었으면 좋겠다.”</strong></p>
<blockquote>
<p><strong>오늘 밤이 유성우가 떨어지는 날이고, 딱 한 가지 소원을 빌 수 있다면 어떤 소원을 비시겠어요?</strong></p>
</blockquote>
<p>제가 생각하기에 행복할 수 있는 가장 쉬운 방법은(저는 지금 행복하고, 행복하고 싶은 사람이라서요) 현재의 나에게 감사하는 거라고 생각해요. 근데 사람은 당연히 내가 가진 것들에 대해서 무뎌지기 마련이라고 생각해서, 한 가지 소원을 빌 수 있다면 <strong>‘평생 처음처럼 내가 가진 것들에 감사하고 행복할 수 있는 삶을 살게 해 주세요’</strong>라고 빌 것 같아요.</p>
<blockquote>
<p><strong>마지막으로, 좋아하는 노래 한 곡 추천해주세요.</strong></p>
</blockquote>
<p><strong>에픽하이 - Love Love Love</strong>로 할게요.
<img src="https://velog.velcdn.com/images/ubin_ing/post/f364dba2-25f5-4cdb-a26c-a3056fe950ca/image.png" width="250px" height="250px" /></p>
<blockquote>
<p><strong>인터뷰는 어떠셨나요?</strong></p>
</blockquote>
<p>제가 생각했던 것과는 질문이 달라서 당황했지만(진로나 커리어적인 질문을 예상했거든요), 인터뷰를 하면서 평소에 하던 생각들을 정리할 수 있었던 시간이 된 것 같아요. 질문에 대한 답을 생각하면서 나는 사실 지금으로써도 행복하다고 생각하게 되었어요. 말씀하셨던 유성우가 소원을 들어준 기분이 든 것 같아요. 지금에 감사할 수 있었던 것 같아요.</p>
<p><em>instagram: @tx.xng__dev</em></p>
<hr>
<h3 id="입력-출력-체력---김은빈-1">“입력 출력 체력” - 김은빈</h3>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/e7c3a657-c8ab-4e48-8c38-12606a3ebc60/image.png" alt=""></p>
<blockquote>
<p><strong>안녕하세요, 간단하게 자기소개 부탁드립니다.</strong></p>
</blockquote>
<p>저는 06년생이고, 이제 스물한 살이 된 2년 차 DevOps 엔지니어로 일하고 있는 김은빈이라고 합니다.</p>
<blockquote>
<p><strong>40% 확률로 연봉 2배, 60% 확률로 연봉 1/2배가 되는 버튼이 있다면 누르시겠어요?</strong></p>
</blockquote>
<p>저는 안 누를 것 같습니다. 지금의 1/2이 되면 퇴사해야 할 것 같아서 (웃음) 지금 회사에 지금의 연봉으로 열심히 계속 다니고 싶습니다.</p>
<blockquote>
<p><strong>본인이 영화 감독으로 일한다면 어떨 것 같나요?</strong></p>
</blockquote>
<p>제가 생각하는 영화감독은 큰 그림을 그리는 사람인 것 같아요. 내가 표현하고 싶은 게 있으면 그걸 표현하기 위한 스토리나 상황을 만들고, 그 상황에 배치할 물건들이나 그것들을 촬영하는 각도 같은 걸 다 계산하고 계획해서, 결국 내가 보여주고 싶은 걸 보여주는 직업이라고 생각하거든요.</p>
<p>근데 저는 아직 복잡한 방식으로 표현하고 싶은 메시지가 뚜렷하게 있지는 않은 것 같아요. 그래서 오히려 좀 더 전하고 싶은 게 분명하게 생기면, 그걸 보여주기 위해서 이런 복잡한 준비를 하는 과정 자체가 더 재밌게 느껴질 수도 있지 않을까, 그런 생각이 드는 것 같습니다.</p>
<blockquote>
<p><strong>여가 시간을 주로 어떻게 보내시나요? 즐기고 있는 취미 또는 최근에 시간을 보냈던 일을 공유해주세요.</strong></p>
</blockquote>
<p>(2025년을 돌아보는 걸 주제로 주셔서) 2025년을 생각해 보면, 반 정도는 여유 시간 없이 회사에만 올인했고, 나머지 절반은 다른 것들을 했던 것 같아요. 아예 회사 생각을 안 할 수 있는, 약간의 독서나 일이랑 연관 없는 공부를 했던 것 같고요.</p>
<p>공부는 원래 조금 관심은 있었는데 시간을 못 냈던 수학이나 CS 기초, 암호학 같은 것들도 공부했었고, 그런 식으로 여가 시간을 보내면서 해소를 한 것 같습니다.</p>
<blockquote>
<p><strong>은빈 님의 2025년을 마음에 드는 세 개의 단어로 표현해주세요.</strong></p>
</blockquote>
<p><strong>‘입력’</strong>, <strong>‘출력’</strong>, <strong>‘체력’</strong> 이렇게 세 단어를 생각해 봤던 것 같아요.</p>
<p>저한테 2025년은 첫 번째 회사에 입사한 게 24년 7월이어서, 25년이 딱 회사에서 1년 사이클을 돌아본 첫해였거든요. 그래서 첫 번째 단어가 <strong>‘입력’</strong>이에요. 새로운 환경이기도 하니까, 다른 개발자분들이 어떤 식으로 일하시는지, 관심사나 행동 방식, 새로운 걸 학습하는 태도 같은 것들을 보고 배웠어요. 회사라는 환경에서 제품이 어떤 식으로 만들어지고, 그 제품의 아이디어들이 어떻게 수집되는지를 파악하려고 많이 지켜보고 관찰하면서 머릿속에 입력하려고 노력했기에 첫 번째 단어를 그렇게 정했고요.</p>
<p>그리고 처음에는 ‘입력’만 잘하면 된다고 생각했었어요. 그런데 더 많은 의견을 내줬으면 좋겠다는 동료분의 피드백을 받기도 했고, 팀 상황이 변하면서 저도 의견을 내야 하는 포지션이 되다 보니까, 입력한 것들을 어떻게 저만의 방식으로 출력해야 할지 고민을 시작하게 된 게 작년에 되게 중요했던 포인트였던 것 같아서 두 번째 단어를 <strong>‘출력’</strong>으로 정했고요.</p>
<p>마지막으로는 <strong>‘체력’</strong>인데, 위에서 말했던 경험을 쌓기 위해서 2025년에 70% 이상은 제 체력을 오버클럭하면서 정말 열심히 했었거든요. (약간 릴랙스한 시간도 있긴 했지만요) 또 체력이 남을 때는 러닝 같은 것도 하면서 제 체력을 잘 활용하려고 노력했기에, 세 번째 단어를 체력으로 생각해 봤습니다.</p>
<blockquote>
<p><strong>지금 삶에서 부족하거나 힘들다고 느끼는 것들이 있나요?</strong></p>
</blockquote>
<p>부족한 점이… 많죠. (웃음) 약간… 기댓값을 예상하는 능력이 아직은 좀 부족하다는 생각이 드는 것 같아요. 예를 들면, 이런 기술을 쓰면 어떤 효과가 있을 것이다, 제품에 어떤 기능을 넣으면 어떻게 될 것이다, 이런 기대를 하고 그 기대를 실현하게 하기 위해서 시도하는 과정들이 있잖아요. 인생을 살면서나 회사에서도요.</p>
<p>근데 제가 생각했을 때 안 될 것 같다고 느꼈던 방향으로 흘러가는데 되는 경우도 있고, 반대로 될 것 같다고 생각했는데 안 되는 경우도 있고, 그런 경험들이 반복되다 보니까 그런 과정에 아직은 잘 적응하지 못한 상태인 것 같아요. 그래서 지금은 뭐가 어떻게 될지 잘 모르겠는 조금 혼란스러운 상태인 것 같고요. 그런 점이 아직 부족한 부분이라고 생각합니다.</p>
<p>그래서 그럴 때마다 드는 생각은, ‘아직 내가 너무 어려서 그런 건가?’라는 생각도 들고, 약간… 지금처럼 계속 열심히 쌓아가다 보면 이런 경험들이 언젠가는 그런 걸 판단하는 데 도움이 되는 좋은 도구가 되지 않을까 하는 생각을 하고 있습니다.</p>
<blockquote>
<p><strong>만약 2026년을 표현한다면 마음에 들었던 세 개의 단어 중 하나를 빼고, 두 개를 추가해야 해요. 어떻게 바꾸시겠어요?</strong></p>
</blockquote>
<p>(근데 약간 ‘입력’이랑 ‘출력’이 한 세트인 것 같아서요. 그래서 <strong>‘체력’</strong>은 빼야 할 것 같고, 약간 라임을 맞추고 싶은데… (웃음) 되게 어렵네요.)</p>
<p>음… 일단은 ‘입력’은 앞으로도 저 혼자 쌓아나갈 수 없는 부분이라고 생각해서 계속 유지해야 할 것 같고요. ‘출력’은 이제 어떻게 하면 더 잘 출력할 수 있을지 고민을 시작한 단계라서, 26년에는 ‘입력’보다는 ‘출력’을 더 훌륭한 질로 유지하는 게 목표가 될 것 같아요. 그래서 이 두 개는 유지할 것 같고요.</p>
<p>그리고… <strong>‘실력’</strong>이요. 지금보다 더 실질적으로 영향을 끼치고, 성과를 확인할 수 있는 일들을 더 많이 하고 싶어서, 결국 실력을 키우고 싶은 마음이 커서 세 번째 단어는 ‘실력’으로 하고 싶어요.</p>
<p>그리고 그거 말고도 앞으로 살아가면서 아직은 어려서 그냥저냥 넘어가도 된다고 생각했던 것들이 조금 있었던 것 같은데, 그런 부분들도 포함해서 저만의 특징을 더 살리고, 지난 1년 반 동안 해왔던 것들을 잘 종합해서 저의 <strong>‘저력’</strong>을 보여줄 수 있는(웃음) 한 해를 보내면 좋지 않을까 싶어요. 그래서 그렇게 네 가지로 생각해 봤습니다.</p>
<blockquote>
<p><strong>5년 뒤 내 삶을 일·취미·사랑으로 나눈다면, 10을 기준으로 각각 얼마나 두고 싶나요?</strong></p>
</blockquote>
<p>일단 저는 일과 취미가 분리되지 않는 상태가 가장 행복한 상태라고 생각하긴 합니다. 물론 세상사가 다 마음대로 하나의 목표만 보고 달려갈 수는 없고, 그렇게 가다 보면 위험 부담도 있고 쉽게 지칠 수도 있기 때문에, 일과 취미를 어느 정도는 나눠서 챙겨야 하는 경우도 있는 것 같아요.</p>
<p>근데 그냥 희망 사항으로는, <strong>일이 8</strong>이고 <strong>취미가 0</strong>인 상태로 둘이 아예 합체될 수 있으면 좋을 것 같고요. 그리고 사랑이라고 하면, 그 사랑에 포함되는 게 뭔지에 대해서도 여러 가지로 해석할 수 있을 것 같은데, 주변 인간관계나 그런 것들도 한 <strong>2 정도</strong>는 잘 유지되면 좋겠다는 생각이 들어서, <strong>일 8</strong>, <strong>취미 0</strong>, <strong>사랑 2</strong> 정도로 분배할 수 있으면 좋을 것 같습니다.</p>
<blockquote>
<p><strong>인생에서 평생 하나를 없앨 수 있다면? 일 때문에 힘들기, 친구 때문에 힘들기, 사랑 때문에 힘들기</strong></p>
</blockquote>
<p>음… <strong>사랑 때문에 힘들어지는 건 없앨</strong> 수 있으면 좋을 것 같습니다.</p>
<p>왜냐하면 일은 힘들어야 제맛인 것 같고요. (웃음) 일이 안 힘들면 그만큼 보람도 적어질 것 같아요. 그래서 일과 힘듦, 그리고 보상은 떼어낼 수 없는 관계라고 생각해서, 일로 인한 힘듦은 있어도 괜찮을 것 같아요.</p>
<p>근데 나머지 두 개를 생각해 보면, 그중에서도 가장 답이 없고 복잡한 게 사랑인 것 같아요. 어떤 관계가 됐든, 상대가 잘 됐으면 좋겠다는 마음이 있는 관계일수록 어긋나는 순간들이 생기는데, 그 힘듦의 강도가 친구보다는 사랑에서 더 크게 느껴지는 것 같아서요. 그래서 만약 한 가지를 없앨 수 있다면, 사랑 때문에 힘든 걸 없애고 싶다는 생각이 드는 것 같습니다.</p>
<blockquote>
<p><strong>어떤 삶을 살아가려고 노력하시는지 궁금해요. 자신만의 철학이나 가치관이 있다면 자유롭게 말씀해주실 수 있나요?</strong></p>
</blockquote>
<p>짧은 요약 버전으로는 <strong>하고 싶은 일을 하면서 사는 삶을 살고 싶다</strong>고 말할 수 있을 것 같습니다.</p>
<p>제가 하고 싶은 거라면, …저는 코딩하는 것도 좋아하고, 책 읽고 글 쓰는 것도 좋아하는 편이에요. 그걸 종합해 보면, 어떤 거든 하나의 대상에 파고들어서 그걸 머릿속에서 논리적으로 정리하는 과정 자체를 재미있어하는 것 같아요. 그래서 앞으로도 글이 됐든, 코드가 됐든, 제 머리로 논리를 정리하는 일들을 계속하고 싶고요.</p>
<p>그리고 그 외에 불편한 상황들은 최대한 피하고 싶은데, 그걸 위해서 필요한 부수적인 것들이 되게 많은 것 같아요. 예를 들면, 제가 하고 싶은 일을 하기 위해서 다른 사람에게 빚을 지게 되면 그걸 계속 신경 써야 하니까, 도덕적으로 살아야 하는 부분도 있고요. 또 지금의 제 생각에만 너무 갇혀 있으면 객관적으로 논리를 정리하는 게 어려워질 수 있으니까, 다른 사람들이랑 소통하고, 다른 사람이 써 놓은 글이나 생각을 이해하는 능력도 키워야 할 것 같고요. 그런 의미에서 좋은 ‘입력’을 받을 수 있는 상황으로 제 몸을 밀어 넣는 능력도 필요하다고 느껴요.</p>
<p>또 다른 한편으로는, 만약 제가 하고 싶은 걸 할 수 있는 환경이 회사라면, 회사 안에서 ‘그냥 내가 하고 싶은 업무만 해야지’ 생각하는 게 오히려 그 환경 자체를 망가뜨릴 수도 있잖아요. 그래서 회사나 팀에서 목표로 하는 것들이 순탄하게 흘러갈 수 있도록 노력해야, 궁극적으로 제가 하고 싶은 일을 꾸준히 할 수 있다고 생각하고 있고요. 그래서 지금도 그런 식으로 많은 선택과 행동들을 하면서 살아가고 있는 것 같습니다.</p>
<blockquote>
<p><strong>21살의 나에게 사랑이란?</strong></p>
</blockquote>
<p>(‘사랑’이라는 단어 안에 되게 많은 게 포함될 수 있을 것 같은데요. 인간에 대한 사랑…? 에 대한 의견이 궁금하신 건가요? 아니면 물체에 대한 사랑이 궁금하신 건가요? <em>자유롭게 표현해 주실 수 있나요?</em> 아… 어려운데요. 음…)</p>
<p>약간 제가 생각하는 사랑이란, 이유가 없는데도 그 대상의 다양한 면들을 포용하고 싶어지는 마음인 것 같아요. 이유가 있어서 좋은 건 사실 그 이유가 전부인 거잖아요. 근데 그 대상이 생명체가 됐든, 생명체가 아니든, 물체가 됐든 아니면 추상적인 개념이 됐든, 그 대상의 좋은 점이랑 나쁜 점을 다 이해하고 싶고 잘 포용하고 싶은 마음, 그런 게 사랑이라고 생각하고요.</p>
<p>그래서 오히려 이유가 없기 때문에, 그 대상의 나쁜 점까지도 받아들여야 하는 감정이라서, 저를 포함한 많은 사람에게 사랑이 어려운 감정이 아닐까… 정도로 생각이 드는 것 같습니다.</p>
<blockquote>
<p><strong>무언가 때문에 정말 힘들거나 아팠을 때, 어떻게 이겨내셨는지 궁금해요.</strong></p>
</blockquote>
<p>힘듦이랑 아픔에도 여러 가지 종류가 있는 것 같아요. 단어를 어떻게 해석하느냐에 따라서 되게 다양하게 나뉠 수 있을 것 같고요. 신체적인 아픔이 됐든, 정신적인 힘듦이나 고통이 됐든, 그 이유가 뭔지, 왜 아픈지 아는 고통은 오히려 움직일 수 있는 동력이 되는 것 같다고 생각해요.</p>
<p>예를 들면, 시험을 봐야 하는데 준비가 안 된 상태라서 너무 불안하고 힘들다고 하면, 이 상태에서 벗어나기 위해서 공부를 해야겠다는 방향으로, 열정적으로 움직일 수 있잖아요. 그리고 그렇게 움직이는 동안에는 뭔가 뿌듯하고, 살아 있다는 감정도 느껴지고요. 그런 식인 것 같아요. 물리적으로 배가 고픈 상태라면, 맛있는 걸 먹기 위한 방향으로 움직일 수 있고요.</p>
<p>그래서 내가 지금 힘들고 아프고, 좀 굶주린 상태이더라도 목표가 분명히 있는 상황이면, 그 힘듦이 오히려 기쁨으로 금방 전환될 수 있다고 생각해요. 근데 반대로, 이유를 알 수 없는 힘듦이나 고통은 조금 다른 것 같아요. 예를 들면 내가 분명히 목표로 하던 게 있어서 그쪽으로 가려고 하는데, 어떻게 해도 이 상황이 해결되지 않을 것처럼 느껴지거나, 아니면 내가 목표로 했던 게 진짜 목표가 맞나? 정도로 의심이 들고 회의감이 생길 때, 그런 게 오히려 진짜 괴로움에 가까운 것 같아요.</p>
<p>약간 미래를 상상할 수 없을 때가 진짜, …진짜 고통에 가까운 순간인 것 같고요. 저는 사회생활 경험이 아직 많지는 않지만, 이제 1년 정도 됐는데, 사회에 나와서도 그런 순간들이 꽤 있었던 것 같아요.</p>
<p>그럴 때 그 고통에 대한 대응 방안으로는, (사실 아직 명확한 답은 잘 모르겠지만) 지금 제가 드는 생각은 그냥… 뭐라도 하는 거? 인 것 같아요. 큰 목표가 됐든, 취미가 됐든, 아니면 다른 목표가 됐든, 심지어 게임하는 거라도, 내가 감당할 수 있는 범위 안에서 뭐라도 하는 거요. 원래 목표했던 방향이랑 조금 다를 수도 있고, 어떻게 보면 회피에 가까운 행동일 수도 있지만, 어쨌든 뭐라도 하면 그게 또 쌓여서 새로 하고 싶은 게 생기기도 하고, 다시 움직일 수 있는 방향의 씨앗이 되기도 하는 것 같아서요.</p>
<p>그래서 뭐라도 하는 게 제가 생각하는 고통을 이겨내는 방법인 것 같습니다.</p>
<blockquote>
<p><strong>언젠가 정말 마음 아플 나에게 지금의 내가 메세지를 보낼 수 있다면, 어떤 이야기를 하고 싶으세요?</strong></p>
</blockquote>
<p>저는 알아서 잘할 거라고 생각합니다. 너무 힘들어서 몸부림을 치고 있거나 부정적인 생각에 완전히 잠겨 있을 수도 있겠지만요. 그래도 지금의 저보다 더 많이 살았으면 알아서 하겠죠? (웃음)</p>
<blockquote>
<p><strong>오늘 밤이 유성우가 떨어지는 날이고, 딱 한 가지 소원을 빌 수 있다면 어떤 소원을 비시겠어요?</strong></p>
</blockquote>
<p><strong>‘미래에 힘들어할 나 자신이 잘 이겨낼 수 있었으면 좋겠다’</strong>라는 소원을 빌고 싶습니다. 지금 당장은 크게 원하는 게 없는 것 같고요. 그리고 지금의 제가 잘되는 건, 사실 노력하면 지금도 할 수 있는 거니까요. 그래서 만약 소원을 빌 수 있다면, 미래의 제가 잘 이겨냈으면 좋겠다는, 조금은 비현실적인 소원을 빌어보고 싶어요.</p>
<p>(<em>약간 츤데레시네요?</em>) 그쵸. (웃음) 앞에서는 “알아서 하겠지”라고 말하면서, 마음속으로는 “잘 됐으면 좋겠다”라고 생각하니까…</p>
<blockquote>
<p><strong>마지막으로, 좋아하는 노래 한 곡 추천해주세요.</strong></p>
</blockquote>
<p>제가 좋아하는 책에서 중요한 테마로 다루는 노래가 하나 있는데요,</p>
<p><strong>Sophie Tucker – Some of These Days</strong>라는 곡이에요.
<img src="https://velog.velcdn.com/images/ubin_ing/post/2a4346ce-3b90-43bc-af76-6e6977979424/image.png" width="250px" height="250px" /></p>
<p>이게 약간 옛날 재즈곡 같은 느낌이라서, 정확한 원조가 누군지는 잘 모르겠는데… 이 노래가 사르트르가 쓴 『구토』라는 책에서 언급이 되거든요.</p>
<p>그 책 주인공이 “내가 어떤 목표로 사는 거지?”, “나라는 사람의 본질이 뭐지?” 같은 걸 계속 고민하고 찾으려는 인물인데, 이 재즈곡을 들으면서 되게 위로를 받아요.</p>
<p>재즈가 즉흥적이고, 어느 정도 흐름은 있는데, 그걸 표현하는 방식은 그때그때 하고 싶은 대로, 시간 흘러가는 대로 되는 느낌이잖아요. 그런 음악을 들으면서 주인공이 위로를 받는 장면이 되게 인상 깊었고요. 그래서 저도 이 노래를 제일 좋아하고, 제일 많이 나는 생각도 나는 노래라고 할 수 있을 것 같습니다.</p>
<blockquote>
<p><strong>인터뷰는 어떠셨나요?</strong></p>
</blockquote>
<p>연락 주셨을 때도 들었던 생각이 있는데요,</p>
<p>약간 평소에 가치관이나, 내가 어떤 생각을 하고 있는지는 머릿속에 분명히 있긴 한데, 그게 완전히 정리된 상태로 들어 있는 건 아니잖아요. 그냥 흐리멍텅하게 있는 느낌?</p>
<p>근데 이렇게 질문을 해주시고, 그걸 말로 표현해보는 과정 자체가, 질문해주신 덕분에 저도 제 생각을 다시 한 번 정리해볼 수 있는 기회가 된 것 같다는 생각이 들었어요.</p>
<p>그래서 되게 감사한 경험이구나 생각이 들었습니다.</p>
<p><em>e-mail: <a href="mailto:rlaisqls@gmail.com">rlaisqls@gmail.com</a></em></p>
<p><em>github: <a href="https://github.com/rlaisqls">https://github.com/rlaisqls</a></em></p>
<h3 id="마무리">마무리</h3>
<p>특이한 케이스로 어린 나이에 사회 생활을 시작한 일곱 명의 이야기, 어떠셨나요?
인터뷰를 준비한 작성자인 저 또한 어린 나이에 사회 생활을 시작해 2026년 스물한 살이 되었습니다. 성인으로의 1년을 마치고, 나 자신에게 스스로 묻고 싶은 질문들을 나와 비슷한 사람들에게도 물어보고 싶다는 궁금증을 필두로 매거진을 작성해보았습니다.</p>
<p>편한 마음으로 인터뷰를 읽어보고, 더 궁금하거나 알아가고 싶은 사람이 있다면 각 섹션에 남겨진 연락처로 연락해주세요.</p>
<p>2025년도 고생 많으셨고, 다가온 2026년도 행복한 한 해 되시길 바라겠습니다.
끝까지 읽어주셔서 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발자 취업 1년만에 Product Owner가 되었습니다]]></title>
            <link>https://velog.io/@ubin_ing/to-product-owner</link>
            <guid>https://velog.io/@ubin_ing/to-product-owner</guid>
            <pubDate>Wed, 05 Nov 2025 14:38:19 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 24년 10월, 웹 프론트엔드 엔지니어로 취업한 후 1년 만에 Product Owner 직무로 직군변환한 경험을 회고와 함께 공유해보려 합니다.</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>웹 프론트엔드 개발자로서 1년 동안의 경험들을 회고합니다.</li>
<li>Product Owner로 전환한 계기와 함께 지금까지의 PO 업무를 회고합니다.</li>
</ul>
</blockquote>
<h3 id="🎯-온보딩-과정에서의-기술-스택-딥다이브">🎯 온보딩 과정에서의 기술 스택 딥다이브</h3>
<p>저는 평소에 관심이 많았던 핀테크 회사에 수습생으로 입사하여 3개월을 보냈어요.</p>
<p>첫 주에 팀 리드님, 사수님과 함께 온보딩 목표를 &#39;사용하는 기술 스택의 동작 원리 이해하기&#39;로 정하고 업무를 시작했습니다. React와 React Query, Next.js의 코어에 대해 딥다이브하고 각각의 주제로 웹 팀원들을 대상으로 세 번의 세미나 발표를 진행하는 것이 최종 목표였어요.</p>
<h4 id="빠르게-잘하고-싶다는-마음가짐">&#39;빠르게, 잘&#39;하고 싶다는 마음가짐</h4>
<p>처음으로 회사에 입사한 사회초년생은 알게모르게 &#39;무언가를 증명해내야 한다&#39;라는 압박과 욕심이 생기더라구요. 정규직으로 전환되지 못하더라도 온보딩 목표를 1개월만에 끝내고 2개월 동안은 실무를 다룰 수 있도록 해보자는 개인적인 목표를 가지고 공부했습니다.</p>
<h4 id="기술을-사용하는-것과-이해하고-사용하는-것">기술을 사용하는 것과 이해하고 사용하는 것</h4>
<p>처음에는 규모 있는 사이드 프로젝트를 개발하고 유지/보수하는 입장에서 제가 쓰고 있는 기술을 잘 알고 있다고 생각했어요. 그런데 막상 &#39;동작 원리&#39;를 알아보려고 하니 너무 새롭고 난이도 있는 내용을 학습해야 해서 숨이 턱 막혔습니다. 구글링을 해보았을 때도 기술을 사용하는 방법을 다루는 자료는 너무나도 많지만, 막상 그 기술이 내부에서 어떻게 작동하고 어떤 식으로 구현되어있는지에 대한 자료는 너무 적었어요. 세미나를 준비하면서 구글과 많은 시간을 보냈고, 끝에 어느 정도의 자료를 찾는 노하우를 터득하게 되었습니다. <em>(1년 전까지만 해도 GPT 답변 수준이 적절한 구글링보다 성능이 좋지 못했고, 당시에 저는 AI 비관론자였답니다)</em></p>
<h4 id="공식-문서-source-code-discussion">공식 문서, Source Code, Discussion</h4>
<p>제가 원하는 정보는 거의 모두 이 세 가지 요소들에 포함되어 있었어요. 현재도 트러블 슈팅이나 내부 동작 원리, 기술적인 논의 주제를 찾아볼 때는 이 요소들을 애용하곤 합니다.</p>
<p><strong>공식 문서</strong>
공식 문서는 라이브러리/프레임워크를 제공하는 개발자들이 직접 사용 방법과 동작 방식에 대해 적어둔 문서이기 때문에, 사실 공식 문서만 열심히 읽어도 단순한 궁금증은 해결됩니다. </p>
<p>Next.js의 공식 문서에서 처음 렌더링 방식과 관련된 문서를 접했는데, 제가 모르고 사용하고 있는 내용들이 너무 많았더라구요. 그래서 Next.js의 공식 문서를 처음부터 끝까지 다 읽어가며 세미나에 발표할 내용들을 정리했고, 당시 Next.js 공식 문서 한글 번역 프로젝트에도 기여해보았었습니다.</p>
<p>또 저는 개인적으로 React Query를 너무너무 좋아하는데, React Query를 딥다이브할 때는 개인적인 애정과 더불어 동작 방식도 너무 흥미로워서 혼자서 <a href="https://react-query.kro.kr/docs/getting-started">React Query v5 문서 전체를 한국어로 번역하는 프로젝트</a>를 진행했어요. 완성된 문서를 배포한 후 처음으로 100개가 넘는 Github Star를 받았습니다. 공부를 위해서 번역한 내용들을 정리하고, 커뮤니티에 공유하여 긍정적인 반응을 얻는 경험은 정말 즐겁고 짜릿했어요.</p>
<p><strong>Source Code</strong></p>
<p>특정 기술의 동작 원리에 관심이 있는 경우, 리포지토리에 방문해 직접 소스 코드를 분석해보는 게 제일 정확합니다. 처음에는 오픈 소스 코드를 읽는다는 것 자체가 처음이라 코드가 잘 읽히지 않았어요. 그런데 잘 찾아보면 오픈 소스의 코드 일부들을 인용하여 라이브러리를 설명하는 수준 높은 블로그 글들이 있더라구요. 그런 글들과 함께 소스 코드를 확인하다보니 점점 글 없이도 오픈 소스 원문을 볼 수 있게 되었어요. 원문을 보며 동작 원리를 이해하는 데 확신(더블체크)을 가질 수 있고, 추후 라이브러리를 사용하다가 개선 포인트가 생각나면 직접 기여를 할 수도 있게 되었습니다.</p>
<p><a href="https://velog.io/@ubin_ing/react-query-options-basement-pattern">React Query를 queryOption 기반으로 관리하는 패턴</a>을 자주 애용했는데요, 문득 useQuery의 파라미터를 Type Safety하게 반환해주는 queryOptions라는 함수는 존재하는데 useMutation을 관리하는 mutationOptions는 왜 존재하지 않을까 싶은 의문이 들었어요. 의문을 기반으로 <a href="https://github.com/TanStack/query/pull/8960">React Query에 feature PR</a>을 올렸고, 리뷰 과정에서 메인테이너분께 많이 두들겨맞았지만 여러 개발자들의 많은 관심을 받고 기능이 Merge되게 되었습니다.</p>
<p>Next.js를 사용하면서도 next/router의 SingletonRouter를 클라이언트 사이드에서 사용하면 context와 관련된 모든 필드(router.query, router.isReady 등)들이 작동하지 않는다는 걸 발견했어요. 그래서 <a href="https://github.com/vercel/next.js/pull/80436">SingletonRouter에서는 context 관련 필드를 반환하지 않도록 하는 PR</a>을 올렸는데, 아직까지도 Merge나 Close는 되지 않은 상태입니다 😅</p>
<p><strong>Discussion</strong></p>
<p>커뮤니티에서 기술적으로 논란이 되고 있는 주제를 확인하고 싶을 땐 Discussion을 확인하는 게 제일 정확합니다. Discussion은 대부분 찬/반으로 나누어지기 때문에, 현재 기술의 동작 원리와 장단점을 깔고 시작해 재미없을 수가 없는 요소에요. 저 또한 불타는 감자들이 생길 때마다 Github Discussion에서 Comment를 읽으며 동작 원리와 소스 코드를 함께 확인했습니다.</p>
<p>지금까지 React의 Suspense 작동 방식 변경에 대한 Discussion, Next.js의 Streaming metadata, Partial prerendering과 관련된 Discussion들을 탐방해봤어요. 공식 문서와 Source Code에 비하면 난이도는 훨씬 높지만, 그만큼 기술 이슈에 딥하게 집중할 수 있다는 점이 매력적입니다. 또한 라이브러리 개발자들이 어떤 철학을 가지고 라이브러리를 개발하는지와 같은 뒷 단의 뒷 단 관련 내용들도 알 수 있어 저는 개인적으로 정말 좋아합니다.</p>
<h3 id="🦿-현업과-실무-성급한-건-빠른-게-아니다">🦿 현업과 실무, 성급한 건 빠른 게 아니다</h3>
<p>여담이지만 저는 글쓰기를 좋아해서 글을 쓸 때는 단 한 글자도 AI를 사용하지 않습니다. 이모티콘으로 소제목을 나누다보니 AI가 쓴 글 냄새가 나서 찔려서 적어봅니다 😗</p>
<p>개인적으로 세웠던 목표대로 정말 1개월만에 세 개의 세미나를 열어 온보딩을 빠르게 끝낼 수 있었습니다. 온보딩 이후에는 회사의 소스 코드에 적응한 후, 실무 작업을 할당받아 PO, Design, QA, BE 등 다른 직군의 팀원들과 직접 소통하는 경험을 접했어요.</p>
<p>직접 현업에서 일을 해 보니 너무 명확한 두 가지를 느낄 수 있었어요. 첫 번째는 내 업무 처리 속도가 매우 빠르다는 것이었고, 두 번째는 빠른만큼 실수가 많고 디테일이 매우 부족하다는 것이었습니다. </p>
<p>사이드 프로젝트를 진행할 때는 비교 대상이나 평가 대상이 없었기 때문에, 제 개발 속도가 어느정도인지를 인지하지 못하고 있었어요. 그런데 현업 개발자분들께 속도가 정말 빠르다는 평가를 듣고 처음으로 &#39;개발 속도가 빠르구나&#39;라고 인지하게 되었습니다.</p>
<p>그런데 함께 알게 된 건 잔실수가 너무 많고 빠르기보다는 성급하게 일을 처리한다는 것이었습니다. 기획서나 논의 내용에 서술된 내용임에도 빠뜨리고 개발 버전을 전달드린다거나, Happy path로 발생한 QA 티켓이 10개 넘게 생성되는 등 성급한 일처리로 인해 타팀에 리소스가 더 많이 발생하게 되는 죄송한 일이 많았어요. 리드님에게도 항상 속도보다 정확한 해결이 더욱 중요하다고 피드백 받았습니다. </p>
<p>이후 타팀에 결과물을 전달드리기 전에 3번 확인하자는 규칙을 세워 잔실수를 대폭 줄일 수 있었어요. IDE에서 코드를 Commit할 때 한번, Github PR을 올렸을 때 한번, Merge된 후 테스트가 가능할 때 직접 사용하여 한번 총 세 번을 확인하는 프로세스를 자체적으로 거쳤어요. 그렇게 몇십개씩 나오던 실수들이 한 자릿수로 대폭 줄게 되었고, 타팀의 업무 효율 향상이나 리소스 최적화보다는, &#39;나 자신에게 떳떳해지는 경험&#39;을 할 수 있었습니다.</p>
<h3 id="😎-프로덕트-업무와-테크-업무-그리고-디자인시스템">😎 프로덕트 업무와 테크 업무, 그리고 디자인시스템</h3>
<p>다행스럽게도 3개월 동안의 온보딩이 끝나고, 정규직으로 전환에 성공해 정식으로 웹 프론트엔드 개발자가 될 수 있었어요. 작년 10월부터 올해 10월까지, 약 1년 동안 했던 여러 업무들을 세 가지로 나누어서 설명드려볼게요.</p>
<p><strong>프로덕트 업무</strong></p>
<p>타 부서에서 기획을 통해서 전달받는 업무 즉, 실제로 고객이 사용할 수 있는 무언가를 새로 개발하거나 유지/보수하고, 삭제하는 등의 작업을 프로덕트 업무라고 정리했어요. 저희 회사의 소스 코드는 서비스를 도메인별로 나눈 모노레포 형식으로 구성되어 있었는데요, 나름 영역별로 분류까지 할 수 있을만큼 레포들의 난이도가 명확했어요. </p>
<p>처음 정규직으로 전환됐을 때는 제일 매출 영향이 낮고 이슈 영향도 또한 낮은 백오피스 업무를 위주로 진행했어요. 그러나 다시 생각해보면 우매할 수 있지만 그 때는 백오피스 업무가 직접 고객이 사용하는 화면을 다루는 것도 아니고, 반복되는 작업이 많다보니 조금 지루하다고 생각했었습니다. 그래서 마침 요청받았던 큰 규모의 백오피스 업무 하나를 완료하고 나서, 리드님께 다른 도메인을 맡아보고 싶다고 말씀드렸어요. 이후 2개월만에 백오피스를 벗어나 고객이 직접 사용하는 도메인의 레포를 도맡아 관리하게 되었습니다.</p>
<p>정규직으로 전환한지 반 년 정도가 지나갔을 때 즈음, 타 1금융권과 밀접히 협업하여 몇 개월에 걸쳐 개발해야 하는 대규모 프로젝트를 할당받았어요. 회사 이름이 걸린 1금융권의 PLCC 카드를 발급하는 개설 플로우를 개발하는 일이었는데, 리드님이 매니징해주시는 상태에서 같은 팀의 미들 개발자 한 분과 제가 담당자가 되었어요. 개발해야 하는 화면은 30개 내외였는데 단순한 화면 구현은 미들 개발자분과 5:5 비율로 작업을 나누어 담당했습니다. 물론 요구사항이 쉬운 화면도 있고, 어려운 화면도 있는데 어려운 화면은 대부분 도맡아주셔서 비교적 쉬운 작업 위주로 진행할 수 있었어요.</p>
<p>문제는 QA 및 소통이었는데요, 외부 금융사와 협력하는 프로덕트이다보니 하루에 몇십 통씩 전화로 금융사와 발을 맞추는 과정이 필요했습니다. 그런데 타이밍이 좋지 않게 출시 전 QA 단계가 미들 개발자분의 리프레시 휴가와 겹쳐 혼자 최종 QA를 대응하고 소통했어야 했어요. 1금융권 개발 부서 과장님과 하루에 20~30통씩 전화를 하며 이슈 제보를 받고 대응하는 일을 2주 정도 반복했습니다. 정신적으로 에너지 소모가 심하고 하루 종일 트러블 슈팅만을 하다보니 많이 힘들었던 시기였어요. 그런데 저는 개발을 잘한다는 건 얼마나 이슈 파악이 빠르고 트러블 슈팅을 잘하는지에 따라 결정된다고 생각하거든요. 프로젝트가 끝나고 이후 다른 업무를 할 때에도 그 경험들 덕에 실력이 향상된 기분이 들었고, 개인적으로도 &quot;1년차인 내가 대기업 과장님과 직접 소통하고, 대규모 프로젝트에 비중 있게 기여했어!&quot;와 같은 생각이 멤도니 매우 뿌듯했어요. 이후 직접 제가 만든 플로우를 통해서 카드 발급도 받아 지갑에 끼워넣고 다니고 있습니다 😊</p>
<p><strong>테크 업무</strong></p>
<p>직접적으로 화면이 변경되거나 로직이 바뀌진 않지만, 클린 코드와 성능 최적화와 관련된 업무들을 테크 업무로 정리했어요. 레거시 코드를 마이그레이션하는 것부터 시작해서 컴포넌트의 props drilling을 줄이거나 컴포넌트들간의 의존성을 역전시키고, 도메인이 붙어있는 컴포넌트에 도메인을 제거해 공통 컴포넌트로 묶어 재사용하는 등 클린 코드를 구성하는 데 힘 썼어요. 하반기에는 팀원들과 함께 FSD 아키텍처에 대해 깊게 조사하고, 맡고 있는 도메인의 레포에 직접 적용해보는 등의 마이그레이션도 진행했습니다. FSD는 파볼수록 매력적이더라구요, 처음에는 단순히 API 관련 코드, 컴포넌트 코드, 이를 묶은 화면 코드와 유틸 코드를 features, entities, widgets 등으로 이름만 바꾸어두었다고 생각했는데, 깊게 이해해보니 이를 코드 기반이 아니라 화면 기반으로 구현한다는 걸 이해할 수 있었어요. entities에도 리액트 컴포넌트가 들어갈 수 있고, features는 화면이 아니라 기능이며, 어떤 레이어가 모인 특정 레벨을 구현하는 레이어가 있다는 규칙 등 알면 알수록 직관적이고 매력적이었습니다. 프론트엔드에서 FSD 구조가 왜 하입받고 있는지 공감할 수 있는 경험이었어요.</p>
<p><strong>디자인시스템 업무</strong></p>
<p>회사에 들어오고나서 PO가 되기 전까지 디자인시스템 관련 업무를 계속 진행했어요. 사실 제일 어려운 업무같기도 해요. 컴포넌트를 만드는 건 쉬운 일이지만, 이 컴포넌트들을 개방성 있게 만들고 사용하기 쉽게 만드는 건 정말 어려운 일이더라구요. 엣지 케이스를 고려하지 못한 경우 컴포넌트를 사용하는 입장에서 자잘한 hack을 넣어야 한다거나 하는 등 불편함이 계속해서 발생했어요. 추후에는 shadcn이나 radix ui같은 오픈 소스들의 컴포넌트 코드를 참고했고, 이 덕에 직군변환 전 마지막으로 만든 컴포넌트는 정말 사용하기 쉽고 확장성 있게 개발하여 사용하는 모든 케이스를 커버할 수 있었어요. 또 기존 컴포넌트들을 새 컴포넌트로 마이그레이션하는 과정에서 너무 많은 리소스가 발생해서, 스크립트를 통해 한 번의 실행으로 모든 코드를 마이그레이션하는 codemod도 직접 개발했습니다. 여러모로 힘든 경험이었지만, 그만큼 실력 향상에 도움이 되는 경험이었어요.</p>
<h3 id="🤗-product-owner로의-전환-계기">🤗 Product Owner로의 전환 계기</h3>
<p>상반기 웹 팀 목표 중 하나가 금융 관련 계산기를 N개 출시하는 것이었는데요, 막상 상반기가 끝났을 때 잡았던 목표에 비해 계산기를 몇 개 출시하지 못한 상태였어요. 그래서 당시 PO분과 회의할 때 아이스브레이킹 느낌으로 &#39;이런 계산기 만드는건 어떻게 생각하시나요?&#39;와 같이 가볍게 말을 꺼내봤어요. 그런데 그 주제가 트래픽도 나름 괜찮고, 안 할 이유가 없다며 PO분께서 기획서를 작성해서 대표님들께 전달드려보라고 해주셨습니다. 이후 짧게 기획서를 작성해서 대표님들께 전달드렸고, 다행히 좋게 봐주셔서 해당 계산기를 직접 기획하고 개발하여 운영 배포까지 나가는 재미있는 경험을 할 수 있었어요.</p>
<p>한 달마다 전사적으로 진행하는 세션이 있는데, 해당 세션에서 대표님께서 제 디자인닥(기획서)을 직접 언급해주셨어요. 그래서 해당 디자인닥을 필두로 PO가 아닌 팀원도 기획서를 제출하고 추진해볼 수 있는 이벤트가 진행됐는데, 그 때 개인적으로 생각하고 있던 규모 있는 서비스 디자인닥을 추가로 하나 더 제출드렸습니다.</p>
<p>디자인닥을 제출하고서 대표님께 따로 1on1 커피챗 요청을 받았는데, 다름이 아니라 지금 하고 있는 프론트엔드 개발자에서 Product Owner로 전향할 생각은 없는지와 관련된 직군 변환 제안이었어요. 처음 제안을 받았을 땐 조금 당황했었어요. 개발자로 5~7년 정도 미들 레벨 급의 연차를 쌓으면 PO로 직군변환해보고 싶다는 생각 정도가 있었는데, 이 기회가 취직한지 1년만에 다가올 줄은 몰랐었기 때문입니다. 그래서 처음에는 제출드렸던 디자인닥을 기반으로 진행하고 있는 개발 일과 PO 일을 병행하며 진행해봐도 되냐고 여쭤봤어요. 그런데 너무 흔쾌히 계속 도와줄테니 편한대로 마음껏 경험해보고, 천천히 원하는대로 결정하라는 답변을 받았습니다. 아직 사회초년생이기도 하고 회사에 들어온지 1년 밖에 안 된 터라 윗 분들에 대한 군기가 바짝 들어있을 때였는데, 예상보다 훨씬 따뜻하게 이야기해주시고 공감해주셔서 너무 감동받았어요.</p>
<p>그렇게 1개월 정도 개발 일과 PO 일을 동시에 하게 되었습니다. 옛날 같았다면 상상도 못할 일이었겠지만, 비교적 최근인 당시에는 MCP를 사용해서 웹 개발 업무 효율을 대폭 증가시켰던 시기였기에 숨 돌릴 여유는 있었어요. 그런데 진행을 하면 할 수록 복잡한 컴플라이언스 이슈를 해결하고, 여러 부서와 협업하며 스펙을 파악하는 일이 스트레스가 아니라 흥미로 느껴져 PO 업무에 더욱 정이 들게 되었습니다. 또한 MCP와 같은 개발 과정을 돕는 AI 툴이 발전하면서, 머지않아 IT 시장도 10년 내에는 테크니컬한 프로덕트 오너가, 프로덕티브한 엔지니어가 선호될 것이라는 생각도 들었어요. 그래서 이번에는 제가 먼저 대표님께 커피챗 요청을 드렸고, 정식적으로 PO 업무를 진행해보고 싶다고 말씀드리게 되었어요. 그래서 스프린트가 끝난 후 추석 이후를 기점으로 전환배치를 통해 정식적으로 Product Owner가 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/1ee20daa-e6bd-4518-a704-7a00f7bbc73f/image.png" alt=""></p>
<h3 id="😏-product-owner가-되고나서의-업무">😏 Product Owner가 되고나서의 업무</h3>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/b4fb9190-0f96-4a68-b9ae-6772191d8a5f/image.png" alt=""></p>
<p>PO로 발령받은지는 글 쓰는 시점으로 3주 정도 되었는데요, 프론트엔드 개발자를 처음 시작했을 때처럼 작은 개선을 제안하는 것부터 시작해 점점 Activate나 매출에 직접적인 영향을 주는 볼륨이 큰 업무를 제안하는 것으로 차차 배워나가고 있습니다. 개발자로 일할 때보다 커뮤니케이션이 필요한 요소는 많아 물리적으로 소모되는 에너지는 많지만, 업무와 성향이 잘 맞아 정말 만족하면서 다니고 있어요. 잠을 훨씬 잘 자야겠다는 생각을 합니다 ^^.</p>
<p>한국에는 Product Owner라는 개념이 들어온지 얼마 되지 않아서 회사마다 PO의 개념이 조금씩 다른 것 같아요. 저는 PO는 &#39;말하고 설득하는 사람&#39;이라고 생각합니다. 여러 유관 부서의 팀원들이 오인 없이 이해할 수 있는 언어로 말하고, 만들거나 개선하고자 하는 프로덕트에 논리를 붙여 설득하는 것이 주 업무라고 생각해요. 여기서 모든 팀원이 이해할 수 있는 언어는 상황에 따라 이벤트 로그 같은 데이터가 될 수도 있고, 유저의 VOC나 NPS/CSAT/CES 지표가 될 수도 있으며, 서비스 외부에서의 시장 트렌드에 대한 자료가 될 수도 있다는 점이 PO의 매력인 것 같아요. 사용할 수 있는 여러가지 언어 중 어떤 요소를 채택해 설득해볼지 지표를 확인하며 고민하고, 설득 이후 개발을 시작하고나서도 팀원들간의 커뮤니케이션 리소스를 최적화시키는 게 PO이지 않을까?와 같은 생각으로 임하고 있습니다!</p>
<h3 id="😃-마무리">😃 마무리</h3>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/d9e6cf9d-d387-4ec2-a499-41dceabecfd9/image.png" alt=""></p>
<p>글에서는 간단히 적었지만, PO를 할지 프론트엔드 개발자로의 커리어를 이어나갈지 고민이 되게 많았어요. 주변의 시니어분들께서는 &#39;개발자로서의 경력이 적은 게 단점이 될 수 있다&#39;고 피드백해주셨는데, 확실히 PO가 되고나서 기술적으로 딥한 영역을 논의해야하는 경우 벽이 느껴지긴 하더라구요.</p>
<p>그러나 계속 지표를 보고, 글을 쓰며, 말로 표현하고 설득하다보니 점차 업무가 손에 붙는 느낌이 들고 있어요. 앞으로도 이 느낌 그대로, 여러 지표와 방법들을 고민하며 발전하는 테크니컬한 프로덕트 오너로 발전하는 것을 목표로 하고 있습니다. 앞으로도 열심히, 하고 싶은 일을 하며 살아가는 사람이 되어보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[계좌번호로 은행을 맞춰보자]]></title>
            <link>https://velog.io/@ubin_ing/detectAccountNumber</link>
            <guid>https://velog.io/@ubin_ing/detectAccountNumber</guid>
            <pubDate>Mon, 13 Oct 2025 09:32:35 GMT</pubDate>
            <description><![CDATA[<p>위 화면은 토스의 송금 화면 중 일부입니다. 
계좌번호를 입력하면 해당 계좌의 은행 선택을 추천해주는 기능으로, 이체가 발생하는 기능에서 
사용자가 간편하게 서비스를 이용할 수 있도록 돕는 유용한 기능입니다.</p>
<p>해당 기능을 서버나 머신러닝의 의존성 없이, 단순 자바스크립트 코드만으로 구현한 경험에 대해 공유합니다.</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>&quot;계좌번호로 은행을 유추하는 게 그렇게 어려운 일일까?&quot;라는 질문을 생각해봅니다</li>
<li>ML이나 서버 의존성 없이, 단순한 구현 코드만으로 해당 기능을 구현한 경험을 공유합니다</li>
</ul>
</blockquote>
<h3 id="용어-정리">용어 정리</h3>
<p><code>과목 코드</code>: 각 은행에서 기관을 식별할 때 사용하는 코드로, 계좌번호에 2자리 또는 3자리로 포함됩니다.
e.g. 기업은행 XXX-YY-ZZZZZZ-C와 같은 형식인 경우, YY가 과목번호이며, 이는 01 또는 02이다.</p>
<h3 id="계좌번호로-은행을-유추하는-게-어려운-일일까">계좌번호로 은행을 유추하는 게 어려운 일일까?</h3>
<p>처음에는 각 금융기관마다 계좌번호 발급시 사용할 패턴이 있을 것이기에, 구현하는 데 난이도가 높진 않을 것이라고 생각했습니다.</p>
<p>그런데 다음과 같은 문제점이 있었습니다.</p>
<ol>
<li>은행에 따라서 계좌번호 자릿수는 10~14자리가 될 수 있다.</li>
<li>은행별 계좌번호 패턴에 대한 공개 자료는 과목코드 단 하나를 제외하고 존재하지 않는다.</li>
<li>은행에 따라 과목코드는 2자리일 수도 있고 3자리일 수도 있다.</li>
<li>과목코드의 위치는 통일되어있지 않아, 과목코드가 은행에 따라 계좌번호의 앞/중간/뒤 등 다양한 곳에 위치한다.</li>
<li>은행끼리 과목코드가 겹치는 경우가 빈번하다.</li>
</ol>
<p>제일 큰 블로커는, 공개 자료가 없기 때문에 14자리까지 올 수 있는 계좌번호에 들어가는 과목코드 달랑 2~3자리만으로 은행을 유추해야 한다는 점이었습니다.</p>
<p>또한 과목코드만으로 은행들을 식별하기에는 은행마다 위치가 다를 때도 있으며 같은 과목코드를 사용하는 곳 또한 존재합니다.</p>
<p>그런데, 이를 단점이 아닌 특징으로 바라보아 해결할 수는 없을까요?</p>
<h3 id="과목코드로-은행-유추하기">과목코드로 은행 유추하기</h3>
<p>먼저 금융결제원에서 제공하는 ‘참가기관별 CMS 계좌번호체계’ 자료를 조사했습니다.
해당 데이터는 각 은행이 어떤 과목코드를 사용하고 있는지를 유추시켜주는 자료입니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/b2fe6334-a02b-4cc0-a854-97ffdc26f757/image.png" alt=""></p>
<p>해당 자료에는 각 은행이 어떻게 과목 코드를 구성하는지, 각 과목 코드는 어떤 값인지에 대해 기록되어있습니다.</p>
<p>같은 은행에서도 계좌 유형이 어떻냐에 따라서 과목 코드가 변경되기 때문에, 다른 은행과 겹치거나 은행 안에서 과목 코드 위치가 다른 등의 이슈가 있습니다.</p>
<p>그런데 한 가지 좋은 소식은, 일부 은행의 계좌번호에는 과목코드를 제외하고도 특정한 규칙이 공개된 경우도 기재되어있다는 겁니다.
e.g. 토스뱅크는 일련번호의 첫자리가 토스머니는 8, 나머지는 0으로 고정된다.</p>
<p>확실히 이를 로직으로 구현하더라도 정확하게 계좌번호를 확인했을 때 &#39;이 계좌번호는 이 은행거야!&#39;라기엔 겹치는 은행이 많을 것 같네요.</p>
<p>그렇다면 해당 계좌번호체계를 정리해서 각 은행별 검증기를 만들고, 입력된 계좌번호를 각 검증기들이 확인하여 점수를 부여한다면, 높은 점수를 반환한 검증기의 은행이 맞을 확률이 높지 않을까요?</p>
<p>해당 논리를 기반으로 다이어그램을 간단히 그려보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/74776906-63b7-4b8a-8d23-b2628606cb70/image.png" alt=""></p>
<ol>
<li>모든 은행을 순회하며 입력받은 계좌번호에 대한 평가를 시작한다.</li>
<li>입력받은 계좌번호가 한 은행의 과목코드와 일치할 경우 해당 은행의 score를 과목코드의 길이만큼 증가시킨다.</li>
<li>입력받은 계좌번호가 한 은행의 계좌번호 규칙과 일치할 경우 해당 은행의 score를 0.5만큼 증가시킨다.</li>
<li>모든 은행의 score를 채점 후, score가 높은 순서대로 정렬한 리스트를 반환한다.</li>
</ol>
<p>해당 알고리즘을 토대로, 서버나 ML의 의존성 없이 단순 코드만으로 기능을 구현해보겠습니다.</p>
<h3 id="아키텍처-구상하기">아키텍처 구상하기</h3>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/5572f9a2-acf9-48f3-bc66-05e36825af78/image.png" alt=""></p>
<p>각 은행사의 패턴을 생성해주는 클래스를 통해 플러그인을 생성하고, 이를 detectAccountNumber라는 감지기에 여럿 넣어주어 작동하는 식으로 설계했습니다.</p>
<p>해당 아키텍처를 기반으로 한 플로우는 다음과 같습니다.</p>
<ol>
<li>모 은행의 과목코드 및 규칙을 GenerateDetector를 통해 생성한다.</li>
<li>detectAccountNumber가 모든 detector를 순회하며 규칙을 평가하고 각 은행의 score를 매긴다.</li>
<li>모든 은행의 score를 채점 후, score가 높은 순서대로 정렬한 리스트를 반환한다.</li>
</ol>
<p>이제 해당 알고리즘과 아키텍처를 기반으로 함수를 구현해보겠습니다.</p>
<h3 id="알고리즘과-아키텍처를-기반으로-함수-구현하기">알고리즘과 아키텍처를 기반으로 함수 구현하기</h3>
<pre><code class="language-ts">interface Detector {
  /**
   * 지원할 은행 종류입니다.
   * ex) kdb, kakao, toss
   */
  bank: string;
  basicRuleList: Array&lt;BasicRule&gt;;
}

interface BasicRule {
  /**
   * 계좌 번호 패턴입니다. 과목코드의 위치를 기준으로 판별합니다.
   * ex) YYYZZZZZZZZC인 경우, YYY를 기반으로 판단
   */
  patternList: Array&lt;string&gt;;
  /**
   * 과목 코드입니다.
   * from ~ to (YCodeRange) 입력 시 이에 해당하는 범위의 과목 코드를 허용합니다.
   * ex) { from: 100, to: 104 } =&gt; 100 ~ 104
   */
  yCodeList?: Array&lt;string | YCodeRange&gt;;
  /**
   * 각 은행 별로 계좌번호 형식에 따른 규칙을 설정합니다.
   * ex) 토스뱅크는 일련번호의 첫자리가 토스머니는 8, 나머지는 0으로 고정된다.
   * additionalRules: [(accountNumber) =&gt; accountNumber[3] === &#39;8&#39; || accountNumber[3] === &#39;0&#39;],
   */
  exceptionalRuleList?: Array&lt;(accountNumber: string) =&gt; boolean)&gt;;
}</code></pre>
<p>해당 코드를 기반으로 각 은행의 계좌 패턴을 입력받아 플러그인을 만들어주는 generateDetector 함수를 작성합니다.</p>
<pre><code class="language-tsx">// generateDetector.ts
export function generateDetector({
  bank,
  basicRuleList,
  globalCustomRuleList,
}: Detector): (props: DetectorProps) =&gt; ScoreResult {

 // 설계한 알고리즘 기반으로 점수 계산하는 로직
 ...

 return { bank, bankCode, score };
}</code></pre>
<p>generateDetector를 기반으로 아키텍처 설계에 따라 각 은행별로 플러그인을 생성합니다.</p>
<pre><code class="language-tsx">// 부산은행.ts
import { generateDetector } from &#39;../src/generateDetector&#39;;

export const BNKDetectorPlugin = generateDetector({
  bank: &#39;부산은행&#39;,
  basicRules: [
    {
      patterns: [&#39;XXXYYYZZZZZC&#39;, &#39;ZYYYZZZZZZZZZC&#39;, &#39;YYYZZZZZZZZZC&#39;],
      yCodes: [&#39;107&#39;, &#39;108&#39;, &#39;109&#39;, &#39;121&#39;, &#39;123&#39;, &#39;124&#39;, &#39;122&#39;, &#39;103&#39;, &#39;101&#39;, &#39;127&#39;, &#39;716&#39;, &#39;112&#39;],
    },
  ],
});</code></pre>
<p>이제 최종적으로, detectAccountNumber 함수에 해당 플러그인들을 주입하고, batch를 통해 플러그인 실행 후 높은 점수가 나온 은행을 반환하는 함수를 구현합니다.</p>
<pre><code class="language-tsx">const detectorList = Object.values(detectors);

export function detectAccountNumber({
  accountNumber,
  length,
  additionalRuleScore,
}: DetectAccountNumberProps): Array&lt;{ bank: string; code: string }&gt; {
  const results: Array&lt;ScoreResult&gt; = detectorList.map((detector) =&gt;
    detector({ accountNumber, additionalRuleScore }),
  );
  const sorted = [...results].sort((a, b) =&gt; b.score - a.score);
  const banks = sorted.filter((result) =&gt; result.score !== 0);
  return banks.map(({ bank, code }) =&gt; ({ bank, code })).slice(0, length);
}</code></pre>
<p>마지막으로 해당 함수를 호출하여 사용해주면 복잡한 로직 없이 계좌번호를 기반으로 유추된 은행을 얻을 수 있습니다!</p>
<pre><code class="language-tsx">import { detectAccountNumber } from &#39;detectAccountNumber&#39;;

const result = detectAccountNumber({ accountNumber: &#39;7777015828112&#39; }); 
// [ { bank: &#39;카카오뱅크&#39;, bankCode: &#39;090&#39; } ]</code></pre>
<h3 id="테스트">테스트</h3>
<p>jest 테스트 코드를 기반으로 해당 함수의 예외 처리와, 은행 유추 정확도가 높은지 테스트해보겠습니다.</p>
<pre><code class="language-tsx">import detectAccountNumber from &#39;./detectAccountNumber&#39;;

const testAccountList = [
  { name: &quot;suhyup&quot;, accountList: ... },
  ...
];

describe(&quot;detectAccountNumber&quot;, () =&gt; {
  testAccountList.forEach(({name, accountList}) =&gt; {
    test(`계좌번호를 입력했을 때 해당 은행을 포함한 배열을 반환한다 : ${name} (${accountList.length})`, () =&gt; {
      accountList.forEach((accountNumber) =&gt; {
        const result = detectAccountNumber(accountNumber, 2);
        expect(result.includes(name)).toBe(true);
      })
    })
  })

  test(&quot;두 번째 인자에 값이 전달되었을 때 해당 값을 초과하지 않는 1개 이상의 배열을 반환한다&quot;, () =&gt; {
    expect(detectAccountNumber(&quot;3021822612521&quot;, 1).length).toBeLessThanOrEqual(1);
    expect(detectAccountNumber(&quot;100216268330&quot;, 2).length).toBeLessThanOrEqual(2);
    expect(detectAccountNumber(&quot;98206928101011&quot;, 3).length).toBeLessThanOrEqual(3);
  })

  test(&quot;해당하는 은행을 찾지 못했을 때에는 빈 배열을 반환한다&quot;, () =&gt; {
    expect(detectAccountNumber(&quot;475475475475&quot;).length).toBe(0)
  })

  test(&quot;잘못된 인자를 전달받았을 경우 빈 배열을 반환한다&quot;, () =&gt; {
    expect(detectAccountNumber(&quot;1234&quot;).length).toBe(0);
    expect(detectAccountNumber(&quot;ABCDEFGHIJKLMN&quot;).length).toBe(0);
    expect(detectAccountNumber(&quot;ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅍ&quot;).length).toBe(0);
    expect(detectAccountNumber(&quot;999999999999999999&quot;).length).toBe(0);
    expect(detectAccountNumber(&quot;!@#$%^&amp;*()_+&quot;).length).toBe(0);
  })
})</code></pre>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/66a9bf93-cf9b-4d5f-a055-ce868aea2bc5/image.png" alt=""></p>
<p>입력한 계좌번호의 은행에 맞게 결과가 반환됩니다.</p>
<h3 id="마무리">마무리</h3>
<p>그리 간단하지는 않지만, 무거운 서버나 머신러닝 등의 의존성 없이 간단 구현 코드만으로 유틸리티한 함수를 만들 수 있었습니다. 혹시나 저가의 비용으로 비슷한 기능을 구현해야 하는 요구사항이 생긴다면 금융결제원 CMS 계좌번호체계 기반 데이터로 시도해보시는 것을 추천드립니다.</p>
<h3 id="참고-링크">참고 링크</h3>
<p><a href="https://toss.tech/article/toss-money-transfer-bank-recommendation">송금할 때 은행 이름을 꼭 입력해야 할까요?
</a></p>
<p><a href="https://github.com/jhaemin/korea-financial-account-number-detector">jhaemin/korea-financial-account-number-detector</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Figma MCP로 30분만에 15개 페이지 개발하는 방법]]></title>
            <link>https://velog.io/@ubin_ing/use-claude-code-with-figma-mcp</link>
            <guid>https://velog.io/@ubin_ing/use-claude-code-with-figma-mcp</guid>
            <pubDate>Wed, 25 Jun 2025 15:29:00 GMT</pubDate>
            <description><![CDATA[<p>아래 화면은 디자이너가 구현한 Figma 상의 다섯 개 페이지 화면입니다.
<code>추천코드 입력 화면</code>과 <code>이메일 입력 화면</code>, <code>인증번호 입력 화면</code>, <code>비밀번호 입력 화면</code>, <code>가입 완료 화면</code> 으로 이루어져있습니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/ca599a1d-077a-42b7-86e3-4782b8b8fbf3/image.png" alt=""></p>
<p>이 페이지들을 직접 퍼블리싱하고, state를 연결하는 데 얼마 정도의 시간이 걸리시나요?
저는 셋업 시간을 제외하고 10분도 채 걸리지 않고 다섯 개의 페이지를 개발했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/a7cffd30-648b-4282-a3be-e90851c4fc64/image.gif" alt=""></p>
<p>빠른 시간 내에 개발을 완료한 거라면 코드 품질이 안 좋지 않을까요? 그것 또한 확인해보겠습니다.</p>
<table>
<thead>
<tr>
<th>아키텍처</th>
<th>레이아웃</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/3dbb69c8-587c-42d7-a8ac-d902f5c454f1/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/03628913-61db-4873-9333-3d1f8a88f590/image.png" alt=""></td>
</tr>
</tbody></table>
<p>아키텍처 또한 제가 원하는대로 Feature 기반으로 slice되어 설계되었고, Next.js 프레임워크의 app router와 private routing 기술을 적절하게 이용했습니다.</p>
<p>레이아웃도 재사용되는 컴포넌트는 확실하게 나누어져있고, 컴포넌트 내부 또한 유연하게 바뀌도록 잘 설계되어 있는 것 같아 보입니다.</p>
<p>이 모든 일들이 10분도 안 되어서 일어났습니다. 어떻게 이런 마법같은 일이 일어날 수 있을까요?</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>MCP와 Claude code가 무엇인지에 대해 설명합니다.</li>
<li>Claude code와 Figma MCP를 연결하는 방법을 설명합니다.</li>
<li>Claude code의 Memory System을 유용하게 쓰는 방법을 설명합니다.</li>
<li>AI를 통해 한 문장만으로 페이지를 개발하고, 결과물과 과정을 리뷰합니다.</li>
</ul>
</blockquote>
<h2 id="mcp가-뭘까">MCP가 뭘까?</h2>
<p>MCP(Model Context Protocol)는 AI 모델과 외부 데이터 소스를 연결하는 표준화된 오픈 프로토콜입니다.
쉽게 말하면 우리가 익숙하게 사용하는 웹 사이트 안의 GPT나 Perplexity, Gemini같은 툴들을 어떤 서비스와 연결해 사용할 수 있게 하는 Bridge나 USB라고도 생각할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/a523c4be-deb8-4b3f-86de-418c4722bb21/image.png" alt=""></p>
<p>요즘 MCP가 굉장히 핫한 주제인데, MCP를 지원하는 서비스라면 어디서든 Claude code와 같은 AI를 붙여서 사용할 수 있기 때문입니다.</p>
<p>대표적으로 Blender MCP가 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/85023064-219f-4e06-adca-8ffcc9d7e94d/image.gif" alt=""></p>
<p>MCP를 연결 후 Claude code에 &#39;집을 모델링해줘&#39;라고 입력하면 gif처럼 Claude code가 직접 블렌더를 활용합니다.</p>
<p>어떻게 이게 가능할까요?</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/1c4880c3-80dc-423d-ba07-4147ab587642/image.png" alt=""></p>
<p>사용자가가 Claude에게 프롬프트를 통해 요청을 하면, MCP 클라이언트가 프로토콜 통신을 통해서 대상 서비스에 API 요청을 보냅니다. 해당 API 요청을 토대로 응답을 처리하면서 위 GIF처럼 마법같은 일이 생길 수 있게 됩니다.</p>
<p>Blender MCP 뿐만 아니라 일감과 문서를 구체화해 관리해주는 Jira, Confluence MCP, Figma 화면을 보고 개발을 자동화해주는 Figma MCP, 더 나아가 MCP가 포켓몬 골드를 플레이하고 하는 등 여러 부면에서 재미있는 시도가 일어나고 있습니다.</p>
<p>이 글에서는 그 중 프론트엔드 개발자들이 매우 유용하게 사용할 수 있는 Figma MCP에 대해 알아볼 예정입니다. 그 전에, 기본적으로 글에서 서술할 Claude code에 대해 알아보겠습니다.</p>
<h2 id="claude-code란-무엇일까">Claude code란 무엇일까?</h2>
<p>Claude code는 개발자가 터미널을 기반으로 프롬프팅을 할 수 있는 툴입니다. 요즘 개발자들 사이에서는 Cursor AI가 유독 많이 알려져 있는데요, Claude code는 터미널을 기반으로 실행할 수 있기 때문에 IntelliJ를 쓰는 개발자, WebStorm을 쓰는 개발자, VSCode를 쓰는 개발자를 불문하고 모든 IDE에서 돌아갈 수 있다는 유연성이 장점입니다.</p>
<p>GPT처럼 어떤 사이트에 들어가서 질문을 하고, 답변을 받는 식과 다르게 Claude code는 질문이나 명령을 전송하면 직접 개발자의 코드를 분석해서 코드를 작성해주기도 합니다.</p>
<p>현재 점유율은 Cursor AI가 훨씬 높지만, 두 가지 툴을 다 써 본 입장에서 주관적인 생각으로는 JavaScript 기반 코드를 짤 때에는 Claude code가 조금 더 뛰어난 성능을 보인다고 느낍니다.</p>
<h2 id="claude-code-setup">Claude code Setup</h2>
<p>Claude code는 npm을 통해 설치할 수 있습니다.</p>
<pre><code>$ npm install -g @anthropic-ai/claude-code</code></pre><p>개발자의 컴퓨터에 전역적으로 claude code를 설치한 후, 원하는 프로젝트에서 <code>claude</code> 명령어만 작성하면 바로 claude code를 실행시킬 수 있습니다.</p>
<p>claude 명령어를 통해 vscode 터미널에서 오늘 저녁 메뉴를 추천받아보겠습니다. 
<img src="https://velog.velcdn.com/images/ubin_ing/post/c15fb77b-86f3-4069-94e6-34349582ba2e/image.gif" alt=""></p>
<p>위 gif처럼 바로 설치 후 작동시킬 수 있습니다. 기본적으로 Claude code는 특정 토큰을 초과하여 사용하면 결제가 필요할 수 있기 때문에, 이 점 참고하시기 바라겠습니다. (필자는 Pro plan을 사용 중입니다)</p>
<h2 id="figma-mcp-setup">Figma MCP Setup</h2>
<p>이제 서두에서 설명드렸던 것처럼 이를 Figma MCP와 연결시켜보겠습니다.</p>
<p><a href="https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Dev-Mode-MCP-Server">해당 링크에서 원문 문서를 확인하실 수 있습니다</a></p>
<ol>
<li>먼저 피그마를 최신 버전으로 업데이트합니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/86094d42-1205-4733-a77d-a2340b7e406c/image.png" alt=""></p>
<ol start="2">
<li>좌측 상단 피그마 아이콘을 클릭하고, Preferences에 있는 <code>Enable Dev Mode MCP Server</code> 옵션을 Check합니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/b61eb9a5-d543-4c60-8807-e6b95edb4981/image.png" alt=""></p>
<p>참고로 옵션 이름 만큼, Dev Mode가 켜져있는 피그마에서만 MCP를 사용할 수 있다는 점 참고 부탁드립니다.</p>
<p>이렇게 되면 기본적으로 Figma에서 할 일은 끝났습니다.</p>
<p>그 다음은 Claude code로 가서 세팅을 해보겠습니다. Figma MCP를 사용하려는 프로젝트의 루트에 다음 명령어를 실행해주세요. (<code>claude</code> 실행 후 해당 커맨드 실행하는 게 아니라, 그냥 터미널에서 명령어처럼 실행해주시면 됩니다)</p>
<pre><code>$ claude mcp add --transport sse figma-dev-mode-mcp-server http://127.0.0.1:3845/sse</code></pre><p>이렇게 되면 Figma MCP 사용 준비가 끝났습니다. 이제 바로 이를 사용해서 개발을 진행해보겠습니다.</p>
<h2 id="project-guide-setup">Project Guide Setup</h2>
<p>먼저 우리는 Claude에게 명령을 내릴 때마다 똑같은 말을 반복하지 않도록, 그리고 우리가 원하는 코드 스타일대로 개발을 진행할 수 있게 프로젝트에 대해 가이드하는 문서를 작성해줄 겁니다.</p>
<p>프로젝트의 루트에 <code>CLAUDE.md</code> 파일을 생성해주세요. 파일명이 다르면 안되며, <code>CLAUDE.md</code> 파일만 생성해두면 claude가 실행될 때 자동으로 해당 파일을 학습합니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/9be315c2-ca3b-4425-b88f-1dae731b02b1/image.png" alt=""></p>
<p>제가 작성한 가이드 파일은 이 정도입니다. 가이드 파일은 한글로 작성해주셔도 되는데요, 저 또한 어떤 가이드 파일이 가장 좋은 효율을 내는지 계속해서 새로운 시도를 해 보고 있는 중이라 영어로 작성하였습니다.</p>
<p>특정 부분마다 나누어 파일을 살펴보겠습니다.</p>
<h3 id="claudemd">CLAUDE.md</h3>
<pre><code># CLAUDE.md

It is a mobile-based web service that matches you with the opposite gender for romantic relationships and blind dates.</code></pre><p>먼저 해당 프로젝트가 어떤 서비스인지에 대해 설명합니다. 저는 디자이너 지인 분께서 해커톤에 참여할 때 사용하셨던 템플릿을 빌렸습니다. 해당 서비스는 소개팅을 연결해주는 웹 기반 서비스라고 명시해주었습니다.</p>
<h3 id="mcp-servers">MCP Servers</h3>
<pre><code># MCP Servers

## Figma Dev Mode MCP Rules

- The Figma Dev Mode MCP Server provides an assets endpoint which can serve image and SVG assets
- IMPORTANT: If the Figma Dev Mode MCP Server returns a localhost source for an image or an SVG, use that image or SVG source directly
- IMPORTANT: DO NOT import/add new icon packages, all the assets should be in the Figma payload
- IMPORTANT: do NOT use or create placeholders if a localhost source is provided</code></pre><p>Figma MCP Server rule에 대해 기본적으로 설명합니다. 대체적으로 asset과 관련된 내용입니다.
미리 말씀드려보면 Figma MCP를 사용하면서 png, svg와 같은 asset을 사용할 때 Claude code가 애를 좀 먹습니다. 
이 부분에 가이드 파일을 원하는대로 더 상세하게 적어본다면 asset을 세부적으로 조절할 수 있습니다.</p>
<h3 id="tech-spec">Tech Spec</h3>
<pre><code># Tech Spec

Please Check dependencies in ./package.json file.

- **Development**: Next.js, TypeScript, React.js
- **Styling**: tailwindcss@4
- **API Request**: axios, @tanstack/react-query
- **Animation** : motion</code></pre><p>어떤 라이브러리나 프레임워크를 사용하고있는지를 명시합니다. 구버전이나 stable 버전을 사용하는 경우 버전까지 같이 명시해주면 좋습니다. 예시에서는 최신버전인 <code>tailwindcss@4</code>를 적어두었지만, 특정 프로젝트에서 MCP를 쓸 때 v3 문법을 쓰는 프로젝트에서 v4 코드를 짜는 등의 이슈가 발생하기도 합니다. </p>
<p>그래서 한 버전으로 제한해야하는 경우 이렇게 버전을 명시해두면 좋고, 저는 혹시나 실수할까봐 첫 줄에 package.json 파일을 참고하라고도 해두었습니다.</p>
<h3 id="directory-architecture">Directory Architecture</h3>
<pre><code># Directory Architecture

figma-mcp/
├── public/ /* statical assets e.g. png, jpg, ... */
├── src/
│   ├── app/
│   │   ├── [pageName]/
│   │   │   ├── _features/
│   │   │   │   ├── ui/
│   │   │   │   │   └── /* being used component in [pageName], If you implement page component, you can naming [pageName]PageView.tsx with camelcase */
│   │   │   │   ├── lib/
│   │   │   │   │   ├── assets/ /* if you need use svg, you can make svg component file with `index.ts` barrel file. Please use svg by tsx module */
│   │   │   │   │   └── hooks/ /* being used custom hook in [pageName] */
│   │   │   ├── _entities/
│   │   │   │   ├── model/
│   │   │   │   │   └── /* being used type or interface in api call, e.g. response dto, request dto, or just model, etc. you can naming &#39;types.ts&#39;, &#39;dto.ts&#39; and export it. (do not use `export default`) */
│   │   │   │   ├── api/
│   │   │   │   │   ├── axios.ts /* axios function group */
│   │   │   │   │   └── queries.ts /* React Query group object with use `queryOptions` in @tanstack/react-query. if you need declare useMutation, you can declare in this file. but mutation don&#39;t have `mutationOptions`, So you should use type annotation with `MutationOptions` type. */
│   │   │   └── page.tsx /* just export in _features/ui/[pageName]PageView.tsx. page.tsx&#39;s component should use type annotation with &#39;NextPage&#39; in &#39;next&#39;. */
│   │   └── page.tsx
│   ├── shared/
│   │   ├── components/ /* shared components in feature page */
│   │   └── hooks/ /* shared hooks in feature page */</code></pre><p>이 부분이 꽤 중요한 부분이라고 생각합니다. 만약 지향하는 아키텍처가 있거나, 기존 프로젝트에서 추가적인 코드를 짜는 경우 프로젝트 아키텍처에 대해 세부적으로 설명해야 합니다.</p>
<p>저는 bun으로 Next.js를 셋업한 다음 아무 구조도 없는 상태에서 다음과 같은 아키텍처로 짜달라고 요청했습니다.
각 디렉터리에 어떤 내용이 어떤 스타일로 들어가야하는지, 코드에는 어떤 게 꼭 포함되어야 하는지도 명시합니다.</p>
<p>영어를 잘 못하는데 공부하는 겸 번역기 안 쓰고 손수 작성해보아서 문법이 엉망입니다. 양해 부탁드립니다ㅠ</p>
<h3 id="implement">Implement</h3>
<pre><code># Implement

- Each page is managed via [pageName] directory in `src/app`.
- If you need implement some page, follow Directory Architecture rules.
- You should declare model and api when you need implement some page. see the figma design and judgment what data is necessary.
- If you think it is a frequently used component, such as a button or input, please implement it flexibly in shared so that the component can be commonly used.</code></pre><p>Implement에서는 페이지를 개발할 때 어떤 부분을 어떻게 개발해야하는지를 세부적으로 설명합니다.</p>
<h3 id="avoid-pattern">Avoid Pattern</h3>
<pre><code># Avoid Pattern

- Do not use any type. If need some interface or type, you can write [feature page name]/types.ts and export it.
- You can use gap or empty `h-{} div` instead of margin and padding. Please avoid margin/padding styling pattern as you can.
- If a component file has more than 150 lines of code, please separate the hooks or components into modules.
- Do not use `React.[module]` pattern. please just import and use it.
- Do not use inline function. please make a handler function and use it. you can naming function with this rule via `&#39;handle&#39;{target}{eventName}` e.g. handleCTAButtonClick, handleAgeInputChange, etc.
- Do not use inline style css.
- If you need assets, use can copy as SVG code in figma. do not implement yourself asset file, just use svg and convert to svg component.
- Please avoid publish with `relative`, `absolute`. you can use flex and grid tailwindcss keyword.</code></pre><p>Avoid Pattern에는 말 그대로 피해야하는 코드 스타일을 적어주었습니다. TypeScript any 쓰지마라, margin이나 padding 말고 gap을 써보아라, 컴포넌트가 너무 길어지면 모듈화를 진행해라, inline style 쓰지마라 등의 내용을 넣어주었습니다.</p>
<p>사실 CLAUDE.md 파일을 잘 작성했다면 모든 준비가 끝났습니다. 이제 본격적으로 Claude code에게 구현을 명령해보겠습니다.</p>
<h2 id="claude-code로-개발해보기">Claude code로 개발해보기</h2>
<p>피그마에서 구현을 원하는 프레임을 우클릭해보시면 <code>Copy link to selection</code>이라는 버튼이 있습니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/1b70ef71-2327-4d1d-9e75-f939a4ca28aa/image.png" alt=""></p>
<p>해당 버튼을 클릭해 링크를 복사하고 claude를 실행한 다음,</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/4a71ba52-6237-484a-bd6b-15dacee7fced/image.png" alt=""></p>
<p>이런 식으로 링크와 함께 구현을 요청하는 커맨드를 입력합니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/ae39abff-9b3d-40bb-a636-ab7a887ee5e1/image.png" alt=""></p>
<p>Claude code는 명령을 받고 자신이 해야 할 일을 Todos로 분류한다음 작업을 진행합니다.
빠른 시간 내에 결과물이 나오는데요, 결과물을 확인해보겠습니다.</p>
<table>
<thead>
<tr>
<th>피그마</th>
<th>구현</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/20a07c76-37b5-4e95-acd4-597ac680b243/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/89d8d30f-a27a-41cf-8343-464334bd6e61/image.png" alt=""></td>
</tr>
</tbody></table>
<p>말씀드렸던 Arrow와 같은 SVG를 제외하고는 거의 동일하게 구현되었네요. 아키텍처를 확인해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/aee95f36-8d8b-43dd-a5da-9cf75be54b73/image.png" alt=""></p>
<p><code>CLAUDE.md</code>에 셋업해둔 명령어처럼 원하는대로 아키텍처를 잘 짰네요. 세부 코드도 확인해보겠습니다.</p>
<pre><code class="language-jsx">  const [formData, setFormData] = useState&lt;ReferralCodeFormData&gt;({ code: &#39;&#39; });

  const handleCodeInputChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    setFormData({ code: e.target.value });
  };

  const handleBackButtonClick = () =&gt; {
    // Navigate back logic
    console.log(&#39;Navigate back&#39;);
  };

  const handleNextButtonClick = () =&gt; {
    if (formData.code.length === 6) {
      // Submit logic
      console.log(&#39;Submit referral code:&#39;, formData.code);
    }
  };

  const isNextButtonEnabled = formData.code.length === 6;</code></pre>
<p>이야기하지도 않았는데 피그마에 적힌 글자만을 보고 6글자인 경우에만 Button을 enable 시키는 플래그도 만들어 주었습니다. state를 개발하고 각 핸들러 함수들을 정확한 이름으로 선언했으며,</p>
<pre><code class="language-jsx">  return (
    &lt;div className=&quot;bg-[#ffffff] flex flex-col min-h-screen w-full&quot;&gt;
      &lt;div className=&quot;flex flex-col shrink-0 pt-4&quot;&gt;
        &lt;NavigationHeader
          leftContent={
            &lt;button
              className=&quot;shrink-0 w-7 h-7&quot;
              onClick={handleBackButtonClick}
            &gt;
              &lt;ArrowBackIcon /&gt;
            &lt;/button&gt;
          }
        /&gt;
        &lt;TitleSection
          title={
            &lt;&gt;
              &lt;p className=&quot;block mb-0&quot;&gt;가입하기 전 추천코드를&lt;/p&gt;
              &lt;p className=&quot;block&quot;&gt;입력해주세요.&lt;/p&gt;
            &lt;/&gt;
          }
          subtitle=&quot;추천코드가 없다면 가입이 힘들어요&quot;
        /&gt;
      &lt;/div&gt;
      &lt;div className=&quot;flex flex-col flex-1 w-full&quot;&gt;
        &lt;div className=&quot;flex flex-col items-center h-full w-full&quot;&gt;
          &lt;div className=&quot;flex flex-col justify-between pt-2 pb-0 px-0 h-full w-full&quot;&gt;
            &lt;Input
              label=&quot;추천 코드&quot;
              value={formData.code}
              onChange={handleCodeInputChange}
              placeholder=&quot;숫자, 영문을 합친 6자리 코드를 입력해주세요.&quot;
              maxLength={6}
            /&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;CTAButton
        onClick={handleNextButtonClick}
        disabled={!isNextButtonEnabled}
        rightIcon={&lt;ArrowRightIcon /&gt;}
      &gt;
        다음
      &lt;/CTAButton&gt;
    &lt;/div&gt;
  );</code></pre>
<p>UI적인 측면에서도 딱히 더럽다고 느껴지는 부분 없이 깔끔하게 코드 작성을 완료했습니다.</p>
<p>이런 식으로 다섯 개의 프레임을 Claude code에 요청했고, 모든 명령을 요청하고 응답을 받는 시간까지 10분도 걸리지 않아 아래와 같은 화면이 뚝딱 만들어졌습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/4e3b25a9-da07-429d-9431-0896fda41f49/image.gif" alt=""></p>
<p>물론 asset과 약간의 레이아웃 관련해서 조금 다른 부분이 있어, 이 부분은 직접 수정을 진행해주었습니다. (해당 시간까지 포함해서도 10분도 걸리지 않았습니다)</p>
<h2 id="사용성">사용성</h2>
<p>Claude code는 이외에도 여러가지 기능들을 제공하는데요, 기능들 중 제일 기본적인 메모리 시스템을 이용한 <code>CLAUDE.md</code> 파일과, 간단하게 셋업할 수 있는 Figma MCP만으로도 이 정도 퀄리티의 코드를 짤 수 있습니다.</p>
<p>더 나아가 세부적인 개발이나 코드 스타일을 원한다면, 계속해서 CLAUDE.md 파일을 업데이트시키거나 Memory 세팅을 통해 원하는대로 Claude를 입맛에 맞게 튜닝할 수 있습니다.</p>
<p>저는 Claude code로 회사에서 진행하고 있는 업무 중, 페이지를 유연하고 확장성있게 개발하는 특정 업무에서 Figma MCP를 사용하고 있습니다. </p>
<p>기존에 재사용할 수 있는 컴포넌트가 많이 존재하고, 피그마 내에서의 오토 레이아웃이나 패딩, 마진, 컴포넌트화 등이 알맞게 적용되어있다면 훨씬 뛰어난 성능을 발휘합니다.</p>
<p>개인적으로는 이런 개발뿐만 아니라, 사용자가 해당 페이지에 접속해서 어떤 흐름으로 페이지를 사용할지를 예상하고 이를 테스트 코드로 작성하는 일 또한 할 수 있다는 점이 좋다고 느껴집니다.</p>
<p>또 해당 작업을 Claude code로 문서화하여 온보딩 가이드를 만들고, Jira MCP를 활용해 작업이 끝난 후 Github에 코드를 push할 때마다 진행 상황을 업데이트하고, Confluence MCP를 활용해 Tech Spec 문서를 업데이트 하고 ...</p>
<p>이런 식으로 AI를 활용한다면 개발자는 직접 코드를 작성하지 않아도 빠르게 일을 처리할 수 있습니다. 개발부터 테스트 코드 작성, 문서화까지 원래 8시간이 걸렸다면 AI와 MCP를 통해 한 시간 만에 이를 구현할 수 있는 것이고, 그 시간 동안 다른 일을 할 수도 있게 되는 거죠.</p>
<h2 id="마무리">마무리</h2>
<p>저도 AI를 그렇게 신용하는 개발자는 아니라서 이번에 거의 처음 Claude code와 MCP를 사용해보게 되었는데, 생각보다 편리하고 코드를 잘 짜주며, 무엇보다도 속도가 너무 빨라서 그 시간에 비개발적인 업무(타 직군과 의사결정 포인트 논의 or 개인적인 스터디, 운영 업무 캐치업 등)를 할 수 있어 너무 여유로웠습니다.</p>
<p>원래는 &#39;돈 벌려고 일하는데 왜 돈을 주면서 일해야 해?&#39;라는 생각을 가지고 이런 AI들을 결제하지 않았었는데, 많이 부풀려서 표현해보면 &#39;달에 3만원씩만 내면 월급을 받을 수 있네?&#39; 라고 생각이 바뀌어 잘 사용해보고 있습니다.</p>
<p>시대가 너무 좋아졌습니다. 개발에 필요한 기본기나 지식, 내부 동작 원리 또한 중요하지만, 이제는 AI를 비서처럼 활용해 본인의 업무를 어떻게 효율화할 수 있는지를 플래닝하는 것 또한 중요하다고 느껴집니다.</p>
<p>&#39;AI가 내 직업을 대체하지 않을까?&#39;, &#39;AI 때문에 취업 못하는 거 아니야?&#39;와 같은 부정적인 생각보다는, AI와 함께 협업한다고 생각하시며 오픈된 마인드로 다가가보신다면 더욱 훌륭한 개발자가 되어있으실 거라 예상합니다.</p>
<p>AI를 좋아하지 않으시더라도 꼭 한 번 써보시길 바라며, 월 3만원이 비싸게 느껴질 수 있지만 ... 저는 술자리 한 번 안 간다고 생각하고 결제했습니다. 한번쯤은 꼭 사용해보시는 걸 추천드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js는 이제 끝났나요? v15.2+의 Streaming metadata]]></title>
            <link>https://velog.io/@ubin_ing/nextjs-streaming-metadata-issue</link>
            <guid>https://velog.io/@ubin_ing/nextjs-streaming-metadata-issue</guid>
            <pubDate>Mon, 16 Jun 2025 12:35:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>Next.js 15.2+ 버전에서 도입된 Streaming metadata에 대해 설명합니다.</li>
<li>어떤 논제와 문제가 있었는지, 기존과 무엇이 다른지를 함께 이야기합니다.</li>
</ul>
</blockquote>
<h2 id="generatemetadata로-인한-병목-현상-발생">generateMetadata로 인한 병목 현상 발생</h2>
<p>다음은 Next.js 13 버전에서 generateMetadata를 사용했을 때와 하지 않았을 때를 비교한 표입니다.</p>
<p>각 페이지는 로딩 시간이 3초가 걸리는 API를 사용해서 렌더링됩니다.</p>
<table>
<thead>
<tr>
<th>미사용(정상)</th>
<th>사용</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/0cdeb235-7790-4ea5-9146-215facefc2e5/image.gif" alt=""></td>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/beeff9d3-2887-43d9-833b-a7615f6f1296/image.gif" alt=""></td>
</tr>
<tr>
<td>클릭하는 즉시 페이지에 접속됨</td>
<td>generateMetadata가 끝난 3초 후에야 페이지에 접속됨</td>
</tr>
</tbody></table>
<pre><code class="language-tsx">// 문제의 코드 : generateMetadata에 느린 작업이 있는 경우 페이지 로딩 자체가 지연됨
export async function generateMetadata() {
  await new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      resolve(void 0);
    }, 3000);
  });

  return {
    title: &#39;Example&#39;,
    description: `example metadata`,
  };
}</code></pre>
<p>app router에서 동적인 metadata를 설정하는 방식을 <code>generateMetadata</code> 라는 하나의 함수로 제공하게 되면서, 해당 함수에서 불러오는 API가 느린 경우 페이지의 UI가 사용자에게 느리게 서빙되는 이슈가 있었습니다.</p>
<p>Next.js v13에서 발생한 해당 이슈를 해결하기 위해 v15에서 도입한 개념이 바로 Streaming metadata입니다.</p>
<h2 id="streaming-metadata">Streaming metadata</h2>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/e38201f8-06cc-44b9-9505-08a3ea5dc35a/image.png" alt="">
해당 기능은 스트리밍을 통해 UI를 그리는 로직과 동시에 metadata를 불러오고, 스트리밍이 끝난 경우 최종적인 메타데이터를 업데이트합니다.</p>
<p>이 경우 메타데이터를 등록하는 함수의 속도가 느리더라도 페이지의 로딩이 블락되는 문제는 없어집니다.</p>
<p>Next.js가 메타데이터를 처리하는 실제 코드 중 일부를 가져와 설명해보겠습니다. (아래 참조 문서에서 원문을 확인하실 수 있습니다)</p>
<pre><code class="language-jsx"> async function Metadata() {
    return await resolveFinalMetadata()
}</code></pre>
<p>기존에는 이런 식으로 generateMetadata를 사용하는 경우, 단순히 await을 통해 무조건 먼저 메타데이터를 처리했습니다. 그러나,</p>
<pre><code class="language-jsx"> async function Metadata() {
    const promise = resolveFinalMetadata()
    if (serveStreamingMetadata) {
      return (
        &lt;Suspense fallback={null}&gt;
          &lt;AsyncMetadata promise={promise} /&gt;
        &lt;/Suspense&gt;
      )
    }
    return await promise
}</code></pre>
<p>Next.js 15.1.8 버전부터 streamingMetadata를 제공하며 코드가 변경되었습니다.
플래그에 따라서 메타데이터를 streaming해야 한다고 판단되면 비동기적으로 메타데이터를 가져옵니다.</p>
<p>메타데이터를 resolve하는 로직을 직접 살펴보겠습니다.</p>
<pre><code class="language-jsx">async getServerInsertedMetadata(): Promise&lt;string&gt; {
  if (!metadataResolver || metadataToFlush) {
    return &#39;&#39;
  }

  metadataToFlush = metadataResolver()
  const html = await renderToString({
    renderToReadableStream,
    element: &lt;&gt;{metadataToFlush}&lt;/&gt;,
  })

  return html
},</code></pre>
<p>위 코드처럼 metadataResolver 함수와, renderToReadableStream 함수를 사용하여 메타데이터를 스트리밍으로 서빙합니다.</p>
<p>renderToReadableStream은 React에서 제공하는 함수로, 렌더할 데이터를 청크 단위로 잘게 쪼개 나누어 작업할 수 있도록 도와줍니다. 기본적으로 Web API인 ReadableStream을 사용해 구현되어 있는데, 이는 HTTP 1.1의 Chunked Transfer Encoding를 이용해 데이터를 분할하여 보낼 수 있도록 합니다. (뭐랄까 웹소켓이나 SSE와 비슷합니다. 다음에는 이 주제를 메인으로도 글 써보겠습니다)</p>
<p>그렇기 때문에 다른 작업(페이지 렌더링)을 진행하면서 동시에 처리가 가능한 것입니다. 리액트 19에서 도입된 서버 컴포넌트 또한, 페이지 단위가 아닌 컴포넌트 단위로 데이터를 스트리밍할 수 있는 이유가 이런 기능 때문입니다. (서버 컴포넌트에서는 이와 비슷한  renderToPipeableStream을 사용합니다)</p>
<p>아무튼 이와 같이 metadataResolver가 <code>for of</code>를 사용해 청크로 쪼개진 메타데이터 스트림을 readableStream 함수와 연결해 처리합니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/df9276bb-3620-44f8-ab28-ea29c33cc214/image.gif" alt=""></p>
<p>이렇게 되면 페이지를 먼저 렌더링하고, 나중에 metadata가 준비되면 보여줄 수 있습니다.</p>
<p>DOM을 해부해보면 streaming된 metadata와 그렇지 않은 metadata의 태그 위치가 다르다는 것 또한 알 수 있습니다. metadata가 스트리밍되지 않은 경우에는 상식처럼 head 내부에 title, description이 주입되지만, 스트리밍된 경우 메타데이터 관련 태그들이 body 내부에 위치합니다. </p>
<p>크롤러에게는 적합하지 않을 수 있으나, 결론적으로 태그가 주입되어 유저가 브라우저의 title을 확인할 때 문제가 발생하지 않습니다. (이와 관련된 자세한 이슈는 아래 참조 문서에서 확인할 수 있습니다.)</p>
<h2 id="seo에-문제가-생기지-않을까">SEO에 문제가 생기지 않을까?</h2>
<p>메타데이터가 중요한 이유는 무엇보다 검색 엔진이 사이트를 방문했을 때 얼마나 많은 정보를 크롤링하는지로 SEO가 결정되기 때문입니다.</p>
<p>그런데 generateMetadata를 스트리밍으로 변경하면서 자바스크립트를 실행해야지만 메타데이터가 불러와지도록 변경되었고, 검색 엔진이 자바스크립트를 실행하지 않는 경우 메타데이터가 아예 노출되지 않는다는 문제가 생겼습니다.</p>
<p>그러나 기존 로직을 그대로 가져가게 되면 이전처럼 generateMetadata의 처리 속도가 느린만큼 페이지가 늦게 로드된다는 문제가 있습니다.</p>
<p>이를 어떻게 해결해야 할까요?</p>
<h2 id="봇과-유저를-분리하는-htmllimitedbots-옵션">봇과 유저를 분리하는 htmlLimitedBots 옵션</h2>
<p>그래서 vercel은 htmlLimitedBots라는 옵션을 추가합니다. 요청값으로 들어오는 user-agent값을 기반으로 크롤러 봇인 경우 이전처럼 느리더라도 풀 metadata를, 유저인 경우 스트리밍한 metadata를 제공하여 SEO와 성능(UX)을 모두 잡은 것입니다.</p>
<p>next.config.js에서 htmlLimitedBots 옵션에 RegExp를 사용해 메타데이터를 스트리밍하면 안되는 UserAgent를 설정할 수 있습니다.</p>
<pre><code class="language-ts">/** @type {import(&quot;next&quot;).NextConfig} */
module.exports = {
  reactStrictMode: true,
  htmlLimitedBots: /Mac|Windows/,
};</code></pre>
<p>위 예제와 같이 설정한 경우, 접속하는 기기의 UserAgent가 Mac 또는 Windows인 경우 기존처럼 메타데이터를 스트리밍하지 않는 식으로 작동합니다.</p>
<p>해당 옵션을 통해 Next.js 내장 함수인 shouldServeStreamingMetadata가 스트리밍이 필요한지를 결정합니다.</p>
<pre><code class="language-jsx">export function shouldServeStreamingMetadata(
  userAgent: string,
  {
    streamingMetadata,
    htmlLimitedBots,
  }: {
    streamingMetadata: boolean
    htmlLimitedBots: string | undefined
  }
): boolean {
  if (!streamingMetadata) {
    return false
  }

  const blockingMetadataUARegex = new RegExp(
    htmlLimitedBots || HTML_LIMITED_BOT_UA_RE_STRING,
    &#39;i&#39;
  )
  return (
    // When it&#39;s static generation, userAgents are not available - do not serve streaming metadata
    !!userAgent &amp;&amp; !blockingMetadataUARegex.test(userAgent)
  )
}</code></pre>
<p>UserAgent에 htmlLimitedBots에 기재한 Agent가 하나라도 포함되는 경우 true, 아닌 경우 false를 반환합니다. (userAgent가 없는 경우 false를 반환합니다)</p>
<p>그런데 사용자가 검색 엔진에 어떤 크롤러들이 존재하는지를 숙지하고 매번 추가하는 것은 너무 복잡한 일입니다. 그렇기 때문에 htmlLimitedBots 옵션을 입력하지 않은 경우, Next.js는 흔히 웹 상에서 떠다니는 크롤러들에게 스트리밍을 제공하지 않도록 옵션을 설정합니다. </p>
<p>그렇기 때문에 아무런 설정을 하지 않아도 사실 개발자가 받는 피해는 없습니다. 기본적으로 사용자에게는 스트리밍, 크롤러 봇에게는 스트리밍되지 않은 메타데이터를 제공하기 때문입니다.</p>
<p>개발자가 아무런 설정을 하지 않은 경우 스트리밍을 진행하지 않는 봇은 다음과 같습니다.</p>
<pre><code class="language-ts">export const HTML_LIMITED_BOT_UA_RE_STRING =
  &#39;Mediapartners-Google|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview&#39;</code></pre>
<p>그러나 크롤러 중에서도 스트리밍된 메타데이터를 보여주는 경우가 있습니다. 이는 다음과 같습니다.</p>
<pre><code class="language-ts">const HEADLESS_BROWSER_BOT_UA_RE =
  /Googlebot|Google-PageRenderer|AdsBot-Google|googleweblight|Storebot-Google/i</code></pre>
<p>대체적으로 구글 크롤러 봇인데요, 구글 크롤러는 무조건 자바스크립트를 실행시키기 때문에 문제가 없어 스트리밍을 진행합니다.</p>
<p>그렇기 때문에 개발자가 모든 크롤러 봇이나 종류를 알고 있지 않아도 사용할 수 있는 것입니다.</p>
<h2 id="문제점">문제점</h2>
<p>그러나 아직 반대 여론은 존재합니다. 메타데이터를 스트리밍으로 제공하면 결국 크롤러에게 잡히는 페이지 지연 시간이 늘어나고, 로딩 시간이 너무 오래 걸리면 검색 엔진이 해당 사이트에게 페널티를 부과하기 때문입니다.
그렇기에 이는 단순하게 성능 문제를 가리려는 눈속임이라고도 평가됩니다.</p>
<p>또한 개발자가 선택할 수 있어야 하거나 표준을 따라야한다고 평가되는 기능들을 계속해서 vercel의
기술과 스타일대로 건드린다면, 오픈소스임에도 벤더 락인이 크게 걸린다는 비판 또한 존재합니다.</p>
<p>또한 공식적으로 등록되지 않은 알 수 없는 크롤러들에게 사이트가 크롤링되고 공유되어 SEO가 향상될 가능성도 존재하는데, 그런 면에서도 메타데이터를 스트리밍하는 일은 위험성이 일부 존재합니다.</p>
<h2 id="마무리">마무리</h2>
<p>저도 긱뉴스를 통해서 해당 이슈를 처음 접했는데요, 깃허브 디스커션이나 레딧 등 여러 곳에서 찬반 여론이 많아 조사해보았는데 꽤 재밌는 경험이었습니다.</p>
<p>현재는 Next.js의 15.1.8+ 이상 버전에서부터 고정으로 도입된 로직이기에, 만약 마이그레이션을 생각 중이시거나 SEO 관련 이슈를 안전하게 가져가야 하는 경우 stable한 Next.js 14 버전을 먼저 사용하시는 것을 추천드립니다.</p>
<p>저번 React의 Suspense 동작 이슈처럼, vercel이 협의점을 찾아 기술적으로 문제를 해결하거나 stable해지고 로직이 검증될 때까지 업데이트 소식을 지켜보아야 할 필요가 있어 보입니다.</p>
<h2 id="참조-문서">참조 문서</h2>
<p><a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/htmlLimitedBots">Next.js 공식 문서 : htmlLimitedBots</a>
<a href="https://nextjs.org/docs/app/api-reference/functions/generate-metadata#streaming-metadata">Next.js 공식 문서 : Streaming metadata</a>
<a href="https://github.com/vercel/next.js/blob/64702a9e422d42034a714ae40a8514d7911d0150/packages/next/src/server/lib/streaming-metadata.ts#L9">Github : shouldServerStreamingMetadata</a>
<a href="https://github.com/vercel/next.js/blob/64702a9e422d42034a714ae40a8514d7911d0150/packages/next/src/build/templates/app-page.ts#L280">Github : serveStreamingMetadata</a>
<a href="https://news.hada.io/topic?id=21430&amp;utm_source=slack&amp;utm_medium=bot&amp;utm_campaign=T0D8R8GPJ">GeekNews : Next.js 15.1+는 Vercel 외 환경에서 사실상 쓸 수 없다</a>
<a href="https://omarabid.com/nextjs-vercel">Next.js 15.1+ is unusable outside of Vercel</a>
<a href="https://github.com/vercel/next.js/blob/13fcfabf1beb3565d2bb191bd8d8ce4ad6d97b55/packages/next/src/shared/lib/router/utils/is-bot.ts#L5-L8">HTML_LIMITED_BOT_UA_RE_STRING</a>
<a href="https://github.com/vercel/next.js/pull/75873">Reland &quot;[metadata] new metadata insertion API and support PPR #75366&quot; #75873</a>
<a href="https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/metadata.tsx#L212">next.js/packages/next/src/lib/metadata/metadata.tsx</a>
<a href="https://github.com/vercel/next.js/blob/64702a9e422d42034a714ae40a8514d7911d0150/packages/next/src/lib/metadata/resolve-metadata.ts#L1075">next.js/packages/next/src/lib/metadata/resolve-metadata.tsx</a>
<a href="https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers?hl=ko">Google의 일반 크롤러 목록</a>
<a href="https://github.com/vercel/next.js/issues/55524">Async generateMetadata hangs the app without any visible sign of loading</a>
<a href="https://github.com/vercel/next.js/issues/79404#issuecomment-2896979747">Title tag position error.</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Query Options 기반 관리 패턴]]></title>
            <link>https://velog.io/@ubin_ing/react-query-options-basement-pattern</link>
            <guid>https://velog.io/@ubin_ing/react-query-options-basement-pattern</guid>
            <pubDate>Fri, 11 Apr 2025 09:21:31 GMT</pubDate>
            <description><![CDATA[<h3 id="tldr">TL;DR</h3>
<p>React Query에서 제공하는 queryOptions를 사용하여 도메인 기반으로 객체를 만들어 관리할 수 있습니다. 이 경우 <code>invalidateQueries</code>와 같은 queryKey가 필요한 작업을 진행할 때 매우 쉽게 접근하고 변경할 수 있어 유지보수에 용이합니다. 하나의 API마다 custom hook을 만들어 파일별로 관리하는 구조와 달리, 외부에서 query에 대한 정보가 필요할 때 내부 구현체에 접근하지 않아도 정보를 빠르게 얻을 수 있다는 점이 장점입니다.</p>
<blockquote>
<p><strong>이 글에서는...</strong>
React Query를 사용하며 아키텍처 구조를 효율적으로 작성하는 방법에 대해 소개합니다.</p>
</blockquote>
<h3 id="적절한-모듈화">적절한 모듈화</h3>
<p>지금껏 API를 다루어보면서 참 다양한 패턴으로 모듈화를 진행해보았는데요, 어떤 모듈화가 &#39;적절한 모듈화&#39;, 즉 유지/보수가 용이하고 접근하기 쉬운 모듈화인지 고민을 많이 해보게 되었습니다.</p>
<h3 id="기존-패턴의-문제점">기존 패턴의 문제점</h3>
<p>간단한 CRUD를 지원하는 게시판이 있다고 가정해보겠습니다.
API는 총 5개로, 목록 조회, 상세 조회, 생성, 수정, 삭제로 이루어져있습니다.
독자분들은 React Query를 사용하는 상황에서 해당 API들을 어떻게 관리하실건가요?</p>
<p>모듈화를 잘게 쪼갠 다음, 물리적인 파일 또한 분리하여 표현해보겠습니다.</p>
<pre><code class="language-jsx">// api/axios/getPostList.ts
export const getPostList = async () =&gt; { axios.get ... }</code></pre>
<pre><code class="language-jsx">// api/axios/getPostDetail.ts
export const getPostDetail = async (id: number) =&gt; { axios.get ... }</code></pre>
<pre><code class="language-jsx">// api/axios/createPost.ts
export const createPost = async (req: ...) =&gt; { axios.post ... }</code></pre>
<pre><code class="language-jsx">// api/axios/updatePost.ts
export const updatePost = async (id: number) =&gt; { axios.put ... }</code></pre>
<pre><code class="language-jsx">// api/axios/deletePost.ts
export const deletePost = async (id: number) =&gt; { axios.delete ... }</code></pre>
<p>이런 식으로 다섯 벌의 api 모듈을 각 물리적인 파일에 주입해주었습니다.
비슷한 방식으로 이들을 import하여 사용할 수 있는 React Query Custom hook도 정의해보겠습니다.</p>
<pre><code class="language-jsx">// api/queries/useGetPostDetailQuery.ts
export const useGetPostDetailQuery = (id: number) =&gt; {
    return useQuery({
        queryKey: [&#39;post&#39;, id],
          queryFn: () =&gt; getPostDetail(id),
    })
}</code></pre>
<pre><code class="language-jsx">// api/queries/useGetPostListQuery.ts
export const useGetPostListQuery = () =&gt; {
    return useQuery({
        queryKey: [&#39;post&#39;],
          queryFn: getPostList,
    })
}</code></pre>
<p>GET이 아닌 method들은 useMutation으로 관리합니다.</p>
<pre><code class="language-jsx">// api/mutations/useCreatePostMutation.ts
export const useCreatePostMutation = (req: ...) =&gt; {
    return useMutation({
          mutationFn: createPost,
    })
}</code></pre>
<pre><code class="language-jsx">// api/mutations/useUpdatePostMutation.ts
export const useUpdatePostMutation = (id: number) =&gt; {
    return useMutation({
        mutationFn: updatePost,
    })
}</code></pre>
<pre><code class="language-jsx">// api/mutations/useDeletePostMutation.ts
export const useDeletePostMutation = (id: number) =&gt; {
    return useMutation({
        mutationFn: deletePost,
    })
}</code></pre>
<p>이제 모든 API 서비스 로직을 정의했으니, 사용하는 단에서 원하는 hook을 호출해 사용할 수 있습니다.</p>
<pre><code class="language-jsx">...

const PostDetailPageView = ({ id }: PostDetailPageViewProps) =&gt; {
    const { data: post } = useGetPostDetailQuery();

      return (
      &lt;main&gt;{...}&lt;/main&gt;
    )
}</code></pre>
<p>그런데 React Query를 사용해보신 분들은 아시겠지만, mutation을 진행한 후 별도의 페이지 새로고침이 없이 새로운 데이터를 가져오려면 기존 데이터를 refetching해주는 작업이 필요합니다.</p>
<p>그러기 위해서 일반적으로 queryClient의 invalidateQueries를 많이 사용합니다.
데이터를 refetching하기 원하는 queryKey를 배열로 주입해주면 해당하는 쿼리에 refetching이 이루어집니다.</p>
<pre><code class="language-jsx">const queryClient = useQueryClient();
const { mutate } = useUpdatePostMutation();

mutate({ post })
queryClient.invalidateQueries({ queryKey: [/** ?? */] });</code></pre>
<p>그런데 문제는 여기서 발생합니다. 우리는 update 로직을 개발하면서 get의 queryKey에 대한 정보를 알아야 하는데, 위에서 설계했던대로라면 update 로직과 get 로직은 모듈도 다르고, queryKey도 따로 정의를 해 두었습니다.</p>
<p>파일 구조로 본다면 이렇게 되어있겠죠.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/1d4d3d53-be20-4f7a-ba53-0d7adca5e442/image.png" alt=""></p>
<p>개발자가 이 상황에서 get detail의 queryKey를 찾으려면 다음과 같은 플로우를 거쳐야 합니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/aae09bf6-3305-451a-867d-76ff8726604a/image.png" alt=""></p>
<p>여러 모듈과 패키지를 횡단하여 끝끝내 목적지에 도착한다면, 새로운 데이터를 fetch하는 간단한 작업에서 꽤 많은 시간을 사용할 수도 있겠네요.</p>
<p>지금은 비록 간단해보이고 큰 문제가 아닌 것 같다면, update에 get과 관련된 단서는 네이밍을 제외하고는 없는 상태에서 query와 mutation이 각각 100개씩 정의되어있다고 생각해볼 수 있습니다.</p>
<p>객관적으로는 프론트엔드에서 가장 합리적인 아키텍처는 개발자가 쉽게 모듈을 찾을 수 있는 구조라고 생각합니다. 에디터에서 디렉터리 -&gt; 파일 -&gt; 파일 -&gt; 다시 뒤로 -&gt; 디렉터리 ... 같은 횡단 없이, 깔끔하게 원하는 모듈을 거의 한 번에 볼 수 있는 구조 말입니다.</p>
<p>이제 글의 본문인 Options를 기반으로 React Query를 사용한 API 로직을 관리하는 패턴을 제시하겠습니다.</p>
<h3 id="options-기반-관리-패턴">Options 기반 관리 패턴</h3>
<p>api 패키지 내부 파일을 어떻게 나눌지를 먼저 정하겠습니다.</p>
<pre><code>├── api/
│   ├── post
│   │   ├── axios.ts
│   │   ├── queries.ts
│   │   └── types.ts</code></pre><p>api 내부에서 추가적인 디렉터리를 사용하지 않고 이 세 개의 파일로 모든 API 시나리오를 관리합니다.
queries나 mutations, axios와 같은 디렉터리가 아닌, <code>post</code>라는 도메인을 기반으로 파일들을 묶습니다.</p>
<p>도메인을 기반으로 그룹화를 하여 아키텍처가 기술이나 라이브러리에 종속되지 않고, 히스토리가 없는 개발자도 api -&gt; post로 이루어지는 구조를 보며 쉽게 이해할 수 있도록 합니다.</p>
<h4 id="axiosts">axios.ts</h4>
<p>axios.ts에는 베이스인 fetch 함수를 모두 정의합니다.</p>
<pre><code class="language-jsx">export const getPostList = async () =&gt; { axios.get ... }
export const getPostDetail = async (id: number) =&gt; { axios.get ... }
export const createPost = async (req: ...) =&gt; { axios.post ... }
export const updatePost = async (id: number) =&gt; { axios.put ... }
export const deletePost = async (id: number) =&gt; { axios.delete ... }</code></pre>
<p>기본적으로 fetch 함수를 건드리는 일은 잘 없습니다. 앞서 말한 데이터를 fetch하고, refetch하고, mutation하는 모든 일에서 fetch가 필요한 정보를 가지고 있지는 않기 때문입니다.</p>
<p>여기서 request를 정의해야할 때 위에서 정의한 types.ts 파일에 <code>CreatePostRequest</code>와 같은 인터페이스명으로 정의 후 이를 import하여 사용합니다.</p>
<h4 id="queriests">queries.ts</h4>
<p>quereis.ts에는 하나의 객체를 정의해줄겁니다.
위에서 서술한 예제처럼, mutation을 관리하다가 query를 보러 위치를 이동하고 하는 불상사가 없게 queries.ts에 mutation 또한 함께 정의합니다.</p>
<p>간단하게 설명하기 위해 주제와 무관한 타입 주석은 잠시 빼두겠습니다.</p>
<pre><code class="language-jsx">import { queryOptions, UseQueryOptions, MutationOptions } from &#39;@tanstack/react-query&#39;;

export const postQueries = {
  all: () =&gt; [&#39;post&#39;],
  list: (): UseQueryOptions&lt;GetPostListResponse&gt; =&gt;
    queryOptions({
      queryKey: [...postQueries.all(), &#39;list&#39;],
      queryFn: getPostList,
    }),
  detail: (id): UseQueryOptions&lt;GetPostListResponse&gt; =&gt;
    queryOptions({
      queryKey: [...postQueries.all(), id],
      queryFn: () =&gt; getPostDetail(id),
    }),
  ...
  create: (req): UseMutationOptions&lt;CreatePostResponse&gt; =&gt; 
      ({
        mutationFn: () =&gt; createPost(req),
      onSuccess: () =&gt; { ... }
      })
};</code></pre>
<p>React Query에서 제공하는 queryOptions라는 함수를 사용하여 각 인스턴스마다 queryKey, queryFn을 갖는 객체를 정의합니다.</p>
<p>서비스가 따로 하나의 useQuery를 물고 있는 hook을 가지는 게 아닌, useQuery와 useMutation을 호출할 때 필요한 정보들만을 가지고 있습니다.</p>
<p>mutationOptions는 4월 11일을 기준으로 아직 정의되지 않은 상태여서, UseMutationOptions 타입을 정의하여 Type Safety하게 개발을 진행할 수 있습니다.</p>
<p>이제 Options 기반 패턴으로 API 관련 로직을 정의했으니, 이를 다시 사용해보겠습니다.</p>
<pre><code class="language-jsx">...

const PostDetailPageView = ({ id }: PostDetailPageViewProps) =&gt; {
    const { data: post } = useQuery(postQueries.detail(id));

      return (
      &lt;main&gt;{...}&lt;/main&gt;
    )
}</code></pre>
<p>이 방식대로 개발을 진행했을 때, 업데이트하는 로직에서 queryKey를 찾으려면 어떻게 해야할까요?
정말 간편하게도 해당 options를 호출해주면 됩니다.</p>
<pre><code class="language-jsx">const queryClient = useQueryClient();
const { mutateAsync } = useMutation(postQueries.update());

...

const { id } = await mutateAsync({ post })
queryClient.invalidateQueries({ queryKey: postQueries.detail(id).queryKey });</code></pre>
<p>같은 객체에 존재하는 detail 프로퍼티의 queryKey에 접근해 이를 전달하면, 개발자가 api 관련 디렉터리에 방문하지 않아도 queryKey에 대한 정보를 가질 수 있게 됩니다.</p>
<p>구조분해할당을 진행한다면 코드가 더욱 간결해지고 명시적여지기도 하겠네요.</p>
<pre><code class="language-jsx">queryClient.invalidateQueries(postQueries.detail(id))</code></pre>
<p>invalidateQueries가 v5로 넘어오면서 <code>([])</code> =&gt; <code>({ queryKey: [] })</code>로 파라미터가 고정이 되었는데, 이런 패치 사항이 queryOptions의 스펙과 딱 맞아 더욱 사용하기 적절하다고 느낍니다.</p>
<h3 id="마무리">마무리</h3>
<p>라이브러리나 프레임워크에서 점점 개발자가 고민해야 할 문제들을 직접 해결하려고 하면서, 프론트엔드 개발자가 고민해야하는 일들이 클린 아키텍처에 더욱 관심이 쏠리고 있는 추세인 것 같습니다.</p>
<p>여러 파일을 만들지 않고도 빠르게 API를 정의할 수 있으며, 개발한 API를 사용하는 입장에서도 리소스를 대폭 줄일 수 있는 Options 기반 관리 패턴을 사용해보시는 걸 추천드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트를 더욱 잘 사용하는 5가지 팁 3분 만에 알아보기]]></title>
            <link>https://velog.io/@ubin_ing/how-to-use-react-well</link>
            <guid>https://velog.io/@ubin_ing/how-to-use-react-well</guid>
            <pubDate>Sat, 22 Mar 2025 15:03:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>리액트 개발자들 사이에서 자주 언급되고 인기있는 주제들을 정리합니다.</li>
</ul>
</blockquote>
<h3 id="1-immer---react를-vanilla-js처럼-사용하기">1. Immer - React를 Vanilla JS처럼 사용하기</h3>
<p>투두리스트를 구현할 때, 클릭한 투두의 done 여부가 바뀌는 state를 관리한다고 해보자.</p>
<pre><code class="language-tsx">import { useState } from &quot;react&quot;;

const initialState = [
  { id: 1, content: &quot;양파 사기&quot;, done: false },
  { id: 2, content: &quot;리액트 공부&quot;, done: false },
]

const Component = () =&gt; {
  const [todoList, setTodoList] = useState(initialState);

  const handleToggleTodoClick = (id) =&gt; {
      setTodoList(prev =&gt; prev.map(
      ((todo) =&gt; todo.id === id ? ({ ...todo, done: !todo.done }) : todo)
    )
  }
}

export default Component;</code></pre>
<p>불변성을 유지하기 위해서 전개 연산자를 섞어 사용하던 상태를, immer로 쉽게 변경 &amp; 적용할 수 있다.</p>
<pre><code class="language-jsx">import { produce } from &quot;immer&quot;;
import { useState } from &quot;react&quot;;

const initialState = [
  { id: 1, content: &quot;양파 사기&quot;, done: false },
  { id: 2, content: &quot;리액트 공부&quot;, done: false },
]

const Component = () =&gt; {
  const [todoList, setTodoList] = useState(initialState);

  const handleToggleTodoClick = (id) =&gt; {
    setTodoList(
      produce((draft) =&gt; {
        const targetTodo = draft.find((todo) =&gt; todo.id === id);
        targetTodo.done = !targetTodo.done;
      })
    );
  }
}

export default Component;</code></pre>
<p>리액트에서는 거의 금기시되던 일반적인 &#39;=&#39; 할당을 쉽게 사용할 수 있다.
사용할 때는 편리하게 접근하여 사용하는 것 같지만, immer가 자동으로 불변성 처리를 해 주어
복잡한 상태(배열이나 무거운 객체)를 핸들링할 때 매우 간편하다.</p>
<h3 id="2-고차-컴포넌트--고차-컴포넌트-합성">2. 고차 컴포넌트 &amp; 고차 컴포넌트 합성</h3>
<p>high-order components (hocs)를 통해 대상 컴포넌트를 Wrapping하여 플러그인처럼 사용할 수 있다.</p>
<pre><code class="language-jsx">const withAuthentication = (Component) =&gt; (props) =&gt; {
    const { user, isFetching } = fetch(props.id);

    if (isFetching) return &lt;SkeletonUIComponent /&gt;
    if (!user.isLoggedIn) return &lt;NotLoggedInComponent /&gt;

      return &lt;Component {...props} user={user} /&gt;
}

const MyPageComponent = withAuthentication(({ user, ...props }) =&gt; {
    return (
        &lt;div&gt;{user.name}님 안녕하세요!&lt;/div&gt;  
    )
})</code></pre>
<p>렌더링을 직접적으로 제어할 수 있고, 감싸진 자식 컴포넌트의 props를 편집하는 등 기능을 Proxy처럼 사용할 수 있다는 것이 장점임.</p>
<p>그런데 한 컴포넌트에서 여러 개의 고차 컴포넌트가 필요하다면 어떨까?
이벤트 로그를 추가하고, 다크 모드를 추가하고, 특정 상태에 맞게 페이지를 리다이렉트하는 것까지 총 세 가지 hocs를 추가한다고 가정해보자.</p>
<pre><code class="language-jsx">const MyPageComponent = withAuthentication(withLogging(withTheme(withRouting((props) =&gt; {
     ... 
}))))</code></pre>
<p>코드가 복잡해질뿐더러 가독성도 좋은 편이라고 말하기는 어렵다.</p>
<pre><code class="language-jsx">const compose = (...hocs) =&gt; (Component) =&gt;
    hocs.reduceRight((acc, hoc) =&gt; hoc(acc), Component);

const MyPageComponent = compose(
  withAuthentication,
  withLogging,
  withTheme,
  withRouting
)(MyComponent)</code></pre>
<p>compose 함수를 통해 가독성적인 측면에서 각 hocs들을 같은 depth에 올려두어
&#39;어떤 플러그인이 적용되어있지?&#39;를 빠르게 확인하고 쉽게 수정/삭제를 할 수 있도록 돕는다.</p>
<p>*hocs 이외에도 compose 패턴은 매우 유용하게 쓰일 수 있다 (이후 서술할 프롭 게터에서 더 알아보자)</p>
<h3 id="3-렌더-프롭">3. 렌더 프롭</h3>
<pre><code class="language-jsx">&lt;TodoList
  todoList={todoList}
/&gt;

const TodoList = ({ todoList }) =&gt; {
   return (
     &lt;ul&gt;
       {todoList.map((todo) =&gt; (
           &lt;li&gt;{todo.name}&lt;/li&gt;
       ))}
     &lt;/ul&gt;
   )
}</code></pre>
<p>TodoList는 이제 단 하나만의 스타일을 가진 컴포넌트만을 렌더링할 수 있다.
TodoList 컴포넌트를 조금 더 유연하게 바꿀 수 있게 만들려면 어떻게 해야 할까?</p>
<pre><code class="language-jsx">&lt;TodoList
  renderTodo={(todo) =&gt; {
      return &lt;li&gt;{todo.name}&lt;/li&gt;
  }}
  todoList={todoList}
/&gt;

const TodoList = ({ renderTodo, todoList }) =&gt; {
    return (
      &lt;ul&gt;
        {renderTodo(todoList)}
      &lt;/ul&gt;
    )
}</code></pre>
<p>각 투두에 대하여 상위 컴포넌트가 이를 렌더링시키며 의존성이 역전된다.
개발자가 코드를 확인할 때 하위 컴포넌트에 진입하여 확인하지 않고, 상위에서 어떤 엘리먼트가
렌더링되는지를 확인할 수 있다는 장점이 있다. + 유연성도 좋음!</p>
<h3 id="4-프롭-컬렉션--프롭-게터">4. 프롭 컬렉션 &amp; 프롭 게터</h3>
<p>여러 개가 사용되는 프롭은 객체로 묶어서 전개 연산자로 표현할 수 있다.
드래그 &amp; 드롭을 구현할 때는 <code>onDragOver</code>, <code>onDrop</code>, <code>onDragEnd</code>, <code>onDragStart</code> 같은 이벤트들이 함께 필요하다.</p>
<p>이를 이벤트 입장에서도 한 번에 묶어서 보고, 컴포넌트에 주입할 때도 깔끔하게 표현할 수 있도록 만들어보자.</p>
<pre><code class="language-jsx">const draggableProps = {
  onDragStart: (e) =&gt; {},
  onDragEnd: (e) =&gt; {},
}

const droppableProps = {
  onDragOver: (e) =&gt; {
      e.preventDefault();
  },
  onDrop: (e) =&gt; {},
};

&lt;DragZone {...draggableProps}/&gt;
&lt;DropZone {...droppableProps}/&gt;</code></pre>
<p>이렇게 프롭 컬렉션으로 깔끔하게 표현할 수 있다.
그런데 프롭 컬렉션을 사용하던 도중, 한 컴포넌트에서 약간의 변경이 필요하다고 생각해보자.</p>
<pre><code class="language-jsx">&lt;DropZone
  {...droppableProps}
  onDragOver={() =&gt; {
      alert(&quot;drag&quot;)
  }}
/&gt;</code></pre>
<p>프롭 게터를 이용하면 프롭 컬렉션에서 정의해둔 함수를 가독성 좋게 사용할 수 있다.</p>
<pre><code class="language-jsx">const compose = (...functions) =&gt; (...args) =&gt;
    functions.forEach((fn) =&gt; fn?.(...args))

const getDroppableProps = ({ onDragOver }) =&gt; {
  const defaultOnDragOver = (e) =&gt; {
    e.preventDefault(); 
  }

  return {
       onDragOver: compose(onDragOver, defaultOnDragOver),
  }
}

&lt;DropZone
  {...getDroppableProps({
      onDragOver: () =&gt; { ... }
  })}  
/&gt;</code></pre>
<p>훨씬 깔끔하게 표현 가능!</p>
<h3 id="5-메모이제이션">5. 메모이제이션</h3>
<p>항상 &#39;선택 사항&#39;이기 때문에 많이 헷갈릴 수 있는 메모이제이션을 간단하게 이야기해보자.</p>
<p><strong>React.memo</strong> : <strong>컴포넌트</strong>를 메모이제이션할 때
<strong>useMemo</strong> : <strong>값</strong>을 메모이제이션할 때
<strong>useCallback</strong> : <strong>함수</strong>를 메모이제이션할 때</p>
<p>useMemo와 useCallback은 도대체 어떨 때 사용해야 합리적일까?
=&gt; React.memo를 사용해도 리렌더링되는 컴포넌트를 최적화하는 데 좋다!</p>
<p>React.memo는 프롭을 얕게 비교하기 때문에, 스칼라 타입인 프롭에 한해서만 메모이제이션이 적용된다.</p>
<p>*스칼라 타입 : number, string, BigInt, undefined, null, boolean, symbol
=&gt; 참조값이 아닌 저장된 값 자체를 사용하는 타입들</p>
<p>*참조 타입(논-스칼라 타입) : 함수, 객체, 배열</p>
<p>즉, 리렌더링이 발생할 때마다 참조 타입은 재생성되며 계속해서 참조값이 바뀌어 리액트가 동일한 프롭이라고 인식할 수 없다. =&gt; 내용이 동일하더라도 렌더링이 발생한다!</p>
<p>참조 타입을 비교할 때에는 값이 아닌 참조값, 즉 메모리에 저장된 주소값을 보기에 비교가 되지 않음.</p>
<pre><code class="language-js">[1, 2, 3] === [1, 2, 3] // false

==

&quot;1,2,3이 저장된 2000번대 메모리 주소&quot; === &quot;1,2,3이 저장된 3000번대 메모리 주소&quot;</code></pre>
<p>프롭이 참조 타입이고, 하려는 작업이 무거운 작업일 때 useMemo를 사용할 수 있음.</p>
<pre><code class="language-jsx">const Component = ({ todoList }) =&gt; {
  const [counter, setCounter] = useState(0);
  const doneTodoList = todoList.filter(todo =&gt; todo.done);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setCounter(counter + 1)}&gt;증가&lt;/button&gt;
      &lt;DoneTodoList items={doneTodoList} /&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>이렇게 되면 버튼 컴포넌트를 클릭해 렌더링이 발생할 때마다 <code>doneTodoList</code> 값이 다시 계산된다.
doneTodoList 값의 계산 시간이 second 단위인 경우 매우 비효율적임.</p>
<pre><code class="language-jsx">const doneTodoList = useMemo(() =&gt; todoList.filter(todo =&gt; todo.done), [todoList])</code></pre>
<p>두 번째 인자인 의존성 배열에 넣은 값이 변경될 때에만 계산이 진행된다.
그렇기에 useMemo를 사용하면 counter가 바뀌어 렌더링이 되더라도 값을 재계산하지 않음.</p>
<p>스칼라 타입을 위한 메모이제이션을 진행하는 경우, 오히려 메모화하는 비용이 훨씬 비싸게 되어 손해가 될 수 있다. 기본적으로 리액트가 스칼라 타입을 계산하는 속도는 매우 빠르며, useMemo를 사용하는 원초적인 이유인 참조 타입의 자료형에 대한 일관성 유지가 무색해지기 때문이다.</p>
<p>예시는 간단하지만, 복잡하게 값을 편집할 땐 처음 다룬 immer와 섞어 사용하면 매우 좋을 듯!</p>
<p><strong>React.memo는 함수로 인해 메모화가 무력화당한다</strong></p>
<pre><code class="language-jsx">&lt;TodoList
  onDoneAllClick={handleDoneClick}
/&gt;</code></pre>
<p>부모 컴포넌트에서 리렌더링이 발생할 때마다 <code>handleDoneClick</code> 함수도 다시 재생성되고, 그 때마다 함수의 참조값이 바뀌어 memo를 썼다 하더라도 자식 컴포넌트도 같이 렌더링이 발생한다.</p>
<p>이럴 때 useCallback을 사용하여 원하는 값이 변경되었을 때만 함수를 재생성해줄 수 있다.</p>
<pre><code class="language-jsx">const handleDoneClick = useCallback(() =&gt; {
  ... progress ...
}, [progress])</code></pre>
<h3 id="마무리">마무리</h3>
<p>똑같은 코드를 짜면서도 어떤 식으로 짤 수 있는지에 대해 많이 고민해보는 것이 성장에 큰 도움이 된다고 느낀다.
최적화가 잘 되어있고, 읽기 쉬우며 가독성이 좋은 코드를 짤 수 있도록 노력해보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ts-morph를 사용하여 대규모 컴포넌트 마이그레이션을 자동화하는 방법]]></title>
            <link>https://velog.io/@ubin_ing/migration-automation-with-ts-morph</link>
            <guid>https://velog.io/@ubin_ing/migration-automation-with-ts-morph</guid>
            <pubDate>Tue, 11 Mar 2025 09:17:23 GMT</pubDate>
            <description><![CDATA[<p>개발 도중 프로젝트 전체에서 사용하던 컴포넌트를 동시에 마이그레이션하는 일이 필요했습니다.
vscode의 replace 기능이나 개발자의 수작업으로도 해결 가능하겠지만, 
그럴 경우 공수가 많이 들고 실수 또한 잦아지게 됩니다. </p>
<p>또한 대상 컴포넌트가 사용되고 있는 곳의 개수가 몇 천, 몇 만 단위인 경우 마이그레이션은 거진 불가능에 가깝습니다.</p>
<p>자동화를 통해서 마이그레이션을 단 하나의 스크립트로 해결하는 방법은 없을까요?</p>
<blockquote>
<p><strong>이 글에서는...</strong>
ts-morph를 사용해 대규모 컴포넌트 마이그레이션을 자동화하는 방법을 소개합니다.</p>
</blockquote>
<h3 id="ast란">AST란?</h3>
<p>AST(추상 구문 트리)는 소스 코드의 구조를 트리 형태로 표현하는 개념입니다.</p>
<p>간단히 두 숫자를 더해 만든 변수가 있다고 가정해보겠습니다.</p>
<pre><code class="language-js">const sum = 1 + 2;</code></pre>
<p>이 코드를 AST로 변환하면 다음과 같이 변형됩니다. (예시입니다)</p>
<pre><code>Program
├── VariableDeclaration (const)
│   ├── Identifier (sum)
│   ├── BinaryExpression (+)
│       ├── NumericLiteral (1)
│       ├── NumericLiteral (2)</code></pre><p>이렇게 소스 코드를 트리 형식으로 변환하여, 값을 조금 더 쉽게 참조하고 변형할 수 있도록 돕는 것이 AST입니다.
AST는 크게 세 가지의 단계를 거치게 됩니다.</p>
<p><strong>토큰화</strong> : 소스 코드를 최소 단위인 토큰으로 분리하여, 코드 구문을 파악합니다.
<strong>파싱</strong> : 토큰들을 문법 규칙에 따라 트리 구조로 변환합니다.
<strong>트리 생성</strong> : AST라는 트리 구조를 생성하고, 이를 활용하여 코드를 변경할 수 있도록 돕습니다.</p>
<p>해당 세 단계를 거치고, 개발자가 원하는 코드를 수정하면 AST는 수정된 트리를 기반으로 새로운 코드를 만들어 저장합니다.</p>
<h3 id="ts-morph">ts-morph</h3>
<p>ts-morph는 TypeScript 기반 라이브러리로, 위에서 설명드린 AST를 개발자가 더 쉽게 접근할 수 있도록 도와줍니다. 변수명을 바꾸거나, 새로운 코드를 생성하거나, 함수의 호출을 변경하는 등 자유도가 매우 높다는 것이 장점입니다.</p>
<p>ts-morph를 사용하면 JSX 구문의 컴포넌트들도 변경할 수 있습니다.</p>
<p>저는 현재의 마이그레이션 이후에도 추후 기호에 맞게 함수를 조합해서 사용할 수 있도록,
플러그인 방식으로 코드를 설계했습니다.</p>
<pre><code class="language-jsx">import { Project, SourceFile } from &quot;ts-morph&quot;;

type CodemodPluginType = (sourceFile: SourceFile) =&gt; void;

interface GenerateCodemodType {
  targetPath: string;
  plugins: Array&lt;CodemodPluginType&gt;;
}

/* generate 함수 */
export const generateCodemod = ({
  targetPath,
  plugins,
}: GenerateCodemodType) =&gt; {
  const runCodemod = () =&gt; {
    const project = new Project({ tsConfigFilePath: &quot;tsconfig.json&quot; });
    const sourceFileList: Array&lt;SourceFile&gt; =
      project.getSourceFiles(targetPath);

    sourceFileList.forEach((sourceFile) =&gt; {
      plugins.forEach((plugin) =&gt; {
        plugin(sourceFile);
      });
      sourceFile.saveSync();
    });
  };
  return { runCodemod };
};</code></pre>
<pre><code class="language-jsx">import { type SourceFile } from &quot;ts-morph&quot;;
import { ConvertPropsType } from &quot;./types&quot;;

/* plugin 함수 */
export const convertImportPathPlugin =
  ({ before, after }: ConvertPropsType) =&gt;
  (sourceFile: SourceFile) =&gt; {
    const importDeclarationList = sourceFile.getImportDeclarations();

    importDeclarationList.forEach((importDecl) =&gt; {
      const moduleSpecifierValue = importDecl.getModuleSpecifierValue();
      if (moduleSpecifierValue === before) {
        importDecl.setModuleSpecifier(after);
      }
    });
  };</code></pre>
<p>위와 같이 generate 함수와 plugin 함수를 두고, codemod script가 필요할 때 
이를 섞어서 조합할 수 있습니다.</p>
<pre><code class="language-jsx">import { generateCodemod } from &quot;../core/generateCodemod&quot;;
import { convertComponentNamePlugin } from &quot;../plugins/convertComponentNamePlugin&quot;;
import { convertImportNamePlugin } from &quot;../plugins/convertImportNamePlugin&quot;;
import { convertImportPathPlugin } from &quot;../plugins/convertImportPathPlugin&quot;;
import { convertPropsNamePlugin } from &quot;../plugins/convertPropsNamePlugin&quot;;

const { runCodemod } = generateCodemod({
  targetPath: &quot;src/**&quot;,
  plugins: [
    convertImportPathPlugin({
      before: &quot;@/components/v3/Legacy/Legacy&quot;,
      after: &quot;@/components/v3/New/New&quot;,
    }),
    convertImportNamePlugin({
      importPath: &quot;@/components/v3/New/New&quot;,
      before: &quot;Legacy&quot;,
      after: &quot;New&quot;,
    }),
    convertComponentNamePlugin({
      before: &quot;Legacy&quot;,
      after: &quot;New&quot;,
    }),
    convertPropsNamePlugin({
      componentName: &quot;New&quot;,
      target: [{ before: &quot;sssss&quot;, after: &quot;prop2&quot; }],
    }),
  ],
});</code></pre>
<p>저는 import path와 import명, 실제 사용되는 컴포넌트명과 props를 바꾸는 네 가지의 플러그인을 개발하여 사용했습니다.</p>
<p>이제 해당 코드를 실행하면 targetPath 내부에 해당하는 모든 코드들은 다음처럼 변경됩니다.</p>
<h4 id="before">before</h4>
<pre><code class="language-jsx">import Legacy from &quot;@/components/v3/Legacy/Legacy&quot;;

const Page2 = () =&gt; {
  return (
    &lt;div&gt;
      &lt;Legacy prop1=&quot;test&quot; sssss={999} /&gt;
    &lt;/div&gt;
  );
};

export default Page2;</code></pre>
<h4 id="after">after</h4>
<pre><code class="language-jsx">{/* import명 변경, import path 변경 */}
import New from &quot;@/components/v3/New/New&quot;;

const Page2 = () =&gt; {
  return (
    &lt;div&gt;
      {/* props명 변경, 컴포넌트명 변경 */}
      &lt;New prop1=&quot;test&quot; prop2={999} /&gt;
    &lt;/div&gt;
  );
};

export default Page2;</code></pre>
<h3 id="마무리">마무리</h3>
<p>codemod를 통해 개발자의 반복 작업에 대한 피로도와 리소스를 줄이고, 변경된 작업에 한하여 UI 확인만 진행하는 등 더욱 효율적으로 업무를 처리할 수 있게 되었습니다.</p>
<p>저와 비슷하게 대규모 프로젝트에서 코드를 마이그레이션해야 한다면, ts-morph를 사용해 스크립트로 자동화해보시는 걸 추천드립니다.</p>
<p><a href="https://github.com/ubinquitous/component-codemod">깃허브 바로가기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[changeset과 turborepo, github actions + husky로 private npm registry 배포하기]]></title>
            <link>https://velog.io/@ubin_ing/setup-private-npm-registry</link>
            <guid>https://velog.io/@ubin_ing/setup-private-npm-registry</guid>
            <pubDate>Tue, 25 Feb 2025 01:23:14 GMT</pubDate>
            <description><![CDATA[<p>현재 속한 조직에서 저는 private npm registry를 도입하자는 의견을 제시하게 되었습니다.
기존에는 하나의 repository 내부에서 사용하는 package를 관리했었는데요, 그렇게 되면 다음과 같은 문제들이 있었습니다.</p>
<ul>
<li>종속성 : 공유 패키지에 변경 사항이 발생하면 모든 어플리케이션 코드가 이에 대한 dependency를 가집니다.</li>
<li>폐쇄성 : 타 repository에서 동일한 패키지를 사용해야할 때 니즈를 맞추기 어렵습니다.</li>
<li>용량 및 크기 : 패키지에만 존재하는 여러 dependency들이 함께 관리되어 repository의 npm dependency가 굉장히 무거워집니다.</li>
</ul>
<blockquote>
<p><strong>이 글에서는...</strong></p>
</blockquote>
<ul>
<li>turborepo와 changeset을 이용해서 private npm registry 환경을 구축합니다.</li>
</ul>
<h2 id="turborepo-setup">turborepo setup</h2>
<p>package manager로는 pnpm을 사용했습니다.</p>
<pre><code class="language-bash">pnpm dlx create-turbo@latest</code></pre>
<p>프로젝트가 생성되면 JSON을 다음과 같이 설정합니다.</p>
<pre><code class="language-js">{
  &quot;name&quot;: &quot;[패키지명]&quot;, // 해당 패키지명에 따라 하위 패키지가 @[패키지명]/** 으로 설정됩니다.
  &quot;version&quot;: &quot;0.0.0&quot;
  &quot;private&quot;: true, // root repository는 배포 대상이 아니기에 private을 true로 설정합니다. 
  &quot;workspaces&quot;: [
    &quot;packages/*&quot;
  ],
  &quot;scripts&quot;: {
    ...
    &quot;changeset&quot;: &quot;changeset&quot;,
    &quot;changeset:publish&quot;: &quot;pnpm prepack &amp;&amp; changeset publish&quot;,
    &quot;changeset:version&quot;: &quot;changeset version &amp;&amp; pnpm i --lockfile-only&quot;,
    ...
    &quot;publish-packages&quot;: &quot;turbo run build &amp;&amp; changeset version &amp;&amp; changeset publish&quot;,
    &quot;version-packages&quot;: &quot;changeset version&quot;,
    &quot;release&quot;: &quot;turbo build &amp;&amp; changeset publish&quot;,
    &quot;prepack&quot;: &quot;turbo run prepack&quot;,
  },
  &quot;dependencies&quot;: {
    &quot;@[패키지명]/a&quot;: &quot;workspace:^&quot;,
    &quot;@[패키지명]/b&quot;: &quot;workspace:^&quot;,
    &quot;@[패키지명]/c&quot;: &quot;workspace:^&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@changesets/changelog-github&quot;: &quot;^0.5.0&quot;,
    &quot;@changesets/cli&quot;: &quot;^2.27.11&quot;,
    &quot;husky&quot;: &quot;^9.1.7&quot;,
    &quot;prettier&quot;: &quot;^3.5.0&quot;,
    &quot;turbo&quot;: &quot;^2.4.1&quot;,
    &quot;typescript&quot;: &quot;5.7.3&quot;
  }
}</code></pre>
<p>위와 같이 package.json에서 기본적인 설정을 마치고, 하위 패키지에서도 세팅을 진행합니다.
turborepo에서 기본으로 세팅해주는 typescript-config을 기준으로 세팅해보겠습니다.</p>
<pre><code class="language-js">{
  &quot;name&quot;: &quot;@testtest/typescript-config&quot;,
  &quot;version&quot;: &quot;0.0.0&quot;,
  &quot;private&quot;: false, // private은 꼭 false 또는 해당 라인을 삭제해주세요. private npm registry 배포와 무관하게 해당 패키지의 배포 가능 여부를 따지는 필드입니다.
  &quot;license&quot;: &quot;MIT&quot;,
  &quot;publishConfig&quot;: {
    &quot;access&quot;: &quot;public&quot;
  }
}</code></pre>
<p><code>pnpm i</code>를 통해 명시된 dependencies를 설치합니다.
혹시나 turborepo에서 default로 생성하는 어플리케이션에 dependency가 걸려있어 install이 안 되고 있는지 확인해주세요. 만약 그런 경우, 사용하지 않는 어플리케이션이나 패키지를 삭제하고(ex) docs, web, ui ...) 필요한 패키지만을 명시해주세요.</p>
<h2 id="changeset-setup">changeset setup</h2>
<p>다음 명령어를 실행합니다.</p>
<pre><code class="language-bash">pnpm changeset init</code></pre>
<p>성공적으로 initialize가 되면 프로젝트의 root에 .changeset 디렉터리가 생깁니다.
changeset setup은 매우 간단해서, 기본적으로는 이게 끝입니다.
추가적으로 changeset에 세팅할 다를 내용들이 있다면, config.json에서 수정할 수 있습니다.</p>
<p><a href="https://github.com/changesets/changesets/blob/main/docs/config-file-options.md">각 옵션의 effect들은 여기서 확인하세요</a></p>
<h2 id="changeset-publish">changeset publish</h2>
<p>이제 changeset을 성공적으로 publish할 수 있습니다.</p>
<pre><code class="language-bash">pnpm changeset // git add와 유사합니다. 어떤 패키지를 업데이트 대상에 포함할지 정합니다.
pnpm changeset:version // git commit과 유사합니다.
pnpm changeset:publish // git push와 유사합니다. 실제로 패키지가 publish됩니다.</code></pre>
<p>작업사항이 생길 떄마다 다음 세 가지 커맨드로 changeset을 사용할 수 있습니다.</p>
<h2 id="private-npm-registry-setup">private npm registry setup</h2>
<p>여기서부터가 중요한데요, private npm registry에 배포하려면 registry url을 가리키는 코드가 필요합니다.</p>
<p>프로젝트의 root에 .npmrc 파일을 생성한 후 다음과 같이 세팅합니다.</p>
<pre><code>@[패키지명]:registry=[registry server url]
//[https를 제외한 registry server url]/:_authToken=${NPM_TOKEN}</code></pre><p>그런 다음, packages 하위의 각 package에 있는 package.json에서 registry url을
바라보게 하는 셋업 코드 두 가지를 추가합니다.</p>
<pre><code class="language-js">...
  &quot;publishConfig&quot;: {
    &quot;registry&quot;: &quot;[registry server url]&quot;
  },
  &quot;repository&quot;: {
    &quot;type&quot;: &quot;git&quot;,
    &quot;url&quot;: &quot;[해당 repository의 git 주소]&quot;
  },
...</code></pre>
<p>publish를 진행할 때 package를 순회하면서 스크립트를 실행하기 때문에, 꼭 root가 아닌
각 package마다 해당 코드가 위치해있어야 합니다.</p>
<h2 id="deploy-with-github-actions">deploy with github actions</h2>
<p>저는 작업사항이 생기면 git push를 진행할 때마다 로컬에서 직접 changeset의 업데이트 과정을
거쳐야 한다는 게 번거롭다고 느껴서, github actions와 함께 해당 이슈를 해결했습니다.</p>
<pre><code class="language-yml">name: Release

on:
  push:
    branches:
      - main

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v4

      - name: Setup pnpm 8
        uses: pnpm/action-setup@v3
        with:
          version: 8

      - name: Setup Node.js 20.x
        uses: actions/setup-node@v4
        with:
          node-version: 20.x

      - name: Install Dependencies
        run: pnpm i

      - name: Create Release Pull Request or Publish to npm
        id: changesets
        uses: changesets/action@v1
        with:
          # This expects you to have a script called release which does a build for your packages and calls changeset publish
          publish: pnpm release
        env:
          GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }}
          NPM_TOKEN: ${{ secrets.GIT_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.GIT_TOKEN }}
permissions:
  contents: write
  id-token: write</code></pre>
<p>pnpm release는 결국 <code>changeser version</code>, <code>changeset publish</code>를 진행하는 로직이기에, 해당 워크플로우가 돌기 전에 <code>changeset</code> 명령어를 수행해야 합니다.</p>
<h2 id="automation-with-husky">automation with husky</h2>
<p>그래서 git push를 진행하면 올리는 동시에 사용자가 changeset을 세팅하도록 husky를 사용했습니다.</p>
<pre><code class="language-bash">#!/bin/sh
. &quot;$(dirname &quot;$0&quot;)/_/husky.sh&quot;

# GitHub Actions에서 실행되는 경우 pre-push 훅을 건너뛰는 로직입니다
if [ &quot;$GITHUB_ACTIONS&quot; = &quot;true&quot; ]; then
  echo &quot;Skipping pre-push hook in GitHub Actions&quot;
  exit 0
fi

echo &quot;Running pre-push hook...&quot;

# 해당 명령어를 작성해주지 않으면 husky가 cli의 changeset을 강제로 skip합니다.
exec &lt; /dev/tty

pnpm build
pnpm changeset

# changeset을 진행하면 .changeset에 임시 업데이트 파일이 생기는데, 해당 파일을 반영하기 위해 pre-push 단계에서 같이 파일을 commit합니다.
git add .changeset/
git commit -m &quot;add changeset dump file&quot;</code></pre>
<p>이렇게 되면 PR을 올리고, 머지한 다음 github actions가 실행됩니다.
github actions 실행이 끝마치게 되면 changeset이 <code>Version Packages</code> PR을 업로드하고, 해당 PR을 머지하면 자동으로 태깅 + 릴리즈 + CHANGELOG + version update가 완료됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[조건에 따른 화면 분기에서의 코드 가독성 향상 및 프론트엔드 UI 테스트 환경 개발 자동화]]></title>
            <link>https://velog.io/@ubin_ing/useConditionalRendering</link>
            <guid>https://velog.io/@ubin_ing/useConditionalRendering</guid>
            <pubDate>Sun, 16 Feb 2025 12:09:27 GMT</pubDate>
            <description><![CDATA[<p>아 제목 뭐로하지</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
</blockquote>
<ul>
<li>조건에 따른 화면 분기에서 테스트 자동화를 동시에 셋업하는 아이디어에 대해 설명합니다.</li>
<li>useConditionalRendering이라는 custom hook에 대한 패러다임을 제시합니다.</li>
</ul>
<h2 id="stage-환경에서의-테스트-자동화">STAGE 환경에서의 테스트 자동화</h2>
<p>규모 있는 조직에서 하나의 어플리케이션을 관리할 때에는 보통 환경을 세 개 이상으로 나누어 테스트합니다.
일반적으로는 다음과 같은 환경들이 존재합니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/f18268fe-98ca-4d47-b01a-f70631266d21/image.png" alt=""></p>
<ul>
<li>PRODUCTION : 실제로 사용자가 서비스를 사용하는 환경입니다.</li>
<li>STAGE : PRODUCTION과 완벽히 똑같으나, 내부에서 테스트를 위해 사용하는 환경입니다.</li>
<li>DEVELOP : 개발자가 개발을 위해 일시적으로 사용하는 환경입니다.</li>
</ul>
<p>기본적으로 개발자는 DEVELOP 환경에서 개발을 끝마친 후 STAGE에 작업 사항을 업로드한 후,
STAGE에서 작업물의 이상이 없음이 확인되면 PRODUCTION으로 배포를 진행하는 프로세스를 거칩니다.</p>
<h2 id="디자인-qa에-대한-번거로움">디자인 QA에 대한 번거로움</h2>
<p>제가 속한 조직에서는 디자인이 변경될 경우 꼭 디자인 QA 프로세스를 STAGE에서 거치는데요,
이 때 어플리케이션이 조건에 따른 분기가 잦다면 쉽게 테스트를 진행하기 어려워집니다.</p>
<p>예를 들어 한 쇼핑몰 사이트가 있는데, 회원 등급이 일반 회원과 VIP 회원으로 나뉜다고 가정하겠습니다.
그렇다면 디자이너는 쇼핑몰 사이트의 STAGE 환경에서 일반 회원으로의 디자인 변경사항을 확인하고,
그 다음은 VIP 회원의 디자인 변경사항을 확인할 것입니다.</p>
<p>기본적으로는 테스트를 어떻게 진행할까요? 서버 개발자에게 테스트 계정을 만들어달라고 부탁한 후
이를 사용하여 테스트를 진행할 것입니다.</p>
<h3 id="그런데-만약">그런데 만약...</h3>
<p>만약 그 데이터가 임의로 조작할 수 없는, 오픈API를 사용한 데이터라면 어떻게 해야 할까요?
예를 들어 무조건 본인 인증을 통해서만 특정 플래그가 할당되어 화면이 바뀌는,
그런 비즈니스 로직이 엉켜있다면 테스트는 매우 번거로워지게 됩니다.</p>
<h3 id="fe에서의-처리">FE에서의 처리</h3>
<p>그렇기에 번거롭게 임의의 서버 데이터를 변경하여 테스트를 진행하지 않고, 프론트엔드의 DEVELOP 환경에서 디자이너와 개발자가 함께 모여 분기를 임의로 변경하여 화면을 확인하여 처리할 수 있습니다.</p>
<p>그런데 이렇게 되면...</p>
<ul>
<li>프론트엔드 개발자의 테스트 의존성이 강하게 묶입니다.</li>
<li>작업 사항이 완전히 반영된 STAGE 환경이 아닌, DEVELOP 환경에서 화면을 확인하기에 실제 배포되는 화면과 다를 가능성이 있습니다.</li>
</ul>
<p>그렇다면, 기본적으로 개발을 진행하고 STAGE 환경에서만 분기를 변경할 수 있는 테스트 툴을 만들면 어떨까요?</p>
<h2 id="useconditionalrendering">useConditionalRendering</h2>
<p>위 아이디어를 바탕으로 아주 간단한 demo 훅을 개발했습니다.
아직 사용할 수 있는 단계는 아니지만, 아이디어를 받쳐주기에는 충분한 정도로 개발되어 있습니다.</p>
<h3 id="usage">Usage</h3>
<ol>
<li>루트에 Wrapper와 DevTools(optional)를 추가합니다.<pre><code class="language-jsx">import { ConditionalRenderingWrapper, ConditionalRenderingDevTools } from &quot;./hooks/ConditionalRenderingWrapper&quot;;
</code></pre>
</li>
</ol>
<p>const RootApplication = () =&gt; {
  return (
    <ConditionalRenderingWrapper>
      <UIComponent />
      <ConditionalRenderingDevTools />
    </ConditionalRenderingWrapper>
  );
};</p>
<pre><code>
2. useConditionalRendering hook을 호출하여 사용합니다. parameter는 배열을 가집니다.
```ts
interface UseConditionalRenderingType {
  name: string; // DevTools에서 보여줄 분기명입니다.
  component: ReactNode; // 조건에 따라 분기를 결정할 대상 컴포넌트입니다.
  condition: boolean; // 해당 프로퍼티가 true인 경우 component를 보여줍니다.
}</code></pre><p>사용 예제는 다음과 같습니다.</p>
<pre><code class="language-jsx">import { useConditionalRendering } from &quot;./hooks/useConditionalRendering&quot;;

const UIComponent = () =&gt; {
  ...
  const { render } = useConditionalRendering([
    {
      name: &quot;USER&quot;,
      component: &lt;UserComponent /&gt;,
      condition: authority === &quot;USER&quot;,
    },
    {
      name: &quot;ADMIN&quot;,
      component: &lt;AdminComponent /&gt;,
      condition: authority === &quot;ADMIN&quot;,
    },
  ]);

  return &lt;main&gt;{render()}&lt;/main&gt;;
};</code></pre>
<blockquote>
<p><strong>useConditionalRendering을 사용하면...</strong></p>
</blockquote>
<ul>
<li>복잡하고 읽기 어려운 분기별 렌더링을 명시적으로 선언하고 사용할 수 있습니다.</li>
<li>DevTools를 사용하여 테스트 환경에서 추가 리소스 없이 자동으로 분기별 UI를 테스트할 수 있습니다.</li>
</ul>
<p>예시 화면은 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/df1527d6-5a3c-41fb-b404-f2fd25b264c5/image.gif" alt=""></p>
<p>checked라는 state에 따라서 UI1과 UI2가 다르게 보이는 간단한 예제입니다.
useConditionalRendering을 사용하면 DevTools를 사용하여 클릭만으로 분기를 변경할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/1e98f9e7-5eab-4709-b555-9a238807b030/image.gif" alt=""></p>
<p>이렇게 되면 개발과 동시에 테스트를 자동화하여, 개발자와 비개발자를 불문하고 STAGE에서도 쉽고 유연하게 UI 테스트를 진행할 수 있습니다.</p>
<h2 id="마무리--해결해야-할-문제들">마무리 &amp; 해결해야 할 문제들</h2>
<p>아직 useConditionalRendering이 demo인 가장 큰 이유 두 가지는 다음과 같습니다.</p>
<ul>
<li>useConditionalRendering을 사용하여 렌더링하는 컴포넌트 내에 또 다른 useConditionalRendering hook이 호출될 경우, 예상과 다르게 렌더링이 재귀적으로 돌아 어플리케이션이 멈춥니다.</li>
<li>STAGE 환경이 아닌 PRODUCTION 환경에서의 영향도를 아직 검증하지 않은 상태입니다.</li>
</ul>
<p>해당 문제를 해결하고 조금더 유연하게 사용할 수 있도록 hook을 리팩토링한다면,
클린 코드와 테스트 자동화를 동시에 챙길 수 있는 좋은 플러그인이 탄생할 것이라 생각합니다.</p>
<p>긴 글 읽어주셔서 감사합니다!</p>
<p><a href="https://github.com/Ubinquitous/use-conditional-rendering">깃허브 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[어떻게 구현하실건가요?]]></title>
            <link>https://velog.io/@ubin_ing/limit-of-css-gap</link>
            <guid>https://velog.io/@ubin_ing/limit-of-css-gap</guid>
            <pubDate>Fri, 13 Dec 2024 10:19:33 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ubin_ing/post/5510ca2e-0c2d-44e1-86d1-4c21d9c6c042/image.png" alt=""></p>
<p>HTML과 CSS로 해당 사이트의 파란색 부분을 퍼블리싱해야 한다면, 여러분은 어떻게 구현하실 건가요?
보통은 <code>display: flex</code>나 <code>display: grid</code>와 함께 <code>gap: 20px</code>을 주어 리스트 내 아이템 간의 간격을 벌리게 됩니다.</p>
<pre><code class="language-jsx">&lt;ul&gt;
  &lt;li&gt;Item 1&lt;/li&gt;
  &lt;li&gt;Item 2&lt;/li&gt;
  &lt;li&gt;Item 3&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<pre><code class="language-css">ul {
  display: flex;
  flex-direction: column;
  gap: 20px;
}</code></pre>
<p>gap을 사용하면 다른 CSS 프로퍼티인 <code>position: relative</code>와 <code>position: absolute</code>를 사용하는 것보다 짧은 코드로 간격을 벌릴 수 있으며,
<code>margin-top: 20px</code>과 같이 각 아이템마다 간격을 명시하는 코드를 작성해야 하는 불편함도 피할 수 있습니다. (더군다나 margin은 tracking 또한 gap에 비해 어렵습니다)</p>
<p>여러 아이템을 렌더링한다고 가정할 때도 아이템들을 감싸는 리스트에만 해당 속성을 부여함으로 가독성을 높이고 반복되는 코드를 피할 수 있습니다.</p>
<p>저 또한 HTML을 사용하면서 <code>flex</code> 와 <code>gap</code> 프로퍼티를 유용하게 사용하며,
CSS에 존재하는 최고의 프로퍼티라고 생각했습니다.</p>
<p>그런데, 이 프로퍼티들을 사용하며 불편함을 느껴보신 적은 없으신가요?</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>간격을 벌려주는 컴포넌트를 통해 gap을 대체하는 방법을 소개합니다.</li>
<li>React의 컴포넌트를 기반으로 내용을 설명합니다.</li>
</ul>
</blockquote>
<h2 id="gap의-단점">gap의 단점</h2>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/a26a6a72-f5f1-4747-a822-b90d9520ea4f/image.png" alt=""></p>
<p>위와 같은 UI를 <code>gap</code> 프로퍼티를 통해 구현해 보겠습니다.</p>
<pre><code class="language-jsx">&lt;ul&gt;
  &lt;li&gt;Item 1&lt;/li&gt;
  &lt;li&gt;Item 2&lt;/li&gt;
  &lt;li&gt;Item 3&lt;/li&gt;
  &lt;li&gt;Item 4&lt;/li&gt;
  &lt;li&gt;Item 5&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<pre><code class="language-css">ul {
  display: flex;
  flex-direction: column;
  gap: 20px;
}</code></pre>
<p>단 세 줄의 CSS만으로 UI를 구현할 수 있습니다. 
그런데 만약 같은 목록에서 벌려야 하는 간격이 다른 경우는 어떻게 해야 할까요?</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/b156203c-716d-45f3-b9f7-7fc62b1bc962/image.png" alt=""></p>
<p>신나게 개발하던 중, 디자인의 변경으로 인해 리스트의 첫 번째 아이템에만 10px만큼의 간격을 벌려야 합니다.</p>
<p>이런 요구사항의 경우, 개발자는 Element를 한 depth 더 래핑하여 10px에 대한 예외 처리를 해 주어야 합니다.</p>
<pre><code class="language-jsx">&lt;ul&gt;
  &lt;li class=&quot;first-list-item&quot;&gt;
       &lt;hgroup&gt;Item 1&lt;/hgroup&gt;
      &lt;hgroup&gt;Item 2&lt;/hgroup&gt;
  &lt;/li&gt;
  &lt;li&gt;Item 3&lt;/li&gt;
  &lt;li&gt;Item 4&lt;/li&gt;
  &lt;li&gt;Item 5&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<pre><code class="language-css">ul {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.first-list-item {
  display: flex;
  flex-direction: column;
  gap: 10px;
}</code></pre>
<p>중복되는 코드도 발생하였고, HTML의 트리 구조도 조금은 복잡해졌지만 나름 유쾌하게 해결했습니다.</p>
<p>그런데 요구사항이 만약 더욱 복잡하게 바뀐다면 어떻게 될까요?</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/122307aa-468d-408c-874d-d995aa0ed292/image.png" alt=""></p>
<p>디자인이 변경되어 제일 마지막의 Item의 gap은 40px을 처리해 주어야 합니다.</p>
<pre><code class="language-jsx">&lt;ul&gt;
  &lt;li class=&quot;first-list-item&quot;&gt;
       &lt;hgroup&gt;Item 1&lt;/hgroup&gt;
      &lt;hgroup&gt;Item 2&lt;/hgroup&gt;
  &lt;/li&gt;
  &lt;li&gt;Item 3&lt;/li&gt;
  &lt;li class=&quot;last-list-item&quot;&gt;
    &lt;hgroup&gt;Item 4&lt;/hgroup&gt;
      &lt;hgroup&gt;Item 5&lt;/hgroup&gt;
  &lt;/li&gt;
&lt;/ul&gt;</code></pre>
<pre><code class="language-css">ul {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.first-list-item {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.last-list-item {
  display: flex;
  flex-direction: column;
  gap: 40px;
}</code></pre>
<p>어떻게든 처리가 되었지만, 똑같은 세 줄의 CSS 코드가 세 번이나 반복되고 있습니다. 
아이템 내 또 다른 아이템을 처리하여 HTML 구조도 훨씬 읽기 복잡해져서, 이제 구조만으로는
정확하게 리스트를 렌더링하고 있다고 한 눈에 알아보기 힘들어졌습니다.</p>
<h3 id="margin은-어떤가요">margin은 어떤가요?</h3>
<p>그렇다고 margin으로 이를 처리한다고 해도, margin을 주어야 하는 각 li에 대해 추가적인 CSS를 작성해 주어야 하는 것뿐만 아니라,
만약 요구 사항이 변경되어 코드를 tracking해야 하는 경우 margin의 행방은 gap에 비해 찾기가 어려워집니다.</p>
<p>또한 gap과 다르게 margin을 부여한 li가 CSS Layout 상에서 해당 공간을 차지하고 있기에, 의도치않은 side-effect까지 발생할 수 있습니다.</p>
<h3 id="gap의-한계점">gap의 한계점</h3>
<p>gap의 한계점은 이렇습니다. 설명드렸던 예제는 비교적으로 간단하지만, 저런 예제의 구조가 끝없이 반복되는 요구 사항을 받았을 경우 개발자는 난처해지기 마련입니다.</p>
<p>그렇다면 이런 문제를 어떻게 해결할 수 있을까요?</p>
<h2 id="spacer로-간격-벌리기">Spacer로 간격 벌리기</h2>
<p>어플리케이션을 개발하는 데 사용되는 프레임워크인 Flutter에는 SizedBox라는 위젯을 사용하여
간격을 벌리는 경우가 많습니다.</p>
<pre><code class="language-dart">Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
        ColoredRectangle(),
        SizedBox(height: 16.0,),
        ColoredRectangle(),
        SizedBox(height: 16.0,),
        ColoredRectangle(),
    ],
),</code></pre>
<p>어쩌면 CSS에서도 간격을 벌리는 빈 컴포넌트를 만들어 사용한다면,
여러 Element를 사용하지 않고도 깔끔하게 표현할 수 있지 않을까요?</p>
<pre><code class="language-jsx">&lt;ul&gt;
  &lt;li&gt;Item 1&lt;/li&gt;
  &lt;Spacer /&gt;
  &lt;li&gt;Item 2&lt;/li&gt;
  &lt;Spacer /&gt;
  &lt;li&gt;Item 3&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<h3 id="구현해-보기">구현해 보기</h3>
<p>저는 업무를 진행하면서 기존에 Spacer를 사용하여 간격을 벌리는 방식을 사용하고 있었는데,
이를 더 빠르고 명시적으로 사용하기 위해서 컴포넌트로 만들어보았습니다.</p>
<pre><code class="language-jsx">// Before
&lt;div className=&quot;h-[42px]&quot; /&gt;
// After 
&lt;Spacer h42 /&gt;</code></pre>
<p>자주 사용되는 컴포넌트인데도 불구하고, <code>div</code>로 표현된다는 점과 <code>h-[xx]</code>와 같이 표현해야 한다는 점이 불편했습니다.</p>
<p>그래서 컴포넌트를 만들 때도 <code>height=&#39;42px&#39;</code>과 같은 props로 전달하기보단 매우 빠르게 간격을 표현할 수 있도록 간소화해 보겠습니다.</p>
<pre><code class="language-jsx">const App = () =&gt; {
  return (
    &lt;div&gt;
      &lt;ul&gt;
        &lt;li&gt;Item 1&lt;/li&gt;
        &lt;Spacer h20 w120 /&gt;
        &lt;li&gt;Item 2&lt;/li&gt;
        &lt;Spacer h20 /&gt;
        &lt;li&gt;Item 3&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  );
};</code></pre>
<pre><code class="language-jsx">const Spacer = ({ ...props }) =&gt; {
  const space = Object.keys(props)
    .map((key) =&gt; ({ [key[0]]: `${key.slice(1, key.length)}px` }))
    .reduce((acc, obj) =&gt; ({ ...acc, ...obj }), {});

  return (
    &lt;div
      style={{
        width: space.w ?? 0,
        height: space.h ?? 0,
      }}
    /&gt;
  );
};</code></pre>
<p>개발자의 수월함을 위해 props를 skip하고 Spacer에 이름 자체를 전달함으로 간격을 벌릴 수 있게 개발했습니다.</p>
<p>언뜻 보면 위험해 보이지만, 결국 style에서 space 객체의 w와 h를 호출하기에
규약에 맞지 않는 props가 들어올 경우 side-effect를 발생시키지 않습니다.</p>
<p>가로 간격과 세로 간격을 결정하는 것을 <code>w</code>, <code>h</code> 접두사로 표현하고,
그 뒤 숫자를 바로 px로 적용할 수 있도록 합니다.</p>
<pre><code class="language-jsx">&lt;Spacer h20 w120 /&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/986376f9-8f6d-4a36-b20f-3b14390d92fc/image.png" alt=""></p>
<p>원하던대로 잘 작동하네요!</p>
<h3 id="javascript-props-safety">JavaScript Props Safety</h3>
<p>JavaScript에서 h나 w와 같은 규약을 어긴채로 props를 전달했을 경우, 개발자가 이를 발견할 수 있도록 하는 safety를 적용해 보겠습니다.</p>
<pre><code class="language-jsx">const space = Object.keys(props)
  .map((key) =&gt; {
    const [target, pixel] = [key[0], `${key.slice(1)}px`];
    if (![&#39;w&#39;, &#39;h&#39;].includes(target)) throw new Error(&#39;Spacer 인자를 잘못 전달하셨습니다.&#39;);
    return { [target]: pixel };
  })
  .reduce((acc, obj) =&gt; ({ ...acc, ...obj }), {});</code></pre>
<p>위와 같이 props를 변환할 때 확인 후, 규약에 알맞지 않을 경우 Error를 throw하는 식으로 예외를 처리할 수 있습니다!</p>
<h3 id="typescript-props-safety">TypeScript Props Safety</h3>
<p>TypeScript에서는 props에 TypeGuard를 지정하여 런타임까지 가지 않고, 린트 단에서 사용자의 실수를 막을 수 있습니다.</p>
<pre><code class="language-tsx">type SpacerTypeGuard = Record&lt;`${&#39;w&#39; | &#39;h&#39;}${number}`, boolean&gt;;

const Spacer = ({ ...props }: SpacerTypeGuard) =&gt; { ... }</code></pre>
<h2 id="결론">결론</h2>
<p>웬만한 상황에서는 gap을 사용하여 간격을 표현할 수 있지만,
어떤 목록에 한하여 각각의 gap이 다른 요구 사항에서는 Spacer를 사용하여 간격을 표현할 수 있습니다.</p>
<p>gap의 한계로 인해 HTML Element가 차곡차곡 쌓여 고민 중이시라면,
Spacer를 도입해 요구 사항을 해결해 보시는 것을 추천드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[완벽한 input[type="password"]를 만드는 방법]]></title>
            <link>https://velog.io/@ubin_ing/how-to-make-perfect-password-input</link>
            <guid>https://velog.io/@ubin_ing/how-to-make-perfect-password-input</guid>
            <pubDate>Sun, 24 Nov 2024 06:10:43 GMT</pubDate>
            <description><![CDATA[<p>브라우저에서 비밀번호 타입의 input을 렌더링할 때, PC와 모바일의 차이점이 있다는 사실 알고 계셨나요?</p>
<table>
<thead>
<tr>
<th>모바일</th>
<th>PC</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/f2ce706d-7740-4962-885f-ea2088fc809b/image.gif" alt=""></td>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/f5678d0d-bf78-4d4c-96cf-e7809a05a3b8/image.gif" alt=""></td>
</tr>
</tbody></table>
<p>모바일에서는 type이 password인데도 방금 입력한 글자를 몇 초간 보여주는 기능이 있습니다.
그런데 어플리케이션의 요구 사항에 따라서 PC나 모바일을 불문하고 
아예 글자를 보여주지 않게 해야 하는 경우가 생기면, 어떻게 대처해야 할까요?</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>React를 기반으로 구성된 어플리케이션을 중점으로 설명합니다.</li>
<li>state를 활용해 패스워드 타입의 input의 value를 완벽히 가리는 방법을 설명합니다.</li>
</ul>
</blockquote>
<h3 id="단방향-바인딩을-이용하기">단방향 바인딩을 이용하기</h3>
<p>다른 라이브러리나 프레임워크와는 다르게 React에서는 state와 view가 단방향으로 바인딩이 되어 있습니다.</p>
<p>그렇기에 state를 view에 연결하려면 onChange와 value에 state를 모두 연결해 주어야
입력값을 완벽히 받아 state에 저장할 수 있습니다.</p>
<pre><code class="language-jsx">const [content, setContent] = useState(&quot;&quot;);

&lt;input 
  onChange={(e) =&gt; setContent(e.target.value)}
  value={content}
/&gt;</code></pre>
<p>그렇다면 onChange와 value가 각각 다른 state를 바라보고 있다면,
입력받는 값을 따로 저장해 두고, 사용자에게 보여주는 값은 원하는대로 보여줄 수 있지 않을까요?</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/258eccbd-8a51-4100-8599-eed342d98bdd/image.png" alt=""></p>
<p>위 사진처럼 onChange가 트리거될 때의 입력값을 기반으로 content와 render content를 관리하고,
input의 value에는 render content를 렌더링하는 식으로 관리해보겠습니다.</p>
<h3 id="하나의-input을-두-개의-state로-관리하기">하나의 input을 두 개의 state로 관리하기</h3>
<p>먼저 저장할 값과 보여줄 값, 총 두 state를 정의해주겠습니다.</p>
<pre><code class="language-jsx">const [content, setContent] = useState(&quot;&quot;);
const [renderContent, setRenderContent] = useState(&quot;&quot;);</code></pre>
<p>이번 글에서의 목적은 값 대신 password에서 사용되는 점을 보여주어야 하기 때문에,
renderContent는 점으로 관리해 보겠습니다.</p>
<pre><code class="language-jsx">const handleContentChange = (e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const typedValue = e.target.value;
    setRenderContent(&quot;•&quot;.repeat(typedValue.length));

    if ((e.nativeEvent as { data?: string }).data === null) {
      setContent((text) =&gt; `${text.slice(0, text.length - 1)}`);
    } else {
      setContent((text) =&gt; `${text}${typedValue[typedValue.length - 1]}`);
    }
};</code></pre>
<p>동작 흐름을 설명 드리겠습니다.</p>
<blockquote>
<ol>
<li>입력값을 &#39;typedValue&#39;로 정의합니다.</li>
<li>렌더링할 content를 입력값의 length만큼 패스워드 점으로 채워줍니다.</li>
<li>사용자가 BackSpace를 입력할 경우 nativeEvent의 data는 null입니다.
BackSpace인 경우 입력값 끝의 한 글자를 지웁니다.
BackSpace가 아닌 값일 경우 마지막으로 입력한 값을 추가합니다.</li>
</ol>
</blockquote>
<p>이런 식으로 state를 구성한 후 렌더링 단에서 renderContent를 보여주면 문제는 해결됩니다.</p>
<pre><code class="language-jsx">&lt;input
  onChange={handleContentChange}
  value={renderContent}
  type=&quot;input&quot;
  maxLength={7}
  pattern=&quot;[0-9]*&quot;
/&gt;</code></pre>
<table>
<thead>
<tr>
<th>view</th>
<th>content</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/2da409b6-3c41-4e60-9c3a-2f28151a1be0/image.gif" alt=""></td>
<td><img src="https://velog.velcdn.com/images/ubin_ing/post/553d9d40-0cd4-49ee-9d18-e08cdbc42599/image.gif" alt=""></td>
</tr>
</tbody></table>
<p>이제 브라우저의 환경과 관계없이 똑같은 UI를 노출시킬 수 있습니다.</p>
<h3 id="edge-case--selection을-이동한다면요">#Edge Case : selection을 이동한다면요?</h3>
<p>현재 로직에는 무조건 기존 입력값의 뒷부분을 기준으로 content 삭제나 추가를 제공합니다.</p>
<p>input 값에서 selection을 이동할 경우 값이 원하는 바와 다르게 저장되는 이슈가 발생합니다.
그렇기에 input에서 사용자가 selection하는 것을 막아, 끝에서부터 입력할 수 있게 함으로
발생하는 Edge Case를 처리할 수 있습니다.</p>
<pre><code class="language-jsx">&lt;input
  onKeyDown={(e) =&gt; {
    (e.target as HTMLInputElement).selectionStart = content.length;
    (e.target as HTMLInputElement).selectionEnd = content.length;
  }}
/&gt;</code></pre>
<p>selection 자체를 막지는 않고, 키보드를 눌렀을 때 바라보는 selection이 무조건 content의 끝이도록 세팅해 주었습니다.</p>
<h3 id="마무리">마무리</h3>
<p>HTML에서 input 태그는 특히나 개발자가 자유롭게 사용하기에 어느 정도 제약이 있는 element입니다.
해당 이슈를 겪었던 이유는 input이 제공하는 <code>&lt;input type=&quot;password&quot; /&gt;</code> 태그의 패스워드 점이 폰트에 비해 너무 작았기 때문입니다.</p>
<p>이 때문에 해당 패스워드 점을 키워주는 <code>pass</code>라는 폰트를 사용하려 했는데,
pass는 기본 시스템의 font를 override하고 있어 점은 커지더라도
다른 숫자와 폰트가 동일하지 않는다는 이슈가 있어 매우 난감했습니다.</p>
<p>그렇기에 해당 이슈를 기술적인 측면에서 state로 처리하여, 모바일에서도
똑같이 최근 입력한 숫자를 보여주지 않게 바꾸었습니다.</p>
<p>비슷한 요구 사항을 처리해야 하는 분들께 도움이 되고자 글을 남깁니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 19 아직도 정식 출시 안 되는 이유]]></title>
            <link>https://velog.io/@ubin_ing/why-they-can-t-release-react-19</link>
            <guid>https://velog.io/@ubin_ing/why-they-can-t-release-react-19</guid>
            <pubDate>Wed, 06 Nov 2024 12:13:52 GMT</pubDate>
            <description><![CDATA[<p>React19가 아직도 공식적으로 release되지 않은 데에는 많은 이유가 있겠지만,
그 중 하나는 Suspense로 인한 개발자들의 열띤 토론에서도 나온다고 느낍니다.</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>React19에서 Suspense가 어떻게 변경되는 지에 대해 이야기합니다.</li>
<li>해외 개발자들의 깃허브(순한맛)와 레딧(매운맛) 반응을 공유합니다.</li>
</ul>
</blockquote>
<h3 id="변경되는-suspense">변경되는 Suspense</h3>
<p>React19가 출시됨을 알린 4월 25일의 트윗에, Gabriel Valfridsson 씨가 답글을 남깁니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/3c15d4f2-0ccf-4e53-983a-069d1a618576/image.png" alt=""></p>
<blockquote>
<p>&quot;캬 ~~ 훌륭한 변경사항이 1톤이 넘는구만요!!!&quot;
&quot;근데 이건 좀 더 주의를 강조해야 할 것 같은데 ... <a href="https://github.com/facebook/react/pull/26380">링크</a>
&quot;리액트 쿼리같은 라이브러리와 Suspense를 사용하면 옛날엔 병렬 로딩이 됐었는데 이제는 워터폴이 생기네용? <a href="https://codesandbox.io/p/devbox/react18-pvf36j">링크 1</a> <a href="https://codesandbox.io/p/devbox/react19-g6n5f7?file=%2Fsrc%2FApp.tsx">링크 2</a></p>
</blockquote>
<p>예제를 축약해보면 다음과 같습니다.</p>
<pre><code class="language-jsx">import { useSuspenseQuery } from &quot;@tanstack/react-query&quot;;
import { Suspense, useState } from &quot;react&quot;;

export default function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
      &lt;Suspense fallback=&quot;Loading...&quot;&gt;
      &lt;Thing1 /&gt;
      &lt;Thing2 /&gt;
    &lt;/Suspense&gt;
  );
}

function Thing1() {
  const query = useSuspenseQuery({ queryKey: [&quot;thing1&quot;], ... });
  return &lt;div&gt;{query.data}&lt;/div&gt;;
}

function Thing2() {
  const query = useSuspenseQuery({ queryKey: [&quot;thing2&quot;], ... });
  return &lt;div&gt;{query.data}&lt;/div&gt;;
}</code></pre>
<p>React18에서의 Suspense를 다음과 같이 쓸 때는 병렬적으로 렌더링이 진행됐었습니다.
그래서 만약 Thing1과 Thing2의 suspense 작업이 둘 다 1초가 걸린다고 가정하면,
병렬적으로 처리되어 1초 후에 Loading fallback이 풀리고 결과물이 렌더링됩니다.</p>
<p>허나 React19에서는 동일한 가정에서 2초 후에 결과물이 렌더링된다는 것을 알 수 있습니다.
Thing1이 처리된 다음, Thing2가 처리되는 식으로 동작하기에 2초가 걸리게 되는 겁니다.</p>
<h3 id="변경되는-이유">변경되는 이유</h3>
<p>변경되는 이유는 기존의 Suspense가 어떻게 동작하는지와도 관련이 있습니다.
이유에 대해 리액트 팀의 결론부터 말하자면 불필요한 렌더링을 줄이기 위함입니다.</p>
<p>Suspense를 사용하면 데이터 로딩이 끝난 후 렌더링을 시작하는 게 아니라,
데이터 로딩을 시작하는 동시에 렌더링을 시작할 수 있게 됩니다.</p>
<pre><code class="language-jsx">export default function App() {
  return (
      &lt;Suspense fallback={&lt;p&gt;...&lt;/p&gt;}&gt;
      &lt;SuspendingComponent /&gt;
      &lt;ExpensiveComponent /&gt;
    &lt;/Suspense&gt;
  )
}</code></pre>
<p><a href="https://velog.io/@cnsrn1874/react-19-and-suspense-a-drama-in-3-acts#%EC%99%9C-%EA%B7%B8%EB%9E%AC%EC%9D%84%EA%B9%8C%EC%9A%94">예제 출처 : 이춘구님의 &quot;[번역] React 19와 Suspense - 3막극&quot; (감사합니다)</a></p>
<p>SuspendingComponent가 비동기 데이터를 로딩하는 컴포넌트이고,
ExpensiveComponent가 렌더링 비용이 큰 컴포넌트라고 가정해보겠습니다.</p>
<p>여기서 상황을 정리해보자면,</p>
<ol>
<li>SuspendingComponent가 비동기 데이터를 로딩하느라 suspend(일시정지)가 됩니다.</li>
<li>기존의 Suspense는 렌더링을 병렬적으로 처리하기 때문에, suspend가 된 동안에도 ExpensiveComponent의 렌더링이 준비됩니다.</li>
<li>ExpensiveComponent의 렌더링이 끝납니다.</li>
<li>SuspendingComponent의 suspend가 아직 끝나지 않았기에, Suspense로 감싼 블록 전체에 한해 fallback UI를 렌더링시킵니다.</li>
<li>fallback UI로 인해 ExpensiveComponent가 병렬적으로, 사전에 렌더링되었는데도 사용되지 않습니다.</li>
<li>SuspensiveComponent의 suspend가 끝납니다.</li>
<li>fallback UI가 사라지면 ExpensiveComponent는 리렌더링을 진행합니다.</li>
</ol>
<p>이 과정에서 suspend되는 요소로 인해 사전에 필요 없는 렌더링이 발생해서,
리액트 팀은 이를 오버헤드로 판단하고 React19에서는 워터폴을 만들기로 결정한 것입니다.</p>
<h3 id="반발하는-이유">반발하는 이유</h3>
<p>개발자들이 반발하는 이유는 일단 첫 번째로 모든 어플리케이션이 위와 같은 상황을 거치지 않습니다.
그리고 워터폴로 바꾸면 렌더링이 너무 느려지게 됩니다.</p>
<p>2초의 suspend를 요구하는 20개의 컴포넌트가 하나의 Suspense내에 있다고 하면,
원래 2초가 걸릴 렌더링이 사전 렌더링 불필요하다고 2*20 -&gt; 40초가 걸리게 됩니다.</p>
<p>리액트 팀의 입장은 어떨까요?</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/b7ad4989-020b-4f59-9263-65b2bf3a01e4/image.png" alt="">
<del>압도적인 싫어요 수...</del> 요약하자면</p>
<blockquote>
<p>&quot;애초에 Suspense lazy loading 하라고 만든거라 fetch랑 쓰는 건 부적합한데용?&quot;
&quot;Suspense는 이제 라우터 단에서 prerender할 때만 쓰세여.&quot;
&quot;Suspense로 fetch 시작하지 말고 그냥 리소스를 소비하는 용도로만 쓰시고여.&quot;</p>
</blockquote>
<p>이런 이유에서 개발자들이 여러 discussion이나 커뮤니티에서 열띤 토론을 펼치고 있습니다.
어쩌면 React19가 아직도 정식 출시되지 않은 이유는 이 때문일 지도 모르겠네요...</p>
<h3 id="github-comment-반응-요약-순한맛">Github Comment 반응 요약 (순한맛)</h3>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/58ff4b13-ac6b-4ef1-a36a-240eb1f42ebd/image.png" alt=""></p>
<blockquote>
<p>&quot;제가 데이터를 가져오는 가장 좋아하는 방법은 데이터를 사용하는 곳에 가까이 두는 방법이에요.&quot; (캡슐화한 각 컴포넌트에서의 데이터 fetch를 이야기하시는 듯함)
&quot;Suspense를 변경하기 전까지는 가능했어요. 이렇게 되면 각 컴포넌트마다 하나씩, 총 20개나 있는 fetch를 전부 다 컴포넌트의 최상위로 끌어올려서 써야되잖아여...&quot;
&quot;님들 React.lazy 때문에 &#39;리액트&#39;를 이루는 성배가 깨진듯 함...&quot;
&quot;워터폴로 렌더링해야 하는 개발자가 있다고 생각하면 일단 Suspense 옛날처럼 되돌려주고 선택 가능한 옵션처럼 만들어주세용...&quot;</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/d7b42469-c430-452d-9a30-b4585d2c87a6/image.png" alt=""></p>
<blockquote>
<p>&quot;아니 당신네들 이거보고도 계속 그 이야기할 수 있는거요?&quot;
&quot;Suspense에 의존하는 React Query랑 더불어 Three.js는 본질적으로 비동기로 움직이니까 이런 플랫폼 전부에 영향을 미치잖소&quot;</p>
</blockquote>
<pre><code class="language-jsx">&lt;Center&gt;
  &lt;AsyncThingA /&gt;
  &lt;AsyncThingB /&gt;
&lt;/Center&gt;</code></pre>
<blockquote>
<p>&quot;위 코드에서 Center는 AsyncThingA와 AsyncThingB가 완료되었을 때를 확인 후 useLayoutEffect에서 그에 따른 처리를 할 수 있었는데, React19에서는 워터폴로 만들어버리면 TTL이 증가하잖아...&quot;
&quot;그럼 이제 Center를 동적이거나 조건적으로 작동시킬 수 없고 만약에 이게 라이브러리라서 고치지도 못하면 해결할 방법이 읎잖소!&quot;</p>
</blockquote>
<pre><code class="language-jsx">&lt;Suspense revealOrder={&quot;forwards&quot; | &quot;backwards&quot; | &quot;together&quot;} fallback={...}&gt;
  &lt;Foo /&gt;
  &lt;Bar /&gt;
&lt;/Suspense&gt;</code></pre>
<blockquote>
<p>&quot;그렇게 바꾸고 싶으면 차라리 이런 opt-in 넣어서 선택할 수 있게 하든가 하쇼&quot;</p>
</blockquote>
<p>필자의 주관적인 생각으로는 Suspense를 바꾼다면 선택할 수 있는 opt-in으로 바꾸면 
호불호가 갈리지 않을텐데, 상황을 조금 더 지켜보아야 할 필요가 있는 듯 합니다.</p>
<h3 id="reddit-반응-요약-매운맛">Reddit 반응 요약 (매운맛)</h3>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/86b7c147-d885-449c-9366-4d51a780cf27/image.png" alt=""></p>
<blockquote>
<p>&quot;React가 이상한 점은 이런 상황에서 opt-in같은걸 안넣는다는 거임.. 핵심적인 기능을 조용하게 바꾸는 건 미친 짓 아님?&quot;</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/3a2d86ad-f327-4ac3-a9d3-301455e6b12a/image.png" alt=""></p>
<blockquote>
<p>&quot;어 Vercel이 React가 Next에 의존하게 만들어서 Next 많이 쓰게 하고 돈 뜯어 묵을라고 그럼 ㅋㅋ&quot;</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/3d1ee91b-3789-41b7-b65d-d1a89dc8438b/image.png" alt=""></p>
<blockquote>
<p>&quot;좀 지나친 발언이라고 말할려고 했는데... Suspense를 Vercel이 contribution했네요? ㅋㅋㅋ&quot;</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/2aac105e-4a3e-4f27-836d-cb3c6e224102/image.png" alt=""></p>
<blockquote>
<p>&quot;버셀이 PR 올리고 자기가 approve했어요 &#39;놀 랍 다!&#39;&quot;
&quot;기여하려면 최소 다른 두 개의 조직이 PR을 approve해야 한다고 생각해요.</p>
</blockquote>
<h3 id="마무리">마무리</h3>
<p>필자 또한 어떤 이유로 인해 Suspense의 동작이 극단적으로 변경되는지는 아직 잘 모르고 있으나,
이런 기능들이 선택할 수 있는 opt-in으로 출시되면 많은 개발자들의 환호를 받을 수 있을 것이라고 생각합니다.</p>
<p>특히나 Suspense를 lazy loading의 관점으로 변경한 contributor가 prerender를 권유하고 있는 Next.js(Vercel)라면, 레딧에서 개발자들이 조금 과격하게 추측할 여지가 있을 것 같기도 합니다.</p>
<p>React19가 정식적으로 출시되지 않는 상태에서 Next15가 정식 출시된 것도 이상하긴 한데,
경과를 지켜보아야 할 것 같습니다.</p>
<p>저도 19에서 출시되는 use나 useOptimistic을 보고 환호했는데, 
이런 이슈가 있었다는 것은 최근에 알게 된 사실이네요.</p>
<p>React19를 쓰신다면 꼭 변경되는 Suspense의 차이점을 숙지하고 
사용하시는 것을 추천드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 신규 기능인 Parallel Routes와 Intercepting Routes로 인스타그램 뺨치는 넥스트그램 만들어보기]]></title>
            <link>https://velog.io/@ubin_ing/parallel-routes-and-intercepting-routes</link>
            <guid>https://velog.io/@ubin_ing/parallel-routes-and-intercepting-routes</guid>
            <pubDate>Tue, 22 Oct 2024 15:57:21 GMT</pubDate>
            <description><![CDATA[<p>PC 인스타그램에는 하나의 신기한 기능이 존재합니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/b3c67627-40a9-498b-8aa8-066529bbb608/image.gif" alt="">
인스타 프로필에서 게시물을 클릭하면 레이아웃이 팝업처럼 뜨지만,
새로고침하면 하나의 사이트로 레이아웃이 바뀝니다.</p>
<p><a href="https://www.instagram.com/wonhee_illit/">직접 해보기</a></p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/9f4329fb-1864-4ec1-85cc-375d14066c22/image.png" alt=""></p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
<ul>
<li>Next.js의 Parallel Routes에 대해 소개합니다.</li>
<li>Next.js의 Intercepting Routes에 대해 소개합니다.</li>
<li>두 라우터를 사용하여 만든 인스타그램 게시물 라우팅을 구현합니다.</li>
</ul>
</blockquote>
<p>*아 TMI로 예제 사진 만든다고 그냥 생각나는 인스타 프로필 들어가서 찍었습니다</p>
<h1 id="parallel-routes">Parallel Routes</h1>
<p>Parallel routes는 nextjs app router에서 최근에 출시된 기능입니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/beae5799-6437-4cea-8a94-1547f8ba505b/image.png" alt="">
사진과 같이 app 라우터의 하위에 @ 키워드를 붙인 slot을 생성하면, 동일 레벨에 위치하는 하나의 레이아웃에서 동시에 표현할 수 있습니다.</p>
<p>사진에서 보시는 것처럼, 동일한 페이지 내에서 사용되는 컴포넌트를 slot을 생성해서 사용하면 이를 병렬로 렌더링할 수 있습니다.</p>
<pre><code class="language-jsx">// layout.tsx
export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    &lt;&gt;
      {children}
      {team}
      {analytics}
    &lt;/&gt;
  )
}</code></pre>
<h2 id="사용-예제">사용 예제</h2>
<h3 id="조건부-렌더링">조건부 렌더링</h3>
<p>Parallel Routes를 사용하면 사용자 역할과 같은 조건에 따라 라우트를 조건부로 렌더링할 수 있습니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/48a05c2f-03ae-44e0-8565-f5bf92d9ea07/image.png" alt=""></p>
<pre><code class="language-jsx">// layout.tsx
import { checkUserRole } from &#39;@/lib/auth&#39;

export default function Layout({
  user,
  admin,
}: {
  user: React.ReactNode
  admin: React.ReactNode
}) {
  const role = checkUserRole()
  return &lt;&gt;{role === &#39;admin&#39; ? admin : user}&lt;/&gt;
}</code></pre>
<h3 id="모달">모달</h3>
<p>우리가 만드려는 궁극적인 목표인데, Parallel Routes를 Intercepting Routes와 함께 사용하여 딥 링크를 지원하는 모달을 생성할 수 있습니다. 
<img src="https://velog.velcdn.com/images/ubin_ing/post/91c0167a-3471-4b0e-a510-2381c49f0181/image.png" alt="">
이건 추후에 Intercepting Routes를 소개드린 후 자세히 알아보겠습니다.</p>
<h3 id="active-state">Active State</h3>
<p>Parallel Routes에는 Active State가 존재합니다.
우리가 navigation을 어떻게 하느냐에 따라 두 가지의 active state로 나뉩니다.</p>
<h4 id="soft-navigation">Soft Navigation</h4>
<p>클라이언트 측을 탐색하며 부분 렌더링을 수행합니다. 새로고침이나 외부에서 접근하는 방식이 아니라, Next.js가 라우팅을 탐색할 수 있는 선(페이지 -&gt; 페이지)에서 slot을 읽습니다. 
soft navigation의 경우, 위에서 언급했던 slot을 먼저 읽어 렌더링합니다.</p>
<h5 id="hard-navigation">Hard Navigation</h5>
<p>전체 페이지를 로드하는 시점에서는 hard navigation이 수행됩니다. 이 상태에서는 Next.js는 slot을 읽을 수 없습니다.</p>
<p>hard navigation의 경우에는 default.tsx 파일을 렌더링하거나, 이 파일이 없는 경우 404를 렌더링합니다. Intercepting Routes와 함께 사용할 때에는 intercept가 취소되어 intercept하는 대상인 기존의 파일을 렌더링합니다.</p>
<h1 id="intercepting-routes">Intercepting Routes</h1>
<p>Intercepting routes는 현재 레이아웃 내에서 애플리케이션의 다른 부분의 라우트를 로드할 수 있는 기능입니다. 어떤 상태나 라우터의 변경 없이 콘텐츠를 표시하려는 경우에 유용합니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/43c67d2f-c7f9-4b5d-bd4b-d87c45c87e1e/image.png" alt="">
인스타그램 피드에서 사진을 클릭할 때, 사진을 피드 위에 모달로 오버레이하여 표시할 수 있도록 합니다. 이 경우에 Next.js는 <code>/photo/123</code> 라우트를 가로채고 URL을 마스킹하여 <code>/feed</code> 위에 오버레이합니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/baef7c36-de23-4cf0-956c-394fd1ca8733/image.png" alt=""></p>
<p>Intercepting Routes는 다음과 같이 설정할 수 있습니다. 인터셉트하려는 라우터의 depth에 따라 세그먼트명을 표기합니다.
<code>(.)</code> : 동일 레벨의 세그먼트를 매칭합니다.
<code>(..)</code> : 한 레벨 위의 세그먼트를 매칭합니다.
<code>(..)(..)</code> : 두 레벨 위의 세그먼트를 매칭합니다.
<code>(...)</code> : 루트 앱 디렉터리의 세그먼트를 매칭합니다.</p>
<p>*일반적인 파일 시스템과 비슷해보일 수 있어도 완전히 다르게 작동하는 컨벤션이기 때문에, 혼동할 수 있는 점에 주의하며 사용해야 합니다.</p>
<p>Intercepting Routes를 사용하려면 무조건 Parallel Routes와 같이 사용해야 합니다. 그렇지 않으면 원활하게 작동하지 않습니다. (사실 Intercepting Routes만 작동해야 하는 요구 사항을 찾아보기 또한 어렵습니다)</p>
<p>사실 설명 만으로는 이를 완벽히 이해하기 어려운 면이 있습니다.
이제 인스타그램의 피드와 똑같은 방식으로 이를 구현해보겠습니다.</p>
<h3 id="넥스트그램-만들기">넥스트그램 만들기</h3>
<p>*제가 창작한 예제가 아니라 Nextjs의 공식 예제를 세부적으로 나누어 담았습니다.
<a href="https://github.com/vercel/nextgram">깃허브 링크 보기</a></p>
<h4 id="패키지-구조">패키지 구조</h4>
<pre><code>├── app
│   ├── @modal                  # Parallel Routes
│   │   ├── (.)photos           # Intercepting Routes
│   │   │    ├──[id]
│   │   │    │    ├── modal.tsx
│   │   │    │    └── page.tsx
│   │   └── default.tsx
│   └── photos
│   │   └── [id]
│   │   │    └── page.tsx
...</code></pre><p><img src="https://velog.velcdn.com/images/ubin_ing/post/d4694a16-ae26-45d5-9032-63fb25dbff6e/image.png" alt=""></p>
<p>먼저 인스타그램 게시물을 리스트로 보여주는 프로필 페이지를 간단히 구조만 구현해보겠습니다.</p>
<pre><code class="language-jsx">// app/page.tsx
import Link from &#39;next/link&#39;;

export default function Page() {
  let photos = Array.from({ length: 6 }, (_, i) =&gt; i + 1);

  return (
    &lt;section&gt;
      {photos.map((id) =&gt; (
        &lt;Link key={id} href={`/photos/${id}`} passHref&gt;
          {id}
        &lt;/Link&gt;
      ))}
    &lt;/section&gt;
  );
}</code></pre>
<p>그 다음은 모달로 뜨는 밑의 페이지를 구현해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/9f34085c-ee50-41d4-a65e-361e13b8b2bb/image.png" alt=""></p>
<p>제일 먼저, 동일한 루트의 레이아웃에서 modal을 children과 같은 하나의 props로 받아 연결해주겠습니다.</p>
<pre><code class="language-tsx">// app/layout.tsx
import &#39;./global.css&#39;;

export const metadata = {
  title: &#39;NextGram&#39;,
  description:
    &#39;A sample Next.js app showing dynamic routing with modals as a route.&#39;,
};

export default function RootLayout(props: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    &lt;html&gt;
      &lt;body&gt;
        {props.children}
        {props.modal}
        &lt;div id=&quot;modal-root&quot; /&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<p>그리고 hard navigation을 진행했을 때 보여줄 페이지를 간단하게 만들어주겠습니다.</p>
<pre><code class="language-tsx">// app/photos/[id]/page.tsx
export const dynamicParams = false;

export function generateStaticParams() {
  let slugs = [&#39;1&#39;, &#39;2&#39;, &#39;3&#39;, &#39;4&#39;, &#39;5&#39;, &#39;6&#39;];
  return slugs.map((slug) =&gt; ({ id: slug }));
}

export default function PhotoPage({
  params: { id },
}: {
  params: { id: string };
}) {
  return &lt;div className=&quot;card&quot;&gt;{id}&lt;/div&gt;;
}</code></pre>
<p>이제 Parallel Routes와 Intercepting Routes를 함께 사용해보겠습니다.
Parallel Routes를 사용하여 피드를 클릭했을 때 게시물이 모달처럼 뜨게 하는 기능을 구현할 것입니다. 그 후, Intercepting Routes를 사용하여 soft navigation이 진행되었을 때에만 구현해둔 모달을 뜨게 하고, hard navigation인 경우에는 위에서 만든 세부 페이지를 렌더링하게 구현해보겠습니다.</p>
<pre><code class="language-tsx">// app/@modal/(.)photos/[id]/page.tsx

import { Modal } from &#39;./modal&#39;;

export default function PhotoModal({
  params: { id: photoId },
}: {
  params: { id: string };
}) {
  return &lt;Modal&gt;{photoId}&lt;/Modal&gt;;
}</code></pre>
<pre><code class="language-tsx">// app/@modal/(.)photos/[id]/modal.tsx

&#39;use client&#39;;

import { type ElementRef, useEffect, useRef } from &#39;react&#39;;
import { useRouter } from &#39;next/navigation&#39;;
import { createPortal } from &#39;react-dom&#39;;

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const dialogRef = useRef&lt;ElementRef&lt;&#39;dialog&#39;&gt;&gt;(null);

  useEffect(() =&gt; {
    if (!dialogRef.current?.open) {
      dialogRef.current?.showModal();
    }
  }, []);

  function onDismiss() {
    router.back();
  }

  return createPortal(
    &lt;div className=&quot;modal-backdrop&quot;&gt;
      &lt;dialog ref={dialogRef} className=&quot;modal&quot; onClose={onDismiss}&gt;
        {children}
        &lt;button onClick={onDismiss} className=&quot;close-button&quot; /&gt;
      &lt;/dialog&gt;
    &lt;/div&gt;,
    document.getElementById(&#39;modal-root&#39;)!
  );
}</code></pre>
<p>react-dom에서 제공하는 createPortal을 사용하여 구현합니다.</p>
<p>이렇게 Parallel Routes 하위에 Intercepting Routes를 구현해두면 다음과 같은 단계를 거칩니다.</p>
<pre><code>1. 사용자가 photos/A 라우터로 접근함
2. Soft Navigation의 경우, Next.js가 라우터를 Intercept해 
구현해둔 Intercepting Routes로 이동함
3. Intercepting Routes의 상위가 Parallel Routes로 구성되어 
있기 때문에 해당 로직이 트리거되어 모달이 보여짐</code></pre><p>이렇게 되면 Instagram과 동일한 UX를 사용자에게 제공할 수 있습니다.</p>
<p><a href="https://nextgram.vercel.app/">프리뷰 보기</a></p>
<h1 id="마무리">마무리</h1>
<p>Parallel Routes와 Intercepting Routes는 비교적으로 다소 생소한 부분이 있었는데, 이번에 자세히 알아보게 됨으로써 다음에 사용해야 할 요구사항이 있으면 꼭 한번 사용해보아야겠다고 생각했습니다. 글에서 설명드렸던 요구 사항과 비슷한 상황에 놓여 있으시다면, 이 방법을 사용해보시는 것을 추천드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[초비상] 드디어 React Query 문서 한글판 지원]]></title>
            <link>https://velog.io/@ubin_ing/react-query-ko</link>
            <guid>https://velog.io/@ubin_ing/react-query-ko</guid>
            <pubDate>Fri, 11 Oct 2024 12:04:51 GMT</pubDate>
            <description><![CDATA[<h3 id="introduce">Introduce</h3>
<p>React Query v5 문서를 비공식적으로 한글 번역하여 출시하였습니다. (어그로 죄송합니다)
10월 11일을 기준으로 Tanstack Query v5의 &#39;React&#39; 탭에 한하여, 현재 업데이트된
86개의 문서를 한글로 번역하였습니다.</p>
<p>React Query에 대해 딥다이브 하려거나, 이제 막 공부를 시작하시는 분들께 추천드립니다.</p>
<p><a href="http://react-query.kro.kr">사이트 바로가기 버튼</a>
<a href="https://github.com/ubinquitous/react-query-ko">깃허브 스타 눌러주기 버튼</a></p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/67fe4eea-f6b3-4ef5-8bc9-5340c599048e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/f3a80647-bba4-4d7b-a369-e8ffa95d6cbf/image.png" alt=""></p>
<h3 id="history">History</h3>
<p>React Query를 공부하다보니 Observer라는 기존에 몰랐던 개념에 대해 공부할 수 있었고,
이를 계기로 라이브러리 자체에 흥미가 생기기 시작했습니다.</p>
<p>그리고 주변에 어떤 기술을 공부하고는 싶은데, 아직 공식 문서를 읽는 습관이 들여져 있지 않아 
어려움을 느끼고 포기하는 사례를 보기도 했습니다.</p>
<p>그래서 프론트엔드를 입문하는 분들에게 도움 주고자 React Query를 공부할 겸 겸사겸사 
한글 번역을 진행했습니다.</p>
<h3 id="contributing">Contributing</h3>
<p>저도 아직 프론트엔드에 능숙하지 않은 주니어 개발자입니다.
아직 영어 원문을 정확하게 번역하지 못해 어색하거나, 잘못된 번역이 있을 수 있습니다.</p>
<p>언제든지 더욱 깔끔한 문서를 만드는 데 기여하고 싶으시다면,
커밋 컨벤션 또는 PR 컨벤션에 얽매이지 않고 자유롭게 기여하실 수 있습니다.</p>
<h3 id="ref">Ref.</h3>
<p>Next.js 커뮤니티 번역에 작게나마 참여한 적이 있는데, 해당 repo의 메인 테이너인 <a href="https://github.com/luciancah">luciancah</a>님의 
코드 스타일을 따라 제작하게 되었습니다. </p>
<p>nextjs에 관심이 있으시다면 꼭 <a href="https://github.com/luciancah/nextjs-ko/">Nextjs 한글 번역 사이트</a>도 읽어보시는 걸 추천드립니다.
저도 요즘 Nextjs 공식 문서를 읽을 때마다 아주 유용하게 사용하고 있습니다 너무 감사드립니다ㅜㅜ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Slate.js로 UI에 구애받지 않는 나만의 에디터를 만드는 방법]]></title>
            <link>https://velog.io/@ubin_ing/slate-js</link>
            <guid>https://velog.io/@ubin_ing/slate-js</guid>
            <pubDate>Mon, 30 Sep 2024 08:58:52 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드에서 에디터와 같은 기능을 구현할 때, 에디터에서 제공하는 UI가 서비스와 달라 
만족하지 못했던 경험이 있습니다.
그렇다고 에디터를 처음부터 끝까지 자체로 구현하기에도 부담스럽기도 합니다.</p>
<blockquote>
<p><strong>이 글에서는...</strong>
Slate.js 라이브러리를 사용하여 나만의 에디터를 만드는 방법을 소개합니다.
에디터를 &#39;컴포넌트&#39;가 아닌 &#39;기능&#39;으로 구성하는 방법을 공유드립니다.
*React(Next.js) 환경을 중심으로 설명합니다.</p>
</blockquote>
<h3 id="부분-스타일링을-어떻게-구현할-수-있을까">부분 스타일링을 어떻게 구현할 수 있을까</h3>
<p>네이버 에디터나 여러가지 시중의 에디터의 코어 로직을 뜯어보신 적이 있으신가요?
대부분의 에디터는 한 줄을 기준으로, 하나의 스타일된 텍스트마다 &quot;블록&quot;이라는 단위로
나누어 렌더링을 진행합니다.</p>
<p>평소에 사용하던 string이라는 구조와 달리, Array&lt;Block&gt;을 사용하는 거죠.
이를 테면...</p>
<p><strong>TEXTAREA</strong></p>
<pre><code>&quot;어제 유명한 OO 카페에 다녀왔어요.\n저는 거기서 아메리카노를 주문했습니다.&quot;</code></pre><p><strong>EDITOR</strong></p>
<pre><code class="language-js">[
  { type: &quot;단락&quot;, content: &quot;어제 유명한 OO 카페에 다녀왔어요.&quot; },
  { type: &quot;단락&quot;, content: &quot;저는 거기서 아메리카노를 주문했습니다.&quot; }
]</code></pre>
<p>아직은 조금 어색하실 수 있습니다. 
하지만 우리가 에디터를 만들 때 보통 어떤 기능을 제일 흔히 요구할까요?
특정 글자에 대한 볼드나 이태릭, 색상 변경 등의 부분 스타일링을 많이 필요로 합니다.
이런 상황에서 textarea는 string만 입력할 수 있기 때문에 바로 제약에 걸리게 됩니다.</p>
<p>하지만 editor의 경우는 다음과 같이 데이터를 표현할 수 있습니다.
&quot;유명한&quot;에 강조를 한다고 가정해보겠습니다.</p>
<pre><code class="language-js">[
  { 
    type: &quot;단락&quot;, 
    content: [
      { text: &quot;어제 &quot; },
      { text: &quot;유명한 &quot;, style: &quot;bold&quot; },
      { text: &quot;OO 카페에 다녀왔어요.&quot; }
      ]
  },
  { type: &quot;단락&quot;, content: &quot;저는 거기서 아메리카노를 주문했습니다.&quot; }
]</code></pre>
<p>이런 식으로 표현하면, Array.prototype.map 함수에서 렌더링 후, 
프로퍼티로 넘어오는 style만 catch해서 특정 부분에 대한 스타일링을 진행할 수 있게 됩니다!</p>
<p>Slate.js는 이 기능을 사용자가 직접 구현할 수 있도록 도와줍니다.
줄마다 블록을 구성하는 복잡한 로직들을 기본적으로 제공하고, 개발자가 원하는 곳을
위와 같이 자동으로 나누어 커스터마이징할 수 있도록 도와줍니다.
이에 대한 기능과 함수를 제공하고, 어떻게 렌더링하고 보여줄 지는 완벽한 개발자의 몫입니다.</p>
<p>다른 에디터와 다르게 &quot;빵&quot;이 아닌 &quot;오븐과 밀가루&quot;를 제공한다고 생각하시면 편합니다.</p>
<h3 id="slatejs로-커스텀-에디터-구현하기">Slate.js로 커스텀 에디터 구현하기</h3>
<p>요구 사항에 대해 이해했으니 바로 구현해보겠습니다.
먼저 Slate.js를 install합니다.</p>
<pre><code>$ yarn add slate slate-react</code></pre><p>Slate.js로 에디터를 구성하는 데에는 총 3가지의 핵심 로직이 있습니다.</p>
<p><strong>value</strong> : 에디터의 content를 배열로 관리하는 하나의 state입니다.
<strong>Transform</strong> : 에디터를 변경할 때 호출하는 기능을 구현하는 함수입니다.
<strong>renderLeaf</strong> : value의 프로퍼티가 가진 속성을 받아 렌더링하는 함수입니다.</p>
<p>먼저 에디터의 내용을 관리하는 state부터 정의하겠습니다.</p>
<pre><code class="language-jsx">/* initialValue를 주입합니다. useState(&quot;&quot;)와 동일합니다. */
const [content, setContent] = useState([
  {
    type: &quot;paragraph&quot;,
    children: [{ text: &quot;&quot; }]
  }
])</code></pre>
<p>그 다음, 에디터를 control하는 구현체 state를 만들어줍니다.
에디터 구현체가 변경될 사항은 없기 때문에 set함수는 할당하지 않습니다.</p>
<pre><code class="language-jsx">import { withReact } from &quot;slate-react&quot;;
import { createEditor } from &quot;slate&quot;;

const [editor] = useState(() =&gt; withReact(createEditor()));</code></pre>
<p>이제 렌더링 단에서 에디터를 렌더링하겠습니다.
렌더링은 총 두 개의 컴포넌트를 사용하여 렌더링합니다.</p>
<p><strong>Slate</strong> : 에디터의 control 범위를 지정하는 컴포넌트로, 렌더링에 영향을 미치지는 않습니다.
<strong>Editable</strong> : 에디터가 실제로 표시되는 컴포넌트입니다.</p>
<pre><code class="language-jsx">import { Editable, withReact } from &quot;slate-react&quot;;
...
&lt;Slate
  initialValue={content}
  editor={editor}
  onChange={(text) =&gt; setContent(text)}
&gt;
  &lt;Editable placeholder=&quot;내용을 입력하세요.&quot; /&gt;
&lt;/Slate&gt;</code></pre>
<p>Slate는 Editable의 직속 부모가 아니어도 됩니다. Slate 컴포넌트의 하위에
Editable 컴포넌트가 있기만 하면 되기에, 저는 사용할 때 Editor 컴포넌트의 루트에
Slate를 provide했습니다.</p>
<p>정말 간단하게 모든 세팅이 끝났습니다! 이제 각 텍스트를 스타일링하는 함수만 자유롭게 짜주면 됩니다.</p>
<h3 id="원하는-텍스트-스타일을-등록하기">원하는 텍스트 스타일을 등록하기</h3>
<p>커스텀 에디터에 대해서, 기존에 디자인된 디자인이나 여러가지 ...
&quot;만들어둔 어떤 버튼을 누르면 어떤 스타일이 되어야 해&quot;라고 잡아둔 컴포넌트가 있으실 겁니다.</p>
<p>해당 컴포넌트를 찾아 onClick 함수 하나만 만들어주면 구현이 끝납니다.</p>
<pre><code class="language-jsx">import { BaseEditor, Editor, Text, Transforms } from &quot;slate&quot;;
import { useSlate } from &quot;slate-react&quot;;

const ItalicButton = () =&gt; {
  /* 한 가지 주의점은, useSlate를 호출하는 depth가 위에서 언급드린
   * &lt;Slate /&gt; 컴포넌트의 내부여야 합니다. 그렇지 않으면 작동하지 않습니다.
   */
  const editor = useSlate();

  return (
    &lt;button
      onClick={() =&gt; {
        toggleItalicMark(editor);
      }}
      className=&quot;flex flex-col items-center justify-center w-[22px] h-full gap-1 hover:brightness-95 bg-white&quot;
    &gt;
      &lt;img src=&quot;/fontbox/1-2.png&quot; alt=&quot;italic&quot; className=&quot;w-auto h-[22px]&quot; /&gt;
    &lt;/button&gt;
  );
};

/* 스타일링 함수는 렌더링과 상호작용하지 않기 때문에 컴포넌트 밖으로 빼도 무관합니다. */

const toggleItalicMark = (editor: BaseEditor) =&gt; {
  const isActive = isItalicMarkActive(editor);
  Transforms.setNodes(
    editor,
    { italic: isActive ? null : true },
    { match: (content) =&gt; Text.isText(content), split: true }
  );
};

const isItalicMarkActive = (editor: BaseEditor) =&gt; {
  const [match] = Editor.nodes(editor, {
    match: (content) =&gt; content.italic === true,
    universal: true,
  });
  return !!match;
};</code></pre>
<p><strong>isItalicMarkActive</strong> : 특정 글자의 상호작용에 대한 프로퍼티의 변화를 제공합니다.
<strong>toggleItalicMark</strong> : isItalicMarkActive로 작용을 판별하여 content 프로퍼티를  변경시킵니다.</p>
<p>이 두 가지 함수를 정의하고, useSlate()로 받아온 editor 객체를 넣어주기만 하면 됩니다.
그럼 이제 어떤 특정 텍스트를 select한 후 이태릭 버튼을 누르면, content는 다음과 같이 바뀔 겁니다.</p>
<p>&quot;안녕하세요&quot;라는 문자에서 &quot;하세&quot;에만 italic을 준다고 가정해볼게요.</p>
<pre><code class="language-js">[
  {
    type: &quot;paragraph&quot;,
    children: [
      { text: &quot;안녕&quot; },
      { text: &quot;하세&quot;, italic: true },
      { text: &quot;요&quot; }
    ]
  }
]</code></pre>
<p>성공적으로 프로퍼티를 넣었으니, 이제 이를 스타일에 맞게 렌더링시켜주기만 하면 끝입니다!</p>
<h3 id="프로퍼티를-기준으로-엘리먼트를-렌더링-시키기">프로퍼티를 기준으로 엘리먼트를 렌더링 시키기</h3>
<p>프로퍼티를 기준으로 개발자가 원하는 스타일로 렌더링시켜주기 위해, Leaf라는 컴포넌트를
사용할 겁니다.</p>
<p>Leaf라는 컴포넌트를 통해서, 쉽게 프로퍼티를 핸들링하여 적용할 겁니다.</p>
<pre><code class="language-jsx">const Leaf = (props) =&gt; {
  return (
    &lt;span
      {...props.attributes}
      style={{
        fontStyle: props.leaf.italic ? &quot;italic&quot; : &quot;normal&quot;,
    &gt;
      {props.children}
    &lt;/span&gt;
  );
};

export default Leaf;</code></pre>
<p>이제 아까 정의했던 Editable 컴포넌트에 우리가 만든 Leaf를 등록해주면 끝입니다.</p>
<pre><code class="language-jsx">const renderLeaf = useCallback((props) =&gt; {
  return &lt;Leaf {...props} /&gt;;
}, []); 

...
&lt;Editable renderLeaf={renderLeaf} /&gt;</code></pre>
<p>이렇게 짧은 시간 안에, 복잡한 에디터의 코어 로직 없이 자유롭게 에디터를 구현할 수 있게 되었습니다.
추가적인 기능이 필요하면, Transform에서 핸들링하는 프로퍼티들의 이름을
<code>bold</code>, <code>underline</code>등 다양하게 바꾸면 되겠죠?</p>
<h3 id="마무리">마무리</h3>
<p>저도 에디터를 커스터마이징된 UI로 구현을 했어야 했는데, onInput과 같은
이벤트를 활용하여 처음부터 끝까지 구현하는 것은 너무 어렵고, 시중에 있는
라이브러리들은 제한적인 UI를 제공해서 고민이 많았습니다.</p>
<p>하지만 Slate.js를 통해 에디터를 &quot;컴포넌트&quot;가 아닌 &quot;기능&quot;으로 만들어 컨트롤할 수 있어
요구사항에 완벽하게 개발을 할 수 있었습니다.</p>
<p>커스터마이징이 필요한 UI의 에디터를 빠르게 적은 양의 코드로 개발하고 싶을 때,
Slate.js를 적극 추천드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[렌더링 관점에서 useEffect 이해하기]]></title>
            <link>https://velog.io/@ubin_ing/useEffect-in-rendering</link>
            <guid>https://velog.io/@ubin_ing/useEffect-in-rendering</guid>
            <pubDate>Fri, 06 Sep 2024 09:21:19 GMT</pubDate>
            <description><![CDATA[<p>평소 리액트를 다룰 때 습관적으로 useEffect를 사용하고, 가끔 무한루프가 발생하면
쩔쩔 매며 deps를 빼고 eslint warning을 disable했던 저에게 이 글을 바칩니다.</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
</blockquote>
<ul>
<li>렌더링 관점에서의 useEffect 동작 원리를 이야기합니다.</li>
<li>effect, cleanup이 언제 발생하는지 이야기합니다.</li>
<li>exhaustive-deps의 중요성에 대해 이야기합니다.</li>
<li>useEffect를 현명하게 사용하는 몇 가지 방법에 대해 이야기합니다.</li>
</ul>
<h2 id="렌더링-관점에서-useeffect-이해하기">렌더링 관점에서 useEffect 이해하기</h2>
<p>useState를 쓴다고 가정할 때, 보통 렌더링이 어떻게 발생한다고 이해하고 계신가요?
사실 우리가 사용하는 state는 변하지 않는 상수값으로 존재합니다.</p>
<h3 id="props-state는-고유한-값이다">props, state는 고유한 값이다</h3>
<p>JS에서 함수를 호출할 때 어떤 일이 생기나요?
함수 내에 있는 내용들이 다시 실행되는 식으로 작동합니다. 리액트 컴포넌트도 이와 똑같이 작용합니다.
그저 함수가 다시 호출될 경우(는 즉슨 &#39;컴포넌트가 렌더링된다&#39;)에 상태가 업데이트되어 변화되는 것처럼 보이는 것입니다.</p>
<pre><code class="language-js">// 처음 랜더링 시
function Counter() {
  const count = 0; // useState() 로부터 리턴
  // ...
  &lt;p&gt;You clicked {count} times&lt;/p&gt;;
  // ...
}

// 클릭하면 함수가 다시 호출된다
function Counter() {
  const count = 1; // useState() 로부터 리턴
  // ...
  &lt;p&gt;You clicked {count} times&lt;/p&gt;;
  // ...
}

// 또 한번 클릭하면, 다시 함수가 호출된다
function Counter() {
  const count = 2; // useState() 로부터 리턴
  // ...
  &lt;p&gt;You clicked {count} times&lt;/p&gt;;
  // ...
}</code></pre>
<p>우리는 보통 한 컴포넌트 내에서 state값이 바뀐다고 생각하지만,
알고보면 state값은 고유하지만, 렌더링 시점마다 해당 state가 변경되는 원리입니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/98267d38-987b-4e12-9ba9-49ca555ebf60/image.gif" alt=""></p>
<blockquote>
<p>렌더링은 마치 플립북 같습니다. 그림은 각 장마다 고유하게 존재합니다.</p>
</blockquote>
<h3 id="handler-또한-고유한-값이다">handler 또한 고유한 값이다.</h3>
<ol>
<li>카운터를 2로 증가시킨다</li>
<li>“Show Alert”를 클릭한다</li>
<li>타임아웃이 실행되기 전에 카운트를 4로 증가시킨다</li>
</ol>
<p>실행 결과를 맞춰보세요.</p>
<pre><code class="language-jsx">function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() =&gt; {
      alert(&quot;You clicked on: &quot; + count);
    }, 3000);
  }

  return (
    &lt;div&gt;
      &lt;p&gt;You clicked {count} times&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;Click me&lt;/button&gt;
      &lt;button onClick={handleAlertClick}&gt;Show alert&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>정답은 &quot;You clicked on: 2&quot; 입니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/eb048693-bc70-48f0-ada4-c416d48fe390/image.png" alt=""></p>
<blockquote>
<p>count = 2가 그려진 종이를 보고 이벤트를 트리거했기에,
추후 state가 변경된다 하더라도 출력 결과는 &#39;2&#39;가 됩니다</p>
</blockquote>
<p>해당 개념을 토대로 useEffect를 다시 이해하면, 실행 과정을 더 쉽게 이해할 수 있습니다.</p>
<h2 id="effect와-cleanup은-언제-실행될까">effect와 cleanup은 언제 실행될까?</h2>
<h3 id="effect">effect</h3>
<pre><code class="language-js">function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() =&gt; {
    document.title = `count: ${count}`;
  });

  return (
    &lt;div&gt;
      &lt;p&gt;You clicked {count} times&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;Click me&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>위 코드를 실행시켜보면 count값이 바뀔 때마다 useEffect가 실행되어 document의 title도 변경됩니다.</p>
<p>위 플로우를 순서대로 설명하면 다음과 같습니다.</p>
<ol>
<li>React가 컴포넌트에게 state가 0일 때의 UI를 요청한다</li>
<li>컴포넌트는 <code>You clicked 0 times</code>를 제공하고,  React에게 렌더링이 끝나면
<code>document.title = count: ${0}</code>을 호출할 것을 요청한다</li>
<li>React는 요청을 받고 브라우저에 UI 업데이트를 요청한다 (렌더링)</li>
<li>브라우저가 UI를 그린다 (페인팅)</li>
<li>React가 약속했던 document.title = count: ${0}를 실행한다.</li>
</ol>
<p>useEffect를 사용할 때 ‘DOM에 UI가 업데이트되기 전에 useEffect가 실행된다&#39;
라고 생각하는 경우가 많지만, 실제로는 UI가 그려진 후 effect가 실행됩니다.</p>
<p>저는 렌더링과 UI 업데이트가 같다고 생각했었는데, 이는 정답이 아닙니다.
리액트의 렌더링 및 UI 업데이트 플로우는 다음과 같습니다.</p>
<blockquote>
<p>업데이트 감지 → UI 업데이트 요청(렌더링) → 브라우저 페인팅 → effect</p>
</blockquote>
<p>NOTE : 
useLayoutEffect는 페인팅 이전에 effect가 실행됩니다.
하지만 동기로 작동하기 때문에, data fetching 등 비싼 비용의 작업을 layout effect할 경우
오랫동안 사용자가 빈 화면을 볼 수 있어 이를 신중하게 선택해야합니다.</p>
<h3 id="cleanup">cleanup</h3>
<p>cleanup은 effect가 destroy되고 다음 effect를 실행하기 전에 실행됩니다.</p>
<pre><code class="language-js">useEffect(() =&gt; {
  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
  return () =&gt; {
    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  };
});</code></pre>
<p>첫 번째 렌더링에서 id가 10, 두 번째에서 20이라고 가정할 경우 :</p>
<ol>
<li>React가 {id: 20}을 가지고 UI 업데이트를 요청(렌더링)함</li>
<li>브라우저가 UI를 그린다.</li>
<li>React는 {id: 10}에 대한 이펙트를 클린업한다.</li>
<li>React는 {id: 20}에 대한 이펙트를 실행한다.</li>
</ol>
<p>저는 이 또한 실제로는 UI를 그리기 전에 effect가 실행된다고 생각했는데,
페인팅이 진행된 다음 클린업이 실행되고 이펙트가 실행됩니다.</p>
<h3 id="결론">결론</h3>
<p>해당 실행 과정을 이해하신다면 훨씬 더 유용하고 쉽게 useEffect를 원하는 대로
사용하실 수 있을 겁니다.</p>
<h2 id="exhaustive-deps를-무시하지-마세요">exhaustive-deps를 무시하지 마세요</h2>
<p>useEffect에는 두 번째 파라미터에 의존성을 넣어줄 수가 있습니다.
하지만 내가 useEffect 내에서 어떠한 의존성을 사용하고 있음에도 불구하고
해당 의존성을 useEffect에게 명시해주지 않으면 warning을 띄웁니다.</p>
<p>이 warning이 단순 정적인 시스템상 React가 요구사항을 읽지 못해 발생한다고 생각하고
이를 eslitn-disable하거나 무시하는 경우가 많습니다.</p>
<p>하지만 해당 warning이 떴을 경우 내가 useEffect를 올바르게 사용하고 있나 고민해보아야 합니다.</p>
<p><strong>컴포넌트에 있는 모든 값 중 이펙트에 사용되는 값은 반드시 deps에 존재해야 합니다.</strong></p>
<p>그렇지 않다는건, useEffect가 필요하지 않은 곳에서 useEffect를 사용하고 있을 수도 있겠죠.
그렇다면 어떻게 이를 바꿀 수 있을까요?</p>
<h2 id="useeffect-현명하게-사용하기">useEffect 현명하게 사용하기</h2>
<h3 id="1-불필요한-useeffect는-걷어내기">1. 불필요한 useEffect는 걷어내기</h3>
<p>props나 state에 의존하는 state를 업데이트해야할 때</p>
<pre><code class="language-js">const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() =&gt; {
  setVisibleTodos(getFilteredTodos(todos, filter));
}, [props.todos, props.filter]);</code></pre>
<blockquote>
<p>props 변경 → 렌더링 → 페인팅 → props에 따른 effect 실행 → visibleTodos 렌더링</p>
</blockquote>
<p>가끔 어떠한 props나 state에 다른 state가 의존해있어서, 의존당하는 값이 변경되었을 때
useEffect에 해당 deps를 넣고 의존중인 state를 변경하는 경우가 있습니다.</p>
<p>이 경우, useState를 사용하지 않아도 됩니다.</p>
<pre><code class="language-js">const visibleTodos = getFilteredTodos(props.todos, props.filter);</code></pre>
<blockquote>
<p>props 변경 → props에 의존하는 상수가 다시 그려짐 → 렌더링 → 페인팅</p>
</blockquote>
<p>위에서 언급했던 것처럼, props나 state가 변경되면 리액트는 &#39;새로운 그림&#39;을 그립니다.
그렇기에 &#39;새로운 그림&#39;에서 업데이트된 props나 state에 따라 의존하는 값이 상수여도
그림은 새로 그려지기에 불필요한 리렌더링 없이 업데이트가 진행되는 것입니다.</p>
<h3 id="2-하나의-useeffect는-하나의-기능만-완수하기">2. 하나의 useEffect는 하나의 기능만 완수하기</h3>
<p>useEffect도 우리가 useCallback, useMemo를 쓸 때처럼 사용해야 합니다.
하나의 useEffect에 여러가지의 deps를 넣어두고 여러가지 일을 실행시키게 하는 건
코드의 가독성을 떨어뜨릴 뿐만 아니라 불필요한 리렌더링을 불러일으키고 있을 수도 있습니다.</p>
<p>사용 중인 deps에서 처리하는 일이 분리 가능한 경우, useEffect를 분리하는 걸 추천드립니다.
useEffect를 분리하고 나면 1번의 경우를 찾을 수도 있습니다. (제가 리팩토링할 때 그랬습니다)</p>
<h3 id="3-사용하고-있는-deps에-대해서만-명시하기">3. 사용하고 있는 deps에 대해서만 명시하기</h3>
<pre><code class="language-js">useEffect(() =&gt; {
  const [recentHistory] = historyList.filter((history) =&gt; ...);
  if(recentHistory ...)
}, [historyList])</code></pre>
<p>실제로 제가 짰던 코드인데, 이 useEffect에서는 historyList를 필터링한
recentHistory 값에만 의존합니다.</p>
<p>하지만 실제로는 historyList에 의존하고, historyList를 내부에서 파싱해서 사용합니다.</p>
<pre><code class="language-js">const [lastHistory] = historyList.filter((history) =&gt; ...);
useEffect(()=&gt; {
  if(lastHistory ...)
}, [lastHistory])</code></pre>
<p>저는 useEffect 외부로 해당 변수를 걷어내서 해결하였습니다.</p>
<h3 id="4-복잡한-로직을-트리거하는-useeffect에-이름-달아주기">4. 복잡한 로직을 트리거하는 useEffect에 이름 달아주기</h3>
<p>가끔 useEffect가 여러 개 쓰이거나, 함수 내용이 많은 경우 어떤 기능을 하는
effect인지 쉽게 알기 어렵습니다. 이럴 때 제공해주는 함수에 이름을 달아주면
effect의 기능을 훨씬 쉽게 확인할 수 있습니다.</p>
<pre><code class="language-jsx">const router = useRouter();

useEffect(
  function 사업자등록번호조회() {
    if (!lastHistory) return;
    const is사업자 = ...
    if (is사업자) {
      router.replace( ... )
    }
  },
  [recentHistory],
);</code></pre>
<h3 id="5-third-party에서의-exhaustive-deps-해결책-탐색하기">5. third-party에서의 exhaustive deps 해결책 탐색하기</h3>
<p>저는 바로 위 코드처럼 next/router의 router를 사용하고 있었는데, 이렇게 사용하니
useEffect deps에 router를 넣으라고 하더군요. 하지만 넣으면 무한루프가 발생합니다.</p>
<p>제 생각에는 컴포넌트가 리렌더링될 때마다 <code>const router = useRouter()</code>로 선언한
함수의 참조값이 계속 바뀌고 있다고 추측했습니다.</p>
<p>검색해보니 useRouter를 싱글톤으로 선언한 <code>Router</code>가 있어서 해당 모듈을 사용했습니다.
싱글톤 패턴으로 생성된 클래스이기에 렌더링에 따른 참조값이 변경될 일이 없어 
useEffect 내에서 해당 클래스를 호출해도 exhaustive-deps warning이 발생하지 않게 됩니다.</p>
<pre><code class="language-jsx">import Router from &quot;next/router&quot;;

useEffect(()=&gt; {
  Router.replace( ... )
}, [recentHistory])</code></pre>
<h2 id="마무리">마무리</h2>
<p>렌더링 원리에 대해 이해하고 그 관점에서 useEffect를 바라보니 추후 사용할 때도
조금 더 원하는 방향대로 useEffect를 사용할 수 있겠구나 싶었습니다.</p>
<p>또한 제가 너무 습관적으로 useEffect를 사용하고 있는지 다시 돌아보는 계기가 되었습니다.
만약 useEffect를 사용할 때 exhaustive-deps warn이 발생하면, 무시하지 말고
한번 쯤 다시 고민해보는 시간을 가지시는 것을 추천드립니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://rinae.dev/posts/a-complete-guide-to-useeffect-ko/">useEffect 완벽 가이드</a>
<a href="https://velog.io/@jay/you-might-need-useEffect-diet#%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C%EC%9A%94">useEffect 잘못 쓰고 계신겁니다.</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Server Component에서 React Query 사용하기]]></title>
            <link>https://velog.io/@ubin_ing/react-query-in-server-components</link>
            <guid>https://velog.io/@ubin_ing/react-query-in-server-components</guid>
            <pubDate>Thu, 22 Aug 2024 11:24:44 GMT</pubDate>
            <description><![CDATA[<p>Next.js 환경의 Server Component(이하 RSC)에서 React Query를 서버 사이드로 사용하는 방법에 대해 서술하려합니다.</p>
<blockquote>
<p><strong>이 글에서는...</strong></p>
</blockquote>
<ul>
<li>RSC에서 React Query를 <strong>왜</strong> 사용하는지 설명합니다.</li>
<li>hydration과 serialization, deserialization에 대해 설명합니다.</li>
<li>query가 <code>Server → Client</code>로 이동하는 구조와 로직(구현 코드)에 대해 설명합니다.</li>
</ul>
<h2 id="rsc에서-react-query가-왜-필요한가">RSC에서 React Query가 왜 필요한가?</h2>
<p>Next.js 환경에서 React Query를 사용하면서 이런 의문이 들 수 있습니다.
이와 관련된 의문에 대해서 하나하나씩 설명해보겠습니다.</p>
<h3 id="nextjs에서-캐싱을-하는데-react-query도-캐싱을-하는게-의미가-있는가">Next.js에서 캐싱을 하는데 React Query도 캐싱을 하는게 의미가 있는가?</h3>
<p>Next.js도 캐싱을 해주지만, React Query가 캐싱하는 목적과 관리 방식 모두 다릅니다.</p>
<table>
<thead>
<tr>
<th>특성/기술</th>
<th>Next.js</th>
<th>React Query</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td>서버 사이드에서 발생하는 다중 페칭 효율화</td>
<td>클라이언트 사이드에서 발생하는 다중 페칭 효율화</td>
</tr>
<tr>
<td>관리 방식</td>
<td>엔드포인트와 option을 기준으로 fetch 상태 관리</td>
<td>queryKey로 fetch 상태 관리</td>
</tr>
</tbody></table>
<p>Next.js에서는 서버 사이드와 관련된 다중 페칭을 효율화시키는 캐싱을 합니다.
<code>100개의 동일한 RSC가 똑같은 요청을 보내는 경우</code> 해당 캐싱이 매우 유용할 수 있습니다.</p>
<h3 id="server-component에서-react-query를-왜-쓰는가">Server Component에서 React Query를 왜 쓰는가?</h3>
<p>다음과 같은 구조가 있다고 가정해보겠습니다.</p>
<pre><code class="language-tsx">const ServerComponent = async () =&gt; {
      await fetch(...);

    return &lt;ClientComponent /&gt;
}</code></pre>
<pre><code class="language-tsx">&quot;use client&quot;;

const ClientComponent = () =&gt; {
     const { data } = useQuery(...); 
}</code></pre>
<p>이런 식으로 구현하는 경우, 만약 React Query가 서버 사이드에 개입하지 않으면
<code>fetch</code>가 요청을 한 번, 클라이언트 사이드에서 <code>useQuery</code>가 또 요청을 한 번 보내기에
비효율적인 구조가 탄생하게 될 수 있습니다.</p>
<p>따라서 클라이언트 사이드에 존재하는 React Query와 서버 사이드의 fetching을 연결해줄 필요가 있습니다.</p>
<h3 id="서버-사이드에서는-fetch를-사용해도-되지-않나">서버 사이드에서는 fetch를 사용해도 되지 않나?</h3>
<p>그렇다면 서버 사이드에서 fetching 진행 후, 클라이언트 컴포넌트에 props로 값을 넘겨주는 등으로도
구성할 수 있는데, 왜 React Query가 필요할까요?</p>
<p>게시판 하나가 있다고 생각해보겠습니다.</p>
<ol>
<li>게시판 목록을 봅니다.</li>
<li>목록 중 하나의 글을 클릭해서 확인합니다.</li>
<li>다시 게시판 목록으로 돌아갑니다.</li>
</ol>
<p>해당 플로우 중 2 → 3으로 넘어갈 때에 React Query를 사용해서 fetch가 진행되어있다면
queryKey로 캐싱되어있을 것이기에 서버 사이드에 부가적인 요청을 보내지 않고도 페이지를
빠르게 제공할 수 있습니다.</p>
<h3 id="nextjs만-써도-충분하지-않나">Next.js만 써도 충분하지 않나?</h3>
<p>Next.js 13부터는 여러 기능을 제공하게 되며 Next.js만 써도 충분히 효율적인 개발이 가능해졌습니다.</p>
<p>하지만 무한 스크롤같은 유틸리티한 기능들을 제공하는 것,
그리고 React Query가 제공하는 데이터 플로우 등을 고려해보았을 때
기호에 맞게 React Query를 사용하는 것도 나쁘지 않다고 생각합니다.</p>
<h2 id="serialization과-deserialization">serialization과 deserialization</h2>
<blockquote>
<p>serialization이 뭔가요?</p>
</blockquote>
<p>라는 FE 리드 분의 질문에... 직렬화를 평탄화로 해석하여 답변했었습니다...</p>
<p>serialization(직렬화)은 데이터를 통신하는 과정에서 파싱할 수 있도록 변환하는 과정입니다.
TCP/IP를 공부해보셨다면 아시겠지만, 읽기 쉬운 데이터가 바로 상대방에게 적용되는 것이 아니라,
데이터 스트림, 패킷, 전파 등 데이터가 변환되어 전송되게 됩니다.
이를 직렬화라고 하며, 반대로 이렇게 변환된 데이터를 읽을 수 있게 변경하는 것이 역직렬화입니다.</p>
<p>조금 어색할 수 있지만 Next.js에서도 직렬화와 역직렬화 개념이 사용됩니다.
바로 우리가 친근한 hydration이 역직렬화 기법 중 하나라고 볼 수 있습니다.</p>
<blockquote>
<p><strong>hydration이 무엇인가요?</strong>
정적 HTML을 React Component로 변경해 동적인 요소로 바꾸어주는 작업입니다.</p>
</blockquote>
<p>서버 사이드에서 클라이언트 사이드로 데이터를 내려줄 때에도 직렬화가 필요합니다.
이를 dehydrate라고 표현합니다.</p>
<p>클라이언트 사이드에서 서버 사이드로 데이터를 받을 때에도 역직렬화가 필요합니다.
이를 hydration이라고 표현합니다.</p>
<p>바로 예제를 보며 사용해보겠습니다.</p>
<h2 id="구현해보기">구현해보기</h2>
<p>패키지 구조는 다음과 같습니다.</p>
<pre><code>project/
├── src/
│   ├── app/
│   │   └── post/
│   │       ├── page.tsx        # Server Component
│   │       └── PostClient.tsx  # Client Component
│   │      
│   └── getQueryClient.ts</code></pre><pre><code class="language-tsx">/* getQueryClient.ts */
import { QueryClient } from &quot;@tanstack/react-query&quot;;
import { cache } from &quot;react&quot;;

export const getQueryClient = cache(() =&gt; new QueryClient());</code></pre>
<p>React가 제공하는 cache를 사용하여 동일한 하나의 QueryClient를 사용하게 합니다.
(필수 사항이 아닙니다. 각 페이지에서 사용하실 때마다 new QueryClient를 해도 무관합니다.)</p>
<pre><code class="language-tsx">/* app/post/page.tsx */
import { dehydrate, HydrationBoundary } from &quot;@tanstack/react-query&quot;;
import getQueryClient from &quot;./getQueryClient&quot;;
import PostClient from &quot;./PostClient&quot;;

const CreatePostPage = async () =&gt; {
    const queryClient = getQueryClient();
      // 또는 const queryClient = new QueryClient();

      await queryClient.prefetchQuery({
        queryFn: fetchPost,
          queryKey: [&quot;post&quot;]
    });

      return (
        &lt;HydrationBoundary state={dehydrate(queryClient)}&gt;
              &lt;PostClient /&gt;
        &lt;/HydrationBoundary&gt;
    )
}</code></pre>
<pre><code class="language-tsx">/* app/post/PostClient.tsx */
&quot;use client&quot;;

import { useQuery } from &quot;@tanstack/react-query&quot;;

const PostClient = () =&gt; {
  const { data } = useQuery({ queryKey: [&quot;post&quot;] });

  return &lt;&gt; ... &lt;/&gt;
};</code></pre>
<ol>
<li>queryClient를 가져옵니다. (또는 생성합니다)</li>
<li>React Query의 prefetchQuery 메서드로 fetching을 진행합니다.
<code>prefetchQuery</code>는 따로 데이터를 반환하지 않으며, fetching된 데이터는 <code>queryClient</code>에 
자동으로 저장됩니다.
반환하는 데이터가 필요할 경우 <code>fetchQuery</code> 메서드를 사용할 수 있습니다.</li>
<li>HydrationBoundary를 Root에 감싸주고, state에 queryClient를 dehydrate(직렬화)시켜 전달합니다.
이렇게 되면 HydrationBoundary의 children 컴포넌트들은 모두 prefetch된 <code>post</code> 데이터를 사용할 수 있습니다.</li>
<li>Client Component에서 prefetch한 데이터의 queryKey만 호출하여 값을 불러와 사용합니다.</li>
</ol>
<p>hydration 과정은 개발자가 따로 지정하지 않고, dehydrate된 데이터를 받으면
자동으로 hydration이 됩니다.</p>
<p>&#39;de&#39;hydrate는 serialization, hydrate는 &#39;de&#39;serialization이라 헷갈릴 수 있습니다.</p>
<blockquote>
<p><strong>한 줄로 정리하면...</strong></p>
</blockquote>
<ul>
<li>serialization : 데이터를 보낼 때 진행하는 파싱, dehydrate. ex) JSON.stringify()</li>
<li>deserialization : 데이터를 받을 때 진행하는 파싱, hydrate. ex) JSON.parse()</li>
</ul>
<p>정상적으로 서버 사이드에서 data가 불러와지는지 확인해보고 싶으시다면 다음과 같이 확인하실 수 있습니다.</p>
<ol>
<li>개발자 도구의 Network → Fetch/XHR 창에서 새로고침을 해도 아무 요청도 뜨지 않지만
데이터는 잘 불러와지는 경우</li>
<li>개발자 도구의 오른쪽 상단 톱니바퀴 아이콘 → 쭉 내려서 Debugger → Disable JavaScript를
check한 후 새로고침을 해도 데이터는 잘 불러와지는 경우</li>
</ol>
<p>두 가지 테스트에서 모두 정상적으로 작동한다면 서버 사이드에서 성공적으로 데이터를 불러온다는 뜻입니다!</p>
<h2 id="마무리">마무리</h2>
<p>Next.js app router와 React Query v5 모두 출시된 지 오래된 라이브러리가 아니기에
공식 문서를 보며 꼼꼼히 공부해야 사용할 수 있었던 개념들을 정리하게 됐습니다.</p>
<p>Next.js와 React Query 모두 좋아하시는 분들, 그리고 어플리케이션이 특성상
서버 사이드 페칭을 진행해야 하는 경우 해당 방법을 추천드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nextjs app router, 정말 알고 사용하시나요?]]></title>
            <link>https://velog.io/@ubin_ing/nextjs-app-router</link>
            <guid>https://velog.io/@ubin_ing/nextjs-app-router</guid>
            <pubDate>Fri, 09 Aug 2024 07:20:48 GMT</pubDate>
            <description><![CDATA[<p>몇 가지 질문을 드리겠습니다.</p>
<blockquote>
<ul>
<li>app router가 출시된 이유는 무엇인가요?</li>
<li>app router에서 기본적으로 server component를 제공하는 이유는 무엇인가요?</li>
<li>app router에서는 SSG와 SSR을 어떻게 사용할 수 있나요?</li>
</ul>
</blockquote>
<blockquote>
<p><strong>해당 글은 위의 질문들을 중심으로 서술됩니다.</strong></p>
</blockquote>
<h2 id="app-router가-출시된-이유">app router가 출시된 이유</h2>
<p>2023년 5월, 평화롭게 pages router로 프로젝트를 개발하던 저는
새로운 라우팅 방식이 나왔다는 소식을 듣고 절망했습니다. 
좋은 소식이지만 그땐 또 공부해야되는게 너무 싫었거든용... 
(Next.js를 입문한지 3개월 째 되던 날이었습니다..)</p>
<p>그렇다면 Next.js가 app router를 출시한 이유는 무엇일까요?
저는...</p>
<blockquote>
<p><strong>Next.js를 오직 클라이언트 사이드가 아닌 풀스택 프레임워크로 발전시키기 위해서</strong></p>
</blockquote>
<p>라고 생각했습니다.
기존에는 &#39;React의 프레임워크&#39;로, 클라이언트 단에서 처리하기 복잡하거나 번거로운 일을
처리해주는 프레임워크로 자리잡았습니다.</p>
<p>하지만 웬걸... Next13부터 app router가 출시되면서 갑작스레 서버와 관련된 유틸들이
매우 많이 내장되기 시작했습니다.</p>
<p>간단하게 표로 정리해보았습니다.</p>
<table>
<thead>
<tr>
<th>기능 / 라우팅 유형</th>
<th>pages router</th>
<th>app router</th>
</tr>
</thead>
<tbody><tr>
<td>라우팅 경로</td>
<td>pages/post.tsx</td>
<td>app/post/page.tsx</td>
</tr>
<tr>
<td>라우팅 유형</td>
<td>기본적으로 클라이언트</td>
<td>기본적으로 서버</td>
</tr>
<tr>
<td>복잡성</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>성능</td>
<td>비교적 나쁨</td>
<td>좋음</td>
</tr>
<tr>
<td>유연성</td>
<td>비교적 나쁨</td>
<td>좋음</td>
</tr>
</tbody></table>
<p>라우팅 경로에서만 볼 때도, 원래는 pages 내에 바로 path 이름으로 파일을 만들면 해당 path가 라우팅 됐었습니다.</p>
<p><strong>app router로 바뀌면서는...</strong></p>
<ol>
<li>&#39;page&#39;로 명시를 해주어야 페이지가 라우팅이 됩니다.</li>
<li>아무것도 명시하지 않으면 기본적으로 서버 중심으로 작동합니다.</li>
<li>server component와 client component라는 개념을 두어 서버와 관련된 처리 또한
전문적으로 핸들링할 수 있게 도와줍니다.</li>
</ol>
<p>이런 단서들을 보면, Next.js의 app router는 서버 기능을 중심적으로 개발된 것이라고 추측할 수 있습니다.</p>
<p>이를 보면 두 번째 질문인, 기본적으로 서버 중심으로 세팅이 되어있는 이유가 무엇인지에 대해서도
궁금증이 자연스럽게 풀리게 됩니다.</p>
<p>그렇다면 바로 세 번째 질문을 server component와 client component와 함께 설명하겠습니다.</p>
<blockquote>
</blockquote>
<p>글의 가독성을 위해 다음과 같이 표시하겠습니다:
*server component -&gt; RSC (React Server Component)
*client component -&gt; RCC (React Client Component)</p>
<h2 id="app-router에서는-ssg와-ssr을-어떻게-사용할-수-있나요">app router에서는 SSG와 SSR을 어떻게 사용할 수 있나요?</h2>
<p>저도 app router에 대해 자세히 몰랐을 때 굉장히 많이 들었던 의문입니다.</p>
<blockquote>
<p>그렇다면, SSR == RSC 인건가요?</p>
</blockquote>
<p>어떻게 생각하시나요? 정답은 X입니다.
먼저 이 개념을 이해하기 위해, RSC와 RCC에 대해서 알아보겠습니다.</p>
<h3 id="rsc-vs-rcc">RSC vs RCC</h3>
<p>app router에서는 컴포넌트를 렌더링하려는 곳이 서버인지, 브라우저인지에 따라 이를 RSC, RCC로 나눕니다.
따라서 어떤 특정 컴포넌트가 월등히 좋다기 보단, 상황에 맞는 컴포넌트를 사용하는 것이 중요합니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/71afe7db-9021-4bce-899f-bec76cd07904/image.png" alt=""></p>
<p>RSC는 보통 데이터나 서버 리소스, 민감한 정보를 처리할 때 자주 사용됩니다.
RCC는 브라우저 단에서 사용자의 이벤트 등을 처리할 때 자주 사용됩니다.</p>
<p>pages router에서는 페이지 전체를 서버 사이드로 렌더링할지, 클라이언트 사이드로 렌더링할지만
정할 수 있었는데, 이를 컴포넌트 단위로 쪼갠다면 어떻게 작동하는 걸까요?</p>
<h3 id="rsc--rcc-동작-방식">RSC &amp; RCC 동작 방식</h3>
<p><small>(<a href="https://velog.io/@giyeon/Next.js-React-Server-Component">giyeon님의 글에서 더욱 자세히 확인하실 수 있습니다</a>)</small>
<img src="https://velog.velcdn.com/images/ubin_ing/post/1d93805a-369f-4203-87ae-f479ac32100f/image.png" alt="">
사용자가 Next 서버에 페이지를 요청하면, 서버는 위처럼 생성된 컴포넌트 트리를 직렬화된 json으로 재구성하는 작업을 진행합니다.</p>
<p>만약 RCC일 경우에는 컴포넌트를 해석하지 않고 &quot;RCC가 렌더링되는 위치입니다&quot; 하는 placeholder만을 배치해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/b03b2cb3-3b98-48f1-9346-4147654f8434/image.png" alt=""></p>
<p>도출된 결과물을 클라이언트가 서버에게 미리 전달받고, 모듈 타입이 RCC일 경우 이를 렌더링해 DOM에 반영하는 방식으로 작동합니다.
<img src="https://velog.velcdn.com/images/ubin_ing/post/801b04c9-183a-47ba-a780-0e69c59a21e4/image.png" alt=""></p>
<h3 id="잘못된-트리-구성">잘못된 트리 구성</h3>
<p>RSC에서 RCC를 자식 컴포넌트로 사용한다고 생각해보세요. 상상이 되시나요?
반대로, RCC에서 RSC를 자식 컴포넌트로 사용한다고 생각해보세요. 상상이 되시나요?</p>
<p>서버에서 클라이언트로 렌더링이 되는 건 쉽게 생각해볼 수 있지만,
그 반대는 플로우 자체가 조금 이상하다고 생각하실 수 있습니다.</p>
<p>만약 RCC -&gt; RSC로 구조를 작성할 경우, 부모인 RCC가 렌더링 되는 &#39;클라이언트 사이드&#39; 시점에서
RSC가 렌더링되는 것이기 때문에 RSC를 사용하는 이유가 상실되는거죠.</p>
<blockquote>
<p><strong>그럼 RCC 하위 RSC도 RCC로 바뀌는 건가요?</strong></p>
</blockquote>
<p>아닙니다. RSC는 그대로 서버 컴포넌트의 특성을 유지하지만, RSC의 장점이라고 할 수 있는
중요한 한 가지 특성이 상실된다고 보시면 됩니다.</p>
<blockquote>
<p><strong>서버 컴포넌트 (RSC):</strong></p>
<ul>
<li><del>서버에서 렌더링됨: 서버 컴포넌트는 서버에서 렌더링되어 클라이언트로 전달되는 HTML 생성</del></li>
<li>상태 관리 없음: 클라이언트 측 상태 관리와 상호작용 로직이 포함되지 않음</li>
<li>데이터 페칭: 서버에서 데이터를 로드하고 렌더링합니다.</li>
</ul>
</blockquote>
<p>RSC의 큰 장점 하나가 죽고 가는 것이죠. 그래서 실제로 이런 식으로 사용하면 build time에서
오류가 발생하고, 공식 문서에서도 이렇게 사용하지 말라고 권유합니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/ce774b98-5e83-444c-92bc-64298e84e0ad/image.png" alt="">nextjs 한국어 문서 번역 사랑합니다</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/37a69743-ad87-46bd-8c32-4b328d59a338/image.png" alt=""></p>
<p>오잉? 근데 신기하게도 children으로 서버 컴포넌트를 넘겨주면 문제가 없다고 합니다.
왜냐하면 RCC는 렌더링 단에서 children의 위치만 정해줄 뿐, children에 어떤 내용이
들어있는지는 정확히 알 수 없기 때문에 가능하다고 하는데요,</p>
<p>children으로 렌더링시키는 것과 관련된 글은
<a href="https://velog.io/@2ast/React-children-prop%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0feat.-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94">2ast님의 벨로그 글에서 더욱 자세히 확인하실 수 있습니다</a>.
존경합니다진짜</p>
<h3 id="rsc--rcc-결론">RSC &amp; RCC 결론</h3>
<p>이제 RSC와 RCC에 대해서 어느정도 알아보았습니다.
그렇다면 아까 드렸던 질문을 다시 해보겠습니다.</p>
<blockquote>
<p>RSC는 SSR인가요?</p>
</blockquote>
<h3 id="rsc와-ssr-ssg는-다르다">RSC와 SSR, SSG는 다르다</h3>
<p>정답은 전혀 아닙니다.</p>
<p>SSR은 페이지의 초기 렌더링을 서버에서 처리하고, 클라이언트에서 이를 받은 후
JS 코드를 덧붙여 상호 작용을 하는 식으로 작동합니다.</p>
<p>RSC는 서버에서 컴포넌트를 직렬화해 클라이언트로 이를 미리 전송하고,
그걸 클라이언트가 받아서 해석한 후 초기 렌더링을 진행하는 것이죠.</p>
<p>서버 단에서 실행된다는 개념 외에는 둘의 역할과 목적 자체가 아예 다릅니다.</p>
<blockquote>
<p>RSC와 SSR은 다르다는 거군요. 그럼 app router에서는 SSR &amp; SSG를 어떻게 구현하는지 알려주세요.</p>
</blockquote>
<p>저도 이런 의문이 들었는데요, 공식 문서를 자세히 읽어보면 충격적인 사실을 알 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ubin_ing/post/f5f522bb-52d6-424d-9030-2548686f1581/image.png" alt=""></p>
<p>바로 사용자가 따로 설정할 필요가 없다는 거죠!!!</p>
<p>pages router에서는 개발자가 SSG, SSR 둘 중 애플리케이션의 특징에 맞게 어떤 렌더링 방식이
더욱 적합한지 고민하고 이를 선택해야했습니다.</p>
<p>하지만 app router에서는 이런 고민을 할 필요가 없어요.</p>
<p>기본적으로 Next.js의 app router는 SSG 방식으로 렌더링됩니다.
그러다가 캐싱되지 않은 데이터 또는 동적 함수를 찾으면, 자동으로 내부에서 SSR을 채택합니다.</p>
<p>뿐만일까요? 렌더링 작업을 청크로 쪼개어 효율적으로 렌더링하는 &#39;스트리밍&#39;이라는 기술이 있습니다.
네 맞습니다, app router에서는 이것 또한 내장되어있기 때문에 자동으로 지원해줍니다.</p>
<p>그러니까 app router에서는 SSG &amp; SSR 자동 최적화 + Streaming까지 아주그냥 럭키비키가 따로 없다 이말입니다 ~!!</p>
<h2 id="무조건-app-router가-좋은가요">무조건 app router가 좋은가요?</h2>
<p>현재 기준으로 Next.js는 app router를 추천하고 있습니다. 하지만 어플리케이션의
특성에 따라 router를 설정할 수 있습니다.</p>
<p>Next.js가 낸 라우팅 유형의 컨셉에 맞게, 만약 클라이언트 사이드 목적으로 사용한다면 pages router를,
풀스택 프레임워크로 사용한다면  app router를 도입해보는 것을 추천드립니다.</p>
<h2 id="마무리">마무리</h2>
<p>요즘 공부하며 많이 느끼는 점은, 모든 상황에서 언제나 우수한 경우의 개발론은 찾기 어렵단 것입니다.
어플리케이션의 특성을 잘 이해하고, 이에 따른 프레임워크와 기술을 도입하는 것을 추천드립니다.</p>
<p>공부하다가 갑자기 쓰고 20분컷 낸 글이라서 다소 어색하고 틀린 부분이 있을 수 있습니다.
틀렸거나 아직 종결되지 않은 사안에 따른 좋은 피드백은 언제나 환영입니다!!</p>
<p>SSR보단 SSG가, SSG보단 롯데자이언츠가 짱입니다 화이팅~</p>
<h3 id="참고한-자료">참고한 자료</h3>
<p><a href="https://velog.io/@giyeon/Next.js-React-Server-Component">https://velog.io/@giyeon/Next.js-React-Server-Component</a></p>
<p><a href="https://funveloper.tistory.com/214">https://funveloper.tistory.com/214</a></p>
<p><a href="https://nextjs-ko.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#revalidating-data">https://nextjs-ko.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#revalidating-data</a></p>
<p><a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default">https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default</a></p>
<p><a href="https://nextjs.org/docs/app/building-your-application/rendering">https://nextjs.org/docs/app/building-your-application/rendering</a></p>
<p><a href="https://velog.io/@yyeonggg/Nextjs-Nextjs%EC%97%90%EC%84%9C-%EC%99%9CReact-Query%EB%A1%9C-%EC%BA%90%EC%8B%B1%EC%9D%B4-%EC%95%88%EB%90%A0%EA%B9%8C">https://velog.io/@yyeonggg/Nextjs-Nextjs%EC%97%90%EC%84%9C-%EC%99%9CReact-Query%EB%A1%9C-%EC%BA%90%EC%8B%B1%EC%9D%B4-%EC%95%88%EB%90%A0%EA%B9%8C</a></p>
<p><a href="https://velog.io/@nab5m/Next-js%EC%9D%98-%EC%BA%90%EC%8B%B1">https://velog.io/@nab5m/Next-js%EC%9D%98-%EC%BA%90%EC%8B%B1</a></p>
<p><a href="https://fe-developers.kakaoent.com/2024/240418-optimizing-nextjs-cache/">https://fe-developers.kakaoent.com/2024/240418-optimizing-nextjs-cache/</a></p>
<p><a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers">https://nextjs.org/docs/app/building-your-application/routing/route-handlers</a></p>
<p><a href="https://nextjs.org/docs/app/api-reference">https://nextjs.org/docs/app/api-reference</a></p>
<p><a href="https://velog.io/@minsang9735/NextJS%EC%97%90%EC%84%A0-React-Query%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B2%8C-%EB%A7%9E%EC%9D%84%EA%B9%8C">https://velog.io/@minsang9735/NextJS%EC%97%90%EC%84%A0-React-Query%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B2%8C-%EB%A7%9E%EC%9D%84%EA%B9%8C</a></p>
<p><a href="https://velog.io/@jkatie1027/Next13%EC%97%90%EC%84%9C-React-Query%EB%A5%BC-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B1%B8%EA%B9%8C">https://velog.io/@jkatie1027/Next13%EC%97%90%EC%84%9C-React-Query%EB%A5%BC-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B1%B8%EA%B9%8C</a></p>
<p><a href="https://velog.io/@hwon3814/NextJS-v13-App-Router%EC%9D%98-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8DSSR-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">https://velog.io/@hwon3814/NextJS-v13-App-Router%EC%9D%98-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8DSSR-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</a></p>
<p><a href="https://www.inflearn.com/community/questions/1137250/ssr%EA%B3%BC-rsc%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC-%EC%A7%88%EB%AC%B8%EC%9D%B4-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4">https://www.inflearn.com/community/questions/1137250/ssr%EA%B3%BC-rsc%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC-%EC%A7%88%EB%AC%B8%EC%9D%B4-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4</a></p>
<p><a href="https://velog.io/@2ast/React-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8React-Server-Component%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0">https://velog.io/@2ast/React-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8React-Server-Component%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[소프트웨어 마이스터고 학생으로 살아오며]]></title>
            <link>https://velog.io/@ubin_ing/destination</link>
            <guid>https://velog.io/@ubin_ing/destination</guid>
            <pubDate>Sun, 16 Jun 2024 03:53:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;중간만 가라는 새끼들 말 절대 믿지 말고, 존나 열심히 해라. 존나 열심히 하면... 다 너한테 돌아온다.&quot; <small>신병 - 심진우</small></p>
</blockquote>
<p>저는 부산소프트웨어마이스터고등학교 졸업 예정을 앞둔 3학년입니다.
곧 졸업하는 학생의 입장에서 고등학교 3년을 회고해보려 합니다.</p>
<h2 id="얻은-경험들">얻은 경험들</h2>
<p>처음 글을 써야겠다 마음 먹을 때에도 생각했지만,
3년이라는 시간을 글 하나에 남기기엔 너무 루즈해질 것이라 판단했습니다.
그래서 대표적으로 겪었던 경험을 섹션별로 나누어 짧게 회고해보겠습니다.</p>
<h3 id="az-900-자격증-수료-small2022-05small">AZ-900 자격증 수료 <small>(2022. 05.)</small></h3>
<p>1학년 학기 초, 학교에서 해당 자격증 수료를 장려 및 지원하여 수료하게 되었습니다.
시험일 전날 코피 흘리며 밤새 공부하여 시험을 합격할 수 있었습니다.
입학 후 처음 <strong>&#39;노력해서 이루어 낸&#39; 경험</strong>이었기에 열정의 발화점이 됐다고 생각합니다.</p>
<h3 id="전교-포트폴리오-경진대회-최우수상-small2022-08small">전교 포트폴리오 경진대회 최우수상 <small>(2022. 08.)</small></h3>
<p>화면 구현 수업 시간에 했던 HTML, CSS만으로 인터랙티브한 자기소개
웹사이트를 개발하여 최우수상을 수상했습니다.
이때부터 <strong>프론트엔드에 흥미를 가지고 개인적인 공부를 시작</strong>했습니다.</p>
<h3 id="교과우수상-small202207-202301small">교과우수상 <small>(2022.07, 2023.01)</small></h3>
<p>중학교 때 공부를 잘하는 축에 들어본 적이 없었지만, <strong>밤새 노력을 통해</strong>
통합사회, 컴퓨터구조, 디자인일반 등의 과목에서 교과우수상을 수상했습니다.</p>
<p>당시 컴퓨터구조 조별과제는 &#39;컴퓨터 관련 자유주제로 10~15분 발표&#39;였습니다.
&#39;양자 컴퓨터&#39;를 다룬 흥미있는 발표로 팀 수행평가 점수 전교 1등과 동시에
기말고사도 전교 1등을 달성해 정말 보람찼습니다.</p>
<h3 id="전교-알고리즘-경진-대회-수상-small202208-202308small">전교 알고리즘 경진 대회 수상 <small>(2022.08, 2023.08)</small></h3>
<p>전교 알고리즘 경진 대회에서 12문제 중 11문제를 풀며 대상을 수상했습니다.
계속되는 좋은 성과로 인해서 잠시동안 눈이 돌아가 자신을 과시하다가,
친구들과 사이가 멀어지고 있다는 걸 깨닫고 사과하며 다시 제자리를 찾았습니다.
<strong>&quot;벼는 익을수록 고개를 숙인다&quot;</strong>, 이후로 살면서 계속 염두하는 속담입니다.</p>
<p>추후에 2학년이 되어서 개최된 대회에서도 최우수상을 수상했습니다.
수상 후 작년의 나를 떠올리며 고개를 숙이고, <strong>한 걸음 성장했음</strong>을 느꼈습니다.</p>
<h3 id="교내-대나무숲-프로젝트-개발-small202206--202209small">교내 대나무숲 프로젝트 개발 <small>(2022.06 ~ 2022.09)</small></h3>
<p>당시 전공동아리 팀원들과 함께 교내 대나무숲 프로젝트를 개발했습니다.
팀장과 디자이너, 프론트엔드 리드를 도맡아 프로젝트를 완성시켰습니다.
<strong>선배의 도움 없이 오로지 우리 기수가 개발했던 최초의 프로젝트</strong>였습니다.
2학년들의 전공동아리 프로젝트 발표회 때 유일하게 1학년이 어깨를 나란히 하는
영광과 자부심을 느낄 수 있었으며, 이때 이후로 서비스 개발에 흥미를 느꼈습니다.</p>
<h3 id="교내-위키-부마위키-개발-small202208--202302small">교내 위키, 부마위키 개발 <small>(2022.08 ~ 2023.02)</small></h3>
<p>학생, 선생님들과 교내 사건/사고들이 담겨있는 부마위키를 개발했습니다.
1년간 <strong>사이트 조회수 50만회, 검색엔진 4만회 노출</strong>이라는 결과를 얻었습니다.
수치상 전국 모든 소마고를 통틀어 가장 성공한 프로젝트이며, 추후 매우 좋은
포트폴리오로 사용되어 제게 자랑스러운 아들 같은 존재가 되었습니다.</p>
<h3 id="스마트-학생-정보-관리-플랫폼-bsm-개발-small202306--202311small">스마트 학생 정보 관리 플랫폼, BSM 개발 <small>(2023.06 ~ 2023.11)</small></h3>
<p>팀 내 프리라이더가 발생하여 심적인 고생을 매우 많이 했던 프로젝트였습니다.
API가 60개에 육박하는 대규모 프로젝트였으나 프론트엔드를 혼자 개발해야 했지만,
당시 팀장으로써 이 또한 내 책무라 생각하고 데드라인 내 프로젝트를 완성시켰습니다.</p>
<h3 id="부산소프트웨어마이스터고등학교-전교회장-small202401--202501-small">부산소프트웨어마이스터고등학교 전교회장 <small>(2024.01 ~ 2025.01 )</small></h3>
<p>평소에도 내가 가진 것들로 동창뿐만 아니라 후배들을 도와주려고 노력했습니다.
덕분에 전교생을 대표하여 의견을 표하는 영광스런 자리를 얻을 수 있었습니다.
선거 공약들을 하나도 빠짐없이 모두 긍정적인 평가와 함께 성공적으로 이행했습니다.</p>
<h3 id="여러가지-경험들-small202203--small">여러가지 경험들 <small>(2022.03 ~ )</small></h3>
<p>외부 해커톤에도 참석해보며 처음 보는 사람들과 협업을 해보는 경험도 하고,
학교를 다니면서도 여러가지 경험들을 쌓아가면서 성장해왔던 것 같습니다.
아직 저는 어리지만, 삶을 살아가는 데 소통만큼 중요한 건 없다고 느꼈습니다.</p>
<h2 id="이로-인해-얻은-것들">이로 인해 얻은 것들</h2>
<p>크게 세 가지를 말씀드리고 싶습니다.</p>
<ol>
<li><p>위에서 언급했던 &#39;고등학교 전교회장&#39;이라는 인생에 남을 업적을 얻었습니다.</p>
</li>
<li><p>Series C 규모의 스타트업 합격과 동시에 면접 과정에서 저를 긍정적으로 봐주셔서 기존 계약 사항에 없었던 병역특례 또한 약속받았습니다.</p>
</li>
</ol>
<p>세 번째는 마지막으로 느낀 점을 이야기할 때 마저 말씀드리고 싶습니다.</p>
<h2 id="공부했던-방법">공부했던 방법</h2>
<p>이에 대해 <strong>공부를 한 동기</strong>와, 정말 의미 그대로 <strong>&#39;어떻게&#39; 공부했는지</strong>, 그리고 <strong>지양하는 공부법</strong>
이렇게 총 세 가지로 나누어 말씀드리겠습니다.</p>
<h3 id="공부를-쭉-할-수-있었던-동기">공부를 쭉 할 수 있었던 동기</h3>
<p>솔직히 말하면 열등감입니다. 입학 때부터 자존감은 낮고 자존심만 높았습니다.
불을 지피게 한 계기는 열등감이었고, 열등감을 상쇄시키기 위해 사용했던
&#39;열정&#39;과 &#39;노력&#39;이 공부를 계속 할 수 있게 만들어 준 주인공들입니다.</p>
<p>저는 피나는 노력만 있다면 누구든 꿈을 이루어낼 수 있다고 생각합니다.
물론 정말 피나는 노력임을 강조드리고 싶습니다. 남들이 쉽게 따라하기 힘든 노력.</p>
<p>글 앞에서 인용했던 한 드라마 인물의 대사처럼, &quot;이정도 했으면 됐어&quot;라는 말에
만족하지 말고, 본인이 정말 만족할 때까지 미친 듯이 노력해야 합니다.</p>
<h3 id="공부를-한-방법">공부를 한 방법</h3>
<p>제일 처음 프론트엔드를 공부할 때에는 강의를 토대로 공부했습니다.
노마드코더의 &#39;바닐라 JS로 크롬 앱 만들기&#39; 강의였습니다. 강의 공부의 중요성은
무엇보다도 &#39;완강&#39;이라고 생각합니다.</p>
<p>그리고 어느새, 강의는 &#39;템포&#39;가 정해져 있기 때문에 내가 더 느리게 배우고 싶거나,
혹은 더 빠르게 배우고 싶을 땐 잘 맞지 않는다고 생각헀습니다.
그때부터 전공 서적을 구입하여 개념을 공부하고, 실제 프로젝트에 사용해보며
직접 트러블 슈팅을 하며 온전히 그 기술을 내 것으로 만들어나가는 연습을 했습니다.</p>
<p>최근에는 책 발매가 기술 트렌드를 따라가지 못한다고 생각하여, 해당 기술 개발 팀의
공식 문서를 읽으면서 공부하고 있습니다.</p>
<p>보통은 한국어 번역 기능을 사용하지만, 영어 원문으로 읽으려고 노력합니다.
번역기가 완벽하지 않아 본래 전하고자 하는 바를 흐리는 경우가 매우 많기 때문입니다.</p>
<h3 id="지양하는-공부법">지양하는 공부법</h3>
<p>은 바로 GPT 사용하기입니다.
추후 GPT가 얼마나 발전할 지는 모르지만, (내심 GPT 5 떡밥보고 기대중입니다)
아직 GPT는 개발자가 원하는 완벽한 답변을 해주지 못하며 개발자에게 필요한
인터넷 서칭 능력을 훈련하지 못하게 막는 요소라고 생각합니다.</p>
<h2 id="개발자가-되려면-갖추어야할-덕목">개발자가 되려면 갖추어야할 덕목</h2>
<p>라는 답변에 대해서도 <strong>외적인 부분</strong>과 <strong>내적인 부분</strong> 두 가지로 나누어 말씀드리겠습니다.</p>
<h3 id="개발-외적인-부분에서는">개발 외적인 부분에서는</h3>
<p>소통이라고 생각합니다. 협업을 경험해보면 정말 소통이 얼마나 중요한지 느낍니다.
같은 내용이어도 전달하는 방식에 있어 한 팀이 어느 방향을 보고 달려가는지 까지에도
영향을 끼치는 것이 커뮤니케이션입니다.</p>
<p>개발자는 자신과 다른 분야를 전공한 사람과, 
때로는 자신과 다른 환경에서 자랐던 사람과,
때로는 자신과 다른 나이대의 사람과 협업해야 할 때도 있습니다.</p>
<p>개발자라면, 여러 방면의 사람들과 유연하게 소통할 수 있어야 한다고 생각합니다.
언론에게 현혹되어 세대, 젠더, 학벌 등의 이슈로 편을 갈라 싸우는 것에 동조하지 않고,
만나는 모든 사람을 배려하며 소통하는 것이 진정한 개발자, 아니, 위인이라고 생각합니다.</p>
<h3 id="개발-내적인-부분에서는">개발 내적인 부분에서는</h3>
<p>트러블 슈팅 능력이라고 생각합니다. 개발을 잘하는 사람과 못하는 사람을 지켜보면,
오류가 발생했을 때 대처하는 능력에 있어 차이가 크다는 것을 느꼈습니다.</p>
<p>트러블 슈팅은 많은 능력들을 내포합니다. 작게는 평소 구글링 능력부터 해서,
크게는 사고력과 논리력까지 필요로 하기도 합니다.</p>
<p>오류가 난다면 당황부터 하거나, 구글 또는 GPT에 에러 메시지부터 복붙하는 것 보다는,
오류가 발생한 근본적인 이유에 대해 생각하고 다가간다면 더욱 짧은 시간 내 오류를
해결할 수 있습니다.</p>
<h2 id="느낀-점">느낀 점</h2>
<p>요즈음 저처럼 빠르게 취업이 확정된 다른 친구들은 밝은 미래를 바라보며 
하루하루를 행복하게 살아가는 것처럼 보입니다.</p>
<p>하지만 저는 요즘 아직 떠나지도 않은 학교인데도 그립다는 생각을 많이 합니다.
성인이 되어 취업을 할 때에도, 지금 이 순간이 너무 그리울 것 같아서
틈만 나면 &quot;아 평생 고등학생이고 싶다&quot;라는 혼잣말을 하곤 합니다.</p>
<p>제일 큰 이유는, &#39;얻은 것들&#39;에서 세 번째로 언급한 &#39;좋은 친구들&#39; 때문입니다.
훌륭하고 귀감이 되는 친구들과 시간을 보낼 수 있어 너무 행복했습니다.</p>
<p>물론 모든 친구들이 저처럼 진심이진 않습니다. 가끔 체육대회나 학예회 등의 행사에서
소극적으로 참여하는 친구들을 보면 제가 그 친구 대신 서운해하기도 합니다.
인생 마지막 고등학교 생활인데, 안 아쉬우려나... 하고 말입니다.</p>
<p>하지만 제가 계속 그리워하고 걱정하는 와중에도, 시간은 계속 흐르고 있다는 걸
알고 있습니다. 
이젠 그리움을 미루어두고 앞으로의 미래를 바라보며 인생의 다음 분기로,
더 희망찬 미래를 위해서 서서히 발을 딛겠습니다.</p>
<blockquote>
<p><i>돌아갈 수 없다 한 대도
이 밤 또 노래를 불러야지
  그리워하는 마음이 
  미래를 향하는 마음이라며</i> 
  -&nbsp;<small>사라진 모든 것들에게(with ELLE KOREA)</small></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>