Node.js TCP 스트림 문제 해결

seungjun.dev·2025년 8월 8일

Node.js

목록 보기
5/13

문제점

  • TCP는 데이터를 물처럼 연속적으로 보내는 '스트림' 방식

  • 그래서 클라이언트가 { "type": "login" } 이라는 데이터를 한 번에 보내도, 서버는 아래처럼 여러 조각으로 나누어 받을 수 있음

    • 수신 1: { "type":
    • 수신 2: "logi
    • 수신 3: n" }
  • 이 상태에서 각 조각을 받을 때마다 JSON.parse()를 시도하면, 문법이 완성되지 않았으므로 에러가 발생할 수 있다.

해결책

  • 데이터 조각(청크)가 올 때마다 바로 처리하지 않고, 임시 저장소(Buffer)에 계속 모아둔다.
  • 또한 완전한 JSON 메시지 하나가 도착했음을 식별할 수 있는 구분자를 사용한다.
  • 가장 간단하게는 메시지 끝에 줄바꿈 문자(\n)를 붙일 수 있다.

클라이언트 측

  • 보낼 JSON 문자열 끝에 \n을 붙여서 전송
    '{ "type": "login" }\n'
const net = require('net');

const HOST = '127.0.0.1';
const PORT = 9999;

// 새로운 소켓 객체를 생성하여 서버에 연결
const client = net.createConnection({ host: HOST, port: PORT }, () => {
    console.log(`✅ 서버(${HOST}:${PORT})에 연결되었습니다.`);

    // 보낼 메시지 리스트 (JavaScript 객체 배열)
    const messages = [
        { type: "login", user: "neo" },
        { type: "message", text: "Follow the white rabbit." }
    ];

    messages.forEach((msg, index) => {
        // 메시지 전송에 약간의 시간차를 두어 스트림 환경을 시뮬레이션
        setTimeout(() => {
            // 1. JavaScript 객체를 JSON 문자열로 변환
            const jsonString = JSON.stringify(msg);
            
            // 2. 메시지 끝에 구분자('\n') 추가
            const messageToSend = jsonString + '\n';
            console.log(`📤 전송 시도: ${messageToSend.trim()}`);

            // 3. 서버로 데이터 전송 (문자열을 보내면 자동으로 UTF-8로 인코딩됨)
            client.write(messageToSend);
        }, index * 500); // 0ms, 500ms 간격으로 전송
    });

    // 모든 메시지를 보낸 후 연결 종료 예약
    setTimeout(() => {
        console.log('🏁 모든 메시지를 전송하여 연결을 종료합니다.');
        client.end(); // 'FIN' 패킷을 보내 정상적으로 연결을 종료
    }, messages.length * 500);
});

// 서버로부터 데이터를 받을 경우 (이 예제에서는 서버가 응답을 보내지 않음)
client.on('data', (data) => {
    console.log('서버로부터 데이터 수신:', data.toString());
});

// 'close' 이벤트: 연결이 완전히 종료되었을 때 발생
client.on('close', () => {
    console.log('🔗 서버와의 연결이 종료되었습니다.');
});

// 'error' 이벤트: 오류 발생 시
client.on('error', (err) => {
    console.error('TCP 클라이언트 오류:', err.message);
});

서버 측

  • 데이터를 받을 때마다 임시 저장소(Buffer)에 추가, 임시 저장소에 \n 문자가 있는지 확인
    \n이 발견되면, 그 앞까지의 문자열을 잘라내 완전한 JSON 메시지로 간주하고 처리
// Node.js의 내장 'net' 모듈을 불러옵니다.
const net = require('net');

const HOST = '127.0.0.1';
const PORT = 9999;

// TCP 서버 생성
const server = net.createServer((socket) => {
    // 'socket'은 현재 연결된 클라이언트를 나타내는 객체입니다.
    console.log(`🤝 클라이언트(${socket.remoteAddress}:${socket.remotePort})가 연결되었습니다.`);

    // 1. 클라이언트별로 독립적인 버퍼를 생성합니다.
    let buffer = '';

    // 2. 'data' 이벤트 리스너: 클라이언트로부터 데이터 조각(chunk)을 수신할 때마다 실행됩니다.
    socket.on('data', (chunk) => {
        // 수신한 데이터(Buffer 객체)를 문자열로 변환하여 버퍼에 추가합니다.
        buffer += chunk.toString();

        // 3. 버퍼에 구분자('\n')가 있는지 확인하고, 있다면 처리합니다.
        let delimiterIndex;
        while ((delimiterIndex = buffer.indexOf('\n')) !== -1) {
            // 4. 구분자 앞부분을 잘라내 완전한 메시지를 만듭니다.
            const message = buffer.substring(0, delimiterIndex);
            // 4-1. 버퍼에서는 처리한 메시지 부분을 삭제합니다.
            buffer = buffer.substring(delimiterIndex + 1);

            if (message) {
                try {
                    // 5. JSON 문자열을 JavaScript 객체로 파싱합니다.
                    const json = JSON.parse(message);
                    console.log(`📥 수신 및 처리 완료:`, json);
                } catch (e) {
                    console.error('⚠️ JSON 파싱 오류 발생:', message);
                }
            }
        }
    });

    // 'end' 이벤트 리스너: 클라이언트가 연결을 종료했을 때 실행됩니다.
    socket.on('end', () => {
        console.log('🔌 클라이언트와의 연결이 종료되었습니다.');
    });

    // 'error' 이벤트 리스너: 오류 발생 시 실행됩니다.
    socket.on('error', (err) => {
        console.error('💥 소켓 오류:', err.message);
    });
});

// 지정된 포트와 호스트에서 클라이언트 연결을 수신 대기합니다.
server.listen(PORT, HOST, () => {
    console.log(`📢 서버가 ${HOST}:${PORT}에서 클라이언트 연결을 기다립니다...`);
});
profile
Frontend Engineer | Microsoft Student Ambassadors Alumni

0개의 댓글