오늘 만난 버그 대부분은 "에러가 안 나서 더 위험한" 종류였다 — useEffect 의존성 배열, query string 문법, 조건부 렌더링의 숫자 0까지 에러가 뜨지 않지만 기대한 결과값이 나오지 않아서 원인을 찾기 어려웠다.
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 } 객체라서 length가 undefined이다.
따라서 비교 루프 조건이 처음부터 거짓으로 세팅되어 한 번도 비교하지 않고 "변경 없음"으로 판정한다.
결과적으로 effect가 mount 시 딱 한 번만 실행되고 cnt가 바뀌어도 재실행되지 않는다.
이때 에러 메시지는 뜨지 않으니 다음부터 주의해야 한다.
핵심: 의존성 자리는 무조건 [ ] 형태여야 함!

// loadData(); // useEffect 없이 매 렌더마다 호출되면 무한 루프 발생
useEffect(() => {
loadData();
}, []);
loadData()를 바디에 직접 써두면 렌더될 때마다 즉시 실행되어 API 요청이 매 렌더마다 나가고, .then(response => setComments(...))가 state를 바꾸면 리렌더링이 스케줄된다. 리렌더링이 일어나면 다시 컴포넌트 함수가 실행되어 loadData()가 또 호출된다. (이렇게 무한 루프에 갇히는 것이다.)"렌더 → API 호출 → setState → 렌더 → ..."의 무한 사이클
while(true) 같은 동기 무한루프는 아니지만, 비동기 요청이 계속 쌓이며 반복되는 거라 체감상 똑같이 무한루프다.
useEffect(fn, [])가 하는 일은 결국 컴포넌트 바디(렌더링 중)와 effect(렌더링 후, 별도 생명주기)를 분리해주는 것이다!
| 방식 | 보내는 쪽 | 받는 쪽 | 언제 |
|---|---|---|---|
| state | moveUrl('/success', {state:{user}}) | useLocation() → location.state | URL에 숨기고 싶은 값 (로그인 유저 정보) |
| query string | moveUrl('/error?category=react&sort=latest') | useSearchParams() → .get('키') | 검색조건, 필터처럼 선택적인 값 |
| path variable | /users/3 | useParams() → {id} | 리소스 자체를 식별하는 값 (게시글/유저 id) |
보내는 방식이 다르면 받는 훅도 반드시 짝을 맞춰서 달라진다는 게 핵심. useParams()로 받은 값은 항상 문자열이라는 점도 기억해둘 것 — API에 그대로 숫자처럼 쓰려다 낚일 수 있다.
localStorage.setItem('userName', user.name); // 영구 저장
const user = location.state.user; // 이 페이지 이동에서만 유효
user.name이 왜 사라지는지 처음엔 sessionStorage랑 헷갈렸는데, 실제로는 useNavigate의 state로 실어 보낸 값을 useLocation()으로 받은 것이었다.
SuccessPage를 새로고침하거나 주소창에 직접 쳐서 들어오면 location.state가 없어서 에러가 난다. 반면 localStorage는 브라우저를 꺼도 유지된다.
useLocation().state: 그 페이지 이동 한 번뿐, 새로고침하면 사라짐sessionStorage: 탭을 닫기 전까지localStorage: 브라우저를 꺼도 계속, 직접 지워야 사라짐&&와 숫자 0의 함정{isLoggedIn && <p>환영합니다</p>}
JS의 &&는 단축 평가라서, 왼쪽이 거짓이면 오른쪽을 볼 필요 없이 왼쪽 값을 바로 리턴한다. React는 false/null/undefined를 화면에 아무것도 안 그리기 때문에, 조건이 거짓일 때 자연스럽게 아무것도 안 보이는 방식으로 조건부 렌더링이 완성된다.
{count && <p>개수: {count}</p>}
근데 count가 0이면 &&는 0을 그대로 리턴하는데, 0은 false와 달리 화면에 실제로 찍힌다. 숫자가 0일 수 있는 조건에서는 count > 0 && ...처럼 명시적으로 boolean으로 바꿔야 안전하다는 걸 기억해두자.
(1) EventPage.jsx에서 이메일/비밀번호 로그인 폼 만들고 json-server와 연동
(2) 로그인 성공/실패 여부에 따라 SuccessPage / ErrorPage로 분기하는 useNavigate 로직 완성
(3) SuccessPage에서 useLocation()으로 state 값(user) 받아오고, localStorage에 저장한 userName과 비교 확인
(4) ErrorPage에서 useSearchParams()로 query string 값(category, sort) 꺼내서 화면에 표시
현재 로그인 시도 → 필터링 확인 → 페이지 분기 → 데이터 전달까지의 전체 흐름을 한 사이클로 완성한 상태
막힌 부분
로그인 버튼을 눌러도 계속 빈 배열이 반환됨. 코드를 몇 번을 다시 봐도 문법상 문제가 없어 보여서 한참 헤맸다.
★ 해결 방법
console.log(url)로 실제 요청 URL을 찍어서 확인 — ? 뒤에 공백이 껴서 파라미터 이름이 깨져있는 걸 발견&로 정확히 연결한 문법으로 수정

★ 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 값을 입력하지 않아도 제대로 반환되는 문제가 있었다.array[1]이 콘솔에 찍히지 않았다.★ signInHandler를 오해하고 있었던 부분
| 오해 | 실제 |
|---|---|
SignIn 버튼 누르면 /users의 전체 데이터가 불러와짐 | 내 입력값과 일치하는 데이터가 있을 때만 → 그 user가 배열로 불러와짐 |
| query string 문법만 고치면 바로 배열이 반환될 것 | 폼에 값을 안 채운 채로 테스트하면 email=&pwd=(빈 문자열)로 요청 나가서 여전히 빈 배열 |
res 자체가 배열이다 | res는 axios 응답 객체 전체(data, status, headers...), 실제 배열은 res.data |