티스토리 뷰
프로젝트를 진행하면서 useEffect를 사용할때 가끔씩 이상하게 동작할때가 많았습니다. useEffect에서 리덕스 스토어의 값을 참조할때 예전값을 가져오는등의 현상이 발생해서 훅스를 제대로 이해하지 못하고 사용하는것 같아 이번 기회에 정리해보려고 합니다.
훅스를 쉽게 이해하기 위해 컴포넌트 라이프사이클 개념을 가져다 쓰다보니, 자꾸 엇나가는 부분이 생긴것같습니다. 훅스를 제대로 이해하기 위해서는 라이프 사이클을 잊으라고 말합니다.
렌더링 마다 고유한 Props와 State가 있다.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
위에서는 count라는 state를 사용해서 컴포넌트를 렌더링 하고 있습니다. 여기서 사용하는 이 count라는 변수는 사실 상수로 봐도 무방합니다.
// 처음 랜더링 시
function Counter() {
const count = 0; // useState() 로부터 리턴
// ...
<p>You clicked {count} times</p>
// ...
}
리액트가 컴포넌트를 렌더링할때 함수 내부를 실행합니다. useState()로 부터 리턴된 값 0은 count에 할당되고, 이 값은 상수입니다. 마찬가지로 컴포넌트 내부에 있는 모든 로컬 변수들은 렌더링 단위로 보면 그저 상수일뿐입니다.
모든 렌더링은 고유의 이벤트 핸들러를 가진다.
Click me를 누르면 카운트가 증가되고 Show alert를 누르면 3초 뒤에 현재 count값을 출력하는 예제입니다.
만약에 Click me를 3번 누르고 alert을 누른뒤, 다시 Click me를 2번 더 누르면 3이 출력될까요? 5가 출력 될까요?
미리 스포를 하자면 3이 나옵니다. 코드를 보시죠
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
우리가 show alert를 눌렀을 당시의 count는 3이었기 때문에 3이 출력되는것입니다. 아까도 말했듯이, 함수 내부의 로컬 변수들은 렌더링 단위로 상수화 됩니다. 그렇기 때문에 5가 아니라 3이 출력되는겁니다. handleAlertClick이라는 내부 함수에서 참조하는 count값은 렌더링 될 당시의 count값입니다.
Click me를 3번 눌렀을때 컴포넌트는 이런 형태일겁니다.
function Counter() {
const count = 3; (useState로 부터 리턴)
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + 3);
}, 3000);
}
<button onClick={handleAlertClick} />
}
count 변수가 상수화 되었기 때문에 이벤트 핸들러 handleAlertClick도 상수화 된 3이라는 값을 참조하고 있는겁니다.
컴포넌트 내부에 정의된 변수나 함수는 렌더링 단위로 갇힌다고 생각하면 될 것 같습니다.
모든 렌더링은 고유의 이펙트를 가진다.
카운터 함수를 다시 살펴봅시다.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
여기서 useEffect 안에 넘겨준 익명함수는 렌더링마다 새로 생성됩니다. 그 여러개의 서로 다른 익명함수는 각자가 생성될 당시의 count값을 바라보고 있습니다. 우리가 useEffect에 전달한 익명함수는 리액트가 변경사항을 DOM에 전부 반영하고 난 뒤에 실행해주는 콜백함수입니다. 그리고 그 콜백함수는 자신이 생성될 당시의 count값을 "바라보고" 있죠.
좀 더 이해를 돕기 위해 대화 예시를 들어보죠.
리액트
state가 0일때 UI 보여줘.
컴포넌트
<p>You clicked 0 times</p> 이거 렌더링 해주면 돼.
렌더링 끝나면 () => { document.title = 'You clicked 0 times'; } 이 함수도 같이 실행해주라.
리액트
OK 알겠어. 브라우저야 이거 DOM에 반영해줘
브라우저
반영했어.
리액트
렌더링이 끝났으니까 컴포넌트가 부탁한 콜백함수 실행시켜줄게.
렌더링 단위로 생각하자.
이번엔 조금 다른 예제를 봅시다.
버튼을 천천히 뜸들이면서 눌러봤습니다. 예상했던대로 버튼을 누를 당시의 count값이 콘솔에 출력되고 있습니다. 여기까지는 이상한게 없습니다.
똑같은걸 클래스형 컴포넌트로 만들면 이렇게 동작합니다.
비슷한 코드를 클래스형 컴포넌트로 만들면 언제나 최신 상태의 값을 바라봅니다.
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
그 이유는 버튼이 눌리고 3초 뒤에 this.state.count로 카운트의 값을 가져오는데 이때의 count값은 버튼이 눌릴 당시의 값이 아니라 객체 내에 저장된 변수의 값을 가져오기 때문에 항상 최신 상태를 가져옵니다.
이런 현상이 발생한 이유는 자바스크립트의 "클로저"라는 개념 때문입니다. 간략히 설명하면, 함수는 자신이 생성될 당시의 주변 변수를 기억합니다. 그래서 본인에게 없는 변수를 참조하라는 명령이 떨어졌을때 자신의 주변에 있는 변수까지 찾아보는 현상이 발생합니다.(스코프 체인) 그때 자기 주변에 있는 변수값은 변하지 않는 상수값으로 저장되어있습니다.
즉, useEffect에 넘겨준 익명함수가 컴포넌트 내부에 있는 state와 props를 렌더링 단위로 기억하는 이유는 그 익명함수가 생성될 당시의 상황(주변 변수)을 기억하는 클로져의 특성 때문입니다.
흐름을 거슬러 올라가기
여태 했던 얘기들을 정리해보자면, 컴포넌트 안에 있는 모든 함수는 렌더링 될 당시의 상수화된 변수값들을 사용합니다.
아래 두 예제는 같은 결과를 출력합니다.
function Example(props) {
useEffect(() => {
setTimeout(() => {
console.log(props.counter);
}, 1000);
});
// ...
}
function Example(props) {
const counter = props.counter;
useEffect(() => {
setTimeout(() => {
console.log(counter);
}, 1000);
});
// ...
}
왜냐면, 어차피 props가 렌더링 스코프 안에 갇히기 때문에 이걸 다시 다른 변수에 담더라도 똑같습니다.
이쯤되면 이제 이런 궁금증이 생기실겁니다.
"아니 그럼 컴포넌트 안에서 state나 props의 최신 상태를 가져오려면 어떻게 해야 하는건데?"
이건 다음 포스팅에서 계속 하도록 하겠습니다. To be Continue
출처
'React' 카테고리의 다른 글
useEffect 완벽가이드 - 3편 (0) | 2020.04.03 |
---|---|
useEffect 완벽 가이드 - 2편, 의존성 배열 deps와 클린업 함수 (1) | 2020.03.30 |
리액트의 Virtual DOM 이란? (리액트가 빠른 이유) (0) | 2020.03.26 |
스토리북에서 스토리를 작성하는 방법 (0) | 2020.03.25 |
스토리북에서 Component Story Format (CSF) 란? (0) | 2020.03.25 |
- Total
- Today
- Yesterday
- reactdom
- server side rendering
- Action
- typescript
- promise
- props
- reducer
- javascript
- storybook
- useEffect
- state
- async
- Next.js
- Polyfill
- react
- rendering scope
- await
- es6
- useRef
- computed
- return type
- hydrate
- type alias
- webpack
- react hooks
- reflow
- design system
- Babel
- atomic design
- mobx
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |