인스타그램 웹사이트 만들어보기 (Javascript)

front-end developer·2022년 7월 2일
1
post-thumbnail

Westagram Project

login Page

1. Page layout

```html
<body>
    <header></header>
    <main class="container">
        <div class="login-container">
            <div class="westagram-logo">westagram</div>
            <input type="text" class="login id-box" placeholder="전화번호, 사용자 이름 또는 이메일">
            <input type="password" class="login pw-box" placeholder="비밀번호">
            <button id="login-button" disabled>로그인</button>
            <div class="forgot-pw"><a href="https://www.instagram.com/accounts/password/reset/">비밀번호를 잊으셨나요?</a></div>
        </div>
    </main>
</body>
```

![Untitled](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/d820b8df-d54a-4de0-87d8-546297ef272b/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220702%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220702T073707Z&X-Amz-Expires=86400&X-Amz-Signature=a5714332242824c03ccabd93920454162ae23a2ac6d9c568acfa0fea9fc530f0&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Untitled.png%22&x-id=GetObject)
  • 위스타그램 페이지 레이아웃은 flex display 속성값을 주로 사용하였다.
  • 로그인 창을 감싸는 태그를 <div> 를 사용하였는데, 이후 wrap-up 세션에서 <main> or <form> 과 같은 semantic 태그를 활용하는 것이 좋다는 피드백을 받아서 앞으로 프로젝트에서는 semantic 태그를 적극적으로 활용하는 방식으로 구현해야겠다.
  • 로그인 button에 disabled 속성을 부여했고, 자바스크립트에서 로그인 유효성 검사 함수에 통과하면 disabled 값을 false로 부여해 동작하게 했다.
  • 로그인 버튼이 활성화 되었을 때, 마우스 오버하면 커서가 바뀌는 css를 적용하고 싶었는데 다음을 통해 구현할 수 있었다.
    #login-button:hover:enabled {
        cursor: pointer;
    }

2. Javascript

  • 자바스크립트에서는 크게 3가지를 구현하려 했다.
    • 입력창에서 유효성 검사를 실시하는 함수 (아이디에 ‘@’이 포함하는지와 pw길이가 5자 이상인지)
    • 사용자가 입력창에 변화를 줬을 때, 유효성 검사가 실시되도록
    • 유효성 검사를 통과하면 로그인 버튼이 활성화되도록
  • 위 3개를 구현하였으며, 완성 직후의 코드는 다음과 같다.
    const loginId = document.getElementsByClassName('login')[0];
    const loginPw = document.getElementsByClassName('login')[1];
    const loginButton = document.getElementById('login-button');
    
    function activateLoginButton (){
       let idValueLength = loginId.value.length;
       let pwValueLength = loginPw.value.length;
    
          (loginId.value.includes('@') && pwValueLength>5) ? 
          (loginButton.disabled = false) :
          (loginButton.disabled = true)   
    }
    
    loginId.addEventListener('keyup', activateLoginButton);
    loginPw.addEventListener('keyup', activateLoginButton);
    loginButton.addEventListener('click', () => {
       window.location.href = '../main.html';
    })
  • 이후 wrap-up 세션을 통해, 함수는 되도록 기능별로 분할하여 코드를 작성해야 나중에 오류가 발생했을 때 대처하기 용이하다는 것을 알게되었다.
  • 1가지의 함수만으로 3개의 기능이 동작하고 있어, 기능별로 3가지로 구분하였다.
  • 또한, use strict 을 전역에 정의함으로써 오류가 발생하기 쉬운 유연한 자바스크립트의 특징을 방지하여 초기에 오류를 찾아낼 수 있게했다.
    "use strict";
    
    const loginId = document.getElementsByClassName('login')[0];
    const loginPw = document.getElementsByClassName('login')[1];
    const loginButton = document.getElementById('login-button');
    
    const checkValue = () => {
    
       let pwValueLength = loginPw.value.length;
       return !(loginId.value.includes('@') && pwValueLength>5);
    
    }
    
    const lgnBttnHandler = () => {
    
       loginButton.disabled = checkValue();
    
    }
    
    const init = () => {
    
      loginId.addEventListener('input', lgnBttnHandler);
      loginPw.addEventListener('input', lgnBttnHandler);
      loginId.addEventListener('keyup', lgnBttnHandler);
      loginPw.addEventListener('keyup', lgnBttnHandler);
      loginButton.addEventListener('click', () => {
       window.location.href = '../main.html';
    })
    
    };
    
    init();

