[프로젝트 회고] React/ Django SPA Website: #2. Custom Hook / Context API

Chaewon Kang·2021년 1월 8일
0

회고

목록 보기
2/3

Custom Hook

프로젝트를 진행하다 보면, 특정한 로직들을 재사용 해야 하는 경우가 자주 생긴다. 이를테면 input을 관리해 주어야 하는 로직의 경우.

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

위 코드와 같이, 어떤 input 엘리먼트에 변화가 발생했을 경우, 타게팅 된 이벤트 객체의 엘리먼트에서 name, value 값을 받아와 input 상태에 추가해 주어야 하는 경우는 엄청나게 자주 쓰인다. 사용자의 submit 이벤트가 일어나는 경우라든가, 로그인/로그아웃을 구현해야 한다든가 하는 경우.

이런 경우에, Custom Hook을 만들어서 사용할 수 있다. 이미 내장된 hook들을 이용해서 원하는 기능을 구현하고, 컴포넌트에서 필요한 값들을 반환해 주면 된다.

예를 들어, inputs를 관리하는 Custom Hook useInputs의 예시를 보자.

import { useState, useCallback } from 'react';

// 해당 input form에서 관리할 초기값을 파라미터로 가져 옴
function useInputs(initialForm) {
	const [form, setForm] = useState(initialForm); 
	// form 이라는 새로운 상태 선언, 초기값은 initialForm
	const onChange = useCallback(e => {
		const { name, value } = e.target;
		setForm(form => ({...form, [name]: value})); // setter
	}, []); // 의존하는 다른 상태 없음.
	const reset = useCallback(() => setForm(initialForm), [initialForm]);

	// 훅에서 만든 값들을 밖으로 내보내 줌.
	// 객체 형태도 가능, 배열 형태도 가능.
	return [form, onChange, reset];
};

export default useInputs;

커스텀 훅 내부에서 관리할 상태인 form에서, 미리 지정한 초기값을 파라미터로 받아온 다음, 훅이 반환하는 onChange를 사용해서 input의 상태 변경 이벤트를 관리하고,
상태는 form에서 조회, 초기화 하고 싶으면 reset을 호출해서 사용할 수 있다.

사용할 때는, 아래와 같이 useInputs의 파라미터로 상태 초기값을 지정해 준다.

const [form, onChange, reset] = useInputs({
	username: '',
  	email: '',
})

const { username, email } = form;

onCreate 이벤트가 일어날 때 reset(); 함수를 이용해서 인풋 창을 초기화 시켜줄 수 있다. useCallback으로 최적화를 진행하고 있을 경우, deps 배열에 custom hook 에서 반환한 값(reset)을 포함시켜 준다.

useInputs에서 setter 함수가 두 번 이용되고 있으니까, useReducer을 이용하는 것이 더 효율적일 수 있겠다. 만약 이 useInputs를, useState대신 useReducer을 써서 구현하려면 어떻게 해야 할까? 같은 custom hook을 아래와 같이 쓸 수 있다.

import { useReducer, useCallback } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "CHANGE":
      return {
        ...state,
        [action.name]: action.value,
      };
    case "RESET":
      return Object.keys(state).reduce((acc, current) => {
        acc[current] = "";
        return acc;
      }, {});
    default:
      return state;
  }
}

function useInputsReducer(initialForm) {
  const [form, dispatch] = useReducer(reducer, initialForm);

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

  const reset = useCallback(() => {
    dispatch({
      type: "RESET",
    });
  }, []);

  return [form, onChange, reset];
}

export default useInputsReducer;

우선, useInputsReducer 함수 바깥에 reducer 함수를 작성 해 준다. 액션 타입은 'CHANGE', 'RESET' 두 가지로 분기한다. 'CHANGE' 액션이 일어날 때는, 상태의 불변성을 지켜 주고, action에서 받아온 name과 value를 이용하여 상태 업데이트를 해 준다. 'RESET' 액션이 일어날 때는, state 객체의 key 값 배열에 대하여 reducer 함수를 적용해 준다. 콜백의 반환값 acc 배열 현재 요소에 대하여 모두 공백 처리를 하고, 배열을 반환한다.

Array.prototype.reduce()

