[코드 리뷰] 서울시청 클론코딩

Carrie·2024년 1월 9일
0
post-thumbnail

1. 웹접근성 향상을 위한 시맨틱 태그와 IR 기법 적용

1) 시맨틱 태그 사용하기

시맨틱 태그는 HTML 문서의 구조를 명확하게 하고, 콘텐츠의 의미를 더 잘 전달하기 위해 사용되는 태그이다.
<div> 태그 대신 <header>, <nav>, <footer>, <button>, <section> 등의 시맨틱 태그를 적절히 사용하여 문서의 구조를 명확히 했다. 예를 들어, 페이지 내에서 사용자와의 상호 작용을 하는 요소는 <button> 태그를 사용하여 접근성을 향상시키고, 의미의 명확성을 제공했다.

2) IR 기법을 사용하여 컨텐츠 숨기기

.blind {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  margin: -1px;
}

스크린 리더 사용자를 위해 추가적인 설명이 필요한 컨텐츠는 .blind 클래스를 사용하여 HTML 문서에 추가했다. 이 기법을 사용하면 시각적으로는 컨텐츠를 숨기지만, 스크린 리더를 통해 해당 내용을 읽을 수 있다.

3) 모든 <img> 태그에 alt 속성 추가

<div class="link-area">
  <a href="">
    <img src="./assets/images/mayor-link1.png" alt="공약(매니페스토)" />
  </a>
  <a href="">
    <img src="./assets/images/mayor-link2.png" alt="정책제안" />
  </a>
  <a href="">
    <img src="./assets/images/mayor-link3.png" alt="투표" />
  </a>
  <a href="">
    <img src="./assets/images/mayor-link4.png" alt="설문조사" />
  </a>
</div>

alt 속성에 이미지를 설명하는 내용을 추가하여, 스크린리더 사용자에게 정확한 정보를 제공하도록 했다.

2. Swiper를 이용한 슬라이더 구현

Swiper 라이브러리를 이용하면 복잡한 코드 없이 간단하게 슬라이더를 구현할 수 있다.

swipers.newsSlide = new Swiper("[data-swiper='newsSlide']", {
    slidesPerView: 1, // 한번에 보여줄 슬라이드 수
    loop: true, // 무한 반복
    pagination: {
      el: ".swiper-pagination",
      type: "custom", // 페이지네이션 타입을 설정해 커스텀할 수 있다.
      renderCustom: function (_, current, total) { // 현재 페이지 번호, 전체 페이지 수
        return current + "/" + total;
      },
    },
    navigation: {
      prevEl: ".btn-prev-news", // 이전 버튼
      nextEl: ".btn-next-news", // 다음 버튼
    },

    autoplay: {
      delay: 5000,
      disableOnInteraction: false, // 사용자 상호작용 후에도 자동 슬라이드 유지
    },
  });

추가적으로 슬라이드가 변경될 때마다 content도 함께 변경되도록 하는 기능도 추가 가능하다. 아래와 같이 슬라이드가 변경될 때 콜백 함수를 요청하면 된다.

on: { // 슬라이드가 변경될 때 실행될 콜백 함수 추가
  init: function () { // 초기 로드시 내용 업데이트
    updateSlideContent("newsSlide", this.activeIndex);
    // 슬라이드 이름, 현재 활성화된 인덱스를 파라미터로 함수를 호출한다.
  },
    slideChange: function () { // 슬라이드가 변경될 때마다 내용 업데이트
      updateSlideContent("newsSlide", this.activeIndex);
    },
},
  

💡 this.activeIndex란?
this.activeIndex는 Swiper 인스턴스 내에서 현재 활성화된 슬라이드의 인덱스를 나타낸다. 특정 슬라이드에 접근해야할 때 해당 값을 사용하여 현재 활성화된 슬라이드에 관한 정보를 얻거나, 해당 슬라이드의 내용을 조작하는 등의 작업을 수행할 수 있다.

// 슬라이드 content 업데이트
function updateSlideContent(slideName, activeIndex) {
	const content = swipers[slideName].slides[activeIndex].querySelector("p").textContent;
  
	document.querySelector(`[data-swiper="${slideName}"] .slide-content`).textContent = content;
}

