문제 마주하기

5/23/23 Tue

문제 상황

  • 지도 생성 기능 앱을 만드려고 함.
  • 지도 생성, 마크 표시 등 모든 기능들이 현재 유저 위치를 받는 것으로 부터 시작함.
  • but, 현재 유저 위치를 받는 getCurrentPosition 내장 함수는 “비동기적”으로 수행됨.
  • 모든 코드를 App 파일에 순차적으로 때려넣었을 때는 순서대로 진행됐지만, 훅으로 분리하니 getCurrentPosition 함수가 값을 받아오는 동안 벌써 다른 함수들이 실행되는 문제가 발생함.
  • 당연히 값이 없으니 undefined 값으로 실행되고 에러가 남.

고민점

  • 어떻게 훅으로 분리할 때 비동기함수를 동기적으로 처리할 수 있을까?

접근

1: useMap 안의 setState
But, 초기값 { lat: 0, lon: 0 } 이 맨처음에 렌더링 되는 게 지저분해보임. 비동기 함수를 좀 더 깔끔히 다루는 방법이 있을 것 같음.

// useMap.tsx

const useMap = () => {
  const [currentPosition, setCurrentPosition] = useState({ lat: 0, lon: 0 });

  const getUserPosition = async () => {
    if (!navigator.geolocation)
      throw new Error("위치 접근 권한을 허용해주세요");

    navigator.geolocation.getCurrentPosition((position) => {
	    const lat = position.coords.latitude;
	    const lon = position.coords.longitude;
	
	    setCurrentPosition({ lat, lon });
	  });
  };

  //...
}
// App.tsx
function App() {
  const { currentPosition, getUserPosition, createMap } = useMap();

  if (Object.keys(currentPosition).length !== 0) {
    createMap(currentPosition.lat, currentPosition.lon);
  }

  useEffect(() => {
    getUserPosition();
  }, []);

  return (
    <div id="app">
      <div id="map" className="map-container"></div>
    </div>
  );
}

 

2. promise, async/await → return new Promise를 통해 해결.

const getUserPosition = () => {
    if (!navigator.geolocation)
      throw new Error("위치 접근 권한을 허용해주세요");

    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition((position) => {
        const lat = position.coords.latitude;
        const lon = position.coords.longitude;

        resolve({ lat, lon });
      });
    });
  };
function App() {
  const { getUserPosition, createMap, displayMarker } = useMap();

  useEffect(() => {
    getUserPosition().then((res) => {
      const { lat, lon } = res;
      const map = createMap(lat, lon);
      displayMarker(lat, lon, "내 위치", map);
    });
  }, []);

  return (
    <div id="app">
      <div id="map" className="map-container"></div>
    </div>
  );
}

 


useHttp 훅 만들기

5/24/23 Wed

 

추가적으로, 비동기함수 중 fetch처럼 상태, 에러 등을 관리할 필요가 있다면 상태관리를 위해 훅으로 빼보자.

찾아보니 그 훅은 비슷비슷한 것 같다.

  1. data, status(loading), error : 이 3가지 상태를 관리한다.
  2. useCallback과 useReducer, 그리고 dispatch를 사용한다.

상태를 다루는 방법에는 알다시피 2가지가 있다: useState, useReducer 가 그것이다. 그런데 useHttp훅은 알다시피 다뤄야할 상태가 3가지이다. 따라서 복수 상태를 다루기 더 편리한 useReducer를 사용하는 것이 편할 것이다.(물론 useState를 사용할 수 도 있다.)

2가지 방법으로 모두 훅을 만들어 사용해 보았다.

 

1. useState를 이용한 커스텀훅

// useFetchMoviesUsingUseState.tsx

import { useCallback, useEffect, useState } from "react";

