웹 호스팅, 배포 포스팅이랑 Docker 포스팅에서 언급했지만, 웹 애플리케이션을 개발하고 배포, 운용하는 과정은 상당히 복잡하다.

2024.04.11 - [Cloud Computing] - [Docker] Docker? 개념과 간단한 설명

 

[Docker] Docker? 개념과 간단한 설명

 

jghdg1234.tistory.com

2024.04.11 - [Cloud Computing] - 웹 서비스 배포, 호스팅

 

웹 서비스 배포, 호스팅

내가 처음 GUI수업을 들었을때가 생각난다. 처음 HTML로 못생긴 웹을 찍어내고, 그래도 내 딴엔 자료구조수업만 들었어서 처음으로 내 손으로 뭔가 웹사이트를 만든게 너무 신기했다. 그래서 이걸

jghdg1234.tistory.com

 

자 그럼, 이번에는 내가 만든 React feedback웹 애플리케이션을 Dockerize해 보도록 하겠다. 먼저 '도커화' 즉, dockerfile을 만들고, 이미지로 변환을 시켜야 컨테이너에서 운용 할 수 있다고 했는데, 어떻게 해야 할지 단계별로 살펴보자.

 

그 전에, DockerHub에 대해 정말 간단히 짚고 넘어가 보자. Dockerhub은 간단히 말해 우리가 흔히 생각하는 Appstore같은 것이다. 우리가 필요한 앱들을 우리 컴퓨터에 다운 받아서 쓰듯이, Docker Hub에서 우리는 program을 다운받는 대신 'image'라는것을 다운받는다. 그리고 program을 실행하면 process가 생성되는것 처럼, docker의 image을 실행하는것을 container 라고 부른다. 이렇게 비유한 이유는 뭐냐면, 하나의 프로그램이 여러개의 독립적인 프로세스를 가질 수 있듯이, 하나의 image는 여러개의 container를 실행 할 수 있다.

출처 : 생활코딩 Youtube

위 그림처럼 hub에서 이미지를 다운받는것을 'pull'이라고 하며, 그 이미지를 컨테이너에서 실행 시키는것을 'run'이라고 한다.

1. Docker를 설치한다.

Docker공식 홈페이지를 가면 OS에 따라 다운로드 가능한 버전들이 잘 나와 있다. 설치가 되면 터미널에 docker --version을 확인해 주는것을 항상 잊지 말자!

 

Docker가 정상적으로 설치 되었으면, 이제 나의 react app이 잘 돌아가는지 확인 해 보자. npm start를 했을때 정상적으로 페이지들이 렌더링 된다면 ok!

 

2. DockerFile 작성하기

이제, 가장 먼저 dockerfile을 작성해야 한다. 위의 Docker 포스팅에 가 보면 알겠지만, 먼저 dockerfile을 작성 한 후, 그 파일을 이미지로 변환 시켜야 한다.

 

먼저, 패키지 구조는 아래와 같다. 

이렇게 루트 디렉토리에 DockerFile이라는 파일을 하나 생성해 준다.

 

  • DockerFile 구성
FROM node:18-alpine

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm install 

COPY . .

RUN npm run build

EXPOSE 3000

CMD [ "npm", "start" ]

 

FROM node:18-alpine Node.js 버전 18이 설치된 alpine Linux를 기반 이미지로 사용


WORKDIR /src Docker 컨테이너 내에서 작업할 디렉토리를 app/로 설정


COPY package.json package-lock.json호스트 컴퓨터의 package.json와 package-lock.json 파일을 작업 디렉토리에 복사


RUN npm install 명령을 실행하여 필요한 패키지들을 설치


COPY . . 호스트 컴퓨터의 모든 파일을 작업 디렉토리에 복사


EXPOSE 3000 컨테이너의 3000번 포트를 외부에 노출. 애플리케이션이 해당 포트에서 실행될 것임을 나타냄


CMD [ "npm", "start" ] 컨테이너가 실행되면 npm start 명령을 실행

 

  • 컨테이너 이미지 생성
#형식 : 
$ docker build -t <컨테이너 이미지 이름> .

$ docker build . -t feedback-app

성공적으로 생성이 되면 위와 같은 터미널 화면을 볼 수 있을것이다.

 

여기서 한번 더 확인해야 할것이 있는데, 우리는 방금 image를 생성 했으므로 Docker Desktop으로 이동해 이미지가 잘 생성이 되었는지 한번 더 확인 해 준다.

 

 

3. 생성된 image를 기반으로 continaer 실행하기

 

기본적인 형식은 아래와 같다.

$ docker run -d --name <컨테이너 이름> -p 3000:3000 <실행할 이미지 이름>

 

 

이제, 여기 Docker Desktop에 가서 containers를 보면, 우리가 방금 실행한 컨테이너가 올라와 있는것을 볼 수 있다.

 

자 그럼, 이제 우리가 생성한 container가 localhost:3000에서 잘 돌아가는지 한번 확인 해 보자.

 

잘 돌아간다! 이제 DockerDesktop에서 실행중인 container를 정지버튼을 눌러 멈춰보고 실행해 보자. 그러면 이제 실행이 안 될 것이다.

 

