CEOS_Front

react-messenger-refactoring

oyatplum 2023. 10. 15. 15:59

 

드디어 메신저 리팩토링..

메신저 과제할 때 타입스크립트를 처음 써 봤는데 아주아주아주 힘들었던 기억이 있다..

 

 

우선 불필요한 리렌더링 문제부터 해결해봤다.


 

1. 렌더링 에러 해결

 

랜더링이 어떻게 되고 있는지 확인하려고 개발자 모드 켜자마자 뜬 에러..

넌 뭐냐 대체... ChattingRoomPage 컴포넌트에서 setState 할 때 오류가 나는 것 같다.

const [selected, setSelected] = useRecoilState(selectedUser);
setSelected(0);

여기!

recoil로 토글 시키는 유저를 저장했었는데 대화방에서 유저를 토글시킨 다음

채팅 목록에서 다른 대화방으로 옮기면 recoil이 업데이트되지 않아서 엉켜버렸다.

아마 그래서 저렇게 setSelected(0)로 대화방이 옮겨져도 기본 유저를 고정시켜놨었다.

 

 

그런데, 저 에러는

컴포넌트가 렌더링 되는 동안 함수가 호출되어서(setSelecte(0)) 렌더링 진행 중 에러가 발생한 것이라고 한다.

 

const [selected, setSelected] = useRecoilState(selectedUser);
useEffect(() => {
setSelected(0);
}, [selected]);

때문에 이렇게 useEffect로 처리해주면 에러 해결!!

 

 

 

 


 

 

2. 불필요한 type , 리렌더링 수정

 

기존 코드로 렌더링 상황을 보니까 이렇게 채팅을 칠 때마다 Talk 컴포넌트에서 기존의 대화들까지 싹 다 렌더링되고 있었다.

 

 

이러면 대화를 칠 때마다 불필요한 리렌더링이 쌓인다는 것이지...

 

 

 

ChattingPage

ㄴChattingContent

   ㄴTalk

 

interface.tsx

export interface ChattingRoom {
chattingRoomId: number;
messageId: number;
}
 

ChattingPage.tsx

<Header chattingRoomId={state} messageId={state} />
<ChattingContent chattingRoomId={state} messageId={state} />
<InputChat chattingRoomId={state} messageId={state} />

ChattingContent.tsx

const chattingList = useRecoilValue(chatList);
<Chatting ref={scrollRef}>
{chattingList[chattingRoomId].message.map((chat, index) => (
<Talk key={index} messageId={index} chattingRoomId={chattingRoomId} />
))}
</Chatting>

 

이게 기존 코드의 일부이다.

우선,Header, ChattingContent, InputChat 컴포넌트에 props 로 chattingRoomId와 messageId를 주고 있는데

 

자세히 코드를 살펴보니 Header, ChattingContent 컴포넌트에서는 messageId를 사용하지 않고 있었고

InputChat 컴포넌트에서는 chattingRoomId만 사용하고 이의 자식 컴포넌트인 Talk 컴포넌트에서 messageId를 쓰고 있었다.

 

 

음.. 그러니까 문제가 많았다.

우선, 내 기억으로 Talk 컴포넌트 코드를 짜면서 type 에러가 많이 났다. 컴포넌트들을 구조적으로 이해하고 그 에러를 근본적으로 해결했어야 했는데 단순히 Talk 컴포넌트에서 발생하는 type 에러들을 해결하려고만 하다 보니 그 상위 컴포넌트들까지 영향을 주면서 불필요한 type을 선언하고, 사용하게 된 것이다.

그리고 Talk 컴포넌트의 상위 컴포넌트인 ChattingContent컴포넌트에서 map 함수를 통해 배열의 대화마다 접근하여 Talk 컴포넌트를 호출하고 있으니까 대화가 추가될 때마다 recoil로 가져오는 chattingList가 업데이트 되면서 기존의 Talk 컴포넌트들이 모두 리렌더링 되는 것이다.

 

 

후... 그래서 ChattingContent 컴포넌트에서 사용했던 map 함수를 Talk 컴포넌트에서 사용해 배열에 접근하면서 대화들을 불러왔고 이렇게 해줌으로써 불필요했던 messageId 라는 type은 사라지게 되었다!

 

interface.tsx

export interface ChattingRoom {
chattingRoomId: number;
}

ChattingPage.tsx

<>
<Header chattingRoomId={state} />
<ChattingContent chattingRoomId={state} />
<InputChat chattingRoomId={state} />
</>

ChattingContent.tsx

<Chatting ref={scrollRef}>
<Talk chattingRoomId={chattingRoomId} />
</Chatting>

Talk.tsx

