프론트엔드 아키텍처: 컴포넌트를 분리하는 기준과 방법

이문기
21 min readFeb 18, 2023

--

Photo by Amélie Mourichon on Unsplash

컴포넌트 분리

컴포넌트는 어떤 문맥이든 가장 작은 단위로서 언급됩니다.¹ 프로그램이나 모듈의 구성 단위로서도 그러합니다.² 그렇기 때문에 컴포넌트라고 하면 프로그램 언어 수준이 아닌 소프트웨어 디자인 수준에서 나눌 수 있는 가장 작은 단위를 의미합니다. 이 의미를 프론트엔드 라이브러리 또는 프레임워크에 적용하면, 웹 앱을 구성 하는 데 있어 가장 작은 단위가 됩니다. 실제로 우리가 많이 사용하는 React를 살펴보면 웹 앱을 구성하는 가장 작은 단위는 컴포넌트 입니다. 페이지를 만드는 것도 컴포넌트이고 아주 작은 요소를 만드는 것도 컴포넌트 입니다. 그렇기 때문에 우린 컴포넌트를 언제 그리고 어떻게 더 작은 단위로 나누어야 하는지 고민하게 됩니다. 그 과정에서 나온 Presentational Components와 Container Components는 가장 유명한 기준 중 하나이고 Atomic Design의 응용이 사용되고 있기도 합니다. 그럼에도 불구하고 컴포넌트를 언제 그리고 어떻게 나눠야 하는지 막막한 경우가 많습니다. 왜냐하면 종종 언급되곤 하는 방법들은 이해하기 쉽지만 적용하기가 어렵고 예외적인 상황이 많이 발생하기 때문입니다. 결국 적용하는 데 있어서 일관성이 깨지고 어느새 앱은 유지보수하기에 충분히 복잡해져 있습니다.

이 글은 컴포넌트의 분리라는 문제에 대해 현재 제가 갖고 있는 아이디어를 소개합니다. 따라서 시간이 지나보니 잘못 되었거나 더 개선될 수 있습니다. 그렇기 때문에 이 글은 기록 그리고 공유에 초점을 맞췄습니다.

전 프론트엔드 개발이 어렵다고 생각합니다. 마땅히 정해진 규칙은 없지만 갈수록 복잡성은 커져갑니다. 빠르게 변하는 기술이 프론트엔드 개발을 이해하는 데 어렵게 만든다는 건 이제 새삼스럽지도 않습니다. 프론트는 기획과 디자인, 백엔드와 인프라 등 모든 방법과 기술을 조합한 결과가 보여지는 곳입니다. HTML과 CSS, Javscript 그리고 React로 조금만 시간을 들이면 화면에 보이는 이쁘고 잘 동작하는 요소들을 만들던 프론트엔드는 갈수록 다루기 어려워지는 느낌입니다. 그렇기 때문에 잘 만들어야 합니다.

그럼 프론트엔드 개발을 잘 하기위한 방법 중 하나인 컴포넌트 분리에 대해 살펴보겠습니다.

언제 나눠야 할까?

컴포넌트를 만드는 기준, 즉 나누는 기준 중 가장 많이 선택되는 이유는 재사용성과 복잡성입니다.

코드가 재사용 될 가능성이 있다면 새로운 컴포넌트를 만드는 건 좋은 생각입니다. 코드가 복잡하다면 가독성을 개선하고 유지보수 할 수 있게 만들기 위해 컴포넌트를 분리할 수 있습니다.³

컴포넌트는 UI를 독립적이고 재사용 가능한 조각으로 나눌 수 있습니다. 그리고 각 조각을 독립적으로 고려할 수 있습니다.⁴

재사용 가능한 컴포넌트를 만들어 놓는 것은 더 큰 앱에서 작업할 때 두각을 나타냅니다. UI 일부가 여러 번 사용되거나 UI 일부가 자체적으로 복잡한 경우에는 별도의 컴포넌트로 만드는 게 좋습니다.⁵

컴포넌트를 만들 때 가장 많이 발생하는 실수 다섯가지⁶
1. 복잡한 컴포넌트를 만든다.
2. 하나의 컴포넌트에 여러 책임을 추가한다.
3. 몇몇 동작하는 부분을 결합하여 컴포넌트를 만든다.
4. 비지니스 로직을 컴포넌트에 추가한다.

