FreeGridv1.6.0English

실시간 업데이트

웹소켓·SSE·폴링으로 들어오는 행을 id로 갱신합니다. 바뀐 행만 다시 그리고 숫자는 오르내림에 따라 깜빡이며, connectLive가 재연결과 일시정지를 맡습니다.

실행 예제실시간 갱신

들어오는 데이터를 id로 갱신합니다. 바뀐 행만 다시 그리고, 숫자는 오르면 초록·내리면 빨강으로 깜빡이며, 수신을 멈출 수 있습니다.

이 데모의 코드
import { createGrid, type Row } from '@freegridjs/core';
import { button, separator, text, toggle, tr, type DemoContext } from '../ui';

export default function mount(ctx: DemoContext) {
  const { lang } = ctx;
  const h = (en: string, ko: string) => tr(lang, { en, ko });
  const symbols = ['SAMSUNG', 'SK HYNIX', 'NAVER', 'KAKAO', 'HYUNDAI', 'LG CHEM', 'POSCO', 'KB FIN', 'CELLTRION', 'KIA', 'SHINHAN', 'COUPANG'];
  const data: Row[] = symbols.map((name, i) => {
    const price = 20000 + i * 13700;
    return { id: name, name, price, change: 0, volume: 1000 + i * 120, history: [price, price, price, price, price, price, price, price] };
  });
  const grid = createGrid(ctx.host, {
    columns: [
      { field: 'name', header: h('Symbol', '종목'), width: 140, pinned: 'left' },
      { field: 'price', header: h('Price', '현재가'), type: 'number', format: '#,##0', width: 120 },
      { field: 'change', header: h('Change', '등락률'), type: 'number', format: '0.00%', width: 110,
        cellClass: ({ value }) => (Number(value) > 0 ? 'demo-up' : Number(value) < 0 ? 'demo-down' : undefined) },
      { field: 'history', header: h('Last moves', '최근 흐름'), width: 140, sparkline: { type: 'area' }, sortable: false, filter: false },
      { field: 'volume', header: h('Volume', '거래량'), type: 'number', format: '#,##0', width: 120 },
    ],
    data, locale: lang, getRowId: r => String(r.id), rowNumbers: true,
  });

  let timer = 0;
  const tick = () => {
    // a pretend market feed: a few symbols move each tick
    const updates = data.filter(() => Math.random() < 0.35).map(r => {
      const price = Math.max(1000, Math.round(r.price * (1 + (Math.random() - 0.5) * 0.02)));
      const base = r.history[0] as number;
      return { id: r.id, price, change: (price - base) / base, volume: (r.volume as number) + Math.round(Math.random() * 50), history: [...(r.history as number[]).slice(1), price] };
    });
    grid.applyTransactionAsync({ update: updates });
  };
  grid.on('transactionApplied', e => ctx.log(`transactionApplied → ${h('updated', '갱신')} ${e.updated.length}`));

  text(ctx, h('Prices arrive like a websocket feed. Only the changed rows are updated, and changed cells flash. Sorting by Change keeps up as prices move.',
    '웹소켓처럼 시세가 들어옵니다. 바뀐 행만 갱신되고 바뀐 셀이 깜빡입니다. 등락률로 정렬해 두면 순위가 계속 바뀝니다.'));
  separator(ctx);
  const start = () => { timer = window.setInterval(tick, 700); };
  toggle(ctx, h('Live feed', '실시간 수신'), true, on => {
    clearInterval(timer);
    if (on) start();
  });
  button(ctx, h('Add a symbol', '종목 추가'), () => {
    const name = `NEW ${Math.floor(Math.random() * 900 + 100)}`;
    const row = { id: name, name, price: 10000, change: 0, volume: 0, history: Array(8).fill(10000) };
    data.push(row);
    grid.applyTransaction({ add: [row], addIndex: 0 });
  });
  button(ctx, h('Remove the last', '마지막 종목 삭제'), () => {
    const row = data.pop();
    if (row) grid.applyTransaction({ remove: [row] });
  });
  start();
  // stop when the page goes away
  window.addEventListener('pagehide', () => clearInterval(timer), { once: true });
  return grid;
}

시세, 주문 현황, 모니터링 화면처럼 데이터가 계속 바뀌는 화면에서는 전체를 다시 넣지 말고 바뀐 행만 보내세요. 행은 getRowId로 찾아 그 자리에서 값만 바꾸고, 정렬·필터·그룹이 그 값에 영향을 받지 않으면 다시 정렬하지 않고 바뀐 행만 다시 그립니다. 숫자가 오르면 초록, 내리면 빨강으로 셀이 잠깐 깜빡입니다. 편집 중인 셀, 선택, 스크롤 위치는 그대로 유지됩니다.

트랜잭션: 바뀐 행만 보내기

