728x90

레이 마칭 ( Ray Marching )을 통해 짧은 코드로도 제법 효과적인 ( 그럴듯한 ) 씬을 렌더 해보자.

작성된 데모 쉐이더 코드는 아래 링크를 통해 확인 할 수 있다.

https://www.shadertoy.com/view/mlcXWM

[첨부1] 쉐이더 토이에서 레이 마칭 데모 실행한 모습

 

레이 마칭?

[첨부2] 레이 마칭 예시 // 원본 출처를 모름

픽셀 별로 가상의 '광선'(Ray)을 카메라로부터 발사하고, 점진적으로 진행시켜서 물체에 닿았다고 판단되면 해당 물체의 색상( Color + Lighting )이 광선을 발사한 픽셀의 색으로 판단하는 기법이다.

 

일반적인 3D 렌더링 즉, 폴리곤(삼각형 집합)을 래스터라이징하여 렌더하는 것과는 사뭇 다른 방식의 렌더링 기법이다.

 

광선을 발사한다는 점에서는 레이 트레이싱(Ray Tracing)과 비슷하지만, 레이 트레이싱은 '점진적'으로 광선을 진행시키지 않고, 물체들의 표면을 돌며(씬에 오브젝트가 많다면 엄청 무겁다. 그래서 공간분할이 중요하다) 광선이 닿았는지 바로 검사하여 처리하는 방식으로 동작한다. 래스터라이징 방식의 렌더링에서는 폴리곤들의 표면 위치 정보가 있기 때문에 이런식으로 계산을 처리할 수 있다.

 

레이 마칭에서는 오브젝들의 표면 정보가 없기때문에, SDF( Signed Distance Function )이라 불리우는 거리 함수들을 통하여 물체의 표면을 판별하여 렌더한다.

그렇다면 레이 마칭은 복잡한 씬을 표현할 수 없는 걸까? 그런 당신에게 아래 링크를 선사한다.

볼때 마다 경이로움 : https://iquilezles.org/articles/raymarchingdf/

제노 블레이드 사례 : https://www.slideshare.net/leemwymw/2-ray-marching

 

설명에 앞서, 데모에서는 위 영상에서 보다시피, 2가지 shape의 거리함수를 사용하였다. 다른 다양한 shape들의 거리함수들 알고 싶다면 아래 링크를 확인.

3D SDF 레시피(경이로움 링크 주인) : https://iquilezles.org/articles/distfunctions/


데모 구현

그 전에 쉐이더 토이의 mainImage() 함수에 대해 알아야 한다. 복잡한 것은 없고, 픽셀 쉐이더(화면 픽셀마다 GPU에서 병렬로 실행되는 쉐이더 프로그램)이다. 따라서 픽셀의 색상을 적절히 반환하면 된다.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
	//...
}

반환할 픽셀 컬러는 fragColor에 넣으면 된다. fragCoord는 픽셀의 좌표인데, 단위와 방향은 다음과 같다.

[첨부3] fragCoord의 좌표 구성

[첨부3]의 빨간 부분에 적힌 해상도에 따른 각 픽셀의 좌표값이 fragCoord를 통해 전달된다. 주의할 것이 OpenGL에 익숙한 사람은 UV좌표의 방향과 같기때문에 상관없지만, DirectX에 익숙한 경우 Y방향이 헷갈릴 수 있다. ( Y가 아래방향이 커지는 방향.)

 

레이 생성

이제 fragCoord를 다음 코드 처럼 적절히 가공하여 픽셀별 광선을 생성할 수 있다.

// uv 좌표를 -1 ~ 1인 좌표로 만들고 z가 1인 레이를 만든다.
// 따라서, 수직수평 시야각이 90인 카메라 FOV가 된다.
vec2 uv = ((fragCoord * vec2(2, 2)) / iResolution.xy ) - vec2(1, 1);
vec3 rayDir = normalize(vec3(uv, 1));

이때, 카메라로부터 안쪽이 Z의 양수방향으로 하였다. ( DirextX 와 동일 )

정점계산이 없기 때문에, 좌표 구성은 입맛대로 해도 된다.

 

레이 진행

[첨부4] 레이 진행 step 1

앞에서 얘기하였듯이, 앞서 구한 레이를 이제 점진적으로 진행시켜야 한다. 씬에 존재하는 물체들의 SDF 값을 비교하여 가장 작은 값(최단거리) 만큼 레이방향으로 전진시킨다.

[첨부5] 레이 진행 step 2

전진 이후 아직 물체에 닿지 않았다면, 동일한 과정을 거쳐 레이를 진행시킨다. 이때 물체에 닿았다고 판단하는 방법은 SDF 값이 판별 거리 ( 아주 작은 값 ) 보다 작으면 닿은 것으로 간주하는 식으로 계산한다.

[첨부6] 레이 진행 step N

이 과정을 반복하다 보면, 언젠가 물체에 닿게 된다. 코드로 옮기면 다음과 같다.

RayResult rayMarch(vec3 ro, vec3 rd)
{
    // RO에서 현재까지 전진한 누적 거리 저장
    float dO = 0.;
    RayResult rr = RayResult(0.0, vec4(0.0), 0.0);
    
    int i = 0;
    for(; i < MAX_STEPS; i++)
    {
        vec3 p = ro + rd * dO;
        
        // GetDist() : 지정한 위치로부터 최단 SDF 거리값 계산
        rr = getDist(p);
        dO += rr._dist;              // 레이 전진
        
        // 하늘에 닿은 것( 최대 거리 )으로 간주
        if( dO > MAX_DIST )
            return RayResult(VERY_FAR, vec4(0.8, 0.9, 1.0, 1.0), 0.0);
            
        // 표면에 닿은 것으로 간주
        if( rr._dist < SURF_DIST)
            break;
    }
    
    rr._dist = dO;
    return rr;
}

코드에서 알 수 있듯, 레이가 허공을 향하는 경우 레이 진행이 안끝나기 때문에 무한루프를 돌 수 있다. 그래서MAX_STEPS 만큼만 진행한다. 또한, 진행 도중 너무 멀리가서 MAX_DIST보다 커지는 경우에도 미리 허공으로 간주하고 끝내버린다.

 

라이팅

레이 마칭을 통해 이제 물체의 표면 위치를 알게 되었다. ( camera position + ray direction * ray march distance 으로 구하면 알 수 있음 ) 이를 토대로 물체 표면의 라이팅을 계산하여 출력 색상을 결정하자.

