[WANTED] 프리온보딩 코스 week 3-2

jun gwon·2022년 11월 26일
0

원티드 프리온보딩

목록 보기
11/14

들어가기

이번강의에서는 이전 과제에 대한 피드백과,
Redux, React-Query에 대해서 다루었습니다.

(해당 게시글은 실제 강의 내용을 그대로 옮겨적은것은 아니며, 주관적인 이해 및 판단으로 요약되어진 글입니다.)

강의 Section_1

과제 피드백

이전과 같은 형식으로 진행되었습니다.
공통적인 사항으로서, 이전까지의 대부분의 팀들은 CRA를 사용하고 있었는데, 꼭 CRA를 사용하기보단, Vite를 사용하여 React project를 만드는것도 좋을 수 있다고 권장해주셨습니다.

저번 과제의 주제이기도 하였고, 이번 강의에서의 주제이기도 한 캐시를 주제로 많이 다루었는데, 캐시 정책에 관한것이 기억에 남았습니다.

일반적으로 캐시 가능한 응답을 요청 받았을 경우 웹 브라우저의 캐시를 찾아보게 되는데, 만약 유효시간(max-age)내에 있는 캐시가 있다면 네트워크 요청없이 바로 응답을 보낼 수 있게됩니다.

이러한 설정은 네트워크의 Header 태그에서 많은 부분을 살펴 볼 수 있습니다.

하지만 유효시간이 지나고 요청이 들어올경우에는, 캐시 정책(cache-contorl)과 검증헤더 (date),etag를 사용하여 요청을 어떻게 할지 결정하게 됩니다.

cache-contorl의 private / public은 프록시 서버 (CDN)에 저장해도 괜찮은지 여부 입니다. 위와 같은 설정일 경우 브라우저 캐시에만 저장되게 됩니다. max-age=0인 경우는 캐시가 즉각적으로 만료됨을 의미합니다. must-revalidate는 캐시 만료 후 최초 조회시 원 서버에 검증하는 설정이며, 캐시 유효 시간이라면 캐시를 사용합니다.

우선 클라이언트와 서버측의 date, 마지막 수정시간을 비교하여, 데이터의 신선도(state)을 체크 하게 됩니다.

  • 만약 설정된 시간조건에 부합하다면 304를 응답하여서 Header만 리턴하고, 브라우저는 304 응답을 확인하고 기존의 브라우저 캐시를 갱신하고, 갱신된 캐시값을 사용합니다.
  • 설정된 시간조건에 부합하지 않는다면 새로운 컨텐츠를 받아오고 200 응답을 받아옵니다.

두번째로, Etag(Entity Tag)는 컨텐츠가 가지는 고유값으로서, ID역할을 수행합니다.

  • 요청이 들어왔을때 서버측 Etag와 일치하다면 304 요청과 브라우저는 기존 캐시값을 재사용합니다.
  • Etag값이 일치하지 않는다면 컨텐츠가 변경되었다는 의미이므로, 새로운 컨텐츠를 받아오고 200 응답을 받아옵니다.

여기까지, 직접적인 과제 피드백은 아니였지만 이전 과제가 캐시와 관련된 주제인 만큼. 이러한 주제를 짚고 넘어갔습니다.

기억에 남는 과제 피드백은 아래와 같았습니다.

  1. 저번 과제에서 검색어는 검색결과 리스트에 BOLD 처리를 하는게 요구기능중 하나였는데, 영문자일 경우 소문자만 처리하는 경우가 많았습니다. 하지만 이러한 경우에도 대소문자 상관없이 BOLD 처리를 해주는게 좋지 않았을까 하는 조언이 있었습니다.

  2. 캐시 데이터의 expiredTime을 지정하고, 캐시 값을 불러오거나 사용할때, 현재 시간과 비교하여 사용하는게 좋다는 조언이 있었습니다.

  3. import를 정리하는 방법으로서
    외부 의존성 / 내부 의존성(멀리서 가져오는) / 내부 의존성(가까이서 가져오는) 종류에 따라 줄바꿈을 하는 편이 좋을 수 있다는 조언이 있었습니다.

  4. 데이터를 처리할때, 성능 측정에 따라 사용하는 함수가 달라질수있는데, 이러한것은 근거로서 명시하는게 좋다고 조언하셨습니다.

