라우터(router) 객체
란 페이지 이동과 관련된 기능을 가지고 있는 객체
를 말한다.
이 객체를 사용해서 A 페이지에서 B 페이지로 이동할 때, "B 페이지로 라우팅한다"
고 말합니다.
/login 페이지는 누가 언제 접속해도 항상 로그인 페이지가 나온다.
이러한 페이지로 이동하는 것을 "정적 라우팅한다"
고 한다.
const router = useRouter()
const onClickMove = () => {
router.push('/section05/05-01-static-routing-moved')
}
return(
<div>
<button onClick={onClickMove} >페이지 이동하기</button>
</div>
)
useRouter 를 import 해주고 버튼을 하나 만들어 다른 폴더의 index.js 로 이동하는 함수를 만들어 사용한다.
백엔드에서 저장되어 있는 게시글을 불러와야 한다.
데이터를 조회하는 것은 graphql의 mutation 이 아닌 useQuery를 사용해야 한다.
const {data} = useQuery(FETCH_BOARD)
import { gql, useQuery } from "@apollo/client"
const FETCH_BOARD = gql`
query {
fetchBoard(number: 222) {
number
writer
title
contents
}
}
`
export default function StaticMovedPage() {
const {data} = useQuery(FETCH_BOARD) // 비동기적 방식 데이터는 나중에 들어온다.
console.log(data)
return(
<div>
여기는 222번 게시글입니다.
<div>writer : {data?.fetchBoard?.writer}</div>
<div>title : {data?.fetchBoard?.title}</div>
<div>contents : {data?.fetchBoard?.contents}</div>
</div>
)
}
왜 optional-chaining 을 사용해서 데이터를 보여줘야 할까?
자바스크립트는 기본적으로 동기적으로 작동하는 언어이다. 하지만 외부 db로부터 데이터를 받아와 변수에 받아오는 그 과정을 기다려주어야 하는데 자바스크립트는 이를 기다리지 않는다.
데이터를 받아오지 않은 undefined 상태로 data에 담겨지게 된다.
이후 자바스크립트가 계속 실행되면서 data 부분의 fetchBoard 부분은 실행되지 못하게 된다.
반면, 게시판 상세보기와 같은 경우, 글 번호에 따라서 주소가 변경된다.
하지만, 이러한 경우에는 게시글이 100개, 1000개가 넘어가게 되면 각각의 글 번호에 따라 페이지를 100개, 1000개씩 만들어 정적라우팅을 해주기는 어렵기 때문에 이러한 라우팅을 효과적으로 처리하기 위해서 동적 라우팅을 사용해야 한다.
/board/1 ⇒ 1번 게시글 상세보기 페이지
/board/2 ⇒ 2번 게시글 상세보기 페이지
/board/3 ⇒ 3번 게시글 상세보기 페이지
/board/4 ⇒ 4번 게시글 상세보기 페이지
... ...
이러한 페이지로 이동하는 것을 "동적 라우팅한다"
고 한다.
next.js에서는 동적 라우팅 기능을 제공한다.
위의 폴더 구조와 같이 보여주고자 하는 폴더 이름의 하위 폴더로 [boardId]
폴더를 만들어 준 후 이 안에 index.js 파일을 만들어주면 동적 라우팅을 사용할 수 있다.
대괄호로 감싸준 폴더를 만들어주면 이동해주고자 하는 페이지 번호, 혹은 게시글 번호가 대괄호 안에 쓰여진 변수명에 담겨져 그 변수 안에 있는 데이터를 꺼내 조회할 수 있습니다. (이 때, 대괄호 안에 쓰여지는 폴더 이름은 단순히 변수명이기 때문에 어떻게 작성해주셔도 상관없음 )
이러한 과정을 router 객체가 도와주는 것이다.
실제로 router.query = { boardId: 1 }
이런 형식으로 들어가게 되면서 자동으로 게시글 번호를 꺼내올 수 있다.
import { gql, useQuery } from "@apollo/client"
import { useRouter } from "next/router"
const FETCH_BOARD = gql`
query fetchBoard($number: Int) {
fetchBoard(number: $number) {
number
writer
title
contents
}
}
`
export default function StaticMovedPage() {
const router = useRouter()
console.log(router) // router를 확인해보자
const {data} = useQuery(FETCH_BOARD, {
variables: {
number: Number(router.query.number)
}
}) // 비동기적 방식 데이터는 나중에 들어온다.
console.log(data)
return(
<div>
여기는 {router.query.number}번 게시글입니다.
<div>writer : {data?.fetchBoard?.writer}</div>
<div>title : {data?.fetchBoard?.title}</div>
<div>contents : {data?.fetchBoard?.contents}</div>
</div>
)
}
정적라우팅과 동적라우팅의 많은 차이중 하나는 router 를 사용하는데 있다.
router 를 변수로 사용하여 주소창 마지막의 번호만 조절하는 방식으로 내가 원하는 곳으로 이동할 수 있다.
또한 fetchBoard 를 사용하면서 db에 보내고싶은 정보인 게시글 번호 number를 $number로 변수로서 받아내게 하여 사용한다는 것이 정적 라우팅과 다른 점일 것이다.
javascript는 작성된 코드가 상단에서부터 순서대로 실행되기 때문에 데이터를 요청하고 응답을 받아오는 동안 화면에 그려질 데이터의 내용이 undefined 이므로
첫 화면이 그려지는 시기에 데이터를 불러오면서 에러가 발생한다.
이 부분이 효율적으로 실행되기 위해서 화면을 미리 그려놓고 데이터를 그려주기 위해서 조건부렌더링을 사용해야 한다.
조건부 렌더링에는 &&연산자, 삼항연산자, 옵셔널체이닝이 있다.
제일 처음에는 삼항 연산자를 썼다.
data는 동기적으로 받아와야 한다. 하지만 데이터가 오기 전에 이미 return 부분에서 rendering을 해주고 있기 때문에 삼항 연산자를 써서 데이터가 있을 때, 없을 때를 모두 적어줘야 했다.
data ? data.fetchProfile : undefined
data가 존재하면 data.fetchProfile을 실행하고 그렇지 않다면 undefined
그 이후에는 && 연산자를 썼다.
data && data.fetchProfile
&&연산자는 데이터가 없을 경우 자동으로 undefined를 반환한다. 데이터가 없을 때 따로 div를 쓸 필요가 없으면 else 부분을 쓸 필요가 없죠. 하지만 이 코드조차 길다고 느껴진다.
&&연산자는 앞의 값이 참일 경우에만 뒤의 값을 보여주었는데, 반대로 앞의 값이 거짓일때 뒤의 값을 보여주는 경우도 있다, Nullish coalescing 연산자라고 한다.
??연산자는 앞의 값이 빈 값이면 뒤의 값을 보여주며 ||연산자는 앞의 값이 거짓(false)일 경우 뒤의 값을 보여줍니다.
data ?? data.fetchProfile
data || data.fetchProfile
optional-chaing이란 기존의 && 연산자를 쓰면서 길어졌던 코드를 더욱 간결하게 사용하는 연산자 이다.
optional-chaing은 ES2020에서 나온 최신 문법이다.
data?.fetchProfile
optional-Chaining은 ? 연산자 앞 객체의 참조가 undefined || null 이라면 undefined를 리턴한다.
결론적으로 위에 있는 삼항연산자, && 연산자와 똑같은 기능을 하면서 더욱 간결해졌다.
아래 3줄의 코드 중에서 어느 것이 더 간결하고 보기 좋은가?
mutation이 항상 성공하는 것은 아니다.
따라서, 우리는 성공에 대한 처리, 실패에 대한 처리를 나누어 작업해야 한다.
try {
await createBoard({
variables: {
aaa: "훈이",
bbb: "1234",
ccc: "안녕하세요 훈이에요",
ddd: "반갑습니다"
}
})
} catch(error) {
alert(error.message) // 경고창(실패했습니다.) ==> 백엔드 개발자가 보내주는 실패 메시지
} finally {
// 성공, 실패 여부와 상관없이 무조건 마지막에 실행되는 부분
// 필요없다면 생략 가능
}
웹 개발을 하면서 게시글을 작성하는 컴포넌트를 만들고 있다고 생각해보자.
유저가 게시글에 이름과 게시글 제목, 게시글의 내용을 작성하고 '게시'버튼을 클릭하면 현재 내가 작성한 게시글의 상세보기 화면으로 넘어가게 하고싶다.
이때 동적 라우팅을 사용하여 다음과 같이 리액트 코드를 작성할 수 있다.
import { gql, useMutation } from "@apollo/client"
import { useRouter } from "next/router"
const CREATE_BOARD = gql`
mutation createBoard($writer: String, $title: String, $contents: String) {
createBoard(writer: $writer, title: $title, contents: $contents) {
_id
number
message
}
}
`
export default function GraphqlMutationPage() {
const router = useRouter()
const [createBoard] = useMutation(CREATE_BOARD)
const onClickSubmit = async () => {
try{
const result = await createBoard({
variables: {
writer: "hans", // 일단 게시글의 입력내용은 하드코딩했다.
title: "Check the file",
contents: "for checking"
}
})
console.log(result)
router.push(`/상세보기 파일이 존재하는 폴더주소/${result.data.createBoard.number}`)
} catch(error) {
alert(error.message)
}
}
return (
<div>
<button onClick={onClickSubmit}>graph-ql 데이터 요청하기</button>
</div>
)
}
이후 라우팅되는 폴더의 파일
import { gql, useQuery } from "@apollo/client"
import { useRouter } from "next/router"
const FETCH_BOARD = gql`
query fetchBoard($number: Int) {
fetchBoard(number: $number) {
number
writer
title
contents
}
}
`
export default function GraphqlMovedPage() {
const router = useRouter()
console.log(router) // router를 확인해보자
const {data} = useQuery(FETCH_BOARD, {
variables: {
number: Number(router.query.number)
}
}) // 비동기적 방식 데이터는 나중에 들어온다.
console.log(data)
return(
<div>
여기는 {router.query.number}번 게시글입니다.
<div>writer : {data?.fetchBoard?.writer}</div>
<div>title : {data?.fetchBoard?.title}</div>
<div>contents : {data?.fetchBoard?.contents}</div>
</div>
)
}
이러한 방식으로 내가 게시글을 입력하면 동적 라우팅을 통하여 내가 작성한 게시글의 번호로 주소가 이동되고
작성한 게시글의 작성자, 제목, 내용이 화면에 나타나게 된다.