여러분 각주를 아시나요? 저는 좋아합니다. 맥락에 맞지 않는 TMI를 쓰고 싶을 때 그냥 각주로 우겨넣으면 되거든요. 이번 글에서는 그 각주, 특히 나무위키에서 사용 중인 마우스를 올리면 말풍선으로 표시해주는 그 방식을 HTML 기반에서 CSS와 자바스크립트로 어떻게 구현하는지 설명드리겠습니다.

시작하기에 앞서서 직접 보여드리는게 좋겠습니다. 이런1거예요.

※블로그가 점점 유지보수 되어감에 따라 모습이 변할 수도 있습니다.
기본 모습
기본 모습
마우스 올렸을 때 모습
마우스를 올렸을 때
각주
하단 각주의 모습
목차
  1. 원리
    1. 디자인
    2. 나타나게 하기
    3. 위치 계산
      1. onmouseover
  2. 위 내용도 자바스크립트로 대체
    1. 모바일 대응
  3. 마무리
설명 없이 그대로 복붙을 원하는 분들을 위한 코드 뭉치

[대괄호] 안에 들어간 부분은 직접 채워넣으셔야 합니다!

  • HTML
    • 본문에 넣는 부분
      <span class='footnote t' id='[본문아이디]' style='--left:[내용 가로의 절반 길이]'><!--
        --><a class='n' href='#[아래아이디]'>[숫자]</a><!--
        --><<-- 생략 가능 -->span>[내용]</span><!--
      --></span>

      가독성 때문에 주석으로 줄바꿈을 했지만 필수는 아닙니다. 다만 주석 없이 줄바꿈을 하면 본문이 줄바꿈돼버립니다.

    • 맨 아래 넣는 부분
      <div class='footnote b'>
        <div id='[아래아이디]'>
          <a class='n' href='#[본문아이디]'>1</a>
          <span>[내용]</span>
        </div>
      </div>
  • CSS
    .footnote .n{
      cursor: help;
      text-decoration: none;
      color: [숫자 색깔];
      margin-right:.1em;
    }
    .footnote.t .n{
      vertical-align: super;
      font-size:.8em;
      letter-spacing: -.1em;
    }
    .footnote.b .n{
      font-size: .8rem;
    }
    .footnote.t{
      display: inline;
      position: relative;
      scroll-margin-top: 20vh;
    }
    .footnote.b>div{
      scroll-margin-top: 20vh;
    }
    .footnote.b{
      border-top: .2rem solid [말풍선 색깔];
      padding-top: .3rem;
      padding-left: .6rem;
      margin-top: 2rem;
    }
    .footnote.b:not(.n){
      font-size: .8rem;
    }
    .footnote.t>:not(.n)::before{
      content: '';
      position: absolute;
      top: -.56rem;
      left: calc(var(--left,0px));
      width: 0;
      height: 0;
      border-style: solid;
      border-width: .3rem;
      border-color: transparent transparent [말풍선 색깔] transparent;
    }
    .footnote.t>:not(.n){
      display: none;
      z-index: 3;
      position: absolute;
      box-sizing: content-box;
      top: 1.25rem;
      left: calc(0px - var(--left,0px));
      background-color: [말풍선 색깔];
      padding: 1em;
      box-shadow: .18rem .18rem .47rem 0 rgba(0,0,0,.1);
      width: 90vw;
      max-width: max-content;
      white-space: pre-line;
    }
    @media (hover:hover){
      .footnote.t .n:hover + *{
        display: block;
      }
      .footnote.t>:not(.n):hover{
        display: block;
      }
    }
    .footnote.b>*>.n{
      float: left;
      clear: left;
    }
    .footnote.b>*>:not(.c-n){
      float: left;
      margin-left: .7em;
      max-width: calc(100% - 2em);
    }
    @media (hover:none){
      .footnote.t>:not(.n){
        padding-top:2rem
      }
    }
    .footnote.t .bar{
      display: block;
      margin-bottom: 1em;
      display: flex;
      justify-content: space-between;
      font-size: 1rem;
      background-color: [모바일 조작 바 색깔];
      position: absolute;
      top: 0;
      right: 0;
      width: 100%;
    }
    .footnote.t .bar a{
      border: hidden;
      cursor: pointer;
      text-align: center;
      width: 1.7em;
    }
    
  • 자바스크립트
    function footnote(obj){
      var position = obj.getBoundingClientRect().left;
      var balloon = obj.parentNode.querySelector(":not(.n)");
      var width = balloon.offsetWidth;
      var win = document.body.clientWidth;
      if(width >= win-20){
        balloon.style.width = (win-20) + "px";
        obj.parentNode.setAttribute("style","--left:"+(position-10)+"px;");
      } else if(width/2 > (position-10)){
        balloon.style.width = width + "px";
        obj.parentNode.setAttribute("style","--left:"+(position-10)+"px;");
      } else if(width/2 > (win-position-15)){
        balloon.style.width = width + "px";
        obj.parentNode.setAttribute("style","--left:"+(width-win+position+15)+"px;");
      } else {
        balloon.style.width = width + "px";
        obj.parentNode.setAttribute("style","--left:"+(width/2-10)+"px;");
      }
    }
    {
      let canHover = window.matchMedia("(hover:hover)").matches;
      let fnlist = document.querySelectorAll(".footnote.t");
      for(let i=0;i<fnlist.length;i++){
        let fnnum = fnlist[i].getElementsByClassName("n")[0];
        if(fnnum){
          let fnhref = fnnum.getAttribute("href");
          if(fnhref){
            if(!fnlist[i].querySelector(":not(.n)")){
              let fnid = document.getElementById(fnhref.replace("#",""));
              if(fnid){
                let fndetail = fnid.querySelector(":not(.n)");
                if(fndetail){
                  let fninner = fndetail.innerHTML;
                  if(fninner){
                    let span = document.createElement("span");
                    span.innerHTML = fninner;
                    fnlist[i].appendChild(span);
                  }
                }
              }
            }
            if(!canHover){
              let fnbar = document.createElement("span");
              fnbar.setAttribute("class","bar");
              fnbar.innerHTML = "<a href=\'"+fnhref+"\' onclick=\'this.parentNode.parentNode.style.display = \"none\"\;\'>↓</a><a onclick=\'this.parentNode.parentNode.style.display = \"none\"\;\'>×</a>";
              fnlist[i].querySelector(":not(.n)").prepend(fnbar);
            }
          }
          switch(canHover){
            case true: fnnum.addEventListener("mouseover",()=>{footnoteP(fnnum);}); break;
            case false:{
              fnnum.removeAttribute("href");
              fnnum.addEventListener("click",()=>{
                let fnbl = fnlist[i].querySelector(":not(.n)");
                if(fnbl) fnbl.style.display = "block";
                footnoteP(fnnum);
              });
            } break;
          }
        }
      }
    }

