grep

누구나 찾아볼 수 있는 중고거래 서버 LLM 릴리즈 노트 도입기

당근

2026년 5월 14일

원문에서 보기 ↗

안녕하세요, 당근 중고거래실 구매팀에서 서버 엔지니어로 일하고 있는 코비예요. 엔지니어라면 한 번쯤, 이런 말을 주변 동료에게 들어본 적이 있지 않으신가요?

"이 기능 언제 나갔어요?", "배포 시점은 정확히 언제인가요?"

다들 공감하시겠지만, 저 역시 이 질문이 나올 때마다 매번 일일이 git log를 정리하거나 Pull Request 목록을 살펴봐야 했어요. 일을 하다 보면 빈번히 발생하는 상황이에요. 최근 2주 동안 서로 작업한 것을 공유하는 Bi-Weekly 미팅 전, CS 문의를 대응하다 특정 기능이 언제 배포됐는지 찾아야 할 때, 근무 시간대가 다른 글로벌 채널에서 "한국에서는 최근 뭐가 배포됐는지" 물어볼 때도요.

이러한 질문에 답하기 위해 다른 작업을 멈추고 배포 내역을 찾아보면 몇 분이 소요될 거예요. 이때 답을 기다리는 분들도 같이 멈추게 되고, 개발 용어를 친숙하게 풀어 적는 작업도 해야 해요. 배포 내역의 맥락이 개발자 안에만 갇혀 있다는 게, 한 명의 불편이 아니라 팀 전체의 커뮤니케이션 비용을 발생시키는 문제였어요.

이 글은 그 비효율을 LLM과 이미 쓰고 있던 도구들로 풀어낸 이야기예요. 기술 용어로 가득한 배포 내역을 누구나 읽을 수 있는 형태로 요약하고, 그 자동화 파이프라인까지 제작한 사례예요.

한눈에 보는 변화

먼저 결과부터 간단히 보여드릴게요.

핵심은 두 가지예요. 배포 데이터를 팀 전체가 쓸 수 있는 자산 으로 쌓고, 그 위에 기술 용어를 비개발자도 이해할 수 있게 풀어주는 LLM 번역 레이어를 얹은 것. LLM 릴리즈 노트는 이 둘이 만들어낸 결과물이자 이 글의 목표예요. 어떻게 만들었는지 하나씩 풀어볼게요.

파이프라인 구조

처음 떠오른 그림은 단순했어요. "LLM한테 PR 목록을 던져서 릴리즈 노트 받자." 근데 한 번 더 생각해보니, 그 앞에 필요한 게 있더라고요. 배포 데이터를 자산으로 쌓는 것이요.

LLM이 릴리즈 노트를 만들려면, 정형화된 배포 내역이 어딘가에 쌓여 있어야 해요. "PR 하나가 어떤 기능을 적용·수정했고 어떻게 바뀌었는지"를 PR 단위로 정리한 문서가 그 형태예요. 저는 그 자리를 Notion으로 잡았고, 거기 모인 데이터를 LLM이 읽어서 비개발자도 이해할 수 있는 문서로 풀어내는 게 최종 목표였어요.

자산이 먼저 만들어져야 LLM이 일할 수 있다 --- 이 인과 관계가 전체 설계의 출발점이에요.

이렇게 정형화해서 쌓아둔 배포 기록은 그 자체로도 활용 폭이 넓어요. 릴리즈 노트만이 아니라 리전별 배포 필터링, 배포 빈도 통계, PR 기여자 추적까지 코드 한 줄 없이 가능해져요. LLM이 더해지면 활용처가 더 넓어지는 거죠. (뒤 "활용 사례"에서 자세히 풀어볼게요)

별도 인프라도 만들지 않기로 했어요. GitHub Actions, Notion, 사내 GenAI 플랫폼, Rails 서버 --- 이미 우리가 잘 쓰고 있는 것들로 전부 해결했어요.

크게 두 스테이지로 구성돼요.

Stage 1: 배포 기록

배포가 일어날 때마다 Notion에 기록해요. 배포 시점에 rake task(Rails의 CLI 진입점, 명령어 스크립트에 해당해요) 하나가 세 가지 일을 해요. git log에서 이번 배포에 포함된 PR 목록을 파싱하고, gh CLI로 각 PR의 description을 수집한 다음, Notion에 upsert해요. PR description이 나중에 LLM이 읽을 원문이 돼요.

여기서 의외로 중요한 결정 하나는 변경 코드(diff)를 직접 보지 않는다 는 거예요. diff를 그대로 LLM에 던지면 토큰 비용이 빠르게 늘어나거든요. 더 큰 문제는, 변경의 의미는 이미 PR description에 정리돼 있는데 LLM이 그걸 무시하고 코드부터 다시 해석하게 된다는 점이에요.

