-
Notifications
You must be signed in to change notification settings - Fork 50
[5팀 오태준] Chapter2-1. 프레임워크 없이 SPA 만들기 #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- vite.config.js 추가 - 저장소 경로(/front_7th_chapter2-1/)를 base로 설정 - 빌드 시 올바른 정적 자산 경로 생성
- deploy.sh: 자동 배포 스크립트 구현 - main 브랜치에서 빌드 후 gh-pages 브랜치로 배포 - 빌드 파일 자동 복사 및 커밋 - package.json: deploy 스크립트 추가 - npm run deploy: 빌드 및 배포 자동화 - npm run deploy:gh-pages: git subtree를 통한 배포
- Router 클래스 구현 - 경로 기반 라우팅 시스템 - popstate 이벤트를 통한 브라우저 뒤로가기/앞으로가기 지원 - data-link 속성을 가진 링크 클릭 시 SPA 네비게이션 처리 - 동적 경로 파라미터 지원 (예: /product/:id) - 404 페이지 라우팅 처리
- cartService.js: 장바구니 상태 관리 서비스 - localStorage를 활용한 장바구니 데이터 영구 저장 - getCart: 장바구니 조회 - addToCart: 상품 추가/수량 증가 - removeFromCart: 상품 제거 - updateCartItemQuantity: 수량 업데이트 - clearCart: 장바구니 비우기 - getSelectedItems: 선택된 상품 조회 - toggleItemSelection: 상품 선택/해제 - toggleAllSelection: 전체 선택/해제 - 에러 처리 및 예외 상황 대응
- toast.js: 사용자 피드백을 위한 토스트 메시지 시스템 - 성공/정보/에러 타입별 스타일링 - 자동 사라짐 기능 (3초) - 수동 닫기 버튼 제공 - 여러 토스트 동시 표시 지원
- productList.js: 상품 목록 페이지 - 무한 스크롤 페이지네이션 - 검색, 카테고리 필터, 정렬 기능 - 로딩/에러 상태 처리 - 브레드크럼 네비게이션 - productDetail.js: 상품 상세 페이지 - 상품 정보 상세 표시 - 관련 상품 추천 - 장바구니 추가 기능 - cart.js: 장바구니 페이지 - 모달 형태의 장바구니 UI - 수량 조절, 선택 삭제, 전체 삭제 기능 - 총 금액 계산 및 표시 - notFound.js: 404 에러 페이지 - 존재하지 않는 경로 접근 시 표시
- pnpm/action-setup@v4에서 version: latest 제거 - package.json의 packageManager 필드를 자동으로 사용하도록 변경 - 버전 불일치 에러(ERR_PNPM_BAD_PM_VERSION) 해결
- GitHub Actions의 pnpm 버전 충돌 해결 - packageManager 필드 제거로 workflow의 version: latest와 충돌 방지
- vite.config.js의 base 경로를 개발 환경에서는 '/'로 설정 - 프로덕션 환경에서만 '/front_7th_chapter2-1/' 사용 - MSW Service Worker가 개발 환경에서 정상적으로 로드되도록 수정
eveneul
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
태준 님! 과제 수행해 주신 거 잘 봤습니다!
router.js를 제가 이전 기수에서 봤다면 배끼고 싶었을 만큼 탐이 나는 코드네요!!
product.js 파일이 긴 느낌이 있는데, header, footer, loading 등 UI 컴포넌트들은 따로 파일로 구분해서 함수를 호출하는 식으로 하면 더 간결하게 할 수도 있었을 것 같아요
4주차도 고생하셨습니다! 5주차도 빠이팅 해 봅시다~~
| </div> | ||
| </div> | ||
| </main> | ||
| <footer class="bg-white shadow-sm sticky top-0 z-40"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
header, footer도 컴포넌트로 나누면 어떨까요?
const Footer = () => ...
// 사용할 때는
`${Footer()}`
| <!-- 2depth 카테고리 --> | ||
| </div> | ||
| <!-- 기존 필터들 --> | ||
| <div class="flex gap-2 items-center justify-between"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요 셀렉트도 컴포넌트로!
| try { | ||
| localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cart)); | ||
| } catch (error) { | ||
| console.error("장바구니 저장 실패:", error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
console.log도 좋지만, 토스트 UI 호출로 사용자에게 장바구니에 상품이 담기지 않았다는 걸 알려 주면 좋을 것 같아요! 그러면 장바구니에 담기지 않은 상품을 고객이 알아서 다시 장바구니에 상품을 담지 않을까~~.. 라는 제 생각!
JunilHwang
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 피드백은 n8n + ai (gpt-5-mini)를 활용하여 자동으로 생성된 내용입니다.
이번 PR은 요구사항에 맞춰 상품 목록, 장바구니, 상품 상세, SPA 라우팅, 토스트 알림까지 전반적인 기능을 잘 구현하여 실제 쇼핑몰 앱에 근접한 완성도를 보여줍니다.
-
라우터 구조는 기본적인 경로 등록과 히스토리 API 활용을 잘 수행하지만, 경로 파라미터 처리 로직이 다소 제한적이며 파라미터를 핸들러에 전달하지 않는 점이 아쉽습니다. 이 부분은 동적 라우팅 확장 시 중요한 개선 포인트입니다.
-
장바구니 모달 및 상태 관리는 로컬 스토리지 기반 데이터 유지와 UI 갱신이 일관되게 이뤄지고 있으며, 이벤트 중복 등록 방지 같은 세심한 처리가 인상적입니다. 단, 선택된 항목의 가격 합산을 UI 텍스트 파싱 대신 데이터 기반 계산으로 바꾸면 유지보수가 더욱 용이해집니다.
-
상품 목록 페이지는 검색, 카테고리 필터, 정렬, 무한 스크롤, URL 쿼리 관리까지 대부분 요구사항을 충실히 반영했습니다. 다만 카테고리 UI 렌더링과 무한 스크롤 임계값 등의 하드코딩 부분은 상수화 및 함수 분리로 개선할 여지가 있습니다.
-
상품 상세 페이지는 카테고리 및 브레드크럼 네비게이션, 관련 상품 표시, 수량 선택 및 장바구니 추가 기능이 잘 구현되어 있습니다.
-
토스트 알림은 타입별 스타일 분리 및 닫기 기능, 자동 제거가 효과적으로 동작합니다.
전반적으로 각 기능의 구현과 이벤트 처리가 명확하며, 요구사항 충족도가 높아 좋은 완성도입니다.
추가 요구사항 예시: 라우터 확장 및 코드 분리 관점
문제 상황
향후 상품 상세 페이지 외에 예약, 사용자 프로필 등 동적 경로가 추가될 때 현재 라우터 구조는 파라미터를 핸들러에 전달하지 않아 확장에 어려움이 생깁니다. 또한 로직이 일부 이벤트 핸들러에서 과하게 몰려 있어 유지보수성이 떨어질 수 있습니다.
현재 한계
- 라우터가 경로 파라미터를 인자로 핸들러에 넘기지 않음
- 이벤트 핸들러 내부에 복잡한 데이터 계산 로직 포함
- UI 문자열에 의존하는 가격 계산
근본 원인
라우터 설계가 정규식 패턴 매칭, 핸들러 호출 단순 구조로 한정되어 있어 파라미터 전달 기능 미구현
데이터 상태와 UI 업데이트가 섞여 있어 관심사가 분리되지 않음
개선 구조
현재 구조:
Router
|- routes: Map<path, handler>
|- navigate(path): handler()
개선 구조:
Router
|- routes: Map<pathPattern, handler(params)>
|- navigate(path): handler(params)
개선 사항:
- 경로 등록 시 파라미터 키 추출 및 경로 매칭 후 파라미터 객체 생성 제공
- 각 컴포넌트/페이지 모듈로 이벤트 핸들러 및 상태 관리 분리
- 상태 기반 가격 계산 및 UI 업데이트 분리
코드 비교 예시
// ❌ 기존
handler();
// ✅ 개선
handler(params);이 외에도 코드 가독성 향상을 위한 함수 분리, 상수화, 이벤트 위임 패턴 일관 적용 등을 고민해보시면 좋겠습니다.URL 쿼리 파라미터에 모든 상태를 관리하는 것은 명확한 상태 추적과 공유, 새로고침 후 복원 용이 등 장점이 많지만, 동시에 복잡성 부담과 유지보수 난이도 증가가 단점으로 나타납니다.
-
쿼리 파라미터에 적합한 상태
- 검색어, 필터 조건, 정렬, 카테고리 선택 등 "페이지 전환 시 유지되어야 하는" 상태
- URL 공유나 즐겨찾기, SEO에 중요한 상태
-
메모리 상태로 관리가 적합한 경우
- 모달 열림/닫힘, 임시 입력 데이터, UI 포커스 상태, 빠른 일시 변화 상태
- URL이 복잡해지거나 노출이 적절하지 않은 정보
-
복합적인 접근 제안
- 핵심 필터나 검색 조건 등은 URL 쿼리로, 모달 오픈 여부나 내부 UI 상태(예: 선택 여부)는 메모리 상태(예: 컴포넌트 상태, 전역 상태)로 관리
- SPA 라우터와 상태관리를 연결하는 전략 수립
-
상태 관리 전략 팁
- URL 처리는
pushState와popstate이벤트 리스닝을 통해 정확히 반응 구현 - 복잡한 상태는 상태 관리 라이브러리(ex. Redux, Zustand, Vuex 등) 사용 고민
- 모달 같이 일시적 UI 상태는 URL에 포함하지 않고 메모리로만 관리하는 편이 직관적임
- URL 처리는
-
실무 경험 조언
- URL에 너무 많은 상태를 넣으면 URL 길이 제한, 인코딩 이슈, 관리 어려움 발생
- 사용자 경험도 중요하여, 신속한 UI 반응은 메모리 상태 활용이 빠르고 쾌적함
- 주요 상태만 URL에 저장하고, 부수적 상태는 내부 관리하는 혼합 방법 추천
요약하자면, "핵심 필터 조건은 URL, 모달과 같은 UI 상태는 메모리로 관리" 하는 것이 좋은 균형입니다. 프로젝트 복잡성과 팀 역량에 맞춰 전략을 조율하시면 됩니다.
|
|
||
| navigate(path) { | ||
| if (this.currentPath !== path) { | ||
| this.currentPath = path; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 Router 클래스는 경로에 파라미터가 포함된 경우(예: /product/:id)를 단순히 정규식으로 패턴 매칭하는 방식을 사용하고 있습니다. 그러나 이후 특정 파라미터를 추출하거나 라우팅 핸들러에 파라미터를 전달하는 구조가 없습니다.
개선 제안
- 라우트 등록 시 핸들러에 파라미터를 인자로 전달하는 구조로 확장할 수 있습니다. 예를 들어
/product/:id에서id를 추출해 핸들러가 해당 값을 받을 수 있어 동적으로 활용 가능하게 합니다. - 이는 상품 상세 페이지 등 동적 라우팅 처리에 필수적이며, 추후 라우트 확장 시 유연성을 크게 향상시킵니다.
// ❌ 현재 방식
if (regex.test(path)) {
handler = routeHandler;
break;
}
// ✅ 개선 방식 예시
if (regex.test(path)) {
const paramValues = [...path.match(regex)].slice(1);
const paramKeys = (routePath.match(/:([^/]+)/g) || []).map(k => k.substring(1));
const params = paramKeys.reduce((acc, key, idx) => {
acc[key] = paramValues[idx];
return acc;
}, {});
handler = () => routeHandler(params);
break;
}이렇게 하면 라우터가 더 확장 가능해지고 핸들러가 패스 파라미터를 활용할 수 있습니다.
| <div class="sticky bottom-0 bg-white border-t border-gray-200 p-4"> | ||
| <!-- 선택된 아이템 정보 (선택된 항목이 있을 때만 표시) --> | ||
| <div id="selected-items-info" class="flex justify-between items-center mb-3 text-sm" style="display: none;"> | ||
| <span class="text-gray-600">선택한 상품 (<span id="selected-count">0</span>개)</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 모달 내 이벤트 등록이 document 레벨에서 한 번만 진행되도록 설계되어 있어 좋은 추상화임을 확인했습니다. 다만 모달이 렌더링될 때마다 이벤트 중복 등록을 방지하는 플래그 cartModalEventsSetup를 사용하고 있는데, 이 플래그는 현재 모듈 스코프 전역 변수로 관리됩니다.
개선 제안
- 이벤트 중복 등록 방지를 위해 플래그를 활용하는 것은 맞지만, 모달이 닫힐 때 이벤트를 제거하는 패턴이라면 더 안전합니다.
- 또는 이벤트 위임이 아니라 모달 엘리먼트 내부에 한정하여 이벤트 위임을 할 수도 있습니다.
- 이벤트 핸들러 안에서
e.preventDefault()를 너무 광범위하게 호출하는 경우가 있으니, 필요한 경우에만 호출하도록 주의 바랍니다.
|
|
||
| updateCartTotalPrice(); | ||
| updateSelectedInfo(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 내 수량 조절 버튼(quantity-decrease-btn / quantity-increase-btn) 클릭 시 updateCartQuantity를 호출하고, 이후 해당 아이템 UI를 업데이트하는 로직이 잘 구현되어 있습니다.
다만 수량 입력창이 readonly 상태이고, 키보드로 직접 편집할 수 없습니다. 이는 의도된 UX일 수 있으나 만약 직접 입력 허용까지 고려한다면, input 이벤트 처리도 추가해야 합니다.
그리고 수량 제한 조건(최소/최대 수량) 처리가 UI 및 비즈니스 로직에 통일성 있게 적용되어야합니다.
| const textNode = document.createTextNode(` 전체선택 (${totalCount}개)`); | ||
| selectAllCheckbox.parentNode.insertBefore(textNode, selectAllCheckbox.nextSibling); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
선택된 아이템 정보(updateSelectedInfo 함수) UI 업데이트 시, 총 가격 계산을 위해 각 상품의 가격 텍스트를 파싱하는 방식을 사용하고 있습니다.
개선 제안
- UI에 있는 텍스트를 파싱하기보다, 장바구니 상태 데이터를 직접 참조하여 계산하는 것이 더 정확하고 유지보수가 쉽습니다.
- 가격 정보가 UI와 비즈니스 로직(데이터 상태)에서 중복 관리되면, UI 변경이나 구조 변경 시 버그가 발생할 수 있습니다.
// 예: getCart()를 활용해 직접 계산
let selectedPrice = 0;
selectedCheckboxes.forEach((checkbox) => {
const productId = checkbox.getAttribute("data-product-id");
const cartItem = cart[productId];
if (cartItem) {
const price = parseInt(cartItem.productData?.lprice || 0);
selectedPrice += price * cartItem.quantity;
}
});이렇게 하면 UI 텍스트에 의존하지 않습니다.
| function handleScroll() { | ||
| const scrollHeight = document.documentElement.scrollHeight; | ||
| const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; | ||
| const clientHeight = document.documentElement.clientHeight; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상품 목록에서 카테고리 필터 UI 생성 시, 1depth 카테고리가 렌더링되고 2depth 카테고리가 이후 조건에 따라 렌더링/삭제됩니다.
개선 제안
- 카테고리 UI 생성 로직이 훨씬 더 명확하게 모듈화 될 수 있습니다.
- 1depth, 2depth 카테고리 버튼 생성 및 상태 관리 로직을 분리된 함수로 만들고, DOM 조작도 버튼 별로 쪼개면 유지보수 및 확장성에 유리합니다.
예: 카테고리 데이터를 트리 구조로 관리하고, 재사용 가능한 렌더 함수로 분리하는 방식 권장.
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||
| d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"></path> | ||
| </svg> | ||
| <span |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상품 무한스크롤 페이징 처리가 동작하도록 handleScroll과 loadNextPage 함수가 잘 구현된 점 확인했습니다.
개선 사항
- 현재 스크롤 위치를 계산하는 로직이
100px임계값에 하드코딩되어 있는데, 이 값을 상수나 설정값으로 분리하면 유지보수와 튜닝에 유리합니다. - 무한 스크롤 상태 정보(
infiniteScrollState)가 전역 상태 변수로 관리되고 있는데, 나중에 다중 인스턴스가 생기는 경우를 고려한다면 별도 클래스 또는 훅 같은 패턴으로 캡슐화하면 좋습니다.
| </div> | ||
| </div> | ||
| <!-- 필터 옵션 --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
코드 내 이벤트 핸들러 등록 시 내부에서 변수 cartButtonHandlerSetup, cartIconClickHandlerSetup 등을 플래그로 중복 등록 방지를 하는 패턴이 반복되고 있습니다.
개선 제안
- 이런 코드가 여러 곳 반복되면 헷갈릴 수 있으므로, 공통 유틸 함수(
once혹은eventSetupManager)를 만들어 등록 여부를 관리하면 코드가 간결해집니다.
예:
function setupEventOnce(eventName, selector, handler) {
let isSetup = false;
return () => {
if (!isSetup) {
document.addEventListener(eventName, (e) => {
if (e.target.closest(selector)) {
handler(e);
}
});
isSetup = true;
}
};
}이런 방식으로 개선 가능.
| ${상품목록_레이아웃_로딩} | ||
| function renderShowcase() { | ||
| const showcaseMarkup = ` | ||
| ${productListLoadingLayout} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메인 진입 함수(main)에서 SPA 라우터를 초기화하고 경로마다 렌더 함수를 등록하는 좋은 구조입니다.
하지만 라우터 글로벌 참조를 window.__router__에 직접 할당하는 방식은 조금 아쉽습니다.
개선 제안
- 전역 스코프에 직접 할당하는 대신, 앱 컨텍스트 또는 전용 상태 관리 객체를 사용해 관리하는 편이 좋습니다.
- 최소한 Symbol 같은 네임스페이스를 활용해 충돌 위험을 낮출 수 있습니다.
- 또한, 라우터 이벤트와 렌더 함수 연결을 좀 더 구조적으로 관리한다면 유지보수에 이롭습니다.
| export function removeFromCart(productId) { | ||
| const cart = getCart(); | ||
| delete cart[productId]; | ||
| saveCart(cart); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 서비스의 상태를 localStorage에 저장하고 불러오는 구현은 실용적이고 좋습니다.
다만, cart 상태의 변경 시점마다 updateCartIcon을 호출하도록 되어 있는데, 이로 인해 중복 호출 가능성이 있을 수 있습니다.
개선 제안
addToCart,removeFromCart,updateCartQuantity함수들은 내부적으로 상태 변경과 UI 업데이트를 담당하므로, 호출자가 한꺼번에 처리하도록 역할을 명확히 나눠 고려해볼 수 있습니다.- 앞으로 상태가 복잡해질 경우 이벤트 발행/구독 패턴 또는 상태 관리 라이브러리 도입을 고민해보세요.
| <div class="bg-blue-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center space-x-2 max-w-sm"> | ||
| <div class="flex-shrink-0"> | ||
| <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토스트 메시지 컴포넌트가 타입별로 적절하게 스타일과 SVG 아이콘을 분리하여 렌더링하는 점이 좋습니다.
다만, DOM 엘리먼트를 동적으로 생성하고 조작하는 부분이 코드 내 여러 영역에 흩어져 있는데, 컴포넌트 패턴 또는 팩토리 함수로 분리하면 좋겠습니다.
개선 제안
- 여러 토스트 메시지 유형을 별도의 함수 또는 클래스 메서드로 분리해 코드 가독성을 높이세요.
- 또한, 토스트 메시지 여러 개가 겹치지 않도록 큐잉을 고려하면 UX 개선에 도움이 됩니다.
과제 체크포인트
배포 링크
https://taejun0.github.io/front_7th_chapter2-1/
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
기술적 성장
SPA 라우터를 직접 구현하엿습니다. History API의 pushState와 popState 이벤트를 활용하였으며, 동적 파라미터를 정규표현식으로 매칭하였습니다.
무한 스크롤을 Intersection Observer API를 활용하여 구현하였습니다.
자랑하고 싶은 코드
장바구니 관련 로직을 하나의 모듈로서 분리하였고, 로컬스토리지 접근, 아이콘 업데이트, 데이터 검증등을 캡슐화 했습니다.
개선이 필요하다고 생각하는 코드
심화를 한번 도전해보는 것이 좋지 않았나 싶습니다.
학습 효과 분석
추기 학습이 필요한 영역
과제 피드백
React를 순수 JavaScript로 구현하는 요구사항이 참 특별했던 것 같습니다.
근본적인 개발 환경 구축을 통해 React보다는 SPA 자체에 대한 이해도가 조금 높아진 것 같습니다.
리뷰 받고 싶은 내용
URL 쿼리 파라미터로 모든 상태를 관리하는 것이 약간은 부담스럽게 느껴졌습니다. 복잡한 필터 조건이나 모달과 같은 상태도 URL에 포함시키는 것이 좋은 구조였는지, 아니면 일부는 메모리 상태로만 관리하는 것이 나은지 궁금합니다!