리액트 기본 배우기 -훅에 대해서-

1. 훅이란 무엇인가

 

클래스 컴포넌트에서는 생성자 constructor에서 state를 정의하고 setState() 함수를 통해 state를 업데이트한다.

 

이처럼 클래스 컴포넌트는 state와 관련된 기능뿐만 아니라 컴포넌트의 생명주기 함수들까지 모두 명확하게 정의되어 있어서 잘 가져다 쓰기만 하면 된다

 

하지만 함수 컴포넌트는 클래스 컴포넌트와는 다르게 코드도 굉장히 간결하며, 별도로 state를 정의해서 사용하거나,

 

컴포넌트의 생명주기에 맞춰 어떤 코드가 실행되도록 할 수 없었다

 

그래서 함수 컴포넌트에 이런 기능을 지원하기 위해 만든것이 훅이다.

 

훅을 사용해서 함수 컴포넌트도 클래스 컴포넌트의 기능을 모두 동일하게 구현할 수 있게 된다.

 

Hook이라는 영단어는 갈고리라는 뜻을 갖는다.

 

프로그래밍에서는 "원래 존재하는 어떤 기능에 마치 갈고리를 거는 것처럼 끼어 들어가 같이 수행되는 것"

 

리액트의 훅도 마찬가지로 리액트의 state와 생명주기 기능에 갈고리를 걸어 원하는 시점에 정해진 함수를 실행되도록 만든 것이다.

 

이런 훅의 이름은 모두 use로 시작한다.

 

훅이 수행하는 기능에 따라 이름을 짓는데, 각 기능을 사용하겠다는 의미로 use를 앞에 붙였다.

 

개발자가 직접 커스텀 훅을 만들어 사용할 수도 있으며, 개발자 마음대로 이름을 지을 수 있지만

 

규칙에 따라 이름 앞에 use를 붙여서 훅이라는 것을 나타내는게 좋다

 

 

2. useState

 

가장 대표적이고 많이 사용되는 훅

 

state를 사용하기 위한 훅

 

함수 컴포넌트에서는 state를 제공하지 않아서, 클래스 컴포넌트처럼 state를 사용하고 싶다면, useState()를 사용해야 한다.

 

import React, {useState} from "react";

function Counter(props){
    var count = 0;

    return (
        <div>
            <p>총 {count}번 클릭했습니다.</p>
            <button onClick={() => count++}>
                클릭
            </button>
        </div>
    )
}

 

위 코드에서 Counter라는 함수 컴포넌트가 정의되었다.

 

Counter 컴포넌트는 버튼을 클릭하면 카운트를 하나씩 증가시키고, 현재 카운트를 보여준다.

 

만약 위처럼 카운트를 하나의 변수 var count로 선언해서 사용하게 되면,

 

버튼 클릭 시 카운트 값을 증가시킬 수는 있어도, 재렌더링이 일어나지 않아 새로운 카운트 값이 화면에 표시되지 않게 된다.

 

이런 경우에 state를 사용해서 값이 바뀔때마다 재렌더링이 되도록 해야한다.

 

하지만 함수 컴포넌트에 이런 기능이 따로 없어서, useState()를 사용해서 state를 선언하고 업데이트해야한다.

 

const [변수명, set함수명] = useState(초기값);

 

useState()를 호출할 때, 파라미터로 선언할 state의 초기값이 들어간다.

 

클래스 컴포넌트의 생성자에서 state를 선언할 때 초기값을 넣어주는 것과 동일하다.

 

초기값을 넣어 useState()를 호출하면 리턴 값으로 배열이 나온다.

 

리턴된 배열에는 2가지 항목이 들어가있다.

 

첫번째는 state로 선언된 변수이며 두번째는 해당 state의 set함수이다.

 

function Counter(props) {
    const [count, setCount] = useState(0)

    return (
        <div>
            <p>총 {count}번 클릭했습니다.</p>
            <button onClick={() => setCount(count+1)}>
                클릭
            </button>
        </div>
    )
}

 

위 코드는 useState()를 사용해서 카운트 값을 state로 관리하도록 만든 것이다.

 

state의 변수명과 set함수가 각각 count, setCount로 되어있다.

 

버튼이 눌렸을때, setCount()함수를 호출해서 count를 1 증가시킨다.

 

