Notice
Recent Posts
Recent Comments
Link
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

빠르게 학습하고 빠르게 적용하자

백엔드 Push 한 번에 프론트 PR이 날아옵니다: 백엔드 개발자가 도전한 Claude+Git Action 웹 개발 자동화 본문

카테고리 없음

백엔드 Push 한 번에 프론트 PR이 날아옵니다: 백엔드 개발자가 도전한 Claude+Git Action 웹 개발 자동화

osoohynn 2026. 3. 11. 20:25

개발 중인 프로젝트 소개

제가 개발하고 있는 SQL 학습자가 직접 쿼리를 작성하고 즉시 채점받을 수 있는 온라인 저지 서비스 Querify입니다.

기존 SQL 학습은 로컬 DB를 직접 세팅해야 하거나, 정답 확인이 수동으로 이루어지는 불편함이 있었습니다. Querify는 문제별로 격리된 sandbox DB 환경을 제공하여 별도 환경 구축 없이 브라우저에서 SQL을 실행하고, 정답 결과셋과 비교하여 즉시 결과를 받을 수 있도록 설계했습니다.

  • SQL 실행 환경을 안전하게 분리하는 구조를 직접 설계해보고 싶었습니다.
  • 또한 평소 관심이 있던 SQL 학습 도메인으로 프로젝트를 만들고, 실제 데이터를 기반으로 사용자 경험을 개선해보고 싶었습니다.

 

자동화의 배경

웹 개발 자동화가 왜 필요했을까요?

저는 백엔드를 주로 하고 있는 개발자입니다. 웹개발보다는 백엔드를 위주로 집중해서 프로젝트를 개발해나가고 있었고, 웹의 경우에는 Claude Code를 사용하여 디자인 및 개발을 진행했습니다.

Querify를 활발하게 개발하다보니 백엔드 API가 자주 변경되었고, 그때마다 프론트 코드를 반복적으로 수정해야 했습니다.

API 변경 → Claude 프롬프트 작성 → Claude 계획 & 실행 → 프론트 Code Push

서버에서 API 사용흐름을 변경하게 된다면, 변경이 된 부분을 찾아서 다시 웹코드를 열고, Claude Code를 통해 수정해야한다는 번거로움이 있었습니다. 같은 작업을 10번 정도 반복하고 나니, “이 부분은 자동화를 해도 되겠는데?” 라는 생각이 들었습니다. 실제로 Git API를 사용해본 경험이 있었기에 충분히 가능할 것이라고 생각했습니다.

 

계획

우선 전체적인 흐름을 먼저 정의해보았습니다. n8n과 Git Actions로 CI/CD 파이프라인을 설계하던 경험을 살려, 다음과 같이 구성해보았습니다.

[백엔드 커밋 & 푸쉬] → [Claude: API 변경 내용 탐지]
→ [변동사항 있다면 Claude 계획 수립 및 개발] → [웹 코드 Branch Push 및 변경 내용 알림]
→ [개발자 수락] → [Branch Merge]

백엔드에서 main Branch에 Push가 일어나면 Claude를 이용해 API 변경 내용이 있는지 탐지하고 Pull Request를 생성하고 개발자에게 검토 요청을 보냅니다. 그다음 제가 그 요청을 수락하면 브랜치에 머지되는 구조로 생각했습니다.

 

워크플로우 환경 구축

가장 크게 파이프라인을 구축하는 툴을 선택해야했습니다. 만약 팀 프로젝트였다면 팀원 모두 유지보수가 가능하게 팀원간 상의가 필요했겠지만, 1인 프로젝트였기 때문에 기술 선택에 있어 다른 관점의 판단이 필요했습니다.

GitHub관련 자동화 파이프라인을 구축하는 방법으로 세가지를 생각했습니다.

n8n, Git Actions, 코드 방식

우선 n8n은 자동화를 할 때 가장 많이 사용하는 툴이였습니다. 기본으로 제공되는 API도 많고 GUI이기 때문에 편리하게 이용할 수 있습니다. n8n을 사용한다면 저희 집에 On-Premise Server에서 돌아갈 것이라 큰 위험은 없었지만 다운될 수 있다는 우려는 있었습니다.

