TIL 23. 11. 08

오늘 한 일

  • React Hooks 학습
    • useState
    • useEffect
    • useRef
    • useContext

React Hooks

In React, useState, as well as any other function starting with ”use”, is called a Hook.

React에서는 use로 시작하는 다른 함수를 훅(hook)이라고 부른다. 훅은 컴포넌트의 최상위 레벨 또는 커스텀 훅에서만 호출할 수 있다. 반복문이나 조건문 안에서는 호출할 수 없다.

컴포넌트 최상위 레벨이란 컴포넌트와 동일한 스코프. 조건문과 반복문 등 더 깊은 depth에서는 호출할 수 없다.

조건에 따라 실행유무가 결정되지 않는 무조건적인 선언으로 생각할 것. React는 state와 setter 함수를 배열로 관리하고 인덱스로 접근하기 때문에 순서가 매우 중요하다.

훅은 동일한 컴포넌트의 모든 렌더링에서 안정적인 호출 순서에 의존한다. 항상 같은 순서로 호출을 보장하고자 최상위 레벨에서 호출하는 것.

React hooks: not magic, just arrays

1. useState

useState는 컴포넌트에 state 변수를 추가할 수 있게 해주는 React Hook

구조분해할당을 이용해 값과 set 함수의 이름을 지정해서 사용한다.

const [state, setState] = useState(initialState);
  • state: 현재 상태 (렌더링 사이에 데이터를 유지하기 위한 변수)
  • setState: 상태를 다른 값으로 업데이트하고 리렌더링을 촉발할 수 있는 set 함수
  • initialState: 초기에 state를 설정할 값. 초기 렌더링 이후는 무시
    • 초기화 함수를 전달할 수도 있다. 컴포넌트가 리렌더링될 때에는 실행되지 않고 초기화 중에만 함수를 호출한다.

주의사항

React는 이벤트 핸들러가 실행을 마친 후 state 업데이트를 일괄처리(batching)한다. 아래 예제에서 setCount(count+ 1) 를 세 번 호출하므로 3이 증가할 것으로 예상하지만 실제로는 1이 증가한다.

import { useState } from 'react';

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

  function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>{count}</button>
  );
}

상태가 변경되면 화면을 새로 그려야한다. 렌더링이 많이 일어나는 것은 곧 성능 문제로 이어진다. 불필요한 렌더링을 피하기 위해 요청 사항을 모아서 렌더링하는 방식을 사용한다.

“Rendering” means that React is calling your component, which is a function. The JSX you return from that function is like a snapshot of the UI in time. Its props, event handlers, and local variables were all calculated using its state at the time of the render. - State as a Snapshot - react.dev

렌더링이란 React에서 컴포넌트를 호출한다는 의미다. 해당 함수에서 반환하는 JSX는 시간상 UI의 스냅샷과 같다. 렌더링 당시의 state를 사용해 계산했기 때문에 count가 1만 증가한 것.

  1. setCount(count + 1): count가 0이므로 1을 더하고 다음 렌더링에서 1로 변경
  2. setCount(count + 1): count가 0이므로 1을 더하고 다음 렌더링에서 1로 변경
  3. setCount(count + 1): count가 0이므로 1을 더하고 다음 렌더링에서 1로 변경

React는 state 업데이트를 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다린다. 이 때문에 리렌더링은 모든 setCount() 호출이 완료된 이후에만 일어난다.

함수형 업데이트

다음 렌더링 전에 동일한 state 변수를 여러 번 업데이트 하고 싶다면 set 함수에 변경할 state 값을 전달하는 대신, 이전 state를 기반으로 다음 state를 계산하는 함수를 전달하는 방법이 있다.

함수형 업데이트란 이전 상태를 기반으로 새로운 상태를 생성하는 함수를 사용하여 상태를 업데이트하는 방법이다. 이때 전달된 함수는 업데이터 함수라고 부른다.

함수를 전달하면 대기 중인 state를 가져와서 다음 state를 계산한다. React는 함수를 큐에 넣고 다음 렌더링 중에 순서대로 호출한다.

import { useState } from 'react';

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

  function handleClick() {
    setCount((count) => count + 1);
    setCount((count) => count + 1);
    setCount((count) => count + 1);
  }

  return <button onClick={handleClick}>{count}</button>;
}
  • setCount((count) => count + 1): 대기중인 state 0을 받아 다음 state로 1을 반환
  • setCount((count) => count + 1): 대기중인 state 1을 받아 다음 state로 2을 반환
  • setCount((count) => count + 1): 대기중인 state 2을 받아 다음 state로 3을 반환

