24일차 - useApolloClient, 검증/폼 라이브러리

류연찬·2023년 5월 7일
0

Codecamp FE07

목록 보기
24/39

이전까지는 유저 정보를 가져올 때 useQuery 를 사용했습니다.

하지만 useQuery 는 유저 정보를 사용하는 페이지가 많다면, 모든 페이지에서 useQuery 를 해와야 한다는 번거로움이 있습니다.

useApolloClient 를 이용해 조금 더 효율적으로 가져와 보도록 하겠습니다.

useApolloClient

💡 여러가지 쿼리 방식

  • useQuery : 컴포넌트가 열리면 바로 실행되며, data 라는 변수에 fetch 해온 데이터를 담아줍니다.
  • useLazyQuery : useQuery 를 원하는 시점에 실행 후 fetch 해 온 데이터를 data 변수에 담아줍니다.
  • useApolloClient : 원하는 시점에 실행 후 fetch 해 온 데이터를 원하는 변수에 담을 수 있습니다. axios같은 느낌으로 사용이 가능합니다.

여러가지 쿼리 방식을 알아보았으니 이를 코드에 직접 적용해보도록 하겠습니다.

지난시간에 사용했던 login-check 폴더와 login-check-success폴더를 복사해 만들어주세요.

useApolloClient 사용해보기

우리의 목표는 로그인이 완료된 후 유저의 정보를 success페이지에서 fetch 하는 것이 아닌 토큰과 함께 global state에 저장하는것이 목표입니다.

우리는 토큰을 App.tsx 안의 context에 저장을 하고 있기때문에 같이 넣어주고 싶은 유저의 정보또한 토큰과 함께 context안에 userInfo라는 이름으로 저장해주도록 하겠습니다.

// src/components/commons/apollo/index.tsx

// Apollo Setting 빼주기
import { useRecoilState } from "recoil";
import { accessTokenState, userInfoState } from "../../../commons/store";

const ApolloSetting = (props) => {
	const [accessToken, setAccessToken] = useRecoilState(accessTokenState)
	const [userInfo, setUserInfo] = useRecoilState(userInfoState)

	useEffect(() => {
		if(localStorage.getItem("accessToken")) {
			setAccessToken(localStorage.getItem("accessToken") || "")
		}
	}, [])

	const uploadLink = createUploadLink({
			uri: "백엔드 주소",
			headers: { Authorization: "Bearer 받아온 토큰" }
    })

	return (
		<ApolloProvider client={client}>
			{props.children}
		</ApolloProvider>
	)
}

export default ApolloSetting

recoil에 넣어줄 userInfo를 만들어준 후 userInfo를 import 해서 추가해줍니다.

이렇게 recoil에 토큰과 함께 유저의 정보가 담기게 되었습니다.

이제 login api를 요청할 때 setUserInfo 를 이용해 실제 유저의 정보를 state안에 넣어주시면 됩니다.

하지만 useQuery는 요청한 mutation의 데이터를 fetch해오고 해당 데이터를 {data} 에 담아 렌더링하고 있기 때문에 로그인을 요청하는 mutation 안에서 같이 사용할 수 없습니다.

따라서 useApolloClient 를 사용해 내가 원하는 위치에서 사용 후 변수에 결과를 담아 사용해야 합니다.

// login-use-apollo-client 폴더
import { useMutation, gql } from "@apollo/client"
import { ChangeEvent } from "react"
import { useRouter } from "next/router"

const LOGIN_USER = gql`
  mutation loginUser($email: String) {
    loginUser(email: $email, password: $password) {
    	accessToken
    }
  }
`
const FETCH_USER_LOGGED_IN = gql`
  query fetchUserLoggedIn {
    fetchUserLoggedIn {
      email
      name
    }
  }
`

