[넘블] 신년메시지 주고받기 챌린지(feat. VanillaJS)

흔한 감자·2023년 1월 19일
0

회고

목록 보기
3/7

넘블에서 주관하는 챌린지를 이번에 처음 참여하였고, 생각지도 못하게 공동 3등을 수상하게 되어서 기분좋게 회고를 하게 되었다.
챌린지 기간내에 만든 결과물이 아쉬워서 설기간동안 보완하는 작업을 했는데, 그 부분을 좋게 봐주셔서 높은 등수를 받게되었다.

[넘블] 신년메시지 주고받기 챌린지 30초 요약

[넘블] 신년메시지 챌린지 공식 링크

넘블챌린지 - 신년메시지 SPA 웹 개발 챌린지

구현 결과물

회고

사실 하면서 너무 고통스러웠다. 코드스테이츠 부트캠프에서 교육과 작게나마 토이프로젝트를 중이였고, 그 탓에 조금 늦게 시작하여 개발하다보니 조금은 벅찼다.
하지만, 바닐 자바스크립트 환경에서 웹팩 직접써보고 이미지 lazy 로딩을 적용하면서 많은 것들을 배웠다.

초기 webpack 설정하기 (고통의 시작)

처음 웹팩 설정부터 쉽지 않았다. 우선 내가 webpack을 쓰기로 결정한 가장 큰 이유는 module.css를 바닐라 자바스크립트에서 적용해 봐야겠다는 욕심에 사용하게 되었다. 적용해보며 CRA에 고마움을 이번에 뼈저리게 느꼈다.

css module 설정은 아래와 같이했다

//webpack.config.js
//...생략
{
	test: /\.module\.css$/i,
	use: [
		'style-loader',
		{
			loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[local]-[hash:base64:5]',
			  },
			},
		},
	],
},

조금 설명을 덧붙이자면, test 표현식에 통과한 파일들에 대해서 use를 내용을 적용됩니다. 해당 파일들에 대해서localIdentName 지정한 패턴으로 class명이 변경된다. [local]은 기존 class 명을 의미하고, [hash:base64:5]은 랜덤으로 5자의 문자열을 붙여준다.
bem 스타일을 사용은 하였지만, 혹시나 class명이 겹치는 것을 방지하기 위해 module.css 방식을 적용하여 5자리의 랜덤 문자열을 붙여주는 작업을 진행하였다.


SPA 라우팅 구현하기

이 부분은 프로그래머스의 '2021 Dev-Matching: 웹 프론트엔드 개발자(하반기)' 기출 문제 해설을 참고해서 구현했다.
핵심은 화면 이동을 a tag의 href로 이동하는게 아니라, historyAPI에 기록하고 변경되는 컴포넌트들을 재렌더링하는 형식으로 구현한다는 것이다.

router 구현

//router.js
const ROUTER_CHAGE_EVENT = 'ROUTE_CHANGE';

export const router = (onRouteChage) => {
  window.addEventListener(ROUTER_CHAGE_EVENT, () => {
    onRouteChage();
  });
};

export const routeChage = (url, params) => {
  history.pushState(null, null, url); //history에 기록
  window.dispatchEvent(new CustomEvent(ROUTER_CHAGE_EVENT, params));
};

url route 구현

//app.js
  this.route = () => {
    const { pathname } = location;

    if (pathname === '/') {
      header.setState({ isShowBackButton: false, isShowAddButton: true });
      section.setState('게시판 글 목록 페이지');
      new PostListPage({ $target: section.$element });
      return;
    }

    if (pathname.includes('/post/')) {
      header.setState({ isShowBackButton: true, isShowAddButton: true });
      section.setState('게시판 상세 페이지');
      new PostDetailPage({
        $target: section.$element,
        postId: pathname.split('/post/')[1],
      });
      return;
    }

    if (pathname.includes('/edit/')) {
      header.setState({ isShowBackButton: true, isShowAddButton: false });
      section.setState('게시판 글 수정 페이지');
      new PostEditPage({
        $target: section.$element,
        postId: pathname.split('/edit/')[1],
      });
      return;
    }

    if (pathname.includes('/upload')) {
      header.setState({ isShowBackButton: true, isShowAddButton: false });
      section.setState('게시판 글 작성 페이지');
      new PostUploadPage({ $target: section.$element });
      return;
    }

    main.$element.innerHTML = 'page not nound';
  };

  router(this.route);
  window.addEventListener('popstate', this.route); //popstate event 발생시 위에서 설정한 pathname 조건에 따라 처리 

