** 오늘의 할 일 **
1. 메인 페이지, 로그인 페이지 디자인 시안 만들기
2. 로고 제작하기
3. 메인 페이지 뼈대 만들기
4. 로그인 페이지 뼈대 만들기
5. 로그인 페이지 DB 연결하기
1. 메인 페이지, 로그인 페이지 디자인 시안 만들기
우선 맥북 freeform으로 간단하게 시안을 만들어 보았다.


2. 로고 제작하기
KRATOS는 고대 북유럽의 신비를 통해 마법과 같은 커피맛을 선사하고자 하는 커피 브랜드로, 로고 역시 노르드풍의 특별함을 뽐내야만 했다. 이를 위해 본사 디자인 팀장과의 열띤 토론이 오고갔다.

팀장이 건네준 디자인 시안을 직접 그림판으로 (무려 2시간 넘게) 열심히 수정해준 뒤에야, 마침내 로고가 완성될 수 있었다.

3. 메인 페이지 뼈대 만들기
몇 가지 주요 사항들을 정리해보았다.
1) 상단 메뉴 바 구현하기
보통 <nav> 태그를 이용하여 메뉴 바를 구현하게 된다. 이는 네비게이션 영역을 나타내는 태그로, 주로 <a> 태그를 이용한 링크 모음을 포함할 때 사용할 수 있다.
<nav class="nav">
<a href="about.html">About KRATOS</a>
<a href="sip_score.html">SIP & SCORE</a>
<a href="coffee_guide.html">COFFEE GUIDE</a>
<a href="shopping.html">SHOPPING</a>
<a href="forum.html">FORUM</a>
<a href="login.html">Login</a>
</nav>
여기에 CSS를 조금 만져서 로고와 상단 메뉴 바 까지 완성할 수 있었다.

+) Login 항목에서 향후 추가적인 수정이 필요할 것 같다. 로그인이 완료된 다음에는 '00님 어서오세요' 같은 환영 문구와 함께 마이페이지로 연결될 수 있는 링크로 바뀌도록 하고 싶기 때문이다. 일단 내일로 미루자..
2) 메인 페이지 사진 슬라이드 만들기
자고로 잘 나가는 웹 사이트라면 응당 메인 페이지에부터 사진 네댓 장이 휙휙 지나가 정신없는 분위기를 연출해야만 할 것이다. (아니다.)
찾아보니 CSS의 animation을 이용하는 방법, javascript의 setInterval()를 이용하는 방법 등등 여러 가지가 있지만, 역시 라이브러리를 활용하는 것이 가장 효율이 좋을 것 같았다. swiper.js를 CDN을 이용하여 사용해보기로 했다.
CDN이란?
Content Delivery Network
웹사이트의 콘텐츠(이미지, CSS, Javascript, 영상 등등)을 빠르게 전달하기 위해 전 세계 여러 서버에 분산하여 제공하는 네트워크 시스템의 일종. JavaScript 라이브러리를 CDN으로 가져오면, 내가 jQuery 파일을 내 서버에 직접 업로드하지 않아도 되기 때문에 서버 부하를 낮출 수 있고, 사용자 입장에서도 해당 웹사이트에 접속할 때 가장 가까운 CDN 서버에서 라이브러리를 가져오기 때문에 전 세계 어디서든 빠르고 안정적으로 로딩된다는 장점이 있다.
swiper.js의 사용 방법에 대해서는 홈페이지에 자세하게 나와있다.
https://swiperjs.com/swiper-api
Swiper - The Most Modern Mobile Touch Slider
Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.
swiperjs.com
우선 라이브러리를 가져온다.
# Swiper 라이브러리 CDN를 head에 넣어준다.
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"
/>
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
다음, html 코드를 써준다. 이 때, Swiper 에서 쓰라고 하는 layout 그대로 써야 한다.
<!-- Slider main container -->
<div class="swiper">
<!-- Additional required wrapper -->
<div class="swiper-wrapper">
<!-- Slides -->
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
...
</div>
<!-- If we need pagination -->
<div class="swiper-pagination"></div>
<!-- If we need navigation buttons -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- If we need scrollbar -->
<div class="swiper-scrollbar"></div>
</div>
참고로 나는 기본 슬라이드에 더해 navigation button(이전 사진, 다음 사진 클릭하는 기능)만 추가했다.
이후 원하는 기능을 골라담아 javascript 코드를 작성하면 된다.
const swiper = new Swiper(".swiper", {
loop: true, //loop 기능을 켜준다.
autoplay: {
delay: 5000, //autoplay의 delay 간격을 설정한다.
disableOnInteraction: false, //버튼을 클릭하거나 상호작용을 해도 autoplay가 멈추지 않는다.
},
effect: "fade", //사진 전환 효과.
navigation: {
prevEl: ".swiper-button-prev",
nextEl: ".swiper-button-next",
}, //navigation 기능을 쓰려면 반드시 넣어야 한다.
speed: 1000, //사진 전환 시간.
});
여기서 주의할 것
1) const swiper = new Swiper
여기서 new 뒤에는 S, 즉 대문자이다. 주의하자.
2) 분리된 javascript 파일을 사용하는 경우, 링크를 body 끝에 넣어 html이 먼저 로드된 다음 실행될 수 있도록 하자.
또는, defer 속성이나 DOMContentLoaded 이벤트 등을 사용하여 Swiper가 .swiper 요소를 찾을 수 있도록 설정해두어도 된다.
이제 CSS를 이용해서 꾸며주는 일만 남았다.
먼저 슬라이드 사진 이미지를 꾸며주었다.
.swiper {
width: 100%;
height: 500px;
}
.swiper-slide img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 3s ease-in-out; /*3초 동안 확대 효과*/
}
.swiper-slide-active img {
transform: scale(1.1);
} /*현재 슬라이드에서 이미지가 1.1배 확대됨.*/
여기서 .swiper-slide-active 의 경우, swiper에서 현재 화면에 보이는(활성화된) 슬라이드에 자동으로 붙는 클래스다.
이렇게 하면 다음 슬라이드로 넘어갈 때마다 자연스럽게 3초동안 천천히 줌 인 되는 애니메이션 효과를 만들 수 있다.
다음은 navigation button이다.
기본 내장된 버튼 모양이 너무 못생겨서, 구글링 끝에 변경하는 방법을 찾아낼 수 있었다.
.swiper-button-prev,
.swiper-button-next {
color: white !important;
background-color: rgba(0, 0, 0, 0.5);
width: 3rem !important;
height: 3rem !important;
border-radius: 50%;
}
.swiper-button-prev:after,
.swiper-button-next:after {
font-size: 1.2rem !important;
font-weight: 600 !important;
}
이미 지정된 속성들 때문인지 충돌이 되는 부분은 !important를 박아넣어 강제로 적용시켰다.
그리하여 최종본.

