React 16.8버전에서 클래스형 컴포넌트만을 사용할 때 부딪히는 수많은 문제들을 해결하기 위해서 나왔다.
컴포넌트 상태 로직 재활용 어려움
ㄴ 클래스 컴포넌트 만이 state을 저장할 수 있다 보니, 클래스를 주로 사용.
=> 관심사 분리가 제대로 되지 않고 컴포넌트 간의 중복이 상당히 많아져 규모가 큰 컴포넌트가 만들어짐
=> 유지보수가 상당히 어려워지고 테스팅 또한 어려워짐
=> 이를 해결하고자 HOC(Higher Order Component)를 사용
클래스는 혼란을 야기
ㄴ 클래스 컴포넌트의 방식이 너무 복잡하고 클래스의 this는 동작방식이 다양하다보니 예상치 못한 오류를 발생
그런데도 왜 클래스 컴포넌트를 사용했을까?
함수는 상태를 가지지 못한다는 문제점 때문에 클래스를 이용하고 있었다.
함수형 컴포넌트들은 리렌더링이 될때, 함수 안에 작성된 모든 코드가 재실행된다.
1번의 결과로 기존에 갖고 있던 상태를 전혀 기억할수 없게 만든다.
리액트는 useState를 통해 생성한 상태에 접근하고 유지하기 위해 Closure를 이용하여 함수형 컴포넌트 바깥에 state를 저장한다.
그래서 상태가 업데이트 될때, 이 상태들은 리액트 컴포넌트 바깥에 선언되어 있는 변수들이기 때문에 업데이트 한 후에도 이 변수들에 접근할 수 있게 된다.
함수 컴포넌트 안에서 state를 사용할 수 있다.
class의 this.state, this.setState와 동일한 기능
class ClassExample extends Component {
constructor() {
super();
this.state = {
count: 0,
};
}
increase = () => {
this.setState((prev) => ({ count: prev.count + 1}));
};
decrease = () => {
this.setState((prev) => ({ count: prev.count - 1}));
};
render() {
return (
<>
<div>{this.state.count}</div>
<button onClick={this.increase}>+</button>
<button onClick={this.decrease}>-</button>
</>
)
}
}
먼저 클래스 컴포넌트의 예시이다. state를 생성자 함수에 선언하고 increase메서드에 setState에 state를 수정하는 함수를 만들었다.
const Example = () => {
const [count,setCount] = useState(0);
const increase = () => {
setCount((prev) => prev + 1);
};
const decrease = () => {
setCount((prev) => prev - 1);
};
return (
<>
<div>{count}</div>
<button onClick={increase}>+</button>
<button onClick={decrease}>-</button>
</>
)
};
함수형 컴포넌트에서는 useState로 비교적 간편하게 생성자와 메서드를 구조분해 할당으로 배열에 할당하고 this바인딩 없이 setState를 정의했다.
함수 컴포넌트 안에서 side effect를 수행할 수 있게한다.
side effect
함수가 실행되면서 함수 외부에 존재하는 값이나 상태를 변경시키는 등의 행위
Class의 Lifecycle와 유사한 기능
class ClassExample extends Component {
constructor() {
super();
this.state = {
name: '',
};
}
componentDidMount() {
console.log('mount');
};
componentDidUpdate(prevProps, prevState) {
if (this.state.name !== prevState.name) {
console.log(`update ${this.state.name}`);
}
};
componentWillUnmount() {
console.log('unmount');
}
render() {
return <div>{this.state.name}</div>
}
}
라이프사이클은,
먼저 클래스에서는 컴포넌트를 불러올때 가장 먼저 실행되는 componentDidMount()
, 어떤 상태가 업데이트 될때마다 실행되는 componentDidUpdate()
, 컴포넌트가 없어질때 실행되는 componentWillUnmount()
가 있다.
const Example = () => {
const [name,setName] = useState('');
useEffect(()=>{
console.log('mount');
return () => {
console.log('unmount');
};
},[]);
useEffect(()=>{
console.log(`update ${name}`);
},[name]);
return <div>{name}</div>;
};
반면 useEffect 훅을 쓰면 앞선 3가지의 상속받은 라이프사이클 메서드를 간편하게 쓸수있다.
다만 규칙이 있는데,
useEffect 내부에 사용하는 상태나 props가 있다면 참조 배열에 넣어줘야한다.
만약 넣지 않으면 useEffect에 등록한 함수가 실행될 때 최신 props/상태를 가리키지 않게 된다.
또, 참조 배열 파라미터를 생략하면 컴포넌트가 리렌더링 될 때마다 호출이 된다.
이번엔 다르게, useEffect는 클래스형 컴포넌트에 있는 모든 생명주기를 표현하는게 가능할까?
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>
}
return this.props.children;
}
}
컴포넌트를 하나 만들었다. 이 컴포넌트는 하위의 컴포넌트에서 발생하는 에러를 에러 바운더리에서 캐치해서 에러UI를 표시한다. 여기서 getDerivedStateFromError(error)
와 componentDidCatch(error, errorInfo)
가 있는데, 이것은 에러가 발생할때 캐치하는 생명주기(lifecycle)이다.
위 예시처럼 에러 바운더리는 useEffect에서 표현할 수 없다.
리액트에서는 제어 컴포넌트를 이용하기 위해서 인풋의 value에 state를 주고, onChange를 이용해서 setState를 해주어야 한다. 그래서 input이 여러개라면 이러한 로직을 반복해서 사용해주어야 하는데, useInput을 이용하면 하나의 훅으로도 모든 인풋을 제어할 수 있다.
const useInput = (initialValue) => {
const [value, setValue] = useState(initialValue);
const onChange = (event) => {
const {
target: {value},
} = event;
setValue(value);
};
return {value, onChange};
};
const App = () => {
const name = useInput('');
return (
<input
placeholder={'Write here...'}
value={name.value}
onChange={name.onChange}
/>
);
};
const useInput = (initialValue,validator) => {
const [value, setValue] = useState(initialValue);
const onChange = (event) => {
const {
target: {value},
} = event;
let updateFlag = true;
if (typeof validator === 'function') {
updateFlag = validator(value);
}
updateFlag ? setValue(value) : alert('Cant enter!')
};
return {value, onChange};
};
const App = () => {
const chkWord = (value) => value.length < 5 && !value.includes('@');
const name = useInput('', chkWord);
return (
<input
placeholder={'Write here...'}
value={name.value}
onChange={name.onChange}
/>
);
};
import {useState, useEffect} from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(()=>{
const callApi = async () => {
try {
const res = await fetch(url, {
methods: 'GET',
headers: {'Content-type': 'application/json'},
});
const data = (await res.json()).data;
setData(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
}, [url]);
return {data, loading, error};
}
export default useFetch;
const Example = () => {
const {name, loading, error} = useFetch(`${API_URL_NAME}`);
return (
<>
{loading ? (
<div>로딩중...</div>
) : error ? (
<div>에러</div>
) : (
<div>{name}</div>
)}
</>
);
};