TIL_47_with Wecode 037 인스타그램 클론 마무리

JIEUN·2021년 3월 13일
0
post-thumbnail

어쩌저찌 위코드 4주차가 되었고 인스타그램 클론도 마무리 단계에 접어들었다. 따로 추가기능들을 구현할 예정이지만 공식적으로는 마무리가 되었기에 간단한 리뷰를 남기도록 하겠다. 코드리뷰 후 부족한 코드들은 보충을 하고, 잘못된 코드들도 수정을 하였고 scss 네스팅도 완료하였다. 여전히 좋은 코드라고 할 순 없지만, 그래도 이전보다는 나아졌다는 것에 스스로를 다독여본다.


폴더의 구성은 이렇게 이루어져있다. 아래 styles 폴더에는 공통적으로 사용하는 reset.scss 또는 common.scss 등과 같은 파일들이 들어가있고, 상단의 Components 폴더에는 팀안에서 공통으로 사용할 Component 파일들을 넣는것이 적절하다. 일단 나는 개인 Components 폴더를 만들어 그 안에서 Component들을 분리해놓고 진행하였다.

로그인 페이지

//Login.js
import React from "react";
import { withRouter } from "react-router-dom";
import { Link } from "react-router-dom";
import "./Login.scss";

class Login extends React.Component {
  constructor() {
    super();
    this.state = {
      id: "",
      password: "",
      btnChangeId: "",
      btnChangePw: "",
    };
  }
  
  goToMain = async () => {
    const { id, password } = this.state;
    if (id.includes("@") && password.length >= 8) {
      return alert("아이디와 비밀번호를 제대로 입력해주세요");
    }
    
    const loginCheck = await fetch("http://10.58.1.171:8000/user/login", {
      method: "POST",
      body: JSON.stringify({
        email: this.state.id,
        password: this.state.password,
      }),
    })
      .then((res) => res.json())
      .then((res) => res.status);

처음으로 백엔드와 맞춰보았던 로그인/회원가입!
로그인/회원가입 부분은 따로 포스팅에 기록하겠다.

    if (parseInt(loginCheck) === 200) {
      alert("🎉로그인 성공🎉");
      this.props.history.push("/main");
    } else {
      alert("아이디 또는 비밀번호를 확인해주세요.");
    }
  };

  handleInputValue = (e) => {
    const { id, value } = e.target;
    //계산된 속성
    this.setState({ [id]: value });
  };

계산된 속성을 사용할 경우에는 [ ] 로 감싸주는게 약속.
id 라는 키값을 가지고 있는 input에 어떤 입력값이 생기면 업데이트 해주기 위해서 선언된 함수이다.
id 와 password 인풋창에 어떤 값이 입력될 경우 setState해주기 위한 것.
코드 리뷰 받기 전까지는 id 따로, password 따로
함수를 만들어서 지정해줬었는데 그럴 필요가 없었던 것이다.

  render() {
    const { id, password } = this.state;
    //비구조화 할당
    const changeHandlerBgColor = id.includes("@") && password.length >= 8;
    return (
      <div id="wrap_main">
        <main className="container">
          <h1 className="westagram">Westagram</h1>
          <div className="mainlayout">
            <div className="inputs">
              <input
                id="id"
                type="text"
                placeholder="아이디"
                onChange={this.handleInputValue}
              />
              <input
                id="password"
                type="password"
                placeholder="비밀번호"
                onChange={this.handleInputValue}
              />
            </div>
            <div className="btn_container">
              <button className=
                {`${changeHandlerBgColor ?
             "trueColor" : "falseColor"} btn`}
                onClick={this.goToMain}>
       /*위에서 선언한 조건문을 가져와 
        삼항연산자 조건문을 통해 버튼 색깔이 바뀔 수 있도록 했다.*/
      //클래스 네임도 두개가 지정된 상태
                로그인 </button>
            </div>
            <div className="divider">
              <div className="line_l"></div>
              <div className="centerchar">또는</div>
              <div className="line_r"></div>
            </div>
            <div className="facebook">
              <button className="btn_fb">
                <i className="fab fa-facebook-square" />
                <span className="fb_char">Facebook으로 로그인</span>
              </button>
            </div>
            <div className="password_container">
              <button className="btn_password">비밀번호를 잊으셨나요?</button>
            </div>
            <footer className="joinform">
              <p className="join">
                계정이 없으신가요?
                <button className="btn_join">
                  <Link to="/Signup">가입하기</Link>
                </button>
              </p>
            </footer>
          </div>
        </main>
      </div>
    );
  }
}
export default withRouter(Login);
//Login.scss
$theme_color: white;
$theme_backgroundColor: lightskyblue;
$border_style: 1px solid lightgray;