init()

  • 처음엔 로그인 텍스트창의 이벤트리스너를 전역에 선언해줬었는데, 이렇게 함수로 묶어놓고 실행해야 나중에 코드가 복잡해졌을 때 어디서 이벤트리스너가 작동하는지 찾을 수 있고 유지보수 하기 용이하다라는 사실을 알게되어 수정했다.
  • 초기에는 keyup 이벤트만 리스너로 작동하게 했는데, 이후에 사용자가 마우스만 이용해서 복사 붙여넣기 할 수도 있다는 사실을 알게되어 input 이벤트리스너를 추가하였다. ⇒ 항상 사용자의 입장에서 고민해보기

checkValue()

  • 이 함수는 boolean값을 리턴하는데, 이 리턴값을 가지고 바로 버튼을 활성화시키는 함수에 적용하려고 했다.
  • 버튼이 활성화 되기 위해서는 버튼요소의 disabled값이 false가 되어야 함. (현재는 true로 비활성화상태)
  • 따라서 checkValue 함수에서 유효성 검사를 만족하면 false를 만족하지 않으면 true를 반환해야 하므로 !를 붙여줬다.
    ⇒ 코드가 간결해지긴 했는데, 코드의 가독성이 좋은지는 모르겠다.

초기에 자바스크립트로 구현하려 했을 때는 로그인버튼이 활성화되는 함수를 페이지가 로드될 때 1번 호출되어 원하는대로 동작하지 않았다.
이후에 사용자가 어느정도 작성을 완료하면 동작하게 해야겠다 라고 생각하여, 이벤트리스너 속성을 걸어줬고 이 안에는 로그인 유효성 함수가 포함되어 있다.

⇒ 유효성 검사의 결과에 따른 버튼 활성화 비활성화

2. Main page

page layout

flex

  • 메인 페이지에서도 flex display 속성을 활용하여 요소들을 원하는 위치에 배치시켰다.

  • flex direction 과 flex가 적용된 자식요소에 각각 flex값을 부여하는 속성을 통해 원하는 대로 쉽게 위치시킬 수 있었다.

  • flex의 활용법을 알게되면서 사전스터디에서 자기소개페이지를 활용했을 때보다 훨씬 더 쉽게 원하는 위치에 요소들을 배치했다. 그러나,

    • container와 wrapper가 늘어나면서 생기는 naming에 대한 문제

    • 그로인해 600줄이 넘어가는 css…

    • 등의 문제점이 남아있음.

      주어진 시간 내에 완성하기 위해 기능 구현에 초점을 맞춰서 작성하다보니 CSS가 많이 길어졌다.
      이후 refactoring 세션을 통해 css style속성들의 배치순서, 불필요한 속성(ex. width: 100%)을 이해하였고,
      부모로부터 상속 받을 수 있는 속성을 활용하거나 스타일 속성이 비슷한 요소들은 클래스로 묶어서 속성을 부여하는 방식들을 통해 코드를 줄여나가야겠다.


vw, vh, em, px

  • 기존에 px단위만 사용하다가 나중에 반응형을 구현하고 싶기도 했고, 화면에 원하는 비율만큼 길이와 높이값을 주고 싶어 vw, vh , em 등 단위별 차이점에 대해 찾아봤다.
    • em 은 부모 요소의 폰트사이즈에 대해서 배수로 계산한다. 예를들어 자식요소 15em이고 부모요소의 폰트사이즈가 10px이라면 15em = 10px * 15 = 150px이다.
      • 이 단위는 px로 구현하기 애매할 때, 즉 1px차이보다 더 세밀한 크기 조정이 필요할 때 사용하였다.

    • rem 은 부모요소가 아닌 최상위 요소의 폰트사이즈에 대해서 계산한다. 즉, 최상단인 html의 폰트사이즈에 대해서 배수로 적용
    • vw, vh 는 보여지는 화면 기준으로 사이즈를 정하는 방식이다. 1024x980에서 100vw를 주면 vw는 1024px, vh는 980px이다.
      • 이 단위는 화면을 꽉 채운 제일 큰 컨테이너를 만들고 싶어서 width: 100vw, height: 100vh 값을 사용했다.

image sprite

  • 메인페이지 우측상단의 아이콘들은 각각의 다른 이미지들을 가져와서 <img> 태그로 넣어줬고, 피드 하단의 아이콘들은 하나의 이미지를 사용하였다.
  • css의 background-position을 각각 부여하여 하나의 이미지 파일을 사용하더라도 (사용한 이미지에는 여러 아이콘들이 포함되어 있음) 보여지는 이미지가 출력되게 하였다.
    • 실제 인스타그램 페이지에서도 image sprite를 활용하여 각각의 아이콘들이 하나의 이미지에서 background-position 값만 다르게 부여하여 사용하고 있다.

