본문 바로가기
Front-End/Project

첫 React Twittler (+ 개념 정리)

by 연제원 2021. 2. 12.

코드스테이츠 과정 중 몇 번이나 만들었던 Twittler를 이번에는 그 유명한 React로 만들어보는 시간이다!!!

공식 사이트에서 몇 번이나 봐도 직접 적용하기가 너무 어렵다... 그래서 과정마다 기록을 남겨 헷갈리는 개념들을 정리해보고자 한다.

(기능 구현에만 신경을 써서 CSS는 건드리지도 않았다..ㅎ)

 

React 준비


1. 기존 폴더에 react 설치

npx create-react-app .

 

2. 실행

npm start

정상 실행 확인!

 

 

Component 폴더 및 파일 생성


만들기 전, 중요한 개념이 있는데 바로 props state다.

 

props vs. state

공통점

  • 리액트에서 컴포넌트가 데이터를 다룰 때 사용하는 개념

차이점

  • props
    읽기 전용(값 변경❌) like 함수 인자
    부모 컴포넌트가 자식 컴포넌트에게 값을 전달할 때만 사용
    자식 컴포넌트는 props를 받아오기만 하고, props를 변경할 수 없다.
  • state
    컴포넌트 내부에서 선언하며 내부에서 값을 변경 할 수 있다.

사실 공식 문서에서 이 개념들을 봤을 때 무슨 말인지 몰랐지만, 직접 구현하다보니 조금은 이해가 가는 듯하다.

 

또한, src 폴더에 기본으로 존재하는 index.js, App.js가 있지만 일부분만 변경해주면 내가 작성한 컴포넌트로 작업할 수 있단 것을 배웠다.

 

Component(컴포넌트)

개념적으로 props를 input으로 하고 UI가 어떻게 보여야 하는지 정의하는 React Element를 output으로 하는 함수

주의
컴포넌트의 이름은 항상 대문자로 시작해야한다. React는 소문자로 시작하는 컴포넌트를 DOM 태그로 처리하기 때문이다. 예를 들어 <div />는 HTML div 태그를 나타내지만, <Welcome />은 컴포넌트를 나타내며 범위 안에 Welcome이 있어야 한다.

 

src 하위 폴더에 component 폴더를 생성하고 두 가지의 컴포넌트를 생성할 것이다.

(App.js는 사용하지 않을 것이다.)

  • Twittler Component : 전체 애플리케이션의 최상위 컨테이너, DOM에 직접 렌더링 하는 용도
  • SingleTweet Component : 트윗 하나를 표시, 다양한 props(작성자, 날짜, 트윗 내용)를 받고, 이를 표시하는 용도

이후, 수정해야 할 것이 있다.

기존 index.js에는 root 경로 안으로 렌더하고자 하는 컴포넌트를 통째로 render 해주는 함수가 위치한다.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// import App from './App'; 삭제
import Twittler from './component/Twittler'    <- 추가
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    // <App /> 삭제
    <Twittler />         <- 추가 
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

정리를 해보자면 기존에 존재하던 App.js 파일에서 App이라는 Component를 가져와서 사용하고 있었는데, 이 작업을 통해 새로 생성한 component 폴더에 존재하는 Twittler.js 파일에서 Twittler라는 Component를 가져와서 렌더링 하고자 하는 것이다.

(App.js를 수정해도 상관이 없다!)

 

 

🤔 코딩 시작!


컴포넌트 생성 방법에는 함수형클래스형 두 가지가 존재하는 데, state(변하는 값)을 사용하려면 클래스 형을 사용한다는 것만 우선 기억하자!

따라서 Twittler 컴포넌트도 클래스 형으로 만들어보자.

 

1. Twittler Component 생성

import React, {Component} from 'react';

class Twittler extends Component {
  constructor(props) { // 초기 this.state를 지정하는 class constructor를 추가
    super(props)
    this.state = {

    }
  }
  
  render() { // 클래스 형에서는 반드시 render() 메소드가 있어야 한다.
    return
    // 최종적으로 렌더링하고 싶은 JSX를 반환
  }
}

export default Twittler;

 

2. this.state에 변할 수 있는 정보 입력

this.state = {
  tweets: [
    {
      uuid: 1,
      writer: "똘이",
      date: "2021-02-12",
      content: "첫 리액트 사용!"
    },
    {
      uuid: 2,
      writer: "멩이",
      date: "2021-02-12",
      content: "어렵다..ㅠ"
    }
  ]
}

 