이렇게 동작하는 이유는 다음과 같다.

  1. React는 이벤트 핸들러의 다른 코드가 모두 실행된 후에 이 함수가 처리되도록 큐에 넣는다.
  2. 다음 렌더링 중에 React는 큐를 순회하여 최종 업데이트된 state를 제공한다.

즉, setCount로 전달된 각각의 업데이트 함수를 큐에 추가하고 다음 렌더링 중에 큐를 순회하며 이전 업데이터 함수의 반환값을 가져와서 다음 업데이터 함수에 전달하는 식으로 반복한다.

Queueing a Series of State Updates - React

2. useEffect

useEffect는 렌더링될 때마다 특정한 작업을 수행해야 될 때 설정하는 React Hook

useEffect(setup, dependencies?)
  • setup: Effect의 로직이 포함된 함수. 컴포넌트가 DOM에 추가되면 셋업 함수를 실행한다. 의존성이 변경되어 리렌더링될 때마다 클린업 함수가 있는 경우 먼저 이전 값으로 클린업 함수를 실행한 다음, 새 값으로 셋업 함수를 실행
    • 클린업 함수: 컴포넌트가 언마운트될 때 실행하는 함수
  • dependencies (optional): setup 코드 내에서 참조된 모든 반응형 값(props, state, 변수, 함수)의 목록. 함수의 실행 조건을 제어한다.
import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
  1. 컴포넌트가 마운트되면 셋업 코드를 실행
  2. 의존성이 변경된 컴포넌트를 다시 렌더링할 때마다
    1. 이전 props와 state로 클린업 코드를 실행
    2. 새 props와 state로 셋업 코드를 실행
  3. 컴포넌트가 마운트 해제되면 클린업 코드를 실행

주의사항

  • 의존성 일부가 컴포넌트 내부에 정의된 객체 또는 함수인 경우 Effect가 필요 이상으로 자주 실행될 위험이 있다. 객체나 함수는 매번 새로 선언되기 때문.
    • 렌더링 중에 생성된 객체나 함수를 의존성으로 사용하지 않고 Effect 내에서 객체나 함수를 생성
  • Effect에서 직접 페칭하면 “네트워크 워터폴”을 만들기 쉽다.
    • 부모 컴포넌트 렌더링, 데이터 페치 ➡ 자식 컴포넌트 렌더링, 데이터 페칭
    • 순차적으로 페칭되기 때문에 병렬로 페칭하는 것보다 훨씬 느리다

반응형 의존성 지정

useEffect는 의존성 배열을 포함하는 형태와 포함하지 않는 형태로 나뉘는데 차이가 있다.

  1. 의존성 배열 전달

    useEffect(() => {
      // ...
    }, [a, b])
    
    • 마운트될 때, 의존성 배열에 명시된 반응형 값이 변경되었을 때마다 실행
  2. 빈 의존성 배열 전달

    useEffect(() => {
      // ...
    }, [])
    
    • 마운트될 때 한 번만 실행
  3. 의존성 배열 전달 X

    useEffect(() => {
      // ...
    }); 
    
    • 컴포넌트가 렌더링될 때마다 실행

3. useRef

useRef는 DOM 요소에 접근할 수 있도록 하는 React Hook

ref.current 속성을 통해 해당 ref의 현재 값에 접근할 수 있다.

const ref = useRef(initialValue)
  • initialValue: 객체의 current 프로퍼티 초기 설정값

useRef의 역할

  • 변수
    • 리렌더링을 발생시키지 않는 값을 저장할 때 사용

    • settet 함수가 없는 일반 state 변수라고 생각

      • function useRef(initialValue) {
          const [ref] = useState({ current: initialValue });
          return ref;
        }
        
  • DOM 요소 접근

    • React에서 DOM 요소를 선택해야 하는 경우 사용
    • e.g. input에 focus 적용

주의사항

  • ref.current 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않는다. ref는 state가 아닌 일반 JavaScript 객체이기 때문에.
    • 컴포넌트에 값을 저장해야 하지만 렌더링과 관련없는 경우 ref를 사용
    • ref의 현재 값을 바꾸면 즉시 변경
  • 렌더링 중에는 ref.current를 읽거나 쓰지 말 것. ref.current가 언제 변경되는지 알지 못하기 때문에, 렌더링 중에 읽어도 컴포넌트의 동작을 예측하기 어렵다.

  • 설정된 ref 값은 컴포넌트가 계속해서 렌더링 되어도 unmount 전까지 값을 유지한다.

state와 ref의 차이

