[TIL] 2022-05-27 / 50일차(React - Hook 1)

미음제

·

2022. 5. 28. 00:25

https://ko.reactjs.org/

 

오늘 배운 내용

 

Day 50의 강의는 여러 컴포넌트를 생성하는 방법(Storybook을 이용한)이었다. 재사용이 가능한 컴포넌트를 생성하고, Storybook에서 결과를 확인하는 식의 방식이었다. Text, Header, Spacer, Image, Spinner 등 자주 사용될 만한 컴포넌트를 생성하는 내용이었고, 각 컴포넌트를 생성하는 함수 내부에서 Hook을 종종 사용했었다.

 

이전 강의(Day 48, 49)에서 '분기와 반복' 그리고 '상태와 이벤트 바인딩'에 대한 내용을 다루면서 Hook을 사용하기 시작했다. 그때 당시에는 대략적인 이해만을 하고 넘어갔었다. 사용 사례 강의나 재사용이 될만한 여러 컴포넌트를 생성하는 것에 대한 강의를 보면서 Hook에 대한 이해가 없이는 무의미하다 싶어 Hook에 대해 짧게나마 공부했다.

 

Hook

 

근데 Hook이 뭔가요?

 

 

Hook 개요 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수입니다. 

class 안에서 작동하지 않습니다. 대신 class 없이 React를 사용할 수 있게 해주는 것입니다.

 

Hook은 React 버전 16.8버전 이후로 새로 추가되었다. 공식문서에 작성된 것처럼, Class 형식의 컴포넌트가 아닌 Function 컴포넌트에서 상태 값과 React의 기능을 사용하기 위해 추가되었다 한다.

 

클래스형과 함수형 컴포넌트

 

// Class Component
import React, { Component } from 'react';

class App extends Component {
  render() {
    const name = 'Hi React';
    return <div>{name}</div>;
  }
}

export default App;

 

// Function Component
import React from 'react';

function App() {
  const name = 'Hi React';
  return <div>{name}</div>;
}

export default App;

 

둘의 역할은 동일하다. 컴포넌트를 생성하고, JSX를 통해 렌더링하는 것. Class 컴포넌트는 상태(state)나 라이프사이클(ex : mount, unmount)을 사용할 수 있고, 내부에서 임의의 메서드를 정의할 수 있다는 것이다. 

 


 

바닐라 JS를 할 당시에도 Class 기반으로 작성하지 않고 Function 기반으로 작성했던 터라 Hook의 도입에 대한 이유를 정확하게 이해는 못했지만 다음과 같은 이유로 Hook을 도입하게 되었다고 한다.

 

Hook은 알고 있는 React 컨셉을 대체하지 않습니다. 대신에, Hook은 props, state, context, refs, 그리고 lifecycle와 같은 React 개념에 좀 더 직관적인 API를 제공합니다. 또한 Hook은 이 개념들을 엮기 위해 새로운 강력한 방법을 제공합니다.

 

  • Class를 사용하려면 Javascript에 대한 이해가 있어야 한다.
  • Class 내부의 lifecycle에서 필요 이상의 로직이 수행되고, 복잡해지며 의존성이 높아진다.
  • 컴포넌트 간 재사용이 어렵다.

 

위와 같은 이유로 Hook을 도입하고, Function 기반 컴포넌트 설계를 편하게 할 수 있도록 도와준다는 것이다.

 

Class 기반으로 사용했던 경험이 없고, React도 처음 접했던 터라 와닿는 이야기는 아니지만 어쨌든 이러한 이유로 인해 Hook이 등장하게 되었다.

 

다시 돌아와서 Hook은 Function 컴포넌트가 Class 컴포넌트에서 사용할 수 있는 기능을 사용할 수 있도록 해주는 기능으로, state와 lifecycle 등의 기능을 사용할 수 있도록 해준다.

 

State Hook

 