원리

크게 두 파트로 나눌 수 있습니다. 하나는 디자인과 마우스를 올리면 나타나는 기능, 다른 하나는 위치 계산입니다.

기본 구조

코드 같이 보기
  • 본문에 넣는 부분
    <span class='footnote t' id='[본문아이디]' style='--left:[내용 가로의 절반 길이]'><!--
      --><a class='n' href='#[아래아이디]'>[숫자]</a><!--
      --><<-- 생략 가능 -->span>[내용]</span><!--
    --></span>

    가독성 때문에 주석으로 줄바꿈을 했지만 필수는 아닙니다. 다만 주석 없이 줄바꿈을 하면 본문이 줄바꿈돼버립니다.

  • 맨 아래 넣는 부분
    <div class='footnote b'>
      <div id='[아래아이디]'>
        <a class='n' href='#[본문아이디]'>1</a>
        <span>[내용]</span>
      </div>
    </div>

HTML 상에서의 구조를 보시면 <span>으로 크게 감싸져 있고, 그 아래 숫자와 또 하나의 <span>이 들어있습니다.

이렇게 된 이유는 <span>이 inline 요소이기 때문입니다. 그 외 별 이유는 없어요. 저 안에 block 요소가 하나라도 들어가면 그 특성이 본문에도 영향을 미쳐 주석 바로 뒤에서 줄바꿈이 돼버립니다.2근데 <span> 태그로 한번 더 감싼 후(주석의 내용 부분처럼)에 CSS로 설정한 block이 들어오면 또 문제 없더라고요? 그래서 display:block 넣은 태그로 이렇게 문단 나누기 하고 있고요? 그러니까 직접 실험해보시면서 알아봐요 우리. 물론 CSS 상에서 display: inline을 해도 되지만 취향차이인 것 같습니다. 저는 <span>이 있기 때문에 그냥 쓴 거여서요.

