<body>
<section>
<!-- 제목, 로고, 날짜 영역 -->
<div>
<!-- 제목, 로고 -->
</div>
</section>
<section>
<!-- 부제목 영역 -->
<div>
<!-- 부제목 -->
</div>
<div>
<!-- 부제목 -->
</div>
</section>
<section>
<!-- 그리드 영역 -->
<div>
<!-- 제목 -->
<!-- 버튼 -->
</div>
<div>
<!-- 버튼 -->
<!-- 그리드 -->
<!-- 버튼 -->
</div>
</section>
</body>-
flex
공통적으로 사용하는 css를 정의하여 재사용성을 높였습니다.
.flex_row { display: flex; flex-direction: row; align-items: center; } .flex_column { display: flex; flex-direction: column; }
-
grid
display: grid속성을 활용하여 grid를 생성하였습니다..grid_container의 좌측과 상단 border를 설정하고,
.grid_container > li의 우측과 하단 border를 설정하여
border가 겹치는 현상을 해결하였습니다..grid_container { display: grid; border-top: solid 1px #d2dae0; border-left: solid 1px #d2dae0; grid-template-rows: repeat(4, 1fr); grid-template-columns: repeat(6, 1fr); ...; } .grid_item { border-right: solid 1px #d2dae0; border-bottom: solid 1px #d2dae0; ...; }
-
애니메이션
-
롤링
각 prev, now, next에 대한 속성은 javascript에서
interval로 변환 시켜주고 transition을 통해 1초간 올라가는 모션의 애니메이션은 css에서 주었습니다..newsbanner__list--prev { top: -20px; display: block; transition: top 1s ease; } .newsbanner__list--now { top: 0px; display: block; transition: top 1s ease; } .newsbanner__list--next { top: 20px; display: block; }
-
프로그레스 바
프로그레스바가 들어있는 카테고리들에 모두 넣어주지만 clicked 된 카테고리만
display:inline속성을 부여해 보이도록 설계했습니다./* progress animation */ .progressbar { display: none; position: absolute; background-color: #4362d0; height: 100%; width: 0px; top: 0; left: 0; z-index: 50; animation: scale 20s linear; animation-iteration-count: infinite; } @keyframes scale { to { width: 100%; } }
-
-
형식을 맞추기 위해 formatting을 하기보다 보다 편한 방법이 있지 않을까 해서 검색해보니
toLocalDateString이라는 함수를 알게 되었는데, 이 함수에 인자로options를 주면 원하는 Date형식으로 커스텀해주는 기능을 알게되었습니다.let today = new Date(); const options = { year: "numeric", month: "2-digit", day: "2-digit", weekday: "long", }; today = today.toLocaleDateString("ko-KR", options);
-
셔플 함수를 이용하여
pressObj.js에 있는 언론사의 리스트를 섞어주었고// 언론사 랜덤 셔플 function shuffleArray(array) { array.sort(() => Math.random() - 0.5); return array; }
셔플된 리스트에
createGridItem(element)함수로 언론사의 이미지와 마우스 호버, 아웃 이벤트를 추가해준 후grid_container안에 넣어주는 방법으로 그리드 아이템을 랜덤배치했습니다.// 셔플된 리스트 그리드리스트에 append function appendGridList() { const gridContainerList = document.getElementsByClassName("grid_container"); const shuffledArr = shuffleArray(pressObjArr); shuffledArr.forEach((element, idx) => { const id = Math.floor(idx / MAX_GRID_COUNT); const gridItem = createGridItem(element); gridContainerList[id].appendChild(gridItem); }); }
-
html에 미리
grid_container를 만들어 놓고 언론사 리스트를 넣어놓은 상태에서display:none으로 보이지 않도록 막아준 후에 버튼클릭으로 각 페이지별로display속성을 바꿔주는 방식으로 페이지간 전환 기능을 구현하였습니다.// 그리드 페이지 업데이트 function showGridPage(increment) { const curPage = document.getElementById(`page${now_grid_page}`); now_grid_page += increment; const nextPage = document.getElementById(`page${now_grid_page}`); now_grid_page = Math.max(0, Math.min(now_grid_page, 3)); curPage.style.display = "none"; nextPage.style.display = "grid"; showGridPageButton(); }
-
좌우 롤링애니메이션을 따로 호버액션을 구현하기 위해
rollingEvent와createBannerItem함수에서 state인자로 좌우를 구분하여 받아주었고,createBannerItem에서 마우스 호버, 아웃 이벤트 리스너를 추가해준 후에 appendChild하는 방식으로 구현하였습니다.// 롤링에 들어갈 뉴스 리스트 추가 function appendRollingList() { const rollingListContainerLeft = document.getElementsByClassName( "newsbanner__list-container--left" ); const rollingListContainerRight = document.getElementsByClassName( "newsbanner__list-container--right" ); for (let i = 0; i < ROLLING_NEWS_NUM; i++) { const leftItem = createBannerItem(i, rollingNewsContentLeft[i], "left"); const rightItem = createBannerItem( i, rollingNewsContentRight[i], "right" ); rollingListContainerLeft[0].appendChild(leftItem); rollingListContainerRight[0].appendChild(rightItem); } }
좌우 배너간의 1초 차이를 구현하기 위하여 오른쪽 배너의
setInterval안에setTimeout으로
1초 지연시켜주었습니다.// 왼쪽 배너 롤링 반복 let rollingIntervalLeft = setInterval(() => { rollingEvent("left"); }, 5000); // 오른쪽 배너 롤링 1초 Timeout 후 반복 let rollingIntervalRight = setInterval(() => { setTimeout(() => { rollingEvent("right"); }, 1000); }, 5000);
-
리스트뷰의 현재 페이지를 증가시켜주는 함수와 그에 따른 리스트 좌우 버튼과 카테고리를 업데이트를 해주는 함수를
CATEGORY_TAB_TIME인 20초 마다 반복해주는 Interval을 멈추거나 시작하는 함수입니다.export function stopCategoryInterval() { clearInterval(categoryInterval); } export function startCategoryInterval() { categoryInterval = setInterval(() => { listPageUp(); updateCategoryClicked(); updateListButton(); }, CATEGORY_TAB_TIME); }
프로그레스바가 진행중인 카테고리를
$(".category_list--clicked")를 사용하여 찾은 후,
innerHTML로 현재와 전체 페이지를 렌더링 해준 후, 전체 페이지와 현재 페이지를 비교하며 다음 카테고리로 넘어가는 경우엔classList.remove와add를 통해 구현하였습니다.// 카테고리 탭 숫자 업데이트 function updateCategoryTabNum() { const firstCategory = $(".category_list"); const clickedCategory = $(".category_list--clicked"); clickedCategory.children[1].children[0].innerHTML = `${NOW_LIST_PAGE.getValue()} / `; if ( // 다음 카테고리로 넘어가야할 경우 isTabFull(clickedCategory.children[1].children[1].innerHTML) ) { if (clickedCategory.nextElementSibling === null) { firstCategory.classList.add("category_list--clicked"); firstCategory.children[1].children[0].innerHTML = "1 / "; NOW_CATEGORY_IDX.setValue(0); } else { clickedCategory.nextElementSibling.classList.add( "category_list--clicked" ); clickedCategory.nextElementSibling.children[1].children[0].innerHTML = "1 /"; NOW_CATEGORY_IDX.incrementValue(1); } clickedCategory.classList.remove("category_list--clicked"); NOW_LIST_PAGE.setValue(1); } appendNewsList(); }
-
리스트 버튼을 각각
$(".left_list_button")와 같이 querySelector을 통해 값을 찾은 후 클릭 이벤트 함수를 지정하여 리스트뷰의 내용을 변경하도록 구현하였습니다.leftListButton.addEventListener("click", () => { listArrowButtonClicked(-1); }); rightListButton.addEventListener("click", () => { listArrowButtonClicked(1); });
좌우간에 리스트 페이지의 변화가 카테고리에도 적용되야하므로 프로그레스 바의 애니메이션 타이밍도 재조정이 필요했는데,
offsetWidth를 사용하여 프로그레스 애니메이션과 interval을 refresh 해주도록 구현하였습니다.export function listArrowButtonClicked(increment) { if (NOW_LIST_PAGE.getValue() + increment === 0) { NOW_CATEGORY_IDX.incrementValue(-1); NOW_LIST_PAGE.setValue( categoryList[NOW_CATEGORY_IDX.getValue()].data.length ); } else { NOW_LIST_PAGE.incrementValue(increment); } const clickedCategory = $(".category_list--clicked"); clickedCategory.children[2].classList.remove("progressbar"); clickedCategory.offsetWidth; clickedCategory.children[2].classList.add("progressbar"); refreshInterval(); updateListButton(); }
처음에는 프로그레스바를 삭제한 후 다시 추가하는 방식으로 애니메이션을 초기화해주고 싶었지만
변화가 없어 검색을 해보니 브라우저는 아래의 순서로 렌더링을 진행하기 때문에 그렇다고 합니다.- 클래스가 존재하면 지운다
- 브라우저는 변화를 감지하지만 바로 적용하지 않고 일단 넘어간다.
- 클래스 다시 추가한다.
- 모든 로직이 종료되고 보니 아무런 변화가 없다. 브라우저는 변화가 없으므로 렌더링을 하지 않는다.
이를 해결하기 위해 중간에
offsetWidth를 요청하면 강제로 브라우저가 계산하게 되어 로직이 바로 적용되는 원리입니다. - 클래스가 존재하면 지운다
-
리스트와 그리드뷰에 공통적으로 좌우 이동 버튼이 있고 이를 개발하면서 결과를 확인할때 일일히 클릭하며 확인하기가 번거로워 키보드의 방향키를 입력받아 이동하는 기능이 있으면 좋겠다고 생각하여 추가기능으로 넣게 되었습니다.
window.addEventListener("keydown", (e) => { if (IS_GRID.getValue()) { if (e.key === "ArrowRight" && NOW_GRID_PAGE.getValue() < 3) { showGridPage(1); } else if (e.key === "ArrowLeft" && NOW_GRID_PAGE.getValue() > 0) { showGridPage(-1); } } else { if ( e.key === "ArrowRight" && (NOW_CATEGORY_IDX.getValue() !== CATEGORY_TAB_NUM || NOW_LIST_PAGE.getValue() !== categoryList[CATEGORY_TAB_NUM].data.length) ) { listArrowButtonClicked(1); } else if ( e.key === "ArrowLeft" && (NOW_LIST_PAGE.getValue() - 1 > 0 || NOW_CATEGORY_IDX.getValue() !== 0) ) { listArrowButtonClicked(-1); } } });
IS_GRID라는 현재 뷰의 상태를 보여주는 전역변수를 반환하는 객체를 통해 현재 보여지는 뷰에 대한 이동만 적용되도록 나누어주고, 인덱스를 넘어가지 않게 if문으로 조건을 걸어주어 구현하였습니다.반환값인
e에서key값을 통해 키보드의 버튼을 구분하는 것이 신기했고, 함수로 많이 구현하다 보니 함수를 재사용하여 금방 구현할 수 있어서, 다시 한 번 함수형 프로그래밍과 재사용성의 중요성을 알게되었습니다.
코드를 설계하고 정리하고나서 구현하지 않고, 기능부터 완성시키고 이를 정리하고 분리하려다보니, 전역변수는 물론 서로 의존성을 분리하기가 쉽지 않았습니다. 이를 해결하기 위해 먼저 종이에 그려보며 함수와 js들의 구조를 파악하고 전역변수와 여러 상수들을 따로 분리하며 코드 리팩토링을 진행했습니다.
- css를 적용할 때에 css를 전체 초기화하고 시작하지 않아서, 미세하게 주어진 디자인과의 차이가 보이는데, 이를 다시 초기화하고 다듬는 작업을 진행하여 css를 다시 개선시키고 싶습니다.
- css에서 색상코드와 같은 중복되는 값들을 변수화하여 재사용하고 보기 좋게 정리하고 싶습니다.
- 시간이 된다면
createElement대신에 템플릿 리터럴을 이용하여 구현해보고 장단점을 느껴보고 싶습니다. - 아직 코드상에서
children을 연속으로 부르는 등 긴 변수들이 자주 사용하고 있는데 이를 깔끔하게 정리하고 싶습니다.




