TokyoAJ

도쿄아재

MEMO 2025.09.17

WebSocket을 활용한 실시간 디지털 서명 시스템 구축하기

안녕하세요! 오늘은 WebSocket을 이용해서 모바일에서 서명을 하면 PC에서 실시간으로 확인할 수 있는 디지털 서명 시스템을 만드는 방법에 대해 알아보겠습니다.

프로젝트 개요

이 시스템은 총 3개의 파일로 구성되어 있습니다:

  1. m.html: 모바일용 서명 입력 페이지
  2. pc.html: PC용 서명 확인 페이지
  3. server.js: WebSocket 서버

실제 업무에서 계약서나 문서에 서명을 받을 때, 고객이 모바일로 서명하면 담당자의 PC에서 바로 확인할 수 있어 매우 유용한 시스템입니다.

서버 구조 분석

먼저 WebSocket 서버부터 살펴보겠습니다.

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const channels = new Map(); // 채널별 클라이언트 관리

서버는 채널 기반으로 동작합니다. 여러 개의 서명 세션을 동시에 처리할 수 있도록 Map을 사용해서 채널별로 클라이언트들을 관리합니다.

채널 참여 처리

if (data.type === 'join') {
// 이전 채널에서 제거
if ([ws.channel](<http://ws.channel>)) {
const prevChannel = channels.get([ws.channel](<http://ws.channel>));
if (prevChannel) {
prevChannel.delete(ws);
}
}

// 새 채널에 추가
[ws.channel](<http://ws.channel>) = [data.channel](<http://data.channel>);
if (!channels.has([data.channel](<http://data.channel>))) {
channels.set([data.channel](<http://data.channel>), new Set());
}
channels.get([data.channel](<http://data.channel>)).add(ws);

// 참여 성공 메시지 전송
ws.send(JSON.stringify({ type: 'join_success' }));
return;
}

클라이언트가 특정 채널에 참여하면:

  1. 기존에 다른 채널에 있었다면 제거
  2. 새로운 채널에 추가 (채널이 없으면 생성)
  3. 참여 성공 메시지 전송

메시지 브로드캐스팅

// 채널의 다른 클라이언트들에게 메시지 전달
const channel = channels.get([data.channel](<http://data.channel>));
if (channel) {
channel.forEach((client) => {
if (client !== ws && client.readyState === [WebSocket.OPEN](<http://WebSocket.OPEN>)) {
client.send(message);
}
});
}

서명 데이터가 오면 같은 채널의 다른 클라이언트들에게만 전송합니다. 본인에게는 전송하지 않는 것이 포인트입니다.

모바일 페이지 (m.html) 분석

모바일 페이지는 터치로 서명을 입력받는 기능이 핵심입니다.

Canvas 설정

const canvas = document.getElementById('signatureCanvas');
const ctx = canvas.getContext('2d');

// 더 자연스러운 그리기를 위한 설정
ctx.strokeStyle = '#000';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = 'source-over';

서명이 자연스럽게 보이도록 선의 끝을 둥글게(round) 처리하고, 적당한 두께(lineWidth: 6)를 설정했습니다.

터치 이벤트 처리

// 좌표를 가져오는 헬퍼 함수
function getCoordinates(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
if (e.touches && e.touches.length > 0) {
return {
x: (e.touches[0].clientX - rect.left) * scaleX,
y: (e.touches[0].clientY - [rect.top](<http://rect.top>)) * scaleY
};
} else {
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - [rect.top](<http://rect.top>)) * scaleY
};
}
}

중요한 부분은 스케일 보정입니다. Canvas의 실제 크기와 화면에 보이는 크기가 다를 수 있기 때문에 scaleX, scaleY로 좌표를 보정해줍니다.

부드러운 선 그리기

function draw(e) {
if (!drawing || !currentChannel) return;
e.preventDefault();
const coords = getCoordinates(e);
// 부드러운 곡선을 위한 이차 베지어 곡선 사용
ctx.quadraticCurveTo(
lastX, lastY,
(lastX + coords.x) / 2, (lastY + coords.y) / 2
);
ctx.stroke();
lastX = coords.x;
lastY = coords.y;
}

단순히 직선으로 연결하지 않고 quadraticCurveTo를 사용해서 부드러운 곡선을 만듭니다. 이전 좌표와 현재 좌표의 중점을 제어점으로 사용하는 것이 핵심입니다.

서명 전송

function sendSignature() {
if (!currentChannel) {
showAlertModal('먼저 채널에 참여해주세요.');
return;
}
const imageData = canvas.toDataURL('image/png');
socket.send(JSON.stringify({
type: 'signature',
channel: currentChannel,
imageData: imageData
}));
showModal();
}

Canvas 내용을 toDataURL로 Base64 인코딩된 PNG 이미지로 변환해서 전송합니다.

PC 뷰어 페이지 (pc.html) 분석

PC 페이지는 서명을 받아서 화면에 표시하는 역할을 합니다.

서명 데이터 수신 및 표시

socket.onmessage = async (event) => {
const text = await [event.data](<http://event.data>).text();
const data = JSON.parse(text);
console.log('받은 데이터:', data.type);

if (data.type === 'join_success') {
alert(`채널 ${currentChannel}에 참여했습니다.`);
return;
}

if ([data.channel](<http://data.channel>) !== currentChannel) return;

if (data.type === 'signature') {
console.log('서명 데이터 수신');
const img = new Image();
img.onload = () => {
console.log('이미지 로드 완료');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
};
img.onerror = (error) => {
console.error('이미지 로드 실패:', error);
};
img.src = data.imageData;
}
};

Base64 이미지 데이터를 받아서 Image 객체로 변환한 후 Canvas에 그려줍니다. 비동기 이미지 로딩을 위해 onload 이벤트를 사용하는 것이 중요합니다.

UI/UX 개선 포인트

현대적인 디자인

body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
color: #333;
}

.container {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 30px;
max-width: 400px;
width: 100%;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
  1. 그라디언트 배경글래스모피즘 효과
  2. **backdrop-filter: blur(10px)**로 배경 흐림 효과
  3. 둥근 모서리와 그림자로 현대적인 느낌

모달 시스템

function showAlertModal(message) {
document.getElementById('alertMessage').textContent = message;
document.getElementById('alertModal').style.display = 'block';
}

function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}

사용자 친화적인 모달 창으로 상태 메시지를 표시합니다.

반응형 디자인

@media (max-width: 480px) {
.container {
margin: 10px;
padding: 20px;
}

h1 {
font-size: 24px;
}

canvas {
width: 100%;
max-width: 280px;
}
}

모바일 환경에 최적화된 반응형 레이아웃을 제공합니다.


실행화면

1.채널ID로 뷰어와 모바일을 연결한다. 모바일에서 싸인입력한다.


2.서명 전송버튼 클릭 전송완료


3.결과

TEST URL

http://tokyoaj.asuscomm.com:9500/m.html

http://tokyoaj.asuscomm.com:9500/pc.html



실제 활용 시나리오

  1. 부동산 계약: 고객이 현장에서 모바일로 서명 → 사무실에서 실시간 확인
  2. 배송 확인: 배송기사가 모바일로 서명 받음 → 본사에서 즉시 확인
  3. 병원 동의서: 환자가 태블릿으로 서명 → 의사 PC에서 바로 확인
  4. 회의 참석 확인: 참석자들이 모바일로 서명 → 진행자가 PC로 모니터링

보안 고려사항

현재 코드는 데모용이므로 실제 운영시에는 다음 사항들을 고려해야 합니다:

  1. 채널 ID 암호화: 무작위 채널 접근 방지
  2. 이미지 크기 제한: 대용량 이미지 업로드 방지
  3. 세션 타임아웃: 일정 시간 후 자동 연결 해제
  4. SSL/TLS: wss:// 프로토콜 사용
  5. 서명 저장: 데이터베이스에 암호화하여 보관

마무리

WebSocket을 활용한 실시간 디지털 서명 시스템을 만들어봤습니다. 핵심은 채널 기반 통신Canvas를 이용한 자연스러운 서명 입력, 그리고 실시간 이미지 전송입니다.

이 시스템을 응용하면 화이트보드, 실시간 그림 그리기, 원격 교육 등 다양한 분야에 활용할 수 있습니다. 특히 비대면 업무가 늘어나는 요즘, 이런 실시간 협업 도구의 필요성이 더욱 커지고 있네요.

궁금한 점이 있으시면 언제든지 댓글로 남겨주세요! ?

댓글