그리고 count 값이 변경되면, 컴포넌트가 재렌더링되면서 화면에 새로운 count값을 표시해준다.

 

이는 마치 클래스 컴포넌트에서 setState() 함수를 호출해서 state가 업데이트되고 이후 컴포넌트가 재렌더링되는 과정과 동일하다고 보면 된다.

 

다만 클래스 컴포넌트에서는 setState()함수 하나를 사용해서 모든 state값을 업데이트할 수 있었으나,

 

useState()를 사용하는 방법에서는 변수 각각에 대해 set함수가 따로 존재한다.

 

 

3. useEffect

 

3-1) 개요

 

useState()와 같이 대표적으로 많이 사용되는 훅

 

사이드 이펙트를 수행하기 위한 훅

 

사이드 이펙트는 사전적으로 "부작용"

 

컴퓨터 프로그래밍에서도 부정적인 의미로 사용되기도 함

 

예를 들어 개발자가 의도치 않은 코드가 실행되면서 버그가 나타나면, 사이드 이펙트가 발생했다고 말함

 

하지만 리액트에서는 부정적인 의미는 아니다.

 

리액트에서는 그냥 "효과, 혹은 영향"을 뜻하는 이펙트(effect)에 불과하다.

 

예를 들어 서버에서 데이터를 받아오거나, 수동으로 DOM을 변경하는 등의 작업

 

이런 작업들이 이펙트인 이유는, 이런 작업들이 다른 컴포넌트에 영향을 미칠 수 있으며, 렌더링중에는 작업이 완료될 수 없기 때문이다.

 

즉, 렌더링이 끝난 이후에 실행되어야 하는 작업들이다.

 

 

3-2) 사용

 

useEffect()는 리액트의 함수 컴포넌트에서 사이드 이펙트를 실행할 수 있도록 해주는 훅이다.

 

useEffect()는 클래스 컴포넌트에서 제공하는 생명주기 함수인 componentDidmount(), componentDidUpdate(), componentWillUnmount()와 동일한 기능

 

생명주기 함수와 동일한 기능

 

useEffect(이펙트 함수, 의존성 배열);

 

첫번째 파라미터로는 이펙트 함수, 두번째 파라미터로는 의존성 배열이 들어간다.

 

 

의존성 배열은 말 그대로 이 이펙트가 의존하고 있는 배열이다.

 

"배열 안에 있는 변수 중에 하나라도 값이 변경되었을 때 이펙트 함수가 실행된다"

 

 

기본적으로 이펙트 함수는 처음 컴포넌트가 렌더링된 이후와, 업데이트로 인한 재렌더링 이후에 실행

 

 

만약 이펙트 함수가 마운트와 언마운트시에 단 한번씩만 실행되게 하고 싶다면...

 

의존성 배열에 빈 배열 []을 넣으면 된다.

 

이렇게 하면 해당 이펙트가 props, state에 있는 어떤 값에도 의존하지 않으므로, 여러번 실행되지 않는다.

 

 

의존성 배열은 생략할 수도 있으나, 생략하게 되면 컴포넌트가 업데이트될 때마다 호출된다.

 

import React, {useState, useEffect } from 'react';

function Counter(props){
    const [count, setCount] = useState(0)

    //componentDidMount, componentDidUpdate와 비슷
    useEffect(() => {
        //브라우저 api를 사용해 document의 title을 업데이트
        document.title = `총 ${count}번 클릭했습니다.`
    })

    return (
        <div>
            <p> 총 {count}번 클릭했습니다. </p>
            <button onClick={() => setCount((count+1))}>
            클릭
            </button>
        </div>
    )
}

 

useEffect()를 사용해서 클래스 컴포넌트에서 제공하는 componentDidMount(), componentDidUpdate()와 같은 생명주기 함수의 기능을 동일하게 수행하도록 만들었다

 

useEffect()안에 있는 이펙트 함수에서는 브라우저에서 제공하는 API를 사용해 document의 title을 업데이트

 

document의 title은 브라우저에서 페이지를 열었을 때 창에서 표시되는 문자열이다.

 

위 코드처럼 의존성 배열 없이 useEffect()를 사용하면, 리액트는 DOM이 변경된 이후에 해당 이펙트 함수를 실행하라는 의미이다.

 

