Koty's Blog
개발 경험과 지식을 공유하는 공간입니다.
개발 경험과 지식을 공유하는 공간입니다.
4,339
5
0

사용자 취향과 실시간 맥락을 반영한 AI 여행 플래너 에이전트인 TriPick을 개발하는 중, 여행 일정에 관광지 혼잡도를 반영하려다, 자동 반영 대신 "이 관광지에 사람이 많이 몰릴 수 있어요" 알림으로 방향을 틀었다. 그 결정과, 한국관광공사 관광지 집중률 API를 붙이며 지역코드 때문에 겪은 삽질을 정리한다. 배경 처음 그림은 단순했다. 붐비는 관광지를 피해 일정을 짜준다. 혼잡할 것 같은 곳은 점수를 깎아 뒤로 미는 식이다. 그런데 막상 넣어보면 부작용이 있다. 여행 일정 추천은 "사용자가 좋아할 곳"을 최우선으로 골라야 한다. 여기에 혼잡도를 점수로 섞으면, 정작 취향에 맞는 장소가 "붐빈다"는 이유로 밀려난다. 붐비는 데는 대개 그럴 만해서 붐빈다. 인기 있는 곳을 혼잡하다고 빼버리면 추천의 질이 오히려 떨어진다. 그래서 혼잡도를 일정 자동 생성에서 빼고 알림으로 돌렸다. 날씨 예보 알림과 같은 방식이다. 비가 올 것 같으면 "일정 바꿀까요?" 하고 물어보듯, 혼잡이 예상되면 알려만 주고 실제로 바꿀지는 사용자가 정하게 둔다. 자동으로 일정을 갈아엎지 않는다. 혼잡도의 역할이 "일정을 자동으로 최적화하는 입력"에서 "사용자에게 판단을 넘기는 알림 신호"로 바뀐 셈이다. 어떤 데이터를 쓰나 — 관광지 집중률 API 한국관광공사가 공공데이터포털에 공개한 관광지 집중률(방문자 추이 예측) API를 쓴다. 오퍼레이션은 tatsCnctrRatedList. 시도·시군구를 주면 그 지역 관광지별로 향후 약 30일간 일자별 집중률을 돌려준다. 응답은 이렇게 생겼다. { "baseYmd": "20260720", "areaCd": "51", "areaNm": "강원특별자치도", "signguCd": "51130", "signguNm": "원주시", "tAtsNm": "간현관광지", "cnctrRate": "20.46" } baseYmd가 날짜, cnctrRate가 그날의 집중률(%)이다. 관광지 하나당 이런 행이 날짜별로 쌓인다. 요청에는 areaCd(시도)·signguCd(시군구)가 필수고 tAtsNm(관광지명)으로 특정 장소만 걸러 조회할 수 있다. 붙이기 전에 두 가지를 확인했다. 첫째, 이 데이터는 관광지만 있다. 음식점·카페는 없다. 그래서 알림은 관광지 일정에만 건다. 둘째, 없는 이름을 tAtsNm에 넣으면 totalCount: 0으로 조용히 빈 응답이 온다. 데이터가 없는 장소는 자연스럽게 걸러진다. 지역코드에서 두 번 걸렸다 이 API의 진짜 난관은 지역코드였다. 한국관광공사 API는 지역코드 체계가 여러 개고 오퍼레이션마다 다른 걸 쓴다. 예전 지역코드가 아니라 법정동 코드였다 처음엔 이 API가 예전 관광정보 API에 쓰던 한국관광공사 고유 지역코드(areaCode2)를 쓸 거라고 생각했다. 그런데 스펙의 샘플값이 이 전제를 깼다. areaCd=51이 강원인데, 고유 지역코드에서 강원은 32다. 51은 아예 코드 범위(1-8, 31-39) 밖이다. 51은 법정동 코드에서 강원의 시도 코드다. 즉 이 API는 고유 지역코드가 아니라 법정동 코드 계열을 쓴다. 한국관광공사는 법정동 코드를 주는 별도 오퍼레이션(ldongCode2)을 제공하므로, 여기서 코드를 조달하기로 했다. 일정에는 장소의 좌표와 주소만 있고 지역코드는 없다. 그래서 주소에서 시도·시군구 이름을 뽑아 ldongCode2로 만든 이름→코드 색인으로 변환한다. // "강원특별자치도 원주시 지정면 ..." → { areaCd: '51', signguCd: '51130' } const sidoName = address.trim().split(/\s+/)[0]; // 강원특별자치도 const sigunguName = parseSigungu(address); // 원주시 const areaCd = sidoIndex.get(regionStem(sidoName)); // 51 시군구 코드가 3자리로 온다 여기서 한 번 더 걸렸다. ldongCode2로 강원(51)의 시군구 목록을 받으면, 원주시 코드가 51130이 아니라 130으로 온다. 뒤 3자리만 준다. ldongCode2 (lDongRegnCd=51) → 110 춘천시 / 130 원주시 / 150 강릉시 집중률 API 가 요구하는 signguCd → 51130 (= 51 + 130) 집중률 API의 signguCd는 법정동 5자리 전체다. 그래서 시도 코드를 앞에 붙여야 한다. const signguCd = ${areaCd}${String(code).padStart(3, '0')}; // '51' + '130' → '51130' 이건 스펙 문서만 봐선 놓치기 쉬웠다. 실제로 API를 한 번 찔러보고 나서야 잡혔다. 강원특별자치도 원주시를 넣어 areaCd 51 / signguCd 51130이 나오고 간현관광지로 26일치 집중률이 돌아오는 걸 확인하고 코드를 확정했다. 실제 API 호출로 areaCd 51 / signguCd 51130, 간현관광지 집중률이 돌아온 터미널 결과 알림은 어떻게 판정하나 전체 흐름은 단순하다. 하루 한 번 예정된 여행을 훑어 관광지 일정의 예정일 집중률을 조회하고 붐빌 것 같으면 알림을 보낸다. 알림을 받은 사용자가 일정을 바꿀지 정한다. 판정 기준은 절대값이 아니라 상대값으로 잡았다. 집중률의 절대 스케일이 관광지마다 다르기 때문이다. 어떤 곳은 평소 5%, 어떤 곳은 평소 30%다. 절대값 하나로 자르면 늘 붐비는 곳만 계속 걸린다. 그래서 "그 관광지 자기 평균 대비 붐비는 날"을 본다. 예정일 집중률이 그 관광지 예측기간 평균의 1.2배 이상이고, 절대값도 하한(10%)을 넘을 때 혼잡으로 본다. 두 값은 실데이터를 보며 맞춰야 할 초기 임계값이라 상수로 빼뒀다. 여기서 조심할 게 하나 있었다. 지역코드 이름→코드 색인은 자주 변하지 않아 메모리에 한 번만 만들어 캐싱한다. 그런데 처음 구현은 실패한 조회 결과까지 캐싱했다. 첫 조회가 일시적 오류나 호출 한도를 만나 실패하면, 그 실패가 캐시에 박혀 프로세스를 재시작하기 전까지 지역 변환이 영영 안 된다. 캐시는 성공한 결과만 담고 실패하면 비워 다음번에 다시 시도하도록 고쳤다. 실패를 캐싱하지 않는다는 기본을 놓쳤던 셈이다. 마치며 관광지 혼잡도는 일정을 자동으로 최적화하는 입력이 아니라, 사용자에게 판단을 넘기는 알림으로 붙였다. 인기 있는 장소가 혼잡하다는 이유로 추천에서 밀려나는 걸 막기 위한 결정이다. 기술적으로 가장 시간을 쓴 건 지역코드였다. 한국관광공사 집중률 API는 예전 고유 코드가 아니라 법정동 코드를 쓰고 시군구 코드는 뒤 3자리만 와서 시도 코드를 붙여 5자리로 만들어야 했다. 공개 API를 붙일 때 스펙 문서만 믿지 말고 실제 응답을 한 번 받아보는 게 결국 제일 빨랐다.

