컴포넌트 방식으로 생각하기

컴포넌트란?

표현하는 부분을 추상화한 것으로 독립적이며, 다른 곳에 영향을 미치지 않아야 한다.

그럼으로써 추후에 재사용이 가능하다. 

추상화란 어떤 독립적인 부분들을 묶어서 단위를 생성한다고 할 수 있다. 

 

만일 투두 리스트를 만든다고 한다면 Header, TodoForm, TodoList 3개의 컴포넌트로 나눌 수 있다. 

그리고 3개의 컴포넌트를 생성하고 관리하는 App 컴포넌트까지 총 4개의 컴포넌트로 동작하는 구조를 가지게 된다. 

 

TodoList 만들기

TodoList 컴포넌트 생성

// params.$target - 해당 컴포넌트가 추가가 될 DOM 엘리먼트
// params.initialState - 해당 컴포넌트의 초기 상태
function TodoList(params) {
  const $todoList = document.createElement("div");
  const $target = params.$target;
  $target.appendChild($todoList);

  this.state = params.initialState;

  this.render = () => {
    $todoList.innerHTML = `
      <ul>
        ${this.state.map((todo) => `<li>${todo.text}</li>`).join("")}
      </ul>
    `;
  }; // 컴포넌트를 그리는 것은 render 함수

  this.render();
}

구조 분해 할당(object destructuring)을 통해 개선할 수 있다.

function TodoList({ $target, initialState }) {
  const $todoList = document.createElement("div");
  $target.appendChild($todoList);

  this.state = initialState;

  this.render = () => {
    $todoList.innerHTML = `
      <ul>
        ${this.state.map(({ text }) => `<li>${text}</li>`).join("")}
      </ul>
    `;
  };

  this.render();
}

 

TodoList 컴포넌트 화면에 그리기

- main.js 추가

// src/main.js
const data = [
  {
    text: "자바스크립트 공부하기",
  },
  {
    text: "자바스크립트 복습하기",
  },
];

const $app = document.querySelector(".app");

new TodoList({
  $target: $app,
  initialState: data,
});

- index.html 추가

<html>
  <head>
    <title>TodoList</title>
  </head>
  <body>
    <main class="app"></main>
    <script src="./src/TodoList.js"></script>
    <script src="./src/main.js"></script>
  </body>
</html>

[주의]

  • script 순서는 중요
    • main.js에서 TodoList를 사용하고 있기 때문에 TodoList가 먼저 불려야 한다.
    • script src는 선언 순서대로 로딩되기 때문에 만일 순서가 반대가 된다면 main.js에서 TodoList를 부를 때 TodoList가 돌려지지 않은 상태이기 때문에 에러가 나게 된다.
  • script를 head 태그가 아니라 body 태그 아래에 넣는 이유
    • head 태그에 넣으면 script를 로딩하는 동안 브라우저 렌더링이 차단 된다.
    • body 태그 아래에 있으면 body 내용이 그려진 뒤 script가 불러진다.

실행

npx serve

실행 결과

만약 port가 사용 중이면 npx serve -l 포트번호 커맨드로 다른 port로 띄울 수 있다. 

 

TodoForm 컴포넌트 생성

function TodoForm({ $target, onSubmit }) {
  const $form = document.createElement("form");
  $target.appendChild($form);

  let isInit = false;

  this.render = () => {
    $form.innerHTML = `
      <input type="text" name="todo" />
      <button>Add</button>
    `;

    if (!isInit) {
      $form.addEventListener("submit", (e) => {
        e.preventDefault(); 

        const text = $form.querySelector("input[name=todo]");
        onSubmit(text).value;
      });
      isInit = true;
    }
  };

  this.render();
}

이벤트 객체의 preventDefault는 태그의 기본 동작을 끈다. 

이벤트를 바인딩하는 코드가 렌더가 호출될 때마다 실행될 수 있으므로 isInit 플래그 변수를 선언하여 false인 경우에만 실행되도록 한다. 

form안의 button은 type을 작성하지 않으면 기본적으로 submit이 된다. 

 

TodoForm에서 submit이 발생했을 때 처리하는 코드onSubmit는 TodoForm 밖에 있다.

 

- main.js에서 TodoForm 생성

new TodoForm({
  $target: $app,
  onSubmit: (text) => {
    console.log(text);
  }
});

TodoForm에서 입력받은 값을 TodoList에 넣으려고 할 때 TodoForm의 생성 파라미터에 TodoList를 넣고 직접 참조를 하면  TodoForm에 TodoList 컴포넌트에 대한 의존성이 강하게 생긴다. TodoForm의 독립적 사용이 불가능해지는 것이다. 

 

