728x90
반응형

전 포스트에서 NextJS 프론트에서 데이터를 요청하는것 까지 해보았다. 이번엔 백엔드를 설계해보자.

먼저 해당 포스트로 접속해 mongoose 까지 설치해준다.

 

https://typo.tistory.com/entry/Nestjs-MongoDB-Schema?category=895068 

 

Nest.js | MongoDB | Schema

이 글은 NestJS document 사이트 기반으로 작성했습니다. https://docs.nestjs.kr/techniques/mongodb 네스트JS 한국어 매뉴얼 사이트 네스트JS 한국, 네스트JS Korea 한국어 매뉴얼 docs.nestjs.kr 몽고DB설치하..

typo.tistory.com

 

- mongoose 설치

 

$ npm install --save @nestjs/mongoose mongoose

 

혹시 기존에 생성했던 apple 리소스가 있다면 삭제해준다.

app.module.ts 를 다음과 같이 수정한다.

 

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [MongooseModule.forRoot('mongodb://test:test1234@localhost:27017/admin')],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

user 리소스를 생성해준다.

$ nest g res apple

 

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import { UserModule } from './user/user.module';

@Module({
  imports: [MongooseModule.forRoot('mongodb://test:test1234@localhost:27017/admin'), UserModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

그럼 이렇게 파일들과 자동으로 import되는것을 볼 수 있다.

 

schemas 폴더를만들고 user.schema.ts 파일을 작성해준다.

 

/src/user/schemas/user.schemas.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
  @Prop()
  id: string;
  
  @Prop()
  pw: string;
  
  @Prop()
  email: string;

  @Prop()
  address: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

 

user.module.tsuserSchema에 관한 내용을 import해준다. 여기서 id부분은 자동으로 1씩 더해서 추가해주기위해 임의로 정의한 것이다.

 

user.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User, UserSchema } from './schemas/user.schema';

@Module({
  imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
  controllers: [UserController],
  providers: [UserService]
})
export class UserModule {}

 

우리는 일단 아이디 중복 확인을 위해 다음 @Get(':id') 데코레이터만 쓸 것이다. 우리는 사용자 id를 string으로 받을 것이기 때문에 수정해주자.

 

user.controller.ts

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }

 

service 부분에 mongoose schema를 추가한다.

 

user.service.ts

import { Injectable } from '@nestjs/common';
//mongoose
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
//dto
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}

  findOne(id: string) {
    return `This action returns a #${id} user`;
  }

}

 

 

findOne 함수를 async 함수로 바꿔주고( mongoose Query은 Promise 함수입니다. ) 지금은 중복확인으로 result 값만 필요하지만 나중에 로그인 할 때 이 함수를 써서 유저의 정보를 받아와야하기 때문에 id에 맞는 유저의 data가 존재할 때 user도 result와 함께 반환해준다.

 

user.service.ts

import { Injectable } from '@nestjs/common';
//mongoose
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
//dto
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}

  async findOne(id: string) {
    const userOne = await this.userModel.findOne({id});
    if(userOne)
      return {result : false, user : userOne}
    else
      return {result : true}
  }

}

 

 

이제 npm start dev로 백엔드를 실행하고 중복확인 버튼을 눌러보자.

 

 

 

정상적으로 중복확인 버튼이 disabled가 되었다.

728x90
반응형
728x90
반응형

 

SignupComponent 부분에 더 입력해야 될 값들을 추가해서 화면구성을 하자.

 

SignupComponent.tsx

import type { NextPage } from 'next'
import { UserState } from "../../store/interfaces/"
import { useDispatch, useSelector } from "react-redux";
import { actionTypesUser } from "../../store/interfaces/user/userAct.interfaces"
import { RootStateInterface } from "../../store/interfaces/RootState";

interface Props {
    userChangeHandler : (event : any) => void;
    userState : UserState;
}

const Signup:NextPage<Props> = ({userChangeHandler,userState}) => {
    const dispatch = useDispatch();
    const submitHandler = (event : any) => {
        event.preventDefault()
    }
    return (
        <div className="signup">
            <form onSubmit={submitHandler}>
                <div>
                    <input 
                        name="id"
                        type="text"
                        placeholder="Enter ID"
                        onChange={userChangeHandler} />
                    <button>
                    중복확인     
                    </button>
                </div>
                <div>
                    <input 
                        name="pw"
                        type="password"
                        placeholder="Enter PW"
                        onChange={userChangeHandler} />
                </div>
                <div>
                    <input 
                        name="pw2"
                        type="password"
                        placeholder="Verify password"/>
                </div>
                <div>
                    <input 
                        name="email"
                        type="email"
                        placeholder="Enter Email"
                        onChange={userChangeHandler} />
                    <button>
                    보내기    
                    </button>
                </div>
                <div>
                    <input 
                        name="address"
                        type="text"
                        placeholder="Enter Address"
                        onChange={userChangeHandler} />
                    <button>
                    주소검색    
                    </button>
                </div>
                <div className="signbutton">
                    <button
                        type="submit">
                    Signup       
                    </button>
                </div>
            </form>
        </div>
        )
}

export default Signup

 

 나중에 중복확인이 완료되고 id 입력칸을 비활성화로 만들어주기 위해 state를 만들어준다.

    //idDuple
    const [idDupleOK,setIdDupleOK] = useState<boolean>(false)

 

 

화면 부분도 바꿔준다.

<div>
    <input 
        name="id"
        type="text"
        placeholder="Enter ID"
        onChange={userChangeHandler}
        disabled={idDupleOK} />
    <button
    	type="button"
        onClick={IdDuplicate}
        disabled={userState.idDuple}
        >
    중복확인
    </button>
</div>

 

 

 화면으로 확인해보자.

 

 

참고로 기능위주로 구현하고 CSS는 나중에 다룰것이다. ( div에 className을 하나씩 추가한 이유 )

 

로그를 확인해보면 userState 값이 정상적으로 바뀌는것을 확인할 수 있다.

axios 모듈이 없으면 npm i axios 로 설치하면 된다.

