syn.$bind: Proxy 기반 양방향 데이터 바인딩을 제공합니다.

순수 객체({}) 및 평면 객체 배열([{},{},...]) 전용입니다. 데이터 변경은 별도 API 없이 표준 JS 문법(store.data.xxx = ..., arr.push(...) 등) 그대로 사용합니다. 화면 바인딩은 syn-bind / syn-bind-list 속성으로 선언합니다.

1. 기본 객체 바인딩 (value / checked / radio / edit)

→
사용중 미사용
위 textarea와 이 영역은 동일한 경로(user.bio)에 바인딩되어 있어 서로 실시간으로 동기화됩니다.

2. 중첩 객체 (nested property / 프로퍼티 재할당)

address 객체 전체를 새 값으로 교체합니다.

3. 배열(리스트) 바인딩 (syn-bind-list / push / pop / splice / index 교체)

상품명수량완료

4. 커스텀 컨트롤 어댑터 (syn.$bind.registerAdapter)

표준 HTMLElement가 아닌 컨트롤은 get / set / on / off 4개 함수만 구현해 syn.$bind.registerAdapter()에 등록하면 동일한 syn-bind="adapter(어댑터명):경로" 문법으로 바인딩됩니다.
★★★★★
1번 섹션의 "사용 여부" 체크박스와 같은 경로(user.active) → 상호 동기화됩니다.
별도 라이브러리 없이 직접 구현한 데모용 컨트롤이며, 3번 섹션 테이블과 동일한 items 배열에 바인딩됩니다. 어느 쪽에서 값을 바꾸든 실시간으로 서로 반영됩니다 — AUIGrid/Handsontable 같은 실제 그리드도 동일한 방식(get/set/on)으로 연결합니다.

5. 실제 서드파티 컨트롤 어댑터 참고 코드


아래 코드는 실제 라이브러리가 프로젝트에 설치되어 있을 때 그대로 사용할 수 있는 참고 구현입니다.
(해당 라이브러리들은 이 문서에 포함되어 있지 않으므로 이 페이지에서는 실행되지 않습니다.)

#### AUIGrid
                        
syn.$bind.registerAdapter('auigrid', {
  get: function (el) { return el._auiGrid.getGridData(); },
  set: function (el, value) { el._auiGrid.setGridData(value || []); },
  on: function (el, handler) {
    var fire = function () { handler(el._auiGrid.getGridData()); };
    el._auiGrid.bind('cellDataChanged', fire);
    el._auiGrid.bind('rowAdded', fire);
    el._auiGrid.bind('rowRemoved', fire);
  },
  off: function (el) {
    el._auiGrid.unbind('cellDataChanged');
    el._auiGrid.unbind('rowAdded');
    el._auiGrid.unbind('rowRemoved');
  }
});

// 초기화: 그리드 인스턴스를 엘리먼트에 보관해두면 어댑터에서 참조하기 편하다
var gridEl = document.getElementById('auiGridContainer');
gridEl._auiGrid = new AUIGrid({ id: 'auiGridContainer', dataSource: [], columnLayout: [ /* ... */ ] });
<div id="auiGridContainer" syn-bind="adapter(auigrid):items"></div>

#### Handsontable
                        
syn.$bind.registerAdapter('handsontable', {
  get: function (el) { return el._hot.getSourceData(); },
  set: function (el, value) { el._hot.loadData(value || []); },
  on: function (el, handler) {
    el._hot.addHook('afterChange', function (changes, source) {
      if (source === 'loadData') return; // set()으로 인한 되먹임(echo) 방지
      handler(el._hot.getSourceData());
    });
    el._hot.addHook('afterCreateRow', function () { handler(el._hot.getSourceData()); });
    el._hot.addHook('afterRemoveRow', function () { handler(el._hot.getSourceData()); });
  }
});

var hotEl = document.getElementById('hotContainer');
hotEl._hot = new Handsontable(hotEl, {
  columns: [ { data: 'title' }, { data: 'qty', type: 'numeric' }, { data: 'done', type: 'checkbox' } ],
  rowHeaders: true,
  colHeaders: ['상품명', '수량', '완료']
});
<div id="hotContainer" syn-bind="adapter(handsontable):items"></div>

