명령형 상호 작용
이번에 다뤄볼 내용은 상위 component와 명령형으로 상호 작용할 수 있는 방법에 대해서 다뤄보고자 한다.
즉, 어떤 state를 전달해서 상위 component에서 무언가를 변경하는 방식이 아니라 하위 component 내부에서 함수를 호출하는 방식이다.
물론, 명령형 접근 방식은 일반적인 React 패턴에 반하는 행위이기 때문에 자주 사용할 필요도 없겠지만 자주 사용해서도 안될 것이다.
여기서 일반적인 React 패턴은 선언형 접근 방식을 말하는 것으로,
기존에 JavaScript로 세세하게 작성해야했던 코드들을 React를 사용함으로써 해결할 수 있었다.
다시 말해, 명령형 접근 방식을 사용한다는 것은 JavaScript로 코딩한다는 것을 의미하며,
React를 사용하는 이유를 상실한 것과 마찬가지이다.
Scenario
한 가지 예를 들어보자, 아래 코드는 로그인 페이지를 구성하는 form이다.
사용자로부터 email과 password를 받아 유효한지를 확인하는 기능을 가진다.
useRef()
를 사용하여 버튼이 눌렸을 때, 유효하지 않은 입력창으로 focus
해주는 기능을 만들고 싶다.
일반적인 HTML 내장 함수같은 경우에는 아래와 같이 useRef()
을 사용하여 구현할 수 있다.
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 if (passwordIsValid) {
passwordInputRef.current.focus() ;
}
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div className={`${classes.control} ${emailIsValid && classes.invalid}`}>
<label htmlFor="email">E-mail</label>
<input
ref={emailInputRef}
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
...
하지만 사용자 지정 component라면 어떨까?
// 📂 In Login.js
// input element 대신에 쓰던 component
<Input
isValid={emailState.isValid}
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
>
E-Mail
</Input>
// 📂 In Input.js
import React, { useRef, useEffect } from "react";
import classes from "./Input.module.css";
const randomId = Math.random();
const Input = (props) => {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus() ;
}, []) ;
return (
<div
className={`${classes.control} ${props.className} ${
props.isValid !== undefined && !props.isValid && classes.invalid
}`}
>
<label htmlFor={props.id || randomId}>{props.children}</label>
<input
ref={inputRef}
type={props.type || "text"}
id={props.id || randomId}
value={props.value}
onChange={props.onChange}
onBlur={props.onBlur}
/>
</div>
);
};
export default Input;
위의 예제와 같이 내부 component에서 uesRef()를 사용한다고 할 때, 원하는 focus
를 얻을 수 없었다.
email과 password가 모두 렌더링되기 때문에 내부 component에서는 focus
의 우선순위가 밀린다.
왜냐하면, 내부 component는 일시적으로 focus
가 맞춰졌기 때문에 끝까지 focus
가 유지되는 것은 가장 최신의 state일 것이다.
그리고 그것은 내부 component에서 구현한 focus
가 아닌 상위 component에서 가지고 있던 focus
다.
이런 것을 원하는 것이 아니다.
우선, Input component에서 아래와 같이 activate라는 함수를 만들어주었다.
// 📂 In Input.js
import React, { useRef } from "react";
import classes from "./Input.module.css";
const randomId = Math.random();
const Input = (props) => {
const inputRef = useRef();
const activate = () => {
inputRef.current.focus() ;
} ;
return (
<div
className={`${classes.control} ${props.className} ${
props.isValid !== undefined && !props.isValid && classes.invalid
}`}
>
<label htmlFor={props.id || randomId}>{props.children}</label>
<input
ref={inputRef}
type={props.type || "text"}
id={props.id || randomId}
value={props.value}
onChange={props.onChange}
onBlur={props.onBlur}
/>
</div>
);
};
export default Input;
이 activate 함수를 내부가 아닌 외부에서 호출하려고 한다.
보통 React에서 데이터는 props와 state로 작업하는 방식을 사용하기 때문에, 이런 식의 코딩은 흔치 않는 방법이다.
// 📂 In Login.js
const emailInputRef = useRef();
const passwordInputRef = useRef();
const submitHandler = (event) => {
event.preventDefault();
if (formIsValid) {
// 로그인 해주는 함수
authCtx.onLogin(emailState.value, passwordState.value);
} else if (!emailIsValid) {
emailInputRef.current.activate() ;
} else {
passwordInputRef.current.activate() ;
}
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<Input
ref={emailInputRef}
isValid={emailState.isValid}
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
>
E-Mail
</Input>
<Input
ref={passwordInputRef}
isValid={passwordState.isValid}
type="password"
id="password"
value={passwordState.value}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
>
Password
</Input>
<div className={classes.actions}>
<Button type="submit" className={classes.btn}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
그리고 Login component에서 useRef()
를 사용해서 activate 함수를 불러와서 쓸 수 있지 않을까?
그런데 실행이 안된다. console을 보면 error가 출력된 것을 볼 수 있다.
Error Occurred
Warning: Function components cannot be given refs.
Attempts to access this ref will fail.
Did you mean to use React.forwardRef()?
Check the render method of `Login`.
at Input (http://localhost:3000/static/js/bundle.js:794:65)
at form
at div
at Card (http://localhost:3000/static/js/bundle.js:722:90)
at Login (http://localhost:3000/static/js/bundle.js:259:88)
at main
at App (http://localhost:3000/static/js/bundle.js:35:64)
at AuthContextProvider (http://localhost:3000/static/js/bundle.js:966:86)
error 코드를 해석하자면,
Function component에게는 ref을 줄 수가 없다.
이 참조에 액세스하려는 시도는 실패했다.
React.forwardRef()을 사용하려고 했니?
일반적인 경우 사용자 지정 component는 ref를 받을 수 없다.
때문에 내부적으로 ref
props를 가지고 아무런 동작도 수행하지 않는 것이다.
ref
는 일종의 예약어로 이런 방식으로 접근하면 안된다.
(그리고 error 코드를 자세히 보면 React.forwardRef()
에 대해서 언급하고 있다.)
useImperativeHandle, forwardRef
하지만 동작시킬 수 있는 방법이 아주 없는 것은 아니다.
error 코드에서도 나와있듯이 React.forwardRef()
를 사용하면 된다.
그 전에 useImperativeHandle()
에 대해서 먼저 알아보자.
useImperativeHandle()
hook은 component나 component 내부에서 오는 기능들을 명령적으로 수행할 수 있게 만들어준다.
즉, 일반적인 state props 관리를 통하지 않고, 부모 component의 state를 통해 component를 제어하지 않고
프로그래밍적으로 component에서 무언가 직접 호출하거나 조작하게 해준다.
useImperativeHandle(, () => {
return {
activate: activate,
};
});
우선, useImperativeHandle()
의 두번째 parameter는 객체를 return
하는 함수이다.
그 객체는 외부 component에서 사용할 수 있는 모든 데이터를 포함한다.
이와 같이 객체는 특정 함수나 변수를 외부에서 접근할 수 있어야 하는 것을 가리킨다.
기본적으로 내부 component와 외부 component 사이를 통역해주는 객체이다.
물론, 두번째 parameter만으로 동작하지 않는다.
첫번째 parameter로 넣어줘야하는데 그것은 component 함수의 ref
parameter로 얻을 수 있다.
하지만 ref
를 parameter로 설정하는 경우에는 설정을 확실히 하기 위해 특별한 방법으로 내보낼 필요가 필요하다.
React에서 제공하는 React.forwardRef()
라는 함수인데, component 함수는 React.forwardRef()
의 첫번째 인수로 들어간다.
그리고 React component를 return
함으로 여전히 React component로 사용할 수 있다.
const Input = React.forwardRef((props, ref) => {
...
}) ;
최종 코드
// 📂 In Input.js
import React, { useRef, useImperativeHandle } from "react";
import classes from "./Input.module.css";
const randomId = Math.random();
const Input = React.forwardRef((props, ref) => {
const inputRef = useRef();
const activate = () => {
inputRef.current.focus();
};
useImperativeHandle(ref, () => {
return {
activate: activate,
};
});
return (
<div
className={`${classes.control} ${props.className} ${
props.isValid !== undefined && !props.isValid && classes.invalid
}`}
>
<label htmlFor={props.id || randomId}>{props.children}</label>
<input
ref={inputRef}
type={props.type || "text"}
id={props.id || randomId}
value={props.value}
onChange={props.onChange}
onBlur={props.onBlur}
/>
</div>
);
});
export default Input;
// 📂 In Login.js
const emailInputRef = useRef();
const passwordInputRef = useRef();
const submitHandler = (event) => {
event.preventDefault();
if (formIsValid) {
// 로그인 해주는 함수
authCtx.onLogin(emailState.value, passwordState.value);
} else if (!emailIsValid) {
emailInputRef.current.activate() ;
} else {
passwordInputRef.current.activate() ;
}
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<Input
ref={emailInputRef}
isValid={emailState.isValid}
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
>
E-Mail
</Input>
<Input
ref={passwordInputRef}
isValid={passwordState.isValid}
type="password"
id="password"
value={passwordState.value}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
>
Password
</Input>
<div className={classes.actions}>
<Button type="submit" className={classes.btn}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
Conclusion
이와 같은 방법으로 React component에서 온 기능을 노출하여 부모 component에 연결한다.
그 다음, 부모 component 에서 ref
를 통해 그 component의 여러 기능들을 트리거할 수 있다.
이런 방법으로 ref
를 사용하면 가능하다. 하지만 항상 이렇게 해야한다는 것은 절대 아니다. 가능하면 반드시 피하는 것이 좋다.
그러나 이와 같이 focus
나 scroll
과 같은 사례에서는 매우 유용하게 사용 가능하다.
실제 적용하기에는 매우 힘들 것 같다는 생각이 든다.
다양한 방법이 있다는 것을 알게 되었고 상황에 알맞게 적용할 수 있게 익히는 것이 필요해 보인다.