개발, 공부, 일상 블로그

[Java] 우아한 테크코스 - 블랙잭

|

우아한 테크코스의 프리코스-블랙잭을 구현하면서 정리한 글입니다.

요구사항

  • 블랙잭 게임을 진행하는 프로그램을 구현한다. 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다.
  • 플레이어는 게임을 시작할 때 배팅 금액을 정해야 한다. 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다.
  • 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. 단, 카드를 추가로 뽑아 21을 초과할 경우 배팅 금액을 모두 잃게 된다.
  • 처음 두 장의 카드 합이 21일 경우 블랙잭이 되면 베팅 금액의 1.5 배를 딜러에게 받는다. 딜러와 플레이어가 모두 동시에 블랙잭인 경우 플레이어는 베팅한 금액을 돌려받는다.
  • 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리해 베팅 금액을 받는다.

우아한테크코스 프리코스 Week 3. 블랙잭 을 참고했습니다.


🎱 프로그래밍 요구사항

  • 자바 코드 컨벤션을 지키면서 프로그래밍한다.
    • 기본적으로 Google Java Style Guide을 원칙으로 한다.
    • 단, 들여쓰기는 ‘2 spaces’가 아닌 ‘4 spaces’로 한다.
  • indent(인덴트, 들여쓰기) depth를 1까지만 허용한다.
  • 3항 연산자를 쓰지 않는다.
  • 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.
  • 프로그래밍 요구사항에서 별도로 변경 불가 안내가 없는 경우 파일 수정과 패키지 이동을 자유롭게 할 수 있다.
  • 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 [ERROR] 로 시작해야 한다.
  • 함수(또는 메소드)의 길이가 10라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.
  • else 예약어를 쓰지 않는다.
  • 생성자와 접근 제어자를 건드리지 않는다.

😎 나의 요구사항

  • 가능하면 Setter, Getter를 사용하지 않는다.
  • TDD 기반으로 개발한다. (공부한 내용 블로깅 하기)
  • MVC 패턴으로 개발한다.



요구사항 분석

요구사항을 간단하게 정리해보았다.

  • 구성 요소
    • 블랙잭 게임
      • 딜러
      • 플레이어
      • 카드
        • Ace는 1 또는 11로 계산
        • J, Q, K는 각각 10으로 계산
  • 게임 흐름
    • 시작 시
      • 플레이어
        1. 배팅 금액을 정함
        2. 2장의 카드를 지급 받음
    • 게임 진행 중
      • 카드의 합이 21을 넘지 않을 경우 계속 카드를 더 뽑을 수 있음 (힛, hit)
      • 21이 넘으면(버스트, burst) 즉시 패배
      • 본인의 패에 만족하는 경우 차례를 마칠 수 있음 (스탠드, stand)
      • 딜러
        • 16 이하면 무조건 히트, 17 이상이면 무조건 스테이
    • 승리 조건
      • 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이김
    • 보상
      • 버스트, burst인 경우 -> 0배
      • 첫 2장이 21인 경우 -> 1.5배
      • 그 외 승리할 경우 -> 2배

0. 프로젝트 구조

초기 프로젝트 구조는 아래와 같다.

