표준 웹 애플리케이션은 일반적으로 HTTP와 같은 특정 통신 프로토콜과 WebSocket, WebRTC와 같은 API로 제한됩니다. 이러한 기능은 강력하지만 악용을 방지하기 위해 엄격하게 제한되도록 설계되었습니다. 원시 TCP 또는 UDP 연결을 설정할 수 없으므로 웹 앱이 자체 비웹 프로토콜을 사용하는 기존 시스템이나 하드웨어 기기와 통신하는 기능이 제한됩니다. 예를 들어 웹 기반 SSH 클라이언트를 빌드하거나, 로컬 프린터에 연결하거나, IoT 기기 모음을 관리할 수 있습니다. 이전에는 브라우저 플러그인이나 네이티브 도우미 애플리케이션이 필요했습니다.
Direct Sockets API는 분리형 웹 앱(IWA)이 중계 서버 없이 직접 TCP 및 UDP 연결을 설정할 수 있도록 하여 이러한 제한사항을 해결합니다. IWA를 사용하면 엄격한 콘텐츠 보안 정책 (CSP) 및 교차 출처 격리와 같은 추가 보안 조치 덕분에 이 API를 안전하게 노출할 수 있습니다.
사용 사례
표준 WebSocket 대신 직접 소켓을 사용해야 하는 경우는 언제인가요?
- IoT 및 스마트 기기: HTTP가 아닌 원시 TCP/UDP를 사용하는 하드웨어와 통신합니다.
- 기존 시스템: 이전 메일 서버 (SMTP/IMAP), IRC 채팅 서버 또는 프린터에 연결합니다.
- 원격 데스크톱 및 터미널: SSH, Telnet 또는 RDP 클라이언트 구현
- P2P 시스템: 분산 해시 테이블 (DHT) 또는 복원력 있는 공동작업 도구 (예: IPFS)를 구현합니다.
- 미디어 브로드캐스팅: UDP를 활용하여 콘텐츠를 여러 엔드포인트에 한 번에 스트리밍 (멀티캐스팅)하여 소매 키오스크 네트워크 전반에서 조정된 동영상 재생과 같은 사용 사례를 지원합니다.
- 서버 및 리스너 기능:
TCPServerSocket를 사용하거나UDPSocket에 바인딩하여 수신 TCP 연결 또는 UDP 데이터그램의 수신 엔드포인트로 작동하도록 IWA를 구성합니다.
Direct Sockets의 기본 요건
직접 소켓을 사용하려면 먼저 기능적 IWA를 설정해야 합니다. 그런 다음 페이지에 Direct Sockets를 통합할 수 있습니다.
권한 정책 추가
Direct Sockets를 사용하려면 IWA 매니페스트에서 permissions_policy 객체를 구성해야 합니다. API를 명시적으로 사용 설정하려면 direct-sockets 키를 추가해야 합니다. 또한 cross-origin-isolated 키를 포함해야 합니다. 이 키는 직접 소켓에만 적용되는 것은 아니지만 모든 IWA에 필요하며 문서가 교차 출처 격리가 필요한 API에 액세스할 수 있는지 여부를 결정합니다.
{
"permissions_policy": {
"direct-sockets": ["self"],
"cross-origin-isolated": ["self"]
}
}
direct-sockets 키는 new TCPSocket(...), new
TCPServerSocket(...) 또는 new UDPSocket(...) 호출이 허용되는지 여부를 결정합니다. 이 정책이 설정되지 않은 경우 이러한 생성자는 NotAllowedError로 즉시 거부합니다.
TCPSocket 구현
애플리케이션은 TCPSocket 인스턴스를 만들어 TCP 연결을 요청할 수 있습니다.
연결 열기
연결을 열려면 new 연산자와 열린 프라미스 await를 사용합니다.
TCPSocket 생성자는 지정된 remoteAddress 및 remotePort를 사용하여 연결을 시작합니다.
const remoteAddress = 'example.com';
const remotePort = 7;
// Configure options like keepAlive or buffering
const options = {
keepAlive: true,
keepAliveDelay: 720000
};
let tcpSocket = new TCPSocket(remoteAddress, remotePort, options);
// Wait for the connection to be established
let { readable, writable } = await tcpSocket.opened;
선택적 구성 객체를 사용하면 세분화된 네트워크 제어가 가능합니다. 이 특정 사례에서는 비활성 기간 동안 연결을 유지하기 위해 keepAliveDelay이 720, 000밀리초로 설정됩니다. 개발자는 여기에서 noDelay와 같은 다른 속성도 구성할 수 있습니다. noDelay는 시스템이 작은 패킷을 일괄 처리하지 못하도록 Nagle 알고리즘을 사용 중지하여 지연 시간을 줄일 수 있습니다. sendBufferSize 및 receiveBufferSize는 처리량을 관리합니다.
위 스니펫의 마지막 부분에서 코드는 핸드셰이크가 완료된 후에만 확인되는 열린 약속을 기다려 데이터 전송에 필요한 읽기 및 쓰기 스트림이 포함된 TCPSocketOpenInfo 객체를 반환합니다.
읽기 및 쓰기
소켓이 열리면 표준 스트림 API 인터페이스를 사용하여 소켓과 상호작용합니다.
- 쓰기: 쓰기 가능한 스트림은
BufferSource(예:ArrayBuffer)를 허용합니다. - 읽기: 읽을 수 있는 스트림은
Uint8Array데이터를 생성합니다.
// Writing data
const writer = writable.getWriter();
const encoder = new TextEncoder();
await writer.write(encoder.encode("Hello Server"));
// Call when done
writer.releaseLock();
// Reading data
const reader = readable.getReader();
const { value, done } = await reader.read();
if (!done) {
const decoder = new TextDecoder();
console.log("Received:", decoder.decode(value));
}
// Call when done
reader.releaseLock();
BYOB로 최적화된 읽기
메모리 할당 관리가 중요한 고성능 애플리케이션의 경우 API는 'Bring Your Own Buffer' (BYOB) 읽기를 지원합니다. 브라우저가 수신된 데이터 청크마다 새 버퍼를 할당하도록 하는 대신 미리 할당된 버퍼를 리더에 전달할 수 있습니다. 이렇게 하면 기존 메모리에 직접 데이터를 써서 가비지 컬렉션 오버헤드가 줄어듭니다.
// 1. Get a BYOB reader explicitly
const reader = readable.getReader({ mode: 'byob' });
// 2. Allocate a reusable buffer (e.g., 4KB)
let buffer = new Uint8Array(4096);
// 3. Read directly into the existing buffer
const { value, done } = await reader.read(buffer);
if (!done) {
// 'value' is a view of the data written directly into your buffer
console.log("Bytes received:", value.byteLength);
}
reader.releaseLock();
UDPSocket 구현
UDPSocket 클래스는 UDP 통신을 허용합니다. 옵션을 구성하는 방식에 따라 두 가지 모드로 작동합니다.
연결 모드
이 모드에서 소켓은 단일 특정 대상과 통신합니다. 이는 표준 클라이언트-서버 작업에 유용합니다.
// Connect to a specific remote host
let udpSocket = new UDPSocket({
remoteAddress: 'example.com',
remotePort: 7 });
let { readable, writable } = await udpSocket.opened;
바운드 모드
이 모드에서는 소켓이 로컬 IP 엔드포인트에 바인드됩니다. 임의 소스에서 데이터그램을 수신하고 임의 대상으로 전송할 수 있습니다. 이는 로컬 검색 프로토콜이나 서버와 유사한 동작에 자주 사용됩니다.
// Bind to all interfaces (IPv6)
let udpSocket = new UDPSocket({
localAddress: '::'
// omitting localPort lets the OS pick one
});
// localPort will tell you the OS-selected port.
let { readable, writable, localPort } = await udpSocket.opened;
UDP 메시지 처리
바이트의 TCP 스트림과 달리 UDP 스트림은 데이터와 원격 주소 정보를 포함하는 UDPMessage 객체를 처리합니다. 다음 코드는 '바운드 모드'에서 UDPSocket를 사용할 때 입력/출력 작업을 처리하는 방법을 보여줍니다.
// Writing (Bound Mode requires specifying destination)
const writer = writable.getWriter();
await writer.write({
data: new TextEncoder().encode("Ping"),
remoteAddress: '192.168.1.50',
remotePort: 8080
});
// Reading
const reader = readable.getReader();
const { value } = await reader.read();
// value contains: { data, remoteAddress, remotePort }
console.log(`Received from ${value.remoteAddress}:`, value.data);
소켓이 특정 피어에 잠겨 있는 '연결된 모드'와 달리 바인드된 모드에서는 소켓이 임의의 대상과 통신할 수 있습니다. 따라서 쓰기 가능한 스트림에 데이터를 쓸 때는 각 패킷의 remoteAddress 및 remotePort를 명시적으로 지정하는 UDPMessage 객체를 전달하여 소켓에 특정 데이터그램을 라우팅할 위치를 정확하게 알려야 합니다. 마찬가지로 읽기 가능한 스트림에서 읽을 때 반환된 값에는 데이터 페이로드뿐만 아니라 발신자의 remoteAddress 및 remotePort도 포함되어 애플리케이션이 수신되는 모든 패킷의 출처를 식별할 수 있습니다.
참고: '연결된 모드'에서 UDPSocket를 사용하면 소켓이 특정 피어에 효과적으로 잠겨 I/O 프로세스가 간소화됩니다. 이 모드에서는 대상이 이미 고정되어 있으므로 쓰기 시 remoteAddress 및 remotePort 속성이 사실상 no-op입니다. 마찬가지로 메시지를 읽을 때 소스가 연결된 피어임을 보장하므로 이러한 속성은 null을 반환합니다.
멀티캐스트 지원
여러 키오스크에서 동영상 재생을 동기화하거나 로컬 기기 검색 (예: mDNS)을 구현하는 등의 사용 사례의 경우 Direct Sockets는 멀티캐스트 UDP를 지원합니다. 이를 통해 메시지를 단일 특정 피어가 아닌 네트워크의 모든 구독자가 수신할 수 있는 '그룹' 주소로 전송할 수 있습니다.
멀티캐스트 권한
멀티캐스트 기능을 사용하려면 IWA 매니페스트에 특정 direct-sockets-multicast 권한을 추가해야 합니다. 이는 표준 직접 소켓 권한과 다르며 멀티캐스트는 비공개 네트워크에서만 사용되므로 필요합니다.
{
"permissions_policy": {
"direct-sockets": ["self"],
"direct-sockets-multicast": ["self"],
"direct-sockets-private": ["self"],
"cross-origin-isolated": ["self"]
}
}
멀티캐스트 데이터그램 전송
멀티캐스트 그룹으로 전송하는 것은 패킷 동작을 제어하는 특정 옵션이 추가된 표준 UDP '연결 모드'와 매우 유사합니다.
const MULTICAST_GROUP = '239.0.0.1';
const PORT = 12345;
const socket = new UDPSocket({
remoteAddress: MULTICAST_GROUP,
remotePort: PORT,
// Time To Live: How many router hops the packet can survive (default: 1)
multicastTimeToLive: 5,
// Loopback: Whether to receive your own packets (default: true)
multicastLoopback: true
});
const { writable } = await socket.opened;
// Write to the stream as usual...
멀티캐스트 데이터그램 수신
멀티캐스트 트래픽을 수신하려면 '바운드 모드'(일반적으로 0.0.0.0 또는 ::에 바인딩)에서 UDPSocket를 열고 MulticastController를 사용하여 특정 그룹에 참여해야 합니다. 동일한 기기의 여러 애플리케이션이 동일한 포트를 수신해야 하는 기기 검색 프로토콜에 필수적인 multicastAllowAddressSharing 옵션 (Unix의 SO_REUSEADDR와 유사)을 사용할 수도 있습니다.
const socket = new UDPSocket({
localAddress: '0.0.0.0', // Listen on all interfaces
localPort: 12345,
multicastAllowAddressSharing: true // Allow multiple applications to bind to the same address / port pair.
});
// The open info contains the MulticastController
const { readable, multicastController } = await socket.opened;
// Join the group to start receiving packets
await multicastController.joinGroup('239.0.0.1');
const reader = readable.getReader();
// Read the stream...
const { value } = await reader.read();
console.log(`Received multicast from ${value.remoteAddress}`);
// When finished, you can leave the group (this is an optional, but recommended practice)
await multicastController.leaveGroup('239.0.0.1');
서버 만들기
또한 이 API는 수신 TCP 연결을 수락하는 TCPServerSocket을 지원하므로 IWA가 로컬 서버 역할을 할 수 있습니다. 다음 코드는 TCPServerSocket 인터페이스를 사용하여 TCP 서버를 설정하는 방법을 보여줍니다.
// Listen on all interfaces (IPv6)
let tcpServerSocket = new TCPServerSocket('::');
// Accept connections via the readable stream
let { readable } = await tcpServerSocket.opened;
let reader = readable.getReader();
// Wait for a client to connect
let { value: clientSocket } = await reader.read();
// 'clientSocket' is a standard TCPSocket you can now read/write to
'::' 주소로 클래스를 인스턴스화하면 서버가 수신 시도를 수신하기 위해 사용 가능한 모든 IPv6 네트워크 인터페이스에 바인딩됩니다. 기존 콜백 기반 서버 API와 달리 이 API는 웹의 Streams API 패턴을 활용합니다. 수신 연결은 ReadableStream로 제공됩니다. reader.read()를 호출하면 애플리케이션이 대기열에서 다음 연결을 기다리고 수락하여 해당 특정 클라이언트와의 양방향 통신을 지원하는 완전한 기능의 TCPSocket 인스턴스인 값으로 확인합니다.
Chrome DevTools로 직접 소켓 디버깅
Chrome 138부터 Chrome DevTools의 네트워크 패널 내에서 직접 Direct Sockets 트래픽을 디버그할 수 있으므로 외부 패킷 스니퍼가 필요하지 않습니다. 이 도구를 사용하면 표준 HTTP 요청과 함께 TCPSocket 연결과 UDPSocket 트래픽 (바운드 모드와 연결 모드 모두)을 모니터링할 수 있습니다.
앱의 네트워크 활동을 검사하려면 다음 단계를 따르세요.
- Chrome DevTools에서 네트워크 패널을 엽니다.
- 요청 표에서 소켓 연결을 찾아 선택합니다.
- 메시지 탭을 열어 전송 및 수신된 모든 데이터의 로그를 확인합니다.

