Skip to main content

서브 컴포넌트를 메인 컴포넌트에 할당시켜보기(Feat. React Compound Component)

import InputText from "@/components/InputText";
import { Box } from "@mui/material";
import React, { useState } from "react";

const MyPage = () => {
const [value, setValue] = useState("");

return (
<Box display="flex" flexDirection="row" gap="16px">
<InputText onChange={(e) => setValue(e.target.value)} value={value} />
<InputText onChange={(e) => setValue(e.target.value)} value={value} />
<InputText.Div>jfidso</InputText.Div>
</Box>
);
};

export default MyPage;

InputText.Div는 뭘까?

컴파운드 컴포넌트 패턴을 이용할 때 주로 사용하는 방식이다.

import { Box, Input, Typography } from "@mui/material";
import React, {
ChangeEventHandler,
ComponentPropsWithRef,
ReactNode,
forwardRef,
useEffect,
useRef,
useState,
} from "react";

interface InputTextProps extends ComponentPropsWithRef<"input"> {
value: string;
onChange: ChangeEventHandler<HTMLInputElement>;
}

// eslint-disable-next-line react/display-name
const InputTextComponent = forwardRef<HTMLDivElement, InputTextProps>(
({ value, onChange, ...rest }, refs) => {
const [isFocused, setIsFocused] = useState(false);
const ref = useRef<HTMLInputElement>(null);
const handlers = {
// onFocus: () => {
// setIsFocused(true);
// console.log("focus");
// },
onClick: () => {
setIsFocused(true);
console.log("cliok", ref.current);
},
onBlur: () => {
setIsFocused(false);

console.log("blur");
},
};

useEffect(() => {
if (isFocused) {
ref.current && ref.current.focus();
} else {
ref.current && ref.current.blur();
}
}, [isFocused]);

return (
<div ref={refs} style={{ width: "100%" }} {...handlers}>
{isFocused ? (
<input
style={{
display: "inline-block",
cursor: "pointer",
minHeight: "30px",
width: "100%",
}}
ref={ref}
value={value as string}
onChange={onChange}
{...rest}
/>
) : (
<div
onClick={() => setIsFocused(true)}
style={{
boxShadow: "2px 0.5px 2px 2px #f2f2f2",
minHeight: "30px",
width: "100%",
display: "inline-block",
cursor: "pointer",
}}
>
<p>{value}</p>
</div>
)}
</div>
);
}
);

interface InputTextDivProps {
children: ReactNode;
}

const InputText = Object.assign(InputTextComponent, {
Div: ({ children }: InputTextDivProps) => {
return (
<div style={{ padding: "16px", border: "1px solid red" }}>{children}</div>
);
},
});

export default InputText;

사용될 때는 기존 컴포넌트처럼 export default한 InputText를 import해서 사용한다. 만약 Div에 해당하는 컴포넌트를 사용할 경우 InputText.Div처럼 property 접근 방식으로 접근하면된다.

Object.assign을 이용해서 Div를 InputTextComponent의 키로 등록했기 때문이다.

Object.assign이 하는 역할은 무엇인가?

Object.assign을 사용하는 이유는 React 컴포넌트에 추가적인 속성을 할당하기 위해서입니다.

이 방법은 React 컴포넌트를 확장하거나 추가적인 기능을 부여할 때 유용합니다. 특히, 컴포넌트에 정적 속성을 추가하고자 할 때 자주 사용됩니다.

InputText.Div를 직접 할당할 때 타입 에러가 발생하는 이유는 TypeScript가 InputText를 React.FunctionComponent 또는 React.ForwardRefExoticComponent로 인식하기 때문입니다.

이러한 컴포넌트 타입들은 기본적으로 추가적인 속성을 가질 수 없도록 설계되어 있습니다. 즉, 이들은 React 요소를 생성하는 함수로만 인식되며, 추가적인 속성을 가지는 객체로 인식되지 않습니다.

반면에 Object.assign을 사용하면, TypeScript는 이를 새로운 객체를 생성하는 것으로 인식합니다. Object.assign은 첫 번째 인자로 주어진 객체에 나머지 인자들의 속성을 복사하여 추가합니다.

이 과정에서 TypeScript는 새로운 객체가 생성되고, 이 객체에는 원래 컴포넌트의 모든 속성과 추가적으로 정의된 속성들이 포함된다고 인식합니다.

따라서, Object.assign을 사용하면 원래의 컴포넌트 타입을 유지하면서도 추가적인 속성을 가진 새로운 객체를 생성할 수 있습니다. 이 방법은 TypeScript의 타입 시스템 내에서 안전하게 컴포넌트를 확장할 수 있는 방법을 제공합니다.