Codestates SEB FE 42기/회고

S1 unit12 | 솔로프로젝트 회고, 더 잘하고 싶었지만 😢

2realzoo 2022. 11. 16. 12:52

📌 git 협업

<local 관리 git 명령어>

◽ git init : local git repository 생성

◽ git add 파일명 : untracked flies를 staging area로 추가/

파일명에 .를 쓰면 현재 디렉토리의 모든 파일이 추가된다.

◽ git staus : staging area에 있는 파일 확인

◽ git restore 파일명 : 파일의 변경사항 폐기

◽ git commit -m "message" : staging area의 파일을 저장소에 기록

커밋 시에는 간결하게 변경 사항의 정확한 기술 용어를 사용해서 기록한다.

◽ git reset HEAD^ : 커밋한 파일을 바로 직전으로 되돌린다.

◽ git log : 커밋 기록 조회

<remote 관리 git 명령어>

◽ git remote add : local repository에 remote repository url 등록

<name> : 로컬 레포지토리에서 remote repository를 등록할 이름

<url> : 원격 레포지토리의 url

◽ git remote -v : 연결 확인

◽ git push : commit한 내용을 원격 repository에 저장

<remote> : 로컬 레포지토리에서 저장한 원격 레포지토리 이름, 원격 레포지토리를 clone하면 기본적으로 'origin' 이라는 이름으로 등록됨

<branch> : 브랜치 이름

◽ git clone : 내 원격 레포지토리에서 내 로컬 레포지토리로 파일을 복사해 가져옴

📌 my agora state

 

  • 나열 기능 : 별도 파일에 주어진 데이터를 이용하여 나열
  • CSS : 질문 리스트 중앙 정렬, 디자인
  • 추가 기능
    • 새로운 질문 추가 가능한 입력 폼 제작
    • 아이디, 본문 입력 후 버튼을 누르면 화면에 질문 추가
    • 원래 배열에 추가한 데이터 실제로 쌓기
  • Github Page 배포
  • Pull request

 

  • 데이터 시간 표기 방식을 보기 편하게 변환
  • 페이지네이션 기능
    • 한 페이지에 10개의 질문 보이도록 함
    • 다음 페이지로 넘어가는 버튼, 이전 페이지로 돌아오는 버튼
    • 다음 페이지가 없거나, 이전 페이지가 없는 경우 페이지 유지
  • 데이터 유지 기능
    • LocalStorage 사용하여 새롭게 추가하는 질문이 페이지를 새로고침해도 유지되도록 제작

1. 주어진 데이터 DOM 이용하여 HTML 요소로 추가하기