float getLight(vec3 p)
{
    vec3 objectToLight = g_lightPos - p;
    vec3 L = normalize(objectToLight);
    vec3 N = getNormal(p);
    
    // Diffuse
    float diff = clamp( dot(N, L), 0.0, 1.0 );
    
    // Specular
    float specFactor = 0.0;
    if ( diff > 0.0 )
    {
        vec3 r = reflect(-L, N);
        vec3 toEye = normalize(g_cameraOrigin - p);
        specFactor = pow(max(dot(r, toEye), 0.0), 10.0);
    }
    
    // Shadow
    float d = rayMarch(p + L, L)._dist;
    if( d < length(objectToLight) )
        diff *= 0.3;
    
    return max(diff + specFactor, 0.15);
}

아주 특별한 것은 없다. Normal 벡터를 구하고, 미리 지정해둔 광원으로부터 Diffused Light와 Specular Light를 계산한다. ( 라이팅이 주제가 아니므로, 자세한 설명은 생략 )

여기서 중요한 것은 Shadow의 표현이다. 해당 표면으로부터 광원 방향으로 다시 한번 레이를 발사하여 구한 거리가 표면에서 광원까지의 거리보다 작다면 '응달'이라는 뜻으로, 해당 표면은 어둡게 표현해주면 단순한 그림자가 지게 된다.

 

반사

라이팅까지만 적용하여도 제법 그럴듯 하게 보인다. 거기에 추가적으로 간단한 반사까지 표현해주자.

vec4 getReflectedColor(vec3 p)
{
    vec3 cameraToObject = p - g_cameraOrigin;
    vec3 N = getNormal(p);
    
    vec3 reflectedRayDir = reflect(normalize(cameraToObject), N);
    
    RayResult rr = rayMarch(p + reflectedRayDir, reflectedRayDir);
    return rr._color;
}

카메라에서 표면으로 가는 벡터를 Normal 벡터에 반사시킨 벡터 reflectedRayDir를 구한뒤, 표면으로부터 해당 방향으로 레이를 쏴서 구해지는 물체의 색상을 반환한다.

( 조금 더 사실적으로 하려면 추가적으로 라이팅을 계산해야 한다. 하지만 원리를 이제 알았으니 귀찮아서 패스 )

 

최종

이렇게 구해진 색상, 1차반사 색상, 라이팅 등을 섞어서 최종 색상으로 반환한다.

// 픽셀의 거리 계산
RayResult rr = rayMarch(g_cameraOrigin, rayDir);
if (rr._dist >= VERY_FAR)
{
    // 하늘
    fragColor = rr._color;
    return;
}
    
// 광원으로부터 light 계산
float diff = getLight(g_cameraOrigin + (rayDir * rr._dist));
       
// 물체 위치로부터 1차 반사 계산
vec4 reflectedColor = getReflectedColor(g_cameraOrigin + (rayDir * rr._dist));
        
// color + reflect color
fragColor = (1.0 - rr._reflectRatio) * rr._color + rr._reflectRatio * reflectedColor;
    
// + light
fragColor = clamp(fragColor * diff, 0.0, 1.0);

위에서 설명하지는 않았지만, 쉐이더 토이에서 키보드 입력을 처리하는 방법은 아래 링크의 코드를 참고하면 알 수 있다.

https://www.shadertoy.com/view/ltsyRS

간단하게 설명하자면, Buffer A로 렌더하는 쉐이더를 하나 더 추가하고, 키보드 버퍼에서 값을 읽어서 Buffer A의 키별 특정 위치에 누적시킨다. 실제 키보드 입력에 따라 동작을 하는 쉐이더에서는 Buffer A의 누적값을 읽어서 처리한다.

참고자료

https://rito15.github.io/posts/ray-marching/

https://www.tylerbovenzi.com/RayMarch/

728x90
728x90

flask-admin을 사용할때, 특정 그룹의 사용자 ( admin? ) 가 아니면 admin 페이지에 접근 못하게 막고 싶을 수 있다.

(아니 대부분의 경우 막아야 한다!)

그럴때 사용하면 된다.

class MyAdminIndexView(AdminIndexView):
    def is_accessible(self):
      #블라블라 허용된 그룹의 사용자인지 판별 
      # session['id'] 가져와서 판별 등등.
      return False

admin = Admin(app, name='flask', template_mode='bootstrap3', index_view = MyAdminIndexView())

바로 is_accessible() 메소드를 오버라이드 하여 막으면 된다.

admin 페이지 접근이 막힌 모습

 

근데 이 코드는 문제가 있다. 딱 index 페이지만 막힌 모습이다. model 별 url을 입력하면 통과해서 확인 가능하다.

testtable 페이지가 바로 확인 가능한 모습

이럴때는 ModelView를 추가할때, 동일하게 is_accessible() 메소드를 오버라이드 해주면 처리할 수 있다.

class MyModelView(ModelView):
  def is_accessible(self):
      #블라블라 허용된 그룹의 사용자인지 판별 
      # session['id'] 가져와서 판별 등등.
      return False

  column_display_pk = True

  def __init__(self, cls, session, **kwargs):
    super(MyModelView, self).__init__(cls, session, **kwargs)

admin = Admin(app, name='flask', template_mode='bootstrap3', index_view = MyAdminIndexView())
admin.add_view(MyModelView(TestTable, db.session))

 

이는 is_accessible() 메소드가 다음과 같은 class diagram에서 최상위 클래스인 'BaseView'에 있기 때문이다.

flask-admin의 view 클래스 구조

그래서 Index용과 그 외 ModelView용 페이지 인가 제어 ( 이 부분도 일반화되어 교체 가능하게 해도 되고 )

ModelView 클래스를 만들고, 모든 model view 만들때 상속시켜주면 이제 진짜 인가된 사용자 그룹만

허용 시킬 수 있게 된다. 

728x90

'프로그래밍 > Python' 카테고리의 다른 글

flask-admin 에서 pk, fk 등이 보이지 않고, 수정, 추가 안될때 해결 방법  (0) 2023.05.01
lof  (0) 2019.08.26
ball tree  (0) 2019.08.01
kd tree  (1) 2019.07.04
keras를 활용한 다중선형회귀분석  (2) 2019.05.30
728x90

1. column_list,  form_columns 를 수정하는 방법

 

String타입의 pk를 가지는 TestTable 이라는 테이블을 만들었다고 치자.

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)

class TestTable(db.Model):
    __tablename__ = 'test'

    id = db.Column(db.String, primary_key=True)
    test_string = db.Column(db.String)

