Project: React/ Django SPA Website: #1. Hook

Chaewon Kang·2021년 1월 6일
0

Project

목록 보기
2/3

Hook이 뭔가요?

Hook은 함수형 컴포넌트에서 리액트의 상태(State)와 생명주기 기능 (Lifecycle features)를 연동(hook into)할 수 있게 해 주는 함수입니다. - 리액트 공식 문서

(왜 처음부터 공식 문서로 공부하지 않았을까?)

Hook은 클래스 안에서는 동작하지 않는다.
Hook은 useState, useEffect, useMemo 등의 내장 Hook을 제공한다.
Hook을 직접 만드는 것도 가능하다.

우선 내장 Hook들을 먼저 공부하고, 컴포넌트 사이에 상태 관리 로직을 재사용할 수 있게 커스텀 Hook을 직접 만드는 법에 대해서도 알아보자.

useEffect

리액트 컴포넌트 내에서 데이터를 가져오거나, 구독하거나, DOM 엘리먼트를 조작하는 작업이 많다. 다른 컴포넌트에 영향을 주거나, 렌더링 과정에서 구현할 수 없는 이러한 작업들을 side effects(effects)라고 한다.

아래 내가 작성한 클래스형 컴포넌트를 보자.
우선, constructor을 이용하여 props들을 상속해 주고, 컴포넌트에서 사용할 상태의 초깃값을 지정해 주고 있다.

그리고, componentWillMount 블럭의 경우, 컴포넌트가 마운트되기 전에, 지정된 컬러들의 조합에서 랜덤하게 하나의 조합 (배경 색+테두리 색)을 골라서, 각 컴포넌트의 스타일을 지정한다.

이후 렌더링이 될 때, 조합한 스타일을 각각의 컴포넌트에 삽입하여 지정한다.

import React, { Component } from "react";
import { PostModule } from "../../components";
import "./Post.css";

class Post extends Component {
  constructor(props) {
    super(props);
    this.state = {
      style: {
        color: null,
        borderColor: null,
      },
    };
  }

  UNSAFE_componentWillMount() {
    const colorArray = [
      "#A3B3C4",
      "#00F5C6",
      "#93F421",
      "#9452FF",
      "#FDFBC1",
      "#BC791E",
      "#00C4FF",
      "#FF3333",
      "#FF01FF",
      "#DEADF0",
      "#9099FF",
      "#3EA455",
      "#FECC99",
      "#959B01",
      "#CDCC33",
    ];

    const borderColorArray = [
      "#78A4B7",
      "#47D2DD",
      "#64CB0C",
      "#6E12D6",
      "#CFD372",
      "#935B0F",
      "#094EFF",
      "#B74A6C",
      "#E00000",
      "#BB12D8",
      "#6F55FF",
      "#0F7946",
      "#FD9191",
      "#6F55FF",
      "#A8B419",
    ];

    const randomIndex = Math.floor(Math.random() * 15);

    const selectedColor = colorArray[randomIndex];
    const selectedBorderColor = borderColorArray[randomIndex];

    this.setState({
      style: {
        ...this.state.style,
        color: selectedColor,
        borderColor: selectedBorderColor,
      },
    });
  }

  render() {
    const { title, id, category, date } = this.props;

    const style = {
      backgroundColor: this.state.style.color,
      border: `2px solid ${this.state.style.borderColor}`,
    };
    return (
      <>
        <PostModule
          style={style}
          title={title}
          id={id}
          category={category}
          date={date}
        ></PostModule>
      </>
    );
  }
}

export default Post;

useEffect 훅은 함수형 컴포넌트 내에서 이런 효과들을 수행할 수 있게 해 준다. 내가 작성한 위의 클래스형 컴포넌트에서의 생명주기 함수들 ComponentDidMount, ComponentDidUpdate, ComponentWillUnmount 등과 동일한 목적으로 제공된다. 그러나 useEffect 하나의 API 만으로 이를 구현할 수 있다.

