<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>오웬의 개발 이야기</title>
    <link>https://yolo2429.tistory.com/</link>
    <description>안녕하세요. 사진과 철학에 관심이 많은 웹 프론트엔드 개발자 오원종입니다. 시간이 지나도 꾸준히 읽힐 수 있는 글을 쓰고 싶습니다. 재미있는 일만 하면서 살고 있는 사람입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 25 May 2026 15:33:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>DevOwen</managingEditor>
    <image>
      <title>오웬의 개발 이야기</title>
      <url>https://tistory1.daumcdn.net/tistory/2738522/attach/418608a299c6427e86eaf3e174103ec8</url>
      <link>https://yolo2429.tistory.com</link>
    </image>
    <item>
      <title>위클리 인사이트 By 오웬 (Y26W21)</title>
      <link>https://yolo2429.tistory.com/528</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;generated_image_4e082d91-57e5-4d22-b31a-a1c4f07752c9.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rskZc/dJMb99TXvLG/k33T5m68oAZWwXsO5cAYKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rskZc/dJMb99TXvLG/k33T5m68oAZWwXsO5cAYKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rskZc/dJMb99TXvLG/k33T5m68oAZWwXsO5cAYKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrskZc%2FdJMb99TXvLG%2Fk33T5m68oAZWwXsO5cAYKK%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;400&quot; height=&quot;400&quot; data-filename=&quot;generated_image_4e082d91-57e5-4d22-b31a-a1c4f07752c9.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;26.05.18 ~ 26.05.22&lt;/i&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;감사하게도 회사에서 AI 도구를 여러 가지 지원해 주어서 이것저것 써 보고 있다. 그런데 단편적으로 채팅하고 명령해서 쓰고 하는 수준에서 크게 벗어나지 못하는 것 같다는 생각이 든다. 지금은 피그마 MCP로 우리 팀의 코드 컨벤션과 디자인 시스템을 이해하고 구현까지 해주는 에이전트를 고도화 하고 있는데, 쓸 수 있는 도구는 많지만 작업의 진도가 잘 안나가는 것 같아서 답답하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무에서는 주로 Claude를 쓰고, 간간히 사이드로 Codex를 쓴다. 그런데 요즘은 Codex가 더 잘하는 것 같다. 이미지 생성은 Gemini를 쓰고, 주식 등 최신 정보를 필요로 하는 조사 작업에서는 Perplexity를 쓴다. 다양한 도구들을 써 보고 있지만, 어느 하나 막 남들한테 가르칠 정도로 잘 쓰는 것은 없는 듯하다. Claude는 CLI로 계속 쓰고 있었는데, 시험삼아 Codex를 데스크탑 앱으로 써보니 오히려 가시성도 더 좋고 쓰기도 편리해서 앞으로 Claude도 데스크탑 앱을 써야 하나 고민이 든다. 업무를 할 때는 확실히 터미널이 아직까지는 편한 것 같다.&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;나를 브랜딩하는 것에 요즘 관심이 많이 생기고 있다. 블로그를 2017년부터 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;수요일 오전에는 회사에서 여러 글로벌 동료들과 career pathfinding이라는 주제로 커리어 탐색 및 확장에 대한 이야기를 나누었다. 여기서도 브랜딩에 대한 이야기가 나왔는데 나는 지금까지 회사 밖에 나를 알리는 브랜딩(링크드인에 글을 쓴다든지, 컨퍼런스를 참여한다든지 등)만 생각했지만 회사 안에서의 나는 어떠한 브랜드를 가지고 있는지를 돌아보게 된다. 회사 안에서는 어려운 문제를 풀고, 많은 사람들에게 영향을 끼칠 수 있는 사람이 브랜드가 있는 사람이 아닐까? 하는 생각이 든다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유플래너 서비스 종료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 최근 몇 년 동안 애정하며 써 왔던 유플래너라는 가계부 앱이 6월부로 서비스 종료한다는 소식을 들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2026-05-18-19-31-41.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blCiFF/dJMcabqJI6x/VEQucPkPZIMdKk7LPZPgJk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blCiFF/dJMcabqJI6x/VEQucPkPZIMdKk7LPZPgJk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blCiFF/dJMcabqJI6x/VEQucPkPZIMdKk7LPZPgJk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblCiFF%2FdJMcabqJI6x%2FVEQucPkPZIMdKk7LPZPgJk%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;400&quot; height=&quot;867&quot; data-filename=&quot;KakaoTalk_Photo_2026-05-18-19-31-41.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료인 가계부 앱인데도 조금 느리다는 단점만 빼면 여러모로 훌륭했던 좋은 서비스였는데, 서비스를 종료한다는 소식을 들으니 아쉬웠다. 그럼에도 가계부는 계속 써야 하기에&amp;hellip; 다른 가계부 앱들을 이것저것 찾아보고 있다. 처음에는 편한 가계부 앱을 깔아서 써 봤는데, 내 지출 내역을 하나하나 수동으로 입력하는 게 너무 불편했다. 이름처럼 편하지는 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 예전부터 쓰던 뱅크샐러드 앱을 가계부로 쓰게 될 것 같다. 뱅크샐러드 가계부는 일단 내가 연동한 모든 은행, 증권사, 카드의 수입/지출 내역을 불러와 주고 있고 카테고라이징도 꽤 잘해 주는 편이다. 물론 커스터마이즈가 어느 정도 필요하긴 하지만&amp;hellip; 일단 5월 한 달 써 보고 6월 이후에도 계속 쓸지 고민을 해 보아야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 나의 수입/지출을 모바일 뿐만 아니라 PC에서도 관리하고 싶고 개인적으로 정리하는 엑셀 파일을 쓰지 않고도 자동으로 export 해줄 수 있다면 편리할 것 같은데 아직까지 이 모든 기능을 만족하는 가계부 서비스는 보지는 못 한 것 같다. 수고스러움이 있긴 하지만, 이렇게라도 내 지출을 하나씩 보고 어디서 많이 썼는지 체크하는 것도 의미가 있다고 생각한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해외 주식 양도소득세&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생애 처음으로 해외 주식 양도소득세를 내게 되었다. 해외 주식 직접 투자를 통해 한 해 수익이 250만 원 이상 발생하는 경우 발생한 수익의 22%를 세금으로 내야 한다. 예를 들어 내가 300만 원의 수익이 발생했다면, (300만 원 - 250만 원) * 0.22 = 11만 원을 세금으로 내야 한다. 이번에는 큰 금액이 아니라서 세금을 많이 내지는 않았다. 앞으로 내 자산이 점점 더 커질수록 내야 할 세금이 많아질 텐데, 절세 방법들을 여러 가지 잘 활용해 보아야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 ISA, 연금저축, IRP 이렇게 3가지 방식으로 절세를 하고 있다. ISA는 계좌에서 내가 난 수익에 한해서 400만 원(서민형 기준)의 비과세 혜택을 주고, 나머지 수익에 대해서도 9.9%의 분리과세를 낮게 책정하는 계좌이다. 일반적으로 이자나 배당에 15.4%의 양도소득세가 붙는 걸 감안하면 세금을 줄일 수 있다는 장점이 있다. 연금저축은 매년 600만 원까지 세액공제 혜택을 제공한다. 소득에 따라 다른데 15.4% 또는 13.2%의 세액공제를 600만 원까지 받을 수가 있다. 다만 이름처럼 이 세금은 나중에 연금으로 받을 때 과세가 되는데, 더 많은 나이에 받게 될수록 세금을 적게 낼 수가 있다. IRP도 연금저축처럼 세액공제를 받을 수 있는 계좌이고, 연금저축 + IRP를 하면 900만 원까지 세액공제 혜택을 받을 수 있다. IRP는 퇴직연금의 성격이 강하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 그래서 앞으로 국내/해외 개별종목(SK하이닉스, 마이크로소프트 등)은 직접 투자를 하고, 해외 지수 추종 ETF(S&amp;amp;P500, 나스닥100 등)을 국내 상장된 종목으로 거래할 때 ISA에서 주로 하려고 한다. 해외 개별 종목도 어떻게 하면 세금을 줄일 수 있을지 고민이 필요하다. 연금저축과 IRP도 해외 지수 추종 ETF나 금, 아니면 TDF 같은 상품들로 꾸려 보려고 한다. 절세도 물론 중요하지만 직접적인 투자로 큰 수익을 내는 것이 더 중요하기에 이러한 고민을 가지고 포트폴리오를 만들어 나가려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메타 레이오프를 보면서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주 쓰레드와 X(구 트위터)에서는 메타 레이오프 관련 소식이 많이 올라왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-22 오후 2.02.58.png&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;1744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GJZ6j/dJMcabj2x0H/VQTq8RLwXunGJuxGtZQ5d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GJZ6j/dJMcabj2x0H/VQTq8RLwXunGJuxGtZQ5d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GJZ6j/dJMcabj2x0H/VQTq8RLwXunGJuxGtZQ5d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGJZ6j%2FdJMcabj2x0H%2FVQTq8RLwXunGJuxGtZQ5d0%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;400&quot; height=&quot;458&quot; data-filename=&quot;스크린샷 2026-05-22 오후 2.02.58.png&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;1744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 레이오프를 당하더라도 바로바로 다른 회사로 이직이 쉽게 되었던 것 같은데 (메타 다니시는 분들 정도면 워낙 뛰어나기도 하니까) 이번 주 올라온 반응을 보면 이직도 어렵고 이전에 받던 보상에서 많이 깎고 가야 하는 경우가 비일비재하다는 것을 알게 된다. 점점 회사들이 채용을 안 하고 AI가 업무를 대체하는 흐름 속에서 많은 레이오프 대상자든 아니든 간에 많은 분들이 두려움을 느끼는 것이 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 시대적인 흐름 속에서 나는 무엇을 해야 할까? 만약 우리 회사가 당장 내일 레이오프를 한다면 나는 어떤 전략을 가져 나가야 할까? 남의 나라 일이라고 보기엔 한국도 요즘 취업 시장이 너무 안 좋은 게 느껴져서 더 나한테도 위기감을 느끼게 한다. 특히 이번에 매니저 직군이 레이오프 대상자로 많이 선정되고 기존에 매니저 직군에 계신 분들이 IC(Individual Contributor, 개인 기여자) 직군으로 전환된 경우도 많은 것 같다. 사람을 다루는 일이 점점 더 필요 없어진 걸까? 그러면 이 시대에 나는 리더십을 어떻게 쌓아 나가야 할까... 고민이 많다.&lt;/p&gt;</description>
      <category>Weekly Insight</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/528</guid>
      <comments>https://yolo2429.tistory.com/528#entry528comment</comments>
      <pubDate>Mon, 25 May 2026 09:00:48 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 리액트는 기본값으로 승리했습니다 &amp;ndash; 그리고 프런트엔드 혁신을 죽이고 있습니다</title>
      <link>https://yolo2429.tistory.com/527</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_p2yhecp2yhecp2yh.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blJXjo/dJMcaaFfWdC/e4JFtlUFIvWpHoGC47JiMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blJXjo/dJMcaaFfWdC/e4JFtlUFIvWpHoGC47JiMk/img.png&quot; data-alt=&quot;출처 : 나노 바나나&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blJXjo/dJMcaaFfWdC/e4JFtlUFIvWpHoGC47JiMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblJXjo%2FdJMcaaFfWdC%2Fe4JFtlUFIvWpHoGC47JiMk%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;1376&quot; height=&quot;768&quot; data-filename=&quot;Gemini_Generated_Image_p2yhecp2yhecp2yh.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 나노 바나나&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;원문: &lt;a href=&quot;https://www.lorenstew.art/blog/react-won-by-default/&quot;&gt;https://www.lorenstew.art/blog/react-won-by-default/&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React-by-default에는 숨겨진 비용이 있습니다. 이 글은 작업에 맞는 올바른 프레임워크를 선택하기 위해 의도적인 선택을 내려야 한다는 주장입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트는 더 이상 기술적 우위로 승리하고 있지 않습니다. 지금은 기본 선택지라는 이유만으로 승리하고 있습니다. 이 기본값이 프런트엔드 생태계 전반의 혁신을 늦추고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀이 새로운 프런트엔드를 만들어야 할 때, 대화는 좀처럼 &quot;제약 조건이 무엇이고, 어떤 도구가 가장 적합한가?&quot;로 시작하지 않습니다. 대부분 &quot;리액트를 쓰자. 다들 리액트는 알잖아.&quot;로 시작합니다. 이 반사적인 선택은 기술적 적합성이 아닌 네트워크 효과가 아키텍처를 결정하는 자기 강화적 순환을 만들어냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한편, 혁신적인 아키텍처 방식을 도입한 프레임워크들은 도입에 어려움을 겪고 있습니다. Svelte는 런타임 시그널(signal)과 컴파일 타임 최적화를 결합해 더 작은 번들을 만들어냅니다. Solid는 가상 DOM의 비용 없이 세밀한 반응성을 제공합니다. Qwik은 재개 가능성(resumability)을 통해 즉각적인 시작을 실현합니다. 이러한 방식들은 일반적인 시나리오에서 리액트 모델을 능가할 수 있지만, 리액트가 기본 선택지로 선택되기 때문에 공정한 평가를 받지 못하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트는 많은 면에서 탁월합니다. 문제는 리액트 자체가 아니라, React-by-default라는 사고방식입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;혁신의 천장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트의 기술적 기반은 오늘날의 불편함 중 일부를 설명해줍니다. 가상 DOM은 2013년의 문제에 대한 영리한 해법이었지만, Rich Harris가 &lt;a href=&quot;https://svelte.dev/blog/virtual-dom-is-pure-overhead&quot;&gt;&quot;Virtual DOM is pure overhead&quot;&lt;/a&gt;에서 설명했듯이, 현대 컴파일러라면 충분히 피할 수 있는 작업을 유발합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅은 클래스 컴포넌트의 고충을 해소했지만, 의존성 배열, 오래된 클로저, 잘못 사용된 이펙트라는 새로운 복잡성을 도입했습니다. 리액트 공식 문서조차 &lt;a href=&quot;https://react.dev/learn/you-might-not-need-an-effect&quot;&gt;&quot;이펙트가 필요 없을 수도 있습니다&quot;&lt;/a&gt;라고 자제를 강조할 정도입니다. 서버 컴포넌트는 클라이언트 측 자바스크립트를 줄이고 서버 전용 데이터 접근을 가능하게 하지만, 아키텍처 복잡성과 새로운 장애 유형을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://react.dev/learn/react-compiler&quot;&gt;리액트 컴파일러&lt;/a&gt;는 &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; 같은 패턴을 자동화하는 영리한 해법입니다. 하지만 그 존재 자체가 신호이기도 합니다. 모델 자체에 내재된 제약을 중심으로 최적화하고 있다는 신호입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 10월에 출시된 리액트 19.2도 이 패턴을 이어갑니다. 새로운 &lt;a href=&quot;https://react.dev/blog/2025/10/01/react-19-2&quot;&gt;&lt;code&gt;useEffectEvent&lt;/code&gt; 훅&lt;/a&gt;은 이펙트의 의존성 배열 문제를 해결하기 위해 특별히 도입되었습니다. 훅 자체가 만들어낸 복잡성에 붙인 패치인 셈입니다. Svelte나 Solid 같은 프레임워크에도 비슷한 untrack 기능이 있지만, 이 프레임워크들은 기본적으로 자동 의존성 추적을 사용하기 때문에 untrack은 엣지 케이스에서만 필요합니다. 리액트는 모든 이펙트에 수동으로 의존성 배열을 작성해야 하고, 그 모델의 한계를 우회하기 위해 &lt;code&gt;useEffectEvent&lt;/code&gt;를 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 릴리스에서는 앱의 표시/숨김 상태를 관리하는 &lt;code&gt;&amp;lt;Activity /&amp;gt;&lt;/code&gt; 컴포넌트와 새로운 부분 프리렌더링(partial pre-rendering) API도 도입되었습니다. 추가될 때마다 개발자가 익혀야 할 API 표면적은 늘어나는 반면, 대안 프레임워크들은 더 단순한 기본 요소로 유사한 결과를 달성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대안적 접근 방식과 비교해보세요. Svelte 5의 &lt;a href=&quot;https://svelte.dev/blog/runes&quot;&gt;Rune&lt;/a&gt;은 런타임 시그널로 세밀한 반응성을 구현하고, Solid의 &lt;a href=&quot;https://www.solidjs.com/docs/latest#reactivity&quot;&gt;세밀한 반응성&lt;/a&gt;은 변경된 부분만 정확히 업데이트하며, Qwik의 &lt;a href=&quot;https://qwik.builder.io/docs/concepts/resumable/&quot;&gt;재개 가능성&lt;/a&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;리액트를 기본값으로 선택하면 더 이상 의문을 품지 않는 런타임 비용과 재조정(reconciliation) 비용이 따라옵니다. 충분히 빠르더라도, 리액트 모델의 한계는 컴파일 타임 모델이나 세밀한 반응성 모델보다 낮습니다. 개발자의 시간은 가치를 전달하는 대신 리렌더링 관리, 이펙트 의존성, 하이드레이션 경계를 다루는 데 쓰입니다. 성능 연구에서 얻은 더 넓은 교훈은 일관됩니다. 자바스크립트는 크리티컬 패스(critical path)에서 비용이 큽니다(&lt;a href=&quot;https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e&quot;&gt;The Cost of JavaScript&lt;/a&gt;).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능을 넘어, 우리는 웹의 근본 원칙 대신 &quot;리액트 패턴&quot;을 중심으로 사고 모델을 구축해왔고, 이는 기술의 이식성을 낮추고 아키텍처 관성을 심화시킵니다. 손실은 성능에만 그치지 않습니다. 더 적합한 대안이 한 번도 평가받지 못할 때 발생하는 기회 비용입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;숨이 막혀가는 프레임워크들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Svelte: 컴파일 타임 최적화를 갖춘 시그널&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Svelte 5는 런타임 시그널을 통한 세밀한 반응성과 적극적인 컴파일 타임 최적화를 결합합니다. 가상 DOM이 없고, 런타임도 리액트보다 작습니다. 컴포넌트는 효율적이고 직접적인 DOM 조작으로 컴파일됩니다. 사고 모델이 웹의 근본 원칙과 잘 맞아떨어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &quot;일자리가 충분하지 않다&quot;는 이유로 Svelte 도입은 기술적 강점에도 불구하고 인위적으로 낮은 수준에 머물고 있습니다. 더 가디언지의 프런트엔드 Svelte 전환 같은 실제 사례들은 번들 크기 감소와 로드 시간 단축을 비롯한 성능과 개발자 생산성에서 측정 가능한 성과를 보여줍니다. &lt;a href=&quot;https://www.wired.com/story/javascript-framework-puts-web-pages-diet/&quot;&gt;Wired의 Svelte 관련 기사&lt;/a&gt;에서 소개된 것처럼, 개발자 Shawn Wang(X/Twitter의 &lt;a href=&quot;https://x.com/swyx&quot;&gt;@swyx&lt;/a&gt;)은 컴파일 타임 최적화를 활용해 자신의 사이트 크기를 리액트 기반의 187KB에서 Svelte 기반의 9KB로 줄였습니다. 특히 느린 연결 환경에서 더 빠르고 효율적인 앱을 만들 수 있게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Solid: 반응형 기본 요소 접근법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Solid는 JSX에 익숙한 방식으로 세밀한 반응성을 제공합니다. 업데이트는 시그널을 통해 영향을 받는 DOM 노드로 직접 전달되어 재조정 병목을 우회합니다. 뛰어난 성능 특성을 갖추고 있지만, 인지도는 제한적입니다. &lt;a href=&quot;https://www.solidjs.com/guides/comparison&quot;&gt;Solid의 비교 가이드&lt;/a&gt;에서 설명하듯, 이 방식은 리액트의 가상 DOM보다 효율적인 업데이트를 가능하게 하며, 정밀한 반응성으로 불필요한 작업을 최소화하고 더 단순한 상태 관리를 통해 개발자 경험을 향상시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 많이 알려진 프레임워크들에 비해 두드러진 사례 연구가 부족한 것은 주로 Solid의 낮은 도입률 때문입니다. 그러나 초기 도입자들의 경험담은 업데이트 효율성과 코드 단순성 면에서 비슷한 혁신적 성과를 시사하며, 더 많은 팀이 실험함에 따라 확산될 가능성이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Qwik: 재개 가능성의 혁신&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Qwik은 하이드레이션 대신 재개 가능성을 사용합니다. 현재 상호작용에 필요한 것만 불러와 즉각적인 시작을 가능하게 합니다. 모든 애플리케이션 상태를 직렬화할 수 있어야 한다는 아키텍처 제약이 있지만, 측정 가능한 성능 향상을 제공합니다. 콘텐츠가 많은 사이트, 느린 네트워크, 모바일 우선 애플리케이션에 이상적입니다. &lt;a href=&quot;https://qwik.dev/docs/concepts/think-qwik/&quot;&gt;Qwik의 Think Qwik 가이드&lt;/a&gt;에 따르면, 이는 점진적 로딩과 상태 및 코드 직렬화를 통해 구현됩니다. 앱은 무거운 클라이언트 사이드 부트스트래핑 없이 즉시 실행을 재개할 수 있어, 기존 프레임워크에 비해 뛰어난 확장성과 단축된 초기 로드 시간을 실현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Qwik의 성공 사례가 잘 알려지지 않은 것은 기본 선택에서 벗어나 시도해본 팀이 적기 때문입니다. 그러나 시도해본 팀들은 시작 시간과 자원 효율성에서 극적인 개선을 보고하며, 도입이 확산될 경우 발휘될 잠재력이 크다는 것을 보여줍니다.&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 표면적은 대안 프레임워크들보다 현저히 크고 복잡합니다. 훅, 컨텍스트(context), 리듀서(reducer), 메모이제이션(memoization) 패턴 등 함정을 피하기 위해 주의 깊게 다뤄야 할 개념들이 가득합니다. 이처럼 방대한 API는 개발자의 인지 부하를 높이고, 의존성을 잘못 이해하거나 과도하게 설계하는 데서 비롯된 버그로 이어지는 경우가 많습니다. 예를 들어, &lt;a href=&quot;https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/&quot;&gt;2025년 9월 12일 Cloudflare 장애&lt;/a&gt;에서는 문제 있는 의존성 배열을 가진 &lt;code&gt;useEffect&lt;/code&gt; 훅이 반복적인 API 호출을 유발해 Tenant Service를 압도하고 광범위한 장애를 일으켰습니다. 반면, Svelte, Solid, Qwik 같은 프레임워크들은 단순함과 웹의 근본 원칙을 강조하는 더 작고 집중된 API를 갖추고 있어, 정신적 부하를 줄이고 익히고 유지 관리하기 쉽게 만들어줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네트워크 효과의 감옥&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트의 지배력은 자기 강화적 장벽을 만들어냅니다. 채용 공고는 &quot;프런트엔드 엔지니어&quot;가 아닌 &quot;리액트 개발자&quot;를 요구해 기술 다양성을 제한합니다. 컴포넌트 라이브러리와 팀의 체득된 경험은 조직적 관성을 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위험을 회피하는 리더들은 &quot;안전한&quot; 선택지를 고릅니다. 학교는 취업 시장이 요구하는 것을 가르칩니다. 이 순환은 기술적 우위와 무관하게 계속됩니다.&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;교육자들은 특정 도구와 함께 프레임워크에 종속되지 않는 개념을 가르칠 수 있습니다. 오픈 소스 기여자들은 대안 생태계가 성숙할 수 있도록 도울 수 있습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;성능 요구사항 파악&lt;/b&gt; &amp;mdash; 시작 시간, 업데이트 효율성, 번들 크기 같은 지표를 평가하세요. 속도가 중요하다면 컴파일 타임 최적화를 제공하는 프레임워크를 우선시하세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀 역량과 학습 곡선&lt;/b&gt; &amp;mdash; 기존 전문성을 고려하되 전환 경로도 함께 살펴보세요. 많은 대안이 완만한 진입로를 제공합니다(예: Solid의 리액트와의 JSX 호환성).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성과 유지 비용&lt;/b&gt; &amp;mdash; 유지 관리, 의존성 관리, 기술 부채를 포함한 장기 비용을 계산하세요. 대안 프레임워크들은 런타임 오버헤드를 줄여 호스팅 비용을 낮추고 확장성을 개선하는 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;생태계 적합성&lt;/b&gt; &amp;mdash; 성숙도와 혁신성 사이의 균형을 맞추세요. 중요하지 않은 영역에서 파일럿을 진행해 전환 가능성과 ROI를 테스트하세요.&lt;/li&gt;
