프로그래머스 상반기 과제 중 고양이 사진첩 과제에 도전해봤다.
프론트엔드 개발자라면 필요한 능력인 구조 디자인 하기와 백엔드와 소통하여 api 데이터 활용하기의 능력이 필요했다.
지금 프론트엔드 개발자로 일하고 있기 때문에 위의 작업을 많이 해봤다.
vue를 쓰고 있는 현직장에서 디자인 및 api 데이터 가공 및 활용에 대해서 많이 해봐서 자신만만하게 그래도 문제는 풀수 있겠지 라며 자신감 뿜뿜이었지만
시험에서는 라이브러리를 사용해서는 안되었다.
바닐라 JS 만 오직 사용 가능 !!
vue와 react를 사용하지 못하다니 ...
문제를 풀면서 라이브러리의 존재감에 대해 확실히 인지 할수 있었다..
그동안 쉽게 개발을 하고 있었구나..
이전의 바닐라로 코딩하던 사람들은 엄청 대단하다..
또, 라이브러리가 확실히 코드량을 줄여주긴 하는 구나 라는 생각이 들었다.
몇번 문제를 풀어보다 바닐라에 막막해서 문제 해설을 보면서 문제를 쭉 훑어 보게 되었다.
복붙이 안되게 금지 되어 있기도 하고 스크롤 압박에 집중이 전혀 되지 않아 직접 코드를 작성해가면서 세세히 훑어 보게 되었다.
여기 까지 긴 서두를 읽어주셔서 감사하다.
자 이제부터 코드를 훑어보며 느꼈던 , 새로 배웠던 것을 작성해보도록 하겠다.
데이터는 많이 보았듯이 배열안에 객체들이 담겨져 있다.
고양이 사진첩은 우리가 자주 접하는 컴퓨터의 폴더구조처럼 폴더를 들어가서 이미지를 클릭하면 고양이 사진이 모달 형식으로 뜨는 것이다.
하위폴더로 이동하기, 뒤로가기 기능은 덤이다.
일단 데이터는 두종류로 나뉜다. File , directory
directory는 폴더명
File 은 사진이름이다.
고유의 id값을 가지고 있고 이름을 가지고 있다
사진의 경우 filePath라는 사진 경로가 들어있다.
부모의 id값도 받는다
해설에서는 컴포넌트 형식으로 라이브러리를 사용한것처럼 코드를 짰다.
라이브러리의 조건부 렌더링을 사용하기 위해서 인것 같다.
구조는 함수형으로 되어있다.
나는 react로 코드를 배웠지만 업무를 할때는 vue로 작업을해서 대부분 react의 기억이 가물가물 했었는데
코드를 옮겨 적으면서 react의 함수형 구조가 생각나게 되었다.
state값도 활용하니 딱 react의 함수형 이었다.
우선 node 컴포넌트가 등장한다.
function Nodes({$app,initialState}){
this.state = initialState
this.$target = document.createElement('ul');
$app.appendChild(this.$target)
this.setState = (nextState)=>{
this.state = nextState
this.render()
}
state 상태값을 표현해주고 target이라고 하여서 우선 처음에 그려질 태그를 만들어준다 vue로 하자면 templete 안의 기본 HTML 태그이다.
그다음에 setState함수를 정의해줌으로 state값을 바꿔줌과 동시에 render 함수로 렌더링을 다시 해준다
//nodes.js
this.render = () => {
this.$target.innerHTML = this.state.node.map(node=>{
`<li>${node.name}</li>`
}
렌더 함수의 역할은 state값이 변화되면 map으로 변화된 값들을 받아서 반복하여서 태그를 생성해주고 그려준다. 렌더 함수를 실행하면 li태그를 이름값을 받아서 돌려준다.
function Breadcrumb({$app, initialState, onClick}){
this.state = initialState
this.$target = document.createElement('nav')
this.$target.className = 'Breadcrumb'
$app.appendChild(this.$target)
this.setState = nextState =>{
this.state = nextState
this.render()
}
다음은 breadcrumb 컴포넌트 부분이다. 이름이 빵부스러기인데 왜 이름을 이렇게 지었는지 모르겠다. 나는 무슨뜻인지 한번에 안들어 온다.
태그를 보면 초기 값과 nav 태그를 그려주는데 nav바 부분인것 같다.
appendChild, createElement 등 dom을 직접 조작하는 부분이 보인다. 라이브러리에서는 직접 dom을 조작하는 것을 비추해서 많이 사용하지 않은 태그들인데 이전 코딩 공부를 하면서 배웠던 기억이 새록새록 난다.
this.render = () =>{
this.$target.innerHTML = `<div class='nav-item'>root</div>${
this.state.map((node,index)=>`<div class='nave-item' data-index="${index}">${node.name}</div>).join()`)
}`
}
렌더링 부분 조차 state값을 받아서 map함수로 돌려주는 부분이다.
data-index 이부분을 처음알았다.
컴포넌트간 의존도 줄이기, 의존도를 줄이려면 vuex,redux처럼 전역 변수를 관리하는 그것만 생각했는데 여기서는 부모 컴포넌트인 app.js를 만들고 node.js, breadcrumb.js를 컴포넌트화 시켜서 사용한다는 말이었다.
부모 컴포넌트의 함수를 전달하기 위해 node.js는 onclick 함수를 전달 받는다
function Nodes({$app,initialState, onClick}){
this.onClick = onClick
this.render = () => {
if(this.state.nodes){
const nodesTemplate = this.state.nodes.map(node => {
const iconPath = node.type ==='FILE' ? './assets/files.png':''
return `
<div class="Node" data-node-id="${node.id}">
<img src="${iconPath}" />
<div>${node.name}</div>
<div>
`
}).join('')
}
this.$target.innerHTML = !this.state.isRoot? `<div class='Node><img src="/assets/prev.png"></div>${nodesTemplate}`:nodesTemplate
자 여기서 onclick 에 this.onClick을 써줌으로 구별을 해주는것 같다.
vue에서도 this. 이런 형식을 많이 써서 익숙하다.
본격적으로 state값을 가지고 렌더함수를 수정해준다.
node의 역할은 폴더 이자 file이 있을때 사진 이미지를 보이게 하고 아닐때는 폴더 처럼 구분하게 하는 것 같다.
그리고 처음 폴더가 아닐때 뒤로가기 버튼을 렌더링 해준다.
this.$target.querySelectorAll('.Node').forEach($node =>{
$node.addEventListener('click',(e)=>{
const {nodeId} = e.target.dataset
const selectedNode = this.state.nodes.find(node=> node.id === nodeId)
if(selectedNode){
this.onClick(selectedNode)
}
})
})
대망의 onClick함수이다.
e.target.dataset 을 처음 알았다. 이렇게 태그 내의 data - 속성 값들을 가져올수가 있다. 태그 내의 nodeId 값을 구조분해 할당으로 가져오고 사용한다. 각 노드들에 클릭 이벤트를 달아주는 것이다.
노드 id가 같을때 클릭 이벤트를 걸어준다.
이제 부모 컴포넌트인 app js를 만들어 준다.
function App($app){
this.state = {
isRoot:false,
nodes:[],
depth:[],
}
const breadcrumb = new Breadcrumb({
$app, initialState : this.state.depth,}
const nodes = new Nodes({
$app, initialState:{
isRoot : this.state.isRoot,
nodes: this.state.nodes
},
onClick: async(node)=>{
if(node.type === 'DIRECTORY'){
}else if(node.type === 'FILE'){
}
그리고 breadcrumb와 nodes를 new를 통해서 만들어 준다.
그리고 props값들을 전달해준다.
nodes는 onClick함수까지 전달해준다.
타입이 폴더일경우 파일일 경우를 나뉘어서 만들어 준다.
// index.js
import App from './App.js'
new App(document.querySelector('.app'))
index js 에서는 app을 연결시켜준다.
import하고 script 태그를 head에 넣어주는데 이 과정은 추후에 나온다.
대망의 api를 호출하는 코드이다.
한곳에서 관리해야 추후에 전체를 다 수정하는 것보다 한곳만 수정하면 되니 js로 관리해준다.
const API_END_POINT = '...'
const request = async (nodeId)=>{
try{
const res = await fetch(`${API_END_POINT}/${nodeId ? nodeId : ''}`)
if(!res.ok){
throw new Error('서버에러 ')
}
return await res.json()
} catch(e){
throw new Error(e.massage)
}
}
fetch함수를 쓰라고 해서 fetch를 쓴다 내장함수 이지만 axios를 많이 쓰긴한다. async await 비동기적 함수를 사용하기 위해서 많이 쓰는 새로운 문법이다. 엄청 자주쓴다. 위의 코드는 해설 과정을 1/4 과정을 생략하여 async await 까지 한번에 적용했다.
fetch는 HTTP 에러를 reject하지 않는다고 한다. try catch를 쓰는데 HTTP 에러를 catch해 내지 못한다.
그러므로 fetch 는 res.ok단계를 체크해줘야 한다.
if(!res.ok){
throw new Error('서버에러 ')
}
요론 식으로 처리 해주자.
app에서 이제 초기 값을 불러오자. react에서 말하는 훅 부분이다.
mounted 되었을때 api 호출 기능이다.
//app js
const init = async()=>{
try{ const rootNodes = await request()
this.setState({
...this.state,
isRoot:true,
nodes:rootNodes
})
request 함수를 호출해서 state값을 변경해준다.
초기 값을 저장해준다. import와 export를 안했는데 어떻게 request함수를 쓸수 있는지 의문이다. 아마 문제 해설자도 문제를 다풀고 다시 쪼개서 해설하느냐고 까먹은것 같다.
나도 완전체 코드를 해설의 코드와 맞추기 위해서 지우고 작성하는게 꽤 귀찮은 작업인데 하고있다.. ㅠㅠ
자 app에서 초기 api를 호출해주고 클릭하였을때 이제 호출해주도록 변경해준다.
node 의 클릭이벤트때 호출하도록 수정해준다.
onClick: async(node)=>{
try{
const nextNodes = await request(node.id)
this.setState({
...this.state,
depth:[...this.state.depth,node],
nodes:nextNodes
자 여기까지 api를 호출해서 데이터를 변경해주고 렌더링 해주는 단계까지왔다.
해설의 반 정도를 다시 리뷰해보았다.
스크롤이 상당히 길어지니 2부로 나머지 작업을 리뷰해보도록 하겠다.