React 4회차

Ocean·2023년 2월 18일
0

JSCODE - React study

목록 보기
4/5

1. useContext

const value = useContext(MyContext);

1.1 Prop Drilling

Prop Drilling은 React에서 props를 이용해 컴포넌트 간 데이터를 전달할 때 해당 데이터가 필요없는 컴포넌트도 거지는 것이다.

위 그림처럼 컴포넌트 A의 데이터를 컴포넌트 C로 전달하기 위해 사이에 있는 컴포넌트 B를 거쳐야한다.

문제점
prop 전달이 깊어질수록 해당 prop를 추적하기 힘들어지고 유지보수가 어려워진다.

1.2 Context란?

context는 React 컴포넌트 트리 안에서 global이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법으로 context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다.

React Hook인 useContext를 이용하면 이러한 Context를 더 편하게 사용할 수 있다.

context API

  • createContext : context 객체 생성
  • Provider : 생성한 context를 하위 컴포넌트에 전달
  • Consumer : context의 변화를 감시하는 컴포넌트

Ex)
App.js

import React, { createContext } from "react";
import Children from "./Children";

// AppContext 객체를 생성한다.
export const AppContext = createContext();

const App = () => {
  const user = {
    name: "김채원",
    job: "가수"
  };

  return (
    <>
      <AppContext.Provider value={user}>
        <div>
          <Children />
        </div>
      </AppContext.Provider>
    </>
  );
};

export default App;

Children.js

import React from "react";
import { AppContext } from "./App";

const Children = () => {
  return (
      <AppContext.Consumer>
        {(user) => (
          <>
            <h3>AppContext에 존재하는 값의 name은 {user.name}입니다.</h3>
            <h3>AppContext에 존재하는 값의 job은 {user.job}입니다.</h3>
          </>
        )}
      </AppContext.Consumer>
  );
};

export default Children;

여러 컴포넌트가 context를 사용하게 되면 코드가 점점 복잡해지는 문제가 있다.

1.3 useContext

useContext를 적용하면 코드가 아래와 같이 바뀐다.

import React, { useContext } from "react";
import { AppContext } from "./App";

const Children = () => {
  // useContext를 이용해서 따로 불러온다.
  const user = useContext(AppContext);
  return (
    <>
      <h3>AppContext에 존재하는 값의 name은 {user.name}입니다.</h3>
      <h3>AppContext에 존재하는 값의 job은 {user.job}입니다.</h3>
    </>
  );
};

export default Children;

App.js에서 Context를 생성하고 Provider를 통해 전달하는 코드는 그대로지만 Children.js에서 AppContext를 사용하는 과정에서 const user = useContext(AppContext)를 이용해 Context를 불러온 후 바로 사용이 가능하게 바뀐다.

2. useReducer

2.1 useReducer 파라미터 설명

const [<상태 객체>, <dispatch 함수>] = useReducer(<reducer 함수>, <초기 상태>, <초기 함수>);

reducer 함수

  • 현재 상태(state) 객체와 행동(action) 객체를 인자로 받아서 새로운 상태(state) 객체를 반환하는 함수

dispatch 함수

  • 컴포넌트 내에서 상태 변경을 일으키기 위해서 사용되는 함수
  • 인자로 reducer 함수에 넘길 행동(action) 객체를 받는다.

행동(action) 객체

  • 관행적으로 어떤 부류의 행동인지를 나타내는 type 속성과 해당 행동과 관련된 데이터를 담고 있다.

→ 즉, 컴포넌트에서 dispatch 함수에 행동(action) 객체를 던지면, reducer 함수가 이 행동(action)에 따라서 상태(state)를 변경해준다.

2.2 카운터 예제

useReducer() Hook 함수를 이용해 현재 카운트 값과 2개 버튼을 보여주는 간단한 카운터 컴포넌트

import React, { useReducer } from "react";

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

  return (
    <>
      <h2>{state.count}</h2>
      <button onClick={() => dispatch({ type: "INCREMENT", step: 1 })}>
      </button>
      <button onClick={() => dispatch({ type: "DECREMENT", step: 1 })}>
      </button>
    </>
  );
}
  • 현재 카운트 값은 state 객체로부터 읽어온다.
  • 카운트 값 변경을 위해서는 각 버튼이 클릭되었을 때 dispatch 함수를 호출하도록 설정되었다.
  • dispatch 함수의 인자로 type 속성에는 어떤 변경인지에 따라 INCREMENT 또는 DECREMENT가 넘어가고, step 속성에는 변경할 값의 크기를 넘긴다.

2.3 reducer 함수

