[React] Hooks 정리

React Hooks는 리액트에서 새로 도입된 기능으로 class형 컴포넌트가 아닌 함수형 컴포넌트에서도 상태를 관리할 수 있도록 업데이트 되었다.

뿐만아니라 렌더링 직후 작업을 하는 componentDidMount와 같은 기능을 함수형 컴포넌트에서도 사용할 수 있게 해주었다.

즉 기존에  클래스형 컴포넌트에서만 가능하던  생명주기를 hooks를 이용하여 함수형 컴포넌트에서도 사용할 수 있게 되었다.


# useState

useState을 사용하여 함수형 컴포넌트에서도 클래스형 컴포넌트처럼 상태를 지닐 수 있게 되었다.

import React, { useState } from 'react'

const Counter = () => {

const [value, setValue] = useState(0);

const onPlusHandler = () => {
setValue(value + 1);

}

const onMinusHandler = () => {
setValue(value - 1);
}

return (
<div>
<p>카운터 <b>{value}</b></p>
<button onClick={onPlusHandler}>plus</button>
<button onClick={onMinusHandler}>minus</button>
</div>
)
}

export default Counter


useState을 사용하기 위해서는 useState를 import 해야 한다.

그리고 state 역할을 대신하는 다음의 코드를 보자.

const [value, setValue] = useState(0);

배열의 첫번째 값은 현재 상태를 나타내며,

배열의 두번째 값은 현재 상태의 값을 변경할때 사용한다. 

클래스형 컴포넌트에서 setState와 같다고 생각하면 된다.


useState값으로 0을 입력했는데 이곳에 작성한값은 초기 state의 값이 설정 된다.

즉 현재 value의 초기 값은 0 이다.

초기값으로 숫자, 문자, 배열, 객체 boolean 등 타입에 상관없이 초기값으로 지정할 수 있다.


만약 관리해야할 상태가 여러개라면 useState을 여러번 사용하면 된다.

하나의 useState 함수는 하나의 상태 값만을 관리할 수 있기 때문이다.

아래의 코드처럼 useState을 여러번 사용할 수 있다.

import React, { useState } from 'react';

const Info = () => {

const [name, setName] = useState('');
const [address, setAddress] = useState('');

const onChangeName = e => {
setName(e.target.value);
};

const onChangeNickname = e => {
setAddress(e.target.value);
};

return (
<div>
<div>
<input value={name} onChange={onChangeName} />
<input value={address} onChange={onChangeNickname} />
</div>
<div>
<div>
<b>이름:</b> {name}
</div>
<div>
<b>주소:</b>{address}
</div>
</div>
</div>
)
}

export default Info


위 코드에서처럼 공백 문자열을 초기값으로도 설정할 수 있다.

* 추가적으로 input 에 값을 입력했을때 입력값을 정상적으로 받기 위해서는 input 태그의 속성으로 value와 onchange는 세트로 작성해야한다.



# useEffect

useEffect는 컴포넌트가 렌더링 될 때마다 특정 작업을 수행할 수 있도록 설정할 수 있는 함수이다.

클래스형 컴포넌트의 componentDidMount 와 componentDidUpdate 가 합쳐진 방식이라고 볼 수 있다.


import React, { useEffect, useState } from 'react';

const Info = () => {

const [name, setName] = useState('');
const [address, setAddress] = useState('');

useEffect(() => {
console.log('useEffect');
console.log({
name,
address
});

})

const onChangeName = e => {
setName(e.target.value);
};

const onChangeNickname = e => {
setAddress(e.target.value);
};

return (
<div>
<div>
<input value={name} onChange={onChangeName} />
<input value={address} onChange={onChangeNickname} />
</div>
<div>
<div>
<b>이름:</b> {name}
</div>
<div>
<b>주소:</b>{address}
</div>
</div>
</div>
)
}

export default Info



 위 코드를 실행해보면 input 창에 값을 입력할 때마다 값이 입력되는 input창이 리랜더링 되는것을 확인 할 수 있다. 






이러한 리랜더링을 막고 컴포넌트가 화면에 처음 랜더링 된 후에 한번만 실행되고 더이상 실행되지 않게 하려면 아래처럼 useEffect 힘수의 두번째 인자로 [] 빈 배열을 넣어주면 된다.

useEffect(() => {
console.log('useEffect');
console.log({
name,
address
});

},[])

이렇게 입력하면 더이상 input 창의 값이 변경될 때마다 리랜더링 되는 현상은 없어진다.

만약 특정 값이 업데이트 될 때만 useEffect 함수를 실행하고 싶다면 빈 배열안에 해당 상태값을 넣어주면 된다.

