https://programmers.co.kr/skill_check_assignments/199

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

오랜만에 바닐라 JS 개발을 해보고 싶었는데, 마침 SPA 개발 과제가 있길래 해보았다.

그 과정에서 긴 시간 고전한 주제에 대해 적고자 한다.

 

결론적으로, 난 완벽한 구현에 성공하지 못했다.

 

첫 번째로 SPA 라우팅 처리를 vanilla js 로 해본 게 처음이라 고전했고 (쉬운 건데 삽질했다)

두 번째로 js 에서 HTML element 를 만들 때 createElement 를 하나하나 일일히 했다. (미쳤나봄)

해답을 보니 innerHTML = `<div></div>` 식으로 편하게 했더라.

 

React 를 쓰는 실무에서 워낙 할 일 없던 거라 생각을 못했다.

 

 

SPA 라우팅 처리

 

SPA는 별도의 서버 사이드 렌더링 없이, 경로 별로 innerHTML 을 변경해주면 된다.

즉, 라우팅 처리가 굉장히 중요한 부분이라고 생각했고, history.pushState 와 popState 가 해결책이 되었다.

 

그 과정에서 한시간 가량 삽질한 부분에 대해 말해보고자 한다.

 

index.html

<html>
  <head>
    <title>커피캣 스토어</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <main class="App">
    </main>
    <script src="src/index.js" type="module"></script>
  </body>
</html>

src 폴더의 index.js 를 body 태그 밑에 불러온다.

 

src/index.js

import App from "./App.js"

const start = () => {
    return App();
}

start();

create-react-app 의 구조가 익숙해서 이런 식으로 했는데, 의미가 없었던 거 같다.

 

src/App.js

import ProductDetail from "./pages/ProductDetail.js";
import ProductList from "./pages/ProductList.js";
import { renderAppContent } from "./utils.js";

const App = async () => {
    const app = document.getElementsByClassName("App")[0];

	// 맨 첫 페이지는 list 이므로, App 에 ProductList를 렌더한다.
	renderAppContent(await ProductList()); // ProductList 함수를 실행시킨 결과값(HTML element)를 app 에 렌더한다
    history.pushState({page : "list"}, '', `.`);  // 렌더하면서, list 로 갔다는 표시로 pushState 를 해준다.

	
    const onPageChange = async () => {
        switch(history.state.page) {
            case "list":
                renderAppContent(await ProductList());
                break;
            case "detail":
                renderAppContent(await ProductDetail({id : history.state.id}));
                break;
            default:
                return;
        }
    }

    window.addEventListener('popstate', onPageChange)
    return app;
}

export default App;

 

pushState 는

history.pushState( data, title, path ) 의 형태로 이루어진다.

 

이 때 실제로 앱 전체를 다시 불러오는 형태가 아닌, url 의 path 만 변경시키면서 data 를 history 스택에 push 하는 방식으로 진행된다.

pushState 를 하게 되면 뒤로 가기 버튼이 활성화가 된다. (실제로 하나 이동했다는 말. history.go(1) 과 같은 느낌이다 )

 

내가 삽질한 부분은, 첫 화면에 들어왔을 때, history.pushState 를 해주지 않고, 리스트의 항목을 클릭했을 때만 pushState 를 해준 것이다.

 

  1. 첫 화면(리스트)이 렌더 됐음에도 pushState 를 해주지 않았기 때문에 null 이 저장됨
  2. 리스트의 항목 중 하나를 클릭해서, pushState 를 통해 detail 페이지로 이동함 ( 잘 됐음 )
  3. 뒤로 가기 하면 popstate 이벤트가 발생하는데, 이 때 state 가 null 이었음.
  4. 근데 앞으로 가기를 다시 누르니까 디테일 페이지로 이동하면서, 디테일 페이지에 전해주려 했던 state 들이 잘 찍혔음 (이것 때문에 헷갈렸음)

