리액트 훅이란?
글을 통해 전 하고 싶은 메세지
- 리액트 훅은 "리액트 기능을 활용할 수 있는 함수"이다.
- 리액트 훅은 "함수형 컴포넌트에서만 사용할 수 있다."
- 리액트 훅은 "클래스형 컴포넌트의 단점을 보완하고자 만들어졌다."
- 리액트 훅은 "make it easier build great UI" 라는 철학을 바탕으로 만들어졌다.
동기
리액트를 사용하면서 핵심 기능 중 하나인 Hooks
에 대해서 제대로 고민을 해본 적이 없었다.
항해플러스를 진행하면서, 리액트 훅의 원리를 깊게 이해하고, 직접 구현해볼 기회가 생겼고, 다음과 같은 영상을 보게 되었다.
학습을 시작하면서 위의 영상을 보게 되었고, 이번 기회에 제대로 정리를 해보고자 한다.
리액트의 개발 철학
리액트는 "make it easier build great UI" 라는 철학을 바탕으로 만들어졌다.
그리고, 이러한 철학을 바탕으로 리액트 팀은 리액트의 기능을 계속해서 발전시켜왔다.
이런 배경을 먼저 언급하는 이후는, 이런 철학에서 개발된 기능이 바로 Hooks
이기 때문이다.
내가 생각하는 훅
나는 훅을 "리액트 기능을 활용할 수 있는 함수"라고 생각한다.
리액트 기능을 사용하기에 몇가지 제약이 있고, Lint 등의 사용 때문에 use
접두사를 붙여야하는 컨벤션이 존재하지만, 그럼에도 결국 리액트 차원에서의 함수라고 생각한다.
그렇기에 함수로 코드를 분리했을 때의 장점을 일부 공유한다.
훅(Hooks)의 어원
"함수라고 표현하면 되는거 아닌가요? 훅이라고 표현한 데에는 이유가 있지 않을까요?"
전문가로서 용어를 명확하게 하는 것은 중요하다고 생각한다.
그리고, 리액트 팀은 훅을 "Hooks"라고 표현하고 있다.
그 이유는 무엇일까?
Hooks are functions that let you “hook into” React state and lifecycle features from function components.
리액트 공식 문서에서는 위와 같이 이야기하고 있다.
Hook은 함수 컴포넌트에서 React 상태 및 생명주기 기능을 "연결"할 수 있는 함수입니다.
즉, 갈고리를 걸다. 라는 의미에서 "Hooks"라는 용어를 사용하고 있는 것이다.
훅이 해결하고자 하는 문제
말이 와닿지 않을 수 있는데, 훅이 어떤 문제를 해결하고자 했는지 살펴보면 도움이 될 것이라 생각한다.
리액트 훅은 클래스형에서 함수형으로 리액트 작성의 페러다임이 넘어가는 과도기에 등장했다.
기존의 클래스 문법에서는 리액트 생명주기와 관련된 다음의 기능을 갖고 있었다.
리액트 컴포넌트와 관련해서 보다 세밀하게 상태에 개입할 수 있다는 장점이 있지만, 이러한 장점은 동시에 단점이 되기도 했다.
- 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다.
- 복잡한 컴포넌트들은 이해하기 어렵다.
- 클래스는 사람과 기계를 혼동시킨다.
이 외에도, 복잡한 생명 주기로 인해 불필요한 코드가 많아진다는 문제도 있었다.
리액트의 개발 철학에서 언급했듯이, 리액트 팀은 이러한 문제를 해결하고자 훅을 도입했다.
코드를 통해서 살펴보자
코드를 바라본 문제 - 1
동기에서 선보인 Demo 코드를 바탕으로 문제를 살펴보자.
여기서는 클래스형과 함수형으로 사용할 때의 차이점, 그리고 훅이 어떻게 사용되는지를 볼 것이다.
- Code
- 결과
import React from "react";
import Row from "./Row";
export default function Greeting(props) {
return (
<section>
<Row label="Name">{props.name}</Row>
</section>
);
}
이 코드를 바탕으로 설명할 예정이다.
여기서의 목표는 "Mary"라는 이름을 내가 직접 화면 상의 입력으로 바꿔주는 것이다.
지금과 같이 함수형 컴포넌트가 보편화 되지 않았고, class
가 대부분이던 시기에는 다음과 같이 코드를 작성했다.
- Code
- 결과
import React from "react";
import Row from "./Row";
export default class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "Mary",
};
this.handleNameChange = this.handleNameChange.bind(this);
}
handleNameChange = (e) => {
this.setState({ name: e.target.value });
};
render() {
return (
<section>
<Row label="Name">
<input value={this.state.name} onChange={this.handleNameChange} />
</Row>
</section>
);
}
}
만약, 이 상황에서 class
를 사용하지 않고, state
사용을 원한다면 어떻게 할까?
우리는 너무나도 당연하게
useState
를 이야기하겠지만, 잊지말자. 이건useState
가 등장하기 전, 혹은 그 순간의 이야기이다.
- 함수형 Code
- 클래스형 Code
- 결과
import React, { useState } from "react";
import Row from "./Row";
export default function Greeting(props) {
const [name, setName] = useState("Mary");
function handleNameChange(e) {
setName(e.target.value);
}
return (
<section>
<Row label="Name">
<input value={name} onChange={handleNameChange} />
</Row>
</section>
);
}
import React from "react";
import Row from "./Row";
export default class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "Mary",
};
this.handleNameChange = this.handleNameChange.bind(this);
}
handleNameChange = (e) => {
this.setState({ name: e.target.value });
};
render() {
return (
<section>
<Row label="Name">
<input value={this.state.name} onChange={this.handleNameChange} />
</Row>
</section>
);
}
}
같은 기능을 함수형 코드로 작성을 했다. 이때, useState
라는 hook
을 사용했다.
비교를 위해서 Tab에 클래스형 코드도 함께 작성했다. 두 가지 접근을 비교하자.
클래스형 함수의 경우 다음과 같은 특징이 있다.
state
가 객체로 선언된다. 그리고 관련 요소들은 객체의 속성으로 존재한다.eventHandler
의 경우state
접근을 위해this
에 바인드가 되어야 한다.- 상태의 값에 접근하기 위해서는
this.state.name
과 같이 사용해야 한다.
반면 hooks
에 기반한 함수형을 사용할 경우 복잡한 과정이 필요 없다.
useState
를 통해 state
를 선언하고, setState
를 통해 상태를 변경할 수 있다.
A Hook is a function provided by react that lets you hook into react features from your function component.
초반에 훅(Hooks)의 어원에서 언급했듯이, 훅은 리액트의 기능을 함수형 컴포넌트에서 사용할 수 있게 해주는 함수이다.
useState
역시 리액트가 제공하는 함수라고 보면 된다.
이제, surname
이라는 항목이 추가되었다고 생각해보자.
- 클래스형 Code
- 함수형 Code
- 결과
import React from "react";
import Row from "./Row";
export default class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "Mary",
surname: "Poppins",
};
this.handleNameChange = this.handleNameChange.bind(this);
this.handleSurnameChange = this.handleSurnameChange.bind(this);
}
handleNameChange = (e) => {
this.setState({ name: e.target.value });
};
handleSurnameChange = (e) => {
this.setState({ name: e.target.value });
};
render() {
return (
<section>
<Row label="Name">
<input value={this.state.name} onChange={this.handleNameChange} />
</Row>
<Row label="Surname">
<input
value={this.state.surname}
onChange={this.handleSurnameChange}
/>
</Row>
</section>
);
}
}
import React, { useState } from "react";
import Row from "./Row";
export default function Greeting(props) {
const [name, setName] = useState("Mary");
const [surname, setSurname] = useState("Poppins");
function handleNameChange(e) {
setName(e.target.value);
}
function handleSurnameChange(e) {
setSurname(e.target.value);
}
return (
<section>
<Row label="Name">
<input value={name} onChange={handleNameChange} />
</Row>
<Row label="Surname">
<input value={name} onChange={handleSurnameChange} />
</Row>
</section>
);
}
DX적으로 함수형이 좀 더 간편하게 수정이 되는 것이 느껴지는가?
여기서, 클래스형의 가장 큰 문제는 관심사가 완전히 분리되지 않는다는 것이다.
state
객체 안에 name
과 surname
이라는 두 가지 상태가 섞여있는 것이다.
코드로 바라본 문제 - 2
이번엔 context
로 살펴볼 것이다.
context, it's like kind of like global variables for a subtree so it's useful for things like read the current theme like visual theme or current language that the user is using and it's useful to avoid passing everything through props if you need all components to be able to read some value.
영상에 나온 내용으로, subtree
에 대한 전역 변수로 쉽게 표현이 가능하다.
다른말로 전역상태로도 표현할 수 있다.
- 클래스형 Code
- 함수형 Code
- 클래스형 결과
- 함수형 결과
import React from "react";
import Row from "./Row";
import { ThemeContext, LocaleContext } from "./context";
export default class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "Mary",
surname: "Poppins",
};
this.handleNameChange = this.handleNameChange.bind(this);
this.handleSurnameChange = this.handleSurnameChange.bind(this);
}
handleNameChange = (e) => {
this.setState({ name: e.target.value });
};
handleSurnameChange = (e) => {
this.setState({ name: e.target.value });
};
render() {
return (
<ThemeContext.Consumer>
{(theme) => (
<section className={theme}>
<Row label="Name">
<input value={this.state.name} onChange={this.handleNameChange} />
</Row>
<Row label="Surname">
<input
value={this.state.surname}
onChange={this.handleSurnameChange}
/>
</Row>
</section>
)}
<LocaleContext.Consumer>
{(locale) => <Row label="Language">{locale}</Row>}
</LocaleContext.Consumer>
</ThemeContext.Consumer>
);
}
}
import React, { useState, useContext } from "react";
import Row from "./Row";
import { ThemeContext, LocaleContext } from "./context";
export default function Greeting(props) {
const [name, setName] = useState("Mary");
const [surname, setSurname] = useState("Poppins");
const theme = useContext(ThemeContext);
const locale = useContext(LocaleContext);
function handleNameChange(e) {
setName(e.target.value);
}
function handleSurnameChange(e) {
setSurname(e.target.value);
}
return (
<section className={theme}>
<Row label="Name">
<input value={name} onChange={handleNameChange} />
</Row>
<Row label="Surname">
<input value={name} onChange={handleSurnameChange} />
</Row>
<Row label="Language">{locale}</Row>
</section>
);
}