React Hooks
- React 에서 기존에 사용하던 Class를 이용한 코드를 작성할 필요 없이, state와, 여러 React 기능을 사용할 수 있도록 만든 라이브러리입니다.
- 함수 컴포넌트도 클래스 컴포넌트처럼 사용할 수 있으며 함수 컴포넌트는 클래스 컴포넌트와 다르게, 모듈로 활용하기가 쉬우므로 서로의 장점을 전부다 가지고 있습니다.
Hooks 간단 소개
1. useState: 컴포넌트의 state(상태)를 관리 할 수 있습니다,
2. useEffect: 어떤 변수가 변경될때마다, 특정기능이 작동하도록 할 수 있습니다.
3. useContext: 여러개의 컴포넌트에서 사용할 수 있는, 변수 or 함수를 만들 수 있습니다. ( 전역으로 관리 )
4. useReducer: state(상태) 업데이트 로직을 reducer 함수에 따로 분리 할 수 있습니다.
5. useRef: 컴포넌트나 HTML 요소를 래퍼런스로 관리할 수 있습니다.
6. useImperativeHandle: useRef로 만든 래퍼런스를 상위 컴포넌트로 전달할 수 있습니다.
7. useMemo, useCallback: 의존성 배열에 적힌 값이 변할 때만 값,함수를 다시 정의할 수 있습니다.
8. useLayoutEffect: 모든 DOM 변경 후 브라우저가 화면을 그리기(render)전에 실행되는 기능을 정할 수 있습니다.
useState
useEffect(function, deps)
- function : 실행하고자 하는 함수
- deps : 배열 형태. function을 실행시킬 조건.
deps에 특정값을 넣게 되면 컴포넌트가 mount 될 때, 지정한 값이 업데이트될 때 useEffect를 실행합니다.
// React에 기본적으로 내장되어 있는 useState 훅을 사용하면, state를 만들 수 있습니다.
import { useState } from "react";
// const [state, state변경함수] = useState(기본 state값);
const [isLogin, setIsLogin] = useState(false);
// "setIsLoging"으로 state의 값을 true로 변경합니다.
setIsLogin(true);
- state는 어디에서든지 사용가능합니다. 하지만 웹, 앱이 커질수록 컴포넌트가 많아지고 자식으로 내려야할 props가 점점 많아지므로 점점 상태관리가 어려워지는 단점이 있습니다.
useEffect
useEffect(function, deps)
- function : 실행하고자 하는 함수
- deps : 배열 형태. function을 실행시킬 조건입니다.
deps에 특정값을 넣게 되면 컴포넌트가 mount 될 때, 지정한 값이 업데이트될 때 useEffect를 실행합니다.
// React에 기본적으로 내장되어 있는 useState와, useEffect 불러오기
import { setState, useEffect } from "react";
...
function App() {
const [data, changeData] = setState(false)
// useEffect(실행할 함수, 트리거가 될 변수)
useEffect(() => {
if (data.me === null) {
console.log("Data changed")
}
}, [data]);
// data변수가 바뀔때마다, react가 이를 감지해, 콘솔창에 "Data changed" 출력
return (
<div>
<button value="적용" onClick={changeData(!data)} />
</div>
)
}
export default App;
- useEffect() 함수는 React component가 렌더링 될 때마다 특정 작업(Sied effect)을 실행할 수 있도록 하는 리액트 Hook입니다. 여기서 Side effect는 component가 렌더링 된 이후에 비동기로 처리되어야 하는 부수적인 효과들을 뜻합니다.
- 이러한 기능으로 인해 함수형 컴포넌트에서도 클래스형 컴포넌트에서 사용했던 생명주기 메서드를 사용할 수 있게 되었습니다.
useContext
- React에서 context 없이 변수나 함수를 다른 컴포넌트로 전달하려면 부모자식간에만 전달이 가능하므로, 컴포넌트가 많아질수록 불필요한 컴포넌트를 많이 거쳐야하는 문제가 발생합니다.
- 하지만 context를 이용하면, 중간과정을 재치고 전역으로 전달할 수 있게되었습니다.
새로운 context 만들기
// newContext.js
import { createContext } from "react" // createContext 함수 불러오기
// context안에 homeText란 변수를 만들고, 공백("") 문자를 저장한다.
const newContext = createContext({
homeText: "",
})
context를 사용할 부분 선택하기, 새로운 정보 저장하기
// App.js
import React from "react";
import { View } from "react-native";
import Home from "./Home"; // 자식 컴포넌트 불러오기
import { newContext } from "./newContext"; // context 불러오기
export default function App() {
// context에 저장할 정보를 입력한다.
const homeText = "is Worked!"
// NewContext.Provider로 우리가 만든 context를 사용할 부분을 감싸준다.
return (
<newContext.Provider value={{ homeText }}>
<View>
<Home />
</View>
</newContext.Provider>
);
}
context 사용하기
// Home.js
import React from "react";
import { Text, View } from "react-native";
import { useContext } from "react";
import { newContext } from "../newContext";
export default function Home() {
// useContext hook 사용해서, newContext에 저장된 정보 가져오기
const { homeText } = useContext(newContext);
// 불러온 정보 사용하기!!
return (
<View>
<Text>{homeText}<Text>
</View>
);
}
- props가 2중 3중으로 넘겨야되는 상황에서 context는 매우 유용한 선택입니다.
- 전역으로 필요한 함수나 로그인 세션 데이터 혹은 유저 데이터를 context에 담아 놓으면 프로젝트 전역 어디서나 props로 넘기지 않아도 쓸 수 있는 편리함을 제공합니다.
useReducer
- useReducer는 State(상태)를 관리하고 업데이트하는 Hook인 useState를 대체할 수 있는 Hook입니다. 다시 말해, useReducer는 useState처럼 State를 관리하고 업데이트 할 수 있는 Hook입니다.
- useReducer의 묘미는, 컴포넌트 내에서 State를 업데이트하는 로직 부분을 그 컴포넌트로부터 분리시키는 것을 가능하게 해준다는 것입니다.
- useReducer는 State 업데이트 로직을 분리하여 컴포넌트의 외부에 작성하는 것을 가능하게 함으로써, 코드의 최적화를 이루게 해줍니다.
- useState 과 useReducer 비교
- useState
- 관리해야 할 State가 1개일 경우
- 그 State가 단순한 숫자, 문자열 또는 Boolean 값일 경우
- useReducer
- 관리해야 할 State가 1개 이상, 복수일 경우
- 혹은 현재는 단일 State 값만 관리하지만, 추후 유동적일 가능성이 있는 경우
- 스케일이 큰 프로젝트의 경우
- State의 구조가 복잡해질 것으로 보이는 경우
const [state, dispatch] = useReducer(reducer, initialState, init);
- state: 컴포넌트에서 사용할 State(상태).
- dispatch: reducer 함수를 실행시키며, 컴포넌트 내에서 state의 업데이트를 일으키기 위해서 사용하는 함수입니다.
- reducer: 컴포넌트 외부에서 state를 업데이트하는 로직을 담당하는 함수. 현재의 state와 action 객체를 인자로 받아서, 기존의 state를 대체(replace)할 새로운 State를 반환(return)하는 함수입니다.
- initialState: 초기 State
- init: 초기 함수
import React, { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "decrement":
// action의 type이 "decrement"일 때, 현재 state 객체의 count에서 1을 뺀 값을 반환함
return { count: state.count - 1 };
case "increment":
// action의 type이 "increment"일 때, 현재 state 객체의 count에서 1을 더한 값을 반환함
return { count: state.count + 1 };
default:
// 정의되지 않은 action type이 넘어왔을 때는 에러를 발생시킴
throw new Error("Unsupported action type:", action.type);
}
}
function Counter() {
const [number, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
{/* 현재 카운트 값은 state인 number 객체의 count로부터 읽어옴 */}
<h1>Count: {number.count}</h1>
{/* 카운트 값의 변경을 위해 각 버튼이 클릭되면 dispatch 함수가 발동되면서 reducer 함수가 실행됨.
dispatch 함수의 인자로, action 객체가 설정되었는데,
action 객체의 type에는 어떤 버튼을 클릭하였는지에 따라
"decrement" 또는 "increment"가 들어감
*/}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
export default Counter;
useRef
- useRef 함수는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당합니다. 이 current 속성은 값을 변경해도 상태를 변경할 때 처럼 React 컴포넌트가 다시 랜더링되지 않습니다.
- React 컴포넌트가 다시 랜더링될 때도 마찬가지로 이 current 속성의 값이 유실되지 않습니다.
- useRef 함수를 사용하는 또 다른 경우가 있는데, DOM 노드나 React 엘리먼트에 직접 접근하기 위해서 입니다.
useState 이용한 카운트
import React, { useState, useEffect } from "react";
function ManualCounter() {
const [count, setCount] = useState(0);
let intervalId;
const startCounter = () => {
// 💥 매번 새로운 값 할당
intervalId = setInterval(() => setCount((count) => count + 1), 1000);
};
const stopCounter = () => {
clearInterval(intervalId);
};
return (
<>
<p>자동 카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
}
- 위의 코드에서 가장 큰 걸릿돌은 안에서 선언된 intervalId 변수를 startCounter() 함수와 stopCounter() 함수가 공유할 수 있도록 해줘야 한다는 것입니다.
- 그럴려면 intervalId 변수를 두 함수 밖에서 선언해야하는데 그럴 경우, count 상태값이 바뀔 때 마다 컴포넌트 함수가 호출되어 intervalId도 매번 새로운 값으로 바뀔 것입니다. 따라서, 브라우저 메모리에는 미처 정리되지 못한 intervalId 들이 1초에 하나식 쌓여나갈 것입니다.
useRef 이용한 카운트
import React, { useState, useRef } from "react";
function ManualCounter() {
const [count, setCount] = useState(0);
const intervalId = useRef(null);
console.log(`랜더링... count: ${count}`);
const startCounter = () => {
// ref의 current에 새로운 intervalId 시작하기
intervalId.current = setInterval(
() => setCount((count) => count + 1),
1000
);
console.log(`시작... intervalId: ${intervalId.current}`);
};
const stopCounter = () => {
// clearInterval 함수로 intervalId 정리
clearInterval(intervalId.current);
console.log(`정지... intervalId: ${intervalId.current}`);
};
return (
<>
<p>자동 카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
}
//랜더링... count: 0
//시작... intervalId: 17
//랜더링... count: 1
//랜더링... count: 2
//랜더링... count: 3
//랜더링... count: 4
//랜더링... count: 5
//정지... intervalId: 17
//시작... intervalId: 32
//랜더링... count: 6
//랜더링... count: 7
//랜더링... count: 8
//정지... intervalId: 32
- 시작 버튼을 누르면 새로운 intervalId가 생성되고, 정지 버튼을 누르면 기존 intervalId가 정리되는 것을 확인할 수 있습니다.
useImperativeHandle
- React hook 중에 useImperativeHandle 이라는 훅이 있습니다. 이 훅은 forwardRef 를 사용해서 ref를 사용하는 부모 측에서 커스터마이징된 메서드를 사용할 수 있게 해줍니다.
- 리액트에서는 데이터가 부모에서 자식으로만 흐르는데, 이렇게 단방향으로만 작동하는 방식을 회피해야 할 때는 Redux나 Context API 같은 것을 사용합니다. 이런 것들을 사용하면 상태를 끌어올리지 않고도 부모에게 변경된 데이터가 적용되게 할 수 있습니다.
- 하지만 굳이 Redux나 Context API를 사용할 만큼의 일이 아닐 때 useImperativeHandle을 사용할 수도 있으므로 상태나 로직들은 자식 컴포넌트가 갖고 있고, 부모 컴포넌트는 ref.current에서 필요한 프로퍼티를 가져오기만 하면 됩니다.
function ParentComponent() {
// inputRef라는 동아줄을 만들어서 자식에게 보낸다
const inputRef = useRef();
return (
<>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.realllyFocus()}>포커스</button>
</>
)
}
function FancyInput(props, ref) {
// 부모가 내려준 동아줄 ref에다가 이것 저것 작업을 한다
// 부모는 이 로직에 대해 모르고, 위로 끌어올리지 않고도 그냥 ref.current로 접근하여 사용만 하면 된다
useImperativeHandle(ref, () => ({
reallyFocus: () => {
ref.current.focus();
console.log('Being focused!')
}
}));
// ref는 input을 참조하고 있다.
return <input ref={inputRef} />
}
// ref를 컴포넌트에 달 때는 forwardRef로 감싼다
FancyInput = forwardRef(FancyInput)
- useImperativeHandle 의 첫 번째 인자로는 프로퍼티를 부여할 ref이고, 두 번째 인자는 객체를 리턴하는 함수이다. 이 객체에 추가하고 싶은 프로퍼티를 정의하면 됩니다.
- 예시 코드에서는 reallyFocus 라는 메서드를 정의했고, 이 메서드를 호출하기 위해서는 사용처에서는 그 ref.current에 정의된 메서드를 호출하기만 하면 된다.
- 이렇게 할 때의 장점은 child component에 상태나 로직을 isolate할 수 있다는 점입니다.
useMemo & useCallback
- useMemo와 useCallback은 매우 유사합니다. 내부에서 발생하는 연산을 최적화하며 특정 값이 바뀌었을 때만 연산을 실행하고, 바뀌지 않았으면 이전에 연산했던 결과를 다시 사용하는 방식입니다.
- useMemo 는 값을 반환하고 useCallback은 함수를 반환합니다.
- 즉 컴포넌트가 랜더링 하였을때 useMemo와 useCallback로 감싼 함수와 값들은 메모제이션용 메모리에 저장해 놓기 때문에 다시 생성되지 않아 성능에 최적화를 제공합니다. 하지만 메모제이션용 메모리가 필요하므로 useMemo와 useCallback을 남용하면 안됩니다.
- useMemo와 useCallback을 쓰지 않을 경우
import React, { useState } from 'react';
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const getAverage = () => {
console.log('평균값 계산 중...');
if (list.length === 0) return 0; const sum = list.reduce((a, b) => a + b);
return sum / list.length;
};
const onChange = (e) => {
setNumber(e.target.value);
};
const onInsert = () => {
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()} </div>
</div> );
};
export default Average;
- 글자 입력 시에도 getAverage함수가 동작하기 때문에 콘솔창에 "평균값 계산중... "이 뜸니다.
- 버튼 클릭시에도 getAverage함수가 동작하기 때문에 콘솔창에 "평균값 계산중..."이 뜸니다.
- 종합적으로, getAverage함수가 필요하지 않은 동작에서도 작동이 됩니다.
- useMemo
import React, { useState, useMemo } from 'react';
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const getAverage = useMemo(() => {
console.log('평균값 계산 중...');
if (list.length === 0) return 0; const sum = list.reduce((a, b) => a + b);
return sum / list.length;
}, [list]); //list값이 업데이트 될때만 실행
const onChange = (e) => { setNumber(e.target.value); };
const onInsert = () => {
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} {/* useMemo는 값을 반환 */}
</div>
</div>
);
};
- 첫 렌더링과 값을 입력 할때만 렌더링되는 코드입니다, 즉 [ ] 안에 써준 값 list가 업데이트 될 때만 렌더링이 돌아갑니다.
- useCallback
import React, { useState, useCallback, 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 = useCallback((e) => {
setNumber(e.target.value); }, []);
const onInsert = useCallback(() => {
const nextList = list.concat(parseInt(number)); setList(nextList); setNumber('');
}, [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;
- onChange와 onInsert함수에 useCallback을 감싸므로, [ ] <- 괄호 안의 값이 바뀌었을 때만 함수를 생성해 줍니다.
- 하지만 useCallback을 사용하지 않은 경우 리렌더링 될때마다 함수를 새로 만들어 줍니다.
useLayoutEffect
- useLayoutEffect와 useEffect 비교
- useLayoutEffect는 컴포넌트들이 render 된 후 실행되며, 그 이후에 paint 가 됩니다. 이 작업은 동기적(synchronous) 으로 실행되기 때문에 paint 가 되기전에 실행되기 때문에 dom 을 조작하는 코드가 존재하더라도 사용자는 깜빡임을 경험하지 않습니다
- 반대로 useEffect 는 컴포넌트들이 render 와 paint 된 후 실행됩니다. 비동기적(asynchronous) 으로 실행됩니다. paint 된 후 실행되기 때문에, useEffect 내부에 dom 에 영향을 주는 코드가 있을 경우 사용자 입장에서는 화면의 깜빡임을 보게됩니다..
- useLayoutEffect을 사용할 때
- useLayoutEffect 는 동기적으로 실행되고 내부의 코드가 모두 실행된 후 painting 작업을 거칩니다.
- 따라서 로직이 복잡할 경우 사용자가 레이아웃을 보는데까지 시간이 오래걸린다는 단점이 있어, 기본적으로는 항상 useEffect 만을 사용하는 것을 권장합니다. 구체적인 예시로는
- 데이터 fetch
- event handler
- state reset
등의 작업은 항상 useEffect 를 사용하되,
const Test = () => {
const [value, setValue] = useState(0);
useLayoutEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log('render', value);
return (
<button onClick={() => setValue(0)}>
value: {value}
</button>
);
};
- 위와 같이 화면이 깜빡거리는 상황은 위 코드의 state가 존재하며, 해당 state 이 조건에 따라 첫 painting 시 다르게 렌더링 되어야 할 때는 useEffect 사용 시 처음에 0이 보여지고 이후에 re-rendering 되며 화면이 깜빡거려지기 때문에 useLayoutEffect 를 사용합니다.
'프로그래밍 개발 > 프론트엔드 개발자 기본 지식' 카테고리의 다른 글
프론트엔드 로드맵 2023 | Front End Road Map 2023 (0) | 2023.08.17 |
---|---|
REST API( Representational State Transfer) (0) | 2022.05.10 |
MVC 패턴 (0) | 2022.05.08 |
Test-Driven Development(TDD) (0) | 2022.05.08 |
JS ES5 핵심 개념(JS ES5 Core Concept) (0) | 2022.05.05 |
댓글