사진첩을 만들고 있는데,

그냥 정사각형으로 늘어놓으니.. 

 

정사각 예시

 

너무 안예쁘죠?

그래서 핀터레스트처럼 만들어보기로 했습니다.

핀터레스트에 사용한 레이아웃이름은 masonry layout이라고 하네요.

 

작업에 사용한 이미지들의 출처는 pixabay 입니다.

 

핀터레스트 메인

 

라이브러리로도 있는데,

어떠분이 css로 만들었더라구요!  => darrengwon.tistory.com/569

 

이 분의 코드를 참고해서 react에 적용했습니다.

거의 다 가져왔지만, ㅎㅎ 그 중에서 수정 두가지를 했는데요.

상단에 마진이 생기는 버그? 부분과 hover 시에 흑백으로 보여지는 것을 반대로 수정했습니다.

 

라이브러리 => masonry.desandro.com/

 

Masonry

Install Download CDN Link directly to Masonry files on unpkg. Package managers Install with Bower:  bower install masonry --save Install with npm:  npm install masonry-layout Getting started HTML Include the Masonry .js file in your site. Masonry works o

masonry.desandro.com

 

 

먼저 react에 적용해보기 전에 테스트로 만든 코드입니다.

 

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="wrap"></div>
</body>
</html>

 

 

CSS

.wrap {
  column-count: 4;
  column-gap: 1em;
}

.items {
  display: flex;
  justify-content: center;
  margin-bottom: 1em;
}

.figure {
	display: inline-block;
}

.figure:hover {
	filter: grayscale(0.8);
}

.figure img {
	width: 100%;
}

 

 

JS

const wrap = document.querySelector('.wrap');
const list = [
  'https://cdn.pixabay.com/photo/2020/09/02/20/52/dock-5539524__340.jpg',
  'https://cdn.pixabay.com/photo/2021/02/03/13/54/cupcake-5978060__340.jpg',
  'https://cdn.pixabay.com/photo/2020/05/25/20/14/holland-iris-5220407__340.jpg',
  'https://cdn.pixabay.com/photo/2020/10/08/17/39/waves-5638587__340.jpg',
  'https://cdn.pixabay.com/photo/2019/01/30/11/17/zebra-3964360__340.jpg',
  'https://cdn.pixabay.com/photo/2021/02/01/13/37/cars-5970663__340.png',
  'https://cdn.pixabay.com/photo/2019/06/05/10/34/mimosa-4253396__340.jpg',
  'https://cdn.pixabay.com/photo/2020/08/04/14/42/sky-5463015__340.jpg',
  'https://cdn.pixabay.com/photo/2021/02/03/13/54/cupcake-5978060__340.jpg',
  'https://cdn.pixabay.com/photo/2020/01/09/01/00/the-eye-on-the-greek-4751572__340.png',
  'https://cdn.pixabay.com/photo/2021/01/30/12/19/couple-5963678__340.png',
  'https://cdn.pixabay.com/photo/2021/01/23/07/53/dogs-5941898__340.jpg',
  'https://cdn.pixabay.com/photo/2020/06/15/01/06/sunset-5299957__340.jpg',
];

for (i = 0; i < list.length; i++) {
  const items = document.createElement('div');
  const figure = document.createElement('div');
  const img = document.createElement('img');

  items.classList.add('items');
  figure.classList.add('figure');

  img.src = list[i];

  figure.append(img);
  items.append(figure);

  wrap.append(items);
}

 

 

list 배열로 반복문을 돌려서 돔을 생성해서 append 시켜주는 형태로 테스트 코드를 만들었습니다.

결과물은 아래와 같이 잘 나옵니다.

 

코드 결과물

 

 

예쁘게 잘 나오네요. 굿굿

css에서  wrap에 column-count: 4;의 숫자를 조절하면 가로로 늘어 놓을 사진의 수를 늘릴 수 있습니다.

 

 

자 그럼 react에 적용한 코드입니다.

import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';

const Wrap = styled.div`
    padding-bottom: 6%;
    column-count: 4;
    column-gap: 1em;
`;

const Items = styled.div`
    display: flex;
    justify-content: center;
    margin-bottom: 1em; 
    cursor: pointer;
`;

const Figure = styled.div`
    display: inline-block;
    filter: grayscale(0.8);

    &:hover { 
        filter: none;
    }
`;

const Image = styled.img`
    width: 100%;
`;

const Card = () => {
    const sample = [
        'https://cdn.pixabay.com/photo/2020/09/02/20/52/dock-5539524__340.jpg',
        'https://cdn.pixabay.com/photo/2021/02/03/13/54/cupcake-5978060__340.jpg',
        'https://cdn.pixabay.com/photo/2020/05/25/20/14/holland-iris-5220407__340.jpg',
        'https://cdn.pixabay.com/photo/2020/10/08/17/39/waves-5638587__340.jpg',
        'https://cdn.pixabay.com/photo/2019/01/30/11/17/zebra-3964360__340.jpg',
        'https://cdn.pixabay.com/photo/2021/02/01/13/37/cars-5970663__340.png',
        'https://cdn.pixabay.com/photo/2019/06/05/10/34/mimosa-4253396__340.jpg',
        'https://cdn.pixabay.com/photo/2020/08/04/14/42/sky-5463015__340.jpg',
        'https://cdn.pixabay.com/photo/2021/02/03/13/54/cupcake-5978060__340.jpg',
        'https://cdn.pixabay.com/photo/2020/01/09/01/00/the-eye-on-the-greek-4751572__340.png',
        'https://cdn.pixabay.com/photo/2021/01/30/12/19/couple-5963678__340.png',
        'https://cdn.pixabay.com/photo/2021/01/23/07/53/dogs-5941898__340.jpg',
        'https://cdn.pixabay.com/photo/2020/06/15/01/06/sunset-5299957__340.jpg',
    ];
   
    return (
        <Wrap>
            {sample.map((v, i) => {
                return (
                    <Items key={i}>
                        <Figure>
                            <Image src={v} />
                        </Figure>
                    </Items>
                )
            })}
        </Wrap>
    );
};

export default Card;

 

 

이렇게 해서 완성!

react 코드 결과물

 

 

움직이는 것도 확인해봅시다.

 

react 코드 결과물 gif

 

 

 

customPaging 옵션을 이용한 작업방식입니다.

슬라이드 2개로 페이징을 구현하려면 새로 작성한 글을 참고해주세요!

 

 


 

 

사진첩 작업을 하기위해 react-slick 라이브러리를 활용해보겠습니다.

그냥 slick을 쓴 경험은 있지만 react에서는 강의를 보고 예제 코드 만들어 본 이후 처음이네요.ㅎㅎ

 

 

라이브러리 설치를 먼저 해주시구요.

npm i react-slick

 

 

