728x90
반응형

반복된 코드

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

 

이 부분을 깔끔하게 리팩토링 해보자. src 디렉토리 하위에 api.js 파일을 만들고 다음과 같이 작성한다.

 

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

 

그 다음에 src 디렉터리에 asyncActionUtils.js라는 파일을 만들고 코드를 작성한다.

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; // 만든 함수를 반환합니다.
}

 

이렇게 만들어주면 Context 파일이 깔끔해진다.

UserContext.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);

 

 

그다음 리듀서쪽 코드도 리팩토링해보자. UsersContextlodingState, success, error를 잘라내서 asyncActionUtils.js 안에 붙여넣고 initialAsyncState 객체와 createAsyncHandler 함수를 만들어서 내보낸다.

 

이제 방금만든 initialAsyncStatecreateAsyncHandler를 사용해서 코드를 고쳐보자.

 

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

(...)

 

이렇게 리팩토링을 하면 반복되는 코드를 함수화해서 재사용 할 수 있다.

728x90
반응형
728x90
반응형

src 디렉터리에 UserContext.js 라는 파일을 만들어보자.

 

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 처리 함수 만들기

이런 작업들을 처리하는 함수를 만들어보겠다. UserContext.js 파일을 열어서 하단에 함수들을 추가한다.

 

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 사용하기

이제는 만든 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;

useUserState로 state를 가져오고 useUserDispatch로 dispatch를 가져온다.

요청을 시작할 때 getUsers 안에 dispatch로 호출해준다.

실행해보면 정상작동 하는것을 볼 수 있다. 이번엔 User를 바꿔보자.

 

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() 함수를 호출해준다. 두번째 파라미터에 props로 받아온 id값을

넣어준다.

 

 

정리

  1. Provider를 만들고 state와 dispatch를 선언하고, userReducer와 initialState를 지정해준다.
  2. state와 dispatch를 Provider로 감싼 내부에서 사용할 수 있도록 Context를 구현한다.
  3. 그 밖에 dispatch를 이용한 함수들을 구현한다.
  4. state의 공유가 필요한 곳을 provider로 감싼다.
  5. 감싸여진 컴포넌트에서 state,dispatch, 함수들을 import 해준다.
  6. 감싸여진 컴포넌트에 할당된 값이 바뀔 때마다 변환을 해주려면 useEffect를 써준다.
728x90
반응형
728x90
반응형

react-async는 우리가 지난 섹션에서 만들었던 useAsync와 비슷한 함수가 들어있는 라이브러리다.

이름은 똑같으나 사용법이 조금 다르다.

 

만약 매번 프로젝트를 만들 때 마다 직접 요청 상태 관리를 위한 커스텀 Hook을 만들기 귀찮으면

이 라이브러리를 사용하면 유용할 것이다.

 

먼저 react-async를 설치한다.

$ npm i 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 을 넣고, 파라미터도 필드 이름과 함께 costomerId를 넣어주어야 한다.

 

User 컴포넌트 전환

User 컴포넌트를 react-async의 useAsync 로 전환하자

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;

 

react-async를 사용할 때에는 프로미스를 반환하는 함수의 파라미터를 객체형태로 해주어야 한다.

async function getUser({ id }) {}

 

그리고 watch 값에 특정 값을 넣어주면 이 값이 바뀔 때마다 promiseFn에 넣은 함수를 다시 호출한다.

 

 

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처럼 나중에 데이터를 불러오려면 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;

 

 

정리 

  • 로그인 할 때는 최상위 컴포넌트에 쿠키를 state에 담고 상태를 관리해준다.(로그아웃, 프로필 수정)
  • 로그아웃 했을 경우(state가 null값이 될 때) 최상위에서 첫 화면 컴포넌트를 return 한다.
  • API가 필요한 경우 처음 페이지가 렌더링 될 때 useAsync로 state(리스트, 사진 등)를 만들어준다.
  • API 필요 없을 경우 처음 페이지 렌더링 될 때 useEffect를 사용해준다.
  • 추가, 수정, 삭제가 필요한 데이터의 경우 reducer를 만들어서 관리한다.(action으로 실행)
728x90
반응형
728x90
반응형

데이터를 요청해야 할 때마다 리듀서를 작성하는 것은 매우 번거롭다.

커스텀 Hook을 만들어서 요청 상태 관리 로직을 쉽게 재사용하는 방법을 알아보자.

src폴더 하위에 useAsync.js 파일을 만든다.

 

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 로 설정된다.

 

나중에 우리가 사용 할 비동기 함수에서 파라미터가 필요하고, 그 파라미터가 바뀔 때 새로운 데이터를 불러오고 싶을 경우에 활욜 할 수 있다.

 

이제 이 Hook을 써보자.

 

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;

  

