리액트 동적 라우팅

open_h·2020년 12월 20일
1

React

목록 보기
1/2
post-thumbnail

이 글은 RESTful API에 관한 글을 작성한 후, 리액트에서 동적 라우팅을 구현하는 방식을 정리한 글이다. RESTful API에 애한 이해를 바탕으로 리액트에서 동적 라우팅 구현을 실제 코드와 함께 알아본다. 그리고 리액트에서 pagination을 어떻게 구현하는지도 알아본다.

react-route를 사용하여 기본적인 페이지 라우팅을 할 수 있다는 전제하에 작성된 글입니다.
이 글에서 사용한 API 주소입니다.

리액트 동적 라우팅Dynamic Routing

리액트에서 동적 라우팅

리액트는 SPA로서 여러 화면으로 구성된 웹 어플리케이션을 만들 때 페이지별 라우팅 기능이 필요하다. 페이스북에서 공식적으로 제공되는 라우팅 라이브러리는 없으며, 보통 라우팅을 구현할 때 react-router라는 써드파티 라이브러리를 사용하여 라우팅 기능을 구현한다.

단순히 다른 페이지로 이동한은 것은 쉽게 라우트의 path를 설정하여 구현할 수 있다. 하지만 쇼핑몰의 제품 목록 중에서 하나를 클릭했을 때 해당 제품의 상세 페이지로 이동할 경우 어떻게 될까? RESTful API에서 살펴보았던 것처럼, parameter argument와 query string을 이용하면 된다.

아래 그림은 다음 항목 코드 살펴보기에서 살펴볼 코드가 구현하고자 하는 화면의 모습이다. Monsters 라는 페이지 컴포넌트 화면에서 MonstersDetil 이라는 페이지 컴포넌트로 넘어가는 것이다. 이 때, 특정한 카드를 클릭했을 때 그 카드에 해당하는 detail 페이지가 나타나게 동적 라우팅을 구현해야 한다.

코드 살펴보기

리액트에서 Routes.js 파일의 코드부터 바로 살펴보자!

// Routes.js
export default class Routes extends Component {
  render() {
    return (
      <Router>
        <Switch>
          <Route exact path="/monsters" component={MonsterList} />
          <Route exact path="/monsters/detail/" component={MonsterDetail} />
          <Route exact path="/monsters/detail/:id" component={MonsterDetail} />
        </Switch>
      </Router>
    );
  }
}

/monsters 에서는 몬스터 사진, 이름, 이메일 주소가 담긴 몬스터 카드의 목록이 나타나고 몬스터 카드를 클릭하면 detail 페이지로 넘어가는 기능을 구현하고자 한다. /monster/detail/ 로 이동할 경우 '어떤 디테일 페이지로' 이동할지에 대한 정보를 가질 수 없다. 그러나 /monsters/detail/:id 라는 path의 경우 클릭한 몬스터의 id에 해당하는 몬스터의 detail 정보를 보여주는 페이지로 이동하는 기능을 구현할 수 있다.

여기서 들어야 할** 의문점은 그럼 MonsterDetail 이라는 컴포넌트에게 path의 /:id 라는 정보를 어떻게 넘겨줄 수 있는가? 라는 의문이다.

먼저 MonsterList 컴포넌트에서 MonsterDetail 컴포넌트로 라우팅 되는 코드를 살펴보자. 아래 코드는 하나의 카드 컴포넌트에 대한 코드이다.

// Card.js
class Card extends Component {
  goToDetail = () => {
    // 라우팅 할 때 id에 대한 정보를 함께 넘겨준다.
    this.props.history.push(`/monsters/detail/${this.props.id}`);
  };
  render() {
    return (
      <div className="card-container" onClick={this.goToDetail}>
        <img alt="monster" src={`https://robohash.org/${this.props.id}`} />
        <h2>{this.props.name}</h2>
        <p>{this.props.email}</p>
      </div>
    );
  }
}

export default withRouter(Card);