const Info = () => {

const [name, setName] = useState('');
const [address, setAddress] = useState('');

useEffect(() => {
console.log('useEffect');
console.log({
name,
address
});

},[name])

// 아래부분 생략

위 코드의 경우 name의 상태값이 변경될때마다 useEffect 함수가 실행되도록 했다.

이렇게 두번째 인자로 오는 배열 안에는 useState을 통해 관리하고 있는 상태를 입력하여도 되고 props로 전달받은 값을 입력해도 된다.



추가적으로 

useEffect 함수는 일반적으로 렌더링 된 직후 실행되며 두번째 파라미터 배열에 어떠한 값을 입력하냐에 따라 실행동작이 달라진다.

만약 컴포넌트가 언마운트되기 전이나, 업데이트 되기 직전에 어떠한 작업을 수행하고자 한다면 useEffect 함수에서 또다른 함수를 반환해주어야 한다.

App.js 파일
import React, { useState } from 'react'

import Info from './components/Info';

const App = () => {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => {
setVisible(!visible);
}}>
{visible ? '숨기기' : '보이기'}
</button>
<hr />
{visible && <Info />}
</div>
);
}

export default App;



info.js 파일
import React, { useEffect, useState } from 'react';

const Info = () => {

const [name, setName] = useState('');
const [address, setAddress] = useState('');

useEffect(() => {
console.log('effect');
console.log({ name, address });
return () => {
console.log('before unmount & update');
console.log({ name, address });
};
});

const onChangeName = e => {
setName(e.target.value);
};

const onChangeNickname = e => {
setAddress(e.target.value);
};

return (
<div>
<div>
<input value={name} onChange={onChangeName} />
<input value={address} onChange={onChangeNickname} />
</div>
<div>
<div>
<b>이름:</b> {name}
</div>
<div>
<b>주소:</b>{address}
</div>
</div>
</div>
)
}

export default Info



해당 코드를 실행해 렌더링 된 화면을 보면 보이기 버튼이 있다.

해당 버튼을 클릭하면 입력받을수있는 컴포넌트가 mount되며 숨기기버튼이 생성되고, 숨기기 버튼을 누르면 컴포넌트가 unmount 된다. 


보이기 버튼을 눌러서 컴포넌트가 mount 되면 console 창에 effect 가 출력되며 



숨기기 버튼을 눌러서 컴포넌트가 unmount 되면 console 창에 before unmount & update가 출력된다.




그리고 mount된 컴포넌트에 값을 입력해보면 현재 내가 입력한 값의 이전 값을 console 창에 출력하고 있는것을 확인할 수 있다.

즉, 업데이트 되기 직전의 값을 보여주고 있다.

그러므로 값이 update 될 때마다 리랜더링 되지않고 unmount 될 때만 반환된 함수를 호출하고자 한다면  useEffect 함수의 두번째 파라미터에 빈 배열을 작성하면 된다.




# useReducer

useReducer는 useState 보다 컴포넌트에서 다양한 상황에 따라 상태값을 달리해야 할 때 사용하는 hook이다.

리듀서는 현재 상태와 업데이트에 필요한 정보를 담은 action 값을 전달받아 새로운 상태를 반환하는 함수이다. 

그리고 이러한 리듀서를 사용하기 위해서는 불변성을 지켜야지 제대로 사용할 수 있다.

Redux를 사용할때 action 객체에 type을 필수적으로 명시해주어야 하는데, useReducer에서 사용하는 action 객체에는 type을 명시할 필요가 없으며 객체가 아닌 문자열이나 숫자여도 상관이 없다.


import React, { 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' })}>plus</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>minus</button>
</div>
);
};

export default Counter;



useReducer의 첫번째 인자로 reducer 함수, 두번째 인자로 state의 초기값을 입력한다.

useReducer를 사용하게되면 state 값과  dispatch 함수를 받아오게 되는데

여기서 state는 현재 가르키고 있는 상태이고

dispatch는 액션을 발생시키는 함수이다.

dispatch(action) 과 같은 형태로 함수안에 인자로 액션갑슬 넣어주면 리듀서 함수가 호출되는 방식이다.

useReducer를 사용하면 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다는 장점이 있다.

위 코드는 Counter.js 파일에 해당하는 코드이고 출력을 하기위해서는 App.js 파일에 import 해서 출력해야 한다.


추가적으로

기존에 input 태그가 여러개인경우 여러개의 useState을 작성하여 상태를 다뤘는데

useReducer를 사용하게되면 input 태그에 name 속성을 작성하여 e.target.name 을 사용하여 state 값을  다룰 수 있다.

import React, { useReducer } from 'react';

const reducer = (state, action) => {
// console.log('state 값은 => ', state);
// console.log('action 값은 =>', action);

return {
...state,
[action.name]: action.value
};
}