버튼 클릭 시 불러오기

만약 특정 버튼을 눌렀을 경우에만 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;

skip 파라미터의 기본 값을 false로 지정하고, 만약 이 값이 true라면 useEffect 에서 아무런 작업도 하지 않도록 설정해주었다.

 

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;

useAsync 의 세 번째 파라미터에 true를 넣어줬고, !users인 상황에 불러오기 버튼을 렌더링해주었다.

 

 

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

이번에는 API를 요청 할 때 파라미터가 필요한 경우에 어떻게 해야 하는지 알아보겠다.

 

User 라는 컴포넌트를 만들고, id값을 props로 받아와서

 

https://jsonplaceholder.typicode.com/users/1

이런 식으로 맨 뒤에  id를 넣어서 API를 요청할 것이다.

src 폴더에 User.js 를 생성 후 다음 코드를 작성해보자.

 

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;

useAsync 를 사용할 때 파라미터를 포함시켜서 함수를 호출하는 새로운 함수를 만들어서 등록한다.

그리고 id가 바뀔 때마다 재호출 되도록 deps에 id를 추가한다.

 

그 다음 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;

 

실행해보면

 

 

id를 클릭할 때마다 useState로 선언된 userId가 바뀌게 되고, User 컴포넌트에 userId가 할당되면서 렌더리이 되는 것을

확인할 수 있다.

728x90
반응형
728x90
반응형

이번에는 useState 대신에 useReducer로 구현해보도록 하겠다.

 

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를 여러번 사용하지 않아도 되며 로직을 분리했으니 다른곳에서도 쉽게 재사용을 할 수 있다는 점이다.

728x90
반응형
728x90
반응형

API 연동의 기본

 

API 연동을 하기 위해 우선 프로젝트를 새로 만들어주자.

$ npx create-react-app react-api

 

그리고 API를 호출하기 위해 axios 라는 라이브러리를 설치한다.

$ npm i axios

 

API의 사용법은 다음과 같다.

import axios from 'axios';

axios.get('/users/1');

 

axios.post() 로 데이터를 등록 할 때에는 두 번째 파라미터에 등록하고자 하는 정보를 넣을 수 있다.

axios.post('/users', {
  username: 'blabla',
  name: 'blabla'
});

 

useState 와 useEffect 로 데이터 로딩하기

 

컴포넌트가 랜더링되는 시점에 요청을 시작하는 작업을 해보자.

요청에 대한 상태를 관리 할 때에는 다음과 같이 총 3가지 상태를 관리해주어야 한다.

  1. 요청의 결과
  2. 로딩 상태
  3. 에러

 

src 컴포넌트에 Users.js를 생성하고 다음 코드를 작성해보자.

 

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;

 

사용자들이 담길 users와 loading상태 여부를 체크할 loading이랑, error 를 출력해주는 error를 선언한다.

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

 

그다음 주석대로 error와 users를 초기화하고 loading을 true로 만들어준 다음, 데이터가 정상적으로 수신되면 loading을 false로 바꿔준다. catch부분에 error를 담아준다.

  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();
  }, []);

 

 

컴포넌트가 잘 작동하는지 확인하기 위해 App 컴포넌트에 User 컴포넌트를 추가하자.

 

 App.js

import React from 'react';
import Users from './Users';

function App() {
  return <Users />;
}

export default App;

 

이제 주소로 들어가보면

 

 

 

로딩중인지, 에러가 발생했는지, user가 존재하지 않은지에 대해 조건문을 설정해준다.

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

 

 

에러 발생까지 확인해준다. 주소를 이상하게 바꿔준다.

const response = await axios.get(
  'https://jsonplaceholder.typicode.com/users/showmeerror'
);

 

확인해보면

 

 

이번엔 버튼을 눌러서  API를 재요청하는 코드를 구현해보자.

 

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;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

참조

https://react.vlpt.us/integrate-api/01-basic.html

 

1. API 연동의 기본 · GitBook

1. API 연동의 기본 API 연동을 하기 위해서, 우선 프로젝트를 새로 만들어주도록 하겠습니다. $ npx create-react-app api-integrate 그리고, API 를 호출하기 위해서 axios 라는 라이브러리를 설치하세요. $ cd a

react.vlpt.us

 

728x90
반응형
728x90
반응형

지난글에 이어서 PATCH와 DELETE를 구현해보자.

 

먼저 npm 모듈을 설치하고 UpdateCatDto를 만들어준다.

 

$ npm i @nestjs/mapped-types

 

update-cat.dto.ts

import {IsString, IsNumber} from 'class-validator'
import { PartialType } from '@nestjs/mapped-types';
import { CreateCatDto } from './create-cat.dto';

