[Project] 글쓰기 기능 구현

young_pallete·2021년 8월 30일
1

no#tation

목록 보기
2/11

시작하며 🌈

최근에 PPT 제작도 하랴, 이렇게 정신 없는 하루는 오랜만이네요.
저는 지금 교육을 들으며 제 앞길을 모색하고 있지만

확실히 바깥은, 하반기가 시작되어가는 듯한 느낌이 들어요.

그래서인지, 최근에 정~말 많은 고민을 했는데요, 그래도 역시 이 모든 것이 제가 개발 실력이 없었기 때문이라 생각하니, 편했습니다. 앞으로는 제가 개발 실력을 기르려 최선을 다하면 되는 일이니까요.

이 글을 읽는 분들도 뭔가 플젝을 시작하면서, 다들 그런 마음가짐으로 시작하셨을 거 같아요. 그럼, 초라한 결과물이지만 같이 시작해볼까요?!

본론 📃

어제는 리스트 기능을 구현했죠?
그러면 이제는 글쓰기 기능을 한 번 시도해볼 거에요.
상당히 간단하답니다. 일단 우리 투두리스트처럼, 한 번 만들어 보자구요.

저는 크게 제목과, 내용물을 넣을 수 있는 컴포넌트를 제작할 건데요,
이 에디터 컴포넌트 이름을 PostForm.js이라 짓겠습니다.

TodoForm.js

import debounce from '../utils/debounce.js';
import { setItem } from '../utils/storage.js';
import Input from './common/Input.js';

export default function PostForm({
  $target,
  initialState = {
    title: '',
    content: '',
  },
}) {
  // 초기 컴포넌트를 DOM에 추가하고, 상태를 초기화합니다.
  const $editor = document.createElement('div');
  $target.appendChild($editor);

  /*
   * this.state = {
   *   title: string
   *   content: string
   * }
   */
  this.state = initialState;

  const savePost = (id = 'new', state = this.state) => {
    setItem(id, state);
  };

  /*************************************
   *            component              *
   *************************************/
  const postTitle = new Input({
    $target: $editor,
    initialState: this.state.title,
    onChange: title => {
      const nextState = {
        ...this.state,
        title,
      };
      this.setState(nextState);
      postTitle.setState(title);
      debounce(savePost, 2000)('new', this.state);
    },
  });

  const $postContent = document.createElement('textarea');
  $editor.appendChild($postContent);

  this.setState = nextState => {
    this.state = {
      ...this.state,
      ...nextState,
    };

    debounce(savePost, 2000)('new', this.state);
    this.render();
  };

  this.render = () => {
    const { content } = this.state;
    $postContent.value = content;
  };

  this.render();

  $postContent.addEventListener('keyup', e => {
    this.setState({
      ...this.state,
      content: e.target.value,
    });
  });
}

네, 특이한 것들이 몇 개 보이시죠?
debouncegetItem, setItem 등등 말이죠!

이를 한 번 깔끔하게 정리를 하자면,

  1. debounce
    나중에 전 api를 처리할 건데, 이를 계속적으로 이벤트가 발생할 때마다 저장하면 비효율적으로 메모리를 낭비하겠죠?! 이를 처리해주는 아이입니다.

  2. getItem, setItem
    설령 이를 api로 처리한다 해도, 갑자기 서버가 끊겼었다면?
    이에 대한 최소한의 방어적인 장치로써, 로컬스토리지를 활용할 거에요.

debounce.js

let timer = null;
export default function debounce(func, delay) {
  return (...args) => {
    if (timer !== null) clearTimeout(timer);
    timer = setTimeout(() => {
      func(...args);
    }, delay);
  };
}

const storage = window.localStorage;

/**
 * @description getItem from localStorage by key
 * @param {*} key
 * @param {*} defaultValue
 * @returns value if value is not null else return defaultValue
 */