아래에서는 이 내용들을 더 자세하게 살펴보고 분리하는 이유와 방법을 정리해보려고 합니다.

A. 재사용 가능한 컴포넌트

A-1. 재사용 가능하다는 것은 일반적이라는 것

재사용이 가능하다는 건 그만큼 일반적이라는 걸 의미합니다. 예를 들어보면, ‘탈것’ 이라는 건 ‘자동차’, ‘자전거’ 보다 일반적입니다. 이것은 포함 관계로 봤을 때 포괄적이라는 걸 의미합니다.

그리고 ‘탈것’은 조금 더 보편적인 속성을 갖습니다.

컴포넌트로 예를 들어보면, 두 개의 버튼 컴포넌트가 아래와 같이 있을 때

function Button(...) {
return <button>...</button>
}

function ButtonWithIcon(...) {
return (
<div>
<i>...</i>
<button>...</button>
</div>
);
}

<button>…</button> 은 모든 버튼을 포함하는 포괄적인 개념입니다. 즉, <button>…</button>이라는 요소는 ButtonButtonIcon을 모두 포함합니다. 그리고 <button>…</button>ButtonButtonWithIcon 보다 보편적인 속성을 갖습니다. 그렇기 때문에 ‘버튼’이라는 기능을 사용해야 하는 곳이라면 <button>…</button>이 가장 일반적이기 때문에 어디에든 사용할 수 있고 재사용 가능합니다. 반면 ButtonWithIcon은 그렇지 않습니다.⁷

우리는 컴포넌트의 재사용성을 고려할 때 일반적이라는 두 가지 측면 중 ‘속성이 보편적’인지 고민합니다. 즉, ‘다른 컴포넌트가 가져가서 사용할 수 있도록 보편적인 속성을 갖고 있는가?’를 고려합니다. 마치 아래 그림에서 빨간색으로 칠해진 부분을 만들어야 합니다.

재사성을 높이려면 더 많은 컴포넌트를 만족시켜야 하고 더욱 일반적이어야 합니다

그렇다면 어떻게 재사용 가능한 컴포넌트를 만들 수 있을지 구체적으로 살펴보겠습니다.

A-2. HTML 요소 측면에서의 재사용성

컴포넌트를 만드는 데 너무 집중을 하다보면 지금 다루고 있는게 HTML을 포함하고 있다는 사실을 까먹을 때가 종종 있습니다. 그래서 컴포넌트를 분리할 때 HTML 요소 측면에서 고려해야할 것을 놓칠 때가 있습니다.

function ListComponent(...) {
return (
<ul>
<li>
<h3>...</h3>
<p>...</p>
</li>
<li>
<h3>...</h3>
<p>...</p>
</li>
</ul>
);
}

이런 형태의 컴포넌트가 있고 분리할 필요가 있다고 가정해보겠습니다. 분리한 컴포넌트는 어떻게 생겨야 할까요?

function ItemComponent(...) {
return (
<li>
<h3>...</h3>
<p>...</p>
</li>
);
}

// 또는

function SomethingComponent(...) {
return (
<>
<h3>...</h3>
<p>...</p>
</>
);
}

다양한 의견이 있을 수 있지만 개인적으로 SomethingComponent와 같이 분리해야 한다고 생각합니다. ItemComponentul과 같은 리스트 요소에서 사용하도록 강제되는 측면이 있는 반면, SomethingComponent는 리스트 요소 말고 다른 곳에서도 사용할 수 있기 때문입니다. 즉, 조금 더 보편적인 속성을 가져야 하고(li, h3, p가 아닌 h3, p), 포괄적이어야(리스트 아이템이 아니라 다른 무언가) 합니다. (사실 h3도 사용할 수 있는 맥락이 한정적이기 때문에 재사용성을 떨어뜨리는 원인 중 하나 입니다.)

그래서 전 ListComponent를 만들 때 li 요소를 블록으로 간주하지 않는 방법을 선호합니다. 즉, li 요소 바로 아래에 구성하는 게 아니라 다른 적당한 요소를 사용합니다.

function ListComponent(...) {
return (
<ul>
<li>
<!-- 다른 요소를 블록으로 활용합니다. -->
<section>
<h3>...</h3>
<p>...</p>
</section>
</li>
...
</ul>
);
}