짠! 

 

이렇게 간단하게 Docker를 이용해 React애플리케이션을 image화 하고, container를 실행 시켜 보았다. 아직은 초기 단계이지만, 나중에 가면 더 복잡해 질것이다.

이번에는 React를 사용한 웹 애플리케이션에서 페이지간의 라우팅을 어떻게 하는지 정말 간단히 짚고 넘어가겠다.

 

이번 프로젝트에서 요구하는 사이트를 예로 들어보자. 이번 페이지의 규모는 그렇게 큰 규모가 아니기 때문에(아직까지는) 일단은 2개의 페이지만 있다. 로그인페이지, 피드백 페이지.

 

로그인 구현은 아직 하지 못했기 때문에 이번에는 간단히 로그인 버튼을 누르면 피드백 페이지로 이동하도록 라우팅을 설정 하도록 하겠다.

 

리액트에서 라우팅이란, 

단일 페이지 애플리케이션(Single Page Application, SPA) 내에서 사용자가 다양한 페이지를 탐색할 수 있도록 하는 메커니즘을 말한다. 전통적인 웹 애플리케이션에서 페이지 간의 이동은 새로운 페이지 요청을 서버에 보내고, 서버는 새로운 페이지를 클라이언트에 전송하는 방식으로 이루어진다. 이 과정에서 페이지 전체가 새로고침되며, 사용자는 그동안 딜레이를 겪게 된다.

그러나 리액트와 같은 SPA 프레임워크나 라이브러리에서는 클라이언트 사이드에서 라우팅을 처리한다. 즉, 사용자가 링크를 클릭할 때마다 서버에 새 페이지를 요청하는 대신, 리액트가 미리 로드해둔 컴포넌트를 화면에 렌더링하여 페이지를 변경한다. 이 과정에서 실제 페이지 전환은 발생하지 않으며, URL만 변경됨으로써 사용자에게 여러 페이지가 있는 것처럼 느껴지게 한다.

 

리액트 라우팅의 장점:

1. 속도: 모든 리소스가 처음에 한 번만 로드되고, 필요한 컴포넌트만 선택적으로 업데이트되므로 페이지 전환 시 새로고침이 필요 없어 속도가 매우 빠르다.
2. UX: 페이지를 새로고침하지 않기 때문에 훨씬 향상된 UX를 가진다. 사용자는 끊김 없는 화면이동과 즉각적인 페이지 전환을 경험할 수 있다.
3. 효율적인 자원 사용: 필요한 컴포넌트만 로드하고 렌더링하기 때문에, 네트워크 사용량과 서버 부하가 감소한다.
4. 검색 엔진 최적화(SEO)**: 리액트 라우팅 라이브러리 중 일부는 서버 사이드 렌더링(SSR)이나 정적 사이트 생성을 지원하여 SEO 문제를 해결한다.

새로고침 여부:
리액트 라우팅을 사용하면, 사용자가 애플리케이션 내에서 페이지 간에 이동할 때 웹 페이지의 새로고침 없이 뷰가 변경된다. URL은 변경되지만, 이는 브라우저의 History API를 통해 관리되며, 실제로 페이지를 새로 불러오는 것이 아니라 애플리케이션 상태의 변경을 반영한다.

예제 설명:

예를 들어, 로그인 페이지에서 피드백 페이지로의 이동을 구현하려면, 리액트의 `react-router-dom` 라이브러리를 사용하여 라우팅을 설정한다. 로그인 버튼을 누르면, `useNavigate` 훅을 사용하여 피드백 페이지로의 경로를 지정할 수 있다. 이러한 방식으로, 리액트 애플리케이션에서는 사용자의 액션에 따라 끊김 없는 페이지 전환을 구현할 수 있다.


 

자 그럼, 이번 프로젝트에서의 예시를 한번 보자.

 

먼저, 올바른 디렉토리에서 npm install react-router-dom 를 입력해 router-dom을 설치해준다. 여기서 잘 확인해야 할 것은, 현재 설치된 react와 router-dom의 버전에서 불일치가 발생하지 않도록 해야한다.(나도 단지 이것때문에 30분동안 스택오버플로우를 뒤졌다...)

 

설치가 되었으면, package.json으로 이동해 dependencies안에 제대로 설치가 되었는지 확인한다.

 

  "name": "feedback_collection_web_app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "again": "^0.0.1",
    "axios": "^1.6.7",
    "bootstrap": "^5.3.2",
    "react": "^18.2.0",
    "react-bootstrap": "^2.10.0",
    "react-dom": "^18.2.0",
    "react-hook-form": "^7.51.2",
    "react-modal": "^3.16.1",
    "react-router-dom": "^6.22.3",
    "react-scripts": "5.0.1",
    "react-toastify": "^10.0.4",
    "web-vitals": "^2.1.4"
  },

 

이렇게  "react-router-dom": "^6.22.3" 설치가 된것이 보이면, router는 제대로 설치가 된 것이다. 먼저, login page에서 login이라는 버튼을 누르면 feedback page로 이동 할 수 있도록 코드를 살펴보자.

 

