1. props에 spread operator
// Section.jsx
export default function section({ title, children }) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
// Examples.jsx
import Section from '../Section.jsx'
// 커스텀 컴포넌트
<Section title="Examples" id="examples">
// <h2>가 빠진 내용
</Section>
이런 형태로 쓰게 되면
css에 #examples h2 {
// 속성들
}
이렇게 정의해 놓았기 때문에 css 적용이 빠지게 된다. Example 내부의 Section 컴포넌트에 id는 지정되어 있지만, Section 컴포넌트 자체에는 id가 지정되어 있지 않아서 그렇다.
이 이야기는 리액트 전반에 영향을 끼지는데, props들은 커스텀 컴포넌트에 설정할 때 자동으로 넘어가거나 해당 컴포넌트 속 JSX 코드로 넘어가지 않는다. 다시말해, Example.jsx의 id=examples는 Section의 속성으로 인식되지 않는 것이다.
대신 리액트에서는 요소에 대한 props가 개발자가 설정하는 대로만 적용되고 따로 작성하지 않았는데 자동으로 적용되는 일이 없는 것이다. 당연히 렌더링된 DOM에도 적용되지 않으니 설정해 둔 스타일이 달라진 것이다.
// Section.jsx
export default function section({ title, children, id, className }) {
return (
<section id={id} className={className}>
<h2>{title}</h2>
{children}
</section>
);
}
물론 이런식으로 속성을 넘겨주는 것도 가능하다. 하지만 이렇게 되면 속성이 추가될 때마다 props를 수정해줘야 하고 규모가 커질수록 비효율적이다.
이때 사용하는 패턴이 바로 forwarded props(전달 속성) 혹은 proxy props(대리 속성)이다. props를 구조분해할당 할 때, 사용할 수 있는 JS 내장 문법이다. 바로, 스프레드 연산자(spread operator)이다. 이 Section 컴포넌트에서 사용할 수 있는 모든 props를 모아 props object(속성 개체)로 병합한다.
// Section.jsx
export default function section({ title, children, ...props }) {
return (
<section {...props}>
<h2>{title}</h2>
{children}
</section>
);
}
title과 children과 같이 수동으로 추출하지 않는 속성은 스프레드 연산자의 속성 객체에 들어가서 이 컴포넌트에서 펼쳐진다. 이 패턴은 내용 모두를 감싸는 wrapper conpoenet(래퍼 컴포넌트) 작성 시 유용하다.
2. jsx 슬롯 사용법
<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>
이런 구조를 바꾸기 위해 Tabs.jsx파일을 만들었다.
{tabContent}는 Examples.jsx에서 제어되는 내용이고, Tab 컴포넌트의 목적이 모든 종류의 탭에 적용되어 앱의 다양한 위치에 있는 내용을 제어하는 것이기 때문이다. 그러므로 그 내용은 Tab 컴포넌트가 아닌, 그 컴포넌트가 사용되는 컴포넌트에서 제어하는 것이 좋다.
만약 이런식으로 TabButton을 Tabs로 옮긴다면 selectedTopic과 handleSelect를 props로 전달해야하기 때문에 이런 방법을 선택해서는 안된다.
// Tabs.jsx
export default function Tabs ({ children )} {
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}
</>
}
그 대신 사용할 수 있는 방법은 Example.jsx 파일 내부에 두는 대신에 jsx코드로 Tabs 요소에 전달하는 것이다. 이 동적인 tabContent를 전달하는 것과 같은 원리이다.
// Tabs.jsx
export default function Tabs({ children, buttons, buttonsContainer }) {
const ButtonsContainer = buttonsContainer;
return (
<>
<ButtonsContainer>{buttons}</ButtonsContainer>
{children}
</>
);
}
// Example.jsx
return (
<Section title="Examples" id="examples">
<Tabs
buttonsContainer="menu" // {Section} 같은 커스텀 컴포넌트도 가능
buttons={
<>
<TabButton
isSelected={selectedTopic === "components"}
onClick={() => handleSelect("components")}
>
// 아래의 내용들
커스텀 컴포넌트는 동적인 값이어야 하니 {}사이에 쓰게되고, 내장 요소인 menu같은 경우에는 ""으로 스트링으로 적게된다. buttonsContainer에는 요소 식별자가 포함되어 있기에 가능한 것이다. 속성에 포함되어 있는 buttonsContainer를 바로 쓰게 되면 소문자로 시작하기 때문에 내장요소로 인식하게 되는데, 당연히 저런 이름의 내장요소는 없다.
따라서, 대문자로 시작하는 ButtonsContainer를 정의해서 props로 받은 값을 할당해주고 그 상수를 <>사이에 넣어서 커스텀 컴포넌트로 인식할 수 있게 하는 것이다.
하지만 더 쉬운 방법이 있는데, 상수로 정의하는 대신에 대문자로 시작하는 ButtonsContainer로 받도록 설정하는 것이다.
당연히 Example.js에도 대문자로 바꿔줬다.
// Tabs.jsx
export default function Tabs({ children, buttons, ButtonsContainer = "menu" }) {
// const ButtonsContainer = buttonsContainer;
return (
<>
<ButtonsContainer>{buttons}</ButtonsContainer>
{children}
</>
);
}
ButtonContainer 또한 원래는 menu까지 가져오는 방법을 썼지만 제사용성을 더 높이기 위해 ButtonsContainer라는 props로 전달 받을 수 있게 하였다. 그리고 대부분의 경우 menu로만 사용될 것이기에 기본값을 줬고, Example에서도 빠질수 있다.
return (
<Section title="Examples" id="examples">
<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>
</Section>
);
}
주의점은 jsx코드이기 때문에 div나 <>로 감싸줘야 한다는 것이다. buttons라는 특별한 props로 전달하고 두 개의 슬롯(slot)을 사용하여 탭 컴포넌트의 메인 내용을 children으로 전달하고 버튼 슬롯 또한 커스텀 버튼 속성을 통새 설정할 수 있다. 또한 필요한 만큼 슬롯을 더 설정할 수 있다.
'내용 복습 > Next.js' 카테고리의 다른 글
[React] 상태관리 라이브러리 Recoil 연습 (0) | 2024.04.01 |
---|---|
[React] state(2) (1) | 2024.03.28 |
[React] State 정리(1) (1) | 2024.03.27 |
[React] Fragment에 대해 알아보자 (0) | 2024.03.05 |
[React] props의 원리 (0) | 2024.02.15 |