그리고 요소의 구조를 잡을 때 BEM의 개념을 사용하곤 합니다. 왜냐하면 BEM의 Block은 독립적인 재사용성을 고려해야 하기 때문이고 이 개념은 컴포넌트를 분리할 때 기준으로서 도움이 됩니다.

따라서 컴포넌트를 분리할 때 HTML 요소들이 주변 문맥에 크게 의존적이지 않을수록 좋습니다.

A-3. 중복을 고려한 재사용성

우리가 컴포넌트의 재사용성에 대해 말할 땐 보통 중복을 떠올립니다. 두 곳 이상에서 중복된 무언가 존재하고 ‘추출’하는 과정을 거칩니다. 그리고 추출한 것을 재사용합니다. (추출이란 단어를 사용한 이유는 컴포넌트로 분리하는 과정이 이미 존재하는 컴포넌트에서 중복된 걸 골라내는, 즉 추출하는 과정과 비슷하기 때문입니다.)

function Page1() {
return (
<ul>
<li>
<section>
<h3>...</h3>
<p>가격...</p>
<p>요약...</p>
</section>
</li>
</ul>
);
}

function Page2() {
return (
<ul>
<li>
<section>
<h3>...</h3>
<p>가격...</p>
</section>
</li>
</ul>
);
}

이렇게 두 페이지에서 비슷한 형태의 요소 구성을 사용한다면 아래와 같이 컴포넌트로 추출 합니다.

function Page1() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}

function Page2() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}

그리고 추출한 Card 컴포넌트는 아래와 같이 만들어집니다.

function Card(props) {
return (
<section>
<h3>...</h3>
<p>가격...</p>
{props.showSummary && <p>요약...</p>}
</section>
);
}

이렇게 둘 이상의 컴포넌트에서 사용할 재사용 가능한 컴포넌트를 만들 때 가장 큰 특징 중 하나는 조건문 입니다. 완벽하게 같은 걸 사용하면 문제가 안 되지만 서로 다른 부분이 있다면 조건문이 들어가게 됩니다. 지금은 아주 간단한 컴포넌트라 문제가 잘 드러나지 않지만, 우리가 현실에서 마주하는 재사용 컴포넌트는 아래와 같이 점점 거대해지곤 합니다.

// 추출 후 1달
function Card(props) {
const [a, setA] = useState(props.a ? props.foo : props.bar);
const condition1 = props.a && !props.b;

return (
<section>
<h3>...</h3>
<p>가격...</p>
<div>
<button>{a ? 'fooValue' : 'barBalue'}</button>
</div>
{props.showSummary && <p>요약...</p>}
{condition1 && <div>...</div>}
</section>
);
}

// 더 미래는 생략...

이렇게 거대해지는 건 상위 컴포넌트와 하위 컴포넌트가 강하게 결합했기 때문입니다. 처음엔 아래 그림처럼 약하게 결합되었던 컴포넌트들이

시간이 지나면서 아래 그림의 두꺼워진 빨간 선처럼 강하게 결합된 것입니다.

강한 결합은 약한 결합보다 변경이 더 어렵다는 걸 의미합니다. 예를 들어, 처음 만들었을 땐 Card 컴포넌트를 다른 컴포넌트로 쉽게 바꿀 수 있었습니다. 또는 속성명을 쉽게 바꿀 수 있었습니다. 이젠 다른 컴포넌트로 바꾸거나 Card 컴포넌트의 속성 이름, 내부 로직을 바꾸기도 어렵습니다.

그리고 다른 문제도 있습니다. 화살표는 의존성을 의미합니다. Page1 컴포넌트와 Page2 컴포넌트는 Card 컴포넌트에 의존적입니다. 의존적이라는 건 Card 컴포넌트를 수정하면 Page1Page2가 영향을 받기 때문에 혹시 문제가 발생하진 않는지 살펴봐야 한다는 걸 의미합니다.

만약 Page1Page2 뿐만 아니라 더 많은 컴포넌트가 Card 컴포넌트를 사용할수록 문제는 더 심각해집니다. 불행하게도 우린 모두 이런 문제에 익숙합니다. 이런 문제는 왜 발생했을까요? 그리고 어떤 문제를 일으킬까요?

가장 먼저 컴포넌트가 반환하는 요소의 중복을 추출해서 재사용해야 한다는 접근 방법이 문제의 발단일 수 있습니다. 중복 제거와 관련된 DRY 원칙⁸은 중복을 겉모습으로 판단하지 않습니다. 만약 모습이 같은 두 코드가 같은 이유로 수정된다면 그 코드는 중복입니다. 하지만 같은 모습의 코드라도 수정의 이유가 다르다면 두 코드는 중복이 아닙니다.

