일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- 빙고
- Get
- Firebase
- socket.io
- 채팅앱
- NoSQL
- 스코프
- Websocket
- 이벤트 루프
- MONGOOSE
- Redis
- 호이스팅
- 프로토타입패턴
- 공 피하기 게임
- 모듈
- react
- crud
- 클로저
- HTTP
- post
- MongoDB
- node.js
- Redux
- 데이터 엔지니어
- 크롬확장앱
- 채팅 앱
- 크롤링
- npm
- 수강관리앱
- JQuery
- Today
- Total
개발로그
React와 Firebase를 이용한 채팅 앱 만들기 - (2) 본문
5. DM 채팅
Side Panel Direct Message UI
<div>
<span style={{ display: 'flex', alignItems: 'center' }}>
<FaRegSmile style={{ marginRight: 3 }}/> DIRECT MESSAGES(1)
</span>
<ul style={{ listStyleType: 'none', padding: 0 }}>
{this.renderDirectMessages()}
</ul>
</div>
user 목록 가져오기
접속 중인 유저 파란색으로 표시
addUsersListeners = (currentUserId) => {
const {usersRef} = this.state;
let usersArray = [];
usersRef.on("child_added", DataSnapshot => {
if(currentUserId !== DataSnapshot.key) {
let user = DataSnapshot.val()
user["uid"] = DataSnapshot.key
user["status"] = "offline";
usersArray.push(user)
this.setState({users: usersArray})
}
})
}
해당 유저와 대화 할 방 아이디 생성
나의 아이디와 상대방의 아이디를 이용한다.
상대가 방을 만들든 내가 만들든 아이디가 동일해야 하므로 userId < currentUserId 로직 사용
getChatRoomId = (userId) => {
const currentUserId = this.props.user.uid
return userId > currentUserId
? `${userID}/${currentUserId}`
: `${currentUserId}/${userId}`
}
currentChatRoom을 클릭한 방으로 변경
changeChatRoom = (user) => {
const chatRoomId = this.getChatRoomId(user.uid);
const chatRoomData = {
id: chatRoomId,
name: user.name
}
this.props.dispatch(setCurrentChatRoom(chatRoomData))
}
클릭한 방 private 방 설정
this.props.dispatch(setPrivateChatRoom(true));
클릭한 방 Active 방으로 표시
this.setActiveChatRoom(user.uid);
private, public icons
{isPrivateChatRoom ?
<FaLock style={{marginBottom: '10px'}} />
:
<FaLockOpen style={{marginBottom: '10px'}} />
}
파일 올릴 때 prviate, public 구분
const filePath = '${getPath()}/${file.name}';
6. 알림, 즐겨찾기 방
Notification
알림 기능 : 여러 채팅 방 중 내가 현재 있는 채팅방이 아닌 곳에서 다른 사람이 채팅하면 하나의 쪽지당 하나의 count가 올라가는 알림
https://react-bootstrap.netlify.app/docs/components/badge/
채팅룸 정보를 가져올 때 알림 정보 리스너도 추가
// chatRooms.js AddChatRoomsListeners 메소드
this.addNotificationListener(DataSnapshot.key);
ChatRoom Collection에서 해당 ChatRoom Id에 해당하는 정보를 가져오기
목표는 방 하나 하나에 맞는 알림 정보를 notifications state에 넣어주는 것
이미 notifications state 안에 알림 정보가 들어있는 채팅방과 그렇지 않은 채팅방을 나누어준다.
if(index === -1) {
notifications.push({
id: chatRoomId,
total: DataSnapshot.numChildren(),
lastKnownTotal: DataSnapshot.numChildren(),
count: 0
})
}
id : 채팅방 아이디
total : 해당 채팅방 전체 메시지 개수
lastKnownTotal : 이전에 확인한 전체 메시지 개수
count : 알림으로 사용될 숫자
DataSnapshot.numChildren() : 전체 children 개수 = 전체 메시지 개수
else {
// 상대방이 메세지를 보낸 채팅방에 있지 않을 때
if(chatRoomId !== currentChatRoomId) {
lastTotal = notifications[index].lastKnownTotal
if(DataSnapshot.numChildren() - lastTotal > 0) {
notifications[index].count = DataSnapshot.numChildren() - lastTotal;
}
}
notifications[index].total = DataSnapshot.numChildren();
}
notification 클릭해서 없애주기
this.clearNotifications();
messageRef 정리해주기
componentWillUnmount() {
this.state.chatRoomsRef.off();
this.state.chatRooms.forEach(chatRoom => {
this.state.messagseRef.child(chatRoom.id).off();
});
}
즐겨찾기 방
Favorite 클릭 버튼 UI
Favorite 버튼 클릭 시 is Not Favorited면 user collection에서 추가, 아니면 제거
const handleFavorite = () => {
if(isFavorited) {
usersRef
.child(`${user.uid}/favorited`)
.child(chatRoom.id)
.remove(err => {
if(err !== null) {
console.error(err);
}
})
setIsFavorited(prev => !prev);
} else {
usersRef
.child(`${user.uid}/favorited`).update({
[chatRoom.id]: {
name: chatRoom.name,
description: chatRoom.description,
createdBy: {
name: chatRoom.createdBy.name,
image: chatRoom.createdBy.image
}
}
})
setIsFavorited(prev => !prev);
}
}
DB : 클릭한 유저 id -> favorited -> 채팅방 id -> createdBy(image, name), description, name
새로고침해도 Favorited가 남아있게 하기
once() : it triggers once and then does not trigger again
const addFavoriteListener = (chatRoomId, userId) => {
usersRef
.child(userId)
.child("favorited")
.once("value")
.then(data => {
if(data.val() !== null) {
const chatRoomIds = Object.keys(data.val());
const isAlreadyFavorited = chatRoomIds.includes(chatRoomId)
setIsFavorited(isAlreadyFavorited)
}
})
}
Favorite 리스트 리스너 추가하기
addListeners = (userId) => {
const {usersRef} = this.state;
usersRef
.child(userId)
.child("favorited")
.on("child_added", DataSnapshot => {
const favoritedChatRoom = { id: DataSnapshot.key, ...DataSnapshot.val() }
this.setState({
favoritedChatRooms: [...this.state.favoritedChatRooms, favoritedChatRoom]
})
})
usersRef
.child(userId)
.child("favorited")
.on("child_removed", DataSnapshot => {
const chatRoomToRemove = { id: DataSnapshot.key, ...DataSnapshot.val() }
const filteredChatRooms = this.state.favoritedChatRooms.filter(chatRoom => {
return chatRoom.id !== chatRoomToRemove.id;
})
this.setState({favoritedChatRooms: filteredChatRooms})
})
}
Favorited를 위한 리스너 제거하기
componentWillUnmount() {
if(this.props.user) {
this.removeListener(this.props.user.uid);
}
}
removeListener = (userId) => {
this.state.usersRef.child(`${userId}/favorited`).off();
}
7. 채팅방 정보
각 사람당 글 쓴 데이터 가져오기
객체 userPosts에는 key로 유저 이름이, value로 count, image가 들어간다.
userPostsCount = (messages) => {
let userPosts = messages.reduce((acc, message) => {
if(message.user.name in acc) {
acc[message.user.name].count += 1;
} else {
acc[message.user.name] = {
image: message.user.image,
count: 1
}
}
return acc;
}, {})
this.props.dispatch(setUserPosts(userPosts))
}
userPosts 데이터 count 큰 순서대로 보여주기
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
const renderUserPosts = (userPosts) =>
Object.entries(userPosts)
.sort((a,b) => b[1].count - a[1].count)
.map(([key, val], i) => (
<Media key={i}>
<img
style={{borderRadius: 25}}
width={48}
height={48}
className="mr-3"
src={val.image}
alt={val.name}
/>
<Media.Body>
<h6>{key}</h6>
<p>
{val.count} 개
</p>
</Media.Body>
</Media>
))
8. Typing
Typing 시작 시 Typing 정보 데이터베이스에 저장
const handleKeyDown = () => {
if(event.ctrlKey && event.keyCode === 13) {
handleSubmit();
}
if(content) {
typingRef.child(chatRoom.id).child(user.uid).set(user.displayName)
} else {
typingRef.child(chatRoom.id).child(user.uid).remove();
}
}
채팅 보내면 typing 정보 데이터베이스에서 지우기
typingRef.child(chatRoom.id).child(user.uid).remove();
리스너를 이용해 데이터베이스에서 Typing 정보를 가져오기
가져올 때 나의 Typing은 제외
addTypingListeners = (chatRoomId) => {
let typingUsers= [];
this.state.typingRef.child(chatRoomId).on("child_added",
DataSnapshot => {
if(DataSnapshot.key !== this.props.user.uid) {
typingUsers = typingUsers.concat({
id: DataSnapshot.key,
name: DataSnapshot.val()
});
this.setState({ typingUsers });
}
})
}
DataSnapshot.key : 타이핑하는 유저, this.props.user.uid : 현재 로그인한 유저
데이터베이스에서 Typing 정보 제거되면 State에서도 지우기
this.state.typingRef.child(chatRoomId).on("child_removed",
DataSnapshot => {
const index = typingUsers.findIndex(user => user.id === DataSnapshot.key);
if(index !== -1) {
typingUsers = typingUsers.filter(user => user.id !== DataSnapshot.key);
this.setState({ typingUsers });
}
}
)
Typing UI 추가하기
renderTypingUsers = (typingUsers) =>
typingUsers.length > 0 &&
typingUsers.map(user => (
<span>{user.name}님이 채칭을 입력하고 있습니다...</span>
))
Typing 리스너 제거하기
addToListenerLists = (id, ref, event) => {
// 이미 등록된 리스너인지 확인
const index = this.state.listenerLists.findIndex(listener => {
return (
listener.id === id &&
listener.ref === ref &&
listener.event === event
)
if(index === -1) {
const newListener = {id, ref, event}
this.setState({
listenerLists: this.state.listenerLists.concat(newListener)
})
}
})
}
componentWillUnmount() {
this.state.messagesRef.off();
this.removeListeners(this.state.listenerLists);
}
9. Auto Scroll & Skeleton
메시지 보낼 때 자동으로 스크롤 내리기
잡은 자리 계속 참조할 수 있게 -> Refs를 생성 React.createRef() -> 생성한 Refs를 ref attribute를 통해 넣어주기
-> 메시지를 보낼 때마다 참조하고 있는 곳으로 스크롤 내려주기
<div ref = {node => (this.messageEndRef = node)}/>
componentDidUpdate() {
if(this.messageEndRef) {
this.messageEndRef.scrollIntoView({ behavior : 'smooth' })
}
}
메시지 로딩 중 스켈레톤 처리
스켈레톤 10개 만들기 -> 컴포넌트 10개 넣어도 되지만 Array Constructor를 이용한다.
renderMessageSkeleton = (loading) =>
loading && (
<>
{[...Array(10)].map((v,i) => (
<Skeleton key={i} />
))}
</>
)
ctrl + 엔터키로 메시지 보내기
if(event.ctrlKey && event.keyCode === 13) {
handleSubmit();
}
10. 데이터베이스 규칙, 스토리지 규칙, 배포
Firebase Storage Rule
악성 유저들로부터 파일을 보호하기 위해 사용
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /message {
allow read : if request.auth != null;
allow write : request.auth != null &&
request.resource.contentType.matches('image/.*) &&
request.resource.size < 10*1024*1024;
}
match /user_image {
match / {userId} {
allow read : if request.auth != null;
allow write : request.auth != null &&
request.auth.uid == userId &&
request.resource.contentType.matches('image/.*') &&
request.resource.size < 10*1024*1024;
}
}
}
}
/message 경로 해당 파일들, 인증된 유저 read, write 가능, 이미지 파일만 업로드 가능, 파일 사이즈 10mb 이하 가능
Firebase Database Rule
{
"rules" : {
"chatRooms" : {
".read" : "auth != null",
"$chatRoomId" : {
".write" : "auth != null",
".validate" : "newData.hasChildren(['id', 'name', 'createdBy', 'description'])",
"id" : {
".validate" : "newData.val() === $chatRoomId"
},
"name" : {
".validate" : "newData.val().length > 0"
},
"description" : {
".validate" : "newData.val().length > 0"
}
}
},
"messages" : {
".read" : "auth != null",
".write" : "auth != null",
"content" : {
".validate" : "newData.val().length > 0"
},
"image" : {
".validate" : "newData.val().length > 0"
},
"user" : {
".validate" : "newData.hasChildren(['id', 'image', 'name'])"
}
},
"presence" : {
".read" : "auth != null",
".write" : "auth != null"
},
"typing" : {
".read" : "auth != null",
".write" : "auth != null"
},
"users" : {
".read" : "auth != null",
"$uid" : {
"write" : "auth != null && auth.uid === $uid",
".validate" : "newData.hasChildren(['name', 'image'])",
"name" : {
".validate" : "newData.val().length > 0"
},
"image" : {
".validate" : "newData.val().length > 0"
}
}
}
}
}
애플리케이션 배포
1. firebase tool 설치 : npm install firebase-tools -g
2. firebase login -> social Oauth 로그인
3. firebase 프로젝트 시작 : firebase init
4. Database, Storage 선택
5. 배포를 위한 빌드 : npm run build
6. firebase.json
"hosting" : {
"public" : "./build"
}
7. 빌드 : firebase deploy
8. 코드 수정하면 5번부터 다시
Storage
> message
> user_image
데이터베이스
> chatRooms
> 채팅방 아이디
> createdBy : image, name
> description
> id
> name
> messages
> 채팅방 아이디
> 메세지
> content / image
> timestamp
> user : id, image, name
> presence
> typing
> users
> 유저 아이디
> image
> name
'군 장병 AI·SW 역량강화' 카테고리의 다른 글
군 장병 AI·SW 역량강화 교육을 마치며 (2) | 2024.02.03 |
---|---|
React와 Firebase를 이용한 채팅 앱 만들기 - (1) (1) | 2024.01.15 |
크롬 확장 앱 만들기 (0) | 2023.12.01 |
Redis에 대해 알아보자! (1) | 2023.10.22 |
mongoose에 대해 알아보자! (0) | 2023.10.20 |