ํด๋น ๊ธ์ Jenkins์ Github Webhook์ ์ด์ฉํ CICD ํ์ดํ๋ผ์ธ ๊ตฌ์ฑํ๊ธฐ ์๋ฆฌ์ฆ ์ ๋๋ค. ์์ธํ ์ฌํญ์ ์๋ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!
๋ง์ฝ ํด๋น ์ค์ต ๋ด์ฉ์ ์ฝ๋๊ฐ ๊ถ๊ธํ๋ค๋ฉด ํ๋ก์ ํธ ๊นํ๋ธ ์์ ํ์ธํ ์ ์์ต๋๋ค.
- 1ํธ ์๋ฆฌ์ฆ๋ฅผ ์์ํ๋ฉฐ :: ์ปจํ ์ธ ๊ฐ์์ ๋๊ธฐ
- 2ํธ ํ๋ก ํธ์๋ ๊ฐ๋ฐํ๊ธฐ :: ๋ฆฌ์กํธ์ axios
- 3ํธ ๋ฐฑ์๋ ๊ฐ๋ฐํ๊ธฐ :: SpringBoot ์ h2
- 4ํธ ec2 ์์ฑ ๋ฐ Jenkins ์ค์น :: AWS EC2๋ก Jenkins ์๋ฒ ๋ง๋ค๊ธฐ
- 5ํธ Dockerizing ๋ฐ Nginx ์ค์ :: ๋ฐฐํฌ๋ฅผ ์ํ ๋์ปค ๋น๋์ Nginx์ ๋ฆฌ๋ฒ์ค ํ๋ก์
- 6ํธ ์นํ ์ค์ ํ๊ธฐ :: Github Webhook ์ฐ๋ํ๊ธฐ
- 7ํธ pipeline์ผ๋ก ๋ฐฐํฌํ๊ธฐ :: Jenkins Pipeline Script ์์ฑํ๊ธฐ
์์
- UI ๋ง๋ค๊ธฐ
- ํต์ ๋ก์ง ๊ตฌํํ๊ธฐ
ํ๋ก ํธ์๋ ๊ฐ๋ฐํ๊ธฐ
์ด๋ฒ ํธ์์๋ CICD ํ์ดํ๋ผ์ธ์ ์ํด์ ํ์ํ ํ๋ก ํธ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํด๋ณด๋ ค ํ๋ค.
์ฐ์ ์ฌ์ ์ ์๋ฆฌ์ฆ๋ฅผ ์์ํ๋ฉฐ์์ ์ด์ผ๊ธฐ ํ๋ฏ, ํ๋ก ํธ์๋๋ ๋ฆฌ์กํธ๋ฅผ ์ด์ฉํ๋ ค ๊ตฌ์ฑํ ์์ ์ด๋ค.
๋ง์ฝ ๋ณธ์ธ์ด ๋ฆฌ์กํธ์ ๋ํด์ ์์ง ๋ชปํ๋ค๋ฉด todo-with-cicd github ์์ ์ฝ๋๋ฅผ ๋ณต์ฌํ์ฌ ์ฌ์ฉํด๋ ๋ฌด๋ฐฉํฉ๋๋ค.
์ปจ์ ์ TodoList์ด๋ค.
์ฐ๋ฆฌ๊ฐ ๋ง๋ค UI๋ฅผ ํ์ธํด๋ณด์
๋ค์๊ณผ ๊ฐ์ด ๊ตฌ์ฑ์ด ๋ ๊ฒ์ด๋ฉฐ, Todo List ์ Item ๋ค์ ๋ฐฑ์๋๋ก๋ถํฐ ๋ฐ์์ค๊ณ ์ถ๊ฐํ ์ ์๋๋ก ํ ์์ ์ด๋ค.
UI ๋ง๋ค๊ธฐ
์์ UI ์์ ์ปดํฌ๋ํธ๋ฅผ ๋ถ๋ฆฌ์ํค๋ฉด ๋ค์๊ณผ ๊ฐ์ ๊ฒ์ด๋ค.
์ด 5๊ฐ์ ์ปดํฌ๋ํธ๊ฐ ์๊ณ , ๊ฐ๊ฐ ๋ค์๊ณผ ๊ฐ๋ค.
TodoPresenter.js
: Container-Presenter Pattern ์ Presenter ์ปดํฌ๋ํธTodoInput.js
: ์๋ก์ด Todo ๋ฅผ ์ ๋ ฅํ ์ ์๋ input ์ปดํฌ๋ํธTodoList.js
: Todo Item ๋ค์ด ๋ชจ์ฌ์๋ ์ปดํฌ๋ํธTodoItem.js
: Todo ์ด๋ฆ๊ณผ ์ญ์ ๋ฒํผ์ด ์๋ ์ปดํฌ๋ํธApp.js
: : TodoPresenter์ Container ์ปดํฌ๋ํธ
์ด์ ํ๋ ํ๋ UI๋ฅผ ๊ตฌ์ฑํด๋ณด๋๋ก ํ์.
1. ํ๋ก์ ํธ ์ธํ
๋ฆฌ์กํธ ์ฑ์ ๊ฐ๋ฐํ๊ธฐ ์ํด์ ์ฐ๋ฆฌ๋ facebook ์ด ๋ง๋ boiler plate project ์ธ create-react-app
์ ์ด์ฉํ ๊ฒ์ด๋ค.
ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ ํ๋๋ฅผ ๋ง๋ ๋ค, ๋ค์๊ณผ ๊ฐ์ ์ปค๋งจ๋๋ฅผ ์ ๋ ฅํ์.
์ฐธ๊ณ ๋ก ํด๋น ์ปค๋งจ๋๋ node ๊ฐ ์ค์น๋์ด ์์ด์ผ ๊ฐ๋ฅํ๊ณ npm ์ ํจํค์ง ๊ด๋ฆฌ์๋ก ์ด์ฉํ ๊ฒ์ด๋ค.
// brew
$ brew install node
$ npx create-react-app frontend
// ํน์ create-react-app ์ด global ํ๊ฒ ์ค์น๋์ด์๋ค๋ฉด
$ create-react-app frontend
๊ทธ๋ฆฌ๊ณ ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ npm ๋ชจ๋์ ์ค์นํ ๊ฒ์ธ๋ฐ, /frontend
๋๋ ํ ๋ฆฌ ์๋๋ก ๊ฐ์, ์๋์ ๋ช
๋ น์ด๋ฅผ ์
๋ ฅํ์.
UI๋ styled-components
๋ฅผ ์ด์ฉํ ๊ฒ์ด๊ณ ํต์ ๋ชจ๋๋ก๋ axios
๋ฅผ ์ด์ฉํ ๊ฒ์ด๋ค.
npm install ์ package.json ์ด ์์นํ ๊ฒฝ๋ก์์ ์ํํด์ผ ํ๋ค
$ npm install -y styled-components // ์ปดํฌ๋ํธ ์คํ์ผ๋ง
$ npm install -y axios // js ํต์ ๋ชจ๋
๊ทธ๋ผ ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ์ /frontend
์๋์ public
, src
์ ๊ฐ์ ๋๋ ํ ๋ฆฌ๊ฐ ์๊ธด๋ค.
/src
๋๋ ํ ๋ฆฌ ์๋ ์ ์๋ js ํ์ผ ์ค์์ index.js
์ App.js
๋ฅผ ์ ์ธํ ๋๋จธ์ง๋ฅผ ์ง์ฐ๊ณ index.js
, App.js
์์ ์ญ์ ๋ ํ์ผ๋ค์ ์์กด์ฑ์ ๋ชจ๋ ์์ ์ค๋ค.
2. ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ๋ฐ ํ์ผ ๊ตฌ์กฐ
/src
๋๋ ํ ๋ฆฌ ์๋์์ /components
๋ผ๋ ๋๋ ํ ๋ฆฌ๋ฅผ ์์ฑํ๊ณ ๋ค์๊ณผ ๊ฐ์ ํ์ผ๋ค์ ๋ง๋ค์
- Presenter ์ปดํฌ๋ํธ
/components/TodoInput.js
/components/TodoItem.js
/components/TodoList.js
/components/TodoPresenter.js
- Container ์ปดํฌ๋ํธ
/App.js
- React Dom Render ํ์ผ
/index.js
- ํต์ ๋ชจ๋๊ณผ ๊ด๋ จ๋ ํ์ผ
/util/SERVER.js
/util/service.js
๊ทธ๋ผ ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๊ฐ ๋ ๊ฒ์ด๋ค.
โโโ package-lock.json
โโโ package.json
โโโ public
โโโ src
โ โโโ App.js
โ โโโ components
โ โ โโโ TodoInput.js
โ โ โโโ TodoItem.js
โ โ โโโ TodoList.js
โ โ โโโ TodoPresenter.js
โ โโโ index.js
โ โโโ util
โ โโโ SERVER.js
โ โโโ service.js
โโโ yarn.lock
์ด์ ๊ฐ๊ฐ์ ์ปดํฌ๋ํธ์ UI ๊ทธ๋ฆฌ๊ณ ํต์ ๋ชจ๋์ ๊ฐ๋ฐํด๋ณด์.
3. TodoInput.js ๊ฐ๋ฐํ๊ธฐ
TodoInput ์ ํด์ผํ ์ผ์ ์ ๋ ฅํ๊ณ ํด๋น text๋ฅผ ์๋ฒ๋ก ์ ์ฅ ์์ฒญ์ ๋ณด๋ด๋ ์ปดํฌ๋ํธ์ด๋ค.
์ฐ๋ฆฌ๊ฐ ์์ ๋ง๋ TodoInput.js
ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ์ ์ด๋ณด์
import React, { useState } from "react";
import styled from "styled-components";
const Input = styled.input`
padding: 12px;
border-radius: 4px;
border: 1px solid #dee2e6;
width: 100%;
outline: none;
font-size: 14px;
box-sizing: border-box;
`;
const InputWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
const Button = styled.button`
padding: 12px;
margin: 25px;
border-radius: 4px;
border: 1px solid #dee2e6;
outline: none;
font-size: 14px;
box-sizing: border-box;
cursor: pointer;
transition: 0.5s ease;
:hover {
background-color: #fff;
border-color: #59b1eb;
color: #59b1eb;
}
`;
const TodoInput = ({ addAndSetTodos }) => {
const [text, setText] = useState("");
const handleChangeTextBox = (e) => {
const { value } = e.target;
setText(value);
};
const handleOnKeyPress = (e) => {
if (e.key === "Enter") {
handleOnClickAddButton();
}
};
const handleOnClickAddButton = () => {
if (text === "") {
alert("๊ฐ์ ์
๋ ฅํ์ธ์");
} else {
addAndSetTodos(text);
setText("");
}
};
return (
<>
<InputWrapper>
<Input
placeholder="์๋ก์ด Todo ๋ฅผ ์
๋ ฅํ์ธ์"
value={text}
onChange={handleChangeTextBox}
onKeyPress={handleOnKeyPress}
/>
<Button onClick={handleOnClickAddButton}>์ถ๊ฐ ํ๊ธฐ</Button>
</InputWrapper>
</>
);
};
export default TodoInput;
์ฌ๊ธฐ์ ๋ณด์ด๋ addAndSetTodos
ํจ์๋ ์ถํ TodoPresenter ์ Container ์ธ App.js
์์ ๋ง๋ค์ด์ฃผ๊ณ ๋ด๋ ค์ค ํจ์์ธ๋ฐ, ๋จผ์ ๋ฒํผ์ onClick์ ๋ฐ์ธ๋ฉ ์์ผ์ฃผ๋๋ก ํ์
4. TodoItem.js ๊ฐ๋ฐํ๊ธฐ
TodoItem ์ App.js ์์ ์๋ฒ๋ก ์์ฒญ์ ๋ณด๋ด ๋ฐ์์จ Todo ๊ฐ์ฒด๋ฅผ ๋ฐ์ ํ๋์ Todo Item ์ผ๋ก ๋ณด์ฌ์ง๊ฒ ํ ์ปดํฌ๋ํธ์ด๋ค.
๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํด๋ณด์.
import React from "react";
import styled from "styled-components";
const Container = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin: 14px;
`;
const List = styled.li`
list-style: none;
margin: 0 15px;
`;
const Button = styled.button`
margin: 0 15px;
cursor: pointer;
border-radius: 4px;
border: 1px solid #dee2e6;
outline: none;
font-size: 12px;
box-sizing: border-box;
transition: 0.5s ease;
:hover {
background-color: #fff;
border-color: #59b1eb;
color: #59b1eb;
}
`;
const TodoItem = ({ todo, deleteAndSetTodos }) => {
const { id, content } = todo;
console.log(id);
return (
<Container>
<List>[ {content} ]</List>
<Button onClick={() => deleteAndSetTodos(id)}>์ญ์ </Button>
</Container>
);
};
export default TodoItem;
deleteAndSetTodos๋ ์๋ฒ์๊ฒ ํด๋น TodoItem ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ง๊ณ ์๋ id๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํน์ todo๋ฅผ ์ญ์ ํด๋ฌ๋ผ๋ ์์ฒญ์ ๋ณด๋ผ ๋ ์ฌ์ฉํ๋๋ฐ, ์ด๋ ๋ง์ฐฌ๊ฐ์ง๋ก App.js
์์ ๋ง๋ค์ด์ props ๋ก ๋ด๋ ค์ค ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์ผ๋จ์ ๋ฃ์ด์ฃผ์
5. TodoList.js
App.js ์์ ์ปดํฌ๋ํธ๊ฐ ๋ธ๋ผ์ฐ์ ์ mount ๋ ๋ ์๋ฒ๋ก ์ ์ฅ๋ todo ๋ค์ ๋ชจ๋ ์กฐํํ๋ ์์ฒญ์ ๋ณด๋ธ๋ค.
๊ทธ๋ฌ๋ฉด ์๋ฒ๋ Todo์ ๊ฐ์ฒด ๋ฐฐ์ด์ ๋ฐํํ๊ฒ ๋๋ค.
ํด๋น ๋ฐฐ์ด์ TodoList ์๊ฒ ์ ๋ฌํด์ฃผ๊ฒ ๋๊ณ TodoList๋ ๋ฐฐ์ด์ map ์ผ๋ก ์ํํ๋ฉฐ ๊ฐ๊ฐ์ ๋ฐฐ์ด ์์๋ค์ TodoItem.js ์ Props ๋ก ๋ด๋ ค์ฃผ๋ ์ญํ ์ ์ํํ๋ค.
๋ค์๊ณผ ๊ฐ์ด ๊ตฌ์ฑ์ ํด๋ณด์.
import React from "react";
import TodoItem from "./TodoItem";
const TodoList = ({ todos, deleteAndSetTodos }) => {
return (
<>
{todos.map((todo, index) => (
<TodoItem
key={index}
todo={todo}
deleteAndSetTodos={deleteAndSetTodos}
/>
))}
</>
);
};
export default TodoList;
TodoItem ์์ ์ฌ์ฉํ deleteAndSetTodo
ํจ์๋ ์ญ์ ๋ฐ์์ TodoItem ์ผ๋ก ๋ด๋ ค์ค์ผ ํ๋ค.
6. TodoPresenter.js ๊ฐ๋ฐํ๊ธฐ
์ด์ ๋ชจ๋ UI ์ปดํฌ๋ํธ๋ ๊ตฌ์ฑ์ด ์๋ฃ๋์๊ณ , UI๋ค์ ํ๋๋ก ๋ชจ์์ค Presenter ๋ฅผ ๊ตฌ์ฑํด๋ณด๋๋ก ํ์.
import React from "react";
import styled from "styled-components";
import TodoInput from "./TodoInput";
import TodoList from "./TodoList";
export const Background = styled.div`
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #e9ecef;
`;
export const Container = styled.div`
width: 512px;
height: 768px;
background: white;
border-radius: 16px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04);
margin: 0 auto;
margin-top: 96px;
margin-bottom: 32px;
display: flex;
flex-direction: column;
align-items: center;
`;
export const Title = styled.h1`
font-size: 2rem;
color: #343a40;
`;
const Subtitle = styled.h2`
font-size: 1rem;
color: gray;
padding-bottom: 30px;
width: 100%;
text-align: center;
border-bottom: 1px solid;
`;
const TodoPresenter = ({ todos, addAndSetTodos, deleteAndSetTodos }) => {
return (
<Background>
<Container>
<Title>ํ๋ฒํ Todo List</Title>
<Subtitle>๊ทผ๋ฐ ์ด์ , cicd pipeline ์ ๊ณ๋ค์ธ</Subtitle>
<TodoInput addAndSetTodos={addAndSetTodos} />
<TodoList todos={todos} deleteAndSetTodos={deleteAndSetTodos} />
</Container>
</Background>
);
};
export default TodoPresenter;
๋ง์ฝ ๋ฆฌ์กํธ์ ์์ ์ด ์๊ฑฐ๋ ์ ๋ชจ๋ฅธ๋ค๋ฉด ๊ทธ๋ฅ ๋ณต์ฌ๋ถ์ฌ๋ฃ๊ธฐ๋ฅผ ํด๋ ์ถฉ๋ถํ๋ค!
7. App.js ๊ฐ๋ฐํ๊ธฐ
App.js ์์๋ TodoPresenter ์์ ์ฌ์ฉํ ๋ชจ๋ ํจ์์ state ๋ค ๊ทธ๋ฆฌ๊ณ props ๋ค์ ๊ด๋ฆฌํด์ค์ผ ํ๋ค.
์ฆ, ์ํ์ ๊ดํ ๋ก์ง์ด ๋ค์ด๊ฐ๋ ๋ถ๋ถ์ด๋ค.
๋ค์๊ณผ ๊ฐ์ด ๊ตฌ์ฑํด๋ณด์!
import React, { useEffect, useState } from "react";
import TodoPresenter from "./components/TodoPresenter";
import { fetchTodos, addTodo, deleteTodo } from "./util/service";
const App = () => {
const [todos, setTodos] = useState([]);
const fetchAndSetTodos = async () => {
const data = await fetchTodos();
setTodos(data);
};
const addAndSetTodos = async (todo) => {
const data = await addTodo(todo);
setTodos(todos.concat(data));
};
const deleteAndSetTodos = async (id) => {
const { data: removedTodo } = await deleteTodo(id);
setTodos(todos.filter((todo) => todo.id !== removedTodo));
};
useEffect(() => {
fetchAndSetTodos();
}, []);
return (
<TodoPresenter
todos={todos}
addAndSetTodos={addAndSetTodos}
deleteAndSetTodos={deleteAndSetTodos}
/>
);
};
export default App;
์ด์ UI ๊ฐ๋ฐ์ ์๋ฃ๋์๋ค!
์ด์ ํต์ ๋ก์ง์ ๊ฐ๋ฐํด๋ณด์
8. ํต์ ๋ชจ๋ ๊ฐ๋ฐํ๊ธฐ
์์ ์ฐ๋ฆฌ๋ util ๋๋ ํ ๋ฆฌ ์๋ฆฌ์ ๋ค์ 2๊ฐ์ ํ์ผ์ ๋ง๋ค์๋ค.
- SERVER.js
- service.js
SERVER.js ์์๋ ์๋ฒ์ url ๊ณผ ํต์ ์ ํ์ํ header ๋ฅผ ์ธํ ํด์ค ๊ฒ์ด๊ณ , service.js ์์ ์ค์ ํต์ ํจ์๋ฅผ ๋ง๋ค ๊ฒ์ด๋ค.
๋ค์๊ณผ ๊ฐ์ด ๊ตฌ์ฑํ์
// SERVER.js
import axios from "axios";
export const SERVER = axios.create({
baseURL: "http://127.0.0.1:8080",
headers: {
"Content-Type": "application/json",
},
});
// service.js
import { SERVER } from "./SERVER";
export const fetchTodos = async () => {
const { data } = await SERVER.get("/api/todos");
return data;
};
export const addTodo = async (todo) => {
const { data } = await SERVER.post("/api/todos", JSON.stringify(todo));
return data;
};
export const deleteTodo = async (id) => {
const data = await SERVER.delete("/api/todos/" + id);
return data;
};
์ด๋ ๊ฒ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ด ๋๋ฌ๋ค.
๊ณ์ํด์ ๊ฐ์กฐํ์ง๋ง ์ด ์๋ฆฌ์ฆ๋ ํ๋ก ํธ๋ ๋ฐฑ์ ๊ฐ๋ฐ ๊ณผ์ ์ ์๋ ค์ฃผ๋ ๊ฒ์ด ์๋๋ผ Jenkins๋ฅผ ์ด์ฉํ๋ ๊ฒ์ด ๋ชฉ์ ์ด๊ธฐ ๋๋ฌธ์ ์ฝ๋๋ฅผ ๋ณต๋ถํด๋ ์๋ฌด๋ฐ ์ง์ฅ์ด ์๋ค
๋ง์ฝ ์ฝ๋๊ฐ ์ ๋๋ก ๋์ํ์ง ์๋๋ค๋ฉด github ์ ๋ฐฉ๋ฌธํด์ ํ์ธํ ์๋ ์์ผ๋ ์ฐธ๊ณ ํ์. github ์ฃผ์๋ ์๋จ์ ์๋ค
๋๊ธ