차트에서 눈에 띄는 점을 발견했는데 그 점이 어느 티켓인지 다시 표에서 찾아야 한다면 탐색이 끊긴다. Flint는 클릭을 데이터 의미가 있는 이벤트로 바꾸고, 데이터 키를 지정한 업데이트를 차트에 적용한다. 가상 고객지원 티켓을 이용해 차트·상세 표·선택 상태·후속 질문 문맥을 함께 설계해 보자.
‘저 점은 어떤 건인가’에서 시작하기
고객지원 회의에서 첫 응답 시간과 해결 시간을 산점도로 본다고 하자. 대부분은 빨리 응답하고 빨리 해결됐는데, 왼쪽 위에 몇 개의 점이 떨어져 있다. 응답은 빨랐지만 해결까지 오래 걸린 티켓이다. 담당자는 그 점을 집어 상세 기록을 보고, “첫 답변 뒤 어떤 대기가 있었나?”라는 질문으로 이어가고 싶다.
정적 차트만 있으면 티켓 번호를 찾고 표로 돌아가며 대상을 기억해야 한다. 차트와 표가 연결돼 있으면 점을 고른 순간 같은 티켓의 숫자와 메모를 나란히 볼 수 있다. 표에서 먼저 찾은 티켓도 차트 안에서 강조할 수 있다. 이때 연결에 필요한 것은 점의 좌표가 아니라 같은 대상을 가리키는 데이터 키다.
Microsoft Research는 2026년 10월 22일 Flint-interactive를 소개했다. 차트 조작을 의미 이벤트로 해석하고, 높은 수준의 업데이트 연산으로 시각 요소를 바꾸는 방식이다. 개발자가 차트와 다른 앱 요소를 연결하는 경로도 설명한다. 연구 발표
연구 발표일과 버전 도입일은 다르다. 공식 저장소의 업데이트 기록은 10월 19일을 Flint 1.0의 상호작용 도입일로 적고 있다. 이 글에서는 10월 24일 확인한 npm 패키지 flint-chart 1.0.1을 사용한다. 문서의 기준 커밋은 a794557384adcc3293a77043f6b9aff1c33d152c다. 고정 저장소 기록 · 버전 메타데이터
먼저 예제에서 찾아볼 두 건
아래 데이터는 설명을 위해 직접 작성한 가상 티켓 8건이다. 실제 고객 메시지나 서비스 통계가 아니다. 두 시간 모두 접수 시각부터 센 경과 시간이며, 영업시간 계산은 하지 않는다. 첫 응답까지 1시간, 해결까지 30시간이면 첫 응답 이후 해결까지는 29시간이다.
| 티켓 | 유형 | 첫 응답까지(h) | 해결까지(h) | 살펴볼 내용 |
|---|---|---|---|---|
| T01 | 계정 | 0.5 | 2 | 로그인 안내 뒤 해결 |
| T02 | 결제 | 1 | 30 | 결제사 확인 대기라는 가상 메모 |
| T03 | 연동 | 2 | 48 | 재현 환경 회신 대기라는 가상 메모 |
| T04 | 계정 | 3 | 6 | 권한 변경 뒤 해결 |
| T05 | 결제 | 5 | 8 | 영수증 재발송 뒤 해결 |
| T06 | 연동 | 8 | 12 | 연동 설정 수정 뒤 해결 |
| T07 | 계정 | 12 | 15 | 계정 병합 뒤 해결 |
| T08 | 결제 | 16 | 60 | 중복 청구 확인 뒤 해결 |
예제에서는 T02의 점을 선택한 다음 표의 T03 버튼을 눌러 비교한다. 선택한 행과 요약이 바뀌고, 차트에서는 다른 점이 옅어지도록 구현했다. 선택 해제나 Esc는 선택 표시와 질문 문맥을 함께 비운다. 전체 표와 비교할 점은 남겨 두므로 한 건에 집중한 뒤에도 전체 분포로 돌아가기 쉽다.
T02와 T03은 첫 응답 지표만 보면 놓치기 쉬운 조사 후보다. 그러나 “결제사 때문에 29시간 지연됐다”는 결론은 이 표에서 나오지 않는다. 메모를 그렇게 작성했더라도 실제 원인을 확인하려면 외부 문의·회신 시각, 고객 대기, 작업 재개 이력이 필요하다. 차트가 돕는 일은 다음에 볼 기록을 구체적으로 고르는 것이다.
패키지 설치와 실제 Flint 컴파일·정적 SVG 생성에 이어, Chromium 자동화 검사에서 점 클릭, 표의 키보드 선택, Esc와 버튼 초기화, 선택을 유지한 창 크기 변경을 확인했다. 390px 너비에서도 점을 눌러 해당 티켓을 선택했으며 320px 너비에서는 페이지가 가로로 넘치지 않았다. 사이트와 같은 스크립트 제한을 적용했고, JavaScript를 끈 경우에도 정적 표를 읽을 수 있었다. 이는 예제의 동작 확인이며 실제 고객 업무의 시간 절감이나 사용성 조사 결과는 아니다.
Flint에 무엇을 맡기고, 앱에는 무엇을 남길까
Flint 입력은 데이터, 의미 유형, 차트 명세 등을 함께 받는다. 이 예제의 산점도에서 가로축은 ResponseHours, 세로축은 ResolutionHours, 색은 Category다. detail에는 Ticket을 지정해 개별 티켓의 정체성을 남긴다. 실제 컴파일 결과의 의미 필드에도 이 네 필드가 유지되는 것을 확인했다.
const input = {
data: { values: tickets },
semantic_types: {
Ticket: 'ID', Category: 'Category',
ResponseHours: 'Quantity', ResolutionHours: 'Quantity'
},
chart_spec: {
chartType: 'Scatter Plot',
encodings: {
x: 'ResponseHours', y: 'ResolutionHours',
color: 'Category', detail: 'Ticket'
},
canvasSize: { width: 640, height: 340 }
},
options: { addTooltips: true }
};
Ticket을 연결 키로 쓰는 이유는 화면 크기가 변해도 번호가 같기 때문이다. “세 번째 SVG 점을 강조한다”는 방식은 정렬·필터·렌더링 변경에 따라 다른 대상을 가리킬 수 있다. 반면 { Ticket: 'T02' }는 데이터의 의미로 대상을 지정한다. 선택 키는 실제 차트의 의미 데이터에 존재해야 한다. 외부 업데이트와 키 선택
역할도 나눠 보자. Flint는 차트 요소를 선택 대상으로 해석하고 강조를 그린다. 앱은 원본 티켓 행을 보유하고, 선택한 ID로 메모를 찾아 표와 요약을 갱신한다. 메모를 그리는 HTML 표, 후속 질문 문구, 모델에 보낼지 결정하는 버튼까지 Flint가 자동으로 만들어 주는 것은 아니다.
실제 업무에서 월별 평균 한 점으로 집계한다면 점 하나가 여러 티켓을 대표할 수 있다. 그때는 Ticket 하나를 꺼내 상세 표 한 행에 붙이는 이 설계를 그대로 적용할 수 없다. 선택한 월·유형에 속하는 원본 행을 조회해야 한다. 이벤트의 target이 없거나 요소 목록이 비어 있는 경우도 처리해야 한다. 이벤트와 집계 요소의 관계
클릭과 표 선택이 같은 길로 들어오게 만들기
차트 조작에는 두 종류의 정보가 있다. 이벤트는 방금 어떤 동작을 했는지를 알려 준다. 상태는 그 동작을 반영한 뒤 무엇이 선택돼 있는지를 알려 준다. 클릭 기록만 계속 쌓으면 표에서 선택을 바꾸거나 초기화했을 때 예전 티켓이 문맥에 남기 쉽다. 이 예제는 onChange에서 현재 선택을 읽어 표와 JSON을 다시 만든다.
점 클릭은 event.target.elements의 의미 값에서 티켓 ID를 얻는다. 표 버튼은 이미 행의 ID를 알고 있다. 두 입구 모두 dispatch('ticket-selection', { id })를 호출한다. 아래는 다운로드 소스에서 오류 처리와 화면 코드를 덜어낸 핵심 부분이다.
import {
mountChart, externalInteraction, selectionUpdate
} from 'flint-chart/interactive';
import { expressionInterpreter } from 'vega-interpreter';
const selection = externalInteraction({
id: 'ticket-selection',
handle: ({ id }) => selectionUpdate(
'ticket-selection',
id === null ? null : { Ticket: id },
{ dimOpacity: 0.18 }
)
});
const click = {
id: 'ticket-click',
eventSource: { type: 'element', gesture: 'click' },
affordances: { mark: { cursor: 'activate', hover: 'target' } },
reset: [],
handle: () => null
};
const surface = mountChart(container, input, {
backend: 'vegalite', renderer: 'svg',
availableSize: { width: container.clientWidth, height: 340 }, expressionInterpreter,
interactions: [click, selection]
});
await surface.ready;
여기서 클릭 정의의 handle은 별도의 차트 업데이트를 만들지 않는다. 클릭으로 확정된 의미 이벤트를 앱이 받아 외부 선택과 같은 경로로 보내기 위해서다. externalInteraction의 핸들러가 selectionUpdate를 반환하면 Flint가 해당 데이터 키를 강조한다. 상태를 유지하는 업데이트의 이름을 하나로 맞춰, 표에서 다른 티켓을 골랐을 때 이전 선택을 교체한다. 외부 상호작용과 선택 업데이트 API
surface.onInteraction(({ event }) => {
if (event.phase !== 'commit' || event.action !== 'click-element') return;
const id = event.target?.elements[0]?.value.Ticket;
if (typeof id === 'string') choose(id);
});
// choose는 실행 소스에서 준비 상태와 작업 순서를 함께 관리한다.
async function choose(id) {
await surface.dispatch('ticket-selection', { id });
}
surface.onChange(({ phase, state }) => {
if (phase !== 'commit') return;
const ids = new Set(state.selected.map(e => e.value.Ticket));
const rows = tickets.filter(row => ids.has(row.Ticket));
renderTableAndContext(rows); // 앱이 구현할 표·요약·문맥 갱신
});
표 버튼에서는 choose('T03'), 초기화에서는 choose(null)을 호출한다. 다운로드의 onChange는 화면을 그리기만 하고 다시 dispatch하지 않는다. 차트 변화가 표 변화를 만들고, 표를 그리는 코드가 또 차트 변화를 만드는 순환을 피하려는 설계다. 빠른 연속 조작은 Promise 큐로 순서대로 적용한다.
초기화를 명시적으로 만드는 것도 중요하다. 공식 문서는 앱이 보낸 업데이트가 차트 프리셋의 초기화 제스처로 자동 제거되지 않는다고 설명한다. 이 예제의 선택은 앱을 통해 적용되므로 버튼과 Esc를 같은 null 업데이트에 연결했다. 화면을 떠날 때는 destroy()로 차트 자원을 정리한다. 상태 읽기와 수명 관리
상세 표와 질문 문맥은 같은 정보가 아니다
넓은 창에서는 차트 옆에 선택 요약을 둔다. 위치와 숫자를 번갈아 확인하기 위해서다. 전체 표는 아래에 남겨 다른 티켓으로 쉽게 이동하게 한다. 좁은 창에서는 차트와 요약을 세로로 쌓고 차트를 사용 가능한 너비에 맞춰 다시 배치하도록 구현했다. availableSize로 처음부터 사용 가능한 공간을 전달하고, 크기가 바뀌면 relayout을 호출한다. 기존 차트의 배치를 그대로 갱신할 수 없는 경우에는 새 너비로 다시 만들고 선택한 티켓을 복원한다. 긴 메모가 있는 표는 표 영역 안에서만 가로 스크롤한다. 작은 점을 누르기 어려우면 표의 버튼을 키보드나 터치로 선택할 수 있다. 색만으로 선택 여부를 알리지 않고 행 배경, 버튼의 눌림 상태, 텍스트 요약을 함께 사용한다.
질문 문맥은 펼쳐 보는 JSON 영역이다. T02를 고르면 데이터셋 이름, 가상 데이터 표시, 시간 단위, 선택한 ID와 숫자, 다음 질문이 담긴다. 상세 표에는 읽기 편한 메모를 보여 주지만, 이 예제의 문맥에는 메모를 그대로 복제하지 않는다. 선택 대상을 식별하는 데 필요한 값과 추가로 확인할 질문을 따로 보여 주려는 선택이다.
예를 들어 다음 질문은 “T02의 원인을 단정해 줘”가 아니라 “선택한 티켓의 해결 대기를 조사하려면 어떤 시각·이력이 더 필요한가?”다. 실제 앱으로 확장한다면 여기에 조회 가능한 티켓 이력이나 사용자가 승인한 자료를 연결할 수 있다. 이 예제는 JSON을 화면에 표시할 뿐 채팅이나 모델에 보내지 않는다. 공식 MCP 경로의 채팅 시연과 이 글의 웹 예제 실행은 별개다. 공식 사용 경로
실행 환경과 지원 범위까지 맞춰 재현하기
소스 ZIP을 풀고 아래 순서로 실행한다. Node 22.12 이상을 권장하며 Python은 간단한 로컬 파일 서버로만 쓴다. Flint의 Python 패키지를 설치하는 절차가 아니다.
npm ci --ignore-scripts
npm test
npm run build
python3 -m http.server 8765 --bind 127.0.0.1 --directory site
# 브라우저에서 http://127.0.0.1:8765 열기
소스에는 package-lock.json이 포함돼 있다. Flint 1.0.1, Vega 6.4.0, Vega-Lite 6.5.0, Vega Tooltip 1.1.0, Vega Interpreter 2.3.2와 빌드 도구 esbuild 0.28.2를 고정했다. node_modules는 ZIP에 넣지 않았다. 빌드는 해당 예제만의 브라우저 번들과 포함된 패키지의 라이선스 원문·목록을 만든다. 로컬 빌드 결과는 실행 중 CDN을 필요로 하지 않는다.
사이트에서 여는 예제도 같은 로컬 번들을 사용한다. 예제 페이지를 열 때만 전용 스크립트를 읽으며 다른 글에 차트 라이브러리를 추가하지 않았다. 포함된 의존성의 권리 표시는 번들 라이선스 원문에서 확인할 수 있다. 차트가 불러와지지 않아도 예제 페이지의 정적 표와 본문의 표에서 같은 데이터를 읽을 수 있다.
한 가지 재현 조건이 더 있다. 사이트는 외부 CDN 스크립트와 동적 코드 평가를 허용하지 않는다. Vega는 기본 실행 경로에서 표현식을 JavaScript 함수로 만들 수 있으므로, 이 예제는 vega-interpreter를 expressionInterpreter 옵션으로 전달한다. 사이트의 보안 정책을 느슨하게 바꾸는 대신 표현식의 구문 트리를 해석하는 경로를 사용한 것이다. 이 선택의 속도 영향은 측정하지 않았다. Vega 표현식 인터프리터 안내
렌더러 선택은 더 주의해서 읽어야 한다. README에는 Vega-Lite·ECharts·Chart.js·Plotly·Excel 등 여러 출력 경로가 나오지만, 현재 상호작용 문서는 Vega-Lite에서 동작하는 상호작용 표면을 명시한다. 다른 백엔드나 정적 렌더링에서는 상호작용을 실행하지 않고 무시 경고를 낼 수 있다. 예제의 Node 검사에서도 ECharts 선택 시 interactions_ignored 경고를 확인했다. 상호작용 명세
또한 공식 저장소의 Python 포트는 소스 미리보기이며 아직 패키지로 출시됐다는 안내가 아니다. 이 글의 실행 경로는 JavaScript npm 패키지다. Python 제공 상태
이 예제를 다른 데이터로 바꿀 때는 차트 유형보다 먼저 행의 식별자와 시간의 의미를 정하자. 월별 평균처럼 집계 단위가 달라지면 상세 조회도 바뀐다. 일회성 보고서에 숫자만 전달한다면 정적 차트가 더 단순할 수 있다. 독자가 대상을 골라 상세 기록을 보고 후속 질문을 바꾸는 작업에서 이 연결 구조의 쓸모가 커진다.
질문을 반복 보고서로 이어가는 사례는 Gemini·BigQuery 연결 시트 분석, JSON의 형식과 실제 업무 의미를 구분하는 문제는 구조화 출력의 업무 검증으로 이어 읽을 수 있다. 예제를 바꿔 쓸 때 다시 찾도록 이 글을 저장하거나 소프트웨어 주제 모음과 RSS를 이용할 수 있다.
- 차트의 픽셀 위치보다 Ticket 같은 데이터 키로 표와 연결한다. 이벤트는 무엇을 했는지, 현재 상태는 지금 무엇이 선택돼 있는지를 알려 준다.
- 표 선택과 점 클릭이 같은 업데이트를 거치면 선택 해제와 후속 문맥도 함께 맞출 수 있다. 이 예제는 가상 데이터만 사용하고 모델을 호출하지 않는다.
- Flint의 여러 렌더러 지원을 상호작용 지원으로 확대하면 안 된다. 이 글의 실행 예제는 Flint 1.0.1과 Vega-Lite로 고정하고, 원본 데이터 표와 재현 소스를 함께 제공한다.
근거와 원자료
자료의 발행 시점과 이번 글에서 참고한 범위를 함께 기록합니다.
- Flint-interactive: A language for creating interactive data visualizations ↗Microsoft Research · 2026
2026-10-22 research announcement; semantic interaction model and app/chat paths. No performance claim adopted.
- Flint README at a794557 ↗Microsoft · 2026
October19 Flint1.0 introduction separate from Oct22 blog; multiple rendering backends; Python source preview/package unreleased.
- Flint Programming API at a794557 ↗Microsoft · 2026
mountChart, externalInteraction/dispatch, selectionUpdate, getState/onChange, relayout/lifecycle; Vega-Lite interactive support and host reset behavior.
- Flint Interaction components at a794557 ↗Microsoft · 2026
Semantic event fields, commit and null/aggregate target handling; field-key updates and host-owned reset.
- Flint Declarative spec at a794557 ↗Microsoft · 2026
interaction_spec and current Vega-Lite-only interactive runtime, static assembler ignored warning.
- flint-chart 1.0.1 npm package metadata ↗Microsoft / npm registry · 2026
Installed version1.0.1 and integrity in isolated example lockfile; does not claim Git main equals published package source exactly.
- Vega Expression Interpreter ↗Vega · 2026 확인
Default Function generation versus AST interpreter under CSP; no performance numbers adopted. Example pins vega-interpreter2.3.2.
갱신 기록
첫 발행. 공식 자료와 고정 버전 예제를 대조하고 실제 브라우저의 선택·초기화·좁은 화면 동작을 검사했다. 업무 효과·채팅 연동은 측정하지 않았다.
오류를 발견했다면 댓글로 알려 주세요 →
이 글에 댓글 남기기
댓글 공개 방식을 확인하고 있습니다. 개인정보나 비밀 값은 적지 마세요.
댓글을 불러오는 중입니다.
내 댓글 확인·수정·삭제
등록 후 안내된 댓글 번호와 비밀번호를 입력하세요. 승인 대기 중인 댓글도 확인할 수 있습니다.