콘텐츠로 이동

2021-03-24

90년생 HTML

  • 이후 웹의 폭발적 성장 - HTML / CSS / JS - 정보 나타냄 / 정보 꾸며줌 / 정보를 움직이게 함
  • HTML 약속 - HyperText Markup Language - 하이퍼텍스트(링크)를 중요한 특징으로 가지는 마크업 형식의 언어 - 정보를 태그로 나타내기!
    <h1> I am a web developer </h1>
    

    - \

    : 여는 태그, \

    : 닫는 태그 - I am ~: 콘텐츠 - 전체: 엘리먼트

    - 태그 150개 이상... 자주 쓰는 것만 알아보고, 나머진 찾아쓰자

DOM

  • 필요성 - JS를 통해 html의 데이터를 가져올 수 있음 - 웹페이지에 보여지는 데이터를 변경할 수 있음 - 인터랙티브한 웹 어플리케이션 만들 수 있음 - 동적인 기능이 있는 웹어플리케이션을 만들어보자!
  • 정의 - Document Object Model의 약자 - 프로그래밍 언어가 DOM 구조에 접근할 수 있는 방법을 제공해, 문서 구조/스타일/내용을 변경할 수 있게 한다
  • DOM API 사용하기 - 엘리먼트 하나 가져오기 - getElement, querySelector - getElementById - querySelector - 엘리먼트 여러 개 가져오기 (HTML Collection 반환) - getElements, querySelectorAll - getElementsByTagName - getElementsByClassName - querySelectorAll
  • 정리 - DOM은 HTML을 위한 API로, HTML을 "탐색"할 수 있고, 구조를 "변경"할 수 있음

BOM

  • 필요성 - 유저에게 경고창 띄워주기 - 유저의 yes/no 선택에 따른 응답 보여주고 싶은 경우 - 유저가 브라우저 나갈 때 진짜 나갈건지 물어보고 싶은 경우 - 유저가 접속한 환경을 알고 싶은 경우 - 현재 url 위치, 접속 history를 알고 싶은 경우!
  • 정의 - BOM은 웹 브라우저 환경의 다양한 기능을 객체처럼 다루는 모델 - 웹 브라우저의 버튼, URL 주소 입력 창, 타이틀 바 등 웹브라우저 일부분을 제어
  • 대표 BOM - window: 브라우저 창 객체 - screen: 사용자 환경의 디스플레이 정보 객체 - location: 현재 페이지의 url을 다루는 객체 - navigator: 브라우저 환경 정보 객체 - history: 현재 브라우저가 접근했던 URL history
  • 정리 - BOM은 웹 브라우저의 기능을 객체처럼 다루는 모델 - BOM의 API 활용하면, UX 올라감

DOM & BOM 테코톡

  • DOM - JS가 HTML 어케아누?... DOM을 통해! - 문서에 대한 모든 내용을 담고 있음 - "HTML 요소간의 부자 관계를 반영하여 트리 구조로 구성한 것"__
  • DOM API를 통해 노드 취득 - getElement~, querySelect~
    - 여러개 라면 HTMLCollection, NodeList 등을 반환 - 유사 배열... 배열 변환 후 사용을 권장
  • DOM API를 통해 노드 추가 - innerHTML: 장) 쉬움, 단) 한번만 삽입할 것 - insertAdjacentHTML: 장) 기존요소 제거 X, 단) 크로스 사이트 스크립팅 공격에 취약 - appendChild: 장) 보안이슈 읎음 단) 새로운 자식 노드 생성 필수
  • 렌더링 과정 - HTML 순차적 탐색하면서 DOM 생성해나감 - link CSS 만남... CSSOM 생성함 - DOM + CSSOM 합쳐서 렌더 트리 만듦 - Script JS 만남... 렌더링 엔진 -> JS 엔진으로 제어권 넘김 - 만약 JS에서 DOM / CSSOM 건드리면, reflow, repaint 불가피 - 질문! - CSS 상단에 있는 이유? - 외부 스타일 시트 최대한 빠르게 다운 받기 위해 - Script 하단에 있는 이유? - HTML 파서는 script 태그를 만나면 파싱 멈추고 스크립트 읽기에 로딩 늦어짐 - 생성안된 DOM 읽거나 조작하는거 불가능해서 오류 발생할 수 있음
  • 렌더링 문제점 - 동적 UI 관리 약함... 성능 이슈 - DOM은 빠른데, css재구성/리페인트가 오래걸림 - SPA에서 비효율적
  • Virtual DOM - 바뀐 부분만 실제 DOM에 적용 - 불필요한 렌더링 횟수 줄임
  • BOM - 웹 브라우저 환경의 다양한 기능을 객체처럼 다룸 - DOM은 BOM에 포함됨 - window - document - history - location - navigator - screen