export class UpdateCatDto extends PartialType(CreateCatDto) {}

 

Update 먼저 구현해보자.  컨트롤러와 서비스를 바꿔준다.

 

cats.controller.ts

import { Controller, Get, Param, Post, Delete, Patch, Body, Query } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './schemas/cat.schema';


@Controller('cats')
export class CatsController {
    constructor(private readonly catsService: CatsService) {}

    @Get()
    async getAll(): Promise<Cat[]> {
        return await this.catsService.getAll();
    }

    @Get('/:id')
    async getOne(@Param('id') catId: number): Promise<Cat[]> {
        console.log(catId)
        return await this.catsService.getOne(catId);
    }

    @Post()
    async create(@Body() catsData : CreateCatDto) {
        return await this.catsService.create(catsData);
    }

    @Patch('/:id')
    async update(@Param('id') catId: number, @Body() updateData: UpdateCatDto) {
        return this.catsService.update(catId,updateData)
    }

}

 

cats.service.ts

import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}

  async getAll(): Promise<Cat[]> {
    return await this.catModel.find().exec();
  }

  async getOne(id:number): Promise<Cat[]> {
    return await this.catModel.find({"id" : id});
  } 

  async create(catsData: CreateCatDto) {
    let latestId = await this.catModel.findOne();
    return await this.catModel.create({...catsData, id : parseInt(latestId ? latestId.id : 0)+1});
  }

  async update(id: number, updateData : UpdateCatDto) {
    try {
      await this.catModel.where({"id" : id}).update(updateData);
      return true
    }
    catch(e) {
      return false
    }
  }
}

PATCH 메소드로 요청을 보내보자.


 GetAll()로 확인해보자.

 

정상적으로 id가 1인 데이터의 name이 "초코"로 바뀐 것을 확인할 수 있다.

 

 

DELETE도 만들어보자.

 

cats.controller.ts

import { Controller, Get, Param, Post, Delete, Patch, Body, Query } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './schemas/cat.schema';


@Controller('cats')
export class CatsController {
    constructor(private readonly catsService: CatsService) {}

    @Get()
    async getAll(): Promise<Cat[]> {
        return await this.catsService.getAll();
    }

    @Get('/:id')
    async getOne(@Param('id') catId: number): Promise<Cat[]> {
        console.log(catId)
        return await this.catsService.getOne(catId);
    }

    @Post()
    async create(@Body() catsData : CreateCatDto) {
        return await this.catsService.create(catsData);
    }

    @Patch('/:id')
    async update(@Param('id') catId: number, @Body() updateData: UpdateCatDto) {
        return this.catsService.update(catId,updateData)
    }

    @Delete('/:id')
    async delete(@Param('id') catId: number) {
        return this.catsService.delete(catId);
    }

}

 

cats.service.ts

import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}

  async getAll(): Promise<Cat[]> {
    return await this.catModel.find().exec();
  }

  async getOne(id:number): Promise<Cat[]> {
    return await this.catModel.find({"id" : id});
  } 

  async create(catsData: CreateCatDto) {
    let latestId = await this.catModel.findOne();
    return await this.catModel.create({...catsData, id : parseInt(latestId ? latestId.id : 0)+1});
  }

  async update(id: number, updateData : UpdateCatDto) {
    try {
      await this.catModel.where({"id" : id}).update(updateData);
      return true
    }
    catch(e) {
      return false
    }
  }

  async delete(id: number) {
    try {
      await this.catModel.remove({"id" : id});
      return true
    }
    catch(e) {
      return false
    }
  }
}

 


postman으로 실행해본다.


getAll() 전체데이터를 확인해본다.

 

 

이렇게 몽고DB로 연결해서 데이터 CRUD 하는 것을 완성하였다.

728x90
반응형

'Back-End > Nest.js' 카테고리의 다른 글

Nest.js | Middleware  (0) 2021.10.28
Nest.js | Swagger  (0) 2021.10.28
Nest.js | MongoDB | Create  (0) 2021.10.06
Nest.js | MongoDB | Find  (0) 2021.10.06
Nest.js | MongoDB | Model,Schema,Controller  (0) 2021.10.06
728x90
반응형

먼저 main.ts 파일에 pipe를 추가한다.

 

main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist : true, 
      forbidNonWhitelisted : true,
      transform : true
    })
  )
  await app.listen(3000);
}
bootstrap();

 

그다음 컨트롤러와 서비스를 바꿔주자. GetOne 메소드도 추가하였다.

 

cats.cantroller.ts

import { Controller, Get, Param, Post, Delete, Patch, Body, Query } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './schemas/cat.schema';


@Controller('cats')
export class CatsController {
    constructor(private readonly catsService: CatsService) {}