이번 과제와 같은경우, 캐시데이터를 Obj 형식보다는, HashMap을 사용하는 경우 더 나은 성능을 보여서 채택한 팀이 있었습니다.

이러한 성능 테스트는
console.time("test")
console.timeEnd("test")
를 사용하여 테스트 해볼수있습니다.

다른팀의 과제중 좋았다고 생각한 코드는 아래와 같았습니다.

export class CacheService<K, V> {
  private state: Map<K, V> = new Map();

  setCache(key: K, value: V) {
    this.state.set(key, value);
  }

  getCache(key: K) {
    return this.state.get(key);
  }

  hasCache(key: K) {
    return this.state.has(key);
  }
}

(출처 : 수업 자료)

제거와 유효시간에 관한 기능은 없었지만, 위와 같은 구조로 코드를 짠다면 클린코드에 가깝게 구현할 수 있다는 생각이 들었습니다.

강의 Section_2

본 강의에서 주 주제는 리액트 쿼리였지만, 리덕스에 대해서 짧게 설명을 가졌습니다.

리덕스

리덕스란?

자바스크립트는 싱글 페이지 애플리케이션이 갖추어야 할 요건이 점점 복잡해지며, 더 많은 상태(state)를 관리할 필요가 생겨났습니다. state가 많아지면 많아질수록, 프로그래머는 어플리케이션에서 state를 통제하기 어려워지는 상황을 맞닥뜨리게 됩니다.
이러한 요건 속에서 리덕스는 데이터를 통제하기 위해 고안된 Flux(단방향 데이터 흐름) 패턴 기반의 구현체입니다.

리덕스는 위와 같은 흐름으로 작동됩니다.

우선적으로, Plain 자바스크립트 객체인 액션함수가 있어야하며, 디스패쳐를 통해 액션 함수를 실행합니다.
이 액션함수를 통해 순수 함수인 Reducer 내에서 데이터의 변경이 일어나고, 이 데이터는 Store에 저장되어 전역적으로 사용되어집니다.

순수 함수란, 동일한 값이 들어오면 항상 같은 값을 리턴하는 함수입니다.(외부 상태에 영향을 주지 않는 함수)

리덕스에서 스스로 소개하는 3가지 원칙은 다음과 같습니다.

  1. 진실은 하나의 근원으로부터
    • 애플리케이션의 모든 상태(state)는 하나의 저장소(store) 안에 하나의 객체 트리 구조로 저장되어야한다
  2. 상태(state)는 읽기 전용이다
    • 상태를 변화시키는 유일한 방법은 액션 객체를 전달하는 방법뿐입니다.
  3. 변화는 순수 함수로 작성되어야 한다.
    • 액션에 의해 상태 트리가 어떻게 변화하는것을 지정하는 리듀서 함수를, 순수 함수로서 작성해야 합니다.

리듀서의 컨셉은 아래와 같습니다.

  1. 리덕스 함수는 reducer를 인자로 받습니다.
  2. 클로저로 state와 listeners 를 갖습니다.
  3. state를 그대로 반환하는 getState 함수를 하나 가지고 있습니다.
  4. subscribe라는 함수에서 listnerer를 인자로 받아, 기존 클로저의 listeners 배열에 넣습니다. 받은 listnerer는 dispatch가 실행 될때마다 실행됩니다. 리턴값으로 unsubscribe 를 반환하여, 등록을 해제 할수있도록 해줍니다.
  5. dispatch 함수는 plain 객체인 action을 받아, 처음 생성시 전달받은 reducer에 기존 state와 action을 전달하여 state를 변경시킵니다.
  6. 3,4,5 함수를 생성하여, 객체 리터럴로 return하여 반환합니다.

