리액트로 위스타그램 리팩토링하기

ddoni·2021년 1월 8일
0

리액트로 인스타그램 클론하기

🙌 프리코스때 바닐라 자바스크립트로 구현한 위스타그램 코드를 가지고 리액트로 리팩토링하는 작업을 진행하였다. 직접적으로 돔에 접근하지 않고 페이지 렌더에 필요한 데이터를 상태값으로 관리해주면서 이벤트를 통해 상태 값을 업데이트 해주는 작업이 생소했지만 길게 쭉 작성했던 자바스크립트 코드가 간결해지는게 신기했다. 페이지를 컴포넌트 별로 잘 나누고 원하는 기능에는 어떤 상태값과 함수가 필요한지 잘 생각해봐야겠다!

로그인페이지

  1. 클래스 컴포넌트 생성
  • constructor 함수 내 super()를 호출하는 이유

부모 클래스의 생성자 함수를 불러준다. 부모 클래스에 있는 변수에 접근하기 위해선 필요하다.

super() 인자로 props를 전달하면 리액트는 컴포넌트 넘어 props에 this.props로 접근 가능하도록 만든다.

  • 초기 상태 값 설정 (initial state)

페이지가 렌더되는데에 필요한 데이터 정보(이 데이터정보는 사용자의 인터랙션에 의해 계속해서 변하는 업데이트 되는 데이터이다.)를 해당 정보들이 각각 어떠한 형태의 데이터인지 설정해준다. 최초의 페이지 렌더시 필요한 구체적인 값이 있다면 그 값을 넣어준다. (값을 넣어주지 않는 경우 undefined 를 리턴한다.)

class LoginMinsun extends React.Component {
  constructor() {
    super();
    //페이지가 렌더하기 위해 필요한데이터 
		//-> 로그인값, 비번값, 비번 보이는 버튼, 인풋한글자씩 이상이면 버튼활성화, 인풋validation
    this.state = {
      id: "",
      pw: "",
      validLoginInfo: false,
      validText: "",
      showPw: false,
    };
  }
	render() {
	}
}

2. 아이디, 비밀번호 인풋 이벤트

아이디와 비밀번호를 입력할때마다 상태값을 받는 이벤트가 필요하다. 각각의 인풋에 해당하는 이벤트를 줄 수 있지만 아래 코드와 같이 효율적으로 작성 할 수 있다.

changeInput = (evt) => {
    const { id, value } = evt.target;
    //when id's input clicked -> evt.target.id === 'id'
		//when pw's input clicked -> evt.target.id === 'pw'
//setState() 내에 evt.target.id: evt.target.value 를 넣어 줄 순 없기 때문에 변수에 할당하여
//넣어준다.
    this.setState({
      [id]: value,
    });
  };

//render() 내에 작성된 아이디 인풋 (id값 말고 name attr에 할당해주는 것이 좋다!)
						<input
              id="id"
              onChange={this.changeInput}
              className="loginInput"
              type="text"
              placeholder="전화번호, 사용자 이름 또는 이메일"
            />

✨ 아이디와 패스워드 인풋에 각각 this.state에 설정된 키 네임과 동일한 id 값을 주어서 이를 이용하여 상태 값을 업데이트해주는 setState의 키값으로 사용한다.

setState() 내에서는 우선적으로 최초에 상태값을 설정한 this.statekey property를 인식하므로 destructuring으로 할당된 변수에 접근하기 위해 []안에 변수명을 써준다.

3. 로그인 버튼 활성화 기능

로그인 버튼이 활성화 되는 조건은 아이디와 패스워드 인풋 값이 둘다 1자 이상인 경우이다.(인풋 값이 있을때) 그런데 이미 각각의 인풋 값을 받는 이벤트는 정의되어 상태 값이 업데이트 되고 있으므로 특정 이벤트가 발생되진 않는다

→ 이런 경우는 이미 존재하는 state 값을 이용하여 원하는 조건을 변수화 하여 이용한다.

toggleShowPw = () => {
    this.setState({
      showPw: !this.state.showPw,
    });
  };

✨ 값을 true로 설정하면 이벤트가 계속해서 발생되도 값은 그대로임

이벤트가 발생할때마다의 상태값을 받아오기위해 this.state.showPw로 주는 것이다.

앞에 !를 주어서 true일땐 false, false일땐 true 왔다갔다 되도록 설정해준다.

4. 로그인 유효성 검사