export const getItem = (key, defaultValue) => {
  try {
    const value = storage.getItem(key);
    return value !== null ? JSON.parse(value) : defaultValue;
  } catch (e) {
    console.error(e);
    return defaultValue;
  }
};

/**
 * @description setItem from localStorage by key
 * @param {*} key
 * @param {*} value
 */
export const setItem = (key, value) => {
  try {
    storage.setItem(key, JSON.stringify(value));
  } catch (e) {
    console.error(e);
  }
};

네, 이런 꼴이지요!

또, Input이라는 컴포넌트를 만들었는데요. 이는 공통으로 쓰일 부분이 참~ 많은 아이니까 따로 common 디렉토리에서 쓸 수 있게끔 만들었습니다.

input.js

export default function Input({ $target, initialState, onChange }) {
  const $input = document.createElement('input');
  $target.appendChild($input);

  this.state = initialState;
  $input.value = this.state;

  this.setState = nextState => {
    this.state = nextState;
  };

  $input.addEventListener('keyup', e => {
    onChange(e.target.value);
  });
}

네, 이제 컴포넌트 부분은 끝났어요!
사실 여기까지는 투두리스트와 비슷해서, 큰 차이는 못느끼겠군요.

다만, 저희는 앞으로 페이지를 만드려 해요.
결과적으로 다양한 기능들을 추가할 거고, 이를 독립적인 페이지로 관리하는 것이 더욱 깔끔하고 독립적으로 기능하기 때문이죠!

따라서, 저는 이렇게 PostEditPage라는 것을 만들었어요.

import PostForm from '../components/PostForm.js';

/*
 * this.state = {
 *   postId: string,
 *   postInfo: {
 *     title: string
 *     content: string
 *   }
 * }
 */
export default function PostEditPage({
  $target,
  initialState = {
    postId: 'new',
    postInfo: {
      title: '',
      content: '',
    },
  },
}) {
  const $page = new DocumentFragment();

  this.state = initialState;

  const postForm = new PostForm({
    $target: $page,
    initialState: this.state.postInfo,
  });

  this.setState = nextState => {
    this.state = nextState;
    this.render();

    postForm.setState(nextState.postInfo);
  };

  this.render = () => {
    $target.appendChild($page);
  };
}

일단 이렇게 만들었는데, 결국에는 App컴포넌트에서 처리해줘야겠죠?

아마 App 컴포넌트가 저의 대략적인 페이지를 라우팅하는 데 있어 관리해주는 중요한 역할을 할 거 같군요.

import SideBar from './components/SideBar.js';
import PostEditPage from './pages/PostEditPage.js';

export default function App({ $target }) {
  new SideBar({
    $target,
    initialState: {
      username: 'jengyoung',
      documents: [//Dummy Data//]
    },
  });

  const postEditPage = new PostEditPage({
    $target,
    initialState: {
      postId: 'new',
      postInfo: {
        title: '',
        content: '',
      },
    },
  });

  this.route = () => {
    postEditPage.render();
  };

  this.route();
}

그래서 여기는 render()라는 것 대신, route라는 것으로 표현을 했어요. 아마 다음 내용일 건데, 라우트를 관리하기 위해서이죠.

그럼 결과물을 볼까요?!

네, 결과물은 잘 나오는 군요!
참고로 아직 SideBar 컴포넌트를 따로 넣지는 않았습니다. 원래 글 쓰는 기능을 테스트하는 목적이었으니까요.

그렇다면 글은 잘 써지나 테스트 해보겠습니다!

다행히도, 잘 써지는군요!

마치며 🖐

후! 슬슬 이제 라우팅을 시작할 거 같은데, 이 부분은 꽤나 제게 어려울 것만 같군요! (아직도 리액트에서 벗어나지 못한 1인...)
여튼, 조만간 계속해서 엄~청 달릴 것 같지만, 결국 거리보다는 방향이니까요.

그렇다면, 다음에는 라우트 세팅한 후 찾아 뵙겠습니다! 😄😄🖐🏻

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉

0개의 댓글