<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>LOONACIA</title>
    <link>https://loonacia.tistory.com/</link>
    <description>developer</description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 21:15:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>LOONACIA</managingEditor>
    <item>
      <title>[Unity] 빌드에서 로그 확인하기</title>
      <link>https://loonacia.tistory.com/17</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;게임을 개발하다 보면 각종 기상천외한 오류들을 마주할 때가 종종 있기 마련이다. 대부분은 에디터에서 수정을 마친 후 빌드를 하겠지만, 그렇게 했음에도 실제 빌드로 테스트를 할 때는 또 다른 버그를 마주하곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 빌드 후 테스트를 하다 버그를 발견할 때면 예외 메시지를 확인해 보고 싶다는 생각이 들 때가 많았다. 예외 메시지를 살펴보는 것만으로도 쉽게 해결의 실마리를 잡을 수 있는 경우가 대부분이기 때문이다. &lt;s&gt;물론 로그에도 찍히지 않는 버그라면 그 여파는...&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티 에디터에서는 콘솔(단축키 Ctrl + Shift + C)을 통해 개발자가 찍는 로그나 각종 예외 메시지를 확인할 수 있는데, 이를 빌드 테스트에서도 활용할 수 있다면 좋을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 찾은 간단한 방법은, 유니티에서 제공하는 로그 이벤트를 활용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Application.logMessageReceived&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.unity3d.com/ScriptReference/Application-logMessageReceived.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application 클래스의 logMessageReceived 이벤트는 해당 클래스에 로그 메시지가 수신될 때 발생한다. 간단히 생각해서, 예외가 발생하거나 Debug.Log 등으로 로깅을 할 때 발생한다고 봐도 무방하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704294206716&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void OnLogMessageReceived(string logString, string stackTrace, LogType type)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 logMessageReceived 이벤트 핸들러의 시그니처이다. 첫 번째 인자에는 로그 메시지가, 두 번째 인자에는 스택 트레이스 정보가, 세 번째 인자에는 해당 로그의 유형(&lt;a href=&quot;https://docs.unity3d.com/ScriptReference/LogType.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#참고&lt;/a&gt;)이 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 활용하면 로그를 파일로 저장하거나, 인게임 화면에 출력해 실시간으로 확인하는 등 테스트에 도움을 받을 수 있다. 예를 들어 로그를 파일로 저장하고 싶다면, 이벤트 핸들러를 다음과 같이 구현해 볼 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704294688684&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private string _logFilePath;

private static void InitializeLogPath()
{
    // 에디터에서는 Assets 폴더를, 빌드에서는 Data 폴더를 저장 경로로 지정
    string directory = Path.Join(Application.dataPath, &quot;Logs&quot;);
    if (!Directory.Exists(directory))
    {
        Directory.CreateDirectory(directory);
    }

    _logFilePath = Path.Join(directory, string.Format(&quot;log-{0}.txt&quot;, DateTime.Now.ToString(&quot;yyMMdd_HHmmss&quot;)));
}