    @Get()
    async getAll(): Promise<Cat[]> {
        return await this.catsService.getAll();
    }

    @Get('/:id')
    async getOne(@Param('id') catId: number): Promise<Cat[]> {
        console.log(catId)
        return await this.catsService.getOne(catId);
    }

    @Post()
    async create(@Body() catsData : CreateCatDto) {
        return await this.catsService.create(catsData);
    }

}

 

cats.service.ts

import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}

  async getAll(): Promise<Cat[]> {
    return await this.catModel.find().exec();
  }

  async getOne(id:number) {
    return await this.catModel.find({"id" : id});
  } 

  async create(catsData: CreateCatDto) {
    let latestId = await this.catModel.findOne();
    return await this.catModel.create({...catsData, id : parseInt(latestId ? latestId.id : 0)+1});
  }
}

생성할 때 id가 최근의 id +1 이 되게끔 하였다.

 

2번 생성한 뒤 postman으로 getAll()을 확인해보자.

 

 

 

mongoose에서 Create는 추가한 값을 그대로 반환해준다. 아주 성공적으로 id가 +1씩 되면서 잘 되었다.

 

 

배열 안에 잘 담겨서 반환된 것을 확인할 수 있다.

728x90
반응형

'Back-End > Nest.js' 카테고리의 다른 글

Nest.js | Swagger  (0) 2021.10.28
Nest.js | MongoDB | PATCH and DELETE  (0) 2021.10.06
Nest.js | MongoDB | Find  (0) 2021.10.06
Nest.js | MongoDB | Model,Schema,Controller  (0) 2021.10.06
Nest.js | MongoDB | Schema  (0) 2021.10.06
728x90
반응형

먼저 다음과 같이 main.ts 파일에 파이프를 추가해준다.

 

main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist : true, 
      forbidNonWhitelisted : true,
      transform : true
    })
  )
  await app.listen(3000);
}
bootstrap();

 

그다음 controller와 service에 GET메소드로 getAll() 메소드를 만들어보자.

 

cats.controller.ts

import { Controller, Get, Param, Post, Delete, Patch, Body, Query } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './schemas/cat.schema';


@Controller('cats')
export class CatsController {
    constructor(private readonly catsService: CatsService) {}

    @Get()
    async getAll(): Promise<Cat[]> {
        return await this.catsService.getAll();
    }


}

 

cats.service.ts

import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}

  async getAll(): Promise<Cat[]> {
    return await this.catModel.find().exec();
  }
}

 

postman으로 확인해보자

 

 

아직 추가한 Document가 없기 때문에 당연히 빈배열로 받아와진다.

728x90
반응형

'Back-End > Nest.js' 카테고리의 다른 글

Nest.js | MongoDB | PATCH and DELETE  (0) 2021.10.06
Nest.js | MongoDB | Create  (0) 2021.10.06
Nest.js | MongoDB | Model,Schema,Controller  (0) 2021.10.06
Nest.js | MongoDB | Schema  (0) 2021.10.06
Nest.js | E2E TESTING | PATCH and DELETE  (0) 2021.09.30
728x90
반응형

기본적인 cats 모듈,컨트롤러,서비스를 만들고 schemas 폴더를 다음과 같이 옮긴다.

$ nest g module cats
$ nest g co cats
$ nest g s cats

 

 

cats.module.ts 파일을 수정해준다.

cats.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat, CatSchema } from './schemas/cat.schema';

@Module({
  imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

 

그 다음 지정된 스키마를 @InjectModel() 데코레이터를 사용하여 CatService에 삽입한다.

cats.service.ts

import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}

}

 

아직 DTO파일을 안 만들었기 때문에 에러가 뜬다. DTO파일을 작성해보자.

 

$ npm i class-validator class-transformer

 

 

create-cat.dto.ts

import {IsString, IsNumber} from 'class-validator'

export class CreateCatDto {
    @IsString()
    readonly name: string;

    @IsNumber()
    readonly age: number;
    
    @IsString({each:true})
    readonly breed: string;
}

 

이제 Contoller를 작성해보자.

 

cat.controller.ts

import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './schemas/cat.schema';


@Controller('cats')
export class CatsController {
    constructor(private readonly catsService: CatsService) {}

}

 

여기까지 cats모듈과 dto를 만들어보았다.

 

728x90
반응형

'Back-End > Nest.js' 카테고리의 다른 글

Nest.js | MongoDB | Create  (0) 2021.10.06
Nest.js | MongoDB | Find  (0) 2021.10.06
Nest.js | MongoDB | Schema  (0) 2021.10.06
Nest.js | E2E TESTING | PATCH and DELETE  (0) 2021.09.30
Nest.js | E2E TESTING | Testing GET movies id  (0) 2021.09.30

+ Recent posts