인프런 커뮤니티 질문&답변

이경석님의 프로필 이미지

작성한 질문수

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

access Token의 저장과 next.js의 렌더링 원리

Expected server HTML to contain a matching <header> in <div>.

해결된 질문

23.02.14 17:28 작성

·

3.5K

0

 

회원가입이나 로그인 후 주소가
http://localhost:3000/login? 이나http://localhost:3000/signUp? 으로 이동해지며, url 뒤에 물음표가 붙습니다.

 

가끔 Failed to fetch 메시지가 뜨는것은 너무 많이 요청을 보내서 그런것같습니다만, 그것과 무관하게 아래의 이슈가 사라지지 않습니다.

 

01.png

지금 학습단계에서 임시로 사용하기로한 로컬스토리지때문에 발생한 이슈일까요?

 

import React, { useEffect } from 'react';
import { ApolloLink, ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import {
	createUploadLink
} from 'apollo-upload-client'
import { useRecoilState } from 'recoil';
import { accessTokenState } from '@/src/commons/store';

interface Props {
	children: JSX.Element;
}

const GLOBAL_STATE = new InMemoryCache()

const ApolloSetting = (props: Props) => {
	const [accessToken, setAccessToken] = useRecoilState(accessTokenState)

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



	const uplodLink = createUploadLink({
		uri: "http://backendonline.codebootcamp.co.kr/graphql",
		headers: { Authorization: `Bearer ${accessToken}` }
	})

	const client = new ApolloClient({
		link: ApolloLink.from([uplodLink as unknown as ApolloLink]),
		cache: GLOBAL_STATE
	});

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

export default ApolloSetting;

import { Title } from '@/src/commons/styles/emotion'
import * as S from './Login_styles'
import React, { useEffect, useState } from 'react'
import { useMutation } from '@apollo/client'
import { type IMutation, type IMutationLoginUserArgs } from '@/src/commons/types/generated/types'
import { LOGIN_USER } from './Login_query'
import { Modal } from 'antd'
import { useRouter } from 'next/router'
import { useRecoilState } from 'recoil'
import { accessTokenState } from '@/src/commons/store'

const Login_presenter = () => {
    const [accessToken, setAccessToken] = useRecoilState(accessTokenState)
    const router = useRouter()

    const [input, setInput] = useState({
        email: '',
        password: '',
    })

    const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
        setInput({ ...input, [e.currentTarget.id]: e.currentTarget.value })
    }

    const [loginUser] = useMutation<Pick<IMutation, 'loginUser'>, IMutationLoginUserArgs>(LOGIN_USER);

    const onSubmitSignUp = async () => {

        if (!input.email.includes("@") || !input.email.includes(".")) { Modal.error({ content: "이메일이 유효하지 않습니다." }); return }

        if (input.email && input.password) {
            try {
                const result = await loginUser({
                    variables: {
                        email: input.email,
                        password: input.password
                    }
                });

                if (!result.data) { Modal.error({ content: "로그인에 실패하였습니다." }); return }

                const accessToken = result.data?.loginUser.accessToken
                if (setAccessToken) {
                    setAccessToken(accessToken || "")

                    // 3. 로그인 성공페이지로 이동하기
                    void router.push('/')
                    localStorage.setItem("accessToken", accessToken) // 임시로 사용 나중에 지울예정
                }

            } catch (error) {
                if (error instanceof Error) { alert(error.message) }
            }
        }
    }

    return (
        <S.LoginForm>
            <Title style={{ color: '#fff', "textAlign": "center", "marginBottom": "20px" }}>로그인</Title>
            <S.InputContainer>
                <S.Label htmlFor='email'>이메일</S.Label>
                <S.Input id='email' placeholder='이메일을 입력해주세요.' onChange={onChangeInput} />
            </S.InputContainer>
            <S.InputContainer>
                <S.Label htmlFor='password'>비밀번호</S.Label>
                <S.Input id='password' placeholder='비밀번호를 입력해주세요.' onChange={onChangeInput} autoComplete="on" type='password' />
            </S.InputContainer>
            <S.SubmitButton onClick={onSubmitSignUp}>로그인</S.SubmitButton>
        </S.LoginForm>
    )
}

export default Login_presenter
import { accessTokenState } from '@/src/commons/store';
import { type IQuery } from '@/src/commons/types/generated/types';
import { gql, useQuery } from '@apollo/client';
import * as S from './Header_style'
import { useRouter } from 'next/router';
import { useRecoilState } from 'recoil';
import { useEffect, useState } from 'react';

const FETCH_USER_LOGGED_IN = gql`
	query fetchUserLoggedIn {
		fetchUserLoggedIn{_id picture email name}
	}
`;

const Header = () => {
	const [accessToken] = useRecoilState(accessTokenState)
	const [isMounted, setIsMounted] = useState(false)
	const router = useRouter();

	useEffect(() => {
		setIsMounted(true)
	}, [])


	const { data } = useQuery<Pick<IQuery, 'fetchUserLoggedIn'>>(FETCH_USER_LOGGED_IN);

	const onClickLogout = () => {
		if (localStorage) {
			localStorage.removeItem("accessToken")
			router.reload()
		}
	}

	return (
		<S.Header>
			<S.Logo onClick={async () => await router.push(`/boards`)}>
				🚢 FREE BOARD
			</S.Logo>
			{isMounted && <S.HeaderButtons>
				{!accessToken && <S.Login onClick={() => { void router.push(`/login`) }}>로그인</S.Login>}
				{!accessToken && <S.Signup onClick={async () => await router.push(`/signUp`)}>회원가입</S.Signup>}
				{accessToken && <S.Login onClick={onClickLogout}>로그아웃</S.Login>}
				{accessToken && <div>{data?.fetchUserLoggedIn.name}</div>}
				{accessToken && <div>{data?.fetchUserLoggedIn.email}</div>}
				{data?.fetchUserLoggedIn.picture && <img src={data?.fetchUserLoggedIn.picture}></img>}
			</S.HeaderButtons>}
		</S.Header>
	);
};

export default Header;

답변 1

0

안녕하세요 경석님!

우선 콘솔이슈는 하이드레이션 이슈입니다.
서버에서 프리렌더링해본 결과 브라우저 실제 렌더 결과가 달라 발생하는 에러입니다.
또한 해당 이슈는 17버전에선 크게 문제가 되진 않습니다.
다만, 거슬리신다면 브라우저의 캐시를 비우고 다시 시도해보세요!
시도해도 안되신다면 구글링을 통해 다른 방법을 알아보시면 도움이 될 것 같습니다.

url 문제는 코드상으론 로그인 후 이동페이지를 '/'로 걸어두셨는데, 질문은 로그인 후 로그인 페이지로 이동 된다라고 해주셨습니다.
해당 페이지로 이동이 안된다는 말씀이실까요...?

 

 

이경석님의 프로필 이미지
이경석
질문자

2023. 02. 16. 12:09

답변 감사합니다.

  1. 로그인 실패 이슈 : 해결하였습니다.

로그인을 성공하면 '/' 로 이동해야 하게 걸어두었기 때문에, 'http://localhost:3000'로 이동해야 합니다.

하지만 로그인 후 정상적으로 'http://localhost:3000' 이동 후, 새로고침 되면서http://localhost:3000/login?으로 이동하게 됩니다.

새로고침되는게 결과가 아니라 원인인 것같아서 로그인 버튼의 이벤트에 e.preventDefault()를 걸어 새로고침을 막아서 해결하였습니다.

※ useHookForm 적용 후에는 e.preventDefault()가 없어도 해결이 가능하였습니다. 감사합니다.

  1. 하이드레이션 이슈 : 해결하였습니다.

하이드레이션 이슈는 쿠키, 캐시를 비워도 계속 되었지만, 아래와 같이 useEffect를 사용하여 하이드레이션 이슈는 해결하였습니다.

const Layout = (props: Props) => {

	const router = useRouter()
	const isHiddenHeader = HIDDEN_HEADERS.includes(router.asPath)
	const isHiddenBanner = HIDDEN_BANNERS.includes(router.asPath)
	const isHiddenMenu = HIDDEN_MENUS.includes(router.asPath)

	const [mounted, setMounted] = useState(false)
	useEffect(() => {
		setMounted(true)

	}, [])

	return (
		<>
			{!isHiddenHeader && mounted && <Header />}
			{!isHiddenBanner && mounted && <Banner />}
			{!isHiddenMenu && mounted && <Menu />}
			<Main>{props.children}</Main>
		</>
	);
};