private void OnLogMessageReceived(string logString, string stacktrace, LogType type)
{
    string line = $&quot;[{type}] {DateTime.Now:yyyy-MM-dd hh:mm:ss} {logString} {stacktrace}\n&quot;;
    File.AppendAllText(_logFilePath, line);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 이용해 GameManager 등 싱글톤 클래스에서 수명에 맞게 적절한 이벤트 구독/해제 과정을 거친다면 빌드에서도 쉽게 로그를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 들어서 빌드 테스트의 중요성을 뼈저리게 느끼고 있다. 에디터에서 확인하고 &quot;됐다 끝!&quot;이 아니라, 꼭 빌드 후 빌드 파일로 플레이 테스트를 하자.&lt;/p&gt;</description>
      <category>게임 개발/유니티 엔진</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/17</guid>
      <comments>https://loonacia.tistory.com/17#entry17comment</comments>
      <pubDate>Thu, 4 Jan 2024 00:17:32 +0900</pubDate>
    </item>
    <item>
      <title>윈도우11에서 내부 오류로 로그인이 안 되는 경우</title>
      <link>https://loonacia.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사용하던 서피스 북 3를 초기화하는 과정에서 '&lt;b&gt;내부 오류입니다. 다시 시도해 보세요.&lt;/b&gt;'라는 메시지와 함께 Microsoft 계정 로그인이 안 되는 상황을 마주했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Microsoft 계정으로 로그인 하지 않고 로컬 계정으로 설정해도 딱히 문제는 없지만, 동기화라든지 여러 면에서 내키지 않았다. 무엇보다 오류가 났는데 그냥 넘긴다는 것부터가 꺼림칙해서 해결하고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 해봤지만 성의없는 답변이 대부분이었고, 그나마 몇 안 되는 해결책은 내게는 도움이 되지 않았다. 상당한 시간을 들여 초기화도 해봤지만 소용없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 집에 와서 우연히 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 마주한 케이스에서는 카페에서 모바일 핫스팟으로 연결한 게 문제였다. 집에 와서 와이파이에 연결한 후 재시도했더니 정상적으로 로그인이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 허탈하다...&lt;/p&gt;</description>
      <category>윈도우</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/16</guid>
      <comments>https://loonacia.tistory.com/16#entry16comment</comments>
      <pubDate>Fri, 28 Jul 2023 23:37:37 +0900</pubDate>
    </item>
    <item>
      <title>[C#/기본기] 참조 타입을 매개변수에 전달할 때는 Pass by reference일까?</title>
      <link>https://loonacia.tistory.com/15</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;c#을 사용하다 보면, 메서드에 참조 형식(class, interface, delegate 등...)을 매개변수에 전달해야 할 일이 빈번하게 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;값 형식: Pass by value&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int, byte, 구조체 등의 값 형식 변수를 매개변수에 전달할 때는 Pass by value로 동작하는 것은 웬만하면 모두 인지하고 있을 것이다. 즉 메서드에 변수를 전달할 때 값 복사가 일어나게 되며, 메서드 내에서 변수의 값을 수정하더라도 원본에는 반영되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1689216754109&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class Program
{
	static void Main(string[] args)
	{
		int x = 5;
		Add(x, 3);
		System.Console.WriteLine($&quot;x={x}&quot;);
	}

	static void Add(int x, int y)
	{
		x = x + y;
	}
}
// output: x=5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짐작하다시피, 위 코드의 출력값은 x=5이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Add 메서드 내에서 이루어진 값 변경이 원본 변수에 반영되게 하려면 ref 등의 키워드로 참조에 의한 전달(Pass by reference)로 동작하게 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1689219766836&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class Program
{
	static void Main(string[] args)
	{
		int x = 5;
		Add(ref x, 3);
		System.Console.WriteLine($&quot;x={x}&quot;);
	}

	static void Add(ref int x, int y)
	{
		x = x + y;
	}
}
// output: x=8&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 기대한 대로 x에 저장된 값이 수정되고, x=8이 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 값 형식을 매개변수로 넘기면 메서드가 호출될 때 값 복사가 일어나기 때문에, 변수의 크기가 큰 경우 오버헤드가 발생할 수 있다. 이런 경우에도&amp;nbsp;Pass by reference로 동작하게 하는 것을 고려해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론으로 돌아와서...&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 매개변수 형식이 값 형식이 아닌 클래스 같은 참조 형식이라고 생각해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1689220787248&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class Program
{
	static void Main(string[] args)
	{
		Person person = new Person();
		person.Name = &quot;철수&quot;;
		ChangeName(person, &quot;영희&quot;);
		System.Console.WriteLine(person.Name);
	}

	static void ChangeName(Person person, string name)
	{
		person.Name = name;
	}
}

public class Person
{
	public string Name { get; set; }
}
// output: 영희&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 출력값은 영희다. 참조 형식인 Person 클래스를 매개변수로 넣었으므로, 객체의 프로퍼티인 Name을 ChangeName 메서드에서 수정하면 원본 객체에도 반영되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 참조 형식은 Pass by reference(Call by reference)인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부러 앞에서 언급하지 않고 넘어갔는데, c#에는 class 외에도 굉장히 자주 쓰는 참조 형식이 또 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 string이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;c#에서 string은 불변 '객체' 어쩌고... gc 압력을 최소화하기 위해 StringBuilder가 저쩌고... 하는 걸 자주 봤을 것이다. 당연히 string이 참조 형식이고, 힙에 올라가기 때문에 이런 말들이 나오는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 c#에서 참조 형식이 Pass by reference로 동작한다면, 참조 형식인 string 역시 메서드 안에서 원본 객체에 접근할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1689220593336&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class Program
{
	static void Main(string[] args)
	{
		string message = &quot;Hello&quot;;
		Add(message, &quot;, World!&quot;);
		System.Console.WriteLine(message);
	}

	static void Add(string str1, string str2)
	{
		str1 = str1 + str2;
	}
}
// output: Hello&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보고 나서 Hello, World가 출력되기를 기대하지는 않았기를...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 출력값은 당연히 Hello다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그럴까? 당연히 결론은 간단명료하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참조 형식도 Pass by value다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 참조 형식에 대한 변경이라고 해봐야 대부분 클래스에 접근해 멤버 변수를 조작하는 선에서 그치고, 새로 객체를 할당하는 경우가 아닌 이상 이런 문제를 마주할 일이 적다 보니 간과하기 쉽지만, 값 형식이든 참조 형식이든 기본적으로는 Pass by value, 정확히는 Pass a reference type by value로 동작한다. 즉, 전달 과정에서 값이 복사된다. 다만 값 형식은 변수에 저장된 값이 복사되고, 참조 형식은 변수가 참조하고 있는 주소가 복사되는 점이 다를 뿐이다. 아니, 참조 형식 변수에 저장된 값이 결국 참조하고 있는 객체의 주소값이므로, 사실 어떻게 보면 둘다 동일한 동작을 하는 셈으로 볼 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값 형식 - 변수에 저장된 값의 복사본을 전달&lt;/li&gt;
&lt;li&gt;참조 형식 - 변수가 참조하는 메모리 주소의 복사본을 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제, 위에서 Person 클래스를 조작하는 코드를 조금만 수정해서 다시 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1689221987840&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class Program
{
	static void Main(string[] args)
	{
		Person person = new Person();
		person.Name = &quot;철수&quot;;
		ChangeName(person, &quot;영희&quot;);
		System.Console.WriteLine(person.Name);
	}

	static void ChangeName(Person person, string name)
	{
		person = new Person();
		person.Name = name;
	}
}

public class Person
{
	public string Name { get; set; }
}
// output: 철수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;달라진 부분은 ChangeName 메서드에서 person 변수에 새 객체를 할당해준 것밖에 없다. 그러나 이번에는, 영희가 아니라 철수가 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main 메서드에서의 person 변수와, ChangeName 메서드에서의 person 변수는 이름만 같을 뿐, 같은 메모리 주소를 참조하는 다른 변수다. 그러므로 ChangeName에서 person에 새 객체를 할당해 변수가 참조하는 주소값을 수정하는 순간 서로 다른 주소를 참조하게 되고, 이후 멤버 변수를 수정하든 말든 서로 무관해지게 된다. 다른 객체니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 클래스 등의 참조 형식도, Pass by reference(Call by reference)로 동작하게 하려면 ref 등의 키워드를 붙여야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 보면 간단한 건데, 은근히 헷갈리기 쉬운 부분이다. 참조 형식, 참조에 의한 전달. 둘다 참조가 들어가 있어서 그런 건지...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 헷갈리는 걸 넘어&amp;nbsp; c#에서 클래스가 Call by reference로 동작한다고 당연하다는 듯 설명하는 블로그도 종종 보이기에 안타까워 다뤄본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;c#에서도 매개변수에 인수를 전달할 땐, 기본적으로 Pass by value다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>프로그래밍 언어/C#</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/15</guid>
      <comments>https://loonacia.tistory.com/15#entry15comment</comments>
      <pubDate>Thu, 13 Jul 2023 19:08:07 +0900</pubDate>
    </item>
    <item>
      <title>[C#] 프로그래머스 131705 - 삼총사</title>
      <link>https://loonacia.tistory.com/14</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/131705&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/131705&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국중학교에 다니는 학생들은 각자 정수 번호를 갖고 있습니다. 이 학교 학생 3명의 정수 번호를 더했을 때 0이 되면 3명의 학생은 삼총사라고 합니다. 예를 들어, 5명의 학생이 있고, 각각의 정수 번호가 순서대로 -2, 3, 0, 2, -5일 때, 첫 번째, 세 번째, 네 번째 학생의 정수 번호를 더하면 0이므로 세 학생은 삼총사입니다. 또한, 두 번째, 네 번째, 다섯 번째 학생의 정수 번호를 더해도 0이므로 세 학생도 삼총사입니다. 따라서 이 경우 한국중학교에서는 두 가지 방법으로 삼총사를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국중학교 학생들의 번호를 나타내는 정수 배열 &lt;code&gt;number&lt;/code&gt;가 매개변수로 주어질 때, 학생들 중 삼총사를 만들 수 있는 방법의 수를 return 하도록 solution 함수를 완성하세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제한 사항&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;3 &amp;le; &lt;code&gt;number&lt;/code&gt;의 길이 &amp;le; 13&lt;/li&gt;
&lt;li&gt;-1,000 &amp;le; &lt;code&gt;number&lt;/code&gt;의 각 원소 &amp;le; 1,000&lt;/li&gt;
&lt;li&gt;서로 다른 학생의 정수 번호가 같을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번호 쌍은 순서와 무관하게 요소가 같다면 동일한 쌍이다. 즉 &lt;code&gt;[1, 2, 3]&lt;/code&gt;과 &lt;code&gt;[3, 2, 1]&lt;/code&gt;은 동일한 쌍이다.&lt;br /&gt;&lt;br /&gt;순서에 따라 평가값이 달라지는 것이 아니기 때문에, 단순 순회로 각 쌍을 평가하고 0이라면 return할 값을 증가하는 것으로 구현하면 된다.&lt;br /&gt;&lt;br /&gt;동일한 쌍을 중복 순회하지 않도록 for문의 초기식과 조건식에 주목하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소스 코드&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public int solution(int[] number)
{
    int length = number.Length;
    if (length &amp;lt; 3 || length &amp;gt; 13)
    {
        return 0;
    }

    int answer = 0;
    for (int i = 0; i &amp;lt; length - 2; i++)
    {
        for (int j = i + 1; j &amp;lt; length - 1; j++)
        {
            int sum = number[i] + number[j];
            for (int k = j + 1; k &amp;lt; length; k++)
            {
                if (sum + number[k] == 0)
                {
                    answer += 1;
                }
            }
        }
    }

    return answer;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘/문제 풀이</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/14</guid>
      <comments>https://loonacia.tistory.com/14#entry14comment</comments>
      <pubDate>Mon, 17 Apr 2023 01:36:36 +0900</pubDate>
    </item>
    <item>
      <title>[C#] GuguClass 한 줄로 구현하기</title>
      <link>https://loonacia.tistory.com/13</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우연히 GuguClass를 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 드라마가 나왔을 당시에 친구가 페이스북에서 이 영상에 태그를 해줘서 본 기억은 나는데, 그때는 대충 보고 넘겼다가 이제야 GuguClass의 자세한 내막을 알게 된 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=UUJKiTcnGK0&quot;&gt;https://www.youtube.com/watch?v=UUJKiTcnGK0&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=UUJKiTcnGK0&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/di01uY/hySeWiNutS/SpppFk5DBy78hTqm5SK5I1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=196_98_414_184&quot; data-video-width=&quot;480&quot; data-video-height=&quot;360&quot; data-video-origin-width=&quot;480&quot; data-video-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/UUJKiTcnGK0&quot; width=&quot;480&quot; height=&quot;360&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백문이 불여일견이라고 자세한 건 유튜브 영상을 보도록 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GuguClass의 실행 결과를 보자면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhAQ1r/btr8905643e/Z6lfbvifThSyLfH1WHe871/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhAQ1r/btr8905643e/Z6lfbvifThSyLfH1WHe871/img.png&quot; data-alt=&quot;GuguClass 실행 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhAQ1r/btr8905643e/Z6lfbvifThSyLfH1WHe871/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhAQ1r%2Fbtr8905643e%2FZ6lfbvifThSyLfH1WHe871%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;511&quot; height=&quot;368&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GuguClass 실행 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딱 봐도 간단해 보이는 프로그램이지만 몇 가지 유의사항이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;곱셈 결과는 총 2자로 출력되어야 한다. 만약 1자라면 2자가 되도록 앞에 Padding을 추가한다.&lt;/li&gt;
&lt;li&gt;각 단은 탭으로 분리하며, 총 4단씩 묶어 출력한다. 묶음과 묶음 사이에는 빈 줄이 추가되어야 한다.&lt;/li&gt;
&lt;li&gt;각 행의 끝에는 공백 문자가 삽입되지 말아야 한다.&lt;/li&gt;
&lt;li&gt;&lt;s&gt;열 줄로 짜지 말고 한 줄로 짜야 한다.&lt;/s&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재밌어 보여서 C#으로 한 줄로 구현하려면 어떻게 할 수 있을까 싶어 구현해 봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1681116222683&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Enumerable.Range(2, 8).
		   Chunk(4).
		   SelectMany(chunk =&amp;gt; Enumerable.Range(1, 9).Select(multiple =&amp;gt; string.Join('\t', chunk.Select(num =&amp;gt; $&quot;{num} X {multiple} = {num * multiple,2}&quot;))).Append(string.Empty)).
		   Prepend(&quot;&amp;lt; gugu&amp;gt;\n&quot;).
		   ToList().
		   ForEach(Console.WriteLine);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 문법이 아닌 LINQ를 포함해 여러 API를 활용하는 거지만 뭐 어때. 한 줄이면 됐지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;근데 이렇게 한 줄로 짜면 정말 열 줄로 짜는 것보다 가벼워지는 거 맞나...?&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘/문제 풀이</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/13</guid>
      <comments>https://loonacia.tistory.com/13#entry13comment</comments>
      <pubDate>Mon, 10 Apr 2023 17:47:01 +0900</pubDate>
    </item>
    <item>
      <title>[WinUI 3/C#] 앱 지역화</title>
      <link>https://loonacia.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서: &lt;a title=&quot;공식 문서&quot; href=&quot;https://learn.microsoft.com/en-us/windows/apps/winui/winui3/localize-winui3-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://learn.microsoft.com/en-us/windows/apps/winui/winui3/localize-winui3-app&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689211437449&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Localize your WinUI 3 app - Windows apps&quot; data-og-description=&quot;This guide shows you how to localize your WinUI 3 application&quot; data-og-host=&quot;learn.microsoft.com&quot; data-og-source-url=&quot;https://learn.microsoft.com/en-us/windows/apps/winui/winui3/localize-winui3-app&quot; data-og-url=&quot;https://learn.microsoft.com/en-us/windows/apps/winui/winui3/localize-winui3-app&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/f94JG/hyTjGeFffu/NvDi3WfUUbIVV4K0TH22zK/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/apps/winui/winui3/localize-winui3-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.microsoft.com/en-us/windows/apps/winui/winui3/localize-winui3-app&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/f94JG/hyTjGeFffu/NvDi3WfUUbIVV4K0TH22zK/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Localize your WinUI 3 app - Windows apps&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This guide shows you how to localize your WinUI 3 application&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.microsoft.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UWP와 그 후신인 WinUI 3(이하 이 둘을 묶어 불러야 할 일이 있을 때는 WinUI로 칭하도록 하겠다.)에서는 간단한 방법으로 애플리케이션을 지역화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 지역화를 적용하는 방법은 크게 애플리케이션과 지역화 대상이 동일한 어셈블리인지, 서로 다른 어셈블리인지에 따라 조금 다를 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 동일한 어셈블리일 때&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션과 지역화할 대상이 동일한 어셈블리 내에 있다면 사용법은 정말로 간단한데, 지역화할 프로젝트에서 아래 순서대로 진행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;대상 프로젝트에 폴더를 생성한다. (폴더명은 어떤 것으로 하든 무관하지만, 일반적으로는 Strings로 명명한다.)&lt;/li&gt;
&lt;li&gt;새로 만든 폴더 안에 지역화할 언어의 ISO 언어 코드명으로 폴더를 생성한다.(ex. ko-KR, en-US)&lt;/li&gt;
&lt;li&gt;각 언어별 폴더 안에 리소스 파일(.resw)을 추가하고, Resources로 명명한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 프로세스대로 지역화를 지원하는 WinUI 3 앱을 만들어 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지된 WinUI 3 앱 프로젝트를 생성한 후, MainWindow를 아래와 같이 구성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1675087745545&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Window
    x:Class=&quot;WinUISample.MainWindow&quot;
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:local=&quot;using:WinUISample&quot;
    xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
    xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
    mc:Ignorable=&quot;d&quot;&amp;gt;

	&amp;lt;Grid&amp;gt;
		&amp;lt;TextBlock x:Uid=&quot;SampleText&quot;
				   VerticalAlignment=&quot;Center&quot;
				   HorizontalAlignment=&quot;Center&quot;/&amp;gt;
	&amp;lt;/Grid&amp;gt;
&amp;lt;/Window&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Uid는 리소스 파일과 컨트롤을 연결하기 위해 사용된다. WinUI 앱 실행 시 리소스 파일을 로드한 후, Uid 문자열로 시작하는 각 Property에 대한 리소스 키(MSDN에서는 Property Identifier, 즉 &lt;b&gt;속성 식별자&lt;/b&gt;로 칭한다.)를 찾아 해당하는 Property를 모두 덮어쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 말이냐 하면, 리소스 파일에 SampleText.Text라는 리소스 키가 있고 그 값이 &quot;Sample Text&quot;라면, Uid가 SampleText인 컨트롤의 Text Property와 연결되어 리소스 파일의 SampleText.Text로 지정된 문자열이 할당되므로 최종적으로 Sample Text가 출력된다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 SampleButton.Content라는 리소스 키가 있고 그 값이 &quot;Sample Button&quot;이라면, Uid가 SampleButton인 컨트롤이 있을 때 그 Content Property에 &quot;Sample Button&quot;이라는 문자열이 할당되어 Button 컨트롤의 Content가 &quot;Sample Button&quot;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의해야 할 점은 &lt;b&gt;속성 식별자&lt;/b&gt;는 연결된 Xaml 컨트롤의 Property 이름과 정확하게 일치해야 한다는 것이다. 가령 바로 위 문단에서 속성 식별자를 &quot;SampleButton.Content&quot;에서 &quot;SampleButton.Text&quot;로 변경한다면, Button 컨트롤에는 Text Property가 없기 때문에 런타임 시 예외가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인을 위해 지역화를 위한 리소스 파일을 추가해 보자. 프로젝트 안에 Strings 폴더를 만든 후, 각 언어별 폴더를 추가하고 그 아래에 리소스 파일을 추가한다. 주의해야 할 점은 리소스 파일 이름은 기본적으로 &lt;b&gt;Resources.resw&lt;/b&gt;여야 하며, 파일 이름을 바꾸고자 한다면 지역화를 위해 별도로 추가 작업을 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTjaYx/btrXypJEHRk/8QRlNT7thwMj3mNOyouWuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTjaYx/btrXypJEHRk/8QRlNT7thwMj3mNOyouWuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTjaYx/btrXypJEHRk/8QRlNT7thwMj3mNOyouWuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTjaYx%2FbtrXypJEHRk%2F8QRlNT7thwMj3mNOyouWuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;439&quot; height=&quot;403&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;403&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pmKxN/btrXCZQdi5l/q5Kny1mHPLUlFL1ySWauK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pmKxN/btrXCZQdi5l/q5Kny1mHPLUlFL1ySWauK0/img.png&quot; data-alt=&quot;Strings/ko-KR/Resources.resw&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pmKxN/btrXCZQdi5l/q5Kny1mHPLUlFL1ySWauK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpmKxN%2FbtrXCZQdi5l%2Fq5Kny1mHPLUlFL1ySWauK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1015&quot; height=&quot;223&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Strings/ko-KR/Resources.resw&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lNETu/btrXEBOQmiE/P9AOMkamRJCPlyjJXd7ZPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lNETu/btrXEBOQmiE/P9AOMkamRJCPlyjJXd7ZPK/img.png&quot; data-alt=&quot;Strings/en-US/Resources.resw&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lNETu/btrXEBOQmiE/P9AOMkamRJCPlyjJXd7ZPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlNETu%2FbtrXEBOQmiE%2FP9AOMkamRJCPlyjJXd7ZPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1021&quot; height=&quot;217&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Strings/en-US/Resources.resw&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 위와 같이 적절한 속성 식별자를 키 값으로 하는 문자열을 언어별로 추가해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 빌드 후 실행해 보면, 사용하는 환경의 언어에 맞게 텍스트가 표시되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;571&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y4oil/btrXD8lIIOJ/4xbO4VbuEYjZEjjmxpKprK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y4oil/btrXD8lIIOJ/4xbO4VbuEYjZEjjmxpKprK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y4oil/btrXD8lIIOJ/4xbO4VbuEYjZEjjmxpKprK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy4oil%2FbtrXD8lIIOJ%2F4xbO4VbuEYjZEjjmxpKprK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;571&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;571&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Xaml이 아닌 Code Behind에서 문자열 리소스를 활용하고 싶다면, ResourceLoader를 활용해야 한다. 다음과 같이 확장 메서드를 정의하여 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675090510206&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Microsoft.Windows.ApplicationModel.Resources;

public static class ResourceExtension
{
	private static readonly ResourceLoader _resourceLoader = new();

	public static string GetLocalized(this string resourceKey) =&amp;gt; _resourceLoader.GetString(resourceKey);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675090599172&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public sealed partial class MainWindow : Window
{
	public MainWindow()
	{
		this.InitializeComponent();
		xSampleText.Text = &quot;SampleText/Text&quot;.GetLocalized();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Xaml이 아니라 코드에서 리소스를 참조하고자 할 경우,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;속성 식별자&lt;/b&gt;는 .이 아닌 /로 구분해야 한다는 점에 유의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드하면 Xaml에서 Uid를 통해 접근했을 때와 동일한 텍스트가 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 서로 다른 어셈블리일 때&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션과 지역화 대상이 서로 다른 어셈블리에 있다면 상황이 조금 복잡해질 수 있다. 가령 컨트롤이나 View 등 Presentation Logic과 Application Service Logic을 분리하고자 하는 경우라든지, 그 외 기능별로 어셈블리를 분리하고 싶은 경우, 라이브러리를 개발하는 경우 등 별개의 어셈블리에 개별로 지역화를 적용해야 할 상황이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는, &lt;b&gt;WinUI의 리소스 관리는 어셈블리 단위로 이루어진다&lt;/b&gt;는 것이다. 위에서 설명한 방법은 애플리케이션과 지역화 대상이 서로 다른 어셈블리에 있는 상황에서는 제대로 동작하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WinUI 앱 빌드 시 리소스 파일을 이용해 PRI 파일을 생성하고 이를 통해 리소스 맵이라고 하는 리소스 파일의 컬렉션을 추가하는데, 어셈블리가 다르다면 생성되는 PRI 파일도 다르고, 각 어셈블리의 리소스 역시 별도의 리소스 맵에 추가된다. 따라서 위에서 설명한 방법 중 Xaml에서 Uid를 이용하는 방법은 속성 식별자를 찾지 못해 아무런 텍스트도 표시되지 않으며, 코드를 이용하는 방법도 빈 문자열을 반환하거나 심지어 런타임 예외를 발생할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 코드에서 리소스에 접근할 때 사용하는 ResourceLoader 클래스의 기본 생성자는 시작 프로젝트의 PRI 파일(일반적으로 resources.pri 파일이다.)을 이용해 리소스 맵을 생성하는데, 시작 프로젝트에 아무런 리소스 파일도 없는 상황이라면 리소스 맵을 찾지 못해 예외를 발생하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 해결방법은 없냐? 하면 그건 아니지만, 조금 번거로운 과정을 거쳐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 리소스 활용 시나리오를 명확하게 잡아놓고 갈 필요가 있다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;리소스 파일이 반드시 대상과 별개의 어셈블리에 위치해야 하는가? (즉, 대상과 동일한 어셈블리에 존재해도 무방한가?)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 비하인드에서 리소스에 접근해야 할 일이 있는가?&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리소스 키는 대상 어셈블리 내에 위치한 리소스 파일에서 찾을 것인가? 외부 어셈블리의 리소스 파일에서 찾을 것인가?&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 리소스 파일이 반드시 대상과 별개의 어셈블리에 위치해야 하는가? (즉, 대상과 동일한 어셈블리에 존재해도 무방한가?)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WinUI의 리소스 관리는 어셈블리 단위로 이루어지므로, 리소스 파일이 대상과 동일한 어셈블리 내에 있어도 무방하다면 애플리케이션이 위치한 어셈블리에는 리소스 파일을 추가하지 않고, 지역화가 필요한 객체가 존재하는 어셈블리 내에 리소스 파일을 추가하는 것을 고려해 볼 수 있다. 만약 코드 비하인드에서 리소스에 접근해야 할 일이 없고 Uid를 통해서만 접근하는 상황이라면, 대상 어셈블리에 리소스 파일을 추가하는 것만으로 목표를 달성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 코드 비하인드에서 리소스에 접근해야 할 일이 있는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 비하인드에서 리소스에 접근하는 것을 고려해야 하는 상황이라면 대상 어셈블리에 리소스 파일을 추가하는 것에 더해서 ResourceLoader를 이용하는 확장 메서드를 조금 수정해 주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 ResourceLoader가 시작 프로젝트의 PRI 파일을 이용해 리소스 맵을 생성한다고 했다. ResourceLoader의 GetDefaultResourceFilePath 메서드를 호출하면 시작 프로젝트인 애플리케이션 패키지의 resources.pri 파일 경로를 반환하는 것을 확인할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 resources.pri 파일 대신에 대상 어셈블리에서 생성하는 pri 파일을 이용해 ResourceLoader를 생성한다면 대상 어셈블리 내에 존재하는 리소스 파일을 가지고 리소스 맵을 구성하지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 ResourceLoader 클래스에는 다른 생성자가 추가로 정의되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675099561471&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ResourceLoader(string fileName);
public ResourceLoader(string fileName, string resourceMap);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 생성자는 동일 어셈블리의 다른 리소스 파일을 참조하려고 하는 경우 사용하는 생성자인데, 지금은 별개 어셈블리이므로 넘어간다.(파라미터 이름을 보면 알겠지만 지금은 리소스 맵이 아닌 PRI 파일 이름을 받고 있는데, 버그거나 MSDN 문서가 잘못된 것으로 보인다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 두 번째 생성자를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 파라미터는 사용할 PRI 파일의 주소고, 두 번째 파라미터는 리소스 맵의 URI 주소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRI 파일 주소의 경우 참조되는 라이브러리의 PRI 파일 이름은 기본적으로 어셈블리 이름과 일치하기 때문에, Assembly 클래스의&lt;span&gt;&amp;nbsp;&lt;/span&gt;GetExecutingAssembly 메서드를 이용하면 쉽게 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 맵의 URI 주소는 Assembly 이름 + 리소스 파일 이름이므로, 역시 쉽게 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675097965466&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 대상 어셈블리 안에 위치
public static class ResourceExtension
{
	private static readonly ResourceLoader _resourceLoader;

	static ResourceExtension()
	{
		var dir = Path.GetDirectoryName(ResourceLoader.GetDefaultResourceFilePath());
		_resourceLoader = new(Path.Join(dir, $&quot;{Assembly.GetExecutingAssembly().GetName().Name}.pri&quot;), $&quot;{Assembly.GetExecutingAssembly().GetName().Name}/Resources&quot;);
	}

	public static string GetLocalized(this string resourceKey)
	{
		return _resourceLoader.GetString(resourceKey);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 생성자를 이용하는 것으로 하고, ResourceExtension을 위와 같이 수정한다. 빌드 후 실행해 보면 대상 어셈블리 내에 리소스 파일이 존재한다면 예외를 발생하지 않고 적절한 값을 찾아 반환하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 리소스 키는 대상 어셈블리 내에 위치한 리소스 파일에서만 찾을 것인가? 외부 어셈블리의 리소스 파일에서 찾을 것인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번과 2번에서 다룬 시나리오는 모두 대상 어셈블리 내에 위치한 리소스 파일에서만 찾는 것을 전제로 수행했다. 그러나 외부 어셈블리, 혹은 대상 어셈블리와 외부 어셈블리를 포함한 모든 어셈블리에서 리소스를 찾아야 하는 경우가 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모든 어셈블리의 리소스 파일을 대상으로 리소스 키를 찾아야 할 필요가 있다면, 해당 어셈블리들의 PRI 파일을 이용해 리소스 맵을 생성해 두고, 리소스 키 검색 요청이 들어올 때마다 리소스 맵들을 순회하며 적절한 값이 있는지 찾고, 적절한 값이 있다면 반환해 주는 식으로 구현하면 쉽게 구현할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675101043158&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static class ResourceExtension
{
	private static List&amp;lt;ResourceMap&amp;gt; _resourceMaps;

	static ResourceExtension()
	{
		ResourceManager resourceManager = new();
		var mainResourceMap = resourceManager.MainResourceMap;
        
		// 기본 리소스 맵 (resources.pri)
		var defaultResourceMap = mainResourceMap.GetSubtree(&quot;Resources&quot;);
        
		_resourceMaps = AppDomain.CurrentDomain.GetAssemblies().Where(assembly =&amp;gt;
		{
			try
			{
				mainResourceMap.GetSubtree($&quot;{assembly.GetName().Name}/Resources&quot;);
				return true;
			}
			catch
			{
				return false;
			}
		}).Select(assembly =&amp;gt; mainResourceMap.GetSubtree($&quot;{assembly.GetName().Name}/Resources&quot;)).Prepend(defaultResourceMap).ToList();
	}

	public static string GetLocalized(this string resourceKey)
	{
		return _resourceMaps.Select(map =&amp;gt; map.TryGetValue(resourceKey)?.ValueAsString).Where(val =&amp;gt; !string.IsNullOrEmpty(val)).FirstOrDefault() ?? string.Empty;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디어를 바탕으로 ResourceExtension을 수정한다. 먼저 리소스 맵에 직접 접근하기 위해 ResourceLoader 대신 ResourceMap으로 바꾸었고, static 생성자에서 모든 어셈블리를 가져와 Resource.resw 파일이 존재하는 어셈블리의 리소스 맵을 _resourceMaps 리스트에 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 GetLocalized 메서드가 호출될 때 리소스 맵을 순회하며 값이 있는지 찾고, 있다면 값을, 없다면 빈 문자열을 반환하도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 후 앱을 실행하면 정상적으로 동작하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 세부적으로 들어간다면 생각보다 다듬어야 할 구석이 많지만, 큰 틀은 대충 다룬 듯하니 일단은 되는 것에 의의를 두기로 하고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 보충할 만한 부분이 있다면 추가로 글을 써서 덧붙이는 것으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 WinUI 지역화하기 끝!&lt;/p&gt;</description>
      <category>UI 프로그래밍/WinUI 3</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/12</guid>
      <comments>https://loonacia.tistory.com/12#entry12comment</comments>
      <pubDate>Tue, 31 Jan 2023 03:03:19 +0900</pubDate>
    </item>
    <item>
      <title>ReadOnlySpan&amp;lt;char&amp;gt; Split하기</title>
      <link>https://loonacia.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;닷넷 7 기준 Span과 ReadOnlySpan에는 아직 Split이 없다. 닷넷 6에서 Span과 Memory에 온갖 기능이 추가되었고 그 외 내부 API도 차근차근 Span과 ReadOnlySpan으로 바뀌어가는 와중에, 꽤 자주 쓸 법한 Split이 추가되지 않은 것은 꽤나 의외이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Split을 하는 목적이 Parse나 문자열 확인 등이라면 string.Split을 쓰는 것보다는 ReadOnlySpan을 이용하는 것이 불필요한 힙 할당을 줄이는 것에 도움이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 Span&amp;lt;T&amp;gt;와 ReadOnlySpan&amp;lt;T&amp;gt;에는 이미 특정 값과 일치하는 첫 번째 인덱스를 반환하는 IndexOf&amp;lt;T&amp;gt;라는 확장 메서드가 구현되어 있어서, 마음먹고 구현하고자 한다면 간단한 수준의 Split 구현은 그리 어려운 일이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 게시물에서는 string 객체의 뷰인 ReadOnlySpan&amp;lt;char&amp;gt;를 이용해 문자열을 Split하는 기능을 ReadOnlySpan&amp;lt;char&amp;gt;의 확장 메서드 형태로 구현해 보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Split을 하게 된다면 Split된 문자열을 순회하면서 특정한 작업을 하는 것이 주 목적이 되므로, Enumerator를 구현해 foreach 문에서 반복할 수 있도록 구현해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 열거자에서 매번 string 객체를 생성해 반환할 거면 string의 Split 메서드를 쓰지 굳이 ReadOnlySpan을 쓸 이유가 없으므로, 가능하면 매번 string 객체를 생성해 제공하는 대신 Split된 문자열에 대한 뷰만 ReadOnlySpan&amp;lt;char&amp;gt; 타입으로 제공해 주는 쪽으로 구현해 보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Enumerator를 본격적으로 구현하기 전에, 먼저 ReadOnlySpan이 ref struct라는 것이 아주 약간의 걸림돌이 된다는 점을 짚고 넘어가야만 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 걸림돌이 되는데?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그걸 알기 위해서는 IEnumerable과 IEnumerator, 그리고 ref struct에 대해서 아주 간략하게나마 이해할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정한 컬렉션을 foreach 문 등을 통해 반복하도록 하기 위해서는 IEnumerable 또는 IEnumerable&amp;lt;T&amp;gt; 인터페이스를 구현함으로써 이루는 것이 일반적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 IEnumerable.cs의 소스 코드를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673770143042&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface IEnumerable
{
	IEnumerator GetEnumerator();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;보다시피 IEnumerable 인터페이스는 &lt;span&gt;단순히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;IEnumerator를 반환하는 GetEnumerator 메서드를&lt;/span&gt; 갖는 간단한 인터페이스이다. 이를 제너릭으로 재구현하는 IEnumerable&amp;lt;T&amp;gt; 인터페이스도, GetEnumerator 메서드의 반환값을 IEnumerator&amp;lt;T&amp;gt; 타입으로 재정의하는 부분 외에 특이사항이 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673770363194&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface IEnumerator
{
	object Current { get; }

	bool MoveNext();
	
	void Reset();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IEnumerator 역시 특별한 부분은 없다. IEnumerator&amp;lt;T&amp;gt;는 Current 프로퍼티의 타입이 T로 바뀌는 것뿐, 다른 것은 모두 IEnumerator와 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;foreach문을 통해 특정한 컬렉션의 열거자를 호출하면 내부적으로는 GetEnumerator 메서드를 호출해 IEnumerator 타입의 열거자를 가져오고, 열거자의 MoveNext 메서드를 호출해 반복 가능 여부를 확인하고, Current 프로퍼티의 getter를 통해 컬렉션 내부의 값을 가져오는 식으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 우리가 핸들링할 타입은 ReadOnlySpan&amp;lt;char&amp;gt;니까 IEnumerable&amp;lt;ReadOnlySpan&amp;lt;char&amp;gt;&amp;gt; 인터페이스를 구현하면 되는 거 아닌가? 싶지만 안타깝게도 그렇게 속편하게 구현되진 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 ReadOnlySpan의 유형이 ref struct라는데 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ref struct는 반드시 스택에만 존재할 수 있으며, 힙에 할당될 수 없다. 그래서 박싱의 여지가 있는 모든 동작이 차단되며, 일반적으로 클래스나 구조체, 또는 레코드의 필드일 수 없다. ref struct는 오직, 똑같이 스택에만 존재할 수 있는 ref struct에서만 필드로 선언될 수 있다. 또한 ref struct는 인터페이스를 구현할 수도 없고, 구조체이므로 특정 클래스를 상속할 수도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 ReadOnlySpan&amp;lt;char&amp;gt; 타입으로는 열거자 구현이 불가능한가...? 하면 그건 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 C#은 덕 타이핑을 지원하기 때문에, IEnumerable 인터페이스와 IEnumerator 인터페이스에서 정의하는 사양을 충족하기만 한다면 ref struct 타입도 열거자를 제공해 foreach 문을 돌리는 것이 가능하다. ref sturct 뿐만 아니라 클래스나 구조체, 레코드도 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 GetEnumerator 메서드가 존재하고, GetEnumerator 메서드를 통해 반환받는 객체 내부에 Current 프로퍼티, MoveNext 메서드가 존재한다면 굳이 &lt;span&gt;IEnumerable&lt;span&gt; 인터페이스를 구현하지 않아도 foreach 문을 돌릴 수 있는 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;그래서 Span이나 ReadOnlySpan을 이용해 열거자를 제공하는 닷넷의 기능들 (예를 들어 ReadOnlySpan&amp;lt;char&amp;gt;에 대 Regex라든지)은 전부 덕 타이핑된 열거자(주로 ref struct 자신이 된다)를 반환하는 식으로 구현되어 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;그럼 열거자는 덕 타이핑으로 구현하는 것으로 하고... Split된 문자열을 ReadOnlySpan&amp;lt;char&amp;gt; 타입으로 제공하는 것을 베이스로 정의 부분만 뽑아보면 다음과 같이 만들어 볼 수 있을 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673771510783&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ref struct SpanSplitEnumerator
{
	// 원본 문자열
	private ReadOnlySpan&amp;lt;char&amp;gt; _string;
        
	// 구분자
	private readonly ReadOnlySpan&amp;lt;char&amp;gt; _separator;

	public SpanSplitEnumerator(ReadOnlySpan&amp;lt;char&amp;gt; source, ReadOnlySpan&amp;lt;char&amp;gt; separator)
	{
		_string = source;
		_separator = separator;
	}

	// ReadOnlySpan&amp;lt;char&amp;gt; 타입을 Element로 제공
	public ReadOnlySpan&amp;lt;char&amp;gt; Current { get; private set; }

	// 자기 자신을 열거자로 제공
	public SpanSplitEnumerator GetEnumerator() =&amp;gt; this;

	public bool MoveNext()
	{
		// 열거가 가능한지 확인
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;foreach (var item in SapnSplitEnumerator) 같은 방법으로 열거자를 순회한다고 치면, GetEnumerator 메서드를 통해 SpanSplitEnumerator를 열거자로 제공받고, 그 내부의 Current와 MoveNext를 호출하는 식으로 반복문이 이루어질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 해야 할 것은 단순히 MoveNext를 통해 계속 열거가 가능한지 (즉 문자열의 끝에 도달하지 않았는지)를 제공하고, 그 때마다 Split된 부분에 해당하는 문자열의 뷰를 Current에 적절히 할당해 주기만 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말했듯이 Span 및 ReadOnlySpan에는 IndexOf 메서드가 구현되어 있기 때문에, 문자열 내에 구분자가 있는지 확인한 후 있다면 그 인덱스까지 잘라 Current에 넣어주고, 없다면 (즉 -1을 반환한다면) 남은 문자열을 싸그리 Current에 넣어준 후 다음에 호출될 때 false를 반환해 주도록 하면 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673771871067&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public bool MoveNext()
{
	if (_isEnd)
	{
		return false;
	}

	int index = _string.IndexOf(_separator);
	if (index &amp;gt;= 0)
	{
		Current = _string[..index];
		_string = _string[(index + _separator.Length)..];
	}
	else
	{
		Current = _string;
		_string = ReadOnlySpan&amp;lt;char&amp;gt;.Empty;
		_isEnd = true;
	}

	return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 열거자를 구현했으니 Split 메서드를 구현할 차례인데, 구분자가 한 개인 경우와 연속된 문자열인 경우 두 가지를 모두 고려해 만들어 보자. 간단하게, 원본 문자열과 구분자를 ReadOnlySpan 타입으로 받아 SpanSplitEnumerator 구조체를 생성한 후 리턴하기만 하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1673772065058&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static SpanSplitEnumerator Split(this ReadOnlySpan&amp;lt;char&amp;gt; src, char separator) =&amp;gt; new(src, new char[] { separator });

public static SpanSplitEnumerator Split(this ReadOnlySpan&amp;lt;char&amp;gt; src, ReadOnlySpan&amp;lt;char&amp;gt; separator) =&amp;gt; new(src, separator);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 foreach문을 돌려보면, 다음과 같은 결과를 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673772209821&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;string str = &quot;Hello, World!&quot;;
var span = str.AsSpan();

Console.WriteLine(&quot;========== string.Split ==========&quot;);
foreach (var split in str.Split(',').Select((s, index) =&amp;gt; (Chars: s, Index: index)))
{
	Console.WriteLine($&quot;{split.Chars}&quot;);
}

Console.WriteLine();
Console.WriteLine(&quot;========== ReadOnlySpan&amp;lt;char&amp;gt;.Split ==========&quot;);
foreach (var split in span.Split(','))
{
	Console.WriteLine($&quot;{split}&quot;);
}

/*
Output
========== string.Split ==========
Hello
 World!

========== ReadOnlySpan&amp;lt;char&amp;gt;.Split ==========
Hello
 World!
*/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다시피, string 타입의 Split 메서드와 동일한 기능을 하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 구현 끝!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...이어도 좋겠지만... &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이대로만 끝내기에는 뭔가 아쉬운 점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열을 Split 할때는 Split된 문자열 자체 뿐만 아니라, 그 문자열이 구분자를 기준으로 원본 문자열의 몇 번째에 있었느냐 하는 것도 필요할 수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 csv를 파싱하는데 첫 번째 셀은 int로 파싱하고, 두 번째 셀은 json 객체로 deserialize하고, 세 번째 셀은 그냥 문자열로 처리해야 한다든지 하는 경우에는 Split 후 Index를 함께 가져오는 것이 필요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 구현은 어렵지 않은데, Split된 문자열(ReadOnlySpan 타입)과 Index를 멤버로 갖는 구조체를 정의해 주고, MoveNext에서 이 구조체의 인스턴스를 생성해 Current에 할당해 주기만 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Split된 문자열과 Index를 노출하는 객체를 SpanSplitEnumerator 구조체 내부에 동일하게 ref struct 타입으로 정의한다.&lt;/p&gt;
&lt;pre id=&quot;code_1673772714538&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public readonly ref struct SpanSplitEntry
{
	internal SpanSplitEntry(ReadOnlySpan&amp;lt;char&amp;gt; chars, int index)
	{
		Chars = chars;
		Index = index;
	}

	public ReadOnlySpan&amp;lt;char&amp;gt; Chars { get; }

	public int Index { get; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Split된 문자열과 인덱스를 가져오는 게 주 목적이고 값을 수정할 일은 없으므로, 이를 명확히 하기 위해 readonly 타입 구조체로 정의해 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음으로는 앞서 만든 &lt;span&gt;SpanSplitEnumerator&lt;span&gt; 구조체의 Current 프로퍼티의 타입을 새로 만들어준 SpanSplitEntry로 변경해 주고, MoveNext를 수정해 주자.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673772926564&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ref struct SpanSplitEnumerator
{
	// 나머지는 다 동일!
	private int _index;
    
	public SpanSplitEnumerator(ReadOnlySpan&amp;lt;char&amp;gt; source, ReadOnlySpan&amp;lt;char&amp;gt; separator)
	{
		//...
		_index = 0;
	}

	public SpanSplitEntry Current { get; private set; }

	public bool MoveNext()
	{
		if (_isEnd)
		{
			return false;
		}

		int index = _string.IndexOf(_separator);
		if (index &amp;gt;= 0)
		{
			Current = new SpanSplitEntry(_string[..index], _index++);
			_string = _string[(index + _separator.Length)..];
		}
		else
		{
			Current = new SpanSplitEntry(_string, _index++);
			_string = ReadOnlySpan&amp;lt;char&amp;gt;.Empty;
			_isEnd = true;
		}

		return true;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 바뀐 부분은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Current를 변경하는 부분을 ReadOnlySpan&amp;lt;char&amp;gt; 타입의 뷰를 할당하는 것에서 SpanSplitEntry를 생성해 할당하는 것으로 바꾸었고, 시퀀스마다 index의 값을 하나씩 증가해 몇 번째 인덱스인지 알 수 있도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 테스트 코드를 살짝 수정해 돌려보면, 정상적으로 잘 동작하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673773149303&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;string str = &quot;Hello, World!&quot;;
var span = str.AsSpan();

Console.WriteLine(&quot;========== string.Split ==========&quot;);
foreach (var split in str.Split(',').Select((s, index) =&amp;gt; (Chars: s, Index: index)))
{
	Console.WriteLine($&quot;index {split.Index}: {split.Chars}&quot;);
}

Console.WriteLine();
Console.WriteLine(&quot;========== ReadOnlySpan&amp;lt;char&amp;gt;.Split ==========&quot;);
foreach (var split in span.Split(','))
{
	Console.WriteLine($&quot;index {split.Index}: {split.Chars}&quot;);
}

/*
Output
========== string.Split ==========
index 0: Hello
index 1:  World!

========== ReadOnlySpan&amp;lt;char&amp;gt;.Split ==========
index 0: Hello
index 1:  World!
*/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다시피 정상적으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에 구분자가 여러 개인 경우에도, IndexOf 대신 IndexOfAny를 사용하는 식으로 조금만 바꿔주면 된다. char 타입이 아닌 byte 타입인 경우 등에는 제네릭으로 구현하면 되지 않을까 생각한다. (아마도...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 ReadOnlySpan&amp;lt;char&amp;gt; 타입의 Split 구현 진짜 끝!&lt;/p&gt;</description>
      <category>프로그래밍 언어/C#</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/10</guid>
      <comments>https://loonacia.tistory.com/10#entry10comment</comments>
      <pubDate>Sun, 15 Jan 2023 18:01:35 +0900</pubDate>
    </item>
    <item>
      <title>[C#] Csv 셀 내부의 JSON string을 고려하는 정규표현식</title>
      <link>https://loonacia.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 csv 파일은 쉼표를 기준으로 split하면 간단하게 파싱할 수 있지만, 셀 내부 데이터에 쉼표가 포함되어 있을 때(특히 JSON string인 경우 등) 별다른 예외 처리없이 split하면 셀 안에 있는 데이터까지 잘라버리는 등 원하지 않는 동작을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 csv 파싱 라이브러리를 쓴다든지 정규표현식을 통해 셀 내부에 있는 쉼표는 무시하고 파싱하는 과정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 csv로 저장되는 JSON string은 따옴표로 쉽게 구분되므로, 별다른 라이브러리를 사용할 필요 없이 구현이 간단한 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673768098774&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;string[] csv = File.ReadAllLines(@&quot;.\csv1.csv&quot;);
Regex regex = new(&quot;,(?=(?:[^\&quot;]*\&quot;[^\&quot;]*\&quot;)*[^\&quot;]*$)&quot;);
foreach (var line in csv)
{
	foreach (var cell in regex.Split(line))
	{
		Console.WriteLine(cell);
	}
}

// Raw Data
box,&quot;{&quot;&quot;x&quot;&quot;:5,&quot;&quot;y&quot;&quot;:6, &quot;&quot;name&quot;&quot;:&quot;&quot;rect&quot;&quot;}&quot;,5

// Output
box
&quot;{&quot;&quot;x&quot;&quot;:5,&quot;&quot;y&quot;&quot;:6, &quot;&quot;name&quot;&quot;:&quot;&quot;rect&quot;&quot;}&quot;
5&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프로그래밍 언어/C#</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/9</guid>
      <comments>https://loonacia.tistory.com/9#entry9comment</comments>
      <pubDate>Sun, 15 Jan 2023 16:35:59 +0900</pubDate>
    </item>
    <item>
      <title>[C#] Span&amp;lt;T&amp;gt;을 이용한 배열 이어붙이기</title>
      <link>https://loonacia.tistory.com/8</link>
      <description>&lt;pre id=&quot;code_1673611933750&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal static class SpanExtension
{
	public static T[] Concat&amp;lt;T&amp;gt;(this ReadOnlySpan&amp;lt;T&amp;gt; span1, ReadOnlySpan&amp;lt;T&amp;gt; span2)
	{
		T[] ret = new T[span1.Length + span2.Length];

		Span&amp;lt;T&amp;gt; span = new(ret);
		span1.CopyTo(span);
		span2.CopyTo(span[span1.Length..]);

		return ret;
	}
}


// Program.cs
ReadOnlySpan&amp;lt;byte&amp;gt; source1 = &quot;Hello, &quot;u8;
ReadOnlySpan&amp;lt;byte&amp;gt; source2 = &quot;World!&quot;u8;

Console.WriteLine(System.Text.Encoding.UTF8.GetString(source1.Concat(source2)));
// output: Hello, World!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로그래밍 언어/C#</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/8</guid>
      <comments>https://loonacia.tistory.com/8#entry8comment</comments>
      <pubDate>Fri, 13 Jan 2023 21:22:35 +0900</pubDate>
    </item>
    <item>
      <title>[C++] 백준 9184 - 신나는 함수 실행</title>
      <link>https://loonacia.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://0041lntc.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://0041lntc.tistory.com/3&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1673277620118&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[C++] 백준 9184 - 신나는 함수 실행&quot; data-og-description=&quot;https://www.acmicpc.net/problem/9184 9184번: 신나는 함수 실행 입력은 세 정수 a, b, c로 이루어져 있으며, 한 줄에 하나씩 주어진다. 입력의 마지막은 -1 -1 -1로 나타내며, 세 정수가 모두 -1인 경우는 입력의&quot; data-og-host=&quot;0041lntc.tistory.com&quot; data-og-source-url=&quot;https://0041lntc.tistory.com/3&quot; data-og-url=&quot;https://0041lntc.tistory.com/3&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bC57sw/hyReHUmlHw/fJMjrLAjD1gOYctDSoiGc1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bmrTem/hyRdQk4OSq/v5kc0GFJ3LgMJKjGIdBKXK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/erGvyu/hyReVkLUXU/PGWHHT1fekQbpadkSCOtW1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://0041lntc.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://0041lntc.tistory.com/3&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bC57sw/hyReHUmlHw/fJMjrLAjD1gOYctDSoiGc1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bmrTem/hyRdQk4OSq/v5kc0GFJ3LgMJKjGIdBKXK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/erGvyu/hyReVkLUXU/PGWHHT1fekQbpadkSCOtW1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[C++] 백준 9184 - 신나는 함수 실행&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://www.acmicpc.net/problem/9184 9184번: 신나는 함수 실행 입력은 세 정수 a, b, c로 이루어져 있으며, 한 줄에 하나씩 주어진다. 입력의 마지막은 -1 -1 -1로 나타내며, 세 정수가 모두 -1인 경우는 입력의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;0041lntc.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세 설명은 이전 블로그에 작성한 아티클을 참고하기 바란다.&lt;/p&gt;
&lt;pre id=&quot;code_1673277610337&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int w(int a, int b, int c);

int dp[21][21][21];
int main()
{
    int a = 0;
    int b = 0;
    int c = 0;
    while (true)
    {
        std::cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
        if (a == -1 &amp;amp;&amp;amp; b == -1 &amp;amp;&amp;amp; c == -1)
            break;
        printf(&quot;w(%d, %d, %d) = %d\n&quot;, a, b, c, w(a, b, c));
    }

    return 0;
}

int w(int a, int b, int c)
{
    static int dp[21][21][21]{};
    if (a &amp;lt;= 0 || b &amp;lt;= 0 || c &amp;lt;= 0)
        return 1;

    else if (a &amp;gt; 20 || b &amp;gt; 20 || c &amp;gt; 20)
        return w(20, 20, 20);
    
    else if (dp[a][b][c] != 0)
        return dp[a][b][c];

    else if (a &amp;lt; b &amp;amp;&amp;amp; b &amp;lt; c)
        dp[a][b][c] = w(a, b, c - 1) + w(a, b - 1, c - 1) - w(a, b - 1, c);
        
    else
        dp[a][b][c] = w(a - 1, b, c) + w(a - 1, b - 1, c) + w(a - 1, b, c - 1) - w(a - 1, b - 1, c - 1);

    return dp[a][b][c];
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘/문제 풀이</category>
      <author>LOONACIA</author>
      <guid isPermaLink="true">https://loonacia.tistory.com/7</guid>
      <comments>https://loonacia.tistory.com/7#entry7comment</comments>
      <pubDate>Tue, 10 Jan 2023 00:20:23 +0900</pubDate>
    </item>
  </channel>
</rss>