11 분 소요

함수 컴포넌트 (Functional Component) : 리액트 훅을 통한 상태와 생명주기 관리

클래스 컴포넌트의 한계?

  • 데이터의 흐름을 추적하기 어려움 : 서로 다른 여러 메소드에서, 작성 순서에 상관없이 상태의 업데이트 발생 가능
  • 어플리케이션 내부 로직의 재사용이 어려움 : 공통 로직이 많아질수록 이를 감싸는 고차 컴포넌트나 props이 많아짐
  • 기능이 많아질수록 컴포넌트의 크기가 커짐 : 내부에서 처리하는 데이터 흐름이 복잡해짐
import { useState, useEffect } from 'react';

const MyFunctionalComponent = () => {
  // useState 훅을 사용하여 상태 정의
  const [count, setCount] = useState(0);

  // useEffect 훅을 사용하여 부수 효과(라이프사이클 작업 등) 처리
  useEffect(() => {
    console.log('Component mounted or count updated:', count);
    // componentWillUnmount 역할을 하는 함수 (클린업 함수)
    return () => {
      console.log('Component will unmount');
    };
  }, [count]); // count가 업데이트될 때만 실행

  const handleIncrement = () => {
    // setCount를 사용하여 상태 업데이트
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

export default MyFunctionalComponent;
  • useState, useEffect, useContext와 같은 리액트 훅을 사용하여 상태를 관리
  • useEffect 훅을 통해 라이프사이클 메소드와 유사한 동작을 수행 가능
  • this 키워드를 사용하지 않고, 클래스 컴포넌트보다 간결하고 명료하게 작성 가능

리액트 훅 (React Hook) : 함수형 컴포넌트에서 상태와 생명주기 메서드를 사용할 때 사용

  • 상태 관리 훅 : useState, userContext, useReducer
  • 시점 훅 : useLayoutEffect, useEffect
  • 메모이제이션 (Memo) 훅 : useMemo, useCallback, memo

훅의 규칙 (Rules-of-Hooks) : 관련 ESLint 규칙으로 react-hooks/rules-of-hooks 또한 존재

(1) 최상위에서만 훅을 호출해야 한다. (반복문, 조건문, 중첩 함수 내에서 훅을 실행할 수 없다.)
→ 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다.
(2) 훅을 호출할 수 있는 것은 함수 컴포넌트, 혹은 커스텀 훅만 가능하다.

useState : 함수형 컴포넌트 내부에서 상태를 정의하고 관리할 때 사용

import { useState } from 'react'
const [state, setState] = useState(initState)
  • 인수로 사용할 상태의 초기값을 받음
  • 첫번째 값에 현재 상태의 값, 두번째 값에 상태를 업데이트하는 함수가 담긴 배열을 반환
const [state, setState] = useState(() => Number.praseInt(window.localStorage.getItem(cacheKey)));
  • 게으른 초기화 (Lazy Initialization) : useState 등 리액트 훅의 인자에 변수 대신 함수 자체를 넘기는 것
    • useState의 초기값이 복잡하거나 무거운 연산을 포함하는 경우에 사용
    • 오직 상태가 처음 만들어질 때만 사용 → 리렌더링이 발생하면 이 함수의 실행은 무시
import { useState } from 'react';

const Component = () => {
  // useState를 사용하여 counter라는 상태를 정의하고 초기값을 0으로 설정
  const [counter, setCounter] = useState(0)

  const handleClick = () => {
    setCounter((prev) => prev + 1)
  }

  return (
    <>
      <h1>{counter}</h1>
      <button onClick={handleClick}>+</button>
    </>
  )
}

useEffect : 어플리케이션 내 컴포넌트의 여러 값들을 통해 부수 효과를 만들 때 사용

const Component = () => {
  useEffect(
    () => {},
    [props, state]
  )
}
  • 첫번째 인수로 콜백 함수, 두번째 인수로 의존성 배열을 받음 : 의존성 배열의 값이 변경되면 콜백 실행
    • componentDidMount 시점에 비동기로 실행 → rendering 이후, mount 이후
  • 클래스 컴포넌트의 생명주기와 비슷한 동작을 구현 가능 : 의존성 배열에 빈 배열을 넣으면 컴포넌트가 마운트될 때만 실행
  • 렌더링이 실행될 때마다 의존성에 있는 값을 보면서 값 (state, props)이 변경되면 부수 효과를 실행
const Component = () => {
  const [counter, setCounter] = useState(0)

  const handleClick = () => {
    setCounter((prev) => prev + 1)
  }

  const counter = 1

  useEffect(
    () => {
      console.log(count) // 1, 2, 3, 4...
    }
  )

  return (
    <>
      <h1>{counter}</h1>
      <button onClick={handleClick}>+</button>
    </>
  )
}
  • 클린업 함수를 반환 가능 : 컴포넌트가 언마운트될 때 실행 (componentWillUnMount)
    • 함수 컴포넌트가 리렌더링되었을 때 의존성 변화가 있었을 당시의 값 기준으로 실행되어 이전 상태를 청소
    • 함수 컴포넌트의 useEffect는 콜백이 실행될 때마다 이전 클린업 함수가 존재하면 그 클린업 함수를 실행한 뒤 콜백 실행
      • 특정 이벤트 핸돌러가 무한히 생성되는 것을 방지
import { useState, useEffect } from 'react'

export default function App() {
  const [counter, setCounter] = useState(0)

  const handleClick = () => {
    setCounter((prev) => prev + 1);
  }

  useEffect(() => {
    const addMouseEffect = () => {
      console.log(counter)
    }

    window.addEventListener('click', addMouseEffect);

    return () => {
      console.log('클린업 함수', counter);
      window.removeEventListener('click', addMouseEffect);
    }
  }, [counter]);

  return (
    <>
      <h1>{counter}</h1>
      <button onclick={handleClick}>+</button>
    </>
  )
}

// 클린업 함수 0
// 1
// 클린업 함수 1
// 2
// ...
  • 의존성 배열 (dependency arra) : 내부의 특정 값이 변경될 때에만 useEffect 콜백이 실행되도록 제어
    • 의존성 배열에 사용자가 원하는 값을 넣는 경우 : 해당 값이 변경될 따마다 실행
    • 의존성 배열에 빈 배열을 두는 경우 : 최초 렌더링 직후에 한번 실행하고 더 이상 실행하지 않음
    • 의존성 배열에 아무런 값을 두지 않는 경우 : 렌더링이 발생할 때마다 매번 실행
      • vs 직접 실행? : 클라이언트 사이드에서 실행 보장, 컴포넌트 렌더링 완료 이후에 실행
useEffect(() => {
  // 실행될 코드
}, [dependency1, dependency2, ...]); // 의존성 배열

useMemo : 큰 연산에 대한 결과를 메모이제이션 (Memoization)할 때 사용

import { useMemo, useState } from 'react'

const ExpensiveComponent = ({ value }) => {
  useEffect(() => {
    console.log('rendering')
  })

  return <span{value + 10000}></span>
}

const App = () => {
  const [value, setValue] = useState(10);
  const [, triggerRendering] = useState(false);

  const MemoizedComponent = useMemo(
    () => <ExpensiveComponent value={value}>, [value]
  )

  const handleChange = (e) => {
    setValue(Number(e.target.value))
  }

  const handleClick = () => {
    setValue(Number(e.target.value))
  }

  return (
    <>
      <input value={value} onChange={handleChange}></input>
      <button = onClick={handleClick}>렌더링 발생</button>
      {MemoizedComponent}
    </>
  )
}
  • 첫번째 인수로 생성 함수, 두번째 인수로 해당 함수가 의존하는 값의 배열을 전달
    • 렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면, 함수를 재실행하지 않고 이전의 값을 반환
    • 렌더링 발생 시 의존성 배열의 값이 변경되었으면, 첫번째 인수의 함수를 실행한 후에 그 값을 반환 및 메모이제이션

React.Memo : 외부 상태나 컨텍스트에 의존하지 않고 순수 함수형 컴포넌트를 메모이제이션할 때 사용

const CreateUser = React.memo(({ username, email, onChange, onCreate }) => {
  return (
    <div>
      <input
        name="username"
        placeholder="계정명"
        onChange={onChange}
        value={username}
      />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
      />
      <button onClick={onCreate}>등록</button>
    </div>
  );
});
  • 컴포넌트의 props이 바뀔 때에만 리렌더링을 수행

useCallback : 인수로 넘겨받은 콜백 자체를 저장해, 재성성 대신 재사용할 때 사용

const ChildComponent = memo(({name, value, onChange}) => {
  useEffect(
    () => {
      console.log('rendering', name)
    }
  )

  return(
    <>
      <h1>{name} (value ? '켜짐' : '꺼짐')</h1>
      <button onClick={onChange}>toggle</button>
    </>
  )
})

const App = () => {
  const [status1, setStatus1] = useState(false)
  const [status2, setStatus2] = useState(false)

  const toggle1 = useCallback(
    function toggle1() {
      setStatus(!status1)
    }, [status1],
  )

  const toggle2 = useCallback(
    function toggle2() {
      setStatus(!status2)
    }, [status2],
  )

  return (
    <>
      <ChildComponent name="1" value={status1} onChange={toggle1}></ChildComponent>
      <ChildComponent name="2" value={status2} onChange={toggle2}></ChildComponent>
    </>
  )
}
  • 첫번째 인수로 함수, 두번째 인수로 해당 함수가 의존하는 값의 배열을 전달
  • 값의 메모이제이션을 위해 useMemo를 사용했다면, 함수의 메모이제이션을 위해 사용하는 것이 useCallback!
    • 해당 의존성이 변경되었을 때만 함수가 재생성 : 불필요한 리소스 및 리렌더링 방지 가능
    • useMemo을 통해 useCallback를 구현할 수 있음

useRef : 함수형 컴포넌트 내에서 참조를 생성하고 관리할 때 사용

import { useRef, useEffect } from 'react';

function MyComponent() {
  const myInputRef = useRef(null);

  useEffect(() => {
    // 컴포넌트가 마운트된 후, input 요소에 포커스를 줌
    myInputRef.current.focus();
  }, []);

  return <input ref={myInputRef} />;
}
  • 반환값인 객체 내부에 있는 .current로 값에 접근 혹은 변경이 가능
  • 컴포넌트의 렌더링과 관계없이 변수를 저장 가능 → useState와 달리, 매번 리렌더링이 일어나지 않음
  • 주로 DOM 요소에 접근하거나 컴포넌트의 생명주기와 독립적으로 값을 유지하는 상황에서 활용
import { useRef, useState, useEffect } from 'react';

function MyComponent() {
  const countRef = useRef(0);
  const [count, setCount] = useState(0);

  useEffect(() => {
    // countRef는 렌더링과 무관하게 유지되는 변수
    countRef.current = count;
  }, [count]);

  const handleClick = () => {
    setCount(count + 1);
    console.log(countRef.current); // 항상 가장 최근 값 출력
  };

  return <button onClick={handleClick}>증가</button>;
}

useContext : React Context를 통해 전역으로 상태를 공유하거나 전달할 때 사용

  • prop 내려주기 : A 컴포넌트가 제공하는 데이터를 D 컴포넌트에서 사용하려면, …
    • 하위 컴포넌트로 필요한 위치까지 계속해서 넘겨주어야 함 → 제공하는 쪽, 제공받는 쪽 모두 번거로운 작업!
<A props={props}>
  <B props={props}>
    <C props={props}>
      <D props={props}>
        ...
      </D>
    </C>
  </B>
</A>
  • 리액트 컨텍스트 (React Context) : 컴포넌트 트리 안에서 전역적으로 데이터를 공유
    • 중첩 컴포넌트 간에 데이터를 명시적으로 전달하지 않아도 되므로, Props를 여러 단계에 걸쳐 전달하지 않아도 됨
// 1. Context 객체 생성
const MyContext = React.createContext(defaultValue);

// 2. Context를 제공하는 컴포넌트 작성
const MyContextProvider = ({ children }) => {
  const contextValue = // ... (상태 값이나 함수 등)

  return (
    <MyContext.Provider value={contextValue}>
      {children}
    </MyContext.Provider>
  );
};

// 3. useContext를 사용하여 값에 접근
import { useContext } from 'react';

const MyComponent = () => {
  const contextValue = useContext(MyContext); // 이때, 리액트가 아닌 자비스크립트가 반환된다.

  // contextValue를 사용하여 렌더링 또는 다른 로직 수행
};
  • Context.Provider : 컨텍스트 값을 하위 컴포넌트에 제공
    • value 속성을 통해 전달할 값을 설정
const MyContextProvider = ({ children }) => {
  const contextValue = // ... (상태 값이나 함수 등)

  return (
    <MyContext.Provider value={contextValue}>
      {children}
    </MyContext.Provider>
  );
};

useReducer : 복잡한 상태 로직을 다룰 때 사용

// useReducer가 사용할 State 정의
type State = {
  count: number
}

// State의 변화를 발생시킬 Action의 타입과 넘겨줄 payload 정의
type Action = {
  type: 'up' | 'down' | 'reset';
  payload?: State;
}

// 무거운 연산이 포함된 게으른 초기화 함수 init 정의
const init = (count: State) => {
  return count
}

const initState: State = { count: 0 }

const reducer = ((state: State, action: Action): State) => {
  switch (action.type) {
    case 'up':
      return { count: state.count + 1 }
    case 'down':
      return { count: state.count - 1 > 0 ? state.count - 1 : 0 }
    case 'down':
      return init(action.payload || { count: 0 })
    default:
      throw new Error(`Unexpected Action type: ${action.type}`)
  }
}

const export default App = () => {
  const [state, dispatcher] = useReducer(reducer, initialState, init)

  const handleUpButtonClick = () => {
    dispatcher({ type: 'up' })
  }

  const handleDownButtonClick = () => {
    dispatcher({ type: 'down' })
  }

  const handleResetButtonClick = () => {
    dispatcher({ type: 'reset', payload: { count: 1 } })
  }

  return {
    <div className="App">
      <button onclick={handleUpButtonClick}>+</button>
      <button onclick={handleDownButtonClick}>-</button>
      <button onclick={handleResetButtonClick}>Reset</button>
    </div>
  }
}
  • 반환값은 useState와 동일하게 길이가 2인 배열
    • state : 현재 useReducer가 갖고 있는 값
    • dispatcher : state를 업데이트하는 함수
      (값만 넘겨주는 setState와 달리, state를 변경할 수 있는 action 반환)
  • 인수는 useState와 달리 2~3개의 인수를 필요로 함
    • reducer : 첫번째 인수 → useReducer의 기본 action을 정의하는 함수
    • initialState : 두번째 인수 → useReducer의 초깃값
    • init : (선택) 세번째 인수 → 초깃값을 지연해서 생성 (게으른 초기화)

useReducer의 목적?

(1) 복잡한 형태의 state를 사전에 정의된 dispatcher로만 수정할 수 있게 하여,
(2) state 값에 대한 접근은 컴포넌트에서만 가능하게 하고,
(3) 이를 업데이트하하는 방법에 대한 상세 정의 컴포넌트 밖에 둔 다음,
(4) state의 업데이트를 dispatcher로 제한한다!

  • state 하나가 가질 값이 복잡하고 이를 수정하는 경우의 수가 많아지면 state를 관리하는 것이 어려워짐
  • 여러 state를 관리하는 것보다 성격이 비슷한 것들을 묶어 useReducer로 관리하는 것이 효율적
  • 게으른 초기화 함수를 인자로 사용하여 useState에 함수를 넣은 것과 같은 이점을 누릴 수 있음
  • 추가로 state에 대한 초기화가 필요할 때 reducer에서 재사용할 수 있음

forwardRef : 부모 컴포넌트에서 자식 컴포넌트로 ref를 전달할 수 있게 함

import { useRef, forwardRef } from 'react';

// 자식 컴포넌트
const ChildComponent = forwardRef((props, ref) => {
  const internalState = useRef(null);

  // 부모 컴포넌트에서 전달한 ref에 직접 접근
  // ref.current를 통해 부모 컴포넌트의 ref를 참조할 수 있음
  const handleButtonClick = () => {
    console.log('자식 컴포넌트에서 버튼 클릭');
    console.log('내부 상태:', internalState.current);
  };

  return (
    // 여기에 컴포넌트 JSX를 작성
    <div>
      <button onClick={handleButtonClick}>자식 컴포넌트에서 클릭</button>
    </div>
  );
});

// 부모 컴포넌트
const ParentComponent = () => {
  // ref를 생성하여 자식 컴포넌트에 전달
  const childRef = useRef();

  // 부모 컴포넌트에서 ref를 자식 컴포넌트에 전달
  return (
    <div>
      {/* forwardRef를 사용하여 자식 컴포넌트에 ref 전달 */}
      <ChildComponent ref={childRef} />
    </div>
  );
};

export default ParentComponent;
  • ref를 받고자 하는 컴포넌트를 forwards로 감싸고, 두번째 인수로 ref 전달
    • 부모 컴포넌트에서 props.ref를 통해 ref를 전달
  • useRef에서 반환된 ref 객체를 상위에서 하위 컴포넌트로 전달할 때, 직접 props를 넣을 수 없을 때 사용
    • 컴포넌트 외부의 DOM에 접근 : 부모에서 선언 → 자식에 전달 → 자식에 참조 걸기 → 부모에서 컨트롤

But, 외부 DOM을 참조하는 ref를 갖는 것은 Coupling이 강해지므로 좋지 않다!

useImperativeHandle (with forwardRef) : 부모 컴포넌트가 자식 컴포넌트를 컨트롤할 수 있게 함

import { forwardRef, useRef, useImperativeHandle } from 'react';

const ChildComponent = forwardRef((props, ref) => {
  // useImperativeHandle을 사용하여 부모 컴포넌트에 특정 함수나 값들을 노출
  useImperativeHandle(ref, () => ({
    // 부모 컴포넌트에서 입력 엘리먼트에 포커스를 맞추기 위해 함수를 노출
    focusInput: () => {
      inputRef.current.focus();
    }
  }));

  const inputRef = useRef(null);

  return <input ref={inputRef} />;
});

const ParentComponent = () => {
  const childRef = useRef(null);

  const handleClick = () => {
    // 자식 컴포넌트에서 노출한 함수를 호출하여 입력 엘리먼트에 포커스를 맞춤
    childRef.current.focusInput();
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>입력에 포커스 맞추기</button>
    </div>
  );
};

export default ParentComponent;
  • useImperativeHandle을 통해 부모 컴포넌트에 함수 및 기능을 노출시킴
    • 부모에게 참조값을 전달 → 자식은 객체를 반환
  • HTMLElement만 주입 가능한 ref에, 자식 컴포넌트에 새로 설정한 객체의 키·값에 대해서도 접근 가능

useLayoutEffect : 시그니처가 useEffect와 동일하나, 모든 DOM 변경 후의 콜백 실행이 동기적으로 발생

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

const MyComponent = () => {
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    // DOM 요소의 너비 측정
    const element = document.getElementById('myElement');
    if (element) {
      const elementWidth = element.clientWidth;
      setWidth(elementWidth);
    }
  }, []); // 빈 의존성 배열은 이 효과가 초기 렌더 이후에 한 번만 실행되도록 함

  return (
    <div>
      <p>내 요소의 너비는: {width}px</p>
      <div id="myElement">이것은 내 요소입니다</div>
    </div>
  );
};

export default MyComponent;
  • ‘시그니처가 useEffect와 동일하나,’ : 두 훅의 형태나 사용 예제가 동일함
  • ‘모든 DOM 변경 후의 콜백 실행이 동기적으로 발생’ :
    (1) 리액트가 DOM을 업데이트 → (2)useLayoutEffect 실행 → (3)useEffect 실행
  • DOM은 계산되었지만, 이것이 화면에 반영되기 전에 하고 싶은 작업을 처리할 때 사용

사용자 정의 훅 (Custom Hook) : 서로 다른 컴포넌트 내부에서 같은 로직을 공유할 때 사용

  • 기존에 존재하는 훅을 기반으로 필요한 훅을 개발 (함수 이름이 반드시 use로 시작)
    • 사용자 정의 훅의 동작, 매개변수, 반환값을 사전에 정의한 상태에서 구현하여 다른 컴포넌트에서 활용

useFetch : HTTP 요청을 fetch하여 데이터를 가져오는 작업을 추상화

useTimeout : 일정 시간이 지난 후에 콜백 함수를 실행하는 작업을 추상화

useToggle : 불리언 상태를 토글하는 작업을 추상화

태그:

업데이트: