✍️ React에 대해서

박상은·2022년 5월 24일
0

🤔 React의 사용 이유

1. DOM vs Virtual DOM

DOMdocument-object-modelHTML 문서를 JavaScript에서 제어하기 위해서 Object로 만든 것을 말합니다.
Virtual DOMDOM에서 렌더링에 필요한 요소를 제외하고 React에서 필요한 데이터만 경량화한 형태로 구현한 DOM입니다.

흔히 Virtual DOM을 사용하는 이유가 DOM이 무겁고 느리기 때문이라고 생각하기도 하지만 실제로 그런 것은 아닙니다.
DOM이 무겁고 느리기보다는 DOM의 변경에 의해서 발생하는 layout, reflow, repaint 때문입니다.

layout, reflow, repaint는 실제로 브라우저 화면을 구성하기 위해서 계산하고 그리는 행위를 하므로 다른 행위보다 많은 시간을 할애합니다.

하나의 Node가 변경되더라도 ( 어떤 값인지에 따라 다르지만 ) layout, reflow, repaint가 발생하게 됩니다.
그 이후 즉시 다른 Node가 변경되면 또다시 layout, reflow, repaint가 발생하게 됩니다.

이런 작업을 하면서 불필요한 연산을 많이 처리하게 됩니다.
불필요한 연산을 줄이기 위해서 React에서는 Virtual DOM을 이용해서 변경된 값들만 감지하고 연속적인 변경은 한 번에 처리하기 때문에 layout, reflow, repaint가 한 번만 발생하게 됩니다.

따라서 Virtual DOMDOM보다 더 빠르고 효율적으로 동작합니다.

2. 컴포넌트

컴포넌트는 객체지향프로그래밍에서 말하는 하나의 객체라고 생각해도 무방할 것 같습니다.
객체들을 하나하나 조립해서 프로그램을 만들듯이 원하는 위치에 원하는 컴포넌트를 조립해서 웹페이지를 만드는 것입니다.
코드를 명확하게 분리해서 작성할 수 있으며, 쉽게 재사용할 수 있습니다. 또한 가상 돔을 이용해서 변화를 감지하여 특정 컴포넌트의 특정 부분만 리랜더링 되도록 합니다.

🔬 React의 메모이제이션

React에서는 변수와 함수를 메모이제이션 할 수 있습니다.

메모이제이션이란 어떤 실행에 대한 결괏값이 저장해두고 다음에 실행할 때 결과값을 재사용함으로써 불필요한 연산을 줄이는 것을 의미합니다.

1. React.useMemo(value, deps)

변수를 메모이제이션해서 컴포넌트를 리렌더링할 때 새로운 변수를 만들지 않고 기존에 저장해놓을 값을 그대로 사용합니다.

물론 useEffect()처럼 deps의 값이 변경된다면 새로운 값을 메모이제이션 합니다.

2. React.useCallback(function, deps)

함수를 메모이제이션해서 컴포넌트를 리렌더링할 때 새로운 함수를 선언하지 않고 기존에 선언한 함수를 그대로 사용합니다.

물론 useEffect()처럼 deps의 값이 변경된다면 새로운 함수를 메모이제이션 합니다.

3. React.memo(component)

컴포넌트를 메모이제이션 하는 방법입니다.
원래 컴포넌트는 아래 5가지 조건에 의해서 리렌더링 되게 됩니다.
하지만 React.memo()를 사용하게 된다면 3번에 의해서 컴포넌트가 리렌더링 되지는 않습니다.

부모 컴포넌트가 변경되었다고 자식의 컴포넌트가 달라질 거라는 확신은 없기 때문에 개발자가 선택할 수 있게 만들어둔 것으로 생각합니다.

또한 React.memo()HOC ( higher-order-component )로 컴포넌트를 반환하는 컴포넌트입니다. 따라서 React.memo(Component) 형식으로 사용합니다.

  • 컴포넌트 리렌더링 조건
    1. 내부 state 변경
    2. props 변경
    3. 부모 컴포넌트 변경
    4. shouldComponentUpdatetrue거나 forceUpdate()가 실행될 때 ( 이 두 가지는 사용해본 적 없음 )

