여러분, 컴포넌트를 만들다 보면 조건에 따라 다른 내용을 보여줘야 할 때가 정말 많죠? React에서는 if 문, &&, 그리고 ? : (삼항 연산자) 같은 친숙한 JavaScript 문법을 사용해서 조건부로 JSX를 렌더링할 수 있어요.
(강사 한마디: 프론트엔드 개발자로 일하시거나 Next.js 같은 프레임워크를 다루실 때도 이 조건부 렌더링은 매일같이 쓰이는 기본기니까 꼭 확실하게 짚고 넘어가 볼까요?)
여러 개의 Item 컴포넌트를 렌더링하는 PackingList 컴포넌트가 있다고 가정해 볼게요. 이 아이템들은 짐을 챙겼는지(packed) 아닌지에 따라 다르게 표시될 수 있어요:
function Item({ name, isPacked }) {
return <li className="item">{name}</li>;
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
Item 컴포넌트 중 일부는 isPacked prop이 false가 아니라 true로 설정되어 있는 거 보이시죠? 우리는 isPacked={true}일 때, 짐을 다 챙겼다는 의미로 아이템 이름 옆에 체크 표시(✅)를 추가하고 싶어요.
이럴 때는 아주 친숙한 if/else 문을 사용해서 아래처럼 작성할 수 있어요:
if (isPacked) {
return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;
만약 isPacked prop이 true라면, 이 코드는 완전히 다른 JSX 트리를 반환하게 됩니다. 이렇게 코드를 수정하면, 목록 중 일부 아이템 끝에 예쁜 체크 표시가 붙게 되죠:
function Item({ name, isPacked }) {
if (isPacked) {
return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
각 경우에 반환되는 내용을 직접 수정해 보시고 결과가 어떻게 달라지는지 눈으로 확인해 보세요!
여기서 JavaScript의 if와 return 문을 사용해서 분기 로직(branching logic)을 만들고 있다는 점에 주목해 주세요. React에서는 (조건문 같은) 제어 흐름(control flow)을 템플릿 문법이 아닌 순수 JavaScript가 직접 처리한답니다.
null을 반환하여 조건부로 아무것도 렌더링하지 않기 {/conditionally-returning-nothing-with-null/}어떤 상황에서는 화면에 아예 아무것도 그리고 싶지 않을 때가 있어요. 예를 들어, 이미 짐을 챙긴 아이템은 목록에서 아예 안 보이게 숨기고 싶다고 해볼까요? 하지만 명심하세요, React 컴포넌트는 반드시 무언가를 반환해야만 해요. 이럴 때는 그냥 null을 반환해주면 됩니다:
if (isPacked) {
return null;
}
return <li className="item">{name}</li>;
만약 isPacked가 참(true)이라면 이 컴포넌트는 아무것도 렌더링하지 않기 위해 null을 반환해요. 그 외의 경우에는 평소처럼 화면에 그릴 JSX를 반환하게 되고요.
function Item({ name, isPacked }) {
if (isPacked) {
return null;
}
return <li className="item">{name}</li>;
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
(강사 한마디: 그런데 실무를 하다 보면, 컴포넌트 안에서 직접 null을 반환하는 경우는 사실 그렇게 흔하지 않아요. 렌더링하려는 개발자 입장에서 컴포넌트를 호출했는데 갑자기 아무것도 안 나오면 당황스러울 수 있거든요. 보통은 부모 컴포넌트의 JSX 안에서 해당 컴포넌트를 조건부로 포함시키거나 제외하는 방식을 훨씬 더 많이 씁니다. 자, 그럼 그 방법도 알아볼까요?)
이전 예제에서는 컴포넌트가 어떤 JSX 트리를 반환할지(혹은 아예 안 할지!)를 전체적으로 제어해 봤어요. 그런데 렌더링 결과를 자세히 보셨다면 코드에 약간의 중복이 있다는 걸 눈치채셨을 거예요:
<li className="item">{name} ✅</li>
이 코드는 아래 코드와 아주 비슷하죠.
<li className="item">{name}</li>
두 조건 분기 모두 <li className="item">...</li>라는 구조를 똑같이 반환하고 있어요:
if (isPacked) {
return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;
이런 중복이 당장 프로그램에 악영향을 미치는 건 아니지만, 나중에 코드를 유지보수하기 어렵게 만들 수 있어요. 만약 <li className="item">의 className을 다른 이름으로 변경하고 싶다면 어떨까요? 코드의 두 군데를 모두 찾아가서 수정해야 하잖아요! 이런 상황에서는 변경이 필요한 부분의 JSX만 조건부로 쏙 포함시켜서 코드를 좀 더 DRY (Don't Repeat Yourself, 반복하지 마라 원칙)하게 만들 수 있어요.
? :) {/conditional-ternary-operator--/}JavaScript에는 조건식을 아주 간결하게 작성할 수 있는 훌륭한 문법이 있어요. 바로 조건부 연산자(Conditional Operator) 혹은 '삼항 연산자'라고 불리는 녀석입니다.
이렇게 길게 쓰는 대신:
if (isPacked) {
return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;
이렇게 단 한 줄로 깔끔하게 작성할 수 있어요:
return (
<li className="item">
{isPacked ? name + ' ✅' : name}
</li>
);
이 코드는 이렇게 읽으시면 돼요. "만약 isPacked가 참(true)이면 (?) name + ' ✅'를 렌더링하고, 그렇지 않으면 (:) name만 렌더링해라."
객체 지향 프로그래밍 배경지식이 있으신 분들이라면, 위의 두 예제가 미묘하게 다르다고 생각하실 수 있어요. 둘 중 하나가 <li>의 완전히 다른 '인스턴스' 두 개를 생성할지도 모른다고 말이죠.
하지만 JSX 엘리먼트는 내부에 어떤 상태(state)를 가지고 있지도 않고, 실제 DOM 노드도 아니기 때문에 우리가 흔히 아는 '인스턴스'라는 개념이 아니에요. 그저 화면을 어떻게 그려야 할지 알려주는 아주 가벼운 설명서(blueprints)일 뿐이죠. 그래서 사실 이 두 예제는 완전히 동일하게 작동한답니다. 상태 보존 및 초기화(Preserving and Resetting State) 문서에서 이 원리가 어떻게 동작하는지 훨씬 더 자세히 알아보실 수 있어요.
자, 이번엔 완료된 아이템의 텍스트를 <del> 같은 HTML 태그로 한 번 더 감싸서 취소선 효과를 주고 싶다고 해볼까요? 각 경우(case)에 더 많은 JSX를 중첩하기 쉽도록 줄바꿈과 괄호를 넉넉히 추가해 줄 수 있어요:
function Item({ name, isPacked }) {
return (
<li className="item">
{isPacked ? (
<del>
{name + ' ✅'}
</del>
) : (
name
)}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
이런 삼항 연산자 스타일은 간단한 조건문에는 아주 찰떡이에요. 하지만 너무 남용하지는 마세요! 컴포넌트 안에 조건부 마크업이 너무 깊게 중첩돼서 코드가 지저분해진다면, 자식 컴포넌트를 밖으로 추출해서 코드를 깔끔하게 정리하는 걸 추천해 드려요. React에서는 마크업(JSX)도 결국 코드의 일부이기 때문에, 변수나 함수 같은 도구들을 마음껏 활용해서 복잡한 표현식들을 단정하게 정돈할 수 있거든요.
&&) {/logical-and-operator-/}React 코드에서 정말 자주 보게 될 또 다른 유용한 단축키는 바로 JavaScript 논리 AND (&&) 연산자입니다. React 컴포넌트 내부에서는 조건이 참일 때만 특정 JSX를 렌더링하고, 거짓일 때는 아무것도 렌더링하지 않고 싶을 때 주로 쓰여요. &&를 사용하면 isPacked가 true일 때만 체크 표시를 렌더링하도록 아주 쉽게 쓸 수 있어요:
return (
<li className="item">
{name} {isPacked && '✅'}
</li>
);
이건 이렇게 읽을 수 있겠죠. "만약 isPacked가 참이면 (&&) 체크 표시를 렌더링하고, 그렇지 않으면 아무것도 렌더링하지 마라."
실제로 어떻게 동작하는지 샌드박스에서 확인해 보세요:
function Item({ name, isPacked }) {
return (
<li className="item">
{name} {isPacked && '✅'}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
JavaScript && 표현식은 왼쪽(우리의 조건)이 true이면 오른쪽 값(여기서는 체크 표시)을 반환해요. 하지만 조건이 false이면 전체 표현식이 false가 되어버리죠. React는 이 false 값을 null이나 undefined처럼 JSX 트리의 '빈 공간(hole)'으로 간주해서 그 자리에는 아무것도 렌더링하지 않아요.
&& 연산자 왼쪽에 절대 숫자를 두지 마세요.
JavaScript는 조건을 테스트할 때 왼쪽 값을 자동으로 불리언(boolean)으로 변환해요. 그런데 만약 왼쪽 값이 0이라면, 전체 표현식은 0이라는 값을 가지게 되고, React는 이 값을 빈 공간으로 무시하는 게 아니라 화면에 기꺼이 0을 렌더링해버린답니다.
예를 들어, 개발자들이 정말 흔하게 하는 실수 중 하나가 messageCount && <p>New messages</p>처럼 코드를 짜는 거예요. messageCount가 0일 때 화면에 아무것도 안 나올 거라고 기대하기 쉽지만, 실제로는 화면에 0이 떡하니 출력된답니다!
이 문제를 해결하려면 왼쪽을 확실한 불리언 값으로 만들어줘야 해요: messageCount > 0 && <p>New messages</p>.
(강사 한마디: 특히 배열의 길이를 가지고 렌더링 유무를 결정할 때 array.length && <Component /> 라고 많이들 쓰시는데, 배열이 비어있으면 UI에 0이 뜬금없이 나타납니다. 꼭 array.length > 0 && <Component /> 로 명시적으로 조건을 적어주는 습관을 들이세요!)
앞서 배운 단축키들이 중첩되면서 오히려 코드를 읽기 어렵게 만든다면, 아주 기본으로 돌아가서 if 문과 변수를 활용해 보세요. let으로 선언한 변수는 재할당이 가능하니까, 먼저 화면에 보여주고 싶은 기본 내용(여기서는 name)을 변수에 담아두고 시작하는 거예요:
let itemContent = name;
그런 다음 if 문을 사용해서, isPacked가 true일 때만 itemContent에 새로운 JSX 표현식을 덮어씌워(재할당) 주는 겁니다:
if (isPacked) {
itemContent = name + " ✅";
}
중괄호는 'JavaScript 세상으로 향하는 창문'을 열어주죠. 방금 계산해 둔 변수를 JSX 내부에 넣고 싶다면, 반환할 JSX 트리 안에 변수를 중괄호로 예쁘게 감싸서 넣어주면 끝이에요:
<li className="item">
{itemContent}
</li>
이 스타일이 코드가 가장 길어 보일 수는 있지만, 그만큼 읽기 쉽고 가장 유연하기도 해요. 어떻게 작동하는지 볼까요:
function Item({ name, isPacked }) {
let itemContent = name;
if (isPacked) {
itemContent = name + " ✅";
}
return (
<li className="item">
{itemContent}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
이전과 마찬가지로 이 방법은 단순한 텍스트뿐만 아니라 아주 복잡하고 임의적인 형태의 JSX 요소에도 똑같이 적용할 수 있습니다:
function Item({ name, isPacked }) {
let itemContent = name;
if (isPacked) {
itemContent = (
<del>
{name + " ✅"}
</del>
);
}
return (
<li className="item">
{itemContent}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
아직 JavaScript의 다양한 문법에 완전히 익숙하지 않으시다면, 이 여러 가지 스타일들이 처음에는 좀 헷갈리고 벅차게 느껴지실 수도 있어요. 하지만 이 문법들을 잘 익혀두시면 비단 React 컴포넌트뿐만 아니라 모든 JavaScript 코드, 나아가 복잡한 분기 처리가 필요한 알고리즘 로직을 짤 때도 엄청난 도움이 될 겁니다! 처음에는 본인에게 가장 편하고 마음에 드는 스타일 하나를 골라서 꾸준히 써보시고요, 나중에 다른 방식이 어떻게 동작했는지 잊어버리셨다면 언제든 이 문서를 다시 열어보세요.
if 문을 사용해서 JSX 표현식을 조건부로 반환할 수 있습니다.{cond ? <A /> : <B />}는 "만약 cond가 참이면 <A />를 렌더링하고, 그렇지 않으면 <B />를 렌더링해라" 라는 뜻입니다.{cond && <A />}는 "만약 cond가 참이면 <A />를 렌더링하고, 그렇지 않으면 아무것도 렌더링하지 마라" 라는 뜻입니다.if 문이 더 편하시다면 굳이 억지로 단축키를 쓰지 않으셔도 괜찮습니다. 편한 게 최고니까요!? : 를 사용하여 미완료 항목에 아이콘 표시하기 {/show-an-icon-for-incomplete-items-with--/}조건부 연산자 (cond ? a : b) 를 사용해서, 만약 isPacked가 true가 아니라면 화면에 ❌ 를 렌더링하도록 코드를 수정해 보세요.
function Item({ name, isPacked }) {
return (
<li className="item">
{name} {isPacked && '✅'}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
function Item({ name, isPacked }) {
return (
<li className="item">
{name} {isPacked ? '✅' : '❌'}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
&& 를 사용하여 항목의 중요도 표시하기 {/show-the-item-importance-with-/}이 예제에서는 각 Item이 숫자 형태의 importance(중요도) prop을 받습니다. && 연산자를 사용해서 이탤릭체로 "(Importance: X)" 를 렌더링하도록 만들어 보세요. 단, 중요도가 0이 아닐 때만 렌더링되어야 합니다. 리스트는 최종적으로 이렇게 보여야 해요:
항목 이름과 중요도 라벨 사이에 공백(스페이스) 하나 추가하는 거 잊지 마시고요!
function Item({ name, importance }) {
return (
<li className="item">
{name}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
importance={9}
name="Space suit"
/>
<Item
importance={0}
name="Helmet with a golden leaf"
/>
<Item
importance={6}
name="Photo of Tam"
/>
</ul>
</section>
);
}
이렇게 작성하시면 원하시는 결과를 얻을 수 있을 거예요:
function Item({ name, importance }) {
return (
<li className="item">
{name}
{importance > 0 && ' '}
{importance > 0 &&
<i>(Importance: {importance})</i>
}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
importance={9}
name="Space suit"
/>
<Item
importance={0}
name="Helmet with a golden leaf"
/>
<Item
importance={6}
name="Photo of Tam"
/>
</ul>
</section>
);
}
여기서 주의할 점! importance && ... 가 아니라 반드시 importance > 0 && ... 라고 작성해야 해요. 그래야 importance가 0일 때 화면에 불필요한 0이 렌더링되지 않거든요!
이 해답에서는 이름과 중요도 라벨 사이에 공백을 확실하게 넣기 위해 두 개의 개별적인 조건을 사용했어요. 다른 방법으로는 선행 공백이 포함된 Fragment를 사용하거나 (importance > 0 && <> <i>...</i></>), <i> 태그 바로 안쪽에 공백을 추가하는 방법(importance > 0 && <i> ...</i>)도 있습니다.
(강사 한마디: 실무에서 Vitest나 Playwright 같은 도구로 UI 테스트/E2E 테스트를 작성하실 때, 이런 미세한 공백 처리 하나 때문에 테스트가 깨지는 경우가 꽤 있어요. 렌더링되는 텍스트의 구조와 공백을 정확하게 제어하는 연습은 아주 중요하답니다!)
? : 를 if 문과 변수로 리팩토링하기 {/refactor-a-series-of---to-if-and-variables/}이 Drink 컴포넌트는 여러 개의 ? : (삼항 연산자) 조건을 사용해서 name prop이 "tea"인지 "coffee"인지에 따라 다른 정보를 보여주고 있어요. 가장 큰 문제는 각 음료에 대한 정보가 여러 조건문에 뿔뿔이 흩어져 있다는 거죠. 이 코드를 세 개의 ? : 조건 대신, 단일 if 문을 사용하도록 리팩토링해 보세요.
function Drink({ name }) {
return (
<section>
<h1>{name}</h1>
<dl>
<dt>Part of plant</dt>
<dd>{name === 'tea' ? 'leaf' : 'bean'}</dd>
<dt>Caffeine content</dt>
<dd>{name === 'tea' ? '15–70 mg/cup' : '80–185 mg/cup'}</dd>
<dt>Age</dt>
<dd>{name === 'tea' ? '4,000+ years' : '1,000+ years'}</dd>
</dl>
</section>
);
}
export default function DrinkList() {
return (
<div>
<Drink name="tea" />
<Drink name="coffee" />
</div>
);
}
if 문을 사용해서 코드를 리팩토링하셨나요? 혹시 이걸 더 간단하게 만들 수 있는 다른 기발한 아이디어도 떠오르시나요?
이 문제를 푸는 데는 여러 가지 방법이 있겠지만, 가장 기본적으로 접근할 수 있는 출발점은 다음과 같습니다:
function Drink({ name }) {
let part, caffeine, age;
if (name === 'tea') {
part = 'leaf';
caffeine = '15–70 mg/cup';
age = '4,000+ years';
} else if (name === 'coffee') {
part = 'bean';
caffeine = '80–185 mg/cup';
age = '1,000+ years';
}
return (
<section>
<h1>{name}</h1>
<dl>
<dt>Part of plant</dt>
<dd>{part}</dd>
<dt>Caffeine content</dt>
<dd>{caffeine}</dd>
<dt>Age</dt>
<dd>{age}</dd>
</dl>
</section>
);
}
export default function DrinkList() {
return (
<div>
<Drink name="tea" />
<Drink name="coffee" />
</div>
);
}
이렇게 하면 각 음료에 대한 정보가 여러 조건문에 흩어지지 않고 하나로 묶이게 되죠. 덕분에 나중에 새로운 음료를 추가하기도 훨씬 수월해집니다.
또 다른 기가 막힌 해결책은, 정보들을 아예 객체(Object) 안으로 옮겨서 조건문 자체를 없애버리는 방법이에요:
const drinks = {
tea: {
part: 'leaf',
caffeine: '15–70 mg/cup',
age: '4,000+ years'
},
coffee: {
part: 'bean',
caffeine: '80–185 mg/cup',
age: '1,000+ years'
}
};
function Drink({ name }) {
const info = drinks[name];
return (
<section>
<h1>{name}</h1>
<dl>
<dt>Part of plant</dt>
<dd>{info.part}</dd>
<dt>Caffeine content</dt>
<dd>{info.caffeine}</dd>
<dt>Age</dt>
<dd>{info.age}</dd>
</dl>
</section>
);
}
export default function DrinkList() {
return (
<div>
<Drink name="tea" />
<Drink name="coffee" />
</div>
);
}
(강사 한마디: 개인 프로젝트나 포트폴리오 사이트를 만드실 때 이렇게 데이터 객체를 활용해서 매핑하는 방식을 사용하시면, 코드가 훨씬 깔끔해지고 유지보수하기도 쉬워서 시니어 프론트엔드 개발자들에게 아주 좋은 인상을 줄 수 있습니다. 꼭 기억해두세요!)