2026-03-22
"AI한테 던져주기 편한" React 컴포넌트 인스펙터, 그리고 QA 마스터 도구
15개의 디자인 QA 티켓을 처리하면서 느낀 반복의 고통에서 시작된 두 개의 크롬 익스텐션
"AI한테 던져주기 편한" React 컴포넌트 인스펙터, 그리고 QA 마스터 도구
15개의 디자인 QA 티켓을 처리하면서 느낀 반복의 고통에서 시작된 두 개의 크롬 익스텐션
우리 회사에는 QA 직군이 없다. PM도 없고 기획자도 없다. 디자이너가 피그마에 QA 피드백을 올리면, 개발자가 직접 Jira 티켓을 확인하고, 코드를 찾고, 수정하고, 스크린샷을 찍고, Jira를 업데이트한다. 전부 개발자 몫이다.
디자인 팀으로부터 15개의 QA 피드백을 받고 처리하는 과정은 생각보다 일관되고 반복적이었다. 그 과정에서 AI 시대에 맞는 브릿지가 하나 필요하다고 느꼈다.
1. 문제: "이 UI가 뭔데?"
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에 붙여넣기
2. 첫 번째 도구: React Component Inspector
한 클릭으로 컴포넌트 경로 복사
만든 크롬 익스텐션의 동작 방식:
- 익스텐션 아이콘 클릭 → 검사 모드 ON
- UI 요소에 마우스 오버 → 요소 하이라이트 + 컴포넌트명 미리보기
- 클릭 → 패널에서 컴포넌트 트리 + 파일 경로 표시
- "Copy to Clipboard" → 포맷된 텍스트 복사
- Claude Code에 붙여넣기 → "이 컴포넌트 수정해 줄래?"
복사되는 포맷
Component: GlobalLayout > LocalMarketLayout > ProductInfo
File: ProductInfo.tsx
간단하지만 AI가 이해하기 좋은 형식이다. 경로도 명확하고, 파일명도 있어서 AI가 바로 코드를 찾을 수 있다.
기술적 도전: Manifest V3와 두 세계의 통신
크롬 익스텐션은 세 가지 실행 환경이 있다:
- 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 19 지원 | AI 최적화 |
|---|---|---|---|---|
| React Grab | npm install (1줄) | 정확한 경로 | ✅ | ✅ |
| LocatorJS | Babel plugin | 행 번호 기반 | ✅ | ❌ |
| React Click To Component | 크롬 익스텐션 | 파일 경로 | ❌ (v19 깨짐) | ❌ |
| React DevTools | 크롬 익스텐션 | 컴포넌트 트리 | ✅ | ❌ (개발자용) |
| React Component Inspector (내 도구) | 크롬 익스텐션 | 경로 + 파일 | ✅ | ✅ (한 줄 복사) |
React Grab은 정확한 파일 경로를 제공하지만 npm install이 필요하다. 내 도구의 차별점은 프로젝트 코드 변경이 전혀 없다는 것. 크롬 익스텐션만 설치하면 어떤 React 프로젝트에서든 바로 동작한다. 대신 파일 경로는 컴포넌트명 기반 추론이라 정확도에서 트레이드오프가 있다.
3. 두 번째 도구: QA Master Tool
개발자가 QA까지 하는 환경을 위한 자동화
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 직접 연동
4. 기술적 설계 패턴
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);
}
5. 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가 한 것 |
|---|---|---|
| 아이디어 | 불편함 정의 + 요구사항 | — |
| 설계 | 리뷰 + 피드백 | 설계 문서 작성 (아키텍처, UI, 권한) |
| 구현 | 테스트 + 버그 발견 | 코드 생성 + 수정 |
| 발견 | "이것도 되면 좋겠는데?" | chunk URL 파싱, props 추출 등 제안 |
| 필터링 | localhost에서 직접 클릭하며 노이즈 발견 | 패턴 기반 필터링 로직 구현 |
| 디버깅 | 에러 로그 공유 | 원인 분석 + 수정 |
핵심은 "설계 → 구현 → 테스트 → 피드백" 루프를 빠르게 도는 것이었다.
6. 배운 것
사이드 프로젝트의 시작점
"내가 반복적으로 불편한 것"이 최고의 아이디어 소스다. Inspector는 디자인 QA 15개를 처리하면서 느낀 불편함에서 시작됐다.
설계 문서의 가치
코드부터 짜지 않고 설계 문서를 먼저 만든 게 좋았다. 설계가 있으니 "이건 1차에서 빼자", "이건 나중에 확장하자"는 판단이 쉬웠다.
Manifest V3의 두 세계
Isolated world와 MAIN world의 제약이 생각보다 복잡하다. 하지만 postMessage 패턴을 한 번 이해하면, 다른 익스텐션도 쉽게 적용할 수 있다.
React 버전 호환성은 fallback 설계가 핵심
React 19에서 _debugSource가 제거되면서 정확한 파일 경로를 못 얻게 됐다. 이때 3단계 fallback을 설계한 게 중요했다:
- chunk URL 파싱 (Turbopack 환경에서 정확한 경로)
- 라우트 기반 추론 (현재 페이지의
_components/디렉토리) - 컴포넌트명 기반 추론 (
ProductInfo→ProductInfo.tsx)
7. 다음 계획
- Source map 파싱 - chunk URL보다 더 정확한 파일 경로
- QA Master의 협업 기능 - 리포트를 팀과 공유하고, 반복 버그 패턴 감지
- Chrome Web Store 배포 - Inspector부터 올리고, 피드백 받으면서 개선
이 글의 모든 코드와 설계는 Claude Code와 함께 작업했다.