[React] props / state

i do as i say·2020년 5월 26일
0

리액트의 주요 개념 12개 중 4번과 5번에 대해서 정리해 봅니다

  1. Components and Props

Componentes?

  • 컴포넌트를 통해 UI를 재사용 가능한 개별적인 여러 조각으로 나눈다.
    라고 공식 문서에 적혀 있는데, 자바스크립트의 모듈화와 비슷해 보인다. 재사용성을 가능케 한다는 점에서 분명한 모듈화 특성이 보인다.

리액트의 컴포넌트는 자바스크립트의 함수와 유사하다.

props라는 임의의 입력(매개변수)를 받은 후 화면에 렌더한다.

함수 컴포넌트와 클래스 컴포넌트

//컴포넌트 이름은 항상 대문자로 시작
//소문자로 시작하는 컴포넌트는 DOM 태그로 처리하기 때문
function Welcome(props) {
  return <h1> Hello, {props.name} </h1>;
}
//데이터를 가진 하나의 props(프로퍼티) 객체 인자를 받음
//이러한 컴포넌트는 JS 함수라서 "함수 컴포넌트"라고 명명
//props 이외에 다른 것을 쓰면 안 됨 props는 문법임
//props에 대해서는 하단에 설명
class Welcome extends React.Component {
  render() {
    return <h1> hello, {this.props.name} </h1>;
  }
}
//함수 컴포넌트와 클래스 컴포넌트는 똑같음.

그러나 분명히 다른 점이 있다. Componentes 장에서는 함수 컴포넌트를 사용하고,
state 장에서는 class 컴포넌트를 사용할 것이다.
그리고 그 둘의 다른 점을 정리해 보자.

컴포넌트 렌더링

리액트 엘리먼트를 돔 태그로 나타낸 것 이외에, 사용자 정의 컴포넌트로도 나타낼 수 있음.

const element = <div />; //root div

const element = <Welcome name="sara" />;
//사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX 어트리뷰트와 자식을 단일 객체로
//컴포넌트에 전달하게 됨. 이 객체를 props라고 함.
//그래서 객체이기 때문에 props.name이 가능했던 것.

ReactDOM.render(
  element,
  document.getElenemtById('root')
  );
// hello, Sara
  1. 변수 element인 엘리먼트로 ReactDOM.render() 호출
  2. 리액트는 {name: 'Sara'}를 props로 하여 Welcome 컴포넌트 호출
  3. Welcome 컴포넌트는

    Hello, Sara

    반환
  4. ReachDOM이 렌더

컴포넌트 합성

자신의 출력에 다른 컴포넌트를 참조할 수 있음.
그러니까 컴포넌트들을 여러 번 렌더링만 해 주는 컴포넌트를 만들 수도 있다는 말.

function Welcome(props) {
  return <h1> Welcome, {props.name} </h1>;
}

function App() { //props를 사용하지 않을 거라면 쓰지 않아도 좋음
  return (
    <div>
    <Welcome name="sara" />
    <Welcome name="Cahal" />
    <Welcome name="Edite" />
    </div>
    );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

보통 리액트는 최상위에 단일 App 컴포넌트를 가지고 있음.

컴포넌트 추출

컴포넌트를 여러 개의 작은 컴포넌트로 나누게 되면 재사용성이 높아진다.
컴포넌트들끼리 합성이 가능하기 때문에 이런 아주 좋은 일들을 할 수 있게 됨.
많이 쪼개면 쪼갤수록 좋다. 나는 그렇게 생각.. 한다. 이유는? 어디에서 어떻게 쓰일지 모르니까?
무엇보다 깔끔해 보이고 정리가 되어 보임.

만약 이런 컴포넌트가 있다고 치자.

function Comment(props) {
  return (
    <div className="Comment"> //1. 코멘트 박스
      <div className="UserInfo"> //2. 유저 인포 박스
        <img className="Avatar" // 2-1. 유저 인포 박스 안에 이미지
          src={props.author.avatarUrl} //객체로 되어 있다
          alt={props.author.name}
        />
        <div className="UserInfo-name"> // 2-2. 유저 인포 박스 안에 이름
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text"> //3. 코멘트텍스트 박스
        {props.text}
      </div>
      <div className="Comment-date"> //4. 코멘트데이트 박스
        {formatDate(props.date)}
      </div>
    </div>
  );
}

객체, 문자열, 날짜를 props로 받는 컴포넌트이다.

props.author : props.author.avatarUrl props.author.name
props.text
props.date

변경의 어려움 및 재사용이 용이하지 않기 때문에 컴포넌트를 추출하여 합성한다.

//1. 유저 인포 박스 안에 있는 아바타
//이 컴포넌트는 자신이 Comment 컴포넌트 내에서 렌더링이 된다는 것을 알 필요가 없음.
//그렇기에 Author을 일반화된 user로 변경함. 이 이름은 컴포넌트 자체의 관점에서 짓는 것을 권장.
function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name} //이건 이름이 아닙니다! 이미지 설명입니다!
     />
   );
}

//2. 아바타와 이름을 가지고 있는 UserInfo 컴포넌트 추출
function UserInfo(props) {
  return (
    <div className="UserInfo"> //유저인포박스 안에
      <Avatar user={props.user} />///아바타 컴포넌트를 넣음
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}

//이렇게 추출하게 되면

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
   );
}
//이렇게 짧게 변경됨.

//props 변수 이름은 아무거나 지어도 상관없다. 대신 객체여야 한다.
const comment = {
  date: new Date(),
  text: '안녕하세요',
  author: {
    name: '예찬',
    avatarUrl: '../img'
  }
};
  
