https://gist.github.com/shimpark/f07523bdb0a459f21a2952da1cd41992
vanilla js 동적 form 구조 - example 2
vanilla js 동적 form 구조 - example 2. GitHub Gist: instantly share code, notes, and snippets.
gist.github.com
ReactiveStore 활용법 — 예제 모음
이 문서는 ReactiveStore와 스키마 기반 폼을 실제로 활용하는 방법을 코드 샘플 중심으로 정리한 문서입니다. 주니어 개발자가 바로 붙여넣고 실습할 수 있도록 간단한 설명과 함께 제공됩니다.
목차
- 기본 사용: 스토어 생성 → 스키마 정의 → 폼 렌더링
- computed(파생값) 예제: 총합/요약 계산
- 동적 필드 추가: 런타임에 필드 추가/삭제
- 검증 사용 예: validateAll 호출과 UI 표시
- 포맷터 적용 예: 금액/배달 정보 표시
- 비동기 저장(옵티미스틱 업데이트) 예: 서버 저장 시 UX 처리
- 디바운스 입력 처리: 타이핑 중 불필요한 업데이트 줄이기
- 단위 테스트 샘플 (Jest + jsdom)
1) 기본 사용: 스토어 생성 → 스키마 정의 → 폼 렌더링
- 목적: 최소한의 코드로 스토어를 초기화하고 폼을 화면에 렌더링합니다.
// 1) 스토어 생성
const store = new ReactiveStore();
// 2) 스키마 정의
store.defineSchema('profile', {
name: { label: '이름', type: 'text', default: '' },
age: { label: '나이', type: 'number', default: 20 },
subscribe: { label: '뉴스레터', type: 'checkbox', default: false }
});
// 3) 렌더링 (index.html에 <div id="profileForm"></div>가 있다고 가정)
store.generateForm('profileForm', 'profile');
- 예상 동작:
#profileForm 내부에 name, age, subscribe 필드가 생성되고, 입력은 store.state로 반영됩니다.
2) computed(파생값) 예제: 총합/요약 계산
- 목적: 여러 필드로부터 계산된 값을 자동으로 갱신하여 표시합니다.
// 스키마
store.defineSchema('cart', {
itemPrice: { label: '단가', type: 'number', default: 1000 },
qty: { label: '수량', type: 'number', default: 1 }
});
// computed 등록: total = itemPrice * qty
store.computed('total', ['itemPrice', 'qty'], (state) => {
const p = Number(state.itemPrice) || 0;
const q = Number(state.qty) || 0;
return p * q;
});
// 렌더링
store.generateForm('cartForm', 'cart');
// UI에 total을 실시간으로 표시하려면 구독
const totalEl = document.getElementById('cartTotal');
function renderTotal() {
totalEl.textContent = store.state.total;
}
store._subscribe('total', renderTotal);
renderTotal();
- 팁: computed 내부에서는 부작용(예: 네트워크 호출)을 하지 마세요. 단순 계산만 수행합니다.
3) 동적 필드 추가: 런타임에 필드 추가/삭제
- 목적: 사용자의 요청에 따라 폼 구조를 동적으로 변경합니다.
// 기존 스키마가 'recipient'라 가정
const recipientSchema = store._schemas['recipient']; // 내부 참조 — 안전한 API를 만들면 더 좋음
const newKey = 'recipient_phone_' + Date.now();
recipientSchema[newKey] = { label: '전화번호 (추가)', type: 'text', default: '' };
// 상태 초기화
store.state[newKey] = '';
// 재렌더(간단 방식)
store.generateForm('recipientForm', 'recipient');
- 주의: 직접
_schemas를 수정하는 것은 캡슐화 측면에서 권장되지 않습니다. 가능하면 addField(schemaName, key, def) 같은 public API를 추가하세요.
4) 검증 사용 예: validateAll 호출과 UI 표시
- 목적: 제출 전 클라이언트 측 유효성 검사 수행 및 에러 표시.
document.getElementById('submitBtn').addEventListener('click', async () => {
const errors = store.validateAll('profile');
if (errors && errors.length > 0) {
showValidationErrors(errors);
return;
}
// validation 통과 시 처리
await saveToServer(store.state);
showToast('저장되었습니다.');
});
- 팁: 서버 측 검증과 클라이언트 검증을 분리하세요.
5) 포맷터 적용 예: 금액/배달 정보 표시
- 목적: 중앙에서 포맷터를 관리하여 UI 표기 일관성 유지
store.addFormatter('currency', (value) => {
if (value == null) return '';
return Number(value).toLocaleString('ko-KR') + '원';
});
// 사용
const price = store.state.itemPrice;
document.getElementById('priceLabel').textContent = store.format('currency', price);
6) 비동기 저장(옵티미스틱 업데이트) 예: 서버 저장 시 UX 처리
- 목적: 서버 응답 대기 중이라도 사용자에게 즉각적인 피드백 제공
async function saveSettingsOptimistic(newData) {
const backup = JSON.parse(JSON.stringify(store.state));
Object.keys(newData).forEach(k => store.state[k] = newData[k]);
showToast('저장 시도 중...');
try {
await fetch('/api/saveSettings', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(newData)
});
showToast('저장 성공');
} catch (err) {
Object.keys(backup).forEach(k => store.state[k] = backup[k]);
showToast('저장 실패 — 변경 내용이 복원되었습니다.');
console.error(err);
}
}
- 주의: 옵티미스틱 업데이트는 충돌 가능성 및 롤백 비용이 있으니 주의해서 사용하세요.
7) 디바운스 입력 처리: 타이핑 중 불필요한 업데이트 줄이기
- 목적: typing 이벤트로 지나치게 많은 상태 업데이트/리렌더를 막습니다.
function debounce(fn, wait = 300) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}
const input = document.querySelector('input[data-key="search"]');
const handler = debounce((e) => {
store.state.search = e.target.value;
}, 250);
input.addEventListener('input', handler);
8) 단위 테스트 샘플 (Jest + jsdom)
- 목적:
computed, defineSchema 초기화, 상태 변경시 파생값 재계산 등을 자동으로 검증합니다.
// store.test.js
const { JSDOM } = require('jsdom');
global.window = new JSDOM(`<!doctype html><html><body></body></html>`).window;
global.document = window.document;
const { ReactiveStore } = require('./path/to/reactive-store');
test('defineSchema sets defaults and computed recalculates', () => {
const store = new ReactiveStore();
store.defineSchema('t', {
a: { type: 'number', default: 2 },
b: { type: 'number', default: 3 }
});
store.computed('sum', ['a','b'], (s) => Number(s.a) + Number(s.b));
expect(store.state.sum).toBe(5);
store.state.a = 10;
expect(store.state.sum).toBe(13);
});