#### tail.select.js
                        
syn.$bind.registerAdapter('tail-select', {
  get: function (el) { return el.tail.select.getValue(); },
  set: function (el, value) { el.tail.select.setValue(value); },
  on: function (el, handler) {
    el.addEventListener('change', function () { handler(el.tail.select.getValue()); });
  }
});

tail.select('#roleSelect'); // 초기화 시 엘리먼트에 el.tail 참조가 생성된다
<select id="roleSelect" syn-bind="adapter(tail-select):user.role">
  <option value="user">일반 사용자</option>
  <option value="admin">관리자</option>
</select>

#### daterangepicker (bootstrap-daterangepicker, jQuery 기반)
                        
syn.$bind.registerAdapter('daterangepicker', {
  get: function (el) {
    var picker = $(el).data('daterangepicker');
    return { start: picker.startDate.format('YYYY-MM-DD'), end: picker.endDate.format('YYYY-MM-DD') };
  },
  set: function (el, value) {
    if (!value) return;
    var picker = $(el).data('daterangepicker');
    picker.setStartDate(value.start);
    picker.setEndDate(value.end);
    $(el).val(value.start + ' ~ ' + value.end);
  },
  on: function (el, handler) {
    $(el).on('apply.daterangepicker', function (ev, picker) {
      handler({ start: picker.startDate.format('YYYY-MM-DD'), end: picker.endDate.format('YYYY-MM-DD') });
    });
  }
});

$('#dateRange').daterangepicker({ /* 옵션 */ });
<input id="dateRange" type="text" syn-bind="adapter(daterangepicker):period">

6. 변경 이벤트 로그 / 원본 데이터 (JSON)

7. API 레퍼런스 예제

DOM에 연결하지 않고 반응형 store만 만듭니다. 화면에 반영하려면 subscribe()로 직접 연결하거나, 나중에 attach()/mount()로 DOM에 붙여야 합니다.
createStore + DOM 스캔·바인딩을 한 번에 수행합니다. 버튼을 눌러야 그 순간 바인딩됩니다.
mount() 실행 전 (아직 바인딩 없음)
mount()의 저수준 버전으로, store를 새로 만들지 않고 이미 있는 store를 다른 DOM 영역에 추가로 연결만 합니다. 위 mount() 데모의 store를 재사용합니다.
위에서 먼저 mount()를 실행하세요
새로운 선언적 바인딩 타입을 추가합니다. 값을 대문자로 표시하는 uppercase 타입을 등록해 1번 섹션의 이름(user.name)을 그대로 재사용합니다.
4번 섹션에서 이미 자세히 다뤘습니다. 여기서는 API 자체에 집중한 최소 예시로, 클릭할 때마다 값이 올라가는 카운터 버튼을 컨트롤로 등록합니다.
트리 중간의 프록시 값(store.data.items[0])을 원본 순수 객체로 변환합니다. store.toRaw()는 루트 전체만 변환합니다.
store.data.user.name = ... 과 store.set('user.name', ...) 는 완전히 동일한 결과를 냅니다.
경로 변경을 구독합니다. deep:true면 하위 경로(items[0].title 등)까지 함께 수신합니다. 반환값은 구독을 끊는 함수입니다.
splice처럼 한 번의 호출로 여러 변경이 연속 발생하는 연산을 감쌉니다. batch 없이는 연산 도중 구독 콜백이 불완전한 중간 상태를 볼 수 있지만, batch로 감싸면 항상 연산이 끝난 뒤의 안정된 상태만 관찰합니다.
프록시가 아닌 원본 데이터 전체(복사본 아님)를 반환합니다. 직접 수정하면 데이터는 실제로 바뀌지만 Proxy의 set 트랩을 거치지 않아 이벤트가 발생하지 않고 화면도 갱신되지 않습니다.
특정 경로를 기준으로 상대 경로만 쓰는 얇은 뷰를 만듭니다. 리스트 바인딩이 각 행마다 내부적으로 자동 생성하는 것과 같은 방식입니다.