숫자 부분은 class로 표시해뒀습니다. CSS로 조건 설정해서 생략할 수 있긴 한데 혹시 모를 특수한 상황이나 관리 편의성이나 생각하면 그냥 class 쓰는게 더 나을 것 같았습니다. vertical-align: super를 통해 위로 띄워주고 색을 연하게 해줬습니다. 저는 user-select:none;을 통해 혹시 본문을 긁어 복사할 때 각주 숫자까지 딸려가지 않도록 해줬는데 이건 취향이니 위 코드에 넣지는 않았습니다.

디자인

코드 같이 보기
.footnote .n{
  cursor: help;
  text-decoration: none;
  color: [숫자 색깔];
  margin-right:.1em;
}
.footnote.t .n{
  vertical-align: super;
  font-size:.8em;
  letter-spacing: -.1em;
}
.footnote.b .n{
  font-size: .8rem;
}
.footnote.t{
  display: inline;
  position: relative;
  scroll-margin-top: 20vh;
}
.footnote.b>div{
  scroll-margin-top: 20vh;
}
.footnote.b{
  border-top: .2rem solid [말풍선 색깔];
  padding-top: .3rem;
  padding-left: .6rem;
  margin-top: 2rem;
}
.footnote.b:not(.n){
  font-size: .8rem;
}
.footnote.t>:not(.n)::before{
  content: '';
  position: absolute;
  top: -.56rem;
  left: calc(var(--left,0px));
  width: 0;
  height: 0;
  border-style: solid;
  border-width: .3rem;
  border-color: transparent transparent [말풍선 색깔] transparent;
}
.footnote.t>:not(.n){
  display: none;
  z-index: 3;
  position: absolute;
  box-sizing: content-box;
  top: 1.25rem;
  left: calc(0px - var(--left,0px));
  background-color: [말풍선 색깔];
  padding: 1em;
  box-shadow: .18rem .18rem .47rem 0 rgba(0,0,0,.1);
  width: 90vw;
  max-width: max-content;
  white-space: pre-line;
}
@media (hover:hover){
  .footnote.t .n:hover + *{
    display: block;
  }
  .footnote.t>:not(.n):hover{
    display: block;
  }
}
.footnote.b>*>.n{
  float: left;
  clear: left;
}
.footnote.b>*>:not(.c-n){
  float: left;
  margin-left: .7em;
  max-width: calc(100% - 2em);
}
@media (hover:none){
  .footnote.t>:not(.n){
    padding-top:2rem
  }
}
.footnote.t .bar{
  display: block;
  margin-bottom: 1em;
  display: flex;
  justify-content: space-between;
  font-size: 1rem;
  background-color: [모바일 조작 바 색깔];
  position: absolute;
  top: 0;
  right: 0;
  width: 100%;
}
.footnote.t .bar a{
  border: hidden;
  cursor: pointer;
  text-align: center;
  width: 1.7em;
}