기본적으로 컴포넌트가 처음 렌더링될 때를 포함해서, 매번 렌더링될때마다 이펙트가 실행된다

 

그래서, 처음 컴포넌트가 마운트되었을 때 실행되고, 이후 컴포넌트가 업데이트될 때마다 실행된다.

 

 

또한 이펙트는 함수 컴포넌트 안에서 선언되므로, props와 state에 접근할 수 있다.

 

    //componentDidMount, componentDidUpdate와 비슷
    useEffect(() => {
        //브라우저 api를 사용해 document의 title을 업데이트
        document.title = `총 ${count}번 클릭했습니다.`
    })

 

위의 경우 state에 존재하는 count에 접근해서 문자열을 만들고 있다

 

function UserStatus(props) {
    const [isOnline, setIsOnline] = useState(null);

    function handleStatusChange(status) {
        setIsOnline(status.isOnline)
    }

    useEffect(() => {
        ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange)

        return () => {
            ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange)
        }
    })

    if (isOnline == null){
        return '대기 중...';
    }

    return isOnline ? '온라인' : '오프라인';
}

 

위 코드는 useEffect()에서 ServerAPI를 사용해 사용자의 상태를 구독하고 있다

 

이후 하나의 함수 () => {~}를 return한다

 

이 return하는 함수는 구독을 해지하는 API를 호출하도록 되어있다.

 

useEffect()에서 리턴하는 함수는 컴포넌트 마운트가 해제될때 호출된다

 

useEffect()의 return 함수의 역할은 componentWillUnmount() 함수가 하는 역할과 동일하다.

 

 

useEffect()는 하나의 함수 컴포넌트에 여러개 사용할 수 있다

 

function UserStatusWithCounter(props) {
    const [count, setCount] = useState(0)
    useEffect(() => {
        document.title = `총 ${count}번 클릭했습니다.`
    })

    const [isOnline, setIsOnline] = useState(null);

    useEffect(() => {
        ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange)
        return () => {
            ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange)
        }
    })

    function handleStatusChange(status){
        setIsOnline(status.isOnline)
    }
}

 

useEffect()의 대략적인 구조는 다음과 같다..

 

 

4. useMemo

 

memoized value를 리턴하는 훅

 

memoized value를 생성하는 create 함수와 의존성 배열을 받는다

 

의존성 배열에 들어있는 변수가 변했을 경우에만 새로 create 함수를 호출하여 결과값을 반환하며, 

 

그렇지 않으면 기존 함수의 결과값을 그대로 반환한다.

 

useMemo() 훅을 사용하면 컴포넌트가 다시 렌더링될때마다 연산량이 높은 작업을 반복하는 것을 피할 수 있다

 

 

useMemo()로 전달된 함수는 렌더링이 일어나는 동안 실행된다

 

그래서 렌더링이 일어나는 동안 실행돼서는 안되는 작업은 useMemo()를 사용해서는 안된다.

 

예를 들어 useEffect() 훅에서 실행돼야 할 사이드 이펙트 같은 것

 

서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 작업 등은

 

렌더링이 일어나는 동안 실행돼서는 안되기 때문에 useMemo() 훅의 함수에 넣으면 안되고 useEffect() 훅을 사용해야함

 

의존성 배열을 넣지 않으면 렌더링이 일어날 때마다 매번 함수가 실행된다.

 

그래서 useMemo()에 의존성 배열을 넣지 않으면 아무런 의미가 없다.

 

빈 배열을 넣으면, 마운트 시에만 함수가 실행된다.

 

const memoizedValue = useMemo(
    () => computeExpensiveValue(a,b)
)

 

 

********

eslint-plugin-react-hooks 패키지는 의존성 배열이 잘못되어있으면, 자동으로 경고를 표시하고 고칠 방법을 제안해준다고 함********

 

 

5. useCallback

 

useMemo()와 비슷한데, 값이 아닌 함수를 반환한다

 

useCallback()은 useMemo()와 마찬가지로 함수와 의존성배열을 파라미터로 받는다

 

useCallback()에서는 파라미터로 받는 이 함수를 콜백(callback)이라고 부른다

 

의존성 배열에 있는 변수 중 하나라도 변경되면 memoized callback function을 반환한다

 

 

의존성 배열에 따라 memoized 값을 반환한다는 점에서 useMemo() 훅과 완전히 동일하다.

 

 

 