4. 메모이제이션 사용에 대한 견해

여기서 한 가지 생각해보면 좋은 것이 모든 함수, 변수, 컴포넌트에 메모이제이션을 한다고 무조건 좋은 것은 아니라는 것입니다. 메모이제이션한다는 행위 자체는 그만큼 메모리를 더 사용한다는 의미이므로 성능상의 문제가 있거나 확실히 사용함에 대한 이점을 갖는 것이 아니라면 사용에 대해 충분히 고민하고 사용해야 한다고 생각합니다.

여태까지 저는 모든 함수에다가 아무 생각 없이 useCallback()을 사용했었는데 이번에 공부하면서 잘못된 방식이라는 것을 알게 되었고 앞으로는 충분히 고민하고 적절한 곳에 사용할 생각입니다.

👀 동작 원리

처음에 React를 접할 때도 동작 원리가 궁금하긴 했지만, 그때는 JavaScript의 주요 개념들조차도 제대로 이해하지 못하는 상태였기 때문에 React의 개념들에 대해서 깊게 이해하지 못하고 넘어갔었지만, 현재는 closure 같은 개념도 어느 정도 이해했으며, React에 대해 많이 익숙해졌기 때문에 평소에 별생각 없이 가져다가 사용하기만 했던 hook 들을 어떻게 사용하는지에 관해서 공부하고 정리해보려고 합니다.

1. React.useState()와 React.useEffect() 간단하게 직접 구현

React.useState()React.useEffect()를 간단하게 구현해보면서 내부적으로 어떤 방식에 의해 동작하는지에 대해 이해하려고 노력했습니다.

다른 포스트들을 보면 클로저부터 순차적으로 알려주는 포스트가 많아서 굳이 여기서는 그렇게 하지 않고 typescript를 적용한 최종 결과물만 작성했습니다.

type Type = {
  click: () => void;
  render: () => void;
  wrtie: (str: string) => void;
};

const React = ((): {
  render: (Component: () => Type) => Type;
  useState: <T>(initialValue: T) => [T, (newState: T) => void];
  useEffect: (cb: () => void, depArray: any[]) => void;
} => {
  // hook들의 값이 들어갈 배열 ( useState, useEffect 등 )
  let states: any[] = [];
  // 현재 어떤 hook인지 판단할 인덱스 ( 몇번째로 선언한 hook인지 )
  let index = 0;

  const useState = <T>(initialValue: T): [T, (newState: T) => void] => {
    // setState()를 사용할 때 선언 시점의 index를 사용하기 위해 index값을 저장해둠
    // 만약 _index를 선언하지 않았다면 여러 개의 hook을 사용하면서 증가한 index의 hook의 값을 변경시키게 됨
    const _index = index;
    // 현재 값 || 초기값을 가짐
    const state = (states[_index] as T) || initialValue;
    // state 변경 함수
    const setState = <T>(newState: T) => (states[_index] = newState);

    // 다음 훅으로 변환
    index++;

    return [state, setState];
  };

  const useEffect = (cb: () => void, depArray: any[]) => {
    // 이전에 받았던 deps값의 배열
    const oldDeps = states[index];
    let hasChanged = true;

    // deps값 배열중에 하나라도 값이 다르다면
    if (oldDeps)
      hasChanged = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));

    // cb() 실행 ( 단, 초기에는 실행 )
    if (hasChanged) cb();

    // deps값 배열 최신화
    states[index] = depArray;

    // 다음 hook으로 변환
    index++;
  };

  const render = (Component: () => Type): Type => {
    index = 0;

    const C = Component();
    C.render();
    return C;
  };

  return { render, useState, useEffect };
})();

const Component = (): Type => {
  const [count, setCount] = React.useState<number>(1);
  const [text, setText] = React.useState<string>("default");

  React.useEffect(() => console.log("count 변경!"), [count]);

  return {
    render: () => console.log({ count, text }),
    click: () => setCount(count + 1),
    wrtie: (str: string) => setText(str),
  };
};