이 부분은 말풍선 꼬리 만드는 거 말고는 뭐 없습니다. 여기를 참고했어요. 핵심은 ::before에 테두리를 줘서 만드는 것입니다.

max-width: max-content는 중요합니다. 오래돼서 기억나지는 않는데 width에 바로 넣어버리면 너비 적용이 제대로 안됐던 것 같아요.

상위 요소에 overflow: visible은 필수입니다. 이걸 넣지 않으면 말풍선이 짤려요. 특정 요소에서 튀어나오지 않게 하려면 자바스크립트 쪽에서 값들을 좀 만지면 됩니다.

var(--left)라는 값을 이용하고 있습니다. 자바스크립트로 결정되는 말풍선의 위치값이라고 보시면 되는데요, 대충 '왼쪽으로 얼마나 이동시킬 것이냐' 라고 생각하시면 됩니다. 저 부분에 0이 들어오면 말풍선 꼬리가 가장 왼쪽에 있어요.

나타나게 하기

:hover를 이용합니다. .footnote.t .n:hover + *이 부분이 되게 중요한데요, 숫자 부분에 :hover가 붙어 있지만 뒤에 + 선택자를 이용해서 내용 부분에 display: block을 적용시킬 수 있게 됩니다. 당연히 말풍선 위에 마우스가 있을 때도 계속 보여지고 있어야 하니 내용 부분에도 :hover{display: none}을 적용해 줬습니다.

위치 계산

코드 같이 보기
function footnote(obj){
  var position = obj.getBoundingClientRect().left;
  var balloon = obj.parentNode.querySelector(":not(.n)");
  var width = balloon.offsetWidth;
  var win = document.body.clientWidth;
  if(width >= win-20){
    balloon.style.width = (win-20) + "px";
    obj.parentNode.setAttribute("style","--left:"+(position-10)+"px;");
  } else if(width/2 > (position-10)){
    balloon.style.width = width + "px";
    obj.parentNode.setAttribute("style","--left:"+(position-10)+"px;");
  } else if(width/2 > (win-position-15)){
    balloon.style.width = width + "px";
    obj.parentNode.setAttribute("style","--left:"+(width-win+position+15)+"px;");
  } else {
    balloon.style.width = width + "px";
    obj.parentNode.setAttribute("style","--left:"+(width/2-10)+"px;");
  }
}

가장 핵심 포인트입니다! 여기서 자바스크립트가 들어가는데요, 위 HTML 부분에서 함수를 호출한 이유가 여기 나옵니다.

간단한 과정은 이렇습니다.

  1. 말풍선 너비, 창 크기, 각주 위치(숫자 있는 곳)를 비교하기
  2. 각 상황별로 적절한 --left값을 전달
  3. --left를 기준으로 CSS 단에서 위치 결정

그래서 .getBoundingClientRect().left로 위치를 가져오고, .parentNode.querySelector(":not(.n)").offsetWidth로 말풍선 너비를, document.body.clientWidth로 창 크기를 가져와서 비교합니다.

조건과 대응은 이렇게 됩니다.

  • 말풍선 크기가 창 크기보다 클 때 → 창 크기를 줄이고 위치는 가운데로
  • 말풍선 크기가 창 크기보다는 작은데 왼쪽으로 튀어나가는 경우 → 튀어나간 만큼 오른쪽으로 치우치게
  • 말풍선 크기가 창 크기보다는 작은데 오른쪽으로 튀어나가는 경우 → 튀어나간 만큼 왼쪽으로 치우치게
  • 그 외 (말풍선 크기도 작고 튀어나가지도 않는 경우) → 가운데로

코드에서 20, 10 이런 숫자들이 등장하는 이유는 제가 임의로 여유 픽셀을 줬기 때문입니다. 길이에 의미가 있지는 않습니다.

onmouseover