admin = Admin(app, name='flask', template_mode='bootstrap3')
admin.add_view(ModelView(TestTable, db.session))


@app.route('/', methods=['GET', 'POST'])
def index():
  return 'hi'

위 코드를 실행하여, flask-admin 페이지로 들어가면 다음과 같다. (임의로 row 하나 넣어둠)

flask-admin 페이지 모습

근데 보면 pk인 id가 보이지 않는다. Create 탭을 들어가도, 막상 데이터를 추가하고자 하면,

추가에 실패한다.

이럴때는 CustomModelView를 만들어서 column_list, form_columns 필드를 수정해주면 해결된다.

class MyModelView(ModelView):
  column_list = ()
  form_columns = ()

  def __init__(self, cls, session, **kwargs):
    MyModelView.column_list = [ c_attr.key for c_attr in db.inspect(cls).mapper.column_attrs ]
    MyModelView.form_columns = [ c_attr.key for c_attr in db.inspect(cls).mapper.column_attrs ]
    super(MyModelView, self).__init__(cls, session, **kwargs)

admin = Admin(app, name='flask', template_mode='bootstrap3')
admin.add_view(MyModelView(TestTable, db.session))

위 코드에서는 model class의 column 을 다 돌면서 추가해주었다. 이렇게 하여 실행하면 잘 나오는 것을 확인할 수 있다.

pk가 잘 나오는 모습
Create에도 이제 id가 잘 노출 된다


2. column_display_pk 를 수정하는 방법

 

이 방법은 create는 안되지만, ( 뭐, form_columns만 따로 채워주면 되긴 한다 ) 볼 수는 있는 방법이다.

class MyModelView(ModelView):
  column_display_pk = True

  def __init__(self, cls, session, **kwargs):
    super(MyModelView, self).__init__(cls, session, **kwargs)

역시 잘 나온다.

이 방법은 Id 부분에도 클릭하여 ordering이 가능하다.

728x90

'프로그래밍 > Python' 카테고리의 다른 글

flask-admin에서 ModelView 페이지 접근 막는 법  (0) 2023.05.01
lof  (0) 2019.08.26
ball tree  (0) 2019.08.01
kd tree  (1) 2019.07.04
keras를 활용한 다중선형회귀분석  (2) 2019.05.30
728x90

보통 여느 어플리케이션이든, header의 우측 상단에 config나 계정 관련 기능이 들어가기 마련이다.

그래서 동영상의 업로드 기능을 header의 우측 상단에 추가해보자.

(한번 찍어 먹어보는 것이기 때문에 거창한 기능은 필요없다.)


Upload Button 추가하기

const UploadButton = styled.div`
  width: 50px;
  height: 25px;
  background: url("/static/images/upload-icon.png") center / contain no-repeat;
  cursor: pointer;

  &:hover {
    background-color: ${(props) => props.hoverColorStyle || "#ddd"};
  }
`;

const Header = () => {
  return (
    <TopPanel>
      <Link href="/">
        <TopLogo />
      </Link>
      <TopRightPanel>
        <UploadButton onClick={() => {}}></UploadButton>
      </TopRightPanel>
    </TopPanel>
  );
};

UploadButton을 styled-components로 꾸며서 넣어준다. 이때 button이 hover가 되었을때 색상을 바꿔주던가 하는

동작들이 필요할 수 있는데, styled-components에서 pseudo class는

&:PseudoClass  로 지정하여 스타일링을 줄 수 있다. 

[그림1] upload button 추가


Modal창 추가하기

보통 저런 업로드 버튼을 누르면 Modal창이 열리며 추가적 UI가 가운데 표시된다.

이 외에도 모달창은 각종 페이지에서 사용자 확인등의 이유로 자주 사용되기 때문에 이러한 컴포넌트를 만들어두면

유용하게 쓸 수 있다.

[그림2] modal창 예시

Modal창을 만드는 방법에는 여러 방법이 있겠지만, 여기서는 React Context를 통하여 "전역" Modal 창을 만들어보자.

이전에 단순 컴포넌트로 만들고 띄우는 페이지에서 state를 관리하게 하였더니 매우 불편해던 경험이 있어서,

Modal 컴포넌트가 알아서 state도 관리하게끔 React Context를 통해 제공하는 인터페이스가 더 좋다 판단하였다.


React Context?

 컴포넌트간 prop drilling를 없애고 데이터를 공유할 수 있는 메커니즘이다.

 이전에는 Redux를 통해 store에 저장하고 공유했지만 왠만한 것은 Context를 통해 해결할 수 있다.

 ( 사실 Redux를 안 써봐서 정확한 비교는 어렵다. )

 React 특성상 페이지의 기능이 방대하고 거대해질 수록 Component가 많아지고 그 깊이가 길어질 것이다.

 예를 들어 다음과 같은 구조라 했을때)

const nameBox = (props) => {
  return (
  <div>{props.userName}</div>
  )
}

const login = (props) => {
  return (
    <nameBox userName={userName}></nameBox>
  )
}

const userInfo = (props) => {
  return (
    <login userName={userName}></login>
  )
}

const index = () => {
  const userName = getUserNameFromServer();

  return (
    <div>
      <div>{userName}</div>

      <userInfo userName={userName}></userInfo>
    </div>
  );
};

index에서 서버로부터 가져온 userName 데이터가 nameBox component가 사용할때, 두번 요청하여 가져올

이유는 없기때문에 이미 가져온 데이터를 넘길 것이다. 근데 userName까지 이미 userInfo, login component가 있을때

사용도 안하는 component들이 불필요하게 props를 받았다가 넘겨야 한다. 이러한 것을 props를 뚫어서 넘긴다 하여

"props drilling"이라 하는데, React Context를 사용하면 불필요한 props drilling을 막을 수 있다.

 

Context를 사용하여 개선하면 다음과 같다.

// context 생성
const UserInfoContext = createContext({});

const nameBox = () => {
  const userContext = useContext(UserInfoContext);
  
  return (
    //방법1) useContext() hook으로 UserInfoContext사용을 명시하고 사용
    <div>{userContext.userName}</div>
  
    //방법2) consumer 컴포넌트로 세팅된 'userName'을 소비한다
    <UserInfoContext.Consumer>
      <div>{userName}</div>
    </UserInfoContext.Consumer>
  );
};

const login = () => {
  return <nameBox></nameBox>;
};

const userInfo = () => {
  return <login></login>;
};

