티스토리 뷰
useEffect 안 먹어요! index는 바뀌는데 왜 useEffect는 꿈쩍도 안 해요?🤯
밍동망동 2025. 4. 7. 18:11회고 성격이 가득한 글로
즉각적인 문제 해결을 원하시는 분은
해결 방법으로 넘어가주세요.
우당탕탕 에러 고치기🔨
이번에 신규 프로젝트로 퀴즈 앱 비슷한 걸 만들고 있다.
리액트로 클론 코딩을 여러 번 해보긴 했지만,
완전히 내 힘으로 진행하는 창작 프로젝트는 처음이라
헤매는 부분도 많고 생각보다 손도 많이 간다.
기능은 주간 스프린트로 하나씩 구현하고 있는데,
지난 주는 정말... 미치도록 안 되던 문제가 하나 있었다.
useEffect가 안 먹힌다! index는 바뀌는데 왜 반응이 없는거야🤯
문제 번호엔 quizIndex가 바뀔 때마다
카드를 자동으로 다시 앞면으로 돌리는 기능을 구현하고 있었다.
그래서 아래처럼 useEffect를 사용했다.
useEffect(() => {
setFlipped(false); // 새 문제일 땐 앞면부터 보여주기
}, [quizIndex]);
근데...!
화면 상에서 아무 일도 일어나지 않았다.
복잡한 코드도 아니고 아주 간단한...
안 되는게 이상한 코드에서 에러가 나서 머리가 터졌다.
로그도 제대로 바뀌는데, 화면은 왜 그대로야?💦💦
quizIndex는 잘 바뀌고 있었고, 상태도 분명 바뀌고 있었다.
그런데 카드의 flip 상태가 전혀 반응 없었다.
처음에는 다른 컴포넌트에서 넘겨운 flipped prop이 문제인가 싶어서
코드 구조도 갈아엎어 보고, 의존성 배열도 바꿔 보고,
console.log도 열심히 찍어봤는데...
index는 잘 바뀌고 있는데 번번히 카드만 아무 반응 없다는 결론만 났다.
계속 삽질하다가 도저히 안 되겠어서 구글링을 시작했다.
사실 이번 프로젝트는 최대한 내 힘으로 해결해보려고 했는데...
key 누락이 문제의 핵심이란 걸 알아낼 수 있었다.
문제 발생 코드👩💻
useEffect(() => {
setFlipped(false); // 새 문제라면 앞면으로 자동 flip
}, [quizIndex]);
<KanjiCard kanji={currentQuiz} flipped={flipped} />
여기에서 문제가 된 건 key를 넘기지 않았다는 점이다.
React는 KanjiCard를 이전과 같은 컴포넌트라고 인식해서
리렌더링은 일어났지만 리마운트는 일어나지 않았다.
useEffect의 실행 순서와 타이밍에 관련된 문제로
결국 useEffect(() => ..., [])는 실행되지 않았던 것이다.
해결 방법🧰
<KanjiCard key={quizIndex} kanji={currentQuiz} flipped={flipped} />
key={quizIndex}를 넘기자마자,
갑자기 모든 게 정상 작동하기 시작했다...
🏄 딥다이브, 왜 이런 일이 벌어졌을까?
문제의 결정적인 핵심은 useEffect의 실행 타이밍과
리렌더링과 리마운트의 차이였다.
useEffect를 사용한다고해서 컴포넌트가 새로 그려지는 것이 아니었다.
상황 | 해결 방법 |
quizIndex가 바뀌었는데 KanjiCard 내부의 useEffect가 동작하지 않는다. | key={quizIndex}를 넣어주자 제대로 동작했다. |
🎨 리렌더링이란?
컴포넌트의 상태(state)나 props가 변경되었을 때 React가 해당 컴포넌트를 다시 그리는 과정이다.
기존 컴포넌트를 기반으로 업데이트하는 점에 유의하자.
상태 변화 useState 부모로부터 받은 props의 변화 context의 변화 forceUpdate()
이 중 하나라도 바뀌면 컴포넌트는 리렌더링된다.
🧱 리마운트란?
컴포넌트가 완전히 언마운트되었다가 다시 마운트되는 것을 의미한다.
더 쉽게 표현하자면, 이전 인스턴스를 버리고 새로운 인스턴스를 생성하는 것이다.
뭐랄까, 리렌더링이 그림을 다시 그리는 느낌이라면 리마운트는 그림판을 통째로 새로 여는 느낌에 가깝다.
리렌더링(Re-render) | 리마운트(Re-mount) | |
정의 | 기존 컴포넌트를 업데이트한다. | 기존 컴포넌트를 제거 후 새로 생성한다. |
트리거 | state, props 변경 | key 변경, 조건 분기 등 |
useEffect | deps가 변하면 재실행 | mount 됐으므로 useEffect의 첫 실행이 다시 발생함 |
내부 상태 | 유지 | 모두 초기화 state,ref 등 |
따라서, 내 경우에 다음과 같은 시나리오가 펼쳐졌을 것이다.
1. quizeIndex가 바뀌었지만 <KanjiCard />는 같은 컴포넌트라고 인식됐다.
2. 리렌더링은 일어났지만 리마운트가 되지 않았기에 useEffect(() => ..., []) 문이 실행되지 않았다.
3. key={quizeIndex}를 줬더니 리액트가 인식해 리마운트했다.
4. const [flipped, setFlipped] = useState(false);
해당 코드가 초기 설정되었다.
결과적으로, useEffect는 DOM 반영 후 다시 실행됐다.
간단한 예시를 들어보자.
<KanjiCard kanji="火" flipped={true} />
다음 코드에서는 같은 key를 사용하고 있기 때문에 같은 컴포넌트로 인식된다.
기존 인스턴스를 재사용하기 때문에 useEffect(() => ..., [])는 실행되지 않는다!!!
<KanjiCard key={quizIndex} kanji="水" flipped={false} />
반면, 이런 식으로 key를 넘겨주면 리마운트가 발생한다.
key가 바뀌기 때문에 기존 컴포넌트는 파괴되면서 새로 마운트가 진행된다.
자연히 useEffect(() => ..., [])가 실행되며 상태나 ref 등도 초기화된다.
결과적으로 useEffect(() => ..., [])는 마운트 시에만 호출된다.
key를 넘겨주지 않는 경우 리렌더링은 일어나지만
리마운트가 일어나지 않아서 useEffect가 실행되지 않았던 것.
key 변경을 통해 강제로 리마운트와 마운트를 유도해주면 해결되는 문제였다.
리마운트 | |
폼 초기화 | 이전 입력값 제거 가능 |
애니메이션 리셋 | CSS 트랜지션 초기화 |
useEffect 처음부터 다시 실행 | 상태 초기화 없이 다시 로직 실행 |
리마운트는 말 그대로 컴포넌트를 완전히 새로 만들어준다.
이 때문에 useEffect, useState, useRef 등이 초기 상태로 되돌아간다.
근데 진짜 왜 안됨?🤷♀️
근데 deps 분명 넘겨줬잖아요.
리렌더링은 useState로도 발생하는거고
quizIndex가 상태관리되고 있는데 그럼 리액트가 변경사항을 아는거 아닌가요?
왜 DOM에 적용되지 않는거죠...?
이건 바로 실행 순서에 따른 타이밍 문제였다.
useEffect는 리렌더링 이후에 실행되기 때문이다.
useEffect(() => {
setFlipped(false);
}, []);
나는 처음에 이렇게 썼다.
하지만 리마운트 동시에 useState(false)로
이미 flipped 초기값이 설정되고 있었기 때문에 저 useEffect는 애초에 불필요했다.
초기값을 설정해주지 않았더라도,
useEffect는 즉각적인 UI 반응과 거리가 먼 녀석이다.
useEffect의 실행 순서 파헤치기 🗺️
렌더링 → 가상 DOM 계산 → 실제 DOM 반영 → useEffect 실행
즉, useEffect는 항상 화면이 그려진 후에 실행된다.
deps가 있으나 없으나 항상 동일하게 실행된다.
DOM이 먼저 생성되니 useEffect가 실행되어봤자 UI 화면에서는...
그래서 UI 제어 목적인 상황에서 useEffect를 써봤자
작동은 하는데 화면 적용이 안 되는 것처럼 보인다.
결과적으로 지금 내 filp이 제대로 동작하는 이유는...
const [flipped, setFlipped] = useState(false);
이 친구 덕분이었다.
// ❌ 불필요한 코드
useEffect(() => {
setFlipped(false);
}, []);
이 코드는 완전히 불필요한 코드였다는 것😂
시나리오 🎥
- key를 바꿔 리마운트가 일어났다.
1. 새 컴포넌트가 처음부터 flipped = false 상태로 렌더링 됐다.
2. 이 렌더링에 맞춰 DOM이 만들어졌다.
3. 그 후 useEffect에서 setFlipped(false)가 일어나지만...
어차피 DOM 생성 이후므로 불필요한 리렌더링만 유발하는 쓸데없는 코드다.
렌더링(가상 DOM 생성)은 항상 DOM 업데이트보다 먼저 일어난다.
useEffect는 그 후에 실행된다.
따라서 useEffect를 이용했을 때 UI상으로 동작하지 않는 것처럼 보일 수 있다.
useEffect는 단순 UI에 반영할 데이터 상태를 변경하기 위해서 쓰는 것이 아니라는 교훈을 얻었다.
🤔 계속 리마운트 써도 괜찮을까?
근데 이렇게 filp 효과 하나 줄 때마다
계속 리마운트를 시키는 건 성능 면에서 걱정된다.
그래서 추후에는 useMemo 같은 걸 써서 최적화하는 방향도 고려 중이다.
이건 전체 기능 구현을 모두 완료하고 나서 별도의 최적화 기간을 두어 정리해볼 예정이다!
UI를 위한 상태값은 렌더 타이밍에
useState, useMemo, 조건부 렌더링 등으로 계산하고,
외부와 관련된 비동기/부수 효과만 useEffect로 처리하자!
목적 | 훅 |
화면에 보일 값을 만들고 싶은 경우 | useState, useMemo |
렌더링 중 데이터 계산이 필요한 경우 | useMemo, useCallback |
컴포넌트가 그려진 후 외부와 상호작용할 경우 | useEffect |
이렇게 사소해보였단 flip 효과 적용하기가
Re-render, Re-mount, useEffect 타이밍에 대한 인사이트로 연결될 줄은 몰랐다.
이 경험 덕분에 앞으로 useEffect는 필요한 곳에서 신중하게 쓸 수 있을 것 같다.
여기까지 긴 글 읽어주셔서 감사드리고
혹시 비슷한 삽질 하셨던 분들은 제가 함께 위로해드리겠습니다!
'Oops, All Code! > 🛠 Oops, My Code!' 카테고리의 다른 글
[React] useLocation을 사용했는데 state가 자꾸 null로 반환됐다. (0) | 2024.05.25 |
---|---|
[React] Warning: Each child in a list should have a unique "key" prop. 해결 방법 (0) | 2024.05.24 |
깃허브 기본 브랜치 변경 방법 (feat. master와 main의 차이) (0) | 2024.05.23 |
install npx 시도할 때 code EEXIST 에러 (0) | 2024.05.22 |
[React] ERROR in Plugin "react" was conflicted between and "BaseConfig" (0) | 2023.05.19 |
- Total
- Today
- Yesterday
- 일급객체
- 플리마켓운영
- 안성스타필드
- 서평
- 카드뉴스
- 회고
- 대학생플리마켓
- 어휘력
- 플리마켓후기
- 어른의어휘공부
- 프리코스
- react
- 소사벌맛집
- 소사벌
- 카페추천
- javascript
- 도서리뷰
- 타입좁히기
- 경험플리마켓
- 책추천
- 코딩테스트
- 대학생팝업스토어
- 프로토타입
- 도서추천
- typescript
- 우아한테크코스
- 프론트엔드
- 비즈플리마켓
- js
- 트러블슈팅
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |