본문 바로가기

front/react

데이터를 기준으로 DOM을 제어해보았다.

내가 만든 구조의 의도

  • 혜택 노출 여부를 DOM이 아니라 데이터로 결정한다.
  • 데이터 갱신은 오직 한 곳에서만 한다.
    (데이터의 진입점을 단일화 — React의 “상태 관리 컴포넌트”처럼 동작)
  • 전역 상태(benefitList)를 모든 화면 컴포넌트가 구독하는 구조.
  • 렌더링 주기가 거의 없기 때문에, 동적 프레임워크 도입보다 효율적.
    즉, 정적이지만 상태 일관성이 필요한 페이지에 맞춤형으로 설계한 코드야.
// main.js
var benefitList = {};

.
.
.

    if (Array.isArray(discountList) && discountList.length > 0) {

       // 청구할인율과 전시혜택 신용카드의 할인율은 같아야 함. 
       const isCreditCard = (order) => order.payMnsCtlId === '01';
       const isAppCard = (order, promo) => order.payMnsCtlId === '39' && order.crcCtlId === promo.crcCtlId;
       // 결제수단: (신용카드, 앱카드 제외) 무통장, 네이버페이, 카카오페이등
       const isPayMethodValid = (order, promo) => order.payMnsCtlId !== '39' && order.crcCtlId === null && order.payMnsCtlId !== promo.crcCtlId;
    
....

       discountList.forEach(promo => {
          const promoKey = promo.crcCtlId;
          if (Array.isArray(orderPayMnsBenefitList) && orderPayMnsBenefitList.length > 0) {
             orderPayMnsBenefitList
                .filter(order =>
                   isCreditCard(order) && order.crcCtlId === promoKey && isValidDiscount(promo, order) ||
                   isAppCard(order, promo) && isValidDiscount(promo, order) ||
                   isPayMethodValid(order, promo)
                )
                .forEach(order => {
                   if (isCreditCard(order) && isValidDiscount(promo, order)) {
                      addBenefit(order.crcCtlId, {
                         cardCoNm: promo.cardCoNm || '',
                         claimDsnRt: String(promo.ofrAmt),
                      ...
                      });
                   }

                   // appCard 배열 정의: orderSheetAppCardPay에서 사용
                   if (isAppCard(order, promo)) {
                      benefitList["appCard"] = benefitList["appCard"] || [];
                      benefitList["appCard"].push({
                         crcCtlId: order.crcCtlId,
                         cardCoNm: promo.cardCoNm || '',
                       ...
                      });
                   }

                   if (isPayMethodValid(order, promo)) {
                    ....
                      addBenefit(order.payMnsCtlId, {
                        ...
                         cardCoNm: promo.cardCoNm || '',
                      });
                   }
                });
          }

          if (!benefitList[promo.crcCtlId]) {
             addBenefit(promo.crcCtlId, {
                cardCoNm: promo.cardCoNm || '',

             });
          }
       });
    }

 

 

//ordersheetpage.js

const isAppCardArray = Array.isArray(benefitList["appCard"]);
const findAppCardBenefit = isAppCardArray ? benefitList["appCard"].find(item => item.crcCtlId === swiperCardCode) : null;
const isSelectCheckCard = swiperCreditFlg === 'N';
const hasBenefit = !!(findAppCardBenefit || benefitList[swiperCardCode]);


