1.React Query v5부터 useQuery의 API 제거

출처 - TkDodo’s Breaking react Query’s API on purpose

  • 리액트 쿼리 v5 부터 useQuery의 onSuccess onError onSettled api 가 제거 될 예정이라고 한다. (useMutation의 경우 해당 api는 유지된다.)
  • 제거되는 이유는 간단히 말해서 사용자가 원하는대로 동작하지 않기 때문이라고 한다. 하지만 몇가지의 이유가 있다.

1. 상태의 싱크가 맞지 않는 문제

특히나 많은 사람들이 아래와 같이 상태를 동기화시키기 위해서 해당 api 를 사용한다.

function useTodos() {
2  const [todoCount, setTodoCount] = React.useState(0)
3  const { data: todos } = useQuery({
4    queryKey: ['todos', 'list'],
5    queryFn: fetchTodos,
7    onSuccess: (data) => {
8      setTodoCount(data.length)
9    },
10  })
11
12  return { todos, todoCount }
13
}

위와 같은 onSuccess api 사용은 state를 변경함으로써 불필요한 렌더링을 추가함과 동시에 아래와 같은 세단계의 렌더 사이클을 가지게 된다고 한다.

  1. todosundefined이고 length 가 0이다. 이는 query 가 fethcing 되는 동안의 정상적인 초기 값이다.
  2. todos가 길이가 5인 배열이 되고, todoCount는 0이 된다. (Out of sync 발생)
    • useQuery른 한번 실행 되었으나 (onSuccess도 포함), setTodoCount가 scheduled 상태이다. 따라서 값들이 동기화 되지 못한다.
  3. todos가 길이 5인 배열이 되고, todoCount는 마침내 5가 된다.

이처럼 동기화가 되지 못하는 문제는 아래와 같이 상태를 onSucces로부터 추출함으로써 간단하게 해결된다. 즉, onSuccess는 필요가 없어진다.

function useTodos() {
2  const { data: todos } = useQuery({
3    queryKey: ['todos', 'list'],
4    queryFn: fetchTodos,
5  })
6
7  const todoCount = todos?.length ?? 0
8
9  return { todos, todoCount }
10
}

2. 콜백이 실행되지 않을 수 도 있다.

function useTodos(filters) {
  const { dispatch } = useDispatch();

  return useQuery({
    queryKey: ['todos', 'list', { filters }],
    queryFn: () => fetchTodos(filters),
    staleTime: 2 * 60 * 1000,
    onSuccess: (data) => {
      dispatch(setTodos(data));
    },
  });
}

위와 같은 staleTime을 가졌을때 해당 쿼리는 아래와 같이 동작한다.

  1. filter의 조건이 done:true 로 넘어왔을 때, 리액트 쿼리는 fethcing을 하고, 해당 데이터를 캐싱한다. onSuccess는 dispatch 를 실행한다.
  2. 1번의 쿼리 이후 filter의 조건을 done:false가 넘어왔을 때 리액트 쿼리는 fethcing을 하고 해당 데이터를 캐싱하낟. onSuccess는 dispatch를 실행한다.
  3. 1번과 2번의 쿼리 이후 filter의 조건을 다시 done:true로 변경했을 때, 캐싱된 데이터를 가져와서 (즉, 데이터는 변경되었어도 fetch는 하지 않았어서) onSuccess를 실행하지 않는다. 따라서 dipsatch 실행도 없다. 오류가 발생한다.

즉, staleTime이 지정되어있어서 캐싱된 데이터를 가져올 때는 데이터가 변경되어도onSuccess도 실행하지 않게 된다. 이는 의도치 않은 버그를 야기하게 된다.

따라서 아래와 같이 작성하는 편이 훨씬 더 안정적이다.

function useTodos(filters) {
  const { dispatch } = useDispatch();

  const query = useQuery({
    queryKey: ['todos', 'list', { filters }],
    queryFn: () => fetchTodos(filters),
    staleTime: 2 * 60 * 1000,
  });

  React.useEffect(() => {
    if (query.data) {
      dispatch(setTodos(query.data));
    }
  }, [query.data]);

  return query;
}

그러면 에러 처리는 어떻게?

  • 글로벌 onError 옵션 혹은 ErrorBoundary 사용을 권장한다.
  • 아래와 같이 쿼리 내 meta옵션을 통해서 각 쿼리마다 필요한 데이터를 global onError 에 넘길 수 있다.
export function useTodos() {
12  return useQuery({
13    queryKey: ['todos', 'list'],
14    queryFn: fetchTodos,
15    meta: {
16      errorMessage: 'Failed to fetch todos',
17    },
18  })
19}

오해하지 말것. useMutation에는 사라지지 않는다. useQuery에서만 사라진다.

처음에 해당 변경점을 들었을 때, 깜짝놀랐지만 멋진 선택이라고 생각이 든다. 라이브러리의 버그가 아니라 사용자들을 괴롭게 하는 요소를 지우는 선택을 하는게 쉽지는 않을텐데 멋지다고 생각했다.


3. iframe 내의 페이지와 통신하기

  • window.postMessageapi 를 사용해 iframe 안의 페이지와 메시지를 주고받을 수 있다. 도메인이 다른 경우에도 사용 가능하다.

송신측에선 아래와 같이 iframe 요소에 해당 api 를 사용한다.

iframeTragetElement.postMessage(message, targetOrigin, [transfer]);

그리고 수신측에서는 message 이벤트를 통해서 외부로부터 송신된 메시지를 받을 수 있다.

window.addEventListener('message', receiveMessage, false);

function receiveMessage(event) {
  if (event.origin !== 'http://example.org:8080') return;

  // ...
}

4. 미디어쿼리 이것저것

사내에서는 반응형을 기본으로 작업한다. 그러다보니 미디어쿼리를 만질 일이 많은데 그 때 참고하면 좋을만한 것들을 발견했다! 출처-The complete guide to CSS media queries | Polypane, The browser for ambitious web developers

// 이런식으로도 되고
@media not print {
  /*...*/
}

// Nesting도 되고
@media (min-width: 400px) {
  @media (max-width: 800px) {
    /*...*/
  }
}

// 이것도 되고
@media (hover: none) and (pointer: coarse) {
  /* you're on a touch-only device */
}

그 외에도 많은 문법들이 되는데 처음보는 것들이 대부분이었다. 잘 사용하면 반응형할 때 빠르게 작업 할 수 있을 것 같다.

matchMedia api

window.matchMedia 라는 함수를 사용하여 자바스크립트에서 쉽게 미디어쿼리를 사용할 수 있다. 인자에는 meida query 문법이 들어간다. 그러면 아래와 같이 match 여부와 media query의 문법을 받을 수 있다.

const match = window.matchMedia("(min-width: 400px)");

// 결과
{
  matches: true,
  media: "(min-width: 400px)",
}

matchMedia와 함께 이벤트 리스너를 사용하여 미디어쿼리의 변경사항을 추적할 수도 있다.

const match = window.matchMedia('(min-width: 400px)');

match.addEventListener('change', (e) => {
  if (e.matches) {
    /* do a thing */
  } else {
    /* do another thing */
  }
});

5. sort 대신 toSorted

Array.prototype.toSorted() - JavaScript | MDN

인터페이스는 sort와 동일하되, 배열의 복사본을 정렬하여 반환한다.


그 외 도움이 되었던 좋은 아티클들