React Hook

Lucy·2023년 3월 16일
0

React

목록 보기
5/7

함수 Component에서 React state와 생명주기 기능을 연동(hook into)할 수 있게 해주는 함수

Hook은 React 버전 16.8부터 새로운 요소로 추가되어, 기존의 React 컨셉을 대체하지 않으면서도 React의 props, state, context, refs, lifecycle 개념에 좀 더 직관적인 API를 제공한다.

기존 React는 Component 간 재사용이 가능한 로직을 붙이는 방법을 제공하지 않는다.
Hook을 통해 기존의 계층의 변화 없이(Class 없이) 상태 관련 로직을 재사용할 수 있게 되어 상태 값 관리가 보다 편리해진다.
그래서 class 내에서는 동작하지 않는다.

Hook 규칙

  1. 최상위에서만 Hook을 호출해야 한다.
    반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하지 말라.
  • 이를 통해 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장되면서, useState와 useEffect가 여러 번 호출되는 중에도 Hook의 상태가 올바르게 유지된다.
  1. React 함수 Component 내에서만 (혹은 Custom Hook에서) Hook을 호출해야 한다.
    일반 JavaScript 함수에서는 Hook을 호출해서는 안 된다.
  • 이를 통해 컴포넌트의 모든 상태 관련 로직을 소스코드에서 명확하게 보이도록 할 수 있다.

ESLint 플러그인

Hook의 2가지 규칙을 강제하는 플러그인으로 Create React App에 기본적으로 포함되어 있다.

설치 방법

npm install eslint-plugin-react-hooks --save-dev

Hook API

State Hook: useState

state를 함수 Component 안에서 사용할 수 있게 해준다.

const [count, setCount] = useState(0);
  • 현재의 state 값과 이 값을 업데이트하는 함수를 쌍으로 반환한다.

  • 초기 state는 첫 번째 렌더링에서만 사용된다.

  • state는 Class Component에서와 달리 객체일 필요가 없고, 숫자나 문자 타입을 가질 수 있다.

Effect Hook: useEffect

React Component 안에서 데이터를 가져 오거나 구독하고, DOM을 직접 조작하는 등의 side effects를 수행할 수 있게 한다.

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

  // componentDidMount, componentDidUpdate와 비슷합니다
  useEffect(() => {
    // 브라우저 API를 이용해 문서의 타이틀을 업데이트합니다
    document.title = `You clicked ${count} times`;
  });
  • React DOM을 바꾼 뒤에 useEffect 함수가 실행된다.

Clean-up을 이용하지 않는 Side-effects

Ex.

  • 네트워크 리퀘스트
  • DOM 수동 조작
  • 로깅

아래의 같은 코드를 비교해보자.

i. Class Component

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>
    );
  }
}

ii. 함수 Component

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>
  );
}

ii.의 경우의 useEffect는 React Component의 모든 렌더링(DOM 업데이트를 수행한; 마운트 + 업데이트) 이후에 수행된다.

Clean-up이 필요한 Side-effects

Ex. 구독(subscription)을 설정해야 하는 경우에는 메모리 누수가 발생하지 않도록 정리가 필요하다.

i. Class Component

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
  	document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentDidUpdate(prevProps) {
    // 이전 friend.id에서 구독을 해지합니다.
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 다음 friend.id를 구독합니다.
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

ii. 함수 Component

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}
  • i. 과 다르게 ii.에서는 업데이트를 위한 특별한 추가 코드가 필요하지 않다.

최적화: 불필요한 re-rendering 방지

i. Class Component

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

ii. 함수 Component

useEffect 두 번째 인자로 배열로써 전달하면, 배열에 해당하는 값들이 동일하다면 useEffect를 건너뛴다.

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // count가 바뀔 때만 effect를 재실행합니다.

심화: useReducer

상태를 관리할 때 컴퍼넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있다.

i. useState

mport React, { useState } from 'react';