url 이동 구현

import PostList from '../components/PostList/PostList.js';
import { readPostList } from '../lib/postsApi.js';
import { routeChage } from '../router.js';

export default function PostListPage({ $target }) {
  this.state = [];

  this.render = async () => {
    new PostList({
      $target,
      props: await readPostList(),
      onClick: (postId) => {
        routeChage(`/post/${postId}`); // rotuer에 정의된 rotueChange 호출
      },
    });
  };

  this.render();
}

여기서 변수명에 $표시가 되어있는 것을 볼 수 있는데, 일반 변수와 element를 담는 변수를 구분짓기위해서 element 변수에 $를 붙여줬다. 이 형식은 예전 jquery 방식이 유행할때 사용하던 방식인데, 최근에는 react가 주를 이루다보니 많이 보지 못한 방식일 수 있다. react에서는 DOM에 직접적으로 접근하는 것을 지양하기 때문에 더더욱 그런거 같다.


컴포넌트 구현

폴더구조

우선 폴더구조는 React구조와 동일하게 다음과 같이하였다

  • assets: 이미지 등과 같은 정적파일 관리
  • pages: routing에 사용되는 가장 큰 단위의 페이지 컴포넌트
  • compoents
    • UI: 모든 컴포넌트가 공유하여 사용되는 컴포넌트 모음
    • Layout: Header, Main과 같이 전체 페이지에서 틀로 사용되는 컴포넌트
    • 그 외: 각 Pages에서 사용되는 컴포넌트

상태관리와 기능 분리하기

이전에 프로그래머스에서 프론트엔드 개발을 위한 자바스크립트 (feat. VanillaJS)를 수강한적 있었는데, 상위(부모) 컴포넌트에서 이벤트를 props로 내려주어 재사용성을 높인다는 것을 배운적이 있어서 이번에 적용해 보았다.

하기에서 처럼 PostEditPage단에서 이벤트를 구현하고, PostForm 컴포넌트에서 이를 props로 내려받아 onClick, onSubmit 이벤트를 발생하도록 구현했다.

import PostForm from '../components/PostForm/PostForm.js';
import { readPost, uploadPost } from '../lib/postsApi.js';
import { getRandomPhoto } from '../lib/unsplashApi.js';
import { routeChage } from '../router.js';

export default function PostEditPage({ $target, postId }) {
  this.state = {
    title: '',
    content: '',
    image: '',
  };

  this.setState = async (newState) => {
    this.state = {
      ...this.state,
      ...newState,
    };
  };

  this.render = async () => {
    const data = await readPost(postId);

    if (!data) {
      return;
    }

    this.setState(data.post);

    new PostForm({
      $target,
      props: {
        ...this.state,
        action: 'edit',
      },
      // 이벤트를 부모컴포넌트에서 구현하여 전달
      onClick: async (event) => {
        event.target.classList.add('click-block');

        const response = await getRandomPhoto();
        const image = response[0].urls.small;
        this.setState({ image });

        event.target.classList.remove('click-block');
        event.target.parentNode.querySelector('#post-image').src = image;
      },

      onSubmit: async (data) => {
        const response = await uploadPost(this.state.postId, data);

        if (response?.code === 200) {
          routeChage(`/post/${postId}`);
        } else if (response?.code === 400) {
          console.log('bad request error');
        } else {
          console.log('server Error');
        }
      },
    });
  };

  this.render();
}

