본문으로 바로가기

  • 라우팅?

    다른 주소에 다른 화면을 보여 주는 것을 라우팅이라고 한다

리액트 라우팅 라이브러리는 리액트 라우터, 리치 라우터, Next.js 등 여러가지가 있지만 리액트 라우터를 살펴보자

13-1-1. 프로젝트에 라우터 적용


BrowserRouter라는 컴포넌트를 이용하면되는데

이 컴포넌트는 HTML5의 History API를 사용해서 페이지의 새로고침을 하지 않고도 주소를 변경, 정보를 담아둘 수 있게 해준다

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

이런식으로 감싸주면된다

13-1-2. 페이지 만들기


라우트로 사용할 페이지를 만들어보자

// Home.js
const Home = ()=>{
    return (
        <div>
            <h2>Home</h2>
            <div>Welcome</div>
        </div>
    );
}
// About.js
const About = ()=>{
    return(
        <div>
            <h1>About.</h1>
            <div>about us</div>
        </div>
    );
}

13-2-1. Route 컴포넌트


이제 페이지를 만들었으니 Route를 이용해서 주소값에 따라서 컴포넌트를 연결해보자

아래와 같이 사용할 수 있다

<Route path="주소" component={컴포넌트}/>

아까 감싸준 BrowserRouter 컴포넌트 안에서 Route를 사용할 수 있다

const App = () => (
    <div>
        <Route path='/' component={Home}/>
        <Route path='/About' component={About}/>
    </div>
);

이렇게 선언하게되면 앞으로 주소창 '/' 에는 Home컴포넌트가 '/About' 에는 About컴포넌트가 렌더링되어서 나온다

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5d02c03a-fbe0-400e-93d6-8f6692ff3486/Untitled.png

하지만 About에 들어가게되면 어찌된 일인지 두개의 컴포넌트가 모두 나타나는 것을 볼 수 있는데

/About의 경로에 Home을 찾아가게되는 '/'가 겹쳐서 일어나는 일이다

이를 방지하기 위해선 Route의 exact라는 props값을 설정해주어야한다

const App = () => (
    <div>
        <Route path='/' component={Home} exact={true}/>
        <Route path='/About' component={About}/>
    </div>
);

여러개의 path 설정


 <Route path={['/About','/info']} component={About} />

이렇게 path를 넘겨줄 때 배열로 감싸고 여러 url을 넣어주면 배열안에 있는 주소들은 모두 About컴포넌트를 출력하게된다

13-2-2. Link 컴포넌트


html에서 페이지를 이동시킬 때는 a태그를 쓴다

하지만 리액트에서는 a태그를 그대로 사용해버리면 페이지를 새로 불러오기 때문에 렌더링되어있던 컴포넌트들이 다 사라지고 새로 렌더링된다

그럼 리액트에서는 어떤 방법으로 페이지를 전환해야할까?

물론 a태그를 사용하고 페이지 전환을 방지해주는 작업을 해주어도되지만 그런 기능을 담아놓은 Link 컴포넌트가 존재한다

이를 사용하여 페이지를 전환하면 페이지를 새로 불러오지않고 html의 history API를 이용하여 페이지의 주소만 변경해준다

<Link to="url"></Link>

이렇게 사용한다

const App = () => (
    <div>
        <ul>
            <li>
                <Link to={'/'}>Home</Link>
            </li>
            <li>
                <Link to={'/About'}>About</Link>
            </li>
        </ul>
        <Route path='/' component={Home} exact={true}/>
        <Route path={['/About','/info']} component={About} />
    </div>
);

Home과 About을 클릭하면 주소가 to안의 문자열로 바뀌고 루트가 바뀐 주소에 따라서 보여질 컴포넌트를 정한다

13-3. URL 파라미터와 쿼리


몇 번째 페이지가 들어간 정보가 담긴 주소나, 유저의 아이디가 들어간 주소에 해당 유저의 정보가 담겨있거나한 경우처럼 페이지 주소에는 유동적인 값도 들어가기 마련이다

  • 파라미터 : /profile/username
  • 쿼리 : /about?page=4