id를 파라미터로 받아 백엔드에 GET 메소드로 데이터를 요청해 반환값을 전달하는 userIdDuplicate 함수를 작성했다.

 

이제 userAct.interfaceuser_reducer부분에 ID중복확인 부분을 추가해준다.

 

user.interface.ts

export enum actionTypesUser {
    USER_INIT = "USER_INIT", // state 초기화
    USER_STATE = "USER_STATE", // userState 변경
    USER_INFO = "USER_INFO",  // userInfo 변경 
    USER_ID_DUPLICATE = "USER_ID_DUPLICATE" // ID 중복 검사
}

export type ActionsUser = UserInit | UserState | UserInfo | UserIdDuplicate

export interface UserInit {
    type : actionTypesUser.USER_INIT;
    data : any;
}

export interface UserState {
    type : actionTypesUser.USER_STATE;
    data : any;
}

export interface UserInfo {
    type : actionTypesUser.USER_INFO;
    data : any;
}

export interface UserIdDuplicate {
    type : actionTypesUser.USER_ID_DUPLICATE;
    payload: any;
}

 

user_reducer.ts

        case actionTypesUser.USER_ID_DUPLICATE:
            return {
                ...state, data : action.payload
            }

 

ID중복확인을 위한 함수를 작성하기 전에 우리가 USER_STATE, USER_INFO 액트를 쓸 땐 백엔드(NestJS)와의 통신이 없어도 상관없었지만 ID 중복확인을 위한 과정에는 백엔드(NestJS)와의 통신이 필요하다.

 

때문에 dispatch에 사용될 Action을 정의할 것이다.

store 밑에 user_action.ts 파일을 생성한다.

/store/user_action.ts

import Axios from 'axios'
import { actionTypesUser } from '../interfaces'

export async function userIdDuplicate (id : string) {
    const request = Axios.get('/api/user/'+id)
    .then(response => response.data)
    
    return {
        type : actionTypesUser.USER_ID_DUPLICATE,
        payload: request
    }
}

 

이제 컴포넌트에서 dispatch로 axios의 결과값에 따른 로직을 만들어준다.

SignupComponent.tsx

import {userIdDuplicate} from '../../store/actions/user_action'

    (...)
    
    //idDuple
    const [idDupleOK,setIdDupleOK] = useState<boolean>(false)
    }
    const IdDuplicate = () => {
        if(userState.userInfo.id !=='') { // 입력된 값이 존재할 경우
            dispatch(userIdDuplicate(userState.userInfo.id)).then((req : any)=> {
                if(!req.payload.result) { // 아이디가 이미 존재할 때
                    alert('해당 아이디가 이미 존재합니다.');
                }else {
                    alert('중복된 아이디가 없습니다.');
                    dispatch({type : actionTypesUser.USER_STATE, data: ['idDuple',true]});
                    setIdDupleOK(true);
                }
            })
        }
    }
    
    (...)
    
    <input 
      name="id"
      type="text"
      placeholder="Enter ID"
      onChange={userChangeHandler}
      disabled={idDupleOK} />
    <button
      type="button"
      onClick={IdDuplicate}
      disabled={userState.idDuple}
    >
    중복확인     
    </button>
    
    (...)

 

  1. 만들어두었던 action을 가져온다.
  2. 버튼을 클릭할 경우 동작될 함수를 선언한다.
  3. 함수에선 dispatchdata에 입력한 ID를 전달한다.
  4. 반환값에 따라 조건문을 써준다. 만약 중복된 아이디가 없을 경우 UserStateidDuple값을 바꿔준다.

 

여기까지 중복확인 버튼을 눌렀을 때 dispatch로 백엔드에 데이터를 요청해서 원하는 값을 받아오고,

받아온 값에 따라 어떻게 처리할지 조건문으로 써주었다. 

 

다음 포스트에서는 백엔드(NestJS)를 구성해보겠다.

 

 

728x90
반응형
728x90
반응형

먼저 회원가입부터 구현하도록 하자.

 

먼저 user.interfaces.ts와 user 부분에 store의 state를 바꿔주는 부분을 추가하고 다음과같이 액션들의

interface부분을 바꿔준다.

 

userAct.interfaces.ts

export enum actionTypesUser {
    USER_INIT = "USER_INIT", // state 초기화
    USER_STATE = "USER_STATE", // userState 변경
    USER_INFO = "USER_INFO",  // userInfo 변경 
}

export type ActionsUser = UserInit | UserState | UserInfo 

export interface UserInit {
    type : actionTypesUser.USER_INIT;
    data : any;
}

export interface UserState {
    type : actionTypesUser.USER_STATE;
    data : any;
}

export interface UserInfo {
    type : actionTypesUser.USER_INFO;
    data : any;
}

 

user_reducer.ts

(...)
        case actionTypesUser.USER_STATE:
            return {
                ...state,
                [action.data[0]] : action.data[1]
            }
        case actionTypesUser.USER_INFO:
                return {
                    ...state,
                    userInfo : {
                        ...state.userInfo,
                        [action.data[0]] : action.data[1]
                    }
                }
(...)

State 안의 값이 변경될 경우와 userInfo 변경만 필요할 경우를 나눠서 선언해준다.

 

그다음 Input에서 쓰일 userChangeHandler를 선언해주고, SignupComponentuserState와 함께 props를 전달한다.

[cate].tsx

import { GetServerSideProps, GetStaticPaths, GetStaticProps } from 'next'
import type { NextPage } from 'next'
//Redux
import { useDispatch, useSelector } from "react-redux";
import { RootStateInterface } from "../../store/interfaces/RootState";
import { UserState } from "../../store/interfaces/"
//React
import { useState } from 'react';
//Components
import LoginComponent from '../../src/components/LoginComponent'
import SignupComponent from '../../src/components/SignupComponent'

interface Props {
    CateData : string
}