그래서 useCallback(function,dependencies)은 useMemo(() => function, dependencies)와 동일하다

 

만약 useCallback()을 쓰지 않고, 컴포넌트 내에 함수를 정의한다면, 매번 렌더링이 될때마다 함수가 새로 정의된다.

 

그래서 useCallback()을 사용해서 특정 변수의 값이 변한 경우에만 함수를 다시 정의하도록 해서 불필요한 반복 작업을 없앤다.

 

 

useCallback()을 사용하지 않고, 컴포넌트 내에서 정의한 함수를 자식 컴포넌트에 props로 넘겨 사용하는 경우,

 

부모 컴포넌트가 렌더링이 될때마다, 매번 자식 컴포넌트도 다시 렌더링이 된다.

 

 

하지만 useCallback()을 사용하면 특정 변수의 값이 변한 경우에만 함수를 다시 정의하게 되므로,

 

함수가 다시 정의되지 않는 경우에 자식 컴포넌트도 재렌더링이 일어나지 않는다.

 

-----------------------------------------------------------------------------------------------------------------

 

메모이제이션 memoization

 

useMemo()와 useCallback()훅에서는 메모이제이션이라는 개념이 나온다

 

컴퓨터 분야에서 메모이제이션은 최적화를 위해 사용하는 개념이다.

 

비용이 높은 (연산량이 많이 드는) 함수의 호출 결과를 저장해두었다가,

 

같은 입력값으로 함수를 호출하면, 새로 함수를 호출하지 않고 이전에 저장해놨던 호출 결과를 바로 반환하는 것이다.

 

이렇게 하면 결과적으로 함수 호출 결과를 받기까지 걸리는 시간도 짧아지면서 불필요한 중복 연산도 하지 않아서 컴퓨터의 자원을 적게 쓰게 된다.

 

알고리즘 잘해야하는 이유가 있긴하네..

 

---------------------------------------------------------------------------------------------------------------------

 

6. useRef

 

useRef() 훅은 레퍼런스를 사용하기 위한 훅

 

리액트에서 레퍼런스는 "특정 컴포넌트에 접근할 수 있는 객체"

 

useRef() 훅은 레퍼런스 객체를 반환한다

 

레퍼런스 객체에는 .current라는 속성이 있는데 현재 레퍼런스하고 있는 엘리먼트를 의미한다

 

const refContainer = useRef(초기값);

 

위와 같이 useRef() 훅을 사용하면 파라미터로 들어온 초기값으로 초기화된 레퍼런스 객체를 반환한다

 

만약 초기값이 null이면 .current값이 null인 레퍼런스 객체가 반환된다.

 

이렇게 반환된 레퍼런스 객체는 컴포넌트의 라이프타임 전체에 걸쳐서 유지된다

 

컴포넌트가 마운트 해제 전까지는 계속 유지하게 된다.

 

쉽게 말해 useRef() 훅은 변경 가능한 .current라는 속성을 가진 하나의 상자라고 생각하면 된다.

 

아래 코드는 useRef() 훅을 사용해서 버튼 클릭 시 <input>에 포커스를 하도록 하는 코드

 

function TextInputWithFocusButton(props) {
    const inputElem = useRef(null);

    const onButtonClick = () => {
        //current는 마운트된 input element를 가리킴
        inputElem.current.focus();
    }

    return (
        <>
        <input ref={inputElem} type="text" />
        <button onClick={onButtonClick}>Focus the input</button>
        </>
    )
}

 

초기값으로 null을 넣어 반환된 inputElem이라는 레퍼런스 객체를 <input> 태그에 넣어준다

 

그리고 버튼 클릭시에 호출되는 onButtonClick 함수에서 inputElem.current를 통해

 

실제 엘리먼트에 접근했고, focus()함수를 호출하고 있다.

 

---------------------------------------------------------------------------------------------------------------------

 

DOM에 접근하기 위해 사용하는 ref 속성과 비슷하다.

 

리액트에서는 <div ref={myRef} />라는 코드를 작성하면, node가 변경될 때마다 myRef의 .current 속성에

 

현재 해당되는 DOM node를 저장한다.

 

----------------------------------------------------------------------------------------------------------------------

 

ref 속성과 기능은 비슷하지만, useRef() 훅은 클래스의 인스턴스 필드를 사용하는 것과 유사하게

 