Event

  • 필요성 - Interactive 한 사용자 경험!
  • 정의 - 웹 탐색과정에서 사용자가 ~했을 때를 담당 - ~했을 때에 대한 이벤트를 다루도록 한다!
  • 종류 - 사용자 인터페이스 이벤트 - load: 웹 페이지 로드 되었을 때 - unload: 새로운 페이지르 요청했을 때 - error: 브라우저가 JS 오류를 만났거나, 요청한 자원이 없을 때 - resize: 브라우저 창의 크기 조정했을 때 - scroll: 사용자가 페이지 위아래로 스크롤할 때

    - 키보드 이벤트 - keydown: 사용자가 키 처음 눌렀을 때 - keyup: 사용자가 키를 뗄 때 - keypress: 사용자가 키 누르고 뗴어서 문자 화면에 표시 될 떄

    - 마우스 이벤트 - click: 마우스 클릭했을 때 - dblclick: 마우스 더블 클릭했을 때 - mousedown: 마우스 누르고 있을 때 - mouseup: 마우스 뗄 때 - mousemove: 마우스 움직일 때 - mouseover: 요소 위로 마우스를 움직였을 때 - mouseout: 요소 바깥으로 마우스 움직였을 때

    - focus와 blur - link, form에서 포커스를 줄 수 있음 - 사용자가 form의 요소들과 상호작용(도움말/피드백 등) - 유효성 검사 수행 등 - focus: HTML 엘리먼트가 포커스를 얻음 - blur: HTML 엘리먼트가 포커스를 잃음 - focusin: HTML 엘리먼트가 포커스를 받고 있는 중일 떄 - focusout: HTML 엘리먼트가 포커스를 잃음

  • 이벤트 핸들러 - element.addEventListener(이벤트, 코드[, options]); - 매개변수를 전달 하고자 하면, 다음과 같은 익명함수를 이용하자
    $stationAddButton.addEventListener('blur', function() {
        onAddStationHandler(name);
    })
    
  • 이벤트 위임 - 이벤트 많다 == 페이지 실행 속도 느리다 - 효율적 이벤트 관리 위해, 자식 요소를 포함할 수 있는 요소에 이벤트 핸들러를 지정하고, 이벤트의 흐름을 이용해 다룰 수 있음 - 장점 - 동적으로 추가되는 "새로운" 요소에도 동작 - 코드가 간결 - 예시
    <ul id="parent-list">
        <li id="item1">Item 1</li>
        <li id="item2">Item 2</li>
        <li id="item3">Item 3</li>
    </ul>        
    
    document.getElementById("parent-list").addEventListener("click", function(e) {
        if (e.target && e.target.nodeName == "LI") {
            console.log(`List item ${e.target.id} was clicked!`);
        }
    });
    