13-3-1. URL 파라미터


유저 아이디가 담겨있는 주소가 들어오면 해당 유저의 정보를 보여주는 컴포넌트를 만들어보자

const App = () => (
    <div>
        <ul>
            <li>
                <Link to={'/'}>Home</Link>
            </li>
            <li>
                <Link to={'/About'}>About</Link>
            </li>
            <li>
                <Link to={'/profile/james'}>James's profile</Link>
            </li>
            <li>
                <Link to={'/profile/john'}>John's profile</Link>
            </li>
        </ul>
        <Route path={'/'} component={Home} exact={true}/>
        <Route path={['/about','/info']} component={About} />
        <Route path={'/profile/:username'} component={Profile}/>
    </div>
);

우선 App컴포넌트를 수정하는데 Link컴포넌트로 각 유저 아이디의 프로필 링크로 이동하게끔 설정해준다

그리고 밑의 Route컴포넌트에서 :username으로 받은 것을 볼 수 있는데 이는 아래에서 살펴보자

// in Profile.js
const data = {
    james : {
        name : 'james',
        description : 'dkanakfdlsk'
    },
    john : {
        name : 'john',
        description: 'wjrrhdlTsmswnd'
    }
}
const Profile = (data)=>{
    console.log(data);
}

이제 Profile컴포넌트를 만들고 /profile/james로 이동해서 로그를 보자

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1b28b85-1351-474f-a6f9-75bee033d680/Untitled.png

라우트로 받아온 컴포넌트의 이러한 객체가 기본값으로 들어가는데

자세히 보면 match내부에 params 객체에 username: 'james'가 들어가있는 것을 볼 수 있다

우리가 위에서 설정해준 주소의 부분에 link에서 전달된 문자열이 매치되어서 저장되어있는 것

이를 이용해서 데이터에서 값을 가져와 동적으로 컴포넌트를 만들어줄 수 있다

const Profile = ({match})=>{
    const profile = data[match.params.username];
    return(
        <div>
            {(profile)?
                <div>Name : {profile.name}, Description : {profile.description}</div> :
                <div>cant find user</div>}
        </div>
    );
}

13-3-2. URL 쿼리


파라미터와 다르게 쿼리는 location 객체에 들어오는데 데이터가 어떻게 들어오는지 부터 확인하자

const About = (data)=>{
    console.log(data);
    return(
        <div>
            <h1>About.</h1>
            <div>about us</div>
        </div>
    );
}

이렇게 설정해준 뒤 /About?detail=true 로 들어가보면

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/169b0dc5-e5ac-462b-9453-18578705cfb9/Untitled.png

location.search 에 우리가 넣어준 url쿼리가 들어간 걸 확인할 수 있다

이 url쿼리를 쉽게 다루기 위해 qs라는 라이브러리를 이용한다

const query = qs.parse(location.search,{
        ignoreQueryPrefix: true,
    });

qs.parse에 location.search 즉, url쿼리를 넣으면 qs라이브러리가 자동으로 쿼리를 파싱해준다

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1a7dc4d7-3224-4a23-b1f0-c1c50cc52845/Untitled.png

parse의 두 번째 인자로 들어가는 객체에서 ignoreQueryPrefix옵션값을 true로 설정하면 윗결과값처럼 ?가 생략되어서 파싱된다

const About = ({location})=>{
    const query = qs.parse(location.search,{
        ignoreQueryPrefix: true,
    });
    const showDetail = query.detail==='true';
    return(
        <div>
            <h1>About.</h1>
            <div>about us</div>
            {showDetail && <div>did you see that</div>}
        </div>
    );
}

13-4. 서브 라우트


서브 라우트는 라우트 내부에 라우트를 또 정의하는 것을 의미한다

이때까지 만든 작업물은 App에서 너무 많은 일들이 이루어지고 있기 때문에 프로필의 url를 파싱해서 동적으로 유저 정보를 보여주는 작업은 따로 빼내보자