추출한 컴포넌트 내부에 사용하는 방법에 따라 조건문이 추가된다는 건, 사용하는 컴포넌트들이 서로 다른 수정의 이유를 갖는 다는 걸 의미합니다. 즉, 중복 제거와 재사용의 대상이 아닙니다. 따라서 처음에 조건문이 들어갈 때부터 산불의 작은 불씨가 시작된 것이었습니다.

물론 조건문이 없는 컴포넌트를 만들기는 너무나도 어렵습니다. 단, 조건문을 추가할 때 컴포넌트의 관계에 어떤 영향을 주는지 이해할 필요는 있습니다.

다음으로 의존성입니다. Page1Page2 컴포넌트는 Card 컴포넌트에 의존적입니다. 즉, 여러 컴포넌트의 집합체인 상위 컴포넌트가 재사용을 위한 하위 컴포넌트에 의존적입니다. 이렇게 되면 Page1Page2 컴포넌트와 같은 상위 컴포넌트들은 변경의 이유가 너무 많아집니다. 즉, Page1Card에 대해 수정 요구사항이 들어왔을 때 Card 컴포넌트만 수정하는 게 아니라 Page2 컴포넌트도 수정해야할 수 있습니다.

이 문제들을 해결하는 방법 중 하나는 재사용하려는 컴포넌트에는 정말 공통적인 것들만 남겨두고 사용하는 컴포넌트의 고유한 것은 속성으로 전달하는 것입니다.

function Page1() {
return (
<ul>
<li>
<Card
summary={<p>요약...</p>}
/>
</li>
</ul>
);
}

function Page2() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}

function Card(props) {
return (
<section>
<h3>...</h3>
<p>가격...</p>
{props.summary}
</section>
);
}

이렇게 하면 Page1만의 특징인 summayPage1이 관리하고 Card는 이에 대해 신경 쓸 필요가 없습니다. 이 말은 Page1에서 summary로 전달하는 요소를 수정해도 Page2 컴포넌트를 살펴볼 필요가 없다는 것을 의미합니다. 물론 현실 세계의 컴포넌트는 이렇게 호락호락하지 않습니다. 요소의 위치나 스타일 등의 상호 의존성도 존재할 수 있기 때문입니다. 하지만 상태나 조건문 등의 결합이 사라진 것만으로도 효과를 볼 수 있고, 개발을 하면서 신경쓸 부분이 객관적으로 줄었다는 건 분명 긍정적입니다. 특히나 이 방법은 props drilling을 피하거나 컴포넌트의 제어를 역전하는 등 좋은 점을 더 많이 갖고 있고 공식문서에서도 소개하는 만큼 반드시 숙지하고 있을 필요가 있습니다. 또한 이렇게 속성으로 컴포넌트를 전달해야 하는 이유와 방법을 잘 안내하고 있는 문서 링크를 공유합니다.

React component as prop: the right way

말씀드렸던 것처럼 이 방법은 결합도와 의존성 문제를 해결할 수 있는 방법 중 하나 입니다. 그렇기 때문에 재사용을 위해 컴포넌트를 분리한다면 어떤 점을 주의해야 하는지 따져보고 그에 맞는 해결책을 상황에 맞게 찾아보는 게 정말 중요합니다.

B. 복잡한 컴포넌트

컴포넌트를 나누는 중요한 또 다른 이유 중 하나는 컴포넌트의 복잡성입니다. 컴포넌트가 복잡해지는 이유는 여러가지가 있습니다. 여기에선 두 가지 경우를 살펴보려고 합니다.

B-1. 컴포넌트가 여러 책임을 갖는 경우

여러 책임을 갖는 컴포넌트는 컴포넌트 분리 없이 만든 거대한 페이지 컴포넌트와 비슷합니다.

function Page(props) {
// 선택한 탭을 변경하면 보여주는 내용을 변경합니다.
// 페이징을 다룹니다.
// 단어를 검색을 합니다.
// 검색 조건 토글을 다룹니다.
// 등등
}

이렇게 되면 기능 간에 결합이 강하게 발생해서 수정이 쉽지않습니다. 이건 마치 삼체문제와 비슷해서 서로 상호작용하는 기능이 많아지는 것보다 문제의 복잡성이 더 빨리 어려워진다는 것을 의미합니다.