#wrap_main {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100vh;

  .container {
    width: 350px;
    height: 400px;
    border: $border_style;

    .westagram {
      font-family: "Lobster", cursive;
      text-align: center;
      font-size: 45px;
      margin: 40px;
    }

    .mainlayout {
      display: flex;
      flex-direction: column;

      .inputs {
        display: flex;
        flex-direction: column;
        align-items: center;

        #id,
        #password {
          width: 250px;
          height: 30px;
          margin: 3px;
          padding: 3px;
          padding-left: 8px;
          font-size: 11px;
          border: $border_style;
          border-radius: 3px;
        }
      }

      .btn_container {
        display: flex;
        flex-direction: column;
        align-items: center;

        .btn {
          width: 260px;
          height: 32px;
          margin: 15px;
          background-color: $theme_backgroundColor;
          color: $theme_color;
          font-weight: bold;
          font-size: 14px;
          border: 0;
          border-radius: 3px;
        }

        .trueColor {
          background-color: #1565c0;
        }

        .falseColor {
          background-color: $theme_backgroundColor;
        }
      }

      .divider {
        display: flex;
        flex-direction: low;
        justify-content: center;
        margin-top: 10px;

        .line_l {
          width: 95px;
          margin-top: 5px;
          margin-bottom: 20px;
          border: 0.5px solid lightgray;
        }

        .centerchar {
          color: lightslategray;
          margin-left: 20px;
          margin-right: 20px;
          font-size: 13px;
          font-weight: bold;
        }

        .line_r {
          width: 95px;
          margin-top: 5px;
          margin-bottom: 20px;
          border: 0.5px solid lightgray;
        }
      }

      .facebook {
        display: flex;
        justify-content: center;
        margin-top: 15px;
        margin-bottom: 15px;

        .btn_fb {
          width: 50%;
          cursor: pointer;
          background-color: $theme_color;
          color: navy;
          border: 0;
          font-weight: bold;
        }

        .fa-facebook-square {
          width: 15px;
          height: 15px;
        }
      }

      .password_container {
        display: flex;
        justify-content: center;
        margin-bottom: 15px;

        .btn_password {
          cursor: pointer;
          background-color: $theme_color;
          border: 0;
          font-size: 12px;
        }
      }

      .joinform {
        display: flex;
        justify-content: center;
        width: 100%;
        height: 60px;
        margin-top: 22px;
        border: $border_style;

        .join {
          display: flex;
          align-items: center;
          padding: 5px;
          font-size: 14px;
        }

        .btn_join {
          display: flex;
          align-items: center;
          cursor: pointer;
          background-color: $theme_color;
          color: blue;
          padding: 5px;
          border: 0;
          font-weight: bold;
        }
      }
    }
  }
}

하기전에는 네스팅 하는거 좀 번거롭게 느껴져셔 미루다가 했는데.. 재밌었던 네스팅.. 오히려 구분도 잘되고 더 직관적이어서 하고나니 장점이 바로 느껴졌다.

회원가입 페이지

import React, { Component } from "react";
import "../Login/Login.scss";
import "../../../Styles/reset.scss";

class Signup extends Component {
  constructor() {
    super();
    this.state = {
      id: "",
      password: "",
    };
  }
  