useReducer() 함수는 첫번째 인자로 넘어오는 reducer 함수를 통해 컴포넌트의 state가 action에 따라 어떻게 변해야하는지를 정의한다.

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + action.step };
    case "DECREMENT":
      return { count: state.count - action.step };
    default:
      throw new Error("Unsupported action type:", action.type);
  }
}
  • INCREMENT 타입의 행동에 대해서는 현재 카운트 값을 step 만큼 증가하여 새로운 상태를 반환하고, DECREMENT 타입의 행동에 대해서는 현재 카운트 값을 step 만큼 감소하여 새로운 상태를 반환한다.
  • 정의하지 않은 행동 타입이 넘어왔을 때는 예외를 발생시키는 것이 좋다.

2.4 복잡한 상태 관리

위 예제 정도의 간단한 상태 관리를 위해서라면 간단하게 useState() 함수를 써도 된다.

더 복잡한 상태 관리를 위해 카운트의 하한 값과 상한 값을 제한하고, 카운트의 값을 무작위로 바꾸는 버튼과 초기화 시키는 버튼을 추가했다.

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return state.count < action.max
        ? { count: state.count + action.step }
        : state;
    case "DECREMENT":
      return state.count > action.min
        ? { count: state.count - action.step }
        : state;
    case "RESET":
      return initialState;
    case "RANDOM":
      return {
        count:
          Math.floor(Math.random() * (action.max - action.min)) + action.min,
      };
    default:
      throw new Error("Unsupported action type:", action.type);
  }
}
import React, { useReducer } from "react";

function Counter({ step = 1, min = 0, max = 10 }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <p>
        단계: {step}, 최소: {min}, 최대: {max}
      </p>
      <h2>{state.count}</h2>
      <button onClick={() => dispatch({ type: "INCREMENT", step, max })}>
        증가
      </button>
      <button onClick={() => dispatch({ type: "DECREMENT", step, min })}>
        감소
      </button>
      <button onClick={() => dispatch({ type: "RANDOM", min, max })}>
        무작위
      </button>
      <button onClick={() => dispatch({ type: "RESET" })}>초기화</button>
    </>
  );
}

상태 로직이 복잡해져도 카운터 컴포넌트 코드는 크게 복잡해지지 않는다.

3. useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

3.1 함수 메모이제이션

useCallback()은 함수를 menoization 하기 위해서 사용되는 Hook 함수이다.

첫번째 인자로 넘어온 함수를, 두번째 인자로 넘어온 배열 내의 값이 변경될 때 저장해놓고 재사용할 수 있게 해준다.

어떤 React 컴포넌트 안에 함수가 선언이 되어 있다면, 이 함수는 해당 컴포넌트가 렌더링될 때 마다 새로운 함수가 생성된다.

하지만, useCallback()을 사용하면, 해당 컴포넌트가 렌더링되더라도 그 함수가 의존하는 값이 바뀌지 않는 한 기존 함수를 계속해서 반환한다.

const add = useCallback(() => x + y, [x, y]);
  • x 또는 y 값이 바뀌면 새로운 함수가 생성되어 add 변수에 할당
  • xy값이 동일하면 다음 랜더링 때 이 함수 재사용

자바스크립트가 브라우저에서 빠르게 실행되므로 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해 useCallback()을 사용하는 것은 큰 의미가 없거나 오히려 손해일 수 있다.

useCallback() 함수가 어떻게 쓰일 때 의미있는 성능 향상을 보일까?

3.2 자바스크립트 함수 동등성

> const add1 = () => x + y;
undefined
> const add2 = () => x + y;
undefined
> add1 === add2
false

자바스크립트에서 함수도 객체로 취급하기 때문에 메모리 주소에 의한 참조 비교가 일어나 동일한 코드의 함수를 === 연산자를 통해 비교하면 false가 반환된다.

이런 자바스크립트 특성은 React 컴포넌트 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제로 이어질 수 있다.

3.3 의존 배열로 함수를 넘길 때

많은 React Hook 함수들이 불필요한 작업을 줄이기 위해 두번째 인자로 첫번째 함수가 의존하는 배열을 받는다.

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

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = () =>
    fetch(`https://your-api.com/users/${userId}`)
      .then((response) => response.json())
      .then(({ user }) => user);

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

예를 들어, 다음과 같은 컴포넌트에서 API를 호출하는 코드는 fetchUser이 변경될 때만 호출된다. 위에서 말한 자바스크립트가 함수의 동등성을 판단하는 방식 대문에 예상치 못한 무한 루프가 발생한다.
fetchUser은 함수이기 때문에, userId 값이 바뀌는 말든 컴포넌트가 랜더링될 때 마다 새로운 참조값으로 변경된다. 그러면 useEffect() 함수가 호출되어 user 상태값이 바뀌고 그러면 다시 컴포넌트가 랜더링되고 그럼 또 다시 useEffect() 함수가 호출되는 것이 반복된다.

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

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = useCallback(
    () =>
      fetch(`https://your-api.com/users/${userId}`)
        .then((response) => response.json())
        .then(({ user }) => user),
    [userId]
  );

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

