프로젝트/블로그 사이트

2. 랜딩 페이지 구현하기

syleemomo 2023. 7. 7. 15:27
728x90

헤더 디자인하기

헤더

<!-- 헤더 -->
<header>
    <a href="#" class="logo"><i class="fa-brands fa-blogger"></i>Sunblog</a>

    <div class="menu">
      <nav class="navbar">
        <ul>
          <li><a href="#about" class="active">서비스</a></li>
          <li><a href="#story">스토리</a></li>
          <li><a href="#contact">연락처</a></li>
        </ul>
      </nav>

      <div class="mode">
        <i class="fa-solid fa-toggle-on"></i>
        <i class="fa-solid fa-toggle-off active"></i>
      </div>
    </div>
</header>

index.html 파일에 위 코드를 추가한다. 헤더는 크게 로고/메뉴 영역으로 구분된다. 메뉴는 다시 네비게이션/모드 영역으로 구분된다. 모드는 다크모드/일반모드 선택이 가능하다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sunrise 블로그</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
  <link rel="stylesheet" href="css/landing.css">
</head>
<body>
  <!-- 헤더 -->
  <header>
    <a href="#" class="logo"><i class="fa-brands fa-blogger"></i>Sunblog</a>
    
    <div class="menu">
      <nav class="navbar">
        <ul>
          <li><a href="#about" class="active">서비스</a></li>
          <li><a href="#story">스토리</a></li>
          <li><a href="#contact">연락처</a></li>
        </ul>
      </nav>
      
      <div class="mode">
        <i class="fa-solid fa-toggle-on"></i>
        <i class="fa-solid fa-toggle-off active"></i>
      </div>
    </div>
  </header>
  
  <script src="js/scroll.js"></script>
  <script src="js/landing.js"></script>
</body>
</html>

헤더가 추가된 index.html 은 위와 같다. 

@import url('reset.css');
@import url('global.css');
@import url('header.css');
@import url('footer.css');
@import url('animation.css');

css 폴더에 landing.css 파일을 생성하고 위와 같이 재사용할 스타일을 추가한다. 헤더/푸터/애니메이션 스타일은 웹페이지 전반에 걸쳐 재사용이 가능하므로 따로 작성한 다음 임포트해서 사용한다. 

/* 다크 테마 */
body.dark, header.dark{
  color: #fff;
  background: #333;
}

css 폴더에 header.css 파일을 생성하고 위와 같이 작성한다. body, header 요소에 dark 클래스가 추가되면 적용될 스타일이다.

/* 헤더  */
header{
  width: 100%;
  background: #fff;
  position: sticky; top: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  z-index: 1;
}
header.active{
  box-shadow: 0 .1rem .7rem rgba(0, 0, 0, .3);
}

position:sticky 는 top 위치에 도달하지 않으면 스크롤될때 함께 움직이다가 top 위치에 도달하면 fixed 로 고정된다. 헤더의 위치를 position:sticky 와 top:0 으로 설정하면 브라우저 상단에 고정된다. 페이지가 아래 방향으로 스크롤되면 header 에 active 클래스가 추가되면서 헤더 하단에 그림자가 추가된다. 

header .menu{
  display: flex;
  align-items: center;
}

네비게이션/모드를 수평 정렬한다.

/* 네비게이션 */
header .navbar ul{
  display: flex;
  align-items: center;
}
header .navbar ul li{
  margin: 0 1rem;
}
header .navbar ul li a{
  font-size: 1.5rem;
  transition: .2s;
}
header .navbar ul li .active,
header .navbar ul li a:hover{
  color: var(--primary-color);
}

네비게이션 메뉴를 수평정렬한다. 네비게이션 메뉴에 마우스를 올리거나 active 클래스가 추가되면 색상을 변경한다. 

/* 모드 */
header .mode{
  font-size: 2.5rem;
  cursor: pointer;
  transition: 1s;
}
header .mode i:not(.active){
  display: none;
}

active 클래스가 적용되지 않은 모드는 화면에서 보이지 않도록 한다. 

/* 로고 */
header .logo{
  display: flex;
  align-items: center;
  font-size: 1.5rem;
  transition: .2s;
}
header .logo i{
  font-size: 3rem;
  margin-right: .5rem;
}
header .logo:hover{
  color: var(--primary-color);
  letter-spacing: .1rem;
}

로고에 마우스를 올리면 색상이 변하고 글자간격이 넓어진다. 

/* 다크 테마 */
body.dark, header.dark{
  color: #fff;
  background: #333;
}
/* 헤더  */
header{
  width: 100%;
  background: #fff;
  position: sticky; top: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  z-index: 1;
}
header.active{
  box-shadow: 0 .1rem .7rem rgba(0, 0, 0, .3);
}
header .menu{
  display: flex;
  align-items: center;
}
/* 네비게이션 */
header .navbar ul{
  display: flex;
  align-items: center;
}
header .navbar ul li{
  margin: 0 1rem;
}
header .navbar ul li a{
  font-size: 1.5rem;
  transition: .2s;
}
header .navbar ul li .active,
header .navbar ul li a:hover{
  color: var(--primary-color);
}
/* 모드 */
header .mode{
  font-size: 2.5rem;
  cursor: pointer;
  transition: 1s;
}
header .mode i:not(.active){
  display: none;
}
/* 로고 */
header .logo{
  display: flex;
  align-items: center;
  font-size: 1.5rem;
  transition: .2s;
}
header .logo i{
  font-size: 3rem;
  margin-right: .5rem;
}
header .logo:hover{
  color: var(--primary-color);
  letter-spacing: .1rem;
}

완성된 header.css 는 위와 같다. 

 

푸터 디자인하기

푸터

<!-- 푸터 -->
<section class="footer">
    <div class="icons">
      <a href="https://ko-kr.facebook.com/" class="fab fa-facebook-f"></a>
      <a href="https://twitter.com/?lang=ko" class="fab fa-twitter"></a>
      <a href="https://www.instagram.com/" class="fab fa-instagram"></a>
      <a href="https://github.com/" class="fab fa-github"></a>
      <a href="https://www.pinterest.co.kr/" class="fab fa-pinterest"></a>
      <div class="scroll-up"><i class="fa-solid fa-circle-up"></i></div>
    </div>
    <div class="credit"><i class="fa-brands fa-blogger"></i>Sunrise. All rights reserved </div>
</section>

index.html 파일에 위 코드를 추가한다. 푸터는 아이콘/크레딧 영역으로 구분된다. 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sunrise 블로그</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
  <link rel="stylesheet" href="css/landing.css">
</head>
<body>
  <!-- 헤더 -->
  <header>
    <a href="#" class="logo"><i class="fa-brands fa-blogger"></i>Sunblog</a>
    
    <div class="menu">
      <nav class="navbar">
        <ul>
          <li><a href="#about" class="active">서비스</a></li>
          <li><a href="#story">스토리</a></li>
          <li><a href="#contact">연락처</a></li>
        </ul>
      </nav>
      
      <div class="mode">
        <i class="fa-solid fa-toggle-on"></i>
        <i class="fa-solid fa-toggle-off active"></i>
      </div>
    </div>
  </header>

  <!-- 푸터 -->
  <section class="footer">
    <div class="icons">
      <a href="https://ko-kr.facebook.com/" class="fab fa-facebook-f"></a>
      <a href="https://twitter.com/?lang=ko" class="fab fa-twitter"></a>
      <a href="https://www.instagram.com/" class="fab fa-instagram"></a>
      <a href="https://github.com/" class="fab fa-github"></a>
      <a href="https://www.pinterest.co.kr/" class="fab fa-pinterest"></a>
      <div class="scroll-up"><i class="fa-solid fa-circle-up"></i></div>
    </div>
    <div class="credit"><i class="fa-brands fa-blogger"></i>Sunrise. All rights reserved </div>
  </section>
  
  <script src="js/scroll.js"></script>
  <script src="js/landing.js"></script>
</body>
</html>

푸터가 추가된 index.html 은 위와 같다. 