  gotoSign = async () => {
    const { id, password } = this.state;
    const signCheck = await fetch("http://10.58.1.171:8000/user/signup", {
      method: "POST",
      body: JSON.stringify({
        email: this.state.id,
        password: this.state.password,
      }),
    })
      .then((res) => res.json())
      .then((res) => res.status);

    if (parseInt(signCheck) === 200) {
      alert("회원가입에 성공하였습니다.");
    } else {
      alert("아이디의 이메일 형식 또는 비밀번호가 8자리 이상인지 확인해주세요");
    }
    if (id.includes("@") && password.length >= 8) {
      this.props.history.push("/");
    }
  };

  handleInputValue = (e) => {
    const { id, value } = e.target;
    this.setState({
      [id]: value,
    });
  };

  changeHandlerBgColor = () => {
    const { btnChangeId, btnChangePw } = this.state;
    return btnChangeId && btnChangePw ? "trueColor" : "falseColor";
  };

  render() {
    const { id, password } = this.state;
    const changeHandlerBgColor = id.includes("@") && password.length >= 8;
    return (
      <div id="wrap_main">
        <main className="container">
          <h1 className="westagram">Westagram</h1>
          <div className="mainlayout">
            <div className="inputs">
              <input
                id="id"
                type="text"
                placeholder="아이디를 이메일 형식에 맞게 입력하세요"
                onChange={this.handleInputValue}
              />
              <input
                id="password"
                type="password"
                placeholder="8자리 이상의 비밀번호를 입력하세요"
                onChange={this.handleInputValue}
              />
            </div>
            <div className="btn_container">
              <button
                className={`${
                  changeHandlerBgColor ? "trueColor" : "falseColor"
                } btn`}
                onClick={this.gotoSign}
              >
                가입하기
              </button>
            </div>
          </div>
        </main>
      </div>
    );
  }
}
export default Signup;

로그인 페이지 그대로 복붙하고 내용만 바꾼거라서 따로 리뷰할 내용이 없다. 로그인 페이지와 마찬가지로 백엔드에서 fetch 주소를 받아왔다. 회원가입 페이지에서 만든 아이디와 비밀번호를 로그인 페이지에서 그대로 입력하여야 메인페이지로 넘어가는 방식이다. 백엔드에서의 조건이 아이디는 이메일 형식이어야 하고, 비밀번호는 8자리 이상으로 해놓았기 때문에 나도 그대로 적용했다.

메인 페이지

//Main.js
import React from "react";
import Nav from "../Components/Nav";
import MainComponent from "../Components/MainComponent";
import Side from "../Components/Side";
import "../Main/Main.scss";

class Main extends React.Component {
  render() {
    return (
      <>
        <Nav />
        <MainComponent />
        <Side />
      </>
    );
  }
}

export default Main;

주요 섹션들을 컴퍼넌트화 했다. 메인.js 코드가 이렇게 깔끔해지니 괜히 뿌듯..

