배경과 문제점
기존 상용 고객 지원 솔루션들은 해외 진출 및 다국어 지원 요구사항에 부적합한 경우가 많습니다. 특히 실시간 대화 경험, 언어별 동적 처리, 오프라인 알림 기능에서 한계가 있었습니다.
- 지연 문제: HTTP 폴링 방식은 사용자 경험을 저하시키는 높은 지연 시간을 초래합니다.
- 복잡한 다국어 관리: 단순 번역이 아닌 전체 시스템 메시지와 날짜 형식의 동적 처리 필요
- 알림 누락: 브라우저 외부에서 발생하는 메시지에 대한 즉각적인 인지 부족
- 확장성 제약: 초기 개발 이후의 성능 확장을 고려하지 않은 아키텍처
시스템 아키텍처 설계
WebSocket 기반 통신을 중심으로 구성된 마이크로서비스 아키텍처를 채택했습니다.
- 통신 계층: WebSocket을 이용한 양방향 실시간 데이터 전송
- 비즈니스 로직: 자연어 이해(NLU) 엔진과 다국어 처리 모듈 포함
- 데이터 저장소: 대화 내역과 다국어 리소스 관리
- 푸시 서비스: 오프라인 상태의 사용자에게 데스크톱 알림 제공
핵심 기술 구현
1. WebSocket 연결 관리 (Node.js)
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
const connections = new Map();
server.on('connection', (socket, request) => {
let isActive = true;
let clientId = null;
// 연결 유지 확인
socket.on('pong', () => isActive = true);
const pingTimer = setInterval(() => {
if (!isActive) return socket.terminate();
isActive = false;
socket.ping();
}, 30000);
// 메시지 처리
socket.on('message', async (rawData) => {
try {
const payload = JSON.parse(rawData);
switch(payload.action) {
case 'authenticate':
clientId = payload.id;
connections.set(clientId, socket);
break;
case 'forward':
const targetSocket = connections.get(payload.recipient);
if(targetSocket && targetSocket.readyState === WebSocket.OPEN) {
targetSocket.send(JSON.stringify({
sender: clientId,
text: payload.text,
sentAt: Date.now()
}));
} else {
await sendPushNotification(payload.recipient, payload.text);
}
break;
}
} catch(error) {
console.error('메시지 파싱 오류:', error);
}
});
socket.on('close', () => {
clearInterval(pingTimer);
if(clientId) connections.delete(clientId);
});
});
async function sendPushNotification(recipientId, content) {
console.log(`[알림] ${recipientId}에게 푸시 발송: ${content.substring(0, 50)}...`);
}
2. 국제화(i18n) 처리 (Vue.js)
// i18n/config.js
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import ko from './langs/ko.json';
import en from './langs/en.json';
import jp from './langs/jp.json';
Vue.use(VueI18n);
export const translator = new VueI18n({
locale: localStorage.getItem('preferred-language') || 'ko',
fallbackLocale: 'en',
messages: { ko, en, jp }
});
export async function changeLanguage(newLang) {
if(translator.locale === newLang) return;
if(!translator.messages[newLang]) {
const module = await import(`./langs/${newLang}.json`);
translator.setLocaleMessage(newLang, module.default);
}
translator.locale = newLang;
localStorage.setItem('preferred-language', newLang);
}
3. 데스크톱 알림 통합 (Electron)
// main/process.js
const { app, BrowserWindow, Notification, nativeImage } = require('electron');
const path = require('path');
let workbench;
let badgeCounter = 0;
app.whenReady().then(() => {
initializeWorkbench();
});
function initializeWorkbench() {
workbench = new BrowserWindow({/* 설정 */});
workbench.loadFile('index.html');
const { ipcMain } = require('electron');
ipcMain.on('trigger-alert', (_, alertData) => displaySystemAlert(alertData));
ipcMain.on('refresh-counter', (_, count) => refreshBadgeIndicator(count));
}
function displaySystemAlert({ title, message }) {
if(Notification.isSupported() && Notification.permission === 'granted') {
new Notification({
title: title || '새로운 메시지',
body: message,
icon: path.join(__dirname, 'assets/alert-icon.png')
}).show();
}
}
function refreshBadgeIndicator(count) {
badgeCounter = count;
if(process.platform === 'darwin') {
app.dock.setBadge(count ? String(count) : '');
} else if(process.platform === 'win32' && workbench) {
const overlay = count ?
nativeImage.createFromPath(path.join(__dirname, 'assets/badge.png')) : null;
workbench.setOverlayIcon(overlay, `${count}개의 미확인 메시지`);
}
}
성능 최적화 전략
WebSocket 연결 확장성
- Nginx를 통한 로드 밸런싱으로 단일 서버 한계 극복
- Redis Pub/Sub을 활용한 메시지 큐잉으로 이벤트 루프 차단 방지
- Protocol Buffers 적용으로 네트워크 트래픽 감소
다국어 리소스 관리
- LRU 캐시 알고리즘을 이용한 자주 사용되는 언어 패키지 메모리 유지
- MongoDB와 같은 NoSQL 저장소를 활용한 드물게 사용되는 언어 리소스 외부화
중요한 주의사항
메시지 중복 처리
// 클라이언트에서 UUID 생성
const generateMessageId = () => crypto.randomUUID();
// 서버에서 중복 검사 (Redis 활용 예시)
async function validateUniqueMessage(id) {
const exists = await redis.exists(`msg:${id}`);
if(exists) return false;
await redis.setex(`msg:${id}`, 5, 'processed');
return true;
}
타임존 처리
- 서버에서는 항상 UTC 시간 기준으로 저장
- 클라이언트에서 사용자 지역 정보에 따라 표시 형식 변환
- 상대적 시간 표현 ("오늘 14:30", "어제 09:15") 사용 권장
추가 기능 가능성
- E2E 암호화: 민감한 정보 보호를 위한 메시지 암호화
- 자동 번역 연동: Google Translate 또는 DeepL API 통합
- 스킬 기반 라우팅: 상담원 전문 분야에 따른 대화 할당
- 읽음 확인 및 입력 중 표시: 실시간 상태 공유 기능 추가