{
  let tags = document.querySelectorAll(".footnote.t .n");
  for(let i=0;i<tags.length;i++) tags[i].addEventListener("mouseover",()=>{footnote(tags[i]);});
}

이전에는 일일이 onmouseover 속성을 넣어줬었는데요, 그럴 필요가 없었더라고요. 자바스크립트로 넣어줬습니다.

위 내용도 자바스크립트로 대체

코드 같이 보기
{
  let canHover = window.matchMedia("(hover:hover)").matches;
  let fnlist = document.querySelectorAll(".footnote.t");
  for(let i=0;i<fnlist.length;i++){
    let fnnum = fnlist[i].getElementsByClassName("n")[0];
    if(fnnum){
      let fnhref = fnnum.getAttribute("href");
      if(fnhref){
        if(!fnlist[i].querySelector(":not(.n)")){
          let fnid = document.getElementById(fnhref.replace("#",""));
          if(fnid){
            let fndetail = fnid.querySelector(":not(.n)");
            if(fndetail){
              let fninner = fndetail.innerHTML;
              if(fninner){
                let span = document.createElement("span");
                span.innerHTML = fninner;
                fnlist[i].appendChild(span);
              }
            }
          }
        }
        if(!canHover){
          let fnbar = document.createElement("span");
          fnbar.setAttribute("class","bar");
          fnbar.innerHTML = "<a href=\'"+fnhref+"\' onclick=\'this.parentNode.parentNode.style.display = \"none\"\;\'>↓</a><a onclick=\'this.parentNode.parentNode.style.display = \"none\"\;\'>×</a>";
          fnlist[i].querySelector(":not(.n)").prepend(fnbar);
        }
      }
      switch(canHover){
        case true: fnnum.addEventListener("mouseover",()=>{footnoteP(fnnum);}); break;
        case false:{
          fnnum.removeAttribute("href");
          fnnum.addEventListener("click",()=>{
            let fnbl = fnlist[i].querySelector(":not(.n)");
            if(fnbl) fnbl.style.display = "block";
            footnoteP(fnnum);
          });
        } break;
      }
    }
  }
}

사실 저는 이런 방식을 좋아하지 않습니다. 자바스크립트가 개입하기 전에, 아니, 개입하지 않고도 글의 구조가 HTML 상에서 완성되어 있어야 한다고 생각하거든요.

이런 계기3로 생각을 바꾸고 위쪽 각주 내용은 자바스크립트로 아래 각주의 내용을 가져다 붙이기로 했습니다.

이거 처음에는 반대 순서로 만들었었는데, 중복 각주에 대응하기 위해서 갈아엎었습니다. 중복 각주는 이MMM 말합니다.

이 부분의 자바스크립트는 반복문이 기본입니다. .footnote.t에 해당하는 모든 요소들을 찾아 반복해줍니다. 수많은 if들은 사실 에러 방지입니다. 스크립트가 뭔가 하나만 삑나도 전체 작동을 멈춰버려서 좀 꼼꼼하게 해놨습니다. 대충 위 .n이 가리키는 id을 찾아 그 내용을 복사해서 appendChild로 붙여넣는 방식입니다.

덤으로 mouseover 이벤트도 여기 합쳤습니다. 반복 두 번은 효율적이지 못한 거 같아서요.

모바일 대응

모바일은 hover가 안되니까 말풍선을 쓰지 않는 쪽으로 방치했었습니다. 누르면 대충 번호쪽으로 가니까 책처럼 직접 찾아봐라는 생각이었습니다. 근데 나무위키를 보다 보니까 이거처럼 모바일 UI도 만들고 싶더라고요. 그래서 만들었습니다.

일단 window.matchMedia()로 hover를 체크합니다. 굳이 이 방식을 택한 이유는 css의 미디어쿼리와 결과를 일치시키기 위함입니다. 그 후 hover가 안되는 녀석들은 prepend()를 통해 아래쪽 각주 보기 버튼과 닫기 버튼을 만들어줬습니다. 중간의 복잡한 문자열이 바로 그것입니다.

