지난 게시물까지 기능·UI 설계를 진행하고 실제 UI 코드까지 제작했다.
API 데이터를 받아와 화면에 출력하는 직전 단계까지 마무리한 상태다. 이번 글에서는 CRUD를 중심으로 api와 hooks 폴더에 기능을 구현해 나가겠다.
먼저 초기 진입 시 API로부터 데이터를 받아 화면에 렌더링하는 GET 로직부터 구현해보겠다.
우리는 이미 https://reqres.in/api/users에서 내려오는 데이터 구조를 확인했고, 그에 맞춰 UI와 Type 정의도 준비해둔 상태다.
// types/users.ts
export type User = {
avatar: string
email: string
first_name: string
id: number
last_name: string
}
현재 UI만 만들어둔 상태라 Dummy 데이터로 화면을 구성하고 있다.
// src/features/users/containers/UsersContainer.tsx
const dummyData: User[] = [
{
avatar: '',
email: 'chanchan@gmail.com',
first_name: 'ChanChan',
id: 1,
last_name: 'Choi',
},
{
avatar: '',
email: 'chanchan@gmail.com',
first_name: 'ChanChan',
id: 2,
last_name: 'Choi',
},
]
앞으로는 api 폴더에서 fetch 요청을 작성하고,
hooks 폴더에서는 해당 요청을 호출해 필요한 값을 반환하는 구조로 구현할 예정이다.
반환받은 데이터나 핸들러는 UsersContainer에서 import 하여 사용한다.
// src/api/users.api.ts
export const getAllUsersApi = async () => {
const response = await fetch('https://reqres.in/api/users', {
headers: {
'x-api-key': 'MY_API_KEY',
},
})
if (!response.ok) throw Error('유저 데이터를 받아올 수 없습니다.')
const result: { data: User[] } = await response.json()
return result
}
fetch 요청을 보내고data: User[] 배열을 그대로 반환한다.// src/hooks/useUsersQuery.ts
export function useUsersQuery() {
const [users, setUsers] = useState<User[]>([])
const getAllUsers = useCallback(async () => {
try {
const { data } = await getAllUsersApi()
setUsers(data)
} catch (err) {
console.error(err)
if (err instanceof Error) setError(err.message)
}
}, [])
return { users, getAllUsers }
}
getAllUsers는 API를 호출해 데이터를 가져오고setUsers로 상태에 저장한다.// src/features/users/containers/UsersContainer.tsx
export default function UsersContainer() {
const { users, getAllUsers } = useUsersQuery()
useEffect(() => {
void getAllUsers()
}, [getAllUsers])
return (
<UsersProvider>
<Users>
{users.map((user) => (
<Users.Item
key={user.id}
profileSrc={user.avatar}
firstName={user.first_name}
lastName={user.last_name}
email={user.email}
id={user.id}
/>
))}
</Users>
</UsersProvider>
)
}
Container에서 getAllUsers를 실행해 users 데이터를 화면에 노출한다.
async 함수는 Promise를 반환하므로, 반환값을 사용하지 않을 때는 void로 처리했다.
이제 화면 진입 시 GET 요청이 실행되며 더 이상 dummy 데이터가 아니라 API의 실제 데이터가 렌더링된다.


데이터 요청 자체는 잘 이루어지고 있지만, 한 가지 아쉬움이 있다.
초기 진입 시 데이터를 가져오기 전까지 화면에 아무것도 보이지 않는다는 점이다.
사용자는 페이지가 멈췄는지, 로딩 중인지 전혀 알 수 없기 때문에
로딩 상태와 에러 상태를 명확히 보여주는 UI가 필요하다.
따라서 useUsersQuery 훅에 로딩 여부와 에러 메시지를 처리할 수 있는 상태를 추가하고, usersContainer 컴포넌트에서 이를 화면에 렌더링하도록 개선해보겠다.
// src/hooks/useUsersQuery.ts
export function useUsersQuery() {
const [users, setUsers] = useState<User[]>([])
const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const getAllUsers = useCallback(async () => {
setIsLoading(true)
setError('')
try {
const { data } = await getAllUsersApi()
setUsers(data)
} catch (err) {
console.error(err)
if (err instanceof Error) setError(err.message)
} finally {
setIsLoading(false)
}
}, [])
return { users, getAllUsers, isLoading, error }
}
이제 isLoading과 error state가 추가되었다.
isLoading은 API 호출 중인지 여부를 나타내며,error는 API 요청 중 Error 객체에서 받은 message를 저장해 UI에 표시하기 위함이다.나는 boolean 값을 나타내는 상태 변수에는 앞에
is를 붙여 명확하게 의미를 드러내는 편이다.
현재는 Error 객체로 내려오는 에러만 state에 담고 있으며,
AbortController의 abort 에러는 이 글에서는 다루지 않는다. 나중에 필요하다면 별도로 처리할 예정이다.
instanceof Error로 체크하는 이유는
에러 객체인지 확인해 message를 안전하게 읽기 위함이다.
// src/features/users/containers/UsersContainer.tsx
export default function UsersContainer() {
const { users, getAllUsers, isLoading, error } = useUsersQuery()
useEffect(() => {
void getAllUsers()
}, [getAllUsers])
return (
<UsersProvider>
<Users>
{isLoading && <img src="src/assets/loading.gif" className="loading" />}
{error.length > 0 && (
<>
<div className="error">
<img src="src/assets/error.jpeg" alt="" className="error__img" />
<span className="error__text">{error}</span>
</div>
</>
)}
{users.map((user) => (
<Users.Item
key={user.id}
profileSrc={user.avatar}
firstName={user.first_name}
lastName={user.last_name}
email={user.email}
id={user.id}
/>
))}
</Users>
</UsersProvider>
)
}
이제 페이지 진입 시 아래 흐름으로 동작하게 된다.
1. getAllUsers 호출 → isLoading: true
2. 로딩 GIF 표시
3. API 요청 성공 → users 렌더링
4. API 요청 실패 → error 메시지 UI 렌더링
이로써 사용자에게 현재 상태를 명확하게 전달할 수 있다.
이전처럼 아무것도 안 나오는 화면이 없어 UX가 개선된다.


GET 초기 작업은 아래 Git Branch 링크에서 확인할 수 있으며,
현재는 Main Branch에서 코드 리펙토링으로 많이 수정되었다.
feature/fetchGet Branch 바로가기
추가(Create) · 수정(Update) · 삭제(Delete) 기능도 동일한 구조로 구현할 예정이다.
즉, API 호출 로직과 화면에서 사용할 핸들러는 api와 hooks에서 각각 분리하고,
최종적으로 필요한 데이터와 액션을 모두 UsersContainer에서 import 하여 UsersProvider 컴포넌트에 props나 value로 보내 사용할 것이다.
이런 구조를 유지하면 다음과 같은 장점이 있다.
CRUD 전체를 같은 패턴으로 확장하여
기능 추가가 자연스럽고 구조적으로 정돈된 형태의 코드베이스를 완성할 예정이다.