리액트에서 재사용할 수 있는 로직을 관리할 수 있는 두 가지 방법이 있는데 이는 사용자 정의 훅, 그리고 고차 컴포넌트이다.
고차 컴포넌트는 굳이 리액트가 아니더라도 사용할 수 있는 기법이지만 사용자 정의 훅은 리액트에서만 사용할 수 있는 방식이다.
반드시 use로 시작하는 함수를 만들어야 한다. 이를 이용해 반복되는 코드를 줄일 수 있다.
use-Hooks, react-use, ahooks 등에서 이런 사용자 정의 훅을 보고 사용할 수 잇다.
고차 컴포넌트(HOC, Higher Ordre Conponent)는 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
JS의 일급 객체, 함수 특징을 이용하므로 굳이 리액트가 아니더라도 JS 환경에서 널리 쓰일 수 있다.
리액트에서 가장 유명한 고차 컴포넌트는 리액트에서 제공하는 API 중 하나인 React.memo다.
자식 컴포넌트의 prop 변경 여부와 관계없이 부모 컴포넌트가 렌더링 될 시 새롭게 렌더링 된다.
ex)
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" />
</>
)
}
위 예제에서 props인 value="hello"가 변경되지 않았음에도 handleChange로 인해 setState를 실행해 state를 변경하므로 리렌더링이 발생한다.
이와 같은 렌더링을 방지하기 위한 것이 바로 React.memo이다.
ex)
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" />
</>
)
}
이제 ParentComponent에서 아무리 state가 변경돼도 ChildComponent는 다시 렌더링되지 않는다.
컴포넌트를 값으로 기억한다는 점에서 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}
</>
)
}
이 경우 JSX 함수 방식이 아닌 {}을 사용한 할당식을 사용하는 차이가 있고, 이는 혼선을 야기할 수 있으므로 memo를 사용하는 편이 좋다.
고차 함수의 사전적 정의는 '함수를 인수로 받거나 결과로 반환하는 함수'라고 정의돼 있다.
Array.prototype.map이 대표적이다. forEach, reduce 등도 고차 함수임을 알 수 있다.
실제 고차함수를 만들어보면 다음과 같다.
ex)
function add(a) {
return function (b) {
return a+b
}
}
const result = add(1) //여기서 result는 앞서 반환한 함수를 가리킨다.
const result2 = result(2) // 비로소 a 와 b를 더한 3이 반환된다.
interface LoginProps {
loginRequired?: boolean
}
function withLoginComponent<T>(Component: ComponentType<T>) {
return function (props: T & LoginProps) {
const { loginRequired, ...restProps } = props
if (loginRequired) {
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} />
}
이렇게 하면 isLogin을 통해 인증처리를 할 수 있고, 고차컴포넌트인 withLoginComponent가 이를 처리해준다.
물론 이러한 인증 처리는 서버나 NGINX와 같이 자바스크립트 이전 단계에서 처리하는 편이 훨씬 효울적이다.
useEffect, useState와 같이 리액트에서 제공되는 훅으로만 공통 로직을 격리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다.
사용자 정의 훅은 그 자체로는 렌더링에 영향 x 따라서 개발자가 훅을 원하는 방향으로만 사용할 수 있다는 장점이 있다.
함수 컴포넌트의 반환값, 즉 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하자. 고차 컴포넌트는 이처럼 공통화된 렌더링 로직을 처리하기에 매우 훌륭한 방법.
개발하는 애플리케이션의 규모가 커지고, 처리해야 하는 로직이 많아질수록 중복 작업에 대한 고민도 커질 수 밖에 없다.
공통화하고 싶은 작업이 무엇인지, 또 현재 이를 처리해야 하는 상황을 잘 살펴보고 적절한 방법을 고른다면 app 개발이 더 효율적으로 개선될 것이다.