Skip to main content

9. 초보자 코드 실습 2: 기존 프로젝트 분석·수정·리팩터링

지난 장에서는 OpenCode에게 새 파일을 만들게 하면서 손을 푸는 느낌으로 연습했습니다. 이번 장부터는 분위기가 조금 달라집니다. 이제는 이미 존재하는 코드를 읽고, 이해하고, 조심스럽게 고치고, 필요하면 더 깔끔하게 정리하는 쪽으로 넘어갑니다.

여기서부터가 진짜 생산성이 드러나는 구간입니다. 새 파일 하나 만들어 주는 일은 멋져 보이지만, 실제 개발에서는 이미 누군가 만들어 둔 코드와 씨름하는 시간이 훨씬 깁니다. 이름이 애매한 함수, 군데군데 복붙된 로직, 테스트는 일부만 있고, 실행은 되는데 결과가 미묘하게 이상한 코드. 바로 이런 곳에서 OpenCode의 가치가 살아납니다.

OpenCode는 CLI에서 run 방식으로 빠르게 요청할 수 있고, --agent, --file, --continue 같은 옵션으로 분석과 실행 흐름을 나눠 다룰 수 있습니다.1 또한 프로젝트 단위 설정으로 기본 에이전트와 권한 정책을 조정할 수 있으므로, 기존 코드베이스를 건드릴 때 더 안전하게 접근할 수도 있습니다.23 이 책의 OpenAI OAuth 플러그인 환경에서는 GPT-5.2 계열과 Codex 계열 모델을 variant와 함께 선택해 실습하기 좋습니다.4

이번 장의 목표는 네 가지입니다.

  • 기존 프로젝트 구조를 설명 요청으로 빠르게 파악하기

  • 특정 파일만 콕 집어 수정하기

  • 중복 로직을 찾아 간단하게 리팩터링하기

  • 계획 모드와 실행 모드를 나눠 더 안전하게 작업하기

초보자 코드 실습 2: 기존 프로젝트 분석·수정·리팩터링

왜 기존 코드베이스를 다루는 능력이 더 중요할까

초보자는 종종 "AI가 새 코드 잘 만들면 끝 아닌가요?"라고 생각합니다. 그런데 실무에서는 상황이 정반대인 경우가 많습니다. 새 프로젝트를 처음부터 만드는 시간보다, 이미 있는 프로젝트를 이해하고 수정하는 시간이 더 길기 때문입니다.

기존 프로젝트를 다룰 때 어려운 점은 단순히 코드 길이가 길어서가 아닙니다. 보통은 이런 문제가 함께 섞여 있습니다.

  • 같은 규칙이 여러 파일에 중복돼 있다.

  • 한 파일을 고쳤더니 다른 파일은 여전히 옛 방식으로 동작한다.

  • 실행은 되지만 결과가 조용히 틀린다.

  • 무엇을 먼저 고쳐야 할지 감이 안 온다.

그래서 이번 장의 핵심은 "한 번에 크게 바꾸기"가 아닙니다. 읽기 → 계획 세우기 → 작은 수정 → 실행 확인 → 필요한 만큼만 리팩터링 이 흐름을 몸에 익히는 데 있습니다.

실습용 기존 프로젝트 만들기

이번에는 일부러 조금 어설픈 미니 프로젝트를 준비해 보겠습니다. 새 프로젝트이긴 하지만, 중요한 포인트는 이미 만들어져 있는 코드처럼 다루는 것입니다. 직접 처음부터 설계하지 않고, 일단 주어진 코드를 읽고 고치는 연습이라고 생각하면 됩니다.

아래 명령으로 연습 폴더와 파일을 만드세요.

mkdir -p ~/opencode-lab-2/src ~/opencode-lab-2/test
cd ~/opencode-lab-2

cat > package.json <<'EOF'
{
  "name": "opencode-lab-2",
  "version": "1.0.0",
  "type": "commonjs",
  "scripts": {
    "start": "node src/index.js",
    "test": "node test/cart.test.js"
  }
}
EOF

