사이트에 Form을 만들고 이것을 통해 '사이트 주인에게 메일 발송'하는 기능을 작업해보도록하겠습니다.

 

 

EmailJS

- http://www.emailjs.com/

 

Send email from Javascript - no server code required | EmailJS

Send email directly from your client-side Javascript code – no server side code required. Add static or dynamic attachments, dynamic parameters, captcha code and more. Start with our free tier!

www.emailjs.com

- 무료는 텍스트만 가능, 파일전송 X

 

 

 

1. 가입

EmailJS 메인

 

 

 

2. Email Services 페이지

Add New service를 클릭해줍니다.

메일을 발송하고, 받을 수 있는 플랫폼을 선택하고 연동하는 작업입니다.

Email services 페이지

 

 

저는 Gmail를 선택했습니다.

Service ID는 옆에 키 모양 아이콘을 누르면 변경 가능합니다.

(이 key가 공개되면 제 메일을 이용할 수 있을 것이므로, 저는 캡쳐에 나온 키 안썼습니다.)

service 선택 팝업

 

 

Connect Account 눌러서 계정 연동해줍니다.

이때, 팝업이 뜨는데 허용 버튼을 눌러줍니다.

EmailJS 권한 팝업

 

 

안내 문구를 읽어보면 발신자가 본인이 된다는 의미입니다.

허용을 모두 마쳤다면, Create Service 버튼을 클릭합니다.

 

대시보드의 Email Services에 내가 방금 생성한 것이 확인됩니다.

Email Services 대시보드 생성예시

 

 

그리고 연동한 메일에도 관련 Test 메일이 와있습니다.

EmailJS 테스트 메일

 

 

허용 누를때 안내나왔던대로 내가 발신자가 됩니다.

이후, 대시보드에서 보낸 히스토리나 이벤트 같은 것들을 확인할 수 있습니다.

Events에서 방금 테스트메일에 대한 Events를 확인할 수 있습니다.

Events 페이지

 

 

 

3. Email Templates 페이지

Create New Template를 클릭해서 템플릿을 만들어줍니다.

Email Templates 페이지 create 버튼

 

일단, 뭐가 뭔지 모르니 그냥 오른쪽 위에 있는 Save버튼을 눌러줍니다...

 

 

 

4. Integration 페이지

본격적으로 작업을 해보겠습니다.

NPM, Browser, API 버튼을 누르면 각 설명이 적혀있습니다.

 

저는 npm을 통해 작업하겠습니다.

integration 페이지

 

자세한 내용은document을 한번 읽어보세요.

https://www.emailjs.com/docs/sdk/installation/

 

SDK Installation | EmailJS

Guide goes through the various methods used to install EmailJS SDK

www.emailjs.com

 

 

 

5. npm install 및 예제 코드 확인

npm install emailjs-com --save

 

document에 적혀있는 예제

import React from 'react';import emailjs from 'emailjs-com';
import './ContactUs.css';

export default function ContactUs() {
  function sendEmail(e) {
    e.preventDefault();

    emailjs.sendForm('YOUR_SERVICE_ID', 'YOUR_TEMPLATE_ID', e.target, 'YOUR_USER_ID')
      .then((result) => {
          console.log(result.text);
      }, (error) => {
          console.log(error.text);
      });
  }

  return (
    <form className="contact-form" onSubmit={sendEmail}>
      <input type="hidden" name="contact_number" />
      <label>Name</label>
      <input type="text" name="user_name" />
      <label>Email</label>
      <input type="email" name="user_email" />
      <label>Message</label>
      <textarea name="message" />
      <input type="submit" value="Send" />
    </form>
  );
  }

 

이 부분이 주의깊게 봐야하는 부분입니다.

 emailjs.sendForm('YOUR_SERVICE_ID', 'YOUR_TEMPLATE_ID', e.target, 'YOUR_USER_ID')
   .then((result) => {
   	console.log(result.text);
    
   }, (error) => {
   	console.log(error.text);
    
   });
 }

sendForm 메서드에는 3개의 id와 함께 form을 넘겨줘야합니다.

(form data가 아니라 form 엘리먼트입니다.)

 

 

3개의 ID를 발급 받을 수 위치입니다.

  • YOUR_SERVICE_ID : Email Services 페이지에서 발급
  • YOUR_TEMPLATE_ID : Email Templates 페이지에서 발급
  • YOUR_USER_ID : Integration 페이지에서 API Keys에 User ID 확인

 

 

 

6. 코드

Email.JS

import React, { useCallback } from 'react';
import useInput from '../hooks/useInput';
import emailjs from 'emailjs-com';
import styled from 'styled-components';