Git Actions는 GitHub에서 공식적으로 제공하는 CI/CD 툴입니다. 따라서 별도의 인프라가 불필요하고, 처음 구축하는 시간이 단축된다는 장점이 있었습니다.

직접 코드를 짜는 방식도 생각해보았습니다. API서버와 분리해야한다고 생각하여, 만약 구축한다면 별도의 프로젝트와 인프라가 필요하다고 판단했습니다. 커스텀하기에는 가장 좋은 방법이지만 workflow 자체가 비동기처리나 병렬처리 없이 순차적으로 실행하는 구조이기에 커스텀의 장점이 크게 매력적으로 다가오지 않았습니다.

따라서 n8n과 GitHub Actions 두 개 중에 고민하였습니다. GitHub Actions는 백엔드 레포에 만든다 가정했을때, 프론트 코드에 접근하는 방법이 안전하지 않고, 프론트에게 트리거를 주는 이중 트리거를 구성하기에는 코드 방식과 불편한 정도가 비슷하다고 느꼈습니다. 자주 사용하는 툴임도 고려했을 때 n8n이 가장 적합하다고 생각하여 n8n workflow를 구성하기로 정했습니다.

 

워크플로우 설계

워크플로우를 설계하면서 막혔던 부분이 있습니다. 백엔드의 API가 어떤게 변경이 되었는지는 알겠는데 프론트에서 어디를 수정해야할지 모른다는 문제가 있었습니다. 매번 GitHub API로 코드베이스를 탐색하는 것은 무겁고, 대신 프론트 구조 요약을 해두자니, 바뀔 때마다 업데이트를 해야하고 정확한 파일내용까지는 읽을 수 없다는 문제가 예상됐습니다.

GitHub API로 트리 조회 후 변경 필요해보이는 파일만 열어볼까? 라는 방법도 생각했는데 변경 필요해보이는 부분만 고르는 데에 또 한 스텝 더 생기고 정확도가 떨어진다고 판단했습니다. 그렇다면 모든 코드를 읽을 수 있으면서도 매번 API호출을 하지 않아도 되게 n8n내에 프로젝트를 클론해두기로 결정했습니다.

 

n8n 시도

결론적으로는 n8n을 사용하지 못했습니다.

Claude를 통해 수정 또는 생성할 내용을 찾아 파일 목록을 추출하는 데까지는 성공했으나 n8n에서 제공하는 GitHub 노드가 Edit 또는 Create file만 제공하였고, 한 번에 한 파일만 다룰 수 있다는 한계가 있었습니다.

구체적으로는 다음과 같습니다

Claude API 응답 파싱이 까다로웠다

n8n의 Claude 노드 응답은 { content: [{ type: "text", text: "..." }] } 구조로 감싸져 있어서, 순수 JSON을 요청해도 한 번 벗겨내야 했습니다. 거기에 Claude가 마크다운 코드블록이나 설명을 붙이면 파싱이 깨졌고, 이걸 처리하는 Code 노드를 따로 만들어야 했습니다.

여러 파일을 GitHub에 올리는 게 복잡했다

Claude가 반환한 files 배열을 개별 아이템으로 쪼개는 Split 노드, 기존 파일의 sha를 가져오는 Get 노드, 실제로 수정하는 Edit 노드 파일 하나 올리는 데 노드가 3개씩 필요했습니다. 특히 GitHub API의 sha(파일의 버전 해시로, 수정 시 충돌 방지를 위해 필요) 때문에 빠르게 진행하기가 어려웠습니다.

Shell 명령어를 쓸 수 없었다

결정적이었습니다. Clone으로 저장한 프론트엔드 코드를 읽으려면 grep이나 fs.readFileSync 같은 걸 써야 하는데, n8n에선 불가능했습니다. 모든 파일 접근을 GitHub API의 HTTP Request로 해야 했고, 파일 트리 조회 → 필터링 → 각 파일 내용 가져오기를 전부 노드로 만들면 10개가 넘어갔습니다.

 

Github Actions로 전환

