요구 사항
- 채팅방 사용자가 실시간으로 접속한 유저를 알 수 있게 구현
- 입장 또는 활동 시 유저목록 내 UI 초록색
- 퇴장 시 유저목록 내 UI 빨간색
- 새로 가입 시 리스트 추가
개요
개발환경
- 에디터 : Intellij Ultimate
- 개발 툴 : SpringBoot 2.7.7
- 자바 : JAVA 11
- 빌드 : Gradle
- 서버 : AWS EC2, AWS RDS, AWS S3
- CI/CD : Docker, Gitlab
- 데이터베이스 : MySql, Redis
- 필수 라이브러리 : Spring Data JPA, Lombok, Spring Security, thymeleaf, JWT, WebSocket, OAuth2.0, Email
- ETC : Git, IntelliJ, JS, KakaoMap API
채팅방 환경
SockJs 를 활용하여 STOMP 방식의 채팅방
https://dev-gorany.tistory.com/235 자료 참고
- 구글링을 통해 쉽게 구현할 수 있는 소스를 변경시킴
진행 사항
시연 사진
채팅방에 접속하게 되면
먼저 User(1) 이 채팅방에 접속하게 되면 유저목록에 해당 ID가 초록색으로 변한다.
User(string) 이 채팅방에 접속하게 되면 채팅방 입장 메시지와 함께 유저목록에 초록색 점등으로 최신화가 된다.
User(1) 이 채팅방에서 나가게 되면 채팅방 퇴장 메시지와 함께 유저 목록에 빨간색으로 변경된다.
새롭게 모임에 참여 신청을 한 후 승인처리가 완료되면 가입한 모임 멤버가 추가 되며, 접속함에 따라 UI 가 변경함을 확인할 수 있다.
플로우차트
- 채팅방 접속과 퇴장시 /pub 을 통해서 메세지를 보내 구독하고 있는 클라이언트 들에게 메세지를 보내거나
- 채팅방 안에서 메세지를 보낼때마다
- 받아온 message 데이터 안에 status 값을 넣어 작성자가 온라인 상태인지 오프라인 상태인지 판별하는 로직이다.
세부로직 및 코드
document.addEventListener("DOMContentLoaded", function () {
console.log("DOMContentLoaded..");
findMember();
loadFetcher();
var username = document.getElementById("myName").innerHTML;
sockJs = new SockJS("/stomp/chat", null, {transports: ["websocket", "xhr-streaming", "xhr-polling"]});
stomp = Stomp.over(sockJs);
stomp.connect({}, function () {
console.log("STOMP Connection")
stomp.subscribe("/sub/chat/room/" + roomId, function (chat) {
var content = JSON.parse(chat.body);
var writer = content.writer;
const d = new Date();
const date = d.toISOString().split('T')[0];
const time = d.toTimeString().split(' ')[0];
var dateTime = date + ' ' + time;
var str = '';
if (writer === username) {
str = "<div class='chatbox__messages__user-message'>";
str += "<div style='float: right;' class='chatbox__messages__user-message--ind-message'>";
str += "<p style='color: #6c757d; size: 3em' >" + dateTime + "</p>";
str += "<p className=\"name\">" + writer + "</p>";
str += "<br/>"
str += "<span className=\"message\">" + content.message + "</span>";
str += "</div>";
str += "</div>";
$("#messagearea").append(str);
// $('#messagearea').scrollTop($('#messagearea')[0].scrollHeight);
} else {
str = "<div class='chatbox__messages__user-message'>";
str += "<div style='float: left' class='chatbox__messages__user-message--ind-message'>";
str += "<p style='color: #6c757d; size: 3em' >" + dateTime + "</p>";
str += "<p className=\"name\">" + writer + "</p>";
str += "<br/>"
str += "<span className=\"message\">" + content.message + "</span>";
str += "</div></div>";
$("#messagearea").append(str);
// $('#messagearea').scrollTop($('#messagearea')[0].scrollHeight);
}
findMember(content);
});
stomp.send('/pub/chat/enter', {}, JSON.stringify({roomId: roomId, writer: username}))
});
console.log("DOMContentLoaded..END");
});
화면이 로드 될때, connection 및 메세지 수신시 callback 함수 정의한 JS 파일
위 플로우 차트를 참고해서 보길 바람.
findMember() : 해당 채팅방에 참여한 유저목록을 화면에 보여주는 사용자 정의 메소드
loadFetcher() : 채팅방 채팅내역 불러오는 사용자 정의 메소드
findMember(data) : 참여한 유저목록 중 참여한 인원만 초록색으로 변경되게 하는 사용자 정의 메소드
@Controller
@RequiredArgsConstructor
public class StompChatController {
private final SimpMessagingTemplate template; //특정 Broker로 메세지를 전달
private final Map<String, Long> map = new HashMap<>();
//Client가 SEND할 수 있는 경로
//stompConfig에서 설정한 applicationDestinationPrefixes와 @MessageMapping 경로가 병합됨
//"/pub/chat/enter"
@MessageMapping(value = "/chat/enter")
public void enter(ChatMessageDTO message){
List<String> liveUser = new ArrayList<>();
message.setMessage(message.getWriter() + "님이 채팅방에 참여하였습니다.");
map.put(message.getWriter(),message.getRoomId());
for(Map.Entry<String, Long> entry : map.entrySet()){
if(entry.getValue().equals(message.getRoomId()) ){
liveUser.add(entry.getKey());
}
}
message.setUserList(liveUser);
message.setState(0);
template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
@MessageMapping(value = "/chat/out")
public void out(ChatMessageDTO message){
message.setMessage(message.getWriter() + "님이 채팅방에 나가셨습니다.");
List<String> liveUser = new ArrayList<>();
map.remove(message.getWriter());
for(Map.Entry<String, Long> entry : map.entrySet()){
if(entry.getValue().equals(message.getRoomId()) ){
liveUser.add(entry.getKey());
}
}
message.setUserList(liveUser);
message.setState(1);
template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
@MessageMapping(value = "/chat/message")
public void message(ChatMessageDTO message){
List<String> liveUser = new ArrayList<>();
for(Map.Entry<String, Long> entry : map.entrySet()){
if(entry.getValue().equals(message.getRoomId()) ){
liveUser.add(entry.getKey());
}
}
message.setUserList(liveUser);
message.setState(0);
template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
}
STOMP 방식의 핵심 컨트롤러인데, /pub 으로 오는 요청들에 대해서는 컨트롤러로 매핑되어 처리된다.
private final SimpMessagingTemplate template; //특정 Broker로 메세지를 전달
private final Map<String, Long> map = new HashMap<>();
Map 을 통해서 사용자에 대한 정보, 채팅방 정보 ( 유저이름 : 채팅방ID ) 를 저장한다.
public class ChatMessageDTO {
private Long roomId;
// 채팅방 정보
private String writer;
// 메세지 보낸사람의 Name
private String message;
// 메세지 내용
private String createdAt;
// 작성일자
private List<String> userList;
// 유저목록 표시할 리스트
private Integer state;
// 보낸사람의 접속상태 : 0 접속, 1 퇴장
}
message 에는 위와 같은 정보가 담겨 있다. 주석 참고
map.put(message.getWriter(),message.getRoomId());
for(Map.Entry<String, Long> entry : map.entrySet()){
if(entry.getValue().equals(message.getRoomId()) ){
liveUser.add(entry.getKey());
}
}
message.setUserList(liveUser);
message.setState(0); // 입장하거나 채팅방 활동시
//또는 message.setState(1); 퇴장시
공통로직
1. 누군가 메세지를 보내면 작성자에 대한 정보를 key 값, 어떤 채팅방에서 왔는지 채팅방 ID 를 value 값으로 넣어서
채팅방에 대한 정보를 기억한다.
2. Map 안에 value 값이 현재 채팅방 ID 와 동일한 data 들에 대해서 list 에 추가한다.
3. 보낼 메세지(message)에 유저목록과 접속상태를 설정해 보낸다.
그렇다면 현재 참여한 정보에 대해서는 어떻게 찾을 수 있을까
현재 참여하고 있는 사용자에 대한 정보는 참여 엔티티에 정의를 해둠으로 간단하게 승인처리된 사용자에 대한 정보만 가져오면 된다.
@GetMapping("/members/{crewId}")
public Response findJoinMember(@PathVariable long crewId){
log.info("#1 participateController crewId: "+ crewId);
return Response.success(participationService.AllowedMember(crewId));
}
@Transactional
public List<PartJoinResponse> AllowedMember(long crewId) {
Crew crew = crewRepository.findById(crewId).orElseThrow(() -> new AppException(ErrorCode.CREW_NOT_FOUND, ErrorCode.CREW_NOT_FOUND.getMessage()));
List<PartJoinResponse> list = new ArrayList<>();
for (Participation p : crew.getParticipations()) {
if ((p.getStatus() == 2 || p.getStatus() == 3) && p.getDeletedAt() == null) {
PartJoinResponse partJoinResponse = PartJoinResponse.builder()
.crewTitle(crew.getTitle())
.crewId(crewId)
.status(p.getStatus())
.writerUserName(crew.getUser().getUsername())
.joinUserName(p.getUser().getUsername())
.joinUserId(p.getUser().getId())
.writerUserNickName(crew.getUser().getNickName())
.joinUserNickName(p.getUser().getNickName())
.now(crew.getParticipations().size())
.limit(crew.getCrewLimit())
.build();
list.add(partJoinResponse);
}
}
return list;
}
모임에 참여한 사용자에 대한 정보를 리스트로 반환한다.
마무리
간단 요약해서 채팅방에 이슈가 생기면 유저 목록을 업데이트 및 최신화를 해준다 생각하면 된다.
- UI로 화면 표시하는 방법이 또한 JS 의 document 를 활용해 HTML을 바꿔주는 방향 또는 css 속성 변경 으로 구현함
- 이 말고도 Redis 를 통해서 실시간 접속 수를 가져오는 방법도 있음
- 입장과 퇴장 그리고 채팅입력 시 모든 경우에 이런 API 호출이 일어나는데, 조금 더 원할하게 할 수 있는 방법은 없는가
- 너무 주먹구구식으로 시간에 밀려 다급하게 구현하다 보니 다소 로직이 어지럽고, 효율적이지 못한 부분이 많이 아쉬웠다. 소켓을 활용해서 참여한 사용자의 정보를 가져올 순 없는가? 에 대한 생각도 들었다.
- 다중서버 환경에서는 위 방법은 제한된다. 고차원적인 기술이 필요할것이라 생각이 듬
자세한 소스코드는 아래의 깃허브 참고 부탁드립니다! 궁금하신 내용은 언제나 댓글로 남겨주세요.
https://github.com/rnrudejr9/poco_a_poco
'2023년 > 멋쟁이사자처럼 팀프로젝트' 카테고리의 다른 글
[팀 프로젝트] SockJS를 활용한 STOMP 방식 채팅방에서 채팅내역 저장과 읽음처리 기능 구현하기 (0) | 2023.03.01 |
---|---|
[팀 프로젝트] 1명의 USER 가 n개의 모임에 참여했는지 여부를 파악하는 방법 구현하기 (0) | 2023.02.28 |
[팀프로젝트] datepicker / Timepicker 적용하기 (2) | 2023.02.07 |
[팀프로젝트] STOMP 활용한 프로젝트 채팅방 구현 (0) | 2023.02.02 |
[팀프로젝트] Spring Boot에서 jwt + 쿠키를 활용한 로그인 구현과 비동기 방식 처리 (0) | 2023.02.01 |