주의

기존에 자바스크립트를 사용할 때, 저 목록같아 보이는 tweets에 데이터들을 추가하려면 this.state.tweets.push({객체}) 이런식으로 값을 추가해줬는데, 리액트에선 그러면 안된다!

그 이유는, 리액트에서는 state 내부의 값을 직접적으로 수정하면 안된다고 한다. 이를 불변성 유지라고 한다.

push, splice, unshift, pop 같은 내장함수는 배열 자체를 직접 수정하므로 안된다.

 

그럼 어떻게 값을 추가할까? 

  1. 기존 배열에 기반해 새 배열을 만들어내는 함수인 concat, slice, map, filter 같은 함수를 사용
  2. setState를 이용

참고 : 벨로퍼트님 블로그

직접 구현해보면서 익숙해지자!

 

3. 기본 UI 렌더링

코드스테이츠에서 제공해 준 참고할 기본 인터페이스를 기입! 

render() { // 반드시 render() 메소드가 있어야 한다.
    return ( // 최종적으로 렌더링하고 싶은 JSX를 반환
      <div>
        <div>작성자: 연제원</div>
        <div id="writing-area">
          <textarea id="new-tweet-content"></textarea>
          <button id="submit-new-tweet">새 글 쓰기</button>
        </div>
        <ul id="tweets">
        <!-- 이 부분은 this.state.tweets를 바탕으로 SingleTweet 컴포넌트가 반복 출력될 것입니다. -->
        </ul>
      </div>
    )
  }

 

이 부분까지는 쉽게 진행했지만, 다음부터 state, props를 어떻게 가져다 쓸지, 이게 맞나?.. 하면서 개념들이 막 섞여 직접 코드를 짜기 너무 어려웠다ㅠㅠ

그래도 뭔가 성공했다!!! 👏👏👏

 

 

4. SingleTweet Component를 통한 this.state.tweets 렌더링  

ul 태그 하위에 this.state.tweets에 존재하는 데이터를 바탕으로 글 목록을 렌더링 할 것이다.

this.state.tweets는 배열이니깐 map 메서드를 이용해보자!

 

In Tweetler.js

<ul id="tweets">
  {this.state.tweets.map((tweet) => {
    return (
      <SingleTweet
        writer={tweet.writer}
        date={tweet.date}
        content={tweet.content}
      />);
  })}
</ul>

this.state.tweets의 각 요소(객체)의 writer, date, content 키-값을 SingleTweet에 적용해준다.

SingleTweet에선 이 값들을 props, 즉 인자로써 받아들일 것이다.

 

In SingletTweet.js (함수형)

import React from 'react';

function SingleTweet(props) {
  return (
    <li>
      <div>{props.writer}</div>
      <div>{props.date}</div>
      <div>{props.content}</div>
    </li>
  )
}

export default SingleTweet

(클래스형)

import React, { Component } from 'react';

class SingleTweet extends Component {
  render() {
    console.log(this)
    return (
      <li>
        <div>{this.props.writer}</div> // this가 없으면 undefined가 뜬다.
        <div>{this.props.date}</div>
        <div>{this.props.content}</div>
      </li>
    )
  }

}

export default SingleTweet

 

 

그 결과...

이렇게 Twittler의 this.state.tweets의 요소들이 렌더링 되어 나온다!!!

 

그런데

렌더링에는 문제가 없지만 크롬 콘솔창을 확인해보면 다음과 같은 key 오류가 발생한다.

리액트 공식문서를 보면, key는 어떤 아이템이 변화되거나, 추가, 삭제되었는지를 알아차리기 위해 필요하다고 말한다.

필요하다니깐 일단 넣어 보자!

참고로, SingleTweet안의 <li>에 키를 넣는 것이 아니라, Twittler 안의 <SingleTweet /> 안에 키를 넣어줘야 한다!

 

In Twittler.js

<SingleTweet
  key={tweet.uuid}
  writer={tweet.writer}
  date={tweet.date}
  content={tweet.content}
/>);

이렇게 해주면 더 이상 key 오류가 생기지 않는다. (key값을 지정해주는 것은 본인 마음대로)

 

 

5. 새 글 쓰기 버튼 클릭 시 새로운 Tweet 렌더링 (이벤트)

기존 HTML 에선 다음과 같이 이벤트를 사용했다면