const useFetchMoviesUsingUseState = (url: string) => {
  const [movies, setMovies] = useState([]);
  const [loading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchMoviesHandler = useCallback(async () => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error("Something went wrong!");
      }

      const data = await response.json();

      const transformedMovies = data.results.map((movieData: any) => {
        return {
          id: movieData.episode_id,
          title: movieData.title,
          openingText: movieData.opening_crawl,
          releaseDate: movieData.release_data,
        };
      });
      setMovies(transformedMovies);
    } catch (error: any) {
      setError(error.message);
    }

    setIsLoading(false);
  }, []);

  useEffect(() => {
    fetchMoviesHandler();
  }, [fetchMoviesHandler]);

  return { movies, loading, error };
};

export default useFetchMoviesUsingUseState;

2. useReducer를 이용한 커스텀훅

// useFetchMoviesUsingUseReducer.tsx

import { useCallback, useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "SEND":
      return {
        data: undefined,
        error: null,
        status: "pending",
      };
    case "SUCCESS":
      return {
        data: action.payload,
        error: null,
        status: "completed",
      };
    case "ERROR":
      return {
        data: undefined,
        error: action.errorMessage,
        status: "completed",
      };
    default:
      return state;
  }
};
const initialState = {
  data: undefined,
  error: null,
  status: "ready",
};

function useFetchMoviesUsingUseReducer(requestFunction) {
  const [state, dispatch] = useReducer(reducer, initialState);
  const loading = state.status === "pending";

  const sendRequest = useCallback(
    async (...requestData) => {
      dispatch({ type: "SEND" });
      try {
        const responseData = await requestFunction(...requestData);
        dispatch({ type: "SUCCESS", payload: responseData });
        return responseData;
      } catch (error) {
        dispatch({ type: "ERROR", errorMessage: error.message });
        return;
      }
    },
    [requestFunction]
  );

  return {
    sendRequest,
    loading,
    ...state,
  };
}

export default useFetchMoviesUsingUseReducer;

확실히 여러가지 상태를 관리할 때에는useState보단 useReducer를 사용하는 편이 가독성이 올라간다고 생각은 하는데, 여전히 작성할 때 코드가 늘어난다는 점에서 (reducer를 따로 작성해줘야 했다) 더 편리한지는 모르겠더라.

 

🔖 학습 참조글
https://itchallenger.tistory.com/258
https://velog.io/@sae1013/Reactcustom-hook으로-httpRequest-구현하기
https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25599794#overview

react-query 이용하기

5/27/23 Sat

 

이렇게 서버측 데이터를 불러오고 관리하는 커스텀훅을 만들다보니 하나 알게된 사실: 한 라이브러리를 쓰면 이렇게 커스텀훅을 만들지 않아도 이러한 훅을 쓴 것처럼 data, isLoading 등 상태관리를 쉽게 할 수 있게 된 것을 알게됐다. 그 라이브러리의 이름은 react-query. 그렇게 react-query에 대해서도 공부해보게 되었다.

 

react-query란?

여러 종류의 데이터(state 등) 중에서도 서버에서 받아온 데이터 상태의 관리를 도와주는 라이브러리이다. 이 글은 react-query를 상세히 다루기 위한 글이 아니어서 이에 대한 학습 및 설명글은 따로 뺐다. 아주 간단히 말하자면  처음 사용시 queryProvider로 감싸주기만 하면 그 하위에선 커스텀훅을 사용했던 것처럼 useQuery훅을 통해 어디서든 서버측 데이터와 데이터의 상태를 이용할 수 있었다. 이렇게 위에서 작성했던 모든 커스텀hook과 api함수들이 불필요해지는 편리함을 맛보았다.

 

react-query 사용시 장점