mouseover 이벤트도 click으로 바꿔줬습니다.

마무리

맨 위에 복붙용으로 코드를 올려놓긴 했지만 본문만 보고는 만들 수 없는 형태가 되어버렸네요. 죄송합니다. 이게 다시 쓰는 거라 디테일한 분량까지 챙기기는 너무 힘들었어요. 그래도 이 미루고 미루던 숙원사업을 해내서 다행입니다.

다시 쓰기 전

그 모습은 이렇습니다.1이런 방식입니다.

※블로그가 점점 유지보수 되어감에 따라 모습이 변할 수도 있습니다.
기본 모습
기본 모습
마우스 올렸을 때 모습
마우스를 올렸을 때
각주
하단 각주의 모습

우선 제가 사용한 방법을 보여드리겠습니다.

FootNote a
{
  cursor:help;
  vertical-align: super;
  text-decoration: none;
  font-size:small;color:rgba(0,0,0,.5);
  margin-right:.1em;
  -webkit-user-select: none !important;
  -moz-user-select: -moz-none !important;
  -ms-user-select: none !important;
  user-select: none !important;
}
.FootNote.Top
{
  display:inline;
  position:relative;
  padding-top:20vh; margin-top:-20vh;
}
.FootNote.Bot>* { padding-top:20vh; margin-top:-20vh; }
.FootNote.Bot
{
  border-top:1px solid rgba(0,0,0,.3);
  padding-top:5px;padding-left:10px;
  margin-top:30px;
}
.FootNote.Bot:not(a) { font-size:small; }
.FootNote.Top a:hover+* { display:block; }
.FootNote.Top>:not(a):hover { display:block; }
.FootNote.Top>:not(a)
{
  z-index:3; position:absolute;
  display:none;
  box-sizing:border-box;
  top:clac(20vh + 20px); left:0;
  background-color:#ffe7ee;
  width:auto; padding: 1em;
  box-shadow:3px 3px 7.5px 0 rgba(0,0,0,.1);
  white-space:nowrap;
}
.FootNote.Top>:not(a)::before
{
  content:'';
  position:absolute;
  top:-9px;left:0;
  width:0;height:0;
  border-style:solid;border-width:5px;
  border-color:transparent transparent #ffe7ee transparent;
}

복사 편하게 하시라고 한번에 올려봅니다. CSS 스크립트구요. 차근차근 설명드리겠습니다.

FootNote a
{
  cursor:help;
  vertical-align: super;
  text-decoration: none;
  font-size:small;color:rgba(0,0,0,.5);
  margin-right:.1em;
  -webkit-user-select: none !important;
  -moz-user-select: -moz-none !important;
  -ms-user-select: none !important;
  user-select: none !important;
}

 이 부분은 각주의 작은 숫자부분을 디자인합니다.

cursor: help;커서가 물음표 달린 것으로 변합니다.
vertical-align: super;숫자 위치를 위로 올립니다.
text-decoration: none;하이퍼링크식 디자인(밑줄)을 없앱니다.
font-size:small; color:gray;숫자 크기를 작게 하고 회색으로 만듭니다.
margin-right: .1em;각주 숫자 옆에 글자가 너무 붙길래 오른쪽 여백을 만들어 줬습니다.
-webkit-user-select: none !important;
-moz-user-select: -moz-none !important;
-ms-user-select: none !important;
user-select: none !important;
글을 드래그해서 복사할 때 같이 복사되지 않도록 해줍니다.

