사용자 정보를 제공받는 폼 페이지를 수정 보완해아하는 일이 생겼다. 처음엔 추가되는 디자인을 집어 넣기에 급급했다. 하지만 디자인에는 예외케이스를 위한 다양한 케이스들이 존재했다. 모든 케이스들을 섬세하게 대응하려다보니 코드가 점점 불어날 수 밖에 없었다. 코드길이가 700줄이 넘어가기 시작하나 전부 내가 짠 코드인데도 뭐가 뭔지 이해하려면 너무나 오랜 시간이 필요했다. 오늘 쓴 코드는 내일의 레거시 코드가 된다는 말이 어느 정도 이해가 갔다. 분리가 답이라는 결론에 도달했다.
처음엔 특정 부분을 새로운 파일에 옮기고(=> child component) 연관된 모든 state와 setState를 (모든 컴포넌트를 가진)parent component에서 props로 보내주려고 했다. 하지만 도저히 props가 지저분해지는걸 두고 볼 수 없었다. 이건 아니라는 생각이 들었다. 그래서 다른 개발자분께 SOS를 청했다.
그렇게 forwardRef()
를 적용하게 되었다. 하지만 난 이해를 하고 적용한 게 아니기 때문에, 여기에 정리하며 조금 더 forwardRef()
를 명확히하고 가려고 한다!
forwardRef()
를 사용하기 위해서는 ref
가 뭔지 알아야 한다. 대신 이 글은 forwardRef()
를 위한 것이니 간략하게 하고 넘어가야겠다.
ref : 리액트에서 특정 DOM 요소를 선택하여 접근하기 위해 사용하는 것
이렇게 정의만 보면 당연히 ref를 언제 써야하는지 애매하니, 공식 문서의 글을 보면,
음, 결국 특정 엘리먼트 스타일 변경과 관련이 되어버리는 건가 싶은 생각이 든다. 공식 문서에서의 설명을 조금 더 가져와보면 일반적인 데이터 플로우를 벗어나 직접적으로 자식(ex. 컴포넌트의 인스턴스, DOM 엘리먼트 등)을 수정하기 위해 ref를 제공한다고 한다.
앞서 간략하게 설명한 상황 설명에 덧붙여 보면 결국 필요한 건 부모 컴포넌트에서 자식 컴포넌트에 있는 입력창의 값을 가져오는 것이었다. 다른 예외 케이스를 위한 state들은 모두 자식 컴포넌트에서만 필요했기 때문에 props로 지저분하게 넘길 필요가 없었다. 그렇기 때문에 input에 ref
를 걸어주면 되는 일이었다. 하지만 두 컴포넌트는 서로 다른 파일로 분리가 되어있기 때문에 그냥 ref
를 지정한다고 끝나는 일이 아니었던 것이다. 게다가 함수 컴포넌트에는 ref
가 존재하지 않는다고 하니, forwardRef()
를 써야 원하던 바를 달성할 수 있었다.
먼저 부모컴포넌트에서는 필요한 ref
를 선언한다. 그리고 자식컴포넌트는 forwardRef()
로 감싸는 것이 가장 기본적인 형식이다. 여기서 내가 잠깐 해맸던 것은 forwardRef()
를 props부분만 감쌌다는 것인데, 그러면 안되고 다음과 같이 컴포넌트 전체를 감싸야 제대로 작동한다. 이렇게 감싸고 나면 두 개의 인자를 보낼 수 있게 되고, 첫 번째 인자에는 기존처럼 props를 두 번째 인자에는 ref를 보낼 수 있게 된다.
// 부모 컴포넌트
const parentComponent = () =>{
const ref1 = useRef() as any;
return <childComponent ref={ref1} />
}
// 자식 컴포넌트
const childComponent = forwardRef((props, ref) => {
return <input ref={ref}/>
})
이렇게 하면 부모 컴포넌트에서 선언된 ref이지만 자식 컴포넌트의 특정 요소를 가르키고 있기 때문에 해당 요소의 변화를 들으면서도, 부모 컴포넌트에서 해당 요소로 접근할 수 있게 된다.
문제는 여기서 끝나지 않았다. 난 하나의 ref만 넘겨주는 것이 아닌, 총 3개의 ref를 넘겨주어야했다. 처음엔 다음과 같이 작성했다.
<InputForm ref={ref1, ref2, ref3}/>
그랬더니 계속 다음과 같은 오류를 내며 코드가 동작하지 않았다.
Type '{ ref1: any; ref2: any; ref3: any }' is not assignable to type
'((instance: unknown) => void) | RefObject<unknown> | null | undefined'.
결국 타입 문제라는 건데, 여러 시도를 해봤으나 통하지 않았고 결국 하나로 묶어준 다음 해당 객체의 타입을 any로 지정하니 해결되었다. 사실 타입을 any로 지정하는 건 좋지 않다는 걸 알지만 일단 돌아가는 게 우선이라 생각해서 이렇게 결론내게 되었다.
const formRef = {
ref1,
ref2,
ref3,
} as any;
<InputForm ref={formRef}/>
마지막으로 보낼 때는 묶어서 보냈지만, 받을 때는 따로 받아도 문제 없이 돌아갔다.
const InputForm = forwardRef((props, {ref1, ref2, ref3} : any) => {});
결론적으로 자식 컴포넌트의 입력창의 value가 변경되는 것을 ref1.current.value
의 형식으로 접근할 수 있게 되어 분리가 완성되었다!
React 공식문서 : Ref와 DOM
[React] ref란? - DOM에 직접 접근하기(useRef)
[React] forwardRef 사용법