이 글에서는 사용자가 채팅 그룹에 참여해 실시간으로 소통할 수 있는 간단한 실시간 채팅 애플리케이션을 만들어 보겠습니다.
낮은 지연 시간으로 사용자 간 실시간 메시징을 처리하는 Ably, 메시지를 영구적으로 저장하는 Upstash Redis, 그리고 애플리케이션을 구축하는 Node.js를 함께 활용합니다.
Ably란?
Ably는 사용자 간 양방향 통신을 가능하게 해주는 실시간 경험 플랫폼입니다.
이번 채팅 앱에서는 Ably의 Pub/Sub 채널을 활용합니다. 사용자가 메시지를 채널에 "발행(publish)"하면 다른 사용자들이 해당 채널을 "구독(subscribe)"하고 있어 메시지를 즉시 받아볼 수 있는 방식입니다.
Ably는 웹소켓 위에 추상화 계층을 추가한 Pub/Sub 채널을 제공하며, 다음과 같은 부가 기능들을 포함합니다:
- 구독자 프레즌스(Presence) 정보
- 인증(Authentication)
- 하트비트(Heartbeat) 메커니즘
- 큐(Queue)
- 발행된 순서대로 메시지 전달 보장
- Ably 이벤트 발생 시 함수를 호출하는 통합 기능
- Kafka로 메시지/프레즌스/메타데이터 스트리밍
더 자세한 내용은 공식 웹사이트에서 확인할 수 있습니다.
이 강력한 실시간 플랫폼을 사용하려면 먼저 Ably에 애플리케이션을 생성해야 합니다.
먼저 Ably 계정을 만들고, "Live Chat" 옵션을 선택해 애플리케이션을 생성합니다.

이것으로 끝입니다! 지금은 더 할 일이 없습니다. 나중에 API 키를 발급받기 위해 Ably 대시보드로 다시 돌아올 것입니다.
Upstash Redis란?
채팅 메시지를 영구 저장하기 위해 Upstash Redis를 사용합니다.
이 덕분에 사용자가 채팅에 참여할 때 이전 대화 기록을 불러올 수 있습니다.
또한 Upstash Redis는 채팅 메시지를 정렬된 리스트(sorted list) 형태로 저장할 수 있어, 데이터베이스에서 클라이언트로 메시지를 전달할 때 별도의 정렬 작업 없이 바로 보낼 수 있다는 장점이 있습니다.
Upstash Redis 데이터베이스를 생성하려면 Upstash 콘솔에 접속해 로그인한 후 Redis 데이터베이스를 만들면 됩니다.

인프라 준비가 완료되었습니다. 이제 애플리케이션 아키텍처를 살펴보겠습니다.
채팅 앱 아키텍처
이 채팅 애플리케이션의 설계는 매우 단순합니다.

데모 프로젝트이므로 단순화를 위해 Ably 채널 하나만 생성합니다. 클라이언트는 자신이 보내는 메시지를 이 채널에 발행하고, 한 클라이언트가 메시지를 발행하면 다른 클라이언트들은 채널 구독을 통해 즉시 메시지를 수신합니다.
클라이언트 간 실시간 메시징 외에도 메시지를 Upstash Redis에 저장해야 합니다. 이를 위해 Ably 채널에는 서버가 추가 구독자로 참여합니다. 서버는 클라이언트가 보낸 메시지를 수신해 Upstash Redis에 전달하고 저장하는 역할을 합니다.
마지막으로, 서버를 통해 Upstash Redis에 저장된 채팅 기록을 활용합니다. 서버 측에 "/history" 엔드포인트를 만들어 Redis에서 채팅 기록을 반환하도록 하고, 클라이언트는 앱을 로드할 때 이 엔드포인트를 호출해 채팅 기록을 가져옵니다.
보시다시피 데모용으로 만든 간단한 채팅 앱입니다. 앞서 소개한 Ably의 다양한 기능을 활용하면 이 애플리케이션을 수정하고 확장할 수 있습니다.
그럼 시작해 보겠습니다.
클라이언트 사이드
먼저 사용자를 위한 기본적인 채팅 UI를 만들어야 합니다. 간단한 index.html 페이지를 작성합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="index.css">
<title>Chat App</title>
</head>
<body>
<div class="container">
<p class="msg">Messages:</p>
<div id="messages" class="messages"></div>
<form id="msgForm" class="msgForm">
<input type="text" placeholder="Send message" class="input" id="inputBox" />
<input type="submit" class="btn" value="Send">
</form>
</div>
<script src="https://cdn.ably.io/lib/ably.min-1.js"></script>
<script src="app.js"></script>
</body>
</html>
사용자 브라우저에는 메시지를 입력하는 필드, 메시지를 전송하는 버튼, 그리고 이전 메시지를 표시하는 메시지 박스가 나타납니다.
이제 웹 페이지에서 실행될 JavaScript 파일인 "app.js"를 작성합니다.
먼저 Ably 채널을 생성해야 합니다. 이를 위해 Ably 대시보드로 돌아가 "API Keys" 메뉴에서 새 API 키를 생성합니다.
이 API 키의 권한(capability)으로 "Publish"와 "Subscribe"를 선택합니다.