기대했던대로 수많은 보일러플레이트를 없앨 수 있을 뿐 아니라 staleTime, cacheTime등을 활용해 캐싱 기능을 더 수월하게 다룰 수 있다는 점이 인상깊었다. 또 혼자 공부하다보니 놓쳤는데 커스텀훅으로 상태관리를 하면 말그대로 ‘커스텀’이기 때문에 개발자마다 다르게 작성된다. 하지만 react-query를 사용하면 통일된 방식으로 서버측 데이터 관리가 가능해진다. 이렇게 react-query를 공부하고 사용하며 느낀 장점을 정리하면 아래와 같다:

  • 귀찮은 boilerplate x — isLoading 등 상태, 캐싱 기능
  • 팀원들끼리 통일된 방식으로 상태 관리 가능 — 누구는 isLoading, 누구는 pending 변수명을 다르게 사용하는 것을 막을 수 있다.
// index.tsx

const queryClient = new QueryClient();
console.log(queryClient);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);
// App.tsx

const { data, status, error } = useQuery(["movies"], fetchMovies, {
    refetchOnWindowFocus: false,
    retry: 0,
    staleTime: 5000,
    cacheTime: Infinity,
    onSuccess: (data) => console.log(data),
    onError: (e) => console.log(e.message),
  });

  let content = <p>No Found Movies.</p>;

  if (status === "success") {
    content = (
      <div>
        {data.results.map((movie, idx) => (
          <div key={idx}>{movie.title}</div>
        ))}
      </div>
    );
  }

  if (status === "loading") {
    content = <p>Loading...</p>;
  }

  if (status === "error") {
    content = <p>{error.message}</p>;
  }

  return <div>{content}</div>;
}

 

🔖 학습 참조글
https://tech.kakaopay.com/post/react-query-1/
https://tanstack.com/query/latest/docs/react/overview?from=reactQueryV3&original=https://tanstack.com/query/v3/docs/overview
https://velog.io/@jay/10-minute-react-query-concept#:~:text=리엑트 쿼리는 데이터,데이터를 다시 불러온다
https://2ham-s.tistory.com/407
https://kyounghwan01.github.io/blog/React/react-query/basic/
https://www.youtube.com/watch?v=novnyCaa7To

 


Sum Up

어찌어찌 하다보니 비동기함수로 시작해, 비동기 함수중에서도 fetch를 집중적으로 다루게 되었다. 나아가 fetch해온 데이터(서버측 데이터)를 프론트측에서 리액트로 관리하는 방법에 대해 찾아보게 된 시간이었다.

 

네이티브(useState/useReducer) 그리고 툴(react-query)

useState+useEffect 혹은 useReducer+useCallback를 사용해 관리할 수 있다.(이경우 훅을 따로 분리해 관리하면 깔끔하다.) 그러나 이 경우 수많은 보일러 플레이트가 필요하고, 특히 캐싱에 관련해서도 수많은 작업을 진행해줘야 한다. 또한 규명된 상태관리 폼이 존재하지 않아 팀원들끼리 공통된 상태관리가 힘들 수 있다.(다른 상태명, 로직….) 이러한 점을 보다 쉽게 해주는 것이 라이브러리겠지. redux의 saga 미들웨어와 mobx의 __을 이용할 수 있겠지만, 이들은 서버상태 전용 라이브러리가 아니다. 서버 상태 전용 라이브러리 react-query는 보일러 플레이트를 줄이며, 캐싱, 클라이언트 측과 서버 측 상태의 동기화에 특화된 인터페이스를 제공해 서버측에서 넘어온 데이터 관리를 간단하게 한다. 또한 공통된 서버 상태 관리로 팀작업을 원활히 한다.

 

마무리하며&다음에는...

fetch를 가지고 작업하다 서버측 데이터 상태관리까지 넘어가게 되었는데 많은 사람들이 fetch 대신 axios를 사용하고 있었다. fetch 에서 넘어오는 데이터의 에러 처리를 간단하게 해준다는 장점이 있어서 쓴다고 하는데, 한 번 가볍게 다뤄보는 것도 좋겠다.


Summary


이 글은 미션을 수행하며 학습했던 컴포넌트 분리 기준에 대한 내용을 정리한 것입니다. 컴포넌트 분리에 대한 절대적인 기준이 없어 여러 선행자분들의 의견을 참고하고자 아래 3가지 강의를 참고해 공통적인 부분을 거르고, 이후 개인적인 의견을 간단히 남기고자 하였습니다.

 

