Dog foot print

useMemo 및 memo 조금 더 자세히 사용해보기 본문

REACT

useMemo 및 memo 조금 더 자세히 사용해보기

개 발자국 2023. 4. 11. 08:54

Memoization이란 ?

useCallback, useMemo, React.memo 와 같은 기능에서 사용되는 memo 라는 개념은 파라메터를 기준으로 이전에 리턴한 해당 함수의 결과값을 리턴하는 개념이다. 순수함수는 파라메터가 동일할 때, 리턴값은 언제나 같다”라는 특징이 있다. 이런 특징을 이용해 연산이 오래걸리는 함수가 이전에 동일한 파라메터로 연산한 결과가 있다면 이전 리턴값을 줌으로써 연산에 필요한 시간을 단축할 수 있게 된다.

아래는 버튼을 클릭 할 때 마다 input값을 숫자로 변경해 O(2^n) 의 시간 복잡도를 가지는 fivo 함수의 결과가 출력되는 코드이다.

function fivo(n : number) : number{
    console.log("FivoStart")
    function __fivo__(n:number) : number{
        if(n < 2) return n
        return __fivo__(n - 1) + __fivo__(n - 2)
    }

    return __fivo__(n)

}

export default function App() {

    const [v, setV] = useState("");
    const [n,setN] = useState(0);
    const nthFivo = fivo(n);


    return (
        <div>
            <div>
                {n}th fivo is {nthFivo}
            </div>
            <div>
                <input type="text" onChange={(e) => setV(e.target.value)}/>
                <button onClick={() => !isNaN(Number(v)) && setN(Number(v))}>
                Calc
            </button>
            </div>
        </div>
    )
}

결과는 다음과 같다. 컴포넌트가 업데이트 될 때마다 fivo함수가 호출된다. 이는 useEffect를 사용함으로써 v변수에 따른 함수 호출은 막을 수 있지만 fivo함수가 다시 연산 되는 것은 막을 수 없다.

useMemo

위의 경우와 같이 같은 파라메터의 값이 전달될 때는 여러번 계산하는 것이 매우 비효율 적이다. useMemo는 이런 비효율성을 제거하기 위해, 의존성에 의해서만 재 계산되도록 함수의 결과를 캐싱한다.

다음은 useMemo의 타입이다.

  /**
     * `useMemo` will only recompute the memoized value when one of the `deps` has changed.
     *
     * @version 16.8.0
     * @see https://reactjs.org/docs/hooks-reference.html#usememo
     */
    // allow undefined, but don't make it optional as that is very likely a mistake
    type DependencyList = ReadonlyArray<unknown>;
    function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;

useMemo는 첫번째 인자로, 결과값이 캐싱될 함수를 전달 받는다. 2번째 인자는 함수가 다시 재 계산될 조건을 명시한다. 파라메터로 전달되는 변수들을 전달하면 된다.

아래 코드는 useMemo를 이용해서, fivo의 값을 캐싱하는 예제이다.

function fivo(n : number) : number{
    console.log("FivoStart")
    function __fivo__(n:number) : number{
        if(n < 2) return n
        return __fivo__(n - 1) + __fivo__(n - 2)
    }

    return __fivo__(n)

}

export default function App() {

    const [v, setV] = useState("");
    const [n,setN] = useState(0);
    const nthFivo = useMemo(() => fivo(n),[n]);

    return (
        <div>
            <div>
                {n}th fivo is {nthFivo}
            </div>
            <div>
                <input type="text" onChange={(e) => setV(e.target.value)}/>
                <button onClick={() => !isNaN(Number(v)) && setN(Number(v))}>
                Calc
            </button>
            </div>
        </div>
    )
}

결과는 다음과 같다.

useMemo는 모든 파라메터를 기억하는 것이아니라, 이전에 존재하던 값과 파라메터만 기억한다. 그렇기에, 위의 결과에서도 ‘v’가 변경되어도 fivo함수가 실행되지 않고, ’n’이 이전과 다른 값으로 전달 될 때, 호출 된다.

Memo

Memo도 위에서 본 useMemo와 비슷한 개념을 가진다. 그러나, ‘useMemo’는 이름에서 알 수 있다시피 함수형 컴포넌트에서 호출해야 하는 ‘hook’이며, ‘memo’ 함수는 함수형 컴포넌트 외부에서 호출 할 수 있는 함수이며, 함수형 컴포넌트를 주로 memo하는 기능으로 사용한다.

 

memo의 타입은 다음과 같다.

function memo<T extends ComponentType<any>>(
        Component: T,
        propsAreEqual?: (prevProps: Readonly<ComponentProps<T>>, nextProps: Readonly<ComponentProps<T>>) => boolean
    ): MemoExoticComponent<T>;

첫번째 인자로 컴포넌트를 전달 받고, 두번째 인자로 이전 프롭스와 현재 프롭스를 비교하여, 재 렌더링 여부를 결정하는 함수를 전달한다.

아래는 외부로 부터 1초마다 대화 메세지를 전달 받고, 하위 컴포넌트로 전달하는 예시 코드와 결과이다.

async function getMessages(){
    return Promise.resolve(["안녕", "너도 안녕!", "반가워"]);
}

export default function App() {

    const [messages, setMessages] = useState<string[]>([]);
    useEffect(() => {

        setInterval(async () => {
            const messages = await getMessages();
            setMessages(messages)
        },1000)

    },[])

    return (
        <div>
            {messages.map((v) => <ConversationBox text={v}/>)}
        </div>
    )
}