cat > src/cart.js <<'EOF'
function calculateLineTotal(price, quantity) {
  if (!quantity) {
    quantity = 1;
  }
  return price * quantity;
}

function applyDiscount(total, discountPercent) {
  if (!discountPercent) {
    return total;
  }
  return total - total * (discountPercent / 100);
}

function formatWon(amount) {
  return amount.toLocaleString("ko-KR") + "원";
}

module.exports = {
  calculateLineTotal,
  applyDiscount,
  formatWon,
};
EOF

cat > src/summary.js <<'EOF'
const { formatWon } = require("./cart");

function printCart(items, discountPercent) {
  let subtotal = 0;

  for (const item of items) {
    subtotal += item.price * (item.quantity || 1);
  }

  const discountAmount = discountPercent
    ? subtotal * (discountPercent / 100)
    : 0;

  const finalTotal = subtotal - discountAmount;

  console.log("장바구니 요약");
  console.log("----------------");

  for (const item of items) {
    const quantity = item.quantity || 1;
    console.log(`${item.name}: ${formatWon(item.price)} x ${quantity}`);
  }

  console.log("----------------");
  console.log("소계:", formatWon(subtotal));
  console.log("할인:", formatWon(discountAmount));
  console.log("최종:", formatWon(finalTotal));
}

module.exports = { printCart };
EOF

cat > src/index.js <<'EOF'
const { printCart } = require("./summary");

const cartItems = [
  { name: "키보드", price: 30000, quantity: 2 },
  { name: "마우스", price: 15000, quantity: 0 },
  { name: "마우스패드", price: 5000 }
];

printCart(cartItems, 10);
EOF

cat > test/cart.test.js <<'EOF'
const assert = require("assert");
const { calculateLineTotal, applyDiscount } = require("../src/cart");

assert.strictEqual(calculateLineTotal(1000, 2), 2000);
assert.strictEqual(calculateLineTotal(1000, 0), 0);
assert.strictEqual(applyDiscount(10000, 10), 9000);

console.log("All tests passed");
EOF

파일 수는 많지 않지만, 초보자 연습용으로는 딱 좋습니다. 왜냐하면 문제도 있고, 중복도 있고, 손볼 여지도 있기 때문입니다. 너무 완벽한 예제는 오히려 실전 감각을 주지 못합니다.

먼저 실행해서 문제를 눈으로 확인하자

AI에게 바로 맡기기 전에, 사람이 먼저 현재 상태를 확인하는 습관이 중요합니다. 지금 이 프로젝트는 "버그 하나 + 중복 로직 하나"가 숨어 있습니다.

먼저 테스트를 실행해 보겠습니다.

npm test

아마 이런 식으로 실패할 가능성이 큽니다.

AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
1 !== 0

이제 프로그램도 실행해 봅니다.

npm start

현재 출력은 대략 이렇게 나옵니다.

장바구니 요약
----------------
키보드: 30,000원 x 2
마우스: 15,000원 x 1
마우스패드: 5,000원 x 1
----------------
소계: 80,000원
할인: 8,000원
최종: 72,000원

이상합니다. 마우스의 수량은 0인데 출력은 1로 나옵니다. 테스트도 그 점을 잡아냈고, 실제 실행 결과도 같은 문제를 보여 줍니다. 아주 좋습니다. 버그가 눈에 보이기 시작했기 때문입니다.

안전장치부터 걸고 시작하기

기존 프로젝트를 다룰 때는 "무조건 바로 수정"보다 "먼저 분석, 그다음 수정"이 훨씬 안전합니다. OpenCode는 프로젝트 설정 파일로 기본 에이전트와 권한 정책을 조절할 수 있고, 프로젝트 config는 전역 설정 위에 덮어써서 적용할 수 있습니다.2 또한 permission 설정으로 edit나 bash를 바로 실행하지 않고 물어보게 만들 수 있습니다.3

연습 프로젝트 루트에 아래 파일을 하나 두겠습니다.

cat > opencode.json <<'EOF'
{
  "$schema": "https://opencode.ai/config.json",
  "default_agent": "plan",
  "permission": {
    "edit": "ask",
    "bash": "ask"
  }
}
EOF

