들어가며

Tanstack Query의 useQuery는 HTTP GET 요청을 보낼 때 사용하는 함수입니다.

이 함수에는 GET 요청에 사용될 비동기 함수를 지정하는 queryFn과, 받아온 데이터를 캐싱할 queryKey를 지정할 수 있습니다.

 

공식문서 보기

 

이때, queryFn의 인자로 아래 코드와 같이 QueryFunctionContext가 제공되는데, 이를 활용하는 방법에 대해 알아보겠습니다.

 

// useQuery에서 제공되는 QueryFunctionContext
  const { data, isPending, isError, error } = useQuery({
    queryKey: ['post', postId],
    queryFn: (QueryFunctionContext) => user_async_function() // QueryFunctionContext의 위치
    enabled: postId !== undefined
  })

QueryFunctionContext의 구성요소

queryFnQueryFunctionContext를 객체 형식의 인자로 제공합니다.

 

공식문서에서는 다음과 같이 설명해줍니다.

[공식문서]
The QueryFunctionContext is the object passed to each query function. It consists of:
1. queryKey: QueryKey - Query Keys  
2. signal?: AbortSignal
- AbortSignal instance provided by TanStack Query
- Can be used for Query Cancellation
3. meta: Record<string, unknown> | undefined
- an optional field you can fill with additional information about your query

 


queryKey

queryKey는 해당 useQuery에서 사용된 queryKey를 반환합니다.

 

이는 Query Key에 변수 값을 사용할 경우, 해당 값을 queryFn의 인자로 적용해야 할 때 유용합니다.

 

예를 들어, 글의 ID값을 기반으로 API를 호출한다고 가정해보겠습니다.

 

[대략적인 프로세스]

  1. 글의 id 값을 async fetch. (다른 API를 통해)
  2. 해당 id를 가진 글을 사용자가 클릭하면, 글의 정보를 async fetch.
  3. 이때, queryKey를 ['post', postId] 형태로 관리.

해당 로직은 기본적으로 다음과 같이 useQuery를 활용하여 구현할 수 있습니다.

// #0. Fetch Info using planId (useQuery)
  const { data, isPending, isError, error } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => getPlan({postId: postId})
    enabled: postId !== undefined
  })

 

이때, 제공되는 queryKey 값을 사용하면 컴포넌트에서 관리하는 state대신 useQuery에서 사용되는 queryKey를 사용할 수 있습니다.

  • queryKey는 위에서 제공해준 값과 동일하게 주어진다. (예시의 경우 ['post', postId])
const { data, isPending, isError, error } = useQuery({
  queryKey: ['post', postId],
  queryFn: ({ queryKey }) => getPlan({ postId: queryKey[1] }),
  enabled: postId !== undefined,
});

 

이처럼 queryKey를 통해 postId 값을 쉽게 활용할 수 있습니다.

그래서 뭐..? 같은 값에 접근하여 사용하는거 아닌가..?🤔

네 맞습니다.

 

위 예시는 postId에 해당하는 하나의 변수만은 queryKey에 사용하지만, 복잡한 구조를 가진 코드에서 변수 값을 찾으러 다니기 보다, 가장 가까운 queryKey에서 찾는 것이 미래의 내가 코드를 다시 읽을때 편리하지 않을까요?

아래 그림의 2번 루트가 1번 루트보다 가깝다고 할 수 있으니, 디버깅 하기에 원활하지 않을까 생각합니다.

queryKey 부연설명


signal: AbortSignal

(AbortSignal은 Typescript에서 사용하는 type입니다. 기본적으로 제공되는 타입이므로 import하지 않아도 됩니다.)

 

다음은 signal입니다. 개인적으로 이해하는데 상당히 애먹었던 개념입니다.

먼저, MDN에서는 Signal에 대해 이렇게 설명합니다.

