개발 기술 · 더 알아보기 · 직접 체험형

채팅 메시지는 앱에서 상대방 화면까지 어떻게 전달될까요?

보내기 버튼 뒤의 회원 확인·메시지 저장·실시간 전달과 읽음 표시를 두 앱과 서버의 실제 동작으로 살펴봅니다.

회원·대화방 확인저장·표시·읽음 분리실패·재연결·별도 조회
A 앱의 메시지를 서버에 저장한 뒤 B 앱에 전달하고, 저장·화면 표시·읽음 확인을 나누는 세 단계.

작동으로 살펴보는 개발 과정

두 앱과 서버를 나란히 확인하세요

A에서 메시지를 보내고 B에서 대화 확인을 눌러보세요. 저장·화면 표시·읽음이 따로 바뀝니다.

메시지 전송 → 서버 저장 → 상대 화면 표시 → 대화 확인

시연만 크게 보기 ↗

앱과 서버를 연결하는 구현 과정

말풍선 한 개 뒤에서 이어지는 앱과 서버

채팅 화면에는 짧은 말풍선 하나가 추가됩니다. 그 뒤에서는 보낸 회원과 대화방을 확인하고, 메시지를 데이터베이스에 저장한 다음 상대 앱의 연결로 전달합니다. 상대의 연결이 끊겨 있다면 지금 화면에 보여주는 일과 나중에 대화를 복구하는 일도 나눠 처리해야 합니다.

데브크래프트는 채팅을 구현할 때 서버 저장, 상대 화면 표시, 읽음 표시의 기준을 각각 정합니다. 이 글에서는 A와 B의 대화방을 실제 WebSocket 연결과 데이터베이스에 연결해, 메시지가 이동하는 과정과 연결이 끊겼을 때의 결과를 확인합니다.

구현 과정 01

서버는 연결된 회원과 대화방 참여 여부부터 확인합니다

앱이 서버에 연결되면 서버는 확인된 회원과 연결을 묶어둡니다. 이후 메시지를 받을 때도 그 회원이 해당 대화방에 참여할 수 있는지 확인합니다. 화면에서 상대 이름을 선택했다는 사실만으로 대화 내역을 보여주거나 메시지를 저장하지 않습니다.

시연은 A와 B에게 서로 다른 시연용 회원 정보를 발급하고, 각 앱이 자기 연결을 엽니다. 대화방에 참여하지 않은 C의 조회 요청은 서버가 거절합니다. 실제 서비스에서는 사용하는 로그인 방식에 맞춰 회원 정보를 확인하고, 대화방 참여·차단·탈퇴 조건을 함께 적용합니다.

앱에서 연결하는 일

자기 회원 정보로 서버에 연결하고, 현재 대화방을 요청합니다.

서버에서 구현하는 일

요청에서 적어 보낸 이름 대신 확인된 회원을 기준으로 대화방 참여 여부를 판단합니다.

요청 값과 구현 기준 자세히 보기
const member = actor(token, roomId);
// 회원 확인과 대화방 참여 확인을 통과한 요청만 처리
// A·B가 아닌 C의 요청은 403으로 거절

구현 과정 02

보내기를 누르면 저장 전 상태와 전송 번호를 함께 관리합니다

A가 메시지를 보내면 앱은 내용과 전송 번호를 서버로 보냅니다. 이때 입력창에서 보낸 내용을 보여주더라도 아직 저장 완료로 표시하지 않습니다. 서버가 저장을 확인해줄 때까지 전송 중 상태를 유지합니다.

전송 번호는 실패 뒤 다시 보내거나 응답이 늦어 같은 요청을 보낼 때 사용합니다. 같은 회원이 같은 번호와 같은 내용으로 재요청하면 서버는 이미 저장한 메시지를 돌려줍니다. 새 말풍선을 하나 더 만드는 대신 기존 메시지에 결과를 맞춥니다.

앱에서 연결하는 일

작성한 내용과 전송 번호를 유지하고, 서버 응답 전에는 전송 중으로 보여줍니다.

서버에서 구현하는 일

같은 대화방·보낸 회원·전송 번호로 이미 저장된 메시지가 있는지 확인합니다.

