[React Hook] 리액트 훅

김관응·2023년 3월 31일
1

React

목록 보기
7/12

Hooks는 리액트 16.8 버전에 신규로 추가된 기능이다. 클래스형 컴포넌트에서만 가능했던 state 및 다양한 기능을 코드작성 없이 라이브러리로 제공한다.

리액트에서는 클래스형 컴포넌트가아닌 함수형 컴포넌트에 훅을 사용하는것을 권장한다.

훅의 간단한 규칙이 있다.

  • 최상위 컴포넌트에서만 호출 : 반복문 혹은 중첩된 함수 안에서 호출을 하게되면 버그가 발생할 수 있어 최상위 한개에서만 호출하는것을 권장한다. 이 규칙을 따르면 컴포넌트가 랜더링되는 순서대로 훅이 호출되는것을 보장한다.
  • 리액트 함수 내에서만 호출 : 소스코드를 명확하게 하기 위해 일반 자바스크립트에서는 호출하지 않는다.

1. useState

useState는 가장 기본적인 훅이다. 함수형 컴포넌트에서 상태를 관리해야할 일이 있으면 useState를 활용하면 된다.

Counter.js (useState 예제)

 import React, { useState } from 'react';

 const Counter = () => {
     const [value, setValue] = useState(0);
     return (
         <div>
             <p>
                 현재 카운터 값은 <b>{value}</b> 입니다.
             </p>
             <button onClick={() => setValue(value + 1)}>+1</button>
             <button onClick={() => setValue(value - 1)}>-1</button>
         </div>
     );
 };

 export default Counter;

const [value, setValue] = useState(0);

위 코드의 뜻은 value라는 상태값에 useState를 통해 0으로 초기화를 시켰고, setValue라는 함수로 해당 상태를 변경한다는 의미이다.

+1을 누르면 setValue를 통해 현재 value + 1 으로 상태를 변경하고, -1을 누르면 value - 1 로 상태를 변경하는것을 볼 수 있다.

useState는 한개의 상태값만 관리하기 때문에 여러개를 관리하려면 여러개의 useState를 사용해야 한다.

2. useEffect

useEffect는 컴포넌트가 랜더링이 될 때 수행되는 훅이다.

Counter.js (useEffect 예제)

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

const Counter = () => {
    const [value, setValue] = useState(0);

    useEffect(() => {
        console.log('랜더링이 완료되었습니다!');
    }, []);

    return (
        <div>
            <p>
                현재 카운터 값은 <b>{value}</b> 입니다.
            </p>
            <button onClick={() => setValue(value + 1)}>+1</button>
            <button onClick={() => setValue(value - 1)}>-1</button>
        </div>
    );
};

export default Counter;

이전 Counter.js에 useEffect를 추가하고 개발자도구를 확인하면 아래와 같다.

처음에 0이 두번 찍히고, 그 이후에 값이 변경될 때 마다 찍히게 된다.

(여기서 만약 초기에 useEffect가 한번씩 더 실행된다면 index.jsdp React.StrictMode 태그를 지우자.
해당 태그의 역할은 추후 포스팅 예정이다.)

만약 useEffect를 쓰면서 특정값이 업데이트될 때 만 하고싶으면 아래와 같이 진행하면 된다.

useEffect deps

useEffect(() => {
        console.log('값 변경 : ' + value);
    }, [value]);

보면 랜더링 완료는 한번만 호출이 되고 값 변경은 value값이 변경될 때 마다 호출이 된다.

deps 부분은 의존성 배열로써, 해당 배열에 있는 값이 변경되면 호출이 되게 된다.

첫 번째 useEffect 처럼 deps에 빈 배열을 넣어 주게 되면 특정 값이 아닌 전체 컴포넌트에 대해 적용이 되게 되어 최초 1회만 호출이 되게 된다.

useEffect 클린업 함수

클린업 함수를 통해 초기화와 같은 전처리를 진행할 수 있다.
(예를 들면 신규 리스너에 연결 전 이전 리스너를 제거하는 등..)

useEffect(() => {
        console.log('값 변경 : ' + value);
        return() => {
            console.log('클린업 : ' + value);
        }
    }, [value]);

로그를 보면 값이 변경되기 직전에 클린업 함수가 실행되는것을 볼 수 있다.

3. useReducer

useContext는 useState보다 다양한 상황에서 state를 업데이트를 할 때 사용한다.

기존 Counter를 reducer를 이용해 만들어보자.

counter.js (useReducer 예제)

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

