[Project 02] Westagram by.React

송나은·2021년 3월 6일
1

Project

목록 보기
2/6

인스타그램 Clone 프로젝트 "Westagram"

  • 프로젝트 기간: 2021.03.02~2021.03.12
  • 기술스택: HTML, CSS, JavaScript(ES6+), React, SCSS

구현사항

  1. 인스타그램 로그인, 메인 페이지 레이아웃
  2. 로그인 버튼 클릭 시 메인 페이지로 이동
  3. 로그인 시 사용자 입력 데이터 저장
  4. 로그인 ID/PW Validation
  5. 로그인 인증 (API 통신)
  6. 메인페이지 내 피드에서 댓글 입력 후 엔터 or 게시 클릭 시 댓글 추가
  7. 메인페이지 내 피드 & 댓글 & 추천영역 데이터 렌더링

따라치는 코딩이 아닌, 개념 공부 후 이해한 내용을 바탕으로 기능을 구현했습니다.

Login page

구현사항 1 로그인 버튼 클릭 시 메인 페이지로 이동

로그인 버튼에 onClick event 발생 시 goToMain 함수 실행.
1. Form태그의 Submit 기능을 제어하기 위해 e.preventDefault()를 먼저 실행.
2. 입력한 ID와 Password를 body에 담아 string으로 변환 후 API에 인증 요청.
3. 요청에 대한 응답을 받아 JSON형식으로 변환.
4. 인증이 되면 메인으로 이동하고 서버로부터 전달받은 token을 localStorage에 저장.
5. 인증에 실패하면 "아이디와 비밀번호를 확인해주세요" 팝업 생성.