const App = () => (
    <div>
        <ul>
            <li>
                <Link to={'/'}>Home</Link>
            </li>
            <li>
                <Link to={'/profiles'}>Profiles</Link>
            </li>
            <li>
                <Link to={'/About'}>About</Link>
            </li>

        </ul>
        <Route path={'/'} component={Home} exact={true}/>
        <Route path={'/profiles'} component={Profiles}/>
        <Route path={['/about','/info']} component={About} />
    </div>
);

프로필즈라는 컴포넌트를 만들어 이전에 App컴포넌트에서 하던 작업들을 넘겨줄 것이다

const Profiles = ()=>{
    return(
        <div>
        <h3>user list</h3>
            <ul>
                <li>
                    <Link to={'/profiles/james'}>james</Link>
                </li>
                <li>
                    <Link to={'/profiles/john'}>john</Link>
                </li>
            </ul>
            <Route path={"/profiles"} exact render={()=><div>select user</div>}/>
            <Route path={'/profiles/:username'} component={Profile}/>
        </div>
    )
}

이렇게 말 그대로 라우트 안의 컴포넌트에서 라우트를 이용하는 것을 서브 라우팅이라고 한다

참고


  • 위에서 exact에는 true값을 주었는데 이번엔 아무값도 주지 않았다 저렇게 아무값도 주지않으면 props는 기본적으로 true를 갖는다
  • 라우트 컴포넌트 안에는 path에 따라 보여지는 화면을 컴포넌트로 넘겨줄 수도 render props를 이용해 직접 그릴 수도 있다

13-5. 리액트 라우터 부가 기능


13-5-1. history


라우트 컴포넌트에 기본적으로 넘어오는 데이터들 중 match와 location은 위에서 다루었고 history를 다룰 차례인데

이 history객체를 이용하면 컴포넌트 내에 구현하는 메서드에서 라우터 API를 호출할 수 있다

//in HistorySample
class HistorySampleClass extends Component{
    handleGoBack= ()=>{
        this.props.history.goBack();
    };

    handleGoHome = () => {
        this.props.history.push('/');
    };

    componentDidMount() {
        this.unblock = this.props.history.block('did you leave?');
    }
    componentWillUnmount() {
        if(this.unblock) {
            this.unblock();
        }
    }

    render() {
        return(
            <div>
                <button onClick={this.handleGoBack}>Back</button>
                <button onClick={this.handleGoHome}>Home</button>
            </div>
        );
    }
}

왜 함수형으로 잘 하다가 클래스형으로 컴포넌트를 만든건지, history객체의 go, push, block이 무엇인지 설명이 하나도 없다

이런 부분에서 책을 읽으면서 깊이가 좀 얇다는 생각이 자꾸자꾸 드는데 얇아도 책의 두께가 이정도고 많은 것을 다루고 있으니 어쩔 수 없이 읽는 사람이 좀 더 적극적으로 공부하게되는, 그렇게 만드는 게 저자의 목적일까..?

history메서드들은 이전에 한번 알아봤는데 block은 처음보는 거라서 인터넷에서 찾아봤다

block은 history 스택에 pop, push같은 동작을 제어하는 이벤트 객체의 preventDefault와 비슷한 느낌이다

    componentDidMount() {
        this.unblock = this.props.history.block('did you leave?');
    }
    componentWillUnmount() {
        if(this.unblock) {
            this.unblock();
        }
    }

컴포넌트가 마운트 될 때 unblock에 history.block을 담아두고 컴포넌트가 언마운트될 때 unblock에 담아둔 block이 있다면 실행, 그렇지 않다면 아무일도 일어나지 않게끔 만든 구문으로 볼 수 있다

이제 함수형으로 만들어보자

const HistorySampleFunction = ({history})=>{
    useEffect(() => {
        const unblock = history.block('did you leave?');
        return () => {
            unblock();
        };
    }, [history]);
    return(
        <div>
            <button onClick={()=>history.goBack()}>Back</button>
            <button onClick={()=>history.push('/')}>Home</button>
        </div>
    );
}

다른 부분들은 이전 예제에서도 많이 봐서 설명이 필요없고