이제 Ably 대시보드에서 발급받은 키를 사용해 JavaScript 파일에서 Ably 클라이언트를 생성할 수 있습니다.
const ably = new Ably.Realtime('<Ably API Key>');
주의
클라이언트에 API 키를 직접 노출하는 것은 안전하지 않습니다. Ably는 "TokenRequest"라는 인증 메커니즘을 제공합니다. 여기서는 데모 애플리케이션이므로 API 키를 JavaScript 파일에 직접 넣겠습니다. Ably 클라이언트 인증에 대한 자세한 내용은 Ably 토큰 문서를 참고하세요.
이제 클라이언트가 통신에 사용할 Ably 채널을 가져옵니다.
const channel = ably.channels.get('chat');
채널을 직접 생성할 필요는 없습니다. 누군가 채널에 무언가를 발행하는 순간 채널이 자동으로 생성됩니다.
메시징 기능을 구현하기 전에, 사용자로부터 이름을 입력받아야 합니다. 인증은 이 글의 주제가 아니므로 아주 단순하게 처리하겠습니다.
let name = window.prompt("Please enter your name.", "Anonymous");
이제 메시지 전송 기능을 구현합니다.
const form = document.getElementById('msgForm');
form.addEventListener('submit', (event) => {
event.preventDefault();
const message = document.getElementById('inputBox').value;
if (message.trim() !== '') {
const messageData = {
name: name,
message: message
}
channel.publish('message', messageData);
document.getElementById('inputBox').value = '';
}
});
메시지 전송은 정말 간단합니다! 사용자 이름과 메시지 내용을 담은 객체를 만들어 Ably 채널에 발행하기만 하면 됩니다.
사용자가 메시지를 수신할 수 있도록 채널을 구독하고, 사용자 이름과 메시지를 담은 메시지 박스를 화면에 추가하는 코드를 작성합니다.
channel.subscribe('message', (message) => {
console.log("Client received: ", message);
displayMessage(message.data);
});
function displayMessage(message) {
const incomingName = message.name;
const incomingMessage = message.message;
const messageElement = document.createElement('div');
const messageValue = document.createElement('div');
const messageWriter = document.createElement('div');
if(incomingName !== name){
messageElement.classList.add('msgSent');
}
else {
messageElement.classList.add('msgReceived');
}
messageWriter.classList.add('msgWriter');
messageValue.classList.add('msgValue');
messageWriter.textContent = incomingName;
messageValue.textContent = incomingMessage;
messageElement.appendChild(messageWriter);
messageElement.appendChild(messageValue);
const list = document.getElementById('messages');
list.appendChild(messageElement);
}
마지막으로, 페이지가 처음 로드될 때 채팅 기록을 가져오는 코드를 추가합니다.
document.addEventListener("DOMContentLoaded", function() {
fetchChatHistory();
});
function fetchChatHistory() {
fetch('/history')
.then((response) => {
if (!response.ok) {
throw new Error('Failed to fetch chat history');
} return response.json();
})
.then((data) => {
const history = data.history;
console.log(history);
if (history && history.length > 0) {
history.forEach((message) => {
displayMessage(JSON.parse(message));
});
}
})
.catch((error) => {
console.error('Error fetching chat history:', error);
});
}
서버 사이드
이 데모 애플리케이션의 서버는 Ably 채널을 구독해 메시지를 Upstash Redis 데이터베이스에 저장하고, 클라이언트의 요청이 오면 Upstash Redis에서 채팅 기록을 반환합니다.
먼저 "app.js" 파일에서 서버를 설정합니다.
var express = require('express'); var path = require('path');
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'ChatApp' });
});
app.use('/', router);
module.exports = app;
다음으로 "server.js" 파일에서 서버를 생성하고, Ably 채널을 구독하며 Upstash Redis에 연결합니다.
var app = require('../app');
var http = require('http');
const redis = require('redis');
const Ably = require('ably');
const port = process.env.PORT || '3000';
app.set('port', port);
var server = http.createServer(app);
server.listen(port);
const redisClient = redis.createClient({ url : "<Upstash Redis Endpoint>" });
redisClient.on("error", function(err) {
throw err;
});
redisClient.connect().then(r => {
console.log("Connected to Redis.")
})
// Ably configuration
const ably = new Ably.Realtime({
key: '<Ably API Key>',
});
// Define a channel
const channel = ably.channels.get('chat');
다음 단계는 수신된 메시지를 Upstash Redis 데이터베이스에 저장하는 것입니다. Ably 채널 구독을 통해 이 작업을 수행합니다.
// Handle incoming messages
channel.subscribe('message', async (message) => {
const convertedMessage = JSON.stringify(message.data);
console.log('Received message:', convertedMessage);
// Store the message in Upstash Redis
await redisClient.LPUSH("AblyChatList",convertedMessage);
});
마지막으로, Upstash Redis에서 채팅 기록을 조회해 클라이언트에 반환하는 "/history" 엔드포인트를 구현합니다.
// Get chat history endpoint
app.get('/history', async (req, res) => {
// Retrieve chat history from Upstash Redis
const messages = await redisClient.LRANGE("AblyChatList", 0, -1);
messages.reverse();
console.log("history api: ", messages);
res.json({ history: messages });
});
앱 실행하기
"server.js" 파일이 있는 디렉터리로 이동해 다음 명령어를 실행합니다.
node server.js
브라우저에서 localhost:3000을 열면 먼저 사용자 이름을 입력하라는 창이 나타납니다.

사용자 이름을 입력하면 채팅 화면에 들어갈 수 있습니다.
한 탭에서 메시지를 보낸 뒤, 다른 탭에서 다른 사용자 이름으로 localhost:3000을 열면 이전 탭에서 보낸 메시지를 확인할 수 있습니다.

Upstash Redis 덕분에 채팅 애플리케이션을 열 때마다 이전 채팅 기록을 불러올 수 있습니다.
마무리
Ably는 애플리케이션 간 실시간 통신을 강화하는 다양한 기능을 제공하며, 그 강력한 실시간 기능은 수많은 사용 사례에 활용될 수 있습니다.
이 글에서는 Ably의 Pub/Sub 채널을 사용해 실시간 채팅 애플리케이션을 구축했고, 동시에 메시지를 Upstash Redis 데이터베이스에 저장했습니다. 이 두 도구 덕분에 애플리케이션을 쉽고 빠르게 만들 수 있었습니다.
이 프로젝트는 Upstash Redis와 Ably의 사용법을 보여주기 위한 것이므로 범위를 아주 단순하게 유지했습니다. 관심이 있다면 Ably와 Upstash Redis의 다양한 기능을 활용해 견고하고 확장 가능하며 안전한 실시간 애플리케이션을 직접 만들어 보세요.