이 뷰는 16진수 뷰어를 제공하므로 TCP 및 UDP 메시지의 원시 바이너리 페이로드를 검사하여 프로토콜 구현이 바이트 단위로 완벽한지 확인할 수 있습니다.
데모
IWA Kitchen Sink에는 여러 탭이 있는 앱이 있으며 각 탭은 Direct Sockets, Controlled Frame 등 다양한 IWA API를 보여줍니다.
또는 텔넷 클라이언트 데모에는 사용자가 대화형 터미널을 통해 TCP/IP 서버에 연결할 수 있는 격리된 웹 앱이 포함되어 있습니다. 즉, Telnet 클라이언트입니다.
결론
Direct Sockets API는 웹 애플리케이션이 네이티브 래퍼 없이 지원할 수 없었던 원시 네트워크 프로토콜을 처리할 수 있도록 지원하여 중요한 기능 격차를 해소합니다. 단순한 클라이언트 연결을 넘어섭니다. TCPServerSocket를 사용하면 애플리케이션이 수신 연결을 수신 대기할 수 있으며 UDPSocket는 피어 투 피어 통신과 로컬 네트워크 검색을 위한 유연한 모드를 제공합니다.
최신 Streams API를 통해 이러한 원시 TCP 및 UDP 기능을 노출함으로써 이제 JavaScript에서 직접 SSH, RDP 또는 맞춤 IoT 표준과 같은 기존 프로토콜의 모든 기능을 갖춘 구현을 빌드할 수 있습니다. 이 API는 하위 수준 네트워크 액세스 권한을 부여하므로 보안에 미치는 영향이 큽니다. 따라서 분리된 웹 앱 (IWA)으로 제한되어 이러한 권한은 엄격한 보안 정책을 적용하는 신뢰할 수 있는 명시적으로 설치된 애플리케이션에만 부여됩니다. 이 균형을 통해 강력한 기기 중심 애플리케이션을 빌드하면서 사용자가 웹 플랫폼에서 기대하는 안전을 유지할 수 있습니다.