const Form = styled.form`   
    margin: 0 auto;
    padding: 20px;  
    width: 400px;
    background: #ccc;
    text-align: center;

    input {
        padding: 5px 10px;
        width: 100%;
        border: 1px solid #666;

        & + input,
        & + textarea {
            margin-top: 20px;
        }
    }

    textarea {
        padding: 5px 10px;
        width: 100%;
        min-height: 100px;
        outline: none;
        resize: none;
        IME-MODE: auto;
    }

    button {
        margin-top: 20px;
        padding: 5px 15px;
        background: #fff;
        border: 1px solid #666;
        outline: none;
        cursor: pointer;
    }
`;

const Email = () => {
    const [name, onChangeName] = useInput('');
    const [email, onChangeEmail] = useInput('');
    const [text, onChangeText] = useInput('');

    const onSubmit = useCallback((e) => {
        e.preventDefault();

        const inputNum = e.target.childElementCount - 1; // [D] 버튼한개 제외
        const data = new FormData(e.target);
        const entries = data.entries();
        let failNum = 0;

        for (let i = 0; i < inputNum; i++) {
            const next = entries.next();
            const key = next.value[0];
            const value = next.value[1];

            if (!value) {
                failNum++;
                alert(`${key} 비어있습니다.`);
                break;
            }
        }

        if (!failNum) {
            emailjs.sendForm(
            	'YOUR_SERVICE_ID', 
                'YOUR_TEMPLATE_ID', 
                e.target, 
                'YOUR_USER_ID'
                
            ).then((result) => {
                console.log('result.text', result.text);

            }, (error) => {
                console.log(error.text);
            });
        }
    }, []);

    return (
        <Form onSubmit={onSubmit}>
            <input
                type="text"
                name="name"
                placeholder="이름"
                value={name}
                onChange={onChangeName}
            />

            <input
                type="text"
                name="email"
                placeholder="이메일"
                value={email}
                onChange={onChangeEmail}
            />

            <textarea
                name="text"
                placeholder="메세지"
                value={text}
                onChange={onChangeText}
            />

            <button type="submit">
                발송
            </button>
        </Form>
    );
};

export default Email;

 

 

 

useInput

커스텀 훅입니다.

import { useState, useCallback } from 'react';

export default (initialValue = null) => {
    const [value, setValue] = useState(initialValue);
    const handler = useCallback(({target}) => {
        setValue(target.value);
    }, []);

    return [value, handler, setValue];
};

 

 

 

7. 템플릿 수정 (EmailJS 대시보드 Email Templates 페이지)

코드 작업이 끝났으니 초반에 만들었던 Email Templates를 수정해줍니다.

input의 name값과 맞춰서 key 값을 삽입해주면 됩니다.

 

 

수정 아이콘을 눌러서 수정페이지로 이동합니다.

Email Tempalte 수정 방법

 

 

제목, 컨텐츠 모두 수정해주었습니다.

수정을 완료하였다면 Save를 눌러주세요.

Template 수정 페이지

 

 

 

8. 메일 발송 확인

이제 폼을 작성하여 발송을 눌러봅니다.

예시 폼

 

그 후 내가 서비스로 선택한 플랫폼의 메일함을 보면 메일이 도착합니다.

 

emailJS 템플릿 테스트 메일

 

 

내용도 수정된 것이 확인됩니다.

정상적으로 동작ㅎㅎ!

emailJS 템플릿 테스트 메일 컨텐츠

 

 

 

지난 포스팅에서 만들었던 슬라이드를 수정하게되었습니다.

포스팅에서도 말했었는데,

기존 방식은 지정된 갯수의 이미지만으로 작업을 한 부분이었기에 문제가 없는데,

이미지 갯수가 동적으로 들어오게되면 스타일이 깨지게됩니다.

그래서 실제로.. 작업하다가 이미지 갯수를 늘리려하니 깨지게되고 이런게 너무 거슬려서 수정하게되었습니다.

 

다만 지난 포스팅과는 방식이 다릅니다.

지난 포스팅은 customPaging 옵션 방식으로 제작한 것이고

이번엔 AsNavFor 방식으로 제작하게되었습니다.

 

 

AsNavFor 옵션 example 확인해보기

react-slick.neostack.com/docs/example/as-nav-for

 

Neostack

The last react carousel you will ever need

react-slick.neostack.com

 

 

끊어서 코드를 확인해보겠습니다.

import 

import React, { useCallback, useEffect, useRef, useState } from 'react';
import Slick from 'react-slick';
import styled, { css } from 'styled-components';

import { LeftOutlined, RightOutlined } from '@ant-design/icons';

Slick이 필수나머지는 꾸미기 위해 import해온 부분들입니다.

 

 

Style

const Wrap = styled.div`
    overflow: hidden;

    & > div + div {
        margin-top: 20px;
    }
`;