2026년 7월 3일(금) ~ 7월 4일(토)간 카카오 AI 캠퍼스에서 커넥트데이를 진행했다. 위치는 경기 용인시였고, 각 학교별로 단체버스를 이용하여 이동했다. 아무래도 경기도랑 먼 부산에서 출발해서 약 5시간 정도에 걸쳐서 도착할 수 있었다. 서울 다닐때는 기차 많이 이용했었는데, 오랜만에 버스타고 이동하니 확실히 멀어서 힘들었다. 현재 카테캠 4기 인원은 한 학교당 30명에, 부산대, 강원대, 전남대, 경북대, 충남대 5개 학교가 있어 약 150명의 인원이 한 장소에 모이는 자리였다. 캠퍼스의 그로잉홀에서 거의 모든 일정을 진행했는데, 규모가 엄청 커서 많은 인원을 수용하는데에도 넉넉했다. 각각 하루 간 다른 팀으로 배정 받고, 대학교 별로 6~8명씩 팀이 만들어져있었다. 일정은 아래 표와 같았다. 도착하자마자 점심을 먹었는데, 점심으로 나온 돼지국밥이 너무 맛있었다. 헬로키친도 새 건물이라 그런지 엄청 깨끗하고(무슨 미슐랭 주방 공간 같았다) 공간도 넓어서 좋았다. 본격적인 일정을 진행하면서 아이스브레이킹으로 1일차 팀원들과 서로의 학과, 학년, 취미활동들을 공유하면서 어색한 분위기를 풀고, 이어지는 첫번째 현업 특강에서는 카카오엔터테인먼트 CTO 마커스님이 AX를 주제로 카카오엔터에서 어떤식으로 AI를 활용하고 고객들에게 제공하는지에 대한 인사이트를 얻을 수 있었다. 빙고 이벤트가 있었는데, 자신과 MBTI가 같은 사람, 특이한 알바 경험이 있는 사람 등 다른 학교의 사람들과 서로 소통하면서 그 사람의 이름으로 빙고를 채우는 형식이었다. 일정 중간중간 쉬는 시간마다 다른 테이블을 열심히 돌아다니며 빙고를 채워나갔었다. (빙고 사진을 깜빡했다) 내 MBTI가 ESFJ였는데, ESFJ인 사람이 150명중에 너무 없어서 아일라 매니저님으로 겨우 채울 수 있었다. MBTI만 빨리 채웠으면 선착순 보상을 받을 수 있었는데 아쉬웠다. 다음으로 팀 챌린지로 외부 레크레이션 강사님이 오셔서 전략 협동 게임을 진행했는데, 여러 구역별로 인원을 적절하게 배치해서 식량/백신을 모아 최종적으로 높은 점수를 이기면 되는 게임이었다. 2번째 턴인 여름에서 좀비가 될 수 있는 이벤트가 있었는데, 좀비는 일반 인간의 파워 1의 두배인 파워 2를 가질 수 있었다. 우리팀은 도박수로 8명 중 6명을 좀비로 변한 뒤에 높아진 파워를 바탕으로 3번째 턴인 가을에서 다른 두 팀을 침략하는데 성공했고, 마지막 4번째 턴에서는 모두 수비를 통해 약탈한 식량/백신을 지킬 수 있었다. 또한 특수 카드인 좀비 치료제를 5개나 모아둬서 점수 정산 시간에 좀비 감점 10점만 받은 채로 높은 점수를 달성하여 1등을 차지할 수 있었다. 2번째 현업특강에서는 취업과 관련된 좋은 정보를 얻을 수 있었는데, 자기소개서에 일반적으로 스펙/경험을 나열하지 말고 개발자로서의 고민이 보일 수 있도록 ‘왜 그 기술을 썼는가’의 사고방식을 증명하는 식의 내용을 써라는 부분이 인상이 깊었다. 이번 하반기부터 여러 회사에 지원해야 하는데, 이 특강으로 많은 도움을 받았다고 느꼈다. 1일차의 마지막은 랜덤으로 추첨해서 선물을 주는 이벤트를 진행했는데, 아쉽게 걸리지 못했다. (2일차 마지막에도 랜덤 뽑기 이벤트를 한번 더 진행했다,) 숙소는 더블트리 바이 힐튼 서울 판교 였는데, 5성급의 약 25만원 상당 킹 사이즈 침대의 객실을 1인 1실로 제공해줘서 정말 쾌적하게 하루를 보낼 수 있었다. (사실 150명 중 100명만 호텔에서 묵고, 나머지는 AI 캠퍼스 내 별도 객실에서 묵었다. 나는 호텔에 걸려서 좋았다.) 2일차에는 다시 AI 캠퍼스로 돌아와 조식 뷔페를 먹었고, 새로운 2번째 날 팀원들과 다시 인사하면서 여러 챌린지들을 진행했다. 첫번째 챌린지로는 한명이 초상화 대상이 되고 나머지가 짧은 시간 안에 돌아가면서 그려야 했다. 내가 초상화 대상이 되서 한분씩 돌아가며 나를 그려줬는데, 생각보다 다들 잘 그리셔서 마음에 들었다. 다음 챌린지로는 카테캠 수료 후 성장한 미래의 모습을 상상하며, 목표와 비전을 30~60초 릴스를 찍는 챌린지였다. 우리 팀은 우리가 나중에 카테캠 매니저가 되는 모습으로 찍었다. 다른 팀들은 인스타 릴스 챌린지에서 많이 추는 댄스들을 써서 재밌는 영상들이 많이 나왔다. 챌린지 이후에는 카테캠 프리코스 우수자 시상과 함께 랜덤 뽑기 이벤트를 진행했고, 마무리로 점심을 먹으며 모든 일정이 마무리되었다. 특강 내용부터 팀 챌린지까지 정말 알차게 보냈던 커넥트데이였다. 이제 남은 카테캠 일정도 힘내서 수료까지 열심히 달려야겠다. #카카오테크캠퍼스 #카카오테크캠퍼스커넥트데이 #카테캠4기 #카카오AI캠퍼스