Function 컴포넌트에서 상태(state)를 다룰 수 있게 해주는 Hook이다. Class 기반에서 cunstructor()를 통해 state를 정의하고 사용하는 것과 동일하다. 이를 useState()라는 메서드를 통해 사용할 수 있다.

 

컴포넌트 내부에서 사용할 수 있는 state를 정의해주어야 한다. 기본 사용법은 다음과 같다.

 

// 1. 컴포넌트에서 지역 상태 관리하는 법
import { useState } from 'react';

function App() {
  const [count, setCount] = useState(1);
  return (
    <div>
      {count}
    </div>
  );
}

 

구조분해할당으로 할당된 배열의 첫 번째 인자(count)는 state(this.count 같은 개념)가 된다.  두 번째 인자는 state를 업데이트하기 위한 함수(setState() 내부에서 this.count를 변경)이다.

 

useState() 메서드로 숫자 1을 넘겨주었는데, count의 초기값을 지정한 것이다. 이를 바탕으로 화면에 렌더링 된 것을 보면 1이 출력된 것을 확인할 수 있다.

 

state를 변경하기 위해서 setCount 함수를 정의해 두었는데 이를 활용하는 방법은 다음과 같은 예가 될 수 있다.

 

import { useState } from 'react';
function App() {
  const [count, setCount] = useState(1);

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

export default App;

 

button 요소를 추가해주고, onClick 이벤트가 발생하면, setCount 메서드를 호출해, count state를 1씩 증가하도록 한다. 값이 증가하고, 변경된 내용을 화면에서 확인할 수 있다.

 


 

여러 state를 정의하고 활용하려면?

 

useState()를 여러개 선언하여 여러 state를 관리할 수 있다.

 

import { useState } from 'react';
function App() {
  const [count, setCount] = useState(1);
  const [totalClick, setTotlClick] = useState(0);

  const plusClick = () => {
    setCount(count + 1);
    setTotlClick(totalClick + 1);
  };

  const minusClick = () => {
    setCount(count - 1);
    setTotlClick(totalClick + 1);
  };

  return (
    <>
      <div>
        <h2>값 : {count}</h2>
        <button onClick={() => plusClick()}>+</button>
        <button onClick={() => minusClick()}>-</button>
      </div>
      <div>
        <h1>총 클릭한 횟수 : {totalClick}</h1>
      </div>
    </>
  );
}

export default App;

 

count 값을 증감하기 위한 버튼이 있고, 증감 버튼을 총 클릭한 횟수를 표현하기 위한 totalClick state를 정의했다. + 혹은 - 버튼을 클릭하면 count state 값이 변경되고, 클릭하면 totalClick 메서드를 통해 totalClick state 값을 변경할 수 있다.

 

+ 버튼을  3번 클릭하고, - 버튼을 2번 클릭하면 위와 같은 결과가 나타난다.

 

props와 state의 차이점

 

props와 state는 모두 JS 객체이다. props와 state의 변경으로 렌더링이 발생한다. 가장 큰 차이는 수정할 수 있느냐 없느냐이다.

  • props: 부모 컴포넌트로부터 물려받는 데이터로 수정이 불가능하다(자식 컴포넌트에서). read only
  • state: 함수 내부의 지역 변수처럼 사용되어 함수 컴포넌트 내부에서 활용이 가능하다.

 

 

 

Effect Hook

 

사용 방법은 useState()와 동일하게 import 한 후, 사용하면 된다.

 

import { useState, useEffect } from 'react';
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`change count ${count} times`)
  });
  
  return (
    <>
      <div>
        <h2>값 : {count}</h2>
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
    </>
  );
}

export default App;

 

강의 내용을 빌리자면, useEffect() Hook은 무엇인가의 변화가 있을 때를 감지하여 반응하는 Hook이라고 할 수 있다. 첫 번째 파라미터로 반응하는 부분(변화가 있을 때 처리할 로직 / 정확히는 side effects)을 넣고, 두 번째 파라미터로 감지할 대상을 넣는다고 보면 된다.

 

