stackoverflow 클론 코딩 프로젝트를 진행하던 중이였다.
질문 등록 페이지를 만들며 input 또는 editor가 focus 될 경우 해당 컴포넌트로 스크롤될 수 있도록 하는 기능을 만들고 싶었다. 그래서 질문 양식에 따라 input 또는 editor 컴포넌트가 담긴 각각의 custom component에 ref를 주어 각 컴포넌트들이 focus됨에 따라 참조되어 스크롤이 해당 컴포넌트로 이동할 수 있도록 해야겠다~~ 싶었는데 왠걸!! 스크롤 이벤트가 전혀 일어나지 않는 것이였다!
그때 당시에는 이유를 찾지못하고 각 custom component를 div 태그로 감싸 그 div에 ref를 주는 것으로 땜빵(?) 해결을 하였고, 여러가지 구글링을 해보았지만 문제 자체를 파악하지 못해 제대로 된 검색 키워드도 찾지 못한 채 넘어갔었다.
이제서야!!! 오늘 forward ref에 대한 짧은 강의를 듣고 난 후!! 왜 그때 내가 의도한 대로 되지 않았는지 깨달았다.
custom component에 ref를 주었으니 그것은 당연히 props에 포함되는 값이 되었고, 각 custom component는 ref라는 props를 받는 로직도 없었을 뿐더러 그것을 사용하지도 않으니 ref가 undefined가 되면서 의도했던 대로 적용이 안되었을 수 밖에..
그때 당시에 블로그 포스팅으로도 삽질의 기록을 남겨놓았었다...(갑자기 추억돋네)
https://bbeeyaks-moment.tistory.com/209
결론적으로, React 컴포넌트에 ref prop을 넘겨서 그 내부에 있는 HTML 엘리먼트에 접근을 하게 해주려면 forwardRef() 함수를 사용해야하는 것이였다! 이제라도 원인을 알게되어 참 다행이다. 모르고 넘어갈 뻔?!
forwardRef
React에서 특수한 목적으로 사용되기 때문에 일반적인 용도로 사용할 수 없는 prop이 몇 가지 있다. 대표적인 예로 루프를 돌면서 동일한 컴포넌트 여러 번 랜더링할 때 사용하는 key prop이 있고, ref prop도 마찬가지로 HTML 엘리먼트 접근이라는 특수한 용도로 사용되기 때문에 일반적인 prop으로 사용을 할 수 없다.
HTML 엘리먼트가 아닌 React 컴포넌트에서 ref prop을 사용하려면 React에서 제공하는 forwardRef()라는 함수를 사용해야 한다. React 컴포넌트를 forwardRef()라는 함수로 감싸주면, 해당 컴포넌트는 함수는 두 번째 매개 변수를 갖게 되는데, 이를 통해 외부에서 ref prop을 넘길 수 있다.
예시를 살펴보자.
아래처럼 Input 컴포넌트를 forwardRef 함수로 감싸주면 부모 컴포넌트로부터 받아온 ref prop을 사용할 수 있게 된다.
추가적으로 알게 된 useImperativeHandle 훅에 대해서도 간단하게 살펴보자.
useImperativeHandle 훅은 forwardRef 를 사용해 ref를 사용하는 부모 측에서 커스터마이징된 메서드를 사용할 수 있게 해준다. useImperativeHandle 의 첫 번째 인자로는 프로퍼티를 부여할 ref이고, 두 번째 인자는 객체를 리턴하는 함수이다. 이 객체에 추가하고 싶은 프로퍼티를 정의하면 된다.
예시에서는 focus 라는 메서드를 정의하였다. 원래 input 엘리먼트에는 focus라는 메서드가 없지만, useImperativeHandle 에서 정의하면 부모 컴포넌트에서 단순하게 이를 호출하여 사용할 수 있다.
import React, { useRef, useImperativeHandle } from 'react';
import classes from './Input.module.css';
const Input = React.forwardRef((props, ref) => {
const inputRef = useRef();
const activate = () => {
inputRef.current.focus();
};
useImperativeHandle(ref, () => {
return {
focus: activate,
};
});
return (
<div
className={`${classes.control} ${
props.isValid === false ? classes.invalid : ''
}`}
>
<label htmlFor={props.id}>{props.label}</label>
<input
ref={inputRef}
type={props.type}
id={props.id}
value={props.value}
onChange={props.onChange}
onBlur={props.onBlur}
/>
</div>
);
});
export default Input;
이 메서드를 호출하기 위해 부모 컴포넌트에서는 해당 ref.current에 정의된 메서드를 호출하기만 하면 된다.
// 부모가 자식에게 내려줄 ref
const emailInputRef = useRef();
const passwordInputRef = useRef();
const submitHandler = (event) => {
event.preventDefault();
if (formIsValid) {
authCtx.onLogin(emailState.value, passwordState.value);
} else if (!emailIsValid) {
emailInputRef.current.focus();
} else {
passwordInputRef.current.focus();
}
};
// 부모 컴포넌트 코드 일부
<Input
ref={emailInputRef}
id="email"
label="E-Mail"
type="email"
isValid={emailIsValid}
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
<Input
ref={passwordInputRef}
id="password"
label="Password"
type="password"
isValid={passwordIsValid}
value={passwordState.value}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
이렇게 할 때의 장점은 child component에 상태나 로직을 isolate할 수 있다는 점이다. 굳이 Redux나 Context API를 사용할 만큼의 일이 아닐 때 useImperativeHandle을 사용할 수도 있다. 상태나 로직들은 자식 컴포넌트가 갖고 있고, 부모 컴포넌트는 ref.current에서 필요한 프로퍼티를 가져오기만 하면 되기 때문이다.
일반적으로 forwardRef() 함수는 HTML 엘리먼트 대신에 사용되는 최말단 컴포넌트(ex. <Input/>, <Button/>)를 대상으로 주로 사용되며, 유지보수의 용이성을 위해 그 보다 상위 컴포넌트에서는 forwardRef() 함수를 사용하는 것이 권장되지 않는다고 한다.
참고한 블로그)
'온라인 강의(유데미, 인프런 등) > React 완벽 가이드(유데미)' 카테고리의 다른 글
[react & typescript] typescript로 forwardRef 사용하기(feat.'Component definition is missing display name' 에러) (0) | 2023.02.27 |
---|---|
[react & typescript] CRA로 typescript 설정하기(feat. esLint, styled-component) (0) | 2023.02.23 |
[react] context API (0) | 2023.02.22 |
[react] useReducer(feat. useState와 비교) (0) | 2023.02.21 |
[react] useEffect에서 Clean-up 함수 사용하기 (0) | 2023.02.21 |