유효한 로그인 값인지 확인하는 조건문을 작성하여 유효성 검사를 통과한 인풋값들은 백엔드 서버로 전달되어 가입된 회원인지 확인하게 된다. 백엔드 API와 통신하기 위해서는 fetch 함수를 사용해준다.

validateLogin = (evt) => {
    evt.preventDefault();
    const checkId = this.state.id.includes("@");
    const checkPw = this.state.pw.length > 4;
    if (checkId && checkPw) {
      this.setState({
        validLoginInfo: true,
      });
      fetch(SIGNIN_API, {
        method: "POST",
        body: JSON.stringify({
          email: this.state.id,
          password: this.state.pw,
        }),
      })
        .then((res) => res.json())
        .then((result) => {
          //꼭 결과를 콘솔에 찍어봐서 토큰값이 어느 property로 전달되는지 확인하기!
          // console.log(result);
          //토근이 전달되는 property명을 확인 후 꺼내서 로컬스토리지에 저장하기
          localStorage.setItem("token", result.Authorization);
        });
      alert("로그인 성공! (❁´◡`❁)");
      this.props.history.push("/main-minsun");
    }
    if (!checkId) {
      this.setState({
        validText: "아이디는 @를 포함합니다.",
      });
    }
    if (!checkPw) {
      this.setState({
        validText: "비밀번호는 5자 이상 입니다.",
      });
    }

    //지금은 로그인 버튼 1개이므로 사인업을 먼저 한 후 사인인 요청을 보낸다(원래는 각 버튼에 따른 api요청을 보냄)
    //로그인을 하게 되면 토큰을 받게되는게 로그인유무를 기억하기 위해 토큰을 저장해둔다
  };

✨ 로컬스토리지에 저장된 토큰을 꺼내기 위해서는 localStorage.getItem("key name")으로 접근한다

메인페이지

  1. 메인페이지 컴포넌트

    코드의 유지보수를 위해 각 섹션별 컴포넌트로 분리해주고 분리된 컴포넌트 내에서도 필요에 따라 분리하는 것이 좋다!

    주로 부모 컴포넌트 내에서 상태 값이나 함수를 선언하여 자식에게 props로 전달해 주는 것이 좋고 해당 컴포넌트에서만 필요한 state는 해당 컴포넌트에서만 할당해줘도 된다

    //부모 컴포넌트에서 선언된 상태값이나 함수는 자식 컴포넌트의 attr로 props name을 통해
    //전달해준다.
    render() {
        const { commentInfo, inputVal } = this.state;
        return (
          <>
            <Nav />
            <main className="feedContainer">
              <MainLeft
                commentLikeHandler={this.commentLikeHandler}
                commentInfo={commentInfo}
                commentInputHandler={this.commentInputHandler}
                inputVal={inputVal}
                addCommentHandler={this.addCommentHandler}
              />
              <MainRight />
            </main>
          </>
        );
      }

2. data 불러오기

페이지의 렌더에 필요한 데이터는 페이지의 요소들이 렌더된 후에 불러와야 되므로 componentDidMount() 함수내에서 fetch 함수를 이용하여 데이터를 불러온다.

componentDidMount() {
//서버주소 중 포트번호를 달라질 수 있으므로 생략해주고
//METHOD가 GET 요청인 경우일때도 생략가능하다.
    fetch("data/commentdata.json")
      .then((res) => res.json())
      .then((res) => {
        this.setState({
          commentInfo: res.commentdata,
        });
      });
  }

3. 댓글 추가 기능

댓글 추가기능을 구현하기 위해 기존에 있는 댓글 데이터의 상태를 업데이트하기 위해 this.setState 내에 기존 데이터에 concat 메소드로 새로추가되는 댓글 데이터를 연결하였는데 spread operater로 연결하는 방법도 있다.

	addCommentHandler = (evt) => {
    evt.preventDefault();
    const { commentInfo } = this.state;
    this.setState({
      commentInfo: this.state.commentInfo.concat([
        {
          //여기서 받아오는 것은 위에서 선언한 변수명
          id: commentInfo.length + 1,
          userId: "usersssss",
          cmt: this.state.inputVal,
          liked: false,
        },
      ]),
      inputVal: "",
    });
  };

4. 댓글 좋아요 기능