hashCode와 equals의 동치성 보장

  • 리뷰어 미립과의 대화!
  • 문제 상황: 나의 Pawn 클래스에서 오버라이딩한 hashCode와 equals 메서드가 동치성을 보장하지 않는다?! - 나는 하는 줄 알았는데... - 현재 코드
    @Override
    public int hashCode() {
      return PAWN_UNICODE_DECIMAL * direction;
    }
    
    @Override
    public boolean equals(Object obj) {
      if (obj == null) return false;
      if (this == obj) return true;
      return getClass() == obj.getClass();
    }
    
  • 1. hashCode 는 같지만 equals 는 같지 않은 케이스는 발생 할 수 없을까요? - 발생할 수 있어요.
    현재 Pawn 클래스의 모든 인스턴스는 hashCode로 9817을 가지고 있어요.
    Integer 클래스 같은 경우, 정수값 그 자체가 hashCode로 사용되어요.
    만약 다음과 같은 코드를 작성하면, 두 객체의 hashCode의 값은 같아요.

    facade

    - 다만 Integer객체와 Pawn 객체의 equals는 달라요.
    HashSet에 둘이 각각 저장된 것이 그것의 반증이에요.
    객체를 HashSet에 넣을 때 hashCode/equals를 비교하여 같다면 같은 키값으로 인지하는데,
    해당 테스트 코드에서는 둘을 다른 객체로 인지했으니까요.

  • 2. 근데 hash 값과 eqauls 가 다른 방식으로 구현되어 있어 수정 중 둘의 동치성이 깨진다면 hashSet에서 어떤 문제가 발생할까요? - hashSet은 hash값과 equals가 같은경우, 같은 Key값으로 인지해요.
    만약 둘 중 하나가 다르다면, hashSet/Map에 key값이 각각 저장될 것이에요.
    각각 저장된 두개의 Key가 사실 같은 객체였어야 한다면, 다음과 같은 문제가 발생해요.
    고유한 Key값에 대응하는 Value를 저장하고 싶어 hashMap을 사용하는데, 이것이 보장이 안될 것이에요.
  • 3. 또한 hash 값의 중복이 발생한다면 또 어떻게 될까요? - 우선 hash 값의 중복이 발생하면, hash 충돌이 일어나요.
    hash 충돌이 일어나면, 해당 hash 값을 저장하는 버킷에 하나 이상의 데이터가 저장되어야 해요.
    만약 hash 충돌이 너무 자주일어나, 하나의 버킷에 주렁주렁 데이터가 저장된다면,
    O(1)의 속도를 보장하기 때문에 쓰려고 했던 hashMap/hashSet의 성능을 매우 저하시킬 것이에요.

    - https://papimon.tistory.com/74
    얼마 전, HashMap/HashSet의 출력이 순서를 가지는 것 같아 공부를 하던 중 발견한 것들을 제 블로그에 정리했었어요.
    이때 참고했던 포스팅이 https://d2.naver.com/helloworld/831311 이거였어요.
    해당 포스팅에서는 hashCode가 충돌하는 데이터를 다음과 같이 처리한다고 명시했어요.
    기존 Java 7까지는 hash 충돌이 발생한 버킷에 대해, LinkedList로 데이터를 이어 저장해요.
    이는 탐색에 최악 O(N)의 시간복잡도를 가져요.
    Java 8 부터는 RBtree를 통한 데이터 저장으로 최악 O(logN)의 시간복잡도를 가지게 했다고 이해했어요.

  • 이제 내 코드의 문제점! - 현재 저의 Pawn의 hashCode, equals 오버라이딩 된 코드에요.
    Pawn의 direction에 따라 다른 hashCode를 반환하는 대신,
    equals는 그저 같은 클래스라면 동등성을 보장하도록 제가 코드를 작성해놨어요.
    여기에서 제가 위에 정리한 문제들이 발생할 수 있을것 같아요!
    ``` Java
    @Override
    public int hashCode() {
        return PAWN_UNICODE_DECIMAL * direction;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Pawn pawn = (Pawn) o;
        return direction == pawn.direction;
    }
    ```
    

    - 해당 방식으로, direction이 같은 경우 동등성을 보장하도록 코드를 작성하는게,
    hashCode와 equals의 동작을 동일하게 가져가는 방향이겠네요!

    - 미립의 답변! - 위처럼 사용자정의로 equals 과 hashCode 를 재정의하면 알 수 없는 사이드 이펙트가 발생 할 수 있기때문에 (사람이 하는 일이라 😭) 신중하게 접근하면 좋을 것 같아요

