프로젝트를 진행하다 보면, 특정한 로직들을 재사용 해야 하는 경우가 자주 생긴다. 이를테면 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 배열 현재 요소에 대하여 모두 공백 처리를 하고, 배열을 반환한다.
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를 사용하여도 전역 상태를 관리할 수 있다. 컴포넌트간의 종속관계가 강해질수록, 컴포넌트의 구조가 복잡해지고, 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로 불러와서 사용해 주면 아주 깔끔하다!