HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
📝
학습 TIL
/
📝
28일차 배운 것 정리 (1)
📝

28일차 배운 것 정리 (1)

대주제
실습
작성완료
작성완료
전날 정리 노트 이동
다음 정리 노트 이동
주제
고양이사진첩
vanillaJS
날짜
Apr 27, 2022

목차

목차1. 고양이 사진첩 강의 (후반부)1.요구사항 분석2. 더미데이터 렌더링3. API 연동하기4. ImageViewer처리5. directory(path)이동 및 loading처리6. BreadCrumb 처리2. 고양이 사진첩 과제0. 과제 시작 전, 각 컴포넌트가 자신의 setState를 독립적으로 가지도록 리팩토링각 컴포넌트 별 State 정리1. State의 Validation 처리2. setState 최적화하여, 변경된 부분만 rendering 처리3. 백 스페이스 키를 통해 이전 경로 이동하도록 설정

1. 고양이 사진첩 강의 (후반부)

1.요구사항 분석

  • 요구사항 리스트업
    • 고양이 사진 API 를 통해 사진과 폴더를 렌더링
      폴더를 클릭하면 내부 폴더의 사진과 폴더를 렌더링
      • 현재 경로도 렌더링
      루트경로가 아닌 경우, 파일 목록 맨 앞에 뒤로가기 추가
      사진을 클릭하면 고양이 사진을 모달창으로 보여줌
      • esc를 누르고너, 사진 밖을 클릭하면 모달을 닫음
      API를 불러오는 중인 경우 로딩 중 처리
  • 컴포넌트 구조
    • App이 전체 컴포넌트를 조율하는 형태
      • notion image
        App
        • state: {isRoot, isLoading, nodes, paths}
        Nodes
        • $target, onClick, onPrevClick
        • state: {isRoot: false, nodes: [] }
        ImageView
        • $target,
        • state: {imageUrl}

2. 더미데이터 렌더링

  • Nodes의 props
    • $target, onClick, onPrevClick
    • state: {isRoot: false, nodes: [] }
  • 내 코드와 비교했을 때 개선 점
    • isRoot 여부 컴포넌트 외부에서 던져주기
    • template부분 공통점 최대한 살려서, 템플릿 분기 처리 해주기

3. API 연동하기

  • request.js 모듈 만들고 더미데이터 부분 API호출로 받은 data로 교체해주기
  • 로딩처리
    • Loading 컴포넌트 따로 만들어서 재사용
 
  • 내 코드와 비교했을 때 개선 점
    • App에서 dataFetch 받아 내려주기
    • 모든 Node에 data-id 주지말고, 최소한으로 주기 (prev Node제외)

4. ImageViewer처리

  • imageViewer내부에 esc 입력과 이미지 외부 모달 부분 클릭 시, onClose함수 실행 이벤트 바인딩
  • onClose 시 selectedImageUrl 을 null 처리하여 모달 닫도록 설정
  • 내 코드와 비교했을 때 개선점
    • 나는 모달을 열고 닫는 처리를 NodeList에서 onClickFile이 발생했을 때, ImageViewer를 class를 통해 열어주고, 이벤트처리도 App에서 해주었었다.
    • 하지만 이 처리를 ImageViewer 안에서 처리해줌으로서, 컴포넌트의 역할을 좀 더 분명히 할 수 있게되었다. 해당 기능은 ImageViewer의 책임이기 때문이다!!

5. directory(path)이동 및 loading처리

  • App의 paths 상태를 정의하여, 현재 방문 경로를 저장한다.
    • paths.length === 0 일 때는 root 이므로 fetchNodes() 를 호출
    • 그 외에는 경로가 존재하므로 fetchNodes(id)로, 해당 경로의 파일을 호출
  • Loading 컴포넌트를 만들고, App의 isLoading 상태에 따라 컴포넌트를 호출/none 처리
    • imageViewer와 같은 로직