class Loginsong extends React.Component {
  goToMain = () => {
    e.preventDefault();
    fetch("http://10.58.3.143:8000/user/signin",{
      method:"POST",
      body: JSON.stringify({
        email: this.state.userId,
        password: this.state.userPassword
      })
    })
    .then(res => res.json())
    .then(result => result.message === "SUCCESS" ? 
    this.props.history.push('/main-naeunsong') && result.token && localStorage.setItem('wtw-token',result.token)
    : alert("아이디와 비밀번호를 확인해주세요")
  }
  
  render(){
    return (
        <button onClick={this.goToMain}>로그인</button>
}

구현사항 2 사용자 입력 데이터 저장

ID/PW Input 창에 onChange event 발생 시 handleInput 함수 실행.
1. event.target.value(Input창에 입력된 ID와 PW)를 state에 저장.

class Loginsong extends React.Component {
  constructor(){
    super();
    this.state ={
      userId: '',
      userPassword: ''
    }
  }
  
  handleInput = (e) => {
    this.setState({
      [e.target.name]: e.target.value // 계산된 속성명
    });
  }
  
 render(){
    const { userId, userPassword } = this.state; // 비구조화 할당
    const { handleInput, goToMain} = this;
    return (
        <input onChange={handleInput} value={userId} name="userId" type="text" placeholder="전화번호, 사용자 이름 또는 이메일" />
        <input onChange={handleInput} value={userPassword} name="userPassword" type="password" placeholder="비밀번호" />        
}

구현사항 3 Validation

ID에 @가 포함되고, PW가 5자 이상일 때 버튼 활성화.
1. ID/PW Input에 따라 변화하는 state값을 이용하여 유효성 검사 실행.
2. 조건에 부합할 경우 className 변경.
3. .activeBtn과 .inactiveBtn의 css 효과를 다르게 주어 버튼 활성화 기능 구현.
ex) .activeBtn{opacity: 1.0}; .inactiveBtn{opacity: 0,5}

<button onClick={goToMain}
	className={userId.includes('@') && (userPassword.length >= 5) ? 'activeBtn' : 'inactiveBtn'}>로그인</button>

Main Page

- Comment

구현사항 1 댓글 입력 후 엔터 or 게시 클릭 시 댓글 추가

  1. Textarea에 onChange event 발생 시 handleCommentInput 함수 실행
    : event.target.value(=comment)를 state에 저장.
  2. 게시 버튼 onClick event 발생 혹은, textarea에 onKeyPress event 발생 시 addComment 함수 실행
    : event.target.comment(Textarea에 입력된 Comment)를 state에 저장.
    -> commentList에 comment추가
    -> comment state를 초기화.
  3. map 함수를 이용하여 commentList에 저장된 데이터를 원하는 형태로 렌더링 (JSX활용)
class Comment extends React.Component {
  constructor(){
    super();
    this.state = {
      comment: '',
      commentList:[]
    }
  }

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

  addComment = () => {
    this.setState({
      commentList: this.state.commentList.concat({userName: "songbetter", , content: this.state.comment}),
      comment: '' // input창에 글씨 남아있지 않도록!
    });
  }

  pressEnter = (e) => {
    if(e.key === 'Enter'){
      this.addComment()
    }
  }

  render(){ 
    const { commentList, comment, userName } = this.state;
    const { pressEnter, addComment, handleCommentInput } = this;
    return (
      <section className="commentArea">
        <ul className="commentList">
      	// map 함수 이용하여 데이터 렌더링
        {commentList.map((listData) =>
    	// 하위 컴포넌트로 인자 넘겨주기
        <CommentList id={listData.id} name={listData.userName} content={listData.content} isLiked={listData.isLiked} newName={userName}/>)} 
        </ul>
        <span>1시간 전</span>
        <div className="comment">
          <img alt="emoji icon" src="https://s3.ap-northeast-2.amazonaws.com/cdn.wecode.co.kr/bearu/explore.png" height="24px" />
          <textarea onKeyPress={pressEnter} onChange={handleCommentInput} value={comment} placeholder="댓글 달기..." className="textArea" />
          <button onClick={addComment} className="commentBtn">게시</button>
        </div>
      </section>
    )
  }
};

export default Comment;

// 하위 컴포넌트: CommentList
class CommentList extends React.Component {
  render(){
    const {id, name, content, isLiked} = this.props
    return (
      <li key={id}>
        <strong>{name}</strong> {content} 
        <span className="heart">{isLiked?'🤍':'💔'}</span> <span className="delete"></span>
      </li>
    )
  }
};

- Feed

구현사항 2 중복되는 UI 단순화 (데이터 렌더링)

Mock data를 만들어 데이터 통신을 하지 않아도 기능이 구현되는 것을 확인할 수 있음.
-> map 함수를 이용하여 저장된 데이터를 원하는 형태로 렌더링 (JSX활용)

// Feed Mock data
[
  {
    "id": 1,
    "userId": "wecode",
    "userName": "WeCode - 위코드",
    "profileImg": "https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-19/s150x150/64219646_866712363683753_7365878438877462528_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&_nc_ohc=G1pc0slT5dYAX-0PnDa&tp=1&oh=a7a51c4739ea8b87566b329ecdc33b43&oe=605C5770",
    "content": "20기, 21기, 22기, 23기 모두 조기마감!!",
    "feedImg": "https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-15/sh0.08/e35/s640x640/150915321_179560660629368_165943275422580666_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&_nc_cat=108&_nc_ohc=uCmsp3r2d7MAX930Q5z&tp=1&oh=40938acaff43a67bb9a8187dccb1abc0&oe=605FCAA5",
    "alt": "위코드 부트캠프 실내 이미지 안에 SOLDOUT이라고 쓰여있다."
  },
  {
    "id": 2,
    "userId": "songtommy",
    "userName": "송토미",
    "profileImg": "https://ca.slack-edge.com/TH0U6FBTN-U01HXJAHJ02-03d0864e58db-72",
    "content": "내가 세상 최고 귀여운 고양이다!!",
    "feedImg": "https://github.com/songbetter/self-introduction/blob/main/tommy1.jpg?raw=true",
    "alt": "귀여운 고양이 사진"
  },
  {
    "id": 3,
    "userId": "jordy",
    "userName": "니니즈 - 죠르디",
    "profileImg": "https://search.pstatic.net/sunny/?src=https%3A%2F%2Fimg.theqoo.net%2Fimg%2FPvwpd.jpg&type=a340",
    "content": "죠르디는 죠르귀",
    "feedImg": "https://search.pstatic.net/sunny/?src=https%3A%2F%2Fimg.theqoo.net%2Fimg%2FPvwpd.jpg&type=a340",
    "alt": "아..여기도 참 많이 변했네"
  }
]

class Feeds extends React.Component {
  constructor(){
    super();
    this.state ={
      feed: []
    }
  }

  componentDidMount(){
  fetch("/data/feedData.json")
    .then (res => res.json())
    .then (res => this.setState ({
      feed: res
    }))
  }

  render(){
    return (
      <div className="feeds">
        {this.state.feed.map((feedData) =>
        <Feed id={feedData.id} userId={feedData.userId} userName={feedData.userName} content={feedData.content} profileImg={feedData.profileImg} feedImg={feedData.feedImg} ale={feedData.alt}/>)}
      </div>
    )
  }
};

export default Feeds;

// 하위 컴포넌트: Feed
class Feed extends React.Component {
  render(){
    const {id, profileImg, feedImg, userName, userId, alt} = this.props
    return(
    <article className="feed" key={id}>
      <section className="articleProfile">
        <img alt={`${userName} 프로필 이미지`} src={profileImg} />
        <div className="info">
          <strong>{userId}</strong>
          <span>{userName}</span>
        </div>
      </section>
      <section className="feedImg">
        <img alt={alt} src={feedImg}/>
      </section>
      <section className="likeShare">
        <img alt="heart icon" src="https://s3.ap-northeast-2.amazonaws.com/cdn.wecode.co.kr/bearu/heart.png" width="24px" />
        <img alt="heart icon" src="https://s3.ap-northeast-2.amazonaws.com/cdn.wecode.co.kr/bearu/heart.png" width="24px" />  
        <img alt="heart icon" src="https://s3.ap-northeast-2.amazonaws.com/cdn.wecode.co.kr/bearu/heart.png" width="24px" /> 
        <img alt="heart icon" src="https://s3.ap-northeast-2.amazonaws.com/cdn.wecode.co.kr/bearu/heart.png" width="24px" />
        <h4>좋아요 100</h4>
      </section>
      <section className="feedInfo">
        <div className="feedInfo1">
          <strong>{userId}</strong> <span>{this.props.content}</span>
        </div>
      </section>
      <Comment/>
    </article>
    )
  }
};

export default Feed;

- Recommend


class Recommend extends React.Component {
  constructor(){
    super();
    this.state = {
      recommendList:[]
    }
  }

  componentDidMount(){
    fetch('/data/recommendData.json')
    .then (res => res.json())
    .then (res => this.setState ({
      recommendList: res
    }))
  }

  render(){
    console.log(this.state.recommendList)
    return (
      <section className="recommend">
        <div className="recommendText">
          <span>회원님을 위한 추천</span>
        </div>
        {this.state.recommendList.map(list=>
        <RecommendInfo id={list.id} userId={list.userId} img_url={list.img_url} userName={list.userName}/>
        )}
      </section>
    )
  }
};

// 하위 컴포넌트: Recommend
export default Recommend;
   
class RecommendInfo extends React.Component {
  render(){
    const { id, userId, img_url, userName} = this.props
    return (
    <div className="recommendInfo" key={id}>
      <img alt={`${userId} 프로필 이미지`} src={img_url} />
      <div className="info">
        <strong>{userId}</strong>
        <span>{userName}</span>
      </div>
      <div className="follow"><a>팔로우</a></div>
    </div>
    )
  }
};

export default RecommendInfo;

Review

한줄을 작성하기 위해 3일을 고민하고, 내가 친 코드가 제대로 실행이 되지 않아 괴로워했던 지난 10일, 절대적인 시간투자를 삽질하는 데 보냈다.
개념을 이해하기까지 시간이 오래 걸려 다양한 기능을 구현하지 못해 아쉽지만,
state와 map()으로 반복되는 UI를 다루는 방법을 알았으니 이제 개발 속도가 붙지 않을까 기대해본다.

멘토 준식님이 맨날 하시는 말씀 흥분하지 말자

(+) 코드리뷰

바닐라JS와 다른점

바닐라JS
DOM을 활용할 때에는 HTML에서 요소를 가지고 와서 또다른 요소를 만들고, 텍스트를 요소 안에 추가하고, 추가한 요소를 상위 요소의 자식으로 또 추가하고 HTML에서 요소의 위치를 확인하고 하는 과정이 반복된다. -> 코드를 작성하는 시간도 작성한 코드도 한없이 길어진다.

React
컴포넌트 단위로 쪼개서 관리하기 때문에 구조를 파악하기가 좋고, 컴포넌트에서 컴포넌트로 인자를 넘겨주고 받는 것도 간단하다. HTML과 JS를 한번에 볼 수 있어 유지보수 하기에도 좋다.
HTML이 하나밖에 없어서 페이지를 이동하는 것도 컴포넌트의 경로를 활용할 수 있다.

-> Westagram by.바닐라JS 바로가기

앞으로 구현해 볼 기능

  • 로그인에서 인증받은 사용자 권한 유지 기능
  • 댓글 삭제기능
  • 좋아요 버튼 클릭 시, 하트 색깔이 변하고 좋아요 수가 증가하는 기능
profile
그때그때 공부한 내용과 생각을 기록하는 블로그입니다.

0개의 댓글