ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] 공식문서 파먹기(1) - hooks[useState]
    React 2023. 4. 27. 23:29

     

    리액트 공식문서가 새로 업데이트(?) 됐습니다.

    새로운 공식문서는 함수형 컴포넌트의 예제들로 구성돼있습니다.

    하여, 새로운 공식문서의 Reference 페이지의 hooks를 정리해보려합니다.

     

    https://react.dev/

     

    hooks

    리액트는 초기에 클래스형 컴포넌트로 세상에 나왔지만 16.8버전에 hooks가 추가됐습니다.

    기존의 클래스형을 대신해 hooks를 통해 함수형 컴포넌트를 작성할 수 있습니다.

    기존이라 하기엔 너무 오래전이긴 합니다.

    공식문서 파먹기 시리즈에서는 새로운 공식문서에서 설명하는 hooks를 정리할 예정입니다.

    자주 사용되는 hooks를 위주로 정리하고, 자주 사용하지 않는 hooks들은 간단하게 정리하겠습니다.

     

    useState

    해당 포스팅에서는 가장 많이 사용되는 useState 훅을 정리하겠습니다.

     

     

    useState는 컴포넌트에 상태변수를 정의하는 훅 입니다.

     

     

    useState(initialState)

    선언은 위와같이 선언합니다.

     

     

    initialState

    useState 훅에 전달하는 매개변수는 이러해야합니다.

    • 어떤 값이던 전달할 수 있습니다.
    • 함수를 전달할 경우 함수는 반드시 순수함수여야하며 매개변수가 없어야하고, 반환값이 있어야합니다.
    • 함수일경우에는 초기 렌더링에 함수가 평가되어 함수의 반환값이 초기 상태값으로 전달됩니다.

    useState에 전달하는 매개변수는 초기 렌더링시에만 전달되고 이후 렌더링에는 무시됩니다.

     

     

    Return

    useState에 매개변수를 전달하여 반환되는 값은 두개의 값을 가진 배열을 반환하며 각 인덱스의 요소는 이러합니다.

    • [0] - 매개변수로 전달한 초기값이 반환됩니다.
    • [1] - set함수로 초기값을 갱신하는 함수를 반환합니다.

    state훅은 배열을 반환합니다. 이 배열을 더 읽기 편하도록 구조분해할당을 통해 보편적으로 사용합니다.

    const [state, setState] = useState(0);
    // [0] = state
    // [1] = setState()

    위와 같은 형태로 초기값이 담겨있는 배열의 값인 [state] 그리고 state의 이름 앞에 'set'을 붙혀 갱신하는 함수가 담긴 배열의 값인 [setState]로 정의합니다.

     

     

    주의사항

    useState를 사용할때의 주의사항은 이러합니다.

    • state훅은 반드시 컴포넌트 내부에서 정의해야하며, 컴포넌트의 최상위에 정의해야합니다.
    • for loop 혹은 if문과같은 논리문 내부에서 호출을하면 안됩니다.
    • React.strict 에서는 에러검사를 위해 두번씩 호출하게됩니다. 이는 개발모드(development)에서만 적용되며 실제 서비스 환경(production)에서는 한번만 호출하게됩니다.

    기본적으로 hooks들은 컴포넌트 최상위에 위치해야하며, for loop나 if문 내부에서 호출될경우 실행 순서를 보장할 수 없기때문에 꼬일수있습니다.

     

    setState

    setState(1);
    setState(() => 1);
    setState((prev) => prev + 1); // prev는 이전 상태의 값을 가지고있습니다.

     

     

    초기 상태값을 갱신하는 set 함수는 위와같이 여러가지 형태로 사용할 수 있습니다.

    전달하는 매개변수는 변경할 값, 혹은 함수를 전달할 수 있습니다.

    함수를 전달하는 경우에는 반드시 반환값이 있어야하며 순수함수여야합니다.  그리고 함수의 매개변수는 이전의 상태값만 유일하게 사용가능합니다.

    함수의 형태는 다르지만 요점은 이전의 상태값을 업데이트해주는겁니다.

    set함수로 상태가 변경되면 리렌더링을 트리거 할 수 있습니다.

     

     

    set함수는 반환값이 없는 함수입니다.

    set 함수의 특징은 이러합니다.

     

    • set함수는 다음 렌더링에 대한 상태값만 업데이트해줍니다. 만약 set함수를 호출하고 리렌더링이 발생하기전에 상태값을 읽으면 갱신된 상태값이 아닌 이전 상태값이 조회됩니다.
    • 만약 이전 상태값과 갱신된 상태값을 비교했을때(Object.is) 동일하다면 리렌더링이 발생하지않습니다. 이는 리액트의 최적화중 하나입니다.
    • 만약 하나의 이벤트 핸들러에 여러가지 상태를 변경하는 set함수가 호출됐다면 리렌더링이 각각 여러번 일어나는게 아니라 한번에 업데이트됩니다(automatic batch). 만약 더 일찍 리렌더링이 발생해야한다면 flushSync를 사용할 수 있습니다.
    • 렌더링 도중에 set함수를 호출하게되면 리액트는 해당 함수를 즉시 실행하여 상태를 업데이트합니다.

     

    set함수를 통해 상태를 변경하면 이러한 과정을 통해 UI가 업데이트됩니다.

     

    1. 변경된 상태저장
    2. 변경된 상태로 리렌더링
    3. UI 업데이트

     

    이전 상태값을 기반한 업데이트

     

    const [state, setState] = useState(1);
    
    const onClickHandler = () => {
        setState(state + 1);
        setState(state + 1);
        setState(state + 1);
        setState(state + 1);
        setState(state + 1);
    }

     

    위 코드와같이 하나의 핸들러에서 하나의 상태를 여러번 변경하는 set함수가 있다고 가정해봅시다.

    일반적으로는 state에 1이 5번 더해진 값으로 업데이트될거라 예상하지만 실제는 1이 한번만 더해진 상태로 업데이트됩니다.

    이는 이미 실행중인 함수 내부에서는 state가 변경되지않기때문입니다.

     

     

    만약 위와같이 하나의 핸들러에서 상태값을 여러번 업데이트하고싶다면 이렇게 작성할 수 있습니다.

    const onClickHandler = () => {
        setState((state) => state + 1); // 1 + 1 = 2
        setState((state) => state + 1); // 2 + 1 = 3
        setState((state) => state + 1); // 3 + 1 = 4
        setState((state) => state + 1); // 4 + 1 = 5
    }

     

     

    위에서 set함수의 여러가지 사용법중에 함수를 전달하는 방법을 알아보았습니다.

    set함수에 함수를 전달할 경우 매개변수는 오직 이전 상태값을 사용할 수 있습니다.

    이때 매개변수의 이름은 아무렇게나 지어도 상관없습니다.

     

    함수를 전달하게되면 업데이트되어 보류중인 상태값을 가져와 업데이트하게됩니다.

    위의 예제에서는 첫번째 호출에서 1을 가져와 1을더해 업데이트하여 보류하고, 두번째 호출에서 2를 가져와 1을 더해 3으로 업데이트하여 보류하는 과정을 거칩니다.

     

     

    배열, 객체 상태 업데이트하기

    상태변수에 객체나 배열 형태값이 들어가면 해당 값은 읽기전용값이되므로 불변성을 지켜줘야합니다.

    직접적으로 접근하여 변경하게되는건 위험하여, 복사하여 대체하는 방식으로 업데이트해야합니다.

     

    const [form, setForm] = useState({firstName: 'choi'});
    
    setForm({
        ...form,
        firstName: 'lee'
    });

     

    위의 예제처럼 스프레드 문법으로 객체를 복사하고 변경할 key만 변경해주는 방식으로 set함수를 작성해주어야합니다.

     

     

    초기 상태 유지하기

     const [todos, setTodos] = useState(createInitialTodos());
     const [todos, setTodos] = useState(createInitialTodos);

     

    state훅에 함수도 전달할 수 있습니다.

    위의 코드에서 두가지의 차이는 무엇일까요?

     

    첫 번째 코드는 함수의 호출문까지 포함돼있습니다. state훅으로 전달된 매개변수는 초기 렌더링에만 사용되고 이후에는 무시되지만, 이렇게 되면 매 렌더링마다 매개변수를 사용하게됩니다. 만약에 전달된 매개변수인 함수가 무거운 함수라면 성능의 영향이 있을 수 있습니다.

     

    두 번째 코드는 정상적으로 초기 렌더링시에만 매개변수를 사용합니다. 함수를 전달할때는 함수 호출문을 제외한 함수만을 전달해줘야합니다.

     

     

     

    이전 렌더링 정보 저장

    보통 상태를 변경하는 경우는 이벤트 핸들러 함수 내부에서 변경하는 경우가 대부분입니다.

    드물지만 렌더된 상태의 결과에 따라 상태를 업데이트해줘야하는 경우도 있습니다.

    만약 전체 컴포넌트 트리의 상태를 변경하려면 props를 전달해주는 편이 좋고, 가능하다면 이벤트 핸들러 내부에서 상태를 변경하는게 좋습니다.

     

      const [prevCount, setPrevCount] = useState(count);
      const [trend, setTrend] = useState(null);
      if (prevCount !== count) {
        setPrevCount(count);
        setTrend(count > prevCount ? 'increasing' : 'decreasing');
      }
      return (
        <>
          <h1>{count}</h1>
          {trend && <p>The count is {trend}</p>}
        </>
      );

     

    count는 필요한 값이고, 이전 상태값은 prevCount에 담겨있습니다. trend는 이전 상태와 현재 상태를 비교하여 결과를 담는 값입니다.

    코드를 보면 if문 내부에서 비교를 진행하여 trend를 업데이트해줍니다, 이때 setPrevCount를 같이 호출하지않으면 count와 prevCount가 꼬이게되면서 무한루프에 빠지게됩니다. 즉 위의 코드는 현재 렌더링중인 상태값만 비교할 수 있다는겁니다.

     

    렌더링 도중에 set함수를 만나게되면 리액트는 return에서 컴포넌트를 반환한 후에 자식 컴포넌트를 렌더링하기 전에 리렌더링합니다.

    이렇게되면 자식 컴포넌트를 두 번 렌더링하지 않기때문에 effect 훅보다 더 좋은 방법이긴 합니다만 피하는게 좋습니다.

     

    조건문이 hooks 함수 호출보다 아래에 위치하면 조기반환을 추가하여 렌더링을 일찍 할 수 있습니다.

     

     

    상태는 스냅샷이다

      const [count, setCount] = useState(0);
    
      function handleClick() {
        console.log(count);  // 0
      
        setCount(count + 1); // 1로 업데이트 후 보류
        console.log(count);  // 0
      
        setTimeout(() => {
          console.log(count); // 0
        }, 5000);
      }

     

    위의 코드를 보면 handleClick 내부에 count를 업데이트하고 setTimeout으로 카운트를 5초뒤에 조회합니다.

    5초뒤에 상태가 1로 업데이트될거라 예상하지만, 상태가 업데이트되는것은 스냅샷과 같습니다.

    count가 초기 렌더된 상태에서는 0이기때문에 다음 렌더 이전까지는 상태는 0으로 스냅샷이 찍힌것과같이 0으로 조회됩니다.

    위에서도 언급했듯 같은 함수 내부에서 set함수가 호출되는경우 업데이트는되지만 실제 업데이트된 값이 아닌 이전 값이 조회됩니다.

     

      function handleClick() {
        const nextCount = count + 1;
        setCount(nextCount);
      
        console.log(count);     // 0
        console.log(nextCount); // 1
      }

     

    이런식으로 해결할 수 있지만 set함수에 함수를 전달하여 업데이트하는것이 더 좋습니다.

     

     

    너무 많은 렌더링발생

    return <button onClick={handleClick()}>Click me</button>
    
    return <button onClick={handleClick}>Click me</button>
    
    return <button onClick={(e) => handleClick(e)}>Click me</button>

     

    위 코드는 같은 이벤트지만 세가지 방식으로 작성했습니다.

    리액트는 무한 리렌더링 루프를 방지하기위해 렌더링을 제한합니다.

     

    컴포넌트는 함수입니다 함수기때문에 리렌더링이 발생하면 해당 함수를 다시 실행합니다. 첫 번째 케이스의경우 이벤트 핸들러에 함수 호출이 들어가있기때문에 너무 많은 렌더루프에 빠집니다.

     

    이벤트 핸들러를 등록할때 매개변수를 전달해야한다면 세 번째 케이스로 작성해줘야합니다.

     

     

    상태에 함수 전달

    const [fn, setFn] = useState(someFunction);
    
    function handleClick() {
      setFn(someOtherFunction);
    }

     

    state 훅에 함수를 전달했습니다. 업데이트할때도 초기 함수를 다시 전달하는 경우가있습니다.

     

    const [fn, setFn] = useState(() => someFunction);
    
    function handleClick() {
      setFn(() => someOtherFunction);
    }

     

    이럴 경우에는 위와같이 함수의 반환값으로 전달해주어야합니다.

    댓글

Designed by Tistory.