여러 명의 캐릭터를 움직이기 위해서는 socket.io를 사용해서 실시간 통신이 필요하다.
해당 글에서는 client 측의 코드만 다룰 것이다.
import { io } from 'socket.io-client';
const socket = io('/socket');
io
를 사용해서 socket 객체가 생성이 가능하다. 나는 proxy 설정을 따로 해줘서 네임스페이스(url)는 간략하게 적어두었다. 이렇게 하면 바로 socket 연결이 가능하다.
내가 입장했을때는 기존 map에 존재하던 character 들의 정보를 불러와야하고, 내가 입장해있을때 다른 캐릭터가 입장하면 입장한 캐릭터의 정보가 들어와야한다.
function Map() {
...
useEffect(() => {
canvasRef.current?.appendChild(app.view);
app.start();
socket.emit("userEnter", character, handleSelfEnter);
return () => {
app.stop();
};
}, [])
useEffect(() => {
socket.on('otherEnter', handleOtherEnter);
return () => {
socket.off('otherEnter');
};
}, [characters, character]);
}
첫 렌더링 시 클라이언트에서 character 정보를 담아 userEnter
이벤트를 emit해주면 서버에서는 해당 character의 정보를 otherEnter
이벤트에 담아 broadCast해주고 기존에 map에 있던 character 목록을 리턴해준다.
handleSelfEnter
에서 character 목록을 받아 state에 저장하고, handleOtherEnter
에서 새로운 character 정보를 받아 characterList에 추가하면 캐릭터 입장은 구현 완료!
(현재 character 위치에 따라 적절하게 위치를 계산해서 stage에 addChild를 해줘야한다)
앞선 글에서는 socket의 연결을 고려하지 않고 구현했기 때문에 로직 수정이 필요하다. 여러 명의 캐릭터가 움직이려면 내 캐릭터가 이동할 때마다 서버에 이벤트를 emit해서 다른 캐릭터들이 이동 이벤트를 받을 수 있어야 한다.
const handleKeyDown = (e: KeyboardEvent) => {
socket.emit('move', e.key, handleSelfMove);
};
key를 down했을 때 위치만 이동시키는게 아니라 emit
해서 해당 캐릭터의 이동을 알리는게 필요하다. emit하고 원래 구현했던 move 로직을 호출한다.
const handleSelfMove = (data: ICharacterResponse) => {
setCharacter({ ...character, direction: data.direction });
let cnt = 0, requestId;
const animation = () => {
setCharacters(
characters.map((char) => {
if (char.id === data.id) {
char.character.texture = Texture.from(data.imgURL);
} else {
char.character.x -= MOVE_LENGTH[data.direction].x / 10;
char.character.y -= MOVE_LENGTH[data.direction].y / 10;
}
return char;
}),
);
...
};
}
여기에 또 하나의 코드가 추가되어있는데, 여러 명의 user를 관리하기 위한 characters
state를 추가해주고 여기서는 현재 map에 존재하는 character들의 Sprite object를 저장한다. 내가 움직일때 멈춰있는 배경이 움직이게 되는 것처럼 멈춰있는 다른 사용자들도 움직여야하기 때문에 다음과 같이 character(Sprite object)
의 x, y 좌표를 내가 움직이고자하는 방향과 반대로 움직이도록 애니메이션을 부여해주었다.
다른 캐릭터가 이동할때를 위한 event를 on하고 있어야한다.
useEffect(() => {
socket.on('otherEnter', handleOtherEnter);
socket.on('otherMove', handleOtherMove);
return () => {
socket.off('otherEnter');
socket.off('otherMove');
};
}, [characters, character]);
여기서 dependancy를 추가해주지 않으면 가장 처음에 선언된 character, characters 상태만 기억하고 함수를 처리해서 작동이 원하는 대로 되지 않는다.
const handleOtherMove = (data: ICharacterResponse) => {
let cnt = 0, requestId;
const animation = () => {
setCharacters(
characters.map((char) => {
if (char.id === data.id) {
char.character.texture = Texture.from(data.imgURL);
char.character.x += MOVE_LENGTH[data.dir].x / 10;
char.character.y += MOVE_LENGTH[data.dir].y / 10;
}
return char;
}),
);
requestId = requestAnimationFrame(animation);
cnt++;
if (cnt === 10) cancelAnimationFrame(requestId);
};
requestAnimationFrame(animation);
};
다른 character가 움직이는 로직은 내가 움직이는 로직과 비슷하다. 다만 내가 움직이는게 아니므로 setCharacterPosition
, setBackgroundPosition
을 해줄 필요가 없고 characters 목록에서 해당 character의 좌표를 수정해주기만 하면 된다.
이렇게 하면,,,
귀여운 캐릭터들이 움직인다🥳
그런데 약간 거슬리는 문제가 있었다. 키보드 방향키를 꾹 누르고 있으면 이벤트가 연속해서 쭈우욱 간다. 비효율적인 호출을 막기 위해서 throttling
을 사용했다.
두 방식 모두 성능을 위해 event를 제어하는 방식
→ 입력이 시작되면 일정 주기로 실행
입력이 연속적으로 들어올 때 하나의 이벤트만 처리하는게 아니라 일정 주기로 이벤트를 호출하도록 제어하는게 더 적절할 것 같다고 생각이 들어서 throttle을 적용했다.
const [throttle, setThrottle] = useState(false);
const handleKeyDown = (e: KeyboardEvent) => {
if (throttle) return;
setThrottle(true);
socket.emit('move', DIRECTIONS[e.key]);
const timer = setTimeout(() => {
setThrottle(false);
clearTimeout(timer);
}, 100);
};
다음과 같이 연속적인 event 요청이 들어오면 0.1초에 하나의 호출만 가도록 수정했다.
이전에 작성한 방법으로 Nginx를 사용해서 클라이언트 배포를 했었는데, 자꾸 소켓 연결에서 오류가 발생했다.
찾아보니까 Nginx가 WebSocket을 지원하면서 리버스 프록시 서버가 직면하는 문제가 몇 가지 있다고 했다. WebSocket이 hop-by-hop protocol로 프록시 서버가 클라이언트의 Upgrade 요청을 가로챌때 적절한 헤더를 포함해 서버에 업그레이드 요청을 보내야한다고 한다.
...
location /socket {
proxy_pass http://SERVER_IP;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
client/server가 다른 protocol에서 connection될 때 사용되는 설정들
proxy_http_version
: HTTP 1.1 버전에서만 Upgrade
header를 사용할 수 있음proxy_set_header Upgrade
: $http_upgrade
로 client 요청 header에서 Upgrade: websocket
정보를 읽어옴proxy_set_header Connection
: 해당 packet이 upgrade될 패킷임을 알 수 있도록 명시이렇게 리버스 프록시 설정을 추가해주면 Nginx 배포 환경에서도 socket이 정상적으로 연결된다!!!
참고자료
https://socket.io/docs/v4/
https://d2.naver.com/helloworld/1336
https://dev-gorany.tistory.com/330
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade
https://shinwusub.tistory.com/111