[MGS 3기 - 24일차] React 심화 (3)

박철연·2022년 5월 13일
0

MGS STFE 3기

목록 보기
21/35

24일차에도 저번 시간에 이어, React 공식 문서를 바탕으로 React 심화 개념을 학습해보았습니다. 이번 시간에는 Hooks들을 본격적으로 다루고, memoization이나 portal과 같은 개인적으로 학습해 본 적 없는 개념들을 공부하게 되어 시간 투자를 필연적으로 많이 하게 되었습니다. 본 정리글을 교재 삼아서 지속적인 복습을 해야할 듯 합니다.

React Hooks

Hooks의 개념

Hook은 React 버전 16.8부터 새롭게 추가된 기능입니다.

Hook을 이용하면 기존에 Class 바탕의 코드를 작성할 필요 없이 상태 값이나 다른 여러 React의 기능을 손쉽게 사용할 수 있습니다.

좀 더 직관적으로, 복잡한 로직을 구현할 필요없이 단축키처럼 리액트의 특정 기능을 활용할 수 있는 수단이라고 볼 수도 있겠습니다.

정리하자면, Hook은 props, state, context, refs, 그리고 lifecycle와 같은 React 개념에 좀 더 직관적인 API를 제공하기 위해 존재합니다.

React v16.8.0이 Hook을 지원하는 첫 번째 버전이며, React Native는 v0.59부터 Hook을 지원합니다.

참고로, 이번 정리글에서는 가장 자주 활용되는 useState와 useEffect를 통해 hook의 유용함을 이해하는 데 중점을 두고, 다른 종류의 hook들에 대해서는 개별 포스팅으로 요약할 계획입니다.

useState

리액트를 처음 학습할 때, state에 대해 파악한 바가 있습니다.

state는 리액트 내에서 동적 렌더링를 가능하게 해주는 일종의 변수인데, 이 state를 선언하고 사용할 때 useState hook을 사용할 수 있습니다.

아래는 간단한 useState 사용 예시 입니다.

import React, { useState } from 'react';