대충 테스트해보기위해 slick 사이트에 있는 예제를 가져와서 간단히 뚝딱 만들어보겠습니다.

아래 사이트에서 docs를 확인해보세요.

https://react-slick.neostack.com/

 

Neostack

The last react carousel you will ever need

react-slick.neostack.com

 

 

예제코드

확인할때 눈에 보기 쉽게 div에 background를 추가해보았습니다.

import React from 'react';
import Slick from 'react-slick';
import styled from 'styled-components';

const Div = styled.div`
    width: 100%;
    height: 50px;
    background: red;
`;

const Test = () => {
    return (
        <Slick
            dots={true}
            infinite
            speed={500}
            slidesToShow={1}
            slidesToScroll={1}
        >
          <Div>
            <h3>1</h3>
          </Div>
          <Div>
            <h3>2</h3>
          </Div>
          <Div>
            <h3>3</h3>
          </Div>
          <Div>
            <h3>4</h3>
          </Div>
          <Div>
            <h3>5</h3>
          </Div>
          <Div>
            <h3>6</h3>
          </Div>
        </Slick>
    );
};

export default Test;

 

 

그 후 결과물을 보고 당황했습니다;

아래처럼 정상적으로 나오지 않더군요.

뭐가 문제냐..

예시 코드 결과물

 

 

음.. 스타일을 수정해주면 됩니다.

객체들이 정렬될 수 있게 display를 수정해줍니다.

이 상태에서 slick이 부여한 스타일을 덮여씌우려면 글로벌 스타일을 통해 작업이 필요할 것 같습니다.

styled-components에서 제공하는 createGlobalStyle를 이용해서 스타일을 작성해줍니다.

그리고 이 Global 컴포넌트를 사용하면 됩니다.

import styled, { createGlobalStyle } from 'styled-components';

const Global = createGlobalStyle`
  .slick-slide {
	  display: inline-block;
  }
`;

 

 

정상적으로 보이네요.

뭐 지금도 1 다음에 2가 보이긴 하지만요;

저것도 스타일로 해결 가능합니다.

수정된 예시 코드 결과물

 

 

여튼,

간단히 예제코드를 확인했으니 어떻게 하면 되는지 머리에 좀 그려졌습니다.

이제 원하는 슬라이드로 만들기 위해 스타일이나 react-slick의 속성을 사용해 코드를 작성해보겠습니다.

설명은 주석을 통해 작성하겠습니다. 연관된 설명은 번호가 동일합니다.

import React, { useRef } from 'react';
import Slick from 'react-slick';
import styled, { css } from 'styled-components';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';

const Wrap = styled.div`
    position: relative;
    padding-bottom: 70px;
    overflow: hidden;
	
    // 1. Global style 추가했던 것을 슬라이드 상단에 Wrap을 만들어 여기서 선언했습니다.
    .slick-slide {
        display: inline-block;
    }
	
    // 2. 제가 추가한 커스텀 클래스입니다.
    // pagination 부분입니다.
    .slick-dots.slick-thumb {
        position: absolute;
        bottom: 0;
        left: 50%;
        padding: 0;
        margin: 0;
        list-style: none;
        transform: translate(-50%);

        li {
            position: relative;
            display: inline-block;
			
            &.slick-active {
                span {
                    filter: none;
                }
            }
        }
    }  
`;

const SlickItems = styled.div`
    width: 100%;    
    height: 400px;
    text-align: center;

    img {
        max-width: 100%;
        height: 100%;
        vertical-align: top;
    }
`;

const defaultButtonStyle = css`
    position: absolute;
    top: calc(50% - 50px);
    padding: 0;
    width: 30px;
    height: 30px;
    line-height: 1;
    border: none;
    border-radius: 50%;
    background: none;
    outline: none;
    cursor: pointer;
`;

const PrevButton = styled.button`
    ${defaultButtonStyle}
    left: 0;
`;

const NextButton = styled.button`
    ${defaultButtonStyle}
    right: 0;
`;

const defaultIconStyle = css`
    font-size: 22px;
    color: #dedede;

    &:focus,
    &:hover {
        color: #666;
    }
`;

const PrevIcon = styled(LeftOutlined)`
    ${defaultIconStyle}
`;

const NextIcon = styled(RightOutlined)`
    ${defaultIconStyle}
`;

const PagingAnchor = styled.a`
    display: block;
    width: 50px;
    height: 50px;

    img {
        width: 100%;
        height: 100%;
    }
`;

// 3. custom pagination을 만듭니다.
// background를 통해 이미지를 넣어줍니다.
// filter를 통해 흑백으로 보이게 하고 active가 되면 흑백을 제거합니다. (31라인참고)
const Paging = styled.span`
    display: inline-block;
    width: 100%;
    height: 100%;
    vertical-align: middle;
    background: url(${props => props.src})no-repeat;
    background-size: 100% 100%;
    filter: grayscale(1);
`;

// 4. 샘플이미지
const images = [
    {
        src: "https://www.artinsight.co.kr/data/tmp/1910/20191029212614_fawslbwd.jpg",
        title: "1"
    },
    {
        src: "https://www.artinsight.co.kr/data/tmp/1910/20191029212649_esiekzxf.jpg",
        title: "2"
    },
    {
        src: "https://www.artinsight.co.kr/data/tmp/1910/20191029212707_zcrkccgp.jpg",
        title: "3"
    },
    {
        src: "https://www.artinsight.co.kr/data/tmp/1910/20191029212724_pacwfbiz.jpg",
        title: "4"
    },
];

const Slide = () => {
	
    // 5. custom arrows를 만들어 ref를 통해 제어합니다.
    const slickRef = useRef(null);

	// 6. slick에 추가할 세팅입니다.
    const settings = {
        dots: true,
        
        // 2. 제가 추가한 커스텀 클래스입니다. (pagination)
        dotsClass: "slick-dots slick-thumb",
        
        // 5. custom arrows를 만들기 위해 기본 arrows옵션을 false로 합니다.
        arrows: false,
        infinite: true,
        slidesToShow: 1,
        slidesToScroll: 1,
        
        // 2. custom pagination을 만듭니다.
        // i(index)를 통해 샘플이미지에서 동일한 이미지를 가져옵니다.
        customPaging: function(i) {
            const imgSrc = images[i].src;
            return (
                <PagingAnchor>
                    <Paging src={imgSrc} />
                </PagingAnchor>
            );
        },
    };
    
	// 5. custom arrows 동작 함수를 만듭니다.
    const previous = useCallback(() => slickRef.current.slickPrev(), []);
    const next = useCallback(() => slickRef.current.slickNext(), []);

    return (
        <Wrap>
			
			// 5.6.
            // custom arrows를 위해 ref를 설정합니다.
            // 세팅을 넣어줍니다. (공식문서 docs참고)
			<Slick ref={slickRef} {...settings}>
            	
                // 4. 샘플이미지로 반복문을 돌려 슬라이드 아이템을 렌더합니다.
                {images.map((v, i) => {
                    return (
                        <SlickItems key={`${v.title}_${i}`}>
                            <img src={v.src} />
                        </SlickItems>
                    )
                })}
            </Slick>
			
            // 5. custom arrows입니다.
            <>
                <PrevButton onClick={previous}>
                    <PrevIcon />
                    <span className="hidden">이전</span>
                </PrevButton>

                <NextButton onClick={next}>
                    <NextIcon />
                    <span className="hidden">다음</span>
                </NextButton>
            </>
        </Wrap>
    );
};

