QA 리포터 제작기(딸깍 한 번에 티켓 발급부터 AI 수정까지)
딸깍으로 QA 끝내보기
얼마 전에 Jira에 티켓이 올라오면 Claude가 읽고 PR 초안까지 써주는 워크플로우를 만들어서 팀에 붙여뒀어요. 기능 버그는 이걸로 꽤 잘 고쳐졌어요.
그런데 UI 티켓만 들어오면 매번 엉뚱한 곳을 고쳤어요. 티켓에 담긴 건 "여기 간격이 좀 이상한데요?!"라는 문장이랑 AI가 이해하기는 힘든 스크린샷뿐이니까요...
그러다 같은 고민을 먼저 하신 인호준님의 링크드인 글을 봤어요. 해결책이 아주 명쾌했어요.
스크린샷을 설명하려 들지 말고, 클릭한 요소의 정보를 그대로 긁어서 넘기자.
"화면을 클릭한 걸, AI가 바로 고칠 수 있는 티켓으로 바꿀 수 없을까?"
이번 글에서는 화면에서 요소를 클릭해 메모만 남기면 AI가 상황을 요약해 Jira 티켓까지 써주는 QA 리포터를 만든 과정을 공유해보려 해요.
우선 실제 동작과정 결과물을 보여드릴게요.
요소 클릭 → 메모 → 발행, 그리고 마지막에 AI가 써준 티켓까지 이어지는 전체 흐름이에요.
처음엔 크롬 확장 프로그램도 후보였어요. 하지만 두 가지 이유로 앱 안에 직접 오버레이를 띄우는 쪽을 택했어요.
- React 내부 정보에 접근하기 쉬움: 클릭한 DOM 요소가 어떤 컴포넌트인지 알아내려면 React fiber에 접근해야 해요. 확장 프로그램의 content script는 페이지와 격리된 컨텍스트에서 돌기 때문에, 페이지 안의 자바스크립트 객체를 보려면 스크립트를 페이지 쪽으로 주입하는 단계를 한 번 더 거쳐야 해요. 앱 코드 안에서 돌면 이 단계 자체가 없어요.
- 배포가 간단함: 확장 프로그램은 설치·업데이트가 번거로운데, 앱에 dev 전용으로 심어두면 QA·디자이너가 별도 설치 없이 바로 써요. QA는 "지금 보고 있는 이 화면"에서 바로 리포트하는 게 핵심이라, 설치라는 한 단계가 생기는 것만으로도 안 쓰게 되거든요.
그래서 dev 빌드에서만 동작하는 오버레이로 만들었어요. 화면 위 요소를 클릭하면 메모를 남기고, 그 메모들을 모아 Jira로 발행하는 구조예요.
요소에 호버하면 크기·색상·컴포넌트 이름이 바로 떠요. (SettingItem · 62×27 · gray-1000)
기준은 하나였어요. AI가 추측하지 않고 근거로 말할 수 있을 만큼 긁어모으는 것. "버튼이 작아 보인다"가 아니라 "폰트가 14px이다"라고 쓰게 하려면, 그 14px이 티켓에 들어 있어야 하니까요.
그래서 요소를 클릭하면 다음을 캡처해요.
- selector:
div > div:nth-of-type(2)같은 DOM 경로 - computed style:
display,flex관련 값,gap,padding,font-size,color등 - 디자인 토큰: 적용된 클래스와 색상값을 토큰명으로
- 컴포넌트 이름:
ProductCard처럼 실제 React 컴포넌트명 - 사용자 컨텍스트: 화면(URL), 로그인 계정, 기기 종류(iOS / 안드로이드 / 데스크탑), 뷰포트 크기
특히 사용자 컨텍스트가 담기기 때문에 "어떤 계정이에요?", "안드로이드예요 iOS예요?" 되묻는 왕복이 사라져요. 캡처 시점에 이미 담겨 있으니까요.
그리고 이 중에서 제일 까다로웠고, 동시에 제일 중요했던 게 컴포넌트 이름이에요.
selector만 있어도 화면에서의 위치는 찾을 수 있어요. 그런데 div > div:nth-of-type(2) > span 같은 경로는 코드베이스로 이어지지 않아요. 저 문자열을 통해서 실제 컴포넌트를 AI가 유추하려면 시간이 오래걸리거나, 아예 불가능 했어요.
하지만 컴포넌트 이름은 달라요.
컴포넌트 이름은 화면에서 코드베이스로 건너가는 다리예요. 이게 없으면 나머지 정보(selector·computed style)가 아무리 정밀해도 사람이든 AI든 고칠 파일을 바로 캐치하기는 힘들어요. 그래서 다른 건 몰라도 이 이름만큼은 꼭 필요했어요.
그럼 클릭한 DOM 요소에서 컴포넌트 이름을 어떻게 알아내지?
React는 렌더링한 DOM 요소에 내부적으로 fiber 노드를 연결해둬요. DOM 요소의 프로퍼티를 보면 __reactFiber$... 같은 키가 붙어 있어요.
// dom.ts
const getFiber = (el: Element) => {
const key = Object.keys(el).find(
(k) =>
k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
)
return key ? (el as Record<string, any>)[key] : null
}
이 fiber 노드는 return 프로퍼티로 부모 fiber를 가리켜요. 그래서 fiber를 타고 위로 올라가면서 컴포넌트 이름을 모으면 돼요.
// dom.ts
export const getComponentPath = (el: Element): string => {
let fiber = getFiber(el)
const names: string[] = []
while (fiber && names.length < 3) {
const name = fiberName(fiber.type)
// PascalCase 인 것만 = 실제 컴포넌트 (소문자 태그는 제외)
if (name && /^[A-Z]/.test(name)) {
names.push(name)
}
fiber = fiber.return
}
return names.join(' < ')
}
fiber.type이 컴포넌트의 실체인데, 여기서 이름을 꺼내는 게 생각보다 조금 번거로워요. 일반 함수 컴포넌트는 type.name이나 type.displayName으로 바로 나오지만, forwardRef나 memo로 감싼 컴포넌트는 한 겹 더 들어가야 해요.
// dom.ts
const fiberName = (type: unknown): string | null => {
if (typeof type === 'function') {
return type.displayName ?? type.name ?? null
}
// forwardRef / memo 래퍼는 안쪽을 한 번 더 들여다봐야 해요
if (type && typeof type === 'object') {
return type.displayName ?? type.render?.name ?? type.type?.name ?? null
}
return null
}
여기서 한 가지 알게 된 점이 있어요. 예전 React에는 _debugSource라는 게 있어서 fiber에서 파일 경로와 줄 번호까지 바로 뽑을 수 있었는데, React 19에서 제거됐어요. (React DevTools조차 이 이후로는 컴포넌트 스택에서 소스 위치를 역산하는 방식으로 갈아탔어요.)
그래서 파일 위치 대신 컴포넌트 이름을 가지고 레포에서 찾는 방식으로 방향을 잡았어요. 어차피 AI에게 넘길 거라면 정확한 줄 번호보다 이름 하나가 더 쓸모 있기도 했고요. 이름만 있으면 AI가 알아서 파일을 찾아 읽으니까요.
아무튼 이렇게 해서 로컬에서 클릭하면 ProductCard 같은 이름이 잘 떴어요.
dev 환경에 배포하고 같은 화면을 눌러봤더니, 컴포넌트 이름이 이렇게 떴어요.
로컬 ProductCard < ProductList < HomePage
배포 t < e < r
처음엔 환경변수를 의심했어요. dev 빌드를 구분하는 NEXT_PUBLIC_NEXT_ENV 값이 안 들어간 줄 알았는데, 확인해보니 멀쩡히 등록돼 있었어요.
원인은 minify였어요. 프로덕션 빌드에서 코드가 minify 되면서 함수 이름이 ProductCard → t 로 망글링된 거예요. 로컬 next dev는 원래 minify를 안 하니까 이름이 살아 있었던 거고요.
그래서 dev 빌드에서만 minify를 꺼야 했어요. 저희는 빌드를 turbopack으로 돌리고 있어요.
// package.json
"build": "next build --turbopack"
turbopack 빌드의 minify는 experimental.turbopackMinify로 제어해요.
// next.config.mjs
// 개발환경(QA환경) 에서만 해당 옵션 끄기..!
experimental: { turbopackMinify: !isDevelopment },
물론 번들크기 증가는 상당히 슬픈 부분이기는 하지만, 실제 사용자가 보는 환경은 아니었기에 엄청나게 크리티컬한 영향은 아니었어요. (어쩔 수 없는 트레이드오프)
제보자는 "이 폰트 크기가 16px이어야 해요" 정도로만 남겨요. 그럼 AI가 클릭 시점에 캡처된 정보를 근거로 "요약 / 추정 원인 / 수정 제안" 구조의 티켓 본문을 써줘요.
computed style에 font-size: 14px가 잡혀 있으니 "폰트가 14px이라 작아 보인다"처럼 수치를 짚고, 컴포넌트 이름이 있으니 어느 파일을 봐야 하는지도 함께 적혀요.
짧은 한마디가 개발자가 바로 착수할 수 있는 티켓이 되는 거예요.
실제로 발행된 티켓이에요. "1. 높이가 62px여야 해요 2. 텍스트 컬러가 gray-900이어야 해요" 정도의 짧은 메모가,

