[이제와서 시작하는 React 마스터하기 #6] 조건부 렌더링과 리스트 - 동적 UI 만들기
React Day 6 - 이제 버튼을 클릭하면 화면이 바뀌고, 리스트를 렌더링할 수 있어요! 오늘은 조건에 따라 다른 화면을 보여주고, 배열 데이터를 화면에 표시하는 방법을 배워봅시다.
오늘 배울 내용
- ✅ 조건부 렌더링: if/else, 삼항 연산자, &&로 화면 제어하기
- ✅ 리스트 렌더링: map()으로 배열 데이터 표시하기
- ✅ key 속성: React가 리스트를 효율적으로 관리하는 방법
- ✅ 실전 예제: 할 일 목록, 필터링, 검색 기능
시작하기 전에
이전에 배운 내용을 기억하시나요?
- Day 4: Props에서 컴포넌트끼리 데이터를 주고받았어요
- Day 5: useEffect에서 컴포넌트 생명주기를 다뤘어요
오늘은 이 지식을 바탕으로 조건에 따라 다른 화면을 보여주는 방법과 배열 데이터를 리스트로 표시하는 방법을 배워봅시다!
조건부 렌더링이란?
조건부 렌더링은 특정 조건에 따라 다른 내용을 화면에 보여주는 것을 말해요.
일상 비유 🏠
스마트폰 화면을 생각해보세요:
- 로그인 전: 로그인 화면 표시
- 로그인 후: 메인 화면 표시
- 로딩 중: 로딩 스피너 표시
- 에러 발생: 에러 메시지 표시
React도 똑같아요! 조건에 따라 다른 컴포넌트나 내용을 보여줄 수 있어요.
조건부 렌더링 방법
방법 1: if/else 문
가장 기본적인 방법이에요. 함수 안에서 조건을 확인하고, 다른 JSX를 return해요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function LoginButton() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
// 조건에 따라 다른 화면 반환
if (isLoggedIn) {
return (
<div>
<h1>환영합니다! 👋</h1>
<button onClick={() => setIsLoggedIn(false)}>
로그아웃
</button>
</div>
);
} else {
return (
<div>
<h1>로그인이 필요합니다</h1>
<button onClick={() => setIsLoggedIn(true)}>
로그인
</button>
</div>
);
}
}
실행 결과:
- 처음: “로그인이 필요합니다” + 로그인 버튼
- 로그인 클릭 후: “환영합니다! 👋” + 로그아웃 버튼
방법 2: 삼항 연산자 (? :)
간단한 조건일 때는 삼항 연산자가 더 깔끔해요!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<div>
<h1>
{isLoggedIn ? '환영합니다! 👋' : '로그인이 필요합니다'}
</h1>
<button onClick={() => setIsLoggedIn(!isLoggedIn)}>
{isLoggedIn ? '로그아웃' : '로그인'}
</button>
</div>
);
}
문법: 조건 ? 참일_때 : 거짓일_때
방법 3: && 연산자
특정 조건일 때만 보여주고 싶을 때 사용해요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function App() {
const [showMessage, setShowMessage] = useState(false);
return (
<div style=>
<button onClick={() => setShowMessage(!showMessage)}>
{showMessage ? '메시지 숨기기' : '메시지 보기'}
</button>
{/* showMessage가 true일 때만 메시지 표시 */}
{showMessage && (
<div style=>
안녕하세요! 이것은 조건부 메시지입니다 ✨
</div>
)}
</div>
);
}
문법: 조건 && 표시할_내용
실습 1: 로딩/에러/성공 상태 관리
실제 앱에서 자주 사용하는 패턴이에요!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function App() {
const [status, setStatus] = useState('idle'); // 'idle', 'loading', 'success', 'error'
const [data, setData] = useState(null);
const fetchData = () => {
setStatus('loading');
// 2초 후 랜덤으로 성공/실패
setTimeout(() => {
if (Math.random() > 0.5) {
setData({ name: '홍길동', age: 25 });
setStatus('success');
} else {
setStatus('error');
}
}, 2000);
};
return (
<div style=>
<h1>데이터 가져오기</h1>
<button onClick={fetchData}>
데이터 로드
</button>
{/* 상태에 따른 조건부 렌더링 */}
{status === 'idle' && (
<p>버튼을 클릭해서 데이터를 불러오세요</p>
)}
{status === 'loading' && (
<p>⏳ 로딩 중...</p>
)}
{status === 'success' && data && (
<div style=>
<h2>✅ 성공!</h2>
<p>이름: {data.name}</p>
<p>나이: {data.age}</p>
</div>
)}
{status === 'error' && (
<div style=>
<h2>❌ 에러 발생!</h2>
<p>데이터를 불러오는데 실패했습니다.</p>
<button onClick={fetchData}>다시 시도</button>
</div>
)}
</div>
);
}
해보기:
- 위 코드를 복사해서 실행해보세요
- “데이터 로드” 버튼을 여러 번 클릭해보세요
- 로딩 → 성공/에러 화면이 어떻게 바뀌는지 관찰하세요
리스트 렌더링이란?
리스트 렌더링은 배열 데이터를 화면에 반복해서 표시하는 것을 말해요.
일상 비유 📝
쇼핑 목록을 생각해보세요:
1
2
3
4
5
쇼핑 목록:
1. 사과
2. 바나나
3. 우유
4. 빵
React에서는 이런 리스트를 map() 함수로 만들어요!
map()으로 리스트 만들기
기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
const fruits = ['사과', '바나나', '오렌지', '포도'];
return (
<div style=>
<h1>과일 목록</h1>
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
</div>
);
}
결과:
1
2
3
4
5
과일 목록
• 사과
• 바나나
• 오렌지
• 포도
객체 배열 렌더링
실제 앱에서는 보통 객체 배열을 다뤄요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function App() {
const users = [
{ id: 1, name: '홍길동', age: 25 },
{ id: 2, name: '김영희', age: 30 },
{ id: 3, name: '이철수', age: 28 }
];
return (
<div style=>
<h1>사용자 목록</h1>
{users.map(user => (
<div
key={user.id}
style=
>
<h3>{user.name}</h3>
<p>나이: {user.age}세</p>
</div>
))}
</div>
);
}
key 속성이 왜 필요할까?
리스트를 렌더링할 때 key 속성을 꼭 넣어야 해요. React가 리스트의 각 항목을 추적하기 위해 필요하답니다.
key 사용 규칙
✅ 좋은 예: 고유한 id 사용
1
2
3
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
⚠️ 피해야 할 예: index 사용 (항목이 추가/삭제/정렬되는 경우)
1
2
3
{users.map((user, index) => (
<div key={index}>{user.name}</div>
))}
왜 index는 피해야 할까요?
항목이 추가되거나 삭제되면 index가 바뀌어서 React가 혼란스러워해요. 예를 들어:
- 원래: [0: 사과, 1: 바나나, 2: 오렌지]
- ‘사과’ 삭제 후: [0: 바나나, 1: 오렌지]
- React: “어? 0번이 ‘사과’에서 ‘바나나’로 바뀌었네?” 😵
하지만 고유한 id를 쓰면:
- 원래: [id:1 사과, id:2 바나나, id:3 오렌지]
- 삭제 후: [id:2 바나나, id:3 오렌지]
- React: “id:1이 사라졌구나! 나머지는 그대로네” 😊
실습 2: 할 일 목록 (Todo List)
리스트 렌더링의 대표적인 예제예요!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
function App() {
const [todos, setTodos] = useState([
{ id: 1, text: 'React 공부하기', completed: false },
{ id: 2, text: '장보기', completed: true },
{ id: 3, text: '운동하기', completed: false }
]);
const [input, setInput] = useState('');
// 할 일 추가
const addTodo = () => {
if (input.trim() === '') return;
const newTodo = {
id: Date.now(), // 간단한 고유 ID
text: input,
completed: false
};
setTodos([...todos, newTodo]);
setInput('');
};
// 완료 상태 토글
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
// 할 일 삭제
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div style=>
<h1>📝 할 일 목록</h1>
{/* 입력 폼 */}
<div style=>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="할 일을 입력하세요"
style=
/>
<button
onClick={addTodo}
style=
>
추가
</button>
</div>
{/* 할 일 리스트 */}
{todos.length === 0 ? (
<p style=>
할 일이 없습니다. 추가해보세요!
</p>
) : (
<div>
{todos.map(todo => (
<div
key={todo.id}
style=
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
style=
/>
<span
style=
>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
style=
>
삭제
</button>
</div>
))}
</div>
)}
{/* 통계 */}
<div style=>
전체: {todos.length}개 |
완료: {todos.filter(t => t.completed).length}개 |
미완료: {todos.filter(t => !t.completed).length}개
</div>
</div>
);
}
해보기:
- 할 일을 추가해보세요
- 체크박스를 클릭해서 완료 상태를 바꿔보세요 (취소선 생김)
- 삭제 버튼으로 항목을 지워보세요
- 통계가 실시간으로 업데이트되는지 확인하세요
실습 3: 필터링 기능 추가
할 일 목록에 필터링을 추가해봅시다!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
function App() {
const [todos, setTodos] = useState([
{ id: 1, text: 'React 공부하기', completed: false },
{ id: 2, text: '장보기', completed: true },
{ id: 3, text: '운동하기', completed: false },
{ id: 4, text: '청소하기', completed: true }
]);
const [filter, setFilter] = useState('all'); // 'all', 'active', 'completed'
// 필터링된 할 일 목록
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true; // 'all'
});
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<div style=>
<h1>📝 할 일 목록 (필터링)</h1>
{/* 필터 버튼 */}
<div style=>
<button
onClick={() => setFilter('all')}
style=
>
전체 ({todos.length})
</button>
<button
onClick={() => setFilter('active')}
style=
>
진행중 ({todos.filter(t => !t.completed).length})
</button>
<button
onClick={() => setFilter('completed')}
style=
>
완료 ({todos.filter(t => t.completed).length})
</button>
</div>
{/* 필터링된 리스트 */}
{filteredTodos.length === 0 ? (
<p style=>
{filter === 'active' && '진행 중인 할 일이 없습니다!'}
{filter === 'completed' && '완료된 할 일이 없습니다!'}
{filter === 'all' && '할 일이 없습니다!'}
</p>
) : (
filteredTodos.map(todo => (
<div
key={todo.id}
style=
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
style=
/>
<span
style=
>
{todo.text}
</span>
</div>
))
)}
</div>
);
}
해보기:
- 필터 버튼을 클릭해보세요 (전체/진행중/완료)
- 체크박스를 클릭해서 완료 상태를 바꿔보세요
- 필터가 실시간으로 업데이트되는지 확인하세요
실습 4: 검색 기능
사용자 목록에 검색 기능을 추가해봅시다!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function App() {
const [users] = useState([
{ id: 1, name: '홍길동', email: 'hong@example.com', age: 25 },
{ id: 2, name: '김영희', email: 'kim@example.com', age: 30 },
{ id: 3, name: '이철수', email: 'lee@example.com', age: 28 },
{ id: 4, name: '박민수', email: 'park@example.com', age: 35 },
{ id: 5, name: '정수진', email: 'jung@example.com', age: 27 }
]);
const [searchTerm, setSearchTerm] = useState('');
// 검색 필터링
const filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div style=>
<h1>👥 사용자 검색</h1>
{/* 검색 입력 */}
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="이름 또는 이메일로 검색..."
style=
/>
{/* 검색 결과 통계 */}
<p style=>
{searchTerm
? `"${searchTerm}" 검색 결과: ${filteredUsers.length}명`
: `전체 사용자: ${users.length}명`
}
</p>
{/* 사용자 리스트 */}
{filteredUsers.length === 0 ? (
<div style=>
<h2>😢</h2>
<p>검색 결과가 없습니다</p>
</div>
) : (
filteredUsers.map(user => (
<div
key={user.id}
style=
>
<h3 style=>
{user.name}
</h3>
<p style=>
📧 {user.email}
</p>
<p style=>
🎂 {user.age}세
</p>
</div>
))
)}
</div>
);
}
해보기:
- 검색창에 “홍” 또는 “kim”을 입력해보세요
- 검색어를 지우면 전체 목록이 다시 나타나는지 확인하세요
- 검색 결과 통계가 업데이트되는지 확인하세요
흔한 실수와 해결 방법
실수 1: key를 빼먹음
❌ 잘못된 코드:
1
2
3
{users.map(user => (
<div>{user.name}</div> // key가 없어요!
))}
⚠️ 콘솔 경고: “Warning: Each child in a list should have a unique ‘key’ prop.”
✅ 올바른 코드:
1
2
3
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
실수 2: key로 index 사용 (항목이 변경되는 경우)
❌ 피해야 할 코드:
1
2
3
{todos.map((todo, index) => (
<div key={index}>{todo.text}</div> // 항목이 추가/삭제되면 문제!
))}
✅ 올바른 코드:
1
2
3
{todos.map(todo => (
<div key={todo.id}>{todo.text}</div> // 고유한 id 사용
))}
실수 3: 조건부 렌더링에서 false/null/undefined 실수
❌ 주의해야 할 코드:
1
{count && <p>카운트: {count}</p>} // count가 0이면 화면에 "0"이 표시됨!
✅ 올바른 코드:
1
{count > 0 && <p>카운트: {count}</p>} // 명확한 조건
또는
1
{count !== 0 && <p>카운트: {count}</p>}
실수 4: map()에서 직접 return 안 함
❌ 잘못된 코드:
1
2
3
{users.map(user => {
<div>{user.name}</div> // return이 없어요!
})}
✅ 올바른 코드 (중괄호 사용 시 return 필요):
1
2
3
{users.map(user => {
return <div key={user.id}>{user.name}</div>;
})}
✅ 또는 (괄호 사용 - return 생략 가능):
1
2
3
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
정리
오늘은 조건부 렌더링과 리스트 렌더링을 배웠어요!
핵심 포인트 ✅
조건부 렌더링:
if/else: 완전히 다른 JSX를 반환할 때삼항 연산자 (? :): 간단한 조건일 때&& 연산자: 특정 조건일 때만 보여줄 때
리스트 렌더링:
map()함수로 배열을 JSX로 변환- key 속성 필수: 고유한 id 사용 (index는 피하기)
filter()로 조건에 맞는 항목만 표시
실전 패턴:
- 로딩/에러/성공 상태 관리
- 할 일 목록 (추가/삭제/완료)
- 필터링과 검색 기능
오늘 배운 내용 체크 ✅
- if/else, 삼항 연산자, && 연산자의 차이를 이해했나요?
- map()으로 배열을 리스트로 변환할 수 있나요?
- key 속성이 왜 필요한지 이해했나요?
- filter()로 리스트를 필터링할 수 있나요?
- 할 일 목록 앱을 직접 만들어봤나요?
숙제 📚
- 쇼핑 카트 만들기: 상품 추가/삭제, 총 가격 계산
- 게시판 만들기: 글 목록, 검색, 페이지네이션
- 날씨 앱: 도시별 날씨 정보 표시 (조건부 아이콘)
다음 단계
다음 포스트에서는 Context API를 배워봅니다! Props를 여러 컴포넌트에 전달하는 불편함을 해결하는 방법을 알아봐요.
“늦었다고 생각할 때가 가장 빠를 때입니다. 오늘도 한 걸음 나아갔어요! 🎉”
React 완벽 가이드 시리즈
- React란 무엇인가? 시작하기 전 알아야 할 모든 것
- 첫 React 앱 만들기 - JSX와 컴포넌트
- useState - 클릭하면 바뀌는 화면 만들기
- Props - 컴포넌트끼리 대화하기
- useEffect - 컴포넌트 생명주기
- 조건부 렌더링과 리스트 - 동적 UI 만들기 ← 현재 글
- Context API로 전역 상태 관리하기
- Custom Hooks 만들기
- React Router 완벽 가이드
- 폼 처리와 유효성 검증
- React 성능 최적화
- 에러 바운더리와 Suspense
- 상태 관리 라이브러리
- 서버 상태 관리
- React와 TypeScript
- Next.js 입문
- React 테스팅 전략
- 고급 패턴과 베스트 프랙티스
- 실전 프로젝트
- React 배포와 DevOps
