본문 바로가기

react

[소플] ch 7. 훅

1. 훅

 

훅(Hook)

 

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

- 리액트의 state와 생명주기 기능에 갈고리를 걸어 원하는 시점에 정해진 함수를 실행되도록 만든 것

- 훅의 이름은 모두 use로 시작 

 

 

2. useState

 

- state를 위한 훅 

- 함수 컴포넌트에서는 기본적으로 state라는 것을 제공하지 않기 때문에 클래스 컴포넌트처럼 state를 사용하고 싶으면 useState() 훅을 사용해야 함 

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

- 초깃값을 넣어 useState()를 호출하면 리턴 값으로 배열이 나옴

- 리턴된 배열의 첫 번째 항목은 state로 선언된 변수, 두 번째 항목은 해당 state의 set함수 

- 클래스 컴포넌트에서는 setState() 함수 하나를 사용해서 모든 state 값을 업데이트할 수 있었지만 useState()를 사용하는 방법에서는 변수 각각에 대해 set 함수가 따로 존재

 

ex)

 

Counter 컴포넌트 

 

 - 버튼을 클릭하면 카운트를 하나씩 증가시키고 현재 카운트를 보여줌

 - 카운트를 함수의 변수로 선언해서 사용하게 되면 버튼 클릭 시 카운트 값을 증가시킬 수 는 있지만, 

재렌더링(Re-rendering)이 일어나지 않아 새로운 카운트 값이 화면에 표시 되지 않음 

import React, { useState } from "react";

function Counter(props) {
    var count = 0;

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

 

useState()를 사용한 counter 컴포넌트 

 

 - useStae()를 사용하여 카운트 값을 state로 관리하도록 만든 것

 - 버튼이 눌렸을 때 setCount() 함수를 호출해서 카운트를 1 증가시킴

 - count의 값이 변경되면 컴포넌트가 재렌더링되면서 화면에 새로운 카운트 값이 표시됨 

 

import React, { useState } from "react";

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

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

export default Counter;

 

 

3. useEffect

 

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

- 리액트에서의 사이드 이펙트는 효과, 영향을 뜻하는 이펙트(effect)의미에 가까움 

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

 

- 클래스 컴포넌트에서 제공하는 생명주기 함수componentDidMount(), componentDidUpdate(), componentWillUnmount()와 동일한 기능을 하나로 통합해서 제공

 

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

- 의존성 배열은 이펙트가 의존하고 있는 배열인데, 배열 안에 있는 변수 중에 하나라도 값이 변경되었을 때 사용 

- 이펙트 함수는 처음 컴포넌트가 렌더링된 이후업데이트로 인핸 재렌더링 이후에 실행 

 

- 이펙트 함수가 마운트와 언마운트시에 단 한번씩만 실행되게 하고 싶으면, 의존성 배열에 빈 배열([ ])을 넣음

 -> 해당 이펙트가 props나 state에 있는 어떤 값에도 의존하지 않는 것이 되므로 여러번 실행되지 않음

 

- 의존성 배열을 생략하게 되면 컴포넌트가 업데이트될 때 마다 호출됨 

 

- useEffect() 혹은 하나의 컴포넌트에 여러 개를 사용할 수 있음

 

useEffect(()=> {
    //컴포넌트가 마운트된 이후,
    //의존성 배열에 있는 변수들 중 하나라도 값이 변경되었을 때 실행
    //의존성 배열에 빈 배열([])을 넣으면 마운트와 언마운트시에 단 한번씩만 실행됨
    //의존성 배열 생략 시 컴포넌트 업데이트 시마다 실행됨
    
    return () => {
    	//컴포넌트가 마운트 해제되기 전에 실행됨 
        ..
    }
}, [의존성 변수1, 의존성 변수2,...]);

 

ex)

 

- 의존성 배열 없이 useEffect()를 사용하면 리액트는 DOM이 변경된 이후에 해당 이펙트 함수를 실행하라는 의미로 받아들임

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

- 이펙트는 함수 컴포넌트 안에서 선언되기 때문에 해당 컴포넌트의 props와 state에 접근할 수도 있음 

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

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

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

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

export default Counter

 

