
operator(연산자)는 operand(피연산자)를 parameter(매개변수)로 하는 특별한 내장함수다. 단지 functionName(parameter) 형태로 사용하는 게 아니라 operand operator operand 와 같은 형태로 사용한다는 점에서 일반적인 함수와 다를 뿐이다. operator에 따라 operand operator 라던가 operator operand 같은 형태로 사용할 수도 있다.
operator description example +좌변에 우변을 더한 값을 반환한다 operandA + operandB-좌변에서 우변을 뺀 값을 반환한다 operandA - operandB*좌변에 우변을 곱한 값을 반환한다 operandA * operandB/좌변을 우변으로 나눈 값을 소수점 단위로 반환한다 operandA / operandB~/좌변을 우변으로 나눈 값을 정수 단위로 반환한다 operandA ~/ operandB%좌변을 우변으로 나눈 나머지를 반환한다 operandA % operandB
분명 지난 시간에 operators를 사용하는 실습이 포함되어 있었는데 operators는 이번에 배운다 ㅋㅋ 나눗셈 연산은 두 가지가 있는데 이 둘은 return type이 다르다. 정수 단위 나눗셈 연산이 // 이 아닌 ~/ 라는 걸 헷갈리지 말아야지. Dart의 정수 단위 나눗셈은 홈 디렉토리 같이 생긴 녀석이다.
operator description example <좌변이 우변보다 작은가 ( true/false)operandA < operandB>좌변이 우변보다 큰가 ( true/false)operandA > operandB<=좌변이 우변보다 작거나 같은가 ( true/false)operandA <= operandB>=좌변이 우변보다 크거나 같은가 ( true/false)operandA >= operandB==좌변이 우변과 같은가 ( true/false)operandA == operandB!=좌변이 우변과 다른가 ( true/false)operandA != operandB
관계 연산자는 두 피연산자의 크기 관계에 따라 bool 값을 반환값으로 갖는 함수다. 어려울 거 없다.
operator (example) description ++a전위증가: 값을 1 증가시킨 후 값을 사용한다 a++후위증가: 값을 사용한 후 값을 1 증가시킨다 --a전위감소: 값을 1 감소시킨 후 값을 사용한다 a--후위감소: 값을 사용한 후 값을 1 감소시킨다
i++; 처럼 사용할 때는 상관 없지만 b = a++; 처럼 대입 연산자와 함께 사용할 경우 전위 증감연산자인지 후위 증감연산자인지 잘 신경쓰도록 해야 한다. c = a++ + --b; 같이 복잡해질 경우 헷갈리기 쉬운데, 가독성 측면에서 좋지 않아 난 개인적으로 이런 건 지양하는 편이다. 성능 차이가 크지 않다면 이해하기 좋은 코드가 좋은 코드 아닐까.
operator description example &&AND: 좌변과 우변이 모두 참인가 operandA && operandB||OR: 좌변과 우변 중 참인 게 있는가 operandA || operandB!NOT: 참/거짓 반전 !operand
이산수학의 명제 파트에서 다루는 내용이다. 아주 오래 전에는 명제가 어쩌고 하면서 중학교 교육과정에 있었는데 지금은 빠졌나 축소되었나 할 것이다. 컴퓨터공학을 전공하면 필수 교양 수업으로 한 학기 정도 다룬다.
연산자는 대체로 다른 언어랑 다를 게 없으니 가볍게 훑고 넘어간다. 모든 설명은 교육 자료의 내용과 별개로 내 표현대로 작성한 것이다.
collection 은 여러 개의 변수(element라고 부른다. item이라고도 하고.)를 하나의 구조에 모아 놓은 것이다. Dart의 모든 자료형은 Class이므로 method와 property를 사용할 수 있다. 수학 시간에 다룬 집합과 유관한데, 집합도 교육과정에서 빠졌던 기억이... 아마 중학교 1학년 때 '집합과 명제'라는 이름으로 같이 있었던 것 같다.
ListList emptyList = []; List<String> listOfText = [ 'text0', 'text1', 'text2' ]; List listOfAnything = [ true, 1, 'something', 3.14 ];
순서를 나타내는 index를 통해 element를 식별하기에 순서가 있는 집합이며, 값이 같아도 index로 구분 가능하므로 중복을 허용한다. index는 0부터 시작한다. C언어에서 array[i] == *(array + i) 로 취급하여 시작점에 해당하는 첫 번째 값의 인덱스가 0이었던 것이 아마 index가 0으로 시작하는 것의 시초인 걸로 알고 있다.
제네릭을 사용하여 따로 자료형을 지정해 주지 않으면 기본값은 List<dynamic> 으로, 어떤 자료형이든 넣을 수 있다. 심지어 List 도 다른 List 의 element로 사용할 수 있다.
length, first, last, isEmpty 등의 property와 add(NEW_VALUE), insert(INDEX, NEW_VALUE), clear() 등의 method를 사용할 수 있다. property와 method는 다 외우고 있을 필요 없고, 필요할 때 찾아가며 사용하는 걸로 충분하다.
MapMap emptyMap = {}; Map<String, int> countOfAnimals = { 'giant panda': 5, 'lesser panda': 3, 'deep one': 0 }; Map mapOfAnything = { 'Map 인가': true, 1: 'key가 숫자일 거면 그냥 List를 쓰고 말지', countOfAnimals: '객체도 key가 될 수 있지만 굳이?', 'key는 String이 일반적이다': 'value는 뭐 아무거나' };
정의된 순서와 상관 없이 key를 통해 element를 식별하기에 순서가 없는 집합이며, 식별자인 key는 중복을 허용하지 않고, 값이 같아도 key로 구분 가능하므로 value는 중복을 허용한다.
제네릭을 사용하여 따로 자료형을 지정해 주지 않으면 기본값은 Map<dynamic, dynamic> 으로, key와 value 모두 어떤 자료형이든 넣을 수 있다. 심지어 객체도 key가 될 수 있지만 key는 String 을 사용하는 게 일반적이다.
Map 에도 다양한 property와 method가 있으며 모두 외울 필요는 없다.
SetSet emptySet = {}; Set programmingParadigms = { '객체지향', '절차지향', '함수형' };
어떤 값을 포함하고 있는지 여부만 관심 있기에 그게 어디에 있는지 등은 중요하지 않아 순서가 없고 중복을 허용하지 않는 집합이다. 그래서 특정 element에 접근해서 그것을 수정한다거나 하지 않고, 이 집합 자체에 대한 연산이 주를 이룬다.
Enum 과 겹치는 부분도 있고 해서 잘 쓰이지 않는다. 봄에 SW 기초교육 들을 때도 Python의 Set 도 잘 안 쓰이니 몰라도 된다고 하셨던 것 같다.
branch: 분기, 가지. 같은 뿌리에서 시작하여 조건에 따라 이 가지로 갈 수도 있고 저 가지로 갈 수도 있고. 조건문도 다른 언어의 조건문과 크게 다르지 않으니 가볍게 살펴보도록 한다. UPPER_SNAKE_CASE로 작성된 부분은 실제 코드에서는 적절한 표현식으로 바꾸어야 한다.
if-elseif (CONDITION) { TASK; }
CONDITON이 참일 경우TASK를 수행한다.
if (CONDITION) { TASK1; } else { TASK2; }
CONDITON이 참일 경우TASK1를 수행하고, 거짓일 경우TASK2를 수행한다.
if (CONDITION1) { TASK1; } else if (CONDITION2) { TASK2; } else { TASK3; }
CONDITON1이 참일 경우TASK1를 수행하고,CONDITON1이 거짓이고CONDITON2이 참일 경우TASK2를 수행하며, 모든 조건이 거짓일 경우TASK3을 수행한다.
else if 는 필요한 만큼 추가하면 된다. 위에서부터 가장 먼저 만나는 참인 명제에 대한 코드 블록이 수행되며, 끝까지 참인 명제를 마주치지 못할 경우 else 블록이 수행된다.
switchswitch (STATE) { case PATTERN1: TASK1; case PATTERN2: TASK2; default: TASK3;
STATE의 패턴이PATTERN1과 일치하면TASK1이 수행되고,PATTERN2와 일치하면TASK2가 수행되며, 그 어떤 패턴하고도 일치하지 않으면TASK3이 수행된다.
Dart 3.0 이전의 switch 문은 C언어의 그것과 마찬가지로 break; 문을 반드시 써주어야 했지만 이제는 그렇지 않다고. 현 시점 Dart의 switch 문은 Rust의 match 문과 닮아 있다. pattern matching을 사용하고, break; 없이도 딱 그것만 실행된다. 심지어 case: 가 아니라 => 를 사용한 pattern matching도 할 수 있다!
? :conditional operator. 다른 언어에서는 흔히 삼항 연산자(ternary operator)라고 부른다.
CONDITION ? VALUE_IF_TRUE : VALUE_IF_FALSE;
CONDITON이 참이면VALUE_IF_TRUE를 수행하고, 거짓이면VALUE_IF_FALSE를 수행한다.
기본적으로 if-else 와 크게 다르지 않다. 나는 분기문이 복잡한 경우에는 if-else 를 사용하고 CONDITION, VALUE_IF_TRUE, VALUE_IF_FALSE 가 모두 간결한 경우에만 이것을 사용한다. 한눈에 파악하기 어려운 경우에 ? : 로 작성하는 걸 지양한다. 가독성이 떨어지면 유지보수성이 떨어진다.
for 문for (INITIAL; CONDITION; INCREMENT) { TASK; }
INITIAL으로 초기값을 설정한 후TASK를 수행한 뒤INCREMENT를 수행하고CONDITION을 확인한다.
CONDITION이 참이라면 다시TASK를 수행한 뒤INCREMENT를 수행하고CONDITION을 확인한다.
CONDITION이 거짓이라면 반복을 종료하고 다음 코드로 넘어간다.
for-in 문for (ELEMENT in ITERABLE) { TASK; }
ITERABLE의 첫 번째 element가ELEMENT에 들어간 채TASK를 수행한다.
ITERABLE에 다음 element가 있다면 그것이ELEMENT이 들어간 채 다시TASK를 수행한다.
ITERABLE의 끝에 도달하면 반복을 종료하고 다음 코드로 넘어간다.
forEach method이건 엄밀히 말하면 반복문은 아니고 객체의 method다.
OBJECT.forEach((ELEMENT) { TASK; };iterable 객체
OBJECT의 첫 번째 element가ELEMENT에 들어간 채TASK를 수행한다.
OBJECT에 다음 element가 있다면 그것이ELEMENT이 들어간 채 다시TASK를 수행한다.
OBJECT의 끝에 도달하면 반복을 종료하고 다음 코드로 넘어간다.
위와 같은 형태보다는 아래와 같은 형태로 작성하는 게 좋다. 위의 경우 이 반복문이 무엇을 하는 것인지 직접 해석해야 하지만 아래와 같이 구현할 경우 함수 이름으로 작업을 유추할 수 있다.
OBJECT.forEach(TASK_FUNCTION); void TASK_FUNCTION(ELEMENT) { TASK; }
break 과 continue 를 통한 반복 변주반복을 하다가 break 를 만나면 더 이상의 반복을 수행하지 않고 loop 문 밖으로 나가 다음 코드로 넘어간다. continue 를 만나면 이번 작업을 중단하고 다음 반복 회차로 넘어가서 loop 문을 이어간다. loop 문 내에 branch를 사용하여 특정 조건을 만나면 break 로 반복을 아예 중단하거나 continue 로 해당 회차만 패스할 수 있다.
특정 조건을 만족하는 데이터를 만날 때까지만 작업을 수행하고 싶으면 break 를 사용하면 되고, 특정 조건을 만족하는 데이터를 제외하고 작업을 수행하고 싶으면 continue 를 사용하면 된다.
while 문while (CONDITION) { TASK; }
CONDITON이 참이면TASK를 수행한다.
여전히CONDITION이 참이라면 다시TASK를 수행한다.
CONDITION이 거짓이 되었다면 반복을 종료하고 다음 코드로 넘어간다.
do { TASK; } while (CONDITION);
CONDITON과 무관하게 일단TASK를 수행한다.
CONDITION이 참이라면 다시TASK를 수행한다.
CONDITION이 거짓이 되었다면 반복을 종료하고 다음 코드로 넘어간다.
조건이 처음부터 거짓이었을 경우 코드 블록을 한 번은 수행할 것인지 완전히 패스할 것인지에 따라 while 혹은 do-while 중 적절한 방식을 사용한다. 5회 시도할 수 있는 로그인 기능을 콘솔에서 구현할 경우 최초로 한 번은 무조건 로그인을 시도해야 하니 do-while 을 사용한다. while 로 구현해도 문제는 없다.
놀다가 걸렸다 ㅋㅋ Dart에서는 이 로직을 어떤 문법으로 작성할까 하는 가벼운 느낌으로 실습하고 있었는데, 실습이 크게 필요하지 않은 수준임을 들켜 버렸다(?). 아래는 놀던 흔적이다.
완전 놀기만 한 건 아니고, 다른 수강생 분의 질의응답을 구경하기도 했다. Dependency Injection, 그러니까 의존성 주입에 대한 거였다. encapsulation이 어쩌고 init이 어쩌고 하는. 나는 기본값이 private인 언어에 익숙하다 보니 그냥 자연스럽게 인식하고 있던 부분이었다. (바꿔 말하면, Dart 같은 데에서는 기본값이 public이라는 사실을 잊지 말도록 주의해야 한다는 것이기도 하고.) Dependency Injection을 사용하면 데이터를 encapsulation한 상태로 각 객체 생성 시 적절한 값을 넣어 class 정의를 수정하지 않고 각 객체가 사용하는 의존성 객체를 지정할 수 있다.
그러니까 NaverController 와 KakaoController 를 따로 정의하지 않고 Controller 하나만 정의해도 Controller(repo=NaverRepository()) 로 생성하냐 Controller(repo=KakaoRepository()) 로 생성하냐에 따라 내부적으로 다른 repo 를 사용할 수 있는 거다. 이렇게 구현하면 나중에 GoogleRepository() 를 사용하는 코드가 추가되어도 Controller class를 수정하지 않고도 코드를 작성할 수 있으며, 테스트용 MockRepository() 같은 class를 생성하여 테스트하기도 용이하다.
그런 거 듣다가 자리로 돌아와서 실습 코드 건드리며 놀고 있었는데, 프로젝트에 대한 걸 좀 고민해 보라고 하셨다. 어떤 앱을 만들어 보고 싶은지. 너무 단순한 거 말고. 기존에 있는 앱을 벤치마킹하여 구현해 볼 수도 있고, 가족이나 주변 사람들에게 도움이 될 만한 앱을 만들어 볼 수도 있고. 당장은 떠오르는 게 없는데 어떤 걸 해보면 좋으려나.
내가 쓰는 앱은... 니트컴퍼니에서 Band를 사용하고, 개인적으로 사용하는 건 Telegram, Instagram, 브런치, 스피릿, Mastodon, Discord, Notion, ... 벤치마킹할 만한 게 뭐가 있지.
아 근데 아키텍처 쪽은 기술 면접 같은 데서 좀 약점이 될 수 있을 것 같아 공부가 필요할 듯하다. 뭔지 모른 채 그냥 그렇게 쓰는 것들이 많다. 이름을 들으면 뭔지 모르는데 설명을 들으면 "아 그거... 그거 따로 신경써서 적용해야 하는 거였어요...?" 한다거나. 내가 쓰는 방식이 어떤 건지를 알아야 그것의 장단점을 파악하고, 상황에 따라 다른 방식도 적용하고 할 수 있을 듯.