2025년 12월 13일 토요일

정직한 번다운 차트 만들기


 "번다운 차트가 거짓말한다"고 했던 지난 편 기억하시나요?

이번엔 정직한 번다운을 만드는 방법을 알아봅시다.

스코프 크립: 목표가 계속 움직인다

프로젝트 중에 이런 일 겪어보셨죠?

Week 1: "100 포인트면 끝나요!"
Week 2: "아, 소셜 로그인도 추가해주세요" (+15)
Week 3: "모바일도 대응해야죠" (+20)
Week 4: "관리자 페이지는 당연히..." (+30)

열심히 일해서 50포인트를 완료했는데, 전체 스코프가 165로 늘어났습니다.
진행률은 오히려 떨어졌네요. (50% → 30%)

이게 바로 스코프 크립입니다.

번업 차트: 진실을 보여주는 도구

번다운 차트는 남은 작업만 보여줍니다.
번업 차트는 완료된 작업과 전체 스코프를 함께 보여줍니다.

class BurnupChart {
  constructor() {
    this.completed = [];
    this.totalScope = [];
  }

  addWeek(completedWork, currentScope) {
    this.completed.push(completedWork);
    this.totalScope.push(currentScope);
  }

  getScopeCreep() {
    const initial = this.totalScope[0];
    const current = this.totalScope[this.totalScope.length - 1];
    return (((current - initial) / initial) * 100).toFixed(1);
  }
}

// 사용 예시
const chart = new BurnupChart();
chart.addWeek(0, 100); // 시작
chart.addWeek(20, 100); // Week 1
chart.addWeek(35, 115); // Week 2 (스코프 증가!)
chart.addWeek(50, 135); // Week 3 (또 증가!)

console.log(`스코프 크립: ${chart.getScopeCreep()}%`); // 35%

번업 차트를 보면 "우리가 느린 게 아니라 목표가 계속 멀어지고 있구나"를 알 수 있습니다.

누적 흐름도: 병목을 찾아라

칸반 보드의 각 컬럼에 있는 작업 수를 시간에 따라 그린 차트입니다.

def analyze_cumulative_flow(daily_data):
    """누적 흐름도 분석"""

    bottlenecks = []

    for day in daily_data:
        # 각 단계별 작업 수
        todo = day["todo"]
        doing = day["doing"]
        review = day["review"]
        done = day["done"]

        # 병목 감지
        if review > doing * 2:
            bottlenecks.append({
                "day": day["date"],
                "issue": "리뷰 병목",
                "action": "리뷰어 추가 필요"
            })

        if doing > 10:
            bottlenecks.append({
                "day": day["date"],
                "issue": "WIP 과다",
                "action": "진행 중 작업 제한"
            })

    return bottlenecks

누적 흐름도에서 특정 영역이 부풀어 오르면? 그곳이 병목입니다.

사이클 타임 추적

작업이 시작부터 완료까지 걸리는 실제 시간을 측정합니다.

class CycleTimeTracker {
  trackTask(task) {
    const startDate = new Date(task.startedAt);
    const endDate = new Date(task.completedAt);
    const cycleTime = (endDate - startDate) / (1000 * 60 * 60 * 24); // 일 단위

    return {
      name: task.name,
      cycleTime: cycleTime,
      category: this.categorize(cycleTime),
    };
  }

  categorize(days) {
    if (days <= 1) return '🟢 빠름';
    if (days <= 3) return '🟡 보통';
    if (days <= 7) return '🟠 느림';
    return '🔴 매우 느림';
  }

  getAverageCycleTime(tasks) {
    const times = tasks.map((t) => this.trackTask(t).cycleTime);
    return times.reduce((a, b) => a + b, 0) / times.length;
  }
}

평균 사이클 타임이 늘어나면 프로세스에 문제가 있다는 신호입니다.

몬테카를로 시뮬레이션으로 예측

과거 데이터로 미래를 예측합니다.

import random

def monte_carlo_forecast(historical_velocities, remaining_work, simulations=1000):
    """완료 날짜 예측"""

    results = []

    for _ in range(simulations):
        days = 0
        work_left = remaining_work

        while work_left > 0:
            # 과거 속도 중 랜덤 선택
            daily_velocity = random.choice(historical_velocities)
            work_left -= daily_velocity
            days += 1

        results.append(days)

    results.sort()

    return {
        "p50": results[int(len(results) * 0.5)],  # 50% 확률
        "p70": results[int(len(results) * 0.7)],  # 70% 확률
        "p90": results[int(len(results) * 0.9)],  # 90% 확률
    }

# 사용 예시
velocities = [3, 5, 2, 8, 4, 6, 3, 7]  # 과거 일일 완료량
remaining = 100  # 남은 작업

forecast = monte_carlo_forecast(velocities, remaining)
print(f"50% 확률로 {forecast['p50']}일 내 완료")
print(f"90% 확률로 {forecast['p90']}일 내 완료")