클래스형 컴포넌트의 경우, 어떤 효과들을 설정할 때, render() 메서드 자체로는 이를 수행할 수 없다. 어떤 효과들을 수행하는 시점은 리액트가 DOM을 업데이트 하고 난 이후 (컴포넌트의 가장 첫 렌더링의 경우에는, 컴포넌트를 마운트하여 JSX를 표시하고 난 이후) 이기 때문이다.

컴포넌트가 업데이트 된 것인지, 마운트 된 것인지를 구분할 수 없는 경우 어떤 효과를 두 생명주기 함수 안에 중복해서 작성해야 한다.

리액트에서 중복은 권장되지 않는다! 모든 언어, 모든 라이브러리에서 마찬가지다. 코딩의 제 1 원칙은 효율성 이기 때문.

그럼 useEffect 훅을 사용해서 이 컴포넌트를 재작성하기 전에, 먼저 개념을 알아보자.

useEffect가 하는 일

두 가지로 나누어 생각해 볼 수 있다.
정리(Clean-up)을 이용하지 않는 useEffect와 정리를 이용하는 useEffect

정리라 함은, 리액트가 DOM을 업데이트 한 뒤에 추가로 어떤 코드를 실행해야 하는 경우를 말한다. 만약 실행 이후에 크게 신경쓸 것들이 없는 상태라면 정리 함수를 필요로 하지 않는다.

이 두 가지 경우를 나누어 보기 전에, useEffect의 특징을 알아보자.

useEffect를 이용하면, 컴포넌트가 렌더링 이후에 어떤 일을 수행해야 하는지 명령할 수 있다. 리액트는 useEffect내에 첫번 째 인자로 넘긴 함수를 기억했다가, DOM 업데이트를 수행한 이후에 이 함수를 불러낸다.

useEffect를 컴포넌트 내부에서 호출하여, 함수형 컴포넌트 내의 상태들에 접근할 수 있다. useEffect는 첫번 째 렌더링과 그 이후의 모든 업데이트에서 수행된다. 이제 클래스형 컴포넌트에서의 컴포넌트 마운팅과 컴포넌트 업데이트라는 개념보다, 컴포넌트 렌더링 이후마다 효과가 발생하는 것으로 개념을 수정하면 편하다.

정리가 필요한 useEffect와, 필요하지 않은 경우를 더 자세히 보자.

정리(Clean-up)을 이용하지 않는 useEffect

  • 네트워크 리퀘스트 (HTTP 통신을 이용해 어떤 데이터를 요청하는 경우. 요청해서 받아오는 것만이 목적일 때.)
  • DOM 엘리먼트 수동 조작
  • 로깅

정리(Clean-up)을 이용하는 useEffect

  • 외부 데이터에 구독(subscription)을 설정하는 경우. 즉, 해당 컴포넌트가 아닌 범위에서의 상태가 업데이트 될 때, 그 상태 값을 참조해서 어떤 효과를 실행해야 하는 경우. 혹은 props로 받아온 함수들이 필요한 경우. 등!
    이 경우, 외부 데이터가 변경될 때 변경된 상태에 대한 로직을 제대로 처리해 주지 않아 버그 발생률이 높다고 한다. useEffect는 훅 내에 기본적으로 update 발생 상황을 내포하고 있기 때문에, 따로 업데이트가 되었을 때의 효과를 명시해 주지 않아도 자동으로 정리를 실행하고, 업데이트를 반영한다.

만약 정리가 필요하지 않은 경우에는 useEffect의 첫번 째 인자 함수를 실행한다. 정리가 필요한 경우에는, 첫번 째 인자 함수가 반환한 함수를 추가적으로 실행한다. 공식 문서의 예시를 보자.

useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // effect 이후에 어떻게 정리(clean-up)할 것인지 표시합니다.
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

정리 함수는 반드시 유명함수(named function)일 필요는 없고, 화살표 함수여도 된다. 여러 상태들에 여러 효과를 지정해야 한다면, 관련이 있는 것들끼리 모아서 정리한다.

모든 렌더링 이후에 효과를 재적용 하거나 정리하는 것이 불필요할 때도 있다. 이를테면, 내가 만든 웹사이트의 경우 아래와 같은 코드가 있었다.

  componentDidMount() {
    this.getEachPost(this.props.postId);
  }

  componentDidUpdate(prevProps) {
    if (this.props.postId !== prevProps.postId) {
      this.getEachPost(this.props.postId);
    }
  }

