Post

JSON 서버 실습

JSON 서버 실습

JSON 요청 실습

  • JSON을 요청하는 서버 실습을 진행하자.
  1. 실습을 위한 projecet, api 폴더를 생성한다.
  2. projecet에서 json-server를 설치한다.

    Image

  3. api 에서 db.json을 생성하여, 다음 코드를 삽입한다.

    1
    2
    3
    4
    5
    6
    
       {
         "posts": [
           { "id": "1", "title": "a title", "views": 100 },
           { "id": "2", "title": "another title", "views": 200 }
         ]
       }
    

    이후, 해당 db.json 파일을 JSON Server CLI에 넘겨준다. (port: 3000)

    Image

  4. 다시 projecet에서 index.html 을 생성하여, 다음 문서를 작성한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
     <!DOCTYPE html>
         <html lang="ko">
           <head>
             <meta charset="UTF-8" />
             <meta name="viewport" content="width=device-width, initial-scale=1.0" />
             <title>index HTML</title>
             <style>
               [contenteditable="true"]:empty::before {
                 content: attr(placeholder);
                 color: #999;
               }
             </style>
           </head>
           <body>
             <div>
               <div>
                 <h1>Notion</h1>
                 <button type="button" id="pageCreateBtn">새페이지 만들기</button>
                 <ul id="notionList"></ul>
               </div>
             </div>
             <div>
               <button type="button" id="pageSaveBtn">저장</button>
               <button type="button" id="historyBackBtn">내용 history 저장</button>
               <button type="button" id="historyForwardBtn">내용 history forward</button>
               <div>pageID:<span id="pageId"></span></div>
               <div id="contentTitle" contenteditable="true" placeholder="새페이지 제목"></div>
               <div id="contentBody" contenteditable="true" placeholder="새페이지 본문"></div>
             </div>
           </body>
     </html>
    

    이후, 해당 파일 실행 후, 작성된 문서 확인! (port: 5500)

    Image

  5. JSONPlaceholderguide에 들어가서, 해당 가이드라인을 따른다.

    JSONPlaceholder - Guide

    index.html에서 생성했던 버튼의 기능을 생각하여,
    이벤트 리스너를 작성한다.

    예) 페이지 만들기 버튼 → Creating a resource의 fetch APi 사용

    Image

    원하는 구성을 위해, 해당 코드를 복사하여 수정한다.

    (+ fetch 주소JSON Server CLI에 명시한다.)

    • 코드 (이벤트 리스너 등록 후)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      
        <!DOCTYPE html>
        <html lang="ko">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>index HTML</title>
            <style>
              [contenteditable="true"]:empty::before {
                content: attr(placeholder);
                color: #999;
              }
            </style>
            <script>
              document.addEventListener("DOMContentLoaded", () => {
                const pageCreateBtn = document.getElementById("pageCreateBtn");
                pageCreateBtn.addEventListener("click", () => {
                  fetch(**"http://localhost:3000/posts"**, {
                    method: "POST",
                    body: JSON.stringify({
                      title: "",
                      body: "",
                      userId: 1,
                    }),
                    headers: {
                      "Content-type": "application/json; charset=UTF-8",
                    },
                  })
                    .then((response) => response.json())
                    .then((json) => makePageTitle(json));
                });
                const notionList = document.getElementById("notionList");
                const makePageTitle = (x) => {
        		      const li = document.createElement("li");
                  const a = document.createElement("a");
                  a.href = "#";
                  a.id = x["id"];
                  a.textContent = x["title"] == "" ? "새 페이지" : x["title"];
                  li.appendChild(a);
                  notionList.appendChild(li);
                };
              });
            </script>
          </head>
          <body>
            <div>
              <div>
                <h1>Notion</h1>
                <button type="button" id="pageCreateBtn">새페이지 만들기</button>
                <ul id="notionList"></ul>
              </div>
            </div>
            <div>
              <button type="button" id="pageSaveBtn">저장</button>
              <button type="button" id="historyBackBtn">내용 history 저장</button>
              <button type="button" id="historyForwardBtn">내용 history forward</button>
              <div>pageID:<span id="pageId"></span></div>
              <div id="contentTitle" contenteditable="true" placeholder="새페이지 제목"></div>
              <div id="contentBody" contenteditable="true" placeholder="새페이지 본문"></div>
            </div>
          </body>
        </html>
              
      
  6. 예제처럼, 새페이지를 만들면,
    db.json가 다음과 같이 동적으로 코드가 추가된다.

