JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명
자바스크립트를 이용해 웹 테트리스를 만들어 보았다. 초심자를 위해 친절하게 설명하였다.
js를 사실상 이 영상으로 처음 하면서 배운 것들, 헷갈렸던 것들, 각 코드의 설명과 전체적인 흐름을 기록했다. 이 영상을 보고 클론코딩하다가 이해가 안 되는 분들은 이 글을 참고하면 좋을 것 같다.
각각의 코드들에 주석을 달아 이게 어떤 의미를 담고 있는지 나중에 까먹었을때 내가 보려고 상세하게 설명하였다.
출처: https://www.youtube.com/watch?v=1lNy2mhvLFk&list=WL&index=2
우선 전체코드는 아래와 같다.
index.html
<!DOCTYPE html>
<html lang="en">
<!--lang 속성은 자동번역과 음성번형을 위한 국가를 지정하는 속성이다. 여기에서는 en으로 영어로 지정하였다. -->
<head>
<meta charset="UTF-8">
<!--라이브러리처럼 다양한 속성 표기를 위해 meta가 쓰인다. 그리고 charset은 문자를 어떻게 인코딩할지 그 방식을 정한다. 여기서는 일반적으로 자주 쓰는 UTF8 방식이 쓰였다.-->
<meta name="viewport" content="width=device-width,inital-scale=1.0">
<!--name에서 viewport를 태그하면 기기에 따른 크기조절을 나타내고 후에 content에서 이 창을 여는 장치넓이로 설정시킨다. -->
<title>테트리스</title>
<!--제목표시줄 및 페이지 탭의 제목으로 사용, 검색엔진에서 표시,2개이상 사용불가능한 title 태그-->
<link rel="stylesheet" href="css/style.css">
<!--link태그에서 rel에 속성값을 넣고 스타일시트로 활용할 외부 리소스 불러옴
href에서 외부리소스의 url 명시한다-->
</head>
<body>
<div class="wrapper">
<!--class태그의 wrapper를 통해 여러 요소를 grouping한다-->
<div class="game-text">
<!--class태그를 통해 .으로 정의된 css 파일의 class를 불러올수 있음-->
<span>게임종료</span>
<button>다시시작</button>
</div>
<div class="score">
0
<!--class태그를 통해 .으로 정의된 css 파일의 class를 불러올수 있다-->
</div>
<div class="playground">
<!--class태그를 통해 .으로 정의된 css 파일의 class를 불러올수 있다-->
<ul></ul>
<!--ul 사이에 빈공간 없어야지 사이에 텍스트값 있는걸로 인식이 안된다.-->
<!--고의적으로 안이 텅빈 ul태그를 테트리스의 한 칸으로 사용힌다.-->
</div>
</div>
<script src="javascript/tetris.js" type="module"></script>
<!--스크립트 태그를 통해 javascript 사용한다. src를 통해 위치시키고 모듈타입으로 지정해야 tetris.js에서 import 가능 -->
</body>
</html>
style.css
* {
margin: 0;
padding: 0;
}
/*전체선택자로 margin은 바깥 여백, padding은 내용과 테두리 사이 간격 의미한다. 여기서 간격은 없다.*/
ul{
list-style:none;
}
/*UL태그의 속성 지정, list-style을 none으로 지정해줌으로써 ul 앞에 나오는 점을 없앰*/
body{
height: 100%;
/*정해진 블럭에서 차지하는 높이다.*/
overflow: hidden;
/*overflow를 hidden으로 지정해줌으로써 화면 넘어갈 경우 걍 숨겨버린다.*/
}
.game-text{
display: none;
justify-content:center;
align-items: center;
flex-direction: column;
position: fixed;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
left: 0;
top: 0;
color: #fff;
font-size: 50px;
}
.game-text > button{
padding: 0.5rem 1rem;
color: #fff;
background: salmon;
border: none;
cursor: pointer;
/*재시작 버튼을 표시한다.*/
}
.score{
text-align: center;
font-size: 36px;
margin-bottom: 2rem;
}
.playground > ul {
border:1px solid #333;
width: 250px;
margin: 0 auto;
/*모든 블럭을 합친 전체 칸의 크기다.*/
}
.playground > ul > li {
width: 100%;
height: 25px;
/*세로 한 줄의 크기다*/
}
.playground > ul > li > ul {
display: flex;
}
.playground > ul > li > ul > li {
width: 25px;
height: 25px;
outline: 1px solid #ccc;
/*테트리스 블럭에서 최종적으로 한 칸이 되는 블럭 단위의 크기를 설정하였다.*/
}
.tree{
background: #67c23a;
}
.bar{
background: salmon;
}
.square{
background: #2c82c9;
}
.zee{
background: #e6a23c;
}
.elLeft{
background: #8e44ad;
}
.elRight{
background: #16a085;
}
/*각 클래스별 색깔을 숫자로 지정하거나 단어로 지정한다*/
teris.js
import BLOCKS from "./blocks.js"
//BLOCKS를 JS파일 blocks.js에서 임포트 합니다. 즉 block.js에서 블럭 데이터 가져 옴
// DOM
const playground = document.querySelector(".playground > ul");
const gameText=document.querySelector(".game-text");
const scoreDisplay=document.querySelector(".score");
const restartButton=document.querySelector(".game-text > button");
/*각각 다른 위치에 있는 첫번째 요소를 받아와 각각의 이름에 할당합니다. 여기서.으로 시작하였기 때문에 클래스를 받아왔습니다. id로 가져오기
위해서는 #을 앞에 붙이는 걸 이용힙니다. 또한 > 기호로 그 안에 세부 내용을 찾게 합니다.*/
// Setting
const GAME_ROWS = 20;
const GAME_COLS = 10;
//varicables
let score = 0;
let duration = 500;
let downInterval;
let tempMovingItem;
//let는 const와 달리 변수 재할당이 가능한 변수 선언문 입니다.
const movingItem = {
type: "",
direction: 0,
top: 0,
left: 3,
};
//현재 떨어지고 있는 블럭의 모양과 방향과 위아래 위치, 좌우 위치 표시한다.
init()
// init을 통해 시작합니다.
// fUNCTIONS
function init() {
tempMovingItem={ ...movingItem };
//일명 three dots 문법으로 안에 있는 것을 전달받아 각각의 요소를 배열로 변환한다. 안의 값만 가져온다.
for (let i = 0; i < GAME_ROWS; i++) {
prependNewLine();
//시작하고 나면 새로운줄을 설정한 높이만큼 생성한다.
}
generateNewBlock();
//새로운 블럭 나와랏!
}
function prependNewLine() {
const li = document.createElement("li");
const ul = document.createElement("ul");
//각각 li랑 ul이라는 li,ul요소를 만든다. 앞부분에서 이름을 정하고 뒷부분에서 어떤 요소를 만드는지 정한다.
for (let j = 0; j < GAME_COLS; j++) {
const matrix = document.createElement("li");
ul.prepend(matrix);
//matrix라는 li 요소를 만들고, 각각의 ul에 자식으로써 li를 가로길이만큼 추가한다.
}
li.prepend(ul);
//위에서 정의된 li에 반복문안에서 만든 cols 만큼의 칸을 가진 것을 추가하고
playground.prepend(li);
//li라는 줄 하나를 마침내 추가한다.
//ul에 li가 가로길이만큼 있고, playground라는 ul 하나에 li가 rows만큼 있다. 그리고 그 li안에 각각 한칸을 나타내는 ul이 cols 만큼 가로에 있다.
}
//새롭게 1줄을 만들어주는 코드
function generateNewBlock(){
//새로운 블럭을 생성하는 코드
clearInterval(downInterval);
//기존의 블럭 계속 내려가게 하던 downInterval을 중단한다.
downInterval=setInterval(()=>{ moveBlock('top',1)},duration)
//새로운 블럭 downinterval을 시작한다
//setinterval은 첫번째 인자에 어떤 함수를, 두번째 인자에 그걸 반복해서 실행할 간격을 ms 숫자로 받는다.
//그리고 이는 화살표 함수다. 화삺표 함수는 (인자)=>함수; 형태이다.
//즉 donwninterval을 duration마다 moveblock(top,1)해주는 함수로 지정한 것이다.
const blockArray = Object.entries(BLOCKS);
//entries 함수를 통해 blocks의 모든 요소를 key:value로 blockarray에 받아온다.
const randomIndex = Math.floor(Math.random()*blockArray.length)
//어떤 블럭을 생성할지 랜덤으로 정하는 과정이다. 블럭의 개수 그러니까 0~블럭의 종류(blockarray.length) 사이의 수를 RANDOM으로 정해 randomindex에 넣는다.
movingItem.type= blockArray[randomIndex][0]
//그렇게 정한 randomIndex는 blockArray의 첫번째 배열 인덱스과 되어 블럭의 종류를 결정한다. 또한 두번째 배열값을 0으로 정해 그렇게 정한 블럭의 이름을 movingItem.type으로 지정한다.
movingItem.top=0;
movingItem.left=3;
movingItem.direction=0;
//처음 블럭이 생성될때 위치와 방향을 movingItem의 값들을 통해 정해준다.
tempMovingItem={...movingItem};
//그렇게 정한 블럭의 종류와 위치 방향을 tempmovingitem에 넣어 현재 생성되는 item인 tempmovingitem에 지정한다.
renderBlocks()
//화면에 모양을 보여주고 결정하는 함수를 호출한다.
}
function renderBlocks(moveType="") {
//moveType이라는 파라미터가 있을때만 별도의 과정을 통해 처리한다.
const { type, direction, top, left } = tempMovingItem;
//여기서 각각의 값에 구조분해하여 할당한다.불러온 변수들을 바로바로 사용할 수 있기 위함. 하나하나 접근하기 귀찮음
const movingBlocks =document.querySelectorAll(".moving");
//아래에서 사용하기 위한 moving이라는 class가진것들 모두 movingblocks로 가져와 추가 관리
//여기서 moving은 원래 정의된 클래스가 아닌 밑의 classlist.add에서 만들어진건데, renderblock 할때 이동하면 겹쳐서 색칠되는 것을 막기 위해 moving 클래스를 부여해 구별하는 것이다
movingBlocks.forEach(moving => {
moving.classList.remove(type,"moving");
//movingblocks 즉, moving class를 가지는 모든 객체에서 type과 moving 클래스를 제거한다.
//화살표함수가 최초로 사용되었는데 a.foreach(b->c)면 a를 b라고 선언하고 그것에 대한 c를 실행한다는 의미이다.
//즉 움직이고 나서 그자리에 있었던 애들은 모양을 담는 값과 moving 클래스를 제거해준다. 그러면 빈값이 되어 색칠이 지워진다.
})
BLOCKS[type][direction].some(block => {
// tempmovingitem에서 불러와 변수값으로 선언한것들 , 블럭의 모양과 방향을 불러온다.
//some 함수 사용 한번이라도 true값이 리턴되면 foreach같은 순회 멈추기위해 foreach 대신 사용
//BLOCKS[type][direction]을 block으로 불러오고
const x = block[0] +left;
const y = block[1] +top;
//block[0]과 [1]값은 블럭한칸의 x좌표와 y좌표의미 이를 x와 y로 불러온다, 이동중이라면 이동시켜서 불러온다.
const target = playground.childNodes[y]? playground.childNodes[y].childNodes[0].childNodes[x] : null;
//배경의 위치가 지정된 블럭에 접근해 해당블럭이 위아래 배경 한계를 넘지 않는다면 해당 블럭 li들 그렇지 않다면target을 null값으로 정한다.
//타겟은 배경 하나하나를 채우는 블럭이다.
const isAvailable = checkEmpty(target);
//isavailable함수로 해당 블럭이 위의 target에서 범위를 벗어나 undefined 되어 있는 상태인지 정한다.
if(isAvailable){
//만약 움직일 수 있는 상태라면 해당 타겟에
target.classList.add(type,"moving")
//여기서 type명과 "moving"이라는 클래스가 추가된다!
}
else{
//해당 블럭이 벽을 넘어가 버려 움직일수 없는 상태라면
tempMovingItem={...movingItem}
//현재 움직이는 아이템의 모양, 위 아래 길이, 방향을 원복시킨다.
if(moveType ==='retry'){
clearInterval(downInterval)
showGameoverText()
//===는 3항자 문법인데 타입형태까지 같은지 비교한다.
//아무튼 그렇게 해서 movetype이 retry라면 downinterval 함수를 중단하고, 게임오버 텍스트를 보여준다.
//renderblocks의 인수가 움직일 수 없는 상태라서 retry 받았는데 여전히 움직일 수 없는 상태로 또 retry받았다? 더이상 못움직이네! 게임끝났네
}
setTimeout(()=>{
renderBlocks('retry')
if(moveType === "top"){
//여기서 top은 문자열로 movingitem의 숫자값 top이랑은 다르다. top이라는 상태는 더이상 블럭이 아래에 닿아 움직일 수 없는 상태인지 확인하는 것이다.
seizeBlock();
}
},0)
//settimeout은 실행시킬 함수, 몇초 뒤에 실행시키는 지를 인자로 받는다.
//그래서 인자 없는 renderblocks 부터 밑의 if문을 0초컷으로 작동시키는 것이다.
//0초 후에 실행시키는 게 무슨 소용인가요? 라고 생각하면 자바스크립트의 비동기 시스템을 생각해야 한다. 0초 후에 함수를 실행시키라는 뜻은 다른 함수 스케쥴링이 끝나고 나서 바로 실행해달라는 뜻이다.
//그러니까 다른 함수 실행이 끝나고 나면 renderblocks를 retry로 재귀호출하고 movetype이 top, 그러니까 ~면 블럭을 움직일 수 없게 굳힌다.
//이벤트 스택 계속 쌓이는 에러 막기 위함이다. 이게 없으면 계속 나온다.
return true;
}
})
movingItem.left=left;
movingItem.top=top;
movingItem.direction=direction;
//방향 변경 가능하다고 여겨지면 그제서야 현재 떨어지는 블럭 바꿔줌
}
function seizeBlock(){
const movingBlocks =document.querySelectorAll(".moving");
movingBlocks.forEach(moving => {
moving.classList.remove("moving");
moving.classList.add("seized");
})
checkMatch()
//이미 쌓은 블럭을 더 이상 움직일 수 없게 굳히는 함수다.
}
function checkMatch(){
//한줄 완성되었나 확인하는 함수
const childNodes = playground.childNodes;
childNodes.forEach(child=>{
let matched = true;
child.children[0].childNodes.forEach(li=>{
if(!li.classList.contains("seized")){
matched=false;
//모두 seized 가지고 있는지 확인
}
})
if(matched){
child.remove();
//매치되면 새로 줄만들고 기존줄은 다 없애기
prependNewLine()
score++;
scoreDisplay.innerText=score;
}
})
generateNewBlock()
}
function checkEmpty(target){
if(!target || target.classList.contains("seized")){
return false;
}
return true;
//타겟이 되는 배경블럭 1칸이 존재하고, 그 블럭의 classlist에 seized가 없다면 true 있다면 false를 반환한다.
}
function moveBlock(moveType, amount){
tempMovingItem[moveType] +=amount;
//현재 떨어지고 있는 아이템인 tempmovingitem의 방향을 amount만큼 수정한 다음에 renderblocks를 호출한다.
//원래 tempmovingitem에는 type,direction,top,left 값을 가졌음을 기억하자
renderBlocks(moveType);
}
function changeDirection(){
const direction=tempMovingItem.direction;
direction === 3 ? tempMovingItem.direction=0 : tempMovingItem.direction +=1
renderBlocks()
//방향바꾸는 함수로 호출되면 tempmovingitem의 값을 1만큼 더해서 바꾸고 renderBlocks함수를 호출한다. 단 3이라면 4를 넘을 수 없으니 0으로 값을 정한다.
}
function dropBlock(){
clearInterval(downInterval);
downInterval = setInterval(()=>{
moveBlock("top",1)
},10)
//고의적으로 블럭 떨구는 함수로 계속 떨어지게 만드는 downInterval함수를 중단하고, 훨씬 빠르게 떨어지게 한다.
}
function showGameoverText(){
gameText.style.display="flex"
//게임 오버되면 index.html에 있는 게임 종료 글짜를 보여준다.
}
//event handling
document.addEventListener("keydown", e => {
switch(e.keyCode){
case 39:
moveBlock("left", 1);
break;
case 37:
moveBlock("left", -1);
break;
case 40:
moveBlock("top",1);
break;
case 38:
changeDirection();
break;
case 32:
dropBlock();
break;
default:
break;
}
//keydown 즉, document에서 버튼이 눌렸을때 e라는 이벤트를 처리합니다.
//e라는 이벤트의 키보드 값을 switch case 문으로 숫자로 방향키를 표기
//좌우하키를 누르면 방향과 그 값을 인수로 moveblock 함수에 전달한다.
//스페이스키를 누르면 떨구고 위쪽 방향키를 누르면 방향을 바꾸는 함수를 호출한다.
})
restartButton.addEventListener("click",()=>{
playground.innerHTML = "";
//playground안의 html 즉 게임에서 사용된 보여주는 각칸의 블럭들을 무로 되돌린다.
gameText.style.display="none"
//게임오버 글씨를 없앤다
init()
//시작함수를 다시 호출해 다시 게임을 시작한다.
})
blocks.js
const BLOCKS = {
//BLOCKS 라는 블럭 상수를 선언하고, 그안에 KEY:VALUE로 객체 선언한다
tree: [
[[2, 1],[0, 1],[1, 0],[1, 1]],
[[1, 2],[0, 1],[1, 0],[1, 1]],
[[1, 2],[0, 1],[2, 1],[1, 1]],
[[2, 1],[1, 2],[1, 0],[1, 1]]
],
//각각 KEY로 이름을 정의하고 블럭들이 어떻게 생겼는지 각각 [x좌표, y좌표]안에 넣고 각각 돌렸을때 어떻게 되는지도 정의해준다.
square: [
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]]
],
bar: [
[[1, 0],[2, 0],[3, 0],[4, 0]],
[[2, -1],[2, 0],[2, 1],[2, 2]],
[[1, 0],[2, 0],[3, 0],[4, 0]],
[[2, -1],[2, 0],[2, 1],[2, 2]]
],
zee: [
[[0, 0],[1, 0],[1, 1],[2, 1]],
[[0, 1],[1, 0],[1, 1],[0, 2]],
[[0, 1],[1, 1],[1, 2],[2, 2]],
[[2, 0],[2, 1],[1, 1],[1, 2]]
],
elLeft: [
[[0, 0],[0, 1],[1, 1],[2, 1]],
[[1, 0],[1, 1],[1, 2],[0, 2]],
[[0, 1],[1, 1],[2, 1],[2, 2]],
[[1, 0],[2, 0],[1, 1],[1, 2]],
],
elRight: [
[[1, 0],[2, 0],[1, 1],[1, 2]],
[[0, 0],[0, 1],[1, 1],[2, 1]],
[[0, 2],[1, 0],[1, 1],[1, 2]],
[[0, 1],[1, 1],[2, 1],[2, 2]]
]
}
export default BLOCKS;
//BLOCKS 하나를 EXPORT해서 나중에 다른 파일에서 IMPORT 할 수 있게 내보냄
하지만 기존의 코드에는 개선되었으면 좋겠는 버그들이 있다.
1. 다시 시작을 눌렀음에도 기존의 점수가 리셋되지 않는다.
2. 게임이 종료 되었음에도 블록이 계속 내려온다.
게임이 종료 되었음에도 좌우키를 꾹 누르면 블록이 내려오는 걸 확인할 수 있다.
3. 블럭이 벽에 닿았을 때 그쪽 벽으로 꾹 누르면 벽이 잠깐씩 뚫렸다가 돌아온다.
다음번 게시물에는 이 3가지 버그를 고치는 법에 대해 포스팅할 것이다.
여담
클론코딩을 하고 따라칠 때 다시 보니 이해가 안 되는 부분들이 꽤 많았다. 강의를 들으면서 타이핑을 하다 보면 계속 일시정지를 누르고 따라치게 되고, 그러면서 강의의 흐름과 내 머릿속 이해의 흐름이 하나의 물줄기를 형성하지 못하고 역류하는 경우가 생겼다. 그로 인해 강의를 볼 때는 이해가 됐는데 내가 입력하고 보니 지식이 그사이 증발해서 알아볼 수 없는 웃지 못할 경험도 많이 했다. 그러다 보니 강의시간은 1시간이지만 실제로 완성하고 코드를 이해하는 데는 꽤 오래 걸렸다. 그리고 그 코드를 개선하여 기능을 추가하는 데에는 더 오래 걸렸다. 내가 이해했다고 생각한 코드가 내가 짠 코드가 아니다 보니 헤매는 경우가 많았다.
예전에는 html, css,js를 아는 척만 했다. 진짜 아는 게 아니라 안다고 착각했다. 특기학교에서 잠깐 배웠을 테니 어느 정도 할 거야 라거나 막연히 프런트엔드는 쉬우니까 같은 안일한 생각으로 스스로를 기만했다. 알고 보니 내가 모르는 것들이 많았다. 따로 떨어져 있는 자바스크립트와 html css 파일이 어떻게 서로 상호작용 할 수 있는지 같이 말이다. 코딩할 때는 오히려 알지 못했고, 나중에 코드를 모아 보면서 이해하는 과정에서 얻게 된 것들이 있다.
html과 자바스크립트는 html의 class나 id를 queryselect하는 DOM과 innerHTML로 한다는 것을 알게 되었다. css와 html도 마찬가지로 class와 내부의 온점과 >로 접근한다는 것을 깨달았다. 서로 다른 자바스크립트 파일끼리는 export와 import로 불러올 수 있다는 것을 학습했다. 어릴 때의 블록코딩(다양한 파일을 사용하지 않는다)의 여파로 서로 다른 파일끼리 어떻게 상호작용하는지 궁금했었는데 좋은 답을 얻게 되었다.
구조체 문법은 1학년때 글로만 배우고 직관적으로 이해가 잘 되지 않았는데, 이걸 진행하면서 어떻게 쓸 수 있을지 알게 되었다. three dots 문법이라 불리는 온점 3개 찍는 문법이 구조체 문법 중 가장 덜 직관적이라 낯설고 신기하게 다가왔다.
화살표 함수는 기존의 파이썬이나 c++문법에서 보지 못한 형태라 많이 해맸다. 직관적으로 저 화살표는 뭐고 빈괄호는 뭐고 중괄호는 뭔지 이해가 되지 않았기 때문이다. 지금은 오히려 화살표 문법이 익숙해져서 나중에 다른 프로젝트를 할 때도 찾을 것 같다.
프런트 엔드를 사용하는 새로운 접근 방식도 많이 알게 되었다.
자바스크립트로 만든 다른 테트리스 게시글은 대부분 캔버스를 사용했는데 여긴 특이하게 캔버스를 사용하지 않고 li, ol 등의 태그만 사용했다. li 같은 태그에도 각 태그에 사각형을 할당하고 배열하면 캔버스를 쓴 것과 같은 이미지효과를 낼 수 있는지 처음 알았다. 기존에 시각적 피드백을 줄려면 Canvas나 이미지 파일을 따로 로드해서 써야만 하는 줄 알았는데 li를 사용하고 거기에 css로 배열을 만들어 판을 만들어내다니. 혼자서는 할 수 없었을 발상이다. 이미 프런트엔드에 익숙한 사람들은 당연하게 생각할 수 있는 부분이지만, 코린이인 나한테는 굉장히 신박하게 다가왔다.
난 처음에 테트리스 블럭을 한 줄 없애는 데 있어 없애는 줄 위의 모든 줄이 한 칸 위의 줄로 정보를 업데이트하는 방식을 쓸 줄 알았는데 그냥 li들을 삭제하고 새로 만드는 방식을 쓰다니 힙하고 훨씬 효과적인 방식이라고 생각한다.
원래는 코드 한 줄 한줄 따로 떼어서 분석하려 했으나 코드의 연속성적인 부분이나, 티스토리 내부에서 코드블록을 쓰는데 불편함이 있어 그냥 전부 주석처리했다. 한 줄 한 줄을 옮겨 주석으로 쓰는 데 있어 아주 기초적인 태그부터 서술했다. 진짜로 처음 하는 사람들은 당연하듯이 있는 코드 한 줄도 제대로 설명을 듣지 않으면 오랫동안 싸매게 되기 때문이다. 다음에 게시글을 올릴 때는 가독성 있는 방법을 찾아야 할 것 같다.
다음글 링크!
2023.04.22 - [분류 전체보기] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명2