// Fasly
undefined
null
0
-0
NaN
""
0n
// Truthy
// 7가지 Fasly 값을 제외한 모든 것
"hello"
123
[]
{}
() => {}
// 어떤 상황에 사용?
function printName(person) {
if (!person) {
return;
}
console.log(person.name);
}
let person;
printName(person);
단락평가
false && true 인 경우에는 false만 나와도 뒤에 부분은 출력하지 않는다.
연산에도 Truthy와 Falsy도 사용이 가능하다.
function returnFalse() {
console.log("False 함수");
return false;
}
function returnTrue() {
console.log("True 함수");
return true;
}
console.log(returnTrue() && return False());
console.log(returnTrue() || return False());
// 활용 사례
function printName(person) {
const name = person && person.name;
console.log(name || "person의 값이 없음");
}
printName();
printName({ name: "트럼프" });
구조분해할당
// 배열의 구조분해 할당
// 순서대로 변수에 배열의 정보를 저장
let [one, two, three] = arr;
// 객체의 구조 분해 할당
let {name, age, hobby} = person
// 객체 구조 분해 할당을 이용해서 함수의 매개 변수를 받기
const func = ({name, age, hobby, extra}) => {
...
}
func(person);
Spread 연산자
// Spread
let arr1 = [1, 2, 3];
let arr2 = [4, ...arr1, 5, 6]
let obj1 = {
a: 1,
b: 2
}
let obj2 = {
...obj1, // obj1 프로퍼티 전부 들어감
c: 3,
d: 4
}
원시타입 객체타입
원시타입은 값이 복사가 되고, 객체 타입은 주소가 저장이 된다.
원시타입은 값이 변경이 되어도 메모리에 있는 값은 수정되지 않고 추가가 된다.
따라서 원시타입은 불변값이다.
객체는 참조값을 이용해서 돌아가기 때문에 잘 생각하자.
얕은 복사와 깊은 복사를 생각해보자.
(남궁성 자바의 정석 들으면 이해됌)
반복문으로 배열과 객체 순회
// 배열 순회
let arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// for of 반복문
// for of 는 배열에만 사용
for (let item of arr) {
console.log(item);
}
// 객체 순회
let person = {
name: "트럼프",
age: 67
}
let keys = Object.keys(person);
for (let key of keys) {
console.log(key, person[key]);
}
let values = Object.values(person);
for (let value of values) {
console.log(value);
}
// for in
// for in은 객체에만 사용
for (let key in person) {
cons value = person[key];
console.log(key, value);
}
배열 메소드
//push
// pop
// shift(느림) : 배열의 맨 앞에 있는 요소를 제거, 반환
// unshift(느림) : 배열의 맨 앞에 요소를 추가하는 메소드, 변경된 배열의 길이를 반환
느린 이유는 내부에서 정렬 동작을 하기 때문에
// slice
let arr5 = [1, 2, 3, 4, 5]
arr5.slice(2, 5); // 3, 4, 5
arr5.slice(2); // 3, 4, 5
arr5.slice(-3); // 3, 4, 5
// concat
// 배열 붙이기
let arr6 = [1, 2];
let arr7 = [5, 6];
arr6.concat(arr7); // 1, 2, 5, 6
// forEach
let arr1 = [1, 2, 3];
arr1.forEach(function (item, idx, arr) {
...
})
let doubledArr = [];
arr1.forEach((item) => {
doubledArr.push(item * 2);
})
// includes
// 배열에 특정 요소가 있는지 확인하는 메소드
let arr2 = [1, 2, 3];
let inInclude = arr2.includes(10);
// indexOf
// 인덱스 반환
// 얕은 비교
let arr3 = [1, 2, 3];
let index = arr3.indexOf(2); // 1
let index = arr3.indexOf(29); // -1
// findIndex
let arr4 = [1, 2, 3];
const findedIndex = arr4.findIndex(
(item) => item === 999
);
console.log(findedIndex);
// find
// 모든 요소를 순회하면서 콜백함수를 만족하는 요소를 찾거나 요소를 반환
let arr5 = [
{name: "트럼프"},
{name: "힐러리"}
];
const finded = arr5.find(
(item) => item.name === "힐러리"
);
// filter
let arr1 = [
{name: "트럼프", hobby: "콜라마시기"},
{name: "힐러리", hobby: "개소리내기"},
{name: "찰리커크", hobby: "연설"}
];
const tennisPeople = arr1.filter(
(item) => item.hobby === "연설"
);
console.log(tennisPeople); // [{name: "찰리커크", hobby: "연설"}]
// map
// 배열 순회하면서 콜백함수 적용하고 새로운 함수 반환
let arr2 = [1, 2, 3];
const mapResult1 = arr2.map((item, idx, arr) = > {
return item * 2
});
let names = arr1.map((item) => item.name);
// sort
let arr3 = [10, 3, 5];
arr3.sort((a, b) => {
if (a > b) {
return -1; // a가 b 앞으로
} else if (a < b) {
return 1; // a가 b 뒤로
} else {
return 0; // 그대로
}
});
// toSorted(최신함수)
// 정렬된 새로운 배열 반환
let arr5 = ["c", "a", "b"];
const sorted = arr5.toSorted();
// join
// 배열 요소 문자열로 합치기
let arr6 = ["Hi", "I am", "트럼프"];
const joined = arr6.join(" "); // Hi I am 트럼프
Date 객체와 날짜
// Date 객체 생성
let date1 = new Date(); // 생성자, 한국 표준시
// 타임 스탬프
// 특정 시간이 UTC 기준으로 몇 ms가 지났는지 의미하는 숫자값
let ts1 = date1.getTime();
let date4 = new Date(ts1);
// 시간 요소들 추출
let year = date1.getFullYear();
let month = date1.getMonth() + 1;
let date = date1.getDate();
let hour = date1.getHours();
let minute = date1.getMinutes();
let seconds = date1.getSeconds();
// 시간 수정
date1.setFullYear(2023);
date1.setMonth(2);
date1.setDate(30);
date1.setHours(23);
date1.setMinutes(59);
date1.setSeconds(59);
// 포맷
date1.toDateString();
동기 비동기
동기는 순서대로 처리하는 것이다. 즉 완료하면서 진행을 하는 것이다. 여기서 작업은 스레드를 의미한다.
하지만 단점이 존재한다. 특정 스레드가 Task가 오래걸리는 경우, 완료할 때까지 기다려야 한다. C와 Java의 경우 이 문제를 해결하기 위해서 멀티스레드로 동작을 한다. 하지만 JS는 스레드가 1개이기 때문에 멀티스레드로 처리가 불가능하다. 따라서 비동기를 사용한다.
비동기라는 것은 작업을 순서대로 처리하는 것이 아니다.
console.log(1);
setTimeout(() => {
console.log(2);
}, 3000); // 3초, Web APIs에게 대신 실행해줘~
console.log(3);
// 비동기 완료 되면 콜백함수 호출
// 1 => 3 => 2(콜백함수 호출)
JS가 여러개의 Task를 처리할 수 있는 이유는 비동기 작업들은 JS엔진이 아니라 브라우저 별도의 Web APIs에서 실행이 되기 때문이다.
function add(a, b) {
setTimeout(() => {
const sum = a + b;
console.log(sum);
}, 3000);
}
add(1, 2); // 3초 뒤에 3출력
function add(a, b, callback) {
setTimeout(() => {
const sum = a + b;
callback(sum);
}, 3000);
}
add(1, 2, (value) => {
console.log(value);
});
// 1. add 를 부르면 setTimeout 비동기 동작
// 2. 3초 뒤에 setTimeout 내부의 콜백함수 동작 sum을 구하고, callback에 sum을 제공한다.
// 3. callback함수는 add에서 제공한 값으로 console에 찍히게 된다.
// 활용
function orderFood(callback) {
setTimeout(() => {
const food = "떡볶이";
callback(food);
}, 3000);
};
function cooldownFood(cooldownFood, callback) {
setTimeout(() => {
const cooldownFood = `식은 ${cooldownFood}`;
callback(cooldownFood);
}, 2000);
}
function freezeFood(food, callback) {
setTimeout(() => {
const freezeedFood = `냉동된 ${food}`;
callback(freezeedFood);
}, 1500);
}
orderFood((food) => {
console.log(food);
cooldownFood(food, (cooldownFood) => {
console.log(cooldownFood);
freezeFood(cooldownFood, (freezeedFood) => {
console.log(freezeedFood);
});
});
});
하지만 이렇게 작성을 하면 indent가 점점 깊어진다. 이것을 콜백 지옥이라고 한다. 이것을 promise가 해결해준다.
Promise는 비동기 작업을 감싸는 객체이다. 비동기와 관련된 모든 기능을 제공한다.
Promise는 3가지 상태를 가진다. Pending, Fulfilled, Rejected => resolve, reject
const promise = new Promise(() => {
// 비동기 작업 실행하는 함수(콜백함수)
// executor
setTimeout(() => {
console.log("안녕");
}, 2000);
});
console.log(promise); // pending 출력
const promise = new Promise((resolve, reject) => {
// 비동기 작업 실행하는 함수(콜백함수)
// executor
setTimeout(() => {
console.log("안녕");
resolve("안녕"); // 값을 제공해야 한다.
}, 2000);
});
setTimeout(() => {
console.log(promise);
}, 3000);
const promise = new Promise((resolve, reject) => {
// 비동기 작업 실행하는 함수(콜백함수)
// executor
setTimeout(() => {
console.log("안녕");
reject("실패한 이유"); // 값을 제공해야 한다.
}, 2000);
});
setTimeout(() => {
console.log(promise);
}, 3000);
// 활용 사례
const promise = new Promise((resolve, reject) => {
// 비동기 작업 실행하는 함수(콜백함수)
// executor
setTimeout(() => {
const num = 10;
if (typeof num === 'number') {
resolve(num + 10);
} else {
rejcet("num이 숫자가 아닙니다.");
}
}, 2000);
});
setTimeout(() => {
console.log(promise);
}, 3000);
// 활용 사례
const promise = new Promise((resolve, reject) => {
// 비동기 작업 실행하는 함수(콜백함수)
// executor
setTimeout(() => {
const num = 10;
if (typeof num === 'number') {
resolve(num + 10);
} else {
rejcet("num이 숫자가 아닙니다.");
}
}, 2000);
});
// then 메소드
// catch
// chaining 메소드
promise
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
});
// 활용 사례
function add10(num) {
const promise = new Promise((resolve, reject) => {
// 비동기 작업 실행하는 함수(콜백함수)
// executor
setTimeout(() => {
if (typeof num === 'number') {
resolve(num + 10);
} else {
rejcet("num이 숫자가 아닙니다.");
}
}, 2000);
});
return promise
}
const p = add10(0);
p.then((result) => {
console.log(result);
return newP = add10(result);
}).then((result) => {
console.log(result);
}).catch((error) => {
console.log(error);
});
// async
// 특정 함수를 비동기로 만든다
// Promise를 반환한다
async function getData() {
return {
name: "트럼프",
id: 1
}
}
console.log(getData());
// await
// async 함수 내부에서만 사용이 가능
// 비동기 함수가 다 처리되기를 기다리는 역할
async function printData() {
const data = await getData();
}
printData();
만약 컴포넌트를 구성하지 않는다면 공통적인 부분을 전구 만들어야 한다. 중복 코드가 또한 늘어난다.
컴포넌트 X
따라서 리액트에서는 중복 코드를 컴포넌트로 만들어서 처리한다.
컴포넌트 O
화면 업데이트에 관해서 알아보자. 사용자의 행동에 따라 웹 페이지가 모습을 바꾸는 것을 의미한다.
리액트는 선언형 프로그래밍이기 때문에 화면 업데이트가 쉽다. 여기서 선언형 프로그래밍이라는 것은 과정은 생략하고 목적만 간결히 명시하는 방식이다. (반대로는 명령형 프로그래밍, 목적을 이루기 위한 모든 일련의 과정을 프로그래밍하는 것)
state 값에 따라서 C 컴포넌트의 UI가 변경된다.
정리하면 리액트는 선언형 프로그래밍으로서 업데이트를 위한 동작을 정의할 필요없이 특정 변수의 값을 바꾸는 것만으로도 화면을 업데이트할 수 있다.
빠른 화면 업데이트 처리를 할 수 있다.
먼저 선수 지식이 필요하다.
브라우저 구동 방식
HTML CSS로 만든 페이지를 어떻게 랜더링
화면 업데이트는 어떻게 처리
브라우저 구동 방식
Critical Rendering Path
HTML => DOM(Document Object Model, DOM이라는 것은 브라우저가 자기가 이해하기 쉬운 형태로 변경한 것)
CSS => CSSOM(CSS Object Model)
DOM(요소들의 위치, 배치, 모양에 관한 모든 정보) + CSSOM(요소들의 스타일 정보) => Render Tree(웹 페이지의 청사진)
Render Tree => Layout(요소의 배치를 잡는 작업) => Painting(실제로 화면에 그려내는 과정)
어떻게 업데이트가 처리될까?
업데이트는 JS가 DOM을 수정하면 업데이트가 발생한다.
그 중에서 Layout과 Painting은 시간이 오래 걸리는 과정이다.
오래 걸리는 과정
따라서 JS를 3000번을 수정하면 RenderTree 이후의 과정을 3000번 하는 것이기 때문에 성능이 약화가 된다.
<script>
function onClick() {
const $ul = document.getElementById("ul");
for (let i = 0; i < 3000; i++) {
$ul.innerHTML += `<li>${i}</li>`; // DOM 수정
}
}
</script>
<body>
<button onClick="onClick()">리스트 추가하기</button>
<ul id="ul"></ul>
</body>
3000 번 돔을 수정하기 때문에 4500ms 수행 => 성능이 좋지 않다
<script>
function onClick() {
const $ul = document.getElementById("ul");
let list = "";
for (let i = 0; i < 3000; i++) {
list += `<li>${i}</li>`;
}
$ul.innerHTML = list; // DOM 수정
}
</script>
<body>
<button onClick="onClick()">리스트 추가하기</button>
<ul id="ul"></ul>
</body>
DOM은 1번만 수행, 250ms
1번에 수정을 하도록 만들자
하지만 서비스의 규모가 커질 수록 힘들다. 따라서 React는 이 과정을 자동으로 해준다.(Virtual DOM)
Virtual DOM이라는 것은 DOM을 JS 객체로 흉내낸 것으로 일종의 복제판이라고 생각하자. React는 업데이트가 발생하면 실제 DOM을 수정하기 전에 이 가상의 복제판 DOM에 먼저 반영해본다.
Virual DOM
React 설치 및 설명
public 폴더 : 정적인 리소스 ex) 이미지
jsx 타입
eslint : 개발자 코드 스타일 통일화
gitignore
index.html : 리액트의 기본 틀
vite.config.js : 비트 도구 설정
React App 구동원리
리액트를 npm run dev를 하면 node를 구동해서 Web Server를 구동한다. 따라서 현재 가동중인 웹 서버의 주소가 나온다.
리액트는 노드 기반으로 돌아간다. 기존 JS는 웹 환경에서만 사용이 가능(개발자 도구 창)했지만, Node는 웹 환경이 아니여도 JS를 실행시켜주는 JS의 실행환경(Run Time)이다. 즉, JS가 구동할 수 있도록 만들어주는 실행환경이라고 생각하면 된다.
NPM은 Node Package Manager라는 것이다. Node들의 패키지를 관리하는 도구이다.
Node.js에서는 패키지가 프로그램의 단위이다. Ex) 쇼핑몰 패키지, 웹 패키지 등등
Node.js 모듈 시스템 이해
모듈이라는 것은 특정 기능을 1개의 파일이 아닌 N개의 파일로 나누어서 만드는 것을 의미한다. 여기서 기능별로 나누어진 JS의 파일들을 모듈이라고 한다. 예를 들면, user.js, cart.js, payment.js 이런식으로 모듈을 1개로 처리하는 것이 아니라 분리를 한다.
코드를 모듈로 나누어 관리하고 필요에 따라 import require 사용하는 방식을 모듈 시스템이라고 한다. 예시로는 CommonJS와 ESModule가 있다.
Node.js 라이브러리 사용
프레임워크와 라이브러리
npmjs에는 npm의 백화점이다. 모든 라이브러리가 존재한다.
라이브러리를 추가하면 package.json의 dependencies 에 라이브러리 의존성이 추가된다.
node_modules는 실제 라이브러리가 설치된 저장하는 곳이다.
package-lock.json는 package.json보다 버전 같은 것을 정밀하게 관리하는 곳(더 상세한 내용이 담겨진다)
npm i를 하면 package.json의 dependencies를 기준으로 다시 node_modules를 생성한다. 따라서 node_modules는 Git에 올리지 않는다. 왜냐하면 package.json만 있으면 되기 때문에 무거운 node_modules를 필요로하지 않기 때문이다.
// 변수
let age = 27; // 선언한다 + 초기화한다
// 만약 let age; 까지만 하고 출력한 경우 undefined가 나온다
// let은 중복 선언이 불가능
let age;
console.log(age); // undefined
// 상수
// 상수라는 것은 한번 저장된 것은 변경이 불가능하다.
const birth = '19970822';
birth = '19970101'; // 불가능하다.
// 변수 이름은 협업에 이쁘게 작성하자
자료형
// 원시타입은 기본형 타입이다(5가지)
// Number
// NaN도 Number에 포함된다.
let inf = Infinity
let mInf = -Infinity
// String
let myName = "트럼프";
let myIntroduce = "대단하다";
console.log(myName + myIntroduct); // 트럼프대단하다
console.log(`${myName}는 ${myIntroduce}`);
// Boolean
true
false
// Null
// 아무것도 없다
let empty = null;
console.log(empty); // null
// Undefined
// 선언만 되어있는 것이다.
let none;
console.log(none); // undefined
형변환
// Type Casting은 형을 다른 타입으로 변환하는 것이다.
// 10 => "10"
// 형변환에는 묵시적 형변환과 명시적 형변환이 존재한다.
// 묵시적 형변환은 JS 엔진이 알아서 형 변환을 해준다.
let num = 10;
let str = "20";
const result = num + str; // 묵시적 형 변환 발생
console.log(result); // 30
// 명시적 형변환은 개발자가 내장함수를 이용해서 직접 형 변환을 명시하는 것이다.
let str1 = "10";
let strToNum1 = Number(str1); // 10
console.log(10 + strToNum1); // 20
// 응용
let str2 = "10개"
let strNum2 = parseInt(str2); // 10 만 반영이 된다
console.log(strNum2); // 10, '개'는 제거
let num1 = 20;
let numToStr1 = String(num1); // "20"
console.log(numToStr1 + "입니다."); // 20입니다.
연산자
// == 는 값 자체만 비교를 하는 것이고, === 은 값과 타입까지 같이 비교하는 것이다.
// null 병합 연산자는 null, undefined 값을 제외하고 진짜로 있는 값만 찾아오는 연산자이다.
let var1; // undefined
let var2 = 10;
let var3 = 20;
let var4 = var1 ?? var2; // var1에는 undefined이기 때문에 var2가 저장이 된다.
let var5 = var2 ?? var3; // 2개 모두 존재하는 경우 앞에 값이 저장이 된다.
// typeof 연산자는 값의 타입을 문자열로 반환하는 기능을 하는 연산자이다.
let var7 = 1;
var7 = "hello";
let t1 = typeof var7; // string 타입
// 삼항 연산자는 3개의 항을 사용하는 것이다.
let var8 = 10;
let response = var8 % 2 === 0 ? "짝수" : "홀수"; // 짝수
조건문
if 문과 switch 문이 존재한다. switch 문의 경우 조건이 많은 경우 if 보다 직관적이다.
switch 문에는 default가 존재하고 break가 반드시 필요하다.
반복문
for (let idx = 1; idx <= 10; idx++) {
if (idx % 2 === 0) {
continue;
}
if (idx >= 5) {
break;
}
}
함수
function greeting() {
console.log("오늘 한화이글스 티켓팅 망함..")
}
greeting();
인수는 함수에서 필요한 값을 받는 것을 의미하고 매개변수는 함수에서 필요로 하는 변수를 의미한다.
function getArea((width, height) { // 매개변수
function another() { // 중첩함수
console.log("another");
}
another();
let area = width * height;
return area;
}
getArea(120, 200); // 인수
함수와 호이스팅에 대해서 알아보자. 아래와 같이 작성되어 있는 경우에도 JS는 동작한다. 일반적으로 Java같은 경우에 선언을 하지 않은 함수를 부르면 오류가 발생한다. 하지만 JS의 경우에는 호이스팅이 존재하기 때문에 가능하다. 호이스팅은 끌어올리다라는 의미이다. 따라서 내부적으로 실행전에 위로 끌어올린다. 그러기 때문에 함수를 가독성이 좋도록 유연하게 작성할 수 있다.
getArea(120, 200); // 인수
function getArea((width, height) { // 매개변수
function another) { // 중첩함수
console.log("another");
}
another();
let area = width * height;
return area;
}
함수 표현식과 Arrow Function
function funcA() {
consol.elog("funcA");
}
let varA = funcA;
varA(); // funcA 출력, 함수도 문자와 같은 값으로 JS에서는 다룬다.
let varB = function funcB() { // 이 부분에서 funcB는 생략이 가능하다.
console.log("funcB");
}
let varB = function () { // 익명함수
console.log("funcB");
}
varB(); // funcB
funcB(); // 불가능하다. 왜냐하면 따로 만들지 않았기 때문에
funcA와 같은 경우에는 호이스팅이 가능하지만 함수 표현식을 사용한 varB와 같은 경우에는 호이스팅이 불가능하다.
화살표 함수는 간결하게 사용하기 위해서 사용한다.
let varC = function() {
return 1;
}
let varC = () => {
return 1;
}
let varC = () => 1;
let varC = (value) => value + 1
let varC = (value) => {
// 비즈니스 로직
return value + 1;
}
Callback 함수
콜백함수란 자신이 아닌 다른 함수에 인수로써 전달된 함수를 의미한다. JS에서는 함수도 문자와 숫자취급이 가능하다고 했다. 따라서 함수를 다른 함수의 인수로 사용할 수 있는 것이다.
function main(value) {
value();
}
function sub() {
console.log("sub");
}
main(sub); // sub 출력, 여기서 sub를 콜백함수라고 한다.
let person = {
name: "트럼프",
age: 75,
job: "대통령"
}
// 객체 프로퍼티 다루는 방법에는 점 표기법과 괄호 표기법이 있다.
let name = person.name;
let name = person["name"];
// 새로운 프로퍼티 추가
person.extra = "미국 상남자";
person["extra"] = "미국 상남자";
// 프로퍼티 수정
person.extra = "주식쟁이";
person["extra"] = "주식쟁";
// 프로퍼티 삭제
delete person.extra;
delete person["extra"];
// 프로퍼티 존재유무 확인
let result = "extra" in person; // false
상수 객체에 새로운 객체를 변경시키는 것은 불가능 하다. 하지만 내부의 값 또는 프로퍼티를 추가 수정 삭제는 가능하다.
개인적으로 큰 비용을 지불하는 만큼 많이 준비를 했습니다. 현재 아직 결과가 나오지는 않았지만 어떻게 학습을 하며 시험준비를 했는지 공유를 해보려고 합니다.
AICE 시험은 총 14문제로 이루어져 있습니다.
책에는 상세하게 적혀있지만 제가 느끼기에는 다음과 같았습니다.
데이터 불러오기
데이터 전처리, 결측치 및 시각화와 분석
딥러닝과 머신러닝 모델링 및 해당 모델링 평가
제 기준에서는 크게 3가지로 이루어져 있었습니다.
시험 준비
저는 이론을 보는 것보다 시험문제를 풀며 체득하는 것을 좋아합니다. 따라서 저의 경우에는 AICE 공식사이트에 올라온 샘플 문항을 시험 보기 1주일 전부터 2일에 걸쳐 10번을 반복해서 풀었습니다.
그 결과 전체적으로 시험문제가 어떻게 출제 되는지 알게 되었고, 그 이후에는 다양한 기출문제가 많은 교재 1개를 구매해서 5개 예상 기출문제를 계속 반복해서 풀었습니다. 그러면 대략적으로 어떤 형태로 문제가 나오는지 어떤 부분이 항상 나오는지 알 수 있게 됐습니다.
시험이 2틀 정도 남았을 때부터는 해당 문제들을 지속적으로 반복해서 2번씩 풀면서, 데이터 전처리, 결측치, 시각화와 분석 부분을 집중해서 학습했습니다.
이유는 데이터 불러오기와 모델링과 평가 부분은 생각보다 문제가 형식화 되어 있고, 출제 유형이 반복해서 보이기 때문입니다. 따라서 앞 부분에서 데이터를 불러오고 전처리가 잘 못 가공된 데이터를 모델리을 만들면 시험 점수 자체가 떨어질 수 있다고 생각해 해당 부분의 함수와 사용을 통암기하는 방법을 선택했습니다.
시험
확실히 시험을 봤을 때는 데이터 불러오기와 모델링 및 평가 부분은 형식화 되어 있기 때문에 쉽다고 느꼈습니다. 하지만 데이터 전처리, 결측치 및 시각화와 분석 부분은 예상하지 못 한 함수와 요구사항이 나와서 공식 문서에서 해당 정보를 찾아서 해결을 했습니다.
꼭 데이터 전처리와 결측치 및 시각화와 분석 부분을 깊게 공부하시는 것을 추천합니다.
총평
아직 결과가 나오지는 않았지만 붙을 것 같기도 하고 떨어질 것 같기도 합니다. 하지만 떨어지면 다음번에 또 봐서 붙을 때까지 하면 될 것이라고 생각이 듭니다. 모두들 화이팅!
시험통과
9/6일 오전 9시에 결과가 나왔습니다. 다행이도 100점이 나와서 기분이 좋네요. 다들 반드시 데이터 전처리 부분을 꼭 학습하시기를 바랍니다. 데이터를 불러오는 것은 read_csv 와 같은 함수로 처리하면 되고 모델링 또한 어느정도 형식이 맞춰저 있기 때문입니다.
따라서 데이터 전처리를 하는 부분의 함수들을 많이 외우고 경험한 상태에서 시험을 보시면 저 처럼 모르는 함수가 나왔을 때 공식문서에서 1시간동안 찾는 수고를 할 필요가 없습니다.
따라서 현재 회사의 구조를 클린아키텍처로 바꿔봐야겠다는 생각이 들었다. 왜냐하면 결제와 주문 부분의 비즈니스 로직이 여러개의 도메인에 묶여있었기 때문에 유지보수와 에러가 발생하는 경우 코드를 파악하기 힘들었고, 내가 작성한 FastAPI코드는 전체적으로 객체지향 개발이 아닌 약간의 스크립트형식의 코드였기 때문이다. 또한 테스트 코드를 작성해보려고 하니, 비즈니스 로직이 너무 길고 다양한 외부 모듈에 의존을 하고 있어 테스트하기 어려웠다.
물론 자바와 스프링부트로 자사의 레거시 코드를 마이그레이션 한 적은 있지만, FastAPI를 처음부터 설계할 때는 생각보다 모르는 것들이 많았기 때문에 빠른 기능 구현을 목적으로 개발을 했고, 현재 실 서비스가 운영되는 상태에서 이제는 유지보수의 단계와 비즈니스 로직 중심으로 유지보수와 확장성을 확보하기 위해서 클린아키텍처를 공부했다.
또한 운영이 될 정도의 시스템을 만들어 놓고 보니, API 테스트 코드의 중요성을 많이 느끼게 되어 테스트 코드를 작성하려면 의존성이 적은 형태의 아키텍처가 완성이 되어야 하기 때문이다.
클린 아키텍처의 핵심 목표는 비즈니스 로직을 외부로부터 보호를 하기위한 이유가 있고, 각 계층별 독립적인 테스트가 가능하다. 왜냐하면 컨테이너로부터 의존성을 주입받기 때문에 Mock Container를 만들어서 의존성을 가짜로 주입하면 외부 의존성을 고려하지 않고 비즈니스 로직에 집중한 테스트코드 작성이 가능하다.
또한 Infra 계층에서 외부 라이브러리를 사용해서 언제든지 교체가 가능하다. 마지막으로 개인적으로 중요하게 생각하는 의존성의 방향이 내부로만 향하도록 되어 있다.
Inteface 계층
API, Controller, Web 의 영역
사용자 요청을 Application Layer로 전달한다.
개인적으로는 Interface 계층에서 DTO 변환 책임을 부여하도록 하는 것이 좋은 방법이라고 생각한다.
왜냐하면 Interface 계층 이후 Application 계층에서는 오로지 비즈니스 로직에만 집중을 해야하기 때문에 비즈니스 로직 이후 DTO 변환 작업까지 책임을 지면 테스트코드와 유지보수 측면에서 많이 좋지 않다고 생각하기 때문이다.
Application 계층
Use-Case 정의
도메인 엔티티를 조합해서 동작을 수행
도메인 계층에만 의존 가능
인터페이스로 리포지토리 등을 정의 - 구현은 Infra 계층에서 구체화
Application 계층에서는 Domain을 가지고만 비즈니스 로직이 수행되어야 한다.
왜냐하면 비즈니스 규칙 보호와 의존성 방향 제어 때문이다.
Application 계층은 Domain(VO/Entity)를 사용해야 내부 비즈니스 로지이 외부 입출력 형태(DTO)에 종속되지 않고 테스트 가능하며, 확장성과 유지보수성이 높아지기 때문이다.
이런 이유 때문에 Domain 계층에만 의존이 가능하다는 것이다.
따라서 return 형태 또한 Domain 형태로 변경을 하고 해당 Domain을 Interface 계층에서 DTO로 변환하는 것이다.
Domain 계층
시스템의 핵심 비즈니스 로직과 규칙이 위치한 곳이다.
의존성이 없다. = 가장 안쪽에 위치한다.
인프라 -> 애플리케이션 -> 도메인 순서로 구성되어 있고, 인프라 계층의 경우 구현체에 불과하기 때문에 Domain 계층에서 Infra 계층은 절대로 알 수 없다. 따라서 Domain 계층은 의존하고 있는 것이 없어야 한다.
project/
├── domain/
│ └── user.py # User Entity
│ └── user_repository.py # IUserRepository (인터페이스)
├── application/
│ └── user_service.py # Application Service (IUserRepository 의존)
├── infrastructure/
│ └── user_repository_memory.py # 메모리 기반 레포지토리 구현체
├── dto/
│ └── user_dto.py # DTO 정의 및 변환
├── interface/
│ └── api/
│ └── user_controller.py # FastAPI Router
Interface 계층
from fastapi import APIRouter
from dto.user_dto import UserRequestDto, UserResponseDto
from application.user_service import UserService
from infrastructure.user_repository_memory import InMemoryUserRepository
router = APIRouter()
# 의존성 주입 수동으로 구성 (예시)
user_repo = InMemoryUserRepository()
user_service = UserService(user_repo)
@router.post("/users", response_model=UserResponseDto)
def create_user(request: UserRequestDto):
user = request.to_domain()
saved = user_service.register_user(user)
return UserResponseDto.from_domain(saved)
@router.get("/users", response_model=list[UserResponseDto])
def get_all_users():
users = user_service.list_users()
return [UserResponseDto.from_domain(u) for u in users]
Application Layer
from domain.user import User
from domain.user_repository import IUserRepository
class UserService:
def __init__(self, user_repo: IUserRepository):
self.user_repo = user_repo
def register_user(self, user: User) -> User:
if "@" not in user.email:
raise ValueError("Invalid email")
return self.user_repo.save(user)
def list_users(self):
return self.user_repo.find_all()
Domain Layer
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
self.is_active = True
def deactivate(self):
self.is_active = False
######################################################
from abc import ABC, abstractmethod
from typing import List
from domain.user import User
class IUserRepository(ABC):
@abstractmethod
def save(self, user: User) -> User:
pass
@abstractmethod
def find_all(self) -> List[User]:
pass
Infra Layer
from typing import List
from domain.user import User
from domain.user_repository import IUserRepository
class InMemoryUserRepository(IUserRepository):
def __init__(self):
self.users: List[User] = []
def save(self, user: User) -> User:
self.users.append(user)
return user
def find_all(self) -> List[User]:
return self.users