[TIL] 5. React: useEffect, react-router-dom

MinseoKim·2026년 8월 5일

1. 오늘의 한 줄 요약

오늘 만난 버그 대부분은 "에러가 안 나서 더 위험한" 종류였다 — useEffect 의존성 배열, query string 문법, 조건부 렌더링의 숫자 0까지 에러가 뜨지 않지만 기대한 결과값이 나오지 않아서 원인을 찾기 어려웠다.

2. 배운 내용

(1) useEffect 의존성 배열은 반드시 "배열"이어야 한다

Capacity를 10으로 설정하고, 10 이상일 경우 입장 btn이 비활성화되고, 0 이하에서는 퇴장 버튼이 비활성화되게 만들었어야 했다.

그런데 에러 메시지 없이, 10에서 입장버튼 비활성화와 0에서 퇴장 버튼 비활성화가 되지 않아 capacity에 걸어뒀던 조건문이 제대로 안 먹히는 문제가 있었다.

여기서 문제는 내가 effect의 상태 갱신을 위해서는 반드시 배열로 감싸야 한다는 개념을 이해하지 못했기 때문이었다.

// (처음 작성한 코드) 잘못된 예시 - {cnt}는 객체라서 length가 undefined
useEffect(() => {
    console.log(`side effect render cnt : ${cnt}`);
}, {cnt});

// 올바른 예시
useEffect(() => {
    console.log(`side effect render cnt : ${cnt}`);
}, [cnt]);
  • React는 내부적으로 이전/이후 deps를 배열로 취급하고 deps.length, deps[i]로 순회하며 비교한다. {cnt}{ cnt: cnt } 객체라서 lengthundefined이다.

  • 따라서 비교 루프 조건이 처음부터 거짓으로 세팅되어 한 번도 비교하지 않고 "변경 없음"으로 판정한다.

  • 결과적으로 effect가 mount 시 딱 한 번만 실행되고 cnt가 바뀌어도 재실행되지 않는다.

이때 에러 메시지는 뜨지 않으니 다음부터 주의해야 한다.

핵심: 의존성 자리는 무조건 [ ] 형태여야 함!

(2) useEffect 없이 API를 부르면 무한루프가 되는 이유

// loadData();  // useEffect 없이 매 렌더마다 호출되면 무한 루프 발생
useEffect(() => {
    loadData();
}, []);
  • 컴포넌트 함수의 바디는 렌더링될 때마다 처음부터 다시 실행된다.
  • loadData()를 바디에 직접 써두면 렌더될 때마다 즉시 실행되어 API 요청이 매 렌더마다 나가고, .then(response => setComments(...))가 state를 바꾸면 리렌더링이 스케줄된다. 리렌더링이 일어나면 다시 컴포넌트 함수가 실행되어 loadData()가 또 호출된다. (이렇게 무한 루프에 갇히는 것이다.)

"렌더 → API 호출 → setState → 렌더 → ..."의 무한 사이클

  • while(true) 같은 동기 무한루프는 아니지만, 비동기 요청이 계속 쌓이며 반복되는 거라 체감상 똑같이 무한루프다.

  • useEffect(fn, [])가 하는 일은 결국 컴포넌트 바디(렌더링 중)와 effect(렌더링 후, 별도 생명주기)를 분리해주는 것이다!

(3) 페이지 이동할 때 데이터를 넘기는 세 가지 통로 (react-router-dom)

방식보내는 쪽받는 쪽언제
statemoveUrl('/success', {state:{user}})useLocation()location.stateURL에 숨기고 싶은 값 (로그인 유저 정보)
query stringmoveUrl('/error?category=react&sort=latest')useSearchParams().get('키')검색조건, 필터처럼 선택적인 값
path variable/users/3useParams(){id}리소스 자체를 식별하는 값 (게시글/유저 id)

보내는 방식이 다르면 받는 훅도 반드시 짝을 맞춰서 달라진다는 게 핵심. useParams()로 받은 값은 항상 문자열이라는 점도 기억해둘 것 — API에 그대로 숫자처럼 쓰려다 낚일 수 있다.

(4) localStorage vs state — "새로고침하면 사라지는가"가 기준

localStorage.setItem('userName', user.name);   // 영구 저장
const user = location.state.user;               // 이 페이지 이동에서만 유효

user.name이 왜 사라지는지 처음엔 sessionStorage랑 헷갈렸는데, 실제로는 useNavigatestate로 실어 보낸 값을 useLocation()으로 받은 것이었다.

SuccessPage를 새로고침하거나 주소창에 직접 쳐서 들어오면 location.state가 없어서 에러가 난다. 반면 localStorage는 브라우저를 꺼도 유지된다.

  • useLocation().state: 그 페이지 이동 한 번뿐, 새로고침하면 사라짐
  • sessionStorage: 탭을 닫기 전까지
  • localStorage: 브라우저를 꺼도 계속, 직접 지워야 사라짐

