React에 hook이 나오기 전까진 리액트 라이프 사이클을 사용하기 위해서 Class를 사용해야 됐었습니다. 리액트를 Class로 사용하기 위해서 React.Component
를 expend
해야 하고 state를 사용하려면 super(props)
도 적어줘야합니다. 대부분은 이게 왜 필요한지 모르고 적고 있었습니다. 사람들은 이런 부분은 자동으로 되면 좋을 것 같다는 생각을 하게 됩니다. 그리고 Class를 사용해서 재사용 가능한 로직을 만들려면 HOC(High-Order-Component)
를 사용해서 만들어야 되는데 나중에는 HOC
헬을 겪게 되고 코드도 이해하기 힘들어집니다. state를 정의하고 업데이트 하는데도 많은 코드가 필요했습니다. 이런 것들을 해결하기 위해 react hook
이 나오게 됩니다.
💡 Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 "연동(hook into)" 할 수 있게 해주는 함수입니다.
- React 공식 문서 -
React 16.8v부터 추가된 기능으로 Class 형식의 리액트 컴포넌트에서만 할 수 있었던 state 관리 및 라이프 사이클 등을 함수형 컴포넌트에서도 사용할 수 있게 만들어주는 새로운 기능입니다.
React Hooks는 주로 Class Component로 사용되어온 React에서 느껴왔던 불편함이나 문제점들을 해결하기 위해 개발되었습니다.
React는 주로 Class Component
를 사용하고 React Hooks는 Functional Component
를 사용합니다. 둘의 차이점은 아래의 이미지와 같습니다.
React Hooks가 나오기 전에는 생명주기를 함수형 컴포넌트에서는 사용하지 못했기 때문에 함수형 컴포넌트가 더 간결하고 빠르더라도 클래스형 컴포넌트를 써왔습니다.
클래스형 리액트 컴포넌트 사용을 위해서는 extends
키워드를 사용해 React.Component
를 상속 받아야합니다. 또한 리액트 컴포넌트를 만들고 state를 사용하기 위해서는 custructor
내부 최 상단에 super(props)
를 해줘야 합니다. React.Component
에도 props
을 넘겨주기 위한 것이죠. 하지만 항상 똑같이 해야 되는 걸 우리가 할 필요가 있나?라는 의문이 들게 하는 코드입니다.
자바스크립트를 공부하다 보면 this가 나오는데 클래스를 사용하다 보면 많이 보입니다.
class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Click happened');
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
위의 코드에서 보면 알 수 있듯이 this
를 bind
해야 class관련 함수와 props에 접근할 수 있습니다. 대부분 bind
를 해줘야 하고, 하는 것과 안하는 것에 큰 퍼포먼스 차이가 없다면 그냥 자동으로 해주면 안될까하는 의견이 나옵니다.
리액트를 쓰다 보면 자주 쓰는 로직을 따로 빼내서 사용할 필요가 있는데, 그 전까지는 HOC를 사용했습니다.
HOC (Higher Order Component)란?
화면에서 재사용 가능한 로직만을 분리해서 component로 만들고, 재사용 불가능한 UI와 같은 다른 부분들은 parameter로 받아서 처리하는 방법입니다.
위의 예제는 api를 통해 user의 list를 가져와서 A 페이지와 B 페이지 모두 한테 뿌려주고 싶은 것입니다. user list를 뿌려주려면 componentDidMount
로 컴포넌트가 시작했을 때 user list
를 가져와 줘야합니다. 이 부분이 어떤 컴포넌트에서든 중복되게 사용하게 됩니다. 이러한 중복되는 부분을 없애기 위해 HOC를 사용했습니다.
user list
를 가져오는 공통적인 부분은 HOC 컴포넌트에 넣어주고,
그 HOC 컴포넌트로 각각의 컴포넌트를 감싸주면 모든 컴포넌트에 따로 인증을 위한 부분은 넣어주지 않아도 됩니다.
Hooks가 나오기 전에는 이 방법이 추천되는 방법이었습니다. 하지만 여기서도 문제가 있습니다. 바로 너무 많은 Wrapper 컴포넌트가 생길 수 있다는 겁니다.
아래의 코드처럼 Wrapper가 너무 많아지면 데이터 흐름을 파악하기가 힘들어집니다.
const HocHell = () => {
return (
<Hoc1>
<WithMousePosition>
<WithWindowSize>
<WithUserLocation>
<SomeComponent />
</WithUserLocation>
</WithWindowSize>
</WithMousePosition>
</Hoc1>
);
};
그런 것이 여러 개가 겹치다보면 HOC 헬을 경험할 수 있습니다.
만약 SomeComponent
가 마우스 포지션, window 크기, 유저 위치 등의 정보가 필요하다면 아래와 같이 HOC로 감싸줄 수 있습니다. 이런 구조는 가독성도 좋지않고 element를 찾기 힘들어집니다.
이러한 문제는 Custom React Hooks를 이용해서 해결할수가 있습니다.
Custom React Hooks을 사용해서 SomeComponent
를 render
해주면 아래와 같이 사용이 가능합니다.
const WithHook = () => {
const mousePosition = useMousePosition()
const windowSizes = useWindowSize()
const userLocation = useUserLocation()
return (
<SomeComponent
mousePosition={mousePosition}
windowSizes={windowSizes}
userLocation={useLocation}
>
)
}
클래스형 컴포넌트와 Hook이 등장하기 전 함수형 컴포넌트에서 사용하던 HOC는 마우스 포지션(WithMousePosition) 하위의 window의 크기(WithWindowSize) 하위의 유저 위치 정보(WithUserLocation) 하위에 SomeComponent가 존재합니다.
즉, 컴포넌트의 차원이 불필요하게 높아집니다.
하지만 Hook을 사용하면 상위 컴포넌트에서 해당 3가지 기능의 모듈을 불러와 SomeComponent로 전달하기 때문에 두 단계의 차원으로 해결이 가능해집니다.
(보통 Hook은 use를 붙여서 이름을 지어줍니다.)
Home
Componenet는 컴포넌트가 마운트 됐을 때, pathName
prop이 변경됐을 때 lookups
데이터를 api에서 받아와야 하는 코드를 class형으로 작성하면 아래와 같습니다.
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
lookups: null
};
// this를 사용하기 위해서 아래와 같이 바이드를 해줍니다.
this.fetchLookups = this.fetchLookups.bind(this);
}
componentDidMount() {
this.fetchLookups(this.props.pathName);
}
componentDidUpdate(prevProps) {
// pathName prop이 변경 되었을 때
if (prevProps.pathName !== this.props.pathName) {
this.fetchLookups(this.props.pathName);
}
}
fetchLookups(pathName) {
fetchApi({ url: `/lookup?url=${pathName}` }).then((lookups) => {
// lookups 데이터가 반환 되면 state업데이트
this.setState({
lookups
});
});
}
render() {
if (!this.state.lookups) return null;
return <pre>{JSON.stringify(this.state.lookups)}</pre>;
}
}
위의 코드를 Hook을 사용했을 때는 아래와 같습니다.
const HomeWithHook = ({ pathName }) => {
const [lookups, updateLookups] = useState(null);
useEffect(() => {
if (pathName) {
fetchApi({ url: `/lookup?url=${pathName}` }).then((lookups) => {
updateLookups(lookups);
});
}
// dependency를 pathName으로 해주면 맨 처음에 한번 pathName이 변경 되었을 때 또 실행
}, [pathName]);
if (!lookups) return null;
return <pre>{JSON.stringify(lookups)}</pre>;
};
훨씬 간결해지고 클래스에서 사용한 반복적이고 불필요한 느낌의 코드가 없어집니다.
왼쪽 코드와 오른쪽 코드를 보면 선명하게 코드가 간결해 진 것을 볼 수 있습니다.
그 이유는 Class Component에서는 생명주기를 이용할 때 ComponentDidMount
와 CompoenentDidUpdate
, compoenentWillUnmount
이렇게 다르게 처리해주지만 리액트 Hook을 사용할 때는 useEffect
안에서 다 처리를 해줄 수 있기 때문입니다.
💡 아래의 규칙을 지켜야 리액트가 훅의 상태를 제대로 기억할 수 있습니다.
반복문, 조건문, 중첩된 함수 내에서는 Hook을 실행할 수 없습니다.
import React, { useState } from "react"
function Hooks(props) {
if (!props.isExist) {
const [state, setState] = useState(); // Error!
}
const [state2, setState2] = useState(); // Error!
}
react가 여러 훅들을 구분할 수 있는 유일한 정보는 훅이 사용된 순서 뿐이기 때문입니다.
일반 JavaScript 함수에서는 Hook을 호출해서는 안 됩니다.
Hook을 호출할 수 있는 곳이 딱 한 군데 더 있습니다. 바로 직접 작성한 custom Hook 내입니다. 이것에 대해서는 나중에 알아보겠습니다.
이제는 클래스 컴포넌트를 사용해서 컴포넌트를 만들 이유가 많이 없어졌습니다. Hook이 써야되는 코드도 많이 줄어들게 되고 보고 읽기도 편하기 때문입니다.
🔗 참고 링크
https://doqtqu.tistory.com/340?category=804293
https://surviveasdev.tistory.com/entry/React-hook이-나온-이유와-사용해야-하는-이유
https://surviveasdev.tistory.com/entry/리액트-훅React-hook-사용-방법-예제로-알아보기?category=1205973
https://study.wecode.co.kr/session/content/196
https://ko.reactjs.org/docs/hooks-intro.html#gatsby-focus-wrapper
https://www.youtube.com/watch?v=C26vJqelKlA&feature=share