다양한 변수를 저장할 수 있다는 장점이 있다.

 

이것이 가능한 이유는 useRef()훅이 일반적인 자바스크립트 객체를 리턴하기 때문이다.

 

또한 useRef() 훅은 매번 렌더링될 때마다 항상 같은 ref객체를 반환한다.

 

한가지 기억할 점은 useRef() 훅은 내부의 데이터가 변경되었을 때 별도로 알리지 않는다는 점

 

.current 속성을 변경하는 것은 재렌더링을 일으키지 않는다.

 

따라서 ref에 DOM node가 연결되거나 분리되었을 경우 어떤 코드를 실행하고 싶다면

 

callback ref를 사용해야 한다.

 

-----------------------------------------------------------------------------------------------------------------------

 

DOM node의 변화를 알기 위한 callback ref를 사용

 

리액트는 ref가 다른 node에 연결될때마다 콜백을 호출한다.

 

function MeasureExample(props) {
    const [height, setHeight] = useState(0)

    const measuredRef = useCallback(node => {
        if (node !== null) {
            setHeight(node.getBoundingClientRect().height)
        }
    },[])

    return (
        <>
        <h1 ref={measureRef}>안녕, 리액트</h1>
        <h2>위 헤더의 높이는 {Math.round(height)}px입니다.</h2>
        </>
    )
}

 

위 코드에서는 레퍼런스를 위해 useRef()가 아니라 useCallback()을 사용한 callback ref를 사용하고 있다.

 

useRef()를 사용하면, 레퍼런스 객체가 .current 속성이 변경되었는지를 따로 알려주지 않기 때문이다.

 

하지만 callback ref 방식을 사용해서, 자식 컴포넌트가 변경되었을 때 알림을 받고

 

이를 통해 다른 정보들을 업데이트 한다.

 

이 예제 코드에서는 <h1> 태그의 높이 값을 매번 업데이트하고 있다.

 

그리고 useCallback() 훅의 의존성 배열로 비어있는 배열 empty array을 넣었는데 

 

이렇게 하면 <h1> 태그가 마운트, 언마운트 될때만 콜백 함수가 호출되고, 재렌더링이 일어날 때에는 호출되지 않는다.

 

 

7. 훅의 규칙

 

첫번째 규칙은 훅은 무조건 최상위 레벨에서만 호출해야한다.

 

여기서 최상위 레벨은, 리액트 함수 컴포넌트의 최상위 레벨을 의미한다.

 

반복문이나 조건문, 중첩된 함수들 안에서 훅을 호출하면 안된다.

 

이 규칙에 따라 훅은 컴포넌트가 렌더링될 때마다 매번 같은 순서로 호출되어야 한다.

 

그래야 리액트가 다수의 useState()훅과 useEffect()훅의 호출에서 컴포넌트의 state를 올바르게 관리할 수 있게 된다.

 

function MyComponent(props) {
    const [name, setName] = useState('daehyuck');
    
    if (name !== '') {
        useEffect(() => {
        ...
        });
    
    }
    ...
}

 

위에서 name !== ''라는 조건문의 값이 참인 경우에만 useEffect() 훅을 호출하도록 되어 있다.

 

이런 경우 중간에 name의 값이 빈 문자열이 되면 useEffect() 훅이 호출되지 않는다.

 

결과적으로 렌더링할 때마다 훅이 같은 순서대로 호출되는 것이 아니라,

 

조건문의 결과에 따라 호출되는 훅이 달라져서 잘못된 코드이다.

 

 

두번째 규칙은 리액트 함수 컴포넌트에서만 훅을 호출해야한다.

 

일반적인 자바스크립트 함수에서 훅을 호출하면 안된다.

 

훅은 리액트 함수 컴포넌트에서 호출하거나, 직접 만든 커스텀 훅에서만 호출할 수 있다.

 

이 규칙에 따라 리액트 컴포넌트에 있는 state와 관련된 모든 로직은 명확하게 확인이 가능해야 한다.

 

 

8. 커스텀 훅

 

기본적으로 제공되는 훅들 이외에 추가적으로 필요한 기능이 있다면, 직접 훅을 만들어서 사용할 수 있다.

 

커스텀 훅을 만드는 이유는 여러 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어 재사용하기 위함

 