그렇기 때문에 컴포넌트를 책임에 맞게 나눠서 문제를 단순화 해야 합니다. Page 컴포넌트가 탭, 검색, 페이징 그리고 이 정보들을 취합해 컨텐츠를 보여주는 등 모든 책임을 갖지 않도록 해야 합니다. 따라서 아래 그림처럼 컴포넌트를 분리해서 책임을 나눠야 합니다.

각 컴포넌트는 각자의 UI에 대한 책임(B)을 갖습니다. 그리고 컨텐츠 컴포넌트와의 소통은 A라는 경로를 통해서만 이뤄집니다. 탭 컴포넌트를 예로 들면

function Tab(...) {
// B
const [selectedTabName, setSelectedTabName] = useState('인기');

const handleSelectTab = (tabName) => {
// B
setSelectedTabName(tabName);

// A
컨텐츠 컴포넌트와 소통수단(tabName); // 예를 들어, Context API
};

return (
<div>
<button
type="button"
style={{ selectedTabName === '인기'라면 하이라이트 }}
onClick={() => handleSelectTab('인기')}
>
인기글
</button>
<button
type="button"
style={{ selectedTabName === '추천'이라면 하이라이트 }}
onClick={() => handleSelectTab('추천')}
>
추천글
</button>
</div>
);
}

와 같습니다. 그리고 분리를 통해 페이지 컴포넌트에 모두 모여있으면 발생할 수 있는 다른 컴포넌트와의 직접적인 상호작용(C)을 제거하거나 차단합니다. 이 방법은 페이지 컴포넌트가 아니라도 여러 컴포넌트에 적용될 수 있습니다. 이 방법은 캡슐화라고 할 수도 있습니다. 여러 책임이 한 곳에 모여있으면 각 컴포넌트가 서로 강하게 결합될 가능성이 높아집니다. 따라서 컴포넌트가 자신의 책임을 갖도록 분리하고 다른 컴포넌트와 상호작용 할 땐 정해진 방법으로 하는 게 좋습니다. 즉, 스스로와 관련된 변경은 각 컴포넌트가 책임짐으로써 감추고, 잘 변경되지 않는 외부와의 메시징은 제한하는 캡슐화는 컴포넌트를 관리하는 좋은 방법 중 하나입니다. 이런 경우 컴포넌트를 분리합니다.

B-2. 컴포넌트에 비지니스 로직이 있는 경우

이전에 적었던 글에서도 다뤘던 내용이지만, 일반적으로 유저 인터페이스(UI)와 비지니스 로직은 변경의 속도, 즉 빈도⁹가 다릅니다. 예를 들어, 상품 카드에 환불이 가능한지 표시하는 방법은 많고 수시로 바꿀 수 있습니다. 빨간색이었다가 회색이 될 수 있고 보여줬다가 안 보여줄 수도 있습니다. 그리고 이런 요청은 실제로 많이 발생합니다. 하지만 환불 가능한 조건을 변경하는 건 사업 초기에 한 번 정해지고 바뀌지 않을 수도 있습니다.

하지만 컴포넌트에 비지니스 로직이 포함되어있다면 빈번한 UI 변경에 따라 자주 영향을 받을 수 있습니다.

그리고 비지니스 로직은 현실 세계의 비지니스 규칙이기 때문에 영향을 자주 받고 변경에 노출되면 문제가 될 수 있습니다. 예를 들어, 컴포넌트 A는 환불 가능 여부에 따라 true 또는 false가 필요하고, 컴포넌트 Btrue 또는 false가 필요했다가 상품의 타입도 같이 필요해졌다면 비지니스 로직의 코드를 수정해야 합니다. 보통 비지니스 로직처럼 여러 군데에서 사용하는 로직은 일반적인 성질을 갖는 무언가(가장 대표적으로 함수)로 분리하기 때문에 이 이슈는 더 큰 문제가 됩니다. 어디에서 해당 로직이 사용되는지 조사하고 변경의 영향을 판단해야 하기 때문입니다. 로직을 사용하는 모든 컴포넌트를 고려하다보면 로직이 같은 함수를 이름만 바꿔서 또 만드는 등의 문제가 발생하기도 합니다.

function isRefundable(product) {
// Business Logic
return true or false;
}

