백엔드 - 1

송현섭 ·2023년 4월 4일
0

개별공부

목록 보기
23/44

SOP


  • CORS 정책이 도입되기 전부터 있던 정책으로 동일한 출처(Origin, URL)에서의 요청에만 응답을 허용하는 정책
    ex. (Daum 브라우저에서 요청을 보냈다면 Daum 백엔드에서만 요청 응답 허용 가능, Naver 백엔드에서는 요청 허용 X)


  • 이런 정책은 브라우저가 저장하는 사용자의 정보(토큰, 쿠키..등) 관련 데이터의 탈취를 방지하기 위해 존재
    *만약 SOP 같은 출처에 대한 제한 정책이 없다면 해커가 직접 만든 출처로 해킹한 사용자 데이터를 이용해 요청을 보냈을 때, 별 제한 없이 응답이 이루어질 것이고 이로 인해 예기치 못한 피해가 발생할 수 있음!





CORS


  • Cross-Origin Resource Sharing 의 줄임말로 서로 출저가 다르더라도 크로스 되어 요청에 대한 응답이 가능토록 하는 것으로 요청 시 지켜야하는 정책
    ex. (Daum 브라우저에서 요청한 것을 네이버 백엔드에서 응답)


  • 등장배경
    웹이라는 오픈스페이스 환경에서 다른 출처에 있는 리소스를 가져와서 사용하는 것은 굉장히 흔한 일인데 SOP 정책으로는 이런 것이 불가능하기에 특정 조항만 지키면 출처가 다르더라도 요청을 허용하기로 해서 만들어진 것이 [CORS 정책]



  • preflight 을 통해 미리 요청을 보내서 요청이 가능한지 여부를 체크할 수 있음
    *preflight을 보내는 이유 = 혹여나 리소스가 큰 파일을 요청을 통해 보냈는 데 유효하지 않은 요청일 경우 그만큼 리소스가 낭비되는 것을 방지, 즉 요청의 유효성을 먼저 체크해 볼 필요가 있기 때문




  • CORS 는 백엔드가 아닌 브라우저를 보호하기 위해 존재한다


    -기본적으로 웹어플리케이션 같이 웹에서 돌아가는 프로그램들은 외부의 공격에 매우 취약함 (크롬 개발자 도구만 열어봐도 DOM 작성구조, 리소스 출저 등 다양한 정보를 열람 가능)

    -CORS 같은 제재 정책이 없다면 악의적인 목적으로 CSRF 같은 방식을 사용하여 사용자 데이터를 탈취하고, 이를 이용해 부적절한 요청을 보낼 수도 있음




프록시 서버


  • CORS 정책을 위반하는 요청을 할 경우 요청이 거부되는 데 이는 사실 서버가 아닌 브라우저에서 거부한 것

  • 기본적으로 서버는 정책을 위반하는 요청에도 정상적으로 응답을 하지만, 해당 응답을 받은 후 브라우저에서 응답을 분석한 후 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 폐기해 버림
    (즉, 요청 거부는 서버가 아닌 브라우저에서 동작되는 것!)





[이 경우 프록시 서버를 이용해서 요청에 대한 응답을 받아올 수 있음]


ex.
Naver(프론트) => [요청보냄] => Naver(백엔드서버) => [요청보냄] => Daum(백엔드서버)

  • 위와 같이 백엔드로 요청을 보내고 해당 백엔드가 다시 본래 요청할 곳인 백엔드 서버로 요청을 보내 응답을 우회적으로 받아올 수 있음

  • 이 때 대신해서 요청해주는 백엔드 서버를 [Proxy(프록시)서버] 라고 함








DataBase (DB)


DataBase = 데이터를 담아두는 저장소


-DataBase 에 접근해서 무언가를 가져오려면 DB가 실행되어 있어야 하며 해당 DB에 접속을 해서 가져올 수 있음!


-이 때 DB 접속(통신)을 도와주는 툴이 ODM(Object Document Mapping), ORM(Object Relation Mapping)



DataBase 담는 방식


  • SQL 방식

    -데이터들을 엑셀과 비슷한 표에 정리해 두는 방식
    -SQL 방식은 표 사이에 관계성을 부여할 수 있음
    -관계성을 부여할 수 있기에 ORM 툴 사용

    +a) 관계형 데이터베이스(RDB) = 관계성을 부여하는 DB (ex. Oracle, MySQL, Postgres...)




  • NoSQL 방식

    -서류 봉투에 Document를 모아두는 방식
    -NoSQL에서는 서류봉투를 컬렉션(Collection)이라고 부름
    -ODM 툴 사용

    +a) NoSQL 방식 DB = (ex. MongoDB, FireBase, Redis...)






