[Team Project] Instagram Clone Project | 리액트 인스타그램 클론 프로젝트

이은진·2020년 12월 13일
7

Projects

목록 보기
2/4

1. Project Intro

1-1. 프로젝트 소개

  • 사진, 동영상, 커머스 서비스를 이용할 수 있는 SNS인 Instagram 클론코딩
  • Vanilla JS로 구현한 클론 페이지를 React를 통하여 구조화 연습
  • CRA를 통한 초기 프로젝트 세팅 연습
  • Git을 활용하여 React 컴포넌트 공유 및 코드리뷰 등 팀 협업 훈련

1-2. 구성원

이은진(PM), 김별이, 박소윤, 안미현, 한채빈

1-3. 기간

  • 2020년 11월 30일 ~ 2020년 12월 13일 (2 Weeks)

1-4. 적용 기술

  • React.js
  • React Router
  • Sass
  • Git

2. Main Features

2-1. 로그인 페이지

이메일 및 패스워드 validation 기능

이메일과 비밀번호를 입력한 후 validation이 이루어지는 시점이 로그인 버튼을 누를 때가 아니라 폼에 특정 값을 입력 후 다른 폼으로 옮겨가거나 다른 곳을 클릭했을 때로 설정하고 싶어, handleIdOnBlur()와 handlePasswordOnBlur() 함수를 만들었습니다. 유효하지 않은 값을 입력하면 해당 입력창에 빨간 테두리가 생깁니다. 이메일은 '@'가 포함됐을 때, 비밀번호는 5자 이상일 때 isIdError와 isPasswordError의 state값이 false로 설정됩니다.

constructor() {
    super()
    this.state = {
      id: '',
      password: '',
      isPasswordHidden: true,
      isIdError: false,
      isPasswordError: false,
    }
  }

  //get input value
  handleSubmitChange = (e) => {
    const { name, value } = e.target
    this.setState({
      [name]: value,
    })
  }

  //check validation id only after first onBlur
  handleIdOnBlur = () => {
    const { id } = this.state
    if (id.includes('@')) {
      this.setState({
        isIdError: false,
      })
    } else {
      this.setState({
        isIdError: true,
      })
    }
  }

  //check validation pw only after first onBlur
  handlePasswordOnBlur = () => {
    const { password } = this.state
    if (password.length >= 5) {
      this.setState({
        isPasswordError: false,
      })
    } else {
      this.setState({
        isPasswordError: true,
      })
    }
  }

Fetch를 활용한 API 통신으로 회원가입 및 로그인 기능 구현

백엔드 로그인 API를 붙여 보았습니다. Fetch 함수를 이용하여 사용자의 이메일과 아이디를 입력하고 회원가입을 하면, 데이터가 stringify되어 보내지고, 응답 메시지로 SUCCESS를 받으면 회원가입이 완료됩니다. 로그인도 마찬가지로 서버에 이메일과 비밀번호가 보내진 후, SUCCESS를 받으면 환영한다는 문구가 띄워지고 메인페이지로 이동됩니다.

//login
  logIn = (e) => {
    e.preventDefault()
    const { id, password, isIdError, isPasswordError } = this.state
    if (!isIdError && !isPasswordError) {
    fetch('API', {
      method: 'POST',
      body: JSON.stringify({
        email: id,
        password: password,
      }),
    })
      .then((response) => response.json())
      .then((result) => {
        if (result.message === "SUCCESS") {
          alert(`Welcome ${id}`)
          this.props.history.push("/main-eunjinlee")
        } else {
          alert("Login failed")
        }
      }).catch(err => alert(err))
    }
  }

  //signup
  signUp = (e) => {
    e.preventDefault()
    const { id, password, isIdError, isPasswordError } = this.state
    if (!isIdError && !isPasswordError) {
    fetch('API', {
      method: 'POST',
      body: JSON.stringify({
        email: id,
        password: password,
      }),
    })
      .then((response) => response.json())
      .then((result) => {
        if (result.message === "SUCCESS") {
          alert("Signed up")
        } else {
          alert("Sign up failed. Retry.")
        }
      }).catch(err => alert(err))
    }
  }

2-2. 메인 페이지

유저 검색 기능

미리 만들어 둔 json 형식의 mock data를 이용해서 검색기능을 구현했습니다. 먼저 검색창에서 입력값을 searchValue의 state값으로 설정했습니다. 그리고 componentDidMount() 함수로 mock data를 fetch해서 searchList의 state값으로 설정했습니다. 이렇게 되면 컴포넌트 렌더 후 불러와진 사용자 데이터를 filter() 메서드를 이용해서 사용자 아이디와 이름 중 검색어를 포함한 정보가 있다면 filteredList라는 변수에 넣어, filteredList가 map() 메서드로 하나씩 렌더되도록 했습니다.

