Go WebSocket 프로그래밍: 실시간 앱을 손쉽게 구축

발행: (2025년 12월 29일 오전 10:00 GMT+9)
13 min read
원문: Dev.to

Source: Dev.to

Hey there, Go developers! Ready to dive into the world of real‑time web applications? WebSocket is your ticket to building chat apps, live dashboards, or collaborative tools that feel instantaneous. Paired with Go’s concurrency superpowers, it’s a match made in heaven. In this guide we’ll walk through WebSocket fundamentals, practical Go implementations, and real‑world tips—perfect for devs with 1–2 years of Go experience. Let’s build something awesome!

왜 WebSocket과 Go인가?

WebSocket은 TCP 기반 프로토콜로, 클라이언트와 서버 사이에 지속적인 양방향 연결을 생성하여 HTTP 폴링의 번거로운 요청‑응답 사이클을 없앱니다. 마치 비둘기를 주고받는 대신 전화 통화를 하는 것과 같습니다. 채팅 방이나 주식 시세와 같은 실시간 애플리케이션에 최적입니다.

Go가 여기서 빛나는 이유

  • Goroutines – 최소 메모리로 수천 개의 연결을 처리합니다.
  • Simplicity – 깔끔한 문법과 표준 라이브러리 덕분에 설정이 매우 간단합니다.
  • Performance – 낮은 지연 시간과 효율적인 자원 사용을 제공합니다.

비교: WebSocket vs. HTTP 기반 접근 방식

FeaturePollingLong PollingWebSocket
Communication주기적인 요청서버가 연결을 유지지속적인 양방향
Latency높음중간낮음
Use Case뉴스 피드알림채팅, 게임, 실시간 대시보드

Source:

시작하기: 첫 번째 WebSocket 서버

WebSocket 연결은 HTTP 핸드셰이크로 시작되며, 클라이언트가 Upgrade 헤더를 보내 WebSocket으로 전환합니다. 연결이 성립되면 양쪽 모두 언제든지 메시지를 보낼 수 있습니다.

아래는 gorilla/websocket 패키지를 사용한 최소한의 에코 서버 예시입니다 (표준 라이브러리보다 훨씬 편리합니다).

간단한 에코 서버

package main

import (
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	// 실제 서비스에서는 반드시 Origin을 검증해야 합니다!
	CheckOrigin: func(r *http.Request) bool { return true },
}

func handleConnections(w http.ResponseWriter, r *http.Request) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("Upgrade failed: %v", err)
		return
	}
	defer ws.Close()

	for {
		var msg string
		if err := ws.ReadJSON(&msg); err != nil {
			log.Printf("Read error: %v", err)
			break
		}
		log.Printf("Received: %s", msg)

		if err := ws.WriteJSON(msg); err != nil {
			log.Printf("Write error: %v", err)
			break
		}
	}
}

func main() {
	http.HandleFunc("/ws", handleConnections)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

무슨 일이 일어나고 있나요?

  • upgrader.Upgrade는 HTTP 요청을 WebSocket 연결로 변환합니다.
  • ws.ReadJSON / ws.WriteJSON 은 JSON 형식의 입출력 메시지를 처리합니다.
  • defer ws.Close() 는 연결을 닫아 자원 누수를 방지합니다.

서버를 go run main.go 로 실행한 뒤, WebSocket 클라이언트(wscat 혹은 브라우저 기반 클라이언트 등)로 연결해 보세요. 메시지를 보내면 서버가 그대로 되돌려 줍니다. 멋지죠? 이제 다중 사용자가 참여할 수 있는 채팅 방으로 확장해 봅시다.

다중 사용자 채팅 방 만들기

이제 여러 사용자가 실시간으로 메시지를 주고받을 수 있는 채팅 서버를 만들겠습니다. gorilla/websocket 과 Go의 동시성 프리미티브를 사용해 클라이언트를 관리하고 메시지를 브로드캐스트합니다.

채팅 서버 (Go)

package main

import (
	"log"
	"net/http"
	"sync"

	"github.com/gorilla/websocket"
)

var (
	upgrader   = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
	clients    = make(map[*websocket.Conn]bool) // 연결된 클라이언트
	broadcast  = make(chan string)             // 브로드캐스트 채널
	clientsMux sync.RWMutex                    // clients 맵을 보호
)

func handleConnections(w http.ResponseWriter, r *http.Request) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("Upgrade failed: %v", err)
		return
	}
	// 새로운 클라이언트 등록
	clientsMux.Lock()
	clients[ws] = true
	clientsMux.Unlock()

	// 함수가 반환될 때 정리
	defer func() {
		clientsMux.Lock()
		delete(clients, ws)
		clientsMux.Unlock()
		ws.Close()
	}()

	for {
		var msg string
		if err := ws.ReadJSON(&msg); err != nil {
			log.Printf("Read error: %v", err)
			break
		}
		// 받은 메시지를 broadcast 채널에 보냄
		broadcast 
	}
}

