바닐라JS로 인스타그램 클론 하기

ddoni·2020년 12월 30일
4

인스타그램 클론(위스타그램)

실제 인스타그램 로그인 페이지와 메인페이지를 참고하여 HTML, CSS, JavaScript로 몇가지 기능을 추가하여 구현해 보았다.

로그인페이지

구현한 내용

  1. 기본 레이아웃

    <section> 태그 내 <h1>, <form> 등으로 구성

  2. 아이디에 '@'가 포함되고 비밀번호 길이가 5자 이상인 경우 로그인 버튼 활성화 되게 하기

    로그인 인풋과 패스워드 인풋에 'input' 이벤트를 주어 조건을 만족하는 경우 로그인 버튼의 disabled 속성이 제거되고 active 속성이 추가되도록 구현

메인 피드 페이지

구현한 내용

  1. 기본 레이아웃

    • <header>

      상단 메뉴는 <nav> 로 구성하였다.

    • <main>

      왼쪽 오른쪽 부분으로 나눠서 왼쪽엔 스토리 피드 보여질 <section>,

      메인 피드부분은 <article>, 오른쪽 추천 리스트는 <aside>, 하단 부분은 <footer> 를 사용하여 시맨틱 마크업을 하였다.

  2. (1) 댓글 추가 기능

    각 피드는 id로 구분하여 댓글 추가 버튼에 클릭 이벤트를 주어 클릭 이벤트 타겟이 있는 피드의 id를 구해서 그 피드에 있는 인풋 값과 댓글이 추가 될 곳의 위치를 구해서 댓글을 추가할 수 있도록 구현하였다.

    (2) 댓글 좋아요, 삭제 기능

    댓글 좋아요 버튼이 눌리면 setAttribute 를 통해 하트 → 빨간하트로 바뀌고 좋아요된 여부는 클래스를 주어 클래스 여부에 따라 버튼 이미지가 바뀌도록 구현하였다.

    (3) 검색 기능

    검색 대상이 될 정보를 배열에 담아 변수에 할당하고 검색어(인풋 값)이 배열 중 id와 일치하는 것만 필터링하여 innerHTML로 검색박스가 꾸며지게 하였다.

    (4) 프로필 메뉴 박스

    프로필 메뉴박스는 클릭하면 기존에 추가해두었던 displaay: none;으로 설정된 클래스 값을 제거하여 보여지게 설정하고 다른곳을 클릭하면 박스가 사라지게 하기 위해 windowclick이벤트를 주었다. (메뉴박스와 프로필사진이 클릭되었을땐 이벤트 영역에 제외되는 로직 추가)

    (5) 반응형 웹 구현

    실제 인스타그램 페이지에서 뷰포트 가로 사이즈가 1000px 이하가 되면 오른쪽에 있는 섹션이 사라지에 되어 미디어 쿼리로 구현하였다.

구현하면서 어려웠던 점

  1. flex 내 아이템 정렬

    메인 피드의 상단에는 피드를 올린 사용자 정보와 더보기 버튼이 있는데 이는 컨테이너에 flex를 주어 가로 정렬로 배치하였다. 더보기 버튼은 맨 오른쪽에 배치되어야하는데 버튼에만 position을 추가하여 따로 배치하기엔 복잡해지는 느낌이라 찾아보았다!

    ✨ 해결방안

flex 관련 속성들에는 align-self 속성이 있는데 이는 flex 아이템에 사용할 수 있는 속성으로 아이템 개별적으로 cross-axis를 기준으로 정렬이 가능하다. 내가 원하는건 main-axis를 기준으로 버튼 아이템 개별적으로만 배치하고 싶었는데 개별적으로 사용할 수 있는 속성이 따로 있진않았다.

이런 경우는 margin-left | margina-right 값을 auto로 주면 된다.



```jsx
//flex-container
.mainFeedProfile {
  width: 100%;
  height: 60px;
  padding: 16px;
  display: flex;
  align-items: center;
}

//flex-item
.moreBtn {
  width: 40px;
  height: 40px;
  border: none;
  background: #fff;
  padding: 8px;
  margin-left: auto;
}
```
  1. 컨테이터 내 인풋과 버튼

댓글을 다는 인풋과 버튼을 컨테이너로 묶고 display: flex를 주어 가로배치하고 컨테이너 내에서 아이템들이 가운데 위치하게 하고 싶어서 align-items: center를 적용했지만 가운데 정렬이 되지 않아 멘토님께 문의드렸다!!

→ 컨테이너에 height 속성이 없어서 기준이 되는 것이 없기 때문에 align-items는 적용되지 않았다. 항상 자식요소들을 정렬할땐 부모의 기준값이 있는지 확인해보자!

  1. currentTarget vs target
  • target is the element that triggered the event (e.g., the user clicked on)
  • currentTarget is the element that the event listener is attached to.
  1. 스토리 피드