ex)

 

- componentWillUnmount() 기능 구현 

- useEffect()에서 ServerAPI를 사용하여 사용자의 상태를 구독하고 있음

  이후 함수 하나를 리턴하는데 해당 함수 안에는 구독을 해지하는 API를 호출하도록 되어 있음

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

 

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

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

    function handleStatusChange(status) {
        setIsOnline(status.inOnline);
    }

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

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

export default UserStatus

 

 

4. useMemo 와 useCallback

 

메모이제이션

 

- 최적화를 위해 사용하는 개념

- 비용이 높은 함수의 호출 결과를 저장해 두었다가, 같은 입력값으로 함수를 호출하면 새로 함수를 호출하지 않고 이전에 저장해놨던 호출 결과를 바로 반환

- 메모이제이션이 된 결괏값을 Memoized value라고 함 

 

useMemo 

 

- Memoized value를 리턴하는 훅 

- 파라미터로 Memoized value를 생성하는 create 함수의존성 배열을 받음 

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

 그렇지 않은 경우에는, 기존 함수의 결괏값을 그대로 반환

 

const memoizedValue = useMemo (
    () => {
    	//연산량이 높은 작업을 수행하여 결과를 반환 
        return computeExpensiveValue(의존성 변수1, 의존성 변수2);
    },
    [의존성 변수1, 의존성 변수2]
);

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

  -> 렌더링이 일어나는 동안 실행돼서는 안될작업을 useMemo() 함수에 넣으면 안됨 

    (ex. 서버에서 데이터를 받아옴)

 

- 의존성 배열을 넣지 않을 경우 렌더링이 일어날 때마다 매번 함수가 실행됨 

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

- 의존성 배열에 빈 배열을 넣게 되면 컴포넌트 마운트 시에만 함수가 실행 

 

useCallback

 

- useMemo()와 유사한 역할을 하나 값이 아닌 함수를 반환 

- 함수와 의존성 배열을 파라미터로 받음

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

 

- useCallback() 훅을 사용하지 않고 컴포넌트 내에 함수를 정의한다면 매번 렌더링이 일어날 떄 마다 함수가 새로 정의됨

- useCallback() 훅을 사용하면 특정 변수의 값이 변한 경우에만 함수를 다시 정의하도록 해서 불필요한 반복 작업을 없애줌

 

const memoizedCallback = useCallback(
    () => {
    	doSomething(의존성 변수1, 의존성 변수2);
    },
    [의존성 변수1, 의존성 변수2]
);

 

 

5. useRef

 

- 변경 가능한 .current라는 속성을 가진 하나의 상자

- 레퍼런스(Reference)를 사용하기 위한 훅 

- 레퍼런스 = 특정 컴포넌트에 접근할 수 있는 객체 

- useRef 훅은 레퍼런스 객체(ref object)를 반환 

 

- 레퍼런스 객체의 .current 속성

  -> 현재 레퍼런스하고 있는 엘리먼트 

 

- 파라미터로 들어온 초깃값으로 초기화된 레퍼런스 객체를 반환 

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

const refContainer = useRef(초깃값);

 

ex) 

- useRef() 훅을 사용하여 버튼 클릭 시 <input>에 포커스를 하도록 함

- 초깃값으로 null을 넣었고 결과로 반환된 inputElem이라는 레퍼런스 객체를 <input>태그에 넣어줌 

- 버튼 클릭 시 호출되는 함수에서 inputElem.current를 통해 실제 엘리먼트에 접근하여 focus() 함수를 호출

import { useRef } from "react";

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>
        </>
    );
}

 

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

 

- useRef() 훅은 내부의 데이터가 변경되었을 때 별도로 알리지 않음 

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

 -> ref에 DOM node가 연결되거나 분리되었을 경우 어떤 코드를 실행하고 싶다면 callback ref를 사용해야 함

 

 

6. 훅의 규칙

 

1) 훅은 무조건 최상위 레벨(Top Level)에서만 호출해야 함 

 

 - 반복문이나 조건문 또는 중첩된 함수들 안에서 훅을 호출하면 안됨

 - 훅은 컴포넌트가 렌더링 될 떄마다 매번 같은 순서로 호출되야 함

 