[MDN, AbortSignal]
AbortSignal 인터페이스는 DOM 요청(Fetch와 같은)과 통신하고 필요한 경우 AbortController 객체를 통해 취소할 수 있게 해주는 신호 객체를 나타냅니다.

 

직역하면, “통신을 취소”할 때 발생하는 “신호”로 이해할 수 있습니다.

 

 

이때, queryFn에서 제공하는 signal은 useQuery로 인해 발생하는 signal을 자동으로 관리하는 역할을 수행합니다.

즉, Tanstack Query의 라이프사이클(쿼리 재시작, 컴포넌트 언마운트 등)에 따라 자동으로 쿼리를 취소하는 역할을 합니다. 따라서, 개발자는 fetch의 옵션으로 signal을 제공해주기만 하면 됩니다.

 

구현 예제

async function fetch_example({ queryKey, signal }) {

	// 다음과 같이 signal을 옵션으로 넘겨주면 됩니다
	const response = await fetch(`/api/todos?status=${status}&page=${page}`, { signal });
	if (!response.ok) {
		throw new Error('Failed to fetch data');
	}
	return response.json();

	if (err.name === 'AbortError') {
		console.log('Request aborted');
		return;
	}
	throw err;
}

 

Signal에 대해 이해하셨다면, 이를 직접 생성하여 fetch 요청을 자유자재로 다룰 수 있습니다.

 

예를 들어

유저는 너무 길어지는 GET 요청에 화난 나머지, “취소” 버튼을 눌러 요청을 중단하고 싶다고 하면, 해당 버튼을 클릭했을때 요청이 중단되어야 합니다. 그렇지 않으면, 화난 유저는 사이트를 떠나 버리거나 쌍욕을 하며 회원탈퇴를 해버릴 수도 있습니다.

 

또는 개발자 측에서 길어지는 요청을 방지하기 위해, 의도적으로 Abort 시키는 방법도 있습니다.

이후, 유저에게 “네트워크 상황을 확인해주세요” 토스트를 띄워, 유저의 네트워크 상황에 의한 시간지연을 웹사이트 성능의 문제점으로 인식시키지 않도록 할 수 있습니다.

 

 

이처럼, AbortSignal은 요청을 취소하는 신호 객체로, 긴 요청을 중단하거나 사용자가 중단 버튼을 클릭했을 때 유용합니다.

 

다음은 일정시간이 지난 이후, 의도적으로 개발자가 요청을 중단시키는 코드입니다.

구현 예제

async function fetchWithTimeout({ queryKey, signal: querySignal }) {
  const [_key, { status, page }] = queryKey;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 10000); // 10초 후 요청 중단

  const signal = querySignal || controller.signal;

  try {
    const response = await fetch(`/api/todos?status=${status}&page=${page}`, { signal });
    clearTimeout(timeoutId);

    if (!response.ok) {
      throw new Error('Failed to fetch data');
    }
    return response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('Request aborted');
      return;
    }
    throw err;
  }
}

 

 

useQuery는 기존처럼 동일하게 사용하면 됩니다.

function Todos({ status, page }) {
  const { data, error, isLoading } = useQuery({
    queryKey: ['todos', { status, page }],
    queryFn: fetchWithTimeout,
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

🤔근데, AbortController을 통해 객체를 따로 생성하는 이유는 무엇이에요?

말씀드렸다 시피, queryFn에서 제공하는 signal은 Tanstack Query의 라이프사이클(쿼리 재시작, 컴포넌트 언마운트 등)에 따라 자동으로 쿼리를 취소하는 역할을 하므로, 특정한 상황에 대응하기 위해서는 객체를 직접 생성해야 합니다.

 

이를 활용하여 다양한 fetch에 대한 커스텀 훅을 만들어서 npm package로 관리해보는 것도 좋은 방법일 것 같습니다.


meta

meta는 개발자가 원하는 값을 객체 형태로 추가할 수 있는 필드입니다.

 

예를 들어, fetch마다 다른 에러 메시지를 관리하고 싶을 때 활용할 수 있습니다.

 

이때, v5부터 onError, onSuccess, onSettled 과 같은 콜백 함수들을 더이상 useQuery에서 사용할 수 없으므로, QueryClient 객체에서 다음과 같이 설정해주어야 합니다.

 

[콜백함수가 사라진 이유]

 

구현 예제

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      if (query.meta.errorMessage) {
        toast.error(query.meta.errorMessage);
      }
    },
  }),
});

