API 연동의 기본
axios를 사용해서 GET, PUT, POST, DELETE 등의 메서드로 API 요청할 수 있습니다. 간단하게 설명을 드리자면, REST API 를 사용 할 때에는 하고 싶은 작업에 따라 다른 메서드로 요청을 할 수 있는데 메서드들은 다음 의미를 가지고 있습니다.
- GET: 데이터 조회
- POST: 데이터 등록
- PUT: 데이터 수정
- DELETE: 데이터 제거
axios 의 사용법은 다음과 같습니다.
import axios from 'axios';
axios.get('/users/1');
get 이 위치한 자리에는 메서드 이름을 소문자로 넣습니다. 예를 들어서 새로운 데이터를 등록하고 싶다면 axios.post() 를 사용하면 됩니다.
그리고, 파라미터에는 API 의 주소를 넣습니다.
axios.post() 로 데이터를 등록 할 때에는 두번째 파라미터에 등록하고자 하는 정보를 넣을 수 있습니다.
axios.post('/users', {
username: 'blabla',
name: 'blabla'
});
API 연동 실습을 할 때에는JSONPlaceholder 에 있는 연습용 API 를 사용해볼 것입니다.
ttps://jsonplaceholder.typicode.com/users
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv",
"address": {
"street": "Victor Plains",
"suite": "Suite 879",
"city": "Wisokyburgh",
"zipcode": "90566-7771",
"geo": {
"lat": "-43.9509",
"lng": "-34.4618"
}
},
"phone": "010-692-6593 x09125",
"website": "anastasia.net",
"company": {
"name": "Deckow-Crist",
"catchPhrase": "Proactive didactic contingency",
"bs": "synergize scalable supply-chains"
}
},
(...)
]
결과물
useState 와 useEffect 로 데이터 로딩하기
요청에 대한 상태를 관리 할 때에는 다음과 같이 총 3가지 상태를 관리해주어야합니다.
- 요청의 결과
- 로딩 상태
- 에러
Users.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function Users() {
const [users, setUsers] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
// 요청이 시작 할 때에는 error 와 users 를 초기화하고
setError(null);
setUsers(null);
// loading 상태를 true 로 바꿉니다.
setLoading(true);
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
setUsers(response.data); // 데이터는 response.data 안에 들어있습니다.
} catch (e) {
setError(e);
}
setLoading(false);
};
fetchUsers();
}, []);
if (loading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return null;
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.username} ({user.name})
</li>
))}
</ul>
);
}
export default Users;
참고로, useEffect 에 첫번째 파라미터로 등록하는 함수에는 async 를 사용 할 수 없기 때문에 함수 내부에서 async 를 사용하는 새로운 함수를 선언해주어야 합니다.
App.js
import React from 'react';
import Users from './Users';
function App() {
return <Users />;
}
export default App;
에러 발생 확인하기
에러가 발생하는지도 확인해봅시다. 에러가 발생하는것을 확인하기 위하여 주소를 이상하게 바꿔봅시다.
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users/showmeerror'
);
버튼을 눌러서 API 재요청하기
아까 구현했던 fetchUsers 함수를 바깥으로 꺼내주고, 버튼을 만들어서 해당 함수를 연결해주면 됩니다.
Users.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function Users() {
const [users, setUsers] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUsers = async () => {
try {
// 요청이 시작 할 때에는 error 와 users 를 초기화하고
setError(null);
setUsers(null);
// loading 상태를 true 로 바꿉니다.
setLoading(true);
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
setUsers(response.data); // 데이터는 response.data 안에 들어있습니다.
} catch (e) {
setError(e);
}
setLoading(false);
};
useEffect(() => {
fetchUsers();
}, []);
if (loading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return null;
return (
<>
<ul>
{users.map(user => (
<li key={user.id}>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={fetchUsers}>다시 불러오기</button>
</>
);
}
export default Users;
useReducer 로 요청 상태 관리하기
useReducer 를 사용하여 LOADING, SUCCESS, ERROR 액션에 따라 다르게 처리를 해봅시다.
Users.js
import React, { useEffect, useReducer } from 'react';
import axios from 'axios';
function reducer(state, action) {
switch (action.type) {
case 'LOADING':
return {
loading: true,
data: null,
error: null
};
case 'SUCCESS':
return {
loading: false,
data: action.data,
error: null
};
case 'ERROR':
return {
loading: false,
data: null,
error: action.error
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
function Users() {
const [state, dispatch] = useReducer(reducer, {
loading: false,
data: null,
error: null
});
const fetchUsers = async () => {
dispatch({ type: 'LOADING' });
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
dispatch({ type: 'SUCCESS', data: response.data });
} catch (e) {
dispatch({ type: 'ERROR', error: e });
}
};
useEffect(() => {
fetchUsers();
}, []);
const { loading, data: users, error } = state; // state.data 를 users 키워드로 조회
if (loading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return null;
return (
<>
<ul>
{users.map(user => (
<li key={user.id}>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={fetchUsers}>다시 불러오기</button>
</>
);
}
export default Users;
useReducer 로 구현했을 때의 장점은 useState 의 setState 함수를 여러번 사용하지 않아도 된다는점과, 리듀서로 로직을 분리했으니 다른곳에서도 쉽게 재사용을 할 수 있다는 점 입니다.
물론, 취향에 따라 useState 로 구현을 해도 무방합니다.
useAsync 커스텀 Hook 만들어서 사용하기
데이터를 요청해야 할 때마다 리듀서를 작성하는 것은 번거로운 일 입니다. 매번 반복되는 코드를 작성하는 대신에, 커스텀 Hook 을 만들어서 요청 상태 관리 로직을 쉽게 재사용하는 방법을 알아봅시다.
useAsync.js
import { useReducer, useEffect } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'LOADING':
return {
loading: true,
data: null,
error: null
};
case 'SUCCESS':
return {
loading: false,
data: action.data,
error: null
};
case 'ERROR':
return {
loading: false,
data: null,
error: action.error
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
function useAsync(callback, deps = []) {
const [state, dispatch] = useReducer(reducer, {
loading: false,
data: null,
error: false
});
const fetchData = async () => {
dispatch({ type: 'LOADING' });
try {
const data = await callback();
dispatch({ type: 'SUCCESS', data });
} catch (e) {
dispatch({ type: 'ERROR', error: e });
}
};
useEffect(() => {
fetchData();
// eslint 설정을 다음 줄에서만 비활성화
// eslint-disable-next-line
}, deps);
return [state, fetchData];
}
export default useAsync;
useAsync 함수는 두가지 파라미터를 받아옵니다. 첫번째 파라미터는 API 요청을 시작하는 함수이고, 두번째 파라미터는 deps 인데 이 deps 값은 해당 함수 안에서 사용하는 useEffect 의 deps 로 설정됩니다.
이 값은 나중에 우리가 사용 할 비동기 함수에서 파라미터가 필요하고, 그 파라미터가 바뀔 때 새로운 데이터를 불러오고 싶은 경우에 활용 할 수 있습니다 (현재 Users 컴포넌트에서는 불필요한 부분입니다). 이 값의 기본값은 [] 입니다. 즉, 컴포넌트가 가장 처음 렌더링 할 때만 API 를 호출하고 싶다는 의미죠.
Users.js
import React from 'react';
import axios from 'axios';
import useAsync from './useAsync';
// useAsync 에서는 Promise 의 결과를 바로 data 에 담기 때문에,
// 요청을 한 이후 response 에서 data 추출하여 반환하는 함수를 따로 만들었습니다.
async function getUsers() {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
}
function Users() {
const [state, refetch] = useAsync(getUsers, []);
const { loading, data: users, error } = state; // state.data 를 users 키워드로 조회
if (loading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return null;
return (
<>
<ul>
{users.map(user => (
<li key={user.id}>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={refetch}>다시 불러오기</button>
</>
);
}
export default Users;
데이터 나중에 불러오기
Users 컴포넌트는 컴포넌트가 처음 렌더링 되는 시점부터 API 를 요청하고 있습니다. 만약에 특정 버튼을 눌렀을 때만 API 를 요청하고 싶다면, 어떻게 해야할까요? 예를 들어서, POST, DELETE, PUT, PATCH 등의 HTTP 메서드를 사용하게 된다면 필요한 시점에만 API 를 호출해야 하기 때문에, 필요할 때에만 요청 할 수 있는 기능이 필요합니다.
useAsync.js
import { useReducer, useEffect } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'LOADING':
return {
loading: true,
data: null,
error: null
};
case 'SUCCESS':
return {
loading: false,
data: action.data,
error: null
};
case 'ERROR':
return {
loading: false,
data: null,
error: action.error
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
function useAsync(callback, deps = [], skip = false) {
const [state, dispatch] = useReducer(reducer, {
loading: false,
data: null,
error: false
});
const fetchData = async () => {
dispatch({ type: 'LOADING' });
try {
const data = await callback();
dispatch({ type: 'SUCCESS', data });
} catch (e) {
dispatch({ type: 'ERROR', error: e });
}
};
useEffect(() => {
if (skip) return;
fetchData();
// eslint 설정을 다음 줄에서만 비활성화
// eslint-disable-next-line
}, deps);
return [state, fetchData];
}
export default useAsync;
Users.js
import React from 'react';
import axios from 'axios';
import useAsync from './useAsync';
// useAsync 에서는 Promise 의 결과를 바로 data 에 담기 때문에,
// 요청을 한 이후 response 에서 data 추출하여 반환하는 함수를 따로 만들었습니다.
async function getUsers() {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
}
function Users() {
const [state, refetch] = useAsync(getUsers, [], true);
const { loading, data: users, error } = state; // state.data 를 users 키워드로 조회
if (loading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return <button onClick={refetch}>불러오기</button>;
return (
<>
<ul>
{users.map(user => (
<li key={user.id}>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={refetch}>다시 불러오기</button>
</>
);
}
export default Users;
API 에 파라미터가 필요한 경우
이번에는, API 를 요청 할 때 파라미터가 필요한 경우에 어떻게 해야 하는지 알아보겠습니다.
우리는 User 라는 컴포넌트를 만들 것이구요, id 값을 props 로 받아와서
https://jsonplaceholder.typicode.com/users/1
이런 식으로 맨 뒤에 id 를 넣어서 API 를 요청 할 것 입니다.
User.js
import React from 'react';
import axios from 'axios';
import useAsync from './useAsync';
async function getUser(id) {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return response.data;
}
function User({ id }) {
const [state] = useAsync(() => getUser(id), [id]);
const { loading, data: user, error } = state;
if (loading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!user) return null;
return (
<div>
<h2>{user.username}</h2>
<p>
<b>Email:</b> {user.email}
</p>
</div>
);
}
export default User;
그 다음에는, Users.js 에서 useState 를 사용하여 userId 상태를 관리해주겠습니다. 초깃값은 null 이며, 리스트에 있는 항목을 클릭하면 클릭한 사용자의 id 를 userId 값으로 설정해줍니다.
Users.js
import React, { useState } from 'react';
import axios from 'axios';
import useAsync from './useAsync';
import User from './User';
// useAsync 에서는 Promise 의 결과를 바로 data 에 담기 때문에,
// 요청을 한 이후 response 에서 data 추출하여 반환하는 함수를 따로 만들었습니다.
async function getUsers() {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
}
function Users() {
const [userId, setUserId] = useState(null);
const [state, refetch] = useAsync(getUsers, [], true);
const { loading, data: users, error } = state; // state.data 를 users 키워드로 조회
if (loading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return <button onClick={refetch}>불러오기</button>;
return (
<>
<ul>
{users.map(user => (
<li
key={user.id}
onClick={() => setUserId(user.id)}
style={{ cursor: 'pointer' }}
>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={refetch}>다시 불러오기</button>
{userId && <User id={userId} />}
</>
);
}
export default Users;
react-async 로 요청 상태 관리하기
이 라이브러리 안에 들어있는 함수 이름도 useAsync 인데요, 사용법이 조금 다릅니다.
만약에 여러분이 매번 프로젝트를 만들 때 마다 직접 요청 상태 관리를 위한 커스텀 Hook 을 만들기 귀찮다면, 이 라이브러리를 사용하시면 됩니다. 정말 많은 기능들이 내장되어있답니다. 다만, 사용법이 조금 다릅니다. 우리가 만들었던 커스텀 Hook 은 결과물을 배열로 반환하는 반면 이 Hook 은 객체 형태로 반환합니다.
$ yarn add react-async
import { useAsync } from "react-async"
const loadCustomer = async ({ customerId }, { signal }) => {
const res = await fetch(`/api/customers/${customerId}`, { signal })
if (!res.ok) throw new Error(res)
return res.json()
}
const MyComponent = () => {
const { data, error, isLoading } = useAsync({ promiseFn: loadCustomer, customerId: 1 })
if (isLoading) return "Loading..."
if (error) return `Something went wrong: ${error.message}`
if (data)
return (
<div>
<strong>Loaded some data:</strong>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
return null
}
react-async 의 useAsync 를 사용 할 때 파라미터로 넣는 옵션 객체에는 호출 할 함수 promiseFn 을 넣고, 파라미터도 필드 이름과 함께 (customerId) 넣어주어야 합니다.
User 컴포넌트 전환
User.js
import React from 'react';
import axios from 'axios';
import { useAsync } from 'react-async';
async function getUser({ id }) {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return response.data;
}
function User({ id }) {
const { data: user, error, isLoading } = useAsync({
promiseFn: getUser,
id,
watch: id
});
if (isLoading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!user) return null;
return (
<div>
<h2>{user.username}</h2>
<p>
<b>Email:</b> {user.email}
</p>
</div>
);
}
export default User;
async function getUser({ id }) {}
Users 컴포넌트 전환
Users.js
import React, { useState } from 'react';
import axios from 'axios';
import { useAsync } from 'react-async';
import User from './User';
async function getUsers() {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
}
function Users() {
const [userId, setUserId] = useState(null);
const { data: users, error, isLoading, reload } = useAsync({
promiseFn: getUsers
});
if (isLoading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return <button onClick={reload}>불러오기</button>;
return (
<>
<ul>
{users.map(user => (
<li
key={user.id}
onClick={() => setUserId(user.id)}
style={{ cursor: 'pointer' }}
>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={reload}>다시 불러오기</button>
{userId && <User id={userId} />}
</>
);
}
export default Users;
만약에 우리가 이전 섹션에서 배웠던 skip 처럼, 렌더링하는 시점이 아닌 사용자의 특정 인터랙션에 따라 API 를 호출하고 싶을 땐 promiseFn 대신 deferFn 을 사용하고, reload 대신 run 함수를 사용하면 됩니다.
Users.js
import React, { useState } from 'react';
import axios from 'axios';
import { useAsync } from 'react-async';
import User from './User';
async function getUsers() {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
}
function Users() {
const [userId, setUserId] = useState(null);
const { data: users, error, isLoading, run } = useAsync({
deferFn: getUsers
});
if (isLoading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return <button onClick={run}>불러오기</button>;
return (
<>
<ul>
{users.map(user => (
<li
key={user.id}
onClick={() => setUserId(user.id)}
style={{ cursor: 'pointer' }}
>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={run}>다시 불러오기</button>
{userId && <User id={userId} />}
</>
);
}
export default Users;
Context 와 함께 사용하기
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 loadingState = {
loading: true,
data: null,
error: null
};
// 성공했을 때의 상태 만들어주는 함수
const success = data => ({
loading: false,
data,
error: null
});
// 실패했을 때의 상태 만들어주는 함수
const error = error => ({
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);
const UsersDispatchContext = createContext(null);
// 위에서 선언한 두가지 Context 들의 Provider 로 감싸주는 컴포넌트
export function UsersProvider({ children }) {
const [state, dispatch] = useReducer(usersReducer, initialState);
return (
<UsersStateContext.Provider value={state}>
<UsersDispatchContext.Provider value={dispatch}>
{children}
</UsersDispatchContext.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 useUsersDispatch() {
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 });
}
API 처리 함수 만들기
우리는 이러한 작업을 처리하는 함수를 만들어주겠습니다. UsersContext.js 를 열어서 상단에 axios 를 불러오고, 코드의 하단 부분에 getUsers 와 getUser 함수를 작성해주세요. 이 함수들은 dispatch 를 파라미터로 받아오고, API 에 필요한 파라미터도 받아오게 됩니다.
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_USER_ERROR', error: e });
}
}
Context 사용하기
App.js
import React from 'react';
import Users from './Users';
import { UsersProvider } from './UsersContext';
function App() {
return (
<UsersProvider>
<Users />
</UsersProvider>
);
}
export default App;
Users.js
import React, { useState } from 'react';
import { useUsersState, useUsersDispatch, getUsers } from './UsersContext';
import User from './User';
function 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>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!users) return <button onClick={fetchData}>불러오기</button>;
return (
<>
<ul>
{users.map(user => (
<li
key={user.id}
onClick={() => setUserId(user.id)}
style={{ cursor: 'pointer' }}
>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={fetchData}>다시 불러오기</button>
{userId && <User id={userId} />}
</>
);
}
export default Users;
useUsersState() 와 useUsersDispatch() 를 사용해서 state 와 dispatch 를 가져오고, 요청을 시작 할 때에는 getUsers() 함수 안에 dispatch 를 넣어서 호출을 해주었습니다.
User.js
import React, { useEffect } from 'react';
import { useUsersState, useUsersDispatch, getUser } from './UsersContext';
function 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>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!user) return null;
return (
<div>
<h2>{user.username}</h2>
<p>
<b>Email:</b> {user.email}
</p>
</div>
);
}
export default User;
여기선 useEffect 를 사용해서 id 값이 바뀔 때마다 getUser() 함수를 호출해주도록 하면 됩니다. 여기서 getUser() 함수를 호출 할 때에는 두번째 파라미터에 현재 props 로 받아온 id 값을 넣어주었습니다.
반복되는 코드를 줄이자!
이번에 우리가 배운 것은, Context + 비동기 API 연동의 정석이라고 볼 수 있습니다. 이 패턴에 대하여 잘 이해하시고, 앞으로 이런 패턴을 잘 활용하시면 됩니다.
반복된 코드
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_USER_ERROR', error: e });
}
}
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;
}
asyncActionUtils.js
// 이 함수는 파라미터로 액션의 타입 (예: GET_USER) 과 Promise 를 만들어주는 함수를 받아옵니다.
export default function createAsyncDispatcher(type, promiseFn) {
// 성공, 실패에 대한 액션 타입 문자열을 준비합니다.
const SUCCESS = `${type}_SUCCESS`;
const ERROR = `${type}_ERROR`;
// 새로운 함수를 만듭니다.
// ...rest 를 사용하여 나머지 파라미터를 rest 배열에 담습니다.
async function actionHandler(dispatch, ...rest) {
dispatch({ type }); // 요청 시작됨
try {
const data = await promiseFn(...rest); // rest 배열을 spread 로 넣어줍니다.
dispatch({
type: SUCCESS,
data
}); // 성공함
} catch (e) {
dispatch({
type: ERROR,
error: e
}); // 실패함
}
}
return actionHandler; // 만든 함수를 반환합니다.
}
UsersContext.js
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);
asyncActionUtils.js
// 이 함수는 파라미터로 액션의 타입 (예: GET_USER) 과 Promise 를 만들어주는 함수를 받아옵니다.
export function createAsyncDispatcher(type, promiseFn) {
// 성공, 실패에 대한 액션 타입 문자열을 준비합니다.
const SUCCESS = `${type}_SUCCESS`;
const ERROR = `${type}_ERROR`;
// 새로운 함수를 만듭니다.
// ...rest 를 사용하여 나머지 파라미터를 rest 배열에 담습니다.
async function actionHandler(dispatch, ...rest) {
dispatch({ type }); // 요청 시작됨
try {
const data = await promiseFn(...rest); // rest 배열을 spread 로 넣어줍니다.
dispatch({
type: SUCCESS,
data
}); // 성공함
} catch (e) {
dispatch({
type: ERROR,
error: e
}); // 실패함
}
}
return actionHandler; // 만든 함수를 반환합니다.
}
export const initialAsyncState = {
loading: false,
data: null,
error: null
};
// 로딩중일 때 바뀔 상태 객체
const loadingState = {
loading: true,
data: null,
error: null
};
// 성공했을 때의 상태 만들어주는 함수
const success = data => ({
loading: false,
data,
error: null
});
// 실패했을 때의 상태 만들어주는 함수
const error = error => ({
loading: false,
data: null,
error: error
});
// 세가지 액션을 처리하는 리듀서를 만들어줍니다
// type 은 액션 타입, key 는 리듀서서 사용할 필드 이름입니다 (예: user, users)
export function createAsyncHandler(type, key) {
// 성공, 실패에 대한 액션 타입 문자열을 준비합니다.
const SUCCESS = `${type}_SUCCESS`;
const ERROR = `${type}_ERROR`;
// 함수를 새로 만들어서
function handler(state, action) {
switch (action.type) {
case type:
return {
...state,
[key]: loadingState
};
case SUCCESS:
return {
...state,
[key]: success(action.data)
};
case ERROR:
return {
...state,
[key]: error(action.error)
};
default:
return state;
}
}
// 반환합니다
return handler;
}
UsersContext.js
import React, { createContext, useReducer, useContext } from 'react';
import {
createAsyncDispatcher,
createAsyncHandler,
initialAsyncState
} from './asyncActionUtils';
import * as api from './api'; // api 파일에서 내보낸 모든 함수들을 불러옴
// UsersContext 에서 사용 할 기본 상태
const initialState = {
users: initialAsyncState,
user: initialAsyncState
};
const usersHandler = createAsyncHandler('GET_USERS', 'users');
const userHandler = createAsyncHandler('GET_USER', 'user');
// 위에서 만든 객체 / 유틸 함수들을 사용하여 리듀서 작성
function usersReducer(state, action) {
switch (action.type) {
case 'GET_USERS':
case 'GET_USERS_SUCCESS':
case 'GET_USERS_ERROR':
return usersHandler(state, action);
case 'GET_USER':
case 'GET_USER_SUCCESS':
case 'GET_USER_ERROR':
return userHandler(state, action);
default:
throw new Error(`Unhanded action type: ${action.type}`);
}
}
(...)
어떤가요? 반복되는 코드가 많이 사라졌지요? 꼭 이렇게 까지 리팩토링을 할 필요가 없지만, 이런 코드가 맘에 든다면, 자주 사용되는 코드를 함수화해서 재사용하시면 좋습니다.
'WINK-(Web & App) > React.js 스터디' 카테고리의 다른 글
[2024-2 React.js 스터디] 윤아영 #5주차 (0) | 2024.11.27 |
---|---|
[2024-2 React.js 스터디] 김지수 #6주차 (2) | 2024.11.27 |
[2024-2 React.js 스터디] 이서영 #4주차 (0) | 2024.11.13 |
[2024-2 React.js 스터디 ] 김지수 #4주차 (0) | 2024.11.13 |
[2024-2 React.js 스터디] 이서영 #3주차 (2) | 2024.11.07 |