첫 두 강의의 공통점은, "컴포넌트를 쪼개는 절대적 기준 같은 것은 없다. 하지만 상대적으로 잘 쪼개는 컴포넌트 분리 기준은 존재한다."는 것이었는데요, 두 분 모두 '유지보수를 쉽게 하는'것에 기준을 두고 컴포넌트 분리를 설명하신 것이 인상 깊었습니다. 유지보수를 쉽게 하려면 어떻게 해야할까요? 데이터를 미래에 변할 수 있는 것과 상대적으로 잘 변하지 않을 데이터를 구분하는 것이었습니다.

 

조금 더 구체적으로는, 첫 강의자인 원지혁님은 컴포넌트의 구성을 스타일, 로직, 전역상태, 리모드 데이터 스키마(데이터)로 분리하셨고, 유지보수를 위해 변화가 가장 잘 일어나는 데이터 모델에 따라 컴포넌트 분리를 해주는 것을 추천하셨습니다. 두 번째 강의의 한재엽님은 변하는 것에 앞서 설명된 데이터를 데이터계산과 상호작용으로 나누셨습니다. 결국 둘 다 데이터 관리부분인데 처음부터 제공되는 데이터냐, 사용자의 액션이 있어야 계산되는 데이터냐에 따라 한 번 더 나누신듯하다. 변하는 부분을 데이터, 변하지 않는 부분을 UI로 잡고 분리한다는 점에서 원지혁님과 의견이 같았습니다. 두 강의자분 다 컴포넌트 분리의 기준을 유지보수로 잡고 계시고, 상대적으로 잘 변하는 것, 즉 컴포넌트가 다루는 데이터를 기준으로 컴포넌트를 분리하는 것을 제안하신다는 데에서 동일했습니다.

 

결론적으로, 저도 유지보수를 쉽게하는 컴포넌트 분리 기준으로 데이터 모델을 잡는 것에 설득되었습니다. 이 데이터를 변경가능성이 높은 데이터인지, 그렇지 않은 데이터인지 - 즉, 도메인 관련인지 아닌지로 나누는 방법이 좋다는 것도 이해하였습니다. 더하여 데이터는 훅스, UI관련은 컴포넌트, 상태관리는 store 등으로 나누는 기준에 동의합니다. 앞으로 리액트를 활용한 개발에 있어서 컴포넌트 분리는 이러한 기준으로 하고자 합니다. (하지만 컴포넌트 분리에 절대적인 기준이 없는 만큼 이 글을 보시는 분들의 다양한 의견에도 열려있습니다.)

 


강의

당근마켓, 원지혁 - 컴포넌트, 다시 생각하기