export function useTodos() {
  return useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
    meta: { errorMessage: 'Failed to fetch todos' },
  });
}

 

 


마치며

QueryFunctionContext는 queryKey, signal, meta와 같은 다양한 기능을 제공합니다.

창의적이고 새로운 기술을 만드는 것도 좋지만, 사용하고 있는 기술에 대해 깊이있게 이해하는 것도 중요하다고 생각되는 공부였습니다.

 

참고 문헌

TODO LIST

  • 길어지는 요청에 대한 fetch 함수를 커스텀 함수로 만들어서 관리하기
  • queryFn의 abort에 대한 역할을 직접 network tab에서 관찰해보기

들어가며

typeof 연산자와 keyof 연산자, keyof typeof 에 대해 알아보고 다음편에 keyof 연산자를 활용할 수 있는 유용한 맵드 타입에 대해 알아보겠습니다.

 


typeof

TypeScript 공식문서

JavaScript에서는 이미 표현식 컨텍스트에서 사용할 수 있는 typeof 연산자가 있습니다.TypeScript는 타입 컨텍스트에서 변수나 프로퍼티의 타입을 추론할 수 있는 typeof 연산자를 추가합니다.

 

간단하게 정리하면, typeof 연산자는 type을 반환합니다.

다음 예시에서, 객체 a의 형태에 따라 type이 선언되는 것을 어렵지 않게 확인 가능합니다.

const a = {
  a: 1,
  b: true
}

type check = typeof a
// 결과 (Hover)
type check = {
    a: number;
    b: boolean;
}

 

typeof 연산자의 특징으로는 자바스크립트로 선언된 변수 혹은 프로퍼티의 Type을 반환한다는 것입니다.

 

다음은, keyof 연산자에 대해 알아봅시다.

 


keyof

TypeSricpt 공식문서

keyof 연산자는 객체 타입에서 객체의 키 값들을 숫자나 문자열 리터럴 유니언을 생성합니다. 


객체의 키 값들을 숫자나 문자열 리터럴 유니언으로 생성한다는 것이라..

머리로는 이해가 되나 마음으로는 이해가 되지 않습니다.

 

다음 예시를 보고 이해해봅시다.

기존에 정의된 Color Type을 Key값들로 이루어진 새로운 type을 생성하고 있는 예시입니다.

type Color = {
    red: string;
    yellow: string;
    isDrawing: boolean;
}

type check = keyof Fruit;
// 결과 (Hover)
type keys = "red" | "yellow" | "isDrawing"

 

결국, keyof 연산자는 type에 적용하는 문법인 것입니다.

 

정리하면

typeof는 자바스크립트 변수 → Key로 이루어진 Type

keyof는 Key로 이루어진 Type → Key로 이루어진 유니언

연결고리가 보이시나요? 아래 그림을 보시면 이해가 되실겁니다.

(주의할 점으로, keyof와 typeof 연산자는 접두사 이므로, 뒤에 오는 연산자가 먼저 실행됩니다)

keyof typeof 

결국 keyof typeof 는 자바스크립트 객체로 선언된 변수의 Key 값들로 이루어진 Type을 선언할때 사용됩니다.

 

개인적인 견해로는 변수를 선언할때는 type을 지정해주는 경우가 대부분입니다. 따라서, 특정 type의 key들로 이루어진 유니언이 필요하면, keyof 연산자만을 사용하면 됩니다. 다만, 간혹 type을 지정하지 않은 객체의 key들로 이루어진 유니언을 일시적으로 사용하고 싶을때 keyof typeof 를 사용하면 될것 같습니다.

 


 