// convertToDiscussion은 아고라 스테이츠 데이터를 DOM으로 바꿔줍니다.
const convertToDiscussion = (obj) => {
  const li = document.createElement('li'); // li 요소 생성
  li.className = 'discussion__big--container'; // 클래스 이름 지정

  const elContainer = document.createElement('div');
  elContainer.className = 'discussion__container'
  const avatarWrapper = document.createElement('div');
  avatarWrapper.className = 'discussion__avatar--wrapper';
  const discussionContent = document.createElement('div');
  discussionContent.className = 'discussion__content';
  const discussionAnswered = document.createElement('div');
  discussionAnswered.className = 'discussion__answered';
  // TODO: 객체 하나에 담긴 정보를 DOM에 적절히 넣어주세요.
  const avatarImage = document.createElement('img');
  avatarImage.className = 'discussion__avatar--image'
  avatarImage.alt = `avatar of ${obj.author}`
  const title = document.createElement('h2');
  title.className = 'discussion__title'
  const information = document.createElement('div');
  information.className = 'discussion__information';
  const url = document.createElement('a');
  const checkbox = document.createElement('p');
  checkbox.className = 'checkbox';

  avatarImage.setAttribute('src', obj.avatarUrl);
  url.textContent = obj.title;
  information.textContent = `${obj.author} / ${new Date(obj.createdAt).toLocaleString()}`;
  url.setAttribute('href', obj.url);
  checkbox.textContent = '□';

  li.append(elContainer);
  elContainer.append(avatarWrapper, discussionContent, discussionAnswered);
  avatarWrapper.append(avatarImage);
  discussionContent.append(title, information);
  discussionAnswered.append(checkbox);
  title.append(url);

처음에는 감이 잡히지 않아서 막막했는데 한번 원리를 알고 나니까 그 이후 요소 추가는 쉬웠다.

모든 정보를 원래 HTML에 있던 것과 똑같은 class 적용해서 그대로 추가했는데 여기서 신기했던 점이 있다.

📝속성 추가

const element = document.createElement('img');
elment.setAttribute('alt',obj.thing);
const element = document.createElement('img');
elment.alt = obj.thing;

이 둘이 똑같이 적용된다는 점이었다.

setattribute를 사용하지 않아도 속성을 정할 수 있었다.

그 다음은 answer부분을 불러왔다.

다만 answer이 있는 데이터도 있고 없는 데이터도 있어서 조건을 달아주었다.

//answer 
  if(obj.answer !== null) {
    const ansWrapper = document.createElement('ul'); // 이중 ul 요소
    ansWrapper.className = 'answer__wrapper';

    const answer = document.createElement('li'); // 첫번째 답변 
    answer.className = 'answer';

    const ansAvartarWrapper = document.createElement('div');
    ansAvartarWrapper.className = 'answer__avatar--wrapper';
    const ansAvartarImg = document.createElement('img');
    ansAvartarImg.className = 'answer__avartar--image'

    const ansTitle = document.createElement('h2');
    ansTitle.className = 'answer__title'
    const url = document.createElement('a');
    const ansContent = document.createElement('div');
    ansContent.className = 'answer__content';
    const ansInformation = document.createElement('div');
    ansInformation.className = 'answer__Information';

    url.textContent = '답변';
    checkbox.textContent = '☑';
    url.href = obj.answer.url;
    ansAvartarImg.src = obj.answer.avatarUrl;
    ansInformation.textContent = `${obj.answer.author} / ${new Date(obj.answer.createdAt).toLocaleString()}`


    li.append(ansWrapper);
    ansWrapper.append(answer);
    answer.append(ansAvartarWrapper, ansContent);
    ansAvartarWrapper.append(ansAvartarImg);
    ansContent.append(ansTitle,ansInformation);
    ansTitle.append(url);

  }

  return li;
};

answer부분을 어떻게 구현할지 고민이 많았는데,

질문 밑에 살짝의 깊이를 주어 달면 둘의 관계가 시각적으로 보이지 않을까 해서 질문 부분을 한번 더

로 감싸고 그 부모 요소 밑으로 질문 요소를 만들었다.

사실 이렇게 번잡하게 만들어도 되나 싶긴 하지만...

// agoraStatesDiscussions 배열의 모든 데이터를 화면에 렌더링하는 함수입니다.
const render = (element) => {
  for (let i = 0; i < agoraStatesDiscussions.length; i += 1) {
    element.append(convertToDiscussion(agoraStatesDiscussions[i]));
  }
  return;
};

 

📝 페이지네이션

//pagination
const paginationNumbers = document.getElementById('pagination__numbers');
const paginatedList = document.getElementById("paginated__list");
let listItems = document.querySelectorAll("li.discussion__big--container");
const nextButton = document.getElementById("next__button");
const prevButton = document.getElementById("prev__button");

const paginationLimit = 10;
const pageCount = Math.ceil(listItems.length / paginationLimit);
let currentPage;

페이지네이션은 주어진 자료가 없어서 혼자 찾아가며 공부해야했다.

다양한 방법이 있었는데 처음엔 이 블로그를 참고했다.

여기서는 해당 글을 append하는 방식으로 한 것 같은데 처음에 블로그를 봤을 땐 도무지 무슨 말인지 이해하기가 어려워서 그냥 복붙으로 넣었지만 장렬하게 실패했다. 