useEffect를 사용하면, 이를 하나로 묶어줄 수 있다.

useEffect(() => {
	this.getEachPost(this.props.postId)
}, [this.props.postId]);
// postId가 바뀔 때만 effect를 재실행한다.

위는, 만약 postId가 100이고 컴포넌트가 리렌더링된 이후에도 여전히 100이라면, 효과를 건너뛴다.

내가 작성한 코드를 useEffect로 수정해 보려 했으나 계속해서 에러가 발생했다. Rendering Limits를 초과한다는 메세지가 떴다. 왜일까?

이 경우에는 여러 개의 포스트를 불러올 때마다 실행하고, 추가적인 정리는 필요하지 않다. 컴포넌트가 마운트 되기 전에 색깔을 지정하고, 마운트 된 이후에는 신경 쓸 일이 아무것도 없다. useState의 경우에는 렌더링 된 '이후'에 실행되기 때문에, 렌더링 되기 '이전'의 색상을 지정해 주기 위해서는 useState가 아닌 useRef를 사용해 주어야 했다.

useRef는 .current 프로퍼티로 전달된 인자로 초기화된 변경 가능한 ref 객체를 반환한다.

useRef Hook

특정 DOM을 선택하는 작업이 리액트에서는 불가하므로 DOM에 직접 접근할 때 주로 쓴다. 특정 DOM 엘리먼트의 크기나 위치 가져오거나, 스크롤바 위치를 가져오거나 설정한다든지, 포커스를 설정한다든지... video.js같은 HTML5 비디오 관련 라이브러리, d3.js 나 chart.js 같은 라이브러리 쓸 때에도 특정 DOM에 라이브러리를 적용하기 때문에 선택할 시 ref를 사용할 수 있다.

특정 DOM을 선택하여, 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 { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value,
    });
  };
  const onReset = () => {
    setInputs({
      name: "",
      nickname: "",
    });
    nameInput.current.focus();
  };
  return (
    <div>
      <input
        name="name"
        placeholder="이름"
        onChange={onChange}
        value={name}
        ref={nameInput}
      ></input>
      <input
        name="nickname"
        placeholder="닉네임"
        onChange={onChange}
        value={nickname}
      ></input>
      <button onClick={onReset}>초기화</button>
      <br></br>
      <b>: </b> {name} {nickname}
    </div>
  );
}

export default InputSample;

useRef 불러오고, 상수에 useRef를 설정해 주고, ref 필드의 값으로 넘겨 준다. 이 엘리먼트의 current 값에 접근해서, 인풋 값을 초기화 했을 시에 포커스를 유지해 준다.

useRef로 컴포넌트 내부의 변수 만들기도 가능하다.

어떤 값을 바꿨을 때, 즉 컴포넌트 내부에서 어떤 상태가 업데이트 되었지만 컴포넌트 리렌더링이 필요하지 않은 경우에는 useState 대신 useRef를 사용한다. 컴포넌트가 리렌더링 될 때 마다 계속 기억할 수 있는 값을 관리할 때도 사용할 수 있음.

setTimeout, setInterval의 id를 기억할 경우, 외부 라이브러리를 사용하여 생성된 인스턴스, 사용자의 Scroll 위치를 알고 있어야 할 때 등. useRef로 관리하는 값은 값이 바뀌어도 컴포넌트가 리렌더링 되지 않는다는 점을 기억.

const users = [
    {
      id: 1,
      username: "velopert",
      email: "public@velopmer.com",
    },
    {
      id: 2,
      username: "tester",
      email: "tester@velopmer.com",
    },
    {
      id: 3,
      username: "liz",
      email: "liz@emxaple.cls",
    },
  ];

  const nextId = useRef(4); 
  // nextId가 바뀐다고 해서 컴포넌트가 리렌더링 되지 않아도 되니까

  const onCreate = () => {
    console.log(nextId.current);
    nextId.current += 1;
  };

