컴포넌트에서 필요한 외부 데이터들은 컴포넌트 내부에서 useAsync 같은 Hook을 사용해서 작업하면 충분하지만, 가끔 특정 데이터들은 다양한 컴포넌트에서 필요하게 될 때도 있는데 (예: 현재 로그인 된 사용자의 정보 등,,)
그럴 때 Context를 사용하면 개발이 편해진다.
Context 를 여러 번 이용해봤지만 자꾸 잊어먹어서 기억 되살리기 겸 다시 공부해보는 겸 기록해 봅니다.,,
어쩜 볼 때 마다 새로운지 ㅎㅎ 눈물 ,,,,,,, 😇
src 폴더에 UsersContext.js 파일을 만들고, 다음 코드의 주석을 꼼꼼히 읽어가며 코드 따라 작성해보기!
import React, {CreateContext, useReducer, useContext} from 'react';
// UsersContext 에서 사용할 초기 상태
const initialState = {
users: {
loading: false,
data: null,
error: null
},
user: {
loading: false,
data: null,
error: null
}
};
// 성공 시, 상태 만들어주는 함수
const success = data => ({
loading: false,
data,
error: null
})
// 실패 시, 상태 만들어주는 함수
const error = data => ({
loading: false,
data: null,
error: error
})
// 위에서 만든 객체 / 유틸 함수들을 사용하여 리듀서 작성
function usersReducer(state, action){
switch (action.type){
case 'GET_USERS':
return {
...state,
users: loadingState
};
case 'GET_USERS_SUCCESS':
return {
...state,
users: success(action.data)
};
case 'GET_USERS_ERROR':
return {
...state,
users: error(action.error)
};
case 'GET_USER':
return {
...state,
user: loadingState
};
case 'GET_USER_SUCCESS':
return {
...state,
user: success(action.data)
};
case 'GET_USER_ERROR':
return {
...state,
user: error(action.error)
};
default:
throw new Error(`Unhanded action type: ${action.type}`);
}
}
// State 용 Context와 Dispatch용 Context 따로 만들기
const UsersStateContext = createContext(null); /* Context용 Context */
const UserDispatchContext = createContext(null); /* Dispatch용 Context */
// 위에 선언한 두가지 Context 들의 Provider로 감싸는 컴포넌트
export function UsersProvider({children}) {
const [state, dispatch] = useReducer(usersReducer, initalState);
return (
<UsersStateContext.Provider value={state}>
<UsersDispatch.Provider value={dispatch}>
{children}
</UsersDispatch.Provider>
</UsersStateContext.Provider>
);
}
// State를 쉽게 조회할 수 있게 해주는 커스텀 훅(Hook)
export function useUsersState(){
const state = useContext(UsersStateContext);
if(!state) {
throw new Error('Cannot find UsersProvider');
}
return state;
}
// Dispatch를 쉽게 사용할 수 있게 해주는 커스텀 훅(Hook)
export function useUserDispatch() {
const dispatch = useContext(UsersDispatchContext);
if(!dispatch) {
throw new Error('Cannot find UsersProvider');
}
return dispatch;
}
만약 id를 가지고 특정 사용자의 정보를 가져오는 API를 호출하고 싶다면 아래와 같은 형식으로 작성해야 한다.
dispatch({ type: 'GET_USER' });
try {
const response = await getUser();
dispatch({ type: 'GET_USER_SUCCESS', data: response.data });
} catch (e) {
dispatch({ type: 'GET_USER_ERROR', error: e });
}
위의 코드 설명 : 요청이 시작했을 때 action을 dispatch 해주고, 요청이 성공하거나 실패했을 때 또 다시 dispatch 해주는 것임 !
💡 여기서 reducer란 ?
⇒ 현재 상태(state)와 액션 객체(action)를 파라미터로 받아와서 새로운 상태를 반환해주는 함수
⇒ reducer 에서 반환하는 상태(state)는 곧 컴포넌트가 지닐 새로운 상태
⇒ reducer 에서 action은 업데이트를 위한 정보를 가지고 있음 (주로 type 값을 지닌 객체 형태로 사용하나, 꼭 따라야 할 규칙은 없음. 대부분 대문자와 _ 로 구성하는 관습)
useReducer 사용법 ?
const [state, dispatch] = useReducer(reducer, initalState);
state ⇒ 앞으로 컴포넌트에서 사용할 수 있는 상태
dispatch ⇒ 액션을 발생시키는 함수
위의 작업을 처리하는 함수를 만들어 주자.
UsersContext.js
를 열어서 상단 axios를 import 해오고, 코드의 하단 부분에 getUers
와 getUser
함수를 작성한다.
이 함수들은 dispatch
를 parameter 로 받아오고, API에 필요한 parameter 도 받아오게 된다.
/* UsersContext.js */
import React, { createContext, useReducer, useContext } from 'react';
import axios from 'axios';
export async function getUsers(dispatch) {
dispatch({ type: 'GET_USERS'});
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
)
dispatch({ type: 'GET_USERS_SUCCESS', data: response.data });
} catch (e) {
dispatch( { type: 'GET_USERS_ERROR', error: e } )
}
}
export async function getUser(dispatch, id) {
dispatch({type: 'GET_USER'});
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users/${id}'
)
dispatch({ type: 'GET_USER_SUCCESS', data: response.data });
} catch (e) {
dispatch( { type: 'GET_USERS_ERROR', error: e } )
}
}
뒤에서 위의 이 부분은 코드 리팩토링하여 작성해둠. 이해를 위해 여기서는 이대로 두겠슴다.
맨 위에 만들어 둔 Context.js 를 사용해보자. App.js 를 열어서 UsersProvider로 감싸준다.
import Users from './Users';
import { UsersProvider } from './UsersContext';
const App () => {
return (
<UsersProvider>
<Users />
</UsersProvider>
);
}
export default App;
그 다음, Users 컴포넌트의 코드를 Context 를 사용하는 형태로 코드를 전환.
import React, { useState } from 'react';
import { useUsersState, useUsersDispatch, getUsers } from './UsersContext';
import User from './User';
const Users = () => {
const [userId, setUserId] = useState(null);
const state = useUsersState();
const dispatch = useUsersDispatch();
const { data: users, loading, error} = state.users;
const fetchData = () => {
getUsers(dispatch);
};
if(loading) return <div>loading ...</div>;
if(error) return <div>Error !</div>;
if(!users) return <button onClick={fetchData}>불러오기</button>;
return (
<>
<ul>
{users && users.map(user => (
<li key={user.id} onClick={() => setUserId(user.id)}>
{user.username} ({user.name})
</li>
)}
</ul>
<button onClick={fetchData}>다시 불러오기</button>
{userId && <User id={userId} />}
</>
);
}
export default Users;
useUsersState()
와 useUsersDispatch()
를 사용해서 state
와 dispatch
를 가져오고, 요청을 시작할 때는 getUsers()
함수 안에 dispatch
를 넣어서 호출해주었음.,,
함수 명이 비슷하다보니 점점 헷갈리기 시작한다 …….^^
중도포기 하지말고 끝까지 해봅시다 …….!!!!!!!! 😇
import React, { useState } from 'react';
import { useUsersState, useUsersDispatch, getUser } from './UsersContext';
const User = ({id}) => {
const state = useUsersState();
const dispatch = useUsersDispatch();
useEffect(() => {
getUser(dispatch, id);
}, [dispatch, id];
const { data: user, loading, error} = state.user;
if(loading) return <div>loading ...</div>;
if(error) return <div>Error !</div>;
if(!user) return null;
return (
<>
<h2>{user.username}</h2>
<p>
<h2>Email: {user.email}</h2>
</p>
</>
);
}
export default User;
반복되는 로직을 함수화하여 재사용성 있게 코드를 다시 작성해봅시당
아까 리팩토링 한다고 한 코드 일부를 가져와보겠습니다.
/* UsersContext.js */
import React, { createContext, useReducer, useContext } from 'react';
import axios from 'axios';
export async function getUsers(dispatch) {
dispatch({ type: 'GET_USERS'});
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
)
dispatch({ type: 'GET_USERS_SUCCESS', data: response.data });
} catch (e) {
dispatch( { type: 'GET_USERS_ERROR', error: e } )
}
}
export async function getUser(dispatch, id) {
dispatch({type: 'GET_USER'});
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users/${id}'
)
dispatch({ type: 'GET_USER_SUCCESS', data: response.data });
} catch (e) {
dispatch( { type: 'GET_USERS_ERROR', error: e } )
}
}
위의 코드는 앞 부분에 작성했던 코드입니다. 이 부분을 리팩토링 해볼게요 !
먼저, api 들이 들어있는 파일들을 따로 분리해주겠습니다. src 폴더에 api.js 를 만듭니당.
import axios from 'axios';
export async function getUsers() {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
}
export async function getUser(id) {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return response.data;
}
그 다음, scr 폴더에 asyncActionUtils.js 라는 파일을 만들고 아래와 같이 코드를 작성해줍니다.
export async function createAsyncDispatcher(type, promiseFn) {
// success, error에 대한 action 타입 문자열 작성
const SUCCESS = `${type}_SUCCESS`;
const ERROR = `${type}_ERROR`;
// 새로운 함수 생성
// ...rest 사용하여 나머지 parameter를 rest 배열에 담기
async function actionHandler(dispatch, ...rest) {
dispatch({ type }); // 요청 시작
try {
const data = await promisFn(...rest); // rest 배열을 spread 로 넣어주기
dispatch({
type: SUCCESS,
data
}); // SUCCESS
} catch (e) {
dispatch({
type: ERROR,
error: e
}); // ERROR
}
}
return actionHandler; // 만든 함수 리턴
}
import React, { createContext, useReducer, useContext } from 'react';
import createAsyncDispatcher from './createAsyncDispatcher';
import * as api from './api'; // api 파일에서 내보낸 모든 함수들을 불러옴
(...)
export const getUsers = createAsyncDispatcher('GET_USERS', api.getUsers);
export const getUser = createAsyncDispatcher('GET_USER', api.getUser);
Context API 이용하여 외부 API 와 연동하기 끝 !
(리팩토링 할 부분이나 덧붙일 부분이 있으면 추후에 추가하도록 하겠숨 ,,, 이만 할 일이 산더미여서 ,,,,)