본문 바로가기
Fronted

[React] state와 리액트 훅

by 감자b 2024. 12. 30.

State

컴포넌트가 현재 가지고 있는 상태로 변화할 수 있는 동적인 값으로 state 값이 변경되었을 때 해당 컴포넌트와 자식 컴포넌트가 리렌더링된다.

참고로 리렌더링이 되는 경우는 3가지로 나눌 수 있다.

  1. 자신이 관리하는 state 값이 변경될 때
  2. 제공받는 props의 값이 변경될 때
  3. 부모 컴포넌트가 리렌더링되면 자식컴포넌트도 리렌더링

State는 컴포넌트 내에서 정의되며, useState 훅을 사용하여 설정 가능

import { useState } from 'react';

const Test = () => {
        // count는 state, setCount는 상태를 업데이트하는 함수
    const [count, setCount] = useState(0);  //useState 함수 인자에는 상태의 초기값

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>+1</button>
        </div>
    );
};

export default Test;

setState() 주의사항

  • 비동기적으로 작동
  • state를 직접 접근해서 바꿀 시 React가 변경을 감지하지 못하게 되어 UI가 동적으로 업데이트 되지 않으므로 setState()를 통해 상태를 변화시켜야 함.
  • 배열, 객체 state 변경 시 새로운 배열, 객체를 생성해서 업데이트해야함.
    • 배열 : map, filter 사용을 통해 새로운 베열 반환
    • 객체 : … 연산자를 통해 새로운 객체를 생성

리액트 훅

위에서 useState()와 같이 클래스 컴포넌트의 기능을 함수 컴포넌트에서도 이용할 수 있도록 도와주는 메서드를 리액트 훅이라고 한다.

  • use라는 접두사가 붙음
  • 함수 컴포넌트 내부에서만 호출될 수 있음
  • 조건문, 반복문 내부에서는 호출이 불가능
  • 커스텀 훅 직접 정의 가능

useRef

컴포넌트 내부에 새로운 reference 객체를 생성하는 기능으로 DOM 요소에 접근하거나, useState와 유사하지만 리렌더링을 하지 않을 때 유용하다.

import { useState, useRef } from 'react';

const Test = () => {
    const inputRef = useRef();
    const [input, setInput] = useState("");

    const handleFocus = () => {
        if(!input) {
            console.log("focus 호출");
            inputRef.current.focus();
            return;
        }
    };

    const onChange = (e) => {
        setInput(e.target.value);
    }

    return (
        <div>
            <input 
                ref={inputRef}  //inputRefinput 태그가 저장
                type="text" 
                placeholder="이름을 입력하세요" 
                onChange={onChange}
            />
            <button onClick={handleFocus}>제출</button>
            <h3>{input}</h3>
        </div>
    );
};

export default Test;

useEffect

컴포넌트의 사이드 이펙트를 제어하는 리액트 훅으로 두 개의 인자를 받는다.

  • 콜백 함수: 수행하고자 하는 작업을 정의한 함수.
  • 의존성 배열(deps): 실행될 조건을 정의하는 배열로 해당 배열에 포함된 값이 변경될 때만 콜백 함수가 실행

해당 훅의 사용법을 보기 전에 리액트의 컴포넌트 라이프사이클에 대해서 알아보도록 하자.

  1. Mount : 컴포넌트의 탄생. 화면에 처음 렌더링 되는 순간
  2. Update : 컴포넌트가 다시 렌더링 되는 순간(리렌더링)
  3. UnMount : 컴포넌트가 화면에서 사라지는 순간, 렌더링에서 제외되는 순간

useEffect()에서 두 번째 인자를 어떻게 전달하느냐에 따라서 컴포넌트의 생명주기와 관련된 로직을 작성할 수 있다.

  1. 마운트(빈 배열 [] 전달 )
useEffect(() => {
    console.log('Component mounted');
}, []);

이렇게 되면 배열에 값이 없으므로 컴포넌트가 처음 마운트될 때만 실행된다.

  1. 업데이트(배열에 변수(props, state) 전달)
