<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Gyu&amp;amp;een</title>
    <link>https://ms-diary.tistory.com/</link>
    <description>개발활동과 일상을 기록하고 있습니다</description>
    <language>ko</language>
    <pubDate>Sun, 21 Jun 2026 09:42:54 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>d 0_0 b</managingEditor>
    <image>
      <title>Gyu&amp;amp;een</title>
      <url>https://tistory1.daumcdn.net/tistory/6031419/attach/ea007ae8c4734d6a9c88432e0c7fb5b7</url>
      <link>https://ms-diary.tistory.com</link>
    </image>
    <item>
      <title>AI 바이브 코딩의 시대, 컴공 졸업 후 무엇을 할 수 있을까 - QR 기반 쿠폰 발급 서비스</title>
      <link>https://ms-diary.tistory.com/67</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서론&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▽&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;핵개인의 시대가 점점 선명해지고 있다고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;취업을 하더라도 결국에는 &amp;lsquo;본인의 영역&amp;rsquo;을 구축해야 하는 시대가 오고 있는 것 같습니다. 조직 안에서 주어진 일을 잘하는 것도 중요하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;동시에 자신이 하고 싶은 비즈니스나 문제의식 하나쯤은 가지고 있어야 사람으로서의 존재성을 입증할 수 있지 않을까&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하는 생각이 드는 요즘입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;AI의 시대가 도래하며 생산성은 대폭 상승했습니다. 이제는 마음만 먹으면 하고자 하는 일을 언제든 학습하고, 시도하고, 작게라도 구현해볼 수 있는 환경이 만들어졌습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;최근 자주 보이는 &amp;lsquo;1인 앱 개발로 앱스토어 1위&amp;rsquo;와 같은 사례도 이러한 흐름의 연장선에 있다고 생각합니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;흥미로운 점은, 이런 결과가 반드시 화려한 경력이나 깊은 개발 경험을 가진 사람들에게서만 나오는 것이 아니라는 점 입니다. 오히려 바이브 코딩을 활용해 아이디어를 빠르게 구현하고, 시장에 직접 던져보는 방식으로 만들어지는 경우를 많이 보게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_8598.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQIrxX/dJMcaiwW2eH/BknOIlvIMeNTKu7qNu1KWK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQIrxX/dJMcaiwW2eH/BknOIlvIMeNTKu7qNu1KWK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQIrxX/dJMcaiwW2eH/BknOIlvIMeNTKu7qNu1KWK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQIrxX%2FdJMcaiwW2eH%2FBknOIlvIMeNTKu7qNu1KWK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;427&quot; data-filename=&quot;IMG_8598.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 흐름을 보며 저 역시도 &amp;lsquo;당장 취업을 하는 것이 맞을까&amp;rsquo;라는 두려움이 생겼습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;물론 조직에 들어가 얻을 수 있는 것도 분명히 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;조직문화 안에서 협업하는 태도, 안정적인 삶, 실무 경험, 더 큰 규모의 문제를 다루는 기회가 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;하지만 동시에 주니어의 기준이라고 불리는 3년이라는 시간을 이 시기에 보낸다면, 그 3년 후에는 오히려 시대에 대한 직관을 놓치게 되는 것은 아닐까 하는 걱정도 생깁니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;개발에 관심을 두고 공부한 컴공 졸업자가 가질 수 있는 가장 큰 이점은,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;조직에 속하지 않더라도 스스로 작은 비즈니스를 만들 수 있다는 점이라고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;지역 사회에 기여하거나, 국제 단체에 공헌하는 일은 막연히 큰 조직에 속해야만 할 수 있다고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 내가 해결하고 싶은 문제가 있고, 하고 싶은 비즈니스가 있다면.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;오히려 특정 분야에 대한 문제의식을 가지고, 그 위에 컴퓨터공학적 사고와 구현 능력을 더할 수 있다면 더 단단한 비즈니스를 만들 수 있다&lt;/b&gt;&lt;/i&gt;고 생각합니다. 물론 그러기 위해서는 컴퓨터 전공지식에 대한 꾸준한 학습과 노력이 필요합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 3년 뒤의 시간은 또 어떻게 변해 있을지 모르겠습니다. 기술이 사회를 바꾸는 일은 오래전부터 있었지만, 지금은 그 변화의 사이클이 훨씬 빠르게 돌아가는 시대이기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 어디선가 들었던 &amp;ldquo;트렌드에서 하고 싶은 것을 찾지 말라&amp;rdquo;는 말도, 이제는 단순한 어른들의 귀 따가운 조언으로만 넘길 수 없게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서론이 길었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이러한 생각의 연장선에서 저는 현재 카페를 운영하고 있습니다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_8585.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT6D68/dJMcagZ9elc/eaPnZA6CaQwOpLHFIBfcJ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT6D68/dJMcagZ9elc/eaPnZA6CaQwOpLHFIBfcJ0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT6D68/dJMcagZ9elc/eaPnZA6CaQwOpLHFIBfcJ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT6D68%2FdJMcagZ9elc%2FeaPnZA6CaQwOpLHFIBfcJ0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;443&quot; height=&quot;332&quot; data-filename=&quot;IMG_8585.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_8587.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/odOGa/dJMcaf1fDA9/961k7tFUIkv4t5x9eZLxr0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/odOGa/dJMcaf1fDA9/961k7tFUIkv4t5x9eZLxr0/img.jpg&quot; data-alt=&quot;와인바도 하고 싶다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/odOGa/dJMcaf1fDA9/961k7tFUIkv4t5x9eZLxr0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FodOGa%2FdJMcaf1fDA9%2F961k7tFUIkv4t5x9eZLxr0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;386&quot; height=&quot;4032&quot; data-filename=&quot;IMG_8587.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;와인바도 하고 싶다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 최근에는 카페 브*비의 방문 전환을 목표로 QR 기반 쿠폰 발급 서비스를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/somea82/cafe-bravi-coupon/tree/main&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/somea82/cafe-bravi-coupon/tree/main&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1781665761803&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - somea82/cafe-bravi-coupon&quot; data-og-description=&quot;Contribute to somea82/cafe-bravi-coupon development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/somea82/cafe-bravi-coupon/tree/main&quot; data-og-url=&quot;https://github.com/somea82/cafe-bravi-coupon&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqObm4/dJMb84qhz04/IwxH8rgsvy1hyryeLaPZNk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/somea82/cafe-bravi-coupon/tree/main&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/somea82/cafe-bravi-coupon/tree/main&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqObm4/dJMb84qhz04/IwxH8rgsvy1hyryeLaPZNk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - somea82/cafe-bravi-coupon&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to somea82/cafe-bravi-coupon development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 만들었나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카페 브라비는 매주 약 100개 수준의 베이글 샌드위치를 납품하고 있다. 납품 제품에 대한 만족도는 높은 편이었지만, 그 만족도가 실제 매장 방문으로 이어지지는 않는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제품을 먹어본 고객은 있지만, 그 고객이 다시 카페로 찾아오게 만드는 장치는 부족했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 1차 목표를 단순하게 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;납품 고객 중 10%를 실제 매장 방문으로 전환시키는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 오프라인에서 QR을 스캔해 이벤트 페이지에 진입하고, 음료 할인 쿠폰을 발급받은 뒤, 매장 방문 시 직원이 쿠폰을 확인하고 사용 처리할 수 있는 작은 이벤트 시스템을 만들었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 흐름은 단순하게 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객은 납품 제품에 함께 제공된 QR을 스캔한다.&lt;br /&gt;QR을 통해 이벤트 페이지에 진입하고, 음료 20% 할인 쿠폰을 발급받는다.&lt;br /&gt;이후 매장 방문 시 쿠폰을 제시하면 직원이 웹 관리자 화면에서 쿠폰을 조회하고 사용 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직원 입장에서도 별도의 앱을 설치할 필요 없이, 웹 관리자 화면에서 쿠폰 조회와 사용 처리를 할 수 있도록 만들었다. 운영자는 관리자 대시보드에서 발급 수, 사용 수, 전환 흐름을 확인할 수 있게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠폰 중복 발급, 중복 사용, 만료 처리도 자동화했다. 단순히 쿠폰 이미지를 보여주는 방식이 아니라, 실제 사용 여부와 만료 여부를 서버에서 관리하는 구조로 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_8603.png&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;2436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTw6yk/dJMcaalk1Ey/U3yfKI9PK3Nmkjj13zfCP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTw6yk/dJMcaalk1Ey/U3yfKI9PK3Nmkjj13zfCP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTw6yk/dJMcaalk1Ey/U3yfKI9PK3Nmkjj13zfCP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTw6yk%2FdJMcaalk1Ey%2FU3yfKI9PK3Nmkjj13zfCP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;412&quot; height=&quot;892&quot; data-filename=&quot;IMG_8603.png&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;2436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;만들면서 중요하게 본 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요했던 것은 작은 데이터라도 운영자가 직접 볼 수 있어야 한다는 점이었다. 단순히 &amp;ldquo;이벤트를 했다&amp;rdquo;에서 끝나는 것이 아닌, 몇 명이 QR을 찍었고, 몇 명이 쿠폰을 발급받았고, 몇 명이 실제로 방문했는지 확인할 수 있어야 다음 의사결정을 할 수 있다고 생각했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel 통계 기준 QR 참여율은 40% 이상을 기록했다.&lt;br /&gt;쿠폰 사용량 기준 실제 고객 전환률은 약 7% 수준으로 확인되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 설정했던 1차 KPI인 10% 전환에는 아직 도달하지 못했지만, 단순 홍보물이 아니라 실제 방문으로 이어지는 흐름을 만들었다는 점에서 의미가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 좋았던 점은 운영자가 직접 확인 가능한 관리자 대시보드를 만들었다는 것이다. 이벤트를 감으로 운영하는 것이 아니라, 데이터를 보며 다음 액션을 고민할 수 있는 구조가 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 QR 쿠폰 서비스는 거대한 서비스는 아닙니다.&lt;br /&gt;하지만 제가 지금 고민하고 있는 방향과는 꽤 맞닿아 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI와 바이브 코딩의 시대에 중요한 것은 단순히 코드를 많이 아는 것이 아니라, 내가 마주한 문제를 빠르게 정의하고, 작은 형태로 구현해보고, 실제 반응을 확인하는 능력이라고 생각합니다. 이걸 Product Engineer라고 한다던가&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴공 졸업 후 무엇을 할 수 있을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 명확한 답을 내리기는 어렵습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 속한 공간에서 문제를 발견하고, 그것을 작게라도 서비스로 만들어볼 수 있다면, 그것만으로도 충분히 다음 가능성을 만들 수 있다는 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 카페 브라비 QR 쿠폰 서비스는 그 가능성을 확인하기 위한 작은 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자소서에 들어가는 말만 주인의식이 아니라, 진짜 돈과 땀을 써가며 결과물을 만들어보는것.&lt;/p&gt;</description>
      <category>포트폴리오</category>
      <category>1인 카페</category>
      <category>개발자 카페</category>
      <category>카페 운영</category>
      <category>컴공 카페</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/67</guid>
      <comments>https://ms-diary.tistory.com/67#entry67comment</comments>
      <pubDate>Wed, 17 Jun 2026 12:17:32 +0900</pubDate>
    </item>
    <item>
      <title>[커리큘럼 페이지 프로젝트] 관리자 로그인 세션을 어떻게 관리할 것인가 - 로그인도 PM의 역할일까</title>
      <link>https://ms-diary.tistory.com/66</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;미리보기&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;로그인도 PM의 역할일까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이번 고민을 하면서 &lt;b&gt;로그인 구현도 결국 정책의 문제&lt;/b&gt;라는 생각이 들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 막상 관리자 로그인 방식을 정리하다 보니, 어떤 사용자를 허용할 것인지, 어떤 상황에서 접속을 끊을 것인지, 얼마나 오랫동안 로그인 상태를 유지할 것인지 정하는 일이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;생각해보면 서비스에서 로그인은 거의 기본값처럼 붙어 있다. 하지만 모든 로그인 방식이 같을 수는 없다. 쇼핑몰의 로그인, 커뮤니티의 로그인, 금융 서비스의 로그인, 관리자 페이지의 로그인은 각자 기준이 다르다. 어떤 서비스는 편의성이 더 중요하고, 어떤 서비스는 보안과 통제가 더 중요하다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트의 관리자 로그인도 그랬다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;관리자 계정은 여러 명이 동시에 접속하는 것보다 하나의 세션만 유지되는 편이 안전하다. 일정 시간 활동이 없으면 자동으로 끊기는 편이 맞다. 브라우저를 닫았을 때도 로그인 상태가 오래 남아 있는 것은 부담스럽다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이 기준을 정하지 않고 바로 구현에 들어가면, 개발자는 각자 익숙한 방식으로 만들게 된다. 그러면 기능은 동작할 수 있지만 &lt;b&gt;서비스에 맞는 로그인인지는 다시 봐야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 로그인도 기획 단계에서 충분히 고민해야 하는 영역이라는 생각이 들었다. 단순히 &amp;ldquo;로그인 기능이 필요하다&amp;rdquo;가 아니라, &amp;ldquo;우리 서비스에서는 어떤 로그인 상태를 정상으로 볼 것인가&amp;rdquo;를 먼저 정해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;이번 작업을 하면서 기획과 개발의 경계가 생각보다 뚜렷하지 않다는 것도 느꼈다&lt;/u&gt;&lt;/b&gt;. PM이 모든 코드를 알아야 한다는 뜻은 아니다. 다만 서비스의 성격에 맞는 정책을 정하려면, 그 정책이 기술적으로 어떻게 구현되는지 어느 정도는 이해해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;로그인은 개발 기능처럼 보이지만, 실제로는 &lt;b&gt;서비스 운영 방식의 일부&lt;/b&gt;다. 사용자의 편의성과 보안 사이에서 어디에 기준을 둘지 정하는 일이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그 기준을 정하는 순간부터, 로그인도 PM의 일이 된다고 생각한다.&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;'서비스'라 통칭 되는것들은 로그인이 필요하다 보니, 웹 백엔드를 담당한다면 서비스에 맞는 로그인 방식을 고민해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기에 나도 관성적으로 로그인을 구현하지 않으려 고민하게 되는것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 관리자 로그인 세션을 어떻게 관리할지 고민했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 JWT 만료 시간을 30분으로 두면 되는 문제로 사고하고&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;로그인하면 토큰을 발급하고, 30분이 지나면 만료되게 하면 끝나는 일로 처리하면 될 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;단순히 spring security를 사용하는것에서 권한을 부여하는것에 생기는 부가적인 것들을 고민한다면&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;토큰 만료를 한 번 더 보아야한다고 생각한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어) 관리자 계정은 하나이고, 여러 사람이 동시에 접속하면 안 된다. 또 30분 동안 아무 활동이 없으면 자동으로 로그아웃되어야 한다. 다른 브라우저나 다른 기기에서 로그인하면 기존 로그인은 바로 끊겨야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그러려면 JWT만으로는 부족하다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;JWT는 한 번 발급되면 그 자체로 유효성을 가진다. 서버가 서명을 검증하고 만료 시간을 확인하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 간단하고 빠르지만, 반대로 말하면 이미 발급된 토큰을 중간에 끊어내기가 어렵다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기에 보통 JWT 안에 세션 정보를 넣고, DB에도 현재 유효한 세션 정보를 저장하는 방식으로 정리한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 세션 정보를 DB에 저장해야 했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 계정은 일반 사용자 회원 테이블이 따로 있는 구조가 아니었다. admin_settings에 관리자 설정 정보가 있고, 이 값을 기준으로 관리자 로그인 여부를 판단하는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 별도의 회원 테이블을 만들기보다는 admin_settings에 현재 로그인 세션 정보를 추가하는 방식이 더 자연스럽다고 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가할 값은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;current_session_id