export default Slide;

이미지 출처는 https://www.artinsight.co.kr/인 것 같습니다. 사실 뉴스 검색해서 사용한 이미지인데, 주소가 저기로 되어있습니다. 문제가되면 수정하겠습니다.

 

 

결과물입니다.

코드 완성 결과물

 

 

아직 간단히 만든 상태라 버그가 존재합니다.

예를들어.. 이미지 개수가 왕창 늘어나면? 아래 pagination은 깨질 것입니다.

그것 말고도 만지다보면 많이 있겠죠.

저는 딱 정해진 갯수의 사진만 쓸거기에 수정하지 않겠지만, 동적으로 이미지 갯수가 달라진다면 수정해야합니다.

 

 

gif

코드 완성 결과물 gif

용량때문에 급하게 누르느라 gif가 엄청빠르네요;;;

 

 

이번시간에 만든 슬라이드는 

화살표, 페이지 번호를 사용자 요구조건으로 수정하는 기능만 적용했습니다.

간단합니다. 

하지만 이 기능 구현하면서 docs를 읽어봤는데, 대부분의 기능은 docs를 보면 만들 수 있을 것 같습니다.(우와)

 

 

 

bug

antd 라이브러리의 card 컴포넌트와 slick을 같이 사용한다면, 버그가 생깁니다.

버그를 해결하기 위해 global에 스타일 하나를 수정해줘야합니다. 

꼭 global일 필요는 없으나 .ant-card-cover를 감싸고 있는 element면 됩니다.

글로벌 스타일 적용방법참고해서 아래 코드를 추가해주세요. 

const Global = createGlobalStyle`
  .ant-card-cover {
	  transform: none !important;
  }
`;

 

 

 

back작업01번에 이후 작업을 진행하겠습니다.

실제 api 요청과 응답 값 받아서 로그인 절차를 진행해보겠습니다.

 

 

Front

 

front 하위에 sagas 폴더를 생성한 후에 관리할 파일명으로 js파일을 생성합니다.

예를 들어 user 정보를 관리할 것이다라고 한다면 user.js가 되겠죠.

저는 user.js로 만들었습니다.

파일명 예시

 

sagas는 리덕스 생태계 패키지입니다.

비동기 작업을 세분화 시킬 수 있습니다.

reducer는 리액트의 상태 생성자입니다. 액션이 오면 리듀서가 스토어의 상태를 변경시키는 방식으로 동작합니다.

reducers와 sagas를 짝으로 사용하기때문에 동일한 파일명으로 작업했습니다.

두 파일모두 확인해보겠습니다.

 

 

sagas/user.js

api 호출 주소는 back의 서버 포트를 입력하면됩니다. (app.js에서 listen하는 포트)

저는 3065이므로 localhost:3065로 접근할 수 있습니다.

import axios from 'axios';
import Router from 'next/router';
import { all, fork, put, takeLatest, delay, call } from 'redux-saga/effects';
import { 
    LOG_IN_ADMIN_REQUEST, LOG_IN_ADMIN_SUCCESS, LOG_IN_ADMIN_FAILURE,
} from '../reducers/user';

function adminLogInAPI(data){
    return axios.post('http://localhost:3065/admin/login', data);
};

function* adminLogIn(action){
    try{
        const result = yield call(adminLogInAPI, action.data);
        
        yield put({
            type: LOG_IN_ADMIN_SUCCESS,
            data: result.data
        });

        yield Router.replace('/');

    }catch(err){
        console.error(err);
        yield put({
            type: LOG_IN_ADMIN_FAILURE,
            error: err.response.data
        })
    }
}

function* watchAdminLogIn(){ 
    yield takeLatest(LOG_IN_ADMIN_REQUEST, adminLogIn);
}

export default function* userSaga(){
    yield all([
        fork(watchAdminLogIn),
    ]);
}

 

 

reducer/user.js

기본 state 정보가 있고 switch 문으로 해당 상태들을 관리하거나 변경 시킵니다.

import produce from '../util/produce';
import Router from 'next/router';

export const initialState = {
    admin: {},
    logInAdminLoading: false,
    logInAdminDone: false,
    logInAdminError: null,
};

export const LOG_IN_ADMIN_REQUEST = 'LOG_IN_ADMIN_REQUEST';
export const LOG_IN_ADMIN_SUCCESS = 'LOG_IN_ADMIN_SUCCESS';
export const LOG_IN_ADMIN_FAILURE = 'LOG_IN_ADMIN_FAILURE';

const reducer = (state = initialState, action) => produce(state,(draft) => {
    switch(action.type){
        case LOG_IN_ADMIN_REQUEST: 
            draft.logInAdminLoading = true;
            draft.logInAdminDone = false;
            draft.logInAdminError = false;
            break;
                
        case LOG_IN_ADMIN_SUCCESS: {
            draft.logInAdminLoading = false;
            draft.logInAdminDone = true;
            draft.logInAdminError = false;
            draft.admin = action.data;

            Router.replace('/');
            break;
        }

        case LOG_IN_ADMIN_FAILURE:
            draft.logInAdminLoading = false;
            draft.logInAdminDone = false;
            draft.logInAdminError = true;

            alert(action.error);
            break;

        default: 
            break;
    }
});

export default reducer;

 

그리고 submit을 하는 컴포넌트가 존재합니다.

 

 

form.js

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { LOG_IN_ADMIN_REQUEST } from '../reducers/user';

import styled from 'styled-components';
import { Button } from 'antd';

const Wrap = styled.div`
    position: absolute;
    padding: 10px;
    width: 300px;
    border-radius: 5px;
    background: #fff;
    box-sizing: border-box;

    button + button {
        margin-left: 10px;
    }
`;

const Input = styled.input`
    padding: 5px;
    margin-bottom: 10px;
    width: 80%;
    border: 1px solid #666;
    border-radius: 3px;
    outline: none;
`;

const AdminButton = styled(Button)`
    padding: 7px 10px;
    height: auto;
    line-height: 1;
    border: 1px solid #666;
    border-radius: 3px;
    background: none;
    outline: none;
    cursor: pointer;

    &:hover, 
    &:focus { 
        color: #666;
        border: 1px solid #666;
        background: none;
    }
`;