position: sticky

  • 인스타그램 홈페이지에서 가장 상단에 위치한 검색창 섹션과 우측 팔로워 추천 섹션은 페이지를 아래로 스크롤 해도 고정되어 있는 섹션이다.
  • 그러나 인스타그램 페이지에서는 고정된 것 뿐만 아니라 브라우저 창의 크기(width)를 줄이면 피드 섹션과 일정한 거리를 유지한 채 좌우 공백만 줄어드는 반면, 내가 구현한 페이지는 피드와의 일정한 거리가 유지되지 못했다.
  • 피드와의 거리를 유지하기 위해 마진 값 px값이 아닌 vw를 사용해보는 등 fixed 포지션으로만 구현하려다가 애를 먹었다.
  • 구글링을 통해 나와 비슷한 case들을 stackoverflow에서 발견하였고 sticky 포지션을 사용하면 원하는 레이아웃을 쉽게 구현할 수 있었다.
  • sticky 포지션은 top, bottom, right, left 중 적어도 한가지의 값을 주고 스크롤을 내리면서 주어진 위치(값)에 도달했을 때(임계점)부터 fixed 포지션처럼 작동한다.
  • 내 경우에는 nav바가 fixed 포지션으로 구현되어 있기 때문에 빈 공간으로 인식이 되고 aside창과 브라우저 최상단의 간격은 90px이다.
  • stickey position에 top:90px을 주게 되면 내가 원하던 fixed 포지션처럼 작용하게 된다. 만약 top: 0 을 주게되면 스크롤을 내리면서 aside 섹션이 브라우저 최상단에 닿을 때 (top: 0) fixed포지션처럼 작동한다.
  • 즉, stickey는 임계점을 설정해주면, 처음엔 static position처럼 작동하다가 그 임계점부터는 달라붙어 fixed처럼 작동한다. ⇒ 이를 이용해서 브라우저 창이 줄어들어도 피드 섹션과 일정한 간격을 유지할 수 있었다.

  • 추가로, 처음 stickey를 사용했을 때 제대로 작동하지 않았는데 그 이유는 stickey 값을 부여한 div요소에 width와 height 값을 부여하지 않았기 때문이었다.
    ⇒ 명확한 크기가 있어야 임계점을 부여할 수 있기 때문에, 꼭 크기를 부여해주자.

javascript

댓글, 좋아요, 삭제 기능

  • 댓글 기능을 처음 구현할 때는, 이전에 배웠던 appendChild, createElement, className, innerHTML 을 이용하여 함수로 구현할 생각이었다.
  • 그러나 구현을 하는 도중, 최종적으로 홈페이지가 완성되면 피드도 여러개일 것이고 그에따른 댓글창도 여러개일 것인데 각각 달리는 댓글을 관리(해당 요소에 접근)하는 것이 기존함수의 방식은 어려울 것 같다고 생각했다.
    ⇒ classname이나 queryselector 같은 dom요소로 접근하기에는 한계가 있을 것 같다고 생각했다.
  • 그래서, 이전에 Enem rain을 구현하며 배웠던 class 객체를 이용하면 각 요소에 this로 쉽게 접근할 수 있고 class prototype안의 함수를 통해 여러 기능을 쉽게 구현할 수 있겠다라고 생각해 댓글 기능은 class 객체를 통해 구현하였다.
  • 크게보면 댓글을 생성하고 좋아요를 누르고 삭제하는 기능을 담고 있는 클래스와 유효성검사를 하는 함수, 버튼 활성화를 하는 함수, 이벤트리스너를 적용하는 함수 4가지로 나뉜다.
  • 먼저, 전역변수를 보자
  • 댓글창은 유저, 댓글, 좋아요, 삭제 4가지 요소로 이루어진 부분을 크게 감싸는 contatiner태그로 이루어져 있으며, flex position이 적용되었다.
        const cmntBox = document.getElementsByClassName('comment-box')[0];
        const user = document.getElementsByClassName('userOfComment-container')[0];
        const cmnt = document.getElementsByClassName('commentOfUser-container')[0];
        const cmntLike = document.getElementsByClassName('comment-like-container')[0];
        const cmntDelete = document.getElementsByClassName('comment-delete-container')[0];
        const cmntPost = document.getElementsByClassName('comment-enter-button')[0]; 

init()

  • 댓글창에 변화가 발생하면 게시버튼을 활성화시키는 이벤트리스너를 적용했다.
  • 게시버튼에 마우스 클릭이 발생하거나 혹은 댓글창에 엔터키가 눌리면 New Comment라는 객체가 생성되게 하는 이벤트리스너를 적용했다.
    => Comment라는 객체는 클래스를 통해서 생성된 객체이며, 댓글을 생성하고 좋아요/삭제 기능을 담고있는 객체이다.
        const init = () => {
            cmntBox.addEventListener('input', activatePostButton);
            cmntBox.addEventListener('keyup', activatePostButton);
            cmntPost.addEventListener('click', function () {
                new Comment();
            })
            cmntBox.addEventListener('keypress', e => {
                if (e.key == 'Enter'){
                    new Comment();
                }
            })    
        }
        
        init();

