안녕하세요, 오픈소스컨설팅 노은지 프로입니다. 저는 Flexible Dev 팀 소속으로, Atlassian 생태계에서 동작하는 플러그인을 개발하고 있습니다. 이번 글에서는, 백엔드 담당자가 자리를 비운 사이 프론트 개발자가 AI 도구를 활용하여 백엔드까지 포함한 기능 하나를 끝까지 배포한 이야기를 공유합니다.
Confluence 앱에서 여러 Space와 Page, 그 외 외부 링크들을 트리 구조로 한눈에 보기 쉽게 보여주는 Flexible Space Navigation 앱은, 프론트 담당자인 저와 백엔드 담당자 1분이 팀을 이뤄 개발하고 있습니다. 그런데 백엔드 담당자분이 장기휴가에 들어간 기간에, 하필 백엔드까지 손대야 하는 긴급 기능 요청이 들어왔습니다.
“스페이스별 내비게이션 고정 노출”
이 기능은 한 고객사의 VoC에서 출발했습니다. Confluence는 스페이스마다 성격이 다른데, 우리 앱이 상단에 그려주는 내비게이션 바는 권한에 따라서 “누가 보느냐”로만 결정됐습니다. 고객은 “이 스페이스에서는 무조건 특정한 내비게이션을 띄우고 싶다”고 하셨습니다. 즉 “어디서 보느냐”라는 기준을 새로 넣는 일이었고, 이건 프론트 작업만으로 끝나지 않았습니다. DB에 컬럼을 추가하고, 노출 판정 로직을 바꾸고, 기존 설치 사이트에 무중단으로 마이그레이션까지 해야 했습니다.
솔직히 부담이 컸습니다. Forge의 백엔드(SQL 스키마, Resolver 등록 규칙, 설치 트리거)는 팀에서 백엔드 담당자의 영역이었지, 제가 매일 만지던 게 아니었으니까요. 아무리 AI가 코드를 AtoZ까지 짜준다는 요즘 세상이지만, 진짜 이 코드가 괜찮은 코드인지 리뷰해줄 사람도 없었습니다. 낯선 서버 코드 위에서, 혼자, 하지만 안전하게 끝까지 배포까지 해야 했습니다. 이 글은 그 공백을 AI로 어떻게 메웠는지에 대한 기록입니다.
가장 먼저 한 일은 코드를 짜는 게 아니라 읽는 것이었습니다. 어떤 내비게이션을 보여줄지 정하는 판단이 정확히 어떤 순서로 이뤄지는지, 데이터 마이그레이션이 언제 실행되는지, 백엔드가 DB와 데이터를 어떤 형태로 주고받는지, 이것들을 모르면 한 줄도 안전하게 코드를 수정할 수 없었습니다.
AI에게 시킨 건 “요약해줘”가 아니었습니다. “이 파일들을 읽고, 노출 우선순위를 실제 코드 흐름 그대로 표로 만들고, 내가 틀리게 이해한 부분이 있으면 지적해달라”는 식으로, 검증 가능한 산출물을 요구했습니다. 그렇게 나온 결과를 코드와 대조하며 팀 위키에 인수인계 문서로 남겼습니다.
예를 들어 노출 우선순위는 이렇게 정리됐습니다.
| 순위 | 조건 | 설명 |
|---|---|---|
| 1 | 스페이스 고정 | 현재 스페이스 키가 내비의 fixedSpaceKeys에 있으면 권한이 있더라도 무시하고 그 내비게이션이 1순위 노출 |
| 2 | 조회자(viewer) | accessPermissions에 viewer 포함된 내비게이션 |
| 3 | 활성화(구독) | 사용자가 subscriptions에 포함된 내비게이션 |
| 4 | 기본 내비 | isDefault=true 인 내비게이션 |
코드를 분석하다 보니, 히스토리를 모르면 놓치기 딱 좋은 예외 케이스들도 여럿 발견할 수 있었습니다. “개인용 내비게이션 판정은 owner 필드가 기준”이라거나, “기본 내비는 스페이스 고정을 무시하고 전체 노출한다” 같은 규칙들이었죠. 이런 내용들을 사람이 먼저 파악해서 문서로 명확히 정리해 둔 덕분에, 이후 AI 도구가 엉뚱한 방향으로 코드를 짜지 않도록 확실한 가드레일을 마련할 수 있었습니다.
시작은 기획을 읽는 일이었습니다. 이 기능의 기획은 Figma에 있었는데, Figma MCP를 붙여 AI가 기획서(화면과 스펙)를 직접 읽도록 했습니다. 사람이 요약해 전달하는 대신 원본을 AI가 보게 하면, 요구사항이 옮겨지는 과정에서 새는 것을 줄일 수 있습니다. 그렇게 파악한 요구사항 위에서 브레인스토밍 → 설계 스펙 → 구현 계획으로 이어지는 워크플로를 그대로 밟았고, 그 산출물을 docs/에 커밋해뒀습니다. AI가 코드를 쏟아내기 전에 “무엇을, 왜”를 사람이 먼저 합의하는 단계입니다.
핵심은 AI에게 자유를 주되 가드레일을 명시한 것이었습니다. 저장소 루트의 CLAUDE.md에 도메인·아키텍처 규칙을 못 박아 뒀습니다. 몇 가지만 예를 들면:
@forge/bridge의 invoke()만. 직접 HTTP 금지.router.open()만 (Forge iframe은 window.open 비호환).manifest.production.yml에도 반영 (배포 직전 swap되므로 한쪽만 고치면 유실됨).owner 기준, viewer로 판정 금지.이렇게 규칙을 명시해두니, AI가 그럴듯하지만 우리 규칙을 지키지 않은 코드를 만들었을 때 근거를 갖고 되돌릴 수 있었습니다.
원칙적으로 한 스페이스에는 내비게이션을 하나만 고정할 수 있습니다. 그래서 이미 다른 내비게이션이 고정돼 있는 스페이스를 또 고정하려 하면, 설정 화면(프론트)에서 저장을 막고 서버(백엔드)에서도 한 번 더 거부해 중복 고정을 이중으로 차단합니다. 그럼에도 데이터 이관이나 동시 편집 같은 예외적인 경로로 같은 스페이스에 둘이 고정되는 일이 혹시 생기더라도, 화면이 그때그때 달라지지 않도록 “가장 최근에 수정된 것을 보여준다”는 최후의 안전망을 하나 더 두었습니다.
특정 스페이스에 고정한 내비게이션은, 그 스페이스에 들어가면 권한과 무관하게 무조건 1순위로 노출됩니다. 문제는 이렇게 만든 내비가 정작 다른 스페이스나 홈 화면에서도 (조회 권한이 있는 사용자에게) 노출될 수 있다는 점입니다. 그래서 고정 스페이스가 지정된 내비게이션은 일반 노출 경로에서 아예 빼서, “그 스페이스에서만” 보이도록 했습니다.
판정에는 스페이스 키(fixedSpaceKeys)만 있으면 되지만, 화면에는 사람이 읽을 이름을 보여줘야 합니다. 그릴 때마다 Confluence Space API로 이름을 조회하면 느리고 호출량도 늘어납니다. 그래서 저장 시점에 이미 알고 있는 이름을 키와 같은 DB 컬럼(fixedSpaceNames)에 나란히 복사해 저장했습니다(denormalized). 읽을 때 API를 다시 안 불러 화면이 빨라지는 대신, 스페이스 이름이 바뀌면 저장된 값은 다음 저장 때 갱신됩니다. 이 부분은 판정이 아닌 표시용 라벨이라 영향은 미미합니다.
가장 까다로운 건 이미 쓰고 있는 고객들의 DB를, 서비스를 멈추지 않고 바꾸는 일이었습니다.
새 기능을 위해 DB 테이블에 컬럼 두 개를 추가해야 했는데, 문제는 우리 앱의 테이블 생성 코드가 “테이블이 없으면 새로 만든다”는 방식이라는 점이었습니다. 이미 앱을 쓰고 있는 고객은 테이블이 벌써 있으니, 이 코드는 아무 일도 하지 않습니다. 즉 새 컬럼이 영영 안 생깁니다. 게다가 앱에는 “이 사이트는 이미 설정 끝남”이라는 표시가 있어서, 설정 루틴 자체를 통째로 건너뛰어 버립니다. 이대로면 신규 설치 고객만 새 기능을 쓰고, 기존 고객은 못 쓰게 됩니다.
그래서 기존 설정 루틴과 완전히 분리된, 별도의 “컬럼 추가 전용” 단계를 만들었습니다. 이 단계는 사이트마다 딱 한 번만 실행되어 새 컬럼을 추가하고, 혹시 이미 컬럼이 있어도 에러 없이 그냥 넘어가도록(안전하게 여러 번 돌려도 문제없게) 만들었습니다. 덕분에 배포 후 고객이 앱을 열면 그 순간 새 컬럼이 만들어집니다. 여기에 버전 번호를 붙여 두어서, 나중에 또 컬럼을 추가할 일이 생기면 번호만 하나 올려 같은 방식을 그대로 재사용할 수 있게 했습니다.
정리하면, 이 기능을 만드는 동안 AI를 아래처럼 단계적으로 활용했습니다.
1번은 앞 절에서 다뤘으니, 백엔드 리뷰어의 공백을 메운 핵심인 2~4번을 이어서 풀어보겠습니다.
여기가 이 프로젝트의 진짜 승부처였습니다. 제 서버 코드를 리뷰해줄 사람이 없었습니다. 그 공백을 사람 대신 테스트로 메우기로 했습니다.
먼저 “어떤 내비를 보여줄지” 같은 까다로운 판단 로직은, 외부에 의존하지 않고 입력만 주면 결과가 딱 정해지는 작은 함수로 분리해냈습니다. 그리고 테스트를 먼저 쓰고 그 테스트를 통과하도록 구현하는 방식(TDD)으로 만들었습니다. 스페이스 중복 검사(findSpaceConflicts), 게스트 판정(resolveIsGuest) 같은 함수들이 그렇습니다. 이렇게 떼어두면 무거운 Forge 환경을 띄우지 않고도 로직만 빠르게 검증할 수 있습니다.
그 위에 E2E 테스트를 얹었습니다. Forge 앱은 원래 Confluence에 배포해야 눌러볼 수 있어서, 회귀 QA 한 번에 5–10분씩 걸렸습니다(배포 대기 + 진입 + 클릭). 이걸 로컬 Playwright로 대체했습니다. @forge/bridge를 가짜 모듈로 바꿔치기해 Confluence 의존을 끊고, 스페이스 검색·고정·저장, 복사 시 고정 해제, 기본 내비 규칙 같은 플로우를 데모 시나리오로 자동화했습니다. 신규 추가된 의존성은 Playwright 하나였고, 새 빌드 파이프라인 없이 기존 dev 서버를 재사용했습니다.
결과적으로 회귀 검증이 분 단위에서 약 4초로 줄었습니다. 하지만 더 중요한 건 속도가 아니라 신뢰였습니다. 리뷰어가 없으니 “이 테스트가 진짜 버그를 잡긴 하나?”를 의심해야 했습니다. 그래서 테스트를 테스트하기로 했습니다. mock에서 “복사 시 고정 해제” 로직을 일부러 지워 버그를 재주입하자, 해당 E2E 테스트가 바로 실패(Error)로 바뀌었습니다. 그리고 원복하니 다시 전부 통과됐습니다. 가드가 실제로 동작한다는 증거를 눈으로 확인한 셈입니다.