이 설정의 느낌은 이렇습니다.

  • 기본 에이전트를 plan으로 시작한다.

  • 파일 수정과 셸 실행은 함부로 하지 않고 한 번 더 확인한다.

  • 특히 기존 프로젝트를 읽을 때 실수로 과감한 수정이 들어가는 일을 줄인다.

처음에는 조금 답답하게 느껴질 수 있습니다. 하지만 기존 코드베이스를 다룰 때는 이 답답함이 오히려 안전벨트입니다.

실습 1: 프로젝트 전체를 설명해 달라고 요청하기

이제 OpenCode에게 이 프로젝트가 무엇을 하는지 설명하게 해보겠습니다. 첫 단계는 수정이 아니라 이해입니다. 여기서 바로 고치라고 시키면, AI도 사람도 방향을 헷갈리기 쉽습니다.

아래처럼 실행합니다.

opencode run "Analyze this small Node.js project for a beginner. Explain what each file does, identify likely bugs, and point out duplicated logic. Do not edit anything yet." --agent=plan --model=openai/gpt-5.2-codex --variant=medium

CLI의 run 명령은 프롬프트를 직접 넘겨 빠르게 실행할 수 있고, --agent로 어떤 에이전트 관점에서 작업할지 선택할 수 있습니다.1

좋은 응답은 대체로 이런 흐름을 가집니다.

src/cart.js는 계산 유틸 함수 모음이고, src/summary.js는 장바구니 출력 담당이며, src/index.js는 예시 데이터를 넣어 실행합니다. 그리고 quantity를 0으로 넣었을 때 if (!quantity) 혹은 item.quantity || 1 같은 코드 때문에 1로 바뀌는 버그 가능성이 높다고 짚어 줄 것입니다.

이 단계에서 중요한 것은 정답 문장 자체가 아닙니다. AI가 프로젝트 구조를 어떤 눈으로 보는지 먼저 확인하는 것입니다. 그게 맞으면 다음 단계가 쉬워지고, 어긋나면 여기서 바로 교정할 수 있습니다.

실습 2: 특정 파일 설명만 따로 요청하기

전체 설명을 들었다면, 이제 문제 파일 하나를 좁혀서 봅니다. 초보자는 여기서 큰 도움을 받습니다. "이 파일 뭐 하는 거야?"를 통째로 물을 수 있기 때문입니다.

예를 들어 src/summary.js만 붙잡고 설명을 요청해 봅시다.

opencode run --file src/summary.js "Explain this file line by line for a beginner. Focus on how quantity, subtotal, discount, and final total are computed." --agent=plan --model=openai/gpt-5.2-codex --variant=medium

--file 옵션으로 특정 파일을 메시지에 첨부할 수 있으니, 긴 프로젝트 전체를 매번 통으로 읽히지 않아도 됩니다.1

이 요청의 장점은 아주 실용적입니다. AI가 '이 파일의 역할'만 설명하는 것이 아니라, 어느 줄에서 수량 기본값이 들어가고, 왜 0이 1로 바뀌는지를 자연어로 풀어 줄 가능성이 높기 때문입니다. 초보자에게는 이 과정이 디버깅 교과서처럼 느껴질 수 있습니다.

실습 3: 계획 모드로 수정 계획만 받기

자, 이제 슬슬 손보고 싶어집니다. 하지만 아직 한 번 더 참겠습니다. 계획 모드에서는 "무엇을 바꿀지"만 정리하고, 실제 수정은 다음 단계에서 합니다. 이 구분이 생각보다 중요합니다.

아래처럼 요청해 보세요.

opencode run "Create a short change plan for this project. Goals: fix the quantity=0 bug, then remove duplicated cart logic. Keep it to 4 steps. Do not edit files yet." --agent=plan --model=openai/gpt-5.2-codex --variant=medium

좋은 계획은 보통 이런 느낌입니다.

  • quantity를 기본값 1로 처리하는 규칙을 명확히 정의한다.

  • 0은 유효한 값으로 유지하고, undefined 또는 null일 때만 기본값 1을 쓴다.

  • 중복 계산 로직을 한곳으로 모은다.

  • 테스트와 실행 결과로 확인한다.