예를들어 위의 코드 userList에서 각 user 객체에 대해 id값이 바뀌는 상황에서, 컴포넌트가 굳이 리렌더링 되지 않아도 된다.

Custom Hook

사실 내 경우에는, 클래스형 컴포넌트의 ComponentWillMount 생명주기 함수를 대체하기 위해 필요했다. willMount라는 상수에 useRef로 참조 지점을 지정해 준다. 초기값은 true로 준다. willMount의 현재 상태 .current의 값이 true이면, 각 컴포넌트의 스타일을 지정한다. 그리고 다시 참조값을 false로 만든다.

즉, 컴포넌트가 리렌더링 될 때마다가 아니라, 렌더링 이전에 한 번 사용하고 더 사용할 일이 없을 경우에 대한 커스텀 훅인 셈이다. 리팩토링한 코드는 아래와 같다.

import React, { useState, useRef } from "react";
import { PostModule } from "../../components";
import "./Post.css";

function Post({ title, id, category, date }) {
  const [style, setStyle] = useState({
    backgroundColor: null,
    border: null,
  });

  const colorArray = [
    "#A3B3C4",
    "#00F5C6",
    "#93F421",
    "#9452FF",
    "#FDFBC1",
    "#BC791E",
    "#00C4FF",
    "#FF3333",
    "#FF01FF",
    "#DEADF0",
    "#9099FF",
    "#3EA455",
    "#FECC99",
    "#959B01",
    "#CDCC33",
  ];

  const borderColorArray = [
    "#78A4B7",
    "#47D2DD",
    "#64CB0C",
    "#6E12D6",
    "#CFD372",
    "#935B0F",
    "#094EFF",
    "#B74A6C",
    "#E00000",
    "#BB12D8",
    "#6F55FF",
    "#0F7946",
    "#FD9191",
    "#6F55FF",
    "#A8B419",
  ];

  const randomIndex = Math.floor(Math.random() * 15);
  const selectedColor = colorArray[randomIndex];
  const selectedBorderColor = borderColorArray[randomIndex];

  const willMount = useRef(true);

  if (willMount.current) {
    setStyle({
      ...style,
      backgroundColor: selectedColor,
      border: `2px solid ${selectedBorderColor}`,
    });
  }

  willMount.current = false;

  return (
    <PostModule
      style={style}
      title={title}
      id={id}
      category={category}
      date={date}
    ></PostModule>
  );
}

export default Post;

88줄의 코드가 74줄로 줄었고, 더이상 클래스형 컴포넌트를 이용하지 않아도 된다.

useMemo

이전에 연산된 값 재사용 하기. 주로 성능 최적화의 경우 사용한다. 특정 값이 바뀌었을 때만 특정 함수를 실행해서 처리하도록 하고, 특정한 값이 바뀌지 않았더라면 리렌더링 하기 전의 상태를 재사용 한다.

첫번 째 파라미터는 함수 형태, 두번 째 파라미터는 useEffect에서 사용한 것 처럼 의존성과 관련된 배열.

const count = useMemo(() => countActiveUsers(users), [users]);

예를들어 활성 사용자 수를 세서 반환하는 countActiveUsers라는 함수가 있다고 칠 때, useMemo를 사용하여 countActiveUsers는 deps 배열 내의 users 값이 바뀔 때만 실행하겠다는 의미이다. 필요한 연산을 필요할 때만 사용할 경우에 사용한다. 연관된 컴포넌트가 계속 불필요하게 리렌더링 되는 것을 방지한다.

useCallback

실습에서 onRemove, onToggle, onCreate 등의 함수는 컴포넌트가 리렌더링 될 때마다 함수를 새로 생성한다. 함수를 새로 만드는 것 자체는 메모리나 CPU 등의 리소스를 많이 잡아먹는 작업은 아니지만, 한 번 만든 함수를 재사용할 수 있으면 재사용 하는 것이 좋다.

이전에 만들었던 함수를 새로 만들지 않고 재사용 하기 위해 useCallback을 씀. useMemo와 비슷하지만, 함수를 위한 Hook이다. 만약 props가 바뀌지 않는다면, Virtual DOM을 새로 그리지 않고 이전 리렌더링 결과물을 재사용할 수 있도록 최적화해주기 위해 사용한다.

