
대대적인 리뉴얼을 통한 자기소개 웹페이지 완성 🙌
박스 그룹화로 div 묶는 법... CSS 변수 설정 및 속성 등 조금 더 공부해보니 초기에 기획했던 AS-IS가 말도안되게 엉망이었다는 것을 깨닫고 전체적인 리뉴얼 결심!!!
HTML 마크업, CSS 설정까지 전반적으로 수정하고 자바스크립트로 기능을 추가하였다.
단순하기 짝이 없는 프로토타입이다... 너무 단순한 구성과 색상, 자바스크립트를 이용하여 구현할 기능이 부족한 페이지라고 판단하고 갈아엎었다.
일정이 빠듯한데..급하게 전면수정해야해서 기획단계는 간소화하였다. 간단하게 펜과 스케치북을 가지고 와이어프레임을 진행하였다. 어떻게 만들지 틀만 대충 생각해보았다.

제일 먼저 보게 될 홈페이지 인트로 부분에서 정적인 것보다는 동적인 애니메이션으로 포인트를 주면 좋을 것 같다고 생각하여 영상을 넣을지 방법을 고민하다가 타이핑효과를 구현하였다.
👇 html
<div class="home__title">
<h2 class="home__introtext"></h2>
</div>
👇 css
.home__introtext::after {
content: "|";
animation: blink 320ms step-end infinite;
}
@keyframes blink {
50% {
border-color: var(--color-white);
opacity: 0;
}
}
animation: .home__introtext뒤에 해당 애니메이션을 넣어준다.
-> 함수명 / 반복시간 / timingfunction / 반복되는count@keyframes: 애니메이션을 재생할 프레임의 스타일을 정의한다.
-> content의 색은 흰색으로 지정하고 실행 시 50% 구간에서 투명하게 해 깜빡거리는 것과 같은 효과를 줄 수 있다.
참고 : MDN(animation)
👇 자바스크립트
/* intro 타이핑효과 */
const introText = document.querySelector(".home__introtext");
const content = "안녕하세요.\n저는 구수정입니다 :)";
let index = 0;
let putWord = "";
const typingInterval = setInterval(() => {
putWord += content[index++];
introText.innerText = putWord;
// introText.innerText += content[index++];
// 위의 코드처럼 진행 시 띄어쓰기가 나올 경우 ex) "저는 "까지 입력된 innerText를 가져올 시 띄어쓰기가 trim됨
}, 320);
setTimeout(() => {
clearInterval(typingInterval);
}, 320 * content.length + 30);
textContent / innerText / innerHTML
textContent는<script>,<style>요소를 포함한 모든 요소의 콘텐츠를 가져온다.innerText는 렌더링이 되어 "사람이 읽을 수 있는" 요소만 처리한다.innerHTML은 DOM트리를 수정하는 것으로 XSS 공격에 취약하므로 가급적 사용하지 않는것이 좋다.
setInterval(): 한글자 입력하는데 설정한 시간인 0.32초마다 함수를 실행시킨다. 따라서 한글자씩 .home__introtext에 들어가진다.setTimeout(): 모든 문장을 입력하는데 설정한 시간이 지나면 함수를 호출한다. -> 사파리에서 약간 시간지연 있어 +30 적용clearInterval(): 해당 함수의 인터벌을 중단시킨다. 함수명 입력 시 '()'를 입력한다면 해당 함수의 반환값을 가져오는 것이므로 함수명만 적어준다.
👇 html
<ul class="navbar__menu">
<li class="navbar__menu__item selected" data-link="#home">Home</li>
<li class="navbar__menu__item" data-link="#about">About</li>
<li class="navbar__menu__item" data-link="#skill">Skill</li>
<li class="navbar__menu__item" data-link="#work">Portfolio</li>
<li class="navbar__menu__item" data-link="#footer">contact</li>
</ul>
👇 css
.navbar__menu {
display: flex;
flex-flow: row;
justify-content: space-evenly;
}
.navbar__menu__item {
padding: 10px;
margin: 0 10px;
cursor: pointer;
color: var(--color-white);
}
.navbar__menu__item:hover {
color: var(--color-blue);
border-radius: 15px;
background-color: var(--color-white);
}
hover: 마우스 위에 커서를 올렸을 때 요소를 선택한다.
->.navbar__menu__item요소 위에 마우스가 올라가면 이렇게 스타일링하겠다.
👇 자바스크립트
const navbarMenu = document.querySelector(".navbar__menu");
navbarMenu.addEventListener("click", (event) => {
// event.target : <li class="navbar__menu__item active">Home</li>
// event.target.dataset : DOMStringMap {link: '#about'}
const link = event.target.dataset.link;
if (link == null) return;
scrollinto(link);
});
/* 해당 목적지까지 스크롤 이동 */
function scrollinto(selector) {
const destination = document.querySelector(selector);
destination.scrollIntoView({ behavior: "smooth" });
}
addEventListener(): 클릭 시 이벤트를 등록한다.event.target.dataset.link: 클릭한 navbar__menu 클래스를 가진 요소의 data객체의 키 link의 value를 가져온다.scrollinto(): 해당 value를 id값으로 가지고 있는 곳으로 스크롤이 이동한다. 옵션으로 behavior:"smooth"를 지정하여 스크롤이 부드럽게 이동하게끔 해주었다.