const LoginPage = () => {
	const [accessToken, setaccessToken] = useRecoilState(accessTokenState)
	const [userInfo, setUserInfo] = useRecoilState(userInfoState)
	const router = useRouter()
	const client = useApolloClient()

	const [email, setEmail] = useState("")
	const [password, setPassword] = useState("")
	const [loginUser] = useMutation<Pick<IMutation, 'loginUser'>, IMutationLoginUserArgus>(LOGIN_USER)
	

	const onChangeEmail = (event: ChangeEvent<HTMLInputElement>) => {
		setEmail(event.target.value)
    }
    
	const onChangePassword = (event:ChangeEvent<HTMLInputElement>) => {
    	setPassword(event.target.value)
    }

	const onClickLogin = async() => {
		try {
			//로그인 하기
			const result = await loginUser({
				variables: {
					email: email,
					password: password
				}
			})
            
			const accessToken = result.data?.loginUser.accessToken

			//유저 정보 가지고 오기
			const resultUserInfo = await client.query({
            query: FETCH_USER_LOGGED_IN,
            	context: {
                    headers: { Authorization: `Bearer ${accessToken}`}
    			}
    		})

			//글로벌스테이트에 저장하기
			if(setAccessToken) setAccessToken(accessToken || "")
			if(setUserInfo) {
                setUserInfo(resultUserInfo.data.fetchUserLoggedIn || {} )
                router.push('/loginsuccess')

                //localstorage 배우기 전까지 저장해두기
                localStorage.setItem(accessToken:"accessToken")
                localStorage.setItem("userInfo",JSON.stringify(userInfo))
			}
		} catch(error) {
			// alert(error.message)을 사용하셔도 무방합니다.
			Modal.error({content: error.message})
		}
	} 

	return (
		<div>
			이메일 : <input type="text" onchange={onChangeEmail}/> <br/>
			비밀번호 : <input type="password" onchange={onChangePassword}/> 
			<button onClick={onClickLogin}>로그인하기!!</button>
		</div>
	)
}

export default LoginPage

useApolloClient를 사용해 queryFETCH_USER_LOGGED_IN를 보내주실 때 해당 유저가 누군지 식별하기 위해 토큰을 함께 보내주셔야 합니다.
useApolloClient 를 사용하실 때 토큰을 함께 보내주시는 방법은 context를 이용해 헤더를 함께 보내주시면 됩니다.

이렇게 해서 받아온 유저의 정보를 변수에 담아 setUserInfo에 넣어주시면 됩니다.

이렇게 생성한 유저 정보를 setUserInfo 에 넣어주시면 됩니다.

💡 tip!

💡 퍼사드 패턴
각 역할을 담당하는 함수를 분리해 컴포넌트에서 호출만 하는 패턴입니다.
이렇게 각자의 역할이 있는 함수를 컴포넌트로 분리 후 import해서 호출만 해줍니다.

이렇게 유저 정보를 토큰과 함께 global state에 저장하고 나면, 이제 loginSuccess 폴더에서 useQuery를 사용하지 않고 global state에서 가지고 올 수 있습니다.

// login-use-apollo-client-success 폴더
import {withAuth} from "경로"

const LoginSuccessPage = () => {
	const [userInfo, setUserInfo] = useRecoilState(userInfoState)
	return(
		<div>
			{userInfo.name}님 환영합니다.
		</div>
	)
}

export default withAuth(LoginSuccessPage)

폼 라이브러리(react-hook-form)

우리는 이전까지 모든 state를 직접 만들고, onChange 함수도 일일히 만들어 바인딩해주는 등 노가다성 코딩을 해왔습니다.

이부분을 폼 라이브러리를 사용하시면 더이상 노가다성 코딩을 하지 않아도 되며 굉장히 간편합니다.

폼 라이브러리는 검증을 대신해주는 폼, state를 대신해주는 폼 등 종류도 굉장히 다양합니다.

폼 라이브러리에는 react-form, redux-form, react-hook-form, formik 등이 있습니다.

react-hook-form 사용하기

함수형 컴포넌트와 hook을 사용하는 경우, 가장 사용하기 쉽고 성능적으로 좋은 폼은 react-hook-form입니다.

react-hook-form 시작히기

react-hook-form의 장점

