-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
260 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
# Effect 의존성 제거하기 | ||
|
||
## useEffect 의존성 배열에 빈 배열을 써도 될까? | ||
|
||
혹시 생각 해본적 있는가? 린터 오류를 무시하기 위해 이유를 모른채 빈배열을 일단 써오지는 않았는가? 오늘은 이 주제에 대해서 얘기해보려 한다. | ||
|
||
## 의존성 배열이란? | ||
|
||
> When you write an Effect, the linter will verify that you’ve included every reactive value (like props and state) that the Effect reads in the list of your Effect’s dependencies. This ensures that your Effect remains synchronized with the latest props and state of your component. Unnecessary dependencies may cause your Effect to run too often, or even create an infinite loop. Follow this guide to review and remove unnecessary dependencies from your Effects. | ||
Effect가 실행될 때 react는 useEffect의 두번째 인자로 들어가는 배열에 들어가있는 값이 바뀌었는지를 확인하고 바뀌었다면 재실행한다. 이 배열을 의존성 배열이라고 하는데 linter가 Effect가 사용하는 모든 props, state와 같은 반응형 값을 포함하고 있는지를 확인한다고 설명이 써있다. | ||
그래서 빈 배열을 주면 | ||
|
||
``` | ||
Lint Error | ||
11:6 - React Hook useEffect has a missing dependency: 'roomId'. Either include it or remove the dependency array. | ||
``` | ||
|
||
라는 오류가 발생하게된다. | ||
|
||
그럼 왜 빈배열을 주면 오류를 발생시키게 만들어놨을까? | ||
|
||
## 의존성에게 거짓말을 하지 마라 | ||
|
||
``` | ||
useEffect(() => { | ||
document.title = 'Hello, ' + name; | ||
}, [name]); | ||
``` | ||
|
||
위의 코드에서 의존성 배열은 이펙트 내에서 쓰이는 모든 값을 가지고있다. 이로써 이펙트는 언제 다시 이펙트를 실행해야 할지 알고있다. 가령 name이 dan에서 yuji로 바뀌게 되면 의존성이 다르기때문에 이펙트를 재실행한다. | ||
하지만 저 배열에 []를 줬다면, 이펙트는 두번 다시 새로 실행 되지 않는다. | ||
|
||
이런 경우는 어떨까? 매 초마다 숫자가 올라가는 카운터를 작성하고싶다. 그러면 이런 생각을 할 수 있다. "이펙트를 한 번만 설정하고, 한 번만 제거하자." 그럼 이런 코드가 나온다. | ||
|
||
``` | ||
function Counter() { | ||
const [count, setCount] = useState(0); | ||
useEffect(() => { | ||
const id = setInterval(() => { | ||
setCount(count + 1); | ||
}, 1000); | ||
return () => clearInterval(id); | ||
}, []); | ||
return <h1>{count}</h1>; | ||
} | ||
``` | ||
|
||
"이게 처음 한 번만 실행 되었으면 좋겠어." 하고 빈배열을 넣었다. | ||
이 코드의 실행 결과를 예상해보라. 어떨 것 같은가? | ||
이 예제는 카운트를 단 한번만 증가시킨다. | ||
만약 우리의 멘탈모델이 "의존성 배열은 내가 언제 이펙트를 다시 실행해야할지 지정해야할 때 쓰인다." 라면, 위 예제를 볼 때 자가당착에 빠지게 된다. 이건 인터벌이니까 한 번만 실행하고 싶다. 왜 이런 문제가 일어나게 됐을까? | ||
|
||
의존성배열은 리액트에게 이 이펙트에서는 이 값만을 쓰겠다고 약속하는 것이다. 그러므로 이펙트에서 쓰이는 값은 모두 알려줘야한다. 하지만 위 예제는 어떤가? count를 쓰면서 쓰지 않는다고 거짓말을 했다. 여기서 이게 어떤식으로 문제가 될까? | ||
|
||
첫번째 렌더링에서 count는 0이다. 따라서 첫번째 렌더링의 이펙트에서 setCount(count + 1)은 setCount(0 + 1)이라는 뜻이 된다. 의존성배열을 []라고 정의했기 때문에 이펙트를 절대 다시 실행하지 않고, 결국 그로 인해 매 초마다 setCount(0 + 1)을 호출하는 것이다. | ||
|
||
``` | ||
// 첫 번째 렌더링, state는 0 | ||
function Counter() { | ||
// ... | ||
useEffect( | ||
// 첫 번째 렌더링의 이펙트 | ||
() => { | ||
const id = setInterval(() => { | ||
setCount(0 + 1); // 언제나 setCount(1) | ||
}, 1000); | ||
return () => clearInterval(id); | ||
}, | ||
[] // 절대 다시 실행하지 않는다 | ||
); | ||
// ... | ||
} | ||
// 매번 다음 렌더링마다 state는 1이다 | ||
function Counter() { | ||
// ... | ||
useEffect( | ||
// 이 이펙트는 언제나 무시될 것 | ||
// 왜냐면 리액트에게 빈 deps를 넘겨주는 거짓말을 했기 때문 | ||
() => { | ||
const id = setInterval(() => { | ||
setCount(1 + 1); | ||
}, 1000); | ||
return () => clearInterval(id); | ||
}, | ||
[] | ||
); | ||
// ... | ||
} | ||
``` | ||
|
||
따라서 의존성배열을 []로 지정하는 것은 버그를 만들 것이다. 그리고 이런 종류의 이슈는 해결책을 떠올리기 어렵다. 그러므로 의존성 배열에게 솔직하게 값을 알려주는 것을 중요한 규칙으로 받아들여야한다. | ||
|
||
그러면 어떻게 솔직하게 쓸까? | ||
|
||
## 의존성을 솔직하게 적는 방법 | ||
|
||
두가지 전략이 있다. 하나는 컴포넌트 안에 있으면서 Effect안에서 쓰이는 모든 값을 배열에 넣는 것이고 하나는 이펙트의 코드를 바꿔서 우리가 원하던 것 보다 자주 바뀌는 값을 요구하지 않도록 만드는 것이다. | ||
|
||
일반적으로 첫번째 방법을 사용해보고, 필요하다면 두번째 방법을 이용하는데 주제가 의존성 제거하기인만큼 후자를 설명해보겠다. | ||
|
||
## 의존성을 더 적게 넘겨주기 | ||
|
||
몇가지 기술을 살펴보자. | ||
|
||
### 이펙트가 자급자족하도록 만들기 | ||
|
||
``` | ||
useEffect(() => { | ||
const id = setInterval(() => { | ||
setCount(count + 1); | ||
}, 1000); | ||
return () => clearInterval(id); | ||
}, [count]); | ||
``` | ||
|
||
여기서 count를 어떻게 제거할까?<br> | ||
그 전에 한번 질문을 해보자. 왜 count를 쓰고있는가?<br> | ||
오로지 setCount를 위해 쓰고있는 것으로 보인다. 이 경우 스코프안에서 count를 쓸 필요가 전혀 없다. 이전상태를 기준으로 상태값을 업데이트 하고싶을때는 setState에 함수형태의 업데이터를 사용하면 된다. | ||
|
||
``` | ||
useEffect(() => { | ||
const id = setInterval(() => { | ||
setCount(c => c + 1); | ||
}, 1000); | ||
return () => clearInterval(id); | ||
}, []); | ||
``` | ||
|
||
setCount(count + 1)이라고 썼기때문에 count는 분명 이펙트 안에서 필요한 의존성이었다. 하지만 우리는 단지 count를 count + 1로 변환하여 리액트에게 "돌려주기"를 원했을 뿐이다.<br> | ||
하지만 리액트는 현재의 count를 이미 알고있다. 우리가 리액트에게 알려줘야 하는것은 지금 값이 뭐든 간에 상태값을 + 1 하라는 것이다. | ||
<br> | ||
그것이 setCount(c => c + 1)이 의도하는 것이다. 리액트에게 상태가 어떻게 바뀌어야하는지 "지침을 보내는 것" 이라고 생각할 수 있다.<br> | ||
꼼수를 쓴게 아니다. 실제로 이펙트는 더이상 렌더링 스코프에서 count값을 읽어들이지 않는다. | ||
<br> | ||
이 이펙트가 한번만 실행 되었다 하더라도 첫번째 렌더링에 포함되는 인터벌 콜백은 인터벌이 실행될 때마다 c => c + 1이라는 지침을 완벽하게 전달한다. 더이상 현재의 count 상태를 알고 있을 필요가 없다. 리액트가 이미 알고있으니까. | ||
<br> | ||
|
||
### 함수를 이펙트 안으로 옮기기 | ||
|
||
``` | ||
function SearchResults() { | ||
const [data, setData] = useState({ hits: [] }); | ||
async function fetchData() { | ||
const result = await axios( | ||
'https://hn.algolia.com/api/v1/search?query=react', | ||
); | ||
setData(result.data); | ||
} | ||
useEffect(() => { | ||
fetchData(); | ||
}, []); // 이거 괜찮은가? | ||
// ... | ||
``` | ||
|
||
일단 이 코드는 동작한다. 하지만 간단히 로컬 함수를 의존성에서 제외하는 해결책은 컴포넌트가 커지면서 모든 경우를 다루고 있는지 보장하기 아주 힘들다는 문제가 있다. | ||
<br> | ||
각 함수가 5배정도는 커져서 코드를 이런 방식으로 나누었다고 생각해보자. | ||
|
||
``` | ||
function SearchResults() { | ||
// 이 함수가 길다고 상상해보자 | ||
function getFetchUrl() { | ||
return 'https://hn.algolia.com/api/v1/search?query=react'; | ||
} | ||
// 이 함수도 길다고 상상해보자 | ||
async function fetchData() { | ||
const result = await axios(getFetchUrl()); | ||
setData(result.data); | ||
} | ||
useEffect(() => { | ||
fetchData(); | ||
}, []); | ||
// ... | ||
} | ||
``` | ||
|
||
이제 나중에 이 함수들 중에 하나가 state나 prop을 사용한다고 생각해보자. | ||
|
||
``` | ||
function SearchResults() { | ||
const [query, setQuery] = useState('react'); | ||
// 이 함수가 길다고 상상해보자 | ||
function getFetchUrl() { | ||
return 'https://hn.algolia.com/api/v1/search?query=' + query; | ||
} | ||
// 이 함수가 길다고 상상해 보자 | ||
async function fetchData() { | ||
const result = await axios(getFetchUrl()); | ||
setData(result.data); | ||
} | ||
useEffect(() => { | ||
fetchData(); | ||
}, []); | ||
// ... | ||
} | ||
``` | ||
|
||
만약 이런 함수를 사용하는 단 하나의 이펙트에서라도 의존성 배열을 업데이트 하는 것을 깜빡했다면 이펙트는 props와 state 동기화에 실패할 것이다.<br> | ||
다행히도 이 문제를 해결할 쉬운 방법이 있다. 어떠한 함수를 이펙트 안에서만 쓴다면, 그 함수를 "직접 이펙트 안으로" 옮겨라. | ||
|
||
``` | ||
function SearchResults() { | ||
// ... | ||
useEffect(() => { | ||
// 아까의 함수들을 안으로 옮겼다! | ||
function getFetchUrl() { | ||
return 'https://hn.algolia.com/api/v1/search?query=react'; | ||
} | ||
async function fetchData() { | ||
const result = await axios(getFetchUrl()); | ||
setData(result.data); | ||
} | ||
fetchData(); | ||
}, []); // ✅ 의존성은 OK | ||
// ... | ||
} | ||
``` | ||
|
||
이러면 뭐가 좋냐? 우리는 더이상 "옮겨지는 의존성"에 신경쓸 필요가 없다. 의존성 배열은 더이상 거짓말 하지 않는다.<br> | ||
**진짜로 이펙트 안에서 컴포넌트의 범위 바깥에 있는 그 어떠한 것도 사용하고 있지 않다.** | ||
<br> | ||
나중에 getFetchUrl 을 수정하고 query state를 써야한다고 하면, 이펙트 안에 있는 함수만 고치면 된다는 것을 쉬이 발견할 수 있다. 거기에 더해 query 를 이펙트의 의존성으로 추가해야 할 것이다. | ||
|
||
``` | ||
function SearchResults() { | ||
const [query, setQuery] = useState('react'); | ||
useEffect(() => { | ||
function getFetchUrl() { | ||
return 'https://hn.algolia.com/api/v1/search?query=' + query; | ||
} | ||
async function fetchData() { | ||
const result = await axios(getFetchUrl()); | ||
setData(result.data); | ||
} | ||
fetchData(); | ||
}, [query]); // ✅ 의존성은 OK | ||
// ... | ||
} | ||
``` | ||
|
||
이 의존성을 더하는 것이 단순히 “리액트를 달래는” 것은 아니다. query 가 바뀔 때 데이터를 다시 페칭하는 것이 말이 된다. useEffect 의 디자인은 사용자가 제품을 사용하다 겪을 때까지 무시하는 대신, 데이터 흐름의 변화를 알아차리고 이펙트가 어떻게 동기화해야할지 선택하도록 강제한다. 그게 린터가 하는 일인것이다. | ||
|
||
### 함수를 이펙트 안에 넣고싶지 않다면? | ||
|
||
시간관계상 생략 |