design-qa-loader
v0.1.1
Published
Turbopack loader that injects data-source and data-component attributes into JSX host elements, so design QA tooling can map a rendered element back to its source location.
Maintainers
Readme
design-qa-loader
Next.js(App Router) + TailwindCSS 프로젝트의 JSX 호스트 요소에
data-source(파일:줄:칼럼)와 data-component(렌더한 컴포넌트명)를 주입하는 Turbopack 로더.
design-qa-bridge 익스텐션이 preview 화면의 요소를 소스 위치로 특정할 때 쓰는 유일한 근거다 — 익스텐션은 런타임 추론 폴백을 두지 않는다.
<!-- 주입 결과 -->
<header data-source="app/home/Header.tsx:8:3" data-component="PageHeader">
<h1 data-source="app/home/Header.tsx:9:5" data-component="PageHeader">데모</h1>
</header>왜 babel.config.js를 만들지 않는가
babel 설정 파일이 존재하면 Next.js의 동작이 바뀐다.
| 번들러 | babel 설정 파일이 있으면 |
|---|---|
| webpack | SWC를 끄고 Babel로 폴백 → next/font가 깨지고 compiler.* 옵션 무효 |
| Turbopack | SWC는 유지하되 모든 앱 파일에 babel-loader 전역 적용 → dev 속도 저하 |
이 로더는 @babel/parser로 위치만 알아내고 원본 문자열에 속성 텍스트를 끼워 넣는다
(MagicString). Babel 변환 파이프라인을 타지 않으므로 설정 파일이 필요 없고,
TypeScript 타입이 그대로 남아 다운스트림 SWC가 정상 처리한다.
설치
pnpm add -D design-qa-loader@babel/parser·@babel/traverse·magic-string은 이 패키지의 의존성이라 함께 설치된다.
직접 설치할 필요 없다.
설정
next.config.js:
const loaderPath = require.resolve('design-qa-loader');
// dev이거나, preview 빌드에서 명시적으로 켠 경우에만 활성화
const designQaEnabled =
process.env.NODE_ENV !== 'production' || process.env.DESIGN_QA === '1';
module.exports = {
...(designQaEnabled && {
turbopack: {
rules: {
'app/**/*.{tsx,jsx}': { loaders: [{ loader: loaderPath, options: {} }] },
'src/**/*.{tsx,jsx}': { loaders: [{ loader: loaderPath, options: {} }] },
},
},
}),
};preview 배포 파이프라인:
DESIGN_QA=1 pnpm buildNext 16.0+ 라면
glob 대신 condition으로 더 정밀하게 제한할 수 있다.
{ not: 'foreign' }은 node_modules와 Next 내부 파일을 제외해 성능에 유리하다.
turbopack: {
rules: {
'*': {
condition: {
all: [{ not: 'foreign' }, { path: '{app,src}/**/*.{tsx,jsx}' }],
},
loaders: [{ loader: loaderPath, options: {} }],
},
},
},주입 정책
호스트 요소(div, h1 등 소문자 태그)에만 붙인다. 이유:
- DOM에 실제로 나타나는 것은 호스트 요소뿐이다. 커스텀 컴포넌트에 붙인 속성은
그 컴포넌트가
{...props}로 DOM까지 전달해야만 보인다. - 커스텀 컴포넌트 주입은 라이브러리를 깨뜨린 전례가 있다 — FullStory의 동종 플러그인은
@react-navigation(예상치 못한 prop 키에 예외를 던짐)과victory(prop 이름 충돌)를 하드코딩으로 제외한다.
대가: <Button />의 사용처 줄은 얻지 못하고 Button 내부 <button> 줄이 잡힌다.
스타일 수정 위치로는 오히려 후자가 정확하다.
건너뛰는 대상:
Fragment,React.Fragment,Suspense,React.Suspense- 이름이
Provider로 끝나는 컴포넌트 - 멤버 표현식 태그 (
<Motion.div>) - 이미
data-source가 있는 요소 node_modules,middleware.*
data-component는 JSX를 감싼 가장 가까운 컴포넌트 정의명을 쓴다.
function 선언, const X = () => …, memo()/forwardRef() 래핑, class 컴포넌트를 커버한다.
프로덕션 안전망
- 로더 등록 자체를 조건부로 (위
designQaEnabled) — 프로덕션 번들에 코드가 아예 들어가지 않음 - Next 16.0+ 라면
condition에'development'추가 - 최후 안전망 — SWC로 속성 제거:
compiler: {
// 정규식은 Rust 문법 (JS RegExp와 다름)
reactRemoveProperties: { properties: ['^data-source$', '^data-component$'] },
},
reactRemoveProperties가 Turbopack에서 적용되는지는 공식 문서에 명시가 없다. 3번에만 의존하지 말고 1번을 반드시 함께 쓰고, 프로덕션 산출물에서grep -r "data-source" .next/가 0건인지 확인할 것.
도입 검증
- [ ]
pnpm dev후 요소 검사 →data-source/data-component가 보이는지 - [ ] 값이 실제 파일:줄과 일치하는지 (에디터로 대조)
- [ ]
DESIGN_QA=1 pnpm build && pnpm start→ preview 빌드에도 주입되는지 - [ ] 일반
pnpm build산출물에grep -r "data-source" .next/→ 0건인지 - [ ]
next/font·이미지 최적화 회귀 없는지 (babel 설정 파일을 안 만들었으므로 정상이어야 함) - [ ] dev 서버 시작 시간·HMR 체감 저하가 수용 범위인지
- [ ] 서버/클라이언트 컴포넌트 양쪽에서 주입되는지 (하이드레이션 경고 없는지)
프로그래매틱 사용
로더를 거치지 않고 변환만 쓰려면:
const { transform } = require('design-qa-loader/transform');
const result = transform(source, { filePath, rootDir });
// 변경 있으면 { code, map }, 없거나 건너뛴 파일이면 null개발 · 릴리스
이 패키지는 design-qa-bridge 레포의 pnpm workspace 패키지다. 레포 루트에서:
pnpm install
pnpm vitest run tests/loader.test.ts테스트는 상대경로가 아니라 패키지 이름(design-qa-loader/transform)으로 import한다.
exports 필드가 깨지면 테스트가 먼저 실패하도록 한 의도적 선택이다.
transform.js는 로더 컨텍스트 없이 호출 가능한 순수 함수라 테스트가 쉽다.
계속 업데이트할 계획이면 새 패턴을 만날 때마다 테스트를 먼저 추가할 것.
릴리스:
cd loader
npm version patch # 또는 minor / major
npm publish # npm login 선행npm pack --dry-run으로 배포될 파일(index.js, transform.js, README.md, LICENSE,
package.json 5개)을 먼저 확인하면 안전하다.
알려진 한계
findEnclosingComponentName은 검증된 오픈소스가 아니라 조합한 구현이다.data-component가 비는 패턴을 만나면 이 함수를 확장하고 테스트를 추가할 것.- 속성은 태그 이름 직후에 삽입되므로 뒤에 오는
{...props}에 같은 키가 있으면 props가 이긴다. 실무에서data-source를 props로 넘기는 경우는 거의 없어 기본값으로 충분하다. - Turbopack은 webpack 로더 API의 일부만 구현한다. 이 로더는 교집합인
async()/resourcePath/rootContext만 사용한다.
참고
- Next.js — turbopack 설정
- locatorjs-nextjs-experimental —
data-source+ MagicString 삽입 선례 - @fullstory/babel-plugin-annotate-react —
data-component프로덕션 구현, 라이브러리 충돌 제외 목록 - 사내 문서: 바벨 플러그인 구성 data-source, data-component