프론트엔드 HTML & CSS

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Go WebSocket Chat</title>
    <style>
        #messages {
            border: 1px solid #ccc;
            padding: 10px;
            height: 300px;
            overflow-y: scroll;
            margin-bottom: 10px;
        }
        #messageInput {
            width: 70%;
        }
        button {
            width: 25%;
        }
    </style>
</head>
<body>
    <div id="messages"></div>
    <input id="messageInput" placeholder="Type a message..." />
    <button onclick="sendMessage()">Send</button>

    <script src="chat.js"></script>
</body>
</html>

프론트엔드 JavaScript (chat.js)

const ws = new WebSocket("ws://localhost:8080/ws");
const messagesDiv = document.getElementById("messages");
const input = document.getElementById("messageInput");

ws.onmessage = function(event) {
    const msg = document.createElement("div");
    msg.textContent = event.data;
    messagesDiv.appendChild(msg);
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
};

function sendMessage() {
    const text = input.value.trim();
    if (text) {
        ws.send(JSON.stringify(text));
        input.value = "";
    }
}

ws.onclose = function() {
    const msg = document.createElement("div");
    msg.textContent = "Connection closed.";
    messagesDiv.appendChild(msg);
};

작동 원리

  1. 서버 측 – 새로운 연결이 들어오면 스레드‑안전한 clients 맵에 저장합니다. 들어온 메시지는 broadcast 채널에 푸시됩니다. 별도의 고루틴(코드에 표시되지 않음)이 이 채널을 읽어 모든 연결된 클라이언트에게 메시지를 전달합니다.
  2. 클라이언트 측 – 브라우저가 ws://localhost:8080/ws 로 WebSocket을 엽니다. 들어오는 메시지는 #messages div에 표시되고, 사용자는 입력 필드를 통해 새 메시지를 보낼 수 있습니다.

팁 및 모범 사례

TipWhy it matters
출처 검증인증되지 않은 도메인이 연결을 여는 것을 방지합니다.
적절한 메시지 형식 사용 (예: typepayload가 포함된 JSON)프로토콜 확장이 용이해집니다 (예: “join”, “leave”, “typing” 추가).
우아한 종료서버가 중지될 때 모든 클라이언트 연결을 닫고 브로드캐스트 고루틴을 중지합니다.
속도 제한 / 백프레셔단일 비정상 클라이언트가 채널을 과도하게 사용하는 것을 방지합니다.
TLS (wss://)프로덕션 환경에서 트래픽을 암호화합니다.
Ping/Pong죽은 연결을 감지합니다; gorilla/websocket이 자동으로 처리할 수 있습니다.

정리

WebSocket과 Go의 가벼운 동시성을 결합하면 실시간 시스템을 구축하는 것이 간단하고 성능도 뛰어납니다. 에코 서버부터 시작해 전체 기능을 갖춘 채팅이나 상상할 수 있는 다른 실시간 업데이트 사용 사례로 확장해 보세요. 즐거운 코딩 되세요! 🚀


프런트엔드 JavaScript (대체 예제)

ws.onmessage = (event) => {
    const messages = document.getElementById("messages");
    const msg = document.createElement("p");
    msg.textContent = event.data;
    messages.appendChild(msg);
    messages.scrollTop = messages.scrollHeight;
};

function sendMessage() {
    const input = document.getElementById("messageInput");
    ws.send(JSON.stringify(input.value));
    input.value = "";
}

작동 방식

  • **clients**는 활성 WebSocket 연결을 추적합니다.
  • **broadcast**는 모든 클라이언트에게 메시지를 배포하는 채널입니다.
  • **sync.RWMutex**는 clients에 대한 스레드‑안전 접근을 보장합니다.

프론트엔드는 ws://localhost:8080/ws에 연결하고, 수신된 메시지를 표시하며, 사용자 입력을 전송합니다.

시도해 보세요:

go run main.go

http://localhost:8080을 여러 브라우저 탭에서 열고 채팅을 시작하세요!

레벨업: Go의 WebSocket 초능력

동시성 마법

Go의 goroutine은 수천 개의 연결을 손쉽게 처리하게 해줍니다. 각 클라이언트는 자체 goroutine에서 실행되어 가볍게 동작합니다. clients 맵과 같은 공유 자원에 안전하게 접근하려면 sync.RWMutex를 사용하세요.

성능 팁

버퍼에 sync.Pool을 활용해 메모리를 최적화합니다:

var bufferPool = sync.Pool{
    New: func() interface{} { return make([]byte, 1024) },
}

func readMessage(ws *websocket.Conn) ([]byte, error) {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf)
    _, data, err := ws.ReadMessage()
    return data, err
}