여기서만 거진 하루를 쓴 것 같다ㅋㅋㅋㅋㅋ

그리고 나중에는 스터디원님이 공유해주신 링크의 방식대로 해보았다.

그나마 내가 실패한 후 혼자 페이지네이션 해보려고 했을 때 썼던 .hide를 이용한 방식이라 조금 이해하기 쉬웠다.

그래도 거의 복붙을 하긴 했지만... 

우선 페이지네이션에 필요한 변수를 지정해주었다!

 

//pagenumber 버튼 생성
const appendPageNumber = (index) => {
  const pageNumber = document.createElement('button');
  pageNumber.className = 'pagination__number';
  pageNumber.textContent = index;
  pageNumber.setAttribute('page-index', index);
  pageNumber.setAttribute('aria-label','page' + index);

  paginationNumbers.append(pageNumber);
};

const getPaginationNumbers = () => {
  for(let i = 1; i <= pageCount; i ++) {
    appendPageNumber(i);
  }
};

여기서는 pagenumber버튼을 생성했다.

앞에서 요소 추가했던 것과 거의 유사하다.

 

아쉬운 점은 무조건 1페이지부터 생성되게 코드가 짜여있다는 점이다.

추후에 시간이 남는다면 고쳐보고 싶다.

getPaginationNumbers()만 고치면 될 것 같긴 하다.

 

//버튼 활성화
const handleActivePageNumber = () => {
  document.querySelectorAll('.pagination__Number').forEach((button) => {
    button.classList.remove('active');

    const pageIndex = Number(button.getAttribute('page-index'));
    if(pageIndex === currentPage) {
      button.classList.add('active');
    }
  });
};

아까 만든 버튼에 .active 클래스를 주는 것인데 사실 아직도 왜 활성화를 시켜야 하는지 모르겠다. 

그냥 모든 버튼이 활성화 상태이면 안되는 것일까? 왜 remove하는거지...?

 

 

📝 이전, 다음 버튼 

<nav class="pagination__container">
        <button class="pagination__button" id="prev__button" aria-label="Previous page" title="Previous page">
          &lt;
        </button>
        <div id="pagination__numbers"> 
        </div>
        <button class="pagination__button" id="next__button" aria-label="Next page" title="Next page">
          &gt;
        </button>
      </nav>

우선 HTML로 이전 버튼과 다음 버튼을 추가해주었다. 

HTML에서 글을 작성하고 열어보면 <>'& 같은 것들이 사라져 있는 경우가 많다. 

태그에 사용되는 문자기 때문에 이런 문자들은 표기하고 싶을 땐 특수 코드로 나타낸다.

 

prevButton.addEventListener("click", () => {
  if(currentPage === 1) {
  }
  else{
    setCurrentPage(currentPage - 1);
  }
});

nextButton.addEventListener("click", () => {
  if(currentPage === 5) {
  }
  setCurrentPage(currentPage + 1);
});

이 부분은 이전 페이지로 돌아가는 버튼과 다음 페이지로 넘어가는 버튼을 구현한 부분이다. 

이전 페이지는 1page가 되면 그 페이지에 머물러야 하므로 1이면 아무 것도 하지 않는 조건을 달았다.

다음 페이지도 마찬가지로 5page가 되면 버튼을 누르더라도 아무 것도 하지 않게 했다. 

 

📝 페이지 속 내용

const setCurrentPage = (pageNum) => {
  currentPage = pageNum;

  handleActivePageNumber();

  const prevRange = (pageNum - 1) * paginationLimit;
  const currRange = pageNum * paginationLimit ;

  listItems.forEach((item, index) => {
    item.classList.add('hide');
    if(index >= prevRange && index < currRange) {
      item.classList.remove('hide');
    }
  })
};

prevRange라는 변수와 currRange라는 변수에 한 페이지에서 보이는 글의 범위를 할당했다.

