2026-03-22·[기술 문서]
React 컴포넌트 인스펙터와 QA 자동화 도구 만들기
15개의 디자인 QA 티켓을 처리하면서 반복되던 컴포넌트 찾기와 QA 기록 작업을 크롬 익스텐션으로 줄인 과정
우리 회사에는 QA 직군이 없다. PM도 없고 기획자도 없다. 디자이너가 피그마에 QA 피드백을 올리면, 개발자가 직접 Jira 티켓을 확인하고, 코드를 찾고, 수정하고, 스크린샷을 찍고, Jira를 업데이트한다. 전부 개발자 몫이다.
디자인 팀으로부터 15개의 QA 피드백을 한꺼번에 받았을 때, 처음 몇 개는 그냥 했다. 그런데 다섯 번째쯤부터 슬슬 지겨워졌고, 열 번째쯤엔 솔직히 이게 내 일이 맞나 싶었다. 코드 수정보다 컴포넌트 찾는 과정, 스크린샷 찍는 과정이 더 오래 걸리는 날도 있었다. 그 과정에서 도구 두 개를 만들게 됐다.
가장 오래 걸리는 건 "이게 어떤 컴포넌트야?"였다
QA 직군이 있는 회사라면, QA 담당자가 버그를 발견하고 재현 단계와 환경 정보를 정리해서 Jira에 올린다. 개발자는 잘 정리된 티켓을 받아서 코드만 고치면 된다.
우리는 다르다. 디자이너가 "여기 간격 좀 다른 것 같아요"라고 피그마에 코멘트를 남기면, 개발자가 모든 과정을 직접 해야 한다:
- Jira 티켓 열기 → 피그마 링크 확인
- 브라우저에서 실제 페이지 확인
- "어떤 컴포넌트에서 이 UI가 나오지?" (가장 오래 걸리는 부분)
- React DevTools 열기 → 컴포넌트 트리 탐색
- 파일 경로 찾기 → IDE에서 열기
- 수정하기
- 스크린샷 찍기
- Jira 업데이트
특히 3번 단계가 문제였다. 디자인은 UI 스크린샷만 있고, "이게 ProductInfo 컴포넌트인지 ProductDetails인지 ProductCard인지" 판단하는 데 5~10분이 소모된다.
그리고 이제는 AI 어시스턴트(Claude Code, Cursor 등)에게 "이 부분 수정해 줄래?"라고 던질 수 있는 시대다. 그런데 React DevTools의 정보는 AI에게 바로 던져주기엔 형식이 맞지 않는다.
필요한 건: 브라우저에서 요소를 클릭 한 번에 → "Component: ProductInfo > PriceSection" + 파일 경로 → 복사 → Claude Code에 붙여넣기
만든 것 하나: React Component Inspector
한 클릭으로 컴포넌트 경로 복사
만든 크롬 익스텐션의 동작 방식:
- 익스텐션 아이콘 클릭 → 검사 모드 ON
- UI 요소에 마우스 오버 → 요소 하이라이트 + 컴포넌트명 미리보기
- 클릭 → 패널에서 컴포넌트 트리 + 파일 경로 표시
- "Copy to Clipboard" → 포맷된 텍스트 복사
- Claude Code에 붙여넣기 → "이 컴포넌트 수정해 줄래?"
복사되는 포맷
Component: GlobalLayout > LocalMarketLayout > ProductInfo
File: ProductInfo.tsx
간단하지만 AI가 이해하기 좋은 형식이다. 경로도 명확하고, 파일명도 있어서 AI가 바로 코드를 찾을 수 있다.
Manifest V3의 두 세계 문제
크롬 익스텐션은 세 가지 실행 환경이 있다. 처음에 이 구조가 생소해서 좀 헤맸는데, 결국 postMessage로 두 세계를 연결하는 패턴 하나로 정리됐다.
- Content Script: 페이지와 같은 DOM에 접근, 하지만 __reactFiber$ 같은 React 내부에 접근 불가
- Page Script (MAIN world): React 내부에 접근 가능하지만, 익스텐션 API 사용 불가
- Background Service Worker: 익스텐션 설정/저장소 관리
Manifest V3에서는 Content Script와 Page Script가 **완전히 격리된 세계(isolated world)**다. postMessage로만 통신할 수 있다.
구현 순서:
- Content Script가 페이지 로드 시 Page Script를 주입
- Page Script에서 window.__REACT_INSPECTOR_BRIDGE__ 글로벌 객체 생성
- 사용자가 "검사 모드" 클릭 → Content Script에 메시지 전송
- Page Script가 마우스 움직임 추적 → __reactFiber$ 접근해서 컴포넌트 정보 추출
- Page Script에서 Content Script로 정보 전송 (postMessage)
- Content Script가 popup 패널에 표시
// page-script.js (MAIN world - React 내부 접근 가능)
function getComponentInfo(element) {
// DOM 요소에서 __reactFiber$ 키를 찾는다
const fiberKey = Object.keys(element).find(
k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
);
const fiber = fiberKey ? element[fiberKey] : null;
if (!fiber) return null;
let node = fiber;
const tree = [];
while (node) {
if (node.type && typeof node.type === 'function') {
const name = node.type.displayName || node.type.name;
if (name && !isInternalComponent(name)) {
tree.unshift(name);
}
}
node = node.return;
}
return {
component: tree.join(' > '),
file: inferFileFromComponent(tree[tree.length - 1])
};
}
// Content Script와 통신
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data.type === 'INSPECT_ELEMENT') {
const target = document.elementFromPoint(event.data.x, event.data.y);
const info = getComponentInfo(target);
window.postMessage({ type: 'COMPONENT_INFO', data: info }, '*');
}
});
React 19에서의 문제: _debugSource 제거
React 18까지는 _debugSource 필드에 파일 경로가 자동으로 들어가 있었다. 하지만 React 19에서는 제거됐다.
대신 컴포넌트명 기반 추론으로 해결했다:
function inferFileFromComponent(componentName) {
// ProductInfo -> ProductInfo.tsx
// PriceSection -> PriceSection.tsx (또는 components/PriceSection.tsx)
return `${componentName}.tsx`;
}
완벽하지는 않지만, 대부분의 경우 파일명 = 컴포넌트명이라는 관례 덕분에 작동한다.
60개 이상의 내부 컴포넌트 필터링
React와 Next.js의 내부 컴포넌트들(ClientComponent, ServerComponent, Suspense, Fragment 등)은 UI 검사할 때 노이즈다. 이들을 필터링하는 블랙리스트를 유지했다:
const INTERNAL_COMPONENTS = [
'Fragment',
'Suspense',
'Memo',
'ClientComponent',
'ServerComponent',
'ContextProvider',
'ContextConsumer',
'Profiler',
'Lazy',
// ... 60개 더
];
function isInternalComponent(name) {
return INTERNAL_COMPONENTS.includes(name);
}
기존 도구와 비교하며 정한 방향
비슷한 문제를 푸는 도구들은 이미 있었다. React DevTools는 컴포넌트 트리를 보는 데 가장 안정적이고, LocatorJS나 click-to-component 계열 도구는 소스 위치로 이동하는 경험이 좋다. React Grab처럼 AI에게 넘기기 좋은 형태를 목표로 하는 도구도 있었다.
내가 원한 건 조금 달랐다. 프로젝트에 Babel plugin이나 npm 패키지를 추가하지 않고, 크롬 익스텐션만 켠 상태에서 컴포넌트 경로를 복사하고 싶었다. 대신 이 선택에는 분명한 트레이드오프가 있다. 소스 위치를 빌드 단계에서 심어두는 도구보다 정확도가 떨어질 수 있고, React 내부 Fiber 구조나 번들러 출력 형식에 더 많이 의존한다.
그래서 이 도구의 목표는 "가장 정확한 소스 점프"가 아니라 "QA 중에 AI에게 바로 넘길 수 있는 컴포넌트 힌트를 빠르게 얻는 것"에 더 가깝다.
두 번째 도구: QA Master
Inspector가 "어떤 컴포넌트인지 찾기"를 해결했다면, QA Master는 QA 프로세스 자체를 자동화하는 도구다.
QA 직군이 없는 우리 같은 환경에서는 개발자가 직접 테스트하면서 버그를 발견하고, 재현 단계를 정리하고, 스크린샷을 찍고, Jira에 올려야 한다. 이 과정이 코드 수정보다 더 오래 걸릴 때가 많다. QA Master는 이 "발견 → 기록 → 보고" 사이클을 레코딩 한 번으로 끝낸다.
workflow:
- 레코딩 시작 → 페이지에서 클릭/입력 반복
- 버그 발견 → 스크린샷 + API 로그 + DOM 정보 자동 수집
- 어노테이션 (선택) → 펜/화살표/텍스트로 마킹
- Jira 연동 → 한 번의 버튼 클릭으로 티켓 자동 생성
기능: 4가지 레이어
1. 레코딩 & 자동 캡처
사용자가 브라우저에서 테스트하는 동안:
- 마우스 이동 추적
- 클릭/입력 감지
- 자동으로 스크린샷 촬영 (매 이벤트마다 또는 주기적)
// content-script.js — 유저 액션 감지 후 background에 캡처 요청
let lastCaptureTime = 0;
document.addEventListener('click', (e) => {
const now = Date.now();
if (now - lastCaptureTime > 500) {
chrome.runtime.sendMessage({
type: 'CAPTURE_SCREENSHOT',
action: `클릭: ${e.target.textContent?.slice(0, 30)}`,
url: location.href,
timestamp: new Date().toISOString()
});
lastCaptureTime = now;
}
});
// background.js — chrome API로 실제 스크린샷 캡처
chrome.runtime.onMessage.addListener((msg, sender) => {
if (msg.type === 'CAPTURE_SCREENSHOT') {
chrome.tabs.captureVisibleTab(sender.tab.windowId, { format: 'png' }, (dataUrl) => {
recordings.push({ image: dataUrl, ...msg });
});
}
});
2. API 레코딩
네트워크 요청도 자동 기록:
// fetch monkey-patch
const originalFetch = window.fetch;
window.fetch = function(...args) {
const startTime = Date.now();
return originalFetch.apply(this, args)
.then(response => {
const duration = Date.now() - startTime;
const [url, options] = args;
store.apiLog.push({
method: (options?.method || 'GET'),
url: url,
status: response.status,
duration: duration,
timestamp: new Date().toISOString()
});
return response.clone();
});
};
// XMLHttpRequest monkey-patch도 비슷하게
API 로그는 "어떤 API 호출이 실패했는가"를 QA 리포트에 포함시킬 때 중요하다.
3. DOM 인스펙트
Inspector의 로직을 재사용해서 현재 페이지의 DOM 구조와 React 컴포넌트 정보를 수집:
function capturePageInfo() {
return {
url: window.location.href,
title: document.title,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
components: extractVisibleReactComponents(),
domStructure: serializeDOMTree()
};
}
4. 어노테이션 도구
마우스로 버그 위치를 마킹하는 기능:
- 펜: 자유 드로잉 (색상 선택 가능)
- 화살표: 특정 요소 지시
- 사각형: 영역 강조
- 텍스트: 메모 추가
// 캔버스 기반 드로잉
const canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
function drawArrow(fromX, fromY, toX, toY, color) {
const headlen = 15;
const angle = Math.atan2(toY - fromY, toX - fromX);
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.stroke();
// 화살표 헤드
ctx.beginPath();
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
}
Jira OAuth 2.0 연동
익스텐션 설정 페이지에서 Jira 계정 연동:
async function createJiraIssue(report) {
const token = await chrome.storage.local.get('jiraToken');
const issue = {
fields: {
project: { key: 'QA' },
issuetype: { name: 'Bug' },
summary: report.title,
description: formatDescription(report),
customfield_XXX: 'Design' // 라벨
}
};
const response = await fetch('https://your-domain.atlassian.net/rest/api/3/issue', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(issue)
});
return response.json();
}
리포트 포맷
자동 생성되는 Jira 티켓 설명:
## 환경 정보
- 브라우저: Chrome 124.0
- 뷰포트: 1920x1080
- URL: https://example.com/products/123
- 시간: 2025-03-22 14:32 UTC+9
## 재현 단계
1. 상품 페이지 방문
2. "Add to Cart" 클릭
3. 수량 입력 필드에 "999" 입력
4. 확인 버튼 클릭
## API 로그
- POST /api/cart (200, 145ms)
- GET /api/product/123 (200, 87ms)
- POST /api/order (500, 2234ms) ❌ ERROR
## 버그 설명
"Add to Cart" 이후 장바구니 개수가 업데이트되지 않음. 콘솔에 에러 메시지 없음.
## 스크린샷
[4개의 자동 캡처된 이미지]
기존 도구와의 비교
| 도구 | 가격 | API 레코딩 | DOM 정보 | React 컴포넌트 | Jira 연동 |
|---|---|---|---|---|---|
| Jam.dev | $50/mo | ✅ | ✅ | ❌ | ✅ |
| Marker.io | $39/mo | ✅ | ⚠️ (기본) | ❌ | ✅ |
| QA Master Tool (내 도구) | 무료 | ✅ | ✅ | ✅ | ✅ |
장점:
- 무료 + 오픈소스
- Inspector 로직 재사용으로 React 컴포넌트 정보 자동 포함
- DOM 구조까지 저장
- Jira 직접 연동
두 도구의 공통 구조
Inspector 로직 재사용
두 도구 모두 React 컴포넌트 정보를 추출하는데, 로직을 공유했다:
// shared/react-inspector.js
export function extractComponentInfo(element) {
const fiber = findReactFiber(element);
return {
path: buildComponentPath(fiber),
file: inferFileName(fiber),
props: extractProps(fiber)
};
}
// react-component-inspector/page-script.js
import { extractComponentInfo } from './shared/react-inspector.js';
// qa-master-tool/page-script.js
import { extractComponentInfo } from './shared/react-inspector.js';
Page Script 주입 방식
두 익스텐션 모두 Manifest V3의 제약을 극복하기 위해 비슷한 패턴을 사용:
// content-script.js
function injectPageScript(scriptPath) {
const script = document.createElement('script');
script.src = chrome.runtime.getURL(scriptPath);
script.type = 'text/javascript';
(document.head || document.documentElement).appendChild(script);
}
injectPageScript('page-script.js');
// 이후 postMessage로 content script와 통신
Catppuccin 테마
QA Master의 UI는 다크 테마 Catppuccin을 사용했다:
:root {
--ctp-rosewater: #f5e0dc;
--ctp-flamingo: #f2cdcd;
--ctp-pink: #f5c2e7;
--ctp-mauve: #cba6f7;
--ctp-red: #f38ba8;
--ctp-green: #a6e3a1;
--ctp-text: #cdd6f4;
--ctp-surface0: #313244;
--ctp-base: #1e1e2e;
}
body {
background-color: var(--ctp-base);
color: var(--ctp-text);
}
button.primary {
background-color: var(--ctp-mauve);
color: var(--ctp-base);
}
Claude Code랑 어떻게 만들었나
설계 문서부터
두 도구 모두 코드를 바로 쓰지 않았다. 먼저 Claude Code에게 "이런 도구가 필요한데"라고 설명하고, 설계 문서를 먼저 뽑았다. Inspector는 3월 초, QA Master는 그 직후에 각각 설계 문서가 나왔다.
설계 문서에는 아키텍처, UI 와이어프레임(ASCII), Manifest V3 권한, 제한사항까지 포함됐다. 이걸 내가 검토하고 "이 부분은 이렇게 바꾸자"고 피드백하면, Claude가 설계를 수정하고 구현으로 넘어가는 방식이었다.
나: "React 요소 클릭하면 컴포넌트 트리랑 파일 경로 보여주는 크롬 익스텐션 만들고 싶어"
Claude: (설계 문서 생성 — 아키텍처, UI, 권한, 제한사항)
나: "React DevTools 없이도 동작해야 해" / "프로덕션 빌드도 고려해"
Claude: (설계 수정)
나: "좋아, 구현하자"
Claude: (manifest.json, background.js, page-script.js, content-script.js 생성)
설계에 없던 것들이 구현 중에 나왔다
설계 문서에는 "컴포넌트명으로 파일 추론"까지만 있었다. 그런데 실제로 Next.js + Turbopack 환경에서 테스트해보니, chunk URL을 파싱하면 더 정확한 경로를 얻을 수 있다는 걸 발견했다.
// Turbopack chunk URL 파싱 — 설계 문서엔 없던 기능
function parseChunkUrl(url) {
const filename = url.split('/').pop() || '';
const prefixMatch = filename.match(/^(?:turbopack-)?apps_\w+_(src_.+)/);
if (!prefixMatch) return null;
let rest = prefixMatch[1];
rest = rest.replace(/\.[_.]\.(?:js|css)$/, '');
const extMatch = rest.match(/^(.+)_(tsx|ts|jsx|js)_[0-9a-z~-]+$/);
if (!extMatch) return null;
return decodeURIComponent(extMatch[1].replace(/_/g, '/')) + '.' + extMatch[2];
}
개발 방식 정리
흐름은 이랬다. 아이디어와 요구사항은 내가 정의하고, 설계 문서는 Claude가 먼저 뽑은 다음 내가 리뷰하면서 방향을 조정했다. 구현 중에 테스트해보다가 "이것도 되면 좋겠는데?"가 나오면 그걸 바로 피드백으로 넣었고, chunk URL 파싱이나 내부 컴포넌트 필터링 같은 건 그렇게 추가됐다. 설계 → 구현 → 테스트 → 피드백 루프를 짧게 도는 게 결국 속도였다.
만들고 나서
반복적으로 불편한 것이 제일 솔직한 아이디어 소스였다. Inspector는 디자인 QA 15개를 처리하면서 느낀 불편함에서 시작됐고, 코드부터 짜지 않고 설계 문서를 먼저 뽑은 게 좋았다. "이건 1차에서 빼자", "이건 나중에 확장하자" 같은 판단이 훨씬 쉬워졌다.
Manifest V3의 Isolated world와 MAIN world 제약은 생각보다 복잡했는데, postMessage 패턴을 한 번 이해하고 나서야 전체 구조가 보였다. React 19에서 _debugSource가 제거되면서 파일 경로 추론이 막혔을 때는 좀 당황했다. 3단계 fallback(chunk URL 파싱 → 라우트 기반 추론 → 컴포넌트명 추론)을 급하게 설계했는데 결국 그게 살아남았다.
Chrome Web Store 배포는 아직 못 했고, source map 파싱이나 팀 공유 기능도 남아 있다. 일단 내 손에서 쓸 만하다는 걸 확인했으니, 그게 먼저인 것 같다.