블로그를 이전하면서 이전 블로그의 내용을 옮겼습니다.
리액트를 공부하면서 지난 시간 State hook에 이어서, 공식문서를 바탕으로 LifeCycle과 Effect hook을 배우면서, 얻은 지식과 내용들을 남겨보려 합니다. 지난 시간 State hook을 공부하면서, 약간의 혼돈이 온 부분이 있었는데요. 바로 기본적인 코드에서 혼돈이 왔었습니다.
아래의 코드를 봅시다.
import React, { useState } from "react"
export default function App() {
const [number, setNumber] = useState(1)
const add = () => setNumber((number) => number + 1)
// jsx
return(
<div>
<h1> Number : {number} </h1>
<div>
<button onClick={add}> + 1</button>
</div>
</div>
)
}
코드에서 왜 setNumber(setState함수)에 왜 콜백 함수를 넣어줘야 하는지 순간 이해가 되지 않았었습니다.
지난 시간에 react가 렌더링 되는 순간은 1) props가 바뀔 때, 2) state가 변경될 때, 3) 자식 컴포넌트를 포함하고 있는 부모 컴포넌트가 리렌더링 될 때 다시 렌더링이 발생한다고 했습니다.
왜 콜백 함수를 넣어줘야 하는지, 그 이유를 다음과 같이 설명할 수 있습니다.
setState 함수는 비동기 함수이기 때문에 인자로 콜백 함수를 넣을 수 있습니다. 참고로, 클래스 컴포넌트로 만들 때와 달리, 인자를 한 개만 받습니다. 이전 상태값을 콜백함수 내에 인자로 받아서, 이 인자를 변경할 콜백 함수 내 로직을 거쳐, 변경된 값을 반환하여야 변경된 값을 렌더링 할 수 있기 때문입니다.
여기서도 약간 혼돈이 오기 시작했었습니다🤔
그럼, useEffect와 빼놓을 수 없는 LifeCycle를 함께 다뤄 얘기해보겠습니다.
LifeCycle, 즉 '생명주기'란 리액트에서 컴포넌트가 mount(생성) 되었다가, unmount(제거)될 때까지의 주기(기간)이라고 말할 수 있습니다.
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
💡 참고로 위에서 말하는 React DOM은 Virtual DOM이라고 할 수 있습니다.
◾️ 공식문서를 바탕으로 LifeCycle이 어떻게 실행되는지 아래의 리액트 코드를 이용해서, 절차 단계를 분리하여 기술해 보겠습니다.
import React from "react"
export default class Clock extends React.Component{
constructor(props) {
super(props);
this.state = {date : new Date()}
}
tick() {
this.setState({
date: new Date(),
})
}
componentDidMount() {
console.log("componentDidMount")
this.timerID = setInterval(() => this.tick(), 1000)
}
componentWillUnmount(){
console.log("componentWillUnmount");
clearInterval(this.timerID);
}
render() {
return (
<div>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
)
}
}
c.f componentDidMount와 componentDidUpdate, componentWillUnmount 메서드는 모두 생명주기 메서드입니다.
✓ 생성될 때
✓ 업데이트 할 때
✓ 제거할 때
보통, setInterval()과 같은 메서드를 componentDidMount 메서드에서 사용한다면, clearInterval()과 같은 메서드를 WillUnmount 시에 사용한다. 즉, 묶음으로 사용하는 경우가 많다.
이 생명주기와 연관지어서, 이제 Effect Hook에 대해서 이야기 할 수 있습니다.
리액트 공식문서에서는 다음과 같이 이야기하고 있습니다.
React 컴포넌트 안에서 데이터를 가져오거나, DOM을 직접 조작하는 작업을 이전에도 종종 해봤을 것입니다. 우리는 이런 동작을 side Effects 또는 effects 라고 합니다. 왜냐하면, 이것은 다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서는 구현할 수 없는 작업이기 때문입니다.useEffect Hook을 이용하여 우리는 React에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 말합니다.
위의 내용을 통해, Effect Hook이 나온 배경을 알 수 있게 되었습니다. 즉, useEffect는 함수 컴포넌트 내에서 이런 저런 side Effects를 수행할 수 있게 도와준다는 것을 알 수 있습니다. side Effects는 영단어를 직역해보면, 부작용이라고 이야기 할 수도 있지만, 리액트에서 상태관리는 필수적이고, 이 상태가 변경되는 현상을 side Effects라고 역설적으로 바라보면 될 것 같습니다.
◾️ useEffect의 구조는 아래의 2가지 경우와 같습니다.
// 경우 1
export default function App(){
useEffect(() => {
}, [])
return (
<div>App is loading...</div>
)
}
// 경우 2
export default function App(){
useEffect(() => {
})
return (
<div>App is loading...</div>
)
}
✓ 경우 1은 빈 배열을 두 번째 인자로 활용하는 경우입니다.
이는 생명주기 메서드에서 componentDidMount처럼 동작하는 데요. return 부분에서 jsx 요소들이 첫 렌더링이 일어난 직 후, mount된 후에 내부의 화살표 함수를 가져와 실행합니다.
✓ 경우 2는 빈배열을 두 번째 인자로 받지 않고, 콜백 함수만 받는 경우 입니다.
이는 생명주기 메서드에서 componentDidMount + componentDidUpdate처럼 동작하는데요.
첫 번째 렌더링 된 직후에, 함수가 실행되고, props를 받거나 state가 업데이트 되면, 다시 리렌더링이 발생합니다.
일반적으로는 경우 2처럼 잘 사용되지 않는데요. 아래의 코드와 같이 사용됩니다.
export default function App(props){
const [state, setState] = useState(initValue);
useEffect(() => {
}, [state, props.a])
return(
<div> App is loading...</div>
)
}
경우 1처럼 배열을 받지만, 빈배열이 아닌 state와 props가 담겨 있습니다.
이 둘을 객체로서 props와 state가 변경되면 리렌더링이 발생하기 때문에, 위 코드에서 useEffect는 componentDidMount + 특정 값이 변경될 때만 실행되는 componentDidUpdate처럼 동작하게 됩니다.
이처럼 두 번째 인자로 들어오는 배열을 의존성 배열(Array Dependencies)라고 칭합니다. state의 상태 변경 또는 props의 변경에 의존한다는 이야기이겠죠?
💡 useEffect를 위와 같이 사용하면 좋은 점은 어떤 것이 있을까요?
state가 setState함수에 의해 업데이트 된 뒤에 렌더링을 하면 이전에 렌더링 된 값과 얕은 비교를 하는데요. 비교했을 때 다른 경우, React가 useEffect를 실행하고, 같으면 건너뛰게 합니다. 이는 componentDidUpdate를 건너뛰게 해서 최적화를 가능케 한다는 장점이 있습니다.
◾️ 그렇다면, useEffect를 생명주기 메서드 중 componentWillUnmount처럼 사용하려면 어떻게 해야할까요?
다음과 같습니다.
export default function App(props){
const [state, setState] = useState(initialState);
useEffect(() => {
return () => {
cleanup
}
}, [state, props.a])
return (
<div>App</div>
)
}
그럼 위에 LifeCycle을 설명하면서 들었던 예시를, useEffect 함수를 이용해서 다음과 같이 만들 수 있습니다. 단, Hook 사용을 위해서는 클래스 컴포넌트에서 함수형 컴포넌트로 변환시켜줘야 합니다.
import React, { useState, useEffect } from "react"
function Clock() {
const [date, setDate] = useState(new Date());
const tick = () => {
setDate(new Date());
}
useEffect(() => {
console.log('componentDidMount');
const timerId = setInterval(tick,1000);
return () => {
console.log('componentWillUnmount');
clearInterval(timerId);
}
}, [])
useEffect(() =>{
console.log('componentDidUpdate');
console.log(date);
}, [date])
return (
<div>
<p> 현재 시각은 | {date.toLocaleTimeString()}</p>
</div>
)
}
export default Clock
componentDidUpdate처럼 동작하는 부분만, 따로 분리해서 작성했는데, 이것이 바로 위에 setInterval과 clearInterval이 같은 useEffect 안에 있어야 가독성이 좋아진다는 장점만 있는 것이 아니라, 성능 최적화 측면에서도 유리하게 하는 것입니다.
즉, update가 발생하는 경우에만, effect를 실행하도록 코드를 분리하기 위함입니다.
이렇게 리액트의 근간이 되는 hook 중에 useState Hook을 회고하고, useEffect hook에 대해서 알아보았습니다. 저 개인적으로도 이렇게 글로 작성하면서, 한 번 더 머릿 속에서 정리할 수 있는 좋은 시간이 되었던 것 같습니다. 이 두 hook을 이용해서, 작은 기능 구현을 해보려 합니다. 다음 글은 구현해 보면서, 어떤 점이 구현하기 어려웠고, 어떻게 hook을 활용했는지 회고해 보겠습니다.