💬 실시간 채팅 앱

120

왜 실시간 채팅 앱을 만들까?

카카오톡이나 슬랙 같은 채팅 앱을 사용할 때, 메시지를 보내면 상대방에게 즉시 도착합니다. 새로고침을 누르지 않아도, 별도 조작 없이, 화면에 바로 메시지가 나타나죠.

실시간 채팅 아키텍처 — WebSocket API Gateway + Lambda + DynamoDB

이것이 가능한 이유는 WebSocket 프로토콜 덕분입니다.

일반적인 HTTP는 "요청-응답" 구조입니다. 클라이언트가 물어봐야 서버가 대답합니다. 새 메시지가 있는지 확인하려면 1초마다 "새 메시지 있나요?"라고 계속 물어봐야 합니다 (폴링). 사용자가 1,000명이면 1초에 1,000번의 불필요한 요청이 발생합니다.

WebSocket은 다릅니다. 한 번 연결하면 서버와 클라이언트가 양방향으로 자유롭게 데이터를 주고받습니다. 서버가 새 메시지를 받으면, 연결된 모든 클라이언트에게 즉시 푸시합니다.

이 실습에서 구축할 채팅 앱의 동작 방식:

이벤트처리
사용자가 접속$connect → connectionId를 DynamoDB에 저장
메시지 전송sendMessage → 모든 연결에 메시지 브로드캐스트
사용자가 떠남$disconnect → connectionId를 DynamoDB에서 삭제

서버 없이, API Gateway WebSocket API + Lambda + DynamoDB로 완전한 실시간 채팅을 만들어 보겠습니다.

실습을 시작하기 전에 AWS 콘솔에 로그인되어 있는지 확인하세요. 리전은 ap-northeast-2 (서울) 을 사용합니다.

아키텍처 개요

HTTP 폴링 vs WebSocket 실시간 통신 비교

WebSocket 통신 흐름

다이어그램 로딩 중...
WebSocket 채팅 메시지 흐름

비용 예측

비용 계산기

2시간
0h24h
API Gateway WebSocket

1,000 요청당 $3.50

$0.0080
Lambda 호출

100만 요청당 $0.20 + 실행시간

$0.0040
DynamoDB

쓰기 $1.25/백만, 읽기 $0.25/백만

$0.0060
예상 총 비용$0.0180

* 실제 비용은 AWS 요금 정책에 따라 달라질 수 있습니다.

Step 1: DynamoDB 연결 테이블 생성

WebSocket 연결 정보를 저장할 테이블을 만듭니다. 사용자가 접속하면 connectionId를 저장하고, 떠나면 삭제합니다.

  1. AWS 콘솔 → DynamoDB → 테이블 생성 클릭
  2. 테이블 이름: chat-connections
  3. 파티션 키: connectionId (문자열) — API Gateway가 각 WebSocket 연결에 부여하는 고유 ID
  4. 설정: 온디맨드 용량 모드 선택
  5. 테이블 생성 클릭
  6. 상태가 활성이 될 때까지 대기합니다 (약 10-20초)
코드
aws dynamodb create-table \
  --table-name chat-connections \
  --attribute-definitions AttributeName=connectionId,AttributeType=S \
  --key-schema AttributeName=connectionId,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

채팅 앱에서는 메시지를 보낼 때 모든 활성 연결에 브로드캐스트해야 하므로, DynamoDB의 Scan 연산으로 전체 connectionId를 조회합니다. 사용자 수가 수천 명 이상이 되면 Scan 대신 GSI(글로벌 보조 인덱스)나 채팅방별 파티셔닝을 고려해야 합니다.

Step 2: Lambda 핸들러 작성

WebSocket의 세 가지 이벤트($connect, $disconnect, sendMessage)를 처리할 Lambda 함수 3개를 만듭니다.