&lt;/ul&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;&quot;생태계 성숙도가 중요하잖아요!&quot;&lt;/b&gt; 성숙도는 분명 가치 있습니다. 하지만 관성을 고착시킬 수도 있습니다. 연륜이 곧 오늘날의 제약에 대한 적합성을 의미하지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성숙한 생태계는 서드파티 패키지에 대한 강한 의존성을 의미하기도 합니다. 이는 의존성 최신화, 보안 취약점 대응, 사용하지 않는 코드로 인한 번들 비대화 같은 유지 관리 부담을 초래할 수 있습니다. 경우에 따라 필수적이기도 하지만, 이런 유연성은 과도한 의존으로 이어질 수 있습니다. 특정 요구에 맞춘 맞춤 솔루션이 장기적으로는 더 가볍고 유지하기 쉬운 경우가 많습니다. 대안 프레임워크의 더 작은 생태계는 기본부터 구축하도록 장려해 더 깊은 이해와 적은 기술 부채로 이어집니다. 게다가 AI 코딩 어시스턴트가 정확한 맞춤 함수를 즉시 생성할 수 있게 된 지금, 특수 목적 유틸리티를 만드는 진입 장벽은 크게 낮아졌습니다. lodash 같은 범용 라이브러리나 Moment, date-fns 같은 날짜 라이브러리를 완전히 배제하고 가볍고 앱에 특화된 구현으로 대체하는 것도 충분히 실현 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;채용이 문제잖아요!&quot;&lt;/b&gt; 채용은 수요를 따릅니다. 중요하지 않은 경로에서 대안을 먼저 시범 운영한 후, 기본기를 갖춘 인재를 채용해 실무 교육을 병행하면 위험을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;컴포넌트 라이브러리가 필요하잖아요!&quot;&lt;/b&gt; 컴포넌트 라이브러리는 군더더기입니다. 출시 속도가 다른 모든 것을 압도할 때 유용할 뿐입니다. 애플리케이션의 특정 요구에 맞춰 컴포넌트를 구축하면 존재하지도 않는 문제를 위한 코드를 배포하지 않아도 되므로 더 가벼운 솔루션이 만들어집니다. 프레임워크에 종속되지 않는 디자인 시스템과 웹 컴포넌트(Web Components)는 공유 컴포넌트가 필요할 때 속도를 유지하면서도 종속성을 줄여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;안정성이 중요하잖아요!&quot;&lt;/b&gt; 클래스에서 훅으로, 서버 컴포넌트로, 다시 리액트 19.2의 &lt;code&gt;useEffectEvent&lt;/code&gt;와 &lt;code&gt;&amp;lt;Activity /&amp;gt;&lt;/code&gt; 컴포넌트로 이어진 리액트의 진화는 안정성이 아닌 끊임없는 변화를 보여줍니다. 대안 프레임워크들은 오히려 더 일관된 API를 제공하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;대규모 검증이 되어 있잖아요!&quot;&lt;/b&gt; jQuery도 대규모로 검증된 기술이었습니다. 과거의 성공이 미래의 적합성을 보장하지는 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 넓은 생태계에 끼치는 해악&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 문화(monoculture)는 한 프레임워크의 제약이 사실상의 한계가 될 때 웹의 발전을 늦춥니다. 인재들은 플랫폼을 발전시키는 대신 프레임워크 특정 문제를 해결하는 데 시간을 씁니다. 투자는 기술적 우위와 무관하게 기존 강자를 따릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교육 과정은 기본기보다 즉각적인 취업 가능성에 최적화되어, 이식 가능한 기술 대신 프레임워크 특화 기술을 만들어냅니다. &quot;리액트가 처리할 수 있다&quot;는 말이 기본 답변이 되면서 플랫폼 개선이 지연됩니다.&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;모든 것을 하나의 모델에 거는 것은 단일 장애점을 만듭니다. 그것이 한계에 부딪히면 어떻게 될까요? 대안을 탐색하지 않음으로써 우리가 놓치고 있는 기회는 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 관성이 아닌 제약 조건과 장점을 기반으로 프레임워크를 선택할 때입니다. 여러분의 다음 프로젝트는 React-by-default보다 나은 선택을 받을 자격이 있습니다. 생태계는 다양성만이 제공할 수 있는 혁신을 받을 자격이 있습니다.&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>Web Frontend Developer</category>
      <category>react</category>
      <category>Web</category>
      <category>개발</category>
      <category>라이브러리</category>
      <category>리액트</category>
      <category>웹</category>
      <category>프레임워크</category>
      <category>프론트엔드</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/527</guid>
      <comments>https://yolo2429.tistory.com/527#entry527comment</comments>
      <pubDate>Fri, 8 May 2026 16:42:30 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 자바스크립트 Date 계산은 얼마나 잘못될 수 있을까요?</title>
      <link>https://yolo2429.tistory.com/526</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://philna.sh/blog/2026/01/11/javascript-date-calculation/&quot;&gt;https://philna.sh/blog/2026/01/11/javascript-date-calculation/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://philna.sh/_astro/time-zones.DFi5vite_ZNhb80.jpg&quot; alt=&quot;터미널 창에 자바스크립트 REPL이 표시되어 있습니다. 코드는 2024년 1월 1일 날짜를 생성한 다음, 날짜에 한 달을 더합니다. 결과는 2023년 3월 4일입니다.&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;2025년 1월, 저는 미국 캘리포니아주 산타클라라에서 보고서를 만들기 위한 자바스크립트를 작성하고 있었습니다. 한 달 동안 발생한 이벤트 수를 구하고 싶었기 때문에, 해당 월의 첫째 날로 날짜 객체를 만들고, 한 달을 더한 뒤, 하루를 빼서 마지막 날을 구하려 했습니다. 간단해 보이죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 정말 이상한 결과가 나왔습니다. 문제를 다음 코드로 재현할 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const date = new Date(&quot;2024-01-01T00:00:00.000Z&quot;);