범위는 이전에 선언해두었던 변수(paginationLimit)를 이용하면 된다.

 

listItem은 모든 질문 글을 넣어 둔 변수인데 이것을 forEach로 반복해서 각각의 요소에 'hide' 클래스를 주고, 아까 선언한 범위에 맞는 요소는 'hide' 클래스를 삭제했다.

CSS에서 .hide{  display: none; } 를 추가해두면 'hide' 클래스에 해당하는 요소는 전부 보이지 않게 된다.

 

//처음 로드될 때 실행시키기
window.addEventListener('load', () => {
  getPaginationNumbers();
  setCurrentPage(1);

  document.querySelectorAll('.pagination__number').forEach((button) => {
    const pageIndex = Number(button.getAttribute('page-index'));
    
    if(pageIndex) {
      button.addEventListener('click' , () => {
        setCurrentPage(pageIndex);
      })
    }
  });
  document.querySelector('.form__avatar--image').src = myAvatar;
})

이후엔 처음 로드될 때 바로 실행되어야 할 것들을 이벤트를 통해 넣어주었다.

window는 최상위 객체로 브라우저를 컨트롤 할 수 있다.

따라서 이 코드는 브라우저가 로드될 때 실행할 것을 나타낸 것이다.

우선 페이지버튼을 getPaginationNumbers로 생성하고, 현재 페이지를 1로 설정한다.

 

그 다음 코드는 pageNumber 버튼을 클릭하면 그 페이지가 나오도록 하는 코드이다.

마지막 코드는 게시글을 달 때 있는 사진을 넣는 코드이다.

 

const makeAvatar = () => {
  const index= Math.floor(Math.random() * 10 );
  const arr = [
    'https://item.kakaocdn.net/do/fd0050f12764b403e7863c2c03cd4d2d7154249a3890514a43687a85e6b6cc82',
    'https://blog.kakaocdn.net/dn/bnD244/btqNjVKUwhT/M9Kdihjk4WeFXcld7lZQ0K/img.jpg',
    'https://blog.kakaocdn.net/dn/cggrTQ/btqNfrK1D8U/eIo8HSrVLAEOnyG3tCNZN0/img.jpg',
    'https://cdn.huffingtonpost.kr/news/photo/202201/116183_225004.png',
    'https://mblogthumb-phinf.pstatic.net/MjAyMTAzMjFfMjE2/MDAxNjE2MzAyNTY1NDQ2.9W0Rfz3nbST4_2Iuc-x-MsV13QfpuMGFtnkXIb8NQ7og.eiRNw4_B69_S8ZrO4f6nUE3otNJ-bNH3TBAW6GgpmhYg.JPEG.gmlwjd5363/FB%EF%BC%BFIMG%EF%BC%BF1612167377216.jpg?type=w800',
    'https://blog.kakaocdn.net/dn/O8ZrD/btqNf6Nowp1/RD9dBIdp9sWO7qGgakQhrk/img.jpg',
    'https://blog.kakaocdn.net/dn/Lhqdm/btqNkPXHImi/3LWT74VgU008srSUDdnH3k/img.jpg',
    'https://blog.kakaocdn.net/dn/cw9feq/btqNf7r2BhI/fAwkqyi70FcKwxj6dnM9Bk/img.jpg',
    'https://blog.kakaocdn.net/dn/cL8bwq/btqNkQWDL9A/2TszBrkZhIc3UwIgH9SCI1/img.jpg',
    'https://i.pinimg.com/236x/57/18/c6/5718c6bb9d9c20c1f37f14ef7b7b1965.jpg']
  return arr[index]
}
let myAvatar =  makeAvatar();
document.querySelector('.form__avatar--image').src = myAvatar;

이렇게 랜덤으로 사진이 결정되도록 했다. 

지금보니까 맨 밑줄은 쓸데없는 코드같아서 삭제했다.

두 번이나  사진을 넣을 필요는 없으니까...

Math.ramdom()이라는 새로운 함수를 배웠다.