class Search extends React.Component {
  
  constructor() {
    super()
    this.state = {
      searchList: [],
      searchValue: '',
    }
  }

  getInputValue = (e) => {
    this.setState({
      searchValue: e.target.value,
    })
  }

  componentDidMount = () => {
    fetch('http://localhost:3000/data/userInfos-eunjinlee.json') 
      .then(response => response.json())
      .then(data => {
        this.setState({
          searchList: data.userInfos
        })
      })
  }
  ...

댓글 입력, 삭제, 좋아요 기능

댓글 입력도 검색과 마찬가지로, 먼저 1. commentValue에 e.target.value 값을 state 값으로 넣어줍니다. 2. 그 값을 이용하여 addComment() 함수에서 newComment라는 변수에 새 댓글에 대한 초기 데이터가 들어 있는 오브젝트를 넣어줍니다. 그리고 3. spread 연산자를 이용해 기존에 commentList가 가지고 있던 state값에 newComment를 추가해 줍니다.

삭제 기능은 자바스크립트의 기본적인 filter() 메서드로 구현했습니다. 1. 클릭한 댓글의 id값을 인자로 받는 removeComment() 함수를 만들었습니다. 2. 클릭한 댓글의 id값이 아닌 것들만 필터해서 새로 filteredComments 변수에 넣어준 후, 3. 그것을 commentList의 state값으로 새로 설정했습니다. 삭제 즉시 해당 댓글만 제외된 리스트만 렌더됩니다.

좋아요 기능은 map() 메서드로 구현했습니다. 1. likeComment() 함수가 클릭한 댓글의 id값을 인자로 받아, 해당 id값을 가진 댓글의 "like" 키값의 value를 반대로 토글할 수 있도록 했습니다. 2. 그렇게 반대로 토글한 댓글을 포함한 리스트를 newLikedComments라는 변수에 넣어주고, 기존의 commentList state값으로 설정해주었습니다. 댓글 오른쪽의 하트를 토글할 수 있게 됩니다.

class Comments extends React.Component {

  constructor() {
    super()
    this.state = {
      commentList: [],
      commentValue: '',
      openComments: false,
    }
  }
  
  // getting input value from comment form
  getInputValue = (e) => {
    this.setState({
      commentValue: e.target.value.trim(),
    })
  }

  // posting comments
  addComment = (e) => {
    e.preventDefault()
    const { commentList } = this.state
    const newComment = {
      id: commentList.length + 1,
      userId: 'workoutbutlazy', 
      text: this.state.commentValue, 
      like: false,
    }
    this.setState({
      commentList: [...commentList, newComment],
      value: '',
    })
    e.target.reset()
  }

  //remove comments
  removeComment = (id) => {
    const filteredComments = this.state.commentList.filter((comment) => comment.id !== id)
    this.setState({commentList: filteredComments})
  }

  //like comments
  likeComment = (id) => {
    const newLikedComments = this.state.commentList.map((comment) => {
      if (comment.id === id) {
        comment.like = !comment.like
        }
      return comment
    })
    this.setState({commentList: newLikedComments})
  }
  
  ...

3. Details

피드 본문 더보기, 댓글 더보기 기능

실제 인스타그램에서 본문의 길이가 길 경우 더보기 버튼으로 모두 볼 수 있습니다. isArticleOpen의 state값에 따라 클래스네임을 추가하여 scss에서 본문 숨기기 더보기를 위한 속성값을 설정했습니다.

// Feeds.js
class Feeds extends React.Component {

  constructor() {
    super()
    this.state = {
      isArticleOpen: false
    }
  }

