커리어를 순서대로 적어보면 조금 낯선 조합이 나옵니다. 환경시스템공학을 공부했고, IoT 소프트웨어 개발자로 일을 시작했습니다. 이후에는 친구와 사업을 해봤고, 쏘카에서 사업운영과 사업관리 업무를 했습니다. 이렇게 적으면 중간중간 설명이 꽤 필요해 보입니다. 저도 이번에 그 설명을 조금씩 해보려고 합니다. 이력서에 적힌 업무보다, 어떻게 그 일을 시작하게 됐고 무엇에 흥미를 느꼈는지를 중심으로요. 첫 이야기는 대학에서 들었던 수업으로 돌아갑니다. 전공 수업에서 만난 Python 저는 환경시스템공학과에 다녔습니다. 전공 수업 중에 Python을 이용해 미분과 적분을 계산하는 수업이 있었는데, 그 수업을 들으며 프로그래밍에 흥미를 느꼈습니다. 환경공학을 공부하던 중에 코드를 접한 셈입니다. 지금 제 커리어를 돌아볼 때, 가장 먼저 떠오르는 출발점도 이 수업입니다. 교양으로는 R을 활용한 데이터 분석과 통계 수업을 들었습니다. Python으로 계산을 해보고, R로 데이터를 다뤄보면서 관심을 조금 더 넓혀갔습니다. 이때의 흥미를 지금의 직무와 바로 연결하면 이야기가 너무 깔끔해질 것 같습니다. 다만 이후에 개발을 배우고 데이터 관련 업무에 관심을 갖게 된 흐름을 되짚어보니, 이 두 수업을 빼놓을 수는 없었습니다. 6개월 동안 AI와 IoT를 배우다 이후 학교에서 진행하는 6개월짜리 국비교육을 수강했습니다. 과정 이름은 ‘AI 사물인터넷 시각센서 데이터 소프트웨어 개발자 과정’이었습니다. 이름만큼 배우는 내용도 다양했습니다. C와 Raspberry Pi로 IoT 통신을 배우고, C++로 센서 데이터를 수집하고 활용했습니다. Qt와 Arduino, Raspberry Pi를 이용한 응용 소프트웨어 제작도 과정에 포함돼 있었습니다. Python으로 데이터를 수집하고 활용하는 내용을 배우고, Scikit-learn과 TensorFlow를 통해 인공지능 구현도 접했습니다. 기술 이름을 나열하면 긴 목록이지만, 지금 돌아보면 하나의 흐름으로 읽힙니다. 센서에서 데이터를 얻고, 그 데이터를 소프트웨어에서 다루고, 분석과 인공지능으로 활용해보는 과정이었습니다. 대학 수업에서 Python과 R에 흥미를 느꼈다면, 이 교육에서는 그 관심을 여러 기술로 이어가 볼 수 있었습니다. 계산과 분석을 넘어, 장치와 소프트웨어가 연결되는 쪽으로 배움의 범위가 넓어졌습니다. 6개월 교육을 들었다고 모든 기술을 깊이 이해했다고 말할 수는 없습니다. 그래도 제가 처음 개발을 배우던 시기를 이야기할 때, 이 과정은 중요한 부분입니다. 수업에서 시작된 관심에 조금 더 긴 시간을 들여본 경험이었으니까요. 교육을 듣다가 첫 회사를 만났다 교육을 수강하던 중 한 회사에서 제안을 받았습니다. 그렇게 IoT 소프트웨어 개발자로 첫 커리어를 시작했습니다. 환경시스템공학과 학생이던 제가 개발자로 일을 하게 된 흐름은 이렇습니다. 전공 수업에서 Python에 흥미를 느꼈고, R을 이용한 데이터 분석 수업을 들었고, AI와 IoT 교육을 수강하다가 첫 회사와 연결됐습니다. 몇 줄로 적으면 빠르게 지나가지만, 제게는 관심이 실제 직업으로 이어진 구간입니다. 수업에서 접했던 기술이 이제는 제가 하는 일의 일부가 됐습니다. 다만 이 글에서 첫 직장 이야기를 전부 하지는 않으려고 합니다. 개발자로 일을 시작한 뒤 약 5~6개월이 지나, 저는 대학 동기와 함께 사업을 시작했습니다. 그 선택부터는 또 다른 이야기가 이어집니다. 돌아보면, 시작은 작은 흥미였다 커리어를 정리하려고 하면 각 선택에 그럴듯한 이유를 붙이고 싶어집니다. 지금 하는 일을 향해 처음부터 차근차근 준비해온 것처럼 설명하고 싶기도 합니다. 하지만 제 첫 출발을 이야기할 때는, Python과 R을 다뤄보며 흥미를 느꼈다는 사실부터 적는 것이 가장 자연스럽습니다. 그 뒤에 교육이 있었고, 입사 제안이 있었고, 첫 직업이 생겼습니다. 이후의 커리어는 개발에서 사업운영과 사업관리로 이어졌습니다. 그 이야기는 앞으로 한 편씩 풀어보려고 합니다. 먼저 이 글에는, 환경공학을 공부하던 제가 어떻게 IoT 개발자로 일을 시작했는지를 남겨둡니다. 다음 글에서는 첫 회사를 떠나 대학 동기와 스마트스토어와 유튜브 채널을 운영했던 이야기를 해보겠습니다. 직접 상품을 고르고 콘텐츠를 만들었던 경험, 그리고 사업을 정리한 뒤 쏘카의 일하는 방식과 돈의 흐름이 궁금해졌던 이야기로 이어집니다.
웹 브라우저 기반 대화형 인공지능이 지닌 가장 큰 한계는 세션이 단절될 때마다 사용자의 이전 작업 맥락과 프로젝트 배경을 완전히 상실한다는 점입니다. 이를 극복하기 위해 로컬 머신의 파일 시스템에 직접 연결되어 영구적 맥락을 유지하고, 다중 에이전트와 브라우저 자동 제어, 백그라운드 스케줄링까지 포괄하는 에이전트 하네스 환경의 운영 방식이 대두되고 있습니다. 비개발자도 자신의 로컬 환경을 에이전트의 실행 공간으로 삼아 1인 기업 및 복합 지식 업무를 자율화하는 구체적 메커니즘을 살펴봅니다. Every Codex Concept Explained for Non-Coders 로컬 프로젝트 기반 컨텍스트 관리와 AGENTS.md 및 목표 지향 실행 전통적인 웹 UI 기반의 거대언어모델(LLM)은 브라우저 탭을 닫거나 새로운 세션을 시작하면 이전 대화의 맥락이 초기화됩니다. 이로 인해 사용자는 프로젝트를 진행할 때마다 시스템 프롬프트나 배경 지침을 반복 주입해야 합니다. 반면 Codex 는 사용자의 컴퓨터 내 특정 로컬 디렉터리(폴더)에 에이전트를 직접 마운트합니다. 에이전트는 해당 디렉터리 내에 존재하는 모든 문서, 코드 파일, 작업 로그, 이전 대화 기록을 지속적으로 탐색하여 컨텍스트를 파악합니다. 따라서 "지난 한 달간 작업한 내용을 정리해달라"는 단일 지시만으로도 로컬 폴더에 기록된 과거 세션과 파일 변경 이력을 종합 분석해 결과물을 산출합니다. 이러한 로컬 프로젝트 환경에서 전체 작업의 기준 헌법 역할을 담당하는 핵심 파일이 루트 디렉터리에 위치하는 AGENTS.md 입니다. 에이전트는 사용자의 메시지를 처리하기에 앞서 항상 AGENTS.md 를 최우선으로 파싱합니다. 이 파일에는 다음과 같은 핵심 요소가 기술됩니다. 에이전트 역할 정의 : 불필요한 행정·운영 업무를 최소화하고 콘텐츠 제작에 집중하는 등 작업의 기본 페르소나와 우선순위를 명시합니다. 안전 규칙 : 로컬 파일 시스템을 조작하는 만큼, 사용자의 동의 없는 임의 파일 삭제 금지나 중요 시스템 파일 접근 제한 등의 안전 가이드라인을 강제합니다. 라우팅 맵(Routing Map) : 프로젝트의 규모가 커져 수많은 하위 폴더와 파일이 생성되었을 때, 에이전트가 탐색 비용을 낭비하지 않고 필요한 정보가 위치한 디렉터리를 즉각 찾아갈 수 있도록 전체 디렉터리 구조를 색인화합니다. 에이전트의 내부 구동 방식은 사용자의 세부 지시를 단계별로 기다리지 않고 스스로 문제를 해결하는 에이전트 루프(Agent Loop) 체계를 따릅니다. 사용자가 /goal 명령어로 최종 달성 목표를 설정하면, 에이전트는 추론(Thinking), 도구 사용(Tool Use), 결과 확인(Observation), 재추론으로 이어지는 피드백 루프를 자율 실행합니다. 중간 과정에서 오류나 차단이 발생하더라도 스스로 우회 수단을 찾아내 목표가 완결될 때까지 루프를 지속합니다. [사용자 입력: /goal] │ ▼ [AGENTS.md 규칙 & 라우팅 파싱] │ ▼ ┌─── [에이전트 루프] ──────────────────────────┐ │ 1. 추론 (목표 분석 및 다음 행동 결정) │ │ 2. 도구 실행 (파일 탐색, 스크립트 실행 등) │ │ 3. 상태 검증 (목표 달성 여부 확인) │ └───┬────────────────────────────────────────┘ │ (목표 미달성 시 반복) ▼ [최종 결과 반환 및 자동 스크린샷 검증] 실행 환경의 격리와 권한 설정, 그리고 재사용 가능한 워크플로(Skills) Codex는 컴퓨터의 실제 운영체제 환경을 수정하는 로컬 실행뿐만 아니라, 외부 위험으로부터 본 시스템을 보호하기 위해 격리된 샌드박스에서 구동되는 클라우드 환경도 지원합니다. 사용자가 작업실의 데스크톱 PC를 켜둔 상태라면, 모바일 앱의 원격(Remote) 연결을 통해 외부 이동 중에도 스마트폰으로 데스크톱의 Codex 환경에 접속하여 로컬 파일 조작 및 실행 지시를 내릴 수 있습니다. 또한, 기존 프로덕션 환경을 손상시킬 우려가 있는 실험적 기능 구현이나 대규모 리팩터링 작업은 Git 워크트리(Worktree) 를 생성하여 메인 브랜치와 분리된 복제 환경에서 병렬 테스트를 거친 후 안전하게 병합할 수 있습니다. 작업의 제어 수준과 설정 데이터는 프로젝트 범위와 사용자 전역(User) 범위로 나뉩니다. 이는 Claude Code 의 설정 구조( .claude 디렉터리 체계)와 유사한 방식으로, 프로젝트 전용 디렉터리와 머신 전체에 걸친 전역 디렉터리가 분리되어 관리됩니다. 구분 프로젝트 범위 ( <project>/.codex ) 사용자 전역 범위 ( ~/.codex ) 적용 대상 해당 로컬 프로젝트 디렉터리 단일 적용 머신 전체 및 모든 Codex 프로젝트 공통 적용 저장 내용 프로젝트별 서브에이전트 정의, 개별 config.toml 전역 시스템 설정, 전체 세션 기록, 장기 기억(memories) 스킬 위치 .agents/skills/ (해당 프로젝트 전용 스킬) ~/.agents/skills/ (어디서나 호출 가능한 전역 스킬) 실행 권한 모드는 운영의 자율성과 보안 통제 사이의 균형을 맞출 수 있도록 3단계로 제공됩니다. 승인 요구 모드(Ask for approval) : 에이전트가 셸 명령어를 실행하거나 파일을 생성·수정할 때마다 사용자에게 명시적 승인을 요청합니다. 조건부 승인 모드(Approve for me) : 통상적인 안전 동작은 자동으로 수행하되, 시스템 파괴 위험이 있는 민감한 명령어만 사용자에게 승인을 구합니다. 욜로 모드(Full access) : 에이전트가 질문 없이 파일 수정, 시스템 명령, 인터넷 통신 등 모든 도구를 즉각 실행합니다. 반복되는 고품질 작업 절차를 자산화하기 위해 SKILL.md 기반의 스킬(Skills) 체계를 활용합니다. 예를 들어 유튜브 대본을 입력받아 핵심 요약본, 링크드인 게시물, X 아티클로 변환하고 HyperFrames (HTML 기반 모션 그래픽 렌더링 프레임워크) 코드를 작성하는 복합 절차를 하나의 마크다운 파일로 정형화해 둘 수 있습니다. 등록된 스킬은 또 다른 스킬이나 로컬 파이썬 스크립트, 내부 스타일 가이드 문서를 참조(Reference)할 수 있어 장기적으로 일관된 작업 표준을 제공합니다. 모델별 비용 구조와 브라우저 조작, 서브에이전트 및 자율 스케줄링 Codex 내부에서는 단일 모델만 사용하는 것이 아니라, 작업의 복잡도와 연산 요구량에 맞춰 모델과 추론 노력(Effort) 단계를 유연하게 전환할 수 있습니다. 텍스트 입력에 따른 비용 효율성은 OpenAI Tokenizer 등을 통해 사전에 글자 수 대비 토큰 비율을 검증하여 통제할 수 있습니다. 복합 추론 및 전체 시스템 제어는 최상위 모델인 GPT-6 Astra 를 사용하지만, 토큰 단가가 높아 모든 루틴에 투입하면 비용이 급증합니다. 따라서 단순 데이터 수집이나 구조화 작업에는 경량화 모델인 5.6 Sol , 5.6 Terra , 5.5 Luna 를 목적에 맞게 배치하는 모델 라우팅이 필수적입니다. API를 직접 호출할 경우 100만 토큰당 입력 $10 / 출력 $50에 달하는 비용이 발생하지만, 월 $200 구독 플랜을 이용하면 매주 갱신되는 한도를 통해 월 최대 약 $14,000 상당의 추론 연산량을 공급받을 수 있어 경제적 효용이 극대화됩니다. 모델명 100만 입력 토큰 비용 100만 출력 토큰 비용 추천 활용 작업 GPT-6 Astra $10.00 $50.00 복합 추론, 메인 세션 오케스트레이션, 복잡한 코드 작성 5.6 Sol $4.00 $20.00 일반 지식 작업, 문서 정리, 일상적인 업무 루틴 5.6 Terra $0.20 $1.20 서브에이전트 대량 분산 리서치, 단순 데이터 수집 5.5 Luna $0.20 $1.20 초경량 텍스트 파싱, 단순 포맷 변환 작업 도구 연동성 측면에서는 공식 API가 제공되지 않는 플랫폼과의 통합을 지원하기 위해 내장 브라우저 환경을 활용합니다. Skool 커뮤니티나 Canva와 같은 플랫폼은 기존 저장된 사용자 로그인 쿠키 세션을 활용해 에이전트가 마우스 클릭과 키 입력을 브라우저 GUI 상에서 시뮬레이션함으로써 포스팅이나 그래픽 작업을 자동화합니다. 또한 로컬에서 생성된 웹 애플리케이션이나 정적 페이지는 /site 명령어를 통해 Vercel 등 외부 배포 플랫폼을 거치지 않고도 즉시 고유 퍼블릭 URL로 배포되며, 기본 애널리틱스와 데이터베이스까지 자동으로 연결됩니다. 규모가 큰 작업에서는 메인 오케스트레이터 모델이 작업을 하위 서브에이전트(Sub-agents) 로 분기(Fan-out)하는 구조를 취합니다. 예컨대 메인 에이전트(GPT-6 Astra)가 조사 기획을 수립하면, 다수의 5.6 Terra 인스턴스를 서브에이전트로 동시 병렬 기동하여 X(구 트위터), 유튜브, 학술 웹 문서를 동시에 스크래핑한 뒤 취합합니다. 이에 더해 예약된 작업(Scheduled Tasks) 기능을 결합하면 정해진 시간마다 에이전트가 백그라운드에서 세션을 열고 특정 시장 지표를 분석하거나 매매를 집행하며 정기 보고서를 출력하는 무인 자율 체계가 구축됩니다. 사용자는 음성 모드(Voice Mode)를 활용해 자연어 대화로 이러한 다중 에이전트 워크트리 생성, 서브에이전트 작업 배분, 스레드 간 결과물 전달 과정을 통합 지시할 수 있습니다. 정리 기존의 AI 사용 방식이 일회성 채팅 프롬프트 입력에 머물렀다면, 에이전트 하네스 환경은 로컬 파일 시스템을 작업의 기억 저장소이자 영구 맥락으로 삼아 작동 범위를 근본적으로 전환합니다. AGENTS.md 를 통해 명문화된 작업 규칙과 디렉터리 색인을 제공하고, 고난도 추론 모델과 저비용 모델을 계층화하여 비용을 통제하며, 브라우저 제어와 서브에이전트 병렬화, 스케줄링을 결합함으로써 AI는 챗봇의 단계를 넘어 컴퓨터 운영체제 전반을 자율 실행하는 전천후 비즈니스 운영 환경으로 진화하고 있습니다. 다룬 영상 Every Codex Concept Explained for Non-Coders — Nate Herk | AI Automation · 34분
📝 플레이어 애니메이션 기본 이동과 Sprint와 Dodge와 Jump까지 플레이어의 이동 계열 State를 정리한 뒤 전투 중 필요한 애니메이션과 상태를 연결했다. 처음에는 공격 애니메이션을 붙이는 것이 목표였다. 하지만 공격을 구현하면서 Combat Idle과 Combat Run이 필요해졌고 공격 중 Jump와 Dodge처럼 기존 State와의 연결도 다시 확인해야 했다. 이후에는 Hit과 Stun과 KnockDown과 Die와 Rebirth와 Interact까지 순서대로 구현했다. 기능의 종류는 서로 달랐지만 작업하면서 계속 확인한 기준은 같았다. 애니메이션이 하나 추가됐다는 이유만으로 새로운 State를 만들지 않는다. 해당 기능으로 인해 실제 플레이어의 행동 규칙이 달라지는지를 기준으로 State 사용 여부를 결정한다. 1. 공격 State와 기본 콤보 구현 1-1. 공격 로직을 PlayerAttackState에서 제어 실제 공격 판정은 PlayerAttack 이 담당한다. 하지만 공격 중 어떤 행동이 가능한지와 현재 몇 번째 콤보인지 같은 행동 규칙까지 PlayerAttack 에서 관리하고 싶지는 않았다. 현재 구조에서는 역할을 다음과 같이 나눴다. PlayerAttackState → 공격 상태 진입 / 유지 / 종료 → 콤보 순서와 입력 예약 → Jump / Dodge 등 다른 State로의 전환 판단 PlayerAttack → 실제 공격 범위 검사 → 적에게 데미지 전달 PlayerAnimation → 공격 Trigger와 Animator Parameter 전달 공격 State에 진입하면 첫 번째 공격부터 시작한다. public override void Enter() { comboIndex = 0; StartAttack(); } 실제 공격 시작 시에는 Timer를 초기화하고 전투 상태를 활성화한 뒤 공격 애니메이션과 공격 판정을 실행한다. private void StartAttack() { attackTimer = 0.0f; nextAttack = false; stateMachine.PlayerAnimation.SetCombat(true); stateMachine.PlayerAnimation.SetAttack(); stateMachine.PlayerAttack.Attack(); Debug.Log($"comboIndex : {comboIndex}"); } PlayerAttackState 가 공격 범위나 데미지 계산 방식까지 알 필요는 없다고 판단했다. State에서는 공격이 시작됐다는 사실만 전달하고 실제 공격 판정은 PlayerAttack.Attack() 에서 처리한다. 이렇게 역할을 나누면 공격 범위나 데미지 계산 방식이 변경돼도 State의 행동 전환 규칙과 직접 섞이지 않는다. 1-2. 공격 입력을 바로 실행하지 않고 다음 콤보로 예약 공격 애니메이션은 총 네 단계로 이어지는 구조였다. 공격 버튼을 누를 때마다 바로 다음 애니메이션을 실행하면 첫 공격이 충분히 재생되기 전에 다음 공격으로 넘어갈 수 있었다. 그래서 공격 중 다시 공격 입력이 들어오면 바로 다음 공격을 실행하지 않고 우선 입력을 예약한다. if (stateMachine.PlayerInput.IsAttack) { nextAttack = true; } 일정 시간이 지난 뒤 예약된 입력이 있다면 다음 콤보를 시작한다. if (nextAttack && comboIndex < 3 && attackTimer >= stateMachine.PlayerAttack.ComboInterval) { comboIndex++; StartAttack(); return; } 현재 ComboInterval 은 0.5초 를 사용한다. 공격 입력은 WasPressedThisFrame() 을 사용한다. 따라서 버튼을 계속 누르고 있는 것만으로 반복 공격이 발생하지 않는다. 공격 중 다시 버튼을 눌렀을 때 다음 공격이 예약되는 구조다. 추가 입력이 없다면 현재 공격 애니메이션의 재생 시간이 끝난 뒤 MoveState 로 복귀한다. if (attackTimer >= stateMachine.PlayerAttack.AttackDuration) { stateMachine.ChangeState(PlayerStateType.Move); } 현재 사용하는 공격 애니메이션의 길이가 모두 2.333초 였기 때문에 AttackDuration 도 같은 값을 사용했다. 현재 단계에서는 공격 State가 종료되는 시점을 맞추기 위한 값으로 사용했다. 2. 전투 상태와 Combat Idle / Run 2-1. 공격 후 바로 일반 Idle로 돌아가지 않도록 IsCombat 추가 공격 애니메이션만 연결했을 때 한 가지 어색한 점이 있었다. 공격이 끝나는 순간 바로 평상시 Idle과 Run으로 돌아가면 방금 전투를 수행했다는 느낌이 사라졌다. 그래서 Animator에 IsCombat 을 추가해 평상시 이동과 전투 중 이동을 구분했다. private static readonly int IsCombatHash = Animator.StringToHash("IsCombat"); public bool IsCombat => animator.GetBool(IsCombatHash); 공격을 시작하면 전투 상태를 활성화한다. public void SetCombat(bool isCombat) { animator.SetBool(IsCombatHash, isCombat); if (isCombat) { cIdleTimer = 0.0f; isCIdleTimer = false; } } Animator에서는 평상시 Normal Idle / Run 과 별도로 Combat Sub-State Machine을 구성했다. 일반 Idle / Run ↓ IsCombat = true ↓ Combat ├─ CIdle ├─ CRun └─ Attack 이 구조를 통해 공격이 끝난 뒤에도 일정 시간 동안 전투 자세를 유지할 수 있게 했다. 2-2. 전투 이동에서도 기존 이동 방향 값을 재사용 Combat 상태에서도 타겟을 바라보면서 전후좌우로 움직일 수 있어야 했다. 단순한 전진 Run만으로는 부족했다. 그래서 기존 이동에서 사용하던 Parameter를 Combat 이동에서도 그대로 사용했다. MoveMagnitude MoveX MoveY Combat의 CRun 은 2D Simple Directional Blend Tree로 구성했다. 이동 방향은 플레이어 Local 기준으로 변환한 뒤 Animator에 전달한다. Vector3 moveDir = stateMachine.PlayerMovement.GetMoveDir(); Vector3 localMoveDir = stateMachine.transform.InverseTransformDirection(moveDir); stateMachine.PlayerAnimation.SetMoveDir(localMoveDir.x, localMoveDir.z); 평상시에는 타겟이 없고 전투 상태도 아니라면 전방 달리기 애니메이션을 사용한다. 타겟이 있거나 Combat 상태라면 실제 Local 이동 방향을 전달한다. if (currentTarget == null && !stateMachine.PlayerAnimation.IsCombat) { stateMachine.PlayerAnimation.SetMoveDir(0.0f, 1.0f); } else { Vector3 moveDir = stateMachine.PlayerMovement.GetMoveDir(); Vector3 localMoveDir = stateMachine.transform.InverseTransformDirection(moveDir); stateMachine.PlayerAnimation.SetMoveDir(localMoveDir.x, localMoveDir.z); } 일반 이동과 전투 이동을 완전히 별개의 이동 시스템으로 만들지 않았다. 기존 이동 방향 계산 결과를 Animator 표현에 맞게 재사용했다. 2-3. 공격 종료와 전투 상태 종료를 분리 처음에는 공격 애니메이션이 끝나는 순간 IsCombat 을 바로 false 로 변경하는 방법도 생각했다. 하지만 이렇게 하면 공격 하나가 끝날 때마다 일반 Idle로 복귀한다. 이후 다시 공격하면 Combat으로 재진입해야 한다. 이 흐름은 원하는 전투 표현과 달랐다. 그래서 공격 종료와 전투 상태 종료를 서로 분리했다. PlayerAnimation 에서 Combat Idle 유지 시간을 관리한다. public void StartCIdleTimer() { cIdleTimer = 0.0f; isCIdleTimer = true; } Timer가 일정 시간을 넘으면 Combat 상태를 종료한다. if (!isCIdleTimer) return; cIdleTimer += Time.deltaTime; if (cIdleTimer >= cIdleDuration) { isCIdleTimer = false; SetCombat(false); } 현재 cIdleDuration 은 3초 를 사용한다. 이 Timer는 공격이 정상적으로 완료됐을 때만 필요한 것이 아니었다. 공격 중 Jump나 Dodge로 다른 State에 진입한 경우에도 공격 상태는 종료된다. 그래서 PlayerAttackState.Exit() 에서 Timer를 시작하도록 했다. public override void Exit() { //공격 상태 종료 후 전투 대기 시간 시작 stateMachine.PlayerAnimation.StartCIdleTimer(); } State가 어떤 이유로 종료되더라도 Exit() 은 실행된다. 따라서 공격이 종료된 경로와 관계없이 같은 위치에서 Combat 종료 규칙을 적용할 수 있었다. 3. 정지 공격과 이동 공격 분리 3-1. 공격 중 이동을 허용하면서 발생한 문제 현재 전투에서는 공격 중에도 이동할 수 있어야 했다. 그래서 PlayerAttackState.Update() 에서도 이동을 계속 처리한다. //공격하면서 움직일 수 있어야함 stateMachine.PlayerMovement.Move(false); 공격 방향은 유지하고 싶었기 때문에 일반 이동처럼 캐릭터를 이동 방향으로 회전시키지 않는다. 그래서 false 를 전달했다. 공격 중에도 이동 애니메이션을 갱신하기 위해 이동 Parameter 역시 계속 전달한다. stateMachine.PlayerAnimation.SetMoveMagnitude(stateMachine.PlayerInput.MoveAction.magnitude); Vector3 moveDir = stateMachine.PlayerMovement.GetMoveDir(); Vector3 localMoveDir = stateMachine.transform.InverseTransformDirection(moveDir); stateMachine.PlayerAnimation.SetMoveDir(localMoveDir.x, localMoveDir.z); 하지만 여기서 Animator 구조 문제가 발생했다. 정지 상태에서 사용하는 공격은 전신 공격 애니메이션이다. 이 상태에서 실제 캐릭터 위치만 이동시키면 다리는 공격 자세에 고정된 채 캐릭터가 미끄러지는 것처럼 보였다. 3-2. 이동 공격을 UpperBody Layer로 분리 정지 공격과 이동 공격은 필요한 표현이 달랐다. 정지 공격 → 전신 공격 모션 이동 공격 → 하체는 이동 → 상체만 공격 그래서 이동 공격은 UpperBody Layer에서 처리했다. Weight : 1 Blending : Override Avatar Mask : 상체 Generic Rig의 Transform Mask에서는 골반과 다리를 제외했다. Spine 위쪽과 팔과 목과 머리 계열을 포함시켰다. UpperBody Layer의 기본 State는 Motion이 없는 Empty State로 구성했다. 이동 중 공격 입력이 들어오면 공격 A부터 D까지 순서대로 재생된다. Base Layer → CRun 8방향 이동 UpperBody Layer → Attack A / B / C / D 이렇게 구성하면서 이동 중에는 Base Layer의 하체 이동을 유지할 수 있었다. 상체만 공격 애니메이션으로 덮어씌울 수 있었다. 반대로 MoveMagnitude < 0.1 인 정지 상태에서는 Combat Sub-State Machine 내부의 전신 공격을 사용한다. 3-3. 트러블슈팅 - 정지 공격 도중 이동하면 하체가 공격 자세로 남는 문제 정지 상태에서 공격을 시작한 뒤 공격 애니메이션 도중 이동 입력을 넣어봤다. 캐릭터의 실제 위치는 움직였지만 하체는 계속 전신 공격 애니메이션을 유지했다. 처음부터 이동 중에 공격을 시작하면 UpperBody Layer를 사용하기 때문에 문제가 없었다. 문제는 다음 순서에서 발생했다. 정지 상태에서 전신 공격 시작 → 공격 중 이동 입력 이미 Base Layer가 전신 공격 State에 진입한 상태였다. 그래서 공격이 끝나기 전까지 하체가 이동 애니메이션으로 전환되지 않았다. 이 문제를 해결하기 위해 하체만 별도로 제어하는 LowerBody Layer도 실험했다. 하지만 실제로 적용해보니 8방향 이동과 공격 상태가 겹치면서 애니메이션이 부자연스럽게 섞였다. 특히 대각선 이동에서 문제가 크게 보였다. LowerBody Layer를 유지하지 않은 이유 현재 문제 하나를 해결하기 위해 Animator Layer 구조를 더 복잡하게 확장하면 다른 이동 애니메이션까지 함께 조정해야 했다. 현재 작업 범위에서는 얻는 효과보다 구조 복잡도가 더 커질 수 있다고 판단했다. 그래서 LowerBody Layer는 Weight를 0 으로 두고 실험 상태로 남겨뒀다. 현재는 다음 구조까지만 유지했다. 정지 상태에서 공격 시작 → 전신 공격 이동 중 공격 시작 → UpperBody 공격 + 하체 이동 정지 공격 도중 이동을 시작하는 경우의 하체 전환은 이후 폴리싱 단계에서 다시 검토하기로 했다. 이번 작업에서는 문제가 발견됐다는 이유만으로 새로운 시스템을 끝까지 확장하지 않았다. 현재 플레이에 미치는 영향과 작업 우선순위를 기준으로 구현 범위를 결정했다. 4. 공격 중 Jump / Dodge / Sprint 연결 4-1. AttackState에서 Jump와 Dodge 허용 AttackState에 진입했다는 이유로 기존 Jump와 Dodge를 모두 막으면 전투 조작이 지나치게 답답해진다. 현재 전투 규칙에서는 공격 중 Jump와 Dodge를 허용하기로 했다. 그래서 PlayerAttackState.Update() 의 앞부분에서 먼저 입력을 확인한다. //공격 중 점프 if (stateMachine.PlayerInput.IsJump && stateMachine.PlayerMovement.IsGrounded) { stateMachine.PlayerAnimation.SetJump(); stateMachine.ChangeJumpState(false); return; } //공격 중 회피 if (stateMachine.PlayerInput.IsSprint && stateMachine.PlayerMovement.IsGrounded) { stateMachine.ChangeState(PlayerStateType.Dodge); return; } 여기서 return 을 사용하는 이유는 State가 변경된 프레임에 기존 AttackState의 나머지 로직이 계속 실행되지 않도록 하기 위해서다. Gameplay State만 변경한다고 UpperBody 공격 애니메이션이 자동으로 종료되는 것은 아니었다. 그래서 Animator에서도 공격 중 Jump와 Dodge가 들어오면 Empty State로 빠질 수 있도록 Transition을 추가했다. UpperBody Attack → Empty Condition : Jump 또는 Dodge Has Exit Time : OFF Gameplay State의 취소 경로와 Animator의 취소 경로를 함께 맞췄다. 4-2. Sprint 중 공격 후 Combat으로 연결 Sprint 중 공격 입력도 별도로 처리했다. PlayerSprintState 에서 공격 입력을 확인한 뒤 AttackState로 전환한다. if (stateMachine.PlayerInput.IsAttack) { stateMachine.ChangeState(PlayerStateType.Attack); return; } Sprint 중 공격을 시작한 뒤 공격이 끝났다고 다시 Sprint로 돌아가는 흐름은 원하는 전투 방식과 달랐다. 공격을 시작하는 순간 IsCombat 이 활성화된다. 따라서 공격 종료 후에는 Combat Idle 또는 Combat Run을 사용하는 것이 자연스럽다고 판단했다. Sprint → Attack → IsCombat = true → 공격 종료 → MoveState → Combat Idle / Run Gameplay State는 기존 MoveState 를 사용한다. Animator가 IsCombat 값을 확인해서 실제 화면에는 Combat 이동을 재생한다. 전투용 MoveState를 별도로 만들지 않고 기존 MoveState를 재사용했다. 4-3. Combat 중 Dodge와 Jump도 기존 State 재사용 Combat 상태라는 이유만으로 DodgeState 와 JumpState 를 새로 만들지 않았다. 일반 상태와 Combat 상태에서 실제 Gameplay 행동 규칙은 크게 달라지지 않았다. 달라지는 부분은 재생해야 하는 애니메이션이었다. 그래서 Gameplay State는 기존 State를 유지했다. Animator에서 IsCombat 을 기준으로 애니메이션 경로만 분리했다. Combat Jump는 다음과 같은 흐름으로 구성했다. Combat Idle / Run → Combat Jump Start → Loop → Land 또는 LandRun 공격 중 JumpState로 전환된 경우에도 Combat 상태가 유지되기 때문에 Combat용 Jump 애니메이션 경로를 사용할 수 있다. Gameplay 행동 규칙이 같다면 애니메이션 종류가 다르다는 이유만으로 State를 추가하지 않았다. State는 그대로 유지하고 Animator 표현을 분기하는 방식이 현재 구조에 더 적합하다고 판단했다. 5. 일반 Hit과 4방향 피격 5-1. 일반 Hit을 PlayerState로 만들지 않은 이유 피격 애니메이션을 추가하면서 PlayerHitState 같은 별도 State가 필요한지도 확인했다. 일반 Hit과 Stun과 KnockDown을 비교해보니 행동 규칙이 달랐다. 일반 Hit은 HP가 감소하고 피격 모션이 재생된다. 하지만 플레이어의 행동을 일정 시간 강제로 제한하지는 않는다. 반면 Stun과 KnockDown은 현재 행동을 중단시킨다. 일정 시간 동안 입력 규칙도 변경된다. 그래서 다음 기준으로 나눴다. 일반 Hit → Gameplay State 변경 없음 Stun / KnockDown → Gameplay State 변경 State Pattern을 피격 종류별로 하나씩 만드는 것이 아니라 행동 규칙을 변경할 필요가 있는 경우에만 사용하기로 했다. 5-2. OnDamaged와 직접 피격 이벤트 분리 기존 PlayerHealth 에는 이미 OnDamaged 이벤트가 있었다. 이 이벤트는 PlayerMovement 에서 데미지를 받은 뒤 일정 시간 이동 속도를 감소시키는 기능에 사용하고 있었다. private void OnEnable() { playerHealth.OnDamaged += DamageSlow; } 그런데 Hit 애니메이션까지 OnDamaged 에 연결하면 문제가 생긴다. 현재 화상 데미지는 Coroutine에서 일정 주기마다 TakeDamage() 를 호출한다. 따라서 OnDamaged 에 Hit 애니메이션을 연결하면 화상 데미지가 발생할 때마다 캐릭터가 피격 애니메이션을 재생하게 된다. 기존 이벤트의 의미는 유지하면서 직접 피격만 따로 구분하기 위해 OnHit 을 추가했다. public event Action OnDamaged; public event Action<Vector3> OnHit; 공격 위치가 존재하는 직접 피격용 TakeDamage() 도 별도로 만들었다. //공격 위치가 있는 직접 피격 public void TakeDamage(int damage, Vector3 hitPosition) { TakeDamage(damage
Claude Code가 단순한 CLI 도구를 넘어 개발자 개개인의 작업 환경에 맞춰 런타임과 UI를 재구성할 수 있는 모듈형 플랫폼으로 진화했습니다. 이번 업데이트를 통해 도입된 모드(Mods) 아키텍처는 프로세스 외부에서 일회성으로 개입하던 기존 방식의 한계를 깨고, 클라이언트 내부 런타임에 직접 상주하며 도구 호출 인터셉트, 실시간 UI 렌더링, 세션 수명 관리까지 에이전트의 전체 루프를 제어합니다. 본문에서는 실무 생산성과 토큰 비용 절감을 이끄는 대표적인 커뮤니티 모드들의 활용법과 내부 실행 메커니즘, 그리고 안전한 모드 제작 및 검증 체계를 살펴봅니다. Claude Code Mods Are Game Changers. Set Up These 5 NOW. Claude Code 모드의 개념과 플러그인 동작 원리 Claude Code의 모드는 복잡한 UI 프레임워크를 다루지 않고도 TypeScript 몇 줄이나 자연어 프롬프트만으로 데스크톱 앱의 UI와 에이전트 동작을 변경할 수 있는 플러그인 확장 기능입니다. 공식 발표에서는 컨텍스트 사용량을 시각화하는 Token Weather, 변경 사항 실행 전 삭제 대상 파일을 미리 보여주는 Blast Radius, 변경된 diff를 단계별로 탐색하는 Replay Theater 등이 소개되었습니다. 이 시스템은 CLI와 데스크톱 앱 내부의 /plugin 명령어로 관리되는 확장 구조를 취합니다. 사용자는 Claude에게 특정 기능을 수행하는 모드를 만들어 달라고 요청하는 것만으로 신규 모드를 생성할 수 있으며, 기존에 작성된 플러그인은 마켓플레이스 등록 및 설치 명령어로 환경에 즉각 주입됩니다. # 플러그인 마켓플레이스 등록 및 커뮤니티 모드 설치 예시 claude plugin marketplace add anthropic/claude-plugins-community claude plugin install next-steps@claude-community 이러한 모드 중 하나인 next-steps 는 에이전트의 턴(Turn)이 끝날 때마다 다음에 수행할 작업 3가지를 UI 팝업 형태로 추천합니다. ClickUp 태스크 정리나 기능 구현 시 다음 단계를 일일이 프롬프트로 작성하지 않고 버튼 클릭이나 단순 선택만으로 워크플로우를 이어갈 수 있도록 돕습니다. 비용 및 세션 최적화 모드: cache-keeper와 recording-mode Claude Code 세션을 효율적으로 유지하기 위해 작업 로그를 바탕으로 설계된 대표 모드가 cache-keeper 입니다. Anthropic의 프롬프트 캐시는 세션 내에서 메시지가 오갈 때 캐시가 유지(warm)되지만, 60분 동안 입력이 없으면 캐시가 만료(cold)되어 이후 턴에서 전체 컨텍스트를 다시 읽는 비용이 발생합니다. cache-keeper 모드는 현재 컨텍스트 토큰 크기(예: 109k 토큰), 캐시 만료까지 남은 시간, 세션 및 주간 사용량, API 환산 예상 비용을 상태 표시줄 형태로 상시 렌더링합니다. 또한 캐시가 만료되기 5분 전에 "154k 토큰 캐시가 5분 뒤 콜드 상태가 됩니다. 재작성 시 약 $1.23가 소요됩니다"라는 경고 알림을 띄워 재프롬프트나 핸드오프를 유도합니다. [UI 상태 표시줄 구성] - Cache Warm: 58m 남음 - Context: 109k tokens - Rewrite Cost: $0.87 (만료 시 재작성 비용) - Rate Limit: 5h 10% / Weekly 59% - Actions: [Clear & Continue] [Session Handoff] 버튼 이 모드에는 단일 클릭으로 동작하는 세션 핸드오프 버튼이 결합되어 있습니다. 버튼을 누르면 내부 스킬이 실행되어 현재 세션의 결정 사항과 남은 태스크를 문서로 요약 저장하고, /clear 실행 후 요약본을 새 세션에 자동 주입하여 컨텍스트 창 비대화( Context Rot )를 방지합니다. 함께 사용되는 recording-mode 는 화면 녹화 중 민감한 이메일 주소, 금융 데이터, 가격 정보가 노출되지 않도록 감지하여 실시간으로 UI 텍스트를 마스킹( pricing.html 의 $29 -> $39 변경 내역 등을 플레이스홀더로 숨김) 처리하는 보안 역할을 수행합니다. 장기 작업 가시화와 다중 세션 파일 보호: Goal Tracker & Collision Guard 백그라운드에서 오래 실행되는 작업을 관리하기 위해 제작된 goal-tracker 모드는 /goal 명령어로 세션이 시작될 때 진가를 발휘합니다. 장기 에이전트 실행 시 현재 어떤 태스크를 수행 중인지, 몇 분이 경과했는지 파악하기 어렵다는 문제를 해결하기 위해, 에이전트가 생성한 실행 계획을 4~5단계의 체크리스트 UI( Goal Meter )로 변환해 실시간 진행률(%)과 경과 시간을 표시합니다. 다중 에이전트 환경에서 발생하는 치명적인 덮어쓰기 문제를 방지하는 모드가 collision-guard 입니다. 사용자가 여러 창에서 각각 다른 Claude 세션을 열어두고 병렬로 작업할 때, 특정 세션이 최근 30분 이내에 다른 세션에서 수정된 파일을 다시 쓰거나 편집(Edit, Write, NotebookEdit)하려 하면 즉각 실행을 중단합니다. 선택 옵션 동작 메커니즘 Proceed 충돌 경고를 무시하고 현재 세션의 수정을 그대로 파일에 반영 Move to a worktree Git Worktree를 별도로 생성하여 기존 수정 사항과 작업 분리 Cancel 파일 편집 요청을 즉시 거부하고 에이전트 동작을 중단 Other 대화창에 직접 지시사항을 입력하여 수동으로 해결 기존의 단순 hook 방식이 실행을 완전히 차단하거나 일방적인 스크립트를 돌렸다면, 이 모드는 파일 접근 이력을 검사한 뒤 UI 레벨에서 사용자에게 4가지 분기 선택지를 제공함으로써 에이전트가 다른 세션의 코드를 파괴하지 않도록 안전장치를 제공합니다. Claude Mods - The Biggest Claude Code Upgrade! 모드(Mods)의 본질: 외부 확장에서 내부 제어로의 진화 Claude Code v2.1.287 에 추가된 모드는 클라이언트 내부 런타임에 직접 상주하며 에이전트 루프, 도구 호출, UI 렌더링을 완전히 가로채고 재정의할 수 있는 내부 플러그인 아키텍처입니다. 기존의 스킬(Skills), 훅(Hooks), MCP 서버는 모두 클로드 코드 런타임 외부에 존재했습니다. 클로드 코드가 작업을 수행하다가 필요할 때 이 외부 도구들을 일회성으로 호출하는 방식이었기 때문에, 에이전트의 내부 실행 사이클이나 터미널 화면 출력 자체를 직접 제어하는 것은 불가능했습니다. 반면 모드는 클로드 코드 런타임 내부에 직접 상주하며 동작합니다. 이 구조는 모델이 프롬프트를 해석하고 실제 도구 호출(Tool call)을 실행하기 바로 전 단계에 모드가 직접 개입할 수 있음을 의미합니다. 모델의 출력을 가로채서 위험한 명령을 차단하거나, 파라미터를 강제로 수정하거나, 화면에 렌더링되는 결과물을 마스킹하는 제어권을 사용자에게 넘깁니다. DeepSeek Harness 가 모델 래퍼, 도구, 에이전트 루프 전체를 플러그인으로 교체 가능하게 만든 것과 동일한 설계 사상이 클로드 코드에 적용된 것입니다. 앤트로픽 자체도 이미 클로드 코드의 기본 기능 일부를 내부 모드로 구현해 배포하고 있습니다. 변경 사항을 보여주는 Diff 뷰( cc-plugin-diff )나 프로젝트 지침을 로드하는 AGENTS.md 지원 기능( cc-plugin-agents-md )이 대표적입니다. 향후 더 많은 코어 기능이 모드 형태로 전환될 예정이므로, 동일한 코어 엔진 위에서도 어떤 모드를 조합하느냐에 따라 사용자마다 완전히 다른 맞춤형 에이전트 하네스를 구축하게 됩니다. 구분 훅 (Hooks) 모드 (Mods) 실행 위치 프로세스 외부 (외부 셸 스크립트) 클로드 코드 내부 런타임 (JS/TS) 생명 주기 특정 이벤트마다 1회 실행 후 프로세스 종료 세션 시작 시 로드되어 세션 내내 상주 상태 유지 불가능 (호출 간 상태 유실) 가능 (세션 메모리 및 클로드 상태 저장) 제어 범위 사전 정의된 결정 반환 ( block 등) 명령 재작성, UI 렌더링 가로채기, 슬래시 커맨드 모드의 동작 메커니즘과 단일 모드의 3가지 개입 방식 모드는 register.ts 또는 register.js 파일을 통해 클로드 코드 이벤트에 등록됩니다. 빌드 단계 없이 디렉터리를 지정하면 핫 리로딩(Hot-reloading)을 지원하여, 코드를 수정하고 저장하는 즉시 열려 있는 세션에 반영됩니다. 단, 핫 리로딩 시 모드 내부 변수는 초기화되므로 영속성이 필요한 데이터는 클로드 코드의 내장 상태 객체( $.state )에 저장해야 합니다. 클로드 코드가 Bash 명령(예: git push --force )을 실행하려고 할 때, 등록된 모드 함수는 세 가지 인자를 전달받습니다: 이벤트 객체( e ), 클로드와 상호작용하는 인터페이스( $ ), 그리고 체인을 이어가는 next 함수입니다. 여기서 모드는 관찰(Observe), 재작성(Rewrite), 차단 및 직접 응답(Answer)의 세 가지 방식으로 동작할 수 있습니다. // Bash 도구 호출을 가로채는 모드 등록 예시 on('tool.call', { tool: 'Bash' }, async ($, e, next) => { // 1. 관찰 (Observe): 원래 흐름을 유지하며 로깅 // await next(e); // 2. 재작성 (Rewrite): 명령을 안전한 형태로 변경 // next({ ...e, command: "git push origin HEAD:fix-branch" }); // 3. 차단 및 직접 응답 (Answer): 명령을 취소하고 모델에게 반환 // return { deny: "No force pushes in this repo." }; }); 이 메커니즘은 단순 명령 제어뿐만 아니라 화면 렌더링 단계에도 그대로 적용됩니다. ui.render 이벤트를 가로채면 모델의 실제 컨텍스트는 손상시키지 않은 채 사용자가 보는 화면에서만 이메일이나 API 키 같은 민감 정보(PII)를 실시간으로 마스킹할 수 있습니다. 또한 터미널 상단에 고정 패널을 그려 로컬 GPU 서버(예: DGX Spark 클러스터)의 상태를 출력하거나, 컨텍스트 윈도우 사용량을 시각화하는 게이지를 띄우는 작업도 단일 모드 스크립트 안에서 동시에 처리됩니다. 세션 기록 기반 자동 모드 생성과 보안 검증 체계 모드는 일일이 코드를 작성할 필요 없이 클로드 코드 내에서 프롬프트만으로 자동 생성할 수 있습니다. 특히 효과적인 방법은 사용자의 이전 작업 기록(최근 20개 세션)을 클로드에게 분석시켜 자주 묻는 질문, 실행 전 망설였던 위험 명령( rm -rf 등), 매번 수동으로 확인하는 작업 패턴을 도출한 뒤 전용 모드로 변환하는 워크플로우입니다. # 과거 세션 기반 커스텀 모드 도출 프롬프트 Read this guide: claude.dev/blog/getting-started-with-claude-code-mods Go through my last 20 sessions and see how I use you day to day. 1. Things I keep asking you, or asking you to do. 2. Commands that make me nervous: ones I stopped, rejected, or had you undo. 3. How I check your changes. For each idea: a name, my exact words, a few 'What it should show' lines. Rank the ideas by how much they'd help me. Show me the ideas first. Don't build anything until I pick. 하지만 모드는 사용자의 권한을 그대로 위임받아 파일 시스템 읽기/쓰기, 임의 프로그램 실행, 네트워크 호출( $.http.fetch )을 직접 수행하므로 치명적인 보안 공격 벡터가 될 수 있습니다. 신뢰할 수 없는 외부 모드를 설치할 경우 세션 정보나 민감한 환경 변수가 탈취될 위험이 있습니다. 따라서 타인이 작성한 모드를 설치하기 전에는 반드시 claude plugin validate <경로> 명령어를 실행해야 합니다. 이 유효성 검사 도구는 해당 모드가 어떤 이벤트를 감시하고, 어떤 내부 함수( $.ui.ask , $.state.set 등)를 호출하며, 외부 네트워크 요청을 시도하는지 매니페스트와 코드를 분석해 명세화합니다. 불필요한 네트워크 통신이나 의심스러운 훅이 발견되면 즉시 설치를 중단해야 합니다. 정리 Claude Code v2.1.287의 모드(Mods) 시스템은 에이전트 확장 패러다임을 '외부 도구 호출'에서 '내부 런타임 제어'로 전환했습니다. 프로세스 수명과 함께 동작하는 모드는 도구 호출의 관찰·재작성·차단은 물론 UI 렌더링 영역까지 깊숙이 개입할 수 있습니다. 이를 통해 캐시 만료 전 핸드오프를 유도해 API 비용을 아끼거나( cache-keeper ), 다중 세션 간 파일 쓰기 충돌을 방지하고( collision-guard ), 민감 정보를 실시간 마스킹하는( recording-mode ) 정교한 제어가 가능해졌습니다. 사용자는 과거 세션 로그를 기반으로 자신에게 최적화된 모드를 직접 구축할 수 있게 되었으며, 강력해진 시스템 권한만큼 외부 모드 도입 시에는 정적 유효성 검증 명령어를 통한 사전 점검이 필수가 되었습니다. 다룬 영상 Claude Code Mods Are Game Changers. Set Up These 5 NOW. — Nate Herk | AI Automation · 11분 Claude Mods - The Biggest Claude Code Upgrade! — Prompt Engineering · 10분
"숫자는 늘었는데, 왜 문제가 해결되지 않을까?" 지역 성과 및 공급 최적화 사례로 보는 지표 상충(Trade-off)과 드라이버 트리(Driver Tree) 사고법 한 줄 요약 단일 지표 상승에 착시를 느끼지 않고, MECE 기반의 원인 분해 트리(Driver Tree)를 통해 지표 간 상충 관계(Trade-off)를 파악하고 진짜 비즈니스 임팩트를 만들어내는 구조적 레버(Lever)를 찾는 사고법을 소개합니다. i️ 이 글은 지역별 서비스 공급 최적화 및 성과 분석 프로젝트 경험을 가상 예시로 재구성하여, 비즈니스 분석가(BA)가 복잡한 사업 문제를 구조화하고 진짜 원인을 역추적하는 해결 프레임워크를 공유하는 글입니다. 들어가며 "이번 달 지역별 배차 완수 건수가 전월 대비 20%나 증가했습니다! 드디어 공급 부족 문제가 해결되고 있는 것 같습니다." 월간 전사 리뷰 회의에서 신나게 발표된 수치. 화면 속 차트는 매끄러운 우상향을 그리고 있고, 대시보드의 전체 처리 건수 KPI는 초록색 불을 밝히고 있습니다. 겉으로 보기엔 모든 문제가 잘 해결되고 있는 것처럼 보입니다. 하지만 불과 일주일 뒤, 영업팀과 CS팀에서 비명이 터져 나오기 시작합니다. *"전체 배차 건수는 늘었다는데, 정작 핵심 매출이 발생하는 A 지역 고객 이탈률은 왜 급증했죠?"* *"단기 프로모션으로 기사님 공급을 끌어모았더니, 수수료 오남용 리스크와 취소 건수가 함께 폭증하고 있습니다."* 어떻게 된 일일까요? "숫자는 분명히 올랐는데, 비즈니스 문제는 오히려 악화되는 현상" . 이는 현업 분석가들이 가장 흔하게 빠지는 '비즈니스 지표의 역설' 입니다. 단일 KPI의 착시에 속지 않고, 지표 뒤에 숨겨진 트레이드오프(Trade-off)를 파악하며, 진짜 임팩트를 내는 레버(Lever)를 찾는 분석 사고법에 대해 이야기해 보겠습니다. 1. 지표의 역설: 하나만 쫓다 다른 핵심 지표가 망가지는 이유 사업이 복잡해질수록 단 하나의 지표(North Star Metric)만으로 비즈니스의 건강도를 측정하는 것은 위험합니다. 특정 지표를 극대화하려는 시도는 필연적으로 다른 지표와의 상충 관계(Trade-off) 를 발생시키기 때문입니다. 구분 단일 지표 집착형 접근 (KPI Illusion) 구조적 지표 설계 접근 (Balanced View) 주요 목표 전체 처리 건수, 총 거래액(GMV) 등 거시 수치 극대화 효율성, 오남용 리스크, 고객 만족도가 결합된 내실 극대화 의사결정 방식 수치가 오르면 문제 해결로 단정 지표 간의 상충 관계(Trade-off) 및 부작용 동시 검증 대표적 부작용 체리피커 유입, 특정 지역/고객군 외면, 오남용 리스크 단기 성과 지연처럼 보이나 지속 가능한 성장의 기틀 마련 문제 원인 분석 "마케팅비 더 쓰자", "공급자 수 늘리자" 식의 단선적 접근 MECE 구조화를 통한 드라이버 트리 역추적 지역 성과 및 공급 최적화에서 발생하는 대표적 Trade-off 지역 기반의 플랫폼/인프라 서비스에서 공급을 늘릴 때 자주 발생하는 3가지 지표 상충 패턴입니다. 전체 공급량 vs 지역적 불균형 (Geographic Imbalance) 전체 배차 건수를 늘리기 위해 외곽 지역의 손쉬운 건만 늘리고, 정작 수요 밀집 지역(Hotspot)의 공급은 방치되는 현상. 양적 성장 vs 오남용 리스크 (Fraud & Abuse Risk) 공급 보조금을 지급하여 처리 건수를 늘렸으나, 공급자 간의 어뷰징이나 단거리 쪼개기 수주 등 체리피킹 행위 증가. 단기 매칭 성공률 vs 장기 고객 retention 매칭 건수는 늘었으나, 품질이 저하된 공급자와 연결되어 고객 불만이 누적되고 핵심 타깃층이 이탈. 2. 복잡한 문제를 쪼개는 기술: MECE 기반 드라이버 트리(Driver Tree) 숫자가 오르고 내림에 갇히지 않으려면, 최상단 지표(Top-line Metric)를 하위의 세부 구성 요인(Driver) 으로 쪼개 들어가는 연습이 필요합니다. 이때 상호 배타적이면서 모의 합이 전체를 이루는 MECE(Mutually Exclusive, Collectively Exhaustive) 원칙이 핵심이 됩니다. 가상의 '지역 공급 최적화' 문제를 해결하기 위한 드라이버 트리(Driver Tree) 예시를 살펴봅시다. [최종 비즈니스 임팩트: 월간 정산 순이익 (Net Profit)] │ ├── [1. 총 매출 (Total Revenue)] │ ├── [1-1. 총 완료 건수 (Completed Orders)] │ │ ├── 전체 유효 수요 (Demand) │ │ └── 매칭 성공률 (Matching Rate %) │ │ ├── 피크타임 매칭률 (Hotspot) │ │ └── 비피크타임 매칭률 (Off-peak) │ └── [1-2. 건당 평균 단가 (AOV)] │ └── [-] [2. 총 비용 및 손실 (Total Cost & Loss)] ├── [2-1. 공급자 프로모션 비용 (Incentive Cost)] │ ├── 정상 유인 비용 │ └── 어뷰징/오남용 비용 (Fraud/Abuse) └── [2-2. 고객 이탈 손실 (Churn Cost)] └── 불만족 취소 건수 × 고객 생애 가치(LTV) 드라이버 트리로 밝혀낸 "숫자가 올라도 문제가 해결되지 않았던 이유" 단순히 총 완료 건수 만 늘리는 프로모션을 집행했을 때 발생한 일은 다음과 같이 분해할 수 있습니다. 표면적 수치: 총 완료 건수 ↑ (성공으로 착각) 드라이버 분해: 비피크타임 매칭률 만 대폭 상승하고, 매출 비중이 큰 피크타임 매칭률 은 제자리. 공급자 보조금 증가로 인한 어뷰징/오남용 비용 폭증. 피크타임 대기 시간 증가로 인한 불만족 취소 건수 증가 및 고객 이탈 손실 누적. 결과적으로 매출 증가분보다 비용 및 이탈 손실의 증가분이 더 커져 순이익과 고객 건강도는 오히려 악화되었던 것입니다. 3. 분석가의 진짜 역할: 임팩트를 만들어내는 '구조적 레버(Lever)' 역추적 뛰어난 데이터 분석가는 단순히 "이번 달 A 지역의 수치가 X% 하락했습니다"라고 리포팅하는 보고자가 아닙니다. "수치를 변화시키기 위해 조직이 당겨야 하는 진짜 레버(Lever)가 무엇인가?" 를 역추적하여 제시하는 사람입니다. [1. 현상 파악] ──> [2. 드라이버 트리 분해] ──> [3. Trade-off 검증] ──> [4. 핵심 레버(Lever) 작동] "완수 건수 ↑" "어떤 요소가 변했나?" "부작용 지표는?" "피크타임 공급 보상 재설계" 임팩트를 내는 레버를 찾는 3단계 역추적 사고법 지표의 수식 구조화 (Mathematical Structuring) 지표를 단순 합계가 아닌 곱셈/나눗셈 수식으로 바꿉니다. 예: 지역 매출 = 방문 고객 수 × 검색 전환율 × 매칭률 × 평균 단가 수식으로 쪼개야 어떤 변수가 지수적(Exponential) 임팩트를 만들어내는지 파악할 수 있습니다. 제약 조건(Constraint)과 Bottleneck 탐색 전체 시스템의 병목이 '수요'에 있는지, '특정 시간대의 공급'에 있는지, '가격 저항'에 있는지 식별합니다. 병목이 아닌 곳의 수치를 올리는 것은 자원 낭비일 뿐입니다. 액셔너블한 레버(Actionable Lever) 정의 "매칭률을 올리자"는 레버가 아닙니다. "피크타임 핵심 3개 지역의 공급자 대기 수수료 구조를 가산제로 전환하여 피크타임 매칭률을 15%p 끌어올리자" 가 진짜 비즈니스 레버입니다. 4. BA 실무자를 위한 문제 해결 체크리스트 다음 프로젝트나 지표 분석을 진행할 때, 스스로에게 아래 질문들을 던져보세요. 내가 보고 있는 표면적 KPI 상승 뒤에 희생되고 있는 부작용 지표(Counter Metric) 는 없는가? 문제를 풀고자 하는 대상 지표가 MECE 관점에서 하위 드라이버로 분해 되어 있는가? 특정 지역/그룹의 착시 효과(평균의 함정) 때문에 세부 병목 지점을 놓치고 있지 않은가? 내 분석의 결과물이 단순 '현상 요약'을 넘어, 현업이 당장 작동시킬 수 있는 '행동 레버(Lever)' 를 제시하고 있는가? 단기적 수치 개선이 아닌 장기적인 LTV 및 비즈니스 건강도 에 긍정적 영향을 미치는가? 마무리 숫자는 거짓말을 하지 않지만, 정돈되지 않은 단일 수치는 우리를 쉽게 속입니다. 비즈니스 분석가의 참된 가치는 대시보드의 숫자를 예쁘게 꾸미는 데 있지 않습니다. 복잡하게 얽힌 비즈니스 지표 간의 실타래를 MECE 구조로 풀어내고, 그 안에서 상충하는 트레이드오프를 통제하며, "어떤 레버를 당겼을 때 비즈니스가 가장 건강하게 성장하는가" 를 증명해 내는 데 있습니다. 다음에 "숫자는 늘었는데 이상하게 문제가 안 풀린다"는 소리가 들린다면, 대시보드에서 한 걸음 물러나 드라이버 트리를 그려보세요. 진짜 해결해야 할 문제가 어디에 숨어있는지 비로소 보이기 시작할 것입니다.
거대 언어 모델(LLM)을 소프트웨어 파이프라인에 결합할 때 발생하는 가장 큰 병목은 텍스트 생성 방식 그 자체에 있습니다. 전통적인 자기회귀 모델의 순차적 토큰 생성과 파싱 과정을 걷어내고, 단일 순방향 패스로 정밀 보정된 확률 결정을 반환하는 '시스템 1' 전용 의사결정 모델이 지연 시간과 비용 문제를 근본적으로 해결하는 대안으로 자리 잡고 있습니다. How Can A Model Be Better Without Generating Words? 텍스트 생성의 한계와 결정 전용 모델 'Jev'의 등장 기존 소프트웨어 시스템은 문장이나 단락 형태의 텍스트가 아니라 approve 나 reject 와 같은 명확하고 구조화된 상태값을 요구합니다. 그러나 전통적인 자기회귀(Autoregressive) LLM은 확률적 텍스트를 토큰 단위로 하나씩 풀어내는 구조로 동작합니다. 개발자는 프롬프트 엔지니어링이나 JSON 스키마 강제 기법을 거쳐 모델이 뱉어낸 텍스트를 다시 소프트웨어 상태값으로 파싱해야 했습니다. 이 과정에서 포맷 오류, 런타임 예외, 불필요한 출력 토큰 대기 시간으로 인한 지연이 상시 발생했습니다. 2026년 9월, ChatGPT 지시 튜닝 공동 연구자인 Diogo Almeida가 창업한 TypeSafe AI는 자유 텍스트 생성을 완전히 배제하고 구조화된 결정만을 내리는 모델 Jev 를 공개했습니다. Jev는 인간 심리학의 시스템 1(System 1) , 즉 직관적이고 즉각적인 판단 개념을 차용했습니다. 비정형 텍스트로 된 입력 상태와 결정해야 할 질문, 그리고 허용된 선택지 목록을 인자로 받아 토큰 생성 없이 옵션별 확률 분포만을 직접 계산해 반환합니다. Jev는 세 가지 의사결정 출력 형식을 지원합니다. CHOICE : 최대 255개의 선택지 중 하나를 선택하며 전체 확률 분포를 반환합니다. SCORE : 순서가 있는 척도(예: 0~4점)에 걸친 가중 평균 점수와 분포를 반환합니다. NOUL : 참/거짓 판단과 함께 각 상태의 정확한 보정 확률(예: 사기 거래 여부 87%)을 직접 출력합니다. 일반 LLM과 Jev를 필두로 한 시스템 1 의사결정 모델의 구조적 차이는 다음과 같습니다. 기능 구분 일반 LLM (Autoregressive) 시스템 1 모델 (Jev 등) 출력 방식 토큰 단위 순차 생성 ( text -> json ) 단일 순방향 패스(1-pass) 확률 직접 출력 소프트웨어 연동 파싱 로직 필수, 문법 에러 위험 결정값·확률을 코드에서 즉시 조건문으로 활용 지연 시간 수백 ms ~ 수 초 (토큰 수 비례) 70 ~ 500 ms (상태 크기에만 비례) 출력 비용 입력 대비 ~5배 고가 $0 (토큰을 생성하지 않음) 성능 격차의 원인: 1-Pass 연산과 RLCD를 통한 신뢰도 보정 일반적인 자기회귀 LLM은 단 하나의 단어를 결정하기 위해서도 이전 토큰을 콘텍스트에 누적해가며 모델 전체를 수십 번 반복 실행(Forward Pass)해야 합니다. 반면 Jev는 허용된 옵션 집합에 대해 단 한 번의 패스(1-pass)로 연산을 끝냅니다. 이러한 구조적 차이 덕분에 엔드투엔드 지연 시간이 70~500ms 수준으로 압축됩니다. TypeSafe 자체 벤치마크 기준 프론티어 생성형 모델 대비 최대 194배 빠르고 440배 저렴합니다. 가격 정책 역시 차별화되어 입력 100만(1M) 토큰당 $0.042(10억 토큰당 $42)이며, 출력 토큰 비용은 아예 책정되지 않습니다($0). 단순 분류기와 Jev를 구분 짓는 가장 중요한 기술적 차이는 RLCD(Reinforcement Learning for Calibrated Decisions) 입니다. 일반적인 소형 분류기나 LLM이 출력하는 소프트맥스 신뢰도 수치는 과적합이나 편향으로 인해 임의적인 경우가 많아 소프트웨어의 분기 조건으로 그대로 사용하기 어렵습니다. Jev는 모델이 90% 신뢰도로 판단한 결정이 통계적으로 실제 10번 중 9번 들어맞도록 정밀 보정(Calibration)을 거쳤습니다. 소프트웨어 제어 하네스(Harness)는 이 정밀 보정된 확률값 $p$를 전달받아 명확한 정책 분기를 실행할 수 있습니다. $p \ge 0.96$: 사람의 개입 없이 즉시 기능을 자동 실행( execute ) $0.70 \le p < 0.96$: 신뢰도가 기준치에 미치지 못하므로 운영자 승인 요청( ask a human ) $p < 0.70$: 단순 결정이 어려운 복잡한 상태로 간주하여 더 무겁고 정밀한 추론형 LLM으로 작업 에스컬레이션( escalate ) 시스템 1 모델의 내부 구조: 양방향 인코더와 대안 프로젝트들 Jev는 세부 아키텍처를 완전히 공개하지 않았으나, Fastino AI가 발표한 정보 추출/분류 모델 GLiNER2 와 입력 가드레일 모델 GLiGuard 의 구조를 통해 기저 동작 원리를 파악할 수 있습니다. 이들은 다음 토큰을 가리는 인과적 마스킹(Causal Masking) 대신 모든 토큰이 입력을 한 번에 상호 참조하는 BERT 스타일의 양방향 트랜스포머 인코더(Bidirectional Transformer Encoder) 를 사용합니다. 입력 텍스트와 태스크 정의, 후보 라벨( [L] safe , [L] unsafe )을 단일 시퀀스로 묶어 인코딩한 뒤, 각 라벨 토큰의 표현 벡터를 소형 공유 MLP(다층 퍼셉트론)에 통과시켜 로짓(Logit)과 소프트맥스 확률을 추출하는 구조입니다. Jev 등장 전후로 오픈소스 및 주요 AI 기업 진영에서도 동일한 메커니즘을 적용한 구현체가 발표되었습니다. Laya (Convai) : 3억 9,500만(395M) 파라미터의 ModernBERT-large 백본에 2개 트랜스포머 레이어로 구성된 의사결정 헤드 및 에스컬레이션 헤드를 얹어 총 421M 파라미터로 구축되었습니다. 2,000개 결정 벤치마크에서 미세조정 버전 기준 76.6%의 정확도와 약 16ms의 지연 시간을 기록했습니다. Von : ModernBERT-large 백본을 사용하며 Jev와 동일한 API 인터페이스 호환성을 제공합니다. 크로스 엔트로피 손실 외에 브라이어 점수 손실(Brier Score Loss)과 후처리 온도 스케일링(Temperature Scaling)을 적용해 확률 보정 품질에 집중했습니다. 로컬 GPU 및 애플 실리콘 환경에서 약 18ms의 지연 시간을 달성했습니다. GLiNER 2.5-Decide (Fastino) : DeBERTa-v3-large (340M) 기반으로 오픈소스 의사결정 벤치마크 성능을 갱신한 후속 모델입니다. 기존 LLM의 로짓 재활용 기법 : Qwen 계열 모델을 활용해 선택지 인덱스(0, 1, 2, 3) 토큰 위치의 로짓만 직접 정규화해 추출하는 System One 방식, 전체 후보 문자열 시퀀스의 누적 로그 확률($\sum \log p$)을 비교하는 OpenJev 방식이 공유 KV 캐시를 통해 연구되고 있습니다. 기타 주요 동향 : Liquid AI는 2026년 9월 다국어 지원 의사결정 전용 모델인 D1 을 공개했으며, OpenAI는 DevDay 2026에서 자체 의사결정 엔드포인트인 Decisions API 티저를 공개했습니다. 한편 음성 에이전트 인프라 측면에서는 83개 언어를 단일 모델로 지원하며 MCP 연동을 제공하는 텍스트-음성 변환 플랫폼 Fish Audio(S2.1 Pro) 등이 연계 기술로 결합되는 추세입니다. 정리 에이전트 파이프라인에서 단순 분류, 부서 라우팅, 가드레일 판별과 같은 결정 작업을 거대 자기회귀 LLM에 맡기고 텍스트 출력을 파싱하는 방식은 속도와 비용 면에서 명확한 한계를 지닙니다. 텍스트 생성을 포기하고 단일 순방향 패스로 보정된 확률 분포를 출력하는 비자기회귀 의사결정 모델(Jev, Laya, Von 등)은 지연 시간을 수십 밀리초대로 낮추고 출력 토큰 비용을 0으로 만듭니다. 에이전트 아키텍처를 설계할 때는 모든 판단을 고가의 생성형 LLM에 의존하기보다, 앞단에 RLCD 기반의 시스템 1 모델을 배치해 신뢰도 확률($p$)에 따라 자동 실행, 운영자 검토, 대형 추론 모델 호출로 분기하는 다단계 제어 구조를 구축하는 것이 실용적인 표준으로 자리잡고 있습니다. 다룬 영상 How Can A Model Be Better Without Generating Words? — bycloud · 17분
📝 플레이어 애니메이션 리팩토링 이번 작업에서는 플레이어 애니메이션 구현을 마무리한 뒤 지금까지 작성한 플레이어 코드를 전체적으로 다시 점검했다. 기능 자체는 대부분 정상적으로 동작하고 있었다. 하지만 기능을 하나씩 추가하면서 애니메이션 길이를 코드에서 직접 관리하는 부분이 생겼다. PlayerAnimation 이 Combat 상태와 종료 시간까지 관리하는 등 각 클래스의 책임도 조금씩 섞이기 시작했다. 몬스터와 실제 전투 로직을 연결하기 전에 플레이어 구조를 한 번 정리하는 편이 좋다고 판단했다. 이번 리팩토링에서는 다음 세 가지를 기준으로 잡았다. 리팩토링 기준 Gameplay 상태와 행동 규칙은 C# 코드에서 관리한다. Animator는 Gameplay 결과를 화면에 표현하는 역할에 집중한다. 애니메이션 Clip 길이가 변경돼도 Gameplay 코드를 함께 수정하지 않는 구조를 만든다. 이번 작업에서는 플레이어가 사용하는 10개의 State와 주요 플레이어 컴포넌트를 다시 확인했다. 애니메이션 Clip 길이를 코드에 직접 입력해서 사용하던 6개의 의존성도 제거했다. 1. 플레이어 애니메이션 구현 마무리 1-1. 플레이어 행동별 애니메이션 연결 현재 플레이어는 다음 행동에 대한 애니메이션 연결을 완료했다. 이동 전투 이동 질주 질주 정지 일반 점프 질주 점프 정지 착지 이동 착지 방향 회피 기본 공격 방향 피격 Stun KnockDown Die Rebirth Interact Gameplay State는 총 10개를 사용한다. Move Jump Dodge Sprint Attack Stun KnockDown Die Rebirth Interact 모든 기능을 별도의 State로 만든 것은 아니다. 처음에는 피격도 하나의 기능이라고 생각해서 PlayerHitState 를 만들었다. 하지만 일반 피격과 Stun과 KnockDown의 실제 행동 규칙을 비교하면서 같은 피격이라도 State 사용 여부가 달라져야 한다고 판단했다. 일반 피격은 HP가 감소해도 현재 행동을 계속할 수 있다. 그래서 별도의 State로 전환하지 않는다. 현재 State가 MoveState 일 때 피격 애니메이션만 재생하도록 구성했다. 반면 Stun과 KnockDown은 현재 행동을 중단시킨다. 일정 시간 동안 행동도 제한한다. 이 경우에는 플레이어의 행동 규칙 자체가 달라지기 때문에 각각 별도의 State로 유지했다. State Pattern을 기능의 개수에 맞춰 만드는 것이 아니라 실제 행동 규칙이 달라지는 시점을 기준으로 사용하도록 정리했다. 1-2. 점프 애니메이션 시작 책임을 JumpState로 이동 기존에는 점프로 전환하기 전 State에서 점프 애니메이션까지 실행하는 부분이 있었다. 이 구조에서는 Move에서 점프하는 경우와 Sprint에서 점프하는 경우마다 이전 State가 Jump의 애니메이션 구현까지 알아야 했다. 점프 상태가 어떤 애니메이션으로 시작될지는 PlayerJumpState 가 결정하는 편이 자연스럽다고 판단했다. 현재는 JumpState로 변경할 때 일반 점프인지 질주 점프인지만 전달한다. stateMachine.ChangeJumpState(false); stateMachine.ChangeJumpState(true); 실제 애니메이션 선택은 PlayerJumpState.Enter() 에서 처리한다. public override void Enter() { if (isSprintJump) { stateMachine.PlayerAnimation.SetSprintJump(); } else { stateMachine.PlayerAnimation.SetJump(); } stateMachine.PlayerMovement.Jump(); } 착지 역시 애니메이션 시간을 기준으로 판단하지 않는다. 실제 CharacterController 의 Ground 상태를 기준으로 착지를 판단한다. if (stateMachine.PlayerMovement.IsGrounded) { bool isMove = stateMachine.PlayerInput.MoveAction.sqrMagnitude > 0.001f; if (isMove) { stateMachine.PlayerAnimation.SetJumpLandRun(); } else { stateMachine.PlayerAnimation.SetJumpLand(); } } 이를 통해 점프의 실제 시작과 종료 기준은 Gameplay 코드가 관리하도록 했다. Animator는 그 결과에 맞는 애니메이션을 표현한다. 1-3. 방향 회피와 Root Motion 회피는 입력 방향에 따라 8방향 애니메이션을 사용한다. 현재 이동 입력은 카메라 기준 월드 방향으로 계산된다. Animator에서는 캐릭터 기준 방향이 필요하기 때문에 회피를 시작할 때 Local 방향으로 변환한다. Vector3 localDodgeDir = stateMachine.transform.InverseTransformDirection(dodgeDir); 이 값을 이용해서 전방과 후방과 좌우와 대각선 방향에 맞는 회피 애니메이션을 선택한다. 회피 이동에는 Root Motion을 사용한다. 하지만 Animator의 Root Motion을 Transform에 바로 적용하지 않는다. PlayerAnimation 이 Animator.deltaPosition 을 받아서 PlayerMovement 로 전달한다. private void OnAnimatorMove() { if (!useRootMotion) return; playerMovement.MoveRootMotion(animator.deltaPosition); } 최종 이동은 기존 이동과 동일하게 CharacterController.Move() 를 사용한다. 일반 이동과 Root Motion 이동 모두 최종적인 이동 처리를 CharacterController 를 통해 수행하도록 유지했다. 2. 애니메이션 길이에 의존하던 코드 제거 2-1. 문제 - 애니메이션 길이를 State에서 직접 관리 기존에는 일부 State가 애니메이션 종료 시점을 초 단위 값으로 직접 관리하고 있었다. 대표적으로 다음 값들이 존재했다. Stun 2.0초 Rebirth 2.458초 Interact End 0.958초 KnockDown End 1.375초 Attack 2.333초 Dodge 회피 애니메이션 길이 처음 구현할 때는 간단한 방법이었다. 애니메이션 길이를 확인한 뒤 같은 시간을 State에 입력하면 해당 시간이 지난 후 다음 State로 이동시킬 수 있었다. 하지만 플레이어 애니메이션 구조를 다시 확인하면서 문제가 보였다. 이 숫자들은 Gameplay 규칙이 아니었다. 현재 사용 중인 애니메이션 Clip이 가지고 있는 재생 시간이었다. 애니메이션을 다른 Clip으로 변경하거나 재생 속도를 수정하면 State 코드의 시간도 함께 수정해야 했다. Animator가 이미 알고 있는 애니메이션 종료 시점을 C#에서도 다시 관리하고 있던 셈이었다. 2-2. Gameplay 시간과 Animation 시간을 구분 이번 리팩토링에서는 모든 시간값을 제거하지 않았다. 숫자가 있다는 이유만으로 제거하는 것이 아니라 해당 값이 무엇을 의미하는지 먼저 구분했다. 현재 유지한 대표적인 값은 다음과 같다. Interact 3.0초 → 실제 상호작용 유지시간 KnockDown 2.0초 → 실제 KnockDown 유지시간 DodgeMoveCancelTime 0.35초 → 회피 후 이동으로 취소할 수 있는 시점 ComboInterval 0.5초 → 다음 콤보로 넘어가는 전투 템포 이 값들은 실제 Gameplay 규칙이나 플레이 조작감을 조절하기 위한 값이다. 반대로 애니메이션 자체가 몇 초인지를 나타내는 값은 제거했다. 같은 시간값이라도 숫자라는 이유만으로 동일하게 취급하지 않았다. 실제 플레이 규칙을 의미하는지부터 확인한 뒤 코드에 남길지 결정했다. 2-3. 공통 AnimationEnd 구조 추가 애니메이션 종료를 확인하기 위해 각 State가 Animator의 현재 State를 직접 조회하는 방법도 사용할 수 있었다. 하지만 그렇게 하면 Gameplay 코드가 Animator Controller 내부 State 이름과 구조를 알아야 한다. 이번 리팩토링에서 정한 방향과 맞지 않았다. 대신 Animation Event를 이용해서 애니메이션이 끝났다는 사실만 코드로 전달하도록 했다. PlayerAnimation 에 공통 이벤트를 추가했다. public event Action OnAnimationEnd; //애니메이션 종료 public void AnimationEnd() { OnAnimationEnd?.Invoke(); } PlayerStateMachine 은 해당 이벤트를 받아 현재 State로 전달한다. //애니메이션 종료 private void AnimationEnd() { currentState?.OnAnimationEnd(); } 모든 State가 필요한 경우 사용할 수 있도록 PlayerState 에도 메서드를 추가했다. public virtual void OnAnimationEnd() { } 애니메이션이 끝난 뒤 실제로 어떤 행동을 해야 하는지는 현재 State가 판단한다. 예를 들어 Rebirth는 애니메이션 종료 후 MoveState로 복귀한다. public override void OnAnimationEnd() { stateMachine.ChangeState(PlayerStateType.Move); } StateMachine은 Rebirth 애니메이션이 몇 초인지 알 필요가 없다. Animator도 MoveState가 무엇인지 알 필요가 없다. Animation Event는 애니메이션이 종료됐다는 사실만 전달한다. 2-4. AnimationEnd 적용 결과 이번 작업에서 다음 애니메이션 길이 의존성을 제거했다. Stun → 2.0초 타이머 제거 Rebirth → 2.458초 타이머 제거 Interact End → 0.958초 타이머 제거 KnockDown End → 1.375초 타이머 제거 Attack → 2.333초 AttackDuration 제거 Dodge → DodgeDuration 제거 총 6개의 애니메이션 길이 기반 로직을 제거했다. 애니메이션 Clip이 변경돼도 해당 State의 종료 시간을 다시 수정할 필요가 없도록 정리했다. 3. State별 종료 규칙 정리 3-1. Stun Stun은 기존에 애니메이션 길이만큼 시간을 계산한 뒤 MoveState로 복귀했다. 현재는 Stun 애니메이션의 마지막에 AnimationEnd Event를 배치했다. public override void OnAnimationEnd() { stateMachine.ChangeState(PlayerStateType.Move); } Stun 상태에서는 회피를 통한 탈출도 가능하다. if (stateMachine.PlayerInput.IsSprint && stateMachine.PlayerMovement.IsGrounded) { stateMachine.ChangeState(PlayerStateType.Dodge); return; } Stun 중에는 무기를 강제로 표시한다. Stun 애니메이션에 맞는 전용 위치와 회전값도 적용한다. 어떤 경로로 State를 빠져나가더라도 원래 무기 상태로 돌아가야 한다. 그래서 원복 처리는 Exit() 에 배치했다. public override void Exit() { stateMachine.PlayerAnimation.ResetStunWeapon(); } 정상적으로 Stun 애니메이션이 끝난 경우에도 실행된다. 회피로 중간에 State를 벗어난 경우에도 동일하게 실행된다. 3-2. KnockDown KnockDown에서는 2.0f 를 제거하지 않았다. 이 값은 KnockDown 애니메이션 길이가 아니다. 실제로 플레이어를 쓰러진 상태로 유지할 Gameplay 시간이다. if (!isKnockDownEnd && knockDownTimer >= 2.0f) { isKnockDownEnd = true; stateMachine.PlayerAnimation.SetKnockDownEnd(); } 반면 일어나는 End 애니메이션 길이를 재던 1.375f 는 제거했다. End 애니메이션이 끝나면 Animation Event가 발생한다. public override void OnAnimationEnd() { if (!isKnockDownEnd) return; stateMachine.ChangeState(PlayerStateType.Move); } isKnockDownEnd 를 함께 확인하는 이유도 있다. KnockDown의 시작 단계나 유지 단계에서 Animation Event가 발생하더라도 바로 MoveState로 빠져나가는 상황을 방지하기 위해서다. 3-3. Interact Interact 역시 실제 상호작용 시간과 애니메이션 시간을 분리했다. 3.0f 는 플레이어가 실제로 상호작용 상태를 유지하는 시간이다. 따라서 그대로 유지했다. if (!isInteractEnd && interactTimer >= 3.0f) { isInteractEnd = true; stateMachine.PlayerAnimation.SetInteractEnd(); } 기존에는 End 애니메이션 길이인 0.958f 까지 State에서 다시 계산했다. 현재는 해당 타이머를 제거했다. End 애니메이션의 AnimationEnd Event를 사용한다. public override void OnAnimationEnd() { if (!isInteractEnd) return; stateMachine.ChangeState(PlayerStateType.Move); } Interact 애니메이션은 Start와 Loop와 End로 나뉘어 있다. 실제 상호작용 전체가 종료되는 시점은 End이다. 그래서 AnimationEnd Event는 End Clip에만 적용했다. 3-4. Dodge Dodge에는 두 종류의 종료 조건이 존재한다. 첫 번째는 회피 중 이동 입력을 통해 조기에 다음 행동으로 넘어가는 경우다. 두 번째는 이동 입력 없이 회피 애니메이션 전체를 재생하는 경우다. 기존에는 두 번째 경우를 위해 DodgeDuration 을 사용했다. 이 값은 회피 애니메이션의 실제 길이를 확인해서 입력한 값이었다. 따라서 DodgeDuration 은 제거했다. 이동 입력을 통한 Cancel은 기존 DodgeMoveCancelTime 을 계속 사용한다. if (moveMagnitude > 0.1f && dodgeTimer >= stateMachine.PlayerMovement.DodgeMoveCancelTime) { if (stateMachine.IsCombat) { stateMachine.ChangeState(PlayerStateType.Move); } else { stateMachine.ChangeState(PlayerStateType.Sprint); } return; } 아무 입력이 없는 경우에는 8방향 Dodge Clip에 추가한 AnimationEnd Event가 MoveState 복귀를 담당한다. public override void OnAnimationEnd() { stateMachine.ChangeState(PlayerStateType.Move); } DodgeMoveCancelTime 은 실제 조작 규칙이라 유지했다. DodgeDuration 은 애니메이션 길이였기 때문에 제거했다. 같은 Dodge 관련 시간값이라도 의미에 따라 다르게 처리했다. 4. 기본 공격 콤보 구조 개선 4-1. 한 번 클릭과 Hold 공격을 동시에 지원 처음에는 공격 입력에 WasPressedThisFrame() 만 사용했다. public bool IsAttack => attackAction.WasPressedThisFrame(); 이 방식은 클릭할 때마다 한 번씩 공격하는 구조에는 적합했다. 하지만 마우스를 계속 누르고 있을 때도 기본 공격 콤보가 이어지도록 만들고 싶었다. 처음에는 IsAttack 자체를 IsPressed() 로 변경했다. 하지만 테스트해보니 한 번 짧게 클릭했을 때도 B 공격까지 이어지는 문제가 발생했다. 첫 공격을 시작하기 위해 누른 입력이 AttackState에 진입한 뒤에도 유지되고 있었다. 그 입력이 다음 콤보 입력으로 다시 사용되고 있었다. 그래서 클릭 입력과 Hold 입력을 분리했다. public bool IsAttack => attackAction.WasPressedThisFrame(); public bool IsAttackHeld => attackAction.IsPressed(); 현재 동작은 다음과 같다. 한 번 클릭 → A 공격만 실행 다시 클릭 → 다음 콤보 예약 계속 누르기 → A → B → C → A → B → C 반복 같은 마우스 버튼을 사용하더라도 클릭 순간과 입력 유지 상태는 다른 의미를 가진다. 그래서 코드에서도 두 입력을 따로 제공하도록 정리했다. 4-2. 트러블슈팅 - 마지막 콤보 이후 Idle이 끼는 문제 처음에는 기본 공격을 A부터 D까지 4단계로 구성했다. Hold 공격을 구현한 뒤 마지막 공격에서 첫 번째 공격으로 다시 돌아갈 때 잠깐 서 있는 자세가 보였다. 처음에는 Animator Transition을 의심했다. 마지막 공격에서 첫 공격으로 직접 Transition을 추가했다. Has Exit Time 도 확인했다. 마지막 공격에서 Idle로 빠지는 Transition의 Exit Time 도 확인했다. 하지만 테스트를 계속하면서 Animator 설정만의 문제는 아니라는 것을 확인했다. 당시 콤보 전환 조건은 현재 comboIndex 가 마지막 Index보다 작을 때만 다음 공격을 실행하는 구조였다. 이 조건에서는 마지막 공격이 시작된 뒤 Update() 에서 첫 공격으로 다시 넘어갈 수 없었다. 결국 마지막 애니메이션의 AnimationEnd 까지 기다린 뒤 첫 공격을 다시 시작하고 있었다. 그래서 앞선 공격들과 연결 타이밍이 다르게 느껴졌다. 해결 - 마지막 콤보에서도 같은 ComboInterval 사용 마지막 공격에서도 동일한 ComboInterval 을 기준으로 다음 공격을 실행할 수 있도록 변경했다. 마지막 인덱스에서는 다시 0으로 돌아간다. if ((nextAttack || stateMachine.PlayerInput.IsAttackHeld) && attackTimer >= stateMachine.PlayerAttack.ComboInterval) { if (comboIndex < 2) { comboIndex++; } else { comboIndex = 0; } StartAttack(); return; } 최종적으로는 현재 전투 템포에 더 잘 맞는 A부터 C까지의 3타 구조를 사용하기로 했다. A → B → C → A comboInterval = 0.5f 는 현재 플레이 테스트에서 콤보 연결 템포를 조절하는 값으로 사용하고 있다. 애니메이션 전체 길이를 의미하는 값은 아니다. 그래서 Gameplay 튜닝값으로 유지했다. 5. PlayerAnimation 책임 정리 5-1. Combat 상태 관리 위치 변경 기존에는 PlayerAnimation 내부에서 Combat 상태와 Combat 종료 타이머까지 관리하고 있었다. 하지만 Combat 상태는 단순한 시각 효과가 아니다. 플레이어 이동 애니메이션의 기준에도 사용된다. 무기 표시 여부에도 영향을 준다. 실제 Gameplay 상태에 가까운 정보라고 판단했다. 그래서 Combat 여부와 Combat 종료 대기 타이머를 PlayerStateMachine 으로 이동했다. 현재 PlayerStateMachine 이 다음 값을 관리한다. private float cIdleTimer; private bool isCIdleTimer; private bool isCombat; public bool IsCombat => isCombat; Combat 진입과 종료 역시 StateMachine이 결정한다. public void EnterCombat() { isCombat = true; cIdleTimer = 0.0f; isCIdleTimer = false; playerAnimation.SetCombat(true); } public void ExitCombat() { isCombat = false; cIdleTimer = 0.0f; is
OpenAI가 DevDay에서 20개에 달하는 대규모 업데이트를 발표하며 단순 챗봇을 넘어선 전방위적 AI 에이전트 생태계로의 전환을 선언했습니다. 기존 단발성 프롬프트 질의응답 구조를 벗어나, 자율 실행 에이전트 Dot , 지식 협업 공간 Space , 비용 효율적인 GPT-6.1 Sol 모델, 그리고 클라우드 기반 Codex 개발 환경을 통해 일상 작업과 소프트웨어 엔지니어링 전 과정을 백그라운드에서 직접 수행하는 환경이 구축되었습니다. 20+ Things The NEW ChatGPT Can Do (Every Feature Explained) 개인 가상 비서 'Dot'과 협업 생태계 연동 새롭게 도입된 에이전트 플랫폼 Dot 은 단순한 챗봇 대화창이 아니라 자체 가상 머신(VM)과 클라우드 브라우저 환경을 보유한 개인용 AI 에이전트입니다. 사용자가 'Bluey'처럼 고유 이름을 부여해 생성할 수 있으며, 클라우드 환경에서 24시간 독립적으로 실행되므로 컴퓨터를 켜두지 않아도 백그라운드에서 지시받은 업무를 처리합니다. 이메일, 캘린더, 슬랙과 같은 외부 생산성 도구의 플러그인과 결합하여 이메일 수신함을 확인하고 문서 초안을 작성한 뒤 팀원에게 전달하는 일련의 워크플로우를 자율 수행합니다. Dot의 주요 소통 채널은 데스크톱, iOS, 웹 앱 전반에 걸쳐 있습니다. 모바일 환경에서는 음성 통화(Call) 기능을 통해 장시간 실시간 대화로 지시를 내리거나 진행 상황을 보고받을 수 있으며, 곧 iMessage 및 RCS 연동도 지원될 예정입니다. 슬랙과의 통합은 개인 DM뿐만 아니라 팀 채널(@멘션) 추가 형태를 지원하므로, 채널 내 여러 작업자가 Dot에게 프로젝트 요구사항 정리, 일정 브리핑, 우선순위 조율을 동시 요청하고 결과를 공유받는 구조가 가능합니다. 에이전트가 브라우저와 클라우드 컴퓨터를 내장하고 있다는 점은 실무 작업 방식에 큰 변화를 줍니다. 웹 폼 자동 입력, 빌드된 웹사이트의 동작 검증, 스크린 캡처 및 시각적 요소 분석 등 기존에 사용자가 직접 브라우저를 띄워 확인해야 했던 작업들을 에이전트가 자체 OS 내부 브라우저에서 수행합니다. 사용자는 필요에 따라 에이전트 화면의 '제어권 회수(Take over)' 버튼을 눌러 수동 조작으로 전환하거나 진행 상황을 실시간 모니터링할 수 있습니다. 구분 주요 기능 지원 채널 / 환경 Dot 비서 이메일·캘린더 제어, 슬랙 메시지 발송, 클라우드 브라우징 iOS/Desktop 앱, 음성 통화, 슬랙 채널, iMessage(예정) 자체 가상 환경 클라우드 PC 내 브라우저 구동, 화면 캡처, 양방향 제어 권한 전환 OpenAI 호스팅 가상 데스크톱 GPT-6.1 Sol 모델과 Pro 500 플랜의 초고속 모드 새로운 기본 작업 모델로 GPT-6.1 Sol 이 투입되었습니다. GPT-6 Astra 수준의 높은 성능을 유지하면서도 토큰 단가를 대폭 절감한 것이 특징입니다. 100만 토큰(입력+출력) 기준 비용은 GPT-6 Astra가 총 60달러(입력 10달러, 출력 50달러)인 반면, GPT-6.1 Sol은 총 12달러(입력 2달러, 출력 10달러)로 약 5배 저렴합니다. 코딩 및 컴퓨터 제어(Computer Use)처럼 반복적이고 토큰 소비량이 큰 실무 작업을 비용 부담 없이 처리할 수 있는 실무형 워크호스(Workhorse) 모델로 설계되었습니다. 속도 측면에서는 기존 Fast 모드(1.5배 속도) 외에 출력 속도를 최대 8배까지 끌어올린 Ultrafast Mode 가 도입되었습니다. 이 모드는 Blender 3D 그래픽 생성, 비디오 게임 로직 빌드, 대용량 스프레드시트 처리처럼 실시간 렌더링에 준하는 시각적 결과물 생성이 필요할 때 지연 시간을 획기적으로 줄여줍니다. 다만 일반 모드 대비 토큰 사용량이 6배 빠르게 차감되는 리소스 집약적 기능입니다. OpenAI는 이 초고속 환경을 지원하기 위해 월 500달러 요금제인 Pro 500 플랜을 신설했습니다. 일반 구독형 계정에서는 사용이 제한되며, 오직 Pro 500 가입자에게만 Codex 및 ChatGPT 내부에서 Ultrafast 토글스위치가 활성화됩니다. 대규모 코드 생성 및 상시 에이전트 운영이 필수적인 기업이나 전문 빌더를 겨냥한 가격 정책입니다. 협업 워크스페이스 'Space'와 에이전트 네이티브 앱 노션 형태의 협업 문서 도구인 Space(ChatGPT Space) 가 플랫폼 내에 통합되었습니다. Space 내부의 Pages 기능을 사용하면 단순 텍스트 출력을 넘어 헤딩, 코드 블록, 코드 Diff, Mermaid 다이어그램, 접기 섹션, 인터랙티브 파이차트 등을 포함한 고도화된 문서를 구축할 수 있습니다. 특히 클릭 한 번으로 새 대화 세션에 컨텍스트로 전달되는 '재사용 가능 프롬프트 블록(Reusable Prompts)'을 문서 내에 직접 삽입할 수 있어 지식 베이스 구축에 최적화되어 있습니다. 문서 작성 도중에 사이드바의 에이전트에게 수정을 지시하면, 에이전트가 즉각 Pages 구조를 인식하고 특정 위치에 요약 표를 삽입하거나 텍스트 포맷을 인라인으로 재구성합니다. 작성된 페이지는 읽기/편집 권한을 부여해 공개 링크나 이메일로 팀원에게 즉시 공유할 수 있습니다. 또한 Google Drive 연동을 지원하여 클라우드 스토리지 내 에셋을 Space 안에서 직접 탐색하고 에이전트 작업 맥락으로 투입할 수 있습니다. 기존 단순 API 호출에 머물렀던 플러그인은 플러그인 익스텐션(Plugin Extensions) 을 통해 ChatGPT 앱 내부에서 직접 렌더링되는 에이전트 네이티브 앱으로 진화했습니다. 예컨대 'MagicPath'나 'Figma' 같은 서드파티 툴이 사이드 패널에 완전한 UI를 갖춘 독립 애플리케이션 형태로 내장됩니다. 여기에 새롭게 공개된 Sign In with ChatGPT 를 적용하면, 빌더는 별도의 에이전트 인프라를 구축할 필요 없이 사용자의 ChatGPT 계정 크레딧(월 200달러 Pro 플랜 등)을 그대로 소비시켜 구동하는 서드파티 AI 서비스를 손쉽게 만들 수 있습니다. Codex Cloud, Decisions API, Agents API 개발자를 위한 Codex 환경은 로컬 머신 의존성을 탈피했습니다. Codex Cloud 기능의 출시로 인해 사용자는 맥미니 등의 로컬 기기를 24시간 켜둘 필요 없이, Clerk, Convex, Vercel, GitHub 계정이 사전 구성된 클라우드 샌드박스 환경을 인스턴스로 생성해 코딩 작업을 위임할 수 있습니다. 이동 중에도 iOS 모바일 앱을 통해 프로젝트를 선택하고 원격으로 빌드 및 디버깅 작업을 실행할 수 있습니다. 터미널 환경에서는 Codex CLI 를 통해 양방향 음성 입력, /agents 명령을 통한 에이전트 뷰 확인, 개선된 프롬프트 편집 기능을 활용할 수 있습니다. 코드 리뷰 파이프라인 역시 Codex 내부에 공식 탭으로 통합되었습니다. GitHub PR(Pull Request)이 생성되면 사이드 패널의 Code Review 탭에서 변경 내역을 확인하고, Codex에게 검토를 요청해 잠재적 결함과 구조적 문제를 짚어낼 수 있습니다. 이에 더해 보안 취약점을 탐지하고 배포 안정성을 평가하는 Codex Security Dashboard 가 추가되어 바이브 코딩으로 빠르게 프로토타입을 만드는 과정에서 발생하는 보안 구멍을 메워줍니다. API 영역에서는 두 가지 핵심 도구가 공개되었습니다. 첫째는 단순 분류 및 라우팅 작업에 특화된 Decisions API 입니다. 텍스트를 자유 생성하는 기존 LLM과 달리, 들어온 입력에 대해 사전 정의된 옵션(예: 영업팀, 정산팀, 기술지원팀) 중 최적의 경로를 출력 토큰 생성 없이 극도로 빠르고 정밀하게 선택합니다. 둘째는 Agents API 및 클라우드 컴퓨터 제어( Cloud Computer Use ) 기능으로, 개발자는 단일 API 키와 플랫폼 프로젝트 설정을 통해 OpenAI가 호스팅하는 가상 브라우저 및 OS를 활용하는 Codex 기반 완전 자율 에이전트 앱을 직접 개발할 수 있습니다. 정리 이번 업데이트의 핵심은 AI를 단순한 질의응답 도구에서 백그라운드에서 상시 작동하는 자율형 에이전트 생태계 로 진화시킨 점입니다. 사용자 관점에서는 가상 환경을 갖춘 비서 Dot과 협업 공간 Space를 통해 업무 자동화를 일상화할 수 있게 되었으며, 개발자 관점에서는 GPT-6.1 Sol의 압도적인 가성비, 8배 빠른 Ultrafast 모드, 클라우드 샌드박스 기반의 Codex Cloud, 그리고 라우팅 특화 Decisions API 및 Agents API를 활용해 에이전트 네이티브 애플리케이션을 손쉽게 구축할 수 있는 발판이 마련되었습니다. 실무에 바로 적용해보기 위해 다음 순서로 시작하는 것을 추천합니다. ChatGPT 데스크톱 앱에서 Dot 설정 : 왼쪽 상단 홈 아이콘을 눌러 나만의 비서(예: Bluey)를 생성하고 구글 계정(이메일·캘린더) 및 슬랙 워크스페이스를 연동하여 일일 업무 브리핑을 위임합니다. Space에서 Pages 문서 생성 실습 : 좌측 패널의 Space로 이동해 새 페이지를 열고, 우측 하단 에이전트 창에 "주요 아키텍처 다이어그램(Mermaid)과 복사 가능한 프롬프트 블록을 포함한 기획서를 작성하라"고 지시해 봅니다. Decisions API 및 Agents API 문서 검토 : 라우팅 로직(고객 문의 분류, 이메일 태깅 등)을 일반 챗 완성형 API 대신 Decisions API로 대체할 수 있는지 공식 문서를 통해 가격 및 속도 구조를 점검합니다. 다룬 영상 20+ Things The NEW ChatGPT Can Do (Every Feature Explained) — Riley Brown · 31분
뷰어 중심의 데이터 프로덕트 설계법 단순히 지표를 나열하는 리포트를 넘어, 액션을 이끌어내는 도구로서의 대시보드 설계 기법 한 줄 요약 대시보드가 폐허가 되는 이유는 정보가 부족해서가 아니라 '누가 무엇을 해야 할지' 보여주지 않기 때문이다. 단순 보고용(Report) 대시보드를 벗어나, 출근 직후 실무자의 액션을 유도하는 '뷰어 중심의 데이터 프로덕트(Dashboard as a Tool)' 로 전환해야 한다. i️ 이 글은 특정 서비스의 데이터에 국한되지 않고, 매출·KPI 및 지역별(서울·인천 등) 데일리 KPI 대시보드 운영 경험을 일반화한 데이터 프로덕트 설계 방법론 입니다. 들어가며 "우리 팀도 주요 지표를 한눈에 볼 수 있는 KPI 대시보드를 만듭시다!" 데이터 기반 의사결정을 강조하는 수많은 조직에서 야심 차게 대시보드를 구축합니다. 매출 데이터, 일간/주간 유저 수, 지역별 실적 그래프까지 빽빽하게 담긴 화려한 화면이 완성되지만, 한 달만 지나면 놀랍게도 아무도 접속하지 않는 '데이터의 폐허' 가 되곤 합니다. 왜 이런 일이 반복될까요? 대부분의 대시보드가 지표를 예쁘게 나열하는 '보고서(Report)' 역할에 머물러 있기 때문입니다. 실무자는 대시보드를 보고 "그래서 내가 지금 뭘 해야 하지?"라는 질문에 답을 얻지 못하면 결국 기존처럼 엑셀을 열거나 데이터 담당자에게 쿼리 추출을 요청하게 됩니다. 본 글에서는 지역별 KPI 데일리 대시보드 구축 경험을 바탕으로, 대시보드를 단순 모니터링판이 아닌 실무자의 행동을 이끌어내는 데이터 프로덕트(Data Product) 로 바꾸는 정보 계층(Hierarchy) 및 레이아웃 설계법을 공유합니다. 1. 대시보드가 '폐허'가 되는 진짜 이유 많은 조직에서 대시보드 활용도가 떨어지면 "원하는 지표가 없어서"라고 착각하고 차트와 필터를 계속 추가합니다. 그러나 이는 상황을 악화시킬 뿐입니다. 데이터 보고서 (Dashboard as a Report) 데이터 프로덕트 (Dashboard as a Tool) 지표의 수동적 나열 (Fact 중심) 액션 중심 정보 제공 (Action 중심) "무슨 일이 일어났는가?"만 보여줌 "누가, 다음 주자로 무엇을 해야 하는가?"를 제시 모든 사용자에게 동일한 수십 개의 차트 노출 뷰어의 역할에 맞춘 정보 계층화(Hierarchy) 지표 하락 시 원인 파악을 위해 또 다시 엑셀 분석 필요 지표 하락 시 하위 원인으로 바로 드릴다운(Drill-down) 가능 ⚠️ 데이터가 많을수록 의사결정이 쉬워지는 것이 아닙니다. '인지 과부하(Cognitive Overload)' 만 불러일으킬 뿐입니다. 훌륭한 대시보드는 정보를 많이 담는 것이 아니라, 실무자의 고민을 줄여주는 대시보드입니다. 2. 핵심 설계 철학: "지표가 떨어졌을 때 다음 주자는 누구인가?" 뷰어 중심 대시보드를 설계할 때 가장 먼저 던져야 할 질문은 "어떤 지표를 볼 것인가?"가 아닙니다. "이 지표가 요동칠 때, 출근해서 가장 먼저 움직여야 하는 담당자(다음 주자)는 누구인가?" 입니다. 지역별 KPI 데일리 대시보드를 예로 들어보겠습니다. 매출 KPI가 전일 대비 15% 하락했다면? 다음 주자: 마케팅/운영 담당자 필요한 액션: 공급 부족 문제인지, 프로모션 종료로 인한 수요 감소인지 파악 후 현장 조치 대시보드의 정보 배치 기준: 단순히 순서로 나열하는 것이 아니라, '담당자별 담당 영역 -> 이상 징후 감지 -> 즉시 조치 항목' 순서로 화면의 정보 흐름이 이어져야 합니다. 3. 액션을 유도하는 정보 계층(Hierarchy)과 레이아웃 설계 실무자가 출근해서 대시보드를 열었을 때, 단 5초 만에 상황을 파악하고 액션을 취할 수 있도록 정보 계층을 3단계로 구조화해야 합니다. ┌─────────────────────────────────────────────────────────┐ │ [Top Level] Health Check (경고 및 핵심 KPI 요약) │ └────────────────────────────┬────────────────────────────┘ │ (이상 징후 포착 시) ▼ ┌─────────────────────────────────────────────────────────┐ │ [Mid Level] Breakdown & Context (원인별 세부 분석) │ └────────────────────────────┬────────────────────────────┘ │ (원인 규명 시) ▼ ┌─────────────────────────────────────────────────────────┐ │ [Bottom Level] Action Trigger (액션 가이드) │ └─────────────────────────────────────────────────────────┘ 3단계 정보 계층 상세 Top Level: 출근 직후 3초 Health Check 전체 매출, 주요 거점의 일간 목표 달성률(%) 정상/경고 상태의 시각적 구분: 단순 숫자가 아닌, 둔화/이상치 발생 시 컬러 코딩(Color Coding)으로 '오늘 주의해야 할 지역'을 즉시 식별하도록 유도 Mid Level: 원인 추적을 위한 Drill-down (Breakdown) "매출이 왜 떨어졌지?"에 대한 즉각적인 답 제시 전환율 문제인가? 트래픽 문제인가? 특정 카테고리의 문제인가? 차트를 더 깊게 클릭하지 않아도 하락 요인의 기여도 를 시각적으로 분해하여 표시 Bottom Level: 다음 주자를 위한 Action Linkage 지표 하락이 확인된 영역 옆에 담당 부서/담당자 및 가이드 Action 표시 4. [사례] KPI 데일리 대시보드 개선 비포&애프터 매출 및 지역별 운영 지표를 다루었던 실제 경험을 일반화하여 대시보드 전환 전후의 차이를 비교해 봅니다. 구분 Before (Dashboard as a Report) After (Dashboard as a Data Product) 화면 구성 전체 20여 개 차트가 한 페이지에 나열됨 '오늘 액션이 필요한 지역' 만 최상단 카드 형태로 하이라이트 지표 표현 "매출 5,000만 원 (전일 대비 -10%)" "매출 달성률 85% ⚠️ (원인: B구역 공급 20% 감소) " 실무자 반응 "그래프는 많은데 결국 원인 찾으려고 DB 쿼리 다시 돌림" "출근 후 1분 만에 오늘 B구역 마케팅 집행 여부 결정" 사용 주기 월간/주간 보고서 작성 때만 임직원이 캡처용으로 접속 실무자가 매일 아침 출근 루틴 으로 가장 먼저 로그인 💡 핵심 가치 뷰어 중심 데이터 프로덕트는 실무자에게 "공부할 거리"를 주는 것이 아니라, "지금 당장 실행해야 할 다음 단계" 를 제안합니다. 5. 지속 가능한 데이터 프로덕트를 만들기 위한 체크리스트 대시보드를 신규 제작하거나 리뉴얼할 때 다음 질문을 반드시 던져보세요. 이 대시보드의 주요 뷰어(User Target) 가 명확히 정의되어 있는가? (C-Level용과 실무자용이 섞여 있지 않은가?) 특정 지표가 붉은색(경고)으로 변했을 때, '누가' 움직여야 하는지 지정되어 있는가? 지표 하락의 원인을 찾기 위해 최대 2번의 클릭(Drill-down) 내에서 원인 파악이 가능한가? 수십 개의 차트 중 가장 중요한 3가지 핵심 지표 가 최상단에 강조되어 있는가? 이 대시보드가 실무자의 '데일리 출근 루틴' 에 자연스럽게 녹아들었는가? 마무리 대시보드 구축의 성공 여부는 "얼마나 많은 차트를 구현했는가"나 "얼마나 기술적으로 화려한가"로 결정되지 않습니다. "이 대시보드가 조직의 의사결정과 액션 속도를 얼마나 가속화했는가" 가 유일한 척도입니다. 지표를 단순히 수집하여 보여주는 보고서(Report) 단계에서 벗어나, 뷰어의 역할과 액션에 맞춘 데이터 프로덕트(Tool)로 진화시킬 때 비로소 대시보드는 '폐허'가 아닌 '조직의 정밀한 조종석'이 될 것입니다.
"더 빠르게"가 아니라 "더 정확한 컨텍스트"를 주는 법 AI 보고자료 제작 Agent와 엑셀 검수 자동화 사례로 보는 실무 AX(AI eXperience) 설계 전략 한 줄 요약 AI 에이전트 도입의 핵심은 "모든 것을 알아서 자동화하는 것"이 아니라, 사람이 검수하기 가장 좋은 형태의 '정확한 컨텍스트' 를 전달하여 페어 워커(Pair Worker) 로 만드는 데이터 전처리 및 비즈니스 규칙 모듈화에 있다. i️ 이 글은 AI 보고자료 제작 Agent 및 엑셀 데이터 검수 자동화 프로젝트 경험을 바탕으로, 실무 현장에서 LLM 기반 AI 에이전트를 성공적으로 안착시키기 위한 입력 구조화 및 프롬프트/규칙 설계 방법론을 정리한 글입니다. 들어가며 "우리 팀도 AI 에이전트를 도입해서 주간 보고서 작성이랑 엑셀 검수 업무를 100% 자동화합시다!" 최근 수많은 조직에서 업무 효율화와 AX(AI Transformation)를 목표로 LLM 기반의 AI 에이전트를 도입하고 있습니다. 특히 매주 반복되는 엑셀 수치 검수나 데일리/주간 보고서 제작 같은 리소스 소모성 업무는 AI 에이전트 적용 1순위 타깃으로 꼽힙니다. 하지만 야심 차게 에이전트를 구축한 뒤 현장에서 들려오는 실제 반응은 기대와 전혀 다릅니다. *"AI가 만들어준 보고서를 검수하고 오류 찾는 데 걸리는 시간이, 차라리 내가 처음부터 엑셀 열고 만드는 시간보다 더 오래 걸려요."* *"엑셀 데이터를 그대로 집어넣었더니 엉뚱한 열의 수치를 인용하거나 없는 사실을 만들어내는 환각(Hallucination) 현상이 너무 심합니다."* 왜 이런 일이 반복될까요? 대부분의 조직이 LLM이나 AI 에이전트를 실무 자동화에 붙일 때 "알아서 다 해주겠지" 라는 막연한 기대를 하기 때문입니다. AI를 단순한 만능 봇으로 접근하는 순간, 생성된 결과물은 '쓸모없는 데이터의 쓰레기통'이 됩니다. 본 글에서는 AI 에이전트 도입 시 빠지기 쉬운 함정을 진단하고, AI가 쓸모없는 결과물을 내뱉지 않도록 하는 입력 데이터 전처리 구조 설계, 프롬프트 엔지니어링보다 중요한 '비즈니스 규칙의 모듈화', 그리고 AI를 '페어 워커(Pair Worker)'로 포지셔닝하는 노하우 를 공유합니다. 1. AI 에이전트 도입 시 흔히 하는 착각: "알아서 다 해주겠지" 많은 팀이 AI 에이전트를 구축할 때 '얼마나 빠르고 자동으로 결과를 내는가' 에 집중합니다. 그러나 프롬프트에 아무리 "정확하게 분석해 줘", "오류 없이 검수해 줘"라고 써붙여도, 입력되는 지식과 데이터의 맥락(Context)이 왜곡되어 있다면 AI는 단지 '매우 빠르고 당당하게 거짓말을 하는 도구'가 될 뿐입니다. 구분 단순 자동화 착각 (All-in-One Bot) 맥락 중심 AI 에이전트 (Pair Worker) 기대 역할 날것의 입력만 넣으면 완제품 보고서를 알아서 생성 사람이 검수하기 가장 좋은 형태의 초안/요약 가공 핵심 입력 복잡한 병합 셀이 섞인 엑셀 시트, 비구조화 텍스트 전처리된 모듈형 데이터 + 명확한 비즈니스 검수 규칙 품질 결정 요인 프롬프트의 미사여구 및 길고 복잡한 지시문 입력 데이터의 전처리 구조 및 컨텍스트 파이프라인 오류 발생 시 AI의 환각을 사람이 일일이 데이터와 대조하며 찾아내야 함 출처와 검수 포인트가 명확히 분리되어 신속 수정 가능 ⚠️ Garbage In, Garbage Out (GIGO) 프롬프트 엔지니어링의 화려한 수식어보다 중요한 것은 "AI에게 어떤 가공된 데이터를 전달하는가" 입니다. AI에게 입력되는 컨텍스트의 밀도와 구조가 결과물의 품질을 90% 이상 결정합니다. 2. AI가 쓸모없는 결과물을 내뱉지 않게 하는 데이터 전처리 구조 설계 AI 보고자료 제작 에이전트나 엑셀 검수 자동화 도구를 만들 때, AI에게 엑셀 파일(.xlsx)을 그대로 던져주는 것은 가장 안 좋은 방식입니다. 엑셀의 병합된 셀, 다중 헤더, 서식 위주의 표현 등은 LLM이 문맥을 오해하기 딱 좋은 구조이기 때문입니다. 엑셀 검수 및 보고자료 Agent를 위한 3단계 전처리 파이프라인 시각적 요소 탈피 및 데이터 평탄화 (Flattening) 병합된 셀을 모두 해제하고, 모든 행(Row)에 결측 없이 기준 키(Key) 값을 채워 넣습니다. 다중 헤더 구조를 하나의 명확한 컬럼명으로 재정의합니다. 비즈니스 컨텍스트 메타데이터 결합 숫자 데이터만 전달하지 않고, 해당 수치가 의미하는 가이드를 메타데이터로 함께 넘깁니다. 예시: {"col": "영업이익률", "unit": "%", "threshold_warning": -5.0, "description": "전월 대비 5%p 이상 하락 시 경고"} LLM 친화적 포맷(JSON / Markdown Table) 전환 대용량 엑셀 전체를 텍스트로 전달하면 토큰 소모가 크고 환각이 발생합니다. 검수가 필요한 이상치(Anomaly) 영역이나 핵심 요약 대상 행만 필터링하여 구조화된 JSON 또는 마크다운 표로 변환한 뒤 에이전트에게 전달합니다. [Raw 엑셀 파일] ──> [셀 병합 해제 및 데이터 평탄화] ──> [메타데이터 및 검수 규칙 결합] ──> [JSON/MD 변환 컨텍스트] ──> [LLM 에이전트] 3. 프롬프트 엔지니어링보다 중요한 '비즈니스 규칙의 모듈화' AI 에이전트에 모든 판단 로직을 프롬프트 하나로 밀어 넣으려고 하지 마세요. "엑셀에서 오류를 찾아서 보고서를 써줘"라는 프롬프트는 실패하기 쉽습니다. '검증(Validation)' 로직과 '생성(Generation)' 로직을 철저히 분리해야 합니다. 비즈니스 규칙 모듈화 (Modular Business Rules) 패턴 ┌─────────────────────────────────────────────────────────┐ │ [Layer 1] 코드 기반 규칙 엔진 (Rule Engine) │ │ - 엑셀 수치 합계 검증, 서식 오류 검출, 이상치 판별 │ └────────────────────────────┬────────────────────────────┘ │ (검증 결과 및 이상치 가공) ▼ ┌─────────────────────────────────────────────────────────┐ │ [Layer 2] AI 에이전트 (LLM Context Processor) │ │ - 검증된 결과를 바탕으로 원인 요약 및 보고서 초안 작성 │ └────────────────────────────┬────────────────────────────┘ │ (초안 전달) ▼ ┌─────────────────────────────────────────────────────────┐ │ [Layer 3] 읽기 좋은 검수 표 레이아웃 (Human Review) │ └─────────────────────────────────────────────────────────┘ 산술적 계산 및 1차 검수: Python/SQL 등 코드 기반의 규칙 엔진에 맡깁니다. (LLM에게 계산을 시키지 마세요!) LLM의 역할: 규칙 엔진이 찾아낸 '오류 항목'과 '변동 원인 데이터'를 컨텍스트로 받아서, "사람이 이해하기 쉬운 자연어 문장으로 요약하고 보고서 형태로 가공" 하는 역할에 집중시킵니다. 4. AI의 올바른 포지셔닝: 만능 봇이 아닌 '페어 워커(Pair Worker)' AI 에이전트를 조직에 안착시키려면 AI에 대한 기대치(Expectation)를 재설정해야 합니다. AI는 사람을 완전 대체하는 자율주행 로봇이 아니라, '초안을 가장 빠르게 가공해 주는 든든한 동료(Pair Worker)' 입니다. 실무자를 위한 페어 워킹(Pair Working) 노하우 검수용 하이라이트 제공 (Traceability) AI 보고서가 인용한 수치가 원본 엑셀의 어떤 셀(Cell)에서 왔는지 출처 표기(Grounding)를 명확히 남깁니다. 예시: "올해 3분기 매출은 120억 원으로 전년 대비 15% 증가했습니다. 확인할 수 있는 질문 템플릿 제안 AI가 보고서를 만들 때 스스로 확신이 떨어지는 구간에는 [❓ 검수 필요: 해당 프로모션 비용 집행 내역 재확인 권장] 과 같은 플래그를 달아두도록 설계합니다. 최종 승인 권한은 사람에게 (Human-in-the-Loop) AI 에이전트의 출력물은 항상 '최종본'이 아닌 '검수 준비 완료된 초안(Draft)' 상태로 제공되며, 실무자의 원클릭 승인을 거쳐 배포되도록 프로세스를 구축합니다. 5. AX 실무 적용 체크리스트 AI 에이전트 기반 보고자료 제작 및 엑셀 검수 자동화를 구축할 때 다음 항목을 체크해 보세요. AI에게 전달되는 입력 데이터가 병합 해제 및 평탄화 전처리 를 거쳤는가? 단순 산술 계산과 수치 검증을 LLM이 아닌 코드/규칙 엔진 이 처리하고 있는가? AI가 생성한 보고서 문장에 원본 데이터의 출처(Cell/Row)가 명시 되어 있는가? AI의 역할이 '완전 자동화 도구'가 아닌 '검수하기 쉬운 초안 작성자(Pair Worker)' 로 포지셔닝되어 있는가? 데이터 이상 징후 발생 시 사람이 1분 이내에 오류를 찾고 수정 할 수 있는 검수 인터페이스가 제공되는가? 마무리 AI 에이전트 도입의 참된 가치는 "사람을 없애고 100% 자동화를 이루는 것"에 있지 않습니다. "사람이 반복적인 엑셀 검수와 단순 수치 작성에 쓰던 4시간을, 단 10분 만에 초안 검수로 끝내고 더 본질적인 전략 의사결정에 집중하게 만드는 것" 에 있습니다. AI에게 화려한 프롬프트를 던지기 전에, AI가 먹고 판단할 '데이터 컨텍스트' 를 얼마나 깨끗하고 명확하게 가공해 주고 있는지 돌아보세요. 정확한 컨텍스트와 모듈화된 비즈니스 규칙이 준비될 때, AI 에이전트는 비로소 조직의 가장 유능한 페어 워커(Pair Worker)로 거듭날 것입니다.
기획 피벗: "인앱 결제(PG), 꼭 지금 붙여야 할까?" 앱 기획 초기에는 당연히 앱 내에서 회원들이 신용카드로 PT 수강권을 직접 결제하는 그림을 그렸습니다. 하지만 PG(결제 대행사) 연동을 알아보며 현실적인 벽에 부딪혔습니다. 높은 수수료와 복잡한 심사 : 앱 내 결제를 붙이려면 사업자 등록, 통신판매업 신고는 기본이고, 구글/애플의 무자비한 인앱 결제 수수려(최대 30%) 정책까지 얽혀 있었습니다. PT샵의 실제 비즈니스 환경 : 현장을 분석해 보니 회원들은 대부분 샵에 방문해서 현장 단말기(카드)로 긁거나 계좌 이체 로 큰 금액을 결제하고 있었습니다. 굳이 수수료를 떼며 앱에서 결제할 이유가 없었던 것입니다. 그래서 과감하게 기획을 피벗했습니다. " 복잡한 외부 PG 연동은 걷어내고, 관리자가 현장 결제를 확인한 뒤 앱에서 '수동으로 수강권을 발급'하는 기능에 집중하자! " 배보다 배꼽이 커지는 것을 막고, MVP(최소 기능 제품)의 핵심에 집중하기 위한 현실적인 선택이었습니다. RDBMS 핵심 스키마 설계 결제 방식이 '관리자 수동 발급'으로 바뀌었어도, 백엔드에서 처리해야 할 데이터의 무결성은 똑같이 중요합니다. 시스템의 근간을 위해 MySQL 을 채택하고 핵심 테이블을 설계했습니다. users (사용자): 관리자, 트레이너, 회원 등 권한별 계정 정보 branches (지점): 운영 중인 PT 지점 정보 tickets (수강권): 회원별 수강권 잔여 횟수( remaining_sessions ) payments (매출/결제 내역): 현장 결제 및 수강권 발급에 따른 매출 기록 트랙잭션 설계 관리자가 앱에서 특정 회원에게 '10회권 발급' 버튼을 누르면, Node.js 백엔드에서는 반드시 두 가지 작업이 동시에 일어나야 합니다. tickets 테이블에 회원의 수강권 횟수(10회)를 INSERT payments 테이블에 해당 상품 금액만큼의 매출 기록을 INSERT 둘 중 하나라도 실패하면(수강권만 생기고 매출이 안 잡히거나, 매출만 잡히고 수강권이 안 생기거나) 큰일이 나기 때문에 이 과정은 하나의 트랜잭션(Transaction)으로 묶어주었습니다. // Node.js (Express) - 관리자 수강권 수동 발급 트랜잭션 const connection = await pool.getConnection(); await connection.beginTransaction(); try { // 1. tickets 테이블에 수강권 발급 await connection.query( 'INSERT INTO tickets (user_id, ticket_name, total_count, used_count, remaining_sessions) VALUES (?, ?, ?, 0, ?)', [member_id, product.name, product.total_count, product.total_count] ); // 2. payments 테이블에 매출 기록 await connection.query( 'INSERT INTO payments (branch_id, amount, payment_date) VALUES (?, ?, NOW())', [branch_id, product.price] ); await connection.commit(); // 모두 성공 시 확정 } catch (error) { await connection.rollback(); // 실패 시 원상 복구 throw error; } 트러블슈팅 관리자 대시보드 화면에 '지점별 총매출'을 띄우는 테스트를 하던 중, 치명적인 함정을 발견했습니다. UI 구현에 너무 매몰된 나머지 payments 테이블을 아래처럼 짰던 것이빈다. 초기 payments 테이블: id , branch_id (지점), amount (금액), payment_date (일시) 당장 총매출액을 구하는 데는 문제가 없었지만, 가상의 회원이 "어제 결제한 거 환불할게요"라고 하면? 이라는 생각이 들었습니다. " 이 결제 내역, 관리자가 누구한테 발급해 준 거고, 회원이 현장 카드로 한 건지 계좌이체로 한 건지 어떻게 알지? " 지점별 전체 매출액만 신경 쓰느라 가장 중요한 user_id 와 payment_method 를 테이블에 넣지 않은 것입니다. 돈이 들어오긴 했는데 출처와 수단이 없는, 상용 앱에서는 절대로 있어서는 안 되는 '반쪽짜리' 영수증이었습니다. 해결: 설계 고도화와 롤백 실수를 깨닫자마자 DBeaver를 열어 즉시 테이블 스키마를 뜯어고쳤습니다. ALTER TABLE payments ADD COLUMN user_id INT AFTER id, ADD COLUMN payment_method VARCHAR(50) AFTER amount; 테이블 구조를 바꾼 후, 관리자 발급용 API 쿼리도 전면 수정했습니다. 관리자가 발급 버튼을 누를 때 [어떤 회원에게 / 무슨 지점에서 / 얼마를 / 현장 카드인지 이체인지 / 언제] 발급했는지 꼼꼼하게 기록하는 완전체 쿼리로 고도화했습니다. await connection.query( 'INSERT INTO payments (user_id, branch_id, amount, payment_method, payment_date) VALUES (?, ?, ?, ?, NOW())', [member_id, branch_id, product.price, payment_method] ); 이제 완벽한 매출/발급 추적 데이터를 쌓을 수 있게 되었습니다. 회고 이번 과정을 통해 두 가지 큰 레슨을 얻었습니다. 첫째, 기술적으로 멋져 보이는 기능(PG 연동)이라도 실제 비즈니스 환경(현장 결제 위주)에 맞지 않으면 과감히 덜어내는 것이 맞다. 둘째, UI에 띄울 '결과물'만 생각해서 DB를 설계하면 안 된다. 데이터베이스는 비즈니스의 모든 예외 상황(환불, 영수증, 수단별 통계)을 위한 '추적 가능성'을 완벽히 보장해야 한다. 다음 편 예고 결제 시스템과 DB의 뼈대가 탄탄하게 잡혔으니, 이제 이 데이터를 바탕으로 최고 관리자가 시스템을 어떻게 쥐락펴락하는지 보여드릴 차례입니다. 다음 편에서는 관리자 포털의 세부 기능과 UI/UX 구현 과정 을 다뤄보겠습니다. 지점과 직원을 관리하는 로직, 복잡한 2-Depth 하단/상단 탭 네비게이션 라우팅 설계, 그리고 이 과정에서 마주쳤던 플러터 생명주기(Lifecycle) 에러 해결기까지 낱낱이 파헤쳐 보겠습니다.
감이 아닌 구조로: 수요·수익성 분석을 의사결정 도구(Simulator)로 확장하기 일회성 보고서에서 살아있는 의사결정 시뮬레이터로 나아가는 법 한 줄 요약 사업 계획이나 자원 배분 때 "느낌"이나 "과거 실적"에 의존하면 논쟁만 길어진다. 수요와 수익성 데이터를 결합하여 조건을 바꾸어가며 선택지를 검토할 수 있는 의사결정 도구(Simulator) 로 분석을 확장해야 한다. i️ 이 글은 특정 회사나 서비스와 무관한 일반 방법론 입니다. 예시는 모두 가상 이고, 숫자는 설명을 위한 가정 값입니다. 들어가며 "어떤 지역에 자원을 먼저 집중해야 할까요?" 이 질문에 대해 "작년에 이 지역이 잘됐으니 올해도 거깁니다"라거나 "감으로 볼 때 여기가 유망합니다"라는 답이 돌아오면 실무는 멈춥니다. 분석가가 수많은 데이터를 모아 멋진 리포트를 만들어도, 의사결정 현장에서 결국 "그래서 지금 조건을 바꾸면 어떻게 되는데?"라는 질문에 막히는 이유가 여기에 있습니다. 분석은 지나간 일을 설명하는 데 그치지 않고, 앞으로의 선택을 시뮬레이션할 수 있는 구조 로 바뀌어야 합니다. 이 글은 일회성 분석 보고서를 실무자가 직접 조건을 만져보는 의사결정 도구(Simulator) 로 확장하는 원리와 단계를 다룹니다. 1. 무엇이 문제인가: 정적인 보고서의 한계 대다수의 분석 프로젝트는 아래와 같은 흐름으로 소비되고 소멸합니다. 기존 분석 흐름 한계점 과거 데이터 수집 이미 지나간 성과만 나열된다 단일 지표 정렬 매출이나 수요가 높은 곳만 단순 순위화된다 정적 리포트 제출 "조건이 조금 바뀌면 어떻게 되지?"라는 추가 질문에 답하기 어렵다 감에 의한 최종 결정 숫자는 참고용일 뿐, 결국 리더의 직관으로 결정을 내린다 ⚠️ 데이터를 아무리 많이 모아도 조건 변동에 따른 결과 변화 를 즉시 보여주지 못하면, 분석가는 매번 새로운 요청에 맞춰 쿼리를 다시 짜야 하는 병목에 갇히게 됩니다. 2. 도구의 3대 축: 수요, 수익성, 제약 조건 의사결정 도구(Simulator)가 되려면 최소한 세 가지 축이 하나의 공식이나 인터페이스로 묶여야 합니다. 축 의미 핵심 질문 1 수요 (Demand) 얼마나 많은 사용자가 찾는가 "이 지역에 잠재 이용량이나 트래픽이 충분한가?" 2 수익성 (Unit Economics) 자원을 투입했을 때 남는 것은 무엇인가 "비용을 제하고 실제 마진이나 효율이 나오는가?" 3 제약 조건 (Constraints) 현실적인 한계는 무엇인가 "예산, 인력, 물리적 인프라 한도가 얼마인가?" 이 세 축을 단순 합산하는 것이 아니라, 가중치와 상호작용 을 반영한 시뮬레이션 모델로 설계해야 합니다. 3. 시뮬레이터 설계 4단계 1 변수 정의와 가중치 설정 분리하기 수요와 수익성의 중요도는 사업 국면(성장기 vs 효율성 중시)에 따라 달라집니다. 가중치를 코드나 쿼리 안에 하드코딩하지 말고, 사용자가 슬라이더나 입력값으로 조절할 수 있게 분리합니다. 2 '정답'이 아니라 '비교 도구'로 설계하기 시뮬레이터는 "여기에 10개를 놓으세요"라는 정답을 맞히는 오라클이 아닙니다. "A안과 B안을 선택했을 때 각각 어떤 트레이드오프(Trade-off)가 발생하는지" 나란히 비교해 주는 거울이어야 합니다. 3 민감도 분석(Sensitivity Analysis) 내장하기 핵심 가정(예: 단가 10% 변동, 수요 증가율 5% 하락)이 흔들릴 때 전체 순위나 최적 선택지가 얼마나 쉽게 뒤집히는지 시각적으로 보여주어야 합니다. 민감도가 높은 모델일수록 리더십이 신뢰합니다. 4 결과의 가시화 (출력 구조화) 복잡한 수치 테이블 대신, 지도나 매트릭스 형태로 ‘고수익·고수요 거점’, ‘잠재 거점’, ‘제외 대상’ 등으로 직관적으로 군집화(Clustering)하여 보여줍니다. 4. 시뮬레이션 모델 뼈대 예시 (가상) 거점 ID 예상 수요 점수 (0-100) 예상 수익성 점수 (0-100) 가중치 적용 종합 점수 (수요 4 : 수익 6) 최종 권장 등급 H-01 85 90 $85 \times 0.4 + 90 \times 0.6 = 88.0$ A등급 (우선 집중) H-02 95 50 $95 \times 0.4 + 50 \times 0.6 = 68.0$ B등급 (조건부 검토) H-03 40 95 $40 \times 0.4 + 95 \times 0.6 = 73.0$ B등급 (효율성 중심) 💡 참고: 사용자가 가중치 슬라이더를 [수요 7 : 수익 3]으로 바꾸면 H-02와 H-03의 순위가 즉시 역전되는 것을 보여줄 수 있어야 합니다. 5. 의사결정 도구를 도입할 때의 효과와 주의점 구분 내용 기대 효과 • "왜 이 결정을 했는가"에 대한 논리적 근거(Audit Trail) 확보 • 회의 시간 단축 (숫자 검증 대신 대안 시뮬레이션에 집중) • 주관적 의견 대립을 객관적 시뮬레이션 조건 변경으로 전환 주의할 점 • 모델이 너무 복잡해지면 '블랙박스'가 되어 외면받음 • 입력 데이터의 정합성이 깨지면 시뮬레이터 전체가 신뢰를 잃음 (Garbage In, Garbage Out) 6. 체크리스트 단순 과거 데이터 나열을 넘어 '조건 변경'이 가능한 구조인가 수요와 수익성 외에 현실적인 제약 조건(예산, 인프라)이 반영되었는가 가중치나 변수를 실무자가 직접 조절할 수 있는 인터페이스가 있는가 민감도 분석을 통해 최적안의 취약점을 점검할 수 있는가 결과가 직관적인 매트릭스나 등급 형태로 출력되는가 마무리 분석가의 진짜 역할은 멋진 보고서를 쓰는 것이 아니라, 조직이 더 빠르고 정확하게 선택할 수 있는 판을 깔아주는 것 입니다. 분석을 의사결정 도구로 확장하는 순간, 분석가는 수동적인 '데이터 추출자'에서 전략을 설계하는 '파트너'로 거듭날 수 있습니다.