ReactDOM.render(
  <Comment
     date={comment.date}
     text={comment.text} //여기에 직접 값을 적어도 가능하다.
     author={comment.author}
  />,
  documetn.getElementById('root')
);

props는 읽기 전용이다.

함수 컴포넌트, 클래스 컴포넌트 모두 컴포넌트의 자체 props를 수정해서는 안 된다.
함수로 인해서 props의 값이 변하면 안 된다는 것이다.
모든 리액트 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 한다.

물론 UI는 동적이며 시간에 따라 변한다.
그래서 그것을 실행시켜 줄 State라는 개념을 지금 바로 소개한다.

state?

동적으로 변화를 위해 째깍거리는 시계 예시를 사용한다.
기존의 리액트 엘리먼트 렌더링은 render()를 사용해서 렌더링했고, 업데이트는 setInterval을 사용하여 매 초마다 함수를 호출하게 했다.

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
    );
}
setInterVal(tick, 1000);

이제 tick이라는 시침과 분침을 담을 Clock라는 컴포넌트를 재사용하고 캡슐화하는 것을 해 본다. 이 컴포넌트는 스스로 타이머를 설정하고, 스스로 업데이트할 것이다.

function Clock(props) { //시간을 담을 수 있는 시계 본체라고 생각
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}</h2>
    </div>
  );
}

//App과 같음 clock 렌더를 해 줌
//시간을 나타내지만 Clock가 없다면 단지 날짜와 시간을 나타내 줄 뿐
//시계는 되지 않는다
function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

Clock 컴포넌트를 생성하고, 캡슐화했다.

그러나 여기에서는 중요한 부분이 빠졌는데, 바로 Clock의 타이머 설정이다.
Clock이 타이머를 설정하고 매초 UI를 업데이트하는 것이 구현되어야 한다.

그러기 위해서는 state라는 것을 추가해야 한다.
state는 props와 유사하지만 비공개이고, 컴포넌트에 의해 제어된다.
이 state는 상태를 변경하게 해 줄 수 있는 수단이라고 볼 수 있다.

  • state는 함수 컴포넌트에서 사용할 수 없기에, 클래스 컴포넌트에서 사용한다.
//React.Component를 확장하는 동일한 이름의 ES6 class를 생성한다.
//extends를 사용하여 리액트 컴포넌트를 부모로 받는다.
class Clock extends React.Component {
  render() { //렌더라고 불리는 빈 메서드를 추가한다.
    return ( //함수의 내용을 렌더 메서드 안으로 옮긴다
      <div> //함수이기 때문에 리턴을 꼭 해 주어야 함
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}</h2>
      </div> 
    ); //컴포넌트를 물려받았기 때문에 props는 this.props가 된다.
  }
}
//render메서드는 업데이트가 발생할 때마다 호출된다.
//단, 같은 DOM 노드로 <Clock />을 렌더링을 하는 경우
//Clock 클래스의 단일 인스턴스만 사용된다.
//이는 로컬 state와 생명주기 메서드와 같은 부가적인 기능을 사용할 수 있게 한다.

아직 state를 추가하지 않았다. state가 들어갈 공간을 마련했을 뿐이다.

클래스에 로컬 state 추가하기

class Clock extends React.Component {
  //초기 this.state를 지정하는 class constructor를 추가한다.
  constructor(props) {
    //super를 사용함으로 리액트 컴포넌트의 props를 물려받을 수 있다
    super(props);
    //초기 this.state를 지정 date는 new Date임.
    this.state = {date: new Date()};
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}</h2>
//this.props.date를 this.state.date로 변경한다.
      </div>
    )
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);
//Clock 요소에서 date = {props:date}를 삭제한다

생명주기 메서드를 클래스에 추가하기

Clock이 처음 DOM에 렌더링될 때마다 타이머를 설정하려고 한다.
이것을 리액트에서는 마운팅이라고 한다.
또, Clock에 의해 생성된 DOM이 삭제될 때마다 타이머를 해제하려고 한다.

컴포넌트 클래스에서 아래와 같은 메서드를 선언하여 컴포넌트가 마운트/언마운트 될 때 일부 코드를 작동시킬 수 있다.

componentDidMount() {
  this.timerID = setInterval(
    () => this.tick(),
    1000
  );
} //컴포넌트 출력물이 DOM에 렌더링된 후 실행.
//props가 리액트에 의해 스스로 설정되고, state가 특수한 의미가 있지만
//타이머 아이디와 같이 데이터 흐름 안에 포함되지 않은 어떠한 항목을 보관할 필요가 있다면
//자유롭게 클래스에 수동으로 부가적인 필드를 추가해도 됨


componentWillUnmount() {
  //생명주기 메서드 안에 있는 타이머 삭제
  clearInterval(this.timerID);
}
//항상 class 안에 작성해 주어야 함

이러한 메서드들을 생명주기 메서드라고 한다.

메서드들을 전부 작성했으니, 컴포넌트가 매초 작동할 수 있는 tick 메서드를 이제, 드디어, 넣어 봅시다.

class Clock extends React.Component {
  constructor(props) {
    super(props);
    //props를 부모로부터 받았고
    //그게 state라는 특수성을 가진 놈도 props로 설정해 줄 수 있음
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({ //컴포넌트 로컬 state의 상태를 변경해 주기 위해
      //this.setState를 사용함. 안 하면 안 됨. 변경할 수 없음.
      date: new Date()
    });
  }
  //여기에서 볼 수 있듯이, Clock라는 class 안에 모든 게 담겨 있음.

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);
  1. 가 ReactDOM.render()로 전달하면 리액트는 Clock 컴포넌트의 constructor를 호출.

나중에.. 나중에 이어서 쓰겠습니다..

profile
커신이 고칼로리

0개의 댓글