React 실습 - Class vs. Hook
React의 기초를 다지는 데 가장 좋은 방법은 공식 튜토리얼을 읽고 따라하는 것인데요,
자바스크립트로 개발해 본 적이 있다면, 맞닥뜨리는 질문 중에 하나가 Hook일 것입니다.
Hook은 뭐지? 그냥 자바스크립트로 짜면 안 되나?
정답은 "됩니다"입니다. Hook은 안 써도 됩니다.
Hook은 자바스크립트 클래스를 대체하기 위해 React 16.8 버전부터 추가된 문법인데요,
다만, 저처럼 클래스를 선호하는 객체지향사람이라도
일부러라도 Hook 문법을 이해하고 알아두는 것이 코드 분석 차원에서도 좋을 것 같습니다.
오늘은 클래스와 Hook의 차이점과 간단한 실습 예제로 문법을 비교해 보겠습니다.
Hook의 기원
대개 프로그래밍 방식은 함수형(functional programming)과 객체지향 클래스형(OOP)의 2가지 스타일로 구분하는데요,말 그대로 함수형은 뚜렷한 목적을 지닌 함수들을 호출해가며 코딩하는 방식이고 객체지향 클래스형은 추상화(캡슐화)된 데이터 클래스를 바탕으로 생성된 객체들 간의 상호작용을 통해 직관적으로 코딩하는 방식입니다.
사실 둘 중에 뭐가 더 좋으냐는 인터넷에 수많은 글들이 있지만(OOP vs. functional programming) 생각해 보면 결국 개인 취향과 요구사항에 따라 그때 그때 적절히 선택하는 것이 옳은 것 같습니다.
특히 React에서는 상속에 대해 다음과 같이 비관적인 어조를 취하는데요,
Facebook에서는 수천 개의 React 컴포넌트를 사용하지만, 컴포넌트를 상속 계층 구조로 작성을 권장할만한 사례를 아직 찾지 못했습니다.
출처: https://ko.reactjs.org/docs/composition-vs-inheritance.html
같은 맥락에서 클래스를 대체할 수 있는 Hook을 발표했습니다.
Class vs. Hook
Hook을 소개하는 공식 페이지에서도 클래스에 대한 문제점을 장황하게 늘어놓지만, 개인적으로 명확히 이해되지는 않습니다. 특히 첫 줄부터 '클래스를 사용하면 재사용성과 코드 구성을 어렵게 만들고 러닝커브가 큰 단점이 있다'고 비판하는데, 저는 어려서부터 객체지향을 해왔기 때문에 옹호하는 쪽으로 선입견을 갖고 있죠.
다만 요즘 핫한 마이크로서비스 아키텍처(웹 페이지 혹은 웹앱의 수많은 컴포넌트들이 마이크로하게 독립적으로 운영되도록 설계) 관점에서 React 컴포넌트를 떠올린다면, 단순한 함수형 컴포넌트에 props와 state를 활용해서 간결하게 코딩하겠다는 의도는 이해할 수도 있을 것 같습니다.
그럼에도 가끔 Hook으로 코딩하다 보면 직관적으로 이해가 안 가는 대목 때문에 클래스로 회귀(?)하곤 합니다. 게다가 React 라이브러리를 만들어주는 create-react-library 패키지에서는 아직 Hook 문법을 지원하지 않습니다. (2021년 3월 기준) cf. React 앱 템플릿을 만들어주는 create-react-app과 헷갈리지 마세요.
어쨌거나 React 공식 홈에서 Hook을 밀고 있으니, 코드 레벨에서 Class와 Hook을 비교해볼까요?
예제 코드: Sample Code
소스 위치: src/tutorial/Tutorial04-ClassHook.js src/subcontents/Tutorial04-sub-Hook.js
import React from 'react';
import * as THREE from 'three';
function Viewer() {
const refDiv = React.useRef(null);
React.useEffect(() => {
const width = window.innerWidth - 1;
const height = window.innerHeight - 1;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, width / height, 1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
refDiv.current.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
const animate = () => {
renderer.render(scene, camera);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
requestAnimationFrame(animate);
}
animate();
}, []);
return (
<div ref={refDiv} style={{ width: '100%', height: '100%', border: '1px solid red' }} />
);
}
export default Viewer;
당연히 결과는 같겠죠?
원래 클래스 코드와 비교해보면,
- 생성자 constructor()가 없어지고 render() 함수가 사라졌어요.
- componentDidMount() 대신 React.useEffect( ... , []) 구문으로 바꼈어요.
- 엘리먼트를 참조하던 refDiv 구문이 React.useRef() / refDiv.current 구문으로 바꼈네요.
나머진 같네요. 1번과 3번은 문법적인 변화라 금방 이해가 되실 건데요, 2번의 경우는 생성주기에 대한 내용이라 정확히 알아두시는 것이 좋습니다. 생성주기에 대해서는 공식 페이지를 참고해 보시는 것이 좋을 것 같습니다.