# React
- facebook에서 만들고 활용하고 있는 ui 라이브러리
핵심 개념인 컴포넌트는 여기에서 설명합니다.
리액트는 ReactDOM 이라는 별도의 패키지의 render 메소드를 사용하여
React 컴포넌트를(VDOM) 실제로 DOM에 렌더링할 수 있습니다.
# Hook API
hook 혹은 hooking 이라는 용어는.. 프로세스를 가로채고 정상적인 프로세스를 방해 할 수 있다는 의미입니다.
react hook에서는 (과거 stateless로 사용했던) functional component에서도 상태를 가질 수 있으며
라이프사이클을 대체할 수 있는 함수를 제공합니다. (useEffect)
이제는 react개발의 핵심이 된 react 라이브러리의 최신 API입니다.
# 사용 규칙
- 컴포넌트에서 훅을 호출하는 순서가 보장되어야한다.
순서가 보장되어야한다는 것은 제어구조 내에서 사용하지 말라는 것입니다.
리액트 내부에서 관리하는, 훅을 위한 인덱싱이 깨질 수 있기 때문이죠.
훅은 함수형 컴포넌트 혹은 커스텀 훅 안에서만 호출한다.
use~ 로 네이밍한다. 에러는 아니지만 경고가 뜬다.
# 근데 왜 이걸 사용하나?
사실 가장 큰 단점은 props가 암묵적으로 넘어온다는점 입니다. hoc를 많이 사용하면 props 이름 충돌의 가능성도 배제할 순 없죠.
함수형 컴포넌트에서 state를 사용하기위해 사용하는것도 맞지만,
공통 비즈니스로직들을 사용하기위해 hoc를 활용했는데, 복잡해질수록 Wrapper가 늘어나기만하고..
hoc를 여러개 중첩하면 보기도 힘들고, 어떤 hoc가 어떤 로직을 가졌는지 파악하기도 쉽지않고,
여러개의 life cycle에 중복 로직도 들어가고, 로직들이 흩어지기도 하고.. 점점 복잡해지고 헷갈리며 테스트하기도 어려워집니다.
vue의 경우에는 hoc와 비슷하면서도 다른 mixins를 이용해서 중복 로직을 처리하는데,
같은 문제가 있어서 vue3에서 react hook에 영감을 받아 개발한 composition API를 공개했죠.
아래에서는 가장 많이 사용하는 hook API 3개와 커스텀 hook을 소개합니다.
# useState
useState 훅은 기존 클래스형 컴포넌트의 state와 setState를 대체합니다.
useState 함수는 파라미터로 state초기값을 넘겨주고, 아래와 같이 배열을 리턴합니다.
const [state, setState] = useState(initialState);
배열의 두번째에 위치한 setState함수로 state를 변경 할 수 있습니다.
또한 setter에 함수를 사용할 수도 있습니다.
아래 코드는 단순 boolean을 toggle하는 함수를 활용하는 예시입니다.
//typescript
const [flag, setFlag] = useState<boolean>(false);
const toggleFlag1 = () => setFlag(!flag);
const toggleFlag2 = () => setFlag((prevState) => !prevState);
장점이 전혀 없는것처럼 보일 수도 있지만..
useCallback으로 toggleFlag를 다시 작성하면..
//typescript
const [flag, setFlag] = useState<boolean>(false);
const toggleFlag1 = useCallback(() => setFlag(!flag), [flag]);
const toggleFlag2 = useCallback(() => setFlag((prevState) => !prevState), []);
deps에서 차이를 보이고 있습니다.
가능하면 deps가 적을수록 좋습니다. deps중에 하나라도 변경되면, 해당 훅이 다시 실행됩니다.
함수를 자주 새로 생성하는것을 줄이는건 성능에 거의 영향을 주지않지만 나름 의미도 있고,
훅이 특정 상태에 의존성을 갖게 되는것이 중요하다고 할 수 있습니다.
# useEffect
기존 클래스 컴포넌트로는 생명주기 메소드를 이용했는데, 이제는 생명주기가 아닌 '상태'주기를 이용해야합니다.
리액트 생명주기 메소드는 호출 타이밍이 정해져있죠. 고정된 파이프라인입니다.
반면에 useEffect로 생성하는 상태주기 메소드는 프로그래밍 가능한 수많은 파이프라인입니다.
이제 API호출해야하는 타이밍에 대해 의견이 엇갈 일 조차도 없습니다.
상태주기를 다루고 있으므로, 각 훅의 deps에 의존성을 모두 추가하는것이 정상이며, 그렇게 해야합니다. eslint에 좋은 플러그인이 존재합니다.
useEffect를 여러번 사용해서, 기능별로 분리해놓으면 원하는 기능을 버그없이 구현할 수 있습니다.
아래는 간단한 예제입니다.
useEffect(
() => {
//...some effect code
return () => {
// ...some clean up code
};
},
[
//deps
]
);
아래 코드처럼 props가 변경될때 state를 변경시키도록 할 수도 있습니다.
const SomeComponent = ({myInput}) => {
const [input, setInput] = useState('');
useEffect(() => {
setInput(myInput);
}, [myInput]); // setInput????
}
setInput은 왜 deps에 포함시키지않아도 될까요?
effect는 deps 중에 하나라도 변경되면 trigger되는데요, setInput은 어차피 변하지않기때문입니다..!
그러므로 변하지않는값은 넣지않아도 됩니다.
아래에 헷갈릴 수도 있는 예제를 하나 추가합니다.
const SomeComponent = ({ a, b }) => {
const isNotZero = useMemo(() => a !== 0 && b !== 0, [a, b]);
useEffect(() => {
somePureFunction(a !== 0 && b !== 0);
}, [a, b]);
useEffect(() => {
somePureFunction(isNotZero);
}, [isNotZero]);
}
두 개의 effect는 로직이 같습니다. 하지만 실행되는 횟수는 달라질 수 있습니다.
isNotZero 는 a, b를 deps로 가지는데, 값은 true, false 중에 하나입니다.
첫번째 effect는 a 혹은 b가 변경되면 실행될것이고
두번째 effect는 isNotZero의 '값'(true, false)가 달라지면 실행될것입니다.
isNotZero가 a, b가 달라져서 재계산하더라도, 결국 값이 달라지지 않았다면 effect가 실행되지 않습니다.
# useMemo, useCallback
용도 자체는 레퍼런스 변경을 방지하는 것 입니다.
예를 들어, 상위 컴포넌트에서 props로 함수를 하위 컴포넌트에게 전달한다고 가정합니다.
이때, 상위 컴포넌트 렌더링이 돌면, props로 넘겼던 함수의 레퍼런스가 변경됩니다.
실제로 함수자체가 변경되지않았음에도 함수가 재생성되어 props가 변경됐다고 가정한 하위 컴포넌트는
쓸데없이 리렌더될 수 있습니다.
useMemo는 값을 메모이제이션 한다는 특징이 있어서 비싼 연산을 기억하여 최적화 할 수도 있습니다.
하지만 react 공식문서에서는 추후 업데이트에서 재계산을 할지도 모르니, 성능향상으로 쓰지말라고 가이드합니다.
const [myName, setMyName] = useState("boseok jung");
const reversedMyName = useMemo(
() => Array.from(myName).reverse().join(""),
[myName]
);
console.log(reversedMyName); // gnuj koesob
아래는 useMemo와 useCallback을 비교한 것입니다.
사실 useCallback은 명시적으로 함수일것이다를 알 수 있는 것 외에는 useMemo와 동일하게 동작합니다.
function someFunction() {
return "boseok";
}
const memo = useMemo(() => someFunction, []);
const callback = useCallback(someFunction, []);
아래처럼 deps관리를 위해서라도 같은 deps를 가진 컴포넌트 변수(함수 포함)들이 있다면, 하나의 useMemo를 통해 변수들을 반환받는게 좋을것입니다.
(곧 나오는 용어인 deps는 아래서 설명하고 있습니다.)
const [name, setName] = useState(0);
const [nameLength, reversedName] = useMemo(
() => [name.length, Array.from(name).reverse().join("")],
[name]
);
// const nameLength = useMemo(() => name.length, [name]);
// const reversedName = useMemo(
// () => name.length,
// Array.from(name)
// .reverse()
// .join(''),
// [name]
// );
간단한 예제인데, 주석처럼 작성하면... 로직이 길고 복잡하다고 가정했을때 => 어떤 훅에 name dependency가 있는지 코드를 전부 파악해야하고, 수정하기 두렵습니다.
useCallback에 대해 조금 더 이야기 해보자면.. memo로 동일한 기능을 구현할 수 있어서 useCallback이 필요없을지도 모르겠지만
useCallback의 존재 이유는 변수인지 함수인지 구분하기 위함도 있고, useEffect와 함께 사용할때
useEffet 훅이 deps의 레퍼런스 체크할때의 의미가 있습니다.
어차피 렌더하는 횟수만큼 함수자체는 생성하겠지만, 변수에 할당을 안한다는 의미입니다.
함수의 생성 자체는 경량함수인 arrow function을 사용할 것이고, 그정도 비용은 무시합니다.
function SomeComponent({ name }) {
const onClick = useCallback(() => {
// do something
}, []);
const someEffect1 = useCallback(() => {
// something...
}, []);
const someEffect2 = () => null;
useEffect(() => {
someEffect1();
}, [someEffect1]);
useEffect(() => {
someEffect2();
}, [someEffect2]);
return (
<div>
<div>name: {name}</div>
<button type="button" onClick={onClick}>
click!
</button>
</div>
);
}
우선 someEffect2는 아마 lint를 쓴다면 경고를 띄워줄겁니다.
someEffect2를 useCallback을 써라 혹은 deps를 지워라.
어차피 매번 someEffect2라는 변수에는 새로운 함수가 할당될것이기때문이죠.
반면 someEffect1의 사용은 옳습니다.
someEffect1의 레퍼런스는 오직 someEffect1의 deps에 의해 변하므로, 변하지않거든요.
# dependency list (deps)
useEffect, useMemo, useCallback의 두번째인자는 DependencyList 입니다.
배열을 넘겨주면되고, 배열의 요소는 특정 변수들을 넣어주면되는데,
useMemo의 경우,
const [myName, setMyName] = useState("boseok jung");
const reversedMyName = useMemo(
() =>
Array.from(myName)
.reverse()
.join(""),
[myName]
);
myName이라는 state에 의존하고있으므로, myName을 넣어줍니다.
reversedMyName이라는 변수가 갱신되는 조건이 myName이라는 변수가 변경됐을 때 콜백을 다시 실행하여 갱신하고, 그것을 메모이제이션하는 겁니다.
vscode 플러그인 같은걸 사용하면, 자동으로 넣어주기도 합니다.
# custom hook
기존 클래스 컴포넌트에서는 공통 로직을 분리하기위해 with~ 라는 단어로 시작하는 hoc를 만들곤 했는데요,
요즘은 클래스 컴포넌트 대신 함수형 컴포넌트와 hook을 사용하기때문에, 공통 로직을 위해서는 hook을 만들어서 사용합니다
네이밍 컨벤션은 use~입니다. useState처럼요. 이 컨벤션은 리액트의 프리컴파일러에 의해 강제됩니다. use로 시작하지않으면 아마 에러가 날겁니다.
이제 예제를 볼까요?
아래와 같은 두 컴포넌트가 있습니다.
const Page1 = () => {
const [isChecked, setIsChecked] = useState(false);
const onChangeCheckBox = () => setIsChecked((prevState) => !prevState);
return (
<div>
<div>페이지1</div>
<input type="checkbox" checked={isChecked} onChange={onChangeCheckBox} />
<span>선택</span>
</div>
);
};
const Page2 = () => {
const [isChecked, setIsChecked] = useState(false);
const onChangeCheckBox = () => setIsChecked((prevState) => !prevState);
return (
<div>
<div>페이지2</div>
<input type="checkbox" checked={isChecked} onChange={onChangeCheckBox} />
<span>선택</span>
{/* some elements... */}
</div>
);
};
엄청 좋은 예제는 아니지만 이해를 돕기엔 충분합니다.
custom hook을 사용하여 이런식으로 유사한 로직을 분리할 수 있습니다.
추가로 useMemo와 유사한 useCallback의 활용도 볼 수 있습니다~
const useCheckBox = (initialState = false) => {
const [isChecked, setIsChecked] = useState(initialState);
const onChangeCheckBox = useCallback(
() => setIsChecked((prevState) => !prevState),
[]
);
return [isChecked, onChangeCheckBox];
};
export default useCheckBox;
커스텀 훅은 별게없고 함수형 컴포넌트랑 유사하지만 단지 JSX (ReactNode)를 리턴하지 않고 로직에 대한 상태나 setter를 리턴하는 것 뿐입니다.
생성한 커스텀 훅을 활용하여 다시 Page1만 작성해보면 아래와 같을 것 입니다.
import useCheckBox from "./useCheckBox";
const Page1 = () => {
const [isChecked, onChangeCheckBox] = useCheckBox();
return (
<div>
<div>페이지1</div>
<input type="checkbox" checked={isChecked} onChange={onChangeCheckBox} />
<span>선택</span>
</div>
);
};
예제의 useCheckBox에 해당하는 로직이 짧아서 큰 메리트를 못 느낄수있지만,
뷰와 모델을 깔끔하게 분리해내고나면 컴포넌트 자체 가독성이 좋아지고 테스트하기 편해집니다.
changeEvent를 받아서 value를 뽑아내고 validate function을 호출하고..
setState해주는 로직을 갖고있는 custom hook 예제입니다 (타입스크립트)
import { useCallback, useState } from "react";
import { IIsValidTextInputOptions, isValidTextInput } from "utils/validate";
interface IUseTextInput {
initialValue?: string;
validatorOption?: IIsValidTextInputOptions;
}
type ChangeEventType = React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement
>;
/**
* [text, onChangeText]
* validator는 validatorOption의 property가 하나라도 있어야 작동합니다.
* 그래서 기본적으로 validate는 하지않습니다
*/
const useTextInput = ({
initialValue = "",
validatorOption = {},
}: IUseTextInput = {}): [string, (e: ChangeEventType) => void] => {
const [text, setText] = useState(initialValue);
const onChangeText = useCallback(
(e: ChangeEventType) => {
const { value } = e.target;
const isUsingValidator = !!Object.keys(validatorOption).length;
if (
!isUsingValidator ||
(isUsingValidator && isValidTextInput(value, validatorOption))
) {
setText(value);
}
},
[validatorOption]
);
return [text, onChangeText];
};
export default useTextInput;
API 호출하는부분에서 API 로딩과 에러를 promise 친숙하게 ts로 작성한 hook입니다. 참고만하세요..
type Method = "GET" | "POST" | "PUT" | "DELETE";
type APIFunctionReturn<T = any> = [T, StateUpdater<T>, boolean, boolean];
type APIStatus = "PENDING" | "FULFILLED" | "REJECTED";
const useFetch = <T = any>(
apiFunction: () => Promise<T>,
method: Method = "GET"
): APIFunctionReturn<T> => {
const [data, setData] = useState<T>(null);
const [apiStatus, setApiStatus] = useState<APIStatus>("PENDING");
const isLoading = useMemo(() => apiStatus === "PENDING", [apiStatus]);
const isError = useMemo(() => apiStatus === "REJECTED", [apiStatus]);
useEffect(() => {
if (!data) {
apiFunction()
.then((data) => {
setData(data);
setApiStatus("FULFILLED");
})
.catch(() => {
setApiStatus("REJECTED");
});
}
}, [apiFunction, method, data]);
return [data, isLoading, isError];
};
export default useFetch;
# 커스텀 훅으로 ui와 로직의 깔끔한 분리
위에 있는 내용을 이해했다면, 커스텀 훅으로 ui와 로직의 분리가 가능하겠죠?
컴포넌트는 props와 하나의 커스텀훅만 가지는게 꽤나 좋은 설계입니다.
그 커스텀 훅 내부에서 또 여러개의 훅을 쓸지라도요.
이렇게 모델과 ui가 분리되면 해당 컴포넌트 자체는 훅만 테스트하면 됩니다.
앱이 안정적이고 유지보수하기 좋게 됩니다.
# Dynamic Component
# with string
string으로 다이나믹 컴포넌트 렌더링하는 방법.
vue와 유사한 형태
import Card1 from './Card1';
import Card2 from './Card2';
import Card3 from './Card3';
const CardComponents = {
Card1,
Card2,
Card3,
};
// 이렇게 하거나..
const Component = ({is, components, ...props}) => {
const DynamicComponent = components[is];
return (
<DynamicComponent {...props} />
);
};
// 혹은..
const Component = ({is, components, ...props}) => React.createElement(components[is], props);
const App = () => {
const [currentComponent, setCurrentComponent] = useState('Card1');
return (
<Component is={currentComponent} components={CardComponents}/>
);
}
# input type file
<input type="file">
요런 녀석으로 file을 업로드할 수 있다.
react에서는 onChange 이벤트를 바인딩해줘야한다.
그런데 브라우저 동작이 같은 파일을 연속 두번 업로드하게되면,
파일이 바뀐게 아니므로, onChange가 작동을 안한다.
그런데 작동하게 해야할 기획이 들어왔다고하면.. 되게 해야한다.
file은 수정할 수 없으므로 value를 바인딩할수도 없다.
그래서 ref를 이용하여 아래와 같이 바인딩해준다
<input type="file" ref={fileRef} onChange={onChangeImage} />
이런식으로.. 바인딩해주고,
const fileRef = useRef();
fileRef.current.value = '';
이런 식으로 value를 적절한 곳에서 초기화시켜버리면 된다.
# Performance
# 1. 클래스 컴포넌트인 경우
(클래스 컴포넌트를 사용할때만!)
react는 react만의 최적화 기법이 있다.
lifecycle 메소드를 이용하는 방법이다.
우선 react는 state, props가 변경되면 해당 컴포넌트를 다시 렌더링한다.
react의 shouldComponentUpdate lifecycle method에는 nextProps, nextState가 순서대로 파라미터에 전달된다.
파라미터 이름만봐도 대충 짐작이 간다.
현재 state와 props 전달받은 state와 props를 비교할수있다.
컴포넌트를 다시 그리고싶다면 true를 리턴하면되고, 최적화를 위해 리렌더링을 하기싫다면- false를 리턴하면된다.
더 자세히 알고싶다면 공식문서를..
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
state와 props 구조가 단순하고, 구현하기귀찮다면
클래스에 Component를 상속받지말고, PureComponent를 상속받으면 된다.
PureComponent에는 shouldComponentUpdate 메소드가 이미 구현되어있다.
react 개발자 중 한명은 PureComponent를 사용하는것을 권장하지않는다.
곧 react가 버전업하게되면, 함수형 컴포넌트의 성능을 향상시킬 것이라고 한다.
컴포넌트에 lifecycle이 필요없다면 함수형 컴포넌트를 사용하도록 하는것이 향후에
성능을 좋게 하는 방법이라고 할 수 있겠다.
# 2. 함수형 컴포넌트인 경우
react hook을 이용하여 프로퍼티 혹은 변수, 메소드를 캐싱하자.
=> 성능이슈가 있는 컴포넌트에서 useMemo, useCallback을 적극적으로 활용하자.
# useSelector를 사용하는 경우
구조분해할당을 알고있다면 아래처럼 사용하고 싶을것이다.
// state.user = {name, email, phone} <= 이렇다고 가정하자.
const {name, email} = useSelector(state => state.user);
하지만 이 방법은 해당 훅에서 state.user를 바라보고있기때문에, 레퍼런스가 변경되면 다시 렌더한다.
다시 말하면 name, email이 변경되지않고 phone이 변경되어도 리렌더한다.
이런 경우에는 디스트럭쳐링을 활용할 수 없고, 풀어서 쓰거나..
아래 코드처럼 useSelector의 두번째 인자에 비교함수를 작성하거나 아래처럼 shallowEqual을 사용하는것이다.
const { name, email } = useSelector(
state => ({
name: state.user.name,
email: state.user.email
}),
shallowEqual
);
둘다 react-redux 패키지에 포함되어있다.
← React Tutorial RxJS →