React Memo 에 대한 간단한 설명 및 실사례 at ChatService

같은 props로 같은 결과를 내는 컴포턴트라면, React.memo로 감싸자 for Performance
일단 급한 분들을 위해 쓰는 법!
const SomeComponent = (props) => { return <div>{props.someProp}</div> } const areEqual = (prevProps, nextProps) => { //-> true를 리턴을 해주면, 리렌더링을 하지 않습니다. //default는 false! prevProps === nextProps //이렇게 통으로 하면, //프랍스가 하나만 바뀌어도 전부 다시 리랜더링이 되겠군요! } const MemoizedSomeComponent = React.memo(SomeComponent, areEqual);
JavaScript

Q. useState, useEffect 만 알면 되는것 아닙니까?

간단한 앱이라면 상관 없습니다. 기본적으로 React에서 state가 바뀌면 모든 컴포넌트는 다 리랜더링이 됩니다. 아래를 보시면
import React, {useState, useEffect} from 'react'; const App = ({}) => { console.log('app rendering'); const [cnt, setCnt] = useState(0); useEffect(() => { console.log('only once') }, []); return <> <div onClick={e => setCnt(cnt => cnt + 1)}> Increase + 1 </div> <div> <span>여기에 복잡한 로직의 최종 결과값이 있다고 해보겠습니다.</span> </div> </> }
JavaScript
카운트를 눌러 1씩 증가 시키는 경우, console로그를 찍어보면,
"app rendering" 이 버튼을 클릭할 때마다 출력 됩니다. 이유는 cnt 스테이트를 수정했고, cnt가 속한 parent Component는 App이기에, App에 있는 모든 것이 별도의 처리를 하지 않는 한은 모두 리-렌더링이 됩니다. 다행히, useEffect(그리고 dependency는 []으로 된부분)은 한 번만 등록이 되서, 콘솔로그 "only once"는 한 번만 프린트 됩니다.
여기서, 아래 있는 부분을 다음처럼 한 번 컴포넌트로 치환해보겠습니다.
import React, {useState, useEffect} from 'react'; const MyComponent = ({}) => { console.log('my component rendering') return <div> <span>여기에 복잡한 로직의 최종 결과값이 있다고 해보겠습니다.</span> </div> } const App = ({}) => { console.log('app rendering'); const [cnt, setCnt] = useState(0); useEffect(() => { console.log('only once') }, []); return <> <div onClick={e => setCnt(cnt => cnt + 1)}> Increase + 1 </div> <MyComponent/> </> }
JavaScript
잘 보시면 my component라는 부분에도 가장 맨 윗줄에 console로그는 렌더링 그리고 리렌더링이 될때마다 프린트 될텐데, 아직 별다른 조치를 취하지 않았으므로, 클릭을 할 때, 계속 my component rendering도 뜨게 됩니다. 뭐, 간단하니까 리랜더링이 된다고, 느려질 것 없어 보입니다.
만약에, 우리가 원하는 MyComponent가 상당히 복잡한 컴포넌트라고 하겠습니다. 아래처럼 말이죠!
import React, {useState, useEffect} from 'react'; const MyComponent = ({propA, propB, propC}) => { console.log('my component rendering') return <div> {propA} {propB} {propC} <span>여기에 복잡한 로직의 최종 결과값이 있다고 해보겠습니다.</span> </div> } const App = ({}) => { console.log('app rendering'); const [cnt, setCnt] = useState(0); const [stateA, setStateA] = useState(""); const [stateB, setStateB] = useState(""); const [stateC, setStateC] = useState(""); useEffect(() => { console.log('only once') }, []); return <> <div onClick={e => setCnt(cnt => cnt + 1)}> Increase + 1 </div> <MyComponent propA={stateA} propB={stateB} propC={stateC}/> </> }
JavaScript
뭐 그렇게 복잡해진 것 같진 않지만..!
MyComponent에는 추가된 세개의 props가 있습니다 (propA, propB, propC). 여기에 각각 stateA~C를 입력해준 상태입니다. 저희는 MyComponent가, 이 세가지 스테이트가 업데이트가 되는 경우를 제외하고는 리렌더링이 되지 않도록 하고 싶습니다. 원래같으면 cnt가 바뀌면, stateA~C가 바뀌지 않아도, App 컴포넌트 안에 있다는 이유 만으로, 해당 부분은 리-렌더링이 되는 운명이지만, higher order component인 React Memo를 사용하면 한 번만 렌더링이 되고 그 이후로는 스킵할 수 있습니다. 즉, 퍼포먼스가 늘어나겠죠 (힘들게 리렌더링을 다시 안해도 되니까요)
방법은 아래와 같습니다.
const MyComponent = React.memo(({propA, propB, propC}) => { console.log('my component rendering') return <div> {propA} {propB} {propC} <span>여기에 복잡한 로직의 최종 결과값이 있다고 해보겠습니다.</span> </div> }, (prevProps, nextProps) => { prevProps === nextProps });
JavaScript
보시면 기존의 컴포넌트를 그대로 React.memo()로 감쌌습니다. 이해가 잘 되지 않는 신택스라면 아래 처럼도 할 수 있습니다.
const MyComponentNoMemo = ({propA, propB, propC}) => { console.log('my component rendering') return <div> {propA} {propB} {propC} <span>여기에 복잡한 로직의 최종 결과값이 있다고 해보겠습니다.</span> </div> } const areEqual = (prevProps, nextProps) => { prevProps === nextProps } const MyComponent = React.memo(MyComponentNoMemo,areEqual);
JavaScript
이렇게 할 수도 있습니다.
이렇게 하면, my component rendering은 최초 한 번만 나오고, 그 이후부터는 더이상 나오지 않습니다. 마지막으로 렌더링된 결과 값을 그래도 사용하기 때문이죠.

Q. 실제 언제 써야 할 일이 있을까요?

채팅 앱을 생각해 봅시다. 채팅이 한 100개 쌓인 상태죠. 이경우에, 누군가가 채팅을 보냈다고 하면, 총 101개가 될텐데, 이 경우에, 모든 채팅이 리렌더링이 된다면, 채팅을 하는 경험이 매우 좋지 않겠죠. 이 경우, 채팅레코드 하나는 다른 채팅이 온다고 해서 내용이 바뀌진 않을 거고, 그말은, 일단 한 번 렌더링이 된 채팅 레코드라면, 별다른 변화가 있지 않은 한, 구지 다시 렌더링할 필요가 없는 상황이 되는것이죠! 감 오셨나요?
예시를 보여 드리면
const Chat = ({chat}) => { console.log('rendering chat') return <div>{chat.message}</div> } const App = () => { const [chats, setchats] = useState([]); useEffect(() => { //listening.. //updating chats.. takes place here setchats! },[]) return <> { chats.map((chat) => <Chat chat={chat}/> ) } </> }
JavaScript
이렇게 간단한 컴포넌트가 있다고 하고, useEffect를 통해서 리스닝을 하고 있는 상황이라고 하겠습니다 (추가 채티이 날라오면 이곳으로 오는 거죠)
자 느낌 오시죠? setChats가 될 때마다, chats가 업데이트가 될 텐데, 여기서, Chat 컴포넌트 하나만 추가 렌더링이 되면 되는거지, 기존에 있던 Chat이 리-렌더링이 될 필요가 없는거죠!
따라서 아래처럼 적용을 하면,
const Chat = React.memo(({chat}) => { console.log('rendering chat') return <div>{chat.message}</div> },(prevProps, nextProps) => { return prevProps.chat === nextProps.chat })
JavaScript
로 수정하면 되겠죠! 요런겁니다