👇 자바스크립트
const intro = document.querySelector(".home__container");
const introHeight = intro.offsetHeight;
document.addEventListener("scroll", () => {
intro.style.opacity = 1 - window.scrollY / introHeight;
});
offsetHeight(): 우리가 보고 있는 부분의 높이값을 가져오는 함수- scroll이벤트가 일어났을 때 scroll한만큼 Y값을 가져와서 opacity값을 지정한다.

👇 css
.top-btn {
position: fixed;
right: 30px;
bottom: 30px;
font-size: 50px;
color: var(--color-white);
border: none;
background-color: transparent;
opacity: 0;
/* opacity로 버튼을 가려놓은 상태라 해당 위치에 마우스를 올리면 포인터가 생기기 때문에
지워주는 스타일링 */
pointer-events: none;
transition: all 300ms ease-in;
}
.top-btn.visible {
opacity: 1;
pointer-events: auto;
cursor: pointer;
}
👇 자바스크립트
const topBtn = document.querySelector(".top-btn");
document.addEventListener("scroll", () => {
if (window.scrollY > introHeight / 2) {
topBtn.classList.add("visible");
} else {
topBtn.classList.remove("visible");
}
});
topBtn.addEventListener("click", () => {
scrollinto("#home");
});
/* 해당 목적지까지 스크롤 이동 */
function scrollinto(selector) {
const destination = document.querySelector(selector);
destination.scrollIntoView({ behavior: "smooth" });
}
- 스크롤이 첫화면의 반이상을 지났을 때
.top-btn의 요소에 visible이라는 클래스를 추가하여 top버튼을 생성한다.- 해당 버튼을 클릭했을 때
#homeid를 가지고 있는 요소, 즉 첫화면으로 스크롤 이동시킨다.

👇 html
<div class="work__categories">
<button class="category__btn selected" data-filter="*">
all
<span class="category__count">3</span>
</button>
<button class="category__btn" data-filter="front">
front
<span class="category__count">2</span>
</button>
<button class="category__btn" data-filter="back">
back
<span class="category__count">1</span>
</button>
</div>
👇 자바스크립트
const categoryBtn = document.querySelector(".work__categories");
const projects = document.querySelectorAll(".project");
const projectContainer = document.querySelector(".work__projects");
categoryBtn.addEventListener("click", (e) => {
const selected = document.querySelector(".category__btn.selected");
// 기존 선택되었던 버튼 스타일링 삭제
selected.classList.remove("selected");
const target =
e.target.nodeName === "BUTTON" ? e.target : e.target.parentNode;
// 선택된 버튼에 스타일링
target.classList.add("selected");
/*
숫자버튼을 나타내는 span 태그를 클릭시 data-filter가 정의되어있지 않아 parentnode인
category__btn의 filter값을 가져온다.
*/
const filter = e.target.dataset.filter || e.target.parentNode.dataset.filter;
if (filter == null) return;
projectContainer.classList.add("anim-out");
setTimeout(() => {
projects.forEach((project) => {
/*
카테고리버튼의 filter값이랑 project이미지의 filter값이랑 같아야
front일때 front프로젝트, back일 때 back프로젝트만 보여짐
*/
if (filter === "*" || filter === project.dataset.type) {
project.classList.remove("invisible");
} else {
project.classList.add("invisible");
}
});
projectContainer.classList.remove("anim-out");
}, 300);
});
- 하나의 버튼에는
<button>과<span>이 함께있어서<button>클릭시에는 바로 target을 지정해주면 되지만<span>클릭 시에는 부모노드를 target으로 지정해주어야 한다.- 이미지들이 안보였다가 필터링 후 0.3초 뒤에 필터링된 이미지들이 새롭게 보여야하므로 setTimeout()안에서 필터링을 진행했다.
-> setTimeout()밖에서 진행할 시 이미지들이 필터링이 된 모습으로 보여졌다가 사라졌다가 다시 보이게 되어 애니메이션이 불편해보인다.- 카테고리버튼의 filter값이랑 project이미지의 filter값이랑 같아야 front일때 front 프로젝트만, back일 때 back프로젝트만 보여짐
미디어쿼리와 flex, position을 이용하여 스타일링하여 반응형으로 구현해보았다.
보편적인 모바일 사이즈인 768px이하일 때 navigation bar를 토글버튼으로 묶어서 구현하였고 전체적인 폰트사이즈, padding값을 수정하였다.
/* 768px 이하의 스크린에서 */
@media screen and (max-width: 768px) { ... }
더 자세한 코드들이 궁금하시다면?
이번에 홈페이지를 만들면서 마크업, 어떻게 div박스들끼리 묶을지 고민하는 과정이 큰 틀을 짜는데 중요한 초기과정이라는 걸 느꼈다. flex, position의 개념을 잡아가는데 좋은 경험이었고 각 브라우저 별 개발자모드에서 요소들의 정보를 파악하고 디버깅에 대해 경험했고 자바스크립트의 문법을 조금 더 공부해야겠다.