
✔️ 기간 : 2023. 12. 21 ~ 12. 24 (4일)
✔️ 작업 환경
✔️ 폴더 구조
📦back
┣ 📜.gitignore
┣ 📜index.js
┣ 📜package-lock.json
┗ 📜package.json
📦front
┣ 📂public
┃ ┗ 📜index.html
┣ 📂src
┃ ┣ 📂components
┃ ┃ ┣ 📜Chat.js
┃ ┃ ┣ 📜Chatting.js
┃ ┃ ┣ 📜Header.js
┃ ┃ ┗ 📜Notice.js
┃ ┣ 📂hooks
┃ ┃ ┗ 📜UseToggle.js
┃ ┣ 📂styles
┃ ┃ ┗ 📜chat.css
┃ ┣ 📜App.js
┃ ┣ 📜index.css
┗ ┗ 📜index.js


닉네임 중복 방지
엔터키 입력 시 버튼 동작

채팅 보낸 시간
개인 DM
메시지 길이에 따라 입력창 길이 자동 조절
상대 닉네임만 보여주기
말풍선 디자인
메시지창 빈값일 땐 버튼 색 어둡게 + disabled
상황 : 각 메세지마다 채팅 시간이 하나씩 나오지 않고, 메시지마다 채팅 시간이 누적
원인 : map을 돌리고 있는 컴포넌트의 하위에 다시 map을 돌렸기 때문
💡 이때 근본적인 원인이 있었다. ChatTime이라는 컴포넌트를 새로 만들어서 시간만 담아줬는데, 메시지 정보를 담고있던 state에 timestamp를 추가해주는 게 훨씬 간결하다.
해결 :
// sendMsg : 메시지 전송
const sendMsg = () => {
if (msgInput.trim() !== '') {
textareaRef.current.style.height = 'auto';
const timestamp = getMsgTime();
socket.emit('sendMsg', {
userId: userId,
msg: msgInput,
dm: dmTo,
timestamp: timestamp,
});
}
};
// msg time 전달하기
const getMsgTime = () => {
const currentTime = new Date();
const hours = currentTime.getHours();
const minutes = currentTime.getMinutes();
const msgTime = `${hours < 10 ? '0' : ''}${hours}:${
minutes < 10 ? '0' : ''
}${minutes}`;
return msgTime;
};
시간을 구하는 함수를 만들어서 메시지를 전송할 때마다 실행해 주었다. 메시지 전송할 때에만 getMsgTime을 실행하도록 useMemo를 사용하려 했으나, 적절한 state가 없어서 생략했다.
상황 : 유저가 최초 접속하면 userId에 res.userId가 담기는 로직인데 초기값인 null이 콘솔에 찍힘
원인 : 처리 순서 문제
해결 : userId를 먼저 보내고, 이후 채팅방에 입장 알리도록 변경
io.on('connection', (socket) => {
socket.on('entry', (res) => {
if (Object.values(userIdArr).includes(res.userId)) {
socket.emit('error', { msg: '중복된 닉네임입니다.' });
} else {
socket.emit('entrySuccess', { userId: res.userId });
io.emit('notice', { msg: `${res.userId}님이 입장했습니다.` });
userIdArr[socket.id] = res.userId;
updateUserList();
}
});
원인 : setMsgInput('')의 실행 시점
해결 : 메시지를 서버로 보낼 때가 아닌, 서버에서 받은 정보로 chatList를 만드는 시점에 setMsgInput('') 적용
// chat : 새로운 채팅 내용
const addChatList = useCallback(
(res) => {
console.log('userid', userId);
const type = res.userId === userId || !userId ? 'my' : 'other';
const newChatList = [
...chatList,
{
type: type,
content: res.msg,
userId: type === 'my' ? '' : res.userId,
timestamp: res.timestamp,
dm: res.dm,
},
];
textareaRef.current.style.height = 'auto';
setMsgInput('');
setChatList(newChatList);
},
[chatList]
);
상황 : 최초 메시지 전송 후 height이 41px로 고정.resizeHeight(자동 높이 조절)의 문제 같은데 정확한 원인은 모르겠다.
임시 해결 :
sendMsg)과 addChatList할 때 기본 길이로 조절하는 코드 추가 textareaRef.current.style.height = 'auto'; const textareaRef = useRef();
const resizeHeight = () => {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height
= `${textareaRef.current.scrollHeight}px`;
};
<textarea
ref={textareaRef}
className="input-msg input-basic"
value={msgInput}
placeholder="메시지를 입력하세요."
onChange={(e) => {
setMsgInput(e.target.value);
resizeHeight();
}}
rows={1}
...
/>
🐞 강제로 height을 줄이기 때문에 메시지 전송 후 height이 움찔하는 버그 존재.
setMsgInput('')이 완전한 빈값으로 동작하지 않는다고 추정. // sendMsg : 메시지 전송
const sendMsg = () => {
if (msgInput.trim() !== '') {
...
// Enter 누르면 button onClick과 동일
const handleMsgEnter = (e) => {
if (isComposing) return;
else {
if (e.key === 'Enter') {
if (msgInput.trim() !== '')
...
🐞 handleMsgEnter도 결국 sendMsg 실행하므로, if (msgInput.trim() !== '')를 적지 않아도 공백 문제는 사라진다. 하지만 이 조건을 추가했을 때 height가 원래 크기로 줄어드는 반응이 더욱 빠르다.
닉네임 목록 나열처럼 단순 텍스트 나열일 땐 빈 배열을 선언해 원하는 요소를 push해주고, 그 배열을 return. 그리고 JSX문에서 그냥 {변수}를 기입하는 식으로 처리할 수 있다.
const userListOptions = useMemo(() => {
const options = [];
for (const key in userList) {
options.push(
<li className="dm-name" key={key}
onClick={() => sendDmTo(key)}>
{userList[key] === userId ?
`${userList[key]} (나)` : userList[key]}
</li>
);
}
return options;
}, [userList, userId]);
...
<ul className="input__select-dm">
<span>채팅 참여자</span>
<li onClick={() => setDmTo('all')}>전체</li>
{userListOptions}
</ul>
IME composition?
영어 외 다른 언어 사용 시 다양한 브러우저에서 해당 언어들을 지원하기 위한 OS 차원의 언어 변환 과정. Web API에서 event target 중isComposing이라는 프러퍼티를 제공하여, event 발생 여부를 불리언 값으로 처리한다.
즉isComposing이 true일 경우 아직 변환 과정에 있으므로 이벤트를 처리하지 않고, false일 경우만 해당 이벤트를 걸어주면 된다.
ex.
const handleEvent = (e) => {
if (e.isComposing) return;
else {
// 원하는 이벤트 동작을 여기에 입력
}
}
키보드 이벤트에 isComposing이라는 프로퍼티가 없다. 대신 리액트에서 제공하는 컴포지션 이벤트가 별도로 존재한다 : onCompositionEnd, onCompositionStart.
그래서 composing state 변수를 하나 선언해서
const [isComposing, setIsComposing] = useState(false);
composing 시작 시점에 true를 반환하고, 끝난 시점에 false를 반환하는 함수를 각 프로퍼티에 설정해준다.
ex.
<textarea
ref={textareaRef}
className="input-msg input-basic"
value={msgInput.replace('\n', '')}
placeholder="메시지를 입력하세요."
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
onKeyDown={handleMsgEnter}
onChange={(e) => {
setMsgInput(e.target.value);
resizeHeight();
}}
rows={1}
/>
이 블로그에서 해당 내용을 배웠다.
뒤늦게 DM 기능에 문제가 생겼음을 발견했다. 여러 기능들이 생겼던지라 어디서부터 문제가 생긴 건지 디버깅 과정이 꽤 험난했다. 달라진 점은 select - option 대신 ul - li를 썼다는 건데..
서버 측 코드를 확인했으나, 여전히 잘 구현되었다. 전적으로 클라이언트 측의 문제였다.
원인 :socket.id가 아닌 userId를 io.to에 보냄
해결 :
userList의 key가 socket id라서 해당 값을 넘겨준다.
const sendDmTo = useCallback(
(selectedUserId) => {
setDmTo(selectedUserId);
},
[dmTo]
);
const userListOptions = useMemo(() => {
const options = [];
for (const key in userList) {
options.push(
<li className="dm-name" key={key}
onClick={() => sendDmTo(key)}>
{userList[key] === userId ?
`${userList[key]} (나)` : userList[key]}
</li>
);
}
return options;
}, [userList, userId]);
이것저것 일 벌이며 기능 추가하다가 갑자기 한참 전에 해둔 기능에 문제가 생길 때 가장 괴롭다.. 허무해서. 하지만 침착하게 하다보면 결국 찾게 된다.. 코딩 하면서 제일 크게 느끼는 건데, 필요한 건 시간뿐이다.
프로젝트가 하나 둘씩 늘면서 약간 자괴감이 들었다. 어느 하나 제대로 끝마친 게 없는 것 같아서. 하면 할수록 미완성이 늘어난다면 아예 안 하는 게 낫겠다는 생각이 순간 들었다.
여러 줄의 input이 필요하면 textarea를 써야 한다!
리액트 hook을 다양하게(useCallback, useMemo, useRef, custom hook) 사용할 수 있어서 좋았던 것 같다.