2월 25, 2022

[django] westagram포스팅 구현, 로그인 데코레이터 구현

1. 특정 요청을 보낸 유저의 권한을 인가(확인)하는 데코레이터를 구현합니다 

2. westagram이라는 SNS에 게시물을 POST할 수 있는 기능을 구현하려 합니다. 


우선 데코레이터의 코드입니다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import jwt

from django.http import JsonResponse

from my_settings import SECRET_KEY, ALGORITHM
from users.models import User


def trace(func):
    def wrapper(self,request, *args, **kwargs):  
        try:
            access_token = request.headers['Authorization']     
            header = jwt.decode(access_token, SECRET_KEY,  algorithms=ALGORITHM)
            pk = header['id']

            User.objects.get(id=pk)
            request.pk = pk 

            return func(self, request, *args, **kwargs) 

        except User.DoesNotExist:
            return JsonResponse({"message":"Invalid User"}, status = 401)
        except jwt.DecodeError:
            return JsonResponse({"message":"Invalid token"}, status = 401) 
            
    return wrapper

  • trace는 실행되지 않은 wrapper라는 함수를 리턴합니다
  • wrapper가 실행됩니다
  • "access_token"이라는 변수에 요청의 헤더에 담긴 jwt토큰 값을 담습니다
  • 이를 jwt.decode()를 통하여 다시 decode 해주면 유저 id와 값이 담긴 딕셔너리가 나옵니다
  • "pk" 변수에 이 id 값을 담아줍니다
  • 인수로 받은 request라는 인스턴스에 .pk로 변수를 선언하여 pk 값을 저장합니다.
  • 예외처리는 두개를 해주었는데
    • 토큰을 발급받은 유저가 탈퇴 등으로 DB에서 삭제된 경우와(토큰은 여전히 유효)
    • 토큰이 잘못된 경우입니다.
  • 두번째 경우는 jwt.DecodeError를 통하여 예외처리를 해줍니다
  • 첫번째 경우는 User.objects.get(id=pk) 를 통해 나온 값이 없을 때, 토큰은 유효하지만 user가 이미 DB에서 삭제된 경우이기에 User.DoesNotExist를 사용해줍니다

에러가 발생하지 않은 경우, 데코레이터를 받는 함수를 그대로 리턴해줍니다.


다음으로 posting 기능을 구현해줍니다. Django에서는 다루는 데이터의 종류가 달라지는 시점에서 앱을 분리합니다. posting에 관한 데이터는 이전까지 다루지 않던 데이터이니 app을 분리해줘야 합니다. startapp으로 app을 만들어줍니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from djang.db import models
from users.models import User