const Auth:NextPage<Props> = ({CateData}) => {
    const dispatch = useDispatch();
    let userState = useSelector( // store에서 가져온 userState
        (state: RootStateInterface) : UserState => state.user
    )
    const userChangeHandler = (event : any):void => { // Input Handler
        dispatch({type : actionTypesUser.USER_INFO, data : [event.target.name,event.target.value]})
    }
    console.log(userState)

    return (

        <div className='auth'>
            {CateData} page
            {(CateData === 'login') ? <LoginComponent /> : ""}
            {(CateData === 'signup') ? 
            <SignupComponent
                userChangeHandler={userChangeHandler} // 회원가입에 props로 Handler함수를 보낸다.
            	userState={userState}
            /> : ""}
        </div>

        )
}


export default Auth;


export const getServerSideProps:GetServerSideProps = async (context) => { // SSR
    const CateData = context.query.cate;
    return {
        props: {
            CateData
        }
    }
    
}

 

컴포넌트에서 State가 잘 바뀌는지 확인하기위해 console.log를 추가하였다.

아직 SignupComponentProps type을 지정해주지 않았기 때문에 에러가 뜬다.

Props type을 설정해주고 log로 확인해보자.

 

SignupComponent.tsx

import type { NextPage } from 'next'
import { UserState } from "../../store/interfaces/"
import { useDispatch, useSelector } from "react-redux";
import { actionTypesUser } from "../../store/interfaces/user/userAct.interfaces"
import { RootStateInterface } from "../../store/interfaces/RootState";

interface Props {
    userChangeHandler : (event : any) => void;
    userState : UserState;
}

const Signup:NextPage<Props> = ({userChangeHandler,userState}) => {
    console.log(userChangeHandler)
    return (
        <div className='signup'>
        </div>
        )
}

export default Signup

 

 

정상적으로 떴으니 이제 form에 필요한 화면과 함수까지 구현해보자.

 

SignupComponent.tsx

import type { NextPage } from 'next'
import { UserState } from "../../store/interfaces/"
import { useDispatch, useSelector } from "react-redux";
import { actionTypesUser } from "../../store/interfaces/user/userAct.interfaces"
import { RootStateInterface } from "../../store/interfaces/RootState";

interface Props {
    userChangeHandler : (event : any) => void;
    userState : UserState;
}

const Signup:NextPage<Props> = ({userChangeHandler,userState}) => {
    const dispatch = useDispatch();
    const submitHandler = (event : any) => {
        event.preventDefault()
    }
    return (
        <div className='signup'>
            <form onSubmit={submitHandler}>
                <input 
                    name="id"
                    type="text"
                    placeholder="Enter ID"
                    onChange={userChangeHandler} />
                <button
                    type="submit">
                회원가입        
                </button>
            </form>
        </div>
        )
}

export default Signup

 

input onChange 부분에 props로 전달받았던 userChangeHandler를 주입했다.

 

아까 수정했던 [cate].tsx 파일에 작성했던 console.log로 확인해보자. userInfo가 정상적으로 바뀌는지 확인해보자.

 

SignupComponentuserChangeHandler 함수가 잘 넘어갔고, [cate].tsx 파일에 선언했던 userState가 정상적으로 바뀌는것을 확인했다.

 

728x90
반응형
728x90
반응형

로그인 회원가입을 위한 페이지를 세팅해보자. 먼저 index.tsx 파일을 수정해준다.

 

index.tsx

import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import Link from 'next/link'
import styles from '../styles/Home.module.css'

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to my Site!
        </h1>

        <div className={styles.grid}>
          <Link href={{
            pathname: '/auth/login'
          }}>
            <a className={styles.card}>
              <p>
                Login
              </p>
            </a>
          </Link>
          <Link href={{
            pathname: '/auth/signup'
          }}>
            <a className={styles.card}>
              <p>
                Signup
              </p>
            </a>
          </Link>
        </div>
      </main>

      <footer className={styles.footer}>
        <p>
        copyright © Teepo
        </p>
      </footer>
    </div>
  )
}

export default Home

 

실행해본다.

 

간단하게 Login, Signup 버튼이 나오고, Link href 옵션에 '/auth/login' 과 '/auth/signup' 을 써주었다.

 

이제 우린 auth라는 파일을 만들고 SSR로 받아오는 데이터를 읽어 어떤 컴포넌트를 보여줄 것인지 정의할 것이다. 

 

 

경로는 /auth/[cate].tsx가 되고, [cate].tsx 부분에 무슨 문자열이 오는지에 따라 다른 컴포넌트를 보여줄 것이다.

 

지금 상태에서 웹을 확인해보면

 

이런 문구가 뜰 것이다. [cate].tsx 파일을 수정해보자.

 

/pages/auth/[cate].tsx

import { GetServerSideProps, GetStaticPaths, GetStaticProps } from 'next'
import type { NextPage } from 'next'

interface Props {
    CateData : string
}

const Auth:NextPage<Props> = ({CateData}) => {
    console.log(CateData)
    return (
        <div className='auth'>
            {(CateData === 'login') ? "Login" : ""}
            {(CateData === 'signup') ? "Signup" : ""}
        </div>
        )
}


export default Auth;


export const getServerSideProps:GetServerSideProps = async (context) => {
    const CateData = context.query.cate;
    return {
        props: {
            CateData
        }
    }
}

 

getServerSideProps로 받아온 데이터의 cate 값을 읽어 어떤 문자열이 들어오는지 읽고,

삼항연산자로 문자열에 따라 보여줄 내용을 정의했다. 나중에 css 추가를 위해 className에 'auth'를 추가해주었다.

 

 

물론 각각 다른 파일들로 만드는 방법도 있다. 하지만 나는 로그인, 회원가입에서 Header를 같이 쓰고싶었기 때문에 

이렇게 구조를 정의했다.

 

이번엔 Component들을 만들어보자. 루트경로에 src 폴더와 하위에 component 폴더를 만들고 아래 파일들을 작성한다.

 

LoginComponent.tsx

import type { NextPage } from 'next'

interface Props {

}

const Login:NextPage<Props> = () => {
    return (
        <div className='auth'>
            Login
        </div>
        )
}


export default Login

 

SignupComponente.tsx

import type { NextPage } from 'next'

interface Props {

}

const Signup:NextPage<Props> = () => {
    return (
        <div className='auth'>
            Signup
        </div>
        )
}