const index = () => {
  const userName = getUserNameFromServer();

  return (
    // provider 컴포넌트로 하위 컴포넌트에 'userName'을 제공한다

    <div>
      <div>{userName}</div>

      <UserInfoContext.Provider value={userName}>
        <userInfo></userInfo>
      </UserInfoContext.Provider>
    </div>
  );
};

 이제 login, userInfo component는 property를 넘기지 않는다. React.createContext()를 통해 생성된 context에는

 두 컴포넌트가 있는데, Provider와 Consumer이다. 하위 컴포넌트에 넘기기 원하는 값은 Provider의 "value"를 통해

 제공한다. 제공되는 value를 사용하는 nameBox component는 2가지 방법이 있다.

 [방법1]useContext() 훅을 통해 넘겨받은 value로 사용하는 것이다.

 [방법2]Consumer를 통해 값을 받겠다 명시하고 사용하면 된다.


다시 모달 창으로 돌아와서, 먼저 Modal component를 만들자.

const ModalContainer = styled.div`
  display: ${(props) => (props.modalState ? "block" : "none")};
  position: fixed;
  left: 0px;
  top: 0px;
  width: 100%;
  height: 100%;
  z-index: 99;
  background-color: rgba(0, 0, 0, 0.6);
`;

const ModalPopupContainer = styled.div`
  position: fixed;
  left: 50%;
  top: 50%;
  background-color: #ff8;

  transform: translateX(-50%) translateY(-50%);
`;

const Modal = ({ state, content }) => {
  return (
    <ModalContainer modalState={state}>
      <ModalPopupContainer>{content}</ModalPopupContainer>
    </ModalContainer>
  );
};

modal창은 팝업되면 항상 화면 전체를 덮어야 하기 때문에 fixed로 만들었다. state에 display를 보이게 할 지,

안보일지 결정하게 한다. 또한 내용물은 외부에서 주입하도록 content를 받았다가 가운데 content영역에 넣어준다.

 

다음은 이 modal을 전역적으로 사용하게할 context이다.

const ModalContext = createContext({});

const ModalContextProvider = ({ children }) => {
  const [modalState, setModalState] = useState(false);
  const [modalContent, setModalContent] = useState("");

  return (
    <ModalContext.Provider
      value={{
        open: () => {
          setModalState(true);
        },
        close: () => {
          setModalState(false);
        },
        setContent: setModalContent,
      }}
    >
      {children}
      <Modal state={modalState} content={modalContent}></Modal>
    </ModalContext.Provider>
  );
};

export const useModal = () => {
  const context = useContext(ModalContext);
  return context;
};

export default ModalContextProvider;

별도의 provider component를 만들어서 Context Provider로 넘겨진 children을 감싸는 형태로 만들었다.

그 아래 Modal component를 위치 시켜 항상 팝업 가능하게 한다.

Context Provider의 value로는  open, close, setContent로 Modal을 열리거나 닫히도록 state를 변경하거나,

Modal의 내용을 주입시키는 메소드 이다.

아래 외부 노출 (export) 용 Custom Hook이 있다. ( useModal ) 단순히 userContext()로 Context를 사용하게 하고

반환한다.

이로써, Modal을 전역으로 제공하기 위한 준비는 마쳤다.

Custom Hook?

  React 진영에서 캡슐화된 함수를 "Hook"이라 부르는데 기본 제공 Hook( useState, useContext 등 )이 아닌

  사용자 정의 Hook들은 모두 Custom Hook이라 부른다. 사실 function이라 부르지 않는데에는 많은 철학과

  마케팅(?)이 담겨있는데, 공통적으로 "use" 라는 prefix로 시작하며, 내부에서 순수 Logic이 아닌, React 진영의

  UI logic즉, component를 제어하고, react hooks를 사용하는 것을 알려 줄 수 있기 때문에 구분해서 부르는 것 같다.

  (심지어 그 철학은 담아 고수시키는 Lint도 있다!!)

 

전역적으로 제공할 것이므로, _app.jsx파일에 방금 작성한 ModalContextProvider component로 감싸주자.

import Header from "../components/Header";
import ModalContextProvider from "../components/Modal";

function MyApp({ Component, pageProps }) {
  return (
    <div>
      <ModalContextProvider>
        <Header></Header>
        <Component {...pageProps} />
      </ModalContextProvider>
    </div>
  );
}

export default MyApp;

 

위에서 Header내 Upload Button에 대한 click 동작이 비어있는데, 이제 Modal을 띄우게 변경하자

//...

const Header = () => {
  const modal = useModal();

  const uploadVideoContent = <Modal_UploadVideo />;

  return (
    <TopPanel>
      <Link href="/">
        <TopLogo />
      </Link>
      <TopRightPanel>
        <UploadButton
          onClick={() => {
            modal.setContent(uploadVideoContent);
            modal.open();
          }}
        ></UploadButton>
      </TopRightPanel>
    </TopPanel>
  );
};

//...

useModal Hook으로 context를 가져온 뒤 onClick시 content를 세팅하고 open한다.

Modal의 upload content를 담당하는 component는 다음과 같이 간단하게 구성했다.

import React, { useRef, useState } from "react";
import styled from "styled-components";
import { useModal } from "./Modal";

//.. style component 생략

const Modal_UploadVideo = () => {
  const modal = useModal();

  const [videoName, setVideoName] = useState("");
  const [videoDescription, setVideoDesc] = useState("");

  return (
    <ModalContentContainer>
      <ContentHeader>비디오 업로드</ContentHeader>

      <ColoredLine />

      <ContentCenter>
        <ContentCenterElement>
          <StyledTextBox
            type="text"
            placeholder="파일 이름"
            disabled />

          <StyledLabel>
            <StyledInputFile
              accept=".mp4"
              type="file" />
            파일 선택
          </StyledLabel>
        </ContentCenterElement>
        <ContentCenterElement>
          <StyledTextBox
            type="text"
            placeholder="제목"
            onChange={(event) => setVideoName(event.target.value)} />
        </ContentCenterElement>
        <ContentCenterElement>
          <StyledTextBox
            id="upload-description"
            type="text"
            placeholder="설명"
            onChange={(event) => setVideoDesc(event.target.value)} />
        </ContentCenterElement>
      </ContentCenter>

      <ColoredLine />

      <ContentFooter>
        <FooterRight>
          <InlineMargin>
            <ModalButton >업로드</ModalButton>
          </InlineMargin>
          <InlineMargin>
            <ModalButton
              backgroundColor="#cfcfcf"
              hoverColor="#e0e0e0"
              textColor="000"
              onClick={() => {
                modal.close();
              }}
            >
              닫기
            </ModalButton>
          </InlineMargin>
        </FooterRight>
      </ContentFooter>
    </ModalContentContainer>
  );
};

