네이버 뉴스 스탠드 프로젝트

섭승이·2024년 7월 20일
post-thumbnail

바닐라 js 를 활용한 뉴스 스탠드 프로젝트

위의 화면이 최종적으로 완성한 화면 입니다.

기능 정리

  1. 맨 위의 날짜를 현재 날짜로 받아오는 기능
  2. 위쪽 뉴스 특보를 autorolling 기술을 활용해 5초에 한번씩 다른 뉴스 제목이 나오는 기능 (왼쪽, 오른쪽은 1초의 차이를 가지고 있다.)
  3. 뉴스 카테고리 부분에서 애니메이션으로 20초동안 왼쪽에서 오른쪽으로 게이지가 차는 효과
  4. 카테고리를 클릭하면 해당 카테고리의 첫번째 언론사로 이동하는 기능
  5. 왼쪽 화살표와 오른쪽 화살표를 이용해 언론사를 바꾸는 기능
  6. 오른쪽 아이콘을 통해 뉴스가 보여지는게 리스트인지 그리드인지 선택하는 기능
  7. 구독하기 버튼을 누르면 5초동안 토스트 메세지가 나오고 내가 구독한 언론사로 이동하는 기능
  8. 구독된 언론사에서 x버튼을 누르면 구독이 해지되는 기능
  9. 왼쪽 전체 언론사, 내가 구독한 언론사 버튼을 통해 전체 언론사를 보거나 내가 구독한 언론사를 확인하는 기능

2주동안 진행한 프로젝트로 바닐라 js 를 학습하고 DOM에 어떻게 접근할 수 있는지에 대해 학습할 수 있었습니다.


가장 어려웠던 기능 구현 2가지

게이지가 차는 애니메이션 효과

  1. css 효과를 주기 위해 after를 사용해야 했습니다.
  2. setInterval와 clearInterval, setTimeout 과 clearTimeout 을 이용해 구현해야 했습니다.
  3. 게이지가 다 차거나 클릭이벤트가 발생하면 아래 언론사와 기사를 보여주는 정보를 바꿔야 했습니다.

주요 코드

// 선택된 카테고리의 스타일을 변경
NewsListCategory.classList.replace("notselectNews", "selectNews"); 

// news 안에 내용을 채워줌 (pressInfo)
const pressInfoText = `
            <img src = ${newsData.companyIcon}>
            <span class = "display-medium12">${newsData.updatedDate}</span>
            ${pressInfoButton(
                subscribedData.includes(newsData.companyName) ? "" : "구독하기"
            )}`;
pressInfo.innerHTML = pressInfoText;

// 모든 정보를 업데이트한 후에 intervalId를 종료하고, 일정 시간이 지난 후에 스타일을 초기화
if (currentMidiaIndex >= totalCompanies) {
	clearInterval(currentIntervalId);
	setTimeout(() => {
      NewsListCategory.classList.replace("selectNews", "notselectNews");
      NewsListCategory.textContent = allNewsData[index].category;
		rightButton.classList.remove("hidden");
		leftButton.classList.remove("show");
		currentMidiaIndex = 0;
	}, 20000);
}

// 일정 시간 간격으로 updateNewsCategory 함수를 실행
        currentIntervalId = setInterval(() => {
            leftButton.className = "show";
            updateNewsCategory();
        }, 20000);

        // 맨 처음 랜더링을 해주기 위해 호출
        updateNewsCategory();

        // 일정 시간이 지난 후에 intervalId를 종료
        await new Promise((resolve) => {
            currentTimeoutId = setTimeout(() => {
                clearInterval(currentIntervalId);
                resolve();
                NewsListCategory.classList.replace("selectNews", "notselectNews");
                NewsListCategory.textContent = allNewsData[index].category;
                currentMidiaIndex = 0;
            }, (totalCompanies - nowIndex) * 20000);
        });

카테고리 안에 언론사에서도 돌아야하기 때문에 clearInterval 을 맨 처음 시작될때나 카테고리를 클릭할때, 화살표를 통해 언론사를 이동할때 3가지로 나눠서 처리를 해줬습니다.

clearInterval, setInterval의 사용이 중요했고 Promise를 통해 비동기로 애니메이션을 처리해줘야 했습니다.


구독하기나 해지하기 버튼을 눌렀을때 일어나는 동작

구독하기 버튼을 누를때는 5초동안 snackBar가 나오고 snackBar가 사라지면 내가 구독한 언론사로 이동해야 했습니다.
해지하기 버튼이나 x버튼을 누를때는 정말로 해지할건지 물어보는 모달창이 나오고 예를 누르면 해지한 언론사의 다음 언론사로 넘어가게 만들어야 했습니다.

import { pressInfoButton } from "../components/button.js";
import { snackBar } from "../components/snackBar.js"; 
import { clickSubscribeNews } from "./clickSubscribeNews.js"; 
import { addSubscribedData } from "./subscribeData.js"; 