처음 구글링을 통해서 좋아요 기능을 구현하긴 했지만 데이터 배열을 복사하고 해당하는 인덱스의 데이터를 수정한 후 상태를 업데이트하는 방식은 직접적으로 와닿지 않아서 멘토님께 문의드렸더니 동일한 방식이지만 좀 더 깔끔한 코드로 표현되어서 더 이해가 잘되었다!

(1) 구글링을 통해 구현한 방식

  1. 각 댓글이 가진 id 값을 함수의 인자로 넘겨준다 2. 댓글 데이터를 변수에 할당해준다 3. findIndex로 넘겨받은 id와 데이터 중의 id가 일치하는 댓글 데이터의 인덱스를 찾는다. 4. 2에서 할당한 변수에 3에서 찾은 인덱스로 접근하여(이벤트가 발생된 댓글 데이터) liked 부분을 수정해준다. 5. 배열은 mutable data 이므로 4에서 반영된 내용으로 2에서 할당된 배열 데이터는 수정된다. 이를 setState로 업데이트 해준다.

(2) map을 이용한 방식

  1. 댓글 데이터의 형식은 배열이기 때문에 map 메소드를 통해 각 데이터에 접근 가능하다. 2. map 메소드 내에 전달받은 id와 데이터의 id 중 일치하는 경우 liked 프로퍼티 값을 변경해주는 조건문을 작성한다 3. map 메소드로 부터 새롭게 생성된 배열을 변수에 할당하여 바로 상태값을 업데이트 해준다.
commentLikeHandler = (selectedId) => {
		//1. 선택된 댓글의 인덱스를 찾아 수정한 뒤 상태값 업데이트 하는 방법
    // let commentsData = [...this.state.commentInfo];
    // const idx = this.state.commentInfo.findIndex((el) => el.id === selectedId);
    // commentsData[idx] = {
    //   ...commentsData[idx],
    //   liked: !commentsData[idx].liked,
    // };
    // this.setState({
    //   commentInfo: commentsData,
    // });

    //2. map 메소드를 이용하여 데이터에 접근하는 방법
    //조건에 해당되는 데이터는 liked를 변경해주고 모든 데이터를 리턴해준 후에 원하는 배열이 완성된다
    const { commentInfo } = this.state;
    const newData = commentInfo.map((data) => {
      if (data.id === selectedId) {
        data.liked = !data.liked;
      }
      return data;
    });
    this.setState({
      commentInfo: newData,
    });
  };

5. 댓글 컴포넌트

댓글은 댓글 리스트 컴포넌트, 댓글 아이템 컴포넌트로 나눠서 부모-자식 관계로 연결해주었다. 댓글 데이터를 map 메소드를 사용해서 하드코딩을 하지 않고 동일한 구조를 가진 부분을 작성할 수 있는데 컴포넌트를 사용하는 가장 큰 이유는 재사용이기 때문에 데이터를 map 한 후 바로 렌더될 요소들을 작성해 주면 코드를 재사용하지 못하게 된다. 컨테이너 컴포넌트에서 먼저 map을 돌린 후 자식 컴포넌트에게 필요한 데이터 부분을 props로 전달해 주는 것이 좋다.

class Lists extends React.Component {
  render() {
    const { commentInfo, commentLikeHandler } = this.props;
    return (
      <ul className="commentsList">
        {commentInfo.map((el) => {
          return (
            <ListItems
              commentLikeHandler={commentLikeHandler}
              id={el.id}
              userId={el.userId}
              cmt={el.cmt}
              liked={el.liked}
            />
          );
        })}
      </ul>
    );
  }
}

6. 검색기능

검색기능은 검색 인풋에 값이 존재할때 인풋값과 데이터가 일치한 경우 데이터를 보여주고 일치하지 않는 경우는 '검색결과가 없다'라고 보여주기 위해 코드를 작성하였다.

const validSearchResult =
      searchData.filter((data) => data.userId.includes(inputVal)).length > 0;

<ul className="searchBoxList">
  {validSearchResult ? (
    searchData
      .filter((data) => data.userId.includes(inputVal))
      .map((data) => {
        return (
          <li className="searchBoxItem">
           <a href="#">
             <img src={data.imgUrl} />
             <div className="accountInfoContainer">
              <p className="searchId">{data.userId}</p>
              <span>{data.userName}</span>
             </div>
            </a>
           </li>
           );
          })
        ) : (
           <li className="searchBoxItem">
             <a href="#">
                <span className="noneOfResult">
                  검색 결과가 없습니다.
                 </span>
              </a>
            </li>
           )}
</ul>

0개의 댓글