💻문제점1
스트리밍 중인 채널에 접속하여 같은 채널에 있는 사람들끼리 채팅을 하도록 구현했다. 하지만 현재는 로그인 하지 않은 유저도 채팅이 가능하기 때문에, 로그인한 유저만 채팅을 보낼 수 있도록 하고 비회원 유저는 채팅을 보기만 하고 전송할 수는 없게 변경해야 했다. (방송 접속시 비회원/회원 무관하게 RSocket은 연결!)
📃문제점1-시도
서버에 채팅 전송 요청을 보낼 때 data에 token을 담아 서버에서 검증을 할까 했다. 그런데 채팅을 한 시간에 한 번만 보내는 것도 아니고, 1분에 10번 이상도 보낼 수 있는게 채팅이다. 매번 data에 token을 담아 서버에 전송해서 서버에서 확인을 하면 오버헤드가 너무 크지 않을까 생각이 들어 다른 방법을 생각했다.
🔍문제점1-해결
채팅을 보낼 때 클라이언트의 쿠키 저장소에 token이 있는지 판단하여 token이 있으면 채팅 전송 요청을 보내고, 그렇지 않다면 보낼 수 없도록 했다.
const accessToken = Cookies.get("Access_Token");
const send = () => {
if (!accessToken) return;
console.log("send nickname: ", nickname);
const sendData = {
nickname,
message,
chattingAddress,
};
console.log("sending data", sendData);
const accessToken = Cookies.get("Access_Token");
socket
.requestResponse({
data: sendData,
metadata: String.fromCharCode("message".length) + "message",
})
.subscribe({
onComplete: (com) => {
console.log("com : ", com);
},
onError: (error) => {
console.log(error);
},
onNext: (payload) => {
console.log(payload.data);
},
onSubscribe: (cancel) => {
console.log("cancel", cancel);
},
});
};
물론 여기서는 문제가 하나 있는데, 만약 token의 유효시간이 1시간이라면 유저가 1시간 동안 방송을 시청하고 그 이후에는 token이 만료가 되었는데도 쿠키에 존재는 하기 때문에 채팅을 보낼 수 있다는 점이다.
때문에 토큰을 검증하는 단계도 필요하고, 유효시간이 얼마남지 않았다면 서버측에서 클라이언트에게 token을 다시 발급받으라고 알려주거나, RefreshToken을 함께 보내라고 알려주어야 한다.
현재 팀이 백엔드로만 구성되어 있어, 백엔드 개발하고, 프론트도 React로 개발하고 있다. 막히는 부분이나 어려운 부분은 도와주시기로 한 프론트분들의 도움을 감사히 받고 있지만... 현재 단계에서는 우선순위가 중요하지 않아 구현하지 않았다. 그리고 프론트에서 토큰 검증 하는 일이 잘 없다고도 하시고... 이 부분은 나중에 고민할 필요가 있을 것 같다.
💻문제점2
회원이 채팅을 보낼 때 서버에 닉네임을 함께 보내주어야 했는데 어떻게 클라이언트가 닉네임을 알 수 있느냐가 문제였다.
📃문제점2-시도
서버에서 /members/info 경로로 사용자의 정보를 요청하는 GET 요청 API를 만들어서 닉네임이나 아이디가 필요할 때 해당 API로 요청을 보내게 할까 해서 서버에서 해당 API를 만들었었다.
@GetMapping("/info")
public Mono<ResponseEntity<MemberInfoResponseDto>> getUserInfo(Mono<Principal> userDetails) {
return userDetails
.map(principal -> getMemberFromPrincipal(principal))
.map(member -> member.getUserId())
.flatMap(userid -> {
return memberService.getUserInfo(userid);
});
}
private Member getMemberFromPrincipal(Principal principal) {
if (principal instanceof Authentication) {
Authentication authentication = (Authentication) principal;
Object principalObject = authentication.getPrincipal();
if (principalObject instanceof UserDetailsImpl) {
UserDetailsImpl userDetails = (UserDetailsImpl) principalObject;
return userDetails.getMember();
}
}
throw new RuntimeException("당신 누구야! Σ(っ °Д °;)っ");
}
이것도 채팅을 보낼 때 마다 요청을 보내는 것은 오버헤드가 크니, 페이지가 로딩될 때 토큰이 있으면 서버에 GET 요청을 보내서 닉네임을 useState로 저장해둘까 했었다.
🔍문제점2-해결
현재 서버에서 token을 만들 때 userId token의 subject에 담는다. 그러면 클라이언트도 token을 decode하여 userId를 얻을 수있는데, 그렇다면 여기에 추가적으로 nickname도 넣으면 클라이언트에서 decode해서 가져올 수 있지 않을까 생각했다.
서버에서 토큰을 생성할 때 subject에는 userId, claim에는 nickname이라는 key로 value에 멤버의 닉네임을 넣어 생성했다.
public String createToken(String userId, String nickname, String type) {
Date date = new Date();
Date exprTime = type.equals(JwtUtil.ACCESS_TOKEN) ?
Date.from(Instant.now().plus(1, ChronoUnit.HOURS)) :
Date.from(Instant.now().plus(14, ChronoUnit.DAYS));
return BEARER_PREFIX +
Jwts.builder()
.signWith(key, signatureAlgorithm)
.claim("nickname", nickname )
.setSubject(userId)
.setExpiration(exprTime)
.setIssuedAt(date)
.compact();
}
클라이언트에서는 jwt-decode 라이브러리를 사용해서 token을 decode 하여 nicknam을 가져와 저장했다.
import Cookies from "js-cookie";
import jwtDecode from "jwt-decode";
const ChatComponent = () => {
//생략
const accessToken = Cookies.get("Access_Token");
const [nickname, setNickname] = useState("");
useEffect(() => {
start();
if (accessToken) {
setNickname(jwtDecode(accessToken).nickname);
}
const send = () => {
if (!accessToken) return;
console.log("send nickname: ", nickname);
const sendData = {
nickname,
message,
chattingAddress,
};
console.log("sending data", sendData);
socket
.requestResponse({
data: sendData,
metadata: String.fromCharCode("message".length) + "message",
})
.subscribe({
onComplete: (com) => {
console.log("com : ", com);
},
onError: (error) => {
console.log(error);
},
onNext: (payload) => {
console.log(payload.data);
},
onSubscribe: (cancel) => {
console.log("cancel", cancel);
},
});
};
//생략
}
💻문제점3
클라이언트의 RSocketClient 객체가 새로고침 또는 뒤로 가기를 해도 삭제 되지 않고 남아있다. 때문에 다시 채널에 입장하면 새로운 RSocketClient가 생성되어 서버에 누적된다. 클라이언트 측에서 브라우저의 뒤로가기 버튼을 누르거나, 새로고침을 감지해서 해당 이벤트가 발생하면 RSocket를 close해주어야 했다. 새로고침할 때 socket 연결을 끊는 것은 금방 됐는데, 뒤로 가기 할 때 연결 끊는게 잘 안 됐다.
import Cookies from "js-cookie";
import jwtDecode from "jwt-decode";
const ChatComponent = () => {
//생략
const accessToken = Cookies.get("Access_Token");
const [nickname, setNickname] = useState("");
useEffect(() => {
start()
if (accessToken) {
setNickname(jwtDecode(accessToken).nickname);
console.log("set Nickname",nickname);
}
return () => {
//새로 고침 시 socket있다면 close
if(socket)
socket.close();
};
}, []);
//생략
}
📃문제점3-시도
시도1
뒤로가기를 감지하기 위해 react-router-dom v6를 사용하고 있기 때문에 history 라이브러리를 사용했다.
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();
//생략
import { history } from "./history"
const ChatComponent = () => {
//생략
useEffect(() => {
const listenBackEvent = () => {
console.log("뒤로 가기");
if(socket) {
socket.close();
}
};
const unlistenHistoryEvent = history.listen(({ action }) => {
if (action === "POP") {
listenBackEvent();
}
});
return unlistenHistoryEvent;
}, []);
}
콘솔에 로그는 찍히는데 socket.close()가 되지 않았다. socket이 null이라서 안되는건가?
시도2
다음 코드로도 뒤로 가기를 감지할 수 있다는데... 실패
const [locationKeys, setLocationKeys] = useState([]);
useEffect(() => {
// 뒷정리 함수 이용
return history.listen((location) => {
1if (history.action === "PUSH") {
2setLocationKeys([location.key]);
}
if (history.action === "POP") {
if (locationKeys[1] === location.key) {
setLocationKeys(([_, ...keys]) => keys);
// 앞으로 가기
} else {
setLocationKeys((keys) => [location.key, ...keys]);
// 뒤로 가기
history.push("/detail");
}
}
});
}, [locationKeys, history]);
🔍문제점3-해결
ㅎㅎ 결국 우리는 다른 프론트분들의 도움을 받기로 했다!
뒤로 가기를 누르면 popstate 이벤트가 발생하는데 해당 이벤트가 발생할 때 closeSocket 메서드를 호출해서 socket이 존재하면 close한다.
const closeSocket = () => {
console.log("closeSocket");
if(socket) {
socket.close()
}
}
useEffect(() => {
window.onpopstate = closeSocket;
return () => {
window.removeEventListener("popstate", closeSocket)
}
})
'TIL' 카테고리의 다른 글
[TIL - 20230605] RSocketRequester Redis 저장 실패 (0) | 2023.06.05 |
---|---|
[TIL - 20230603] Webflux, Mock 사용한 Channel 도메인, ChannelService 테스트 케이스 작성 (0) | 2023.06.03 |
[TIL - 20230531-0601] RSocket을 사용해 채팅방 별 실시간 채팅하기 (2) | 2023.06.02 |
[TIL - 20230531] webflux 채널 생성/조회 (0) | 2023.06.01 |
[TIL - 20230530] Webflux + RSocket 채팅 (0) | 2023.06.01 |