문제 상황 Next.js 개발 서버를 Docker로 띄우고, 소스 코드를 수정해도 HMR(Hot Module Replacement)가 동작하지 않았다. 이미지 출처: https://vercel.com/blog/turbopack 환경 OS: Windows 11 Next.js 16 (App Router) Webpack 방식으로 전환 관련 Github Discussion에서 참고한 결과, Turbopack을 포기하고 Webpack 방식으로 전환을 한 뒤, Polling 설정을 추가하는 방식이 있었다. next.config.ts에서 아래와 같은 옵션을 추가한다. const nextConfig = { webpack: (config) => { config.watchOptions = { poll: 1000, aggregateTimeout: 300, }; return config; }, }; 그리고 docker-compose.yaml 에 Polling 관련 옵션을 추가한다. environment: WATCHPACK_POLLING=true CHOKIDAR_USEPOLLING=true CHOKIDAR_INTERVAL=500 Webpack으로 전환 후 HMR이 동작은 했지만, 리빌딩하는 시간이 너무 길었고 간헐적으로 저장 이후에 수정 사항이 없는데도 반복적으로 리빌딩되는 현상이 생겨 메모리를 너무 많이 사용하는 문제가 발생되었다. 원인 분석 Turbopack은 파일 변경을 감지할 때 inotify 메커니즘을 사용한다. 파일이 바뀌면 OS가 이벤트를 날려주고, Turbopack이 그걸 받아서 HMR을 트리거한다. 문제는 Windows Docker가 WSL2 위에서 동작하는데, 프로젝트 파일이 C:\Users\... 같은 Windows 파일 시스템에 있으면 inotify 이벤트가 컨테이너까지 제대로 전달되지 않는다는 거다. Windows 파일시스템 (C:\) → WSL2 → Docker 컨테이너 ↑ inotify 이벤트가 여기서 소실됨 Docker 공식 문서에서도 이 문제를 언급하고 있다. "Linux containers only receive file change events, “inotify events”, if the original files are stored in the Linux filesystem. For example, some web development workflows rely on inotify events for automatic reloading when files have changed." Docker 공식 문서 - WSL "if you mount files that live in the Windows file system (such as with docker run -v /mnt/c/Users/Simon/windows-project:/sources ), you won’t get those performance benefits, as /mnt/c is actually a mountpoint exposing Windows files through a Plan9 file share. " Docker 공식 블로그 - WSL Best Practice 해결 방법 1. WSL2에 Ubuntu 설치 이미 설치된 Ubuntu가 있다면 이 단계는 건너뛰면 된다. WSL2에 Ubuntu가 없다면 먼저 설치해야 한다. PowerShell을 관리자 권한으로 실행한 뒤 아래 명령어를 입력한다. wsl --install -d Ubuntu-24.04 설치가 완료되면 자동으로 Ubuntu 터미널이 열리면서 계정 설정 화면이 나온다. username과 password를 설정하면 된다. 이후 접속은 윈도우 검색창에서 Ubuntu-24.04 를 검색하거나, PowerShell에서 아래 명령어를 입력하면 된다. wsl -d Ubuntu-24.04 2. 프로젝트 이동 및 설정 로컬 프로젝트를 복사를 통해 이동시키거나, git에 올린 경우 git clone을 통해 가져온다. 초기에 node.js의 경우 윈도우에 있는 경로로 설정되어 있을 가능성이 높다. which npm 명령어를 실행했을 때, /mnt/c/... 경로가 출려된다면 Windows npm을 바라보는 것이므로 별도로 nvm 등을 통해서 node.js를 설치해야 한다. 3. Docker Desktop WSL Integration 설정 Docker Desktop → Settings → Resources → WSL Integration → Ubuntu-24.04 토글 활성화 이렇게 하면 Ubuntu에서도 docker compose up 해도 Docker Desktop에서 컨테이너가 생성이 된다. 4. VSCode 연결 Ctrl + Shift + P → WSL: Connect to WSL using Distro → Ubuntu-24.04 선택 접속 이후 Open Folder를 통해 2단계에서 복사한 프로젝트 폴더로 이동한 뒤, 파일을 확인하고 수정할 수 있다. 요약 Windows + Docker 환경에서는 Turbopack의 HMR이 정상적으로 동작하지 않으므로, WSL2 파일시스템으로 옮겨서 실행해야 한다. 참고한 링크 Next.js 공식 문서 - Local Development Docker 공식 문서 - WSL Docker 공식 블로그 - WSL Best Practice GitHub Issue - Hot reload doesn't work inside Docker container GitHub Issue - Docker Compose Watch does not trigger hot reload with Turbopack GitHub Discussion - Hot Reload Not Working with WSL