const AdminLoginForm = ({ onClose }) => {
    const dispatch = useDispatch();
    const { logInAdminLoading, logInAdminError } = useSelector((state) => state.user);
    const nicknameRef = useRef(null);
    const passwordRef = useRef(null);

    useEffect(() => {
        nicknameRef.current.focus();
    }, []);

    useEffect(() => {
        logInAdminLoading && onClose();
    }, [logInAdminLoading]);

    const onSubmit = useCallback(() => {
        const nickname = nicknameRef.current.value;
        const password = passwordRef.current.value;
        
        if(!nickname || !nickname.trim()) {
            alert('관리자 id를 입력해주세요.');
            nicknameRef.current.focus();
            return;
        }
        
        if(!password || !password.trim()) {
            alert('관리자 password를 입력해주세요.');
            passwordRef.current.focus();
            return;
        }
		
        // 1.
        dispatch({
            type: LOG_IN_ADMIN_REQUEST,
            data: {
                nickname: nickname,
                password: password
            }
        });
    }, []);

    return (
        <Wrap>
            <form>
                <Input 
                    type="text"
                    name="admin-id"
                    placeholder="id"
                    ref={nicknameRef}
                    maxLength={20}
                />
                <Input 
                    type="password"
                    name="admin-password"
                    placeholder="password"
                    ref={passwordRef}
                    maxLength={20}
                />
            </form>
            <AdminButton onClick={onClose}>닫기</AdminButton>
            <AdminButton 
                type="submit"
                loading={logInAdminLoading}
                onClick={onSubmit}
            >
                접속
            </AdminButton>
        </Wrap>
    );
};

export default AdminLoginForm;

 1. dispatch하는 부분이 중요합니다.

LOG_IN_ADMIN_REQUEST가 dispatch되면 reducer와 sagas안의 user.js가 동작합니다.

 

 

 

back

(back_01 블로그에서 sagas에서 api 호출 시 처리할 라우터를 참고해서 만들면되고...)

그 이에 라우터에서 프론트에서 보내온 값을 처리하기 위해서 선행되어야 하는 작업이 있습니다.

 

 

app.js

라우터 연결한 것과 listen한 것보다 위에 작성해야합니다.

미들웨어의 순서는 위에서 아래로 실행되기 때문입니다.

json, urlencoded 예시

두줄의 역할은.. 

프론트에서 보낸 데이터를 해석해서 라우터의 req.body 안에 넣어주는 역할을 합니다.

json은 json 형식의 데이터를 처리하고 urlencoded는 폼 데이터로 넘어왔을때 처리를 해줍니다.

 

데이터가 넘어올때 비밀번호도 있을테니까, 보안을 위해 bcrypt 라이브러리를 사용해보겠습니다.

bcrypt 참고 => www.npmjs.com/package/bcrypt

npm i bcrypt

 

라우터에 적용해보도록 하겠습니다.

가입이라고 예시를 들어보겠습니다.

 

 

routes/admin.js

적용 전

const express = require('express');
const router = express.Router();
const { Admin } = require('../models');

router.post('/signup', async (req, res, next) => { // POST /admin/signup
	
    await Admin.create({
    	nickname: req.body.nickname,
        password: req.body.password    
    });
    
    res.send('ok');
});

module.exports = router;

적용 후 

const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const { Admin } = require('../models');

router.post('/signup', async (req, res, next) => { // POST /admin/signup
	try {
    	const hashedPW = await bcrypt.hash(req.body.password, 12);
    
      await Admin.create({
          nickname: req.body.nickname,
          password: hashedPW    
      });

      res.send('ok');
      
    } catch (error) {
    	console.error(error);
        next(error);
    }
});

module.exports = router;

이때 bcrypt도 비동기이므로 await을 붙여줘야합니다. 

이때 비동기 작업이 있다면 try, catch로 감싸줘야합니다.

그리고 bcrypt.hash는 암호화가 아닌 해쉬화가 되는 것입니다.

첫번째 인자는 변경할 문자열이고 두번째 인자의 숫자는 보안 등급? 같은 것이라고 합니다. 숫자가 높아질수록 해쉬화가 쎄진다고 합니다. 다만 많이 높을 경우 해쉬화하는 시간이 길어져 곤란하니 적당한 값을 찾아넣는 것이 중요하다고 합니다.

(보통 10-13을 많이 넣는다 합니다)

에러가 나면 next를 통해 에러를 보냅니다. next를 통해 에러를 보내면 에러가 한번에 처리된다고 합니다.(express가 알아서 브라우저로 전달한다고..)

 

 

 

이렇게 작업을 하고나서 시도해보면 cors 문제가 발생합니다.

cors 에러

서버에서 서버로 요청 시에는 cors 문제가 발생하지 않지만 브라우저에서 서버로 요청할 경우에는 cors 문제가 발생합니다. 이때 cors를 피해가는 방법들이 있는데, 

 

강의에서 간단히 말해주신 proxy방식은

브라우저에서 프론트 서버로 먼저 요청을 보내고 프론트 서버에서 백엔드 서버로(서버-서버) 요청을 보내는 형식으로하여 cors 문제를 피할 수 있다고 합니다.

거꾸로 응답도 저런식이겠죠?

직접적으로 피해가려면 브라우저에서 백엔드 서버로 요청을 보낼때 header 설정을 해주면 됩니다.

 

cors에 대해 이해할때 차단은 브라우저, 허용은 서버가 해줘야한다는 것을 알고가야합니다.

cors 미들웨어를 통해 해결해줍니다.

npm i cors

 

 

그다음 app.js로 가서 미들웨어를 등록합니다.

cors 등록 예시

 

cors는 보안 정책이어서 아래와 같이 간단하게 작업하지는 않는다고 합니다.

사용자 중에는 해커들도 존재하기 때문이죠.

cors를 전체 허용하면 위험하니 세세하게 잡는 것이 좋다고 합니다.

전체 허용 예시입니다.

app.use(cors({
    origin: '*',
    credentials: false,
}));

 

특정 주소 예시입니다.

app.use(cors({
    origin: 'https://okayoon.com',
    credentials: false,
}));

 

*는 모두다 허용을 뜻하므로 조심해야합니다.

개발 단계여서 *로 개발하고 나중에는 꼭 수정해주세요.

credentials 옵션은 기본 값은 같은 출처내에서만 인증 정보를 사용하겠다는 것입니다.

개발 상태여서 false하지만 true로 해야 같은 출처내에서만 되므로 나중에는 true로 바꿔줘야합니다.

참고 -> evan-moon.github.io/2020/05/21/about-cors/

 

 

잠깐 front로 가서

front/sagas/user.js로 갑니다.

우리가 작업을 할때마다 api 주소에 http://localhost:3065를 등록하기도 번거롭고

