Google Antigravity를 사용한 Vibe 코딩 React Color Picker
I’m happy to translate the article for you, but I need the actual text of the post. Could you please paste the content you’d like translated (excluding the source link you already provided)? Once I have the article text, I’ll keep the source line unchanged and translate the rest into Korean while preserving all formatting, markdown, and code blocks.
개요
저는 이번 주말에 Google Antigravity를 사용해 ReactJS/NextJS 프로젝트용 Color Picker의 MVP 버전을 즉석 코딩했습니다. 목표는 JavaScript 자체 테스트라기보다, 프롬프트 엔지니어링으로 제가 프로젝트에서 자주 사용하는 컴포넌트를 만들 수 있는지 도전해 보는 것이었습니다.
기술 스택
- Lit – 웹 컴포넌트 구축용
- Open Web Components – 시작점으로서의 생성기
- Rollup – 모듈 번들러 (ESM 및 UMD 출력 모두에 대해 구성)
- Chrome EyeDropper API – 네이티브 아이드롭퍼 지원
- ElementInternals – 폼 호환성
- Canvas‑based fallback – EyeDropper API가 없는 Firefox와 같은 브라우저용 대체 구현
All CSS is self‑contained and encapsulated within the component’s Shadow DOM; there are no external .css files or heavy dependencies. → 모든 CSS는 컴포넌트의 Shadow DOM 내에 자체 포함되어 캡슐화됩니다; 외부 .css 파일이나 무거운 의존성이 없습니다.
Features
- HEX 및 RGBA 출력 형식
- 스타일 격리를 위한 Shadow DOM 캡슐화
- Firefox용 Canvas fallback 아이드롭퍼
- 접근성을 향상시키는 다양한 레이아웃
- jQuery, ReactJS, NuxtJS, NextJS에서 네이티브하게 작동
요구 사항 및 가드레일
범용 호환성
- 라이브러리는 외부 의존성 없이 작동해야 하며 컴파일러 독립적이어야 합니다 (Vite, Babel, Turbopack 등 사용 금지).
- 빌드 출력: Rollup을 사용한 ESM 모듈 및 UMD 번들.
API 및 이벤트
color-change라는 표준CustomEvent를 노출하며, 이는 버블링되고 컴포즈되어 Shadow DOM 경계를 넘어 전달됩니다.- React에서는 작은 래퍼를 제공하거나
ref를 사용하여 React 18/19에서 이벤트 바인딩이 원활하게 이루어지도록 합니다.
서버‑사이드 렌더링(SSR) 안전성
- 모든 브라우저 전용 API(
window,document,customElements등)를 검사하거나 컴포넌트가 마운트될 때까지 실행을 연기하여 보호합니다. - 라이브러리는 Node.js 환경에서
ReferenceError: window is not defined오류 없이 임포트 가능해야 합니다.
폼 통합
- “ 내부에 배치될 경우, 해당 컴포넌트는 ElementInternals API를 사용해 네이티브 폼 컨트롤처럼 동작해야 합니다.
성능
- 무거운 외부 색상 수학 라이브러리를 피하고 가벼운 내부 유틸리티를 구현합니다.
- 스타일 캡슐화를 위해 반드시 Shadow DOM을 사용합니다.
구현 세부 사항
Rollup 구성 (rollup.config.js)
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
import copy from 'rollup-plugin-copy';
export default {
input: 'src/color-picker.js',
output: [
{
file: 'dist/color-picker.esm.js',
format: 'esm',
sourcemap: true,
},
{
file: 'dist/color-picker.umd.js',
format: 'umd',
name: 'ColorPicker',
sourcemap: true,
},
],
plugins: [
nodeResolve(),
terser(),
copy({
targets: [{ src: 'src/*.css', dest: 'dist' }],
}),
],
};
핵심 컴포넌트 (color-picker.js)
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
@customElement('color-picker')
class ColorPicker extends LitElement {
static styles = css`
:host { display: block; }
/* component‑specific styles go here */
`;
@property({ type: String }) value = '#000000';
@state() _eyeDropperSupported = false;
constructor() {
super();
if (typeof window !== 'undefined' && 'EyeDropper' in window) {
this._eyeDropperSupported = true;
}
}
connectedCallback() {
super.connectedCallback();
// Ensure any browser‑only APIs run after mount
}
_dispatchColorChange(color) {
this.dispatchEvent(new CustomEvent('color-change', {
detail: { color },
bubbles: true,
composed: true,
}));
}
async _activateEyeDropper() {
if (!this._eyeDropperSupported) return;
try {
const eyeDropper = new EyeDropper();
const { sRGBHex } = await eyeDropper.open();
this.value = `#${sRGBHex}`;
this._dispatchColorChange(this.value);
} catch (e) {
console.error('EyeDropper cancelled or failed', e);
}
}
render() {
return html`
{
this.value = e.target.value;
this._dispatchColorChange(this.value);
}}>
${this._eyeDropperSupported
? html\`Eye Dropper\`
: html\`\`}
`;
}
}
React 래퍼 (ColorPickerReact.jsx)
import React, { useRef, useEffect } from 'react';
import '@my-org/color-picker'; // registers
export default function ColorPickerReact({ onChange, ...props }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
const handler = e => onChange?.(e.detail.color);
el?.addEventListener('color-change', handler);
return () => el?.removeEventListener('color-change', handler);
}, [onChange]);
return ;
}
Web Test Runner 예제
import { fixture, expect, a11yCheck } from '@open-wc/testing-helpers';
import './color-picker.js';
describe('color-picker', () => {
it('passes accessibility audit', async () => {
const el = await fixture('');
await expect(el).to.be.accessible();
});
it('dispatches color-change on input', async () => {
const el = await fixture('');
const input = el.shadowRoot.querySelector('input');
const spy = sinon.spy();
el.addEventListener('color-change', spy);
input.value = '#ff0000';
input.dispatchEvent(new Event('input'));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.color).to.equal('#ff0000');
});
});
향후 단계
- 코드를 더 human‑friendly하게 리팩터링하고 가독성을 향상시킵니다.
- math behind color conversion에 대해 더 깊이 탐구하여 라이브러리를 확장 가능하고 유지 보수하기 쉽게 만듭니다.
- 피드백을 기반으로 UI/UX를 다듬고 그라디언트 생성이 품질 기대에 부합하도록 보장합니다.