이름이 왜 useEffect인지, side effects를 처리한다는 것이 와닿지가 않았다. 공식문서를 참고해보면 다음과 같이 정의를 하고 있다.

 

컴포넌트 안에서 데이터를 가져오거나 구독하고, DOM을 직접 조작하는 작업을 이전에도 종종 해보셨을 것입니다. 우리는 이런 모든 동작을 “side effects”(또는 짧게 “effects”)라고 합니다. 왜냐하면 이것은 다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서는 구현할 수 없는 작업이기 때문입니다.

Effect Hook, 즉 useEffect는 함수 컴포넌트 내에서 이런 side effects를 수행할 수 있게 해 줍니다. 

 

내용을 참고하여, 컴포넌트가 생성되고 나서 처리해야 할 로직, 데이터가 변경되었을 때의 로직, 컴포넌트의 역할을 다 했을 때의(unmount) 로직을 다루는 Hook이라고 이해를 했다.

 

앞서 Hook을 도입하게 된 배경에서, Function 기반의 컴포넌트에서 life cycle을 다룰 수 있게 함이 있었는데, useEffect Hook이 이 부분을 다룬다고 생각된다.

 

 

라이프사이클 훅 | Vue.js

라이프사이클 훅 Note 모든 라이프사이클 훅은 자동으로 this 컨텍스트가 인스턴스에 바인딩되어 있으므로, data, computed 및 methods 속성에 접근할 수 있습니다. 즉, 화살표 함수를 사용해서 라이프사

v3.ko.vuejs.org

 

Vue를 배울 때에도, life cycle Hook을 사용했었다. 아마 Class 기반의 React 컴포넌트도 Vue와 비슷하게 사용했을 것이다(함수 네이밍 정도의 차이가 있는 것 같다. 직접 사용해보지 않아 정확한 사용법은 모르겠으나, 이름에서 유추해보면 비슷한 것 같다).

 

React 공식 문서의 말을 다시 빌려보면 다음과 같다.

 

React class의 componentDidMount나 componentDidUpdate, componentWillUnmount와 같은 목적으로 제공되지만, 하나의 API로 통합된 것입니다.

 

import { useState, useEffect } from 'react';
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`change count ${count} times`)
  });
  
  return (
    <>
      <div>
        <h2>값 : {count}</h2>
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
    </>
  );
}

export default App;

 

다시 예제를 보면, 버튼을 클릭하게 되면 count 값이 증가가 된다. 그리고 count 값이 h2 태그 내부에 작성되어 화면에 노출된다.

 

useEffect Hook은 count 값의 변경이 있고, 화면에 노출(정확히는 렌더링 결과가 반영된 후)된 후에 실행되어 버튼을 클릭할 때마다 console.log()를 통해 count 값이 출력될 것이다.

 

 

 

useEffect Hook은 매 렌더링마다 실행된다. 처음 렌더링 되었을 때도 실행되기 때문에 count의 초기값인 0일 때도 실행 결과가 출력된 것을 확인할 수 있다(버튼 클릭은 4번만 했을 뿐인데).

 

다시 예제에서, 두 번째 인자로 빈 배열을 추가해 출력해보자.

 

import { useState, useEffect } from 'react';
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`change count ${count} times`)
  }, []);
  
  // 이하 동일
}

 

그리고 버튼을 6번 클릭하면 결과는 처음 useEffect를 사용했던 것과 전혀 다른 결과가 나타난다. 

 

6번의 버튼 클릭으로 count 값이 6번 변경되었음에도 최초 렌더링 되었을 당시에만 useEffect Hook이 실행되었다. 즉, 컴포넌트가 처음 실행되었을 때 딱 한 번만 호출하는 것이다. Vue의 life cycle과 연관 지어 이해해 보자면, mounted 되었을 때 처리할 로직을 작성하면 된다고 생각된다. 최초 mounted가 되었을 때 side effect를 제어하기 위해 사용한다. 

 

가령, 컴포넌트 지역 상태 값이 여럿 존재하고 특정 상태 값의 변경이 있을 경우에만 로직을 수행하고 싶다면 다음과 같이 작성한다.

 