//Main.scss
body {
  width: 100%;

  .header {
    position: fixed;
    width: 100%;
    z-index: 5;
    background-color: white;
    border: 1px solid lightgray;

    .header_container {
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 60px;

      .logo {
        margin-left: 150px;
        font-family: "Lobster", cursive;
        font-size: 25px;
      }

      .header_bar {
        display: flex;
        justify-content: center;
        align-items: center;

        .search_bar {
          width: 170px;
          height: 20px;
          border: 0.5px solid lightgray;
          border-radius: 3px;
        }

        .fa-search {
          order: -1;
        }
      }

      .icons {
        display: flex;
        margin-right: 170px;

        .fa-home,
        .fa-comments,
        .fa-compass,
        .fa-heart {
          font-size: 23px;
          margin-right: 17px;
        }

        .profile_img {
          width: 22px;
          height: 22px;
          border-radius: 70%;
          margin-right: 17px;
        }
      }
    }
  }

  .main_container {
    position: relative;
    width: 900px;
    height: 1600px;

    .feeds {
      position: absolute;
      width: 600px;
      left: 17%;
      margin-top: 100px;
      border: 0.5px solid lightgray;

      .feeds_header {
        margin-top: 15px;
        margin-left: 10px;
        margin-bottom: 15px;

        .account_info {
          display: flex;
          justify-content: space-between;

          .profile {
            width: 35px;
            height: 35px;
            border-radius: 70%;
          }

          .account_id {
            font-size: 13px;
            font-weight: bold;
            margin-top: 4px;
            margin-right: 430px;
          }

          .more_info {
            margin-top: 10px;
            margin-right: 10px;
          }
        }
        .wrap_location {
          display: flex;
          flex-direction: row;
          .location {
            font-size: 13px;
            margin-left: 48px;
            margin-top: -13px;
          }
        }
      }

      .main_photo {
        width: 600px;
        height: 750px;
      }

      .section_footer {
        display: flex;
        justify-content: space-around;

        .icons2 {
          margin-right: 470px;

          .fa-grin-hearts,
          .fa-comment-dots,
          .fa-paper-plane {
            font-size: 25px;
            margin-top: 5px;
            margin-left: 5px;
          }
          .fa-grin-hearts {
            cursor: pointer;
          }
        }
        .fa-bookmark {
          font-size: 25px;
          margin-top: 5px;
        }
      }

      .like_number {
        font-size: 15px;
        font-weight: bold;
        margin-top: 10px;
        margin-left: 10px;
      }

      .section_description {
        margin-top: 8px;
        margin-left: 11px;

        .wrap_description {
          display: flex;

          .nickname {
            font-weight: bold;
          }

          .description {
            font-size: 13px;
            margin-top: 4px;
            margin-left: 6px;
          }
        }
      }

      .textBox {
        font-size: 13px;
        margin-top: 10px;
        margin-left: 10px;
      }

      .time {
        font-size: 10px;
        margin-top: 20px;
        margin-left: 10px;
        color: gray;
      }

      .section_comment {
        display: flex;
        justify-content: space-around;
        width: 600px;
        margin-top: 10px;
        border-top: 0.5px solid lightgray;

        .fa-smile {
          position: relative;
          top: 5px;
          font-size: 23px;
        }

        .input_comment {
          width: 500px;
          height: 40px;
          font-size: 14px;
          margin-left: 5px;
          border: 0;
          outline: none;
        }

        .enter {
          background-color: white;
          color: lightskyblue;
          font-size: 15px;
          font-weight: bold;
          border: 0;
          outline: none;
        }
      }
    }
  }

  .side_right {
    position: fixed;
    z-index: 6;
    width: 300px;
    top: 100px;
    right: 180px;

    .about_me {
      display: flex;
      margin: 10px;

      .me {
        width: 50px;
        height: 50px;
        border-radius: 70%;
      }

      .yang {
        margin-top: 12px;
        margin-left: 13px;
        font-size: 14px;
        font-weight: bold;
      }
    }

    .first {
      display: flex;
      justify-content: space-between;

      .id {
        color: grey;
        margin-top: -30px;
        margin-left: 72px;
        font-size: 14px;
      }

      .change {
        color: rgb(40, 169, 250);
        margin-top: -40px;
        margin-right: 10px;
        font-size: 12px;
        font-weight: bold;
      }
    }

    .second {
      display: flex;
      justify-content: space-between;

      .recommend {
        color: rgb(125, 139, 153);
        margin-top: 13px;
        margin-left: 13px;
        font-size: 14px;
        font-weight: bold;
      }

      .recommend_side {
        margin-top: 13px;
        margin-right: 10px;
        font-size: 13px;
        font-weight: bold;
      }
    }

    .their_description {
      color: grey;
      margin-top: -30px;
      margin-left: 73px;
      font-size: 12px;
    }
  }
}

컴퍼넌트 파일들

//Nav.js
import React, { Component } from "react";
import { Link } from "react-router-dom";

class Nav extends Component {
  render() {
    return (
      <nav className="header">
        <div className="header_container">
          <h1 className="logo">
            <Link to="/">Westagram</Link>
          </h1>
          <div className="header_bar">
            <input className="search_bar" type="text" />
            <i className="fas fa-search" />
          </div>
          <div className="icons">
            <i className="fas fa-home" />
            <i className="far fa-comments" />
            <i className="far fa-compass" />
            <i className="far fa-heart" />
            <img
              className="profile_img"
              src="https://media.vlpt.us/images/poohv7/post/d60edb23-650d-4c2b-8880-b5b6aa66d320/my%20profile.jpg"
              alt="profile img"
            />
          </div>
        </div>
      </nav>
    );
  }
}