date.toISOString();
// =&amp;gt; &quot;2024-01-01T00:00:00.000Z&quot; as expected
date.setMonth(1);
date.toISOString();
// =&amp;gt; &quot;2023-03-04T00:00:00.000Z&quot; WTF?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 1월 1일에 한 달을 더했는데 2023년 3월 4일이 되었습니다. 무슨 일이 일어난 걸까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시간과 시간대&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이야기의 배경이 왜 미국 서부 해안인지 의아했을 수도 있지만, 사실 장소가 중요한 요인이었습니다. 이 코드는 UTC 시간대와 그보다 동쪽인 지역에서는 정상적으로 동작했을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트의 날짜는 단순한 날짜가 아니라 시간까지 함께 다룹니다. 이 예제에서는 일과 월만 다루고 싶었지만, 시간도 중요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 이 사실을 알고 있었기에 시간을 UTC로 설정하면 어디서든 잘 동작할 거라 생각했습니다. 그게 패착이었습니다. 무슨 일이 일어났는지 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 1월 1일 자정(UTC)은 태평양 표준시(UTC-8)로는 아직 2023년 12월 31일 오후 4시입니다. &lt;code&gt;date.setMonth(1)&lt;/code&gt;은 월을 2월로 설정합니다(월은 일과 달리 0부터 시작합니다). 하지만 시작 날짜가 2023년 12월 31일이었으므로, 자바스크립트는 존재하지 않는 2023년 2월 31일을 처리해야 합니다. 이때 다음 달로 넘겨서 3월 3일이 됩니다. 마지막으로, 출력을 위해 날짜를 다시 UTC로 변환하면 최종 결과는 2023년 3월 4일 자정이 됩니다.&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;항상 UTC를 사용하세요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 시간에는 관심이 없었고 UTC로 작업하고 싶었기 때문에, &lt;code&gt;Date&lt;/code&gt; 객체의 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMonth&quot;&gt;&lt;code&gt;setUTCMonth&lt;/code&gt; 메서드&lt;/a&gt;를 사용해서 코드를 수정했습니다. 원래 코드에서는 하루를 빼서 해당 월의 마지막 날을 구했으므로, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCDate&quot;&gt;&lt;code&gt;setUTCDate&lt;/code&gt; 메서드&lt;/a&gt;도 함께 사용했습니다. 모든 &lt;code&gt;set${timePeriod}&lt;/code&gt; 메서드에는 대응하는 &lt;code&gt;setUTC${timePeriod}&lt;/code&gt; 메서드가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const date = new Date(&quot;2024-01-01T00:00:00.000Z&quot;);
date.toISOString();
// =&amp;gt; &quot;2024-01-01T00:00:00.000Z&quot;
date.setUTCMonth(1);
date.toISOString();
// =&amp;gt; &quot;2024-02-01-T00:00:00.000Z&quot;&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;Temporal을 소개합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제가 발생한 이유 중 하나는 날짜를 조작하려 했지만, 실제로는 자신도 모르게 날짜와 시간을 함께 조작하고 있었기 때문입니다. 글 서두에서 &lt;code&gt;Temporal&lt;/code&gt;을 언급한 이유는 바로 이런 상황을 위한 객체가 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 &lt;code&gt;Temporal&lt;/code&gt;로 작성한다면 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate&quot;&gt;&lt;code&gt;Temporal.PlainDate&lt;/code&gt;&lt;/a&gt;를 사용할 수 있습니다. 시간이나 시간대 없이 달력상의 날짜만 표현하는 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것만으로도 훨씬 단순해지지만, &lt;code&gt;Temporal&lt;/code&gt;은 날짜를 조작하는 방법도 더 직관적으로 만들어 줍니다. 월이나 일을 직접 설정하거나 밀리초를 더하는 대신, 기간을 더합니다. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration&quot;&gt;&lt;code&gt;Temporal.Duration&lt;/code&gt; 객체&lt;/a&gt;로 기간을 생성하거나, 기간을 정의하는 객체를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Temporal&lt;/code&gt;은 객체를 불변(immutable)으로 만들기 때문에, 날짜를 변경할 때마다 새로운 객체를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 한 달을 더하고 싶었으므로, &lt;code&gt;Temporal&lt;/code&gt;로는 다음과 같이 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const startDate = Temporal.PlainDate.from(&quot;2024-01-01&quot;);
// =&amp;gt; Temporal.PlainDate 2024-01-01
const nextMonth = startDate.add({ months: 1 });
// =&amp;gt; Temporal.PlainDate 2024-02-01
const endDate = nextMonth.subtract({ days: 1 });
// =&amp;gt; Temporal.PlainDate 2024-01-31&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;code&gt;Temporal&lt;/code&gt; API에는 이 외에도 많은 장점이 있으며, 모든 자바스크립트 런타임에 포함되는 날이 기다려집니다.&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;code&gt;Temporal&lt;/code&gt;은 아직 많은 자바스크립트 엔진에 탑재되지 않았습니다. 이 글을 쓰는 시점에서 Firefox에서만 사용할 수 있으므로, 직접 테스트해 보고 싶다면 Firefox를 열거나 폴리필(polyfill)인 &lt;a href=&quot;https://github.com/js-temporal/temporal-polyfill&quot;&gt;@js-temporal/polyfill&lt;/a&gt; 또는 &lt;a href=&quot;https://www.npmjs.com/package/temporal-polyfill&quot;&gt;temporal-polyfill&lt;/a&gt;을 확인해 보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수정:&lt;/b&gt; 이 글을 게시한 지 불과 이틀 만에 &lt;a href=&quot;https://developer.chrome.com/blog/new-in-chrome-144#temporal&quot;&gt;Chrome 144에서 Temporal 지원이 시작되었습니다&lt;/a&gt;. &lt;a href=&quot;https://caniuse.com/temporal&quot;&gt;Can I Use에 따르면 Temporal&lt;/a&gt;은 Safari Technology Preview에서 플래그를 활성화해야 사용할 수 있으므로, Safari에서도 곧 지원될 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 &lt;code&gt;Date&lt;/code&gt;를 사용해야 한다면 시간대를 반드시 염두에 두세요. 지금이라도 &lt;code&gt;Temporal&lt;/code&gt;로 전환하거나, 최소한 사용 방법을 익혀 두시길 권합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 시간대를 조심하세요. 피하려 해도 결국 골치 아프게 만드니까요.&lt;/p&gt;</description>
      <category>Web Frontend Developer</category>
      <category>Date</category>
      <category>javascript</category>
      <category>TimeZone</category>
      <category>시간대</category>
      <category>자바스크립트</category>
      <category>프로그래밍</category>
      <category>프론트엔드</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/526</guid>
      <comments>https://yolo2429.tistory.com/526#entry526comment</comments>
      <pubDate>Fri, 8 May 2026 16:35:39 +0900</pubDate>
    </item>
    <item>
      <title>마라톤 / 트레일러닝 준비물 체크리스트</title>
      <link>https://yolo2429.tistory.com/510</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;마라톤 준비물&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;index-col&quot;&gt;번호&lt;/th&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;풀코스 기준&lt;/th&gt;
&lt;th&gt;특이사항&lt;/th&gt;
&lt;th class=&quot;check-col&quot;&gt;체크&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;필수장비&lt;/td&gt;
&lt;td&gt;배번표(bib)&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;대회 참가 필수&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;필수장비&lt;/td&gt;
&lt;td&gt;카본화&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;레이스용 러닝화&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;필수장비&lt;/td&gt;
&lt;td&gt;플라스틱 카드&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;보관함 이용, 비상 결제용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;필수장비&lt;/td&gt;
&lt;td&gt;스마트 워치&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;페이스 체크용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;필수장비&lt;/td&gt;
&lt;td&gt;이어폰&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;대회 규정 확인 필요&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;반팔 / 싱글렛&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;날씨에 맞게 선택&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;러닝 쇼츠&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;장거리 착용감 중요&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;러닝 양말&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;마찰 방지, 쿠셔닝 고려&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;모자&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;햇빛 차단, 체온 조절&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;뉴트리션&lt;/td&gt;
&lt;td&gt;에너지젤&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;보급 계획에 맞춰 준비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;뉴트리션&lt;/td&gt;
&lt;td&gt;식염 포도당&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;염분 보충용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;뉴트리션&lt;/td&gt;
&lt;td&gt;마그네슘&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;쥐 예방 목적&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;뉴트리션&lt;/td&gt;
&lt;td&gt;아미노산&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;개인 루틴에 따라 준비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;히트 랙 (동절기)&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;기온 낮을 때 사용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;러닝 벨트&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;젤, 휴대폰 수납용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;슬리퍼&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;레이스 후 회복용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;마사지볼&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;레이스 전후 근막 이완&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;씻고 갈아입을 속옷&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;대회 후 갈아입기용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;니플 밴드&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;쓸림 방지&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;스포츠 테이프&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;부상 예방 및 보조&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;진통제&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;복용 여부는 개인 판단&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;헤임타 풀링바&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;개인 회복 루틴용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;충전기, 보조배터리&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;워치/휴대폰 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;팔토시&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;일교차, 자외선 대응&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;바세린&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;쓸림 방지&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;선크림&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;장시간 야외 노출 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트레일러닝 준비물&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;index-col&quot;&gt;번호&lt;/th&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;단거리(20~30K)&lt;/th&gt;
&lt;th&gt;중거리(40~50K)&lt;/th&gt;
&lt;th&gt;장거리 (70K+)&lt;/th&gt;
&lt;th&gt;특이사항&lt;/th&gt;
&lt;th class=&quot;check-col&quot;&gt;체크&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;필수장비&lt;/td&gt;
&lt;td&gt;배번표(bib)&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;대회 참가 필수&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;장비&lt;/td&gt;
&lt;td&gt;러닝 베스트&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;필수 장비 수납용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;장비&lt;/td&gt;
&lt;td&gt;개인 물컵&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;친환경 정책으로 필수화 추세&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;장비&lt;/td&gt;
&lt;td&gt;물통 / 하이드레이션&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;급수 구간 간격 고려&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;안전&lt;/td&gt;
&lt;td&gt;응급처치 키트&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;소독약, 반창고, 테이프, 알콜솜 포함&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;안전&lt;/td&gt;
&lt;td&gt;서바이벌 블랭킷&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;저체온 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;장비&lt;/td&gt;
&lt;td&gt;헤드랜턴&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status unnecessary&quot;&gt;불필요&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;야간 구간 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;스틱 / 폴&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;고도 많은 코스에서 유리&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;방수자켓&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;우천, 강풍, 저체온 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;장비&lt;/td&gt;
&lt;td&gt;보조배터리&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status unnecessary&quot;&gt;불필요&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;휴대폰, 워치 충전 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;뉴트리션&lt;/td&gt;
&lt;td&gt;에너지젤&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;보급 계획 기준 준비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;뉴트리션&lt;/td&gt;
&lt;td&gt;연양갱 / 에너지바&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;고형 보급 대체용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;뉴트리션&lt;/td&gt;
&lt;td&gt;이온 음료&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;전해질 보충용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;반팔티&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;기온 따라 반팔/긴팔 조합&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;러닝바지&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;활동성 좋은 제품 추천&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;팔토시&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;자외선, 체온 조절용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;카프 슬리브&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;피로도 관리용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;선크림&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;장시간 야외 노출 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;고글&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;강한 햇빛, 바람, 먼지 대응&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;모자&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;햇빛 차단 및 땀 관리&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;양말 / 팬티&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;장거리는 여분 1개 권장&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;씻은 후 갈아입을 여벌옷&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;레이스 종료 후 갈아입기용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;바세린&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;쓸림 방지&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류 / 기타&lt;/td&gt;
&lt;td&gt;니플 밴드&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;장시간 마찰 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;장비&lt;/td&gt;
&lt;td&gt;스마트워치(가민, 순토) / GPS 워치&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;페이스 및 경로 확인용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;러닝 이어폰&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;대회 규정 확인 필요&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;트레일러닝 신발&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;접지력 좋은 트레일 전용 필수&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;방수바지&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;우천 및 저체온 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;긴팔상의&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;체온 유지 및 기상 변화 대응&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;방수장갑&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;저체온 및 비 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;장비&lt;/td&gt;
&lt;td&gt;크램픽스&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;근육 경련 대비&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;뉴트리션&lt;/td&gt;
&lt;td&gt;포도당 / 프레스온 / 마그네슘&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status recommended&quot;&gt;권장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;에너지 및 근경련 예방&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;index-col&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;장비&lt;/td&gt;
&lt;td&gt;포카락&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status unnecessary&quot;&gt;불필요&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status optional&quot;&gt;선택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;status required&quot;&gt;필수&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;CP에서 식사 시 사용&lt;/td&gt;
&lt;td class=&quot;check-col&quot;&gt;&lt;input class=&quot;check-item&quot; type=&quot;checkbox&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>끄적끄적</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/510</guid>
      <comments>https://yolo2429.tistory.com/510#entry510comment</comments>
      <pubDate>Thu, 2 Apr 2026 00:59:54 +0900</pubDate>
    </item>
    <item>
      <title>[번역] CSS in 2026: 프런트엔드 개발을 바꾸는 새로운 기능들</title>
      <link>https://yolo2429.tistory.com/525</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://blog.logrocket.com/css-in-2026/&quot;&gt;https://blog.logrocket.com/css-in-2026/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpiZDs/dJMcacbsFOi/7dnndOl20Qq4oSBJ5aOqGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpiZDs/dJMcacbsFOi/7dnndOl20Qq4oSBJ5aOqGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpiZDs/dJMcacbsFOi/7dnndOl20Qq4oSBJ5aOqGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpiZDs%2FdJMcacbsFOi%2F7dnndOl20Qq4oSBJ5aOqGK%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;895&quot; height=&quot;597&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한동안 웹사이트를 만들어 왔다면, CSS에 대해 어느 정도 확고한 의견이 있을 겁니다. 디자인과 레이아웃에는 당연한 선택이지만, 기본 애니메이션을 넘어 더 인터랙티브한 기능이 필요해지면 대부분의 개발자는 본능적으로 자바스크립트에 손을 뻗습니다. 하지만 끊임없이 발전하는 플랫폼 덕분에 웹 기능을 구현할 때 &quot;CSS는 디자인, 자바스크립트는 인터랙션&quot;이 더 이상 기본값일 필요가 없어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모던 CSS는 이제 커스텀 스크립트가 필요했던 복잡한 애니메이션과 사용자 인터렉션을 처리할 만큼 강력합니다. 이 글에서는 CSS에 새로 도입되는 최신 기능들을 살펴보고, 자바스크립트로 익숙하게 구현하던 수준의 인터랙티비티를 유지하면서도 개발 워크플로를 어떻게 단순화할 수 있는지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.logrocket.com/stop-using-js-for-css/&quot;&gt;자바스크립트를 한 줄도 작성하지 않고&lt;/a&gt; 완전히 커스터마이징 가능한 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; 요소를 만들거나, &lt;code&gt;scrollLeft&lt;/code&gt;를 계산하거나 여러 이벤트 리스너를 연결하지 않고도 인터랙티브 마커가 달린 스크롤 가능한 캐러셀을 만드는 세상을 상상해 보세요. 바로 이런 것들이 새로운 CSS 기능으로 가능해지고 있으며, 분명 기대할 만한 일입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2026년 CSS의 새로운 기능&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;b&gt;참고.&lt;/b&gt; 이 기능들은 대부분 매우 새롭고 아직 브라우저에 배포되는 중이므로, 프로덕션에서 사용하기에는 아직 이릅니다. 이 기능들이 어떻게 동작하는지 더 깊이 알고 싶다면 Chrome DevRel 팀의 &lt;a href=&quot;https://chrome.dev/css-wrapped-2025/&quot;&gt;CSS Wrapped 2025&lt;/a&gt; 글을 확인해 보세요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appearance: base-select&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;를 새로운 커스터마이징 모드로 전환하여, 네이티브 동작을 잃지 않으면서 스타일을 적용하고 피커를 꾸밀 수 있게 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;select::picker(select)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커스터마이징 가능한 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;의 드롭다운 표면을 나타내는 의사 요소(pseudo-element)로, 그림자, 테두리, 간격 등을 스타일링할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;selectedcontent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;select 필드에 표시되는 선택된 옵션의 스타일을 지정합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;::scroll-button()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스크롤 가능한 컨테이너에 생성되는 버튼으로, 클릭하면 좌우로 스크롤합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;::scroll-marker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;각 스크롤 항목과 쌍을 이루는 생성 요소로, 브라우저가 페이지네이션 점이나 시각적 인디케이터로 사용할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;::scroll-marker-group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;생성된 스크롤 마커들의 컨테이너로, 스타일링하거나 배치(예: 하단 중앙)할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scroll-target-group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;링크 컨테이너에 적용할 수 있는 속성으로, CSS가 현재 뷰에 있는 타깃과 일치하는 링크를 매칭(&lt;code&gt;:target-current&lt;/code&gt;)할 수 있게 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:target-current&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;타깃(ID 앵커)이 현재 스크롤 활성 요소인 링크(또는 다른 타깃 가능 요소)와 매칭하는 선택자입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container-type: scroll-state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;요소에 스크롤 상태 쿼리를 활성화하여, 스크롤 컨테이너가 특정 스냅 또는 고정 상태인지 CSS가 반응할 수 있게 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@container scroll-state(snapped: x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;컨테이너가 x축에서 &quot;스냅된&quot; 스크롤 위치일 때 스타일을 적용하는 컨테이너 쿼리입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sibling-index()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;형제 요소 중 1부터 시작하는 위치를 반환하며, 동적 딜레이와 레이아웃 규칙에 유용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sibling-count()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;형제 요소의 총 개수를 반환하며, 개수 기반 레이아웃이나 비례 스타일링에 유용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attr()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;속성을 색상으로 읽어 &lt;code&gt;background-color&lt;/code&gt; 같은 프로퍼티에 사용할 수 있는 타입 지정 &lt;code&gt;attr()&lt;/code&gt; 함수 호출입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@starting-style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;트랜지션이나 애니메이션 시퀀스의 시작 시점에 스타일을 정의할 때 사용하는 컨테이너 쿼리 유사 블록입니다(실험적 구문).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 새로운 기능들의 실제 사용 사례를 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데모: 네이티브 HTML select 커스터마이징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 몇 년간 기다려 온 기능이니, 실제로 어떻게 동작하는지 살펴보겠습니다. &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; 요소는 접근 가능한 드롭다운을 위한 브라우저 내장 솔루션이지만, 스타일링에는 항상 제약이 있었습니다. 패딩을 조정하거나 색상을 바꾸는 것 이상의 작업을 하려면 보통 추가 마크업, 자바스크립트 핸들러, 그리고 네이티브 동작을 모방하기 위한 온갖 복잡함을 동원해 완전히 커스텀 드롭다운을 만들어야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스터마이징 가능한 select를 사용하면 두 세계의 장점을 결합할 수 있습니다. 실제 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;의 네이티브 접근성과 시맨틱을 유지하면서, 이전에는 자바스크립트 기반 컴포넌트에서만 가능했던 수준의 스타일링 유연성을 갖출 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 데모에서는 세 가지 최신 CSS 기능을 사용하여 포켓몬 선택기를 만들겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시맨틱과 접근성을 위해 네이티브 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;를 사용&lt;/li&gt;
