Section 1에서 배운 내용을 총 동원하여 나만의 아고라 스테이츠를 만든다.
그동안 배웠던 것들과 내가 아는 것을 모두 최대한 동원해서 만들어내는 첫 솔로프로젝트이다.
아마 css 부분에서 애를 먹지 않을까 싶은데 이 또한 과정이고 경험이 될 것이라 생각한다 🔥
[✅] 디스커션 나열 기능
[✅] CSS
[✅] 디스커션 추가 기능
[✅] Github Page 배포
[✅] 코드스테이츠 fe-sprint-my-agora-states 리포지토리로 Pull Request
[✅] 현지 시간 적용
[❌] 페이지네이션 기능
[❌] 디스커션 유지 기능
사용하기 편안하고 심플한 UI/UX
페이지네이션, 디스커션 유지 (ToDo로 실행할 예정 💪)
목차
1. 초기 세팅
2. 과정을 기록하기
3. Error note
딱 처음에 내가 받은 기본적인 폼 코드는 다음과 같았다.
어느 정도 갖춰져있지만 나의 손을 기다리는 듯한 느낌이었다 🤩
<!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>My Agora States</title>
<link rel="stylesheet" href="style.css" />
<script
src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.8/purify.js"
integrity="sha512-QaF+0tDlqVmwZaQSc0kImgYmw+Cd66TxA5D9X70I5V9BNSqk6yBTbyqw2VEUsVYV5OTbxw8HD9d45on1wvYv7g=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
</head>
<body>
<main>
<h1>My Agora States</h1>
<section class="form__container">
<form action="" method="get" class="form">
<div class="form__input--wrapper">
<div class="form__input--name">
<label for="name">Enter your name: </label>
<input type="text" name="name" id="name" required />
</div>
<div class="form__input--title">
<label for="title">Enter your title: </label>
<input type="text" name="title" id="title" required />
</div>
<div class="form__textbox">
<label for="story">Your question: </label>
<textarea
id="story"
name="story"
placeholder="질문을 작성하세요"
required
></textarea>
</div>
</div>
<div class="form__submit">
<input type="submit" value="submit" />
</div>
</form>
</section>
<section class="discussion__wrapper">
<ul class="discussions__container">
<li class="discussion__container">
<div class="discussion__avatar--wrapper">
<img
class="discussion__avatar--image"
src="https://avatars.githubusercontent.com/u/12145019?s=64&u=5c97f25ee02d87898457e23c0e61b884241838e3&v=4"
alt="avatar of kimploo"
/>
</div>
<div class="discussion__content">
<h2 class="discussion__title">
<a
href="https://github.com/codestates-seb/agora-states-fe/discussions/6"
>[notice] 좋은 질문하는 법</a
>
</h2>
<div class="discussion__information">
kimploo / 2022-04-22T14:08:33Z
</div>
</div>
<div class="discussion__answered"><p>☑</p></div>
</li>
</ul>
</section>
</main>
</body>
<script src="data.js"></script>
<script src="script.js"></script>
</html>
// index.html을 열어서 agoraStatesDiscussions 배열 요소를 확인하세요.
console.log(agoraStatesDiscussions);
// convertToDiscussion은 아고라 스테이츠 데이터를 DOM으로 바꿔줍니다.
const convertToDiscussion = (obj) => {
const li = document.createElement("li"); // li 요소 생성
li.className = "discussion__container"; // 클래스 이름 지정
const avatarWrapper = document.createElement("div");
avatarWrapper.className = "discussion__avatar--wrapper";
const discussionContent = document.createElement("div");
discussionContent.className = "discussion__content";
const discussionAnswered = document.createElement("div");
discussionAnswered.className = "discussion__answered";
// TODO: 객체 하나에 담긴 정보를 DOM에 적절히 넣어주세요.
li.append(avatarWrapper, discussionContent, discussionAnswered);
return li;
};
// agoraStatesDiscussions 배열의 모든 데이터를 화면에 렌더링하는 함수입니다.
const render = (element) => {
for (let i = 0; i < agoraStatesDiscussions.length; i += 1) {
element.append(convertToDiscussion(agoraStatesDiscussions[i]));
}
return;
};
// ul 요소에 agoraStatesDiscussions 배열의 모든 데이터를 화면에 렌더링합니다.
const ul = document.querySelector("ul.discussions__container");
render(ul);
const agoraStatesDiscussions = [
{
id: "D_kwDOHOApLM4APjJi",
createdAt: "2022-05-16T01:02:17Z",
title: "koans 과제 진행 중 npm install 오류로 인해 정상 작동 되지 않습니다",
url: "https://github.com/codestates-seb/agora-states-fe/discussions/45",
author: "dubipy",
answer: {
id: "DC_kwDOHOApLM4AKg6M",
createdAt: "2022-05-16T02:09:52Z",
url: "https://github.com/codestates-seb/agora-states-fe/discussions/45#discussioncomment-2756236",
author: "Kingsenal",
bodyHTML:
'<p dir="auto">안녕하세요. <a class="user-mention notranslate" data-hovercard-type="user" data-hovercard-url="/users/dubipy/hovercard" data-octo-click="hovercard-link-click" data-octo-dimensions="link_type:self" href="https://github.com/dubipy">@dubipy</a> 님!<br>\n코드스테이츠 교육 엔지니어 권준혁 입니다. <g-emoji class="g-emoji" alias="raised_hands" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f64c.png">🙌</g-emoji></p>\n<p dir="auto">질문 주신 내용은 노드 환경이 구성되어 있지 않기 때문에 발생되는 문제로 확인됩니다.</p>\n<p dir="auto"><code class="notranslate">brew unlink node && brew link node</code></p>\n<p dir="auto">노드를 연결해 보시고 안된다면</p>\n<p dir="auto"><code class="notranslate">brew link --overwrite node</code></p>\n<p dir="auto">이 명령어를 그 다음에도 안된다면 접근권한 문제일 가능성이 큽니다.</p>\n<p dir="auto"><code class="notranslate">$ sudo chmod 776 /usr/local/lib</code> 접근 권한 변경 후<br>\n<code class="notranslate">$ brew link --overwrite node</code> 다시 연결을 해보세요 !</p>\n<p dir="auto">그럼에도 안된다면 다시 한 번 더 질문을 남겨주세요 !</p>\n<p dir="auto">답변이 되셨다면 내용을 간략하게 정리해서 코멘트를 남기고 answered를 마크해주세요 <g-emoji class="g-emoji" alias="white_check_mark" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2705.png">✅</g-emoji><br>\n감사합니다.<g-emoji class="g-emoji" alias="rocket" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png">🚀</g-emoji><br>\n코드스테이츠 교육 엔지니어 권준혁</p>',
avatarUrl: "https://avatars.githubusercontent.com/u/79903256?s=64&v=4",
},
bodyHTML:
'<p dir="auto">--------------- 여기서부터 복사하세요 ---------------</p>\n<p dir="auto">운영 체제: 예) macOS</p>\n<p dir="auto">현재 어떤 챕터/연습문제/과제를 진행 중이고, 어떤 문제에 부딪혔나요?<br>\nPair 과제 / JavaScript Koans</p>\n<p dir="auto">npm install 명령어 입력 시 env: node: No such file or directory 라고 뜹니다</p>\n<p dir="auto">에러 발생하여 아래 명령어 실행 했는데도 불구하고 똑같은 에러가 발생했습니다<br>\nnpm cache clean --force</p>\n<p dir="auto">rm package-lock.json</p>\n<p dir="auto">rm -rf ./node_modules/</p>\n<p dir="auto">npm --verbose install</p>\n<p dir="auto">폴더 자체가 문제가 있다고 생각하여 github에서 다시 fork 후 진행했는데도 같은 에러가 발생했습니다<br>\n리눅스 기초 챕터 때 npm 설치해서 마지막 submit까지는 잘 됐는데 현재 짝수 생성기 폴더도 똑같이 npm install 시 no such file or directory가 발생합니다</p>\n<p dir="auto">에러가 출력된 곳에서, 이유라고 생각하는 부분을 열 줄 이내로 붙여넣기 해 주세요. (잘 모르겠으면 에러라고 생각하는 곳을 넣어주세요)</p>\n<div class="highlight highlight-source-js position-relative overflow-auto" data-snippet-clipboard-copy-content="minjun@dubi fe-sprint-javascript-koans-main % pwd \n/Users/minjun/Documents/fe_frontand_39/fe-sprint-javascript-koans-main\nminjun@dubi fe-sprint-javascript-koans-main % npm install \nenv: node: No such file or directory"><pre><span class="pl-s1">minjun</span>@<span class="pl-s1">dubi</span> <span class="pl-s1">fe</span><span class="pl-c1">-</span><span class="pl-s1">sprint</span><span class="pl-c1">-</span><span class="pl-s1">javascript</span><span class="pl-c1">-</span><span class="pl-s1">koans</span><span class="pl-c1">-</span><span class="pl-s1">main</span> <span class="pl-c1">%</span> <span class="pl-s1">pwd</span> \n<span class="pl-c1">/</span><span class="pl-v">Users</span><span class="pl-c1">/</span><span class="pl-s1">minjun</span><span class="pl-c1">/</span><span class="pl-v">Documents</span><span class="pl-c1">/</span><span class="pl-s1">fe_frontand_39</span><span class="pl-c1">/</span><span class="pl-s1">fe</span><span class="pl-c1">-</span><span class="pl-s1">sprint</span><span class="pl-c1">-</span><span class="pl-s1">javascript</span><span class="pl-c1">-</span><span class="pl-s1">koans</span><span class="pl-c1">-</span><span class="pl-s1">main</span>\n<span class="pl-s1">minjun</span><span class="pl-kos"></span>@<span class="pl-s1">dubi</span> <span class="pl-s1">fe</span><span class="pl-c1">-</span><span class="pl-s1">sprint</span><span class="pl-c1">-</span><span class="pl-s1">javascript</span><span class="pl-c1">-</span><span class="pl-s1">koans</span><span class="pl-c1">-</span><span class="pl-s1">main</span> <span class="pl-c1">%</span> <span class="pl-s1">npm</span> <span class="pl-s1">install</span> \nenv: node: <span class="pl-v">No</span> <span class="pl-s1">such</span> <span class="pl-s1">file</span> <span class="pl-s1">or</span> <span class="pl-s1">directory</span></pre></div>\n<p dir="auto">검색했던 링크가 있다면 첨부해 주세요.<br>\n<a href="https://mia-dahae.tistory.com/89" rel="nofollow">https://mia-dahae.tistory.com/89</a></p>\n<p dir="auto"><a href="https://stackoverflow.com/questions/38143558/npm-install-resulting-in-enoent-no-such-file-or-directory" rel="nofollow">https://stackoverflow.com/questions/38143558/npm-install-resulting-in-enoent-no-such-file-or-directory</a></p>\n<p dir="auto"><a href="https://velog.io/@hn04147/npm-install-%ED%95%A0-%EB%95%8C-tar-ENOENT-no-such-file-or-directory-lstat-%EC%97%90%EB%9F%AC%EB%82%A0-%EA%B2%BD%EC%9A%B0" rel="nofollow">https://velog.io/@hn04147/npm-install-%ED%95%A0-%EB%95%8C-tar-ENOENT-no-such-file-or-directory-lstat-%EC%97%90%EB%9F%AC%EB%82%A0-%EA%B2%BD%EC%9A%B0</a></p>\n<p dir="auto"><a href="https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=chandong83&logNo=221064506346" rel="nofollow">https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=chandong83&logNo=221064506346</a></p>\n<p dir="auto"><a href="https://webisfree.com/2021-07-15/npm-install-%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D-rename-no-such-file-or-directory-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B0%80" rel="nofollow">https://webisfree.com/2021-07-15/npm-install-%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D-rename-no-such-file-or-directory-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B0%80</a></p>\n<p dir="auto"><a href="https://hellowworlds.tistory.com/57" rel="nofollow">https://hellowworlds.tistory.com/57</a></p>',
avatarUrl:
"https://avatars.githubusercontent.com/u/97888923?s=64&u=12b18768cdeebcf358b70051283a3ef57be6a20f&v=4",
},
...
위에서 본 것처럼 처음에 받았던 코드는 정말 쌩 자연이었다.
완전 흰 스케치북을 받은 느낌이었고, 어떻게 그릴까 정말 고민이 많아졌다.
머릿속으로 가장 먼저 든 생각은 일단 CSS를 구현하는 것이었다.
근데 그 전에 Mock 데이터 파일에서 데이터들을 리스트에 나열해줄 필요가 있었다.
현재는 제공받은 데이터이므로 제한이 있고, 업데이트가 크게 필요없을 것 같다고 생각했다.
따라서 script.js 파일을 열었다.
현재 discussion 부분은 html을 보면 알겠지만, 매우 세분화되어 있다.
먼저, 프로필 사진부터 천천히 채워나가기로 했다.
const avatarImg = document.createElement('img');
avatarImg.src = agoraStatesDiscussions[0].avatarUrl;
avatarImg.alt = 'avatar of ' + agoraStatesDiscussions[0].author;
avatarWrapper.append(avatarImg);
순서는 다음과 같았다.
createElement
을 이용하여, img 태그를 만들어준다.
임의의 element에 할당한다. (avatarImg)
element에 정보를 할당해준다. (src, alt)
append
를 이용하여 상위 Class에 포함시킨다.
그런데 아무 생각없이 하다보니, 처음부터 위기를 맛봤다.
모든 데이터의 프로필 사진이 똑같은 사진으로 쫙 나오는 것이다..
이게.. 뭐지?
분명 render
함수 안에서 반복문을 이용해서 모든 데이터 값을 나열시켜주고 있는데..?
고민하고 고민했다.
결국은 src와 alt를 지정할 때, agoraStatesDiscussions[0]로 값을 아예 지정해서 그런건가?
라는 질문에 '유레카' 를 외칠 수 있게 되었다.
정말 쉬운 문제인데도 기본기가 없다는 것을 스스로 다시 한 번 느꼈다.
동시에 문제를 스스로 해결할 때, 알 수 없는 희열(?)이 있었다.
const avatarImg = document.createElement("img");
avatarImg.className = "discussion__avatar--image";
avatarImg.src = obj.avatarUrl;
avatarImg.alt = "avatar of " + obj.author;
avatarWrapper.append(avatarImg);
convertToDiscussion는 매개변수를 obj로 받는다.
따라서 Mock 데이터인 agoraStatesDiscussions[0]를 obj
로 바꿔주고 해결했다.
그 후 글 정보와 답변 여부에 대해서는 코드를 작성하는데 큰 어려움이 없었다.
const convertToDiscussion = (obj) => {
const li = document.createElement("li");
li.className = "discussion__container";
const avatarWrapper = document.createElement("div");
avatarWrapper.className = "discussion__avatar--wrapper";
const discussionContent = document.createElement("div");
discussionContent.className = "discussion__content";
const discussionAnswered = document.createElement("div");
discussionAnswered.className = "discussion__answered";
li.append(avatarWrapper, discussionContent, discussionAnswered);
const avatarImg = document.createElement("img");
avatarImg.className = "discussion__avatar--image";
avatarImg.src = obj.avatarUrl;
avatarImg.alt = "avatar of " + obj.author;
avatarWrapper.append(avatarImg);
const title = document.createElement("h2");
title.className = "discussion__title";
const titleText = document.createElement("a");
titleText.href = obj.url;
titleText.innerText = obj.title;
discussionContent.append(title);
title.append(titleText);
const information = document.createElement("div");
information.className = "discussion__information";
information.innerText = `${obj.author} / ${new Date(
obj.createdAt
).toLocaleTimeString()}`;
discussionContent.append(information);
const answer = document.createElement("p");
answer.innerText = obj.answer ? "✅" : "❌";
discussionAnswered.append(answer);
return li;
};
const render = (element) => {
for (let i = 0; i < agoraStatesDiscussions.length; i += 1) {
element.append(convertToDiscussion(agoraStatesDiscussions[i]));
}
return;
};
const ul = document.querySelector("ul.discussions__container");
render(ul);
포인트가 있다면, 답변 여부 부분에서 삼항연산자를 이용했다.
각 질문에 따른 답변 여부에 따라 텍스트가 다르게 나타나게 했다.
그리고 toLocaleTimeString 메소드를 이용해서 시간을 나타낸 것..?
추가 구현한 기능은 댓글을 작성하는 대로 화면에 바로 띄워주는 기능이다.
const form = document.querySelector("form.form");
const author = document.querySelector("div.form__input--name > input");
const title = document.querySelector("div.form__input--title > input");
const textbox = document.querySelector("div.form__textbox > textarea");
form.addEventListener("submit", (event) => {
// 폼 제출을 하면 완전히 새로운 html 파일을 받아와야 했다. -> 새로고침
event.preventDefault();
const obj = {
id: "unique id",
createdAt: new Date().toISOString(),
title: title.value,
url: "https://github.com/codestates-seb/agora-states-fe/discussions",
author: author.value,
answer: null,
bodyHTML: textbox.value,
avatarUrl:
"https://avatars.githubusercontent.com/u/97888923?s=64&u=12b18768cdeebcf358b70051283a3ef57be6a20f&v=4",
};
// data에 객체 추가
agoraStatesDiscussions.unshift(obj);
// 화면 다 지우고
while (ul.firstChild) {
ul.removeChild(ul.firstChild);
}
// 다시 agoraStatesDiscussions 기반으로 화면에 보여주기 (랜더링)
render(ul);
});
오랜만이다 😚 event.preventDefault()
을 이용하여 submit 이벤트가 발생 시, 새로고침이 되는 것을 막아줬다.
그리고 작성자가 적는 데이터를 obj라고 지정했고, 작성이 되면 Mock 데이터 배열 맨 앞에 obj를 추가시킨다.
그 후, 화면을 전체적으로 한 번 지우고 리랜더링시켜, 데이터가 추가된 결과화면을 나타내준다.
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css");
* {
margin: 0;
padding: 0;
border: 0px;
box-sizing: border-box;
font-family: Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto,
"Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR",
"Malgun Gothic", sans-serif;
}
main {
display: flex;
gap: 8vw;
}
.form__info {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: 40vw;
margin-left: auto;
margin-right: auto;
}
.form__active {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: 40vw;
margin-left: auto;
margin-right: auto;
}
body {
display: flex;
justify-content: center;
align-items: center;
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 0.7),
rgba(255, 255, 255, 0)
),
url("./bg.jpg");
background-repeat: repeat;
background-size: 100%;
margin-left: auto;
margin-right: auto;
}
main h1 {
margin-bottom: 5vh;
margin-left: 3vw;
font-size: 4rem;
text-align: center;
font-style: italic;
text-shadow: 3px 8px 5px rgba(103, 100, 100, 0.2);
}
form {
width: 30vw;
}
.form__container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-top: 10px;
margin-bottom: 5px;
}
.form__input--wrapper {
display: contents;
padding: 20px;
}
#story {
width: 32vw;
height: 15vh;
border-radius: 10px;
padding: 10px;
margin: 10px;
border: 2px solid #0078aa;
}
.form__input--wrapper {
margin: 5px;
margin-left: auto;
margin-right: auto;
}
.form__input--wrapper input {
width: 32vw;
height: 5vh;
font-size: 1.2rem;
margin: 8px;
border-radius: 10px;
border: 2px solid #0078aa;
margin-bottom: 3vh;
}
.form__submit {
margin: 3px;
margin-top: 5vh;
}
.form__submit input {
width: 30vw;
height: 5vh;
border-radius: 10px;
font-size: 1rem;
margin-left: 25px;
margin-right: 0px;
border: 2px solid black;
display: flex;
justify-content: center;
align-items: center;
}
.discussion__wrapper {
overflow-y: scroll;
align-items: baseline;
height: 86vh;
}
.discussion__wrapper::-webkit-scrollbar {
display: none;
}
.discussion__container {
background-color: rgba(245, 245, 220, 0.8);
display: flex;
justify-content: space-between;
width: 40vw;
height: 18vh;
font-size: 1rem;
border-radius: 1.5vw;
margin: 2vh;
padding: 3vh;
}
.discussion__avatar--wrapper {
display: flex;
flex-direction: column;
justify-content: center;
padding: 1rem;
}
.discussion__avatar--image {
width: 6vh;
border-radius: 50%;
}
.discussion__content {
flex: 1 0 0;
padding: 0.8rem;
}
.discussion__title {
font-size: 1.3rem;
margin-bottom: 4vh;
}
.discussion__information {
display: flex;
justify-content: flex-end;
}
.discussion__answered {
display: flex;
flex-direction: column;
justify-content: center;
}
이번 프로젝트는 저번에 눈여겨보던 트렌디한 글씨체인 pretendard를 채택했다 !
확실히 글씨체가 예쁘기도하고 가독성 면에서도 좋은 것 같다 😚
뭔가 주어진 과제는 px로 지정하라고 했지만, 내 생각은 살짝 달랐다.
저번 과제였던 계산기를 브라우저 반응형으로 하기도 했고,
비율을 정하기 쉽다는 느낌을 받아 이번에도 채택했다.
discussion 컴포넌트는 크게 3구간 [ 프로필 / 제목, 작성자 & 작성시간 / 답변여부 ]으로 나뉜다.
따라서 justify-content: space-between
을 이용해서 비율을 조정했고,
제목이 한 줄이면, 왼쪽 정렬이 아니라 자동으로 가운데 정렬이 나타나는 현상을 flex로 해결했다.
그리고 배경 이미지를 삽입시켰는데 제목의 가독성을 방해하는 상황이 발생했다.
이 부분은 고민하다가 linear-gradient
를 이용하여, 그라데이션으로 제목 가독성을 높였고
동시에 투명도 조절까지 하여, discussion 구간 가독성까지 높일 수 있었다 🔥
추가로 소소하게 discussion 구간은 overflow-y: scroll
을 사용하여 화면에 넘어가는
y축 부분은 scroll로 지정해주었고, display: none
으로 가려주었으나 스크롤 바가 보이지 않으면
사용자가 사용하는 기계에 따라 화면에 넘어가는 정보가 스크린에서 가려지는 경우가 있을 수도 있다.
따라서 다음과 같이 가장 하단 컴포넌트의 상단 부분을 일부러 vh 값을 조정하여 노출시켰다.
배포는 항상 하던 식으로 진행하였다.
settings > pages > source 에서 main 브랜치로 선택하여, action에서 확인하였다.
https://widrns15.github.io/fe-sprint-my-agora-states/
제출은 코드스테이츠에 Pull request 했다.
코드의 변동사항을 최소한으로 쪼개 유다시티에 맞춰 커밋을 남기고 있었다 ✨
개인적인 자산 + 협업할 수 있는 능력
모두 갖출 수 있는 느낌이 들어 유지한다면, 꽤 좋은 경험이 앞으로도 일어날 것 같다 :)
💡 먼저 키보드에서 손 떼고 구간부터 나눌 것
💡
justify-content: space-between
을 사용할 수 있는 사고를 기를 것
💡
flex
속성을 적용했을 때 flex-item이 유동적으로 크기 조절이 안된다면 ?
💡 scroll 적용이 되지 않는다면 ?
💡 centering
다음과 같이 사용 시, 폼이 가운데 정렬이 되는 것이었다.
그런데 flex를 주고 justify-content: center
, align-items: center
를 이용하는 방법도 있지 않은가?
보기엔 둘 다 가운데 정렬시키는 것 같은데 어떤 부분이 다른지 이해가 잘 되지 않아서 찾아보게 되었다.
결국 같은 것이나, 현재는 flex를 이용하는 방법을 조금 더 추천하는 추세라고 한다.
event.preventDefault()를 이용하여, ID는 영어만 입력이 가능하게 제한하기
폼 제출 시, alert로 창 띄워 사용자에게 메세지 전달하기
CSS 기능 추가 (게시물에 마우스 커서를 올릴 시, section 확대 등)
페이지네이션, 디스커션 유지