중고거래 서버 레포에는 PR이 올라올 때 CodeRabbit이 변경 사항을 자동으로 요약해 PR description에 작성해줘요. 품질이 꽤 정확해서 팀원들에게 "PR description 잘 작성해주세요"라고 요청하기보다 자동으로 만들어지는 요약을 그대로 활용하기로 했어요.

릴리즈 노트가 필요한 정보의 결도 여기에 맞았어요. 우리에게 필요한 건 자세한 맥락이나 코드 변경 디테일이 아니라 **'어떤 기능이 적용·수정됐고 어떻게 바뀌었는지 한 줄'**이거든요. CodeRabbit 요약이 정확히 그 수준에서 멈춰주니까, 그대로 LLM 입력으로 가져가도 충분했어요. PR description은 이미 한 번 정제된 입력인 셈이라, LLM이 코드부터 다시 해석할 필요 없이 좋은 원문으로 바로 일을 시작할 수 있어요.

CodeRabbit이 아니라도 Claude Code, Codex, GitHub Copilot PR summary, Gemini Code Assist 등 여러 AI 도구를 활용하거나 PR 템플릿을 강제하는 방식으로 같은 결을 만들 수 있어요.

핵심은 LLM에 들어가기 전에 PR description이 한 번 정제되는 흐름이 팀에 자리 잡혀 있느냐예요.

Notion에는 세 개의 DB가 있어요. 배포 DB, PR DB, 그리고 LLM 릴리즈 노트 DB.

배포 DB와 PR DB를 relation으로 연결해두면 "이 배포에 어떤 PR이 들어갔나", "이 PR은 어느 배포에서 나갔나"를 양방향으로 조회할 수 있어요. (relation은 Notion에서 두 페이지를 양방향으로 묶어주는 속성이에요 --- 관계형 DB의 foreign key와 비슷해요.)

그리고 LLM 릴리즈 노트 DB는 LLM이 매일 채우는 곳이에요. 어떻게 채워지는지 다음 Stage 2에서 알려드릴게요.

Stage 2: LLM 릴리즈 노트

매일 UTC 01:00 (KST 10:00)에 CronJob이 실행돼요. 여기서 PR 본문은 GitHub에서 다시 가져오는 게 아니라, Stage 1에서 이미 Notion PR DB에 저장해둔 본문을 그대로 읽어요. PR 정보를 한 번만 가져오면 그다음부터는 Notion이 우리 데이터 소스가 되는 거예요.

읽어온 PR 묶음은 Prompt Studio로 넘어가 LLM 요약을 거친 다음, LLM 릴리즈 노트 DB에 저장돼요. 어떻게 요약하는지는 "프롬프트 설계" 섹션에서 정리할게요.

Prompt Studio 는 당근에서 자체 개발한 GenAI 플랫폼이에요. 코드 없이 누구나 프롬프트를 작성·테스트·배포할 수 있고, 여러 모델(OpenAI / Anthropic / Google)을 비교하거나 수백 개 테스트셋으로 품질 평가도 가능해요. 자세한 내용은당근의 GenAI 플랫폼글을 참고해주세요.

이 글에서 중요한 건 Prompt Studio가 가진 여러 기능 중 '프롬프트를 코드 바깥에서 관리할 수 있다' 는 점이에요. 이 분리가 뒤에 나올 프롬프트를 다듬어가는 과정에서 결정적인 역할을 했는데, 자세한 이야기는 "왜 이 조합이었나" 섹션에서 정리할게요.

어떻게 만들었나

💡 이 부분에서는 코드와 디테일이 자주 등장해요. 결과만 알고 싶다면 이 섹션을 건너뛰고 바로 "왜 이 조합이었나" 로 넘어가셔도 글 흐름을 이해하시는 데에 큰 지장은 없을 거예요.

Rake task 진입점

파이프라인의 핵심은 rake task 두 개예요. 배포 시점에 도는 notion:record_deploy**(배포 기록)** , 주기적으로 예약 시간에 도는 release_notes:daily**(릴리즈 노트 생성)**. 둘 다 같은 진입점 패턴이라 코드도 닮았어요. 시간 순서대로 보면 --- 배포 시점에 도는 record_deploy부터.

