✔️ 기간 : 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) 사용할 수 있어서 좋았던 것 같다.