2차 공부/TIL

24.08.16 리액트 숙련주차 강의

공대탈출 2024. 8. 15. 22:39

useState VS useRef

useState와 useRef는 둘 다 어떤 값을 저장하기 위해 사용하는 훅이다.

다만, useState로 만든 state는 변경점이 있을 때마다 컴포넌트가 리렌더링되지만, useRef로 만들어진 값은 변경이 되더라도 리렌더링을 유발하지 않는다.

import { useState, useRef } from "react";

const App = () => {
    const [count, setCount] = useState(0);
    const countRef = useRef(0);

    const plusStateBtnHandler = () => {
        setCount((prev) => prev + 1);
    };
    const plusRefBtnHandler = () => {
        countRef.current++;
    };

    return (
        <div>
            <h1>useRef vs useState</h1>
            <div>
                state 영역 {count} <br />
                <button onClick={plusStateBtnHandler}>state변경</button>
                <br />
                state 영역 {countRef.current} <br />
                <button onClick={plusRefBtnHandler}>Ref변경</button>
            </div>
        </div>
    );
};

export default App;

useState로 만든 count state와 useRef로 만든 countRef 값이 있다.

state가 변경되면 바로 리렌더링이 일어나 해당 값이 변하지만, ref로 변경된 값은 리렌더링을 일으키지않고 그냥 countRef값을 늘리기만한다. 그리고 state를 변경하면 리렌더링이 일어나 그 값이 화면에 반영된다.

 

useRef는 DOM요소도 지정이 가능하다.

import React from "react";
import { useEffect } from "react";
import { useRef } from "react";

const App = () => {
    const idRef = useRef("");
    const pwRef = useRef("");
    const modalRef = useRef("");

    useEffect(() => {
        idRef.current.focus();
        setTimeout(() => {
            pwRef.current.focus();
        }, 2000);
        setTimeout(() => {
            modalRef.current.showModal();
        }, 4000);
    });

    return (
        <div>
            <div>
                아이디 : <input type="text" ref={idRef} />
            </div>
            <div>
                비밀번호 : <input type="password" ref={pwRef} />
            </div>
            <dialog ref={modalRef}>모달!</dialog>
        </div>
    );
};

export default App;

아이디 input, 비밀번호 input, 모달 dialog가 있다고 해보자.

컴포넌트가 마운트되면 먼저 아이디 input에 focus를 하고, 2초뒤에 비밀번호 input에 focus를하고, 4초뒤에 dialog를 open한다.

이렇게 useRef를 사용하면 DOM 요소의 어떤 작업도 가능하다.

 

useContext

우리가 기존에 사용했던 props drilling은 문제점이 있다.

몇가지 컴포넌트를 따라서 데이터가 이동하게되면 불필요한 중간 전달자도 생기고, 데이터의 흐름을 알아내기 어려워지거나 변경시 개발자에게 혼동을 줄 수 있다.

따라서 우리는 전역상태관리에 대해 알아볼 필요가 있다.

useContext는 같은 Context안에 있는 컴포넌트끼리 상태를 공유할 수 있게 만들어주는 훅이다.

 

import { createContext } from "react";

export const FamilyContext = createContext(null);

먼저 FamilyContext.js파일을 만들고, 컴포넌트끼리 공유할 Context를 createContext를 사용하여 만들어주고 export해준다.

import React from "react";
import Father from "./Father";
import { FamilyContext } from "../context/FamilyContext";

const GrandFather = () => {
    const houseName = "스파르타";
    const pocketMoney = 10000;
    return (
        <FamilyContext.Provider value={{ houseName, pocketMoney }}>
            <Father />
        </FamilyContext.Provider>
    );
};

export default GrandFather;

GrandFather컴포넌트에서 공유할 Context를 import하고 return태그 전체를 Context.Provider으로 감싸준다.

그리고 Context에 value값으로 공유할 데이터를 객체형식으로 전달해준다.

import React, { useContext } from "react";
import { FamilyContext } from "../context/FamilyContext";

const Child = () => {
    const stressedWord = {
        color: "red",
        fontWeight: "bold",
    };
    const { houseName, pocketMoney } = useContext(FamilyContext);
    return (
        <div style={{ marginLeft: "20px" }}>
            나는 이 집의 막내에요.
            <br />
            할아버지가 우리 집 이름은 <span style={stressedWord}>{houseName}</span>이에요
            <br />
            용돈도 <span style={stressedWord}>{pocketMoney}</span>만큼 받습니다.
        </div>
    );
};

export default Child;

