컴포넌트 함수 내부에서 특정 값에 따라 선택적으로 렌더링하는 것을 조건부 렌더링이라고 한다. 가독성을 높이는 조건부 렌더링 방법과 특징을 알아보고 어떻게 가장 최적화된 코드를 작성할 수 있는지 알아보았다.
function GreetingA({ isLogin, name }) {
if (isLogin) {
return <p>{`${name}님 안녕하세요.`}</p>;
} else {
return <p>권한이 없습니다.</p>;
}
}
function GreetingB({ isLogin, name }) {
return <p>{isLogin ? `${name}님 안녕하세요.` : '권한이 없습니다.'}</p>
}
리액트 공식 문서에서 소개하는 가장 기본적인 패턴이다. GreetingA 함수의 경우 if문 조건인 로그인 여부에 따라, 어떤 요소를 렌더링할지 결정한다. 이 패턴은 컴포넌트의 일부분만 선택적으로 렌더링하기 적합하지 않다. GreetingB 함수는 삼항연산자를 활용해서 조건부렌더링을 한다. if else 패턴의 단점인 부분 렌더링이 가능하도록 개선한 패턴이다. GreetingB의 코드가 더 간결하고 p 태그가 한 번만 등장해서 GreetingA보다 좋은 것 같지만, 매번 그렇지는 않다. 컴포넌트를 다른 조건에 따라 다르게 렌더링하는 게 아니라, 컴포넌트 렌더링 여부 자체를 결정할 때는 불필요하게 null을 반환하는 코드를 작성해야 한다.
function GreetingB({ isLogin, name }) {
return (
<React.Fragment>
{isLogin ? <p>`${name}님 안녕하세요.`</p> : null }
</React.Fragment>
)
}
null을 반환한다고 해서 심각한 성능 저하가 발생하지는 않기 때문에 그대로 사용해도 되지만 이 부분은 && 패턴으로 더 효율적으로 개선할 수 있다.
조건부 렌더링을 구현할 때는 삼항 연산자가 유용한 경우도 있지만, 대부분 && 연산자가 가독성이 더 좋다. && 패턴은 (선행 조건) && (후행 조건) 논리 연산자는 선행 조건이 참이어야만 후행 조건을 평가하고, 후행 조건을 평가한 결과를 반환한다.
function Greeting({ isLogin, name, cash }) {
return (
<div>
저희 사이트에 방문해주셔서 감사합니다.
{
isLogin && (
<div>
<p>{name}님 안녕하세요.</p>
<p>현재 보유하신 금액은 {cash}원입니다.</p>
</div>
)
}
</div>
)
}
컴포넌트의 일부분만 선택적으로 렌더링할 수 있을 뿐만 아니라, 코드의 끝에 null을 생략해도 되기 때문에 가독성이 좋아진다.
실제 프로젝트에서는 여러 개의 조건에 따라 다양한 상태를 렌더링해야 하는 경우가 많은데 이런 경우 && 패턴이 의미있게 사용될 수 있다. 다음 코드는 메인페이지에서 보여줄 문구를 조건에 따라 &&패턴으로 표현한 결과다. 조건은 다음과 같다.
function Greeting() {
return (
<div>
저희 사이트에 방문해주셔서 감사합니다.
{isEvent && (
<div>
<p>오늘의 이벤트를 놓치지 마세요.</p>
<button onClick={onClickEvent}>이벤트 참여하기</button>
</div>
)}
{!isEvent &&
isLogin &&
cash <= 100000 && (
<div>
<p>{name}님 안녕하세요.</p>
<p>현재 보유하신 금액은 {cash}원입니다.</p>
</div>
)}
</div>
)
}
이벤트 여부 또는 사용자의 상태에 따라 조건이 복잡한데도 코드가 크게 두 그룹이라는 점, 그리고 각 그룹의 조건들이 한눈에 잘 드러난다.
&& 패턴을 사용할 때는 주의할 점이 있다. 변수가 null 또는 undefined일 때를 꼭 고려해야 하는 경우도 있다는 점을 생각해야 한다. 변수가 숫자 타입인 경우, 0은 false고, 문자열 타입인 경우 빈 문자열도 false다. 다음 코드는 && 연산자를 잘못 사용한 예시다.
// 잘못된 예시
<div>
{cash && <p>{cash}원 보유 중</p>}
{memo && <p>{200 - memo.length}자 입력 가능</p>}
</div>
&& 연산자를 사용할 때 내가 자주 실수하는 내용이다. cash가 숫자 형태인데 0이면 '0원 보유 중'이 출력되지 않을 것이다. 또 memo에 아무 내용이 없으면 '200자 입력 가능'이라고 출력되어야 하는데 출력되지 않는다. 위 경우에는 명확하게 undefined, null이 아닌 경우라고 표현해야 한다. 다음은 의도한 대로 동작하도록 고친 코드다.
// 올바른 예시
<div>
{cash !== null && <p>{cash}원 보유 중</p>}
{memo !== null && <p>{200 - memo.length}자 입력 가능</p>}
</div>
cash !== null 은 cash가 undefined가 아니고 null도 아니면 참이 된다.
변수가 배열인 경우에는 기본값으로 빈 배열을 넣어 주는 게 좋다. 배열의 기본값이 빈 배열이면 조건부 렌더링을 할 때마다 &&을 입력하지 않아도 되기 때문에 편하게 map 함수를 사용할 수 있다.
<div>
{
students && students.map((student, idx) => {
<div key={idx}>{student}</div>
})
}
</div>
students 배열의 기본값으로 빈 배열을 설정해주지 않는다면 students && 또는 students.length > 0 && 를 입력해주어야 map 돌리는 대상이 undefined라는 오류창을 피할 수 있다.
<div>
{
products.map((product, idx) => {
<div key={idx}>{product}</div>
})
}
</div>
product 배열의 기본값을 빈 배열로 설정하면 코드가 간결해진다.
const RealTimeUsagePage = () => {
const paramsId = useParams().id
const [ params, setParams ] = useState(paramsId)
useEffect(() => setParams(paramsId), [paramsId])
const choosePage = () => {
switch (params) {
case 'bytime':
return <RealTimeUsageByTime />
case 'bydate':
return <RealTimeUsageByDate />
case 'bymonth':
return <RealTimeUsageByMonth />
case 'byyear':
return <RealTimeUsageByYear />
default:
return <RealTimeUsageByTime />
}
}
return (
<React.Fragment>{choosePage()}</React.Fragment>
);
}
현재 진행중인 프로젝트에서 조금 변경한 코드를 가져왔다. 선택한 메뉴에 따라 전체 페이지 컴포넌트를 다르게 보여주기 위해 useParams()에 switch case를 사용했다. Switch case는 if else문과 비슷한 방법으로 코드를 작성할 수 있고, 마찬가지로 부분 렌더링이 어렵다. 이 방법은 TypeScript와 작성되었을 때 시너지가 난다고 한다.
&& 은 약간 hacky 하고, truthy/falsey 한 두 경우를 모두 커버해야 하는 상황이 되면 다시 a ? b : c 형태로 돌아오기 때문에, 저는 처음부터 삼항 연산자를 쓰는것을 선호합니다.
undefined, null 모두 falsey하기 때문에, && 다음의 내용은 실행되지 않습니다.
cash ?? 0 > 0 && ... 그리고 memo?.length ?? 0 > 0 && ... 가 원래 의도하신 바가 아닌가 싶어요.
배열의 경우도 array && array.map(...) 을 array?.map(...) 으로 간소화할 수 있겠습니다.