먼저, loginpage.js로 이동해 아래처럼 import를 해 준다.

import { useNavigate } from 'react-router-dom';

 

 

React Router v5까지는 페이지 이동을 위해 useHistory 훅을 사용했었다. 그러나 v6부터는 useNavigate로 대체되어, 라우팅 및 페이지 이동 관련 기능을 좀 더 직관적이고 간결하게 사용할 수 있게 되었다. useNavigate 훅은 라우터의 history 스택에 직접적으로 접근하지 않고도 페이지 이동이나 뒤로 가기 등의 작업을 수행할 수 있게 해준다.

 

 

 

자 그럼, 우리는 login버튼 하나만 가지고 있으므로, 그 버튼에 대한 핸들러 함수를 추가한다. 단, 핸들러를 추가 하기 전, navigation을 담당 할 상수를 선언 해 준다.

const navigate = useNavigate();

 

그 다음 핸들러 함수를 작성 해 준다.

 

    const handleLogin = (event) => {
        event.preventDefault();
        toast.info('Navigating to VT CAS login page...');
        // login stuff going on
        setTimeout(() => {
          navigate('/FeedbackForm');
        }, 2000)
    };


...



          <Button onClick={handleLogin} style={{ backgroundColor: 'maroon', borderColor: 'maroon', 
          padding: '10px 20px', fontSize: '16px' }} type="submit">CAS Login</Button>

 