사용할 컴포넌트에서 해당 Context를 import해오고, useContext훅에 넣어 해당 Context에 들어있는 데이터를 꺼내 사용하면 더이상 props drilling없이 state를 주고 받는 것이 가능해진다.

 

React.memo / useCallback

import { useState } from "react";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";
import Box3 from "./components/Box3";

const App = () => {
    const [count, setCount] = useState(0);
    console.log("app렌더링");
    return (
        <div style={{ marginLeft: "30px" }}>
            <h3>카운트</h3>
            <p>현재 카운트 : {count}</p>
            <button
                onClick={() => {
                    setCount(count + 1);
                }}
            >
                +
            </button>
            <button
                onClick={() => {
                    setCount(count - 1);
                }}
            >
                -
            </button>

            <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
                <Box1 />
                <Box2 />
                <Box3 />
            </div>
        </div>
    );
};

export default App;

App컴포넌트는 Box1, Box2, Box3을 자식 컴포넌트로 갖고있으며, count state를 가지고있고, 버튼을 누르면 count가 변경된다.

이때 버튼을 누르면 App컴포넌트와 자식 컴포넌트 모두가 리렌더링 되어 state와 상관없는 Box들도 리렌더링된다.

성능최적화를 하기위해 리렌더링이 불필요한 컴포넌트는 리렌더링을 막아줄 필요가 있다.

 

import React from "react";

const Box1 = () => {
    console.log("box1 렌더링");
    return (
        <div
            style={{
                width: "100px",
                height: "100px",
                backgroundColor: "#91c483",
                color: "white",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
            }}
        >
            Box1
        </div>
    );
};

export default React.memo(Box1);

Box1 컴포넌트를 React.memo로 감싸서 export해준다.

이렇게 한다면, 컴포넌트를 처음 렌더링할 때 React가 Memoizing, 기억하게 된다. 그리고 리렌더링이 일어날 때 props가 같다면, React는 Memoizing된 내용을 재사용한다.

 

 

const App = () => {
    const [count, setCount] = useState(0);
    console.log("app렌더링");
    const initState = () => {
        setCount(0);
    };
    return (
    		...
            <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
                <Box1 initState={initState} />
                <Box2 />
                <Box3 />
            </div>
        </div>
    );
};

만약 state를 초기화하는 initState함수를 Box1에 props로 내려주고, Box1에서 해당 버튼을 눌러 초기화할 수 있다면 어떻게 될까?

import React from "react";

const Box1 = ({ initState }) => {
    console.log("box1 렌더링");
    return (
        <div
            style={{
                width: "100px",
                height: "100px",
                backgroundColor: "#91c483",
                color: "white",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
            }}
        >
            <button onClick={initState}>count초기화</button>
        </div>
    );
};

export default React.memo(Box1);

 

count가 변할때마다 Box1도 리렌더링 되는 것을 볼 수 있다.

왜일까?

const App = () => {
    const [count, setCount] = useState(0);
    console.log("app렌더링");
    const initState = () => {
        setCount(0);
    };
	...
}

먼저, 함수는 원시 데이터가 아닌, 참조형 데이터이다. App컴포넌트가 리렌더링 될 때마다 initState라는 함수표현식도 다시 할당된다. 그러니까, 같은 기능을 하는 함수를 initState에 할당하는 것이지만, JS의 관점으로 보았을 때는 다른 함수를 할당한다고 생각하게 되는 것이다.(참조하는 주소값이 다르므로)

따라서 새로운 initState가 생기고, 새롭게 생긴 initState함수를 Box1에 props로 보내주니, Box1은 props가 변하여 리렌더링이 일어나게 된다.

하지만 초기화기능을 누를때는 count값이 변하고, Box1은 React.memo가 되어있으므로 리렌더링이 일어나지 않는다.

 

그럼 이 불필요한 리렌더링을 막으려면 어떻게 해야할까?

바로 useCallback훅을 사용하는 것이다.

const initState = useCallback(() => {
    setCount(0);
}, []);

useCallback훅은 자신을 포함한 컴포넌트가 처음 렌더링 될 때 콜백함수를 기억하게 된다. 그리고 의존성 배열 속 값이 변하게되면 다시 함수를 기억하게 된다.

 

const initState = useCallback(() => {
    console.log(`count값이 ${count}에서 0으로 초기화 됨`);
    setCount(0);
}, []);

만약 위와 같이 count값이 몇에서 0으로 초기화 되었는지 console.log를 찍는다면 어떻게 출력될까?

초기화 시점의 count에서 0으로 초기화 되었다고 출력이 될 것이라고 생각했지만 아니었다.

