직무적성검사
인적성 수리 문제
고차 컴포넌트는 컴포넌트 자체의 로직을 재사용하기 위한 방법입니다.
고차 컴포넌트는 고차 함수의 일종으로, 자바스크립트의 일급 객체, 함수의 특징을 이용하므로 리액트가 아니더라도 자바스크립트 환경에서 널리 쓰일 수 있습니다.
리액트에서는 이러한 고차 컴포넌트 기법으로 다양한 최적화나 중복 로직 관리를 할 수 있습니다.
리액트에서 가장 유명한 고차 컴포넌트는 리액트에서 제공하는 API 중 하나인 React.memo입니다.
아래의 예제의 ChildComponent는 props인 value=”hello”가 변경되지 않았음에도 handleChange로 인해 setState를 실행해 state를 변경하므로 리렌더링이 발생합니다.
const ChildComponent = ({ value }: { value: string }) => {
useEffect(() => {
console.log('렌더링!');
})
return <>안녕하세요! {value}</>
}
function ParentComponent() {
const [state, setState] = useState(1)
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setState(Number(e.target.value))
}
return (
<>
<input type="number" value={state} onChange={handleChange} />
<ChildComponent value="hello" />
</>
)
}
}
React.memo는 렌더링하기에 앞서 props를 비교해 이전과 props가 같다면 렌더링 자체를 생략하고 이전에 기억해 둔(memoization) 컴포넌트를 반환합니다다.
const ChildComponent = memo(({ value }: { value: string }) => {
useEffect(() => {
console.log('렌더링')
})
return <>안녕하세요! {value}</>
})
function ParentComponent() {
const [state, setState] = useState(1)
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setState(Number(e.target.value))
}
return (
<>
<input type="number" value={state} onChange={handleChange} />
<ChildComponent value="hello" />
</>
)
}
위의 방식은 클래스형 컴포넌트의 PureComponent와 매우 유사하다고 볼 수 있습니다.
React.memo는 컴포넌트도 값이라는 관점에서 본 것이므로 useMemo를 사용해서도 동일하게 메모이제이션할 수 있지 않을까요?
function ParentComponent() {
const [state, setState] = useState(1)
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setState(Number(e.target.value))
}
const MemoizedChildComponent = useMemo(() => {
return <ChildComponent value="hello" />
}, [])
return (
<>
<input type="number" value={state} onChange={handleChange} />
{MemoizedChildComponent}
</>
)
}
useMemo를 사용할 경우 값을 반환받기 때문에 JSX 함수방식이 아닌 {}을 사용한 할당식을 사용한다는 차이점이 있습니다.
필요에 따라 이러한 방식으로 구현할 수도 있지만 코드를 작성하고 리뷰하는 입장에서 혼선을 빚을 수 있으므로 목적과 용도가 뚜렷한 memo를 사용하는 편이 좋습니다.
리액트의 고차 컴포넌트를 만들기에 앞서 먼저 자바스크립트에서 고차 함수를 만드는 것에 대해 살펴보고자 합니다.
리액트의 함수형 컴포넌트도 결국 함수이기 때문에 함수를 기반으로 고차 함수를 만드는 것을 먼저 이해해야 합니다.
고차 함수의 사전적인 정의는 ‘함수를 인수로 받거나 결과로 반환하는 함수’ 입니다.
가장 대표적인 고차함수로는 Array.prototype.map을 들 수 있습니다.
function add(a) {
return function (b) {
return a + b
}
}
const result = add(1) // 여기서 result는 앞서 반환한 함수를 가리킨다.
const result = result(2) // 비로소 a와 b를 더한 3이 반환된다.
a=1이라는 정보가 담긴 클로저가 result에 포함됐고, result(2)를 호출하면서 이 클로저에 담긴 a=1인 정보를 이용해 1 + 2의 결과를 반환할 수 있게 됐습니다. 고차함수를 활용하면 인수로 받거나 새로운 함수로 반환해 완전히 새로운 결과를 만들어 낼 수 있다. 자연스럽게, 리액트의 함수형 컴포넌트도 함수이므로 고차 함수를 사용하면 다양한 작업을 할 수 있습니다.
사용자 인증 정보에 따라서 인증된 사용자에게는 개인화된 컴포넌트를, 그렇지 않은 사용자에게는 별도로 정의된 공통 컴포넌트를 보여주는 시나리오를 떠올려봅시다.
고차 함수의 특징에 따라 개발자가 만든 또 다른 함수를 반환할 수 있다는 점에서 고차 컴포넌트를 사용하면 매우 유용합니다.
interface LoginProps {
loginRequired?: boolean
}
function withLoginComponent<T>(Component: ComponentType<T>) {
return function (props: T & LoginProps) {
const { loginRequired, ...restProps } = props
if (loginProps) {
return <>로그인이 필요합니다.</>
}
return <Component {...(restProps as T)} />
}
}
// 원래 구현하고자 하는 컴포넌트를 만들고, withLoginComponent로 감싸기만 하면 끝이다.
// 로그인 여부, 로그인이 안되면 다른 컴포넌트를 렌더링하는 책임은 모두
// 고차 컴포넌트인 withLoginComponent에 맡길 수 있ㅇ 매우 편리하다.
const Component = withLoginComponent((props: { value: string }) => {
return <h3>{props.value}></h3>
})
export default function App() {
const isLogin = true
return <Component value="text" loginRequired={isLogin} />
}
Component는 우리가 아는 일반적인 함수형 컴포넌트와 같은 평범한 컴포넌트지만, 이 함수 자체를 withLoginCompnent라 불리는 고차 컴포넌트로 감싸뒀습니다.
withComponent는 함수(함수형 컴포넌트)를 인수로 받으며, 컴포넌트를 반환하는 고차 컴포넌트입니다.
이 컴포넌트는 props에 loginRequired가 있다면 넘겨받은 함수를 반환하는 것이 아니라 “로그인이 필요합니다”라는 전혀 다른 결과를 반환하게 돼 있습니다.
고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다 더욱 큰 영향력을 컴포넌트에 미칠 수 있습니다.
단순히 값을 반환하거나 부수 효과를 실행하는 사용자 정의 훅과는 다르게, 고차 컴포넌트는 컴포넌트의 결과물에 영향를 미칠 수 있는 다른 공통된 작업을 처리할 수 있습니다.
사용자 정의 훅과 고차 컴포넌트 모두 리액트 코드에서 어떠한 로직을 공통화해 별도로 관리할 수 있다는 특징이 있습니다.
사용자 정의 훅이 필요한 경우
단순히 useEffect, useState와 같이 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋습니다.
사용자 정의 훅은 그 자체로는 렌더링에 영향을 미치지 못하기 때문에 사용이 제힌적이므로 반환하는 값을 바탕으로 무엇을 할지는 개발자에게 달려 있습니다.
// 사용자 정의 훅을 사용하는 경우
function HookComponent() {
const { loggedIn } = useLogin()
useEffect(() => {
if(!loggedIn) {
// do something
}
}, [loggedIn])
}
// 고차 컴포넌트를 사용하는 경우
const HOCComponent = withLoginComponent(() => {
// do something
})
로그인 정보를 가지고 있는 훅인 useLogin은 단순 loggedIn에 대한 값만 제공할 뿐, 이에 대한 처리는 컴포넌트를 사용하는 쪽에서 원하는 대로 사용이 가능합니다.
따라서 부수효과가 비교적 제한적이라고 볼 수 있습니다.
반면 withLoginComponent는 고차 컴포넌트가 어떤 일을 하는지, 어떤 결과물을 반환할지는 고차 컴포넌트를 직접 보거나 실행하기 전까지는 알 수 없습니다.
대부분의 고차 컴포넌트는 렌더링에 영향을 미치는 로직이 존재하므로 사용자 정의 훅에 비해 예측하기가 어렵습니다.
따라서 단순히 컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 하고 싶다면 사용자 정의 훅을 사용하는 것이 좋습니다.
고차 컴포넌트를 사용해야 하는 경우
만약 로그인되지 않은 어떤 사용자가 컴포넌트에 접근하려 할 때 애플리케이션 관점에서 컴포넌트를 감추고 로그인을 요구하는 공통 컴포넌트를 노출하는 것이 좋습니다.
또는 에러 바운더리와 비슷하게 어떠한 특정 에러가 발생했을 때 현재 컴포넌트 대신 에러가 발생했음을 알릴 수 있는 컴포넌트를 노출하는 경우도 있을 것입니다.
function HookComponent() {
const { loggedIn } = useLogin()
if(!loggedIn) {
return <LoginComponent />
}
return <>안녕하세요.</>
}
const HOCComponent = withLoginComponent(() => {
// loggedIn state의 값을 신경 쓰지 않고 그냥 컴포넌트에 필요한 로직만
// 추가해서 간단해졌다. loggedIn state에 따른 제어는 고차 컴포넌트에서 해줄 것이다.
return <>안녕하세요</>
}
이러한 작업을 사용자 정의 훅으로 표현해야 한다고 가정해 봅시다.
loggedIn이 false인 경우에 렌더링해야 하는 컴포넌트는 동일하지만 사용자 정의 훅으로는 이를 표현하기 어렵습니다.
사용자 정의 훅은 해당 컴포넌트가 반환하는 렌더링 결과물에까지 영향을 미치기는 어렵기 때문입니다.
그리고 이러한 중복 처리가 해당 사용자 정의 훅을 사용하는 애플리케이션 전반에 걸쳐 나타나게 될 것이므로 사용자 정의 훅 보다는 고차 컴포넌트를 사용해 처리하는 것이 좋습니다.
렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용합시다. 고차 컴포넌트가 많아질수록 복잡성이 기하급수적으로 증가하므로 신중하게 사용해야 합니다.
Leave a comment