export default Signup

 

이제 [cate].tsx 파일에서 컴포넌트를 사용해보자.

 

[cate].tsx

import { GetServerSideProps, GetStaticPaths, GetStaticProps } from 'next'
import type { NextPage } from 'next'
import LoginComponent from '../../src/components/LoginComponent'
import SignupComponent from '../../src/components/SignupComponent'


interface Props {
    CateData : string
}

const Auth:NextPage<Props> = ({CateData}) => {
    return (
        <div className='auth'>
            {CateData} page
            {(CateData === 'login') ? <LoginComponent /> : ""}
            {(CateData === 'signup') ? <SignupComponent /> : ""}
        </div>
        )
}


export default Auth;


export const getServerSideProps:GetServerSideProps = async (context) => {
    const CateData = context.query.cate;
    return {
        props: {
            CateData
        }
    }
}

 

Header에 쓰일 부분에 임의로 어떤 페이지인지 나타내주는 문구를 작성했다.

컴포넌트들이 잘 나타나는지 확인해본다.

 

 

 

728x90
반응형
728x90
반응형

나중에 프론트 쪽에서 할 상태관리를 위해 front폴더에 세팅을 해준다.

자세한 내용은 아래 링크에 접속하면 확인 가능하다.

https://typo.tistory.com/entry/Nextjs-next-redux-wrapper

 

Next.js | next-redux-wrapper

NextJS에서 상태를 관리하기 수월하게 해주는 redux를 TypeScript와 함께 사용해보자. 이 포스트는 counter에 대한 state를 다루는 포스트이다. 사전 준비 create-next-app으로 프로젝트를 생성한다. $ npx create.

typo.tistory.com

 

로그인에 관한 기본적인 세팅만 해주겠다.

 

1. 먼저 필요한 npm 모듈들을 설치하자.

$ npm i react-redux redux next-redux-wrapper redux-thunk @types/redux-promise @types/redux-logger
$ npm i redux-devtools-extension redux-logger --dev-save

 

2. 루트 디렉토리에 store폴더를 생성하고 아래와 같이 구조를 만들어준다.

3. 그다음 아래 코드들을 입력해준다.

user.interfaces.ts

export interface UserState {
    userInfo : UserInfo;
    islogined : boolean;
    loginError : '';
    signupDone : boolean;
    signupError : '';
    idDuple : boolean;
    emailDuple : boolean;
    emailAuth : boolean;
    data : any;
}

export interface UserInfo {
    id : string;
    pw : string;
    email : string;
    address : string;
}

입력값으로 쓰일 User interface와 함수실행 후 반환값으로 받을 값들을 선언해준다.

마지막 data 속성은 차후에 payload로 데이터를 받기위한 저장공간이다.

UserStateUser를 포함한다.

 

 

userAct.interfaces.ts

export enum actionTypesUser {
    USER_INIT = "USER_INIT", // state 초기화
}

export type ActionsUser = UserInit 

export interface UserInit {
    type : actionTypesUser.USER_INIT;
    data : any;
}

아직은 UserState를 초기화하는 actionType 밖에 없지만 차근차근 늘려나갈 것이다.

 

 

 

/store/interfaces/index.ts

export * from './user/user.interfaces'
export * from './user/userAct.interfaces'

4. user 인터페이스와 액트를 export한다.

 

/store/interfaces/RootState.ts

import { UserState } from './index'

export interface RootStateInterface {
    user : UserState;
}

5. UserState를 가져와 RootState에 넣어준다. ( 이곳은 모든 State를 포함합니다. )

 

/store/reducer/index.ts

import { combineReducers, Reducer, AnyAction } from "redux";
import { RootStateInterface } from "../interfaces/RootState";
import user from "./user_reducer";

const rootReducer: Reducer<
	RootStateInterface,
	AnyAction
> = combineReducers<RootStateInterface>({
	user,
});

export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;

RootState 폴더에 선언한 State들을 가져와서 Reducer와 엮어준다.

 

 

/store/reducer/user_reducer.ts

import { HYDRATE } from "next-redux-wrapper";
import { getFontDefinitionFromManifest } from "next/dist/server/font-utils";
import {
    UserState,
    UserInfo,
    actionTypesUser,
    ActionsUser
} from "../interfaces";

export const initialState : UserState = {
    userInfo : <UserInfo>{
        id : "",
        pw : "",
        email : "",
        address : ""
    },
    islogined : false,
    loginError : '',
    signupDone : false,
    signupError : '',
    idDuple : false,
    emailDuple : false,
    emailAuth : false,
    data : {}
}

interface HydratePayload {
    user : UserState
}

const user = (
    state = initialState,
    action : ActionsUser | { type : typeof HYDRATE; payload : HydratePayload }
) : UserState => {
    switch (action.type) {
        case HYDRATE:
            return { ...state, ...action.payload.user};
        case actionTypesUser.USER_INIT:
            return {
                ...state,
                userInfo : initialState.userInfo
            }
        default:
            return state
    }
}

export default user;

userinitialState를 정의해주고 dispatch가 실행됐을 때의 로직을 정의한다.

나중엔 action이 추가됨에 따라 reducer의 case도 늘어날 것이다.

 

 

/store/index.ts

import { createStore, applyMiddleware, compose } from "redux";
import { createWrapper } from "next-redux-wrapper";
import promiseMiddleware from 'redux-promise';
import ReduxThunk from 'redux-thunk';
import rootReducer from "./reducer";

const createStoreWithMiddleware = applyMiddleware(promiseMiddleware, ReduxThunk)(createStore)

const configureStore = () => {
	const store = createStoreWithMiddleware (rootReducer);
	return store;
};

const wrapper = createWrapper(configureStore, { debug: true });

export default wrapper;

store를 생성하고 _app.tsx에 쓰일 wrapper를 만들어준다.

 

 

/pages/_app.tsx

import { AppProps } from "next/app";
import { NextPage } from "next";
import wrapper from "../store"; // store.ts 파일

const MyApp: NextPage<AppProps> = ({ Component, pageProps }: AppProps) => {
	return (
		<>
			<Component {...pageProps} />
		</>
	);
};