6. BreadCrumb 처리

  • App의 paths 상태를 내려 받아, 경로를 렌더링 해주는 컴포넌트
  • 특정 경로를 눌렀을 때, 현재 paths 중에서 해당 경로까지의 path만 잘라서 setState() 처리
  • 내 코드와 비교했을 때 개선점
    • 나는 fetch, update로직을 NodeList에 두었기 때문에, fetch 작업이 다른 기능들과 결합되어 사용될 수 없었다.
    • fetchNodes() 함수처럼 컴포넌트 흐름에 맞게, API요청 후 setState해주는 함수를 잘 만들어 두면, 이후 기능에 이를 활용하여 기능 구현을 할 수 있다는 점
      • fetchNodes() 는 의존성 없이 독립적인 기능을 할 수 있다.

2. 고양이 사진첩 과제

0. 과제 시작 전, 각 컴포넌트가 자신의 setState를 독립적으로 가지도록 리팩토링

기존구조가 가지는 문제점
  • App이 render이후 4개의 컴포넌트가 mount 함수안에서 호출 되는 구조이기 때문에, setState()에서 해당 컴포넌트를 참조할 수가 없다.
  • 따라서 App이 변경된 state에 따라 컴포넌트들을 조율할 수 없다
변경후
  • App 이 render를 담당하지 않고, 각각의 컴포넌트가 직접 root Element에 추가하여 렌더하도록 변경
    • App은 state를 관리하고, data를 fetch해와서 내려주는 용도로 사용

각 컴포넌트 별 State 정리

💡
App : { isRoot:boolean, isLoading:boolean, nodeList: node[], selectedImageUrl: null || string, visitedNodeList: node[] } node: { id:number, name: string, type: string, parent: null || id, filePath: null}
Loading: { isLoading:boolean }
BreadCrumb: { visitedNodeList: node[] }
NodeList: { isRoot:boolean, nodeList: node[] }

1. State의 Validation 처리

지금 구현된 코드에서는 state에 대한 정합성 체크를 전혀 하지 않는데, 이 부분을 보충해주세요.
컴포넌트별로 올바르지 않은 state를 넣으면 오류가 발생하도록 해주세요.

2. setState 최적화하여, 변경된 부분만 rendering 처리

각 컴포넌트의 setState를 최적화하여 이전 상태와 비교해서 변경사항이 있을 때만 render 함수를 호출하도록 최적화를 해봅니다.
  • property에 따라, 해당 property를 state로 가지는 component를 update 해줌
    • 문제 : 한 개의 컴포넌트의 여러 property가 동시에 변경될 때, 각각 update를 진행하며 여러번 리렌더링 됨
      • 폴더 이동으로 NodeList의 ‘isRoot’와 ‘nodelist’ 상태가 변하며 2번 렌더링 됨
        • notion image
    • 해결 : Map을 통해, 컴포넌트별 업데이트 해야하는 property를 모아 둔 후, 한 번에 update를 진행하도록 함
      •  

3. 백 스페이스 키를 통해 이전 경로 이동하도록 설정