8-1) 커스텀 훅을 만들어야하는 경우

 

아래 코드의 UserStatus라는 컴포넌트는 isOnline이라는 state에 따라 사용자의 상태가 온라인인지 아닌지를 텍스트로 보여주는 컴포넌트입니다.

 

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

function UserStatus(props) {
    const [isOnline, setIsOnline] = useState(null);

    useEffect(() => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline)
        }

        ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
        return () => {
            ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange)

        }
    })

    if (isOnline == null) {
        return '대기중...';
    }
    return isOnline ? '온라인' : '오프라인';
}

 

그리고 동일한 웹사이트에서 연락처 목록을 제공하는데, 이때 온라인인 사용자의 이름은 초록색으로 표시해주고 싶다고 가정하고,

 

이 컴포넌트의 이름을 UserListItem이라고 하자.

 

import React, {useState, useEffect} from 'react';

function UserListItem(props) {
    const [isOnline, setIsOnline] = useState(null)

    useEffect(() => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline)
        }

        ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);

        return () => {
            ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange)
        }
    })

    return (
        <li style={{ color: isOnline ? 'green' : 'black' }}>
            {props.user.name}
        </li>
    )
}

 

 

위에 나온 UserStatus와 useState(), useEffect() 훅을 사용하는 부분이 동일하다.

 

여러 곳에서 중복된다는 이야기

 

기존의 리액트에서는 보통 이렇게 state와 관련된 로직이 중복되는 경우, render props 또는 HOC를 사용한다.

 

여기서는 중복되는 코드를 추출해서 커스텀 훅으로 만들어보자

 

 

8-2) 커스텀 훅 추출

 

중복되는 로직을 커스텀 훅으로 추출하자.

 

2개의 자바스크립트 함수에서 하나의 로직을 공유하도록 하고 싶으면 새로운 함수를 하나 만드는 방법을 사용한다.

 

리액트 컴포넌트와 훅은 모두 함수이기 때문에 동일한 방법을 사용한다.

 

커스텀 훅은 특별한 것이 아닌, "이름이 use로 시작하며 내부에서 다른 훅을 호출하는 하나의 자바스크립트 함수"

 

import {useState, useEffect} from 'react';

function useUserStatus(userId) {
    const [isOnline, setIsOnline] = useState(null)

    useEffect(() => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline)
        }

        ServerAPI.subscribeUserStatus(userId, handleStatusChange)
        return () => {
            ServerAPI.unsubscribeUserStatus(userId, handleStatusChange)
        }
    })

    return isOnline
}

 

위 코드는 중복되는 부분을 useUserStatus()라는 훅으로 구현한 것이다.

 

특별한 것은 없고 그냥 두개의 컴포넌트에서 중복되는 로직을 추출해서 가져왔다.

 

다만 다른 컴포넌트 내부에서와 마찬가지로 다른 훅을 호출하는 것은 무조건 커스텀 훅의 최상위 레벨에서만 해야한다.

 

리액트 컴포넌트와 다르게 커스텀 훅은 특별한 규칙은 없다.

 

예를 들어 파라미터로 무엇을 받을지, 어떤 것을 리턴할지 개발자가 직접 정할 수 있다.

 

다시 말해 커스텀 훅은 그냥 단순한 함수와 같다.

 

하지만 이름이 use로 시작하도록 해서 이것이 단순한 함수가 아니라 리액트 훅이라는 것을 나타내준다.

 

useUserStatus() 훅의 목적은 사용자의 온라인/오프라인 상태를 구독하는 것이다.

 

그렇기 때문에 아래 코드처럼 useUserStatus() 훅의 파라미터로 userId를 받도록 만들었고, 해당 사용자가 온라인인지 오프라인인지의 상태를 리턴하게 했다.

 

function useUserStatus(userId) {
    const [isOnline, setIsOnline] = useState(null)
    
    //...
    
    return isOnline
}

 

8-3) 커스텀 훅 사용하기

 

커스텀 훅을 만들기로 했을 때 목표는 UserStatus와 UserListItem 컴포넌트로부터 중복된 로직을 제거하는 것

 

그리고 2개의 컴포넌트는 모두 사용자가 온라인 상태인지를 알기 원하였다.

 

중복되는 로직을 useUserStatus()로 변경하였으므로, 다음과 같이 코드를 작성할 수 있다.

 