export default wrapper.withRedux(MyApp);

_app.tsx 파일의 MyApp을 만들었던 wrapper로 감싸주면 store 설정이 끝난다.

 

마지막으로 tsconfig.json에 store부분을 추가해준다.

 

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx","store/**/*.ts"],
  "exclude": ["node_modules"]
}
728x90
반응형
728x90
반응형

nest-next npm 카테고리에서 우리는 프론트엔드와 백엔드를 어느정도 통합시킨 프로젝트를 진행했었다.

서버를 개발하다보면 규모가 커지고, 보안상의 이유로 프론트엔드와 백엔드를 나눠야 할 때가 있다.

 

React를 쓸 때는 명확하게 client와 server로 나눠서 개발을 진행했었다.

이번에는 NextJS와 NestJS로 분리된 서버를 만들어보겠다.

 

Pakage 설치

1. 먼저 필요한 npm, node, npx들을 설치한다.

$ sudo apt-get install npm
$ sudo npm i -g n 
$ sudo n 14.18.1
$ sudo npm i npx -g

 

2. create-next-app( Next.js 프로젝트를 생성하고 기본 패키지를 설치해주는 프로그램)을 설치한다.

$ sudo npm i create-next-app -g

 

3. @nestjs/cli(NestJS 프레임워크)를 설치한다.

$ sudo npm i -g @nestjs/cli

 

Front-End 구축

 

1. front라는 이름의 NextJS 프로젝트를 생성한다.

$ npx create-next-app front --typescript

--typescript 옵션을 추가하면 NextJS 프로젝트가 typescript 버전으로 설치된다.

front 디렉토리로 이동해서 개발모드로 서버를 실행해보자.

 

eslint 설치

$ npx eslint --init

npx eslint --init
✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · react
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · airbnb
✔ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-airbnb@latest
The config that you've selected requires the following dependencies:

eslint-plugin-react@^7.21.5 @typescript-eslint/eslint-plugin@latest eslint-config-airbnb@latest eslint@^5.16.0 || ^6.8.0 || ^7.2.0 eslint-plugin-import@^2.22.1 eslint-plugin-jsx-a11y@^6.4.1 eslint-plugin-react-hooks@^4 || ^3 || ^2.3.0 || ^1.7.0 @typescript-eslint/parser@latest
✔ Would you like to install them now with npm? · No / Yes

 

 

$ npm run dev

2. locallhost:3000으로 접속해서 웹페이지가 정상적으로 뜨는지 확인한다.

 

 

 

Back-End 구축

1. back이라는 이름의 NestJS 프로젝트를 생성한다.

$ nest new back

 

2. back 폴더로 들어가 개발모드로 서버를 실행해보자. 아직 포트를 둘다 3000번을 쓰고있기 때문에 Next 서버를 먼저 종료하고 실행해보자.

$ npm run start:dev

 

 

Front-Back 연동

1. Back-End에 새로운 리소스 apple을 추가해보자. 먼저 nest에 apple이라는 resource를 하나 만들어준다.

$ nest g res apple

 

2. localhost의 3000번 포트로 확인해본다 

 

 

 

Back-End 포트 변경

1. back/src/main.ts 의 파일을 열어 기존 3000번 포트를 3001번으로 바꾼다.

 

Front-End 프록시 설정

Front에서 Back의 API를 쓰기위해 프록시를 설정해주어야 한다.

1. 먼저 .env 파일을 생성하고 다음과 같이 작성한다.

.env

SOURCE_PATH = '/api/:path*'
DESTINATION_URL = 'http://localhost:3001/:path*'

 

2. 그다음 next.config.js 파일을 수정해준다.

 

next.config.js

module.exports = {
  reactStrictMode: true,
  async rewrites() {
    console.log(process.env.NODE_ENV)
    if (process.env.NODE_ENV !== 'production') {
      return [
        {
          source: process.env.SOURCE_PATH,
          destination: process.env.DESTINATION_URL,
        },
      ];
    }
    else {
      return [
        {
          source: process.env.SOURCE_PATH,
          destination: process.env.DESTINATION_URL,
        },
      ];
    }
  },
}

 

3. 프록시 설정이 끝났다. 이제 front 서버와 back 서버를 실행하고 localhost:3000/api/apple에 접속해보자.

 

 

NextJS가 열린 3000번 포트에서 /api 경로를타니 3001번 포트에 열린 NestJS의 자원을 쓸 수 있는것을 확인할 수 있다.

 

 

728x90
반응형
728x90
반응형

이번 포스트는 주로 pages폴더를 분석할 것이다. 차근차근 _app.tsx 파일부터 보자.

 

app.tsx

import { FC } from 'react';
import type { AppProps /*, AppContext */ } from 'next/app';
import Sidebar from '../components/sidebar';

const MyApp: FC<AppProps> = ({ Component, pageProps }) => {
  console.log(Component)
  console.log(pageProps)
  return (
    <div style={{ display: 'flex', maxWidth: 1100 }}>
      <div style={{ flexBasis: '30%', margin: 25 }}>
        <Sidebar />
      </div>
      <div style={{ flexBasis: '70%', margin: 25 }}>
        <Component {...pageProps} />
      </div>
    </div>
  );
};

export default MyApp;

 

FCAppProps는 타입스크립트를 위한 모듈이다.

Sidebar 컴포넌트를 import하고 최상위인 MyAppSidebarComponent를 사용하였다.

파라미터인 ComponentpageProps를 로그로 확인해보면

 

Component는 리액트 Myapp에서도 쓰이는 함수이고, Nest에서 '/'경로에 home 을 렌더링 해주었기 때문에 home.tsx가 들어온다. pageProps는 Nest에서 넘겨주었던 name 파라미터가 null로 담겨있는것을 확인할 수 있다.

 


FC의 사용법은 아래 링크로 자세히 볼 수 있다.

https://react.vlpt.us/using-typescript/02-ts-react-basic.html

 

2. 리액트 컴포넌트 타입스크립트로 작성하기 · GitBook

