리액트 훅은 useState, useEffect 등과 같이 상태 관리 및 사이드 이펙트(코드가 의도한 주된 효과 외에 추가적으로 발생하는 부수 효과) 등의 처리를 함수형 컴포넌트에서 간단하게 수행할 수 있도록 도와주는 함수이다. 이러한 훅들을 이용해 반복되는 로직을 하나의 훅으로 구현할 수 있는데, 이를 커스텀 훅이라 한다.
커스텀 훅의 사용 방식은 마치 Redux로 앱 상태 관리자를 따로 두면서 필요한 컴포넌트에 action과 state를 연결하는 방식과 유사하다. 이런 점을 통해 커스텀 훅으로 Redux가 없지만 Redux와 비슷한 방식으로 컴포넌트 간의 상태를 효율적으로 관리할 수 있다.
1. state를 관리하기 위해서 action.js, reducer.js, useStore.jsx 파일을 생성한다.
- action.js : 액션에 대한 형태 및 키워드를 정의하는 곳
- reducer.js : 초기 값과 각 케이스별 로직의 상세 정의
- useStore.jsx: state, dispatch, actions 를 활용한 리듀서 정의
2. action.js에서는 액션에 대한 키워드를 정의한다.
action.js는 액션 타입과 액션 생성자를 정의하는 파일이다. action은 상태에 변화를 주기 위한 이벤트를 정의하는 객체로, type 속성을 가지고 있으며 필요한 정보는 payload라는 속성을 통해 전달된다.
- export const 액션이름명 = "액션타입명";
- export const 액션함수명 = () => ({type: 액션이름명});
action.js
// 액션 타입 정의
// 이름을 변경하는 액션 타입
export const CHANGE_NAME = "CHANGE_NAME";
// 취미를 변경하는 액션 타입
export const CHANGE_HOBBY = "CHANGE_HOBBY";
// 좋아하는 것들을 변경하는 액션 타입
export const CHANGE_MY_FAVORITES = "CHANGE_MY_FAVORITES";
// 액션 생성자 정의
// 이름을 변경하는 액션 타입 반환
export const changeName = () => ({type: CHANGE_NAME});
// 취미를 변경하는 액션 타입 반환
export const changeHobby = () => ({type: CHANGE_HOBBY});
// 좋아하는 것들을 변경하는 액션 타입 반환, 매개변수 받아서 사용
export const changeMyFavorites = (value) => ({type: CHANGE_MY_FAVORITES, payload: value});
3. reducer.js에서는 초기 상태를 설정하고, 각 액션 타입에 따라 상태를 변경하는 로직을 구현한다.
(1) 액션 타입을 import로 가져온다.
import {CHANGE_NAME, CHANGE_HOBBY, CHANGE_MY_FAVORITES } from "./action";
(2) 초기 상태를 설정한다.
const initialState = {
name : "이춘향",
hobby: "코딩",
favorites: {color: "하늘", food: "햄버거"}
}
(3) Reducer 함수를 작성한다.
reducer함수는 state와 action을 매개변수로 받아, switch 문을 통해 액션 타입에 따라 상태를 변경한다. 각 case 문에서 새로운 상태를 반환할 때, 스프레드 연산자(...)를 사용하여 해당 상태를 업데이트 한다.
// reducer.js
import { CHANGE_NAME, CHANGE_HOBBY, CHANGE_MY_FAVORITES } from "./action";
// 초기 상태
const initialState = {
name: "이춘향",
hobby: "코딩",
favorites: { color: "하늘", food: "햄버거" }
};
// state, action을 매개변수로 받음
function reducer(state, action) {
//action type에 따라서 반환값 다르게 설정
switch (action.type) {
case CHANGE_NAME:
return { ...state, name: '김서라' };
case CHANGE_HOBBY:
return { ...state, hobby: '책읽기' };
case CHANGE_MY_FAVORITES:
return {
...state,
favorites: {
...state.favorites,
...action.payload // payload를 활용해 여러 값을 업데이트 가능
}
};
default:
throw new Error("잘못된 접근입니다.");
}
}
export default reducer;
4. useStore.jsx에서는 state, dispatch, actions 를 활용해서 리듀서를 정의한다.
(1) useReducer, initialState, reducer함수 그리고 액션생성자를 가져온다.
import { useReducer } from "react";
import {initialState, reducer} from "./reducer";
import { changeName, changeHobby, changeMyFavorites } from "./action";
(2) 커스텀 훅을 생성한다.
- const [state, dispatch] = useReducer(reducer, initialState, [init]);
- const actions = { 함수명 : () => dispatch(reducer함수())}
import { useReducer } from "react";
import { initialState, reducer } from "./reducer";
import { changeName, changeHobby, changeMyFavorites } from "./action";
export const useStore = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const actions = {
changeName : () => dispatch(changeName()),
changeHobby : () => dispatch(changeHobby()),
changeMyFavorites : (value) => dispatch(changeMyFavorites(value))
};
return { state, actions }; // 상태와 액션을 반환
};
5. 커스텀 훅을 사용한다.
(1) useStore을 가져온다 .
import {useStore} from "../store/useStore"
(2) state와 action을 구조분해할당으로 가져온다.
import {useStore} from "../store/useStore"
const Inner1 = () => {
const {state, actions} = useStore();
}
export default Inner1;
(3) 컴포넌트 내에서 사용한다.
//Inner1.jsx
import { useState } from "react";
import { useStore } from "../store/useStore";
const Inner1 = () => {
const { state, actions } = useStore();
const [color, setColor] = useState("");
const [food, setFood] = useState("");
const handleFavorites = () => {
if (color && food) {
actions.changeMyFavorites({ color, food });
}
else if (color) {
actions.changeMyFavorites({ color });
} else if (food) {
actions.changeMyFavorites({ food });
}
// 입력 필드를 비워줌
setColor("");
setFood("");
};
return (
<div>
<h2>
이름: {state.name} | 취미: {state.hobby} | 좋아하는 것: {state.favorites.color}, {state.favorites.food}
</h2>
<button onClick={() => actions.changeName()}>이름 변경</button>
<button onClick={() => actions.changeHobby()}>취미 변경</button>
<div>
<input
type="text"
value={color}
onChange={(e) => setColor(e.target.value)}
placeholder="좋아하는 색을 입력해주세요."
/>
<input
type="text"
value={food}
onChange={(e) => setFood(e.target.value)}
placeholder="좋아하는 음식을 입력해주세요."
/>
<button onClick={handleFavorites}>좋아하는 것들 변경</button>
</div>
</div>
);
};
export default Inner1;
Redux처럼 해당하는 state가 있는 부분만 렌더링이 되고 다른 컴포넌트는 렌더링이 되지 않는 모습을 볼 수 있다.
'Frontend > React' 카테고리의 다른 글
React : forwardRef에 대해서 (0) | 2024.09.15 |
---|---|
React : Redux에 대해서 알아보자 (0) | 2024.09.09 |
React : Context API에 대해서 알아보자 (0) | 2024.09.06 |
React: Memo & useMemo & useCallback을 통한 성능개선 (0) | 2024.09.06 |
React : Lazy import을 통한 성능 개선 (0) | 2024.09.05 |
React: useReducer에 대해서 (0) | 2024.09.05 |
React: React에서 Styled Component 사용하기 (0) | 2024.09.04 |
React: useEffect에 대해서 (1) | 2024.09.03 |