React Hooks 이해하기
React Hooks 이해하기
React Hooks는 함수형 컴포넌트가 React의 생명주기와 상태에 “연결”할 수 있는 방법입니다. React 16.8.0에서 처음 소개되었습니다. 이전에는 클래스 기반 컴포넌트만 React의 생명주기와 상태를 사용할 수 있었습니다. Hooks는 함수형 컴포넌트가 이를 가능하게 할 뿐만 아니라, 컴포넌트 간에 상태 관련 로직을 쉽게 재사용할 수 있도록 해줍니다.
Hooks를 처음 사용한다면, 변화가 다소 생소할 수 있습니다. 이 장은 Hooks가 어떻게 동작하는지, 그리고 어떻게 생각해야 하는지 이해하는 데 도움을 주기 위해 작성되었습니다. 클래스 컴포넌트의 사고방식에서 React Hooks를 사용하는 함수형 컴포넌트로 전환하는 데 도움을 드리고자 합니다. 이 장에서는 다음 내용을 다룹니다:
- 클래스 컴포넌트의 생명주기 간단 복습
- Hooks를 사용한 함수형 컴포넌트의 생명주기 개요
- 함수형 컴포넌트에서 React Hooks를 이해하기 위한 좋은 사고방식
- 클래스와 함수형 컴포넌트의 미묘한 차이
이 장에서는 특정 React Hooks에 대한 자세한 내용을 다루지 않습니다. React 공식 문서가 그런 내용을 찾기에 좋은 곳입니다.
시작해봅시다.
React 클래스 컴포넌트 생명주기
React 클래스 컴포넌트를 사용해 본 적이 있다면 주요 생명주기 메서드에 익숙할 것입니다.
constructor
render
componentDidMount
componentDidUpdate
componentWillUnmount
- 기타
예제를 통해 간단히 이해해 봅시다. Hello
라는 컴포넌트가 있다고 가정해 보겠습니다.
class Hello extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {}
componentDidUpdate() {}
componentWillUnmount() {}
render() {
return (
<div>
<h1>Hello, world!</h1>
</div>
);
}
}
이것이 React가 Hello
컴포넌트를 생성할 때 대략적으로 수행하는 작업입니다. 이는 단순화된 모델이며, 실제로는 이와 정확히 같지 않을 수 있습니다.
-
React는 컴포넌트의 새 인스턴스를 생성합니다.
const HelloInstance = new Hello(someProps);
-
이는 컴포넌트의
constructor(someProps)
를 호출합니다. -
그런 다음
HelloInstance.render()
를 호출하여 처음으로 렌더링합니다. -
다음으로
HelloInstance.componentDidMount()
를 호출합니다. 여기서 API 호출을 실행하고setState
를 호출하여 컴포넌트를 업데이트할 수 있습니다. -
setState
를 호출하면 React가HelloInstance.render()
를 다시 호출합니다. 이는 React가 컴포넌트를 다시 렌더링하려는 경우(예: 부모 컴포넌트가 다시 렌더링되는 경우)에도 해당됩니다. -
업데이트된 렌더링 후, React는
HelloInstance.componentDidUpdate()
를 호출합니다. -
마지막으로 컴포넌트를 제거할 때(예: 사용자가 다른 화면으로 이동하는 경우), React는
HelloInstance.componentWillUnmount()
를 호출합니다.
생명주기에서 이해해야 할 핵심은 클래스 컴포넌트가 한 번 인스턴스화되고, 동일한 인스턴스에서 다양한 생명주기 메서드가 호출된다는 점입니다. 이는 클래스 변수를 사용하여 클래스 인스턴스에 일종의 “상태”를 로컬로 저장할 수 있음을 의미합니다. 이는 아래에서 논의할 흥미로운 함의를 가지고 있습니다.
하지만 지금은 함수 컴포넌트의 흐름을 살펴보겠습니다.
React 함수 컴포넌트 생명주기
기본적인 React 함수 컴포넌트부터 시작해 React가 이를 어떻게 렌더링하는지 살펴보겠습니다.
function Hello(props) {
return (
<div>
<h1>Hello, world!</h1>
</div>
);
}
React는 이 함수를 단순히 실행하여 렌더링합니다!
Hello(someProps);
리렌더링이 필요한 경우, React는 여러분의 함수를 다시 실행합니다!
여기서는 단순화된 React 모델을 사용하고 있지만, 개념은 직관적입니다. 함수 컴포넌트의 경우, React는 렌더링이나 리렌더링이 필요할 때마다 여러분의 함수를 실행합니다.
이 간단한 함수 컴포넌트는 스스로를 제어할 수 없습니다. 또한, 앞서 살펴본 클래스 컴포넌트처럼 React 렌더링 생명주기와 관련된 작업을 수행할 수 없습니다.
이때 훅(Hooks)이 등장합니다!
React Hooks 추가하기
React Hooks는 함수형 컴포넌트가 React의 상태와 생명주기에 “연결”할 수 있게 해줍니다. 예제를 살펴보겠습니다.
function Hello(props) {
const [stateVariable, setStateVariable] = useState(0);
useEffect(() => {
console.log("마운트 및 업데이트");
return () => {
console.log("클린업");
};
});
return (
<div>
<h1>Hello, world!</h1>
</div>
);
}
여기서 두 가지 Hooks를 사용하고 있습니다: useState
와 useEffect
. 하나는 React에게 상태를 저장하라고 알려주고, 다른 하나는 렌더링 생명주기 동안 호출하라고 알려줍니다.
-
컴포넌트가 렌더링될 때,
useState(<VARIABLE>)
을 호출하여 React에게 상태에 무언가를 저장하고 싶다고 알립니다. React는[ stateVariable, setStateVariable ]
을 반환합니다. 여기서stateVariable
은 상태에서 이 변수의 현재 값이고,setStateVariable
은 이 변수의 새로운 값을 설정할 수 있는 함수입니다. useState가 어떻게 동작하는지 여기서 읽어볼 수 있습니다. -
다음으로
useEffect
Hook을 사용합니다. 컴포넌트가 렌더링되거나 업데이트될 때마다 React가 실행할 함수를 전달합니다. 이 함수는 컴포넌트가 이전 렌더링을 정리해야 할 때 호출될 함수를 반환할 수도 있습니다. 따라서 React가 컴포넌트를 렌더링하고, 어느 시점에서setStateVariable
을 호출하면 React는 이를 다시 렌더링해야 합니다. 대략 다음과 같은 일이 발생합니다:// React가 컴포넌트를 렌더링 Hello(someProps); // 콘솔에 표시: 마운트 및 업데이트 ... // React가 컴포넌트를 다시 렌더링 // 콘솔에 표시: 클린업 Hello(someProps); // 콘솔에 표시: 마운트 및 업데이트
-
마지막으로 컴포넌트가 언마운트되거나 제거될 때, React는 클린업 함수를 다시 한 번 호출합니다.
여기서 생명주기 흐름이 이전과 정확히 같지 않다는 것을 알 수 있습니다. 그리고 useEffect
Hook은 모든 렌더링에서 실행(및 정리)됩니다. 이는 의도된 설계입니다. 클래스 컴포넌트와 달리 함수형 컴포넌트는 모든 렌더링에서 실행된다는 점을 마음에 새겨야 합니다. 그리고 단순한 함수이기 때문에 내부적으로 자체 상태를 가지고 있지 않습니다.
추가로, useEffect
가 초기 마운트와 최종 언마운트 시에만 호출되도록 하려면 빈 배열([]
)을 두 번째 인수로 전달할 수 있습니다.
useEffect(() => {
console.log("마운트");
return () => {
console.log("언마운트 예정");
};
}, []);
React Hooks의 멘탈 모델
여러분이 Hooks를 사용한 함수 컴포넌트를 생각할 때, 이들은 매번 다시 실행된다는 점에서 매우 단순합니다. 코드를 보면서, 매번 순서대로 실행된다고 상상해 보세요. 그리고 함수에는 로컬 상태가 없기 때문에 사용 가능한 값은 React가 상태로 저장한 것뿐입니다.
클래스 컴포넌트와는 다릅니다. 클래스 컴포넌트에서는 렌더링 시 특정 메서드가 호출됩니다. 또한, 로컬 상태 변수에 일부 상태를 저장했을 수도 있습니다. 이는 코드를 디버깅할 때 로컬 상태 변수의 현재 값을 염두에 두어야 한다는 것을 의미합니다.
이러한 로컬 상태의 미묘한 차이는 클래스 컴포넌트 버전에서 매우 미묘한 버그를 유발할 수 있으며, 이를 자세히 이해하는 것이 중요합니다. 반면, JavaScript 클로저 덕분에 함수 컴포넌트는 더 직관적인 실행 모델을 가지고 있습니다.
이제 이에 대해 자세히 살펴보겠습니다.
클래스 컴포넌트와 함수 컴포넌트의 미묘한 차이
이 섹션은 Dan Abramov의 훌륭한 포스트 “How Are Function Components Different from Classes?”를 기반으로 합니다. 이 글은 React Hooks와 직접적으로 관련되지는 않지만, 클래스 컴포넌트에서 함수 컴포넌트로의 사고방식 전환에 도움을 주기 때문에 핵심 내용을 다룰 것입니다. React Hooks를 사용하기 시작하면 이러한 전환이 필요합니다.
Dan의 포스트에서 가져온 예제를 통해 동일한 컴포넌트를 클래스와 함수 컴포넌트로 비교해 보겠습니다.
class ProfilePage extends React.Component {
showMessage = () => {
alert("Followed " + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <Button onClick={this.handleClick}>Follow</Button>;
}
}
이제 함수 컴포넌트 버전을 살펴보겠습니다.
function ProfilePage(props) {
const showMessage = () => {
alert("Followed " + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <Button onClick={handleClick}>Follow</Button>;
}
컴포넌트가 무엇을 하는지 잠시 이해해 보세요. setTimeout
호출 대신 API 호출을 한다고 상상해 보세요. 두 버전 모두 거의 동일한 작업을 수행합니다.
그러나 클래스 버전은 매우 미묘한 방식으로 버그가 있습니다. Dan은 이 코드의 데모 버전을 제공합니다. 팔로우 버튼을 클릭하고 3초 이내에 선택된 프로필을 변경한 후 어떤 메시지가 표시되는지 확인해 보세요.
클래스 버전의 버그는 다음과 같습니다. 버튼을 클릭한 후 3초 이내에 this.props.user
가 변경되면, 경고 메시지는 새로운 사용자를 표시합니다! 이 장을 따라왔다면 이는 놀라운 일이 아닙니다. React는 리렌더링 사이에 동일한 클래스 컴포넌트 인스턴스를 사용합니다. 즉, 코드 내에서 this
객체는 동일한 인스턴스를 참조합니다. 따라서 개념적으로 React는 다음과 같이 ProfilePage
인스턴스의 prop을 변경합니다.
// 인스턴스 생성
const ProfilePageInstance = new ProfilePage({ user: "First User" });
// 첫 번째 렌더링
ProfilePageInstance.render();
// 버튼 클릭
this.handleClick();
// 타이머 시작
// prop 업데이트
ProfilePageInstance.props.user = "New User";
// 리렌더링
ProfilePageInstance.render();
// 타이머 완료
// 여기서 this <=> ProfilePageInstance
alert("Followed " + this.props.user);
따라서 alert
가 실행될 때 this.props.user
는 New User
입니다!
이제 함수 컴포넌트 버전이 이를 어떻게 처리하는지 살펴보겠습니다.
// 첫 번째 렌더링
ProfilePage({ user: "First User" });
// 버튼 클릭
handleClick();
// 타이머 시작
// 업데이트된 prop으로 리렌더링
ProfilePage({ user: "New User" });
// 타이머 완료
// 첫 번째 ProfilePage() 호출 스코프에서
alert("Followed " + props.user);
여기서 중요한 차이점은 alert
호출이 첫 번째 ProfilePage()
호출 스코프에서 이루어진다는 것입니다. 이는 JavaScript 클로저 덕분에 가능합니다. 여기서는 “인스턴스”가 없기 때문에 코드는 일반 JavaScript 함수이며, 실행된 스코프에 묶여 있습니다.
위 패턴은 React Hooks에만 국한된 것이 아니라 JavaScript 함수의 동작 방식입니다. 그러나 지금까지 클래스 컴포넌트를 사용해 왔다면 React Hooks로 전환할 때 이 패턴을 정확히 이해하는 것이 중요합니다.
요약
이를 통해 우리는 컴포넌트를 일반적인 자바스크립트 함수처럼 생각할 수 있습니다. 라이프사이클 메서드가 호출되는 특별한 순서도 없고, 추적할 로컬 상태도 없습니다. 핵심 내용은 다음과 같습니다:
“React는 렌더링이 필요할 때 여러분의 함수 컴포넌트를 반복해서 호출합니다. 상태를 저장하고 React 렌더링 라이프사이클에 연결하려면 React Hooks를 사용해야 합니다. 그리고 자바스크립트 클로저 덕분에 변수는 특정 함수 호출에 스코프가 제한됩니다.”
이 장이 React Hooks를 사용한 함수 컴포넌트를 이해하는 데 도움이 되길 바랍니다. 추가로 설명이 필요한 부분이 있다면 아래 토론 스레드에 댓글을 남겨주세요.
For help and discussion
Comments on this chapter