위 코드에서 지금은 다른 부분은 신경쓰지 말고, 카드를 클릭했을 때 onClick , goToDetail 이라는 함수를 통해 다른 컴포넌트로 라우팅하는 코드임을 알아야 한다. 코드를 보면 Card 컴포넌트는 상위컴포넌트로부터 받은 idthis.props.id 로 접근한다. 그리고 그 id 값을 this.props.history.push() 에서 넘겨주고 있다. 그리고 img 태그의 주소도 그 id 값에 따라 바뀌고 있다는 것을 확인할 수 있다.

따라서 Routes.js 에서 본 path /monsters/detail/:idid 에 대한 정보를 받아 MonsterDetail 에게 넘겨줄 수 있는 것이다. 이렇게 넘겨주는 것은 리액트 라우터 라이브러리를 사용하기 때문이다. 코드를 다시 살펴보면 goToDetail 함수에서 this.props.history 와 같이 object에 접근하는 dot 문법을 사용하고 있다.

이에 대해서 가장 확실하게 궁금증을 해결시킬 방법은 역시 console.log 로 직접 확인하는 것이다. MonsterDetail 컴포넌트가 받는 props 는 무엇인지 아래 코드로 알아보자.

// MonsterDetail.js
render() {
  console.log("this.props는?", this.props);
  return (
    // code...
  );
}

위와 같이 MonsterDetail 컴포넌트 코드를 작성해서 this.props 가 무엇이 있는지 확인해보자. 사용자가 MonsterList 에서 id 가 5인 Card 를 클릭해 MonsterDetail 페이지로 넘어가는 경우에 console.log 의 결과값은 아래와 같다.

this.props 에 있는 history, location, match 이 세가지에 대해 알아보아야 할 느낌이 강하게 든다.

그러나 잠시 멈추고 props를 다시 짚어보기 위해 다른 코드를 하나 살펴보자. 아래 코드는 여러개의 Card 를 보여주기 위해 리액트에서 보통 사용하는 배열 매소드 map 을 사용한 코드이다. CardList 컴포넌트에서는 여러개의 Card 를 렌더한다.

// CardList.js
return (
  <div className="card-list">
    {monsters.map(monster => {
      return (
        <Card
          key={monster.id}
          id={monster.id}
          name={monster.name}
          email={monster.email}
          />
      );
    })}
  </div>
);

위 코드와 같이 보통 props에는 개발자가 하위 컴포넌트에게 전달하고 싶은 변수명을 사용해 데이터를 직접 만들어 직접 넘겨준다. 하지만 라우트로 설정한 컴포넌트는 3가지의 props history , location , match 를 추가적으로 전달받게 된다.

  • history : 이 객체에서 push , replace 를 사용해 다른 경로로 이동하거나 앞, 뒤 페이지로 전환 할 수 있다.
  • location : 이 객체는 현재 경로에 대한 정보를 담고 있으며 /about?abc=def 형식의 URL쿼리 정보도 가지고 있다.
  • match : 객체에는 어떤 라우트 매칭이 되었는지에 대한 정보가 담겨 있으며 /about/:myVar 과 같은 형식의 params 정보를 가지고 있다.

따라서 MonsterDetail 컴포넌트에서 아래와 같은 코드가 나타난다.

getData = () => {
  // 몬스터에 대한 정보: 이름, 이메일을 받아온다.
  fetch(
    `https://jsonplaceholder.typicode.com/users/${this.props.match.params.id}`
  )
    .then((res) => res.json())
    .then((res) => this.setState({ data: res }));
};
componentDidMount() {
  this.getData();
}

즉, this.props.history.push() 에서 받은 id 에 대한 정보를 위 코드와 같이 this.props.match.params.id 로 부터 전달받는 것이다. 아래 그림과 같이 정리를 할 수 있다. 만약 Card 컴포넌트에서 history.push 를 할 때 path에 id 값을 그냥 3으로 고정시킨다면, 즉 push.('monsters/detail/3') 와 같이 코드를 작성한다면 어떤 카드를 클릭하더라도 id가 3인 몬스터의 정보가 MonsterDetail 페이지에 보여지게 될 것이다. 하지만 백틱 문법을 사용해 주소와 path를 동적으로 표현할 수 있어 클릭한 카드의 상세 정보를 볼 수 있게 되는 것이다.

