재우니 개발자 블로그

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와 스키마 기반 폼을 실제로 활용하는 방법을 코드 샘플 중심으로 정리한 문서입니다. 주니어 개발자가 바로 붙여넣고 실습할 수 있도록 간단한 설명과 함께 제공됩니다.


목차

  1. 기본 사용: 스토어 생성 → 스키마 정의 → 폼 렌더링
  2. computed(파생값) 예제: 총합/요약 계산
  3. 동적 필드 추가: 런타임에 필드 추가/삭제
  4. 검증 사용 예: validateAll 호출과 UI 표시
  5. 포맷터 적용 예: 금액/배달 정보 표시
  6. 비동기 저장(옵티미스틱 업데이트) 예: 서버 저장 시 UX 처리
  7. 디바운스 입력 처리: 타이핑 중 불필요한 업데이트 줄이기
  8. 단위 테스트 샘플 (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);
});