이렇게 받아 두면 다음 단계에서 훨씬 침착해집니다. 그냥 "고쳐 줘"보다, "방금 세운 계획의 1단계만 적용해 줘"가 훨씬 안전하기 때문입니다.

실습 4: 실행 모드로 작은 수정 하나만 적용하기

이제 실제 수정입니다. 여기서 핵심은 작게 고치기입니다. 한 번에 버그 수정과 대규모 리팩터링을 같이 시키면, 초보자는 어디서 뭐가 바뀌었는지 따라가기 어렵습니다.

먼저 quantity=0 버그만 고치게 해 보겠습니다.

opencode run --continue "Implement only step 1 from the previous plan. Fix the bug where quantity 0 is treated like 1 in src/cart.js. Keep the change as small as possible and do not refactor other files yet." --agent=build --model=openai/gpt-5.2-codex --variant=medium

--continue를 붙이면 직전 세션의 문맥을 이어받을 수 있어서, 방금 세운 계획을 같은 흐름으로 활용하기 쉽습니다.1

수정 전 핵심 코드는 이랬습니다.

function calculateLineTotal(price, quantity) {
  if (!quantity) {
    quantity = 1;
  }
  return price * quantity;
}

수정 후에는 예를 들어 이런 식이 될 수 있습니다.

function calculateLineTotal(price, quantity) {
  if (quantity === undefined || quantity === null) {
    quantity = 1;
  }
  return price * quantity;
}

이 변화가 왜 중요할까요? !quantity는 0, "", false, null, undefined를 한꺼번에 같은 취급으로 묶어 버립니다. 그런데 이번 프로젝트에서는 0이 진짜 의미 있는 값입니다. 마우스를 0개 담았다는 뜻이니까요. 그래서 없음과 0개를 구분해야 합니다.

실습 5: 테스트는 통과하지만 앱은 아직 틀릴 수 있다

이제 테스트를 다시 돌려 보세요.

npm test

아마 이렇게 통과할 수 있습니다.

All tests passed

그런데 여기서 끝내면 안 됩니다. 실제 프로그램도 다시 돌려 보세요.

npm start

놀랍게도 출력은 여전히 이상할 수 있습니다.

장바구니 요약
----------------
키보드: 30,000원 x 2
마우스: 15,000원 x 1
마우스패드: 5,000원 x 1
----------------
소계: 80,000원
할인: 8,000원
최종: 72,000원

왜 이런 일이 벌어질까요? 이유는 간단합니다. src/cart.js는 고쳤지만, src/summary.js 안에 같은 규칙이 복붙된 채로 남아 있기 때문입니다.

이 순간이 정말 중요합니다. 초보자는 여기서 큰 깨달음을 얻습니다. 버그는 한 줄에서 시작될 수 있지만, 중복 로직이 있으면 같은 버그가 여러 군데 살아남는다는 사실 말입니다. 그래서 리팩터링은 사치가 아니라, 같은 실수를 반복하지 않기 위한 정리 작업입니다.

실습 6: 간단한 리팩터링으로 중복 로직 없애기

이제 두 번째 단계입니다. 이번에는 구조를 과하게 바꾸지 말고, 중복된 계산만 한곳으로 모으는 정도로 정리해 보겠습니다.

아래처럼 요청해 보세요.

opencode run --continue "Now refactor the project to remove duplicated cart logic. Reuse logic from src/cart.js inside src/summary.js. Keep the console output text the same, stay beginner-friendly, and avoid over-engineering." --agent=build --model=openai/gpt-5.2-codex --variant=medium

이 요청이 좋은 이유는 두 가지입니다. 첫째, 목표가 분명합니다. 둘째, "과하게 설계하지 말라"는 제약을 걸었기 때문에 작은 프로젝트를 괜히 거대한 구조로 바꿀 가능성을 줄여 줍니다.

예를 들어 최종적으로 src/cart.js가 이런 식으로 정리되면 좋습니다.

function normalizeQuantity(quantity) {
  if (quantity === undefined || quantity === null) {
    return 1;
  }
  return quantity;
}

