Gila - 초기 lint 세팅, auth 구현
프로젝트 시작 초기 세팅
이제 어느정도 의견 교통정리가 되어서 프로젝트 시작전 초기 세팅을 하기로 했다. 팀원분들이 lint사용을 안해보신 분들이 계셔서 내가 두번이나 사용해본적 있기 때문에 내가 맡아서 했다.
eslint+airbnb
이번에도 airbnb lint를 사용해서 사용하기로 했다. 처음에는 직접 린트를 만들어서 구현해 볼 생각이였지만 그렇게하면 너무 헐거운 규제가 될것 같아서 일단 엄격한 규칙을 정하고 생산성이 떨어지는 옵션만 꺼두기로 했다. 전체적인 것은 이전과 거의 똑같다.
{ "env": { "browser": true, "node": true, "es6": true }, "extends": [ "airbnb", "airbnb-typescript", "airbnb/hooks", "next/core-web-vitals", "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:prettier/recommended", "prettier" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module", "project": "./tsconfig.json" }, "plugins": ["@typescript-eslint", "react", "prettier"], "ignorePatterns": ["node_modules/"], "rules": { "react/react-in-jsx-scope": "off", //코드에 react import안하면 error off "react/jsx-filename-extension": ["warn", { "extensions": [".ts", ".tsx"] }], // 파일 확장자 .ts, .tsx로 한정 "no-useless-catch": "off", // 불필요한 catch 사용 off "no-console": "error", // console 사용하면 error "react/jsx-props-no-spreading": "off", //spread사용시 error off "jsx-a11y/no-static-element-interactions": "off", // div에 이벤트 등록이 error off "jsx-a11y/click-events-have-key-events": "off", //onClick 이벤트에 key event 포함 안하면 error off "@typescript-eslint/naming-convention": [ "error", { "selector": "function", // 컴포넌트 네이밍 컨벤션 : pascal "format": ["PascalCase"], "modifiers": ["exported"] }, { "selector": "interface", "format": ["PascalCase"] } // interface 네이밍 컨벤션 : pascal ], "react/require-default-props": 0 // optional prop 유형에 해당 defaultProps값이 있는지 확인 off } }
이번에 린트 설정을 하면서 상당히 신경을 많이 썼다. 전에는 잘 되던 린트가 적용되지 않는 상황에 발생했기 때문이다. 우선 원인은 eslint의 최신 버전으로 업데이트해서 사용하면서 이전에 작성해서 사용하던 .eslintrc.json파일이 eslint.config.js로 변경되어야하기 때문이다. 그래서 이 부분을 마이그레이션 하려고 상당한 노력을 다했지만 결국 해결이 안됬다. 정확히 말하면 npm run lint를하면 에러 확인이 되지만 작성하는 시점에서는 확인이 불가능 하다는 것이다. 그래서 최종 결정은 eslint를 다운그레이드하기로 결정했다.
다행히 지금은 에러가 잘 확인된다.
prettier
그리고 팀원들이 같은 컨벤션으로 코드를 작성할 수 있도록 프리티어 설정도 같이 작성했다.
{ "tabWidth": 2, "printWidth": 100, "singleQuote": true, "bracketSpacing": true, "trailingComma": "all", "arrowParens": "always", "endOfLine": "lf", "useTabs": false }
전 프로젝트에서 세미콜론을 생략하는 옵션을 넣었었는데 nextjs 공식문서를 확인해보니 세미콜론이 잘 들어가 있는 모습을 보고 이번에는 생략하지 않고 넣도록 했다.
.nvmrc
이번 프로젝트에서 처음 사용해보는 기능이였다. 이것은 nodejs의 버전을 통일해서 사용할 수 있는 기능이다. 모든 팀원들이 최신 버전의 nodejs를 사용하고 있다면 아무 문제가 없지만 다른 버전을 사용하고 있는 경우에 충돌이 날수 있는 상황을 대비해 같은 버전으로 사용할 수 있도록 도와주는 것이다. 그래서 이 파일에는 적용할 nodejs의 버전만 들어가 있다. 이제 nvm이라는 기능을 어떻게 사용하는지 간단하게 알아보겟다.
homebrew
우선 homebrew를 통해 nvm을 설치해줘야한다. homebrew는 복잡한 것이 아니라 패키지 매니지 애플리케이션이다. 그래서 명령어 한줄로 설치 및 제거가 가능하도록 돕는다.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
해당 명렁어를 터미널에 입력해 설치해준다. 그리고 homebrew는 zsh에서 사용가능하기 때문에 맥의 기본인 bash에서 zsh로 변경해주면 된다.
chsh -s /bin/zsh
이렇게 해주면 zsh방식으로 가능해진다.
nvm
brew install nvm
이제 설치한 homebrew를 사용해 nvm을 설치해준다.
nvm use
이제 파일에 포함되어 있는 .nvmrc파일의 버전을 사용하기 위해 위의 명령어를 실행시켜주면 작성된 버전에 해당하는 nodejs가 설치된다.
나도 급하게 찾아서 프로젝트에 적용시켜놔서 정확하게 어떻게 됬는지 정확하게 남겨둔게 없다. 아직 터미널을 이용해서 뭔가를 하는건 참 어렵다.
인증 기능 구현
우선 기초 설정은 어느정도 끝이 났다. 이제 R&R을 통해서 어떤 작업을 누가할지 정했다.
페이지에 보여지는 ui, ui에 들어갈 form에 해당하는 container, 로그인과 회원가입 로직, api 리스폰스 타입, fetcher, 미들웨어로 역할을 나눴다. 나는 이번 서비스에서 나름 핵심인 인증부분을 맡아보고 싶어서 인증부분을 다른 팀원분과 같이 맡았다.
지금까지 나는 인증을 nextauth를 사용해서 토큰을 관리했었는데 우리가 Oauth를 사용할게 아니라면 굳이 nextauth를 사용할 필요가 없다고 판단해서 일단 서버액션을 통해 서버에 토큰을 쿠키로 저장하도록 결정했다. 이부분은 같이 작업하는 팀원분이 잘 알고 계셔서 나는 로그인보다는 회원가입을 구현하기로 했다.
회원가입
회원가입을 했을때 로직 자체는 너무 쉽다. 아이디와 닉네임, 비밀번호를 받아서 post요청을 보내기만 하면 된다.
const register = async ({ email, nickname, password }: Props): Promise<ActionType<User>> => { try { await api.post('/users', { email, nickname, password }); return { success: true, message: '성공' }; } catch (error) { const customError = error as CustomError; return { success: false, message: customError.response.data.message }; } };
하지만 이번에는 조금더 체계적인 코드를 작성하기 위해 함수의 타입을 정의해줬다. 우선 해당 로직의 결과로 promise객체가 리턴되고 객체의 타입을 지정해줬다.
interface ActionType<T> { success: boolean; message: string; data?: T; }
그리고 액션 타입은 결과값과 결과 메세지, 데이터를 포함한다. 데이터는 제네릭으로 처리해줬다. 하지만 회원가입에서는 제네릭으로 사용할만한 데이터는 없다. 회원가입에 성공하면 토큰도 발행되지 않기 때문에 바로 로그인 페이지로 이동시켜줄 것이다. 그래서 회원가입의 결과로 User타입의 객체가 리턴되는데 사용할 상황이 없다.
리퀘스트의 결과로 결과 값을 boolean을 넣어주고 메세지를 넣어주면 된다. 회원가입의 경우 다양한 400에러 메세지가 구현되어 있어서 에러에 표시된 메세지를 바로 넘겨주는 방식을 사용했다.
try...catch문에서 error는 unknown타입이라서 커스텀 에러라는 타입을 만들어서 as로 타입 지정을 해줬다.
interface CustomError extends Error { response: { data: { message: string; }; }; }
그러면 error의 타입이 명시되어서 타입 오류가 발생하지 않는다.
이제 실제로 결과값을 어떻게 사용하는지 간단하게 보겠다.
async function onSubmit(values: z.infer<typeof formSchema>) { const { nickname, email, password } = values; const action = await register({ email, nickname, password }); if (!action.success) { toast.error(action.message); return; } toast.success(action.message); }
register의 결과를 받아서 success값에 따라 토스트의 메세지를 세팅하는 것이다.
로그인
내가 구현한 부분은 아니지만 간단하게 보겠다.
'use server' import { cookies } from 'next/headers'; const login = async (email: string, password: string): Promise<ActionType<AuthResponse>> => { try { const response = await api.post('/auth/login', { email, password }); const { data } = response; const token = data.accessToken; cookies().set('GilaToken', token); return { success: true, message: '성공' }; } catch (error) { return { success: false, message: '실패' }; } };
우선 서버 액션을 사용했다. 서버액션을 사용한 가장 큰 이유는 토큰을 쿠키에 저장하기 위함이다. 서버액션하지 않고 클라이언트액션을 하게 되면 토큰을 결국 로컬스토리지에밖에 저장을 못한다. 하지만 next서버에서 해당 api를 실행하면 결과를 next서버에 저장할 수 있게된다. 그래서 cookies라는 것을 사용해 저장하면된다. 위의 코드를 보면 결과로 accessToken이 오는데 그 토큰을 GilaToken이라는 이름으로 쿠키에 저장하는 것이다.
로그인했을때 쿠키에 우리가 설정한 이름으로 잘 저장되어 있는 것을 확인할 수 있다.
middleware
추가적으로 미들웨어까지 이어서 설명해보겠다. 우리가 저장한 쿠키의 인증 토큰을 활용해 인증 여부를 확인할 수 있는 것이다. 내가 작성한 코드는 아니지만 팀원이 작성한 코드이기 때문에 정리해본다.
export const middleware = (request: NextRequest) => { const hasAccessToken = request.cookies.has('GilaToken'); const { pathname } = request.nextUrl; const authRoutes = ['/login', '/register']; const isAuthRoutes = authRoutes.includes(pathname); if (hasAccessToken && isAuthRoutes) { return NextResponse.redirect(new URL('/', request.url)); } return NextResponse.next(); }; export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };
우선 config의 matcher로 모든 경로를 넣어준다. 그러면 모든 경로에 미들웨어가 적용된다. 아직 모든 페이지가 구현되지 않았기 때문에 로그인 여부를 확인해 로그인과 회원가입 페이지에서 리다이렉션을 적용시켜준다.
로그인시에 쿠키에 저장된 토큰을 가져온다. 이 토큰 여부에 따라 로그인여부도 판단이 가능하다. 그리고 인증과 관련된 route를 authRoutes로 묶는다. 이제 인증 페이지주소와 현재 페이지 주소를 비교한다. 최종적으로 로그인했고 인증 주소와 현재 페이지 주소가 일치하는 경우에 메인 페이지 /로 리다이렉션 시켜준다.
fetcher
기획단계에서 fetcher를 만들어달라고 하셨는데 정확하게 어떤 것을 원하시는지 확인이 어려워서 일단 받은 토큰을 일괄적으로 헤더에 넣어주는 axios interceptor를 설정했다. 우선 쿠키에 저장된 토큰을 가져오기 위해 서버액션 함수를 하나 만들어줬다.
const getAccessToken = async (): Promise<string | null> => { const cookieStore = cookies(); const accessToken = cookieStore.get('GilaToken'); if (!accessToken) return null; return accessToken.value; };
이 함수도 서버에서 실행되며 쿠키에 GilaToken으로 저장된 값이 있으면 그 값을 리턴하는 함수이다.
try { api.interceptors.request.use( async (config) => { const token = await getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); }, ); } catch (error) {}
그리고 axios interceptor로 설정해준다. 이제 로그인했을때 모든 api 헤더에 토큰이 잘 삽입된다.
nextAuth는 안될까?
팀원들과 회의를 거쳐서 nextauth를 사용하지 않기로 했지만 내가 다른 사람의 코드를 보면서 느낀점은 nextauth도 똑같이 기능을 한다는 것이다. 그리고 내가 느끼기에는 nextauth가 인증 여부를 확인하고 cookie에서도 토큰을 직접 확인할 수 없다는 장점이 있다. 여기에서 파생되는 점은 미들웨어나 fetcher의 코드가 조금더 간결해지지 않을까 싶은 생각이 든다.
마무리
프로젝트가 드디어 시작됬다. 며칠동안 여러 논의를 거쳐 시작된만큼 진전속도나 코드 안정성등에서 더 좋은 결과가 나왓으면 한다. 지금 내가 사용해보지 못한 것들이 많아서 조금 복잡하다. 현재 form을 구현함에 있어서 shadcn을 사용하는데 react-hook-form은 사용해봤지만 zod는 한번도 사용해본적이 없다. 하지만 찾아보니 많이 사용하는 것으로 봐서 결국 배우긴 해야한다...
내가 모르는 것들이 너무나도 많다. 하지만 어쩌겠나 해야지..해야지...처음인만큼 복잡하고 어려운것은 당연하다. 기죽지말고 열심히 코드작성하고 노력하자.
개의 댓글
0
프로젝트 시작 초기 세팅
eslint+airbnb
prettier
.nvmrc
homebrew
nvm
인증 기능 구현
회원가입
로그인
middleware
fetcher
nextAuth는 안될까?
마무리