state refs
변경 시 리렌더링 O 변경 시 리렌더링 X
Immutable - setter 함수를 사용해 state 변수를 변경 Mutable - 렌더링 프로세스 외부에서 current 값을 수정하고 변경
언제든지 state 참조 가능 렌더링 중에는 current 값 참조 X

4. useContext

useContext는 컴포넌트에서 context를 읽고 구독할 수 있게 해주는 React Hook

const value = useContext(SomeContext)
  • SomeContext: 이전에 createContext로 생성한 context
    • context란 React 컴포넌트 트리 안에서 전역적이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법

Context API가 출현하기 전에는 상태 관리가 주로 props에 의해 이루어졌다. 그러나 이러한 방식은 컴포넌트 계층 구조가 깊어질수록 복잡성이 증가하고 유지 관리가 어려움이 있었다.

Prop drilling

prop drilling은 props를 통해 컴포넌트 트리의 여러 레이어를 거쳐 해당 데이터가 필요한 깊이 중첩된 컴포넌트로 데이터를 전달하는 과정을 뜻한다.

img

출처 : https://www.copycat.dev/blog/react-context/

애플리케이션이 더 크고 복잡해지고, 컴포넌트 트리의 깊이가 깊어지기 시작하면 props를 통한 상태 관리는 여러 가지 문제를 발생시킨다.

  • 코드 중복: 하위 컴포넌트에 전달하기 위해 단순히 prop을 내려주기만 하는 코드가 반복되어 불필요하게 복잡해짐
  • 재사용성 저하: 컴포넌트는 재사용을 염두해둔다. 어느 컴포넌트에는 prop이 필요하고 다른 컴포넌트에서는 필요하지 않다면 이는 재사용을 어렵게 한다.
  • 유지 관리 어려움: prop의 형태가 바뀌면 전달해야 하는 모든 컴포넌트를 변경해야한다. 또한, 여러 레이어의 컴포넌트를 거쳐야 하므로 추적이 어렵다.

context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다.

데이터 전달하기

STEP1: Context 만들기

먼저 Context를 생성한다. createContext를 사용하면 컴포넌트가 제공하거나 읽을 수 있는 context를 만들 수 있다.

const SomeContext = createContext(defaultValue)
  • SomeContext: 컨텍스트를 읽는 컴포넌트 상위 트리에 일치하는 컨텍스트 provider가 없을 때 이 컨텍스트가 갖도록 할 값 (default 값)
import { createContext } from 'react';

export const ThemeContext = createContext('light');
STEP2: context 사용하기

컴포넌트의 최상위 레벨에서 useContext를 호출하여 context에 접근한다.

import { useContext } from 'react';
import { ThemeContext } from './ThemeContext.js'

function Button() {
  const theme = useContext(ThemeContext);
  // ...

useContext는 전달한 context에 대한 context 값을 반환한다. context 값을 결정하기 위해 컴포넌트 트리를 검색하고 특정 context에 대해 위에서 가장 가까운 context provider를 찾는다.

STEP3: context 제공하기

context provider로 감싸 컴포넌트에 context 값을 제공한다. value의 값은 해당 provider 내에서 context를 읽는 모든 컴포넌트에 전달되는 값이다.

context를 업데이트하려면 부모 컴포넌트에 state 변수를 선언하고 현재 state를 context 값으로 provider에 전달한다.

import { useState, useContext } from 'react';
import { ThemeContext } from './ThemeContext.js'

export default function App() {
    const [theme, setTheme] = useState('light')
    
  return (
	<ThemeContext.Provider value={theme}>
  	  <Button />
	</ThemeContext.Provider>
  );
}

트리의 일부분을 다른 값의 provider로 감싸 해당 부분에 대한 context를 재정의할 수도 있다.

<ThemeContext.Provider value="dark">
  ...
  <ThemeContext.Provider value="light">
    <Footer />
  </ThemeContext.Provider>
  ...
</ThemeContext.Provider>

주의사항

  • <Context.Provider>는 반드시 useContext() 호출을 수행하는 컴포넌트의 위에 있어야 함
  • 변경된 value를 받는 provider부터 시작해서 해당 context를 사용하는 자식들에 대해서까지 전부 자동으로 리렌더링된다

Context를 사용하기 전에

  • props를 몇 단계 깊이 전달해야 한다고 해서 context를 사용해야 한다는 것은 아니다

  • UI 컴포넌트의 children으로 prop을 사용하도록 만들면 prop을 전달할 컴포넌트 수가 하나 줄어든다

    • <Layout posts={posts} />
      <Layout><Posts posts={posts} /></Layout> 
      

사용 사례

  • 테마
  • 계정(auth)
  • 라우팅
  • state 관리

카테고리:

업데이트:

댓글남기기