이상, typeof keyof연산자와 keyof typeof 에  대해 배워보았습니다.

해당 포스트에서 틀린 내용이 있으면 댓글로 알려주시면 감사하겠습니다 :)

들어가며

타입스크립트를 이용하여 개발을 하고 있지만, 여전히 헷갈리는 문법들이 많고 제대로 활용하지 못하고 있다는 생각에 처음부터 깊게 파고들어야 하는 필요를 느꼈다.

이에, 우아한형제들에서 제작한 타입스크립트 책을 토대로 단단한 타입스크립트 실력을 다지기 위해 작성한다.


웹 개발의 역사

1. 자바스크립트와 ECMASCRIPT

TL;DR

  • 자바스크립트는 두 브라우저의 경쟁 속에 탄생되었으며, 이에 따라 크로스 브라우징 이슈가 존재했다
  • 크로스 브라우징을 해결하기 위해 Ecma (국제 표준화 기구)에 자바스크립트 표준화 규격을 제시, 채택되었다.

자바스크립트는 1990년대 마이크로소프트의 IE와 넷스케이프 커뮤니케이션즈의 넷스케이프 내비게이터(첨 들어봄) 두가지 브라우저를 가장 많이 사용하던 당시에 웹의 다양한 콘텐츠를 표현하고자 만들어졌다. 

당시, IE와 내비게이터의 DOM 구조는 완전히 달랐다. 따라서, 개발자는 브라우저마다 코드를 다르게 작성해야 했다(크로스 브라우징). 또한, 새로운 버전의 브라우저가 출시되어 자바스크립트의 새로운 기능을 지원하더라도 사용자가 예전 브라우저를 사용한다면 해당 기능을 사용할 수 없게 되었다. 이에 대응하기 위해 폴리필(Polyfill)과 트랜스파일(Transpile)이 등장하였다.

 

폴리필 (Polyfill)

지원하지 않는 이전 브라우저에서 최신 기능을 제공하는 데 필요한 코드.

 

트랜스파일 (Transpile)
한 언어로 작성된 소스 코드를 비슷한 수준의 추상화를 거친 다른 언어로 변환하는 것.

트랜스파일은 컴파일의 Subset으로, 추상화 정도가 유사한 언어를 다른 언어로 컴파일하는 것을 특별히 Transpile이라 부른다. 예를 들어, 높은 버전의 자바스크립트 코드를 호환성을 위해 낮은 버전의 자바스크립트로 변환하는 Babel, tsc, ESBuild, SWC가 이에 해당한다.**

 

따라서, 모든 브라우저에서 동일하게 작성될 수 있는 표준화된 자바스크립트의 필요성이 제기되었고습니다.

이에 넷스페이크는 컴퓨터 시스템의 표준을 관리하는 Ecma 인터내셔널에 자바스크립트 기술 규격을 제출하였고 이를 ECMAScript 라는 이름으로 표준화를 공식화하였다.

 


2. 개발 생태계의 발전

TL;DR

  • 대규모 프로젝트가 많아짐에 따라, 컴포넌트 단위 개발이 늘어났다

대규모 프로젝트가 늘어남에 따라, 컴포넌트 단위로 개발하는 방식이 생겨났다. 또한, 서비스 규모가 커짐에 따라 다뤄야 하는 데이터가 폭발적으로 늘어났고, 표현해야 하는 화면도 다양해졌다. 이를 테면, 모바일, 패드, 노트북, 랩톱 PC가 해당된다. 게다가 사용자는 저마다의 디바이스의 최적화된 UI/UX를 기대한다.

이러한 상황에 맞물려 컴포넌트 베이스 개발 (CBD, Component Based Development) 방법론이 등장했다.