(5) 조건부 렌더링 — &&와 숫자 0의 함정

{isLoggedIn && <p>환영합니다</p>}

JS의 &&는 단축 평가라서, 왼쪽이 거짓이면 오른쪽을 볼 필요 없이 왼쪽 값을 바로 리턴한다. React는 false/null/undefined를 화면에 아무것도 안 그리기 때문에, 조건이 거짓일 때 자연스럽게 아무것도 안 보이는 방식으로 조건부 렌더링이 완성된다.

{count && <p>개수: {count}</p>}

근데 count0이면 &&0을 그대로 리턴하는데, 0false와 달리 화면에 실제로 찍힌다. 숫자가 0일 수 있는 조건에서는 count > 0 && ...처럼 명시적으로 boolean으로 바꿔야 안전하다는 걸 기억해두자.

3. 실습 결과 (트러블 슈팅 중심)

(1) EventPage.jsx에서 이메일/비밀번호 로그인 폼 만들고 json-server와 연동

(2) 로그인 성공/실패 여부에 따라 SuccessPage / ErrorPage로 분기하는 useNavigate 로직 완성

(3) SuccessPage에서 useLocation()으로 state 값(user) 받아오고, localStorage에 저장한 userName과 비교 확인

(4) ErrorPage에서 useSearchParams()로 query string 값(category, sort) 꺼내서 화면에 표시

현재 로그인 시도 → 필터링 확인 → 페이지 분기 → 데이터 전달까지의 전체 흐름을 한 사이클로 완성한 상태

4. 문제와 해결

막힌 부분

로그인 버튼을 눌러도 계속 빈 배열이 반환됨. 코드를 몇 번을 다시 봐도 문법상 문제가 없어 보여서 한참 헤맸다.

★ 해결 방법

  1. console.log(url)로 실제 요청 URL을 찍어서 확인 — ? 뒤에 공백이 껴서 파라미터 이름이 깨져있는 걸 발견
  2. 공백 제거하고 &로 정확히 연결한 문법으로 수정
  3. 그래도 빈 배열이 나와서 다시 확인해보니, 테스트할 때 폼 입력창을 채우지 않고 버튼만 눌렀던 것이 원인이었음 (함수 목적 이해x)
  4. Email/Password 칸을 실제로 채우고 나서야 정상적으로 필터링된 배열이 반환되는 것을 확인

제대로 잡고 가야 하는 개념들

★ query string 올바른 공식

// 처음 썼던 코드 (공백 때문에 문제)
await api.get(`/users? email=${email} &pwd=${pwd}`)

// 올바른 형태
await api.get(`/users?email=${email}&pwd=${pwd}`)
  • 공백이 하나라도 끼면 ? 뒤의 파라미터 이름 자체가 깨진다. " email"(공백 포함)이라는 이상한 key로 인식되기 때문에 json-server는 이 key를 필터로 인식 못 하고 필터링 자체를 포기한 뒤, 전체 데이터를 그대로 돌려준다.
  • 계속 array[1]이 올바른 id, pwd 값을 입력하지 않아도 제대로 반환되는 문제가 있었다.
  • 어차피 결과가 1명이라서 로그인이 제대로 동작하는 것처럼 보였다. 유저가 여러 명이었다면 이메일/비밀번호가 틀려도 전체 유저 목록이 반환됐을 거다. 사실상 인증이 뚫린 상태인 거다.
  • 또 헷갈렸던 이유는 쿼리 스트링 공식을 제대로 작성하면, 내가 원하는 값인 array[1]이 콘솔에 찍히지 않았다.
  • 이건 아래 내가 로그인 핸들러를 잘못 이해하고 있었기 때문이었다.

★ signInHandler를 오해하고 있었던 부분

오해실제
SignIn 버튼 누르면 /users의 전체 데이터가 불러와짐내 입력값과 일치하는 데이터가 있을 때만 → 그 user가 배열로 불러와짐
query string 문법만 고치면 바로 배열이 반환될 것폼에 값을 안 채운 채로 테스트하면 email=&pwd=(빈 문자열)로 요청 나가서 여전히 빈 배열
res 자체가 배열이다res는 axios 응답 객체 전체(data, status, headers...), 실제 배열은 res.data
  • 코드가 이상한가부터 의심하기 전에 실제로 나가는 값/URL을 콘솔로 찍어서 확인하는 습관을 기르자!

5. 다음에 할 일

  • 8월 5일 배운 내용 전부 FE 면접 관점에서 정리해보기
  • 오늘 배운 내용 전부 회고용으로 기록 남기기
  • 나의 약점은 모르는 게 아니라 설명할 수 없음이다. "써봤다, 보면 안다" 말고 내 언어로 설명할 수 있는 개발자가 되기 위해서는 지금까지의 내용을 이번 주에는 제대로 기본 개념부터 회고해봐야겠다!

0개의 댓글