{chattingList[chattingRoomId].message.map((chat, index) => (
<ChatList key={index}>
{selected === chat.userNum ? ( // 원래 여기 chat 자리는 chattingList[chattingRoomId].message[messageId] 였음

 

 

그러면 이렇게 새로운 대화만 리렌더링되는 것을 확인할 수 있다!!! 휴우...

 

 

 

 

+) 참고로

ChattingPage.tsx를 보면 props 로 state를 넘겨 주고있는데

이는 ChattingRoom.tsx 컴포넌트에서(채팅 목록) 아래와 같이 하나의 채팅방을 선택했을 때 useNavigate 훅을 사용해 경로를 이동시키면서 두 번째 인자로 이동시킬 페이지에 함께 보낼 데이터를 저장해서 보낼 수 있도록 해줬다. (Link 태그 사용해도 가능하다.)

 

 

그러면 ChattingPage.tsx 컴포넌트에서 useLocation 훅을 사용해서 전달된 데이터를 받을 수 있게 된다.

const ChattingPage = () => {
console.log('ChattingPage');
const { state } = useLocation();

return (
<>
<Header chattingRoomId={state} />
<ChattingContent chattingRoomId={state} />
<InputChat chattingRoomId={state} />
</>
);
};
<User onClick={() => navigate('/chatting', { state: user.userId })}>

 

이렇게 부모 컴포넌트에서 자식 컴포넌트로 props를 전달하는 방법 뿐만 아니라 페이지 이동시 routing으로 props를 전달할 수도 있다는 점!!

useHistory, useLocation 훅을 잘 알아놓자~~

 

 


 

3. shift + Enter 줄 바꿈  &  두 번 출력 문제 해결 & e.preventDefault 위치

 

문제가 없어 보였는데 콘솔 창을 유심히 보니 채팅을 하나 보냈을 때, InputChat 컴포넌트가 두 번 찍히고 있었다.

우선 예상한 문제의 원인은 채팅 입력 창에 보이는 것처럼 입력 커서가 한 줄 아래로 내려가 있는 것이었다.

 

<InputText onSubmit={submitText}>
<textarea onChange={handleChange} onKeyUp={handleKeyup} value={text} />
<EnterButton>전송</EnterButton>
</InputText>
const handleKeyup = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!e.shiftKey && e.key === 'Enter') {
submitText();
}
};
const submitText = (e?: React.FormEvent<HTMLFormElement>) => {
e?.preventDefault();
생략
setText('');
}
};

코드는 이렇게 진행되고 있었다.

 

문제를 확인하기 위해 submitText 함수에 있는 setText를 지우고 보니

인풋이 두 번씩 보내지고 있었다..

 

문제는 두 가지였다.

1. Enter 입력 시 한 줄 아래로 내려가는 것

2. 인풋 두 번 출력

 

먼저, 인풋이 두 번 출력되는 두 번째 문제는 isComposingkeyDown으로 해결했다.

 

타이핑할 때, 한글은 영어와 달리 조합이 언제 끝나는 지 예측하지 못 하기 때문에 마지막 글자 아래 검정 밑줄이 뜨는 것을 확인할 수 있다.

따라서 타이핑을 모두 마쳐도 isComposing(조합중) 상태가 true가 되는 것이다.

물론 isComposing만을 처리해준다고 이 문제가 해결되지 않았다.

 

keyUp 함수를 keyDown으로 변경해주니 두 번 출력되는 문제가 해결됐다.

 

그 이유를 찾아보니....

keyUp 함수는 키보드를 눌렀다 뗀 직후에 이벤트가 발생하고, keyDown 함수는 키보드를 누를 때 이벤트가 발생한다.

따라서 keyUp 함수를 사용하면 키보드를 떼고 나서야 동작하기 때문에 isComposing에 대한 코드가 돌아가지 않게 된다.

(키보드를 떼면 조합 중인지 알 수 없는.. 뭐 그런 것 같다.)

 

반대로 isComposing을 처리해주지 않고 keyDown으로만 바꿔도 문제는 해결되지 않는다.

콘솔을 찍어보면 영어를 입력할 때는 isComposing이 true라고 한 번만 나오지만 한글을 입력하면 true, false 두 번이 순차적으로 찍히기 때문이다.

따라서 isComposing이 true인 경우를 처리해줬다.

<textarea
onChange={handleChange}
onKeyDown={handleKeyDown}
value={text}
/>
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.nativeEvent.isComposing) {
return;
} else if (!e.shiftKey && e.key === 'Enter') {
submitText();
}
};

이렇게~

 

 

그러면 보이는 것처럼 한글을 입력해도 한 번만 보내진다!!!

 

 

남은 문제는... Enter 입력 시 한 줄 아래로 내려가는 것.. 콘솔 창을 보니 InputChat이 두 번 찍히는 이유는 아무래도 여기였다.

(이 문제를 해결하려 했던 건데 덕분에 좋은 문제점도 함께 고칠 수 있었다.)

이 문제의 원인은 e.preventDefault(); 선언의 위치였다.

 

 

우선 preventDefault는 브라우저가 적용하는 기본 동작을 방지하는 역할을 한다.

지금 나와 같은 submit의 상황이면 form 데이터를 서버에 전송하고 페이지를 새로 고침하는 것이 기본 동작이다.

 

따라서 submit 함수 내에서 preventDefault를 하는 것이 아니라 preventDefault를 하고 submit 함수를 실행시켰다.

const submitText = () => {
생략
setText('');
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.nativeEvent.isComposing) {
return;
} else if (!e.shiftKey && e.key === 'Enter') {
e?.preventDefault();
submitText();
}
};

이렇게 바꿨더니

 

 

 

짜잔~ 줄 바뀜도 사라지고 InputChat도 한 번만 렌더링 됐다~~~~

 

 

 

 

우선.. 리렌더링 관련된 이슈 해결은 여기까지..!

 

'CEOS_Front' 카테고리의 다른 글

next-netflix-refactoring  (1) 2023.11.28
react-todo-refactoring  (1) 2023.10.05
Week_5,6  (0) 2023.05.22
Week_3,4  (1) 2023.05.08
Week_2  (4) 2023.03.27