// 이 함수를 아래와 같이 변경합니다.

function isRefundable(product) {
// Business Logic
return true or false;
}

function getRefundableWithType(product) {
// Business Logic
return {
type: product type,
isRefundable: true or false,
};
}

// 이 함수를 또 아래와 같이 변경합니다.

function refundable(product) {
// Business Logic
return true or false;
}

function isRefundable(product) {
return refundable(product);;
}

function getRefundableWithType(product) {
return {
type: refundable(product);,
isRefundable: true or false,
};
}

// 등등...

따라서 UI와 비지니스 로직을 적절하게 분리하는 건 소프트웨어를 오랫동안 유지보수 하는 데 있어서 아주 중요합니다. 이 내용은 이전에 작성했던 글을 참고하시거나 발표자료를 보시면 더 자세하게 확인할 수 있습니다. 이렇게 컴포넌트에 비지니스 로직이 있다면 로직을 분리하게 되고 그 과정에서 컴포넌트를 책임에 맞게 분리하는 과정이 포함될 수 있습니다.

C. 렌더링 퍼포먼스

재사용과 복잡성 이외에 컴포넌트를 분리하면 좋은 기준 중 하나는 렌더링 퍼포먼스 입니다. 하나의 컴포넌트 안에서 서로 영향을 주지 않는 상태가 여럿 있으면 발생하는 문제입니다.

function Page1() {
const [카드 호버 상태, set카드 호버 상태] = useState(false);
const [탭 호버 상태, set탭 호버 상태] = useState('none');

return (
...
<ul>탭</ul>
...
<ul>카드</ul>
...
);
}

이 코드에서 탭과 카드는 서로 영향을 주지 않습니다. 하지만 탭에 호버를 하면 카드들이 렌더링되고 카드에 호버를 하면 탭이 렌더링 됩니다. 그리고 카드들 같은 경우 각 카드도 보통은 호버 상태가 다른 카드의 호버 상태에 영향을 주지 않습니다. 따라서 아래와 같이 분리를 하면 좋습니다.

function Page1() {
return (
...
<Tab></Tab>
...
<ul>
...
<li><Card></Card><li>
...
</ul>
...
);
}

function Tab() {
const [탭 호버 상태, set탭 호버 상태] = useState('none');

return (
<ul>탭</ul>
);
}

function Card() {
const [카드 호버 상태, set카드 호버 상태] = useState(false);

return (
<section>...</section>
);
}

특히 input을 다루는 경우 상태 변경에 대한 업데이트가 다른 컴포넌트로 전파될 수 있기 때문에 주의해야 합니다.

마무리

지금까지 제가 리액트 컴포넌트를 분리하거나 다룰 때 사용하는 기준에 대해 살펴봤습니다. 당연하지만 이 기준들이 정답은 아닙니다. 프로젝트가 처한 상황, 개발 문화, 더 넓게는 회사의 상황이나 구성원들과의 커뮤니케이션 등 많은 문제가 연관되어 있을 수 있기 때문입니다. 소프트웨어를 만들고 유지보수 한다는 건 영문 타이핑 만큼 단순했지만 갈수록 더 복잡하고 고려할게 많아지는 것 같습니다. 이 방법들이 이 글을 읽으시는 분들의 환경에 맞게 고려되고 참고가 되면 좋겠습니다.

마지막으로 유연한 컴포넌트를 만드는 데 도움이 많이 됐던 글들을 소개합니다.

[1]: 로버트 C. 마틴, 클린 아키텍처, 인사이트, 100p

[2]: https://www.techtarget.com/whatis/definition/component

[3]: https://stackoverflow.com/questions/46192426/how-to-tell-when-to-create-a-new-component

[4]: https://vuejs.org/guide/essentials/component-basics.html

[5]: https://ko.reactjs.org/docs/components-and-props.html, https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children

[6]: https://blog.bitsrc.io/5-steps-to-build-react-components-like-a-pro-fb1f3af6ba17

[7]: 조영호, 오브젝트, 위키북스, 437p

[8]: 조영호, 오브젝트, 위키북스, 309p

[9]: 조영호, 오브젝트, 위키북스, 229p

--

--

이문기

사용자를 생각하고 개발자를 생각하는 프런트엔드를 만드는 데 관심이 많습니다. 표준, 접근성, 아키텍처, 테스트 등을 꾸준히 훈련하고 적용하려고 노력합니다.