순수 TypeScript를 사용한 반응형 UI (Reactive UIs). 배우기 쉽고, 빠르게 배포할 수 있습니다.
Aberdeen은 세밀한 속성 접근 추적 (fine-grained property access tracking)을 위해 상태를 ES6 Proxy 객체로 감싼 다음, 변경된 데이터에 의존하는 DOM 구축 클로저 (closures)만을 자동으로 재실행합니다. 따라서 가상 DOM 디핑 (virtual DOM diffing)이나 컴파일러 매직 (compiler magic) 없이도 정밀한 DOM 업데이트를 구현할 수 있습니다.
먼저, 필수적인 반응형 카운터 예제로 시작해 보겠습니다. 만약 공식 웹사이트에서 이 글을 읽고 있다면, 코드 아래에서 작동하는 데모를 볼 수 있으며, 코드 오른쪽 상단에 직접 테스트해 볼 수 있는 'edit' 버튼이 있을 것입니다.
import A from 'aberdeen';
// 상태를 프록시된 (observable) 객체로 정의합니다.
// '$' 접두사는 반응형 객체를 눈에 띄게 만들기 위한 관례일 뿐입니다.
const $state = A.proxy({question: "How many roads must a man walk down?", answer: 42});
A('h3', () => {
// 이 함수는 question이나 answer가 변경될 때마다 다시 실행됩니다.
A('text=', ${$state.question} ↪ ${$state.answer || 'Blowing in the wind'})
});
// $state.question을 <input>에 양방향 바인딩 (Two-way bind) 합니다.
A('input placeholder=Question bind=', A.ref($state, 'question'))
// <input>과 버튼 모두를 사용하여 $state.answer를 수정할 수 있도록 합니다.
A('div.row margin-top:1em', () => {
A('button text=- click=', () => $state.answer--);
A('input type=number bind=', A.ref($state, 'answer'))
A('button text=+ click=', () => $state.answer++);
});
자, 다음은 조금 더 복잡한 앱인 다음과 같은 동작을 가진 할 일 목록 (todo-list)입니다:
휴.. 이제 코드를 살펴보겠습니다:
import A from "aberdeen";
import {grow, shrink} from "aberdeen/transitions";
// 데이터를 저장하기 위해 간단한 클래스를 사용하겠습니다.
class TodoItem {
constructor(public label: string = '', public done: boolean = false) {}
toggle() { this.done = !this.done; }
}
// 최상위 사용자 인터페이스 (user interface).
function drawMain() {
// 초기 아이템들을 추가합니다. 여기에 A.proxy()를 적용하겠습니다!
let $items: TodoItem[] = A.proxy([
new TodoItem('Make todo-list demo', true),
new TodoItem('Learn Aberdeen', false),
]);
// 라벨(label) 순으로 정렬된 리스트를 그립니다.
A.onEach($items, drawItem, $item => $item.label);
// 아이템 추가 및 완료된 항목 삭제 버튼을 추가합니다.
A('div.row', () => {
A('button text=+ click=', () => $items.push(new TodoItem("")));
A('button.outline text="Delete checked" click=', () => {
for(let idx in $items) {
if ($items[idx].done) delete $items[idx];
}
});
});
};
// 각 할 일(todo) 리스트 아이템마다 호출됩니다.
function drawItem($item) {
// 라벨이 없는 아이템은 편집(editing) 상태로 열립니다.
// 아래에서 생성될 div.row 스코프 외부에서 이 A.proxy를 생성하여,
// 해당 상태가 재실행될 때도 유지되도록 합니다.
let $editing: {value: boolean} = A.proxy($item.label == '');
A('div.row', todoItemStyle, 'create=', grow, 'destroy=', shrink, () => {
// item.done에 따라 div.row에 조건부로 클래스를 추가합니다.
A({".done": A.ref($item,'done')});
// 체크표시는 CSS를 사용하여 숨겨집니다.
A('div.checkmark text=✅');
if ($editing.value) {
// 편집 중인 동안 라벨을 유지하기 위한 프록시된(Proxied) 문자열입니다.
const $labelCopy = A.proxy($item.label);
function save() {
$editing.value = false;
$item.label = $labelCopy.value;
}
// 라벨 <input>. Enter 키 또는 버튼을 사용하여 저장합니다.
A('input placeholder=Label bind=', $labelCopy, 'keydown=', e => e.key==='Enter' && save());
A('button.outline text=Cancel click=', () => $editing.value = false);
A('button text=Save click=', save);
} else {
// 텍스트로서의 라벨.
A('p text=', $item.label);
// 완료되지 않은 경우 편집 아이콘을 표시합니다.
if (!$item.done) {
A('a text=Edit click=', e => {
$editing.value = true;
$e.stopPropagation(); // 상태 토글(toggle)도 함께 일어나는 것을 방지합니다.
});
}
// 행(row)을 클릭하면 완료 상태를 토글합니다.
A('cursor:pointer click=', () => $item.done = !$item.done);
}
});
}
// 이 데모에 특화된 컴포넌트 로컬 CSS를 삽입합니다.
const todoItemStyle = A.insertCss({
"&": "mb:0.5rem",
".checkmark": "opacity:0.2",
"&.done": "text-decoration:line-through",
"&.done .checkmark": "opacity:1"
});
// 시작!
drawMain();
추가 예시:
물론 위의 예시들을 살펴보는 것도 좋을 것입니다!
만약 Claude Code, GitHub Copilot 또는 Skills를 지원하는 다른 AI 에이전트 (AI agents)를 사용한다면, Aberdeen에는 라이브러리를 효과적으로 사용하는 방법에 대해 AI에게 전문적인 지식을 제공하는 skill/ 디렉토리가 포함되어 있습니다.
이를 사용하려면, 해당 skill을 프로젝트의 .claude/skills 디렉토리로 심볼릭 링크 (symlink)를 거는 것을 권장합니다:
mkdir -p .claude/skills
ln -s ../../node_modules/aberdeen/skill .claude/skills/aberdeen
변경 사항의 전체 이력은 CHANGELOG.md를 참조하세요.