인프런 커뮤니티 질문&답변
63,65 중복
해결된 질문
작성
·
29
·
수정됨
0
탄스택쿼리 파트를 먼저 수강중에 중복되는 자료가 있어 말씀드려요. 63,65강의 자료가 동일합니다.
추가적으로, 65강 처음에 나오는 코드는 useEffect가 아닌 useQuery를 통한 분기처리인데, 이 경우 race condition위험이 없지 않나요?
답변 1
0
안녕하세요 훈씨님! 우선 자료 오류로 인해 학습에 불편을 드린 점 진심으로 사과드립니다. 말씀해 주신 63강과 65강의 중복된 자료 부분은 꼼꼼히 확인한 뒤, 수강에 지장이 없으시도록 바로 수정해 두겠습니다. 학습하시다가 또 다른 수정 사항이 발견되면 언제든 편하게 말씀해 주시기를 바라며, 훈씨님의 열공을 항상 응원하겠습니다.
추가로 남겨주신 질문 내용도 무척 흥미롭게 읽었습니다. 비동기 처리를 다루시면서 경합 조건, 즉 레이스 컨디션(Race Condition) 문제까지 깊게 고민해보신 점이 깊은 인상을 주었습니다. 저 역시 실무 환경에서 여러 아키텍처를 설계하며 치열하게 고민했던 주제이기에 제 경험을 바탕으로 생각을 나누어 보겠습니다. 결론부터 말씀드리자면, 65강 초반에 나오는 코드처럼 useQuery나 useSuspenseQuery를 통한 분기 처리를 활용할 경우에는 우려하신 레이스 컨디션 문제가 발생하지 않겠습니다. 질문해 주신 내용은 과거 명령형 방식인 useEffect로 데이터를 페칭할 때 저를 포함한 많은 개발자가 실무에서 빈번하게 마주했던 고질적인 버그였습니다. 탠스택 쿼리(TanStack Query)와 같은 라이브러리는 바로 이러한 문제를 선언적으로 해결하기 위해 도입하는 도구라고 생각합니다. 제가 실제 프로젝트를 진행하며 겪었던 상황들을 떠올려보며, 기존 방식과 어떤 차이가 있는지 상세히 공유해 드리겠습니다.
먼저 useEffect가 레이스 컨디션에 취약했던 이유를 제가 경험했던 이커머스 쇼핑몰의 카테고리 필터링 기능에 빗대어 설명해 보겠습니다. 어떤 고객이 의류, 신발, 모자 순으로 카테고리를 매우 빠르게 연속해서 클릭했다고 가정해 보겠습니다. 이때 네트워크 상황에 따라 의류 데이터 응답에는 3초, 신발 데이터에는 2초, 그리고 가장 마지막에 클릭한 모자 데이터에는 1초가 소요된다고 생각하면 문제가 명확해집니다. 응답 시간이 가장 짧은 모자 데이터가 제일 먼저 도착해서 화면에 성공적으로 렌더링되겠습니다. 진짜 문제는 바로 그 직후에 발생합니다. 3초 뒤, 네트워크 지연으로 가장 늦게 도착한 의류 데이터가 방금 잘 그려진 모자 화면을 무참히 덮어씌워 버리는 현상이 일어납니다. 결과적으로 고객 화면의 상단 필터에는 분명히 모자가 선택되어 있지만, 실제 하단 상품 목록은 의류가 노출되는 치명적인 버그로 이어지게 됩니다. 과거에는 이를 방어하기 위해 useEffect 내부에서 AbortController를 사용해 이전 요청을 수동으로 취소하거나, 불리언(boolean) 플래그 변수를 선언하고 클린업 함수를 복잡하게 작성하는 등 직접적인 제어 코드를 구현해야만 했습니다. 이해를 돕기 위해 문제가 발생하던 과거의 취약한 코드 구조를 보여드리겠습니다.
// 레이스 컨디션 버그에 노출된 과거의 명령형 페칭 방식 예시
useEffect(() => {
const fetchProducts = async () => {
// 카테고리가 변경될 때마다 호출되지만, 응답이 도착하는 순서는 보장되지 않음
const data = await getProductsByCategory(categoryId);
// 3초 뒤 늦게 도착한 과거 데이터가 최신 상태를 덮어씌우는 치명적 버그 발생
setProducts(data);
};
fetchProducts();
}, [categoryId]);
하지만 최근 실무에서 활용하는 useSuspenseQuery를 도입하면 별도의 방어 코드를 작성하지 않아도 이 문제가 아주 자연스럽고 자동화된 방식으로 해결됨을 체감할 수 있었습니다. 그 해결의 핵심은 바로 쿼리 키(Query Key)에 있습니다. 코드에서 쿼리 키를 유저 아이디나 카테고리 아이디로 설정했던 부분을 떠올려보시면 이해가 한결 수월하시겠습니다. 앞서 말씀드린 쇼핑몰 상황에 이를 대입해 보면, 고객이 선택한 카테고리가 의류에서 모자로 바뀌는 순간 탠스택 쿼리는 즉각적으로 현재 활성화된 쿼리 키가 모자라는 사실을 내부적으로 인지하고 추적합니다. 그렇기 때문에 네트워크 지연으로 뒤늦게 의류 데이터가 도착하더라도, 이를 이미 지나간 유효하지 않은 데이터로 간주하여 화면 상태에 전혀 반영하지 않고 깔끔하게 무시하게 됩니다. 만약 쿼리 함수에 AbortSignal까지 적절히 연결해 두었다면, 이전 네트워크 요청 자체를 취소해 버리기도 합니다. 결과적으로 복잡한 상태 제어 로직을 직접 짜지 않아도, 화면은 항상 고객이 가장 마지막에 클릭한 상태인 최신 쿼리 키의 데이터와 완벽하게 일치하도록 보장받을 수 있었습니다. 이 선언적인 구조는 다음과 같이 훨씬 간결하게 작성됩니다.
// 탠스택 쿼리를 활용하여 레이스 컨디션이 자동 해결된 선언적 방식 예시
const { data: products } = useSuspenseQuery({
queryKey: ['products', categoryId], // 카테고리 아이디를 쿼리 키로 실시간 추적
queryFn: ({ signal }) => getProductsByCategory(categoryId, { signal }),
});
// 쿼리 키가 '모자'로 변경되면, 늦게 도착한 '의류' 응답은 무시되거나 signal에 의해 취소됨
여기에 서스펜스(Suspense) 기술이 더해지면 실무 환경에서 UI의 안정성은 더욱 극대화됩니다. 방대한 데이터를 다루는 어드민 대시보드나 유저 프로필을 빠르게 넘겨보는 상황을 예로 들어보겠습니다. useSuspenseQuery는 데이터가 완전히 준비되기 전까지 해당 컴포넌트의 렌더링을 일시 정지시킵니다. 즉, 데이터가 오고 가는 그 짧은 찰나의 순간에 이전 정보와 새로운 정보가 화면상에서 충돌할 수 있는 여지 자체를 원천적으로 차단해 버리는 것입니다. 대기 시간 동안의 렌더링 제어권은 부모 컴포넌트의 스켈레톤 화면 같은 아주 안전한 대체 UI로 넘어가기 때문에, 사용자가 마우스를 빠르게 연타하며 조작하더라도 UI 레벨에서 발생할 수 있는 경합 조건까지 완벽하게 막아낼 수 있음을 경험했습니다. 실제 렌더링 트리에서는 아래와 같이 구성되겠습니다.
// Suspense를 통한 UI 레벨의 경합 조건 완벽 차단 예시
<Suspense fallback={<ProductSkeleton />}>
{/* 데이터가 준비될 때까지 렌더링이 일시 정지되며 이전 데이터와의 충돌을 원천 방지함 */}
<ProductList categoryId={categoryId} />
</Suspense>
요약하자면, 훈씨님께서 날카롭게 짚어주신 레이스 컨디션 문제는, 우리가 useEffect 기반의 수동적인 페칭 방식을 단호하게 지양하고 탠스택 쿼리와 서스펜스를 결합한 선언적 아키텍처를 실무에 적극적으로 도입해야 하는 가장 강력하고 핵심적인 이유 중 하나라고 생각합니다. 비동기 흐름 전반에 대해 깊이 있는 고민을 나누어 주셔서 저 또한 실무 경험을 다시금 되짚어보는 뜻깊은 시간이 되었습니다.
언제든 질문이 있으시면 편하게 말씀 부탁드리겠습니다! 훈씨님처럼 본질을 꿰뚫는 좋은 질문은 제가 더 나은 강의를 만드는 데 정말 큰 귀감이 됩니다. 늘 훈씨님의 성장을 진심으로 응원하며, 혹시 추가로 필요한 강의가 있으시면 jeony0535@naver.com으로 이메일 부탁 드리겠습니다! 감사의 마음을 담아 할인 쿠폰을 함께 전달해 드리도록 하겠습니다.
감사합니다 😃😃