4번같은 경우 React Redux를 통해 <Provider store={store}> 와 같은형식으로 뷰 바인딩을 하게 됩니다.

이러한 리덕스를 사용하게 된다면, 전역 상태관리를 보다 손 쉽게 할수있게되고, 멀리 떨어진 컴포넌트 간의 통신을 필요한 값만을 전달할수있게 해주어, props drilling을 막을 수도있게 됩니다.

React Query

리덕스의 문제점

리덕스는 확실히 비동기 통신에서의 데이터를 store에 보관하는데 많이 사용됩니다. 하지만 여기서 의문이 하나 발생할 수 있습니다. store에 보관된 값이 정말 현재 DB에 저장되어있는 값이라고 볼 수 있을까?

클라이언트에서 가져와 쓰는 순간부터 이미 그 데이터는 정말 진실한 데이터가 아닐 위험이 존재하지 않을 수 있을까?

state-while-revalidate

이러한 문제점을 개선하기위해, 리액트 쿼리는 state-while-revalidate라는 전략을 가지고 왔습니다.

이것은 두 가지 프로세스로 분리할 수 있는데.
요청이 들어왔을경우.
1. Cache-Control Header의 max-age를 확인하여, max-age값이 아직 유효하다면 기존 값을 그대로 사용하기때문에 아무것도 하지 않습니다. 하지만, max-age값이 넘어갈 경우 두번째 스텝으로 넘어갑니다.
2. stale-while-revalidate 값을 확인하여 두가지 경우의 수를 가집니다.
2-1. stale-while-revalidate 값을 넘지 않았다면 우선 아직 캐싱된 값을 사용합니다. 하지만 동시에 캐시된 응답의 사용을 지연시키지 않는 방식으로, 데이터에 대한 재검증 요청이 이루어집니다. 이 값은 기존에 캐시된 항목을 대체하고, max-age값에 비교되는 타이머를 재설정합니다.
2-2 만약 값이 넘었다면 캐시값을 사용하지 않고, 데이터를 새로 요청해서 최신화 합니다.

이러한 전략으로 리액트 쿼리는 데이터에 대한 진실한 데이터를 지키기 위해서 노력합니다.

React Query

우선, 리액트 쿼리는 stale-while-revalidate 값을 staleTime 이라는 값으로 설정합니다.
이 값은 개별적인 reactQuery 함수에 설정할수도, 전체적인 설정을 가지는 queryClient에서도 설정할 수 있습니다.

리액트 쿼리는 상당히 많은 기능들이 추상화 되어있습니다.
대표적인것을 꼽자면
1. 리액트 쿼리의 작동방식
2. 리액트 쿼리의 비동기 호출의 과정 자동화가 있습니다.