n8n workflow가 적합할 것이라 생각했으나 막상 구현을 하니 까다로운 부분이 있었습니다. 만약 여러 방법으로 시도를 하더라도 유지보수 측면에서 적절치 않을 듯 하여 차선안이였던 GitActions를 도전하기로 했습니다. 막상 도입을 해보니 단점이라고 우려했던 오히려 장점이 된듯하여 편리함과 개발 속도에서 좋은 느낌을 받았습니다.

우선 백엔드 Push Trigger를 설정했습니다. main Branch에 Push가 일어나면 github.event.before 과 github.event.after로 변경점을 찾아 프론트엔드에게 전달했습니다. 처음에 구상했던 개발자 수락 -> 머지 부분은 제외했는데요, 직접 코드리뷰를 하는게 간단하고 정확할 것 같아서 제외했습니다.

name: Notify Frontend
on:
  push:
    branches: [main]

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Send to frontend
        env:
          GH_TOKEN: ${{ secrets.FRONTEND_DISPATCH_TOKEN }}
        run: |
          DIFF=$(git diff ${{ github.event.before }} ${{ github.event.after }})
          MSG=$(git log --oneline ${{ github.event.before }}..${{ github.event.after }} | tr '\n' ' ')
          gh api repos/sql-onlinejudge/soj-frontend/dispatches \
            -f event_type="backend-updated" \
            -f "client_payload[diff]=$DIFF" \
            -f "client_payload[commit_message]=$MSG"

 

그다음은 프론트엔드에서 backend-updated Trigger를 수신하여 이를 실행합니다. 해당 프로젝트에서는 repository_dispatch를 사용했습니다. repository_dispatch는 외부 시스템이 GitHub에 이벤트 보내서 실행하는 구조인인데요, 다른 자동화에서도 적용시켜보고 싶은 부분입니다.

name: Sync Frontend from Backend Changes
on:
  repository_dispatch:
    types: [backend-updated]
    # payload: { commit_message, diff, repo }

jobs:
  update:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4

      - name: Analyze & Update
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          BACKEND_DIFF: ${{ github.event.client_payload.diff }}
          COMMIT_MSG: ${{ github.event.client_payload.commit_message }}
        run: node scripts/sync-backend.js

      - name: Save commit msg
        run: echo "COMMIT_MSG=${{ github.event.client_payload.commit_message }}" >> $GITHUB_ENV

      - name: Create PR
        id: cpr
        uses: peter-evans/create-pull-request@v5
        with:
          branch: ai/update-api-${{ github.run_number }}
          title: "AI: 백엔드 API 변경 반영"
          body: |
            백엔드 변경에 따른 프론트엔드 자동 업데이트
            
            **커밋:** ${{ env.COMMIT_MSG }}
          commit-message: "feat: backend API 변경 반영"
            
      - name: Save PR URL
        if: ${{ steps.cpr.outputs.pull-request-url }}
        run: echo "PR_URL=${{ steps.cpr.outputs.pull-request-url }}" >> $GITHUB_ENV
            
      - name: Notify Discords
        if: ${{ env.PR_URL != '' }}
        run: |
          curl -H "Content-Type: application/json" \
            -d "{\"content\": \"🔄 **프론트엔드 자동 업데이트 PR 생성됨**\n커밋: ${{ github.event.inputs.commit_message }}\nPR: ${PR_URL}\n\n리뷰 후 머지해주세요.\"}" \
            ${{ secrets.DISCORD_WEBHOOK_URL }}

 

처음 구조는 workflow 내부에서 Claude API호출과 프롬프트 텍스트를 전부 저장하는 구조였습니다.

하지만 두가지 문제가 있었습니다.

첫번째로는 GitHub Actions에서 인라인 스크립트가 ESM/CommonJs 호환 문제를 겪어서 별도 스크립트 파일로 분리하는 게 깔끔하다고 생각했습니다.

두번째로는 사소하지만 Git Actions 파일에 모든 내용을 넣다보니 파일이 길어져서 가독성이 안 좋아졌고 유지보수하기에 어려움을 겪을 것으로 예상했습니다.