이것을 방지하기 위해서는 TodoForm 생성 파라미터에 이벤트 콜백(onSubmit)을 넣고, text를 입력 받으면 해당 콜백을 통해 text를 넘겨줄 수 있다. 즉 콜백을 통해서 독립적인 컴포넌트들이 통신할 수 있게 된다. 

 

- index.html에서 스크립트 불러오기

...
<main class="app"></main>
<script src="./src/TodoForm.js"></script>
<script src="./src/TodoList.js"></script>
...

 

TodoList의 상태를 변할 수 있게 만들기

- TodoList 컴포넌트에 setState 함수 추가

// TodoList.js
this.setState = (nextState) => {
  this.state = nextState;
  this.render();
};

투두 리스트가 state를 기준으로 그려지고 있기 때문에 setState를 이용하여 상태를 바꾸고 변경된 상태를 화면에 다시 그린다. 

 

 - onSubmit 콜백에서 todoList.setState 호출

// main.js
new TodoForm({
  $target: $app,
  onSubmit: (text) => {
    const nextState = [
      ...todoList.state,
      {
        text,
      },
    ];
    todoList.setState(nextState);
  },
});

const todoList = new TodoList({
  $target: $app,
  initialState: data,
});

 

Todo 추가 후 input 비우기, 최소 글자 설정

// TodoList.js
$form.addEventListener("submit", (e) => {
  e.preventDefault();

  const $todo = $form.querySelector("input[name=todo]");
  const text = $todo.value;

  if (text.length > 1) {
    $todo.value = "";
    onSubmit(text);
  }
});

 

main.js에서 하는 작업을 App.js로 묶기

- App.js 생성

function App({ $target, initialState }) {
  new TodoForm({
    $target,
    onSubmit: (text) => {
      const nextState = [
        ...todoList.state,
        {
          text,
        },
      ];
      todoList.setState(nextState);
    },
  });

  const todoList = new TodoList({
    $target,
    initialState,
  });
}

- index.html에서 스크립트 불러오기

 

<script src="./src/TodoForm.js"></script>
<script src="./src/TodoList.js"></script>
<script src="./src/App.js"></script>
<script src="./src/main.js"></script>

- main.js 수정

...
const $app = document.querySelector(".app");

new App({
  $target: $app,
  initialState: data,
});

App.js가 하는 역할: 컴포넌트 생성·관리

 

헤더 추가하기

- Header.js 생성

function Header({ $target, text }) {
  const $header = document.createElement("h1");
  $target.appendChild($header);

  this.render = () => {
    $header.textContent = text;
  };

  this.render();
}

- index.html에서 스크립트 불러오기

...
<main class="app"></main>
<script src="./src/Header.js"></script>
<script src="./src/TodoForm.js"></script>
...

- App.js에서 Header 생성

new Header({
  $target,
  text: "Simple Todo List",
});

 

컴포넌트 간의 의존성

  • App - Header
  • App - TodoForm
  • App - TodoList

App 컴포넌트가 세 개의 컴포넌트를 컨트롤하는 형태이다.

TodoForm과 TodoList는 아무런 의존성도 없다.

 

Client Side에서 데이터를 저장하기

쿠키

  • 쿠키는 브라우저에 저장되는 작은 문자열
  • RFC 6265 명세에서 정의한 HTTP 프로토콜의 일부
  • 다른 저장 방법에 비해 가장 오래된 방식

쿠키 추가하기

document.cookie = "language=javascript" // key=value

이전 쿠키를 덮어쓰지 않고 새로 추가된다. 

 

쿠키 읽어 오기

const cookie = document.cookie;

각 쿠키는 ;로 구분되어 있어 불러온 후 splite 등으로 쪼개서 써야 한다. 

 

쿠키 유효기간 넣기

쿠키는 유효기간을 설정하지 않으면 브라우저를 닫는 순간 날아간다

/* 쿠키 유효기간 설정 방법 1 */
// expires의 경우 GMT String을 넣어야 함
// GMT 기준이기 때문에 한국 시간은 +9시간을 해줘야 함
const date = new Date();
date.setDate(date.getDate() + 1); // 하루 뒤 만료

document.cookie = 'user=mike; expires=`${date.toGMTString()}`';

/* 쿠키 유효기간 설정 방법 2 */
// max-age를 넣으면 생성시점 기준으로 유효기간 설정 가능
// 단위는 1초, 3600의 경우 1시간
document.cookie = 'user=mike; max-age=60'; // 1분 뒤 만료

 