.FootNote.Top
{
  display:inline;
  position:relative;
  padding-top:20vh; margin-top:-20vh;
}
display:inline;각주가 본문을 줄바꿈시키지 않도록 합니다.
position:relative;말풍선이 부모 위치를 기준으로 하도록 합니다.
padding-top:20vh; margin-top:-20vh;각주를 클릭해서 이동할 시 목적지 각주가 화면의 약 80%에 위치하도록 합니다.
.FootNote.Bot>* { padding-top:20vh; margin-top:-20vh; }
.FootNote.Bot
{
  border-top:1px solid rgba(0,0,0,.3);
  padding-top:5px;padding-left:10px;
  margin-top:30px;
}
.FootNote.Bot:not(a) { font-size:small; }

 이번엔 아래에 있는 각주를 디자인합니다.

.FootNote.Bot>* { padding-top:20vh; margin-top:-20vh; }마찬가지로 각주 클릭 이동시 위치를 내립니다.
border-top:1px solid rgba(0,0,0,.3);각주와 본문을 구분하는 선입니다.
padding-top:5px;padding-left:10px;
margin-top:30px;
여백입니다.
.FootNote.Bot:not(a) { font-size:small; }각주 내용 부분의 글자를 작게 합니다.
.FootNote.Top a:hover+* { display:block; }
.FootNote.Top>:not(a):hover { display:block; }
.FootNote.Top>:not(a)
{
  z-index:3; position:absolute;
  display:none;
  box-sizing:border-box;
  top:clac(20vh + 20px); left:0;
  background-color:#ffe7ee;
  width:auto; padding: 1em;
  box-shadow:3px 3px 7.5px 0 rgba(0,0,0,.1);
  white-space:nowrap;
}
.FootNote.Top>:not(a)::before
{
  content:'';
  position:absolute;
  top:-9px;left:0;
  width:0;height:0;
  border-style:solid;border-width:5px;
  border-color:transparent transparent #ffe7ee transparent;
}

 여기가 가장 중요한 말풍선 부분입니다.

.FootNote.Top a:hover+* { display:block; }각주에 마우스를 올리면 말풍선이 나타납니다.
.FootNote.Top>:not(a):hover { display:block; }각주 내용에 마우스가 머물러 있으면 말풍선을 계속 표시합니다.
.FootNote.Top>:not(a)
z-index:3; position:absolute;말풍선을 다른 글들 위로 올립니다.
display:none;평소에는 말풍선이 보이지 않습니다.
box-sizing:border-box;솔직히.. 왜 넣었는지 기억은 나지 않습니다.. 아마 말풍선 크기 때문일 거 같습니다.
top:clac(20vh + 20px);left:0;아까 위치 잡는다고 패딩 줬던 거에 말풍선 꼬리만큼 내립니다.
background-color:#ffe7ee;바탕색입니다.
width:auto; padding: 1em;내용에 따라 너비를 바꿉니다. 여백을 줍니다.
box-shadow:3px 3px 7.5px 0 rgba(0,0,0,.1);그림자입니다.
white-space:nowrap;글이 자동으로 줄바꿈 되지 않게 합니다. 이걸 안하면 너비가 엄청 좁게 책정되더라구요.

이제 꼬리입니다.

.FootNote.Top>:not(a)::before꼬리가 풍선 위에 있기 때문에 before를 사용했습니다.
content:'';아무것도 없는 무언가를 만듭니다.
position:absolute;
top:-9px;left:0;
위치를 잡습니다.
width:0;height:0;
border-style:solid;border-width:5px;
border-color:transparent transparent #ffe7ee transparent;
외곽선만을 이용해서 말풍선 꼬리를 만듭니다. 정확한 원리는 여기를 참고했습니다.

우와! 이제 디자인은 끝났습니다! 이제 HTML로 작성하기만 하면 되는데, 본문 부분과 아래 각주 부분을 따로 작성합니다.

<span class="FootNote Top" id="아무거나겹치치않는거1">
  <a href="#아무거나겹치치않는거2">보통 여기 숫자가 들어갑니다</a>
  <span>말풍선 안에 들어갈 내용</span>
</span>