우리가 이전에 사용하던 onchange를 만들어 setState를 해주고 바인딩 하는 방법은 state가 변화할때마다 렌더링이 되기때문에 불필요한 렌더링이 지속적으로 일어나 굉장히 비효율 적 이었습니다.

또한 변화한 state를 받아서 다시 넣어주고 렌더링하기 때문에 굉장히 느립니다.

하지만 react-hook-form은 imput의 값을 실시간으로 state에 반영하는것이 아닌 등록함수가 실행 될 때 한번에 처리하기 때문에 불필요한 렌더링이 제거되고 한번에 바꿔 렌더링하기 때문에 빠르고 효율적입니다.

이렇게 처리하는 방식을 비제어 컴포넌트라고 하는데, 리액트 훅 폼은 비제어 컴포넌트입니다.

💡 비제어 컴포넌트 vs 제어 컴포넌트

  • 비제어 컴포넌트 : 바닐라 스크립트처럼 submit 함수를 실행할 때 refinput 값을 끌고 옴
  • 제어 컴포넌트 : 사용자의 입력을 기반으로 state를 실시간으로 관리(setState 사용)


    한치의 오차도 용납할 수 없는 중요한 데이터를 저장하고 있다면 제어 컴포넌트를 이용하는게 좋습니다.
    하지만 그런게 아니라면 비제어 컴포넌트를 이용해서 성능을 높여주는게 더 좋습니다.

react-hook-form 설치

npm 사용자 : npm install react-hook-form
yarn 사용자 : yarn add react-hook-form

react-hook-form 사용

const ReactHookForm = () => {
	// react-hook-form 에서 useForm을 제공합니다.
	const {register , handleSubmit} = useForm()

	// 등록하기 함수 -> handleSubmit이 조종해주는 함수 입니다.
	const onClickSubmit = (data) => {
		console.log(data)
	}

	return (
		<form onSubmit={handleSubmit(onClickSubmit)}>
			<input type="text" {...register("state이름")}/>
			<button type="reset"> 등록하기 </button>
		</form>
	)
}

export default ReactHookForm

💡 register , handleSubmit , form

  • register : state를 등록하는데 필요한 모든 기능이 들어있습니다
  • handleSubmit : register에 적힌 state를 등록해주는 함수입니다
  • form : 실제 html에 있는 input들을 묶어주는 태그입니다

form 태그는 input에 적힌 내용을 전송해주는 기능이 있습니다.

react-hook-form 에서는 이 기능을 이용합니다.

button 태그의 type에 reset을 주게 되면 클릭시에 폼 안에 있는 인풋값을 초기화 합니다.

submit을 주시게 되면 form태그에 바인딩된 submit 함수를 실행시키게 됩니다.

버튼 타입의 기본은 submit입니다. 따라서 폼안에서 사용하게 되면, 따로 명시 하지 않아도 submit의 기능을 하게 됩니다.

만일 form 태그 내에서 form과 상관없는 버튼을 만들어야 한다 하시면, type을 button으로 주셔야 합니다.

💡 주의하세요!!
form 태그 내의 button의 타입이 submit일 경우, 버튼에 다른 함수를 바인딩할 시 예상치 못한 오류가 발생할 수 있습니다.
form 태그의 submit 함수와 바인딩 된 함수를 모두 처리하기 때문입니다.


검증 라이브러리(yup)

지금까지는 검증을 할 때 우리가 직접 검증단계를 만들어 진행해왔습니다.

하지만 실제 검증의 과정은 조금 더 복잡하고 까다롭게 진행됩니다.

가령 숫자인지, 문자인지, 최소 8자리인지, 특수문자가 들어가는 지 등의 단계 들이 있습니다.

yup은 이렇게 까다로운 검증과정을 대신해주는 라이브러리 입니다.

yup 사용하기

yup 공식문서

yup과 같은 검증 라이브러리는 보통 form과 함께 사용합니다.

