1. API 연동의 기본
API 를 호출하기 위해서 axios 라는 라이브러리를 설치해야 한다.
axios를 사용해서 GET, PUT, POST, DELETE 등의 메서드로 API 요청을 할 수 있다.
npm install axios
# 또는 yarn add axios
- GET: 데이터 조회
- POST: 데이터 등록
- PUT: 데이터 수정
- DELETE: 데이터 제거
import axios from 'axios';
axios.get('/users/1');
위와 같이 사용할 수 있다.
axios.post('/users', {
username: 'blabla',
name: 'blabla'
});
axios.post() 로 데이터를 등록할 수 있다.
(1) useState 와 useEffect 로 데이터 로딩하기
요청에 대한 상태를 관리 할 때에는 다음과 같이 총 3가지 상태를 관리해주어야 한다.
- 요청의 결과
- 로딩 상태
- 에러
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;
useState 를 사용하여 요청 상태를 관리하고, useEffect 를 사용하여 컴포넌트가 렌더링되는 시점에 요청을 하는 코드이다.
로딩 상태가 활성화 됐을 땐 로딩중.. 이라는 문구를 보여주고, users 값이 아직 없을 때에는 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();
}, []);
// ...
<button onClick={fetchUsers}>다시 불러오기</button>
위 코드는 버튼을 눌러서 API를 재요청하는 기능이 구현된 코드이다.
2. useReducer 로 요청 상태 관리하기
useReducer 로 구현했을 때의 장점은 useState 의 setState 함수를 여러번 사용하지 않아도 된다는점과, 리듀서로 로직을 분리했으니 다른곳에서도 쉽게 재사용을 할 수 있다는 점이다.
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 });
}
};
3. useAsync 커스텀 Hook 만들어서 사용하기
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];
}
useAsync 함수는 두가지 파라미터를 받아오는데, 첫번째 파라미터는 API 요청을 시작하는 함수이고, 두번째 파라미터는 deps 인데 이 deps 값은 해당 함수 안에서 사용하는 useEffect 의 deps 로 설정된다.
// 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>
</>
);
}
- 데이터 나중에 불러오기
처음부터 API를 요청하는 것이 아니라, 필요할 때에만 요청 할 수 있는 기능이 필요하다.
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];
}
skip 파라미터의 기본 값을 false 로 지정하고, 만약 이 값이 true 라면 useEffect 에서 아무런 작업도 하지 않도록 설정한 코드이다.
- API 에 파라미터가 필요한 경우
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>
);
}
useAsync 를 사용 할 때, 파라미터를 포함시켜서 함수를 호출하는 새로운 함수를 만들어서 등록해주었다. 그리고, id 가 바뀔 때 마다 재호출 되도록 deps 에 id 를 넣어주었다.
// User.js
// 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} />}
</>
);
}
4. react-async 로 요청 상태 관리하기
react-async는 앞에서 만들었던 useAsync 와 비슷한 함수가 들어있는 라이브러리이다.
매번 프로젝트를 만들 때 마다 직접 요청 상태 관리를 위한 커스텀 Hook 을 만들기 귀찮다면, 이 라이브러리를 사용하면 된다.
npm install react-async
// 또는 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) 넣어주어야 한다.
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;
useAsync 를 사용할 때에는 프로미스를 반환하는 함수의 파라미터를 객체형태로 해주어야 한다.
async function getUser({ id }) {}
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
});
위 코드는 Users 컴포넌트를 react-async 의 useAsync 를 사용하는 코드로 전환한 결과이다.
react-async 의 useAsync 는 옵션이 다양하고 (promiseFn, deferFn, watch, ...) 결과 값도 객체 안에 다양한 값이 들어있어서 (run, reload, ...) 헷갈릴 수 있는 단점이 있긴 하지만 다양한 기능이 이미 내장되어있고 (예를 들어서 요청을 취소할 수도 있다.) Hook 을 직접 만들 필요 없이 바로 불러와서 사용 할 수 있는 측면에서는 정말 편하다.
Hook을 직접 만들어도 되고, 라이브러리를 사용해도 된다.
※ async-react는 현재(2022-)에 들어서는 사용하지 않는다. 대신 react-use를 사용할 수 있다.
5. Context 와 함께 사용하기
컴포넌트에서 필요한 외부 데이터들은 컴포넌트 내부에서 useAsync 같은 Hook 을 사용해서 작업을 하면 충분하지만, 가끔씩 특정 데이터들은 다양한 컴포넌트에서 필요하게 될 때도 있는데 (예: 현재 로그인된 사용자의 정보, 설정 등) 그럴 때에는 Context 를 사용하면 개발이 편해진다.
1) Context 준비하기
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;
}
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 });
}
위 코드는 id 를 가지고 특정 사용자의 정보를 가져오는 API 를 호출하는 코드이다.
2) 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 });
}
}
3) Context 사용하기
import React from 'react';
import Users from './Users';
import { UsersProvider } from './UsersContext';
function App() {
return (
<UsersProvider>
<Users />
</UsersProvider>
);
}
export default App;
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
const state = useUsersState();
const dispatch = useUsersDispatch();
useEffect(() => {
getUser(dispatch, id);
}, [dispatch, id]);
const { data: user, loading, error } = state.user;
useEffect 를 사용해서 id 값이 바뀔 때마다 getUser() 함수를 호출해주도록 하였다.
'WINK-(Web & App) > React.js 스터디' 카테고리의 다른 글
[2025 겨울방학 React.js 스터디] 백채린 #1주차 (0) | 2025.01.12 |
---|---|
[2024-2 React.js 스터디] 이서영 #5주차 (0) | 2024.11.28 |
[2024-2 React.js 스터디] 김지수 #6주차 (2) | 2024.11.27 |
[2024-2 React.js 스터디 ] 김지수 #5주차 (1) | 2024.11.19 |
[2024-2 React.js 스터디] 이서영 #4주차 (0) | 2024.11.13 |