(예시처럼 줄바꿈을 하신다면 각주가 다른 글자들과 거리두기를 할 것입니다. 하지 않으시거나 줄바꿈을 주석처리 해주세요)

이제 문서의 마지막에 이것을 추가해 줍니다.

<div class="FootNote Bot">
  <div id="아무거나겹치치않는거2">
    <a href="#아무거나겹치치않는거1">보통 여기 숫자가 들어갑니다</a>
    <span>각주의 내용</span>
  </div>
</div>

 여기까지 달려오신 여러분 축하드립니다! 이제 내 블로그에도 주석을 달 수 있어요!
 그런데 나무위키는 주석이 위에 달리는데 이건 왜 밑에 달리냐구요?

 ...

고이즈미 신지로 "그것이 ~니까"
일본의 어느 섹-시한 장관
 그것이.. 내.. 한계이기.. 때문에..!

이후 변경사항(위 본문에는 반영되지 않음)
  • css에서 transform 제거. 이 속성을 각각의 문서에서 따로 수정하는 방법 사용.
  • 유연성을 위해 다양한 경우의 수를 포함할 수 있도록 특정 태그만 지정하는 부분 변경.
  • 각주 이동 위치를 내리는 margin 값 때문에 윗 여백에도 hover 효과 적용 되는 것 수정하기 위해 id값을 a 태그에서 상위 태그로 이동.
  • 하단 각주가 2개 이상일 시 아래의 각주에 겹쳐 윗 각주 클릭 안되는 문제 발생. 각 각주를 감싸는 div 태그를 추가하고 id를 이동.
  • 말풍선 내부의 <a> 태그의 배경을 하얗게 하기 위해서 따로 클래스를 부여하는 방식에서 반대로 각주 숫자에 클래스를 부여하는 방식으로 변경.
  • 하단 각주에서 줄바꿈시 정렬을 위해 내용 부분을 div태그로 전환하고 숫자와 내용 모두에 float:left; 속성을 넣었습니다.
  • 'var()'(css 변수)를 이용하여 style 태그의 내용을 css 코드 쪽으로 이동시키고 각주마다 인라인 style에 변수를 입력하는 형태로 변경되었습니다.
  • 말풍선 부분 white-space를 pre-line으로 바꾸고, width를 calc(100vw - 5rem), 그리고 max-width: max-content를 추가하여 화면이 작아질 때 각주 내용이 잘리는 현상을 해결했습니다.
  • 자바스크립트를 이용해서 자동으로 배치 혹은 크기조절이 될 수 있게 만들었습니다.
    이제는 위에 적혀있는 옛날 코드의 한계를 너무 잘 알게 됐습니다. 나중에 시간 되면 최신버전으로 업데이트 해야겠습니다.
1
이런 방식입니다.
1 이런 방식입니다.
2 근데 <span> 태그로 한번 더 감싼 후(주석의 내용 부분처럼)에 CSS로 설정한 block이 들어오면 또 문제 없더라고요? 그래서 display:block 넣은 태그로 이렇게 문단 나누기 하고 있고요? 그러니까 직접 실험해보시면서 알아봐요 우리.
M 같은 내용을 보여줍니다.
3 시작은 모바일 환경에 대한 고민이었습니다. 모바일은 hover가 안 되니까 onclick으로 바꾸고 전용 UI를 따로 디자인해야 하나? 이런 고민을 하고 있었는데요, 이 당시 저는 css를 통해서 이미 모바일에서는 hover가 작동하지 않도록 만들어 놓은 상황이었습니다. 스크롤 할 때 자꾸 걸리더라고요. 근데 그러고보니 이렇게 되면 모바일에서는 주석 말풍선 내용이 없는 것이나 다름없잖아요? 결국 각주의 '기본' 구성은 '번호를 누르면 페이지 아래쪽 같은 번호를 가진 문항을 찾아서 보여주는 것'이었습니다. 그러니까 말풍선은 기본 구성에 포함되지 않은 것이죠.