$.calculation.orderSheetAmt(function (isSuccess) {
    if(isSuccess) {
       $("#appCardDcInfo").toggle(!isSelectCheckCard && hasBenefit);
       ....
       }}

장점

1. 데이터 일관성 확보

할인/프로모션 계산 로직이 여러 군데 흩어지지 않고, 한 함수(discountList.forEach)에서만 정의되어 있음.
이건 React의 “상태를 단일 책임으로 관리한다”는 원리를 충실히 따름.
즉, benefitList는 단 한 곳에서만 수정되고, 다른 모든 화면은 그 결과만 의존하기 때문에 정합성이 확보.


2. 유지보수 및 디버깅 용이

혜택 계산 규칙이 복잡하더라도,
benefitList만 찍어보면 전체 로직 결과를 바로 알 수 있음.
기존 jQuery 구조였다면 DOM 곳곳에서 .text()나 .append()로
문구를 수동 갱신했을 텐데, 이건 “데이터 먼저, 화면 나중”으로 설계돼서 훨씬 관리하기 쉽다.


3. 렌더링 낭비 없음

React처럼 Virtual DOM을 돌리지 않아도,
이 페이지는 변경이 거의 없는 정적 페이지이기 때문에
한 번 계산된 benefitList를 계속 재사용해도 문제 없음.
즉, 계산은 단 한 번,
이후에는 “읽기 전용”으로 사용하는 구조. 메모리 효율도 괜찮고 CPU 부하도 거의 없음.


4. 전시혜택 vs 프로모션 구분이 명확

isCreditCard, isAppCard, isPayMethodValid로 결제수단별 구분이
정의 함수로 분리되어 있어서,
업무 로직(도메인 로직)을 읽는 사람이 바로 이해 가능함.
이건 실무적으로 “업무 규칙의 코드화”가 잘 된 예야.

 

단점

전역 상태의 위험 (side-effect 가능성)

benefitList가 전역이라 다른 코드가 실수로 수정할 수 있음.
특히 같은 스크립트 블록에 benefitList = [] 재정의가 들어가면
모든 UI가 깨져버려.
이건 전역 변수로 상태를 관리하는 고질적 문제지,
너의 로직이 잘못된 건 아니야.
(해결책이라면 Object.freeze나 네임스페이스 캡슐화 정도.)


2. 데이터-뷰 간 단방향 동기화의 한계

현재 구조는 “데이터 → 화면”만 반영하지,
화면에서 유저가 뭔가 조작해도
그게 다시 benefitList로 반영되지는 않아.
즉, 단방향 데이터 흐름만 있음.

이건 좋은 선택이지만,
추후 할인 관련 UI가 “동적 변경”을 요구하게 되면 (예: 쿠폰 선택, 카드 변경)
그때는 리렌더링 로직을 새로 짜야 해.
지금은 단순한 구조라 문제 없지만,
확장성은 낮은 편.


3. 할인 조건이 늘어나면 코드 복잡도 상승

현재는 isCreditCard, isAppCard, isPayMethodValid 세 가지 조건이지만
실무에서는 “간편결제”, “포인트 복합 결제”, “이중 혜택 불가” 같은
조건이 추가될 수 있잖아?
이럴 경우 if / filter / forEach 블록이 기하급수적으로 길어짐.
즉, 규칙 기반 시스템으로 발전하면 리팩터링이 필요해질 거야.
(예: 룰 매핑 객체나 전략 패턴으로 분리 가능)


4. 타임리프 데이터 주입 의존성

[[${orderSheet.orderPayMnsBenefitList}]]와 같이
서버에서 데이터 주입받는 구조라,
자바스크립트 단독 실행 테스트가 어려움.
즉, 이 코드는 템플릿 환경에 강하게 종속됨.
결국 브라우저에서만 테스트 가능해서 CI/CD 자동화나 유닛테스트가 힘들지.


5. 데이터 캐싱 타이밍 불명확

혜택 데이터는 “페이지 최초 로드 시점” 기준으로 계산되잖아?
근데 만약 주문서가 오래 켜져 있고,
그 사이 프로모션이 만료되면
UI는 그대로지만 데이터는 실제 서버와 달라질 수 있음.
즉, “실시간성”은 보장되지 않아.
이건 현실적으로 큰 문제는 아니지만,
정확성을 요구하는 환경이라면 API 새로고침 타이머가 필요할 수도 있어.

 

 

만약 기존 운영대로 했다면?

 

$('.benefit-item').each(function() {
  const $this = $(this);
  const promoId = $this.data('promo-id');
  const discount = $this.find('.discount').text();

  if (discount && Number(discount) > 0) {
    $this.show();
  } else {
    $this.hide();
  }
});

이런식으로 했을 것. 템플릿에서 데이터를 긁어와 간단하게 조건만 정의하고 조건대로 show, hide하는 것도 좋은 방법이 됐을 것이다.

하지만 이렇게 했다면 공통 데이터 없이 페이지마다 같은 로직을 조금씩 수정해서 쓰고
예를 들어, “카드 할인 문구”를 3군데에서 노출해야 한다면

  • orderPayment.js
  • promotionPopup.js
  • benefitLayer.js

이렇게 세 파일에 비슷한 if문이 각각 존재하고,
운영 중에 한 곳만 수정되면 싱크가 어긋남. 실제 페이지별로 하드코딩을 한 덕에 문구 띄어쓰기나 오타가 발견됐다.


대표적 문제점

  • DOM을 자주 탐색해서 성능이 떨어지고
  • 변경 시 사이드 이펙트가 큼 (한 곳 수정했는데 다른 화면이 깨짐)
  • 데이터 의존 관계를 파악하기 어려움
  • QA나 기획이 “이 문구 왜 안 떠요?” 물어보면
    실제로는 코드 5개 파일을 다 열어봐야 원인을 알 수 있음. (이게 진짜 많았음)

그래서 나는 “한 군데서 데이터 정리 → 전역 저장 → 각 영역이 그걸 가져다 씀”으로 설계했는데
gpt는 이러한 방식이 단일 진실의 원천(single source of truth) 구조라고 알려주었다.
렌더 주기가 짧지 않은 페이지라면 전역 캐시 기반 관리가 가장 효율적이라는데, 현재 리액트 전환이 될 때 개발자도구에서 전역 값이 검색하면 나오는 점도 보완되어 훨씬 더 나아질 것으로 예상한다.