# lib/tasks/notion.rake
namespace :notion do
  desc "배포 PR 정보를 Notion에 기록 (GitHub Actions에서 호출)"
  task :record_deploy, [:country_code] => :environment do |_t, args|
    country_code = args[:country_code]&.to_sym
    head_sha = `git rev-parse --short HEAD`.chomp

    # 이전 배포 SHA 기준으로 git log 수집 (없거나 invalid면 최근 N개 커밋 fallback)
    last_sha = fetch_last_deployed_sha_from_notion(country_code)
    git_diff = collect_git_diff(last_sha)

    Deploy::DeployNotionRecorder.new(
      git_diff:,
      country_code:,
      head_sha:,
      deployer: ENV["DEPLOYER"] || ENV["GITHUB_ACTOR"] || ENV["USER"]
    ).record
  end
end

그다음은 주기적으로 도는 일간 릴리즈 노트 생성 task예요.

# lib/tasks/release_notes.rake
namespace :release_notes do
  desc "일간 릴리즈 노트 생성 (예: rake release_notes:daily[2025-12-25,dry_run])"
  task :daily, [:date, :dry_run] => :environment do |_t, args|
    date = args[:date].present? ? Date.parse(args[:date]) : nil
    dry_run = args[:dry_run] == "dry_run"

    result = Deploy::ReleaseNotesGenerator.new(
      type: :daily, dry_run:, date:
    ).generate

    if result.nil?
      puts "해당 기간에 배포된 PR이 없습니다."
    elsif dry_run
      puts "#{result[:date_range][:start_date]} ~ #{result[:date_range][:end_date]}"
      puts "PR #{result[:pr_count]}개"
      puts result[:content]
    else
      puts "생성 완료: #{result[:url]} (PR #{result[:pr_count]}개)"
    end
  end
end

두 task 모두 진입점이 통일돼 있어요. CronJob도, GitHub Actions도, 로컬에서 손으로 돌릴 때도 결국 같은 task 한 줄을 호출해요. 그래서 새 환경에 붙이거나 디버깅할 때 따로 손볼 게 없어요.

dry_run 모드는 그 위에 얹은 작은 도구예요. 프롬프트를 다듬는 단계에서는 Notion에 저장하지 않고 LLM 출력만 콘솔로 보는 게 빨라서, 아예 인자 하나로 빼뒀어요.

DeployNotionRecorder.record (배포 기록)

rake task에서 호출하는 DeployNotionRecorder.record 한 줄은 실제로 네 단계를 거쳐요.

# lib/deploy/deploy_notion_recorder.rb
def record
  return dry_run_result if @dry_run

  pr_list = collect_pr_info                                                   # 1. PR 정보 수집
  deploy_page = find_or_create_deploy_page(pr_list)                           # 2. 배포 문서 upsert
  pr_results = save_prs_with_deploy_relation(pr_list, deploy_page[:page_id])  # 3. PR 저장 + relation
  update_deploy_page_pr_relations(deploy_page[:page_id], pr_results)          # 4. 양방향 relation 마무리

  { deploy_page: deploy_page, pr_results: pr_results }
end

# "리전-SHA" 조합으로 unique 식별 → upsert (예: "KR-abc1234")
def find_or_create_deploy_page(pr_list)
  deploy_id = "#{@country_code.to_s.upcase}-#{@head_sha}"
  existing_page = find_deploy_page_by_id(deploy_id)
  existing_page ? { page_id: existing_page["id"], action: :existing } : create_deploy_page(deploy_id, pr_list)
end

# git_diff → PR/커밋 정보 배열. PR 번호 없으면 커밋 SHA로 fallback.
def collect_pr_info; end

# PR 단위로도 upsert. 기존 PR이면 deploy_relations에 이 배포만 추가.
def save_prs_with_deploy_relation(pr_list, deploy_page_id); end

# 배포 문서 쪽 pr_relations 일괄 업데이트 → 양방향 relation 마무리
def update_deploy_page_pr_relations(deploy_page_id, pr_results); end

PR 정보를 먼저 수집한 다음에 배포 문서를 만드는 순서가 핵심이에요. 배포 문서 본문(요약, PR 테이블, 작업자별 기여)을 만들 때 PR 정보가 이미 있어야 하거든요. 마지막에 양방향 relation을 업데이트해서 "이 배포에 어떤 PR이 들어갔나", "이 PR은 어느 배포에서 나갔나"를 양쪽에서 조회할 수 있게 해요.