2) 리액트 함수 컴포넌에서만 훅을 호출해야 함

 

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

- 훅은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 커스텀 훅(Custom Hook)에서만 호출 가능 

 

 

7. 커스텀 훅 

 

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

 

1) 커스텀 훅을 만들어야 하는 상황 

 

UserStatus 컴포넌트 

 - isOnline State에 따라 사용자의 상태가 온라인인지 아닌지를 텍스트로 보여주는 컴포넌트 

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

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

    function handleStatusChange(status) {
        setIsOnline(status.inOnline);
    }

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

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

export default UserStatus

 

UserListItem 컴포넌트 

 

- 연락처 목록을 제공

- 이때 온라인인 사용자의 이름은 초록색으로 표시

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

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

    function handleStatusChange(status) {
        setIsOnline(status.inOnline);
    }

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

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

export default UserListItem

- UserStatus와 useState(), useEffect() 훅을 사용하는 부분이 동일, 중복된 코드

 

2) 커스텀 훅 추출하기 

 

- 중복되는 로직을 useUserStatus() 커스텀 훅으로 추출

 

useUserStatus 훅

 

- 사용자의 온라인 / 오프라인 상태를 구독

- 해당 사용자가 온라인인지 오프라인인지의 상태를 리턴 

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

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

    function handleStatusChange(status) {
        setIsOnline(status.inOnline);
    }

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

    return isOnline;

}

export default useUserStatus

 

3) 커스텀 훅 사용하기 

- 커스텀 훅을 사용하여 코드 변경 

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>
    );
}

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

 -> 커스텀 훅을 사용하는 컴포넌트들은 state 를 공유하지 않음

 -> 여러 개의 컴포넌트에서 하나의 커스텀 훅을 사용할 떄에 컴포넌트 내부에 있는 모든 state와 effects는 전부 분리되어 있음

 

 

8. 실습 - 훅을 사용한 컴포넌트 개발 

 

useCounter 커스텀 훅 - 카운트 기능 훅
Accommodate 컴포넌트  - useCounter() 훅을 사용하는 사람 수용 관리 컴포넌트 
- 최대 수용 인원을 초과하면 경고 문구 표시 

 

useCounter.jsx

 

- 초기 카운트 값을 파라미터로 받아서 count라는 이름의 state를 생성하여 값을 제공하고 카운트 증가 및 감소를 편리하게 할 수 있도록 함수를 제공하는 훅 

import React from "react";
import { 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;

 

Accomodate.jsx

 

- useCounter() 훅을 사용하여 카운트를 관리

 

- 의존성 배열이 없는 useEffect() 훅

  -> 컴포넌트가 마운트된 직후에 호출되며 이후 컴포넌트가 업데이트 될때마다 호출

 

- 의존성 배열이 있는 useEffect() 훅

  -> 컴포넌트가 마운트된 직후에 호출되며, 이후 count 값이 바뀔때 마다 호출됨

  -> 용량이 가득 찼는지 아닌지의 상태를 isFull이라는 state에 저장

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

const MAX_CAPCITY = 10;

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

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

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

    return (
        <div style={{padding:100}}>
            <p>{`총 ${count} 명 수용했습니다.`}</p>
            
            <button onClick={increseCount} disabled={isFull}>
                입장
            </button>
            <button onClick = {decreaseCount}>퇴장</button>

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

- 두 개의 useEffect() 훅이 호출되고 카운트 값 증가 

- 의존성 배열이 없는 useEffect() 훅은 컴포넌트가 업데이트됐기 때문에 호출됨 

- 의존성 배열이 있는 useEffect() 훅은 count 값이 변경되었기 떄문에 호출됨 

 

'react' 카테고리의 다른 글

[소플] ch 9. 조건부 렌더링  (1) 2023.10.28
[소플] ch 8. 이벤트 핸들링  (0) 2023.10.28
[소플] ch 6. state와 생명 주기  (0) 2023.10.27
[소플] ch 5. 컴포넌트와 Props  (0) 2023.10.27
[소플] ch4. 엘리먼트 렌더링  (1) 2023.10.27