💡 slides란?
slides는 Swiper 인스턴스에서 관리하는 모든 슬라이드 요소들의 배열이다. Swiper 인스턴스가 초기화되면, .swiper-slide 요소들이 slides 배열에 자동으로 저장된다. 해당 배열에는 슬라이드 요소들이 순서대로 저장되어 있고, 각 요소는 배열의 인덱스를 통해 접근할 수 있다.

문제 발생🧨

하지만 위와 같이 하니, 첫번째 슬라이드의 내용을 가져오지 못하는 문제가 발생했다. Swiper의 init 이벤트가 너무 빨리 발생하여, DOM이 완전히 준비되지 않은 상황에서 updateSlideContent 함수가 호출된 것이 문제의 원인이다.

해결🥳

setTimeout 함수를 사용하여 함수 호출 시점을 조정하여 해결했다.

on: {
  init: function () {
    setTimeout(() => updateSlideContent(this.activeIndex), 0); // 지연 없이 바로, 하지만 비동기적으로 호출
  },
  slideChange: function () {
    updateSlideContent(this.activeIndex);
  },
},

.
.
.

function updateSlideContent(slideName, activeIndex) {
  let content;
  if (swipers[slideName]) {
    // 실제로 해당 슬라이드가 존재하는지 먼저 확인, undefined 오류를 방지할 수 있다.
    content = swipers[slideName].slides[activeIndex].querySelector("p").textContent;
  }
  document.querySelector(`[data-swiper="${slideName}"] .slide-content`).textContent = content;
}

💡비동기적으로 호출한다는 게 무슨 말이지?
setTimeout 함수를 사용하면 지정한 함수가 비동기적으로 실행된다. 비동기적으로 실행된다는 말은 즉, 현재 실행중인 코드가 모두 완료된 후에 실행된다는 의미이다. 예를 들어, Swiper와 같은 라이브러리에서는 내부적으로 DOM 요소를 조작하고 초기화하는 작업이 있을 수 있으며, 이러한 작업이 완전히 끝난 후에 함수를 실행하고 싶을 때 setTimeout을 사용할 수 있다. 위의 코드에서는 현재의 코드 블록의 실행이 완전히 끝난 후에 updateSlideContent 함수를 호출한다.

3. slideToggle 함수를 사용한 드롭다운 메뉴 구현

기존 코드🤔

document.querySelectorAll(".menu-toggle").forEach(function (element) {
  element.addEventListener("click", function () {
    // 클릭한 메뉴의 속성 가져오기
    var targetMenu = this.getAttribute("data-menu");
    var menu = document.querySelector(
      ".sub-menu[data-menu='" + targetMenu + "']"
    );

    // 메뉴가 이미 열려 있는지 확인
    var isMenuOpen = menu.classList.contains("open");

    // 모든 하위 메뉴 클래스 초기화
    document.querySelectorAll(".sub-menu").forEach(function (menu) {
      menu.classList.remove("open");
    });
    document.querySelectorAll(".menu-toggle").forEach(function (button) {
      button.classList.remove("active", "rotate-icon");
    });

    if (!isMenuOpen) {
      menu.classList.toggle("open"); // 메뉴 열기
      this.classList.toggle("active"); // 메뉴 색상 변경
      this.classList.toggle("rotate-icon"); // 아이콘 회전
    }
  });
});

다소 길이가 길고 비효율적인 코드를 jQuery와 slideToggle() 함수를 사용하여 간결하고 효율적으로 개선했다. slideToggle() 함수를 사용하면 메뉴가 열려있는지에 대한 별도의 확인 없이 메뉴를 토글할 수 있다.

수정 코드🥳

// 모든 menu-toggle 요소에 대한 반복문 처리
$(".menu-toggle").each(function () {
  $(this).click(function () {
    const targetMenu = $(this).data("menu"); // 클릭한 대상 메뉴
    // 해당 data-menu 값을 가진 subMenu 선택
    const subMenu = $(`.sub-menu[data-menu="${targetMenu}"]`);

    // 현재 클릭한 subMenu를 제외한 모든 메뉴를 닫는다
    $(".sub-menu").not(subMenu).slideUp();
    // 현재 클릭한 메뉴 외에 다른 모든 메뉴의 클래스를 제거한다
    $(".menu-toggle").not($(this)).removeClass("active rotatoe-icon");

    
    subMenu.slideToggle(); // 클릭한 메뉴에 해당하는 서브 메뉴 토글
    $(this).toggleClass("active rotate-icon");
  });
});
profile
Markup Developer🧑‍💻

0개의 댓글