useEffect(() => {
    console.log(`${count}`);
}, [count]);
// count가 바뀔 때 effect를 실행
  1. 언마운트 (클린업 함수)
    • useEffect 안의 첫 번째 인자인 콜백함수 내 return 함수는 해당 컴포넌트가 제거될 때 실행되며, 클린업 함수라고 한다.
useEffect(() => {
    return () => {
        console.log('unmount');
    }    
}, []);

 

useReducer

컴포넌트 내부에 새로운 state를 생성하는 리액트 훅으로 모든 useState는 useReducer로 대체 가능하다.

useState()와 차이점은 컴포넌트가 UI를 렌더링하는데 집중할 수 있도록 상태 관리 코드를 컴포넌트 외부로 분리하여 구조화된 방식으로 상태 관리를 할 수 있도록 한다.

const [state, dispatch] = useReducer(reducer, initialState);
  • reducer : 현재 상태와 액션을 인자로 받아 새로운 상태를 반환하는 함수
  • initialState: 리듀서의 초기 상태 값
  • state : 상태
  • dispatch : 상태 변경 시 필요한 정보를 전달하는 함수
import React, { useReducer } from 'react';

// 초기 상태
const initialState = { count: 0 };

// 리듀서 함수
const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        case 'reset':
            return initialState; // 초기 상태로 리셋
        default:
            return state;
    }
};

