지난 포스팅에서 함수 컴포넌트와 클래스 컴포넌트를 비교하였다. 함수 컴포넌트는 코드가 간결하다는 장점이 있지만, state 관리 및 생명 주기 메서드를 사용할 수 없다는 치명적인 단점이 존재한다. 바로 이러한 단점을 개선하기 위해 사용하는 것이 Hook이다.
Hook을 사용하면, 함수 컴포넌트에서 클래스 컴포넌트의 모든 기능을 동일하게 사용할 수 있게 된다. Hook은 갈고리라는 뜻의 영어 단어인데, 이는 원하는 기능에 갈고리를 걸어 원하는 시점에 실행될 수 있도록 만든 함수라는 의미이다. Hook의 이름 앞에는 use를 붙여 명명하는 것이 관례이다.
가장 대표적인 Hook으로, useState()가 있다. useState() 함수는 말 그대로, state를 사용하기 위한 Hook이다. 함수 컴포넌트에서는 state 관리 기능이 제공되지 않기 때문에 함수 컴포넌트에서 state를 사용하고 싶다면, useState 훅을 사용해야 한다. 아래의 예시를 보자.
/** Counter.jsx **/
import React from "react";
function Counter(props) {
var count = 0;
return(
<div>
<p>총 {count}번 클릭했습니다.</p>
<button onClick={() => count++}>클릭</button>
</div>
);
}
export default Counter;
/** index.js **/
import Counter from './first_proj/Counter';
...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Counter />
</React.StrictMode>
);
...
위와 같이 코드를 작성 후 리액트 앱을 실행시키면, 아무리 클릭해도 클릭 횟수가 늘어나지 않는다.
사실 count 변수가 1씩 증가하고는 있지만, 화면에 리렌더링이 안 되기 때문에 마치 count가 안 올라가는 것처럼 보이는 것이다. 리렌더링을 하기 위해선 state가 변경되어야 하기 때문에 useState 훅을 사용해야 한다. 먼저 useState() 메서드의 형식은 아래와 같다.
import React, {useState} from "react";
const [변수명, setter 메서드명] = useState(초기 값);
위 코드가 정상적으로 동작하게 하기 위해 수정한 코드는 아래와 같다.
import React, {useState} from "react";
function Counter(props) {
const [count, setCount] = useState(0);
return(
<div>
<p>총 {count}번 클릭했습니다.</p>
<button onClick={() => setCount(count + 1)}>클릭</button>
</div>
);
}
export default Counter;
이제부터 React는 State 변화를 감지하여 화면을 리렌더링하게 된다.
useState와 더불어 자주 사용되는 Hook 중 하나로, useEffect Hook이 있다. useEffect 훅은 Side Effect를 수행하기 위한 목적으로 사용된다. Side Effect는 어떤 함수가 input이 아닌 값을 조작할 때 발생한다. 함수가 input이 아닌 다른 요소를 조작하는 상황은 생각보다 빈번하게 발생한다. 아래의 코드를 보자.
let count = 0;
function toggle(name) {
conunt += 1; // Side Effect
return `${name}님 입장`;
}
위 코드에선 현재 함수 외부에 선언되어 있는 count 변수의 값을 조작하고 있다. 즉, Side Effect는 어떤 함수가 함수 외부에 영향력을 행사하는 상황을 가리키는 것이다. 이러한 Side Effect의 특성상, 잠재적인 위험성이 어느 정도 내포될 수 있지만, 그렇다고 해서 Side Effect를 무작정 억제하는 방식도 좋은 방법은 아니다. 따라서, 상황에 맞게 Side Effect를 활용하되, 주의 깊게 사용하는 것이 중요하다.
useEffect는 두 개의 입력인자를 받는다. 첫번째 인자는 필수 값으로 콜백 메서드이며, 두번째 인자는 선택 값으로 의존성 배열이다. 의존성 배열을 전달할 경우, 해당 의존성 배열이 변경될 때마다 useEffect의 콜백 메서드가 실행된다.
먼저 useEffect를 사용하기 위해선 아래의 import 구문을 추가해야 한다.
import React, { useRef } from 'react';
이제 useEffect의 사용법에 대해 알아보자. useEffect 훅은 아래의 3가지 형태로 사용될 수 있다.
① 렌더링될 때마다 반복
useEffect(() => {
console.log("Hi"); // 렌더링될 때마다 Hi를 출력
})
② 첫번째 렌더링에서만 실행
useEffect(() => {
console.log("Hi"); // 렌더링될 때마다 Hi를 출력
}, [])
③ 첫번째 렌더링 및 의존성 배열의 변경을 감지할 때마다 반복
const [formData, setFormData] = useState({
emailAddress: '',
name: '',
phone: ''
});
useEffect(() => {
console.log(formData);
}, [formData]); // formData가 업데이트될 때마다 formData를 출력
※ useState와 useEffect 비교
주로 useState는 컴포넌트의 상태 데이터를 저장하기 위한 용도로 사용되고, useEffect는 새로운 값을 갱신하기 위한 용도로 사용된다. 즉, 사용자의 클릭 횟수처럼 이전 값을 저장해두어야 할 때에는 useState()를 사용하고, API 응답처럼 이전 값을 저장해두지 않아도 될 때에는 useEffect()를 사용한다.
useRef 훅은 Reference를 사용하기 위한 훅이다. 여기서 Reference란, 특정 컴포넌트에 접근할 수 있는 객체를 의미한다. useRef의 사용법은 아래와 같다.
function InputTextWithButton(props) {
// 초기 값 설정
const inputText = useRef(null);
const onBtnClick = () => {
inputText.current.focus(); // 현재 참조 중인 엘리먼트에 포커싱
};
return (
<>
<input ref={inputText} type="text" /> // 현재 참조 중인 엘리먼트
<button onClick={onBtnClick}>Focus to InputText</button>
</>
);
}
useRef를 사용하는 상황은 아래와 같다.
① 특정 DOM 요소에 접근하고자 할 때
② 데이터 저장 및 유지
React에서 이벤트를 처리하는 방법에 대해 알아보기에 앞서 DOM의 이벤트에 대해서 알아보기로 하자. DOM에서는 클릭 이벤트에 대한 트리거 함수를 onclick 속성에 문자열 형태로 전달한다.
<button onclick="activate()">
Activate
</button>
React에서는 이와 조금 다르게 Camel Case가 적용된 onClick을 사용하고, 문자열이 아닌 JavaScript 함수 그대로를 전달한다.
<button onClick={activate}>
Activate
</button>
그러면 이제부터 본격적으로 이벤트 리스너를 등록하는 방법에 대해 알아보자.
import React, {useState} from "react";
function Toggle(props) {
const [isToggleOn, setIsToggleOn] = useState(true);
function handleClick() { // 또는 const handlClick = () => {
setIsToggleOn((isToggleOn) => !isToggleOn);
}
return (
<button onClick={handleClick}>
{isToggleOn ? '켜짐' : '꺼짐'}
</button>
);
}
export default Toggle;
이벤트 핸들러에 매개변수를 전달해야 하는 상황은 빈번히 발생한다. 함수 컴포넌트에서 이벤트 핸들러에 매개변수를 전달하는 방법을 알아보자.
return (
<button onClick={(event) => handleDelete(1, event)}>삭제하기</button>);
}
같은 컴포넌트를 화면에 반복적으로 나타내야 할 때, JavaScript 배열의 map 함수를 사용할 수 있다. 여러 종류의 과일을 반복적으로 화면에 나타내야 하는 경우를 가정해보자. index.js에 아래의 코드를 작성한다.
...
const fruits = ['apple', 'banana', 'grapes', 'orange', 'lemon'];
// 각 과일을 리스트의 아이템으로 매핑
const listItems = fruits.map((fruit) =>
<li>{fruit}</li>
);
const root = ReactDOM.createRoot(document.getElementById('root'));
// 리스트 아이템들을 Ordered List로 나타냄
root.render(
<React.StrictMode>
<ol>{listItems}</ol>
</React.StrictMode>
);
...
리스트 아이템이 잘 렌더링된 것을 확인할 수 있다. 이제 F12 버튼을 눌러보자. Console을 확인해보면, 아래와 같은 경고 메시지가 나타나고 있을 것이다.
위 경고는 각 아이템에 key가 할당되지 않았음을 나타내는 경고 문구이다.
Unique Key가 할당되지 않았다고 해서 에러가 발생하는 것은 아니지만, map 함수를 사용할 때에는 기본적으로 Key를 할당할 것을 권장한다.
① id를 사용하는 방식
const listItems = items.map((item) =>
<li key={item.id]>
{item.name}
</li>
);
② Index를 사용하는 방식
const listItems = items.map((item, index) =>
<li key={index]>
{item.name}
</li>
);