2. 리액트 컴포넌트 타입스크립트로 작성하기 이번 섹션에서는 리액트 컴포넌트를 타입스크립트로 작성하는 방법을 알아보도록 하겠습니다. 프로젝트 생성 우선, 타입스크립트를 사용하는 리

react.vlpt.us


 

home.tsx

import * as React from 'react';
import { NextPage, NextPageContext } from 'next';

interface Props {
  query: { name?: string };
}

const Home: NextPage<Props> = ({ query }) => {
  const greetName = query.name ? query.name : 'World';

  return (
    <div>
      <div>Hello, {greetName}!</div>
    </div>
  );
};

export async function getServerSideProps(ctx: NextPageContext) {
  const query = {
    name: ctx.query.name || null,
  };
  return { props: { query } };
}

export default Home;

next에서 NextPage,NextPageContext를 import해서 Home과 getServerSideProps 에서 사용하였다.

서버사이드렌더링으로 Nest로부터 name 값을 query로 가져왔고 Home 에 props로 넘겨주었다.

 

이 때 Home은 next에서 import되었던 NextPage의 타입을 따르며 파라미터는 interface로 선언했던 Props를 따른다.

 

 

about.tsx 파일은 나중에 클라이언트 측 API요청을 할 때 설명하겠다.

 

 

pages/views/blog/index.tsx

import * as React from 'react';
import { NextPage, NextPageContext } from 'next';
import { IPost } from '../../../types';
import PostPreview from '../../../components/post-preview';
import { BlogService } from '../../../src/blog/blog.service';

interface Props {
  posts: IPost[];
  source: string;
}

const Blog: NextPage<Props> = ({ posts, source }) => {
  return (
    <div>
      <h1>blog</h1>
      <div>
        {posts.map((post) => (
          <PostPreview key={post.slug} post={post} />
        ))}
      </div>
      <div style={{ fontStyle: 'italic', fontSize: 14 }}>
        this page was rendered on the {source}
      </div>
    </div>
  );
};

export async function getServerSideProps(ctx: NextPageContext) {
  const props: Props = {
    source: 'server',
    posts: ctx.query.posts as any,
  };

  if (!Array.isArray(props.posts)) {
    const service = new BlogService();
    props.posts = service.all();
    props.source = 'client';
  }

  return { props };
}

export default Blog;

 

Nest에서 우린 '/blog'에 'blog'를 렌더링해주었다. 때문에 '/blog'경로로 접속하면 blog/index.tsx 파일이 반환된다.

 

 

아까 _app.tsx에서 확인했었던 log를 보면,

 

 

이렇게 Blog 컴포넌트가 잘 들어왔고, pageProps에 Nest에서 준 데이터와 getServerSideProps에서 넘겨준

데이터가 잘 들어온 것을 확인할 수 있다.

 

 

interface Props {
  posts: IPost[];
  source: string;
}

Props 인터페이스를 만들어 타입을 지정했다. 

 

 

export async function getServerSideProps(ctx: NextPageContext) {
  const props: Props = {
    source: 'server',
    posts: ctx.query.posts as any,
  };

  if (!Array.isArray(props.posts)) {
    const service = new BlogService();
    props.posts = service.all();
    props.source = 'client';
  }

  return { props };
}

props 변수를 선언하고, source에는 임의로 'server'를 넣고, posts 쪽에 Nest에서 받은 데이터를 넣어주었다.

 

확인해보니 이 코드에선 Nest에서 보내준 데이터라 getServerSideProps 에 ctx로 잘 못들어오는 것을 확인했다.

if 구문에서 쓰고있는 Service는 Nest의 Service클래스라 사실 쓰지 않는것이 좋다. 

 

굳이 NestJS와 NextJS를 섞어서 개발을 한다면 쓸 수는 있겠지만 서버 인프라를 구축할 때 프론트와 백엔드를 나눠야 할 때를 생각하면 각각 쓰는 메소드가 분리되어야 무조건 좋다. 이런 방법이 있구나 정도만 알고 이렇게 쓰는것보단 axios로 ServerSideProps에서 값을 받아오도록 하자.

 

 

 

 

 

728x90
반응형
728x90
반응형

 

Nest에 관한 기본적인 내용은 블로그 내 Nest.js 카테고리에서 확인 가능하다.

 

main.ts 파일부터 분석해보자.

 

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './application.module';

async function bootstrap() {
  const server = await NestFactory.create(AppModule);

  await server.listen(3000);
}

bootstrap();

 

- AppModule을 import해서 가져오고 NestFactory.create() 메소드로 3000번 포트에 서버를 열어주었다.

 

 

application.module.ts

import { Module } from '@nestjs/common';
import { RenderModule } from 'nest-next';
import Next from 'next';
import { AppController } from './app.controller';
import { BlogController } from './blog/blog.controller';
import { BlogService } from './blog/blog.service';

@Module({
  imports: [
    RenderModule.forRootAsync(
      Next({
        dev: process.env.NODE_ENV !== 'production',
        conf: { useFilesystemPublicRoutes: false },
      }),
    ),
  ],
  controllers: [AppController, BlogController],
  providers: [BlogService],
})
export class AppModule {}

- npm으로 설치했었던 nest-next 모듈에서 RenderModule을 가져와 import해주고, .forRootAsync메소드에 Next를 넣어주었다.

 

 

app.controller.ts

import { Controller, Get, Query, Render } from '@nestjs/common';

@Controller()
export class AppController {
  @Render('home')
  @Get()
  public index(@Query('name') name?: string) {
    return { name };
  }

  @Render('about')
  @Get('/about')
  public about() {
    return {};
  }
}

루트 라우트에서 '/home' 경로와 '/about' 경로에 대한 @Get 데코레이터를 사용했다.

단, 'home'으로 렌더를 하고, @Get데코레이터 파라미터에 아무것도 넣지 않았기 때문에 Next 에서 home파일이 결국 주소상 루트 라우트가 된다.

 

실제로 확인해보면

 

home.tsx

import * as React from 'react';
import { NextPage, NextPageContext } from 'next';

interface Props {
  query: { name?: string };
}