나는 핸들러 함수에 setTimeout함수를 추가해 toast로 유저들에게 로그인 페이지로 이동 중이라고 알려주었다. 저기서 위에서 선언한 상수 'navigate'에다가 ( 를 열고, import한 FeedbackForm이라는 컴포넌트를 넣어준다. 이렇게 하면 버튼이 클릭되었을때, navigation 상수가 알아서 페이지 간 전환을 담당 할 것이다. 이렇게 보니까 정말 간단하다.. 나는 의존성부터 시작해서 여러가지 애를 먹었지만 다음에는 이런 일이 없었으면 좋겠다.

 

이렇게 loginPage.js에서 라우팅을 담당하는 함수를 작성 한 후, React의 엔트리 포인트인 index.js에서 이제 페이지들을 어떻게 렌더링 할지 결정 해야한다.

 

현재 나의 index.js는

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
// Bootstrap CSS
import "bootstrap/dist/css/bootstrap.min.css";
// Bootstrap Bundle JS
import "bootstrap/dist/js/bootstrap.bundle.min";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

그냥 이렇게 <App /> 컴포넌트를 받아 렌더링을 하기 때문에 나는 App.js를 수정해야 한다.

import FeedBackForm from './pages/FeedBackForm';
import LoginPage from './pages/loginPage';
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';


function App() {
  return (
    <React.StrictMode>
    <BrowserRouter>
    <Routes>
      <Route path="/" element={<LoginPage />} />
      <Route path="/FeedbackForm" element={<FeedBackForm />} />
    </Routes>
    </BrowserRouter>
    </React.StrictMode>
  );
}

export default App;

 

보다시피 위에 BrowserRouter, Routes, Route를 import 한 후, 제일 상단을 <BrowserRouter>로 감싼다. 그 후, <Routes> 태그 안에는 우리가 이동하고싶은 페이지들의 경로를 <Route>태그 안에 넣어 준다. 여기서 짚고 넘어가야 할 것이 있다.

 

먼저, element = {<>}안에 들어 가야 할 내용은 우리가 import한 페이지 컴포넌트들을 넣어주면 된다.

 

그리고, 

위에서 보이는 <Route path ="/"는 루트 경로로 매핑 한다는 뜻이다. 즉, path에서 / 를 가진 컴포넌트가 웹을 실행할때 가장 먼저 렌더링 될 페이지이다. 보통의 웹 페이지라면 서비스를 이용하기 전에 로그인을 먼저 해야함으로 나는 login page를 루트 경로로 설정 해 줬다.(이걸 몰라서 화면에 아무것도 뜨지 않아 또 새벽 두시에 스오플을 뒤졌다...)

 

이렇게 모든 과정을 마친 후,

이렇게 페이지간 이동을 할 수 있게 되었다!

자바스크립트로 웹 개발을 한번이라도 해본 사람이라면 절대 모르고 지나칠 수 없는것이 몇가지 있는데, 그중 하나가 바로 DOM(Document Object Model)이다. JS는 기본적으로 DOM을 사용하여 웹 브라우저의 요소들에 엑세스 하고, 이를 수정 할 수 있다.
 

기본 DOM 계층구조

DOM의 기본적인 계층 구조

 

  • Window : 계층 구조의 최상위층. document를 포함한 환경을 제어 할 수 있다. 브라우저가 페이지를 로드하면 자동으로 윈도우 객체가 만들어지고, 자바스크립트 코드에서 윈도우 개체 속성과 함수에 엑세스 할 수 있음.
  • History : 브라우저의 최근 방문 기록에 대한 세부 내용을 기록함. 히스토리 객체는 브라우저가 앞으로 또는 뒤로 가는 버튼 클릭을 시뮬레이션 할 수 있음.
  • Location : 페이지의 URL에 대한 정보가 들어있음
  • Navigator : 클라이언트 인터넷 브라우저 또는 사용자 에이전트의 개체 표현
  • Screen : 디스플레이 화면의 크기와 같은 사용자 화면에 대한 정보를 도출하는데 사용됨. 모바일 장치에서 실행되는 브라우저의 크기를 결정하는데 중요함
  • Document : 페이지 내의 모든 HTML 요소에 대한 엑세스를 제공함. 브라우저에 로드되는 각각의 HTML요소들은 document 객체가 됨.

 
우리가 자주 사용하는 alert()는 사실 winow.alert()에서 가져온것이지만 더 간단하게 표현하기 위해 접두사를 생략해서 사용한다.
 

그렇다면, DOM을 이용하여 html요소에 접근을 어떻게 할까?

가장 많이 사용되는 함수는 document.getElementById("___"); 이다.
 
아래는 자바스크립트 파일에서 HTML요소를 접근하는 예시이다.
 

<div id = "box">

var divTag = document.getElementById("box")

if(divTag) {
//상황에 맞는 로직 구현
}

 
getElementById를 사용해 괄호 안의 id에 할당된 HTML요소를 가져와 변수 divTag에 할당한 다음, 그 다음에는 그 HTML 요소에 우리가 원하는 로직을 추가 할 수 있다. => 정적인 웹에서 좀 더 반응형 웹으로 변하는 과정이다.
 
이제 그럼 inspect 기능을 활용해 자바 스크립트 코드를 써 보자. 웹 브라우저를 열고, 아무곳이나 커서를 올린다음 우클릭을 하면 inspect라는 탭이 있을것이다. 탭을 누르면 콘솔이 보일것이다. 이 콘솔 창에서 여러가지를 실험해볼것이다.
 
먼저, 모든 프로그래밍의 기본 출력 문장인 Hello world를 출력해보자. 자바에서는 System.out.println()을 통해 출력문을 한줄씩 출력했지만, 자바스크립트에서는 console.log을 통해 출력한다. 해보면,
 

이렇게 나온다. 아래의 undefined는 커맨드(console.log)는 아무 값도 리턴을 하지 않는다는 뜻이다.
 
이번에는 정말 간단한 함수를 하나 작성해보자

function printMyInput(user_input) {
    console.log("The parameter passed is " + user_input)
}

함수는 function 이라는 키워드를 통해 선언하고, 리턴 타입은 지정하지 않아도 되며, 괄호 안에는 파라미터를 받는다.
 
여기서 가장 중요한게 나온다. 화살표 함수!
 
자바스크립트 ES6부터 지원하는 화살표 함수다. 사실 난 화살표 함수를 안 좋아한다. 그냥 function키워드를 사용해 함수를 만드는게 더 가독성도 좋고, 직관적이다. 위의 printMyInput을 화살표 함수로 나타내 보겠다.

let printMyInputES6 = (user_input) => {
    console.log(user_input)
}

 
일단 함수를 let으로 선언해 변수화 하고, 더이상 function이라는 키워드 대신 =>를 쓴다. 나처럼 화살표 함수를 싫어해도 쓰는것이 좋을것이다,,, 왜냐면 이미 javascript 코드를 쓰는 사람들은 화살표 함수가 익숙해져있으므로 그냥 얼른 익숙해지는게 답이다 :(
 
마지막으로, 이벤트 핸들러를 짧게 짚고 넘어가보자. 우리가 웹사이트의 버튼들을 눌렀을때, 어떠한 동작을 하게 하고 싶다면, 자바스크립트의 eventHandler를 사용해야 한다. 예시를 보면 바로 이해가 될 것이다!
 

<button type="button" onclick="showAnswers()"> Show Solution
 <script>
  function showAnswers() {
   //code
   alert("A")
  }
 </script>
</button>

 
위의 코드는 'Show Solution'이라는 button을 만들고, onclick이라는 EventHandler를 통해 버튼이 클릭 되었을때 showAnswers()라는 함수를 실행하도록 한다. 간단하다! 이벤트 핸들러는 이런것이다. 정적인 웹에서 조금 더 동적인 웹으로 바뀌어 가는 중이다.

운영체제 강의를 들었을때 수업 처음부터 많이 등장하는 용어가 있다. "프로세스". 

 

그렇다면 과연 프로세스란 무엇일까? 구글링을 하면 가장 많이 나오는 답변은 '현재 실행중인 프로그램'이다. 정말 간단하다. 예를들어 컴퓨터를 켜고 '메모장'을 켠다면, 나의 컴퓨터는 '메모장' 이라는 프로세스를 실행중이다. 그렇다면 조금 더 깊이 들어가서, "프로세스가 실행되려면 무엇이 필요할까?"

 

모든 프로세스는 실행을 위해  CPU가 필요하다. 그 러 나 CPU의 자원은 한정되어 있다.


이 말인 즉슨, 프로세스는 돌아가며 한정된 시간 만큼만 CPU를 이용한다. 아래는 여러 프로세스들이 CPU를 공유하는 모습을 간단히 나타낸 그림이다.

CPU

위의 그림과 같이 프로세스 1이 CPU에 할당을 받는다면, 주어진 시간만큼 CPU를 이용하고, 그 시간이 끝나면 queue에 있는 다른 프로세스에게 CPU를 이용할 권리를 부여한다. 위 그림에 보이는 '타이머 인터럽트'는 현재 프로세스에게 시간이 다 되었다고 알려주는것이다.

 

자 그럼, 여기서 CPU의 상태가 등장한다. 운영체제마다 조금씩의 차이는 있지만, 대부분의 운영체제들은 기본적으로 아래의 상태들을 가진다.

 

CPU 상태

  • 생성상태
    • 이제 막 메모리에 적재되어 메모리를 할당받은 상태
    • 준비가 완료되었다면 준비 상태로
  • 준비상태
    • 당장이라도 CPU를 할당받아 실행 할 수 있지만 자신의 차례가 아니기에 대기중인 상태.
    • 자신의 차례가 된다면 실행상태로!
  • 실행상태(Running)
    • CPU를 할당받아 실행중인 상태
    • 할당된 시간 모두 사용시(타이머 인터럽트 발생 시) 준비 상태로 되돌아감
    • 실행 도중 입출력 장치를 이용하면 입출력 작업이 끝날때까지 대기 상태 유지
  • 대기상태(Blocked)
    • 프로세스가 실행 도중 입출력장치를 사용하는 경우
    • 입출력 작업은 cpu에 비해 느리기에 이 경우 대기 상태로 접어듬
    • 입출력 작업이 끝나면(입출력 완료 인터럽트를 받으면) 준비 상태로
  • 종료상태
    • 프로세스가 종료된 상태

계층구조

우리는 프로세스 실행 도중 시스템 호출을 통해 다른 프로세스 생성이 가능하다.

  • 새 프로세스를 생성한 프로세스 : 부모 프로세스
  • 부모 프로세스에 의해 생성된 프로세스 : 자식 프로세스
  • 부모, 자식 프로세스는 별개의 프로세스이기 때문에 별개의 pid를 가진다.

부모 프로세스는 fork()를 통해 자식 프로세스를 생성하고(자식 프로세스는 부모 프로세스의 자원을 상속받는다), 자식 프로세스는 exec()을 통해 새로운 프로그램을 실행할 수 있도록 함.

부모 프로세스의 fork() 리턴값은 자식 프로세스의 pid이고, 자식 프로세스의 fork()리턴값은 0이다.

 

부모 프로세스는 wait()을 통해 자식 프로세스가 종료 될때까지 기다리거나, kill(pid, SIGKILL)을 통해 강제종료 할 수 있다.

 

즉, 정말 직관적으로 fork()와 exec()을 표현하면,

 

1. 프로세스 A를 fork()를 이용하여 복제본을 만든다.

2. 복제본이 생성되었다(자식 프로세스). 하지만 지금 자식과 부모 프로세스는 똑같은 두개의 프로세스이다.

3. 자식 프로세스에게 부모 프로세스가 하는 일을 하라는 대신, 본인만의 프로세스를 실행시키고 싶으면 exec()를 이용하여 자신의 메모리 공간을 새로운 프로그램으로 덮어쓴다.

이번 학기에 시작한 안드로이드 개발 수업! 아직 아무것도 모르겠고 어색하지만 언제나 그래왔듯 다시 한번 시작해보겠다.

 

원래 나는 무언가를 배우기 시작할때 이론적인것부터 들이밀고 공부하라고 하면 진절머리가 나는 스타일이다. 직접 간단한 앱을 만들면서 차근차근 더 깊이 배워보도록 하겠다.

 

사용 언어는 Kotlin을 사용하고, 코틀린은 자바에 대한 이해가 있으면 코딩하기가 좀 더 수월할거라고 들었다. 다행히 자료구조 알고리즘을 자바로 배운터라 크게 걱정하진 않았다. 앱을 만드는데 쓰는 프로그램은 Android Studio를 썼고, Mac M1 pro에서 사용중이다. 이번에 만들어볼 앱은 정말 간단한 퀴즈 맵이다. 문제가 나오고 True/ False를 선택 할 수 있는 앱이다. 한번 시작해보자!

 

먼저, Android Studio를 실행하고 New Project(Empty)를 누르면 아래와 같은 창이 나온다.

 

중요한 태그 몇개만 짚고 넘어가겠다.

 

  • Name : 말 그대로 "앱 이름" 이다. 여기서는GeoQuiz라고 칭하겠다.
  • Language : 사용 언어는 위에서 말했듯 코틀린이다.
  • Minimum SDK(최소 지원 API레벨) : 이건 현재 만드려는 앱이 어디까지 지원 할 수 있는지를 나타내는 좌표인데, API뒤의 숫자가 커질수록 최신 API이며, 그만큼 현존하는 기기들과 호환되지 않는 경우가 많다. 일반적으로는 낮은 API를 사용한다.

 

그 다음, 이러한 창이 나오면 왼쪽 아래 망치버튼을 누르면 콘솔에 알 수 없는 명령어들이 나오면서 BUILD SUCCESSFUL이라는 문구가 뜬다.

 

 

왼쪽 위에 보면, project view를 설정 할 수 있는데, Android Studio의 디폴트 view는 Android view이다[개발자들이 디폴트값을 이렇게 설정 해놓은데에는 항상 이유가 있다]. Android view는 실제 진행하는 프로젝트의 디렉토리를 숨겨 우리가 현재 진행중인 안드로이드 프로젝트에만 더 집중 할 수 있도록 해준다.

 

Android view와 Project view의 차이

한눈에 봐도 왼쪽이 훨씬 더 간결해보인다.

 

그 다음, 아래와 같이 Empty Views Activity를 누른다. 되게 복잡해 보이고 처음 해보는 사람들은 헷갈리겠지만, 지금은 하나하나가 뭔지 알 필요 없다!

 

 

 

그러면 아래와 같이 처음 보는 코드들이 보일것이다. 

 

이제, 안드로이드에게 앱을 시작할때 화면에 무엇을 로드할지 알려주어야 한다. [UI 레이아웃]

 

 

그 다음, 왼쪽 에디터에 보이는 AndroidManifest.xml파일을 연다. 그러면 처음 보는 코드들이 쓰여있는데, 거기서 <activity/> 태그로 감싸진 부분을 수정해준다. 디폴트는 아래와 같지만

 

<activity
    android:name=".MainActivity"
    android:exported="false" />

 

 

다음과 같도록 수정해준다

 

<activity
    android:name=".MainActivity"
    android:exported="true">
<intent-filter>
    <action android:name="android.intent.action.MAIN" />

    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

 

간단히 짚고 넘어가자면, android : exported = "false"에서 "true"로 변경한 부분과 <intent-filter>부분은 앱에게 "앱을 실행하면 MainActivity가 보여질것" 이라고 변경해준것과 같다.

 

 

다음은 app/res/layout/activity_main.xml  을 열고, 코드를 보면 디폴트 값으로 되어있는데, 이 디폴트 코드들을 아래와 같이 변경해준다.

 <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity">

        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

 

 

 

기본 값은 두가지 view가 있다. Constraint layout과 TextView. 아래는 두가지를 간단히 표현한 그림이다.

 

 

그런데 우리가 만들 앱은 단순히 텍스트 뷰만 있어서는 안된다. 우리가 만들 앱의 MainActivity는 더 많은 view들이 필요하다.

  • a vertical LinearLayout
  • a TextView
  • a horizontal LinearLayout
  • two Buttons

추가적으로 필요한 view들을 나타낸 모습

그러면 나머지 필요한 view들을 activity_main.xml파일에 추가해주자. 원래 있던 코드들은 모두 지워주고 아래처럼 작성하면 된다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="24dp"
        android:text="@string/question_text" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/true_button" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/false_button" />

    </LinearLayout>

</LinearLayout>

 

뭔가 서식들이 전에 웹 프론트엔드를 공부할때 css에서 봤던것들과 비슷해보인다.

 

그런데 여기서 문제가 하나 생긴다. 위의 코드를 작성하면 오른쪽 위에 IDE Warning이 뜰것이다. 버튼에 대한 warning인데, 여러 버튼들이 horizontal layout에 인접해 있으면 뜨는 오류이다. 해결 방법은 간단하다. 버튼 태그 안에 style="?android:buttonStyle" 만 추가해주면 된다.

 

코드를 살펴보면 Textview, Button 는 android : text라는 속성이 있다. 이 속성은 view에게 어떤 텍스트를 보여줄건지 명령하는 속성이다. 하지만 여기서 중요한 부분이 있다. 이 text속성의 값은 raw 문자열이 아닌 대신에, @string/ syntax를 이용하여 string값을 참조해야 한다. 단순히 hardcoding으로 android : text = "True" 라고 할 수 있지만, 하드코딩은 좋은 방법이 아니다(유지보수 문제). 대신 문자열을 별도의 파일에 넣고 참조하는것이 좋은 방법이다.

 

자 그럼 이제, 참조할 문자열 파일을 만들어보자!

 

모든 프로젝트는 디폴트 문자열 파일 res/values/strings.xml 이 포함되어있다. 이 파일을 열고, 우리가 view에 전달해야 할 문자열을 MainAcrtivity가 참조 할 수 있도록 문자열을 추가해주자.

 

strings.xml파일에 문자열을 더한 모습

문자열 파일은 이름이 꼭 strings가 아니여도 되고, 여러가지 문자열 파일을 만들수도 있다.(단, 무조건 res라는 상위폴더 아래에 만들어야 한다) 각 분야마다 문자열 파일을 따로 만들어 놓는다면(직관적인 문자열 파일 이름과 함께) 유지보수가 더욱 더 쉬워질 것이다.

 

그다음 이제 다시 activity_main으로 돌아가 preview를 본다면 

 

 

짜잔! 우리가 원하는 모습이 되어있다.

 

포스팅이 너무 길어지므로 다음 포스트에서 이어가도록 하겠다!

운영체제란 무엇일까? 우리가 흔히 알고있는 windows, macOS, Linux, Android, IOS등이 있다. 그렇다면 이러한 운영체제는 무슨 일을 할까?

 

컴퓨터의 운영체제는 프로그램 실행에 필요한 자원을 할당하고 프로그램이 올바르게 실행되도록 돕는 '프로그램' 이다.
컴퓨터의 "프로그램"은 컴퓨터의 메모리에 적재된다고 배웠었는데, 운영체제를 위한 메모리 공간이 따로 할당되어 있다. 아래 그림을 보자

운영체제를 간략한 그림으로 나타낸 모습

메모리에는 두가지 영역이 존재하는데, 커널 영역, 사용자 영역으로 나뉜다.

 

 커널 영역

  • 목적: 운영체제의 핵심 기능을 수행하는 코드와 데이터를 저장하는 영역이다. 여기에는 운영체제의 커널(kernel)이 위치하며, 시스템의 핵심 기능을 관리하고 실행한다.
  • 권한: 가장 높은 권한을 가지고 있어, 하드웨어 및 다양한 시스템 리소스에 접근할 수 있다.
  • 기능: 프로세스 스케줄링, 메모리 관리, 입출력 관리, 인터럽트 처리 등과 같은 핵심적인 시스템 기능을 담당한다.

사용자 영역

  • 목적: 사용자 애플리케이션 및 프로세스가 실행되는 영역으로, 실제 응용프로그램의 코드와 데이터가 위치한다.
  • 권한: 상대적으로 낮은 권한을 가지고 있어, 일반적인 응용프로그램이 시스템 자원에 직접 접근하지 못하도록 보호한다.
  • 기능: 사용자 애플리케이션의 실행, 데이터 처리, 파일 시스템 접근 등을 담당한다.

운영체제의 메모리 관리

위의 그림에서 우리는 메모장을 실행할때 컴퓨터에 "메모리 1000번지에 메모장을 실행시켜줘" 라거나, 프로그램을 종료할때 "1000번지에 있는 메모장을 종료해" 라고 하지 않는다. 우리는 단순 x버튼을 눌러 메모장을 닫지만, 저 너머의 운영체제는 1000번지로 찾아가 해제시켜준다.

 

 

그렇다면 운영체제를 사용할때의 장점은 무엇일까?

- 운영체제는 응용 프로그램들이 자원에 접근하려고 할때, 오직 자신을 통해서만 접근하도록 하여 자원을 보호한다.

 

즉, 운영 체제는 자원과 응용 프로그램의 소통의 다리 역할을 하는 매개체이다.

 

운영체제가 하드웨어에 접근하는 모습

 

마지막으로, 운영체제의 핵심 기능들은 무엇일까?

  • 프로세스(== 현재 실행중인 프로그램) 관리
    • 컴퓨터에서 여러 프로그램을 동시에 실행한다고 가정할때, 사실 컴퓨터는 여러가지의 프로그램들을 "동시에" 실행하고 있는것은 아니다. 커널 영역에 존재하는 운영체제가 매우 빠르게 "번갈아가며" 이 프로그램들을 관리한다.
  • 자원 접근 및 할당
    • CPU = (CPU 스케쥴링 : 어떤 프로세스를 먼저, 얼마나 오래 실행할까?)
    • 메모리 = (페이징, 스와핑)
    • 입출력 장치
  • 파일 시스템 관리

 

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

You can return the answer in any order.

Example 1:

Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].

Example 2:

Input: nums = [3,2,4], target = 6
Output: [1,2]

Example 3:

Input: nums = [3,3], target = 6
Output: [0,1]

Constraints:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109

다시 말해, 이 문제에서 해결하고자 하는 문제는

주어진 배열에 대한 각 정수에 대하여 두개의 각기 다른 인덱스의 정수의 합이 target과 같도록 두개의 인덱스를 반환해라.

몇가지 방법이 있는데 내가 처음 생각난 방법은 Brute Force이다. 말 그대로 하나하나씩 일일히 짝을 찾아보는것이다. 예를들어, [2,7,11,15]의 배열이 있고, 타겟이 9일때, for문을 돌면서 비교한다. 예를들어, 배열의 첫번째 요소인 2부터 시작해서 이중 for문을 사용하여 그 다음 요소와 계속 비교한다. 그렇지만 이 문제에서는 정확한 두개의 인덱스를 반환해야하고, 배열은 정렬되어 있지 않기 때문에 수많은 탐색 시간이 소요된다. - O(n^2).

두번째 - 두개의 포인터를 이용.[해시테이블]

먼저 해시 테이블에 대해 알아야 한다. 해시 테이블이란 Key-Value pair로 매핑되는 가장 중요한 자료구조중 하나로써, 해시 함수를 이용한 삽입, 검색, 삭제에서 낮은 시간복잡도를 보인다[해시 테이블에 대해 잘 모른다면 유튜브나 구글링을 해보면 쉽게 이해 할 수 있다]. 그렇다면 위의 문제에서 해시 테이블을 어떻게 활용해야할까? [파이썬 에서는 해시맵이나 테이블 대신 딕셔너리 라고 부른다.] {} 기호로 표시한다.

먼저 리턴값을 생각해보자. 우리는 두개의 인덱스를 리스트형태로 반환해야 한다. 두개의 값을 더해 의도된 정수값이 나오는 방법을 생각해보자.

1. 리스트를 순회하며 타겟값에서 리스트의 정수값을 뺀다.
- 이유는 예를들어 타겟이 9이고 리스트의 0번째 정수가 2라면 9에서 2를 뺀 7을 리스트에서 찾아내면 그대로 반환하면 된다.

이해가 안 간다면 다시, 주어진 리스트 list = [1,5,3,12,9], 타겟을 10이라고 해보자. 이 리스트에서 가능한 조합은 1+9 이다. 리스트의 순서는 상관 없다고 했으므로 리턴되는 리스트 값은 [0, 4] 또는 [4,0]이여야 한다.

 

자 그렇다면 위의 내용을 코드로 어떻게 구현할까?

 

파이썬의 딕셔너리(aka HashTable)은 {} 이 기호로 표시해준다. 자료구조 수업에서는 자바를 이용했는데 해시맵을 import하고 객체를 만들때도 HashMap<Integer> map = new HashMap<>(); 처럼 직접 자료형을 선언해줬어야 했는데 이런거 보면 파이썬은 다른 프로그래밍 언어보다 훨씬 간결해보인다.

 

1. 딕셔너리 선언하기

numDictionary = {}

 

{}로 표현한 딕셔너리에 numDictionary라는 이름을 부여해줬다. 이제 우리의 딕셔너리는 이 이름으로 참조하면 된다.

 

2. 반복문

이제 반복문을 통해 리스트를 순회하면서 주어진 값을 찾아야 한다. 가독성이 쉽도록 리스트의 길이는 변수로 만들겠다.

반복문중에서 가장 많이 쓰이는  for문은 파이썬에서 이렇게 쓰인다.

n = len(list)

for i in range n :
	complement = target - nums[i]

 

여기서의 complement는 타겟값에서 리스트의 정수를 뺀것이다. 즉, 이 변수는 우리가 리스트에서 찾아야 할 값이다.

 

3. 조건문

파이썬의 여러가지 메서드중 편리한 built-in 메서드가 있는데, if a in b 메서드이다.

b안에 a가 있으면 true를 반환하고, 그렇지 않으면 false를 반환한다. 여기서의 a는 딕셔너리 안의 "키값"을 참조한다.  b의 자료형은 리스트, 딕셔너리, 튜플, 문자열(즉, 순서가 있는) 모두 가능하다.

 

이제 조건문을 통해 우리가 계산한 complement(보수, 즉 찾아야 할 값)이 리스트 안에 있는지 확인해보면 된다.

n = len(list)

for i in range n :
	complement = target - nums[i]
    if complement in numDictionary :
    	return [numMap[complement], i]
    numDictionary[nums[i]] = i #key - value 매핑
return []

 

 

if문을 살펴보자. 만약 딕셔너리 안에 보수가 있다면 현재 루프의 기준 인덱스 i와 그 보수값의 인덱스를 찾아 리턴하면 된다. 만약 딕셔너리 안에 찾으려는 보수값이 없다면, 딕셔너리 안에 키-값으로 매핑된 값을 넣어준다. 여기서의 Key는 리스트에서의 인덱스 i에 해당하는 값이고, value는 for문을 도는 기준값 i이다. 원하는 값이 없다면 비어있는 리스트를 리턴한다.

 

위에서 든 예시를 다시 코드와 함께 보자.

 

주어진 리스트 [2,7,11,15], 타겟은 9이고, 텅 빈 딕셔너리{} 가 있다. 리스트의 길이는 4이고, for문을 돈다면 0부터 3까지 순회할것이다.

 

  • 첫번째 루프 (i = 0)
    • complement = 9 - nums[0] = 7 이다.
    • if 7 in numDictionary : => 현재 딕셔너리는 비어있으므로 False를 반환하고 if문 밖으로 빠져 나간다.
    • numDictionary[nums[0]] = 0 => 딕셔너리 안에 키 2, value값 0을 매핑해준다. {2 : 0}
  • 두번째 루프 (i = 1)
  • complement = 9 - nums[1] = 2 이다.
  • if 2 in numDictionary : 현재 딕셔너리에 2 라는 키값이 존재하는지 확인한다. 근데 우리가 위에서 {2 : 0}의 쌍을 딕셔너리에 추가했다. 그렇다면 이 조건문은 true가 반환되어 if문 안의 리턴문이 실행될것이다.
  • 리턴 타입은 리스트로 지정되었기 때문에, return []형식으로 리스트를 반환한다. 첫번째 반환값은 numDictionary[2]이다. 이렇게 하면 딕셔너리에서 키값 2에 해당하는 value를 찾아 리턴할 것이다. 위에서 우리는 딕셔너리에 {2 : 0}을 매핑하였으므로 위의 구문에선 0이 리턴된다.
  • 그 다음 리턴값은 현재 순회중인 i값을 나타낸다. 그렇다면 차례로 [0, 1]이 리턴되고, 주어진 리스트 nums = [2,7,11,15]에서 0번째, 1번째 값 (2 + 7)은 9이므로 정답이다.

 

 

+ Recent posts