useMemo를 이용해서 결과값(연산값)을 재사용하는 것을 배웠는데, useCallback도 마찬가지로, 만들어 둔 '함수'를 재사용 할 때 사용한다고 생각하면 편하다. 이 또한 deps 배열이 필요한다.

useCallback을 불러오고, 만들어 둔 함수를 useCallback으로 감싸면 된다.

const onChange = useCallback((e) => {
	const { name, value } = e.target;
    setInputs({
    	...inputs,
        [name]: value
    }, [inputs]);
});

예를들어 위와 같은 onChange 함수에서, 함수에서 의존하고 있는 값들이 무엇인지 찾는다. inputs를 함수 내에서 사용하고 있기 때문에, 두 번째 파라미터 deps 배열에 Inputs를 명시해 준다. 그러면 이 함수는 inputs 값이 변경될 때만 새로 만들어 지고, 그렇지 않을 경우에는 기존에 사용한 함수를 재사용한다. 만약 deps 배열이 없을 경우, 함수 내부에서 해당 값들을 참조하게 될 때, 최신 상태가 아닌 컴포넌트가 처음 만들어 질 때의 옛날 상태를 참조하는 일이 생길 수도 있다.

어떤 값들이 함수와 의존 관계에 있는지를 명확하게 알아야 한다. React Devtools를 이용해서 어떤 컴포넌트들이 어떤 시점에 리렌더링 되는지를 파악할 수 있다.

React.memo

컴포넌트 리렌더링 성능 최적화. 컴포넌트 리렌더링이 불필요할 때 이전에 렌더링 했던 결과를 재사용할 수 있게 성능 최적화를 해 준다.

export default React.memo(CreateUser);

컴포넌트 내보낼 때 React.memo 키워드로 컴포넌트 이름을 감싸 줌.

그리고 컴포넌트 내부에 있는 함수들은, 함수 통째로 감싸서 React.memo를 적용해 줌. 만약 props가 바뀌지 않았을 경우에는 리렌더링을 방지하도록.

const User = React.memo(function User({ user, onRemove, onToggle }) {
	const { username, email, id, active } = user;
	/* deps 배열이 비어 있으면 컴포넌트가 처음 화면에 마운트 될 때만 첫번 째 인자 함수 실행 */
	useEffect(() => {
			console.log('Mounted!');
		return () => {
			console.log('Unmounted!');
		}
	}, []);
	/*
	useEffect(() => {
		...
	}, [user]);
	이렇게 하면 deps 내에 있는 인자들에 변경이 가해지는 모든 경우(모든 업데이트)에 함수가 실행됨.
	컴포넌트 마운팅 시점 뿐만 아니라.
	그리고 cleaner 함수까지 써주면, deps 해당 값이 업데이트 될 떄마다 등록한 함수가 전부 실행됨.

	*/
	return (
		<div>
			<b
			style={{
				color: active ? 'green' : 'black',
				cursor: 'pointer'
			}}
			onClick={() => onToggle(id)}
			>
				{username}
			</b>
			&nbsp;
			<span>({email})</span>
			<button onClick={() => onRemove(id)}>삭제</button>
			{/* onClick에서 함수를 만드는 이유? id값을 파라미터로 받아서 삭제하기 위함 */}
			{/* 그냥 여기서 onRemove 호출하면 컴포넌트 렌더링 시점에 호출돼버려서 다 지워짐 */}
		</div>
	);
});

그런데 개별 user의 상태 업데이트가 될 때, 다른 개별 user들도 리렌더링 되는 것을 방지하려면 props로 전달해 주고 있는 애들과, dependencies 배열 내에 인자들과의 관계를 잘 살펴봐야함.

예를들어 userList 배열에 onToggle, onRemove를 props로 전달해 주는데,onToggle, onRemove같은 아이들이 users 배열을 deps로 갖고 있으면서 useCallback을 사용하고 있다면, users 배열이 변경될 때 (새로운 항목이 추가될 때) onToggle, onRemove 함수들도 리렌더링 되고, 그러면 userList 배열도 리렌더링 된다.