const Home: NextPage<Props> = ({ query }) => {
  const greetName = query.name ? query.name : 'World';

  return (
    <div>
      <div>Hello, {greetName}!</div>
    </div>
  );
};

export async function getServerSideProps(ctx: NextPageContext) {
  const query = {
    name: ctx.query.name || null,
  };
  return { props: { query } };
}

export default Home;

home.tsx파일에서 반환되는 페이지가

 

 

루트경로에 나타나는 것을 확인할 수 있다.

 

home.tsx파일에 보면 name 을 query로 받을 수 있게 코딩되어있는데, @Get데코레이터에 담아서 웹을 실행해보면

 

 

 

이렇게 name이 잘 전달되는 것을 확인할 수 있다. Nest controller에 console.log()를 실행해봐도 좋다.

 

 

blog.contoller.ts

import {
  Controller,
  Get,
  NotFoundException,
  Param,
  Render,
} from '@nestjs/common';
import { BlogService } from './blog.service';

@Controller('/blog')
export class BlogController {
  constructor(private service: BlogService) {}

  @Render('blog')
  @Get()
  public index() {
    return { posts: this.service.all() };
  }

  @Render('blog/[slug]')
  @Get(':slug')
  public get(@Param('slug') slug: string) {
    const post = this.service.find(slug);

    if (post === null) {
      throw new NotFoundException();
    }

    return { post };
  }
}

@Cotroller 데코레이터에 파라미터로 '/blog' 를 넣었기 때문에 '/blog' 경로는 이 BlogController에 들어오게 된다.

 

import { BlogService } from './blog.service';
...
constructor(private service: BlogService) {}
...

BlogService를 import하고 class안에 생성자를 선언해준다.

 

@Render('blog')
  @Get()
  public index() {
    return { posts: this.service.all() };
  }

pages/views/blog/index.tsx페이지에 '/blog' 경로를 주고, service 클래스의 all()메소드로 받아온 posts 내용들을 반환해준다.

 

@Render('blog/[slug]')
  @Get(':slug')
  public get(@Param('slug') slug: string) {
    const post = this.service.find(slug);

    if (post === null) {
      throw new NotFoundException();
    }

    return { post };
  }

pages/views/blog/[slug].tsx 페이지에 '/blog/:slug' 경로를 주고 @Param데코레이터로 slug의 내용을 받아오며 

service에 선언된 find메소드를 사용해서 반환 값이 없을 경우 에러 메세지를 보내주며 있을 경우 post를 반환해준다.

 

 

blog.service.ts

import { Injectable, Inject } from '@nestjs/common';
import { IPost } from '../../types';

const POSTS: Record<string, IPost> = {
  'first-post': {
    title: 'First Post!',
    slug: 'first-post',
    content: [
      'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sed suscipit quam, sit amet feugiat ligula. Nunc sit amet velit vestibulum, mattis orci gravida, aliquam velit. Donec eget lectus nec ipsum suscipit gravida et et odio. Morbi hendrerit dui scelerisque, imperdiet ligula in, ornare risus. Aliquam blandit sem risus, a ornare orci finibus ut. Maecenas interdum lacus arcu, nec finibus nibh semper quis. Vivamus venenatis pharetra ligula, eget semper justo finibus et. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam finibus accumsan elit, et ornare nulla accumsan id. Cras nec leo sed ex egestas malesuada. Nullam a bibendum libero. Cras ullamcorper massa sed odio euismod vulputate. Nullam at ullamcorper dolor. Maecenas et fermentum arcu. Sed interdum nunc neque, eu consectetur ex commodo finibus. Nunc interdum aliquam purus, eu lobortis enim semper et.',
      'Ut sed dolor odio. Mauris cursus aliquet tortor, a posuere mi elementum in. Morbi sed efficitur mauris. Donec sed nulla efficitur, finibus massa ut, aliquet elit. Praesent eu mattis velit. Fusce sodales tincidunt mi, ut placerat turpis lobortis eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nam at scelerisque lacus, ut commodo leo. Morbi vitae iaculis arcu. Donec finibus erat sed tristique feugiat. Morbi lorem tellus, elementum et facilisis eu, egestas fringilla eros. In quis arcu aliquam, ornare nulla malesuada, convallis massa. Donec tellus neque, tempor eu porttitor at, malesuada eget tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque vel pellentesque elit. Morbi semper purus velit, a pulvinar eros blandit vel.',
    ],
  },
  'second-post': {
    title: 'Second Post!',
    slug: 'second-post',
    content: [
      'Nulla sed purus ullamcorper, volutpat leo ac, blandit sem. Aenean efficitur ante rhoncus, lobortis est nec, consequat nisl. Fusce quis semper ligula, eget commodo magna. In tincidunt nisl sed dui ornare, nec pulvinar nibh laoreet. Suspendisse lobortis elit at nunc egestas fermentum. Etiam leo dui, fermentum ac nulla et, hendrerit varius arcu. Quisque porttitor congue mattis. Mauris non lorem suscipit turpis dictum porttitor. Nullam eget blandit felis. Duis eu erat ac mauris egestas placerat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
      'Etiam vel tellus sollicitudin, laoreet quam id, dignissim eros. Suspendisse dapibus tempor magna eget eleifend. Morbi molestie arcu id sagittis tristique. Suspendisse luctus id velit et elementum. Cras gravida sodales quam vel iaculis. Cras aliquet ex a placerat tincidunt. Fusce at ligula urna. Pellentesque id sapien lacus. Nullam eleifend ultrices tortor a hendrerit. Vivamus cursus leo eget tortor porttitor finibus. Quisque at quam gravida, aliquam orci ut, volutpat enim. Vivamus sit amet lobortis lacus. In aliquet consectetur diam vitae lacinia. Suspendisse ultrices malesuada turpis ac congue. Pellentesque vestibulum, nulla nec mollis euismod, sapien ipsum lobortis tortor, nec pellentesque sem nulla gravida diam.',
    ],
  },
};

export class BlogService {
  public all(): IPost[] {
    return Object.values(POSTS);
  }