쿠키 주의 사항

  • HTTP 요청시 헤더에 쿠키가 같이 나가기 때문에 쿠키 사이즈가 커지면 HTTP 요청 크기도 커짐
  • 사이즈에 제한이 있음
  • 여러가지 보안 취약점을 조심해야 함

 

Local Storage

  • key, value 기반 Local에 데이터를 저장할 수 있음
  • 도메인 기반으로 Storage가 생성 (도메인이 같다면 여러 탭 내에서 같은 Storage가 공유 됨)
  • 직접 삭제하지 않는 이상 브라우저를 껐다 켜도 삭제되지 않음

값 저장하기

window.localStorage.name = 'mike';
window.localStorage['name'] = 'mike';
window.localStorage.setItem('name', 'mike');

setItem을 사용하는 것이 가장 안전하다.

property를 수정하는 식으로 하면 length, toString 같은 내장 함수들을 덮어 씌울 수 있기 때문이다. 

 

불러오기, 삭제, 전체 삭제

// 불러오기
const storedName = localStorage.getItem('name');
// 삭제하기
localStorage.removeItem('name');
// 전체 삭제하기
localStorage.clear();

 

로컬 스토리지에는 string만 넣을 수 있기 때문에 JSON.stringify로 넣고 JSON.parse로 꺼낸 값을 파싱해서 쓰는 것이 좋다. 

const user = {
  name: "mike",
  genter: "male",
};
// 저장
localStorage.setItem("user", JSON.stringify(user));
// 불러오기
const storedName = JSON.parse(localStorage.getItem("user"));

object를 stringify하지 않으면 문자열로 [object Object]가 들어간다. 

 

Session Storage

  • 전체적으로 Local Storage와 동일
  • 브라우저를 닫으면 저장된 내용이 초기화

 

TodoList에 Local Storage 사용하기

- App.js에서 데이터 저장시 로컬 스토리지에도 저장

new TodoForm({
  $target,
  onSubmit: (text) => {
    const nextState = [
      ...todoList.state,
      {
        text,
      },
    ];
    todoList.setState(nextState);

    localStorage.setItem("todos", JSON.stringify(nextState));
  },
});

- main.js의 data를 삭제하고 로컬 스토리지에서 불러오기

const initialState = JSON.parse(localStorage.getItem("todos") || "[]");

const $app = document.querySelector(".app");

new App({
  $target: $app,
  initialState,
});

잘 동작하는 것 같지만 외부 툴(크롬 개발 도구) 등을 이용하여 todos의 json string을 올바르지 않은 형태로 바꾸면 에러가 발생한다. 

 

특히 로컬 스토리지나 세션 스토리지는 용량에 제한이 있는데 캐싱을 잘못 설계한 경우, 예를 들어 HTTP 요청을 캐싱하게 했는데 캐시를 날리지 않아서 너무 많이 쌓이게 되면 setItem을 했을 때 에러가 발생하는 경우가 있다. 

이런 경우에는 setItem을 무시하고 원래대로 동작하게 해야 한다. 

 

이런 문제를 대비하여 storage를 다루는 것은 별도의 함수로 두는 것이 좋고, try-catch 문을 이용하여 처리하는 것이 안전하다.

최소한 getItem을 할 때는 key에 해당하는 값이 없으면 default 값을 저장할 수 있게 하는 것이 좋다. 

 

- storage.js 생성하고 여기서만 로컬 스토리지에 접근하게 함

const storage = (function (storage) { // 전역 오염 최소화를 위해 IIFE
  const setItem = (key, value) => {
    try {
      storage.setItem(key, value);
    } catch (e) {
      console.log(e);
    }
  };

  const getItem = (key, defaultValue) => {
    try {
      const storedValue = storage.getItem(key);

      if (storedValue) {
        return JSON.parse(storedValue);
      }
      return defaultValue;
    } catch (e) {
      console.log(e);
      return defaultValue;
    }
  };

  return {
    setItem,
    getItem,
  };
})(window.localStorage);

- index.html에서 스크립트 불러오기

- 기존 로컬 스토리지 사용 코드 수정

// App.js
storage.setItem("todos", JSON.stringify(nextState));

// main.js
const initialState = storage.getItem("todos", []);

 


출처: 프로그래머스 프론트엔드 데브코스 

[Day 11] VanillaJS를 통한 자바스크립트 기본 역량 강화 I (3)

복사했습니다!