6 React | hooks

Choi jeongmin·2024년 10월 1일
0

React

목록 보기
6/8

Hooks 는 리액트에 새로 도입된 기능으로, 함수 컴포넌트에서 사용 불가능한
생명주기 메소드의 한계점으로 인해 상태 관리 및 렌더링 이후
시점 컨트롤 등 다양한 문제를 해결하기 위해 만든 함수 집합을 의미한다..

그 중 useState는 가장 기본적인 hook 이며, 함수 컴포넌트에서도
상태를 관리할 수 있게 해준다..

1. useEffect

컴포넌트가 렌더링 된 이후 특정 작업을 수행할 필요가 있다면 클래스형 컴포넌트에서는
componentDidMount 혹은 componenetDidUpdate 메소드를 이용하면 된다..
하지만, 함수형 컴포넌트 에서는 생명주기 사용이 불가능하다..
그렇기에 함수형 컴포넌트에서도 렌더링 된 이후 시점에 수행할 내용이 필요한 경우,
사용할 수 있는 기능을 제공하고 있고, useEffect 이다..

import { useEffect, useState } from "react"

const UseEffectMount = () => {
    const [time, setTime] = useState(new Date().toLocaleTimeString());

    useEffect(()=>{
        console.log("마운트 시점 동작");
    }, 
         // [] // 두번째 인자로 빈 배열을 넣으면 업데이트 시점에는 동작하지 않고 최초 마운트 시에만 동작한다.
        [time] // 대괄호 안의 값이 바뀔때마다 동작한다.
        
    ); 

    return(
        <>
            <button onClick={()=>setTime(new Date().toLocaleTimeString())}>
                현재 시간 확인
            </button>
            <h1>{time}</h1>
        </>
    )


}

export default UseEffectMount;

이전 Effect 정리

useEffect 는 기본적으로 렌더링 직후와 업데이트 직후 호출된다..
컴포넌트가 마운트 해제되기 직전이나 업데이트 되기 전에
실행할 내용이 있다면 정리 (clean-up) 을 할 수 있다..
이전 effect 내용을 정리하고 난 뒤 새로운 effect 가 동작하도록 할 때 사용한다..
이전 effect 가 남아있는 상태에서 새로운 effect 가 발생하게 되면
마운트 해제가 일어나고 나서도 메모리 누수나 충돌이 발생할 가능성이 있다..

import { useEffect, useState } from "react";


const Timer = () => {
    const [cont, setCont] = useState(new Date().toLocaleTimeString());
    useEffect(()=>{

        console.log("타이머가 시작됨..");

        const timer = setInterval(()=>{
            // console.log(new Date().toLocaleTimeString());
            setCont(new Date().toLocaleTimeString());
        }, (1000));

        return (
            ()=>{
                clearInterval(timer);
                console.log("타이머 끗");
            }
        ) // useEffect가 unMount될때 return이 동작함
    }, []);

    // return <h1>타이머를 시작합니다..</h1>
    return (
        <>
            <h1>타이머를 시작합니다..</h1>
            <h3>{cont}</h3>
        </>
    )
}

export const Container = () => {
    const [isTrue, setIsTrue] = useState(false);

    return (
        <>
            <button onClick={()=>{setIsTrue(!isTrue)}}>타이머 토글</button>
            {isTrue && <Timer/>}
        </>
    )
}

2. useReducer

import { useReducer, useState } from "react"

export const Counter = () => {

    const [counter, setCounter] = useState(0);

    // useState의 set은 자기자신의 현재 값을 콜백함수의 인자로 받아올 수 있다..
    const plus = () => {
        setCounter(r=>r+1);
    }
    const minus = () => {
        setCounter(r=>r-1);
    }

    return (
        <>
            <h1>counter : {counter}</h1>
            <button onClick={plus}>+1</button>
            <button onClick={minus}>-1</button>
        </>
    )
}


const reducer = (state, action) => { // (state: reducer에 넣어준 state, action: dispatch 인자)

    switch(action.type){
        case 'DECREMENT' :
            return {value: state.value-1};
        case 'INCREMENT' :
            return {value: state.value+1};
        default :
            return state;
    }
}

const UseReducerBasic = () => {
    const [state, dispatch] = useReducer(reducer, {value: 0});
    // [상태, 상태에 변화를 줄 함수] = useReducer(함수, state(객체)) ->순서가 바뀜

    return (
        <>
            <h1>count : {state.value}</h1>
            <button onClick={()=>dispatch({type: "DECREMENT"})}>-1</button>
            <button onClick={()=>dispatch({type: "INCREMENT"})}>+1</button>
        </>
    );
}

export default UseReducerBasic;

3. useMemo

메모이제이션(memoization)

  • 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때
    이전에 계산한 값을 메모리에 저장함으로써
    동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술..

useMemo

  • 특정 값이 변경될 때만 메모이제이션된 값을 재계산하여 성능 개선
    useMemo 의 두번째 인자로 전달된 값이 변경될 때만 연산 수행..
import { useEffect, useMemo, useState } from "react";