인스타처럼 핑크+주황 그라데이션이 들어간 색상은 보더에 적용시 border-radius 속성이 적용되지 않았다. css로만 구현하고 싶은 경우 주로 background-image에 색상을 적용하여 구현가능하다!

.activeStory {
  width: 64px;
  height: 64px;
/* 투명한 두줄의 보더가 생성됨 */
  border: 2px double transparent;
  border-radius: 50%;
/* 안쪽 보더 내 색상은 화이트로 바깥 보더 내 색상은 그라데이션이 들어가게 설정 */
  background-image: linear-gradient(white, white), linear-gradient(45deg, #F99848, #CC3C95);
	background-origin: border-box;
/* 안쪽 보더 내의 배경은 컨텐트 사이즈로 적용된다, 바깥쪽 보더 내 배경은 보더기준으로 적용된다 */
  background-clip: content-box, border-box;
  display: flex;
  justify-content: center;
  align-items: center;
}

border-sytle: doule; 보더 스타일 타입 중 보더가 두줄 생성되게 해준다.

border-color: transparent; 보더 색상을 투명하게 만든다.

linear-gradient() 2개 이상의 색상으로 구성되어 그라데이션 효과를 내고 싶을 때 사용하는 css 함수이다.

background-origin: border-box; 배경 이미지가 어디서부터 시작되야할지 정해주는 속성이다.

background-clip 배경의 이미지나 색상과 요소의 패딩이나 컨텐트 사이의 간격을 정해주는 속성이다. 첫번째 보더 내 배경은 컨텐트까지만, 두번째 보더 내 배경은 보더까지 닿게된다.

  1. 검색기능

처음엔 검색기능을 구현하기 위해 아래와 같이 생각해보았다.

  1. 검색 대상 데이터들은 배열형태로 각각 객체로 변수에 할당 (id, name, imgurl 포함) 2. 검색 인풋에 'input' 이벤트가 발생할때마다 이벤트 value를 가져와서 value가 데이터의 id 값과 일치하면 검색 대상 데이터가 나옴 (데이터 배열에 filter 메소드 적용) 3. filter 메소드 내 검색결과가 있으면 데이터 박스가 나오게 하고 없으면 안나오게 조건문 추가

위와 같이 생각했을땐 이벤트가 발생할때마다 중복된 검색결과가 계속 검색결과 박스에 추가되어 이부분 수정이 필요했다. (filter 메소드에 대한 이해가 부족해서 잘못 작성하여 발생한 문제인거 같다)

구글링해서 이전코드를 아래과 같이 수정하였다!

  1. 'input' 이벤트가 발생할때마다 이벤트 타겟 값이 데이터 userId 와 일치하는 걸 filter 메소드를 이용하여 새로운 배열의 형태로 받는다. 2. 받은 배열의 길이가 0 이상인 경우(검색결과가 있는 경우) 검색내역 박스는 보여지게 되고 각 검색결과는 배열의형태이므로 문자열로 바꿔주어 html 형식으로 반환된다. 3. 받은 배열의 길이가 0인 경우(검색결과가 없는 경우) 검색내역 박스는 보여지지만 검색결과가 없다고 나온다.
function paintSearchList(resultArr) {
  const searchBox = document.querySelector('.searchBox');
//필터된 검색결과 배열에 일치하는 것이 있는경우
  if (resultArr.length > 0) {
    searchBox.classList.remove('hide');
//검색결과 배열을 반복하며 각 요소들에 있는 id, name. url 값 가져와서 검색내역 박스에 
//보여질 형식으로 반환
    const appendList = resultArr.map((e) => {
      return `<li class="searchBoxItem">
                  <a href="#">
                    <img src="${e.imgUrl}" />
                    <div class="accountInfoContainer">
                      <p class="searchId">${e.userId}</p>
                      <span>${e.userName}</span>
                    </div>
                  </a>
                </li>`
//배열을 문자열로 바꾸기 (배열로 그대로 검색박스에 넣을 경우 배열 구분기준인 쉽표도 그대로나옴)
    }).join('');
    searchBox.innerHTML = appendList;
  }
//필터된 검색결과 배열에 일치하는 것이 없는경우
  if (resultArr.length === 0) {
    searchBox.classList.remove('hide');
    searchBox.innerHTML =  `<li class="searchBoxItem">
                                <a href="#">
                                 <div class="accountInfoContainer">
                                    <span>검색 결과가 없습니다</span>
                                  </div>
                                </a>
                              </li>`
  }
}

function showSearchResult(evt) {
  const value = evt.target.value;
//유저아이디가 이벤트타겟값과 일치한 경우에만 반환되는 새로운배열 생성(filter메소드 이용)
  const result = searchData.filter((e) => {
    return e.userId.includes(value);
  })
  paintSearchList(result);
}

function activeSearchbar() {
  const searchInput = document.querySelector('.searchInput');
  searchInput.addEventListener('input', showSearchResult);
}
  1. 댓글 추가 기능

댓글 추가 기능은 코드를 작성하면서도 정말 '이건 아닌데...이건 노가다 인데..' 이런 생각으로 작성했다. 일단 기능을 구현하잔 맘으로 노가다식으로 작성했고 추후 멘토님 리뷰를 받고 힌트를 얻어서 의도한 대로 효율적이진 않는 코드이지만 이전의 방식보단 나아졌다 생각한다!

<처음 구현한 방식> 1. 클릭된 버튼, 클릭된 버튼과 형제요소인 인풋, 댓글이 추가될 리스트 각각에 id 값 설정 2. 이벤트 타겟의 id 가 첫번째 피드의 버튼 id와 일치하면 인풋값과 댓글이 추가 될 곳을 배열 인덱스로 접근 → 현재 페이지 내에선 피드가 2개 뿐이라 가능한 방법이지만 실제 수많은 피드가 생긴 경우 각각의 요소에 접근하기 위해 for문을 돌리고 id를 주는 것은 매우 비효율적인 방법이다.

function commentInputHandler(evt) {
  const targetVal = evt.target.value;
  if (evt.target.id === 'input1') {
    if (targetVal !== '') {
      commentBtns[0].removeAttribute('disabled');
      return;
    }
  }
  if (evt.target.id === 'input2') {
    if (targetVal !== '') {
      commentBtns[1].removeAttribute('disabled');
      return;
    }
  }
}

<코드 리뷰 후> 1. 각 피드마다 id를 주어 구분 2. 클릭한 버튼의 이벤트 타겟이 속한 컨테이너(피드) 요소를 구하여 그 컨테이너가 가진 자식 요소들에 접근하여 각각 인풋과 리스트를 구할 수 있었다. → 여전히 클릭이벤트가 발생하기 위해 for문을 돌렸지만 input과 list 요소는 id를 따로 주지 않고 구할 수 있었다!

function activeCommentBtn(evt) {
 const targetVal = evt.target.value;
 if (targetVal.length > 0) {
   const targetParent = evt.target.parentElement.parentElement.parentElement.parentElement;
   for (let i = 0; i < commentBtns.length; i++) {
     if (commentBtns[i].parentElement.parentElement.parentElement.parentElement.id ===
      targetParent.id) {
        commentBtns[i].removeAttribute('disabled');
      }
   }
 }
}

function commentBtnHandler(evt) {
  evt.preventDefault();
  const containerId = evt.target.parentElement.parentElement.parentElement.parentElement.id;
  const getContainer = document.getElementById(containerId);
  let inputVal = getContainer.children[3].children[0].children[0].children[0].value;
  const commentList= getContainer.children[2].children[3].children[0];
  addCommentPaint(inputVal, commentList);
}

느낀 점

  1. 레이아웃 / 클래스 설정의 중요성

    메인 피드를 구성하면서 위에서 마크업 한 순서대로 부분부분씩 스타일을 적용하는데 유저아이디, 버튼 등 중복되는 곳엔 동일한 클래스를 주어 한번에 스타일을 중요할 걸을 느꼈다. 전체를 못보고 부분만 보다가 동일한 스타일 코드를 곳곳에 적용해야하는 불편한 점이 느껴졌다. 레이아웃도 부분씩 잡다보니 댓글 폼을 스타일해야 할때 전체 줄을 그어야해서 위로 다시 올라가서 컨테이너를 찾아야하는 수고로움이 있었다. ✨먼저 전체를 확인 후 중복되는 곳엔 동일한 클래스를 주고 레이아웃도 전체를 보며 짜야겠다!! ✨

  2. 배열 메소드

배열 메소드를 다양하게 활용하고 싶은데 아직 원하는 생각대로 결과가 나오지 않아 많이 연습을 해야 될거 같다!!

  1. 처음으로 온전한 페이지를 클론해본거라서 전체적인 틀 잡는게 많이 부족하다고 느껴졌다. 전체를 놓치니까 코드를 중복 작성하는 부분도 많고 리액트 프로젝트 진행시에 이런 부분을 많이 보안해보고 싶당😊✨

2개의 댓글

comment-user-thumbnail
2021년 1월 3일

민선님 정리 정말 깔끔하세요!!!대박!

답글 달기
comment-user-thumbnail
2021년 1월 6일

안녕하세요, tech 기업에서 일하는/ 일하기를 희망하는 여성들을 모아서 모임을 만들고 있어요!
자세한 사항은 및 링크 참조바랍니다 :)
https://velog.io/@emilyscone/SheKorea-1%EA%B8%B0-%EB%A9%A4%EB%B2%84%EB%A5%BC-%EB%AA%A8%EC%A7%91%ED%95%A9%EB%8B%88%EB%8B%A4

답글 달기