[CRUD 실습 - React + Vite] GET (초기 데이터 불러오기)

Chan의 기술 블로그·2025년 11월 17일

산출물 링크
- GitHub
- 배포 페이지

지난 게시물까지 기능·UI 설계를 진행하고 실제 UI 코드까지 제작했다.
API 데이터를 받아와 화면에 출력하는 직전 단계까지 마무리한 상태다. 이번 글에서는 CRUD를 중심으로 apihooks 폴더에 기능을 구현해 나가겠다.

GET (초기 데이터 불러오기)

먼저 초기 진입 시 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 하여 사용한다.

API 함수 만들기

// 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[] 배열을 그대로 반환한다.

GET 요청을 수행하는 Custom Hook 만들기

// 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로 상태에 저장한다.

Container에서 실제 호출

// 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의 실제 데이터가 렌더링된다.

loading, error시 UI처리

데이터 요청 자체는 잘 이루어지고 있지만, 한 가지 아쉬움이 있다.
초기 진입 시 데이터를 가져오기 전까지 화면에 아무것도 보이지 않는다는 점이다.
사용자는 페이지가 멈췄는지, 로딩 중인지 전혀 알 수 없기 때문에
로딩 상태와 에러 상태를 명확히 보여주는 UI가 필요하다.

따라서 useUsersQuery 훅에 로딩 여부와 에러 메시지를 처리할 수 있는 상태를 추가하고, usersContainer 컴포넌트에서 이를 화면에 렌더링하도록 개선해보겠다.

useUsersQuery 코드 수정

// 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 }
}

이제 isLoadingerror state가 추가되었다.

  • isLoading은 API 호출 중인지 여부를 나타내며,
  • error는 API 요청 중 Error 객체에서 받은 message를 저장해 UI에 표시하기 위함이다.

나는 boolean 값을 나타내는 상태 변수에는 앞에 is를 붙여 명확하게 의미를 드러내는 편이다.

현재는 Error 객체로 내려오는 에러만 state에 담고 있으며,
AbortController의 abort 에러는 이 글에서는 다루지 않는다. 나중에 필요하다면 별도로 처리할 예정이다.

instanceof Error로 체크하는 이유는
에러 객체인지 확인해 message를 안전하게 읽기 위함이다.

Container에서 loading & error UI 처리

// 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 호출 로직과 화면에서 사용할 핸들러는 apihooks에서 각각 분리하고,
최종적으로 필요한 데이터와 액션을 모두 UsersContainer에서 import 하여 UsersProvider 컴포넌트에 propsvalue로 보내 사용할 것이다.

이런 구조를 유지하면 다음과 같은 장점이 있다.

  • API 로직이 한곳에 모여 유지보수가 쉬워진다
  • Hook에서 상태/로직을 캡슐화해 재사용할 수 있다
  • Container, Provider에서 필요한 데이터와 핸들러를 조합해 UI에 깔끔하게 전달할 수 있다

CRUD 전체를 같은 패턴으로 확장하여
기능 추가가 자연스럽고 구조적으로 정돈된 형태의 코드베이스를 완성할 예정이다.

profile
퍼블리셔에서 프론트앤드로 전향하기

0개의 댓글