const Inner = styled.div`
    position: relative;

    .paging_items {
        filter: grayscale(1);

        &:hover {
            filter: none;
        }
    }

    .slick-current .paging_items {
        filter: none;
    }
`;

const defaultItemStyle = css`
    width: 100%;    
    text-align: center;

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

const MainSlickItems = styled.div`
    ${defaultItemStyle}    
    height: 350px;

    img {
        max-width: 100%;
    }
`;

const PagingItems = styled.div`
    ${defaultItemStyle}    
    height: 80px;
    cursor: pointer;
    
    img {
        width: 100%;
    }
`;

const defaultButtonStyle = css`
    position: absolute;
    top: 50%;
    padding: 0;
    width: 30px;
    height: 30px;
    line-height: 1;
    border: none;
    border-radius: 50%;
    background: none;
    outline: none;
    transform:translateY(-50%);
    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}
`;

스타일은 꾸미기 나름이고 여기서 설명할 부분은 슬릭이 부여하는 클래스들인데요. 

선택된 아이템에는 slick-current가 노출되고 있는 아이템들에는 slick-active가..

그리고 centerMode가 true로 되어있어서 선택 후 가운데에 위치하게되는 객체에 .slick-current 클래스가 붙습니다.

저는 선택된 객체에 스타일을 주기위해 해당 .slick-current 클래스를 사용했고, 

hover를 추가해주기 위해서 Slick을 감싸고 있는 엘리먼트에 스타일을 추가하는 방식으로 진행했습니다.

 

 

Component_1

return 영역을 제외하고 먼저 보겠습니다.

const Slide = ({ images }) => {

	// 1.
    const [mainSlick, setMainSlick] = useState(null);
    const [pagingSlick, setPagingSlick] = useState(null);
    const mainSlickRef = useRef(null);
    const pagingSlickRef = useRef(null);

    useEffect(() => {
        setMainSlick(mainSlickRef.current);
        setPagingSlick(pagingSlickRef.current);
    }, []);
	
    // 2.
    const mainSettings = {
        dots: false,
        arrows: false,
        infinite: true,
        slidesToShow: 1,
        slidesToScroll: 1,
    };
    
	// 2.
    const pagingSettings = {
        dots: false,
        arrows: false,
        centerMode: true,
        slidesToShow: 8,
        swipeToSlide: true,
        focusOnSelect: true,
    };
	
    // 3.
    const onClickPrev = useCallback((ref) => () => ref.current.slickPrev(), []);
    const onClickNext = useCallback((ref) => () => ref.current.slickNext(), []);

    return (
    	//...
    );
};

export default Slide;

1.

ref만 저장해서 작업을 끝내고 싶었지만.. 

state를 사용하지않으면 동작하지않아서 state를 추가했습니다. 

아마 비동기때문인 것 같은데, 좋은 방법이있다면 댓글로 짚어주시면 감사합니다.

컴포넌트 마운트 시에 state 부분에 ref.current를 넣어줍니다.

 

2.

메인이 될 슬라이드와 페이지 번호를 담당할 슬라이드의 셋팅입니다.

 

3.

클릭 함수 하나로 해결하기 위해 대상이 될 ref를 인자로 받아 처리합니다.

 

 

Component_2

return (
  <Wrap>
    <Inner>
      <Slick 
      
      	{/* 1 */}
        ref={mainSlickRef} 
        asNavFor={pagingSlick}
        {...mainSettings}
      >
      
      	{/* 2 */}
        {images.map((v, i) => {
          return (
            <MainSlickItems key={`${v.title}_${i}`}>
            	<img src={v.src} />
            </MainSlickItems>
          )
        })}
      </Slick>

	{/* 3 */}
    <>
      <PrevButton onClick={onClickPrev(mainSlickRef)}>
        <PrevIcon />
      </PrevButton>

      <NextButton onClick={onClickNext(mainSlickRef)}>
        <NextIcon />
      </NextButton>
    </>
    </Inner>

    <Inner>
      <Slick
        ref={pagingSlickRef}
        asNavFor={mainSlick}
        {...pagingSettings}
      >
        {images.map((v, i) => {
          return (
            <PagingItems 
              key={`${v.title}_${i}`}
              className="paging_items"
            >
            	<img src={v.src} />
            </PagingItems>
          )
        })}
      </Slick>

      <>
        <PrevButton onClick={onClickPrev(pagingSlickRef)}>
          <PrevIcon />
        </PrevButton>

        <NextButton onClick={onClickNext(pagingSlickRef)}>
          <NextIcon />
        </NextButton>
      </>
    </Inner>
  </Wrap>
);

1.

ref에 해당 객체를 담아줍니다.

asNavFor에 같이 동작할 객체를 저장한 state를 넣어줍니다.

세팅도 넣어줍니다.

 

2.

슬라이드에 표현할 이미지를 반복시킵니다. (당연히 메인이랑 페이지번호부분이 동일한 이미지여야겠죠)

 

3. 

슬라이드 이전, 다음 버튼입니다.

이 부분에서 ref를 넣어줘야 prev, next 함수 실행 시에 어떤 객체를 대상으로 할 지 설정할 수 있습니다.

 

 

메인 슬라이드와 페이지번호 슬라이드는 매우 비슷합니다.

하지만 스타일이나 조금씩 다른 부분이 있어서 하나로 사용하지 못했습니다.

(따로 컴포넌트로 분리하면 가능할 법도 합니다만...)

 

 

완성코드입니다.

import React, { useCallback, useEffect, useRef, useState } from 'react';
import Slick from 'react-slick';
import styled, { css } from 'styled-components';

import { LeftOutlined, RightOutlined } from '@ant-design/icons';

const Wrap = styled.div`
    overflow: hidden;

    & > div + div {
        margin-top: 20px;
    }
