5일차(4.1~4.5)

yoon Y·2021년 12월 29일

01. API연동의 기본

axios를 사용해서 GET, PUT, POST, DELETE 등의 메서드로 API 요청

사용법

import axios from 'axios'; 

// 메서드 이름을 소문자로
// 파라미터에는 API 의 주소
axios.get('/users/1'); 

//post일 시 두번 째 파라미터로 등록하려는 정보를 넣는다
axios.post('/users', {
  username: 'blabla',
  name: 'blabla'
});

useState 와 useEffect 로 데이터 로딩하기

useState: 요청 상태 관리 (요청 결과, 로딩 상태, 에러)

const [users, setUsers] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

useEffect: 렌더링 시점에 요청 시작

useEffect(() => { // 파라미터 함수에는 async사용 불가
    const fetchUsers = async () => { // 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를 users에 할당한다
      } catch (e) {
        setError(e);
      }
      setLoading(false); // 요청 끝나고 false로 변경
    };

    fetchUsers();
  }, []);

이벤트로 axios함수를 호출하려면 fetchUsers 함수를 바깥으로 꺼내주고, 컴포넌트에 이벤트 핸들러로 전달해주면 된다

// 따로 생성
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(); // 처음 렌더링 시 호출
  }, []);

return (
    <>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.username} ({user.name})
          </li>
        ))}
      </ul>
      <button onClick={fetchUsers}>다시 불러오기</button> // 버튼 클릭 시 호출
    </>
  );

02. useReducer 로 요청 상태 관리하기

useState 대신에 useReducer 를 사용해서 구현할 수 있다

장점

  • setState 함수를 여러번 사용하지 않아도 된다 - 여러 상태를 한꺼번에 변경할 수 있음
  • 리듀서로 로직을 분리했기 때문에 다른 곳에서도 쉽게 재사용 할 수 있다

// 각 action이 일어 났을 때의 3가지 상태를 설정해서 리턴한다
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}`);
  }
}

// useReducer의 2번째 인자로 초기 상태를 설정해준다
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 }); // 에러 넘기기
    }
  };

const { loading, data: users, error } = state; // 각 상태를 꺼내서 조건에 맞게 렌더링한다

  if (loading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!users) return null;

03. useAsync 커스텀 Hook 만들어서 사용하기

데이터를 요청해야 할 때마다 리듀서를 작성하는 것은 번거로운 일,
커스텀 Hook 을 만들어서 요청 상태 관리 로직을 쉽게 재사용 할 수 있다

useAsync.js

useReducer, fetchData함수와 useEffect로 처음 렌더링 시 호출해주는 로직을 useAsync함수에 넣어서

state와 fetchData를 반환해준다

첫번째 파라미터: API 요청을 시작하는 함수
두번째 파라미터: useEffect의 실행 의존 값이다 (API 요청 함수를 다시 실행하기 위한 의존 값)
-> 한 번 실행하고 난 뒤의 상태들과 API요청 함수를 반환받는다

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;

사용하기

function Users() {

	// state와 fetch함수를 꺼낸다
  const [state, refetch] = useAsync(getUsers, [], true);

  // state에서 상태들을 꺼내준다
  const { loading, data: users, error } = state; 

  // 상태에 따라 조건부 렌더링한다
  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> // API호출 함수를 원하는 곳에서 사용한다
    </>
  );
}

궁금한 점

한 번 실행하고 난 뒤의 상태들과 API요청 함수를 반환받고, 그 후에 fetch함수가 실행될 때 상태들에 따라 조건부 렌더링되는 건지?

데이터 나중에 불러오기

3번째 인자로 skip이라는 bool값의 인자를 받아서 처음 실행에 조건을 걸 수 있다

function useAsync(callback, deps = [], skip = false) {
  const [state, dispatch] = useReducer(reducer, {
    loading: false,
    data: null,
    error: false
  });

  useEffect(() => {
    if (skip) return;
    fetchData();
    // eslint 설정을 다음 줄에서만 비활성화
    // eslint-disable-next-line
  }, deps);

  return [state, fetchData];
}

API 에 파라미터가 필요한 경우

Users.js

user데이터에 담긴 id → li를 만들면서 전달 → li클릭 시 userId상태에 id값 저장 →user컴포넌트에 userId전달

function Users() {
  const [userId, setUserId] = useState(null);
  const [state, refetch] = useAsync(getUsers, [], true);

  const { loading, data: users, error } = state;

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

User.js

users.js에서 id값을 파라미터로 받아서 해당 id의 데이터를 불러온다

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 }) {
  // id가 바뀔 때마다 재호출 되도록 deps 에 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;

05. Context와 함께 사용하기

// User.js
****
import React, { useReducer, createContext, useContext } from 'react';
import axios from 'axios';

만드는 법

  1. 초기 상태와 reducer함수를 만든다
  2. Context를 만들어준다(state, dispatch 각각)
  3. UserProvider함수를 만든 후, useReducer(reducer, initialState) 를 실행해서 state와 dispatch를 꺼내준다
  4. value에 state와 dispatch를 전달한 Provider 컴포넌트를 반환한다
  5. useContext 를 실행해 각 값을 꺼내 반환하는 함수를 만든다
  6. dispatch를 사용해 각 action을 실행하며 api를 호출하는 함수를 만든다

export하는 함수들

  1. UsersProvider : 값을 사용할 범위를 정해주는 컴포넌트
  2. useUsersState : State 를 쉽게 조회 할 수 있게 해주는 커스텀 Hook
  3. useUsersDispatch : Dispatch 를 쉽게 사용 할 수 있게 해주는 커스텀 Hook
  4. getUsers, getUser : dispatch + Api호출 함수

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

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 });
  }
}

출처 https://react.vlpt.us/

profile
#프론트엔드

0개의 댓글