// useMemo : 메모이제이션을 통해 비용이 큰 계산을 최적화 하거나,
// 참조(주소값)이 동일해야 하는 경우에 사용한다.
// 의존성이 변경되지 않으면 이전의 계산된 값을 재사용 하여 불필요한 렌더링을 방지한다.

export const LocationComponent = () => {
    const [isKorea, setIsKorea] = useState(true);
    const [number, setNumber] = useState(0);

    console.log("렌더링");

    // 1. 지역 변수에 문자열로 초기화 - 기본 자료형(state 변화 시 location 변화 없음)
    // const location = isKorea? "한국":"일본";

    // 2. 지역 변수에 객체로 초기화 - 객체의 주소값이 렌더링 시 마다 변화..(재할당)
    /* const location = {
        country: isKorea? "한국" : "일본"
    } */

    // 3. 지역 변수에 useMemo 의 반환값으로 초기화
    const location = useMemo(()=>{
        return {
            country: isKorea?"한국":"일본"
        };
    }, [isKorea]);

    useEffect(()=>{
        console.log("useEffect 호출");
    }, [location]);

    return (
        <>
            <h2>지금 당신의 위치는?</h2>
            <p>국가: {location.country}</p>
            <button onClick={()=>setIsKorea(!isKorea)}>국가 변경</button>
            <hr/>
            <h2>좋아하는 숫자를 입력 해주세요</h2>
            <input type="number" value={number} onChange={e=>setNumber(e.target.value)} />
        </>
    )
}

4. useCallback

함수 자체를 자식에 props로 뿌려줄 때 사용
-> 부모에서 바뀌면 자식 모두에서 다시 렌더링되기때문에 useCallback으로 불필요한 렌더링을 막아줘야한다.

  • useMemo :
    특정 값을 메모이제이션한다..
    주로 연산이 비싼 값이나 복잡한 계산..
    사용 목적은 값이 자주 계산되는 것을 방지하여 성능 최적화..

  • useCallback :
    특정 함수를 메모이제이션한다.. 함수의 재생성을 방지한다..
    자식 컴포넌트에 함수를 전달할 때 불필요하게 함수가 재생성되는 것을 방지한다..

  • 단점 : useCallback 을 많은 함수에 남용하면 오히려 성능 저하가 발생할 수 있다..
    메모이제이션 자체도 메모리 비용이 들기 때문..
    함수의 재생성에 큰 비용이 없는 경우 굳이 사용할 필요는 없다..

import { useCallback, useEffect, useState } from "react"

export const CallbackComponent = () => {
    const [size, setSize] = useState(200);
    const [isDark, setIsDark] = useState(false);

    const genSquareStyle = useCallback(() => {
        return {
            backgroundColor : "orangered",
            width: size,
            height: size,
        }
    }, [size]);

    return (
        <>
            <div style={{backgroundColor:isDark?"black":"white"}}>
                <input type="range" min="100" max="300" onChange={e=>setSize(parseInt(e.target.value))}/>
                <button onClick={()=>setIsDark(!isDark)}>{isDark.toString()}</button>
                <Square genSquareStyle={genSquareStyle}/>
            </div>
        </>
    );
}

const Square = ({genSquareStyle}) => {

    const [style, setStyle] = useState({});

    useEffect(()=>{
        console.log("style 변경");
        // setStyle(genSquareStyle()); 둘다가능
        setStyle(genSquareStyle); 
    }, [genSquareStyle]);

    return <div style={style}></div>
}

5. useRef

useRef 를 사용하는 목적

  1. 값 유지 - 렌더링 사이에 값을 유지할 수 있게 해준다... 리렌더링 시 초기화되지 않는다..
  2. 렌더링 방지 - 값이 변경되더라도 리렌더링을 일으키지 않는다. 성능 최적화
  3. 애니메이션, 타이머 등 특정 값이 변경 되더라도 리렌더링을 원치 않을 때 저장 용도
  4. DOM 요소에 직접 접근하는 방법을 제공함. 특정 요소 포커스 등
import { useRef, useState } from "react";

export const UseRefCounter = () => {
    console.log("useRefCounter 렌더링 됨..");

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

    let variable = 0;

    const countRef = useRef(0);

    const increaseCount = () => { // 실행 시 재렌더링
        setCount(count+1);
    };

    const increaseVariable = () => { // 실행 시 콘솔만 찍히고 렌더링 안됨 / 렌더링 될 때마다 초기화
        variable += 1;
        console.log("variable : "+ variable);
    };

    const increaseCountRef = () => { // 실행 시 콘솔만 찍히고 렌더링 안됨 / 렌더링 다시 될 때 출력됨
        countRef.current = countRef.current+1;
        console.log("카운트Ref: " + countRef.current);
    };

    return (
        <>
            <h1>counter : {count}</h1>
            <h1>variable : {variable}</h1>
            <h1>countRef : {countRef.current}</h1>

            <button onClick={increaseCount}>카운트증가</button> 
            <button onClick={increaseVariable}>variable 증가</button>
            <button onClick={increaseCountRef}>카운트 Ref 증가</button>
        </>
    );
}