Image



Notion 따라하기

  • Notion 페이지 생성과 유사한 동작을 만들어보자.
  • [새 페이지 만들기] 버튼 클릭 시, 목록에 새로운 페이지가 생성된다
    • 페이지 제목은 빈 문자열인 경우, “새 페이지”로 표현
  • 목록에서 페이지를 선택했을 때, 해당 페이지(들)의 내용을 불러온다.
    • 추가로, 해당 페이지의 ID를 메인에 표현한다.
  • 기본적으로, 페이지 목록이 생성되는 UI가 동작한다.
  • 제목은 개행되서는 안되기 떄문에, 엔터키를 막는 동작을 넣는다.
    • keydown 이벤트에 해당 키코드를 막는 이벤트를 추가한다.
      Image
  • [저장] 버튼을 클릭 시, 해당 페이지를 저장한다.
  • 본문은 개행될 때 마다, 히스토리에 저장되어야 한다.
    • 히스토리에 저장되어, 개행된 행마다의 뒤로가기와 앞으로가기의 기능을 구현한다.
    • 이 때, 더이상 뒤로 갈 곳이 없거나 앞으로 갈 곳이 없으면 이벤트는 더이상 동작하지 않는다.

index.html

  • 코드 (전체 코드)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    
      <!DOCTYPE html>
      <html lang="ko">
        <head>
          <meta charset="UTF-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1.0" />
          <title>index HTML</title>
          <style>
            [contenteditable="true"]:empty::before {
              content: attr(placeholder);
              color: #999;
            }
            div[contenteditable="true"] {
              border: 1px dashed #aaa;
              padding: 5px;
            }
          </style>
          <script>
            document.addEventListener("DOMContentLoaded", () => {
              const pageCreateBtn = document.getElementById("pageCreateBtn");
              pageCreateBtn.addEventListener("click", () => {
                fetch("http://localhost:3000/posts", {
                  method: "POST",
                  body: JSON.stringify({
                    title: "",
                    body: "",
                  }),
                  headers: {
                    "Content-type": "application/json; charset=UTF-8",
                  },
                })
                  .then((response) => response.json())
                  .then((json) => makePageTitle(json));
                // .then((json) => console.log(json));
              });
        
              const notionList = document.getElementById("notionList");
              const makePageTitle = (x) => {
                const li = document.createElement("li");
                const a = document.createElement("a");
                a.href = "#";
                a.id = x["id"];
                // 페이지 제목이 빈 문자열인 경우, 새 페이지로 표현
                a.textContent = x["title"] == "" ? "새 페이지" : x["title"];
        
                // 리스트 목록을 클릭했을 때, 내용이 연결되도록
                a.addEventListener("click", (e) => {
                  e.preventDefault();
                  fetch("http://localhost:3000/posts/" + e.currentTarget.id)
                    .then((response) => response.json())
                    .then((json) => {
                      setContents(json);
                    });
                });
                li.appendChild(a);
                notionList.appendChild(li);
              };
        
              // 페이지 목록 생성
              const getPageTitleList = (x) => {
                fetch("http://localhost:3000/posts")
                  .then((response) => response.json())
                  .then((json) => {
                    json.forEach((x) => {
                      makePageTitle(x);
                    });
                    // 목록 중, 첫번째 페이지 내용을 보여주도록
                    setContents(json[0]);
                  });
              };
              getPageTitleList();
        
              // 목록에서 페이지를 선택했을 때 내용 불러오기
              const pageId = document.getElementById("pageId");
              const contentTitle = document.getElementById("contentTitle");
              const contentBody = document.getElementById("contentBody");
              const setContents = (x) => {
                pageId.textContent = x["id"];
                contentTitle.textContent = x["title"];
                contentBody.textContent = x["body"];
        
                history.back = [];
                history.forward = [];
              };
        
              // 제목 엔터키 막기
              contentTitle.addEventListener("keydown", (e) => {
                if (e.keyCode == 13) {
                  e.preventDefault();
                }
              });
        
              // 페이지 저장
              const pageSaveBtn = document.getElementById("pageSaveBtn");
              pageSaveBtn.addEventListener("click", (e) => {
                if (confirm("저장하시겠습니까?")) {
                  fetch("http://localhost:3000/posts/" + pageId.textContent, {
                    method: "PUT",
                    body: JSON.stringify({
                      title: contentTitle.innerHTML,
                      body: contentBody.innerHTML,
                    }),
                    headers: {
                      "Content-type": "application/json; charset=UTF-8",
                    },
                  })
                    .then((response) => response.json())
                    .then((json) => {
                      notionList.querySelector("a[id='" + pageId.textContent + "']").textContent =
                        contentTitle.innerHTML;
                      alert("저장되었습니다!");
                    });
                }
              });
        
              // 본문에서 엔터키를 누를 때 마다, 히스토리에 저장됨 LIFO
              const history = {
                back: [],
                forward: [],
              };
              contentBody.addEventListener("keydown", (e) => {
                if (e.keyCode == 13) {
                  // 엔터키를 누를 때, forward가 남아 있으면 클리어
                  if (history.forward.length > 0) {
                    history.forward = [];
                  }
                  history.back.push(e.currentTarget.innerHTML);
                }
              });
        
              const historyBackBtn = document.getElementById("historyBackBtn");
              historyBackBtn.addEventListener("click", (e) => {
                if (history.back.length == 0) {
                  return;
                }
                history.forward.push(contentBody.innerHTML);
                contentBody.innerHTML = history.back.pop();
              });
        
              const historyForwardBtn = document.getElementById("historyForwardBtn");
              historyForwardBtn.addEventListener("click", (e) => {
                if (history.forward.length == 0) {
                  return;
                }
                history.back.push(contentBody.innerHTML);
                contentBody.innerHTML = history.forward.pop();
              });
            });
          </script>
        </head>
        <body>
          <div>
            <div>
              <h1>Notion</h1>
              <button type="button" id="pageCreateBtn">새페이지 만들기</button>
              <ul id="notionList"></ul>
            </div>
          </div>
          <div>
            <button type="button" id="pageSaveBtn">저장</button>
            <button type="button" id="historyBackBtn">내용 history back</button>
            <button type="button" id="historyForwardBtn">내용 history forward</button>
            <div>pageID:<span id="pageId"></span></div>
            <div id="contentTitle" contenteditable="true" placeholder="새페이지 제목"></div>
            <div id="contentBody" contenteditable="true" placeholder="새페이지 본문"></div>
          </div>
        </body>
      </html>
    

db.json

  • 코드

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      {
        "posts": [
          {
            "id": "1111",
            "title": "",
            "body": ""
          }
        ]
      }
    



추가 개념

confirm(): 사용자가 확인(OK) 또는 취소(Cancel)를 선택할 수 있는 팝업 창을 띄움

1
let result = confirm("메시지 문구");
  • 결과 값으로 true or false를 반환
    • true: 사용자가 확인(OK) 버튼을 눌렀을 때
    • false: 사용자가 취소(Cancel) 버튼을 눌렀을 때

This post is licensed under CC BY 4.0 by the author.