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

App
Nodes
ImageView
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번 렌더링 됨
해결
: 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(); }