applyTransaction은 추가·갱신·삭제를 한 번에 처리하고 결과(added, updated, removed, notFound)를 돌려줍니다. 서버에서 온 변경이라 실행 취소나 변경 내역(getModifiedRows)에 남지 않습니다. 빠르게 쏟아지는 업데이트는 applyTransactionAsync로 모아서 한 번에 그리세요.

const grid = createGrid(el, { columns, data, getRowId: r => r.id });

// 바뀐 행만 id로 보냅니다. 없는 행은 notFound로 돌아옵니다.
grid.applyTransaction({
  update: [{ id: 'A100', price: 15200 }],
  add: [{ id: 'A200', name: '신규', price: 9800 }],
  remove: [{ id: 'A050' }],
  upsert: [{ id: 'A300', price: 1200 }],      // 있으면 갱신, 없으면 추가
});

// 초당 수백 건이 들어오면: 모아서 한 번에 적용 (기본 50ms)
socket.onmessage = e => grid.applyTransactionAsync(JSON.parse(e.data));

connectLive: 웹소켓·SSE·폴링 연결

connectLive는 연결, 메시지 해석, 끊겼을 때 다시 연결, 일시정지를 맡습니다. 메시지가 행 배열이면 upsert, { add, update, remove, upsert }이면 트랜잭션, { rows }이면 전체 교체로 읽습니다.

import { connectLive } from '@freegridjs/core';

// 웹소켓
const live = connectLive(grid, { url: 'wss://example.com/prices' });

// 서버 전송 이벤트(SSE), 메시지 형식이 다르면 parse로 바꿉니다
connectLive(grid, {
  url: '/api/orders/stream',
  parse: msg => ({ upsert: JSON.parse(msg).items }),
  onStatus: s => (statusEl.textContent = s),      // connecting · open · paused · closed · error
});

// 폴링: 3초마다 서버에 묻기
connectLive(grid, { poll: () => fetch('/api/orders/changes').then(r => r.json()), interval: 3000 });

live.pause();    // 편집하는 동안 잠시 멈추고, 들어온 변경은 모아 둡니다
live.resume();   // 모아 둔 변경을 한 번에 적용
live.close();
이름타입설명
urlstringws:// 또는 wss://면 웹소켓, 그 밖의 주소는 서버 전송 이벤트(EventSource)로 연결합니다.
poll / interval() => Promise<unknown> / number또는 interval ms마다(기본 2000) 서버에 묻습니다.
parse(message) => RowTransaction | Row[] | { rows } | null메시지 형식을 트랜잭션으로 바꿉니다. 기본은 JSON이며, 배열은 upsert, { rows }는 전체 교체입니다.
reconnect / maxDelayboolean / number끊기면 0.5초, 1초, 2초… maxDelay(기본 30초)까지 늘려 가며 다시 연결합니다.
immediateboolean모아서 적용(applyTransactionAsync)하지 않고 메시지마다 바로 적용합니다.
onStatus / onError(status: 'connecting' | 'open' | 'paused' | 'closed' | 'error') => void연결 상태를 보여 줄 때 씁니다. onError는 읽지 못한 메시지를 받습니다.

깜빡임과 속도

createGrid(el, {
  columns, data, getRowId: r => r.id,
  flashCells: true,           // 바뀐 셀 깜빡임 (기본 true)
  flashDuration: 600,         // 깜빡임 길이 ms (기본 1000)
  asyncTransactionWait: 100,  // applyTransactionAsync를 모으는 시간 ms (기본 50)
});

// CSS 변수로 색을 바꿀 수 있습니다
// .fg-root { --fg-up: #16a34a; --fg-down: #dc2626; }

정렬 기준 컬럼이 바뀌면 순서를 다시 맞추고, 필터 조건 컬럼이 바뀌면 다시 거릅니다. 그 밖의 컬럼만 바뀌면 순서는 그대로 두고 해당 행만 다시 그리기 때문에 행이 많아도 잦은 갱신을 가볍게 처리합니다.

서버에서 보내기

// Node: 웹소켓으로 바뀐 행만 보내기 (ws 패키지)
wss.clients.forEach(c => c.send(JSON.stringify({ update: changedRows })));

// Spring Boot: SSE
@GetMapping(path = "/api/orders/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() { SseEmitter e = new SseEmitter(0L); emitters.add(e); return e; }
// 변경이 생기면: emitter.send(Map.of("upsert", rows));

이 페이지에 대한 질문

웹소켓 데이터로 그리드를 실시간 갱신하려면?

getRowId를 주고 connectLive(grid, { url: "wss://…" })를 호출하거나, 메시지마다 applyTransactionAsync({ update: rows })를 부르세요. 바뀐 행만 다시 그립니다.

실시간 갱신 중에 선택이나 스크롤이 풀리나요?

아니요. 행을 id로 찾아 그 자리에서 값만 바꾸기 때문에 선택, 스크롤 위치, 편집 중인 셀이 그대로 유지됩니다.