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

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

+ Recent posts