참고로 여기서 .swiper-button-prev:after 이란...
웹페이지 개발자 도구를 통해 확인해보면 튀어나오는 요소로, swiper가 자신들의 기본 화살표 이미지를 여기 'after'라는 가상 요소에 추가한 것을 알 수 있다.
따라서 이걸 지우고 개인적으로 갖고 있는 이미지 파일을 사용하려면, 다음과 같이 입력하면 될 것이다.
/* Swiper 기본 화살표 제거 */
.swiper-button-prev::after,
.swiper-button-next::after {
display: none; /* 기본 화살표 숨김 */
}
swiper-button-prev {
background-image: url('images/left-arrow.svg'); /* 화살표 이미지 */
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
...
하지만 따로 이미지 파일을 준비하기 귀찮다면, 위 예시처럼 적당히 수정해서 써도 좋을 것 같다.
3) 로그인 페이지로 유도하기
사용자가 링크를 클릭하면, 해당 사용자가 로그인 상태인지 판단하고, 비로그인 상태라면 로그인 페이지로 보내고 싶다.
먼저, 다음과 같이 권한 검토용 php 파일을 생성한다.
//auth_check.php
<?php
session_start();
//로그인 여부 확인
if (!isset($_SESSION['user_id'])) {
//로그인이 안 되어 있으면 로그인페이지로 리다이렉트
header("Location: login.php");
exit();
}
?>
참고로 여기서 isset 앞에 느낌표(!)가 붙은 것은, not 이라는 의미이다.
이제 앞으로 해당되는 모든 페이지의 php 파일 상단에 auth_check.php를 포함하면 된다.
<?php include 'auth_check.php'; ?>
//뒤에는 실제 콘텐츠가 이어진다.
물론 아직 로그인 페이지를 만들지도, 로그인 기능을 구현하지도 못했지만말이다.
이제부터 만들어 보자.
3. 로그인 페이지 뼈대 만들기
예전에 DB연결 없이, PHP로만 간이 로그인 페이지를 만들어본 적은 있으나, DB에 연결한 적은 없었다.
먼저 MySQL로 DB를 생성한다.
CREATE DATABASE ragnarok_db;
USE ragnarok_db;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL
);
이제 로그인 정보를 받을 수 있도록 최대한 간단하게 html을 작성한다.
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log in</title>
</head>
<body>
<h2>로그인</h2>
<form method="POST" action="login.php">
<label for="user_id">아이디:</label>
<input type="text" name="user_id" required><br><br>
<label for="user_password">비밀번호:</label>
<input type="password" name="user_password" required><br><br>
<button type="submit">로그인</button>
</form>
</body>
</html>
이전에는 다음과 같이
<p>아이디 : <input type="text" name="username"></p>
단순히 <p> 태그를 썼다면 이번에는 <label> 태그를 사용해보았는데, 이건 Input 입력 창 앞에 있는 텍스트(라벨)를 의미한다.
label 부분만 클릭해도 input 태그에 입력이 가능하기 때문에 훨씬 매끄럽다.
이제 본격적으로 login.php를 만들 차례다.
1) session 열기
<?php
session_start();
2) DB와 연결하기
// DB와 연결하기
$servername = "localhost"; // DB 서버 주소
$username = "root"; // MySQL 사용자명
$password = ""; // MySQL 비밀번호
$dbname = "ragnarok_db"; // DB 이름
$conn = new mysqli($servername, $username, $password, $dbname);
// 연결 확인
if ($conn->connect_error) {
die("DB 연결 실패 :(" . $conn->connect_error);
}
3) 사용자 입력값 가져오기
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$user_id = trim($_POST["user_id"]);
$user_password = trim($_POST["user_password"]);
지난번 간이 로그인 페이지를 만들 때와는 많은 부분이 달라졌다. 하나씩 살펴보면,
$_SERVER["REQUEST_METHOD"] // 현재 페이지가 요청된 HTTP 메서드를 가져온다.
== "POST" // 이 코드 블록 안에 있는 코드가 POST 요청일 때만 실행된다.
이 코드를 굳이 넣는 이유는, 사용자가 직접 웹사이트를 방문했을 때 GET 요청을 방지하기 위함이다.
예시)
http://example.com/login.php?user_id=mimir&user_password=1234
이런 식으로 로그인하면, 로그인 폼이 아니라 URL로도 로그인할 수 있게 되어 보안상 매우 위험해진다.
따라서 POST 요청일 때만 실행될 수 있도록 해당 코드를 넣는 것이다.
이 부분의 보안 취약성(CSRF 공격 등)에 대하여 근시일 내 따로 포스팅하겠다.
또한,
$user_id = trim($_POST["user_id"]);
여기서 trim()은 공백(스페이스, 개행문자 등)을 제거하여 입력값을 정리해주는 함수다.
이는 사용자가 실수로 공백을 입력해도 문제 없도록 도와주는 한편, SQL injection 방지에도 도움을 준다.
이 부분에 대해서도 나중에 좀 더 자세히 포스팅하겠다.
4) 입력값 검증(빈 값인지 확인)
if (empty($user_id) || empty($user_password)) {
echo "아이디와 비밀번호를 입력해주세요.";
}
html에서 reguired 속성이 있는데도 이 코드를 넣는 이유는 뭘까? 이것도 역시 보안 문제 때문이다.
required 속성은 클라이언트(사용자)에서만 동작하기 때문에, 이걸 우회해서 빈 값으로 로그인 시도를 할 수 있어 보안에 취약해진다. 따라서 PHP에서 한번 더 empty() 함수로 백엔드 차원에서 빈 값을 차단해주는 것이다.
5) 사용자 정보 가져오기
$sql = "SELECT id, password FROM users WHERE user_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $user_id);
$stmt->execute();
$result = $stmt->get_result();
여기서 sql을 실행하는데, SQL injection을 방지하기 위한 prepared statement이다.
해당 코드에 대해서는 이 포스팅에 자세하게 설명해두었다. -> https://nailed.tistory.com/17
6) 사용자 정보 확인하기
if ($result->num_rows == 1) {
$row = $result->fetch_assoc();
$hashed_password = $row["password"];
// num_rows == 1이 아닐 경우 뒤에서 else{ echo "아이디가 존재하지 않습니다.";)
먼저 첫 줄은, 사용자가 실제로 존재하는지를 확인하는 과정이라 할 수 있다.
$result->num_rows는, MySQL이 반환한 행(Row)의 개수를 나타낸다.
이 값이 1 이라면, 해당 user_id가 DB에 존재한다는 것을 의미하고, 0 이라면 없다는 뜻이다.
근데 만약에 1보다 크다면? user_id가 중복된 것이다.
사실, 앞서 DB에서 user_id를 UNIQUE로 설정하였기 때문에 중복되면 비정상이다.
그 다음에 행(row)을 가져온다.
fetch_assoc() 은, 한 행을 연관 배열 형태로 가져오는 함수다.
연관 배열(Associative Array)이란?
키(Key)와 값(value)의 형태로 데이터를 저장하는 배열이다.
보통 일반 배열은 숫자(index)로 값을 가져오지만, -> $users[0]
연관 배열은 키(Key)로 값을 가져올 수 있다. -> $user["username"]
따라서 $row = $result->fetch_assoc() 을 실행하면, ["컬럼명" => "값" ] 형태로 데이터를 저장하게 되고,
예컨대 다음과 같은 형태가 될 것이다.
$row = [
"id" => 1,
"user_id" => "root",
"password" => "1111"
];
연관 배열을 사용하면 가독성이 좋아지고, 컬럼명을 직접 참조할 수 있어서 코드가 헷갈릴 가능성이 줄어든다.
마지막 줄인 $hashed_password = $row["password"] 을 이해해보자.
아직 회원 가입 페이지를 만들지 않았지만, 보안상 안전하도록 비밀번호를 해싱(암호화)할 예정이다.
그렇게 되면 password 컬럼의 값들은 해시된 값일 것이므로, $hashed_password에 저장해두는 것이다.
7) 비밀번호 검증 및 마무리
if (password_verify($user_password, $hashed_password)) {
$_SESSION["user_id"] = $user_id;
header("Location: welcome.php"); // 로그인 성공 시 이동할 페이지
exit();
} else {
echo "비밀번호가 올바르지 않습니다.";
}
} else {
echo "아이디가 존재하지 않습니다."; //이 부분은 앞서 num_rows() 검증 실패시 실행됨.
}
$stmt->close();
}
}
$conn->close();
?>
해시되어 저장된 비밀번호와 사용자가 입력한 비밀번호를 비교할 수 있도록 해주는 함수가 바로 password_verify() 이다.
애초에 '해싱된 비밀번호'와 '입력된 비밀번호'를 비교하는 함수기 때문에, DB에 해싱되지 않은 평문 비밀번호가 저장되어 있으면 이를 사용할 수 없다. 따라서 이미 DB에 해싱되지 않은 데이터가 있다면, 한번에 해싱해주는 과정을 거쳐야 한다.
만약 검증이 완료되었다면, 이제 세션(session)에 사용자 정보를 저장해야 한다.
$_SESSION["user_id"] = $user_id;
여기서 $_SESSION 은 PHP에서 제공하는 완전 전역 변수(supergolbals) 중 하나다.
해당 array에 사용자가 입력한 아이디 값($user_id)이 "user_id"라는 키(Key)로 저장되며, 이후 로그인 상태를 유지할 수 있도록 해준다.
완전 전역 변수(supergolbals)
PHP에서 어디서든 접근할 수 있는 특별한 전역 배열을 의미한다.
superglobals의 다른 예시로는 $_GET, $_POST, $_COOKIE 등이 있다.
이제 드디어 로그인 페이지 뼈대가 완성되었다.
잘 작동하는 것을 확인할 수 있었다.

로그인 페이지 꾸미는건 다음 시간으로 미루자..
'Learning Web > 2024~2025 Web Development' 카테고리의 다른 글
| 원격 DB (MySQL) 와 연결하기 (0) | 2025.02.02 |
|---|---|
| [KRATOS] #0 밑그림 작업 (2) | 2024.12.17 |
| 로그인 페이지 업그레이드 (2) | 2024.10.23 |
| PHP를 활용하여 간이 로그인 페이지 만들기 (0) | 2024.10.21 |
| UTM으로 SSH 접속하기 (2) | 2024.10.18 |