볼 것은 라이프사이클메서드로 사용한 useEffect훅인데

useEffect(() => {
        const unblock = history.block('did you leave?');
        return () => {
            unblock();
        };
}, [history]);

history가 변경될 때(페이지를 이동하려고 할 때) unblock에 담아둔 block을 실행시키는 것

13-5-2. withRouter


withRouter 함수는 고차 컴포넌트로 라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체를 접근할 수 있게 해 준다

const WithRouterSample = ({location, match, history}) =>{
    return(
        <div>
            <h4>location</h4>
            <textarea value={JSON.stringify(location,null,2)} rows="7" readOnly/>
            <h4>match</h4>
            <textarea value={JSON.stringify(match,null,2)} rows="7" readOnly/>
            <h4>history</h4>
            <button onClick={()=>history.push('/')}>Home</button>
        </div>
    );
}

withRouter를 사용하면 현재 자신을 보여주고 있는 라우터 컴포넌트를 기준으로 데이터들이 전달된다

Profile에 WithRouterSample을 넣고 렌더링해 보자

const Profile = ({match})=>{
    const profile = data[match.params.username];
    return(
        <div>
            {(profile)?
                <div>Name : {profile.name}, Description : {profile.description}</div> :
                <div>cant find user</div>}
            <WithRouterSample/>
        </div>
    );
}

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bd781d8d-ddfd-4ffc-8234-7976fcb060e1/Untitled.png

profile의 상위 라우터는 profiles이기 때문에 profiles를 기준으로 데이터가 들어간 것을 볼 수 있다

13-5-3. Switch


Switch는 우리가 알고있는 조건문 Switch와 거의 동일하게 동작한다

Route들을 다 감싼뒤 일치하는 단 하나의 라우트만 렌더링 시켜주고 일치하는 것이 없다면 default 라우트를 설정하는 것도 가능하다

//in App.js
<Switch>
  <Route path={'/'} component={Home} />
  <Route path={'/profiles'} component={Profiles}/>
  <Route path={['/about','/info']} component={About} />
  <Route path={'/history'} component={HistorySampleClass}/>
  <Route path={'/history2'} component={HistorySampleFunction}/>
</Switch>

일치하는 하나만 렌더링하니까 '/'를 가르키는 라우트에 exact를 빼도 괜찮지 않을까?하는 마음에 빼봤는데

어느 링크를 클릭하든 Home컴포넌트만 렌더링되었다.. 제일 위에있는 Home라우트가 가장 먼저 일치하기 때문에 다른 라우트를 처다보지도 않는듯..

Home을 맨 밑으로 빼니까 exact를 빼도 정상적으로 작동했다 뭔가 제대로된 코딩이 아닌것 같아서 다시 exact를 부여

<Switch>
    <Route path={'/'} component={Home} exact={true}/>
    <Route path={'/profiles'} component={Profiles}/>
    <Route path={['/about','/info']} component={About} />
    <Route path={'/history'} component={HistorySampleClass}/>
    <Route path={'/history2'} component={HistorySampleFunction}/>
    <Route render={({location})=>(
        <div>
            <h2>404</h2>
            <p>{location.pathname}</p>
        </div>)}/>
</Switch>

이런식으로 path를 정의하지 않으면 default처럼 동작한다

13-5-4. NavLink


NavLink는 Link에서 추가기능이 더해진 것인데 현재 경로와 NavLink가 가르키는 경로가 일치한다면

CSS스타일을 적용시킬 수 있는 컴포넌트이다

const Profiles = ()=>{
    const activeStyle = {
        background: 'black',
        color: 'white'
    }
    return(
        <div>
        <h3>user list</h3>
            <ul>
                <li>
                    <NavLink activeStyle={activeStyle} to={'/profiles/james'}>james</NavLink>
                </li>
                <li>
                    <NavLink activeStyle={activeStyle} to={'/profiles/john'}>john</NavLink>
                </li>
            </ul>
            <Route path={"/profiles"} exact render={()=><div>select user</div>}/>
            <Route path={'/profiles/:username'} component={Profile}/>
        </div>
    );
}