function UserStatus(props) {
    const isOnline = useUserStatus(props.user.id);

    if(isOnline === null) {
        return '대기중...';
    }
    return isOnline ? '온라인' : '오프라인';
}

function UserListItem(props) {
    const isOnline = useUserStatus(props.user.id)

    return (
        <li style={{ color: isOnline ? 'green' : 'black' }}>
            {props.user.name}
        </li>
    )
}

 

 

위 코드는 커스텀 훅을 적용하기 전과 동일하게 작동한다.

 

동작에 변경이 없고, 중복되는 로직만을 추출하여 커스텀 훅으로 만든 것이기 때문이다.

 

커스텀 훅은 리액트 기능이 아닌, 훅의 디자인에서 자연스럽게 따르는 규칙이다.

 

 

8-4) 커스텀 훅의 이름은 꼭 use로 시작해야하나?

 

이것은 중요한 약속이기 때문에 반드시 따라야한다.

 

만약 이름이 use로 시작하지 않는다면, 특정 함수의 내부에서 훅을 호출하는지 알 수 없어서, 훅의 규칙 위반 여부를 자동으로 확인할 수 없다.

 

또한 같은 커스텀 훅을 사용하는 2개의 컴포넌트는 state를 공유하는 것인가?

 

그렇지는 않다.

 

커스텀 훅은 단순히 state와 연관된 로직을 재사용이 가능하게 만든 것이다.

 

따라서 여러개의 컴포넌트에서 하나의 커스텀 훅을 사용할 때에 컴포넌트 내부에 있는 모든 state와 effects는 전부 분리되어 있다.

 

그것이 어떻게 가능한가?

 

각각의 커스텀 훅 호출에 대해, 분리된 state를 얻게 된다.

 

위 코드에서 useUserStatus() 훅을 직접 호출하는 것처럼 리액트의 관점에서는 컴포넌트에서 useState()와 useEffect() 훅을 호출하는 것과 동일한 것이다.

 

또한 하나의 컴포넌트에서 useState()와 useEffect() 훅을 여러 번 호출할 수 있는 것처럼, 각 커스텀 훅의 호출 또한 완전히 독립적이다.

 

---------------------------------------------------------------------------------------------------------------

 

훅들 사이에 데이터를 공유하고 싶다면 어떻게 해야할까?

 

const userList = [
    {id:1, name:'daehyuck'},
    {id:2, name:'taeyeon'},
    {id:3, name:'suzy'}
]

function ChatUserSelector(props) {

    const [userId, setUserId] = useState(1)
    const isUserOnline = useUserStatus(userId)

    return (
        <>
        <Circle color={isUserOnline ? 'green':'red'}/>
        <select
        value={userId}
        onChange={event => setUserId(Number(event.target.value))}
        >
            {userList.map(user => {
                <option key={user.id} value={user.id}>
                    {user.name}
                </option>
            })}
        </select>
        </>
    )
}

 

위 코드에서 ChatUserSelector라는 컴포넌트가 나온다.

 

이 컴포넌트는 <select> 태그를 통해 목록에서 사용자를 선택할 수 있게 해주고 있으며, 

 

사용자를 선택할 경우 해당 사용자가 온라인인지 아닌지를 보여주게 된다.

 

여기서 주목할 부분은 다음과 같다.

 

const [userId, setUserId] = useState(1)
const isUserOnline = useUserStatus(userId);

 

이 코드를 자세히 보면, useState() 훅을 사용해서 userId라는 state를 만들었다.

 

현재 선택된 사용자의 아이디를 저장하기 위한 용도이다.

 

이 userId는 바로 다음에 나오는 useUserStatus 훅의 파라미터로 들어가게 된다.

 

이렇게 하면, setUserId함수로 userId가 변경될 때마다,

 

useUserStatus 훅은 이전에 선택된 사용자를 구독 취소하고 새로 선택된 사용자의 온라인 여부를 구독하게 된다.

 

훅들 사이에서는 이런 방법으로 데이터를 공유하게 된다.

 

 

9. 실습하면서 따라해보기

 

$npx create-react-app my-app으로 react app 생성

 

src 폴더 내에 chapter_07 폴더 생성

 

useCounter.jsx라는 파일을 만들고, useCounter라는 이름의 리액트 훅을 만든다

 