const Info = () => {

const [state, dispatch] = useReducer(reducer, { name: '', address: '' });
const { name, address } = state;

const onChange = (e) => {
dispatch(e.target);
}

return (
<div>
<div>
<input name='name' value={name} onChange={onChange} />
<input name='address' value={address} onChange={onChange} />
</div>
<div>
<div>
<b>이름:</b> {name}
</div>
<div>
<b>주소:</b>{address}
</div>
</div>
</div>
)
}

export default Info

useReducer 에서 action 값은 어떤 값이어도 상관없다. 

그렇기 때문에 위의 코드에서 처럼 e.target 값 자체를 action 값으로도 사용할 수 있다.

이렇게 작성하게되면 아무리 많은 input 태그가 생기더라도 코드를 간결하고 깔끔하게 유지할 수 있다.


++ Redux는 state들이 동기적으로 변경되지만 useReducer는 state들이 비동기적으로 변경된다.


# useMemo

useMemo를 사용하여 컴포넌트의 연산을 최적화 할 수 있다. 

만약 입력한 숫자의 평균값을 구하는 컴포넌트를 만들었는데 결과값을 출력할때 뿐만 아니라 

입력을 받는 input 태그에 값을 입력할때마다 리랜더링이 발생한다고하면 메모리 낭비가 발생하게 되는데

이때 useMemo 를 사용하여 이러한 문제점을 해결하고 최적화를 할 수 있다.


useMemo는 랜더링 하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고 

만약에 원하는 값이 바뀐것이 아니라면 이전에 연산했던 결과를 다시 사용하는 방식이다.

import React, { 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;


# useCallback

useMemo와 비슷한 함수이며 주로 렌더링 성능을 최적화 해야할 때 사용한다.

useCallback을 사용하면 이벤트 핸들러 함수를 필요할 때만 생성 할 수 있다.

useMemo 에서의 예시를 보면 onChange와 onInsert 라는 함수를 선언했다. 

이처럼 이벤트 함수를 선언하면 컴포넌트가 리렌더링 될 때마다 이 함수들이 새로 생기게 되면서 메모리 낭비가 발생하게 된다.

대부분의 경우에는 문제가 없을 수 있으나 렌더링해야 될 컴포넌트의 갯수가 많다던가 또는 컴포넌트의 렌더링이 자주 발생하게 되면 useCallback을 사용하여 최적화 하는것이 좋다.

import React, { 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((e) => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber('');
}, [list, number]);

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 의 첫번째 인자로는 우리가 생성한 함수를 넣어주고, 두번째 인자로 배열을 넣어준다. 

onChange 함수처럼 빈배열을 두번째 인자로 넣으면 위에서 작성한 useEffect 에서처럼 컴포넌트가 랜더링될 때 한번만 함수가 생성되며

 onInsert 함수처럼 useCallback의 두번째 인자로 [list, number]를 입렵하면 list와 number의 상태값이 변경될 때 마다 함수가 ㅅ생성된다.

함수 내부에서 기존의 상태값을 필요로할 때 즉, 불변성을 유지해야할 때는 꼭 두번째 인자로오는 배열안에 포함시켜 주어야 한다.

위의 코드를 예시로 설명하면 onChange 함수의 경우  두번째 인자로 빈 배열을 입력하여도 문제가 없지만 

onInsert 함수에서 처럼 기존의 list 배열을 가져와 불변성을 유지하면서 새로운 nextlist 배열을 반환하기 때문에  두번째 인자로 오는 배열안에 list 와 number을 꼭 작성해주어야 원하는 방식대로 동작한다.


useCallback과 useMemo의 차이점은 useCallback은 함수를 반환하는 상황에서 더 편리하게 사용할 수 있는 hook 이다.

그러므로 숫자, 문자열, 객체 등과 같은 값을 재사용하기 위해서는 useMemo를 사용하는것이 좋으며

함수를 재사용하기 위해서는 useCallback을 사용하는것이 좋다.



#useRef


useRef는 특정 요소의 크기를 가져오거나, 포커스를 설정하거나, DOM을 선택해야할 때 사용한다.

import React, { useState, useRef, useCallback } from 'react';


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(
e => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber('');
inputEl.current.focus();

},
[number, 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>
);
};

export default Average;


위의 예시처럼 useRef를 사용하면 입력 버튼을 눌러 값을 목록에 추가하고나서 자동적으로 input 태그에 focus 되어 다시 입력부분을 클릭하지않아도 바로 값을 입력할 수 있도록 할 수 있다.

useRef를 사용하여 ref 속성을 설정하면 useRef를 통해 만든 객체안의 current는 ref 속성을 작성한 실제 엘리먼트를 가리키게 된다.







댓글