6. useContext

context는 react 컴포넌트 트리 안에서 전역적으로 데이터를 공유할 수 있도록 고안된 방법이다..
트리 구조가 복잡해질수록 하위 컴포넌트로 props를 전달하는 양이 많아지고, 그러면 유지보수와 코드 가독성에 악영향을 준다..

하지만 context 를 사용하면 중간 컴포넌트 들에게 props 를 넘겨주지 않아도 되고 유지보수도 수월해지게 된다..
단, context 를 사용하면 컴포넌트를 재사용하기 어려워지기 때문에 꼭 필요할 때 써야한다..
따라서 때에 따라서는 context 보다 props 가 더 간단한 해결책이 될 수 있다.

import { createContext, useContext, useState } from "react";
import { Styles } from "./style";

// 컨텍스트 생성 -> 별도의 저장소를 만듦..
const DarkModeContext = createContext(null);
// 전역적으로 공유하고 싶은 것 중 useState 가 아닌 것들은 createContext()에 넣어주면 된다. // 사용은 잘안함

const Header = () => {

    const context = useContext(DarkModeContext);
    const {isDark} = context;

    return(
        <header style={{
            ...Styles.header,
            backgroundColor:isDark?"black":"lightgray",
            color:isDark?"white":"black"
        }}>
            <h1>Welcome to useContext</h1>
        </header>
    );
}

const Content = () => {
    const context = useContext(DarkModeContext);
    const {isDark} = context;

    return(
        <div style={{
            ...Styles.content,
            backgroundColor:isDark?"gray":"white",
            color:isDark?"white":"black"
        }}>
            <p>내용입니다..</p>
        </div>
    );
}

const Footer = () => {
    const context = useContext(DarkModeContext);
    const {isDark, setIsDark} = context;
    const toggleHandler = () => setIsDark(!isDark);

    return(
        <footer style={{
            ...Styles.footer,
            backgroundColor:isDark?"black":"lightgray",
            color:isDark?"white":"black"
        }}>
            <button onClick={toggleHandler}>{isDark?"Light Mode":"Dark Mode"}</button>
            useContext App
        </footer>
    );
}

const Page = () => {
    return (
        <div style={{
            ...Styles.page
        }}>
            <Header/>
            <Content/>
            <Footer/>
        </div>
    )
}

const ContextContainer = () => {
    const [isDark, setIsDark] = useState(false);

    return (
        // context이름.Provider 컴포넌트 :  자식 요소들(<Page/>)이 사용할 수 있도록 value에 전달할 값들을 담아줌.
        <DarkModeContext.Provider value={{isDark, setIsDark}}> 
            <Page/>
        </DarkModeContext.Provider>
    )
}

export default ContextContainer;

useContext 의 단점..

  1. 성능 저하
    Context 는 전역 상태를 공유하기 때문에 공유된 상태가 변경되면
    그 상태를 구독하는 모든 컴포넌트가 리렌더링 된다.
    만약 Context 를 많이 사용하고 그 상태들이 자주 변경되면,
    불필요한 리 렌더링이 발생하여 성능이 저하될 수 있다..

  2. 컴포넌트 재사용성 감소
    Context 에 의존하는 컴포넌트가 많아질 수록, 해당 컴포넌트는 그 Context 환경 안에서만 동작할 수 있다..
    이런 컴포넌트는 독립적으로 재사용하기 어려워지며, 다른 부분에서 쓰기 어렵다..

  3. 구조적인 복잡성 증가
    Context 를 많이 사용하면 프로젝트 구조가 복잡해진다..
    여러 Context 가 중첩되면 코드의 가독성이 떨어지고 유지보수에 악영향을 끼친다..
    많은 컴포넌트가 여러 Context 에 의존하면, 프로젝트의 복잡도가 급격히 증가한다..

-> 관리 x 공유 o 관리는 useState에서 상태 관리를 하지 않기 때문에 단순한 것을 넘기는것은 괜찮지만 복잡한 것을 넘기는것은 어려움

7. customHooks

동일한 동작이 필요한 것들에 hooks를 만들어 공통으로 사용할 수 있다.

import { useState } from "react";

const useInput = () => {
    const [value, setValue] = useState("");

    const onChange = e => setValue(e.target.value);

    return {value, onChange};
}


export const CustomHooks = () => {
    const name = useInput();
    const pass = useInput();
    const email = useInput();

    return (
        <>
            <label htmlFor="">이름 : </label>
            <input type="text" value={name.value} onChange={name.onChange}/>
            <br/>
            <label htmlFor="">비밀번호 : </label>
            <input type="password" {...pass}/> 
            {/* 속성이랑 키 값이 같을 때 {...pass} 전개연산자로 사용가능 */}
            <br/>
            <label htmlFor="">이메일 : </label>
            <input type="email" {...email}/> 

            <h4>name : {name.value}</h4>
            <h4>pass : {pass.value}</h4>
            <h4>email : {email.value}</h4>
        </>
    )
}

0개의 댓글