props는 jsx태그에 전달하는 정보이다.
기본적으로 존재하는 html태그에 전달할 수 있는 props는 미리 정의되어있지만, 커스텀 컴포넌트에 대해서는 어떤 props를 전달받을지 설정할 수 있다.
import { ReactNode } from "react";
interface Props {
children: ReactNode;
onSelect: () => void;
isSelected: boolean;
}
export default function TabButton({ children, onSelect, isSelected }: Props) {
console.log("TABBUTTON COMPONENT EXECUTING");
return (
<li>
<button className={isSelected ? "active" : undefined} onClick={onSelect}>
{children}
</button>
</li>
);
}
위의 코드를 보면 컴포넌트를 선언할 때 매개변수로 어떠한 props들을 전달받을지 미리 선언해 둔 것을 볼 수 있다.
해당하는 커스텀 컴포넌트를 호출할 때 다음과 같이 props들을 넘겨줄 수 있다.
<TabButton isSelected={selectedTopic === "components"} onSelect={() => handleSelect("components")}>
Components
</TabButton>
위에서는 구조분해할당을 통해 어떤 props를 전달받을지 미리 설정해두었기 때문에 해당하는 props를 제외한 데이터는 넘겨줄 수 없다.
하지만 spread문법을 통해 props를 전달하게 되면 조금 더 유연하게 전달이 가능하다.
import { ButtonHTMLAttributes, ReactNode } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
isSelected: boolean;
}
export default function TabButton({ children, isSelected, ...props }: Props) {
console.log("TABBUTTON COMPONENT EXECUTING");
return (
<li>
<button className={isSelected ? "active" : undefined} {...props}>
{children}
</button>
</li>
);
}
이전의 코드를 위와같이 변경하게되면 ...props를 통해 받아온 props들을 button태그로 넘겨줄 수 있게 된다.
현재 typescript를 사용하고 있기 때문에 interface Props extends ButtonHTMLAttributes<HTMLButtonElement>를 통해 미리 타입을 설정해주고있는데, extends ButtonHTMLAttributes<HTMLButtonElement>이 부분을 통해 버튼요소에 전달할 수 있는 모든 props들에 대한 타입을 가져와주고있다.
일반적인 태그들은
HTMLAttributes<HTMLElement>를 사용한다.
children property는 특수한 property로 자식을 jsx요소로 전달해주는 프로퍼티이다.
children에는 부모컴포넌트에서 값이나 컴포넌트 여러 콘텐츠 받아올 수 있기 때문에 잘 사용해야한다.
위에서 children 프로퍼티를 통해 jsx요소를 전달할 수 있다고 하였는데, children prop은 특수한 프로퍼티라 한번밖에 사용하지 못한다.
그렇기때문에 wrapper컴포넌트를 선언할때는 특수한 패턴이 사용되게된다.
바로 jsx요소를 children이 아닌 props로 넘겨주는 패턴이다.
import { useState } from "react";
import TabButton from "./TabButton";
import { EXAMPLES } from "../data";
import Section from "./Section";
const Examples = () => {
const [selectedTopic, setSelectedTopic] = useState<"components" | "jsx" | "props" | "state">();
function handleSelect(selectedButton: "components" | "jsx" | "props" | "state") {
// selectedButton => 'components', 'jsx', 'props', 'state'
setSelectedTopic(selectedButton);
// console.log(selectedTopic);
}
console.log("APP COMPONENT EXECUTING");
let tabContent = <p>Please select a topic.</p>;
if (selectedTopic) {
tabContent = (
<div id="tab-content">
<h3>{EXAMPLES[selectedTopic].title}</h3>
<p>{EXAMPLES[selectedTopic].description}</p>
<pre>
<code>{EXAMPLES[selectedTopic].code}</code>
</pre>
</div>
);
}
return (
<Section title="Examples" id="examples">
<menu>
<TabButton isSelected={selectedTopic === "components"} onClick={() => handleSelect("components")}>
Components
</TabButton>
<TabButton isSelected={selectedTopic === "jsx"} onClick={() => handleSelect("jsx")}>
JSX
</TabButton>
<TabButton isSelected={selectedTopic === "props"} onClick={() => handleSelect("props")}>
Props
</TabButton>
<TabButton isSelected={selectedTopic === "state"} onClick={() => handleSelect("state")}>
State
</TabButton>
</menu>
{tabContent}
</Section>
);
};
export default Examples;
위의 코드에서
<menu>
<TabButton isSelected={selectedTopic === "components"} onClick={() => handleSelect("components")}>
Components
</TabButton>
<TabButton isSelected={selectedTopic === "jsx"} onClick={() => handleSelect("jsx")}>
JSX
</TabButton>
<TabButton isSelected={selectedTopic === "props"} onClick={() => handleSelect("props")}>
Props
</TabButton>
<TabButton isSelected={selectedTopic === "state"} onClick={() => handleSelect("state")}>
State
</TabButton>
</menu>
{tabContent}
이 부분을 새로운 컴포넌트인 Tabs로 정의한다고 해보자.
import { ReactNode } from "react";
interface Props {
children: ReactNode;
}
const Tabs = ({ children }: Props) => {
return (
<>
<menu>
<TabButton isSelected={selectedTopic === "components"} onClick={() => handleSelect("components")}>
Components
</TabButton>
<TabButton isSelected={selectedTopic === "jsx"} onClick={() => handleSelect("jsx")}>
JSX
</TabButton>
<TabButton isSelected={selectedTopic === "props"} onClick={() => handleSelect("props")}>
Props
</TabButton>
<TabButton isSelected={selectedTopic === "state"} onClick={() => handleSelect("state")}>
State
</TabButton>
</menu>
{children}
</>
);
};
export default Tabs;
Tabs를 위와 같이 구현하여 <Tabs>{tabContent}</Tabs>이렇게 사용하게될경우 TabButton컴포넌트에 전달해야할 정보들을 다 옮겨와야하며, 재사용이 힘들어진다.
<TabButton isSelected={selectedTopic === "components"} onClick={() => handleSelect("components")}>
Components
</TabButton>
<TabButton isSelected={selectedTopic === "jsx"} onClick={() => handleSelect("jsx")}>
JSX
</TabButton>
<TabButton isSelected={selectedTopic === "props"} onClick={() => handleSelect("props")}>
Props
</TabButton>
<TabButton isSelected={selectedTopic === "state"} onClick={() => handleSelect("state")}>
State
</TabButton>
위의 부분을 children으로 넘겨주려해도 이미 children prop은 사용중이라 추가적으로 사용할 수 없다.
이럴때 jsx요소를 children이 아닌 일반 prop으로 전달하는 패턴을 사용하게된다.
import { ReactNode } from "react";
interface Props {
children: ReactNode;
buttons: ReactNode;
}
const Tabs = ({ children, buttons }: Props) => {
return (
<>
<menu>{buttons}</menu>
{children}
</>
);
};
export default Tabs;
Tabs에서 property로 buttons라는것을 ReactNode타입으로 받아와준 뒤 Tabs컴포넌트에 prop으로 jsx코드를 넘겨주면 된다.
<Tabs
buttons={
<>
<TabButton isSelected={selectedTopic === "components"} onClick={() => handleSelect("components")}>
Components
</TabButton>
<TabButton isSelected={selectedTopic === "jsx"} onClick={() => handleSelect("jsx")}>
JSX
</TabButton>
<TabButton isSelected={selectedTopic === "props"} onClick={() => handleSelect("props")}>
Props
</TabButton>
<TabButton isSelected={selectedTopic === "state"} onClick={() => handleSelect("state")}>
State
</TabButton>
</>
}
>
{tabContent}
</Tabs>
이번에도 역시 Tabs컴포넌트를 수정해볼것이다.
import { ReactNode } from "react";
interface Props {
children: ReactNode;
buttons: ReactNode;
}
const Tabs = ({ children, buttons }: Props) => {
return (
<>
<menu>{buttons}</menu>
{children}
</>
);
};
export default Tabs;
위의 코드를 조금 더 유연하게 만들어볼것이다.
그러기위해 buttons를 감싸고있는 menu태그를 menu로 고정하는것이 아닌 동적으로 받아오려한다.
그렇게하기위해서는 다음과 같이 코드를 수정해주면 된다.
import { ElementType, ReactNode } from "react";
interface Props {
children: ReactNode;
buttons: ReactNode;
ButtonContainer: ElementType;
}
const Tabs = ({ children, buttons, ButtonContainer }: Props) => {
return (
<>
<ButtonContainer>{buttons}</ButtonContainer>
{children}
</>
);
};
export default Tabs;
ElementType을 통해 jsx요소를 ButtonContainer에 동적으로 받아올 수 있게되고 Tabs컴포넌트를 사용할때는 다음과 같이 사용하면 된다.
<Tabs
ButtonContainer={"menu"}
buttons={
<>
<TabButton isSelected={selectedTopic === "components"} onClick={() => handleSelect("components")}>
Components
</TabButton>
<TabButton isSelected={selectedTopic === "jsx"} onClick={() => handleSelect("jsx")}>
JSX
</TabButton>
<TabButton isSelected={selectedTopic === "props"} onClick={() => handleSelect("props")}>
Props
</TabButton>
<TabButton isSelected={selectedTopic === "state"} onClick={() => handleSelect("state")}>
State
</TabButton>
</>
}
>
{tabContent}
</Tabs>
여기서 중요한점은 기본적으로 존재하는 html요소의 경우 ""를 통해 문자열로 넘겨주어야하며, 커스텀컴포넌트의 경우 ""나 <>없이 그대로 넘겨주어야한다.ButtonContainer={Section}