오늘 학습 키워드
MVP 패턴
오늘 학습 한 내용을 나만의 언어로 정리하기
계산기 만들기
UICalculator가 메인으로 있고 Buttons에서 OnClickButton 만들어서 버튼을 누르면 표시되도록 UICalculator로 데이터 전달 그리고 equal (=) 누르면 UICalculator가 ItemHistory를 생성하도록 만듬
// UICalculator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UICalculator : MonoBehaviour
{
public static UICalculator instance;
public Text Result;
public Text TotalResult;
public RectTransform HistoryParent;
public GameObject itemHistoryPref;
public float first;
public float second;
private float answer;
private bool isFirstFull;
private bool isSecondFull;
private bool isAnswerFull;
public string temp = "";
public string oper = "";
private void Awake()
{
if(instance == null)
{
instance = this;
}
else
{
Destroy(this);
}
}
// Start is called before the first frame update
void Start()
{
Result.text = "";
TotalResult.text = "";
}
// Update is called once per frame
void Update()
{
}
public void InputNumber(string str)
{
temp += str;
ChangeText();
}
public void InputOther(string str)
{
switch(str)
{
case "+":
case "-":
case "X":
case "/":
case "%":
if(isAnswerFull)
{
first = answer;
isFirstFull = true;
isSecondFull = false;
isAnswerFull = false;
}
else if(!isFirstFull)
{
first = float.Parse(temp);
temp = "";
isFirstFull = true;
}
else
{
second = float.Parse(temp);
temp = "";
}
oper = str;
break;
case ".":
temp += ".";
break;
case "+/-":
if (temp == "")
{
temp = "-";
}
else if (temp[0] == '-')
{
temp = temp.Substring(1);
}
else
{
temp = "-" + temp;
}
break;
case "<-":
case "CE":
if(temp.Length == 0) break;
temp = temp.Substring(0, temp.Length - 1);
break;
case "C":
first = 0.0f;
second = 0.0f;
answer = 0.0f;
TotalResult.text = "";
isFirstFull = false;
isSecondFull = false;
isAnswerFull = false;
break;
case "=":
OnClickEqual();
break;
}
ChangeText();
}
public void OnClickEqual()
{
second = float.Parse(temp);
temp = "";
isSecondFull = true;
switch (oper)
{
case "+":
answer = first + second;
break;
case "-":
answer = first - second;
break;
case "X":
answer = first * second;
break;
case "/":
if(second == 0)
{
answer = 0;
break;
}
answer = first / second;
break;
case "%":
answer = first % second;
break;
}
if(oper == "/" && second == 0)
{
TotalResult.text = "0으로 나눌 수 없습니다.";
}
else
{
TotalResult.text = answer.ToString("N4");
}
isAnswerFull = true;
GameObject newHistory = Instantiate(itemHistoryPref, HistoryParent);
newHistory.GetComponent<ItemHistory>().SetStrings(Result.text, TotalResult.text);
}
public void ChangeText()
{
if (!isFirstFull)
{
Result.text = temp;
}
else if (isSecondFull)
{
Result.text = first + " " + oper + " " + second;
}
else
{
Result.text = first + " " + oper + " " + temp;
}
}
}
// Buttons.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Buttons : MonoBehaviour
{
public Text text;
public void OnClickButton()
{
if (isNumber(text.text))
{
UICalculator.instance.InputNumber(text.text);
} else
{
UICalculator.instance.InputOther(text.text);
}
}
bool isNumber(string str)
{
if (str[0] - '0' >= 0 && str[0] - '0' < 10) return true;
else return false;
}
}
// ItemHistory.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ItemHistory : MonoBehaviour
{
public Text result;
public Text totalResult;
public void SetStrings(string result, string totalResult)
{
this.result.text = result;
this.totalResult.text = totalResult;
}
}
코드를 깔끔하게 리팩토링
어제 만든 코드는 진짜 작동만 하도록 만든 코드라서, MVP 패턴이 전혀 사용되지 않았음. 그래서 리팩토링을 하고자 함
코드 수정은 직접 해보고 싶어서, 챗지피티에게 리팩토링 힌트만 부탁했음.
🎯 목표: 기존의 UICalculator
를 MVP 패턴으로 분리하기
1. Model 분리 힌트
-
계산 로직(+, -, *, /, %)을 담당하는 순수 클래스 하나를 만들자.
-
이 클래스는
first
,second
,oper
를 받고, 결과를 반환하기만 해야 해. -
즉, 상태 저장보다는 입력 → 출력만 담당.
📌 힌트 키워드: CalculatorModel
, Calculate(first, second, operator)
함수 만들기
2. View 역할 분리 힌트
-
UICalculator
는 View가 될 거야. UI(Text, Button 등)와 직접 연결돼 있지? -
이제 이 클래스는 사용자 입력이나 버튼 처리를 Presenter에게 전달만 해야 해.
-
View는
InputField
,Text
,Button
만 알고 있어야 해.
📌 힌트 키워드:
-
ICalculatorView
인터페이스 만들기 -
DisplayResult(string)
같은 함수 포함시키기
3. Presenter 만들기 힌트
-
사용자 입력 처리는 이제 Presenter가 담당해야 해.
-
버튼에서 받은 입력을 Presenter가 받고,
-
숫자면 숫자 처리,
-
연산자면 연산 처리,
-
’=’ 이면 모델을 호출해서 계산,
-
결과를 View에 넘겨주는 역할을 해야 해.
-
📌 힌트 키워드:
CalculatorPresenter
,OnNumberInput(string)
,OnOperatorInput(string)
함수
4. Buttons.cs 수정 힌트
-
현재는
UICalculator
에 직접 접근하고 있는데, -
버튼은 Presenter에 직접 접근하게 바꾸는 게 좋아.
-
View에서 Presenter를 참조할 수 있도록 BindPresenter() 같은 메서드를 만들어두면 돼.
📌 힌트 키워드:
-
BindPresenter(CalculatorPresenter presenter)
-
Buttons
→UICalculator
→CalculatorPresenter
5. ItemHistory와 MVP 관계 힌트
-
ItemHistory
는 사실상 UI 전용이니까 View 영역에 속해. -
다만 히스토리 추가 시점을 Presenter에서 제어하도록 해보자.
-
즉, Presenter가 계산이 끝나면 View에게 “히스토리 하나 생성해줘”라고 명령하는 식.
📌 힌트 키워드:
- View 인터페이스에
AddHistory(string exp, string result)
추가
🧠 정리된 역할별 분리
역할 | 하는 일 |
---|---|
Model | 숫자 계산만. 내부 상태 없음. |
View (UICalculator) | 사용자 UI 표시. 버튼 연결. Presenter에 이벤트 위임. |
Presenter | 숫자 입력 처리, 연산 제어, 계산 요청, 결과 전달 |
Buttons | View를 통해 Presenter에게 전달 |
ItemHistory | View에 속함. Presenter 지시에 따라 UI에 생성됨 |
문자열에서 특정 글자 찾기
-
IndexOf : 문자열의 인덱스 번호 출력 (존재하지 않으면 -1)
-
LastIndexOf : 같은 문자열이 여러번 있을때 가장 마지막에 출력된 문자열의 인덱스 번호 출력 (존재하지 않으면 -1)
-
Contains : 해당 문자열이 존재하는지 확인 (true,false 반환)
그냥 싱글톤으로 엮으면 되는거 아냐..?
이 고민은 Buttons → UiCalculator 를 연결하는데에 있어서 시작된다… 아무리 생각해도 그냥 UICalculator를 싱글톤으로 만드는게 낫지 않나..? 그래서 우리의 챗지피티에게 물어본 결과…
🔥 정답부터 말하자면:
단기적으로 편하긴 하지만,
MVP 아키텍처 철학에는 어긋나고,
유지보수/확장성/테스트성 측면에서 추천되지 않아.
이유는 버튼이 뷰에 너무 의존하게 된다고 함. 그리고 MVP 패턴의 흐름이 깨져버림. presenter 중심의 코드가 view 중심으로 변경됨.
Csharp의 var와 C++의 auto?
뭔가 비슷해보여서, 챗지피티의 도움을 빌려서 정리해봄
비교 항목 | C# var | C++ auto |
---|---|---|
역할 | 타입 추론 | 타입 추론 |
foreach에서 사용 | O | O |
값 복사 vs 참조 | 값 타입이면 복사, 수정 불가 | auto = 복사, auto& = 참조 |
수정 가능 여부 | ❌ 불가능 (readonly) | ✅ auto& 사용하면 수정 가능 |
사용 위치 | 지역 변수만 | 거의 어디든 가능 |
몰랐는데, C#은 foreach var에서 대부분은 읽기 전용임. 만약에 반복하는 데이터가 클래스의 경우에는 내부 데이터를 변경할 수 있다고 함.
// 내부 데이터 변경 가능한 경우
public class Person
{
public string Name;
public int Age;
}
List<Person> people = new List<Person>
{
new Person { Name = "Alice", Age = 20 },
new Person { Name = "Bob", Age = 25 }
};
foreach (var p in people)
{
p.Age += 1; // ✅ 가능! 내부 값 수정
p = new Person(); // ❌ 불가능! p 변수 자체는 readonly
}
변수 자체인 p를 바꿀 수는 없지만, p 안에 있는 내부 값은 바꿀 수 있다!
MVP 패턴으로 변경해본 결과
// CalculatorPresenter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CalculatorPresenter
{
public CalculatorPresenter(ICalculatorView view, CalculatorModel model)
{
this.view = view;
this.model = model;
view.BindPresenter(this);
}
private CalculatorModel model;
private ICalculatorView view;
public float first;
public float second;
private float answer;
private bool isFirstFull;
private bool isSecondFull;
private bool isAnswerFull;
public string temp = "";
public string oper = "";
public void Debuging(string str)
{
Debug.Log(str);
}
public void OnNumberInput(string str)
{
temp += str;
Debuging("Temp : "+temp);
ChangeText();
}
public void OnOperatorInput(string str)
{
switch (str)
{
case "+":
case "-":
case "X":
case "/":
case "%":
if (isAnswerFull)
{
first = answer;
isFirstFull = true;
isSecondFull = false;
isAnswerFull = false;
}
else if (!isFirstFull)
{
first = float.Parse(temp);
temp = "";
isFirstFull = true;
}
oper = str;
break;
case ".":
int idx = temp.IndexOf('.');
if(idx < 0)
{
temp += ".";
}
break;
case "+/-":
if (temp == "")
{
temp = "-";
}
else if (temp[0] == '-')
{
temp = temp.Substring(1);
}
else
{
temp = "-" + temp;
}
break;
case "<-":
case "CE":
if (temp.Length == 0) break;
temp = temp.Substring(0, temp.Length - 1);
break;
case "C":
first = 0.0f;
second = 0.0f;
answer = 0.0f;
view.DisplayTotalResult("");
isFirstFull = false;
isSecondFull = false;
isAnswerFull = false;
break;
case "=":
OnClickEqual();
break;
}
ChangeText();
}
public void OnClickEqual()
{
second = float.Parse(temp);
temp = "";
isSecondFull = true;
if (oper == "/" && second == 0)
{
view.DisplayTotalResult("0으로 나눌 수 없습니다.");
}
else
{
answer = model.Calculate(first, second, oper);
view.DisplayTotalResult(answer.ToString("N4"));
}
isAnswerFull = true;
view.AddHistory();
}
public void ChangeText()
{
if (!isFirstFull)
{
view.DisplayResult(temp);
}
else if (isSecondFull)
{
view.DisplayResult(first + " " + oper + " " + second);
}
else
{
view.DisplayResult(first + " " + oper + " " + temp);
}
}
}
// ICalculatorView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ICalculatorView
{
public void DisplayResult(string str);
public void DisplayTotalResult(string str);
public void AddHistory();
public void BindPresenter(CalculatorPresenter presenter);
}
// UICalculator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UICalculator : MonoBehaviour, ICalculatorView
{
public Text Result;
public Text TotalResult;
public RectTransform HistoryParent;
public GameObject itemHistoryPref;
public Buttons[] allButtons;
private CalculatorPresenter presenter;
// Start is called before the first frame update
void Start()
{
Result.text = "";
TotalResult.text = "";
presenter = new CalculatorPresenter(this, new CalculatorModel());
foreach(var btn in allButtons)
{
btn.BindPresenter(presenter);
}
}
public void DisplayResult(string str)
{
Result.text = str;
}
public void DisplayTotalResult(string str)
{
TotalResult.text = str;
}
public void AddHistory()
{
GameObject newHistory = Instantiate(itemHistoryPref, HistoryParent);
newHistory.GetComponent<ItemHistory>().SetStrings(Result.text, TotalResult.text);
}
public void BindPresenter(CalculatorPresenter presenter)
{
this.presenter = presenter;
}
}
// CalculatorModel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CalculatorModel
{
public float Calculate(float first, float second, string oper)
{
float result = 0.0f;
switch (oper)
{
case "+":
result = first + second; break;
case "-":
result = first - second; break;
case "X":
result = first * second; break;
case "%":
result = first % second; break;
case "/":
result = first / second; break;
}
return result;
}
}
// Buttons.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Buttons : MonoBehaviour
{
public Text text;
private CalculatorPresenter presenter;
public void OnClickButton()
{
if (isNumber(text.text))
{
presenter.OnNumberInput(text.text);
} else
{
presenter.OnOperatorInput(text.text);
}
}
public void BindPresenter(CalculatorPresenter presenter)
{
this.presenter = presenter;
}
bool isNumber(string str)
{
if (str[0] - '0' >= 0 && str[0] - '0' < 10) return true;
else return false;
}
}
학습하며 겪었던 문제점 & 에러
문제 1
- 문제&에러에 대한 정의
같은 조 분이 타입캐스팅이 안됐음
- 내가 한 시도
const를 붙여줬음
- 해결 방법
함수 안에서 변경하신게 아니고 클래스 안에서 자꾸 선언하시고 바꾸려고 하시니까 안되신거였음.
- 새롭게 알게 된 점
깜빡했음. 함수 밖에서는 변수 써다가 뭐 할 수가 없음
- 이 문제&에러를 다시 만나게 되었다면?
데이터는 함수 안에서 바꾸자.
문제 2
- 문제&에러에 대한 정의
Presenter 가 new로 생성되지 않았음.
- 내가 한 시도
검색을 함..
- 해결 방법
모노비헤이비어(MonoBehaviour) 형태는 new 로 만들면 안되고, AddComponent로 만들어줘야 함. 그런데 Presenter는 UI에 붙이는 용도가 아니기 때문에, 일반 C# 클래스로 만들어주면 됨.
- 새롭게 알게 된 점
Presenter는 일반 C# 클래스로 만들자.
- 이 문제&에러를 다시 만나게 되었다면?
게임 내부 UI에 붙이는 클래스가 아니라면 MonoBehaviour를 뗀다.
문제 3
- 문제&에러에 대한 정의
Presenter를 일반 C# 스크립트로 만들다보니까 디버깅이 너무 어려웠음
- 내가 한 시도
검색을 함.. 22
- 해결 방법
Debug.Log 출력 자체는 여전히 가능함.
- 새롭게 알게 된 점
UI에 붙는 MonoBehaviour가 아니더라도 디버그 로그는 쓸 수 있다! 또한, View를 활용해서 인게임에 보이게 할 수도 있다.
- 이 문제&에러를 다시 만나게 되었다면?
Debug.Log()를 적극 활용하자.
내일 학습 할 것은 무엇인지
이제 구조를 MVP 패턴으로 바꾸었으니, 이 외에 로직 부분을 더 깔끔하게 수정해보고자 함