객체 생성을 캡슐화 하는 것

체스에 필요한 팀 객체를 인스턴스화 하기 위해 나는 그동안 Team 클래스의 생성자를 통해 각 팀을 생성해주었다.

public Team(final PiecePosition piecePosition, final PieceCaptured pieceCaptured, final Score score) {
    this.piecePosition = piecePosition;
    this.pieceCaptured = pieceCaptured;
    this.score = score;
}
하지만 생각해보니까, 체스에는 블랙팀, 화이트팀만 있으면 된다.
그러면 굳이 생성자를 열어두지 말고(물론 열어줘도 됨),
BlackTeam을 만드는 정적 팩토리 메서드와 WhiteTeam을 만드는 정적 팩토리 메서드를 만들어 보는 것은 어떨까?
private Team(final PiecePosition piecePosition, final PieceCaptured pieceCaptured, final Score score) {
    this.piecePosition = piecePosition;
    this.pieceCaptured = pieceCaptured;
    this.score = score;
}
public static Team blackTeam() {
    return new Team(PiecePosition.initBlackPosition(), new PieceCaptured(), new Score());
}
public static Team whiteTeam() {
    return new Team(PiecePosition.initWhitePosition(), new PieceCaptured(), new Score());
}
즉, 생성자를 private으로 숨기고,
blackTeam()이라는 정적 팩토리 메서드를 통해, 블랙팀에 필요한 정보를 담아 Team 객체를 생성하여 반환하고,
whiteTeam()이라는 정적 팩토리 메서드를 통해, 화이트팀에 필요한 정보를 담아 Team 객체를 생성하여 반환하면,
체스에 필요한 팀 생성의 역할을 Team 클래스가 완벽히 수행하면서 + 지정된 팀(블랙팀/화이트팀) 만 생성하도록 강제할 수 있다.

해당 방식으로 코드를 리팩토링 했더니,
Team 생성에 필요한 정보를 생성자를 통해 외부로 공개하지 않고,
체스에 필요한 팀을 만드는 역할을 충분히 하게 되었다.

사용할 객체를 캐싱해 둔다는 것

반복적으로 사용되는 인스턴스의 경우 캐싱을 통한 성능 향상이 가능하다.
저번 로또 미션 같은 경우 다음과 같이 45개의,
“변하지 않는 int 값”을 인스턴스 필드로 가지는 LottoBall 인스턴스를 생성해 두었고,
나 같은 경우, 배열에 저장을 해두었다.

private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 45;

private static final LottoBall[] lottoBalls = new LottoBall[MAX_NUMBER];

static {
    for (int i = MIN_NUMBER; i <= MAX_NUMBER; i++) {
        lottoBalls[i - MIN_NUMBER] = new LottoBall(i);
    }
}
다음과 같이 필요할 때 마다 미리 만들어둔 배열에 접근하여 필요한 인스턴스를 반환하는 것을 알 수 있다.
public static LottoBall valueOf(final int number) {
    validateLottoBall(number);
    return lottoBalls[number - MIN_NUMBER];
}
이번 체스 미션에서도 그러면,
블랙팀/화이트팀의 초기 기물 위치가 항상 똑같으니,
매번 필요할 때 마다 새로운 PiecePosition 인스턴스를 생성하는 것이 아니라,
blackPiecePosition과 whitePiecePosition을 캐싱해두는 것은 어떨까?
private static final int BLACK_PAWN_COLUMN = 6;
private static final int BLACK_PAWN_DIRECTION = -1;
private static final int BLACK_PIECE_COLUMN = 7;

private static final int WHITE_PAWN_COLUMN = 1;
private static final int WHITE_PAWN_DIRECTION = 1;
private static final int WHITE_PIECE_COLUMN = 0;