/* 푸터 */
.footer{
  width: 100vw; /* section width 는 45vw 이지만 css 우선순위 때문에 푸터에는 100vw 가 적용됨 */
  background: var(--primary-color);
}

css 폴더에 footer.css 파일을 생성하고 위와 같이 작성한다. 푸터의 너비와 색상을 설정한다.

/* 위쪽 화살표 */
.footer .icons{
  padding: 3rem 0;
  display: flex;
}
.footer .icons .scroll-up{
  margin-left: auto;
  margin-right: 2rem;
  cursor: pointer;
}
.footer .icons .scroll-up i{
  width: 3rem;
  height: 3rem;
  line-height: 3rem;
  text-align: center;
  font-size: 2rem;
  transition: .2s;
}
.footer .icons .scroll-up i:hover{
  color: #fff;
  transform: scale(1.2);
}

위쪽 화살표에 margin-left: auto 를 설정해서 푸터의 우측에 정렬한다.

/* SNS 아이콘 */
.footer .icons a{
  width: 3rem;
  height: 3rem;
  line-height: 3rem;
  text-align: center;
  border-radius: 50%;
  font-size: 1.5rem;
  margin: 0 .2rem;
  transition: .2s linear;
}
.footer .icons a:hover{
  color: #fff;
  transform: scale(1.5);
}

SNS 아이콘에 마우스를 올리면 아이콘이 커지도록 디자인한다.

/* 크레딧 */
.footer .credit{
  font-size: 1rem;
  width: 100%;
  padding-bottom: 1rem;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
}
.footer .credit i{
  width: 1.5rem;
  height: 1.5rem;
  font-size: 1.5rem;
  margin-right: .5rem;
}

저작권 정보를 화면에 보여준다. 

/* 푸터 */
.footer{
  width: 100vw; /* section width 는 45vw 이지만 css 우선순위 때문에 푸터에는 100vw 가 적용됨 */
  background: var(--primary-color);
}
/* 위쪽 화살표 */
.footer .icons{
  padding: 3rem 0;
  display: flex;
}
.footer .icons .scroll-up{
  margin-left: auto;
  margin-right: 2rem;
  cursor: pointer;
}
.footer .icons .scroll-up i{
  width: 3rem;
  height: 3rem;
  line-height: 3rem;
  text-align: center;
  font-size: 2rem;
  transition: .2s;
}
.footer .icons .scroll-up i:hover{
  color: #fff;
  transform: scale(1.2);
}
/* SNS 아이콘 */
.footer .icons a{
  width: 3rem;
  height: 3rem;
  line-height: 3rem;
  text-align: center;
  border-radius: 50%;
  font-size: 1.5rem;
  margin: 0 .2rem;
  transition: .2s linear;
}
.footer .icons a:hover{
  color: #fff;
  transform: scale(1.5);
}
/* 크레딧 */
.footer .credit{
  font-size: 1rem;
  width: 100%;
  padding-bottom: 1rem;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
}
.footer .credit i{
  width: 1.5rem;
  height: 1.5rem;
  font-size: 1.5rem;
  margin-right: .5rem;
}

완성된 footer.css 는 위와 같다. 

 

서비스 섹션 디자인하기

서비스 섹션

 

<!-- 서비스 -->
  <section class="about" id="about">
    <div class="image">
      <img src="imgs/about.jpg" alt="about">
    </div>
    <div class="content">
      <h3>나만의 스토리를 <br/> 만들어보세요 </h3>
      <p>Sunblog는 자신의 이야기나 친구들과 공유할 이야기 등 여러분들의 창의성을 마음껏 발휘해볼 수 있도록 많은 서비스를 제공해드립니다. 
        저희 서비스를 이용하여 일상에서 일어나는 일들을 스토리로 기록해보세요!</p>
      <a href="html/home.html"><button>글쓰기</button></a>
    </div>
  </section>

index.html 파일에 위 코드를 추가한다. 크게 이미지/텍스트 영역으로 구분된다. 텍스트 영역에는 타이틀/설명/버튼으로 구분한다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sunrise 블로그</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
  <link rel="stylesheet" href="css/landing.css">
</head>
<body>
  <!-- 헤더 -->
  <header>
    <a href="#" class="logo"><i class="fa-brands fa-blogger"></i>Sunblog</a>
    
    <div class="menu">
      <nav class="navbar">
        <ul>
          <li><a href="#about" class="active">서비스</a></li>
          <li><a href="#story">스토리</a></li>
          <li><a href="#contact">연락처</a></li>
        </ul>
      </nav>
      
      <div class="mode">
        <i class="fa-solid fa-toggle-on"></i>
        <i class="fa-solid fa-toggle-off active"></i>
      </div>
    </div>
  </header>

  <!-- 서비스 -->
  <section class="about" id="about">
    <div class="image">
      <img src="imgs/about.jpg" alt="about">
    </div>
    <div class="content">
      <h3>나만의 스토리를 <br/> 만들어보세요 </h3>
      <p>Sunblog는 자신의 이야기나 친구들과 공유할 이야기 등 여러분들의 창의성을 마음껏 발휘해볼 수 있도록 많은 서비스를 제공해드립니다. 
        저희 서비스를 이용하여 일상에서 일어나는 일들을 스토리로 기록해보세요!</p>
      <a href="html/home.html"><button>글쓰기</button></a>
    </div>
  </section>

  

  <!-- 푸터 -->
  <section class="footer">
    <div class="icons">
      <a href="https://ko-kr.facebook.com/" class="fab fa-facebook-f"></a>
      <a href="https://twitter.com/?lang=ko" class="fab fa-twitter"></a>
      <a href="https://www.instagram.com/" class="fab fa-instagram"></a>
      <a href="https://github.com/" class="fab fa-github"></a>
      <a href="https://www.pinterest.co.kr/" class="fab fa-pinterest"></a>
      <div class="scroll-up"><i class="fa-solid fa-circle-up"></i></div>
    </div>
    <div class="credit"><i class="fa-brands fa-blogger"></i>Sunrise. All rights reserved </div>
  </section>
  
  <script src="js/scroll.js"></script>
  <script src="js/landing.js"></script>
</body>
</html>

 서비스 섹션이 추가된 index.html 파일은 위와 같다. 

/* 서비스, 스토리, 연락처 */
section:not(.footer){
  width: 50vw;  
  height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  min-height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  max-height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  margin: 0 auto;
  position: relative;
}

landing.css 파일에 위 코드를 추가한다. 푸터를 제외한 서비스/스토리/연락처 섹션의 공통적인 스타일을 정의한다. 너비는 브라우저의 절반으로 하고, 높이는 브라우저에서 헤더높이만큼 제외한 크기로 설정한다. 

/* 이미지 */
.image img{
  height: 50vh;
  width: 100%;
}

이미지의 너비는 브라우저 절반으로 하고, 높이는 섹션의 절반으로 설정한다. 나머지 절반의 공간에 텍스트가 위치한다.

/* 텍스트 */
.content{
  padding: 3rem 2rem;
}
.content h3{
  width: 50%;
  font-size: 2rem;
  color: var(--primary-color);
  transition: .7s ease-out;
}
.content p{
  font-size: 1.2rem;
  padding: 1rem 0;
  width: 50%;
  margin-left: auto;
  line-height: 2rem;
  transition: .7s ease-out;
}

텍스트 애니메이션을 적용할 예정이므로 transition 속성을 설정한다. 설명 부분은 margin-left: auto 를 설정해서 우측에 정렬한다.

/* 버튼 */
.content button{
  width: 12rem;
  height: 3.5rem;
  background: var(--primary-color);
  color: #fff;
  border: none;
  border-radius: 2rem;
  cursor: pointer;
  font-size: 1.5rem;
  transition: .2s;
  margin-top: 2rem;
  position: absolute;
  bottom: 2rem;
}
.content button:hover{
  background: var(--secondary-color);
  letter-spacing: .2rem;
}

버튼은 position: absolute 와 bottom: 2rem 으로 설정해서 섹션 하단에 고정되도록 한다.

@import url('reset.css');
@import url('global.css');
@import url('header.css');
@import url('footer.css');
@import url('animation.css');

