반응형
개요
웹 소켓 서버에 대한 기능은 이전 포스팅으로 분리하였으므로,
이번에는 채팅 앱 클라이언트에서 웹 소켓 서버와 통신하고
실제로 실시간 채팅이 이루어지는 기능을 상호작용할 수 있도록 해보자.
[ 시리즈 포스팅 내용 보기 ]
이번 포스팅에서는 아래와 같은 내용을 다룬다.
클라이언트에서 메시지를 보내는 방법
- 단순히 모든 클라이언트에게 메시지를 보내는 방법
- 특정 클라이언트에게 메시지를 보내는 방법
- 포스팅에서는 '귓속말'이라는 기능으로 구현하였다.
[ 각각의 클라이언트 1, 2, 3의 유저가 접속하여 메시지를 나누고 1과 2의 유저가 단 둘만의 메시지를 전달하는 예시 화면 ]
포스팅에서 다루는 플러터 채팅 앱은 깃허브에서 다운로드 할 수 있다.
웹소켓 클라이언트
구조 설계
[ 파일 구조 ]
- main.dart
- 플러터 앱이 실행되는 메인 함수를 포함하는 파일
- 사용자의 식별을 위해 닉네임을 입력 받아 채팅 페이지로 이동하는 페이지를 포함하고 있다.
- socket.dart
- 웹 소켓 서버에 연결하고, 소켓 서버 데이터를 송수신하는 기능을 포함하고 있다.
- chat 폴더
- chat_main.dart
- 채팅 페이지를 구성할 메인 뼈대 역할을 하는 파일
- 해당 페이지에서 채팅 내용을 입력할 영역과 채팅 내용을 보여줄 영역을 지정한다.
- 클라이언트로 전달 받은 데이터를 하위 요소들에게 데이터를 전달하는 역할을 한다.
- 또한, setState 함수를 하위 요소로 전달하여 상태값을 각 클래스와 공유한다.
- chat_area.dart
- 채팅 내용을 보여줄 영역을 구성하는 파일
- 서버에서 전달 받은 데이터를 토대로 채팅 메시지 내용을 구성하는 역할을 한다.
- 메시지의 내용은 생성자를 통해 chat_main.dart로부터 전달 받는다.
- input_text_area.dart
- 채팅 내용을 입력하는 기능으로 구성된 파일
- 작성한 메시지 내용을 사용자와 상호작용하여 서버로 요청하는 역할을 한다.
- 사용자의 이름과 메시지의 내용을 생성자를 통해 chat_main.dart으로부터 전달받고, 해당 값을 전달받은 setState(포스팅에서는 updateMessage로 지정) 함수를 통해 값을 공유한다.
- chat_main.dart
[ 연결 구조 ]
- main.dart
- chat_main.dart
- chat_area.dart
- input_text_area.dart
- socket.dart
- chat_main.dart
기능 구현
main.dart (MainPage)
메인 함수 (main)
- 메인 함수를 통해 메인 파일의 메인 페이지를 처음으로 보여주도록 한다.
void main() async {
runApp(
const MaterialApp(
home: MainPage(), //ChatMainPage(),
),
);
}
메인 페이지 (MainPage)
- 사용자의 이름을 입력 받고, 확인 버튼을 누르면 채팅 페이지로 이동하는 기능을 하는 페이지
- 텍스트 에디팅 컨트롤러로 입력 받은 사용자 이름을 chat_main.dart 파일의 ChatMainPage 클래스로 값을 넘겨준다.
class MainPage extends StatelessWidget {
const MainPage({super.key});
@override
Widget build(BuildContext context) {
// 텍스트필드 컨트롤러
TextEditingController textEditingController = TextEditingController();
return Scaffold(
body: Center(
child: Container(
width: 250,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.grey,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 유저 이름 입력
SizedBox(
width: 200,
child: TextField(
controller: textEditingController,
decoration: const InputDecoration(
label: Center(child: Text("사용자 이름 입력")),
),
textAlign: TextAlign.center,
),
),
ElevatedButton(
onPressed: () {
String txtValue = textEditingController.text;
txtValue = txtValue.trim();
if (txtValue != "") {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return ChatMainPage(
username: txtValue,
);
}));
}
},
child: const Text("확인"),
)
],
),
),
),
);
}
}
socket.dart (FlutterWebSocket)
클라이언트가 웹 소켓 서버에 연결하고, 메시지 데이터를 보내기 위한 클래스를 정의한다.
웹 소켓 서버에 연결
웹 소켓 서버를 연결하기 위한 함수 getSocket을 정의한다.
반환 값으로 연결된 소켓 인스턴스를 반환한다.
List messageList = [];
String SERVER = "ws://192.168.10.103:4001";
// 웹 소켓 서버 연결
Future<WebSocket> getSocket() async {
WebSocket socket = await WebSocket.connect(SERVER);
return socket;
}
소켓 서버에 데이터 송신
소켓 서버에 데이터를 보낼 함수 addMessage를 정의한다.
해당 함수에서는 JSON 데이터로 내보내기 위해 맵 형식으로 구성한다.
웹 소켓 서버에 데이터를 보내는 구성 형식은 다음과 같다.
- username : 서버로 보낼 식별 가능한 유저의 이름
- message : 서버로 보낼 메시지 내용
- type : 서버에서 기능을 구행하기 위한 유형
- 유형에는 3가지로 나누었다.
- init : 클라이언트 접속 정보 초기화, 맨 처음 소켓 서버에 연결할 때 접속 정보를 넘겨주기 위해 사용한다.
- all : 모든 클라이언트에게 메시지 전송
- whisper : 특정 클라이언트에게 메시지 전송, '|' 기호를 구분자로 처리하여 뒤의 문자열을 특정 클라이언트의 정보를 기입하도록 하였다.
// 소켓 서버에 데이터 송신
addMessage(socket, username, message, type) {
Map<String, dynamic> data = {
'username': username,
'message': message,
'type': type,
// [type]
// - init : 클라이언트 접속 정보 초기화
// - all : 모든 클라이언트에게 메시지 전송
// - whisper|username : 특정 클라이언트에게 메시지 전송
};
print("[socket.dart] 메시지 전송 : $username : $message");
socket?.add(jsonEncode(data));
}
chat_main.dart (ChatMainPage)
상태 값을 공유할 변수 및 함수 선언
// 메시지 내용을 저장하는 변수
List messageList = [];
// 메시지 내용을 setState 함수를 통해 상태를 업데이트하는 함수
void setStateMessage(data) {
print("[chat_main.dart] (setStateMessage) 업데이트 할 값 : $data");
setState(() => messageList.add(data));
}
채팅 영역과 메시지 내용 입력 영역 구분 및 각 생성자 데이터 할당
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
// 메시지 내용 표시 영역
ChatArea(
messageList: messageList,
),
// 메시지 입력 영역
InputTextArea(
username: widget.username,
messageList: messageList,
updateMessage: setStateMessage,
)
],
),
),
);
}
input_text_area.dart (InputTextArea)
웹소켓 서버 연결 및 설정
웹소켓 서버를 연결하기 위해 위에서 정의한 FlutterWebSocket 클래스를 이용하여 소켓 서버에 연결한다.
- 위젯이 초기화 될 때(initState) 소켓 서버를 연결하기 위해 FlutterWebSocket 클래스의 getSocket() 메서드를 이용하여 소켓 서버를 연결한다.
- 소켓 인스턴스를 반환받은 소켓의 listen 함수를 이용하여 메시지를 수신 받도록 한다.
- 서버에서 메시지를 수신 받을 경우, 메시지의 값을 업데이트한다. (setState)
- updateMessage 함수는 상위 요소로 전달한 값을 setState 함수를 통해 상태 값을 업데이트 하는 역할을 한다.
// 웹소켓 할당을 위한 변수
final FlutterWebSocket flutterWebSocket = FlutterWebSocket();
WebSocket? socket;
@override
void initState() {
super.initState();
createSocket(); // 웹소켓 서버 연결하기
}
// 서버 연결
void createSocket() async {
try {
socket = await flutterWebSocket.getSocket();
// 클라이언트 초기 설정 (서버측 클라이언트 정보 알림용 메시지 전송)
flutterWebSocket.addMessage(socket, widget.username, "", "init");
socket?.listen((data) {
print("[input_text_area.dart] (createSocket) 서버로부터 받은 값 : $data");
setState(() {
widget.updateMessage(data);
});
});
} catch (e) {
print("[input_text_area.dart] (createSocket) 소켓 서버 접속 오류");
}
}
채팅앱 클라이언트 전체 소스 코드
main.dart
더보기
import 'package:flutter/material.dart';
import 'chat/chat_main.dart';
void main() async {
runApp(
const MaterialApp(
home: MainPage(), //ChatMainPage(),
),
);
}
class MainPage extends StatelessWidget {
const MainPage({super.key});
@override
Widget build(BuildContext context) {
// 텍스트필드 컨트롤러
TextEditingController textEditingController = TextEditingController();
return Scaffold(
body: Center(
child: Container(
width: 250,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.grey,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 유저 이름 입력
SizedBox(
width: 200,
child: TextField(
controller: textEditingController,
decoration: const InputDecoration(
label: Center(child: Text("사용자 이름 입력")),
),
textAlign: TextAlign.center,
),
),
ElevatedButton(
onPressed: () {
String txtValue = textEditingController.text;
txtValue = txtValue.trim();
if (txtValue != "") {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return ChatMainPage(
username: txtValue,
);
}));
}
},
child: const Text("확인"),
)
],
),
),
),
);
}
}
socket.dart
더보기
import 'dart:convert';
import 'dart:io';
class FlutterWebSocket {
List messageList = [];
String SERVER = "ws://192.168.10.103:4001";
// 웹 소켓 서버 연결
Future<WebSocket> getSocket() async {
WebSocket socket = await WebSocket.connect(SERVER);
return socket;
}
// 소켓 서버에 데이터 송신
addMessage(socket, username, message, type) {
Map<String, dynamic> data = {
'username': username,
'message': message,
'type': type,
// [type]
// - init : 클라이언트 접속 정보 초기화
// - all : 모든 클라이언트에게 메시지 전송
// - whisper|username : 특정 클라이언트에게 메시지 전송
};
print("[socket.dart] 메시지 전송 : $username : $message");
socket?.add(jsonEncode(data));
}
}
chat_main.dart
더보기
import 'package:flutter/material.dart';
import 'chat_area.dart';
import 'input_text_area.dart';
class ChatMainPage extends StatefulWidget {
final String username;
const ChatMainPage({super.key, required this.username});
@override
State<ChatMainPage> createState() => _ChatMainPageState();
}
class _ChatMainPageState extends State<ChatMainPage> {
// 메시지 내용을 저장하는 변수
List messageList = [];
// 메시지 내용을 setState 함수를 통해 상태를 업데이트하는 함수
void setStateMessage(data) {
print("[chat_main.dart] (setStateMessage) 업데이트 할 값 : $data");
setState(() => messageList.add(data));
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
// 메시지 내용 표시 영역
ChatArea(
messageList: messageList,
),
// 메시지 입력 영역
InputTextArea(
username: widget.username,
messageList: messageList,
updateMessage: setStateMessage,
)
],
),
),
);
}
}
chat_area.dart
더보기
import 'dart:convert';
import 'package:flutter/material.dart';
class ChatArea extends StatefulWidget {
final List messageList;
const ChatArea({super.key, required this.messageList});
@override
State<ChatArea> createState() => _ChatAreaState();
}
class _ChatAreaState extends State<ChatArea> {
ScrollController scrollController = ScrollController();
@override
Widget build(BuildContext context) {
// 리스트뷰에 메시지가 추가되면 스크롤을 가장 아래로 이동
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.animateTo(scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 500), curve: Curves.ease);
});
return Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: widget.messageList.length,
itemBuilder: (BuildContext context, int index) {
// 추가된 메시지 내용
print(
"[chat_area.dart] (build) 추가된 메시지 내용 : ${widget.messageList[index]}");
// JSON 문자열을 맵으로 변환
Map<String, dynamic> data = jsonDecode(widget.messageList[index]);
return Stack(
children: [
Text("${data['username']}"),
Card(
margin: const EdgeInsets.fromLTRB(0, 20, 0, 10),
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
child: Text(data['message']),
),
),
],
);
},
),
);
}
}
input_text_area.dart
더보기
import 'dart:io';
import 'package:flutter/material.dart';
import '../socket.dart';
class InputTextArea extends StatefulWidget {
final String username;
final List messageList;
final Function updateMessage;
const InputTextArea(
{super.key,
required this.username,
required this.messageList,
required this.updateMessage});
@override
State<InputTextArea> createState() => _InputTextAreaState();
}
class _InputTextAreaState extends State<InputTextArea> {
final TextEditingController _controller = TextEditingController();
// 웹소켓 할당을 위한 변수
final FlutterWebSocket flutterWebSocket = FlutterWebSocket();
WebSocket? socket;
@override
void initState() {
super.initState();
createSocket(); // 웹소켓 서버 연결하기
}
// 서버 연결
void createSocket() async {
try {
socket = await flutterWebSocket.getSocket();
// 클라이언트 초기 설정 (서버측 클라이언트 정보 알림용 메시지 전송)
flutterWebSocket.addMessage(socket, widget.username, "", "init");
socket?.listen((data) {
print("[input_text_area.dart] (createSocket) 서버로부터 받은 값 : $data");
setState(() {
widget.updateMessage(data);
});
});
} catch (e) {
print("[input_text_area.dart] (createSocket) 소켓 서버 접속 오류");
}
}
// 메시지 보내기
void sendMessage() {
if (_controller.text.trim().isNotEmpty) {
String message = _controller.text; // 메시지 내용
String messageType = ""; // 메시지 타입
// 귓속말 명령어 확인
// 예) /w 사용자 내용
// - /w : 귓속말 명령어
// - 사용자 : 귓속말 보낼 사용자
// - 내용 : 귓속말 내용
if (_controller.text.split(" ")[0] == "/w") {
messageType = "whisper|${_controller.text.split(" ")[1]}";
String excludeString =
"${_controller.text.split(" ")[0]} ${_controller.text.split(" ")[1]}";
message = "(귓속말)${_controller.text.replaceFirst(excludeString, "")}";
}
// 귓속말 명령어가 없으면 모두에게 메시지 보내기
else {
messageType = "all";
}
// 웹소켓 서버에 메시지 내용 전송
flutterWebSocket.addMessage(
socket, widget.username, message, messageType);
_controller.clear();
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
// 메시지 입력란
Expanded(
child: TextField(
controller: _controller,
decoration:
const InputDecoration(labelText: 'Enter your message'),
),
),
// 전송버튼
IconButton(
icon: const Icon(Icons.send),
onPressed: () => sendMessage(), // 메시지 보내기
),
],
),
);
}
}
반응형