Counter.js
import { useState } from "react";
const Counter = () => {
const [value, setValue] = useState(0);
return (
<div>
<p>
현재 카운터 값은 <b>{value}</b>입니다.
</p>
<button onClick={() => setValue(value + 1)}>+1</button>
<button onClick={() => setValue(value - 1)}>-1</button>
</div>
);
};
export default Counter;
useState는 코드 상단에서 import 구문을 통해 불러오고, 다음과 같이 사용합니다.
const [value, setValue] = useState(0);
useState 함수의 파라미터에는 상태의 기본값을 넣어줍니다.
하나의 useState 함수는 하나의 상태값만 관리할 수 있습니다. 컴포넌트에서 관리해야할 상태가 여러개라면 useState를 여러번 사용하면 됩니다.
Info.js
import { useState } from "react";
const Info = () => {
const [name, setName] = useState("");
const [nickname, setNickname] = useState("");
const onChangeName = (e) => {
setName(e.target.value);
};
const onChangeNickname = (e) => {
setNickname(e.target.value);
};
return (
<div>
<div>
<input value={name} onChange={onChangeName} />
<input value={nickname} onChange={onChangeNickname} />
</div>
<div>
<div>
<b>이름:</b> {name}
</div>
<div>
<b>닉네임:</b> {nickname}
</div>
</div>
</div>
);
};
export default Info;
Info.js
import { useState, useEffect } from "react";
const Info = () => {
const [name, setName] = useState("");
const [nickname, setNickname] = useState("");
useEffect(() => {
console.log("렌더링이 완료되었습니다.");
console.log(name, nickname);
});
const onChangeName = (e) => {
setName(e.target.value);
};
const onChangeNickname = (e) => {
setNickname(e.target.value);
};
return (
<div>
<div>
<input value={name} onChange={onChangeName} />
<input value={nickname} onChange={onChangeNickname} />
</div>
<div>
<div>
<b>이름:</b> {name}
</div>
<div>
<b>닉네임:</b> {nickname}
</div>
</div>
</div>
);
};
export default Info;
결과물을 보면 렌더링 완료 문구가 두번 출력되는데 React.strictMode가 적용된 환경에서만 발생합니다.
useEffect는 개발환경에서 컴포넌트가 화면에 나타날 때 두번 호출된다는 것만 알아두면 됩니다.
useEffect에서 설정한 함수를 컴포넌트가 화면에 맨 처음 렌더링될 때만 실행하고, 업데이트 될 때는 실행하지 않으려면 두번째 파라미터로 비어있는 배열을 넣어주면됩니다.
Info.js
import { useState, useEffect } from "react";
const Info = () => {
const [name, setName] = useState("");
const [nickname, setNickname] = useState("");
useEffect(() => {
console.log("마운트될 때만 실행됩니다");
}, []);
const onChangeName = (e) => {
setName(e.target.value);
};
const onChangeNickname = (e) => {
setNickname(e.target.value);
};
return (
<div>
<div>
<input value={name} onChange={onChangeName} />
<input value={nickname} onChange={onChangeNickname} />
</div>
<div>
<div>
<b>이름:</b> {name}
</div>
<div>
<b>닉네임:</b> {nickname}
</div>
</div>
</div>
);
};
export default Info;
코드 작성 후, 인풋을 수정해도 컴포넌트가 처음 나타날 때만 콘솔에 문구가 나타나고 그 이후에는 나타나지 않음을 확인할 수 있습니다.앞서 설명했듯이 useEffect가 두번 호츨이 되는 부분은 개발환경에서만 발생하는 현상입니다.
클래스형 컴포넌트로 특정값이 변경될 때만 호출하고 싶은 경우는 다음과 같이 작성합니다.
componentDidUpdate(prevProps, prevState){
if(prevProps.value !== this.props.value){
doSomething();
}
}
useEffect로 특정값이 변경될 때만 호출하고 싶을땐, useEffect의 두번째 파라미터로 전달되는 배열안에 검사하고 싶은 값을 넣어주면 됩니다.
Info.js -useEffect
useEffect(() => {
console.log(name);
}, [name]);
배열 안에는 useState를 통해 관리하고 있는 상태를 넣어주어도 되고, props로 전달받은값을 넣어주어도 됩니다.
useEffect는 기본적으로 렌더링되고 난 직후마다 실행되며, 두번째 파라미터 배열에 무엇을 넣는지에 따라 실행되는 조건이 달라집니다.
컴포넌트가 언마운트되기 전이나 업데이트 직전에 어떠한 작업을 수행하고 싶다면 useEffect에서 뒷정리 함수를 반환해 주어야 합니다.
Info.js - useEffect
useEffect(() => {
console.log("Effect");
console.log(name);
return () => {
console.log("Cleanup");
console.log(name);
};
}, [name]);
App.js
import { useState } from "react";
import Info from "./Info";
const App = () => {
const [visible, setVisible] = useState(false);
return (
<div>
<button
onClick={() => {
setVisible(!visible);
}}
>
{visible ? "숨기기" : "보이기"}
</button>
<hr />
{visible && <Info />}
</div>
);
};
export default App;
React.strictMode가 활성화 되어 있기 때문에, 보이기 버튼 클릭 시 컴포넌트가 두번 마운트되면서 effect, cleanup, effect가 차례대로 호출됩니다. 그 다음 숨기기를 누르면 컴포넌트가 언마운트되면서 cleanup이 호츨됩니다.
인풋박스에 이름을 적어보면 렌더링될 때마다 뒷정리 함수가 계속 나타나는 것을 확인할 수 있습니다. 그리고 뒷정리 함수가 호출될 때는 업데이트되기 직전의 값을 보여줍니다.
오직 언마운트될 때만 뒷정리 함수를 호출하고 싶다면 useEffect함수의 두번쨰 파라미터에 비어있는 배열을 넣으면 됩니다.
useEffect(()=>{
console.log('effect');
return()=>{
console.log('unmount');
}
},[]);
useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트해 주고 싶을때 사용하는 Hook입니다.
리듀서(reducer)는 현재상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션(action)값을 전달받아 새로운 상태를 반환하는 함수입니다. 리듀서 함수에서 새로운 상태를 만들때는 반드시 불변성을 지켜주어야 합니다.
function reducer(state,action){
return{...}; //불변성을 지키면서 업데이트한 새로운 상태를 반환합니다.
}
액션값은 주로 다음과 같은 형태로 이루어져 있습니다.
{
type:"INCREMENT"
//다른 값들이 필요하다면 추가로 들어감
}
Counter.js
import { useState, useReducer } from "react";
function reducer(state, action) {
//action.type에 따라 다른 작업 수행
switch (action.type) {
case "INCREMENT":
return { value: state.value + 1 };
case "DECREMENT":
return { value: state.value - 1 };
default:
//아무것도 해당되지 않을때 기존 상태 반환
return state;
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, { value: 0 });
return (
<div>
<p>
현재 카운터 값은 <b>{state.value}</b>입니다.
</p>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button>
<button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button>
</div>
);
};
export default Counter;
useReducer의 첫번째 파라미터에는 리듀서 함수를 넣고, 두번째 파라미터에는 해당 리듀서의 기본값을 넣어줍니다. 이 Hook을 사용하면 state값과 dispatch 함수를 받아옵니다.
state는 현재 가리키고 있는 상태, dispatch는 액션을 발생시키는 함수입니다.
useReducer를 사용했을 때 가장 큰 장점은 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다는 것입니다.
기존엔 인풋이 여러개일때 useState를 여러번 사용했습니다.
useReducer를 사용하여 여러개의 인풋 상태를 관리해보겠습니다.
Info.js
import { useReducer } from "react";
function reducer(state, action) {
return {
...state,
[action.name]: action.value,
};
}
const Info = () => {
const [state, dispatch] = useReducer(reducer, {
name: "",
nickname: "",
});
const { name, nickname } = state;
const onChange = (e) => {
dispatch(e.target);
};
return (
<div>
<div>
<input name="name" value={name} onChange={onChange} />
<input name="nickname" value={nickname} onChange={onChange} />
</div>
<div>
<div>
<b>이름:</b> {name}
</div>
<div>
<b>닉네임:</b> {nickname}
</div>
</div>
</div>
);
};
export default Info;
useReducer의 액션은 그 어떤 값도 사용가능합니다.
그래서 이번에는 이벤트 객체가 지니고 있는 e.target value 자체를 액션값으로 사용했습니다.
이렇게 관리하면 인풋의 개수가 많아져도 코드를 짧고 깔끔하게 유지할 수 있습니다.
useMemo를 사용하면 함수 컴포넌트 내부에서 발생하는 연산을 최적화할 수 있습니다.
먼저, 리스트에 숫자를 추가하면 추가된 숫자들의 평균을 보여주는 함수 컴포넌트를 작성해 봅시다.
Average.js
import { useState } from "react";
const getAverage = (numbers) => {
console.log("평균값 계산 중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a, b) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState("");
const onChange = (e) => {
setNumber(e.target.value);
};
const onInsert = (e) => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber("");
};
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {getAverage(list)}
</div>
</div>
);
};
export default Average;
해당코드를 실행하면 숫자를 등록할 때 뿐만 아니라 인풋내용이 수정될 때도 getAverage함수가 호출됩니다.
이러한 낭비를 최적화하기 위해 useMemo Hook을 사용합니다.
Average.js
import { useState, useMemo } from "react";
const getAverage = (numbers) => {
console.log("평균값 계산 중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a, b) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState("");
const onChange = (e) => {
setNumber(e.target.value);
};
const onInsert = (e) => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber("");
};
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {avg}
</div>
</div>
);
};
export default Average;
이제 list 배열의 내용이 바뀔 때만 getAverage 함수가 호출됩니다.
useCallback은 useMemo와 상당히 비슷한 함수입니다. 주로 렌더링 성능을 최적화해야하는 상황에서 사용하는데, 이 Hook을 사용하면 만들어놨던 함수를 재사용할 수 있습니다.
Average.js
import { useState, useMemo, useCallback } from "react";
const getAverage = (numbers) => {
console.log("평균값 계산 중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a, b) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState("");
const onChange = useCallback((e) => {
setNumber(e.target.value);
}, []); //컴포넌트가 처음 렌더링될 때만 함수 생성
const onInsert = useCallback(() => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber("");
}, [number, list]); //number혹은 list가 바뀌었을 때만 함수 생성
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {avg}
</div>
</div>
);
};
export default Average;
useCallback의 첫번재 파라미터에는 생성하고 싶은 함수를 넣고, 두번재 파라미터에는 배열을 넣으면됩니다.
이 배열에는 어떤 값이 바뀌었을때 함수를 새로 생성해야하는지 명시해야합니다.
useRef Hook은 함수 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해줍니다.
Average 컴포넌트에서 등록 버튼 클릭 시, 포커스가 인풋으로 넘어가도록 작성해보겠습니다.
Average.js
import { useState, useMemo, useCallback, useRef } from "react";
const getAverage = (numbers) => {
console.log("평균값 계산 중..");
if (numbers.length === 0) return 0;
const sum = numbers.reduce((a, b) => a + b);
return sum / numbers.length;
};
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState("");
const inputEl = useRef(null);
const onChange = useCallback((e) => {
setNumber(e.target.value);
}, []); //컴포넌트가 처음 렌더링될 때만 함수 생성
const onInsert = useCallback(() => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber("");
inputEl.current.focus();
}, [number, list]); //number혹은 list가 바뀌었을 때만 함수 생성
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} ref={inputEl} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {avg}
</div>
</div>
);
};
export default Average;
useRef를 사용하여 ref를 설정하면 useRef를 통해 만든 객체안의 current값이 실제 엘리먼트를 가리킵니다.
로컬변수란 렌더링과 상관없이 바뀔 수 있는 값을 의미합니다.
클래스형 컴포넌트 예시
import {Component} from 'react';
class MyComponent extends Component{
id = 1
setId = (n) => {
this.id = n;
}
printId = () => {
console.log(this.id);
}
render(){
return(
<div>MyComponent</div>
)
}
}
export default MyComponent
함수형 컴포넌트 예시
import {useRef} from 'react'
const RefSample = () => {
const id = useRef(1);
const setId = (n) => {
id.current = n;
}
const printId = () => {
console.log(id.current);
}
return(
<div>refsample</div>
)
}
export default RefSample;
여러 컴포넌트에서 비슷한 기능을 공유할 경우, 여러분만의 Hook로 작성하여 로직을 재사용할 수 있습니다.
리액트를 다루는 기술 [개정판] (김민준, 길벗출판사) 책을 참고하였습니다.