/* 서비스, 스토리, 연락처 */
section:not(.footer){
  width: 50vw;  
  height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  min-height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  max-height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  margin: 0 auto;
  position: relative;
}
/* 이미지 */
.image img{
  height: 50vh;
  width: 100%;
}
/* 텍스트 */
.content{
  padding: 3rem 2rem;
}
.content h3{
  width: 50%;
  font-size: 2rem;
  color: var(--primary-color);
  transition: .7s ease-out;
}
.content p{
  font-size: 1.2rem;
  padding: 1rem 0;
  width: 50%;
  margin-left: auto;
  line-height: 2rem;
  transition: .7s ease-out;
}
/* 버튼 */
.content button{
  width: 12rem;
  height: 3.5rem;
  background: var(--primary-color);
  color: #fff;
  border: none;
  border-radius: 2rem;
  cursor: pointer;
  font-size: 1.5rem;
  transition: .2s;
  margin-top: 2rem;
  position: absolute;
  bottom: 2rem;
}
.content button:hover{
  background: var(--secondary-color);
  letter-spacing: .2rem;
}

현재까지의 landing.css 파일은 위와 같다. 

 

스토리 섹션 디자인하기

스토리 섹션

<!-- 스토리 -->
  <section class="story" id="story">
    <div class="image">
      <img src="imgs/story.jpg" alt="story">
    </div>
    <div class="content">
      <h3 class="right">다른 사람들의 스토리가 <br/> 궁금하신가요? </h3>
      <p class="down">Sunblog는 다양한 사람들의 흥미진진한 이야기들을 담고 있습니다. 여러분의 삶에서 일어나는 기상천외한 이야기를 읽어보실 준비가 되셨나요?</p>
      <a href="html/story.html"><button>스토리 보기</button></a>
    </div>
  </section>

index.html 파일에 위 코드를 추가한다. 텍스트 애니메이션을 적용하기 위하여 right, down 클래스를 추가한다. 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sunrise 블로그</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
  <link rel="stylesheet" href="css/landing.css">
</head>
<body>
  <!-- 헤더 -->
  <header>
    <a href="#" class="logo"><i class="fa-brands fa-blogger"></i>Sunblog</a>
    
    <div class="menu">
      <nav class="navbar">
        <ul>
          <li><a href="#about" class="active">서비스</a></li>
          <li><a href="#story">스토리</a></li>
          <li><a href="#contact">연락처</a></li>
        </ul>
      </nav>
      
      <div class="mode">
        <i class="fa-solid fa-toggle-on"></i>
        <i class="fa-solid fa-toggle-off active"></i>
      </div>
    </div>
  </header>

  <!-- 서비스 -->
  <section class="about" id="about">
    <div class="image">
      <img src="imgs/about.jpg" alt="about">
    </div>
    <div class="content">
      <h3>나만의 스토리를 <br/> 만들어보세요 </h3>
      <p>Sunblog는 자신의 이야기나 친구들과 공유할 이야기 등 여러분들의 창의성을 마음껏 발휘해볼 수 있도록 많은 서비스를 제공해드립니다. 
        저희 서비스를 이용하여 일상에서 일어나는 일들을 스토리로 기록해보세요!</p>
      <a href="html/home.html"><button>글쓰기</button></a>
    </div>
  </section>

  <!-- 스토리 -->
  <section class="story" id="story">
    <div class="image">
      <img src="imgs/story.jpg" alt="story">
    </div>
    <div class="content">
      <h3 class="right">다른 사람들의 스토리가 <br/> 궁금하신가요? </h3>
      <p class="down">Sunblog는 다양한 사람들의 흥미진진한 이야기들을 담고 있습니다. 여러분의 삶에서 일어나는 기상천외한 이야기를 읽어보실 준비가 되셨나요?</p>
      <a href="html/story.html"><button>스토리 보기</button></a>
    </div>
  </section>

  <!-- 푸터 -->
  <section class="footer">
    <div class="icons">
      <a href="https://ko-kr.facebook.com/" class="fab fa-facebook-f"></a>
      <a href="https://twitter.com/?lang=ko" class="fab fa-twitter"></a>
      <a href="https://www.instagram.com/" class="fab fa-instagram"></a>
      <a href="https://github.com/" class="fab fa-github"></a>
      <a href="https://www.pinterest.co.kr/" class="fab fa-pinterest"></a>
      <div class="scroll-up"><i class="fa-solid fa-circle-up"></i></div>
    </div>
    <div class="credit"><i class="fa-brands fa-blogger"></i>Sunrise. All rights reserved </div>
  </section>
  
  <script src="js/scroll.js"></script>
  <script src="js/landing.js"></script>
</body>
</html>

스토리 섹션이 추가된 index.html 파일은 위와 같다. 디자인은 서비스 섹션과 동일하므로 따로 구현하지 않아도 된다. 

 

연락처 섹션 디자인하기

연락처 섹션

<!-- 연락처 -->
  <section class="contact" id="contact">
    <div class="image">
      <img src="imgs/contact.jpg" alt="contact">
    </div>
    <div class="content">
      <h3 class="right">서비스에 대해 <br/> 하고싶은 말이 있나요? </h3>
      <p class="down">Sunblog는 여러분의 생각과 제안을 기다립니다. 아이디어가 생각나시거나 궁금한 사항이 있으시면 언제든지 저희에게 연락주세요.</p>
      <a href="#"><button>연락하기</button></a>
    </div>
  </section>

index.html 파일에 위 코드를 추가한다. 텍스트 애니메이션을 적용하기 위하여 right, down 클래스를 추가한다. 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sunrise 블로그</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
  <link rel="stylesheet" href="css/landing.css">
</head>
<body>
  <!-- 헤더 -->
  <header>
    <a href="#" class="logo"><i class="fa-brands fa-blogger"></i>Sunblog</a>
    
    <div class="menu">
      <nav class="navbar">
        <ul>
          <li><a href="#about" class="active">서비스</a></li>
          <li><a href="#story">스토리</a></li>
          <li><a href="#contact">연락처</a></li>
        </ul>
      </nav>
      
      <div class="mode">
        <i class="fa-solid fa-toggle-on"></i>
        <i class="fa-solid fa-toggle-off active"></i>
      </div>
    </div>
  </header>

  <!-- 서비스 -->
  <section class="about" id="about">
    <div class="image">
      <img src="imgs/about.jpg" alt="about">
    </div>
    <div class="content">
      <h3>나만의 스토리를 <br/> 만들어보세요 </h3>
      <p>Sunblog는 자신의 이야기나 친구들과 공유할 이야기 등 여러분들의 창의성을 마음껏 발휘해볼 수 있도록 많은 서비스를 제공해드립니다. 
        저희 서비스를 이용하여 일상에서 일어나는 일들을 스토리로 기록해보세요!</p>
      <a href="html/home.html"><button>글쓰기</button></a>
    </div>
  </section>

  <!-- 스토리 -->
  <section class="story" id="story">
    <div class="image">
      <img src="imgs/story.jpg" alt="story">
    </div>
    <div class="content">
      <h3 class="right">다른 사람들의 스토리가 <br/> 궁금하신가요? </h3>
      <p class="down">Sunblog는 다양한 사람들의 흥미진진한 이야기들을 담고 있습니다. 여러분의 삶에서 일어나는 기상천외한 이야기를 읽어보실 준비가 되셨나요?</p>
      <a href="html/story.html"><button>스토리 보기</button></a>
    </div>
  </section>

  <!-- 연락처 -->
  <section class="contact" id="contact">
    <div class="image">
      <img src="imgs/contact.jpg" alt="contact">
    </div>
    <div class="content">
      <h3 class="right">서비스에 대해 <br/> 하고싶은 말이 있나요? </h3>
      <p class="down">Sunblog는 여러분의 생각과 제안을 기다립니다. 아이디어가 생각나시거나 궁금한 사항이 있으시면 언제든지 저희에게 연락주세요.</p>
      <a href="#"><button>연락하기</button></a>
    </div>
  </section>

  <!-- 푸터 -->
  <section class="footer">
    <div class="icons">
      <a href="https://ko-kr.facebook.com/" class="fab fa-facebook-f"></a>
      <a href="https://twitter.com/?lang=ko" class="fab fa-twitter"></a>
      <a href="https://www.instagram.com/" class="fab fa-instagram"></a>
      <a href="https://github.com/" class="fab fa-github"></a>
      <a href="https://www.pinterest.co.kr/" class="fab fa-pinterest"></a>
      <div class="scroll-up"><i class="fa-solid fa-circle-up"></i></div>
    </div>
    <div class="credit"><i class="fa-brands fa-blogger"></i>Sunrise. All rights reserved </div>
  </section>
  
  <script src="js/scroll.js"></script>
  <script src="js/landing.js"></script>