const Counter = () => {
    const [state, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <p>Count: {state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
            <button onClick={() => dispatch({ type: 'reset' })}>초기화</button>
        </div>
    );
};

export default Counter;

 

useMemo, React.memo

useMemo(메모이제이션할 콜백 함수, 의존성 배열)

메모이제이션 기법을 기반으로 불필요한 연산을 최적화하는 리액트 훅으로 주어진 값(의존성 배열 내부 값)이 변경되지 않는 한, 이전에 계산된 값을 재사용한다.

const Test = () => {
    const [filter, setFilter] = useState('');
    const items = ['apple', 'banana', 'cherry', 'date', 'fig', 'grape'];

    const filteredItems = useMemo(() => {
        console.log('Filtering items...');
        return items.filter(item => item.includes(filter));
    }, [filter]); // filter가 변경될 때만 호출

    return (
        <div>
            <input
                type="text"
                value={filter}
                onChange={(e) => setFilter(e.target.value)}
                placeholder="Filter items..."
            />
            <ul>
                {filteredItems.map((item, index) => (
                    <li key={index}>{item}</li>
                ))}
            </ul>
        </div>
    );
};

export default Test;

이렇게 하면 컴포넌트가 리렌더링될 때마다 연산이 수행되지 않으며, 의존성 배열의 값이 변경되지 않으면 이전에 계산된 값을 재사용하여 최적화가 가능하다.

 

React.memo(컴포넌트, 커스텀비교함수)

함수형 컴포넌트를 인수로 받아, 최적화된 컴포넌트로 만들어 반환하는 것으로 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트가 불필요하게 리렌더링되지 않도록 한다.

import { memo } from 'react';

const Test = (props) => {
    // 컴포넌트 내용
};

export default memo(Test);

여기서 넘어온 props가 변경되지 않는다면 해당 컴포넌트는 리렌더링 되지않는다.

  • React.memo는 기본적으로 props의 얕은 비교, 즉 props가 동일한 참조라면 컴포넌트를 리렌더링 X
  • 따라서 부모 컴포넌트가 상태나 props를 변경하여 리렌더링될 때, 자식 컴포넌트의 props가 동일하다면 자식 컴포넌트는 리렌더링을 하지 않는다.

만약 자식 컴포넌트의 props에서 함수를 받는다면 이러한 함수는 주소값으로 저장이 된다. 부모 컴포넌트에서 리렌더링될 때 컴포넌트가 호출되는데 여기서 props로 넘겨주는 함수가 재생성되어 새로운 주소값을 가지지 않도록 주의해야 한다.

 

해결 방법으로는 useCallback() 리액트 훅 사용하거나 커스텀 비교 함수를 정의해서 비교한다.

import { memo } from 'react';

const Test = ({count, name, onCreate}) => {
    // 컴포넌트 내용
};

export default memo(Test,() => (prevProps, nextProps) => {
        // count가 변경되지 않거나 name이 동일한 경우 재렌더링 하지 않음
        return prevProps.count === nextProps.count && prevProps.name === nextProps.name;
    }
);

 

useCallback

함수를 메모이제이션하여 리렌더링될 때 함수가 재정의 되지않도록 한다.

의존성 배열에 명시된 값이 변경될 때만 새로운 함수가 생성되며, 그 외에는 이전에 생성된 함수를 재사용하며 이는 자식 컴포넌트에 함수를 props로 전달할 때 유용하다.

  • 메모이제이션할 함수
  • 의존성 배열 : 이 배열의 값이 변경될 때만 새로운 함수가 생성
import { useState, useCallback } from 'react';
import Child from './Child';

const Parent = () => {
    const [count, setCount] = useState(0);

    const increment = useCallback(() => {
        setCount(count => count + 1);
    }, []); // 의존성 배열이 비어 있으므로, 최초 렌더링 시에만 생성

    return (
        <div>
            <h1>Count: {count}</h1>
            <Child increment={increment} />
        </div>
    );
};

export default Parent;
import { memo } from "react";

const Child = ({increment}) => {
    return (
        <button onClick={increment}>+1</button>
    );
}

export default memo(Child);

이렇게 하면 increment 함수는 컴포넌트가 마운트 될 때 메모이제이션되고 부모 컴포넌트가 리렌더링될 때 마다 함수(객체)가 생성되는 것이 아니라 재사용하므로 자식에서 받는 props에 영향을 미치지 않는다.


Context

props 방식은 부모에서 자식으로만 값을 전달할 수 있다는 특징이 있었다.

이는 만약 최상위 계층에서 3단계 밑에 있는 자식 컴포넌트에 값을 전달하고 싶다면 중간 컴포넌트들에게 모두 props를 전달해야하는 문제가 있다. (props drilling)

Context는 컴포넌트간의 데이터를 전달하는 또 다른 방법으로 컴포넌트 트리에서 전역적으로 데이터를 공유할 수 있게 하여 여러 컴포넌트 간에 데이터를 쉽게 전달할 수 있도록 한다.

import { createContext, useCallback, useState } from 'react';
import Child from './Child';

// Context 생성
export const MyStateContext = createContext();
export const MyDispatchContext = createContext();

const Parent = () => {
    const [count, setCount] = useState(0);

    const increment = useCallback(() => {
        setCount(c => c + 1); // 상태 업데이트
    }, []); // 의존성 배열이 비어 있으므로, 최초 렌더링 시에만 생성

    return (
        <MyStateContext.Provider value={{ name: "hbb", age: 20, count }}>
            <MyDispatchContext.Provider value= {increment}>
                 <Child />   
            </MyDispatchContext.Provider>
        </MyStateContext.Provider>
    );
}

export default Parent;
import { memo, useContext } from "react";
import { MyStateContext, MyDispatchContext } from "./Parent";

const Child = () => {
    const {name, age, count} = useContext(MyStateContext); //인수로 전달한 컨텍스트로부터 공급된 데이터를 반환
    const increment = useContext(MyDispatchContext);
    console.log(`${name} : ${age}`);
    console.log(`Count : ${count}`);
    return (
        <button onClick={increment}>+1</button>
    );
}

export default memo(Child);

컨텍스트의 생성은 컴포넌트의 외부에서 선언 (내부에 선언하면 리렌더링 될 때마다 컨텍스트를 생성하므로)

만약 하나의 컨텍스트에 값을 모두 넣는다면 변하지 않는 increment() 함수만 사용하는 컴포넌트에서도 name, age, count가 변하면 value={{ … }} 내부의 객체를 재생성하므로 리렌더링된다.

따라서 컨텍스트는 변하지 않는 값, 변경될 수 있는 값으로 분리하도록 하여 영향을 최소화하도록 한다.

'Fronted' 카테고리의 다른 글

[React] JSX, props  (0) 2024.12.30
[React] Critical Rendering Path, SPA  (0) 2024.12.30
[HTML, CSS, JS] 웹 개발 기초  (0) 2024.12.30