function reducer(state, action) {
    // action.type 에 따라 다른 작업 수행
    switch (action.type) {
        case 'INCREMENT':
            return { value: state.value + 1 };
        case 'DECREMENT':
            return { value: state.value - 1 };
        default:
            // 아무것도 해당되지 않을 때 기존 상태 반환
            return state;
    }
}
const Counter = () => {
    const [state, dispatch] = useReducer(reducer, { value: 0 });

    useEffect(() => {
        console.log('랜더링이 완료되었습니다!');
    }, []);

    useEffect(() => {
        console.log('값 변경 : ' + state.value);
        return() => {
            console.log('클린업 : ' + state.value);
        }
    }, [state.value]);

    return (
        <div>
            <p>
                현재 카운터 값은 <b>{state.value}</b> 입니다.
            </p>
            <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
            <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
        </div>
    );
};

export default Counter;

const [state, dispatch] = useReducer(reducer, { value: 0 });

state, dispate의 용도는 useState와 동일하다. 하지만 useReducer의 파라미터는 두개가 되는데, 첫 번째는 호출 시 실행하게되는 함수이고, 두 번째는 초기화 값이다.

아래 온클릭에서 dispatch안에 파라미터로 type: 'INCREMENT'를 넘겨줬고, 그럼 가전에 정의한 reducer라는 함수가 실행되고 해당 함수의 return이 state가 된다.

counter.js (useReducer 예제2)

관리해야할 state가 2개 이상이거나 엘리멘트의 value값을 사용하는 경우는 아래와 같이 작성할 수 있다.

import React, { useReducer } from 'react';

function reducer(state, action) {
    return {
        ...state,
        [action.name]: action.value
    };
}

const Info = () => {
    const [state, dispatch] = useReducer(reducer, {
        name: '',
        nickname: ''
    });
    const { name, nickname } = state;
    const onChange = e => {
        dispatch(e.target);
    };

    return (
        <div>
            <div>
                <input name="name" value={name} onChange={onChange} />
                <input name="nickname" value={nickname} onChange={onChange} />
            </div>
            <div>
                <div>
                    <b>이름:</b> {name}
                </div>
                <div>
                    <b>닉네임: </b>
                    {nickname}
                </div>
            </div>
        </div>
    );
};

export default Info;

state 안에 값이 name과 nickname으로 두개이며, input 엘리멘트 안에 name이 키가되고 value가 값이되어 onChange함수에에서 dispatch를 실행하고 reducer 함수에서는 입력맏은 엘리맨트의 name을 키로, value를 값으로 셋팅해 리턴 해준다.

4. useContext

useContext는 컨텍스트를 보다 쉽게 관리해줄 수 있게 해준다.

ContextSample.js (useContext 예제)

 import React, { createContext, useContext } from 'react';

 const ThemeContext = createContext('black');
 const ContextSample = () => {
     const theme = useContext(ThemeContext);
     const style = {
         width: '24px',
         height: '24px',
         background: theme
     };
     return <div style={style} />;
 };

 export default ContextSample;


검정색 사각형이 나타난것을 볼 수 있다.

5. useMemo

useMemo는 컴포넌트 내부에서 발생하는 불필요한 연산을 최적화할 수 있다.

아래와 같이 소스코드를 작성한다.

Average.js (useMemo 사용 전)

 import React, { useState } from 'react';

 const getAverage = numbers => {
     console.log('평균값 계산중..');
     if (numbers.length === 0) return 0;
     const sum = numbers.reduce((a, b) => a + b);
     return sum / numbers.length;
 };

 const Average = () => {
     const [list, setList] = useState([]);
     const [number, setNumber] = useState('');

     const onChange = e => {
         setNumber(e.target.value);
     };
     const onInsert = e => {
             const nextList = list.concat(parseInt(number));
             setList(nextList);
             setNumber('');
     );

     return (
         <div>
             <input value={number} onChange={onChange} />
             <button onClick={onInsert}>등록</button>
             <ul>
                 {list.map((value, index) => (
                     <li key={index}>{value}</li>
                 ))}
             </ul>
             <div>
                 <b>평균 값:</b> {getAverage(list)}
             </div>
         </div>
     );
 };

 export default Average;

해당 컴포넌트를 실행하고, input에 입력을 해보자.

로그를 보면 button의 onClick이 발생하지 않아도 input값의 변경으로 인해 getAverage가 일어나게 된다.

getAverage가 값들이 들어있는 list가 변경될 때 마다만 호출하려면 useMemo를 사용하면 된다.