PR 번호 파싱. 저희 프로젝트는 squash merge를 쓰기 때문에 커밋 메시지가 제목 (#1234) 형태예요. 여기서 PR 번호를 정규식 /\\(#(\\d+)\\)\\s*$/으로 뽑아요. merge commit 전략을 쓰는 팀이라면 git log --merges로 PR을 뽑는 방식이 더 맞아요. 이 정규식 하나가 파이프라인 전체의 입력 품질을 결정하기 때문에, 팀의 merge 전략에 맞게 조정하는 게 첫 번째 과제예요.

배포 범위 산정. "이번 배포"에 포함된 PR을 어떻게 알 수 있을까요? 중고거래 서버 배포 워크플로에서는 GitHub Actions에서 현재 배포 SHA를 받아요. 같은 리전의 이전 배포 SHA와 현재 SHA 사이의 커밋이 "이번 배포"의 범위가 돼요.

만약 이전 배포 SHA가 없거나(첫 배포) force-push 등으로 git 히스토리에서 못 찾는 경우엔 최근 50개 커밋을 fallback으로 잡아요. 이건 가용성과 정확성을 맞바꾸는 선택이에요. 이전 배포에 이미 포함됐던 커밋이 섞여 들어와 데이터 SSoT가 오염될 수 있거든요. 그대로 복제할 팀이라면 이 지점을 '오염 가능 포인트'로 명시적으로 인지하고, 필요하면 수동 보정 경로를 준비해두는 게 좋아요.

중복 방지와 upsert. 같은 SHA가 여러 리전에 배포될 수 있어서, unique key를 리전-HEAD_SHA 조합으로 잡고 upsert 처리해요.

이렇게 만들어지는 결과물은 다음과 같아요.

PR DB에는 이런 속성과 본문으로 Notion 문서가 이뤄지고 있어요.

배포 DB는 이런 속성과 본문으로 Notion 문서가 이뤄지고 있어요.

ReleaseNotesGenerator.generate (LLM 릴리즈 노트 생성)

release_notes:daily task에서 호출하는 Deploy::ReleaseNotesGenerator.generate도 record와 비슷한 흐름이에요.

날짜 범위 계산 → 배포·PR 수집 → PR 본문 읽고 LLM 입력 빌드 → Prompt Studio 호출 → Notion DB 저장.

# lib/hoian/deploy/release_notes_generator.rb
def generate
  date_range = calculate_date_range  # 마지막 릴리즈 노트 이후 ~ 어제
  return nil if !@dry_run && date_range[:start_date] > date_range[:end_date]

  prs = fetch_prs_from_deploys(date_range)
  return nil if prs.empty?

  prs_with_descriptions = fetch_pr_descriptions(prs)
  pr_list_text = build_pr_list_text(prs_with_descriptions)

  llm_result = generate_with_llm(pr_list_text, date_range)
  return llm_result if @dry_run

  save_to_notion(llm_result[:content], date_range, prs_with_descriptions, llm_result[:llm_metadata])
end

# 마지막 릴리즈 노트 end_date + 1일 ~ 어제. CronJob 실패해도 다음 실행에서 자동 보강.
def calculate_date_range; end

# 배포 N건 조회 → 연결된 PR을 리전 정보와 함께 수집 (중복 제거)
def fetch_prs_from_deploys(date_range); end

# PR 한 건당 마크다운으로 묶어 LLM 입력 텍스트 빌드 ("### PR #1234: 제목 [🇰🇷] / Linear / 설명 ...")
def build_pr_list_text(prs); end

# Prompt Studio에 변수만 채워서 호출. 프롬프트는 코드 바깥.
def generate_with_llm(pr_list_text, date_range); end

PR 본문을 GitHub에서 다시 가져오지 않고 Notion PR DB에서 그대로 읽어요 --- Stage 1에서 이미 정제해둔 입력이니까요. LLM 호출도 코드 바깥의 프롬프트에 변수만 채워서 넘기는 형태예요 (분류 규칙·용어 변환 테이블 수정이 코드 배포 없이 가능해요).

KST/UTC 경계. CronJob은 UTC 01:00 (KST 10:00)에 실행돼요. KST 기준 '어제'를 UTC로 변환할 때 자정 근처 배포는 날짜가 어긋날 수 있어서, Time.zone.yesterday를 KST로 고정해서 처리해요. 로컬에서 재현하기 어려운 타입의 버그라 처음엔 하루치 데이터가 빠지거나 중복으로 들어오는 현상이 반복됐어요.

예외 케이스를 다 막은 건 아니지만, 어디서 어떻게 깨질 수 있는지 알고 있는 게 중요했어요. 누락이 발견됐을 때 어디부터 봐야 할지 알 수 있거든요.

이렇게 만들어지는 LLM 릴리즈 노트는 다음과 같아요.

그리고 이런 속성과 본문으로 Notion 문서가 이뤄지고 있어요.

왜 이 조합이었나

rake task --- 배포 시점을 트리거하려면 GitHub Actions이 필요했어요. 그 워크플로우 안에서 돌릴 스크립트를 중고거래 서버의 Rails 스택 위에 그대로 얹기로 했어요. n8n이나 Zapier 같은 외부 서비스를 도입하지 않고도 별도 인프라 없이 빠르게 적용할 수 있었거든요. bundle exec rake release_notes:daily 한 줄로 로컬에서도 돌아가서 디버깅도 편하고요.

**Notion ---**이미 쓰고 있어서예요. Notion에 있으면 일하다가 자연스럽게 열어보게 돼요. 배포 DB 하나로 "리전별 필터 뷰", "주간 배포 캘린더 뷰"를 코드 없이 만들 수 있었고요.

Prompt Studio --- 프롬프트가 코드 바깥에 있다는 게 결정적이었어요. 뒤에서 풀 텐데, 릴리즈 노트의 품질 개선은 거의 다 프롬프트를 반복해서 다듬는 작업으로 해결됐어요. 분류 규칙 한 줄 추가, 용어 변환 테이블에 항목 하나 더 넣는 변경에 매번 코드 배포가 필요했다면 이런 식의 다듬기가 어려웠을 거예요.

다른 기술을 선택한다면

위 세 도구는 저희 사내 환경에 맞춰 고른 거지만, 구조 자체는 도구에 종속적이지 않아요. 다른 환경에서는 이렇게 치환할 수 있어요.

핵심은 세 가지예요.

**(1) 배포 시점에 워크플로우를 돌릴 트리거 ---**GitLab CI · Jenkins · CircleCI 또는 CLI 스크립트

**(2) PR 정보를 정형화해서 쌓을 저장소 ---**SQLite 또는 PostgreSQL, Obsidian

**(3) 코드 바깥에서 프롬프트를 관리할 수 있는 자리 ---**OpenAI · Anthropic 같은 LLM API 직접 호출

이 세 자리만 채우면 언어가 Ruby든 Java, Python이든, 저장소가 Notion이 아니어도 같은 파이프라인을 만들 수 있어요.

프롬프트 설계와 품질 관리

파이프라인을 만드는 것까지는 어렵지 않았어요. 진짜 복병은 그다음이었어요.

이 단계는 앞서 쌓은 데이터 위에 '사람이 읽을 수 있는 번역 레이어'를 얹는 작업이에요.

PR description 묶음을 LLM에 넘기고 응답 받는 코드는 금방 썼어요. 진짜 시간이 걸린 건 LLM이 원하는 형태로 답하게 만드는 것 , 그리고 누구나 쉽게 이해할 수 있는 문서로 만드는 것이었어요.

분류 체계 설계

릴리즈 노트를 보시는 분들이 가장 먼저 원하는 건 "어떤 종류의 변경이 있었나"예요. 그래서 다섯 가지 라벨을 정의했어요.

라벨 정의만 프롬프트에 넣는다고 되지 않아요. 분류 우선순위 규칙이 필요했어요. PR 하나가 버그 수정이면서 성능 개선인 경우, 버그 수정으로 분류해요. 사용자 영향이 있는 변경은 내부 작업보다 앞에 오고요. 이런 규칙이 없으면 LLM이 매번 다르게 분류해요.

"비개발자도 읽을 수 있게" --- 기술 용어를 팀 언어로 번역하다

처음엔 단순했어요. 프롬프트는 한 줄. "이 PR 목록을 요약해줘."

결과물을 보니 기술 용어가 그대로 나타나 있더라고요. "notification-batch OOM 수정" 같은 식으로요. 릴리즈 노트를 만든 이유가 비개발자도 배포 내역을 알 수 있게 하려는 건데, 결과물은 개발자만 알아볼 수 있는 문장이었던 거예요.

처음엔 프롬프트만 손보면 되는 줄 알았어요. "PM이 읽는다고 가정하고 써줘"를 추가했죠. 결과는 거의 그대로였어요. LLM은 'PM이 읽는다'는 의도는 받아들였지만, notification-batch가 사내에서 알림 발송 배치 작업으로 불린다는 사실을 모르거든요. 모르는 정보는 LLM이 만들어낼 수 없어요.

그때 깨달았어요. LLM은 PR description을 그대로 요약하지, 독자를 고려해서 번역하지는 않는다는 걸요. 번역에 필요한 사전(=용어 변환 규칙)은 우리가 직접 줘야 했어요. 그래서 프롬프트에 용어 변환 규칙을 직접 넣기 시작했어요.

이 목록은 처음부터 완성되지 않았어요. 릴리즈 노트를 보다가 "이건 무슨 말이지?" 싶은 개발 용어가 보이면 그때그때 추가했어요. 최근엔 LLM 모델이 좋아지면서 테이블에 명시하지 않은 용어도 좀 더 알아서 자연스럽게 치환해주고 있고요.

프롬프트 품질 개선: 구체적 사례 두 가지

프롬프트를 다듬은 과정을 두 가지 사례로 보여드릴게요.

사례 1: 기술 용어가 그대로 남는 문제

입력 PR이 notification-batch 쿼리 개선 및 OOM 해결일 때, 처음엔 LLM이 notification-batch 배치 OOM 수정이라고 사내 용어를 그대로 노출했어요. 용어 변환 테이블과 "내부 시스템명을 그대로 노출하지 마세요" 규칙을 추가하니 알림 발송 배치 작업의 메모리 부족 문제 해결로 풀어주게 됐어요.

사례 2: 리전 정보가 누락되는 문제

입력 PR이 fix: jp terms config(일본 리전에만 영향)일 때, 처음엔 약관 설정 오류 수정으로 어느 리전인지 안 보이게 출력됐어요. "PR에 특정 국가/리전이 언급된 경우 반드시 포함" 규칙과 리전 라벨 필수를 추가하니 일본 약관 동의 화면 노출 오류 수정 (#1237, 🇯🇵)처럼 리전 정보가 명시되게 됐어요.

이런 식으로 그날 릴리즈 노트를 확인하고 수정하며 개선시켜 나갔어요. 프롬프트가 코드 밖에 있다 보니, 규칙 한 줄을 추가하면 다음 LLM 호출부터 즉시 반영됐어요.

출력 형식 제어

마크다운을 그대로 Notion에 넣으면 헤더(##)가 크게 렌더링되어 가독성이 떨어져요. 그래서 규칙에 마크다운 헤더 사용 금지를 명시했어요. 항목당 100자 제한도 넣었어요. 없으면 LLM이 PR description을 요약 없이 그대로 붙여넣는 경우가 생기거든요.

LLM이 PR을 합쳐버리는 문제

PR 10개 정도를 넘기면 가끔 비슷한 PR 두세 개를 한 항목으로 합쳐버려요.

"1 PR = 1 항목" 규칙을 넣어도 LLM이 "이 두 PR은 같은 기능이니까 합치는 게 자연스럽다"고 판단하는 거예요. "절대 합치지 마세요" 강조 + 출력 검증 체크리스트(입력 PR 수 = 출력 항목 수 등)를 프롬프트 말미에 추가해서 빈도를 줄였어요. 완전히 없어지진 않았지만, 매일 릴리즈 노트를 확인하는 팀원들이 이상한 부분을 알려주는 식으로 보완하고 있어요.

최종 프롬프트

실제로 쓰는 프롬프트는 다섯 블록으로 구성돼 있어요. 핵심만 추리면 이래요.

  1. **역할 정의:**PM·디자이너·비개발자 동료가 이해할 릴리즈 노트 작성자
  2. **분류 라벨 5종 × 세분 라벨:**🚀 신규 / ✨ 개선(UX·성능) / 🐛 버그(수정·동작·표시) / 🧪 실험(추가·변경·종료) / 🔧 내부(설정·도구·쿼리·리팩토링·의존성)
  3. **필수 규칙:**1 PR = 1 항목 / PR 번호 + 리전 라벨 끝에 필수 / 100자 이내 / 비개발자 어휘
  4. 어조·용어 가이드: 너무 기술적(notification-batch OOM 수정)도, 너무 추상적(메모리 문제 해결)도 안 됨. 사내 식별자(notification-batch, instant-buy 등)는 친화 표현(알림 발송 배치 작업, 바로구매)으로 변환
  5. 출력 형식 + 자체 검증: 마크다운 헤더 금지, 이모지 카테고리 줄 + 항목 줄. 응답 직전 PR 수 일치·라벨 위반·사내명 노출 체크
당신은 PM, 디자이너, 비개발자 동료가 이해할 수 있는 릴리즈 노트 작성자입니다.
아래 PR 목록을 읽고, 각 PR에 적절한 분류 라벨을 붙여 릴리즈 노트를 작성하세요.

## 분류 라벨

**신규 기능**
- 🚀 신규 기능: 완전히 새로운 기능 추가

**개선**
- ✨ UX 개선: 사용자 경험 향상
- ✨ 성능 개선: 속도, 효율성 향상

**버그 수정**
- 🐛 버그 수정: 오류/장애 해결
- 🐛 동작 수정: 의도와 다른 동작 교정
- 🐛 표시 수정: UI/텍스트 오류 교정

**실험**
- 🧪 실험 추가: 새 실험 시작
- 🧪 실험 변경: 실험 키/버전 변경
- 🧪 실험 종료: 실험 종료/정리

**내부 작업** — 사용자에게 직접 보이지 않는 변경
- 🔧 설정 변경: 설정값/config 수정
- 🔧 도구 추가: 클로드 스킬 추가, 스크립트 등 추가
- 🔧 쿼리 개선: 데이터 조회 속도/효율 개선
- 🔧 리팩토링: 코드 구조 개선
- 🔧 의존성: 라이브러리/패키지 업데이트

## 필수 규칙
1. **1 PR = 1 항목** — 각 PR은 정확히 한 번만 등장 (중복/누락 금지)
2. **PR 번호 필수** — 각 항목 끝에 `(#번호, 리전라벨)` 형식
3. **리전 라벨 필수** — PR 제목/설명의 리전 정보 포함
4. **간결함** — 각 항목 100자 이내
5. **쉬운 용어** — 비개발자도 이해할 수 있는 수준

## 국가별 맥락 유지
PR 제목/설명에 특정 리전이 언급되면 (CA, JP, KR 등) 반드시 포함.

예시:
- "fix: jp terms config" → "일본 약관 동의 화면 노출 오류 수정"
- "CA 환경에서 notification-batch OOM 이슈 해결" → "캐나다 알림 발송 배치 작업의 메모리 초과 문제 해결"

## 어조 및 용어 수준

### 원칙
- 내부 시스템명/배치 작업명을 그대로 노출하지 마세요
- 변경이 **사용자에게 어떤 기능으로 보이는지** 기준으로 설명
- 일반인도 맥락을 이해할 수 있는 수준 유지

### 예시
❌ 너무 기술적(개발자만 이해): `notification-batch OOM 수정`
❌ 너무 추상적(무슨 기능인지 불명확): `대량 알림 처리 시 메모리 문제 해결`
✅ 적절한 수준: `끌어올리기 리마인더 알림 발송 시 메모리 문제 해결`

### 용어 변환 가이드
| 내부 용어 | 사용자 친화적 표현 |
|-----------|-------------------|
| notification-batch | 알림 발송 배치 작업 |
| instant-buy | 바로구매 |
| OOM / 메모리 초과 | 메모리 문제 / 안정성 개선 |
| batch job | (구체 기능명) 처리 |

## 분류 우선순위
1. 사용자에게 보이는 변화 > 내부 변화
2. 버그 수정 > 개선
3. 기능 추가 > 설정 변경

## 출력 형식
- 마크다운 헤더(##, ###) 금지
- 카테고리 제목은 이모지로 시작
- 같은 카테고리 항목은 `-`로 시작
- 카테고리 사이에 빈 줄 하나

## 출력 전 검증
- [ ] 입력 PR 수 == 출력 항목 수?
- [ ] 5개 라벨 외 카테고리를 만들지 않았나?
- [ ] 모든 항목에 PR 번호와 리전 라벨이 붙어있나?
- [ ] 내부 시스템명이 그대로 남아있는 항목이 없나?

---

## 입력 데이터

### 배포 기간
{{start_date}} ~ {{end_date}}

### PR 목록
{{pr_list}}

프롬프트의 input 변수({{start_date}}, {{end_date}}, {{pr_list}})를 전달해서 Prompt Studio를 호출해요. 코드에 프롬프트를 두지 않으니 "분류 우선순위 한 줄 추가"나 "용어 변환 테이블에 항목 추가" 같은 변경을 배포 없이 Prompt Studio에서 바로 반영해요.

실제 릴리즈 노트 결과물

Claude에게 "5월 4일 뭐 배포됐어?"라고 단순히 물었을 때 응답과 LLM 릴리즈 노트 일부 인상적인 내용을 통해 서로 비교해봤어요.

Claude의 응답은 git log를 정리해주는 수준이라 개발 용어가 그대로 노출되고, 배포 리전 정보와 분류가 빠져요. 이러한 정보는 추가적으로 알려줘야 하는 번거로움이 생겨요. 우측은 LLM이 분류 라벨을 붙이고, 기술 용어를 비개발자 표현으로 풀어주고, 기능을 특정 리전에만 배포했다면 그에 맞는 리전 라벨을 달아줘요.

같은 LLM도 어떤 입력과 프롬프트를 받느냐에 따라 결과가 이렇게 달라져요. 이 차이가 글에서 풀어온 데이터 자산 + 프롬프트 설계의 결과예요.

활용 사례

릴리즈 노트 DB를 만들고 나서 활용처가 자연스럽게 늘어났어요.

팀 위클리 자동화. 매주 위클리 미팅 전에 "이번 주에 뭐 나갔냐"를 정리하는 건 보통 서버 개발자 한 명의 몫이었어요. git log를 뒤지고, 의미 있는 변경만 골라내고, 비개발자도 이해할 수 있게 풀어쓰는 데 꽤 시간을 썼어요. 이제는 주간 릴리즈 노트를 그대로 가져다 붙여요. 개인별로 어떤 Task가 배포되었는지 추적할 수 있어서 스프린트 회고에서도 활용하게 됐어요.

LLM에게 줄 수 있는 맥락으로도. 릴리즈 노트 DB를 Claude Code 스킬로 만들 수 있어요. 이제는 LLM이 우리 팀 배포 내역을 알고 답해요. "A 기능 언제 배포된 거야?", "이번 분기 핵심 변경 요약해줘" 같은 즉석 질문에 자산을 그대로 참고해 답을 만들어줘요.

다른 팀에서 손을 뻗는 신호들. 파이프라인을 만들고 나서는 우리 팀의 작은 도구로 생각했어요. 그런데 신호가 다른 곳에서 먼저 왔어요. 동료 한 분이 Slack에서 "주간 배포 내용을 요약 버전으로 볼 수 있으면 좋겠다"고 제안해줬고, 그 피드백을 반영해 주간 요약본 기능을 추가했어요. 주간 요약은 일간 요약을 모아 LLM에게 다시 전달하는 방식으로 만들었어요.

앞으로 남은 과제

만들고 보니 이 파이프라인은 최종 형태가 아니라 출발점이라는 게 더 명확해졌어요.

다음 단계로 생각하고 있는 것들이에요.

마치며

마지막으로 LLM을 이용한 릴리즈 노트를 만들어보고 싶다면 이 순서를 추천해요.

  1. 배포 시점에 PR 목록을 뽑아보세요 --- squash merge를 쓴다면 커밋 메시지에서 (#PR번호)를 파싱하고, merge commit 전략이라면 git log --merges를 쓰면 돼요
  2. 그 목록을 Notion이든 어디든 쌓기 시작하세요 --- 이 단계에서 LLM은 필요 없어요
  3. 데이터가 쌓이면, 그 위에 LLM 요약을 얹으세요

핵심 구조(git log 파싱 → PR 정보 수집 → 데이터 저장 → LLM 요약)는 언어나 환경에 무관해요. Python이든 Go든, Notion이든 SQLite든, GPT든 Claude든 같은 구조로 만들 수 있어요.

그리고 드리고 싶은 강조점은 세 가지예요.

  1. 먼저 데이터를 쌓기 --- 릴리즈 노트를 위해선 배포 데이터를 정형화해서 쌓는 게 출발점이에요. 자산이 먼저 만들어져야 LLM이 일할 수 있다는 인과 관계가 설계의 핵심이에요.
  2. 기술 요약이 아니라 팀 언어로 번역하기 --- 분류 체계와 출력 형식보다, 사내 식별자(notification-batch, instant-buy)를 비개발자 표현으로 바꾸는 용어 변환 사전이 프롬프트의 핵심이에요.
  3. 한 곳에서 뻗어가는 자산 --- 같은 데이터 자산이 위클리 자동화 · 동료들의 피드백 · Claude Code 스킬 · 다른 팀 도입까지 가지를 뻗었어요. 결과물은 릴리즈 노트 한 장이지만, 영향은 팀 협업 방식 자체에 닿았어요.

이제 저는 "어제 뭐 나갔어요?"라는 질문에 빠르게, 누구나 알아들을 수 있는 말로 답할 수 있게 됐어요.

배포 정보는 git log 어딘가에 남아 있어요. **그걸 팀 전체가 쓸 수 있는 자산으로 만들고, 기술 용어로 가득한 배포 내역을 LLM으로 누구나 읽을 수 있게 번역하는 것 ---**이 두 가지가 핵심이었어요.

당근에서는 이런 식으로 일상의 작은 비효율을 AI로 풀어가는 작업이 곳곳에서 일어나고 있어요. 요즘 회자되는 **'하네스 엔지니어링'**과 같은 결의 흐름이라고 봐요. 거대한 인프라가 아니라 자기 일에 AI를 녹여 환경을 점진적으로 개선해 가는 작업이요.

이 글이 비슷한 비효율로 답답해하는 누군가에게 작은 힌트가 됐으면 좋겠어요. 긴 글 읽어주셔서 감사해요.


누구나 찾아볼 수 있는 중고거래 서버 LLM 릴리즈 노트 도입기 was originally published in 당근 기술 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.