티스토리 뷰

1. Redux / React Redux / Redux Thunk

1-1. Redux

 Redux는 전역 상태 관리를 위한 라이브러리이다. 기존에는 하나의 상태를 여러 곳에서 사용하기 위해서는 최상단의 부모 컴포넌트에서 자식 컴포넌트에게 Props로 상태를 전달해줘야했다. 만약 자식 컴포넌트가 부모 컴포넌트로부터 깊은 곳에 위치해있다면 중간에 상태를 사용하지 않는 컴포넌트에게도 Props를 전달해야하는 Prop drilling이 발생하게 된다. 리액트가 아무리 바닐라 자바스크립트보다 편하다고 해도 이렇게 수고스러운 작업이 반가울리 없다. 따라서 사람들은 모든 컴포넌트에서 전역으로 상태에 접근하기 위한 도구의 필요성을 느꼈고 여러 전역 상태 관리 라이브러리들이 등장하게 됐다.

 

 처음 리덕스를 배우는 입장에서 리덕스는 꽤나 복잡한 구조를 가지고 있었다. 그래서 다른 라이브러리들보다 학습을 하는데 꽤나 애를 먹었다. 결국 가장 좋은 방법은 역시나 카운터 예제를 직접 짜보며 이해해보기

 

Redux 기본 개념

- 액션(Action)

: 상태에 특정 변화가 필요할 때 발생시키는 것

 

- 액션 생성 함수(Action Creator)

: 파라미터를 받아 액션을 생성하는 함수

 

- 리듀서(Reducer)

: 현재의 상태와, 전달 받은 액션에 따라 새로운 상태를 만들어서 반환하는 함수

 

- 스토어(Store)

: 상태를 저장하는 장소로 보통 앱 하나에 하나의 스토어를 가지게 된다.

 

- 디스패치(Dispatch)

: 디스패치는 스토어의 내장함수 중 하나로 액션을 발생 시키는 것

 

- 구독(Subscribe)

: 액션이 디스패치 될 때 마다 등록해놓은 함수가 실행되도록 함.

 

1-2. React Redux

 React Redux는 Redux와 React를 같이 사용하기 위한 리덕스 공식 라이브러리이다. Context API처럼 최상위에 전역 상태를 선언해놓고 dispatch를 통해 상태가 변경되면 하위 컴포넌트가 모두 재렌더링 되도록 하는 방식으로 동작한다. 이 때문에 React Redux를 사용하면 따로 Subscribe 함수를 쓸 필요가 없다. 또한 useSelector, useDispatch 등의 편리한 hooks을 제공해준다.

 

1-3. Redux Thunk

Redux Thunk는 비동기 처리를 위한 Redux Middleware 중 가장 많이 사용되는 라이브러리이다. 미들웨어는 리덕스의 액션과 스토어 사이에서 추가적인 작업을 할 수 있도록 돕는다.

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

Redux Thunk는 클로저와 커링을 사용하여 필요한 인자를 받는다. 여기서 next는 다음 미들웨어가 있는 경우 다음 미들웨어를, 미들웨어가 없다면 reducer를 실행시킨다. 

 

현재까지 이해한 바에 따르면 applyMiddleware(thunk)가 적용되면 내부적으로 다음과 같이 동작할 것이다.

thunk({dispatch, getState})(nextMiddleware || combinedReducer)(action)

 

Thunk란, 특정 작업을 나중에 하도록 미루기 위해서 함수 형태로 감싼 것을 말한다. 아래와 같이 'getCart' 함수를 Thunk 함수로 생성해보자. 그리고 dispatch(getCart())를 실행하면 앞서 살펴본 'thunk' 함수의 가장 마지막 action에 'getCart()'가 들어갈 것이다. 그리고 'getCart()'는 thunk 함수였기 때문에 아직도 함수 형태이다. 따라서 'createThunkMiddleware' 내에서 action의 type이 함수이므로 우리가 만든 getCart에 'dispatch'가 인자로 들어올 것이다('createThunkMiddleware' 네번째 라인). 이후 getCart가 리턴한 익명함수 로직이 실행된다. 익명함수 내부에서 실행되는 dispatch들은 인자로 객체를 받기 때문에 'createThunkMiddleware' 내에서 바로 next(action)이 실행된다. (여기서 다음 미들웨어가 수행되고 최종적으로 리듀서에 액션이 들어가게 되어 스토어에 상태가 반영된다.)