요약

  • 바라보기: React 컴포넌트의존성 - React 컴포넌트를 만드는데 필요한 것들은 어떤 것들이 있을까?
    • 기능적 분류(Type): props 와 hooks
    • 특징적 분류(Feature):
      • 스타일(컴포넌트의 스타일, src/components/Something.css)
      • 로직(UI조작에 필요한 커스텀 로직, src/components/Something.useInfinityScroll.ts),
      • 전역 상태(현재 UI를 표현하기 위해 유저의 액션을 통해 초래된 상태, src/store.ts)
      • 리모트 데이터 스키마(API 서버에서 내려주는 데이터의 모양, https://examples.com/api/v2/articles/json)
    • React 컴포넌트의 숨은 의존성
      • 한 컴포넌트에 정보를 추가한다고 하자. 그 컴포넌트에 새로운 props 를 추가하면 된다.
      • 하지만, 그게 끝이 아니다. 숨은 의존성이 존재한다. 타겟 컴포넌트와 루트 컴포넌트 사이의 props들의 추가 수정이 필연적일 수 밖에 없다. props drilling 없이 따로 store를 둔다해도 페이지 기반 라우팅을 한다면 결국 root 컴포넌트에 의존할 수 밖에 없을 것이다. 즉, 해당 컴포넌트에 새로운 정보(의존성)를 추가하기 위해서는 root 컴포넌트 수정도 필연적이다.
  • 함께 두기(co-locate): 이러한 의존성들을 어떻게 정리할 수 있을까?
    • 원칙1 - 비슷한 관심사라면 가까운 곳에(Keep Locality)
      • 한 컴포넌트에 필요한 스타일과 로직은 함께 두기 쉽다.
      • 전역상태는 여러 컴포넌트가 공유하고 있기 때문에 특정 컴포넌트와 함께 두기 어렵다.
      • 리모트 데이터 스키마 함께두기는 좀 더 복잡하므로 원칙2로 따로 분류해보자.
    • 원칙2 - 데이터를 ID 기반으로 정리하기 (Abstraction by Normalization)
      • 데이터 정규화(Normalization) 이라고도 한다.
        • yarn add mormalizer : 정규화를 도와주는 라이브러리
        • 하지만 여전히 숨은 의존성 문제는 존재한다: 사용 컴포넌트의 상위 컴포넌트에서 id 값을 정확히 받아와야 한다.
      • 글로벌 아이디(Global Id): 모델명을 따로 넘길 필요 없이 ID값만 가지고 특정 데이터를 유일하게 식별할 수 있도록 하는 체계.(상위 컴포넌트에서 ID값을 받아올 필요가 없다.)
        • 예시: 첫번째는 필요한 데이터를 바깥(./store.ts)에서 받아오고 있고, 두번째에서는 GlobalId를 이용해서 id 데이터를 같은 컴포넌트 안에 둘 수 있다! (16:02)
        • 도우미: 전역 객체 식별 (Global Object Identification, GOI): (17:02)
  • 이름 짓기(Naming): 프롭스 네이밍(Props Naming)에 대해서 생각해보자.
    • 원칙3 - 의존한다면 그대로 드러내기(Make Explicit)
      • 프로필 컴포넌트 - User 와 User가 의존하고 있는 Image, 이렇게 크게 2가지 데이터에 의존하고 있다. 2번째는 User와 Image의 의존성까지 보여주는 이름짓기를 활용해 더 직관적으로 의존성을 알 수 있다!
      • 하지만 위와 같이 한 컴포넌트에서 여러 모델의 정보(User 컴포넌트 내에 user 뿐 아니라 image 정보도 받아오고 있다.)를 표현하는 것은 일종의 관심사 분리가 제대로 안되었다는 일종의 신호이기도 하기에 아래와 같이 수정해주면 더욱 좋다.
    • 재사용하기(Reuse): 개발할 때 편리하기 위한 것보다 변경할 때 편리하기 위해 = 유지보수에 편하기 위해!
      • 변화하는 부분을 미리 예측하고 컴포넌트를 나누는게 중요하다.
        • 대부분의 변화는 리모트 데이터 스키마가 변화하는 방향을 따라서 움직인다.
        • 그렇다면 어떻게 변하는 것들과 변하지 않는 것들을 분리할 수 있을까?
    • 원칙4 - 데이터 모델 기준으로 컴포넌트 분리하기, Separating Components by Data model
      • 같은 모델을 의존하는 컴포넌트: 재사용
      • 다른 모델을 의존하는 같은 컴포넌트: 분리
  • 결론(Conclusion)
    • 원칙 요약
      • 원칙1 - 비슷한 관심사라면 가까운 곳에
      • 원칙2 - 데이터를 Id 기반으로 정리하기
      • 원칙3 - 의존한다면 그대로 드러내기
      • 원칙4 - 모델 기준으로 컴포넌트 분리하기
    • 추가
      • 이러한 위 4가지 원칙을 강제해서 리액트 클라이언트 개발을 더 편하게 해주는 GraphQL 데이터 레이어 프레임워크 Relay 를 소개해주시면서 강의를 마치셨다.

강의

토스슬래시, 한재엽: Effective Component 지속 가능한 성장과 컴포넌트


요약

  • 무엇이 변경될지 알았다면… 변경은 예측불가하다. 그래서 변경은 예측하지 말고 대응해야 한다.
  • 변경에 대응하기: 변경에 유연하게 대응하도록 컴포넌트를 나누기
    • 만들다보니 페이지가 커지고, 이 커진 코드를 ‘적당히’ 나누기 → ‘적당히’: 변경에 유연하게 대응하도록
    • 어떻게 하면 변경에 유연하게 대응하도록 컴포넌트를 분리할 수 있을까?
      1. Headless 기반의 추상화하기: 변하는 것 vs 상대적으로 변하지 않는 것
      2. 한 가지 역할만 하기: 또는 한가지 역할만 하는 컴포넌트의 조합으로 구성하기
      3. 도메인 분리하기: 도메인을 포함하는 컴포넌트와 그렇지 않은 컴포넌트 분리하기
  • Headless 기반의 추상화하기
    • 컴포넌트는 크게 3가지 역할을 한다:
      • 데이터 관리: 외부에서 받은 데이터, 상태과 같은 내부 데이터를 어떻게 관리하는지
      • UI 관리: 그러고 이러한 데이터를 어떻게 유저에게 보여줄지(UI)
      • 상호작용: UI를 기반으로 어떻게 사용자와 상호작용할지
    • 데이터와 UI의 분리. 컴포넌트를 구성하는데 필요한 데이터를 계산해야 하는데 이 역할을 use~훅스에 위임한다. 이러한 use~훅스는 UI를 관심사에서 제외하고 오직 데이터를 모듈화 하는데에만 집중할 수 있다.
    • UI와 상호작용 분리: 상호작용 부분도 역시 훅스로 만들어 관리한다.
  • 한 가지 역할만 하기(Composition)
  • 도메인 분리하기
    • UI패턴 공통화
  • 결론: 컴포넌트 분리시 고민해야 하는 것
    • 의도가 무엇인가? 분리하면 어떻게 좋아지는가?
    • 이 컴포넌트의 기능은 무엇인가?: 어떤 데이터를 다루는가?
    • 어떻게 표현되어야 하는가?

강의

당근마켓 원지혁 : 그래서 DECLARATIVE가 뭔데?


Declarative UI 패턴을 통해 자바스크립트를 마치 마크업짜듯 작성할 수 있게 되었다 → HTML, CSS / JavaScript 가 아니라 “관심사(정보)”를 기준으로 분리하게 되었다.

정보는 트리구조가 아니다! (함수형-순서형이 아니다!) 정보는 그래프 형태이다!

 

단일 진실 공급원 Single source of Truth

특정 상태가 변경되었을 때, 다른 부수효과를 최소로 하기 위해 단일 상태로 만들자. → ID 하나만 가지고 찾자. (Normalization) React가 UI에 대한 HOW를 제공한다면, GraphQL은 데이터에 대한 HOW를 제공한다.

 

선언적으로 데이터 요청하기 Declarative Data Fetching

리액트 컴포넌트는 데이터에 의존한다. 리액트는 어떤 정보(WHAT)가 필요한지만 요청한다. GraphQL 같은 라이브러리는 데이터를 어떻게(HOW)를 담당해 알아서 데이터를 처리해 넘겨준다. 결국 **선언적(Declarative)**란 공통된 HOW를 잘 분석해서 꺼내고 이를 통해 내 코드에 WHAT을 잘 뭉치는 과정. 이를 위해 HOW와 WHAT을 잘 가르는 안목이 필요하다.

 

'Programming > 웹 프론트엔드' 카테고리의 다른 글

[React] 비동기함수 Hook으로 다루기  (0) 2023.05.30

+ Recent posts