배포했을때 저 많은 것을 하나하나 바꾸기에도 어려우니까 베이스 url을 등록해주겠습니다.

베이스url 설정 예시1

 

 

front/sagas/index.js 에 베이스 url를 추가합니다.

베이스url 설정 예시2

axios의 기본 베이스 url을 설정해준 다음에 sagas/user.js에서 localhost 주소를 지웁니다.

베이스url 설정 예시3

주소를 지워도 기존과 같이 http://localhost:3065/admin/login으로 호출이 됩니다.

 

패스포트를 이용해서 로그인 전략을 작성해보겠습니다.

passport는 로그인 기능을 만들기 위한 라이브러리입니다.

페북, 깃헙, 이메일 로그인등을 한번에 관리할 수 있는 라이브러리입니다.

npm i passport passport-local

passport-local은 로컬에서 email이나 id로 로그인할 수 있도록 해주는 것입니다.

설치 후 패스포트 셋팅을 위해서 app.js로 이동합니다.

 

 

back

app.js

app.js에 passport등록 

그 후 back 하위에 passport폴더를 만들고 index.js와 local.js 파일을 생성합니다.

passport 폴더

 

차례로 입력해주겠습니다.

 

 

passport/index.js

const passport = require('passport');
const local = require('./local');

module.exports = () => {
    passport.serializeUser(() => {});
    passport.deserializeUser(() => {});

    local();
};

먼저 큰 틀입니다. 

local()은 local.js에 작성합니다.

 

 

passport/local.js

const passport = require('passport');
// 1.
const { Strategy: LocalStrategy } = require('passport-local');
const { Admin } = require('../models');

module.exports = () => {
    // 2.
    passport.use(
        // 3.
    new LocalStrategy({
        usernameField: 'adminId',
        passwordField: 'password',
        
    // 4.
    }, async (adminId, password, done) => {
        try {
            // 5.
            const user = await Admin.findOne({
                where: { adminId }
            });
            
            // 5.
            if(!user) {
                return done(null, false, { reason: '존재하지않는 사용자입니다.' });
            }
            
            // 6.
            const result = password === user.password;

            if (result) {
                return done(null, user);
            } else {
                return done(null, false, { reason: '비밀번호가 틀렸습니다.' });
            }

        } catch(error) {
            console.error(error);
            return done(error);
        }
    }));
};

이 파일에 로그인 전략을 세우게됩니다.

 

1. 

Strategy 메서드를 passport-local에서 구조분해로 가져오는데 이때 네임을 변경하면서 가져오는 이유는

나중에 로그인 전략이 많이 생기면 구분하기 위함입니다.

구글 로그인, 카카오 로그인 등...

변경하지 않고 사용해도 무방합니다.

 

2. 

passport.use 메서드는 두 개의 인자를 넘길 수 있으며 하나는 객체, 하나는 함수입니다.

 

3.

new LocalStrategy에서 usernameField, passwordField에 해당하는 값은 req.body의 네임과 일치해야합니다.

 

4.

함수에서 전략을 세웁니다.

위의 객체에 넣어준 값이 인자로 옵니다.

 

5.

Admin 모델을 require해ㅇ서 findOne 메서드를 통해 db에서 해당 아이디가 있는지 찾습니다.

passport에서는 응답을 보내지 않고 done으로 결과를 판단해줍니다.

done첫번째 인자는 서버에러, 두번째 인자는 성공, 세번째는 클라이언트 에러를 넣어줍니다.

이것을 통해 routes에서 작업을 이어갑니다.

 

6.

bcrypt.compare를 통해 비밀번호 비교를 합니다.

저는 테스트 때문에 bcrypt를 안써서 작업했는데, 사용 예시는 아래와 같습니다.

const result = await bcrypt.compare(password, user.password)

첫번째 인자와 두번째를 비교합니다.

 

 

routes/admin.js 

로그인 api 호출 작업을 이어갑니다.

const express = require('express');
const router = express.Router();
const passport = require('passport');
const { Admin } = require('../models');

router.post('/login', (req, res, next) => {
    // 1.
    passport.authenticate('local', (err, user, info) => {
       
        if (err) {
            console.error(err);            
            return next(err);
        }

		// 2.
        if (info) {
            return res.status(401).send(info.reason);
        }

       // 3.
        return req.login(user, async (loginErr) => {
            if (loginErr) {
                console.error(error);
                return next(loginErr);
            }

            // 4.
            return res.status(200).json(user);
        });
    
    // 1.
    })(req, res, next);
});


module.exports = router;

 

1. 라우터 내부에 미들웨어 확장 방식으로 작업합니다.

내부에서 req, res, next를 사용하기 위해 맨 마지막에 호출하면서 인자를 전달합니다.

done이 콜백같이 동작해서 authenticate 두번째 인자로 전달됩니다. (네이밍은 상관없습니다.)

err은 서버 에러, user은 사용자 정보, info는 클라이언트 에러 입니다.

이 정보를 통해 if문을 통해 분기합니다.

 

2.

info는 클라이언트 에러입니다.

상태 값을 내려줄 때는 http 상태 코드를 참고하세요.

(10자리나 1자리는 프론트 개발자와 백엔드 개발자간의 약속으로 유하게 변경하여 작업하고는 합니다)

https://developer.mozilla.org/ko/docs/Web/HTTP/Status

 

3.

로그인되었을때 실행될 코드입니다.

login 메서드를 통해 로그인하는 것은 패스포트 로그인입니다.

우리서버 로그인 통과 후에 패스포트 로그인이 실행됩니다.

 

4.

에러가 없다면 사용자 정보를 프론트로 넘겨줍니다.

 

데이터 통신 흐름을 짚어보겠습니다.

로그인 폼에서 로그인 시도 -> saga 데이터 받음 -> 백엔드 서버로 데이터를 보냄 -> 백엔드 라우터에서 req.body로 데이터를 받고 -> authenticate 내부가 실행됨 -> passport 전략으로 진행(local.js) -> 성공 시 authenticate의 콜백으로 감 -> 다른 에러가 없다면 패스포트 login 시도 -> 문제가 없다면 서버 프론트로 응답을 보냅니다.

 

 

쿠키와 세션을 통해 로그인을 실행해보겠습니다.

로그인을 하면 브라우저랑 서바가 같은 정보를 가지고 있어야합니다.

자동으로 되는 것이 아니기때문에..

백엔드 서버에서 로그인이 완료되면 브라우저나 프론트 서버로 정보를 보내주는 작업을 해야합니다.

이때 정보 그대로 보내게 되면 해킹에 취약합니다. (모든 정보를 다 보내도 안됨)

+ 메모리도 너무 과해집니다.

따라서 어떠한 문자열만을 보내고 서로 각각 가지고 있게 됩니다.

브라우저도 가지고 있고 서버들도 각각 가지고 있습니다.(각각 쿠키와 세션을 통해)