마지막으로 이 테스트 안전망을 강제했습니다. 훅은 저장소 안(.githooks/pre-push)에 버전 관리되고, 팀원이 npm install만 하면 자동으로 켜집니다. 그래서 “저는 훅을 안 깔았는데요” 같은 구멍이 생기지 않습니다.
git push를 하면 세 단계가 순서대로 돕니다.
이 중 1번 게이트의 핵심 로직만 추리면 이렇습니다.
# .githooks/pre-push (핵심만 발췌)
# dev에서 갈라진 뒤 바뀐 파일 목록
base=$(git merge-base origin/dev "$local_sha")
changed=$(git diff --name-only "$base" "$local_sha")
for app in nav-config-page config-modal switch-modal top-navigation; do
# 이 앱의 기능 코드가 바뀌었나? (.test/.spec 파일은 제외)
app_changed=$(echo "$changed" | grep -E "^static/$app/src/" | grep -vE '\.test\.|\.spec\.')
# 이 앱의 E2E 스펙은 바뀌었나?
spec_changed=$(echo "$changed" | grep -E "^static/$app/e2e/specs/")
# 기능은 바뀌었는데(O) 스펙은 안 바뀌었으면(X) → push 차단
if [ -n "$app_changed" ] && [ -z "$spec_changed" ]; then
echo "🛑 [$app] 기능이 바뀌었는데 E2E 스펙 변경이 없습니다."
exit 1
fi
done
# 게이트 통과 후: mock 계약 타입체크 → E2E 실행 (하나라도 실패하면 exit 1)
npm run typecheck:e2e || exit 1
npm run e2e || exit 1
즉 “기능 변경 O” 그리고 “스펙 변경 X” 두 조건이 동시에 참일 때만 막습니다.
그럼 “기능은 그대로 두고 스타일만 손봤을 때”는 어떻게 될까요? 이 게이트는 파일 경로만 보는 휴리스틱이라, 스타일 파일도 앱 src/ 안에 있으면 “기능 변경”으로 간주해 스펙을 요구합니다. 의미까지 똑똑하게 구분하지는 못하는 것이죠. 대신 빠져나갈 구멍을 명시적으로 열어뒀습니다. 정말 스펙이 필요 없는 변경이면 SKIP_E2E_GATE=1 git push로 게이트만 건너뛰고, 긴급 상황이면 git push --no-verify로 훅 전체를 건너뜁니다. 몰래 지나가는 게 아니라, 개발자가 “이건 예외다”라고 의식적으로 선언하게 만든 것입니다.
결국 사람 리뷰어가 물어봤을 “테스트 추가했어요?”를, 훅이 대신 물어보는 셈입니다.
읽고, 설계하고, 테스트로 방어망까지 세운 끝에 기능은 무사히 프로덕션에 배포됐습니다. 스페이스 고정 노출은 의도대로 동작했고, 기존 설치 사이트들도 배포 직후 첫 호출 시점에 새 컬럼을 무중단으로 받았습니다. 첫 커밋부터 프로덕션 배포까지 걸린 시간은 영업일로 7일. 프론트 개발자 한 명이 DB 스키마·Resolver·마이그레이션·E2E까지 포함한 기능을, 백엔드 리뷰어 없이 그 안에 끝냈습니다.
이 일에는 처음부터 데드라인이 하나 더 있었습니다. 백엔드 담당자가 언젠가 복귀한다는 것. 그가 돌아왔을 때 “내 코드가 왜 이렇게 바뀌었지?”로 시간을 버리지 않게 하는 것도 제 책임이었습니다.
그래서 세 가지를 남겼습니다. 첫째, 제 의도를 코드에 박제했습니다. 제가 함부로 판단할 수 없는 부분은 코드를 지우지 않고 TODO(ABQ-XXX) ... 복귀 후 재검토 주석과 함께 비활성화만 해뒀습니다. 원래 담당자의 의도를 존중하고 결정을 그에게 넘기는 것입니다. 둘째, 과설계를 참았습니다. 향후 요구사항으로 나올 수 있는 “특정 스페이스에서 내비 숨김”까지 가면 별도 매핑 테이블이 맞지만, 지금 기능엔 임베드 배열로 충분합니다. 미리 테이블을 만드는 건 과설계라 판단하고, “그 시점에 전환하라”는 권고만 문서에 남겼습니다. 셋째, 핸드오버 문서를 위키에 완성했습니다. DB 스키마 변경, 백엔드 구조·로직, 그리고 “이 설계가 최적인가”를 효율성·정합성·확장성 관점에서 스스로 검증한 절과, 알려진 비용(프론트/백 선택 로직이 두 곳에 미러링되어 있으니 규칙 변경 시 둘 다 고칠 것)까지 적었습니다.
AI는 기존 코드를 읽어 새로운 기능 추가를 위한 개발 방향을 잡고 그 지도를 그리는 일, 테스트 mock 데이터와 스펙 초안을 작성하는 일, 반복적인 회귀 검증을 놀라우리만치 빠르게 처리해 주었습니다. 그 덕분에 프론트 개발자 한 명이 DB·Resolver·마이그레이션까지 포함한 기능을 혼자 단시간 내에 배포할 수 있었습니다.
그러나 AI가 인간을 대체하지 못한 것도 분명히 존재했습니다. “개인용은 owner로 판정한다”와 같은 도메인 규칙, “이건 과설계이니 참자”는 판단, “이 마이그레이션은 원 담당자에게 결정을 넘기자”는 책임의 소재 — 이건 전부 사람이 정하고 사람이 문서화해야 했습니다. AI는 그 가드레일 안에서 제 역할을 충실히 빠르게 해낼 수 있었습니다.
공백을 메운 건 AI가 아니라, AI에게 무엇을 검증하게 할지 아는 사람과 그가 남긴 테스트와 문서였습니다.
<프론트엔드 관련 포스트 더 보기>
📌State, 슬기롭게 관리하기 (1)
📌State, 슬기롭게 관리하기 (2)
📌dangerouslySetInnerHTML을 아시나요?