<button onclick="activateLasers()">
  Activate Lasers
</button>

 

리액트에선 다음과 같이 사용해야한다.

<button onClick={activateLasers}>
  Activate Lasers
</button>

 

 

React에서 이벤트

  • React 이벤트는 소문자 대신 camelCase를 사용
  • JSX에 문자열 대신 함수를 전달

이를 생각하고 새 글 쓰기 버튼을 눌렀을 때, textarea에 작성된 내용이 렌더링되는 것을 구현해보자

 

사용 ❌

1. handleClick 함수 생성

 handleClick() {
  let newTweet = {
    uuid: this.state.tweets.length + 1,
    writer: '연제원',
    date: new Date().toLocaleString(),
    content: document.querySelector('#new-tweet-content').value
  };
  this.setState({tweets: this.state.tweets.concat(newTweet)});
 }

 

2. constructor 안에 함수 bind

 

이 과정이 없다면 render 메소드에서 this.onClickButton 함수를 호출했을 때 함수의 this가 window나 undefined가 되어버린다. 

this.handleClick = this.handleClick.bind(this);

 

❗ 이 과정이 신경쓰인다면 handleClick 함수를 화살표 함수로 구현하고 bind과정을 없앨 수 있다.

// constructor 안의 bind 삭제
// this.handleClick = this.handleClick.bind(this); << 이 것

// 화살표 함수로 구현
handleClick = () => {
  let newTweet = {
    uuid: this.state.tweets.length + 1,
    writer: '연제원',
    date: new Date().toLocaleString(),
    content: document.querySelector('#new-tweet-content').value
  };
  this.setState({tweets: this.state.tweets.concat(newTweet)});
}

 

3, button tag 안에 이벤트 함수 기입

<button id="submit-new-tweet" onClick={this.handleClick}>새 글 쓰기</button>

⬆⬆⬆⬆⬆⬆⬆⬆

찾아보니 기존 js에서 DOM을 조작하듯이 직접 다루면 안된다고 한다.

따라서 공식 문서를 참고하여 다시 만들어 보자!

 

 

1. textarea 값을 받아오는 handleChange 함수

앞서 언급했듯이, 화살표 함수를 사용하면 constructor 안에 bind를 해줄 필요가 없어진다.

또한 textarea에 입력하는 값을 잠시 저장하는 임시저장소를 constructor 안에 새로운 키-값쌍인 value: '' 를 기입했다.

constructor(props) {
  super(props)
  this.state = {
    tweets: [
      {
        uuid: 1,
        writer: "똘이",
        date: "2021-02-12",
        content: "첫 리액트 사용!"
      },
      {
        uuid: 2,
        writer: "멩이",
        date: "2021-02-12",
        content: "어렵다..ㅠ"
      }
    ],
    value: ''    <- 여기
  }
}

this.state.value 를 생성자 함수에서 초기화하기 때문에, 일부 텍스트를 가진채로 textarea를 시작할 수 있다.

handleChange = (e) => {
  console.log(e.target.value)
  this.setState({value: e.target.value})
}
render() {
  return (
    ...
    <textarea
      id="new-tweet-content"
      value={this.state.value}
      onChange={this.handleChange} <- onChange 속성임을 기억!
    />
  )
}

 

Twittler의 state.value에 값이 잘 저장된 것을 확인해볼 수 있다!

값이 잘 저장된다!

 

2. 버튼을 클릭하면 새로운 내용을 렌더링해주는 handleSubmit 함수

여기서 인자로 들어갈 value는 state.value값이 들어가도록 할 것이다.

또한 setState로 tweets에 새로운 tweet을 추가해 렌더링 하고, value값을 초기화한다.

handleClick = (value) => {
    // e.preventDefault();
    let newTweet = {
      uuid: this.state.tweets.length + 1,
      writer: '연제원',
      date: new Date().toLocaleString(),
      content: value
    };
    this.setState({tweets: this.state.tweets.concat(newTweet)});
    this.setState({value: ''});
  }

button태그안에 onClick속성에 handleClick과 그 인자로 value를 넣어준다

render() {
  return(
    <button
      id="submit-new-tweet"
      onClick={() => {this.handleClick(this.state.value)}}
    >새 글 쓰기</button>
  )
}

 

최종 코드 및 결과

Twittler.js

import React, {Component} from 'react';
import SingleTweet from './SingleTweet';

