바닐라JS에서 Redux 실습
1. 바닐라JS에서 직접 만들어보기
1.1 Redux 설치하기
환경에 따라 설치한 후 실습해보시길 바랍니다.
// cdn
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.2.0/redux.js"></script>
1.2 요구사항
Google Fonts 사이트와 같은 사이트를 만들 것입니다. 여기서 sentence에 text를 입력하면 모든 font box에 반영이 됩니다. 이 기능을 구현해보도록 하겠습니다.
Google Fonts
최종 화면은 아래와 같습니다.
- Click Me : 입력한 텍스트 반영
- Rounded, Circle, Rect : 모든 박스에 각각 border-radius 10px, 15px, 0px 반영
- Red, Green, Blue : 모든 박스에 Border color 변경

1.3 정답코드
Google Fonts 클론 정답 코드 (꼭 다 푸신 다음 확인하세요)
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Anek+Malayalam:wght@300&family=Fascinate&family=Kdam+Thmor+Pro&family=Noto+Sans+KR:wght@300&family=Open+Sans&family=Roboto:wght@300&display=swap"
rel="stylesheet">
<style>
.container {
border: 5px solid black;
padding: 10px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.item {
border: 5px solid #f6866a;
height: 100px;
padding: 20px;
font-size: 32px;
}
.item:nth-child(1) {
font-family: 'Anek Malayalam', sans-serif;
}
.item:nth-child(2) {
font-family: 'Fascinate', cursive;
}
.item:nth-child(3) {
font-family: 'Kdam Thmor Pro', sans-serif;
}
.item:nth-child(4) {
font-family: 'Noto Sans KR', sans-serif;
}
.item:nth-child(5) {
font-family: 'Open Sans', sans-serif;
}
.item:nth-child(6) {
font-family: 'Roboto', sans-serif;
}
</style>
</head>
<body>
<input type="text" id="inputText">
<input type="button" value="눌러버렷!" onclick="
const value = document.getElementById('inputText').value
store.dispatch({type:'TEXT', text:value})
" />
<input type="button" value="Rounded!" onclick="
store.dispatch({type:'STYLE_BORDER', borderRadius:'10px'})
" />
<input type="button" value="Circle!" onclick="
store.dispatch({type:'STYLE_BORDER', borderRadius:'20px'})
" />
<input type="button" value="Rect!" onclick="
store.dispatch({type:'STYLE_BORDER', borderRadius:'0px'})
" />
<input type="button" value="Red!" onclick="
store.dispatch({type:'STYLE_BORDER_COLOR', border:'5px solid red'})
" />
<input type="button" value="Green!" onclick="
store.dispatch({type:'STYLE_BORDER_COLOR', border:'5px solid green'})
" />
<input type="button" value="Blue!" onclick="
store.dispatch({type:'STYLE_BORDER_COLOR', border:'5px solid blue'})
" />
<br>
<br>
<div class="container">
<div id="item1" class="item"></div>
<div id="item2" class="item"></div>
<div id="item3" class="item"></div>
<div id="item4" class="item"></div>
<div id="item5" class="item"></div>
<div id="item6" class="item"></div>
</div>
<script>
const store = Redux.createStore(reducer)
function reducer(state, action) {
console.log(state, action)
if (state === undefined) {
return {
text: 'hello',
color: 'black',
border: '5px solid #f6866a',
borderRadius: '0px'
}
} else if (action.type === 'TEXT') {
return {
...state,
text: action.text
}
} else if (action.type === 'STYLE_BORDER') {
return {
...state,
borderRadius: action.borderRadius
}
} else if (action.type === 'STYLE_BORDER_COLOR') {
return {
...state,
border: action.border
}
}
}
function item1() {
const state = store.getState()
const $item = document.querySelector('#item1')
$item.innerText = `${state.text}`
$item.style.borderRadius = `${state.borderRadius}`
$item.style.border = `${state.border}`
}
store.subscribe(item1)
item1()
function item2() {
const state = store.getState()
const $item = document.querySelector('#item2')
$item.innerText = `${state.text}`
$item.style.borderRadius = `${state.borderRadius}`
}
store.subscribe(item2)
item2()
function item3() {
const state = store.getState()
const $item = document.querySelector('#item3')
$item.innerText = `${state.text}`
$item.style.borderRadius = `${state.borderRadius}`
}
store.subscribe(item3)
item3()
function item4() {
const state = store.getState()
const $item = document.querySelector('#item4')
$item.innerText = `${state.text}`
$item.style.borderRadius = `${state.borderRadius}`
}
store.subscribe(item4)
item4()
function item5() {
const state = store.getState()
const $item = document.querySelector('#item5')
$item.innerText = `${state.text}`
$item.style.borderRadius = `${state.borderRadius}`
}
store.subscribe(item5)
item5()
function item6() {
const state = store.getState()
const $item = document.querySelector('#item6')
$item.innerText = `${state.text}`
$item.style.borderRadius = `${state.borderRadius}`
}
store.subscribe(item6)
item6()
</script>
</body>
</html>
1.4 추가 문제
Airbnb와 비슷한 서비스를 만들어보도록 하겠습니다. 오른쪽에 있는 지도는 구현할 필요가 없고, 위 Google fonts 서비스를 참고하여 ‘무료 주차 공간, 대형 욕조, 바베큐 그릴, 게스트용 별채, 수영장, 주방, TV’에 따라 각 방들의 테두리가 변하게, 또는 나타나게 해주세요.
일반 JavaScript의 filter를 사용한 것과 어떤 차이가 있는지도 비교해보세요.
- 가능하면 위 Google Fonts 정답을 보면서 직접 짜보려 해보세요.
- 어려우시면 LLM에게 힌트 정도를 구해보세요.
- 그래도 못풀겠다면 LLM에게 문제 전체를 Google Fonts 코드와 함께 올리고, 정답을 받은 다음, 꼭 손으로 써보세요.

Airbnb 필터링 정답 코드
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.js"></script>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
.filter-btn {
padding: 8px 16px;
margin: 5px;
border: 2px solid #ddd;
background: white;
cursor: pointer;
}
.filter-btn.active {
background: #ff5a5f;
color: white;
}
.listing {
border: 3px solid #ddd;
padding: 15px;
margin: 10px 0;
}
.listing.hidden {
display: none;
}
.amenity {
display: inline-block;
background: #f0f0f0;
padding: 2px 6px;
margin: 2px;
font-size: 12px;
}
.amenity.highlight {
background: #ff5a5f;
color: white;
}
</style>
</head>
<body>
<h1>Redux Airbnb 필터링</h1>
<div>
<h3>편의시설 필터:</h3>
<button class="filter-btn" onclick="toggleFilter('무료 주차 공간')">무료 주차 공간</button>
<button class="filter-btn" onclick="toggleFilter('대형 욕조')">대형 욕조</button>
<button class="filter-btn" onclick="toggleFilter('바베큐 그릴')">바베큐 그릴</button>
<button class="filter-btn" onclick="toggleFilter('게스트용 별채')">게스트용 별채</button>
<button class="filter-btn" onclick="toggleFilter('수영장')">수영장</button>
<button class="filter-btn" onclick="toggleFilter('주방')">주방</button>
<button class="filter-btn" onclick="toggleFilter('TV')">TV</button>
</div>
<div id="resultsInfo"></div>
<div id="listingsContainer">
<!-- 숙소 목록이 여기에 렌더링 -->
</div>
<script>
// 샘플 데이터
const listings = [
{ id: 1, title: "도심 아파트", location: "강남구", price: "85,000원", amenities: ["무료 주차 공간", "주방", "TV"] },
{ id: 2, title: "럭셔리 펜트하우스", location: "한남동", price: "250,000원", amenities: ["대형 욕조", "수영장", "게스트용 별채", "주방", "TV"] },
{ id: 3, title: "바다뷰 펜션", location: "해운대", price: "120,000원", amenities: ["바베큐 그릴", "주방", "무료 주차 공간"] },
{ id: 4, title: "산속 별장", location: "평창", price: "95,000원", amenities: ["바베큐 그릴", "게스트용 별채", "무료 주차 공간", "TV"] },
{ id: 5, title: "모던 스튜디오", location: "홍대", price: "65,000원", amenities: ["주방", "TV"] },
{ id: 6, title: "리조트 빌라", location: "제주도", price: "180,000원", amenities: ["수영장", "대형 욕조", "바베큐 그릴", "무료 주차 공간", "주방", "TV"] }
];
// Redux 스토어 생성
const store = Redux.createStore(reducer);
function reducer(state, action) {
console.log('Redux Action:', action); // Redux 액션 로깅
if (state === undefined) {
return {
listings: listings,
activeFilters: [],
filteredListings: listings
};
}
if (action.type === 'TOGGLE_FILTER') {
const newFilters = state.activeFilters.includes(action.filter)
? state.activeFilters.filter(f => f !== action.filter)
: [...state.activeFilters, action.filter];
const filteredListings = newFilters.length === 0
? state.listings
: state.listings.filter(listing =>
newFilters.every(filter => listing.amenities.includes(filter))
);
return {
...state,
activeFilters: newFilters,
filteredListings: filteredListings
};
}
return state;
}
// 액션 디스패치
function toggleFilter(filter) {
store.dispatch({ type: 'TOGGLE_FILTER', filter: filter });
}
// UI 업데이트 함수들
function updateUI() {
const state = store.getState();
console.log('Current State:', state); // 현재 상태 로깅
// 필터 버튼 업데이트
document.querySelectorAll('.filter-btn').forEach(btn => {
const filterText = btn.textContent.trim();
btn.classList.toggle('active', state.activeFilters.includes(filterText));
});
// 결과 정보 업데이트
const resultsInfo = document.getElementById('resultsInfo');
resultsInfo.innerHTML = `<p><strong>활성 필터:</strong> ${state.activeFilters.join(', ') || '없음'} | <strong>결과:</strong> ${state.filteredListings.length}개 숙소</p>`;
// 숙소 목록 업데이트
const container = document.getElementById('listingsContainer');
container.innerHTML = state.filteredListings.map(listing => `
<div class="listing">
<h4>${listing.title} - ${listing.location}</h4>
<p>가격: ${listing.price}</p>
<p>편의시설:
${listing.amenities.map(amenity =>
`<span class="amenity ${state.activeFilters.includes(amenity) ? 'highlight' : ''}">${amenity}</span>`
).join(' ')}
</p>
</div>
`).join('');
}
// 스토어 구독 - 상태가 변경될 때마다 UI 업데이트
store.subscribe(updateUI);
// 초기 렌더링
updateUI();
/*
Claude에게 지금 프로젝트가 오버엔지니어링이 아니냐고 물어봤을 때 답변입니다.
## 현재 프로젝트에서는 Redux가 과한 이유
**일반 Filter 방식:**
```javascript
let activeFilters = [];
function toggleFilter(filter) {
if (activeFilters.includes(filter)) {
activeFilters = activeFilters.filter(f => f !== filter);
} else {
activeFilters.push(filter);
}
updateUI();
}
```
- 코드 10줄로 끝
- 이해하기 쉬움
- 성능도 충분
**Redux 방식:**
- 코드 50줄 이상
- 개념 학습 필요 (store, reducer, action, dispatch)
- 설정이 복잡
## Redux가 진짜 필요한 상황들
### 1. **복잡한 상태 공유**
```javascript
// 여러 컴포넌트에서 같은 데이터를 사용할 때
- Header: 로그인 상태, 장바구니 개수
- Sidebar: 사용자 정보
- Main: 상품 목록
- Footer: 알림 개수
```
### 2. **상태 변화의 복잡성**
```javascript
// 하나의 액션이 여러 상태를 동시에 변경
- "상품 구매" 버튼 클릭 시:
* 재고 수량 감소
* 장바구니에서 제거
* 주문 내역에 추가
* 포인트 적립
* 알림 생성
```
### 3. **디버깅이 중요한 경우**
```javascript
// 버그 발생 시 "어떤 순서로 상태가 변했는지" 추적 필요
- 은행 앱: 잔액 변화 추적
- 게임: 점수/레벨 변화 추적
- 전자상거래: 주문 과정 추적
```
### 4. **실제 Airbnb 같은 서비스라면?**
```javascript
// 실제로는 이런 복잡한 상태들이 있음:
{
user: { id, name, preferences },
filters: { amenities, price, location, dates },
listings: { data, loading, error },
booking: { selectedDates, guestCount, totalPrice },
map: { center, zoom, markers },
reviews: { data, rating },
payments: { methods, processing }
}
```
## 예측 가능한 상태 변화가 중요한 이유
### 버그 발생 시나리오:
```javascript
// 일반 방식 - 어디서 상태가 변했는지 모름
function someFunction() {
activeFilters.push("수영장");
// ... 100줄 후
activeFilters = []; // 누가 언제 이걸 했지?
}
// Redux 방식 - 모든 변화가 기록됨
// Action Log:
// 1. { type: 'ADD_FILTER', filter: '수영장' }
// 2. { type: 'CLEAR_FILTERS' }
```
## 결론: 언제 Redux를 써야 할까?
**Redux 필요 없는 경우:**
- 단순한 필터링, 검색
- 컴포넌트 1-2개
- 상태가 간단함
- 개인 프로젝트나 프로토타입
**Redux 고려해야 하는 경우:**
- 컴포넌트 5개 이상에서 같은 상태 공유
- 복잡한 비즈니스 로직
- 팀 프로젝트 (상태 변화 규칙 통일)
- 사용자 액션 추적이 중요한 앱
**현재 프로젝트는 학습용이라 Redux를 쓰지만, 실제로는 일반 filter가 더 적합합니다!** Redux는 "복잡한 상태 관리가 필요할 때의 해결책"이지, "모든 상황의 정답"은 아니에요.
*/
</script>
</body>
</html>