이런 문제를 해결하려면, onToggle, onRemove, onCreate 같은 함수들에서 기존의 users를 참조하면 안 된다. 대신에 useState 훅의 함수형 업데이트를 사용한다. 함수형 업데이트를 사용하면, dependencies의 인자를 users 없이 수정해줄 수 있다.

즉, setUsers(users.concat(user)); 같은 형식으로 되어 있던 useState의 상태 설정 함수를 setUsers(users = > users.concat(user)) 이런 식으로 함수형 업데이트 형식으로 바꾸어 준다. 그러면 이 setUsers에 등록한 callback 함수의 파라미터에서 최신 users를 조회하기 때문에, deps 배열에 users를 넣지 않아도 된다.

export default로 내보낸 컴포넌트를 React.memo(컴포넌트이름) 이렇게 감싸줄 수 있고, 내보내지 않은 함수형 컴포넌트 자체를 React.memo로 감싸줄 수 있다.

두번 째 파라미터로 propsAreEqaul 함수를 쓸 수도 있다.

정리하자면

연산된 값을 재사용 할 때는 useMemo를 사용하고, 특정한 함수를 재사용 할 때는 useCallback을 사용하고, 컴포넌트 자체, 즉 렌더링 결과물을 재사용하기 위해서는 React.Memo를 사용한다.

무조건 useCallback을 사용한다고 성능 개선이 일어나는 것은 아니다. useCallback, useMemo는 정말 사용해서 성능 개선이 일어날 때만 잘 계산해서 사용. React.memo 또한 최적화가 필요한 컴포넌트에 선별적으로 사용한다.

useReducer

컴포넌트 상태 업데이트 시 useState를 사용해서 새로운 상태를 설정해 주었는데, 이 말고도 useReducer Hook으로 상태 업데이트 가능.

useState는 설정하고 싶은 다음 상태를 직접 지정해 주는 방식으로 상태를 업데이트하지만, useReducer은 action 객체를 기반으로 상태를 업데이트 한다. action 객체는 상태를 업데이트 할 때 참조하는 객체이다.

dispatch({
	type: 'INCREMENT',
	diff: 4
})

type이라는 키워드를 통해 어떤 업데이트를 할 지 명시해 줄 수 있다. 또 업데이트 시 참조하고 싶은 다른 값이 있다면 객체 안에 넣을 수 있다.

useReducer을 사용하면, 컴포넌트의 상태 업데이트 로직을 컴포넌트 밖으로 분리할 수 있다. 아니면 다른 파일에 작성해서 불러올 수도 있다.

reducer

상태를 업데이트 하는 함수이다. 어떻게 생겼냐하면...

function reducer(state, action) {
	switch (action.type) {
		case 'INCREMENT': // 액션 타입이 increment 이면
			return state + 1;
		case 'DECREMENT': // 액션 타입이 decrement 이면
			return state -1;
		default:
			return state; // 또는 throw new Error('Unhandled action');
	}
}

reducer 함수의 인자로는 현재 상태와 액션 객체를 받아와서, 새로운 업데이트 된 상태를 반환해 준다. action 객체의 type 값에 따라 처리할 상태 업데이트를 return 다음에 명시해 준다.

useReducer

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

useReducer의 첫번 째 인자로는 위에서 알아 본 reducer 함수를 받아온다. 두번째는 기본값을 넣어 준다. number은 현재 상태를, 두번 째 dispatch는 액션을 발생시키는 함수이다. dispatch는 '보내다'라는 의미를 가지고 있다고 이해하면 쉽다.

dispatch({
	type: 'INCREMENT'
});

useReducer을 사용하려면 가장 먼저, 리듀서 함수를 만든다. 꼭 기억하기. 첫번 째 파라미터에는 state, 두번 째 파라미터는 action. 결과값은 '그 다음의 상태'.

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

reducer(상태 업데이트 함수)는 switch문 안에서 액션 타입 이름에 따라 어떤 작업을 해 줄건지를 분기하여 실행한다.

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