function Example() {
  // 새로운 state 변수 선언
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

먼저 react 패키지로부터 useState를 불러와야 hook을 사용할 수 있습니다. (이는 거의 대부분의 hook들에 해당됩니다.)

그리고 디스트럭쳐링 할당의 형태로 useState를 사용해주었습니다.

useState()안에 인자로 넣는 값은 최초로 state에 할당할 값입니다. 위 예시에는 0이므로, count라는 state에는 최초값으로 0이 들어갈 것입니다.

또한 count뒤에 나오는 setCount는 setState 함수입니다. 리액트에서는 state가 의도치 않게 변하는 상황을 막고, state 조작을 쉽게 하기 위해 setState 함수를 통해서만 state를 변경할 수 있게 합니다.

setState 함수, 즉, 위의 예시에서는 setCount 함수만이 count를 변경시킬 수 있습니다. 어떤 값이 되었든 setCount() 안에 인자로 들어가는 값이 현재의 count 상태값을 대체합니다.

위 예시에서는 count + 1을 넣어주었기 때문에, 클릭을 할 때 마다 count의 숫자는 1씩 증가할 것입니다.

참고로, useState 없이 class 문법으로 state를 사용하는 방식은 아래와 같습니다.

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

한눈에 봐도, [state, setState] = useState()의 형식이 더 단순하고 가독성도 좋습니다.

useEffect

useEffect hook은 함수형 컴포넌트들에서 side effect를 조작할 수 있게 해줍니다.

데이터 가져오기, 구독(subscription) 설정하기, 수동으로 React 컴포넌트의 DOM을 수정하는 것까지 이 모든 것이 side effect에 해당합니다.

리액트 기초를 학습할 때 접했던 생명 주기의 개념을 접목시켜보면, useEffect Hook을 componentDidMount와 componentDidUpdate, componentWillUnmount가 합쳐진 것으로 이해할 수도 있습니다.

useEffect 사용 예시

clean-up이라 함은 실행 후에 신경써야 하는 부분을 뜻한다고 볼 수 있습니다. 말 그래도 실행 이후 추가적인 처리가 필요없는 기능/동작들입니다.

네트워크 리퀘스트, DOM 수동 조작, 로깅 등은 정리(clean-up)가 필요 없는 경우들입니다.

리액트의 class 컴포넌트에서 render 메서드 그 자체는 side effect를 발생시키지 않습니다. 이때는 아직 이른 시기이므로, 이러한 effect를 수행하는 것은 React가 DOM 구조를 업데이트하고 난 이후에 이루어집니다.

아래는 class 문법을 통해 렌더링 직후 문서 타이틀을 수정하는 내용의 코드입니다.

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

코드를 살펴보면, 컴포넌트가 mount 되었을 때와 update가 되었을 때에 동일한 작업을 수행하도록 한 것을 볼 수 있습니다.

다만, 클래스 컴포넌트에서는 저 둘을 한 번에 묶을 방법이 없기 때문에, 완벽하게 동일한 동작임에도 둘을 나눠서 작성해줄 수 밖에 없습니다.

이러한 상황을 useEffect는 해결해 줄 수 있습니다.

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect Hook을 이용하면 React에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 전할 수 있습니다. React는 우리가 넘긴 함수를 기억했다가(이 함수를 ‘effect’라고 부릅니다.) DOM 업데이트를 수행한 이후에 불러낼 것입니다.

또한, useEffect를 컴포넌트 내부에 둠으로써 effect를 통해 count state 변수(또는 그 어떤 prop에도)에 접근할 수 있게 됩니다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있는 것입니다.

Hook 규칙

최상위 레벨에서만 호출할 것

반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출해서는 안됩니다.

대신 초기 return이 실행되기 전에 항상 React 함수의 최상위 레벨에서 Hook을 호출해야 합니다.

이 규칙을 따르는 이유는 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장되기 때문입니다.

이러한 점은 React가 useState 와 useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있도록 해줍니다.

오직 React 함수 내에서 Hook을 호출

Hook을 일반적인 JavaScript 함수에서 호출하는 대신, 아래와 같이 호출할 수 있습니다.

1.React 함수 컴포넌트에서 Hook을 호출
2. Custom Hook에서 Hook을 호출

이 규칙을 지키면 컴포넌트의 모든 state 관련 로직을 소스 코드 단에서 명확하게 보이도록 할 수 있습니다.

합성 (Composition)

합성(composition)은 서로 다른 컴포넌트 간에 부모-자식 관계를 형성하는 과정에서 코드를 재사용할 수 있도록 결합 관계를 구성하는 것입니다.

React에서는 상속을 활용하는 것도 가능하지만, 상속의 개념으로 인해 발생하는 몇몇 문제점들을 해결하기 위해서 composition의 개념이 등장하게 되었습니다.

컴포넌트에서 다른 컴포넌트를 담기

어떤 컴포넌트들은 어떤 자식 엘리먼트가 들어올 지 미리 예상할 수 없는 경우가 있습니다. 특히, container 역할을 하는 것들이 대표적인 사례라고 볼 수 있을 것입니다.

이러한 컴포넌트에서는 특수한 children prop을 사용하여 자식 엘리먼트를 출력에 그대로 전달하는 것이 좋습니다. 아래를 참고해 보세요.

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

...

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

FancyBorder 태그 안에 있는 것들이 FancyBorder 컴포넌트의 children prop으로 전달됩니다. FancyBorder는 {props.children}을 div 안에 렌더링하므로 전달된 엘리먼트들이 최종적으로 출력됩니다.

흔하진 않지만 종종 컴포넌트에 여러 개의 “구멍”이 필요할 수도 있습니다. 이런 경우에는 다음 코드 블럭과 같이 children 대신 자신만의 고유한 방식을 적용할 수도 있습니다.

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

...

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

특수화

때때로 어떤 컴포넌트의 “특수한 경우”인 컴포넌트를 고려해야 하는 경우가 있습니다. 예를 들어, WelcomeDialog는 Dialog의 특수한 경우라고 할 수 있습니다.

React에서는 이 역시 합성을 통해 해결할 수 있습니다. 더 구체적인 컴포넌트가 일반적인 컴포넌트를 렌더링하고 props를 통해 내용을 구성합니다.

이를 특수화라고 지칭하며, 다음과 같은 형태로 작성할 수 있습니다.

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </FancyBorder>
  );
}

function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thank you for visiting our spacecraft!" />
  );
}

고차 컴포넌트 (HOC)

고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술입니다.

고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수라고 요약할 수 있습니다.

컴포넌트는 props를 UI로 변환하는 반면에, 고차 컴포넌트는 컴포넌트를 새로운 컴포넌트로 변환합니다.

고차 컴포넌트(HOC)는 Redux의 connect와 Relay의 createFragmentContainer와 같은 서드 파티 React 라이브러리에서 흔하게 찾아볼 수 있습니다.

profile
Frontend Developer

0개의 댓글