class Posting(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    content = models.CharField(max_length=100, blank=True)
    class Meta:
        db_table = 'postings'

class Image(models.Model):
        posting = models.ForeignKey(Posting, on_delete=models.CASCADE)
        image = models.CharField(max_length=100)
        class Meta:
            db_table = 'images'

posting은 어떠한 유저가 올린 글이기에, 각 posting은 users app의 User클래스를 정참조합니다

posting 하나에 여러개의 사진이 등록될 수 있기에 images가 postings를 정참조 하도록 모델을 구현합니다. 

created_at은 auto_now_add 속성을 통하여 글을 올리는 시간을 기록해줍니다. 

그런데 이렇게 하니 updated_at도 같이 생성됩니다. 



PostingView는 posting과 담당된 기능을 총괄하는 클래스입니다.

우선, post함수를 구현하여 포스팅을 올릴 수 있게 해주고, 추후에 get으로 이를 받아 올 수 있도록 구현하겠습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import json

from django.http            import JsonResponse
from django.views           import View

from postings.models import Posting,Image
from postings.utils import trace
from users.models import User
# Create your views here.

class PostingView(View):
    @trace
    def post(self, request):
        data = json.loads(request.body)

        user_id = User.objects.get(id=request.pk) 
        content = data.get("content", '')
        image = data["image"]
 
        post = Posting.objects.create(
            user = user_id,
            content = content
        )

        for i in image:
            Image.objects.create(
                image = i,
                posting = post #Posting 객체를 넣어줬는데 알아서 테이블에 객체의 id값이 들어가는지? 
            )

 

        return JsonResponse({'message':'SUCCESS'}, status=201)

글을 올리려는 유저가 유효한 토큰을 가지고 있는지 확인하기 위하여 @trace로 데코레이터를 먼저 선언해줍니다.


앞서 데코레이터의 reqeust인스턴스에 pk라는 변수를 저장해주고, 이를 함수의 인자로 리턴해주었죠? 이를 이용하여 user FK컬럼에 글을 올리는 유저의 id값을 넣어줍니다.

이미지를 여러개 유저가 보내주었을시, 이를 각각 images 테이블에 넣기 위하여, 이를 for문을 통하여 각각 테이블에 넣어줘야 합니다.

앞서 만들어진 포스팅에 이미지가 들어가야 하므로, 방금 만들어진 포스팅 인스턴스를 담은 "post" 변수를 그대로 넣어줍니다. 

주석을 달아놓았듯이 Image 객체를 만들어줄때, posting값에 post라는 인스턴스를 그대로 할당하면 , sql상의 posting_id에는 해당 인스턴스의 id값이 알아서 잘 들어갑니다.






2월 23, 2022

[WSL] 외부 네트워크와 통신 불가한 문제해결

 장고로 구현한 로그인/회원가입을 프론트와 합쳐서 기능을 구현해보는 세션에서 모든 윈도우 유저가 통신이 되지 않았다. 에러가 난 것도 아니고, 그냥 프론트에서 POST로 값을 보내도 아무런 응답이 발생하지 않았다.


https://webisfree.com/2021-07-14/wsl2-외부-remote-ip-접속-가능하도록-설정하기-방화벽-해제

https://blog.dalso.org/linux/wsl2/11430

https://codeac.tistory.com/118

https://github.com/microsoft/WSL/issues/4150

해당 링크들을 참조하여 포스팅을 작성했습니다.


내 로컬(윈도우)와 WSL(우분투)가 연결 돼 있지 않아 생긴 문제였다.

  • 로컬로 온 요청이 WSL포트로 전해지지 않았기에 이를 이동시켜 줄 방법이 필요했다.
2월 23, 2022

[인증&인가] JWT / [장고]DoesNotExist

 JWT는 authorization, 즉 인가를 위한 것입니다.


jwt를 이용한 authorization 과정입니다. 서버에 저장되는 것은 없습니다.


반면 기존의 방식은 서버 메모리에 user 정보를 저장합니다


왜 이렇게 jwt를 쓸까요?

대부분의 경우 사이트는 한개의 DB만을 가지고 있지 않습니다. 은행을 예시로 들면, 뱅킹에 대한 서버와- 은퇴자들의 정보를 모아놓은 서버가 별도로 존재할 수 있습니다.


이 상황에서, 각 서버에 따로 유저의 인가 정보를 저장한다면, 유저는 backend에서 서버를 이동(유저는 본인이 서버를 옮겨가는지도 모를 가능성이 높습니다) 할 때마다 로그인을 다시 해줘야 하는 불편함을 겪습니다. 

따라서 client가 마치 같은 서버 안에 있는 것처럼, 두 서버를 자유자재로 왔다갔다 하면서 접속할 수 있게 해주려면, JWT를 사용해줘야 합니다. JWT를 사용하면서 Bank와 Retirement서버가 같은 SECRET_KEY를 공유한다면, 클라이언트는 자유자재로 jwt를 가지고 서버를 왔다갔다 할 수 있기에 매우 편리할 것입니다.


SECRET_KEY가 갑자기 왜 나올까요..?

이것이 JWT의 구조입니다.

  • 헤더에는 알고리즘과 토큰의 타입에 대한 정보가 들어갑니다. 즉, 어떤 타입의 데이터인지, 해시 알고리즘 정보는 무엇인지의 정보가 들어가는 것입니다.
  • 내용에는 실제 보관할 데이터를 담습니다. 여기에는 JWT의 만료시간을 나타내는 exp와 같은 registered claim, 공개용 정보 전달 목적의 public claim, 클라이언트와 서버간 협의 하에 사용하는 private claim등이 있습니다.(유저의 ID, iat-토큰 만들어진 시간, exp 등)
  • 위의 두 가지 요소는 Base64방식으로 인코딩(암호화 아닙니다) 돼 있기에, 누구나 그 안의 정보를 볼 수 있기에, 개인정보를 담아서는 안됩니다. 그래서 유저의 이름 등의 값이 아닌, 보안이 지켜지는 DB안의 PK값을 담는 것입니다,
  • 시그니처는 토큰이 바뀌지 않았다는 것을 verify하는 역할입니다. 시그니처 덕분에 유저는 데이터를 보기만 할 수 있고, 서버에만이 이를 수정할 수 있게 됩니다.
  • 헤더와 내용(페이로드)를 조합하고, SECRET_KEY를 이용하여 해시 값을 만들어 냅니다. 이것이 바로 시그니처입니다. 따라서 SECRET_KEY는 과제에서처럼 외부에 노출되지 않게, my_settings등에 따로 보관하며 사용시에는 import해서 사용합니다.
  • 이 시그니처의 비교를 통해 서버는 유저를 verify합니다. 시그니처가 바뀌었다면, 토큰에 변화가 있다는 뜻입니다.
  • 이때, 사용하는 알고리즘 값도 깃헙 등에 올라가서는 안되는 비즈니스 로직이기에, my_settings에 따로 보관해주는 것이 맞습니다.
    • 알고리즘 값은 상수이기에 컨벤션 상, 대문자로 상수명을 지어야 합니다.

2월 22, 2022

[django] westagram mission 5~6 를 진행하며 겪은 문제들(회원가입 비밀번호 암호화, 로그인시 JWT 발급)

 



미션 5 코멘트에 이런 과제를 주셨습니다.

  • phone_number가 꼭 들어가지 않아도 되도록 model을 수정하고
  • phone_number가 없어도 키에러가 나지 않고, 디폴트 값으로 무언가를 불러오도록 해주면 될 듯합니다
첫번째로, models.py의 phone_number 컬럼의 속성으로 null=True를 줄 지, blank=True를 줄 지 고민해보았습니다
  • null=True: 필드 값이 NULL로 저장되는 것 허용. DB의 열에 관한 설정.
    • 주어진 DB컬럼이 NULL값을 가질 수 있는지 정의
  • blank=True: 필드가 폼(입력 양식)에서 빈채로 저장되는 것 허용
    • 장고 관리자 및, 유효성과 관련돼 있습니다
    • blank가 False이면 해당 필드 없이 정보를 저장하고자 하면 유효성 검사에서 걸러지게 됩니다
    • blank=True로 할 시, 해당 필드가 없어도 정상적으로 동작합니다.
    • blank=True는 장고에 관련된 속성이기에 DB에 아무 영향도 주지 않습니다.
  • CharField, TextField에서는 null=True 를 설정할 시 문제가 생깁니다. Null의 "데이터 없음"이 문자열에서는 "빈 문자열"로 표현될 수 있기에. [데이터가 없는 상태]에 대해서 두가지 중복된 값을 가질 수 있게 됩니다. 
  • 따라서 CharField, TextField에는 null=True가 아닌 blank=True를 주는 것이 옳습니다

음... phone_number에 blank=True 속성을 추가해주면 될 듯 합니다.
메인에서 model만을 수정한 코드를 담은 브랜치를 따로 판 이후(현업에서는 모델을 수정할 때 무조건 이렇게 따로 브랜치를 파서 관리해줘야만 한다고 합니다) , 이를 PR후 merge받아서 main에서 migration을 시행하는데!  


에러가 발생합니다. users라는 테이블이 이미 있다고 합니다.
blank=True는 어차피 DB스키마에 영향을 미치지 않기에 migrate 해줄 필요가 없나 했더니, 그것 아닌가 봅니다


마이그레이션 실행자의 내부적인 모델 상태 그래프가 최신상태로 남기 위해 migration이 필요하다고 합니다, migration을 했으니 migrate를 하기 위해 다시 에러를 확인해봅니다.

해결법을 구글링하니 세가지 정도가 나옵니다.
  1. drop으로 users테이블 날린다
  2. python manage.py migrate --fake <appname> 으로 fake migrate한다
  3. 마이그레이션 파일 편집
3번을 하려고 0003_user_delete_users.py 파일을 열어봤습니다

db에 'users'라고 저장되는 테이블을 만든 이후, "Users"모델(db에선 'users'임)을 지워주려 합니다. 
지우고 -> 새로만들어야하는데
새로만들고 -> 지우려니 똑같은걸 또 만드려고 하는 에러가 나는 듯 합니다.

따라서 DeleteModel 부분을 CreateModel위로 올려줬습니다. 문제는 이렇게 수정하게 되면 DB가 날아가게 됩니다. 1번처럼 drop으로 테이블을 날린 것과 차이를 모르겠습니다.
DB에 별 정보가 없어서 망정이지, 매번 이렇게 할 수는 없을 듯 합니다.

다음번에 같은 에러와 마주한다면 fake migrate도 한번 시도해봐야겠습니다.

  • phone_number가 없어도 키에러가 나지 않고, 디폴트 값으로 무언가를 불러오도록 해주면 될 듯합니다
이를 해결해주기 위해 views에서 phone_number를 저장할 때 ,phone_number   = data["phone_number"],
가 아니라


이렇게 딕셔너리의 get메소드를 사용해주었습니다.
get메소드는 해당 키가 존재하지 않을 경우 , 디폴트 값을 저장해줍니다(아까 blank=True 해줬던거 기억하시죠?) 오른쪽의 빈 칸이 디폴트입니다.

어라? 근데 이렇게 저장하니 키에러가 뜹니다.
알고보니 "" 이렇게 디폴트 값으로 "빈 값"을 명시해줘야만 했습니다. 이렇게 하면 클라이언트가 전화번호 키 자체를 주지 않아도 해당 키에 디폴트로 ""값이 들어갑니다. 








2월 22, 2022

[인증&인가] bcrypt

bcypt라는 salting과 key stretching(해싱을 여러번 반복함)을 담당하는 라이브러리를 사용하여 사용자가 입력한 password의 암호화를 진행합니다.



bcrypt는 "해싱 알고리즘" , 즉 일종의 블랙박스 같은 것입니다.


'hello'라는 string을 넣으면, 블랙박스를 거쳐서 랜덤한 값이 나옵니다.
여기서 hello를 몇번 넣던간에. 블랙박스는 항상 같은 해쉬값을 뱉어냅니다.
그러나 역으로 해쉬값 -> 'hello'로 거슬러 올라가기는 불가능합니다.

bcrypt는 고의적으로 느리게 설계됐습니다. 이는 가끔식 DB가 공격당할 수 있기 때문입니다.
좀 더 자세히 설명해보겠습니다.
대부분의 사람은 각기 다른 비밀번호를 기억하기 힘들기 때문에, 비슷한 패턴 혹은 완전히 똑같은 비밀번호를 이 사이트, 저 사이트에서 돌려쓰고는 합니다.
이렇게 같게 설정된 비밀번호는 여러 사이트의 DB에 저장돼 있습니다.

만약 해커가 이들 중 한 사이트의 DB에 접근하는 데 성공했다면 어떻게 될까요?
해커는 DB에서 얻어낸 pw로, 이를 여기저기 다른 사이트에서 사용할 수 있습니다.

따라서, 해커가 DB에 접근해도 사용자의 비번을 알 수 없도록 비밀번호를 암호화해서 DB에 저장해야 합니다. 이때 해싱을 하기 위해 bcrypt를 사용합니다.
이제 해커는 DB를 얻어내도, 해싱된 pw값만을 가지고 있습니다. 

그러나 이것도 완벽한 보안 방법은 아닙니다.
해커들은 password와 그것의 bcrypt hash를 가지고 Dictionary attack 이라는 것을 합니다.
Dictionary와 훔쳐낸 db를 일일이 대조하며, 해싱값에 일치하는 password를 찾아내는 것이죠
따라서 , 이를 방지하기위해, password를 바로 해싱하지 않고 salt라는 것을 씁니다.

salt는 단순 스트링입니다.

password + salt를 해싱하고, 이것을 DB에 저장한다면 기존의 딕셔너리 공격은 통하지 않습니다.

그러나 해커가 salt값에도 접근하게 된다면, 해커는 이 salt값을 가지고 또 다른 딕셔너리를 만들어 공격을 시도할 수 있습니다. 

그러나, 앞서 설명했듯이 bcrypt는 매우 느리기에, 이 딕셔너리를 만드는데 시간이 한참 걸립니다. 그래서 공격이 무의미하게 만들어버리죠. 이것이 bcrypt를 굳이 느리게 만든 이유입니다.


  • bcrypt는 str이 아닌 Bytes 형태의 데이터를 암호화하기에, pw를 암호화할시 bytes로 만들어줘야 합니다.
  • 이때 encode()를 사용하는데, 알아볼 수 있는 형태로 만들기 위해 utf-8 유니코드 문자 규격을 사용합니다
  • .encode() 의 기본값은 'utf-8'입니다
  • str을 encode하면 bytes / bytes를 decode하면 str입니다
  • 암호화를 마친 데이터의 타입 또한 bytes입니다.
  • 암호화한 비밀번호가 맞는지 확인하기 위하여
    • bcrypt.checkpw(사용자로부터 입력받은 패스워드, 저장된 암호화된 패스워드)
    • 이때 두 인자의 데이터 타입은 모두 Bytes여야 합니다

2월 22, 2022

[CodeKata] 배열에서 과반수가 넘는 숫자

 첫번째 풀이

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def more_than_half(nums):
    # 아래 코드를 입력해주세요.
    dic = {}     # 숫자와, 그것의 출현횟수 알기 위해 딕셔너리만듬
    for i in set(nums): # 배열 안 숫자들(집합)을 for로 돌려서,
      dic[i] = nums.count(i) 
      # 숫자와(키), 그것의 출현횟수(값) 딕셔너리에 담음
    max_value = max(dic.values()) # 딕셔너리 값들 중 제일 큰 값
    for j in dic.keys(): # 딕셔너리의 키들 중 
      if dic[j] == max_value: # 해당 딕셔너리키가 제일큰값의 키면
        return j  # 리턴해라 조

좀 난잡하게 풀었습니다. set로 nums안 숫자를 추린후, 딕셔너리에 숫자와 그것의 출현횟수를 담고, 딕셔너리의 키 중 가장 큰 value를 가진 키를 리턴해주었습니다.


두번째 풀이 

1
2
3
4
5
def more_than_half(nums):
  dic = {}
  for i in set(nums):
    dic[i] = nums.count(i)
  return [k for k,v in dic.items() if max(dic.values()) == v][0]

풀이 방식은 같은데, 코드가 훨씬 깔끔해졌습니다.

특히, 딕셔너리에서 가장 큰 값을 가진 키를 불러오는 법을 리스트 컴프리헨션 을 가지고 수행했는데, 신선한 접근이었습니다. 

리스트 컴프리헨션을 만들며 고생을 좀 했는데, 왜냐면 k,v in dic 이런식으로 dic.items()가 아닌 딕셔너리 자체를 썼기 때문이다.


dic.itmes()를 프린트해보면 이와 같습니다. 리스트 안에 튜플이 담기는데, 튜플의 앞의 값은 키이고/ 뒤의 값은 1입니다.

만약 이 튜플값을 리턴하고 싶으면, 

def func_1(num1,num2): return num1,num2 result1, result2= func_1(10,20) print(result1) >>10 print(result2) 
>>20 

이런식으로 각각의 변수에 담아 서 각각의 값을 리턴 받을 수도 있다.
2월 20, 2022

[django][westagram] 로그인 기능 구현

 로그인 기능을 구현하면서 개인적으로 겪은 실수들을 기록합니다.


KeyError를 except구문을 통해 처리하려했는데, KeyError가 발생해도 except에 따른 코드가 도무지 실행되지 않습니다.



이렇게 try문 안에 login_email과 password 변수를 넣어주니 except에 따른 코드가 작동합니다.
try 구문에 따른 except가 작동하려면... try안에서 시도한 무언가가 실패해야 except로 코드가 넘어가는 것인데... try 밖에서 이미 KeyError가 났으니 except가 실행될리가 없죠... 아주 바보같은 실수지만 몇 시간동안 골머리를 앓았습니다ㅎ..

저번에는 함수 안에서 return이 되면서, 나머지 코드와 상관없이 함수가 끝나는 것을 모르고 실수를 했는데, 이런 실수는 하면서 고쳐가는 거겠죠..? 맞으면서 배우나 봅니다.

password에 대한 INVALID_USER 처리는 try안의 if문을 통해서 했지만, email에 대한 INVALID_USER처리는 except ObjectDoesNotExist: 를 통해 해주었습니다.

이메일이 틀렸을 경우에는 password에 대한 오류 처리처럼 

Users.objects.get(email=login_email)

를 통해 맞는 쿼리를 get 해올수가 없습니다. 


이렇게 DoesNotExist에러가 나게됩니다. 따라서 해당에러를 except문을 통해처리해줬습니다.

따라서, 이메일이 틀리거나, 이메일과 패스워드가 모두 틀린경우에는 except ObjectDoesNotExist를 통하여 에러처리를 하게됩니다.

2월 20, 2022

[django][westagram] 회원가입 기능 구현 : 이메일, 비밀번호 검증 정규식 / 비밀번호 앞에 @ 붙일시 에러 / django의 exist()

email_regex = '^[a-zA-Z0-9+-\_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$’

이메일 정규식은 다음과 같습니다.

정규식에서 [] 대괄호는 안에 있는 값들 중 하나라도 만족하는 것을 찾을 때 이용합니다.

대괄호 "안에" [^a-z] 이렇게 ^를 붙여주면 대괄호 안에 있는 아이들을 제외한 값을 찾아달란 뜻입니다.

반대로, 정규식 맨 처음의(대괄호 안에 들어있지 않은) ^는, 문장에서 시작하는 아이를 지정한다는 뜻입니다.

특정 문자열 다음의 + 는, 하나 있거나, 그 이상(one or more)을 의미합니다.

종합해서 보자면, 

^[a-zA-Z0-9+-\_.]

는 문장의 맨 앞에 영문 대소문자, 숫자, +-/_. 들 중 만족하는 것 하나라도 있는경우 를 따져라. 라는 뜻입니다

그 이후 @ 가 오고, 같은 패턴의 대괄호가 반복됩니다.

그 이후 \.(역슬래쉬 .) 이 오는데요 바로 . 을 찍으면 그것이 정규식에서 어떠한 기능을 할 수 있기에, 단순히 . 문자 자체가 온다는 것을 표현하기 위해 \ 뒤에 . 을 찍어주었습니다.

마지막 대괄호 뒤에 오는 $는 문장의 끝임을 알려주는 기호입니다


password_regex = (?=.*[A-Za-z])(?=.*\d)(?=.*[$@!%*#?&])[A-Za-z\d$@!%*#?&]{8,}

비밀번호 정규식입니다. 이 친구가 좀 어렵습니다. 


"비밀번호는 8자리 이상의 문자,숫자,특수문자의 복합" 이라는 조건에 대한 정규식입니다.

보이는 대로 해석을 해보면 그룹() 이 세개이고, 대괄호 안에는 영문 대소문자와 \d(숫자), 그리고 특수문자가 있습니다

(?=expr)

이 아이는 positive lookahead입니다.

Positive lookahead assertion. 이것은 포함된 정규표현식(expr)이 현재위치에서 성공적으로 match되면 성공하고, 그렇지 않으면 실패합니다. 그러나, 포함된 정규표현식(expr)이 시도된 이후, matching engine은 advance하지 않습니다; 나머지 패턴은 assertion(참이어야 한다고 생각한 사항을 표현한 논리식)이 시작한 곳에서 시도됩니다(tried). 


정규표현식 문서를 그대로 직역했습니다

. 은 모든 문자이고,

* 는 zero or more(없거나 있거나 많거나) 입니다

정리하자면, positive lookahead는 뒤의 표현식이 어떠한 문자열에 있는지 사전검증 하는 역할이고, 뒤의 패턴은 

어떠한 문자가 오거나 오지 않은 이후, 영문 대소문자가 있는지 확인합니다.

이 lookahead 기능을 여러번 사용하면 정규식의 AND연산을 수행할 수 있습니다.

(?=expr1)(?=expr2)(?=expr3) 이런식으로 

1. positive lookahaed를 통해 표현식에 해당하는 문자들이 있는지 찾습니다

2. 찾았지만 이를 포함하지 않습니다(advance 하지 않습니다)

3. 찾은 이후 뒤의 정규표현식을 실행하는데, 그 뒤의 정규표현식이 전방탐색에서 찾은 문자를 포함하면 이를 저장하고, 그렇지 않으면 저장하지 않습니다.

3. 이런식으로 패턴의 일치를 연속적으로 확인합니다 


따라서 위의 식은,

[A-Za-z\d$@!%*#?&] 

이 대괄호에 해당하는 문자열이, (?=) 안에 있는 패턴 세개를 포함하는지 검증하고, 

문자열의 길이는 8자 이상 {8,} 이어야 한다는 뜻인듯 합니다. 


저도 아직 정확히 이해하지 못했지만, 나름대로 이해한 것을 기술해 보았습니다.


2월 20, 2022

ssh로 클론 받은 이후 origin push시 오류 발생

git push origin feature/yeongseo-initial-setting

깃헙 remote repo에서 ssh방식으로 clone을 받은 후, 해당 명령어로 origin에 push를 진행하려 했을 때 다음과 같은 오류를 마주했습니다.


no such identity: /home/park/.ssh/id_ed25519: No such file or directory 

[git@github.com]: Permission denied (publickey). 

fatal: Could not read from remote repository.

Please make sure you have the correct access rights and the repository exists.


.ssh/id_ed25519라는 파일 또는 디렉토리가 없다고 합니다

없으면 만들어주거나, 키를 가져오는 경로를 변경해야겠죠


경로 변경하는 법을 몰라서 만들어주기로 했습니다


https://docs.github.com/en/authentication/connecting-to-github-with-ssh/checking-for-existing-ssh-keys

깃허브의 설명에 따르면, GitHub을 위한 public keys의 filenmaes는 이 세개중 하나랍니다



저 같은 에러를 겪고 있는 사람들을 위한 설명도 밑에 있습니다. 새로운 SSH키를 만들라네요.


ssh-keygen -t -ed25519


이 명령어로 새로운 ed25519키를 만들어줍니다


~./ssh 안에 id_ed25519의 개인키와 pub키가 생성됐길래, 이제 될줄 알고 push를 다시 해줬습니다. 

그런데 이번엔 permission denied가 뜹니다.


만들기만 하고 키들을 등록해주는 것을 깜빡했습니다.


cat명령어를 통해 공개키를 확인하고, 깃허브에 이를 등록해줍니다.


ssh-add ~/.ssh/id_ed25519

위의 명령어로 개인키 또한 ssh-agent에 등록해줍니다. 


이제 push를 진행하니 문제없이 잘 됩니다.



2월 20, 2022

[Django] CRUD assignment1 review

 우선, 과제의 요구사항이 이러했는데, 제가 아주 큰 실수를 했습니다


저는 master브랜치에서 feature/owner 브랜치를 만든 이후, 필수 과제를 모두 진행한 이후, feature/owner 브랜치에서 바로 check out -b 명령어로 feature/movie 브랜치로 이동했습니다. 

이렇게 하게 되니, feature/movie 브랜치에는 owners app 과 movies app이 공존하게 되는 상황이 벌어졌습니다. 결국 해당 브랜치를 삭제하고, master 브랜치로 이동했음에도 


이런식으로 pycache파일이 여전히 남아있는 문제가 발생했습니다. remote에서 pull을 받아와도 문제는 해결되지 않았습니다. 결국은 새로운 디렉토리를 파서 거기서 remote의 master 브랜치를 pull받은 이후, 새로운 feature/movie 브랜치를 생성하는 식으로 과제를 진행했습니다.

branch를 만들때는 무조건 master(main)브랜치로 이동하여 브랜치를 만들기!! 라는 큰 교훈을 얻었습니다...


실수한 점 2. 



이 요구사항을 보고, 주인의 - post / 주인의 - get / 강아지의 - post / 강아지의 - get 

이렇게 클래스를 총 네개로 나눠서 만들 생각을 했습니다.

그러나 views의 각 클래스는, 어떠한 자원(데이터)를 다룰지 결정하는 것입니다. 

예를 들어, owner클래스가 있다면, owner클래스는 주인에 대한 데이터를 클라이언트에게 줄건지(GET), 클라이언트가 준 데이터를 담을건지(POST)를 결정하는 것입니다.

담거나, 주거나 하는 각각의 기능들은 함수로 이뤄지고, 함수의 이름들은 http요청들의 이름과 똑같아야 합니다. 이는 urls.py에서 as_view() 함수를 사용하는 경우, as_view()가 http method와 , class안의 메소드의 이름을 비교해서, 똑같은 것을 실행할 수 있도록 해주기 때문입니다.

정리하자면, 주인에 대한 데이터를 다루는 OwnersView 클래스와 / 강아지에 대한 데이터를 다루는 DogsView 클래스를 나눠준 뒤, 각각의 클래스 함수에 get과 post함수를 구현해주면 되는 것입니다.


OwnersView클래스의 get함수를 구현하면서 겪은 시행착오들입니다.

주인 한명당, 하나의 age/email/name만 나오고, 강아지의 이름과 - 나이만 리스트로 나오게 하고 싶었습니다.



첫번째 시도입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def get(self, request):
        owners = Owners.objects.all()
        results = []
        dog_dictionary = {}
        # dogs = []
        for owner in owners:
            results.append(
                    {
                        "name" : owner.name,
                        "email" : owner.email,
                        "age"   : owner.age,
                    }
            )        
            for owner_dog in owner.dogs_set.all():
                dog_dictionary["name"] = owner_dog.name,
                dog_dictionary["age"]   = owner_dog.age,
            results.append(dog_dictionary)
        return JsonResponse({'results':results}, status=200)

{
    "results": [
        {
            "age": 25,
            "email": "1park4170@gmail.com",
            "name": "박영서"
        },
        {
            "age": [
                11
            ],
            "name": [
                "김치"
            ]
        },
        {
            "age": 35,
            "email": "mutd@gmail.com",
            "name": "이승준"
        },
        {
            "age": [
                11
            ],
            "name": [
                "김치"
            ]
        }
    ]
}

return 이후는 요청을 보내본 결과입니다.

데이터 상 "이승준"은 가지고 있는 강아지가 없음에도 "박영서"의 강아지와 함께 나오고, 강아지의 나이와 이름도 불필요하게 리스트에 담겨져 나옵니다. 심지어 "박영서"가 가진 강아지는 세마리인데, 마지막 한마리만 나오고 있습니다.

우선 마지막 한마리만 나오고 있는 이유는, for문이 돌면서 dog_dictionary의 키 값에 해당하는 value값을 계속 바꿔주기 때문입니다. 값이 누적돼서 쌓이는 것이 아니라, for문이 돌때마다 값이 바뀌게 되는 것이죠.

왜 강아지가 없는 승준이도 강아지를 갖게 된 것일까요?

자세히 보니 두번째로 나온 11살의 김치란 강아지는 승준이의 강아지로 나온것이 아닙니다. 

dog_dictionary는 for문의 바깥에 이미 선언돼 있는 딕셔너리입니다. 첫번째 주인인 "박영서"의 강아지 세명 중 마지막 아이의 값이 반영된 이후, dog_dictionary의 값은 그대로 변함없이 존재합니다. 왜냐면, 승준이가 owner인 상황에서, owner_dog라는 것은 존재하지 않기에, owner_dog.name이라는 값이 들어갈 수 없는 것이죠. .name 속성을 지닌 객체 자체가 없기에 값이 들어갈 수가 없습니다.

 따라서 이전의 "박영서"owner에서 만들어졌던 dog_dictionary는 그대로 존재하다가 results.append(dog_dictionary)코드에 의해 한번 더 results리스트 안에 append 되게 됩니다.


왜 dog_dictionary의 age와 name 키의 값은 리스트에 담겨서 나올까요?

....이에 대해서는 멘토님께 여쭤봤는데도 명확한 답이 나오지 않았습니다. 추가적으로 고민하고 서치해보고 업데이트 하겠습니다




다음 시도한 get함수의 코드와 결과는 다음과 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def get(self, request):
        owners = Owners.objects.all()
        results = []
        #dog_dictionary = {}
        dogs = []
        for owner in owners:
            results.append(
                    {
                        "name" : owner.name,
                        "email" : owner.email,
                        "age"   : owner.age,
                    }
            )        
            for owner_dog in owner.dogs_set.all():
                dog_attribute = []
                dog_attribute.append(owner_dog.name)
                dog_attribute.append(owner_dog.age)
                dogs.append(dog_attribute)
            results.append(dogs)

        return JsonResponse({'results':results}, status=200)


{
    "results": [
        {
            "age": 25,
            "email": "1park4170@gmail.com",
            "name": "박영서"
        },
        [
            [
                "마파",
                9
            ],
            [
                "두부",
                10
            ],
            [
                "김치",
                11
            ]
        ],
        {
            "age": 35,
            "email": "mutd@gmail.com",
            "name": "호날두"
        },
        [
            [
                "마파",
                9
            ],
            [
                "두부",
                10
            ],
            [
                "김치",
                11
            ]
        ]
    ]
}

이번에도 강아지가 없는 주인에게 강아지가 나옵니다.

이번에도 강아지가 없는 주인에게 강아지가 나온 것이 아니라, 강아지가 있던 전 for문의 주인에서 만들어졌던 dogs 리스트가, 강아지가 없는 주인이 for문을 도는  경우에 수정되고 있지 않다가, results.append(dogs) 구문으로 인해 results리스트에 또 더해져서 그렇습니다


3번째 시도입니다

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def get(self, request):
        owners = Owners.objects.all()
        results = {}
        #dog_dictionary = {}
        try:
            for owner in owners:
                results["name"] = owner.name
                results["email"] = owner.email
                results['age']    = owner.age  
                for owner_dog in owner.dogs_set.all():
                    results["dog"]=(owner_dog.name, owner_dog.age)

            return JsonResponse({'results':results}, status=200)
        except KeyError:
            for owner in owners:
                results["name"] = owner.name
                results["email"] = owner.email
                results['age']    = owner.age  
            return JsonResponse({'results':results}, status=200)

결과
{
    "results": {
        "age": 35,
        "dog": [
            "김치",
            11
        ],
        "email": "mutd@gmail.com",
        "name": "이승준"
    }
}

이번에는 강아지가 있는 영서는 결과에 나오지도 않고, 강아지가 없는 승준이는 강아지 정보와 함께 출력됩니다. 게다가 첫번째 시도와 마찬가지로 강아지의 정보가 리스트에 담겨서 나옵니다.

일단, 영서가 나오지 않고 승준이만 나오는 것과, 마지막 강아지인 김치만 출력되는 것은 1번의 문제와 같은 원인입니다. results 라는 딕셔너리는 for 문 바깥에서 별도로 존재합니다. 

이 별도로 존재하는 딕셔너리의 'name' , 'email', 'age' 값에 각 주인들의 값이 들어왔다가, 교체되는 것이죠. 'dog'의 경우도 강아지가 있는 첫번째 주인이 for문을 돌 때 키값에 저장된 값이 그대로 남아있다가 return되는 것입니다.

except KeyError의 경우도 적용될 일이 없는 코드입니다. 왜냐하면 키에러는 딕셔너리의 키 값이 없어서 생기는 에러인데, 해당 코드의 강아지를 가져오는 for 문에서는 "dog"키값에 대한 값이 이미 존재하고, 강아지가 없는 주인이 for문을 돌 경우 값이 그대로 남아있기 때문입니다.



2월 17, 2022

[Django] ForeignKeyField on_delete

ForeignKey는 relation이 작동하는 방식을 정의하는 arguments를 받습니다.

ForeignKey로 참조되는 객체가 삭제됐을때, 장고는 on_delete argument가 지시한 대로 SQL동작을 모방할 것입니다. 

on_delete의 가능한 값들은 다음과 같습니다.

  • CASCADE
장고는 참조 된 오브젝트가 삭제되면 ForeignKey를 가지고 있는 object들(해당 오브젝트를 참조하는 오브젝트들) 을 삭제합니다. - 

  • PROTECT
참조된 객체의 삭제를 차단하고 ProtectedError를 발생시킵니다. 삭제하려면 수동으로 참조하는 모든 객체를 삭제해야 합니다.
  • RESTRICT
PROTECT와 유사한 동작입니다. RestrictedError를 발생시켜 참조된 개체의 삭제를 방지합니다. 
  • SET_NULL
ForeignKey를 null로 둡니다. 이 값은 필드에 null 이 입력 가능할때(null=True)에만 사용할 수 있습니다. 
ex) 주석이 사용자를 참조할 때, 사용자가 삭제되더라도 사용자가 게시한 주석은 익명의 사용자가 게시한 것으로 남아있습니다.
  • SET_DEFAULT
ForeignKey의 디폴트값을 설정합니다. 디폴트값은 반드시 설정돼야 합니다.
  • SET()
ForeignKey의 값을 SET()함수에서 설정한 대로 설정합니다. 
  • DO_NOTHING
말그대로 어떠한 액션도 취하지 않습니다. 참조 무결성을 해칠 수 있어서 권장되지 않습니다. 
실제로는 존재하지 않는 개체를 참조하는 문제가 발생할 수 있다는 뜻입니다.

2월 16, 2022

[CodeKata] 중복되지 않은 알파벳으로 이루어진 제일 긴 단어의 길이를 반환

 문제는 이러합니다.


풀이 1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def get_len_of_str(s):
    # 아래 코드를 작성해주세요.
    lst = []
    if s == "":
        return 0
    else:
        lst.append(s[0])
        longest = []
        for i in s[1:]: 
            if i in lst:
                longest.append(len(lst))
                lst = [i]
            elif i not in lst:
                lst.append(i)
                longest.append(len(lst))
        return max(longest)저

접근 방법:

for 문을 통해 s의 각 요소들을 리스트에 넣어주다가, 중복되는 값이 나올 시, 리스트의 길이를 저장해 준 뒤, 리스트를 초기화하고 중복되는 값부터 새롭게 추가되는 리스트를 추가하려 했습니다.


우선, 요소들을 넣어줄 lst라는 리스트를 만들어줍니다. 

원래는 else: 문 이하의 코드만 만들었는데요, s가 빈 리스트인 경우에는 7번째 줄의 

lst.append(s[0])이 Index out of range 에러가 나게 됩니다. 따라서 if와 else로 s가 빈 문자열인 부분과, 아닌 부분을 분기해주고 시작합니다.

s가 0이 아니라면, lst라는 리스트에, 문자열 s의 첫 번째 값을 넣어줍니다.

여기서 실수를 하나 했습니다.

lst.append([s[0]])


수를 하나 했습니다. 이런식으로 s의 첫번째 값을 리스트 안에 넣어서 lst에 append 해주는 바람에, 나중에 for문에서 lst안에 i 값이 있냐를 따질때 문제가 발생했습니다.
예를들어 [['a']] 면 리스트 안에 ['a']가 있는 것이지 'a'가 있는 것이 아니잖아요? 그래서 고생을 좀 하다가 바깥의 []를 제거해줌으로써 문제를 해결할 수 있었습니다.
또한, 짤리기 전까지의 각 리스트의 길이를 비교해주기 위해 longest란 리스트를 만들었습니다.

다음으로 for문을 돌려주어서 i가 lst안에 있나 / 없나를 확인합니다.
문제에서 "중복되지 않은 알파벳" 이라고 해주었기 때문에, 바로 전의 값이 아니더라도 , 이전의 문자열 어딘가에서 중복되는 값이 있다면, 거기서 lst가 커지는 것을 멈춰줘야 합니다.
따라서 in 을 사용하여, lst 리스트의 어디에라도 i와 같은 값이 있는지를 확인했습니다.

만약 i가 lst안에 있다면, longest에 여태까지의 lst의 length값을 넣어주고, lst를 i값으로 초기화해줍니다.
만약에 i가 lst안에 없다면, lst에 i값을 더해주는 것을 계속합니다. 이때도 longest에 lst의 length 값을 계속 append 해줘도 되는데, 어차피 max의 값을 꺼낼것이기에 상관없습니다.

그리고 longest의 max값을 리턴해주면 됩니다.
2월 16, 2022

[Django] 프로젝트 초기 세팅

 Django프로젝트를 생성하고, 초기 개발 환경을 구성하는 과정에 대해 알아보겠습니다.

환경 구성의 큰 틀은 다음과 같습니다.

a. Django 프로젝트 초기 세팅

b. git & github 으로 프로젝트 관리하기


a-1 : 가상환경 생성

가상환경은 각각의 프로젝트마다 하나씩 만들어줍니다. 

왜 각각의 프로젝트마다 별개의 가상환경을 만들어줘야 할까요? 이유는 패키지의 호환 때문입니다.

보통 Python을 설치할 때 딸려오는 패키지 매니저 pip를 사용해 필요한 패키지를 설치하면, 설치된 패키지는 "전역으로 설치"됩니다. 이 경우, 프로젝트를 하나만 진행한다면 문제가 없겠지만, 프로젝트를 여러개 진행하게 될 시 문제가 발생할 수 있습니다. pip로 설치된 패키지는 꾸준히 최신 버전으로 업데이트 받습니다. 그런데 업데이트 한 새로운 버전이 이전의 구버전과의 호환을 지원하지 않는다면, 구 버전으로 만든 서비스 또는 프로젝트는 동작하지 않게 되는 문제가 발생할 수 있습니다. 

따라서 가상환경을 이용해, 프로젝트에 따른 패키지의 버전을 달리하면 구버전과 신버전간의 호환 문제가 발생하지 않습니다.

conda create -n "가상환경 이름" python=3.8

conda activate "가상환경 이름"

가상환경을 생성하고 활성화 하는 코드는 다음과 같습니다.


a-2 : 데이터베이스 생성

다음으로 프로젝트에서 사용할 mysql 데이터베이스를 생성해줍니다.
mysql에 접속해 준 뒤, 다음의 명령어를 입력해줍니다.

mysql> create database NAME character set utf8mb4 collate utf8mb4_general_ci;

character set은 이 데이터베이스가 받아들일 수 있는 문자 세트를 utf8mb4로 지정하라는 뜻입니다.

collate 는 문자열이 정렬될 때, 어떤 문자들이 먼저 올지 결정하는 기준입니다.(정렬방식)

알파벳 정렬시의 순서가 AaBb 가 맞는지, 혹은 ABab가 맞는지를 결정한다고 보면 됩니다.

collate의 기본값은 utf8mb4_general_ci 입니다. 


a-3 : Python package 생성

pip install django
로 django를 설치해줍니다.

pip install mysqlclient

다음으로 Mysql Server에 접속할 수 있게 해주는 mysqlcilent를 설치해줍니다.

이 때, Mysql이 미리, 먼저 설치돼 있어야 합니다.


a-4 : Django project 생성

django-admin startproject westarbucks

해당 명령어로 'westarbucks'라는 장고 프로젝트를 생성해주고, cd를 통해 해당 프로젝트 디렉토리로 이동합니다.
Django 프로젝트의 모든 기준점은 바로 이 폴더, 즉 manage.py가 있는 곳입니다. 추후에 프로젝트에서 명령어를 실행할 때(ex: python manage.py runserver) 모두 manage.py에서 시작하는 것을 통해 이를 알 수 있습니다.

a-5 : Settings.py 변경/ my_settings.py 생성

프로젝트의 설정 정보를 담고 있는 파일이 settings.py입니다. 프로젝트의 목적에 맞게 settings.py를 조율해주어야 합니다.

ALLOWED_HOSTS = ['*']
로 allowed host를 개방해줍니다. 즉, 네트워크에 연결할 수 있는 컴퓨터들을 열어주는 것입니다.

INSTALLED_APPS 리스트에서 사용하지 않는 앱들은 주석처리 해줍니다.
제가 진행하는 프로젝트에서는 admin과 authentication을 사용하지 않으므로 주석처리해줍니다.

admin을 사용하지 않을 것이므로, westarbucks/urls.py의 admin 루트도 지워주도록 합니다.

 

저희는 이 프로젝트를 깃허브에 연동해 사용할 것입니다. 그런데 깃허브에 올라간 파일에 SECRET_KEY와 DATABASES에 대한 정보가 버젓이 올라가 있으면 안되겠죠? 이를 방지하기 위해 my_settings.py라는 파일을 만들고, 이 파일은 .gitignore을 통해 깃과 연동되지 않도록 해줄 것입니다.

manage.py와 같은 위치에 my_settings.py를 touch명령어로 만들어줍니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
DATABASES = {
    'default' : {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'DATABASE 명',
        'USER': 'DB접속 계정명',
        'PASSWORD': 'DB접속용 비밀번호',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}

SECRET_KEY = '시크릿키' #settings.py에 있는 
secret_key 

그리고 그 안에 해당 정보를 적어줍니다.

이 my_settings.py를 settings.py와 연동해 주어야겠죠.

from my_settings import DATABASES, SECRET_KEY

로 my_settings의 값들을 settings.py에 연동해줍니다

그리고 기존의 DATABASES와 SECRET_KEY에 
DATABASES = DATABASES SECRET_KEY = SECRET_KEY

이런식으로 import한 값들을 넣어줍니다.


추가적으로 django-cors-headers도 설치해줘야 합니다.

얘는 뭘까요?? 다음은 MDN의 CORS설명입니다.

출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

예... 그냥 봐서는 무슨 말인지 전혀 모르겠습니다.

우선 CORS는 "브라우저"의 보안정책입니다. 브라우저는 보안을 이유로 cross-origin인 HTTP요청들을 제한합니다. 따라서 cross-origin 요청을 하려면 "서버"의 동의가 필요합니다 . 서버가 동의한다면 브라우저는 cross-origin 요청을 허락하고, 반대의 경우에는 허락하지 않습니다. 이러한 cross-origin 허락과 거절을 메커니즘을 HTTP-header로 가능케하는 것이 CORS입니다.

cross-origin은 어떤 경우를 말하는 걸까요?? 

a. 프로토콜이 다르거나(http와 https)

b. 도메인이 다르거나 

c. 포트번호가 다른경우

입니다.

따라서 cross-origin HTTP요청을 경우에 따라서는 허가해줘야 합니다.

django-cors-headers는 CORS에 필요한 서버의 헤더를 조작하는 Django앱입니다.

이것을 설치해주고, INSTALLED_APPS와 MIDDLLEWARE에 추가해줍니다.


여기까지 해보고, pythonmanage.py runserver로 서버를 실행시켜봅니다. 로켓이 날아간다면 성공한 것입니다


다음으로, 저희가 생성한 프로젝트를 git과 github를 통해 관리할 수 있어야 합니다.

b-1 : git 초기화 & .gitignore 생성

생성한 장고 프로젝트를 git으로 관리할 수 있도록 git을 초기화해줍니다.

manage.py 와 같은 위치에서 git init 명령어를 실행해 줍니다


앞서서 DATABASES와 SECRET_KEY에 대한 정보를 settings.py에서 분리해서 관리해주었던 과정을 기억하시나요?

그때 만들었던 my_settings.py 파일은 깃에 연동되지 않도록 해주어야 합니다.

에서 운영체제, 에디터, zsh, 언어 등의 키워드를 넣으면 gitignore파일의 내용을 생성해줍니다. 

.gitigore파일을 touch 명령어를 통해 만들어준 후, 생성된 gitignore파일의 내용을 복사 붙여넣기 해줍니다. 그리고, 파일 하단에 my_settings.py를 추가하여, my_settings.py를 git이 무시하도록 해줍니다.


b-2 : add & commit

이제 깃과 연동하여 프로젝트를 진행할 준비가 마쳐졌습니다. 

add와 commit을 진행해줍니다.


b-3 : branch 생성

이제 브랜치를 생성한 이후, 해당 브랜치에서 app을 생성할 것입니다.
주의할 점이 있습니다. 

master(main) 브랜치에서는 초기 세팅 이외에는 어떠한 작업도 하지 않습니다.

브랜치를 새로 딸때에는 master(main)에서 새로 만들어야 합니다.

main브랜치는 초기세팅까지만 딱 완료한 그 상태로 존재해야 합니다.

또한, 브랜치를 생성할때에는 내가 현재 위치하고 있는 브랜치를 기준으로 생성이 됩니다.

app을 생성한 1번브랜치에서, 다른 앱을 생성하기 위해 2번 브랜치를 만들게 된다면, 1번 브랜치와 2번 브랜치가 연동되어 버리는 불편함이 생길것입니다. 

따라서 초기세팅까지 완료된 main브랜치로 돌아와 새로운 브랜치를 만들어줘야만 합니다!


b-4 : branch 생성

이제 app을 만들 준비가 다 됐습니다.

python manage.py startapp products

로 products란 app을 만들어주고, 

settings.py 의 INSTALLED_APPS에 추가해줍니다.


b-5 : Github Push

앱까지 만들었으니, 진행상황을 깃헙에 푸쉬해줄 필요가 있습니다.
add와 commit을 먼저 진행해줍니다.

remote한 github repository에 로컬의 진행상황을 push해주려면, remote 저장소를 origin에 넣어준 뒤, push를 진행하면 됩니다.

git remote add origin깃헙 repo링크

origin이란 변수에 깃헙 repo의 링크를 추가해주겠다는 코드입니다.

이후 밑의 코드로 로컬의 내용을 remote에 push(밀어넣어) 줍니다.

git push origin 브랜치이름