</body>
</html>

연락처 섹션이 추가된 index.html 파일은 위와 같다. 디자인은 서비스 섹션과 동일하므로 따로 구현하지 않아도 된다. 

 

반응형 웹 적용하기

반응형 웹

landing.css 파일에 아래 코드를 추가한다. 

@media screen and (max-width: 1300px){
  .content {
    padding: 1rem 1rem;
  }
  .content h3{
    font-size: 1.5rem;
  }
  .content p{
    font-size: 1rem;
    line-height: 1.4rem;
    padding: .5rem 0;
  }
  .content button{
    width: 9rem;
    height: 2.5rem;
    font-size: 1.2rem;
    bottom: 1rem;
  }
}

디바이스 너비가 1300px 이하인 경우 반응형웹을 적용하는 코드이다. 폰트크기와 버튼크기를 조정하였다. 

@media (max-width: 1000px){
  section:not(.footer){ /* 이미지와 텍스트 너비 조정 */
    width: 90vw;  
  }
  .content{ /* 이미지와 텍스트 세로방향 나열 */
    display: flex;
    flex-flow: column;
  }
  .content h3, .content p{
    width: 100%;
    text-align: center;
  }
  .content h3{
    font-size: 4rem;
  }
  .content p{
    font-size: 2.5rem;
    line-height: 3rem;
  }
  .content button{ /* 모바일 환경에 맞춘 버튼 너비 조정 */
    width: 100%;
    height: 5rem;
    border-radius: 3rem;
    font-size: 2.5rem;
    left: 50%;
    transform: translateX(-50%);
    bottom: 1.5rem;
  }
}

디바이스 너비가 1000px 이하이면 섹션의 너비를 조정해서 이미지와 텍스트가 화면에서 찌그러지지 않고 잘 보이도록 한다. 또한, 타이틀/설명은 flex-flow: column 을 적용하여 세로방향으로 나열되도록 한다. 버튼 너비도 모바일 환경에 맞게 화면너비에 꽉 차도록 조정한다. 

@media (max-width: 830px){ /* 텍스트 폰트 크기 조정 */
  .content h3{
    font-size: 3rem;
  }
  .content p{
    font-size: 2rem;
    line-height: 2rem;
  }
}

디바이스 너비가 830px 이하이면 텍스트의 폰트 크기를 조정한다.

@media (max-width: 550px){
  header .navbar ul{ /* 네비게이션 메뉴 화면에서 가리기 */
    display: none;
  }
  .content h3{ /* 텍스트 폰트 크기 조정 */
    font-size: 1.5rem;
  }
  .content p{
    font-size: 1.1rem;
    line-height: 1.5rem;
  }
  .content button{ /* 버튼 크기 조정 */
    height: 3rem;
    font-size: 1.2rem;
  }
  .image img{ /* 이미지 높이 조정 */
    height: 40vh;
  }
}

디바이스 너비가 550px 이하이면 네비게이션 메뉴를 화면에서 가린다. 또한, 텍스트와 버튼 크기를 조정하고, 이미지의 높이도 조정한다. 

@media (max-width: 420px){ /* 로고, 텍스트 폰트 크기 조정 */
  header .logo{
    font-size: 1.2rem;
  }
  .content{
    padding: 2rem 1rem;
    display: flex;
    flex-flow: column;
  }
  .content h3{
    font-size: 1.7rem;
  }
  .content p{
    font-size: 1.2rem;
    line-height: 1.5rem;
  }
  .content button{
    bottom: 2rem;
  }
}

디바이스 너비가 420px 이하이면 로고 폰트는 줄이고, 텍스트 폰트는 키운다. 

@media (max-width: 380px){ /* 타이틀 폰트 크기 조정 */
  .content{
    padding: 1rem 1rem;
  }
  .content h3{
    font-size: 1.7rem;
  }
  .content button{
    bottom: 1rem;
  }
}

디바이스 너비가 380px 이하이면 타이틀 폰트 크기만 변경한다.

@media (max-width: 300px){ /* 텍스트 폰트 크기 조정 */
  .content h3{
    font-size: 1.2rem;
  }
  .content p{
    font-size: .8rem;
    line-height: 1.5rem;
  }
}

디바이스 너비가 300px 이하이면 텍스트 폰트 크기를 줄인다. 

@import url('reset.css');
@import url('global.css');
@import url('header.css');
@import url('footer.css');
@import url('animation.css');

/* 서비스, 스토리, 연락처 */
section:not(.footer){
  width: 50vw;  
  height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  min-height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  max-height: calc(100vh - var(--header-height)); /* 헤더높이만큼 제외함 */
  margin: 0 auto;
  position: relative;
}
/* 이미지 */
.image img{
  height: 50vh;
  width: 100%;
}
/* 텍스트 */
.content{
  padding: 3rem 2rem;
}
.content h3{
  width: 50%;
  font-size: 2rem;
  color: var(--primary-color);
  transition: .7s ease-out;
}
.content p{
  font-size: 1.2rem;
  padding: 1rem 0;
  width: 50%;
  margin-left: auto;
  line-height: 2rem;
  transition: .7s ease-out;
}
/* 버튼 */
.content button{
  width: 12rem;
  height: 3.5rem;
  background: var(--primary-color);
  color: #fff;
  border: none;
  border-radius: 2rem;
  cursor: pointer;
  font-size: 1.5rem;
  transition: .2s;
  margin-top: 2rem;
  position: absolute;
  bottom: 2rem;
}
.content button:hover{
  background: var(--secondary-color);
  letter-spacing: .2rem;
}

@media (max-width: 1000px){
  section:not(.footer){ /* 이미지와 텍스트 너비 조정 */
    width: 90vw;  
  }
  .content{ /* 이미지와 텍스트 세로방향 나열 */
    display: flex;
    flex-flow: column;
  }
  .content h3, .content p{
    width: 100%;
    text-align: center;
  }
  .content h3{
    font-size: 4rem;
  }
  .content p{
    font-size: 2.5rem;
    line-height: 3rem;
  }
  .content button{ /* 모바일 환경에 맞춘 버튼 너비 조정 */
    width: 100%;
    height: 5rem;
    border-radius: 3rem;
    font-size: 2.5rem;
    left: 50%;
    transform: translateX(-50%);
    bottom: 1.5rem;
  }
}
@media (max-width: 830px){ /* 텍스트 폰트 크기 조정 */
  .content h3{
    font-size: 3rem;
  }
  .content p{
    font-size: 2rem;
    line-height: 2rem;
  }
}
@media (max-width: 550px){
  header .navbar ul{ /* 네비게이션 메뉴 화면에서 가리기 */
    display: none;
  }
  .content h3{ /* 텍스트 폰트 크기 조정 */
    font-size: 1.5rem;
  }
  .content p{
    font-size: 1.1rem;
    line-height: 1.5rem;
  }
  .content button{ /* 버튼 크기 조정 */
    height: 3rem;
    font-size: 1.2rem;
  }
  .image img{ /* 이미지 높이 조정 */
    height: 40vh;
  }
}
@media (max-width: 420px){ /* 로고, 텍스트 폰트 크기 조정 */
  header .logo{
    font-size: 1.2rem;
  }
  .content{
    padding: 2rem 1rem;
    display: flex;
    flex-flow: column;
  }
  .content h3{
    font-size: 1.7rem;
  }
  .content p{
    font-size: 1.2rem;
    line-height: 1.5rem;
  }
  .content button{
    bottom: 2rem;
  }
}
@media (max-width: 380px){ /* 타이틀 폰트 크기 조정 */
  .content{
    padding: 1rem 1rem;
  }
  .content h3{
    font-size: 1.7rem;
  }
  .content button{
    bottom: 1rem;
  }
}
@media (max-width: 300px){ /* 텍스트 폰트 크기 조정 */
  .content h3{
    font-size: 1.2rem;
  }
  .content p{
    font-size: .8rem;
    line-height: 1.5rem;
  }
}