DB 관리 프로그램

  • DB 관리 프로그램은 데이터베이스 안의 데이터를 조금 더 편리하게 조회할 수 있도록 도와주는 프로그램
    *[주의!!] DB 관리 프로그램은 관리형 프로그램이지, 데이터베이스가 아님!







백엔드 서버구축 실습



Node.js에서 TypeScript 실행하기

  • 본래 javascript 는 브라우저에서만 작동 가능

  • javascript를 더 활용하기 위해 브라우저 외에서도 실행이 되도록 할 필요가 있었고, 이를 위해 만들어진 것이 node.js

  • 사전에 node.js 를 설치했다면 터미널에서 node [파일명] 명령어를 입력하여 터미널 상에서 js 파일을 로드할 수 있음


  • 따라서 node는 javascript 실행기이고, 타입스크립트를 실행하기 위해서는 TS 실행 프로그램인 ts-node 의 설치가 필요함




[BUT!] => ts-node 설치 후 해당 명령어로 실행 시 [Command not found] 라는 오류가 발생

  • 기존에 ts-node 설치 경로는 만들어 둔 class_backend 폴더 안에만 존재하기에 컴퓨터 전역에서 해당 명령어를 찾지 못해 발생하는 오류

    [해결책]

    1. node.js 설치 때 처럼 ts-node 를 로컬 컴퓨터에 직접 설치한다 (전역에 설치)







    2. package.json "scripts" 안에 새로운 명령어를 넣고 실행 문구로 "ts-node index.js" 입력
    *(이 경우 실행하면 ts-node 명령어를 로컬 컴퓨터 전역에서 찾는 것이 아닌 class_Backend 폴더 안의 node-modules 에서 찾기 때문에 "command not found" 에러가 발생하지 않음!)







프로젝트와 데이터베이스 연결실습

typeORM 을 사용해 데이터베이스(DB) 와 백엔드 연결 가능



yarn add typeorm  //  typeorm 설치

yarn add pg   //  pg 설치 (typeorm 이 postgres와 연결하기 쉽게 돕는 라이브러리)



1. 데이터베이스(DB) 와 백엔드 연결

// [index.ts]


import { DataSource } from "typeorm";
import { Board } from "./Board.postgres";  //table 만든 파일

const AppDataSource = new DataSource({
type: "postgres",
host: "34.64.244.122", // DB가 있는 컴퓨터의 IP 주소
port: 5014, // DB가 있는 컴퓨터의 port
username: "postgres",
password: "postgres2022",
database: "postgres",
entities: [Board],    // entity가 [ ] 여기 안에 들어감!
synchronize: true, 
logging: true, 
});

AppDataSource.initialize()
.then(() => {
  console.log("연결 성공!");
})
.catch((error) => console.log(error, "연결 실패!"));



+a) entities["./*.postgres.ts"]

-> 해당 위치의 postgres.ts 로 끝나는 모든 파일들을 데이터베이스(DB) 와 연결시켜 주라는 뜻






2. entity 만들기


// [Board.postgres.ts]


// entities 만들어주기 _ 새로운 타입스크립트파일을 만들어 주세요
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";

// @ -> 데코레이터(타입 오알엠에게 테이블임을 알려줍니다. 데코레이터는 함수입니다.)
@Entity() 
	export class Board extends BaseEntity {
		// primaryGenerateColumn: 자동으로 생성되는 번호			
			@primaryGenerateColumn(’increment’) 
			number!: number;
			
			@column({type : “text”})
			wrtier!: string;
		
		  @Column({ type: "text" })
		  title!: string;
		
		  @Column({ type: "text" })
		  contents!: string;

}
  • 여기 작성하는 부분이 테이블 형식이 됨







3. DBeaver 연동

  • 좌측 상단의 플러그 모양 클릭




  • 사용하는 SQL 클릭





  • Host 부분에 백엔드 주소 입력
    *localhost는 내 컴퓨터를 의미, 현재 컴퓨터에 백엔드가 없기에 백엔드로 접속가능하도록 백엔드 주소를 입력





  • test Connection 을 눌러서 정상적으로 연결되는지 확인









백엔드 서버 열어두기



  • 지금까지의 과정은 백엔드와 데이터베이스(DB)를 연결하는 과정이었고, 이제 브라우저에서 API를 요청할 수 있도록 백엔드 서버를 24시간 열어둘 필요가 있음


  • graphql 을 이용해 API를 만들 경우 apollo-server 설치
  • rest-API 를 이용해 API를 만들 경우 express(koa) 사용





1. ApolloServer 세팅

yarn add graphql   // graphql 설치

yarn add apollo-server   // apolloServer 설치





2. 기본 API 틀 생성 및 서버 열어두기

  • 설치 후 API틀을 생성하고 서버를 열어두어 브라우저의 요청을 기다릴 수 있도록 해야 함 (이를 위해선 서버를 생성하고 API를 만들어야 함!)



