본문 바로가기

NodeJS

풀스택 틱톡 클론 따라하기 - 3. Resolver, ProtectedRoutes

반응형

https://youtu.be/Xy15S0nQxBQ

1:12:00 ~ 1:28:07

백엔드 목차.

  1. 모듈 프로바이더 설정
  2. 리졸버 뮤테이션 및 쿼리 작성

프론트엔드 목차.

  1. ProtectedRoutes

모듈 프로바이더 설정

[Nest] 9400 - 2023. 11. 06. 오후 8:45:17 ERROR [ExceptionHandler] Nest can't resolve dependencies of the AuthService (?, PrismaService, ConfigService). Please make sure that the argument JwtService at index [0] is available in the AuthModule context.

Potential solutions:

  • Is AuthModule a valid NestJS module?
  • If JwtService is a provider, is it part of the current AuthModule?
  • If JwtService is exported from a separate @Module, is that module imported within AuthModule?
    @Module({
    imports: [ /* the Module containing JwtService */ ]
    })

Error: Nest can't resolve dependencies of the AuthService (?, PrismaService, ConfigService). Please make sure that the argument JwtService at index [0] is available in the AuthModule context.

Potential solutions:

  • Is AuthModule a valid NestJS module?
  • If JwtService is a provider, is it part of the current AuthModule?
  • If JwtService is exported from a separate @Module, is that module imported within AuthModule?
    @Module({
    imports: [ /* the Module containing JwtService */ ]
    })

백엔드 서버를 실행하려면 발생하는 에러 메세지이다.

 

// src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from 'src/prisma.service';

@Module({
  providers: [AuthService, JwtService, ConfigService, PrismaService],
})
export class AuthModule {}

 

auth.service.ts에서 JwtService, ConfigService, PrismaServic를 사용하는데, auth.module.ts에서 provider로 설정되지않아 발생하는 오류이다.

그리고 아직 graphql에 쿼리가 하나도 작성되어 있지 않아 오류가 발생한다.

GraphQLError: Query root type must be provided.

리졸버 뮤테이션 및 쿼리 작성

Mutation Response 작성.

// src/auth/types.ts

import { ObjectType, Field } from '@nestjs/graphql';
import { User } from '../user/user.model';

@ObjectType()
export class ErrorType {
  @Field()
  message: string;

  @Field({ nullable: true })
  code?: string;
}

@ObjectType()
export class RegisterResponse {
  @Field(() => User, { nullable: true }) // Assuming User is another ObjectType you have
  user?: User;

  @Field(() => ErrorType, { nullable: true })
  error?: ErrorType;
}

@ObjectType()
export class LoginResponse {
  @Field(() => User)
  user: User;

  @Field(() => ErrorType, { nullable: true })
  error?: ErrorType;
}

 

// src/user/user.resolvers.ts

import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
import { AuthService } from 'src/auth/auth.service';
import { UserService } from './user.service';
import { LoginResponse, RegisterResponse } from 'src/auth/types';
import { LoginDto, RegisterDto } from 'src/auth/dto';
import { Response, Request } from 'express';
import { BadRequestException } from '@nestjs/common';

@Resolver()
export class UserResolver {
  constructor(
    private readonly authService: AuthService,
    private readonly userService: UserService,
  ) {}

  // 사용자 등록을 처리하는 뮤테이션
  @Mutation(() => RegisterResponse)
  async register(
    @Args('registerInput') registerDto: RegisterDto, // GraphQL 뮤테이션에서 전달된 입력 데이터
    @Context() context: { res: Response }, // Express 응답 객체
  ): Promise<RegisterResponse> {
    // 비밀번호와 확인 비밀번호를 비교하여 일치하지 않으면 예외 발생
    if (registerDto.password !== registerDto.confirmPassword) {
      throw new BadRequestException({
        confirmPassword: 'Password and confirm password are not the same.',
      });
    }

    try {
      // AuthService를 사용하여 사용자 등록을 시도하고 결과를 반환
      const { user } = await this.authService.register(
        registerDto,
        context.res, // Express 응답 객체를 전달하여 쿠키를 설정
      );

      console.log('user', user);

      return { user };
    } catch (error) {
      // 에러가 발생한 경우 에러 메시지를 반환
      return {
        error: {
          message: error.message,
        },
      };
    }
  }

  // 사용자 로그인을 처리하는 뮤테이션
  @Mutation(() => LoginResponse)
  async login(
    @Args('loginInput') loginDto: LoginDto, // GraphQL 뮤테이션에서 전달된 입력 데이터
    @Context() context: { res: Response }, // Express 응답 객체
  ): Promise<LoginResponse> {
    // AuthService를 사용하여 사용자 로그인을 시도하고 결과를 반환
    return this.authService.login(loginDto, context.res); // Express 응답 객체를 전달하여 인증 쿠키를 설정
  }

  // 사용자 로그아웃을 처리하는 뮤테이션
  @Mutation(() => String)
  async logout(@Context() context: { res: Response }) {
    // AuthService를 사용하여 사용자 로그아웃을 처리
    this.authService.logout(context.res); // Express 응답 객체를 전달하여 인증 쿠키를 제거
  }

  // 액세스 토큰을 갱신하는 뮤테이션
  @Mutation(() => String)
  async refreshToken(@Context() context: { req: Request; res: Response }) {
    try {
      // AuthService를 사용하여 액세스 토큰을 갱신하려고 시도
      return this.authService.refreshToken(context.req, context.res);
    } catch (error) {
      // 에러가 발생한 경우 BadRequestException 예외를 던짐
      throw new BadRequestException(error.message);
    }
  }