export default Nav;

네브 바는 그냥 그대로 네브 부분 똑 떼와서 새로 파일 만든것 밖에 없어서 남길 멘트가 없다.. 아, 코드 리뷰 후 셀프클로징이 가능한 태그들은 다 수정해주었다.

//MainComponent.js
import React, { Component } from "react";

class MainComponent extends Component {
  constructor() {
    super();

    this.state = {
      value: "",
      commentList: [
        {
          name: "",
          text: "",
        },
      ],
    };
  }

  inputComment = (e) => {
    this.setState({ value: e.target.value });
  };

  pressEnter = (e) => {
    this.setState({
      commentList: this.state.commentList.concat({
        name: "yang_ji_eun ",
        text: this.state.value,
      }),
    });
  };

  render() {
    return (
      <main className="main_container">
        <div className="feeds">
          <section className="feeds_header">
            <div className="account_info">
              <img
                className="profile"
                src="https://media.vlpt.us/images/poohv7/post/d60edb23-650d-4c2b-8880-b5b6aa66d320/my%20profile.jpg"
                alt="profile img"
              />
              <div className="account_id">yang_ji_eun</div>
              <div className="more_info">•••</div>
            </div>
            <div className="wrap_location">
              <div className="location">Brisbane Australia</div>
            </div>
          </section>
          <section className="section_main">
            <img
              className="main_photo"
              src="https://media.vlpt.us/images/poohv7/post/ab43d22f-6e37-4150-be54-3e425a973086/main%20photo.jpg"
              alt="main img"
            />
          </section>
          <section className="section_footer">
            <div className="icons2">
              <i className="fas fa-grin-hearts" />
              <i className="far fa-comment-dots" />
              <i className="far fa-paper-plane" />
            </div>
            <i className="far fa-bookmark" />
          </section>
          <section className="section_input">
            <div className="like_number">좋아요 20,549</div>
            <div className="section_description">
              <div className="wrap_description">
                <div className="nickname">yang_ji_eun</div>
                <div className="description">조명맛집들</div>
              </div>
            </div>
            <ul className="textBox">
              {this.state.commentList.map((el, key) => {
                return (
                  <li key={key}>
                    <span>{el.name}</span>
                    <span>{el.text}</span>
                  </li>
                );
              })}
            </ul>
            <div className="time">5시간 전</div>
          </section>
          <section className="section_comment">
            <div className="comment_box">
              <i className="far fa-smile" />
              <input
                className="input_comment"
                type="text"
                placeholder="댓글 달기..."
                onChange={this.inputComment}
                value={this.state.value}
              />
            </div>
            <button className="enter" onClick={this.pressEnter}>
              게시
            </button>
          </section>
        </div>
      </main>
    );
  }
}

export default MainComponent;

네브바와 마찬가지로 똑 떼서 새로 파일 만들어준 것 뿐.. 댓글 기능 관련되어서는 이미 포스팅을 했기 때문에 자세한 내용은 생략하겠다.

//Side.js
import React, { Component } from "react";
import SideComponent from "../Components/SideComponent";