function calculateLineTotal(price, quantity) {
  return price * normalizeQuantity(quantity);
}

function applyDiscount(total, discountPercent) {
  if (!discountPercent) {
    return total;
  }
  return total - total * (discountPercent / 100);
}

function calculateCartTotals(items, discountPercent) {
  let subtotal = 0;

  for (const item of items) {
    subtotal += calculateLineTotal(item.price, item.quantity);
  }

  const finalTotal = applyDiscount(subtotal, discountPercent);
  const discountAmount = subtotal - finalTotal;

  return { subtotal, discountAmount, finalTotal };
}

function formatWon(amount) {
  return amount.toLocaleString("ko-KR") + "원";
}

module.exports = {
  normalizeQuantity,
  calculateLineTotal,
  applyDiscount,
  calculateCartTotals,
  formatWon,
};

그리고 src/summary.js는 이렇게 단순해질 수 있습니다.

const {
  normalizeQuantity,
  calculateCartTotals,
  formatWon,
} = require("./cart");

function printCart(items, discountPercent) {
  const { subtotal, discountAmount, finalTotal } = calculateCartTotals(
    items,
    discountPercent
  );

  console.log("장바구니 요약");
  console.log("----------------");

  for (const item of items) {
    const quantity = normalizeQuantity(item.quantity);
    console.log(`${item.name}: ${formatWon(item.price)} x ${quantity}`);
  }

  console.log("----------------");
  console.log("소계:", formatWon(subtotal));
  console.log("할인:", formatWon(discountAmount));
  console.log("최종:", formatWon(finalTotal));
}

module.exports = { printCart };

보시면 거창하지 않습니다. 클래스도 없고, 디자인 패턴도 없고, 갑자기 파일이 여섯 개로 늘어나지도 않습니다. 그저 같은 계산 규칙을 여러 군데 적지 않게 정리했을 뿐입니다. 초보자 리팩터링은 이 정도면 충분히 훌륭합니다.

실습 7: 리팩터링 후 결과를 다시 검증하기

이제 다시 테스트와 실행을 확인합니다.

npm test
npm start

이제 출력은 이런 식으로 바뀌어야 합니다.

장바구니 요약
----------------
키보드: 30,000원 x 2
마우스: 15,000원 x 0
마우스패드: 5,000원 x 1
----------------
소계: 65,000원
할인: 6,500원
최종: 58,500원

바로 이 장면이 핵심입니다. 테스트도 맞고, 실제 출력도 맞고, 중복도 줄었습니다. 즉, 버그 수정 + 구조 개선 + 실행 검증까지 한 번의 흐름으로 끝난 것입니다.

OpenCode를 그냥 "코드 대신 써 주는 도구"로만 보면 여기까지 오기 어렵습니다. 하지만 "기존 코드를 읽고, 위험한 부분을 찾고, 작은 단위로 안전하게 고쳐 나가는 짝 프로그래머"처럼 쓰면 훨씬 강력해집니다.

계획 모드와 실행 모드를 나누면 왜 좋은가

이번 실습에서 사실 가장 중요했던 부분은 계획과 실행을 나눴다는 점입니다. 초보자는 종종 이 두 단계를 한 번에 섞습니다.

예를 들어 이런 요청은 위험합니다.

이 프로젝트 분석하고 버그 고치고 리팩터링하고 테스트도 다 정리해줘.

나쁜 요청은 아니지만, 범위가 너무 넓습니다. 결과가 좋더라도 무엇이 왜 바뀌었는지 파악하기 어렵습니다.

반대로 이번 장에서 쓴 방식은 훨씬 안전합니다.

  • 먼저 구조 설명만 요청한다.

  • 그다음 수정 계획만 받는다.

  • 그다음 1단계 수정만 적용한다.

  • 마지막으로 필요한 만큼만 리팩터링한다.