그래서 scripts/sync-backend.js 파일을 제작했습니다. 백엔드에서 API 수정이 아닌 ‘생성’의 경우에는 그에 맞는 페이지와 디자인이 필요했기 때문에 이 파이프라인에서는 수정만 다루기로 결정했습니다.

function findMatchingBrace(text, startIndex) {
  let depth = 0;
  let inString = false;
  let escape = false;
  for (let i = startIndex; i < text.length; i++) {
    const ch = text[i];
    if (escape) { escape = false; continue; }
    if (ch === '\\' && inString) { escape = true; continue; }
    if (ch === '"') { inString = !inString; continue; }
    if (inString) continue;
    if (ch === '{') depth++;
    if (ch === '}') { depth--; if (depth === 0) return i; }
  }
  return -1;
}

import fs from 'fs';
import path from 'path';

try {
  console.log('=== START ===');
  console.log('COMMIT_MSG:', process.env.COMMIT_MSG);
  console.log('DIFF length:', process.env.BACKEND_DIFF?.length);

  function getFiles(dir) {
    let results = [];
    for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
      const full = path.join(dir, f.name);
      if (f.isDirectory() && !f.name.includes('node_modules')) {
        results = results.concat(getFiles(full));
      } else if (f.name.endsWith('.ts') || f.name.endsWith('.tsx')) {
        results.push(full);
      }
    }
    return results;
  }

  const files = getFiles('src');
  console.log('Found files:', files.length);

  const fileContents = files.map(f =>
    `--- ${f} ---\n${fs.readFileSync(f, 'utf-8')}`
  ).join('\n\n');

  console.log('Total content length:', fileContents.length);
  console.log('Calling Claude API...');
    
  const prompt = `당신은 시니어 프론트엔드 개발자입니다.
백엔드 API가 변경되었습니다. 아래 diff를 분석하고 프론트엔드 코드를 수정하세요.

## 백엔드 커밋 메시지
${process.env.COMMIT_MSG}

## 백엔드 코드 변경사항 (git diff)
${process.env.BACKEND_DIFF}

## 현재 프론트엔드 코드
${fileContents}

## 분석 순서
1. diff에서 변경된 API 엔드포인트, 요청/응답 필드, 파라미터를 파악하세요
2. 프론트엔드 코드에서 해당 API를 사용하는 파일을 찾으세요
3. 타입 정의, API 호출 코드, 컴포넌트 순서로 수정하세요

## 규칙
- 기존 파일만 수정하세요. 새 파일을 만들지 마세요
- 기존 코드 스타일과 패턴을 그대로 유지하세요
- 사용자에게 노출되는 Page 파일도 수정이 필요한 경우 UX를 고려하여 수정합니다
  - 새로운 필드가 추가되었다면 사용자에게 보여주어야할 가능성이 있으니 검토하세요
- 외에도 변경이 필요한 부분은 찾아서 함께 수정하되 절대 오류를 범하지 마세요
- 변경이 필요 없는 파일은 포함하지 마세요
- API 변경과 무관한 코드는 절대 건드리지 마세요
- .github, 설정 파일, package.json 등은 절대 수정하지 마세요
- diff가 API 변경이 아니면 (CI 설정, 문서, 리팩토링 등) 빈 배열을 반환하세요
- 파일의 content는 해당 파일의 전체 내용이어야 합니다 (부분 수정 아님)

## 반환 형식 (순수 JSON만, 설명 없이)
{"files": [{"path": "src/...", "content": "전체 파일 내용"}]}

API 변경이 없으면:
{"files": []}`;

  const res = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.ANTHROPIC_API_KEY,
      'anthropic-version': '2023-06-01',
    },
    body: JSON.stringify({
      model: 'claude-sonnet-4-5-20250929',
      max_tokens: 8192,
      messages: [{ role: 'user', content: prompt }],
    }),
  });

  console.log('API status:', res.status);
  const data = await res.json();
  console.log('API response keys:', Object.keys(data));

  if (data.error) {
    console.error('API error:', JSON.stringify(data.error));
    process.exit(1);
  }

  let text = data.content[0].text;
  console.log('Response length:', text.length);
  console.log('Response preview:', text.substring(0, 300));

  text = text.replace(/```json\s*/g, '').replace(/```/g, '');
  const start = text.indexOf('{');
  const end = findMatchingBrace(text, start);
  const result = JSON.parse(text.substring(start, end + 1));
    
  console.log('Files to update:', result.files.length);

  if (result.files.length === 0) {
    console.log('No frontend changes needed. Skipping.');
  }

  for (const file of result.files) {
    fs.mkdirSync(path.dirname(file.path), { recursive: true });
    fs.writeFileSync(file.path, file.content);
    console.log('Updated:', file.path);
  }

  console.log('=== DONE ===');
} catch (e) {
  console.error('=== ERROR ===');
  console.error(e.message);
  console.error(e.stack);
  process.exit(1);
}