진행률 0/6
  1. 1Lambda 콘솔 → 함수 생성 → 이름: chat-connect, 런타임: Node.js 20.x
  2. 2chat-connect 코드: connectionId를 DynamoDB에 저장하고 statusCode 200 반환 import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { PutCommand, DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({})); export const handler = async (event) => { await ddb.send(new PutCommand({ TableName: 'chat-connections', Item: { connectionId: event.requestContext.connectionId, connectedAt: new Date
  3. 3chat-disconnect 함수 생성: connectionId를 DynamoDB에서 삭제 import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DeleteCommand, DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({})); export const handler = async (event) => { await ddb.send(new DeleteCommand({ TableName: 'chat-connections', Key: { connectionId: event.requestContext.connectionId } })); return { statusCode: 200 };
  4. 4chat-sendmessage 함수 생성: 아래 전체 코드를 붙여넣습니다
  5. 5세 함수 모두 실행 역할에 DynamoDB 읽기/쓰기 권한 부여 (AmazonDynamoDBFullAccess)
  6. 6chat-sendmessage 역할에 추가로 API Gateway 관리 연결 권한 부여 (AmazonAPIGatewayInvokeFullAccess)

아래는 핵심인 chat-sendmessage Lambda의 전체 코드입니다. 모든 연결에 메시지를 브로드캐스트하며, 끊어진 연결은 자동으로 정리합니다.

코드
// sendMessage Lambda — 메시지 브로드캐스트 함수 (Node.js 20.x 런타임)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
  ScanCommand,
  DeleteCommand,
  DynamoDBDocumentClient,
} from '@aws-sdk/lib-dynamodb';
import {
  ApiGatewayManagementApiClient,
  PostToConnectionCommand,
} from '@aws-sdk/client-apigatewaymanagementapi';
 
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.CONNECTIONS_TABLE || 'chat-connections';
 
export const handler = async (event) => {
  const { connectionId, domainName, stage } = event.requestContext;
  const endpoint = `https://${domainName}/${stage}`;
  const apigw = new ApiGatewayManagementApiClient({ endpoint });
 
  const { message } = JSON.parse(event.body);
  const payload = JSON.stringify({
    action: 'message',
    sender: connectionId.slice(-6), // 발신자 식별용 짧은 ID
    message,
    timestamp: new Date().toISOString(),
  });
 
  // 모든 활성 연결 조회
  const { Items } = await ddb.send(new ScanCommand({ TableName: TABLE }));
 
  // 각 연결에 메시지 브로드캐스트
  const broadcasts = Items.map(async ({ connectionId: id }) => {
    try {
      await apigw.send(
        new PostToConnectionCommand({
          ConnectionId: id,
          Data: payload,
        })
      );
    } catch (err) {
      if (err.statusCode === 410) {
        // 410 Gone — 이미 끊어진 연결은 DynamoDB에서 정리
        await ddb.send(
          new DeleteCommand({
            TableName: TABLE,
            Key: { connectionId: id },
          })
        );
      }
    }
  });
 
  await Promise.all(broadcasts);
  return { statusCode: 200 };
};

postToConnection 호출 시 410 상태 코드가 반환되면 해당 연결이 이미 끊어졌다는 의미입니다. 이런 "stale connection"을 DynamoDB에서 삭제하여 불필요한 전송 시도를 방지하세요. 이 패턴은 프로덕션 채팅 서비스에서도 동일하게 사용됩니다.

Step 3: WebSocket API 생성 및 라우트 설정

WebSocket API를 만들고 각 이벤트를 Lambda 함수에 연결합니다.

진행률 0/6
  1. 1API Gateway 콘솔 → WebSocket API 카드에서 빌드 클릭
  2. 2API 이름: chat-websocket-api
  3. 3라우트 선택 표현식: $request.body.action (클라이언트가 보내는 JSON의 action 필드로 라우팅)
  4. 4라우트 설정 화면: $connect 라우트가 자동 생성됨 → 통합 대상: chat-connect Lambda 선택 $disconnect 라우트 → 통합 대상: chat-disconnect Lambda 선택 라우트 추가 클릭 → 라우트 키: sendMessage → 통합 대상: chat-sendmessage Lambda 선택
  5. 5스테이지 이름: production → 배포 클릭
  6. 6배포 완료 후 화면에 표시되는 WebSocket URL 복사 (형식: wss://{api-id}.execute-api.ap-northeast-2.amazonaws.com/production)

라우트 선택 표현식 $request.body.action은 클라이언트가 보내는 JSON 메시지에서 action 필드 값을 읽어 해당 라우트로 라우팅합니다. 예: {"action": "sendMessage", "message": "안녕!"}sendMessage 라우트로 전달됩니다.

Step 4: wscat으로 테스트

두 개의 터미널을 열어 실시간 채팅을 테스트합니다.

코드
# wscat 설치 (Node.js 필요)
npm install -g wscat
 
# WebSocket 연결 (URL을 실제 값으로 교체)
wscat -c wss://{api-id}.execute-api.ap-northeast-2.amazonaws.com/production
 
# 연결 성공 메시지가 나타나면 아래 명령으로 메시지 전송:
> {"action": "sendMessage", "message": "안녕하세요! 저는 사용자 A입니다."}
 
# 사용자 B가 보낸 메시지가 여기에 수신됩니다
< {"action":"message","sender":"abc123","message":"반갑습니다!","timestamp":"..."}
코드
# 두 번째 터미널에서 동일한 WebSocket에 연결
wscat -c wss://{api-id}.execute-api.ap-northeast-2.amazonaws.com/production
 
# 사용자 A가 보낸 메시지가 여기에 수신됩니다
< {"action":"message","sender":"def456","message":"안녕하세요! 저는 사용자 A입니다.","timestamp":"..."}
 
# 사용자 B도 메시지를 보내봅니다
> {"action": "sendMessage", "message": "반갑습니다!"}

Step 5: 완성 후 통합 테스트

전체 시스템의 동작을 체계적으로 검증합니다.

진행률 0/6
  1. 1연결 테스트: wscat으로 연결 → DynamoDB 콘솔에서 chat-connections 테이블에 connectionId가 저장되었는지 확인
  2. 2브로드캐스트 테스트: 터미널 2~3개를 동시에 연결 → 한 곳에서 메시지 전송 → 모든 터미널에서 수신되는지 확인
  3. 3연결 해제 테스트: wscat에서 Ctrl+C로 종료 → DynamoDB에서 해당 connectionId가 삭제되었는지 확인
  4. 4Stale 연결 정리 테스트: 네트워크를 강제 종료(랜 케이블 뽑기 시뮬레이션)하여 $disconnect가 호출되지 않는 상황 만들기 → 다른 사용자가 메시지 보내면 410 처리로 자동 정리되는지 확인
  5. 5동시 접속 테스트: 3개 이상의 터미널에서 동시에 메시지를 주고받으며 순서와 안정성 확인
  6. 6CloudWatch Logs 확인: 각 Lambda 함수의 로그에서 정상 동작 여부 확인

트러블슈팅

wscat 연결 시 "Unexpected server response: 500" 에러: chat-connect Lambda 함수에 오류가 있을 가능성이 높습니다. CloudWatch Logs에서 해당 함수의 에러를 확인하세요. 가장 흔한 원인은 DynamoDB 권한 부족 또는 테이블 이름 오타입니다.

메시지를 보냈는데 다른 터미널에 수신되지 않으면:

  1. chat-sendmessage Lambda의 CloudWatch 로그를 확인합니다
  2. ApiGatewayManagementApiClient의 endpoint가 올바른지 확인 — https://{domainName}/{stage} 형식이어야 합니다
  3. Lambda 실행 역할에 execute-api:ManageConnections 권한이 있는지 확인합니다

"ForbiddenException" 에러 — postToConnection 호출 시: Lambda 실행 역할에 API Gateway 연결 관리 권한이 필요합니다. IAM 정책에 다음 Action을 추가하세요: execute-api:ManageConnections, 리소스: arn:aws:execute-api:ap-northeast-2:ACCOUNT_ID:{api-id}/*

wscat이 설치되지 않으면: Node.js 18+ 이 설치되어 있는지 확인하세요. npm install -g wscat 실패 시 npx wscat -c wss://... 으로 직접 실행할 수도 있습니다.

핵심 개념 확인

확장 아이디어

기본 채팅 앱이 완성되었으니, 더 발전시켜 보세요:

  1. 채팅방 기능: DynamoDB에 roomId 필드를 추가하여 방별로 메시지를 분리. GSI로 roomId별 연결 조회
  2. 메시지 저장: DynamoDB Messages 테이블을 추가하여 채팅 이력 저장 및 조회 기능
  3. 타이핑 표시: typing 액션을 추가하여 "상대방이 입력 중입니다..." 표시
  4. Cognito 인증: $connect 라우트에 Cognito 인증을 연동하여 사용자 이름으로 메시지 표시
  5. 파일 공유: S3 Presigned URL을 생성하여 이미지/파일 공유 기능 구현

학습 정리

핵심 치트시트

리소스 정리

실습 완료 후 반드시 아래 순서대로 리소스를 정리하여 불필요한 과금을 방지하세요.

  1. API Gateway → WebSocket API 삭제
  2. Lambda 함수 삭제 (chat-connect, chat-disconnect, chat-sendmessage)
  3. IAM 역할 삭제 (Lambda 실행 역할)
  4. DynamoDB → chat-connections 테이블 삭제
  5. CloudWatch 로그 그룹 삭제