private static final PiecePosition blackPiecePosition;
private static final PiecePosition whitePiecePosition;

static {
    blackPiecePosition = new PiecePosition(BLACK_PAWN_COLUMN, BLACK_PAWN_DIRECTION, BLACK_PIECE_COLUMN);
    whitePiecePosition = new PiecePosition(WHITE_PAWN_COLUMN, WHITE_PAWN_DIRECTION, WHITE_PIECE_COLUMN);
}

private final Map<Position, Piece> piecePosition;

private PiecePosition(final int pawnColumn, final int pawnDirection, final int pieceColumn) {
    piecePosition = new HashMap<>();
    initializePawn(pawnColumn, pawnDirection);
    initializePiece(pieceColumn);
}

public static PiecePosition initBlackPosition() {
    return blackPiecePosition;
}

public static PiecePosition initWhitePosition() {
    return whitePiecePosition;
}
하지만 안타깝게도 해당 방식으로 코드 작성 후 테스트 코드를 돌리면 다 틀린다.
이유가 뭘까? 생각해보자.
1번 테스트코드에서 initBlackPosition()으로 캐싱되어 있던 blackPiecePosition 객체를 반환 받아 테스트에 사용한다.
기물이 정상적으로 움직이는지 확인한다.

이후 2번 테스트코드에서 initBlackPosition()으로 캐싱되어 있던 blackPiecePosition 객체를 반환 받는다.
이때 넘겨 받은 PiecePosition 객체 내의 Map 인스턴스 필드는,
1번 테스트 코드에 의해 이미 기물 이동이 이루어진 상태이다.

PiecePosition 객체 내의 Map는 final로 선언되어 마치 불변인 인스턴스 일 것 같아 보이지만,,,
해당 인스턴스 변수의 final이 지칭하는 것은 Map이 저장되어 있는 위치가 불변이라는 것이지
Map이 저장하고 있는 key-value값이 변하지 않는 다는 말이 아니기 때문이다.

PiecePosition 클래스에서 제공하는 movePiece()와 같은 로직은 기물을 움직여 인스턴스 필드인 Map 에 저장해두기에,
해당 동작이 캐싱한 blackPiecePosition 객체에 적용이 되었다면,

public final void movePiece(final Position current, final Position destination) {
    final Piece chosenPiece = choosePiece(current);
    piecePosition.remove(current);
    piecePosition.put(destination, chosenPiece);
    chosenPiece.moved();
}
캐싱해뒀을 때 목적에 (BlackPiecePosition은 초기에 항상 같은 위치에 있으니, 이 정보를 캐싱해둬야지!)
어긋난 상태값을 가지게 된다.

그러면 각 팀 별로 초기 기물 위치를 저장해둔 Map black/whiteInitPiecePosition을 만들어,

private static final Map<Position, Piece> blackInitPiecePosition = new HashMap<>();
private static final Map<Position, Piece> whiteInitPiecePosition = new HashMap<>();