이 함수는 랜덤으로 0에서 1사이의 소수를 반환하는 함수이다.

그래서 소수점을 오른쪽으로 한 번 옮겨주고 Math.floor로 나머지 소수점 이하의 숫자를 내림하여 버렸다.

사진 파일의 url이 있는 배열의 index로 그 숫자를 사용했다.

 

📝새로운 질문 추가

// 새로운 질문 추가 기능
const newTitle = document.querySelector('#title');
const newName = document.querySelector('#name');
const newStory = document.querySelector('#story');
const form = document.querySelector('form.form');

새로운 질문을 추가하기 위해서 만들어두었던 입력박스들을 변수에 할당했다.

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const obj = {
    id: "new id",
    createdAt: new Date(),
    title: newTitle.value,
    url: undefined,
    author: newName.value,
    answer: null,
    bodyHTML: newStory.value,
    avatarUrl:myAvatar,
  }
  agoraStatesDiscussions.unshift(obj);
  ul.prepend(convertToDiscussion(obj));
  newName.value = '';
  newStory.value = '';
  newTitle.value = '';
  listItems = document.querySelectorAll("li.discussion__big--container");
  setCurrentPage(1);
  myAvatar = makeAvatar();
  document.querySelector('.form__avatar--image').src = myAvatar;

  localStorage.setItem('json', JSON.stringify(agoraStatesDiscussions))
});

submit버튼을 눌렀을 때 이벤트가 실행되도록 하고

기존 데이터와 같은 key를 사용해서 앞에 만들었던 질문을 생성하는 함수에 넣도록 했다.

submit버튼의 기본값이 페이지를 새로고침하므로 preventDefault()로 새로고침을 없애고, 입력폼에 작성했던 글이 사라지도록 했다. listItem의 값도 다시 할당해서 새로 추가된 질문이 할당될 수 있도록 한다.

마지막으로 localStorage에 새로 추가된 질문을 저장하는데, localStorage는 string값으로 저장되므로 JSON을 이용해 기존 데이터를 string으로 바꾸고 나중에 불러올 때 JSON.parse를 이용하여 객체로 다시 바꿔주었다.

 

📝localStorage

// ul 요소에 agoraStatesDiscussions 배열의 모든 데이터를 화면에 렌더링합니다.
function getLocalStorage() {
  if(localStorage.length === 0) {  
  }
  else {
    agoraStatesDiscussions = JSON.parse(localStorage.getItem('json'));
  }
}
getLocalStorage();

const ul = document.querySelector('ul.discussions__container');
render(ul);

원래 있었던 렌더링 함수에 if문을 추가했다.

localStorage를 사용해야 하기 때문에 처음 화면을 열었을 때,

localStorage에 요소가 있다면 그것을 불러와서 원래의 데이터에 할당했다.

이 함수는 무조건 실행되어야 해서 호출을 해두었다.

 // localStorage reset 버튼
 const reserbtn = document.getElementById('reset');
 reserbtn.addEventListener('click',() => {
  for(let i = 0; i < localStorage.length; i++) {
    agoraStatesDiscussions.splice(i,1);
  }
  localStorage.clear()
  location.reload();
 })

localStorage를 비워주는 버튼도 하나 만들었다.

만들었던 질문을 기존 데이터에 넣었기 때문에 이것들을 제거해주고

localStorage를 비웠다.

그리고 페이지를 새로고침 해주었다.

 

이번 과제를 하면서, 생각보다 내가 모르는 게 너무 많고, 부족하다는 생각이 들었다.

다른 사람이랑 비교하면 안되지만 자꾸 저울질하게 되어서 괴롭기도 했다.

시간이 부족했던 과제는 처음이라 더 아쉽기도 했다.

그래서 과제 제출이 끝난 후에 여러 기능을 추가하고 고쳤다.

원래는 localStrage랑 pagenation이 부족했다.

고치면서 배우는 점도 많고 DOM과 javascript에 대해 더 이해하게 되어서 좋다.