웹소켓을 활용한 실시간 데이터 반영 및 하이라이팅 기능 구현

웹소켓을 활용한 실시간 데이터 반영 및 하이라이팅 기능 구현

1. 개요
프로젝트 수행 중 리스트 형태의 화면에서 데이터 변경 사항을 실시간으로 반영하기 위해 웹소켓을 적용한 사례를 정리해보았습니다.
해당 기능은 사용자가 데이터를 추가하거나 수정했을 때, 다른 사용자 화면에서도 즉시 반영되고 변경된 row를 시각적으로 강조해야 하는 요구사항에서 시작되었습니다.
초기에는 폴링(Polling) 방식을 우선적으로 고려하였지만, 요구사항을 충족하기에 한계가 있다고 판단하여 웹소켓 기반 구조로 전환하였습니다.

2. 기술 선택 배경
요구사항은 다음과 같았습니다.

2.1 요구사항

  • 리스트 화면에서 데이터 변경 사항을 실시간으로 반영한다.
  • 신규 또는 수정된 row에 대해 하이라이팅 처리한다.
  • 사용자 권한에 따라 데이터 노출 범위가 다르다.

2.2 통신 구조 특징
본 기능은 클라이언트와 서버 간 양방향 통신이 필요한 구조는 아니었습니다. 서버에서 클라이언트로 변경 사항을 전달하는 단방향 구조였기 때문에 처음에는 웹소켓 대신 폴링 방식도 충분히 고려 대상이었습니다. 일반적으로 클라이언트와 서버 간 통신 방식은 다음과 같이 구분할 수 있습니다.

  • Polling: 클라이언트가 일정 주기로 서버에 요청 (Pull 방식)
  • WebSocket: 서버가 클라이언트로 이벤트를 전달 (Push 방식)

2.3 폴링 방식 검토
폴링은 일정 주기로 서버에 요청을 보내 변경 여부를 확인하는 구조입니다. 처음에는 구현 난이도가 낮고 기존 API 구조를 그대로 사용할 수 있기 때문에 폴링 방식이 적절하다고 판단하였습니다.
하지만, 실제 요구사항을 기준으로 검토해보니 다음과 같은 문제가 있었습니다.

  • 데이터 변경이 없어도 지속적으로 요청이 발생한다.
  • 짧은 주기로 설정하면 서버 부하가 증가할 수 있다.
  • 긴 주기로 설정하면 실시간성이 떨어진다.
  • 변경된 row를 즉시 강조해야 하는 UX 요구를 만족하기 어렵다.

특히 “변경 발생 시 바로 화면에 반영되어야 한다”는 요구사항이 중요했기 때문에, 폴링 방식은 적합하지 않다고 판단하였습니다.

2.4 웹소켓 선택 이유
웹소켓은 서버와 클라이언트 간 연결을 유지한 상태에서 서버가 클라이언트로 데이터를 직접 전달할 수 있는 구조입니다.
즉, 요청이 없어도 서버에서 이벤트를 전달할 수 있는 Push 기반 통신 방식입니다.
본 프로젝트에서는 양방향 통신이 필수는 아니었지만, 다음과 같은 이유로 웹소켓을 선택하였습니다.

  • 데이터 변경 시 즉시 전달 가능
  • 불필요한 반복 요청 제거
  • 서버 부하 감소
  • 사용자 경험 개선 (지연 없는 UI 반응)

결과적으로 단방향 구조임에도 불구하고, “실시간성” 요구사항을 만족시키기 위해 웹소켓을 적용하였습니다.

3. 적용 과정

3.1 전체 구조
웹소켓 통신을 위해 STOMP 프로토콜을 함께 사용했습니다. 그 이유는 기존 API 요청과 마찬가지로 Bearer 토큰 기반 인증이 필요했기 때문입니다.
기본 WebSocket 방식은 브라우저 환경에서 인증 헤더를 유연하게 처리하기 어려운 제약이 있기 때문에, 메시지 헤더를 활용할 수 있는 STOMP를 적용했습니다.
STOMP는 WebSocket을 메시지 기반 통신으로 확장해주는 프로토콜로, 클라이언트와 서버 간 메시지를 Publish/Subscribe 구조로 주고받을 수 있는 특징이 있는데, 클라이언트는 특정 topic을 구독하고, 서버는 해당 topic으로 메시지를 발행하는 방식으로 동작합니다. 또한 클라이언트와 서버 간 통신을 명령(Command), 헤더(Header), 본문(Body) 구조로 처리합니다.

프론트엔드에서는 stomp.js를 사용하여 연결 시 인증 토큰을 포함한 헤더 정보를 전달하였고, 백엔드에서는 STOMP 메시지 헤더를 기반으로 인증을 처리하였습니다. 또한 STOMP의 메시징 구조를 활용하여, 프론트엔드에서는 특정 topic을 구독하고 백엔드에서는 데이터 변경 이벤트를 해당 topic으로 전달하도록 구성하였습니다. 이를 통해 클라이언트는 필요한 데이터만 선택적으로 수신할 수 있습니다.