Average.js (useMemo 사용 후)

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

 const getAverage = numbers => {
     console.log('평균값 계산중..');
     if (numbers.length === 0) return 0;
     const sum = numbers.reduce((a, b) => a + b);
     return sum / numbers.length;
 };

 const Average = () => {
     const [list, setList] = useState([]);
     const [number, setNumber] = useState('');

     const onChange = e => {
         setNumber(e.target.value);
     ;
     const onInsert = e => {
             const nextList = list.concat(parseInt(number));
             setList(nextList);
             setNumber('');
     );

     const avg = useMemo(() => getAverage(list), [list]);

     return (
         <div>
             <input value={number} onChange={onChange} />
             <button onClick={onInsert}>등록</button>
             <ul>
                 {list.map((value, index) => (
                     <li key={index}>{value}</li>
                 ))}
             </ul>
             <div>
                 <b>평균 값:</b> {avg}
             </div>
         </div>
     );
 };

 export default Average;

위와같이 변경 하면 button의 onClick 이벤트로 인해 list가 수정될 때만 getAverage함수가 호출되는것을 볼 수 있다.

6. useCallback

useCallback은 useMemo와 유사하다.

이전에 만든 Average 컴포넌트를 보면 컴포넌트가 리랜더링 될 때 마다 onChange와 onInsert라는 함수가 매번 새로 생성된다.

랜더링이 자주 일어나는 화면에서는 위와같이 하면 성능 이슈가 발생할 수 있다.

두 번째 파라미터는 배열이 들어가며, useEffect와 같이 빈 배열이면 최초 랜더링 될 때만 발생한다.

  • useEffect : 값이 변경되면 실행
  • useCallback : 값이 변경 되고 함수가 이전과 다르면 리랜더링
  • useMemo : 값이 변경되고 함수 리턴이 이전과 다르면 리랜더링

inChange와 onInsert를 useCallback으로 만들면 아래와 같다.

Average.js (useCallback 예제)

 import React, { useState, useMemo, useCallback } from 'react';

 const getAverage = numbers => {
     console.log('평균값 계산중..');
     if (numbers.length === 0) return 0;
     const sum = numbers.reduce((a, b) => a + b);
     return sum / numbers.length;
 };

 const Average = () => {
     const [list, setList] = useState([]);
     const [number, setNumber] = useState('');

     const onChange = useCallback(e => {
         setNumber(e.target.value);
     }, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성
     const onInsert = useCallback(
         e => {
             const nextList = list.concat(parseInt(number));
             setList(nextList);
             setNumber('');
         },
         [number, list]
     ); // number 혹은 list 가 바뀌었을 때만 함수 생성

     const avg = useMemo(() => getAverage(list), [list]);

     return (
         <div>
             <input value={number} onChange={onChange} />
             <button onClick={onInsert}>등록</button>
             <ul>
                 {list.map((value, index) => (
                     <li key={index}>{value}</li>
                 ))}
             </ul>
             <div>
                 <b>평균 값:</b> {avg}
             </div>
         </div>
     );
 };

 export default Average;

7. useRef

useRef는 useState와 비슷하게 상태를 관리하기 위한 함수이다.

useState와 다른점은 랜더링이 상대적으로 덜 발생한다는 장점이 있다. 하지만 동적으로 화면을 계속 랜더링 해야하는 경우는 useState를 사용해야 한다.

아래 예를 확인해보자.

SignUp.js (useRef 사용 전)

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

 function SignUp() {
     const [mail, setMail] = useState('');
     const [result, setResult] = useState('');

     console.log('render');

     return (
         <div>
             <span>이메일 주소</span><br/>
             <input value={mail} onChange={(e) => setMail(e.target.value)} /><br/>
             <span>결과: {result}</span><br/>
             <button onClick={() => setResult(mail)}>회원가입</button>
         </div>
     );
 }
 export default SignUp;

처음 화면이 랜더링되면서 한번, 그리고 1234를 입력하면서4번, 회원가입 버튼을 누르면서 1번 렌더가 됬다.

해당 경우를 useRef를 사용하면 다음과 같다.

SignUp.js (useRef 사용 후)

import React, {useState, useRef, useCallback} from 'react';

function SignUp() {
    const mail = useRef('');
    const [result, setResult] = useState('');

    console.log('render');

    const onClick = useCallback(() => {
        setResult(mail.current.value);
        mail.current.focus();
    }, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성

    return (
        <div>
            <span>이메일 주소</span><br/>
            <input ref={mail} /><br/>
            <span>결과: {result}</span><br/>
            <button onClick={onClick}>회원가입</button>
        </div>
    );
}
export default SignUp;

useRef를 사용하면 render가 최초 한번, 회원가입 버튼을 누를 때 한번 나오는것을 볼 수 있다.

current.focus() 메소드를 사용하면 해당 state로 커서를 옮길 수 있다.

profile
엔지니어였던 개발자

0개의 댓글