컴포넌트(Components)는 React의 가장 핵심적인 개념 중 하나예요. 사용자 인터페이스(UI)를 구축하는 튼튼한 토대가 되기 때문에, React 여정을 시작하기에 가장 완벽한 출발점이라고 할 수 있죠!
👨🏫 Gemini 강사의 보충 설명: > 컴포넌트를 한마디로 표현하자면 '레고 블록'과 같아요. 우리가 레고 블록들을 하나하나 조립해서 거대한 성이나 자동차를 만들듯이, React에서는 작은 컴포넌트(버튼, 입력창, 메뉴 등)들을 조립해서 하나의 완성된 웹 페이지를 만들어내는 거랍니다.
이 페이지에서 배울 내용:
웹에서 HTML은 <h1>이나 <li> 같은 내장된 태그 세트를 사용해서 구조가 풍부한 문서를 만들 수 있게 해줍니다.
<article>
<h1>My First Component</h1>
<ol>
<li>Components: UI Building Blocks</li>
<li>Defining a Component</li>
<li>Using a Component</li>
</ol>
</article>
위의 마크업 코드는 <article>이라는 글 영역, <h1>이라는 제목, 그리고 <ol>이라는 순서가 있는 리스트로 만든 (요약된) 목차를 나타내고 있어요. 이런 마크업에 스타일을 입히기 위한 CSS, 그리고 상호작용(인터랙션)을 위한 JavaScript가 결합되어 웹에서 여러분이 보는 모든 사이드바, 아바타 이미지, 모달 창, 드롭다운 같은 모든 UI 조각들이 만들어집니다.
React는 여러분이 작성한 마크업, CSS, 그리고 JavaScript를 결합해서 사용자 정의 "컴포넌트", 즉 앱에서 재사용 가능한 UI 요소로 만들 수 있게 해줘요. 방금 위에서 본 목차 코드도 <TableOfContents />라는 이름의 컴포넌트로 만들어서 모든 페이지에 똑같이 렌더링(화면에 띄움)할 수 있습니다. 물론 내부적으로는 여전히 <article>, <h1> 같은 똑같은 HTML 태그들을 사용하고 있죠.
👨🏫 Gemini 강사의 보충 설명: > 위 이미지를 상상해 보세요. 하나의 큰 웹페이지(화면)가 헤더, 사이드바, 본문 내용 등 여러 개의 네모난 구역(컴포넌트)으로 쪼개지는 모습입니다. 이렇게 화면을 잘게 쪼개서 관리하면 코드도 깔끔해지고, 같은 모양의 버튼이나 리스트를 여기저기서 쉽게 '재사용'할 수 있어서 개발 속도가 엄청나게 빨라집니다.
HTML 태그들과 마찬가지로, 여러분은 컴포넌트들을 조합하고, 순서를 맞추고, 안에 중첩시켜서 전체 페이지를 디자인할 수 있어요. 예를 들어, 지금 여러분이 읽고 계신 이 공식 문서 페이지도 아래처럼 React 컴포넌트들로 만들어져 있답니다.
<PageLayout>
<NavigationHeader>
<SearchBar />
<Link to="/docs">Docs</Link>
</NavigationHeader>
<Sidebar />
<PageContent>
<TableOfContents />
<DocumentationText />
</PageContent>
</PageLayout>
프로젝트 규모가 점점 커질수록, 이미 만들어둔 컴포넌트들을 재사용해서 수많은 디자인들을 조합해 낼 수 있다는 걸 알게 되실 거예요. 개발 속도가 엄청나게 빨라지는 거죠! 위에서 만든 목차 컴포넌트도 <TableOfContents /> 한 줄만 쓰면 어떤 화면에든 쉽게 추가할 수 있으니까요. 심지어 Chakra UI나 Material UI 같은 React 오픈소스 커뮤니티에서 공유하는 수천 개의 멋진 컴포넌트들을 가져와서 프로젝트를 아주 빠르게 시작할 수도 있습니다.
전통적인 방식의 웹 페이지 개발에서는 웹 개발자들이 먼저 콘텐츠를 마크업(HTML)으로 작성한 다음, 그 위에 JavaScript를 살짝 뿌려주는(sprinkling) 방식으로 상호작용을 추가했어요. 이 방식은 웹에서 상호작용이 그저 '있으면 좋은 것(nice-to-have)'이었던 시절에는 아주 훌륭하게 작동했죠. 하지만 지금은 수많은 웹사이트와 모든 앱에서 상호작용은 필수적인 요소가 되었습니다.
React는 동일한 기술을 사용하되 상호작용을 최우선으로 생각합니다. 즉, React 컴포넌트는 마크업을 흩뿌릴 수 있는 JavaScript 함수입니다. 코드가 어떻게 생겼는지 한번 볼까요? (아래 예제를 직접 수정해 볼 수도 있어요):
export default function Profile() {
return (
<img
src="[https://i.imgur.com/MK3eW3Am.jpg](https://i.imgur.com/MK3eW3Am.jpg)"
alt="Katherine Johnson"
/>
)
}
img { height: 200px; }
👨🏫 Gemini 강사의 보충 설명:
"JavaScript 함수인데 마크업(HTML)을 반환한다고?" 처음엔 이 부분이 제일 어색하실 거예요. 기존에는 HTML 파일과 JS 파일을 엄격하게 분리했지만, React에서는 UI 조각(컴포넌트)을 하나의 JS 함수로 묶어버립니다. 화면에 보여줄 껍데기(HTML)와 그 껍데기를 움직이게 할 논리(JS)를 한곳에 두어서 한 번에 관리하겠다는 똑똑한 발상이죠!
자, 이제 컴포넌트를 만드는 방법을 단계별로 알아볼게요.
export default라는 접두어는 표준 JavaScript 문법이에요 (React만의 전용 문법이 아니랍니다). 이 키워드를 사용하면 파일의 메인 함수를 표시해 두어서 나중에 다른 파일에서 이 함수를 가져와서(import) 쓸 수 있게 해줍니다. (가져오기에 대한 더 자세한 내용은 컴포넌트 임포트 및 익스포트 하기 페이지에서 배울 수 있어요!)
function Profile() { } 코드를 사용해서 이름이 Profile인 JavaScript 함수를 정의합니다.
🚨 주의하세요!
React 컴포넌트들은 일반적인 JavaScript 함수지만, 이름은 반드시 대문자로 시작해야 합니다. 그렇지 않으면 제대로 작동하지 않아요!
👨🏫 Gemini 강사의 보충 설명:
소문자로 시작하면(예:profile) React는 이것을 일반적인 HTML 태그(예:div,span등)로 착각해버립니다. 우리가 만든 특별한 컴포넌트라는 걸 React에게 알려주려면 반드시 첫 글자를 대문자(예:Profile)로 써주셔야 해요. 약속입니다!
이 컴포넌트는 src와 alt 속성을 가진 <img /> 태그를 반환(return)하고 있습니다. <img />는 HTML처럼 생겼지만, 사실 내부적으로는 JavaScript랍니다! 이런 문법을 JSX라고 부르며, 이 문법 덕분에 JavaScript 코드 안에 마크업을 자연스럽게 삽입할 수 있어요.
이 컴포넌트처럼 return 문을 한 줄로 쭉 이어서 쓸 수도 있습니다.
return <img src="[https://i.imgur.com/MK3eW3As.jpg](https://i.imgur.com/MK3eW3As.jpg)" alt="Katherine Johnson" />;
하지만 마크업 코드가 return 키워드와 같은 줄에 있지 않고 여러 줄로 넘어간다면, 반드시 괄호 ()로 마크업을 감싸주어야 합니다:
return (
<div>
<img src="[https://i.imgur.com/MK3eW3As.jpg](https://i.imgur.com/MK3eW3As.jpg)" alt="Katherine Johnson" />
</div>
);
🚨 주의하세요!
괄호 없이 쓴다면, return 키워드 다음 줄에 있는 코드들은 모두 무시되어 버립니다! (JavaScript의 자동 세미콜론 삽입 규칙 때문이죠)
자, 이제 Profile 컴포넌트를 정의했으니, 이 컴포넌트를 다른 컴포넌트 안에 중첩해서 사용할 수 있습니다. 예를 들어, 여러 개의 Profile 컴포넌트를 사용하는 Gallery 컴포넌트를 익스포트(export) 해볼 수 있겠죠:
function Profile() {
return (
<img
src="[https://i.imgur.com/MK3eW3As.jpg](https://i.imgur.com/MK3eW3As.jpg)"
alt="Katherine Johnson"
/>
);
}
export default function Gallery() {
return (
<section>
<h1>Amazing scientists</h1>
<Profile />
<Profile />
<Profile />
</section>
);
}
img { margin: 0 10px 10px 0; height: 90px; }
대소문자의 차이에 주목해 보세요:
<section>은 소문자입니다. 따라서 React는 우리가 일반 HTML 태그를 지칭한다는 것을 압니다.<Profile />은 대문자 P로 시작합니다. 따라서 React는 우리가 Profile이라는 이름의 커스텀 컴포넌트를 사용하고 싶어 한다는 것을 알게 되죠.그리고 Profile 안에는 <img />라는 또 다른 HTML이 들어있습니다. 결국, 최종적으로 브라우저가 화면에 그리기 위해 보게 되는 코드는 다음과 같습니다:
<section>
<h1>Amazing scientists</h1>
<img src="[https://i.imgur.com/MK3eW3As.jpg](https://i.imgur.com/MK3eW3As.jpg)" alt="Katherine Johnson" />
<img src="[https://i.imgur.com/MK3eW3As.jpg](https://i.imgur.com/MK3eW3As.jpg)" alt="Katherine Johnson" />
<img src="[https://i.imgur.com/MK3eW3As.jpg](https://i.imgur.com/MK3eW3As.jpg)" alt="Katherine Johnson" />
</section>
컴포넌트들은 일반적인 JavaScript 함수이기 때문에, 한 파일 안에 여러 개의 컴포넌트들을 계속 만들어둘 수 있습니다. 이 방식은 컴포넌트들의 크기가 비교적 작거나 서로 아주 밀접하게 연관되어 있을 때 꽤 편리해요. 하지만 이 파일 안이 코드로 너무 붐비게 되면, 언제든지 Profile을 분리된 다른 파일로 옮길 수 있습니다. 이 방법은 임포트에 관한 페이지에서 곧 배우게 될 거예요.
Profile 컴포넌트들이 Gallery 안에서 렌더링되고 있으므로 (그것도 여러 번이나요!), 우리는 Gallery를 부모 컴포넌트(parent component)라고 부르고, 렌더링되는 각각의 Profile을 "자식(child)"이라고 부를 수 있습니다. 이것이 바로 React의 마법 중 하나입니다. 컴포넌트를 한 번만 정의해 두면, 원하는 곳 어디에서든 원하는 횟수만큼 얼마든지 재사용할 수 있답니다.
🚨 주의하세요!
컴포넌트가 다른 컴포넌트를 렌더링(반환)할 수는 있지만, 컴포넌트의 정의(함수 선언) 자체를 중첩해서는 절대 안 됩니다:
export default function Gallery() {
// 🔴 절대 다른 컴포넌트 안에서 컴포넌트를 정의하지 마세요!
function Profile() {
// ...
}
// ...
}
위의 코드는 매우 느려지고 버그를 유발합니다. 대신에, 모든 컴포넌트는 항상 최상위 레벨(top level)에서 정의해 주세요:
export default function Gallery() {
// ...
}
// ✅ 컴포넌트들은 최상위 레벨에서 선언하세요.
function Profile() {
// ...
}
자식 컴포넌트가 부모로부터 어떤 데이터가 필요할 때는, 함수를 중첩해서 선언하는 대신 props를 통해 전달하세요.
👨🏫 Gemini 강사의 보충 설명:
실무에서 주니어 개발자들이 은근히 자주 하는 실수 중 하나입니다! 부모 컴포넌트 안에 자식 컴포넌트 함수를 만들어 버리면, 부모가 렌더링될 때마다 자식 컴포넌트가 '새로 생성'됩니다. 메모리 낭비는 물론이고, 나중에 배울 state(상태)가 예기치 않게 초기화되는 끔찍한 버그를 만나게 될 수 있으니 꼭 기억해 두세요! 함수는 파일 맨 바깥에 나란히 선언하는 겁니다!
여러분의 React 애플리케이션은 "루트(root, 뿌리)" 컴포넌트에서부터 시작됩니다. 일반적으로 새 프로젝트를 시작하면 이 루트 컴포넌트가 자동으로 생성돼요. 예를 들어, CodeSandbox를 사용하거나 Next.js 프레임워크를 사용한다면 루트 컴포넌트는 pages/index.js에 정의되어 있습니다. 지금까지 예제들에서도 여러분은 루트 컴포넌트들을 익스포트해 오고 있었던 거고요.
대부분의 React 앱들은 가장 위부터 가장 아래까지 모든 것이 컴포넌트로 이루어져 있습니다(components all the way down). 이 말인즉슨, 버튼처럼 재사용하기 좋은 작은 조각들에만 컴포넌트를 쓰는 게 아니라, 사이드바, 리스트 같은 더 큰 조각들, 그리고 궁극적으로는 페이지 전체까지도 모두 컴포넌트라는 뜻이에요! 컴포넌트는 어떤 UI 요소가 단 한 번만 사용된다고 하더라도, UI 코드와 마크업을 깔끔하게 정리하고 구조화하는 데 아주 유용한 방법입니다.
React 기반 프레임워크들은 여기서 한 걸음 더 나아갑니다. 텅 빈 HTML 파일을 두고 React가 JavaScript로 페이지 전체 관리를 "알아서 하게" 내버려 두는 대신, 여러분이 만든 React 컴포넌트들을 바탕으로 초기 HTML을 자동으로 생성해 주기도 합니다. 이렇게 하면 브라우저가 JavaScript 코드를 전부 다 불러오기 전에도 유저가 앱의 일부 콘텐츠를 미리 볼 수 있게 되죠.
그럼에도 불구하고 여전히 많은 웹사이트들은 단지 기존 HTML 페이지에 상호작용을 추가하는 데에만 React를 사용하기도 합니다. 이런 경우엔 전체 페이지를 아우르는 단일 루트 컴포넌트 대신, 여러 개의 루트 컴포넌트들을 군데군데 가지게 됩니다. 결국, 여러분의 필요에 따라 React를 아주 조금만 쓸 수도 있고 전체 영역에 다 쓸 수도 있다는 뜻이에요.
요약해 봅시다 (Recap)
React의 첫 맛을 보셨군요! 핵심 포인트들을 다시 한번 짚고 넘어가 볼까요.
아래 샌드박스 코드는 루트 컴포넌트가 익스포트(내보내기) 되어 있지 않아서 제대로 작동하지 않고 있습니다:
function Profile() {
return (
&<img
src="[https://i.imgur.com/lICfvbD.jpg](https://i.imgur.com/lICfvbD.jpg)"
alt="Aklilu Lemma"
/>
);
}
img { height: 181px; }
정답을 보기 전에 스스로 한번 문제를 해결해 보세요!
정답:
함수 정의부 앞에 아래와 같이 export default를 추가해 주세요:
export default function Profile() {
return (
<img
src="[https://i.imgur.com/lICfvbD.jpg](https://i.imgur.com/lICfvbD.jpg)"
alt="Aklilu Lemma"
/>
);
}
img { height: 181px; }
왜 그냥 export만 써서는 이 문제가 해결되지 않는지 궁금하실 텐데요. export와 export default의 차이점은 컴포넌트 임포트 및 익스포트 하기에서 자세히 배우실 수 있습니다.
아래의 return 구문에 뭔가 잘못된 부분이 있습니다. 고칠 수 있으시겠어요?
힌트:
문제를 고치는 과정에서 "Unexpected token(예기치 못한 토큰)" 에러가 발생할 수도 있습니다. 그런 경우에는 세미콜론(;)이 닫는 괄호 ) 뒤에 위치해 있는지 확인해 보세요. return ( ) 괄호 안쪽에 세미콜론을 남겨두면 에러가 발생합니다.
export default function Profile() {
return
<img src="[https://i.imgur.com/jA8hHMpm.jpg](https://i.imgur.com/jA8hHMpm.jpg)" alt="Katsuko Saruhashi" />;
}
img { height: 180px; }
정답:
이 컴포넌트를 고치려면, return 문과 마크업을 같은 줄(한 줄)로 합쳐주시면 됩니다:
export default function Profile() {
return <img src="[https://i.imgur.com/jA8hHMpm.jpg](https://i.imgur.com/jA8hHMpm.jpg)" alt="Katsuko Saruhashi" />;
}
img { height: 180px; }
또는 return 키워드 바로 뒤에서 괄호를 열고 반환될 JSX 마크업 전체를 괄호로 감싸주셔도 됩니다:
export default function Profile() {
return (
<img
src="[https://i.imgur.com/jA8hHMpm.jpg](https://i.imgur.com/jA8hHMpm.jpg)"
alt="Katsuko Saruhashi"
/>
);
}
img { height: 180px; }
Profile 컴포넌트가 선언되고 사용된 방식에 무언가 문제가 있습니다. 어떤 실수가 있는지 찾으셨나요? (React가 일반 HTML 태그와 컴포넌트를 어떻게 구분하는지 떠올려 보세요!)
function profile() {
return (
<img
src="[https://i.imgur.com/QIrZWGIs.jpg](https://i.imgur.com/QIrZWGIs.jpg)"
alt="Alan L. Hart"
/>
);
}
export default function Gallery() {
return (
<section>
<h1>Amazing scientists</h1>
<profile />
<profile />
<profile />
</section>
);
}
img { margin: 0 10px 10px 0; height: 90px; }
정답:
React 컴포넌트의 이름은 반드시 대문자로 시작해야 합니다.
function profile()을 function Profile()로 대문자로 수정하고, 사용되는 모든 <profile /> 태그도 <Profile />로 수정해 주어야 합니다:
function Profile() {
return (
<img
src="[https://i.imgur.com/QIrZWGIs.jpg](https://i.imgur.com/QIrZWGIs.jpg)"
alt="Alan L. Hart"
/>
);
}
export default function Gallery() {
return (
<section>
<h1>Amazing scientists</h1>
<Profile />
<Profile />
<Profile />
</section>
);
}
img { margin: 0 10px 10px 0; }
아무것도 없는 백지상태에서 직접 컴포넌트를 하나 작성해 보세요. 문법에만 맞다면 이름은 마음대로 지어도 되고, 어떤 마크업이든 반환하게 만들어도 됩니다. 정 아이디어가 떠오르지 않는다면, <h1>Good job!</h1>을 보여주는 Congratulations라는 컴포넌트를 만들어 보는 건 어떨까요? 다 만들고 나서 익스포트(export) 하는 것도 잊지 마시고요!
// 아래에 직접 컴포넌트를 작성해 보세요!
정답 예시:
export default function Congratulations() {
return (
<h1>Good job!</h1>
);
}