&lt;li&gt;각 옵션에 아이콘과 배경색을 표시&lt;/li&gt;
&lt;li&gt;옵션이 엇갈린 타이밍으로 슬라이드하며 나타나는 애니메이션 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/01/up_1_pokemon-gif.gif&quot; alt=&quot;gif of pokemon selector for css demo&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고.&lt;/b&gt; 이 기능들은 Chrome 135 이상이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 것은 하나의 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; 요소와 몇 개의 &lt;code&gt;data-*&lt;/code&gt; 속성으로 이루어집니다. 인터랙티비티는 다음 기능들에서 나옵니다. &lt;code&gt;appearance: base-select&lt;/code&gt;(및 select 피커), 트리 카운팅 함수, 그리고 타입 지정 &lt;code&gt;attr()&lt;/code&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;appearance: base-select&lt;/code&gt;과 select 피커&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 단계는 컨트롤을 커스터마이징 모드로 전환하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;select,
select::picker(select) {
  appearance: base-select;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/appearance#base-select&quot;&gt;&lt;code&gt;appearance: base-select&lt;/code&gt;&lt;/a&gt;는 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;를 새로운 커스터마이징 렌더링 모델로 전환하며, 점진적 향상(progressive enhancement) 접근법으로도 훌륭합니다. 지원하지 않는 브라우저는 단순히 프로퍼티를 무시하고 select를 정상적으로 렌더링합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전환이 완료되면 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::picker&quot;&gt;&lt;code&gt;::picker(select)&lt;/code&gt;&lt;/a&gt; &lt;a href=&quot;https://blog.logrocket.com/css-pseudo-elements-guide/&quot;&gt;의사 요소&lt;/a&gt;가 드롭다운 표면 자체를 나타내므로, 다른 UI 패널처럼 스타일을 적용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;select::picker(select) {
  margin-block-end: 1em;
  border-radius: 12px;
  border: 1px solid #e0e0e0;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스터마이징 가능한 select를 사용하면 브라우저가 드롭다운의 많은 복잡한 부분을 대신 처리해 줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자동 오버플로 처리.&lt;/b&gt; 드롭다운이 가용 공간보다 크면 브라우저가 오버플로를 처리합니다. 피커 높이를 제한하고 필요할 때 스크롤 가능하게 만들어 주므로, 화면 밖으로 넘치거나 높이를 수동으로 계산할 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;앵커 포지셔닝 폴백.&lt;/b&gt; 피커는 새로운 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Anchor_positioning&quot;&gt;앵커 포지셔닝 구문&lt;/a&gt;을 사용하여 트리거 요소에 고정되므로, 가용 뷰포트 공간을 기반으로 최적의 배치를 자동으로 선택할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/01/2_baby-pokemon.png&quot; alt=&quot;anchor positioning fallback 1&quot; /&gt;&lt;img src=&quot;https://blog.logrocket.com/wp-content/uploads/2026/01/3_dropdown.png&quot; alt=&quot;anchor positioning fallback 2&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;b&gt;내장 포커스 관리.&lt;/b&gt; 포커스 동작이 네이티브로 처리되므로, select가 열리면 포커스가 예측 가능하게 피커로 이동하고, 닫히면 포커스가 적절히 돌아옵니다. 직접 포커스 트래핑이나 &quot;트리거로 포커스 반환&quot; 로직을 구현할 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;완전한 키보드 내비게이션(화살표 키, Enter, Escape).&lt;/b&gt; 사용자가 &lt;code&gt;Arrow&lt;/code&gt; 키로 옵션을 탐색하고, &lt;code&gt;Enter&lt;/code&gt;로 선택을 확정하고, &lt;code&gt;Escape&lt;/code&gt;로 피커를 닫을 수 있습니다. 일반 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;와 동일합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;더 풍부한 옵션 콘텐츠 지원.&lt;/b&gt; 각 &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&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;또 하나의 큰 장점은 내장된 점진적 향상 모델입니다. 브라우저가 아직 커스터마이징 가능한 select를 지원하지 않으면 사용자는 일반 네이티브 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; 요소를 받게 됩니다. 아무것도 깨지지 않습니다. 폴리필도 필요 없고, 컴포넌트의 두 가지 버전을 유지할 필요도 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;sibling-index()&lt;/code&gt;로 엇갈린 애니메이션 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 &lt;a href=&quot;https://blog.logrocket.com/simplifying-css-animations-display-size-properties/&quot;&gt;애니메이션을 추가&lt;/a&gt;합니다. 드롭다운이 열리면 각 옵션이 약간의 딜레이를 두고 옆에서 슬라이드합니다. 모든 옵션에 수동으로 인덱스를 지정하는 대신, 트리 카운팅 함수를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;option {
  transition: opacity 0.25s ease, translate 0.5s ease;
  transition-delay: calc(0.2s * (sibling-index() - 1));

  @starting-style {
    opacity: 0;
    translate: 30px 0;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/sibling-index&quot;&gt;&lt;code&gt;sibling-index()&lt;/code&gt;&lt;/a&gt;는 형제 요소 중 1부터 시작하는 위치를 제공합니다. 따라서 첫 번째 보이는 옵션은 &lt;code&gt;0.2s * (1 - 1)&lt;/code&gt;, 즉 &lt;code&gt;0s&lt;/code&gt;의 딜레이를 갖습니다. 다음은 &lt;code&gt;0.2s&lt;/code&gt;, 그 다음은 &lt;code&gt;0.4s&lt;/code&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;트리 카운팅 함수가 없던 시절에는 CSS에서 같은 엇갈린 효과를 내려면 훨씬 번거로웠습니다. &lt;code&gt;:nth-child()&lt;/code&gt; 선택자의 긴 목록으로 딜레이를 하드코딩하거나, HTML의 모든 항목에 &lt;code&gt;--index&lt;/code&gt; 커스텀 프로퍼티를 수동으로 추가해야 했습니다. 두 방법 모두 동작하긴 했지만 금세 지저분해졌고, 리스트가 바뀔 때 업데이트하는 것을 잊어버리기 쉬웠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 기반 스타일링을 위한 고급 &lt;code&gt;attr()&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 이 데모는 시각적 디테일을 &lt;code&gt;data-*&lt;/code&gt; 속성에 보관하기 위해 타입 지정 &lt;code&gt;attr()&lt;/code&gt; 함수를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;attr()&lt;/code&gt; 함수는 꽤 오래전부터 기본적으로 사용 가능했습니다. 하지만 최근까지는 &lt;code&gt;content&lt;/code&gt; 프로퍼티에서만 안정적으로 사용할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 타입 지정 버전의 &lt;code&gt;attr()&lt;/code&gt;을 사용하면, 브라우저에 어떤 타입을 기대하는지 알려주기만 하면 CSS의 더 많은 곳에서 속성 값을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데모에서 각 옵션은 호버 배경색을 정의하는 &lt;code&gt;data-bg-color&lt;/code&gt; 속성을 포함하며, CSS에서 직접 그 값을 읽습니다.&lt;/p&gt;
&lt;pre class=&quot;rsl&quot;&gt;&lt;code&gt;// HTML
&amp;lt;option data-bg-color=&quot;#F8C9A0&quot; value=&quot;charmander&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;// CSS
option {
  background-color: attr(data-bg-color color, transparent);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성을 명시적으로 색상으로 취급하기 때문에 브라우저가 올바르게 파싱하며, 속성이 없을 때를 위한 폴백 값도 안전하게 제공할 수 있습니다. 결과적으로 더 데이터 기반 스타일링 접근법이 됩니다. CSS를 건드리지 않고 HTML에서 테마 색상을 조정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;appearance: base-select&lt;/code&gt;, select 피커, 트리 카운팅 함수, 타입 지정 &lt;code&gt;attr()&lt;/code&gt;을 조합하면 근본적으로 여전히 실제 &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; 요소인 기능이 풍부한 애니메이션 드롭다운을 만들 수 있습니다. 원하는 대로 커스터마이징하면서도 네이티브 동작과 내장 &lt;a href=&quot;https://blog.logrocket.com/ai-has-an-accessibility-problem/&quot;&gt;접근성 기능&lt;/a&gt;을 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교를 위해, 자바스크립트로 비슷한 드롭다운을 만들면 어떤 모습인지 보겠습니다(스포일러. CSS로 만든 것과 비슷한 수준을 달성하는 데 대략 150줄 이상의 자바스크립트가 필요합니다).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CSS in 2026: 앞으로의 전망&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저에게 이 데모는 CSS가 향하는 방향에서 가장 흥미로운 부분을 보여줍니다. 150줄 이상의 자바스크립트를 단 몇 가지 CSS 기능으로 대체하는 것은 정말 놀랍습니다. 예전과 같은 수준의 복잡함을 달성할 수 있지만, 이제 훨씬 적은 노력으로 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플랫폼이 키보드 내비게이션, 포커스 처리, 합리적인 포지셔닝 동작 같은 기본값을 제공하면, 모든 코드베이스에서 같은 인터렉션 패턴을 재구축하거나 매번 새 라이브러리를 설치하는 대신, 기존 컴포넌트를 개선하는 데 더 많은 시간을 쓸 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 시대에 특히 적절하게 느껴지기도 합니다. 이런 기능들이 단순하고 선언적일수록, AI 에이전트가 솔루션을 과도하게 엔지니어링하거나 요청하지 않은 동작을 만들어낼 가능성이 줄어듭니다.&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;b&gt;자바스크립트가 무거운 UI 컴포넌트 하나를 재검토하세요.&lt;/b&gt; 이 새로운 기능들을 사용하기 시작하는 가장 좋은 방법은 코드베이스에서 이미 구현된 기능에 어디서 활용할 수 있는지 찾아보는 것입니다. 캐러셀, 툴팁, &lt;a href=&quot;https://www.youtube.com/watch?v=WwSXibMRWm8&quot;&gt;드롭다운&lt;/a&gt;이 좋은 후보입니다. 비교적 작은 UI에 많은 코드가 들어가는 경우가 많고, 이를 만드는 데 필요한 네이티브 기능 대부분이 이미 기본 지원되기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;접근성을 빠뜨리지 마세요.&lt;/b&gt; 네이티브 지원이 보통 더 나은 출발점을 제공하지만, 테스트를 대체하지는 않습니다. 키보드로 데모를 시도해 보고, 가능하면 &lt;a href=&quot;https://blog.logrocket.com/what-using-a-screen-reader-taught-me/&quot;&gt;스크린 리더&lt;/a&gt;를 사용하여 모든 사람이 이 기능들을 사용할 수 있는지 확인하세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;호기심을 유지하세요.&lt;/b&gt; 이런 새 기능들은 완전히 기본 지원될 때까지는 안 되는 법입니다. 최신 상태를 유지하는 가장 쉬운 방법은 이 기능들의 최신 변경 사항을 주시하는 것입니다(&lt;a href=&quot;https://web.dev/blog&quot;&gt;web.dev 블로그&lt;/a&gt;를 추천합니다). 그래야 프로덕션에 드디어 도입할 수 있는 시점을 알 수 있습니다. 브라우저 지원을 지켜보고, 내부 도구에서 실험하고, 지원이 안정화될 때까지 프로덕션 배포는 보수적으로 유지하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 개인적으로, CSS가 모든 무거운 작업을 해주는 덕분에 코드를 덜 쓸 수 있다면, 전적으로 찬성입니다.&lt;/p&gt;</description>
      <category>Web Frontend Developer</category>
      <category>CSS</category>
      <category>html</category>
      <category>개발</category>
      <category>디자인</category>
      <category>레이아웃</category>
      <category>웹</category>
      <category>자바스크립트</category>
      <category>프로그래밍</category>
      <category>프론트엔드</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/525</guid>
      <comments>https://yolo2429.tistory.com/525#entry525comment</comments>
      <pubDate>Fri, 27 Mar 2026 14:25:58 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 이제 모던 CSS가 SPA를 끝낼 때입니다</title>
      <link>https://yolo2429.tistory.com/524</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://www.jonoalderson.com/conjecture/its-time-for-modern-css-to-kill-the-spa/&quot;&gt;https://www.jonoalderson.com/conjecture/its-time-for-modern-css-to-kill-the-spa/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;file_00000000be4861f49d1f8af54cec64de-760x350-2x.png&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nxTMt/dJMcahX8u5D/eNdej4Y57a9ZjppWikIrd0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nxTMt/dJMcahX8u5D/eNdej4Y57a9ZjppWikIrd0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nxTMt/dJMcahX8u5D/eNdej4Y57a9ZjppWikIrd0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnxTMt%2FdJMcahX8u5D%2FeNdej4Y57a9ZjppWikIrd0%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;1520&quot; height=&quot;700&quot; data-filename=&quot;file_00000000be4861f49d1f8af54cec64de-760x350-2x.png&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이티브 CSS 트랜지션은 클라이언트 사이드 라우팅의 가장 강력한 근거를 조용히 무너뜨렸습니다. 그런데도 사람들은 성능 좋은 웹사이트 대신 끔찍한 앱을 계속 만들고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱처럼 보여야 한다는 착각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;i&gt;앱처럼 느껴지게 만들어 주세요.&lt;/i&gt;&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획 단계 어딘가에서 누군가 이 말을 꺼냅니다. CMO일 수도, 디지털 리드일 수도, 브랜드 매니저일 수도 있습니다. 그리고 이 한마디로 아키텍처가 결정됩니다. SPA로 가겠다고요. 아마 리액트겠죠. 뷰일 수도 있습니다. 거의 확실히 Vercel이나 Netlify에 배포될 것이고, 헤드리스 CMS와 GraphQL 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;가정은 단순했습니다. 매끄러운 내비게이션을 구현하려면 앱을 만들어야 한다는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가정은 이제 낡았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPA의 거짓 약속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPA가 기본 선택지가 된 이유는 더 _뛰어나서_가 아닙니다. 한동안, 부드러운 느낌을 줄 수 있는 유일한 방법이었기 때문입니다. 페이지 사이에 화면이 하얗게 번쩍이거나 스크롤 위치가 뒤틀리지 않는 경험을 제공하는 유일한 방법이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현실은 조금 다릅니다. 대부분의 SPA는 약속한 매끄러움을 실제로 제공하지 못합니다.&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;i&gt;매끄러워 보이지만&lt;/i&gt;, 사실 두 개의 로딩 상태 사이를 페이드하는 것에 불과함&lt;/li&gt;
&lt;li&gt;깨진 스크롤 복원&lt;/li&gt;
&lt;li&gt;일관되지 않은 포커스 동작&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;이론적인 이야기가 아닙니다. Next.js, Gatsby, Nuxt로 만든 대부분의 사이트를 보세요. 네이티브 내비게이션을 &lt;i&gt;흉내 내기&lt;/i&gt; 위해 &lt;i&gt;킬로바이트&lt;/i&gt; 단위(종종 &lt;i&gt;메가바이트&lt;/i&gt; 단위)의 자바스크립트를 전송하고 있습니다. 라우팅 로직, 하이드레이션 코드, 로딩 스피너 등 전부 브라우저가 이미 네이티브로 할 줄 알았던 것을 짜깁기하려는 것뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매끄러움 대신 시뮬레이션을 얻게 됩니다. 빠르고 안정적이며 SEO 친화적인 경험 대신, 우리가 버린 네이티브 동작을 재현하려는 무거운 자바스크립트 기계를 얻게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우리는 &quot;빠르게 느껴지게&quot; 만들려고 산더미 같은 JS를 쌓아 올리면서, 모든 것을 더 느리게 만들고 있었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로 이 주제를 &lt;a href=&quot;https://www.jonoalderson.com/conjecture/javascript-broke-the-web-and-called-it-progress/&quot;&gt;JavaScript broke the web&lt;/a&gt;에서 더 깊이 다루었습니다. JS 중심 개발에 대한 집착이 어떻게 웹의 기반을 적극적으로 잠식하는지 정리한 글입니다.&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;모던 브라우저, 구체적으로 크롬이나 에지 같은 크로미움 기반 브라우저는 이제 네이티브 선언적 페이지 트랜지션을 지원합니다. &lt;a href=&quot;https://developer.chrome.com/docs/web-platform/view-transitions/&quot;&gt;View Transitions API&lt;/a&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;여기서 말하는 &quot;모던 CSS&quot;란 &lt;a href=&quot;https://developer.chrome.com/docs/web-platform/view-transitions/&quot;&gt;View Transitions&lt;/a&gt;, &lt;a href=&quot;https://developer.chrome.com/docs/web-platform/implementing-speculation-rules/&quot;&gt;Speculation Rules&lt;/a&gt;, 그리고 내비게이션, 인터렉션, 레이아웃을 처음부터 처리하도록 설계된 네이티브 브라우저 기능으로 돌아가는 것을 통칭하는 표현입니다. 이런 기능들은 자바스크립트로 브라우저를 다시 작성하지 않고도 풍부하고 매끄러운 경험을 구축할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담 &amp;mdash; CSS는 선언적이고, 탄력적이며, 표현력이 풍부하고, 확장 가능하며, 점점 더 직관적입니다. 평범한 HTML을 작성할 줄 아는 사람이라면 누구나 다룰 수 있습니다. 이런 구조적 명료함은 &lt;a href=&quot;https://www.jonoalderson.com/conjecture/why-semantic-html-still-matters/&quot;&gt;Why semantic HTML still matters&lt;/a&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;공유 요소 애니메이션 (예: 섬네일 &amp;rarr; 상품 상세)&lt;/li&gt;
&lt;li&gt;헤더나 내비게이션 바 같은 지속 요소 유지&lt;/li&gt;
&lt;li&gt;실제 URL, 실제 페이지 로드, JS 라우팅 꼼수 없이 이 모든 것을 실현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적으로 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  기본 크로스 페이지 페이드 트랜지션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 몇 줄만으로 페이지 간 부드러운 시각적 전환을 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 페이지와 도착 페이지 양쪽에 다음을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: fade 0.3s ease both;
}

@keyframes fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 전부입니다. 브라우저가 트랜지션을 처리합니다. 클라이언트 사이드 라우팅도, 하이드레이션도, 로딩 스피너도 필요 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  공유 요소 트랜지션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;섬네일 이미지를 다음 페이지의 전체 크기 상품 이미지로 애니메이션하고 싶다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트가 필요 없습니다. 양쪽 페이지의 요소에 같은 &lt;code&gt;view-transition-name&lt;/code&gt;을 지정하기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상품 목록 페이지에서는 이렇게 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;a href=&quot;/product/red-shoes&quot;&amp;gt;
  &amp;lt;img src=&quot;/images/red-shoes-thumb.jpg&quot; style=&quot;view-transition-name: product-image;&quot; /&amp;gt;
&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상품 상세 페이지에서는 이렇게 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;img src=&quot;/images/red-shoes-large.jpg&quot; style=&quot;view-transition-name: product-image;&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 내비게이션 사이의 요소를 매칭하고 애니메이션합니다. 위치, 스케일, 불투명도, 레이아웃까지 모두 CSS로 애니메이션할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  JS 기반 트랜지션이 필요하다면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 내에서 트랜지션을 수동으로 트리거할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;document.startViewTransition(() =&amp;gt; {
  document.body.classList.toggle('dark-mode');
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭 토글이나 테마 전환 같은 경우에 완벽합니다. 프레임워크나 하이드레이션 레이어가 필요 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Speculation Rules: JS 없는 즉각적인 내비게이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View Transitions는 매끄러움을 만들어 줍니다. 그럼 &lt;i&gt;빠르게&lt;/i&gt; 하려면 어떻게 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &lt;a href=&quot;https://developer.chrome.com/docs/web-platform/implementing-speculation-rules/&quot;&gt;Speculation Rules&lt;/a&gt;가 등장할 차례입니다. 사용자가 링크에 마우스를 올리거나 터치하는 것처럼 사용자 행동을 기반으로, 클릭하기 전에 브라우저가 전체 페이지를 미리 로드하거나 미리 렌더링할 수 있게 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;script type=&quot;speculationrules&quot;&amp;gt;
{
  &quot;prerender&quot;: [
    {
      &quot;where&quot;: {
        &quot;selector_matches&quot;: &quot;a&quot;
      }
    }
  ]
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 어떨까요? &lt;i&gt;즉각적인&lt;/i&gt; 내비게이션입니다. 대기도 없고, 로딩도 없고, 스피너도 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 주의 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Speculation Rules는 성능 배율기입니다. 가벼운 사이트에서는 모든 것이 즉각적으로 느껴지게 만들어 줍니다. 하지만 페이지가 느리거나 비대하거나 JS가 무겁다면, 추측이 그 비용을 앞당겨 지불하게 할 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사이트가 비대하다면, 추측은 여전히 추측할 것이며 그 대가는 사용자가 치릅니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 CPU, 네트워크 대역폭, 모바일 배터리가 낭비된다는 뜻입니다. 종종 사용자가 방문조차 하지 않을 페이지를 위해서요.&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;가장 명확한 예시 중 하나가 &lt;a href=&quot;https://web.dev/articles/bfcache&quot;&gt;Back/Forward Cache(bfcache)&lt;/a&gt;입니다. 사용자가 뒤로가기나 앞으로가기를 할 때 전체 페이지의 스냅샷을 찍어 즉시 복원할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상 공짜 성능 향상입니다. 하지만 &quot;잘 작동하는&quot; 페이지에서만 가능합니다. 제멋대로인 자바스크립트도 없고, 가로채진 내비게이션도 없고, 생명주기의 혼란도 없어야 합니다. 깔끔한 선언적 아키텍처만 있으면 됩니다. HTML과 CSS만으로요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 잘 구조화된 멀티 페이지 사이트와 아름답게 어울립니다. 하지만 대부분의 SPA에서는 아예 사용할 수 없습니다. SPA를 정의하는 바로 그 디자인 패턴들 &amp;mdash; 가로채진 라우팅, 클라이언트 사이드 렌더링, 복잡한 상태 관리 &amp;mdash; 이 bfcache가 의존하는 전제를 깨뜨리기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 훨씬 더 큰 주제의 축소판입니다. 브라우저는 단순함과 회복탄력성에 보상하는 방향으로 진화하고 있습니다. 우리가 처음부터 받아들였어야 할 그런 웹을 위해 만들어지고 있습니다. 그리고 SPA는 점점 더 이질적인 존재가 되고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  SPA vs MPA: 성능 현실 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;평균적인 Next.js 마케팅 사이트&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS 번들: 1 &amp;ndash; 3MB&lt;/li&gt;
&lt;li&gt;TTI: ~3.5 &amp;ndash; 5초 (하이드레이션 전략에 따라 다름)&lt;/li&gt;
&lt;li&gt;라우트 전환: 시뮬레이션&lt;/li&gt;
&lt;li&gt;SEO: 복잡하고 취약&lt;/li&gt;
&lt;li&gt;스크롤/앵커 동작: 불안정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모던 MPA + View Transitions + Speculation Rules&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS 번들: 0KB (선택적 향상만)&lt;/li&gt;
&lt;li&gt;TTI: ~1초&lt;/li&gt;
&lt;li&gt;라우트 전환: 실제, 네이티브&lt;/li&gt;
&lt;li&gt;SEO: 간단&lt;/li&gt;
&lt;li&gt;스크롤/포커스/히스토리: 브라우저 기본값, 완벽&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모던 CSS는 SPA의 동작을 대체하는 것에 그치지 않습니다. &lt;i&gt;능가합니다.&lt;/i&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;공유 상태가 필요 없습니다. 클라이언트 사이드 라우팅이 필요 없습니다. 모든 화면에 인터랙티브 컴포넌트가 필요하지 않습니다. 하지만 어느 순간부터 우리는 이 구분을 그만두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우리는 실시간 협업 UI를 위해 설계된 스택으로 이커머스 스토어, 문서 포털, 마케팅 사이트, 블로그를 만들고 있습니다. 미친 짓입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠 블록 여섯 개와 문의 폼 하나 있는 홈페이지에 하이드레이션, 서스펜스 바운더리, 렌더링 전략이 필요하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 마크업, 깔끔한 URL, 그리고 어쩌면 &amp;mdash; 어쩌면 &amp;mdash; 위에 얹는 약간의 인터랙티비티가 필요할 뿐입니다.&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;이해관계자가 &quot;앱처럼 느껴지게 해주세요&quot;라고 말합니다.&lt;/li&gt;
&lt;li&gt;개발팀이 Next.js나 Nuxt를 꺼냅니다.&lt;/li&gt;
&lt;li&gt;라우팅이 클라이언트 사이드로 넘어갑니다.&lt;/li&gt;
&lt;li&gt;성능이 곤두박질칩니다.&lt;/li&gt;
&lt;li&gt;이제 엣지 함수, 스트리밍, ISR, 로딩 전략, 디버깅 계획이 필요해집니다.&lt;/li&gt;
&lt;li&gt;그런데도 어째서인지... 일반 링크 클릭과 CSS 애니메이션보다 &lt;i&gt;여전히&lt;/i&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;리액트를 쓰고 싶다면 쓰세요. Tailwind든, Vite든, 뭐든 좋습니다. 다만 &lt;i&gt;필요하지&lt;/i&gt; 않다면 브라우저에 다 보내지 마세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이트는 사이트답게 만드세요. HTML을 사용하세요. 내비게이션을 사용하세요. 플랫폼을 사용하세요.&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;SPA는 일시적인 한계에 대한 영리한 해결책이었습니다. 하지만 그 한계는 더 이상 존재하지 않습니다.&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;Speculation Rules를 통한 즉각적인 프리렌더링 내비게이션&lt;/li&gt;
&lt;li&gt;우아한 점진적 저하(graceful degradation)&lt;/li&gt;
&lt;li&gt;깔끔한 마크업, 빠른 로드, 실제 URL&lt;/li&gt;
&lt;li&gt;&lt;i&gt;도와주고 싶어하는&lt;/i&gt; 플랫폼 &amp;mdash; 허락한다면&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 &quot;매끄러움&quot;을 위해 사이트를 SPA로 만들고 있다면, 브라우저가 이미 해결한 문제를 풀고 있는 것입니다. 그리고 그 대가를 복잡성, 성능, 유지보수성으로 치르고 있는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모던 서버 렌더링을 사용하세요. 실제 페이지를 사용하세요. CSS로 애니메이션하세요. 의도를 가지고 프리로드하세요. 자바스크립트를 줄이세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년답게 만드세요. 2018년 Gatsby 데모에 갇힌 것처럼 만들지 마세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 빠른 사이트, 더 행복한 사용자, 그리고 더 적은 후회를 얻게 될 것입니다.&lt;/p&gt;</description>
      <category>Web Frontend Developer</category>
      <category>CSS</category>
      <category>Spa</category>
      <category>Web</category>
      <category>개발</category>
      <category>웹</category>
      <category>자바스크립트</category>
      <category>프로그래밍</category>
      <category>프론트엔드</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/524</guid>
      <comments>https://yolo2429.tistory.com/524#entry524comment</comments>
      <pubDate>Fri, 27 Mar 2026 14:21:49 +0900</pubDate>
    </item>
    <item>
      <title>[LeetCode] 3600. Maximize Spanning Tree Stability with Upgrades (Hard)</title>
      <link>https://yolo2429.tistory.com/523</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 풀기 위해서는 MST(Minimum Spanning Tree, 최소 신장 트리)에 대해서 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spanning Tree(신장 트리)는 그래프 내의 모든 정점을 포함하는 트리이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3600-3.jpg&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;2157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgXFYY/dJMcaiP2mCc/V1caKMbx5D6Ni5jogRidb0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgXFYY/dJMcaiP2mCc/V1caKMbx5D6Ni5jogRidb0/img.jpg&quot; data-alt=&quot;스패닝 트리는 다음과 같이 여러 개가 나올 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgXFYY/dJMcaiP2mCc/V1caKMbx5D6Ni5jogRidb0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgXFYY%2FdJMcaiP2mCc%2FV1caKMbx5D6Ni5jogRidb0%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;1668&quot; height=&quot;2157&quot; data-filename=&quot;3600-3.jpg&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;2157&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;스패닝 트리는 사이클을 만들면 안 되고 n개의 노드를 (n-1)개의 간선으로 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 스패닝 트리를 연결하는 간선에 가중치(strength)가 있을 때 그 가중치의 값을 최소로 구하는 경우가 바로 MST이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 접근 : 문제를 단순화 하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서는 노드가 n개 주어지고 각각의 간선이 edges[i]로 주어진다고 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;edges[i] = [u, v, s, must] 인데 u - v 이어지는 무방향 간선이며 여기에 가중치가 s, 그리고 must가 1이면 필수, 0이면 한 번까지 업그레이드가 가능하다. 업그레이드를 하면 가중치는 2배가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;이 노드와 간선들의 그래프에서 MST를 구하고 그 MST들의 최대 가중치 값을 구하는 것이 문제&lt;/b&gt;&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n &amp;lt;= 10^5, u,v &amp;lt; 10^5, s &amp;lt;= 10^5 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 문제는 시간 복잡도를 O(N^2) 이상으로 푸는 순간 바로 시간 초과다. 이 점을 기억하고 문제를 접근해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. must 간선을 먼저 공략하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;edges[i]에서 must = 1 이면 그 가중치는 변하지 않고 반드시 들어가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 must = 1 인 간선들을 먼저 찾아서 연결해 본다. 만약 이 때 사이클이 생기면 -1을 리턴해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. DSU(Disjoint Set Union, 서로소 집합 자료구조)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 풀기 위해서는 DSU의 개념에 대해서 먼저 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로소 집합은 두 집합이 겹치는 원소가 없는 집합 관계를 의미한다. 예를 들어 {1,3,5}와 {2,4}는 서로소 집합 관계이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 어떤 원소가 해당 집합에 속해있는지를 알기 위해서 이 DSU 자료구조를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DSU에는 find와 union 이라는 두 가지의 중요한 연산이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;find(x) : 원소 x가 어느 집합에 속해 있는지 대표(root)를 찾아 반환&lt;/li&gt;
&lt;li&gt;union(a, b) : 원소 a가 속한 집합과 원소 b가 속한 집합을 결합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념이 다소 헷갈릴 수가 있어서 보충 설명을 하면 각각의 서로소 집합에는 하나씩 대표 노드가 존재한다. 그래서 그 집합에 포함된 요소들 중 하나를 인풋으로 find() 메서드에 넣으면 대표 노드를 아웃풋으로 반환하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 어떤 두 요소가 각각 속해있는 집합이 있다고 했을 때, 그 두 요소를 union() 메서드에 인풋으로 받으면, 그 두 집합의 합집합을 아웃풋으로 반환한다. 아래의 그림을 참고하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3600-4.jpg&quot; data-origin-width=&quot;1663&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn0ph7/dJMcagEL4th/K8Z2eaYymDvxzrZcWhXdkk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn0ph7/dJMcagEL4th/K8Z2eaYymDvxzrZcWhXdkk/img.jpg&quot; data-alt=&quot;find 메서드와 union 메서드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn0ph7/dJMcagEL4th/K8Z2eaYymDvxzrZcWhXdkk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn0ph7%2FdJMcagEL4th%2FK8Z2eaYymDvxzrZcWhXdkk%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;1663&quot; height=&quot;1244&quot; data-filename=&quot;3600-4.jpg&quot; data-origin-width=&quot;1663&quot; data-origin-height=&quot;1244&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;find 메서드와 union 메서드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이러한 그래프를 트리 형태로 작성할 수 있는데, 하나의 트리에서 어떤 요소가 대표 노드를 찾아가는 과정을 담은 배열이 parent 이다. parent[i] = j 라고 하는 것은 i 요소의 부모 요소가 j라는 말이고 이렇게 타고타고 올라가면 대표 노드를 찾을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 DSU 자료구조를 타입스크립트로 작성하면 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773406842169&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DSU {
  parent: number[];
  size: number[];
  components: number;

  constructor(n: number) {
    this.parent = Array.from({ length: n }, (_, i) =&amp;gt; i);
    this.size = Array(n).fill(1);
    this.components = n;
  }

  find(x: number): number {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x];
  }

  union(a: number, b: number): boolean {
    let pa = this.find(a);
    let pb = this.find(b);

    if (pa === pb) return false;

    if (this.size[pa] &amp;lt; this.size[pb]) {
      [pa, pb] = [pb, pa];
    }

    this.parent[pb] = pa;
    this.size[pa] += this.size[pb];
    this.components--;
    return true;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 DSU 자료구조를 구현한 다음에 우리가 위에서 살펴본 must 간선을 체크해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 간선들을 돌면서 must 간선일 경우(must === 1) 이 간선들의 최소 가중치를 찾고, 그 과정에서 사이클이 발견이 된다면 -1을 반환하게 한다. 타입스크립트 코드는 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773325376681&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function maxStability(
  n: number,
  edges: number[][],
  k: number
): number {
  let hasMust = false;
  let mustMin = Infinity;

  // 1) must 간선끼리 사이클 체크
  const mustDsu = new DSU(n);
  for (const [u, v, s, must] of edges) {
    if (must === 1) {
      hasMust = true;
      mustMin = Math.min(mustMin, s);
      if (!mustDsu.union(u, v)) {
        return -1;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&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. 모든 간선을 체크해서 그래프 연결 가능 여부를 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음으로는 모든 간선들을 다 연결해서 그래프를 연결할 수 있는지를 체크해야 한다. 새로운 DSU 인스턴스를 만들어서 모든 노드들을 union 메서드로 연결시킨다. 그리고 DSU의 component라는 변수는 지금 집합이 총 몇 개인지를 의미하는데, 처음 생성자 객체 DSU(n)에서는 이 값이 n이고, union이 하나씩 생길 때 마다 두 개의 집합이 하나가 되므로 -1을 해 주게 된다. 만약 모든 노드를 결합했는데 이 값이 1이 아니라면 어디선가 끊어져 있다는 뜻이고, 이는 그래프 연결이 이루어지지 않음을 의미해서 -1을 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1773407336102&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function maxStability(
  n: number,
  edges: number[][],
  k: number
): number {
  // 1) must 간선끼리 사이클 체크
  // ...

  // 2) 전체 그래프 연결 가능 여부 체크
  const allDsu = new DSU(n);
  for (const [u, v] of edges) {
    allDsu.union(u, v);
  }
  if (allDsu.components !== 1) {
    return -1;
  }
  
  // ...
}&lt;/code&gt;&lt;/pre&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. 다음으로 안정성을 체크한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불가능한 케이스를 제거했으면 이제 최대 가중치 값을 구해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 MST에서 어떤 간선 (u, v, s)가 안정성 X를 만족하려면 s &amp;gt;= X여야 한다. 만약 s가 X보다 작더라도 업그레이드가 가능하고 이 때 2*s &amp;gt;= X 이면 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 간선을 3종류로 나눌 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;바로 사용 가능 : s &amp;gt;= X&lt;/li&gt;
&lt;li&gt;업그레이드 하면 가능 : s &amp;lt; X 이고 2*s &amp;gt;= X&lt;/li&gt;
&lt;li&gt;불가능 2*s &amp;lt; X&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 결국 간선들을 골라서 모든 정점을 연결할 수 있는지를 빠르게 확인하면 된다. 여기서 이분 탐색과 Union-Find(DSU)를 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 판정(check) 하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 X = 6을 검사한다고 했을 때, 간선이 이렇게 있다고 가정하자. 업그레이드는 최대 k = 2&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A --- (8) --- B&lt;/li&gt;
&lt;li&gt;B --- (5) --- C&lt;/li&gt;
&lt;li&gt;A --- (3) --- C&lt;/li&gt;
&lt;li&gt;C --- (6) --- D&lt;/li&gt;
&lt;li&gt;B --- (4) --- D&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계 : s &amp;gt;= 6인 간선 먼저 다 붙이기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A --- (8) --- B&lt;/li&gt;
&lt;li&gt;C --- (6) --- D&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 연결은 [A B] [C D] 이렇게 되어 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계 : 업그레이드 하면 6 이상 되는 간선 보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5 -&amp;gt; 10 : 가능&lt;/li&gt;
&lt;li&gt;4 -&amp;gt; 8 : 가능&lt;/li&gt;
&lt;li&gt;3 -&amp;gt; 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;예를 들어 B --- (5) --- C 를 쓰면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[A B] --- upgrade --- [C D]&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 전체 연결을 완료할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773938758791&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const can = (x: number): boolean =&amp;gt; {
    const dsu = new DSU(n);

    // must 간선은 반드시 포함 + 업그레이드 불가
    for (const [u, v, s, must] of edges) {
      if (must === 1) {
        if (s &amp;lt; x) return false;
        if (!dsu.union(u, v)) return false;
      }
    }

    // 업그레이드 없이 x 이상인 일반 간선 먼저 사용
    for (const [u, v, s, must] of edges) {
      if (must === 0 &amp;amp;&amp;amp; s &amp;gt;= x) {
        dsu.union(u, v);
      }
    }

    // 업그레이드해서 x 이상이 되는 간선 사용
    let upgradesLeft = k;
    for (const [u, v, s, must] of edges) {
      if (must === 0 &amp;amp;&amp;amp; s &amp;lt; x &amp;amp;&amp;amp; s * 2 &amp;gt;= x &amp;amp;&amp;amp; upgradesLeft &amp;gt; 0) {
        if (dsu.union(u, v)) {
          upgradesLeft--;
        }
      }
    }

    return dsu.components === 1;
  };&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;질문1 : 어떤 간선을 업그레이드 해야 하나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;check(X)를 할 때&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 X 이상인 간선은 바로 쓰면 됨&lt;/li&gt;
&lt;li&gt;업그레이드가 필요한 간선은 컴포넌트를 실제로 합칠 때만 카운트&lt;/li&gt;
&lt;li&gt;DSU로 합쳐지지 않는 간선은 무시&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;2s &amp;gt;= X&lt;/li&gt;
&lt;li&gt;아직 다른 컴포넌트를 연결한다면&lt;/li&gt;
&lt;li&gt;업그레이드 1회 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;질문2 : must 간선은 어떻게 판정하나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;must 간선은 무조건 포함이 되어야 하고 업그레이드도 할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 must 간선의 가중치값의 최소를 mn 이라고 하면 정답은 절대 mn을 넘을 수 없다. 그래서 이분 탐색의 상한을 mn으로 둘 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, must 간선의 가중치가 3, 5, 7 이면 이분 탐색의 상한은 3인 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1773938674093&quot; class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;function maxStability(
  n: number,
  edges: number[][],
  k: number
): number {
  let hasMust = false;
  let mustMin = Infinity;

  // 1) must 간선끼리 사이클 체크
  // ...

  // 2) 전체 그래프 연결 가능 여부 체크
  // ...

  // upper bound
  let hi = 0;
  if (hasMust) {
    hi = mustMin;
  } else {
    for (const [, , s] of edges) {
      hi = Math.max(hi, s * 2);
    }
  }

  const can = (x: number): boolean =&amp;gt; {
    // ...
  };

  let lo = 1;
  while (lo &amp;lt; hi) {
    const mid = Math.floor((lo + hi + 1) / 2);
    if (can(mid)) {
      lo = mid;
    } else {
      hi = mid - 1;
    }
  }

  return lo;
}&lt;/code&gt;&lt;/pre&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. 최종 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 최종 구현은 다음과 같다.&lt;/p&gt;
&lt;h3 data-end=&quot;3360&quot; data-start=&quot;3330&quot; data-section-id=&quot;1rgn8v6&quot; data-ke-size=&quot;size23&quot;&gt;Step 1. must 간선만 먼저 DSU에 넣기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3463&quot; data-start=&quot;3361&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3389&quot; data-start=&quot;3361&quot; data-section-id=&quot;1rx8gs2&quot;&gt;합치다가 이미 같은 집합이면 사이클 &amp;rarr; -1&lt;/li&gt;
&lt;li data-end=&quot;3463&quot; data-start=&quot;3390&quot; data-section-id=&quot;qaju8w&quot;&gt;동시에 must 간선 최소 strength mn 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;3494&quot; data-start=&quot;3465&quot; data-section-id=&quot;1l9o28b&quot; data-ke-size=&quot;size23&quot;&gt;Step 2. 전체 그래프가 연결 가능한지 확인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3572&quot; data-start=&quot;3495&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3572&quot; data-start=&quot;3495&quot; data-section-id=&quot;szokvt&quot;&gt;모든 간선을 DSU에 넣어 봤는데도 하나로 안 합쳐지면 -1&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;3590&quot; data-start=&quot;3574&quot; data-section-id=&quot;1wirmwz&quot; data-ke-size=&quot;size23&quot;&gt;Step 3. 이분 탐색&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3705&quot; data-start=&quot;3591&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3602&quot; data-start=&quot;3591&quot; data-section-id=&quot;1nefp9s&quot;&gt;low = 1&lt;/li&gt;
&lt;li data-end=&quot;3631&quot; data-start=&quot;3603&quot; data-section-id=&quot;ydeoqe&quot;&gt;high = mn (must 간선이 있으면)&lt;/li&gt;
&lt;li data-end=&quot;3705&quot; data-start=&quot;3632&quot; data-section-id=&quot;140p7st&quot;&gt;중간값 mid = X에 대해 check(X) 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;3728&quot; data-start=&quot;3707&quot; data-section-id=&quot;1imfdf6&quot; data-ke-size=&quot;size23&quot;&gt;Step 4. check(X)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3906&quot; data-start=&quot;3729&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3740&quot; data-start=&quot;3729&quot; data-section-id=&quot;ishr4i&quot;&gt;DSU 새로 생성&lt;/li&gt;
&lt;li data-end=&quot;3766&quot; data-start=&quot;3741&quot; data-section-id=&quot;16zr7jr&quot;&gt;s &amp;gt;= X 인 간선들 먼저 union&lt;/li&gt;
&lt;li data-end=&quot;3824&quot; data-start=&quot;3767&quot; data-section-id=&quot;1gh8np3&quot;&gt;그 다음 2s &amp;gt;= X 인 간선들 중 아직 다른 컴포넌트를 잇는 간선을 업그레이드해서 union&lt;/li&gt;
&lt;li data-end=&quot;3843&quot; data-start=&quot;3825&quot; data-section-id=&quot;fbzm35&quot;&gt;업그레이드 횟수는 최대 k&lt;/li&gt;
&lt;li data-end=&quot;3906&quot; data-start=&quot;3844&quot; data-section-id=&quot;6wwukg&quot;&gt;마지막에 전체가 하나로 연결되면 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773938896789&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DSU {
  parent: number[];
  size: number[];
  components: number;

  constructor(n: number) {
    this.parent = Array.from({ length: n }, (_, i) =&amp;gt; i);
    this.size = Array(n).fill(1);
    this.components = n;
  }

  find(x: number): number {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x];
  }

  union(a: number, b: number): boolean {
    let pa = this.find(a);
    let pb = this.find(b);

    if (pa === pb) return false;

    if (this.size[pa] &amp;lt; this.size[pb]) {
      [pa, pb] = [pb, pa];
    }

    this.parent[pb] = pa;
    this.size[pa] += this.size[pb];
    this.components--;
    return true;
  }
}

function maxStability(
  n: number,
  edges: number[][],
  k: number
): number {
  let hasMust = false;
  let mustMin = Infinity;

  // 1) must 간선끼리 사이클 체크
  const mustDsu = new DSU(n);
  for (const [u, v, s, must] of edges) {
    if (must === 1) {
      hasMust = true;
      mustMin = Math.min(mustMin, s);
      if (!mustDsu.union(u, v)) {
        return -1;
      }
    }
  }

  // 2) 전체 그래프 연결 가능 여부 체크
  const allDsu = new DSU(n);
  for (const [u, v] of edges) {
    allDsu.union(u, v);
  }
  if (allDsu.components !== 1) {
    return -1;
  }

  // upper bound
  let hi = 0;
  if (hasMust) {
    hi = mustMin;
  } else {
    for (const [, , s] of edges) {
      hi = Math.max(hi, s * 2);
    }
  }

  const can = (x: number): boolean =&amp;gt; {
    const dsu = new DSU(n);

    // must 간선은 반드시 포함 + 업그레이드 불가
    for (const [u, v, s, must] of edges) {
      if (must === 1) {
        if (s &amp;lt; x) return false;
        if (!dsu.union(u, v)) return false;
      }
    }

    // 업그레이드 없이 x 이상인 일반 간선 먼저 사용
    for (const [u, v, s, must] of edges) {
      if (must === 0 &amp;amp;&amp;amp; s &amp;gt;= x) {
        dsu.union(u, v);
      }
    }

    // 업그레이드해서 x 이상이 되는 간선 사용
    let upgradesLeft = k;
    for (const [u, v, s, must] of edges) {
      if (must === 0 &amp;amp;&amp;amp; s &amp;lt; x &amp;amp;&amp;amp; s * 2 &amp;gt;= x &amp;amp;&amp;amp; upgradesLeft &amp;gt; 0) {
        if (dsu.union(u, v)) {
          upgradesLeft--;
        }
      }
    }

    return dsu.components === 1;
  };

  let lo = 1;
  while (lo &amp;lt; hi) {
    const mid = Math.floor((lo + hi + 1) / 2);
    if (can(mid)) {
      lo = mid;
    } else {
      hi = mid - 1;
    }
  }

  return lo;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-end=&quot;2712&quot; data-start=&quot;2703&quot; data-section-id=&quot;wul5pq&quot; data-ke-size=&quot;size23&quot;&gt;시간복잡도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2774&quot; data-start=&quot;2714&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2741&quot; data-start=&quot;2714&quot; data-section-id=&quot;1u3y95a&quot;&gt;can(x) 한 번: O(m &amp;alpha;(n))&lt;/li&gt;
&lt;li data-end=&quot;2774&quot; data-start=&quot;2742&quot; data-section-id=&quot;1pdvuhs&quot;&gt;이분 탐색 포함 전체: O(m &amp;alpha;(n) log V)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2807&quot; data-start=&quot;2776&quot; data-ke-size=&quot;size16&quot;&gt;m &amp;lt;= 1e5에서도 충분히 통과 가능한 방식&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;a href=&quot;https://gmlwjd9405.github.io/2018/08/28/algorithm-mst.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gmlwjd9405.github.io/2018/08/28/algorithm-mst.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773325832310&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[알고리즘] 최소 신장 트리(MST, Minimum Spanning Tree)란 - Heee's Development Blog&quot; data-og-description=&quot;Step by step goes a long way.&quot; data-og-host=&quot;gmlwjd9405.github.io&quot; data-og-source-url=&quot;https://gmlwjd9405.github.io/2018/08/28/algorithm-mst.html&quot; data-og-url=&quot;http://gmlwjd9405.github.io/2018/08/28/algorithm-mst.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1cveC/dJMb86O0gfV/bPHNtSOsaUbPEP2hSmBpi0/img.png?width=1882&amp;amp;height=1162&amp;amp;face=0_0_1882_1162&quot;&gt;&lt;a href=&quot;https://gmlwjd9405.github.io/2018/08/28/algorithm-mst.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gmlwjd9405.github.io/2018/08/28/algorithm-mst.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1cveC/dJMb86O0gfV/bPHNtSOsaUbPEP2hSmBpi0/img.png?width=1882&amp;amp;height=1162&amp;amp;face=0_0_1882_1162');&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;[알고리즘] 최소 신장 트리(MST, Minimum Spanning Tree)란 - Heee's Development Blog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Step by step goes a long way.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gmlwjd9405.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devkuk.tistory.com/27&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devkuk.tistory.com/27&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773325835995&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[자료구조] 서로소 집합 자료구조 (Disjoint Set Union, DSU) - Union Find&quot; data-og-description=&quot;이 글은 서로소 집합 자료구조(Disjoint Set Union, DSU)에 대해서 정리한 글입니다.DSU와 Union Find✅ &amp;quot;Union Find&amp;quot;라는 이름은 합치기를 뜻하는 Union과 찾기를 뜻하는 Find라는 두 가지의 주요 연산에서 유래&quot; data-og-host=&quot;devkuk.tistory.com&quot; data-og-source-url=&quot;https://devkuk.tistory.com/27&quot; data-og-url=&quot;https://devkuk.tistory.com/27&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/D6GyG/dJMb9efcomz/JXaKhgfpPxEKzUHqvMjKKK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cCox5D/dJMb9g49CoP/okWhfKVWfO4ELjOS7AsSa0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/brpY0O/dJMb9fZtMdh/s6wGjnXxZusjjr0WMAHcg1/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080&quot;&gt;&lt;a href=&quot;https://devkuk.tistory.com/27&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devkuk.tistory.com/27&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/D6GyG/dJMb9efcomz/JXaKhgfpPxEKzUHqvMjKKK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cCox5D/dJMb9g49CoP/okWhfKVWfO4ELjOS7AsSa0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/brpY0O/dJMb9fZtMdh/s6wGjnXxZusjjr0WMAHcg1/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080');&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;[자료구조] 서로소 집합 자료구조 (Disjoint Set Union, DSU) - Union Find&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 서로소 집합 자료구조(Disjoint Set Union, DSU)에 대해서 정리한 글입니다.DSU와 Union Find✅ &quot;Union Find&quot;라는 이름은 합치기를 뜻하는 Union과 찾기를 뜻하는 Find라는 두 가지의 주요 연산에서 유래&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devkuk.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ayW5B2W9hfo&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=ayW5B2W9hfo&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=ayW5B2W9hfo&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/jxYuc/dJMb86O0mby/uzbZIMKYsC7E4o1Hh9tYiK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/mqkKI/dJMb9efcufg/KY3o0EFikDWuKEOxeE94D0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bLx3xN/dJMb9g49IrG/oRxWJFNXgbF1OaF1b5FqaK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;Union Find in 5 minutes &amp;mdash; Data Structures &amp;amp; Algorithms&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/ayW5B2W9hfo&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Computer Sci./Algorithms</category>
      <category>LeetCode</category>
      <category>리트코드</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/523</guid>
      <comments>https://yolo2429.tistory.com/523#entry523comment</comments>
      <pubDate>Fri, 20 Mar 2026 01:49:30 +0900</pubDate>
    </item>
    <item>
      <title>[요즘IT] AI 에이전트 정복 : 규칙, 스킬, 커맨드, 서브 에이전트 활용 전략</title>
      <link>https://yolo2429.tistory.com/522</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AI 도구의 발전 속도는 매우 빠르다. 매주 새로운 AI 도구들이 생겨나고 있고, 클로드(Claude) 같은 에이전트는 하루에도 여러 차례 업데이트된다. 이런 변화 속에서 많은 현업 개발자들이 따라가기에 벅차다고 느낀다. &lt;/span&gt;&lt;span style=&quot;color: #26221f;&quot;&gt;모두가 AI를 이야기하니 뒤처질 것 같은 FOMO를 느끼기도 하고, AI가 마법처럼 모든 일을 해결해줄 것이라 기대했다가 오히려 불필요한 작업이 늘어나는 경험을 한 사람도 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;필자는 현업 개발자로서 급변하는 트렌드를 맹목적으로 좇기보다, 본질적인 원리를 깊게 이해하는 데 집중하려 한다. 이 글에서는 AI 에이전트를 실무에서 어떻게 활용할 수 있는지, 필자의 경험을 바탕으로 스킬(Skill), 규칙(Rules), 커맨드(Commands), 서브 에이전트(Sub-Agents)의 차이와 적용 방법을 정리한다. 또한 이러한 구성 요소들을 도입했을 때 작업 흐름에 어떤 변화가 생기는지도 다룬다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1) 스킬(Skills)&lt;/span&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;span&gt;클로드 코드 공식 가이드에서는 스킬을 다음과 같이 정의한다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A skill is a set of instructions - packaged as a simple folder - that teaches Claude how to handle specific tasks or workflows.&amp;nbsp;&lt;br /&gt;스킬은 클로드에게 특정 작업이나 워크플로우를 처리하는 방법을 알려주는 일종의 지침서로, 간단한 폴더 형태로 패키징되어 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;br /&gt;(이하 본문은 아래 요즘 IT 홈페이지에서 확인해주세요.)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yozm.wishket.com/magazine/detail/3646/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yozm.wishket.com/magazine/detail/3646/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773209380106&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;AI 에이전트 정복: 규칙, 스킬, 커맨드, 서브 에이전트 활용 전략  | 요즘IT&quot; data-og-description=&quot;AI 도구의 발전 속도는 매우 빠르다. 매주 새로운 AI 도구들이 생겨나고 있고, 클로드(Claude) 같은 에이전트는 하루에도 여러 차례 업데이트 된다. 모두가 AI를 이야기하니 뒤처질 것 같은 FOMO(Fear Of&quot; data-og-host=&quot;yozm.wishket.com&quot; data-og-source-url=&quot;https://yozm.wishket.com/magazine/detail/3646/&quot; data-og-url=&quot;https://yozm.wishket.com/magazine/detail/3646/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bHfCUs/dJMb87NUIjY/3mx5C1HAZPLI7PUkvrCcDK/img.png?width=764&amp;amp;height=305&amp;amp;face=0_0_764_305,https://scrap.kakaocdn.net/dn/c1Asi5/dJMb8U8RVY7/MSrMKgukYy3NyD21uZZ8n1/img.png?width=764&amp;amp;height=305&amp;amp;face=0_0_764_305,https://scrap.kakaocdn.net/dn/rhU60/dJMb81GVwIi/l25K7zopMbDSGCvPIJ1Z9k/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://yozm.wishket.com/magazine/detail/3646/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yozm.wishket.com/magazine/detail/3646/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bHfCUs/dJMb87NUIjY/3mx5C1HAZPLI7PUkvrCcDK/img.png?width=764&amp;amp;height=305&amp;amp;face=0_0_764_305,https://scrap.kakaocdn.net/dn/c1Asi5/dJMb8U8RVY7/MSrMKgukYy3NyD21uZZ8n1/img.png?width=764&amp;amp;height=305&amp;amp;face=0_0_764_305,https://scrap.kakaocdn.net/dn/rhU60/dJMb81GVwIi/l25K7zopMbDSGCvPIJ1Z9k/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&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;AI 에이전트 정복: 규칙, 스킬, 커맨드, 서브 에이전트 활용 전략 | 요즘IT&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AI 도구의 발전 속도는 매우 빠르다. 매주 새로운 AI 도구들이 생겨나고 있고, 클로드(Claude) 같은 에이전트는 하루에도 여러 차례 업데이트 된다. 모두가 AI를 이야기하니 뒤처질 것 같은 FOMO(Fear Of&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yozm.wishket.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;</description>
      <category>Computer Sci.</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/522</guid>
      <comments>https://yolo2429.tistory.com/522#entry522comment</comments>
      <pubDate>Wed, 11 Mar 2026 15:14:39 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 디자인 엔지니어란 무엇인가?</title>
      <link>https://yolo2429.tistory.com/521</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://www.nucleate.dev/blog/what-is-a-design-engineer&quot;&gt;What is a Design Engineer?&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 엔지니어는 디자인과 프런트엔드 개발 사이를 연결하며, 훌륭하게 느껴지고 완벽하게 작동하는 인터페이스를 만듭니다. 디자인 엔지니어가 무엇을 하는지, 디자이너 및 개발자와 어떻게 다른지, 그리고 Vercel, Stripe, Linear 같은 회사들이 왜 이들을 채용하는지 알아보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 엔지니어는 소프트웨어가 단순히 '작동'하는 것을 넘어, 실제로 좋게 &lt;i&gt;느껴지도록&lt;/i&gt; 만드는 사람들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역할은 최근 큰 주목을 받고 있습니다. Vercel, Stripe, Linear, Cursor가 모두 &lt;a href=&quot;https://www.telerik.com/blogs/design-engineers-filling-frontend-gap&quot;&gt;적극적으로 채용&lt;/a&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;i&gt;하고&lt;/i&gt; 코드를 배포할 수 있습니다. 별개의 기술이 아니라 하나의 유동적인 워크플로우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 짚고 넘어갈 점이 있습니다. &quot;디자인 엔지니어&quot;를 구글에서 검색하면 교량이나 공조 시스템을 설계하는 직군의 채용 공고가 나옵니다. 여기서 말하는 건 그게 아닙니다. 소프트웨어에서 디자인 엔지니어링이란 UI 레이어에 관한 것입니다. 보기에도 훌륭하고, 반응이 빠르게 느껴지며, 설계한 대로 실제로 구현되는 인터페이스를 만드는 일입니다.&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;&lt;b&gt;디자인을 코드로 직접 구현합니다.&lt;/b&gt; 디자이너가 Figma 파일을 넘기고 최선을 바라는 대신, 디자인 엔지니어는 간단한 스케치를 가져와서 의도를 논의하고 구축할 수 있습니다. 프로토타입이 &lt;i&gt;곧&lt;/i&gt; 구현입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디자인 시스템을 구축하고 유지합니다.&lt;/b&gt; 누군가는 컴포넌트 라이브러리를 책임져야 합니다. Figma 컴포넌트뿐만 아니라 엔지니어들이 매일 사용하는 실제 리액트 컴포넌트를 말입니다. 디자인 엔지니어는 이들을 동기화된 상태로 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UI 완성도의 마지막 10%를 책임집니다.&lt;/b&gt; 마이크로 인터랙션, 로딩 상태, 인터페이스를 생동감 있게 만드는 애니메이션. 이 작업은 마감일이 다가오면 보통 잘립니다. 디자인 엔지니어는 그것이 잘리지 않도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;변형을 빠르게 탐색합니다.&lt;/b&gt; 페이월을 미니멀하게 해야 할까요, 아니면 대담하게 해야 할까요? 온보딩이 3단계여야 할까요, 아니면 5단계여야 할까요? 디자인 엔지니어는 정적인 목업이 아니라 코드로 작업하기 때문에 여러 버전을 빠르게 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장인정신을 옹호합니다.&lt;/b&gt; 팀이 &quot;충분히 좋은&quot; 것을 배포하려고 할 때, 디자인 엔지니어는 반대합니다. 그들은 세부 사항이 전체 경험으로 합쳐진다는 것을 이해합니다.&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;디자이너는 부드러운 호버 애니메이션과 정밀한 간격을 가진 아름다운 카드 컴포넌트를 만듭니다. 개발자는 마감 압박 속에서 이것을 구현합니다. 애니메이션이 단순화되고, 간격은 눈대중으로 맞추고, 그림자는 정확하지 않습니다. PM은 &quot;일단 작동하니까&quot;라며 승인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 이러한 세부 사항을 의식적으로 알아차리지 못합니다. 하지만 그들은 느낍니다. 이유를 설명할 수 없어도 제품이 싸구려처럼 느껴집니다. 전환율이 떨어집니다. 무언가 &quot;이상하다&quot;는 지원 티켓이 들어옵니다. 더 세련된 UI를 가진 경쟁사가 거래를 따냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 엔지니어는 이 격차를 메우기 위해 존재합니다. 그들은 디자인과 코드 두 언어를 모두 구사하므로 번역 과정에서 아무것도 손실되지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디자인 엔지니어 vs. 다른 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vs. 프런트엔드 개발자:&lt;/b&gt; 디자인 엔지니어는 더 강한 디자인 감각을 가지고 있습니다. 그들은 시각적 세부 사항에 더 신경 쓰며 애니메이션 곡선을 다듬는 데 한 시간을 들일 것입니다. 프런트엔드 개발자는 일반적으로 기능과 유지보수성을 먼저 최적화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vs. 프로덕트 디자이너:&lt;/b&gt; 디자인 엔지니어는 코드를 배포합니다. 그들은 단순히 사양을 넘기고 구현이 일치하기를 바라지 않습니다. 그들이 직접 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vs. 풀스택 개발자:&lt;/b&gt; 디자인 엔지니어는 스택 전반에 걸쳐 넓게 다루기보다는 UI 레이어에 깊이 들어갑니다. 범위는 좁지만 장인정신은 더 깊습니다.&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;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;li&gt;엔지니어들이 UI 세부 사항에 너무 많은 시간을 쓰고 있습니다(그리고 그것을 원망합니다)&lt;/li&gt;
&lt;li&gt;더 나은 UX를 가진 경쟁사에게 거래를 빼앗기고 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제는 다음과 같습니다. 디자인 엔지니어는 비싸고 찾기 어렵습니다. 그들은 드문 하이브리드이며, Vercel과 Stripe 같은 회사들이 이들을 채용하기 위해 $200k 이상을 지불하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 엔지니어가 있든 없든, &lt;a href=&quot;https://nucleate.dev&quot;&gt;Nucleate&lt;/a&gt;는 빠르고 브랜드에 맞는 프로토타이핑을 위한 강력한 도구입니다. AI 디자인 엔지니어 역할을 합니다. GitHub 저장소(repository)에 연결하고, 기존 컴포넌트와 디자인 토큰을 학습하며, 실제로 시스템을 사용하는 프로토타입을 생성합니다.&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;b&gt;디자이너&lt;/b&gt;: 엔지니어링을 기다리지 않고 자신의 아이디어를 코드로 보고 싶은 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로덕트 매니저&lt;/b&gt;: 리소스를 투입하기 전에 여러 접근 방식을 탐색해야 하는 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디자인 엔지니어&lt;/b&gt;: 더 빠르게 움직이고 더 많은 변형을 탐색하고 싶은 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;창업자&lt;/b&gt;: 브랜드 일관성을 유지하면서 빠르게 프로토타입을 만들어야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다섯 가지 다른 페이월 변형을 탐색해야 하나요? Nucleate는 실제 컴포넌트를 사용하여 몇 분 안에 생성합니다. 하나가 마음에 들면 저장소에 직접 PR을 엽니다. 코드는 깨끗하고, 검토 가능하며, 기존 패턴과 일치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 코드베이스에 살아 숨 쉬는 프로토타이핑입니다. 더 많은 핸드오프 문제를 만드는 별도의 놀이터가 아닙니다. 스크린샷에서 UI를 재구축할 필요가 없습니다. Figma와 코드가 분리되는 디자인 드리프트가 없습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 엔지니어링은 비전과 실행 사이의 격차를 메우는 것입니다. 이 역할이 부상하는 이유는 사용자 기대가 그 어느 때보다 높기 때문입니다. 사람들은 직접적인 경쟁사뿐만 아니라 Linear와 Notion에 앱을 비교합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 엔지니어를 고용하든 AI를 사용하여 기능을 채우든, 목표는 동일합니다. 작동하는 만큼 좋게 느껴지는 제품을 배포하는 것입니다.&lt;/p&gt;</description>
      <category>Web Frontend Developer</category>
      <category>Ai</category>
      <category>개발자</category>
      <category>디자인</category>
      <category>소프트웨어</category>
      <category>엔지니어</category>
      <category>코딩</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/521</guid>
      <comments>https://yolo2429.tistory.com/521#entry521comment</comments>
      <pubDate>Wed, 11 Mar 2026 14:48:57 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 까다로운 버그에서 배운 9년간의 교훈</title>
      <link>https://yolo2429.tistory.com/520</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://henrikwarne.com/2025/06/15/lessons-from-9-more-years-of-tricky-bugs/&quot;&gt;Lessons From 9 More Years of Tricky Bugs&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2002년부터 저는 제가 마주친 모든 &lt;a href=&quot;https://henrikwarne.com/2016/04/28/learning-from-your-bugs/&quot;&gt;까다로운 버그를 추적&lt;/a&gt;해왔습니다. 9년 전, 그때까지의 &lt;a href=&quot;https://henrikwarne.com/2016/06/16/18-lessons-from-13-years-of-tricky-bugs/&quot;&gt;버그에서 얻은 교훈&lt;/a&gt;을 담아 블로그 글을 작성했습니다. 그 이후로 기록해온 버그들을 이번에 전부 다시 돌아봤습니다. 첫 번째 회고에서 정리했던 교훈들을 실제로 잘 실천해왔는지 확인하고 싶었고, 그사이 어떤 유형의 버그들을 마주쳤는지도 살펴보고 싶었습니다. 이전과 마찬가지로 교훈을 &lt;i&gt;코딩&lt;/i&gt;, &lt;i&gt;테스팅&lt;/i&gt;, &lt;i&gt;디버깅&lt;/i&gt;의 카테고리로 나눠 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://henrikwarne.com/wp-content/uploads/2025/05/path.jpg&quot; alt=&quot;&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;b&gt;1. 빈 케이스.&lt;/b&gt; 다섯 개의 버그가 빈 줄, 빈 파일, 공백, 또는 값이 0인 경우와 관련이 있었습니다. 예를 들어, 공백 한 칸(0이 아닌)이 있는 줄은 비어있는 것으로 건너뛰어야 했지만 그렇지 않았습니다. 다른 경우에는 csv 파일의 빈 헤더가 문제를 일으켰습니다. 최근 예시에서는 수정이 필요한 누락된 매핑이 0개임에도 불구하고 알림 메일이 발송되었습니다. 이전 글에서 0과 null 케이스를 고려하지 못했다는 것을 알게 되었습니다. 분명히 저는 이런 종류의 오류를 발견하는 데 더욱 주의를 기울여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 날짜.&lt;/b&gt; 네 개의 버그가 어떤 식으로든 날짜와 관련이 있었습니다. 예를 들어, 이전 날짜를 확인하는 로직은 이전 날이 주말인 경우 어떻게 해야 하는지를 고려해야 합니다. 연속 공휴일이 며칠까지 이어질 수 있는지 가정할 때는 일본의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Golden_Week_(Japan)&quot;&gt;골든위크(Golden Week)&lt;/a&gt;를 기억하세요. 또한, 종료 날짜가 오늘 이후인지 확인하는 것만으로는 계약이 활성화되어 있는지 알기에 충분하지 않습니다. 시작 날짜도 오늘 이후일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 오래된 데이터 형식.&lt;/b&gt; 변경된 데이터 형식을 사용하도록 로직을 업그레이드하는 것은 항상 까다롭습니다. 데이터베이스의 오래된 데이터를 새 형식으로 변환해야 할 수도 있다는 것을 고려해야 합니다. 또한, 새 로직이 배포되었더라도 진행 중인 작업이 여전히 이전 형식을 사용하는 과도기가 있을 수 있습니다. 계약 이름 끝의 공백을 제거했지만, 4-eye 승인 로직(역자 주: 두 명의 독립적인 검토자가 승인해야 하는 원칙)이 오래된 이름에서 실패한 경우도 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 별칭이 생긴 딕셔너리/해시맵.&lt;/b&gt; 한 번 이상, 저는 실수로 이미 존재하는 딕셔너리에 대한 별칭에 불과한 두 번째 딕셔너리를 만들었습니다. 이는 한쪽의 변경 사항이 다른 쪽에도 나타난다는 것을 의미했습니다. 이로 인해 코드 실행 결과가 매우 혼란스러워졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 로컬 변경사항.&lt;/b&gt; 때때로 푸시를 잊어버린 로컬 변경사항이 있어서, 로컬에서 테스트한 것이 배포된 것과 달랐습니다. 이상적으로는 CI 테스트에서 잡아야 했지만, 이러한 특정 케이스에 대한 테스트가 없었습니다. 비슷한 경우도 있습니다. 로컬에서 작업하는 동안 일부 코드를 주석 처리하고 몇 가지 변경을 수행한 다음 코드의 주석을 다시 해제했습니다. 하지만 그 사이에 다른 로직이 변경되어 (코드가 주석 처리되어 있는 동안) 버그가 발생했습니다.&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;b&gt;6. 탐색적 테스팅.&lt;/b&gt; 기능을 완료하기 전에 탐색적 테스팅을 수행할 때 많은 버그를 발견했습니다. 종종 다양한 기능의 활성화/비활성화 상태에 따른 기능 간 상호 작용과 관련이 있었고, 이로 인해 버그가 드러났습니다. 다른 경우에는 고객이 특정 방식으로 기능을 사용하고 있다고 생각했습니다. 하지만 그것이 작동하지 않자 그들에게 물어봤고, 그들은 완전히 다른 방식으로 기능을 사용하고 있다고 말했습니다. 또한, GUI에서 보면 명백해지는 것들도 있습니다. 예를 들어, 제가 만든 한 변경사항은 실수로 표시된 모든 레코드에 *&quot;hasApiKey=false&quot;&lt;i&gt;를 추가했지만, 의도는 *&quot;false&quot;&lt;/i&gt;로 설정된 것은 숨기는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. 테스트에서 더 작은 설정.&lt;/b&gt; 일반적으로 테스트 시스템은 프로덕션(prod) 시스템보다 여러 면에서 더 작습니다. 예를 들어, 테스트 시스템에는 이벤트 핸들러가 하나만 있을 수 있지만 프로덕션 시스템에는 두 개가 있습니다. 이로 인해 순서대로 처리되어야 하는 두 개의 이벤트가 프로덕션에서 병렬로 처리되는 버그가 발생했습니다. 이벤트들은 두 개의 다른 이벤트 핸들러로 갔지만, 테스트에서는 (이벤트 핸들러가 하나만 있어서) 항상 순차적으로 처리되었습니다. 이런 종류의 버그는 당연히 테스트에서 발견하기 매우 어렵습니다(불가능할 수도 있습니다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8. 접근 권한.&lt;/b&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;b&gt;9. 좋은 로깅.&lt;/b&gt; 많은 버그의 경우, 문제를 해결하는 핵심은 로그를 보고 무슨 일이 일어났는지 파악하는 것이었습니다. 예를 들어, 동일해야 하는 세 개의 캘린더 서비스 중 하나가 잘못된 답을 제공했을 때, 로그에서 결함이 있는 서비스가 시작 시점에 데이터의 일부만 받았다는 것을 볼 수 있었습니다(오류 표시도 없었습니다). 로그와 오류 메시지를 주의 깊게 읽는 것도 중요합니다. 종종 저는 로그를 꼼꼼히 확인하지 않고 무슨 일이 일어났는지 안다고 가정하곤 했습니다. 타임스탬프도 매우 도움이 됩니다. 여러 버그의 &quot;발견 방법&quot; 섹션에서, 저는 다음과 같은 내용을 작성했습니다. &quot;그런 다음 데드 레터가 발생한 시점 즈음에 Kibana에서 검색했습니다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;10. 동료와의 논의.&lt;/b&gt; 이전과 마찬가지로, 동료와 논의하는 것은 어려운 버그를 해결하는 매우 효과적인 방법입니다. 최근의 한 경우에, 문제를 해결하는 동안 우리 모두 사무실에 함께 있었습니다. 보통 우리는 일주일에 3일은 원격으로 일하지만, 물리적으로 가까이 있으면 협력이 훨씬 더 효과적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;11. 알림.&lt;/b&gt; 알림이 없었다면 일부 오류는 아예 발견되지 않았거나 늦게 발견됐을 것입니다. 좋은 알림을 설정하는 것은 정말로 효과가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;12. 최소한의 케이스로 재현.&lt;/b&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;9년 전의 제 글을 되돌아보면, 제가 그곳에서 강조했던 문제들을 피했을까요? 대부분의 경우에는 그렇습니다. 하지만 저는 여전히 빈 값, 0 또는 null 케이스를 처리하는 데 여러 번 실패했습니다. 빈 값, 0, null 케이스는 제가 더욱 주의를 기울여야 하는 부분입니다. 잘못된 if 문으로 인해 발생한 잠재적으로 정말로 심각한 버그도 하나 있었습니다. 다행히도, 탐색적 테스팅을 수행하면서 이를 발견했고, 로그에서 이상한 것을 발견했습니다. 로그를 읽는 것에 관해서는, &quot;세심하게 주의를 기울이라&quot;는 제 오래된 조언을 더 자주 따라야 합니다. 하지만 전반적으로, 저는 과거에 일으켰던 많은 유형의 버그를 피하는 데 성공했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 다이어그램은 시작 이후로 매년 제가 기록한 버그의 수를 보여줍니다. 지난 9년 동안 평균적으로 두 달에 한 번 까다로운 버그를 마주했습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://henrikwarne.com/wp-content/uploads/2025/05/trickybugsperyear.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 버그가 제가 일으킨 것은 아닙니다. 때때로 다른 사람들이 일으킨 버그가 너무 흥미로워서 저도 포함시킵니다. 지난 9년 동안 약 70%의 버그가 제가 일으킨 것이었습니다. 저는 버그를 수정하는 데 걸린 시간도 기록합니다. 여기에는 문제 해결, 수정 및 수정 사항 테스트가 포함됩니다. 아래는 소요 시간에 대한 다이어그램입니다. 8시간 이상은 여러 날을 의미한다는 점에 유의하세요. 따라서 24시간은 3일이지, 24시간 쉬지 않고 작업한 것이 아닙니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://henrikwarne.com/wp-content/uploads/2025/06/timetakentoresolve.jpg&quot; alt=&quot;&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;많은 오류는 원인을 파악하기 전까지는 도저히 이해할 수 없어 보입니다. 예를 들어, 우리 중 누구도 어떻게 발생할 수 있는지 이해할 수 없었던 SQL 오류가 있었습니다. 결국, (데이터베이스 쿼리를 수행하는) 한 노드가 재시작되지 않아서 이전 버전의 소프트웨어를 실행하고 있었다는 것이 밝혀졌습니다. 다른 경우에는, 일단 보게 되면 파악하기 어렵지 않지만 그럼에도 흥미롭습니다. 몇 년 전에 Cassandra에서 오버플로우가 있었습니다. 문제의 변수는 파이썬과 카산드라 모두에서 int였지만, 파이썬에서는 정수가 임의로 클 수 있는 반면 카산드라에서는 int가 32비트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인이 무엇이든, 무슨 일이 일어났는지 파악하는 것은 항상 만족스럽습니다. 버그는 학습을 위한 훌륭한 소스이며, 가장 까다로운 버그를 추적함으로써 저는 각각의 버그로부터 최대한 많이 배우려고 노력하고 있습니다.&lt;/p&gt;</description>
      <category>Computer Sci.</category>
      <category>개발</category>
      <category>교훈</category>
      <category>디버깅</category>
      <category>버그</category>
      <category>분석</category>
      <category>코딩</category>
      <category>테스트</category>
      <author>DevOwen</author>
      <guid isPermaLink="true">https://yolo2429.tistory.com/520</guid>
      <comments>https://yolo2429.tistory.com/520#entry520comment</comments>
      <pubDate>Wed, 11 Mar 2026 14:46:06 +0900</pubDate>
    </item>
  </channel>
</rss>