본문 바로가기

바닐라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
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를 사용한 것과 어떤 차이가 있는지도 비교해보세요.

  1. 가능하면 위 Google Fonts 정답을 보면서 직접 짜보려 해보세요.
  2. 어려우시면 LLM에게 힌트 정도를 구해보세요.
  3. 그래도 못풀겠다면 LLM에게 문제 전체를 Google Fonts 코드와 함께 올리고, 정답을 받은 다음, 꼭 손으로 써보세요.
Airbnb
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>
바닐라JS에서 Redux 실습 - React 에센셜 | 위니버시티