이미지 출처
1번에 대한 설명으로서, 리액트 쿼리는 위 이미지에서 보이는것과 같은 state를 가집니다.
2가지 관점으로 보여질 수 있을것 같습니다.
데이터를 받아 stale에서 active상태를 유지하는것과, 더 이상 사용되지 않는 Inactive상태 이렇게 두가지로 나뉘어질 수 있을것 같습니다.
우선, active에 대한 설명으로는 아래와 같습니다.

  • Fetching은 서버에서 데이터를 가져오는 것을 의미합니다.

  • Fresh는, 데이터를 막 받아온 상태이기때문에, 서버/클라이언트의 정보가 동일하다는것이 보장됩니다. 하지만 서버는 항상 데이터를 주고받기 때문에, 시간이 조금이라도 지나면 데이터의 동일성은 보장받기 어렵습니다. 때문에 react-query에서는 Fresh에서 stale 상태로 넘기는 옵션인 staleTiem의 기본값을 0(즉시)로 설정되어져있습니다.

  • Stale은 서버/클라이언트의 정보가 동일함을 보장할 수 없는(신선하지 않은) 상태입니다. 서버로부터 새로운 값을 받지 않았다면 Stale하다고 할 수 있습니다. 이경우 React Query는 값을 업데이트 하기 위해 새요청을 보냅니다.

    요청을 주고받는경우, 위의 3가지 상태가 계속해서 진행됩니다. 하지만 Stale에서 더 이상 사용되지 않는경우, Inactive상태로 변환됩니다.

  • Inactive는 해당 쿼리가 React Query 가비지 컬렉터에 의해 제거될 예정임을 알립니다.

  • Deleted는 삭제된 쿼리를 의미합니다.

    여기까지가 React Query의 기본적인 작동 개념이라고 볼 수 있을것 같습니다.

    이어서, 2번에 대한 설명으로는, 정말 많은 return값과 query option등이 있습니다.

    const {
    data,
    error,
    isError,
    isLoading,
    status,
    ...
    } = useQuery({
    queryKey,
    queryFn,
    cacheTime,
    enabled,
    onError,
    onSettled,
    onSuccess,
    select,
    staleTime,
    useErrorBoundary,
    ...
    })

    공식 Dev API출처
    더 많은 값들을 받아 올 수있지만, 대표적으로는 저런것 등이 있습니다.
    앞의 값은 객체 리터럴로 return된 값이고, 뒤는 리액트 쿼리 함수의 옵션입니다.

    data,isLoading 등은 기존 reducer등을 사용하여 많이 사용하였습니다.
    또한 onSuccess, onError등, 기존 middlewere에서 처리하던것들을 React-qeury는 단지 하나의 함수내에서 모두 처리 할 수 있도록 도와줍니다.
    기존 redux-thunk 또는 saga등을 통하여 server state를 처리하던것을 React-query는 레이어 전체를 추상화 시켜 개발자가 앱 내에서 서버 상태를 제외한 UI 상태에 더욱 집중 하여 개발 할 수 있도록 도와주었습니다.

    React Query 사용방법

    React Query의 대표적인 함수로는 3가지가 있습니다.

  • useMutaion : 데이터의 추가 및 수정등에 사용되어집니다.

  • useQuery : 데이터의 조회에 사용되어집니다.

  • queryClient.invalidateQueries : 전체 또는 특정 쿼리를 stale취급하고, 현재 사용되어지고 있는 query들은 백그라운드에서 refetch 시키게 됩니다. useMutaion과 같이, 값이 수정되는 작업 이후(onSuccess) 사용하여 쿼리를 stale상태로 옮겨 refetch 하는것을 도와줍니다.

    가장 많이 사용되어지는 useQuery에 대한것을 설명하자면,
    useQuery에 필수적인 props로 2가지를 필요로 합니다, 하나는 고유 키값, 하나는 fetch 함수입니다.
    키값은 반드시 배열로서 들어가야 합니다.

const [id, setId] = useState(1)

const { data } = useQuery(['item', id], () => fetchItem({ id }))

<button onClick={() => {
  // ✅ set id without explicitly refetching
  setId(2)
}})>Show Item 2</button>

(출처 : 수업 자료)

위와 같이, useQuery 함수는 첫번재 인자로 고유 키값 ['item', id]을 받고, 두번째로는 fetch 함수를 사용합니다. 이후 3번째 인자부터는 옵션이기 때문에 꼭 필요하지는 않습니다.

useQuery에서 응답된 data들은 고유 키값에 보관되어있고, 또한 캐시에 존재하는 응답값에 접근하여 값을 읽어 올수도, 업데이트 할 수도 있습니다. 때문에 useQuery는 전역 상태로도 생각 할수 있습니다.

이러한 키값들이 사용되는 경우가 많다면, 이러한것을 아래와 같이 묶음화해서 관리하는것도 좋은 방법일 수 있습니다.

  ['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]

=> todoKeys 객체에서 함수를 통해 키값을 관리

const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}

(출처 : 수업 자료)

React-Query를 커스텀 훅을 사용하여 관리하기