루트 탐색 중이 아닌 경우, 백스페이스 키를 눌렀을 때 이전 경로로 이동하도록 만들어봅니다.
백업
App
import BreadCrumb from "./src/components/BreadCrumb.js"; import ImageViewer from "./src/components/ImageViewer.js"; import NodeList from "./src/components/NodeList.js"; import Loading from "./src/components/Loading.js"; import { request } from "./src/api/request.js"; const catheStorage = new Map(); export default function App({ $target }) { this.state = { isRoot: true, isLoading: true, nodeList: [], selectedImageUrl: null, visitedNodeList: [], }; this.setState = (nextState) => { console.log("!!!!!!!! <App> - SetState 호출".nextState); this.state = { ...this.state, ...nextState }; //* 컴포넌트 별 분기 or State별 분기 //* state별 분기시, 여러개 state가지는 컴포넌트 업데이트시 setState,rendering 단계적으로 일어남 //* map을 통해 컴포넌트 별로 변화가 일어난 property를 모아 한 번에 setState 진행 const updateMap = new Map(); // {key: component, value: property[] } const setProperty = (key, property) => { if (updateMap.has(key)) { const newPropertyList = [...updateMap.get(key), property]; updateMap.set(key, newPropertyList); } else { updateMap.set(key, [property]); } }; if (nextState.hasOwnProperty("isLoading")) { setProperty(loading, "isLoading"); } if (nextState.hasOwnProperty("visitedNodeList")) { setProperty(breadcrumb, "visitedNodeList"); } if (nextState.hasOwnProperty("isRoot")) { setProperty(nodes, "isRoot"); } if (nextState.hasOwnProperty("nodeList")) { setProperty(nodes, "nodeList"); } if (nextState.hasOwnProperty("selectedImageUrl")) { setProperty(imageViewer, "selectedImageUrl"); } //* map 순회하며, 컴포넌트 별 setState 진행 console.log(updateMap); [...updateMap].forEach(([component, propertyList]) => { const newState = {}; propertyList.forEach((p) => (newState[p] = this.state[p])); component.setState(newState); }); }; this.init = () => { fetchNodes(); }; const onClickBreadCrumb = async (targetId) => { const { visitedNodeList } = this.state; if ( visitedNodeList.length === 0 || targetId === visitedNodeList[visitedNodeList.length - 1].id ) { return; } if (targetId) { const nextVisitedNodeList = [...visitedNodeList]; const targetIdx = visitedNodeList.findIndex((node) => node.id === targetId); this.setState({ visitedNodeList: nextVisitedNodeList.slice(0, targetIdx + 1) }); } else { this.setState({ visitedNodeList: [], isRoot: true }); } await fetchNodes(targetId); }; const onClickItem = async (node) => { if (node.type === "DIRECTORY") { await fetchNodes(node.id); this.setState({ visitedNodeList: [...this.state.visitedNodeList, node], }); } if (node.type === "FILE") { this.setState({ selectedImageUrl: `https://fe-dev-matching-2021-03-serverlessdeploymentbuck-t3kpj3way537.s3.ap-northeast-2.amazonaws.com/public${node.filePath}`, }); } }; const onClickPrev = async () => { const newVisitedNodeList = [...this.state.visitedNodeList]; newVisitedNodeList.pop(); this.setState({ visitedNodeList: newVisitedNodeList, }); if (newVisitedNodeList.length === 0) { await fetchNodes(); } else { const lastNode = newVisitedNodeList[newVisitedNodeList.length - 1]; await fetchNodes(lastNode.id); } }; const { isRoot, nodeList, selectedImageUrl, visitedNodeList } = this.state; const loading = new Loading({ $target, }); const breadcrumb = new BreadCrumb({ $target, initialState: { visitedNodeList, }, onClickBreadCrumb, }); const nodes = new NodeList({ $target, initialState: { isRoot, nodeList, }, onClickItem: (node) => onClickItem(node), onClickPrev, }); const imageViewer = new ImageViewer({ $target, initialState: { selectedImageUrl, }, onClose: () => { this.setState({ selectedImageUrl: null, }); }, }); const fetchNodes = async (id) => { console.log("fetchNodes, id: ", id); if (catheStorage.has(id)) { this.setState({ nodeList: catheStorage.get(id) }); return; } if (!id && catheStorage.has("root")) { this.setState({ isRoot: true, nodeList: catheStorage.get("root") }); return; } this.setState({ isLoading: true, }); const nodeList = await request(id ? `/${id}` : `/`); if (!id) { catheStorage.set("root", nodeList); } else { catheStorage.set(id, nodeList); } this.setState({ nodeList, isRoot: id ? false : true, isLoading: false, }); }; this.init(); }