전체적으로 클라이언트는 웹소켓 연결을 유지한 상태에서 서버로부터 이벤트를 수신하고, 서버에서는 데이터 변경 시점에만 이벤트를 전달하는 이벤트 기반 구조로 구성하여 불필요한 통신을 줄이면서도 실시간성을 확보할 수 있었습니다.

주요 동작

  • CONNECT: 클라이언트가 서버와 연결
  • SUBSCRIBE: 특정 topic을 구독
  • SEND: 메시지를 서버로 전송
  • MESSAGE: 서버가 구독자에게 메시지 전달
  • DISCONNECT: 연결 종료

3.2 데이터 흐름
기능 흐름은 다음과 같습니다.

  1. 사용자가 데이터를 추가 또는 수정
  2. 서버에서 변경 이벤트 발생
  3. 웹소켓을 통해 변경 데이터 전송
  4. 클라이언트에서 데이터 수신
  5. 기존 리스트와 비교하여 변경된 row 식별
  6. 해당 row에 하이라이팅 적용

3.3 하이라이팅 처리 방식
웹소켓으로 전달받은 데이터를 기존 데이터와 비교하여 상태를 구분하였습니다.
이 과정에서는 기존 데이터와 신규 데이터를 비교하는 방식으로 변경된 row를 식별하는 diff 처리 방식을 사용하였습니다.

  • 전달받은 데이터의 id 값이 기존 rowData에 존재하지 않는다 -> NEW
  • 전달받은 데이터의 id 값이 기존 rowData에 존재 하고, 기존 데이터와 값이 다르다 -> UPDATED

이후 리스트(Ag-Grid)에 데이터를 반영하고, rowClass와 cellClass를 활용하여 변경된 row에 대해 시각적으로 강조하였습니다.
데이터 반영은 Ag-Grid의 GridApi 기능을 활용하여, 전체 데이터를 다시 렌더링하지 않고, 변경된 row만 업데이트하도록 처리하였습니다.

  • NEW -> gridApi.applyTransaction({ add: [ … ]})
  • UPDATED -> gridApi.applyTransaction({ update: [ … ]})

4. 문제 해결 및 고려사항

4.1 Role 기반 데이터 분리
해당 시스템은 사용자 권한에 따라 조회 가능한 데이터가 다르게 설계되어 있었습니다.
이 상태에서 웹소켓을 통해 동일 데이터를 모든 사용자에게 전달할 경우, 권한에 맞지 않는 데이터가 노출될 수 있는 문제가 있었습니다.

해결 방법

  • 사용자 연결 시 role 과 project 정보를 기준으로 구독 구조 분리
  • 서버에서 role 기준으로 데이터 필터링 후 전송

예를 들어, 관리자(System Admin Role)일 경우엔 해당 프로젝트에 대한 전체 데이터를 수신하고, 일반 사용자 (User Role)의 경우엔 User가 접근할 수 있는 관련 데이터만 수신하도록 구독 구조를 분리하였습니다. 이와 같은 구조를 통해 웹소켓 환경에서도 기존 권한 정책을 유지할 수 있었습니다.

4.2 웹소켓 연결 관리
웹소켓을 적용하면서 연결 관리 측면에서 몇 가지 문제가 발생하였습니다. 먼저, 사용자가 화면을 이동할 때 기존 웹소켓 연결을 유지할지 종료할지에 대한 기준이 필요했고, 별도의 처리 없이 구현할 경우 동일한 화면 진입 시 웹소켓이 중복으로 연결되는 문제가 발생하였습니다.
이로 인해 동일한 이벤트를 여러 번 수신하거나 불필요한 리소스가 사용되는 상황이 발생할 수 있었습니다.
웹소켓은 HTTP 요청과 달리 연결을 지속적으로 유지하는 상태 기반 통신이기 때문에 연결 lifecycle을 명확히 관리하는 것이 중요했습니다.
이를 해결하기 위해 컴포넌트의 lifecycle을 기준으로 웹소켓 연결을 제어하도록 구현하였습니다. 화면이 마운트될 때 연결을 생성하고, 언마운트될 때 연결을 종료하도록 하여 불필요한 연결이 유지되지 않도록 하였습니다.

5. 결론 및 회고
이번 구현을 통해 단순 조회 중심의 구조에서 벗어나, 이벤트 기반 실시간 처리 구조를 직접 설계하고 적용해볼 수 있었습니다. 특히 단방향 데이터 흐름에서도 웹소켓을 활용하는 것이 충분히 효과적이라는 점을 경험하였습니다.
또한 기술 선택 시 단순히 구현 가능 여부가 아니라 요구사항, 성능, 사용자 경험까지 함께 고려해야 한다는 점을 느낄 수 있었습니다.

kancho

참고