useCallback()을 사용하면 컴포넌트가 다시 랜더링 되더라도 fetchUser 함수의 참조값을 동일하게 유지시킬 수 있다.
따라서 의도대로 useEffect()에 넘어온 함수는 userId 값이 변경되지 않는 한 재호출 되지 않게 된다.

4.useMemo

4.1 Memoization

memoization이란 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말한다.
memoization을 적절하게 사용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화 할 수 있다.

4.2 컴포넌트 함수 호출

함수형 컴포넌트은 React 앱에서 랜더링이 일어날 때마다 호출이 된다. React에서 컴포넌트의 랜더링은 한 번만 일어나는 것이 아니라 수시로 계속 일어날 수 있다.

  • 자신의 상태 변경이 일어났을 때
  • 부포 컴포넌트의 상태 변경이 일어나 함께 랜더링 되어야 하는 경우

함수형 컴포넌트 내에 내부적으로 매우 복잡한 연산을 수행하는 함수가 들어있으면 컴포넌트의 재랜더링이 필요할 때마다 이 함수가 호출이 되므로 UI에 지속적으로 지연이 발생할 것이다.

4.2 함수형 컴포넌트에 memoization 적용

function MyComponent({ x, y }) {
  const z = compute(x, y);
  return <div>{z}</div>;
}

랜더링이 일어날 때 마다, compute 함수의 인자로 넘어오는 xy 값이 항상 바뀌는 게 아니라면 굳이 compute 함수를 계속 호출할 필요가 없다.

→ 랜더링이 발생했을 때 이전 랜더링과 현재 랜더링 간에 xy값이 동일한 경우, 다시 함수를 호출하여 z값을 구하는 대신, 기존에 메모리에 저장해두었던 z값을 그대로 사용한다.

이런 경우에 memoization을 간편하게 사용할 수 있게 하는 것이 useMemo()이다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

첫번째 인자 : 결과값을 생성하는 팩토리 함수
두번째 인자 : 기존 결과값 재활용 여부의 기준이 되는 입력값 배열

function MyComponent({ x, y }) {
  const z = useMemo(() => compute(x, y), [x, y]);
  return <div>{z}</div>;
}
  • xy 값이 이 전에 랜더링했을 때와 동일할 경우, 이 전 랜더링 때 저장해두었던 결과값을 재활용한다.
  • xy 값이 이 전에 랜더링했을 때와 달라졌을 경우, () => compute(x, y) 함수를 호출하여 결과값을 새롭게 구해 z에 할당한다.

5.useRef

const refContainer = useRef(initialValue);

프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환
useRef는 매번 렌더링을 할 때 동일한 ref 객체를 제공 → 리렌더링 되지 않는다.

useRef는 저장공간 또는 DOM 요소에 접근하기 위해 사용되는 React Hook이다.

자바스크립트를 사용할 때, 특정 DOM을 선택하기 위해서 querySelector 등의 함수를 썼다면, React를 사용하는 프로젝트에선 useRef라는 리액트 훅을 사용한다.

5.1 useRef 사용 예시 - 변수 관리

무한 로프 예시

function App() {
  const [count, setCount] = useState(1);
  const [renderingCount, setRedneringCount] = useState(1);
 
  useEffect(() => {
    console.log("rendering Count : ", renderingCount);
    setRedneringCount(renderingCount + 1);
  });

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

useEffect 안에 있는 setRedneringCount()가 계속해서 컴포넌트를 리랜더링해서 무한 루프에 빠지게 된다.

useRef 사용

function App() {
  const [count, setCount] = useState(1);
  const renderingCount = useRef(1);

  useEffect(() => {
    console.log("renderingCount : ", renderingCount.current);
    ++renderingCount.current;
  });

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

useRef로 관리하는 값은 값이 변해도 화면이 랜더링 되지 않기 때문에 무한 루프가 발생하지 않는다.

5.2 useRef 사용 예시 - DOM 요소 선택

import { useRef, useEffect } from "react";
import "./styles.css";

function App() {
  const inputRef = useRef();

  function focus() {
    inputRef.current.focus();
    console.log(inputRef.current);
  }

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="아이디 또는 이메일" />
      <button>Login</button>
      <br />
      <button onClick={focus}>focus</button>
    </div>
  );
}

export default App;

useRef를 통해 input DOM에 접근해 focus할 수 있음

profile
chick! chick!

0개의 댓글