export const getCart = () => async (dispatch: Dispatch<CartAction>) => {
  dispatch({ type: LOADING });
  try {
    const response = await axios.get(URL.CART);
    if (response.status !== STATUS_CODE.GET_SUCCESS) {
      throw new Error('장바구니 정보를 불러오는데 실패하였습니다.');
    }

    dispatch({ type: LOADING_SUCCESS, payload: FORMAT_DATA.CART(response.data) });
  } catch (error) {
    dispatch({ type: LOADING_FAILURE, loadingError: error });
  }
}

 

2. 타입스크립트

 자바스크립트는 동적 타이핑을 지원하는 언어이기 때문에 항상 타입이 무엇인지 고민하게 된다. 콘솔로그 주도 개발이라는 말이 있을 정도로 바닐라 자바스크립트에서는 타입을 확인하기 위해 콘솔로그를 많이 찍어보게 된다. 물론, 쉽게 타입을 확인하는 방법이 없는 것은 아니다. 바로, 변수명에 타입을 명시하는 헝가리안 표기법을 사용하면 된다. 하지만 헝가리안 표기법은 변수명을 직관적으로 이해하기 어렵고 변수의 데이터 타입이 바뀌면 해당 변수명을 모두 바꿔야하는 문제점 등이 있어 잘 사용되지 않는다.

 

 타입스크립트는 이러한 문제점을 획기적으로 해결해주는 툴이다. TS는 컴파일 타임에서 코드의 타입을 체크해주는 강력한 도구로 기존 자바스크립트 개발자들이 타입으로 고민하던 문제를 해결해주었다. 

 

 이번 미션에서 타입스크립트를 사용해보면서 느낀 타입스크립트의 장단점은 다음과 같다.

 

장점

  • 변수나 함수에 마우스를 올리는 것 만으로도 타입을 쉽게 알 수 있다. (콘솔로그 안녕 🖐)
  • 잘못된 타입을 할당하거나 인자로 넣을 경우 타입스크립트가 알려준다.
  • null 병합 연산자나 옵셔널 체이닝을 사용해야하는 경우를 알려주어 예상치 못한 에러를 방지할 수 있다.

단점

  • 타입을 일일이 명시하는 게 귀찮게 느껴질 때가 있다.
  • 다른 라이브러리들과 같이 사용할 때 해당 라이브러리가 타입스크립트를 지원하는지 확인해야한다.
  • 다른 라이브러리들과 같이 사용할 때 타입을 지정하기 까다로운 경우가 있다.
  • 초반 러닝 커브가 있다.

타입스크립트를 처음 써본 입장에서 타입스크립트는 마치 족쇄를 차고 코딩을 하는 느낌이었다. 하지만 타입스크립트에 점차 익숙해질수록 단점보다 장점이 훨씬 더 크다고 느껴졌다.

 

3. Custom Hook

 커스텀 훅을 처음으로 만들어 사용해보았다. 커스텀 훅은 공통되는 컴포넌트 로직을 추상화하여 여러 컴포넌트에서 재사용 가능하도록 만드는 것이다. 물론, 아래의 함수도 추가적인 추상화가 가능하다. useFetchingStatus 함수 안에 API 비동기 통신을 위한 로직을 추가할 수 있다.

const useOrders = () => {
  const [orders, setOrders] = useState<Orders>([]);
  const { loading, setLoading, responseOK, setResponseOK } = useFetchingStatus();

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await axios.get(URL.ORDERS);
        if (response.status !== STATUS_CODE.GET_SUCCESS) {
          throw new Error('주문 목록 조회 실패');
        }
        setOrders(FORMAT_DATA.ORDERS(response.data));
        setResponseOK(true);
      } catch (error) {
        setResponseOK(false);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [setLoading, setResponseOK]);

  return { orders, loading, responseOK };
};