Pagination

많은 사이트에서 대량의 정보 한 페이지에 보여주기 힘드므로, 페이지별로 나누어 조금씩 보여준다. 한 페이지에 최대 몇 개의 아이템을 보여줄지 정하고, 사용자가 다음페이지 혹은 이전 페이지 뷰로 바꿀 수 있다. 혹은 페이지 숫자를 누르기도 한다. 이런 기능을 pagination이라고 한다.

리액트의 입장에서 보면 pagination은 하나의 컴포넌트에서 내부 데이터(state)만 달라지는 것이다. 하지만 리액트의 라이프사이클상 componentDidMount 는 최초 1회만 실행되고 컴포넌트의 state가 변하더라도 다시 실행되지 않는 함수이다.

따라서 pagination을 할 때는 componentDidUpdate 를 사용하야 한다. 아래 코드는 실제 pagination 보다 간소화된 경우이지만 pagination에서 componentDidUpdate 를 사용하는 이유를 이해하기에는 충분하다.

// MonsterDetail.js
getData = () => {
  fetch(
    `https://jsonplaceholder.typicode.com/users/${this.props.match.params.id}`
  )
    .then((res) => res.json())
    .then((res) => this.setState({ data: res }));
};
componentDidUpdate(prevProps, prevState) {
    this.getData();
}
goToPrevious = () => {
  this.props.history.push(
    `/monsters/detail/${+this.props.match.params.id - 1}`
  );
};
goToNext = () => {
  this.props.history.push(
    `/monsters/detail/${+this.props.match.params.id + 1}`
  );
};

match.params.id 로 넘어온 데이터는 string type이기에 number type으로 바꿔주기 위해 + 를 붙여주었다.

위 코드는 단순하기 다음 id 와 이전 id 에 대한 MonsterDetail 페이지로 이동하는 것이다.

물론 실제 pagination에서는 한 페이지에 여러개의 아이템을 보여준다. 하지만 여기서는 pagination을 구현하는 방법을 쉽게 이해하기 위해 위처럼 코드를 구현하였다. 만약 실제 pagination을 구현하려면 pagination이 구현된 API 주소를 사용해야 하며 아래와 같은 RESTful API 주소가 있을 수 있다. limit은 한 페이지에 보여줄 데이터 수, offset은 데이터가 시작하는 위치index일 경우에

GET https://호스트/products?limit=10&&offset=10

위와 같이 API를 사용할 수 있을 것이다. 다시 monster 예시로 돌아가자.

사실 위 몬스터 코드는 큰 문제가 발생한다. this.getData() 함수 내부에는 setState 가 불려지고 있다. 그리고 리액트 라이프사이클 상 setState 로 인해 render 가 다시 일어나고 다시 componentDidUpdate 가 실행된다. 따라서 this.getData() 는 무한으로 실행이 되는 것이다. setState 무한 반복에 빠지게 되버리는 것!

이런 문제를 해결하기 위해서는 componentDidUpdate 함수의 첫번째 인자와 두번째 인자를 사용할 필요가 있다. 리액트에서는 componentDidUpdate 함수의

  • 첫번째 인자로 prevProps 이전 props
  • 두번째 인자로 prevState 이전 state

로 만들어 두었다. 따라서 setState 의 무한 굴레에서 벗어나기 위해서는 아래와 같은 조건문이 필요하다.

componentDidUpdate(prevProps, prevState) {
  if (prevProps.match.params.id !== this.props.match.params.id) {
    this.getData();
  }
}

따라서 이전 상태와 다를 경우(즉, 페이지가 바뀔 경우)에만 getData() 를 실행하게 되어 무한의 굴레에 빠지지 않게 된다.

실제 pagination을 구현할 때도 이와 같이 무한의 굴레에 빠지지 않기 위해서는 이전 props에 대한 정보와 현재 props에 대한 정보를 비교하는 로직이 필요할 것이다.

profile
The only thing that interferes with my learning is my education.

2개의 댓글

comment-user-thumbnail
2020년 12월 20일

잘 읽었습니다 위에 그려주신 그림들 덕분에 이해가 잘 됐네요

1개의 답글