이렇게 처리함으로써 PostForm 스타일을 재사용하면서 서로 다른 이벤트를 발생시키는 컴포넌트를 구현이 가능해진다.

사실 강의를 수강할 당시에는 프론트엔드에 대해 1도 모르고 js 공부를 시작한지 2주차에 들어서, 컴포넌트가 뭔지 props를 내린다는 개념이 이해하기 어려웠는데 나중에 리엑트를 공부하면서 깨달았다. 나중에 여유가 된다면 꼭 들어보길 추천하는 강의다.
지금 나의 웹 기초 지식은 해당 강의에서 나온 단어들로 대부분 완성되었다고 해도 과언이 아니라고 생각된다.


lighthouse 사용과 이미지 lazy 로딩 처리하기

이번 프로젝트에 하다보니 욕심이 생겨서 lighthouse를 사용해보았다. 접근성을 놓진 부분들에서 개선할 수 있었지만 개선할 수 없었던 가장 큰 문제는 이미지 사이즈였다. 해당 챌린지에서 백엔드가 제공되지만, 이미지에 대해서 url만 받다보니 다른 개발자들이 큰 사이즈를 보내면 막을 방도가 없어 개선시키기 어려웠다.
그래서 Lazy loading을 적용하기로 했다. 이 부분에 대해서도 시행착오가 많았다. 처음에는 아이템 전체에 걸었지만, 제 각각의 크기로 인해 아이템이 심하게 차이가나게 뜨는 경우가 많았다. 그래서 해결책은 이미지에만 lazy 로딩을 적용하였다. lighthouse의 점수는 개선시킬 수는 없었지만, 사용자가 이미지를 제외한 부분은 빠르게 받아볼 수 있도록 로딩 속도를 개선할 수 있었다.
구현은 이미지 컴포너트에 placeholder 클래스 명과 src주소를 dataset에 저장하여, setTimeout을 통해 비동기로 나중에 실제 이미지로 대체되도록 구현하였다. setTimeout에 별도의 시간은 설정하지 않아도 충분하다고 생각되어 설정하지 않았다.

img tag 자체에도 lazy 속성을 적용하는 것도 가능하지만, 좀더 자연스러운 변환을 만들어 보고 싶어 위와 같이 별도로 javascript를 변경하는 placeholder 방식으로 적용해습니다.

구현하기

export async function imageLoad(sourceClass) {
  const $placeholders = document.querySelectorAll('.placeholder');

  $placeholders.forEach(($placeholder) => {
    const $img = document.createElement('img');
    $img.src = $placeholder.dataset.src;
    $img.alt = $placeholder.dataset.alt;
    $img.className = sourceClass;

    $placeholder.removeAttribute('data-src');
    $placeholder.removeAttribute('data-alt');

    $img.addEventListener('load', () => {
      setTimeout(() => {
        $placeholder.appendChild($img);
      });
    });
  });
}

배포하기

배포는 netlify를 통해 배포하였다. 마지막 배포에 와서 다시한번 고통이 찾아왔다.
바로 https to http 요청을 하면서 발생한 문제인데, 이것때문에 거의 날밤을 샜다. 별거 아닐줄 알았는데 netlify에서 처음 배포하다보니 여기서 설정해야하는 부분들을 알지 못해서 많이 해맸던거 같다. 생각보다 간단한 문제였는데, 자세한 내용은 git PR로 남겨두었으니 혹시나 비슷한 오류가 발생한다면 참고해보면 좋을거 같다

Git PR: netlify - http api 호출 이슈

끝으로

바닐라 자바스크립트로 이렇게까지 구현한게 처음이여서, 사실 코드가 조금 가독성이 떨어진다는 느낌을 스스로 많이 받았다. 하지만 react가 없더라도 컴포넌트 기반으로 구현이 가능할 수 있게된거 같아 뿌듯한 프로젝였다.

넘블에서 비슷한 챌린지가 열린다면 지금 보다는 가독성이 높은 코드로 작성해보고 싶다.

profile
프론트엔드 개발자

0개의 댓글