class Side extends Component {
  render() {
    const sideComponentLists = [
      {
        name: "wombat",
        src:
          "https://media.vlpt.us/images/poohv7/post/8011c9a2-97ac-4f26-9a59-4226809b10fa/KakaoTalk_20210225_133013188.jpg",
        description: "안녕하세요 웜뱃입니다.",
      },
      {
        name: "cat",
        src:
          "https://media.vlpt.us/images/poohv7/post/821a116f-1508-4417-b100-905ffa4e473d/KakaoTalk_20210225_132834557.jpg",
        description: "고양이다옹. 맞팔환영이라옹.",
      },
      {
        name: "platypus",
        src:
          "https://media.vlpt.us/images/poohv7/post/34d9f7e2-0921-46bc-aacc-6a33a85781c5/KakaoTalk_20210225_133012878.jpg",
        description: "92년생, 주로 물가에 서식",
      },
      {
        name: "i_am_a_dog",
        src:
          "https://media.vlpt.us/images/poohv7/post/2b4246cc-4cb9-46ee-a9d8-fb938612384f/KakaoTalk_20210225_133011781.jpg",
        description: "강아지(30)",
      },
      {
        name: "quokka",
        src:
          "https://media.vlpt.us/images/poohv7/post/be1b1e79-b823-4aae-9cfb-096281eda8e4/1.jpg",
        description: "이런 귀한 곳에 누추한 분이..",
      },
    ];
    
    return (
      <section className="wrap_sideright">
        <div className="side_right">
          <div className="about_me">
            <img
              className="me"
              src="https://media.vlpt.us/images/poohv7/post/d60edb23-650d-4c2b-8880-b5b6aa66d320/my%20profile.jpg"
              alt="profile img"
            />
            <div className="yang">yang_ji_eun</div>
          </div>
          <div className="first">
            <div className="id">지은</div>
            <div className="change">전환</div>
          </div>
          <div className="second">
            <div className="recommend">회원님을 위한 추천</div>
            <div className="recommend_side">모두 보기</div>
          </div>
          {sideComponentLists.map((el, idx) => {
            return (
              <SideComponent
                key={idx}
                nickName={el.name}
                img={el.src}
                content={el.description}
              />
            );
          })}
        </div>
      </section>
    );
  }
}

export default Side;

대망의 사이드 바. 댓글기능에서 map함수를 사용하긴 했지만, 메인에서는 댓글들의 리스트를 나열하기 위해 사용했다면, 위 사이드에서는 지정한 배열을 가져와서 배열 안에 있는 객체를 나열하기 위해 사용되었다. 바로 전 포스트 내용인 본죽 리스트를 만드는 것과 같다고 보면 된다. 내 위스타그램 메인페이지 사이드 바에는 친구추천 목록이 있는데 각 친구들의 이미지, 이름, 설명은 다르지만 구성은 똑같다. 하나의 구성을 컴퍼넌트화 하여 맵함수로 통해 좀 더 간단하게 바꿀 줄 알아야 한다.

Side.js 상단의 코드를 보면, sideComponentLists 라는 이름의 배열을 선언해주었다. 그 배열안에는 5개의 객체가 있고 5개의 객체 안에는 각 각 3개의 키와 값이 있다. 이 배열을 맵 함수를 통해 새로운 배열로 반환할 것이다. 어떤 식의 새로운 배열로 반환할 것이냐면, Sidecomponent를 만들어 그 컴퍼넌트 안에 각 배열마다 바껴야 하는 요소들을 props화 했다. 이 props 들을 Sidecomponent에서 어떻게 이용되는지 봐보자.

//SideComponent.js
import React, { Component } from "react";

class SideComponent extends Component {
  render() {
    return (
      <>
        <div className="about_me">
          <img className="me" src={this.props.img} alt="profile img" />
          <div className="yang">{this.props.nickName}</div>
        </div>
        <div className="first">
          <div className="their_description">{this.props.content}</div>
          <div className="change">팔로우</div>
        </div>
      </>
    );
  }
}

export default SideComponent;

이 파일이 반복되는 하나의 구성만 똑 떼와 컴퍼넌트화 시킨 파일이다. 구성은 똑같지만 이름, 이미지, 설명이 각각 다르게 들어가야하므로, 위에서 만든 props 를 지정해주면 된다. 각각 다르게 들어가야 하는 이미지, 이름, 설명을 각각 지정해주었다.

너무나 이해가 가지 않았던 state 와 props 그리고 map. 사이드바의 코드를 정리하면서 정말 많이 깨우쳤던 것 같다.
한 시간 반동안 나를 이해시키기 위해 부단히 노력해주신 멘토님께 감사를..


항상 잊지말자 리팩토링!

0개의 댓글