session_expires_at
last_activity_at
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 current_session_id다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인할 때마다 새로운 sessionId를 만들고, 이 값을 DB에 저장한다. 그리고 JWT 안에도 같은 sessionId를 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이후 API 요청이 들어올 때마다 JWT 안의 sessionId와 DB에 저장된 current_session_id를 비교할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘이 같으면 현재 유효한 로그인이다.&lt;br /&gt;둘이 다르면 이미 다른 곳에서 새로 로그인한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 쓰면 기존 토큰이 아직 만료되지 않았더라도 바로 거부할 수 있다. 이 점이 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인 성공 시 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 서버는 새 세션을 만든다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;새 sessionId 생성

admin_settings.current_session_id = 새 sessionId
admin_settings.session_expires_at = now + 30분
admin_settings.last_activity_at = now

JWT 안에도 sessionId 포함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 서버와 클라이언트가 같은 세션 값을 공유하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 세션의 최종 기준은 클라이언트가 아니라 서버다. JWT 안에 sessionId가 들어 있어도, DB에 저장된 current_session_id와 맞지 않으면 유효하지 않은 요청으로 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 JWT 하나만 믿어도 되지 않을까 싶었다. 그런데 동시 로그인을 차단하려면 서버가 &amp;ldquo;지금 살아 있는 세션은 이것 하나다&amp;rdquo;라고 기억하고 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현재 세션의 기준을 DB에 두는 쪽이 맞다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;API 요청마다 확인할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 API 요청이 들어올 때마다 JwtAuthenticationFilter에서 몇 가지를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;JWT 서명이 유효한가?

JWT가 만료되지 않았는가?

JWT의 sessionId가 DB의 current_session_id와 같은가?

last_activity_at이 30분 이내인가?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건을 모두 통과하면 정상 요청으로 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 요청이 정상적으로 통과할 때마다 last_activity_at을 현재 시간으로 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 단순히 로그인 시점부터 30분이 아니라, 마지막 활동 시점부터 30분을 기준으로 만료를 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이도 생각보다 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자가 계속 사용 중인데 로그인 후 30분이 지났다는 이유만으로 끊기면 불편하다. 반대로 아무 활동이 없는데 토큰 만료 시간만 길게 남아 있으면 보안상 애매하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 기준은 로그인 시간이 아니라 마지막 활동 시간이어야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다른 곳에서 로그인하면 기존 로그인은 끊긴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 동시 로그인 차단이 비교적 단순하게 처리된다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 A 브라우저에서 먼저 로그인했다고 하자. 이때 DB에는 A의 sessionId가 저장되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤 B 브라우저에서 다시 로그인하면 새 sessionId가 생성되고, DB의 current_session_id가 B의 값으로 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;기존 current_session_id = A 세션

B 브라우저 로그인

current_session_id = B 세션으로 교체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 A 브라우저가 API를 요청하면 JWT 자체는 아직 살아 있을 수 있다. 하지만 JWT 안의 sessionId는 A 세션이고, DB의 current_session_id는 B 세션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 A 브라우저의 요청은 거부된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &amp;ldquo;다른 세션에서 접속 시 기존 로그인 끊김&amp;rdquo;을 구현할 수 있다. 굳이 기존 토큰을 직접 찾아서 삭제하지 않아도 된다. 서버가 현재 세션 기준을 하나만 들고 있으면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그아웃 API도 필요하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 만료만으로는 부족하다. 사용자가 직접 로그아웃할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 로그아웃 API를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;POST /api/auth/logout
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃 요청이 들어오면 서버에서는 현재 세션 정보를 비운다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;current_session_id = null
session_expires_at = null
last_activity_at = null
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서는 저장해둔 토큰을 삭제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 서버 기준에서도, 클라이언트 기준에서도 로그아웃 상태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 중요한 것은 서버 값을 비우는 것이다. 프론트에서 토큰만 삭제하면 현재 브라우저에서는 로그아웃처럼 보일 수 있다. 하지만 서버에는 세션이 남아 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 로그인처럼 보안을 조금 더 신경 써야 하는 구조에서는 서버 기준의 로그아웃 처리가 필요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브라우저 종료는 서버가 완벽히 알 수 없다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 고민했던 지점이 하나 더 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저를 닫았을 때 바로 로그아웃 처리를 할 수 있을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 브라우저 종료 시 서버에 로그아웃 요청을 보내면 되지 않을까 생각했다. 하지만 브라우저 종료는 서버가 100% 감지하기 어렵다. 사용자가 탭을 닫을 수도 있고, 브라우저를 강제 종료할 수도 있고, 네트워크가 끊길 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 브라우저 종료를 서버가 완벽히 감지하는 방식으로 설계하는 것은 무리가 있다고 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 프론트에서 토큰 저장 위치를 localStorage가 아니라 sessionStorage로 두는 방식을 선택할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;localStorage
-&amp;gt; 브라우저를 껐다 켜도 값이 남아 있음