완성된 landing.css 파일은 위와 같다. 

 

텍스트 애니메이션 디자인하기

/* 애니메이션 효과 */
.up{
  /* y 좌표를 아래쪽으로 100px 만큼 내린다 */
  transform: translate(0, 3rem); 
  opacity: 0;
}
.down{
  /* y 좌표를 위쪽으로 100px 만큼 올린다 */
  transform: translate(0, -3rem);
  opacity: 0;
}
/* x 좌표를 오른쪽으로 100px 만큼 이동한다 */
.left {
  transform: translate(3rem, 0);
  opacity: 0;
}
/* x 좌표를 왼쪽으로 100px 만큼 이동한다 */
.right {
  transform: translate(-3rem, 0);
  opacity: 0;
}
.show{
  opacity: 1;
  transform: none;
}

css 폴더에 animation.css 파일을 생성하고 위와 같이 작성한다. right, down 클래스가 있으면 스토리/연락처 섹션의 텍스트는 화면에 보이지 않는다. 스크롤되면 show 클래스가 추가되면서 텍스트가 페이드인(fade in)된다. 개발자 도구의 Elements 탭에서 텍스트에 show 클래스를 추가해서 애니메이션을 테스트해보자!

 

페이지 모드(다크/일반) 기능 구현하기

다크모드 적용

window.addEventListener("load", (event) => {
  // 테마변경 (다크모드/ 일반모드)
  const mode = document.querySelector('.mode')
  const icons = mode.querySelectorAll('.fa-solid')
  const header = document.querySelector('header')

  mode.addEventListener('click', (event) => {
    document.body.classList.toggle('dark')
    header.classList.toggle('dark')
    
    for(const icon of icons){
      icon.classList.contains('active') ? 
        icon.classList.remove('active') 
        : icon.classList.add('active')
    }
  })
})

js 폴더에 landing.js 파일을 생성하고 위와 같이 작성한다. 

 

스크롤 라이브러리 만들기

js 폴더에 scroll.js 파일을 생성하고 아래와 같이 작성한다. class 문법을 이용하여 Scroller 클래스를 정의한다. 샵(#) 이 붙은 멤버변수와 메서드는 private 이다. 즉, 외부에서 사용할 수 없다.

class Scroller{
  #isScrolling    // 스크롤 상태 
  #scrollEndTimer // 스크롤 타이머 

  constructor(isScrolling){
    this.#isScrolling = isScrolling
    this.#scrollEndTimer = null 
  }
  getScrollPosition(){  // 현재 스크롤 위치 조회
    return window.pageYOffset
  }
  setScrollPosition(position){ // 해당 위치로 스크롤링
    window.scrollTo(position);
    this.#setScrollState(true)
  }
  getScrollState(){ // 스크롤 상태 조회 
    return this.#isScrolling
  }
  #setScrollState(state){ // 스크롤 상태 변경 
    this.#isScrolling = state 
  }
  isScrollended(){ // 스크롤이 끝났음을 감지
    return new Promise((resolve, reject) => {
      clearTimeout(this.#scrollEndTimer)
      this.#scrollEndTimer = setTimeout(() => {
        this.#setScrollState(false)
        resolve()
      }, 100)
    })
  }
}

isScrolling 은 스크롤 상태를 나타낸다. isScrolling 이 true 이면 스크롤 중이고, false 이면 스크롤이 끝났거나 스크롤이 멈춰있는 상태이다. window.pageYOffset  은 현재 스크롤 위치이다. window.scrollTo(position) 는 position 위치로 스크롤링된다.

isScrollended(){ // 스크롤이 끝났음을 감지
    return new Promise((resolve, reject) => {
      clearTimeout(this.#scrollEndTimer)
      this.#scrollEndTimer = setTimeout(() => {
        this.#setScrollState(false)
        resolve()
      }, 100)
    })
  }

isScrollended 는 스크롤이 끝났음을 감지한다. isScrollended 는 스크롤링중이면 계속 실행되면서 clearTimeout 에 의하여 타이머를 해제한다. 하지만 스크롤이 끝나게 되면 더이상 clearTimeout 에 의하여 타이머가 중지되지 않고, 100ms 후에 비동기로 설정된 타이머가 동작하면서 스크롤 상태를 false 로 변경하여 스크롤이 끝났음을 알려준다. 

 

브라우저 상단으로 스크롤하기

// 브라우저 상단으로 스크롤하기
const scroller = new Scroller(false) // 스크롤 객체 생성 

const arrowUp = document.querySelector('.footer .icons .scroll-up') // 위쪽화살표 클릭
    arrowUp.addEventListener('click', (event) => {
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'})
})

const logo = document.querySelector('header .logo') // 로고 클릭 
logo.addEventListener('click', (event) => {
    event.preventDefault() // 부드러운 스크롤링
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'}) 
})