컴포넌트를 설계할 때 중요한 점은 의존성을 최소화하거나 없애는 것이다. 개발자는 컴포넌트 간의 의존성을 파악하고 이를 최소화하여야 변화에 대응할 수 있다.

 


3. 협업을 위한 도구

TL;DR

  • 자바스크립트는 동적 언어 타입이므로, 협업 및 개발에 불리하다.
  • 타입스크립트는 자바스크립의 슈퍼셋 언어이다.

하나의 프로젝트에 많은 인원이 투입되고, 채용상황에 따라 수시로 개발자가 바뀌는 상황에서 과거의 코드를 타 개발자가 유지보수하기 쉽게 개발하는 것은 선택이 아닌, 필수가 되었다. 하지만, 자바스크립트 자체는 이를 방해하는 1등 방해 요소였다.

 

왜 자바스크립트가 뭐 어째서!?

자바스크립의 한계

1. 동적 언어 타입
자바스크립트는 동적 타입 언어이다. 즉, 변수에 타입을 명시적으로 지정하지 않고 코드가 실행되는 런타임에 변숫값이 할당될 때 해당 값의 타입에 따라 변수 타입이 결정된다. 예를 들어 해당 코드에서 자바스크립트는 변수 a 의 타입이 number인지 string인지 실제 코드가 동작할 때 a에 값이 할당되는 순간 그 값이 1인지 ‘1’인지에 따라 결정된다. 따라서, 아래 코드는 최종적으로 string 결정된다.

let a = 1
a = '1'

 

2. 동적 언어 타입의 한계
동적 언어 타입의 한계는 다음 예시로 쉽게 설명가능하다.

const sum = (a,b) => {
	return a+b;
}

sum(100) // NaN
sum("a", "b") //ab


해당 코드는 에러를 발생시키지 않고 정상적으로 동작한다.또한, 해당 코드는 상식적으로 두 숫자를 더하는 함수이지만, 어떤 개발자는 두 문자열을 합치려고 사용한 코드일 수도 있다.
따라서, 개발자가 실수할 여지도 많아지며 협업하는 과정에서 일관성을 지키기 어려워진다.
하나의 숫자만 전달했을 때 b를 undefined로 두는 관대함을 지닌 자바스크립트의 특징 때문에, 오류를 발생시키지 않는다. 자바스크립트는 b를 적절한 타입인 NaN로 형변환 후 실행을 이어간다.

3. 타입스크립트

타입스크립트는 마이크로스프트에서 제시한 자바스크립트 SuperSet이다.

SuperSet
기존 언어에 새로운 기능과 문법을 추가해서 보완하거나 향상하는 것을 말함. 슈퍼셋 언어는 기존 언어와 호환되며 일반적으로 컴파일러 등으로 기존 언어 코드로 변환되어 실행됨.

 

타입스크립트는 다음과 같은 장점을 제공한다.

  1. 안정성 보장 → 정적 타이핑 제공, 컴파일 단계에서 타입 검사를 해준다
  2. 개발 생산성 향상 → IDE에서 타입 검사를 해주며, 이를 통해 타입 추론이 가능하다

협업에 유리 → Interface, Generic 등을 통해 타입을 지정하면 코드 이해와 공유가 편리하다.

 


마치며

자바스크립트의 탄생과 ECMAScript가 무엇인지, 타입스크립트의 특징에 대해 알아보았다.

기사와 같은 과정들을 읽으며 느낀 점은 현대의 개발자는 데이터를 잘 다루는 능력이 중요하다는 생각이다.

어느 분야를 가던지 데이터를 다루는 방법이 우선시된다.

프론트엔드는 캐싱, Lazy Loading 등과 같은 최적화 방법이 중요하듯이 말이다.

백엔드는 말할 필요도 없고, 빅데이터 분야 심지어 머신러닝과 AI도 사실은 데이터를 다루는 학문이 아닌가?

+ Recent posts