이렇게 문제 · Computed Style 근거 · 추정 원인 · 수정 제안까지 붙은 티켓으로 정리돼요.

py-4가 실제로 몇 px인지, 어떤 클래스를 어떻게 바꾸면 되는지까지 짚어줘요.
모델은 Claude Haiku 4.5를 썼어요. 저렴한 건 둘째 치고, 여러 모델을 견줘봤을 때 가장 빨랐어요. 제보자가 발행 속도 때문에 답답함을 느끼면 아예 안 쓰게 되니까, 정확도보다 속도를 먼저 봤어요. 어차피 AI가 하는 일은 이미 캡처된 정보를 정리하는 쪽에 가까워서 모델의 정확도는 크게 상관은 없었어요.
이름·AI 말고도 QA 흐름을 매끄럽게 하려고 몇 가지를 더 붙였어요.
- 담당자 자동 추천: 컴포넌트 이름으로 레포에서 파일을 찾고, git 이력에서 그 파일을 최근에 수정한 사람을 담당자로 추천해요. fiber에서 뽑은 이름이 여기서도 쓰여요.
- 보고자 자동 등록: 이메일을 한 번만 적어두면 그 이메일로 보고자(reporter)가 등록돼요.
- 에픽 선택: 발행할 에픽을 검색해서 그 하위로 티켓이 달려요.
- 스크린샷 첨부 · 개별/일괄 발행: 쌓인 QA 항목을 티켓 하나로 묶거나 각각 따로 발행할 수 있어요.
- 색상 토큰 표시: 호버하면 색을
#1a1a1a같은 헥사코드가 아니라gray-1000처럼 디자인 토큰명으로 보여줘요. 보이지 않는 probe 요소에 디자인 시스템의--color-*변수를 하나씩 적용해보고, 브라우저가 계산한 색값을 역으로 매핑하는 방식이에요.
Jira에 버그 티켓이 올라오면 자동으로 GitHub 이슈로 옮기고, 그 내용을 근거로 Claude가 PR 초안까지 작성해주는 워크플로우. 처음에 UI 티켓만 만나면 엉뚱한 곳을 고친다고 했던 그 워크플로우예요.
디자이너·PM이 버그를 설명하는 말과, 개발자가 코드를 고칠 때 쓰는 말은 결이 꽤 달라요. 그런데 개발자가 쓰는 언어는 AI가 이해하는 언어와 꽤 가까워요. 컴포넌트 이름, 속성, 수치 같은 것들이요. 정작 그 정밀한 정보가 티켓에 없으니 AI가 PR을 써도 엉뚱한 곳을 고치는 일이 잦았던 거예요.
QA 리포터가 메우는 게 바로 이 간극이에요. 리포팅하는 시점에 컴포넌트와 수치가 이미 티켓에 담기니까, 디자이너가 클릭 한 번으로 남긴 메모가 AI가 바로 고칠 수 있는 정밀한 입력으로 바뀌어요.
흐름으로 보면 이렇게 돼요.
화면 클릭 → (AI 요약) Jira 티켓 → GitHub 이슈 → Claude가 PR 작성
디자이너의 "여기 이상해요"가, 사람 손을 거의 안 거치고 PR 초안까지 이어지는 그림이에요.