리액트는 현재는 대다수의 코드에서 함수형 컴포넌트로서 컴포넌트를 만들어 내지만, 이전에는 클래스형 컴포넌트와 같은 방법들이 존재하였다. 물론 클래스형 컴포넌트는 현재도 사용이 가능하지만, 함수형 컴포넌트가 거의 웬만한 React 코드들에서 사용되어진다. 왜 그런 것이며, 왜 함수형 컴포넌트까지의 진화가 이루어졌을까? 함수형 컴포넌트로 진화한 결과를 우리는 어떻게 이용할 수 있을까?
이를 React component의 변화 과정, 역사를 알아보며 궁금증들을 하나하나 해결해보자.
이 글은 https://ui.dev/why-react-hooks?source=post_page-----ae94d4ad3867 의 내용을 기반으로 작성하였습니다.
리액트는 2013년에 등장에 새로 등장하였다.

Javascript 자체의 class가 존재하는 es6의 탄생 이전이다보니, 클래스형으로 컴포넌트를 구현할 수 없었고 React.createClass 메서드를 활용하여 구현되었다.
var TextBoxList = React.createClass({
getInitialState: function() {
return {count: 1};
},
add: function() {
this.setState({count: this.state.count + 1});
},
render: function() {
var items = [];
for (var i = 0; i < this.state.count; i++) {
items.push(
<li key={i}>
<input type="text" placeholder="change me!" />
</li>
);
}
return (
<ul>
{items}
<input type="button" value="Add an item" onClick={this.add} />
</ul>
);
}
});
ReactDOM.render(
<div>
<p>Every time you add a new text box to the list, we "re-render" the whole list, but any text entered in the text boxes is left untouched because React is smart enough to just append the new text box instead of blowing away the old DOM nodes.</p>
<TextBoxList />
</div>,
document.getElementById('container')
);
이러한 형태의 컴포넌트 생성은 직관적이지 않는 것과 같이 여러 불편한 경험을 유발하였고, es6 문법의 등장으로 Javascript에 자체적인 class 문법이 탄생하자, React 팀은 javascript plain class를 이용하여 컴포넌트를 구현할 수 있는 React.Component를 만들어내게 되었다.
https://legacy.reactjs.org/blog/2014/03/28/the-road-to-1.0.html Before we even launched React publicly, members of the team were talking about how we could leverage ES6, namely classes, to improve the experience of creating React components. Calling
React.createClass(...)isn’t great. We don’t quite have the right answer here yet, but we’re close. We want to make sure we make this as simple as possible. It could look like this:class MyComponent extends React.Component { render() { ... } }