export default Modal_UploadVideo;

좀 길어지기는 했지만, 중요한 것은 modal.close()이다. 닫기 버튼을 눌렀을때 modal.close()를 통해 간단하게

Modal창이 닫히도록 했다. ( 좀 더 예쁘게 하고 싶다면 keyframe 으로 연출을 해주면 된다. )

[영상1] modal 구현 확인

728x90
728x90

보통 유튜브 어느 페이지든 상관없이 항상 위쪽에 달려있는 것이 있다.

보통 그러한 유형의 UI를 Header라고 부른다. 명색의 스트리밍 사이트니까 우리도 추가해주자

[그림1] 유튜브 header


styled-components

이전 강좌에서 기본 styling 을 위한 디렉토리를 삭제했는데, 그 이유는 styled-components를 사용할 것이기 때문이다.

styled-copmonents는 javascript 라이브러리로 CSS-in-JS를 위한 라이브러리다. 별도의 css 파일을 작성하지 않고,

component 파일에 함께 작성 할 수 있기 때문에 파일이 많아졌을때 일일이 어떤 style인지 확인해야할 

수고를 덜어줄 수 있다.

대신 CSS때문에 파일 길이가 증가한다. 때문에 컴포넌트의 경계를 잘 설정하여 한 컴포넌트가

너무 비대해지지 않게 주의해야 한다.

설치는 간편하게 프로젝트 디렉토리에서 "yarn add styled-components"으로 모듈을 추가하면 된다.


컴포넌트 추가하기

react에서 컴포넌트 추가하는것과 동일하다 (애초에 react 위에서 돌아가니..) class 형보다는 짧게 작성 가능한

함수형 컴포넌트가 요새 트렌드 이기때문에 여기서도 그렇게 한다.

import styled from "styled-components";

const TopPanel = styled.div`
  display: flex;
  top: 0px;
  justify-content: space-between;
  width: 100%;
  height: 60px;
  background: #ffffff;
`;

const TopLogo = styled.div`
  height: 100%;
  width: 300px;
  background: url("/static/images/logo.png") no-repeat;
  background-size: 90% 70%;
  background-position: 10% 30%;
  box-sizing: border-box;
`;

const TopRightPanel = styled.div`
  height: 100%;
  margin: 10px;
`;

const Header = () => {
  return (
    <TopPanel>
      <TopLogo />
      <TopRightPanel></TopRightPanel>
    </TopPanel>
  );
};

export default Header;

애초에 전문 프론트엔드 개발자도 아니고 div로 퉁쳐서 만들었다.

styled-components의 사용법은 보다시피 간단하다!

1. 컴포넌트의 태그로 들어갈 이름을 변수명으로 한다.