let App: Type;

// "count 변경!"
App = React.render(Component);  // { count: 1, text: "default" }
App.click();  // "count 변경!"
App.wrtie("blue");
App = React.render(Component);  // ( count: 2, text: "blue" )

🧊 React의 ref

1. React.useRef()

React에서는 특정 노드를 선택할 때는 useRef()를 이용해서 선택합니다.

하지만 useRef()가 노드를 선택하기 위해서만 존재하지는 않습니다.

useRef()로 생성한 변수는 값이 변경되어도 리렌더링이 되지 않습니다.
따라서 컴포넌트에서 사용은 하지만 값의 변화가 렌더링에 영향을 미치지 않는 변수를 useRef()를 이용해서 사용하면 됩니다.

2. callback ref

ref속성값에 callback함수를 넣어주는 방식을 말합니다.
이번에 공부하면서 알게 되었고 여태까지 합리적이지 못한 방식으로 hook을 사용했다는 것을 깨달았습니다.

아래 두 가지의 예시를 비교해보면 callback ref의 유용함을 알 수 있습니다.
( 좋은 예시가 떠오르지 않아서 autoFocus 속성을 못쓴다는 가정함 )

  • useRef() 사용 예시
import React, { useEffect, useRef } from "react";

function App() {
  const inputRef = useRef<HTMLInputElement | null>(null);

  useEffect(() => inputRef.current?.focus(), [inputRef]);

  return (
    <div className="App">
      <input type="text" ref={inputRef} />
    </div>
  );
}

export default App;
  • callback ref 사용 예시
import React from "react";

function App() {
  return (
    <div className="App">
      <input type="text" ref={(node) => node?.focus()} />
    </div>
  );
}

export default App;

이전에는 특정 노드를 선택해서 ref변수에 넣어서 사용했었습니다.
그렇게되면 불필요한 useRef(), useEffect()를 사용해야하는데 callback ref를 이용해서 간단하게 처리할 수 있습니다.

3. React.forwardRef()

forwardRef()는 하위 컴포넌트에 ref를 전달하는 방법입니다.
기본적으로 컴포넌트에 ref를 전달하면 에러가 발생합니다.
그래서 forwardRef()라는 특수한 방식을 이용해서 ref를 하위 컴포넌트로 전달할 수 있습니다.

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

function App() {
  const inputRef = useRef<HTMLInputElement | null>(null);
  const buttonRef = useRef<HTMLButtonElement | null>(null);

  useEffect(() => {
    setTimeout(() => {
      console.log("타이머 실행! >> ", buttonRef);
      buttonRef.current?.click();
    }, 2000);
  }, [buttonRef]);

  return (
    <div className="App">
      { /* 정상 작동 */ }
      <Form title="제목" ref={inputRef} />
      { /* 비정상 작동 : 코드상에서 에러가 나지는 않지만 실행했을 때 콘솔창에 경고 메시지 띄워줌 + buttonRef에 값이 들어가지 않음 */ }
      <Button ref={buttonRef} />
    </div>
  );
}

type FormProps = {
  title: string;
};

const Form = React.forwardRef<HTMLInputElement, FormProps>((props, ref) => (
  <form>
    <h2>{props.title}</h2>
    <input type="text" ref={ref} />
  </form>
));

type ButtonProps = {
  ref: React.MutableRefObject<HTMLButtonElement | null>;
};

const Button = ({ ref }: ButtonProps) => {
  return (
    <button type="button" ref={ref} onClick={() => console.log("click!!")}>
      click me
    </button>
  );
};

export default App;

🎢 Context API와 useReducer

리액트는 단방향 데이터 흐름을 지킵니다.
각 컴포넌트는 useState()useReducer()를 이용해서 관리하며, 하위 컴포넌트로 데이터를 전달할 때는 props를 이용합니다.

1. Context API