function Counter() {
  const [number, setNumber] = useState(0);

  const onIncrease = () => {
    setNumber(prevNumber => prevNumber + 1);
  };

  const onDecrease = () => {
    setNumber(prevNumber => prevNumber - 1);
  };

  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;

ii. useReducer

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

function Counter() {
  const [number, dispatch] = useReducer(reducer, 0);

  const onIncrease = () => {
    dispatch({ type: 'INCREMENT' });
  };

  const onDecrease = () => {
    dispatch({ type: 'DECREMENT' });
  };

  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;

useRef

용도

  1. Component 안에서 조회/수정할 수 있는 변수 관리하는 경우(setTimeout, setInterval을 통해 만들어진 id, 외부 라이브러리를 사용하여 생성된 인스턴스, scroll 위치)에 사용한다.
    • useState와 달리 useRef로 관리하는 변수는 값이 바뀐다고 해서 컴포넌트가 리렌더링되지 않는다.
  2. 직접 DOM을 선택해야 하는 경우(특정 엘리먼트 크기를 가져올 때, 스크롤바 위치를 가져오거나 설정할 때, 포커스를 설정할 때) React에서는 ref를 사용하는데 Hook 함수로는 useRef가 존재한다.

사용 방식

  1. useRef()를 이용하여 ref 객체를 만든다.
  2. 선택하고 싶은 DOM에 ref 값으로 설정해준다.
  3. 그러면 ref 객체의 .current 값이 우리가 원하는 DOM을 가르킨다.

Ex1.

import React, { useRef } from 'react';
import UserList from './UserList';

function App() {
  const users = [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ];

  const nextId = useRef(4);
  const onCreate = () => {
    // 나중에 구현 할 배열에 항목 추가하는 로직
    // ...

    nextId.current += 1;
  };
  return <UserList users={users} />;
}

export default App;

Ex2. 초기화 버튼을 누르면 이름 input에 포커스가 잡힘

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

function InputSample() {
  const [inputs, setInputs] = useState({
    name: '',
    nickname: ''
  });
  const nameInput = useRef();

  const { name, nickname } = inputs; // 비구조화 할당을 통해 값 추출

  const onChange = e => {
    const { value, name } = e.target; // 우선 e.target 에서 name 과 value 를 추출
    setInputs({
      ...inputs, // 기존의 input 객체를 복사한 뒤
      [name]: value // name 키를 가진 값을 value 로 설정
    });
  };

  const onReset = () => {
    setInputs({
      name: '',
      nickname: ''
    });
    nameInput.current.focus();
  };

  return (
    <div>
      <input
        name="name"
        placeholder="이름"
        onChange={onChange}
        value={name}
        ref={nameInput}
      />
      <input
        name="nickname"
        placeholder="닉네임"
        onChange={onChange}
        value={nickname}
      />
      <button onClick={onReset}>초기화</button>
      <div>
        <b>값: </b>
        {name} ({nickname})
      </div>
    </div>
  );
}

export default InputSample;

useMemo

성능 최적화를 위해 연산된 값을 재사용할 때 사용한다.

사용 방식

useMemo((값을 어떻게 연산할지 정의하는 함수), (배열));

사용 예시

다음은 useMemo를 사용하지 않는 것과 사용했을 때의 차이를 보여주는 예시 코드이다.
useMemo를 사용하게 되면 useMemo의 두 번째 인자로 넣은 배열의 내용이 바뀌면 첫 번째 인자로 등록한 함수를 호출해 값을 연산하고, 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용한다.

import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function countActiveUsers(users) {
  console.log('활성 사용자 수를 세는중...');
  return users.filter(user => user.active).length;
}

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]);

  const nextId = useRef(4);
  const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };

  const onRemove = id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users.filter(user => user.id !== id));
  };
  const onToggle = id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  };
  const count = countActiveUsers(users);
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;
import React, { useRef, useState, useMemo } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function countActiveUsers(users) {
  console.log('활성 사용자 수를 세는중...');
  return users.filter(user => user.active).length;
}

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]);

  const nextId = useRef(4);
  const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };

  const onRemove = id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users.filter(user => user.id !== id));
  };
  const onToggle = id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  };
  const count = useMemo(() => countActiveUsers(users), [users]);
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

useCallback

특정 함수를 재사용하며 성능 최적화

사용 방식

useCallback((재사용 함수), (함수에 필요한 값들이 포함된 배열));

Ref

https://ko.reactjs.org/docs/hooks-overview.html

https://ko.reactjs.org/docs/hooks-state.html

https://ko.reactjs.org/docs/hooks-effect.html

https://ko.reactjs.org/docs/hooks-rules.html

https://ko.reactjs.org/docs/hooks-reference.html#usecontext

https://react.vlpt.us/basic/10-useRef.html

https://react.vlpt.us/basic/17-useMemo.html

profile
나아가는 OnlyOne 개발자

0개의 댓글