[React Native] ChatApp Socket.io Room ์ ์ฉํ๊ธฐ (feat. TypeScript)
์๋ ํ์ธ์ Foma ์ ๋๋ค.
์ ๋ฒ ๊ธ์์ Node.js์์ Socket.io๋ฅผ ์ด์ฉํ์ฌ Room์ ๋ง๋ค์ด ๋ณด์๋๋ฐ์.
(ํน์ ์๋ณด์ ๋ถ๋ค์ ์ฌ๊ธฐ ์์ ํ์ธํด ์ฃผ์ธ์!)
์ค๋์ ๋ฐฑ์๋ ์๋ฒ์์ ์จ ๋ฐ์ดํฐ๋ฅผ ํ๋ก ํธ์ ์ ์ฉํด ๋ณด๋ ๊ณผ์ ์ ์ ๋ฆฌํด ๋ณด๋ ค๊ณ ํฉ๋๋ค. ๋ฐ๋ก ์์ํ ๊ฒ์~
Preview
Install
npm install @react-navigation/native
npm install @react-navigation/native-stack
npm install react-native-screens
npm install react-native-safe-area-context
npm install socket.io-client
ScreenEnums
์คํฌ๋ฆฐ ์ข ๋ฅ๋ฅผ Enum์ผ๋ก ๋ง๋ค์ด ๋์ต๋๋ค.
export enum ScreenEnums {
Login = 'LoginScreen',
Chat = 'ChatScreen',
ChatRoom = 'ChatRoomScreen',
}
Navigation/index.tsx
ํ๋ฉด ๋ค๋น๊ฒ์ด์ ์ ์ค์ ํด ์ค๋๋ค.
(์์ธํ ์ค๋ช ์ ์ฌ๊ธฐ ์์ ํ์ธํด ์ฃผ์ธ์!)
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {NavigationContainer} from '@react-navigation/native';
import React from 'react';
import LoginScreen from '../Screens/LoginScreen';
import ChatScreen from '../Screens/ChatScreen';
import {ChatRoomScreen} from '../Screens/ChatRoomScreen';
import {ScreenEnums as screens} from '../Models/ScreenEnums';
const Stack = createNativeStackNavigator();
export default function Navigation() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName={screens.Login}>
<Stack.Screen name={screens.Login} component={LoginScreen} />
<Stack.Screen name={screens.ChatRoom} component={ChatRoomScreen} />
<Stack.Screen name={screens.Chat} component={ChatScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
Login.tsx
์ ์ ์ ์ด๋ฆ์ ์ ๋ ฅํ๊ณ ์ ์ฅํ ํ๋ฉด์ ๋ง๋ค์ด ์ค๋๋ค.
์ ์ฅ ๋ฒํผ์ ๋๋ฅด๋ฉด ChatRoomScreen์ผ๋ก navigate ๋๋๋ฐ ๊ทธ ๋ ์ธํ ํ ์คํธ๋ฅผ user ๊ฐ์ผ๋ก ์ ๋ฌํด ์ค๋๋ค.
import {Pressable, Text, TextInput, View} from 'react-native';
import React, {useState} from 'react';
import {styles} from '../Styles/LoginStyles';
import {ScreenEnums as screens} from '../Models/ScreenEnums';
export default function LoginScreen({navigation}) {
const [inputText, setInputText] = useState('');
return (
<View>
<TextInput
style={styles.textInput}
onChangeText={text => setInputText(text)}></TextInput>
<Pressable
onPress={() => {
navigation.navigate(screens.ChatRoom, {
navigation: navigation,
user: inputText,
});
}}>
<Text>์
์ฅ</Text>
</Pressable>
</View>
);
}
styles
import {StyleSheet} from 'react-native';
export const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 30,
padding: 8,
},
textInput: {
borderRadius: 10,
borderWidth: 1,
borderColor: 'black',
},
});
ChatRoomCell.tsx
์ฑ๋ฃธ ๋ฆฌ์คํธ์ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด ์ค๋๋ค.
import React from 'react';
import {Text, View} from 'react-native';
import {styles} from '../Styles/ChaRoomCellStyles';
interface ChatRoomCellParams {
chatRoom: ChatRoom;
}
export default function ChatRoomCell({chatRoom}: ChatRoomCellParams) {
return (
<View style={styles.container}>
<Text style={styles.title}> {chatRoom.title} </Text>
<Text style={styles.message}> {chatRoom.lastMessage} </Text>
<Text style={styles.time}> 2022/08/09 </Text>
</View>
);
}
styles
import {StyleSheet} from 'react-native';
export const styles = StyleSheet.create({
container: {
margin: 20,
padding: 20,
borderRadius: 10,
borderWidth: 1,
borderColor: 'black',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: 'black',
},
message: {
fontSize: 16,
color: 'black',
},
time: {
alignSelf: 'flex-end',
fontSize: 14,
color: '#D3D3D3',
},
});
ChaRoom.ts
์ฑ๋ฃธ์ ๋ชจ๋ธ์ ๋ง๋ค์ด ์ค๋๋ค.
interface ChatRoom {
title: string;
lastMessage: string;
time: Date;
}
ChatRoomScreen.tsx
์ฑํ ๋ฃธ ํ๋ฉด์ ๋ง๋ค์ด ์ค๋๋ค.
์ฑ๋ฃธ ์ปดํฌ๋ํธ๋ฅผ ๋๋ฅด๋ฉด ์ฑ์คํฌ๋ฆฐ์ผ๋ก ์ด๋ํ๊ฒ ๋๊ณ , ๊ทธ ๋ ์ ์ ์ ์ ๋ณด์ ๋ฃธ ์ด๋ฆ์ ๊ฐ์ด ์ ๋ฌํ๊ฒ ๋ฉ๋๋ค.
import {NativeStackHeaderProps} from '@react-navigation/native-stack';
import React from 'react';
import {FlatList, Pressable, Text, View} from 'react-native';
import ChatRoomCell from '../Components/ChatRoomCell';
import {ScreenEnums as screens} from '../Models/ScreenEnums';
type ChatRoomParams = {
route: {
params: {
navigation: any;
user: string;
};
};
};
type Navigation = NativeStackHeaderProps & ChatRoomParams;
export function ChatRoomScreen({route}: Navigation) {
const mockData: ChatRoom[] = [
{title: 'A', lastMessage: 'test A', time: new Date()},
{title: 'B', lastMessage: 'test B', time: new Date()},
{title: 'C', lastMessage: 'test C', time: new Date()},
];
return (
<View>
<FlatList
contentContainerStyle={{paddingBottom: 50}}
data={mockData}
keyExtractor={item => item.lastMessage}
renderItem={({item}) => (
<Pressable
onPress={() => {
route.params.navigation.navigate(screens.Chat, {
user: route.params.user,
room: item.title,
});
}}>
<ChatRoomCell chatRoom={item}></ChatRoomCell>
</Pressable>
)}
/>
</View>
);
}
styles
import {StyleSheet} from 'react-native';
export const styles = StyleSheet.create({
container: {
margin: 20,
padding: 20,
borderRadius: 10,
borderWidth: 1,
borderColor: 'black',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: 'black',
},
message: {
fontSize: 16,
color: 'black',
},
time: {
alignSelf: 'flex-end',
fontSize: 14,
color: '#D3D3D3',
},
});
ChatScreen.tsx
Chat ํ๋ฉด์ ํ์ํ ๊ฐ๋ค์ ์ธํ ํด ์ค๋๋ค.
import {Text, View, FlatList, TextInput, Pressable} from 'react-native';
import React, {useState, useEffect, useRef} from 'react';
import {ChatScreenStyles as styles} from '../Styles/ChatScreenStyles';
import io from 'socket.io-client';
import {NativeStackHeaderProps} from '@react-navigation/native-stack';
type ChatScreenParams = {
route: {
params: {
navigation: any;
user: string;
room: string;
};
};
};
type Navigation = NativeStackHeaderProps & ChatScreenParams;
export default function ChatScreen({route}: Navigation) {
const [messageText, setMessageText] = useState('');
const [serverMessages, setServerMessages] = useState([]);
const serverMessagesList: Message[] = [];
const webSocket = useRef(null);
const [user, setUser] = useState('');
useEffect์ ์์ผ์์ ๋ฐ์์จ ์ด๋ฒคํธ๋ค์ ์ฒ๋ฆฌํ๋๋ก ํฉ๋๋ค.
welcome: ์ ์ ๊ฐ ์ ์ํ ์ ๋ณด,
message: ๋ฉ์ธ์ง ์ ๋ณด,
leave: ์ ์ ๊ฐ ๋ ๋๋ ์ ๋ณด
useEffect(() => {
setUser(route.params.user);
webSocket.current = io(`http://192.168.111.34:3001/chat`);
webSocket.current.on('connect', () => {
let message = {
type: 'Welcome',
user: route.params.user,
message: `${route.params.user} ๋์ด ์
์ฅํ์
จ์ต๋๋ค.`,
room: route.params.room,
};
webSocket.current.emit('welcome', message);
console.log('Connected Server');
});
webSocket.current.on('message', e => {
console.log(route.params.user, 'message', e);
serverMessagesList.push(e);
setServerMessages([...serverMessagesList]);
});
webSocket.current.on('welcome', e => {
console.log('welcome', e);
serverMessagesList.push(e);
setServerMessages([...serverMessagesList]);
});
webSocket.current.on('leave', e => {
serverMessagesList.push(e);
setServerMessages([...serverMessagesList]);
});
webSocket.current.on('error', e => {
console.log(e.message);
});
webSocket.current.on('disconnect', e => {
console.log('Disconnected. Check internet or server.');
});
return () => {
let message = {
type: 'Leave',
user: route.params.user,
message: `${route.params.user} ๋์ด ํด์ฅํ์
จ์ต๋๋ค.`,
room: route.params.room,
};
webSocket.current.emit('leave', message);
webSocket.current.disconnect();
};
}, []);
๋ฉ์ธ์ง๋ฅผ ์๋ฒ์ ๋ณด๋ด๋ ํจ์
const sendMessage = () => {
let message = {
type: 'Chat',
user: user,
message: messageText,
room: route.params.room,
};
webSocket.current.emit('message', message);
setMessageText('');
};
View
return (
<View>
<View
style={{
padding: 5,
flexGrow: 1,
}}>
<FlatList
style={styles.list}
contentContainerStyle={{paddingBottom: 50}}
data={serverMessages}
keyExtractor={(item, index) => item.message + index}
renderItem={({item}) =>
item.type == 'Welcome' || item.type == 'Leave' ? (
<Text style={styles.welcomeChat}>{item.message}</Text>
) : item.user == user ? (
<Text style={styles.myChat}>{item.message}</Text>
) : (
<Text style={styles.otherChat}>{item.message}</Text>
)
}
/>
</View>
<View style={styles.bottomContainer}>
<TextInput
style={styles.input}
placeholder={'Add Message'}
onChangeText={text => {
setMessageText(text);
}}
value={messageText}></TextInput>
<Pressable onPress={sendMessage} disabled={messageText == ''}>
<Text style={styles.send}>Send</Text>
</Pressable>
</View>
</View>
);
styles
import {StyleSheet} from 'react-native';
export const ChatScreenStyles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 30,
padding: 8,
},
welcomeChat: {
alignSelf: 'center',
padding: 10,
color: 'white',
backgroundColor: '#212124',
fontSize: 16,
borderRadius: 20,
marginVertical: 10,
},
myChat: {
alignSelf: 'flex-end',
padding: 10,
color: 'white',
backgroundColor: 'yellow',
fontSize: 16,
borderRadius: 20,
marginVertical: 10,
},
otherChat: {
alignSelf: 'flex-start',
padding: 10,
color: 'white',
backgroundColor: 'gray',
fontSize: 16,
borderRadius: 20,
marginVertical: 10,
},
input: {
width: '70%',
borderWidth: 1,
borderColor: 'black',
padding: 10,
marginRight: 30,
},
send: {
backgroundColor: 'black',
color: 'white',
padding: 20,
textAlign: 'center',
textAlignVertical: 'center',
},
bottomContainer: {
marginHorizontal: 10,
flexDirection: 'row',
marginBottom: 80,
},
list: {
height: '80%',
},
});