연결 유지

ping/pong 하트비트를 사용해 끊어진 연결을 감지합니다:

func handlePingPong(ws *websocket.Conn) {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    for range ticker.C {
        if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
            log.Println("Ping failed:", err)
            return
        }
    }
}

보안 우선

오리진을 제한하고 TLS(wss://)를 사용하세요:

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return r.Header.Get("Origin") == "https://yourdomain.com"
    },
}

실제 사용 사례

엔터프라이즈 채팅

10,000명 이상의 사용자를 위해 gorilla/websocket와 Redis Pub/Sub을 결합하여 효율적인 메시지 배포를 구현합니다:

import "github.com/go-redis/redis/v8"

func subscribeMessages(ws *websocket.Conn, roomID string) {
    ctx := context.Background()
    pubsub := rdb.Subscribe(ctx, roomID)
    defer pubsub.Close()
    for {
        msg, err := pubsub.ReceiveMessage(ctx)
        if err != nil {
            log.Printf("Redis error: %v", err)
            return
        }
        ws.WriteJSON(msg.Payload)
    }
}

모니터링 대시보드

매초 업데이트를 푸시합니다:

func pushStatus(ws *websocket.Conn) {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()
    for range ticker.C {
        status := getDeviceStatus() // Fetch your data
        if err := ws.WriteJSON(status); err != nil {
            log.Printf("Write error: %v", err)
            return
        }
    }
}

베스트 프랙티스

  • 연결 관리: context를 사용하여 연결을 우아하게 종료합니다.
  • 메시지 형식: 디버깅에 JSON이 좋으며, 성능을 위해 Protobuf를 고려하세요.
  • 오류 처리: 클라이언트 재연결을 위해 지수 백오프를 구현합니다.
  • 모니터링: Prometheus로 메트릭을 추적합니다(예: 연결 수, 메시지 속도).
  • 프로덕션: wss://를 사용하고, 연결 수를 제한하며, wscat 같은 도구로 테스트합니다.

Try It Out!

채팅 방을 로컬에 배포합니다:

  1. 서버 코드를 main.go 파일에, HTML을 index.html 파일에 저장합니다.

  2. 실행:

    go run main.go
  3. http://localhost:8080에 접속합니다.

Docker

docker build -t chat-room .
docker run -p 8080:8080 chat-room

마무리

Go + WebSocket은 실시간 앱에 강력한 조합입니다. goroutine, channel, 그리고 gorilla/websocket을 활용하면 확장 가능하고 저지연인 시스템을 빠르게 구축할 수 있습니다.

핵심 정리

  • 동시성을 위해 goroutine을 사용하세요.
  • 신뢰성과 보안을 위해 하트비트와 TLS를 구현하세요.
  • sync.Pool과 Redis를 활용해 확장성을 최적화하세요.

다음 단계는?

  • 채팅 방을 추가하세요.
  • 안정적인 메시징을 위해 Kafka를 통합하세요.
  • 하이브리드 솔루션을 위해 gRPC‑Web을 탐색하세요.

질문이나 멋진 WebSocket 프로젝트가 있나요? 아래에 댓글을 남겨 주세요—여러분의 이야기를 듣고 싶습니다!

Back to Blog

관련 글

더 보기 »