이 패턴이 좋은 이유는, 사람이 방향키를 쥐고 있기 때문입니다. OpenCode는 빠르게 일하지만, 방향을 정하는 책임은 여전히 사용자에게 있습니다. 계획 모드가 지도라면, 실행 모드는 실제 운전입니다. 지도를 펴 보지도 않고 액셀을 밟으면, 차가 빨리 가도 원하는 곳에 도착하긴 어렵습니다.

TUI에서 같은 흐름을 쓰면 더 자연스럽다

지금까지는 opencode run 위주로 실습했습니다. 이 방식은 재현하기 좋고, 책으로 따라 하기도 편합니다. 하지만 실제로 조금 더 길게 작업할 때는 TUI 쪽이 더 자연스러울 수 있습니다.

프로젝트 폴더에서 아래처럼 시작해 보세요.

opencode

그리고 대화는 이런 흐름으로 가져가면 됩니다.

이 프로젝트 구조를 초보자 눈높이에서 설명해줘. 아직 수정하지 말고, 버그 후보와 중복 로직만 찾아줘.

이어서:

좋아. 이제 수정 계획을 4단계로 짧게 정리해줘. 1단계는 quantity=0 버그 수정, 2단계는 중복 제거로 해줘.

그다음:

이제 1단계만 적용해줘. 다른 파일 구조는 건드리지 마.

마지막으로:

좋아. 테스트와 실행 결과를 보고, 중복이 남아 있으면 가장 작은 리팩터링만 해줘.

이렇게 하면 사람이 흐름을 끊지 않고 대화하듯 조정할 수 있습니다. 말 그대로 "잠깐 계획 세워 보고, 그다음 한 발만 떼자"가 가능해집니다.

초보자에게 잘 먹히는 수정 프롬프트 공식

기존 프로젝트를 고칠 때는 프롬프트를 조금만 정리해도 결과가 훨씬 좋아집니다. 특히 아래 네 요소가 잘 먹힙니다.

  • 문제 증상

  • 수정 범위

  • 금지 사항

  • 검증 방법

예를 들어 이번 장의 나쁜 요청은 이렇습니다.

코드 좀 깔끔하게 고쳐줘.

이건 너무 넓습니다. 무엇이 문제인지, 어디까지 바꿔도 되는지, 어떻게 확인할지가 전혀 없습니다.

조금 더 좋은 요청은 이렇습니다.

Fix the bug where quantity 0 is treated like 1 in src/cart.js. Keep the change minimal and do not refactor other files yet.

더 실전적인 요청은 이렇게 갈 수 있습니다.

Refactor duplicated subtotal and discount logic so src/summary.js reuses helpers from src/cart.js. Keep the output text unchanged and verify with npm test and npm start.

이런 프롬프트는 짧아도 강합니다. 대상, 목표, 제한, 확인 방법이 모두 들어 있기 때문입니다.

이번 장에서 가져가야 할 감각

이번 장의 표면적인 결과는 단순합니다. 버그를 하나 고쳤고, 계산 로직을 조금 정리했습니다. 하지만 진짜로 가져가야 할 것은 결과물보다 작업 방식입니다.

처음에는 프로젝트 전체를 설명하게 했고, 그다음 특정 파일을 좁혀 봤고, 수정 전에 계획을 세웠고, 실제 수정은 한 번에 하나씩만 적용했습니다. 그리고 테스트와 실행 결과로 다시 확인했습니다. 이 흐름이 익숙해지면, 실제 회사 코드나 개인 프로젝트에서도 훨씬 덜 무섭게 접근할 수 있습니다.

새 파일 생성은 화려합니다. 하지만 진짜 생산성은, 이미 얽혀 있는 코드를 안전하게 다룰 때 드러납니다. 이번 장에서 바로 그 첫 감각을 잡았다면 아주 잘하고 있는 것입니다. 다음 장에서는 이런 실전 감각을 바탕으로, 자주 만나는 오류 상황과 복구 방법까지 이어서 다뤄 보겠습니다.

참고

1OpenCode CLI 문서

2OpenCode Config 문서

3OpenCode Permissions 문서

4opencode-openai-codex-auth GitHub 저장소

책 목록

초보자를 위한 OpenCode × OpenAI OAuth 실전 가이드: 설치, 설정, 로그인, 코드 실습까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기