리액트 쿼리를 사용 view를 담당하는 컴포넌트에서 사용하는것은 별로 좋은 판단이 아닐것입니다. 기존 커스텀 훅을 사용한것처럼, 리액트 쿼리도 커스텀 훅을 사용하여, 필요한곳에서 적재적소에 사용하는것이 코드관리에 좋을것입니다.

export const useTodos = () => {
 const client = useQueryClient();

 const { data, ...queryResult } = useQuery("todos", todoRepository.getTodos);

 const createTodo = useMutation(todoRepository.createTodo, {
   onSuccess: () => client.invalidateQueries("todos"),
 }).mutate;

 const updateTodo = () => {
		// ...
	};

 const deleteTodo = useMutation(todoRepository.deleteTodo, {
   onSuccess: (data, id) => client.invalidateQueries("todos"),
 }).mutate;

 return {
   ...queryResult,
   todos: data,
   createTodo,
   updateTodo,
   deleteTodo,
 };
};

(출처 : 수업 자료)

  • todoRepository는 todo 관련 api통신을 하는 class입니다.
  • useMutation 함수는 키값이 필요하지 않고, fetch 함수를 필수 인자로 합니다
  • 성공 후, invalidateQueries 함수를 실행하여 기존 캐시값의 todos 값들을 stale로 옮겨 refetch 시킬 수 있도록 합니다. 위와 같이, 커스텀 함수를 사용하여 관리한다면 기존 redux를 사용하여 서버 상태관리를 했을때와 비교했을때 훨씬 더 간결하고 응집도가 높게 관리할수있습니다.

    co-location(함께 위치시키기) 방식의 파일구조.

    리액트 쿼리의 기능과 직접적인 관련이 있는것은 아니지만,
    리액트 쿼리의 메인터너는 쿼리 파일을 기능 파일에서 떨어진곳에 위치하거나, 한곳에 위치하는것 보다는, 기능 디렉터리에 함께 보관하는것을 제안하였습니다.
  - src
  - features
    - Profile
      - index.tsx
      - queries.ts
    - Todos
      - index.tsx
      - queries.ts

이러한 형식으로 코드구조를 짜게 된다면, queries 파일 뿐만아니라, api,hooks 및 기타 필요한 파일 및 폴더 등이 모두 features의 각 feature 항목에 위치시키게 될것입니다.

마무리 소감

사실, 글 올리는 시점 기준으로, 수업 자체는 거의 2주전이였습니다.
그 사이에 1주일 짜리 과제가 끝나고, 전체적인 과정이 끝난 1주일간은 밀린 일들을 처리하고, 밀려있던 게으름 등.. 여러 이유를 핑계로 글 쓰는게 늦어진것 같습니다.

이번 강의에서는 header에서 캐시를 살펴보는 방법에 대해서 알아보았던점이 좋았고,
react query를 사용하기이전에는 거의 항상 사용하던 redux의 개념 및 컨셉, 3가지원칙등에 대해서 자세히 알아보게 되어 좋았습니다.
그래도 프로젝트가 끝난 시점에서 말하는거지만, 가장 좋았던건 react-query에 대해서 알게 된점인것 같습니다.
실제로 사용해보니 마법 같은 Lib였던것 같습니다.
기존에 번거로웠던 loading 및 error 처리등을 리액트 쿼리를 사용한 fetch 요청 한번에 모두 객체 리턴값으로 받아 사용 할 수있고, 성공시 method를 추가 할수있는점, 자동으로 캐시를 사용하여 관리해준다는점. 그리고 활용하는 방법으로서의 커스텀훅 방법 등.. 개념적으로도, 실무적인 점으로도 많은 점을 배울 수 있어 좋았습니다.
하지만, 이번 강의를 들으면서 한편으로는, 단순히 마법같은 lib를 그저 받아들이는게 아니라, 실제로 파헤쳐보고 이해해봐야 하는 필요성을 느낄 수 있었습니다.
아마, 지금은 아니겠지만.. 언젠간 react를 비롯해서 자주 사용하는 Lib등을 모두 소스를 파헤쳐보고 싶어졌습니다.

0개의 댓글