Apollo 서버 생성을 위해 필요한 두 가지

1. resolver(API)

2. typeDefs(API Docs)









~~~
데이터베이스 연결하는 코드
~~~

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// API Docs 만들기 [playground에서 볼 수 있는 API Docs]
const typeDefs = `#graphql
type Query {
  hello: String
}
`;

// API 만들기 [실제 실행하는 함수 (playground 에서 작성할 때의 방식)]
const resolvers = {
Query: {
		// 해당 api를 요청하면 아래의 함수가 실행됩니다.
  hello: () => 'world',
},
};

// ApolloServer를 생성합니다.
// 위에서 만든 Docs(typeDefs)와 API(resolvers)를 넣어줍니다.
const server = new ApolloServer({
typeDefs,
resolvers,
});

// 서버를 열어두고 접속을 기다립니다.
// 아래 함수의 인자로 server 를 넣으면 생성해 둔 ApolloServer가 실행 됨
startStandaloneServer(server).then(() => {
    console.log(`🚀 GraphQL 서버가 실행되었습니다.`); // 기본 port는 4000입니다.
  });

~~~
데이터베이스 연결하는 코드
~~~







AppDataSource.initialize()
.then(() => {
  console.log("DB 연결 성공!");
  startStandaloneServer(server).then(() => {
    console.log(`🚀 GraphQL 서버가 실행되었습니다.`); // port: 4000
  });
})
.catch((error) => console.log(error, "DB 연결 실패ㅜ"));
  • 이후 프로젝트와 database가 연결되고 난 후 서버가 실행될 수 있도록 startStandaloneServer(server).~~~ 코드의 위치를 위와 같이 옮겨 줌
    *DB와 연결이 우선적으로 되어야 이후 API 요청을 받았을 때 해당 DB에서 데이터를 가져와서 적절한 작업이 가능함으로 DB가 우선적으로 연결된 다음, API 요청을 받는 서버가 실행되어야 함!!





게시판 CRUD 만들기 실습

  //Board.postgres.ts 파일(table 설정 파일)에서 BaseEntity를 입력했는지 확인합니다.

import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
 @Entity() 
	export class Board extends BaseEntity{	
			@PrimaryGeneratedColumn("increment")
		  number!: number;

			@Column({ type: "text" })
		  wrtier!: string;
		
		  @Column({ type: "text" })
		  title!: string;
		
		  @Column({ type: "text" })
		  contents!: string;

}
  • 테이블의 데이터를 삭제, 수정, 추가하는 일이 발생할 수 있음 (CRUD)

  • 이런 기본적인 CRUD 기능을 사용하기 위해서는 table을 설정한 파일에서 BaseEntity 를 사용하고 있는지 우선 확인!






API 만들기 전체 코드

 import { Board } from "./Board.table";
 
 // API 만들기
const resolvers = {
 Query: {
   fetchBoards: async (parent: any, args: any, context: any, info: any) => {
     // 모두 꺼내기
     const result = await Board.find();

     // 한개만 꺼내기
     // const result = await Board.findOne({
     //   where: { number: 3 },
     // });
     return result;
   },
 },

 Mutation: {
   createBoard: async (_: any, args: any) => {
     await Board.insert({
       ...args.createBoardInput,

       // 하나 하나 모두 입력하는 비효율적인 방식
       // writer: args.createBoardInput.writer,
       // title: args.createBoardInput.title,
       // contents: args.createBoardInput.contents,
     });

     return "게시글 등록에 성공했어요!!";
   },

   // updateBoard: async () => {
   //   // 3번 게시글을 영희로 바꿔줘!
   //   await Board.update({ number: 3 }, { writer: "영희" });
   // },

   // deleteBoard: async () => {
   //   await Board.delete({ number: 3 }); // 3번 게시글 삭제해줘!
   //   await Board.update({ number: 3 }, { isDeleted: true }); // 3번 게시글 삭제했다 치자! (소프트삭제) => isDeleted가 초기값인 false 이면? 삭제 안된거, true 이면? 삭제 된거
   //   await Board.update({ number: 3 }, { deletedAt: new Date() }); // 3번 게시글 삭제했다 치자! (소프트삭제) => deletedAt이 초기값인 NULL 이면? 삭제 안된거, new Date() 들어가 있으면? 삭제 된거
   // },
 },
};





[모두 조회하기 API]

  // resolvers

const resolvers = {
  Query: {
    fetchBoards: async () => {
      const result = await Board.find();
      console.log(result);
      return result;
    },
  },
};
  • resolvers 안에 API 작성
  • Board.find() 로 import 해 온 Board(테이블 설정한 파일) 에서 모든 데이터를 찾아서 가져올 수 있음



  
# typeDefs