reduce() 메서드는 배열의 각 요소에 대해 주어진 리듀서(reducer) 함수를 실행하여, 하나의 결과값을 반환한다.

arr.reduce(callback[, initialValue])

callback 파라미터는 배열의 각 요소에 대해 실행할 함수이다. 네 가지 인수를 받는다.

accummulator(acc): 콜백의 반환값을 누적한다.
currentValue(cur): 처리할 현재 요소.
currentIndex(idx): 처리할 현재 요소의 인덱스. initialValue를 제공한 경우 0, 아니면 1부터 시작한다.
array(arr): reduce()을 호출한 배열.
initialValue(optional): callback의 최초 호출에서 첫 번째 인수에 제공하는 값. 초기값을 제공하지 않으면 배열의 첫 번째 요소를 사용한다.

리턴 값은 누적 계산의 결과값이다.

출처

이후, useInputsReducer 함수에서 기존의 onChange, reset 함수에 대해 dispatch를 적용하여 액션을 디스패치하고, 액션에 필요한 값들을 함께 전달 해 준다. 그러면 디스패치된 액션 타입에 맞추어 액션 발생 함수인 리듀서에서 필요한 작업들을 수행해 준다.

Context API

Context API를 사용하여도 전역 상태를 관리할 수 있다. 컴포넌트간의 종속관계가 강해질수록, 컴포넌트의 구조가 복잡해지고, props를 건네주는 다리 역할을 하는 컴포넌트들이 많아진다. 이런 경우, 필요한 컴포넌트에서 필요한 상태를 바로 바로 접근할 수 있게 해 주면 훨씬 효율적이다.

이런 경우, 징검다리처럼 말고 바로 다이렉트로 필요한 상태를 전달해 주고 싶을 때, context API를 사용하면 된다.

사용법은 아래와 같다.

import React, { createContext, useContext, useState } from 'react';

const MyContext = createContext('defaultValue');

우선 createContext, useContext를 불러오고, createContext를 실행한 뒤 초기값을 설정해 준다. 그리고 필요한 컴포넌트에서,

const value = useContext(MyContext);

이렇게 해 주면, context에서 지정한 value값을 사용해 줄 수 있다. 엄청 간단하다! 만약, 어떤 컴포넌트에서 MyContext의 값을 수정하거나 지정해 주고 싶다면, context를 사용하는 가장 상위 컴포넌트에서 MyContext 내부에 있는 Provider 라는 컴포넌트를 사용해 주어야 한다.

<MyContext.Provider value="지정하고자 하는 값">
  blah blah...
</MyContext.Provider>

그래서, 부모-자식 관계로 연결되어 있는 컴포넌트들에서 가장 하위 컴포넌트 또는 그 안에 속해있는 어떤 컴포넌트에서 MyContext를 이용하고자 한다면, 가장 상위 컴포넌트에서 Provider을 통해 value를 지정해 주면 된다.

정리하자면, 새로운 context를 만들 때는 createContext 함수를 사용하고, 파라미터로 컨텍스트의 기본 값을 설정해 준다. 만약 이 기본 값을 다른 값으로 설정해 주고자 한다면, createContext로 지정해 준 상수 내부의 Provider 컴포넌트를 이용해서 value를 지정해 주면 된다.

예를들어, useState로 value값을 관리해주고, 초기값으로 true를 설정해 준다.

const [value, setValue] = useState(true);
return {
	<myContext.Provider value={value ? 'TRUE' : 'FALSE'>
    	<GrandParent />
      	<button onClick={() => setValue(!value)}>CLICK ME</button>
    </MyContext.Provider>
};

GrandParent - Parent - Child 의 관계로 이루어 져 있다면, 최상위 컴포넌트에서 value를 지정하면 이 value가 유동적으로 바뀌면서 Child 컴포넌트에서 사용할 수 있게 된다.

만약 특정 함수를 여러 컴포넌트에 거쳐서 전달해 주어야 하는 경우가 있다면, dispatch를 관리하는 context를 만들어서, 필요한 곳에서 useContext로 불러와서 사용해 주면 아주 깔끔하다!

profile
문학적 상상력과 기술적 가능성

0개의 댓글