문제 상황 Object Storage인 Cloudflare R2에 이미지를 올리고, Docker(Nginx + Next.js)로 배포한 앱에서 next/image로 이미지를 불러오려 하니 아래와 같은 에러가 발생했다. 환경 Next.js (App Router) Docker: Nginx + Next.js 멀티스테이지 빌드 Cloudflare R2 + 커스텀 도메인 삽질한 과정 1. remotePatterns 확인 // next.config.ts const nextConfig: NextConfig = { images: { remotePatterns: { protocol: "https", hostname: "images.kotys.dev" }, ], }, ..., }; next.config.ts 설정 자체는 문제없었다. 2. Docker 재빌드 next.config.ts 변경사항은 빌드 타임에 번들링되기 때문에 컨테이너 재시작만으로는 반영이 안 된다. docker compose build --no-cache docker compose up -d --no-cache 옵션으로 재빌드했지만 여전히 같은 에러가 발생했다. 3. 빌드 결과물에 config 반영 여부 확인 컨테이너 안에서 실제 빌드된 config를 확인해봤다. docker exec -it sh cat .next/required-server-files.json | grep -A 20 "remotePatterns" "remotePatterns": [ { "protocol": "https", "hostname": "images.kotys.dev" }, ..., ] 빌드에는 제대로 반영돼 있었다. 4. 네트워크 문제 확인 Next.js는 이미지 최적화 시 서버 사이드에서 직접 이미지를 fetch한다. 컨테이너 안에서 네트워크 접근이 되는지 확인했다. wget -O- https://images.kotys.dev/test.png 네트워크에도 문제가 없었다. 5. Cloudflare Cache Purge 혹시 Cloudflare가 이전의 잘못된 응답을 캐싱하고 있을 수도 있어서 Cache Purge를 수행했지만, 동일한 에러가 계속 발생했다. 원인 및 해결 Dockerfile의 멀티스테이지 빌드에서 runner 스테이지에 next.config.ts를 복사하지 않은 것이 원인이었다. Runner stage ..., COPY --from=builder /usr/src/app/.next ./.next COPY --from=builder /usr/src/app/public ./public COPY --from=builder /usr/src/app/next.config.ts ./next.config.ts # 추가 ..., next.config.ts가 없으면 Next.js 서버가 기본값으로 동작해서 remotePatterns가 빈 상태로 실행된다. [Next.js 공식문서에 아래와 같은 설명이 있다. next.config.js is a regular Node.js module, not a JSON file. It gets used by the Next.js server and build phases, and it's not included in the browser build. .next/required-server-files.json에는 config가 반영되어 있었다 하더라도, 서버 환경에서 next.config.ts가 없으면 소용이 없다. 수정 후 재빌드하니 문제가 해결되었다. 마치며 Docker의 멀티스테이지 빌드를 쓸 때 runner 스테이지에 필요한 파일을 빠뜨리기 쉽다. 공식 문서를 잘 참고하여 런타임에 필요한 설정 파일들을 누락하지 않도록 조심해야겠다. 비슷한 증상으로 헤매는 사람에게 도움이 되었으면 좋겠다. 참고한 링크 Next.js 공식 문서 - next/image remotePatterns Next.js 공식 문서 - Un-configured Host 에러 Next.js 공식 문서 - next.config.js GyanBlog - Docker 환경에서 next/image url parameter not allowed 해결