https://legacy.reactjs.org/blog/2015/01/27/react-v0.13.0-beta-1.html
class HelloMessage extends React.Component {
render() {
return <div>Hello {this.props.name}</div>;
}
}
React.render(<HelloMessage name="Sebastian" />, mountNode);
class ReposGrid extends React.Component {
constructor (props) {
super(props)
this.state = {
repos: [],
loading: true
}
this.updateRepos = this.updateRepos.bind(this)
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos (id) {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render() {
if (this.state.loading === true) {
return <Loading />
}
return (
<ul>
{this.state.repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
}
다음과 같이 대부분의 컴포넌트들이 클래스로 작성되었는데,
constructor (props) {
super(props)
this.state = {
repos: [],
loading: true
}
this.updateRepos = this.updateRepos.bind(this)
}
해당 부분을 살펴보면 constructor 함수 안에다가 state를 모두 선언해야하며, 자바스크립트의 this는 우리가 다른 언어들에서 사용하는 this와는 적용되는 방식이 달라 위 코드와 같이 bind를 해줄 필요가 있었다. 따라서 이러한 코드들이 코드의 가독성, 즉 개발자 경험을 해치는 결과가 발생한다.
이는 class field의 등장으로 이 문제들을 어느정도 해결할 수 있게되었음.
class ReposGrid extends React.Component {
state = {
repos: [],
loading: true
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render() {
const { loading, repos } = this.state
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
}
다음과 같이 class field와 arrow function을 이용하여 좀더 직관적인 코드로 변경되어 가독성이 증가할 수 있었다.
하지만 여전히 문제가 있었다.
이는 리액트에서 컴포넌트의 특성으로 정의하였던 것들과 연관이 있는데, 우선 클래스 컴포넌트에서는 컴포넌트 생명주기를 기준으로 컴포넌트의 동작을 정의할 수 있는 메서드가 있다.
위 코드의 componentDidMount , componentDidUpdate 가 다음과 같다.또한 이뿐만이 아닌 여러 상세한 생명주기들이 정의되어 있다.
이러한 상세한 생명주기에 따른 코드 작성은 리액트 컴포넌트의 visual logic과 non-visual logic이 뒤 섞이는 결과를 만든다. 물론 리액트 컴포넌트는 non-visual logic과 visual logic이 함께 어우러진 하나의 개체로 볼수 있다. 하지만 이러한 각각의 로직의 크기가 커진 상태에서 두개의 로직이 뒤섞여진다면 이는 꽤 코드가 복잡해지는 결과를 나을 수 있고, 이는 가독성 저하 등으로 인해 컴포넌트의 재사용성이 떨어지는 결과를 만들어 낼 수 있다.
이러한 문제를 해결하기위해, React Higher-Order Components 일명 HOC 기법이 도입된다. 코드로 살펴보자.
function withRepos (Component) {
return class WithRepos extends React.Component {
state = {
repos: [],
loading: true
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render () {
return (
<Component
{...this.props}
{...this.state}
/>
)
}
}
}
다음과 같이 visual logic이 존재하는 컴포넌트를 감싸는 상위의 컴포넌트를 만들어 그 컴포넌트에다가 non-visual logic을 관리하는 전략이다. 이는 원했던 두개의 로직의 분리가 가능해지는 결과를 만들어 낼 수 있었다. 이러한 장점도 있었지만 단점들도 존재했다.
non visual logic들 또한 여러개 존재한다면, 하나의 hoc에다가 때려박는거 보다는 분리하는 것이 가독성, 재사용성 측면에서 좋지 않겠는가, 따라서 이러한 non visual logic의 분리를 진행하다보면 이런 결과가 발생한다.
export default withHover(
withTheme(
withAuth(
withRepos(Profile)
)
)
)
극단적인 결과로는 이렇게도.
<WithHover>
<WithTheme hovering={false}>
<WithAuth hovering={false} theme='dark'>
<WithRepos hovering={false} theme='dark' authed={true}>
<Profile
id='JavaScript'
loading={true}
repos={[]}
authed={true}
theme='dark'
hovering={false}
/>
</WithRepos>
</WithAuth>
<WithTheme>
</WithHover>
이보다 더 심해질수도있다. 이역시 가독성 측면에서는 조금 아쉬운 결과를 보여준다. 마치 React Context 기능에서 존재하는 provider 지옥과 같은 결과가 발생한다.
또한 컴포넌트를 인수로 받아서 이에 비즈니스 로직을 추가한 새로운 컴포넌트로 만든다는 개념 자체가 꽤 복잡한 과정이다. 소프트웨어는 단순할수록 장점이 많다. 이런 복잡한 구조로는 아쉬운 점이 너무나 많다. 이러한 측면에서는 클래스형 컴포넌트 구조에 관해서도 할말이 많아진다. 클래스형 컴포넌트는 구조가 복잡하다. 생명주기 메서드는 너무 세부적이며, constructor와 같은 요소들은 코드가 장황해지는 결과를 낳는다.
이를 해결하기 위해, 함수형 컴포넌트가 나왔을까? 물론 이는 말을 정확하게 할 필요는 있다. 함수형 컴포넌트는 이전에도 존재하였다. 다만 클래스형 컴포넌트처럼 state를 정의하고 생명주기에 따른 동작과 같은 여러 사이드 이펙트 동작들을 관리할 방법이 없었기에 비주얼적인 요소들만을 정의하는 요소로서 사용되었다.
const PureFComponent = () => {
return (<div>PureFComponent</div>)
}
하지만 16.8 업데이트 클래스형 컴포넌트 고유 기능인 상태(state)관리와 라이프사이클 관리 기능을 함수형 컴포넌트에서도 쓸 수 있도록 해주는 함수들을 총칭한 Hooks 가 등장하였고, 이를 이용하여 함수형 컴포넌트에서도 state를 관리할 수 있게 되었다.
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
useEffect의 등장은 이전 클래스형 컴포넌트에서 사용하던 생명주기에 따른 동작에 관한 개념의 변화를 만들어 냈다.
이전의 세세한 생명주기에 따른 동작에 따른 메서드 정의는 non visual logic과 visual logic이 뒤섞이는 결과를 만들어 냈다고 하였다. 이를 해결하기 위해, synchronization의 측면으로 묶어서 생각하기로 하였다. state를 어떤 조건에 따라 새로 업데이트 해야한다 던지, 데이터를 fetch한다던지, dom 요소의 업데이트에 따른 변화를 감지해야한다던지, 이러한 요소들을 모두 사이드 이펙트적인 synchronization 측면에서의 요소로 묶어서 생각한다.
이를 하나의 측면으로 생각하여 관리하기위해 useEffect가 React hook중 하나로 등장하여 이전의 세세함의 문제를 해결할 수 있게 되었다.
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
다음과 같이 코드가 많이 깔끔해진것을 확인할 수 있다.
하지만 두 로직간의 분리가 이루어지진 않았다. hoc와 같이 말이다.
이 또한 React hooks에 해결법이 있다.
function useRepos (id) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
return [ loading, repos ]
}
다음과 같이 자신만의 원하는 non visual logic을 담아낼 수 있는 custom hook을 만들어내어 로직의 분리를 이끌어 낼 수 있으며, 재사용 또한 자유로워 가독성과 재사용성 두마리의 토끼를 잡아낼 수 있다.
리액트의 컴포넌트 진화 과정은 지금까지와 같다. 현재 react hook을 사용하는 함수형 컴포넌트는 강력한 단순함을 가짐으로써, 재사용성, 가독성이 매우 뛰어나다. 이러한 장점들로 인해, 기존의 여러 클래스형 컴포넌트 코드들을 대체하고 대다수의 곳에서 사용되고 있으며, 리액트 팀에서 또한 함수형 컴포넌트를 장려하는 것을 넘어 함수형 컴포넌트를 기반으로 새로운 리액트의 미래를 만들어 나가고 있다.
코드를 작성할 때는 이러한 변화 과정을 통해 왜 함수형 컴포넌트가 만들어 졌으며, 왜 사용해야하는지를 알며 이를 생각하여 코드를 작성할 필요가 있다.
이러한 과정을 알아보면서 내가 느낀 함수형 컴포넌트의 강점은 무엇보다도 단순함이다. 단순하면서도 강력한 기능을 가진 가독성이 뛰어난 컴포넌트를 만들어내어 재사용성을 극대화할 수 있으며, 이러한 과정은 리액트의 강점을 효과적으로 이용하는 것이다.
이를 생각하며 리액트 코드를 작성하면, 좀더 강력한 코드를 작성할 수 있을 것이다. 컴포넌트의 동작이 단순하지 않고, 복잡해진다면 무언가 문제가 생긴것이다. 기능을 다시 생각해보거나, 혹은 컴포넌트를 좀더 단순한 여러 컴포넌트들로 분리할 필요가 있다고 볼수 있다.
이러한 점들을 생각하며 현명하게 리액트 세계를 탐험해나가보자!