그리고 이 문자열을 통해 서로를 연결해주는 역할을 합니다.

(이 문자열로 원하는 정보를 요청해서 따로 가져옵니다)

 

 

쿠키와 세션 라이브러리를 설치합니다.

npm i express-sesstion cookie-parser

 

 

back/app.js

미들웨어들을 추가합니다.

const session = require('express-session');
const cookieParser = require('cookie-parser');
const passport = require('passport');

// ...

app.use(cookieParser());
app.use(session());
app.use(passport.initialize());
app.use(passport.session());

세션, 쿠키 미들웨어등록

 

실제 로그인 시.. back의 라우터 부분입니다.

주석된 코드가 존재하지 않아도 내부적으로 res.setHeader에서 문자열을 보내줍니다(아까 말한 문자열)

그리고 세션과 연결해줍니다.

위의 코드에서 req.login이 동작하게되면 동시에 passport/index.js의 serializeUSer가 실행됩니다.

passport/index.js 코드

 

이때 매칭하는 작업을 하게됩니다.

serializeUser를 통해 id(문자열)을 저장하고 deseializeUser를 통해 복원해서 db에서 정보를 가져옵니다.

 

아래와 같이 수정해줍니다.

const passport = require('passport');
const local = require('./local');
const { Admin } = require('../models');

module.exports = () => {
   // 1.
    passport.serializeUser((user, done) => {
        // 2.
        done(null, user.id);
    });

    // 3.
    passport.deserializeUser(async (id, done) => {
        try {
            const admin = await Admin.findOne({ where: { id } });
            done(null, admin);
        } catch (error) {
            console.error(error);
            done(error);
        }   
    });

    local();
};

 

1. 

req.login을 했을때 동시에 실행되며 user인자는 req.login의 user가 들어온다.

 

2.

user 정보 중에서 쿠키랑 묶어줄 정보만 저장합니다.

첫번째 인자는 서버에러, 두번째 인자는 성공입니다.

 

3. 

복원하는 작업입니다.

db에서 정보를 가져옵니다.

 

수정이 끝났으면 app.js를 실행해보겠습니다. 

npm run dev

// 만약 스크립트 등록을 안했다면
// nodemon app.js

nodemon app.js시 경고문구

 

위와 같이 경고가뜹니다.

따라서 세션 옵션을 설정해줍니다.

 

 

back/app.js

app.js 세션 옵션

// 1.
app.use(cookieParser('secret'));
app.use(session({
    // 2.
    saveUninitialized: false,
    resave: false,
    // 1.
    secret: 'secret',
}));
app.use(passport.initialize());
app.use(passport.session());

1.

쿠키, 세션 간의 통신 중에 랜덤한 문자열을 생성할 기반 문자(보안문자열)를 넣어 줍니다.

테스트여서 secret으로 작성해 넣었는데, 다른걸 작성하세요.

이 문자열은 쿠키파서에 넣은 것과 세션 시크릿에 넣은 문자열이 동일해야합니다.

또한 이 문자열은 보안에 신경써서 보관해야합니다.

만약 이것을 해커가 가져가면 이 문자열을 통해 생성한 랜덤 문자열을 복원할 수 있다고 합니다.

따라서 .env 파일로 옮기겠습니다.

 

2.

세션설정입니다.

 

 

back/.env

COOKIE_SECRET=secret
DB_PASSWORD=secret

저는 그냥 secret으로 작성했는데, 변경해서 사용해야합니다.

키=값 형식으로 작성합니다.

 

그리고 app.js와서 dotenv 작업을 이어갑니다.

dotenv 설정

 

dotenv 문자열로 변경

문자열을 저렇게 변경해 주면 .env 에 등록한 것으로 치환된다고 합니다.

.env 파일 보안에 주의하세요(공개되면 안됨)

 

여기까지 로그인 시도에 대한 것이었습니다.

다음 작업을 이어가야겠습니다.

 

 

--추가 참고할 글--

1. 쿠키를 통해 로그인한 정보를 가지고 있어야한다.

2. 백엔드 서버와 프론트 서버 cors문제가 생기는 것을 이해하고 있다면 

-> 백엔드에서 app.js core에서 credentials를 true로 

-> 프론트에서는 saga index.js에서 axios.default.withCredentials를 true로 변경

(axios 세번째 인자로 withCredentials를 각각 true로 해줘도 되나 번거로우니..)

3. 한가지 제약사항이 생김, credentials 모드일 경우 민감해지니 보안 철저 cors에서 origin에 정확한 url 주소를 적어야한다. (origin: true해도 됨)

 

 

 

이쪽 분야는 거의 갓난아기 수준이라 강의에서 가르쳐준대로 진행했습니다.

(따로 제가 검색해서 개선하거나 추가한 부분이 없다는 의미)

제가 만들고자한 테이블은 강의보다 단순해서리..ㅎㅎ;

 

++

back/app.js에서 라우터 연결할때 첫번째 인자 app.use('/guestbook', GuestbookRouter); 처럼 '/'가 있어야합니다.

++

 

back 폴더를 생성

front 폴더를 만들어 작업한 것처럼 root 폴더 하위에 back 폴더를 생성합니다.

 

back 폴더 예시 이미지

 

front 와는 별개로 서버를 돌리기 때문에 back 폴더에 들어가서 npm init을 해줍니다.

npm init

 

front 서버, back 서버 모두 run 시켜줘야 사이트를 정상 작동시킬 수 있습니다.

express를 통해 라우팅을 진행 할 것이기 때문에 express를 install해주세요.

npm i express

 

 

강의에서 front와 back 서버를 나누어 작업하는 이유에 대해 설명을 해주는데요..

간단히 요약하자면...

서버가 꽉 차게되면 확장을 위해 서버 스케일링 작업을 할 수 있는데,

이때  front와 back 서버가 하나에 존재할 경우 front 서버만 가득차서 확장을 할때에 back도 같이 확장할 수 밖에 없습니다. 이렇게되면 서버가 낭비되는 현상이 발생합니다.

그래서 보통 front와 back의 서버를 나누어 작업을 하고 꽉 찬 서버만 확장 한다고 합니다.