요청 값과 구현 기준 자세히 보기
POST /api/rooms/:room/messages
{
  body: "수업 시간을 변경할 수 있을까요?",
  clientKey: "전송마다 만든 고유 번호"
}

구현 과정 03

메시지를 데이터베이스에 저장한 뒤 보낸 앱에 응답합니다

서버는 참여 권한과 메시지 내용을 확인한 뒤 대화방, 보낸 회원, 내용, 전송 번호와 저장 시각을 기록합니다. 데이터베이스가 변경을 확정한 다음 메시지 번호와 저장 결과를 돌려줍니다. 앱은 이 응답을 받은 뒤 서버 저장 상태로 바꿉니다.

저장이 실패하면 성공 응답을 보내지 않습니다. 시연의 ‘저장 실패’로 보내기를 실행하면 데이터베이스 변경을 되돌리고, 입력 내용과 전송 번호를 남깁니다. 다시 보내기에 성공하면 저장된 메시지가 한 건 증가하는지 따로 조회해 확인할 수 있습니다.

앱에서 연결하는 일

성공 응답 뒤 서버 저장을 표시하고, 실패하면 입력을 유지한 채 다시 보낼 수 있게 합니다.

서버에서 구현하는 일

저장을 확정한 뒤 메시지 번호를 응답합니다. 실패한 변경은 되돌립니다.

요청 값과 구현 기준 자세히 보기
db.exec('BEGIN IMMEDIATE');
// 회원·대화방·내용·전송 번호 저장
db.exec('COMMIT');
// 저장 확정 뒤 saved 이벤트와 응답 전달
// 실패하면 ROLLBACK 후 오류 응답
A의 전송 번호로 메시지를 저장하고 B 화면 표시와 읽음 확인을 같은 메시지 번호에 기록하는 과정.
A의 전송 번호로 메시지를 저장하고 B 화면 표시와 읽음 확인을 같은 메시지 번호에 기록하는 과정. 이미지를 눌러 크게 볼 수 있습니다.

구현 과정 04

상대 앱의 연결로 전달하고 화면 표시 결과를 따로 받습니다

저장된 메시지는 상대 회원의 살아 있는 연결로 전달합니다. 시연에서는 WebSocket을 사용해 서버가 B 앱에 새 메시지를 보냅니다. B 앱은 메시지 번호를 기준으로 기존 내용과 합치고, 실제 대화 화면에 표시한 뒤 서버로 표시 확인을 보냅니다.

서버가 저장했다는 응답과 B의 화면 표시 확인은 서로 다른 결과입니다. B의 연결이 없거나 대화 화면이 열려 있지 않으면 A에게 상대 화면 표시를 확정하지 않습니다. 모바일 시연에서는 B 화면 탭을 열었을 때 표시 확인이 전송됩니다.

앱에서 연결하는 일

받은 메시지를 번호로 합치고, 열린 대화 화면에 표시한 뒤 확인을 보냅니다.

서버에서 구현하는 일

대화방의 상대 회원에게만 전달하고, 화면 표시 확인을 저장해 보낸 앱에 알려줍니다.

요청 값과 구현 기준 자세히 보기
// B 앱에서 메시지를 받아 화면에 표시한 다음
socket.send(JSON.stringify({
  type: 'displayed', id: message.id
}));

구현 과정 05

읽음 표시는 서비스가 정한 확인 조건을 충족했을 때 바꿉니다

메시지를 화면에 표시한 것만으로 사람이 읽었다고 단정할 수는 없습니다. 읽음 표시는 대화 화면 진입, 특정 위치까지 확인 같은 서비스의 기준에 맞춰 별도 상태로 다룹니다. 서버도 확인을 보낸 회원이 해당 메시지의 상대인지 검사한 뒤 읽음 정보를 반영합니다.

시연에서는 B가 ‘대화 확인’을 눌렀을 때 읽음 정보를 보냅니다. A의 말풍선 아래에는 서버 저장, 상대 화면 표시, 읽음 표시가 순서대로 바뀝니다. 같은 메시지 번호에 어떤 확인이 추가됐는지 보면 저장 성공을 읽음으로 잘못 표시하는 일을 구분할 수 있습니다.