2. styled.태그 뒤에 백틱( ` ) 으로 감싸고 그 안에 css style을 명시한다.

 

이렇게 하고 header 이기 때문에 _app.jsx 컴포넌트에다가 넣어준다.

import Header from "../components/Header";

function MyApp({ Component, pageProps }) {
  return (
    <div>
      <Header></Header>
      <Component {...pageProps} />
    </div>
  );
}

export default MyApp;

이제 모든 페이지는 header가 출력된다.

[그림2] index 페이지 모습


정적 라우팅

유튜브를 보면 로고 부분 클릭시 home( index ) 페이지로 이동한다. Next JS에서는 요러한 정적 페이지로 이동하는 것을

<Link> 태그를 이용하여 구현 할 수 있다.

import styled from "styled-components";
import Link from "next/link";

//...

const Header = () => {
  return (
    <TopPanel>
      <Link href="/">
        <TopLogo />
      </Link>
      <TopRightPanel></TopRightPanel>
    </TopPanel>
  );
};

export default Header;

위 코드처럼 Logo를 Link태그로 감싸면 자동으로 클릭을 바인딩하여 href="" 페이지로 이동시켜준다.

(그냥 / (index) 페이지로 해두면 이동 안하는 것처럼 보일 수 있으니 확인하고 싶으면 없는 페이지라도

써보면 확인 가능하다!)

로고 위로 올라오면 커서 모양도 바뀌게 css<a>태그로 적당히 묶어주자!

[영상1] 정적 라우팅 적용

 

728x90
728x90

법선 벡터( 면에 수직인 벡터 )의 변환의 경우 일반적인 정점의 matrix를 그대로 사용하면 안된다.

법선 벡터 예시

실제로 단순 vertex를 월드 좌표로 옮길때 "world marix"라 부르는 SRT ( 스케일, 회전, 이동 ) 이 적용된,

matrix를 사용하게 되는데, 법선 벡터도 동일하게 world matrix를 사용하면 "직교성"을 잃어버린다.

(실제로 손으로 계산하면 바로 알 수 있다.)

월드 좌표로 변환 예시

그럼 어떤 matrix를 곱해야 월드 좌표로 옮겨도 직교성을 유지할 수 있을까??

W, world matrix

결론부터 얘기하면, world matirx의 역 전치를 곱하면 된다.

예를 들어 삼각형의 위의 벡터 V가 있고, 법선 벡터를 N이라 하자. ( N · V = 0, 직교하므로)

월드 좌표로 변환된 두벡터의 내적은 여전히 0이어야 한다. 따라서,

 

Nt · Vt = (T N) · (W V) = 0 이다.  이때 T를 구하면 된다.

 

(T N) · (W V) = trans(T N) (W V) = trans(N) trans(T) W V = 0 인데, ( trans()는 전치 )

 

trans(N) V = 0이므로, trans(T) W가 단위 행렬이 되면 된다.

 

따라서, trans(T) = inv(W)   =>  T = trans( inv(W) )  

 

 

728x90
728x90

NextJS? 

React 프레임워크로, 이미 웹 프레임워크인 React 위에 굳이 NextJS를 올리는 이유는

바로 SSR을 쉽게 지원하기 때문이다.


SSR?

SSR(Sever Side Rendering)은 CSR (Client Side Rendering)과는 대조적인 방식으로, 결국

브라우저가 해석하는 문서 (HTML)의 대부분이 이미 만들어져서 나온다.

원래 다 그런 것 아니야?? 할 수 있지만, 요새 대부분의 웹사이트에 접속하면 

데이터가 아직 없어서 아무것도 표시 못하다가 사용자별 데이터가 들어오면 그제서야 component를

만들어서 렌더링을 한다.

[그림1] 유튜브 접속했을때 아직 페이지 렌더링이 덜 된 모습

그래서 SSR을 사용하면 사용자 경험을 올릴 수 있는 장점이 있지만, 그만큼 서버의 연산 자원을 활용해야 하는

단점이 있다. 그래서 SSR과 CSR을 적절히 섞어서 써야 한다.


프로젝트 생성

[그림2-1] 프로젝트 생성

"npx create-next-app"을 하면 알아서 npm이 관련 dependencies를 함께 설치해준다.

그래서 react, react-dom, next가 함께 설치 된다.

[그림2-2] 설치 완료

설치된 디렉토리를 VSCode를 열어보면 다음과 같을 것이다.

[그림2-3] 기본 directory

pages : next js의 기본 라우팅을 해주는 디렉토리로 해당 디렉토리내 컴포넌트의 이름으로 자동으로 라우팅이 된다.

     ex) pages/board.js  =>  "xxx.myapp-domain.xx/board"  로 외부에서 접속 가능 

public : image나 영상등을 넣게 된다. 기본적으로 정적 리소스에 대한 참조( background-image 같은것들 )를

          이 디렉토리 내에서 한다.

styles : next js에서 제공하는 기본 styling 방법을 위한 디렉토리.

pages/api : 기본 api route를 지원받기 위한 디렉토리로, 이 녀석 덕분에 백엔드의 기능이 적은 

               규모가 작은 어플리케이션이라면, 별도 백엔드 없이 요거 하나로 충분하다.

_app.js : 좀 특별한 컴포넌트로 모든 페이지에 적용되는 컴포넌트다. 보통 공통의 레이아웃은 여기에 위치한다.

 

일단 컴포넌트는 _app.js, index.js를 제외하고 싹다 날리자. styles 디렉토리도 안쓸거니 날리자.

index.jsx의 내용들도 다 날리고 심플하게 "hello, nextjs"만 남긴 뒤 app을 구동시켜서 브라우저로 확인해보자.

"npm run dev" 혹은 "yarn run dev"로 실행 할 수 있다.

// index.jsx
export default function Home() {
  return <div>hello, next js</div>;
}

[그림2-4] 실행에 성공하여 브라우저에서 접속 한 모슴

이제 기본 프로젝트 설정은 다 끝났다.

728x90
728x90

[ 자료1 ]에서 소개된 알고리즘을 javascript로 포팅하였습니다.


bin-packing 알고리즘은 굉장히 많이 쓴다. 

1d bin-packing의 대표적 예시가 메모리 어느 지점에 프로세스를 적재할 것인지 결정하는 알고리즘이

될 것이고, 2d bin-packing의 대표적 예시는 다양한 크기의 glyph를 atlas상에 배치 하거나,

여러 sprite 이미지를 큰 atlas이미지에 압축하는 것이 될 것이다.

                                                                               [그림1 출처]

[그림1] texture atlas 예시

[ 자료2 ] 에서와 같이 수많은 bin packing 알고리즘이 있지만, 개중에 하나인

skyline bottom-left bin packing algorithm를 소개하고자 한다.

 

 바로 본론으로 들어가서, skyline bottom-left는 빈공간을 추적하고 있는데, 이것들 node라고 한다.

이 빈공간은 아주 단순한 형태로 저장하고 있는데, [ x, y, width ]의 형태로 저장하고 있다.

x : 추적하는 빈공간 node의 왼쪽 시작 지점

y : 추적하는 빈공간 node의 위쪽 시작 지점

width : 추적하는 빈공간 node의 너비

 

height가 없어서 의아할 수 있는데, skyline bottome-left가 추적하는 "빈공간"을 알면 이해할 수 있다.

다음 그림을 보면,

[그림2] box를 3개 채운 시점의 node들

box를 채웠을때, 생성된 node의 아래쪽은 전부 해당 node가 추적중인 "빈공간"으로 간주하기

때문에, height정보가 필요없다.

항상 box들의 아래쪽 빈공간을 추적하기 때문에 box크기가 들쑥날쑥하면, 정렬시켜서 넣던가 하는

휴리스틱들을 추가로 적용해보아햐 한다.

[그림3] 추적되지 않는 공간 예시

이제 실제 packing 과정을 살펴보자.

var bestWidth = Number.MAX_SAFE_INTEGER;
var bestHeight = Number.MAX_SAFE_INTEGER;
var bestIndex = -1;
var region = [0, 0, width, height];

//1) 모든 노드들을 돌면서 들어갈 수 있는 곳을 탐색한다.
for (var i = 0; i < this._nodes.length; ++i) {
	//1-1) fit에서 width 만큼의 node중 가장 큰 봉우리를 찾는다
	var y = this.fit(i, width, height);

	//1-2) y가 minus가 아니라면 들어갈 수 있는 봉우리를 찾았다.
	if (y >= 0) {
		var node = this._nodes[i];

		//1-3) 봉우리중에 (하늘에서)가장 낮은 놈이 best다
		if ( y + height < bestHeight || (y + height == bestHeight && node[2] < bestWidth)) {
			bestHeight = y + height;
			bestIndex = i;
			bestWidth = node[2];
			region = [node[0], y, width, height];
		}
	}
}

모든 노들을 돌면서, fit과정을 거치는데, fit은 width만큼 단순히 node들을 방문하면서 (하늘에서)가장 높은

봉우리를 찾는다. width만큼 방문하기 때문에, 높아야 겹치지 않고 배치할 수 있기 때문이다.

[그림4] 새로운 박스의 fit

[그림4] 에서는 1번 node쪽 봉우리가 가장높으므로 해당 봉우리의 y값을 취한다.

그리고 그렇게 선택된 봉우리중에 가장 "낮은"값을 취하여 region( 박스가 배치될 영역 )을 갱신한다.

//2) 들어갈만한 적당한 곳이 없다
if (bestIndex == -1) {
	return undefined;
}

//3) 적당한 곳이 결정되어 node를 넣는다.
var node = [region[0], region[1] + height, width];
this._nodes.insert(bestIndex, node);

//4) 현재 노드가 들어감으로 기존노드와 겹침이 있을 수 있으므로 shrink작업을 한다
var i = bestIndex + 1;
while (i < this._nodes.length) {
	node = this._nodes[i];
	var prevNode = this._nodes[i - 1];

	//4-1) 이전 노드의 위치 + width 보다 작다 => 겹친다
	if (node[0] < prevNode[0] + prevNode[2]) {
		var shrink = prevNode[0] + prevNode[2] - node[0];

		var x = node[0];
		var y = node[1];
		var w = node[2];

		//4-2) shrink
		this._nodes[i][0] = x + shrink;
		this._nodes[i][1] = y;
		this._nodes[i][2] = w - shrink;

		//4-3) width가 minus라면 그 node는 삭제
		if (this._nodes[i][2] <= 0) {
			this._nodes.splice(i, 1);
			i -= 1;
		}
	} else {
		break; // 겹침이 없다면 괜춘괜춘
	}

	i += 1;
}

그리고 그렇게 배치가 되면 새로운 빈공간( 새로 배치된 박스의 아래부분 )이 생기므로 node를 추가한다.

새로운 node가 들어감으로, 기존 노드와의 영역다툼 해소를 위해 shrink를 해준다.

이때, shrink도중 음수가 나오게 되면 해당 node는 invalid로 제거한다.

 

//5) y위치가 같은 노드가 있다면 하나의 node로 만든다
this.merge();

//6) 사용량 추적용
this._used += width * height;

return region;

마지막으로, y위치( = 봉우리)가 같은 node가 있다면, merge하여 하나의 node로 만들어서 여러모로 계산도 줄이고,

최적화를 한다. 그리고 region을 반환하면 끝!

[그림5] 다양한 box를 채워넣은 모습


+ full source

Array.prototype.insert = function (index, item) {
  this.splice(index, 0, item);
};

class RectBox {
  constructor(x, y, width, height, color) {
    this._x = x;
    this._y = y;
    this._width = width;
    this._height = height;
    this._color = color;

    this.render = this.render.bind(this);
  }

  render(context) {
    context.fillStyle = this._color;
    context.fillRect(this._x, this._y, this._width, this._height);
  }
}

class Bin {
  constructor() {
    this._handle = null;

    this.addRect = this.addRect.bind(this);
    this.fill = this.fill.bind(this);
    this.draw = this.draw.bind(this);

    this.pack = this.pack.bind(this);
    this.fit = this.fit.bind(this);
    this.merge = this.merge.bind(this);

    var canvas = document.getElementById("myCanvas");
    var addButton = document.getElementById("addRect");
    addButton.onclick = this.addRect;

    this._canvasContext = canvas.getContext("2d");

    this._width = 512;
    this._height = 512;
    this._nodes = [[0, 0, this._width]];
    this._used = 0;

    this._rects = [];
  }

  fill() {
    var width = Math.floor(Math.random() * 100 + 30); // 30 ~ 130
    var height = Math.floor(Math.random() * 100 + 30); // 30 ~ 130

    var region = this.pack(width, height);
    if (undefined == region) {
      return;
    }

    var color = "#" + (((1 << 24) * Math.random()) | 0).toString(16);

    var newRect = new RectBox(
      region[0],
      region[1],
      region[2],
      region[3],
      color
    );
    this._rects.push(newRect);

    this.draw();
  }

  addRect() {
    this.fill();
  }

  draw() {
    for (var i = 0; i < this._rects.length; ++i) {
      this._rects[i].render(this._canvasContext);
    }
  }

  fit(index, width, height) {
    var node = this._nodes[index];
    var x = node[0];
    var y = node[1];
    var widthLeft = width;

    if (x + width > this._width) {
      return -1;
    }

    var i = index;
    while (widthLeft > 0) {
      node = this._nodes[i];
      y = Math.max(y, node[1]);
      if (y + height > this._height) {
        return -1;
      }

      widthLeft -= node[2];
      i += 1;
    }

    return y;
  }

  merge() {
    var i = 0;
    while (i < this._nodes.length - 1) {
      var node = this._nodes[i];
      var nextNode = this._nodes[i + 1];
      if (node[1] == nextNode[1]) {
        this._nodes[i][0] = node[0];
        this._nodes[i][1] = node[1];
        this._nodes[i][2] = node[2] + nextNode[2];
        this._nodes.splice(i + 1, 1);
      } else {
        i += 1;
      }
    }
  }

  pack(width, height) {
    var bestWidth = Number.MAX_SAFE_INTEGER;
    var bestHeight = Number.MAX_SAFE_INTEGER;
    var bestIndex = -1;
    var region = [0, 0, width, height];

    //1) 모든 노드들을 돌면서 들어갈 수 있는 곳을 탐색한다.
    for (var i = 0; i < this._nodes.length; ++i) {
      //1-1) fit에서 width 만큼의 node중 가장 큰 봉우리를 찾는다
      var y = this.fit(i, width, height);

      //1-2) y가 minus가 아니라면 들어갈 수 있는 봉우리를 찾았다.
      if (y >= 0) {
        var node = this._nodes[i];

        //1-3) 봉우리중에 (하늘에서)가장 낮은 놈이 best다
        if (
          y + height < bestHeight ||
          (y + height == bestHeight && node[2] < bestWidth)
        ) {
          bestHeight = y + height;
          bestIndex = i;
          bestWidth = node[2];
          region = [node[0], y, width, height];
        }
      }
    }

    //2) 들어갈만한 적당한 곳이 없다
    if (bestIndex == -1) {
      return undefined;
    }

    //3) 적당한 곳이 결정되어 node를 넣는다.
    var node = [region[0], region[1] + height, width];
    this._nodes.insert(bestIndex, node);

    //4) 현재 노드가 들어감으로 기존노드와 겹침이 있을 수 있으므로 shrink작업을 한다
    var i = bestIndex + 1;
    while (i < this._nodes.length) {
      node = this._nodes[i];
      var prevNode = this._nodes[i - 1];

      //4-1) 이전 노드의 위치 + width 보다 작다 => 겹친다
      if (node[0] < prevNode[0] + prevNode[2]) {
        var shrink = prevNode[0] + prevNode[2] - node[0];

        var x = node[0];
        var y = node[1];
        var w = node[2];

        //4-2) shrink
        this._nodes[i][0] = x + shrink;
        this._nodes[i][1] = y;
        this._nodes[i][2] = w - shrink;

        //4-3) width가 minus라면 그 node는 삭제
        if (this._nodes[i][2] <= 0) {
          this._nodes.splice(i, 1);
          i -= 1;
        }
      } else {
        break; // 겹침이 없다면 괜춘괜춘
      }

      i += 1;
    }

    //5) y위치가 같은 노드가 있다면 하나의 node로 만든다
    this.merge();

    //6) 사용량 추적용
    this._used += width * height;

    return region;
  }
}

var s_bin = null;
window.onload = () => {
  s_bin = new Bin();
};
728x90
728x90

원래는 보안상의 문제로, browser에서 local file을 직접적으로 수정할 수 없었다.

애초에 Wep App 이라면 로컬에 파일을 남길 이유가 없다. (캐시용 임시파일, 쿠키값 제외)

서버의 db( 파일, RDBMS 등)에 기록하면 되니까.

 

근데, 단순 로컬 파일 하나에 "현재 상태 기록"만 할 수 있으면 되는데,

localhost에서 도는 서버 하나 띄우고(이거 띄우려고 언어별 프레임워크 설치하고... ), url 라우팅하고,

request/response 로직 만들고, 여간 귀찮은게 아니다. 그래서 검색도중 저걸 발견했다.

 

1. 파일 내용 읽어서 <textarea> 에 넣기

let _fileHandle;
async function openFile() {
	//1) 파일 선택기에서 파일 선택
	[_fileHandle] = await window.showOpenFilePicker();

	//2) file 객체 얻기
	const file = await _fileHandle.getFile();

	//3) USVString( Unicode scalar values ) 으로 파일 내용 가져오기
	const contents = await file.text();

	//4) text box에 내용 넣기
	document.getElementById("textbox").textContent = contents;
}

[그림1] 파일을 읽어서 textarea에 넣은 모습

 

2. <textarea>에서 수정하여 다시 파일에 저장하기

async function saveFile() {
	//1) write를 위해 FileSystemWritableFileStream 생성
	const writable = await _fileHandle.createWritable();

	//2) stream에 textare 내용을 쓴다
	await writable.write(document.getElementById("textbox").value);

	//3) 파일이 닫히면서 실제 디스크에 반영됨
	await writable.close();
}

[그림2] 수정하여 파일에 다시 저장한 모습


간단한 api로 로컬 파일 수정이 가능해졌다. ( safari에서는 안되는 모양이다.. 크롬만 쓰니 상관없다. )

이제 단순한 web app을 서버없이도 만들 수 있다! ( web이란 말도 무색하다. '브라우저' app이 맞지 않나? )

728x90
728x90

방문자 패턴은 연산(행동)을 적용할 클래스를 변경하지 않고도 새로운 연산을 정의할 수 있게 하는 패턴이다.

 

방문자 패턴은 단일 클래스로만 구성 되어 있을때도 유용하지만, 이미 수많은 클래스가 "군"(많은 결합)

이루고 있어서, 새로운 기능 추가시 많은 비용( 많은 수정 )이 들어갈때 매우 유용하게 사용할 수 있다.

(수정하는 양이 많아지면, qa 대상도 곱으로 늘어난다)

 

예를 들어 현재 라이브 서비스중인 게임회사에 새로 입사했다.

프로그래머가 1명뿐이던 영세 회사인데, 이전 프로그래머가 그만두면서 들어와서 legacy 코드를 물어볼 사람도 없다.

(실제 상황이라면, 뤈)

 

현재 웹서버 기반(HTTP)으로 게임을 만들었으나, 답답한 지연성 때문에 좀더 compact하게

깡 TCP로 만들기로 하여, 일단 간단히 TCP Socket으로 송수신 가능한 기능을 만들었다.

class TCPClient
{
public:
	//@brief : socket 초기화 및 서버 연결등의 처리
	void init(void) noexcept;

	//@brief : 별도의 스레드에서 로그아웃, 종료, 서버 응답 없음 등의 이유로 게임이 종료되어야 할때까지 실행됨
	void run(void) noexcept;

	//@brief : 송/수신 관련 인터페이스. 
	//{@
	void send(void) noexcept;
	void receive(void) noexcept;
	//@}
};

이제 기존에 HTTP로 요청하던 부분들을 다음과 같이 변경할 것이다.

class Player
{
public:
	void func(void) noexcept
	{
		// 플레이어 관련 동기화 데이터 전송
		//httpClient->requect();
		tcpClient->send();
	}
};

 

근데 얼마 안가서 또 이번에는 TCP의 "연결지향성"이 여전히 느리다 판단하고, UDP로 변경하자고 한다....

이럴때마다, 코드를 다 엎는 거는 힘들고(심지어 라이브 서비스중) 지친다. 이럴때 "방문자 패턴"을 적용해보자.

class NetworkingVisitor
{
public:
	virtual void visit(Player* player) noexcept = 0;
};

class TCPVisitor : public NetworkingVisitor
{
public:
	virtual void visit(Player* player) noexcept
	{
		// tcp용 패킷 만들기
		_tcpClient->send();
	}
};

class HTTPVisitor : public NetworkingVisitor
{
public:
	virtual void visit(Player* player) noexcept
	{
		// http용 패킷 만들기
		_httpClient->request();
	}
};

networking 기능을 확장시킬 NetworkingVisitor를 interface로 만들고, 프로토콜별로 기능을 수행할 TCPVisitor와

HTTPVisitor를 추가하였다. 

 

이제 Player class는 visitor를 방문하기만 하면 해당 기능을 사용할 수 있다!

#define Networking_Method new TCPVisitor()
//#define Networking_Method new HTTPVisitor();

class Player : GameObject
{
public:
	void func(void) noexcept
	{
		// 플레이어 관련 동기화 데이터 전송
		//httpClient->requect();
		//tcpClient->send();

		NetworkingVisitor* visitor = Networking_Method;
		visitor->visit(this);
	}
};

이렇게 Player코드에서 아예 네트워킹 관련 코드는 빠지게 되고, Player class와

TCPClient class(혹은 HTTPClient)와의 결합도 사라졌다. (c++이니까 define으로 switch 쉽게 해둔 것은 덤.)


서두에서 말했듯 수많은 클래스에 걸쳐서 비슷한 연산(기능)을 추가할때 기존 구조를 확장할 수 있어서

유용하게 사용할 수 있다.

 

또한, 위 예제 처럼 visitor만 교체하면 새로운 기능을 할 수 있으므로, 일종의 "전략 패턴"으로도 볼 수 있다.

728x90

'프로그래밍 > C++' 카테고리의 다른 글

Observer Pattern  (0) 2021.08.13
Bridge Pattern  (0) 2021.07.15
Prototype Pattern  (0) 2021.07.13
Adaptor Pattern  (0) 2021.06.30
Factory Pattern - Simple Factory  (1) 2021.02.21

+ Recent posts