관리하고자 하는 상태와 dispatch를 구조 분해하고, useReducer을 useState처럼 적용해 준다. 첫번 째 파라미터는 우리가 앞서 만든 reducer(상태 업데이트 함수), 두번 째 파라미터는 number에 넘겨 줄 초깃값.

그리고 onIncrease, onDecrease(예시에서의 함수)... 즉 상태 업데이트를 발생시키려고 하는 함수에 각각 dispatch를 적용해 준다. dispatch는 어떤 함수가 실행되었을 때, reducer을 참조해서 액션 타입에 맞게끔 어떤 일을 해 줄건지를 알려 준다.

useReducer vs useState

정해진 답은 없고, 상황에 따라 선별해서 사용.

컴포넌트에서 관리하는 상태가 딱 하나고, 원시 데이터 타입이면 useState로 관리하는 게 편할 수도 있다. 그런데 상태가 여러개고 구조가 복잡할 경우, 혹은 뭔가를 추가하거나 수정하거나 업데이트 할 경우에는 useReducer가 편할 때가 있다.

프로젝트에 적용하기

const initialState = {
	inputs: {...},
    users: {...},
    ...
}

가장 상위 컴포넌트의 바깥 영역에 초기값 설정해 주고, reducer 작성하고, useReducer 적용해 주기.

액션을 발생시키는 함수 reducer

function reducer(state, action) {
	...
    return state;
}

첫번 째 파라미터로는 reducer, 두번 째 파라미터는 상태들의 초기값

function App() {
	const [state, dispatch] = useReducer(reducer, initialState);
  	...
}

그리고 상태 객체에서 필요한 값들을 비구조화 할당을 통해 사용할 수 있게 선언해 준다

const { users } = state;
const { username, email } = state.inputs;

이렇게 한 뒤 필요한 곳에서 참조해서 사용한다.
어떤 효과가 발생할 때 사용할 함수들을 사용한다. useCallback을 사용해서 미리 최적화를 해 준다. 컴포넌트가 처음 렌더링 될 때 함수를 만들고, 그 이후에는 deps 배열 안에 있는 값들이 업데이트 되는 경우에만 사용하도록.

const onChange = useCallback(e => {
	const { name, value } = e.target;
  	dispatch({
    	type: 'CHANGE_INPUT',
      	name,
      	value
    })
}, [])

const onCreate = useCallback(() => {
	disaptch({
    	type: 'CREATE_USER',
      	user: {
        	id: nextId.current,
          	username,
          	email,
        }
    })
  	nextId.current += 1;
}, [username, email]) 
// 함수에서 기존 상태에 의존적이므로 deps에 할당. 만약 ref (참조)를 이용해야 할 경우가 있으면, useRef를 통해 관리해 준다.

const onToggle = userCallback(id => {
	dispatch({
    	type: 'TOGGLE_USER',
      	id
    })
}, []);

const onRemove = userCallback(id => {
	dispatch({
    	type: 'REMOVE_USER',
      	id
    });
},[]);

아래와 같이 썼으면, 발생시키고자 하는 액션에 맞추어 리듀서를 작성해 준다.

function reducer(state, action) {
	swtich (action.type) {
      case 'CHANGE_INPUT':
      	return {
        	...state, // 불변성 유지
          	inputs: {
            	...state.inputs,
              	[action.name]: action.value
            }
        };
      case 'CREATE_USER': // input값 초기화, users 배열 업데이트
      	return {
        	inputs: initialState.inputs,
          	users: state.users.concat(action.user)
        };
      case 'TOGGLE_USER':
      	return {
       		...state,
          	users: state.users.map(user => 
            	user.id === action.id
                ? { ...user, active: !user.active }
                : user
        	)
        };
      case 'REMOVE_USER':
      	return {
        	...state,
          	users: state.user.filter(user => user.id !== action.id)
        }
    }
  default:
  	return state; // or throw new Error('Unhandled Action');
}

작성한 함수들을 하위 컴포넌트에 props로 전달.
활성 사용자 구할 때는 useMemo로 최적화.

	const count = useMemo(() => countActiveUsers(users), [users])
profile
문학적 상상력과 기술적 가능성

0개의 댓글