  @Query(() => String)
  async hello() {
    return 'hello';
  }
}

 

@Args 어노테이션은 Nestjs에서 graphql 리졸버 함수에서 입력 매개변수를 가져오는데 사용한다.

@Args('registerInput')에서 'registerInput'은 GraphQL 스키마에서 정의한 입력 인수의 이름과 일치해야 한다.

@Context 어노테이션은 graphql 요청과 관련된 컨텍스트를 가져올때 사용된다.

app.module.ts의 graphql 모듈 설정과 연결되는 부분이다.

 

// src/user/user.module.ts

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';
import { AuthService } from 'src/auth/auth.service';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from 'src/prisma.service';
import { ConfigService } from '@nestjs/config';

@Module({
  providers: [
    UserService,
    UserResolver,
    AuthService,
    JwtService,
    ConfigService,
    PrismaService,
  ],
})
export class UserModule {}
// src/types.d.ts

declare namespace Express {
  export interface Request {
    user?: {
      username: string;
      sub: number;
    };
  }
}

 

이제 서버 실행이 된다.

백엔드와 프론트엔드 모두 서버를 실행시키고 프론트엔드 터미널에서 코드젠 명령어 실행.

 

npx graphql-codegen --watch

 

ProtectedRoutes

// src/components/ProtectedRoutes.tsx

import { ReactNode, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useUserStore } from "../stores/userStore";
import { useGeneralStore } from "../stores/generalStore";

// ProtectedRoutes 컴포넌트 정의
const ProtectedRoutes = ({ children }: { children: ReactNode }) => {
  // 전역적으로 관리되는 사용자 상태 및 전역 상태 훅 사용
  const user = useUserStore((state) => state);
  const navigate = useNavigate();
  const setLoginIsOpen = useGeneralStore((state) => state.setLoginIsOpen);

  // useEffect를 사용하여 컴포넌트가 마운트될 때와 user.id 상태가 변경될 때 실행
  useEffect(() => {
    // 사용자가 인증되어 있지 않으면 홈 페이지로 리디렉션하고 로그인 모달 열기
    if (!user.id) {
      navigate("/"); // or your login page
      setLoginIsOpen(true);
    }
  }, [user.id, navigate, setLoginIsOpen]); // user.id 상태가 변경될 때마다 실행

  // 사용자가 인증되어 있지 않은 경우 "No Access" 반환, 그렇지 않은 경우 자식 요소들 반환
  if (!user.id) {
    return <>No Access</>;
  }

  return <>{children}</>; // children은 해당 컴포넌트로 전달된 자식 요소들을 나타냄
};

export default ProtectedRoutes;
// src/main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Feed from "./pages/Feed.tsx";
import Upload from "./pages/Upload";
import Profile from "./pages/Profile.tsx";
import Post from "./pages/Post.tsx";
import ProtectedRoutes from "./components/ProtectedRoutes.tsx";

const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <ProtectedRoutes>
        <Feed />
      </ProtectedRoutes>
    ),
  },
  {
    path: "/upload",
    element: (
      <ProtectedRoutes>
        <Upload />
      </ProtectedRoutes>
    ),
  },
  {
    path: "/profile/:id",
    element: <Profile />,
  },
  {
    path: "/post/:id",
    element: <Post />,
  },
]);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
    <App />
  </React.StrictMode>
);

 

React 애플리케이션에서 사용자 인증 상태를 확인하여 특정 경로에 접근하는 것을 제한하는 컴포넌트인 ProtectedRoutes를 정의한다.

기본적으로 React Router를 사용하고, 상태 관리를 위해 Zustand 라이브러리의 useUserStore 및 useGeneralStore 훅을 활용한다.

이 컴포넌트는 자식 요소를 감싸고 있으며, 자식 요소는 보호된 경로에만 접근 가능하다. 그렇지 않은 경우 사용자를 다른 페이지 혹은 로그인 페이지로 리디렉션한다.

 

// src/main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Feed from "./pages/Feed.tsx";
import Upload from "./pages/Upload";
import Profile from "./pages/Profile.tsx";
import Post from "./pages/Post.tsx";
import ProtectedRoutes from "./components/ProtectedRoutes.tsx";
import { ApolloProvider } from "@apollo/client";
import { client } from "./utils/apolloClient";

const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <ProtectedRoutes>
        <Feed />
      </ProtectedRoutes>
    ),
  },
  {
    path: "/upload",
    element: (
      <ProtectedRoutes>
        <Upload />
      </ProtectedRoutes>
    ),
  },
  {
    path: "/profile/:id",
    element: <Profile />,
  },
  {
    path: "/post/:id",
    element: <Post />,
  },
]);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <RouterProvider router={router} />
      <App />
    </ApolloProvider>
  </React.StrictMode>
);

 

main.tsx에서 RouterProvider와 App 컴포넌트를 ApolloProvider 컴포넌트로 감싼다.

프론트엔드 서버에서 apollo-upload-client 관련 오류가 발생. 최신버전인 18버전을 삭제하고 17버전을 설치하였다. 타입스크립트 호환 관련 문제가 있는 듯 하다.

 

npm uninstall apollo-upload-client
npm i apollo-upload-client@17

 

반응형