export default useOrders;

 

4. 스낵바 띄우기 좌충우돌

 

 

 

스낵바를 자연스럽게 띄우기 위해서 겪은 여러 난관들이 있었다.

먼저, 스낵바를 클릭할 때마다 CSS 애니메이션을 적용하기 위해 새로운 엘리먼트를 생성해야했다. 리액트에서 key prop을 변경하면 새로운 인스턴스(컴포넌트)를 만들 수 있다.

 

<SnackBar key={Math.random()} message={snackBarMessage} setMessage={setSnackBarMessage} />

 

다음으로 이전과 똑같은 메세지가 출력될 때도 리렌더링이 일어나도록 하기 위한 작업이 필요했다. 리액트는 상태값이 이전과 동일한 경우 리렌더링이 일어나지 않는다. 

 

const useSnackBar = () => {
  const [snackBarMessage, _setSnackBarMessage] = useState({ message: '' });

  const setSnackBarMessage = (message: string) => {
    _setSnackBarMessage({ message });
  };

  return { SnackBar, snackBarMessage: snackBarMessage.message, setSnackBarMessage };
};

export default useSnackBar;

 

 이를 해결하기 위해 스낵바 커스텀 훅을 만들어 스낵바 메세지를 객체로 관리하도록 하였다. 객체는 내용이 같더라도 참조값으로 비교를 하기 때문에 기존 객체와 새로운 객체는 다르다고 인식한다. 따라서 사용하는 측에서 동일한 메세지더라도 setSnackbarMessage를 호출하기만 하면 무조건 리렌더링을 보장받을 수 있다.

 

 마지막으로 스낵바가 일정 시간 동안만 유지되도록 하는 작업이 필요하다. 이를 위해서는 스낵바가 클릭될 때마다 타이머가 초기화 돼야한다. setTimeout을 사용하면 간단하게 해결할 수 있는 문제지만 우리는 앞서 매번 새로운 인스턴스(컴포넌트)가 생성되게 만들었기 때문에 useEffect가 매번 초기화되어 컴포넌트 내부에서 타이머를 기억하지 못하는 문제가 발생하게 된다. 따라서 타이머를 기억하기 위해서 컴포넌트를 클로저로 만들어 매번 새로운 인스턴스(컴포넌트)가 생성되더라도 기존의 타이머를 기억할 수 있도록 하였다.

 

export interface Props {
  message: string;
  setMessage: (message: string) => void;
}

const SnackBar = (() => {
  let timer: NodeJS.Timer | null;

  return ({ message, setMessage }: Props) => {
    useEffect(() => {
      const prevTimer = timer;
      return () => {
        if (prevTimer) {
          clearTimeout(prevTimer);
        }
      };
    }, []);

    if (!$snackBar || !message) return null;

    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    timer = setTimeout(() => {
      setMessage('');
    }, 3000);

    return ReactDOM.createPortal(<Styled.SnackBar>{message}</Styled.SnackBar>, $snackBar);
  };
})();

 

 

5. JSON server

 JSON server를 이용하면 간단하게 Mock server를 만들어 REST API를 테스트를 해볼 수 있다. 사용법이 굉장히 간단해서 복잡한 로직이 필요하지 않은 경우에 유용하게 사용할 수 있다. heroku를 이용하면 리액트 프로젝트와 JSON server를 동시에 배포할 수도 있다.

 

Heroku로 react 프로젝트 배포하기 (json-server 포함)

json-server가 프로덕션 전용이 아닌 것을 알지만, 백엔드 서버가 없기 때문에.. 프로젝트 배포 시 json-server도 같이 배포하기 위해 여러 배포 서비스를 찾아봤다. 그전에 가장 쉽게 접근 가능한 gh-pag

heeyeonjeong.tistory.com