리액트의 단방향 데이터 흐름을 지키다보면 props drilling이 발생하게 됩니다.
Context API를 사용하면 props를 이용하지 않고 다른 컴포넌트에서 데이터를 사용할 수 있습니다.

2. React.useReducer()

리액트에서 상태 관리를 하는 방법중 하나입니다. ( useState(), useReducer() )

Flux 패턴을 따르며 actiondispatch하면 reducer를 통해서 state를 변경할 수 있습니다.
즉, 기존 state + action dispatch--reducer--> 새로운 state를 만들어냅니다.

기존에 사용하던 MVC 패턴의 문제점인 데이터가 여러 방향으로 흘러서 데이터 관리가 힘들다는 점을 보완하기 위해 나온 Flux 패턴은 단방향으로 데이터가 흘러가고 actionstatedispatch하는 방법으로만 데이터를 조작할 수 있습니다.

3. Context API + useReducer() 사용 예시

import { createContext, useCallback, useReducer } from "react";

type Props = {
  children: React.ReactNode;
};

type ContextType = {
  todoList: StateType[];
  onAddTodo: (todo: string) => void;
  onRemoveTodo: (id: number) => void;
};
// 컨텍스트 생성 ( 여러 컴포넌트에서 공유할 데이터 )
export const todoContext = createContext<ContextType>({
  todoList: [],
  onAddTodo: () => {},
  onRemoveTodo: () => {},
});

type StateType = {
  id: number;
  contents: string;
};
type ActionType =
  | {
      type: "ADD_TODO";
      payload: string;
    }
  | {
      type: "REMOVE_TODO";
      payload: number;
    };
// 리듀서 생성 ( 상태 변경 방법 작성 ( 기존 상태 + action => 새로운 상태 ) )
const todoReducer = (
  initialState: StateType[],
  action: ActionType
): StateType[] => {
  switch (action.type) {
    case "ADD_TODO":
      return [
        ...initialState,
        {
          id: Date.now(),
          contents: action.payload,
        },
      ];
    case "REMOVE_TODO":
      return initialState.filter((todo) => todo.id !== action.payload);

    default:
      return initialState;
  }
};

// Provider Wrapper
const PersonProvider = ({ children }: Props) => {
  const [todoList, dispatch] = useReducer(todoReducer, []);

  // todo 추가
  const onAddTodo = useCallback(
    (todo: string) => {
      dispatch({
        type: "ADD_TODO",
        payload: todo,
      });
    },
    [dispatch]
  );
  // todo 제거
  const onRemoveTodo = useCallback(
    (id: number) => {
      dispatch({
        type: "REMOVE_TODO",
        payload: id,
      });
    },
    [dispatch]
  );

  return (
    // 상태가 변경되면 context를 구독하는 컴포넌트에게 변화를 알리는 역할을 하는 컴포넌트
    <todoContext.Provider
      value={{
        todoList,
        onAddTodo,
        onRemoveTodo,
      }}
    >
      {children}
    </todoContext.Provider>
  );
};

export default PersonProvider;
  • /src/App.tsx
import { useContext } from "react";
import { todoContext } from "./context/dotoProvider";

function App() {
  const { todoList, onAddTodo, onRemoveTodo } = useContext(todoContext);

  return (
    // 대부분은 index.tsx 최상위에서 씌워줍니다.
    // 그러면 하위의 모든 컴포넌트들에서 todoContext를 사용할 수 있게 됩니다.
    <todoContext>
      <div className="App">
        <h1>ToDoList</h1>

        <ul>
          {todoList.map((todo) => (
            <li key={todo.id}>
              <span>{todo.contents}</span>
              <button type="button" onClick={() => onRemoveTodo(todo.id)}>
                remove todo
              </button>
            </li>
          ))}
        </ul>

        <button type="button" onClick={() => onAddTodo("추가" + Math.random())}>
          add todo
        </button>
      </div>
    </todoContext>
  );
}

export default App;

추가로 정리할 것

  1. key 사용 이유

0개의 댓글