WITH MOST AS (
SELECT MEMBER_ID, COUNT(*) AS CNT
FROM REST_REVIEW
GROUP BY MEMBER_ID
ORDER BY CNT DESC
LIMIT 1
)
SELECT P.MEMBER_NAME,
R.REVIEW_TEXT,
DATE_FORMAT(R.REVIEW_DATE, '%Y-%m-%d') AS REVIEW_DATE
FROM MEMBER_PROFILE P
JOIN REST_REVIEW R ON P.MEMBER_ID = R.MEMBER_ID
JOIN MOST M ON R.MEMBER_ID = M.MEMBER_ID
ORDER BY R.REVIEW_DATE ASC, R.REVIEW_TEXT ASC;
#틀린 이유 1. WITH 절 SELECT문에서 GROUP BY로 집계하지 않는 REVIEW_TEXT를 반환시켰다.
# 2. LIMIT 1이 아닌 LIMIT 3을 사용했다.
# 3. 문제를 잘 안읽고 ASC를 DESC로 작성했다.
# 4. GROUP BY를 사용한 집계함수 결과에 대한 필터링은 HAVING을 사용해야만 한다.
첫 with문 사용. 언젠가는 알아야 할 개념을 이악물고 외면하다가 처음 사용해봤다. 막상 써보니 별 거 아니라서 시시했다. 틀린 이유를 복습하면서 자세히 복기하자.
SELECT DATE_FORMAT(SALES_DATE, '%Y-%m-%d')SALES_DATE,
PRODUCT_ID, USER_ID, SALES_AMOUNT
FROM
(SELECT SALES_DATE, PRODUCT_ID, USER_ID, SALES_AMOUNT
FROM ONLINE_SALE
WHERE SALES_DATE LIKE '2022-03%'
UNION
SELECT SALES_DATE, PRODUCT_ID, NULL, SALES_AMOUNT
FROM OFFLINE_SALE
WHERE SALES_DATE LIKE '2022-03%') A
ORDER BY 1,2,3
UNION을 처음 사용한 쿼리.
JOIN: 관계가 있어야 한다. 두 테이블에 각각 존재하는 '다른 의미를 가지는' 날짜 데이터끼리 JOIN을 해 봐야 의미가 없다. '같은 내용의 행 데이터를 같는 횡결합
UNION: 독립된 두 테이블이지만 '동일한 형식의 열 칼럼'을 갖는 종결합
이 때, 1 UNION 2 테이블에서 2테이블의 칼럼이 하나 없더라도, SELECT 칼럼 자리에 값을 넣으면 1테이블 칼럼명을 그대로 쓰되 2테이블의 값이 모든 행에 채워진다.
SELECT B.TITLE, B.BOARD_ID, R.REPLY_ID, R.WRITER_ID, R.CONTENTS,
DATE_FORMAT(R.CREATED_DATE, '%Y-%m-%d') CREATED_DATE
FROM USED_GOODS_REPLY R JOIN USED_GOODS_BOARD B
ON R.BOARD_ID = B.BOARD_ID
WHERE B.CREATED_DATE LIKE '2022-10%'
ORDER BY R.CREATED_DATE, B.TITLE
WITH RECURSIVE CREATED_HOURS AS ( # 'WITH RECURSIVE' 까지가 재귀CTE 선언 함수
-- 앵커 멤버: 0을 반환
SELECT 0 AS HOUR
UNION ALL
-- 재귀 멤버: number에 1을 더해가면서 24 이하까지
SELECT HOUR + 1
FROM CREATED_HOURS
WHERE HOUR < 23 # SELECT에서 + 1 이 있으니까 23까지 반환한다.
)
SELECT C.HOUR,
CASE WHEN COUNT IS NULL THEN '0' # NULL 값에 '0' 을 넣어주고
ELSE COUNT END COUNT # 나머지 값은 그대로 소환
FROM CREATED_HOURS C
LEFT JOIN # 재귀CTE로 생성한 0~23 숫자를 모두 살려야 하기에 LEFT JOIN
(SELECT HOUR(DATETIME) HOUR, COUNT(ANIMAL_ID) COUNT
FROM ANIMAL_OUTS
GROUP BY HOUR(DATETIME)) A
ON C.HOUR = A.HOUR
SELECT -- 자동차 ID, 자동차 종류, 대여 금액(컬럼명: FEE) 출력
C.CAR_ID
, C.CAR_TYPE
-- FEE: (대여 날짜 * 하루 대여비) * 할인적용률 -> 소수점 반올림(.00 제거)
, CASE WHEN DATEDIFF(H.END_DATE,H.START_DATE)+1 >= 30
THEN ROUND(C.DAILY_FEE * (DATEDIFF(H.END_DATE,H.START_DATE)+1) * 0.93)
WHEN DATEDIFF(H.END_DATE,H.START_DATE)+1 >= 7
THEN ROUND(C.DAILY_FEE * (DATEDIFF(H.END_DATE,H.START_DATE)+1) * 0.95)
ELSE C.DAILY_FEE * (DATEDIFF(H.END_DATE,H.START_DATE)+1)
END FEE
FROM -- 3개 태이블 조인
CAR_RENTAL_COMPANY_CAR C
LEFT JOIN
CAR_RENTAL_COMPANY_RENTAL_HISTORY H ON C.CAR_ID = H.CAR_ID
JOIN
CAR_RENTAL_COMPANY_DISCOUNT_PLAN P ON C.CAR_TYPE = P.CAR_TYPE
WHERE
P.CAR_TYPE IN ('세단','SUV') AND -- '세단' or 'SUV'
H.END_DATE < '2022-11-01' AND H.START_DATE < '2022-11-01' AND -- 2022-11-01 ~ 2022-11-30 대여 가능
(C.DAILY_FEE * 30) - (C.DAILY_FEE * 30 * 0.1) >= 500000 AND -- 30일간의 대여 금액 >= 50만원
(C.DAILY_FEE * 30) - (C.DAILY_FEE * 30 * 0.1) < 2000000 -- 30일간의 대여 금액 < 200만원
ORDER BY -- 대여 금액 DESC, 차 종류ASC, 자동차 ID DESC
3 DESC, 2, 1 DESC
JOIN 시 디폴트는 INNER JOIN이다. 위 사례에서 첫 번째 조인은 CAR_RENTAL_COMPANY_CAR 'LEFT JOIN'CAR_RENTAL_COMPANY_RENTAL_HISTORY 'ON' 을 했어야 했다.
그래야 한번도 렌탈되지 않았지만 렌탈 가능한 차량이 조회되기 때문이다.
JOIN - AND 로 WHERE절 필터링을 상당수 대체할 수 있다.
select
a.car_id,
a.car_type,
round(a.daily_fee*30*((100-b.discount_rate)/100)) as fee
# c.*
from CAR_RENTAL_COMPANY_CAR as a
inner join CAR_RENTAL_COMPANY_DISCOUNT_PLAN as b
on a.car_type = b.car_type and a.car_type IN ('세단', 'SUV') and b.duration_type = '30일 이상'
left join CAR_RENTAL_COMPANY_RENTAL_HISTORY as c
on a.car_id = c.car_id and c.end_date > '2022-11-01'# and c.car_id is null
where c.history_id is null -- 2022년 11월 1일 이후에 대여 기록이 없는 자동차
having fee >= 500000 and fee < 2000000
order by 3 desc, 2, 1 desc
각 조인별로 해당되는 테이블에서 수행할 수 있는 필터링을 AND로 수행하였기에 정확도와 가독성이 높아진다.
LEFT JOIN 으로 발생한 NULL값을 필터링에 사용하셨다.
이틀간 풀었다..
WITH TRUCK AS(
SELECT H.*,
C.DAILY_FEE,
DATEDIFF(END_DATE, START_DATE) + 1 DURATION,
CASE WHEN DATEDIFF(END_DATE, START_DATE) + 1 >= 90 THEN '90일 이상'
WHEN DATEDIFF(END_DATE, START_DATE) + 1 >= 30 THEN '30일 이상'
WHEN DATEDIFF(END_DATE, START_DATE) + 1 >= 7 THEN '7일 이상'
ELSE '해당 없음' END duration_type # 할인하지 않는 경우를 꼭 기억해라. 이것때문에 30분 더 걸림.
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY H JOIN CAR_RENTAL_COMPANY_CAR C
ON H.CAR_ID = C.CAR_ID AND C.CAR_TYPE = '트럭' # CAR TABLE '트럭' 필터링
), TRUCK2 AS(
SELECT
HISTORY_ID,
CASE WHEN T.DURATION_TYPE = '해당 없음' THEN DAILY_FEE * DURATION
WHEN T.DURATION_TYPE IN ('7일 이상','30일 이상','90일 이상') THEN DAILY_FEE * DURATION * (1-discount_rate/100)
ELSE 0 END FEE
FROM TRUCK T LEFT JOIN CAR_RENTAL_COMPANY_DISCOUNT_PLAN P
ON T.DURATION_TYPE = P.DURATION_TYPE AND P.CAR_TYPE = '트럭' # PLAN TABLE '트럭' 필터링
ORDER BY 2 DESC, 1 DESC)
SELECT HISTORY_ID, ROUND(FEE) # ROUNDING을 위한... MULTI CTE 활용
FROM TRUCK2
조건이 많은 문제를 풀 때는 "조건에 해당하지 않는 값" 을 어떻게 처리해야 하는지를 생각하고 쿼리를 작성하자.
위 문제의 경우 할인을 받지 않는 7일 미만의 대여 데이터가 절대 다수였는데 해당 경우를 쿼리에 포함하지 않아 정답 테이블 데이터의 거의 70퍼센트를 날려버릴뻔 했다.
실제 현업 데이터였으면 시말서 감이다. 목적을 분명하게 세운 뒤에 분석하자.
WITH
SUM_2021 AS(
SELECT COUNT(DISTINCT(USER_ID))
FROM USER_INFO # 2021년 가입한 전체 회원 수
WHERE SUBSTR(JOINED,1,4) = '2021') # 158명
,
SUM_PURCHASED_2021 AS(
SELECT COUNT(DISTINCT(S.USER_ID))
FROM ONLINE_SALE S JOIN USER_INFO U
ON S.USER_ID = U.USER_ID # 2021년 가입 회원 중 구매한 회원 수
WHERE SUBSTR(U.JOINED,1,4) = '2021') # 83명
SELECT
YEAR(S.SALES_DATE) YEAR
, MONTH(S.SALES_DATE) MONTH
, COUNT(DISTINCT(S.USER_ID)) PURCHASED_USERS
, ROUND(COUNT(DISTINCT(S.USER_ID))/(SELECT * FROM SUM_2021),1) PUCHASED_RATIO # 비율 구하기
FROM ONLINE_SALE S JOIN USER_INFO U
ON S.USER_ID = U.USER_ID
WHERE SUBSTR(U.JOINED,1,4) = '2021'
GROUP BY
YEAR(S.SALES_DATE),
MONTH(S.SALES_DATE)
ORDER BY 1, 2
비율을 구하는 과정이 복잡하여 with로 각 변수(2021 가입 회원 수, 2021 가입 회원 중 구매 회원 수) 를 각각 구해놓고 시작했다.
한 눈에 보기에도 비효율적이다...
WITH JOIN_2021 AS(
SELECT COUNT(*) USERS FROM USER_INFO
WHERE YEAR(JOINED) = 2021)
,
PUCHASED_2021 AS(
SELECT
YEAR(SALES_DATE) AS YEAR,
MONTH(SALES_DATE) AS MONTH,
COUNT(DISTINCT(A.USER_ID)) AS PUCHASED_USERS
FROM ONLINE_SALE A JOIN (
SELECT USER_ID
FROM USER_INFO
WHERE YEAR(JOINED) = 2021) B
ON A.USER_ID = B.USER_ID
GROUP BY YEAR, MONTH)
SELECT P.YEAR, P.MONTH, P.PUCHASED_USERS, ROUND(P.PUCHASED_USERS/J.USERS,1) AS PUCHASED_RATIO
FROM PUCHASED P, JOIN_2021 J
ORDER BY YEAR, MONTH
def solution(phone_number):
length = len(phone_number) - 4 # 길이 변수 생성
masking_part = '*' * length # 마스킹 파트 변수 생성
last_four_digits = phone_number[-4:] # 뒤 4자리 변수 생성
masked_number = masking_part + last_four_digits # 마스킹 파트 + 뒤 4자리를 통해 마스킹 완료된 값 반환
return masked_number
indexing, 문자열 사칙연산 같은 마법카드를 몇 장 사용하면 다음과 같은 최적화 코드가 나온다.
def hide_numbers(phone_number):
return ('*'*(len(phone_number)-4)) + phone_number[-4:]
나도 개념들이 머리에 자리잡혀 이렇게 간단하게 코드를 짤 수 있기를..
def solution(numbers):
for num in numbers: # 이러면 안돼...문제에 1~10에 없는 값을 number 로 준다고 하잖아.
if num != range(1:10): # range함수 구조: range(시작,미만값,구간) 이다
answer = print(sum(num)) # sum함수는 literable 객체에만 사용 가능하다.
return answer
총체적 난국인 나의 코드를 하나씩 짚어보자.
1. 우선 sum()함수는 literable 객체에만 사용이 가능하다.
2. range() 의 구조는 range(시작값, 미만값, 구간) 이다. ' : '은 대체 어디서 온 걸까.
3. 멤버십 검증을 위해 멤버십 연산자 in or not in 을 사용해야 한다. =, !=은 비교 연산자로 두 값을 비교하는 역할이다.
def solution(numbers):
total_number = sum(range(10))
given_number = sum(numbers)
answer = total_number - given_number
return answer
말 그대로 직관적이며 이해가 쉽지만 변수가 많아 효율적이진 않은 것 같다.
def solution(numbers):
answer = 0
for i in range(1,10):
if i not in numbers:
answer += i
return answer
가장 흔하게 쓰이는 구조이다. 이 구조를 디폴트로 생각할 줄 알아야겠다.
answer = 0 변수 초기화, range()를 통한 구간 생성, 멤버십 연산자 등의 개념을 기억하자.
def solution(numbers):
answer = sum(range(10)) - sum(numbers)
return answer
머리가 좋으면 손가락이 편하다.
def solution(arr):
min_value = min(arr)
answer = arr.remove(min_value) # .remove() 메서드는 arr 리스트를 직접 수정하기 때문에 answer에 아무것도 반환하지 않는다.
if len(arr) < 2:
return [-1]
else:
return answer
내가 answer 변수에 remove()메서드로 최소값을 제거한 arr리스트를 담으려고 한 것에서 오류가 발생했다.
리스트 내 값을 삭제하는 .remove(값)메서드는 리스트를 직접 수정하기 때문에 아무것도 반환하지 않느다.
따라서 .remove()메서드의 결과를 저장하는 answer 또한 null값을 가지게 된다.
def solution(arr):
min_value = min(arr)
arr.remove(min_value)
if len(arr) < 2:
return [-1]
else:
return arr
def solution(s):
answer = 0
if len(s) % 2 != 0:
answer = s[len(s)//2]
else:
answer = s[len(s)//2-1:len(s)//2+1]
return answer
indexing 문제다. 짝수와 홀수의 경우로 나눈 뒤 인덱스 범위를 사용할 수 있는지 확인하는 문제였음. list[이상,미만,구간]을 잘 생각해서 비슷한 문제가 나오면 적극 활용하자.
def solution(n):
if n % 2 == 0:
answer = '수박'*(n//2)
else:
answer = '수박'*(n//2) + '수'
return answer
zip() 함수를 사용하는 문제
def solution(a, b):
answer = 0 # answer 변수 초기화, 반복해서 더해줘야 하니까.
for x, y in zip(a, b): # for i in x: x에서 i를 하나씩 뽑아서 반복
answer = answer + x*y # 반복해서 더해줌
return answer # zip(a,b) 가 key point
아직까지도 for반복문을 바로 사용하지 못한다..
이래서 나중에 크롤링은 어떻게 할 것이며 머신러닝은 어떻게 돌릴거냐?
오늘 순공시간 3시간.. 반성하고 내일은 순공시간을 6시간 이상으로 가져가자.
그래도 zip()함수 하나를 알고 넘어가는 오늘이다.
zip(iterator1, iterator2, iterator3 ...)def solution(left, right):
measure = []
for x in range(left, right+1):
for i in range(1, x+1):
if x % i == 0: # 약수 찾기
measure.append(i)
if len(measure) % 2 == 0: # 약수 개수 점검
return x
else:
return -x
약수 리스트 measure를 만들어 담아낸 후, 해당 약수의 짝/홀 여부를 필터링하다가 실패했다.
feedback
1. 약수의 개수:
measure리스트는 매 반복마다 초기화되지 않아 약수가 계속 누적됨
2. return 위치:
return문이 for 루프 안에 있어서 첫 번째 결과를 계산한 뒤 바로 함수가 종료된다. 즉 누적된 결과를 계산하지 못한다.
def count_division(n): # 약수 개수
count = 0
for i in range(1, n+1): # range(n)은 '0' ~ 'n-1' 까지의 숫자를 생성. i로 나눌 시 ZeroDivisionError 발생, 시작 범위를 정해줘야함.
if n % i == 0: # i 를 계속 1로 써서 모든 경우를 count+1 해버렸다..
count += 1
return count
def solution(left, right):
result = 0 # 자꾸 sum 쓰려고 하는데, for문에서는 sum 말고 변수 선언과 += 를 통해 iteration 별 sum을 수행하라.
for x in range(left, right+1):
if count_division(x) % 2 == 0: # 짝수면
result += x
else: # 홀수면
result -= x
return result
약수의 개수를 구하는 함수를 따로 정의하여 편리하게 사용했다.
sql에서 cte의 역할을 하는 def 함수를 적극 활용하자.
ZeroDivisionError 를 주의하자. range(n)은 '0' ~ 'n-1' 까지의 숫자를 생성한다. 따라서 나눗셈의 경우 시작을 명시해줌으로써 ZeroDivisionError를 피해야 할 것이다.
반복문을 통한 누적 덧셈이 필요할 땐, sum 이 아니라 변수선언과 =+를 통한 iteration별 합계를 사용해야 한다.
def solution(left, right):
answer = 0
for i in range(left,right+1):
if int(i**0.5)==i**0.5: # 제곱수인지 확인!
answer -= i
else:
answer += i
return answer
int(i**0.5) == i**0.5 는 'i'가 제곱수인지 아닌지 확인한다.
"제곱수는 약수 커플 중 쌍둥이를 갖는다."
일반적으로 숫자 n의 약수는 쌍을 이룬다.
ex)
36의 약수: (1, 36), (2, 18), (3, 12), (4, 9), (6, 6)
-> 쌍둥이 커플(6, 6)을 갖고 개수는 홀수이다.
20의 약수: (1, 20), (2, 10), (4, 5)
-> 약수들이 서로 다른 쌍을 이루며 개수는 짝수이다.
def solution(s):
answer = "".join(sorted(s, reverse=True))
return answer
# s: "Zbcdefg"
# 결과: "gfedcbZ"
문자열 매서드 join()과 sorted함수를 적절히 사용하였다.
sort와 sorted의 차이가 헷갈렸다.
이 문제에서는 input값이 str 이라서 sorted()함수를 사용했다.
sort VS sorted
sort: 리스트 객체 메서드. 리스트를 직접 수정.
sorted: 일반 함수. literable 객체(튜플, 문자열..)에 사용 가능. 정렬된 리스트를 반환.
def solution(price, money, count):
fee = 0
for i in range(1, count + 1):
fee = fee + (price * i)
return abs(money-fee)
다 잘 해놓고 문제 마지막 줄 "단, 금액이 부족하지 않으면 0을 return 하세요."를 안읽어서 오래걸렸다.
문제 똑바로 읽어라
def solution(price, money, count):
fee = 0
for i in range(1, count + 1):
fee = fee + (price * i)
if money-fee > 0:
return 0
else:
return abs(money-fee)
절댓값 abs()가 사용된 문제였다.
def solution(s):
if (len(s) == 4 or 6) and s.isdigit():
return True
else:
return False
...틀린이유
1. 컴퓨터 이해 능력 이슈:
len(s) = 4 or 6 을 컴퓨터는 4 or True 로 인식한다.
컴퓨터는 0 아니면 전부 True로 해석하며 병렬정 이해를 하지 못한다.
또한 ==가 or보다 연산 속도가 빠르다.
def solution(s):
if len(s) == 4 or len(s) == 6:
return s.isdigit()
else:
return False # 반드시 명시 필요
오답 원인을 제거하고 len(s) == 6을 명시했다.
또한 len(s) == 5인 경우 True로 해석하기 때문에 return s.isdigit()에서 끝내면 안되고 else조건을 걸어 false조건을 명시해줘야 한다.
def solution(s):
return s.isdigit() if len(s) == 4 or len(s) == 6 else false
: 삼항 연산자를 사용하여 간단하게 풀었다.
def alpha_string46(s):
# 함수가 주어진 문자열 s가 숫자로만 이루어져 있는지와 길이가 4 또는 6인지 확인합니다.
return s.isdigit() and len(s) in [4, 6]
# 테스트 코드
print(alpha_string46("a234")) # False
print(alpha_string46("1234")) # True
and로 두 개의 조건을 연결하고, in 연산자를 통해 문자열의 길이 조건 명시했다.
논리연산자만을 사용한 깔끔한 코드.
def solution(arr1, arr2):
answer = []
for i in range(len(arr1)): # 행의 합
arr_sum = []
for j in range(len(arr1[0])): # 열의 합
arr_sum.append(arr1[i][j] + arr2[i][j])
answer.append(arr_sum)
return answer
WITH
cte1 AS (SELECT a, b FROM table1),
cte2 AS (SELECT c, d FROM table2)
SELECT b, d FROM cte1 JOIN cte2
WHERE cte1.a = cte2.c;
: 하위 쿼리를 정의하고 참조하는 방법
WITH cte_name AS (
SELECT column1, column2, ...
FROM table_name
WHERE condition
)
SELECT column1, column2, ...
FROM cte_name;
: 자기 자신을 반복적으로 호출하여 계층적/반복적 데이터를 처리한다. 엥커, 재귀로 구성됨.
UNION ALL은 모든 결과 행을 포함하는 반면 UNION은 중복된 행을 제거한다. RECURSIVE CTE에서는 보통 의도적으로 중복된 행을 포함하기에 UNION ALL을 사용한다. WITH RECURSIVE cte_name AS (
-- 앵커 멤버
SELECT initial_query
UNION ALL
-- 재귀 멤버
SELECT recursive_query
FROM cte_name
WHERE termination_condition
)
SELECT * FROM cte_name;
0 ~ 10 숫자를 생성하는 재귀CTE 예제
WITH RECURSIVE Numbers AS (
-- 앵커 멤버: 0을 반환
SELECT 0 AS number
UNION ALL
-- 재귀 멤버: number에 1을 더해가면서 10 이하일 때까지 반복
SELECT number + 1
FROM Numbers
WHERE number < 10
)
SELECT number FROM Numbers;
zip(iterator1, iterator2, iterator3 ...)a = ("John", "Charles", "Mike")
b = ("Jenny", "Christy", "Monica", "Vicky")
x = zip(a, b)
#use the tuple() function to display a readable version of the result:
print(tuple(x))
# 결과: (('John', 'Jenny'), ('Charles', 'Christy'), ('Mike', 'Monica'))
sort(): 리스트 객체 메서드, 기존 객체를 정렬
sorted(): iterable 객체(튜플, 문자열 등) 메서드, 정렬된 new 리스트를 반환하며 기존 객체는 변하지 않음
sort, sorted 비교 예시
# sort() 예시 numbers = [3, 1, 4, 1, 5, 9] numbers.sort() print(numbers) # 출력: [1, 1, 3, 4, 5, 9] --- # sorted() 예시 numbers = [3, 1, 4, 1, 5, 9] sorted_numbers = sorted(numbers) print(numbers) # 출력: [3, 1, 4, 1, 5, 9] print(sorted_numbers) # 출력: [1, 1, 3, 4, 5, 9]
numbers.sort(reverse, key)reverse: 오름차순:reverse=False 디폴트. 내림차순:reverse=Truekey: 정렬 기준을 정하는 함수. ex) len, lambda...메서드 (Method): 객체를 대상으로 종속적으로 작동. 점 표기
object.method_name()
함수 (Function): 어디서든 독립적으로 작동하는 코드. 괄효 표기
function()
Functions are independent blocks of code that can be called from anywhere, while methods are tied to objects or classes and need an object or class instance to be invoked
삼항 연산자(ternary operator):
syntax -> True 값 if 조건 else False 값
삼항연산자 활용 예시
ex-1)# 홀/짝 판별 number = 5 result = "Even" if number % 2 == 0 else "Odd" print(result) # 출력: Oddex-2)
age = 18 message = "Adult" if age >= 18 else "Minor" print(message) # 출력: Adult코딩 팁
직접 쓰면서 해봐라.
work flow를 그리면서 하면 훨씬 괜찮다.