Web Awesome으로 프레임워크 독립적인 UI 만들기

관리자 화면에 버튼 하나를 추가하려는데 디자인부터 다시 잡아야 했던 적이 있나요? 버튼만 끝나면 다행이지만 입력창, 대화 상자, 탭, 드롭다운까지 필요해지면 이야기가 달라집니다. 직접 만들자니 접근성과 키보드 조작이 걱정되고, 익숙한 UI 프레임워크를 쓰자니 특정 JavaScript 프레임워크에 프로젝트 전체가 묶일 수 있죠.
Web Awesome은 이 사이를 파고드는 UI 컴포넌트 라이브러리입니다. React나 Vue 전용 컴포넌트 대신 브라우저 표준인 웹 컴포넌트(Web Components)로 만들어졌는데요. 일반 HTML 페이지에서도 쓸 수 있고 React, Vue, Angular, Svelte 같은 프레임워크에도 같은 마크업을 가져갈 수 있습니다.
이번 글에서는 Web Awesome을 CDN과 패키지로 불러오는 방법부터 버튼과 폼을 만드는 법, CSS로 테마를 바꾸는 법까지 살펴보겠습니다. 끝부분에서는 React에서 사용할 때의 차이와 실제 프로젝트에 도입하기 전에 확인할 점도 짚어볼게요.
Web Awesome은 무엇인가요?
Web Awesome은 접근성과 사용자화를 염두에 두고 만든 프레임워크에 독립적인(framework-agnostic) UI 라이브러리입니다. 컴포넌트 태그가 <wa-button>, <wa-input>처럼 wa-로 시작하는데요. 브라우저가 사용자 정의 요소(custom element)로 인식하는 표준 HTML 요소입니다.
직접 사용자 정의 요소와 Shadow DOM을 구현하는 과정이 궁금하다면 웹 컴포넌트의 기본 원리를 먼저 읽어 보세요. Web Awesome은 그 표준 기술 위에 실무에서 반복해서 필요한 컴포넌트와 스타일, 접근성 동작을 미리 구현해 둔 셈입니다.
Web Awesome의 뿌리는 Shoelace입니다. 공식 마이그레이션 문서는 Web Awesome을 Shoelace의 다음 메이저 버전으로 설명합니다. 단순히 태그 접두사만 바꾼 제품은 아닙니다. 폼 연결에는 ElementInternals를 활용하고, 테마에는 계단식 레이어(cascade layer)와 OKLCH 색상을 사용하며, 레이아웃과 간격을 위한 유틸리티 CSS도 제공합니다.
여기서 기대치를 하나 맞춰야 합니다. Web Awesome은 애플리케이션 프레임워크가 아닙니다. 라우팅이나 서버 상태 관리, 화면 전환을 대신해 주지 않아요. 이미 갖고 있는 HTML이나 프레임워크 안에 완성도 높은 UI 부품을 넣는 도구에 가깝습니다.
CDN으로 가장 빠르게 시작하기
먼저 별도의 빌드 설정 없이 HTML 파일 하나로 체험해 볼까요? <head>에서 스타일시트와 로더(loader)를 불러온 뒤 원하는 wa- 태그를 쓰면 됩니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web Awesome 시작하기</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@awesome.me/webawesome@3/dist-cdn/styles/webawesome.css"
/>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@awesome.me/webawesome@3/dist-cdn/webawesome.loader.js"
></script>
</head>
<body>
<wa-button variant="brand">시작하기</wa-button>
</body>
</html>
webawesome.css는 기본 테마와 공통 스타일을 가져옵니다. webawesome.loader.js는 문서에 놓인 Web Awesome 태그를 찾아 필요한 컴포넌트를 등록하고요. CDN 주소에서 @3처럼 메이저 버전을 고정하면 새 메이저 버전 때문에 화면이 갑자기 깨지는 일을 피할 수 있습니다. 운영 환경에서 같은 결과를 확실히 재현해야 한다면 정확한 버전을 고정하세요.
CDN 방식은 프로토타입이나 정적 HTML에 특히 편합니다. 다만 외부 CDN 장애와 콘텐츠 보안 정책(Content Security Policy, 이하 CSP)을 함께 고려해야 합니다. 엄격한 CSP를 적용하거나 자산을 직접 통제해야 하는 서비스라면 패키지를 설치해 번들에 포함하는 편이 낫습니다.
패키지로 설치하고 필요한 것만 가져오기
Vite나 Astro처럼 번들러를 쓰는 프로젝트라면 패키지로 설치하는 편이 자연스럽습니다. Bun에서는 다음 명령어로 추가합니다.
bun add @awesome.me/webawesome
그다음 진입 파일에서 공통 스타일과 실제로 사용할 컴포넌트를 가져옵니다.
import "@awesome.me/webawesome/dist/styles/webawesome.css";
import "@awesome.me/webawesome/dist/components/button/button.js";
import "@awesome.me/webawesome/dist/components/input/input.js";
import "@awesome.me/webawesome/dist/components/callout/callout.js";
이 방식의 장점은 필요한 컴포넌트만 골라 가져올 수 있다는 데 있습니다. 모든 컴포넌트를 한꺼번에 등록하면 시작은 편하지만 사용하지 않는 코드까지 내려갈 수 있어요. 공식 문서도 컴포넌트별 경로에서 선별해 가져오는 방식을 안내합니다.
설치된 패키지에는 dist와 dist-cdn이 함께 들어 있습니다. 번들러가 의존성을 분석하고 최적화하게 하려면 dist를 사용합니다. 반대로 빌드 도구 없이 파일을 직접 호스팅하려면 의존성이 묶여 있는 dist-cdn이 알맞습니다. 이름이 비슷하지만 용도가 다르니 복사할 경로를 주의하세요.
아이콘처럼 별도 자산을 읽는 컴포넌트를 직접 호스팅할 때는 기본 경로(base path)도 맞춰야 할 수 있습니다. 로더와 자산을 같은 위치에 두면 자동 감지가 되지만, 다른 경로에 배포한다면 setBasePath()로 위치를 알려 주세요.
import { setBasePath } from "@awesome.me/webawesome/dist/webawesome.js";
setBasePath("/assets/webawesome");
버튼과 입력 폼 구성하기
기본 사용법은 HTML 요소를 다루는 것과 크게 다르지 않습니다. 속성으로 모양과 상태를 지정하고, 태그 사이에는 표시할 내용을 넣습니다.
<form id="signup-form" class="signup-form">
<wa-input
name="email"
type="email"
label="이메일"
hint="업무용 이메일을 입력해 주세요."
required
></wa-input>
<wa-button type="submit" variant="brand">가입하기</wa-button>
</form>
<wa-callout id="result" variant="success" hidden>
신청이 접수되었습니다.
</wa-callout>
Web Awesome의 폼 컨트롤은 네이티브 폼 연결(native form association)을 지원합니다. 따라서 name이 붙은 <wa-input>의 값은 일반 <input>처럼 FormData에 포함되고, required 같은 유효성 검사에도 참여합니다.
const form = document.querySelector("#signup-form");
const result = document.querySelector("#result");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
console.log(data.get("email"));
result.hidden = false;
});
이 차이는 생각보다 큽니다. 겉모습만 입력창처럼 만든 div와 달리 브라우저의 폼 제출, 초기화, 유효성 검사 흐름을 그대로 활용할 수 있기 때문입니다. 물론 라이브러리가 모든 접근성 문제를 자동으로 해결해 주는 것은 아닙니다. label을 빠뜨리지 않고 오류 메시지를 이해하기 쉽게 쓰는 일은 여전히 사용하는 쪽의 몫입니다.
컴포넌트에 값을 넘기고 변화를 감지하는 원리가 궁금하다면 웹 컴포넌트 속성 활용법도 이어서 살펴보세요. Web Awesome의 속성 API를 읽을 때 훨씬 덜 낯설게 느껴질 겁니다.
슬롯으로 컴포넌트 안을 조합하기
텍스트만 넣는 것으로 부족할 때는 슬롯(slot)을 사용합니다. 슬롯은 컴포넌트가 열어 둔 자리에 아이콘이나 설명 같은 외부 마크업을 끼워 넣는 웹 컴포넌트 표준입니다.
<wa-button variant="brand">
<wa-icon slot="start" name="paper-plane-top"></wa-icon>
메시지 보내기
</wa-button>
여기서는 아이콘에 slot="start"를 지정해 버튼 텍스트 앞에 배치했습니다. 이런 조합 API는 컴포넌트마다 다릅니다. 슬롯 이름을 추측하기보다는 해당 컴포넌트 문서의 예제와 API 표를 확인하는 편이 안전해요.
모든 컴포넌트가 무료인 것도 아닙니다. Web Awesome은 무료 구성과 Pro 구성을 함께 운영합니다. 토스트, 콤보박스, 파일 입력, 차트처럼 Pro로 제공되는 컴포넌트가 있으므로 도입 전에 필요한 목록과 라이선스를 확인해야 합니다. 무료 패키지만으로 화면 설계를 마친 뒤 꼭 필요한 부품이 Pro라는 사실을 발견하면 교체 비용이 커집니다.
CSS 토큰으로 디자인 바꾸기
Web Awesome은 CSS 사용자 정의 속성(custom property)을 디자인 토큰으로 사용합니다. Shadow DOM 안쪽의 세부 선택자를 억지로 뚫기보다 공개된 토큰을 바꾸는 방식이 먼저입니다. CSS 변수의 동작 원리를 알고 있다면 기존 스타일 시스템과 연결하기도 어렵지 않습니다.
:root {
--wa-color-brand-50: oklch(0.62 0.2 265);
--wa-font-family-body: "Pretendard", sans-serif;
--wa-border-radius-m: 0.75rem;
}
.signup-form {
display: grid;
gap: var(--wa-space-m);
max-width: 28rem;
}
다크 모드는 상위 요소에 wa-dark 클래스를 붙여 켤 수 있습니다. 페이지 전체에 적용하려면 <html>에 붙이고, 특정 영역만 반전하려면 그 영역에 wa-invert를 사용할 수 있어요.
const toggle = document.querySelector("#theme-toggle");
toggle.addEventListener("click", () => {
document.documentElement.classList.toggle("wa-dark");
});
Web Awesome의 컴포넌트 스타일은 CSS 계단식 레이어 안에 정의되어 있습니다. 그래서 레이어 밖에 작성한 애플리케이션 CSS가 같은 명시도라면 우선할 수 있습니다. 무작정 !important를 붙이기 전에 디자인 토큰, 공개된 CSS 파트(CSS part), 일반 재정의 순서로 접근해 보세요. 내부 Shadow DOM 구조에 기대어 스타일을 덮으면 버전이 올라갈 때 깨지기 쉽습니다.
React에서는 어떻게 사용하나요?
React 19부터는 사용자 정의 요소를 네이티브로 지원합니다. Web Awesome의 스타일과 컴포넌트 모듈을 가져온 다음 JSX에서 wa- 태그를 바로 사용할 수 있습니다.
import "@awesome.me/webawesome/dist/styles/webawesome.css";
import "@awesome.me/webawesome/dist/components/button/button.js";
export default function SubscribeButton() {
return <wa-button variant="brand">구독하기</wa-button>;
}
React 18 이하에서는 전용 래퍼(wrapper)를 컴포넌트별로 가져오는 방식이 권장됩니다.
import WaButton from "@awesome.me/webawesome/dist/react/button/index.js";
export default function SubscribeButton() {
return <WaButton variant="brand">구독하기</WaButton>;
}
둘을 섞으면 사용법과 타입이 헷갈리기 쉽습니다. React 버전과 TypeScript 설정에 맞춰 한 방식을 고르세요. React 18용 래퍼는 단일 진입점에서 전부 가져오기보다 컴포넌트별 경로를 사용하는 편이 번들 크기를 관리하기 쉽습니다.
이벤트도 React 합성 이벤트 이름과 Web Awesome 문서의 이벤트 이름을 구분해서 봐야 합니다. 네이티브 input 이벤트는 onInput으로 받을 수 있지만, 컴포넌트 고유 이벤트는 현재 React 버전과 타입 선언에서 어떻게 노출되는지 확인하세요. TypeScript 프로젝트라면 패키지의 custom-elements-jsx.d.ts를 연결하면 자동 완성과 속성 검사를 받을 수 있습니다.
도입 전에 확인할 점
Web Awesome은 여러 프레임워크가 섞인 조직이나 오래 유지할 디자인 시스템에서 특히 매력적입니다. 서버가 렌더링한 HTML에 몇 개의 상호작용 컴포넌트만 넣을 때도 잘 맞고요. 반면 이미 한 프레임워크의 UI 생태계를 깊게 사용하고 있다면 폼 라이브러리, 테스트 도구, 서버 렌더링과의 궁합을 먼저 검증해야 합니다.
웹 컴포넌트는 브라우저에서 업그레이드되기 전에도 태그 자체는 DOM에 나타납니다. 그 사이 레이아웃이 흔들리거나 스타일이 적용되지 않은 콘텐츠가 잠깐 보일 수 있는데요. 핵심 화면에서는 실제 네트워크 조건으로 초기 표시를 확인해야 합니다.
테스트 환경도 살펴보세요. 실제 브라우저에서는 잘 작동해도 JSDOM에는 matchMedia 같은 API가 빠져 있을 수 있습니다. 접근성과 키보드 동작까지 확인하려면 Playwright처럼 실제 브라우저를 실행하는 테스트를 곁들이는 편이 든든합니다.
마지막으로 다음 항목을 작은 화면 하나에서 먼저 검증해 보세요.
- 필요한 컴포넌트가 무료 구성에 포함되는가?
- 실제 번들에 사용한 컴포넌트만 포함되는가?
- 서버 렌더링 뒤 컴포넌트가 등록될 때 화면이 흔들리지 않는가?
- 키보드만으로 모든 기능을 사용할 수 있는가?
- 다크 모드와 고대비 환경에서 상태를 구분할 수 있는가?
- 폼 제출, 초기화, 오류 표시가 기존 코드와 잘 연결되는가?
마치며
Web Awesome은 웹 컴포넌트의 이식성과 완성된 UI 라이브러리의 생산성을 함께 노립니다. CDN 링크 두 개로 체험할 수 있고, 프로젝트에서는 @awesome.me/webawesome 패키지에서 필요한 컴포넌트만 골라 가져올 수 있습니다. 디자인 토큰과 테마 클래스를 사용하면 브랜드에 맞게 바꾸기도 수월하고요.
그렇다고 프레임워크나 디자인 결정을 모두 대신해 주는 만능 도구는 아닙니다. 무료와 Pro의 경계, 번들 크기, 초기 업그레이드 화면, 테스트 환경을 작은 기능에서 먼저 검증해 보세요. 이 조건이 맞는 프로젝트라면 프레임워크를 바꿔도 오래 살아남는 UI 기반을 만드는 데 꽤 실용적인 선택이 될 수 있습니다.
설치 경로와 컴포넌트별 API는 Web Awesome 공식 문서에서 확인할 수 있습니다. Shoelace 프로젝트를 옮긴다면 공식 마이그레이션 가이드를 함께 참고하세요.
This work is licensed under CC BY 4.0