  public find(slug: string): IPost | null {
    return POSTS[slug] || null;
  }
}

 

먼저 IPost를 가져온다. types 파일은 루트경로에서 확인할 수 있다.

import { IPost } from '../../types';

 

types.ts

export interface IPost {
  title: string,
  slug: string,
  content: string[]
}

 

POSTS는 TypeScript의 Recode 형식을 따른다. key값으로 문자열을 가지고, value값으로 IPost를 가진다.

const POSTS: Record<string, IPost> = {

 


TypeScript 유틸리티 클래스에 관한 내용은 다음 링크를 확인하면 된다.

https://medium.com/harrythegreat/typescript-%EC%9C%A0%ED%8B%B8%EB%A6%AC%ED%8B%B0-%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-7ae8a786fb20

 

Typescript 유틸리티 클래스 파헤치기

Utility Types Docs를 중점으로

medium.com


 

export class BlogService {
  public all(): IPost[] {
    return Object.values(POSTS);
  }

  public find(slug: string): IPost | null {
    return POSTS[slug] || null;
  }
}

import 했던 IPost[]의 타입을 가진 POSTSvalue값들을 반환값으로 가지는 all()메소드와,

slug를 파라미터로 가지고 POSTS 배열 중에 slug의 키 값을 가지며 IPost 타입을 가진 value를 반환하는 find() 메소드를 선언했다.

 


/blog

 

 


/blog/first-post

 

 

 

여기까지 NestJS로 이루어진 백엔드를 살펴보았다. 다음 포스트부터는 Next.JS로 이루어진 프론트 쪽을 살펴보겠다.

728x90
반응형
728x90
반응형

https://www.npmjs.com/package/nest-next

 

nest-next

Nestjs module to allow for server side rendering with Nextjs

www.npmjs.com

 

nest-next npm모듈에 관한 내용은 위 사이트를 통해 자세히 알 수 있다.

먼저 아래 github repository에서 git clone을 해준다.

 

https://github.com/kyle-mccarthy/nest-next

 

GitHub - kyle-mccarthy/nest-next: Render Module to add Nextjs support for Nestjs

Render Module to add Nextjs support for Nestjs. Contribute to kyle-mccarthy/nest-next development by creating an account on GitHub.

github.com

 

$ git clone git@github.com:kyle-mccarthy/nest-next.git

 

그럼 아래와 같이 폴더들이 생기는데, 우리는 이 중에서 examples 하위 폴더인 basic만 이용할 것이다.

 

 

우리가 하는 모든 것들은 basic폴더 안에서 이루어질 것이다.

npm-check-updates

pakage들을 최신버전으로 업데이트 해주기 위해 npm-check-updates를 설치하고 업데이트 해준다.

 

$ sudo npm install -g npm-check-updates
$ ncu -u
$ sudo npm i

 

pakage.json

  "scripts": {
    "dev": "nodemon src/main.ts",
    "build:clean": "rimraf .next",
    "build:ui": "npx next build",
    "build:server": "npx tsc --project tsconfig.server.json && npx babel .next/production-server -d .next/production-server --extensions \".js\"",
    "build": "yarn build:clean && yarn build:ui && yarn build:server",
    "start": "cross-env NODE_ENV=production node .next/production-server/src/main.js"
  },

pakages.json 내에 scripts를 보면 우리가 실행할 수 있는 명령어들이 명시되어있다. 

  •  dev : src/main.ts 파일을 nodemon으로 실행한다.
  • build:clean : .next파일을 제거한다.
  • build:ui : next를 빌드해준다.
  • build:server : next 서버측을 빌드해준다.
  • build : 위의 세가지 명령어를 함께 실행해준다.
  • start : 프로젝트를 배포한다.

 

혹시 yarn이 아니라 npm으로 하고싶으면 build부분을 아래와 같이 바꿔준다.

"build": "npm run build:clean && npm run build:ui && npm run build:server",

 

이제 빌드를 해보자.

$ npm run build

 

 

 

아래와 같이 eslint에 에러가 뜬 것을 볼 수 있다.

 

 

ESLint

ESLint 는 ES 와 Lint를 합친것이다.

ES는 Ecma Script로서, Ecma라는 기구에서 만든 Script. 즉 표준 자바스크립트를 의미한다.

Lint는 에러가 있는 코드에 표시를 달아놓는 것을 의미한다.

따라서 ESLint는 자바스크립트 문법에서 에러를 표시해주는 도구를 의미한다.

 

아래 링크에 접속하면 ESLint에 관한 내용이 자세히 나와있다.

https://eslint.org/docs/user-guide/getting-started

 

Getting Started with ESLint

Getting Started with ESLint ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code, with the goal of making code more consistent and avoiding bugs. In many ways, it is similar to JSLint and JSHint with a few exceptio

eslint.org

 

이 프로젝트에서 아직 ESLint 에러를 고치는 방법을 알지 못하였기 때문에 알게되면 포스트를 수정하겠다.

 

웹에서 명령어를 실행하기 전에 pakage.json에서 바꾸어야 할 부분이 있다.

npm run start 부분을 다음과 같이 바꾸어주자.

"start": "npx next start"

 

이제 npm run start 명령어를 실행해주면 웹에서 잘 뜨는 것을 볼 수 있다.

 

 

만약 실행 포트를 바꾸고 싶으면 다음과 같이 바꿀 수 있다.

"start": "sudo npx next start -p 80"

 

포트 변경에 관한 내용은 아래 링크에서 자세히 확인할 수 있다.

https://velog.io/@hojin9622/Next.js-%EC%8B%A4%ED%96%89-%ED%8F%AC%ED%8A%B8-%EB%B3%80%EA%B2%BD-%EB%B0%A9%EB%B2%95

 

Next.js 실행 포트 변경 방법

package.json 파일의 scripts에 -p 옵션을 주고 원하는 포트를 입력한다.PORT=2345 npm run dev 실행 시 2345포트에서 실행된다.맥, 리눅스 계열에서만 사용가능하며, 윈도우에서는 불가능하다.npm i -D cross-envcr

velog.io

 

728x90
반응형

+ Recent posts