앱에서 연결하는 일

정한 확인 조건에서 읽음 정보를 보내고, 확인이 온 메시지의 상태만 갱신합니다.

서버에서 구현하는 일

다른 회원의 메시지를 확인할 권한을 검사하고 읽음 시각을 저장합니다.

요청 값과 구현 기준 자세히 보기
// 이 시연의 읽음 조건: 상대가 대화 확인 버튼 누르기
socket.send(JSON.stringify({
  type: 'read', id: message.id
}));

구현 과정 06

연결이 다시 열리면 저장 내역을 조회해 빠진 메시지를 채웁니다

B의 연결을 끊고 A가 메시지를 보내면, 서버에는 저장돼도 B 화면에는 새 말풍선이 생기지 않습니다. 지금 연결되지 않았다는 이유로 저장된 대화까지 없애지 않습니다. B가 돌아오면 마지막으로 받은 번호 이후의 대화를 서버에서 다시 조회합니다.

시연에서는 WebSocket 재연결 뒤 별도의 HTTP 요청으로 저장 내역을 읽습니다. 기존 메시지는 번호를 기준으로 합쳐 중복 표시를 피합니다. 연결 복구와 저장 내역 조회를 함께 구현해야 연결이 끊긴 동안 놓친 메시지도 화면에 채울 수 있습니다.

앱에서 연결하는 일

마지막 메시지 번호 뒤의 내역을 조회하고, 기존 말풍선과 번호 기준으로 합칩니다.

서버에서 구현하는 일

참여 권한을 다시 확인하고 데이터베이스에 남은 메시지를 번호 순으로 돌려줍니다.

요청 값과 구현 기준 자세히 보기
GET /api/rooms/:room/messages?after=마지막_메시지_번호
// 서버의 저장 내역을 별도로 조회
// message.id 기준으로 기존 목록과 합치기
B 연결이 없는 동안 A의 메시지를 저장하고, 재연결 후 마지막 번호 뒤의 대화를 조회하는 과정.
B 연결이 없는 동안 A의 메시지를 저장하고, 재연결 후 마지막 번호 뒤의 대화를 조회하는 과정. 이미지를 눌러 크게 볼 수 있습니다.

구현 과정 07

말풍선과 서버 저장 결과를 나란히 확인합니다

채팅 동작을 확인할 때는 정상 전송 외에도 저장 실패 후 재시도, 상대 연결이 없는 동안의 전송, 재연결 후 내역 조회를 실행합니다. 같은 전송을 반복해도 저장 건수가 불필요하게 늘지 않는지, 대화방에 참여하지 않은 회원의 조회가 거절되는지도 확인합니다.

시연 아래의 ‘저장 내역 새로 조회’는 지금 앱에 보이는 말풍선을 복사하지 않습니다. 서버의 데이터베이스를 별도 요청으로 읽고 메시지 번호와 저장·표시·읽음 정보를 보여줍니다. 화면에 보이는 결과와 서버가 보관하는 결과를 함께 확인하는 것이 구현 검증의 기준입니다.

앱에서 연결하는 일

정상·실패·재연결에서 입력과 메시지 상태가 실제 결과에 맞는지 보여줍니다.

서버에서 구현하는 일

저장된 내역과 확인 상태를 별도로 조회할 수 있게 해 화면 결과와 대조합니다.

요청 값과 구현 기준 자세히 보기
GET /api/rooms/:room/inspect
// SQLite에 저장된 메시지와 확인 상태 재조회
// 대화방 비참여 회원의 조회는 403
처리 원리의 참고 자료

MDN · WebSocket · 연결을 유지하며 메시지를 주고받는 방식을 확인합니다.

SQLite · 트랜잭션 · 변경을 함께 확정하거나 되돌리는 기준을 확인합니다.

앱의 채팅과 메시지 전달 흐름을 함께 정리하세요

누가 누구와 대화하고, 어떤 상태까지 확인해야 하는지 알려 주세요. 메시지 저장과 실시간 전달, 재연결에 필요한 개발 범위를 함께 나눕니다.

30분 무료 상담 신청
앱이 만들어지는 방식으로 돌아가기