sessionStorage
-&amp;gt; 브라우저 또는 탭 세션이 종료되면 값이 사라짐
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 브라우저나 탭 세션이 종료될 때 클라이언트에 저장된 토큰도 같이 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 방식도 완벽한 해결책은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sessionStorage는 탭 단위로 동작하기 때문에 여러 탭에서 로그인 상태를 공유하는 데 불편함이 생길 수 있다. 새 탭을 열었을 때 다시 로그인이 필요할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 보안 관점에서 sessionStorage가 모든 문제를 해결하는 것도 아니다. 스크립트에서 접근할 수 있기 때문에 XSS에 취약한 구조라면 토큰 탈취 위험은 여전히 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 이번 구조에서는 관리자 단일 계정이고, 브라우저 종료 시 로그인 상태가 오래 남는 것을 피하는 것이 더 중요하다고 봤다. 그래서 accessToken은 sessionStorage에 저장하는 쪽이 더 적절하다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리한 구현 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 백엔드와 프론트에서 해야 할 일은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서는 admin_settings에 세션 컬럼을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;current_session_id
session_expires_at
last_activity_at
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 시에는 새 sessionId를 만들고, DB와 JWT에 함께 반영한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 필터에서는 매 요청마다 JWT 유효성뿐 아니라 DB에 저장된 현재 세션과 일치하는지도 확인한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;JWT 서명 검증
JWT 만료 검증
sessionId 일치 여부 검증
30분 미활동 여부 검증
last_activity_at 갱신
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃 API도 추가한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;POST /api/auth/logout
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃 시에는 DB의 세션 정보를 비우고, 프론트에서는 저장된 토큰을 삭제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서는 accessToken을 sessionStorage에 저장한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;로그인 성공 시 sessionStorage에 저장
로그아웃 시 sessionStorage에서 삭제
브라우저 또는 탭 세션 종료 시 토큰 제거
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;로그인도&amp;nbsp;PM의&amp;nbsp;역할?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 고민을 하면서 로그인 구현도 결국 정책의 문제라는 생각이 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 로그인이라고 하면 단순히 아이디와 비밀번호를 확인하고, 토큰을 발급하는 기능 정도로 생각했다. 하지만 막상 관리자 로그인 방식을 정리하다 보니, 이건 단순한 개발 기능이 아니었다. 어떤 사용자를 허용할 것인지, 어떤 상황에서 접속을 끊을 것인지, 얼마나 오랫동안 로그인 상태를 유지할 것인지 정하는 일이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;생각해보면 서비스에서 로그인은 거의 기본값처럼 붙어 있다. 하지만 모든 로그인 방식이 같을 수는 없다. 쇼핑몰의 로그인, 커뮤니티의 로그인, 금융 서비스의 로그인, 관리자 페이지의 로그인은 각자 기준이 다르다. 어떤 서비스는 편의성이 더 중요하고, 어떤 서비스는 보안과 통제가 더 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 프로젝트의 관리자 로그인도 그랬다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;관리자 계정은 여러 명이 동시에 접속하는 것보다 하나의 세션만 유지되는 편이 안전하다. 일정 시간 활동이 없으면 자동으로 끊기는 편이 맞다. 브라우저를 닫았을 때도 로그인 상태가 오래 남아 있는 것은 부담스럽다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 기준을 정하지 않고 바로 구현에 들어가면, 개발자는 각자 익숙한 방식으로 만들게 된다. 그러면 기능은 동작할 수 있지만 서비스에 맞는 로그인인지는 다시 봐야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 로그인도 기획 단계에서 충분히 고민해야 하는 영역이라는 생각이 들었다. 단순히 &amp;ldquo;로그인 기능이 필요하다&amp;rdquo;가 아니라, &amp;ldquo;우리 서비스에서는 어떤 로그인 상태를 정상으로 볼 것인가&amp;rdquo;를 먼저 정해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 작업을 하면서 기획과 개발의 경계가 생각보다 뚜렷하지 않다는 것도 느꼈다. PM이 모든 코드를 알아야 한다는 뜻은 아니다. 다만 서비스의 성격에 맞는 정책을 정하려면, 그 정책이 기술적으로 어떻게 구현되는지 어느 정도는 이해해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로그인은 개발 기능처럼 보이지만, 실제로는 서비스 운영 방식의 일부다. 사용자의 편의성과 보안 사이에서 어디에 기준을 둘지 정하는 일이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그 기준을 정하는 순간부터, 로그인도 PM의 일이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구조에서는 DB 회원 테이블 없이도 관리자 단일 계정에 대해 두 가지를 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;30분 미활동 로그아웃
동시 로그인 차단
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 JWT만 믿지 않고, 서버가 현재 유효한 세션을 하나 기억하게 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 계정은 편의성보다 통제가 더 중요하다. 그래서 이번에는 조금 더 엄격한 쪽으로 설계하는 것이 맞다고 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이번 작업은 로그인 기능을 만드는 일이기도 했지만, 관리자 페이지의 접근 기준을 정하는 일이기도 했다. 기능은 코드로 구현되지만, 그 전에 어떤 상태를 허용하고 어떤 상태를 막을지 먼저 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 기준을 세우는 것이 이번 작업에서 가장 중요했던 부분이었다.&lt;/p&gt;</description>
      <category>프로그래밍/spring</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/66</guid>
      <comments>https://ms-diary.tistory.com/66#entry66comment</comments>
      <pubDate>Thu, 11 Jun 2026 14:35:12 +0900</pubDate>
    </item>
    <item>
      <title>[커리큘럼 페이지 프로젝트] Mock API를 걷어내고 Spring을 붙이며</title>
      <link>https://ms-diary.tistory.com/65</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;6/10 작업 현황 정리&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h3 style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR 정리&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR #1: Spring main/careers/subject detail 통합&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR:&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring-careers-api-test&amp;nbsp;&amp;rarr;&amp;nbsp;spring-api-test&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주요 내용:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/main&amp;nbsp;Spring 프록시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/subject/table&amp;nbsp;Spring 프록시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/careers/list&amp;nbsp;Spring 프록시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/subjects/{subjectId}&amp;nbsp;과목 툴팁 Spring 프록시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;normalizeSpringMainData()&amp;nbsp;추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;mock 기반 메인 초기 데이터 제거&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;merge 완료&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring-api-test에 반영&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR #2: Career side detail 통합&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR:&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring-careerside-api-test&amp;nbsp;&amp;rarr;&amp;nbsp;spring-api-test&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주요 내용:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/careers/detail&amp;nbsp;Next 프록시 추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커리어 상세 페이지를 mock에서 Spring API 기반으로 변경&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커리어 대표 직무 예시와 참고 링크를 Spring 데이터로 렌더링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;backend&amp;nbsp;CareerPath에&amp;nbsp;representativeExamples,&amp;nbsp;referenceLinks&amp;nbsp;필드 추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;별도&amp;nbsp;CareerExample,&amp;nbsp;CareerReferenceLink&amp;nbsp;엔티티/레포지토리 제거&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;seed 데이터에 커리어별 예시와 참고 링크 추가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Next.js에서 mock으로 받아오던 데이터를 Spring API로 바꾸는 작업이었다. 말로만 들으면 간단하다. fetch 주소를 바꾸고, 응답 형태를 맞추고, 화면에서 잘 나오는지 확인하면 될 것 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 막상 해보니 이건 단순히 API 주소를 바꾸는 일이 아니었다. 화면이 믿고 있는 데이터의 기준을 옮기는 일이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 프론트 안에 있는 mock 데이터가 기준이었다. 화면도 그 데이터를 보고 있었고, Next의 route.ts도 그 데이터를 내려주고 있었다. 겉으로는 API를 호출하는 구조였지만, 실제로는 프론트 프로젝트 안에서 모든 게 해결되고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Spring을 붙인다는 건 이 기준을 바꾸는 일이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이제 데이터의 기준은 mock이 아니라 Spring이어야 했다. 그리고 Spring 뒤에는 Controller, Service, Repository, DB가 있었다. 생각해보면 당연한 이야기인데, 처음에는 이 차이를 조금 가볍게 봤던 것 같다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;기존에는 Next가 임시 백엔드 역할을 하고 있었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 흐름은 이랬다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;MainPageClient
  -&amp;gt; useCareerList
    -&amp;gt; fetchCareerList
      -&amp;gt; fetch(&quot;/api/careers/list&quot;)
        -&amp;gt; Next route.ts
          -&amp;gt; createCareerListMock()
          -&amp;gt; JSON 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainPageClient가 화면을 만들고, useCareerList가 데이터를 관리하고, fetchCareerList가 API를 호출한다. 여기까지는 일반적인 프론트 흐름처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제 요청은 Spring으로 가는 것이 아니라 Next 안의 route.ts로 갔다. 그리고 route.ts는 mock 데이터를 만들어 JSON으로 돌려줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 이 구조는 정확히 말하면 이런 상태였다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;브라우저
  -&amp;gt; Next 프론트엔드
    -&amp;gt; Next route.ts
      -&amp;gt; mock 데이터
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 개발 초기에는 이런 구조가 편하다. 백엔드가 완성되지 않아도 화면을 만들 수 있고, 필요한 데이터도 원하는 모양으로 바로 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 어느 순간부터는 이 구조가 발목을 잡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면은 API를 호출하는 것처럼 보이는데, 실제 데이터는 DB가 아니라 mock에서 온다. 그러면 Spring DB의 값을 바꿔도 화면이 바뀌지 않는다. 처음에는 이상하게 느껴지지만, 사실 당연한 일이다. 화면이 Spring을 보고 있지 않았으니까.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring을 붙이면 바뀌는 것은 화면이 아니라 목적지다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙인 뒤의 흐름은 이렇게 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;MainPageClient
  -&amp;gt; useCareerList
    -&amp;gt; fetchCareerList
      -&amp;gt; fetch(&quot;http://localhost:8081/api/careers/list&quot;)
        -&amp;gt; Spring Controller
          -&amp;gt; Service
            -&amp;gt; Repository
              -&amp;gt; DB
          -&amp;gt; JSON 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 건 프론트 전체가 바뀌는 게 아니라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainPageClient는 그대로 있다.&lt;br /&gt;useCareerList도 그대로 있다.&lt;br /&gt;fetchCareerList도 그대로 있다.&lt;br /&gt;CareerSide도 그대로 데이터를 받아 렌더링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바뀌는 것은 결국 fetchCareerList가 어디를 바라보느냐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 이랬다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;fetchCareerList
  -&amp;gt; Next route.ts
  -&amp;gt; mock 데이터
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙이면 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;fetchCareerList
  -&amp;gt; Spring Controller
  -&amp;gt; Service
  -&amp;gt; Repository
  -&amp;gt; DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 fetch(&quot;/api/careers/list&quot;)를 fetch(&quot;http://localhost:8081/api/careers/list&quot;)로 바꾸는 정도라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 한 줄의 의미가 생각보다 컸다. URL만 바뀐 게 아니라, 화면이 믿는 데이터 출처가 바뀌는 것이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브랜치 기준 정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 하면서 먼저 정리해야 했던 것은 브랜치였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dev 브랜치에는 아직 mock 기반 코드가 남아 있었다. 그래서 Spring 연동 작업을 바로 dev에 섞기보다는, 따로 통합 브랜치를 두기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이렇게 정리했다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;dev
-&amp;gt; mock 기반 안정 브랜치

spring-api-test
-&amp;gt; Spring API 통합 기준 브랜치

spring-careers-api-test
-&amp;gt; 메인, 과목 테이블, 커리어 목록, 과목 툴팁 연동 브랜치

spring-careerside-api-test
-&amp;gt; 커리어 상세 페이지 연동 브랜치
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 중간에 한 번 꼬였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-api-test를 만들 때 이미 Spring 연동 작업이 들어가 있던 spring-careers-api-test에서 딴 것이 아니라, dev에서 바로 따왔다. 그러다 보니 앞에서 작업했던 메인 화면과 과목 테이블 연동 코드가 빠졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 이상한 상태가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;툴팁은 Spring을 보고 있는데, 메인 화면은 여전히 mock을 보고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 브랜치를 어떻게 나누느냐도 중요하지만, 더 중요한 건 어느 브랜치를 기준으로 통합할지였다. 기준 브랜치가 흔들리면 그 위에 올라가는 작업도 같이 흔들린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 spring-careers-api-test의 내용을 PR로 spring-api-test에 병합했고, 이후에는 spring-api-test를 Spring API 통합 기준 브랜치로 고정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별것 아닌 것 같지만, 이런 기준이 없으면 작업이 진행될수록 어디까지가 반영된 코드인지 점점 흐려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;혼자하는 프로젝트이기에 기존 기능 단위 브랜치 분기가 아닌 화면 단위의 브랜치 분기를 선정해볼 수 있었다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같은 화면에서 서로 다른 데이터를 보고 있었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업에서 가장 기억에 남는 문제는 이산수학 과목이었다. 몇몇 과목들은 이수과정이 바뀌면서 전필(전공 필수)가 전선(전공 선택) 과목이 되는 경우가 있었기에 이 전에 택했었던 DB 설계 전략이 정확히 먹혀 들어갈 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;i&gt;고심했던 문제이니, 아래의 글을 읽어봐주시면 너무너무 감사할 것 같습니당.&lt;/i&gt;&lt;br /&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://ms-diary.tistory.com/49&quot;&gt;2026.01.01 - [프로그래밍] - [트러블 슈팅] 매년 바뀌는 전공 이수 기준, 어떻게 설계해야 깨지지 않을까?&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring DB에서 이산수학의 requirementType을 MAJOR_REQUIRED로 바꿨다. 당연히 화면에서도 전공 필수로 보여야 한다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 툴팁에는 &amp;ldquo;전공 필수&amp;rdquo;가 잘 나왔다. 반면 메인 카드에는 M 뱃지가 보이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 프론트 렌더링 문제라고 생각했다. 조건문이 잘못됐나, 타입이 안 맞나, 뱃지 표시 로직이 빠졌나 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 원인은 더 단순했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;툴팁과 메인 카드가 서로 다른 데이터를 보고 있었다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;툴팁
-&amp;gt; Spring API 조회

메인 카드
-&amp;gt; mock createMainMock() 조회
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;툴팁은 Spring DB의 바뀐 값을 보고 있었다. 그래서 &amp;ldquo;전공 필수&amp;rdquo;가 정상적으로 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 메인 카드는 아직 mock 데이터를 보고 있었다. 그러니 Spring DB에서 아무리 값을 바꿔도 메인 카드에는 반영될 수가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 메인 카드의 M 뱃지는 requirementType을 직접 보는 구조도 아니었다. course.badges 값을 보고 렌더링하고 있었다. 즉, Spring에서 requirementType을 바꿔도 mock의 badges가 그대로라면 화면은 그대로였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 알게 된 것은 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API는 하나만 붙인다고 끝나는 게 아니다. 같은 화면을 구성하는 데이터는 같은 출처를 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;툴팁만 Spring으로 바꾸는 건 부분적으로는 성공처럼 보인다. 하지만 사용자가 보는 화면 기준에서는 오히려 모순이 생긴다. 사용자에게는 &amp;ldquo;여기는 mock이고 여기는 Spring이라서 그렇다&amp;rdquo;는 설명이 의미가 없다. 그냥 화면이 이상한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 /api/main, /api/subject/table까지 Spring으로 연결했다. 메인 화면과 과목 카드가 Spring 데이터를 보도록 맞췄다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;normalizeSpringMainData가 필요했던 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 응답을 바로 프론트 컴포넌트에 넣을 수도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그렇게 하면 백엔드 응답 구조가 조금만 바뀌어도 UI 컴포넌트들이 같이 흔들릴 수 있다. 기존 컴포넌트들은 이미 MainData라는 형태에 맞춰져 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 중간에 normalizeSpringMainData()를 두었다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Spring 응답
  -&amp;gt; normalizeSpringMainData()
    -&amp;gt; 기존 MainData 형태
      -&amp;gt; 기존 UI 컴포넌트
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조가 꽤 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 내려주는 데이터는 백엔드 기준의 데이터다. 반면 프론트 컴포넌트가 원하는 데이터는 화면 기준의 데이터다. 둘이 항상 같을 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 중간에서 한 번 변환해주는 계층이 필요했다. 덕분에 기존 UI를 크게 흔들지 않고 데이터 출처만 Spring으로 바꿀 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 하기 전에는 이런 변환 계층을 단순한 매핑 정도로 생각했다. 그런데 실제로 해보니 이 계층은 프론트와 백엔드 사이의 완충지대에 가까웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드는 백엔드답게 응답하고, 프론트는 프론트답게 화면을 그리기 위해 필요한 지점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검증은 화면만 보면 부족했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 중에는 프론트 타입 검사를 계속 돌렸다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;npm run typecheck
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 테스트도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;./gradlew test
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 로컬 Spring API 응답도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;/api/main 200
/api/subject/table 200
/api/subjects/{subjectId} 200
/api/careers/detail 연동 구조 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 화면이 잘 나온다고 끝낼 수 없었다. 화면은 정상처럼 보여도 내부에서는 여전히 mock을 보고 있을 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제로 어떤 API를 호출하는지, 응답이 어디서 오는지, 프론트 타입은 깨지지 않는지, 백엔드 테스트는 통과하는지를 같이 봐야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 화면에서 잘 보이면 어느 정도 된 것이라고 생각했다. 그런데 이번 작업에서는 그 생각이 조금 바뀌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 전환 작업에서는 화면이 아니라 데이터 출처를 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 작업에서 배운 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업의 핵심은 Spring API를 붙였다는 사실 자체가 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 중요했던 것은 mock 기반으로 만들어진 화면을 실제 API 기준으로 전환할 때, 무엇을 기준으로 삼아야 하는지였다. 개발자로 보면 API 연결 작업이지만, &lt;b&gt;&lt;u&gt;PM 관점에서 보면 전환 범위를 정하고, 기준을 맞추고, 사용자에게 보이는 화면의 일관성을 관리하는 일이었다.&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 전환은 API 단위가 아니라 화면 단위로 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 특정 API가 Spring과 연결되었는지가 중요해 보였다. 하지만 실제로 문제가 된 것은 API 하나의 연결 여부가 아니었다. 같은 화면 안에서 어떤 영역은 Spring 데이터를 보고, 어떤 영역은 mock 데이터를 보고 있다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;툴팁은 Spring을 보고 있었고, 메인 카드는 mock을 보고 있었다. 그래서 이산수학의 requirementType을 Spring DB에서 MAJOR_REQUIRED로 바꿨을 때, 툴팁에는 &amp;ldquo;전공 필수&amp;rdquo;가 보였지만 메인 카드에는 M 뱃지가 보이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 중에는 이런 상태가 잠깐 생길 수 있다. 하지만 사용자 입장에서는 내부 사정을 알 수 없다. 사용자는 툴팁과 메인 카드를 하나의 화면으로 본다. 한쪽에서는 전공 필수라고 하고, 다른 쪽에서는 전공 필수 표시가 없다면 그것은 단순한 개발 과정이 아니라 화면의 불일치로 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 PM 관점에서는 &amp;ldquo;어떤 API를 붙였는가&amp;rdquo;보다 &amp;ldquo;이 화면은 이제 어떤 데이터 기준으로 동작하는가&amp;rdquo;를 먼저 봐야 한다. API 전환 범위도 화면 단위로 정의하는 편이 더 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 데이터 구조의 차이는 일정과 QA에 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 응답을 프론트가 바로 쓰게 만들 수도 있다. 하지만 그 방식은 프론트와 백엔드를 강하게 묶는다. 백엔드 응답 구조가 조금만 바뀌어도 화면 컴포넌트가 같이 흔들릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 normalizeSpringMainData()를 통해 Spring 응답을 기존 프론트의 MainData 형태로 변환했다. 이 작업은 단순한 코드 정리가 아니었다. 기존 UI를 최대한 유지하면서 데이터 출처만 바꾸기 위한 완충 장치였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM 관점에서 이런 변환 계층은 꽤 중요하다. 백엔드와 프론트가 동시에 완벽히 맞아떨어지는 경우는 많지 않다. 응답 필드명, 데이터 묶음 방식, 화면에서 필요한 값이 조금씩 다를 수 있다. 이 차이를 어디서 흡수할지 정하지 않으면, 작은 응답 변경도 화면 수정과 QA 범위 확대로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 API 통합을 관리할 때는 단순히 &amp;ldquo;응답이 온다&amp;rdquo;가 아니라 &amp;ldquo;이 응답이 기존 화면 구조에 어떤 영향을 주는가&amp;rdquo;를 함께 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, 통합 브랜치는 작업의 기준선이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 한 번 꼬였던 부분은 브랜치 기준이었다. spring-api-test가 처음부터 Spring 연동 코드가 반영된 브랜치에서 파생되지 않고, mock 기반의 dev에서 만들어지면서 이미 진행된 작업 일부가 빠졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 어떤 화면은 Spring 기준으로 전환되어 있고, 어떤 화면은 다시 mock 기준으로 남아 있는 상태가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 겪고 나서 느낀 것은, 통합 브랜치는 단순히 코드를 모으는 장소가 아니라 작업의 기준선이라는 점이다. 기준 브랜치가 명확해야 기능 브랜치도 의미가 있다. 어느 브랜치에 무엇이 들어가야 하는지 정해져 있어야 PR의 목적도 분명해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM 입장에서는 기능이 얼마나 개발되었는지만 볼 것이 아니라, 그 기능이 어느 기준 브랜치에 반영되어 있는지도 확인해야 한다. 개발은 되었지만 통합 기준에 올라오지 않은 작업은 아직 제품 흐름 안에 들어온 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷째, 전환 작업은 기능 개발보다 관리 업무에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mock을 걷어내고 Spring을 붙이는 일은 겉으로 보기에는 개발 작업이다. 하지만 실제로는 기준을 맞추는 일이 더 많았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;API 주소가 맞아야 했다.&lt;br /&gt;Spring 포트가 맞아야 했다.&lt;br /&gt;응답 구조가 맞아야 했다.&lt;br /&gt;seed 데이터가 맞아야 했다.&lt;br /&gt;화면 문구와 fallback 상태도 맞아야 했다.&lt;br /&gt;브랜치 기준도 맞아야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 하나만 어긋나도 화면은 정상적으로 보이지 않는다. 더 어려운 점은, 어떤 문제는 화면만 봐서는 바로 드러나지 않는다는 것이다. 화면은 정상처럼 보이지만 내부적으로는 아직 mock을 보고 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 전환 작업에서는 체크리스트가 필요하다. 단순히 &amp;ldquo;API 연결 완료&amp;rdquo;라고 표시하는 것이 아니라, 해당 화면의 데이터 출처, 호출 API, 응답 형태, 예외 처리, seed 데이터, 테스트 여부까지 함께 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 API 통합은 기술 작업이면서 동시에 운영 관리 작업이라는 것을 느꼈다. 특히 mock에서 실제 API로 넘어가는 시점에는 더 그렇다. 이 구간에서는 개발 속도보다 기준의 일관성이 더 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 spring-api-test에는 다음 작업이 병합되어 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메인 화면 Spring 연동
학기별 과목 테이블 Spring 연동
커리어 목록 Spring 연동
과목 툴팁 Spring 연동
커리어 상세 페이지 Spring 연동
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 메인 화면과 커리어 사이드의 주요 흐름은 Spring API 기준으로 맞춰졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 방향은 admin API나 추가 세부 화면도 같은 방식으로 Spring에 붙이는 것이다. 앞으로는 spring-api-test를 기준으로 기능별 브랜치를 따서 작업하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 하고 나서 Spring API 통합을 조금 다르게 보게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙인다는 건 단순히 mock을 지우는 일이 아니다. 화면이 신뢰하는 데이터의 출처를 바꾸는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터의 출처를 바꾸려면 코드만 바꿔서는 부족하다. 브랜치도 맞아야 하고, 포트도 맞아야 하고, 응답 형태도 맞아야 하고, seed 데이터도 맞아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 개발은 기능을 하나씩 붙이는 일이기도 하지만, 기준을 하나씩 정리하는 일이기도 한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR 정리&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR #1: Spring main/careers/subject detail 통합&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR:&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring-careers-api-test&amp;nbsp;&amp;rarr;&amp;nbsp;spring-api-test&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주요 내용:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/main&amp;nbsp;Spring 프록시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/subject/table&amp;nbsp;Spring 프록시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/careers/list&amp;nbsp;Spring 프록시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/subjects/{subjectId}&amp;nbsp;과목 툴팁 Spring 프록시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;normalizeSpringMainData()&amp;nbsp;추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;mock 기반 메인 초기 데이터 제거&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;merge 완료&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring-api-test에 반영&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR #2: Career side detail 통합&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PR:&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring-careerside-api-test&amp;nbsp;&amp;rarr;&amp;nbsp;spring-api-test&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #cccccc; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주요 내용:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #cccccc; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/api/careers/detail&amp;nbsp;Next 프록시 추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커리어 상세 페이지를 mock에서 Spring API 기반으로 변경&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커리어 대표 직무 예시와 참고 링크를 Spring 데이터로 렌더링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;backend&amp;nbsp;CareerPath에&amp;nbsp;representativeExamples,&amp;nbsp;referenceLinks&amp;nbsp;필드 추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;별도&amp;nbsp;CareerExample,&amp;nbsp;CareerReferenceLink&amp;nbsp;엔티티/레포지토리 제거&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;seed 데이터에 커리어별 예시와 참고 링크 추가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍/next.js</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/65</guid>
      <comments>https://ms-diary.tistory.com/65#entry65comment</comments>
      <pubDate>Wed, 10 Jun 2026 21:32:06 +0900</pubDate>
    </item>
    <item>
      <title>[커리큘럼 페이지 프로젝트] Next.js App Router에서 화면과 데이터 호출 흐름 이해하기 (2) - Next.js에 Spring을 붙이면 데이터 흐름은 어떻게 바뀔까</title>
      <link>https://ms-diary.tistory.com/64</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재는 먼저 화면을 잡고 백엔드를 잡고 싶어서, next로 구조를 잡다보니, route.ts로 api호출의 역할을 대신하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞선 글에서도 큰 흐름은 그렇게 정리하다보니 정작 중요한 얘기를 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Q. Next.js에 Spring을 붙이면 데이터 흐름은 어떻게 바뀔까&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 흐름에서는 Next.js 프로젝트 안에서 화면을 만들고, 필요한 데이터도 Next 내부의 API Route에서 mock 데이터로 응답하는 구조를 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 여기에 Spring 백엔드를 붙이면 어떤 부분이 바뀌는지 정리해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, Spring을 붙인다고 해서 React 컴포넌트의 흐름이 전부 바뀌는 것은 아니다. 핵심적으로 바뀌는 부분은 route.ts가 하던 서버 역할을 Spring 서버가 가져간다는 점이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 현재 구조는 Next 내부 API를 사용한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트의 데이터 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;MainPageClient
  -&amp;gt; useCareerList
    -&amp;gt; fetchCareerList
      -&amp;gt; fetch(&quot;/api/careers/list&quot;)
        -&amp;gt; Next route.ts
          -&amp;gt; createCareerListMock()
          -&amp;gt; JSON 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 MainPageClient는 화면을 구성하는 클라이언트 컴포넌트다.&lt;br /&gt;useCareerList는 데이터 상태를 관리하는 hook이다.&lt;br /&gt;fetchCareerList는 실제 API 요청을 보내는 함수다.&lt;br /&gt;route.ts는 Next.js 내부에서 API 요청을 받아 응답을 만들어주는 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 Spring 서버가 없기 때문에 route.ts가 임시 백엔드 역할을 한다.&lt;br /&gt;즉, 실제 DB에서 데이터를 가져오는 것이 아니라 createCareerListMock() 같은 mock 데이터를 만들어 JSON으로 응답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조를 더 단순하게 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;브라우저
  -&amp;gt; Next 프론트엔드
    -&amp;gt; Next route.ts mock API
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 프론트엔드와 임시 API 서버가 모두 Next.js 프로젝트 안에 있는 상태다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring을 붙이면 route.ts 역할이 Spring으로 이동한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙이면 가장 크게 바뀌는 부분은 API 요청의 도착지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 클라이언트에서 다음과 같이 요청을 보낸다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;const res = await fetch(&quot;/api/careers/list&quot;, {
  method: &quot;POST&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
  body: JSON.stringify(payload),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 요청은 Next.js 내부의 다음 파일로 연결된다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;app/api/careers/list/route.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Spring 서버를 붙이면 이 요청은 Next의 route.ts로 가지 않고 Spring 서버의 Controller로 가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Spring 서버가 localhost:8080에서 실행 중이라면 요청 코드는 다음과 같이 바뀔 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;const res = await fetch(&quot;http://localhost:8080/api/careers/list&quot;, {
  method: &quot;POST&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
  body: JSON.stringify(payload),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바뀐 것은 전체 구조가 아니라 API 요청 주소다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;fetch(&quot;/api/careers/list&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 요청이 Next 내부 API를 바라보느냐,&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;fetch(&quot;http://localhost:8080/api/careers/list&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 서버 API를 바라보느냐의 차이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Spring을 붙인 뒤의 전체 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙이면 흐름은 다음과 같이 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;MainPageClient
  -&amp;gt; useCareerList
    -&amp;gt; fetchCareerList
      -&amp;gt; fetch(&quot;http://localhost:8080/api/careers/list&quot;)
        -&amp;gt; Spring Controller
          -&amp;gt; Service
            -&amp;gt; Repository
              -&amp;gt; DB
          -&amp;gt; JSON 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 fetchCareerList가 Next 내부의 route.ts를 호출했다.&lt;br /&gt;이제는 Spring의 Controller를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 프론트엔드 쪽 흐름은 거의 그대로 유지된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;MainPageClient
useCareerList
fetchCareerList
CareerSide
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 그대로 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 다음 부분이 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Next route.ts
createCareerListMock()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역할이 Spring 서버로 이동한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Spring 서버에서는 어떤 일이 일어날까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 쪽에서는 보통 Controller, Service, Repository 구조로 요청을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Next에서 /api/careers/list로 요청을 보냈다면, Spring에서는 다음과 같은 Controller가 요청을 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/careers&quot;)
public class CareerController {

    @PostMapping(&quot;/list&quot;)
    public CareerListResponse getCareerList(@RequestBody CareerListRequest request) {
        return careerService.getCareerList(request);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서 Controller는 HTTP 요청을 받는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 실제 비즈니스 로직은 Service가 처리한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Controller
  -&amp;gt; HTTP 요청을 받는다.

Service
  -&amp;gt; 필요한 비즈니스 로직을 처리한다.

Repository
  -&amp;gt; DB에 접근해 데이터를 조회하거나 저장한다.

DB
  -&amp;gt; 실제 데이터가 저장되는 공간이다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Spring을 붙이면 mock 데이터를 직접 만드는 방식에서 벗어나 실제 DB 기반의 데이터를 응답할 수 있게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 프론트엔드 입장에서 바뀌는 것과 바뀌지 않는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙인다고 해서 프론트엔드 컴포넌트가 전부 바뀌는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainPageClient는 여전히 useCareerList를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;const careerList = useCareerList(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCareerList는 여전히 데이터를 불러오고, items, loading, error 상태를 관리한다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;items
loading
error
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CareerSide는 여전히 전달받은 데이터를 화면에 렌더링한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;CareerSide
  error={careerList.error}
  items={careerList.items}
  loading={careerList.loading}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 화면을 구성하는 방식은 크게 바뀌지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바뀌는 것은 fetchCareerList가 요청을 보내는 대상이다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;기존
fetchCareerList -&amp;gt; Next route.ts

변경 후
fetchCareerList -&amp;gt; Spring Controller
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Spring을 붙일 때 가장 먼저 확인해야 하는 부분은 API 호출 함수다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 기존 구조와 Spring 적용 후 구조 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;브라우저
  -&amp;gt; Next 프론트엔드
    -&amp;gt; Next route.ts
      -&amp;gt; mock 데이터 생성
      -&amp;gt; JSON 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙인 뒤의 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;브라우저
  -&amp;gt; Next 프론트엔드
    -&amp;gt; Spring 백엔드 API
      -&amp;gt; Service
        -&amp;gt; Repository
          -&amp;gt; DB
      -&amp;gt; JSON 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 구조의 차이는 명확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Next 안에서 임시로 데이터를 만들었다.&lt;br /&gt;Spring을 붙이면 백엔드 서버가 따로 생기고, 실제 DB와 연결된 데이터를 내려준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. route.ts는 더 이상 필요 없을까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙이면 보통 route.ts는 필요 없어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 route.ts가 단순히 mock 데이터를 반환하는 역할이었다면, Spring Controller가 그 역할을 대체하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 항상 route.ts를 제거해야 하는 것은 아니다.&lt;br /&gt;프로젝트 구조에 따라 Next의 API Route를 중간 서버처럼 사용할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 구조도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;브라우저
  -&amp;gt; Next route.ts
    -&amp;gt; Spring API
      -&amp;gt; DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 route.ts는 브라우저와 Spring 서버 사이에서 중간 계층 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 일반적인 프론트엔드와 백엔드 분리 구조에서는 다음 흐름을 더 많이 사용한다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;브라우저
  -&amp;gt; Next 프론트엔드
    -&amp;gt; Spring 백엔드 API
      -&amp;gt; DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 코드에서는 route.ts가 mock API 역할을 하고 있었기 때문에, Spring을 붙인다면 이 부분이 Spring Controller로 대체된다고 보는 것이 가장 이해하기 쉽다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙이면 Next.js의 화면 구조가 완전히 바뀌는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainPageClient, useCareerList, fetchCareerList, CareerSide 같은 프론트엔드 흐름은 그대로 유지된다.&lt;br /&gt;다만 fetchCareerList가 요청을 보내는 목적지가 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Next 내부의 route.ts가 요청을 받았다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;fetchCareerList
  -&amp;gt; Next route.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 붙이면 Spring Controller가 요청을 받는다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;fetchCareerList
  -&amp;gt; Spring Controller
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Spring 내부에서는 Controller, Service, Repository, DB 순서로 요청을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 내용을 통해 Next.js와 Spring의 역할을 다음처럼 구분할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Next.js
-&amp;gt; 화면을 만들고 사용자와 상호작용한다.

React hook
-&amp;gt; 데이터의 상태를 관리한다.

API 함수
-&amp;gt; 서버에 요청을 보낸다.

Spring
-&amp;gt; 요청을 받아 비즈니스 로직을 처리하고 DB 데이터를 응답한다.

DB
-&amp;gt; 실제 데이터를 저장한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로그래밍/next.js</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/64</guid>
      <comments>https://ms-diary.tistory.com/64#entry64comment</comments>
      <pubDate>Sun, 7 Jun 2026 15:07:44 +0900</pubDate>
    </item>
    <item>
      <title>[커리큘럼 페이지 프로젝트] Next.js App Router에서 화면과 데이터 호출 흐름 이해하기</title>
      <link>https://ms-diary.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;커리큘럼 페이지를 리팩토링 하며 1인 개발에 도전하기로 했다. 그러기 위해선 Next.js를 이해할 필요가 있다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가장 궁금했던 부분은 Next.js가 어떤 흐름으로 동작하는지였다. 특히 &lt;/span&gt;&lt;span&gt;page.tsx&lt;/span&gt;&lt;span&gt;, 클라이언트 컴포넌트, hook, API 함수, &lt;/span&gt;&lt;span&gt;route.ts&lt;/span&gt;&lt;span&gt;가 각각 어떤 역할을 맡고 있는지 정리할 필요가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span&gt;현재 구조는 백엔드를 구축하기 이전에 next만을 이용한 구조입니다.&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span&gt;route.ts가 그 역할을 대신하고 있으니,&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span&gt;백엔드와 프론트엔드의 api호출 흐름까지 보기 위해선 다음 글도 보시면서 이해하면 좋을 것 같습니다.&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;이 프로젝트의 Next.js 데이터 흐름은 다음과 같이 정리할 수 있다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;app/page.tsx
  -&amp;gt; 서버에서 초기 데이터 생성
  -&amp;gt; MainPageClient에 initialData 전달
  -&amp;gt; MainPageClient가 useCareerList 실행
  -&amp;gt; useCareerList가 items/loading/error 상태 관리
  -&amp;gt; useEffect에서 load 실행
  -&amp;gt; load 안에서 fetchCareerList 호출
  -&amp;gt; fetchCareerList가 /api/careers/list로 POST 요청
  -&amp;gt; app/api/careers/list/route.ts의 POST 함수 실행
  -&amp;gt; JSON 응답 반환
  -&amp;gt; useCareerList가 setItems로 상태 업데이트
  -&amp;gt; CareerSide가 items를 화면에 렌더링&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전체 흐름을 한 문장으로 정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Next.js는 서버에서 초기 화면을 만들고, 브라우저에서 클라이언트 컴포넌트가 상태와 이벤트를 관리하며, 필요한 데이터는 내부 API Route를 통해 다시 호출하는 구조로 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 파일 위치가 곧 URL이 된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js App Router에서는 app 디렉터리 안의 파일 구조가 곧 라우팅 구조가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 파일은 / 경로의 페이지가 된다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;frontend/src/app/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export default function Home() {
  return &amp;lt;MainPageClient initialData={createMainMock()} /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app/page.tsx는 기본적으로 서버 컴포넌트다. 따라서 브라우저가 아니라 서버에서 먼저 실행된다. 이 코드에서는 서버에서 createMainMock()으로 초기 데이터를 만들고, 그 데이터를 MainPageClient에 initialData라는 prop으로 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 첫 화면에 필요한 기본 데이터는 서버에서 만들어지고, 그 결과가 클라이언트 컴포넌트로 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 서버 컴포넌트와 클라이언트 컴포넌트가 나뉜다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js App Router에서는 컴포넌트가 기본적으로 서버 컴포넌트로 동작한다. 반대로 파일 상단에 &quot;use client&quot;가 있으면 해당 컴포넌트는 클라이언트 컴포넌트가 된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;&quot;use client&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainPageClient.tsx에는 &quot;use client&quot;가 있기 때문에 브라우저에서 실행된다. 그래서 이 컴포넌트 안에서는 useState, useEffect, useMemo 같은 React hook을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조는 다음과 같이 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;app/page.tsx
  -&amp;gt; 서버에서 초기 데이터 생성
  -&amp;gt; MainPageClient에 initialData 전달
  -&amp;gt; 브라우저에서 상태, 이벤트, 추가 데이터 호출 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, page.tsx는 초기 화면을 준비하는 역할을 하고, MainPageClient는 사용자가 보는 화면의 상태와 동작을 관리하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. app/api 아래의 route.ts는 내부 API 서버처럼 동작한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 파일은 Next.js의 API Route다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;frontend/src/app/api/careers/list/route.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 경로가 다음과 같기 때문에 실제 URL은 /api/careers/list가 된다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;app/api/careers/list/route.ts
-&amp;gt; /api/careers/list
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 파일 안에서 POST 함수를 export하면, POST /api/careers/list 요청을 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export async function POST(request: Request) {
  const body = (await request.json().catch(() =&amp;gt; ({}))) as CareerListRequest

  const response: CareerListResponse = {
    code: 200,
    status: &quot;OK&quot;,
    message: &quot;커리어 사이드 목록 조회 성공&quot;,
    data: {
      items: createCareerListMock(body),
    },
  }

  return NextResponse.json(response)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 입장에서는 백엔드 API를 호출하는 것처럼 보이지만, 실제로는 같은 Next.js 프로젝트 안에 있는 route.ts가 요청을 받아 응답을 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MainPageClient는 hook을 통해 데이터를 사용한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainPageClient에서는 커리어 목록을 가져오기 위해 useCareerList라는 hook을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;const careerList = useCareerList(
  initialData.careerSide.items.map((career) =&amp;gt; ({
    careerId: career.id,
    name: career.name,
    category: {
      categoryId: career.categoryId,
      name: career.categoryName,
    },
    displayOrder: career.displayOrder,
  })),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 MainPageClient가 직접 fetch를 호출하지 않는다는 것이다. MainPageClient는 useCareerList를 호출하고, 그 결과로 items, loading, error 같은 상태를 받아 화면에 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;CareerSide
  error={careerList.error}
  items={careerList.items}
  loading={careerList.loading}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, MainPageClient는 화면 전체를 조립하는 컴포넌트이고, 실제 데이터 상태 관리는 useCareerList hook이 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. useCareerList는 데이터 상태를 관리한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCareerList는 커리어 목록 데이터를 React 상태로 관리하는 hook이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const [items, setItems] = useState&amp;lt;CareerListItem[]&amp;gt;(initialItems)
const [loading, setLoading] = useState(initialItems.length === 0)
const [error, setError] = useState&amp;lt;string | null&amp;gt;(null)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 관리하는 상태는 크게 세 가지다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;items   -&amp;gt; 화면에 보여줄 데이터
loading -&amp;gt; 데이터를 불러오는 중인지 여부
error   -&amp;gt; API 호출 중 에러가 발생했는지 여부
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 hook 안에는 데이터를 다시 불러오는 load() 함수가 있고, 컴포넌트가 브라우저에 처음 렌더링된 뒤 useEffect를 통해 load()가 실행된다.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  void load()
}, [load])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분 때문에 초기 데이터가 이미 있더라도 브라우저에서 다시 한 번 /api/careers/list를 호출하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 현재 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;서버에서 initialData로 첫 화면을 빠르게 그림
-&amp;gt; 브라우저가 실행됨
-&amp;gt; useEffect가 실행됨
-&amp;gt; API를 다시 호출함
-&amp;gt; 응답 결과로 items를 갱신함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 서버에서 받은 데이터로 화면이 바로 보이고, 이후 클라이언트에서 다시 API를 호출해 최신 상태로 갱신하는 구조라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. fetchCareerList는 실제 API 요청을 보내는 함수다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 헷갈렸던 부분은 useCareerList 다음에 왜 fetchCareerList가 나오는지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 역할이 다르다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;useCareerList   -&amp;gt; 데이터 상태를 관리하는 hook
fetchCareerList -&amp;gt; 실제 서버에 요청을 보내는 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MainPageClient가 직접 fetchCareerList를 부르는 것이 아니다. 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;MainPageClient 렌더링
  -&amp;gt; useCareerList 실행
    -&amp;gt; useEffect 실행
      -&amp;gt; load 실행
        -&amp;gt; fetchCareerList 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetchCareerList는 다음과 같이 API를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export async function fetchCareerList(payload = {}) {
  const res = await fetch(&quot;/api/careers/list&quot;, {
    method: &quot;POST&quot;,
    headers: {
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
    body: JSON.stringify(payload),
  })

  return await res.json()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 fetch(&quot;/api/careers/list&quot;)는 Next.js 내부 API Route를 호출한다. 즉, 이 요청은 다음 파일의 POST 함수로 연결된다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;frontend/src/app/api/careers/list/route.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 호출 관계를 다시 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;MainPageClient
  -&amp;gt; useCareerList
    -&amp;gt; load
      -&amp;gt; fetchCareerList
        -&amp;gt; fetch(&quot;/api/careers/list&quot;)
          -&amp;gt; app/api/careers/list/route.ts의 POST 함수
            -&amp;gt; JSON 응답
              -&amp;gt; setItems
                -&amp;gt; CareerSide 렌더링
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 화면, hook, API 함수, route.ts의 역할 구분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 이해하려면 각 파일의 역할을 분리해서 봐야 한다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;page.tsx
-&amp;gt; 서버에서 초기 페이지를 준비한다.

MainPageClient.tsx
-&amp;gt; 브라우저에서 실행되는 화면 컴포넌트다.
-&amp;gt; 상태와 이벤트가 필요한 컴포넌트를 포함한다.

useCareerList
-&amp;gt; 커리어 목록 데이터의 상태를 관리한다.
-&amp;gt; loading, error, items 같은 값을 제공한다.

fetchCareerList
-&amp;gt; 실제 API 요청을 보낸다.
-&amp;gt; fetch(&quot;/api/careers/list&quot;)를 실행한다.

route.ts
-&amp;gt; API 요청을 받는다.
-&amp;gt; 데이터를 만들고 JSON으로 응답한다.

CareerSide
-&amp;gt; 전달받은 데이터를 화면에 렌더링한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 보면 useCareerList와 fetchCareerList의 차이가 분명해진다. useCareerList는 상태 관리의 관점이고, fetchCareerList는 네트워크 요청의 관점이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Link는 Next.js 라우팅을 사용한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커리어 항목을 클릭할 때는 next/link를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;&amp;lt;Link href={`/careers/${item.careerId}`}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 careerId가 1이면 /careers/1로 이동한다. 이 URL은 다음 파일이 처리한다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;app/careers/[careerId]/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 [careerId]는 동적 라우트다. 대괄호로 감싼 폴더 이름은 URL 파라미터가 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/careers/1
/careers/2
/careers/3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 URL들이 모두 [careerId]에 매핑된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 이 코드의 전체 데이터 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트의 데이터 흐름은 다음과 같이 정리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;1. 사용자가 / 페이지에 접속한다.

2. app/page.tsx가 서버에서 실행된다.

3. createMainMock()으로 초기 데이터를 만든다.

4. MainPageClient에 initialData를 전달한다.

5. MainPageClient가 브라우저에서 실행된다.

6. useCareerList가 초기 데이터를 상태에 넣는다.

7. useEffect가 실행되면서 load()를 호출한다.

8. load() 안에서 fetchCareerList가 실행된다.

9. fetchCareerList가 /api/careers/list로 POST 요청을 보낸다.

10. app/api/careers/list/route.ts의 POST 함수가 요청을 처리한다.

11. JSON 응답이 돌아온다.

12. useCareerList가 setItems로 상태를 갱신한다.

13. CareerSide가 갱신된 items를 화면에 렌더링한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 이번에 이해한 핵심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 코드를 통해 이해한 핵심은 Next.js에서 화면과 데이터 호출이 한 곳에 섞여 있는 것이 아니라 역할별로 나뉘어 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page.tsx는 서버에서 초기 화면을 준비한다. &quot;use client&quot;가 붙은 컴포넌트는 브라우저에서 상태와 이벤트를 처리한다. hook은 데이터의 로딩, 에러, 결과 상태를 관리한다. API 함수는 실제 요청을 보낸다. route.ts는 그 요청을 받아 응답을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 useCareerList 다음에 왜 fetchCareerList가 이어지는지 헷갈렸지만, 지금은 둘의 역할을 구분해서 이해할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;useCareerList는 데이터를 관리하는 사람이고,
fetchCareerList는 서버에 실제로 요청을 보내는 사람이다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 프로젝트의 Next.js 구조는 다음과 같이 요약할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;서버에서 초기 화면을 만들고,
클라이언트에서 상태와 이벤트를 관리하며,
필요한 데이터는 내부 API Route를 통해 다시 호출하는 구조
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 이해하면 Next.js 프로젝트에서 page.tsx, 클라이언트 컴포넌트, custom hook, API 함수, route.ts가 각각 왜 필요한지 더 명확하게 볼 수 있다.&lt;/p&gt;</description>
      <category>프로그래밍/next.js</category>
      <category>next.js</category>
      <category>next초보</category>
      <category>프엔 걸음마</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/63</guid>
      <comments>https://ms-diary.tistory.com/63#entry63comment</comments>
      <pubDate>Sun, 7 Jun 2026 14:52:04 +0900</pubDate>
    </item>
    <item>
      <title>상반기 취준 회고: 누가 포기를 배추 셀 때 쓴다고 하였나</title>
      <link>https://ms-diary.tistory.com/62</link>
      <description>&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;상반기를 정리해봤다&lt;/h2&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;상반기가 거의 끝나간다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;br /&gt;&amp;ldquo;나 지금 제대로 하고 있는 건가?&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;뭔가 계속 하긴 했다.&lt;br /&gt;근데 결과가 딱 보이는 건 많지 않았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그래서 한 번 정리해보기로 했다.&lt;br /&gt;나중에 보면 이 시기도 분명 의미가 있을 것 같아서.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_8056.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFkQdv/dJMcagL5RSI/hAzqpA4HB15o4IrXp4kbBk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFkQdv/dJMcagL5RSI/hAzqpA4HB15o4IrXp4kbBk/img.jpg&quot; data-alt=&quot;바람 쐴 때 넣고 다니는 친구&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFkQdv/dJMcagL5RSI/hAzqpA4HB15o4IrXp4kbBk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFkQdv%2FdJMcagL5RSI%2FhAzqpA4HB15o4IrXp4kbBk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;566&quot; height=&quot;755&quot; data-filename=&quot;IMG_8056.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;바람 쐴 때 넣고 다니는 친구&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;1월, 일단 어학부터&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;올해 초에는 오픽을 봤다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;취업 준비를 하려면&lt;br /&gt;어학 기준은 맞춰두는 게 좋을 것 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;1월 7일에 시험을 봤고,&lt;br /&gt;&lt;b&gt;다행히 내가 원하던 최저 기준은 맞췄다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;엄청 대단한 성과라고 하긴 어렵지만,&lt;br /&gt;그래도 하나는 끝냈다는 느낌이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;2월까지는 알바도 했다&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;작년 10월부터 2월까지는&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;뮤직바와 약국에서 주 4-5일 알바를 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;취준을 하려면 시간도 필요하지만&lt;br /&gt;당장 생활비도 필요했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;취준 비용이 은근히 많이 든다.&lt;/b&gt;&lt;br /&gt;교통비, 카페, 식비, 시험비 같은 것들이 계속 쌓인다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그래서 약간 도토리 모으듯이&lt;br /&gt;먹고 살 돈을 모으다가 그만뒀다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;알고리즘 공부&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그리고 백준으로 알고리즘 공부를 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;코딩테스트를 대비해야 한다는 생각은 계속 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;br /&gt;&lt;b&gt;그래서 구현, BFS, DFS, DP 같은 문제들을 풀었다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;문제를 풀면서 느낀 건&lt;br /&gt;내가 아직 많이 부족하다는 거였다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;그리고 개발자가 되어서 개발을 할 것이라면&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;취업 이후에도 꾸준히 문제를 풀어야 할 것 같다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;워낙 손으로 코드 짜는 시대가 저물고 있기에,&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;직접 구현가능한 인적자원은 뭔가&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;유기농 개발자 느낌(?)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;여튼 알고리즘 중&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;구현 문제는&lt;br /&gt;알고리즘을 모르는 것보다&lt;br /&gt;생각을 코드로 옮기는 과정에서 많이 막혔다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그래도 계속 풀다 보니까&lt;br /&gt;조금씩 문제를 나누어 보는 감각은 생긴 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;3월부터 본격 취준&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;3월부터는 본격적으로 공채를 쓰기 시작했다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;자소서를 쓰고, 고치고, 다시 쓰고,&lt;br /&gt;직무를 보고 내 경험을 맞춰보고,&lt;br /&gt;또 떨어지고, 다시 쓰는 과정이 반복됐다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;결과는 좋지 않다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;내 경험이 부족한 건지,&lt;br /&gt;표현을 못 하는 건지,&lt;br /&gt;&lt;b&gt;직무 선택이 잘못된 건지 계속 고민하게 됐다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;결과만 놓고 본다면 엔지니어의 직무보다는 기획이 섞여있는 직무에서&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;훨씬 더 높은 서합률을 보여주는 걸 보고&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;b&gt;사실 나도 이런걸 하고 싶었던걸지도?&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;b&gt;모르겠다 아직까지는 요즘 제일 큰 고민이다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;면접도 두 번 봤다&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;상반기에는 2번의 면접을 보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;외국계 하나, 중견 하나&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;항상 면접을 준비하며 느끼는거지만&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;면접의 언어는 영어 공부를 하는 것과 같아서,&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;익숙하지 않은 언어를 여러번 뱉어보는게 중요한 것 같다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그리고&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그냥 &amp;ldquo;했다&amp;rdquo;가 아니라&lt;br /&gt;왜 했고, 어떤 기준으로 판단했고, 결과가 어땠는지까지&lt;br /&gt;말할 수 있어야 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;결과가 안좋았던 이유는,&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;1. 질문에 맞는 답을 해야한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;듣기 좋은 답이 아니라 물어본 질문에 답을 해야한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;2. 기술질문 대비 미미&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;직무마다 다르겠지만, 스스로 생각하기에 CS지식이 필요한 직무라면&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;CS문제는 어느정도 대비를 해야할 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageslideblock alignCenter&quot; data-image=&quot;[{&amp;quot;src&amp;quot;:&amp;quot;https://blog.kakaocdn.net/dn/n20of/dJMcadV4B3R/ZKm5bfkRRxXtOoSEiP2io1/img.jpg&amp;quot;},{&amp;quot;src&amp;quot;:&amp;quot;https://blog.kakaocdn.net/dn/lMnpJ/dJMcabRwePV/8EP6kSh2oKN3PHijAsotbK/img.jpg&amp;quot;},{&amp;quot;src&amp;quot;:&amp;quot;https://blog.kakaocdn.net/dn/MFDdL/dJMcadV4B3J/iEfldhDXI8QzeB44z6V6U1/img.jpg&amp;quot;},{&amp;quot;src&amp;quot;:&amp;quot;https://blog.kakaocdn.net/dn/YHdlO/dJMcadV4B3W/qpgOXqZSgeR2HpUfI9zPLk/img.jpg&amp;quot;}]&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span class=&quot;image-wrap selected&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n20of/dJMcadV4B3R/ZKm5bfkRRxXtOoSEiP2io1/img.jpg&quot; data-url=&quot;https://blog.kakaocdn.net/dn/n20of/dJMcadV4B3R/ZKm5bfkRRxXtOoSEiP2io1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n20of/dJMcadV4B3R/ZKm5bfkRRxXtOoSEiP2io1/img.jpg&quot; loading=&quot;lazy&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn20of%2FdJMcadV4B3R%2FZKm5bfkRRxXtOoSEiP2io1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot;/&gt;&lt;/span&gt;&lt;span class=&quot;image-wrap &quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lMnpJ/dJMcabRwePV/8EP6kSh2oKN3PHijAsotbK/img.jpg&quot; data-url=&quot;https://blog.kakaocdn.net/dn/lMnpJ/dJMcabRwePV/8EP6kSh2oKN3PHijAsotbK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lMnpJ/dJMcabRwePV/8EP6kSh2oKN3PHijAsotbK/img.jpg&quot; loading=&quot;lazy&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlMnpJ%2FdJMcabRwePV%2F8EP6kSh2oKN3PHijAsotbK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot;/&gt;&lt;/span&gt;&lt;span class=&quot;image-wrap &quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MFDdL/dJMcadV4B3J/iEfldhDXI8QzeB44z6V6U1/img.jpg&quot; data-url=&quot;https://blog.kakaocdn.net/dn/MFDdL/dJMcadV4B3J/iEfldhDXI8QzeB44z6V6U1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MFDdL/dJMcadV4B3J/iEfldhDXI8QzeB44z6V6U1/img.jpg&quot; loading=&quot;lazy&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMFDdL%2FdJMcadV4B3J%2FiEfldhDXI8QzeB44z6V6U1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot;/&gt;&lt;/span&gt;&lt;span class=&quot;image-wrap &quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YHdlO/dJMcadV4B3W/qpgOXqZSgeR2HpUfI9zPLk/img.jpg&quot; data-url=&quot;https://blog.kakaocdn.net/dn/YHdlO/dJMcadV4B3W/qpgOXqZSgeR2HpUfI9zPLk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YHdlO/dJMcadV4B3W/qpgOXqZSgeR2HpUfI9zPLk/img.jpg&quot; loading=&quot;lazy&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYHdlO%2FdJMcadV4B3W%2FqpgOXqZSgeR2HpUfI9zPLk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot;/&gt;&lt;/span&gt;&lt;button class=&quot;btn btn-prev&quot;&gt;&lt;span class=&quot;ico-prev&quot;&gt;이전&lt;/span&gt;&lt;/button&gt;&lt;button class=&quot;btn btn-next&quot;&gt;&lt;span class=&quot;ico-next&quot;&gt;다음&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;
  &lt;div class=&quot;mark&quot;&gt;&lt;span data-index=&quot;0&quot;&gt;0&lt;/span&gt;&lt;span data-index=&quot;1&quot;&gt;1&lt;/span&gt;&lt;span data-index=&quot;2&quot;&gt;2&lt;/span&gt;&lt;span data-index=&quot;3&quot;&gt;3&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;상반기의 일상&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;상반기가 끝나가면서&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;지금은 일경험도 지원하고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;상반기가 거의 끝나가는데&lt;br /&gt;아직 확실한 결과가 없다 보니까&lt;br /&gt;&lt;b&gt;어디서부터 잘못된 건지 계속 생각하게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;자소서&lt;/b&gt;를 못 쓰는 건지,&lt;br /&gt;&lt;b&gt;직무&lt;/b&gt;를 너무 넓게 보고 있는 건지,&lt;br /&gt;&lt;b&gt;기술이&lt;/b&gt; 애매한 건지,&lt;br /&gt;아니면 그냥 &lt;b&gt;시간&lt;/b&gt;이 더 필요한 건지.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;정답은 아직 잘 모르겠다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;다만 계속 멈춰 있으면&lt;br /&gt;더 불안해질 것 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그래서 미뤄왔던 커리큘럼 페이지 리팩토링을 다시 해보려고 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;커리큘럼 페이지 리팩토링&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;예전에 만들었던 커리큘럼 페이지는&lt;/b&gt;&lt;br /&gt;&lt;b&gt;나에게 꽤 중요한 프로젝트다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;처음으로 실제 사용자를 생각하면서 만들었고,&lt;br /&gt;배포와 운영까지 해봤던 서비스였다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;다만 지금 보면 아쉬운 부분도 많다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;구조도 더 다듬고 싶고,&lt;br /&gt;화면도 다시 만들고 싶고,&lt;br /&gt;관리하기 쉬운 형태로 바꾸고 싶다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;이번에는 프론트를 Next.js로 해보려고 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;예전부터 써보고 싶었고,&lt;br /&gt;금융권을 희망한다면&lt;br /&gt;이번 기회에 제대로 익혀보고 싶다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;혼자 하는 작업이라 쉽지는 않겠지만&lt;br /&gt;그래도 내가 직접 운영했던 서비스를 다시 개선하는 거라&lt;br /&gt;의미는 있을 것 같다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;정리하면서&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;상반기를 돌아보면&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;솔직한 마음으로는 실패라고 얘기하고 싶다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;결과가 남은 것도 없고,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;조급함은 아직 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그래도 지금은&lt;br /&gt;내가 뭘 잘못했는지만 생각하기보다&lt;br /&gt;다시 움직일 수 있는 걸 하나씩 만드는 게 먼저인 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;하반기에는 결과가 조금 더 남았으면 좋겠다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;그리고 그 결과가&lt;br /&gt;단순히 합격 하나만이 아니라&lt;br /&gt;내가 어떤 사람인지 더 분명하게 보여주는 과정이었으면 좋겠다.&lt;/p&gt;</description>
      <category>그 외 등등</category>
      <category>대졸무직백수</category>
      <category>취준</category>
      <category>취준생</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/62</guid>
      <comments>https://ms-diary.tistory.com/62#entry62comment</comments>
      <pubDate>Mon, 27 Apr 2026 13:25:09 +0900</pubDate>
    </item>
    <item>
      <title>당신은 무엇을 오래 사랑하십니까 - AI와 나의 헤리티지</title>
      <link>https://ms-diary.tistory.com/61</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 거드럭대는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 써보려합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;000039.JPG&quot; data-origin-width=&quot;2735&quot; data-origin-height=&quot;1830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFjYkE/dJMcadasv6A/Akl7nRTRx4bl87ktNH8rRk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFjYkE/dJMcadasv6A/Akl7nRTRx4bl87ktNH8rRk/img.jpg&quot; data-alt=&quot;상수동 책방에서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFjYkE/dJMcadasv6A/Akl7nRTRx4bl87ktNH8rRk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFjYkE%2FdJMcadasv6A%2FAkl7nRTRx4bl87ktNH8rRk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;426&quot; data-filename=&quot;000039.JPG&quot; data-origin-width=&quot;2735&quot; data-origin-height=&quot;1830&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상수동 책방에서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AI 시대에 &amp;lsquo;나&amp;rsquo;를 만든다는 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘에는 문장을 쓰는 일보다, 그 문장이 정말 나에게서 비롯된 것인지 의심하는 일이 더 잦아졌습니다. 또한 그것이 진정 나에게서 비롯된 것이 아님을 알고 있습니다. 이제 우리는 어떤 문장도 혼자 끝내지 않습니다. 쓰는 동시에 다듬고, 다듬는 동시에 의심하며, 의심하는 동시에 더 나은 표현을 권유받습니다. 분명 더 정확하고 더 매끄러운 문장을 얻게 되었지만, 그와 함께 조금씩 잃어버리는 것도 있는 것 같습니다. &lt;b&gt;말이 나에게서 나왔다는 감각, 생각이 내 안에서 '오래' 머물다 비로소 문장이 되었다는 고유한 시간의 감각입니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;766&quot; data-start=&quot;470&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;766&quot; data-start=&quot;470&quot; data-ke-size=&quot;size16&quot;&gt;AI의 발전은 이 감각의 지형을 바꾸고 있습니다. 생산은 쉬워졌고, 정리는 빨라졌으며, 표현은 이전보다 매끈해졌습니다. 그러나 바로 그 효율성 때문에 오히려 &amp;lsquo;나&amp;rsquo;라는 존재의 경계는 희미해집니다. 예전에는 서툰 문장 속에서도 한 사람의 리듬과 고집이 읽혔지만, 지금은 누구나 일정 수준 이상의 문장을 빠르게 얻을 수 있게 되었고, 그 결과 잘 쓰인 문장이 곧 그 사람을 증명해 주던 시대는 서서히 저물고 있다 생각합니다. &lt;b&gt;이제 문장의 완성도만으로는 한 사람의 내면을 판별하기 어렵다.&lt;/b&gt; 말은 정교해졌지만, 그 말의 출처는 오히려 더 불분명해집니다.&lt;/p&gt;
&lt;p data-end=&quot;766&quot; data-start=&quot;470&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1249&quot; data-start=&quot;768&quot; data-ke-size=&quot;size16&quot;&gt;그래서 기록에 대해서도 다시 생각하게 됩니다. 우리는 오랫동안 기록을 미덕처럼 여겨왔습니다. 써두면 남고, 남으면 축적되며, 축적된 것은 결국 자신을 증명해 줄 것이라 믿었습니다. 그러나 &lt;b&gt;기록되어 있다는 사실만으로 그것이 곧 나의 생각이 되는 것은 아닙니다.&lt;/b&gt; 단편적인 문장들은 얼마든지 만들어질 수 있고, 감정조차 형식적으로 재현될 수 있습니다. 오늘의 기술은 기억을 보존하는 데 그치지 않고, 기억처럼 보이는 형식을 생산하는 데에도 능숙합니다. 그런 시대에 중요한 것은 얼마나 많이 남겼는가가 아니라, &lt;b&gt;그 기록들 사이에 어떤 문제의식이 흐르고 있는가입니다&lt;/b&gt;. 내 생각은 하나의 문장 안에 있는 것이 아니라, 오랜 시간 반복해서 붙들어 온 질문의 방향 속에 있습니다. 무엇을 여러 번 궁금해했는지, 무엇 앞에서 쉽게 결론짓지 못했는지, 무엇을 끝내 포기하지 않았는지. &lt;b&gt;어쩌면 한 사람은 자신이 남긴 결과물보다 자신이 오래 붙들어 온 질문들로 더 정확히 설명됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1249&quot; data-start=&quot;768&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1731&quot; data-start=&quot;1251&quot; data-ke-size=&quot;size16&quot;&gt;전문성도 비슷합니다.&lt;br /&gt;한때 전문성은 인간이 오랜 시간 공들여 쌓아 올린 고유한 자산처럼 여겨졌습니다. 어떤 분야를 오래 공부하고, 거기서 숙련을 만들고, 그에 맞는 보수를 받는 건 아주 자연스러운 일이었습니다. 그런데 이제는 지식의 접근, 정리, 요약, 응용의 많은 부분을 AI가 빠르게 대신합니다. 인간이 공들여 외우고 익혀 온 것들이 몇 초 안에 추출되고 재배열되는 모습을 보면, 가끔은 허탈할 정도 입니다. 내가 애써 쌓아 온 것이 생각보다 너무 쉽게 복제되는 것처럼 보이기도 합니다.&lt;/p&gt;
&lt;p data-end=&quot;2209&quot; data-start=&quot;1892&quot; data-ke-size=&quot;size16&quot;&gt;그렇다고 전문성이 완전히 사라지는 건 아닐 것입니다. 다만 &lt;b&gt;전문성의 자리가 조금 옮겨 가는 것&lt;/b&gt; 같습니다. 단순히 많이 아는 것, 빨리 정리하는 것만으로는 예전 같은 설득력을 갖기 어려워집니다. 앞으로 더 중요해지는 건 서로 멀리 떨어져 있던 것들 사이에서 관계를 발견하는 힘, 무엇이 중요한지 가려내는 감각, 그리고 자기 판단에 책임을 지는 태도일 것입니다. 정보의 속도에서는 인간이 기계를 따라잡기 어렵습니다. 그렇다면 인간은 이제 &lt;b&gt;속도가 아니라 맥락&lt;/b&gt;, &lt;b&gt;정답이 아니라 방향&lt;/b&gt;, &lt;b&gt;효율이 아니라 책임&lt;/b&gt;의 자리에서 자기 가치를 다시 설명해야 하는 것 아닐까.&lt;/p&gt;
&lt;p data-end=&quot;2209&quot; data-start=&quot;1892&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;2209&quot; data-start=&quot;1892&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;000037.JPG&quot; data-origin-width=&quot;2735&quot; data-origin-height=&quot;1830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1r9Dc/dJMcahcUKhI/l83V369WzovYgzUL4N555K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1r9Dc/dJMcahcUKhI/l83V369WzovYgzUL4N555K/img.jpg&quot; data-alt=&quot;상수동 모 위스키바에서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1r9Dc/dJMcahcUKhI/l83V369WzovYgzUL4N555K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1r9Dc%2FdJMcahcUKhI%2Fl83V369WzovYgzUL4N555K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;471&quot; data-filename=&quot;000037.JPG&quot; data-origin-width=&quot;2735&quot; data-origin-height=&quot;1830&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상수동 모 위스키바에서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;2209&quot; data-start=&quot;1892&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2209&quot; data-start=&quot;1892&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2209&quot; data-start=&quot;1892&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 AI 시대에 &amp;lsquo;나&amp;rsquo;를 만든다는 것은 무엇일까.&lt;br /&gt;나는 그게 &lt;b&gt;자신을 넓게, 오래, 조금 느리게 축적해 가는 일&lt;/b&gt;이라고 생각합니다. 여기서 말하는 축적은 자신을 요란하게 홍보하는 일이 아닙니다. 관심사를 많이 나열하는 일도 아닙니다. 중요한 건 흩어진 관심사들을 자기만의 문제의식으로 조금씩 묶어 가는 일입니다. 기술을 공부하면서도 사람에 대한 관심을 놓지 않는 것, 생산성을 높이면서도 취향과 감각을 같이 데리고 가는 것, 실용적인 선택을 하면서도 끝내 자기만의 질문을 잃지 않는 것. 결국 브랜딩이라는 것도 거창한 말처럼 들리지만, 따지고 보면 &lt;b&gt;내가 어떤 것에 오래 붙잡히는 사람인지를 보여 주는 일&lt;/b&gt;에 더 가까운 것 같다는 생각이듭니다.&lt;/p&gt;
&lt;p data-end=&quot;2209&quot; data-start=&quot;1892&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2904&quot; data-start=&quot;2570&quot; data-ke-size=&quot;size16&quot;&gt;그래서 앞으로는 단편적인 성취보다 &lt;b&gt;축적된 흔적&lt;/b&gt;이 더 중요해질 것입니다. 블로그, 유튜브, 출간된 글, 프로젝트, 작업물, 운영해 온 서비스 같은 것들. 이런 것들은 단순히 뭘 해냈다는 증명서가 아니라, 한 사람이 어떤 시간을 살아왔는지를 보여 주는 물증에 가깝습니다. 오늘 잘 만든 하나보다, 몇 년에 걸쳐 비슷한 문제를 다른 방식으로 다뤄 온 흔적이 더 강하게 사람을 설명할 수 있습니다. 사람들은 점점 더 완성된 답변 그 자체보다, &lt;b&gt;그 답변을 만들게 한 사람의 시간과 맥락&lt;/b&gt;을 보게 될 것이라 생각합니다. 무엇을 만들었는가 못지않게, 어떤 사람이라서 그런 것을 만들게 되었는가를 궁금해하게 될 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;2904&quot; data-start=&quot;2570&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2904&quot; data-start=&quot;2570&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3166&quot; data-start=&quot;2906&quot; data-ke-size=&quot;size16&quot;&gt;어쩌면 AI 시대에 필요한 것은 AI를 이기겠다는 발상 자체가 아닐지도 모르겠습니다. 기계를 더 빨리 따라가거나 더 효율적으로 흉내 내는 일은 처음부터 인간에게 썩 유리한 싸움이 아니기 때문입니다. 대신 인간은 인간만이 오래 쌓을 수 있는 것을 쌓아야 합니다. 오래 지속된 관심, 쉽게 대체되지 않는 취향, 반복된 실패와 수정, 자기만의 호흡으로 남겨 온 흔적들. 그런 것들은 당장 눈에 띄는 생산성으로 환산되지는 않지만, 결국 &lt;b&gt;한 사람을 그 사람으로 남게 하는 자산&lt;/b&gt;이 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;3166&quot; data-start=&quot;2906&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3166&quot; data-start=&quot;2906&quot; data-ke-size=&quot;size16&quot;&gt;우리는 점점 더 잘 정리된 말 속에서도 진짜 사람을 찾게 될 것입니다. 그리고 그때 한 사람을 증명하는 것은 오랜 시간 자기 방식으로 쌓아 온 이력, 다시 말해 자기만의 헤리티지일 것입니다. AI 시대에 인간의 가치는 사라지는 것이 아니라, 오히려 더 까다롭게 질문받게 되고 있습니다. 그러니 이제 중요한 것은 잘 생산하는 것보단, &lt;b&gt;무엇을 오래 사랑했는지, 무엇을 쉽게 넘기지 못했는지, 무엇을 끝내 자기 것으로 남겼는지&lt;/b&gt;일 것이겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;3166&quot; data-start=&quot;2906&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3559&quot; data-start=&quot;3511&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lsquo;나&amp;rsquo;는 단번에 생성되는 것이 아니라, 오래 축적된 끝에 겨우 읽히는 것이니까.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3559&quot; data-start=&quot;3511&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_7840.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dk9HIy/dJMcaiJCRjP/LKraox1YSZzQPXBq37nBNk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dk9HIy/dJMcaiJCRjP/LKraox1YSZzQPXBq37nBNk/img.jpg&quot; data-alt=&quot;도림천&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dk9HIy/dJMcaiJCRjP/LKraox1YSZzQPXBq37nBNk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdk9HIy%2FdJMcaiJCRjP%2FLKraox1YSZzQPXBq37nBNk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;715&quot; data-filename=&quot;IMG_7840.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;도림천&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>그 외 등등</category>
      <category>AI</category>
      <category>거드럭거드럭</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/61</guid>
      <comments>https://ms-diary.tistory.com/61#entry61comment</comments>
      <pubDate>Wed, 8 Apr 2026 15:52:37 +0900</pubDate>
    </item>
    <item>
      <title>[코딩테스트, 더 이상 미룰 수 없다] BOJ 14891 - 구현, 실행의 판단 파트와 실제 실행파트 구분하기</title>
      <link>https://ms-diary.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;판단과 실행을 분리해야 한다는 점을 깨닫기까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서 &amp;ldquo;1번 톱니를 회전시킨다&amp;rdquo;는 말은&lt;br /&gt;&lt;b&gt;1번 톱니를 회전의 시작점으로 삼는다&lt;/b&gt;는 뜻에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 실제 동작 순서는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 상태를 기준으로 인접한 톱니끼리 맞닿은 극을 비교한다.&lt;/li&gt;
&lt;li&gt;어떤 톱니가 어느 방향으로 회전할지 결정한다.&lt;/li&gt;
&lt;li&gt;회전 여부가 모두 결정된 뒤, 실제 회전을 한꺼번에 적용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;비교는 현재 상태로 하고, 회전은 나중에 한다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 바로 회전하면 안 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;처음에는 DFS로 인접 톱니를 타고 들어가면서 바로 회전시키면 될 것 같았다.&lt;/b&gt;&lt;br /&gt;하지만 이 방식에는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 1번 톱니를 먼저 회전시켜 버리면, 1번 톱니의 배열이 바뀐다.&lt;br /&gt;그러면 그 다음에 2번 톱니와의 접점을 비교할 때는 원래 상태가 아니라 &lt;b&gt;이미 회전된 상태&lt;/b&gt;를 기준으로 비교하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제는 회전 전 상태를 기준으로 연쇄 회전 여부를 판단해야 한다.&lt;br /&gt;즉, DFS를 쓰더라도 다음과 같이 해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DFS에서는 실제 회전을 하지 않는다.&lt;/li&gt;
&lt;li&gt;각 톱니가 회전해야 하는지, 회전한다면 어느 방향인지 rotate 배열에 기록만 한다.&lt;/li&gt;
&lt;li&gt;DFS가 끝난 뒤 rotate 배열을 바탕으로 실제 회전을 적용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;판단 단계와 실행 단계를 분리해야 하는 시뮬레이션 문제가 있다는 사실을 알게 되었다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 문제를 통해 정리한 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 이해하면서 자연스럽게 하나의 기준도 생겼다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 상태를 바꾸면, 아직 처리하지 않은 다른 대상의 판단 기준이 바뀌는가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 대한 답이 &amp;ldquo;그렇다&amp;rdquo;라면, 판단과 실행을 분리해야 할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톱니바퀴 문제는 여기에 정확히 해당한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 톱니를 먼저 돌려 버리면&lt;/li&gt;
&lt;li&gt;그 톱니의 2번, 6번 위치 값이 달라지고&lt;/li&gt;
&lt;li&gt;그 결과 다음 톱니의 회전 여부 판단이 바뀔 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 문제는 반드시 다음 구조를 따라야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회전 여부 판단&lt;/li&gt;
&lt;li&gt;회전 방향 기록&lt;/li&gt;
&lt;li&gt;마지막에 한꺼번에 회전&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회전 방향을 기록하는 rotate 배열&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 구현할 때 중요한 도구가 회전 방향을 기록하는 배열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 톱니가 4개라면 다음처럼 둘 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;rotate = [0] * 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 값의 의미는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0: 회전하지 않음&lt;/li&gt;
&lt;li&gt;1: 시계 방향 회전&lt;/li&gt;
&lt;li&gt;-1: 반시계 방향 회전&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 rotate = [1, -1, 1, 0] 이라면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1번 톱니는 시계 방향&lt;/li&gt;
&lt;li&gt;2번 톱니는 반시계 방향&lt;/li&gt;
&lt;li&gt;3번 톱니는 시계 방향&lt;/li&gt;
&lt;li&gt;4번 톱니는 회전하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 배열을 두면 DFS에서는 회전 방향만 결정하면 되고,&lt;br /&gt;모든 탐색이 끝난 후 실제 회전은 따로 처리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DFS로 회전 전파를 확인하는 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 톱니가 1차원으로 연결되어 있기 때문에 꼭 DFS가 필요한 것은 아니다.&lt;br /&gt;왼쪽으로 한 번, 오른쪽으로 한 번 훑는 방식으로도 충분히 풀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &amp;ldquo;회전이 인접한 톱니로 전파된다&amp;rdquo;는 구조만 보면 DFS로도 충분히 표현할 수 있다.&lt;br /&gt;다만 중요한 점은 &lt;b&gt;DFS 안에서 실제 회전을 하면 안 된다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DFS의 역할은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 톱니의 회전 방향을 rotate[idx]에 기록한다.&lt;/li&gt;
&lt;li&gt;왼쪽 톱니와 맞닿은 극이 다르면, 반대 방향으로 DFS를 호출한다.&lt;/li&gt;
&lt;li&gt;오른쪽 톱니와 맞닿은 극이 다르면, 반대 방향으로 DFS를 호출한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DFS는 전파 여부를 확인하는 용도로만 쓰인다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;접점을 비교하는 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 인접 톱니끼리 비교해야 하는 위치는 고정되어 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 톱니의 오른쪽 접점: 인덱스 2&lt;/li&gt;
&lt;li&gt;왼쪽 톱니의 왼쪽 접점: 인덱스 6&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왼쪽 톱니와 비교할 때는 gears[idx][6] 과 gears[left][2]&lt;/li&gt;
&lt;li&gt;오른쪽 톱니와 비교할 때는 gears[idx][2] 와 gears[right][6]&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 비교하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;극이 다르면 회전이 전파되고, 같으면 전파가 끊긴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 위의 사고를 그대로 반영한 DFS 방식의 구현이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;from collections import deque
import sys
input = sys.stdin.readline

gears = [deque(input().strip()) for _ in range(4)]
k = int(input())

def dfs(idx, direction, visited, rotate):
    visited[idx] = True
    rotate[idx] = direction

    left = idx - 1
    right = idx + 1

    if left &amp;gt;= 0 and not visited[left]:
        if gears[idx][6] != gears[left][2]:
            dfs(left, -direction, visited, rotate)

    if right &amp;lt; 4 and not visited[right]:
        if gears[idx][2] != gears[right][6]:
            dfs(right, -direction, visited, rotate)

for _ in range(k):
    num, direction = map(int, input().split())
    num -= 1

    visited = [False] * 4
    rotate = [0] * 4

    dfs(num, direction, visited, rotate)

    for i in range(4):
        if rotate[i] != 0:
            gears[i].rotate(rotate[i])

score = 0
for i in range(4):
    if gears[i][0] == '1':
        score += (1 &amp;lt;&amp;lt; i)

print(score)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 문제를 통해 얻은 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 &amp;ldquo;어떻게 회전하지?&amp;rdquo;에 집중했다면,&lt;br /&gt;풀고 나서는 &amp;ldquo;언제 상태를 바꾸면 안 되는가?&amp;rdquo;를 생각하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 감각은 톱니바퀴 문제 하나에만 필요한 것이 아니다.&lt;br /&gt;시뮬레이션 문제 전반에서 매우 자주 등장하는 사고 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번 상태를 기준으로 다음 상태를 만들어야 하는 문제&lt;/li&gt;
&lt;li&gt;동시에 여러 대상이 변하는 문제&lt;/li&gt;
&lt;li&gt;순서대로 처리하면 결과가 달라질 수 있는 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서는 대부분 같은 고민이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 점에서 이 문제는 단순 구현 문제라기보다는,&lt;br /&gt;시뮬레이션 문제에서 &lt;b&gt;판단과 실행을 어떻게 분리해서 생각해야 하는지&lt;/b&gt;를 훈련하게 해주는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로그래밍/코딩 테스트, 더 이상 미룰 수 없다</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/60</guid>
      <comments>https://ms-diary.tistory.com/60#entry60comment</comments>
      <pubDate>Thu, 2 Apr 2026 13:28:17 +0900</pubDate>
    </item>
    <item>
      <title>[코딩테스트, 더 이상 미룰 수 없다] BOJ 14501 - 코드를 외워둘 만한 대표적 DP 문제</title>
      <link>https://ms-diary.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;14501은 DP를 공부할 때 꼭 한 번 정리해둘 만한 문제였다.&lt;br /&gt;코드 길이도 길지 않고, 상태 정의와 점화식이 분명해서 기본기를 익히기에 좋다.&lt;br /&gt;앞으로 비슷한 유형의 문제를 만났을 때 떠올리기 쉬운 형태이기도 하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 기억해둘 만한 문제인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 매 날짜마다 선택이 하나씩 주어진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오늘 상담을 한다&lt;/li&gt;
&lt;li&gt;오늘 상담을 하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 두 선택 중 더 좋은 결과를 고르면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 구조가 단순해서 DP의 핵심이 잘 보인다.&lt;br /&gt;현재 위치에서 어떤 선택을 할 수 있는지, 그 선택이 다음 상태에 어떤 영향을 주는지를 그대로 식으로 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 형태는 다른 문제에서도 자주 나온다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 일을 수행할지 말지 고르는 경우&lt;/li&gt;
&lt;li&gt;특정 구간을 사용할지 건너뛸지 정하는 경우&lt;/li&gt;
&lt;li&gt;현재 선택 때문에 다음에 가능한 날짜나 위치가 달라지는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 14501은 한 문제 풀이로 끝내기보다, 선택형 DP의 기본 예제로 기억해두기 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심은 dp 정의다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 dp를 어떻게 정의하느냐가 가장 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 이렇게 두면 가장 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dp[i] = i번째 날부터 퇴사일까지 얻을 수 있는 최대 수익&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정의하면 i번째 날에서 할 수 있는 일은 간단해진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;오늘 상담을 하지 않는다&lt;br /&gt;&amp;rarr; 다음 날로 넘어간다&lt;br /&gt;&amp;rarr; dp[i+1]&lt;/li&gt;
&lt;li&gt;오늘 상담을 한다&lt;br /&gt;&amp;rarr; 상담이 끝난 날 다음으로 넘어간다&lt;br /&gt;&amp;rarr; p[i] + dp[i+t[i]]&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 상담이 퇴사 전에 끝나야 하므로&lt;br /&gt;i + t[i] &amp;lt;= n 조건을 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점화식은 다음처럼 정리된다.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;if i + t[i] &amp;lt;= n:
    dp[i] = max(dp[i+1], p[i] + dp[i+t[i]])
else:
    dp[i] = dp[i+1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 식이 이 문제의 핵심이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 뒤에서부터 채우는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 바텀업으로 풀 때 뒤에서부터 앞으로 채운다. ( 그냥 웬만하면 바텀업으로..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 dp[i]를 구할 때 미래 값이 필요하기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dp[i+1]&lt;/li&gt;
&lt;li&gt;dp[i+t[i]]&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값들이 먼저 계산되어 있어야 현재 값을 정할 수 있다.&lt;br /&gt;그래서 뒤에서 앞으로 오는 방식이 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 통해 DP는 항상 앞에서부터 누적하는 것이 아니라는 점도 함께 익힐 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 문제에서 배울 수 있는 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;14501은 기본적인 DP 사고를 연습하기 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 상태를 어떻게 정의할지 생각해야 하고,&lt;br /&gt;그다음 현재 선택지를 정리하고,&lt;br /&gt;각 선택이 어디로 이어지는지 확인하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 풀면서 익혀둘 만한 포인트는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dp[i]의 의미를 정확히 정하기&lt;/li&gt;
&lt;li&gt;현재 선택지를 두 가지로 나누기&lt;/li&gt;
&lt;li&gt;각 선택 뒤에 도착하는 다음 상태 찾기&lt;/li&gt;
&lt;li&gt;현재 값은 다음 상태들의 결과로 결정된다는 점 이해하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 다른 DP 문제를 풀 때도 그대로 도움이 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&lt;/h2&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;n = int(input())
t = []
p = []

for _ in range(n):
    a, b = map(int, input().split())
    t.append(a)
    p.append(b)

dp = [0] * (n + 1)

for i in range(n - 1, -1, -1):
    if i + t[i] &amp;lt;= n:
        dp[i] = max(dp[i + 1], p[i] + dp[i + t[i]])
    else:
        dp[i] = dp[i + 1]

print(dp[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;14501은 DP 입문 문제로 정리해두기 좋다.&lt;br /&gt;상태 정의가 분명하고, 점화식도 짧고, 사고 흐름도 깔끔하다.&lt;br /&gt;비슷한 유형의 문제를 다시 만났을 때 기준점으로 삼기 좋은 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 이런 문제를 보면 먼저 다음을 떠올리면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dp[i]를 무엇으로 정의할지&lt;/li&gt;
&lt;li&gt;현재 할 수 있는 선택이 무엇인지&lt;/li&gt;
&lt;li&gt;선택마다 어디로 이동하는지&lt;/li&gt;
&lt;li&gt;그 결과 중 최댓값을 고르면 되는지&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍/코딩 테스트, 더 이상 미룰 수 없다</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/59</guid>
      <comments>https://ms-diary.tistory.com/59#entry59comment</comments>
      <pubDate>Tue, 31 Mar 2026 14:54:04 +0900</pubDate>
    </item>
    <item>
      <title>[코딩테스트, 더 이상 미룰 수 없다] BOJ 14502 연구소 - 구현을 감 잡아보zㅏ</title>
      <link>https://ms-diary.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 단순히 정답을 맞히는 것보다, 구현 문제를 어떤 식으로 바라봐야 하는지 다시 생각하게 해 준 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현, 조합, 시뮬레이션, 탐색이 함께 섞여 있는 문제여서 더 좋았고, 동시에 더 배울 점이 많았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 좋은 문제라고 느꼈는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 좋은 점은 여러 개념이 따로 노는 것이 아니라, 문제 안에서 자연스럽게 연결된다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벽을 3개 세워야 하니 빈 칸 중 3개를 고르는 조합이 필요하고,&lt;br /&gt;벽을 세운 뒤 바이러스가 퍼지는 과정을 표현하려면 BFS가 필요하다.&lt;br /&gt;그리고 그 모든 과정을 반복해서 최대 안전영역을 구해야 하니 결국 시뮬레이션 문제의 성격도 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제는 단순히 한 알고리즘을 외워서 푸는 문제보다 훨씬 좋은 연습이 된다.&lt;br /&gt;실제로 코딩테스트에서는 &amp;ldquo;이건 무조건 BFS&amp;rdquo;, &amp;ldquo;이건 무조건 DP&amp;rdquo;처럼 딱 떨어지는 문제보다&lt;br /&gt;이렇게 구현 안에 탐색이 들어가고, 경우의 수 안에 시뮬레이션이 들어가는 문제가 더 자주 연습해야 한다고 느낀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 문제에서 진짜 중요했던 건 BFS 자체가 아니었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 바이러스를 퍼뜨리는 BFS가 핵심이라고 생각했는데,&lt;br /&gt;막상 풀면서 더 중요하다고 느낀 건 &amp;ldquo;상태를 어떻게 관리할 것인가&amp;rdquo;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 벽을 세운 뒤 바이러스를 퍼뜨리는 작업은 매 경우의 수마다 새롭게 이루어져야 한다.&lt;br /&gt;그러면 원본 배열은 유지되어야 하고, 실험은 복사본 배열에서만 진행되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분이 흐려지는 순간 바로 실수가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 구현 문제에서 자주 틀리는 이유는 알고리즘을 몰라서가 아니라&lt;br /&gt;원본과 복사본을 헷갈리거나, 이번 경우의 상태와 다음 경우의 상태를 섞어버리기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 문제는 &amp;ldquo;탐색을 할 줄 아느냐&amp;rdquo;보다&lt;br /&gt;&amp;ldquo;한 번의 시뮬레이션 단위를 독립적으로 관리할 수 있느냐&amp;rdquo;를 묻는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 유형의 문제로 카테고리를 묶어서 생각해야 좋겠더rrr라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앞으로 구현 문제를 볼 때 생각해야 할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 풀면서, 구현 문제가 막막하게 느껴질 때는&lt;br /&gt;일단 문제를 한 번에 보지 말고 상태 변화의 단계로 끊어 봐야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제도 처음에는 복잡해 보이지만, 실제로는 다음 세 단계로 나뉜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벽을 세운다&lt;/li&gt;
&lt;li&gt;바이러스를 퍼뜨린다&lt;/li&gt;
&lt;li&gt;남은 안전 영역을 센다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나누고 나면 각 단계가 해야 할 역할이 분명해진다.&lt;br /&gt;구현 문제는 보통 아이디어가 어려운 게 아니라, 여러 작업이 한 덩어리로 보이기 때문에 어렵게 느껴진다.&lt;br /&gt;그래서 앞으로는 문제를 보면 &amp;ldquo;무엇을 몇 단계로 나눌 수 있는가&amp;rdquo;부터 먼저 생각해야겠다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특히 조심해야 할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서 가장 크게 느낀 건 구현 문제는 작은 실수가 전체 정답을 무너뜨린다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 조심해야 할 것은 이런 것들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 원본 배열과 실험 배열을 반드시 구분해야 한다.&lt;br /&gt;이번 경우의 수에서 벽을 세우고 바이러스를 퍼뜨리는 것은 원본이 아니라 복사본에서 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 결과를 셀 때도 어떤 배열을 기준으로 세는지 끝까지 일관되어야 한다.&lt;br /&gt;실험은 temp에서 했는데 결과 계산은 board에서 해버리면, 논리는 맞아 보여도 답은 틀리게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, 2차원 배열 복사는 생각보다 자주 실수하는 부분이다.&lt;br /&gt;겉만 복사하면 안 되고, 각 행까지 복사해 주어야 한다.&lt;br /&gt;이 문제는 그런 기본적인 복사 개념을 점검하기에도 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷째, BFS나 DFS보다 오히려 입력을 어떻게 저장하고, 빈 칸과 바이러스 위치를 어떻게 따로 관리할지가 더 중요할 수 있다.&lt;br /&gt;즉 탐색 함수 하나만 잘 짠다고 끝나는 문제가 아니라, 탐색에 들어가기 전 준비가 훨씬 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 문제를 통해 다시 느낀 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 문제를 어려워하는 이유는 보통 &amp;ldquo;구현력이 부족해서&amp;rdquo;라고 생각하기 쉽다. (본인이 그렇게 참 많이 느낀다.)&lt;br /&gt;그런데 실제로는 구현력이 부족한 게 아니라, 상태 변화의 흐름을 분리해서 보지 못하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 그걸 잘 보여준다.&lt;br /&gt;문제를 보고 바로 코드를 쓰기 시작하면 헷갈리지만,&lt;br /&gt;상태를 나누고, 경우의 수를 나누고, 한 번의 시뮬레이션이 어디서 시작해서 어디서 끝나는지만 정리하면 훨씬 단순해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 문제는 단순한 BFS 입문 문제라기보다는&lt;br /&gt;&amp;ldquo;구현 문제를 풀 때 무엇을 먼저 정리해야 하는가&amp;rdquo;를 훈련시키는 문제라고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앞으로 비슷한 문제를 풀 때의 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 이런 문제를 만나면 먼저 아래를 체크해야겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 한 번의 탐색으로 끝나는가, 아니면 여러 경우를 반복 실험해야 하는가.&lt;br /&gt;원본 상태와 실험 상태를 분리해야 하는가.&lt;br /&gt;결과를 계산하는 기준 배열이 무엇인가.&lt;br /&gt;탐색 자체보다 그 전에 좌표나 후보군을 정리하는 게 더 중요한가.&lt;br /&gt;문제를 단계별 시뮬레이션으로 나눌 수 있는가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기준을 먼저 세우면, 구현 문제가 훨씬 덜 추상적으로 보일 것 같다.&lt;/p&gt;</description>
      <category>프로그래밍/코딩 테스트, 더 이상 미룰 수 없다</category>
      <author>d 0_0 b</author>
      <guid isPermaLink="true">https://ms-diary.tistory.com/58</guid>
      <comments>https://ms-diary.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 30 Mar 2026 19:43:51 +0900</pubDate>
    </item>
  </channel>
</rss>