ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React Hooks
    React 2021. 1. 2. 00:11

    @ React 17.0.1을 기준으로 작성합니다

     

     

    React를 처음 공부했을 때에는 뭐가 뭔지 며칠동안 계속 궁금증에 시달렸었는데

    이제부터 배우는 사람들은 그렇지 않기를 바라면서 보는 사람이 이해하기 최대한 쉽게 작성해보고자 한다

     

    16.8버전부터 추가된 hook은 정말 편리하게 사용가능하다.

    더불어 lifecycle 메서드도 useEffect로 통합되는 등 관리하기 편해졌다.

     

    0) hook의 명명 규칙

    hook은 use라는 접두사가 붙으며 camelCase로 이름을 짓는다.

    그래서 다른 라이브러리의 hook들은 대부분 use~~~로 시작한다

    React에는 어떤 hook이 있는지 알아보자

     

    1) useState

    상태를 바꾸는 hook

     

    사용법

    import React, { useState } from 'react'
    
    export default function App() {
      const [count, setCount] = useState(0)
    
      return (
        <div>
          {count}
          <button onClick={() => setCount(count + 1)}>
            Click me!
          </button>
        <div>
      )
    }

    - useState의 구조

    useState는 상태를 관리하는 함수이지만 그 안에 있는 것은 길이가 2인 배열이라고 생각하면 편하다.

    그것을 구조분해 할당하여 끄집어낸 것이다.

    첫번째 원소는 상태를 관리할 변수

    두번째 원소는 상태를 관리하는 함수

    그리고 useState에 들어가는 parameter는 상태를 관리할 변수의 초기값을 넣는다.

     

    - useState 사용 시 주의사항

    구조 분해 할당한 식의 const를 let으로 바꿔도 count += 1을 하면 count는 변하지 않는다.

    count를 변경하려면 상태를 관리할 함수를 사용하여 바꿔야 한다.

     

    통상적으로 두번째 원소인 상태를 관리하는 함수는 이름을 지을 때는 camelCase로 짓고

    set이라는 접두사가 붙는다.

    또한 초기값과 동일한 자료형으로 상태를 관리해주는 것이 좋다.

    뜬금없이 setCount로 문자열을 할당한다거나 그런 사태는 벌이지 않아야 함!

     

    또한 setCount안에 함수를 작성할 수 있고 반환값은 number 그대로 반환해주면 된다.

    setCount(prev => prev += 1)

     

    이제 버튼을 누를 때마다 count의 값이 올라가는 것을 알 수 있다.

     

    2] useEffect

    useEffect는 side effects를 처리하는 hook이다.

    useEffect는 화면이 업데이트되고 비동기적으로 실행된다 => 따라서 지연시간이 있을 수 있음

    동기적으로 실행하고자 할 때, 공식문서에서는 useEffect 대신 useLayoutEffect를 사용을 권장하고 있다

    그리고 다른 Effect가 실행되기 전에 항상 cleanup 함수를 실행한다

    // fn: 함수, deps?: 배열 (앞으로 언급할 모든 deps는 동일한 역할을 한다고 생각하면 된다)
    useEffect(fn, deps)

    함수에는 보통 익명함수를 작성하는데, 함수 안에 작성된 내용은 컴포넌트가 렌더링될 때 수행하며

    return문에 작성하는 함수는 컴포넌트가 사라질 때 수행된다.

    deps는 배열인데 의존성 배열이라고 한다. 여기에 들어가는 것은 보통은 변수나 함수가 된다.

     

    1. 의존성 배열 자리에 빈 배열을 넣으면 컴포넌트가 처음으로 렌더링이 될 때, 언마운트될 때 useEffect 내부의 함수를 수행한다.

    2. 의존성 배열 자리에 아무것도 넣지 않으면 리렌더링이 자주 일어나거나 버그가 발생할 수 있다. 절대로 변하지 않는 값인 경우에만 빈 배열을 넣도록 하자

    3. 의존성 배열 자리에 변수 1개를 넣는다고 하면, 해당 변수의 이전 상태와 비교해서 현재 상태와 같으면 수행하지 않고

    상태가 달라지면 useEffect 내부의 함수를 수행한다. => 의존성 배열 내부의 값이 여러 개인 경우에는 그 중에 1개만 다르더라도 이펙트를 실행한다

     

     

    사용사례는 대표적으로 (속해있는) 컴포넌트가 렌더링되었을 때 서버에 데이터를 요청한다던지, 컴포넌트가 사라질 때 event를 제거한다던지 등 여러가지 effect들을 수행할 수 있다.

     

    사용법

    import React, { useState, useEffect } from 'react'
    
    
    export default App() {
      const [data, setData] = useState('hello')
      
      useEffect(() => {
        setData('useEffect')
      }, [])
    
      return (
        <div>{data}</div>
      )
    }

    data의 값이 처음에 hello로 설정되어있어 화면이 렌더링될 때에는 div태그 안에 hello라는 문자열이 출력될 것이다.

    그러나 useEffect에서 data를 useEffect라는 문자열로 바꾸었기 때문에 App컴포넌트가 렌더링될 때는

    div태그 안에 useEffect라는 문자열이 출력된다.

     

    비유가 이상하긴 하지만

    한 학생이 숙제를 했는지 안했는지를 체크하는 상황이라고 가정한다

    그러나 학생이 누군지 알 수 없으면 학생의 데이터를 가져와야 한다고 해보자

    import React, { useState, useEffect } from 'react'
    
    
    export default function App() {
      const [data, setData] = useState({ id: null, done: false })
      
      useEffect(() => {
        if (data.id === null) {
          fetch('http://example.com/data.json')
            .then((res) => setData(res.data))
            .catch((err) => {
              throw new Error('no Data!');
            });
        }
      }, [data])
    
      return (
        <div>{data.id}</div>
      )
    }

    ※ fetch는 데이터를 가져오는 JavaScript 함수이다

    어떤 학생인지(data.id) 화면에 출력을 해줘야 하는데 null이라면 선생님은 어떤 학생인지 알 도리가 없다

    그래서 자동으로 출력을 해주려고 한다.

    useEffect는 return문 위에서는 화면에 렌더링될 때 실행되니 렌더링되자마자 id값이 null이면 데이터를 요청한다

    데이터가 있으면 setData를 이용하여 data를 갱신해주고 없다면 error를 발생시킨다

     

    3] useLayoutEffect

    사용법은 useEffect와 동일하며 사용하는 경우를 알면 될 것 같다.

    useLayoutEffect는 useEffect와 유사하지만 모든 DOM 변경 후에 동기적으로 발생한다.

    DOM을 변경한다거나 상태가 업데이트될 때 요소가 깜빡이는 경우에 사용하면 되겠다

     

    공식문서에서는 useEffect와 useLayoutEffect를 비교해서 이렇게 설명하고 있다

    '가급적이면 useEffect를 먼저 사용하고 사용 중에 문제가 있다면 useLayoutEffect를 사용하세요'

    4] useMemo

    사용법 useMemo(() => any, deps)

    값을 메모이제이션하여 잦은 렌더링을 방지해주는 최적화 hook이다

    이 함수는 useEffect, useEffectLayout과는 다르게 return에 값을 반환해주면 된다.

    import React, { useState, useMemo } from 'react'
    
    const memory = (num) => num + 3
    export default function App() {
      const [data, setData] = useState(0)
      
      useMemo(() => memory(data), [data])
    
      return (
        <div>{data}</div>
      )
    }

    4] memo

    memo함수는 함수를 useMemo와 비슷한 기능을 하지만 그 대상이 보통 컴포넌트이다.

    대표적인 사용 예시는 사용자가 입력을 할 때마다 컴포넌트는 계속 리렌더링이 된다.

    이런 리렌더링이 동시다발적으로 일어나게 되면 부하가 발생하여 성능이 저하된다.

    이것을 최적화해주는 것이 memo함수이다.

     

    import React, { memo } from 'react'
    
    export default memo(function App() {
      const [data, setData] = useState(0)
      const onChange = (event) => setData(event.target.value)
      
      return (
        <input value={data} onChange={onChange} />
      )
    })
    
    // 혹은
    
    import React from 'react'
    
    export default React.memo(function App() {
      const [data, setData] = useState(0)
      const onChange = (event) => setData(event.target.value)
      
      return (
        <input value={data} onChange={onChange} />
      )
    })

     

    5] useCallback

    useMemo와 동일한 최적화 기능을 한다.

    메모이제이션된 콜백을 반환함

    사용법 useCallback(fn, deps)

    useMemo(() => fn, deps) === useCallback(fn, deps)

    import React, { memo } from 'react'
    
    export default memo(function App() {
      const [data, setData] = useState(0)
      const onChange = useCallback((event) => setData(event.target.value), [])
      
      return (
        <input value={data} onChange={onChange} />
      )
    })
    

     

    6] useRef

    useRef는 initialValue로 초기화된 변경가능한 ref객체를 .current로 반환한다.

    React 공식문서에서는 ref를 사용한 코드를 되도록이면 지양하라고 권장하고 있으니 되도록이면

    적게 사용하는 쪽으로 코드를 작성하자 (공식문서 useImperativeHandle 부분 참고)

    const ref = useRef(initialValue)

    보통은 DOM을 조작할 때 사용한다. 또는 가변값을 유지할 때에도 사용된다

    다음은 렌더링될 때 자동으로 input에 focus를 잡는 예제이다

    사용할 때에는 .current를 붙여서 사용해야 한다.

    import React, { useRef, useEffect } from 'react'
    
    export default function App() {
      const ref = useRef(null)
      
      useEffect(() => {
        if (ref.current !== null) {
          ref.current.focus()
        }
      }, [ref])
      
      return (
        <input value={data} onChange={onChange} ref={ref} />
      )
    }

    다음은 가변값을 유지하는 데에 사용한 예제이다

    useRef의 값이 바뀌더라도 화면은 자동으로 리렌더링이 되지 않는다는 것에 주의하자.

    import React, { useRef, useEffect } from 'react'
    
    export default function App() {
      const [state, setState] = useState([
        { id: 1, done: false },
        { id: 2, done: true },
      ]);
      const ref = useRef(3);
    
      const onClick = () => {
        setState([...state, { id: ref.current, done: false }]);
        ref.current += 1;
      };
    
      return (
        <>
          <div>
            {state.map((value, index) => (
              <p key={index}>
                {value.id} : {value.done ? 'done' : 'not done'}
              </p>
            ))}
          </div>
          <button onClick={onClick}>increase</button>
        </>
      );
    }

    7] useImperativeHandle

    React 공식 문서에서는 useImperativeHandle을 forwardRef와 같이 사용하라고 권장하고 있다.

     

    useImperativeHandle은 ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값을 사용자화한다

    무슨 말인고 하니 자식 컴포넌트 안에서 정의된 메서드를 부모 컴포넌트에서 사용할 수 있다.

     

    부모 컴포넌트

    import React, { useRef } from 'react';
      
    function App() {
      const ref = useRef();
    
      const onClick = () => {
        if (ref.current) {
          ref.current.addCount();
        }
      };
    
      return (
        <div>
          <Child ref={ref} />
          <button onClick={onClick}>1 increase</button>
        </div>
      );
    }
    
    export default App;

     

    자식 컴포넌트

    import React, { useState, useImperativeHandle, forwardRef } from 'react';
    
    function Child(props, ref) {
      const [count, setCount] = useState(0);
    
      useImperativeHandle(
        ref,
        () => ({
          addCount: () => setCount(count + 1),
        }),
        [count],
      );
    
      return <p>{count}</p>;
    }
    export default forwardRef(Child);

    주의사항은 보통 props부분에 props안에 있는 것들만 따로 빼기 위해서 구조분해할당으로 ({ ref }) 이렇게 사용했을 것인데 forwardRef를 사용하게 되면 (props, ref)로 작성해줘야 한다. 이것은 forwardRef의 규칙이다.

    순서도 바뀌면 에러를 보여준다. 

    기존에 전달하던  props 속성을 구조분해 할당하고 싶다면,

    Child({ 속성명 }, ref) 으로 작성해야 한다.

    8] useReducer

    const [state, dispatch] = useReducer(reducer, initialArg, init)
    const [정의할 상태, 상태를 변경하는 함수] = useReducer(상태를 변경하는 로직, 초기값, 값을 초기화하는 함수)

    공식문서에서는 useState를 대체하는 함수라고 소개하고 있고 redux를 사용한다면 어떻게 작동하는지 알거라고 말한다

    실제로도 redux와 별 다를 건 없다.

    다만 다른 점은 state와 dispatch를 사용할 수 있는 범위가 어디까지인지가 다르다

     

    간단하게 counter 예제를 만들어보자면 다음과 같이 사용할 수 있다

    const reducer = (state, action) => {
      switch (action.type) {
        case 'increase':
          return (state += 1);
        case 'decrease':
          return (state -= 1);
        default:
          throw new Error();
      }
    };
    
    function App() {
      const [count, dispatch] = useReducer(reducer, 0);
    
      return (
        <div>
          <p>{count}</p>
          <button onClick={() => dispatch({ type: 'increase' })}>increase</button>
          <button onClick={() => dispatch({ type: 'decrease' })}>decrease</button>
        </div>
      );
    }
    
    export default App;
    

    9] useContext

    useContext는 createContext에서 반환된 객체를 받아 객체의 값을 반환한다. 반환하는 값은 hook을 호출하는 value prop에 의해 결정되며 하위 컴포넌트에게 값을 공유하고 싶을 때 사용한다.

     

    const context = useContext(anyContext)

     

    counter예제를 useContext로 하여 하위 컴포넌트에서 dispatch(상태변경을)를 하고 싶다면 아래와 같이 작성해보자

    import React, { useReducer, createContext, useContext } from 'react';
    
    
    const reducer = (state, action) => {
      switch (action.type) {
        case 'increase':
          return (state += 1);
        case 'decrease':
          return (state -= 1);
        default:
          throw new Error();
      }
    };
    
    function Child() {
      const dispatch = useContext(MyContext);
    
      return (
        <>
          <button onClick={() => dispatch({ type: 'increase' })}>increase</button>
          <button onClick={() => dispatch({ type: 'decrease' })}>decrease</button>
        </>
      );
    }
    
    export const MyContext = createContext(null);
    
    function App() {
      const [count, dispatch] = useReducer(reducer, 0);
    
      return (
        <MyContext.Provider value={dispatch}>
          <p>{count}</p>
          <Child />
        </MyContext.Provider>
      );
    }
    
    export default App;
    

     

    현재 reducer는 App 컴포넌트 내부에 정의되어있다. 따라서 Child 컴포넌트에서는 사용할 수 없다

    Child 컴포넌트에서 count의 수를 바꿔주려면 dispatch를 공유하거나 따로 child 내부에서 reducer를 사용해야 하는데

    만약 여러 컴포넌트에서 같은 코드를 반복해주면 코드량이 늘어나니 context를 통해 해결하는 것이다

     

    1. createContext() 안에 초기값을 설정해주고 변수에 저장한 후 다른 파일에도 사용하고 싶다면 export 해준다

    컴포넌트명에 써야하니 첫 글자는 대문자로 작성해주자

    2. 어디부터 공유할지 상위 컴포넌트를 정하고 그 안에 <MyContext.Provider>로 감싼다

    3. 여는 태그 MyContext.Provider 우측에 value={}을 작성하고 중괄호 안에 공유하고 싶은 값을 지정한다

    4. 값을 공유할 컴포넌트 내부에서 useContext(MyContext)를 작성해주면 하위 컴포넌트에서도 context객체를 공유받을 수 있다

    - 이외의 Hook

    10] useDebugValue

     

    'React' 카테고리의 다른 글

    Recoil  (0) 2021.04.28
    React의 성능을 올려보자!  (0) 2021.01.18
    React 알아보기  (0) 2021.01.01

    댓글

Designed by Tistory.