import { useState, useEffect } from 'react';
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`mounted`)
  }, []);
  useEffect(() => {
    console.log(`change count ${count} times`)
  }, [count]);
  
  return (
    <>
      <div>
        <h2>값 : {count}</h2>
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
    </>
  );
}

export default App;

 

두 번째 인자로 넘긴 배열에 상태 값을 요소로 넘겨준다. 예제에서는 count 값 하나만 존재하지만 여럿 존재하는 경우에 어떤 상태 값이 변경되었을 때, 다시 렌더링 된 후 함수를 실행하는 것은 굉장히 비효율적이라고 할 수 있다. 따라서 원하는 상태 값의 변경이 있을 때만 렌더링 후 실행하고 싶다면 위와 같이 배열의 요소로 넘겨주면 된다.

 

cleanup 함수

 

useEffect 내부에서 return문을 실행하면 컴포넌트가 unmounted 되고 처리할 로직을 실행한다.

 

import { useState, useEffect } from 'react';
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('mounted')
    return () => {
      console.log('cleanup useEffect')
      console.log(count)
    }
  },[count])


  return (
    <>
      <h2>{count}</h2>
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

export default App;

 

컴포넌트가 처음 mounted 되고 console.log()를 통해 mounted가 출력된다. 이후 버튼 클릭으로 count 상태 값이 변경될 때마다 useEffect()가 호출되고, 컴포넌트가 unmounted 되면서 cleanup 로직을 실행하게 된다.

 

 

컴포넌트가 mounted 되고, 콘솔 창에 출력된 결과다. 이후 버튼을 한번 클릭하게 되면 다음과 같은 출력문이 나오게 된다.

 

 

 

처음 mounted 되었던 컴포넌트가 unmounted가 되면서 'cleanup useEffect'를 출력했고 cleanup 되기 이전의 값(최초 count 상태 값 = 0)을 출력하고, 새로 mounted 되었다는 출력문이 나오게 되었다. count 상태 값은 1로 증가되어 화면에 렌더링 되었다.

 

return 문을 통해 다음 useEffect가 실행되기 전의 useEffect를 정리하는 것이다. 메모리를 관리하기 위해 컴포넌트를 제거하기 전에 수행한다고 보면 된다. 함수에 사용된 메모리 공간을 반환한다고 생각이 된다. 만약 이벤트를 컴포넌트에 등록해 두었는데, 컴포넌트가 unmounted 되고 이벤트가 남아 있다면 메모리를 반환받지 못해 메모리 누수가 발생할 수 있다.

 

정리해보자면, useEffect Hook의 사용방법은 크게 3가지이다.

 

useEffect(() => {}) // 렌더링될 때마다 동작한다. 최초 mounted 되었을 때부터 동작
useEffect(() => {}, []) // 컴포넌트가 처음 mounted 되었을 때만 동작
useEffect(() => {}, [dep, dep, ...]) // 의존 데이터가 변경될 때만 동작한다.

useEffect(() => {... return () => {/* clean-up */}}) // 컴포넌트가 unmounted된 후 이전 useEffect를 정리

 


 

오늘의 회고

 

지난 강의 내용을 다시 복습했다. 복습을 하면서 조금 더 이해가 된 느낌이다(아직도 많이 부족하지만). 아쉬운 점은 미리 그날그날 복습을 했음 더 좋았을 것 같다는 생각이 든다. Day 50 강의를 들으며 Hook을 사용할 때마다 정지해두고 천천히 생각하려 하니 헷갈리기도 하고, 강의 내용을 온전히 집중하지 못했던 것 같다. 아직 useRef나 custom Hook 등에 대한 내용은 다시 보지 못했다. 주말 내로 해당 내용을 조금이나마 복습하고, 이해하도록 해야겠다.

 

참고

 

 

Hook의 개요 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

 

컴포넌트 State – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

 

강의 : 프로그래머스 데브코스 프론트엔드 2기

반응형