function ConversationBox({text} : {text : string}){
    console.log("ConversationBox!")
    return <div>{text}</div>
}

‘setMessages’로 인하여, 상태가 변경되고 프롭스가 새롭게 주입되니, ConversationBox가 계속 업데이트된다. 위의 경우에서 props의 text는 같기 때문에, 계속해서 렌더링이 발생하는 것은 필요가 전혀 없고, 렌더링으로 인한 단점만이 존재한다.

위의 코드를 memo기능과 함께 사용한다면 불 필요한 컴포넌트 업데이트를 방지 할 수 있다.

async function getMessages(){
    return Promise.resolve(["안녕", "너도 안녕!", "반가워"]);
}

export default function App() {

    const [messages, setMessages] = useState<string[]>([]);
    useEffect(() => {

        setInterval(async () => {
            const messages = await getMessages();
            setMessages(messages)
        },1000)

    },[])

    return (
        <div>
            {messages.map((v) => <MemoizedConversationBox text={v}/>)}
        </div>
    )
}

const MemoizedConversationBox = React.memo(ConversationBox,(p,n) => p.text === n.text)

function ConversationBox({text} : {text : string}){
    console.log("ConversationBox!")
    return <div>{text}</div>
}

아래는 위의 코드의 결과이다. memo의 두번째 함수로 “props의 text속성이 동일할 때 이전 프롭스와 현재 프롭스는 같다”라는 의미의 콜백을 전달하였기 때문에 “setMessages”가 실행되어도 ConversationBox는 다시 업데이트 되지 않는다.

memo를 올바르게 사용하기 위해서는 같은 프롭스로 인해 빈번히 렌더링 되는 컴포넌트에 사용하는 것이 옳바르다.

Memo의 잘못된 예시

메모이징된 컴포넌트를 사용한다는 의미는 이전과 동일한 컴포넌트를 사용한다는 의미와 같다. 조금 더 풀어보면, 현재 화면에 존재하는 요소들이 이전과 같은 프롭스를 보고 있다는 의미이다.

아래는 fivo함수를 사용한 예시에서, “button”만을 따로 분리하여, 메모이징한 코드이다. 이 코드로 기대했던 바는 인풋 상태값의 변화로 인한 버튼의 렌더링을 막는 것이다.

...
export default function App() {

    const [v, setV] = useState("");
    const [n,setN] = useState(0);
    const nthFivo = useMemo(() => fivo(n),[n]);
    const buttonCallback = useCallback((v : string) => !isNaN(Number(v)) && setN(Number(v)),[]);
    return (
        <div>
            <div>
                {n}th fivo is {nthFivo}
            </div>
            <div>
                <input type="text" onChange={(e) => setV(e.target.value)}/>
                <MemoizedButton inputV={v} clickCallback={buttonCallback}/>
            </div>
        </div>
    )
}

interface ButtonProps{
    inputV : string,
    clickCallback : (v : string) => void
}
// inputV는 화면에 보여질 필요가 없으니, 굳이 이 컴포넌트는 다시 렌더링될 필요가 없겠다 ! 
const MemoizedButton = React.memo(Button,() => true)
function Button({clickCallback,inputV} : ButtonProps){
    return (<button onClick={() => clickCallback(inputV)}>
                Calc
            </button>)
}

위의 코드는 아래의 결과 이미지와 같이 아무리 값을 변경하고 버튼을 클릭해도 0말고는 변경되지 않는다.

변경되지 않는 이유는 React.memo 의 결과로 함수를 다시 호출한것이 아니기 때문에, 현재 엘리먼트들이 가지고 있는 clickCallbackinputV는 이전 프롭스의 클로저를 형성하고 있기 때문이다. 이처럼 memo를 남발하거나, 오용하는 경우 오히려 가독성을 해치고 버그를 양산하는 등 코드의 질을 떨어뜨리게 된다.

useMemo를 사용해 Memo처럼 사용 할 수 있을까 ?

결론 부터 말하면 사용 할 수 있다. 함수형 컴포넌트가 렌더링 하는 엘리먼트들도 결국에는 값이기 떄문에 다음과 같이 코드를 사용 할 수 잇다.

export default function App() {

    const [v, setV] = useState("");
    const [n,setN] = useState(0);
    const nthFivo = useMemo(() => fivo(n),[n]);
    const buttonCallback = useCallback((v : string) => !isNaN(Number(v)) && setN(Number(v)),[]);
    const MemoizedButton = useMemo(() => () => <Button inputV={v} clickCallback={buttonCallback} />,[v]);
    return (
        <div>
            <div>
                {n}th fivo is {nthFivo}
            </div>
            <div>
                <input type="text" onChange={(e) => setV(e.target.value)}/>
                <MemoizedButton></MemoizedButton>
            </div>
        </div>
    )
}

interface ButtonProps{
    inputV : string,
    clickCallback : (v : string) => void
}
// inputV는 화면에 보여질 필요가 없으니, 굳이 이 컴포넌트는 다시 렌더링될 필요가 없겠다 ! 
function Button({clickCallback,inputV} : ButtonProps){
    return (<button onClick={() => clickCallback(inputV)}>
                Calc
            </button>)
}

memo함수와 다르게 useMemo는 hook이기 때문에, 컴포넌트 내부에서 사용해야만 한다. 그렇기에, 함수형컴포넌트와 결합하고, 의존성 리스트의 변경사항을 확인해야하기 때문에 위와 같이 컴포넌트를 기억할 때는 Memo 함수를 사용하는 편이 올바르다.

반응형
Comments