import React,{useState} from 'react';

function useCounter(initialValue) {
    const [count,setCount] = useState(initialValue)

    const increaseCount = () => setCount((count) => count+1)
    const decreaseCount = () => setCount((count) => Math.max(count - 1,0))

    return [count, increaseCount, decreaseCount]
}

export default useCounter;

 

초기에 initialValue를 파라미터로 받아 count라는 이름의 state를 생성하고,

 

카운트 증가, 감소를 편리하게 할 수 있도록 함수를 제공한다

 

useCounter() 훅으로 어떤 함수 컴포넌트에서든지 카운트 기능을 쉽게 사용할 수 있다.

 

다음은 Accommodate.jsx라는 이름의 파일을 만들고 Accommodate라는 이름의 리액트 함수 컴포넌트 작성

 

import React, {useState, useEffect} from "react";
import useCounter from './useCounter';

const MAX_CAPACITY = 10

function Accommodate(props) {
    const [isFull, setIsFull] = useState(false)
    const [count, increaseCount, decreaseCount] = useCounter(0)

    useEffect(() => {
        console.log("----------------------")
        console.log("useEffect() is called.")
        console.log(`isFull: ${isFull}`)
    })

    useEffect(() => {
        setIsFull(count >= MAX_CAPACITY)
        console.log(`Current count value: ${count}`)
    }, [count])

    return (
        <div style={{ padding: 16}}>
            <p>{`총 ${count}명 수용했습니다.`}</p>

            <button onClick={increaseCount} disabled={isFull}>
                입장
            </button>
            <button onClick={decreaseCount}>퇴장</button>

            {isFull && <p style={{color:"red"}}>정원이 가득찼습니다.</p>}
        </div>
    )
}

export default Accommodate;

 

Accommodate 컴포넌트는 앞에서 만든 useCounter() 훅을 사용해서 카운트를 관리한다

 

최대 카운트 수는 MAX_CAPACITY라는 이름의 상수로 정의되어 있다.

 

카운트 개수가 최대 용량을 초과하면, 경고 문구가 표시되어 더 이상 입장이 불가능해진다.

 

여기서 useEffect() 훅의 작동 방식을 확인하기 위해 일부러 2개의 useEffect() 훅을 사용했다.

 

하나는 의존성 배열이 없고, 다른 하나는 있다.

 

의존성 배열이 없는 훅은 컴포넌트가 마운트된 직후에 호출, 이후 컴포넌트가 업데이트 될때마다 호출된다.

 

의존성 배열이 있으면, 컴포넌트가 마운트된 직후에 호출되고, 이후 count값이 바뀔때마다 호출되며

 

이때 용량이 가득 찼는지, 아닌지의 상태를 isFull이라는 state에 저장한다.

 

이제 만든 컴포넌트를 실제 화면에 렌더링하기 위해 index.js 파일을 수정하자

 

import Accommodate from './chapter_07/Accommodate'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Accommodate />
  </React.StrictMode>
);

 

이후 npm start로 실행

 

 

2개의 useEffect() 훅이 호출되었다.

 

입장버튼을 눌러보면

 

 

의존성배열이 없는 useEffect() 훅(1)이 호출된 이유는 컴포넌트가 업데이트되어서

 

의존성배열이 있는 useEffect() 훅(2)이 호출된 이유는 count값이 변경되어서

 

이번엔 정원이 가득찰때까지 눌러보면..

 

 

정원이 가득차면 isFull값이 true가 되면서 입장 버튼이 비활성화되어 더 이상 누를 수 없고,

 

빨간 글씨로 경고 문구가 출력된다.

 

로그를 보면 카운트 값이 10이 된 후에는 더 이상 변하지 않으므로, count를 의존성 배열로 갖는 useEffect() 훅은 호출되지 않는다.

 

이제 퇴장 버튼을 눌러 수용인원을 줄여본다

 

useEffect() 훅이 호출되면서 카운트 값이 줄어든다.

 

0명이 될때까지 눌러보면..

 

 

useCounter() 훅에서 Math.max() 함수를 사용해서 카운트 값이 0 아래로 내려갈 수 없게 만들어 놔서 값이 0이 되면 더 이상 useEffect() 훅도 호출되지 않는다.

TAGS.

Comments