  render() {
    const { isArticleOpen } = this.state
	  ...
            <article className='articleContainer'>
              <span>
                <a className={isArticleOpen ? 'open' : ''}> eunjinlog </a>
                <p className={isArticleOpen ? 'open' : ''}>
                  (본문)
                </p>
                <button
                  className={isArticleOpen ? 'open' : ''}
                  onClick={() => this.setState({isArticleOpen: !isArticleOpen})}>
                  {isArticleOpen ? 'hide' : 'more'}
                </button>
              </span>
            </article>
      	  ...

댓글을 입력하면 댓글의 개수대로 'View all 4 comments'와 같은 버튼 문구가 업데이트되고, 토글하면 'Hide comments' 버튼으로 바뀌도록 했습니다. 삼항연산자로 render() 함수 안에서 간단히 구현했습니다. 보여지고 숨겨지는 댓글의 개수는 scss에서 설정했습니다.

// Comments.js

  // toggle comments list
  viewComments = () => {
    this.setState({
      openComments: !this.state.openComments
    })
  }
  ...
  
  render() {
    const { commentList, openComments, commentValue } = this.state
    const { viewComments, removeComment, likeComment, addComment, getInputValue } = this

    return (
      <div className='Comments'>
        <div className='commentsContainer'>
          <p 
            className='viewAll'
            onClick={viewComments}>
            {openComments ? "Hide comments" : `View ${commentList.length === 1 ? '' : 'all'} ${commentList.length} comment${commentList.length === 1 ? '' : 's'}`}
          </p>
          <ul className={openComments ? 'view' : ''}>
	...

팔로우 버튼 토글

팔로우 버튼은 위에서 구현했던 좋아요와 같은 방법으로 구현했습니다. id값을 인자로 받는 followUser(id) 함수 안에, 좋아요를 누른 유저의 아이디에 해당하는 경우 "isFollowing" 의 value 값을 반대로 토글하도록 했습니다.

class Aside extends React.Component {
  constructor() {
    super()
    this.state = {
      users: [],
    }
  }

  followUser = (id) => {
    const newFollowingList = this.state.users.map((user) => {
      if (user.userId === id) {
        user.isFollowing = !user.isFollowing
      }
      return user
    })
    this.setState({users: newFollowingList})
  }

상단 Nav바 모달창


내비게이션 바의 프로필 사진을 누르면 모달창이 뜨도록 했고, 닫을 때는 모달창을 제외한 화면의 어떤 곳을 클릭해도 닫을 수 있도록 했습니다.

class Nav extends React.Component {

  constructor() {
    super()
    this.state = {
      isMenuOpen : false,
    }
  }

  toggleMenuBtn = () => {
    this.setState({
      isMenuOpen: !this.state.isMenuOpen
    })
  }

Fetch API를 통해 data 불러오기


로컬의 mock data를 불러와서 스토리 부분이나 사용자 검색 기능을 구현했습니다. 실제 코드에서는 배열에서 원하는 데이터 갯수만큼 랜덤하게 뽑는 함수를 추가로 만들어서, 새로고침 후 렌더가 새로 될 때마다 랜덤으로 사용자가 렌더되도록 했습니다.

class Stories extends React.Component {
  constructor() {
    super()
    this.state = {
      users: [],
    }
  }
  
  fetch('http://localhost:3000/data/userInfos-eunjinlee.json')
  	.then(response => response.json())
    .then(data => {
      this.setState({
        users: data.users
      })
    })

Responsive web


실제 인스타그램에서 브라우저의 가로길이를 줄이면 반응형으로 콘텐츠가 축약되어서 나오는 모습을 따라해보았습니다.

@media (max-width: 1000px) {
  .leftRightContainer {
    @include flex-center;
    width: 614px;
  }
}

4. Review

4-1. 팀워크

처음으로 깃을 이용해서 팀원들과 함께 CRA 세팅을 하고 컴포넌트를 공유해 보았습니다. 제가 만들어 공유한 컴포넌트로 인해서 팀원들의 로컬에서 발생한 컨플릭트에 긴 시간을 보냈지만, 깃에 적응하고 깃을 사용하는 데 용감해지게 되는 기회가 되었습니다. 사실 팀으로 하나의 결과물을 낸 건 아니고, 이번 프로젝트에서는 본인이 바닐라 JS로 짠 코드를 리액트로 옮기는 과정을 팀별로 한 레포지토리를 공유해서 진행한 것이라 개인 프로젝트나 마찬가지긴 했습니다. 하지만 처음으로 다른 사람들과 작은 설정들을 공유하고 합의점을 찾아 코드를 짜는 경험은 정말 유익했습니다.

4-2. 깃

깃에 익숙하지 않아서 실수로 작업한 것을 한방에 날려버린 경험을 하게 해 준 프로젝트였습니다. 복구도 할 수 없는 상태였지만 다행히 그나마 작업 초반이어서 그냥 처음부터 다시 시작해도 멘탈에 큰 피해는 가지 않는 정도였습니다. 그 일로 git reset --hard 를 언제 쓰게 되는지 알게 되었습니다. 깃을 사용할 줄 알게 되었다는 것만으로도 이 프로젝트는 제 개발 인생에 커다란 시작점이 되었다고 생각합니다. 생각보다 깃은 친절하다가도 불친절한 녀석이라는 생각을 많이 하게 됐습니다.

4-3. 리액트

리액트가 뭔지 아무것도 모르는 상태에서 바로 기능구현에 들어갔기에 처음에는 정말 막막하고 아무런 코드도 칠 수 없었지만, 점차 기능을 붙여가면서 피바다 오류화면를 해결하는 것에 희열을 느꼈습니다. 또 비효율적으로 기능구현에 급급해서 짠 코드를 코드리뷰를 통해 리팩토링하면서 거북이처럼 조금씩 조금씩 나아가는 제 모습이 정말 뿌듯했습니다.

profile
빵굽는 프론트엔드 개발자

0개의 댓글