activatePostButton(), checkValue()

  • 댓글창에 입력되는 내용에 유효성 검사를 실시하며, 유효성 검사가 통과되면 게시버튼이 활성화되는 함수이다.
  • 유효성 검사는 댓글창의 입력한 내용이 1글자 이상이며, 빈공백으로만 이루어져있지 않아야한다.
  • 이때, 빈공백만으로 작성하였는지 구현하기가 어려웠는데, 처음엔 falsey값으로 접근하기 위해 고민했었는데,
  • stackoverflow에 나와 비슷한 질문을 한 사람이 있었는데 그 분을 통해서 str.trim() 을 알게되었다.
  • str.trim() 은 스트링 양끝 공백을 없애준다. 따라서 공백을 작성하게 되면 “” 빈값이 도출되어 falsey가 된다.
  • login 유효성검사에서 언급했듯이, disabled 버튼은 false가 나와야 활성화 되므로 checkValue() 반환값에 !을 붙여줬다.
            const checkValue = () => {
                let cmntValue = cmntBox.value;
                return (cmntValue.length > 0 && !!(cmntValue.trim()))
            }
            
            const activatePostButton = () => {
                cmntPost.disabled = !checkValue();
            }

Class

  • constructor는 객체가 새로 생성될 때 동작하는 함수로 class 가 담고있는 함수들을 호출시켜줬다.
  • crete()
    • appendChild, createElement, className, innerHTML 등을 dom요소를 이용하여 댓글을 생성하는 함수다.
    • 내가 구현한 페이지에서는 아이디, 댓글내용, 좋아요, 삭제 항목이 1줄의 flex요소로 이루어져있기 때문에 다음과 같이 코드를 작성했다.
    • 마지막에는 댓글창에 빈 값을 주어 댓글이 생성되면 댓글창이 초기화되도록 구현했다.
    • 그리고 이건 나중에 추가한 기능인데, 실제 인스타그램에서는 빈 공백이 작성되면 게시 버튼이 활성화 되지 않으며 게시도 되지 않는다.
    • 이 부분을 적용하기 위해 checkValue라는 함수를 클래스 밖에서 선언해줬고, create객체 함수가 실행될 때 조건으로 걸었다.
  • liked(), deleted()
    • 좋아요 기능은 눌리면 이미지 소스가 빨간 하트로, 다시 눌리면 빈 하트로 변경되도록 구현했다.
    • 반복적으로 눌려도 좋아요 기능이 구현되도록 likeState라는 키값을 constructor에 줬다. 좋아요 눌리면 1, 없애면 0
    • 삭제도 마찬가지로 발동되면 모든 요소들을 삭제한다.
    • 초기에 함수 이름을 like, delete로 정의했다가 전역변수의 create 함수 안에 like, delete와 충돌을 일으켜 애를 먹었다.. ⇒ 변수 이름의 중요성,,
        class Comment {
            constructor() {
                this.likeState = 0;
                this.create();
                this.liked();
                this.deleted();
            }
        
            create(){
        
                if(checkValue()){
                    let cmntValue = cmntBox.value;
        
                    this.cmntValue = cmntValue;
                    
                    this.myId = document.createElement('span');
                    this.myId.className = 'userOfComment';
                    this.myId.innerHTML = 'ore.zeno';
                    user.appendChild(this.myId);
        
                    this.newcmnt = document.createElement('span');
                    this.newcmnt.className = 'commentOfUser';
                    this.newcmnt.innerHTML = this.cmntValue;
                    cmnt.appendChild(this.newcmnt);
        
                    this.like = document.createElement('img');
                    this.like.className = 'comment-like';
                    this.like.src = 'image/love.png';
                    this.like.alt = '댓글-좋아요';
                    cmntLike.appendChild(this.like);
        
                    this.delete = document.createElement('img');
                    this.delete.className = 'comment-delete';
                    this.delete.src = 'image/delete.png';
                    this.delete.alt = '댓글-삭제';
                    cmntDelete.appendChild(this.delete);
        
                    cmntBox.value = '';
                }
            }
        
            liked(){
                this.like.addEventListener('click', ()=>{
                    if(this.likeState == 0){
                        this.like.src = 'image/liked.png';
                        this.likeState = 1;
                    }
                    else {
                        this.like.src = 'image/love.png';
                        this.likeState = 0;                
                    }
                })
            }
        
            deleted (){
                this.delete.addEventListener('click', ()=>{
                        
                    this.myId.remove();
                    this.newcmnt.remove();
                    this.like.remove();
                    this.delete.remove();
        
                })
            }
        }
        ```
profile
학습한 지식을 개인적으로 정리하기 위해 만든 블로그입니다 :)

0개의 댓글