React (4) Hooks

이종호·2022년 7월 24일
0

React

목록 보기
4/7
post-thumbnail

영상링크

Hooks란?

React 16.8버전에서 클래스형 컴포넌트만을 사용할 때 부딪히는 수많은 문제들을 해결하기 위해서 나왔다.

클래스 컴포넌트 문제점

  1. 컴포넌트 상태 로직 재활용 어려움

    ㄴ 클래스 컴포넌트 만이 state을 저장할 수 있다 보니, 클래스를 주로 사용.

    => 관심사 분리가 제대로 되지 않고 컴포넌트 간의 중복이 상당히 많아져 규모가 큰 컴포넌트가 만들어짐

    => 유지보수가 상당히 어려워지고 테스팅 또한 어려워짐

    => 이를 해결하고자 HOC(Higher Order Component)를 사용

  2. 클래스는 혼란을 야기

    ㄴ 클래스 컴포넌트의 방식이 너무 복잡하고 클래스의 this는 동작방식이 다양하다보니 예상치 못한 오류를 발생

그런데도 왜 클래스 컴포넌트를 사용했을까?
함수는 상태를 가지지 못한다는 문제점 때문에 클래스를 이용하고 있었다.

함수가 상태를 갖지 못하는 이유

  1. 함수형 컴포넌트들은 리렌더링이 될때, 함수 안에 작성된 모든 코드가 재실행된다.

  2. 1번의 결과로 기존에 갖고 있던 상태를 전혀 기억할수 없게 만든다.

Hooks를 사용하며 함수가 상태를 가질 수 있는 이유

리액트는 useState를 통해 생성한 상태에 접근하고 유지하기 위해 Closure를 이용하여 함수형 컴포넌트 바깥에 state를 저장한다.
그래서 상태가 업데이트 될때, 이 상태들은 리액트 컴포넌트 바깥에 선언되어 있는 변수들이기 때문에 업데이트 한 후에도 이 변수들에 접근할 수 있게 된다.

Hooks 종류

1. useState

함수 컴포넌트 안에서 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를 정의했다.

2. useEffect

함수 컴포넌트 안에서 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()가 있다.

  • componentDidMount()
    ㄴ 참조 배열에 빈 배열을 넣었을때와 같은 기능을한다.
  • componentDidUpdate()
    ㄴ 참조 배열에 넣은 상태를 감지해서 실행한다.
  • componentWillUnmount()
    ㄴ useEffect내의 리턴에 콜백형식으로 넣으면 컴포넌트가 없어질때 실행된다.
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에서 표현할 수 없다.

Hooks 규칙

1. 최상위에서만 Hook을 호출

  • React 함수(컴포넌트)의 최상위에서만 Hook을 호출 할 것
  • 이 규칙을 따라야 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.

2. React 함수에서만 Hook을 호출

  • Custom Hook에서는 호출 가능
  • 일반적인 Javascript 함수에서는 호출X
  • 이 규칠을 지키면 컴포넌트의 모든 상태 관련 로직을 소스코드에서 명확하게 보이도록 할 수 있다.

3. Hook을 만들 때 앞에 use 붙이기

  • 한눈에 보아도 Hook 규칙이 적용되는지를 파악할수 있기 때문.
  • 이를 따르지 않으면 특정 함수가 그 안에서 Hook을 호출하는지를 알 수 없기 때문에 Hook 규칙 위반 여부를 체크할 수 없다.

Custom Hook

  • React의 특별한 기능이라기보다 기본적으로 Hook의 디자인을 따르는 관습을 말함.
  • 주로 중복된 로직을 재활용할때 사용한다.
  • 공식문서에서는 복잡한 로직을 단순한 인터페이스 속에 숨길 수 있도록 하거나 복잡하게 엉킨 컴포넌트를 풀어내도록 도울때 사용하도록 권장한다.

useInput

리액트에서는 제어 컴포넌트를 이용하기 위해서 인풋의 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}
        />
    );
};

useFetch

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>
        	)}
        </>
    );
};
profile
Frontend

0개의 댓글