(서버스케일링이란?? https://www.hooni.net/xe/study/95321)

 

 

강의에서 설명해주신다고 대충 그려주신 이미지 입니다.

front 서버만 꽉찼을때 서버가 같이 있다면 같이 확장이 된다는 이미지입니다.ㅋㅋ..

서버스케일링 작업 예시

자, 이제 express가 설치가 되었을테니 다음으로 넘어갑니다.

 

 

라우팅

/back 폴더 하위에 app.js 파일을 생성합니다.

나중에 app.js를 통해 서버를 run 시켜줄 것입니다.

 

 

그리고

back/routes 폴더를 생성해주고

그 하위에 내가 api 통신을 할 주소의 이름으로 파일을 생성해줍니다.

예를 들어보겠습니다.

'아래와 같은 주소들로 호출할 것이다.' 라고 한다면 /routes/user.js 폴더를 생성합니다.

https://example.com/user 
https://example.com/user/nickname
https://example.com/user/post

 

 

저는 방명록으로 호출 할 것이라서 아래와 같이 작업했습니다.

routes/guestbook.js 예시

 

 

이제 생성한 파일들에 내용을 추가해주겠습니다.

app.js

const express = require('express');
const guestbookRouter = require('./routes/guestbook');
const app = express();

app.use('/guestbook', guestbookRouter);

app.listen(3065, () => {
    console.log('서버 실행 중');
});

 

1. express와 routes 하위의 파일들을 require해주시고 routes로 가져온 파일들을 추가해줍니다.

 

이때 import를 안쓰고 require를 사용하는 이유는?

과거에는 import나 export를 사용하려면 변환해주는 과정이 필요했습니다.

하지만 Node.js 13버전부터는 ES6 모듈을 지원해서 설정을 통해 import, export를 사용할 수 있습니다.

import를 사용하면 변환을 통해 require가 되는 점 때문이기도 하고..

기존에 지원안하던 Node.js 버전에서 쭉 사용해왔었기 때문이기도 하고...

뭐 이러한 여러가지 이유에서 require를 사용하는경우가 더 많다고 합니다.

(강의에서는, 그래서 추후에는 달라질 것 같다고 말하셨습니다.)

아래 글 읽어보시면 더 자세히 알수있어요!

Node.js에서 import/export 사용하기 글 바로가기

 

2. app.use를 통해 추가하면되고 첫번째 인자는 접두어이고 두번째는 라우터파일입니다.

이때 'guestbook'으로 안하고 'book'으로한다면 api 호출때 https://example.com/book으로 호출되야하므로 호출할 주소의 이름으로 입력해주세요.

 

3. 그리고 listen을 통해 서버를 run할 것인데 3065는 포트번호입니다. 수정하셔도 상관은 없습니다.

localhost:3065 로 접근가능합니다.

 

 

routes/guestbook.js

const express = require('express');
const router = express.Router();

router.post('/', (req, res) => { // POST /guestbook
    // res.json({ }); 
});

module.exports = router;

 

router.postpost는 rest api의 메소드입니다.

같은 주소로 호출해도 메소드에 따라 구분됩니다.

https://example.com/guestbook -> post일때와 delete일때 라우터를 2개 생성하여 각각 구현합니다.

(methods => www.restapitutorial.com/lessons/httpmethods.html )

 

router.post의 첫번째 인자는 접근할 주소입니다.

즉 app.js에서 app.use('guestbook', ...)으로 했을때 https://example.com/guestbook/이 되고

guestbook.js 파일 내에서 router.post('user' ...)라고 첫번째 인자를 작성했다면 https://example.com/guestbook/user가 됩니다.  

 

두번째 인자로는 요청 값, 응답 값이 옵니다.

 

세번째는 콜백, 응답이 오고나서 res.json을 통해 값을 front로 전달 할 수 있습니다.

 

 

백엔드의 역할은

프론트에서 데이터 요청이 오면 받는다 -> db에서 데이터 꺼내서 가공 -> 프론트로 전달 입니다.

 

DB 셋팅

MySQL을 통해 작업하겠습니다.

1. dev.mysql.com/downloads/ 사이트에서 (혹은 구글 검색해서 들어가세요)

2. MySQL Installer for Windows

3. 저는 아래 큰 파일 다운로드 했습니다.. 이 부분은 위에 작은거 받아도 된다고 하는 사람도있고... 여러가지 방법이있겠지만 저는 localhost 비밀번호 설정이 안나와서 ㅠ 저는 갓난아기라.. 여튼 다운로드 누르면

4. 로그인 가입하기 나오면 왼쪽아래 No thanks, just start my download 클릭!

5. 설치파일 쭉 설치 (다 기본으로 두고 했음) 중간에 비밀번호 입력하는 거 나오는데, 그거 입력하시고 외워두셔야합니다...  

다운로드 참고 -> m.blog.naver.com/bjh7007/221829548634

제로초님 책 참고 링크 https://thebook.io/080229/ch07/

 

 

그리고 다시 /back 폴더에서 터미널을 열어줍니다.

npm i sequelize sequelize-cli mysql2

 

시퀄라이즈는 자바스크립트 문법으로 mysql을 컨트롤 할 수 있게 해주는 라이브러리입니다.

js를 mysql 문법으로 변환해줍니다. 

mysql2는 노드와 mysql을 연결해주는 드라이버 역할을 합니다.

 

 

 

시퀄라이즈 init을 해줍니다.

npx sequelize init

 

init이 끝나면 여러 파일들이 생성됩니다.

config/config.json으로 가서 db 패스워드와 데이터베이스 이름을 수정해주겠습니다.

password는 mysql 다운로드 하실때 등록한 비밀번호입니다.

보안상 위험하기때문에 dotenv를 통해 작업하겠습니다.

먼저 dotenv를 install 해줍니다.

 

npm i dotenv

 

back 폴더 하위에 .env 파일을 생성합니다.

주의할 점은 .env 파일은 공개적인 장소에 업로드하면 절-대 안됩니다.

(비밀번호와 같은 것들을 작성해둘 것이기에..)

 

.env 예시

 

그리고 안에 db 패스워들 작성해줍니다.

저는 예시로 DB_PASSWORD=secret 이라고 문자열을 넣었는데, 본인이 원하는 변수명에 db 패스워드를 넣어주면됩니다.

 

DB_PASSWORD=secret

 

 

그 다음 config.json가서 파일을 수정하겠습니다.

먼저 dotenv를 사용하기 위해 config.json을 config.js로 확장자를 바꿉니다.

그 다음 아래와 같이 작성해줍니다.

process.env.DB_PASSWORD는 .env에서 작성했던 값 입니다.

config.js 예시

 

개발, 테스트, 배포 이렇게 나누어져있는데,

개발이나 테스트할때 필요한 db 정보를 넣고 빼고, 수정하는 작업이 빈번하기 때문 운영(배포) DB와 별도로 둡니다.

database는 데이터베이스 이름입니다. (이 이름으로 최초 db 생성 시 테이블이름이 등록됩니다.)

위와 같이 database_development를 react-okayoon로 수정하고 나중에 db 생성까지 마친다면

아래와 같이 생성됩니다.

 

db 예시

host가 127.0.0.1인 이유는 기본적으로 mysql 호스트 주소입니다.

기본 포트는 3306입니다.

 

 

그리고 이제 모델 설계를 하겠습니다.

back/ 하위에 models 폴더를 생성합니다. (폴더가 기본으로 만들어져있나????)

그리고 models 하위에 index.js 파일을 생성합니다.

기존에 파일 내용을 다 지우고 (= 27라인 위로 다 지웁니다.) 

그러면 아래와 같이 남습니다.

남겨진 코드들

 

 

그리고 

아래 코드처럼 수정합니다.

const Sequelize = require('sequelize');
// db가져올때 개발, 테스트, 배포를 나누어서 작업할 수 있다.
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const db = {};

// 시퀄라이즈랑 노드-mysql을 연결해준다.
// 연결 성공 시 연결 정보다 sequelize에 담긴다.
const sequelize = new Sequelize(config.database, config.username, config.password, config);

Object.keys(db).forEach(modelName => {
    if (db[modelName].associate) {
        db[modelName].associate(db);
    }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

 

이제 모델들을 만들어보겠습니다.

저는 방명록 테이블, 코멘트 테이블, 이미지테이블을 만들겠습니다.

guestbook.js, comment.js, image.js 파일을 생성했습니다.

 

생성파일 예시

 

 

models/guestbook.js

module.exports = (sequelize, DataTypes) => {
    const Guestbook = sequelize.define('Guestbook', { }, { });

    Guestbook.associate = (db) => {};

    return Guestbook;
};

 

먼저 큰틀을 설명하겠습니다.

Guestbook으로 변수 선언합니다. 그리고 sequelize.define의 첫번째 인자는 테이블 이름이 됩니다.

대문자로 작성해주면 db에 등록될때는 소문자, 복수로 등록됩니다.

 

guestbooks 예시

두번째 인자는 컬럼에 대한 설정 값입니다.

세번째 인자는 모델에 대한 설정 값입니다.

 

 

채워보도록 하겠습니다.

models/guestbook.js

module.exports = (sequelize, DataTypes) => {
    const Guestbook = sequelize.define('Guestbook', {
        nickname: {
            type: DataTypes.STRING(20),
            allowNull: false,
        },
        avatar: {
            type: DataTypes.INTEGER(1),
            allowNull: false,
        },
        password: {
            type: DataTypes.STRING(100),
            allowNull: false, 
        },
        superkey: {
            type: DataTypes.STRING(100),
            allowNull: false, 
        },
        content: {
            type: DataTypes.STRING(100),
            allowNull: false, 
        },

    }, {
        charset: 'utf8mb4',
        collate: 'utf8mb4_general_ci',
    });

    Guestbook.associate = (db) => {
        db.Guestbook.hasMany(db.Comment);
        db.Guestbook.hasMany(db.Image);
    };

    return Guestbook;
};

 

두번째 인자에 값들은 내가 어떠한 값들을 받아서 컬럼에 넣겠냐는 의미입니다.

저는 닉네임, 아바타, 패스워드, 수퍼키, 콘텐츠가 필요했습니다.

nickname, avatar, password, superkey, content

이때 내가 등록하지 않아도 자동으로 등록되는 값들이 생깁니다.

컬럼 예시

세번째 인자에서 작업한 모델간의 관계에 의해서도 자동으로 값이 생성됩니다. (GuestbookId)

 

 

그리고 그 컬럼의 오브젝트로 type, allowNull 값을 세팅합니다.

type은 어떤 값으로 받겠느냐는것이고 이 값은 STRING, TEXT, BOOLEAN, INTEGER, FLOAT, DATETIME이 있습니다.

allowNull은 필수 여부인데, false가 필수를 뜻하는 값이라고 합니다.

 

 

세번째 인자는 모델에 대한 설정 값입니다.

charset은 문자와 encoding에 대해, collate는 charset 안에서의 정렬방식이라고 합니다.

참고 => sshkim.tistory.com/128

2개 다 작성해줘야 한글을 작성할 수 있다고하고 에러가 안난다고합니다.

utf8에 mb4가 추가되면 이모티콘 저장 가능합니다.

 

 

Guestbook.associate는 db의 모델간의 관계 정리입니다.

저는 방명록과의 모델 관계에 comment와 image 모델간의 관계만 존재하기 때문에 아래와 같이 했습니다.

db.Guestbook.hasMany(db.Comment);
db.Guestbook.hasMany(db.Image);

hasMany => 방명록이 코멘트 여러개 가질 수 있다.

hasMany => 방명록이 이미지 여러개 가질 수 있다.

다른 것들도 많습니다.

예를 들어 belongTo => 어떠한것이 어떤것에 속해있다...

복잡해지면 사용해야하는 것들이 더 나오기때문에 블로그님 글을 참고해보세요 => velog.io/@cadenzah/sequelize-document-4

 

 

그 후에 models/index.js에서 불러와줘야합니다.

const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const db = {};

const sequelize = new Sequelize(config.database, config.username, config.password, config);

db.Guestbook = require('./guestbook')(sequelize, Sequelize);
db.Comment = require('./comment')(sequelize, Sequelize);
db.Image = require('./image')(sequelize, Sequelize);

Object.keys(db).forEach(modelName => {
    if (db[modelName].associate) {
        db[modelName].associate(db);
    }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

require 한 다음 sequelize, Sequelize 값을 넣어 호출해줍니다. (그래서 변수에 담았)

그리고 그 값을 db.Guestbook 형식으로 db 오브젝트에 넣어 준뒤, 

아래에 있는 Object.keys에서 반복하면서 associate를 실행해줍니다.

이렇게 하면 시퀄라이즈에 모델을 등록한 것입니다.

 

 

이제 express에 시퀄라이즈를 등록합니다.

app.js

 

 

그 다음 db 생성을 위해 명령어 입력!!! 와 끝이다!! 와 시작이다!!

npx sequelize db:create

 

 

서버 실행

node app

 

이렇게 진행한 뒤 MySQL 설치 시 같이 다운로드 받아진 workbench에 들어가서 확인해봅니다.

workbench db생성 예시

 

 

서버 수정 시 자동으로 재실행 시키기

서버는 코드가 변경되어도 재 실행하기 전까지 반영이 안됩니다.

그렇다고 개발자가 끄고 켜고 얼마나 번거롭습니까..

그래서 이것을 실시간으로 반영해주는 nodemon 라이브러리를 사용해보겠습니다.

npm i -D nodemon@2

 

그리고 바로 node app 명령어로 서버 실행시키지 말고 아래 명령어로 실행하면 됩니다.

nodemon app

 

근데 나중에 테스트일때, 개발일때, 배포일때 명령어를 각각 칠거 아니기 때문에

package.json에 스크립트로 등록해줍니다.

package.json 예시

 

이렇게 작성해두면 아래와 같이 nodemon을 통해 서버 실행이 가능합니다.

npm run dev

 

이렇게 하면 수정이 생겼을때 알아서 서버를 재 실행해줍니다.

 

여기까지 db 생성해보기였습니다.

다음 포스팅으로... 추가 작업을 더 진행하겠습니다.

 

+ Recent posts