`;

const Inner = styled.div`
    position: relative;

    .paging_items {
        filter: grayscale(1);

        &:hover {
            filter: none;
        }
    }

    .slick-current .paging_items {
        filter: none;
    }
`;

const defaultItemStyle = css`
    width: 100%;    
    text-align: center;

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

const MainSlickItems = styled.div`
    ${defaultItemStyle}    
    height: 350px;

    img {
        max-width: 100%;
    }
`;

const PagingItems = styled.div`
    ${defaultItemStyle}    
    height: 80px;
    cursor: pointer;
    
    img {
        width: 100%;
    }
`;

const defaultButtonStyle = css`
    position: absolute;
    top: 50%;
    padding: 0;
    width: 30px;
    height: 30px;
    line-height: 1;
    border: none;
    border-radius: 50%;
    background: none;
    outline: none;
    transform:translateY(-50%);
    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 Slide = ({ images }) => {
    const [mainSlick, setMainSlick] = useState(null);
    const [pagingSlick, setPagingSlick] = useState(null);
    const mainSlickRef = useRef(null);
    const pagingSlickRef = useRef(null);

    useEffect(() => {
        setMainSlick(mainSlickRef.current);
        setPagingSlick(pagingSlickRef.current);
    }, []);

    const mainSettings = {
        dots: false,
        arrows: false,
        infinite: true,
        slidesToShow: 1,
        slidesToScroll: 1,
    };

    const pagingSettings = {
        dots: false,
        arrows: false,
        centerMode: true,
        slidesToShow: 8,
        swipeToSlide: true,
        focusOnSelect: true,
    };

    const onClickPrev = useCallback((ref) => () => ref.current.slickPrev(), []);
    const onClickNext = useCallback((ref) => () => ref.current.slickNext(), []);

    return (
        <Wrap>
            <Inner>
                <Slick 
                    ref={mainSlickRef} 
                    asNavFor={pagingSlick}
                    {...mainSettings}
                >
                    {images.map((v, i) => {
                        return (
                            <MainSlickItems key={`${v.title}_${i}`}>
                                <img src={v.src} />
                            </MainSlickItems>
                        )
                    })}
                </Slick>

                <>
                    <PrevButton onClick={onClickPrev(mainSlickRef)}>
                        <PrevIcon />
                    </PrevButton>

                    <NextButton onClick={onClickNext(mainSlickRef)}>
                        <NextIcon />
                    </NextButton>
                </>
            </Inner>

            <Inner>
                <Slick
                    ref={pagingSlickRef}
                    asNavFor={mainSlick}
                    {...pagingSettings}
                >
                    {images.map((v, i) => {
                        return (
                            <PagingItems 
                                key={`${v.title}_${i}`}
                                className="paging_items"
                            >
                                <img src={v.src} />
                            </PagingItems>
                        )
                    })}
                </Slick>

                <>
                    <PrevButton onClick={onClickPrev(pagingSlickRef)}>
                        <PrevIcon />
                    </PrevButton>

                    <NextButton onClick={onClickNext(pagingSlickRef)}>
                        <NextIcon />
                    </NextButton>
                </>
            </Inner>
        </Wrap>
    );
};

export default Slide;

 

슬라이드 완성 이미지

이미지 출처: pixabay 

 

 

동작도 확인해보겠습니다.

슬라이드 완성 이미지 gif

 

 

이렇게 하면 많은 이미지로도 깨지지않고 작업할 수 있습니다~

 

 

 

사진첩을 만들고 있는데,

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

 

정사각 예시

 

너무 안예쁘죠?

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

핀터레스트에 사용한 레이아웃이름은 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

 

 

 

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

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

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

 

++

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