백엔드 action 파일, 프론트 action 파일, 스크립트 파일 총 세개의 파일로 구성을 완료하였습니다.

 

통합 테스트

물론 구현하면서 테스트를 진행했지만 실제로 적용이 얼마나 잘 되는지 궁금했습니다. 만약 몇몇 파일이 누락된다거나, 코드 수정 도중 문제가 생긴다면 직접 처리해야하고 결국 수동으로 진행하는 것과 마찬가지이기 때문입니다.

우선 백엔드 API에서 문제를 조회하면 submissionCount, solvedCount를 반환했습니다. 프론트에서는 이 두 정보를 표시하지만, 정답률 정보도 표시하면 좋을 것 같다고 생각했습니다. 백엔드와 프론트엔드 중에서는 백엔드에서 계산하는 편이 좋을 것 같아서 백엔드 작업으로 남겨두었었는데요, API변경이 필요한 부분이라 지금 시도하면 좋을 것 같아서 acceptanceRate를 response에 포함시켰습니다.

 

main Branch에 Push 후 프론트로 알림하는 트리거가 성공하고, 업데이트도 성공했습니다.

적절한 File들이 수정이 된 것을 확인했습니다.

 

로컬에서 브랜치 이동하여 실행했습니다. 브랜치 이름 뒤에 숫자는 Actions의 번호입니다.

 

정답률이 잘 표시되는 것을 확인하고 프론트 코드를 머지했습니다.

 

 

완성된 시점에서 정리해보니, 흐름 자체는 처음 계획대로 진행되었고 몇몇 부분은 진행해보며 상황에 따라 유동적으로 진행했습니다. 앞으로 더 개선한다면 변경될 수 있을 것 같아요.

[백엔드 커밋 & 푸쉬] → [Git log로 백엔드 변경사항 프론트로 전송]
→ [Trigger 발동] → [Claude 개발 및 PR오픈]
→ [변경 내용 디스코드 알림] → [개발자 코드 리뷰 및 머지]

 

앞으로의 계획 및 회고

지금처럼 간단한 수정에는 깔끔하게 동작하지만 대량의 API수정이 있는 상황에서는 변경이 적절하지 않을 가능성이 있습니다. 프롬프트 고도화나 분기 처리를 통해 처리할 수 있도록 디벨롭하려 합니다.

기능 개발을 진행하며 알게 되었는데, OpenAPI codegen으로 타입 정의까지는 자동화가 가능하단 것을 알게 되었어요. 제 프로젝트에 둘을 결합하는 방향으로 보완하여 타입 정의까지는 OpenAPI를 이용하고, 보여지는 것에 대한 수정은 Claude에게 시키도록 하면 토큰 절약이 될 것 같다고 생각이 듭니다. 만약 비슷한 자동화를 하게 된다면 다음에는 설계부터 잡고 가는 것도 좋겠습니다.

 

또한 기능을 사용하는 지금까지는 실패나 부자연스러운 수정이 0건이였지만 앞으로 이용하며 문제나 개선점을 찾아나가고 싶습니다.

결과적으로 제가 집중하고 싶은 부분에 더 집중할 수 있어서 좋았습니다. 프론트엔드 코드 작성에 드는 시간을유저 데이터 분석이나 서비스 고도화, 비지니스 목표 탐색에 사용할 수 있어서 만족스럽습니다.