"2주면 끝날 것 같아요" 대신 "70% 확률로 12일, 90% 확률로 16일"이라고 말할 수 있습니다.

리드 타임 분포도

작업 크기별로 걸리는 시간을 분석합니다.

const leadTimeDistribution = {
  'XS (1-2 points)': {
    average: '0.5일',
    p50: '0.5일',
    p90: '1일',
    recommendation: '즉시 처리',
  },

  'S (3-5 points)': {
    average: '2일',
    p50: '1.5일',
    p90: '3일',
    recommendation: '일반 처리',
  },

  'M (8-13 points)': {
    average: '5일',
    p50: '4일',
    p90: '8일',
    recommendation: '분해 검토',
  },

  'L (20+ points)': {
    average: '15일',
    p50: '12일',
    p90: '25일',
    recommendation: '반드시 분해',
  },
};

function estimateBySize(points) {
  if (points <= 2) return leadTimeDistribution['XS (1-2 points)'];
  if (points <= 5) return leadTimeDistribution['S (3-5 points)'];
  if (points <= 13) return leadTimeDistribution['M (8-13 points)'];
  return leadTimeDistribution['L (20+ points)'];
}

실시간 대시보드 만들기

class HonestDashboard:
    """정직한 프로젝트 대시보드"""

    def __init__(self):
        self.metrics = {}

    def update_daily(self, data):
        self.metrics = {
            "진행률": self.calculate_progress(data),
            "예상_완료일": self.forecast_completion(data),
            "스코프_변경": self.scope_change(data),
            "병목_지점": self.find_bottleneck(data),
            "리스크": self.assess_risks(data)
        }

    def calculate_progress(self, data):
        # 포인트 기반 진행률 (작업 개수가 아닌)
        completed_points = data["completed_points"]
        total_points = data["total_points"]
        return f"{(completed_points/total_points*100):.1f}%"

    def forecast_completion(self, data):
        # 실제 속도 기반 예측
        avg_velocity = data["avg_velocity_last_2weeks"]
        remaining = data["remaining_points"]
        days = remaining / avg_velocity if avg_velocity > 0 else "∞"
        return f"{days:.0f}일 후"

    def scope_change(self, data):
        # 스코프 변경률
        initial = data["initial_scope"]
        current = data["current_scope"]
        change = ((current - initial) / initial * 100)
        return f"{change:+.1f}%"

    def find_bottleneck(self, data):
        # 가장 많이 쌓인 단계
        stages = data["work_in_progress_by_stage"]
        bottleneck = max(stages, key=stages.get)
        return f"{bottleneck} ({stages[bottleneck]}개)"

    def assess_risks(self, data):
        risks = []

        if data["velocity_trend"] < 0:
            risks.append("속도 감소 중")

        if data["scope_creep_rate"] > 10:
            risks.append("과도한 스코프 변경")

        if data["blocked_tasks"] > 3:
            risks.append(f"블로커 {data['blocked_tasks']}개")

        return risks or ["정상"]

정직한 커뮤니케이션

def honest_status_report():
    """정직한 상태 보고"""

    return f"""
    ## 프로젝트 현황 (Week 8)

    ### 숫자로 보는 진실
    - 완료: 120 포인트 / 180 포인트 (67%)
    - 초기 스코프 대비: +35% 증가
    - 현재 속도: 주당 15 포인트
    - 예상 완료: 4주 후 (70% 신뢰도)

    ### 좋은 소식
    - 핵심 기능 80% 완료
    - 품질 지표 향상 (버그 -40%)

    ### 나쁜 소식
    - 스코프 계속 증가 중
    - 백엔드 병목 심화

    ### 필요한 결정
    1. 추가 요구사항 동결 여부
    2. 백엔드 개발자 지원 여부
    3. 출시 일정 조정 여부
    """

숫자를 조작하지 마세요. 진실을 말하세요.
그래야 올바른 결정을 내릴 수 있습니다.

체크리스트: 정직한 번다운 만들기

프로젝트 시작 시:

  •  번업 차트도 함께 준비
  •  스토리 포인트 기준 통일
  •  완료 기준 명확히 정의
  •  사이클 타임 측정 도구 준비

매일:

  •  완료된 포인트 기록
  •  새로 추가된 스코프 기록
  •  블로커 기록
  •  각 단계별 작업 수 기록

매주:

  •  평균 속도 계산
  •  완료 예상일 재계산
  •  스코프 변경률 분석
  •  병목 지점 파악

마무리

정직한 번다운은 단순히 차트를 그리는 것이 아닙니다.

현실을 있는 그대로 보여주는 용기입니다.

"90% 완료"라는 거짓말 대신,
"67% 완료, 스코프 35% 증가, 4주 더 필요"라고 말하세요.

그래야 팀이 올바른 결정을 내릴 수 있습니다.


투명하고 정확한 프로젝트 관리가 필요하신가요? Plexo를 확인해보세요.

댓글 없음:

댓글 쓰기