들어가며 TanStack Query의 Devtools에는 Trigger Error 버튼으로 쿼리의 에러 상태를 테스트할 수 있다. 그런데 이미 데이터가 캐시에 존재하는 상태에서 Trigger Error를 눌러도 에러가 발생하지 않아, Error Boundary가 작동되지 않는 현상을 확인했다. 이 글에서는 왜 이런 현상이 발생하는지 원인을 분석하고, 현재는 어떻게 해결됐는지에 대해서 정리한다. 문제 상황 useSuspenseQuery는 useQuery와는 다르게 데이터가 보장된 상태에서 컴포넌트를 렌더링한다. 그리고 로딩 상태와 에러 상태를 각각 `와 `가 처리하는 구조이다. }> }> function MyComponent() { const { data } = useSuspenseQuery({ queryKey: "category"], queryFn: fetchCategory, }); return {data.name}; } 이 구조에서 에러 처리를 테스트하려고 Devtools의 Trigger Error 버튼을 클릭하였지만, ``의 fallback UI가 전혀 나타나지 않았다. 원인 분석 useSuspenseQuery의 에러 처리 방식 useSuspenseQuery는 에러가 발생했을 때 throw를 통해 가장 가까운 Error Boundary로 에러를 전파한다. 그런데 여기에 핵심적인 조건이 있다. [공식 문서에 따르면, useSuspenseQuery의 throwOnError는 기본값이 (error, query) => typeof query.state.data === 'undefined'로 고정되어 있다. throwOnError: (error, query) => typeof query.state.data === 'undefined' 캐시에 데이터가 있으면 에러를 throw하지 않고, 컴포넌트를 그대로 렌더링하는 것이 의도된 동작이다. Devtools Trigger Error가 하는 일 (수정 전) 기존 Devtools Trigger Error 버튼의 동작 코드는 다음과 같았다. activeQueryVal.setState({ status: 'error', error, fetchMeta: { ...activeQueryVal.state.fetchMeta, __previousQueryOptions, } as any, }) 쿼리가 한 번이라도 성공적으로 데이터를 받아온 상태라면, data에는 이전에 fetch한 값이 남아 있다. 버튼을 눌러도 data는 지워지지 않았기 때문에, useSuspenseQuery는 에러를 throw하지 않고, Error Boundary가 동작하지 않는 것이다. 공식 PR을 통한 해결 이 문제는 Discussion #10044에서 제기되었고, PR #10072를 통해 수정되어 @tanstack/[email protected]에 반영되었다. 수정된 코드는 간단하다: activeQueryVal.setState({ data: undefined, // 추가된 부분 status: 'error', error, fetchMeta: { ...activeQueryVal.state.fetchMeta, __previousQueryOptions, } as any, }) 이제 "Trigger Error"를 누르면 data가 undefined로 설정되기 때문에, useSuspenseQuery가 에러를 정상적으로 throw하고 Error Boundary가 동작한다. 핵심 정리 | 구분 | 수정 전 | 수정 후 | | -------------------------- | ---------------- | -------------------- | | Trigger Error 시 data 상태 | 기존 데이터 유지 | undefined로 초기화 | | Error Boundary 동작 | ❌ 동작 안 함 | ✅ 정상 동작 | | 적용 버전 | 5.92.x | 5.93.0 | 마치며 라이브러리 내부 동작을 이해하지 못하면 이런 함정에 빠지기 쉽다. useSuspenseQuery가 에러를 throw하려면 단순히 status: 'error'가 아니라 data: undefined도 함께 충족되어야 한다는 점을 기억하자. 현재는 @tanstack/[email protected] 이상으로 업그레이드하면 Trigger Error 버튼 하나로 Error Boundary 테스트가 가능하다. 참고한 링크 Discussion #5956 - Comment Discussion #10044 PR #10072 @tanstack/query-devtools 5.93.0 ChangeLog