.
|-- main
|   `-- java
|       `-- domain
|           |-- card
|           |   |-- Card.java
|           |   |-- CardFactory.java
|           |   |-- Symbol.java
|           |   `-- Type.java
|           |-- user
|               |-- Dealer.java
|               `-- Player.java
`-- test
    `-- java
        `-- domain
            `-- card
                `-- CardFactoryTest.java

1. repository 추가

repository 패키지를 생성하고 PlayerRepository 인터페이스를 추가했다.

package domain.repository;

import domain.user.Player;

import java.util.List;

public interface PlayerRepository {
    List<Player> getPlayers();
    void addPlayer(Player player);
}

그리고 PlayerRepositoryImpl 클래스를 구현하였다.

package domain.repository;

import domain.user.Player;

import java.util.ArrayList;
import java.util.List;

public class PlayerRepositoryImpl implements PlayerRepository {
    private List<Player> players;

    public PlayerRepositoryImpl() {
        players = new ArrayList<>();
    }

    @Override
    public List<Player> getPlayers() {
        return players;
    }

    @Override
    public void addPlayer(Player player) {
        players.add(player);
    }
}

그리고 테스트 코드를 작성했다.

[Design Pattern] DI(Dependency Injection, 의존성 주입)와 IoC(Inversion of Control, 제어 반전)

|

🤔 DI와 IoC?

duh

처음 스프링을 접하면 DI (Dependency Injection, 의존성 주입)IoC(Inversion of Control, 제어 반전)라는 개념이 등장한다.
흔히 스프링의 3대 요소들 중 하나로 불리는 DI, IoC에 대해 알아보려고 한다.

DI와 IoC는 디자인 패턴으로 스프링에만 국한되는 것 이 아니기 때문에 디자인 패턴이라는 카테고리로 정리했다.

🐥 의존성 (Dependency)

우선 가장 중요한 개념인 의존성에 대해 정리해보도록 하자.

의존성… 뭔가 딱 와닿지 않는다.

나만 그런 것일 수 있겠지만, 난 의존성이라는 단어가 아주 낯설게 느껴진다.
좀 익숙하게 쉽게 풀어보자. ~에 대한 의존성을 가진다 라는 것은 ~가 필요하다 라고 쉽게 풀어 쓸 수 있다.

예를 들어, Gradle(build.gradle)이나 Maven(pom.xml), 더 나아가 NPM(package.json)을 보면 dependency는 외부 패키지, 라이브러리를 나타낸다.
즉, 해당 패키지, 라이브러리가 필요함을 명시할 때 우리는 dependency, 의존성이라는 말을 쓴다.

A가 B에 대한 의존성을 가진다. 또는 B는 A의 의존성이다.

이 말은 A는 B가 필요하다. 라고 쉽게 풀어 쓸 수 있다.

A는 의존성을 스스로 만든다.

이 말은 A는 자기가 필요한 요소를 스스로 생성한다. 라고 쉽게 풀어 쓸 수 있다.

예를 들어보자,

public class Pizza {
    void eat() {
        System.out.println("피자를 먹었다. 냠냠");
    }
}
public class Person {
    void eat() {
        Pizza pizza = new Pizza();
        pizza.eat();
    }
}

위의 코드에서, PersonPizza의 관계는 어떠한가?

PersonPizza를 직접 만들어 먹는다.

따라서 PersonPizza사이에는 강한 결합 (강한 의존성)이 생긴다.

만약 피자(Pizza)라는게 존재하지 않는다면, 사람(Person)은 식사(eat)를 할 수 없다.
Pizza를 다른 음식으로 대체하려면 Person의 대부분의 코드를 수정해야한다.

pizza

↑ 피자에 강한 의존성을 가지는 강아지

그럼 어떻게 하면 요소들 사이의 결합을 약하게 할 수 있을까?

public interface Food {
    void eat();
}

자바에는 Interface가 있다.
자바가 익숙하지 않은 사람들은 도저히 인터페이스가 왜 필요한 것인지 이해하지 못한다. (나도 그랬다.)

인터페이스를 사용하면 특정 메소드를 반드시 구현하도록 강제할 수 있다.
따라서 해당 인터페이스를 implements 하는 클래스에는 특정 메소드가 존재함이 보장된다.

자, 이제 다시 작성해보자.

public class Pizza implements Food{
    @Override
    void eat() {
        System.out.println("피자를 먹었다. 냠냠");
    }
}
public class Hotdog implements Food{
    @Override
    void eat() {
        System.out.println("핫도그를 먹었다. 냠냠");
    }
}
public class Person {
    void eat(Food food) {
        food.eat();
    }
}

이제 Person은 Pizza 외의 음식도 (Food를 implements하는 클래스라면) 먹을 수 있다.
이 것이 바로 느슨한 결합, 또는 약한 결합, 약한 의존성이다.

hotdog

이제 핫도그도 먹을 수 있다.


💉 DI: 의존성을 주입한다고?

injection

그래 이제 의존성은 알겠다.
주입? 또 뭔가 낯설다…

주입(Injection)전달(Pass)로 해석하면 이해하기 좀 더 수월할 것이다.
의존성을 주입한다는 것은 즉, 필요한 것을 전달한다는 것과 같다.

위의 Person, Food의 예로 쉽게 설명하자면

Person에게 Food를 전달하는 것

이다.

의존성을 전달(주입)하기 위해 사용할 수 있는 방법은 뭐가 있을까?

1. 메소드의 파라미터를 이용한 전달(주입)

public class Person {
    void eat(Food food) {
        food.eat();
    }
}

이게 바로 파라미터를 이용한 전달이다.

하지만 Food를 두고두고 조금씩 먹고싶다면 어떨까?
Person의 필드(Field, 멤버 변수)로 설정한다면 재사용할 수 있을것이다.

public class Person {
    Food food;

    void setFood(Food food) {
        this.food = food;
    }

    void eat() {
        if (food == null) {
            System.out.println("먹을게 없어..");
            return;
        }
        food.eat();
    }
}

이렇게 필드를 설정하는 setFood와 같은 메소드를 Setter라고 한다. (필드 값을 반환하는 getFood와 같은 메소드는 Getter라고 한다.)

위처럼 Setter를 이용해 의존성을 전달(주입)하는 방식을 설정자를 이용한 주입 (Setter Injection)이라고 한다.

2. 생성자를 이용한 전달(주입)

Person 인스턴스를 생성할 때 Food를 전달하는 방식이다.
생성자의 매개변수로 받으면 된다.

public class Person {
    Food food;

    public Person(Food food) {
        if (food == null) {
            throw IllegalArgumentException("음식을 내놔라 😡!!");
        }
        this.food = food;
    }

    void eat() {
        food.eat();
    }
}

이런식으로 만들 수 있겠다. 😏


🙃 IoC: 제어를 반전한다고?

역흐름제어

웹툰 신의탑

자, DI에 대해 알아보았다.
이제 IoC(Inversion of Control, 제어 반전)에 대해 알아보자!

프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게 되는 소프트웨어 디자인 패턴
Wiki

한국말이 맞나 싶다… 다른 정의를 찾아보자!

프레임워크가 정의한 인터페이스를 클라이언트 코드가 구현을 하고, 구현된 객체를 프레임워크에 전달(주입)하여
프레임워크가 제어를 가지게 하는 것
Inversion of Control 컨테이너 (IoC Container)란?

좀더 가볍게 정의해보자!

인터페이스를 구현하고, 구현한 객체를 외부에 전달하여 제어를 외부로 넘기는것

이렇게까지 가볍게 정의하면, 앞서 공부한 DI의 개념과 IoC의 개념이 거의 일치한다는 것을 알 수 있다.

Food를 구현한 PizzaPerson에게 전달하여 제어를 Person에게 넘기는 것

PersonPizza를 생성하던 기존의 제어 흐름에서 반전된
Food를 구현하는 Pizza를 생성하여 Person에게 전달하는 역전된 제어 흐름이 바로 IoC의 개념이다.

😎 즉, DI ~= IoC

같다

😱 이게 IoC였어?!!?!?

Java로 쓰레드를 구현해본 적이 있다면 다음과 같은 코드를 한번쯤 사용해봤을 것이다.

new Thread(new Runnable() {
    @Override
    public void run() {
    	// Do something
    }
}).start();

그래서 이게 IoC랑 무슨 상관인데! 싶다면 이 글을 천천히 다시 읽어보자!
(설명이 부족했다 싶으면 다른 포스트를 참고해봐도 좋다.)

Thread의 생성자에 Runnable 인터페이스를 구현한 객체를 넘겨준다.
그리고 start 메소드를 호출하면 내부적으로 Runnable의 run 메소드를 호출한다.

이 것이 바로 DI를 사용한 전달이며, IoC를 사용한 제어이다.

그 외에도 만약 Android 개발을 해보았다면?

btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Do something
    }
});

리스너로 인터페이스, 콜백을 전달하는 방식도 DI, IoC의 개념이 적용된 것이다!

놀란 고양이


📕 마무리

DI (Dependency Injection, 의존성 주입)IoC(Inversion of Control, 제어 반전)에 대해 깊게 정리해보았다!
우리가 일상속에서 당연한 것으로 받아들이고 개발했던 것들에 이런 개념이 숨어 있었다니… 참 놀랍지 않은가?! (ㅔ)

🚀 참고

  • https://moonscode.tistory.com/58
  • https://vandbt.tistory.com/44
  • https://velog.io/@wickedev/IoC-DIP-IoC-Container-DI-DI-Framework-%EB%8F%84%EB%8C%80%EC%B2%B4-%EA%B7%B8%EA%B2%8C-%EB%AD%94%EB%8D%B0

[Spring] 스프링 프레임워크의 주요 모듈들을 알아보자

|

☘️ 스프링 프레임워크의 주요 모듈들

📚 시리즈 - 스프링 5.0

  1. 왜 스프링 프레임워크를 사용할까? (Spring vs EJB, JavaEE)
  2. 스프링 프레임워크의 주요 모듈들
  3. DI(Dependency Injection, 의존성 주입)와 IoC(Inversion of Control, 제어 반전)

⚽︎ 목표

스프링 프레임워크의 주요 모듈들을 살펴보자

🤔 스프링 모듈?

지난 번 포스트 에서 공부했듯이 스프링 프레임워크의 모듈성은 스프링이 인기있는 이유 중 하나다.
이번에는 스프링 프레임워크의 주요 모듈들에 대해 살펴보도록 하겠다.


모듈

  1. 스프링 코어 컨테이너 (Spring Core Container)
  2. 횡단 관심 (Crosscutting Concerns)
  3. 웹 (WEB)
  4. 비즈니스 (Business)
  5. 데이터 (Data)

하나씩 살펴보자.

1. 스프링 코어 컨테이너 (Spring Core Container)

스프링 코어 컨테이너는

  • DI (Dependency Injection, 의존성 주입)
  • IoC (Inversion of Control, 제어 역전) 컨테이너
  • Application Context (애플리케이션 컨텍스트)

의 핵심 기능을 제공한다.

💉 DI (Dependency Injection, 의존성 주입)

의존성을 외부에서(주로 IoC 컨테이너가) 지정해 주는 것

⏳ IoC (Inversion of Control, 제어 역전)

프로그래머가 작성한 프로그램이 프레임워크의 제어를 받게 되는 소프트웨어 디자인 패턴

🏭 IoC 컨테이너

객체의 생성, 의존성을 관리하는 컨테이너

🐣 빈 (Bean)

Spring IoC Container가 관리하는 자바 객체
스프링이 제어권을 가지고 직접 생성하며 관계를 부여하는 오브젝트

🐔 빈 팩토리 (Bean Factory)

빈을 생성하고 관계를 설정하는등의 제어를 담당하는 IoC 오브젝트

🐓 애플리케이션 컨텍스트 (Application Context)

애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진, 일종의 빈 팩토리라고 볼 수 있다.
ApplicationContext 인터페이스는 BeanFactory 인터페이스를 상속한다.

  모듈/아티팩트 사용
Core spring-core 다른 스프링 모듈이 사용하는 유틸리티
Beans spring-beans 스프링 빈 지원, 의존성 주입을 제공한다. 빈 팩토리(BeanFactory)의 구현을 포함한다.
Context spring-context 빈 팩토리를 상속하는 애플리케이션 컨텍스트를 구현하고 리소스 로드 및 국제화 지원을 제공한다.
SpEL spring-expression EL(Expression Language, 표현 언어)을 확장하고 빈 속성 및 접근, 처리를 위한 언어를 제공한다.

2. 횡단 관심 (Crosscutting Concerns)

횡단 관심
AOP(Aspect Oriented Programming, 관점지향 프로그래밍) 에서
핵심적인 기능 (Core Concerns) 외에 중간중간 삽입되어야 할 기능들을 횡단 관심 (Crosscutting Concerns) 이라고 한다.

Crosscutting Concerns
예를 들어서, 로깅, 보안, 예외처리, 모니터링 등이 대표적인 횡단 관심이다.

횡단 관심은 로깅 및 보안과 같은 모든 애플리케이션 레이어에 적용할 수 있다.
AOP는 일반적으로 횡단 관심을 구현하는 데 사용된다.
단위 테스트와 통합 테스트는 모든 레이어에 적용될 수 있으므로 횡단 관심 카테고리에 적합하다.

  모듈/아티팩트 사용
AOP spring-aop AOP(Aspect Oriented Programming, 관점지향 프로그래밍)에 대한 기본적인 기능을 제공한다.
Aspect spring-aspects 인기있는 AOP 프레임워크인 AspectJ와의 통합을 제공한다.
Instrument spring-instrument 기본적인 instrumentation을 제공한다.
(Byte Code Instrumentation: 런타임이나 로딩 때 클래스의 바이트코드를 변경하는 것)
Test spring-test 단위 및 통합 테스트에 대한 기본적인 기능을 제공한다.

3. 웹 (WEB)

스프링은 다른 대중적인 웹 프레임워크들(ex. Apache Struts)과의 훌륭한 통합을 제공하는 것 외에도 자체 MVC 프레임워크인 Spring MVC를 제공한다.

  모듈/아티팩트 사용
Web spring-web 멀티파트 파일 업로드와 같은 기본 웹 기능을 제공한다. 다른 웹 프레임워크와의 통합을 제공한다.
Servlet spring-webmvc 자체 MVC 프레임워크를 제공한다. Spring MVC, REST 웹 서비스를 구현을 포함한다.
WebSocket spring-websocket 웹 소켓을 지원한다.
Portlet spring-webmvc-portlet 포틀릿 환경에서 사용할 MVC 구현을 제공한다.

4. 비즈니스 (Business)

비즈니스 레이어는 애플리케이션의 비즈니스 로직을 실행하는 데 초점을 맞춘다.
스프링에서는 비즈니스 로직을 POJO (Plain Old Java Object)로 구현한다.

🤔 POJO..?

순수한 자바 객체를 의미한다.
다시 말해, 어떤 클래스를 확장(extends)하거나, 인터페이스를 구현(implements)하거나, 어노테이션(Annotation)을 포함할 필요가 없는 객체이다..

  모듈/아티팩트 사용
Transaction spring-tx 선언적 트랜잭션 관리를 제공한다.

5. 데이터 (Data)

데이터 레이어는 일반적으로 데이터베이스 및 외부 인터페이스와 상호작용한다.

  모듈/아티팩트 사용
JDBC (Java Database Connectivity) spring-jdbc JDBC 추상화를 제공한다. 이 모듈의 이점에 대해서는 이전 포스트에서 잠깐 언급했다.
ORM (Object Relational Mapping) spring-orm JPA (Java Persistence API), JDO (Java Data Objects), Hibernate와 같은
ORM API를 위한 통합레이어를 제공한다.
OXM (Object XML Mapping) spring-oxm JAXB, Castor, XMLBeans, JiBX, XStream과 같은
Object/XML 매핑을 지원한다.
Messaging spring-messaging 메시지 기반 애플리케이션을 작성할 수 있는 기능을 제공한다.
JMS (Java Message Service) spring-jms 메시지 생산(Producing)과 소비(Consuming)를 위한 기능을 제공한다.
Spring Framework 4.1 부터는 spring-messaging과의 통합을 제공한다.

📕 마무리

책에서 설명이 부실한 부분은 직접 찾아가면서 내용을 채웠다.
스프링의 주요 모듈들이 어떤 기능들을 제공하는지 짚고 다음 단계로 넘어가면 될 것 같다.

크게 코어, 횡단 관심, , 비즈니스, 데이터 계층으로 나뉘고, 각각의 역할을 간단히 정리해보자

  • 코어 : DI, IoC에 대한 핵심 기능 제공 (Bean, Bean Factory, Application Context)
  • 횡단 관심 : AOP에서 핵심기능 외에 중간중간 삽입되어야 할 기능들
  • 웹 : 웹 서비스를 위한 기능들
  • 비즈니스 : 비즈니스 로직을 실행, 선언적 트랜잭션 관리
  • 데이터 : 데이터베이스 및 외부 인터페이스와의 상호작용

다음에는 스프링의 핵심 기능 중 하나인 DI (Dependency Injection, 의존성 주입) 에 대해 자세하게 알아보자.


🚀 참고

[리뷰] 맛있닭 더담은 도시락 - 다섯가지나물밥 & 갈릭스테이크소스 닭가슴살

|

🐔 닭가슴살 도시락 리뷰

도시락

이 것은 도시락이다.
정확히 말하자면, 맛있닭 더담은 도시락 - 다섯가지나물밥 & 갈릭스테이크소스 닭가슴살이다.

나와 졸업작품 한 배를 탄 유영균 선생님께서 먹어보라고 그냥 주셨다.

기미상궁의 마음으로 먹고, 냉정하게 리뷰해보도록 하겠다.

🐓 구성

구성

더 알찬 구성이라고 한다.

  • 다섯가지나물밥
  • 화이트 오믈렛
  • 갈릭스테이크소스 닭가슴살
  • 닭가슴살 장조림
  • 베이비캐롯 & 새송이버섯

탄산수

아, 그리고 이것도 받았다. 1am ZERO kcal 원에이엠 스파클링 (라임)

👨‍🍳 조리

조리법

그냥 그대로 전자레인지에 4분 돌리면 된다.

4분

⚠ 주의사항

뜨거운것

정보 : 전자레인지에서 갓 나온 도시락은 매우 뜨겁다.
반드시 가장자리를 조심히 잡도록 하자.

🔪 포장벗기기

포장이 잘 안뜯어진다.
충분한 인내를 가지고 뜯도록 하자.

그 만큼 포장을 꼼꼼하게 했다는 것이다.

🐓 먹기 및 리뷰

포장

1. 다섯가지나물밥

밥은 그냥 무난하게 맛있다.
곤드레밥 느낌이다.
나는 곤드레밥을 좋아한다.

성분표를 확인해보니 다섯가지나물이 취나물, 콩나물, 고사리나물, 도라지나물, 참나물 이라고 한다.
근데 잘 모르겠다.

2. 화이트 오믈렛

계란 흰자로 만들어서 화이트 오믈렛인가보다.
계란 흰자 맛이 난다.

3. 갈릭스테이크소스 닭가슴살

부드럽고 맛있다.
난 닭가슴살이 이렇게 맛있는 것인지 몰랐다.
닭가슴살도 양념을 잘 하면 맛있구나 라는 것을 깨달았다.
식견(食見)이 넓어지는 순간이다.

4. 닭가슴살 장조림

처음엔 참치인줄 알았다.
식감은 장조림과 게맛살 사이인데, 짜지 않다.
무난하게 맛있다.

5. 베이비캐롯 & 새송이버섯

난 원래 당근을 좋아하지만, 이 당근은 식감이 너무 이상하다.
새송이버섯은 그냥 익숙한 새송이버섯 맛이 난다.

😄 총평

일단 맛있다. 전체적으로 무난하게 맛있어서 싹싹 긁어먹었다.
특히 닭가슴살이 맛있다.

밥도 질리지 않고 맛있다.

근데 더담은 도시락인데 양이 부족하다.
만약 더 담지 않았다면, 어느정도만 담으려고 했던 것일까? 🤔

컵라면의 도움으로 허기에서 벗어날 수 있었다.

그리고 함께 받은 탄산수는 0칼로리인 것이 믿겨지지 않을만큼 맛있다.

🚀 여기에서 사셨다고 합니다.

근데 꼭 여기에서 살 필요는 없다.
구글에 검색해서 싸고 좋은곳에서 사는 것이 좋을 것 같다.

구매링크 - 랭킹닭컴

[Github] 블로그 포스트에 스크롤에 따른 목차(Table of Contents, TOC)를 띄우는 ScrollSpy 기능 구현하기

|

✨ 스크롤에 따른 목차를 띄우는 ScrollSpy 기능 구현하기

지난번에 약속한 대로, allejo/jekyll-toc을 이용해서 포스트 오른쪽에 목차를 띄우는 기능을 구현하는 방법을 포스팅하겠다.

🤔 뭘 한다고?

직접 보는게 이해하기 쉬울 것이다. 이걸 구현할거다.

이걸 구현할 것이다.
처음엔 이걸 도대체 뭐라고 검색해야 나오는지 몰라서 이것저것 검색해봤는데

이 글을 보고 있는 여러분들도 나와 같은 심정이였을 것 같다.

velog.io의 포스트를 보고, 이 기능을 구현해야 겠다고 생각하게 됐다.

그래서 디자인도 거의 유사동일하다..

🔨 이제 만들어보자!

이 기능을 구현하기 위한 과정은 크게 3단계로 나눌 수 있다.

  1. _includes 디렉터리에 toc.html 등록하기
  2. _layouts/post.html 파일 수정하기
  3. _scss/component/_post.scss 파일 수정하기

테마에 따라 파일명은 다를 수 있지만, 결국 해야 하는 일은 같다.

1. _includes 디렉터리에 toc.html 등록하기

간단하다. 말 그대로 toc.html를 _includes 디렉터리에 다운로드 하면 된다. (클릭하면 바로 다운로드 됩니다.)

그리고 Github 저장소에 Push하자.

git add _includes/toc.html
git commit -m "docs(toc): toc.html 추가"
git push origin master

커밋 메시지 타입이 docs가 맞는지 확실하지는 않다… 아무튼 html 문서니까 docs로 했다.

2. _layouts/post.html 파일 수정하기

반드시 내가 작성한 코드대로 하는 것이 정답은 아니다.
상당히 비효율적이고, 나중에 리팩토링이 필요한 코드라고 생각한다.

1. 본문이 들어가는 <article>을 수정한다.

<div class="post">
  <h1 class="post-title">{{ page.title }}</h1>
  <span class="post-date">{{ page.date | date_to_string }}</span>
  {% if page.tags %} | 
  {% for tag in page.tags %}
    <a href="{{ site.baseurl }}{{ site.tag_page }}#{{ tag | slugify }}" class="post-tag">{{ tag }}</a>
  {% endfor %}
  {% endif %}
  <article>
    <!-- 이 부분을 수정하면 된다. -->
    {{ content }}
  </article>
</div>

이 부분에서 <article>...</article> 부분을 아래와 같이 수정한다.

<article class="post-article">
    <div class="toc">
      <a href="#">&lt;맨 위로&gt;</a>
      {% include toc.html html=content %}
    </div>
    {{ content }}
  </article>

간단하게 설명하자면, article에 class를 추가해주었다.
이는 스타일링을 위한 클래스가 아니라, script에서 엘리먼트로 가져오기 위해 추가한 것이다.

또, toc 클래스를 가지는 div 엘리먼트를 추가했다.
안에는 아까 다운로드한 toc.html의 내용이 들어간다.

2. 스크립트를 추가한다.

스크롤할 때마다 계속 반복하므로, 효율적이지 못한 것 같다.
추후에 수정하도록 하겠다.

_layouts/post.html의 맨 아래에 아래 코드를 추가한다.

<script>
  function getTOCNodes(master) {
    var nodes = Array.prototype.slice.call(master.getElementsByTagName("*"), 0);
    var tocNodes = nodes.filter(function(elem) {
        return elem.tagName == "A";
    });
    return tocNodes;
  }
  function getHeaderNodes(master) {
    var nodes = Array.prototype.slice.call(master.getElementsByTagName("*"), 0);
    var headerNodes = nodes.filter(function(elem) {
        return elem.tagName == "H1" || elem.tagName == "H2" || elem.tagName == "H3" || elem.tagName == "H4" || elem.tagName == "H5" || elem.tagName == "H6";
    });
    return headerNodes;
  }

  var title = document.getElementsByClassName("post-title")[0];
  var titleY = window.pageYOffset + title.getBoundingClientRect().top;
  
  var article = document.getElementsByClassName("post-article")[0];
  var articleY = window.pageYOffset + article.getBoundingClientRect().top;

  var toc = document.getElementsByClassName("toc")[0];

  var headerNodes = getHeaderNodes(article);
  var tocNodes = getTOCNodes(toc);

  var before = undefined;

  document.addEventListener('scroll', function(e) {
    if (window.scrollY >= articleY-60) {
      toc.style.cssText = "position: fixed; top: 60px;";
    }
    else {
      toc.style.cssText = "";
    }

    var current = headerNodes.filter(function(header) {
      var headerY = window.pageYOffset + header.getBoundingClientRect().top;
      return window.scrollY >= headerY - 60;
    });

    if (current.length > 0) {
      current = current[current.length-1];

      var currentA = tocNodes.filter(function(tocNode) {
        return tocNode.innerHTML == current.innerHTML;
      })
      
      currentA = currentA[0];
      if (currentA) {
        if (before == undefined) before = currentA;

        if (before != currentA) {
          before.classList.remove("toc-active");
          before = currentA;
        }

        currentA.classList.add("toc-active");
      }
      else {
        if (before) 
          before.classList.remove("toc-active");
      }
    }
    else {
      if (before) 
          before.classList.remove("toc-active");
    }

  }, false);
</script>

개선의 여지가 아~~~주 많이 보이는 코드이지만, 어쨌든 잘 동작한다.

간단하게 설명하자면, scroll 이벤트가 발생할 때 마다
article의 <h1~h6>엘리먼트들의 위치와 스크롤 위치를 비교하면서
현재 보고있는 부분에 해당하는 toc의 <a>태그의 스타일을 바꿔주는 스크립트이다.

놀랍게도 벌써 _layouts/post.html 파일 수정이 끝났다.

3. Github 저장소에 Push하기

git add _layouts/post.html
git commit -m "feat(article): toc엘리먼트 및 스크립트 추가"
git push origin master

3. _scss/component/_post.scss 파일 수정하기

스타일을 아래와 같이 추가해준다.

.toc {
  position: absolute;
  right: 0px;
  width: 240px;
  color: $default;
  overflow-y: auto;
  overflow-x: hidden;
  padding-left: 0.75rem;
  padding-right: 0.75rem;
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
  margin-right: 0px;
  font-size: 0.7rem;
  border-left: 2px solid #e0d9e7;

  display: none;
  @media (min-width: 75em){
    width: 240px;
    display: block;
  }
  @media (min-width: 85em){
    width: 300px;
    display: block;
  }
  @media (min-width: 95em){
    width: 360px;
    display: block;
  }

  a.toc-active {
    font-weight: bold; 
    transition: all 0.125s ease-in 0s; 
    font-size: 0.75rem;
    color: #9075aa;
  }

  ul {
    list-style-type: none;
    margin-bottom: 0.1rem;
    padding-left: 0rem;
    li {
      padding-left: .5rem;
    }
  }

  a {
      color: $default;
      text-decoration: none;
  }
 
  a:hover {
     color:$theme-color;
  }
}

velog.io의 디자인과 최대한 비슷하게 만들어봤다.

여러분들 취항에 따라서 각자 수정하면 되겠다.

그리고 Github 저장소에 Push하자.

git add _scss/component/_post.scss
git commit -m "style(toc): toc 스타일 추가"
git push origin master

😨 이게 다야..?

여러분들 입맛대로 수정해보시길 추천한다!
내가 봐도 수정할 부분이 많으니, 각자 잘 수정해보자.
아무리 생각해도 본인이 작성한 코드가 더 낫다 싶으면 댓글에 남겨줬으면 좋겠다.