static {
    for (int i = 0; i < 8; i++) {
      blackInitPiecePosition.put(new Position(i, BLACK_PAWN_COLUMN), new Pawn(BLACK_PAWN_DIRECTION));
    }
    blackInitPiecePosition.put(new Position(0, BLACK_PIECE_COLUMN), new Rook());
    blackInitPiecePosition.put(new Position(1, BLACK_PIECE_COLUMN), new Knight());
    blackInitPiecePosition.put(new Position(2, BLACK_PIECE_COLUMN), new Bishop());
    blackInitPiecePosition.put(new Position(3, BLACK_PIECE_COLUMN), new Queen());
    blackInitPiecePosition.put(new Position(4, BLACK_PIECE_COLUMN), new King());
    blackInitPiecePosition.put(new Position(5, BLACK_PIECE_COLUMN), new Bishop());
    blackInitPiecePosition.put(new Position(6, BLACK_PIECE_COLUMN), new Knight());
    blackInitPiecePosition.put(new Position(7, BLACK_PIECE_COLUMN), new Rook());

    for (int i = 0; i < 8; i++) {
      whiteInitPiecePosition.put(new Position(i, WHITE_PAWN_COLUMN), new Pawn(WHITE_PAWN_DIRECTION));
    }
    whiteInitPiecePosition.put(new Position(0, WHITE_PIECE_COLUMN), new Rook());
    whiteInitPiecePosition.put(new Position(1, WHITE_PIECE_COLUMN), new Knight());
    whiteInitPiecePosition.put(new Position(2, WHITE_PIECE_COLUMN), new Bishop());
    whiteInitPiecePosition.put(new Position(3, WHITE_PIECE_COLUMN), new Queen());
    whiteInitPiecePosition.put(new Position(4, WHITE_PIECE_COLUMN), new King());
    whiteInitPiecePosition.put(new Position(5, WHITE_PIECE_COLUMN), new Bishop());
    whiteInitPiecePosition.put(new Position(6, WHITE_PIECE_COLUMN), new Knight());
    whiteInitPiecePosition.put(new Position(7, WHITE_PIECE_COLUMN), new Rook());
}
블랙팀/화이트팀 초기 PiecePosition 객체를 생성할 때마다,
미리 캐싱해둔 Map black/whiteInitPiecePosition에 대한 참조를 new HashMap<>( black/whiteInitPiecePosition); 통해 끊고,
이를 인스턴스 필드인 Map 에 복사하여,
객체를 생성하여 반환하면 어떨까?
private final Map<Position, Piece> piecePosition;

private PiecePosition(Map<Position, Piece> cachedPosition) {
    piecePosition = cachedPosition;
}

public static PiecePosition initBlackPosition() {
    return new PiecePosition(new HashMap<>(blackInitPiecePosition));
}

public static PiecePosition initWhitePosition() {
    return new PiecePosition(new HashMap<>(whiteInitPiecePosition));
}
테스트를 돌렸다. 이번에도 안된다. 이유는 무엇일까?
이번에는 놀랍게도 윗 문장의,
[이를 인스턴스 필드인 Map 에 “복사”하여]
중 “복사”에서 문제가 발생한다.
HashMap을 복사하여 새로운 Map을 만드는 과정에서, (그니까 new HashMap<>(~~); 에서)
Key-value 인 Position과 Piece는 각각 얕은 복사 가 이루어진다.
/**

 * Implements Map.putAll and Map constructor.
 *

 * @param m the map
 * @param evict false when initially constructing this map, else
 * true (relayed to method afterNodeInsertion).
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey(); // 여기여기!!!
            V value = e.getValue(); // 여기여기!!
            putVal(hash(key), key, value, false, evict);
        }
    }
}
얕은 복사란, 객체의 참조 만을 복사한다는 것으로,
new HashMap<>()에 map을 파라미터로 넣는 방식으로는,
Position과 Piece에 대해 참조만이 복사가 된다.

그것이 무엇이 문제인가 하니...

Piece의 경우, isFirstMove를 필드 값으로 가지는 가변 객체이다.
해당 필드는 Pawn의 첫 행마(2칸 이동가능)과 King과 Rook의 캐슬링에서 사용된다.

1번 테스트 코드에서 초기화된 PiecePosition의 Map에서의 Pawn은
최초 생성되었으니 isFirstMove가 true일 것이다.
하지만 1번 테스트 코드에서 Pawn이 이동하게 되면, isFirstMove가 false로 바뀌게 된다.

이 상태에서, 2번 테스트 코드가 PiecePosition을 받아보았을 때,
1번 테스트 코드에서 이동시킨 pawn의 경우,
Map 상으로는 알맞은 초기 위치에 있겠지만,
Pawn의 isFirstMove 값은 false인 상태로 초기화 되는 것이었다.
(참조가 끊기지 않고 그저 복사만 한 후 PiecePosition을 생성해 반환하니까)