const typeDefs = `#graphql
  # 객체의 타입을 지정해줍니다.
  type MyBoard {
    writer: String
    number: Int
    title: String
    contents: String
  }
  type Query {
    # // 결과가 여러개이므로 배열에 담아서 보내줍니다.
    # // GraphQL에서는 배열 안의 객체를 [객체]로 표기합니다.
    fetchBoards: [MyBoard]
  }
`;
  • typeDefs 안에는 API-DOCS 를 만들어 줌

  • 이 때 값을 조회(fetch)하는 경우 type 으로 지정해 주고, 값을 입력하는 경우 input 으로 지정해 줌
    *위의 경우 type을 MyBoard로 작성 후 그 값을 fetchBoards 안에 넣어 줌 (코드의 간결화)








[한 개만 조회하기 API]

  
  // resolvers

const resolvers = {
  Query: {
    fetchBoard: async () => {
      const resultOne = await Board.findOne({
        where: { number: 3 },
      });
      return resultOne;
    },
  },
};
  • 한 개만 조회할 때는 findOne( ) 을 사용하고, 조건을 추가해줘야 함
  • where : {} 을 사용해 어떤 값을 가져올지를 정확히 지정







[생성하기 API]


// resolvers

const resolvers = {
Mutation: {
  // parent vs args: 브라우저의 요청은 args로 인자를 받고, 다른 api의 요청은 parent로 인자를 받습니다.
  createBoard: async (parent: any, args: any, context: any, info: any) => {
    await Board.insert({
      /* 1. 연습용(backend-example 방식) */
      // writer: args.writer,
      // title: args.title,
      // contents: args.contents,

      /* 2. 실무용(backend-practice 방식) */
      ...args.createBoardInput,
    });
    return "게시글 등록에 성공했습니다.";
  },
},
};
  • 조회를 제외한 Create, Update, Delete는 Mutation 안에 작성







  
# typeDefs

const typeDefs = `#graphql
  # 인자로 들어가는 객체의 타입은 type이 아닌 input으로 작성합니다.
  input CreateBoardInput {
    writer: String
    title: String
    contents: String
  }
  type Mutation {
    # GraphQL에서는 문자열의 타입을 String으로 입력해주셔야 합니다.
    # 필수값인 경우에는 콜론(:) 앞에 !를 붙여주세요.

    # 1. 연습용(backend-example 방식)
    # createBoard(writer: String, title: String, contents: String): String

    # 2. 실무용(backend-practice 방식)
    createBoard(createBoardInput: CreateBoardInput): String
  }
`;
  • typeDefs는 위와 같이 작성








[수정하기 API]



// resolvers

const resolvers = {
Mutation: {
  updateBoard: async () => {
    // update(조건, 수정할내용)
    // 👇🏻 3번 게시글을 영희로 바꿔줘!
    await Board.update({ number: 3 }, { writer: "영희" });
  },
},
};
  • Mutation 안에 조건, 수정할 내용을 알맞게 작성

  • typeDefs도 마찬가지로 작성








[삭제하기 API]

 // resolvers

const resolvers = {
  Mutation: {
    deleteBoard: async () => {
      // delete(삭제할 조건)
      // 👇🏻 3번 게시글을 삭제해줘!
      await Board.delete({ number: 3 });
    },
  },
};
  • 삭제하는 API 도 제작할 수 있으나 실무에서는 잘 사용 안함! (X)








[삭제하기 API] - 실무용 (Soft Delete)

  • Data를 삭제하면 돌이킬 수 없기에 실무에서는 추후 데이터가 필요한 상황을 대비하여 실제 데이터를 지우는 경우가 거의 없음!!

  • 대신 Soft Delete 라는 방식 사용




Soft Delete

// resolvers

const resolvers = {
Mutation: {
  deleteBoard: async () => {
    await Board.update({ number: 3 }, { deletedAt: new @Column({ type: "timestamp", default: null, nullable: true })
deletedAt?: Date;Date() });
  },
},
};
  • 삭제 여부를 담는 column( deleteAt )을 만들고, update를 이용해 삭제 여부 입력

  • 이 방법을 사용할 경우, 데이터 조회(fetchBoards) 시에 deleteAt 이 NULL 인 행만 조회한다는 조건을 추가해야 함
    *즉 fetch 시에 deleteAt이 null 인 것만 조회하게 하여 마치 삭제가 처리된 것처럼 보이게 하는 것!





  // Board.postgres.ts

import { column, Entity, primaryGeneratedColum, BaseEntity } from "typorm"

 @Entity() 
	export class Board extends BaseEntity{

			~~~
		
			@Column({ type: "timestamp", default: null, nullable: true })
		  deletedAt?: Date;
}
  • typeDefs에 API-DOCS도 만들고, Column 추가
profile
막 발걸음을 뗀 신입

0개의 댓글