[Flutter]웹소켓 클라이언트 제작하기

한상욱·2023년 12월 18일
0

Flutter

목록 보기
10/26
post-thumbnail

들어가며

수많은 예제에서 주로 다루는 인터넷 통신은 REST 통신입니다. 이는 간단하게 설명하자면 요청과 응답 사이에서 HTTP를 이용해서 JSON을 주고받는 통신이에요. 그런데, 상대방과의 실시간 채팅에 경우 이러한 통신방식이 적절할까요? 아닙니다. 채팅이 오갈때마다 요청과 응답을 주고받는 것은 굉장히 비효율적이라고 할 수 있습니다. 이를 해소하기 위해서 웹소켓을 이용하여 실시간 통신을 구축할 수 있습니다. Flutter에서는 웹소켓통신의 클라이언트기능을 제작해야 그 기능을 사용할 수 있겠죠? 이 글에서 한번 Flutter프로젝트에서 웹소켓 클라이언트를 구축하는 방법에 대해서 간단하게 알아보겠습니다.

웹소켓(Web Socket)이란?

웹소켓은 클라이언트와 서버사이에 실시간 양방향 스트림을 만들어주는 기술입니다. 우리가 사용하는 HTTP 역시도 사실 웹소켓 방식의 하나입니다. 자세한 이론적인 내용은 나중에 작성하도록 하겠습니다.

Web Socket Channel

Flutter 공식문서에 따르면 웹소켓 클라이언트를 구현하기 위하여 web_socket_channel이라는 라이브러리를 이용한다고 합니다. 우리도 이거 사용해보겠습니다. 근데, 웹소켓 클라이언트를 만들려면 먼저 서버가 있어야겠죠? 공식문서에서는 테스트용 웹소켓서버를 이용합니다. 그래서 우리도 이용할겁니다.

공식문서 예제 따라해보기

공식문서에서 제공하는 예제는 연결한 웹소켓 서버와의 통신예제인데요. 연결하는 서버는 연습용 서버로 우리가 보낸 데이터를 다시 echo하는 서버입니다. 그러니까 우리가 데이터를 전송하면 그친구가 똑같은걸 뱉는다는 소리죠.

이제 연습을 위한 예제앱을 구성해보겠습니다. 먼저 기본UI는 아래와 같습니다.

import 'package:flutter/material.dart';

class App extends StatefulWidget {
  const App({super.key});

  
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  late final TextEditingController _controller;

  
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Flutter Web Socket Example"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.send),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            const TextField(),
            Container(),
          ],
        ),
      ),
    );
  }
}

_controller는 TextField 위젯을 이용하기 위해서 선언한 것입니다. 이제 웹소켓 연결을 위한 작업을 수행하도록 하겠습니다.

가장 먼저 web_socket_channel 라이브러리를 추가합니다.

$ flutter pub add web_socket_channel

이제 우리가 연결한 웹소켓 서버의 정보를 앱의 상단에 선언하겠습니다. 이것은 예제라서 대충 위에다가 선언한 것이지, 상황에 따라 적절하게 선언하시면 됩니다.

...
class _AppState extends State<App> {
  late final WebSocketChannel _channel;
  late final TextEditingController _controller;

  
  void initState() {
    super.initState();
    _controller = TextEditingController();
    _channel =
        WebSocketChannel.connect(Uri.parse('wss://echo.websocket.events'));
  }
  ...

만약 더이상 서버를 이용하지 않는다면 소켓서버와의 연결을 끊어야됩니다. 그러기 위해서 하단과 같이 작성할 수 있습니다.

...
  
  void dispose() {
    super.dispose();
    _controller.dispose();
    _channel.sink.close();
  }
  ...

현재 연결은 스트림이라고 했죠? 그렇기 때문에 StreamBuilder를 이용하여 소켓서버의 데이터를 화면에 그려줄 수 있습니다.

	...
    StreamBuilder(
    	stream: _channel.stream,
        builder: (context, snapshot) {
        	if (snapshot.hasData) {
            	return Text(snapshot.data);
            } else {
                return Container();
            }
       	})
	...

데이터가 있다면 데이터를 화면에 나타내게끔 작성하였습니다. 이제 데이터를 보내는 코드를 한번 보시죠.

_channel.sink.add(data);

이 메소드를 이용해서 데이터를 소켓서버에 전송할 수 있습니다. 이제 완성입니다.

전체코드

import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

class App extends StatefulWidget {
  const App({super.key});

  
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  late final WebSocketChannel _channel;
  late final TextEditingController _controller;

  
  void initState() {
    super.initState();
    _controller = TextEditingController();
    _channel =
        WebSocketChannel.connect(Uri.parse('wss://echo.websocket.events'));
  }

  
  void dispose() {
    super.dispose();
    _controller.dispose();
    _channel.sink.close();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Flutter Web Socket Example"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _channel.sink.add(_controller.text.toString());
        },
        child: const Icon(Icons.send),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            TextField(
              controller: _controller,
            ),
            StreamBuilder(
                stream: _channel.stream,
                builder: (context, snapshot) {
                  if (snapshot.hasData) {
                    return Text(snapshot.data);
                  } else {
                    return Container();
                  }
                })
          ],
        ),
      ),
    );
  }
}
profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

0개의 댓글