landing.js 파일에 위 코드를 추가한다. 헤더의 로고와 푸터의 위쪽화살표를 클릭하면 브라우저 상단으로 스크롤된다. a 태그의 내부링크(#)로 각 섹션으로 이동하면 URL 주소에 #story #contact 과 같이 추가된다. 이때 history.pushState() 를 사용하면 브라우저 상단으로 스크롤되면서 URL 주소는 다시 초기화된다. event.preventDefault() 를 사용하지 않으면 a 태그의 기본동작이 실행되면서 부드럽게 스크롤되지 않는다. 

const scroller = new Scroller(false) // 스크롤 객체 생성 

window.addEventListener("load", (event) => {
  // 테마변경 (다크모드/ 일반모드)
  const mode = document.querySelector('.mode')
  const icons = mode.querySelectorAll('.fa-solid')
  const header = document.querySelector('header')

  mode.addEventListener('click', (event) => {
    document.body.classList.toggle('dark')
    header.classList.toggle('dark')
    
    for(const icon of icons){
      icon.classList.contains('active') ? 
        icon.classList.remove('active') 
        : icon.classList.add('active')
    }
  })

  // 브라우저 상단으로 스크롤하기
  const arrowUp = document.querySelector('.footer .icons .scroll-up') // 위쪽 화살표 클릭 
  arrowUp.addEventListener('click', (event) => {
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'})
  })

  const logo = document.querySelector('header .logo') // 로고 클릭 
  logo.addEventListener('click', (event) => {
    event.preventDefault() // 부드러운 스크롤링
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'}) 
  })
})

현재까지의 landing.js 파일은 위와 같다. 

 

네비게이션 메뉴 클릭시 해당 섹션으로 스크롤하기 

https://developer.mozilla.org/ko/docs/Web/API/Window/scrollTo

 

Window.scrollTo() - Web API | MDN

문서의 지정된 위치로 스크롤합니다.

developer.mozilla.org

https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect

 

Element: getBoundingClientRect() method - Web APIs | MDN

The Element.getBoundingClientRect() method returns a DOMRect object providing information about the size of an element and its position relative to the viewport.

developer.mozilla.org

https://developer.mozilla.org/ko/docs/Web/API/Window/pageYOffset

 

Window.pageYOffset - Web API | MDN

Window 인터페이스의 pageYOffset 읽기 전용 속성은 scrollY의 다른 이름으로, 문서가 수직으로 얼마나 스크롤됐는지 픽셀 단위로 반환합니다.

developer.mozilla.org

 

landing.js 파일에 아래 코드를 추가한다. 네비게이션 메뉴에 클릭 이벤트를 등록한다. !scroller.getScrollState() 이라는 조건을 설정하여 현재 스크롤링이 멈춘 경우에만 해당 섹션으로 스크롤링 되도록 한다.

// 네비게이션 메뉴 클릭시 해당 섹션으로 스크롤하기
  const sections = document.querySelectorAll('section:not(.footer)') // 푸터를 제외한 section 엘리먼트 조회
  const nav = document.querySelector('.navbar ul')

  nav.querySelectorAll('li a').forEach(anchor => {
    anchor.addEventListener('click', function (event) {
      const section = this.getAttribute('href') // 네비게이션 메뉴에서 클릭한 섹션 
      const offsetToElementFromViewport = document.querySelector(section).getBoundingClientRect().top // 브라우저 상단에서 엘리먼트까지의 거리
      
      if(!scroller.getScrollState()){ // 스크롤링이 멈춘 경우만 해당 섹션으로 스크롤링
        event.preventDefault() // 부드러운 스크롤링 
        history.pushState({}, "", `${section}`);  // URL 주소에 #about, #story, #contact 과 같은 파라미터 추가해서 URL 변경하기

        const offsetToElementFromDocument = offsetToElementFromViewport + scroller.getScrollPosition() // 문서 상단에서 엘리먼트까지의 거리 
        scroller.setScrollPosition({
          top: offsetToElementFromDocument - header.offsetHeight - 10, // 헤더높이에서 10px 아래 위치로 스크롤링 
          behavior: "smooth",
        })
      }
    });
  });

scroller.setScrollPosition() 이 실행되면 window.scrollTo() 가 동작하면서 문서 상단으로부터 top 에 설정된 y 좌표로 스크롤한다. 문서 상단을 기준으로 해당 섹션의 y 좌표를 구하려면 (문서 상단 ~ 브라우저 상단) + (브라우저 상단 ~ 엘리먼트 상단) 으로 계산한다. (문서 상단 ~ 브라우저 상단) 은 스크롤 위치와 동일하다. 즉, window.pageYOffest 이다. (브라우저 상단 ~ 엘리먼트 상단) 은 해당 섹션의 getBoundingClientRect().top 으로 구할수 있다. 하지만 해당 섹션은 브라우저 상단이 아니라 헤더의 하단까지만 스크롤되어야 하므로 top 값을 헤더높이만큼 빼서 스크롤이 조금 덜 되도록 해야 한다. 

const scroller = new Scroller(false) // 스크롤 객체 생성 

window.addEventListener("load", (event) => {
  // 테마변경 (다크모드/ 일반모드)
  const mode = document.querySelector('.mode')
  const icons = mode.querySelectorAll('.fa-solid')
  const header = document.querySelector('header')

  mode.addEventListener('click', (event) => {
    document.body.classList.toggle('dark')
    header.classList.toggle('dark')
    
    for(const icon of icons){
      icon.classList.contains('active') ? 
        icon.classList.remove('active') 
        : icon.classList.add('active')
    }
  })

  // 브라우저 상단으로 스크롤하기
  const arrowUp = document.querySelector('.footer .icons .scroll-up') // 위쪽 화살표 클릭 
  arrowUp.addEventListener('click', (event) => {
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'})
  })

  const logo = document.querySelector('header .logo') // 로고 클릭 
  logo.addEventListener('click', (event) => {
    event.preventDefault() // 부드러운 스크롤링
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'}) 
  })

  // 네비게이션 메뉴 클릭시 해당 섹션으로 스크롤하기
  const sections = document.querySelectorAll('section:not(.footer)') // 푸터를 제외한 section 엘리먼트 조회
  const nav = document.querySelector('.navbar ul')

  nav.querySelectorAll('li a').forEach(anchor => {
    anchor.addEventListener('click', function (event) {
      const section = this.getAttribute('href') // 네비게이션 메뉴에서 클릭한 섹션 
      const offsetToElementFromViewport = document.querySelector(section).getBoundingClientRect().top // 브라우저 상단에서 엘리먼트까지의 거리

      if(!scroller.getScrollState()){ // 스크롤링이 멈춘 경우만 해당 섹션으로 스크롤링
        event.preventDefault() // 부드러운 스크롤링 
        history.pushState({}, "", `${section}`);  // URL 주소에 #about, #story, #contact 과 같은 파라미터 추가해서 URL 변경하기

        const offsetToElementFromDocument = offsetToElementFromViewport + scroller.getScrollPosition() // 문서 상단에서 엘리먼트까지의 거리 
        scroller.setScrollPosition({
          top: offsetToElementFromDocument - header.offsetHeight - 10, // 헤더높이에서 10px 아래 위치로 스크롤링 
          behavior: "smooth",
        })
      }
    });
  });
})

현재까지의 landing.js 파일은 위와 같다. 

 

스크롤이 끝났음을 감지하기

네비게이션 메뉴에서 특정 섹션을 선택하면 한번은 부드럽게 스크롤링되지만 그 다음부터는 부드럽게 스크롤되지 않는다. 이유는 setScrollPosition() 이 실행되면서 isScrolling 이 true 가 되기 때문이다. 즉, 두번째 클릭부터는 !scroller.getScrollState() 이 false 가 되면서 조건문이 실행되지 않고, 더이상 setScrollPosition() 이 실행되지 않는다. 

let lastScrollLocation = 0 // 최근 스크롤 위치 기억하기
let sectionToMove, menulink

window.addEventListener('scroll', (event) => {
    
    // 스크롤이 끝났음을 감지하기
    scroller.isScrollended()
    .then(result => console.log('scroll ended!'))
    .catch(err => console.log('scrolling...'))
})

landing.js 파일에 위 코드를 추가한다. window 객체에 스크롤 이벤트를 등록하여 스크롤링을 감지한다. 해당 코드는 스크롤링되는동안 isScrollended() 를 호출하면서 타이머를 해제한다. 스크롤이 멈추면 더이상 isScrollended() 이 호출되지 않고, 100ms 후에 타이머가 동작하면서 isScrolling 을 false 로 설정한다. 즉, 스크롤이 멈추면 스크롤이 멈춰있는 상태로 변경한다. 이렇게 하면 다시 네비게이션 메뉴를 클릭할때 !scroller.getScrollState() 이 true 가 되고, 조건문이 실행되면서 setScrollPosition() 을 호출하여 부드럽게 스크롤된다. 

const scroller = new Scroller(false) // 스크롤 객체 생성 

window.addEventListener("load", (event) => {
  // 테마변경 (다크모드/ 일반모드)
  const mode = document.querySelector('.mode')
  const icons = mode.querySelectorAll('.fa-solid')
  const header = document.querySelector('header')

  mode.addEventListener('click', (event) => {
    document.body.classList.toggle('dark')
    header.classList.toggle('dark')
    
    for(const icon of icons){
      icon.classList.contains('active') ? 
        icon.classList.remove('active') 
        : icon.classList.add('active')
    }
  })

  // 브라우저 상단으로 스크롤하기
  const arrowUp = document.querySelector('.footer .icons .scroll-up') // 위쪽 화살표 클릭 
  arrowUp.addEventListener('click', (event) => {
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'})
  })

  const logo = document.querySelector('header .logo') // 로고 클릭 
  logo.addEventListener('click', (event) => {
    event.preventDefault() // 부드러운 스크롤링
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'}) 
  })

  // 네비게이션 메뉴 클릭시 해당 섹션으로 스크롤하기
  const sections = document.querySelectorAll('section:not(.footer)') // 푸터를 제외한 section 엘리먼트 조회
  const nav = document.querySelector('.navbar ul')

  nav.querySelectorAll('li a').forEach(anchor => {
    anchor.addEventListener('click', function (event) {
      const section = this.getAttribute('href') // 네비게이션 메뉴에서 클릭한 섹션 
      const offsetToElementFromViewport = document.querySelector(section).getBoundingClientRect().top // 브라우저 상단에서 엘리먼트까지의 거리

      if(!scroller.getScrollState()){ // 스크롤링이 멈춘 경우만 해당 섹션으로 스크롤링
        event.preventDefault() // 부드러운 스크롤링 
        history.pushState({}, "", `${section}`);  // URL 주소에 #about, #story, #contact 과 같은 파라미터 추가해서 URL 변경하기

        const offsetToElementFromDocument = offsetToElementFromViewport + scroller.getScrollPosition() // 문서 상단에서 엘리먼트까지의 거리 
        scroller.setScrollPosition({
          top: offsetToElementFromDocument - header.offsetHeight - 10, // 헤더높이에서 10px 아래 위치로 스크롤링 
          behavior: "smooth",
        })
      }
    });
  });

  let lastScrollLocation = 0 // 최근 스크롤 위치 기억하기
  let sectionToMove, menulink
   

  window.addEventListener('scroll', (event) => {
    
    // 스크롤이 끝났음을 감지하기
    scroller.isScrollended()
    .then(result => console.log('scroll ended!'))
    .catch(err => console.log('scrolling...'))
  })
})

현재까지의 landing.js 파일은 위와 같다. 

 

스크롤이 어느정도 아래로 내려오면 헤더에 그림자 주기

// 스크롤이 어느정도 아래로 내려오면 헤더에 그림자 주기
scroller.getScrollPosition() > header.offsetHeight ? 
  header.classList.add('active') 
  : header.classList.remove('active')

landing.js 파일의 스크롤 이벤트핸들러 함수 내부에 위 코드를 추가한다. 스크롤이 어느정도 아래로 내려오면 헤더에 그림자를 추가한다. 

window.addEventListener('scroll', (event) => {
    
    // 스크롤이 끝났음을 감지하기
    scroller.isScrollended()
    .then(result => console.log('scroll ended!'))
    .catch(err => console.log('scrolling...'))
    

    // 스크롤이 어느정도 아래로 내려오면 헤더에 그림자 주기
    scroller.getScrollPosition() > header.offsetHeight ? 
      header.classList.add('active') 
      : header.classList.remove('active')
  })

 

스크롤바 위치에 따라 텍스트 애니메이션 적용하기 

sections.forEach(section => {
  // console.log(section.id, section.getBoundingClientRect().top, section.offsetHeight)

  const title = section.querySelector('.content h3') 
  const paragraph = section.querySelector('.content p')

  if(section.getBoundingClientRect().top < header.offsetHeight + 50){
    // 해당 섹션이 헤더에 가까워지면 해당 메뉴에 하이라이트 주기
    nav.querySelector('a.active').classList.remove('active')
    nav.querySelector(`a[href="#${section.id}"]`).classList.add('active')

    // 해당 섹션이 헤더에 가까워지면 텍스트 애니메이션 적용하기
    title.classList.add('show')
    paragraph.classList.add('show')
  }

  // 스크롤바가 브라우저 상단에 도달하면 텍스트 애니메이션 해제하기
  if(scroller.getScrollPosition() < 10){
    title.classList.remove('show')
    paragraph.classList.remove('show')
  }
})

landing.js 파일의 스크롤 이벤트핸들러 함수 내부에 위 코드를 추가한다. 스크롤링될때 해당 섹션이 헤더에 가까워지면 title, paragraph 에 show 클래스를 추가하여 텍스트 애니메이션을 보여준다. 반대로 스크롤바가 브라우저 상단에 도달하면 title, paragraph 에 show 클래스를 제거하여 텍스트를 화면에서 가린다. 또한, 스크롤링될때 해당 섹션이 헤더에 가까워지면 해당하는 네비게이션 메뉴에 active 클래스를 추가함으로써 스타일을 변경한다.  

window.addEventListener('scroll', (event) => {
    
    // 스크롤이 끝났음을 감지하기
    scroller.isScrollended()
    .then(result => console.log('scroll ended!'))
    .catch(err => console.log('scrolling...'))
    

    // 스크롤이 어느정도 아래로 내려오면 헤더에 그림자 주기
    scroller.getScrollPosition() > header.offsetHeight ? 
      header.classList.add('active') 
      : header.classList.remove('active')

    sections.forEach(section => {
      // console.log(section.id, section.getBoundingClientRect().top, section.offsetHeight)

      const title = section.querySelector('.content h3') 
      const paragraph = section.querySelector('.content p')

      if(section.getBoundingClientRect().top < header.offsetHeight + 50){
        // 해당 섹션이 헤더에 가까워지면 해당 메뉴에 하이라이트 주기
        nav.querySelector('a.active').classList.remove('active')
        nav.querySelector(`a[href="#${section.id}"]`).classList.add('active')

        // 해당 섹션이 헤더에 가까워지면 텍스트 애니메이션 적용하기
        title.classList.add('show')
        paragraph.classList.add('show')
      }

      // 스크롤바가 브라우저 상단에 도달하면 텍스트 애니메이션 해제하기
      if(scroller.getScrollPosition() < 10){
        title.classList.remove('show')
        paragraph.classList.remove('show')
      }
    })
  })

 

스크롤할때 곧바로 이전/다음 섹션으로 이동하기

if(!scroller.getScrollState()){ // 스크롤이 멈춘 경우 
      
  menulink = nav.querySelector('a.active').closest('li') // 현재 화면에 보이는 섹션에 대한 네비게이션 메뉴 

  // 스크롤시 이전, 다음 섹션으로 불연속적으로 이동하기
  if (scroller.getScrollPosition() > lastScrollLocation) {              // 스크롤을 내리는 경우 
    lastScrollLocation = scroller.getScrollPosition()                   // 최근 스크롤 위치 저장
    sectionToMove = menulink.nextElementSibling?.querySelector('a')     // 다음 메뉴
  } else {                                                              // 스크롤을 올리는 경우 
    lastScrollLocation = scroller.getScrollPosition()                   // 최근 스크롤 위치 저장            
    sectionToMove = menulink.previousElementSibling?.querySelector('a') // 이전 메뉴
  }

  // 스크롤링할때 이전/다음 메뉴를 프로그램적으로 클릭함으로써 해당 섹션으로 이동함   
  if(sectionToMove?.getAttribute('href') !== undefined){ // 이동할 이전/다음 섹션이 존재하는 경우
    sectionToMove.click() // 이미 작성된 a 태그의 클릭 이벤트에서 처리함
  }
}

landing.js 파일의 스크롤 이벤트핸들러 함수 내부에 위 코드를 추가한다. 기본적인 아이디어는 스크롤의 마지막 위치를 저장해서 현재 스크롤이 이전보다 올라갔는지 내려갔는지 판단한다. 스크롤을 내리면 프로그램적으로 다음 메뉴를 클릭해서 다음 섹션으로 이동한다. 스크롤을 올리면 프로그램적으로 이전 메뉴를 클릭해서 이전 섹션으로 이동한다. 

const scroller = new Scroller(false) // 스크롤 객체 생성 

window.addEventListener("load", (event) => {
  // 테마변경 (다크모드/ 일반모드)
  const mode = document.querySelector('.mode')
  const icons = mode.querySelectorAll('.fa-solid')
  const header = document.querySelector('header')

  mode.addEventListener('click', (event) => {
    document.body.classList.toggle('dark')
    header.classList.toggle('dark')
    
    for(const icon of icons){
      icon.classList.contains('active') ? 
        icon.classList.remove('active') 
        : icon.classList.add('active')
    }
  })

  // 브라우저 상단으로 스크롤하기
  const arrowUp = document.querySelector('.footer .icons .scroll-up') // 위쪽 화살표 클릭 
  arrowUp.addEventListener('click', (event) => {
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'})
  })

  const logo = document.querySelector('header .logo') // 로고 클릭 
  logo.addEventListener('click', (event) => {
    event.preventDefault() // 부드러운 스크롤링
    history.pushState({}, "", `#`); // URL 주소 변경 
    scroller.setScrollPosition({top: 0, behavior: 'smooth'}) 
  })

  // 네비게이션 메뉴 클릭시 해당 섹션으로 스크롤하기
  const sections = document.querySelectorAll('section:not(.footer)') // 푸터를 제외한 section 엘리먼트 조회
  const nav = document.querySelector('.navbar ul')

  nav.querySelectorAll('li a').forEach(anchor => {
    anchor.addEventListener('click', function (event) {
      const section = this.getAttribute('href') // 네비게이션 메뉴에서 클릭한 섹션 
      const offsetToElementFromViewport = document.querySelector(section).getBoundingClientRect().top // 브라우저 상단에서 엘리먼트까지의 거리

      if(!scroller.getScrollState()){ // 스크롤링이 멈춘 경우만 해당 섹션으로 스크롤링
        event.preventDefault() // 부드러운 스크롤링 
        history.pushState({}, "", `${section}`);  // URL 주소에 #about, #story, #contact 과 같은 파라미터 추가해서 URL 변경하기

        const offsetToElementFromDocument = offsetToElementFromViewport + scroller.getScrollPosition() // 문서 상단에서 엘리먼트까지의 거리 
        scroller.setScrollPosition({
          top: offsetToElementFromDocument - header.offsetHeight - 10, // 헤더높이에서 10px 아래 위치로 스크롤링 
          behavior: "smooth",
        })
      }
    });
  });

  let lastScrollLocation = 0 // 최근 스크롤 위치 기억하기
  let sectionToMove, menulink
   

  window.addEventListener('scroll', (event) => {
    
    // 스크롤이 끝났음을 감지하기
    scroller.isScrollended()
    .then(result => console.log('scroll ended!'))
    .catch(err => console.log('scrolling...'))
    

    // 스크롤이 어느정도 아래로 내려오면 헤더에 그림자 주기
    scroller.getScrollPosition() > header.offsetHeight ? 
      header.classList.add('active') 
      : header.classList.remove('active')

    sections.forEach(section => {
      // console.log(section.id, section.getBoundingClientRect().top, section.offsetHeight)

      const title = section.querySelector('.content h3') 
      const paragraph = section.querySelector('.content p')

      if(section.getBoundingClientRect().top < header.offsetHeight + 50){
        // 해당 섹션이 헤더에 가까워지면 해당 메뉴에 하이라이트 주기
        nav.querySelector('a.active').classList.remove('active')
        nav.querySelector(`a[href="#${section.id}"]`).classList.add('active')

        // 해당 섹션이 헤더에 가까워지면 텍스트 애니메이션 적용하기
        title.classList.add('show')
        paragraph.classList.add('show')
      }

      // 스크롤바가 브라우저 상단에 도달하면 텍스트 애니메이션 해제하기
      if(scroller.getScrollPosition() < 10){
        title.classList.remove('show')
        paragraph.classList.remove('show')
      }
    })

    if(!scroller.getScrollState()){ // 스크롤이 멈춘 경우 
      
      menulink = nav.querySelector('a.active').closest('li') // 현재 화면에 보이는 섹션에 대한 네비게이션 메뉴 

      // 스크롤시 이전, 다음 섹션으로 불연속적으로 이동하기
      if (scroller.getScrollPosition() > lastScrollLocation) {              // 스크롤을 내리는 경우 
        lastScrollLocation = scroller.getScrollPosition()                   // 최근 스크롤 위치 저장
        sectionToMove = menulink.nextElementSibling?.querySelector('a')     // 다음 메뉴
      } else {                                                              // 스크롤을 올리는 경우 
        lastScrollLocation = scroller.getScrollPosition()                   // 최근 스크롤 위치 저장            
        sectionToMove = menulink.previousElementSibling?.querySelector('a') // 이전 메뉴
      }

      // 스크롤링할때 이전/다음 메뉴를 프로그램적으로 클릭함으로써 해당 섹션으로 이동함   
      if(sectionToMove?.getAttribute('href') !== undefined){ // 이동할 이전/다음 섹션이 존재하는 경우
        sectionToMove.click() // 이미 작성된 a 태그의 클릭 이벤트에서 처리함
      }
    }
  })
})

완성된 landing.js 파일은 위와 같다. 

 

코드 리팩터링

// 스크롤이 끝났음을 감지하기
    scroller.isScrollended()
    .then(result => {
      console.log('scroll ended!')
      lastScrollLocation = scroller.getScrollPosition()  // 최근 스크롤 위치 저장
    })
    .catch(err => console.log('scrolling...'))

최근 스크롤 위치는 스크롤이 끝났을때 저장하도록 수정한다. 예를 들어 서비스 섹션에서 스토리 섹션으로 이동한다고 하면 스토리 섹션에서 멈추고 나서 해당 위치를 최근 스크롤 위치로 저장해야 다음에 스크롤할때 스크롤을 내렸는지 올렸는지 정확한 판단이 가능하다. 만약 스크롤을 시작할때 저장하면 서비스 섹션 근처가 최근 스크롤 위치가 되므로 스토리 섹션에서 스크롤을 올려도 스크롤을 내린것처럼 동작한다. scroller.getScrollPosition() > lastScrollLocation 이기 때문이다. 

let sectionToMove, menulink

위 코드는 제거하고 아래 코드를 추가한다. 

let index = 0 // 현재 메뉴를 선택하기 위한 인덱스 값

현재 메뉴를 선택하기 위한 인덱스 변수를 추가한다. 

if(!scroller.getScrollState()){ // 스크롤이 멈춘 경우 
      
      menulink = nav.querySelector('a.active').closest('li') // 현재 화면에 보이는 섹션에 대한 네비게이션 메뉴 

      // 스크롤시 이전, 다음 섹션으로 불연속적으로 이동하기
      if (scroller.getScrollPosition() > lastScrollLocation) {              // 스크롤을 내리는 경우 
        sectionToMove = menulink.nextElementSibling?.querySelector('a')     // 다음 메뉴
      } else {                                                              // 스크롤을 올리는 경우            
        sectionToMove = menulink.previousElementSibling?.querySelector('a') // 이전 메뉴
      }

      // 스크롤링할때 이전/다음 메뉴를 프로그램적으로 클릭함으로써 해당 섹션으로 이동함   
      if(sectionToMove?.getAttribute('href') !== undefined){ // 이동할 이전/다음 섹션이 존재하는 경우
        sectionToMove.click() // 이미 작성된 a 태그의 클릭 이벤트에서 처리함
      }
    }

위 코드를 아래와 같이 추가한다. 스크롤되면서 메뉴에 active 가 적용되기까지는 시간이 걸린다. 예를 들어 스토리 섹션에서 아래로 스크롤하여 연락처 섹션으로 이동할때 현재 화면에 보이는 섹션(active 가 적용된 섹션)은 스토리 섹션이므로 여기서 스크롤을 위로 올리면 서비스 섹션으로 다시 돌아간다. 그래서 즉각적으로 현재 섹션을 선택할 수 있도록 인덱스 값으로 변경하였다. 

const menus = nav.querySelectorAll('li a') 
    if(!scroller.getScrollState()){ // 스크롤이 멈춘 경우 
  
      // 스크롤시 이전, 다음 섹션으로 불연속적으로 이동하기
      if (scroller.getScrollPosition() > lastScrollLocation) {              // 스크롤을 내리는 경우 
        index++                                                             // 다음 메뉴
      } else {                                                              // 스크롤을 올리는 경우            
        index--                                                             // 이전 메뉴
      }
      if(index < 0) index = 0
      if(index > menus.length - 1) index = menus.length - 1

      // 스크롤링할때 이전/다음 메뉴를 프로그램적으로 클릭함으로써 해당 섹션으로 이동함   
      if(menus[index]){ // 이동할 이전/다음 섹션이 존재하는 경우
        menus[index].click() // 이미 작성된 a 태그의 클릭 이벤트에서 처리함
      }
    }

이렇게 하면 위아래로 스크롤할때마다 이전/다음 섹션을 바로 선택할 수 있다. 

window.addEventListener('load', (event) => {
  scroller.setScrollPosition({ top: 0, behavior: "smooth" })
})

새로고침(F5) 할때 브라우저 상단으로 이동하도록 수정한다. 

 

728x90

'프로젝트 > 블로그 사이트' 카테고리의 다른 글

5. 포스트 페이지 구현하기  (0) 2023.07.16
4. 홈페이지 구현하기  (0) 2023.07.09
3. 스토리 페이지 구현하기  (0) 2023.07.08
1. 프로젝트 준비  (0) 2023.07.07