왜냐하면 useCallback은 처음 렌더링 시점의 count값을 기억하는 함수를 저장한 것이고, 의존성배열에 의해 업데이트 되지 않았으므로 count값이 0에서 0으로 초기화 됨이 출력된다.

만약 이렇게 업데이트 된 값을 보여주고싶다면 의존성 배열에 count를 넣으면 된다. 하지만 count값이 변할 때마다 콜백함수를 다시 지정하는 것도 염두에 두어야 한다.

 

useMemo

import React from "react";
import HeavyComponent from "./components/HeavyComponent";

const App = () => {
    return (
        <div>
            <h1>useMemo</h1>
            <nav style={{ backgroundColor: "yellow", marginBottom: "30px" }}>내비게이션 바</nav>
            <HeavyComponent />
            <footer style={{ backgroundColor: "green", marginTop: "30px" }}>푸터 영역이에요</footer>
        </div>
    );
};

export default App;

App.js에서 아주 무거운 계산을 하는 HeavyComponent를 자식 컴포넌트로 불러온다.

import React, { useState } from "react";

const HeavyComponent = () => {
    const [value, setValue] = useState(0);
    const heavyWork = () => {
        for (let i = 0; i < 10000000000; i++) {
            return 100;
        }
    };
    const sampleValue = heavyWork();
    return (
        <div>
            <p>나는 {sampleValue}를 가져오는 엄청나게 무거운 컴포넌트야</p>
            <button
                onClick={() => {
                    setValue(value + 1);
                }}
            >
                누르면 count가 올라가요
            </button>
            <br />
            {value}
        </div>
    );
};

export default HeavyComponent;

100억번의 반복문을 통해 결과를 return하는 heavyWork함수가 작동되어야 sampleValue가 할당되어 리렌더링이 일어날 때마다 해당 함수가 실행되어 비효율적인 리렌더링을 하는 것이다.

이럴땐 useMemo를 사용하여 변하지 않는 계산을 React에게 기억시킬 필요가 있다.

import React, { useState } from "react";

const HeavyComponent = () => {
    const [value, setValue] = useState(0);
    const heavyWork = () => {
        for (let i = 0; i < 10000000000; i++) {
            return 100;
        }
    };
    const sampleValue = useMemo(() => heavyWork(),[]);
    return (
        <div>
            <p>나는 {sampleValue}를 가져오는 엄청나게 무거운 컴포넌트야</p>
            <button
                onClick={() => {
                    setValue(value + 1);
                }}
            >
                누르면 count가 올라가요
            </button>
            <br />
            {value}
        </div>
    );
};

export default HeavyComponent;

이렇게 의존성 배열에 따라 무거운 작업이 실행되므로 필요할 때만 리렌더링시 계산을 작동시킬 수 있다.

 

useMemo를 남발하게 되면 별도의 메모리 확보를 너무 많이 하게 되기때문에 오히려 성능을 악화시킬 수 있다. 따라서 적절한 곳에서만, 필요할 때만 사용을 해야한다.

 

커스텀 훅(custom hook)

 

import React from "react";
import { useState } from "react";

const App = () => {
    const [title, setTitle] = useState("");
    const [body, setBody] = useState("");

    return (
        <div>
            <input
                type="text"
                name="body"
                value={body}
                onChange={(e) => {
                    setBody(e.target.value);
                }}
            />
            <input
                type="text"
                name="title"
                value={title}
                onChange={(e) => {
                    setTitle(e.target.value);
                }}
            />
        </div>
    );
};

export default App;

이렇게 input을 입력하고 입력한 데이터를 저장하기위해 state를 만들고, input에 type name value onChange를 지정한다.

이게 여러번 반복되다보면 반복적으로 같은코드를 여러번 쳐야하고, 코드의 가독성도 떨어진다. 따라서 우리는 커스텀 훅을 만들어 쉽게 같은 행동을 반복해야한다.

 

import React from "react";

const useInput = () => {
    const [value, setValue] = useState("");
    const handler = (e) => {
        setValue(e.target.value);
    };
    return [value, handler];
};

export default useInput;

useInput은 state와 setState를 진행하는 handler를 리턴해준다.

import React from "react";
import { useState } from "react";
import useInput from "./hooks/useInput";

const App = () => {
    const [title, onChangeTitleHandler] = useInput();
    const [body, onChangeBodyHandler] = useInput();

    return (
        <div>
            <input type="text" name="body" value={body} onChange={onChangeTitleHandler} />
            <input type="text" name="title" value={title} onChange={onChangeBodyHandler} />
        </div>
    );
};

export default App;

App.jsx에서 useInput의 리턴값을 받아 input에 넣어주면 된다.