따라서 우리는 리액트 훅 폼과 함께 사용할 것 이며, 함께 사용하실땐 리액트 훅 폼의 Schema Validation을 잘 봐주시고 함께 사용해주시면 됩니다.

yup 설치

npm 사용자 : npm install -s yup
yarn 사용자 : yarn add yup

react-hook-form과 함께 사용하기 위한 세팅

npm 사용자 : npm install @hookform/resolvers yup
yarn 사용자 : yarn add @hookform/resolvers yup

yup관련 코드를 작성하고, resolver에 연결을 해주면 폼과 yup을 함께 사용하기위한 준비는 완료된 상태입니다.

resolver에는 yup이외에도 다른 검증 라이브러리를 사용할 수 있습니다. ‼️

yup 사용하기

import * as yup from 'yup'
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import styled from '@emotion/styled'

const myButton = styled.button`
	background-color: ${(props) => props.isValid ? "yellow" : ""}
`

// yup 에러메세지 생성해주기
const schema = yup.object().shape({
	myWriter: yup.string()
  		.email('이메일 형식이 적합하지 않습니다.')
		.required('필수 입력값입니다.'),
	myPassword: yup.string()
		.min(4, '비밀번호는 최소 4자리 이상입니다.')
		.max(15, '비밀번호는 최대15자리 입니다.')
		.required('필수 입력값 입니다.') 
})

const ReactHookForm = () => {
	// formState에서 에러메세지들을 받아오게 됩니다.
	const { register , handleSubmit, formState } = useForm({
		// schema는 위에서 만들어 둔 schema입니다.
		resolver : yupResolver(schema)
	})

	// 등록하기 함수 -> handleSubmit이 조종해주는 함수 입니다.
	const onClickSubmit = (data) => {
		console.log(data)
	}

	return (
		<form onSubmit={handleSubmit(onClickSubmit)}>
			이메일 : <input type="text" {...register("myEmail")}/>
				<div> {formState.error.myEmail?.message}</div>
			비밀번호 : <input type="text" {...register("myPassword")}/>
				<div> {formState.error.myPassword?.message}</div>
			<MyButton type="reset" isValid={isValid}> 등록하기 </button>
		</form>
	)
}

export default ReactHookForm

에러가 있을수도 있고 없을수도 있기때문에 옵셔널 체이닝을 사용해서 조건부 렌더링을 걸어주셔야 합니다.

최종적으로 에러가 있는지 있는지 없는지 확인후 버튼을 활성화 하는건 formState의 isValid를 사용해주시면 됩니다.

정규식(정규표현식)

우리는 기존에 회원가입이나 로그인시에 무언갈 포함하고 있는지 확인할 때 includes를 사용했었습니다.

정규표현식을 배우게 되면 조금 더 편하게 확인 할 수 있습니다.

정규표현식 사용하기

정규표현식은 아래의 예시와 같이 사용합니다.

/ 조건 /.test("검사하고 싶은 것")

정규 표현식에서 test 라는 메서드를 사용할 수 있는데,

이 메서드의 인자값으로 체크할 비밀번호 문자열을 넣어주게 되면

조건에 해당되는 경우 (비밀번호 체크 사항에 하나라도 불일치하는 경우) 에는 false 값을

조건에 해당되지 않으면 true 값을 리턴하게 됩니다.

여러가지 의미의 정규표현식 문자

문자 의미문자
문자열\w
숫자\d
없거나 한개?
하나 이상일 때+
특정개수 지정{}
또는,
~ 사이-
모든.
원래 가지고 있는 의미 탈피\
// 정규표현식 예제
/\w+@\w+.\w+/.test("yeontan@ccc.com")

//결과
true

위의 코드에서 정규표현식의 조건은 \w+@\w+.\w+ 이 되겠습니다

💡 tip!
정규표현식에서의 조건이 검사하고 싶은 문자열에 하나라도 포함되어 있다면, true를 반환합니다. 따라서 시작점(^)과 끝점($)을 정해주시면 됩니다.

📖 MDN 공식문서를 참고해보세요!
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Regular_Expressions

0개의 댓글