class Twittler extends Component {
  constructor(props) {
    super(props)
    this.state = {
      tweets: [
        {
          uuid: 1,
          writer: "똘이",
          date: "2021-02-12",
          content: "첫 리액트 사용!"
        },
        {
          uuid: 2,
          writer: "멩이",
          date: "2021-02-12",
          content: "어렵다..ㅠ"
        }
      ],
      value: ''
    }
  }
  
  handleChange = (e) => {
    this.setState({value: e.target.value})
  }

  handleClick = (value) => {
    let newTweet = {
      uuid: this.state.tweets.length + 1,
      writer: '연제원',
      date: new Date().toLocaleString(),
      content: value
    };
    this.setState({tweets: this.state.tweets.concat(newTweet)});
    this.setState({value: ''});
  }

  render() { // 반드시 render() 메소드가 있어야 한다.
    return ( // 최종적으로 렌더링하고 싶은 JSX를 반환
      <div>
        <div>작성자: 연제원</div>
        <div id="writing-area">
          <textarea
            id="new-tweet-content"
            value={this.state.value}
            onChange={this.handleChange}
          />
          <button
            id="submit-new-tweet"
            onClick={() => {this.handleClick(this.state.value)}}
          >새 글 쓰기</button>
        </div>
        <ul id="tweets">
          {this.state.tweets.map((tweet) => {
            return (
              <SingleTweet
                key={tweet.uuid}
                writer={tweet.writer}
                date={tweet.date}
                content={tweet.content}
              />);
          })}
        </ul>
      </div>
    )
  }
}

export default Twittler;

 

SingleTweet.js

import React, { Component } from 'react';

function SingleTweet(props) {
  return (
    <li>
      <div>{props.writer}</div>
      <div>{props.date}</div>
      <div>{props.content}</div>
    </li>
  )
}

 

결과

끝!


 

❗정리 및 기억


1. Component와 props

부모 컴포넌트(Twittler)에서 변하지 않는 데이터(tweets의 각 요소 = 객체의 값은 변하지 않는다)가 필요 할 땐, 자식 컴포넌트(SingleTweet) 안에
함수형은 {props.propsName}형식으로
클래스형은 {this.props.propsName}형식으로 넣는다.
부모 컴포넌트에서 자식 컴포넌트를 사용할 땐, < > 안에 propsName="값"을 넣어 값을 설정한다.  

부모

import { Component } from 'react';
import SingleTweet from './SingleTweet'

class Twittler extends Component {
  render (
    <div>
      <SingleTweet text="Hi there!"/>
    </div>
  );
}

자식 (클래스)

import { Component } from 'react';

class SingleTweet extends Component {
  render() {
    return (
      <div>{this.props.text}<div>
    );
  }
}

export default SingleTweet

자식 (함수)

import React from 'react';

const SingleTweet = (props) => {
  return (
    <div>{props.text}<div>
  );
}

export default SingleTweet;

 

2. state

클래스 컴포넌트에서 State를 사용하지 않아 State의 초기값 설정이 필요하지 않다면, 생성자 함수(constructor) 생략이 가능하다. 생성자 함수를 사용할 때는 반드시 super(props) 함수를 호출하여 부모 클래스의 생성자를 호출한다. 생성자 함수는 컴포넌트가 생성될 때 한 번만 호출된다.

 

3. 함수 사용 시 bind or 화살표 함수 사용

 

4. state를 변경 시 setState 사용

 

5. 이벤트 함수

  • 1. 이벤트 이름은 came|Case로 작성
    예를 들어 HTML의 onclick은 리액트에서는 onClick으로 작성해야 한다
  • 2. 이벤트에 실행할 함수 형태의 값을 전달
    자바스크립트 코드를 전달x 함수 형태의 값을 전달한다.
    화살표 함수 문법으로 전달하던가, 렌더링 부분 외부에 미리 만들어서 전달해야 한다
  • 3. DOM 요소에만 이벤트를 설정 가능
    div, button, input 등의 DOM요소에는 이벤트 설정을 할 수 있지만, 직접 만든 컴포넌트에는 이벤트를 자체적으로 설정할 수 없다.
    예를 들면, <Twittler onClick={handleClick}> 을 한다면 함수 실행이 아닌 Twittler의 onClick 속성을 props로 받는다.
  • 4. 이벤트 기본동작 방지
    e.preventDefault()

 

 

댓글