영어에서의 Composition
= 구성
리액트에서의 Composition
= 합성
= 여러 개의 컴포넌트를 합쳐서 새로운 컴포넌트를 만드는 것
이 페이지에서 A라는 컴포넌트와 B라는 컴포넌트가 반복적으로 나오는 것을 볼 수 있습니다.
또한 이 페이지 자체도 하나의 리액트 컴포넌트입니다. 결국 이 페이지는 컴포넌트 A와 B를 합쳐서 페이지 컴포넌트를 만든 것이기 때문에 합성을 사용했다고 볼 수 있습니다.
리액트로 개발을 하다 보면 이처럼 여러 개의 컴포넌트를 합쳐서 새로운 컴포넌트를 만드는 일이 굉장히 많습니다. 그래서 합성은 리액트 전반에 걸쳐서 굉장히 많이 사용되는 방법이기 때문에 잘 알고 있는 것이 좋습니다. 합성이라고 해서 무작정 그냥 컴포넌트들을 붙이는 것이 아니라 여러 개의 컴포넌트를 어떻게 조합할 것인가에 대한 고민이 필요합니다. 조합 방법에 따라 합성의 사용 기법이 나뉘는데 대표적인 합성 사용 기법에 대해서 하나씩 배워 보도록 하겠습니다.
= 하위 컴포넌트를 포함하는 형태의 합성 방법
Sidebar나 Dialog 같은 Box 형태의 컴포너트는 자신의 하위 컴포넌트를 미리 알 수 없습니다.
예를 들어 동일한 사이드바 컴포넌트를 사용하는 두 개의 쇼핑몰이 있다고 가정해 보겠습니다. 하나의 쇼핑몰에는 의류와 관련된 메뉴가 8개 들어 있고 다른 쇼핑몰에는 식료품과 관련된 메뉴가 10개 존재합니다. 사이드바 컴포넌트 입장에서는 자신의 하위 컴포넌트로 어떤 것들이 올지 알 수 없겠죠? 해당 컴포넌트를 사용하는 개발자가 어떤 것을 넣느냐에 따라 하위 컴포넌트가 달라지기 때문입니다.
그렇기 때문에 이런 경우에는 Containment 방법을 사용하여 합성을 사용하게 됩니다. Containment를 사용하는 방법은 리액트 컴포넌트의 props에 기본적으로 들어 있는 children 속성을 사용하면 됩니다.
function FancyBorder(props) {
return (
<div className={`FancyBorder FancyBorder-` + props.color}>
{props.children}
</div>
);
}
위 코드에는 FancyBorder라는 굉장히 간단한 컴포넌트가 나옵니다. props.children을 사용하면 해당 컴포넌트의 하위 컴포넌트가 모두 children으로 들어오게 됩니다. children이라는 prop은 개발자가 직접 넣어주는 것x, 리액트에서 기본적으로 제공해주는 것.
앞에서 리액트의 createElement() 함수에 대해서 배울 때 아래와 같은 형태로 호출했었습니다.
React.createElement(
type,
[props],
[...children]
)
여기에서 세 번째에 들어가는 파라미터가 바로 children입니다. children이 배열로 되어 있는 이유는 여러 개의 하위 컴포넌트를 가질 수 있기 때문입니다. 결과적으로 FancyBorder 컴포넌트는 자신의 하위 컴포넌트를 모두 포함(Containment)하여 예쁜 테두리Border로 감싸주는 컴포넌트가 됩니다. 실제로 FancyBorder 컴포넌트를 사용하는 예제를 볼까요?
functino WelcomeDialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
어서오세요
</h1>
<p className="Dialog-message">
우리 사이트에 방문하신 것을 환영합니다!
</p>
</FancyBorder>
);
}
위 코드에서는 WelcomeDialog라는 컴포넌트가 나오고 여기에서 FancyBorder 컴포넌트를 사용하고 있습니다. FancyBorder 컴포넌트로 감싸진 부분 안에는 <h1> 과 <p> 이렇게 두 개의 태그가 들어 있습니다. 이 두 개의 태그는 모두 FancyBorder 컴포넌트에 children이라는 이름의 props로 전달됩니다. 결과적으로 파란색의 테두리로 모두 감싸지는 결과가 나오겠죠.
리액트에서는 props.children를 통해 하위 컴포넌트를 하나로 모아서 제공해줍니다 그렇다면 여러 개의 children 집합이 필요한 경우는 어떻게 해야 할까요? 이런 경우에는 별도로 props를 정의해서 각각 원하는 컴포넌트를 넣어 주면 됩니다. 아래 예제 코드를 봅시다.
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App(props) {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
}
/>
);
}
먼저 위의 코드에는 화면을 왼쪽과 오른쪽으로 분할해서 보여주는 SplitPane이라는 컴포넌트가 있습니다. 그리고 아래쪽에 나와있는 App 컴포넌트에서는 이 SplitPane 컴포넌트를 사용하고 있는데 여기에서 left, right라는 두 개의 props를 정의하여 그 안에 각각 다른 컴포넌트를 넣어주고 있습니다. SplitPane에서는 이 left, right를 props로 받게 되고 각각 화면의 왼쪽과 오른쪽에 분리해서 렌더링하게 됩니다. 이처럼 여러 개의 children 집합이 필요한 경우에는 별도의 props를 정의해서 사용하면 됩니다.
지금까지 살펴 본 것처럼 props.children이나 직접 정의한 props를 이용하여 하위 컴포넌트를 포함하는 형태로 합성하는 방법을 Containment라고 합니다.
전문화, 특수화
웰컴다이얼로그(WelcomeDialog)는 다이얼로그(Dialog) 의 특별한 케이스이다.
다이얼로그라는 것은 굉장히 범용적인 의미를 갖고 있습니다. 모든 종류의 다이얼로그를 다 포함하는 개념이라고 볼 수 있죠
반면에 '웰컴'다이얼로그는 누군가를 반기기 위한 다이얼로그라고 볼 수 있습니다.
범용적인 의미가 아니라 좀 더 구체화된 것이죠.
이처럼 범용적인 개념을 구별이 되게 구체화하는 것을 Specialization이라고 합니다.
객체지향에 대해서 배운 분들은 알고 있겠지만 기존의 객체지향 언어에서는 상속을 사용하여 Specialization을 구현합니다.
하지만 리액트에서는 합성을 사용하여 Specialization을 구현하게 됩니다.
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
);
}
function WelcomeDialog(props) {
return (
<Dialog
title="어서오세요"
message="우리 사이트에 방문하신 것을 환영합니다!"
/>
);
}
위 코드에는 먼저 Dialog라는 범용적인 의미를 가진 컴포넌트가 나옵니다. 그리고 이 Dialog 컴포넌트를 사용하는 WelcomeDialog 컴포넌트가 나옵니다. Dialog 컴포넌트는 title과 message라는 두 가지 props를 갖고 있는데 각각 다이얼로그에 나오는 제목과 메시지를 의미합니다. 그래서 제목과 메시지르 어떻게 사용하는냐에 따라서 경고 다이얼로그가 될 수도 있고, 인사말 다이얼로그가 될 수도 있습니다. WelcomeDialog 컴포넌트에는 제목을 '어서 오세요'라고 짓고 접속한 사용자에게 인사말을 하는 다이얼로그를 만들었습니다.
지금까지 살펴 본 것처럼 Specialization은 범용적으로 쓸 수 있는 컴포넌트를 만들어 놓고 이를 특수화 시켜서 컴포넌트를 사용하는 합성 방식입니다.
Containment를 위해서 props.children을 사용하고
Specialization을 위해 직접 정의한 props를 사용하면 될 것 같습니다.
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}
function SignUpDialog(props) {
const [nickname, setNickname] = useState('');
const handleChange = (event) => {
setNickname(event.target.value);
}
const handleSignUp = () => {
alert(`어서 오세요, ${nickname}님!`);
}
return (
<Dialog
title="화성 탐사 프로그램"
message="닉네임을 입력해 주세요."> //Specialization
<input
value={nickname}
onChange={handleChange} /> //Containment
<buttion onClick={handleSignUp}
가입하기
</button> //Containment
</Dialog>
);
}
위 코드에 나오는 Dialog 컴포넌트는 이전에 나왔던 코드와 거의 비슷한데 Containment를 위해 끝부분에 props.children를 추가했습니다. 이를 통해 하위 컴포넌트가 다이얼로그 하단에 렌더링됩니다.
실제로 Dialog 컴포넌트를 사용하는 SignUpDialog 컴포넌트를 살펴보면 Specialization을 위한 props인 title, message에 값을 넣어 주고 있으며 사용자로부터 닉네임을 입력받고 가입하도록 유도하기 위해 <input> 과 <button> 태그가 들어 있습니다. 이 두 개의 태그는 모두 props.children으로 전달되어 다이얼로그에 표시됩니다. 이러한 형태로 Containment와 Specialization을 동시에 사용할 수 있습니다.
지금까지 리액트의 컴포넌트 합성을 위한 두 가지 방법인 Containment와 Specialization에 대해서 배워 보았습니다. 각 방법을 따로 사용하거나 또는 동시에 함께 사용하면 다양하고 복잡한 컴포넌트를 효율적으로 개발할 수 있습니다.
합성과 대비되는 개념으로 상속(Inheritance)이 있습니다. 영단어 Inheritance는
상속
이라는 뜻을 갖고 있습니다. 일상생활에서 상속이라는 말은 부모님이 죽고나서 자식에게 자산을 물려줄 때 사용하는 개념입니다. 다만 컴퓨터 프로그래밍에서의 상속은 객체지향 프로그래밍에서 나온 개념입니다. 부모 클래스를 상속받아서 새로운 자식 클래스를 만든다는 개념으로 자식 클래스는 부모 클래스가 가진 변수나 함수 등의 속성을 모두 갖게 됩니다.
리액트에서는 다른 컴포넌트로부터 상속받아서 새로운 컴포넌트를 만드는 것을 고려해 볼 수 있습니다. 리액트를 개발한 메타는 수천 개의 리액트 컴포넌트를 사용한 경험을 바탕으로 추천할 만한 상속 기반의 컴포넌트 생성 방법을 찾아보려 했으나 그러지 못했다고 합니다. 결국 리액트에서는 상속이라는 방법을 사용하는 것보다는 앞에서 배운 합성을 사용해서 개발하는 것이 더 좋은 방법입니다. 따라서 결론은 다음과 같습니다.
복잡한 컴포넌트를 쪼개 여러 개의 컴포넌트로 만들고, 만든 컴포넌트들을 조합하여 새로운 컴포넌트를 만들자!