나는 바보같이 pushState 를 처음에 해줘야 하는 지 몰랐다. 그래서 계속 null 이 나타나서 한시간 가량 날렸다. ㅋㅋ

 

저 한줄을 추가하자 거짓말 처럼 잘 됐다.

 

두 번째 실수 : innerHTML = <div> 를 쓰지 않음

나의 ProductDetail 페이지 코드를 보면 ( 다 안 읽는 걸 추천한다 )

import { makeElement } from "../utils.js"

const ProductDetail = async ({id, name, imageUrl, price}) => {
    const productDetailPage = makeElement("div", "ProductDetailPage");

    const productDetailPageTitle = makeElement("h1");
    productDetailPageTitle.innerText = `${name} 상품 정보`;
    productDetailPage.appendChild(productDetailPageTitle);

    const productDetail = makeElement("div", "ProductDetail");

    const produdctImage = makeElement("img", "", {
        src : imageUrl
    })

    productDetail.appendChild(produdctImage);

    const productDetailInfo = makeElement("div", "ProductDetail__info");

    const productDetailInfoTitle = makeElement("h2");
    productDetailInfoTitle.innerText = `${name}`;
    productDetailInfo.appendChild(productDetailInfoTitle)

    const productDetailPrice = makeElement("div", "ProductDetail__price");
    productDetailPrice.innerText = `${price}원~`;
    productDetailInfo.appendChild(productDetailPrice)

    const productSelectElement = makeElement("select");
    const defaultOption = makeElement("option");
    defaultOption.innerText = `선택하세요.`;
    productSelectElement.appendChild(defaultOption);

    productDetailInfo.appendChild(productSelectElement)
    
    const selectedOptions = makeElement("div", "ProductDetail__selectedOptions");
    const selectedOptionsTitle = makeElement("h3");
    selectedOptionsTitle.innerText = `선택된 상품`;

    const selectedOptionsUl = makeElement("ul");
    selectedOptions.appendChild(selectedOptionsUl);

    productDetailInfo.appendChild(selectedOptions);

    productSelectElement.onchange = () => {
        const selected = productSelectElement.options[productSelectElement.selectedIndex].value
        console.log(selected)
        productSelectElement.value = productSelectElement.options[0].value;

        const selectedOption = makeElement("li");
    }

    try {
        const {productOptions} = await (await fetch(`https://uikt6pohhh.execute-api.ap-northeast-2.amazonaws.com/dev/products/${id}`)).json();
        productOptions.forEach((productOption) => {
            const option = makeElement("option");
            option.innerText = `${name} ${productOption.name}`;
            if(productOption.price >= 0) {
                option.innerText += `(+${productOption.price}원)`;
            }
            if(productOption.stock === 0) {
                option.innerText = `(품절) ${option.innerText}`;
            }
            productSelectElement.appendChild(option);
        })
        
    } catch(e) {

    }
    
    

    productDetail.appendChild(productDetailInfo);
    productDetailPage.appendChild(productDetail)

    return productDetailPage;
}

export default ProductDetail;

너무 힘들었다.

왜 productDetailPage.innerHTML = `blahblah` 를 생각하지 못했을까?

해답을 보고 힘이 다 빠져버렸다..

 

데브 매칭은 바닐라 JS 로 항상 이루어졌고, 앞으로도 그럴 것 같다. (프레임워크 별로 차이가 있는데, 리액트만 받을 순 없자나.. )

배울 게 아직도 많다는 걸 여실히 깨달은 하루였다.

 

이 과제에서의 남은 점은, UI 조작의 자잘한 부분 ( 가격에 콤마찍기, 선택 물품 가격 계산하기) 와 주문하기 ( 로컬 스토리지 사용하기 ) 인 것 같은데, 이것들은 쉬우니까 더 안봐도 될 것 같다.

 

SPA 구현을 해보는 데 굉장히 좋은 기회가 되었다.

 

 

 

고양이 사진첩 만들기 (상반기 문제) 도 풀어 봐야지.

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기