// 구독하기 버튼이나 해지하기 버튼을 눌렀을 때 실행되는 함수
export const handlePressInfoButtonClick = (
  button,
  newsData,
  subscribedData,
  isList
) => {
  button.addEventListener("click", () => { // 버튼에 클릭 이벤트 리스너 추가
    const pressInfo = document.querySelector(".pressInfo"); 
    const leftButton = document.getElementById("leftButton"); 

    if (!subscribedData.includes(newsData.companyName)) { // 구독 데이터에 현재 뉴스 회사가 포함되어 있지 않은 경우
      const pressInfoText = `
                  <img src="${newsData.companyIcon}"> <!-- 뉴스 회사 아이콘 표시 -->
                  <span class="display-medium12">${newsData.updatedDate}</span> <!-- 업데이트 날짜 표시 -->
                  ${pressInfoButton("구독하기")} <!-- 구독하기 버튼 생성 -->
              `;
      pressInfo.innerHTML = pressInfoText; // pressInfo 요소에 위의 HTML을 삽입
      snackBar("내가 구독한 언론사에 추가되었습니다.", isList); // 스낵바 알림 표시
      if (isList) { // 만약 리스트 형태인 경우
        setTimeout(() => { // 5초 후에 실행되는 함수
          clickSubscribeNews(newsData.companyName); // 뉴스 회사 이름을 인자로 clickSubscribeNews 함수 호출
          leftButton.className = "show"; // leftButton 요소의 클래스 이름을 "show"로 변경
        }, 5000);
      } else { // 리스트 형태가 아닌 경우
        button.innerHTML = `<img src='../../images/closed.svg' alt='closed icon'/>
              <span>해지하기</span>`; // 버튼의 내용을 해지하기로 변경
      }
      addSubscribedData(newsData.companyName); // 뉴스 회사 이름을 구독 데이터에 추가
    } else { // 구독 데이터에 현재 뉴스 회사가 포함되어 있는 경우
      const cancelAlert = document.getElementById("cancelAlert"); // cancelAlert 아이디를 가진 요소르ㄹ 선택
      const cancelText = document
        .getElementById("cancelAlertTop")
        .querySelector("p"); // cancelAlertTop 아이디를 가진 요소의 자식 요소 p를 선택
      cancelText.innerHTML = `<strong>${newsData.companyName}</strong>을/를<br>구독해지하시겠습니까?`; // 구독 해지 확인 메시지 설정
      cancelAlert.className = "show"; // cancelAlert 요소의 클래스 이름을 "show"로 변경
    }
  });
};

위와 같이 구현을 했고 구독하기 / 해지하기 로 나누어 컴포넌트화 했으며 해당 코드를 그리드, 리스트일때 동일하게 사용했습니다.
또한 그리드와 리스트 각각에서 전체 언론사 / 내가 구독한 언론사에서도 동일하게 클릭이벤트로 재사용했습니다.


배운 점


1. 프로젝트 설계의 중요성

프로젝트의 범위를 정의하고, 필요한 기능을 나열하며, 이를 바탕으로 구조와 흐름을 계획했습니다.
이를 통해 구현 중간에 발생할 수 있는 문제를 미리 예측하고 대비할 수 있었습니다.

2. DOM 접근 방식

선택자 활용: document.querySelector와 document.getElementById를 사용하여 필요한 요소를 선택하고 조작했습니다.
이벤트 리스너: addEventListener를 통해 사용자 인터랙션을 처리했습니다. 클릭 이벤트, 마우스 오버 이벤트 등을 통해 사용자와 상호작용했습니다.
동적 생성 및 삽입: innerHTML을 사용하여 동적으로 콘텐츠를 생성하고 삽입했습니다.

3. setInterval 활용 방법

setInterval을 사용하여 주기적으로 특정 작업을 수행할 수 있습니다.
이번 프로젝트에서는 자동으로 뉴스 기사를 갱신하거나 특정 동작을 반복하기 위해 활용했습니다.

4. CSS :after

CSS의 :after 가상 요소를 사용하여, DOM 요소의 실제 구조를 변경하지 않고도 스타일을 추가할 수 있음을 배웠습니다.
추가적인 스타일링: 애니메이션을 적용할때 이용했습니다.


이번 프로젝트를 통해 React와 같은 라이브러리의 중요성을 다시 한번 느낄 수 있었습니다.
바닐라 js 로 기능을 구현하면서 불편함을 많이 느낄 수 있었고 특히 useState 와 useEffect의 필요성을 느낄 수 있었습니다!!

profile
소통하며 성장하는 프론트엔드 개발자 이승섭입니다! 👋

1개의 댓글

comment-user-thumbnail
2024년 8월 2일

완벽하다...

답글 달기